tianshu-mcp 0.3.2 → 0.3.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 +25 -4
- package/CHANGELOG.md +25 -4
- package/dist/agents/builtin.js +4 -4
- package/dist/agents/zcode/cdp.d.ts +12 -6
- package/dist/agents/zcode/cdp.js +20 -5
- package/dist/agents/zcode/project.d.ts +4 -0
- package/dist/agents/zcode/project.js +24 -0
- package/dist/agents/zcode/run.js +79 -51
- package/dist/agents/zcode/selectors.js +7 -4
- package/dist/config/schema.d.ts +3 -0
- package/dist/config/schema.js +1 -0
- package/dist/util/path.js +4 -5
- package/dist/verify/acceptance.d.ts +1 -0
- package/dist/verify/acceptance.js +69 -6
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.en.md
CHANGED
|
@@ -19,9 +19,29 @@ Chinese version: [CHANGELOG.md](CHANGELOG.md)
|
|
|
19
19
|
- Best-effort stop of a GUI-side pending session (via a temporary CDP connection) when cancelling
|
|
20
20
|
a task in the `needs_user` state.
|
|
21
21
|
|
|
22
|
-
---
|
|
23
|
-
|
|
24
|
-
## [0.3.
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## [0.3.3] — 2026-09-12
|
|
25
|
+
|
|
26
|
+
Fixes issues #4 / #7 by adapting the ZCode GUI driver to the 3.11.2 model-menu and project-binding semantics, while making acceptance fail closed for zero-test and zero-change outcomes.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- ZCode provider selectors now support both `group-provider` and the 3.11.2 `group-family` prefix. Model selection tries the visible model first and only expands a provider/family group as a fallback; failures include visible text and `data-testid` candidates.
|
|
31
|
+
- New-project import dismisses the stale workspace menu before opening Add Project and uses a bounded three-round dismiss/click/verify loop, preventing the first click from being consumed as an outside click.
|
|
32
|
+
- Project binding prioritizes the composer menu's `menuitemcheckbox`; the legacy sidebar item remains a compatibility fallback. Display-name matching normalizes NFKC, whitespace, and case.
|
|
33
|
+
- Binding read-back combines the composer trigger text with the full-path fast path, rejects known unbound placeholders, and retries the idempotent bind operation for up to two rounds.
|
|
34
|
+
- Built-in ZCode and TraeWork discovery paths use `{PROGRAMFILES}` / `{PROGRAMFILES(X86)}`. Environment placeholders in custom profiles are expanded case-insensitively while unknown placeholders remain unchanged.
|
|
35
|
+
- A mandatory test check is failed when it exits with code 0 but its output reports zero executed tests.
|
|
36
|
+
- Git projects now require a change relative to the pre-work baseline by default. Pure question/analysis tasks can explicitly opt out with `"requireChanges": false` in `.tianshu-mcp/acceptance.json`.
|
|
37
|
+
|
|
38
|
+
### Tests
|
|
39
|
+
|
|
40
|
+
- Regression coverage now models ZCode 3.11.2 flat family layouts, legacy provider fallback, testid diagnostics, swallowed project clicks, composer binding and read-back retries, plus zero-test/zero-change acceptance gates.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## [0.3.2] — 2026-09-12
|
|
25
45
|
|
|
26
46
|
Fixes for issues #5 / #6: **the MCP task model was disconnected from the state of the turn inside
|
|
27
47
|
the Codex GUI**. The former is the completion-detection deadlock that kept reporting `running`
|
|
@@ -494,7 +514,8 @@ project → pick model and reasoning level → send instructions → run detecti
|
|
|
494
514
|
|
|
495
515
|
---
|
|
496
516
|
|
|
497
|
-
[Unreleased]: https://github.com/lanlan0811/tianshu-mcp/compare/v0.3.
|
|
517
|
+
[Unreleased]: https://github.com/lanlan0811/tianshu-mcp/compare/v0.3.3...HEAD
|
|
518
|
+
[0.3.3]: https://github.com/lanlan0811/tianshu-mcp/compare/v0.3.2...v0.3.3
|
|
498
519
|
[0.3.2]: https://github.com/lanlan0811/tianshu-mcp/compare/v0.3.1...v0.3.2
|
|
499
520
|
[0.3.1]: https://github.com/lanlan0811/tianshu-mcp/compare/v0.3.0...v0.3.1
|
|
500
521
|
[0.3.0]: https://github.com/lanlan0811/tianshu-mcp/compare/v0.2.0...v0.3.0
|
package/CHANGELOG.md
CHANGED
|
@@ -17,9 +17,29 @@
|
|
|
17
17
|
- Codex GUI 的 macOS 真机验证(当前内置状态为 `research`)。
|
|
18
18
|
- needs_user 状态下取消任务时经临时 CDP 连接尽力停止 GUI 内等待中的会话。
|
|
19
19
|
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
## [0.3.
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## [0.3.3] — 2026-09-12
|
|
23
|
+
|
|
24
|
+
修复 issue #4 / #7:适配 ZCode 3.11.2 的模型菜单与项目绑定语义,并将验收引擎收紧为 fail-closed,防止零用例、零变更任务假绿。
|
|
25
|
+
|
|
26
|
+
### 修复
|
|
27
|
+
|
|
28
|
+
- ZCode 供应商分组选择器同时兼容 `group-provider` 与 3.11.2 `group-family`;模型流程改为直选优先、供应商分组展开兜底,并在失败信息中附带可见文本与 `data-testid` 候选。
|
|
29
|
+
- 添加新项目前收起残留工作区菜单,以最多三轮“收起—点击—验证”闭环消除首次点击被吞。
|
|
30
|
+
- 项目选择优先点击 composer 菜单的 `menuitemcheckbox`,旧版侧栏项仅作回退;项目名称按 NFKC、空白和大小写归一化。
|
|
31
|
+
- 项目绑定回读同时校验 composer 触发器文本与完整路径,未绑定占位词不再误判;绑定失败最多两轮幂等重试。
|
|
32
|
+
- ZCode/TraeWork 内置发现目录统一为 `{PROGRAMFILES}` / `{PROGRAMFILES(X86)}`;自定义 profile 的环境占位符按大小写不敏感方式展开,未知占位符保留原样。
|
|
33
|
+
- 非 optional 测试检查在退出码为 0 但输出显示零用例时改判失败。
|
|
34
|
+
- Git 项目默认要求相对动工前基线产生变更;纯问答/分析任务可在 `.tianshu-mcp/acceptance.json` 设置 `"requireChanges": false` 显式关闭。
|
|
35
|
+
|
|
36
|
+
### 测试
|
|
37
|
+
|
|
38
|
+
- 回归覆盖 ZCode 3.11.2 family 平铺模型、旧版分组兜底、testid 诊断、项目点击被吞、composer 绑定与回读重试,以及零用例/零变更门禁。
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## [0.3.2] — 2026-09-12
|
|
23
43
|
|
|
24
44
|
修复 issue #5 / #6:**MCP 任务模型与 Codex GUI 内 turn 的状态脱节**。
|
|
25
45
|
前者是"等用户确认时恒报 running"的完成判定死锁,后者是 `cancel_task` 只停 MCP 侧等待、
|
|
@@ -431,7 +451,8 @@ Codex 桌面端改为 **GUI 驱动**:新增 `codex-gui` adapter,通过 MSIX
|
|
|
431
451
|
|
|
432
452
|
---
|
|
433
453
|
|
|
434
|
-
[未发布]: https://github.com/lanlan0811/tianshu-mcp/compare/v0.3.
|
|
454
|
+
[未发布]: https://github.com/lanlan0811/tianshu-mcp/compare/v0.3.3...HEAD
|
|
455
|
+
[0.3.3]: https://github.com/lanlan0811/tianshu-mcp/compare/v0.3.2...v0.3.3
|
|
435
456
|
[0.3.2]: https://github.com/lanlan0811/tianshu-mcp/compare/v0.3.1...v0.3.2
|
|
436
457
|
[0.3.1]: https://github.com/lanlan0811/tianshu-mcp/compare/v0.3.0...v0.3.1
|
|
437
458
|
[0.3.0]: https://github.com/lanlan0811/tianshu-mcp/compare/v0.2.0...v0.3.0
|
package/dist/agents/builtin.js
CHANGED
|
@@ -73,8 +73,8 @@ export const BUILTIN_PROFILES = {
|
|
|
73
73
|
authNote: "复用本机 ZCode 登录态",
|
|
74
74
|
executableDiscovery: {
|
|
75
75
|
dirs: [
|
|
76
|
-
"{
|
|
77
|
-
"{
|
|
76
|
+
"{PROGRAMFILES}/ZCode",
|
|
77
|
+
"{PROGRAMFILES(X86)}/ZCode",
|
|
78
78
|
"{LOCALAPPDATA}/Programs/ZCode",
|
|
79
79
|
"{LOCALAPPDATA}/ZCode",
|
|
80
80
|
"/Applications/ZCode.app/Contents/MacOS",
|
|
@@ -129,8 +129,8 @@ export const BUILTIN_PROFILES = {
|
|
|
129
129
|
executableDiscovery: {
|
|
130
130
|
dirs: [
|
|
131
131
|
"D:/TRAE Work CN",
|
|
132
|
-
"{
|
|
133
|
-
"{
|
|
132
|
+
"{PROGRAMFILES}/TRAE WORK CN",
|
|
133
|
+
"{PROGRAMFILES(X86)}/TRAE WORK CN",
|
|
134
134
|
"{LOCALAPPDATA}/Programs/TRAE WORK CN",
|
|
135
135
|
"{LOCALAPPDATA}/TRAE WORK CN",
|
|
136
136
|
"{APPDATA}/TRAE SOLO CN",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CdpDisconnectedError, CdpUnavailableError } from "../traework/cdp/client.js";
|
|
2
2
|
import { type ZcodeSelectorKey } from "./selectors.js";
|
|
3
3
|
import type { ZcodePoll } from "./liveness.js";
|
|
4
|
-
import type
|
|
4
|
+
import { type ZcodeProjectItem } from "./project.js";
|
|
5
5
|
export { CdpDisconnectedError, CdpUnavailableError };
|
|
6
6
|
export declare function retryZcodeEvaluation<T>(operation: () => Promise<T>, sleep?: (ms: number) => Promise<void>): Promise<T>;
|
|
7
7
|
export interface ZcodeSessionItem {
|
|
@@ -14,6 +14,12 @@ export interface ZcodeQuestionAnswerResult {
|
|
|
14
14
|
available: string[];
|
|
15
15
|
error?: string;
|
|
16
16
|
}
|
|
17
|
+
export interface ZcodeClickExactResult {
|
|
18
|
+
clicked: boolean;
|
|
19
|
+
count: number;
|
|
20
|
+
available: string[];
|
|
21
|
+
testids?: string[];
|
|
22
|
+
}
|
|
17
23
|
export declare class ZcodeCdpClient {
|
|
18
24
|
private readonly selectors;
|
|
19
25
|
private readonly inner;
|
|
@@ -27,11 +33,7 @@ export declare class ZcodeCdpClient {
|
|
|
27
33
|
exists(key: ZcodeSelectorKey): Promise<boolean>;
|
|
28
34
|
text(key: ZcodeSelectorKey): Promise<string>;
|
|
29
35
|
click(key: ZcodeSelectorKey): Promise<boolean>;
|
|
30
|
-
clickExact(key: ZcodeSelectorKey, value: string): Promise<
|
|
31
|
-
clicked: boolean;
|
|
32
|
-
count: number;
|
|
33
|
-
available: string[];
|
|
34
|
-
}>;
|
|
36
|
+
clickExact(key: ZcodeSelectorKey, value: string): Promise<ZcodeClickExactResult>;
|
|
35
37
|
selection(key: ZcodeSelectorKey): Promise<{
|
|
36
38
|
display: string;
|
|
37
39
|
internal: string;
|
|
@@ -39,6 +41,10 @@ export declare class ZcodeCdpClient {
|
|
|
39
41
|
projects(): Promise<ZcodeProjectItem[]>;
|
|
40
42
|
clickProject(id: string | undefined, projectPath: string | undefined): Promise<boolean>;
|
|
41
43
|
boundProjectPath(): Promise<string>;
|
|
44
|
+
workspaceBinding(): Promise<{
|
|
45
|
+
triggerText: string;
|
|
46
|
+
projectPath: string;
|
|
47
|
+
}>;
|
|
42
48
|
session(): Promise<{
|
|
43
49
|
id?: string;
|
|
44
50
|
title?: string;
|
package/dist/agents/zcode/cdp.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TraeworkCdpClient, CdpDisconnectedError, CdpUnavailableError, } from "../traework/cdp/client.js";
|
|
2
2
|
import { candidateExpr } from "./selectors.js";
|
|
3
|
+
import { projectDisplayName } from "./project.js";
|
|
3
4
|
import { normalizeZcodeModelSelection } from "./model.js";
|
|
4
5
|
export { CdpDisconnectedError, CdpUnavailableError };
|
|
5
6
|
export async function retryZcodeEvaluation(operation, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
@@ -90,11 +91,21 @@ export class ZcodeCdpClient {
|
|
|
90
91
|
return true;
|
|
91
92
|
}
|
|
92
93
|
async clickExact(key, value) {
|
|
93
|
-
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(),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);
|
|
94
|
+
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}})()`);
|
|
94
95
|
if (!found.point)
|
|
95
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
clicked: false,
|
|
98
|
+
count: found.count,
|
|
99
|
+
available: found.available,
|
|
100
|
+
testids: found.testids,
|
|
101
|
+
};
|
|
96
102
|
await this.clickAt(found.point.x, found.point.y);
|
|
97
|
-
return {
|
|
103
|
+
return {
|
|
104
|
+
clicked: true,
|
|
105
|
+
count: found.count,
|
|
106
|
+
available: found.available,
|
|
107
|
+
testids: found.testids,
|
|
108
|
+
};
|
|
98
109
|
}
|
|
99
110
|
async selection(key) {
|
|
100
111
|
const raw = 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)continue;const data=e.closest('[data-model-id],[data-model],[data-value]')||e.querySelector('[data-model-id],[data-model],[data-value]')||e;return {display:(e.value||e.textContent||'').trim(),ariaLabel:(e.getAttribute('aria-label')||'').trim(),currentValue:(e.getAttribute('data-model-current-value')||'').trim(),legacyInternal:(data.getAttribute('data-model-id')||data.getAttribute('data-model')||data.getAttribute('data-value')||'').trim()}}return {display:''}})()`);
|
|
@@ -104,14 +115,18 @@ export class ZcodeCdpClient {
|
|
|
104
115
|
return this.evaluate(`(function(){const out=[];for(const s of ${candidateExpr("projectItem", this.selectors)})for(const e of document.querySelectorAll(s)){const testid=e.getAttribute('data-testid')||'';const testPath=testid.startsWith('workspace-item-')?testid.slice('workspace-item-'.length):undefined;const name=(e.getAttribute('data-project-name')||e.querySelector('[class*=name]')?.textContent||e.textContent||'').trim();const p=e.getAttribute('data-project-path')||e.querySelector('[data-project-path]')?.getAttribute('data-project-path')||testPath||e.getAttribute('title')||undefined;const id=e.getAttribute('data-project-id')||e.getAttribute('data-id')||testid||undefined;if(name)out.push({name,path:p,id})}return out})()`);
|
|
105
116
|
}
|
|
106
117
|
async clickProject(id, projectPath) {
|
|
107
|
-
const
|
|
118
|
+
const displayName = projectPath ? projectDisplayName(projectPath) : "";
|
|
119
|
+
const point = await this.evaluate(`(async function(){const norm=s=>(s||'').normalize('NFKC').trim().toLocaleLowerCase();const visible=e=>{const r=e.getBoundingClientRect();return r.width&&r.height&&r.bottom>0&&r.right>0&&r.top<innerHeight&&r.left<innerWidth};const targetName=norm(${JSON.stringify(displayName)});const triggers=[];for(const s of ${candidateExpr("projectTrigger", this.selectors)})for(const e of document.querySelectorAll(s))if(visible(e)&&!triggers.includes(e))triggers.push(e);const trigger=triggers.length===1?triggers[0]:null;let containers=[];const controlledId=trigger?.getAttribute('aria-controls')||'';if(controlledId){const controlled=document.getElementById(controlledId);if(controlled&&visible(controlled))containers.push(controlled)}if(trigger){let parent=trigger.parentElement;while(parent&&!containers.length){if(parent.querySelector('[role="menuitemcheckbox"]'))containers.push(parent);parent=parent.parentElement}}if(!containers.length)containers=[...document.querySelectorAll('[role="menu"]')].filter(e=>visible(e)&&e.querySelector('[role="menuitemcheckbox"]'));const checkboxMatches=[];for(const container of containers)for(const e of container.querySelectorAll('[role="menuitemcheckbox"]'))if(visible(e)&&norm(e.getAttribute('aria-label')||e.getAttribute('data-value')||e.textContent||'')===targetName&&!checkboxMatches.includes(e))checkboxMatches.push(e);let selected=checkboxMatches.length===1?checkboxMatches[0]:null;if(!selected){const matches=[];for(const s of ${candidateExpr("projectItem", this.selectors)})for(const e of document.querySelectorAll(s)){const testid=e.getAttribute('data-testid')||'';const itemId=e.getAttribute('data-project-id')||e.getAttribute('data-id')||testid;const p=e.getAttribute('data-project-path')||e.querySelector('[data-project-path]')?.getAttribute('data-project-path')||(testid.startsWith('workspace-item-')?testid.slice('workspace-item-'.length):'')||e.getAttribute('title')||'';if((${JSON.stringify(id ?? "")}&&itemId===${JSON.stringify(id ?? "")})||(${JSON.stringify(projectPath ?? "")}&&p===${JSON.stringify(projectPath ?? "")}))matches.push(e)}if(matches.length===1)selected=matches[0]}if(!selected)return null;selected.scrollIntoView({block:'center'});await new Promise(r=>setTimeout(r,50));const r=selected.getBoundingClientRect();return visible(selected)?{x:r.left+r.width/2,y:r.top+r.height/2}:null})()`);
|
|
108
120
|
if (!point)
|
|
109
121
|
return false;
|
|
110
122
|
await this.clickAt(point.x, point.y);
|
|
111
123
|
return true;
|
|
112
124
|
}
|
|
113
125
|
async boundProjectPath() {
|
|
114
|
-
return (
|
|
126
|
+
return (await this.workspaceBinding()).projectPath;
|
|
127
|
+
}
|
|
128
|
+
async workspaceBinding() {
|
|
129
|
+
return this.evaluate(`(function(){let projectPath='';for(const s of ${candidateExpr("projectPath", this.selectors)})for(const e of document.querySelectorAll(s)){const p=e.getAttribute('data-project-path')||e.getAttribute('title')||(e.textContent||'').trim();if(p){projectPath=p;break}}const triggers=[];for(const s of ${candidateExpr("projectTrigger", this.selectors)})for(const e of document.querySelectorAll(s)){const r=e.getBoundingClientRect();if(r.width&&r.height&&!triggers.includes(e))triggers.push(e)}const triggerText=triggers.length===1?(triggers[0].textContent||'').normalize('NFKC').trim():'';if(!projectPath&&triggerText){const matches=[...document.querySelectorAll('[data-testid^="workspace-item-"]')].filter(e=>(e.textContent||'').normalize('NFKC').trim()===triggerText);if(matches.length===1)projectPath=(matches[0].getAttribute('data-testid')||'').slice('workspace-item-'.length)}return {triggerText,projectPath:projectPath||window.__ZCODE_PROJECT_PATH__||''}})()`);
|
|
115
130
|
}
|
|
116
131
|
async session() {
|
|
117
132
|
return this.evaluate(`(function(){const stable=id=>id&&id!=='draft'?id:undefined;const idOf=e=>{if(!e)return undefined;const explicit=stable(e.getAttribute('data-session-id'));if(explicit)return explicit;const testid=e.getAttribute('data-testid')||'';return testid.startsWith('task-item-')?stable(testid.slice('task-item-'.length)):undefined};const selectors=['[data-testid^="v4-session-pane"][data-session-id]','[data-session-id][aria-current=true]','[data-session-id][aria-selected=true]','[data-testid^="task-item-"][aria-current=true]','[data-testid^="task-item-"][aria-selected=true]','[data-testid^="task-item-"][data-state=active]','[data-testid^="task-item-"][data-state=selected]'];let active=null;for(const s of selectors){active=[...document.querySelectorAll(s)].find(e=>{const r=e.getBoundingClientRect();return r.width&&r.height})||null;if(active)break}const id=idOf(active)||stable(history.state?.sessionId)||stable(history.state?.taskId)||undefined;const item=id?[...document.querySelectorAll('[data-testid^="task-item-"]')].find(e=>(e.getAttribute('data-testid')||'').slice('task-item-'.length)===id):null;return {id,title:id?(item?.getAttribute('title')||item?.textContent||active?.getAttribute('title')||'').trim().slice(0,240)||undefined:undefined}})()`);
|
|
@@ -3,6 +3,10 @@ export interface ZcodeProjectItem {
|
|
|
3
3
|
path?: string;
|
|
4
4
|
id?: string;
|
|
5
5
|
}
|
|
6
|
+
export declare const PROJECT_PLACEHOLDER_TEXTS: readonly ["选择项目", "select project", "select a project", "choose project"];
|
|
7
|
+
export declare function projectDisplayName(projectPath: string, platform?: NodeJS.Platform): string;
|
|
8
|
+
export declare function sameDisplayName(a: string, b: string): boolean;
|
|
9
|
+
export declare function isUnboundTriggerText(text: string): boolean;
|
|
6
10
|
export declare function normalizeProjectPath(value: string, platform?: NodeJS.Platform): string;
|
|
7
11
|
export declare function matchZcodeProject(items: ZcodeProjectItem[], target: string, platform?: NodeJS.Platform): {
|
|
8
12
|
item?: ZcodeProjectItem;
|
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
export const PROJECT_PLACEHOLDER_TEXTS = [
|
|
3
|
+
"选择项目",
|
|
4
|
+
"select project",
|
|
5
|
+
"select a project",
|
|
6
|
+
"choose project",
|
|
7
|
+
];
|
|
8
|
+
function normalizeDisplayName(value) {
|
|
9
|
+
return value.normalize("NFKC").trim().toLocaleLowerCase();
|
|
10
|
+
}
|
|
11
|
+
export function projectDisplayName(projectPath, platform = process.platform) {
|
|
12
|
+
const api = platform === "win32" ? path.win32 : path.posix;
|
|
13
|
+
return api.basename(projectPath).normalize("NFKC").trim();
|
|
14
|
+
}
|
|
15
|
+
export function sameDisplayName(a, b) {
|
|
16
|
+
return normalizeDisplayName(a) === normalizeDisplayName(b);
|
|
17
|
+
}
|
|
18
|
+
export function isUnboundTriggerText(text) {
|
|
19
|
+
const normalized = normalizeDisplayName(text);
|
|
20
|
+
const compact = normalized.replace(/\s+/g, "");
|
|
21
|
+
return PROJECT_PLACEHOLDER_TEXTS.some((placeholder) => {
|
|
22
|
+
const normalizedPlaceholder = normalizeDisplayName(placeholder);
|
|
23
|
+
return (normalized === normalizedPlaceholder || compact === normalizedPlaceholder.replace(/\s+/g, ""));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
2
26
|
export function normalizeProjectPath(value, platform = process.platform) {
|
|
3
27
|
const api = platform === "win32" ? path.win32 : path.posix;
|
|
4
28
|
let out = api.normalize(value).replace(/[\\/]+$/, "");
|
package/dist/agents/zcode/run.js
CHANGED
|
@@ -4,9 +4,9 @@ import { createHash } from "node:crypto";
|
|
|
4
4
|
import { mkdirp } from "../../util/fs.js";
|
|
5
5
|
import { parseZcodeModel, exactUiName } from "./model.js";
|
|
6
6
|
import { validateTaskReferences } from "./references.js";
|
|
7
|
-
import { matchZcodeProject, normalizeProjectPath } from "./project.js";
|
|
7
|
+
import { isUnboundTriggerText, matchZcodeProject, normalizeProjectPath, projectDisplayName, sameDisplayName, } from "./project.js";
|
|
8
8
|
import { judgeZcodePoll } from "./liveness.js";
|
|
9
|
-
import { ZcodeCdpClient, CdpDisconnectedError, CdpUnavailableError } from "./cdp.js";
|
|
9
|
+
import { ZcodeCdpClient, CdpDisconnectedError, CdpUnavailableError, } from "./cdp.js";
|
|
10
10
|
import { ensureZcodeInstance, listZcodeProcesses } from "./instance.js";
|
|
11
11
|
import { listOwnedDialogs, selectZcodeFolder } from "./dialog.js";
|
|
12
12
|
const DEFAULT_DEPS = {
|
|
@@ -71,9 +71,14 @@ function prompt(ctx, refs) {
|
|
|
71
71
|
return out;
|
|
72
72
|
}
|
|
73
73
|
async function waitBound(cdp, target, deps) {
|
|
74
|
+
const targetName = projectDisplayName(target);
|
|
74
75
|
for (let i = 0; i < 30; i++) {
|
|
75
|
-
const
|
|
76
|
-
if (
|
|
76
|
+
const binding = await cdp.workspaceBinding();
|
|
77
|
+
if ((binding.projectPath &&
|
|
78
|
+
normalizeProjectPath(binding.projectPath) === normalizeProjectPath(target)) ||
|
|
79
|
+
(binding.triggerText &&
|
|
80
|
+
!isUnboundTriggerText(binding.triggerText) &&
|
|
81
|
+
sameDisplayName(binding.triggerText, targetName)))
|
|
77
82
|
return true;
|
|
78
83
|
await deps.sleep(300);
|
|
79
84
|
}
|
|
@@ -107,9 +112,9 @@ async function connectStableZcode(ready, gui, deps, allowLoginPage = false) {
|
|
|
107
112
|
// eslint-disable-next-line no-await-in-loop
|
|
108
113
|
await deps.sleep(500);
|
|
109
114
|
}
|
|
110
|
-
throw
|
|
115
|
+
throw lastError instanceof Error
|
|
111
116
|
? lastError
|
|
112
|
-
: new Error(`ZCode CDP 未在 ${gui.launchTimeoutMs}ms 内恢复`)
|
|
117
|
+
: new Error(`ZCode CDP 未在 ${gui.launchTimeoutMs}ms 内恢复`);
|
|
113
118
|
}
|
|
114
119
|
async function clickExactWhenReady(cdp, key, value, deps) {
|
|
115
120
|
let last = { clicked: false, count: 0, available: [] };
|
|
@@ -216,6 +221,36 @@ export async function runZcodeTask(args) {
|
|
|
216
221
|
let session = ctx.resume ? activeSession : {};
|
|
217
222
|
let permission = ctx.resume?.permissionMode ?? gui.defaultPermissionMode ?? "完全访问";
|
|
218
223
|
let answeredQuestion = false;
|
|
224
|
+
const ensureProjectBound = async (initialItem) => {
|
|
225
|
+
if (!initialItem && (await waitBound(cdp, ctx.projectPath, deps)))
|
|
226
|
+
return { bound: true, ambiguous: false };
|
|
227
|
+
let item = initialItem;
|
|
228
|
+
for (let round = 0; round < 2; round++) {
|
|
229
|
+
if (!item || round > 0) {
|
|
230
|
+
// eslint-disable-next-line no-await-in-loop
|
|
231
|
+
await cdp.dismissMenus();
|
|
232
|
+
// eslint-disable-next-line no-await-in-loop
|
|
233
|
+
if (!(await cdp.click("projectTrigger")))
|
|
234
|
+
continue;
|
|
235
|
+
// eslint-disable-next-line no-await-in-loop
|
|
236
|
+
await deps.sleep(300);
|
|
237
|
+
// eslint-disable-next-line no-await-in-loop
|
|
238
|
+
const matched = matchZcodeProject(await cdp.projects(), ctx.projectPath);
|
|
239
|
+
if (matched.ambiguous)
|
|
240
|
+
return { bound: false, ambiguous: true };
|
|
241
|
+
item = matched.item;
|
|
242
|
+
}
|
|
243
|
+
if (!item)
|
|
244
|
+
continue;
|
|
245
|
+
// eslint-disable-next-line no-await-in-loop
|
|
246
|
+
const clicked = await cdp.clickProject(item.id, item.path);
|
|
247
|
+
// eslint-disable-next-line no-await-in-loop
|
|
248
|
+
if (clicked && (await waitBound(cdp, ctx.projectPath, deps)))
|
|
249
|
+
return { bound: true, ambiguous: false };
|
|
250
|
+
item = undefined;
|
|
251
|
+
}
|
|
252
|
+
return { bound: false, ambiguous: false };
|
|
253
|
+
};
|
|
219
254
|
if (ctx.resume?.kind === "continue" && ctx.resume.sendMessage) {
|
|
220
255
|
const pending = await cdp.poll();
|
|
221
256
|
if (pending.question) {
|
|
@@ -256,44 +291,34 @@ export async function runZcodeTask(args) {
|
|
|
256
291
|
endReason: "project_ambiguous",
|
|
257
292
|
});
|
|
258
293
|
if (matched.item) {
|
|
259
|
-
|
|
294
|
+
const binding = await ensureProjectBound(matched.item);
|
|
295
|
+
if (!binding.bound)
|
|
260
296
|
return result({
|
|
261
297
|
hardFailure: true,
|
|
262
|
-
error:
|
|
263
|
-
|
|
298
|
+
error: binding.ambiguous
|
|
299
|
+
? "项目绑定重试时目标项目无法唯一匹配"
|
|
300
|
+
: "ZCode 项目绑定两轮重试均未生效",
|
|
301
|
+
endReason: "project_mismatch",
|
|
264
302
|
});
|
|
265
|
-
if (!(await waitBound(cdp, ctx.projectPath, deps))) {
|
|
266
|
-
// ZCode may ignore a menu click while its workspace list is still
|
|
267
|
-
// animating. Retrying this pre-send, idempotent binding is safe.
|
|
268
|
-
if (!(await cdp.click("projectTrigger")))
|
|
269
|
-
return result({
|
|
270
|
-
hardFailure: true,
|
|
271
|
-
error: "项目首次绑定未生效,且无法重新打开项目列表",
|
|
272
|
-
endReason: "project_mismatch",
|
|
273
|
-
});
|
|
274
|
-
await deps.sleep(300);
|
|
275
|
-
const retried = matchZcodeProject(await cdp.projects(), ctx.projectPath);
|
|
276
|
-
if (retried.ambiguous ||
|
|
277
|
-
!retried.item ||
|
|
278
|
-
!(await cdp.clickProject(retried.item.id, retried.item.path)))
|
|
279
|
-
return result({
|
|
280
|
-
hardFailure: true,
|
|
281
|
-
error: "项目首次绑定未生效,重试时无法唯一选择目标项目",
|
|
282
|
-
endReason: "project_mismatch",
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
303
|
}
|
|
286
304
|
else {
|
|
287
305
|
const pids = deps.listProcesses().map((p) => p.pid);
|
|
288
306
|
const before = await deps.listDialogs(pids);
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
307
|
+
let folderOption = {
|
|
308
|
+
clicked: false,
|
|
309
|
+
count: 0,
|
|
310
|
+
available: [],
|
|
311
|
+
};
|
|
312
|
+
for (let round = 0; round < 3 && !folderOption.clicked; round++) {
|
|
313
|
+
// ZCode 3.11.2 的工作区下拉会吞掉第一次 outside-click。每轮均先收起残留菜单再验证新菜单。
|
|
314
|
+
// eslint-disable-next-line no-await-in-loop
|
|
315
|
+
await cdp.dismissMenus();
|
|
316
|
+
// eslint-disable-next-line no-await-in-loop
|
|
317
|
+
if (!(await cdp.click("addProject")))
|
|
318
|
+
continue;
|
|
319
|
+
// eslint-disable-next-line no-await-in-loop
|
|
320
|
+
folderOption = await clickAnyExactWhenReady(cdp, "chooseFolder", ["打开文件夹", "Open Folder"], deps);
|
|
321
|
+
}
|
|
297
322
|
if (!folderOption.clicked)
|
|
298
323
|
return result({
|
|
299
324
|
hardFailure: true,
|
|
@@ -326,10 +351,13 @@ export async function runZcodeTask(args) {
|
|
|
326
351
|
cdp = await connectStableZcode(ready, gui, deps);
|
|
327
352
|
logger.info("[zcode] 项目导入后的 ZCode CDP 页面已恢复");
|
|
328
353
|
}
|
|
329
|
-
|
|
354
|
+
const finalBinding = await ensureProjectBound();
|
|
355
|
+
if (!finalBinding.bound)
|
|
330
356
|
return result({
|
|
331
357
|
hardFailure: true,
|
|
332
|
-
error:
|
|
358
|
+
error: finalBinding.ambiguous
|
|
359
|
+
? "ZCode 项目绑定回读不一致,且重试时目标项目无法唯一匹配"
|
|
360
|
+
: "ZCode 项目绑定回读与 projectPath 不一致,两轮幂等重试均失败",
|
|
333
361
|
endReason: "project_mismatch",
|
|
334
362
|
});
|
|
335
363
|
logger.info(`[zcode] 项目绑定回读通过:${ctx.projectPath}`);
|
|
@@ -345,19 +373,18 @@ export async function runZcodeTask(args) {
|
|
|
345
373
|
endReason: "setup_failed",
|
|
346
374
|
});
|
|
347
375
|
await deps.sleep(250);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const model = await clickExactWhenReady(cdp, "modelOption", spec.model, deps);
|
|
376
|
+
let model = await clickExactWhenReady(cdp, "modelOption", spec.model, deps);
|
|
377
|
+
let provider = { clicked: false, count: 0, available: [] };
|
|
378
|
+
if (!model.clicked) {
|
|
379
|
+
provider = await clickExactWhenReady(cdp, "providerOption", spec.provider, deps);
|
|
380
|
+
if (provider.clicked)
|
|
381
|
+
await deps.sleep(250);
|
|
382
|
+
model = await clickExactWhenReady(cdp, "modelOption", spec.model, deps);
|
|
383
|
+
}
|
|
357
384
|
if (!model.clicked)
|
|
358
385
|
return result({
|
|
359
386
|
hardFailure: true,
|
|
360
|
-
error: `模型不存在或同名歧义:${spec.provider}/${spec.model}
|
|
387
|
+
error: `模型不存在或同名歧义:${spec.provider}/${spec.model}(模型匹配 ${model.count}${model.available?.length ? `;模型候选=${model.available.slice(0, 20).join("、")}` : ""}${model.testids?.length ? `;模型 testid=${model.testids.slice(0, 20).join("、")}` : ""};供应商匹配 ${provider.count}${provider.available?.length ? `;供应商候选=${provider.available.slice(0, 20).join("、")}` : ""}${provider.testids?.length ? `;供应商 testid=${provider.testids.slice(0, 20).join("、")}` : ""})`,
|
|
361
388
|
endReason: "model_unavailable",
|
|
362
389
|
});
|
|
363
390
|
await deps.sleep(300);
|
|
@@ -386,7 +413,7 @@ export async function runZcodeTask(args) {
|
|
|
386
413
|
if (!perm.clicked)
|
|
387
414
|
return result({
|
|
388
415
|
hardFailure: true,
|
|
389
|
-
error: `无法唯一选择权限模式:${permission}
|
|
416
|
+
error: `无法唯一选择权限模式:${permission}(匹配 ${perm.count}${perm.available?.length ? `;可见候选=${perm.available.slice(0, 20).join("、")}` : ""}${perm.testids?.length ? `;可见 testid=${perm.testids.slice(0, 20).join("、")}` : ""})`,
|
|
390
417
|
endReason: "permission_unknown",
|
|
391
418
|
});
|
|
392
419
|
await deps.sleep(250);
|
|
@@ -431,7 +458,8 @@ export async function runZcodeTask(args) {
|
|
|
431
458
|
let seenRunning = false;
|
|
432
459
|
let markedSession = undefined;
|
|
433
460
|
const confirmationAttempts = Math.ceil(Math.min(60_000, Math.max(5_000, ctx.taskTimeoutMs)) / 250);
|
|
434
|
-
for (let i = 0; i < confirmationAttempts &&
|
|
461
|
+
for (let i = 0; i < confirmationAttempts &&
|
|
462
|
+
!(seenMessage && (seenStateChange || seenRunning || markedSession)); i++) {
|
|
435
463
|
// eslint-disable-next-line no-await-in-loop
|
|
436
464
|
await deps.sleep(250);
|
|
437
465
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -113,15 +113,18 @@ export const ZCODE_SELECTORS = {
|
|
|
113
113
|
},
|
|
114
114
|
providerOption: {
|
|
115
115
|
primary: '[data-testid^="chat-model-select-group-provider:"]',
|
|
116
|
-
fallbacks: [
|
|
116
|
+
fallbacks: [
|
|
117
|
+
'[data-testid^="chat-model-select-group-family:"]',
|
|
118
|
+
'[role="option"][data-provider]',
|
|
119
|
+
],
|
|
117
120
|
verifiedVersion: "3.11.x",
|
|
118
|
-
note: "
|
|
121
|
+
note: "3.11.2 分组前缀漂移为 family;家族标签可能不可点,仅作兜底展开",
|
|
119
122
|
},
|
|
120
123
|
modelOption: {
|
|
121
124
|
primary: '[data-testid^="chat-model-select-item-"][role="menuitemradio"]',
|
|
122
|
-
fallbacks: ['[role="option"][data-model]'],
|
|
125
|
+
fallbacks: ['[data-testid^="chat-model-select-item-"]', '[role="option"][data-model]'],
|
|
123
126
|
verifiedVersion: "3.11.x",
|
|
124
|
-
note: "
|
|
127
|
+
note: "3.11.2 模型平铺于家族标签下,testid 形如 chat-model-select-item-custom:…",
|
|
125
128
|
},
|
|
126
129
|
modelValue: {
|
|
127
130
|
primary: '[data-testid="chat-model-select-trigger"]',
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -1237,6 +1237,7 @@ export declare const AcceptanceConfigSchema: z.ZodObject<{
|
|
|
1237
1237
|
timeoutMs?: number | undefined;
|
|
1238
1238
|
optional?: boolean | undefined;
|
|
1239
1239
|
}>, "many">>;
|
|
1240
|
+
requireChanges: z.ZodDefault<z.ZodBoolean>;
|
|
1240
1241
|
}, "strip", z.ZodTypeAny, {
|
|
1241
1242
|
checks: {
|
|
1242
1243
|
name: string;
|
|
@@ -1244,6 +1245,7 @@ export declare const AcceptanceConfigSchema: z.ZodObject<{
|
|
|
1244
1245
|
timeoutMs?: number | undefined;
|
|
1245
1246
|
optional?: boolean | undefined;
|
|
1246
1247
|
}[];
|
|
1248
|
+
requireChanges: boolean;
|
|
1247
1249
|
}, {
|
|
1248
1250
|
checks?: {
|
|
1249
1251
|
name: string;
|
|
@@ -1251,5 +1253,6 @@ export declare const AcceptanceConfigSchema: z.ZodObject<{
|
|
|
1251
1253
|
timeoutMs?: number | undefined;
|
|
1252
1254
|
optional?: boolean | undefined;
|
|
1253
1255
|
}[] | undefined;
|
|
1256
|
+
requireChanges?: boolean | undefined;
|
|
1254
1257
|
}>;
|
|
1255
1258
|
export type AcceptanceConfig = z.infer<typeof AcceptanceConfigSchema>;
|
package/dist/config/schema.js
CHANGED
|
@@ -255,4 +255,5 @@ export const ProjectsFileSchema = z.record(z.string().min(1), ProjectRecordSchem
|
|
|
255
255
|
/* ---------------- 项目内 .tianshu-mcp/acceptance.json ---------------- */
|
|
256
256
|
export const AcceptanceConfigSchema = z.object({
|
|
257
257
|
checks: z.array(AcceptanceCheckSchema).default([]),
|
|
258
|
+
requireChanges: z.boolean().default(true),
|
|
258
259
|
});
|
package/dist/util/path.js
CHANGED
|
@@ -56,11 +56,10 @@ export function expandEnvPath(tpl) {
|
|
|
56
56
|
SYSTEMDRIVE: process.env.SYSTEMDRIVE ?? process.env.SystemDrive,
|
|
57
57
|
XDG_DATA_HOME: process.env.XDG_DATA_HOME,
|
|
58
58
|
};
|
|
59
|
-
let out = tpl
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
59
|
+
let out = tpl.replace(/\{([^{}]+)\}/g, (placeholder, key) => {
|
|
60
|
+
const value = envMap[key.toUpperCase()];
|
|
61
|
+
return value || placeholder;
|
|
62
|
+
});
|
|
64
63
|
// Windows 下把模板里的正斜杠统一为平台分隔符(env 值本身已是平台分隔)
|
|
65
64
|
if (process.platform === "win32")
|
|
66
65
|
out = out.split("/").join("\\");
|
|
@@ -18,6 +18,7 @@ export interface VerifyRequest {
|
|
|
18
18
|
store: TaskStore;
|
|
19
19
|
logger: Logger;
|
|
20
20
|
}
|
|
21
|
+
export declare function detectZeroTestCases(output: string): boolean;
|
|
21
22
|
/** 从项目内容推导默认检查集(开发计划 §8.2) */
|
|
22
23
|
export declare function deriveDefaultChecks(projectPath: string): Promise<{
|
|
23
24
|
checks: AcceptanceCheckDef[];
|
|
@@ -12,6 +12,23 @@ import { analyzeChanges } from "./code-analysis.js";
|
|
|
12
12
|
import { captureBaseline, gitDiffCheckSince } from "./git-baseline.js";
|
|
13
13
|
import { nowIso } from "../util/id.js";
|
|
14
14
|
const SCRIPT_NAMES = ["typecheck", "lint", "test", "build"];
|
|
15
|
+
const ZERO_TEST_CASE_PATTERNS = [
|
|
16
|
+
/#\s+tests\s+0\b/i,
|
|
17
|
+
/\bno tests (?:were )?found\b/i,
|
|
18
|
+
/\bno tests ran\b/i,
|
|
19
|
+
/\b0 tests (?:ran|executed|found)\b/i,
|
|
20
|
+
];
|
|
21
|
+
export function detectZeroTestCases(output) {
|
|
22
|
+
return ZERO_TEST_CASE_PATTERNS.some((pattern) => pattern.test(output));
|
|
23
|
+
}
|
|
24
|
+
function isTestCheck(name, cmd) {
|
|
25
|
+
const normalizedName = name.toLocaleLowerCase();
|
|
26
|
+
const normalizedCmd = cmd.join(" ").toLocaleLowerCase();
|
|
27
|
+
return (normalizedName.includes("test") ||
|
|
28
|
+
/\bnode\s+--test\b/.test(normalizedCmd) ||
|
|
29
|
+
/\bnpm\s+run\s+test(?:\b|:)/.test(normalizedCmd) ||
|
|
30
|
+
/\bpytest\b|\bgo\s+test\b|\bcargo\s+test\b|\bvitest\b|\bjest\b|\bmocha\b/.test(normalizedCmd));
|
|
31
|
+
}
|
|
15
32
|
/** 从项目内容推导默认检查集(开发计划 §8.2) */
|
|
16
33
|
export async function deriveDefaultChecks(projectPath) {
|
|
17
34
|
const checks = [];
|
|
@@ -64,12 +81,13 @@ export class AcceptanceEngine {
|
|
|
64
81
|
const out = [];
|
|
65
82
|
const mode = req.checksMode ?? "append";
|
|
66
83
|
const hasExtra = !!req.extraChecks?.length;
|
|
84
|
+
const inProject = await this.readProjectAcceptance(req.projectPath);
|
|
85
|
+
const requireChanges = inProject?.requireChanges ?? true;
|
|
67
86
|
// 先取基础集(项目/默认),除非 replace 模式只用 extraChecks
|
|
68
87
|
if (!(mode === "replace" && hasExtra)) {
|
|
69
|
-
const inProject = await this.readProjectAcceptance(req.projectPath);
|
|
70
88
|
if (inProject) {
|
|
71
|
-
notes.push(`使用项目内验收配置 <project>/.tianshu-mcp/acceptance.json(${inProject.length} 项)。`);
|
|
72
|
-
out.push(...inProject);
|
|
89
|
+
notes.push(`使用项目内验收配置 <project>/.tianshu-mcp/acceptance.json(${inProject.checks.length} 项)。`);
|
|
90
|
+
out.push(...inProject.checks);
|
|
73
91
|
}
|
|
74
92
|
else if (req.projectVerify?.length) {
|
|
75
93
|
notes.push(`使用 server 数据目录 projects.json 补录的验收配置(${req.projectVerify.length} 项)。`);
|
|
@@ -92,7 +110,7 @@ export class AcceptanceEngine {
|
|
|
92
110
|
}
|
|
93
111
|
out.push(...(req.extraChecks ?? []));
|
|
94
112
|
}
|
|
95
|
-
return { checks: out, notes };
|
|
113
|
+
return { checks: out, notes, requireChanges };
|
|
96
114
|
}
|
|
97
115
|
async readProjectAcceptance(projectPath) {
|
|
98
116
|
const p = path.join(projectPath, ".tianshu-mcp", "acceptance.json");
|
|
@@ -104,7 +122,11 @@ export class AcceptanceEngine {
|
|
|
104
122
|
this.logger.warn(`项目 ${projectPath} 的 acceptance.json 解析失败: ${r.error.message}`);
|
|
105
123
|
return null;
|
|
106
124
|
}
|
|
107
|
-
|
|
125
|
+
const config = r.data;
|
|
126
|
+
return {
|
|
127
|
+
checks: config.checks.map((c) => toAcceptanceDef(c)),
|
|
128
|
+
requireChanges: config.requireChanges,
|
|
129
|
+
};
|
|
108
130
|
}
|
|
109
131
|
/** 执行一轮完整验收。返回 report + 是否 pass。 */
|
|
110
132
|
async runVerify(req) {
|
|
@@ -114,8 +136,9 @@ export class AcceptanceEngine {
|
|
|
114
136
|
const baseline = req.baseline ?? (await captureBaseline(req.projectPath));
|
|
115
137
|
if (req.baseline)
|
|
116
138
|
this.logger.debug(`使用 run_task 动工前基线(HEAD=${baseline.head ?? "n/a"})`);
|
|
117
|
-
const { checks: rawChecks, notes } = await this.resolveChecks(req);
|
|
139
|
+
const { checks: rawChecks, notes, requireChanges } = await this.resolveChecks(req);
|
|
118
140
|
const checks = [];
|
|
141
|
+
const zeroTestChecks = [];
|
|
119
142
|
// 内置 git diff --check(相对动工前基线;非 git 仓库跳过并标注)
|
|
120
143
|
if (baseline.isRepo) {
|
|
121
144
|
const r = await gitDiffCheckSince(req.projectPath, baseline.head);
|
|
@@ -152,6 +175,16 @@ export class AcceptanceEngine {
|
|
|
152
175
|
logFile: verifyLog,
|
|
153
176
|
env: {},
|
|
154
177
|
});
|
|
178
|
+
if (!c.optional &&
|
|
179
|
+
res.exitCode === 0 &&
|
|
180
|
+
res.passed &&
|
|
181
|
+
isTestCheck(c.name, argv) &&
|
|
182
|
+
detectZeroTestCases(res.outputTail)) {
|
|
183
|
+
res.passed = false;
|
|
184
|
+
res.outputTail = `${res.outputTail}\n[fail-closed] 命令退出码 0 但未执行任何测试用例`.slice(-4000);
|
|
185
|
+
res.reason = "fail-closed:命令退出码 0 但未执行任何测试用例";
|
|
186
|
+
zeroTestChecks.push(c.name);
|
|
187
|
+
}
|
|
155
188
|
if (c.optional)
|
|
156
189
|
res.optional = true;
|
|
157
190
|
checks.push(res);
|
|
@@ -164,6 +197,30 @@ export class AcceptanceEngine {
|
|
|
164
197
|
});
|
|
165
198
|
for (const n of notes)
|
|
166
199
|
analysis.notes.push(n);
|
|
200
|
+
const hasChanges = analysis.changedFiles.length > 0 ||
|
|
201
|
+
analysis.untrackedFiles.length > 0 ||
|
|
202
|
+
analysis.diffstat.totalAdd > 0 ||
|
|
203
|
+
analysis.diffstat.totalDel > 0;
|
|
204
|
+
if (baseline.isRepo && !hasChanges) {
|
|
205
|
+
if (requireChanges) {
|
|
206
|
+
checks.push({
|
|
207
|
+
name: "no-changes",
|
|
208
|
+
cmd: "requireChanges 门禁",
|
|
209
|
+
passed: false,
|
|
210
|
+
durationMs: 0,
|
|
211
|
+
exitCode: null,
|
|
212
|
+
outputTail: "fail-closed:相对动工前基线零文件变更",
|
|
213
|
+
timeout: false,
|
|
214
|
+
reason: "requireChanges 默认开启,但未检测到任务产出",
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
analysis.notes.push("requireChanges=false:相对动工前基线零变更,仅提示不拦截。");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else if (!baseline.isRepo && requireChanges) {
|
|
222
|
+
analysis.notes.push("requireChanges=true,但项目不是 git 仓库,零变更门禁已跳过。");
|
|
223
|
+
}
|
|
167
224
|
// optional:true 的失败只记 warning,不使本轮 verdict 失败(R4)
|
|
168
225
|
const failed = checks.filter((c) => !c.passed && !c.skipped && !c.optional);
|
|
169
226
|
const optFailed = checks.filter((c) => !c.passed && !c.skipped && c.optional);
|
|
@@ -173,6 +230,12 @@ export class AcceptanceEngine {
|
|
|
173
230
|
if (failed.length) {
|
|
174
231
|
summaryBits.push(`未通过检查: ${failed.map((c) => c.name).join(", ")}`);
|
|
175
232
|
}
|
|
233
|
+
if (zeroTestChecks.length) {
|
|
234
|
+
summaryBits.push(`测试命令零用例: ${zeroTestChecks.join(", ")}`);
|
|
235
|
+
}
|
|
236
|
+
if (failed.some((check) => check.name === "no-changes")) {
|
|
237
|
+
summaryBits.push("相对动工前基线未检测到文件变更");
|
|
238
|
+
}
|
|
176
239
|
if (optFailed.length) {
|
|
177
240
|
summaryBits.push(`optional 检查未通过(不影响结论): ${optFailed.map((c) => c.name).join(", ")}`);
|
|
178
241
|
}
|