@optima-chat/dev-skills 0.16.2 → 0.16.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/logs.md +60 -3
- package/.claude/commands/trace-user.md +18 -6
- package/.claude/skills/cn-deploy/SKILL.md +5 -3
- package/.claude/skills/gateway-admin/SKILL.md +15 -0
- package/.claude/skills/logs/SKILL.md +10 -2
- package/.claude/skills/reset-onboarding/SKILL.md +116 -0
- package/.claude/skills/yzsgo-e2e/SKILL.md +51 -0
- package/.claude/skills/yzsgo-e2e/SYNC.md +10 -0
- package/.claude/skills/yzsgo-e2e/chat_driver.py +659 -0
- package/.claude/skills/yzsgo-e2e/judge_outcome.js +11 -0
- package/.claude/skills/yzsgo-e2e/judge_workflow.js +100 -0
- package/.claude/skills/yzsgo-e2e/preflight.py +44 -0
- package/.claude/skills/yzsgo-e2e/prep_conversation.py +21 -0
- package/.claude/skills/yzsgo-e2e/pull_wire.py +312 -0
- package/.claude/skills/yzsgo-e2e/run_e2e.py +122 -0
- package/.claude/skills/yzsgo-e2e/verify_drift.py +42 -0
- package/.codex/skills/cn-deploy/SKILL.md +5 -3
- package/.codex/skills/reset-onboarding/SKILL.md +116 -0
- package/.codex/skills/yzsgo-e2e/SKILL.md +51 -0
- package/.codex/skills/yzsgo-e2e/SYNC.md +10 -0
- package/.codex/skills/yzsgo-e2e/chat_driver.py +659 -0
- package/.codex/skills/yzsgo-e2e/judge_outcome.js +11 -0
- package/.codex/skills/yzsgo-e2e/judge_workflow.js +100 -0
- package/.codex/skills/yzsgo-e2e/preflight.py +44 -0
- package/.codex/skills/yzsgo-e2e/prep_conversation.py +21 -0
- package/.codex/skills/yzsgo-e2e/pull_wire.py +312 -0
- package/.codex/skills/yzsgo-e2e/run_e2e.py +122 -0
- package/.codex/skills/yzsgo-e2e/verify_drift.py +42 -0
- package/AGENTS.md +12 -6
- package/README.md +74 -61
- package/bin/helpers/cn-deploy.ts +44 -37
- package/bin/helpers/logs.ts +464 -31
- package/dist/bin/helpers/cn-deploy.js +43 -37
- package/dist/bin/helpers/logs.js +410 -31
- package/docs/superpowers/plans/2026-08-31-yzsgo-e2e.md +969 -0
- package/docs/superpowers/specs/2026-08-31-yzsgo-e2e-design.md +154 -0
- package/package.json +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// yzsgo-e2e 三态裁决纯函数(node --test 覆盖)。逻辑与 judge_workflow.js 内联副本保持一致。
|
|
2
|
+
function decideOutcome(votes) {
|
|
3
|
+
const good = (votes || []).filter(Boolean);
|
|
4
|
+
const n = good.length;
|
|
5
|
+
const refuted = good.filter((v) => v.refuted).length;
|
|
6
|
+
if (n === 0) return "needs_review";
|
|
7
|
+
if (refuted === n) return "rejected";
|
|
8
|
+
if (refuted > n / 2) return "needs_review";
|
|
9
|
+
return "confirmed";
|
|
10
|
+
}
|
|
11
|
+
module.exports = { decideOutcome };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// .claude/skills/yzsgo-e2e/judge_workflow.js
|
|
2
|
+
// 改编自 optima-gateway conversation-iq/workflow.js(见 SYNC.md)。
|
|
3
|
+
// 输入 args: { base, sids, knownIssues }(备料稿目录 / 待判对话文件名列表 / gh 实时拉的开着 issue 文本)。
|
|
4
|
+
|
|
5
|
+
export const meta = {
|
|
6
|
+
name: "yzsgo-e2e-judge",
|
|
7
|
+
description: "读 e2e 备料稿(wire + 前端证据)逐对话判缺陷,对抗验证出三态",
|
|
8
|
+
phases: [
|
|
9
|
+
{ title: "Judge", detail: "每对话一个 agent 读备料稿判缺陷(含前后端一致性)" },
|
|
10
|
+
{ title: "Verify", detail: "每条 novel finding 3 个 skeptic 读 runtime 源码反驳,三态裁决" },
|
|
11
|
+
],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// decideOutcome 与 judge_outcome.js 同步(后者有 node 单测)
|
|
15
|
+
function decideOutcome(votes) {
|
|
16
|
+
const good = (votes || []).filter(Boolean);
|
|
17
|
+
const n = good.length;
|
|
18
|
+
const refuted = good.filter((v) => v.refuted).length;
|
|
19
|
+
if (n === 0) return "needs_review";
|
|
20
|
+
if (refuted === n) return "rejected";
|
|
21
|
+
if (refuted > n / 2) return "needs_review";
|
|
22
|
+
return "confirmed";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const A = typeof args === "string" ? JSON.parse(args) : args || {};
|
|
26
|
+
|
|
27
|
+
const STABLE_NORMAL = `## 正常现象(不是缺陷,别报):
|
|
28
|
+
- compaction 摘要调用、abort、max_tokens 截断本身;
|
|
29
|
+
- 前端 new_conversation 后主区只有本轮(预期)。`;
|
|
30
|
+
|
|
31
|
+
const JUDGE_SCHEMA = {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
impression: { type: "string" },
|
|
35
|
+
findings: {
|
|
36
|
+
type: "array",
|
|
37
|
+
items: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
what: { type: "string" },
|
|
41
|
+
evidence: { type: "string" },
|
|
42
|
+
knownIssue: { type: "string" },
|
|
43
|
+
},
|
|
44
|
+
required: ["what", "evidence"],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required: ["impression", "findings"],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const VERDICT_SCHEMA = {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: { refuted: { type: "boolean" }, reason: { type: "string" } },
|
|
54
|
+
required: ["refuted", "reason"],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function judgePrompt(path) {
|
|
58
|
+
return `读备料稿 ${path}(含 wire transcript + 「前端所见」证据)。判这次端到端对话有没有网关/agent 缺陷。
|
|
59
|
+
重点核对:① wire 里 agent 真实产出 vs 前端渲染是否一致(丢内容/半截/前端报错但 wire 成功、或反之);
|
|
60
|
+
② 悬空 tool_use / max_tokens 截断 / error / abort 是否造成用户可感问题;③ 回答是否编数据/答非所问/活没干完。
|
|
61
|
+
${STABLE_NORMAL}
|
|
62
|
+
已知开着的 issue(命中就在 knownIssue 里标 #号,仅打标签、不要因此不报):
|
|
63
|
+
${A.knownIssues || "(无)"}
|
|
64
|
+
默认健康:证据不足别硬报。输出 impression + findings。`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function verifyPrompt(f, path) {
|
|
68
|
+
return `有人在 ${path} 报了缺陷:「${f.what}」,证据:${f.evidence}。
|
|
69
|
+
你是 skeptic:读**真实 runtime 源码**(optima-gateway / agent-runtime)核对机制,尽力反驳。默认判假(refuted=true),
|
|
70
|
+
只有确凿证明该缺陷真实存在才 refuted=false。输出 refuted + reason。`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const results = await pipeline(
|
|
74
|
+
A.sids,
|
|
75
|
+
(sid) =>
|
|
76
|
+
agent(judgePrompt(`${A.base}/${sid}.md`), {
|
|
77
|
+
label: `judge:${sid}`,
|
|
78
|
+
phase: "Judge",
|
|
79
|
+
schema: JUDGE_SCHEMA,
|
|
80
|
+
}).then((judge) => ({ sid, judge })),
|
|
81
|
+
async (prev) => {
|
|
82
|
+
if (!prev || !prev.judge) return { sid: prev?.sid, confirmed: [], needsReview: [], rejected: [] };
|
|
83
|
+
const path = `${A.base}/${prev.sid}.md`;
|
|
84
|
+
const novel = prev.judge.findings || [];
|
|
85
|
+
if (novel.length === 0)
|
|
86
|
+
return { sid: prev.sid, impression: prev.judge.impression, confirmed: [], needsReview: [], rejected: [] };
|
|
87
|
+
const verified = await parallel(
|
|
88
|
+
novel.map((f) => async () => {
|
|
89
|
+
const votes = await parallel(
|
|
90
|
+
[0, 1, 2].map(() => () => agent(verifyPrompt(f, path), { label: `verify:${prev.sid}`, phase: "Verify", schema: VERDICT_SCHEMA }))
|
|
91
|
+
);
|
|
92
|
+
return { finding: f, outcome: decideOutcome(votes) };
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
const by = (o) => verified.filter(Boolean).filter((v) => v.outcome === o);
|
|
96
|
+
return { sid: prev.sid, impression: prev.judge.impression, confirmed: by("confirmed"), needsReview: by("needs_review"), rejected: by("rejected") };
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
return { results: results.filter(Boolean) };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""yzsgo-e2e 环境自检。探测函数有副作用(读端口/文件),汇总用纯函数 summarize_preflight。
|
|
3
|
+
缺项只指路(不替用户做一次性登录/充值),准备步骤见 store-skills 的 setting-up-yzsgo-test-env。"""
|
|
4
|
+
import os, shutil, socket, sys
|
|
5
|
+
|
|
6
|
+
def _port_open(port: int) -> bool:
|
|
7
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
8
|
+
s.settimeout(1.0)
|
|
9
|
+
return s.connect_ex(("127.0.0.1", port)) == 0
|
|
10
|
+
|
|
11
|
+
def probe(env: str = "cn-prod") -> list[dict]:
|
|
12
|
+
return [
|
|
13
|
+
{"name": "chrome-9222", "ok": _port_open(9222),
|
|
14
|
+
"hint": '起调试端口 Chrome:open -na "Google Chrome" --args --remote-debugging-port=9222 '
|
|
15
|
+
'--user-data-dir=/tmp/yzsgo-chrome https://www.yzsgo.com(手动登测试账号)'},
|
|
16
|
+
{"name": "buildbox-pw", "ok": os.path.exists(os.path.expanduser("~/.buildbox_pw")),
|
|
17
|
+
"hint": "拉 wire 需 buildbox 口令文件 ~/.buildbox_pw(见 setting-up-yzsgo-test-env)"},
|
|
18
|
+
{"name": "sshpass", "ok": shutil.which("sshpass") is not None,
|
|
19
|
+
"hint": "brew install hudochenkov/sshpass/sshpass"},
|
|
20
|
+
{"name": "playwright", "ok": _has_playwright(),
|
|
21
|
+
"hint": "pip install playwright && playwright install chromium"},
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
def _has_playwright() -> bool:
|
|
25
|
+
try:
|
|
26
|
+
import playwright # noqa: F401
|
|
27
|
+
return True
|
|
28
|
+
except Exception:
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
def summarize_preflight(checks: list) -> dict:
|
|
32
|
+
ok = all(c["ok"] for c in checks)
|
|
33
|
+
missing = [c["name"] for c in checks if not c["ok"]]
|
|
34
|
+
rows = []
|
|
35
|
+
for c in checks:
|
|
36
|
+
mark = "✅" if c["ok"] else "❌"
|
|
37
|
+
rows.append(f"{mark} {c['name']}" + ("" if c["ok"] else f"\n → {c['hint']}"))
|
|
38
|
+
return {"ok": ok, "missing": missing, "report": "\n".join(rows)}
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
env = sys.argv[1] if len(sys.argv) > 1 else "cn-prod"
|
|
42
|
+
r = summarize_preflight(probe(env))
|
|
43
|
+
print(r["report"])
|
|
44
|
+
sys.exit(0 if r["ok"] else 1)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""yzsgo-e2e 备料层:wire 侧备料稿(复用 pull_wire.render_conversation)+ 合并浏览器侧证据。
|
|
3
|
+
改编自 optima-gateway conversation-iq/prep_session.py(备料思路)与 pull_wire(渲染)。见 SYNC.md。"""
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
def merge_browser_evidence(wire_md: str, browser_turns: list) -> str:
|
|
7
|
+
lines = [wire_md.rstrip(), "",
|
|
8
|
+
"## 前端所见(浏览器侧证据;与上面 wire 对照)",
|
|
9
|
+
"> 判定要求:逐轮核对**前端渲染的**与**wire 里 agent 真实产出的**是否一致——"
|
|
10
|
+
"前端丢内容/半截/报错但 wire 成功(或反之)即为缺陷。", ""]
|
|
11
|
+
for i, t in enumerate(browser_turns):
|
|
12
|
+
lines.append(f"### 轮 #{i} 发送: {t.get('sent','')!r}")
|
|
13
|
+
lines.append(f"- state: {t.get('state','')} timed_out: {t.get('timed_out', False)}")
|
|
14
|
+
tt = t.get("tool_trace")
|
|
15
|
+
if tt:
|
|
16
|
+
lines.append(f"- tool_trace: {json.dumps(tt, ensure_ascii=False)[:1500]}")
|
|
17
|
+
lines.append("- 前端整轮渲染:")
|
|
18
|
+
lines.append("```")
|
|
19
|
+
lines.append((t.get("transcript") or "(空)")[:6000])
|
|
20
|
+
lines.append("```")
|
|
21
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""拉取 + 渲染 yzsgo 会话的 Wire(Optima Gateway #2261 落盘的完整 LLM 上下文)。
|
|
3
|
+
|
|
4
|
+
Wire = 每个 runtime session 的完整 LLM 调用记录,存境内 buildbox 的 NAS:
|
|
5
|
+
/mnt/nas-cn-prod/workspaces/cn-prod/<userId>/.agent/llm-wire/<sessionId>/
|
|
6
|
+
records.jsonl —— 每行一条 {kind:request/response/error, callId, seq, ts, body{system,tools,messages}, finalMessage...}
|
|
7
|
+
blobs/ —— 大内容按 {"$blob": "<hash>"} 引用,deref 时读这里
|
|
8
|
+
TTL 14 天。经 buildbox(47.94.105.163,口令 ~/.buildbox_pw)拉。
|
|
9
|
+
|
|
10
|
+
⚠️ warm-pool 会把**同一账号顺序跑的多个 web 对话聚在一个 runtime session** 里 —— 一个
|
|
11
|
+
records.jsonl 常含几十个对话。切分靠 seq 复位:agent 每轮 req 的 messages 递增(2,4,6…)、
|
|
12
|
+
seq 递增(1,3,5…),**seq==1(msgs==2)= 新对话开始**。本脚本据此切分,每个对话取「末 req 的
|
|
13
|
+
全历史 + 末 response」= 该对话的完整 transcript(含推理/工具调用/工具结果/最终回复)。
|
|
14
|
+
|
|
15
|
+
用法:
|
|
16
|
+
# 拉 + 渲染(默认测试账号,近 1 天)
|
|
17
|
+
pull_wire.py --user <userId> --since 1 --out e2e/wire
|
|
18
|
+
# 已拉过、只重渲染本地数据(迭代用,不再 SSH)
|
|
19
|
+
pull_wire.py --local e2e/wire/raw/llm-wire --out e2e/wire
|
|
20
|
+
|
|
21
|
+
参考 optima-gateway 的 conversation-iq/prep_session.py(同一 Wire 数据源)。
|
|
22
|
+
"""
|
|
23
|
+
import argparse
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
|
|
29
|
+
BUILDBOX = os.environ.get("OPTIMA_BUILDBOX_HOST", "root@47.94.105.163")
|
|
30
|
+
PW_FILE = os.path.expanduser(os.environ.get("OPTIMA_BUILDBOX_PW", "~/.buildbox_pw"))
|
|
31
|
+
NAS = os.environ.get("OPTIMA_NAS_CN", "/mnt/nas-cn-prod/workspaces/cn-prod")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def ssh(cmd):
|
|
35
|
+
return subprocess.run(
|
|
36
|
+
["sshpass", "-f", PW_FILE, "ssh", "-o", "StrictHostKeyChecking=no",
|
|
37
|
+
"-o", "ConnectTimeout=25", BUILDBOX, cmd],
|
|
38
|
+
capture_output=True, text=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def pull(user, since_days, out):
|
|
42
|
+
"""SSH buildbox:打包该 user 近 since_days 天有更新的 wire session,scp 回 out/raw/。"""
|
|
43
|
+
raw = os.path.join(out, "raw")
|
|
44
|
+
os.makedirs(raw, exist_ok=True)
|
|
45
|
+
base = f"{NAS}/{user}/.agent/llm-wire"
|
|
46
|
+
# 只打包近 since_days 天有更新的 session(records.jsonl mtime)
|
|
47
|
+
remote_tar = "/tmp/optima_wire_pull.tgz"
|
|
48
|
+
pack = (f"cd {NAS}/{user}/.agent 2>/dev/null && "
|
|
49
|
+
f"sids=$(find {base}/*/records.jsonl -mtime -{since_days} 2>/dev/null "
|
|
50
|
+
f"| sed 's#/records.jsonl##' | sed 's#.*/##') && "
|
|
51
|
+
f"[ -z \"$sids\" ] && echo NO_SESSIONS && exit 0; "
|
|
52
|
+
f"tar czf {remote_tar} $(for s in $sids; do echo llm-wire/$s; done) && "
|
|
53
|
+
f"echo PACKED $(echo \"$sids\" | wc -w)")
|
|
54
|
+
r = ssh(pack)
|
|
55
|
+
if "NO_SESSIONS" in r.stdout:
|
|
56
|
+
print(f"[pull] {user} 近 {since_days} 天无 wire session(TTL 14 天,或该账号没跑过)")
|
|
57
|
+
return None
|
|
58
|
+
if "PACKED" not in r.stdout:
|
|
59
|
+
print("[pull] 打包失败:", r.stdout, r.stderr, file=sys.stderr)
|
|
60
|
+
return None
|
|
61
|
+
print(f"[pull] 远端打包 {r.stdout.strip().splitlines()[-1]} session")
|
|
62
|
+
local_tar = os.path.join(raw, "wire.tgz")
|
|
63
|
+
subprocess.run(["sshpass", "-f", PW_FILE, "scp", "-o", "StrictHostKeyChecking=no",
|
|
64
|
+
f"{BUILDBOX}:{remote_tar}", local_tar], check=True)
|
|
65
|
+
subprocess.run(["tar", "xzf", local_tar, "-C", raw], check=True)
|
|
66
|
+
ssh(f"rm -f {remote_tar}")
|
|
67
|
+
return os.path.join(raw, "llm-wire")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ── 渲染(源自 conversation-iq/prep_session.py,扩展为「一个 session 切多个对话」)──
|
|
71
|
+
|
|
72
|
+
def make_deref(session_dir):
|
|
73
|
+
def deref(v):
|
|
74
|
+
if isinstance(v, dict) and "$blob" in v:
|
|
75
|
+
try:
|
|
76
|
+
return json.load(open(os.path.join(session_dir, "blobs", v["$blob"])))
|
|
77
|
+
except Exception:
|
|
78
|
+
return v
|
|
79
|
+
return v
|
|
80
|
+
return deref
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def first_user_text(req, deref):
|
|
84
|
+
for m in (deref(req.get("body", {}).get("messages")) or []):
|
|
85
|
+
m = deref(m)
|
|
86
|
+
if m.get("role") == "user":
|
|
87
|
+
c = m.get("content")
|
|
88
|
+
if isinstance(c, str):
|
|
89
|
+
return c
|
|
90
|
+
if isinstance(c, list):
|
|
91
|
+
for b in c:
|
|
92
|
+
b = deref(b)
|
|
93
|
+
if isinstance(b, dict) and b.get("type") == "text":
|
|
94
|
+
return b.get("text", "")
|
|
95
|
+
return "(无 user 消息)"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# 渲染截断上限(审查要逐格核数字 → tool_result 尽量全;截断处标真实长度,别让审查以为"就这些")
|
|
99
|
+
CAP_TEXT = 6000
|
|
100
|
+
CAP_THINK = 3000
|
|
101
|
+
CAP_TOOL_IN = 6000
|
|
102
|
+
CAP_TOOL_OUT = 16000
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _clip(s, cap):
|
|
106
|
+
s = str(s)
|
|
107
|
+
return s if len(s) <= cap else s[:cap] + f" …[截断,共 {len(s)} 字]"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def render_block(b, deref):
|
|
111
|
+
b = deref(b)
|
|
112
|
+
if not isinstance(b, dict):
|
|
113
|
+
return " " + _clip(b, CAP_TOOL_OUT)
|
|
114
|
+
t = b.get("type")
|
|
115
|
+
if t == "text":
|
|
116
|
+
return " [text] " + _clip(b.get("text") or "", CAP_TEXT)
|
|
117
|
+
if t == "thinking":
|
|
118
|
+
return " [thinking] " + _clip(b.get("thinking") or "", CAP_THINK)
|
|
119
|
+
if t == "tool_use":
|
|
120
|
+
return f" [tool_use {b.get('name')}] " + _clip(json.dumps(deref(b.get("input")), ensure_ascii=False), CAP_TOOL_IN)
|
|
121
|
+
if t == "tool_result":
|
|
122
|
+
c = deref(b.get("content"))
|
|
123
|
+
if isinstance(c, list):
|
|
124
|
+
c = " ".join(json.dumps(deref(x), ensure_ascii=False) for x in c)
|
|
125
|
+
return f" [tool_result err={b.get('is_error')}] " + _clip(c, CAP_TOOL_OUT)
|
|
126
|
+
if t == "image":
|
|
127
|
+
return " [image]"
|
|
128
|
+
return " [?] " + _clip(json.dumps(b, ensure_ascii=False), CAP_TOOL_OUT)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def segment(reqs, deref):
|
|
132
|
+
"""按 seq==1(新对话起点)把 requests 切成多段对话。返回 [[req,...], ...](各段按 ts 有序)。"""
|
|
133
|
+
reqs = sorted(reqs, key=lambda r: r.get("ts", ""))
|
|
134
|
+
convs, cur = [], []
|
|
135
|
+
for r in reqs:
|
|
136
|
+
if r.get("seq", 1) == 1 and cur:
|
|
137
|
+
convs.append(cur)
|
|
138
|
+
cur = []
|
|
139
|
+
cur.append(r)
|
|
140
|
+
if cur:
|
|
141
|
+
convs.append(cur)
|
|
142
|
+
return convs
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def render_conversation(conv, resps, deref, idx):
|
|
146
|
+
"""一个对话 = 一串 req(末 req 含全历史)。渲染事实卡 + 完整 transcript。"""
|
|
147
|
+
last = conv[-1]
|
|
148
|
+
prompt = first_user_text(conv[0], deref)
|
|
149
|
+
ts0 = conv[0].get("ts", "")
|
|
150
|
+
# 事实卡
|
|
151
|
+
n_turns = len(conv)
|
|
152
|
+
errs = [resps.get(r["callId"]) for r in conv
|
|
153
|
+
if resps.get(r["callId"]) and resps[r["callId"]].get("kind") == "error"]
|
|
154
|
+
maxtok = sum(1 for r in conv
|
|
155
|
+
if (resps.get(r["callId"]) or {}).get("kind") == "response"
|
|
156
|
+
and ((resps[r["callId"]].get("finalMessage") or {}).get("stopReason") == "max_tokens"))
|
|
157
|
+
# 悬空 tool_use(末 req 全历史)
|
|
158
|
+
msgs = [deref(m) for m in (deref(last["body"].get("messages")) or [])]
|
|
159
|
+
use_ids, res_ids = {}, set()
|
|
160
|
+
for m in msgs:
|
|
161
|
+
c = m.get("content")
|
|
162
|
+
if isinstance(c, list):
|
|
163
|
+
for b in c:
|
|
164
|
+
b = deref(b)
|
|
165
|
+
if isinstance(b, dict):
|
|
166
|
+
if b.get("type") == "tool_use":
|
|
167
|
+
use_ids[b.get("id")] = b.get("name")
|
|
168
|
+
if b.get("type") == "tool_result":
|
|
169
|
+
res_ids.add(b.get("tool_use_id"))
|
|
170
|
+
dangling = [(u, n) for u, n in use_ids.items() if u not in res_ids]
|
|
171
|
+
|
|
172
|
+
L = [f"# 对话 #{idx} {ts0}",
|
|
173
|
+
"", f"**prompt**: {prompt[:200]}", "",
|
|
174
|
+
"## 事实卡(代码算的确定信息)",
|
|
175
|
+
f"- LLM 轮数(req) {n_turns}",
|
|
176
|
+
f"- error 响应 {len(errs)}" + (f":{[json.dumps(deref(e.get('error') or {}),ensure_ascii=False)[:120] for e in errs]}" if errs else ""),
|
|
177
|
+
f"- stop_reason=max_tokens 的响应 {maxtok}",
|
|
178
|
+
f"- 悬空 tool_use(无匹配 result){len(dangling)}: {dangling[:5]}",
|
|
179
|
+
"", "## 完整 transcript(末 req 全历史 + 末 response)", ""]
|
|
180
|
+
# 末 response 拼到历史尾
|
|
181
|
+
lastresp = resps.get(last["callId"])
|
|
182
|
+
if lastresp and lastresp.get("kind") == "response":
|
|
183
|
+
fm = lastresp.get("finalMessage") or {}
|
|
184
|
+
msgs.append({"role": "assistant", "content": fm.get("content", [])})
|
|
185
|
+
for i, m in enumerate(msgs):
|
|
186
|
+
role = m.get("role")
|
|
187
|
+
c = m.get("content")
|
|
188
|
+
L.append(f"\n--- #{i} [{role}] ---")
|
|
189
|
+
if isinstance(c, str):
|
|
190
|
+
L.append(" " + _clip(c, CAP_TEXT))
|
|
191
|
+
elif isinstance(c, list):
|
|
192
|
+
for b in c:
|
|
193
|
+
L.append(render_block(b, deref))
|
|
194
|
+
return prompt, ts0, len(errs), len(dangling), "\n".join(L)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def render_all(wire_root, out):
|
|
198
|
+
conv_dir = os.path.join(out, "conversations")
|
|
199
|
+
os.makedirs(conv_dir, exist_ok=True)
|
|
200
|
+
index = ["# Wire 会话索引", ""]
|
|
201
|
+
sessions = sorted(d for d in os.listdir(wire_root)
|
|
202
|
+
if os.path.isdir(os.path.join(wire_root, d)))
|
|
203
|
+
gidx = 0
|
|
204
|
+
for sid in sessions:
|
|
205
|
+
sdir = os.path.join(wire_root, sid)
|
|
206
|
+
recf = os.path.join(sdir, "records.jsonl")
|
|
207
|
+
if not os.path.exists(recf):
|
|
208
|
+
continue
|
|
209
|
+
deref = make_deref(sdir)
|
|
210
|
+
recs = [json.loads(l) for l in open(recf, encoding="utf-8") if l.strip()]
|
|
211
|
+
reqs = [r for r in recs if r.get("kind") == "request"]
|
|
212
|
+
resps = {r.get("callId"): r for r in recs if r.get("kind") in ("response", "error")}
|
|
213
|
+
convs = segment(reqs, deref)
|
|
214
|
+
index.append(f"\n## session `{sid}` — {len(convs)} 对话 / {len(reqs)} req\n")
|
|
215
|
+
for conv in convs:
|
|
216
|
+
gidx += 1
|
|
217
|
+
prompt, ts0, n_err, n_dang, md = render_conversation(conv, resps, deref, gidx)
|
|
218
|
+
fn = f"{gidx:03d}.md"
|
|
219
|
+
open(os.path.join(conv_dir, fn), "w", encoding="utf-8").write(md)
|
|
220
|
+
flag = (" ⚠️err" if n_err else "") + (" ⚠️dangling" if n_dang else "")
|
|
221
|
+
index.append(f"- [{fn}](conversations/{fn}) [{len(conv):>2} req] {prompt[:64]}{flag}")
|
|
222
|
+
idxpath = os.path.join(out, "index.md")
|
|
223
|
+
open(idxpath, "w", encoding="utf-8").write("\n".join(index))
|
|
224
|
+
print(f"[render] {gidx} 个对话 → {conv_dir}/ 索引 → {idxpath}")
|
|
225
|
+
return idxpath
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def main():
|
|
229
|
+
ap = argparse.ArgumentParser()
|
|
230
|
+
ap.add_argument("--user", help="userId(拉该账号的 wire)")
|
|
231
|
+
ap.add_argument("--since", type=int, default=1, help="拉近 N 天有更新的 session(默认 1)")
|
|
232
|
+
ap.add_argument("--out", default="e2e/wire", help="输出目录(默认 e2e/wire)")
|
|
233
|
+
ap.add_argument("--local", help="跳过 SSH,直接渲染这个本地 llm-wire 目录(迭代用)")
|
|
234
|
+
a = ap.parse_args()
|
|
235
|
+
|
|
236
|
+
if a.local:
|
|
237
|
+
render_all(a.local, a.out)
|
|
238
|
+
return
|
|
239
|
+
if not a.user:
|
|
240
|
+
ap.error("需 --user <userId>(或 --local <dir> 只渲染已拉数据)")
|
|
241
|
+
wire_root = pull(a.user, a.since, a.out)
|
|
242
|
+
if wire_root:
|
|
243
|
+
render_all(wire_root, a.out)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ── yzsgo-e2e 增补:结构化对话索引 + 本次对话定位 ──
|
|
247
|
+
|
|
248
|
+
def emit_conversation_index(wire_root):
|
|
249
|
+
"""遍历 wire_root 下各 session,切分对话,产出与 render_all 同序的结构化索引。"""
|
|
250
|
+
out = []
|
|
251
|
+
gidx = 0
|
|
252
|
+
sessions = sorted(d for d in os.listdir(wire_root)
|
|
253
|
+
if os.path.isdir(os.path.join(wire_root, d)))
|
|
254
|
+
for sid in sessions:
|
|
255
|
+
sdir = os.path.join(wire_root, sid)
|
|
256
|
+
recf = os.path.join(sdir, "records.jsonl")
|
|
257
|
+
if not os.path.exists(recf):
|
|
258
|
+
continue
|
|
259
|
+
deref = make_deref(sdir)
|
|
260
|
+
recs = [json.loads(l) for l in open(recf, encoding="utf-8") if l.strip()]
|
|
261
|
+
reqs = [r for r in recs if r.get("kind") == "request"]
|
|
262
|
+
for conv in segment(reqs, deref):
|
|
263
|
+
gidx += 1
|
|
264
|
+
out.append({"gidx": gidx, "sid": sid,
|
|
265
|
+
"ts": conv[0].get("ts", ""),
|
|
266
|
+
"prompt": first_user_text(conv[0], deref)})
|
|
267
|
+
return out
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _ts_key(ts):
|
|
271
|
+
"""把 wire 的 ...Z 与 datetime.isoformat 的 ...+00:00 归一成可比较的 aware datetime;解析不了回 None。"""
|
|
272
|
+
from datetime import datetime
|
|
273
|
+
if not ts:
|
|
274
|
+
return None
|
|
275
|
+
try:
|
|
276
|
+
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
277
|
+
except Exception:
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def locate_conversation(index, started_ts, first_message):
|
|
282
|
+
"""按 (started_ts, first_message) 定位本次对话;禁用 ls -t。规则见 plan Task 3 Interfaces。
|
|
283
|
+
时间比较先把两侧 ISO(Z / +00:00、小数位宽不同)归一成 datetime,避免依赖字典序=数值序的脆弱假设。"""
|
|
284
|
+
key = (first_message or "").strip()[:40]
|
|
285
|
+
cands = []
|
|
286
|
+
for it in index:
|
|
287
|
+
p = (it.get("prompt") or "").strip()
|
|
288
|
+
if not p or not key:
|
|
289
|
+
continue
|
|
290
|
+
if p[:40].startswith(key) or key.startswith(p[:40]):
|
|
291
|
+
cands.append(it)
|
|
292
|
+
if not cands:
|
|
293
|
+
return None
|
|
294
|
+
from datetime import datetime, timezone
|
|
295
|
+
FAR = datetime.max.replace(tzinfo=timezone.utc)
|
|
296
|
+
cands.sort(key=lambda x: _ts_key(x.get("ts", "")) or FAR)
|
|
297
|
+
st = _ts_key(started_ts)
|
|
298
|
+
after = []
|
|
299
|
+
for c in cands:
|
|
300
|
+
ck = _ts_key(c.get("ts", ""))
|
|
301
|
+
if st is not None and ck is not None:
|
|
302
|
+
if ck >= st:
|
|
303
|
+
after.append(c)
|
|
304
|
+
elif c.get("ts", "") >= (started_ts or ""): # 任一解析不了 → 退字符串比较保底
|
|
305
|
+
after.append(c)
|
|
306
|
+
if after:
|
|
307
|
+
return after[0]
|
|
308
|
+
return cands[-1]
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
if __name__ == "__main__":
|
|
312
|
+
main()
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""yzsgo-e2e 编排:驱动对话 → 拉 wire → 定位本次对话 → 备料(含浏览器证据)→ 出备料稿+元数据。
|
|
3
|
+
judge 由 Claude 用 judge_workflow.js 跑;提 issue 由 Claude 依 SKILL.md 用 gh 做。用户不敲本脚本。"""
|
|
4
|
+
import argparse, json, os, sys
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
|
|
7
|
+
def render_report(result: dict) -> str:
|
|
8
|
+
L = [f"# yzsgo-e2e 报告 · {result['env']} · {result['started_ts']}", ""]
|
|
9
|
+
if result.get("blocked"):
|
|
10
|
+
L.append(f"> ⚠️ blocked: {result['blocked']}(环境问题,非缺陷,未提 issue)")
|
|
11
|
+
L.append("")
|
|
12
|
+
L += ["## 本次端到端问题(按严重度)", "", "| 状态 | 问题 | 证据 |", "|---|---|---|"]
|
|
13
|
+
for f in result.get("confirmed", []):
|
|
14
|
+
L.append(f"| confirmed | {f['what']} | {f['evidence']} |")
|
|
15
|
+
for f in result.get("needs_review", []):
|
|
16
|
+
L.append(f"| needs_review | {f['what']} | {f['evidence']} |")
|
|
17
|
+
if not result.get("confirmed") and not result.get("needs_review"):
|
|
18
|
+
L.append("| — | 无 confirmed/needs_review | — |")
|
|
19
|
+
L += ["", "## 覆盖边界", result.get("coverage", "-"),
|
|
20
|
+
"", "## 判断修正", "(如判定过程中修正过结论,如实记此;无则写'无')"]
|
|
21
|
+
return "\n".join(L)
|
|
22
|
+
|
|
23
|
+
def _utc_now() -> str:
|
|
24
|
+
return datetime.now(timezone.utc).isoformat()
|
|
25
|
+
|
|
26
|
+
def _parse_answers(pairs):
|
|
27
|
+
out = []
|
|
28
|
+
for p in pairs or []:
|
|
29
|
+
if "=" in p:
|
|
30
|
+
k, v = p.split("=", 1)
|
|
31
|
+
out.append({"match": k, "answer": v})
|
|
32
|
+
else:
|
|
33
|
+
print(f"[warn] 忽略格式不对的 --answer {p!r}(应为 关键词=答案)", file=sys.stderr)
|
|
34
|
+
return out
|
|
35
|
+
|
|
36
|
+
def select_conversation_in_session(convs, deref, hit):
|
|
37
|
+
"""在同一 session 的 convs 里按 (ts, prompt) 重新定位本次对话;找不到回退末个。
|
|
38
|
+
不能用跨 session 的全局 gidx 去索引 session-local 列表。"""
|
|
39
|
+
import pull_wire
|
|
40
|
+
for c in convs:
|
|
41
|
+
if not c:
|
|
42
|
+
continue
|
|
43
|
+
if c[0].get("ts", "") == hit.get("ts") and pull_wire.first_user_text(c[0], deref) == hit.get("prompt"):
|
|
44
|
+
return c
|
|
45
|
+
return convs[-1] if convs else None
|
|
46
|
+
|
|
47
|
+
def main():
|
|
48
|
+
ap = argparse.ArgumentParser()
|
|
49
|
+
ap.add_argument("--env", default="cn-prod", choices=["cn-prod", "cn-stage"])
|
|
50
|
+
ap.add_argument("--message", action="append", required=True, help="逐轮发送的消息(可多次)")
|
|
51
|
+
ap.add_argument("--answer", action="append", help="反问预设答案 关键词=答案(可多次)")
|
|
52
|
+
ap.add_argument("--expect", default="", help="关注点,喂给判定层")
|
|
53
|
+
ap.add_argument("--user", required=True, help="测试账号 userId(拉 wire 用)")
|
|
54
|
+
ap.add_argument("--out", default="e2e-out")
|
|
55
|
+
ap.add_argument("--timeout", type=int, default=180,
|
|
56
|
+
help="每轮等回复超时秒;长任务(简报/多工具)调大到 500+")
|
|
57
|
+
ap.add_argument("--since", type=int, default=1, help="拉 wire 的天数窗口(默认 1)")
|
|
58
|
+
ap.add_argument("--issue-repo", default="Optima-Chat/optima-gateway")
|
|
59
|
+
args = ap.parse_args()
|
|
60
|
+
|
|
61
|
+
if args.env == "cn-stage":
|
|
62
|
+
print("[warn] cn-stage 的 wire 取法未验证(spec §8 待核实);仅 cn-prod 全链路已打通。", file=sys.stderr)
|
|
63
|
+
|
|
64
|
+
os.makedirs(args.out, exist_ok=True)
|
|
65
|
+
import preflight, chat_driver, pull_wire, prep_conversation
|
|
66
|
+
pf = preflight.summarize_preflight(preflight.probe(args.env))
|
|
67
|
+
if not pf["ok"]:
|
|
68
|
+
print(pf["report"]); print("\n[preflight 未通过,先按提示准备环境]"); sys.exit(2)
|
|
69
|
+
|
|
70
|
+
started_ts = _utc_now()
|
|
71
|
+
answers = _parse_answers(args.answer)
|
|
72
|
+
d = chat_driver.ChatDriver().attach()
|
|
73
|
+
try:
|
|
74
|
+
d.new_conversation()
|
|
75
|
+
turns = []
|
|
76
|
+
for msg in args.message:
|
|
77
|
+
r = d.send_and_wait(msg, answers=answers, timeout=args.timeout)
|
|
78
|
+
turns.append({"sent": msg, "state": r.get("state"), "transcript": r.get("transcript", ""),
|
|
79
|
+
"tool_trace": r.get("tool_trace"), "timed_out": r.get("timed_out", False)})
|
|
80
|
+
finally:
|
|
81
|
+
d.close()
|
|
82
|
+
|
|
83
|
+
wire_root = pull_wire.pull(args.user, since_days=args.since, out=args.out)
|
|
84
|
+
meta = {"env": args.env, "started_ts": started_ts, "first_message": args.message[0],
|
|
85
|
+
"expect": args.expect, "issue_repo": args.issue_repo, "turns": turns,
|
|
86
|
+
"located": None, "located_reason": None}
|
|
87
|
+
wrote_prepped = False
|
|
88
|
+
if not wire_root:
|
|
89
|
+
meta["located_reason"] = "no_wire_session"
|
|
90
|
+
if wire_root:
|
|
91
|
+
index = pull_wire.emit_conversation_index(wire_root)
|
|
92
|
+
hit = pull_wire.locate_conversation(index, started_ts, args.message[0])
|
|
93
|
+
meta["located"] = hit
|
|
94
|
+
if hit:
|
|
95
|
+
sdir = os.path.join(wire_root, hit["sid"])
|
|
96
|
+
deref = pull_wire.make_deref(sdir)
|
|
97
|
+
with open(os.path.join(sdir, "records.jsonl"), encoding="utf-8") as f:
|
|
98
|
+
recs = [json.loads(l) for l in f if l.strip()]
|
|
99
|
+
reqs = [x for x in recs if x.get("kind") == "request"]
|
|
100
|
+
resps = {x.get("callId"): x for x in recs if x.get("kind") in ("response", "error")}
|
|
101
|
+
convs = pull_wire.segment(reqs, deref)
|
|
102
|
+
conv = select_conversation_in_session(convs, deref, hit)
|
|
103
|
+
if conv is not None:
|
|
104
|
+
_, _, _, _, wire_md = pull_wire.render_conversation(conv, resps, deref, hit["gidx"])
|
|
105
|
+
prepped = prep_conversation.merge_browser_evidence(wire_md, turns)
|
|
106
|
+
with open(os.path.join(args.out, "prepped.md"), "w", encoding="utf-8") as f:
|
|
107
|
+
f.write(prepped)
|
|
108
|
+
wrote_prepped = True
|
|
109
|
+
else:
|
|
110
|
+
meta["located_reason"] = "no_match"
|
|
111
|
+
with open(os.path.join(args.out, "meta.json"), "w", encoding="utf-8") as f:
|
|
112
|
+
f.write(json.dumps(meta, ensure_ascii=False, indent=2))
|
|
113
|
+
if wrote_prepped:
|
|
114
|
+
print(f"[done] 备料稿 → {args.out}/prepped.md 元数据 → {args.out}/meta.json")
|
|
115
|
+
print("下一步:Claude 用 judge_workflow.js 判定,confirmed 提 issue 到", args.issue_repo)
|
|
116
|
+
else:
|
|
117
|
+
reason = "未拉到本账号 wire session(TTL/该窗口没跑过)" if not wire_root else "未能在 wire 里定位到本次对话(ts/prompt 未匹配)"
|
|
118
|
+
print(f"[warn] 只写了 {args.out}/meta.json,未生成 prepped.md:{reason}")
|
|
119
|
+
print(" 检查测试账号 userId / --since 窗口 / 前端是否真落 wire。")
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
main()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""比对 vendored 文件与上游是否漂移。上游 repo 不在本机则跳过(不破坏自包含)。"""
|
|
3
|
+
import os, subprocess
|
|
4
|
+
|
|
5
|
+
# adapted=True 的文件在上游基础上有本地改动/追加,diff 必然 ≠0 → ⚠️ 是「去看上游有无新变更」而非缺陷;
|
|
6
|
+
# adapted=False 是逐字 vendored,应与上游一致,⚠️ 才是真漂移。
|
|
7
|
+
VENDORED = [
|
|
8
|
+
{"file": "chat_driver.py", "repo": "store-skills", "adapted": False,
|
|
9
|
+
"upstream": "~/optima-store-skills/.claude/skills/operating-yzsgo-chat/chat_driver.py"},
|
|
10
|
+
{"file": "pull_wire.py", "repo": "store-skills", "adapted": True, # 追加了 emit_conversation_index/locate_conversation
|
|
11
|
+
"upstream": "~/optima-store-skills/.claude/skills/pulling-yzsgo-session-wire/pull_wire.py"},
|
|
12
|
+
{"file": "prep_conversation.py", "repo": "gateway", "adapted": True,
|
|
13
|
+
"upstream": "~/optima-gateway/.claude/skills/conversation-iq/prep_session.py"},
|
|
14
|
+
{"file": "judge_workflow.js", "repo": "gateway", "adapted": True,
|
|
15
|
+
"upstream": "~/optima-gateway/.claude/skills/conversation-iq/workflow.js"},
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
def plan_drift_checks(present: dict) -> list:
|
|
19
|
+
out = []
|
|
20
|
+
for v in VENDORED:
|
|
21
|
+
out.append({"file": v["file"], "upstream": v["upstream"],
|
|
22
|
+
"adapted": v.get("adapted", False),
|
|
23
|
+
"checkable": bool(present.get(v["repo"], False))})
|
|
24
|
+
return out
|
|
25
|
+
|
|
26
|
+
def _present() -> dict:
|
|
27
|
+
return {"store-skills": os.path.isdir(os.path.expanduser("~/optima-store-skills")),
|
|
28
|
+
"gateway": os.path.isdir(os.path.expanduser("~/optima-gateway"))}
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
32
|
+
for c in plan_drift_checks(_present()):
|
|
33
|
+
if not c["checkable"]:
|
|
34
|
+
print(f"⏭ {c['file']}:上游不在本机,跳过"); continue
|
|
35
|
+
up = os.path.expanduser(c["upstream"])
|
|
36
|
+
r = subprocess.run(["diff", "-q", os.path.join(here, c["file"]), up], capture_output=True, text=True)
|
|
37
|
+
if r.returncode == 0:
|
|
38
|
+
print(f"✅ {c['file']} 与上游一致")
|
|
39
|
+
elif c.get("adapted"):
|
|
40
|
+
print(f"⚠️(改编·预期) {c['file']}:与上游有差异属正常,去看上游有无值得同步的新变更")
|
|
41
|
+
else:
|
|
42
|
+
print(f"⚠️ 漂移 {c['file']} vs {c['upstream']}(逐字 vendored,应一致 → 去同步)")
|