tianshu-mcp 0.7.5 → 0.7.6
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/CHANGELOG.en.md +57 -0
- package/CHANGELOG.md +39 -0
- package/README.en.md +20 -15
- package/README.md +20 -15
- package/dist/agents/zcode/cdp.js +128 -22
- package/dist/agents/zcode/model.js +100 -2
- package/dist/agents/zcode/run.js +244 -26
- package/dist/agents/zcode/selectors.js +22 -4
- package/dist/mcp/handlers.js +11 -3
- package/dist/version.generated.js +1 -1
- package/package.json +1 -1
- package/skills/tianshu-mcp/SKILL.md +3 -1
package/CHANGELOG.en.md
CHANGED
|
@@ -8,6 +8,63 @@ Chinese version: [CHANGELOG.md](CHANGELOG.md)
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## [0.7.6] - 2026-09-30
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **Three ZCode adapter defects (issue #27), reproduced and fixed on real ZCode `3.14.3.7762` (Windows)**:
|
|
16
|
+
- **Split project-collection channels causing a binding deadlock**: on 3.14.3 the
|
|
17
|
+
`[data-testid^="workspace-item-"]` nodes are **not gone from the DOM** — they are scrolled out of
|
|
18
|
+
the viewport instead (measured on the real machine: **40 of 42 nodes invisible**). The old
|
|
19
|
+
implementation collected them without any visibility filter, so the ghost entries made
|
|
20
|
+
`if (!out.length)` permanently false and the only trustworthy channel (the dropdown menu) never
|
|
21
|
+
ran. `matchZcodeProject` then matched a ghost item, the working auto-import branch was skipped,
|
|
22
|
+
and the task died with `project_mismatch`. `projects()` now filters the legacy channel by
|
|
23
|
+
visibility, **always merges both channels**, and lets same-named menu items override sidebar
|
|
24
|
+
entries (only the menu's `aria-checked` is binding evidence rendered by ZCode itself).
|
|
25
|
+
- **Fall back to import when clicks never land**: `clickProject` now reports a distinguishable
|
|
26
|
+
reason (`trigger-unavailable` / `not-found` / `not-visible`). When the target is listed but not a
|
|
27
|
+
single click ever landed, the adapter falls back to the `selectZcodeFolder` import path instead
|
|
28
|
+
of declaring failure (still fail-closed when `allowCreateProject=false`).
|
|
29
|
+
- **Runtime CDP disconnects no longer declare death**: a single `Runtime.evaluate` timeout or an
|
|
30
|
+
endpoint hiccup does not mean CDP is dead (in the reported incident the agent kept writing
|
|
31
|
+
artifacts after MCP had already failed the task). The send phase and the runtime loop now share
|
|
32
|
+
one guard: the first disconnect reconnects **once, for observation only** (matching the existing
|
|
33
|
+
`codex`/`qoder` pattern — **never resending the task**); a failed reconnect or a second
|
|
34
|
+
disconnect lands on `needs_user(setup_recovery)` with both facts attached — whether the process
|
|
35
|
+
is still alive and whether the window still shows running signals — so "still running" is never
|
|
36
|
+
misread as "stopped".
|
|
37
|
+
- **`reasoningLevel` implemented end to end**: the tier set **varies per model**, so it is read
|
|
38
|
+
from the UI **after the model is confirmed** and validated. Out-of-range tiers and unreadable
|
|
39
|
+
tier sets are rejected **before sending** (`reasoning_level_invalid`) instead of silently reusing
|
|
40
|
+
the current value; omitting the tier never touches the UI. Real-machine contract:
|
|
41
|
+
trigger `chat-thought-level-select-trigger` (combobox), options
|
|
42
|
+
`chat-thought-level-select-item-{enabled,disabled}` (binary on/off), options mounted only while
|
|
43
|
+
the menu is open.
|
|
44
|
+
- **Root cause of the "two-level model menu is flaky" symptom**: model items live in the provider
|
|
45
|
+
group's **second-level submenu**, which renders only on **hover** — clicking selects the group
|
|
46
|
+
itself or collapses the menu. The provider group testid has also drifted to
|
|
47
|
+
`chat-model-select-group-registry-provider:`. Group expansion now uses hover
|
|
48
|
+
(`clickExact(..., "hover")`), and when two rounds both yield an empty candidate list the menu is
|
|
49
|
+
reopened for one more round instead of declaring `model_unavailable`.
|
|
50
|
+
- **Permission-menu contract drift (surfaced by the real-machine run, fixed as the same class of defect)**:
|
|
51
|
+
on 3.14.3 the permission items use `menuitemradio` / `menuitemcheckbox` (**not** `option`), and the
|
|
52
|
+
visible name lives in the item's **direct text node**, followed by an explanatory sentence
|
|
53
|
+
(e.g. "Full access — fewer confirmations."). Both mismatched the old implementation and stalled
|
|
54
|
+
dispatch at `permission_unknown`. The selector now carries a role-agnostic fallback, and
|
|
55
|
+
`clickExact` resolves labels from direct text nodes first.
|
|
56
|
+
|
|
57
|
+
### Real-machine verification
|
|
58
|
+
|
|
59
|
+
- `npm run probe:zcode -- dom-contracts`: `workspaceItems=42` (2 visible / 40 invisible),
|
|
60
|
+
`dataProjectPath=0`; with the menu open `projects()` returns the menu items with correct checked
|
|
61
|
+
state — independently reproducing both the issue's geometric premise and the fixed collection.
|
|
62
|
+
- `npm run probe:zcode -- models` / `permission` plus one-off probes: pinned the provider-group hover
|
|
63
|
+
semantics, the permission item role/text structure, and the binary tier shape
|
|
64
|
+
(`current=on`, `tiers=Off/On`).
|
|
65
|
+
- `npm run smoke:zcode`: end-to-end real-machine dispatch (project binding → model switch →
|
|
66
|
+
permission confirm → send → poll to terminal state).
|
|
67
|
+
|
|
11
68
|
## [0.7.5] - 2026-09-29
|
|
12
69
|
|
|
13
70
|
### Fixed
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,45 @@
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.7.6] - 2026-09-30
|
|
11
|
+
|
|
12
|
+
### 修复
|
|
13
|
+
|
|
14
|
+
- **ZCode 适配器三项缺陷(issue #27),已在真机 ZCode `3.14.3.7762`(Windows)上复现并修复**:
|
|
15
|
+
- **项目采集渠道分裂导致绑定死锁**:3.14.3 上 `[data-testid^="workspace-item-"]` **并未从 DOM 消失**,
|
|
16
|
+
只是被滚出视口(真机实测 **42 个节点中 40 个不可见**)。旧实现采集时不做可见性过滤,幽灵项入列使
|
|
17
|
+
`if (!out.length)` 短路恒为假,唯一可信的菜单渠道永不执行 → `matchZcodeProject` 命中幽灵项 →
|
|
18
|
+
可用的自动导入分支被跳过 → 任务卡死在 `project_mismatch`。现在 `projects()` 对旧契约项做可见性过滤、
|
|
19
|
+
两条渠道**始终合并**,且同名的菜单项覆盖侧边栏项(只有菜单的 `aria-checked` 是 ZCode 自己渲染的绑定证据)。
|
|
20
|
+
- **点击落空时回落导入**:`clickProject` 现在返回可区分的原因(`trigger-unavailable` / `not-found` /
|
|
21
|
+
`not-visible`);目标项在列表里却一次都没真正点中时,回落 `selectZcodeFolder` 导入路径而不是直接判死
|
|
22
|
+
(`allowCreateProject=false` 时仍在下一道闸门 fail-closed)。
|
|
23
|
+
- **运行期 CDP 断连不再判死**:单次 `Runtime.evaluate` 超时或端点抖动不等于 CDP 已死(issue 现场是
|
|
24
|
+
「MCP 判定失败后 agent 仍在写产物」)。发送阶段与运行期统一护栏:首次断连重连**观察一次**
|
|
25
|
+
(对齐 `codex`/`qoder` 的既有范式,重连只用于观察、**绝不重发任务**),重连失败或再次断连落
|
|
26
|
+
`needs_user(setup_recovery)`,并附「进程是否仍在」「窗口是否仍有运行信号」两侧事实,避免把「仍在跑」
|
|
27
|
+
误读成「已停」。
|
|
28
|
+
- **`reasoningLevel` 端到端实现**:档位集合**随模型变化**,因此在**模型确认之后**才读取界面实际渲染的选项
|
|
29
|
+
并校验;越权档位与「集合读不到」都在**发送前**报错(`reasoning_level_invalid`),绝不静默沿用;
|
|
30
|
+
未指定档位时完全不触碰界面。真机契约:触发器 `chat-thought-level-select-trigger`(combobox),
|
|
31
|
+
选项 `chat-thought-level-select-item-{enabled,disabled}`(二值 开启/关闭),选项只在菜单展开时挂载。
|
|
32
|
+
- **「两级模型菜单点击不稳」根因**:模型项在 provider 分组的**二级子菜单**里,**必须 hover 分组**才渲染,
|
|
33
|
+
用 click 会选中分组本身或收起菜单;provider 分组 testid 也已漂移为
|
|
34
|
+
`chat-model-select-group-registry-provider:`。现在展开分组改用 hover(`clickExact(..., "hover")`),
|
|
35
|
+
并在两轮候选皆空时把菜单重开一次再试一轮,不凭一轮空列表判 `model_unavailable`。
|
|
36
|
+
- **权限菜单契约漂移(真机测试暴露,同类问题一并修复)**:3.14.3 的权限项 role 是
|
|
37
|
+
`menuitemradio` / `menuitemcheckbox`(**不是** `option`),且可见名写在项内的**直接文本节点**里、
|
|
38
|
+
后面还跟一句说明(如「完全访问减少确认次数。」)。旧实现两处都会 0 命中,导致派发卡在 `permission_unknown`。
|
|
39
|
+
候选选择器补无 role 限制的兜底,`clickExact` 的标签解析优先取直接文本节点。
|
|
40
|
+
|
|
41
|
+
### 真机验证
|
|
42
|
+
|
|
43
|
+
- `npm run probe:zcode -- dom-contracts`:`workspaceItems=42`(可见 2 / 不可见 40)、`dataProjectPath=0`、
|
|
44
|
+
菜单展开后 `projects()` 返回菜单项且勾选态正确 —— 独立复现了 issue 的几何前提与修复后的采集结果。
|
|
45
|
+
- `npm run probe:zcode -- models` / `permission` 与一次性探针:锁定 provider 分组 hover 语义、
|
|
46
|
+
权限项 role 与文本结构、档位二值形态(`current=on`、`tiers=Off/On`)。
|
|
47
|
+
- `npm run smoke:zcode`:真机端到端派发(项目绑定 → 模型切换 → 权限确认 → 发送 → 轮询终态)。
|
|
48
|
+
|
|
10
49
|
## [0.7.5] - 2026-09-29
|
|
11
50
|
|
|
12
51
|
### 修复
|
package/README.en.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<img src="./assets/tianshu-mcp-banner.svg" alt="Tianshu Orchestration MCP tianshu-mcp" width="100%">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
<h1 align="center">Tianshu Orchestration MCP
|
|
5
|
+
<h1 align="center">Tianshu Orchestration MCP tianshu-mcp</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<b>Dispatch with agents, verify with evidence. · 把开发交给 AI-Agent,把验收交给运行时</b>
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
<p align="center">
|
|
31
31
|
<img src="https://img.shields.io/github/actions/workflow/status/lanlan0811/tianshu-mcp/ci.yml?branch=master&style=for-the-badge&logo=github&label=CI" alt="CI">
|
|
32
32
|
<img src="https://img.shields.io/npm/v/tianshu-mcp?style=for-the-badge&logo=npm&logoColor=white&label=npm&color=cb3837" alt="npm version">
|
|
33
|
-
<img src="https://img.shields.io/npm/dm/tianshu-mcp?style=for-the-badge&logo=npm&logoColor=white&label=downloads&color=cb3837" alt="npm downloads">
|
|
34
33
|
<img src="https://img.shields.io/github/stars/lanlan0811/tianshu-mcp?style=for-the-badge&logo=github&label=stars&color=24292e" alt="GitHub stars">
|
|
35
34
|
<img src="https://img.shields.io/badge/License-Apache%202.0-3B5BDB?style=for-the-badge&logo=apache" alt="License">
|
|
36
35
|
<img src="https://img.shields.io/badge/TypeScript-5.7-3178c6?style=for-the-badge&logo=typescript&logoColor=white" alt="TypeScript">
|
|
@@ -39,8 +38,20 @@
|
|
|
39
38
|
<img src="https://img.shields.io/badge/Tests-1383%20Passed-green?style=for-the-badge" alt="Tests">
|
|
40
39
|
</p>
|
|
41
40
|
|
|
41
|
+
<p align="center">
|
|
42
|
+
<a href="https://www.npmjs.com/package/tianshu-mcp"><img src="https://img.shields.io/npm/d18m/tianshu-mcp?style=for-the-badge&logo=npm&logoColor=white&label=mcp%20downloads&color=cb3837" alt="MCP downloads (npm)"></a>
|
|
43
|
+
<a href="https://github.com/lanlan0811/tianshu-mcp/releases?q=v&expanded=true"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Flanlan0811%2Ftianshu-mcp%2Fmaster%2Fupdate%2Fstats.json&query=%24.mcpDownloads&style=for-the-badge&logo=github&logoColor=white&label=mcp%20tarball%20downloads&color=2ea44f" alt="MCP tarball downloads (GitHub releases)"></a>
|
|
44
|
+
<a href="https://github.com/lanlan0811/tianshu-mcp/releases?q=gui-v&expanded=true"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Flanlan0811%2Ftianshu-mcp%2Fmaster%2Fupdate%2Fstats.json&query=%24.guiDownloads&style=for-the-badge&logo=github&logoColor=white&label=gui%20downloads&color=1f6feb" alt="Log viewer GUI downloads (GitHub releases)"></a>
|
|
45
|
+
</p>
|
|
46
|
+
|
|
42
47
|
---
|
|
43
48
|
|
|
49
|
+
<p align="center">
|
|
50
|
+
<img src="./assets/mcp-running.png" alt="Tianshu Harness desktop orchestrating ZCode through tianshu-mcp" width="100%">
|
|
51
|
+
<br>
|
|
52
|
+
Tianshu Harness desktop in action — <b>Tianshu</b> calls <b>tianshu-mcp</b> over MCP to orchestrate <b>ZCode</b> through a development task: tasks dispatched and tracked on the left, tianshu-mcp tool calls and event stream in the middle (<code>mcp__tianshu-mcp__query_task</code> polling a running task), and ZCode doing the actual work on the right
|
|
53
|
+
</p>
|
|
54
|
+
|
|
44
55
|
### An orchestration layer and objective acceptance gate for AI agents
|
|
45
56
|
|
|
46
57
|
> **tianshu-mcp** is an orchestration layer plugged into **Tianshu** as a standard MCP server. Tianshu is the commander and the user-facing surface; this server does three jobs: **scheduling** (queues / concurrency gate / state machine / cancellation), the **execution surface** (delivering task briefs to external AI agents), and the **objective acceptance gate** (command checks, code analysis and optional visual comparison, all relative to a git baseline).
|
|
@@ -236,7 +247,7 @@ run_task(projectPath=D:/xxx/my-app, task="…task brief…", agentId=codex,
|
|
|
236
247
|
| agentId | driver / adapter | status | Notes |
|
|
237
248
|
|---|---|---|---|
|
|
238
249
|
| `codex` | `gui` / `codex-gui` | **ready** (`research` on macOS) | Codex desktop GUI (Windows: MSIX COM activation + CDP; macOS: spawn .app + CDP); supports `model` / `reasoningLevel` / `planDoc` / `designSystem`; user-confirmation wait, cancellation and re-dispatch guards are all machine-verified |
|
|
239
|
-
| `zcode` | `gui` / `zcode-gui` | **
|
|
250
|
+
| `zcode` | `gui` / `zcode-gui` | **ready** (closed-loop verified on real Windows; macOS unverified) | CDP GUI adapter; supports project-less dispatch, `allowCreateProject` and `reasoningLevel` (the tier set **varies per model**; out-of-range tiers fail **before sending**); **v0.7.4 adapted to the missing 3.14.x path contracts** (the binding verdict became "path first, display-name when no path is available + global name disambiguation", failing closed on duplicates); **v0.7.6 fixes the binding deadlock** (sidebar `workspace-item-*` nodes scrolled out of view were still collected, short-circuiting the only trustworthy menu channel), **adds a recovery entry point for runtime CDP disconnects** (reconnect once for observation, never resend; a failed reconnect lands on `needs_user(setup_recovery)`) and **fixes the two-level model menu** (provider groups render their submenu only on hover) |
|
|
240
251
|
| `traework` | `gui` / `traework-gui` | **ready** | CDP-driven TRAE SOLO CN desktop UI; supports `mode` (Work / Code / Design, each of the three modes maintaining its own project binding); all three modes machine-verified |
|
|
241
252
|
| `kimicode` | `gui` / `kimicode-gui` | **ready** (`research` on macOS) | Kimi Code desktop (Electron); **dual renderer processes** (main window plus a `Kimi Browser Overlay` that hosts the model / reasoning / mode menus); workspaces bind by full path; supports `model` / `reasoningLevel`, **not `mode`**, and **not project-less dispatch** |
|
|
242
253
|
| `qoder` | `gui` / `qoder-gui` | closed-loop verified on Windows; **research** on macOS | Qoder CN only; requires an existing `projectPath` and a readable `planDoc`; `modelSource=default\|custom` disambiguates same-named models, and the reasoning level is saved as a global preference via "Model management" and read back |
|
|
@@ -384,21 +395,15 @@ Key capabilities:
|
|
|
384
395
|
- **Update-notes window and dual-source auto-update** — a silent update check at startup pops the "update notes" window when a new version is found (download and install / ignore this version / later), whose body is that version's bilingual release notes; the update source is chosen by **concurrently probing Gitee / GitHub and picking the better one** (never relying on system region) and is shown truthfully, and every package is **minisign-verified** — **a failed signature is never installed**.
|
|
385
396
|
|
|
386
397
|
<p align="center">
|
|
387
|
-
<img src="./assets/tianshu-mcp-gui-overview.png" alt="Log viewer · task overview page" width="
|
|
398
|
+
<a href="./assets/tianshu-mcp-gui-overview.png"><img src="./assets/tianshu-mcp-gui-overview.png" alt="Log viewer · task overview page" width="32%"></a>
|
|
399
|
+
<a href="./assets/tianshu-mcp-gui-event-stream.png"><img src="./assets/tianshu-mcp-gui-event-stream.png" alt="Log viewer · task event stream workspace" width="32%"></a>
|
|
400
|
+
<a href="./assets/tianshu-mcp-gui-agent-log.png"><img src="./assets/tianshu-mcp-gui-agent-log.png" alt="Log viewer · agent raw log workspace" width="32%"></a>
|
|
388
401
|
<br>
|
|
389
|
-
<
|
|
390
|
-
</p>
|
|
391
|
-
|
|
392
|
-
<p align="center">
|
|
393
|
-
<img src="./assets/tianshu-mcp-gui-event-stream.png" alt="Log viewer · task event stream workspace" width="88%">
|
|
402
|
+
<b>Task overview page</b> — resident left rail · five-cell metrics (total / running / finished / succeeded / failed) · status chips · task card grid
|
|
394
403
|
<br>
|
|
395
|
-
<
|
|
396
|
-
</p>
|
|
397
|
-
|
|
398
|
-
<p align="center">
|
|
399
|
-
<img src="./assets/tianshu-mcp-gui-agent-log.png" alt="Log viewer · agent raw log workspace" width="88%">
|
|
404
|
+
<b>Full-screen workspace · event stream</b> — line / time / event / detail columns, with status transitions and progress records colour-coded
|
|
400
405
|
<br>
|
|
401
|
-
<
|
|
406
|
+
<b>Full-screen workspace · agent log</b> — level filtering · line numbers and word wrap · "loaded N / M" with "jump to latest"
|
|
402
407
|
</p>
|
|
403
408
|
|
|
404
409
|
Decoupling and release boundaries (read before changing anything here):
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<img src="./assets/tianshu-mcp-banner.svg" alt="天枢编排 MCP tianshu-mcp" width="100%">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
<h1 align="center">天枢编排 MCP
|
|
5
|
+
<h1 align="center">天枢编排 MCP tianshu-mcp</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<b>把开发交给 AI-Agent,把验收交给运行时 · Dispatch with agents, verify with evidence.</b>
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
<p align="center">
|
|
31
31
|
<img src="https://img.shields.io/github/actions/workflow/status/lanlan0811/tianshu-mcp/ci.yml?branch=master&style=for-the-badge&logo=github&label=CI" alt="CI">
|
|
32
32
|
<img src="https://img.shields.io/npm/v/tianshu-mcp?style=for-the-badge&logo=npm&logoColor=white&label=npm&color=cb3837" alt="npm version">
|
|
33
|
-
<img src="https://img.shields.io/npm/dm/tianshu-mcp?style=for-the-badge&logo=npm&logoColor=white&label=downloads&color=cb3837" alt="npm downloads">
|
|
34
33
|
<img src="https://img.shields.io/github/stars/lanlan0811/tianshu-mcp?style=for-the-badge&logo=github&label=stars&color=24292e" alt="GitHub stars">
|
|
35
34
|
<img src="https://img.shields.io/badge/License-Apache%202.0-3B5BDB?style=for-the-badge&logo=apache" alt="License">
|
|
36
35
|
<img src="https://img.shields.io/badge/TypeScript-5.7-3178c6?style=for-the-badge&logo=typescript&logoColor=white" alt="TypeScript">
|
|
@@ -39,8 +38,20 @@
|
|
|
39
38
|
<img src="https://img.shields.io/badge/Tests-1383%20Passed-green?style=for-the-badge" alt="Tests">
|
|
40
39
|
</p>
|
|
41
40
|
|
|
41
|
+
<p align="center">
|
|
42
|
+
<a href="https://www.npmjs.com/package/tianshu-mcp"><img src="https://img.shields.io/npm/d18m/tianshu-mcp?style=for-the-badge&logo=npm&logoColor=white&label=MCP%20%E4%B8%8B%E8%BD%BD%E6%AC%A1%E6%95%B0&color=cb3837" alt="MCP 下载次数(npm)"></a>
|
|
43
|
+
<a href="https://github.com/lanlan0811/tianshu-mcp/releases?q=v&expanded=true"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Flanlan0811%2Ftianshu-mcp%2Fmaster%2Fupdate%2Fstats.json&query=%24.mcpDownloads&style=for-the-badge&logo=github&logoColor=white&label=MCP%20%E5%8E%8B%E7%BC%A9%E5%8C%85%E4%B8%8B%E8%BD%BD%E6%AC%A1%E6%95%B0&color=2ea44f" alt="MCP 压缩包下载次数(GitHub 发行)"></a>
|
|
44
|
+
<a href="https://github.com/lanlan0811/tianshu-mcp/releases?q=gui-v&expanded=true"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Flanlan0811%2Ftianshu-mcp%2Fmaster%2Fupdate%2Fstats.json&query=%24.guiDownloads&style=for-the-badge&logo=github&logoColor=white&label=GUI%20%E4%B8%8B%E8%BD%BD%E6%AC%A1%E6%95%B0&color=1f6feb" alt="日志台 GUI 下载次数(GitHub 发行)"></a>
|
|
45
|
+
</p>
|
|
46
|
+
|
|
42
47
|
---
|
|
43
48
|
|
|
49
|
+
<p align="center">
|
|
50
|
+
<img src="./assets/mcp-running.png" alt="天枢 harness 桌面端通过 tianshu-mcp 调用 ZCode 完成开发" width="100%">
|
|
51
|
+
<br>
|
|
52
|
+
天枢 harness 桌面端实况 —— <b>天枢</b>经 MCP 调用 <b>tianshu-mcp</b> 编排 <b>ZCode</b> 完成一次开发任务:左侧派发与跟踪任务,中间是 tianshu-mcp 的工具调用与事件流(<code>mcp__tianshu-mcp__query_task</code> 轮询运行中的任务),右侧 ZCode 正在执行实际开发
|
|
53
|
+
</p>
|
|
54
|
+
|
|
44
55
|
### 面向 AI-Agent 的编排层与客观验收仪
|
|
45
56
|
|
|
46
57
|
> **tianshu-mcp** 是被 **天枢(Tianshu)** 当作标准 MCP server 接入的编排层。天枢是总指挥与用户交互面,本 server 承担三件事:**调度**(队列 / 并发闸 / 状态机 / 取消)、**执行面**(把任务书送达外部 AI-Agent)、**客观验收仪**(相对 git 基线做命令检查、代码分析与可选视觉比对)。
|
|
@@ -236,7 +247,7 @@ run_task(projectPath=D:/xxx/my-app, task="…任务书…", agentId=codex,
|
|
|
236
247
|
| agentId | driver / adapter | status | 说明 |
|
|
237
248
|
|---|---|---|---|
|
|
238
249
|
| `codex` | `gui` / `codex-gui` | **ready**(macOS 为 `research`) | Codex 桌面端 GUI(Windows:MSIX COM 激活 + CDP;macOS:spawn .app + CDP);支持 `model` / `reasoningLevel` / `planDoc` / `designSystem`;等待用户确认、取消与重派护栏均已真机验证 |
|
|
239
|
-
| `zcode` | `gui` / `zcode-gui` | **
|
|
250
|
+
| `zcode` | `gui` / `zcode-gui` | **ready**(Windows 真机闭环;macOS 未验证) | CDP GUI adapter;支持无项目派发、`allowCreateProject` 与 `reasoningLevel`(档位集合**随模型变化**,越权在**发送前**报错);**v0.7.4 适配 3.14.x 的路径契约缺席**(绑定判据改为「路径优先、无路径渠道时按显示名 + 全局同名消歧」,同名即 fail-closed);**v0.7.6 修掉绑定死锁**(侧边栏 `workspace-item-*` 滚出视口仍被采集 → 唯一可信的菜单渠道被短路)、**运行期 CDP 断连的恢复入口**(重连观察一次、绝不重发,失败落 `needs_user(setup_recovery)`)与**两级模型菜单**(provider 分组须 hover 才渲染子项) |
|
|
240
251
|
| `traework` | `gui` / `traework-gui` | **ready** | CDP 驱动 TRAE SOLO CN 桌面 UI;支持 `mode`(Work / Code / Design,三种模式各自维护独立项目绑定);三种面板模式真机验证通过 |
|
|
241
252
|
| `kimicode` | `gui` / `kimicode-gui` | **ready**(macOS 为 `research`) | Kimi Code 桌面端(Electron);**双渲染进程**(主窗口 + `Kimi Browser Overlay` 浮层承载模型 / 档位 / 模式菜单);工作区以完整路径绑定;支持 `model` / `reasoningLevel`,**不支持 `mode`**,且**不支持无项目派发** |
|
|
242
253
|
| `qoder` | `gui` / `qoder-gui` | Windows 真机闭环通过;macOS **research** | 仅 Qoder CN;必须提供已有 `projectPath` 与可读 `planDoc`;`modelSource=default\|custom` 消除同名模型歧义,思考等级经「模型管理」保存为全局偏好并回读 |
|
|
@@ -384,21 +395,15 @@ run_task(projectPath=D:/xxx/my-app, task="…任务书…", agentId=codex,
|
|
|
384
395
|
- **更新日志窗口与双源自动更新** —— 启动静默检查更新,命中即弹「更新日志」(下载并安装 / 忽略此版本 / 稍后),正文即该版本的双语发行说明;更新源由 **Gitee / GitHub 并发实测择优**(不依赖系统区域)决定并如实展示,包体经 **minisign 验签**,**验签不通过一律拒绝安装**。
|
|
385
396
|
|
|
386
397
|
<p align="center">
|
|
387
|
-
<img src="./assets/tianshu-mcp-gui-overview.png" alt="日志台 · 任务概览页" width="
|
|
398
|
+
<a href="./assets/tianshu-mcp-gui-overview.png"><img src="./assets/tianshu-mcp-gui-overview.png" alt="日志台 · 任务概览页" width="32%"></a>
|
|
399
|
+
<a href="./assets/tianshu-mcp-gui-event-stream.png"><img src="./assets/tianshu-mcp-gui-event-stream.png" alt="日志台 · 任务事件流工作区" width="32%"></a>
|
|
400
|
+
<a href="./assets/tianshu-mcp-gui-agent-log.png"><img src="./assets/tianshu-mcp-gui-agent-log.png" alt="日志台 · Agent 原始日志工作区" width="32%"></a>
|
|
388
401
|
<br>
|
|
389
|
-
<
|
|
390
|
-
</p>
|
|
391
|
-
|
|
392
|
-
<p align="center">
|
|
393
|
-
<img src="./assets/tianshu-mcp-gui-event-stream.png" alt="日志台 · 任务事件流工作区" width="88%">
|
|
402
|
+
<b>任务概览页</b> —— 常驻左侧栏 · 五格指标仪(任务总数 / 进行中 / 已结束 / 已成功 / 已失败)· 状态圆片 · 任务卡网格
|
|
394
403
|
<br>
|
|
395
|
-
<
|
|
396
|
-
</p>
|
|
397
|
-
|
|
398
|
-
<p align="center">
|
|
399
|
-
<img src="./assets/tianshu-mcp-gui-agent-log.png" alt="日志台 · Agent 原始日志工作区" width="88%">
|
|
404
|
+
<b>全屏工作区 · 事件流</b> —— 行 / 时间 / 事件 / 详情四列,状态跃迁与进度记录分色标注
|
|
400
405
|
<br>
|
|
401
|
-
<
|
|
406
|
+
<b>全屏工作区 · Agent 日志</b> —— 级别过滤 · 行号与自动换行 · 「已加载 N / 共 M」与「跳到最新」
|
|
402
407
|
</p>
|
|
403
408
|
|
|
404
409
|
解耦与发布边界(改这里之前先读):
|
package/dist/agents/zcode/cdp.js
CHANGED
|
@@ -2,7 +2,7 @@ import { TraeworkCdpClient, CdpDisconnectedError, CdpUnavailableError, } from ".
|
|
|
2
2
|
import { candidateExpr } from "./selectors.js";
|
|
3
3
|
import { projectDisplayName } from "./project.js";
|
|
4
4
|
import { normalizeZcodeModelSelection } from "./model.js";
|
|
5
|
-
import { projectTriggerDom, projectTriggerProbeExpression, projectMenuOpenExpression, workOutsideProjectExpression, workspaceBindingExpression, modelSelectionExpression, sendButtonPointExpression, } from "./dom.js";
|
|
5
|
+
import { ZCODE_DOM, projectTriggerDom, projectTriggerProbeExpression, projectMenuOpenExpression, workOutsideProjectExpression, workspaceBindingExpression, modelSelectionExpression, sendButtonPointExpression, } from "./dom.js";
|
|
6
6
|
export { CdpDisconnectedError, CdpUnavailableError };
|
|
7
7
|
export async function retryZcodeEvaluation(operation, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
8
8
|
for (let attempt = 0;; attempt++) {
|
|
@@ -156,8 +156,15 @@ export class ZcodeCdpClient {
|
|
|
156
156
|
await this.clickAt(found.point.x, found.point.y);
|
|
157
157
|
return { clicked: true, count: 1 };
|
|
158
158
|
}
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
/**
|
|
160
|
+
* 按可见文本精确定位候选项并作用。
|
|
161
|
+
*
|
|
162
|
+
* `mode="hover"` 只发 mouseMoved、不发按键:radix 的**二级子菜单**(如模型菜单的
|
|
163
|
+
* provider 分组)要 hover 才渲染子项,click 会选中分组本身或把菜单收起——
|
|
164
|
+
* 这是 issue #27「两级模型菜单点击不稳」的真机根因(3.14.3 实测)。
|
|
165
|
+
*/
|
|
166
|
+
async clickExact(key, value, mode = "click") {
|
|
167
|
+
const found = await this.evaluate(`(async function(){const norm=s=>(s||'').normalize('NFKC').trim().toLocaleLowerCase();const target=norm(${JSON.stringify(value)});const sels=${candidateExpr(key, this.selectors)};const visible=e=>{const r=e.getBoundingClientRect();if(!(r.width>0&&r.height>0&&r.bottom>0&&r.right>0&&r.top<innerHeight&&r.left<innerWidth))return false;const x=Math.max(0,Math.min(innerWidth-1,r.left+r.width/2)),y=Math.max(0,Math.min(innerHeight-1,r.top+r.height/2)),hit=document.elementFromPoint(x,y);return !!hit&&(hit===e||e.contains(hit))};const items=()=>{const out=[];for(const s of sels)for(const e of document.querySelectorAll(s))if(visible(e)&&!out.includes(e))out.push(e);return out};const leafTexts=e=>[...e.querySelectorAll('*')].filter(n=>n.children.length===0).map(n=>(n.textContent||'').trim()).filter(Boolean);const directText=e=>[...e.childNodes].filter(n=>n.nodeType===3).map(n=>(n.textContent||'').trim()).filter(Boolean).join(' ');const label=e=>(e.getAttribute('data-value')||e.getAttribute('data-model')||e.getAttribute('data-provider')||directText(e)||leafTexts(e)[0]||e.textContent||'').trim();let nodes=items();let scroller=nodes[0];while(scroller&&scroller!==document.body&&scroller.scrollHeight<=scroller.clientHeight)scroller=scroller.parentElement;const start=scroller?.scrollTop||0;if(scroller)scroller.scrollTop=0;const seen=new Map(),seenTestids=new Map(),matches=new Map();for(let i=0;i<60;i++){await new Promise(r=>setTimeout(r,25));nodes=items();for(const e of nodes){const text=label(e);const testid=e.getAttribute('data-testid')||'';const id=e.getAttribute('data-id')||e.getAttribute('data-model-id')||e.getAttribute('data-value')||e.getAttribute('data-model')||e.getAttribute('data-provider')||testid||text;const key=norm(id)+'|'+norm(text);if(testid)seenTestids.set(key,testid);if(!text)continue;seen.set(key,text);if(norm(text)===target)matches.set(key,{top:scroller?.scrollTop||0,text})}if(!scroller||scroller.scrollTop+scroller.clientHeight>=scroller.scrollHeight-1)break;const before=scroller.scrollTop;scroller.scrollTop=Math.min(scroller.scrollTop+Math.max(100,scroller.clientHeight*.8),scroller.scrollHeight);if(scroller.scrollTop===before)break}const result={available:[...seen.values()],testids:[...seenTestids.values()]};const matchesFound=[...matches.values()];if(matchesFound.length===1){if(scroller)scroller.scrollTop=matchesFound[0].top;await new Promise(r=>setTimeout(r,50));const exact=items().filter(e=>norm(label(e))===target);if(exact.length===1){exact[0].scrollIntoView({block:'center'});const r=exact[0].getBoundingClientRect(),x=r.left+r.width/2,y=r.top+r.height/2;const hit=document.elementFromPoint(x,y);if(hit&&(hit===exact[0]||exact[0].contains(hit)))return {count:1,...result,point:{x,y}};return {count:1,...result}}}if(scroller)scroller.scrollTop=start;return {count:matchesFound.length,...result}})()`);
|
|
161
168
|
if (!found.point)
|
|
162
169
|
return {
|
|
163
170
|
clicked: false,
|
|
@@ -165,7 +172,14 @@ export class ZcodeCdpClient {
|
|
|
165
172
|
available: found.available,
|
|
166
173
|
testids: found.testids,
|
|
167
174
|
};
|
|
168
|
-
|
|
175
|
+
if (mode === "hover")
|
|
176
|
+
await this.send("Input.dispatchMouseEvent", {
|
|
177
|
+
type: "mouseMoved",
|
|
178
|
+
x: found.point.x,
|
|
179
|
+
y: found.point.y,
|
|
180
|
+
});
|
|
181
|
+
else
|
|
182
|
+
await this.clickAt(found.point.x, found.point.y);
|
|
169
183
|
return {
|
|
170
184
|
clicked: true,
|
|
171
185
|
count: found.count,
|
|
@@ -177,44 +191,136 @@ export class ZcodeCdpClient {
|
|
|
177
191
|
const raw = await this.evaluate(modelSelectionExpression(this.selectors));
|
|
178
192
|
return normalizeZcodeModelSelection(raw);
|
|
179
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* 采集项目候选(issue #27 修复)。
|
|
196
|
+
*
|
|
197
|
+
* 两条渠道**始终合并**,不再用 `if(!out.length)` 短路——3.14.x 下必被视口外的侧边栏
|
|
198
|
+
* 幽灵项污染,那时短路恒为假,唯一可信的菜单渠道被整条掐掉。
|
|
199
|
+
* 侧边栏项(旧契约)必须先过 `ZCODE_DOM.visible`:3.14.x 的 `workspace-item-*`
|
|
200
|
+
* 并未从 DOM 消失,只是尺寸塌陷/被滚出视口,它们不是可信的项目证据。
|
|
201
|
+
* 同名时菜单项覆盖侧边栏项:只有菜单的 `aria-checked` 是 ZCode 自己渲染的绑定证据。
|
|
202
|
+
*/
|
|
203
|
+
/**
|
|
204
|
+
* 思考档位的只读快照(issue #27 问题三)。
|
|
205
|
+
*
|
|
206
|
+
* 真机实测(2026-09-30,ZCode 3.14.3-Windows):档位**选项只在菜单展开时挂载**,
|
|
207
|
+
* 因此没有挂载任何选项时按需点开一次、读完立刻关闭(不给后续步骤留副作用)。
|
|
208
|
+
* 只采集原始事实,档位归一与校验交给 model.ts 的纯函数。
|
|
209
|
+
*/
|
|
210
|
+
async thoughtLevelSnapshot() {
|
|
211
|
+
const triggerExpr = `(function(){${ZCODE_DOM}
|
|
212
|
+
const found=pick(${candidateExpr("thoughtLevelTrigger", this.selectors)});
|
|
213
|
+
return {mounted:found.count?1:0, text:(found.node?.textContent||'').replace(/\\s+/g,' ').trim()};
|
|
214
|
+
})()`;
|
|
215
|
+
const optionsExpr = `(function(){${ZCODE_DOM}
|
|
216
|
+
const out=[];
|
|
217
|
+
for(const s of ${candidateExpr("thoughtLevelOption", this.selectors)})for(const e of document.querySelectorAll(s)){
|
|
218
|
+
const r=e.getBoundingClientRect();
|
|
219
|
+
if(!(r.width&&r.height))continue;
|
|
220
|
+
const id=e.getAttribute('data-testid')||'';
|
|
221
|
+
out.push({id,
|
|
222
|
+
token:id.startsWith('chat-thought-level-select-item-')?id.slice('chat-thought-level-select-item-'.length):'',
|
|
223
|
+
text:(e.textContent||'').replace(/\\s+/g,' ').trim(),
|
|
224
|
+
checked:e.getAttribute('aria-checked')==='true'});
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
})()`;
|
|
228
|
+
const probe = await this.evaluate(triggerExpr);
|
|
229
|
+
if (!probe.mounted)
|
|
230
|
+
return { triggerMounted: false, opened: false, triggerText: probe.text, options: [] };
|
|
231
|
+
let options = await this.evaluate(optionsExpr);
|
|
232
|
+
let opened = false;
|
|
233
|
+
if (!options.length && (await this.click("thoughtLevelTrigger"))) {
|
|
234
|
+
await this.evaluate(`new Promise(r=>setTimeout(r,250))`);
|
|
235
|
+
options = await this.evaluate(optionsExpr);
|
|
236
|
+
opened = options.length > 0;
|
|
237
|
+
await this.dismissRadixSelect();
|
|
238
|
+
}
|
|
239
|
+
const after = await this.evaluate(triggerExpr);
|
|
240
|
+
return { triggerMounted: true, opened, triggerText: after.text, options };
|
|
241
|
+
}
|
|
242
|
+
/** 点击指定 testid 的档位选项(选项需已挂载)。 */
|
|
243
|
+
async clickThoughtLevelOption(id) {
|
|
244
|
+
const point = await this.evaluate(`(function(){${ZCODE_DOM}
|
|
245
|
+
for(const s of ${candidateExpr("thoughtLevelOption", this.selectors)})for(const e of document.querySelectorAll(s)){
|
|
246
|
+
if((e.getAttribute('data-testid')||'')!==${JSON.stringify(id)})continue;
|
|
247
|
+
const r=e.getBoundingClientRect();
|
|
248
|
+
if(!(r.width&&r.height))continue;
|
|
249
|
+
e.scrollIntoView({block:'center'});
|
|
250
|
+
const b=e.getBoundingClientRect();
|
|
251
|
+
return {x:b.left+b.width/2,y:b.top+b.height/2};
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
})()`);
|
|
255
|
+
if (!point)
|
|
256
|
+
return false;
|
|
257
|
+
await this.clickAt(point.x, point.y);
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* 关闭 radix select 的 listbox。`dismissMenus` 只认 `[role="menu"]`,
|
|
262
|
+
* 而档位下拉是 combobox + listbox,需要单独的 Escape。
|
|
263
|
+
*/
|
|
264
|
+
async dismissRadixSelect() {
|
|
265
|
+
for (let i = 0; i < 2; i++) {
|
|
266
|
+
// eslint-disable-next-line no-await-in-loop
|
|
267
|
+
await this.send("Input.dispatchKeyEvent", {
|
|
268
|
+
type: "keyDown",
|
|
269
|
+
key: "Escape",
|
|
270
|
+
code: "Escape",
|
|
271
|
+
windowsVirtualKeyCode: 27,
|
|
272
|
+
});
|
|
273
|
+
// eslint-disable-next-line no-await-in-loop
|
|
274
|
+
await this.send("Input.dispatchKeyEvent", {
|
|
275
|
+
type: "keyUp",
|
|
276
|
+
key: "Escape",
|
|
277
|
+
code: "Escape",
|
|
278
|
+
windowsVirtualKeyCode: 27,
|
|
279
|
+
});
|
|
280
|
+
// eslint-disable-next-line no-await-in-loop
|
|
281
|
+
await this.evaluate(`new Promise(r=>setTimeout(r,120))`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
180
284
|
async projects() {
|
|
181
|
-
return this.evaluate(`(function(){
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
const
|
|
285
|
+
return this.evaluate(`(function(){${ZCODE_DOM}
|
|
286
|
+
const order=[];
|
|
287
|
+
const byName=new Map();
|
|
288
|
+
const put=(name,path,id,checked,channel)=>{
|
|
185
289
|
const n=(name||'').trim();
|
|
186
290
|
if(!n)return;
|
|
187
|
-
const key=norm(n)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
291
|
+
const key=norm(n);
|
|
292
|
+
const previous=byName.get(key);
|
|
293
|
+
// 菜单渠道优先:同名的侧边栏项被菜单项顶掉;同渠道之间保持先入先得。
|
|
294
|
+
if(previous&&previous.channel==='menu'&&channel!=='menu')return;
|
|
295
|
+
if(!previous)order.push(key);
|
|
296
|
+
byName.set(key,{item:{name:n,path:path||undefined,id:id||undefined,checked},channel});
|
|
191
297
|
};
|
|
192
298
|
for(const s of ${candidateExpr("projectItem", this.selectors)})for(const e of document.querySelectorAll(s)){
|
|
299
|
+
if(!visible(e))continue;
|
|
193
300
|
const testid=e.getAttribute('data-testid')||'';
|
|
194
301
|
const testPath=testid.startsWith('workspace-item-')?testid.slice('workspace-item-'.length):undefined;
|
|
195
302
|
const name=(e.getAttribute('data-project-name')||e.querySelector('[class*=name]')?.textContent||e.textContent||'').trim();
|
|
196
303
|
const p=e.getAttribute('data-project-path')||e.querySelector('[data-project-path]')?.getAttribute('data-project-path')||testPath||e.getAttribute('title')||undefined;
|
|
197
304
|
const id=e.getAttribute('data-project-id')||e.getAttribute('data-id')||testid||undefined;
|
|
198
|
-
|
|
305
|
+
put(name,p,id,undefined,'sidebar');
|
|
199
306
|
}
|
|
200
307
|
// issue #24:ZCode 3.14.x 已删除 workspace-item-*,项目列表只能从展开的下拉菜单采集。
|
|
201
|
-
|
|
202
|
-
if(!out.length)for(const s of ${candidateExpr("projectMenuItem", this.selectors)})for(const e of document.querySelectorAll(s)){
|
|
308
|
+
for(const s of ${candidateExpr("projectMenuItem", this.selectors)})for(const e of document.querySelectorAll(s)){
|
|
203
309
|
const name=(e.getAttribute('aria-label')||e.textContent||'').trim();
|
|
204
310
|
// 「不在项目中工作」是工作区切换项,不是项目——按本地化标签排除。
|
|
205
311
|
if(/不在项目中|outside a project|work outside/i.test(name))continue;
|
|
206
|
-
|
|
312
|
+
put(name,undefined,e.getAttribute('data-value')||undefined,e.getAttribute('aria-checked')==='true','menu');
|
|
207
313
|
}
|
|
208
|
-
return
|
|
314
|
+
return order.map(key=>byName.get(key).item)
|
|
209
315
|
})()`);
|
|
210
316
|
}
|
|
211
317
|
async clickProject(id, projectPath) {
|
|
212
318
|
const displayName = projectPath ? projectDisplayName(projectPath) : "";
|
|
213
|
-
const
|
|
214
|
-
if (!point)
|
|
215
|
-
return false;
|
|
216
|
-
await this.clickAt(point.x, point.y);
|
|
217
|
-
return true;
|
|
319
|
+
const outcome = await this.evaluate(`(async function(){${projectTriggerDom(this.selectors)}if(!trigger)return {reason:'trigger-unavailable'};const targetName=norm(${JSON.stringify(displayName)});let containers=[];const controlledId=trigger?.getAttribute('aria-controls')||'';if(controlledId){const controlled=document.getElementById(controlledId);if(controlled&&visible(controlled))containers.push(controlled)}if(trigger){let parent=trigger.parentElement;while(parent&&!containers.length){if(parent.querySelector('[role="menuitemcheckbox"]'))containers.push(parent);parent=parent.parentElement}}if(!containers.length)containers=[...document.querySelectorAll('[role="menu"]')].filter(e=>visible(e)&&e.querySelector('[role="menuitemcheckbox"]'));const checkboxMatches=[];for(const container of containers)for(const e of container.querySelectorAll('[role="menuitemcheckbox"]'))if(visible(e)&&norm(e.getAttribute('aria-label')||e.getAttribute('data-value')||e.textContent||'')===targetName&&!checkboxMatches.includes(e))checkboxMatches.push(e);let selected=checkboxMatches.length===1?checkboxMatches[0]:null;if(!selected){const matches=[];for(const s of ${candidateExpr("projectItem", this.selectors)})for(const e of document.querySelectorAll(s)){const testid=e.getAttribute('data-testid')||'';const itemId=e.getAttribute('data-project-id')||e.getAttribute('data-id')||testid;const p=e.getAttribute('data-project-path')||e.querySelector('[data-project-path]')?.getAttribute('data-project-path')||(testid.startsWith('workspace-item-')?testid.slice('workspace-item-'.length):'')||e.getAttribute('title')||'';if(((${JSON.stringify(id ?? "")}&&itemId===${JSON.stringify(id ?? "")})||(${JSON.stringify(projectPath ?? "")}&&p===${JSON.stringify(projectPath ?? "")}))&&!matches.includes(e))matches.push(e)}const shown=matches.filter(visible);if(matches.length&&!shown.length)return {reason:'not-visible'};if(shown.length===1)selected=shown[0]}if(!selected)return {reason:'not-found'};selected.scrollIntoView({block:'center'});await new Promise(r=>setTimeout(r,50));const r=selected.getBoundingClientRect();return visible(selected)?{point:{x:r.left+r.width/2,y:r.top+r.height/2}}:{reason:'not-visible'}})()`);
|
|
320
|
+
if (!("point" in outcome))
|
|
321
|
+
return { clicked: false, reason: outcome.reason };
|
|
322
|
+
await this.clickAt(outcome.point.x, outcome.point.y);
|
|
323
|
+
return { clicked: true, reason: "clicked" };
|
|
218
324
|
}
|
|
219
325
|
async boundProjectPath() {
|
|
220
326
|
return (await this.workspaceBinding()).projectPath;
|
|
@@ -1,12 +1,110 @@
|
|
|
1
1
|
export class ZcodeModelReadbackError extends Error {
|
|
2
2
|
}
|
|
3
|
-
|
|
3
|
+
/** 思考档位校验失败(值域外或不在界面实际档位集合内)。 */
|
|
4
|
+
export class ZcodeReasoningLevelError extends Error {
|
|
5
|
+
}
|
|
6
|
+
/** 界面 token(testid 后缀或可见文本)→ 规范档位。中英双语都接受。 */
|
|
7
|
+
const THOUGHT_LEVEL_ALIASES = {
|
|
8
|
+
关闭: "off",
|
|
9
|
+
关闭思考: "off",
|
|
10
|
+
off: "off",
|
|
11
|
+
disabled: "off",
|
|
12
|
+
开启: "on",
|
|
13
|
+
on: "on",
|
|
14
|
+
enabled: "on",
|
|
15
|
+
低: "low",
|
|
16
|
+
low: "low",
|
|
17
|
+
中: "medium",
|
|
18
|
+
medium: "medium",
|
|
19
|
+
高: "high",
|
|
20
|
+
high: "high",
|
|
21
|
+
max: "max",
|
|
22
|
+
最大: "max",
|
|
23
|
+
极高: "max",
|
|
24
|
+
xhigh: "max",
|
|
25
|
+
};
|
|
26
|
+
/** 档位在错误文案里的界面写法 */
|
|
27
|
+
const THOUGHT_LEVEL_UI = {
|
|
28
|
+
off: "Off",
|
|
29
|
+
on: "On",
|
|
30
|
+
low: "Low",
|
|
31
|
+
medium: "Medium",
|
|
32
|
+
high: "High",
|
|
33
|
+
max: "Max",
|
|
34
|
+
};
|
|
35
|
+
/** 规范顺序(读到的集合按此排序,便于比较与展示) */
|
|
36
|
+
const THOUGHT_LEVEL_ORDER = ["off", "on", "low", "medium", "high", "max"];
|
|
37
|
+
function thoughtKey(value) {
|
|
38
|
+
return value.normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase();
|
|
39
|
+
}
|
|
40
|
+
/** 界面 token → 档位;无法识别返回 undefined(绝不猜) */
|
|
41
|
+
export function thoughtLevelOfToken(token) {
|
|
42
|
+
const key = thoughtKey(token ?? "");
|
|
43
|
+
return key ? THOUGHT_LEVEL_ALIASES[key] : undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 请求值归一。取值域外的值**不归一**,原值放进 unsupportedLevel 供发送前报错——
|
|
47
|
+
* 静默丢弃会让「请求高档却被沿用当前档」这种偏差无人察觉。
|
|
48
|
+
*/
|
|
49
|
+
export function normalizeZcodeReasoningLevel(value) {
|
|
50
|
+
const raw = value?.normalize("NFKC").trim() ?? "";
|
|
51
|
+
if (!raw)
|
|
52
|
+
return {};
|
|
53
|
+
const level = thoughtLevelOfToken(raw);
|
|
54
|
+
return level ? { level } : { unsupported: raw };
|
|
55
|
+
}
|
|
56
|
+
/** 由界面实际档位标签集合归类 */
|
|
57
|
+
export function thoughtTierSetOf(labels) {
|
|
58
|
+
const levels = new Set();
|
|
59
|
+
for (const label of labels) {
|
|
60
|
+
const level = thoughtLevelOfToken(label);
|
|
61
|
+
if (level)
|
|
62
|
+
levels.add(level);
|
|
63
|
+
}
|
|
64
|
+
const tiers = THOUGHT_LEVEL_ORDER.filter((level) => levels.has(level));
|
|
65
|
+
if (levels.has("on") && levels.has("off"))
|
|
66
|
+
return { tiers, kind: "onoff" };
|
|
67
|
+
return tiers.length ? { tiers, kind: "multi" } : { tiers, kind: "unknown" };
|
|
68
|
+
}
|
|
69
|
+
export function thoughtTierLabels(tiers) {
|
|
70
|
+
return tiers.tiers.map((level) => THOUGHT_LEVEL_UI[level]).join("/") || "(空)";
|
|
71
|
+
}
|
|
72
|
+
export function thoughtLevelUiName(level) {
|
|
73
|
+
return THOUGHT_LEVEL_UI[level];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 校验请求档位是否落在**界面实际档位集合**内。
|
|
77
|
+
* 读不到档位标签(unknown)一律 fail-closed:宁可报错也不按内置名单猜,
|
|
78
|
+
* 否则模型/UI 升级后会把不支持的值"成功"发出去。
|
|
79
|
+
*/
|
|
80
|
+
export function assertZcodeLevelSupported(spec, tiers) {
|
|
81
|
+
if (tiers.kind === "unknown")
|
|
82
|
+
throw new ZcodeReasoningLevelError(`无法从界面读到模型 ${spec.model} 的思考档位(读到 ${thoughtTierLabels(tiers)}),拒绝猜测档位;请检查 ZCode 版本与选择器`);
|
|
83
|
+
const labels = thoughtTierLabels(tiers);
|
|
84
|
+
if (spec.unsupportedLevel !== undefined)
|
|
85
|
+
throw new ZcodeReasoningLevelError(`模型 ${spec.model} 的思考档位仅支持 ${labels},收到「${spec.unsupportedLevel}」`);
|
|
86
|
+
if (spec.level && !tiers.tiers.includes(spec.level))
|
|
87
|
+
throw new ZcodeReasoningLevelError(`模型 ${spec.model} 的思考档位仅支持 ${labels},收到「${spec.level}」`);
|
|
88
|
+
}
|
|
89
|
+
/** 参数级(handlers)可取值的取值域错误文案;无错时返回 undefined。 */
|
|
90
|
+
export function describeZcodeLevelValueError(spec) {
|
|
91
|
+
if (spec.unsupportedLevel === undefined)
|
|
92
|
+
return undefined;
|
|
93
|
+
return `ZCode 的思考档位不支持「${spec.unsupportedLevel}」:仅支持 低/low、中/medium、高/high、max、on/off,且必须落在所选模型界面实际渲染的档位集合内`;
|
|
94
|
+
}
|
|
95
|
+
export function parseZcodeModel(value, level) {
|
|
4
96
|
if (!value)
|
|
5
97
|
throw new Error("ZCode 必须指定 model,格式为 供应商/模型");
|
|
6
98
|
const parts = value.split("/");
|
|
7
99
|
if (parts.length !== 2 || !parts[0].trim() || !parts[1].trim())
|
|
8
100
|
throw new Error(`ZCode model 格式错误:${value}(应为 供应商/模型)`);
|
|
9
|
-
|
|
101
|
+
const normalized = normalizeZcodeReasoningLevel(level);
|
|
102
|
+
return {
|
|
103
|
+
provider: parts[0].trim(),
|
|
104
|
+
model: parts[1].trim(),
|
|
105
|
+
level: normalized.level,
|
|
106
|
+
unsupportedLevel: normalized.unsupported,
|
|
107
|
+
};
|
|
10
108
|
}
|
|
11
109
|
export function exactUiName(a, b) {
|
|
12
110
|
return (a.normalize("NFKC").trim().toLocaleLowerCase() ===
|
package/dist/agents/zcode/run.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { ZCODE_SETUP_DEFAULTS } from "../../config/schema.js";
|
|
5
5
|
import { mkdirp } from "../../util/fs.js";
|
|
6
|
-
import { parseZcodeModel, exactUiName, ZcodeModelReadbackError } from "./model.js";
|
|
6
|
+
import { parseZcodeModel, exactUiName, ZcodeModelReadbackError, ZcodeReasoningLevelError, assertZcodeLevelSupported, describeZcodeLevelValueError, thoughtLevelOfToken, thoughtTierSetOf, } from "./model.js";
|
|
7
7
|
import { validateTaskReferences } from "./references.js";
|
|
8
8
|
import { boundProjectVerdict, matchZcodeProject, isUnboundTriggerText, } from "./project.js";
|
|
9
9
|
import { judgeZcodePoll } from "./liveness.js";
|
|
@@ -121,11 +121,11 @@ async function connectStableZcode(ready, gui, deps, allowLoginPage = false) {
|
|
|
121
121
|
? lastError
|
|
122
122
|
: new Error(`ZCode CDP 未在 ${gui.launchTimeoutMs}ms 内恢复`);
|
|
123
123
|
}
|
|
124
|
-
async function clickExactWhenReady(cdp, key, value, deps) {
|
|
124
|
+
async function clickExactWhenReady(cdp, key, value, deps, mode = "click") {
|
|
125
125
|
let last = { clicked: false, count: 0, available: [] };
|
|
126
126
|
for (let i = 0; i < 15; i++) {
|
|
127
127
|
// eslint-disable-next-line no-await-in-loop
|
|
128
|
-
last = await cdp.clickExact(key, value);
|
|
128
|
+
last = await cdp.clickExact(key, value, mode);
|
|
129
129
|
if (last.clicked || last.count > 1)
|
|
130
130
|
return last;
|
|
131
131
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -168,6 +168,18 @@ function describeProjectTriggerFailure(outcome) {
|
|
|
168
168
|
return "无法打开 ZCode 项目列表:项目触发器始终未挂载(等待项目触发器超时)";
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* 运行期 CDP 断连且重连无效(issue #27):任务已经发出,因此不再判死,
|
|
173
|
+
* 而是由外层归位到可恢复出口(needs_user/setup_recovery),把现场与已确认的会话
|
|
174
|
+
* 一起交回用户处理。
|
|
175
|
+
*/
|
|
176
|
+
class ZcodeRuntimeDisconnect extends Error {
|
|
177
|
+
session;
|
|
178
|
+
constructor(message, session) {
|
|
179
|
+
super(message);
|
|
180
|
+
this.session = session;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
171
183
|
export async function runZcodeTask(args) {
|
|
172
184
|
const started = Date.now(), { ctx, resolved, opts, logFile } = args, baseDeps = { ...DEFAULT_DEPS, ...args.deps };
|
|
173
185
|
const gui = guiOf(resolved);
|
|
@@ -199,6 +211,8 @@ export async function runZcodeTask(args) {
|
|
|
199
211
|
},
|
|
200
212
|
};
|
|
201
213
|
let cdp;
|
|
214
|
+
/** 运行期断连时组织诊断文案(进程侧 + 窗口侧);在发送阶段的护栏里装配。 */
|
|
215
|
+
let runtimeDiagnosis;
|
|
202
216
|
const result = (extra) => ({
|
|
203
217
|
ok: false,
|
|
204
218
|
exitCode: null,
|
|
@@ -211,7 +225,7 @@ export async function runZcodeTask(args) {
|
|
|
211
225
|
});
|
|
212
226
|
try {
|
|
213
227
|
await mkdirp(path.dirname(logFile));
|
|
214
|
-
const spec = parseZcodeModel(ctx.model);
|
|
228
|
+
const spec = parseZcodeModel(ctx.model, ctx.reasoningLevel);
|
|
215
229
|
// 无项目模式(issue #12):进入 ZCode 的 default 工作区,不解析项目引用、不绑定/导入项目。
|
|
216
230
|
const defaultWorkspace = ctx.workspaceMode === "default";
|
|
217
231
|
const refs = validateTaskReferences(ctx.task, ctx.context, defaultWorkspace ? undefined : ctx.projectPath);
|
|
@@ -336,6 +350,94 @@ export async function runZcodeTask(args) {
|
|
|
336
350
|
let session = activeSession;
|
|
337
351
|
let permission = ctx.resume?.permissionMode ?? gui.defaultPermissionMode ?? "完全访问";
|
|
338
352
|
let answeredQuestion = false;
|
|
353
|
+
/**
|
|
354
|
+
* 运行期 CDP 断连的恢复位(issue #27):单次 evaluate 超时或端点抖动不等于 CDP 已死。
|
|
355
|
+
* 首次断连只**重连观察一次**(对齐 qoder/run.ts 与 codex/run.ts 的既有范式)——
|
|
356
|
+
* 重连只用于继续观察,绝不重发任务;重连失败或再次断连才归位到可恢复出口。
|
|
357
|
+
*
|
|
358
|
+
* 护栏从**发送阶段**就生效,而不只是主循环:issue 的物理现场正是「任务已经发出、
|
|
359
|
+
* agent 仍在写产物,MCP 却因一次 poll 失败把任务判死」。
|
|
360
|
+
*/
|
|
361
|
+
let reconnected = false;
|
|
362
|
+
/** 已确认发出的会话身份:随断连一起交回,用户 continue_task 时能回到原会话观察。 */
|
|
363
|
+
const sessionEvidence = () => ({
|
|
364
|
+
id: session.id,
|
|
365
|
+
title: session.title,
|
|
366
|
+
boundProjectPath: ctx.projectPath,
|
|
367
|
+
provider: spec.provider,
|
|
368
|
+
model: spec.model,
|
|
369
|
+
permissionMode: permission,
|
|
370
|
+
});
|
|
371
|
+
const reconnectOnce = async () => {
|
|
372
|
+
try {
|
|
373
|
+
logger.warn("[zcode] 运行期 CDP 连接中断;重连观察一次(不重发任务)");
|
|
374
|
+
cdp.disconnect();
|
|
375
|
+
cdp = await connectStableZcode(ready, { ...gui, launchTimeoutMs: Math.min(gui.launchTimeoutMs, 20_000) }, deps);
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
logger.error(`[zcode] 运行期 CDP 重连失败:${error instanceof Error ? error.message : String(error)}`);
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
const guardRuntimeCdp = async (operation) => {
|
|
384
|
+
try {
|
|
385
|
+
return await operation();
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
const isCdpLoss = error instanceof CdpUnavailableError || error instanceof CdpDisconnectedError;
|
|
389
|
+
if (!isCdpLoss)
|
|
390
|
+
throw error;
|
|
391
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
392
|
+
if (!reconnected) {
|
|
393
|
+
reconnected = true;
|
|
394
|
+
if (await reconnectOnce()) {
|
|
395
|
+
logger.info("[zcode] 运行期 CDP 已重连,继续观察原任务(不重发)");
|
|
396
|
+
try {
|
|
397
|
+
return await operation();
|
|
398
|
+
}
|
|
399
|
+
catch (retryError) {
|
|
400
|
+
if (!(retryError instanceof CdpUnavailableError) &&
|
|
401
|
+
!(retryError instanceof CdpDisconnectedError))
|
|
402
|
+
throw retryError;
|
|
403
|
+
throw new ZcodeRuntimeDisconnect(`ZCode 调试连接在任务运行期重连后再次中断(${retryError instanceof Error ? retryError.message : String(retryError)})`, sessionEvidence());
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
throw new ZcodeRuntimeDisconnect(`ZCode 调试连接在任务运行期中断(${message})`, sessionEvidence());
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
/** 进程侧事实:区分「ZCode 已退出」与「进程还在但端点无响应」——两者的自救动作不同。 */
|
|
411
|
+
const describeProcessState = async () => {
|
|
412
|
+
if (!ready.pid)
|
|
413
|
+
return "未能确认 ZCode 进程 id";
|
|
414
|
+
try {
|
|
415
|
+
const processes = await Promise.resolve(deps.listProcesses());
|
|
416
|
+
return processes.some((process) => process.pid === ready.pid)
|
|
417
|
+
? `ZCode 进程仍在(pid=${ready.pid})但调试端点无响应`
|
|
418
|
+
: `ZCode 进程已退出(pid=${ready.pid} 不在进程表中)——请重启 ZCode 客户端`;
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
return `ZCode 进程状态不可读:${error instanceof Error ? error.message : String(error)}`;
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
/**
|
|
425
|
+
* 窗口侧事实:issue 的物理现场是「agent 被判死后仍在写产物」,因此断连时必须回读
|
|
426
|
+
* 窗口内是否仍有运行信号,避免把「仍在跑」误读成「已停」。
|
|
427
|
+
*/
|
|
428
|
+
const describeRuntimeWindow = async () => {
|
|
429
|
+
try {
|
|
430
|
+
const snapshot = await cdp.poll();
|
|
431
|
+
const running = snapshot.stopVisible || snapshot.loading || snapshot.activeTool;
|
|
432
|
+
return running
|
|
433
|
+
? "窗口内仍有运行信号(stop/loading/tool-call 可见),任务可能仍在继续"
|
|
434
|
+
: "窗口内未见运行信号(任务可能已停止)";
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
return "窗口状态不可读(调试端点无响应)";
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
runtimeDiagnosis = async () => `${await describeProcessState()};${await describeRuntimeWindow()}`;
|
|
339
441
|
/**
|
|
340
442
|
* 无项目模式的就绪判据:必须确认当前会话真的处于「未绑定项目的 default 工作区」。
|
|
341
443
|
* 「不点击项目按钮」不足以证明——当前 UI 可能继承上一次绑定,所以要求触发器文本
|
|
@@ -431,21 +533,35 @@ export async function runZcodeTask(args) {
|
|
|
431
533
|
/**
|
|
432
534
|
* 幂等确认绑定:每轮先读判定,已绑定即通过;未绑定则(必要时先展开菜单)点击目标项目再复检。
|
|
433
535
|
* 不再接受外部传入的 item——那会把「菜单是否恰好还开着」变成隐式前提(issue #24 暴露的脆弱时序)。
|
|
536
|
+
*
|
|
537
|
+
* `unclickable`(issue #27):所有点击尝试都**从未成功发出一次点击**(`clickProject` 每次
|
|
538
|
+
* 返回 `clicked:false`,例如目标项在 UI 上不可达)。继续点下去是徒劳——上层据此回落
|
|
539
|
+
* `selectZcodeFolder` 导入路径,而不是把任务判死在 project_mismatch。
|
|
434
540
|
*/
|
|
435
541
|
const ensureProjectBound = async () => {
|
|
542
|
+
let clickAttempts = 0;
|
|
543
|
+
let clickFailures = 0;
|
|
544
|
+
const attemptClick = async (item) => {
|
|
545
|
+
clickAttempts += 1;
|
|
546
|
+
const click = await cdp.clickProject(item.id, item.path ?? ctx.projectPath);
|
|
547
|
+
if (!click.clicked) {
|
|
548
|
+
clickFailures += 1;
|
|
549
|
+
logger.warn(`[zcode] 项目项点击未生效:reason=${click.reason};target=${item.name};尝试=${clickAttempts}`);
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
return waitBound(cdp, ctx.projectPath, deps);
|
|
553
|
+
};
|
|
436
554
|
for (let round = 0; round <= gui.setupRecoveryMaxRetries; round++) {
|
|
437
555
|
// eslint-disable-next-line no-await-in-loop
|
|
438
556
|
const { verdict, match } = await readBoundVerdict();
|
|
439
557
|
if (verdict.bound)
|
|
440
|
-
return { bound: true, ambiguous: false };
|
|
558
|
+
return { bound: true, ambiguous: false, unclickable: false };
|
|
441
559
|
if (verdict.ambiguous)
|
|
442
|
-
return { bound: false, ambiguous: true };
|
|
560
|
+
return { bound: false, ambiguous: true, unclickable: false };
|
|
443
561
|
if (match.item) {
|
|
444
562
|
// eslint-disable-next-line no-await-in-loop
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if (clicked && (await waitBound(cdp, ctx.projectPath, deps)))
|
|
448
|
-
return { bound: true, ambiguous: false };
|
|
563
|
+
if (await attemptClick(match.item))
|
|
564
|
+
return { bound: true, ambiguous: false, unclickable: false };
|
|
449
565
|
continue;
|
|
450
566
|
}
|
|
451
567
|
// 目标不在当前列表(菜单多未展开):显式展开菜单后再采集一次。
|
|
@@ -459,16 +575,18 @@ export async function runZcodeTask(args) {
|
|
|
459
575
|
// eslint-disable-next-line no-await-in-loop
|
|
460
576
|
const expanded = matchZcodeProject(await cdp.projects(), ctx.projectPath);
|
|
461
577
|
if (expanded.ambiguous)
|
|
462
|
-
return { bound: false, ambiguous: true };
|
|
578
|
+
return { bound: false, ambiguous: true, unclickable: false };
|
|
463
579
|
if (!expanded.item)
|
|
464
580
|
continue;
|
|
465
581
|
// eslint-disable-next-line no-await-in-loop
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
if (clicked && (await waitBound(cdp, ctx.projectPath, deps)))
|
|
469
|
-
return { bound: true, ambiguous: false };
|
|
582
|
+
if (await attemptClick(expanded.item))
|
|
583
|
+
return { bound: true, ambiguous: false, unclickable: false };
|
|
470
584
|
}
|
|
471
|
-
return {
|
|
585
|
+
return {
|
|
586
|
+
bound: false,
|
|
587
|
+
ambiguous: false,
|
|
588
|
+
unclickable: clickAttempts > 0 && clickFailures === clickAttempts,
|
|
589
|
+
};
|
|
472
590
|
};
|
|
473
591
|
if (ctx.resume?.kind === "continue" && ctx.resume.sendMessage) {
|
|
474
592
|
const pending = await cdp.poll();
|
|
@@ -586,20 +704,33 @@ export async function runZcodeTask(args) {
|
|
|
586
704
|
needsUserKind: "setup_recovery",
|
|
587
705
|
pendingQuestion: "项目路径存在歧义,请在 ZCode 中确认目标项目后调用 continue_task。",
|
|
588
706
|
});
|
|
707
|
+
let projectBound = false;
|
|
589
708
|
if (matched.item) {
|
|
590
709
|
const binding = await ensureProjectBound();
|
|
591
|
-
|
|
710
|
+
projectBound = binding.bound;
|
|
711
|
+
if (!projectBound && binding.ambiguous)
|
|
712
|
+
return result({
|
|
713
|
+
hardFailure: true,
|
|
714
|
+
error: `项目绑定重试时目标项目无法唯一匹配:${ctx.projectPath};${await describeBinding()}`,
|
|
715
|
+
endReason: "project_mismatch",
|
|
716
|
+
needsUserKind: "setup_recovery",
|
|
717
|
+
pendingQuestion: "项目绑定尚未确认,请在 ZCode 中确认目标项目后调用 continue_task。",
|
|
718
|
+
});
|
|
719
|
+
if (!projectBound && !binding.unclickable)
|
|
592
720
|
return result({
|
|
593
721
|
hardFailure: true,
|
|
594
|
-
error:
|
|
595
|
-
? `项目绑定重试时目标项目无法唯一匹配:${ctx.projectPath};${await describeBinding()}`
|
|
596
|
-
: `ZCode 项目绑定有限重试均未生效:${ctx.projectPath};${await describeBinding()}`,
|
|
722
|
+
error: `ZCode 项目绑定有限重试均未生效:${ctx.projectPath};${await describeBinding()}`,
|
|
597
723
|
endReason: "project_mismatch",
|
|
598
724
|
needsUserKind: "setup_recovery",
|
|
599
725
|
pendingQuestion: "项目绑定尚未确认,请在 ZCode 中确认目标项目后调用 continue_task。",
|
|
600
726
|
});
|
|
727
|
+
// 目标项就在列表里、却一次点击都没真正落下去(issue #27 的幽灵项/不可达项):
|
|
728
|
+
// 继续重试是徒劳,但也不该直接判死——交给下面的导入路径兜底;
|
|
729
|
+
// allowCreateProject=false 时仍在下一道闸门 fail-closed。
|
|
730
|
+
if (!projectBound)
|
|
731
|
+
logger.warn(`[zcode] 目标项目在列表中但点击始终不生效,回落项目导入路径:${ctx.projectPath}`);
|
|
601
732
|
}
|
|
602
|
-
|
|
733
|
+
if (!projectBound && ctx.allowCreateProject === false) {
|
|
603
734
|
// 明确禁止创建:在打开文件夹面板等任何导入副作用之前停止派发。
|
|
604
735
|
return result({
|
|
605
736
|
hardFailure: true,
|
|
@@ -607,7 +738,7 @@ export async function runZcodeTask(args) {
|
|
|
607
738
|
error: `目标目录未在 ZCode 项目列表中登记,且本次调用禁止自动创建项目(allowCreateProject=false):${ctx.projectPath}。请在 ZCode 中手动添加该项目后重新提交,或省略 allowCreateProject 以允许自动导入。`,
|
|
608
739
|
});
|
|
609
740
|
}
|
|
610
|
-
|
|
741
|
+
if (!projectBound) {
|
|
611
742
|
budget.setStage("准备文件夹面板");
|
|
612
743
|
let pids = [];
|
|
613
744
|
let before;
|
|
@@ -778,11 +909,26 @@ export async function runZcodeTask(args) {
|
|
|
778
909
|
let model = await clickExactWhenReady(cdp, "modelOption", spec.model, deps);
|
|
779
910
|
let provider = { clicked: false, count: 0, available: [] };
|
|
780
911
|
if (!model.clicked) {
|
|
781
|
-
|
|
912
|
+
// issue #27:模型项在 provider 分组的二级子菜单里,**hover 分组**才会渲染子项;
|
|
913
|
+
// 用 click 会选中分组本身或收起菜单,这正是「两级模型菜单点击不稳」的根因。
|
|
914
|
+
provider = await clickExactWhenReady(cdp, "providerOption", spec.provider, deps, "hover");
|
|
782
915
|
if (provider.clicked)
|
|
783
|
-
await deps.sleep(
|
|
916
|
+
await deps.sleep(600);
|
|
784
917
|
model = await clickExactWhenReady(cdp, "modelOption", spec.model, deps);
|
|
785
918
|
}
|
|
919
|
+
/**
|
|
920
|
+
* issue #27 问题三:radix 子菜单由 hover 维持,press 时子菜单可能已收回——
|
|
921
|
+
* 表现为「本轮候选为空」,而不是「模型真的不存在」。此时把菜单重开一次再试一轮,
|
|
922
|
+
* 不要凭一轮空列表就把任务判死在 model_unavailable。
|
|
923
|
+
*/
|
|
924
|
+
if (!model.clicked && !model.count && !provider.clicked) {
|
|
925
|
+
logger.warn("[zcode] 模型项两轮均未命中且候选为空(菜单可能已收起);重开模型菜单后再试一轮");
|
|
926
|
+
await cdp.dismissMenus();
|
|
927
|
+
if (await cdp.click("modelTrigger")) {
|
|
928
|
+
await deps.sleep(250);
|
|
929
|
+
model = await clickExactWhenReady(cdp, "modelOption", spec.model, deps);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
786
932
|
if (!model.clicked)
|
|
787
933
|
return result({
|
|
788
934
|
hardFailure: true,
|
|
@@ -803,6 +949,63 @@ export async function runZcodeTask(args) {
|
|
|
803
949
|
error: `模型切换回读不一致:display=${modelValue.display || "空"},internal=${modelValue.internal || "空"}`,
|
|
804
950
|
endReason: "model_mismatch",
|
|
805
951
|
});
|
|
952
|
+
/**
|
|
953
|
+
* 思考档位(issue #27 问题三):`reasoningLevel` 此前完全没有实现。
|
|
954
|
+
*
|
|
955
|
+
* 档位集合**随模型变化**(真机实测 3.14.3:当前模型只有 开启/关闭 二值),
|
|
956
|
+
* 所以必须在模型确认之后才读界面;未指定档位时完全不碰界面(沿用当前值)。
|
|
957
|
+
* 越权档位与「集合读不到」都在**发送前**报错,绝不静默沿用。
|
|
958
|
+
*/
|
|
959
|
+
if (spec.unsupportedLevel !== undefined)
|
|
960
|
+
return result({
|
|
961
|
+
hardFailure: true,
|
|
962
|
+
error: describeZcodeLevelValueError(spec) ??
|
|
963
|
+
`ZCode 的思考档位不支持「${spec.unsupportedLevel}」`,
|
|
964
|
+
endReason: "reasoning_level_invalid",
|
|
965
|
+
});
|
|
966
|
+
if (spec.level) {
|
|
967
|
+
budget.setStage("确认思考档位");
|
|
968
|
+
const snapshot = await cdp.thoughtLevelSnapshot();
|
|
969
|
+
// 控件本身不存在(该版本/该模型不提供档位选择)与「控件存在但读不到选项」是两种失败,
|
|
970
|
+
// 文案必须区分,否则用户会去翻一个根本不存在的菜单。
|
|
971
|
+
if (!snapshot.triggerMounted)
|
|
972
|
+
return result({
|
|
973
|
+
hardFailure: true,
|
|
974
|
+
endReason: "reasoning_level_unavailable",
|
|
975
|
+
error: "ZCode 界面没有思考档位控件(该版本或该模型不提供档位选择);请移除 reasoningLevel 后重试",
|
|
976
|
+
});
|
|
977
|
+
const tiers = thoughtTierSetOf(snapshot.options.map((option) => option.token || option.text));
|
|
978
|
+
assertZcodeLevelSupported(spec, tiers);
|
|
979
|
+
const current = thoughtLevelOfToken(snapshot.triggerText);
|
|
980
|
+
if (current === spec.level) {
|
|
981
|
+
logger.info(`[zcode] 思考档位已是「${spec.level}」,无需切换`);
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
const target = snapshot.options.find((option) => thoughtLevelOfToken(option.token || option.text) === spec.level);
|
|
985
|
+
if (!target)
|
|
986
|
+
return result({
|
|
987
|
+
hardFailure: true,
|
|
988
|
+
endReason: "reasoning_level_unavailable",
|
|
989
|
+
error: `ZCode 思考档位「${spec.level}」在界面上没有对应选项(界面档位=${snapshot.options.map((option) => option.token || option.text).join("、") || "空"})`,
|
|
990
|
+
});
|
|
991
|
+
if (!(await cdp.clickThoughtLevelOption(target.id)))
|
|
992
|
+
return result({
|
|
993
|
+
hardFailure: true,
|
|
994
|
+
endReason: "reasoning_level_unavailable",
|
|
995
|
+
error: `无法点击 ZCode 思考档位选项:${target.id}(当前显示=${snapshot.triggerText || "空"})`,
|
|
996
|
+
});
|
|
997
|
+
await deps.sleep(250);
|
|
998
|
+
const applied = await cdp.thoughtLevelSnapshot();
|
|
999
|
+
const readback = thoughtLevelOfToken(applied.triggerText);
|
|
1000
|
+
if (readback !== spec.level)
|
|
1001
|
+
return result({
|
|
1002
|
+
hardFailure: true,
|
|
1003
|
+
endReason: "reasoning_level_mismatch",
|
|
1004
|
+
error: `ZCode 思考档位回读不一致:期望「${spec.level}」,实际「${applied.triggerText || "空"}」`,
|
|
1005
|
+
});
|
|
1006
|
+
logger.info(`[zcode] 思考档位已切换为「${spec.level}」`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
806
1009
|
permission = gui.defaultPermissionMode ?? "完全访问";
|
|
807
1010
|
if (!exactUiName(await cdp.text("permissionValue"), permission)) {
|
|
808
1011
|
if (!(await cdp.click("permissionTrigger")))
|
|
@@ -884,7 +1087,7 @@ export async function runZcodeTask(args) {
|
|
|
884
1087
|
const [after, input, polled] = await Promise.all([
|
|
885
1088
|
cdp.conversationText(),
|
|
886
1089
|
cdp.inputText(),
|
|
887
|
-
cdp.poll(),
|
|
1090
|
+
guardRuntimeCdp(() => cdp.poll()),
|
|
888
1091
|
]);
|
|
889
1092
|
seenMessage ||= after.includes(marker);
|
|
890
1093
|
seenStateChange ||= !input.includes(marker);
|
|
@@ -941,7 +1144,7 @@ export async function runZcodeTask(args) {
|
|
|
941
1144
|
error: "ZCode 任务总时限已到;已停止 MCP 等待并保留 ZCode 现场",
|
|
942
1145
|
});
|
|
943
1146
|
await deps.sleep(gui.pollIntervalMs);
|
|
944
|
-
const poll = await cdp.poll();
|
|
1147
|
+
const poll = await guardRuntimeCdp(() => cdp.poll());
|
|
945
1148
|
const verdict = judgeZcodePoll(poll, state, gui.stableRounds, gui.idleTimeoutMs);
|
|
946
1149
|
state = verdict.state;
|
|
947
1150
|
if (Date.now() - lastProgress >= gui.progressIntervalMs) {
|
|
@@ -1011,8 +1214,23 @@ export async function runZcodeTask(args) {
|
|
|
1011
1214
|
});
|
|
1012
1215
|
if (e instanceof ZcodeBudgetError)
|
|
1013
1216
|
return result({ hardFailure: true, endReason: "cdp_disconnected", error: msg });
|
|
1217
|
+
if (e instanceof ZcodeReasoningLevelError)
|
|
1218
|
+
return result({ hardFailure: true, error: msg, endReason: "reasoning_level_invalid" });
|
|
1014
1219
|
if (e instanceof ZcodeModelReadbackError)
|
|
1015
1220
|
return result({ hardFailure: true, error: msg, endReason: "model_mismatch" });
|
|
1221
|
+
if (e instanceof ZcodeRuntimeDisconnect) {
|
|
1222
|
+
const diagnosis = runtimeDiagnosis
|
|
1223
|
+
? await runtimeDiagnosis().catch(() => "窗口状态不可读")
|
|
1224
|
+
: "无法读取 ZCode 窗口状态";
|
|
1225
|
+
// 任务已经发出:不再判死,交回用户处理现场(进程/窗口两侧事实随附),绝不重发。
|
|
1226
|
+
return result({
|
|
1227
|
+
endReason: "needs_user",
|
|
1228
|
+
needsUserKind: "setup_recovery",
|
|
1229
|
+
pendingQuestion: `${msg}。${diagnosis}。请检查 ZCode 客户端后调用 continue_task 继续观察原任务(不会重发任务)。`,
|
|
1230
|
+
session: e.session,
|
|
1231
|
+
progressSummary: "ZCode 运行期 CDP 断连,已保留现场并等待处理",
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1016
1234
|
if (e instanceof CdpDisconnectedError || e instanceof CdpUnavailableError)
|
|
1017
1235
|
return result({ hardFailure: true, error: msg, endReason: "cdp_disconnected" });
|
|
1018
1236
|
return result({ hardFailure: true, error: msg, endReason: "internal" });
|
|
@@ -120,11 +120,12 @@ export const ZCODE_SELECTORS = {
|
|
|
120
120
|
providerOption: {
|
|
121
121
|
primary: '[data-testid^="chat-model-select-group-provider:"]',
|
|
122
122
|
fallbacks: [
|
|
123
|
+
'[data-testid^="chat-model-select-group-registry-provider:"]',
|
|
123
124
|
'[data-testid^="chat-model-select-group-family:"]',
|
|
124
125
|
'[role="option"][data-provider]',
|
|
125
126
|
],
|
|
126
127
|
verifiedVersion: "3.11.x",
|
|
127
|
-
note: "3.11.2 分组前缀漂移为 family
|
|
128
|
+
note: "3.11.2 分组前缀漂移为 family;3.14.3 真机实测为 registry-provider(如 chat-model-select-group-registry-provider:new-provider)。模型项在分组的二级子菜单里,必须 hover 分组才渲染(click 会选中/收起)",
|
|
128
129
|
},
|
|
129
130
|
modelOption: {
|
|
130
131
|
primary: '[data-testid^="chat-model-select-item-"][role="menuitemradio"]',
|
|
@@ -156,9 +157,26 @@ export const ZCODE_SELECTORS = {
|
|
|
156
157
|
},
|
|
157
158
|
permissionOption: {
|
|
158
159
|
primary: '[data-testid^="chat-mode-select-item-"][role="option"]',
|
|
159
|
-
fallbacks: [
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
fallbacks: [
|
|
161
|
+
// 3.14.3 真机实测:权限项 role 是 menuitemradio/menuitemcheckbox(不是 option),
|
|
162
|
+
// 加 role 限制会 0 命中;与 modelOption 一样保留无 role 的兜底候选。
|
|
163
|
+
'[data-testid^="chat-mode-select-item-"]',
|
|
164
|
+
'[role="option"][data-permission]',
|
|
165
|
+
],
|
|
166
|
+
verifiedVersion: "3.14.3-Windows",
|
|
167
|
+
note: "权限候选。真机 testid 形如 chat-mode-select-item-{plan,build,edit,yolo},可见名是项内的直接文本节点(如「完全访问」),其后还跟着一句说明文本",
|
|
168
|
+
},
|
|
169
|
+
thoughtLevelTrigger: {
|
|
170
|
+
primary: '[data-testid="chat-thought-level-select-trigger"]',
|
|
171
|
+
fallbacks: ['[role="combobox"][aria-label*="思考"]', '[role="combobox"][data-state]'],
|
|
172
|
+
verifiedVersion: "3.14.3-Windows",
|
|
173
|
+
note: "思考档位触发器(combobox)。真机实测(2026-09-30,模型 step-plan/step-5-preview):存在且可见,文本为当前档位(如「开启」);档位选项只在菜单展开后挂载",
|
|
174
|
+
},
|
|
175
|
+
thoughtLevelOption: {
|
|
176
|
+
primary: '[data-testid^="chat-thought-level-select-item-"]',
|
|
177
|
+
fallbacks: ['[role="option"][data-thought-level]'],
|
|
178
|
+
verifiedVersion: "3.14.3-Windows",
|
|
179
|
+
note: "思考档位选项。真机实测:testid 后缀为 enabled/disabled(开启/关闭),aria-checked 表示当前档位;档位集合随模型变化,因此集合的唯一判据是这里实际渲染出来的选项",
|
|
162
180
|
},
|
|
163
181
|
workOutsideProject: {
|
|
164
182
|
primary: '[data-testid="composer-work-outside-project"]',
|
package/dist/mcp/handlers.js
CHANGED
|
@@ -19,7 +19,7 @@ import { captureBaseline, gitRefExists, } from "../verify/git-baseline.js";
|
|
|
19
19
|
import { readTextSafe, readJsonSafe } from "../util/fs.js";
|
|
20
20
|
import { readLatestReportSummary } from "../loop/fix-loop.js";
|
|
21
21
|
import { readDirSafe } from "../util/fs.js";
|
|
22
|
-
import { parseZcodeModel } from "../agents/zcode/model.js";
|
|
22
|
+
import { parseZcodeModel, describeZcodeLevelValueError } from "../agents/zcode/model.js";
|
|
23
23
|
import { describeLevelValueError, parseKimicodeModel } from "../agents/kimicode/model.js";
|
|
24
24
|
import { validateTaskReferences } from "../agents/zcode/references.js";
|
|
25
25
|
/** 任务目录里下一可用 report round(避免手动验收覆盖已有 report-0/1…) */
|
|
@@ -264,7 +264,12 @@ function runTaskHandler(ctx, defaults, idempotency) {
|
|
|
264
264
|
if (args.mode !== undefined)
|
|
265
265
|
return errorResult("ZCode 不支持 mode 参数;请移除 mode 后重试");
|
|
266
266
|
try {
|
|
267
|
-
|
|
267
|
+
// 参数级只做「格式 + 取值域」校验(issue #27 问题三):档位集合随模型变化,
|
|
268
|
+
// 只能在运行期读界面实际渲染的选项,故「越权档位」的判定留给 run.ts。
|
|
269
|
+
const spec = parseZcodeModel(args.model, args.reasoningLevel);
|
|
270
|
+
const levelError = describeZcodeLevelValueError(spec);
|
|
271
|
+
if (levelError)
|
|
272
|
+
throw new Error(levelError);
|
|
268
273
|
validateTaskReferences(args.task, args.context, norm);
|
|
269
274
|
}
|
|
270
275
|
catch (e) {
|
|
@@ -451,7 +456,10 @@ async function runTaskWithoutProject(ctx, defaults, args, idempotency) {
|
|
|
451
456
|
return errorResult("无项目模式不支持 dryRun:没有可静态分析的项目基线与文件树。请提供 projectPath,或移除 dryRun。");
|
|
452
457
|
}
|
|
453
458
|
try {
|
|
454
|
-
parseZcodeModel(args.model);
|
|
459
|
+
const spec = parseZcodeModel(args.model, args.reasoningLevel);
|
|
460
|
+
const levelError = describeZcodeLevelValueError(spec);
|
|
461
|
+
if (levelError)
|
|
462
|
+
throw new Error(levelError);
|
|
455
463
|
// 无项目模式不做项目引用解析:识别到本地引用就在发送前说明需要 projectPath。
|
|
456
464
|
validateTaskReferences(args.task, args.context, undefined);
|
|
457
465
|
}
|
package/package.json
CHANGED
|
@@ -69,7 +69,7 @@ run_task(秒回 taskId,异步)
|
|
|
69
69
|
| `projectPath` | 必填 | **可省略**(无项目模式,见 §3.4) | 必填 | 必填 | 必填 | 必填(无项目派发现仅支持 zcode) |
|
|
70
70
|
| `model` | **必填**(面板模型名) | **必填**,`供应商/模型` | 可选 | **必填**(界面模型名) | 可选 | **必填**(界面模型名;精确匹配,未命中回显候选) |
|
|
71
71
|
| `modelSource` | ✗ | ✗ | ✗ | ✗ | 可选 `default`/`custom` | ✗ |
|
|
72
|
-
| `reasoningLevel` | `低/中/高`(`low/medium/high`) |
|
|
72
|
+
| `reasoningLevel` | `低/中/高`(`low/medium/high`) | `低/low`、`中/medium`、`高/high`、`max`、`on/off`(以界面实际档位为准) | ✗ | `低/low`、`高/high`、`max`、`on`、`off` | `低/中/高/极高/最大/关闭思考` | ✗ |
|
|
73
73
|
| `mode` | ✗ | ✗ | **唯一支持**(`Work`/`Code`/`Design`) | ✗ | ✗ | ✗(用 `designDirection`) |
|
|
74
74
|
| `designDirection` | ✗ | ✗ | ✗ | ✗ | ✗ | **必填**(`原型`/`文档`/`网站复刻`) |
|
|
75
75
|
| `planDoc` | 可选 | ✗ | ✗ | ✗ | **必填**且必须可读 | ✗ |
|
|
@@ -92,6 +92,8 @@ run_task(秒回 taskId,异步)
|
|
|
92
92
|
- **`codex`**(默认,推荐先试):ChatGPT/Codex 桌面端,MSIX COM 激活 + CDP(Windows 冷启动实测 60–90 秒,首轮偏慢属正常;macOS spawn `.app` + CDP)。
|
|
93
93
|
`model` 必填且用**面板里的模型名**;`planDoc`/`designSystem` 会拼进初始指令且必须存在、在项目内。**不支持 `mode`**。
|
|
94
94
|
- **`zcode`**:ZCode 桌面端(Electron CDP)。`model` 必填 `供应商/模型`(如 `DeepSeek/deepseek-flash`);发送前确认「完全访问」权限模式;**不支持 `mode`**。支持省略 `projectPath` 的无项目模式(§3.4)。
|
|
95
|
+
`reasoningLevel` 可选:档位集合**随模型变化**,以界面实际渲染的选项为准(真机实测 3.14.3:当前模型只有 `on`/`off` 两档,触发器 `chat-thought-level-select-trigger`)。传了界面不存在的档位在**发送前**报错(`reasoning_level_invalid`),绝不静默沿用;不传则不触碰档位界面(沿用当前值)。**换模型必须重开任务**(`model` 在 `run_task` 时固化)。
|
|
96
|
+
项目绑定按「路径优先、无路径时按显示名」分层判定;3.14.x 侧边栏的 `workspace-item-*` 可能仍挂在 DOM 里但被滚出视口(issue #27:实测 42 个中 40 个不可见),采集时按可见性过滤,不可达时回落「打开文件夹」导入路径。
|
|
95
97
|
- **`traework`**:TraeWork / TRAE SOLO CN。`model` 可选;**唯一支持 `mode`**(不传时从任务书文本识别「切换 X 模式」,识别不到保持 `Work`);实现顺序固定为「新建会话 → 切模式 → 在目标模式内绑定项目」。窗口必须保持可见(发送依赖模拟输入)。**`continue_task` 不支持**。
|
|
96
98
|
- **`kimicode`**:Kimi Code 桌面端(普通 Electron,实测 1.0.2;CDP 基准端口 9666)。`model` 必填,直接填**界面模型名**(如 `K3`、`K2.8 Preview`、`stepfun/step-3.7-flash:free`)。
|
|
97
99
|
`reasoningLevel` 取值域刻意**不含 `中`/`medium`**(那不是任何模型的合法档位):官方模型 `低/low`、`高/high`、`max`;非官方模型只有 `on`/`off`。档位集合以**界面实际渲染的标签**为准,传了界面不存在的档位在发送前报错,绝不静默沿用;不传时官方档位沿用界面当前值、非官方强制 `on`。
|