@code4bug/jarvis-agent 1.2.1 → 1.3.2
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 +75 -343
- 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/core/WorkerBridge.d.ts +3 -0
- package/dist/core/WorkerBridge.js +75 -16
- package/dist/core/query.js +68 -10
- package/dist/hooks/useMultilineInputStream.d.ts +17 -0
- package/dist/hooks/useMultilineInputStream.js +141 -0
- package/dist/hooks/useTerminalCursorSync.d.ts +11 -0
- package/dist/hooks/useTerminalCursorSync.js +46 -0
- package/dist/hooks/useTerminalSize.d.ts +7 -0
- package/dist/hooks/useTerminalSize.js +21 -0
- package/dist/screens/repl.js +74 -33
- 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 +22 -0
- package/dist/tools/writeFile.js +63 -2
- package/package.json +1 -1
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,63 @@ 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 abortRequestedRef = useRef(false);
|
|
43
|
+
const lastAbortNoticeRef = useRef(0);
|
|
46
44
|
const sessionRef = useRef({
|
|
47
45
|
id: '', messages: [], createdAt: 0, updatedAt: 0, totalTokens: 0, totalCost: 0,
|
|
48
46
|
});
|
|
49
47
|
const engineRef = useRef(null);
|
|
48
|
+
const tokenCountRef = useRef(0);
|
|
50
49
|
// 节流 hooks
|
|
51
50
|
const { streamText, streamBufferRef, startStreamTimer, stopStreamTimer, appendStreamChunk, clearStream, handleThinkingUpdate, finishThinking, thinkingIdRef, stopAll, } = useStreamThrottle(setMessages);
|
|
52
|
-
const
|
|
51
|
+
const syncTokenDisplay = useCallback((count) => {
|
|
52
|
+
tokenCountRef.current = count;
|
|
53
|
+
}, []);
|
|
54
|
+
const resetTokens = useCallback(() => {
|
|
55
|
+
tokenCountRef.current = 0;
|
|
56
|
+
}, []);
|
|
57
|
+
const appendAbortNotice = useCallback(() => {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
if (now - lastAbortNoticeRef.current < 800)
|
|
60
|
+
return;
|
|
61
|
+
lastAbortNoticeRef.current = now;
|
|
62
|
+
setMessages((prev) => [...prev, {
|
|
63
|
+
id: `abort-notice-${now}`,
|
|
64
|
+
type: 'system',
|
|
65
|
+
status: 'aborted',
|
|
66
|
+
content: '已中断当前推理',
|
|
67
|
+
timestamp: now,
|
|
68
|
+
abortHint: '推理已中断(ESC)',
|
|
69
|
+
}]);
|
|
70
|
+
}, []);
|
|
71
|
+
const markPendingMessagesAborted = useCallback(() => {
|
|
72
|
+
setMessages((prev) => prev
|
|
73
|
+
.filter((msg) => !(msg.type === 'thinking' && msg.status === 'pending'))
|
|
74
|
+
.map((msg) => {
|
|
75
|
+
if (msg.status !== 'pending')
|
|
76
|
+
return msg;
|
|
77
|
+
if (msg.type === 'tool_exec') {
|
|
78
|
+
return {
|
|
79
|
+
...msg,
|
|
80
|
+
status: 'aborted',
|
|
81
|
+
content: `${msg.toolName || '工具'} 已中断`,
|
|
82
|
+
abortHint: msg.abortHint ?? '命令已中断(ESC)',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return msg;
|
|
86
|
+
}));
|
|
87
|
+
finishThinking();
|
|
88
|
+
clearStream();
|
|
89
|
+
}, [clearStream, finishThinking]);
|
|
90
|
+
const requestAbort = useCallback(() => {
|
|
91
|
+
if (!isProcessing || !engineRef.current || abortRequestedRef.current)
|
|
92
|
+
return;
|
|
93
|
+
abortRequestedRef.current = true;
|
|
94
|
+
logWarn('ui.abort_by_escape');
|
|
95
|
+
markPendingMessagesAborted();
|
|
96
|
+
appendAbortNotice();
|
|
97
|
+
engineRef.current.abort();
|
|
98
|
+
}, [appendAbortNotice, isProcessing, markPendingMessagesAborted]);
|
|
53
99
|
// ===== 新会话逻辑 =====
|
|
54
100
|
const handleNewSession = useCallback(() => {
|
|
55
101
|
logInfo('ui.new_session');
|
|
@@ -61,6 +107,7 @@ export default function REPL() {
|
|
|
61
107
|
clearStream();
|
|
62
108
|
setLoopState(null);
|
|
63
109
|
setIsProcessing(false);
|
|
110
|
+
abortRequestedRef.current = false;
|
|
64
111
|
setShowWelcome(true);
|
|
65
112
|
resetTokens();
|
|
66
113
|
generateAgentHint().then((hint) => setPlaceholder(hint)).catch(() => { });
|
|
@@ -125,17 +172,16 @@ export default function REPL() {
|
|
|
125
172
|
setLoopState(state);
|
|
126
173
|
setIsProcessing(state.isRunning);
|
|
127
174
|
if (state.isRunning) {
|
|
128
|
-
startTokenTimer();
|
|
129
175
|
startStreamTimer();
|
|
130
176
|
}
|
|
131
177
|
else {
|
|
132
|
-
|
|
178
|
+
abortRequestedRef.current = false;
|
|
133
179
|
stopAll();
|
|
134
180
|
}
|
|
135
181
|
},
|
|
136
182
|
onSessionUpdate: (s) => {
|
|
137
183
|
sessionRef.current = s;
|
|
138
|
-
|
|
184
|
+
tokenCountRef.current = s.totalTokens;
|
|
139
185
|
},
|
|
140
186
|
onConfirmDangerousCommand: (command, reason, ruleName) => {
|
|
141
187
|
return new Promise((resolve) => {
|
|
@@ -212,6 +258,7 @@ export default function REPL() {
|
|
|
212
258
|
setShowWelcome(false);
|
|
213
259
|
pushHistory(trimmed);
|
|
214
260
|
clearStream();
|
|
261
|
+
abortRequestedRef.current = false;
|
|
215
262
|
await engineRef.current.handleQuery(prompt, callbacks);
|
|
216
263
|
return;
|
|
217
264
|
}
|
|
@@ -270,6 +317,7 @@ export default function REPL() {
|
|
|
270
317
|
pushHistory(trimmed);
|
|
271
318
|
setInput('');
|
|
272
319
|
clearStream();
|
|
320
|
+
abortRequestedRef.current = false;
|
|
273
321
|
await engineRef.current.handleQuery(trimmed, callbacks);
|
|
274
322
|
}, [isProcessing, pushHistory, clearStream, slashMenu, handleNewSession]);
|
|
275
323
|
// ===== 输入处理 =====
|
|
@@ -321,19 +369,11 @@ export default function REPL() {
|
|
|
321
369
|
}, [placeholder]);
|
|
322
370
|
// ===== 隐藏系统默认终端光标,使用 ink 渲染的反色光标代替 =====
|
|
323
371
|
useEffect(() => {
|
|
324
|
-
|
|
372
|
+
hideTerminalCursor();
|
|
325
373
|
return () => {
|
|
326
|
-
|
|
374
|
+
showTerminalCursor();
|
|
327
375
|
};
|
|
328
376
|
}, []);
|
|
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
377
|
// ===== processing 期间隐藏终端光标,阻止 IME composing 显示 =====
|
|
338
378
|
const isProcessingRef = useRef(isProcessing);
|
|
339
379
|
isProcessingRef.current = isProcessing;
|
|
@@ -341,7 +381,7 @@ export default function REPL() {
|
|
|
341
381
|
if (!isProcessing)
|
|
342
382
|
return;
|
|
343
383
|
// 隐藏终端光标 — 终端 IME 的 inline composing 仅在光标可见时渲染
|
|
344
|
-
|
|
384
|
+
hideTerminalCursor();
|
|
345
385
|
// 高优先级 stdin 拦截:吞掉 processing 期间的所有输入(Ctrl+C / ESC 除外)
|
|
346
386
|
// 防止字符积累在缓冲区,processing 结束后污染输入框
|
|
347
387
|
const drain = (data) => {
|
|
@@ -373,8 +413,8 @@ export default function REPL() {
|
|
|
373
413
|
handleCtrlC();
|
|
374
414
|
return;
|
|
375
415
|
}
|
|
376
|
-
if (key.escape
|
|
377
|
-
|
|
416
|
+
if (key.escape) {
|
|
417
|
+
requestAbort();
|
|
378
418
|
return;
|
|
379
419
|
}
|
|
380
420
|
if (key.ctrl && ch === 'o') {
|
|
@@ -410,9 +450,8 @@ export default function REPL() {
|
|
|
410
450
|
return;
|
|
411
451
|
}
|
|
412
452
|
if (key.escape) {
|
|
413
|
-
if (isProcessing
|
|
414
|
-
|
|
415
|
-
engineRef.current.abort();
|
|
453
|
+
if (isProcessing) {
|
|
454
|
+
requestAbort();
|
|
416
455
|
}
|
|
417
456
|
else if (input.length > 0) {
|
|
418
457
|
const now = Date.now();
|
|
@@ -428,8 +467,10 @@ export default function REPL() {
|
|
|
428
467
|
}
|
|
429
468
|
});
|
|
430
469
|
// ===== 渲染 =====
|
|
431
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, children: [showWelcome && _jsx(WelcomeHeader, { width: width }),
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
470
|
+
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) => {
|
|
471
|
+
if (!dangerConfirm)
|
|
472
|
+
return;
|
|
473
|
+
dangerConfirm.resolve(choice);
|
|
474
|
+
setDangerConfirm(null);
|
|
475
|
+
} }) }), _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
476
|
}
|
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, extraRowsUp?: number): void;
|
|
@@ -0,0 +1,22 @@
|
|
|
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, extraRowsUp = 0) {
|
|
18
|
+
const totalRowsUp = Math.max(rowsBelow + extraRowsUp, 0);
|
|
19
|
+
const up = totalRowsUp > 0 ? `${ESC}${totalRowsUp}A` : '';
|
|
20
|
+
const down = totalRowsUp > 0 ? `${ESC}${totalRowsUp}B` : '';
|
|
21
|
+
process.stdout.write(`${up}${ESC}${column}G${down}`);
|
|
22
|
+
}
|
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) {
|