dsh-my-observability 0.1.0

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 ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ 本文件记录 dsh-my-observability 的所有版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
+
5
+ ## [0.1.0] - 2026-08-28
6
+
7
+ ### 新增
8
+
9
+ - **事件审计(issue #53)**:监听 `agent/status`(agent_status,含顶层/子代理标记)、`llm/stream`(llm_stream,同步包装流透传全部 chunk,记录开始/结束/错误 + chunk/字符/耗时统计)、`tools/pre-execute`(tool_call,工具名 + 参数键列表 + 文本摘要截断)、`tools/execute`(tool_result,成功/失败 + 耗时);waterfall 一律透传 `next()`,只读观察不改变流程。
10
+ - **审计持久化**:`$DSH_HOME/observability/audit.json`(防抖 500ms + 原子写 tmp+rename + teardown flush),启动异步加载(加载前事件缓冲不丢),重启后完整恢复;按会话隔离;每会话 2000 条 / 全局 20000 条上限防膨胀。
11
+ - **轨迹回放面板**:侧边栏页签 `dsh-my-observability:replay`(dsh-better-sidebar 服务),会话选择 + 类型过滤(全部/状态/模型流/工具)+ 时间轴事件列表(徽标 + 时间 + 摘要);可见时 5s 轮询、隐藏时暂停。
12
+ - **结构化 Git 工具**:侧边栏页签 `dsh-my-observability:git`——仓库状态(分支 + 暂存/未暂存计数)、差异查看(工作区/暂存区)、类型化提交(Conventional Commits:type 枚举 feat/fix/docs/style/refactor/test/chore + 可选 scope/body,服务端生成消息 + execFile 执行 git 不经 shell)。
13
+ - **增量 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,超时/失败/解析失败降级,规则结果不受影响)。
14
+ - **查询接口**:`GET /observability/api/events`(会话事件,type 过滤 + limit)、`GET /observability/api/sessions`(会话列表)、`GET /observability/api/status`(审计统计 + 开关);全部经 loopback 信任围栏。
15
+ - **测试**:`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%。
16
+
17
+ **真实环境验证**(独立端口 3081 隔离实例):
18
+ - 侧边栏「轨迹回放」页签出现,时间轴展示真实 agent 行为(状态/模型流/工具调用);
19
+ - 「Git 工具」页签:真实仓库状态/差异/类型化提交/提交前审查全流程可用;
20
+ - 重启验证实例后审计事件恢复。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bsfeng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # dsh-my-observability
2
+
3
+ [![插件生态](https://img.shields.io/badge/插件生态-topic%20dsh-4d6bfe)](https://github.com/topics/dsh)
4
+
5
+ <div align="center">
6
+ <img alt="轨迹回放面板:侧边栏时间轴展示 agent 行为(状态/模型流/工具调用)" src="https://unpkg.com/dsh-my-observability/assets/replay-panel.png" width="640" />
7
+ <img alt="Git 工具面板:仓库状态 / 差异 / 提交前审查 / 类型化提交" src="https://unpkg.com/dsh-my-observability/assets/git-panel.png" width="640" />
8
+ </div>
9
+
10
+ **DSH 可观测性 + Git 工程工具插件**:**事件审计**记录 agent 的每一次行为(状态变化 / 模型流 / 工具调用与结果),**轨迹回放面板**在侧边栏按时间轴回放会话轨迹;**结构化 Git 工具**提供 Conventional Commits 类型化提交;**增量 diff 审查**在提交前用规则引擎(可选 AI 增强)检查调试残留、密钥泄露等问题。
11
+
12
+ ## 功能
13
+
14
+ ### 1. 事件审计(agent 行为可追溯)
15
+
16
+ Server 端只读观察 DSH 生命周期事件并记录审计日志:
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.json`(防抖 + 原子写),重启后完整恢复;
27
+ - **防膨胀**:每会话最多 2000 条(FIFO 淘汰)、全局 20000 条(轮转淘汰);
28
+ - **只读观察**:waterfall 事件一律透传 `next()`,绝不改变工具/模型流程。
29
+
30
+ ### 2. 轨迹回放面板(时间轴)
31
+
32
+ 侧边栏「轨迹回放」页签:选择会话 + 类型过滤(全部 / 状态 / 模型流 / 工具),按时间轴展示 agent 行为(类型徽标 + 时间 + 摘要);面板可见时 5s 轮询、隐藏时暂停。
33
+
34
+ ### 3. 结构化 Git 工具(类型化提交)
35
+
36
+ 侧边栏「Git 工具」页签:输入仓库路径 → 查看状态(分支 + 暂存/未暂存计数)与差异(工作区/暂存区)→ **类型化提交**:
37
+
38
+ - 类型枚举:`feat` / `fix` / `docs` / `style` / `refactor` / `test` / `chore`;
39
+ - 消息格式:`<type>(<scope>): <description>`(scope 可选,body 可选);
40
+ - 服务端生成消息 + `execFile` 执行 git(不经 shell),提交历史清晰可追溯。
41
+
42
+ ### 4. 增量 diff 审查(提交前审查)
43
+
44
+ 「提交前审查」按钮对增量 diff 运行规则引擎(确定性、可测试):
45
+
46
+ | 规则 | 严重级别 | 检查内容 |
47
+ |------|---------|---------|
48
+ | `secret-leak` | error | 密钥/凭据硬编码(password/api_key/secret/token) |
49
+ | `conflict-marker` | error | 合并冲突标记残留(`<<<<<<<` / `=======` / `>>>>>>>`) |
50
+ | `debug-statement` | warning | 调试残留(console.* / debugger / print 族) |
51
+ | `large-diff` | warning | 单文件变更 > 300 行 |
52
+ | `binary-file` | warning | 二进制文件变更 |
53
+ | `todo-marker` | info | TODO/FIXME/HACK 标记 |
54
+ | `trailing-space` | info | 行尾多余空格 |
55
+ | `no-test-change` | info | 有源码变更但无测试变更 |
56
+
57
+ - **可选 AI 审查**:配置 `aiReview`(默认开)且 agents 服务可用时,创建独立审查 agent 阅读 diff 输出总评(verdict/summary/topIssues);超时/失败/解析失败自动降级,规则引擎结果不受影响(AI 是增强,不是门禁)。
58
+
59
+ ## 工作原理
60
+
61
+ - **Server 端**(`lib/index.js`):`audit.js` 监听四类事件 → `store.js` 按会话分桶持久化;`git.js` 类型化提交(execFile);`review.js` + `diff.js` 规则引擎;`ai.js` 可选 AI 增强;`routes.js` 提供 `/observability/api` 路由(全部经 loopback 信任围栏)。
62
+ - **Client 端**(`lib/client.js`):两个侧边栏页签(`dsh-my-observability:replay` 轨迹回放、`dsh-my-observability:git` Git 工具),样式走 DSH 语义 token,随 fiber 卸载无残留。
63
+
64
+ ## 安装
65
+
66
+ > 💡 **npm 安装(普通用户推荐)**:`dsh plugin --profile web add dsh-my-observability`——无需克隆本仓库;以下 link 方式供本仓库开发者使用。
67
+
68
+ ```bash
69
+ # 1) 克隆本仓库(任意目录)
70
+ git clone https://github.com/baosfeng/my-dsh-plugins.git
71
+ # 2) 以本地 link 方式安装(将 <仓库路径> 替换为上面的克隆目录)
72
+ dsh plugin --profile web add link:<仓库路径>/plugins/dsh-my-observability
73
+ ```
74
+
75
+ - server 端改动需重启 `dsh web`;client 端改动浏览器硬刷新(Cmd/Ctrl+Shift+R)即可。
76
+
77
+ ## 配置
78
+
79
+ 插件级配置(`cordis.patch.yml` 对应插件行的 `config` 字段,均为可选):
80
+
81
+ ```yaml
82
+ - insert:
83
+ - id: observability
84
+ name: 'dsh-my-observability'
85
+ - config: # 传给 apply(ctx, config)
86
+ aiReview: true # 增量 diff 审查启用 AI 增强(默认 true;agents 服务不可用时自动降级)
87
+ aiTimeoutMs: 60000 # AI 审查超时(毫秒,默认 60000)
88
+ ```
89
+
90
+ ## 依赖
91
+
92
+ | 依赖 | 用途 | 可选 |
93
+ |------|------|------|
94
+ | `cordis` | 插件运行时 | 是(宿主提供) |
95
+ | `react` | client 端组件 | 是(宿主提供) |
96
+
97
+ ## 限制与说明
98
+
99
+ - **Git 工具作用于本机仓库**:仓库路径由你在面板中输入(localStorage 记住),仅本机可访问(loopback 围栏)。
100
+ - **AI 审查需要 agents 服务**:不可用时审查自动降级为纯规则引擎结果,不影响使用。
101
+ - **审计上限**:每会话 2000 条 / 全局 20000 条,超出自动淘汰最旧事件。
102
+
103
+ ## 相关文档
104
+
105
+ → [可观测性模块文档](../../docs/可观测性/概述.md) · [需求清单](../../docs/可观测性/需求清单.md) · [CHANGELOG](CHANGELOG.md)
Binary file
Binary file
@@ -0,0 +1,10 @@
1
+ # dsh-my-observability bundle patch: inserts the plugin row into the profile roster.
2
+ # The row mounts the host half (agent event audit + trajectory store + git
3
+ # tooling + diff review) and the client half (sidebar replay timeline + git
4
+ # panel). Install with:
5
+ #
6
+ # git clone https://github.com/baosfeng/my-dsh-plugins.git
7
+ # dsh plugin --profile web add link:<仓库路径>/plugins/dsh-my-observability
8
+ - insert:
9
+ - id: observability
10
+ name: 'dsh-my-observability'
package/lib/ai.js ADDED
@@ -0,0 +1,165 @@
1
+ /**
2
+ * dsh-my-observability — AI-augmented diff review (optional).
3
+ *
4
+ * 规则引擎(lib/review.js)给出确定性结果后,若 agents 服务可用且配置
5
+ * 开启 aiReview,创建独立审查 agent 阅读增量 diff 并输出 JSON 结论:
6
+ * - 成功:报告追加 { enabled, verdict, summary, topIssues }
7
+ * - 超时 / agents 不可用 / 结论解析失败:降级 { enabled, failed, note },
8
+ * 规则引擎结果不受影响(AI 是增强,不是门禁)
9
+ *
10
+ * 参考 dsh-task-reliability 的校验 agent 模式(agents.create + whenIdle
11
+ * 超时 + dispose)。
12
+ */
13
+ import { REVIEW_TIMEOUT_MS } from './constants.js'
14
+
15
+ /** diff 送入 prompt 的最大长度(截断防 token 爆炸)。 */
16
+ const AI_DIFF_MAX = 8000
17
+
18
+ /** 运行 AI 审查(尽力而为;任何失败都降级为 failed 标记)。 */
19
+ export async function runAiReview(ctx, diffText, report, timeoutMs = REVIEW_TIMEOUT_MS) {
20
+ const agents = agentsServiceOf(ctx)
21
+ if (agents === undefined) return { enabled: true, failed: true, note: 'agents service unavailable' }
22
+ const sessionId = `obs-review-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
23
+ let handle
24
+ try {
25
+ handle = await agents.create({
26
+ sessionId,
27
+ meta: { origin: 'subagent', delegationDepth: 1 },
28
+ agentOptions: {},
29
+ })
30
+ handle.agent.followup(userMessage(aiReviewPrompt(diffText, report)))
31
+ await withTimeout(handle.agent.whenIdle(), timeoutMs)
32
+ const parsed = parseAiConclusion(lastAssistantText(handle.agent.session))
33
+ if (parsed === undefined) return { enabled: true, failed: true, note: 'AI 结论解析失败' }
34
+ return { enabled: true, ...parsed }
35
+ } catch (error) {
36
+ return { enabled: true, failed: true, note: error instanceof Error ? error.message : String(error) }
37
+ } finally {
38
+ await disposeHandle(handle)
39
+ }
40
+ }
41
+
42
+ /** agents 服务读取(不可用返回 undefined)。 */
43
+ function agentsServiceOf(ctx) {
44
+ const agents = ctx.get ? ctx.get('agents') : undefined
45
+ if (agents === undefined || agents === null || typeof agents.create !== 'function') return undefined
46
+ return agents
47
+ }
48
+
49
+ /** 释放审查 agent(尽力而为)。 */
50
+ async function disposeHandle(handle) {
51
+ if (handle === undefined) return
52
+ try {
53
+ await handle.dispose()
54
+ } catch {
55
+ // dispose is best-effort
56
+ }
57
+ }
58
+
59
+ /** 审查 prompt:规则引擎发现 + 截断的增量 diff,要求严格 JSON 输出。 */
60
+ function aiReviewPrompt(diffText, report) {
61
+ const diff = diffText.length > AI_DIFF_MAX ? `${diffText.slice(0, AI_DIFF_MAX)}\n…(diff 过长已截断)` : diffText
62
+ const rules = report.issues
63
+ .map((issue) => `- [${issue.severity}] ${issue.rule} ${issue.file}:${issue.line} ${issue.message}`)
64
+ .join('\n')
65
+ return `你是一个代码审查员。请审查以下提交前的增量 diff。规则引擎已给出参考问题,请结合 diff 给出总评与最重要的补充问题。
66
+
67
+ 规则引擎发现:
68
+ ${rules === '' ? '(无)' : rules}
69
+
70
+ 增量 diff:
71
+ ${diff}
72
+
73
+ 请严格只输出一个 JSON 对象(不要输出其他内容):
74
+ {"verdict": "approve" 或 "changes", "summary": "50 字以内的总评", "topIssues": ["补充问题 1", "补充问题 2"]}
75
+
76
+ 判断标准:存在阻塞性问题(密钥泄露、冲突标记、明显 bug)时 verdict 必须为 changes。`
77
+ }
78
+
79
+ /** 从 agent 回复提取 JSON 结论(容忍 markdown 代码块包裹)。 */
80
+ function parseAiConclusion(text) {
81
+ const json = jsonTextOf(text)
82
+ if (json === undefined) return undefined
83
+ try {
84
+ const parsed = JSON.parse(json)
85
+ if (!isObject(parsed)) return undefined
86
+ return {
87
+ verdict: parsed.verdict === 'changes' ? 'changes' : 'approve',
88
+ summary: stringOf(parsed.summary).slice(0, 500),
89
+ topIssues: issuesOf(parsed.topIssues),
90
+ }
91
+ } catch {
92
+ return undefined
93
+ }
94
+ }
95
+
96
+ /** 提取 JSON 文本:裸 JSON 或 ```json 代码块。 */
97
+ function jsonTextOf(text) {
98
+ if (typeof text !== 'string' || text === '') return undefined
99
+ const trimmed = text.trim()
100
+ return trimmed.startsWith('{') ? trimmed : extractJsonBlock(trimmed)
101
+ }
102
+
103
+ function isObject(value) {
104
+ return value !== null && typeof value === 'object'
105
+ }
106
+
107
+ function stringOf(value) {
108
+ return typeof value === 'string' ? value : ''
109
+ }
110
+
111
+ /** topIssues 规整:字符串数组,上限 10 条。 */
112
+ function issuesOf(raw) {
113
+ return Array.isArray(raw) ? raw.filter((item) => typeof item === 'string').slice(0, 10) : []
114
+ }
115
+
116
+ /** 从 ```json ... ``` 代码块提取 JSON 文本。 */
117
+ function extractJsonBlock(text) {
118
+ const match = /```(?:json)?\s*([\s\S]*?)```/.exec(text)
119
+ return match !== null ? match[1].trim() : undefined
120
+ }
121
+
122
+ // ── helpers(与 dsh-task-reliability 同契约的轻量实现)────────────────────
123
+
124
+ function withTimeout(promise, ms) {
125
+ return new Promise((resolve) => {
126
+ const timer = setTimeout(() => resolve(undefined), ms)
127
+ Promise.resolve(promise).then(
128
+ (value) => { clearTimeout(timer); resolve(value) },
129
+ () => { clearTimeout(timer); resolve(undefined) },
130
+ )
131
+ })
132
+ }
133
+
134
+ function userMessage(text) {
135
+ return {
136
+ id: `msg-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`,
137
+ role: 'user',
138
+ content: [{ type: 'text', text }],
139
+ source: { kind: 'user' },
140
+ }
141
+ }
142
+
143
+ function lastAssistantText(session) {
144
+ try {
145
+ const events = session?.events
146
+ if (!Array.isArray(events)) return ''
147
+ for (let i = events.length - 1; i >= 0; i--) {
148
+ const text = assistantTextOf(events[i])
149
+ if (text !== '') return text
150
+ }
151
+ } catch {
152
+ // best-effort
153
+ }
154
+ return ''
155
+ }
156
+
157
+ function assistantTextOf(event) {
158
+ if (event?.type !== 'assistant/message') return ''
159
+ const blocks = event.data?.message?.content
160
+ if (!Array.isArray(blocks)) return ''
161
+ return blocks
162
+ .filter((block) => block !== null && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string')
163
+ .map((block) => block.text)
164
+ .join('\n')
165
+ }
package/lib/audit.js ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * dsh-my-observability — agent event audit listeners.
3
+ *
4
+ * 只读观察 DSH 生命周期事件并记录审计日志(agent 行为可追溯):
5
+ * - `agent/status` → `agent_status`(状态变化,含顶层/子代理标记)
6
+ * - `llm/stream` → `llm_stream`(流开始/结束/错误 + chunk/字符统计;
7
+ * waterfall,包装流透传全部 chunk)
8
+ * - `tools/pre-execute` → `tool_call`(工具调用开始 + 参数摘要;透传 next)
9
+ * - `tools/execute` → `tool_result`(工具结果 ok/失败 + 耗时;透传 next)
10
+ *
11
+ * ⚠️ llm/stream 监听器必须保持同步函数:cordis waterfall 不 await listener
12
+ * 返回值,next() 同步返回流;async listener 会让消费方(vision-toolkit 等
13
+ * yield* 委托)拿到 Promise 而崩溃。tools/* 同理必须调用 next()。
14
+ */
15
+ import { MAX_ARG_KEYS, MAX_TEXT_LEN } from './constants.js'
16
+
17
+ /** 注册全部审计监听;返回 disposer 数组(全部经 ctx.on 注册)。 */
18
+ export function attachAuditListeners(ctx, record) {
19
+ return [
20
+ ctx.on('agent/status', (payload) => handleStatus(payload, record)),
21
+ ctx.on('llm/stream', (options, next) => handleStream(options, next, record)),
22
+ ctx.on('tools/pre-execute', (exec, next) => handlePreExecute(exec, next, record)),
23
+ ctx.on('tools/execute', (exec, next) => handleExecute(exec, next, record)),
24
+ ]
25
+ }
26
+
27
+ /** agent/status → agent_status 事件(含顶层/子代理标记)。 */
28
+ function handleStatus(payload, record) {
29
+ const agent = payload?.agent
30
+ if (agent === null || typeof agent !== 'object') return
31
+ record({
32
+ type: 'agent_status',
33
+ sessionId: typeof agent.id === 'string' ? agent.id : '',
34
+ data: { status: String(payload?.status ?? ''), agentType: agentTypeOf(agent) },
35
+ })
36
+ }
37
+
38
+ /** llm/stream → 包装流(同步返回;无 sessionId 时原样透传)。 */
39
+ function handleStream(options, next, record) {
40
+ const sessionId = typeof options?.sessionId === 'string' ? options.sessionId : ''
41
+ if (sessionId === '') return next()
42
+ const stream = next()
43
+ return wrapStream(sessionId, stream, record)
44
+ }
45
+
46
+ /** tools/pre-execute → tool_call 事件(透传 next)。 */
47
+ async function handlePreExecute(exec, next, record) {
48
+ if (exec !== null && typeof exec === 'object' && typeof exec.agent?.id === 'string') {
49
+ record({
50
+ type: 'tool_call',
51
+ sessionId: exec.agent.id,
52
+ data: { name: String(exec.name ?? ''), args: summarizeArguments(exec.arguments) },
53
+ })
54
+ }
55
+ return next()
56
+ }
57
+
58
+ /** tools/execute → tool_result 事件(透传 next 结果)。 */
59
+ async function handleExecute(exec, next, record) {
60
+ const sessionId = exec?.agent?.id
61
+ const name = exec?.name
62
+ if (typeof sessionId !== 'string' || sessionId === '') return next()
63
+ const startedAt = Date.now()
64
+ const result = await next()
65
+ record({
66
+ type: 'tool_result',
67
+ sessionId,
68
+ data: {
69
+ name: String(name ?? ''),
70
+ ok: isToolOk(result),
71
+ ms: Date.now() - startedAt,
72
+ },
73
+ })
74
+ return result
75
+ }
76
+
77
+ /** 顶层/子代理标记(白名单化:任何子代理标记命中即 subagent)。 */
78
+ function agentTypeOf(agent) {
79
+ const header = agent.session?.header
80
+ if (header === undefined || header === null) return 'unknown'
81
+ if (header.origin === 'subagent') return 'subagent'
82
+ if (typeof header.delegationDepth === 'number' && header.delegationDepth > 0) return 'subagent'
83
+ return typeof agent.options?.subagentDepth === 'number' && agent.options.subagentDepth > 0
84
+ ? 'subagent'
85
+ : 'top'
86
+ }
87
+
88
+ /** 工具结果 ok 判定:非对象视为成功;error 字段非空视为失败。 */
89
+ function isToolOk(result) {
90
+ if (result === null || typeof result !== 'object') return true
91
+ const error = result.error
92
+ if (error === undefined || error === null) return true
93
+ return typeof error === 'object' && Object.keys(error).length === 0
94
+ }
95
+
96
+ /** 参数摘要:键列表(上限)+ 主要文本参数摘要(上限),防审计膨胀。 */
97
+ function summarizeArguments(args) {
98
+ if (args === null || typeof args !== 'object') return { keys: [] }
99
+ const keys = Object.keys(args).slice(0, MAX_ARG_KEYS)
100
+ const summary = textSummaryOf(args)
101
+ return summary === '' ? { keys } : { keys, summary }
102
+ }
103
+
104
+ /** 主要文本参数(command/message/content/question 等)的截断摘要。 */
105
+ function textSummaryOf(args) {
106
+ for (const key of ['command', 'message', 'content', 'question', 'description', 'prompt']) {
107
+ const value = args[key]
108
+ if (typeof value === 'string' && value !== '') return truncate(value)
109
+ }
110
+ return ''
111
+ }
112
+
113
+ /** 截断长文本(保留首段,去换行)。 */
114
+ function truncate(text) {
115
+ const oneLine = text.split('\n')[0].trim()
116
+ return oneLine.length > MAX_TEXT_LEN ? `${oneLine.slice(0, MAX_TEXT_LEN)}…` : oneLine
117
+ }
118
+
119
+ /** 包装 llm 流:透传全部 chunk,记录开始/结束/错误统计。 */
120
+ function wrapStream(sessionId, stream, record) {
121
+ let chunks = 0
122
+ let chars = 0
123
+ const startedAt = Date.now()
124
+ record({ type: 'llm_stream', sessionId, data: { phase: 'start' } })
125
+ return (async function* () {
126
+ try {
127
+ for await (const chunk of stream) {
128
+ chunks += 1
129
+ if (chunk !== null && typeof chunk === 'object' && typeof chunk.text === 'string') {
130
+ chars += chunk.text.length
131
+ }
132
+ yield chunk
133
+ }
134
+ record({ type: 'llm_stream', sessionId, data: streamSummary('end', startedAt, chunks, chars) })
135
+ } catch (error) {
136
+ record({ type: 'llm_stream', sessionId, data: streamSummary('error', startedAt, chunks, chars, error) })
137
+ throw error
138
+ }
139
+ })()
140
+ }
141
+
142
+ /** 流结束/错误统计(错误消息截断)。 */
143
+ function streamSummary(phase, startedAt, chunks, chars, error) {
144
+ const summary = { phase, chunks, chars, ms: Date.now() - startedAt }
145
+ if (error !== undefined) {
146
+ const message = error instanceof Error ? error.message : String(error)
147
+ summary.message = message.length > MAX_TEXT_LEN ? `${message.slice(0, MAX_TEXT_LEN)}…` : message
148
+ }
149
+ return summary
150
+ }