@zhin.js/core 1.0.37 → 1.0.39

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.
Files changed (204) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +57 -3
  3. package/lib/adapter.d.ts +11 -0
  4. package/lib/adapter.d.ts.map +1 -1
  5. package/lib/adapter.js +61 -0
  6. package/lib/adapter.js.map +1 -1
  7. package/lib/ai/index.d.ts +3 -39
  8. package/lib/ai/index.d.ts.map +1 -1
  9. package/lib/ai/index.js +2 -44
  10. package/lib/ai/index.js.map +1 -1
  11. package/lib/ai/providers/anthropic.d.ts.map +1 -1
  12. package/lib/ai/providers/anthropic.js +2 -0
  13. package/lib/ai/providers/anthropic.js.map +1 -1
  14. package/lib/ai/providers/openai.d.ts.map +1 -1
  15. package/lib/ai/providers/openai.js +8 -0
  16. package/lib/ai/providers/openai.js.map +1 -1
  17. package/lib/ai/types.d.ts +5 -3
  18. package/lib/ai/types.d.ts.map +1 -1
  19. package/lib/built/ai-trigger.js.map +1 -1
  20. package/lib/built/common-adapter-tools.d.ts +55 -0
  21. package/lib/built/common-adapter-tools.d.ts.map +1 -0
  22. package/lib/built/common-adapter-tools.js +158 -0
  23. package/lib/built/common-adapter-tools.js.map +1 -0
  24. package/lib/built/dispatcher.d.ts.map +1 -1
  25. package/lib/built/dispatcher.js +50 -46
  26. package/lib/built/dispatcher.js.map +1 -1
  27. package/lib/built/skill.d.ts.map +1 -1
  28. package/lib/built/skill.js +0 -1
  29. package/lib/built/skill.js.map +1 -1
  30. package/lib/built/tool.d.ts +3 -3
  31. package/lib/built/tool.d.ts.map +1 -1
  32. package/lib/built/tool.js.map +1 -1
  33. package/lib/feature.d.ts +16 -1
  34. package/lib/feature.d.ts.map +1 -1
  35. package/lib/feature.js +41 -2
  36. package/lib/feature.js.map +1 -1
  37. package/lib/index.d.ts +1 -0
  38. package/lib/index.d.ts.map +1 -1
  39. package/lib/index.js +2 -0
  40. package/lib/index.js.map +1 -1
  41. package/lib/plugin.d.ts +38 -1
  42. package/lib/plugin.d.ts.map +1 -1
  43. package/lib/plugin.js +73 -22
  44. package/lib/plugin.js.map +1 -1
  45. package/lib/scheduler/scheduler.js +1 -1
  46. package/lib/scheduler/scheduler.js.map +1 -1
  47. package/lib/types.d.ts +43 -28
  48. package/lib/types.d.ts.map +1 -1
  49. package/lib/utils.d.ts +12 -3
  50. package/lib/utils.d.ts.map +1 -1
  51. package/lib/utils.js +64 -54
  52. package/lib/utils.js.map +1 -1
  53. package/package.json +5 -5
  54. package/src/adapter.ts +85 -5
  55. package/src/ai/index.ts +8 -186
  56. package/src/ai/providers/anthropic.ts +1 -0
  57. package/src/ai/providers/openai.ts +5 -1
  58. package/src/ai/types.ts +6 -4
  59. package/src/built/ai-trigger.ts +2 -2
  60. package/src/built/common-adapter-tools.ts +207 -0
  61. package/src/built/dispatcher.ts +51 -52
  62. package/src/built/skill.ts +3 -4
  63. package/src/built/tool.ts +3 -3
  64. package/src/feature.ts +45 -2
  65. package/src/index.ts +2 -0
  66. package/src/plugin.ts +92 -31
  67. package/src/scheduler/scheduler.ts +1 -1
  68. package/src/types.ts +39 -28
  69. package/src/utils.ts +63 -52
  70. package/tests/ai/setup.ts +2 -2
  71. package/tests/utils.test.ts +1 -3
  72. package/lib/ai/agent.d.ts +0 -130
  73. package/lib/ai/agent.d.ts.map +0 -1
  74. package/lib/ai/agent.js +0 -702
  75. package/lib/ai/agent.js.map +0 -1
  76. package/lib/ai/bootstrap.d.ts +0 -91
  77. package/lib/ai/bootstrap.d.ts.map +0 -1
  78. package/lib/ai/bootstrap.js +0 -243
  79. package/lib/ai/bootstrap.js.map +0 -1
  80. package/lib/ai/builtin-tools.d.ts +0 -59
  81. package/lib/ai/builtin-tools.d.ts.map +0 -1
  82. package/lib/ai/builtin-tools.js +0 -777
  83. package/lib/ai/builtin-tools.js.map +0 -1
  84. package/lib/ai/compaction.d.ts +0 -132
  85. package/lib/ai/compaction.d.ts.map +0 -1
  86. package/lib/ai/compaction.js +0 -370
  87. package/lib/ai/compaction.js.map +0 -1
  88. package/lib/ai/context-manager.d.ts +0 -213
  89. package/lib/ai/context-manager.d.ts.map +0 -1
  90. package/lib/ai/context-manager.js +0 -313
  91. package/lib/ai/context-manager.js.map +0 -1
  92. package/lib/ai/conversation-memory.d.ts +0 -181
  93. package/lib/ai/conversation-memory.d.ts.map +0 -1
  94. package/lib/ai/conversation-memory.js +0 -581
  95. package/lib/ai/conversation-memory.js.map +0 -1
  96. package/lib/ai/cron-engine.d.ts +0 -92
  97. package/lib/ai/cron-engine.d.ts.map +0 -1
  98. package/lib/ai/cron-engine.js +0 -278
  99. package/lib/ai/cron-engine.js.map +0 -1
  100. package/lib/ai/follow-up.d.ts +0 -131
  101. package/lib/ai/follow-up.d.ts.map +0 -1
  102. package/lib/ai/follow-up.js +0 -265
  103. package/lib/ai/follow-up.js.map +0 -1
  104. package/lib/ai/hooks.d.ts +0 -143
  105. package/lib/ai/hooks.d.ts.map +0 -1
  106. package/lib/ai/hooks.js +0 -108
  107. package/lib/ai/hooks.js.map +0 -1
  108. package/lib/ai/init.d.ts +0 -30
  109. package/lib/ai/init.d.ts.map +0 -1
  110. package/lib/ai/init.js +0 -686
  111. package/lib/ai/init.js.map +0 -1
  112. package/lib/ai/output.d.ts +0 -93
  113. package/lib/ai/output.d.ts.map +0 -1
  114. package/lib/ai/output.js +0 -176
  115. package/lib/ai/output.js.map +0 -1
  116. package/lib/ai/rate-limiter.d.ts +0 -38
  117. package/lib/ai/rate-limiter.d.ts.map +0 -1
  118. package/lib/ai/rate-limiter.js +0 -86
  119. package/lib/ai/rate-limiter.js.map +0 -1
  120. package/lib/ai/service.d.ts +0 -88
  121. package/lib/ai/service.d.ts.map +0 -1
  122. package/lib/ai/service.js +0 -285
  123. package/lib/ai/service.js.map +0 -1
  124. package/lib/ai/session.d.ts +0 -186
  125. package/lib/ai/session.d.ts.map +0 -1
  126. package/lib/ai/session.js +0 -443
  127. package/lib/ai/session.js.map +0 -1
  128. package/lib/ai/subagent.d.ts +0 -50
  129. package/lib/ai/subagent.d.ts.map +0 -1
  130. package/lib/ai/subagent.js +0 -144
  131. package/lib/ai/subagent.js.map +0 -1
  132. package/lib/ai/tone-detector.d.ts +0 -19
  133. package/lib/ai/tone-detector.d.ts.map +0 -1
  134. package/lib/ai/tone-detector.js +0 -72
  135. package/lib/ai/tone-detector.js.map +0 -1
  136. package/lib/ai/tools.d.ts +0 -45
  137. package/lib/ai/tools.d.ts.map +0 -1
  138. package/lib/ai/tools.js +0 -206
  139. package/lib/ai/tools.js.map +0 -1
  140. package/lib/ai/user-profile.d.ts +0 -56
  141. package/lib/ai/user-profile.d.ts.map +0 -1
  142. package/lib/ai/user-profile.js +0 -130
  143. package/lib/ai/user-profile.js.map +0 -1
  144. package/lib/ai/zhin-agent/builtin-tools.d.ts +0 -17
  145. package/lib/ai/zhin-agent/builtin-tools.d.ts.map +0 -1
  146. package/lib/ai/zhin-agent/builtin-tools.js +0 -220
  147. package/lib/ai/zhin-agent/builtin-tools.js.map +0 -1
  148. package/lib/ai/zhin-agent/config.d.ts +0 -54
  149. package/lib/ai/zhin-agent/config.d.ts.map +0 -1
  150. package/lib/ai/zhin-agent/config.js +0 -76
  151. package/lib/ai/zhin-agent/config.js.map +0 -1
  152. package/lib/ai/zhin-agent/exec-policy.d.ts +0 -20
  153. package/lib/ai/zhin-agent/exec-policy.d.ts.map +0 -1
  154. package/lib/ai/zhin-agent/exec-policy.js +0 -71
  155. package/lib/ai/zhin-agent/exec-policy.js.map +0 -1
  156. package/lib/ai/zhin-agent/index.d.ts +0 -70
  157. package/lib/ai/zhin-agent/index.d.ts.map +0 -1
  158. package/lib/ai/zhin-agent/index.js +0 -404
  159. package/lib/ai/zhin-agent/index.js.map +0 -1
  160. package/lib/ai/zhin-agent/prompt.d.ts +0 -21
  161. package/lib/ai/zhin-agent/prompt.d.ts.map +0 -1
  162. package/lib/ai/zhin-agent/prompt.js +0 -111
  163. package/lib/ai/zhin-agent/prompt.js.map +0 -1
  164. package/lib/ai/zhin-agent/tool-collector.d.ts +0 -22
  165. package/lib/ai/zhin-agent/tool-collector.d.ts.map +0 -1
  166. package/lib/ai/zhin-agent/tool-collector.js +0 -218
  167. package/lib/ai/zhin-agent/tool-collector.js.map +0 -1
  168. package/src/ai/agent.ts +0 -831
  169. package/src/ai/bootstrap.ts +0 -309
  170. package/src/ai/builtin-tools.ts +0 -849
  171. package/src/ai/compaction.ts +0 -529
  172. package/src/ai/context-manager.ts +0 -440
  173. package/src/ai/conversation-memory.ts +0 -774
  174. package/src/ai/cron-engine.ts +0 -337
  175. package/src/ai/follow-up.ts +0 -357
  176. package/src/ai/hooks.ts +0 -223
  177. package/src/ai/init.ts +0 -762
  178. package/src/ai/output.ts +0 -261
  179. package/src/ai/rate-limiter.ts +0 -129
  180. package/src/ai/service.ts +0 -331
  181. package/src/ai/session.ts +0 -544
  182. package/src/ai/subagent.ts +0 -209
  183. package/src/ai/tone-detector.ts +0 -89
  184. package/src/ai/tools.ts +0 -218
  185. package/src/ai/user-profile.ts +0 -181
  186. package/src/ai/zhin-agent/builtin-tools.ts +0 -247
  187. package/src/ai/zhin-agent/config.ts +0 -113
  188. package/src/ai/zhin-agent/exec-policy.ts +0 -78
  189. package/src/ai/zhin-agent/index.ts +0 -512
  190. package/src/ai/zhin-agent/prompt.ts +0 -131
  191. package/src/ai/zhin-agent/tool-collector.ts +0 -243
  192. package/tests/ai/agent.test.ts +0 -614
  193. package/tests/ai/context-manager.test.ts +0 -413
  194. package/tests/ai/conversation-memory.test.ts +0 -128
  195. package/tests/ai/follow-up.test.ts +0 -175
  196. package/tests/ai/integration.test.ts +0 -584
  197. package/tests/ai/output.test.ts +0 -128
  198. package/tests/ai/rate-limiter.test.ts +0 -108
  199. package/tests/ai/session.test.ts +0 -375
  200. package/tests/ai/subagent.test.ts +0 -270
  201. package/tests/ai/tone-detector.test.ts +0 -80
  202. package/tests/ai/tools-builtin.test.ts +0 -346
  203. package/tests/ai/user-profile.test.ts +0 -73
  204. package/tests/ai/zhin-agent.test.ts +0 -177
@@ -1,849 +0,0 @@
1
- /**
2
- * AI 内置系统工具
3
- *
4
- * 借鉴 OpenClaw/MicroClaw 的实用工具设计,为 ZhinAgent 提供:
5
- *
6
- * 文件工具: read_file, write_file, edit_file, list_dir, glob, grep
7
- * Shell: bash
8
- * 网络: web_search, web_fetch
9
- * 计划: todo_read, todo_write
10
- * 记忆: read_memory, write_memory (AGENTS.md)
11
- * 技能: activate_skill, install_skill
12
- * 会话: session_status, compact_session
13
- * 技能发现: 工作区 skills/ 目录自动扫描
14
- * 引导文件: SOUL.md, TOOLS.md, AGENTS.md 自动加载
15
- */
16
-
17
- import * as fs from 'fs';
18
- import * as os from 'os';
19
- import * as path from 'path';
20
- import { exec } from 'child_process';
21
- import { promisify } from 'util';
22
- import { Logger } from '@zhin.js/logger';
23
- import { ZhinTool } from '../built/tool.js';
24
-
25
- // 从新模块中 re-export 向后兼容的函数
26
- export { loadSoulPersona, loadToolsGuide, loadAgentsMemory } from './bootstrap.js';
27
-
28
- const execAsync = promisify(exec);
29
- const logger = new Logger(null, 'builtin-tools');
30
-
31
- /**
32
- * 获取数据目录路径
33
- */
34
- function getDataDir(): string {
35
- const dir = path.join(process.cwd(), 'data');
36
- fs.mkdirSync(dir, { recursive: true });
37
- return dir;
38
- }
39
-
40
- /** 展开路径中的 ~ 为实际 home 目录 */
41
- function expandHome(p: string): string {
42
- if (p === '~') return os.homedir();
43
- if (p.startsWith('~/') || p.startsWith('~\\')) return path.join(os.homedir(), p.slice(2));
44
- return p;
45
- }
46
-
47
- /** 将 Node 文件错误转为 miniclawd 风格的结构化短句,便于模型区分并重试 */
48
- function nodeErrToFileMessage(err: unknown, filePath: string, kind: 'read' | 'write' | 'edit' | 'list'): string {
49
- const e = err as NodeJS.ErrnoException;
50
- if (e?.code === 'ENOENT') {
51
- if (kind === 'list') return `Error: Directory not found: ${filePath}`;
52
- return `Error: File not found: ${filePath}`;
53
- }
54
- if (e?.code === 'EACCES') return `Error: Permission denied: ${filePath}`;
55
- const action = kind === 'read' ? 'reading file' : kind === 'write' ? 'writing file' : kind === 'edit' ? 'editing file' : 'listing directory';
56
- return `Error ${action}: ${e?.message ?? String(err)}`;
57
- }
58
-
59
- // ============================================================================
60
- // 工具工厂函数
61
- // ============================================================================
62
-
63
- export interface BuiltinToolsOptions {
64
- /** Max chars for skill instruction extraction (model-size-aware) */
65
- skillInstructionMaxChars?: number;
66
- }
67
-
68
- /**
69
- * 创建所有内置系统工具
70
- */
71
- export function createBuiltinTools(options?: BuiltinToolsOptions): ZhinTool[] {
72
- const DATA_DIR = getDataDir();
73
- const skillMaxChars = options?.skillInstructionMaxChars ?? 4000;
74
-
75
- const tools: ZhinTool[] = [];
76
-
77
- // ── read_file(清晰描述 + 强关键词) ──
78
- tools.push(
79
- new ZhinTool('read_file')
80
- .desc('读取指定路径的文件内容。用于查看、打开或读取任意文本文件。')
81
- .keyword('读文件', '读取文件', '查看文件', '打开文件', '文件内容', 'read file', 'read', 'cat', '查看', '打开')
82
- .tag('file', 'read')
83
- .kind('file')
84
- .param('file_path', { type: 'string', description: '要读取的文件路径(绝对路径或相对项目根目录)' }, true)
85
- .param('offset', { type: 'number', description: '起始行号(0-based,可选,默认从第 1 行开始)' })
86
- .param('limit', { type: 'number', description: '最多读取行数(可选,默认全部)' })
87
- .execute(async (args) => {
88
- try {
89
- const fp = expandHome(args.file_path);
90
- const stat = await fs.promises.stat(fp);
91
- if (!stat.isFile()) return `Error: Not a file: ${fp}`;
92
- const content = await fs.promises.readFile(fp, 'utf-8');
93
- const lines = content.split('\n');
94
- const offset = args.offset ?? 0;
95
- const limit = args.limit ?? lines.length;
96
- const sliced = lines.slice(offset, offset + limit);
97
- const numbered = sliced.map((line: string, i: number) => `${offset + i + 1} | ${line}`).join('\n');
98
- return `File: ${fp} (${lines.length} lines, showing ${offset + 1}-${Math.min(offset + limit, lines.length)})\n${numbered}`;
99
- } catch (e: unknown) {
100
- return nodeErrToFileMessage(e, args.file_path, 'read');
101
- }
102
- }),
103
- );
104
-
105
- // ── write_file ──
106
- tools.push(
107
- new ZhinTool('write_file')
108
- .desc('向指定路径写入内容,创建或覆盖文件;若目录不存在会自动创建。')
109
- .keyword('写文件', '写入文件', '创建文件', '保存文件', 'write file', 'write', '保存', '创建')
110
- .tag('file', 'write')
111
- .kind('file')
112
- .param('file_path', { type: 'string', description: '要写入的文件路径' }, true)
113
- .param('content', { type: 'string', description: '要写入的完整内容' }, true)
114
- .execute(async (args) => {
115
- try {
116
- const fp = expandHome(args.file_path);
117
- await fs.promises.mkdir(path.dirname(fp), { recursive: true });
118
- await fs.promises.writeFile(fp, args.content, 'utf-8');
119
- return `✅ Wrote ${Buffer.byteLength(args.content)} bytes to ${fp}`;
120
- } catch (e: unknown) {
121
- return nodeErrToFileMessage(e, args.file_path, 'write');
122
- }
123
- }),
124
- );
125
-
126
- // ── edit_file(old_text 必须精确匹配) ──
127
- tools.push(
128
- new ZhinTool('edit_file')
129
- .desc('在文件中查找并替换一段文本。old_string 必须在文件中精确存在且唯一;建议包含完整行或足够上下文以避免重复匹配。')
130
- .keyword('编辑文件', '修改文件', '替换内容', '查找替换', 'edit file', 'edit', '修改', '替换')
131
- .tag('file', 'edit')
132
- .kind('file')
133
- .param('file_path', { type: 'string', description: '要编辑的文件路径' }, true)
134
- .param('old_string', { type: 'string', description: '文件中要替换的原文(必须与文件内容完全一致)' }, true)
135
- .param('new_string', { type: 'string', description: '替换后的新文本' }, true)
136
- .execute(async (args) => {
137
- try {
138
- const fp = expandHome(args.file_path);
139
- const content = await fs.promises.readFile(fp, 'utf-8');
140
- const count = content.split(args.old_string).length - 1;
141
- if (count === 0) return `Error: old_string not found in file. Make sure it matches exactly.`;
142
- if (count > 1) return `Warning: old_string appears ${count} times. Please provide more context to make it unique.`;
143
- const newContent = content.replace(args.old_string, args.new_string);
144
- await fs.promises.writeFile(fp, newContent, 'utf-8');
145
-
146
- const oldLines = args.old_string.split('\n');
147
- const newLines = args.new_string.split('\n');
148
- return `✅ Edited ${fp}\n--- before ---\n${oldLines.slice(0, 5).join('\n')}${oldLines.length > 5 ? '\n...' : ''}\n--- after ---\n${newLines.slice(0, 5).join('\n')}${newLines.length > 5 ? '\n...' : ''}`;
149
- } catch (e: unknown) {
150
- return nodeErrToFileMessage(e, args.file_path, 'edit');
151
- }
152
- }),
153
- );
154
-
155
- // ── list_dir(列出目录内容,便于 AI 匹配「列表」「目录」「ls」) ──
156
- tools.push(
157
- new ZhinTool('list_dir')
158
- .desc('列出指定目录下的文件和子目录名称。用于查看目录结构、有哪些文件。')
159
- .keyword('列目录', '列出目录', '目录列表', '查看目录', 'list directory', 'list dir', 'ls', 'dir', '目录内容', '有哪些文件')
160
- .tag('file', 'list')
161
- .kind('file')
162
- .param('path', { type: 'string', description: '要列出的目录路径(绝对或相对项目根目录)' }, true)
163
- .execute(async (args) => {
164
- try {
165
- const dirPath = path.resolve(process.cwd(), expandHome(args.path));
166
- const stat = await fs.promises.stat(dirPath);
167
- if (!stat.isDirectory()) {
168
- return `Error: Not a directory: ${args.path}`;
169
- }
170
- const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
171
- if (entries.length === 0) {
172
- return `Directory ${args.path} is empty`;
173
- }
174
- const lines: string[] = [];
175
- for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
176
- lines.push((e.isDirectory() ? '[DIR] ' : ' ') + e.name);
177
- }
178
- return lines.join('\n');
179
- } catch (e: unknown) {
180
- return nodeErrToFileMessage(e, args.path, 'list');
181
- }
182
- }),
183
- );
184
-
185
- // ── glob ──
186
- tools.push(
187
- new ZhinTool('glob')
188
- .desc('按 glob 模式查找匹配的文件路径(如 **/*.ts)。用于按模式找文件,而非列出目录。')
189
- .keyword('glob', '查找文件', '按模式找文件', 'find', '匹配文件')
190
- .tag('file', 'search')
191
- .kind('file')
192
- .param('pattern', { type: 'string', description: 'Glob 模式(如 **/*.ts)' }, true)
193
- .param('cwd', { type: 'string', description: '工作目录(默认项目根目录)' })
194
- .execute(async (args) => {
195
- try {
196
- const cwd = args.cwd || process.cwd();
197
- const { stdout } = await execAsync(
198
- `find . -path './${args.pattern}' -type f 2>/dev/null | head -100`,
199
- { cwd },
200
- );
201
- const files = stdout.trim().split('\n').filter(Boolean);
202
- return files.length === 0
203
- ? `No files matching '${args.pattern}'`
204
- : `Found ${files.length} files:\n${files.join('\n')}`;
205
- } catch (e: any) {
206
- return `Error: ${e.message}`;
207
- }
208
- }),
209
- );
210
-
211
- // ── grep ──
212
- tools.push(
213
- new ZhinTool('grep')
214
- .desc('按正则搜索文件内容,返回匹配行和行号')
215
- .keyword('搜索', '查找内容', 'grep', '正则')
216
- .tag('search', 'regex')
217
- .kind('file')
218
- .param('pattern', { type: 'string', description: '正则表达式' }, true)
219
- .param('path', { type: 'string', description: '搜索路径(默认 .)' })
220
- .param('include', { type: 'string', description: '文件类型过滤(如 *.ts)' })
221
- .execute(async (args) => {
222
- try {
223
- const searchPath = args.path || '.';
224
- const includeFlag = args.include ? `--include='${args.include}'` : '';
225
- const { stdout } = await execAsync(
226
- `grep -rn ${includeFlag} '${args.pattern}' ${searchPath} 2>/dev/null | head -50`,
227
- { cwd: process.cwd() },
228
- );
229
- return stdout.trim() || `No matches for '${args.pattern}'`;
230
- } catch (e: any) {
231
- if (e.code === 1) return `No matches for '${args.pattern}'`;
232
- return `Error: ${e.message}`;
233
- }
234
- }),
235
- );
236
-
237
- // ── bash ──
238
- tools.push(
239
- new ZhinTool('bash')
240
- .desc('执行 Shell 命令(带超时保护)')
241
- .keyword('执行', '运行', '命令', '终端', 'shell', 'bash')
242
- .tag('shell', 'exec')
243
- .kind('shell')
244
- .param('command', { type: 'string', description: 'Shell 命令' }, true)
245
- .param('cwd', { type: 'string', description: '工作目录' })
246
- .param('timeout', { type: 'number', description: '超时毫秒数(默认 30000)' })
247
- .execute(async (args) => {
248
- try {
249
- const timeout = args.timeout ?? 30000;
250
- const { stdout, stderr } = await execAsync(args.command, {
251
- cwd: args.cwd || process.cwd(),
252
- timeout,
253
- maxBuffer: 1024 * 1024,
254
- });
255
- let result = '';
256
- if (stdout.trim()) result += `STDOUT:\n${stdout.trim()}`;
257
- if (stderr.trim()) result += `${result ? '\n' : ''}STDERR:\n${stderr.trim()}`;
258
- return result || '(no output)';
259
- } catch (e: any) {
260
- return `Error (exit ${e.code || '?'}): ${e.message}\nSTDOUT:\n${e.stdout || ''}\nSTDERR:\n${e.stderr || ''}`;
261
- }
262
- }),
263
- );
264
-
265
- // ── web_search(搜索网页,返回标题、URL、摘要) ──
266
- tools.push(
267
- new ZhinTool('web_search')
268
- .desc('在互联网上搜索,返回匹配的标题、URL 和摘要片段。用于查资料、找网页。')
269
- .keyword('搜索', '网上搜', '网页搜索', '搜索引擎', 'search', 'google', '百度', '查询', '搜一下')
270
- .tag('web', 'search')
271
- .kind('web')
272
- .param('query', { type: 'string', description: '搜索关键词或完整查询语句' }, true)
273
- .param('limit', { type: 'number', description: '返回结果数量(默认 5,建议 1–10)' })
274
- .execute(async (args) => {
275
- try {
276
- const limit = args.limit ?? 5;
277
- const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(args.query)}`;
278
- const res = await fetch(url, {
279
- headers: {
280
- 'User-Agent': 'Mozilla/5.0 (compatible; ZhinBot/1.0)',
281
- 'Accept': 'text/html',
282
- 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
283
- },
284
- signal: AbortSignal.timeout(15000),
285
- });
286
- if (!res.ok) return `HTTP ${res.status}: ${res.statusText}`;
287
- const html = await res.text();
288
-
289
- // 从 DuckDuckGo HTML 页面提取搜索结果
290
- const results: { title: string; url: string; snippet: string }[] = [];
291
- const resultBlocks = html.split(/class="result\s/);
292
-
293
- for (let i = 1; i < resultBlocks.length && results.length < limit; i++) {
294
- const block = resultBlocks[i];
295
-
296
- // 提取标题和 URL
297
- const titleMatch = block.match(/<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/);
298
- if (!titleMatch) continue;
299
-
300
- let href = titleMatch[1];
301
- // DuckDuckGo 会将 URL 编码到 uddg 参数中
302
- const uddgMatch = href.match(/[?&]uddg=([^&]+)/);
303
- if (uddgMatch) href = decodeURIComponent(uddgMatch[1]);
304
-
305
- const title = titleMatch[2].replace(/<[^>]+>/g, '').trim();
306
-
307
- // 提取摘要
308
- const snippetMatch = block.match(/<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/);
309
- const snippet = snippetMatch
310
- ? snippetMatch[1].replace(/<[^>]+>/g, '').trim()
311
- : '';
312
-
313
- if (title && href) {
314
- results.push({ title, url: href, snippet });
315
- }
316
- }
317
-
318
- if (results.length === 0) return 'No results found.';
319
- return results.map((r, i) =>
320
- `${i + 1}. ${r.title}\n URL: ${r.url}\n ${r.snippet}`,
321
- ).join('\n\n');
322
- } catch (e: any) {
323
- return `Error: ${e.message}`;
324
- }
325
- }),
326
- );
327
-
328
- // ── web_fetch(抓取 URL 并提取正文) ──
329
- tools.push(
330
- new ZhinTool('web_fetch')
331
- .desc('抓取指定 URL 的网页内容并提取正文(去除广告等),返回可读文本。用于读文章、获取网页内容。')
332
- .keyword('抓取网页', '打开链接', '获取网页', '读网页', 'fetch', 'url', '链接内容', '网页内容')
333
- .tag('web', 'fetch')
334
- .kind('web')
335
- .param('url', { type: 'string', description: '要抓取的完整 URL(需 http 或 https)' }, true)
336
- .execute(async (args) => {
337
- try {
338
- const response = await fetch(args.url, {
339
- headers: { 'User-Agent': 'Mozilla/5.0 (compatible; ZhinBot/1.0)' },
340
- signal: AbortSignal.timeout(15000),
341
- });
342
- if (!response.ok) return `HTTP ${response.status}: ${response.statusText}`;
343
- const html = await response.text();
344
- const text = html
345
- .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
346
- .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
347
- .replace(/<[^>]+>/g, ' ')
348
- .replace(/\s+/g, ' ')
349
- .trim();
350
- const maxLen = 20 * 1024;
351
- return text.length > maxLen ? text.slice(0, maxLen) + '\n...(truncated)' : text;
352
- } catch (e: any) {
353
- return `Error: ${e.message}`;
354
- }
355
- }),
356
- );
357
-
358
- // ── todo_read ──
359
- tools.push(
360
- new ZhinTool('todo_read')
361
- .desc('读取当前任务计划列表,用于查看进度和待办事项')
362
- .keyword('任务', '计划', '进度', 'todo', '待办')
363
- .tag('plan', 'todo')
364
- .kind('plan')
365
- .param('chat_id', { type: 'string', description: '聊天范围(传 "global" 表示全局,或传具体聊天 ID)' }, true)
366
- .execute(async (args) => {
367
- try {
368
- const dir = args.chat_id && args.chat_id !== 'global' ? path.join(DATA_DIR, 'groups', args.chat_id) : DATA_DIR;
369
- const todoPath = path.join(dir, 'TODO.json');
370
- if (!fs.existsSync(todoPath)) return 'No tasks found. Use todo_write to create a plan.';
371
- const data = JSON.parse(await fs.promises.readFile(todoPath, 'utf-8'));
372
- if (!data.items || data.items.length === 0) return 'Task list is empty.';
373
- const lines = data.items.map((item: any, i: number) => {
374
- const status = item.status === 'done' ? '✅' : item.status === 'in-progress' ? '🔄' : '⬜';
375
- return `${status} ${i + 1}. ${item.title}${item.detail ? ' — ' + item.detail : ''}`;
376
- });
377
- return `📋 Tasks (${data.items.filter((i: any) => i.status === 'done').length}/${data.items.length} done):\n${lines.join('\n')}`;
378
- } catch (e: any) {
379
- return `Error: ${e.message}`;
380
- }
381
- }),
382
- );
383
-
384
- // ── todo_write ──
385
- tools.push(
386
- new ZhinTool('todo_write')
387
- .desc('创建或更新任务计划,用于分解复杂任务并跟踪进度')
388
- .keyword('创建计划', '更新任务', '标记完成', 'todo')
389
- .tag('plan', 'todo')
390
- .kind('plan')
391
- .param('items', { type: 'array', description: '任务列表 [{title, detail?, status: pending|in-progress|done}]' } as any, true)
392
- .param('chat_id', { type: 'string', description: '聊天范围(可选)' })
393
- .execute(async (args) => {
394
- try {
395
- const dir = args.chat_id ? path.join(DATA_DIR, 'groups', args.chat_id) : DATA_DIR;
396
- const todoPath = path.join(dir, 'TODO.json');
397
- await fs.promises.mkdir(path.dirname(todoPath), { recursive: true });
398
- const data = { updated_at: new Date().toISOString(), items: args.items };
399
- await fs.promises.writeFile(todoPath, JSON.stringify(data, null, 2), 'utf-8');
400
- const done = args.items.filter((i: any) => i.status === 'done').length;
401
- return `✅ Tasks updated (${done}/${args.items.length} done)`;
402
- } catch (e: any) {
403
- return `Error: ${e.message}`;
404
- }
405
- }),
406
- );
407
-
408
- // ── read_memory ──
409
- tools.push(
410
- new ZhinTool('read_memory')
411
- .desc('读取持久化记忆(AGENTS.md)。记忆跨会话保持。scope: global(共享)或 chat(按聊天隔离)')
412
- .keyword('记忆', '记住', '回忆', '之前', '上次', 'memory')
413
- .tag('memory', 'agents')
414
- .kind('memory')
415
- .param('scope', { type: 'string', description: "'global' 或 'chat'(默认 chat)", enum: ['global', 'chat'] }, true)
416
- .param('chat_id', { type: 'string', description: '聊天 ID(chat scope 时使用)' })
417
- .execute(async (args) => {
418
- try {
419
- const memPath = args.scope === 'global'
420
- ? path.join(DATA_DIR, 'AGENTS.md')
421
- : path.join(DATA_DIR, 'groups', args.chat_id || 'default', 'AGENTS.md');
422
- if (!fs.existsSync(memPath)) return 'No memory stored yet.';
423
- return await fs.promises.readFile(memPath, 'utf-8');
424
- } catch (e: any) {
425
- return `Error: ${e.message}`;
426
- }
427
- }),
428
- );
429
-
430
- // ── write_memory ──
431
- tools.push(
432
- new ZhinTool('write_memory')
433
- .desc('写入持久化记忆。当用户说"记住…"、"记录…"时使用此工具')
434
- .keyword('记住', '保存', 'remember', '记录')
435
- .tag('memory', 'agents')
436
- .kind('memory')
437
- .param('content', { type: 'string', description: '要保存的记忆内容(Markdown)' }, true)
438
- .param('scope', { type: 'string', description: "'global' 或 'chat'(默认 chat)", enum: ['global', 'chat'] })
439
- .param('chat_id', { type: 'string', description: '聊天 ID' })
440
- .execute(async (args) => {
441
- try {
442
- const memPath = args.scope === 'global'
443
- ? path.join(DATA_DIR, 'AGENTS.md')
444
- : path.join(DATA_DIR, 'groups', args.chat_id || 'default', 'AGENTS.md');
445
- await fs.promises.mkdir(path.dirname(memPath), { recursive: true });
446
- await fs.promises.writeFile(memPath, args.content, 'utf-8');
447
- return `✅ Memory saved (${args.scope || 'chat'} scope)`;
448
- } catch (e: any) {
449
- return `Error: ${e.message}`;
450
- }
451
- }),
452
- );
453
-
454
- // ── activate_skill ──
455
- tools.push(
456
- new ZhinTool('activate_skill')
457
- .desc('按名称激活技能,加载其完整指令。当判断某个技能与用户请求相关时使用')
458
- .keyword('技能', '激活', '启用', '使用', 'skill', 'activate', 'use')
459
- .tag('skill', 'activate')
460
- .kind('skill')
461
- .param('name', { type: 'string', description: '技能名称' }, true)
462
- .execute(async (args) => {
463
- try {
464
- // 与 discoverWorkspaceSkills 顺序一致:Workspace > Local > Bundled
465
- const dirs = [
466
- path.join(process.cwd(), 'skills'),
467
- path.join(os.homedir(), '.zhin', 'skills'),
468
- path.join(DATA_DIR, 'skills'),
469
- ];
470
- for (const dir of dirs) {
471
- const skillPath = path.join(dir, args.name, 'SKILL.md');
472
- if (fs.existsSync(skillPath)) {
473
- const fullContent = await fs.promises.readFile(skillPath, 'utf-8');
474
- // 5.3 可执行环境检查:若 SKILL 声明了 deps,再次检查;缺失则在返回内容中提示
475
- const depWarning = await checkSkillDeps(fullContent);
476
- const instructions = extractSkillInstructions(args.name, fullContent, skillMaxChars);
477
- return depWarning ? `${depWarning}\n\n${instructions}` : instructions;
478
- }
479
- }
480
- return `Skill '${args.name}' not found. Check skills/ directory.`;
481
- } catch (e: any) {
482
- return `Error: ${e.message}`;
483
- }
484
- }),
485
- );
486
-
487
- // ── install_skill(从 URL 下载并安装技能) ──
488
- tools.push(
489
- new ZhinTool('install_skill')
490
- .desc('从 URL 下载 SKILL.md 并安装到本地 skills/ 目录。用户要求加入/安装/下载某个技能时使用')
491
- .keyword('安装技能', '下载技能', '加入', '添加技能', 'install', 'skill', 'join', '学会', '学习技能')
492
- .tag('skill', 'install')
493
- .kind('skill')
494
- .param('url', { type: 'string', description: 'SKILL.md 文件的完整 URL(如 https://example.com/skill.md)' }, true)
495
- .execute(async (args) => {
496
- try {
497
- const response = await fetch(args.url, {
498
- headers: { 'User-Agent': 'Mozilla/5.0 (compatible; ZhinBot/1.0)' },
499
- signal: AbortSignal.timeout(15000),
500
- });
501
- if (!response.ok) return `Error: HTTP ${response.status} ${response.statusText}`;
502
- const content = await response.text();
503
-
504
- const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
505
- if (!fmMatch) return 'Error: 无效的 SKILL.md 文件(缺少 frontmatter)';
506
-
507
- let yaml: any;
508
- try {
509
- yaml = await import('yaml');
510
- if (yaml.default) yaml = yaml.default;
511
- } catch {
512
- return 'Error: 无法加载 yaml 解析器';
513
- }
514
-
515
- const metadata = yaml.parse(fmMatch[1]);
516
- if (!metadata?.name) return 'Error: SKILL.md 缺少 name 字段';
517
-
518
- const skillName: string = metadata.name;
519
- const skillDir = path.join(process.cwd(), 'skills', skillName);
520
- await fs.promises.mkdir(skillDir, { recursive: true });
521
- const skillPath = path.join(skillDir, 'SKILL.md');
522
- await fs.promises.writeFile(skillPath, content, 'utf-8');
523
-
524
- logger.info(`技能已安装: ${skillName} → ${skillPath}`);
525
- return `✅ 技能「${skillName}」已安装到 ${skillPath}。现在可以用 activate_skill("${skillName}") 激活它。`;
526
- } catch (e: any) {
527
- return `Error: ${e.message}`;
528
- }
529
- }),
530
- );
531
-
532
- logger.info(`已创建 ${tools.length} 个内置系统工具`);
533
- return tools;
534
- }
535
-
536
- /**
537
- * 检查技能声明的依赖是否在环境中可用;若有缺失返回提示文案,否则返回空字符串
538
- */
539
- async function checkSkillDeps(content: string): Promise<string> {
540
- const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
541
- if (!fmMatch) return '';
542
- let yaml: any;
543
- try {
544
- yaml = await import('yaml');
545
- if (yaml.default) yaml = yaml.default;
546
- } catch {
547
- return '';
548
- }
549
- const metadata = yaml.parse(fmMatch[1]);
550
- if (!metadata) return '';
551
- const compat = metadata.compatibility || {};
552
- const deps = compat.deps || metadata.deps;
553
- if (!deps || !Array.isArray(deps)) return '';
554
- const missing: string[] = [];
555
- for (const dep of deps) {
556
- try {
557
- await execAsync(`which ${dep} 2>/dev/null`);
558
- } catch {
559
- missing.push(dep);
560
- }
561
- }
562
- if (missing.length === 0) return '';
563
- return `⚠️ 当前环境缺少以下依赖,请先安装后再使用本技能:${missing.join(', ')}`;
564
- }
565
-
566
- /**
567
- * 从 SKILL.md 全文中提取精简的执行指令
568
- * 只保留 frontmatter(工具列表)和执行规则,去掉示例、测试场景等冗余内容
569
- * 这样可以大幅减少 token 占用,让小模型能有足够空间继续调用工具
570
- */
571
- function extractSkillInstructions(name: string, content: string, maxBodyLen: number = 4000): string {
572
- const lines: string[] = [];
573
- lines.push(`Skill '${name}' activated. 请立即根据以下指导执行工具调用:`);
574
- lines.push('');
575
-
576
- const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
577
- if (fmMatch) {
578
- const fmContent = fmMatch[1];
579
- const toolsMatch = fmContent.match(/tools:\s*\n((?:\s+-\s+.+\n?)+)/);
580
- if (toolsMatch) {
581
- lines.push('## 可用工具');
582
- lines.push(toolsMatch[0].trim());
583
- lines.push('');
584
- }
585
- }
586
-
587
- const bodyAfterFm = fmMatch && fmMatch.index !== undefined
588
- ? content.slice(fmMatch.index + fmMatch[0].length).replace(/^\s+/, '')
589
- : content;
590
-
591
- // Priority: "## 快速操作" / "## Quick Actions" summary for small models
592
- const quickActionsMatch = bodyAfterFm.match(/## (?:快速操作|Quick\s*Actions)[\s\S]*?(?=\n## [^\s]|$)/i);
593
- if (quickActionsMatch && maxBodyLen <= 2000) {
594
- lines.push(quickActionsMatch[0].trim());
595
- lines.push('');
596
- lines.push('## 立即行动');
597
- lines.push('根据上面的指导,立即调用工具完成用户请求。禁止重复调用 activate_skill,禁止用文本描述代替实际工具调用。');
598
- return lines.join('\n');
599
- }
600
-
601
- const rulesMatch = content.match(/## 执行规则[\s\S]*?(?=\n## [^\s]|$)/);
602
- const workflowMatch = content.match(/## (?:Workflow|Instructions|使用说明)[\s\S]*?(?=\n## [^\s]|$)/);
603
-
604
- if (rulesMatch) {
605
- lines.push(rulesMatch[0].trim());
606
- lines.push('');
607
- } else if (workflowMatch) {
608
- lines.push(workflowMatch[0].trim());
609
- lines.push('');
610
- } else if (bodyAfterFm.trim()) {
611
- const firstH2 = bodyAfterFm.match(/\n## [^\s]/);
612
- const intro = firstH2 ? bodyAfterFm.slice(0, firstH2.index).trim() : bodyAfterFm.trim();
613
-
614
- const quickStartMatch = bodyAfterFm.match(/## (?:快速开始|Quick\s*Start|Getting\s*Started)[\s\S]*?(?=\n## [^\s]|$)/i);
615
- const authMatch = bodyAfterFm.match(/## (?:认证|Authentication|Auth)[\s\S]*?(?=\n## [^\s]|$)/i);
616
-
617
- if (quickStartMatch || (intro.length < 200 && bodyAfterFm.length > intro.length)) {
618
- lines.push('## 指导');
619
- lines.push(intro);
620
- lines.push('');
621
- const extra: string[] = [];
622
- if (quickStartMatch) extra.push(quickStartMatch[0].trim());
623
- if (authMatch) extra.push(authMatch[0].trim());
624
- if (extra.length > 0) {
625
- const joined = extra.join('\n\n');
626
- lines.push(joined.length > maxBodyLen ? joined.slice(0, maxBodyLen) + '\n...(truncated)' : joined);
627
- } else {
628
- const rest = bodyAfterFm.slice(intro.length).trim();
629
- lines.push(rest.length > maxBodyLen ? rest.slice(0, maxBodyLen) + '\n...(truncated)' : rest);
630
- }
631
- lines.push('');
632
- } else if (intro) {
633
- lines.push('## 指导');
634
- lines.push(intro);
635
- lines.push('');
636
- }
637
- }
638
-
639
- lines.push('## 立即行动');
640
- lines.push('根据上面的指导,立即调用工具完成用户请求。禁止重复调用 activate_skill,禁止用文本描述代替实际工具调用。');
641
-
642
- return lines.join('\n');
643
- }
644
-
645
- // ============================================================================
646
- // 技能发现
647
- // ============================================================================
648
-
649
- export interface SkillMeta {
650
- name: string;
651
- description: string;
652
- keywords?: string[];
653
- tags?: string[];
654
- /** SKILL.md frontmatter 中声明的关联工具名列表 */
655
- toolNames?: string[];
656
- filePath: string;
657
- /** 是否常驻注入 system prompt(frontmatter always: true) */
658
- always?: boolean;
659
- /** 当前环境是否满足依赖(bins/env) */
660
- available?: boolean;
661
- /** 缺失的依赖描述(如 "CLI: ffmpeg", "ENV: API_KEY") */
662
- requiresMissing?: string[];
663
- }
664
-
665
- /**
666
- * 扫描技能目录,发现 SKILL.md 技能文件
667
- * 加载顺序:Workspace(cwd/skills)> Local(~/.zhin/skills)> Bundled(data/skills),同名先发现者优先
668
- * 支持平台/依赖兼容性过滤。内置技能由 create-zhin 在创建项目时写入 skills/summarize 等。
669
- */
670
- export async function discoverWorkspaceSkills(): Promise<SkillMeta[]> {
671
- const skills: SkillMeta[] = [];
672
- const seenNames = new Set<string>();
673
- const dataDir = getDataDir();
674
- const skillDirs = [
675
- path.join(process.cwd(), 'skills'), // Workspace
676
- path.join(os.homedir(), '.zhin', 'skills'), // Local
677
- path.join(dataDir, 'skills'), // Bundled / 默认 data
678
- ];
679
-
680
- // 确保 data/skills 目录存在
681
- const defaultSkillDir = path.join(dataDir, 'skills');
682
- if (!fs.existsSync(defaultSkillDir)) {
683
- fs.mkdirSync(defaultSkillDir, { recursive: true });
684
- logger.debug(`Created skill directory: ${defaultSkillDir}`);
685
- }
686
-
687
- for (const skillsDir of skillDirs) {
688
- if (!fs.existsSync(skillsDir)) continue;
689
-
690
- let entries: fs.Dirent[];
691
- try {
692
- entries = await fs.promises.readdir(skillsDir, { withFileTypes: true });
693
- } catch {
694
- continue;
695
- }
696
-
697
- for (const entry of entries) {
698
- if (!entry.isDirectory()) continue;
699
- const skillMdPath = path.join(skillsDir, entry.name, 'SKILL.md');
700
- if (!fs.existsSync(skillMdPath)) continue;
701
-
702
- try {
703
- const content = await fs.promises.readFile(skillMdPath, 'utf-8');
704
- // 改进的 frontmatter 正则:支持多种换行符、可选的尾部空白
705
- const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/);
706
- if (!match) {
707
- logger.debug(`Skill文件 ${skillMdPath} 没有有效的frontmatter格式`);
708
- continue;
709
- }
710
-
711
- // 动态导入 yaml,使用 .default 兼容 ESM 模块
712
- let yaml: any;
713
- try {
714
- yaml = await import('yaml');
715
- if (yaml.default) yaml = yaml.default;
716
- } catch (e) {
717
- logger.warn(`Unable to import yaml module: ${e}`);
718
- continue;
719
- }
720
-
721
- const metadata = yaml.parse(match[1]);
722
- if (!metadata || !metadata.name || !metadata.description) {
723
- logger.debug(`Skill文件 ${skillMdPath} 缺少必需的 name/description 字段`);
724
- continue;
725
- }
726
-
727
- // 平台兼容检查
728
- const compat = metadata.compatibility || {};
729
- if (compat.os && Array.isArray(compat.os)) {
730
- const currentOs = process.platform === 'darwin' ? 'darwin' : process.platform === 'win32' ? 'windows' : 'linux';
731
- if (!compat.os.includes(currentOs)) {
732
- logger.debug(`Skipping skill '${metadata.name}' (unsupported OS)`);
733
- continue;
734
- }
735
- }
736
-
737
- // 依赖检查:支持 metadata.requires.bins / requires.env 或 compat.deps / metadata.deps
738
- const requiresBins: string[] = metadata.requires?.bins || compat.deps || metadata.deps || [];
739
- const requiresEnv: string[] = metadata.requires?.env || [];
740
- const binsToCheck = Array.isArray(requiresBins) ? requiresBins : [];
741
- const envToCheck = Array.isArray(requiresEnv) ? requiresEnv : [];
742
- const requiresMissing: string[] = [];
743
- for (const bin of binsToCheck) {
744
- try {
745
- await execAsync(`which ${bin} 2>/dev/null`);
746
- } catch {
747
- requiresMissing.push(`CLI: ${bin}`);
748
- }
749
- }
750
- for (const envKey of envToCheck) {
751
- if (!process.env[envKey]) {
752
- requiresMissing.push(`ENV: ${envKey}`);
753
- }
754
- }
755
- const available = requiresMissing.length === 0;
756
-
757
- if (seenNames.has(metadata.name)) {
758
- logger.debug(`Skill '${metadata.name}' 已由先序目录加载,跳过: ${skillMdPath}`);
759
- continue;
760
- }
761
- seenNames.add(metadata.name);
762
-
763
- skills.push({
764
- name: metadata.name,
765
- description: metadata.description,
766
- keywords: metadata.keywords || [],
767
- tags: [...(metadata.tags || []), 'workspace-skill'],
768
- toolNames: Array.isArray(metadata.tools) ? metadata.tools : [],
769
- filePath: skillMdPath,
770
- always: Boolean(metadata.always),
771
- available,
772
- requiresMissing: requiresMissing.length > 0 ? requiresMissing : undefined,
773
- });
774
- logger.debug(`Skill发现成功: ${metadata.name}, tools: ${JSON.stringify(metadata.tools || [])}`);
775
- } catch (e) {
776
- logger.warn(`Failed to parse SKILL.md in ${skillMdPath}:`, e);
777
- }
778
- }
779
- }
780
-
781
- if (skills.length > 0) {
782
- logger.info(`发现 ${skills.length} 个工作区技能: ${skills.map(s => `${s.name}(tools:${(s.toolNames || []).join(',')})`).join(', ')}`);
783
- }
784
-
785
- return skills;
786
- }
787
-
788
- /**
789
- * 获取 frontmatter 中 always: true 的技能名列表(用于常驻注入 system prompt)
790
- */
791
- export function getAlwaysSkillNames(skills: SkillMeta[]): string[] {
792
- return skills.filter(s => s.always && s.available).map(s => s.name);
793
- }
794
-
795
- /**
796
- * 去除 frontmatter,返回正文
797
- */
798
- function stripFrontmatter(content: string): string {
799
- const match = content.match(/^---\s*\n[\s\S]*?\n---\s*(?:\n|$)/);
800
- if (match) {
801
- return content.slice(match[0].length).trim();
802
- }
803
- return content.trim();
804
- }
805
-
806
- /**
807
- * 加载 always 技能的正文内容并拼接为「Active Skills」段
808
- */
809
- export async function loadAlwaysSkillsContent(skills: SkillMeta[]): Promise<string> {
810
- const always = skills.filter(s => s.always && s.available);
811
- if (always.length === 0) return '';
812
- const parts: string[] = [];
813
- for (const s of always) {
814
- try {
815
- const content = await fs.promises.readFile(s.filePath, 'utf-8');
816
- const body = stripFrontmatter(content);
817
- parts.push(`### Skill: ${s.name}\n\n${body}`);
818
- } catch (e) {
819
- logger.warn(`Failed to load always skill ${s.name}: ${(e as Error).message}`);
820
- }
821
- }
822
- return parts.join('\n\n---\n\n');
823
- }
824
-
825
- /** 转义 XML 特殊字符 */
826
- function escapeXml(s: string): string {
827
- return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
828
- }
829
-
830
- /**
831
- * 构建技能列表的 XML 摘要,供 model 区分可用/不可用及缺失依赖
832
- */
833
- export function buildSkillsSummaryXML(skills: SkillMeta[]): string {
834
- if (skills.length === 0) return '';
835
- const lines = ['<skills>'];
836
- for (const s of skills) {
837
- const available = s.available !== false;
838
- lines.push(` <skill available="${available}">`);
839
- lines.push(` <name>${escapeXml(s.name)}</name>`);
840
- lines.push(` <description>${escapeXml(s.description)}</description>`);
841
- lines.push(` <location>${escapeXml(s.filePath)}</location>`);
842
- if (!available && s.requiresMissing && s.requiresMissing.length > 0) {
843
- lines.push(` <requires>${escapeXml(s.requiresMissing.join(', '))}</requires>`);
844
- }
845
- lines.push(' </skill>');
846
- }
847
- lines.push('</skills>');
848
- return lines.join('\n');
849
- }