tianshu-mcp 0.5.2 → 0.5.3
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 +21 -0
- package/CHANGELOG.md +21 -0
- package/README.en.md +5 -3
- package/README.md +5 -3
- package/dist/agents/codex/instance.js +5 -8
- package/dist/agents/gui-instance.js +18 -0
- package/dist/agents/traework/launcher.js +3 -5
- package/dist/agents/zcode/cdp.js +29 -2
- package/dist/agents/zcode/dom.js +4 -1
- package/dist/agents/zcode/instance.js +4 -7
- package/dist/agents/zcode/run.js +41 -7
- package/dist/version.generated.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.en.md
CHANGED
|
@@ -20,6 +20,27 @@ Chinese version: [CHANGELOG.md](CHANGELOG.md)
|
|
|
20
20
|
- Best-effort stop of a GUI-side pending session (via a temporary CDP connection) when cancelling
|
|
21
21
|
a task in the `needs_user` state.
|
|
22
22
|
- Real-machine verification of project-less dispatch on macOS (this round covers Windows 10 only).
|
|
23
|
+
- ZCode's **automatic import of an unregistered project** cannot complete on Windows: the native-panel script relies on `SetForegroundWindow` to bring the dialog forward before activating its address bar, but a child process of a background MCP server is refused by Windows, so the address-bar Edit never appears and the script spins until its deadline (measured: 56s, then classified by `budget.check()` as an exhausted setup budget). PowerShell's stdout is also block-buffered through a pipe, so killing the process loses the buffer and not a single `native:` stage reaches the log, misdirecting diagnosis. Workaround: add the target directory to the ZCode project list manually first; the fix direction is to grab the foreground with `AttachThreadInput` inside the script, or to use a supported ZCode registration entry point.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## [0.5.3] — 2026-09-15
|
|
28
|
+
|
|
29
|
+
**ZCode hardware-revisit fixes (issue #12, second round)**: three defects found on hardware are fixed — GUI instances could not outlive the server on Windows, "new task" did not switch pages and everything waited silently, and send-failure attribution was misleading. See the [v0.5.3 release notes](docs/release-v0.5.3.en.md) and the [Windows 10 acceptance record](docs/zcode-issue-12-windows-evidence.en.md).
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **ZCode / Codex desktop instances could not outlive the server on Windows (found on hardware)**: the `zcode` and `codex` GUI instances were spawned behind a platform branch (`detached: process.platform !== "win32"`) while `traework` already used an unconditional `detached: true`. A minimal experiment (Windows 10 / Node 24.18.0) on the same spawn shows a non-detached child's survival after the parent exits is 0 and a detached child's is 1. As a result, as soon as the MCP server (or a one-shot smoke / probe script) exited on Windows, ZCode was killed along with it and `keptInstance`'s "instance survives the server exit" was a no-op — `needs_user` told the user to "handle it in ZCode, then call continue_task" while the window was already gone. The invariant now lives in one place, `guiInstanceSpawnOptions()`, shared by all three GUI instances (`codex` even branched `unref()` by platform; that branch is gone). The execution-type children in `verify/runner`, `visual/services` and `agents/spawn` keep their platform branch because their semantics are the opposite — they must be terminable as a group.
|
|
34
|
+
- **"New task" reported success without switching pages, then everything waited silently (found on hardware)**: when ZCode is parked on an existing conversation, the top-bar `conversation-new-task` is a lazily mounted icon — the click is dispatched and returns `true` but the page does not switch, and a conversation page's composer does **not** mount `composer-workspace-trigger`. Both the project-less default confirmation and the project-binding wait therefore idled until their deadline and reported nothing better than `needs_user/setup_recovery` (measured: 30 seconds of dead waiting). The draft is now verified by "the project trigger is mounted" after clicking new-task; when it is not, the flow falls back to the sidebar `task-new-button` (reliable on Windows 3.11.2; that fallback previously existed only on the project branch) and only fails closed with `setup_failed` — reporting "the trigger is still not mounted" — when both entry points fail.
|
|
35
|
+
- **Misleading attribution for send failures (found on hardware)**: when the window is minimised or fully occluded, Chromium throttles the page (`visibilityState=hidden`) and the send button becomes unclickable even though it sits inside the viewport — `elementFromPoint` does not hit the button itself. The old message said only "the ZCode send button was not enabled or was covered within the observation window", pointing users at the button; the driver now recognises that state and reports "the ZCode window is not in the foreground" together with the instruction to bring it forward. `Page.bringToFront` was measured to be **unable** to restore an occluded Electron window, so no automatic recovery is pretended.
|
|
36
|
+
|
|
37
|
+
### Tests
|
|
38
|
+
|
|
39
|
+
- Full suite: **532 passed / 10 skipped** (Windows 10 x64, Node 24.18.0), a net gain of 7 cases over v0.5.2: falling back to the sidebar entry when no draft is established and still dispatching, failing closed without sending when neither entry point creates a draft, attributing a send failure to "window not in the foreground" when the page is throttled, keeping the original button attribution when the page is visible, plus 2 regression cases for the GUI-instance spawn invariant (unconditional detached + unref across platforms).
|
|
40
|
+
|
|
41
|
+
### Docs
|
|
42
|
+
|
|
43
|
+
- The [issue #12 Windows 10 acceptance record](docs/zcode-issue-12-windows-evidence.en.md) gains a "second visit (after v0.5.2)" section: results and on-site evidence for 6 hardware runs, the reproduction criteria for the new-task page-switch failure, evidence for each send-stage failure mode, and the facts that were **not** reproduced or **not** verified this round (including that the true cause of the first `send_unknown` is still undetermined and that the new send diagnostic was never reached on hardware).
|
|
23
44
|
|
|
24
45
|
---
|
|
25
46
|
|
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,27 @@
|
|
|
18
18
|
- Codex 与 ZCode GUI 的 macOS 取消/返修/新建项目矩阵(当前两者 darwin 均保持 `research`)。
|
|
19
19
|
- `needs_user` 状态下取消任务时经临时 CDP 连接尽力停止 GUI 内等待中的会话。
|
|
20
20
|
- ZCode 无项目派发在 macOS 上的真机验证(本轮仅 Windows 10 实测)。
|
|
21
|
+
- ZCode **未登记项目**的自动导入在 Windows 上无法完成:原生面板脚本靠 `SetForegroundWindow` 抢前台来激活地址栏,而后台 MCP server 的子进程会被 Windows 拒绝,地址栏 Edit 永不出现,脚本空转到 deadline(实测 56s 后由 `budget.check()` 归类为 setup 预算耗尽);且 PowerShell 的 stdout 在管道里被缓冲、进程被 kill 后缓冲丢失,日志里连一条 `native:` 阶段都看不到,排障方向被误导。临时对策:先在 ZCode 中手动把目标目录加入项目列表;修复方向是脚本内改用 `AttachThreadInput` 抢前台(或改走 ZCode 受支持的登记入口)。
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## [0.5.3] — 2026-09-15
|
|
26
|
+
|
|
27
|
+
**ZCode 真机回访修复(issue #12 第二轮)**:修复 Windows 上 GUI 实例无法跨 server 退出驻留、新建任务不切页导致静默空等、发送失败归因误导三个真机缺陷。完整说明见 [v0.5.3 发布说明](docs/release-v0.5.3.md),真机证据见 [Windows 10 验收记录](docs/zcode-issue-12-windows-evidence.md)。
|
|
28
|
+
|
|
29
|
+
### 修复
|
|
30
|
+
|
|
31
|
+
- **ZCode / Codex 桌面实例在 Windows 上无法跨 server 退出驻留(真机发现)**:`zcode`、`codex` 的 GUI 实例此前按平台分支 spawn(`detached: process.platform !== "win32"`),而 `traework` 用的是无条件 `detached: true`。最小实验(Windows 10 / Node 24.18.0)显示同一段 spawn:non-detached 子进程在父进程退出后存活 0,detached 存活 1。因此 Windows 上 MCP server(或一次性 smoke / probe 脚本)一退出,ZCode 就被连坐杀掉,`keptInstance` 的「实例跨 server 退出驻留」形同虚设——`needs_user` 提示「请在 ZCode 中处理后再调用 continue_task」,而窗口其实已经消失。现把该不变量收敛为单一来源 `guiInstanceSpawnOptions()`,三处 GUI 实例共用(`codex` 原先连 `unref()` 都带平台分支,一并去掉);`verify/runner`、`visual/services`、`agents/spawn` 这些**需要整组终止**的执行型子进程语义相反,继续按平台分支,不受影响。
|
|
32
|
+
- **新建任务点击返回成功但页面不切换,后续一路静默空等(真机发现)**:ZCode 停在已有会话时,顶部 `conversation-new-task` 是惰性挂载的图标——点击派发成功(返回 `true`)却不切换页面,而会话页的 composer **不挂载** `composer-workspace-trigger`。于是无项目模式的 default 确认、有项目模式的绑定等待都会空等到截止时间,最后只报一句 `needs_user/setup_recovery`(实测空转 30 秒)。现在新建任务后以「项目触发器已挂载」验证草稿**真的**建立;未建立则回退侧栏 `task-new-button`(Windows 3.11.2 实测可靠,此前该兜底只覆盖有项目模式),两者都失败才以 `setup_failed` fail-closed 并报出「触发器仍未挂载」。
|
|
33
|
+
- **发送失败归因误导(真机发现)**:窗口被最小化或完全遮挡时页面被 Chromium 节流(`visibilityState=hidden`),发送按钮「明明在视口内」却点不到,`elementFromPoint` 命中的也不是按钮本身。旧文案只报「ZCode 发送按钮未在观察期内启用或被遮挡」,会把用户引向按钮;现在会识别该状态并报出「ZCode 窗口当前不在前台」及把窗口置于前台的操作指引。`Page.bringToFront` 经真机实测**无法**恢复被遮挡的 Electron 窗口,因此不假装能自动恢复。
|
|
34
|
+
|
|
35
|
+
### 测试
|
|
36
|
+
|
|
37
|
+
- 全量 **532 passed / 10 skipped**(Windows 10 x64,Node 24.18.0),较 v0.5.2 净增 7 项用例——草稿未建立时回退侧栏入口并完成派发、两个入口都建立不了草稿时 fail-closed 且不发送、页面被节流时发送失败归因为窗口不在前台、页面可见时保留原有的按钮归因文案,以及 GUI 实例 spawn 不变量(跨平台无条件 detached + unref)的 2 项回归。
|
|
38
|
+
|
|
39
|
+
### 文档
|
|
40
|
+
|
|
41
|
+
- [issue #12 Windows 10 真机验收记录](docs/zcode-issue-12-windows-evidence.md) 追加「第二轮回访(v0.5.2 之后)」:6 次真机运行的结果与现场取证、`newTask` 不切页的复现判据、发送阶段各失败面的证据,以及本轮**未复现 / 未验证**的事实(含第 1 次 `send_unknown` 的真因仍未定、`sendMessage` 诊断文案在真机上未走到)。
|
|
21
42
|
|
|
22
43
|
---
|
|
23
44
|
|
package/README.en.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
# tianshu-mcp
|
|
10
10
|
|
|
11
|
-
Visual acceptance (since v0.5.0): [English guide](docs/visual-acceptance.en.md) · [Validation record](docs/visual-validation.en.md) · [Latest release notes](<docs/release-v0.5.
|
|
11
|
+
Visual acceptance (since v0.5.0): [English guide](docs/visual-acceptance.en.md) · [Validation record](docs/visual-validation.en.md) · [Latest release notes](<docs/release-v0.5.3.en.md>).
|
|
12
12
|
|
|
13
13
|
**Tianshu × AI-Agent orchestration MCP server**
|
|
14
14
|
|
|
@@ -196,6 +196,8 @@ Use `server.log` when troubleshooting connections; do not treat stderr output it
|
|
|
196
196
|
| [docs/codex-gui-cdp.en.md](docs/codex-gui-cdp.en.md) | Codex desktop GUI driver: MSIX COM activation, CDP attach, selectors, run detection, verify/repair |
|
|
197
197
|
| [docs/codex-windows-smoke.en.md](docs/codex-windows-smoke.en.md) | Codex Windows hardware record (incl. verify-fail → auto plan → repair-pass loop) |
|
|
198
198
|
| [docs/release-v0.3.4.en.md](<docs/release-v0.3.4.en.md>) | v0.3.4 release notes (ZCode project/model read-back, initialization recovery, session dispatch confirmation, issues #8/#9/#10) |
|
|
199
|
+
| [docs/release-v0.5.3.en.md](<docs/release-v0.5.3.en.md>) | v0.5.3 release notes (ZCode hardware-revisit fixes: instance survival across server exit, new-task page switch, send-failure attribution) |
|
|
200
|
+
| [docs/release-v0.5.2.en.md](<docs/release-v0.5.2.en.md>) | v0.5.2 release notes (ZCode project-less dispatch and `allowCreateProject`, issue #12) |
|
|
199
201
|
| [docs/release-v0.5.1.en.md](<docs/release-v0.5.1.en.md>) | v0.5.1 release notes (skill/validation docs, archived platform evidence, lockfile version sync; no runtime changes) |
|
|
200
202
|
| [docs/release-v0.5.0.en.md](<docs/release-v0.5.0.en.md>) | v0.5.0 release notes (optional visual acceptance: screenshots, image specs, baseline approval, offline report) |
|
|
201
203
|
| [docs/visual-acceptance.en.md](<docs/visual-acceptance.en.md>) | Visual acceptance primer and full configuration: three page sources, baseline candidates/approval, rule freezing, thresholds and troubleshooting |
|
|
@@ -240,7 +242,7 @@ Use `server.log` when troubleshooting connections; do not treat stderr output it
|
|
|
240
242
|
- **Engineering / CI** ✅
|
|
241
243
|
- GitHub Actions: `CI` (`build-test` ubuntu/windows/macos × Node 20/22/24 + `pack-check`, plus a `visual-browser` real-browser matrix ubuntu/windows/macos-15-intel/macos-15 × Node 20/22/24, all green with the v0.5.1 tag) and `Release` (tag-triggered) both green
|
|
242
244
|
- Skill self-install verified idempotent on this machine's real `~/.rivet/skills/tianshu-mcp`
|
|
243
|
-
- npm package name `tianshu-mcp` published continuously since v0.1.1 (currently `0.5.
|
|
245
|
+
- npm package name `tianshu-mcp` published continuously since v0.1.1 (currently `0.5.3`)
|
|
244
246
|
- **Real Tianshu host integration (DoD #6)** ✅ (2026-09-07)
|
|
245
247
|
- Configured the local mode in the real `D:\Tianshu` desktop host `mcp.servers` → sidecar reported `MCP: 2 servers connected, 10 tools` (including this server's 8 tools), spawned the child process and connected over stdio
|
|
246
248
|
- Exposed and fixed a skill-install source-path bug (fileURLToPath, commit 55cf2d0)
|
|
@@ -386,7 +388,7 @@ Behavior and limits:
|
|
|
386
388
|
|
|
387
389
|
| Document | Content |
|
|
388
390
|
|---|---|
|
|
389
|
-
| [CHANGELOG.en.md](<CHANGELOG.en.md>) | Version history (v0.1.0 → v0.5.
|
|
391
|
+
| [CHANGELOG.en.md](<CHANGELOG.en.md>) | Version history (v0.1.0 → v0.5.3) |
|
|
390
392
|
| [CONTRIBUTING.en.md](CONTRIBUTING.en.md) | Dev setup, conventions, commit/release flow, adding an agent |
|
|
391
393
|
| [SECURITY.en.md](SECURITY.en.md) | Security model (zero credentials / command whitelist / process & desktop-automation boundaries) and private reporting |
|
|
392
394
|
| [CODE_OF_CONDUCT.en.md](CODE_OF_CONDUCT.en.md) | Contributor Code of Conduct |
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
# tianshu-mcp
|
|
10
10
|
|
|
11
|
-
视觉验收(v0.5.0 起):[中文指南](docs/visual-acceptance.md) · [验证记录](docs/visual-validation.md) · [最新发布说明](<docs/release-v0.5.
|
|
11
|
+
视觉验收(v0.5.0 起):[中文指南](docs/visual-acceptance.md) · [验证记录](docs/visual-validation.md) · [最新发布说明](<docs/release-v0.5.3.md>)。
|
|
12
12
|
|
|
13
13
|
**天枢 × AI-Agent 编排 MCP server**
|
|
14
14
|
|
|
@@ -191,6 +191,8 @@ ZCode 提问、需要登录、旧实例无 CDP、系统权限不足,或自动
|
|
|
191
191
|
| [docs/zcode-windows-smoke.md](docs/zcode-windows-smoke.md) | ZCode Windows 真机开发、同会话返修与提问续跑验收记录 |
|
|
192
192
|
| [docs/codex-gui-cdp.md](docs/codex-gui-cdp.md) | Codex 桌面端 GUI 驱动:MSIX COM 激活、CDP 接管、选择器、运行检测、验收返修 |
|
|
193
193
|
| [docs/codex-windows-smoke.md](docs/codex-windows-smoke.md) | Codex Windows 真机验收记录(含验收失败→自动生成计划→返修通过闭环) |
|
|
194
|
+
| [docs/release-v0.5.3.md](<docs/release-v0.5.3.md>) | v0.5.3 发布说明(ZCode 真机回访修复:实例跨 server 驻留、新建任务切页、发送失败归因) |
|
|
195
|
+
| [docs/release-v0.5.2.md](<docs/release-v0.5.2.md>) | v0.5.2 发布说明(ZCode 无项目派发与 `allowCreateProject`,issue #12) |
|
|
194
196
|
| [docs/release-v0.5.1.md](<docs/release-v0.5.1.md>) | v0.5.1 发布说明(技能/验证文档补齐、平台证据归档、锁文件版本同步;无运行时变更) |
|
|
195
197
|
| [docs/release-v0.5.0.md](<docs/release-v0.5.0.md>) | v0.5.0 发布说明(可选视觉验收模块:截图对比、图片规格、基准批准、离线报告) |
|
|
196
198
|
| [docs/visual-acceptance.md](<docs/visual-acceptance.md>) | 视觉验收入门与完整配置:三种页面来源、基准候选/批准、规则冻结、阈值与排查 |
|
|
@@ -239,7 +241,7 @@ ZCode 提问、需要登录、旧实例无 CDP、系统权限不足,或自动
|
|
|
239
241
|
- **工程 / CI** ✅
|
|
240
242
|
- GitHub Actions:`CI`(`build-test` ubuntu/windows/macos × Node 20/22/24 + `pack-check`,另加 `visual-browser` 真实浏览器矩阵 ubuntu/windows/macos-15-intel/macos-15 × Node 20/22/24,随 v0.5.1 tag 全绿)与 `Release`(tag 触发)均绿
|
|
241
243
|
- 技能自检安装已在本机真实 `~/.rivet/skills/tianshu-mcp` 验证生效且幂等
|
|
242
|
-
- npm 包名 `tianshu-mcp` 自 v0.1.1 起持续发布(当前 `0.5.
|
|
244
|
+
- npm 包名 `tianshu-mcp` 自 v0.1.1 起持续发布(当前 `0.5.3`)
|
|
243
245
|
- **天枢宿主真实接入(DoD #6)** ✅(2026-09-07,[host-integration-record.md](docs/host-integration-record.md))
|
|
244
246
|
- 在真实 `D:\Tianshu` 桌面宿主 `mcp.servers` 配置本地模式 → sidecar `MCP: 2 servers connected, 10 tools`(含本 server 8 工具),spawn 子进程并 stdio 连通
|
|
245
247
|
- 实测暴露并修复技能安装源路径 bug(fileURLToPath,提交 55cf2d0)
|
|
@@ -386,7 +388,7 @@ run_task(projectPath=/path/to/项目, agentId=codex-cli, task="任务书", autoV
|
|
|
386
388
|
| 文档 | 内容 |
|
|
387
389
|
|---|---|
|
|
388
390
|
| [HANDOFF.md](HANDOFF.md) | 项目交接文档:当前状态快照、架构导览、硬性红线、已知限制、接手建议 |
|
|
389
|
-
| [CHANGELOG.md](<CHANGELOG.md>) | 版本变更日志(v0.1.0 → v0.5.
|
|
391
|
+
| [CHANGELOG.md](<CHANGELOG.md>) | 版本变更日志(v0.1.0 → v0.5.3) |
|
|
390
392
|
| [CONTRIBUTING.md](CONTRIBUTING.md) | 开发环境、工程规范、提交与发布流程、如何新增 agent |
|
|
391
393
|
| [SECURITY.md](SECURITY.md) | 安全模型(凭证零管理/命令白名单/进程与桌面自动化边界)与私密报告渠道 |
|
|
392
394
|
| [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) | 贡献者行为准则 |
|
|
@@ -12,6 +12,7 @@ import { activateCodexApp, buildActivationArgs, buildSpawnArgs } from "./launche
|
|
|
12
12
|
import { expandEnvPath } from "../../util/path.js";
|
|
13
13
|
import { execFileAsync } from "../../verify/exec.js";
|
|
14
14
|
import { TtlCache } from "../../util/ttl-cache.js";
|
|
15
|
+
import { guiInstanceSpawnOptions } from "../gui-instance.js";
|
|
15
16
|
export function parseProcessRows(raw) {
|
|
16
17
|
const out = [];
|
|
17
18
|
for (const line of raw.split(/\r?\n/)) {
|
|
@@ -214,15 +215,11 @@ export async function ensureCodexInstance(candidate, gui, logger) {
|
|
|
214
215
|
else {
|
|
215
216
|
const argv = buildSpawnArgs(userDataDir, port);
|
|
216
217
|
logger.info(`[codex] 启动受管实例:${candidate.path},CDP 端口 ${port},profile=${userDataDir}`);
|
|
217
|
-
//
|
|
218
|
-
// unref
|
|
219
|
-
const child = spawn(candidate.path, argv,
|
|
220
|
-
detached: process.platform !== "win32",
|
|
221
|
-
stdio: "ignore",
|
|
222
|
-
});
|
|
218
|
+
// 桌面实例必须 detached:不变量与实测依据见 guiInstanceSpawnOptions(Windows 同样适用);
|
|
219
|
+
// unref 使其不拖住父进程退出。
|
|
220
|
+
const child = spawn(candidate.path, argv, guiInstanceSpawnOptions(false));
|
|
223
221
|
spawnState.child = child;
|
|
224
|
-
|
|
225
|
-
child.unref();
|
|
222
|
+
child.unref();
|
|
226
223
|
child.on("error", (e) => {
|
|
227
224
|
spawnState.error = e instanceof Error ? e : new Error(String(e));
|
|
228
225
|
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GUI 桌面实例(ZCode / Codex / TraeWork)的 spawn 选项。
|
|
3
|
+
*
|
|
4
|
+
* `detached: true` 是**不变量**,不是平台偏好:桌面实例必须自成进程组、脱离父进程,否则父进程
|
|
5
|
+
* (MCP server,或一次性 smoke / probe 脚本)退出时会把它连坐杀掉,`run_task` 的 `keptInstance`
|
|
6
|
+
* (「实例跨 server 退出驻留」)语义随之失效——表现为 `needs_user` 提示「请在 ZCode 中处理后再
|
|
7
|
+
* 调用 continue_task」,而窗口其实已经消失,用户根本无从操作。
|
|
8
|
+
*
|
|
9
|
+
* 真机实测(Windows 10 / Node 24.18.0,2026-09-15):同一段 spawn,non-detached 子进程在父进程
|
|
10
|
+
* 退出后存活 0,detached 存活 1;对照组是 detached 启动的 ZCode 实例跨多次 server 退出仍驻留。
|
|
11
|
+
*
|
|
12
|
+
* 注意:不要把它套到「执行型子进程」上:`verify/runner`、`visual/services`、`agents/spawn` 的语义是
|
|
13
|
+
* **可被整组 SIGTERM/SIGKILL 终止**,因此它们继续按平台分支(POSIX detached、Windows 用
|
|
14
|
+
* taskkill /T),与这里的驻留语义相反。
|
|
15
|
+
*/
|
|
16
|
+
export function guiInstanceSpawnOptions(windowsHide = true) {
|
|
17
|
+
return { detached: true, stdio: "ignore", windowsHide };
|
|
18
|
+
}
|
|
@@ -14,6 +14,7 @@ import path from "node:path";
|
|
|
14
14
|
import { spawn } from "node:child_process";
|
|
15
15
|
import { TraeworkCdpClient } from "./cdp/client.js";
|
|
16
16
|
import { execFileAsync } from "../../verify/exec.js";
|
|
17
|
+
import { guiInstanceSpawnOptions } from "../gui-instance.js";
|
|
17
18
|
/** 端口是否空闲(可绑定) */
|
|
18
19
|
export function isPortFree(port, host = "127.0.0.1") {
|
|
19
20
|
return new Promise((resolve) => {
|
|
@@ -101,11 +102,8 @@ export function launchInstance(opts) {
|
|
|
101
102
|
const args = gui.exeArgs.map((a) => a.replace(/<port>/g, String(port)));
|
|
102
103
|
// 原生 Windows 路径:确保 exePath 用反斜杠形式传给 GUI 进程
|
|
103
104
|
const nativeExe = process.platform === "win32" ? path.win32.normalize(exePath) : exePath;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
stdio: "ignore",
|
|
107
|
-
windowsHide: false, // 窗口必须可见:发送依赖模拟输入
|
|
108
|
-
});
|
|
105
|
+
// 窗口必须可见:发送依赖模拟输入
|
|
106
|
+
const child = spawn(nativeExe, args, guiInstanceSpawnOptions(false));
|
|
109
107
|
child.unref();
|
|
110
108
|
const pid = child.pid ?? -1;
|
|
111
109
|
const commandLine = [nativeExe, ...args].join(" ");
|
package/dist/agents/zcode/cdp.js
CHANGED
|
@@ -20,6 +20,12 @@ export async function retryZcodeEvaluation(operation, sleep = (ms) => new Promis
|
|
|
20
20
|
}
|
|
21
21
|
/** 点击后确认菜单打开的轮询间隔(不是独立超时;窗口由调用方从集中配置与总预算算出)。 */
|
|
22
22
|
const PROJECT_MENU_POLL_MS = 100;
|
|
23
|
+
/**
|
|
24
|
+
* 触发器点击被吞后的重试间隔。真机实测:ZCode 窗口被其他窗口完全遮挡时,Chromium 判定
|
|
25
|
+
* occluded 并节流页面,合成鼠标事件常被吞掉(首次点击无效、第二次才打开菜单),
|
|
26
|
+
* 所以「点一次然后干等」会必然失败。重试有界,不重置调用方给的 deadline。
|
|
27
|
+
*/
|
|
28
|
+
const TRIGGER_RECLICK_MS = 1_500;
|
|
23
29
|
export class ZcodeCdpClient {
|
|
24
30
|
selectors;
|
|
25
31
|
inner;
|
|
@@ -122,8 +128,15 @@ export class ZcodeCdpClient {
|
|
|
122
128
|
return { opened: true, probe };
|
|
123
129
|
if (!probe.ready || !probe.point)
|
|
124
130
|
return { opened: false, reason: "not-ready", probe };
|
|
125
|
-
|
|
131
|
+
let lastClick = 0;
|
|
126
132
|
while (Date.now() < deadline) {
|
|
133
|
+
// 有界重试点击:窗口被遮挡时节流会让首次点击落空(真机实测第二次才生效)。
|
|
134
|
+
// 每轮都先确认菜单未开,因此不会把已经打开的菜单 toggle 掉。
|
|
135
|
+
if (Date.now() - lastClick >= TRIGGER_RECLICK_MS) {
|
|
136
|
+
// eslint-disable-next-line no-await-in-loop
|
|
137
|
+
await this.clickAt(probe.point.x, probe.point.y);
|
|
138
|
+
lastClick = Date.now();
|
|
139
|
+
}
|
|
127
140
|
// eslint-disable-next-line no-await-in-loop
|
|
128
141
|
if (await this.projectMenuOpen())
|
|
129
142
|
return { opened: true, probe };
|
|
@@ -218,7 +231,21 @@ export class ZcodeCdpClient {
|
|
|
218
231
|
}
|
|
219
232
|
await this.evaluate("new Promise(resolve=>setTimeout(resolve,200))");
|
|
220
233
|
}
|
|
221
|
-
|
|
234
|
+
// 窗口被遮挡/最小化时 Chromium 会节流页面:合成鼠标事件与 elementFromPoint 都不可靠,
|
|
235
|
+
// 按钮「明明在视口内」却点不到(3.11.2-Windows 真机实测 visibilityState=hidden、
|
|
236
|
+
// elementFromPoint 命中非按钮节点)。此时只报按钮会把用户引向错误方向;
|
|
237
|
+
// Page.bringToFront 实测无法恢复被遮挡的 Electron 窗口,所以如实报出真因并给出
|
|
238
|
+
// 可操作指引,不假装能自动恢复。
|
|
239
|
+
let throttled = false;
|
|
240
|
+
try {
|
|
241
|
+
throttled = await this.evaluate("document.visibilityState === 'hidden' || document.hidden === true");
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
throttled = false;
|
|
245
|
+
}
|
|
246
|
+
throw new Error(throttled
|
|
247
|
+
? "ZCode 发送按钮在观察期内不可点击:ZCode 窗口当前不在前台(页面被节流,合成点击不可靠)——请把 ZCode 窗口置于前台后重试;未发送任务"
|
|
248
|
+
: "ZCode 发送按钮未在观察期内启用或被遮挡;未发送任务");
|
|
222
249
|
}
|
|
223
250
|
async answerQuestion(answer) {
|
|
224
251
|
const located = await this.evaluate(`(function(){const norm=s=>(s||'').normalize('NFKC').trim().toLocaleLowerCase();const target=norm(${JSON.stringify(answer)});const visible=e=>{const r=e.getBoundingClientRect(),s=getComputedStyle(e);return r.width>0&&r.height>0&&r.bottom>0&&r.right>0&&r.top<innerHeight&&r.left<innerWidth&&s.display!=='none'&&s.visibility!=='hidden'};const boxes=[...document.querySelectorAll('[role="listbox"][aria-label]:has([role="option"])')].filter(visible);if(boxes.length!==1)return {count:boxes.length,available:[],buttonStates:[]};const box=boxes[0];const options=[...box.querySelectorAll('[role="option"]')].filter(visible);const label=e=>{const leaves=[...e.querySelectorAll('*')].filter(n=>n.children.length===0).map(n=>(n.textContent||'').trim()).filter(Boolean);return leaves.find(x=>norm(x)===target)||e.getAttribute('aria-label')||e.getAttribute('data-value')||''};const available=options.map(e=>{const leaves=[...e.querySelectorAll('*')].filter(n=>n.children.length===0).map(n=>(n.textContent||'').trim()).filter(Boolean);return leaves.find(x=>x&&!/^\\d+[.\uff0e]$/.test(x))||(e.textContent||'').trim()}).filter(Boolean);const matches=options.filter(e=>norm(label(e))===target);let scope=box,buttons=[];while(scope.parentElement){scope=scope.parentElement;buttons=[...scope.querySelectorAll('button:not([role="option"])')].filter(visible);if(buttons.length)break}if(matches.length!==1)return {count:matches.length,available,buttonStates:buttons.map(e=>e.disabled||e.getAttribute('aria-disabled')==='true')};matches[0].scrollIntoView({block:'center'});const r=matches[0].getBoundingClientRect();return {count:1,available,point:{x:r.left+r.width/2,y:r.top+r.height/2},buttonStates:buttons.map(e=>e.disabled||e.getAttribute('aria-disabled')==='true')}})()`);
|
package/dist/agents/zcode/dom.js
CHANGED
|
@@ -92,7 +92,10 @@ export function projectTriggerProbeExpression(overrides) {
|
|
|
92
92
|
for (const s of sels) for (const e of document.querySelectorAll(s)) mountedNodes.add(e);
|
|
93
93
|
const mounted = mountedNodes.size;
|
|
94
94
|
const m = pick(sels);
|
|
95
|
-
|
|
95
|
+
// 窗口被其他窗口完全遮挡时 Chromium 判定 occluded 并节流页面,合成点击常被吞掉——
|
|
96
|
+
// 这个环境事实必须随探测结果一起回传,否则失败信息会把用户引向错误方向。
|
|
97
|
+
const pageHidden = document.visibilityState === 'hidden';
|
|
98
|
+
const base = { selector: m.selector, count: m.count, mounted, ready: false, pageHidden };
|
|
96
99
|
if (!mounted) return Object.assign({}, base, { state: 'missing' });
|
|
97
100
|
if (m.count === 0)
|
|
98
101
|
return Object.assign({}, base, { state: 'hidden', detail: describe(mountedNodes.values().next().value) });
|
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { TraeworkCdpClient } from "../traework/cdp/client.js";
|
|
7
7
|
import { execFileAsync } from "../../verify/exec.js";
|
|
8
8
|
import { TtlCache } from "../../util/ttl-cache.js";
|
|
9
|
+
import { guiInstanceSpawnOptions } from "../gui-instance.js";
|
|
9
10
|
export function parseProcessRows(raw) {
|
|
10
11
|
const out = [];
|
|
11
12
|
for (const line of raw.split(/\r?\n/)) {
|
|
@@ -159,13 +160,9 @@ export async function ensureZcodeInstance(exePath, gui, logger, options = {}) {
|
|
|
159
160
|
throw new Error(`ZCode CDP 端口 ${port} 已被占用`);
|
|
160
161
|
const args = gui.exeArgs.map((arg) => arg.replaceAll("<port>", String(port)));
|
|
161
162
|
options.signal?.throwIfAborted();
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
const child = spawn(exePath, args,
|
|
165
|
-
detached: process.platform !== "win32",
|
|
166
|
-
stdio: "ignore",
|
|
167
|
-
windowsHide: false,
|
|
168
|
-
});
|
|
163
|
+
// 桌面实例必须 detached:不变量与实测依据见 guiInstanceSpawnOptions。Windows 上此前按平台
|
|
164
|
+
// 分支给 false,父进程退出会连坐,实例无法跨 server 退出驻留(2026-09-15 真机实测修正)。
|
|
165
|
+
const child = spawn(exePath, args, guiInstanceSpawnOptions(false));
|
|
169
166
|
let launchError;
|
|
170
167
|
child.once("error", (error) => {
|
|
171
168
|
launchError = error;
|
package/dist/agents/zcode/run.js
CHANGED
|
@@ -153,7 +153,7 @@ async function clickAnyExactWhenReady(cdp, key, values, deps) {
|
|
|
153
153
|
function describeProjectTriggerFailure(outcome) {
|
|
154
154
|
const { probe, reason } = outcome;
|
|
155
155
|
if (reason === "menu-not-open")
|
|
156
|
-
return `点击项目触发器后项目菜单未打开(selector=${probe.selector || "n/a"},匹配 ${probe.count}
|
|
156
|
+
return `点击项目触发器后项目菜单未打开(selector=${probe.selector || "n/a"},匹配 ${probe.count} 个可见节点${probe.pageHidden ? ";ZCode 窗口当前不可见,浏览器已节流该页面,点击可能被吞——请把 ZCode 窗口置于前台后重试" : ""})`;
|
|
157
157
|
switch (probe.state) {
|
|
158
158
|
case "ambiguous":
|
|
159
159
|
return `无法打开 ZCode 项目列表:项目触发器不唯一(匹配 ${probe.count} 个可见节点,selector=${probe.selector})`;
|
|
@@ -261,6 +261,43 @@ export async function runZcodeTask(args) {
|
|
|
261
261
|
},
|
|
262
262
|
});
|
|
263
263
|
await cdp.dismissMenus();
|
|
264
|
+
// 等待并确认项目触发器就绪的共享截止时间:上限来自集中配置(gui.projectTriggerTimeoutMs),
|
|
265
|
+
// 并被 setup 恢复预算与任务总时限夹住。回退重试不重置它,也不新增第二个魔法超时值。
|
|
266
|
+
const projectTriggerDeadline = () => Math.min(Date.now() + gui.projectTriggerTimeoutMs, started + ctx.taskTimeoutMs, started + gui.setupRecoveryTimeoutMs);
|
|
267
|
+
/**
|
|
268
|
+
* 新建任务后必须确认**草稿真的建立了**,不能只信点击的返回值。
|
|
269
|
+
* 3.11.2-Windows 真机实测:页面停在已有会话时,顶部 `conversation-new-task` 点击返回 true
|
|
270
|
+
* 却不切换页面,composer 上根本不挂载 `composer-workspace-trigger`(会话页不挂载它),
|
|
271
|
+
* 于是后续 default 确认与项目绑定会一路空等到 deadline,最后只报一句 needs_user。
|
|
272
|
+
* 侧栏 `task-new-button` 实测可靠。判据取「项目触发器已挂载」:它只存在于草稿页,
|
|
273
|
+
* 既证明草稿建立,也证明后续等待有依托。
|
|
274
|
+
*/
|
|
275
|
+
const draftReady = async () => {
|
|
276
|
+
const deadline = projectTriggerDeadline();
|
|
277
|
+
for (;;) {
|
|
278
|
+
// eslint-disable-next-line no-await-in-loop
|
|
279
|
+
const probe = await cdp.probeProjectTrigger();
|
|
280
|
+
if (probe.mounted > 0)
|
|
281
|
+
return true;
|
|
282
|
+
if (Date.now() >= deadline)
|
|
283
|
+
return false;
|
|
284
|
+
// eslint-disable-next-line no-await-in-loop
|
|
285
|
+
await deps.sleep(Math.min(250, Math.max(0, deadline - Date.now())));
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
const ensureFreshDraft = async () => {
|
|
289
|
+
const topClicked = await cdp.click("newTask");
|
|
290
|
+
if (topClicked && (await draftReady()))
|
|
291
|
+
return true;
|
|
292
|
+
logger.warn(`[zcode] 顶部新建任务按钮未建立草稿(clicked=${topClicked});回退侧栏新建任务按钮`);
|
|
293
|
+
const sidebarClicked = await cdp.click("newTaskSidebar");
|
|
294
|
+
if (sidebarClicked && (await draftReady())) {
|
|
295
|
+
logger.info("[zcode] 已通过侧栏新建任务按钮进入新草稿");
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
logger.warn(`[zcode] 未能进入新草稿(顶部点击=${topClicked},侧栏点击=${sidebarClicked}):项目触发器始终未挂载`);
|
|
299
|
+
return false;
|
|
300
|
+
};
|
|
264
301
|
const initialDispatch = !ctx.resume ||
|
|
265
302
|
(ctx.resume.kind === "continue" &&
|
|
266
303
|
!ctx.resume.sendMessage &&
|
|
@@ -280,10 +317,10 @@ export async function runZcodeTask(args) {
|
|
|
280
317
|
endReason: "session_lost",
|
|
281
318
|
});
|
|
282
319
|
}
|
|
283
|
-
else if (gui.freshSession && !(await
|
|
320
|
+
else if (gui.freshSession && !(await ensureFreshDraft()))
|
|
284
321
|
return result({
|
|
285
322
|
hardFailure: true,
|
|
286
|
-
error: "无法创建新的 ZCode
|
|
323
|
+
error: "无法创建新的 ZCode 任务会话:点击新建任务后项目触发器仍未挂载,未能进入草稿页",
|
|
287
324
|
endReason: "setup_failed",
|
|
288
325
|
});
|
|
289
326
|
await deps.sleep(500);
|
|
@@ -298,9 +335,6 @@ export async function runZcodeTask(args) {
|
|
|
298
335
|
let session = activeSession;
|
|
299
336
|
let permission = ctx.resume?.permissionMode ?? gui.defaultPermissionMode ?? "完全访问";
|
|
300
337
|
let answeredQuestion = false;
|
|
301
|
-
// 等待并确认项目触发器就绪的共享截止时间:上限来自集中配置(gui.projectTriggerTimeoutMs),
|
|
302
|
-
// 并被 setup 恢复预算与任务总时限夹住。回退重试不重置它,也不新增第二个魔法超时值。
|
|
303
|
-
const projectTriggerDeadline = () => Math.min(Date.now() + gui.projectTriggerTimeoutMs, started + ctx.taskTimeoutMs, started + gui.setupRecoveryTimeoutMs);
|
|
304
338
|
/**
|
|
305
339
|
* 无项目模式的就绪判据:必须确认当前会话真的处于「未绑定项目的 default 工作区」。
|
|
306
340
|
* 「不点击项目按钮」不足以证明——当前 UI 可能继承上一次绑定,所以要求触发器文本
|
|
@@ -489,7 +523,7 @@ export async function runZcodeTask(args) {
|
|
|
489
523
|
// eslint-disable-next-line no-await-in-loop
|
|
490
524
|
await deps.sleep(Math.min(300, Math.max(0, deadline - Date.now())));
|
|
491
525
|
}
|
|
492
|
-
logger.warn(`[zcode] 项目触发器未就绪;state=${last.probe.state};reason=${last.reason ?? "n/a"};attempts=${attempts};elapsed=${Date.now() - attemptStarted}ms;剩余预算=${Math.max(0, deadline - Date.now())}ms;selector=${last.probe.selector || "n/a"};count=${last.probe.count};mounted=${last.probe.mounted}${last.probe.detail ? `;命中=${last.probe.detail}` : ""}`);
|
|
526
|
+
logger.warn(`[zcode] 项目触发器未就绪;state=${last.probe.state};reason=${last.reason ?? "n/a"};attempts=${attempts};elapsed=${Date.now() - attemptStarted}ms;剩余预算=${Math.max(0, deadline - Date.now())}ms;selector=${last.probe.selector || "n/a"};count=${last.probe.count};mounted=${last.probe.mounted};页面隐藏=${last.probe.pageHidden === true}${last.probe.detail ? `;命中=${last.probe.detail}` : ""}`);
|
|
493
527
|
return last;
|
|
494
528
|
};
|
|
495
529
|
let triggerOutcome = await attemptProjectMenu();
|