dsh-my-observability 0.3.0 → 0.3.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.md +9 -79
- package/README.md +18 -76
- package/lib/ai.js +123 -99
- package/lib/audit-view.js +173 -172
- package/lib/audit.js +175 -166
- package/lib/client.js +1024 -1278
- package/lib/client.src.js +55 -29
- package/lib/constants.js +4 -4
- package/lib/diff.js +61 -56
- package/lib/git.js +97 -98
- package/lib/index.js +46 -47
- package/lib/parts/native-tabs.js +45 -0
- package/lib/resource-monitor.js +82 -134
- package/lib/resource-rules.js +9 -99
- package/lib/review.js +68 -77
- package/lib/routes.js +223 -200
- package/lib/store-persist.js +114 -111
- package/lib/store.js +215 -212
- package/package.json +15 -7
- package/lib/parts/git.js +0 -298
- package/lib/parts/i18n.js +0 -104
- package/lib/parts/replay-ext.js +0 -396
- package/lib/parts/replay.js +0 -386
- package/lib/parts/resource.js +0 -80
- package/lib/parts/styles.js +0 -156
package/CHANGELOG.md
CHANGED
|
@@ -2,89 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录 dsh-my-observability 的所有版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
-
## [0.3.
|
|
5
|
+
## [0.3.2] - 2026-09-17
|
|
6
6
|
|
|
7
7
|
### 变更
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
9
|
+
- docs: skill 合并 12→10 并拆分超限文件,修 observability 聚合端点缺陷
|
|
10
|
+
- docs(清理): #341 文档瘦身 23765 → 8309 行并固化精简规范 (#348)
|
|
11
|
+
- chore(artifacts): #318 同步 8 处共享件产物漂移 + 新增产物一致性门禁(fail-closed) (#325)
|
|
12
|
+
- fix(test): cucumber-js --import glob 改用双引号,兼容 Windows
|
|
13
|
+
- fix(security): 批量修复 'Insecure temporary file' 安全漏洞
|
|
14
|
+
- chore: 项目全面优化和完善
|
|
13
15
|
|
|
14
|
-
## [0.
|
|
16
|
+
## [0.3.1] - 2026-09-10
|
|
15
17
|
|
|
16
18
|
### 变更
|
|
17
19
|
|
|
18
|
-
- feat(
|
|
19
|
-
- feat(observability): #155 资源监控扩展——新增 $DSH_HOME 目录总大小监控 (#176)
|
|
20
|
-
- feat(observability): #155 插件状态查询聚合——统一 status-query 事件 + /plugin-status API (#170)
|
|
21
|
-
- chore(plugins): #165 清理失效的 dsh.client.inject 声明(13 插件) (#167)
|
|
22
|
-
|
|
23
|
-
## [0.1.7] - 2026-09-06
|
|
24
|
-
|
|
25
|
-
### 变更
|
|
26
|
-
|
|
27
|
-
- fix(task-reliability,observability): #165 Session.events 迁移——snapshotEvents 优先 + 旧 API 兜底 (#166)
|
|
28
|
-
- feat(observability,task-reliability): #154 插件关键行为纳入事件审计(干预/ask 决策/救场/verify/恢复)+ 文件体积控制 (#158)
|
|
29
|
-
- fix(observability): #142 dispose 落盘链可等待,修复 release 门禁 flaky 测试 (#148)
|
|
30
|
-
|
|
31
|
-
## [0.1.6] - 2026-09-04
|
|
32
|
-
|
|
33
|
-
### 变更
|
|
34
|
-
|
|
35
|
-
- fix(observability): 轨迹回放会话下拉可读标题(首条用户消息)——此前只展示 UUID 清单
|
|
36
|
-
|
|
37
|
-
## [0.1.5] - 2026-09-04
|
|
38
|
-
|
|
39
|
-
### 变更
|
|
40
|
-
|
|
41
|
-
- feat(observability): #127 资源看门狗自动降级——写入速率/文件大小连续 ≥3 次采样超限自动暂停落盘(事件仍入内存,FIFO 有界不丢)+ 日志告警;连续 ≥3 次正常自动恢复(全量快照补齐降级窗口);API 暴露 degraded 标记(#136)
|
|
42
|
-
- 发版前功能级验证清单(issue #67):verification/dsh-my-observability-0.1.5.md
|
|
43
|
-
|
|
44
|
-
## [0.1.4] - 2026-09-03
|
|
45
|
-
|
|
46
|
-
### 变更
|
|
47
|
-
|
|
48
|
-
- feat(observability): #R17 资源监控看板——写放大/资源超限提前发现
|
|
49
|
-
|
|
50
|
-
## [0.1.3] - 2026-09-03
|
|
51
|
-
|
|
52
|
-
### 变更
|
|
53
|
-
|
|
54
|
-
- fix(observability): 审计持久化改增量追加,根治写放大磁盘风暴
|
|
55
|
-
- feat(observability): #89 审计日志搜索/过滤 + JSON/CSV 导出 (#120)
|
|
56
|
-
- docs: #106 安装命令统一加 --trust-lockfile (#113)
|
|
57
|
-
|
|
58
|
-
## [0.1.2] - 2026-09-01
|
|
59
|
-
|
|
60
|
-
### 变更
|
|
61
|
-
|
|
62
|
-
- fix(ui): 9 个插件未定义 token danger-primary 改用 error-primary(DSH 主题仅定义 business/error/success/warn)
|
|
63
|
-
|
|
64
|
-
## [0.1.1] - 2026-08-28
|
|
65
|
-
|
|
66
|
-
### 变更
|
|
67
|
-
|
|
68
|
-
- feat(ui): dsh-my-observability 时间轴翻新——类型图标/连线节点/前缀(issue #54)
|
|
69
|
-
- fix(security): 修复 CodeQL 告警——正则 ReDoS/随机数/命令注入(issue #5/#6/#7/#9/#10)
|
|
70
|
-
- refactor(shared): 抽取 dsh-shared 共享工具包,10 个插件迁移消除重复实现(issue #45)
|
|
71
|
-
- chore(deps): 升级 react 19 兼容性——13 个插件 peer 声明 ^18.2.0 || ^19.2.0(issue #49)
|
|
72
|
-
- style(format): 全仓 prettier 格式化(issue #44)
|
|
73
|
-
|
|
74
|
-
## [0.1.0] - 2026-08-28
|
|
75
|
-
|
|
76
|
-
### 新增
|
|
77
|
-
|
|
78
|
-
- **事件审计(issue #53)**:监听 `agent/status`(agent_status,含顶层/子代理标记)、`llm/stream`(llm_stream,同步包装流透传全部 chunk,记录开始/结束/错误 + chunk/字符/耗时统计)、`tools/pre-execute`(tool_call,工具名 + 参数键列表 + 文本摘要截断)、`tools/execute`(tool_result,成功/失败 + 耗时);waterfall 一律透传 `next()`,只读观察不改变流程。
|
|
79
|
-
- **审计持久化**:`$DSH_HOME/observability/audit.json`(防抖 500ms + 原子写 tmp+rename + teardown flush),启动异步加载(加载前事件缓冲不丢),重启后完整恢复;按会话隔离;每会话 2000 条 / 全局 20000 条上限防膨胀。
|
|
80
|
-
- **轨迹回放面板**:侧边栏页签 `dsh-my-observability:replay`(dsh-better-sidebar 服务),会话选择 + 类型过滤(全部/状态/模型流/工具)+ 时间轴事件列表(徽标 + 时间 + 摘要);可见时 5s 轮询、隐藏时暂停。
|
|
81
|
-
- **结构化 Git 工具**:侧边栏页签 `dsh-my-observability:git`——仓库状态(分支 + 暂存/未暂存计数)、差异查看(工作区/暂存区)、类型化提交(Conventional Commits:type 枚举 feat/fix/docs/style/refactor/test/chore + 可选 scope/body,服务端生成消息 + execFile 执行 git 不经 shell)。
|
|
82
|
-
- **增量 diff 审查**:`POST /observability/api/review`——unified diff 解析 + 8 条规则引擎(secret-leak/conflict-marker 为 error,debug-statement/large-diff/binary-file 为 warning,todo-marker/trailing-space/no-test-change 为 info);可选 AI 审查增强(agents.create,超时/失败/解析失败降级,规则结果不受影响)。
|
|
83
|
-
- **查询接口**:`GET /observability/api/events`(会话事件,type 过滤 + limit)、`GET /observability/api/sessions`(会话列表)、`GET /observability/api/status`(审计统计 + 开关);全部经 loopback 信任围栏。
|
|
84
|
-
- **测试**:`test/host-audit.mjs`(事件监听/会话隔离/重启恢复/上限/fence)、`test/host-git.mjs`(真实临时 git 仓库 status/diff/commit)、`test/host-review.mjs`(规则引擎 + AI 各路径)、`test/host-mutation.mjs`(变异补充)、Gherkin 验收(observability/git/review 3 个 feature 16 场景);覆盖率 行 95% / 分支 85%;变异测试 ≥70%。
|
|
85
|
-
|
|
86
|
-
**真实环境验证**(独立端口 3081 隔离实例):
|
|
87
|
-
|
|
88
|
-
- 侧边栏「轨迹回放」页签出现,时间轴展示真实 agent 行为(状态/模型流/工具调用);
|
|
89
|
-
- 「Git 工具」页签:真实仓库状态/差异/类型化提交/提交前审查全流程可用;
|
|
90
|
-
- 重启验证实例后审计事件恢复。
|
|
20
|
+
- feat(ts): 第四轮 JS→TS 迁移(5 个插件,server + client 全量)
|
package/README.md
CHANGED
|
@@ -7,101 +7,43 @@
|
|
|
7
7
|
<img alt="Git 工具面板:仓库状态 / 差异 / 提交前审查 / 类型化提交" src="https://unpkg.com/dsh-my-observability/assets/git-panel.png" width="640" />
|
|
8
8
|
</div>
|
|
9
9
|
|
|
10
|
-
**DSH 可观测性 + Git
|
|
10
|
+
**dsh-my-observability**:DSH 可观测性 + Git 工程工具插件——**事件审计**记录 agent 的每一次行为(状态变化 / 模型流 / 工具调用与结果),**轨迹回放面板**按时间轴回放会话轨迹;**结构化 Git 工具**提供 Conventional Commits 类型化提交;**增量 diff 审查**在提交前用规则引擎(可选 AI 增强)检查调试残留、密钥泄露等问题。
|
|
11
11
|
|
|
12
12
|
## 功能
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
| `agent/status` | `agent_status` | 状态变化 + 顶层/子代理标记 |
|
|
21
|
-
| `llm/stream` | `llm_stream` | 流开始/结束/错误 + chunk/字符/耗时统计(同步包装流,透传全部 chunk) |
|
|
22
|
-
| `tools/pre-execute` | `tool_call` | 工具名 + 参数键列表 + 主要文本参数截断摘要 |
|
|
23
|
-
| `tools/execute` | `tool_result` | 工具名 + 成功/失败 + 耗时 |
|
|
24
|
-
|
|
25
|
-
- **会话隔离**:事件按会话分桶,切换会话互不串扰;
|
|
26
|
-
- **重启恢复**:持久化到 `$DSH_HOME/observability/audit.jsonl`(**增量追加** + 防抖批量 flush + 周期 compact 原子快照),重启后完整恢复;升级前旧格式 `audit.json` 自动迁移;
|
|
27
|
-
- **防膨胀**:每会话最多 2000 条(FIFO 淘汰)、全局 20000 条(轮转淘汰);
|
|
28
|
-
- **零写放大**:落盘只写新增事件(≈事件本体字节),不会因事件流持续而反复全量重写审计文件;
|
|
29
|
-
- **资源监控 + 自动降级(资源看门狗)**:面板「资源」区块展示本进程 CPU/内存 + 审计文件大小与写入速率(15s 采样),写放大/超限自动告警(告警列表展示在「资源」区块,阈值见 `resource-budget-review`);写入速率/文件大小连续超限自动**暂停落盘**(事件仍入内存,有界不丢),回落自动恢复(全量快照补齐降级窗口),全程日志告警 + API `degraded` 标记(issue #127)。
|
|
30
|
-
- **只读观察**:waterfall 事件一律透传 `next()`,绝不改变工具/模型流程。
|
|
31
|
-
|
|
32
|
-
### 2. 轨迹回放面板(时间轴)
|
|
33
|
-
|
|
34
|
-
侧边栏「轨迹回放」页签:选择会话 + 类型过滤(全部 / 状态 / 模型流 / 工具),按时间轴展示 agent 行为(类型徽标 + 时间 + 摘要);面板可见时 5s 轮询、隐藏时暂停。
|
|
35
|
-
|
|
36
|
-
### 3. 结构化 Git 工具(类型化提交)
|
|
37
|
-
|
|
38
|
-
侧边栏「Git 工具」页签:输入仓库路径 → 查看状态(分支 + 暂存/未暂存计数)与差异(工作区/暂存区)→ **类型化提交**:
|
|
39
|
-
|
|
40
|
-
- 类型枚举:`feat` / `fix` / `docs` / `style` / `refactor` / `test` / `chore`;
|
|
41
|
-
- 消息格式:`<type>(<scope>): <description>`(scope 可选,body 可选);
|
|
42
|
-
- 服务端生成消息 + `execFile` 执行 git(不经 shell),提交历史清晰可追溯。
|
|
43
|
-
|
|
44
|
-
### 4. 增量 diff 审查(提交前审查)
|
|
45
|
-
|
|
46
|
-
「提交前审查」按钮对增量 diff 运行规则引擎(确定性、可测试):
|
|
47
|
-
|
|
48
|
-
| 规则 | 严重级别 | 检查内容 |
|
|
49
|
-
| ----------------- | -------- | ----------------------------------------------------- |
|
|
50
|
-
| `secret-leak` | error | 密钥/凭据硬编码(password/api_key/secret/token) |
|
|
51
|
-
| `conflict-marker` | error | 合并冲突标记残留(`<<<<<<<` / `=======` / `>>>>>>>`) |
|
|
52
|
-
| `debug-statement` | warning | 调试残留(console.* / debugger / print 族) |
|
|
53
|
-
| `large-diff` | warning | 单文件变更 > 300 行 |
|
|
54
|
-
| `binary-file` | warning | 二进制文件变更 |
|
|
55
|
-
| `todo-marker` | info | TODO/FIXME/HACK 标记 |
|
|
56
|
-
| `trailing-space` | info | 行尾多余空格 |
|
|
57
|
-
| `no-test-change` | info | 有源码变更但无测试变更 |
|
|
58
|
-
|
|
59
|
-
- **可选 AI 审查**:配置 `aiReview`(默认开)且 agents 服务可用时,创建独立审查 agent 阅读 diff 输出总评(verdict/summary/topIssues);超时/失败/解析失败自动降级,规则引擎结果不受影响(AI 是增强,不是门禁)。
|
|
60
|
-
|
|
61
|
-
## 工作原理
|
|
62
|
-
|
|
63
|
-
- **Server 端**(`lib/index.js`):`audit.js` 监听四类事件 → `store.js` 按会话分桶持久化;`git.js` 类型化提交(execFile);`review.js` + `diff.js` 规则引擎;`ai.js` 可选 AI 增强;`routes.js` 提供 `/observability/api` 路由(全部经 loopback 信任围栏)。
|
|
64
|
-
- **Client 端**(`lib/client.js`):两个侧边栏页签(`dsh-my-observability:replay` 轨迹回放、`dsh-my-observability:git` Git 工具),样式走 DSH 语义 token,随 fiber 卸载无残留。
|
|
14
|
+
- **事件审计**:只读观察并记录 `agent/status`(状态变化 + 顶层/子代理标记)、`llm/stream`(流开始/结束/错误 + chunk 与耗时统计,透传全部 chunk)、`tools/pre-execute`(工具名 + 参数摘要)、`tools/execute`(成功/失败 + 耗时);waterfall 事件一律透传 `next()`,绝不改变工具/模型流程。
|
|
15
|
+
- **持久化与上限**:按会话分桶增量追加到 `$DSH_HOME/observability/audit.jsonl`(防抖批量 flush + 周期原子 compact),重启后完整恢复;每会话上限 2000 条、全局 20000 条,超出淘汰最旧事件。
|
|
16
|
+
- **轨迹回放面板**:侧边栏「轨迹回放」页签,选会话 + 类型过滤(全部 / 状态 / 模型流 / 工具),时间轴展示类型徽标 + 时间 + 摘要;可见时 5s 轮询、隐藏时暂停。
|
|
17
|
+
- **结构化 Git 工具**:侧边栏「Git 工具」页签,输入仓库路径查看分支与暂存/未暂存计数、工作区/暂存区差异,按 `<type>(<scope>): <description>` 生成类型化提交(`feat` / `fix` / `docs` / `style` / `refactor` / `test` / `chore`);服务端 `execFile` 执行 git,不经 shell。
|
|
18
|
+
- **增量 diff 审查**:「提交前审查」对增量 diff 跑确定性规则引擎——error:密钥/凭据硬编码、合并冲突标记;warning:调试残留(console/debugger/print)、单文件变更 > 300 行、二进制文件变更;info:TODO/FIXME/HACK 标记、行尾空格、有源码变更但无测试变更。
|
|
19
|
+
- **资源看门狗**:面板「资源」区块展示本进程 CPU/内存 + 审计文件大小与写入速率(15s 采样),写放大或连续超限时自动暂停落盘(事件仍入内存,有界不丢),回落自动恢复,附日志告警与 API `degraded` 标记。
|
|
65
20
|
|
|
66
21
|
## 安装
|
|
67
22
|
|
|
68
|
-
> 💡 **npm 安装(普通用户推荐)**:`dsh plugin --profile web add dsh-my-observability --trust-lockfile
|
|
23
|
+
> 💡 **npm 安装(普通用户推荐)**:`dsh plugin --profile web add dsh-my-observability --trust-lockfile`——无需克隆本仓库;link 方式供本仓库开发者使用。
|
|
69
24
|
|
|
70
25
|
```bash
|
|
71
|
-
# 1) 克隆本仓库(任意目录)
|
|
72
26
|
git clone https://github.com/baosfeng/my-dsh-plugins.git
|
|
73
|
-
# 2) 以本地 link 方式安装(将 <仓库路径> 替换为上面的克隆目录)
|
|
74
27
|
dsh plugin --profile web add link:<仓库路径>/plugins/dsh-my-observability
|
|
75
28
|
```
|
|
76
29
|
|
|
77
|
-
|
|
30
|
+
server 端改动需重启 `dsh web`;client 端改动硬刷新浏览器(Cmd/Ctrl+Shift+R)即可。
|
|
78
31
|
|
|
79
32
|
## 配置
|
|
80
33
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
```yaml
|
|
84
|
-
- insert:
|
|
85
|
-
- id: observability
|
|
86
|
-
name: 'dsh-my-observability'
|
|
87
|
-
- config: # 传给 apply(ctx, config)
|
|
88
|
-
aiReview: true # 增量 diff 审查启用 AI 增强(默认 true;agents 服务不可用时自动降级)
|
|
89
|
-
aiTimeoutMs: 60000 # AI 审查超时(毫秒,默认 60000)
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## 依赖
|
|
34
|
+
插件行 `config` 字段,均可选:
|
|
93
35
|
|
|
94
|
-
|
|
|
95
|
-
|
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
36
|
+
| 配置键 | 作用 |
|
|
37
|
+
| ------------- | ---------------------------------------------------------------------- |
|
|
38
|
+
| `aiReview` | 增量 diff 审查启用 AI 增强(默认 `true`;agents 服务不可用时自动降级) |
|
|
39
|
+
| `aiTimeoutMs` | AI 审查超时(毫秒,默认 60000) |
|
|
98
40
|
|
|
99
41
|
## 限制与说明
|
|
100
42
|
|
|
101
|
-
- **Git
|
|
102
|
-
- **AI
|
|
103
|
-
-
|
|
43
|
+
- **Git 工具作用于本机仓库**:仓库路径由你在面板输入(localStorage 记住),仅本机可访问(loopback 围栏)。
|
|
44
|
+
- **AI 审查是增强不是门禁**:agents 服务不可用或超时/解析失败时自动降级为纯规则引擎结果。
|
|
45
|
+
- **审计有上限**:每会话 2000 条 / 全局 20000 条,超出自动淘汰最旧事件。
|
|
104
46
|
|
|
105
47
|
## 相关文档
|
|
106
48
|
|
|
107
|
-
→ [可观测性模块文档](../../docs/可观测性/概述.md) · [
|
|
49
|
+
→ [可观测性模块文档](../../docs/可观测性/概述.md) · [CHANGELOG](CHANGELOG.md)
|
package/lib/ai.js
CHANGED
|
@@ -10,67 +10,88 @@
|
|
|
10
10
|
* 参考 dsh-task-reliability 的校验 agent 模式(agents.create + whenIdle
|
|
11
11
|
* 超时 + dispose)。
|
|
12
12
|
*/
|
|
13
|
-
import { randomUUID } from 'node:crypto'
|
|
14
|
-
import { withTimeout, userMessage } from 'dsh-shared'
|
|
15
|
-
import { REVIEW_TIMEOUT_MS } from './constants.js'
|
|
16
|
-
|
|
13
|
+
import { randomUUID } from 'node:crypto';
|
|
14
|
+
import { withTimeout, userMessage } from 'dsh-shared';
|
|
15
|
+
import { REVIEW_TIMEOUT_MS } from './constants.js';
|
|
17
16
|
/** diff 送入 prompt 的最大长度(截断防 token 爆炸)。 */
|
|
18
|
-
const AI_DIFF_MAX = 8000
|
|
19
|
-
|
|
17
|
+
const AI_DIFF_MAX = 8000;
|
|
18
|
+
/** 从 AiAgentOptions 构建 agents.create 所需的 agentOptions 对象。 */
|
|
19
|
+
function buildAgentOptions(opts) {
|
|
20
|
+
if (opts === undefined)
|
|
21
|
+
return {};
|
|
22
|
+
const result = {};
|
|
23
|
+
if (opts.provider !== undefined)
|
|
24
|
+
result.provider = opts.provider;
|
|
25
|
+
if (opts.model !== undefined)
|
|
26
|
+
result.model = opts.model;
|
|
27
|
+
if (opts.cwd !== undefined)
|
|
28
|
+
result.cwd = opts.cwd;
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
20
31
|
/** 运行 AI 审查(尽力而为;任何失败都降级为 failed 标记)。 */
|
|
21
|
-
export async function runAiReview(ctx, diffText, report, timeoutMs = REVIEW_TIMEOUT_MS) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
export async function runAiReview(ctx, diffText, report, timeoutMs = REVIEW_TIMEOUT_MS, agentOptions) {
|
|
33
|
+
const agents = agentsServiceOf(ctx);
|
|
34
|
+
if (agents === undefined)
|
|
35
|
+
return { enabled: true, failed: true, note: 'agents service unavailable' };
|
|
36
|
+
// CodeQL js/insecure-randomness 修复:sessionId 随机段改用 crypto.randomUUID()
|
|
37
|
+
// (密码学安全随机,无需种子),保持 `obs-review-<ts>-<6位hex>` 格式兼容。
|
|
38
|
+
const sessionId = `obs-review-${Date.now()}-${randomUUID().slice(0, 6)}`;
|
|
39
|
+
let handle;
|
|
40
|
+
try {
|
|
41
|
+
handle = await agents.create({
|
|
42
|
+
sessionId,
|
|
43
|
+
meta: { origin: 'subagent', delegationDepth: 1 },
|
|
44
|
+
agentOptions: buildAgentOptions(agentOptions),
|
|
45
|
+
});
|
|
46
|
+
handle.agent.followup(userMessage(aiReviewPrompt(diffText, report)));
|
|
47
|
+
await withTimeout(handle.agent.whenIdle(), timeoutMs);
|
|
48
|
+
const text = lastAssistantText(handle.agent.session);
|
|
49
|
+
// 区分「模型未调用」与「结论解析失败」:
|
|
50
|
+
// - text 为空 → agent 未产出 assistant 消息(模型从未被调用或调用失败)
|
|
51
|
+
// - text 非空但解析失败 → 模型回复了,但结论无法解析
|
|
52
|
+
if (text === '')
|
|
53
|
+
return { enabled: true, failed: true, note: '模型未产出回复(agent 可能未调用模型或模型请求失败)' };
|
|
54
|
+
const parsed = parseAiConclusion(text);
|
|
55
|
+
if (parsed === undefined)
|
|
56
|
+
return { enabled: true, failed: true, note: 'AI 结论解析失败' };
|
|
57
|
+
return { enabled: true, ...parsed };
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return {
|
|
61
|
+
enabled: true,
|
|
62
|
+
failed: true,
|
|
63
|
+
note: error instanceof Error ? error.message : String(error),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await disposeHandle(handle);
|
|
44
68
|
}
|
|
45
|
-
} finally {
|
|
46
|
-
await disposeHandle(handle)
|
|
47
|
-
}
|
|
48
69
|
}
|
|
49
|
-
|
|
50
70
|
/** agents 服务读取(不可用返回 undefined)。 */
|
|
51
71
|
function agentsServiceOf(ctx) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
72
|
+
const agents = ctx.get ? ctx.get('agents') : undefined;
|
|
73
|
+
if (agents === undefined || agents === null || typeof agents.create !== 'function')
|
|
74
|
+
return undefined;
|
|
75
|
+
return agents;
|
|
55
76
|
}
|
|
56
|
-
|
|
57
77
|
/** 释放审查 agent(尽力而为)。 */
|
|
58
78
|
async function disposeHandle(handle) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
79
|
+
if (handle === undefined)
|
|
80
|
+
return;
|
|
81
|
+
try {
|
|
82
|
+
await handle.dispose();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// dispose is best-effort
|
|
86
|
+
}
|
|
65
87
|
}
|
|
66
|
-
|
|
67
88
|
/** 审查 prompt:规则引擎发现 + 截断的增量 diff,要求严格 JSON 输出。 */
|
|
68
89
|
function aiReviewPrompt(diffText, report) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
const diff = diffText.length > AI_DIFF_MAX ? `${diffText.slice(0, AI_DIFF_MAX)}\n…(diff 过长已截断)` : diffText;
|
|
91
|
+
const rules = report.issues
|
|
92
|
+
.map((issue) => `- [${issue.severity}] ${issue.rule} ${issue.file}:${issue.line} ${issue.message}`)
|
|
93
|
+
.join('\n');
|
|
94
|
+
return `你是一个代码审查员。请审查以下提交前的增量 diff。规则引擎已给出参考问题,请结合 diff 给出总评与最重要的补充问题。
|
|
74
95
|
|
|
75
96
|
规则引擎发现:
|
|
76
97
|
${rules === '' ? '(无)' : rules}
|
|
@@ -81,79 +102,82 @@ ${diff}
|
|
|
81
102
|
请严格只输出一个 JSON 对象(不要输出其他内容):
|
|
82
103
|
{"verdict": "approve" 或 "changes", "summary": "50 字以内的总评", "topIssues": ["补充问题 1", "补充问题 2"]}
|
|
83
104
|
|
|
84
|
-
判断标准:存在阻塞性问题(密钥泄露、冲突标记、明显 bug)时 verdict 必须为 changes
|
|
105
|
+
判断标准:存在阻塞性问题(密钥泄露、冲突标记、明显 bug)时 verdict 必须为 changes。`;
|
|
85
106
|
}
|
|
86
|
-
|
|
87
107
|
/** 从 agent 回复提取 JSON 结论(容忍 markdown 代码块包裹)。 */
|
|
88
108
|
function parseAiConclusion(text) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
const json = jsonTextOf(text);
|
|
110
|
+
if (json === undefined)
|
|
111
|
+
return undefined;
|
|
112
|
+
try {
|
|
113
|
+
const parsed = JSON.parse(json);
|
|
114
|
+
if (!isObject(parsed))
|
|
115
|
+
return undefined;
|
|
116
|
+
return {
|
|
117
|
+
verdict: parsed.verdict === 'changes' ? 'changes' : 'approve',
|
|
118
|
+
summary: stringOf(parsed.summary).slice(0, 500),
|
|
119
|
+
topIssues: issuesOf(parsed.topIssues),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return undefined;
|
|
98
124
|
}
|
|
99
|
-
} catch {
|
|
100
|
-
return undefined
|
|
101
|
-
}
|
|
102
125
|
}
|
|
103
|
-
|
|
104
126
|
/** 提取 JSON 文本:裸 JSON 或 ```json 代码块。 */
|
|
105
127
|
function jsonTextOf(text) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
128
|
+
if (typeof text !== 'string' || text === '')
|
|
129
|
+
return undefined;
|
|
130
|
+
const trimmed = text.trim();
|
|
131
|
+
return trimmed.startsWith('{') ? trimmed : extractJsonBlock(trimmed);
|
|
109
132
|
}
|
|
110
|
-
|
|
111
133
|
function isObject(value) {
|
|
112
|
-
|
|
134
|
+
return value !== null && typeof value === 'object';
|
|
113
135
|
}
|
|
114
|
-
|
|
115
136
|
function stringOf(value) {
|
|
116
|
-
|
|
137
|
+
return typeof value === 'string' ? value : '';
|
|
117
138
|
}
|
|
118
|
-
|
|
119
139
|
/** topIssues 规整:字符串数组,上限 10 条。 */
|
|
120
140
|
function issuesOf(raw) {
|
|
121
|
-
|
|
141
|
+
return Array.isArray(raw) ? raw.filter((item) => typeof item === 'string').slice(0, 10) : [];
|
|
122
142
|
}
|
|
123
|
-
|
|
124
143
|
/** 从 ```json ... ``` 代码块提取 JSON 文本。 */
|
|
125
144
|
function extractJsonBlock(text) {
|
|
126
|
-
|
|
127
|
-
|
|
145
|
+
const match = /```(?:json)?\s*([\s\S]*?)```/.exec(text);
|
|
146
|
+
return match !== null ? match[1].trim() : undefined;
|
|
128
147
|
}
|
|
129
|
-
|
|
130
148
|
/** 会话事件快照:新 API snapshotEvents() 优先,旧 API events 兜底(issue #165)。 */
|
|
131
149
|
function sessionEvents(session) {
|
|
132
|
-
|
|
150
|
+
const target = session;
|
|
151
|
+
return target?.snapshotEvents?.() ?? target?.events;
|
|
133
152
|
}
|
|
134
|
-
|
|
135
153
|
function lastAssistantText(session) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
154
|
+
try {
|
|
155
|
+
const events = sessionEvents(session);
|
|
156
|
+
if (!Array.isArray(events))
|
|
157
|
+
return '';
|
|
158
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
159
|
+
const text = assistantTextOf(events[i]);
|
|
160
|
+
if (text !== '')
|
|
161
|
+
return text;
|
|
162
|
+
}
|
|
142
163
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
164
|
+
catch {
|
|
165
|
+
// best-effort
|
|
166
|
+
}
|
|
167
|
+
return '';
|
|
147
168
|
}
|
|
148
|
-
|
|
149
169
|
function assistantTextOf(event) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
170
|
+
const target = event;
|
|
171
|
+
if (target?.type !== 'assistant/message')
|
|
172
|
+
return '';
|
|
173
|
+
const blocks = target.data?.message?.content;
|
|
174
|
+
if (!Array.isArray(blocks))
|
|
175
|
+
return '';
|
|
176
|
+
return blocks
|
|
177
|
+
.filter((block) => block !== null &&
|
|
178
|
+
typeof block === 'object' &&
|
|
179
|
+
block.type === 'text' &&
|
|
180
|
+
typeof block.text === 'string')
|
|
181
|
+
.map((block) => block.text)
|
|
182
|
+
.join('\n');
|
|
159
183
|
}
|