mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 04:40:45 +08:00
Simplify oclaw product surface: drop niche specialists and admin noise.
Remove stock/image/video specialists, Gmail watcher, CocoLoop market, desktop packaging, and extra memory plugins; disable dynamic agents and hide Session Monitor / API grants / Admin Audit from nav. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
efa72df362
commit
cf31fd104d
290 changed files with 127 additions and 64576 deletions
|
|
@ -1,106 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from runtime.application.gateway import process_inbound_payload_usecase
|
||||
from svc.config.paths import db_path
|
||||
from svc.persistence.sqlite_store import SqliteStore
|
||||
from svc.persistence.assistant_store import get_assistant_store
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Case:
|
||||
case_id: str
|
||||
kind: str
|
||||
payload: dict[str, Any]
|
||||
assert_contains: list[str]
|
||||
assert_not_contains: list[str]
|
||||
|
||||
|
||||
def _load_cases(path: str) -> list[Case]:
|
||||
p = Path(path)
|
||||
if not p.exists():
|
||||
raise FileNotFoundError(path)
|
||||
out: list[Case] = []
|
||||
for idx, line in enumerate(p.read_text(encoding="utf-8").splitlines(), start=1):
|
||||
raw = line.strip()
|
||||
if not raw:
|
||||
continue
|
||||
row = json.loads(raw)
|
||||
cid = str(row.get("id") or f"line-{idx}")
|
||||
kind = str(row.get("kind") or "gateway")
|
||||
payload = row.get("payload") if isinstance(row.get("payload"), dict) else {}
|
||||
ac = row.get("assert_contains") or []
|
||||
anc = row.get("assert_not_contains") or []
|
||||
out.append(
|
||||
Case(
|
||||
case_id=cid,
|
||||
kind=kind,
|
||||
payload=payload,
|
||||
assert_contains=[str(x) for x in ac if str(x).strip()],
|
||||
assert_not_contains=[str(x) for x in anc if str(x).strip()],
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _extract_reply_text(resp: dict[str, Any]) -> str:
|
||||
try:
|
||||
replies = resp.get("replies")
|
||||
if isinstance(replies, list) and replies:
|
||||
first = replies[0]
|
||||
if isinstance(first, dict):
|
||||
return str(first.get("text") or "")
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
def run_gateway_eval(dataset_path: str) -> dict[str, Any]:
|
||||
store = get_assistant_store()
|
||||
# Seed a tenant + bind code for tests
|
||||
tenants = store.list_tenants(limit=1)
|
||||
if tenants:
|
||||
tenant_id = tenants[0]["id"]
|
||||
else:
|
||||
tenant_id = store.create_tenant("Eval")["id"]
|
||||
code = "EVALCODE"
|
||||
try:
|
||||
store.create_bind_code(tenant_id=tenant_id, role="member", code=code)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Binding creates the user; we will use external ids in payloads.
|
||||
cases = _load_cases(dataset_path)
|
||||
results = []
|
||||
passed = 0
|
||||
for c in cases:
|
||||
payload = dict(c.payload)
|
||||
# inject tenant/code shortcuts
|
||||
payload.setdefault("channel", "wecom")
|
||||
payload.setdefault("chat_id", "room_eval")
|
||||
payload.setdefault("user_id", "wxid_eval_u1")
|
||||
payload.setdefault("is_group", True)
|
||||
payload["text"] = str(payload.get("text") or "").replace("EVALCODE", code)
|
||||
resp = process_inbound_payload_usecase(payload)
|
||||
text = _extract_reply_text(resp)
|
||||
failures = []
|
||||
for must in c.assert_contains:
|
||||
if must not in text:
|
||||
failures.append(f"missing:{must}")
|
||||
for bad in c.assert_not_contains:
|
||||
if bad in text:
|
||||
failures.append(f"unexpected:{bad}")
|
||||
ok = not failures
|
||||
passed += 1 if ok else 0
|
||||
results.append({"id": c.case_id, "ok": ok, "text": text, "failures": failures})
|
||||
return {"total": len(results), "passed": passed, "pass_rate": (passed / len(results)) if results else 0.0, "results": results}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
rep = run_gateway_eval("data/eval/assistant_gateway.jsonl")
|
||||
print(json.dumps({k: v for k, v in rep.items() if k != "results"}, ensure_ascii=False, indent=2))
|
||||
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from runtime.agents.factory import build_gateway_executor
|
||||
from svc.persistence.sqlite_store import SqliteStore
|
||||
from svc.persistence.assistant_store import get_assistant_store
|
||||
from runtime.orchestration.evaluation import eval_summary
|
||||
from svc.config.paths import db_path
|
||||
from runtime.gateway import OclawGateway
|
||||
from runtime.types import StandardMessage
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EvalCase:
|
||||
case_id: str
|
||||
input_text: str
|
||||
assert_contains: list[str]
|
||||
assert_not_contains: list[str]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EvalCaseResult:
|
||||
case_id: str
|
||||
ok: bool
|
||||
latency_ms: int
|
||||
failures: list[str]
|
||||
|
||||
|
||||
def _load_dataset(dataset_path: str) -> list[EvalCase]:
|
||||
ds = Path(dataset_path)
|
||||
if not ds.exists():
|
||||
raise FileNotFoundError(dataset_path)
|
||||
cases: list[EvalCase] = []
|
||||
with ds.open("r", encoding="utf-8") as f:
|
||||
for idx, line in enumerate(f, start=1):
|
||||
raw = line.strip()
|
||||
if not raw:
|
||||
continue
|
||||
row = json.loads(raw)
|
||||
input_text = str(row.get("input") or "").strip()
|
||||
if not input_text:
|
||||
continue
|
||||
case_id = str(row.get("id") or row.get("case_id") or f"line-{idx}").strip()
|
||||
ac = row.get("assert_contains") or []
|
||||
anc = row.get("assert_not_contains") or []
|
||||
assert_contains = [str(x) for x in ac if str(x).strip()]
|
||||
assert_not_contains = [str(x) for x in anc if str(x).strip()]
|
||||
cases.append(
|
||||
EvalCase(
|
||||
case_id=case_id,
|
||||
input_text=input_text,
|
||||
assert_contains=assert_contains,
|
||||
assert_not_contains=assert_not_contains,
|
||||
)
|
||||
)
|
||||
return cases
|
||||
|
||||
|
||||
def run_eval(
|
||||
dataset_path: str,
|
||||
*,
|
||||
report_path: str | None = None,
|
||||
limit: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Run a simple offline regression eval.
|
||||
|
||||
Dataset format: JSONL, each line:
|
||||
{"id": "...", "input": "...", "assert_contains": ["..."], "assert_not_contains": ["..."]}
|
||||
"""
|
||||
store = get_assistant_store()
|
||||
agent = build_gateway_executor(store)
|
||||
session = store.create_session("offline-eval")
|
||||
gw = OclawGateway(store=store)
|
||||
cases = _load_dataset(dataset_path)
|
||||
if limit is not None:
|
||||
cases = cases[: max(0, int(limit))]
|
||||
|
||||
results: list[EvalCaseResult] = []
|
||||
for c in cases:
|
||||
t0 = time.perf_counter()
|
||||
msg = StandardMessage(
|
||||
session_id=str(session.id),
|
||||
tenant_id="",
|
||||
user_id="",
|
||||
role="owner",
|
||||
channel="eval",
|
||||
text=str(c.input_text or ""),
|
||||
attachments=[],
|
||||
metadata={"channel": "eval"},
|
||||
)
|
||||
out = str(gw.handle_turn(msg=msg, lang="zh", executor=agent).reply_text or "")
|
||||
latency_ms = int((time.perf_counter() - t0) * 1000)
|
||||
failures: list[str] = []
|
||||
for must in c.assert_contains:
|
||||
if must not in out:
|
||||
failures.append(f"missing_substring:{must}")
|
||||
for bad in c.assert_not_contains:
|
||||
if bad in out:
|
||||
failures.append(f"unexpected_substring:{bad}")
|
||||
results.append(EvalCaseResult(case_id=c.case_id, ok=not failures, latency_ms=latency_ms, failures=failures))
|
||||
|
||||
passed = sum(1 for r in results if r.ok)
|
||||
report = {
|
||||
"dataset": str(dataset_path),
|
||||
"total": len(results),
|
||||
"passed": passed,
|
||||
"pass_rate": round((passed / len(results)) if results else 0.0, 4),
|
||||
"results": [
|
||||
{"id": r.case_id, "ok": r.ok, "latency_ms": r.latency_ms, "failures": r.failures} for r in results
|
||||
],
|
||||
"agent_metrics": eval_summary(store, limit=5000),
|
||||
}
|
||||
if report_path:
|
||||
Path(report_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
Path(report_path).write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
return report
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = run_eval("data/eval/mvp_tasks.jsonl", report_path="data/eval/report.json")
|
||||
print(json.dumps({k: v for k, v in result.items() if k != "results"}, ensure_ascii=False, indent=2))
|
||||
|
|
@ -81,7 +81,7 @@ def skill_market_install_tool() -> ToolSpec:
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"slug": {"type": "string"},
|
||||
"provider": {"type": "string", "description": "Optional provider override: clawhub or cocoloop."},
|
||||
"provider": {"type": "string", "description": "Optional provider override (clawhub)."},
|
||||
"version": {"type": "string"},
|
||||
"overwrite": {"type": "boolean"},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,187 +0,0 @@
|
|||
"""CocoLoop 技能商店 HTTP 客户端(与 ClawHub 并列,供 `skills_market` 使用)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
def _strip_trailing_slash(url: str) -> str:
|
||||
return str(url or "").strip().rstrip("/")
|
||||
|
||||
|
||||
def _join_url(base: str, path: str) -> str:
|
||||
b = _strip_trailing_slash(base)
|
||||
p = str(path or "").strip()
|
||||
if not p:
|
||||
return b
|
||||
if not p.startswith("/"):
|
||||
p = "/" + p
|
||||
return b + p
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CocoloopConfig:
|
||||
api_base_url: str = "https://api.cocoloop.com"
|
||||
|
||||
|
||||
def load_cocoloop_config() -> CocoloopConfig:
|
||||
base = str(os.getenv("AIA_COCOLOOP_API_BASE") or os.getenv("COCOLOOP_API_BASE") or "https://api.cocoloop.com").strip()
|
||||
return CocoloopConfig(api_base_url=_strip_trailing_slash(base))
|
||||
|
||||
|
||||
def _default_headers() -> dict[str, str]:
|
||||
return {
|
||||
"User-Agent": "Oclaw-SkillMarket/1.0 (+https://github.com/oclaw)",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
|
||||
def _get_json(url: str, *, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
try:
|
||||
with httpx.Client(timeout=12.0, follow_redirects=True) as c:
|
||||
r = c.get(url, params=params or {}, headers=_default_headers())
|
||||
if r.status_code != 200:
|
||||
return {}
|
||||
obj = r.json()
|
||||
return obj if isinstance(obj, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _list_items(cfg: CocoloopConfig, *, keyword: str, page: int, page_size: int) -> list[dict[str, Any]]:
|
||||
url = _join_url(cfg.api_base_url, "/api/v1/store/skills")
|
||||
blob = _get_json(
|
||||
url,
|
||||
params={
|
||||
"page": max(1, int(page)),
|
||||
"page_size": max(1, min(int(page_size), 100)),
|
||||
"keyword": str(keyword or "").strip(),
|
||||
"sort": "downloads",
|
||||
},
|
||||
)
|
||||
data = blob.get("data") if isinstance(blob.get("data"), dict) else {}
|
||||
items = data.get("items")
|
||||
if not isinstance(items, list):
|
||||
return []
|
||||
return [x for x in items if isinstance(x, dict)]
|
||||
|
||||
|
||||
def _normalize_list_row(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
slug = str(raw.get("name") or "").strip()
|
||||
dl = str(raw.get("download_url") or "").strip()
|
||||
ver = str(raw.get("version") or "").strip() or "latest"
|
||||
return {
|
||||
"source": "cocoloop",
|
||||
"slug": slug,
|
||||
"name": str(raw.get("subtitle") or raw.get("summary") or slug),
|
||||
"description": str(raw.get("brief") or raw.get("summary") or raw.get("original_desc") or ""),
|
||||
"version": ver,
|
||||
"owner": str(raw.get("author") or ""),
|
||||
"updatedAt": "",
|
||||
"downloads": _parse_count(raw.get("downloads")),
|
||||
"stars": _parse_count(raw.get("github_stars")),
|
||||
"homepage": f"https://hub.cocoloop.cn/skills/{raw.get('id')}" if raw.get("id") else "",
|
||||
"archiveUrl": dl,
|
||||
"raw": raw,
|
||||
}
|
||||
|
||||
|
||||
def _parse_count(v: Any) -> int:
|
||||
if isinstance(v, int):
|
||||
return v
|
||||
s = str(v or "").strip().lower().replace(",", "")
|
||||
if not s:
|
||||
return 0
|
||||
mult = 1
|
||||
if s.endswith("k"):
|
||||
mult = 1000
|
||||
s = s[:-1]
|
||||
if s.endswith("m"):
|
||||
mult = 1_000_000
|
||||
s = s[:-1]
|
||||
try:
|
||||
return int(float(s) * mult)
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
|
||||
def search_store_skills(query: str, *, limit: int = 20, cfg: CocoloopConfig | None = None) -> list[dict[str, Any]]:
|
||||
cfg = cfg or load_cocoloop_config()
|
||||
lim = max(1, min(int(limit or 20), 100))
|
||||
rows = _list_items(cfg, keyword=str(query or "").strip(), page=1, page_size=lim)
|
||||
return [_normalize_list_row(r) for r in rows if str(r.get("name") or "").strip()]
|
||||
|
||||
|
||||
def get_skill_detail_by_slug(slug: str, *, cfg: CocoloopConfig | None = None) -> dict[str, Any]:
|
||||
"""按商店 `name`(slug)解析技能;必要时用数字 id 直查。"""
|
||||
cfg = cfg or load_cocoloop_config()
|
||||
s = str(slug or "").strip()
|
||||
if not s:
|
||||
return {}
|
||||
if s.isdigit():
|
||||
return _detail_from_id(cfg, int(s))
|
||||
rows = _list_items(cfg, keyword=s, page=1, page_size=80)
|
||||
want = s.lower()
|
||||
hit: dict[str, Any] | None = None
|
||||
for r in rows:
|
||||
if str(r.get("name") or "").strip().lower() == want:
|
||||
hit = r
|
||||
break
|
||||
if hit is None:
|
||||
for r in rows:
|
||||
nm = str(r.get("name") or "").strip().lower()
|
||||
if want in nm or nm in want:
|
||||
hit = r
|
||||
break
|
||||
if hit is None:
|
||||
return {"slug": s, "source": "cocoloop"}
|
||||
return _detail_from_list_row(cfg, hit)
|
||||
|
||||
|
||||
def _detail_from_id(cfg: CocoloopConfig, skill_id: int) -> dict[str, Any]:
|
||||
url = _join_url(cfg.api_base_url, f"/api/v1/store/skills/{int(skill_id)}")
|
||||
blob = _get_json(url)
|
||||
data = blob.get("data") if isinstance(blob.get("data"), dict) else {}
|
||||
if not data:
|
||||
return {"slug": str(skill_id), "source": "cocoloop"}
|
||||
return _detail_from_list_row(cfg, data)
|
||||
|
||||
|
||||
def _detail_from_list_row(cfg: CocoloopConfig, row: dict[str, Any]) -> dict[str, Any]:
|
||||
slug = str(row.get("name") or "").strip()
|
||||
dl = str(row.get("download_url") or "").strip()
|
||||
if not dl and slug:
|
||||
asset = str(row.get("asset_name") or f"{slug}.zip").strip()
|
||||
if not asset.endswith(".zip"):
|
||||
asset = f"{asset}.zip"
|
||||
dl = f"https://dl.cocoloop.cn/bss/skills/{asset.lstrip('/')}"
|
||||
ver = str(row.get("version") or "").strip() or "latest"
|
||||
ver_clean = ver.lstrip("vV") if ver not in {"", "latest"} else ver
|
||||
versions: list[dict[str, Any]] = [{"version": ver_clean or "latest", "changelog": "", "createdAt": "", "archiveUrl": dl, "raw": row}]
|
||||
return {
|
||||
"source": "cocoloop",
|
||||
"slug": slug,
|
||||
"name": str(row.get("subtitle") or row.get("summary") or slug),
|
||||
"description": str(row.get("brief") or row.get("summary") or row.get("original_desc") or ""),
|
||||
"owner": str(row.get("author") or ""),
|
||||
"updatedAt": "",
|
||||
"homepage": f"https://hub.cocoloop.cn/skills/{row.get('id')}" if row.get("id") else "",
|
||||
"latestVersion": ver_clean if ver_clean else "latest",
|
||||
"archiveUrl": dl,
|
||||
"downloads": _parse_count(row.get("downloads")),
|
||||
"stars": _parse_count(row.get("github_stars")),
|
||||
"versions": versions,
|
||||
"raw": row,
|
||||
}
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CocoloopConfig",
|
||||
"load_cocoloop_config",
|
||||
"search_store_skills",
|
||||
"get_skill_detail_by_slug",
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue