tianshu-mcp 0.6.2 → 0.6.4

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 CHANGED
@@ -8,6 +8,56 @@ Chinese version: [CHANGELOG.md](CHANGELOG.md)
8
8
 
9
9
  ---
10
10
 
11
+ ## [0.6.4] - 2026-09-24
12
+
13
+ ### Added
14
+
15
+ - **Structured repair directives `repairDirectives`** ([issue #19](https://github.com/lanlan0811/tianshu-mcp/issues/19)): failed rounds parse acceptance failure reasons into **directly executable actions** (`file? / line? / issue / action / source`) carried in both the repair plan and the rework message, sparing the agent the cost of locating "which line has the type mismatch, which file has a TODO" in a full narrative report. See [structured repair directives](docs/repair-directives.en.md).
16
+ - **Two built-in extraction sources**: `typecheck` (parses pretty / plain tsc errors out of failed typecheck checks' output tails; absolute paths normalized to project-relative POSIX; duplicates collapsed) and `diffstat` (oversized single-file changes, modified lockfiles, and line-level counts for TODO / debug output / secret-like patterns).
17
+ - **New optional `repairHint` on `rework_task`** (free-form string, max 4000 chars): the caller supplies its own structured repair hint, rendered in the next round's task book as a `【结构化修复提示】` block placed **before** `feedback`.
18
+
19
+ ### Changed
20
+
21
+ - **`report-<round>.md` gained a `## Structured repair directives` section**; `report-<round>.json` gained a `repairDirectives` field (**failed rounds only**).
22
+ - **Both repair-plan variants (generic `rework-*.md` and Codex `codex-fix-r*.md`) gained a `## 2.5 Structured repair directives` section**, placed between section 2 (failures) and section 3 (passing checks).
23
+ - `LOCKFILE_PATTERN` is now exported from `code-analysis.ts` so the analysis warnings and the extractor **share one list**, preventing drift between two copies.
24
+
25
+ ### Compatibility
26
+
27
+ - **No tool contract, data model or MCP annotation changes.** `repairDirectives` is a new optional field inside the report, which readers may simply ignore; when `repairHint` is omitted, `rework_task` behaves exactly as in v0.6.3.
28
+ - Extraction runs **only on failed acceptance rounds**; passing rounds do not produce the field (no report bloat).
29
+
30
+ ### Notes (disclosed honestly)
31
+
32
+ - **Explicit fallback when extraction fails**: a non-empty `fallbackReason` means renderers state "unavailable, falling back to the full report" and tell the agent to return to the full failure output — a **silent gap is not allowed**. An exception from a single source is swallowed and recorded in the reason while other sources keep working — the extractors never throw.
33
+ - **No extraction for test-class failures**: test-framework output has no stable file/line; parsing it anyway would produce **wrong** locations, which is worse than producing none.
34
+ - **`diffstat`'s line-level signals never fake a location**: `signals.ts` only counts and has no stable file or line, so those directives omit the `file` field.
35
+ - **Known limitation**: `outputTail` is truncated to the last 4000 characters, so a large project only yields tail type errors and the rest is covered by the fallback — a deliberately accepted trade-off.
36
+
37
+ ## [0.6.3] - 2026-09-24
38
+
39
+ ### Added
40
+
41
+ - **Fine-grained event stream** ([issue #18](https://github.com/lanlan0811/tianshu-mcp/issues/18)): adapters can proactively report semantic events at key nodes, and `query_task` returns the most recent N of them, so long tasks can be told apart as "working normally" versus "stuck on a dialog waiting for a human". Five event kinds: `task_dispatched` / `confirmation_dialog_detected` / `awaiting_user_authorization` / `file_modification_started` / `rework_triggered`. See the [event stream doc](docs/event-stream.en.md).
42
+ - **New optional `query_task` input `eventLimit`** (integer 1..50, **default 10**): events appear both in the meta block's `recentEvents` array and in a "recent events" section of the text area.
43
+ - **Two built-in GUI adapters actually report events**: codex and traework (four emission points each); `rework_triggered` is emitted engine-side (automatic rework `mode:"auto"`, manual `rework_task` `mode:"manual"`).
44
+
45
+ ### Changed
46
+
47
+ - **Manual `rework_task` now emits a typed `rework_triggered` event instead of an anonymous `note`** (visible in `task.jsonl`). The semantics and purpose of the existing `note` event are unchanged; `progressSummary` / `lastRunSignal` are still carried by `note`.
48
+ - **The `TaskEventName` union gained five members**, sourced from `AGENT_EVENT_NAMES` so the vocabulary cannot drift between two places.
49
+
50
+ ### Compatibility
51
+
52
+ - **No tool contract, data model or MCP annotation changes.** `recentEvents` is a new optional field: adapters that do not implement event reporting (including all CLI adapters) return an empty array with no event section in the text area, and **every other field is exactly as in v0.6.2**.
53
+ - Events are written into the existing `task.jsonl` (**no parallel event file is created**); the read side only reads a 64 KiB tail window, so memory use is decoupled from total file size.
54
+
55
+ ### Notes (disclosed honestly)
56
+
57
+ - **`file_modification_started` is a heuristic.** The codex / traework adapters do not observe the filesystem directly; they can only infer that execution started from the UI's "running" signal (stop button). Its detail always reads "stop button appeared, execution started (files may be modified)" and **does not claim files were actually changed**. For hard evidence of file changes, read `changedFiles` / `diffstat` from the acceptance report.
58
+ - **Event reporting is an optional capability.** The hook lives on `AgentRunOptions.onEvent`, not the agent profile (`agent-profiles.json` is plain JSON and cannot hold a function); adapters that don't implement it need not change a single byte. Adapters report through `makeEmitter` — a no-op when no hook is provided, swallowing reporting exceptions so that **a failed report never affects the task itself**.
59
+ - **Events are not delivery-guaranteed.** This is an observability capability, not a delivery guarantee; `query_task` reflects only "the last event that was persisted".
60
+
11
61
  ## [0.6.2] - 2026-09-23
12
62
 
13
63
  ### Fixed
package/CHANGELOG.md CHANGED
@@ -7,6 +7,56 @@
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.6.4] - 2026-09-24
11
+
12
+ ### 新增
13
+
14
+ - **结构化修复指令 `repairDirectives`**([issue #19](https://github.com/lanlan0811/tianshu-mcp/issues/19)):失败轮次把验收失败原因解析为**可直接执行的动作**(`file? / line? / issue / action / source`),随返修计划、返修消息一起喂给 agent,省去它从整篇报告里定位「哪一行类型不匹配、哪个文件有 TODO」的开销。详见 [结构化修复指令](docs/repair-directives.md)。
15
+ - **两个内置提取来源**:`typecheck`(解析失败类型检查项输出尾部的 tsc pretty / plain 两式报错,绝对路径归一化为项目相对 posix 路径,同处报错去重)与 `diffstat`(超大单文件改动、被改动的锁文件、TODO / 调试输出 / 疑似密钥的行级计数)。
16
+ - **`rework_task` 新增可选 `repairHint`**(自由字符串,上限 4000 字符):调用方自带结构化修复提示,在下一轮任务书的 `【结构化修复提示】` 块中**排在 `feedback` 之前**。
17
+
18
+ ### 变更
19
+
20
+ - **`report-<round>.md` 新增 `## 结构化修复指令` 小节**;`report-<round>.json` 新增 `repairDirectives` 字段(**仅失败轮次**)。
21
+ - **两块返修计划(通用 `rework-*.md` 与 Codex `codex-fix-r*.md`)新增 `## 2.5 结构化修复指令` 小节**,位于第 2 节(失败项)与第 3 节(通过项)之间。
22
+ - `LOCKFILE_PATTERN` 由 `code-analysis.ts` 导出,供分析告警与提取器**共用一份清单**,避免两处漂移。
23
+
24
+ ### 兼容性
25
+
26
+ - **无工具契约、数据模型或 MCP 注解变更**。`repairDirectives` 是报告内的新增可选字段,读取方按缺省忽略即可;`repairHint` 不传时 `rework_task` 行为与 v0.6.3 完全一致。
27
+ - 提取**只在验收失败的轮次**执行;通过的轮次不产出该字段(不徒增报告体积)。
28
+
29
+ ### 说明(如实披露)
30
+
31
+ - **提取不到时显式回退**:`fallbackReason` 非空 ⇒ 渲染方写明「不可用,回退完整报告」并要求 agent 回到完整失败输出,**不允许静默留空**。单个来源抛错会被吞掉并记入原因,其余来源继续工作 —— 提取器永不抛错。
32
+ - **测试类失败不做提取**:测试框架输出没有稳定的文件/行号,强行解析会产出**错误**定位,比不给更糟。
33
+ - **`diffstat` 的行级信号不伪造位置**:`signals.ts` 只做计数、无稳定文件与行号,故对应指令省略 `file` 字段。
34
+ - **已知限制**:`outputTail` 被截断到最后 4000 字符,大型项目只能提取到尾部类型错误,其余靠回退兜底 —— 有意接受的取舍。
35
+
36
+ ## [0.6.3] - 2026-09-24
37
+
38
+ ### 新增
39
+
40
+ - **细粒度事件流**([issue #18](https://github.com/lanlan0811/tianshu-mcp/issues/18)):适配器可在关键节点主动上报语义事件,`query_task` 回传最近 N 条,长任务下可区分「正常执行」与「卡在弹窗等人」。词表 5 类:`task_dispatched` / `confirmation_dialog_detected` / `awaiting_user_authorization` / `file_modification_started` / `rework_triggered`。详见 [事件流文档](docs/event-stream.md)。
41
+ - **`query_task` 新增可选入参 `eventLimit`**(整数 1..50,**缺省 10**):事件同时出现在 meta 块的 `recentEvents` 数组与文本区的「最近事件」段落。
42
+ - **两个内置 GUI 适配器落地上报**:codex 与 traework(各 4 个发射点);`rework_triggered` 由引擎侧统一上报(自动返修 `mode:"auto"`、手动 `rework_task` `mode:"manual"`)。
43
+
44
+ ### 变更
45
+
46
+ - **手动 `rework_task` 的事件由匿名 `note` 改为类型化 `rework_triggered`**(`task.jsonl` 可见)。既有 `note` 事件的语义与用途不变,`progressSummary` / `lastRunSignal` 仍由 `note` 承载。
47
+ - **`TaskEventName` 联合类型新增 5 个成员**,与 `AGENT_EVENT_NAMES` 同源(避免两处词表漂移)。
48
+
49
+ ### 兼容性
50
+
51
+ - **无工具契约、数据模型或 MCP 注解变更**。`recentEvents` 是新增可选字段:未实现事件上报的适配器(含全部 CLI 适配器)返回空数组、文本区不出现事件段落,**其余字段与 v0.6.2 完全一致**。
52
+ - 事件写入既有的 `task.jsonl`(**不新建并行事件文件**);读取侧只读尾部 64 KiB 窗口,内存占用与文件总大小解耦。
53
+
54
+ ### 说明(如实披露)
55
+
56
+ - **`file_modification_started` 是启发式推断**:codex / traework 适配器并不直接观测文件系统,只能从界面「运行中」信号(停止按钮)推断执行已开始。其 detail 一律写「停止按钮出现,开始执行(可能开始改动文件)」,**不声称文件确已改动**;确切的文件改动证据请看验收报告的 `changedFiles` / `diffstat`。
57
+ - **事件上报是可选能力**:钩子挂在 `AgentRunOptions.onEvent` 而非 agent profile(`agent-profiles.json` 是纯 JSON,装不下函数);未实现的适配器一个字节都不用改。适配器侧统一经 `makeEmitter` 上报 —— 未提供钩子时空操作,且吞掉上报异常,**上报失败绝不影响任务本体**。
58
+ - **事件不保证送达**:属观测能力而非交付保证;`query_task` 只反映「最后一次落盘的事件」。
59
+
10
60
  ## [0.6.2] - 2026-09-23
11
61
 
12
62
  ### 修复
package/README.en.md CHANGED
@@ -39,6 +39,8 @@ Tianshu plays the role of the overall commander; this MCP server is the **schedu
39
39
 
40
40
  - **11 MCP tools**: `run_task / continue_task / query_task / list_tasks / get_task_report / cancel_task / verify_task / rework_task / get_profiles`, plus `prepare_visual_baseline / approve_visual_baseline` for visual acceptance
41
41
  - **Async contract**: `run_task` returns a `taskId` immediately; long-running work is polled via `query_task` (never blocks `tools/call`).
42
+ - **Long-task observability (issue #18)**: adapters report fine-grained events at key nodes (`task_dispatched` / `confirmation_dialog_detected` / `awaiting_user_authorization` / `file_modification_started` / `rework_triggered`), and `query_task` returns the most recent N via `eventLimit` (default 10) — **so you can tell "the agent is working" apart from "stuck on a dialog waiting for a human"**. Event reporting is an optional capability: adapters that don't implement it behave unchanged. codex and traework report in this version. See [event stream](docs/event-stream.en.md).
43
+ - **Structured repair directives (issue #19)**: failed rounds parse the reasons into **directly executable actions** (`file:line / issue / action`) carried in both the repair plan and the rework message, sparing the agent the cost of locating problems in a full narrative report; when extraction is unavailable it **explicitly falls back** to the full report (never a silent gap). `rework_task` also accepts an optional `repairHint`. See [structured repair directives](docs/repair-directives.en.md).
42
44
  - **Objective acceptance**: automated command checks (typecheck/lint/test/build — skipped when absent, plus tech-stack derivation) + programmatic code analysis (changed-file list / diffstat / suspicious signals such as TODO, debugger, secret-like patterns), all relative to a **git baseline**; never auto-commits or stashes. The acceptance engine is **fail-closed**: a test check fails when its output reports zero executed tests even if the exit code is 0; git projects must produce changes relative to the pre-work baseline by default (pure analysis tasks can opt out with `"requireChanges": false` in `.tianshu-mcp/acceptance.json`).
43
45
  - **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).
44
46
  - **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.
@@ -188,12 +190,12 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
188
190
  |---|---|---|
189
191
  | `run_task` | write + approval | Dispatch work (optional auto-verify / auto-rework); returns `taskId` asynchronously. Optional `idempotencyKey`: a retry with the same key returns the original `taskId` instead of creating a task |
190
192
  | `continue_task` | write + approval | Resume the session behind `needs_user` (ZCode resumes the recorded session; Codex re-observes for `user_confirmation` / re-dispatches for `login_required`; Kimi Code resumes the recorded session and distinguishes question answering / re-observation / full re-dispatch) |
191
- | `query_task` | read | Poll status / progress / log tail |
193
+ | `query_task` | read | Poll status / progress / log tail / recent fine-grained events. The optional `eventLimit` (1..50, default 10) controls how many `recentEvents` the meta block carries, so long tasks can be told apart as "working normally" versus "stuck on a dialog waiting for a human" |
192
194
  | `list_tasks` | read | Filtered history of tasks |
193
195
  | `get_task_report` | read | Full text of a verification round's report (`report.md`) |
194
196
  | `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
197
  | `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
- | `rework_task` | write + approval | Manual rework (feed the failure report back to the same agent) |
198
+ | `rework_task` | write + approval | Manual rework (feed the failure report back to the same agent). Optional `repairHint` (≤4000 chars) supplies a structured repair hint rendered as a block before `feedback` |
197
199
  | `get_profiles` | read | Inspect agent adapters and executable discovery results |
198
200
  | `prepare_visual_baseline` | write + approval | Capture or import reference images into a reviewable candidate with a digest |
199
201
  | `approve_visual_baseline` | write + approval | Validate the reviewed digest and write the baseline and approval record |
package/README.md CHANGED
@@ -39,9 +39,11 @@
39
39
 
40
40
  - **11 个 MCP 工具**:`run_task / continue_task / query_task / list_tasks / get_task_report / cancel_task / verify_task / rework_task / get_profiles`,外加视觉验收的 `prepare_visual_baseline / approve_visual_baseline`
41
41
  - **异步契约**:`run_task` 秒回 `taskId`,长任务用 `query_task` 轮询(长任务不卡 `tools/call`)。
42
+ - **长任务可观测(issue #18)**:适配器在关键节点上报细粒度事件(`task_dispatched` / `confirmation_dialog_detected` / `awaiting_user_authorization` / `file_modification_started` / `rework_triggered`),`query_task` 经 `eventLimit`(默认 10)回传最近 N 条——**能区分「agent 正在干活」与「卡在弹窗等人工介入」**。事件上报是可选能力:未实现的适配器行为不变。本版 codex 与 traework 已落地上报。详见 [事件流](docs/event-stream.md)。
42
43
  - **客观验收**:自动命令检查(typecheck/lint/test/build,缺则跳过 + 技术栈推导)+ 程序化代码分析(变更清单/diffstat/TODO·debugger·密钥形态等可疑标记),全部相对 **git 基线**,不自动 commit/stash。验收引擎 **fail-closed**:测试命令退出码为 0 但输出显示零用例时判失败;git 项目默认要求相对动工前基线产生变更(纯分析任务可在 `.tianshu-mcp/acceptance.json` 设 `"requireChanges": false` 显式关闭)。
43
44
  - **验收并行度**:命令检查默认**有界并行**(`verifyConcurrency`,默认 2、范围 1–4)。检查项之间有顺序依赖时(后续检查读取 build 产物、带 `--fix`、共享缓存目录)请设 `1` 完全退化为串行;项目级 `.tianshu-mcp/acceptance.json` 可覆盖,server 级在 `config.json`。报告与日志格式不变(结果按声明顺序返回)。
44
45
  - **失败返修闭环**:自动返修(`autoFixRounds`)+ 手动 `rework_task`;验收失败时自动生成修复计划文件并回填给 agent;轮次用尽 → `needs_attention` 等天枢裁决。
46
+ - **结构化修复指令(issue #19)**:失败轮次会把原因解析为**可直接执行的动作**(`文件:行 / 问题 / 做什么`),随返修计划与返修消息一起喂给 agent,省去它从整篇报告里定位的开销;提取不到时**显式回退**到完整报告(不静默留空)。`rework_task` 另可选 `repairHint` 自带提示。详见 [结构化修复指令](docs/repair-directives.md)。
45
47
  - **执行面**:`driver: "gui"` 由显式 adapter 驱动桌面 UI(Codex / TraeWork / ZCode / Kimi Code 各自使用隔离的 CDP 流程);`driver: "spawn"` 走外部 CLI 子进程。
46
48
  - **无项目派发(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)。
47
49
  - **幂等重试(issue #15)**:`run_task` / `verify_task` 接受可选 `idempotencyKey`——同一 key 在 TTL(默认 24h)内的重试**不会**重复派单(恒返回原 `taskId` 与当前状态)或重复跑验收(执行中返回进行中提示,已完成直接返回既有报告);同键异参 fail-closed 报错。映射落盘于 `<数据目录>/idempotency.json`,跨 server 重启仍生效。详见 [v0.5.10 发布说明](<docs/release-v0.5.10.md>)。
@@ -184,12 +186,12 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
184
186
  |---|---|---|
185
187
  | `run_task` | write + 审批 | 派活(可带自动验收/自动返修),异步返回 `taskId`;可选 `idempotencyKey`:同键重试恒返回原 `taskId`,不新建任务 |
186
188
  | `continue_task` | write + 审批 | 恢复 `needs_user` 的原会话(ZCode 恢复原会话;Codex 按 `user_confirmation` 重新观察 / `login_required` 重派;Kimi Code 恢复原会话并区分提问续答 / 重新观察 / 补发任务书) |
187
- | `query_task` | read | 轮询状态 / 进度 / 日志尾 |
189
+ | `query_task` | read | 轮询状态 / 进度 / 日志尾 / 最近细粒度事件。可选 `eventLimit`(1..50,默认 10)控制 meta 的 `recentEvents` 条数,长任务下可区分「正常执行」与「卡在弹窗等人」 |
188
190
  | `list_tasks` | read | 历史任务过滤列表 |
189
191
  | `get_task_report` | read | 某轮验收报告全文(`report.md`) |
190
192
  | `cancel_task` | write + 审批 | 取消运行中任务:CLI agent kill 进程树;GUI agent 经 CDP 点击停止并在 `gui.cancelWaitMs`(默认 15s)内有界等待 GUI 空闲,未确认停止时终态明示。对已终态的 GUI 任务,本调用兼任人工确认入口——核实窗口无残留运行后调用可清除 `guiStopUnconfirmed` 待确认标记 |
191
193
  | `verify_task` | execute(不改源码,免审批) | 对任务/项目路径做一次验收。能力归 `execute`:会跑项目配置命令、可能产生构建产物,故 MCP `readOnlyHint` 为 `false`——但**不改源码、仍免审批**。可选 `idempotencyKey`:同键重试不重跑(执行中返回进行中提示,已完成返回既有报告) |
192
- | `rework_task` | write + 审批 | 手动返修(把失败报告喂回同一 agent) |
194
+ | `rework_task` | write + 审批 | 手动返修(把失败报告喂回同一 agent)。可选 `repairHint`(≤4000 字符)自带结构化修复提示,以【结构化修复提示】块置于 `feedback` 之前 |
193
195
  | `get_profiles` | read | 查看 agent 适配与可执行探测结果 |
194
196
  | `prepare_visual_baseline` | write + 审批 | 截图或导入参考图,生成待审阅候选和摘要 |
195
197
  | `approve_visual_baseline` | write + 审批 | 用户审阅后校验摘要并写入基准与审批记录 |
@@ -0,0 +1,50 @@
1
+ /**
2
+ * agent 细粒度事件词表(issue #18)。
3
+ *
4
+ * 事件上报是**可选能力**:适配器实现 `AgentRunOptions.onEvent` 即在关键节点主动上报,
5
+ * 未实现的适配器一个字节都不用改(调用侧一律走 `opts.onEvent?.(...)` 可选链)。
6
+ *
7
+ * 本模块刻意**零依赖**:`adapter.ts`、`tasks/task.ts`、`tasks/task-store.ts` 都要引用它,
8
+ * 若它反向引用这些模块会形成循环。因此这里只有类型与常量,不 import 任何东西。
9
+ *
10
+ * 与既有 `note` 事件的关系:`note` 仍是进度/审计通道(承载 progressSummary、
11
+ * lastRunSignal 等自由文本),本词表只表达**语义化节点**,两者同写 task.jsonl、共用时序。
12
+ */
13
+ export const AGENT_EVENT_NAMES = [
14
+ /** 指令已确认送达 agent(进入其执行队列) */
15
+ "task_dispatched",
16
+ /** 检测到需要人工处理的确认类对话框(含原生文件夹选择框、残留弹窗清理) */
17
+ "confirmation_dialog_detected",
18
+ /** 等待用户授权/登录/确认,任务已卡在人工介入上 */
19
+ "awaiting_user_authorization",
20
+ /**
21
+ * agent 开始执行(GUI 侧观测到「运行中」信号首次出现)。
22
+ * 注意:这是**启发式**推断——适配器并不直接观测文件系统,
23
+ * 因此 detail 文案一律如实写「可能开始改动文件」,不声称已改动。
24
+ */
25
+ "file_modification_started",
26
+ /** 验收失败后进入返修(自动或 manual) */
27
+ "rework_triggered",
28
+ ];
29
+ /** 判断某个事件名是否属于本词表(供读取侧过滤使用) */
30
+ export function isAgentEventName(name) {
31
+ return AGENT_EVENT_NAMES.includes(name);
32
+ }
33
+ /**
34
+ * 把可选的 `onEvent` 钩子包成「永不抛错」的上报函数。
35
+ *
36
+ * 这是 issue #18「事件上报为可选能力,不影响原有逻辑」的落点:上报失败(例如落盘 IO 出错)
37
+ * 绝不能中断正在进行的 GUI 任务,因此这里吞掉异常。未提供钩子时是空操作。
38
+ */
39
+ export function makeEmitter(onEvent) {
40
+ return async (kind, detail, data) => {
41
+ if (!onEvent)
42
+ return;
43
+ try {
44
+ await onEvent({ kind, detail, data });
45
+ }
46
+ catch {
47
+ // 有意吞掉:事件上报属观测能力,不得影响任务本体
48
+ }
49
+ };
50
+ }
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import path from "node:path";
12
12
  import { visualEvidence } from "../../visual/report.js";
13
+ import { renderDirectiveSection } from "../../verify/directives.js";
13
14
  import { mkdirp, writeTextAtomic } from "../../util/fs.js";
14
15
  import { fixPlanRelPath, fixPlanAbsPath } from "./input.js";
15
16
  /** 渲染修复计划正文(含失败证据、通过项、代码分析、修复要求) */
@@ -50,6 +51,7 @@ export function renderCodexFixPlan(input) {
50
51
  lines.push("", "输出尾部:", "```text", (c.outputTail || "(无输出)").slice(-2000), "```", "");
51
52
  });
52
53
  }
54
+ lines.push(renderDirectiveSection(report));
53
55
  lines.push("## 3. 通过的项(勿破坏)", "");
54
56
  if (passed.length === 0)
55
57
  lines.push("(无)", "");
@@ -3,6 +3,7 @@
3
3
  * 同时提供修复指令模板(决策 11/12:修复计划文档由 MCP 自动生成并固定命名)。
4
4
  */
5
5
  import path from "node:path";
6
+ import { renderDirectiveLines } from "../../verify/directives.js";
6
7
  /** 修复计划文档文件名(含轮次号,每轮独立、不覆盖) */
7
8
  export function fixPlanFileName(round) {
8
9
  return `codex-fix-r${round}.md`;
@@ -47,6 +48,9 @@ export function buildFixPrompt(input) {
47
48
  "",
48
49
  input.summary,
49
50
  ];
51
+ if (input.directives?.items.length) {
52
+ lines.push("", "【结构化修复指令(摘要,最多 10 条;完整清单见修复计划文档)】", ...renderDirectiveLines(input.directives, 10));
53
+ }
50
54
  if (input.evidence?.trim())
51
55
  lines.push("", "关键证据:", "```text", input.evidence.trim().slice(-2000), "```");
52
56
  if (input.reportPath)
@@ -14,6 +14,7 @@ import { ZCODE_SETUP_DEFAULTS } from "../../config/schema.js";
14
14
  import fs from "node:fs";
15
15
  import path from "node:path";
16
16
  import { createHash } from "node:crypto";
17
+ import { makeEmitter } from "../agent-events.js";
17
18
  import { mkdirp } from "../../util/fs.js";
18
19
  import { parseCodexModel, exactUiName, parseTriggerValue } from "./model.js";
19
20
  import { matchCodexProject, projectBasename } from "./project.js";
@@ -143,6 +144,8 @@ export async function runCodexTask(args) {
143
144
  const deps = { ...DEFAULT_DEPS, ...args.deps };
144
145
  const gui = codexGuiOf(resolved);
145
146
  const { logger, close } = fileLogger(logFile, opts.logger);
147
+ // 细粒度事件上报(issue #18):未提供钩子时为空操作,失败不影响任务本体。
148
+ const emit = makeEmitter(opts.onEvent);
146
149
  let cdp;
147
150
  const result = (extra) => ({
148
151
  ok: false,
@@ -193,13 +196,17 @@ export async function runCodexTask(args) {
193
196
  cdp?.disconnect();
194
197
  cdp = next;
195
198
  };
196
- if (await cdp.exists("loginIndicator"))
199
+ if (await cdp.exists("loginIndicator")) {
200
+ await emit("awaiting_user_authorization", "Codex 登录指示可见,等待用户完成登录或引导", {
201
+ needsUserKind: "login_required",
202
+ });
197
203
  return result({
198
204
  endReason: "needs_user",
199
205
  needsUserKind: "login_required",
200
206
  pendingQuestion: "请在 Codex 窗口中完成登录或引导,然后调用 continue_task 确认。",
201
207
  session: { boundProjectPath: ctx.projectPath, model: spec.model, permissionMode: gui.defaultPermissionMode },
202
208
  });
209
+ }
203
210
  await cdp.dismissMenus();
204
211
  const resumeKind = ctx.resume?.kind;
205
212
  // 重观察恢复(user_confirmation):用户在 GUI 处理完等待项后 turn 自行继续,
@@ -250,7 +257,7 @@ export async function runCodexTask(args) {
250
257
  logger.info(`[codex] 已选择既有项目:${matched.item.name}`);
251
258
  }
252
259
  else {
253
- const created = await createProject(cdp, ctx.projectPath, aumid, gui, deps, logger);
260
+ const created = await createProject(cdp, ctx.projectPath, aumid, gui, deps, logger, emit);
254
261
  if (!created.ok)
255
262
  return result({
256
263
  hardFailure: true,
@@ -353,6 +360,12 @@ export async function runCodexTask(args) {
353
360
  endReason: "send_unknown",
354
361
  });
355
362
  logger.info(`[codex] 指令已确认发送(对话区=${seenMessage},输入清空=${seenCleared},运行信号=${seenRunning})`);
363
+ await emit("task_dispatched", `第 ${ctx.round} 轮指令已确认送达 Codex`, {
364
+ round: ctx.round,
365
+ seenMessage,
366
+ seenCleared,
367
+ seenRunning,
368
+ });
356
369
  }
357
370
  // ---- 步骤 6:运行检测 ----
358
371
  const deadline = started + ctx.taskTimeoutMs;
@@ -364,6 +377,8 @@ export async function runCodexTask(args) {
364
377
  }
365
378
  let cdpFailures = 0;
366
379
  let lastProgress = 0;
380
+ // file_modification_started 每次进程运行只发一次(首次观测到运行信号时)
381
+ let emittedRunning = false;
367
382
  for (;;) {
368
383
  // 取消(issue #6):不止退出 MCP 等待循环,还要尽力点击 GUI 停止按钮并等待空闲,
369
384
  // 结果经 guiStop 上报,由编排方在终态文案中如实反映。
@@ -402,28 +417,44 @@ export async function runCodexTask(args) {
402
417
  }
403
418
  continue;
404
419
  }
420
+ // 判定前先记下上一轮的运行信号:判定的输出含本轮状态,二者比较才能识别「首次开始运行」
421
+ const wasRunning = state.sawRunning;
405
422
  const verdict = judgeCodexPoll(poll, state, gui.stableRounds, gui.idleTimeoutMs, Date.now(), gui.stallTimeoutMs);
406
423
  state = verdict.state;
424
+ if (!wasRunning && state.sawRunning && !emittedRunning) {
425
+ emittedRunning = true;
426
+ // 文案如实保留:Codex 适配器并不直接观测文件系统,无法声称文件确已改动。
427
+ await emit("file_modification_started", "停止按钮出现,Codex 开始执行(可能开始改动文件)", {
428
+ round: ctx.round,
429
+ evidence: verdict.evidence,
430
+ });
431
+ }
407
432
  if (Date.now() - lastProgress >= gui.progressIntervalMs) {
408
433
  const note = `Codex 进度:${verdict.kind};运行证据=${verdict.evidence};对话哈希=${state.hash};稳定轮=${state.stable}`;
409
434
  await Promise.resolve(opts.onProgress?.(note)).catch(() => { });
410
435
  logger.info(note);
411
436
  lastProgress = Date.now();
412
437
  }
413
- if (verdict.kind === "needs_login")
438
+ if (verdict.kind === "needs_login") {
439
+ await emit("awaiting_user_authorization", "Codex 需要登录,等待用户完成登录", {
440
+ needsUserKind: "login_required",
441
+ });
414
442
  return result({
415
443
  endReason: "needs_user",
416
444
  needsUserKind: "login_required",
417
445
  pendingQuestion: "Codex 需要登录,请在窗口中完成登录后调用 continue_task 确认。",
418
446
  session: { boundProjectPath: ctx.projectPath, model: spec.model, permissionMode: gui.defaultPermissionMode },
419
447
  });
420
- if (verdict.kind === "needs_user")
448
+ }
449
+ if (verdict.kind === "needs_user") {
450
+ await emit("awaiting_user_authorization", "停止按钮持续可见且对话长时间未变化,疑似等待用户确认(方案确认/订阅确认等)", { needsUserKind: "user_confirmation" });
421
451
  return result({
422
452
  endReason: "needs_user",
423
453
  needsUserKind: "user_confirmation",
424
454
  pendingQuestion: "Codex 停止按钮持续可见且对话内容长时间未变化,疑似在等待用户确认(方案确认/订阅确认等)。请在 Codex 窗口完成处理后调用 continue_task(taskId, message=已处理说明) 恢复;恢复后仅重新接入观察,不会发送消息。",
425
455
  session: { boundProjectPath: ctx.projectPath, model: spec.model, permissionMode: gui.defaultPermissionMode },
426
456
  });
457
+ }
427
458
  if (verdict.kind === "idle_timeout")
428
459
  return result({ endReason: "idle_timeout", error: "Codex 空闲超时;已停止 MCP 等待并保留现场" });
429
460
  if (verdict.kind === "finished")
@@ -495,7 +526,7 @@ async function stopGuiTurn(cdp, gui, deps, logger) {
495
526
  * 打开项目选择层 → 新建项目 → 点源文件夹中心空白区 → 原生对话框键盘填路径 →
496
527
  * 确认源文件夹 → 点创建项目。任一步无法唯一定位即 fail-closed。
497
528
  */
498
- async function createProject(cdp, projectPath, aumid, gui, deps, logger) {
529
+ async function createProject(cdp, projectPath, aumid, gui, deps, logger, emit) {
499
530
  // 新建会话后输入框会重渲染,触发器可能短暂缺席 —— 先等它出现再点。
500
531
  if (!(await waitFor(cdp, "projectPickerTrigger", deps, 12_000))) {
501
532
  // issue #23 诊断机制:解析失败时把页面可见候选一并给出,使用者一步定位文案漂移。
@@ -529,8 +560,12 @@ async function createProject(cdp, projectPath, aumid, gui, deps, logger) {
529
560
  .map((p) => p.pid);
530
561
  // 先清理残留原生对话框(上一轮失败可能留下,遮挡界面且会让本轮误判「无新对话框」)
531
562
  const closed = await deps.closeDialogs(pids);
532
- if (closed)
563
+ if (closed) {
533
564
  logger.warn(`[codex] 已清理 ${closed} 个残留原生对话框`);
565
+ await emit("confirmation_dialog_detected", `清理残留原生对话框 ${closed} 个`, {
566
+ nativeDialogsClosed: closed,
567
+ });
568
+ }
534
569
  // 原生文件夹选择器只在应用窗口处于前台时弹出;无人值守下先用 COM 激活把窗口带到前台
535
570
  // (SetForegroundWindow 会被前台锁拒绝,应用模型激活不会)。
536
571
  const focused = aumid ? await deps.focusApp(aumid) : false;
@@ -541,6 +576,9 @@ async function createProject(cdp, projectPath, aumid, gui, deps, logger) {
541
576
  const baseline = await deps.listDialogs(pids);
542
577
  if (!(await cdp.clickTrusted("sourceFolderArea")))
543
578
  return { ok: false, error: "无法触发「源文件夹」点击(元素不可见或落点被遮挡)" };
579
+ await emit("confirmation_dialog_detected", "已唤起原生「选择文件夹」对话框", {
580
+ dialog: "source_folder",
581
+ });
544
582
  await deps.sleep(1500);
545
583
  const selected = await deps.selectFolder(projectPath, pids, baseline);
546
584
  if (!selected.ok)
@@ -11,6 +11,7 @@ import { ZCODE_SETUP_DEFAULTS } from "../../config/schema.js";
11
11
  import fs from "node:fs";
12
12
  import path from "node:path";
13
13
  import { mkdirp } from "../../util/fs.js";
14
+ import { makeEmitter } from "../agent-events.js";
14
15
  import { CdpDisconnectedError, CdpUnavailableError, interpretLiveness, TraeworkCdpClient, } from "./cdp/client.js";
15
16
  import { launchInstance, probeReady, releaseInstance, resolvePort, waitReady, diagnosePortFailure, } from "./launcher.js";
16
17
  import { bindProject, ensureMode, matchProjectItem, projectBasename, readBoundProject, readMode, resolveMode, startNewSession, } from "./ui/session.js";
@@ -178,6 +179,8 @@ export async function runTraeworkTask(args) {
178
179
  const deps = { ...DEFAULT_DEPS, ...args.deps };
179
180
  const gui = guiOf(resolved);
180
181
  const { logger, close: closeLog } = makeFileLogger(logFile, args.logger);
182
+ // 细粒度事件上报(issue #18):未提供钩子时为空操作,失败不影响任务本体。
183
+ const emit = makeEmitter(opts.onEvent);
181
184
  let endReason;
182
185
  let keptInstance = false;
183
186
  const fail = (error, extra = {}) => ({
@@ -264,6 +267,12 @@ export async function runTraeworkTask(args) {
264
267
  sleep: deps.sleep,
265
268
  dialogWaitTimeoutMs: deps.dialogWaitTimeoutMs,
266
269
  });
270
+ if (bound.method === "native-dialog") {
271
+ // 走原生「选择文件夹」对话框绑定:这正是无人值守下最容易卡住的确认类交互
272
+ await emit("confirmation_dialog_detected", bound.bound
273
+ ? `项目绑定经原生「选择文件夹」对话框完成:${bound.message}`
274
+ : `原生「选择文件夹」对话框驱动失败:${bound.message}`, { dialog: "source_folder", bound: bound.bound });
275
+ }
267
276
  if (!bound.bound) {
268
277
  return stop("setup_failed", `项目文件夹绑定失败(${bound.method}):${bound.message}`, {
269
278
  hardFailure: true,
@@ -298,6 +307,10 @@ export async function runTraeworkTask(args) {
298
307
  const promptText = buildPromptText(ctx.task, ctx.context, ctx.feedback);
299
308
  const marker = makeMarker();
300
309
  await typeAndSend(cdp, marker + promptText, { selectors: gui.selectors, logger, sleep: deps.sleep });
310
+ await emit("task_dispatched", `第 ${ctx.round} 轮任务书已发送至 TraeWork`, {
311
+ round: ctx.round,
312
+ chars: promptText.length,
313
+ });
301
314
  // ---- 7. 轮询到完成 ----
302
315
  const base = await cdp.text("messageContainer", gui.selectors);
303
316
  let state = { prev: "", stable: 0, idleSince: 0 };
@@ -340,8 +353,16 @@ export async function runTraeworkTask(args) {
340
353
  const live = interpretLiveness(liveness);
341
354
  const now = Date.now();
342
355
  if (live.running) {
343
- if (runningSince === 0)
356
+ if (runningSince === 0) {
344
357
  runningSince = now;
358
+ // 首次由静止转为运行:会话确实开始干活了。文案如实保留 —— 适配器并不直接
359
+ // 观测文件系统,无法声称文件确已改动。
360
+ // eslint-disable-next-line no-await-in-loop
361
+ await emit("file_modification_started", "运行信号首次出现,TraeWork 开始执行(可能开始改动文件)", {
362
+ round: ctx.round,
363
+ evidence: live.evidence,
364
+ });
365
+ }
345
366
  if (!runningWarned && now - runningSince >= gui.idleTimeoutMs) {
346
367
  logger.warn(`[traework] 运行信号已持续 ${Math.round((now - runningSince) / 1000)}s(${live.evidence}),仅记录诊断,继续等待`);
347
368
  runningWarned = true;
@@ -375,6 +396,11 @@ export async function runTraeworkTask(args) {
375
396
  endReason = "ask_user";
376
397
  logger.warn("[traework] 模型发起原生提问(ask_user),会话被阻塞,按本轮结束处理");
377
398
  replyText = verdict.added;
399
+ // 这是「卡在人工介入」的典型形态,必须让调用方能从事件流直接看出来
400
+ // eslint-disable-next-line no-await-in-loop
401
+ await emit("awaiting_user_authorization", "模型发起原生提问(ask_user),会话被阻塞等待用户回答", {
402
+ endReason: "ask_user",
403
+ });
378
404
  await cdp.pressEscape().catch(() => undefined);
379
405
  break;
380
406
  }
@@ -100,9 +100,17 @@ export const RunTaskParamsSchema = z.object({
100
100
  */
101
101
  idempotencyKey: IdempotencyKeySchema.optional(),
102
102
  });
103
+ /** query_task 返回的细粒度事件条数默认值(issue #18) */
104
+ export const QUERY_TASK_EVENT_LIMIT_DEFAULT = 10;
103
105
  export const QueryTaskParamsSchema = z.object({
104
106
  taskId: z.string().min(1),
105
107
  tailLines: z.number().int().positive().optional(),
108
+ /**
109
+ * 返回最近 N 条细粒度 agent 事件(issue #18):task_dispatched /
110
+ * confirmation_dialog_detected / awaiting_user_authorization / file_modification_started /
111
+ * rework_triggered。缺省 10,上限 50。未实现事件上报的适配器返回空数组。
112
+ */
113
+ eventLimit: z.number().int().min(1).max(50).optional(),
106
114
  });
107
115
  export const ListTasksParamsSchema = z.object({
108
116
  projectPath: AbsPath.optional(),
@@ -151,6 +159,12 @@ export const VerifyTaskParamsSchema = z.object({
151
159
  export const ReworkTaskParamsSchema = z.object({
152
160
  taskId: z.string().min(1),
153
161
  feedback: z.string().optional(),
162
+ /**
163
+ * 结构化修复提示(issue #19):调用方自带的一小段「文件 / 行 / 做什么」,会以
164
+ * 【结构化修复提示】块置于 feedback 之前,便于 agent 先精确定位再读整段说明。
165
+ * 自由字符串(上限 4000 字符);不传则行为与既有版本一致。
166
+ */
167
+ repairHint: z.string().max(4000).optional(),
154
168
  });
155
169
  export const ContinueTaskParamsSchema = z.object({
156
170
  taskId: z.string().min(1),
@@ -10,6 +10,7 @@ import { readTextSafe, exists, readDirSafe, mkdirp, writeJsonAtomic, readJsonSaf
10
10
  import { runChild } from "../agents/spawn.js";
11
11
  import { captureBaseline } from "../verify/git-baseline.js";
12
12
  import { summarizeReport } from "../verify/report.js";
13
+ import { renderDirectiveLines } from "../verify/directives.js";
13
14
  import { writeRepairPlan } from "./repair-plan.js";
14
15
  import { writeCodexFixPlan } from "../agents/codex/fixplan.js";
15
16
  import { buildFixPrompt } from "../agents/codex/input.js";
@@ -224,6 +225,12 @@ export class TaskOrchestrator {
224
225
  if (maxRounds > round) {
225
226
  await store.updateStatus(meta, "fixing", `第 ${round} 轮验收失败,进入第 ${round + 1} 轮返修`);
226
227
  round += 1;
228
+ // 事件流(issue #18):返修是与适配器无关的引擎侧节点,由编排器直接上报。
229
+ await store.appendEvent(meta.taskId, "rework_triggered", "fixing", `第 ${round - 1} 轮验收失败,进入第 ${round} 轮自动返修`, {
230
+ round,
231
+ mode: "auto",
232
+ failedChecks: verdict.report.checks.filter((c) => !c.passed && !c.skipped).map((c) => c.name),
233
+ });
227
234
  if (meta.agentId === "codex") {
228
235
  // 决策 11/12:Codex 的修复计划由 MCP 自动生成,落在**项目内** .zcode/plans/
229
236
  // (文件名含轮次号 codex-fix-r<N>.md,不覆盖历史);因文件名发送前已知,
@@ -251,6 +258,7 @@ export class TaskOrchestrator {
251
258
  planRelPath: plan.relPath,
252
259
  reportPath: verdict.mdPath,
253
260
  evidence: extractFailureEvidence(verdict.report),
261
+ directives: verdict.report.repairDirectives,
254
262
  });
255
263
  logger.info(`[codex] 第 ${roundNo} 轮返修指令已引用修复计划 ${plan.relPath}`);
256
264
  continue;
@@ -273,7 +281,7 @@ export class TaskOrchestrator {
273
281
  if (planReadable == null || reportReadable == null) {
274
282
  return this.finish("failed", "internal", "返修计划或验收报告在任务数据目录中不可读,拒绝发送降级摘要");
275
283
  }
276
- feedback = buildFixFeedback(meta.task, verdict.summary, verdict.mdPath, plan.taskPath);
284
+ feedback = buildFixFeedback(meta.task, verdict.summary, verdict.mdPath, plan.taskPath, verdict.report.repairDirectives);
277
285
  if (meta.agentId === "qoder")
278
286
  feedback += `\n\n修复计划全文(${path.basename(plan.taskPath)}):\n${planReadable}`;
279
287
  continue;
@@ -417,6 +425,12 @@ export class TaskOrchestrator {
417
425
  await this.deps.store.appendEvent(ctx.taskId, "note", this.meta.status, note);
418
426
  await this.deps.store.writeSnapshot(this.meta);
419
427
  },
428
+ // 细粒度事件(issue #18):适配器主动上报,编排器只负责落盘,不做任何解释或推断。
429
+ onEvent: async (ev) => {
430
+ this.meta.updatedAt = new Date().toISOString();
431
+ await this.deps.store.appendEvent(ctx.taskId, ev.kind, this.meta.status, ev.detail, ev.data);
432
+ await this.deps.store.writeSnapshot(this.meta);
433
+ },
420
434
  });
421
435
  }
422
436
  await this.deps.registry.prepareInvocation(ctx.agentId, ctx, resolved);
@@ -466,11 +480,17 @@ export class TaskOrchestrator {
466
480
  };
467
481
  }
468
482
  }
469
- function buildFixFeedback(taskText, verifySummary, reportMd, planPath) {
483
+ function buildFixFeedback(taskText, verifySummary, reportMd, planPath, directives) {
470
484
  const lines = ["【上一轮验收失败反馈 —— 请针对下列失败项定向修复,不要大范围重构】", ""];
471
485
  if (planPath) {
472
486
  lines.push(`修复计划文档:\`${planPath}\`(MCP 任务数据目录绝对路径;请先读取并逐条处理)`, "");
473
487
  }
488
+ // issue #19:把结构化指令摘要直接放进返修消息,省去 agent 从整篇报告里定位的开销。
489
+ // 仅在确有指令时追加;提取失败时**不**在这里说明(计划文档的 2.5 节已如实交代),
490
+ // 避免返修消息被「提取失败」的噪声占据。
491
+ if (directives?.items.length) {
492
+ lines.push("【结构化修复指令(摘要,最多 10 条;完整清单见修复计划文档)】", ...renderDirectiveLines(directives, 10), "");
493
+ }
474
494
  lines.push(verifySummary, "", `完整验收报告:${reportMd}`, "修复完成后正常结束本轮即可。");
475
495
  return lines.join("\n");
476
496
  }
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import path from "node:path";
10
10
  import { visualEvidence } from "../visual/report.js";
11
+ import { renderDirectiveSection } from "../verify/directives.js";
11
12
  import { mkdirp, writeTextAtomic } from "../util/fs.js";
12
13
  /** 生成修复计划 markdown 正文 */
13
14
  export function renderRepairPlan(input) {
@@ -50,6 +51,7 @@ export function renderRepairPlan(input) {
50
51
  lines.push("", "输出尾部:", "```text", (c.outputTail || "(无输出)").slice(-2000), "```", "");
51
52
  });
52
53
  }
54
+ lines.push(renderDirectiveSection(report));
53
55
  lines.push("## 3. 通过的项(勿破坏)", "");
54
56
  if (passed.length === 0)
55
57
  lines.push("(无)", "");