@optima-chat/dev-skills 0.16.7 → 0.16.9

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.
Files changed (32) hide show
  1. package/.claude/commands/logs.md +1 -1
  2. package/.claude/commands/query-db.md +6 -1
  3. package/.claude/skills/discount-codes/SKILL.md +4 -4
  4. package/.claude/skills/generate-test-token/SKILL.md +8 -4
  5. package/.claude/skills/logs/SKILL.md +5 -3
  6. package/.claude/skills/show-env/SKILL.md +1 -1
  7. package/.claude/skills/yzsgo-e2e/SKILL.md +24 -0
  8. package/.claude/skills/yzsgo-e2e/SYNC.md +64 -2
  9. package/.claude/skills/yzsgo-e2e/chat_driver.py +1070 -98
  10. package/.claude/skills/yzsgo-e2e/pull_wire.py +39 -8
  11. package/.claude/skills/yzsgo-e2e/run_e2e.py +24 -3
  12. package/.codex/skills/generate-test-token/SKILL.md +3 -2
  13. package/.codex/skills/logs/SKILL.md +4 -2
  14. package/.codex/skills/show-env/SKILL.md +4 -2
  15. package/.codex/skills/yzsgo-e2e/SKILL.md +24 -0
  16. package/.codex/skills/yzsgo-e2e/SYNC.md +64 -2
  17. package/.codex/skills/yzsgo-e2e/chat_driver.py +1070 -98
  18. package/.codex/skills/yzsgo-e2e/pull_wire.py +39 -8
  19. package/.codex/skills/yzsgo-e2e/run_e2e.py +24 -3
  20. package/AGENTS.md +1 -1
  21. package/bin/cli.js +3 -3
  22. package/bin/helpers/billing-http.ts +12 -3
  23. package/bin/helpers/cn-deploy.ts +2 -1
  24. package/bin/helpers/db-utils.ts +8 -5
  25. package/bin/helpers/generate-test-token.ts +40 -3
  26. package/bin/helpers/verify-health.ts +77 -29
  27. package/dist/bin/helpers/billing-http.js +9 -1
  28. package/dist/bin/helpers/cn-deploy.js +2 -1
  29. package/dist/bin/helpers/db-utils.js +8 -2
  30. package/dist/bin/helpers/generate-test-token.js +40 -3
  31. package/dist/bin/helpers/verify-health.js +83 -27
  32. package/package.json +1 -1
@@ -169,6 +169,19 @@ def render_conversation(conv, resps, deref, idx):
169
169
  res_ids.add(b.get("tool_use_id"))
170
170
  dangling = [(u, n) for u, n in use_ids.items() if u not in res_ids]
171
171
 
172
+ # 末 response 状态:审查靠它判「最终回复能不能核」。缺失时**必须显式标注**,
173
+ # 否则 transcript 静默停在末条 user 消息(常是超长 tool_result 截断处),
174
+ # 会被误读成「响应被截断吞了」(上游 #102 真根因:不是截断,是末 req 无 response 记录)。
175
+ lastresp = resps.get(last["callId"])
176
+ if not lastresp:
177
+ last_state = "⚠️ 末 request 无 response 记录(生成中断/未落盘/末轮是 continuation)——最终回复不可核"
178
+ elif lastresp.get("kind") == "error":
179
+ last_state = "⚠️ 末 response 是 error:" + json.dumps(deref(lastresp.get("error") or {}), ensure_ascii=False)[:160]
180
+ elif (lastresp.get("finalMessage") or {}).get("stopReason") == "max_tokens":
181
+ last_state = "⚠️ 末 response stop_reason=max_tokens(回复被截断,可能未说完)"
182
+ else:
183
+ last_state = "有(正常 response)"
184
+
172
185
  L = [f"# 对话 #{idx} {ts0}",
173
186
  "", f"**prompt**: {prompt[:200]}", "",
174
187
  "## 事实卡(代码算的确定信息)",
@@ -176,9 +189,9 @@ def render_conversation(conv, resps, deref, idx):
176
189
  f"- error 响应 {len(errs)}" + (f":{[json.dumps(deref(e.get('error') or {}),ensure_ascii=False)[:120] for e in errs]}" if errs else ""),
177
190
  f"- stop_reason=max_tokens 的响应 {maxtok}",
178
191
  f"- 悬空 tool_use(无匹配 result){len(dangling)}: {dangling[:5]}",
192
+ f"- 末 response: {last_state}",
179
193
  "", "## 完整 transcript(末 req 全历史 + 末 response)", ""]
180
- # 末 response 拼到历史尾
181
- lastresp = resps.get(last["callId"])
194
+ # 末 response 拼到历史尾(仅 response 类才有 finalMessage 内容可拼)
182
195
  if lastresp and lastresp.get("kind") == "response":
183
196
  fm = lastresp.get("finalMessage") or {}
184
197
  msgs.append({"role": "assistant", "content": fm.get("content", [])})
@@ -190,8 +203,15 @@ def render_conversation(conv, resps, deref, idx):
190
203
  L.append(" " + _clip(c, CAP_TEXT))
191
204
  elif isinstance(c, list):
192
205
  for b in c:
193
- L.append(render_block(b, deref))
194
- return prompt, ts0, len(errs), len(dangling), "\n".join(L)
206
+ try:
207
+ L.append(render_block(b, deref))
208
+ except Exception as e: # 单个块渲染失败不该中断后续消息(上游 #102 加固)
209
+ L.append(f" [渲染失败 {type(e).__name__}: {str(e)[:80]}]")
210
+ # transcript 结尾显式收口——末 response 非正常时补一行,让审查者绝不把静默结束当完整
211
+ resp_ok = bool(lastresp and lastresp.get("kind") == "response")
212
+ if not resp_ok:
213
+ L.append(f"\n--- [transcript 结束] {last_state} ---")
214
+ return prompt, ts0, len(errs), len(dangling), resp_ok, "\n".join(L)
195
215
 
196
216
 
197
217
  def render_all(wire_root, out):
@@ -214,10 +234,10 @@ def render_all(wire_root, out):
214
234
  index.append(f"\n## session `{sid}` — {len(convs)} 对话 / {len(reqs)} req\n")
215
235
  for conv in convs:
216
236
  gidx += 1
217
- prompt, ts0, n_err, n_dang, md = render_conversation(conv, resps, deref, gidx)
237
+ prompt, ts0, n_err, n_dang, resp_ok, md = render_conversation(conv, resps, deref, gidx)
218
238
  fn = f"{gidx:03d}.md"
219
239
  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 "")
240
+ flag = (" ⚠️err" if n_err else "") + (" ⚠️dangling" if n_dang else "") + ("" if resp_ok else " ⚠️无末response")
221
241
  index.append(f"- [{fn}](conversations/{fn}) [{len(conv):>2} req] {prompt[:64]}{flag}")
222
242
  idxpath = os.path.join(out, "index.md")
223
243
  open(idxpath, "w", encoding="utf-8").write("\n".join(index))
@@ -278,9 +298,20 @@ def _ts_key(ts):
278
298
  return None
279
299
 
280
300
 
281
- def locate_conversation(index, started_ts, first_message):
301
+ def locate_conversation(index, started_ts, first_message, session_id=None):
282
302
  """按 (started_ts, first_message) 定位本次对话;禁用 ls -t。规则见 plan Task 3 Interfaces。
283
- 时间比较先把两侧 ISO(Z / +00:00、小数位宽不同)归一成 datetime,避免依赖字典序=数值序的脆弱假设。"""
303
+ 时间比较先把两侧 ISO(Z / +00:00、小数位宽不同)归一成 datetime,避免依赖字典序=数值序的脆弱假设。
304
+
305
+ `session_id`(2026-09-09 起,鸭嘴兽支持并发任务后):驱动侧现在知道自己跑在**哪个 gateway
306
+ session** 上(一 tab 一 session,wire 目录名就是 sessionId),传进来就先把候选缩到该 session。
307
+ 这是**确定性**定位,比 (时间, 首句) 的启发式可靠得多 —— 同一句 prompt 重跑多次时,
308
+ 启发式只能靠时间戳挑,而并发跑的多个会话时间戳本来就交叠。
309
+ 传了但该 session 在索引里一条都没有(wire 还没落盘/TTL 过期/拉的账号不对)→ **不回退**到
310
+ 全局启发式:那样会安静地定位到别的 session 的对话,比返回 None 更糟。"""
311
+ if session_id:
312
+ index = [it for it in index if it.get("sid") == session_id]
313
+ if not index:
314
+ return None
284
315
  key = (first_message or "").strip()[:40]
285
316
  cands = []
286
317
  for it in index:
@@ -85,7 +85,21 @@ def main():
85
85
 
86
86
  started_ts = _utc_now()
87
87
  answers = _parse_answers(args.answer)
88
- d = chat_driver.ChatDriver().attach()
88
+ # attach() 默认就自己开 tab 独占一个 gateway session("auto" 档)。两个好处——
89
+ # ① 不去抢用户已经开着的 chat tab(抢了会互相串台,见 chat_driver 模块 docstring);
90
+ # ② 拿到确定的 sessionId,后面按它**确定性**定位 wire 对话,不靠 (时间,首句) 猜。
91
+ # 该环境 multi-tab 没开(flag 默认关,cn-stage 未验)时 auto 档自己会降级复用已有 tab
92
+ # 并打 warn —— 单 driver 复用是安全的,功能不减,只是 wire 定位退回启发式。
93
+ # ziniao 是 attach() 的必填关键字参数(上游 store-skills #611,漏传即 TypeError):
94
+ # 本 skill 测的是鸭嘴兽对话链路,这个 tab 不绑定任何紫鸟 profile,所以显式声明 None + 理由。
95
+ # 若 --message 正文里写了 `--ziniao-profile <id>`,send() 会抛 ZiniaoDeclarationConflict
96
+ # 拒发 —— 那是上游有意的闸,不是本脚本的 bug。装到 ~/.claude 后已知 profile 表为空
97
+ # (它读 store-skills 仓库里的 e2e/registry*.yaml),所以只认 `--ziniao-profile` 这种显式写法。
98
+ d = chat_driver.ChatDriver().attach(
99
+ ziniao=None, reason="yzsgo-e2e 驱动鸭嘴兽网页对话做端到端测试,本脚本不绑定任何紫鸟 profile")
100
+ session_id = d.session_id # attach 后立刻取:放在 try 里的话,中途抛异常会留下未绑定名
101
+ if not d.tab_isolated:
102
+ print("[warn] 未能独占 tab(该环境 multi-tab 未开)—— wire 定位退回 (时间,首句) 启发式")
89
103
  try:
90
104
  d.new_conversation()
91
105
  turns = []
@@ -99,13 +113,17 @@ def main():
99
113
  wire_root = pull_wire.pull(args.user, since_days=args.since, out=args.out)
100
114
  meta = {"env": args.env, "started_ts": started_ts, "first_message": args.message[0],
101
115
  "expect": args.expect, "issue_repo": args.issue_repo, "turns": turns,
116
+ "session_id": session_id, # 本轮跑在哪个 gateway session(= wire 目录名)
102
117
  "located": None, "located_reason": None}
103
118
  wrote_prepped = False
104
119
  if not wire_root:
105
120
  meta["located_reason"] = "no_wire_session"
106
121
  if wire_root:
107
122
  index = pull_wire.emit_conversation_index(wire_root)
108
- hit = pull_wire.locate_conversation(index, started_ts, args.message[0])
123
+ hit = pull_wire.locate_conversation(index, started_ts, args.message[0],
124
+ session_id=session_id)
125
+ if session_id and not hit:
126
+ meta["located_reason"] = f"session {session_id} 在 wire 索引里没有对话(未落盘/TTL 过期/账号不对)"
109
127
  meta["located"] = hit
110
128
  if hit:
111
129
  sdir = os.path.join(wire_root, hit["sid"])
@@ -117,7 +135,10 @@ def main():
117
135
  convs = pull_wire.segment(reqs, deref)
118
136
  conv = select_conversation_in_session(convs, deref, hit)
119
137
  if conv is not None:
120
- _, _, _, _, wire_md = pull_wire.render_conversation(conv, resps, deref, hit["gidx"])
138
+ # 6 元返回(上游 #102 起多了 resp_ok):末 response 不正常时 wire_md 结尾已显式收口,
139
+ # 但 meta 也记一份,报告层不必去 grep transcript 才知道「最终回复不可核」。
140
+ _, _, _, _, resp_ok, wire_md = pull_wire.render_conversation(conv, resps, deref, hit["gidx"])
141
+ meta["wire_last_response_ok"] = resp_ok
121
142
  prepped = prep_conversation.merge_browser_evidence(wire_md, turns)
122
143
  with open(os.path.join(args.out, "prepped.md"), "w", encoding="utf-8") as f:
123
144
  f.write(prepped)
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: "generate-test-token"
3
- description: "Use when the user needs a test merchant account, an access token for API testing, or a temporary account for CI, Stage, or Prod verification."
3
+ description: "Use when the user needs a test merchant account, an access token for API testing, or a temporary account for CI, Stage, Prod, cn-prod, or cn-stage verification."
4
4
  ---
5
5
 
6
6
  # Generate Test Access Tokens
@@ -19,6 +19,7 @@ optima-generate-test-token [options]
19
19
  optima-generate-test-token
20
20
  optima-generate-test-token --env stage
21
21
  optima-generate-test-token --business-name "Demo Shop" --env prod
22
+ optima-generate-test-token --env cn-stage
22
23
  ```
23
24
 
24
25
  ## Guidance
@@ -26,7 +27,7 @@ optima-generate-test-token --business-name "Demo Shop" --env prod
26
27
  - Default to `ci`.
27
28
  - The command handles merchant registration, OAuth token creation, and merchant profile setup.
28
29
  - The command writes the token to a temporary file; report that path back to the user.
29
- - For `prod`, remind the user that the created account will exist in the production system.
30
+ - For `prod` and `cn-prod`, remind the user that the created account will exist in the production system.
30
31
 
31
32
  ## Follow-up
32
33
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: "logs"
3
- description: "Use when the user asks to inspect service logs, debug runtime failures, or compare CI, Stage, and Prod logs for Optima services."
3
+ description: "Use when the user asks to inspect service logs, debug runtime failures, or compare logs across CI, Stage, Prod, cn-prod, and cn-stage for Optima services."
4
4
  ---
5
5
 
6
6
  # Inspect Service Logs
@@ -14,6 +14,7 @@ Use the local shell and follow the environment-specific workflow:
14
14
  - `ci`: SSH to the shared CI host and read Docker Compose logs
15
15
  - `stage`: read AWS CloudWatch logs from `/ecs/<service>-stage`
16
16
  - `prod`: read AWS CloudWatch logs from `/ecs/<service>-prod` with `--region ap-southeast-1`
17
+ - `cn-prod` / `cn-stage`: Alibaba Cloud SAE (cn-beijing) — use `optima-logs <service> --env cn-prod|cn-stage`, which queries SLS directly (no buildbox hop)
17
18
 
18
19
  ## Common Services
19
20
 
@@ -29,7 +30,8 @@ Use the local shell and follow the environment-specific workflow:
29
30
 
30
31
  ## Guidance
31
32
 
32
- - Default to `ci` unless the user clearly requests `stage` or `prod`.
33
+ - Default to `ci` unless the user clearly requests another environment — note `ci` is the SSH + Docker Compose path, **not** `optima-logs`.
34
+ - 🔴 `optima-logs` with no `--env` defaults to **`cn-prod` (Alibaba Cloud production)**, not `ci` (see `bin/helpers/logs.ts`). Always pass `--env` explicitly, or you will be reading production logs while believing you are reading CI.
33
35
  - For `prod`, keep the query narrow and use the exact service the user asked for.
34
36
  - When investigating an error, read enough context around the failure instead of only grepping a single line.
35
37
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: "show-env"
3
- description: "Use when the user asks to inspect environment variables, service configuration, or Infisical-backed settings for Stage or Prod."
3
+ description: "Use when the user asks to inspect environment variables, service configuration, or Infisical-backed settings for Stage, Prod, cn-prod, or cn-stage."
4
4
  ---
5
5
 
6
6
  # Inspect Environment Configuration
@@ -10,7 +10,7 @@ Use this skill to inspect current shell environment variables or service configu
10
10
  ## Preferred Command
11
11
 
12
12
  ```bash
13
- optima-show-env <service> <stage|prod> [options]
13
+ optima-show-env <service> <stage|prod|cn-prod|cn-stage> [options]
14
14
  ```
15
15
 
16
16
  ## Common Options
@@ -18,6 +18,7 @@ optima-show-env <service> <stage|prod> [options]
18
18
  ```bash
19
19
  optima-show-env commerce-backend stage --filter DATABASE
20
20
  optima-show-env user-auth prod --keys-only
21
+ optima-show-env gateway-core cn-prod --filter REDIS
21
22
  ```
22
23
 
23
24
  ## Guidance
@@ -25,3 +26,4 @@ optima-show-env user-auth prod --keys-only
25
26
  - For local shell variables, simple shell commands like `env` or `echo $VAR` are enough.
26
27
  - For service configuration, prefer `optima-show-env` over raw Infisical API calls.
27
28
  - If the user only needs key names, use `--keys-only` to avoid exposing values unnecessarily.
29
+ - `cn-prod` / `cn-stage` read the separate Alibaba Cloud cn Infisical instance and need `INFISICAL_CN_EMAIL` / `INFISICAL_CN_PASSWORD` (or a `~/.infisical_cn_creds` file).
@@ -27,6 +27,30 @@ allowed-tools: ["Bash", "Read", "Write", "Agent", "Workflow"]
27
27
  - 每个 ✋ 缺项:引导用户——`chrome-9222`:`python3 $S/bootstrap.py launch-chrome` 起窗口、让用户**手动登测试账号**(登一次长期免登);`buildbox-pw`:让用户把口令放 `~/.buildbox_pw`(内部拉 wire 用,向团队要);`test-user-id`:让用户登录 Optima(run_e2e 自动从 `~/.optima/token.json` 读 userId)。
28
28
  - 补完 `python3 $S/preflight.py <env>` 复检,直到全 ✅ 才进四段。
29
29
 
30
+ ## 并发(2026-09-09 起):本 skill 自己开 tab
31
+
32
+ 鸭嘴兽支持并发任务了 —— **一个浏览器 tab = 一个独立 gateway session**(一 tab 一 session,
33
+ 跨 tab 可真并行,同 tab 内仍串行;上限按 plan:free 1 / starter 2 / pro 4 / enterprise 20)。
34
+
35
+ `chat_driver.attach()` **默认就自己开一个 tab**(`own_tab="auto"`)——平台既然支持多 tab,
36
+ 独占一个 session 就是常态,共用别人的 tab 才是例外。所以 `run_e2e.py` 不传 `own_tab`、用默认档即可,
37
+ 不去抢用户已经开着的 chat tab。两个好处——① 不跟用户/别的跑测互相串台;② 拿到确定的 `sessionId`,
38
+ `locate_conversation(..., session_id=...)` 按它**确定性**定位 wire 对话,不再靠 (时间, 首句) 猜
39
+ (并发跑时多个会话时间戳交叠,启发式会挑错)。`meta.json` 里多了 `session_id` 字段。
40
+
41
+ 该环境 multi-tab flag 没开(`NEXT_PUBLIC_MULTI_TAB_SESSION`,build-time、**默认关**;
42
+ cn-prod 已验开、cn-stage 未验)时 `"auto"` 档自己会降级复用已有 tab(`d.tab_isolated=False`)
43
+ 并打 warn,功能不减,只是 wire 定位退回启发式。
44
+
45
+ 🔴 **`attach()` 必须声明紫鸟 profile**(2026-09-17 同步上游 #611 起):`ziniao` 是必填关键字参数,
46
+ 漏传直接 `TypeError`;`ziniao=None` 必须带非空 `reason`,否则 `ValueError`。`run_e2e.py` 传的是
47
+ `ziniao=None` + 理由,因为本 skill 测对话链路、不绑定任何店。**自己写脚本调 `chat_driver` 时照此声明。**
48
+ 消息正文里出现 `--ziniao-profile <id>` 而声明是 `None`,`send()` 会抛 `ZiniaoDeclarationConflict` 拒发。
49
+
50
+ 🔴 **绝不能让两个 driver 共用一个 tab**:实证会**静默串台**——两个线程写同一个 textarea,
51
+ 后写的覆盖先写的,只有一条消息真到服务端,两边却都抓到同一份回复,还全程无报错。
52
+ 细节见 `chat_driver.py` 模块 docstring。
53
+
30
54
  ## 四段流程
31
55
  1. **preflight + 自举**:见上「环境自举」,**全绿**才继续。
32
56
  2. **驱动 + 拉 wire + 备料**:`$VENV/bin/python $S/run_e2e.py --env <env> --message ... [--answer k=v] [--expect ...] --out <dir>` → 出 `prepped.md` + `meta.json`。🔑 用 **venv 的 python**(playwright 在那儿);`--user` 不给则自动读 `~/.optima/token.json`。
@@ -2,9 +2,71 @@
2
2
 
3
3
  | 本文件 | 上游 repo · 路径 | commit | 同步日期 |
4
4
  |---|---|---|---|
5
- | chat_driver.py | optima-store-skills · .claude/skills/operating-yzsgo-chat/chat_driver.py | 7a1685a | 2026-08-31 |
6
- | pull_wire.py | optima-store-skills · .claude/skills/pulling-yzsgo-session-wire/pull_wire.py | 7a1685a | 2026-08-31 |
5
+ | chat_driver.py | optima-store-skills · .claude/skills/operating-yzsgo-chat/chat_driver.py | 010c578a(PR #1852,含 #1635) | 2026-09-17 |
6
+ | pull_wire.py | optima-store-skills · .claude/skills/pulling-yzsgo-session-wire/pull_wire.py | 3cfc2d6 | 2026-09-09 |
7
7
  | prep_conversation.py | optima-gateway · .claude/skills/conversation-iq/prep_session.py(改编:+浏览器证据合并) | b75f575c | 2026-08-31 |
8
8
  | judge_workflow.js | optima-gateway · .claude/skills/conversation-iq/workflow.js(改编:+前后端一致性维度) | b75f575c | 2026-08-31 |
9
9
 
10
10
  > 注:judge_workflow.js 内联的 decideOutcome 是 judge_outcome.js(有 node 单测)的副本,改逻辑需同步两处。
11
+
12
+ ## 2026-09-17 这次同步带了什么
13
+
14
+ `chat_driver.py` 逐字取自上游 `010c578a`(optima-store-skills#1852 合入 main 的那个 merge commit;
15
+ 同步时上游 main 上该文件与它逐字节一致)。`3cfc2d6..010c578a` 之间上游动了这个文件 25 次,对本仓有影响的三组:
16
+
17
+ - **#1635 多张 question-card**:活卡按「有 確認/下一題/補充回答 按钮」认,不再取第一张可见卡;
18
+ `wait_reply` 回执带卡片原文与来源。这是本次同步的直接动机。
19
+ - **#611 / #666 紫鸟 profile 声明闸**:`attach(*, ziniao, reason=None, ...)` 的 `ziniao` 变成**必填**,
20
+ 漏传 `TypeError`;`ziniao=None` 无理由 `ValueError`。`send()` / `send_and_wait()` 多了可选的
21
+ `ziniao=` 按条声明,并对账正文点名的 profile,不符抛 `ZiniaoDeclarationConflict`。
22
+ 互斥锁本身 2026-09-14 已撤,只剩声明 + 对账;每次放行/拦截追加到 `~/.optima-locks/ziniao-gate.jsonl`(写失败不抛)。
23
+ - **#870 本轮开的 tab 本轮关**:新增 `keep_tab_for_human(reason)`(停手交人时保留 tab)和 `with` 用法
24
+ (`__enter__` / `__exit__`)。`close()` 只关自己开的 tab 是 09-09 那版就有的行为,不是这次新增。
25
+
26
+ **没接的**:`wait_reply` 新增的 `question` 回执(#1635,用来区分「读不到卡片」和「根本没问」)没有写进
27
+ `run_e2e.py` 的 turns / `meta.json`,判定层看不到它;要用得另改。`tool_trace` 末尾追加的十来个字段不影响现有读取。
28
+
29
+ **新增副作用**:每次 `attach()` 都会建 `~/.optima-locks/` 并往 `ziniao-gate.jsonl` 追加一行(写失败不抛)。
30
+ 驱动报错提示里的 `scripts/ziniao-gate-report.py` 在 store-skills 仓库,本仓没有。
31
+
32
+ **本仓为此做的适配**(驱动本身仍逐字,不改):
33
+
34
+ 1. `run_e2e.py` 改传 `attach(ziniao=None, reason=…)`。🔴 **不改会在第一步就崩**——2026-09-16 曾有人只把新驱动手工拷进
35
+ `~/.claude/skills/yzsgo-e2e/` 而没改调用方,结果本机这个 skill 一直是坏的。
36
+ 2. 新增 `tests/yzsgo-e2e/test_attach_declaration.py`:从驱动的 ast 重建 `attach()` 的参数表,
37
+ 把本 skill 每处 `attach()` 调用用 `inspect.Signature.bind` 绑一遍 ⇒ 缺必填参数、传了驱动不收的关键字、
38
+ 位置参数不对,都会红。不需要 playwright。
39
+ 原有的 `test_imports.py` 只在装了 playwright 时才导入驱动,而且只查 `ChatDriver` 类存在,挡不住签名变化。
40
+ 3. `test.yml` 加一步 `python3 -m unittest discover -s tests/yzsgo-e2e`。🔴 **此前 CI 只跑 `npm test`,
41
+ `tests/yzsgo-e2e/*.py` 一条都不跑**,这些测试只在有人手动跑时才生效。runner 与 optima-store-skills
42
+ 的 `publish-plugin.yml` 同一个标签,那边直接用 `python3`。
43
+ 4. ⚠️ **装好后已知 profile 表恒为空**:`known_profile_ids()` 读的是「驱动所在目录再往上三层」的
44
+ `e2e/registry*.yaml`,那是 store-skills 仓库的布局;装到 `~/.claude/skills/yzsgo-e2e` 算出来是 `~`,
45
+ 装到 `~/.codex/skills/optima-dev/yzsgo-e2e` 算出来是 `~/.codex`,都读不到 ⇒ 对账只认
46
+ `--ziniao-profile <id>` 这种显式写法,裸 14 位 profile id 认不出来。不影响本 skill(本来就声明 `None`)。
47
+
48
+ `pull_wire.py`:核过上游 `3cfc2d6..origin/main` 该文件无新提交,不用动。
49
+
50
+ ## 2026-09-09 这次同步带了什么
51
+
52
+ 上游把 e2e 改成**并发跑**(鸭嘴兽支持并发任务:一个 tab = 一个独立 gateway session)。
53
+ `chat_driver.py` 逐字取自上游 `3cfc2d6`(optima-store-skills#206,已合入 main),本次变更点:
54
+
55
+ - `attach()` **默认自己开 tab** 独占 session(`own_tab="auto"`;拿不到独立 session 就降级复用
56
+ 已有 tab 并置 `tab_isolated=False`)。`own_tab=True` 是严格档,并行必用——auto 档多 worker
57
+ 一起降级会都落到同一个 tab 上 = 静默串台。
58
+ - `session_id` / `close()` 只关自己开的 tab / `concurrency_status()` 读并发名额。
59
+ - send 拒绝分流从三码扩到五码(新增 `concurrency_limit` / `busy_elsewhere`)。
60
+
61
+ `pull_wire.py` 是**改编**(不逐字)。这次做了两件事:
62
+
63
+ 1. **补回漏掉的上游修复 #102**(`bfa103c`「末 response 缺失显式标注,不再静默截断」)——
64
+ vendored 副本从 2026-08-31 起就没跟上,一直在静默截断。上游的 4 条回归也一并移植到
65
+ `tests/yzsgo-e2e/test_pull_wire_render.py`(另加一条锁 `render_conversation` 的 6 元 arity,
66
+ 因为 `run_e2e.py` 按 6 元解包,少一个会在出报告前一步崩)。
67
+ ⚠️ 教训:`verify_drift.py` 对改编文件只标「⚠️(改编·预期)」,**看到这个标记要真的去读上游有什么新提交**,
68
+ 不能当成「预期差异」放过——这次就是那么漏掉半个月的。
69
+ 2. 自行加了 `locate_conversation(..., session_id=...)`:
70
+ 按 gateway sessionId **确定性**定位本轮对话,取代 (时间, 首句) 启发式 —— 并发跑时多个会话
71
+ 时间戳交叠,启发式会挑错。传了 sid 却命不中就返回 None,**不静默回退**全局启发式
72
+ (回退会安静地定位到别的 session 的对话)。上游没有这个函数,是本仓独有的腿。