@furongjun1999/dsh-memory 0.2.8 → 0.3.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/src/tools.ts ADDED
@@ -0,0 +1,226 @@
1
+ /**
2
+ * 工具桥接:运行时从灵枢拉取工具清单(tools/list),把 JSON Schema
3
+ * 转换为 dsh-tools 的 ParameterSchemaSpec,经 defineTool 注册进 ctx.tools。
4
+ *
5
+ * 动态拉取意味着灵枢库升级新增工具后,DSH 侧零改动即可获得新能力。
6
+ */
7
+
8
+ import type { Context } from '@deepseek-ai/cordis'
9
+ import { defineTool, type ParameterPropertySpec, type ParameterSchemaSpec, type ValueSchemaSpec } from '@deepseek-ai/dsh-tools'
10
+ import type { LingshuBridge, McpTool } from './bridge.ts'
11
+
12
+ /** 默认暴露的核心工具集合(记忆/推理/摄取/元认知)。 */
13
+ export const CORE_TOOLS = [
14
+ 'remember', // 写入记忆
15
+ 'recall', // 组合联想召回
16
+ 'search', // 内容检索
17
+ 'timeline', // 记忆时间线
18
+ 'think', // 推理前记忆注入
19
+ 'relate', // 建立关系边
20
+ 'predict_routes', // 生成式预测
21
+ 'ingest_text', // 外部知识摄取
22
+ 'ingest_url', // URL 摄取
23
+ 'session_note', // 会话要点外部化
24
+ 'self_check', // 完整性自检
25
+ 'service_info', // 服务状态
26
+ ] as const
27
+
28
+ /**
29
+ * 大脑模式工具集(轻量版:去掉身体的完整大脑)。
30
+ * 保留灵枢全部心智能力(记忆/认知/推理/学习/飞轮/反思/长期记忆门),
31
+ * 排除身体/视觉类工具(body/device_call/see/world3d 等)。
32
+ */
33
+ export const BRAIN_TOOLS = [
34
+ // 记忆
35
+ 'remember', 'recall', 'search', 'timeline',
36
+ 'session_note', 'session_recall', 'compact_context',
37
+ // 推理与关系
38
+ 'think', 'relate', 'reason', 'predict_routes',
39
+ // 认知与元认知
40
+ 'self_check', 'gap_trend', 'cognition', 'cognition_report',
41
+ 'emotional_bias', 'self_reliability', 'action_log', 'preflight',
42
+ // 反思
43
+ 'recursive_reflect',
44
+ // 学习与盲区
45
+ 'blindspots', 'learn', 'induce',
46
+ // 知识飞轮
47
+ 'distill', 'flywheel_report', 'transfer_test', 'calibrate',
48
+ // 外部知识摄取
49
+ 'ingest_text', 'ingest_file', 'ingest_url', 'web_search',
50
+ // 生命周期
51
+ 'step', 'lifecycle_state',
52
+ // 长期记忆门(v1.15:重要性评估主动沉淀)
53
+ 'longterm_snapshot', 'promote_memories',
54
+ // 服务
55
+ 'service_info',
56
+ ] as const
57
+
58
+ /** tools 配置:'core' | 'brain' | 'all' | 显式名称数组。 */
59
+ export type ToolSelection = 'core' | 'brain' | 'all' | string[]
60
+
61
+ /** P1 修复(GPT 审查):只读/无副作用工具才允许并发——写操作(记忆/关系/
62
+ * 生命周期/摄取/学习)标 false,防止 DSH 并行调用导致 SQLite 写入竞争、
63
+ * 状态顺序错乱、关系边重复等。 */
64
+ const READ_TOOLS = new Set([
65
+ 'recall', 'search', 'timeline', 'think', 'reason', 'predict_routes',
66
+ 'self_check', 'service_info', 'session_recall', 'gap_trend', 'transfer_test',
67
+ 'cognition_report', 'self_reliability', 'emotional_bias', 'flywheel_report',
68
+ 'distill', 'insight_report', 'prediction_stats', 'blindspots', 'pattern_separation',
69
+ ])
70
+
71
+ /** 按工具名判定并发安全(只读查询 true;写操作 false) */
72
+ export function isToolConcurrencySafe(name: string): boolean {
73
+ return READ_TOOLS.has(name)
74
+ }
75
+
76
+ /**
77
+ * P1 完善(GPT 审查·tools:all 自动扩权):即使 selection='all' 也排除的宿主级
78
+ * 风险工具——此前后端新增工具即自动暴露给 Agent(动态扩权无 denylist)。
79
+ * 排除原则:宿主命令执行/权限终裁/外部设备/自主生命周期控制/角色卡写入。
80
+ * 显式名称数组(显式配置)不受此名单限制(配置者已明确选择)。
81
+ */
82
+ export const RISK_TOOLS = new Set([
83
+ 'run_command', // 宿主命令执行(安全边界:不由 Agent 动态调用)
84
+ 'designer_decide', // 设计者裁决(fail-closed 权限,绝不由 Agent 调用)
85
+ 'device_call', // 外部设备统一调用(屏幕/进程/音频/浏览器)
86
+ 'see', // 视觉感知(身体工具)
87
+ 'world3d', // 时空 3D 重建(身体工具)
88
+ 'vprim', // 视觉原语(身体工具)
89
+ 'visual_check', // 视觉面检查(身体工具)
90
+ 'start_lifecycle', // 启动自主生命周期循环
91
+ 'stop_lifecycle', // 中断生命周期循环
92
+ 'web_ingest_search', // 外部搜索摄取(网络调用 + 写知识层)
93
+ 'role_create', // 角色卡创建(写角色数据)
94
+ 'role_import', // 角色导入(写角色数据)
95
+ 'role_block', // 角色扮演注入块组装
96
+ ])
97
+
98
+ /** 按配置筛选工具名('all' 时排除 RISK_TOOLS 宿主级危险工具)。 */
99
+ export function selectTools(all: string[], selection: ToolSelection): string[] {
100
+ if (selection === 'all') return all.filter((name) => !RISK_TOOLS.has(name))
101
+ const allowed = new Set(
102
+ selection === 'core' ? CORE_TOOLS
103
+ : selection === 'brain' ? BRAIN_TOOLS
104
+ : selection,
105
+ )
106
+ return all.filter((name) => allowed.has(name))
107
+ }
108
+
109
+ /** 把 MCP JSON Schema 的属性表转换为 ParameterSchemaSpec。 */
110
+ export function schemaToParameters(inputSchema: Record<string, unknown> | undefined): ParameterSchemaSpec {
111
+ if (!inputSchema || typeof inputSchema !== 'object') return {}
112
+ const properties = inputSchema['properties'] as Record<string, unknown> | undefined
113
+ if (!properties || typeof properties !== 'object') return {}
114
+ const required = new Set(Array.isArray(inputSchema['required']) ? (inputSchema['required'] as string[]) : [])
115
+ const spec: Record<string, ParameterPropertySpec> = {}
116
+ for (const [key, raw] of Object.entries(properties)) {
117
+ const value = toValueSpec(raw)
118
+ if (required.has(key)) {
119
+ spec[key] = { ...value, required: true as const }
120
+ } else {
121
+ spec[key] = value
122
+ }
123
+ }
124
+ return spec
125
+ }
126
+
127
+ /** 递归转换单个 JSON Schema 节点为 ValueSchemaSpec。 */
128
+ function toValueSpec(raw: unknown): ValueSchemaSpec {
129
+ if (typeof raw !== 'object' || raw === null) return { type: 'json' }
130
+ const node = raw as Record<string, unknown>
131
+ const annotations: { description?: string } = {}
132
+ if (typeof node['description'] === 'string') annotations.description = node['description'] as string
133
+ const type = node['type']
134
+ const enumValues = Array.isArray(node['enum']) ? (node['enum'] as unknown[]) : undefined
135
+ switch (type) {
136
+ case 'string':
137
+ return { type: 'string', ...annotations, ...(enumValues ? { enum: enumValues as string[] } : {}) }
138
+ case 'number':
139
+ return { type: 'number', ...annotations }
140
+ case 'integer':
141
+ return { type: 'integer', ...annotations }
142
+ case 'boolean':
143
+ return { type: 'boolean', ...annotations }
144
+ case 'null':
145
+ return { type: 'null', ...annotations }
146
+ case 'array': {
147
+ const spec: { type: 'array'; items?: ValueSchemaSpec } & typeof annotations = { type: 'array', ...annotations }
148
+ if (node['items'] !== undefined) spec.items = toValueSpec(node['items'])
149
+ return spec
150
+ }
151
+ case 'object': {
152
+ const props = schemaToParameters(node)
153
+ // P1 修复(GPT 审查):尊重后端 additionalProperties 声明(false 保留),
154
+ // 不再无条件 true——此前后端写 false 也会被覆盖成 true,DSH 侧认为
155
+ // 参数合法但后端拒绝。
156
+ const additional = node['additionalProperties'] === false ? false : true
157
+ return { type: 'object', properties: props, additionalProperties: additional, ...annotations }
158
+ }
159
+ default:
160
+ return { type: 'json', ...annotations }
161
+ }
162
+ }
163
+
164
+ /** 从灵枢 content 数组中提取文本(MCP text block 拼接)。 */
165
+ export function extractText(content: Array<{ type: string; text?: string; [key: string]: unknown }>): string {
166
+ return content
167
+ .map((block) => (block.type === 'text' && typeof block.text === 'string' ? block.text : ''))
168
+ .filter(Boolean)
169
+ .join('\n')
170
+ }
171
+
172
+ /** 注册灵枢工具到 ctx.tools;返回取消注册函数。 */
173
+ export async function registerLingshuTools(
174
+ ctx: Context,
175
+ bridge: LingshuBridge,
176
+ opts: { selection: ToolSelection; toolPrefix: string },
177
+ ): Promise<() => void> {
178
+ const tools = await bridge.listTools()
179
+ const wanted = new Set(selectTools(tools.map((t) => t.name), opts.selection))
180
+ const disposers: Array<() => void> = []
181
+ const registered: string[] = []
182
+ try {
183
+ for (const tool of tools) {
184
+ if (!wanted.has(tool.name)) continue
185
+ const publicName = `${opts.toolPrefix}${tool.name}`
186
+ const definition = defineTool({
187
+ name: publicName,
188
+ description: tool.description || `灵枢 ${tool.name}`,
189
+ parameters: schemaToParameters(tool.inputSchema),
190
+ output: {
191
+ schema: { type: 'json' } as never,
192
+ render(_args, value) {
193
+ return [{ type: 'text', text: extractText((value as McpCallResultLike).content ?? []) }]
194
+ },
195
+ },
196
+ timeoutMs: 120_000,
197
+ // P1 修复(GPT 审查):按工具分类——只读查询可并发,写操作串行
198
+ isConcurrencySafe: () => isToolConcurrencySafe(tool.name),
199
+ // P1 修复(GPT 审查):接收 exec.signal(用户取消/上层超时)——
200
+ // 此前完全忽略取消,取消后写操作(remember/relate/ingest 等)仍可能产生副作用
201
+ async execute(args: Record<string, unknown>, exec: { signal: AbortSignal }) {
202
+ if (exec.signal.aborted) throw new Error(`灵枢 ${tool.name} 已取消`)
203
+ const result = await bridge.callTool(tool.name, args as Record<string, unknown>, exec.signal)
204
+ if (exec.signal.aborted) throw new Error(`灵枢 ${tool.name} 已取消`)
205
+ if (result.isError) {
206
+ throw new Error(extractText(result.content) || `灵枢 ${tool.name} 执行失败`)
207
+ }
208
+ return { content: result.content } as never
209
+ },
210
+ })
211
+ disposers.push(ctx.tools.register(definition))
212
+ registered.push(publicName)
213
+ }
214
+ } catch (err) {
215
+ for (const dispose of disposers) dispose()
216
+ throw err
217
+ }
218
+ ctx.logger.info(`dsh-memory: 已注册 ${registered.length} 个灵枢工具(${registered.join(', ')})`)
219
+ return () => {
220
+ for (const dispose of disposers) dispose()
221
+ }
222
+ }
223
+
224
+ interface McpCallResultLike {
225
+ content: Array<{ type: string; text?: string; [key: string]: unknown }>
226
+ }