@code4bug/jarvis-agent 1.0.2 → 1.0.3

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 (59) hide show
  1. package/LICENSE +1 -1
  2. package/dist/cli.js +2 -2
  3. package/dist/commands/index.js +2 -2
  4. package/dist/commands/init.js +1 -1
  5. package/dist/components/MessageItem.d.ts +1 -1
  6. package/dist/components/MessageItem.js +10 -2
  7. package/dist/components/MultilineInput.d.ts +7 -1
  8. package/dist/components/MultilineInput.js +148 -4
  9. package/dist/components/SlashCommandMenu.d.ts +1 -1
  10. package/dist/components/StatusBar.js +1 -1
  11. package/dist/components/StreamingText.js +1 -1
  12. package/dist/components/WelcomeHeader.js +1 -1
  13. package/dist/config/constants.js +3 -3
  14. package/dist/config/loader.d.ts +2 -0
  15. package/dist/core/QueryEngine.d.ts +4 -4
  16. package/dist/core/QueryEngine.js +19 -17
  17. package/dist/core/WorkerBridge.d.ts +9 -0
  18. package/dist/core/WorkerBridge.js +109 -0
  19. package/dist/core/hint.js +4 -4
  20. package/dist/core/query.d.ts +8 -1
  21. package/dist/core/query.js +279 -57
  22. package/dist/core/queryWorker.d.ts +44 -0
  23. package/dist/core/queryWorker.js +66 -0
  24. package/dist/core/safeguard.js +1 -1
  25. package/dist/hooks/useDoubleCtrlCExit.d.ts +5 -0
  26. package/dist/hooks/useDoubleCtrlCExit.js +34 -0
  27. package/dist/hooks/useInputHistory.js +35 -3
  28. package/dist/hooks/useSlashMenu.d.ts +36 -0
  29. package/dist/hooks/useSlashMenu.js +216 -0
  30. package/dist/hooks/useStreamThrottle.d.ts +20 -0
  31. package/dist/hooks/useStreamThrottle.js +120 -0
  32. package/dist/hooks/useTerminalWidth.d.ts +2 -0
  33. package/dist/hooks/useTerminalWidth.js +13 -0
  34. package/dist/hooks/useTokenDisplay.d.ts +13 -0
  35. package/dist/hooks/useTokenDisplay.js +45 -0
  36. package/dist/index.js +1 -1
  37. package/dist/screens/repl.js +164 -636
  38. package/dist/screens/slashCommands.d.ts +7 -0
  39. package/dist/screens/slashCommands.js +134 -0
  40. package/dist/services/api/llm.d.ts +4 -2
  41. package/dist/services/api/llm.js +70 -16
  42. package/dist/services/api/mock.d.ts +1 -1
  43. package/dist/skills/index.d.ts +2 -2
  44. package/dist/skills/index.js +3 -3
  45. package/dist/tools/createSkill.d.ts +1 -1
  46. package/dist/tools/createSkill.js +3 -3
  47. package/dist/tools/index.d.ts +9 -8
  48. package/dist/tools/index.js +10 -9
  49. package/dist/tools/listDirectory.d.ts +1 -1
  50. package/dist/tools/readFile.d.ts +1 -1
  51. package/dist/tools/runCommand.d.ts +1 -1
  52. package/dist/tools/runCommand.js +38 -7
  53. package/dist/tools/searchFiles.d.ts +1 -1
  54. package/dist/tools/semanticSearch.d.ts +9 -0
  55. package/dist/tools/semanticSearch.js +159 -0
  56. package/dist/tools/writeFile.d.ts +1 -1
  57. package/dist/tools/writeFile.js +125 -25
  58. package/dist/types/index.d.ts +10 -1
  59. package/package.json +1 -1
@@ -0,0 +1,7 @@
1
+ import { Message } from '../types/index';
2
+ /**
3
+ * 斜杠命令执行器
4
+ *
5
+ * 纯函数,返回要追加的系统消息。不涉及 React 状态。
6
+ */
7
+ export declare function executeSlashCommand(cmdName: string): Message | null;
@@ -0,0 +1,134 @@
1
+ import { executeInit } from '../commands/init';
2
+ import { APP_VERSION } from '../config/constants';
3
+ import { allTools } from '../tools/index';
4
+ import { listSkills } from '../skills/index';
5
+ import { getExternalSkillsDir } from '../skills/loader';
6
+ import { listPermanentAuthorizations, DANGER_RULES, } from '../core/safeguard.js';
7
+ /**
8
+ * 斜杠命令执行器
9
+ *
10
+ * 纯函数,返回要追加的系统消息。不涉及 React 状态。
11
+ */
12
+ export function executeSlashCommand(cmdName) {
13
+ switch (cmdName) {
14
+ case 'init': {
15
+ const result = executeInit();
16
+ return {
17
+ id: `init-${Date.now()}`,
18
+ type: 'system',
19
+ status: 'success',
20
+ content: result.displayText,
21
+ timestamp: Date.now(),
22
+ };
23
+ }
24
+ case 'help': {
25
+ const helpText = [
26
+ '可用命令:',
27
+ ' /init 初始化项目信息,生成 JARVIS.md',
28
+ ' /new 开启新会话,重新初始化上下文',
29
+ ' /resume 恢复历史会话(支持二级菜单选择)',
30
+ ' /resume <ID> 直接恢复指定会话',
31
+ ' /help 显示此帮助信息',
32
+ ' /session_clear 清理所有非当前会话的历史记录',
33
+ ' /skills 查看当前所有 tools 和 skills',
34
+ ' /permissions 查看所有持久化授权列表',
35
+ ' /create_skill <描述> 根据需求创建新 skill',
36
+ ' /agent <名称> 切换智能体(需重启生效)',
37
+ ' /read <路径> 读取文件内容',
38
+ ' /write <路径> 写入文件',
39
+ ' /bash <命令> 执行 Bash 命令',
40
+ ' /ls <路径> 列出目录',
41
+ ' /search <词> 搜索文件内容',
42
+ ' /version 显示当前版本号',
43
+ '',
44
+ '快捷键:',
45
+ ' Ctrl+L 清屏重置',
46
+ ' Ctrl+O 切换详情显示',
47
+ ' ESC 中断推理 / 双击清空输入',
48
+ ' Ctrl+C ×2 退出',
49
+ ].join('\n');
50
+ return {
51
+ id: `help-${Date.now()}`,
52
+ type: 'system',
53
+ status: 'success',
54
+ content: helpText,
55
+ timestamp: Date.now(),
56
+ };
57
+ }
58
+ case 'permissions': {
59
+ const perms = listPermanentAuthorizations();
60
+ const lines = ['持久化授权列表 (~/.jarvis/.permissions.json)', ''];
61
+ if (perms.rules.length > 0) {
62
+ lines.push('按规则授权:');
63
+ for (const r of perms.rules) {
64
+ const rule = DANGER_RULES.find((d) => d.name === r);
65
+ lines.push(` [v] ${r}${rule ? ` — ${rule.reason}` : ''}`);
66
+ }
67
+ lines.push('');
68
+ }
69
+ if (perms.commands.length > 0) {
70
+ lines.push('按命令授权:');
71
+ for (const c of perms.commands) {
72
+ lines.push(` [v] [${c.ruleName}] ${c.command} (${c.grantedAt})`);
73
+ }
74
+ lines.push('');
75
+ }
76
+ if (perms.rules.length === 0 && perms.commands.length === 0) {
77
+ lines.push('(空) 暂无持久化授权记录');
78
+ }
79
+ return {
80
+ id: `perms-${Date.now()}`,
81
+ type: 'system',
82
+ status: 'success',
83
+ content: lines.join('\n'),
84
+ timestamp: Date.now(),
85
+ };
86
+ }
87
+ case 'skills': {
88
+ const skills = listSkills();
89
+ const parts = [];
90
+ parts.push('### Built-in Tools\n');
91
+ allTools.forEach((t, i) => {
92
+ parts.push(`${i + 1}. \`${t.name}\` - ${t.description.slice(0, 60)}`);
93
+ });
94
+ parts.push('');
95
+ parts.push(`### External Skills\n`);
96
+ parts.push(`> ${getExternalSkillsDir()}\n`);
97
+ if (skills.length === 0) {
98
+ parts.push('_(empty)_');
99
+ }
100
+ else {
101
+ skills.forEach((s, i) => {
102
+ const hint = s.meta.argumentHint ? ` \`${s.meta.argumentHint}\`` : '';
103
+ const flags = [];
104
+ if (s.meta.disableModelInvocation)
105
+ flags.push('manual-only');
106
+ if (s.meta.userInvocable === false)
107
+ flags.push('hidden');
108
+ const flagStr = flags.length > 0 ? ` _(${flags.join(', ')})_` : '';
109
+ parts.push(`${i + 1}. \`${s.meta.name}\`${hint} - ${s.meta.description}${flagStr}`);
110
+ });
111
+ }
112
+ parts.push('');
113
+ parts.push(`**Total:** ${allTools.length} tools + ${skills.length} skills = ${allTools.length + skills.length}`);
114
+ return {
115
+ id: `skills-${Date.now()}`,
116
+ type: 'system',
117
+ status: 'success',
118
+ content: parts.join('\n'),
119
+ timestamp: Date.now(),
120
+ };
121
+ }
122
+ case 'version': {
123
+ return {
124
+ id: `version-${Date.now()}`,
125
+ type: 'system',
126
+ status: 'success',
127
+ content: `当前版本: ${APP_VERSION}`,
128
+ timestamp: Date.now(),
129
+ };
130
+ }
131
+ default:
132
+ return null;
133
+ }
134
+ }
@@ -6,14 +6,16 @@
6
6
  * 2. ~/.jarvis/config.json
7
7
  * 3. ./.jarvis/config.json
8
8
  */
9
- import { LLMService, StreamCallbacks, TranscriptMessage, Tool, AbortSignal as AppAbortSignal } from '../../types/index.js';
10
- import { ModelConfig } from '../../config/loader.js';
9
+ import { LLMService, StreamCallbacks, TranscriptMessage, Tool, AbortSignal as AppAbortSignal } from '../../types/index';
10
+ import { ModelConfig } from '../../config/loader';
11
11
  export interface LLMConfig {
12
12
  apiKey: string;
13
13
  model: string;
14
14
  maxTokens: number;
15
15
  baseUrl?: string;
16
16
  temperature?: number;
17
+ /** 额外请求体参数,直接合并到 API 请求 body */
18
+ extraBody?: Record<string, unknown>;
17
19
  }
18
20
  /** 从配置文件构建 LLMConfig,找不到则回退环境变量 */
19
21
  export declare function getDefaultConfig(): LLMConfig;
@@ -6,11 +6,11 @@
6
6
  * 2. ~/.jarvis/config.json
7
7
  * 3. ./.jarvis/config.json
8
8
  */
9
- import { loadConfig, getActiveModel } from '../../config/loader.js';
10
- import { getAgent } from '../../agents/index.js';
11
- import { DEFAULT_AGENT } from '../../config/constants.js';
12
- import { getActiveAgent } from '../../config/agentState.js';
13
- import { getSystemInfoPrompt } from '../../config/systemInfo.js';
9
+ import { loadConfig, getActiveModel } from '../../config/loader';
10
+ import { getAgent } from '../../agents/index';
11
+ import { DEFAULT_AGENT } from '../../config/constants';
12
+ import { getActiveAgent } from '../../config/agentState';
13
+ import { getSystemInfoPrompt } from '../../config/systemInfo';
14
14
  /** 从配置文件构建 LLMConfig,找不到则回退环境变量 */
15
15
  export function getDefaultConfig() {
16
16
  const jarvisCfg = loadConfig();
@@ -34,6 +34,7 @@ export function fromModelConfig(mc) {
34
34
  maxTokens: mc.max_tokens ?? 4096,
35
35
  baseUrl: mc.api_url,
36
36
  temperature: mc.temperature,
37
+ extraBody: mc.extra_body,
37
38
  };
38
39
  }
39
40
  /** 将内部 TranscriptMessage[] 转为 OpenAI messages 格式 */
@@ -118,7 +119,15 @@ function parseSSELine(line) {
118
119
  return null;
119
120
  try {
120
121
  const parsed = JSON.parse(data);
121
- return parsed.choices?.[0]?.delta ?? null;
122
+ const choice = parsed.choices?.[0];
123
+ if (!choice)
124
+ return null;
125
+ const delta = choice.delta;
126
+ if (!delta)
127
+ return null;
128
+ // 兼容 Qwen/vLLM:thinking 内容可能在 delta.reasoning_content 或 delta.content(role=thinking 时)
129
+ // 部分 vLLM 部署会将 thinking 内容放在 content 字段,通过 choice.finish_reason 或特殊标记区分
130
+ return delta;
122
131
  }
123
132
  catch {
124
133
  return null;
@@ -166,6 +175,10 @@ export class LLMServiceImpl {
166
175
  body.tools = openaiTools;
167
176
  body.tool_choice = 'auto';
168
177
  }
178
+ // 合并额外请求体参数(如 enable_thinking、chat_template_kwargs 等)
179
+ if (this.config.extraBody) {
180
+ Object.assign(body, this.config.extraBody);
181
+ }
169
182
  const url = this.config.baseUrl || 'https://api.openai.com/v1/chat/completions';
170
183
  // 创建 AbortController 用于取消 HTTP 请求
171
184
  const controller = new AbortController();
@@ -213,6 +226,15 @@ export class LLMServiceImpl {
213
226
  let buffer = '';
214
227
  // 用于累积 tool_calls(可能跨多个 chunk)
215
228
  const pendingToolCalls = new Map();
229
+ // 轮询 abortSignal,一旦外部中断立即 abort HTTP 请求,打断 reader.read() 阻塞
230
+ const abortPollTimer = abortSignal
231
+ ? setInterval(() => {
232
+ if (abortSignal.aborted) {
233
+ controller.abort();
234
+ clearInterval(abortPollTimer);
235
+ }
236
+ }, 50)
237
+ : null;
216
238
  try {
217
239
  while (true) {
218
240
  // 检查是否需要中断
@@ -240,8 +262,8 @@ export class LLMServiceImpl {
240
262
  if (delta.reasoning_content && callbacks.onThinking) {
241
263
  callbacks.onThinking(delta.reasoning_content);
242
264
  }
243
- // 文本内容
244
- if (delta.content) {
265
+ // 文本内容(注意:用 != null 判断,避免空字符串 "" 被跳过)
266
+ if (delta.content != null && delta.content !== '') {
245
267
  callbacks.onText(delta.content);
246
268
  }
247
269
  // 工具调用(增量拼接)
@@ -285,16 +307,46 @@ export class LLMServiceImpl {
285
307
  }
286
308
  }
287
309
  }
288
- // 流结束后,触发工具调用回调(只取第一个,与 agentic loop 单步设计一致)
310
+ // 流结束后,触发工具调用回调
289
311
  if (pendingToolCalls.size > 0) {
290
- const first = pendingToolCalls.get(0);
291
- if (first && first.name) {
292
- let input = {};
293
- try {
294
- input = JSON.parse(first.args);
312
+ // index 排序,收集所有有效的 tool_call
313
+ const sortedCalls = Array.from(pendingToolCalls.entries())
314
+ .sort(([a], [b]) => a - b)
315
+ .map(([, tc]) => tc)
316
+ .filter((tc) => tc.name);
317
+ if (sortedCalls.length > 0) {
318
+ if (sortedCalls.length === 1) {
319
+ // 单工具:走原有路径
320
+ const tc = sortedCalls[0];
321
+ let input = {};
322
+ try {
323
+ input = JSON.parse(tc.args);
324
+ }
325
+ catch { /* 参数解析失败则传空 */ }
326
+ callbacks.onToolUse(tc.id, tc.name, input);
327
+ }
328
+ else if (callbacks.onMultiToolUse) {
329
+ // 多工具:触发并行回调
330
+ const calls = sortedCalls.map((tc) => {
331
+ let input = {};
332
+ try {
333
+ input = JSON.parse(tc.args);
334
+ }
335
+ catch { /* ignore */ }
336
+ return { id: tc.id, name: tc.name, input };
337
+ });
338
+ callbacks.onMultiToolUse(calls);
339
+ }
340
+ else {
341
+ // 降级:逐个触发 onToolUse(只触发第一个,保持原有行为)
342
+ const tc = sortedCalls[0];
343
+ let input = {};
344
+ try {
345
+ input = JSON.parse(tc.args);
346
+ }
347
+ catch { /* ignore */ }
348
+ callbacks.onToolUse(tc.id, tc.name, input);
295
349
  }
296
- catch { /* 参数解析失败则传空 */ }
297
- callbacks.onToolUse(first.id, first.name, input);
298
350
  return; // tool_use 时不触发 onComplete
299
351
  }
300
352
  }
@@ -308,6 +360,8 @@ export class LLMServiceImpl {
308
360
  callbacks.onError(new Error(`流式读取失败: ${err.message}`));
309
361
  }
310
362
  finally {
363
+ if (abortPollTimer)
364
+ clearInterval(abortPollTimer);
311
365
  reader.releaseLock();
312
366
  }
313
367
  }
@@ -1,4 +1,4 @@
1
- import { LLMService, StreamCallbacks, TranscriptMessage, Tool, AbortSignal as AppAbortSignal } from '../../types/index.js';
1
+ import { LLMService, StreamCallbacks, TranscriptMessage, Tool, AbortSignal as AppAbortSignal } from '../../types/index';
2
2
  /**
3
3
  * Mock LLM 服务 - 模拟智能体行为,支持工具调用
4
4
  */
@@ -9,8 +9,8 @@
9
9
  * 如果 skill 目录下存在 skill.py,execute 时直接调用 Python 脚本获取真实结果;
10
10
  * 否则回退为返回 skill 指令文本(由 LLM 解释执行)。
11
11
  */
12
- import { Tool } from '../types/index.js';
13
- import { SkillDefinition } from './loader.js';
12
+ import { Tool } from '../types/index';
13
+ import { SkillDefinition } from './loader';
14
14
  /** 加载所有外部 skills(带缓存) */
15
15
  export declare function loadExternalSkills(): SkillDefinition[];
16
16
  /** 获取合并后的所有工具:内置 tools + 外部 skills */
@@ -12,8 +12,8 @@
12
12
  import { exec } from 'child_process';
13
13
  import fs from 'fs';
14
14
  import path from 'path';
15
- import { scanExternalSkills, getExternalSkillsDir } from './loader.js';
16
- import { allTools as builtinTools } from '../tools/index.js';
15
+ import { scanExternalSkills, getExternalSkillsDir } from './loader';
16
+ import { allTools as builtinTools } from '../tools/index';
17
17
  // ===== 缓存 =====
18
18
  let _skillCache = null;
19
19
  let _mergedTools = null;
@@ -110,7 +110,7 @@ async function executeSkillScript(scriptPath, skill, args) {
110
110
  // 写入临时 Python 文件,避免 shell -c 的转义问题
111
111
  const tmpFile = path.join(skill.dirPath, `_tmp_run_${Date.now()}.py`);
112
112
  const pyCode = [
113
- 'import sys, json',
113
+ 'import sys,os,json',
114
114
  `sys.path.insert(0, ${JSON.stringify(path.dirname(scriptPath))})`,
115
115
  `from skill import ${funcName}`,
116
116
  `result = ${funcName}(${kwargs.join(', ')})`,
@@ -4,5 +4,5 @@
4
4
  * 根据用户需求,调用 LLM 基于 SKILL_INSTRUCTIONS.md 规范自动生成 skill,
5
5
  * 并写入 ~/.jarvis/skills/ 目录。
6
6
  */
7
- import { Tool } from '../types/index.js';
7
+ import { Tool } from '../types/index';
8
8
  export declare const createSkill: Tool;
@@ -7,9 +7,9 @@
7
7
  import fs from 'fs';
8
8
  import path from 'path';
9
9
  import os from 'os';
10
- import { getExternalSkillsDir } from '../skills/loader.js';
11
- import { reloadSkills } from '../skills/index.js';
12
- import { LLMServiceImpl, getDefaultConfig } from '../services/api/llm.js';
10
+ import { getExternalSkillsDir } from '../skills/loader';
11
+ import { reloadSkills } from '../skills/index';
12
+ import { LLMServiceImpl, getDefaultConfig } from '../services/api/llm';
13
13
  // SKILL_INSTRUCTIONS.md 查找路径:项目根目录 > 用户主目录
14
14
  function loadSkillInstructions() {
15
15
  const candidates = [
@@ -1,11 +1,12 @@
1
- import { Tool } from '../types/index.js';
2
- import { readFile } from './readFile.js';
3
- import { writeFile } from './writeFile.js';
4
- import { runCommand } from './runCommand.js';
5
- import { listDirectory } from './listDirectory.js';
6
- import { searchFiles } from './searchFiles.js';
7
- import { createSkill } from './createSkill.js';
8
- export { readFile, writeFile, runCommand, listDirectory, searchFiles, createSkill };
1
+ import { Tool } from '../types/index';
2
+ import { readFile } from './readFile';
3
+ import { writeFile } from './writeFile';
4
+ import { runCommand } from './runCommand';
5
+ import { listDirectory } from './listDirectory';
6
+ import { searchFiles } from './searchFiles';
7
+ import { semanticSearch } from './semanticSearch';
8
+ import { createSkill } from './createSkill';
9
+ export { readFile, writeFile, runCommand, listDirectory, searchFiles, semanticSearch, createSkill };
9
10
  /** 所有内置工具 */
10
11
  export declare const allTools: Tool[];
11
12
  /** 按名称查找内置工具 */
@@ -1,18 +1,19 @@
1
- import { readFile } from './readFile.js';
2
- import { writeFile } from './writeFile.js';
3
- import { runCommand } from './runCommand.js';
4
- import { listDirectory } from './listDirectory.js';
5
- import { searchFiles } from './searchFiles.js';
6
- import { createSkill } from './createSkill.js';
7
- export { readFile, writeFile, runCommand, listDirectory, searchFiles, createSkill };
1
+ import { readFile } from './readFile';
2
+ import { writeFile } from './writeFile';
3
+ import { runCommand } from './runCommand';
4
+ import { listDirectory } from './listDirectory';
5
+ import { searchFiles } from './searchFiles';
6
+ import { semanticSearch } from './semanticSearch';
7
+ import { createSkill } from './createSkill';
8
+ export { readFile, writeFile, runCommand, listDirectory, searchFiles, semanticSearch, createSkill };
8
9
  /** 所有内置工具 */
9
- export const allTools = [readFile, writeFile, runCommand, listDirectory, searchFiles, createSkill];
10
+ export const allTools = [readFile, writeFile, runCommand, listDirectory, searchFiles, semanticSearch, createSkill];
10
11
  /** 按名称查找内置工具 */
11
12
  export function findTool(name) {
12
13
  return allTools.find((t) => t.name === name);
13
14
  }
14
15
  // ===== 合并工具(内置 + 外部 Skills)=====
15
- import { getMergedTools, findMergedTool } from '../skills/index.js';
16
+ import { getMergedTools, findMergedTool } from '../skills/index';
16
17
  /** 获取所有工具(内置 + 外部 skills),供 QueryEngine 使用 */
17
18
  export function getAllTools() {
18
19
  return getMergedTools();
@@ -1,2 +1,2 @@
1
- import { Tool } from '../types/index.js';
1
+ import { Tool } from '../types/index';
2
2
  export declare const listDirectory: Tool;
@@ -1,2 +1,2 @@
1
- import { Tool } from '../types/index.js';
1
+ import { Tool } from '../types/index';
2
2
  export declare const readFile: Tool;
@@ -1,2 +1,2 @@
1
- import { Tool } from '../types/index.js';
1
+ import { Tool } from '../types/index';
2
2
  export declare const runCommand: Tool;
@@ -1,11 +1,20 @@
1
1
  import { exec } from 'child_process';
2
- import { sanitizeOutput } from '../core/safeguard.js';
2
+ import { sanitizeOutput } from '../core/safeguard';
3
3
  /**
4
- * 异步执行命令,不阻塞事件循环,保证 TUI 渲染正常
4
+ * 异步执行命令,支持通过 abortSignal 中断子进程
5
5
  */
6
- function execAsync(command, options) {
6
+ function execAsync(command, options, abortSignal) {
7
7
  return new Promise((resolve, reject) => {
8
- exec(command, options, (error, stdout, stderr) => {
8
+ const child = exec(command, options, (error, stdout, stderr) => {
9
+ // 清理轮询
10
+ if (pollTimer !== null)
11
+ clearInterval(pollTimer);
12
+ // 被中断时直接返回已有输出,不视为错误
13
+ if (abortSignal?.aborted) {
14
+ const partial = sanitizeOutput(String(stdout ?? '').trim());
15
+ resolve(partial ? `(命令被中断)\n${partial}` : '(命令被中断)');
16
+ return;
17
+ }
9
18
  if (error) {
10
19
  const parts = [];
11
20
  if (stderr)
@@ -21,6 +30,29 @@ function execAsync(command, options) {
21
30
  }
22
31
  resolve(sanitizeOutput(String(stdout).trim()) || '(命令执行完成,无输出)');
23
32
  });
33
+ // 轮询 abortSignal,检测到中断时 kill 子进程
34
+ let pollTimer = null;
35
+ if (abortSignal) {
36
+ pollTimer = setInterval(() => {
37
+ if (abortSignal.aborted && child.pid) {
38
+ if (pollTimer !== null)
39
+ clearInterval(pollTimer);
40
+ pollTimer = null;
41
+ // 先尝试 SIGTERM,给进程优雅退出的机会
42
+ try {
43
+ child.kill('SIGTERM');
44
+ }
45
+ catch { /* ignore */ }
46
+ // 500ms 后强制 SIGKILL
47
+ setTimeout(() => {
48
+ try {
49
+ child.kill('SIGKILL');
50
+ }
51
+ catch { /* ignore */ }
52
+ }, 500);
53
+ }
54
+ }, 100);
55
+ }
24
56
  });
25
57
  }
26
58
  export const runCommand = {
@@ -29,15 +61,14 @@ export const runCommand = {
29
61
  parameters: {
30
62
  command: { type: 'string', description: '要执行的 Bash 命令', required: true },
31
63
  },
32
- execute: async (args) => {
64
+ execute: async (args, abortSignal) => {
33
65
  const command = args.command;
34
- // 安全围栏拦截已在 query 层(executeTool)统一处理,此处仅负责执行 + 脱敏
35
66
  return execAsync(command, {
36
67
  encoding: 'utf-8',
37
68
  timeout: 30000,
38
69
  maxBuffer: 1024 * 1024,
39
70
  env: sanitizeEnv(process.env),
40
- });
71
+ }, abortSignal);
41
72
  },
42
73
  };
43
74
  /**
@@ -1,2 +1,2 @@
1
- import { Tool } from '../types/index.js';
1
+ import { Tool } from '../types/index';
2
2
  export declare const searchFiles: Tool;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 语义搜索工具
3
+ *
4
+ * 当配置了大模型时,先通过 LLM 对中文关键词进行语义扩展(同义词、近义词、相关表达),
5
+ * 再用扩展后的词集合在文件中进行匹配搜索,实现"相似搜索"能力。
6
+ * 未配置大模型时降级为普通关键词搜索。
7
+ */
8
+ import { Tool } from '../types/index';
9
+ export declare const semanticSearch: Tool;