@code4bug/jarvis-agent 1.2.1 → 1.3.1
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/README.md +1 -1
- package/dist/agents/jarvis.md +1 -1
- package/dist/components/AnimatedStatusText.d.ts +10 -0
- package/dist/components/AnimatedStatusText.js +17 -0
- package/dist/components/ComposerPane.d.ts +25 -0
- package/dist/components/ComposerPane.js +10 -0
- package/dist/components/FooterPane.d.ts +9 -0
- package/dist/components/FooterPane.js +22 -0
- package/dist/components/InputTextView.d.ts +11 -0
- package/dist/components/InputTextView.js +44 -0
- package/dist/components/MessageList.d.ts +9 -0
- package/dist/components/MessageList.js +8 -0
- package/dist/components/MessageViewport.d.ts +21 -0
- package/dist/components/MessageViewport.js +11 -0
- package/dist/components/MultilineInput.js +62 -344
- package/dist/components/StreamingDraft.d.ts +11 -0
- package/dist/components/StreamingDraft.js +14 -0
- package/dist/components/inputEditing.d.ts +20 -0
- package/dist/components/inputEditing.js +48 -0
- package/dist/hooks/useMultilineInputStream.d.ts +17 -0
- package/dist/hooks/useMultilineInputStream.js +141 -0
- package/dist/hooks/useTerminalCursorSync.d.ts +8 -0
- package/dist/hooks/useTerminalCursorSync.js +44 -0
- package/dist/hooks/useTerminalSize.d.ts +7 -0
- package/dist/hooks/useTerminalSize.js +21 -0
- package/dist/screens/repl.js +39 -28
- package/dist/services/api/llm.js +3 -1
- package/dist/skills/index.js +10 -3
- package/dist/terminal/cursor.d.ts +6 -0
- package/dist/terminal/cursor.js +21 -0
- package/dist/tools/writeFile.js +63 -2
- package/package.json +1 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
export function useMultilineInputStream({ stdin, isActive, pasteDetectWindowMs, imeComposeWindowMs, isAsciiLowerAlpha, hasNonAscii, insertPaste, handleNormalInput, commitImeBuffer, flushImeBuffer, appendImeBuffer, getImeBuffer, clearImeBufferWithInsertedLenRollback, }) {
|
|
3
|
+
const pasteBufferRef = useRef(null);
|
|
4
|
+
const batchBufferRef = useRef('');
|
|
5
|
+
const batchTimerRef = useRef(null);
|
|
6
|
+
const imeTimerRef = useRef(null);
|
|
7
|
+
const escTimerRef = useRef(null);
|
|
8
|
+
const ESC_WAIT_MS = 50;
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!stdin || !isActive)
|
|
11
|
+
return;
|
|
12
|
+
const flushBatchBuffer = () => {
|
|
13
|
+
const buf = batchBufferRef.current;
|
|
14
|
+
batchBufferRef.current = '';
|
|
15
|
+
batchTimerRef.current = null;
|
|
16
|
+
if (!buf)
|
|
17
|
+
return;
|
|
18
|
+
const cleaned = buf.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
19
|
+
if (cleaned.includes('\n') && cleaned.length > 1) {
|
|
20
|
+
insertPaste(cleaned);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (isAsciiLowerAlpha(buf)) {
|
|
24
|
+
appendImeBuffer(buf);
|
|
25
|
+
if (imeTimerRef.current !== null) {
|
|
26
|
+
clearTimeout(imeTimerRef.current);
|
|
27
|
+
}
|
|
28
|
+
imeTimerRef.current = setTimeout(flushImeBuffer, imeComposeWindowMs);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (hasNonAscii(buf) && getImeBuffer().length > 0) {
|
|
32
|
+
if (imeTimerRef.current !== null) {
|
|
33
|
+
clearTimeout(imeTimerRef.current);
|
|
34
|
+
imeTimerRef.current = null;
|
|
35
|
+
}
|
|
36
|
+
clearImeBufferWithInsertedLenRollback();
|
|
37
|
+
handleNormalInput(buf);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (getImeBuffer().length > 0) {
|
|
41
|
+
commitImeBuffer();
|
|
42
|
+
}
|
|
43
|
+
handleNormalInput(buf);
|
|
44
|
+
};
|
|
45
|
+
const onData = (data) => {
|
|
46
|
+
const raw = data.toString('utf-8');
|
|
47
|
+
if (raw.includes('\x1B[200~')) {
|
|
48
|
+
const startIdx = raw.indexOf('\x1B[200~') + 6;
|
|
49
|
+
const endIdx = raw.indexOf('\x1B[201~');
|
|
50
|
+
if (endIdx !== -1) {
|
|
51
|
+
insertPaste(raw.slice(startIdx, endIdx));
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
pasteBufferRef.current = raw.slice(startIdx);
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (pasteBufferRef.current !== null) {
|
|
59
|
+
const endIdx = raw.indexOf('\x1B[201~');
|
|
60
|
+
if (endIdx !== -1) {
|
|
61
|
+
pasteBufferRef.current += raw.slice(0, endIdx);
|
|
62
|
+
insertPaste(pasteBufferRef.current);
|
|
63
|
+
pasteBufferRef.current = null;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
pasteBufferRef.current += raw;
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (escTimerRef.current !== null) {
|
|
71
|
+
clearTimeout(escTimerRef.current);
|
|
72
|
+
escTimerRef.current = null;
|
|
73
|
+
if (raw === '\r' || raw === '\n') {
|
|
74
|
+
handleNormalInput('\x1B\r');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
handleNormalInput('\x1B');
|
|
78
|
+
}
|
|
79
|
+
const isSingleControl = raw === '\r' || raw === '\n' ||
|
|
80
|
+
raw === '\x7F' || raw === '\x08' ||
|
|
81
|
+
raw === '\t' || raw === '\x1B' ||
|
|
82
|
+
raw === '\x1B\r' || raw === '\x1B\n' ||
|
|
83
|
+
raw.startsWith('\x1B[') || raw.startsWith('\x1B]') || raw.startsWith('\x1BO');
|
|
84
|
+
if (raw === '\x03' || raw === '\x0F' || raw === '\x0C')
|
|
85
|
+
return;
|
|
86
|
+
if (raw === '\x1B') {
|
|
87
|
+
escTimerRef.current = setTimeout(() => {
|
|
88
|
+
escTimerRef.current = null;
|
|
89
|
+
handleNormalInput('\x1B');
|
|
90
|
+
}, ESC_WAIT_MS);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (isSingleControl && batchBufferRef.current === '') {
|
|
94
|
+
if (getImeBuffer().length > 0) {
|
|
95
|
+
commitImeBuffer();
|
|
96
|
+
}
|
|
97
|
+
handleNormalInput(raw);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (isSingleControl && batchBufferRef.current !== '') {
|
|
101
|
+
batchBufferRef.current += raw;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
batchBufferRef.current += raw;
|
|
105
|
+
if (batchTimerRef.current !== null) {
|
|
106
|
+
clearTimeout(batchTimerRef.current);
|
|
107
|
+
}
|
|
108
|
+
batchTimerRef.current = setTimeout(flushBatchBuffer, pasteDetectWindowMs);
|
|
109
|
+
};
|
|
110
|
+
stdin.prependListener('data', onData);
|
|
111
|
+
return () => {
|
|
112
|
+
stdin.off('data', onData);
|
|
113
|
+
if (batchTimerRef.current !== null) {
|
|
114
|
+
clearTimeout(batchTimerRef.current);
|
|
115
|
+
batchTimerRef.current = null;
|
|
116
|
+
}
|
|
117
|
+
if (escTimerRef.current !== null) {
|
|
118
|
+
clearTimeout(escTimerRef.current);
|
|
119
|
+
escTimerRef.current = null;
|
|
120
|
+
}
|
|
121
|
+
if (imeTimerRef.current !== null) {
|
|
122
|
+
clearTimeout(imeTimerRef.current);
|
|
123
|
+
imeTimerRef.current = null;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}, [
|
|
127
|
+
stdin,
|
|
128
|
+
isActive,
|
|
129
|
+
pasteDetectWindowMs,
|
|
130
|
+
imeComposeWindowMs,
|
|
131
|
+
isAsciiLowerAlpha,
|
|
132
|
+
hasNonAscii,
|
|
133
|
+
insertPaste,
|
|
134
|
+
handleNormalInput,
|
|
135
|
+
commitImeBuffer,
|
|
136
|
+
flushImeBuffer,
|
|
137
|
+
appendImeBuffer,
|
|
138
|
+
getImeBuffer,
|
|
139
|
+
clearImeBufferWithInsertedLenRollback,
|
|
140
|
+
]);
|
|
141
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
interface UseTerminalCursorSyncOptions {
|
|
2
|
+
showCursor: boolean;
|
|
3
|
+
isActive: boolean;
|
|
4
|
+
rowsBelow: number;
|
|
5
|
+
delayMs?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function useTerminalCursorSync({ showCursor, isActive, rowsBelow, delayMs, }: UseTerminalCursorSyncOptions): void;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { moveCursorToColumn, relocateCursorToInputLine } from '../terminal/cursor.js';
|
|
3
|
+
export function useTerminalCursorSync({ showCursor, isActive, rowsBelow, delayMs = 80, }) {
|
|
4
|
+
const cursorRelocTimerRef = useRef(null);
|
|
5
|
+
const lastCursorCommandRef = useRef('');
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
return () => {
|
|
8
|
+
if (cursorRelocTimerRef.current !== null) {
|
|
9
|
+
clearTimeout(cursorRelocTimerRef.current);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
}, []);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (cursorRelocTimerRef.current !== null) {
|
|
15
|
+
clearTimeout(cursorRelocTimerRef.current);
|
|
16
|
+
}
|
|
17
|
+
if (!showCursor || !isActive) {
|
|
18
|
+
cursorRelocTimerRef.current = setTimeout(() => {
|
|
19
|
+
cursorRelocTimerRef.current = null;
|
|
20
|
+
const commandKey = 'col:1';
|
|
21
|
+
if (lastCursorCommandRef.current === commandKey)
|
|
22
|
+
return;
|
|
23
|
+
lastCursorCommandRef.current = commandKey;
|
|
24
|
+
moveCursorToColumn(1);
|
|
25
|
+
}, delayMs);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
cursorRelocTimerRef.current = setTimeout(() => {
|
|
29
|
+
cursorRelocTimerRef.current = null;
|
|
30
|
+
const commandKey = `input:${rowsBelow}:3`;
|
|
31
|
+
if (lastCursorCommandRef.current === commandKey)
|
|
32
|
+
return;
|
|
33
|
+
lastCursorCommandRef.current = commandKey;
|
|
34
|
+
relocateCursorToInputLine(rowsBelow, 3);
|
|
35
|
+
}, delayMs);
|
|
36
|
+
}
|
|
37
|
+
return () => {
|
|
38
|
+
if (cursorRelocTimerRef.current !== null) {
|
|
39
|
+
clearTimeout(cursorRelocTimerRef.current);
|
|
40
|
+
cursorRelocTimerRef.current = null;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}, [showCursor, isActive, rowsBelow, delayMs]);
|
|
44
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
/** 响应式终端尺寸 */
|
|
3
|
+
export function useTerminalSize() {
|
|
4
|
+
const [size, setSize] = useState(() => ({
|
|
5
|
+
width: process.stdout.columns || 80,
|
|
6
|
+
height: process.stdout.rows || 24,
|
|
7
|
+
}));
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const onResize = () => {
|
|
10
|
+
setSize({
|
|
11
|
+
width: process.stdout.columns || 80,
|
|
12
|
+
height: process.stdout.rows || 24,
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
process.stdout.on('resize', onResize);
|
|
16
|
+
return () => {
|
|
17
|
+
process.stdout.off('resize', onResize);
|
|
18
|
+
};
|
|
19
|
+
}, []);
|
|
20
|
+
return size;
|
|
21
|
+
}
|
package/dist/screens/repl.js
CHANGED
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
3
|
-
import { Box,
|
|
4
|
-
import Spinner from 'ink-spinner';
|
|
5
|
-
import MultilineInput from '../components/MultilineInput.js';
|
|
3
|
+
import { Box, useInput, useApp } from 'ink';
|
|
6
4
|
import WelcomeHeader from '../components/WelcomeHeader.js';
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import SlashCommandMenu from '../components/SlashCommandMenu.js';
|
|
11
|
-
import DangerConfirm from '../components/DangerConfirm.js';
|
|
5
|
+
import MessageViewport from '../components/MessageViewport.js';
|
|
6
|
+
import ComposerPane from '../components/ComposerPane.js';
|
|
7
|
+
import FooterPane from '../components/FooterPane.js';
|
|
12
8
|
import { useWindowFocus } from '../hooks/useFocus.js';
|
|
13
9
|
import { useInputHistory } from '../hooks/useInputHistory.js';
|
|
14
10
|
import { useDoubleCtrlCExit } from '../hooks/useDoubleCtrlCExit.js';
|
|
15
11
|
import { useTerminalWidth } from '../hooks/useTerminalWidth.js';
|
|
16
12
|
import { useStreamThrottle } from '../hooks/useStreamThrottle.js';
|
|
17
|
-
import { useTokenDisplay } from '../hooks/useTokenDisplay.js';
|
|
18
13
|
import { useSlashMenu } from '../hooks/useSlashMenu.js';
|
|
19
14
|
import { executeSlashCommand } from './slashCommands.js';
|
|
20
15
|
import { QueryEngine } from '../core/QueryEngine.js';
|
|
@@ -24,6 +19,7 @@ import { subscribeAgentCount, getActiveAgentCount } from '../core/spawnRegistry.
|
|
|
24
19
|
import { logError, logInfo, logWarn } from '../core/logger.js';
|
|
25
20
|
import { getAgentSubCommands } from '../commands/index.js';
|
|
26
21
|
import { setActiveAgent } from '../config/agentState.js';
|
|
22
|
+
import { hideTerminalCursor, showTerminalCursor } from '../terminal/cursor.js';
|
|
27
23
|
export default function REPL() {
|
|
28
24
|
const { exit } = useApp();
|
|
29
25
|
const width = useTerminalWidth();
|
|
@@ -43,13 +39,34 @@ export default function REPL() {
|
|
|
43
39
|
const [placeholder, setPlaceholder] = useState('');
|
|
44
40
|
const [activeAgents, setActiveAgents] = useState(getActiveAgentCount());
|
|
45
41
|
const lastEscRef = useRef(0);
|
|
42
|
+
const lastAbortNoticeRef = useRef(0);
|
|
46
43
|
const sessionRef = useRef({
|
|
47
44
|
id: '', messages: [], createdAt: 0, updatedAt: 0, totalTokens: 0, totalCost: 0,
|
|
48
45
|
});
|
|
49
46
|
const engineRef = useRef(null);
|
|
47
|
+
const tokenCountRef = useRef(0);
|
|
50
48
|
// 节流 hooks
|
|
51
49
|
const { streamText, streamBufferRef, startStreamTimer, stopStreamTimer, appendStreamChunk, clearStream, handleThinkingUpdate, finishThinking, thinkingIdRef, stopAll, } = useStreamThrottle(setMessages);
|
|
52
|
-
const
|
|
50
|
+
const syncTokenDisplay = useCallback((count) => {
|
|
51
|
+
tokenCountRef.current = count;
|
|
52
|
+
}, []);
|
|
53
|
+
const resetTokens = useCallback(() => {
|
|
54
|
+
tokenCountRef.current = 0;
|
|
55
|
+
}, []);
|
|
56
|
+
const appendAbortNotice = useCallback(() => {
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
if (now - lastAbortNoticeRef.current < 800)
|
|
59
|
+
return;
|
|
60
|
+
lastAbortNoticeRef.current = now;
|
|
61
|
+
setMessages((prev) => [...prev, {
|
|
62
|
+
id: `abort-notice-${now}`,
|
|
63
|
+
type: 'system',
|
|
64
|
+
status: 'aborted',
|
|
65
|
+
content: '已中断当前推理',
|
|
66
|
+
timestamp: now,
|
|
67
|
+
abortHint: '推理已中断(ESC)',
|
|
68
|
+
}]);
|
|
69
|
+
}, []);
|
|
53
70
|
// ===== 新会话逻辑 =====
|
|
54
71
|
const handleNewSession = useCallback(() => {
|
|
55
72
|
logInfo('ui.new_session');
|
|
@@ -125,17 +142,15 @@ export default function REPL() {
|
|
|
125
142
|
setLoopState(state);
|
|
126
143
|
setIsProcessing(state.isRunning);
|
|
127
144
|
if (state.isRunning) {
|
|
128
|
-
startTokenTimer();
|
|
129
145
|
startStreamTimer();
|
|
130
146
|
}
|
|
131
147
|
else {
|
|
132
|
-
stopTokenTimer();
|
|
133
148
|
stopAll();
|
|
134
149
|
}
|
|
135
150
|
},
|
|
136
151
|
onSessionUpdate: (s) => {
|
|
137
152
|
sessionRef.current = s;
|
|
138
|
-
|
|
153
|
+
tokenCountRef.current = s.totalTokens;
|
|
139
154
|
},
|
|
140
155
|
onConfirmDangerousCommand: (command, reason, ruleName) => {
|
|
141
156
|
return new Promise((resolve) => {
|
|
@@ -321,19 +336,11 @@ export default function REPL() {
|
|
|
321
336
|
}, [placeholder]);
|
|
322
337
|
// ===== 隐藏系统默认终端光标,使用 ink 渲染的反色光标代替 =====
|
|
323
338
|
useEffect(() => {
|
|
324
|
-
|
|
339
|
+
hideTerminalCursor();
|
|
325
340
|
return () => {
|
|
326
|
-
|
|
341
|
+
showTerminalCursor();
|
|
327
342
|
};
|
|
328
343
|
}, []);
|
|
329
|
-
// ===== processing/streaming 期间,每次 re-render 后将物理光标移到行首 =====
|
|
330
|
-
// ink 每次渲染后光标停在 StatusBar 行末,macOS IME 会在该位置显示 composing 候选框,
|
|
331
|
-
// 导致候选框与进度条重叠。将光标移到列 1(行首)让候选框出现在屏幕左侧空白区域。
|
|
332
|
-
useEffect(() => {
|
|
333
|
-
if (!isProcessing)
|
|
334
|
-
return;
|
|
335
|
-
process.stdout.write('\x1B[1G');
|
|
336
|
-
});
|
|
337
344
|
// ===== processing 期间隐藏终端光标,阻止 IME composing 显示 =====
|
|
338
345
|
const isProcessingRef = useRef(isProcessing);
|
|
339
346
|
isProcessingRef.current = isProcessing;
|
|
@@ -341,7 +348,7 @@ export default function REPL() {
|
|
|
341
348
|
if (!isProcessing)
|
|
342
349
|
return;
|
|
343
350
|
// 隐藏终端光标 — 终端 IME 的 inline composing 仅在光标可见时渲染
|
|
344
|
-
|
|
351
|
+
hideTerminalCursor();
|
|
345
352
|
// 高优先级 stdin 拦截:吞掉 processing 期间的所有输入(Ctrl+C / ESC 除外)
|
|
346
353
|
// 防止字符积累在缓冲区,processing 结束后污染输入框
|
|
347
354
|
const drain = (data) => {
|
|
@@ -374,6 +381,7 @@ export default function REPL() {
|
|
|
374
381
|
return;
|
|
375
382
|
}
|
|
376
383
|
if (key.escape && engineRef.current) {
|
|
384
|
+
appendAbortNotice();
|
|
377
385
|
engineRef.current.abort();
|
|
378
386
|
return;
|
|
379
387
|
}
|
|
@@ -412,6 +420,7 @@ export default function REPL() {
|
|
|
412
420
|
if (key.escape) {
|
|
413
421
|
if (isProcessing && engineRef.current) {
|
|
414
422
|
logWarn('ui.abort_by_escape');
|
|
423
|
+
appendAbortNotice();
|
|
415
424
|
engineRef.current.abort();
|
|
416
425
|
}
|
|
417
426
|
else if (input.length > 0) {
|
|
@@ -428,8 +437,10 @@ export default function REPL() {
|
|
|
428
437
|
}
|
|
429
438
|
});
|
|
430
439
|
// ===== 渲染 =====
|
|
431
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, children: [showWelcome && _jsx(WelcomeHeader, { width: width }),
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
440
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, children: [showWelcome && _jsx(WelcomeHeader, { width: width }), _jsx(Box, { marginTop: showWelcome ? 0 : 1, children: _jsx(MessageViewport, { messages: messages, streamText: streamText, showDetails: showDetails, dangerConfirm: dangerConfirm, loopState: loopState, onResolveDangerConfirm: (choice) => {
|
|
441
|
+
if (!dangerConfirm)
|
|
442
|
+
return;
|
|
443
|
+
dangerConfirm.resolve(choice);
|
|
444
|
+
setDangerConfirm(null);
|
|
445
|
+
} }) }), _jsx(ComposerPane, { width: width, countdown: countdown, isProcessing: isProcessing, input: input, placeholder: placeholder, windowFocused: windowFocused, slashMenuVisible: slashMenu.slashMenuVisible, slashMenuItems: slashMenu.slashMenuItems, slashMenuIndex: slashMenu.slashMenuIndex, onInputChange: handleInputChange, onSubmit: handleEditorSubmit, onUpArrow: handleUpArrow, onDownArrow: handleDownArrow, onSlashMenuUp: slashMenu.handleSlashMenuUp, onSlashMenuDown: slashMenu.handleSlashMenuDown, onSlashMenuSelect: handleSlashMenuAutocomplete, onSlashMenuClose: slashMenu.handleSlashMenuClose, onTabFillPlaceholder: handleTabFillPlaceholder }), _jsx(FooterPane, { width: width, tokenCountRef: tokenCountRef, activeAgents: activeAgents })] }));
|
|
435
446
|
}
|
package/dist/services/api/llm.js
CHANGED
|
@@ -185,7 +185,9 @@ export class LLMServiceImpl {
|
|
|
185
185
|
'不要为了讨好用户而突破角色边界。' +
|
|
186
186
|
'\n\n[安全围栏] 系统已内置安全围栏(Safeguard),会自动拦截危险命令并弹出交互式确认菜单。' +
|
|
187
187
|
'当你需要执行任何命令时,直接调用 Bash 工具即可,不要自行判断命令是否危险,不要用文字询问用户"是否确认执行"。' +
|
|
188
|
-
'安全围栏会自动处理拦截和用户确认流程。'
|
|
188
|
+
'安全围栏会自动处理拦截和用户确认流程。' +
|
|
189
|
+
'\n\n[文件修改约束] 修改已有文件时,先读取目标文件,再优先使用局部修改方式。' +
|
|
190
|
+
'如果只改少量内容,优先使用 write_file 的 replace 模式;需要结构化补丁时使用 diff 模式;只有新建文件或确实需要大范围重写时才使用 overwrite。';
|
|
189
191
|
// 追加系统环境信息,帮助 LLM 感知用户运行环境
|
|
190
192
|
const systemInfo = getSystemInfoPrompt();
|
|
191
193
|
// 追加当前激活模型信息
|
package/dist/skills/index.js
CHANGED
|
@@ -85,7 +85,6 @@ function buildParamsFromScript(scriptPath, skill) {
|
|
|
85
85
|
* 通过写入临时 .py 文件再执行,避免 shell 转义问题。
|
|
86
86
|
*/
|
|
87
87
|
async function executeSkillScript(scriptPath, skill, args) {
|
|
88
|
-
const funcName = skill.meta.name.replace(/-/g, '_');
|
|
89
88
|
const kwargs = [];
|
|
90
89
|
for (const [key, value] of Object.entries(args)) {
|
|
91
90
|
if (key === 'arguments') {
|
|
@@ -112,8 +111,16 @@ async function executeSkillScript(scriptPath, skill, args) {
|
|
|
112
111
|
const pyCode = [
|
|
113
112
|
'import sys,os,json',
|
|
114
113
|
`sys.path.insert(0, ${JSON.stringify(path.dirname(scriptPath))})`,
|
|
115
|
-
|
|
116
|
-
`
|
|
114
|
+
'import skill as skill_module',
|
|
115
|
+
`tool_name = getattr(skill_module, "TOOL_METADATA", {}).get("name") or ${JSON.stringify(skill.meta.name.replace(/-/g, '_'))}`,
|
|
116
|
+
'func = getattr(skill_module, tool_name, None)',
|
|
117
|
+
'if func is None:',
|
|
118
|
+
' candidates = [name for name, value in vars(skill_module).items() if callable(value) and not name.startswith("_")]',
|
|
119
|
+
' if len(candidates) == 1:',
|
|
120
|
+
' func = getattr(skill_module, candidates[0])',
|
|
121
|
+
' else:',
|
|
122
|
+
' raise ImportError(f"未找到可调用函数: {tool_name},候选: {candidates}")',
|
|
123
|
+
`result = func(${kwargs.join(', ')})`,
|
|
117
124
|
'print(json.dumps(result, ensure_ascii=False, indent=2))',
|
|
118
125
|
].join('\n');
|
|
119
126
|
try {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function hideTerminalCursor(): void;
|
|
2
|
+
export declare function showTerminalCursor(): void;
|
|
3
|
+
export declare function enableBracketedPaste(): void;
|
|
4
|
+
export declare function disableBracketedPaste(): void;
|
|
5
|
+
export declare function moveCursorToColumn(column: number): void;
|
|
6
|
+
export declare function relocateCursorToInputLine(rowsBelow: number, column: number): void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const ESC = '\x1B[';
|
|
2
|
+
export function hideTerminalCursor() {
|
|
3
|
+
process.stdout.write(`${ESC}?25l`);
|
|
4
|
+
}
|
|
5
|
+
export function showTerminalCursor() {
|
|
6
|
+
process.stdout.write(`${ESC}?25h`);
|
|
7
|
+
}
|
|
8
|
+
export function enableBracketedPaste() {
|
|
9
|
+
process.stdout.write(`${ESC}?2004h`);
|
|
10
|
+
}
|
|
11
|
+
export function disableBracketedPaste() {
|
|
12
|
+
process.stdout.write(`${ESC}?2004l`);
|
|
13
|
+
}
|
|
14
|
+
export function moveCursorToColumn(column) {
|
|
15
|
+
process.stdout.write(`${ESC}${column}G`);
|
|
16
|
+
}
|
|
17
|
+
export function relocateCursorToInputLine(rowsBelow, column) {
|
|
18
|
+
const up = rowsBelow > 0 ? `${ESC}${rowsBelow}A` : '';
|
|
19
|
+
const down = rowsBelow > 0 ? `${ESC}${rowsBelow}B` : '';
|
|
20
|
+
process.stdout.write(`${up}${ESC}${column}G${down}`);
|
|
21
|
+
}
|
package/dist/tools/writeFile.js
CHANGED
|
@@ -73,6 +73,35 @@ function applyUnifiedDiff(original, diff) {
|
|
|
73
73
|
}
|
|
74
74
|
return result.join('\n');
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* 基于文本片段做精确替换,适合只改少量代码的场景。
|
|
78
|
+
*/
|
|
79
|
+
function applyStringReplace(original, oldString, newString, replaceAll, expectedReplacements) {
|
|
80
|
+
if (!oldString) {
|
|
81
|
+
throw new Error('replace 模式下 old_string 不能为空');
|
|
82
|
+
}
|
|
83
|
+
let matchCount = 0;
|
|
84
|
+
let searchStart = 0;
|
|
85
|
+
while (true) {
|
|
86
|
+
const idx = original.indexOf(oldString, searchStart);
|
|
87
|
+
if (idx === -1)
|
|
88
|
+
break;
|
|
89
|
+
matchCount++;
|
|
90
|
+
searchStart = idx + oldString.length;
|
|
91
|
+
}
|
|
92
|
+
if (matchCount === 0) {
|
|
93
|
+
throw new Error('replace 模式未找到要替换的内容');
|
|
94
|
+
}
|
|
95
|
+
if (expectedReplacements !== undefined && matchCount !== expectedReplacements) {
|
|
96
|
+
throw new Error(`replace 模式匹配数量不符合预期,期望 ${expectedReplacements},实际 ${matchCount}`);
|
|
97
|
+
}
|
|
98
|
+
if (!replaceAll && matchCount !== 1) {
|
|
99
|
+
throw new Error(`replace 模式要求目标内容唯一,当前匹配到 ${matchCount} 处。请提供更精确的 old_string,或开启 replace_all`);
|
|
100
|
+
}
|
|
101
|
+
return replaceAll
|
|
102
|
+
? original.split(oldString).join(newString)
|
|
103
|
+
: original.replace(oldString, newString);
|
|
104
|
+
}
|
|
76
105
|
/**
|
|
77
106
|
* 写入文件内容(支持安全检测)。
|
|
78
107
|
* 返回写入结果消息。
|
|
@@ -95,13 +124,33 @@ function doWrite(filePath, content) {
|
|
|
95
124
|
}
|
|
96
125
|
export const writeFile = {
|
|
97
126
|
name: 'write_file',
|
|
98
|
-
description: '
|
|
127
|
+
description: '写入内容到指定文件。修改已有文件时优先使用 replace 或 diff 做局部更新,只有新建文件或大范围重写时才使用 overwrite。支持三种模式:overwrite(完整替换)、replace(按文本片段精确替换)、diff(基于 unified diff 增量更新)。',
|
|
99
128
|
parameters: {
|
|
100
129
|
path: { type: 'string', description: '文件路径', required: true },
|
|
101
130
|
content: { type: 'string', description: '文件内容(overwrite 模式必填)', required: false },
|
|
102
131
|
mode: {
|
|
103
132
|
type: 'string',
|
|
104
|
-
description: '写入模式:overwrite(完整替换,默认)| diff(增量更新)',
|
|
133
|
+
description: '写入模式:overwrite(完整替换,默认)| replace(局部替换)| diff(增量更新)',
|
|
134
|
+
required: false,
|
|
135
|
+
},
|
|
136
|
+
old_string: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
description: 'replace 模式必填:要查找并替换的原始文本片段',
|
|
139
|
+
required: false,
|
|
140
|
+
},
|
|
141
|
+
new_string: {
|
|
142
|
+
type: 'string',
|
|
143
|
+
description: 'replace 模式必填:替换后的文本片段',
|
|
144
|
+
required: false,
|
|
145
|
+
},
|
|
146
|
+
replace_all: {
|
|
147
|
+
type: 'boolean',
|
|
148
|
+
description: 'replace 模式可选:是否替换所有匹配项,默认 false',
|
|
149
|
+
required: false,
|
|
150
|
+
},
|
|
151
|
+
expected_replacements: {
|
|
152
|
+
type: 'number',
|
|
153
|
+
description: 'replace 模式可选:期望匹配数量,不符合时直接报错,防止误替换',
|
|
105
154
|
required: false,
|
|
106
155
|
},
|
|
107
156
|
diff: {
|
|
@@ -113,6 +162,18 @@ export const writeFile = {
|
|
|
113
162
|
execute: async (args) => {
|
|
114
163
|
const filePath = args.path;
|
|
115
164
|
const mode = args.mode || 'overwrite';
|
|
165
|
+
if (mode === 'replace') {
|
|
166
|
+
const oldString = args.old_string;
|
|
167
|
+
const newString = args.new_string ?? '';
|
|
168
|
+
const replaceAll = Boolean(args.replace_all);
|
|
169
|
+
const expectedReplacements = args.expected_replacements === undefined ? undefined : Number(args.expected_replacements);
|
|
170
|
+
if (!fs.existsSync(filePath)) {
|
|
171
|
+
throw new Error(`replace 模式要求目标文件已存在: ${filePath}`);
|
|
172
|
+
}
|
|
173
|
+
const original = fs.readFileSync(filePath, 'utf-8');
|
|
174
|
+
const replaced = applyStringReplace(original, oldString, newString, replaceAll, expectedReplacements);
|
|
175
|
+
return doWrite(filePath, replaced);
|
|
176
|
+
}
|
|
116
177
|
if (mode === 'diff') {
|
|
117
178
|
const diffContent = args.diff;
|
|
118
179
|
if (!diffContent) {
|