@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.
- package/LICENSE +1 -1
- package/dist/cli.js +2 -2
- package/dist/commands/index.js +2 -2
- package/dist/commands/init.js +1 -1
- package/dist/components/MessageItem.d.ts +1 -1
- package/dist/components/MessageItem.js +10 -2
- package/dist/components/MultilineInput.d.ts +7 -1
- package/dist/components/MultilineInput.js +148 -4
- package/dist/components/SlashCommandMenu.d.ts +1 -1
- package/dist/components/StatusBar.js +1 -1
- package/dist/components/StreamingText.js +1 -1
- package/dist/components/WelcomeHeader.js +1 -1
- package/dist/config/constants.js +3 -3
- package/dist/config/loader.d.ts +2 -0
- package/dist/core/QueryEngine.d.ts +4 -4
- package/dist/core/QueryEngine.js +19 -17
- package/dist/core/WorkerBridge.d.ts +9 -0
- package/dist/core/WorkerBridge.js +109 -0
- package/dist/core/hint.js +4 -4
- package/dist/core/query.d.ts +8 -1
- package/dist/core/query.js +279 -57
- package/dist/core/queryWorker.d.ts +44 -0
- package/dist/core/queryWorker.js +66 -0
- package/dist/core/safeguard.js +1 -1
- package/dist/hooks/useDoubleCtrlCExit.d.ts +5 -0
- package/dist/hooks/useDoubleCtrlCExit.js +34 -0
- package/dist/hooks/useInputHistory.js +35 -3
- package/dist/hooks/useSlashMenu.d.ts +36 -0
- package/dist/hooks/useSlashMenu.js +216 -0
- package/dist/hooks/useStreamThrottle.d.ts +20 -0
- package/dist/hooks/useStreamThrottle.js +120 -0
- package/dist/hooks/useTerminalWidth.d.ts +2 -0
- package/dist/hooks/useTerminalWidth.js +13 -0
- package/dist/hooks/useTokenDisplay.d.ts +13 -0
- package/dist/hooks/useTokenDisplay.js +45 -0
- package/dist/index.js +1 -1
- package/dist/screens/repl.js +164 -636
- package/dist/screens/slashCommands.d.ts +7 -0
- package/dist/screens/slashCommands.js +134 -0
- package/dist/services/api/llm.d.ts +4 -2
- package/dist/services/api/llm.js +70 -16
- package/dist/services/api/mock.d.ts +1 -1
- package/dist/skills/index.d.ts +2 -2
- package/dist/skills/index.js +3 -3
- package/dist/tools/createSkill.d.ts +1 -1
- package/dist/tools/createSkill.js +3 -3
- package/dist/tools/index.d.ts +9 -8
- package/dist/tools/index.js +10 -9
- package/dist/tools/listDirectory.d.ts +1 -1
- package/dist/tools/readFile.d.ts +1 -1
- package/dist/tools/runCommand.d.ts +1 -1
- package/dist/tools/runCommand.js +38 -7
- package/dist/tools/searchFiles.d.ts +1 -1
- package/dist/tools/semanticSearch.d.ts +9 -0
- package/dist/tools/semanticSearch.js +159 -0
- package/dist/tools/writeFile.d.ts +1 -1
- package/dist/tools/writeFile.js +125 -25
- package/dist/types/index.d.ts +10 -1
- package/package.json +1 -1
package/dist/core/query.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Message, LoopState, Tool, LLMService, TranscriptMessage } from '../types/index.js';
|
|
1
|
+
import { Message, LoopState, Tool, LLMService, TranscriptMessage, ToolCallInfo } from '../types/index.js';
|
|
2
2
|
/** 危险命令确认结果 */
|
|
3
3
|
export type DangerConfirmResult = 'once' | 'always' | 'cancel';
|
|
4
4
|
export interface QueryCallbacks {
|
|
@@ -22,3 +22,10 @@ export interface QueryCallbacks {
|
|
|
22
22
|
export declare function executeQuery(userInput: string, transcript: TranscriptMessage[], _tools: Tool[], service: LLMService, callbacks: QueryCallbacks, abortSignal: {
|
|
23
23
|
aborted: boolean;
|
|
24
24
|
}): Promise<TranscriptMessage[]>;
|
|
25
|
+
/**
|
|
26
|
+
* 直接执行工具(供 Worker 线程调用)
|
|
27
|
+
* 注意:此函数在 Worker 线程中运行,不能使用 callbacks
|
|
28
|
+
*/
|
|
29
|
+
export declare function runToolDirect(tc: ToolCallInfo, abortSignal: {
|
|
30
|
+
aborted: boolean;
|
|
31
|
+
}): Promise<string>;
|
package/dist/core/query.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { v4 as uuid } from 'uuid';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
2
|
+
import { Worker } from 'worker_threads';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { findToolMerged as findTool } from '../tools/index';
|
|
6
|
+
import { MAX_ITERATIONS } from '../config/constants';
|
|
7
|
+
import { sanitizeOutput, validateCommand, authorizeCommand, authorizeRule } from './safeguard';
|
|
8
|
+
// 兼容 ESM __dirname
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
5
11
|
/**
|
|
6
12
|
* 单轮 Agentic Loop:推理 → 工具调用 → 循环
|
|
7
13
|
*/
|
|
@@ -19,50 +25,59 @@ export async function executeQuery(userInput, transcript, _tools, service, callb
|
|
|
19
25
|
loopState.iteration++;
|
|
20
26
|
callbacks.onLoopStateChange({ ...loopState });
|
|
21
27
|
const result = await runOneIteration(localTranscript, _tools, service, callbacks, abortSignal);
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
id: result.toolCall.id,
|
|
29
|
-
name: result.toolCall.name,
|
|
30
|
-
input: result.toolCall.input,
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
localTranscript.push({ role: 'assistant', content: blocks });
|
|
28
|
+
// 构建 assistant transcript 块
|
|
29
|
+
const assistantBlocks = [];
|
|
30
|
+
if (result.text)
|
|
31
|
+
assistantBlocks.push({ type: 'text', text: result.text });
|
|
32
|
+
for (const tc of result.toolCalls) {
|
|
33
|
+
assistantBlocks.push({ type: 'tool_use', id: tc.id, name: tc.name, input: tc.input });
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
localTranscript.push({
|
|
37
|
-
role: 'assistant',
|
|
38
|
-
content: [{
|
|
39
|
-
type: 'tool_use',
|
|
40
|
-
id: result.toolCall.id,
|
|
41
|
-
name: result.toolCall.name,
|
|
42
|
-
input: result.toolCall.input,
|
|
43
|
-
}],
|
|
44
|
-
});
|
|
35
|
+
if (assistantBlocks.length > 0) {
|
|
36
|
+
localTranscript.push({ role: 'assistant', content: assistantBlocks });
|
|
45
37
|
}
|
|
46
38
|
// 无工具调用 → 结束
|
|
47
|
-
if (
|
|
39
|
+
if (result.toolCalls.length === 0)
|
|
48
40
|
break;
|
|
49
|
-
|
|
41
|
+
// 中断发生在推理阶段
|
|
42
|
+
if (abortSignal.aborted) {
|
|
43
|
+
for (const tc of result.toolCalls) {
|
|
44
|
+
const skippedResult = `[用户中断] 工具 ${tc.name} 未执行(用户按下 ESC 中断)`;
|
|
45
|
+
localTranscript.push({ role: 'tool_result', toolUseId: tc.id, content: skippedResult });
|
|
46
|
+
const skipMsgId = uuid();
|
|
47
|
+
callbacks.onMessage({
|
|
48
|
+
id: skipMsgId, type: 'tool_exec', status: 'aborted',
|
|
49
|
+
content: `${tc.name} 已跳过(用户中断)`, timestamp: Date.now(),
|
|
50
|
+
toolName: tc.name, toolArgs: tc.input, toolResult: skippedResult,
|
|
51
|
+
abortHint: '命令已跳过(ESC)',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
50
54
|
break;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
}
|
|
56
|
+
// 判断是否可并行执行
|
|
57
|
+
let toolResults;
|
|
58
|
+
if (result.toolCalls.length > 1 && canRunInParallel(result.toolCalls)) {
|
|
59
|
+
toolResults = await executeToolsInParallel(result.toolCalls, callbacks, abortSignal);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
toolResults = [];
|
|
63
|
+
for (const tc of result.toolCalls) {
|
|
64
|
+
const r = await executeTool(tc, callbacks, abortSignal);
|
|
65
|
+
toolResults.push({ tc, ...r });
|
|
66
|
+
if (r.isError)
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// 将所有工具结果写入 transcript
|
|
71
|
+
for (const { tc, content } of toolResults) {
|
|
72
|
+
localTranscript.push({ role: 'tool_result', toolUseId: tc.id, content });
|
|
73
|
+
}
|
|
74
|
+
// 任意工具出错则终止循环
|
|
75
|
+
if (toolResults.some((r) => r.isError))
|
|
60
76
|
break;
|
|
61
77
|
}
|
|
62
78
|
loopState.isRunning = false;
|
|
63
79
|
loopState.aborted = abortSignal.aborted;
|
|
64
80
|
callbacks.onLoopStateChange({ ...loopState });
|
|
65
|
-
// 如果被用户中断,在 transcript 中追加中断标记,让 LLM 知道上轮回复未完成
|
|
66
81
|
if (abortSignal.aborted) {
|
|
67
82
|
localTranscript.push({
|
|
68
83
|
role: 'user',
|
|
@@ -76,7 +91,7 @@ async function runOneIteration(transcript, tools, service, callbacks, abortSigna
|
|
|
76
91
|
const startTime = Date.now();
|
|
77
92
|
let accumulatedText = '';
|
|
78
93
|
let accumulatedThinking = '';
|
|
79
|
-
let
|
|
94
|
+
let toolCalls = [];
|
|
80
95
|
let tokenCount = 0;
|
|
81
96
|
let firstTokenTime = null;
|
|
82
97
|
const thinkingId = uuid();
|
|
@@ -88,23 +103,29 @@ async function runOneIteration(transcript, tools, service, callbacks, abortSigna
|
|
|
88
103
|
timestamp: Date.now(),
|
|
89
104
|
});
|
|
90
105
|
await new Promise((resolve, reject) => {
|
|
106
|
+
let resolved = false;
|
|
107
|
+
const safeResolve = () => { if (!resolved) {
|
|
108
|
+
resolved = true;
|
|
109
|
+
resolve();
|
|
110
|
+
} };
|
|
91
111
|
service
|
|
92
112
|
.streamMessage(transcript, tools, {
|
|
93
113
|
onThinking: (text) => {
|
|
94
|
-
if (abortSignal.aborted)
|
|
114
|
+
if (abortSignal.aborted) {
|
|
115
|
+
safeResolve();
|
|
95
116
|
return;
|
|
117
|
+
}
|
|
96
118
|
accumulatedThinking += text;
|
|
97
|
-
|
|
119
|
+
tokenCount++;
|
|
98
120
|
callbacks.onUpdateMessage(thinkingId, { content: accumulatedThinking });
|
|
99
121
|
},
|
|
100
122
|
onText: (text) => {
|
|
101
123
|
if (abortSignal.aborted) {
|
|
102
|
-
|
|
124
|
+
safeResolve();
|
|
103
125
|
return;
|
|
104
126
|
}
|
|
105
127
|
if (firstTokenTime === null) {
|
|
106
128
|
firstTokenTime = Date.now();
|
|
107
|
-
// 收到首 token,将 thinking 消息标记为完成(保留 think 内容)
|
|
108
129
|
callbacks.onUpdateMessage(thinkingId, {
|
|
109
130
|
status: 'success',
|
|
110
131
|
content: accumulatedThinking ? '思考完成' : '',
|
|
@@ -117,13 +138,21 @@ async function runOneIteration(transcript, tools, service, callbacks, abortSigna
|
|
|
117
138
|
},
|
|
118
139
|
onToolUse: (id, name, input) => {
|
|
119
140
|
if (abortSignal.aborted) {
|
|
120
|
-
|
|
141
|
+
safeResolve();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
toolCalls = [{ id, name, input }];
|
|
145
|
+
safeResolve();
|
|
146
|
+
},
|
|
147
|
+
onMultiToolUse: (calls) => {
|
|
148
|
+
if (abortSignal.aborted) {
|
|
149
|
+
safeResolve();
|
|
121
150
|
return;
|
|
122
151
|
}
|
|
123
|
-
|
|
124
|
-
|
|
152
|
+
toolCalls = calls;
|
|
153
|
+
safeResolve();
|
|
125
154
|
},
|
|
126
|
-
onComplete: () =>
|
|
155
|
+
onComplete: () => safeResolve(),
|
|
127
156
|
onError: (err) => reject(err),
|
|
128
157
|
}, abortSignal)
|
|
129
158
|
.catch(reject);
|
|
@@ -132,7 +161,6 @@ async function runOneIteration(transcript, tools, service, callbacks, abortSigna
|
|
|
132
161
|
const firstTokenLatency = firstTokenTime !== null ? firstTokenTime - startTime : 0;
|
|
133
162
|
const durationSec = duration / 1000;
|
|
134
163
|
const tokensPerSecond = durationSec > 0 ? tokenCount / durationSec : 0;
|
|
135
|
-
// 最终更新 thinking 消息状态
|
|
136
164
|
callbacks.onUpdateMessage(thinkingId, {
|
|
137
165
|
status: 'success',
|
|
138
166
|
content: accumulatedThinking ? '思考完成' : '',
|
|
@@ -141,7 +169,6 @@ async function runOneIteration(transcript, tools, service, callbacks, abortSigna
|
|
|
141
169
|
});
|
|
142
170
|
if (accumulatedText) {
|
|
143
171
|
const isAborted = abortSignal.aborted;
|
|
144
|
-
// 先清空流式文本,再 push reasoning 消息,避免 streamText 被后续工具消息挤到底部
|
|
145
172
|
callbacks.onClearStreamText?.();
|
|
146
173
|
callbacks.onMessage({
|
|
147
174
|
id: uuid(),
|
|
@@ -156,15 +183,199 @@ async function runOneIteration(transcript, tools, service, callbacks, abortSigna
|
|
|
156
183
|
...(isAborted ? { abortHint: '推理已中断(ESC)' } : {}),
|
|
157
184
|
});
|
|
158
185
|
}
|
|
159
|
-
|
|
186
|
+
else if (accumulatedThinking && toolCalls.length === 0) {
|
|
187
|
+
const isAborted = abortSignal.aborted;
|
|
188
|
+
callbacks.onClearStreamText?.();
|
|
189
|
+
callbacks.onStreamText(accumulatedThinking);
|
|
190
|
+
callbacks.onClearStreamText?.();
|
|
191
|
+
callbacks.onMessage({
|
|
192
|
+
id: uuid(),
|
|
193
|
+
type: 'reasoning',
|
|
194
|
+
status: isAborted ? 'aborted' : 'success',
|
|
195
|
+
content: accumulatedThinking,
|
|
196
|
+
timestamp: Date.now(),
|
|
197
|
+
duration,
|
|
198
|
+
tokenCount,
|
|
199
|
+
firstTokenLatency,
|
|
200
|
+
tokensPerSecond,
|
|
201
|
+
...(isAborted ? { abortHint: '推理已中断(ESC)' } : {}),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const effectiveText = accumulatedText || (accumulatedThinking && toolCalls.length === 0 ? accumulatedThinking : '');
|
|
205
|
+
return { text: effectiveText, toolCalls, duration, tokenCount, firstTokenLatency, tokensPerSecond };
|
|
206
|
+
}
|
|
207
|
+
// ===== 并行执行判断 =====
|
|
208
|
+
/** 判断一组工具调用是否可以并行执行(无写-写冲突、无读-写冲突) */
|
|
209
|
+
function canRunInParallel(calls) {
|
|
210
|
+
// 写操作工具集合
|
|
211
|
+
const WRITE_TOOLS = new Set(['WriteFile', 'Bash']);
|
|
212
|
+
// 读操作工具集合
|
|
213
|
+
const READ_TOOLS = new Set(['ReadFile', 'ListDirectory', 'SearchFiles', 'SemanticSearch']);
|
|
214
|
+
const hasWrite = calls.some((c) => WRITE_TOOLS.has(c.name));
|
|
215
|
+
const hasRead = calls.some((c) => READ_TOOLS.has(c.name));
|
|
216
|
+
// 有写操作时:写-写 或 读-写 混合均不可并行(避免竞态)
|
|
217
|
+
if (hasWrite)
|
|
218
|
+
return false;
|
|
219
|
+
// 全部是读操作 → 可并行
|
|
220
|
+
if (hasRead && !hasWrite)
|
|
221
|
+
return true;
|
|
222
|
+
// Bash 命令:检查是否有文件路径重叠(简单启发式)
|
|
223
|
+
const bashCalls = calls.filter((c) => c.name === 'Bash');
|
|
224
|
+
if (bashCalls.length > 1) {
|
|
225
|
+
// 多个 Bash 命令默认不并行(无法静态分析副作用)
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
// 其余情况(如多个只读 skill)允许并行
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
// ===== 并行工具执行(多 Worker 线程) =====
|
|
232
|
+
/** 在独立 Worker 线程中执行单个工具,返回结果字符串 */
|
|
233
|
+
function runToolInWorker(tc, abortSignal) {
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
const isTsx = __filename.endsWith('.ts');
|
|
236
|
+
const workerScript = isTsx
|
|
237
|
+
? `
|
|
238
|
+
import { tsImport } from 'tsx/esm/api';
|
|
239
|
+
import { workerData, parentPort } from 'worker_threads';
|
|
240
|
+
import { pathToFileURL } from 'url';
|
|
241
|
+
const mod = await tsImport(workerData.__file, pathToFileURL(workerData.__file).href);
|
|
242
|
+
const result = await mod.runToolDirect(workerData.tc, workerData.abortSignal);
|
|
243
|
+
parentPort.postMessage({ result });
|
|
244
|
+
`
|
|
245
|
+
: `
|
|
246
|
+
import { runToolDirect } from '${__filename.replace(/\.ts$/, '.js')}';
|
|
247
|
+
import { workerData, parentPort } from 'worker_threads';
|
|
248
|
+
const result = await runToolDirect(workerData.tc, workerData.abortSignal);
|
|
249
|
+
parentPort.postMessage({ result });
|
|
250
|
+
`;
|
|
251
|
+
const worker = new Worker(workerScript, {
|
|
252
|
+
eval: true,
|
|
253
|
+
workerData: {
|
|
254
|
+
__file: __filename,
|
|
255
|
+
tc,
|
|
256
|
+
abortSignal: { aborted: abortSignal.aborted },
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
worker.on('message', (msg) => {
|
|
260
|
+
worker.terminate();
|
|
261
|
+
resolve(msg.result);
|
|
262
|
+
});
|
|
263
|
+
worker.on('error', (err) => {
|
|
264
|
+
worker.terminate();
|
|
265
|
+
reject(err);
|
|
266
|
+
});
|
|
267
|
+
worker.on('exit', (code) => {
|
|
268
|
+
if (code !== 0)
|
|
269
|
+
reject(new Error(`工具 Worker 异常退出 code=${code}`));
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* 直接执行工具(供 Worker 线程调用)
|
|
275
|
+
* 注意:此函数在 Worker 线程中运行,不能使用 callbacks
|
|
276
|
+
*/
|
|
277
|
+
export async function runToolDirect(tc, abortSignal) {
|
|
278
|
+
// 动态导入避免循环依赖
|
|
279
|
+
const { findToolMerged } = await import('../tools/index.js');
|
|
280
|
+
const tool = findToolMerged(tc.name);
|
|
281
|
+
if (!tool)
|
|
282
|
+
return `错误: 未知工具 ${tc.name}`;
|
|
283
|
+
try {
|
|
284
|
+
const result = await tool.execute(tc.input, abortSignal);
|
|
285
|
+
const { sanitizeOutput } = await import('./safeguard.js');
|
|
286
|
+
return sanitizeOutput(result);
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
return `错误: ${err.message || '工具执行失败'}`;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/** 并行执行多个工具,每个工具在独立 Worker 线程中运行,实时更新 UI */
|
|
293
|
+
async function executeToolsInParallel(calls, callbacks, abortSignal) {
|
|
294
|
+
const groupId = uuid();
|
|
295
|
+
// 为每个工具预先创建 pending 消息节点(TUI 立即渲染占位)
|
|
296
|
+
const msgIds = calls.map((tc) => {
|
|
297
|
+
const msgId = uuid();
|
|
298
|
+
const isSkill = tc.name.startsWith('skill_');
|
|
299
|
+
const displayContent = tc.name === 'Bash' && tc.input.command
|
|
300
|
+
? `Bash(${tc.input.command})`
|
|
301
|
+
: isSkill
|
|
302
|
+
? `${tc.name.replace(/^skill_/, '')}(${Object.values(tc.input).join(', ')})`
|
|
303
|
+
: `调用工具: ${tc.name}`;
|
|
304
|
+
callbacks.onMessage({
|
|
305
|
+
id: msgId,
|
|
306
|
+
type: 'tool_exec',
|
|
307
|
+
status: 'pending',
|
|
308
|
+
content: displayContent,
|
|
309
|
+
timestamp: Date.now(),
|
|
310
|
+
toolName: tc.name,
|
|
311
|
+
toolArgs: tc.input,
|
|
312
|
+
parallelGroupId: groupId,
|
|
313
|
+
});
|
|
314
|
+
return msgId;
|
|
315
|
+
});
|
|
316
|
+
// 并行启动所有工具(各自在独立 Worker 线程)
|
|
317
|
+
const tasks = calls.map(async (tc, i) => {
|
|
318
|
+
const msgId = msgIds[i];
|
|
319
|
+
const start = Date.now();
|
|
320
|
+
// 安全围栏检查(Bash 命令)
|
|
321
|
+
if (tc.name === 'Bash' && tc.input.command) {
|
|
322
|
+
const { validateCommand } = await import('./safeguard.js');
|
|
323
|
+
const check = validateCommand(tc.input.command);
|
|
324
|
+
if (!check.allowed) {
|
|
325
|
+
if (!check.canOverride) {
|
|
326
|
+
const errMsg = `${check.reason}\n🚫 该命令已被永久禁止。\n命令: ${tc.input.command}`;
|
|
327
|
+
callbacks.onUpdateMessage(msgId, { status: 'error', content: errMsg, toolResult: errMsg });
|
|
328
|
+
return { tc, content: `错误: ${errMsg}`, isError: true };
|
|
329
|
+
}
|
|
330
|
+
// 危险命令在并行模式下直接跳过(无法弹出交互式确认)
|
|
331
|
+
const skipMsg = `⚠️ 并行模式下跳过危险命令: ${tc.input.command}\n原因: ${check.reason}`;
|
|
332
|
+
callbacks.onUpdateMessage(msgId, { status: 'error', content: skipMsg, toolResult: skipMsg });
|
|
333
|
+
return { tc, content: skipMsg, isError: true };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const content = await runToolInWorker(tc, abortSignal);
|
|
338
|
+
const wasAborted = abortSignal.aborted;
|
|
339
|
+
const isSkill = tc.name.startsWith('skill_');
|
|
340
|
+
const doneContent = wasAborted
|
|
341
|
+
? `${tc.name} 已中断`
|
|
342
|
+
: tc.name === 'Bash' && tc.input.command
|
|
343
|
+
? `Bash(${tc.input.command}) 执行完成`
|
|
344
|
+
: isSkill
|
|
345
|
+
? `${tc.name.replace(/^skill_/, '')}(${Object.values(tc.input).join(', ')}) 执行完成`
|
|
346
|
+
: `工具 ${tc.name} 执行完成`;
|
|
347
|
+
callbacks.onUpdateMessage(msgId, {
|
|
348
|
+
status: wasAborted ? 'aborted' : 'success',
|
|
349
|
+
content: doneContent,
|
|
350
|
+
toolResult: content,
|
|
351
|
+
duration: Date.now() - start,
|
|
352
|
+
parallelGroupId: groupId,
|
|
353
|
+
...(wasAborted ? { abortHint: '命令已中断(ESC)' } : {}),
|
|
354
|
+
});
|
|
355
|
+
return { tc, content, isError: false };
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
358
|
+
const errMsg = err.message || '工具执行失败';
|
|
359
|
+
callbacks.onUpdateMessage(msgId, { status: 'error', content: errMsg, toolResult: errMsg });
|
|
360
|
+
return { tc, content: `错误: ${errMsg}`, isError: false };
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
return Promise.all(tasks);
|
|
160
364
|
}
|
|
161
365
|
/** 执行工具并返回结果 */
|
|
162
|
-
async function executeTool(tc, callbacks) {
|
|
366
|
+
async function executeTool(tc, callbacks, abortSignal) {
|
|
163
367
|
const toolExecId = uuid();
|
|
164
|
-
// 对 Bash 工具使用更直观的显示格式
|
|
368
|
+
// 对 Bash / Skill 工具使用更直观的显示格式
|
|
369
|
+
const isSkill = tc.name.startsWith('skill_');
|
|
370
|
+
const skillName = isSkill ? tc.name.replace(/^skill_/, '') : '';
|
|
371
|
+
const skillArgsSummary = isSkill
|
|
372
|
+
? Object.values(tc.input).map((v) => String(v)).filter(Boolean).join(', ')
|
|
373
|
+
: '';
|
|
165
374
|
const displayContent = tc.name === 'Bash' && tc.input.command
|
|
166
375
|
? `Bash(${tc.input.command})`
|
|
167
|
-
:
|
|
376
|
+
: isSkill
|
|
377
|
+
? `${skillName}(${skillArgsSummary})`
|
|
378
|
+
: `调用工具: ${tc.name}`;
|
|
168
379
|
callbacks.onMessage({
|
|
169
380
|
id: toolExecId,
|
|
170
381
|
type: 'tool_exec',
|
|
@@ -219,17 +430,28 @@ async function executeTool(tc, callbacks) {
|
|
|
219
430
|
}
|
|
220
431
|
try {
|
|
221
432
|
const start = Date.now();
|
|
222
|
-
const result = await tool.execute(tc.input);
|
|
433
|
+
const result = await tool.execute(tc.input, abortSignal);
|
|
223
434
|
// 对工具输出统一脱敏
|
|
224
435
|
const safeResult = sanitizeOutput(result);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
436
|
+
// 工具执行期间被中断
|
|
437
|
+
const wasAborted = abortSignal?.aborted;
|
|
438
|
+
const doneContent = wasAborted
|
|
439
|
+
? (tc.name === 'Bash' && tc.input.command
|
|
440
|
+
? `Bash(${tc.input.command}) 已中断`
|
|
441
|
+
: isSkill
|
|
442
|
+
? `${skillName}(${skillArgsSummary}) 已中断`
|
|
443
|
+
: `工具 ${tc.name} 已中断`)
|
|
444
|
+
: (tc.name === 'Bash' && tc.input.command
|
|
445
|
+
? `Bash(${tc.input.command}) 执行完成`
|
|
446
|
+
: isSkill
|
|
447
|
+
? `${skillName}(${skillArgsSummary}) 执行完成`
|
|
448
|
+
: `工具 ${tc.name} 执行完成`);
|
|
228
449
|
callbacks.onUpdateMessage(toolExecId, {
|
|
229
|
-
status: 'success',
|
|
450
|
+
status: wasAborted ? 'aborted' : 'success',
|
|
230
451
|
content: doneContent,
|
|
231
452
|
toolResult: safeResult,
|
|
232
453
|
duration: Date.now() - start,
|
|
454
|
+
...(wasAborted ? { abortHint: '命令已中断(ESC)' } : {}),
|
|
233
455
|
});
|
|
234
456
|
return { content: safeResult, isError: false };
|
|
235
457
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { DangerConfirmResult } from './query.js';
|
|
2
|
+
import { TranscriptMessage, Message, LoopState, Session } from '../types/index.js';
|
|
3
|
+
export type WorkerInbound = {
|
|
4
|
+
type: 'run';
|
|
5
|
+
userInput: string;
|
|
6
|
+
transcript: TranscriptMessage[];
|
|
7
|
+
} | {
|
|
8
|
+
type: 'abort';
|
|
9
|
+
} | {
|
|
10
|
+
type: 'danger_confirm_result';
|
|
11
|
+
requestId: string;
|
|
12
|
+
choice: DangerConfirmResult;
|
|
13
|
+
};
|
|
14
|
+
export type WorkerOutbound = {
|
|
15
|
+
type: 'message';
|
|
16
|
+
msg: Message;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'update_message';
|
|
19
|
+
id: string;
|
|
20
|
+
updates: Partial<Message>;
|
|
21
|
+
} | {
|
|
22
|
+
type: 'stream_text';
|
|
23
|
+
text: string;
|
|
24
|
+
} | {
|
|
25
|
+
type: 'clear_stream_text';
|
|
26
|
+
} | {
|
|
27
|
+
type: 'loop_state';
|
|
28
|
+
state: LoopState;
|
|
29
|
+
} | {
|
|
30
|
+
type: 'session_update';
|
|
31
|
+
session: Session;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'danger_confirm_request';
|
|
34
|
+
requestId: string;
|
|
35
|
+
command: string;
|
|
36
|
+
reason: string;
|
|
37
|
+
ruleName: string;
|
|
38
|
+
} | {
|
|
39
|
+
type: 'done';
|
|
40
|
+
transcript: TranscriptMessage[];
|
|
41
|
+
} | {
|
|
42
|
+
type: 'error';
|
|
43
|
+
message: string;
|
|
44
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker 线程入口 — 在独立线程中执行 executeQuery
|
|
3
|
+
* 通过 parentPort.postMessage 将回调事件传回主线程
|
|
4
|
+
*/
|
|
5
|
+
import { parentPort } from 'worker_threads';
|
|
6
|
+
import { executeQuery } from './query.js';
|
|
7
|
+
import { getAllTools } from '../tools/index.js';
|
|
8
|
+
import { LLMServiceImpl } from '../services/api/llm.js';
|
|
9
|
+
import { MockService } from '../services/api/mock.js';
|
|
10
|
+
import { loadConfig, getActiveModel } from '../config/loader.js';
|
|
11
|
+
if (!parentPort)
|
|
12
|
+
throw new Error('queryWorker must run inside worker_threads');
|
|
13
|
+
// ===== 初始化 LLM 服务 =====
|
|
14
|
+
const config = loadConfig();
|
|
15
|
+
const activeModel = getActiveModel(config);
|
|
16
|
+
let service;
|
|
17
|
+
try {
|
|
18
|
+
service = activeModel ? new LLMServiceImpl() : new MockService();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
service = new MockService();
|
|
22
|
+
}
|
|
23
|
+
// ===== 中断信号 =====
|
|
24
|
+
const abortSignal = { aborted: false };
|
|
25
|
+
// ===== 危险命令确认:挂起 Promise,等待主线程回复 =====
|
|
26
|
+
const pendingConfirms = new Map();
|
|
27
|
+
// ===== 监听主线程消息 =====
|
|
28
|
+
parentPort.on('message', async (msg) => {
|
|
29
|
+
if (msg.type === 'abort') {
|
|
30
|
+
abortSignal.aborted = true;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (msg.type === 'danger_confirm_result') {
|
|
34
|
+
const resolve = pendingConfirms.get(msg.requestId);
|
|
35
|
+
if (resolve) {
|
|
36
|
+
pendingConfirms.delete(msg.requestId);
|
|
37
|
+
resolve(msg.choice);
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (msg.type === 'run') {
|
|
42
|
+
abortSignal.aborted = false;
|
|
43
|
+
const send = (out) => parentPort.postMessage(out);
|
|
44
|
+
const callbacks = {
|
|
45
|
+
onMessage: (m) => send({ type: 'message', msg: m }),
|
|
46
|
+
onUpdateMessage: (id, updates) => send({ type: 'update_message', id, updates }),
|
|
47
|
+
onStreamText: (text) => send({ type: 'stream_text', text }),
|
|
48
|
+
onClearStreamText: () => send({ type: 'clear_stream_text' }),
|
|
49
|
+
onLoopStateChange: (state) => send({ type: 'loop_state', state }),
|
|
50
|
+
onConfirmDangerousCommand: (command, reason, ruleName) => {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
const requestId = `${Date.now()}-${Math.random()}`;
|
|
53
|
+
pendingConfirms.set(requestId, resolve);
|
|
54
|
+
send({ type: 'danger_confirm_request', requestId, command, reason, ruleName });
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
try {
|
|
59
|
+
const newTranscript = await executeQuery(msg.userInput, msg.transcript, getAllTools(), service, callbacks, abortSignal);
|
|
60
|
+
send({ type: 'done', transcript: newTranscript });
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
send({ type: 'error', message: err.message ?? '未知错误' });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
package/dist/core/safeguard.js
CHANGED
|
@@ -42,7 +42,7 @@ export const DANGER_RULES = [
|
|
|
42
42
|
/** 敏感信息匹配规则 — 用于输出脱敏 */
|
|
43
43
|
export const SENSITIVE_PATTERNS = [
|
|
44
44
|
{ name: 'AWS Access Key', pattern: /\b(AKIA[0-9A-Z]{16})\b/g, replacement: '[AWS_ACCESS_KEY]' },
|
|
45
|
-
{ name: 'AWS Secret Key', pattern:
|
|
45
|
+
{ name: 'AWS Secret Key', pattern: /(?<![A-Za-z0-9/])([A-Za-z0-9+=]{40})(?![A-Za-z0-9/])/g, replacement: '[AWS_SECRET_KEY]' },
|
|
46
46
|
{ name: 'Generic API Key', pattern: /\b(api[_-]?key|apikey)\s*[:=]\s*['"]?([^\s'"]+)/gi, replacement: '$1=[REDACTED]' },
|
|
47
47
|
{ name: 'Generic Secret', pattern: /\b(secret|token|password|passwd|pwd)\s*[:=]\s*['"]?([^\s'"]+)/gi, replacement: '$1=[REDACTED]' },
|
|
48
48
|
{ name: 'Bearer Token', pattern: /Bearer\s+[A-Za-z0-9\-._~+/]+=*/g, replacement: 'Bearer [REDACTED]' },
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
/** 双击 Ctrl+C 退出:第一次按下后显示倒计时,3 秒内再按一次退出,否则取消 */
|
|
3
|
+
export function useDoubleCtrlCExit(exit) {
|
|
4
|
+
const [countdown, setCountdown] = useState(null);
|
|
5
|
+
const timerRef = useRef(null);
|
|
6
|
+
const clearTimer = useCallback(() => {
|
|
7
|
+
if (timerRef.current) {
|
|
8
|
+
clearInterval(timerRef.current);
|
|
9
|
+
timerRef.current = null;
|
|
10
|
+
}
|
|
11
|
+
setCountdown(null);
|
|
12
|
+
}, []);
|
|
13
|
+
const handleCtrlC = useCallback(() => {
|
|
14
|
+
if (countdown !== null) {
|
|
15
|
+
clearTimer();
|
|
16
|
+
exit();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
setCountdown(1);
|
|
20
|
+
timerRef.current = setInterval(() => {
|
|
21
|
+
setCountdown((prev) => {
|
|
22
|
+
if (prev === null || prev <= 1) {
|
|
23
|
+
clearTimer();
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return prev - 1;
|
|
27
|
+
});
|
|
28
|
+
}, 1000);
|
|
29
|
+
}, [countdown, clearTimer, exit]);
|
|
30
|
+
// 组件卸载时清理
|
|
31
|
+
useEffect(() => () => { if (timerRef.current)
|
|
32
|
+
clearInterval(timerRef.current); }, []);
|
|
33
|
+
return { countdown, handleCtrlC };
|
|
34
|
+
}
|
|
@@ -5,6 +5,28 @@ import os from 'os';
|
|
|
5
5
|
const HISTORY_DIR = path.join(os.homedir(), '.jarvis');
|
|
6
6
|
const HISTORY_FILE = path.join(HISTORY_DIR, '.input_history');
|
|
7
7
|
const MAX_HISTORY = 20;
|
|
8
|
+
// 历史记录使用 JSON 数组格式存储,天然支持多行内容,无需手动转义
|
|
9
|
+
/** 还原旧 encodeLine 格式:将字面量 \n 还原为真实换行,\\\\ 还原为 \\ */
|
|
10
|
+
function decodeLegacyLine(s) {
|
|
11
|
+
let result = '';
|
|
12
|
+
for (let i = 0; i < s.length; i++) {
|
|
13
|
+
if (s[i] === '\\' && i + 1 < s.length) {
|
|
14
|
+
const next = s[i + 1];
|
|
15
|
+
if (next === 'n') {
|
|
16
|
+
result += '\n';
|
|
17
|
+
i++;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (next === '\\') {
|
|
21
|
+
result += '\\';
|
|
22
|
+
i++;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
result += s[i];
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
8
30
|
/** 从文件加载历史记录 */
|
|
9
31
|
function loadHistory() {
|
|
10
32
|
try {
|
|
@@ -13,19 +35,29 @@ function loadHistory() {
|
|
|
13
35
|
const content = fs.readFileSync(HISTORY_FILE, 'utf-8').trim();
|
|
14
36
|
if (!content)
|
|
15
37
|
return [];
|
|
16
|
-
|
|
38
|
+
// 尝试 JSON 格式(新格式)
|
|
39
|
+
if (content.startsWith('[')) {
|
|
40
|
+
try {
|
|
41
|
+
const arr = JSON.parse(content);
|
|
42
|
+
if (Array.isArray(arr))
|
|
43
|
+
return arr.filter((s) => typeof s === 'string').slice(-MAX_HISTORY);
|
|
44
|
+
}
|
|
45
|
+
catch { /* JSON 解析失败,回退纯文本 */ }
|
|
46
|
+
}
|
|
47
|
+
// 回退:旧的纯文本格式,对每行做 decode 还原可能的转义
|
|
48
|
+
return content.split('\n').filter(Boolean).slice(-MAX_HISTORY).map(decodeLegacyLine);
|
|
17
49
|
}
|
|
18
50
|
catch {
|
|
19
51
|
return [];
|
|
20
52
|
}
|
|
21
53
|
}
|
|
22
|
-
/**
|
|
54
|
+
/** 保存历史记录到文件(JSON 数组格式) */
|
|
23
55
|
function saveHistory(history) {
|
|
24
56
|
try {
|
|
25
57
|
if (!fs.existsSync(HISTORY_DIR)) {
|
|
26
58
|
fs.mkdirSync(HISTORY_DIR, { recursive: true });
|
|
27
59
|
}
|
|
28
|
-
fs.writeFileSync(HISTORY_FILE, history.slice(-MAX_HISTORY)
|
|
60
|
+
fs.writeFileSync(HISTORY_FILE, JSON.stringify(history.slice(-MAX_HISTORY)), 'utf-8');
|
|
29
61
|
}
|
|
30
62
|
catch {
|
|
31
63
|
// 静默失败
|