deeper-cli 1.0.6 → 1.2.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/LICENSE +21 -0
- package/dist/cli/index.js +581 -156
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.js +21 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/chat-repl.ts +1251 -961
- package/src/model/DeepSeekClient.ts +35 -37
- package/src/model/RetryManager.ts +54 -7
- package/src/model/StreamHandler.ts +111 -17
- package/src/model/types.ts +25 -6
package/src/cli/chat-repl.ts
CHANGED
|
@@ -1,695 +1,955 @@
|
|
|
1
|
-
import readline from 'node:readline';
|
|
2
|
-
import process from 'node:process';
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync,
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync, copyFileSync, unlinkSync } from 'node:fs';
|
|
4
|
+
import { join, relative } from 'node:path';
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { DEEPER_HOME } from '../core/constants.js';
|
|
7
|
+
import { TOOL_SAFETY_MAP } from '../tools/tool-types.js';
|
|
8
|
+
import type { Tool } from '../tools/tool-types.js';
|
|
9
|
+
import { ToolValidator } from '../tools/ToolValidator.js';
|
|
10
|
+
import { xmemory, setSessionId } from '../memory/xmemory.js';
|
|
11
|
+
import { MarkdownStreamRenderer } from '../ui/markdown.js';
|
|
12
|
+
import { getTodos, todoSummary } from '../tools/builtin/ai/todo_manager.js';
|
|
13
|
+
import { estimateTokens } from '../tools/builtin/ai/token_count.js';
|
|
14
|
+
import { SkillEngine } from '../skills/SkillEngine.js';
|
|
15
|
+
import { MCPClient } from '../mcp/MCPClient.js';
|
|
16
|
+
import { DeepSeekClient } from '../model/DeepSeekClient.js';
|
|
17
|
+
import type { ChatMessage, StreamChunk } from '../model/types.js';
|
|
18
|
+
import { O, Oflush, A, d, b, c, g, y, r, B, G, resetTimer, thinkingAnim, thinkingAnimAt } from '../ui/ansi.js';
|
|
19
|
+
|
|
20
|
+
export interface ReplOptions {
|
|
21
|
+
apiKey: string; model: string; baseUrl: string;
|
|
22
|
+
maxTokens: number; temperature: number;
|
|
23
|
+
thinkEnabled: boolean; thinkBudget: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type Role = 'system' | 'user' | 'assistant' | 'tool';
|
|
27
|
+
|
|
28
|
+
interface Message {
|
|
29
|
+
role: Role; content: string | null;
|
|
30
|
+
tool_calls?: Array<{ id: string; name: string; arguments: Record<string, unknown> }>;
|
|
31
|
+
tool_call_id?: string; name?: string; reasoning_content?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ToolDef {
|
|
35
|
+
type: 'function';
|
|
36
|
+
function: { name: string; description: string; parameters: Record<string, unknown> };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const MAX_HISTORY = 60;
|
|
40
|
+
const CONTEXT_LIMIT = 1_048_576;
|
|
41
|
+
const CTX_WARN = 786_432;
|
|
42
|
+
const TOOL_RESULT_MAX = 4000;
|
|
43
|
+
const SESSION_DIR = join(DEEPER_HOME, 'sessions');
|
|
44
|
+
const AUTOSAVE_FILE = join(SESSION_DIR, '_autosave.json');
|
|
45
|
+
|
|
46
|
+
const TOOL_TIMEOUT_MAP: Record<string, number> = {
|
|
47
|
+
run_command: 120_000, run_async: 120_000, pipe_commands: 120_000,
|
|
48
|
+
shell_script: 120_000, build_project: 180_000, run_test: 180_000,
|
|
49
|
+
npm_manage: 120_000, docker_manage: 120_000, project_init: 120_000,
|
|
50
|
+
web_fetch: 60_000, web_search: 60_000, http_request: 60_000,
|
|
51
|
+
browser_action: 60_000, screenshot_page: 60_000,
|
|
52
|
+
sql_query: 60_000, sql_migrate: 120_000, db_backup: 180_000, db_restore: 180_000,
|
|
53
|
+
secret_scan: 60_000, vulnerability_check: 60_000,
|
|
54
|
+
subagent: 180_000,
|
|
55
|
+
};
|
|
56
|
+
const DEFAULT_TOOL_TIMEOUT = 30_000;
|
|
57
|
+
|
|
58
|
+
let GS = { tc: 0, api: 0, ch: 0 };
|
|
59
|
+
let skillEngine: SkillEngine | null = null;
|
|
60
|
+
let mcpClient: MCPClient | null = null;
|
|
61
|
+
const validator = new ToolValidator();
|
|
62
|
+
let currentAbortController: AbortController | null = null;
|
|
63
|
+
|
|
64
|
+
const BACKUP_DIR = join(DEEPER_HOME, 'backups');
|
|
65
|
+
const MAX_BACKUPS = 50;
|
|
66
|
+
const fileBackupStack: Array<{ path: string; backupPath: string; timestamp: number }> = [];
|
|
67
|
+
|
|
68
|
+
function backupFile(filePath: string): void {
|
|
69
|
+
if (!existsSync(filePath)) return;
|
|
70
|
+
try {
|
|
71
|
+
if (!existsSync(BACKUP_DIR)) mkdirSync(BACKUP_DIR, { recursive: true });
|
|
72
|
+
const rel = relative(process.cwd(), filePath).replace(/[/\\]/g, '_');
|
|
73
|
+
const ts = Date.now();
|
|
74
|
+
const backupPath = join(BACKUP_DIR, `${ts}_${rel}`);
|
|
75
|
+
copyFileSync(filePath, backupPath);
|
|
76
|
+
fileBackupStack.push({ path: filePath, backupPath, timestamp: ts });
|
|
77
|
+
if (fileBackupStack.length > MAX_BACKUPS) {
|
|
78
|
+
const old = fileBackupStack.shift()!;
|
|
79
|
+
try { unlinkSync(old.backupPath); } catch {}
|
|
80
|
+
}
|
|
81
|
+
} catch {}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getSkillSystemPrompt(): string {
|
|
85
|
+
if (!skillEngine) return '';
|
|
86
|
+
return skillEngine.getSystemPrompt();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function startRepl(opts: ReplOptions): Promise<void> {
|
|
90
|
+
const tools = await loadBuiltinTools();
|
|
91
|
+
const toolDefs = toolsToDefs(tools);
|
|
92
|
+
|
|
93
|
+
const { setSubagentRunner } = await import('../tools/builtin/index.js');
|
|
94
|
+
setSubagentRunner(async (task: string, mode: 'foreground' | 'background') => {
|
|
95
|
+
const isBg = mode === 'background';
|
|
96
|
+
const run = async () => {
|
|
97
|
+
const lh: Message[] = [
|
|
98
|
+
{ role: 'system', content: `DeeperCode 子代理。用工具完成任务,完成输出摘要。cwd=${process.cwd()}` },
|
|
99
|
+
{ role: 'user', content: task },
|
|
100
|
+
];
|
|
101
|
+
const tds = toolsToDefs(tools);
|
|
102
|
+
let ce = 0, stag = 0;
|
|
103
|
+
while (true) {
|
|
104
|
+
const msgs = buildMsgs(lh);
|
|
105
|
+
let fc = '', th = '';
|
|
106
|
+
let tcs: Array<{ id: string; name: string; args: Record<string, unknown> }> = [];
|
|
107
|
+
let curTc: { id: string; name: string; argsStr: string } | null = null;
|
|
108
|
+
let se: string | null = null;
|
|
109
|
+
try {
|
|
110
|
+
const stream = await callApi(opts, msgs, tds, 131072); GS.api++; ce = 0;
|
|
111
|
+
for await (const chunk of stream) {
|
|
112
|
+
if (chunk.type === 'text') { fc += chunk.content || ''; }
|
|
113
|
+
if (chunk.type === 'thinking') th += chunk.content || '';
|
|
114
|
+
if (chunk.type === 'tool_call_start') {
|
|
115
|
+
const tc = (chunk as any).tool_call;
|
|
116
|
+
if (tc) curTc = { id: tc.id, name: tc.name, argsStr: '' };
|
|
117
|
+
}
|
|
118
|
+
if (chunk.type === 'tool_call_end' && curTc) {
|
|
119
|
+
const tcData = (chunk as any).tool_call;
|
|
120
|
+
try {
|
|
121
|
+
const args = tcData?.arguments || JSON.parse(curTc.argsStr || '{}');
|
|
122
|
+
tcs.push({ id: curTc.id, name: curTc.name, args });
|
|
123
|
+
} catch { tcs.push({ id: curTc.id, name: curTc.name, args: {} }); }
|
|
124
|
+
curTc = null;
|
|
125
|
+
}
|
|
126
|
+
if (chunk.type === 'done') break;
|
|
127
|
+
if (chunk.type === 'error') { se = chunk.error || '?'; break; }
|
|
128
|
+
}
|
|
129
|
+
} catch (e: unknown) { se = e instanceof Error ? e.message : String(e); }
|
|
130
|
+
|
|
131
|
+
if (se) { ce++; stag++; if (ce >= 2) return `子代理失败: ${se}`; await new Promise(r2 => setTimeout(r2, 1000)); continue; }
|
|
132
|
+
|
|
133
|
+
if (tcs.length > 0) {
|
|
134
|
+
stag = 0;
|
|
135
|
+
lh.push({ role: 'assistant', content: fc || null, reasoning_content: th || undefined, tool_calls: tcs.map(t => ({ id: t.id, name: t.name, arguments: { ...t.args } })) });
|
|
136
|
+
for (const tc of tcs) {
|
|
137
|
+
const tool = tools.find(t => t.name === tc.name);
|
|
138
|
+
if (!tool) { lh.push({ role: 'tool', content: `Error: unknown ${tc.name}`, tool_call_id: tc.id }); continue; }
|
|
139
|
+
const s2 = TOOL_SAFETY_MAP[tc.name] || 'safe';
|
|
140
|
+
if (s2 === 'dangerous') { lh.push({ role: 'tool', content: 'Skipped', tool_call_id: tc.id }); continue; }
|
|
141
|
+
try {
|
|
142
|
+
const ac = new AbortController();
|
|
143
|
+
const timeout = TOOL_TIMEOUT_MAP[tc.name] || DEFAULT_TOOL_TIMEOUT;
|
|
144
|
+
const timer = setTimeout(() => ac.abort(), timeout);
|
|
145
|
+
const r = await tool.execute(tc.args, ac.signal);
|
|
146
|
+
clearTimeout(timer);
|
|
147
|
+
const txt = sanitize(r.output || '').slice(0, TOOL_RESULT_MAX);
|
|
148
|
+
lh.push({ role: 'tool', content: r.success ? txt : `Error: ${(r as any).error}`, tool_call_id: tc.id });
|
|
149
|
+
GS.tc++;
|
|
150
|
+
} catch (e: unknown) { lh.push({ role: 'tool', content: `Error: ${e instanceof Error ? e.message : String(e)}`, tool_call_id: tc.id }); }
|
|
151
|
+
}
|
|
152
|
+
trimHistory(lh, 20); continue;
|
|
153
|
+
}
|
|
154
|
+
if (fc) { lh.push({ role: 'assistant', content: fc, reasoning_content: th || undefined }); stag = 0; }
|
|
155
|
+
else { stag++; if (stag >= 3) return `停滞: 连续${stag}轮无进展`; continue; }
|
|
156
|
+
const final = lh[lh.length - 1]?.content || '完成';
|
|
157
|
+
return final.slice(0, 800);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
if (isBg) {
|
|
162
|
+
run().then(result => {
|
|
163
|
+
history.push({ role: 'system', content: `[子代理] ${task.slice(0, 50)} → ${result.slice(0, 300)}` });
|
|
164
|
+
});
|
|
165
|
+
return `后台子代理已启动: ${task.slice(0, 80)}`;
|
|
166
|
+
} else {
|
|
167
|
+
O(G(` 🟊 subagent ${task.slice(0, 60)}...\n`));
|
|
168
|
+
const result = await run();
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const history: Message[] = [];
|
|
174
|
+
if (!existsSync(SESSION_DIR)) mkdirSync(SESSION_DIR, { recursive: true });
|
|
175
|
+
|
|
176
|
+
const sid = `sess_${Date.now()}`;
|
|
177
|
+
setSessionId(sid);
|
|
178
|
+
await xmemory.load();
|
|
179
|
+
const prevCount = xmemory.totalEntries;
|
|
180
|
+
const prevSummary = xmemory.getSessionSummary();
|
|
181
|
+
if (prevSummary) {
|
|
182
|
+
history.push({ role: 'system', content: `[跨会话记忆·已加载 ${prevCount} 条]\n${prevSummary.slice(0, 800)}` });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
skillEngine = new SkillEngine();
|
|
186
|
+
let skillCount = 0;
|
|
187
|
+
try { skillCount = await skillEngine.loadAll(); } catch { /* skills optional */ }
|
|
188
|
+
|
|
189
|
+
let mcpCount = 0;
|
|
190
|
+
let mcpToolCount = 0;
|
|
191
|
+
try {
|
|
192
|
+
mcpClient = new MCPClient();
|
|
193
|
+
const { getConfig } = await import('../core/config.js');
|
|
194
|
+
const cfg = getConfig();
|
|
195
|
+
const servers = cfg.mcpServers || [];
|
|
196
|
+
for (const srv of servers) {
|
|
197
|
+
if (!srv.enabled || !srv.autoConnect) continue;
|
|
198
|
+
try {
|
|
199
|
+
const mcpConfig: { name: string; type: 'stdio' | 'sse'; command?: string; args?: string[]; url?: string; env?: Record<string, string> } = {
|
|
200
|
+
name: srv.name,
|
|
201
|
+
type: srv.url ? 'sse' : 'stdio',
|
|
202
|
+
};
|
|
203
|
+
if (srv.command) { mcpConfig.command = srv.command; mcpConfig.args = srv.args; }
|
|
204
|
+
if (srv.url) mcpConfig.url = srv.url;
|
|
205
|
+
await mcpClient.connect(mcpConfig);
|
|
206
|
+
mcpCount++;
|
|
207
|
+
} catch (e: unknown) {
|
|
208
|
+
O(y(` MCP ${srv.name} 连接失败: ${e instanceof Error ? e.message.slice(0, 60) : String(e).slice(0, 60)}\n`));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (mcpCount > 0) {
|
|
212
|
+
const mcpTools = mcpClient.getAdaptedTools();
|
|
213
|
+
mcpToolCount = mcpTools.length;
|
|
214
|
+
tools.push(...mcpTools);
|
|
215
|
+
}
|
|
216
|
+
} catch { /* MCP optional */ }
|
|
217
|
+
|
|
218
|
+
const drawHeader = (sCount = skillCount, mc = mcpCount, mt = mcpToolCount) => {
|
|
219
|
+
O('\x1b[2J\x1b[H');
|
|
220
|
+
O(b(c(' DeeperCode')) + G(' · 全栈 AI 编程代理') + '\n');
|
|
221
|
+
const modes: string[] = [`${tools.length}工具`];
|
|
222
|
+
if (xmemory.totalEntries > 0) modes.push(`${xmemory.totalEntries}记忆`);
|
|
223
|
+
if (sCount > 0) modes.push(`${sCount}技能`);
|
|
224
|
+
if (mc > 0) modes.push(`${mc}MCP·${mt}工具`);
|
|
225
|
+
O(G(` ▸ V4-Pro · ${modes.join(' · ')}`) + '\n\n');
|
|
226
|
+
};
|
|
227
|
+
drawHeader();
|
|
228
|
+
|
|
229
|
+
let resizeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
230
|
+
const onResize = () => {
|
|
231
|
+
if (resizeTimer) return;
|
|
232
|
+
resizeTimer = setTimeout(() => { resizeTimer = null; Oflush(); }, 200);
|
|
233
|
+
};
|
|
234
|
+
process.stdout.on('resize', onResize);
|
|
235
|
+
|
|
236
|
+
let resolveLine: ((v: string) => void) | null = null;
|
|
237
|
+
let running = true;
|
|
238
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
239
|
+
let currentPrompt = c('❯ ');
|
|
240
|
+
const showPrompt = () => { Oflush(); rl.setPrompt(currentPrompt); rl.prompt(true); };
|
|
241
|
+
rl.on('line', (line: string) => { if (resolveLine) { const cb = resolveLine; resolveLine = null; cb(line); } });
|
|
242
|
+
const ask = (): Promise<string> => new Promise(r => { resolveLine = r; showPrompt(); });
|
|
243
|
+
const confirm = async (msg: string): Promise<boolean> => {
|
|
244
|
+
const sv = resolveLine; resolveLine = null;
|
|
245
|
+
O(y(`\n ⚠ ${msg} [y/N] `));
|
|
246
|
+
const a = await new Promise<string>(r2 => { resolveLine = r2; rl.prompt(true); });
|
|
247
|
+
const ok = a.toLowerCase().startsWith('y');
|
|
248
|
+
if (sv) resolveLine = sv; else showPrompt();
|
|
249
|
+
return ok;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const onSigint = () => {
|
|
253
|
+
if (currentAbortController) {
|
|
254
|
+
currentAbortController.abort();
|
|
255
|
+
currentAbortController = null;
|
|
256
|
+
O(y('\n ⚡ 已中断当前请求\n'));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (resolveLine) { const cb = resolveLine; resolveLine = null; cb('/quit'); return; }
|
|
260
|
+
Oflush();
|
|
261
|
+
process.stdout.removeListener('resize', onResize);
|
|
262
|
+
process.removeListener('uncaughtException', onUCE);
|
|
263
|
+
process.removeListener('unhandledRejection', onUHR);
|
|
264
|
+
xmemory.save().then(() => { O('\n' + y('再见!') + '\n'); running = false; rl.close(); process.exit(0); });
|
|
265
|
+
};
|
|
266
|
+
rl.on('SIGINT', onSigint);
|
|
267
|
+
const onUCE = (err: Error) => { if (!err.message?.includes('readline') && !err.message?.includes('abort')) O(r(`\n ⚠ ${err.message}`) + '\n'); };
|
|
268
|
+
process.on('uncaughtException', onUCE);
|
|
269
|
+
const onUHR = (reason: unknown) => { const m = reason instanceof Error ? reason.message : String(reason); if (!m.includes('readline') && !m.includes('Abort') && !m.includes('timeout')) O(r(`\n ⚠ ${m}`) + '\n'); };
|
|
270
|
+
process.on('unhandledRejection', onUHR);
|
|
271
|
+
|
|
272
|
+
while (running) {
|
|
273
|
+
currentPrompt = c('❯ ');
|
|
274
|
+
const tasks = getTodos();
|
|
275
|
+
if (tasks.length > 0) {
|
|
276
|
+
const active = tasks.filter(t => t.status === 'pending' || t.status === 'in_progress');
|
|
277
|
+
if (active.length > 0) currentPrompt = c(`❯ [${active.length}/${tasks.length}] `);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const input = await ask();
|
|
281
|
+
const trimmed = input.trim();
|
|
282
|
+
if (!trimmed) continue;
|
|
283
|
+
|
|
284
|
+
if (trimmed.startsWith('/')) {
|
|
285
|
+
if (trimmed === '/') {
|
|
286
|
+
O(c(' 可用命令:\n'));
|
|
287
|
+
O(G(' /help') + G(' 帮助信息') + '\n');
|
|
288
|
+
O(G(' /clear') + G(' 清空对话') + '\n');
|
|
289
|
+
O(G(' /quit') + G(' 退出') + '\n');
|
|
290
|
+
O(G(' /save') + G(' [n] 保存会话') + '\n');
|
|
291
|
+
O(G(' /load') + G(' [n] 加载会话') + '\n');
|
|
292
|
+
O(G(' /sessions') + G(' 会话列表') + '\n');
|
|
293
|
+
O(G(' /tools') + G(' [c] 工具列表') + '\n');
|
|
294
|
+
O(G(' /stats') + G(' 统计信息') + '\n');
|
|
295
|
+
O(G(' /memory') + G(' 记忆系统') + '\n');
|
|
296
|
+
O(G(' /tasks') + G(' 任务列表') + '\n');
|
|
297
|
+
O(G(' /rules') + G(' 规则管理') + '\n');
|
|
298
|
+
O(G(' /mcp') + G(' MCP服务器') + '\n');
|
|
251
299
|
O(G(' /plan') + G(' <任务> 先出方案') + '\n');
|
|
252
300
|
O(G(' /spec') + G(' <任务> 先出规格') + '\n');
|
|
253
|
-
O(G(' /
|
|
254
|
-
O(G(' /
|
|
255
|
-
O(G(' /
|
|
256
|
-
O(G(' /
|
|
257
|
-
O(G(' /
|
|
258
|
-
O(G(' /
|
|
259
|
-
O('\n');
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
if (cmd === '/
|
|
272
|
-
if (cmd === '/
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (cmd === '/
|
|
278
|
-
if (cmd === '/
|
|
279
|
-
if (cmd === '/
|
|
280
|
-
if (cmd === '/
|
|
281
|
-
if (cmd === '/
|
|
282
|
-
if (cmd === '/
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
if (cmd === '/
|
|
321
|
-
|
|
322
|
-
O(
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
if (
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
}
|
|
692
|
-
|
|
301
|
+
O(G(' /review') + G(' <路径> 代码审查') + '\n');
|
|
302
|
+
O(G(' /fix') + G(' [目标] 自动修复') + '\n');
|
|
303
|
+
O(G(' /commit') + G(' 智能提交') + '\n');
|
|
304
|
+
O(G(' /analyze') + G(' [路径] 项目分析') + '\n');
|
|
305
|
+
O(G(' /diff') + G(' <文件> 变更预览') + '\n');
|
|
306
|
+
O(G(' /undo') + G(' 撤销操作') + '\n');
|
|
307
|
+
O(G(' /status') + G(' 当前状态') + '\n');
|
|
308
|
+
O(G(' /model') + G(' 模型设置') + '\n');
|
|
309
|
+
O(G(' /config') + G(' 配置管理') + '\n');
|
|
310
|
+
O(G(' /cwd') + G(' 当前目录') + '\n');
|
|
311
|
+
O(G(' /export') + G(' 导出对话') + '\n');
|
|
312
|
+
O(G(' /init') + G(' 初始化项目') + '\n');
|
|
313
|
+
O('\n'); continue;
|
|
314
|
+
}
|
|
315
|
+
const [cmd, ...rest] = trimmed.split(/\s+/);
|
|
316
|
+
const arg = rest.join(' ');
|
|
317
|
+
if (cmd === '/quit') break;
|
|
318
|
+
if (cmd === '/clear') { history.length = 0; O('\x1b[2J\x1b[H' + g('已清空') + '\n\n'); continue; }
|
|
319
|
+
if (cmd === '/save') { await saveSession(history, arg); continue; }
|
|
320
|
+
if (cmd === '/load' || cmd === '/resume') {
|
|
321
|
+
if (arg) await loadNamedSession(history, arg);
|
|
322
|
+
else await loadLatestSession(history);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (cmd === '/sessions') { await listSessions(); continue; }
|
|
326
|
+
if (cmd === '/tools') { if (arg) await showToolsOf(arg, tools); else await showToolsBrief(tools); continue; }
|
|
327
|
+
if (cmd === '/stats') { O(B(`▸ API:${GS.api} 工具:${GS.tc} 字符:${GS.ch}`) + '\n\n'); continue; }
|
|
328
|
+
if (cmd === '/memory') { await showMemory(); continue; }
|
|
329
|
+
if (cmd === '/tasks') { await showTasks(); continue; }
|
|
330
|
+
if (cmd === '/model') { O(c('模型: deepseek-v4-pro | deepseek-v4-flash\n deeper config set model <name>\n\n')); continue; }
|
|
331
|
+
if (cmd === '/config') { O(c('配置: deeper config list | deeper config set <key> <value>\n\n')); continue; }
|
|
332
|
+
if (cmd === '/cwd') { O(G(` ${process.cwd()}\n\n`)); continue; }
|
|
333
|
+
if (cmd === '/export') { await exportHistory(history); continue; }
|
|
334
|
+
if (cmd === '/init') { await initProject(); continue; }
|
|
335
|
+
if (cmd === '/status') { O(B(`▸ API:${GS.api} 工具:${GS.tc} 字符:${GS.ch} · 上下文:${history.length}条`) + '\n\n'); continue; }
|
|
336
|
+
if (cmd === '/mcp') {
|
|
337
|
+
if (!mcpClient) { O(y(' MCP 未初始化\n\n')); continue; }
|
|
338
|
+
const servers = mcpClient.getConnectedServers();
|
|
339
|
+
if (!servers.length) { O(G(' 无已连接的 MCP 服务器\n 使用 deeper mcp add 添加\n\n')); continue; }
|
|
340
|
+
O(b(c(' MCP Servers')) + G(` · ${servers.length} 个`) + '\n');
|
|
341
|
+
for (const name of servers) {
|
|
342
|
+
const mcpTools = mcpClient.listTools(name);
|
|
343
|
+
O(G(` 📡 ${name}`) + G(` · ${mcpTools.length} 工具`) + '\n');
|
|
344
|
+
for (const t of mcpTools.slice(0, 5)) O(G(` - ${t.name}: ${t.description.slice(0, 50)}`) + '\n');
|
|
345
|
+
if (mcpTools.length > 5) O(G(` …还有 ${mcpTools.length - 5} 个`) + '\n');
|
|
346
|
+
}
|
|
347
|
+
O('\n'); continue;
|
|
348
|
+
}
|
|
349
|
+
if (cmd === '/rules') {
|
|
350
|
+
const projectRules = join(process.cwd(), '.deeper', 'rules.md');
|
|
351
|
+
const globalRules = join(DEEPER_HOME, 'rules.md');
|
|
352
|
+
O(b(c(' Rules')) + '\n');
|
|
353
|
+
const showRules = (label: string, path: string) => {
|
|
354
|
+
if (existsSync(path)) {
|
|
355
|
+
const content = readFileSync(path, 'utf-8');
|
|
356
|
+
O(G(` ${label}: `) + c(path) + G(` (${content.split('\n').length}行)`) + '\n');
|
|
357
|
+
const lines = content.split('\n').slice(0, 8);
|
|
358
|
+
for (const l of lines) O(G(` ${l.slice(0, 70)}`) + '\n');
|
|
359
|
+
if (content.split('\n').length > 8) O(G(' …') + '\n');
|
|
360
|
+
} else {
|
|
361
|
+
O(G(` ${label}: `) + y('未设置') + G(` (创建: ${path})`) + '\n');
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
showRules('项目规则', projectRules);
|
|
365
|
+
showRules('全局规则', globalRules);
|
|
366
|
+
O('\n'); continue;
|
|
367
|
+
}
|
|
368
|
+
if (cmd === '/help') {
|
|
369
|
+
O(c(' /help /clear /quit /save [name] /load|resume [name] /sessions\n'));
|
|
370
|
+
O(c(' /tools [cat] /stats /memory /tasks /model /config /cwd /export /init /mcp /rules\n'));
|
|
371
|
+
O(c(' /plan <任务> /spec <任务> /review <路径> /fix [目标]\n'));
|
|
372
|
+
O(c(' /commit /analyze [路径] /diff <文件> /undo /status\n\n'));
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (cmd === '/plan') {
|
|
376
|
+
if (!arg) { O(y(' 用法: /plan <任务描述>\n\n')); continue; }
|
|
377
|
+
O(b(c(' Plan Mode')) + G(` • ${arg.slice(0, 50)}`) + '\n\n');
|
|
378
|
+
history.push({ role: 'system', content: `[Plan Mode]
|
|
379
|
+
你必须遵循以下流程,严格按步骤执行:
|
|
380
|
+
1. 先输出一份详尽的实施方案(不要写代码)
|
|
381
|
+
2. 方案必须包含:需求分析、技术选型、架构设计、数据流、模块划分、实施步骤
|
|
382
|
+
3. 等待用户确认方案(输入 ok/继续/可以 等确认词)
|
|
383
|
+
4. 用户确认后,按方案逐步实施,每完成一步更新 todo
|
|
384
|
+
5. 完成后总结交付内容
|
|
385
|
+
|
|
386
|
+
当前任务:${arg}` });
|
|
387
|
+
history.push({ role: 'user', content: arg });
|
|
388
|
+
const pfdefs = toolsToDefs(tools);
|
|
389
|
+
await runLoop(opts, history, tools, pfdefs, confirm);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (cmd === '/spec') {
|
|
393
|
+
if (!arg) { O(y(' 用法: /spec <任务描述>\n\n')); continue; }
|
|
394
|
+
O(b(c(' Spec Mode')) + G(` • ${arg.slice(0, 50)}`) + '\n\n');
|
|
395
|
+
history.push({ role: 'system', content: `[Spec Mode]
|
|
396
|
+
你必须遵循以下流程,严格按步骤执行:
|
|
397
|
+
1. 先输出一份完整的产品规格文档(不要写代码)
|
|
398
|
+
2. 规格文档必须包含:需求概述、功能清单、交互设计、数据结构、API设计、组件树、非功能性需求、测试策略、里程碑
|
|
399
|
+
3. 文档使用 Markdown 格式,标题用 # 层次清晰
|
|
400
|
+
4. 等待用户确认规格(输入 ok/继续/可以 等确认词)
|
|
401
|
+
5. 用户确认后,按规格逐步实施,每完成一步更新 todo
|
|
402
|
+
6. 完成后总结交付内容
|
|
403
|
+
|
|
404
|
+
当前任务:${arg}` });
|
|
405
|
+
history.push({ role: 'user', content: arg });
|
|
406
|
+
const sfdefs = toolsToDefs(tools);
|
|
407
|
+
await runLoop(opts, history, tools, sfdefs, confirm);
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (cmd === '/review') {
|
|
411
|
+
const target = arg || '.';
|
|
412
|
+
O(b(c(' Review Mode')) + G(` • ${target}`) + '\n\n');
|
|
413
|
+
history.push({ role: 'system', content: `[Review Mode]
|
|
414
|
+
你是一位资深代码审查专家。请对指定代码进行全面审查:
|
|
415
|
+
|
|
416
|
+
1. 先用 read_file / glob_find / grep_search 读取相关代码
|
|
417
|
+
2. 从以下维度逐项审查并评分(1-10):
|
|
418
|
+
- 代码质量:可读性、命名、结构
|
|
419
|
+
- 安全性:注入、XSS、密钥泄露、权限问题
|
|
420
|
+
- 性能:N+1查询、内存泄漏、不必要的计算
|
|
421
|
+
- 可维护性:耦合度、重复代码、测试覆盖
|
|
422
|
+
- 最佳实践:设计模式、SOLID原则、错误处理
|
|
423
|
+
3. 对每个问题给出:文件路径、行号范围、问题描述、修复建议、严重程度(🔴高/🟡中/🟢低)
|
|
424
|
+
4. 最后给出总体评分和改进优先级列表
|
|
425
|
+
|
|
426
|
+
审查目标:${target}` });
|
|
427
|
+
history.push({ role: 'user', content: `审查 ${target} 的代码质量` });
|
|
428
|
+
const rvdefs = toolsToDefs(tools);
|
|
429
|
+
await runLoop(opts, history, tools, rvdefs, confirm);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (cmd === '/commit') {
|
|
433
|
+
O(b(c(' Commit Mode')) + G(' • 智能提交') + '\n\n');
|
|
434
|
+
try {
|
|
435
|
+
const statusOut = execSync('git status --porcelain', { encoding: 'utf-8', timeout: 10000, cwd: process.cwd() });
|
|
436
|
+
if (!statusOut.trim()) { O(y(' 没有未提交的变更\n\n')); continue; }
|
|
437
|
+
const diffOut = execSync('git diff --stat', { encoding: 'utf-8', timeout: 15000, cwd: process.cwd() });
|
|
438
|
+
const stagedOut = execSync('git diff --cached --stat', { encoding: 'utf-8', timeout: 15000, cwd: process.cwd() });
|
|
439
|
+
const files = statusOut.trim().split('\n').filter(Boolean);
|
|
440
|
+
O(G(` ${files.length} 个文件变更:\n`));
|
|
441
|
+
for (const f of files.slice(0, 20)) {
|
|
442
|
+
const status = f.slice(0, 2).trim();
|
|
443
|
+
const path = f.slice(3);
|
|
444
|
+
const icon = status === 'M' ? y('M') : status === 'A' ? g('A') : status === 'D' ? r('D') : status === '?' ? G('?') : c(status);
|
|
445
|
+
O(G(` ${icon} ${path.slice(0, 60)}`) + '\n');
|
|
446
|
+
}
|
|
447
|
+
if (files.length > 20) O(G(` …还有 ${files.length - 20} 个`) + '\n');
|
|
448
|
+
O('\n');
|
|
449
|
+
history.push({ role: 'system', content: `[Commit Mode]
|
|
450
|
+
分析 git 变更并生成提交信息:
|
|
451
|
+
1. 先用 run_command 执行 git diff 查看具体变更内容
|
|
452
|
+
2. 分析变更类型和范围,生成符合 Conventional Commits 规范的提交信息
|
|
453
|
+
3. 格式: type(scope): description
|
|
454
|
+
- type: feat/fix/refactor/docs/style/test/chore/perf
|
|
455
|
+
- scope: 可选,影响的模块
|
|
456
|
+
4. 用 run_command 执行 git add 和 git commit
|
|
457
|
+
5. 如果变更较多,建议分多次提交
|
|
458
|
+
|
|
459
|
+
当前变更统计:
|
|
460
|
+
${diffOut || stagedOut || '无staged变更'}` });
|
|
461
|
+
history.push({ role: 'user', content: '分析变更并提交' });
|
|
462
|
+
const cmdefs = toolsToDefs(tools);
|
|
463
|
+
await runLoop(opts, history, tools, cmdefs, confirm);
|
|
464
|
+
} catch (e: unknown) {
|
|
465
|
+
O(r(` Git 操作失败: ${e instanceof Error ? e.message.slice(0, 80) : String(e).slice(0, 80)}\n\n`));
|
|
466
|
+
}
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (cmd === '/analyze') {
|
|
470
|
+
const target = arg || process.cwd();
|
|
471
|
+
O(b(c(' Analyze Mode')) + G(` • ${target}`) + '\n\n');
|
|
472
|
+
history.push({ role: 'system', content: `[Analyze Mode]
|
|
473
|
+
你是一位项目架构分析师。请对项目进行全面分析:
|
|
474
|
+
|
|
475
|
+
1. 用 glob_find 扫描项目目录结构
|
|
476
|
+
2. 用 read_file 读取 package.json / tsconfig.json / Cargo.toml 等配置
|
|
477
|
+
3. 用 grep_search / codebase_search 分析代码模式
|
|
478
|
+
4. 输出以下分析报告:
|
|
479
|
+
- 📁 项目结构树(关键目录和文件)
|
|
480
|
+
- 🛠 技术栈识别(语言、框架、库、工具链)
|
|
481
|
+
- 📊 代码统计(文件数、代码行数估算、各语言占比)
|
|
482
|
+
- 🏗️ 架构模式(MVC/微服务/单体/插件式等)
|
|
483
|
+
- 🔗 依赖关系(核心依赖、开发依赖、版本风险)
|
|
484
|
+
- ⚠️ 潜在问题(过时依赖、安全风险、代码异味)
|
|
485
|
+
- 💡 改进建议(优先级排序)
|
|
486
|
+
|
|
487
|
+
分析目标:${target}` });
|
|
488
|
+
history.push({ role: 'user', content: `分析项目 ${target}` });
|
|
489
|
+
const andefs = toolsToDefs(tools);
|
|
490
|
+
await runLoop(opts, history, tools, andefs, confirm);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (cmd === '/fix') {
|
|
494
|
+
const target = arg || '';
|
|
495
|
+
O(b(c(' Fix Mode')) + G(' • 自动修复') + '\n\n');
|
|
496
|
+
history.push({ role: 'system', content: `[Fix Mode]
|
|
497
|
+
你是一位自动修复工程师。请执行以下流程:
|
|
498
|
+
|
|
499
|
+
1. 先用 run_command 运行构建命令(如 npm run build, tsc --noEmit, cargo check 等)
|
|
500
|
+
2. 如果构建成功,运行测试(npm test, cargo test 等)
|
|
501
|
+
3. 如果有错误:
|
|
502
|
+
a. 仔细分析错误信息,定位到具体文件和行号
|
|
503
|
+
b. 用 read_file 读取出错文件的相关代码
|
|
504
|
+
c. 理解错误根因,制定修复方案
|
|
505
|
+
d. 用 edit_file 精确修复(优先用 edit 而非重写整个文件)
|
|
506
|
+
e. 重新运行构建验证修复
|
|
507
|
+
f. 重复直到所有错误清除
|
|
508
|
+
4. 如果测试失败,同样分析并修复
|
|
509
|
+
5. 最多尝试修复 5 轮,避免无限循环
|
|
510
|
+
6. 每次修复后都重新验证
|
|
511
|
+
|
|
512
|
+
${target ? `修复目标:${target}\n` : '自动检测并修复所有构建/测试错误'}` });
|
|
513
|
+
history.push({ role: 'user', content: target || '检测并修复项目错误' });
|
|
514
|
+
const fxdefs = toolsToDefs(tools);
|
|
515
|
+
await runLoop(opts, history, tools, fxdefs, confirm);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
if (cmd === '/diff') {
|
|
519
|
+
if (!arg) { O(y(' 用法: /diff <文件路径>\n 显示文件最近的变更\n\n')); continue; }
|
|
520
|
+
const filePath = arg.startsWith('/') || arg.startsWith('\\') || arg.length > 2 ? arg : join(process.cwd(), arg);
|
|
521
|
+
if (fileBackupStack.length === 0) { O(y(' 没有可用的备份记录\n\n')); continue; }
|
|
522
|
+
const backups = fileBackupStack.filter(b => b.path === filePath).reverse();
|
|
523
|
+
if (backups.length === 0) {
|
|
524
|
+
const allBackups = [...fileBackupStack].reverse();
|
|
525
|
+
O(G(' 最近的文件变更:\n'));
|
|
526
|
+
for (const b of allBackups.slice(0, 10)) {
|
|
527
|
+
const rel = relative(process.cwd(), b.path);
|
|
528
|
+
const time = new Date(b.timestamp).toLocaleTimeString();
|
|
529
|
+
O(G(` ${time} ${rel.slice(0, 50)}`) + '\n');
|
|
530
|
+
}
|
|
531
|
+
O('\n'); continue;
|
|
532
|
+
}
|
|
533
|
+
const latest = backups[0];
|
|
534
|
+
try {
|
|
535
|
+
const currentContent = existsSync(filePath) ? readFileSync(filePath, 'utf-8') : '(文件已删除)';
|
|
536
|
+
const backupContent = readFileSync(latest.backupPath, 'utf-8');
|
|
537
|
+
const diffResult = computeSimpleDiff(backupContent, currentContent, filePath);
|
|
538
|
+
O(b(c(' Diff')) + G(` • ${relative(process.cwd(), filePath)}`) + '\n\n');
|
|
539
|
+
O(diffResult + '\n\n');
|
|
540
|
+
} catch {
|
|
541
|
+
O(y(' 无法读取文件进行对比\n\n'));
|
|
542
|
+
}
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (cmd === '/undo') {
|
|
546
|
+
if (fileBackupStack.length === 0) { O(y(' 没有可撤销的操作\n\n')); continue; }
|
|
547
|
+
const last = fileBackupStack[fileBackupStack.length - 1];
|
|
548
|
+
try {
|
|
549
|
+
const rel = relative(process.cwd(), last.path);
|
|
550
|
+
copyFileSync(last.backupPath, last.path);
|
|
551
|
+
fileBackupStack.pop();
|
|
552
|
+
try { unlinkSync(last.backupPath); } catch {}
|
|
553
|
+
O(g(' ✓ 已撤销') + G(` ${rel}`) + '\n');
|
|
554
|
+
O(G(' 提示: 可继续 /undo 撤销更多操作\n\n'));
|
|
555
|
+
} catch (e: unknown) {
|
|
556
|
+
O(r(` 撤销失败: ${e instanceof Error ? e.message.slice(0, 60) : String(e).slice(0, 60)}\n\n`));
|
|
557
|
+
}
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
O(G(` 未知命令: ${cmd} (输入 /help 查看帮助)\n\n`));
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const fdefs = toolsToDefs(tools);
|
|
565
|
+
O('\n');
|
|
566
|
+
const um: Message = { role: 'user', content: trimmed };
|
|
567
|
+
history.push(um); trimHistory(history, MAX_HISTORY);
|
|
568
|
+
await runLoop(opts, history, tools, fdefs, confirm);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
rl.close();
|
|
572
|
+
Oflush();
|
|
573
|
+
process.stdout.removeListener('resize', onResize);
|
|
574
|
+
process.removeListener('uncaughtException', onUCE);
|
|
575
|
+
process.removeListener('unhandledRejection', onUHR);
|
|
576
|
+
await xmemory.save();
|
|
577
|
+
if (mcpClient) mcpClient.disconnectAll();
|
|
578
|
+
Oflush();
|
|
579
|
+
O(y('\n再见!\n')); process.exit(0);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ========== Core loop ==========
|
|
583
|
+
|
|
584
|
+
async function runLoop(
|
|
585
|
+
opts: ReplOptions, history: Message[], tools: Tool[],
|
|
586
|
+
toolDefs: ToolDef[], confirm: (msg: string) => Promise<boolean>,
|
|
587
|
+
): Promise<void> {
|
|
588
|
+
let ttc = 0, ce = 0, stagnation = 0;
|
|
589
|
+
|
|
590
|
+
for (let iter = 0; ; iter++) {
|
|
591
|
+
const ctxTokens = estimateTokens(history.map(m => m.content || '').join('\n'));
|
|
592
|
+
const toolsTokenOverhead = toolDefs.length * 80;
|
|
593
|
+
const totalCtx = ctxTokens + toolsTokenOverhead;
|
|
594
|
+
const nearLimit = totalCtx > CONTEXT_LIMIT * 0.95;
|
|
595
|
+
|
|
596
|
+
if (totalCtx > CONTEXT_LIMIT * 0.7 && history.length > 10) {
|
|
597
|
+
compressHistory(history);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (stagnation >= 5) {
|
|
601
|
+
O(y(`\n 连续${stagnation}轮无进展,任务交还\n\n`));
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const msgs = buildMsgs(history);
|
|
606
|
+
const st = Date.now();
|
|
607
|
+
let fc = '', th = '';
|
|
608
|
+
let tcs: Array<{ id: string; name: string; args: Record<string, unknown> }> = [];
|
|
609
|
+
let curTc: { id: string; name: string; argsStr: string } | null = null;
|
|
610
|
+
let se: string | null = null;
|
|
611
|
+
const amax = iter === 0 ? 131072 : nearLimit ? 32768 : 65536;
|
|
612
|
+
let showingThink = false;
|
|
613
|
+
|
|
614
|
+
resetTimer();
|
|
615
|
+
const animLabel = () => showingThink ? '思考中' : '解析中';
|
|
616
|
+
const animTick = () => { O('\r' + thinkingAnim(animLabel())); };
|
|
617
|
+
let thinkAnimIv: ReturnType<typeof setInterval> | null = null;
|
|
618
|
+
animTick();
|
|
619
|
+
thinkAnimIv = setInterval(animTick, 80);
|
|
620
|
+
const md = new MarkdownStreamRenderer();
|
|
621
|
+
|
|
622
|
+
const startStreamAnim = () => {
|
|
623
|
+
if (thinkAnimIv) return;
|
|
624
|
+
resetTimer();
|
|
625
|
+
animTick();
|
|
626
|
+
thinkAnimIv = setInterval(animTick, 80);
|
|
627
|
+
};
|
|
628
|
+
const stopStreamAnim = () => {
|
|
629
|
+
if (thinkAnimIv) { clearInterval(thinkAnimIv); thinkAnimIv = null; }
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
currentAbortController = new AbortController();
|
|
633
|
+
|
|
634
|
+
try {
|
|
635
|
+
const stream = await callApi(opts, msgs, toolDefs, amax, currentAbortController.signal);
|
|
636
|
+
GS.api++; ce = 0;
|
|
637
|
+
|
|
638
|
+
const cols = process.stdout.columns || 80;
|
|
639
|
+
|
|
640
|
+
for await (const chunk of stream) {
|
|
641
|
+
if (chunk.type === 'text') {
|
|
642
|
+
const t = chunk.content || '';
|
|
643
|
+
fc += t;
|
|
644
|
+
if (fc.length > 100_000) fc = fc.slice(-80_000);
|
|
645
|
+
if (fc === t) {
|
|
646
|
+
if (showingThink) { showingThink = false; stopStreamAnim(); }
|
|
647
|
+
Oflush(); O('\r' + ' '.repeat(cols) + '\r');
|
|
648
|
+
if (!fc.slice(t.length - t.length)) O(b(c('●')) + ' ');
|
|
649
|
+
}
|
|
650
|
+
const rendered = md.feed(t);
|
|
651
|
+
if (rendered) O(rendered);
|
|
652
|
+
GS.ch += t.length;
|
|
653
|
+
}
|
|
654
|
+
if (chunk.type === 'thinking') {
|
|
655
|
+
if (!showingThink) { showingThink = true; startStreamAnim(); }
|
|
656
|
+
th += chunk.content || '';
|
|
657
|
+
if (th.length > 20_000) th = th.slice(-16_000);
|
|
658
|
+
}
|
|
659
|
+
if (chunk.type === 'tool_call_start') {
|
|
660
|
+
if (showingThink) { O('\r' + ' '.repeat(cols) + '\r'); showingThink = false; }
|
|
661
|
+
stopStreamAnim();
|
|
662
|
+
const tc = (chunk as any).tool_call;
|
|
663
|
+
if (tc) curTc = { id: tc.id, name: tc.name, argsStr: '' };
|
|
664
|
+
}
|
|
665
|
+
if (chunk.type === 'tool_call_end' && curTc) {
|
|
666
|
+
const tcData = (chunk as any).tool_call;
|
|
667
|
+
try {
|
|
668
|
+
const args = tcData?.arguments || JSON.parse(curTc.argsStr || '{}');
|
|
669
|
+
tcs.push({ id: curTc.id, name: curTc.name, args });
|
|
670
|
+
} catch { tcs.push({ id: curTc.id, name: curTc.name, args: {} }); }
|
|
671
|
+
curTc = null;
|
|
672
|
+
}
|
|
673
|
+
if (chunk.type === 'done') break;
|
|
674
|
+
if (chunk.type === 'error') { se = chunk.error || '?'; break; }
|
|
675
|
+
}
|
|
676
|
+
} catch (e: unknown) {
|
|
677
|
+
stopStreamAnim();
|
|
678
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
679
|
+
if (errMsg.includes('abort') || errMsg.includes('cancel')) {
|
|
680
|
+
se = null;
|
|
681
|
+
O(y('\n ⚡ 请求已取消\n'));
|
|
682
|
+
} else {
|
|
683
|
+
se = errMsg;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
currentAbortController = null;
|
|
688
|
+
stopStreamAnim();
|
|
689
|
+
Oflush();
|
|
690
|
+
const remaining = md.flush();
|
|
691
|
+
if (remaining) O(remaining);
|
|
692
|
+
md.reset();
|
|
693
|
+
|
|
694
|
+
if (se) {
|
|
695
|
+
ce++; O(r(`\n ✗ ${se}`));
|
|
696
|
+
if (ce >= 3) { O(r('\n 连续失败,放弃\n\n')); return; }
|
|
697
|
+
const w = Math.min(1000 * ce, 5000); O(G(` 重试(${ce}/3)...\n`)); await new Promise(r2 => setTimeout(r2, w)); continue;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const el = Date.now() - st;
|
|
701
|
+
if (fc && !tcs.length) O('\n');
|
|
702
|
+
|
|
703
|
+
if (tcs.length > 0) {
|
|
704
|
+
stagnation = 0;
|
|
705
|
+
Oflush();
|
|
706
|
+
const compactFc = fc && fc.length > 500 ? fc.replace(/\n/g, ' ').slice(0, 300) + '…' : fc;
|
|
707
|
+
history.push({ role: 'assistant', content: compactFc || null, reasoning_content: th || undefined, tool_calls: tcs.map(t => ({ id: t.id, name: t.name, arguments: { ...t.args } })) });
|
|
708
|
+
fc = ''; th = '';
|
|
709
|
+
let doneTools = 0;
|
|
710
|
+
|
|
711
|
+
if (iter === 0) {
|
|
712
|
+
const tdSummary = todoSummary();
|
|
713
|
+
if (tdSummary) { Oflush(); O('\n' + d(tdSummary.split('\n').join('\n ')) + '\n'); }
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const safe = tcs.filter(t => (TOOL_SAFETY_MAP[t.name] || 'safe') === 'safe');
|
|
717
|
+
const rest = tcs.filter(t => (TOOL_SAFETY_MAP[t.name] || 'safe') !== 'safe');
|
|
718
|
+
if (safe.length > 1) {
|
|
719
|
+
const results = await Promise.allSettled(safe.map(async tc => { const r = await execTool(tc, tools, opts); doneTools++; return r; }));
|
|
720
|
+
for (const r of results) {
|
|
721
|
+
if (r.status === 'fulfilled') { history.push(r.value); ttc++; GS.tc++; }
|
|
722
|
+
else { history.push({ role: 'tool', content: `Error: ${String(r.reason)}`, tool_call_id: 'parallel' }); }
|
|
723
|
+
}
|
|
724
|
+
} else if (safe.length === 1) {
|
|
725
|
+
const r = await execTool(safe[0], tools, opts);
|
|
726
|
+
history.push(r); ttc++; GS.tc++; doneTools++;
|
|
727
|
+
}
|
|
728
|
+
for (const tc of rest) {
|
|
729
|
+
const r2 = await execTool(tc, tools, opts, confirm);
|
|
730
|
+
history.push(r2); if (!r2.content?.startsWith('Error:') && !r2.content?.includes('skipped')) { ttc++; GS.tc++; }
|
|
731
|
+
doneTools++;
|
|
732
|
+
}
|
|
733
|
+
tcs.length = 0; safe.length = 0; rest.length = 0;
|
|
734
|
+
|
|
735
|
+
trimHistory(history, MAX_HISTORY); continue;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (fc) { history.push({ role: 'assistant', content: fc, reasoning_content: th || undefined }); stagnation = 0; }
|
|
739
|
+
else { stagnation++; }
|
|
740
|
+
|
|
741
|
+
const ctxPct = ((totalCtx / CONTEXT_LIMIT) * 100).toFixed(1);
|
|
742
|
+
const ctxWarn = totalCtx > CTX_WARN ? y(` ${ctxPct}%`) : G(` ${ctxPct}%`);
|
|
743
|
+
O(G(` ▸ ${(el / 1000).toFixed(1)}s · ${ttc} 工具 · 上下文${ctxWarn}`) + '\n\n');
|
|
744
|
+
trimHistory(history, MAX_HISTORY);
|
|
745
|
+
|
|
746
|
+
if (fc) {
|
|
747
|
+
const lu = lastUser(history);
|
|
748
|
+
xmemory.storeEpisodic(`完成任务: ${lu.slice(0, 100)} → ${fc.slice(0, 200)}`, ['task', 'complete'], 5);
|
|
749
|
+
xmemory.storeSemantic(`学会了关于 ${lu.slice(0, 80)} 的处理方式`, ['learning'], 4);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
async function execTool(
|
|
757
|
+
tc: { id: string; name: string; args: Record<string, unknown> },
|
|
758
|
+
tools: Tool[], opts: ReplOptions,
|
|
759
|
+
confirm?: (msg: string) => Promise<boolean>,
|
|
760
|
+
): Promise<Message> {
|
|
761
|
+
const tool = tools.find(t => t.name === tc.name);
|
|
762
|
+
if (!tool) return { role: 'tool', content: `Error: unknown tool ${tc.name}`, tool_call_id: tc.id };
|
|
763
|
+
|
|
764
|
+
const vResult = validator.validate(tool, tc.args);
|
|
765
|
+
if (!vResult.success) {
|
|
766
|
+
return { role: 'tool', content: `Error: ${vResult.error}`, tool_call_id: tc.id };
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const s = TOOL_SAFETY_MAP[tc.name] || 'safe';
|
|
770
|
+
const toolName = tc.name;
|
|
771
|
+
const tSyms = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏';
|
|
772
|
+
let ti = 0;
|
|
773
|
+
|
|
774
|
+
if ((s === 'dangerous' || s === 'confirm') && confirm) {
|
|
775
|
+
Oflush(); O('\r' + ' '.repeat(process.stdout.columns || 80) + '\r');
|
|
776
|
+
const prefix = s === 'dangerous' ? '⛔' : '⚠';
|
|
777
|
+
O(y(` ${prefix}确认? `));
|
|
778
|
+
const ok = await confirm(`执行 ${toolName}?`);
|
|
779
|
+
if (!ok) { O(G(' 跳过\n')); return { role: 'tool', content: 'User skipped', tool_call_id: tc.id }; }
|
|
780
|
+
O('\r' + A.d + ' ' + A.R + A.c + tSyms[0] + A.R + A.G + ' ' + toolName + A.R + ' ');
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
let execAnimIv: ReturnType<typeof setInterval> | null = null;
|
|
784
|
+
const stopToolAnim = () => { if (execAnimIv) { clearInterval(execAnimIv); execAnimIv = null; } };
|
|
785
|
+
try {
|
|
786
|
+
const toolStart = Date.now();
|
|
787
|
+
const rawWrite = (s: string) => { try { process.stdout.write(s); } catch {} };
|
|
788
|
+
rawWrite('\r' + thinkingAnimAt(toolName, toolStart));
|
|
789
|
+
execAnimIv = setInterval(() => { rawWrite('\r' + thinkingAnimAt(toolName, toolStart)); }, 80);
|
|
790
|
+
|
|
791
|
+
const timeout = TOOL_TIMEOUT_MAP[tc.name] || DEFAULT_TOOL_TIMEOUT;
|
|
792
|
+
const ac = new AbortController();
|
|
793
|
+
const timer = setTimeout(() => ac.abort(), timeout);
|
|
794
|
+
|
|
795
|
+
if (tc.name === 'write_file' || tc.name === 'edit_file') {
|
|
796
|
+
const fp = (tc.args.file_path || tc.args.path || tc.args.file || '') as string;
|
|
797
|
+
if (fp) backupFile(fp);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
let result: { success: boolean; error?: string; output?: string };
|
|
801
|
+
try {
|
|
802
|
+
result = await tool.execute(tc.args, ac.signal) as any;
|
|
803
|
+
clearTimeout(timer);
|
|
804
|
+
} catch (e: unknown) {
|
|
805
|
+
clearTimeout(timer);
|
|
806
|
+
result = { success: false, error: (e as Error).message, output: '' };
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
stopToolAnim();
|
|
810
|
+
const rawResult = result.success ? result.output || '' : `Error: ${result.error || 'failed'}`;
|
|
811
|
+
const text = result.success ? sanitize(rawResult).slice(0, TOOL_RESULT_MAX) : `Error: ${result.error || 'failed'}`;
|
|
812
|
+
const showPath = (p: string) => p.split(/[/\\]/).filter(Boolean).slice(-2).join('/').slice(-35);
|
|
813
|
+
|
|
814
|
+
let brief = '';
|
|
815
|
+
if (tc.name === 'write_file' || tc.name === 'edit_file') {
|
|
816
|
+
const fp = (tc.args.file_path || tc.args.path || tc.args.file || '') as string;
|
|
817
|
+
brief = showPath(fp);
|
|
818
|
+
} else if (tc.name === 'todo_manager') {
|
|
819
|
+
brief = '任务已更新';
|
|
820
|
+
} else {
|
|
821
|
+
brief = rawResult.replace(/\n/g, ' ').slice(0, 60);
|
|
822
|
+
}
|
|
823
|
+
Oflush(); O('\r' + ' '.repeat(process.stdout.columns || 80) + '\r');
|
|
824
|
+
if (tc.name === 'todo_manager' && result.success) {
|
|
825
|
+
O(A.y + A.b + ' ▸▸ █ 任务面板' + A.R + '\n');
|
|
826
|
+
const lines = rawResult.split('\n').filter(Boolean);
|
|
827
|
+
let shown = 0; const MAX_SHOW = 16;
|
|
828
|
+
for (const line of lines) {
|
|
829
|
+
const trimmed = line.trim();
|
|
830
|
+
if (!trimmed) continue;
|
|
831
|
+
if (/^📋/.test(trimmed)) {
|
|
832
|
+
O(A.b + ' ' + trimmed + A.R + '\n');
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
shown++;
|
|
836
|
+
if (shown > MAX_SHOW) { O(G(' …\n')); break; }
|
|
837
|
+
const depth = line.match(/^(\s*)/)?.[1].length ?? 0;
|
|
838
|
+
const colored = trimmed
|
|
839
|
+
.replace(/^✓\s*/, g('✓ '))
|
|
840
|
+
.replace(/^◉\s*/, y('◉ '))
|
|
841
|
+
.replace(/^○\s*/, G('○ '))
|
|
842
|
+
.replace(/^✗\s*/, r('✗ '));
|
|
843
|
+
if (depth >= 4) O(G(' ') + colored + '\n');
|
|
844
|
+
else if (depth >= 2) O(G(' ') + colored + '\n');
|
|
845
|
+
else O(' ' + colored + '\n');
|
|
846
|
+
}
|
|
847
|
+
O('\n');
|
|
848
|
+
} else {
|
|
849
|
+
O(g(' ✓') + G(` ${c(tc.name)} ${brief}\n`));
|
|
850
|
+
}
|
|
851
|
+
if (result.success) {
|
|
852
|
+
xmemory.storeWorking(`${tc.name}: ${brief}`, [tc.name, 'tool']);
|
|
853
|
+
if (tc.name === 'write_file' || tc.name === 'edit_file' || tc.name === 'delete_file') {
|
|
854
|
+
xmemory.storeEpisodic(`修改了文件: ${brief}`, ['file', 'modified'], 6);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return { role: 'tool', content: text, tool_call_id: tc.id };
|
|
859
|
+
} catch (e: unknown) {
|
|
860
|
+
stopToolAnim();
|
|
861
|
+
const em = e instanceof Error ? e.message : String(e);
|
|
862
|
+
Oflush(); O('\r' + ' '.repeat(process.stdout.columns || 80) + '\r');
|
|
863
|
+
O(r(' ✗') + G(` ${tc.name} ${em.slice(0, 60)}\n`));
|
|
864
|
+
return { role: 'tool', content: `Error: ${em}`, tool_call_id: tc.id };
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// ========== Helpers ==========
|
|
869
|
+
|
|
870
|
+
function buildMsgs(history: Message[]): Array<Record<string, unknown>> {
|
|
871
|
+
const r: Array<Record<string, unknown>> = [];
|
|
872
|
+
let sysContent = `You are DeeperCode V4-Pro, an elite full-stack AI coding agent with autonomous execution capability.
|
|
873
|
+
|
|
874
|
+
## Core Principles
|
|
875
|
+
1. READ BEFORE WRITE — Always inspect project state before making changes.
|
|
876
|
+
2. COMPLETE CODE — Write full files. Never use placeholders like "// ..." or "rest of code".
|
|
877
|
+
3. ONE CALL = ONE FILE — Each write_file contains exactly one complete file. Batch independent writes.
|
|
878
|
+
4. PLAN WITH TODOS — Use todo_manager for multi-step tasks. Update status as you progress.
|
|
879
|
+
5. ACT OVER EXPLAIN — Keep reasoning brief. Prefer tool calls over lengthy explanations.
|
|
880
|
+
6. VERIFY AFTER WRITE — After writing code, run type checks, linters, or tests to confirm correctness.
|
|
881
|
+
7. FIX ERRORS PROACTIVELY — If a tool returns an error, diagnose and fix it immediately.
|
|
882
|
+
8. PARALLEL WHEN POSSIBLE — Execute independent tool calls simultaneously for efficiency.
|
|
883
|
+
|
|
884
|
+
## Smart Editing Strategy
|
|
885
|
+
- For EXISTING files, prefer edit_file over write_file to minimize diff size.
|
|
886
|
+
- For NEW files, use write_file with complete content.
|
|
887
|
+
- Before editing, read the file first to understand its current state.
|
|
888
|
+
- Make targeted, minimal edits rather than rewriting entire files.
|
|
889
|
+
|
|
890
|
+
## Auto-Verification Loop
|
|
891
|
+
After writing or editing code files, ALWAYS verify the changes:
|
|
892
|
+
1. Run the project's build command (npm run build, tsc --noEmit, cargo check, etc.)
|
|
893
|
+
2. If build fails, read the error output, fix the issues, and re-verify
|
|
894
|
+
3. If tests exist, run them to confirm nothing is broken
|
|
895
|
+
4. Repeat until all checks pass — do NOT leave the user with broken code
|
|
896
|
+
|
|
897
|
+
## Error Recovery
|
|
898
|
+
- When a tool call fails, analyze the error message carefully before retrying
|
|
899
|
+
- If the same approach fails twice, try a different strategy
|
|
900
|
+
- For type errors: read the file, understand the types, fix precisely
|
|
901
|
+
- For runtime errors: add proper error handling and logging
|
|
902
|
+
- Never give up after one failure — adapt and overcome
|
|
903
|
+
|
|
904
|
+
## Execution Strategy
|
|
905
|
+
- Start by reading relevant files and understanding the codebase structure.
|
|
906
|
+
- Break complex tasks into small, verifiable steps.
|
|
907
|
+
- After each code change, verify it compiles/passes before moving on.
|
|
908
|
+
- When stuck, try a different approach rather than repeating the same failed action.
|
|
909
|
+
- Use subagent for independent subtasks that can run in parallel.
|
|
910
|
+
|
|
911
|
+
## Context
|
|
912
|
+
- cwd=${process.cwd()} plat=${process.platform} arch=${process.arch}
|
|
913
|
+
- Respond in the same language as the user's input.`;
|
|
914
|
+
|
|
915
|
+
const ts = todoSummary(4);
|
|
916
|
+
if (ts) sysContent += '\n[Remaining Tasks]\n' + ts;
|
|
917
|
+
|
|
918
|
+
const lastU = history.filter(m => m.role === 'user').pop();
|
|
919
|
+
if (lastU?.content) {
|
|
920
|
+
const hints = xmemory.getProceduralHints(lastU.content || '', 3);
|
|
921
|
+
if (hints) sysContent += '\n' + hints;
|
|
922
|
+
}
|
|
923
|
+
const workCtx = xmemory.getWorkingContext(400);
|
|
924
|
+
if (workCtx) sysContent += '\n[Recent Work]\n' + workCtx;
|
|
925
|
+
|
|
926
|
+
const skillPrompt = getSkillSystemPrompt();
|
|
927
|
+
if (skillPrompt) sysContent += '\n' + skillPrompt;
|
|
928
|
+
|
|
929
|
+
r.push({ role: 'system', content: sysContent });
|
|
930
|
+
|
|
931
|
+
const deeperCtx = cachedRead(join(process.cwd(), 'deeper.md'));
|
|
932
|
+
if (deeperCtx && deeperCtx.trim() && !deeperCtx.includes('<!-- 填写')) {
|
|
933
|
+
r.push({ role: 'system', content: `[项目上下文 deeper.md]\n${deeperCtx}` });
|
|
934
|
+
}
|
|
935
|
+
const projRules = cachedRead(join(process.cwd(), '.deeper', 'rules.md'));
|
|
936
|
+
if (projRules && projRules.trim()) r.push({ role: 'system', content: `[项目规则 rules.md]\n${projRules}` });
|
|
937
|
+
const globalRules = cachedRead(join(DEEPER_HOME, 'rules.md'), 2000);
|
|
938
|
+
if (globalRules && globalRules.trim()) r.push({ role: 'system', content: `[全局规则]\n${globalRules}` });
|
|
939
|
+
|
|
940
|
+
for (const m of history.slice(-30)) {
|
|
941
|
+
const e: Record<string, unknown> = { role: m.role };
|
|
942
|
+
if (m.content != null) e.content = (m.content || '').slice(0, 8000);
|
|
943
|
+
if (m.reasoning_content) e.reasoning_content = (m.reasoning_content || '').slice(0, 2000);
|
|
944
|
+
if (m.tool_calls) e.tool_calls = m.tool_calls.map(t => ({
|
|
945
|
+
id: t.id, type: 'function', function: { name: t.name, arguments: JSON.stringify(t.arguments) },
|
|
946
|
+
}));
|
|
947
|
+
if (m.tool_call_id) e.tool_call_id = m.tool_call_id;
|
|
948
|
+
r.push(e);
|
|
949
|
+
}
|
|
950
|
+
return r;
|
|
951
|
+
}
|
|
952
|
+
|
|
693
953
|
function trimHistory(h: Message[], max: number) { while (h.length > max) h.shift(); }
|
|
694
954
|
|
|
695
955
|
const _fileCache = new Map<string, { mtime: number; content: string }>();
|
|
@@ -703,274 +963,304 @@ function cachedRead(path: string, maxLen = 4000): string | null {
|
|
|
703
963
|
_fileCache.set(path, { mtime: st.mtimeMs, content });
|
|
704
964
|
return content;
|
|
705
965
|
} catch { return null; }
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
function compressHistory(h: Message[]): void {
|
|
709
|
-
if (h.length <= 6) return;
|
|
710
|
-
const keep = 6;
|
|
711
|
-
const old = h.slice(0, h.length - keep);
|
|
712
|
-
const recent = h.slice(h.length - keep);
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
.
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
if (
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
});
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function compressHistory(h: Message[]): void {
|
|
969
|
+
if (h.length <= 6) return;
|
|
970
|
+
const keep = 6;
|
|
971
|
+
const old = h.slice(0, h.length - keep);
|
|
972
|
+
const recent = h.slice(h.length - keep);
|
|
973
|
+
|
|
974
|
+
const sections: string[] = [];
|
|
975
|
+
let userParts: string[] = [];
|
|
976
|
+
let assistantParts: string[] = [];
|
|
977
|
+
let toolParts: string[] = [];
|
|
978
|
+
|
|
979
|
+
const flushSection = () => {
|
|
980
|
+
if (userParts.length > 0) sections.push(`[用户] ${userParts.join(' → ')}`);
|
|
981
|
+
if (assistantParts.length > 0) sections.push(`[助手] ${assistantParts.join(' → ')}`);
|
|
982
|
+
if (toolParts.length > 0) sections.push(`[工具] ${toolParts.join(', ')}`);
|
|
983
|
+
userParts = []; assistantParts = []; toolParts = [];
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
for (const m of old) {
|
|
987
|
+
if (m.role === 'system') {
|
|
988
|
+
flushSection();
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
if (m.role === 'user' && m.content) {
|
|
992
|
+
if (assistantParts.length > 0 || toolParts.length > 0) flushSection();
|
|
993
|
+
userParts.push(m.content.slice(0, 120));
|
|
994
|
+
} else if (m.role === 'assistant') {
|
|
995
|
+
if (m.tool_calls && m.tool_calls.length > 0) {
|
|
996
|
+
const names = m.tool_calls.map(t => t.name).join(', ');
|
|
997
|
+
toolParts.push(names);
|
|
998
|
+
}
|
|
999
|
+
if (m.content) {
|
|
1000
|
+
assistantParts.push(m.content.slice(0, 150));
|
|
1001
|
+
}
|
|
1002
|
+
} else if (m.role === 'tool' && m.content) {
|
|
1003
|
+
const isErr = m.content.startsWith('Error:');
|
|
1004
|
+
toolParts.push(isErr ? '❌' : '✓');
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
flushSection();
|
|
1008
|
+
|
|
1009
|
+
const compressed = sections.join('\n').slice(0, 4000);
|
|
1010
|
+
h.length = 0;
|
|
1011
|
+
h.push({ role: 'system', content: `[上下文压缩·${old.length}条摘要]\n${compressed}` });
|
|
1012
|
+
h.push(...recent);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function sanitize(t: string): string {
|
|
1016
|
+
return t.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
|
|
1017
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
|
|
1018
|
+
.replace(/\n{6,}/g, '\n\n');
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function lastUser(h: Message[]): string { for (let i = h.length - 1; i >= 0; i--) if (h[i].role === 'user' && h[i].content) return h[i].content!; return ''; }
|
|
1022
|
+
|
|
1023
|
+
function computeSimpleDiff(oldText: string, newText: string, filePath: string): string {
|
|
1024
|
+
const oldLines = oldText.split('\n');
|
|
1025
|
+
const newLines = newText.split('\n');
|
|
1026
|
+
const maxLines = Math.max(oldLines.length, newLines.length);
|
|
1027
|
+
const result: string[] = [];
|
|
1028
|
+
let changeCount = 0;
|
|
1029
|
+
const contextLines = 3;
|
|
1030
|
+
|
|
1031
|
+
for (let i = 0; i < maxLines; i++) {
|
|
1032
|
+
const oldLine = oldLines[i];
|
|
1033
|
+
const newLine = newLines[i];
|
|
1034
|
+
if (oldLine !== newLine) {
|
|
1035
|
+
changeCount++;
|
|
1036
|
+
const start = Math.max(0, i - contextLines);
|
|
1037
|
+
const end = Math.min(maxLines, i + contextLines + 1);
|
|
1038
|
+
if (result.length === 0 || result[result.length - 1] !== '...') {
|
|
1039
|
+
result.push(G(` @@ line ${i + 1} @@`));
|
|
1040
|
+
}
|
|
1041
|
+
for (let j = start; j < end; j++) {
|
|
1042
|
+
if (j === i) {
|
|
1043
|
+
if (oldLine !== undefined && newLine !== undefined) {
|
|
1044
|
+
result.push(r(` - ${oldLines[j]}`));
|
|
1045
|
+
result.push(g(` + ${newLines[j]}`));
|
|
1046
|
+
} else if (oldLine !== undefined) {
|
|
1047
|
+
result.push(r(` - ${oldLines[j]}`));
|
|
1048
|
+
} else {
|
|
1049
|
+
result.push(g(` + ${newLines[j]}`));
|
|
1050
|
+
}
|
|
1051
|
+
} else if (j < oldLines.length && j < newLines.length) {
|
|
1052
|
+
result.push(G(` ${oldLines[j]}`));
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
result.push(G(' ...'));
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (changeCount === 0) return G(' (无变更)');
|
|
1060
|
+
const rel = relative(process.cwd(), filePath);
|
|
1061
|
+
return `${G(`文件: ${rel} (${changeCount} 处变更)`)}\n${result.join('\n')}`;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function toolsToDefs(tools: Tool[]): ToolDef[] {
|
|
1065
|
+
return tools.map(t => ({ type: 'function' as const, function: { name: t.name, description: t.description, parameters: t.parameters as unknown as Record<string, unknown> } }));
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// ========== Session commands ==========
|
|
1069
|
+
|
|
1070
|
+
async function saveSession(history: Message[], name?: string) {
|
|
1071
|
+
const label = name || new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
1072
|
+
const file = join(SESSION_DIR, `sess_${label}.json`);
|
|
1073
|
+
writeFileSync(file, JSON.stringify({ savedAt: new Date().toISOString(), cwd: process.cwd(), messages: history }, null, 2), 'utf-8');
|
|
1074
|
+
O(g(`已保存: sess_${label}`) + '\n\n');
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
async function listSessions() {
|
|
1078
|
+
if (!existsSync(SESSION_DIR)) { O(r('无保存的会话\n\n')); return; }
|
|
1079
|
+
const files = readdirSync(SESSION_DIR).filter(f => f.endsWith('.json') && !f.startsWith('_')).sort().reverse();
|
|
1080
|
+
if (!files.length) { O(r('无保存的会话\n\n')); return; }
|
|
1081
|
+
O(b(c(' Sessions')) + G(` · ${files.length} 个`) + '\n');
|
|
1082
|
+
for (let i = 0; i < Math.min(files.length, 15); i++) {
|
|
1083
|
+
const f = files[i];
|
|
1084
|
+
try {
|
|
1085
|
+
const data = JSON.parse(readFileSync(join(SESSION_DIR, f), 'utf-8'));
|
|
1086
|
+
const label = f.replace(/^sess_|\.json$/g, '');
|
|
1087
|
+
const msgCount = data.messages?.length || 0;
|
|
1088
|
+
const savedAt = data.savedAt?.slice(0, 16) || '?';
|
|
1089
|
+
O(G(` ${i + 1}. `) + c(label) + G(` · ${msgCount}条 · ${savedAt}`) + '\n');
|
|
1090
|
+
} catch { O(G(` ${i + 1}. ${f} (损坏)\n`)); }
|
|
1091
|
+
}
|
|
1092
|
+
O('\n 加载: /load <name> | 恢复: /resume\n\n');
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
async function loadNamedSession(history: Message[], name: string) {
|
|
1096
|
+
const file = join(SESSION_DIR, `sess_${name}.json`);
|
|
1097
|
+
if (!existsSync(file)) { O(r(`会话不存在: ${name}\n\n`)); return; }
|
|
1098
|
+
const data = JSON.parse(readFileSync(file, 'utf-8'));
|
|
1099
|
+
history.push({ role: 'system', content: `[已加载: ${name} (${data.messages?.length || 0} 条)]` });
|
|
1100
|
+
if (data.messages) for (const m of data.messages) history.push(m);
|
|
1101
|
+
trimHistory(history, MAX_HISTORY);
|
|
1102
|
+
O(g(`已加载: ${name}`) + '\n\n');
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
async function loadLatestSession(history: Message[]) {
|
|
1106
|
+
if (!existsSync(SESSION_DIR)) { O(r('无保存的会话\n\n')); return; }
|
|
1107
|
+
const files = readdirSync(SESSION_DIR).filter(f => f.endsWith('.json') && !f.startsWith('_')).sort().reverse();
|
|
1108
|
+
if (!files.length) { O(r('无保存的会话\n\n')); return; }
|
|
1109
|
+
const file = join(SESSION_DIR, files[0]);
|
|
1110
|
+
const data = JSON.parse(readFileSync(file, 'utf-8'));
|
|
1111
|
+
const label = files[0].replace(/^sess_|\.json$/g, '');
|
|
1112
|
+
history.push({ role: 'system', content: `[已加载: ${label} (${data.messages?.length || 0} 条)]` });
|
|
1113
|
+
if (data.messages) for (const m of data.messages) history.push(m);
|
|
1114
|
+
trimHistory(history, MAX_HISTORY);
|
|
1115
|
+
O(g(`已加载: ${label}`) + '\n\n');
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
async function exportHistory(history: Message[]) {
|
|
1119
|
+
const file = join(process.cwd(), `deeper-export-${Date.now()}.json`);
|
|
1120
|
+
writeFileSync(file, JSON.stringify(history, null, 2), 'utf-8');
|
|
1121
|
+
O(g(`已导出: ${file}`) + '\n\n');
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
async function initProject(): Promise<void> {
|
|
1125
|
+
const file = join(process.cwd(), 'deeper.md');
|
|
1126
|
+
if (existsSync(file)) {
|
|
1127
|
+
O(y(' deeper.md 已存在,跳过\n\n'));
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
const content = `# DeeperCode 项目上下文
|
|
1131
|
+
|
|
1132
|
+
> 此文件由 \`/init\` 自动生成,AI 会在每次对话中自动读取。
|
|
1133
|
+
> 你可以手动编辑,添加项目规则、约定和背景信息。
|
|
1134
|
+
|
|
1135
|
+
## 📁 项目名称
|
|
1136
|
+
|
|
1137
|
+
<!-- 填写项目名称 -->
|
|
1138
|
+
|
|
1139
|
+
## 🎯 项目目标
|
|
1140
|
+
|
|
1141
|
+
<!-- 简要描述项目目的 -->
|
|
1142
|
+
|
|
1143
|
+
## 🛠 技术栈
|
|
1144
|
+
|
|
1145
|
+
<!-- 例如: React + TypeScript + Vite -->
|
|
1146
|
+
|
|
1147
|
+
## 📐 代码规范
|
|
1148
|
+
|
|
1149
|
+
<!-- 命名约定、文件组织等 -->
|
|
1150
|
+
|
|
1151
|
+
## ⚙️ 常用命令
|
|
1152
|
+
|
|
1153
|
+
<!-- npm run dev, npm test 等 -->
|
|
1154
|
+
|
|
1155
|
+
## 📝 注意事项
|
|
1156
|
+
|
|
1157
|
+
<!-- AI 需要注意的任何特殊要求 -->
|
|
1158
|
+
|
|
1159
|
+
---
|
|
1160
|
+
|
|
1161
|
+
*最后更新: ${new Date().toISOString()}*
|
|
1162
|
+
`;
|
|
1163
|
+
writeFileSync(file, content, 'utf-8');
|
|
1164
|
+
O(g('已创建: deeper.md') + G(' (可编辑后 AI 自动读取)') + '\n\n');
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// ========== Display commands ==========
|
|
1168
|
+
|
|
1169
|
+
async function showTasks(): Promise<void> {
|
|
1170
|
+
const items = getTodos();
|
|
1171
|
+
if (items.length === 0) { O(G(' 任务列表为空\n\n')); return; }
|
|
1172
|
+
let total = 0, done = 0;
|
|
1173
|
+
for (const t of items) {
|
|
1174
|
+
total++; if (t.status === 'done') done++;
|
|
1175
|
+
if (t.subtasks) for (const st of t.subtasks) { total++; if (st.status === 'done') done++; }
|
|
1176
|
+
}
|
|
1177
|
+
O(b(c(' Tasks')) + G(` · ${done}/${total}`) + '\n');
|
|
1178
|
+
let shown = 0; const MAX = 15;
|
|
1179
|
+
for (const t of items) {
|
|
1180
|
+
if (++shown > MAX) { O(G(' …\n')); break; }
|
|
1181
|
+
const s = t.status === 'done' ? g('✓') : t.status === 'in_progress' ? y('◉') : t.status === 'cancelled' ? r('✗') : G('○');
|
|
1182
|
+
const plan = t.plan ? G(` → ${t.plan.slice(0, 30)}`) : '';
|
|
1183
|
+
O(` ${s} ${t.title.slice(0, 50)}${plan}\n`);
|
|
1184
|
+
if (t.subtasks) for (const st of t.subtasks) {
|
|
1185
|
+
const ss = st.status === 'done' ? g(' ✓') : G(' ○');
|
|
1186
|
+
O(` ${ss} ${st.title.slice(0, 40)}\n`);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
O('\n');
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
async function showMemory(): Promise<void> {
|
|
1193
|
+
const total = xmemory.totalEntries;
|
|
1194
|
+
const types = {
|
|
1195
|
+
semantic: xmemory.getByType('semantic', 3),
|
|
1196
|
+
procedural: xmemory.getByType('procedural', 3),
|
|
1197
|
+
episodic: xmemory.getByType('episodic', 3),
|
|
1198
|
+
working: xmemory.getWorking().slice(-3),
|
|
1199
|
+
};
|
|
1200
|
+
O(b(c(' XMemory')) + G(` · ${total} 条记忆`) + '\n');
|
|
1201
|
+
for (const [t, entries] of Object.entries(types)) {
|
|
1202
|
+
if (entries.length === 0) continue;
|
|
1203
|
+
const label = t === 'semantic' ? '知识' : t === 'procedural' ? '技能' : t === 'episodic' ? '经历' : '工作';
|
|
1204
|
+
O(G(` [${label}]\n`));
|
|
1205
|
+
for (const e of entries) { O(G(` ${e.content.slice(0, 120)}`) + '\n'); }
|
|
1206
|
+
}
|
|
1207
|
+
O('\n');
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
async function showToolsBrief(tools: Tool[]): Promise<void> {
|
|
1211
|
+
const cats = [...new Set(tools.map(t => t.category))];
|
|
1212
|
+
for (const cat of cats.slice(0, 8)) {
|
|
1213
|
+
const ns = tools.filter(t => t.category === cat).map(t => t.name);
|
|
1214
|
+
O(B(`${cat}: `) + ns.join(', ') + '\n');
|
|
1215
|
+
}
|
|
1216
|
+
O(c(`\n /tools <category> 查看分类详情 (共 ${cats.length} 个分类)\n\n`));
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
async function showToolsOf(cat: string, tools: Tool[]): Promise<void> {
|
|
1220
|
+
const filtered = tools.filter(t => t.category === cat);
|
|
1221
|
+
if (!filtered.length) { O(r(`分类不存在: ${cat}\n\n`)); return; }
|
|
1222
|
+
O(b(c(` ${cat}`)) + '\n');
|
|
1223
|
+
for (const t of filtered) {
|
|
1224
|
+
const safe = TOOL_SAFETY_MAP[t.name] === 'dangerous' ? r('⚠') : TOOL_SAFETY_MAP[t.name] === 'confirm' ? y('?') : g('✓');
|
|
1225
|
+
O(` ${safe} ${c(t.name)} ${G(t.description.slice(0, 50))}\n`);
|
|
1226
|
+
}
|
|
1227
|
+
O('\n');
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
async function loadBuiltinTools(): Promise<Tool[]> {
|
|
1231
|
+
const { builtinTools } = await import('../tools/builtin/index.js');
|
|
1232
|
+
return builtinTools as Tool[];
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// ========== API (uses DeepSeekClient, no double retry) ==========
|
|
1236
|
+
|
|
1237
|
+
async function callApi(
|
|
1238
|
+
opts: ReplOptions, msgs: Array<Record<string, unknown>>,
|
|
1239
|
+
tools: ToolDef[], cmt = 8192, signal?: AbortSignal,
|
|
1240
|
+
): Promise<AsyncIterable<StreamChunk>> {
|
|
1241
|
+
const client = new DeepSeekClient({
|
|
1242
|
+
apiKey: opts.apiKey || '',
|
|
1243
|
+
model: opts.model || 'deepseek-chat',
|
|
1244
|
+
baseUrl: opts.baseUrl || 'https://api.deepseek.com',
|
|
1245
|
+
temperature: opts.temperature ?? 0,
|
|
1246
|
+
maxTokens: cmt,
|
|
1247
|
+
think: { enabled: true, budgetTokens: cmt },
|
|
1248
|
+
signal,
|
|
1249
|
+
});
|
|
1250
|
+
const chatMsgs: ChatMessage[] = msgs.map(m => ({
|
|
1251
|
+
role: (m.role as ChatMessage['role']) || 'user',
|
|
1252
|
+
content: m.content != null ? String(m.content) : null,
|
|
1253
|
+
tool_calls: m.tool_calls as ChatMessage['tool_calls'],
|
|
1254
|
+
tool_call_id: m.tool_call_id as string | undefined,
|
|
1255
|
+
name: m.name as string | undefined,
|
|
1256
|
+
reasoning_content: m.reasoning_content as string | undefined,
|
|
1257
|
+
thinking: m.thinking as string | undefined,
|
|
1258
|
+
}));
|
|
1259
|
+
const stream = await client.chatStream(chatMsgs, tools.map(t => ({
|
|
1260
|
+
name: t.function.name,
|
|
1261
|
+
description: t.function.description,
|
|
1262
|
+
category: '',
|
|
1263
|
+
parameters: t.function.parameters as unknown as import('../tools/tool-types.js').JSONSchema,
|
|
1264
|
+
})));
|
|
1265
|
+
return stream;
|
|
1266
|
+
}
|