tianshu-mcp 0.5.1 → 0.5.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 +22 -0
- package/CHANGELOG.md +22 -0
- package/README.en.md +1 -0
- package/README.md +1 -0
- package/dist/agents/zcode/cdp.js +49 -4
- package/dist/agents/zcode/dom.js +57 -0
- package/dist/agents/zcode/references.js +18 -5
- package/dist/agents/zcode/run.js +320 -169
- package/dist/agents/zcode/selectors.js +9 -0
- package/dist/config/schema.js +23 -1
- package/dist/loop/fix-loop.js +42 -20
- package/dist/mcp/context.js +2 -0
- package/dist/mcp/handlers.js +78 -0
- package/dist/tasks/task-manager.js +14 -3
- package/dist/tasks/task.js +10 -0
- package/dist/version.generated.js +1 -1
- package/dist/visual/manage.js +12 -0
- package/docs/visual-validation.en.md +10 -0
- package/docs/visual-validation.md +10 -0
- package/package.json +1 -1
- package/skills/tianshu-mcp/SKILL.md +2 -1
- package/skills/tianshu-mcp/usage-examples.md +31 -0
package/CHANGELOG.en.md
CHANGED
|
@@ -12,12 +12,34 @@ Chinese version: [CHANGELOG.md](CHANGELOG.md)
|
|
|
12
12
|
|
|
13
13
|
### Planned
|
|
14
14
|
|
|
15
|
+
- **Visual acceptance phase 2 — AI visual content validation** (issue #13): validate whether image/page-screenshot *content* matches the task description (logo elements, style match, page semantics, etc.). Marked in issue #3 as an "optional extension"; its pixel-comparison phase 1 shipped with v0.5.0. Requires settling the model/credential source (without breaking the "zero credential management" red line), judgement debouncing, and gate placement (warning-only by default is suggested).
|
|
15
16
|
- More external AI-Agent adapters (a new agent = one profile + an optional adapter file).
|
|
16
17
|
- TraeWork executable discovery and native-dialog driving on macOS (currently fail-closed).
|
|
17
18
|
- Optional project-level skill seeding (by default nothing is written into target repos).
|
|
18
19
|
- Cancel/rework/new-project matrices for the Codex and ZCode GUI drivers on macOS (both remain `research` on darwin).
|
|
19
20
|
- Best-effort stop of a GUI-side pending session (via a temporary CDP connection) when cancelling
|
|
20
21
|
a task in the `needs_user` state.
|
|
22
|
+
- Real-machine verification of project-less dispatch on macOS (this round covers Windows 10 only).
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## [0.5.2] — 2026-09-14
|
|
27
|
+
|
|
28
|
+
**Project-less dispatch for ZCode (issue #12)**: `run_task`'s `projectPath` is now optional, letting ZCode run tasks in its `default` workspace; the companion `allowCreateProject` can forbid automatic project import. See the [v0.5.2 release notes](docs/release-v0.5.2.en.md) and the [Windows 10 acceptance record](docs/zcode-issue-12-windows-evidence.en.md).
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **Project-less dispatch for ZCode (issue #12)**: `run_task`'s `projectPath` is now optional. When omitted, ZCode runs the task in its `default` workspace — no directory assigned, no project registered or imported, no Git baseline, no project snapshot freeze, no project lock, no project acceptance. On success the task is marked structurally as `not_applicable: no_project` and the terminal message states "no project acceptance performed". `query_task` / `list_tasks` display such tasks normally; `verify_task` / `get_task_report` return an explicit not-applicable explanation instead of deriving a directory from cwd.
|
|
33
|
+
- **`allowCreateProject` (ZCode-only, optional boolean)**: omitted keeps the existing "auto-import when the target is unregistered" behaviour; explicit `false` stops dispatch **before any import side effect** when the target is unregistered, returning a recognisable `project_not_registered` reason with remediation (no native folder dialog, no project added). Other agents passing this parameter get an explicit "not supported" error rather than a silent ignore.
|
|
34
|
+
- **Windows 10 hardware acceptance record** (`docs/zcode-issue-12-windows-evidence{,.en}.md`): complete evidence for project-less dispatch and `allowCreateProject=false` on ZCode 3.11.2.6792, including the before/after comparison "ZCode project entries 34 → 34, 0 added / 0 removed".
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **Divergent project-trigger readiness criteria (issue #12 §5)**: waiting used `exists` (element has width/height only) while clicking went through `pick` (exactly one unclipped visible node in the winning tier), so an `exists=true` / `click=false` window existed. Waiting and clicking now share one **structured probe** that distinguishes not-mounted / mounted-but-invisible-or-clipped / ambiguous / disabled / covered / ready, plus a post-click condition: the project menu must actually open, and `menu-not-open` is classified separately.
|
|
39
|
+
- **Error message contradicting behaviour**: "waiting for the project trigger timed out" is no longer used for early exits (multiple matches, disabled) or a menu that never opened; failure text carries `selector`, match count and minimal hit-node attributes, and diagnostics log attempt count, elapsed time and remaining budget.
|
|
40
|
+
- **Centralised timeout**: new `gui.projectTriggerTimeoutMs` (default 15s) replaces the two hard-coded `15_000` literals; the whole "wait → one sidebar fallback → wait" sequence shares a single deadline, retries do not reset the budget, and it is clamped by the setup-recovery budget and the task deadline.
|
|
41
|
+
- **`projectPath` was never opened up in the MCP schema (found on hardware)**: the handler already had the project-less branch, but `RunTaskParamsSchema.projectPath` was still required, so a real `run_task` was rejected by the SDK with `-32602 Required at projectPath`. Unit tests call the handler directly and therefore bypass `inputSchema`, which is why a green suite missed it. Changed to `AbsPath.optional()`, plus a protocol-level regression case in `test/integration/task-flow.test.ts` asserting neither `-32602` nor `Input validation error` appears.
|
|
42
|
+
- **No "work outside a project" switch, and the menu click was undone by toggle semantics (found on hardware)**: ZCode's "New task" inherits the previous binding, so project-less dispatch parked at `needs_user` forever; meanwhile `clickProjectTriggerAndConfirm` kept clicking the trigger even when the project menu was **already open**, closing the Radix dropdown and then polling until its deadline, misreported as "the project menu did not open". Added the `workOutsideProject` selector and `enterDefaultWorkspace()` for an explicit switch (confirmed by `workspaceBinding` read-back, not by click success), made the click check the menu state first, and made `confirmDefaultWorkspace` return immediately for "definitely bound to a project".
|
|
21
43
|
|
|
22
44
|
---
|
|
23
45
|
|
package/CHANGELOG.md
CHANGED
|
@@ -11,11 +11,33 @@
|
|
|
11
11
|
|
|
12
12
|
### 计划中
|
|
13
13
|
|
|
14
|
+
- **视觉验收第二阶段——AI 视觉内容校验**(issue #13):校验图片/页面截图**内容**是否符合任务描述(Logo 元素、风格匹配、页面语义等)。issue #3 中标注为「可选扩展」,其像素级对比第一阶段已随 v0.5.0 完成。需先确认模型与凭证来源(不得破坏「凭证零管理」红线)、判定防抖与门禁定位(建议默认仅告警)。
|
|
14
15
|
- 更多外部 AI-Agent 适配(新 agent = 一个 profile +(如需)一个 adapter 文件)。
|
|
15
16
|
- TraeWork 在 macOS 下的可执行探测与原生对话框驱动(当前 macOS 分支 fail-closed)。
|
|
16
17
|
- 可选的项目级技能播种(默认不写入目标项目仓库)。
|
|
17
18
|
- Codex 与 ZCode GUI 的 macOS 取消/返修/新建项目矩阵(当前两者 darwin 均保持 `research`)。
|
|
18
19
|
- `needs_user` 状态下取消任务时经临时 CDP 连接尽力停止 GUI 内等待中的会话。
|
|
20
|
+
- ZCode 无项目派发在 macOS 上的真机验证(本轮仅 Windows 10 实测)。
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## [0.5.2] — 2026-09-14
|
|
25
|
+
|
|
26
|
+
**ZCode 无项目派发(issue #12)**:`run_task` 的 `projectPath` 变为可选,ZCode 在 `default` 工作区承接任务;配套 `allowCreateProject` 可禁止自动导入项目。完整说明见 [v0.5.2 发布说明](docs/release-v0.5.2.md),真机证据见 [Windows 10 验收记录](docs/zcode-issue-12-windows-evidence.md)。
|
|
27
|
+
|
|
28
|
+
### 新增
|
|
29
|
+
|
|
30
|
+
- **ZCode 无项目派发(issue #12)**:`run_task` 的 `projectPath` 改为可选。省略时 ZCode 在 `default` 工作区承接任务——不分配目录、不登记/导入项目、不采集 Git 基线、不冻结项目快照、不进入项目锁与项目验收;成功后以结构化字段标注 `not_applicable: no_project`,终态文案明示「未进行项目验收」。`query_task` / `list_tasks` 正常展示该类任务;`verify_task` / `get_task_report` 返回明确的不适用说明,不从 cwd 推导目录。
|
|
31
|
+
- **`allowCreateProject`(ZCode 专用,可选布尔)**:省略 = 保持既有「目标未登记即自动导入」行为;显式 `false` 时,目标目录未登记即在**任何导入副作用之前**停止派发,返回可识别的 `project_not_registered` 与处理说明(不打开原生文件夹对话框、不添加项目)。其他 agent 显式传入该参数会得到明确的「不支持」错误,而不是被静默忽略。
|
|
32
|
+
- **Windows 10 真机验收记录**(`docs/zcode-issue-12-windows-evidence{,.en}.md`):ZCode 3.11.2.6792 上无项目派发与 `allowCreateProject=false` 的完整证据,含「派发前后 ZCode 项目条目 34 → 34、新增 0 / 消失 0」的对比。
|
|
33
|
+
|
|
34
|
+
### 修复
|
|
35
|
+
|
|
36
|
+
- **ZCode 项目触发器就绪判据不一致(issue #12 §五)**:等待用的是 `exists`(只看元素宽高),点击走的是 `pick`(要求该优先级层恰好一个未被裁剪的可见节点),两者判据不同,因此存在 `exists=true` 但 `click=false` 的窗口。现在等待与点击**共用同一份结构化探测**,区分未挂载 / 已挂载但不可见或被裁剪 / 不唯一 / 禁用 / 被遮挡 / 就绪;并新增点击后置检查——项目菜单必须真正打开,`menu-not-open` 单独归类。
|
|
37
|
+
- **错误信息与行为失实**:「等待项目触发器超时」不再被用来描述早退(多匹配、禁用)或菜单未打开;失败文案携带 `selector`、匹配数与命中节点最小属性,诊断日志记录尝试次数、实际耗时与剩余预算。
|
|
38
|
+
- **集中超时**:新增 `gui.projectTriggerTimeoutMs`(默认 15s)替换原先写死的两处 `15_000`;整个「等待 → 回退一次侧栏新建任务 → 再等待」共享同一截止时间,重试不重置预算,并被 setup 恢复预算与任务总时限夹住。
|
|
39
|
+
- **`projectPath` 未在 MCP schema 层放开(真机发现)**:handler 已支持无项目分支,但 `RunTaskParamsSchema.projectPath` 仍是必填,真实 `run_task` 会在协议层被 SDK 拒成 `-32602 Required at projectPath`;而单元测试直接调 handler、绕过了 `inputSchema`,所以全绿也没抓到。已改为 `AbsPath.optional()`,并在 `test/integration/task-flow.test.ts` 补协议层回归用例(断言文本不出现 `-32602` / `Input validation error`)。
|
|
40
|
+
- **缺少「不在项目中工作」切换,且项目菜单已开时点击被 toggle 反噬(真机发现)**:ZCode「新建任务」会继承上一次绑定,使无项目派发永久停在 `needs_user`;同时 `clickProjectTriggerAndConfirm` 在项目菜单**已经打开**时仍点击触发器,把 Radix 下拉关掉后一路轮询到 deadline,误报「项目菜单未打开」。现新增 `workOutsideProject` 选择器与 `enterDefaultWorkspace()` 显式切换(切换后以 `workspaceBinding` 回读为准),点击前先查菜单是否已开;`confirmDefaultWorkspace` 对「明确绑定着项目」立即返回而非空等。
|
|
19
41
|
|
|
20
42
|
---
|
|
21
43
|
|
package/README.en.md
CHANGED
|
@@ -42,6 +42,7 @@ Tianshu plays the role of the overall commander; this MCP server is the **schedu
|
|
|
42
42
|
- **Acceptance parallelism**: command checks run **bounded-parallel** by default (`verifyConcurrency`, default 2, range 1–4). When checks depend on an order (a later check reading build output, `--fix`, shared cache dirs), set it to `1` for fully serial behaviour; a project can override it in `.tianshu-mcp/acceptance.json`, and the server level lives in `config.json`. Report and log formats are unchanged (results are returned in declaration order).
|
|
43
43
|
- **Rework loop**: automatic rework (`autoFixRounds`) + manual `rework_task`; on verification failure a repair-plan file is generated and fed back to the agent; when rounds run out → `needs_attention` awaiting Tianshu's verdict.
|
|
44
44
|
- **Execution surfaces**: `driver: "gui"` selects an explicit, isolated Codex/TraeWork/ZCode CDP adapter; `driver: "spawn"` runs an external CLI child process.
|
|
45
|
+
- **Project-less dispatch (ZCode, issue #12)**: `run_task`'s `projectPath` may be omitted — ZCode runs the task in its `default` workspace without registering/importing a project, collecting a Git baseline, or running project acceptance (the result is marked structurally as `verificationNotApplicable: "no_project"` and `verify_task`/`get_task_report` return a not-applicable explanation). The companion `allowCreateProject: false` stops dispatch before any import side effect when the target directory is unregistered. See the [ZCode CDP adapter](docs/zcode-cdp.en.md).
|
|
45
46
|
- **Scheduling discipline**: per-project serial queue + global concurrency cap (default 2, configurable).
|
|
46
47
|
- **No key handling**: each agent uses its own login state; this server never stores or forwards any API key.
|
|
47
48
|
- **Extensible**: a new agent = one profile (data) + (if needed) one adapter file — no changes to the orchestration core.
|
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
- **验收并行度**:命令检查默认**有界并行**(`verifyConcurrency`,默认 2、范围 1–4)。检查项之间有顺序依赖时(后续检查读取 build 产物、带 `--fix`、共享缓存目录)请设 `1` 完全退化为串行;项目级 `.tianshu-mcp/acceptance.json` 可覆盖,server 级在 `config.json`。报告与日志格式不变(结果按声明顺序返回)。
|
|
43
43
|
- **失败返修闭环**:自动返修(`autoFixRounds`)+ 手动 `rework_task`;验收失败时自动生成修复计划文件并回填给 agent;轮次用尽 → `needs_attention` 等天枢裁决。
|
|
44
44
|
- **执行面**:`driver: "gui"` 由显式 adapter 驱动桌面 UI(Codex / TraeWork / ZCode 各自使用隔离的 CDP 流程);`driver: "spawn"` 走外部 CLI 子进程。
|
|
45
|
+
- **无项目派发(ZCode,issue #12)**:`run_task` 的 `projectPath` 可省略——ZCode 在 `default` 工作区承接任务,不登记/导入项目、不采集 Git 基线、不执行项目验收(结果以 `verificationNotApplicable: "no_project"` 结构化标注,`verify_task`/`get_task_report` 返回不适用说明)。配套 `allowCreateProject: false` 可在目标目录未登记时于任何导入副作用之前停止派发。详见 [ZCode CDP 适配器](docs/zcode-cdp.md)。
|
|
45
46
|
- **调度纪律**:每项目串行队列 + 全局并发上限(默认 2,可配)。
|
|
46
47
|
- **不碰密钥**:各 agent 用自己的登录态;本 server 不保存/转发任何 API key。
|
|
47
48
|
- **可扩展**:新 agent = 一个 profile(数据)+(如需)一个 adapter 文件,零改编排核心。
|
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, workspaceBindingExpression, modelSelectionExpression, sendButtonPointExpression, } from "./dom.js";
|
|
5
|
+
import { 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++) {
|
|
@@ -18,6 +18,8 @@ export async function retryZcodeEvaluation(operation, sleep = (ms) => new Promis
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
/** 点击后确认菜单打开的轮询间隔(不是独立超时;窗口由调用方从集中配置与总预算算出)。 */
|
|
22
|
+
const PROJECT_MENU_POLL_MS = 100;
|
|
21
23
|
export class ZcodeCdpClient {
|
|
22
24
|
selectors;
|
|
23
25
|
inner;
|
|
@@ -86,10 +88,11 @@ export class ZcodeCdpClient {
|
|
|
86
88
|
}
|
|
87
89
|
async click(key) {
|
|
88
90
|
if (key === "projectTrigger") {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
// 与等待共用同一就绪判据:多匹配、不可见、禁用、被遮挡一律不算就绪,不发鼠标事件。
|
|
92
|
+
const probe = await this.probeProjectTrigger();
|
|
93
|
+
if (!probe.ready || !probe.point)
|
|
91
94
|
return false;
|
|
92
|
-
await this.clickAt(point.x, point.y);
|
|
95
|
+
await this.clickAt(probe.point.x, probe.point.y);
|
|
93
96
|
return true;
|
|
94
97
|
}
|
|
95
98
|
const point = await this.evaluate(`(function(){for(const s of ${candidateExpr(key, this.selectors)}){for(const e of document.querySelectorAll(s)){const r=e.getBoundingClientRect();if(r.width&&r.height)return {x:r.left+r.width/2,y:r.top+r.height/2}}}return null})()`);
|
|
@@ -98,6 +101,48 @@ export class ZcodeCdpClient {
|
|
|
98
101
|
await this.clickAt(point.x, point.y);
|
|
99
102
|
return true;
|
|
100
103
|
}
|
|
104
|
+
/** 项目触发器结构化探测(只读,不发事件)。 */
|
|
105
|
+
async probeProjectTrigger() {
|
|
106
|
+
return this.evaluate(projectTriggerProbeExpression(this.selectors));
|
|
107
|
+
}
|
|
108
|
+
/** 项目菜单是否已真正可见(点击后置条件)。 */
|
|
109
|
+
async projectMenuOpen() {
|
|
110
|
+
return this.evaluate(projectMenuOpenExpression());
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 探测 → 点击 → 确认项目菜单打开。事件发出不算成功:菜单没开就返回 menu-not-open。
|
|
114
|
+
* deadline 由调用方从集中配置与 setup/task 总预算取上限(重试不重置截止时间)。
|
|
115
|
+
*/
|
|
116
|
+
async clickProjectTriggerAndConfirm(deadline) {
|
|
117
|
+
const probe = await this.probeProjectTrigger();
|
|
118
|
+
// 菜单可能**已经开着**(上一次尝试或外部操作的残留)。Radix 下拉是 toggle:此时再点
|
|
119
|
+
// 触发器会把它关掉,随后整段等待都会落到 menu-not-open(3.11.2 真机实测踩过)。
|
|
120
|
+
// 所以先查后点。
|
|
121
|
+
if (await this.projectMenuOpen())
|
|
122
|
+
return { opened: true, probe };
|
|
123
|
+
if (!probe.ready || !probe.point)
|
|
124
|
+
return { opened: false, reason: "not-ready", probe };
|
|
125
|
+
await this.clickAt(probe.point.x, probe.point.y);
|
|
126
|
+
while (Date.now() < deadline) {
|
|
127
|
+
// eslint-disable-next-line no-await-in-loop
|
|
128
|
+
if (await this.projectMenuOpen())
|
|
129
|
+
return { opened: true, probe };
|
|
130
|
+
// eslint-disable-next-line no-await-in-loop
|
|
131
|
+
await new Promise((resolve) => setTimeout(resolve, PROJECT_MENU_POLL_MS));
|
|
132
|
+
}
|
|
133
|
+
return { opened: false, reason: "menu-not-open", probe };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* 点击「不在项目中工作」(要求该层唯一可见):进入 ZCode 的 default(无项目)工作区。
|
|
137
|
+
* 新建任务会继承上一次绑定,所以无项目派发必须先显式执行这一步。
|
|
138
|
+
*/
|
|
139
|
+
async clickWorkOutsideProject() {
|
|
140
|
+
const found = await this.evaluate(workOutsideProjectExpression(this.selectors));
|
|
141
|
+
if (!found.point)
|
|
142
|
+
return { clicked: false, count: found.count };
|
|
143
|
+
await this.clickAt(found.point.x, found.point.y);
|
|
144
|
+
return { clicked: true, count: 1 };
|
|
145
|
+
}
|
|
101
146
|
async clickExact(key, value) {
|
|
102
147
|
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 label=e=>(e.getAttribute('data-value')||e.getAttribute('data-model')||e.getAttribute('data-provider')||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();return {count:1,...result,point:{x:r.left+r.width/2,y:r.top+r.height/2}}}}if(scroller)scroller.scrollTop=start;return {count:matchesFound.length,...result}})()`);
|
|
103
148
|
if (!found.point)
|
package/dist/agents/zcode/dom.js
CHANGED
|
@@ -74,3 +74,60 @@ export function sendButtonPointExpression(overrides) {
|
|
|
74
74
|
return hit && (hit===e || e.contains(hit)) ? {x,y} : null;
|
|
75
75
|
})()`;
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* 项目触发器结构化探测:等待与点击共用这一份就绪判据,消除「exists 说有、click 说没有」
|
|
79
|
+
* (exists 只看宽高,pick 还要求该优先级层唯一且未被祖先裁剪)。
|
|
80
|
+
*
|
|
81
|
+
* 返回 selector / 该层匹配数 count / 全部候选命中的挂载数 mounted / 命中节点最小诊断属性,
|
|
82
|
+
* 让上层区分未挂载、不可见、不唯一、禁用、遮挡与真实超时,而不是统统归成「等待超时」。
|
|
83
|
+
* 只用于项目触发器;登录页等其它 exists 调用语义不变。
|
|
84
|
+
*/
|
|
85
|
+
export function projectTriggerProbeExpression(overrides) {
|
|
86
|
+
return `(function(){${ZCODE_DOM}
|
|
87
|
+
const describe = e => e ? String(e.tagName || '')
|
|
88
|
+
+ (e.getAttribute('data-testid') ? '#' + e.getAttribute('data-testid') : '')
|
|
89
|
+
+ (e.getAttribute('aria-label') ? '[' + e.getAttribute('aria-label') + ']' : '') : '';
|
|
90
|
+
const sels = ${candidateExpr("projectTrigger", overrides)};
|
|
91
|
+
const mountedNodes = new Set();
|
|
92
|
+
for (const s of sels) for (const e of document.querySelectorAll(s)) mountedNodes.add(e);
|
|
93
|
+
const mounted = mountedNodes.size;
|
|
94
|
+
const m = pick(sels);
|
|
95
|
+
const base = { selector: m.selector, count: m.count, mounted, ready: false };
|
|
96
|
+
if (!mounted) return Object.assign({}, base, { state: 'missing' });
|
|
97
|
+
if (m.count === 0)
|
|
98
|
+
return Object.assign({}, base, { state: 'hidden', detail: describe(mountedNodes.values().next().value) });
|
|
99
|
+
if (m.count > 1) return Object.assign({}, base, { state: 'ambiguous' });
|
|
100
|
+
const node = m.node;
|
|
101
|
+
if (node.disabled || node.getAttribute('aria-disabled') === 'true')
|
|
102
|
+
return Object.assign({}, base, { state: 'disabled', detail: describe(node) });
|
|
103
|
+
const r = node.getBoundingClientRect(), x = r.left + r.width / 2, y = r.top + r.height / 2;
|
|
104
|
+
const hit = document.elementFromPoint(x, y);
|
|
105
|
+
// 命中目标本身或其子树才算命中目标;被别的元素盖住不是可点击状态。
|
|
106
|
+
if (!hit || !(hit === node || node.contains(hit)))
|
|
107
|
+
return Object.assign({}, base, { state: 'covered', detail: describe(hit) });
|
|
108
|
+
return Object.assign({}, base, { state: 'ready', ready: true, point: { x, y }, detail: describe(node) });
|
|
109
|
+
})()`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 点击项目触发器后的后置条件:项目菜单必须真正可见。
|
|
113
|
+
* 鼠标事件发出去本身不算成功——菜单没开就说明这次点击没有生效。
|
|
114
|
+
*/
|
|
115
|
+
export function projectMenuOpenExpression() {
|
|
116
|
+
return `(function(){${ZCODE_DOM}
|
|
117
|
+
if ([...document.querySelectorAll('[role="menu"]')].some(visible)) return true;
|
|
118
|
+
return [...document.querySelectorAll('[role="menuitemcheckbox"]')].some(visible);
|
|
119
|
+
})()`;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 「不在项目中工作」菜单项(`composer-work-outside-project`):ZCode 用它进入 default
|
|
123
|
+
* (无项目)工作区。要求命中层唯一可见,否则不点击——多匹配说明菜单结构已漂移,
|
|
124
|
+
* 猜一个点会把任务送到错误的工作区。
|
|
125
|
+
*/
|
|
126
|
+
export function workOutsideProjectExpression(overrides) {
|
|
127
|
+
return `(function(){${ZCODE_DOM}
|
|
128
|
+
const found = pick(${candidateExpr("workOutsideProject", overrides)});
|
|
129
|
+
if (!found.node) return { count: found.count };
|
|
130
|
+
const r = found.node.getBoundingClientRect();
|
|
131
|
+
return { count: found.count, point: { x: r.left + r.width / 2, y: r.top + r.height / 2 } };
|
|
132
|
+
})()`;
|
|
133
|
+
}
|
|
@@ -4,18 +4,31 @@ function inside(root, candidate) {
|
|
|
4
4
|
const rel = path.relative(root, candidate);
|
|
5
5
|
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
6
6
|
}
|
|
7
|
-
|
|
7
|
+
/** 从任务书/上下文中抽取「像本地路径」的候选值(URL、模型名、命令文本等已被排除)。 */
|
|
8
|
+
export function collectLocalReferences(task, context) {
|
|
8
9
|
const text = `${task}\n${context ?? ""}`;
|
|
9
10
|
const values = new Set();
|
|
10
11
|
for (const m of text.matchAll(/`([^`]+)`/g))
|
|
11
12
|
values.add(m[1].trim());
|
|
12
13
|
for (const m of text.matchAll(/(?:^|\s)((?:[A-Za-z]:[\\/]|\/|\.\.?[\\/])[^\s,。;;]+)/gm))
|
|
13
14
|
values.add(m[1].trim());
|
|
15
|
+
return [...values].filter((source) => path.isAbsolute(source) || /^\.\.?[\\/]/.test(source) || /[\\/]/.test(source));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 校验任务书里的本地引用。
|
|
19
|
+
*
|
|
20
|
+
* `projectRoot` 缺失(无项目模式,issue #12)时**不得**退回 cwd / 主目录 / 日志目录去解析:
|
|
21
|
+
* 识别到明确引用就直接说明需要 projectPath,没有引用则返回空表。
|
|
22
|
+
*/
|
|
23
|
+
export function validateTaskReferences(task, context, projectRoot) {
|
|
24
|
+
const candidates = collectLocalReferences(task, context);
|
|
25
|
+
if (!projectRoot) {
|
|
26
|
+
if (candidates.length)
|
|
27
|
+
throw new Error(`任务书包含本地文件引用(${candidates.slice(0, 5).join("、")}${candidates.length > 5 ? ` 等 ${candidates.length} 项` : ""});无项目模式无法解析路径,请提供 projectPath`);
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
14
30
|
const out = [];
|
|
15
|
-
for (const source of
|
|
16
|
-
// Backticks frequently contain identifiers/commands. Only path-like values are interpreted.
|
|
17
|
-
if (!path.isAbsolute(source) && !/^\.\.?[\\/]/.test(source) && !/[\\/]/.test(source))
|
|
18
|
-
continue;
|
|
31
|
+
for (const source of candidates) {
|
|
19
32
|
const absolutePath = path.resolve(projectRoot, source);
|
|
20
33
|
if (!inside(path.resolve(projectRoot), absolutePath))
|
|
21
34
|
throw new Error(`引用路径越出项目范围:${source}`);
|