tianshu-mcp 0.6.0 → 0.6.2
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 +46 -0
- package/CHANGELOG.md +46 -0
- package/README.en.md +14 -1
- package/README.md +14 -1
- package/dist/agents/builtin.js +10 -9
- package/dist/agents/codex/cdp.js +18 -2
- package/dist/agents/codex/run.js +14 -4
- package/dist/agents/codex/selectors.js +28 -16
- package/dist/agents/gui-diagnostics.js +81 -0
- package/dist/agents/qoder/cdp.js +41 -5
- package/dist/agents/qoder/selectors.js +217 -28
- package/dist/agents/qoder/workspace.js +13 -2
- package/dist/agents/registry.js +20 -0
- package/dist/agents/traework/cdp/client.js +13 -0
- package/dist/agents/traework/cdp/selectors.js +24 -24
- package/dist/agents/traework/discovery.js +194 -0
- package/dist/agents/traework/launcher.js +57 -1
- package/dist/agents/traework/run.js +14 -3
- package/dist/config/schema.js +5 -1
- package/dist/config/store.js +12 -2
- package/dist/mcp/handlers.js +12 -4
- package/dist/mcp/tools.js +6 -2
- package/dist/server.js +3 -0
- package/dist/util/path.js +45 -9
- package/dist/version.generated.js +1 -1
- package/docs/qoder-cdp.en.md +4 -0
- package/docs/qoder-cdp.md +4 -0
- package/package.json +1 -1
- package/scripts/probe-codex.mjs +52 -2
- package/scripts/probe-traework.mjs +1 -1
- package/skills/tianshu-mcp/SKILL.md +2 -1
- package/skills/tianshu-mcp/usage-examples.md +2 -2
package/CHANGELOG.en.md
CHANGED
|
@@ -8,6 +8,52 @@ Chinese version: [CHANGELOG.md](CHANGELOG.md)
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## [0.6.2] - 2026-09-23
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **Codex project-picker trigger copy drifts across versions** ([issue #23](https://github.com/lanlan0811/tianshu-mcp/issues/23), case 1): the trigger's `aria-label` is "选择项目:<name>" on some versions and "切换项目:<name>" on others (26.915, measured locally). `projectPickerTrigger` now covers both copies via primary/fallback/pattern (including the English `Select/Switch project`), and `boundProjectName` reads both back. **A full 20-key hardware audit was run** (`node scripts/probe-codex.mjs --launch audit`): on local 26.915 there is no drift beyond this trigger, and 9 verified keys were bumped to `verifiedVersion: 26.915.x`.
|
|
16
|
+
- **Qoder workspace binding failed on 0.3.4** (issue #23, case 2): a fresh hardware probe **corrected the issue's conclusion** — the 0.3.4 workspace menu **does render**; the real cause is that the page has **two** `[data-workspace-picker-trigger]` elements, and the old single-match `click()` failed as ambiguous. The workspace trigger's primary selector is now the unique `button[aria-label^="切换或清空当前工作区"]` (with `[data-workspace-picker-trigger]` demoted to a fallback); the "menu is open" check is relaxed to "search box **or** overlay". The production `bindWorkspace` now passes on real Qoder 0.3.4.
|
|
17
|
+
- **TraeWork install discovery failed** (issue #23, case 3): added `src/agents/traework/discovery.ts` (fixed-drive enumeration + registry `InstallLocation` + relative paths) with a dedicated `traework-gui` branch in `registry.ts`. Fixed the built-in profile — removed the wrong `{APPDATA}/TRAE SOLO CN` (measured to be the **user-data dir**: Cache/Crashpad/nested tool exes, not the install location) in favor of `{LOCALAPPDATA}/Programs/TRAE SOLO CN` etc., and added `preferredDrives`/`relativePaths`. **On Windows the executable name is narrowed to `TRAE SOLO CN.exe`** (the old list's `Trae CN` mismatched the unrelated TraeCode CN product). `waitReady` timeouts now emit on-site diagnostics (child exit code, port-listener enumeration, detection of existing instances without the debug port) — **diagnosis only: no launch-strategy change, no termination of existing instances**.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **Unified selector diagnostics across the three GUI agents** (`src/agents/gui-diagnostics.ts`): on selector-resolution failure the nearest visible page candidates (aria-labels / short texts) are appended to the error and log, so drift can be located in one step without opening CDP by hand. Wired into codex / qoder / traework.
|
|
22
|
+
- **Codex full-key audit mode** `scripts/probe-codex.mjs --launch audit`: prints the primary hit count and matched labels for all 20 selector keys as a "script + evidence table" gate.
|
|
23
|
+
- **Qoder layered selector structure**: `src/agents/qoder/selectors.ts` upgraded from flat strings to `primary/fallbacks/texts/ariaLabels/ariaPatterns/verifiedVersion` (27 keys); `QoderCdpClient.selector()` keeps its string semantics and adds `candidates()/existsKey()/clickKey()`.
|
|
24
|
+
- **TraeWork selector version field unified**: `verified: boolean` → `verifiedVersion: string` (matching codex/kimicode).
|
|
25
|
+
|
|
26
|
+
### Docs
|
|
27
|
+
|
|
28
|
+
- New [issue #23 verification record](docs/issue-23-selector-drift-record.md) (20-key audit table, Qoder 0.3.4 re-probe, TraeWork discovery hardware results); release notes [v0.6.2](docs/release-v0.6.2.en.md).
|
|
29
|
+
|
|
30
|
+
## [0.6.1] - 2026-09-23
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **Dangerous-directory decision collapsed into a platform-injectable pure function** `isDangerousProjectDir(norm, platform)` (`src/util/path.ts`): exact root / drive root / system-directory subtree, so all three platform shapes are verifiable on any OS.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- **`verify_task` capability moves from `read` to `execute`** ([issue #17](https://github.com/lanlan0811/tianshu-mcp/issues/17) item 2): it runs the project's configured commands and may produce build artifacts, so it never was read-only. **Host note**: its MCP `readOnlyHint` changes from `true` to **`false`**; `requireApproval` stays `false` (still approval-free, the R11 conclusion is unchanged) — **policy layers should key off `_meta.requireApproval`, not `readOnlyHint`**. The policy examples in `docs/tianshu-integration` (both languages) are updated.
|
|
39
|
+
- **The `"network"` value, used by no tool at all, is removed from the `ToolDef.capability` union**, collapsed to three families (`read` / `write` / `execute`) documented on the type and in the `tools.ts` header.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **System directories are now denied as subtrees** (issue #17 item 4): `/etc`, `/usr`, `/bin`, `/sbin`, `/private/etc` plus `c:/windows`, `c:/program files`, `c:/program files (x86)` moved from exact-equality to **boundary-aware subtree denial**, closing leaks such as `c:/windows/system32` and `/etc/anything`. Boundary-awareness keeps `c:/windows.old`, `/etcetera` and `/usrlocal` from being falsely matched; `/var`, `/tmp`, `/opt`, the home directory and friends stay exact-match (on macOS `os.tmpdir()` *is* `/var/folders/...`, so a subtree rule would sever the test base and legitimate workspaces).
|
|
44
|
+
- **Project registration failures are no longer silent** (issue #17 item 3): `run_task` used to discard the `registerProject` return value (`void registered;`) and then re-read the same record via `projectByPath`; it now consumes the return value, drops the redundant read, and on failure logs `WARN` and returns a structured `isError` **without dispatching** (eliminating the "task created but project unregistered" half-state).
|
|
45
|
+
- **Count-vs-reality wrap-up** (issue #17 item 1): the leftover "9 tools" header comment in `test/protocol/protocol.test.ts` is corrected and a `TOOL_DEFS` count assertion added; the lagging "6 scenarios" references in the `ci.yml` comment, `HANDOFF` (two places) and `CONTRIBUTING` (both languages) are synced to **8** (they lagged behind the two skill scenarios added in issue #16).
|
|
46
|
+
|
|
47
|
+
### Documentation
|
|
48
|
+
|
|
49
|
+
- **The `cmd` string-form pitfalls made explicit** (issue #17 item 5, zero behaviour change): `docs/acceptance-config.md` / `.en.md` gain a measured table (no escaping, an unclosed quote does not error, an empty quote yields an empty argument), and the `schema.ts` / `store.ts` comments, `SKILL.md` and `usage-examples.md` carry an "always prefer the array form" note.
|
|
50
|
+
- Bilingual: README (`verify_task` capability row + M31 milestone), ARCHITECTURE (three-family tool table + `readOnlyHint` derivation rule + two new §15 gaps), SECURITY (§3 dangerous-directory matching semantics + §5 wording corrected), `docs/tianshu-integration`, `docs/acceptance-config`; single-language: HANDOFF, `SKILL.md`, `usage-examples.md`; new `docs/issue-17-small-fixes-record.md` (measured evidence).
|
|
51
|
+
|
|
52
|
+
### Tests
|
|
53
|
+
|
|
54
|
+
- Full suite **940 passed / 12 skipped** (Windows 10 x64, Node 24.18.0), a net **+42** over v0.6.0's 898: `project-dir-guard` gains `isDangerousProjectDir` table-driven cases for all three platform shapes (including the three key counter-examples `c:/windows.old`, `/etcetera`, `/private/var/folders/...`) and a win32 subtree integration case; `protocol` gains an 11-tool × capability × four-annotation **truth table** and a count assertion; `zcode-handler` gains registration-failure-does-not-dispatch and return-value-consumed cases (the latter asserts a `projectByPath` call count of 0); `core` gains `splitCmd` boundary-semantics locks.
|
|
55
|
+
- The strict stdio gate passes **8/8** on both the dist and src entry points; `pack:check` passes (232 files).
|
|
56
|
+
|
|
11
57
|
## [0.6.0] - 2026-09-23
|
|
12
58
|
|
|
13
59
|
### Added
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,52 @@
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.6.2] - 2026-09-23
|
|
11
|
+
|
|
12
|
+
### 修复
|
|
13
|
+
|
|
14
|
+
- **Codex 项目选择触发器文案跨版本漂移**([issue #23](https://github.com/lanlan0811/tianshu-mcp/issues/23) 例 1):触发器 `aria-label` 在部分版本为「选择项目:<名>」、部分(本机实测 26.915)仍为「切换项目:<名>」。`projectPickerTrigger` 主/回退/模板并列覆盖两文案(含英文 `Select/Switch project`),`boundProjectName` 的项目名回读同步兼容。**并完成全量 20 键真机审计**(`node scripts/probe-codex.mjs --launch audit`):本机 26.915 除该触发器外无其他漂移,9 个已实测键的 `verifiedVersion` 提升为 `26.915.x`。
|
|
15
|
+
- **Qoder 工作区绑定在 0.3.4 失败**(issue #23 例 2):真机重探**更正了 issue 的结论**——0.3.4 工作区菜单**并非不渲染**,真因是页面存在**两个** `[data-workspace-picker-trigger]`,旧 `click()` 要求唯一命中而判歧义失败。工作区触发器主选择器改用唯一的 `button[aria-label^="切换或清空当前工作区"]`(`[data-workspace-picker-trigger]` 降为回退);「菜单已打开」判定放宽为「搜索框 **或** 浮层」。生产 `bindWorkspace` 已在真实 Qoder 0.3.4 上跑通。
|
|
16
|
+
- **TraeWork 安装发现失败**(issue #23 例 3):新增 `src/agents/traework/discovery.ts`(固定盘枚举 + 注册表 `InstallLocation` + 相对路径),`registry.ts` 增 `traework-gui` 专用分支。修正内置 profile——删除错误的 `{APPDATA}/TRAE SOLO CN`(实测该目录是**用户数据目录**:Cache/Crashpad/嵌套工具 exe,非安装位置),改为 `{LOCALAPPDATA}/Programs/TRAE SOLO CN` 等,并补 `preferredDrives`/`relativePaths`。**Windows 文件名收窄为只认 `TRAE SOLO CN.exe`**(旧清单含 `Trae CN`,会误匹配另一产品 TraeCode CN)。`waitReady` 超时新增现场诊断(子进程退出码、端口监听者枚举、既有未带调试端口实例检测),**只诊断、不改启动策略、不终止既有实例**。
|
|
17
|
+
|
|
18
|
+
### 新增
|
|
19
|
+
|
|
20
|
+
- **三 GUI agent 统一的选择器诊断**(`src/agents/gui-diagnostics.ts`):选择器解析失败时把页面可见候选标签(最接近的 aria-label / 短文本)写进错误与日志,使用者一步定位漂移,无需人工开 CDP。已接入 codex / qoder / traework。
|
|
21
|
+
- **Codex 全键审计模式** `scripts/probe-codex.mjs --launch audit`:对全部 20 个语义键输出 primary 命中数与命中标签,作为「脚本 + 证据表」门禁。
|
|
22
|
+
- **Qoder 选择器分层结构**:`src/agents/qoder/selectors.ts` 由扁平字符串升级为 `primary/fallbacks/texts/ariaLabels/ariaPatterns/verifiedVersion`(27 键);`QoderCdpClient.selector()` 保留字符串语义,另增 `candidates()/existsKey()/clickKey()`。
|
|
23
|
+
- **TraeWork 选择器版本字段统一**:`verified: boolean` → `verifiedVersion: string`(与 codex/kimicode 一致)。
|
|
24
|
+
|
|
25
|
+
### 文档
|
|
26
|
+
|
|
27
|
+
- 新增 [issue #23 验证记录](docs/issue-23-selector-drift-record.md)(含 20 键审计表、Qoder 0.3.4 重探、TraeWork discovery 真机结果);发布说明 [v0.6.2](docs/release-v0.6.2.md)。
|
|
28
|
+
|
|
29
|
+
## [0.6.1] - 2026-09-23
|
|
30
|
+
|
|
31
|
+
### 新增
|
|
32
|
+
|
|
33
|
+
- **危险目录判定收敛为可注入平台的纯函数** `isDangerousProjectDir(norm, platform)`(`src/util/path.ts`):精确根 / 盘符根 / 系统目录子树三选一判定,任意平台都能验证三平台形态。
|
|
34
|
+
|
|
35
|
+
### 变更
|
|
36
|
+
|
|
37
|
+
- **`verify_task` 的能力由 `read` 改为 `execute`**([issue #17](https://github.com/lanlan0811/tianshu-mcp/issues/17) 问题 2):它会执行项目配置命令、可产生构建产物,本就不是只读。**宿主需知**:其 MCP `readOnlyHint` 由 `true` 变为 **`false`**;`requireApproval` 维持 `false`(仍免审批,R11 结论不变)——**策略层请以 `_meta.requireApproval` 而非 `readOnlyHint` 判断是否需授权**,`docs/tianshu-integration` 双语的 policy 示例已同步。
|
|
38
|
+
- **`ToolDef.capability` 联合类型删除始终无人使用的 `"network"`**,收敛为三族语义(`read` / `write` / `execute`),并写进类型定义与 `tools.ts` 头注释。
|
|
39
|
+
|
|
40
|
+
### 修复
|
|
41
|
+
|
|
42
|
+
- **系统目录改为子树拒绝**(issue #17 问题 4):`/etc`、`/usr`、`/bin`、`/sbin`、`/private/etc` 与 `c:/windows`、`c:/program files`、`c:/program files (x86)` 由「仅精确相等」升级为**边界感知子树拒绝**,修补了 `c:/windows/system32`、`/etc/anything` 这类漏挡。边界感知使 `c:/windows.old`、`/etcetera`、`/usrlocal` 不被误伤;`/var`、`/tmp`、`/opt`、用户主目录等维持精确匹配(macOS 的 `os.tmpdir()` 就是 `/var/folders/...`,子树拒绝会切断测试基座与合法工作区)。
|
|
43
|
+
- **项目登记失败不再静默**(issue #17 问题 3):`run_task` 原先丢弃 `registerProject` 的返回值(`void registered;`)又用 `projectByPath` 二次读取同一条记录;现直接消费返回值、删除冗余读取,登记失败时记 `WARN` 并返回结构化 `isError` 且**不派单**(杜绝「任务已建、项目未登记」的半状态)。
|
|
44
|
+
- **计数与实际不符收尾**(issue #17 问题 1):改正 `test/protocol/protocol.test.ts` 头注释残留的「9 个工具」,并新增 `TOOL_DEFS` 数量硬断言;`ci.yml` 注释、`HANDOFF`(两处)与 `CONTRIBUTING` 双语滞后的「6 场景」同步为 **8**(issue #16 新增两技能场景后未同步)。
|
|
45
|
+
|
|
46
|
+
### 文档
|
|
47
|
+
|
|
48
|
+
- **`cmd` 字符串形态的坑显式化**(issue #17 问题 5,零行为变更):`docs/acceptance-config.md` / `.en.md` 新增实测表(无转义、引号不闭合不报错、空引号产出空参数),`schema.ts` / `store.ts` 注释、`SKILL.md`、`usage-examples.md` 补「推荐一律用数组形态」提示。
|
|
49
|
+
- 双语:README(`verify_task` 能力列 + M31 里程碑)、ARCHITECTURE(工具表三族语义 + `readOnlyHint` 推导规则 + §15 两条新缺口)、SECURITY(§3 危险目录匹配语义 + §5 措辞改准)、`docs/tianshu-integration`、`docs/acceptance-config`;单语:HANDOFF、`SKILL.md`、`usage-examples.md`;新增 `docs/issue-17-small-fixes-record.md`(实测证据)。
|
|
50
|
+
|
|
51
|
+
### 测试
|
|
52
|
+
|
|
53
|
+
- 全量 **940 passed / 12 skipped**(Windows 10 x64,Node 24.18.0),较 v0.6.0 的 898 净增 **42** 项:`project-dir-guard` 新增 `isDangerousProjectDir` 三平台表驱动用例(含 `c:/windows.old`、`/etcetera`、`/private/var/folders/...` 三个关键反例)与 win32 子树集成用例;`protocol` 新增 11 工具 × capability × 四注解**真值表**与数量硬断言;`zcode-handler` 新增登记失败不派单、返回值被消费(断言 `projectByPath` 调用计数为 0);`core` 新增 `splitCmd` 边界语义锁定。
|
|
54
|
+
- 严格 stdio 门禁 dist 与 src 两条入口各 **8/8**;`pack:check` 通过(232 文件)。
|
|
55
|
+
|
|
10
56
|
## [0.6.0] - 2026-09-23
|
|
11
57
|
|
|
12
58
|
### 新增
|
package/README.en.md
CHANGED
|
@@ -192,7 +192,7 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
|
|
|
192
192
|
| `list_tasks` | read | Filtered history of tasks |
|
|
193
193
|
| `get_task_report` | read | Full text of a verification round's report (`report.md`) |
|
|
194
194
|
| `cancel_task` | write + approval | Cancel a running task: CLI agents kill the process tree; GUI agents click the in-app stop control over CDP and bounded-wait (`gui.cancelWaitMs`, default 15s) for the GUI to go idle, stating so explicitly in the final message when the stop is unconfirmed. For a terminal GUI task this call doubles as the manual acknowledgement entry point — after verifying the window holds no residual run, it clears the `guiStopUnconfirmed` marker |
|
|
195
|
-
| `verify_task` |
|
|
195
|
+
| `verify_task` | execute (no source changes, no approval) | Run one verification pass on a task/project path. Capability is `execute`: it runs the project's configured commands and may produce build artifacts, so MCP `readOnlyHint` is `false` — but it **does not modify sources and still needs no approval**. Optional `idempotencyKey`: a retry with the same key never re-runs (a running pass answers "in progress", a finished one returns the existing report) |
|
|
196
196
|
| `rework_task` | write + approval | Manual rework (feed the failure report back to the same agent) |
|
|
197
197
|
| `get_profiles` | read | Inspect agent adapters and executable discovery results |
|
|
198
198
|
| `prepare_visual_baseline` | write + approval | Capture or import reference images into a reviewable candidate with a digest |
|
|
@@ -450,6 +450,19 @@ On stderr, `INFO` means skipped/installed; `WARN` means a stale copy was upgrade
|
|
|
450
450
|
- **Atomic install and log levels**: "tmp dir → backup → swap in", with rollback on failure so no half-copied tree remains; skip = INFO, overwrite/keep/unknown-source/failure = WARN; after an overwrite, historical `.bak-<timestamp>` directories are pruned to `backupKeep`
|
|
451
451
|
- **Gates**: the strict stdio check gains `skill-locally-modified` / `skill-approve-update` scenarios (6→8); Windows 10 real-machine verification R1–R7 recorded in the [issue-16 record](docs/issue-16-skill-install-hardening-record.md)
|
|
452
452
|
- See the [v0.6.0 release notes](<docs/release-v0.6.0.en.md>)
|
|
453
|
+
- **M31 — Small-fixes sweep (comment & count alignment / capability tri-family / registration failure stops dispatch / system-directory subtree denial / splitCmd docs) + v0.6.1** (2026-09-23) — 42 new cases (issue #17)
|
|
454
|
+
- **System directories now denied by subtree**: `/etc` `/usr` `/bin` `/sbin` `/private/etc` plus `c:/windows` and `c:/program files*` moved from exact-equality to **boundary-aware subtree denial** (`c:/windows.old` and `/etcetera` are not falsely matched). `/var`, `/tmp` and the home directory stay exact-match (`/var/folders/...` on macOS *is* `os.tmpdir()`, so a blanket subtree rule would sever the test base and many legitimate workspaces). The decision is a platform-injectable pure function `isDangerousProjectDir`, so all three platform shapes are verifiable on any OS
|
|
455
|
+
- **`capability` collapsed to three families**: the never-used `"network"` value is removed; `verify_task` moves from `read` to **`execute`** (it runs project commands and may produce build artifacts, so it never was read-only) — so its `readOnlyHint` flips from `true` to `false`, **but it still needs no approval** (`requireApproval` stays false; the R11 conclusion is unchanged). A new **truth-table test** covers all 11 tools × capability × four annotations
|
|
456
|
+
- **Project registration failure now stops dispatch**: `run_task` no longer discards the `registerProject` return value (previously `void registered;` was followed by a redundant `projectByPath` re-read); a registration failure logs WARN and returns a structured `isError`, eliminating the "task created but project unregistered" half-state
|
|
457
|
+
- **`cmd` string form documented**: the bilingual docs and the schema/source comments state "prefer the array form; the string form **supports no escaping**, an unclosed quote does not error, and a typo silently splits into multiple argv". Five boundary cases pin the existing tokenizer semantics (no behaviour change)
|
|
458
|
+
- **Counts and scenario numbers aligned**: the "9 tools" header comment in `protocol.test.ts` is corrected to 11 and a `TOOL_DEFS` count assertion is added; the "6 scenarios" references in `ci.yml` / `HANDOFF` / `CONTRIBUTING` (both languages) are synced to 8 (they lagged behind the two skill scenarios added in issue #16)
|
|
459
|
+
- See the [v0.6.1 release notes](<docs/release-v0.6.1.en.md>); verification record in the [issue-17 record](docs/issue-17-small-fixes-record.md)
|
|
460
|
+
- **M32 — GUI selector version-drift fixes (codex / qoder / traework) + v0.6.2** (2026-09-23) — 26 new cases (issue #23)
|
|
461
|
+
- **Codex**: the project-picker trigger copy drifts across versions (26.915 locally measured "切换项目", 26.917 "选择项目"); primary+fallback now cover both, and `boundProjectName` reads both back. A new `probe-codex.mjs audit` prints a hit table for all 20 keys — **on local 26.915 there is no drift other than this trigger**
|
|
462
|
+
- **Qoder**: selectors upgraded from flat strings to a layered structure matching Codex (27 keys with `verifiedVersion`). **The hardware re-probe corrected the issue's conclusion**: on 0.3.4 the workspace menu **does render** — the real cause was **two** `[data-workspace-picker-trigger]` elements breaking the single-match click. Switching to the unique `button[aria-label^="切换或清空当前工作区"]` makes the production `bindWorkspace` pass on real 0.3.4
|
|
463
|
+
- **TraeWork**: added `discovery.ts` (fixed-drive enumeration + registry + relative paths) and fixed the built-in dir (`{APPDATA}/TRAE SOLO CN` is actually the **user-data dir**, not the install location). On Windows the executable name is narrowed to `TRAE SOLO CN.exe` (the old list's `Trae CN` mismatched the unrelated TraeCode CN product); a port-not-ready path now prints diagnostics (exit code / port listeners / existing instances) — **diagnosis only, no launch-strategy change**
|
|
464
|
+
- **Unified diagnostics**: new `src/agents/gui-diagnostics.ts` appends the nearest visible page candidates to selector-resolution failures across all three GUI agents
|
|
465
|
+
- See the [v0.6.2 release notes](<docs/release-v0.6.2.en.md>); verification record in the [issue-23 record](docs/issue-23-selector-drift-record.md)
|
|
453
466
|
|
|
454
467
|
## Agent support status
|
|
455
468
|
|
package/README.md
CHANGED
|
@@ -188,7 +188,7 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
|
|
|
188
188
|
| `list_tasks` | read | 历史任务过滤列表 |
|
|
189
189
|
| `get_task_report` | read | 某轮验收报告全文(`report.md`) |
|
|
190
190
|
| `cancel_task` | write + 审批 | 取消运行中任务:CLI agent kill 进程树;GUI agent 经 CDP 点击停止并在 `gui.cancelWaitMs`(默认 15s)内有界等待 GUI 空闲,未确认停止时终态明示。对已终态的 GUI 任务,本调用兼任人工确认入口——核实窗口无残留运行后调用可清除 `guiStopUnconfirmed` 待确认标记 |
|
|
191
|
-
| `verify_task` |
|
|
191
|
+
| `verify_task` | execute(不改源码,免审批) | 对任务/项目路径做一次验收。能力归 `execute`:会跑项目配置命令、可能产生构建产物,故 MCP `readOnlyHint` 为 `false`——但**不改源码、仍免审批**。可选 `idempotencyKey`:同键重试不重跑(执行中返回进行中提示,已完成返回既有报告) |
|
|
192
192
|
| `rework_task` | write + 审批 | 手动返修(把失败报告喂回同一 agent) |
|
|
193
193
|
| `get_profiles` | read | 查看 agent 适配与可执行探测结果 |
|
|
194
194
|
| `prepare_visual_baseline` | write + 审批 | 截图或导入参考图,生成待审阅候选和摘要 |
|
|
@@ -451,6 +451,19 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
|
|
|
451
451
|
- **安装原子化与日志分级**:改为「tmp 目录 → 备份 → 换入」,失败回滚不留半成品;跳过=INFO、覆盖/保留/来源不明/失败=WARN;覆盖后按 `backupKeep` 收敛历史 `.bak-<时间戳>`
|
|
452
452
|
- **门禁**:严格 stdio 检查新增 `skill-locally-modified` / `skill-approve-update` 两场景(6→8);Windows 10 真机复验 R1–R7 留档 [issue-16 记录](docs/issue-16-skill-install-hardening-record.md)
|
|
453
453
|
- 详见 [v0.6.0 发布说明](<docs/release-v0.6.0.md>)
|
|
454
|
+
- **M31 — 五项小项扫尾(注释与计数对齐 / capability 三族 / 登记失败不派单 / 系统目录子树拒绝 / splitCmd 文档)+ v0.6.1**(2026-09-23)— 新增 42 项用例(issue #17)
|
|
455
|
+
- **系统目录改为子树拒绝**:`/etc` `/usr` `/bin` `/sbin` `/private/etc` 与 `c:/windows`、`c:/program files*` 由「仅精确相等」升级为**边界感知子树拒绝**(`c:/windows.old`、`/etcetera` 不误伤);`/var`、`/tmp`、家目录维持精确匹配(macOS 的 `/var/folders/...` 正是 `os.tmpdir()`,一刀切会切断测试基座与大量合法工作区)。判定抽为可注入平台的纯函数 `isDangerousProjectDir`,任意平台都能验证三平台形态
|
|
456
|
+
- **`capability` 收敛为三族语义**:删除始终无人使用的 `"network"`;`verify_task` 由 `read` 改为 **`execute`**(会跑项目命令、可产生构建产物,本就不是只读)——连带 `readOnlyHint` 由 `true` 变 `false`,**但仍免审批**(`requireApproval` 维持 false,R11 结论不变);新增 11 工具 × capability × 四注解的**真值表测试**锁死全部映射
|
|
457
|
+
- **项目登记失败即终止派单**:`run_task` 不再丢弃 `registerProject` 的返回值(原先 `void registered;` 后还冗余地 `projectByPath` 二次读取);登记失败时记 WARN 并返回结构化 `isError`,杜绝「任务已建、项目未登记」的半状态
|
|
458
|
+
- **`cmd` 字符串形态文档化**:双语文档与 schema/代码注释明示「推荐数组形态;字符串形态**不支持转义**、引号不闭合不报错、写错会静默拆成多个 argv」;补 5 条边界用例锁定既有分词语义(零行为变更)
|
|
459
|
+
- **计数与场景数对齐**:`protocol.test.ts` 头注释「9 个工具」更正为 11 并新增 `TOOL_DEFS` 数量硬断言;`ci.yml` / `HANDOFF` / `CONTRIBUTING` 双语的「6 场景」同步为 8(issue #16 新增两技能场景后未同步)
|
|
460
|
+
- 详见 [v0.6.1 发布说明](<docs/release-v0.6.1.md>);验证记录见 [issue-17 记录](docs/issue-17-small-fixes-record.md)
|
|
461
|
+
- **M32 — GUI 选择器版本漂移修复(codex / qoder / traework 三例)+ v0.6.2**(2026-09-23)— 新增 26 项用例(issue #23)
|
|
462
|
+
- **Codex**:项目选择触发器文案跨版本漂移(26.915 本机实测为「切换项目」,26.917 为「选择项目」),主/回退并列覆盖两文案;`boundProjectName` 回读同步兼容;新增 `probe-codex.mjs audit` 对全部 20 键输出命中表,**本机 26.915 除该触发器外无其他漂移**
|
|
463
|
+
- **Qoder**:选择器由扁平字符串升级为与 Codex 同构的分层结构(27 键,含 `verifiedVersion`);**真机重探更正 issue 结论**——0.3.4 工作区菜单**并非不渲染**,真因是页面有**两个** `[data-workspace-picker-trigger]` 致唯一点击判歧义失败;改用唯一的 `button[aria-label^="切换或清空当前工作区"]` 后,生产 `bindWorkspace` 已在真实 0.3.4 跑通
|
|
464
|
+
- **TraeWork**:新增 `discovery.ts`(固定盘枚举 + 注册表 + 相对路径),修正内置目录(`{APPDATA}/TRAE SOLO CN` 实为**用户数据目录**,非安装位置);Windows 文件名收窄为只认 `TRAE SOLO CN.exe`(旧清单含 `Trae CN` 会误匹配另一产品 TraeCode CN);端口未就绪时输出诊断(退出码 / 端口监听者 / 既有实例),**只诊断不改启动策略**
|
|
465
|
+
- **统一诊断**:新增 `src/agents/gui-diagnostics.ts`,三 GUI agent 在选择器解析失败时把「页面可见候选」写进错误,使用者一步定位漂移
|
|
466
|
+
- 详见 [v0.6.2 发布说明](<docs/release-v0.6.2.md>);验证记录见 [issue-23 记录](docs/issue-23-selector-drift-record.md)
|
|
454
467
|
|
|
455
468
|
## Agent 适配现状
|
|
456
469
|
|
package/dist/agents/builtin.js
CHANGED
|
@@ -135,20 +135,21 @@ export const BUILTIN_PROFILES = {
|
|
|
135
135
|
killTree: "taskkill",
|
|
136
136
|
authNote: "复用 TraeWork 桌面端登录态;窗口需保持可见(发送依赖模拟输入)",
|
|
137
137
|
executableDiscovery: {
|
|
138
|
+
// issue #23 C3:原先写成 {APPDATA}/TRAE SOLO CN(Roaming),实际安装在
|
|
139
|
+
// {LOCALAPPDATA}/Programs(Local)。补 preferredDrives + relativePaths + 注册表探测
|
|
140
|
+
// (由 traework/discovery.ts 消费),Windows 只认 TRAE SOLO CN.exe。
|
|
138
141
|
dirs: [
|
|
139
|
-
"
|
|
140
|
-
"{
|
|
141
|
-
"{PROGRAMFILES
|
|
142
|
-
"{
|
|
143
|
-
"{LOCALAPPDATA}/TRAE WORK CN",
|
|
144
|
-
"{APPDATA}/TRAE SOLO CN",
|
|
142
|
+
"{LOCALAPPDATA}/Programs/TRAE SOLO CN",
|
|
143
|
+
"{LOCALAPPDATA}/TRAE SOLO CN",
|
|
144
|
+
"{PROGRAMFILES}/TRAE SOLO CN",
|
|
145
|
+
"{PROGRAMFILES(X86)}/TRAE SOLO CN",
|
|
145
146
|
"/Applications/TraeWork.app/Contents/MacOS",
|
|
146
147
|
"/Applications/Trae CN.app/Contents/MacOS",
|
|
147
148
|
],
|
|
148
|
-
fileNames: ["TRAE SOLO CN
|
|
149
|
+
fileNames: process.platform === "darwin" ? ["TRAE SOLO CN", "TraeWork", "Trae CN"] : ["TRAE SOLO CN.exe"],
|
|
149
150
|
fallbackCommand: undefined,
|
|
150
|
-
preferredDrives: [],
|
|
151
|
-
relativePaths: [],
|
|
151
|
+
preferredDrives: ["D:"],
|
|
152
|
+
relativePaths: ["TRAE Work CN/TRAE SOLO CN.exe"],
|
|
152
153
|
installRelativeExe: [],
|
|
153
154
|
scanRoots: [],
|
|
154
155
|
},
|
package/dist/agents/codex/cdp.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { TraeworkCdpClient, CdpDisconnectedError, CdpUnavailableError } from "../traework/cdp/client.js";
|
|
8
8
|
import { resolveFnSource, specArgs } from "./selectors.js";
|
|
9
|
+
import { visibleLabelsExpr, normalizeLabels } from "../gui-diagnostics.js";
|
|
9
10
|
export { CdpDisconnectedError, CdpUnavailableError };
|
|
10
11
|
export class CodexCdpClient {
|
|
11
12
|
selectors;
|
|
@@ -81,6 +82,20 @@ export class CodexCdpClient {
|
|
|
81
82
|
async exists(key) {
|
|
82
83
|
return this.evaluate(this.withResolve(`${this.visibleFilter}const els=__codexResolve(${specArgs(key, this.selectors)});return els.some(vis);`));
|
|
83
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* 收集页面可见候选标签(issue #23 诊断机制):选择器解析失败时,
|
|
87
|
+
* 把「页面实际有什么」写进错误信息,使用者一步定位漂移(无需人工开 CDP)。
|
|
88
|
+
* 永不抛错——诊断失败返回空数组,不影响主流程。
|
|
89
|
+
*/
|
|
90
|
+
async visibleLabels(scopeCss) {
|
|
91
|
+
try {
|
|
92
|
+
const raw = await this.evaluate(visibleLabelsExpr({ scope: scopeCss }));
|
|
93
|
+
return normalizeLabels(raw);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
84
99
|
/** 第一个可见元素文本 */
|
|
85
100
|
async text(key) {
|
|
86
101
|
return ((await this.evaluate(this.withResolve(`${this.visibleFilter}const els=__codexResolve(${specArgs(key, this.selectors)});for(const e of els){if(vis(e))return ((e.value!==undefined?e.value:'')||e.textContent||'').trim()}return '';`))) || "");
|
|
@@ -293,7 +308,8 @@ export class CodexCdpClient {
|
|
|
293
308
|
}
|
|
294
309
|
/**
|
|
295
310
|
* 读取当前会话已绑定的项目名。
|
|
296
|
-
* 权威信号:输入框内的项目 chip 带 `aria-label="
|
|
311
|
+
* 权威信号:输入框内的项目 chip 带 `aria-label="选择项目:<名>"`(26.917 起;
|
|
312
|
+
* 26.903 及更早为「切换项目:<名>」;未绑定时为「不在项目中工作」)。
|
|
297
313
|
* 真机教训:早期实现把所有非 chip 按钮文本拼起来,会返回整页文案并意外“通过”匹配。
|
|
298
314
|
*/
|
|
299
315
|
async boundProjectName() {
|
|
@@ -303,7 +319,7 @@ export class CodexCdpClient {
|
|
|
303
319
|
for(const e of document.querySelectorAll('[aria-label]')){
|
|
304
320
|
if(!vis(e))continue;
|
|
305
321
|
const aria=norm(e.getAttribute('aria-label'));
|
|
306
|
-
const m=/^(
|
|
322
|
+
const m=/^(?:选择项目|切换项目|Select project|Switch project)[::]\\s*(.+)$/i.exec(aria);
|
|
307
323
|
if(m&&m[1])return m[1].trim();
|
|
308
324
|
}
|
|
309
325
|
return '';
|
package/dist/agents/codex/run.js
CHANGED
|
@@ -26,6 +26,7 @@ import { focusCodexApp } from "./launcher.js";
|
|
|
26
26
|
import { ensureProjectRegistered } from "./registry.js";
|
|
27
27
|
import { buildInitialPrompt } from "./input.js";
|
|
28
28
|
import { validateTaskReferences } from "../zcode/references.js";
|
|
29
|
+
import { withDiagnostics } from "../gui-diagnostics.js";
|
|
29
30
|
const DEFAULT_DEPS = {
|
|
30
31
|
discover: discoverCodex,
|
|
31
32
|
ensureInstance: ensureCodexInstance,
|
|
@@ -496,15 +497,24 @@ async function stopGuiTurn(cdp, gui, deps, logger) {
|
|
|
496
497
|
*/
|
|
497
498
|
async function createProject(cdp, projectPath, aumid, gui, deps, logger) {
|
|
498
499
|
// 新建会话后输入框会重渲染,触发器可能短暂缺席 —— 先等它出现再点。
|
|
499
|
-
if (!(await waitFor(cdp, "projectPickerTrigger", deps, 12_000)))
|
|
500
|
+
if (!(await waitFor(cdp, "projectPickerTrigger", deps, 12_000))) {
|
|
501
|
+
// issue #23 诊断机制:解析失败时把页面可见候选一并给出,使用者一步定位文案漂移。
|
|
502
|
+
const labels = await cdp.visibleLabels();
|
|
500
503
|
return {
|
|
501
504
|
ok: false,
|
|
502
|
-
error: "新建会话后未出现项目选择触发器(可用 gui.selectors.projectPickerTrigger 热修复)",
|
|
505
|
+
error: withDiagnostics("新建会话后未出现项目选择触发器(可用 gui.selectors.projectPickerTrigger 热修复)", labels),
|
|
503
506
|
};
|
|
507
|
+
}
|
|
504
508
|
if (!(await cdp.click("projectPickerTrigger")))
|
|
505
|
-
return {
|
|
509
|
+
return {
|
|
510
|
+
ok: false,
|
|
511
|
+
error: withDiagnostics("项目选择触发器点击失败", await cdp.visibleLabels()),
|
|
512
|
+
};
|
|
506
513
|
if (!(await waitFor(cdp, "newProjectMenuItem", deps, 8_000)))
|
|
507
|
-
return {
|
|
514
|
+
return {
|
|
515
|
+
ok: false,
|
|
516
|
+
error: withDiagnostics("项目选择弹层未出现(找不到「新建项目」项)", await cdp.visibleLabels()),
|
|
517
|
+
};
|
|
508
518
|
const menu = await cdp.clickExact("newProjectMenuItem", "新建项目");
|
|
509
519
|
if (!menu.clicked)
|
|
510
520
|
return {
|
|
@@ -13,14 +13,14 @@ export const CODEX_SELECTORS = {
|
|
|
13
13
|
primary: 'div.ProseMirror[contenteditable="true"]',
|
|
14
14
|
fallbacks: ['[contenteditable="true"][role="textbox"]', 'textarea[placeholder]'],
|
|
15
15
|
ariaLabels: ["随心输入", "Type a message", "Message", "输入消息"],
|
|
16
|
-
verifiedVersion: "26.
|
|
16
|
+
verifiedVersion: "26.915.x",
|
|
17
17
|
note: "ProseMirror 富文本输入框;必须走 CDP 输入,不能设 value",
|
|
18
18
|
},
|
|
19
19
|
sendButton: {
|
|
20
20
|
primary: 'button[aria-label="发送"]',
|
|
21
21
|
fallbacks: ['button[aria-label*="Send" i]', 'button[type="submit"]'],
|
|
22
22
|
ariaLabels: ["发送", "Send"],
|
|
23
|
-
verifiedVersion: "26.
|
|
23
|
+
verifiedVersion: "26.915.x",
|
|
24
24
|
note: "条件渲染:输入框为空时不存在,出现即代表可发送",
|
|
25
25
|
},
|
|
26
26
|
stopButton: {
|
|
@@ -49,40 +49,52 @@ export const CODEX_SELECTORS = {
|
|
|
49
49
|
primary: 'button.sidebar-item',
|
|
50
50
|
fallbacks: ['nav button', 'aside button'],
|
|
51
51
|
texts: ["新对话", "New chat"],
|
|
52
|
-
verifiedVersion: "26.
|
|
52
|
+
verifiedVersion: "26.915.x",
|
|
53
53
|
note: "侧边栏新建会话",
|
|
54
54
|
},
|
|
55
55
|
projectSection: {
|
|
56
56
|
primary: 'button[class*="section-toggle"]',
|
|
57
57
|
fallbacks: ['nav button', 'aside button'],
|
|
58
58
|
texts: ["项目", "Projects"],
|
|
59
|
-
verifiedVersion: "26.
|
|
59
|
+
verifiedVersion: "26.915.x",
|
|
60
60
|
note: "侧边栏项目分组开关",
|
|
61
61
|
},
|
|
62
62
|
addProject: {
|
|
63
63
|
primary: 'button[aria-label="添加新项目"]',
|
|
64
64
|
fallbacks: ["button[aria-label*='添加' i]", "button[aria-label*='Add project' i]"],
|
|
65
65
|
ariaLabels: ["添加新项目", "Add new project", "Add project"],
|
|
66
|
-
verifiedVersion: "26.
|
|
66
|
+
verifiedVersion: "26.915.x",
|
|
67
67
|
note: "打开新建项目入口",
|
|
68
68
|
},
|
|
69
69
|
projectPickerTrigger: {
|
|
70
|
-
//
|
|
71
|
-
//
|
|
70
|
+
// issue #23:触发器 aria-label 存在版本间文案漂移。
|
|
71
|
+
// - 26.915(本机实测):仍为「切换项目:<名>」
|
|
72
|
+
// - 26.917(issue #23 报告):已漂移为「选择项目:<名>」
|
|
73
|
+
// 两者均为 aria-haspopup="dialog",故主/回退并列覆盖,兼容 26.903 及更早的「切换项目」。
|
|
74
|
+
// 注意两点(沿用旧结论,不得回归):
|
|
72
75
|
// 1) 它**不在** ComposerLayout 作用域内;
|
|
73
76
|
// 2) 侧边栏有 aria-label="添加新项目"、输入框另有独立按钮 aria-label="不在项目中工作"
|
|
74
77
|
// (那是「离开项目」动作,不是触发器)——都不能混入本键,否则会误点。
|
|
75
|
-
primary: 'button[aria-haspopup="dialog"][aria-label^="
|
|
76
|
-
fallbacks: [
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
primary: 'button[aria-haspopup="dialog"][aria-label^="选择项目"]',
|
|
79
|
+
fallbacks: [
|
|
80
|
+
'button[aria-haspopup="dialog"][aria-label^="切换项目"]',
|
|
81
|
+
"button[aria-haspopup='dialog'][aria-label^='Select project']",
|
|
82
|
+
"button[aria-haspopup='dialog'][aria-label^='Switch project']",
|
|
83
|
+
],
|
|
84
|
+
ariaPatterns: [
|
|
85
|
+
"^选择项目[::]",
|
|
86
|
+
"^切换项目[::]",
|
|
87
|
+
"^Select project[::]",
|
|
88
|
+
"^Switch project[::]",
|
|
89
|
+
],
|
|
90
|
+
verifiedVersion: "26.915–26.917",
|
|
91
|
+
note: "输入框内的项目选择触发器;26.915 实测文案为「切换项目」,issue #23 报告 26.917 为「选择项目」,两者并列命中(含 26.903 及更早)",
|
|
80
92
|
},
|
|
81
93
|
projectItem: {
|
|
82
94
|
primary: 'button[aria-label$="的项目操作"]',
|
|
83
95
|
fallbacks: ["button[aria-label*='project actions' i]"],
|
|
84
96
|
ariaPatterns: ["的项目操作$", "project actions$"],
|
|
85
|
-
verifiedVersion: "26.
|
|
97
|
+
verifiedVersion: "26.915.x",
|
|
86
98
|
note: "项目项(按 aria-label 前缀提取项目名)",
|
|
87
99
|
},
|
|
88
100
|
newProjectMenuItem: {
|
|
@@ -144,7 +156,7 @@ export const CODEX_SELECTORS = {
|
|
|
144
156
|
// 作用域:输入框容器(CSS Module 基名 ComposerLayout 稳定,哈希后缀会变)。
|
|
145
157
|
// 兼容性优先,同时列出 ProseMirror 元素本身。
|
|
146
158
|
scope: '[class*="ComposerLayout"],div.ProseMirror[contenteditable="true"]',
|
|
147
|
-
verifiedVersion: "26.
|
|
159
|
+
verifiedVersion: "26.915.x",
|
|
148
160
|
note: "模型+思考等级菜单触发器;限定在输入框作用域内,排除顶部菜单栏与模式切换器",
|
|
149
161
|
},
|
|
150
162
|
reasoningSlider: {
|
|
@@ -178,7 +190,7 @@ export const CODEX_SELECTORS = {
|
|
|
178
190
|
primary: 'button[aria-label="更改权限"]',
|
|
179
191
|
fallbacks: ["button[aria-label*='权限' i]", "button[aria-label*='permission' i]"],
|
|
180
192
|
ariaLabels: ["更改权限", "Change permission", "Permissions"],
|
|
181
|
-
verifiedVersion: "26.
|
|
193
|
+
verifiedVersion: "26.915.x",
|
|
182
194
|
note: "权限模式触发器(文本如「完全访问」)",
|
|
183
195
|
},
|
|
184
196
|
permissionOption: {
|
|
@@ -205,7 +217,7 @@ export const CODEX_SELECTORS = {
|
|
|
205
217
|
'[class*="MessageList" i]',
|
|
206
218
|
'[class*="message-list" i]',
|
|
207
219
|
],
|
|
208
|
-
verifiedVersion: "26.
|
|
220
|
+
verifiedVersion: "26.915.x",
|
|
209
221
|
note: "对话正文区域;用于文本稳定兜底判定(勿用裸 main/#root,会混入导航壳)",
|
|
210
222
|
},
|
|
211
223
|
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GUI 选择器解析失败的统一诊断(issue #23「机制」建议)。
|
|
3
|
+
*
|
|
4
|
+
* 三个 GUI agent(codex / qoder / traework)的选择器解析失败信息往往只有
|
|
5
|
+
* 「未出现 X」——但报告者真正需要的是「页面上实际有什么」。issue #23 的调查里,
|
|
6
|
+
* 最有价值的线索(`aria-haspopup="dialog"` 恰 1 个、aria-label 已变成「选择项目」)
|
|
7
|
+
* 是人工打开 CDP 探针才拿到的。本模块把这步自动化:解析失败时把页面可见候选
|
|
8
|
+
* 标签一并写进错误信息与日志。
|
|
9
|
+
*
|
|
10
|
+
* 约定:本模块只产出「页面内表达式」与「文本」,不参与任何控制流或判定,
|
|
11
|
+
* 因此注入失败、页面无元素等情况都必须安全降级为空候选。
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_DIAGNOSTIC_LIMIT = 20;
|
|
14
|
+
/** 交互元素扫描选择器(与 codex resolve 的 scan 面保持一致,宁可多扫后截断) */
|
|
15
|
+
const DIAGNOSTIC_SCAN = '[aria-label],[role="button"],[role="menuitem"],[role="menuitemradio"],[role="option"],[role="tab"],button,a,label';
|
|
16
|
+
/**
|
|
17
|
+
* 生成页面内表达式:收集当前可见、带 aria-label 或可见文本的交互元素标签
|
|
18
|
+
* (归一化、去重、截断)。返回 `string[]`,可直接交给 `cdp.evaluate`。
|
|
19
|
+
*/
|
|
20
|
+
export function visibleLabelsExpr(opts = {}) {
|
|
21
|
+
const limit = Math.max(1, Math.floor(opts.limit ?? DEFAULT_DIAGNOSTIC_LIMIT));
|
|
22
|
+
const scopeSel = opts.scope ? JSON.stringify(opts.scope) : "''";
|
|
23
|
+
const includeText = opts.includeText !== false;
|
|
24
|
+
const scan = JSON.stringify(DIAGNOSTIC_SCAN);
|
|
25
|
+
return `(function(){
|
|
26
|
+
try{
|
|
27
|
+
var limit=${limit},includeText=${includeText},scopeSel=${scopeSel},scan=${scan};
|
|
28
|
+
var root=scopeSel?document.querySelector(scopeSel):document;
|
|
29
|
+
if(!root)return [];
|
|
30
|
+
var vis=function(e){try{var r=e.getBoundingClientRect();return r.width>0&&r.height>0&&r.bottom>0&&r.right>0&&r.top<innerHeight&&r.left<innerWidth}catch(_){return false}};
|
|
31
|
+
var norm=function(s){return (s||'').normalize('NFC').replace(/\\s+/g,' ').trim()};
|
|
32
|
+
var out=[],seen={};
|
|
33
|
+
var push=function(s){var v=norm(s);if(!v||v.length>80)return;var k=v.toLocaleLowerCase();if(seen[k])return;seen[k]=1;out.push(v)};
|
|
34
|
+
var nodes=root.querySelectorAll(scan);
|
|
35
|
+
for(var i=0;i<nodes.length;i++){
|
|
36
|
+
if(out.length>=limit)break;
|
|
37
|
+
var e=nodes[i];
|
|
38
|
+
if(!vis(e))continue;
|
|
39
|
+
var aria=norm(e.getAttribute('aria-label')||'');
|
|
40
|
+
if(aria){push(aria);continue}
|
|
41
|
+
if(includeText){var t=norm(e.innerText||e.textContent||'');if(t&&t.length<=40)push(t)}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}catch(_){return []}
|
|
45
|
+
})()`;
|
|
46
|
+
}
|
|
47
|
+
/** 从 CDP 返回值安全提取字符串标签数组(过滤非字符串、去重、截断)。 */
|
|
48
|
+
export function normalizeLabels(raw, max = DEFAULT_DIAGNOSTIC_LIMIT) {
|
|
49
|
+
if (!Array.isArray(raw))
|
|
50
|
+
return [];
|
|
51
|
+
const out = [];
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
for (const item of raw) {
|
|
54
|
+
if (typeof item !== "string")
|
|
55
|
+
continue;
|
|
56
|
+
const v = item.replace(/\s+/g, " ").trim();
|
|
57
|
+
if (!v)
|
|
58
|
+
continue;
|
|
59
|
+
const key = v.toLocaleLowerCase();
|
|
60
|
+
if (seen.has(key))
|
|
61
|
+
continue;
|
|
62
|
+
seen.add(key);
|
|
63
|
+
out.push(v);
|
|
64
|
+
if (out.length >= max)
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
/** 把候选标签渲染成错误信息后缀(无候选返回空串)。 */
|
|
70
|
+
export function formatCandidates(labels, max = DEFAULT_DIAGNOSTIC_LIMIT) {
|
|
71
|
+
const list = normalizeLabels(labels, max);
|
|
72
|
+
if (!list.length)
|
|
73
|
+
return "";
|
|
74
|
+
return `;页面可见候选=[${list.join(" | ")}]`;
|
|
75
|
+
}
|
|
76
|
+
/** 幂等追加诊断:已含候选段时原样返回,避免重复拼接。 */
|
|
77
|
+
export function withDiagnostics(message, labels, max = DEFAULT_DIAGNOSTIC_LIMIT) {
|
|
78
|
+
if (/页面可见候选=/.test(message))
|
|
79
|
+
return message;
|
|
80
|
+
return message + formatCandidates(labels, max);
|
|
81
|
+
}
|
package/dist/agents/qoder/cdp.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TraeworkCdpClient } from '../traework/cdp/client.js';
|
|
2
|
-
import { QODER_SELECTORS } from './selectors.js';
|
|
2
|
+
import { QODER_SELECTORS, qoderCandidates, qoderPrimary } from './selectors.js';
|
|
3
|
+
import { visibleLabelsExpr, normalizeLabels } from '../gui-diagnostics.js';
|
|
3
4
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
5
|
const VISIBLE = `(e)=>{
|
|
5
6
|
if(!e||e.closest('[inert],[hidden],[aria-hidden="true"],[role="dialog"][data-state="closed"]'))return false;
|
|
@@ -20,7 +21,9 @@ export class QoderCdpClient {
|
|
|
20
21
|
this.overrides = overrides;
|
|
21
22
|
this.client = new TraeworkCdpClient({ port, sendTimeoutMs: timeout, targetRank: qoderTargetRank });
|
|
22
23
|
}
|
|
23
|
-
selector(key) { return this.overrides
|
|
24
|
+
selector(key) { return qoderPrimary(key, this.overrides); }
|
|
25
|
+
/** 该语义键的全部候选选择器(覆盖优先,其后 primary 与 fallbacks,去重保序) */
|
|
26
|
+
candidates(key) { return qoderCandidates(key, this.overrides); }
|
|
24
27
|
async connect() {
|
|
25
28
|
await this.client.connect();
|
|
26
29
|
if (qoderTargetRank({ url: await this.evaluate('location.href') }) !== 0) {
|
|
@@ -33,8 +36,39 @@ export class QoderCdpClient {
|
|
|
33
36
|
async sessionId() {
|
|
34
37
|
return this.evaluate(`location.hash.startsWith('#/chat/')?location.hash.split('?')[0].slice(7):undefined`);
|
|
35
38
|
}
|
|
39
|
+
/** 收集页面可见候选标签(issue #23 诊断机制);失败安全返回空数组。 */
|
|
40
|
+
async visibleLabels(scopeCss) {
|
|
41
|
+
try {
|
|
42
|
+
return normalizeLabels(await this.evaluate(visibleLabelsExpr({ scope: scopeCss })));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** 按候选顺序探测某语义键是否存在(单次 evaluate,无等待循环,故多候选代价低) */
|
|
49
|
+
async existsKey(key) {
|
|
50
|
+
for (const css of this.candidates(key))
|
|
51
|
+
if (await this.exists(css))
|
|
52
|
+
return true;
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 按候选顺序点击某语义键:先探测哪个候选存在(避免每个候选各自打满超时),
|
|
57
|
+
* 再对命中的那个执行坐标点击。全部候选都不存在则抛错并附诊断。
|
|
58
|
+
*/
|
|
59
|
+
async clickKey(key, text, index) {
|
|
60
|
+
const cands = this.candidates(key);
|
|
61
|
+
for (const css of cands) {
|
|
62
|
+
if (await this.exists(css)) {
|
|
63
|
+
await this.click(css, text, index);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const labels = await this.visibleLabels();
|
|
68
|
+
throw new Error(`qoder_control_missing_or_ambiguous: ${key}; candidates=${cands.join(' , ')}${labels.length ? `; 页面可见候选=[${labels.join(' | ')}]` : ''}`);
|
|
69
|
+
}
|
|
36
70
|
async poll() {
|
|
37
|
-
const s = Object.fromEntries(Object.keys(QODER_SELECTORS).map(k => [k, this.
|
|
71
|
+
const s = Object.fromEntries(Object.keys(QODER_SELECTORS).map(k => [k, this.candidates(k).join(',')]));
|
|
38
72
|
return this.evaluate(`(()=>{
|
|
39
73
|
const s=${JSON.stringify(s)},visible=e=>!!e&&!!e.getBoundingClientRect().width&&!e.hidden;
|
|
40
74
|
const user=[...document.querySelectorAll(s.userMessage)].at(-1);
|
|
@@ -87,8 +121,10 @@ export class QoderCdpClient {
|
|
|
87
121
|
if (Date.now() < deadline)
|
|
88
122
|
await delay(Math.min(100, deadline - Date.now()));
|
|
89
123
|
} while (Date.now() < deadline);
|
|
90
|
-
if (!point)
|
|
91
|
-
|
|
124
|
+
if (!point) {
|
|
125
|
+
const labels = await this.visibleLabels();
|
|
126
|
+
throw new Error(`qoder_control_missing_or_ambiguous: ${css} ${text ?? ''}${labels.length ? `; 页面可见候选=[${labels.join(' | ')}]` : ''}`);
|
|
127
|
+
}
|
|
92
128
|
await this.client.send('Input.dispatchMouseEvent', { type: 'mousePressed', ...point, button: 'left', clickCount: 1 });
|
|
93
129
|
await this.client.send('Input.dispatchMouseEvent', { type: 'mouseReleased', ...point, button: 'left', clickCount: 1 });
|
|
94
130
|
}
|