@ww_nero/mini-cli 1.0.56
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/bin/mini.js +3 -0
- package/package.json +38 -0
- package/src/chat.js +1008 -0
- package/src/config.js +371 -0
- package/src/index.js +38 -0
- package/src/llm.js +147 -0
- package/src/prompt/tool.js +18 -0
- package/src/request.js +328 -0
- package/src/tools/bash.js +241 -0
- package/src/tools/convert.js +297 -0
- package/src/tools/index.js +66 -0
- package/src/tools/mcp.js +478 -0
- package/src/tools/python/html_to_png.py +100 -0
- package/src/tools/python/html_to_pptx.py +163 -0
- package/src/tools/python/pdf_to_png.py +58 -0
- package/src/tools/python/pptx_to_pdf.py +107 -0
- package/src/tools/read.js +44 -0
- package/src/tools/replace.js +135 -0
- package/src/tools/todos.js +90 -0
- package/src/tools/write.js +52 -0
- package/src/utils/cliOptions.js +8 -0
- package/src/utils/commands.js +89 -0
- package/src/utils/git.js +89 -0
- package/src/utils/helpers.js +93 -0
- package/src/utils/history.js +181 -0
- package/src/utils/model.js +127 -0
- package/src/utils/output.js +76 -0
- package/src/utils/renderer.js +92 -0
- package/src/utils/settings.js +90 -0
- package/src/utils/think.js +211 -0
package/src/chat.js
ADDED
|
@@ -0,0 +1,1008 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const { encode } = require('gpt-tokenizer');
|
|
7
|
+
|
|
8
|
+
const { ConfigError, loadEndpointConfig, getDefaultConfigPath, ensureConfigFiles, loadSettings, DEFAULT_COMPACT_TOKEN_THRESHOLD } = require('./config');
|
|
9
|
+
const { chatCompletion } = require('./llm');
|
|
10
|
+
const { performCompactSummary } = require('./request');
|
|
11
|
+
const { toolSystemPrompt } = require('./prompt/tool');
|
|
12
|
+
const { createToolRuntime } = require('./tools');
|
|
13
|
+
const { renderToolCall } = require('./utils/renderer');
|
|
14
|
+
const { safeJsonParse } = require('./utils/helpers');
|
|
15
|
+
const { describeModel, createModelManager } = require('./utils/model');
|
|
16
|
+
const { SLASH_COMMANDS, createCommandHandler } = require('./utils/commands');
|
|
17
|
+
const { CLI_OPTIONS } = require('./utils/cliOptions');
|
|
18
|
+
const { ensureGitRepository } = require('./utils/git');
|
|
19
|
+
const {
|
|
20
|
+
createThinkParserState,
|
|
21
|
+
processThinkChunk,
|
|
22
|
+
flushThinkState
|
|
23
|
+
} = require('./utils/think');
|
|
24
|
+
const {
|
|
25
|
+
mergeReasoningContent,
|
|
26
|
+
renderAssistantOutput,
|
|
27
|
+
sanitizeContentWithThink,
|
|
28
|
+
printUsageTokens
|
|
29
|
+
} = require('./utils/output');
|
|
30
|
+
const {
|
|
31
|
+
createHistoryFilePath,
|
|
32
|
+
saveHistoryMessages,
|
|
33
|
+
listHistoryFiles,
|
|
34
|
+
trimHistoryFiles,
|
|
35
|
+
readHistoryMessages,
|
|
36
|
+
extractFirstUserQuestion,
|
|
37
|
+
formatPreviewText,
|
|
38
|
+
refreshHistoryFilePath
|
|
39
|
+
} = require('./utils/history');
|
|
40
|
+
|
|
41
|
+
const CURSOR_STYLE_CODES = {
|
|
42
|
+
default: '\u001B[0 q',
|
|
43
|
+
loading: '\u001B[5 q'
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const MINI_FILE_NAME = 'MINI.md';
|
|
47
|
+
const MINI_DIR_NAME = '.mini';
|
|
48
|
+
|
|
49
|
+
const readMiniFileContent = (filePath) => {
|
|
50
|
+
try {
|
|
51
|
+
if (fs.existsSync(filePath)) {
|
|
52
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
53
|
+
return content.trim();
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.log(chalk.yellow(`读取 ${filePath} 失败:${error.message}`));
|
|
57
|
+
}
|
|
58
|
+
return '';
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const appendMiniInstructions = (baseContent, workspaceRoot) => {
|
|
62
|
+
const sections = [];
|
|
63
|
+
const globalMiniPath = path.join(os.homedir(), MINI_DIR_NAME, MINI_FILE_NAME);
|
|
64
|
+
const workspaceMiniPath = path.join(workspaceRoot, MINI_FILE_NAME);
|
|
65
|
+
|
|
66
|
+
const globalContent = readMiniFileContent(globalMiniPath);
|
|
67
|
+
if (globalContent) {
|
|
68
|
+
sections.push(`<system_instruction>\n${globalContent}\n</system_instruction>`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const workspaceContent = readMiniFileContent(workspaceMiniPath);
|
|
72
|
+
if (workspaceContent) {
|
|
73
|
+
sections.push(`<project_instruction>\n${workspaceContent}\n</project_instruction>`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (sections.length === 0) {
|
|
77
|
+
return baseContent;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return `${baseContent}\n\n${sections.join('\n\n')}`;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const stringifyToolResult = (value) => {
|
|
84
|
+
if (typeof value === 'string') {
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
return JSON.stringify(value);
|
|
89
|
+
} catch (_) {
|
|
90
|
+
return String(value);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const enforceToolOutputLimit = (text, tokenLimit) => {
|
|
95
|
+
const content = typeof text === 'string' ? text : stringifyToolResult(text);
|
|
96
|
+
const safeContent = typeof content === 'string' ? content : '';
|
|
97
|
+
try {
|
|
98
|
+
const tokens = encode(safeContent);
|
|
99
|
+
const tokenCount = tokens.length;
|
|
100
|
+
if (tokenCount > tokenLimit) {
|
|
101
|
+
return {
|
|
102
|
+
content: '输出内容太长,请优化工具调用方式。',
|
|
103
|
+
truncated: true,
|
|
104
|
+
originalTokenCount: tokenCount,
|
|
105
|
+
limit: tokenLimit
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
content: safeContent,
|
|
110
|
+
truncated: false,
|
|
111
|
+
tokenCount
|
|
112
|
+
};
|
|
113
|
+
} catch (_) {
|
|
114
|
+
// 忽略分词异常,保持原始输出
|
|
115
|
+
return {
|
|
116
|
+
content: safeContent,
|
|
117
|
+
truncated: false,
|
|
118
|
+
tokenCount: null
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const truncateForDisplay = (text, maxLength = 100) => {
|
|
124
|
+
if (!text || typeof text !== 'string') {
|
|
125
|
+
return '';
|
|
126
|
+
}
|
|
127
|
+
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
128
|
+
if (normalized.length <= maxLength) {
|
|
129
|
+
return normalized;
|
|
130
|
+
}
|
|
131
|
+
return normalized.substring(0, maxLength) + '...';
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const supportsCursorControl = () => Boolean(process.stdout && process.stdout.isTTY);
|
|
135
|
+
|
|
136
|
+
let loadingCursorActive = false;
|
|
137
|
+
let cursorCleanupRegistered = false;
|
|
138
|
+
|
|
139
|
+
const applyCursorStyleCode = (code) => {
|
|
140
|
+
if (!supportsCursorControl()) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
process.stdout.write(code);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const restoreCursorStyle = () => {
|
|
147
|
+
if (!supportsCursorControl()) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
applyCursorStyleCode(CURSOR_STYLE_CODES.default);
|
|
151
|
+
loadingCursorActive = false;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const setLoadingCursorState = (isLoading) => {
|
|
155
|
+
if (!supportsCursorControl()) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (isLoading) {
|
|
159
|
+
if (loadingCursorActive) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
loadingCursorActive = true;
|
|
163
|
+
applyCursorStyleCode(CURSOR_STYLE_CODES.loading);
|
|
164
|
+
if (!cursorCleanupRegistered) {
|
|
165
|
+
cursorCleanupRegistered = true;
|
|
166
|
+
process.once('exit', restoreCursorStyle);
|
|
167
|
+
}
|
|
168
|
+
} else if (loadingCursorActive) {
|
|
169
|
+
restoreCursorStyle();
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const startChatSession = async ({
|
|
174
|
+
initialQuestion = '',
|
|
175
|
+
initialModelName,
|
|
176
|
+
cliOptions = []
|
|
177
|
+
} = {}) => {
|
|
178
|
+
const initResult = ensureConfigFiles();
|
|
179
|
+
if (initResult.createdFiles && initResult.createdFiles.length > 0) {
|
|
180
|
+
console.log(chalk.yellow(`已在 ${initResult.configDir} 创建配置文件:${initResult.createdFiles.join(', ')},请完善模型 key 等信息后重试。`));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let endpoints = [];
|
|
184
|
+
let resolvedPath;
|
|
185
|
+
try {
|
|
186
|
+
const loaded = loadEndpointConfig();
|
|
187
|
+
endpoints = loaded.endpoints;
|
|
188
|
+
resolvedPath = loaded.configPath;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (error instanceof ConfigError) {
|
|
191
|
+
console.error(chalk.red(error.message));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const modelManager = createModelManager(endpoints, { configPath: resolvedPath || getDefaultConfigPath() });
|
|
198
|
+
modelManager.setActiveModel(0, { updateDefault: true, announce: false, persistConfig: false });
|
|
199
|
+
|
|
200
|
+
if (initialModelName) {
|
|
201
|
+
modelManager.applyModelByName(initialModelName, { source: 'cli', announceSuccess: true, showError: true });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const workspaceRoot = process.cwd();
|
|
205
|
+
const { gitInitialized, gitignoreCreated, initialCommitCreated } = ensureGitRepository(workspaceRoot);
|
|
206
|
+
if (gitInitialized) {
|
|
207
|
+
console.log(chalk.gray('已自动初始化 Git 仓库。'));
|
|
208
|
+
}
|
|
209
|
+
if (gitignoreCreated) {
|
|
210
|
+
console.log(chalk.gray('已创建默认 .gitignore。'));
|
|
211
|
+
}
|
|
212
|
+
if (initialCommitCreated) {
|
|
213
|
+
console.log(chalk.gray('已创建初始提交:init project。'));
|
|
214
|
+
}
|
|
215
|
+
const systemPromptContent = appendMiniInstructions(toolSystemPrompt, workspaceRoot);
|
|
216
|
+
|
|
217
|
+
// Load settings to get toolOutputTokenLimit and compactTokenThreshold
|
|
218
|
+
const { settings } = loadSettings();
|
|
219
|
+
const toolOutputTokenLimit = settings.toolOutputTokenLimit;
|
|
220
|
+
const compactTokenThreshold = settings.compactTokenThreshold || DEFAULT_COMPACT_TOKEN_THRESHOLD;
|
|
221
|
+
|
|
222
|
+
let toolRuntime;
|
|
223
|
+
try {
|
|
224
|
+
toolRuntime = await createToolRuntime(workspaceRoot);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.log(chalk.yellow(`加载工具失败,将仅使用基础对话:${error.message}`));
|
|
227
|
+
toolRuntime = {
|
|
228
|
+
tools: [],
|
|
229
|
+
handlers: {},
|
|
230
|
+
dispose: null,
|
|
231
|
+
mcpConfigPath: null,
|
|
232
|
+
mcpToolNames: new Set(),
|
|
233
|
+
enabledToolNames: [],
|
|
234
|
+
enabledMcpNames: []
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const {
|
|
238
|
+
tools: toolSchemas,
|
|
239
|
+
handlers: toolHandlers,
|
|
240
|
+
dispose: disposeTools,
|
|
241
|
+
mcpConfigPath,
|
|
242
|
+
mcpToolNames,
|
|
243
|
+
enabledToolNames,
|
|
244
|
+
enabledMcpNames
|
|
245
|
+
} = toolRuntime || {
|
|
246
|
+
tools: [],
|
|
247
|
+
handlers: {},
|
|
248
|
+
dispose: null,
|
|
249
|
+
mcpConfigPath: null,
|
|
250
|
+
mcpToolNames: new Set(),
|
|
251
|
+
enabledToolNames: [],
|
|
252
|
+
enabledMcpNames: []
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const createSystemMessage = () => ({ role: 'system', content: systemPromptContent });
|
|
256
|
+
const messages = [createSystemMessage()];
|
|
257
|
+
let activeHistoryFilePath = null;
|
|
258
|
+
|
|
259
|
+
const resetHistorySession = () => {
|
|
260
|
+
activeHistoryFilePath = null;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const resetMessages = () => {
|
|
264
|
+
messages.length = 0;
|
|
265
|
+
messages.push(createSystemMessage());
|
|
266
|
+
resetHistorySession();
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const ensureHistoryFileReady = () => {
|
|
270
|
+
if (activeHistoryFilePath) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
const firstQuestion = extractFirstUserQuestion(messages);
|
|
274
|
+
const hasAssistantMessage = messages.some((entry) => entry && entry.role === 'assistant');
|
|
275
|
+
if (!firstQuestion || !hasAssistantMessage) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
activeHistoryFilePath = createHistoryFilePath(workspaceRoot);
|
|
280
|
+
return true;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
activeHistoryFilePath = null;
|
|
283
|
+
console.log(chalk.yellow(`创建历史记录失败:${error.message}`));
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const persistHistorySafely = () => {
|
|
289
|
+
if (!ensureHistoryFileReady()) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
const saved = saveHistoryMessages(activeHistoryFilePath, messages);
|
|
294
|
+
if (saved) {
|
|
295
|
+
const removed = trimHistoryFiles(undefined, workspaceRoot);
|
|
296
|
+
if (Array.isArray(removed) && removed.includes(activeHistoryFilePath)) {
|
|
297
|
+
activeHistoryFilePath = null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.log(chalk.yellow(`保存历史记录失败:${error.message}`));
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const loadHistoryFromFile = (filePath, historyMessages) => {
|
|
306
|
+
messages.length = 0;
|
|
307
|
+
if (Array.isArray(historyMessages) && historyMessages.length > 0) {
|
|
308
|
+
historyMessages.forEach((entry) => {
|
|
309
|
+
if (entry && typeof entry === 'object') {
|
|
310
|
+
messages.push(entry);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (messages.length === 0) {
|
|
315
|
+
messages.push(createSystemMessage());
|
|
316
|
+
}
|
|
317
|
+
activeHistoryFilePath = filePath || null;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const replayHistoryConversation = (historyMessages) => {
|
|
321
|
+
if (!Array.isArray(historyMessages) || historyMessages.length === 0) {
|
|
322
|
+
console.log(chalk.yellow('历史记录为空,暂无内容可展示。'));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
console.log(chalk.gray('-------- 历史会话开始 --------'));
|
|
327
|
+
|
|
328
|
+
const toolMessageQueues = new Map();
|
|
329
|
+
const consumedToolMessages = new Set();
|
|
330
|
+
|
|
331
|
+
historyMessages.forEach((message) => {
|
|
332
|
+
if (message && message.role === 'tool' && message.tool_call_id) {
|
|
333
|
+
const queue = toolMessageQueues.get(message.tool_call_id) || [];
|
|
334
|
+
queue.push(message);
|
|
335
|
+
toolMessageQueues.set(message.tool_call_id, queue);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const toDisplayString = (value) => {
|
|
340
|
+
if (typeof value === 'string') {
|
|
341
|
+
return value;
|
|
342
|
+
}
|
|
343
|
+
if (value == null) {
|
|
344
|
+
return '';
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
return JSON.stringify(value, null, 2);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
return '';
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
historyMessages.forEach((message) => {
|
|
354
|
+
if (!message || typeof message !== 'object') {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (message.role === 'system') {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (message.role === 'user') {
|
|
363
|
+
const content = typeof message.content === 'string' ? message.content : '';
|
|
364
|
+
console.log(`${chalk.cyan('用户>')} ${content}`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (message.role === 'assistant') {
|
|
369
|
+
const ensureHistoryNewline = (() => {
|
|
370
|
+
let used = false;
|
|
371
|
+
return () => {
|
|
372
|
+
if (!used) {
|
|
373
|
+
process.stdout.write('\n');
|
|
374
|
+
used = true;
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
})();
|
|
378
|
+
|
|
379
|
+
const reasoningText = mergeReasoningContent(message.reasoning_content || '');
|
|
380
|
+
const primaryContent = typeof message.content === 'string' ? message.content : '';
|
|
381
|
+
const sanitizedContent = sanitizeContentWithThink(primaryContent, primaryContent);
|
|
382
|
+
|
|
383
|
+
renderAssistantOutput(reasoningText, sanitizedContent, ensureHistoryNewline);
|
|
384
|
+
|
|
385
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
386
|
+
toolCalls.forEach((toolCall) => {
|
|
387
|
+
const functionName = toolCall?.function?.name || 'unknown_tool';
|
|
388
|
+
const rawArgs = toolCall?.function?.arguments || '';
|
|
389
|
+
const parsedArgs = typeof rawArgs === 'string' ? safeJsonParse(rawArgs) : rawArgs;
|
|
390
|
+
const args = parsedArgs && typeof parsedArgs === 'object'
|
|
391
|
+
? parsedArgs
|
|
392
|
+
: { raw: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
393
|
+
|
|
394
|
+
const queue = toolMessageQueues.get(toolCall.id) || [];
|
|
395
|
+
const toolMessage = queue.shift();
|
|
396
|
+
if (queue.length === 0) {
|
|
397
|
+
toolMessageQueues.delete(toolCall.id);
|
|
398
|
+
} else {
|
|
399
|
+
toolMessageQueues.set(toolCall.id, queue);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (toolMessage) {
|
|
403
|
+
consumedToolMessages.add(toolMessage);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
renderToolCall({
|
|
407
|
+
functionName,
|
|
408
|
+
args
|
|
409
|
+
}, mcpToolNames);
|
|
410
|
+
});
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (message.role === 'tool') {
|
|
415
|
+
if (consumedToolMessages.has(message)) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const fallback = toDisplayString(message.content) || '(无输出)';
|
|
419
|
+
console.log(chalk.magenta(`工具输出: ${fallback}`));
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
console.log(chalk.gray('-------- 历史会话结束,可继续提问 --------'));
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const handleResumeCommand = (rawArgs = '') => {
|
|
427
|
+
const entries = listHistoryFiles(workspaceRoot);
|
|
428
|
+
if (entries.length === 0) {
|
|
429
|
+
console.log(chalk.yellow('暂无历史记录。'));
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (!rawArgs) {
|
|
434
|
+
entries.forEach((entry, index) => {
|
|
435
|
+
const historyMessages = readHistoryMessages(entry.filePath);
|
|
436
|
+
if (!historyMessages) {
|
|
437
|
+
console.log(`${index + 1}. ${chalk.red('(记录损坏)')}`);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const previewSource = extractFirstUserQuestion(historyMessages);
|
|
441
|
+
const preview = formatPreviewText(previewSource);
|
|
442
|
+
console.log(`${index + 1}. ${preview}`);
|
|
443
|
+
});
|
|
444
|
+
console.log(chalk.gray('使用 /resume <序号> 恢复指定对话。'));
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const numeric = Number.parseInt(rawArgs, 10);
|
|
449
|
+
if (Number.isNaN(numeric)) {
|
|
450
|
+
console.log(chalk.yellow('请输入合法的序号,例如 /resume 2。'));
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const target = entries[numeric - 1];
|
|
455
|
+
if (!target) {
|
|
456
|
+
console.log(chalk.yellow(`序号 ${numeric} 超出范围,当前共有 ${entries.length} 条历史记录。`));
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const historyMessages = readHistoryMessages(target.filePath);
|
|
461
|
+
if (!Array.isArray(historyMessages) || historyMessages.length === 0) {
|
|
462
|
+
console.log(chalk.yellow('该历史记录为空或已损坏,无法恢复。'));
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const refreshedPath = refreshHistoryFilePath(target.filePath, workspaceRoot) || target.filePath;
|
|
467
|
+
loadHistoryFromFile(refreshedPath, historyMessages);
|
|
468
|
+
console.log(chalk.green(`已切换到历史记录 #${numeric},可继续对话。`));
|
|
469
|
+
replayHistoryConversation(historyMessages);
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
console.log(chalk.blueBright('Mini CLI 已启动,随时输入问题开始提问。'));
|
|
473
|
+
console.log(chalk.gray(`聊天模型: ${describeModel(modelManager.getActiveEndpoint())}`));
|
|
474
|
+
|
|
475
|
+
// 打印启用的工具列表
|
|
476
|
+
if (enabledToolNames.length > 0) {
|
|
477
|
+
console.log(chalk.gray(`启用的工具 (${enabledToolNames.length}): ${enabledToolNames.join(', ')}`));
|
|
478
|
+
} else {
|
|
479
|
+
console.log(chalk.gray('未启用任何工具'));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// 打印启用的 MCP 列表
|
|
483
|
+
if (enabledMcpNames.length > 0) {
|
|
484
|
+
console.log(chalk.gray(`启用的 MCP (${enabledMcpNames.length}): ${enabledMcpNames.join(', ')}`));
|
|
485
|
+
} else {
|
|
486
|
+
console.log(chalk.gray('未启用任何 MCP 服务器'));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const startupOptions = (Array.isArray(cliOptions) && cliOptions.length > 0) ? cliOptions : CLI_OPTIONS;
|
|
490
|
+
if (startupOptions.length > 0) {
|
|
491
|
+
console.log(chalk.gray('启动参数:'));
|
|
492
|
+
startupOptions.forEach((option) => {
|
|
493
|
+
console.log(chalk.gray(` ${option.flags} ${option.description}`));
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
console.log(chalk.gray('快捷命令:'));
|
|
498
|
+
SLASH_COMMANDS.forEach((cmd) => {
|
|
499
|
+
console.log(chalk.gray(` ${cmd.value} - ${cmd.description}`));
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const rl = readline.createInterface({
|
|
503
|
+
input: process.stdin,
|
|
504
|
+
output: process.stdout,
|
|
505
|
+
terminal: true
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
const DEFAULT_PROMPT = `${chalk.cyan('用户>')} `;
|
|
509
|
+
rl.setPrompt(DEFAULT_PROMPT);
|
|
510
|
+
|
|
511
|
+
const LOADING_PROMPT_BASE_TEXT = '⌛ 回答中...(按 Esc 取消)';
|
|
512
|
+
const FLOW_GRADIENT_COLORS = ['#FFF7CC', '#FFE58F', '#FFD666', '#FFC53D', '#FFA940', '#FF7A45'];
|
|
513
|
+
const FLOW_DIM_COLOR = '#5F5F6A';
|
|
514
|
+
const loadingPromptCharacters = Array.from(LOADING_PROMPT_BASE_TEXT);
|
|
515
|
+
const loadingPromptCharCount = loadingPromptCharacters.length || 1;
|
|
516
|
+
const supportsHexColor = typeof chalk.hex === 'function';
|
|
517
|
+
const flowGradientFns = supportsHexColor
|
|
518
|
+
? FLOW_GRADIENT_COLORS.map((hex) => chalk.hex(hex))
|
|
519
|
+
: [];
|
|
520
|
+
const dimTextFn = supportsHexColor ? chalk.hex(FLOW_DIM_COLOR) : chalk.yellow;
|
|
521
|
+
|
|
522
|
+
const formatLoadingPrompt = (frame) => {
|
|
523
|
+
if (!supportsHexColor) {
|
|
524
|
+
return chalk.yellow(LOADING_PROMPT_BASE_TEXT);
|
|
525
|
+
}
|
|
526
|
+
const gradientLength = Math.min(flowGradientFns.length, loadingPromptCharCount);
|
|
527
|
+
const startIndex = frame % loadingPromptCharCount;
|
|
528
|
+
return loadingPromptCharacters.map((char, index) => {
|
|
529
|
+
const distance = (index - startIndex + loadingPromptCharCount) % loadingPromptCharCount;
|
|
530
|
+
if (distance < gradientLength) {
|
|
531
|
+
return flowGradientFns[distance](char);
|
|
532
|
+
}
|
|
533
|
+
return dimTextFn(char);
|
|
534
|
+
}).join('');
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const LOADING_PROMPT_INTERVAL = 140;
|
|
538
|
+
let loadingPromptFrame = 0;
|
|
539
|
+
let loadingPromptTimer = null;
|
|
540
|
+
let loadingPromptVisible = false;
|
|
541
|
+
|
|
542
|
+
const getCurrentLoadingPromptText = () => formatLoadingPrompt(loadingPromptFrame);
|
|
543
|
+
|
|
544
|
+
const tickLoadingPromptAnimation = () => {
|
|
545
|
+
if (!loadingPromptVisible || !supportsHexColor) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
loadingPromptFrame = (loadingPromptFrame + 1) % loadingPromptCharCount;
|
|
549
|
+
refreshLoadingPrompt();
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const startLoadingPromptAnimation = () => {
|
|
553
|
+
if (loadingPromptTimer || !process.stdout.isTTY || !supportsHexColor) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
loadingPromptTimer = setInterval(tickLoadingPromptAnimation, LOADING_PROMPT_INTERVAL);
|
|
557
|
+
if (typeof loadingPromptTimer.unref === 'function') {
|
|
558
|
+
loadingPromptTimer.unref();
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
const stopLoadingPromptAnimation = () => {
|
|
563
|
+
if (loadingPromptTimer) {
|
|
564
|
+
clearInterval(loadingPromptTimer);
|
|
565
|
+
loadingPromptTimer = null;
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const writeLoadingPrompt = () => {
|
|
570
|
+
if (!process.stdout.isTTY) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
readline.cursorTo(process.stdout, 0);
|
|
574
|
+
readline.clearLine(process.stdout, 0);
|
|
575
|
+
process.stdout.write(getCurrentLoadingPromptText());
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
const showLoadingPrompt = () => {
|
|
579
|
+
if (loadingPromptVisible || !process.stdout.isTTY) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
loadingPromptVisible = true;
|
|
583
|
+
writeLoadingPrompt();
|
|
584
|
+
startLoadingPromptAnimation();
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const refreshLoadingPrompt = () => {
|
|
588
|
+
if (!loadingPromptVisible) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
writeLoadingPrompt();
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const clearLoadingPrompt = () => {
|
|
595
|
+
if (!loadingPromptVisible) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (process.stdout.isTTY) {
|
|
599
|
+
readline.cursorTo(process.stdout, 0);
|
|
600
|
+
readline.clearLine(process.stdout, 0);
|
|
601
|
+
}
|
|
602
|
+
stopLoadingPromptAnimation();
|
|
603
|
+
loadingPromptFrame = 0;
|
|
604
|
+
loadingPromptVisible = false;
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
let inFlightController = null;
|
|
608
|
+
let closing = false;
|
|
609
|
+
let cancelNoticeShown = false;
|
|
610
|
+
let skipNextLoopPrompt = false;
|
|
611
|
+
let toolsDisposed = false;
|
|
612
|
+
|
|
613
|
+
const cleanupTools = () => {
|
|
614
|
+
if (toolsDisposed || typeof disposeTools !== 'function') {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
toolsDisposed = true;
|
|
618
|
+
Promise.resolve(disposeTools()).catch((error) => {
|
|
619
|
+
console.log(chalk.yellow(`清理工具失败:${error.message}`));
|
|
620
|
+
});
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const closeSession = () => {
|
|
624
|
+
if (closing) return;
|
|
625
|
+
closing = true;
|
|
626
|
+
clearLoadingPrompt();
|
|
627
|
+
setLoadingCursorState(false);
|
|
628
|
+
cleanupTools();
|
|
629
|
+
rl.close();
|
|
630
|
+
console.log(chalk.gray('已退出 mini 对话。'));
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
const commandHandler = createCommandHandler({
|
|
634
|
+
modelManager,
|
|
635
|
+
resetMessages,
|
|
636
|
+
closeSession,
|
|
637
|
+
handleResume: handleResumeCommand
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
const resizeHandler = () => {
|
|
641
|
+
refreshLoadingPrompt();
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
if (process.stdout && typeof process.stdout.on === 'function') {
|
|
645
|
+
process.stdout.on('resize', resizeHandler);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const cancelActiveRequest = (message) => {
|
|
649
|
+
if (!inFlightController) {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
inFlightController.abort();
|
|
653
|
+
inFlightController = null;
|
|
654
|
+
clearLoadingPrompt();
|
|
655
|
+
setLoadingCursorState(false);
|
|
656
|
+
cancelNoticeShown = true;
|
|
657
|
+
console.log(`\n${chalk.yellow(message)}`);
|
|
658
|
+
if (!closing) {
|
|
659
|
+
rl.prompt();
|
|
660
|
+
skipNextLoopPrompt = true;
|
|
661
|
+
}
|
|
662
|
+
return true;
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
const inputStream = rl.input;
|
|
666
|
+
if (inputStream && typeof inputStream.on === 'function') {
|
|
667
|
+
readline.emitKeypressEvents(inputStream, rl);
|
|
668
|
+
const handleKeypress = (str, key = {}) => {
|
|
669
|
+
if (key && key.name === 'escape') {
|
|
670
|
+
cancelActiveRequest('已中断当前请求。');
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
inputStream.on('keypress', handleKeypress);
|
|
674
|
+
rl.on('close', () => {
|
|
675
|
+
inputStream.removeListener('keypress', handleKeypress);
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
rl.on('SIGINT', () => {
|
|
680
|
+
if (cancelActiveRequest('已取消当前请求。')) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
closeSession();
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
rl.on('close', () => {
|
|
687
|
+
if (process.stdout && typeof process.stdout.off === 'function') {
|
|
688
|
+
process.stdout.off('resize', resizeHandler);
|
|
689
|
+
} else if (process.stdout && typeof process.stdout.removeListener === 'function') {
|
|
690
|
+
process.stdout.removeListener('resize', resizeHandler);
|
|
691
|
+
}
|
|
692
|
+
cleanupTools();
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
const sendQuestion = async (rawInput) => {
|
|
696
|
+
const text = (rawInput || '').trim();
|
|
697
|
+
if (!text) {
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const slashCommandResult = commandHandler.handleSlashCommand(text);
|
|
702
|
+
if (slashCommandResult.handled) {
|
|
703
|
+
return !slashCommandResult.shouldExit;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const targetEndpoint = modelManager.getActiveEndpoint();
|
|
707
|
+
|
|
708
|
+
const userMessage = { role: 'user', content: text };
|
|
709
|
+
messages.push(userMessage);
|
|
710
|
+
persistHistorySafely();
|
|
711
|
+
|
|
712
|
+
setLoadingCursorState(true);
|
|
713
|
+
|
|
714
|
+
const controller = new AbortController();
|
|
715
|
+
inFlightController = controller;
|
|
716
|
+
cancelNoticeShown = false;
|
|
717
|
+
let conversationAdvanced = false;
|
|
718
|
+
|
|
719
|
+
const ensureNotAborted = () => {
|
|
720
|
+
if (controller.signal.aborted) {
|
|
721
|
+
const abortError = new Error('Aborted');
|
|
722
|
+
abortError.name = 'AbortError';
|
|
723
|
+
throw abortError;
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
const runAgentLoop = async (endpointForRequest) => {
|
|
728
|
+
const buildHistoryAssistantMessage = (sourceMessage, fallbackContent, reasoningText) => {
|
|
729
|
+
const baseMessage = (sourceMessage && typeof sourceMessage === 'object')
|
|
730
|
+
? sourceMessage
|
|
731
|
+
: { role: 'assistant' };
|
|
732
|
+
const historyMessage = { ...baseMessage };
|
|
733
|
+
if (typeof baseMessage.content === 'string') {
|
|
734
|
+
historyMessage.content = baseMessage.content;
|
|
735
|
+
} else if (typeof fallbackContent === 'string') {
|
|
736
|
+
historyMessage.content = fallbackContent;
|
|
737
|
+
} else {
|
|
738
|
+
historyMessage.content = '';
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (reasoningText) {
|
|
742
|
+
historyMessage.reasoning_content = reasoningText;
|
|
743
|
+
} else if ('reasoning_content' in historyMessage) {
|
|
744
|
+
delete historyMessage.reasoning_content;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return historyMessage;
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
while (true) {
|
|
751
|
+
process.stdout.write('\n');
|
|
752
|
+
showLoadingPrompt();
|
|
753
|
+
const streamState = {
|
|
754
|
+
thinkState: createThinkParserState(),
|
|
755
|
+
content: '',
|
|
756
|
+
reasoningFromThink: ''
|
|
757
|
+
};
|
|
758
|
+
let reasoningFromModel = '';
|
|
759
|
+
let printedNewline = false;
|
|
760
|
+
|
|
761
|
+
const ensureNewline = () => {
|
|
762
|
+
if (!printedNewline) {
|
|
763
|
+
clearLoadingPrompt();
|
|
764
|
+
process.stdout.write('\n');
|
|
765
|
+
printedNewline = true;
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
const handleStreamChunk = (chunk) => {
|
|
770
|
+
const { contentChunk, reasonChunk } = processThinkChunk(streamState.thinkState, chunk);
|
|
771
|
+
|
|
772
|
+
if (contentChunk) {
|
|
773
|
+
streamState.content += contentChunk;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (reasonChunk) {
|
|
777
|
+
streamState.reasoningFromThink += reasonChunk;
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
const handleReasoningChunk = (_, aggregate) => {
|
|
782
|
+
reasoningFromModel = aggregate || reasoningFromModel;
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
const finalizeStreamState = () => {
|
|
786
|
+
const { contentChunk, reasonChunk } = flushThinkState(streamState.thinkState);
|
|
787
|
+
if (contentChunk) {
|
|
788
|
+
streamState.content += contentChunk;
|
|
789
|
+
}
|
|
790
|
+
if (reasonChunk) {
|
|
791
|
+
streamState.reasoningFromThink += reasonChunk;
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
const result = await chatCompletion({
|
|
796
|
+
endpoint: endpointForRequest,
|
|
797
|
+
messages,
|
|
798
|
+
abortController: controller,
|
|
799
|
+
tools: toolSchemas,
|
|
800
|
+
onToken: handleStreamChunk,
|
|
801
|
+
onReasoningToken: handleReasoningChunk,
|
|
802
|
+
onRetry: (retryCount, error) => {
|
|
803
|
+
const detail = error ? `(${error.message})` : '';
|
|
804
|
+
process.stdout.write(`\n${chalk.yellow(`请求失败,${retryCount} 次重试中${detail}...`)}\n`);
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
finalizeStreamState();
|
|
809
|
+
|
|
810
|
+
const rawReasoning = (reasoningFromModel || result.reasoningContent || '').trim();
|
|
811
|
+
const mergedReasoning = mergeReasoningContent(
|
|
812
|
+
rawReasoning,
|
|
813
|
+
streamState.reasoningFromThink
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
const toolCalls = Array.isArray(result.toolCalls) ? result.toolCalls : [];
|
|
817
|
+
|
|
818
|
+
if (toolCalls.length > 0) {
|
|
819
|
+
conversationAdvanced = true;
|
|
820
|
+
const baseMessage = result.message || { role: 'assistant', content: result.content || '' };
|
|
821
|
+
const sanitizedContent = sanitizeContentWithThink(
|
|
822
|
+
streamState.content,
|
|
823
|
+
typeof baseMessage.content === 'string' ? baseMessage.content : result.content
|
|
824
|
+
);
|
|
825
|
+
renderAssistantOutput(mergedReasoning, sanitizedContent, ensureNewline);
|
|
826
|
+
const assistantMessage = buildHistoryAssistantMessage(
|
|
827
|
+
baseMessage,
|
|
828
|
+
result.content,
|
|
829
|
+
rawReasoning
|
|
830
|
+
);
|
|
831
|
+
messages.push(assistantMessage);
|
|
832
|
+
|
|
833
|
+
for (const toolCall of toolCalls) {
|
|
834
|
+
ensureNotAborted();
|
|
835
|
+
const functionName = toolCall?.function?.name || 'unknown_tool';
|
|
836
|
+
const rawArgs = toolCall?.function?.arguments || '';
|
|
837
|
+
const parsedArgs = safeJsonParse(rawArgs);
|
|
838
|
+
const handler = toolHandlers[functionName];
|
|
839
|
+
const displayArgs = parsedArgs && typeof parsedArgs === 'object'
|
|
840
|
+
? parsedArgs
|
|
841
|
+
: { raw: typeof rawArgs === 'string' ? rawArgs : '' };
|
|
842
|
+
|
|
843
|
+
renderToolCall({ functionName, args: displayArgs }, mcpToolNames);
|
|
844
|
+
|
|
845
|
+
let toolResult;
|
|
846
|
+
|
|
847
|
+
if (!parsedArgs || typeof parsedArgs !== 'object') {
|
|
848
|
+
toolResult = '工具调用失败:参数解析错误';
|
|
849
|
+
} else if (!handler) {
|
|
850
|
+
toolResult = `工具 ${functionName} 未实现`;
|
|
851
|
+
} else {
|
|
852
|
+
showLoadingPrompt();
|
|
853
|
+
try {
|
|
854
|
+
toolResult = await handler(parsedArgs);
|
|
855
|
+
} catch (toolError) {
|
|
856
|
+
toolResult = `工具执行异常: ${toolError.message}`;
|
|
857
|
+
} finally {
|
|
858
|
+
clearLoadingPrompt();
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const resultInfo = enforceToolOutputLimit(stringifyToolResult(toolResult), toolOutputTokenLimit);
|
|
863
|
+
const toolContent = resultInfo.content;
|
|
864
|
+
|
|
865
|
+
// Display tool output
|
|
866
|
+
if (functionName === 'write_todos') {
|
|
867
|
+
// For todos, display full formatted output without truncation
|
|
868
|
+
if (toolContent) {
|
|
869
|
+
console.log(chalk.gray(` ${toolContent}`));
|
|
870
|
+
}
|
|
871
|
+
} else {
|
|
872
|
+
// For other tools, show preview in gray
|
|
873
|
+
const preview = truncateForDisplay(toolContent, 100);
|
|
874
|
+
if (preview) {
|
|
875
|
+
console.log(chalk.gray(` ${preview}`));
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// If truncated, show warning with token count
|
|
879
|
+
if (resultInfo.truncated) {
|
|
880
|
+
console.log(chalk.yellow(` ⚠ 输出已截断 (${resultInfo.originalTokenCount} tokens > ${resultInfo.limit} limit)`));
|
|
881
|
+
} else if (resultInfo.tokenCount != null) {
|
|
882
|
+
console.log(chalk.gray(` (${resultInfo.tokenCount} tokens)`));
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
messages.push({
|
|
887
|
+
role: 'tool',
|
|
888
|
+
tool_call_id: toolCall.id,
|
|
889
|
+
content: toolContent
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
printUsageTokens(result.usage);
|
|
893
|
+
persistHistorySafely();
|
|
894
|
+
|
|
895
|
+
// 检测是否需要触发 compact
|
|
896
|
+
const totalTokens = result.usage?.total_tokens;
|
|
897
|
+
if (typeof totalTokens === 'number' && totalTokens >= compactTokenThreshold) {
|
|
898
|
+
console.log(chalk.yellow(`\n⏳ 上下文过长 (${totalTokens} tokens),正在压缩...`));
|
|
899
|
+
showLoadingPrompt();
|
|
900
|
+
|
|
901
|
+
try {
|
|
902
|
+
const summaryContent = await performCompactSummary(endpointForRequest, messages, controller);
|
|
903
|
+
|
|
904
|
+
// 提取 system prompt
|
|
905
|
+
const systemMessage = messages.find(msg => msg.role === 'system');
|
|
906
|
+
|
|
907
|
+
// 重置消息列表
|
|
908
|
+
messages.length = 0;
|
|
909
|
+
if (systemMessage) {
|
|
910
|
+
messages.push(systemMessage);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// 添加总结内容作为新的用户消息
|
|
914
|
+
messages.push({
|
|
915
|
+
role: 'user',
|
|
916
|
+
content: `<context_summary>\n${summaryContent}\n</context_summary>\n\n请根据以上总结的上下文,继续完成待完成的任务。`
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
clearLoadingPrompt();
|
|
920
|
+
console.log(chalk.green('✓ 上下文已压缩,可继续对话。'));
|
|
921
|
+
persistHistorySafely();
|
|
922
|
+
} catch (compactError) {
|
|
923
|
+
clearLoadingPrompt();
|
|
924
|
+
if (compactError.name === 'AbortError') {
|
|
925
|
+
throw compactError;
|
|
926
|
+
}
|
|
927
|
+
console.log(chalk.yellow('⚠ 上下文压缩失败,将继续使用当前上下文'));
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
conversationAdvanced = true;
|
|
935
|
+
const finalText = sanitizeContentWithThink(streamState.content, result.content);
|
|
936
|
+
renderAssistantOutput(mergedReasoning, finalText, ensureNewline);
|
|
937
|
+
const assistantMessage = buildHistoryAssistantMessage(
|
|
938
|
+
result.message,
|
|
939
|
+
result.content,
|
|
940
|
+
rawReasoning
|
|
941
|
+
);
|
|
942
|
+
messages.push(assistantMessage);
|
|
943
|
+
persistHistorySafely();
|
|
944
|
+
printUsageTokens(result.usage);
|
|
945
|
+
break;
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
try {
|
|
950
|
+
await runAgentLoop(targetEndpoint);
|
|
951
|
+
} catch (error) {
|
|
952
|
+
const isAbortError = error.name === 'AbortError';
|
|
953
|
+
if (!isAbortError) {
|
|
954
|
+
process.stdout.write('\n');
|
|
955
|
+
}
|
|
956
|
+
if (!conversationAdvanced) {
|
|
957
|
+
const lastMessage = messages[messages.length - 1];
|
|
958
|
+
if (lastMessage && lastMessage.role === 'user') {
|
|
959
|
+
messages.pop();
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (isAbortError) {
|
|
963
|
+
if (!cancelNoticeShown) {
|
|
964
|
+
console.log(chalk.yellow('对话已取消。'));
|
|
965
|
+
}
|
|
966
|
+
} else {
|
|
967
|
+
console.error(chalk.red(`请求失败: ${error.message}`));
|
|
968
|
+
}
|
|
969
|
+
} finally {
|
|
970
|
+
clearLoadingPrompt();
|
|
971
|
+
setLoadingCursorState(false);
|
|
972
|
+
inFlightController = null;
|
|
973
|
+
cancelNoticeShown = false;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
return true;
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
if (typeof initialQuestion === 'string' && initialQuestion.trim()) {
|
|
980
|
+
console.log(`${chalk.cyan('用户>')}${initialQuestion.trim()}`);
|
|
981
|
+
const shouldContinue = await sendQuestion(initialQuestion);
|
|
982
|
+
if (!shouldContinue) {
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
rl.prompt();
|
|
988
|
+
for await (const input of rl) {
|
|
989
|
+
const shouldContinue = await sendQuestion(input);
|
|
990
|
+
if (!shouldContinue) {
|
|
991
|
+
break;
|
|
992
|
+
}
|
|
993
|
+
if (closing) {
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
if (skipNextLoopPrompt) {
|
|
997
|
+
skipNextLoopPrompt = false;
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
rl.prompt();
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
closeSession();
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
module.exports = {
|
|
1007
|
+
startChatSession
|
|
1008
|
+
};
|