snow-ai 0.2.15 → 0.2.17
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/dist/api/anthropic.d.ts +1 -1
- package/dist/api/anthropic.js +52 -76
- package/dist/api/chat.d.ts +4 -4
- package/dist/api/chat.js +32 -17
- package/dist/api/gemini.d.ts +1 -1
- package/dist/api/gemini.js +20 -13
- package/dist/api/models.d.ts +3 -0
- package/dist/api/models.js +101 -17
- package/dist/api/responses.d.ts +5 -5
- package/dist/api/responses.js +29 -27
- package/dist/app.js +4 -1
- package/dist/hooks/useClipboard.d.ts +4 -0
- package/dist/hooks/useClipboard.js +120 -0
- package/dist/hooks/useCommandHandler.d.ts +26 -0
- package/dist/hooks/useCommandHandler.js +158 -0
- package/dist/hooks/useCommandPanel.d.ts +16 -0
- package/dist/hooks/useCommandPanel.js +53 -0
- package/dist/hooks/useConversation.d.ts +9 -1
- package/dist/hooks/useConversation.js +152 -58
- package/dist/hooks/useFilePicker.d.ts +17 -0
- package/dist/hooks/useFilePicker.js +91 -0
- package/dist/hooks/useHistoryNavigation.d.ts +21 -0
- package/dist/hooks/useHistoryNavigation.js +50 -0
- package/dist/hooks/useInputBuffer.d.ts +6 -0
- package/dist/hooks/useInputBuffer.js +29 -0
- package/dist/hooks/useKeyboardInput.d.ts +51 -0
- package/dist/hooks/useKeyboardInput.js +272 -0
- package/dist/hooks/useSnapshotState.d.ts +12 -0
- package/dist/hooks/useSnapshotState.js +28 -0
- package/dist/hooks/useStreamingState.d.ts +24 -0
- package/dist/hooks/useStreamingState.js +96 -0
- package/dist/hooks/useVSCodeState.d.ts +8 -0
- package/dist/hooks/useVSCodeState.js +63 -0
- package/dist/mcp/filesystem.d.ts +24 -5
- package/dist/mcp/filesystem.js +52 -17
- package/dist/mcp/todo.js +4 -8
- package/dist/ui/components/ChatInput.js +71 -560
- package/dist/ui/components/DiffViewer.js +57 -30
- package/dist/ui/components/FileList.js +70 -26
- package/dist/ui/components/MessageList.d.ts +6 -0
- package/dist/ui/components/MessageList.js +47 -15
- package/dist/ui/components/ShimmerText.d.ts +9 -0
- package/dist/ui/components/ShimmerText.js +30 -0
- package/dist/ui/components/TodoTree.d.ts +1 -1
- package/dist/ui/components/TodoTree.js +0 -4
- package/dist/ui/components/ToolConfirmation.js +14 -6
- package/dist/ui/pages/ChatScreen.js +174 -373
- package/dist/ui/pages/CustomHeadersScreen.d.ts +6 -0
- package/dist/ui/pages/CustomHeadersScreen.js +104 -0
- package/dist/ui/pages/WelcomeScreen.js +5 -0
- package/dist/utils/apiConfig.d.ts +10 -0
- package/dist/utils/apiConfig.js +51 -0
- package/dist/utils/incrementalSnapshot.d.ts +8 -0
- package/dist/utils/incrementalSnapshot.js +63 -0
- package/dist/utils/mcpToolsManager.js +6 -1
- package/dist/utils/retryUtils.d.ts +22 -0
- package/dist/utils/retryUtils.js +180 -0
- package/dist/utils/sessionConverter.js +80 -17
- package/dist/utils/sessionManager.js +35 -4
- package/dist/utils/textUtils.d.ts +4 -0
- package/dist/utils/textUtils.js +19 -0
- package/dist/utils/todoPreprocessor.d.ts +1 -1
- package/dist/utils/todoPreprocessor.js +0 -1
- package/dist/utils/vscodeConnection.d.ts +8 -0
- package/dist/utils/vscodeConnection.js +44 -0
- package/package.json +1 -1
- package/readme.md +3 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useApp } from 'ink';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { homedir, platform } from 'os';
|
|
7
|
+
const CONFIG_DIR = join(homedir(), '.snow');
|
|
8
|
+
const CUSTOM_HEADERS_FILE = join(CONFIG_DIR, 'custom-headers.json');
|
|
9
|
+
function getSystemEditor() {
|
|
10
|
+
if (platform() === 'win32') {
|
|
11
|
+
return 'notepad';
|
|
12
|
+
}
|
|
13
|
+
return process.env['EDITOR'] || 'vim';
|
|
14
|
+
}
|
|
15
|
+
function ensureConfigDirectory() {
|
|
16
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
17
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const DEFAULT_HEADERS_TEMPLATE = `{
|
|
21
|
+
"X-Custom-Header": "custom-value",
|
|
22
|
+
"User-Agent": "MyApp/1.0"
|
|
23
|
+
}`;
|
|
24
|
+
export default function CustomHeadersScreen({ onBack }) {
|
|
25
|
+
const { exit } = useApp();
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const openEditor = async () => {
|
|
28
|
+
ensureConfigDirectory();
|
|
29
|
+
// Read existing custom headers, or use template if not exists
|
|
30
|
+
let currentHeaders = DEFAULT_HEADERS_TEMPLATE;
|
|
31
|
+
if (existsSync(CUSTOM_HEADERS_FILE)) {
|
|
32
|
+
try {
|
|
33
|
+
currentHeaders = readFileSync(CUSTOM_HEADERS_FILE, 'utf8');
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Read failed, use template
|
|
37
|
+
currentHeaders = DEFAULT_HEADERS_TEMPLATE;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Write to file for editing
|
|
41
|
+
writeFileSync(CUSTOM_HEADERS_FILE, currentHeaders, 'utf8');
|
|
42
|
+
const editor = getSystemEditor();
|
|
43
|
+
exit();
|
|
44
|
+
const child = spawn(editor, [CUSTOM_HEADERS_FILE], {
|
|
45
|
+
stdio: 'inherit'
|
|
46
|
+
});
|
|
47
|
+
child.on('close', () => {
|
|
48
|
+
// Read edited content
|
|
49
|
+
if (existsSync(CUSTOM_HEADERS_FILE)) {
|
|
50
|
+
try {
|
|
51
|
+
const editedContent = readFileSync(CUSTOM_HEADERS_FILE, 'utf8');
|
|
52
|
+
const trimmedContent = editedContent.trim();
|
|
53
|
+
// Validate JSON format
|
|
54
|
+
if (trimmedContent === '' || trimmedContent === '{}') {
|
|
55
|
+
// Empty or empty object, delete file to reset
|
|
56
|
+
try {
|
|
57
|
+
const fs = require('fs');
|
|
58
|
+
fs.unlinkSync(CUSTOM_HEADERS_FILE);
|
|
59
|
+
console.log('Custom headers cleared. Please use `snow` to restart!');
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Delete failed, save empty object
|
|
63
|
+
writeFileSync(CUSTOM_HEADERS_FILE, '{}', 'utf8');
|
|
64
|
+
console.log('Custom headers cleared. Please use `snow` to restart!');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// Validate JSON
|
|
69
|
+
try {
|
|
70
|
+
const headers = JSON.parse(trimmedContent);
|
|
71
|
+
if (typeof headers !== 'object' || headers === null || Array.isArray(headers)) {
|
|
72
|
+
throw new Error('Headers must be a JSON object');
|
|
73
|
+
}
|
|
74
|
+
// Validate all values are strings
|
|
75
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
76
|
+
if (typeof value !== 'string') {
|
|
77
|
+
throw new Error(`Header value for "${key}" must be a string`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Save valid headers
|
|
81
|
+
writeFileSync(CUSTOM_HEADERS_FILE, JSON.stringify(headers, null, 2), 'utf8');
|
|
82
|
+
console.log('Custom headers saved successfully! Please use `snow` to restart!');
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
console.error('Invalid JSON format:', error instanceof Error ? error.message : 'Unknown error');
|
|
86
|
+
console.error('Custom headers were NOT saved. Please fix the JSON format and try again.');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error('Failed to save custom headers:', error instanceof Error ? error.message : 'Unknown error');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
process.exit(0);
|
|
95
|
+
});
|
|
96
|
+
child.on('error', (error) => {
|
|
97
|
+
console.error('Failed to open editor:', error.message);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
openEditor();
|
|
102
|
+
}, [exit, onBack]);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
@@ -27,6 +27,11 @@ export default function WelcomeScreen({ version = '1.0.0', onMenuSelect, }) {
|
|
|
27
27
|
value: 'systemprompt',
|
|
28
28
|
infoText: 'Configure custom system prompt (overrides default)',
|
|
29
29
|
},
|
|
30
|
+
{
|
|
31
|
+
label: 'Custom Headers Settings',
|
|
32
|
+
value: 'customheaders',
|
|
33
|
+
infoText: 'Configure custom HTTP headers for API requests',
|
|
34
|
+
},
|
|
30
35
|
{
|
|
31
36
|
label: 'MCP Settings',
|
|
32
37
|
value: 'mcp',
|
|
@@ -42,3 +42,13 @@ export declare function validateMCPConfig(config: Partial<MCPConfig>): string[];
|
|
|
42
42
|
* 否则返回 undefined (使用默认系统提示词)
|
|
43
43
|
*/
|
|
44
44
|
export declare function getCustomSystemPrompt(): string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* 读取自定义请求头配置
|
|
47
|
+
* 如果 custom-headers.json 文件存在且有效,返回其内容
|
|
48
|
+
* 否则返回空对象
|
|
49
|
+
*/
|
|
50
|
+
export declare function getCustomHeaders(): Record<string, string>;
|
|
51
|
+
/**
|
|
52
|
+
* 保存自定义请求头配置
|
|
53
|
+
*/
|
|
54
|
+
export declare function saveCustomHeaders(headers: Record<string, string>): void;
|
package/dist/utils/apiConfig.js
CHANGED
|
@@ -18,6 +18,7 @@ const DEFAULT_MCP_CONFIG = {
|
|
|
18
18
|
};
|
|
19
19
|
const CONFIG_DIR = join(homedir(), '.snow');
|
|
20
20
|
const SYSTEM_PROMPT_FILE = join(CONFIG_DIR, 'system-prompt.txt');
|
|
21
|
+
const CUSTOM_HEADERS_FILE = join(CONFIG_DIR, 'custom-headers.json');
|
|
21
22
|
function normalizeRequestMethod(method) {
|
|
22
23
|
if (method === 'chat' || method === 'responses' || method === 'gemini' || method === 'anthropic') {
|
|
23
24
|
return method;
|
|
@@ -216,3 +217,53 @@ export function getCustomSystemPrompt() {
|
|
|
216
217
|
return undefined;
|
|
217
218
|
}
|
|
218
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* 读取自定义请求头配置
|
|
222
|
+
* 如果 custom-headers.json 文件存在且有效,返回其内容
|
|
223
|
+
* 否则返回空对象
|
|
224
|
+
*/
|
|
225
|
+
export function getCustomHeaders() {
|
|
226
|
+
ensureConfigDirectory();
|
|
227
|
+
if (!existsSync(CUSTOM_HEADERS_FILE)) {
|
|
228
|
+
return {};
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
const content = readFileSync(CUSTOM_HEADERS_FILE, 'utf8');
|
|
232
|
+
const headers = JSON.parse(content);
|
|
233
|
+
// 验证格式:必须是对象,且所有值都是字符串
|
|
234
|
+
if (typeof headers !== 'object' || headers === null || Array.isArray(headers)) {
|
|
235
|
+
return {};
|
|
236
|
+
}
|
|
237
|
+
// 过滤掉非字符串的值
|
|
238
|
+
const validHeaders = {};
|
|
239
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
240
|
+
if (typeof value === 'string') {
|
|
241
|
+
validHeaders[key] = value;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return validHeaders;
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return {};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* 保存自定义请求头配置
|
|
252
|
+
*/
|
|
253
|
+
export function saveCustomHeaders(headers) {
|
|
254
|
+
ensureConfigDirectory();
|
|
255
|
+
try {
|
|
256
|
+
// 过滤掉空键值对
|
|
257
|
+
const filteredHeaders = {};
|
|
258
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
259
|
+
if (key.trim() && value.trim()) {
|
|
260
|
+
filteredHeaders[key.trim()] = value.trim();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const content = JSON.stringify(filteredHeaders, null, 2);
|
|
264
|
+
writeFileSync(CUSTOM_HEADERS_FILE, content, 'utf8');
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
throw new Error(`Failed to save custom headers: ${error}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -60,6 +60,14 @@ declare class IncrementalSnapshotManager {
|
|
|
60
60
|
* Rollback to a specific snapshot
|
|
61
61
|
*/
|
|
62
62
|
rollbackToSnapshot(sessionId: string, messageIndex: number): Promise<boolean>;
|
|
63
|
+
/**
|
|
64
|
+
* Rollback all snapshots after a specific message index
|
|
65
|
+
* This is used when user selects to rollback to a specific message
|
|
66
|
+
* @param sessionId Session ID
|
|
67
|
+
* @param targetMessageIndex The message index to rollback to (inclusive)
|
|
68
|
+
* @returns Number of files rolled back
|
|
69
|
+
*/
|
|
70
|
+
rollbackToMessageIndex(sessionId: string, targetMessageIndex: number): Promise<number>;
|
|
63
71
|
/**
|
|
64
72
|
* Clear all snapshots for a session
|
|
65
73
|
*/
|
|
@@ -157,6 +157,69 @@ class IncrementalSnapshotManager {
|
|
|
157
157
|
return false;
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Rollback all snapshots after a specific message index
|
|
162
|
+
* This is used when user selects to rollback to a specific message
|
|
163
|
+
* @param sessionId Session ID
|
|
164
|
+
* @param targetMessageIndex The message index to rollback to (inclusive)
|
|
165
|
+
* @returns Number of files rolled back
|
|
166
|
+
*/
|
|
167
|
+
async rollbackToMessageIndex(sessionId, targetMessageIndex) {
|
|
168
|
+
await this.ensureSnapshotsDir();
|
|
169
|
+
try {
|
|
170
|
+
const files = await fs.readdir(this.snapshotsDir);
|
|
171
|
+
const snapshots = [];
|
|
172
|
+
// Load all snapshots for this session
|
|
173
|
+
for (const file of files) {
|
|
174
|
+
if (file.startsWith(sessionId) && file.endsWith('.json')) {
|
|
175
|
+
const snapshotPath = path.join(this.snapshotsDir, file);
|
|
176
|
+
const content = await fs.readFile(snapshotPath, 'utf-8');
|
|
177
|
+
const metadata = JSON.parse(content);
|
|
178
|
+
snapshots.push({
|
|
179
|
+
messageIndex: metadata.messageIndex,
|
|
180
|
+
path: snapshotPath,
|
|
181
|
+
metadata
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Filter snapshots that are >= targetMessageIndex and sort in descending order
|
|
186
|
+
// We rollback from newest to oldest to ensure correct restoration
|
|
187
|
+
const snapshotsToRollback = snapshots
|
|
188
|
+
.filter(s => s.messageIndex >= targetMessageIndex)
|
|
189
|
+
.sort((a, b) => b.messageIndex - a.messageIndex);
|
|
190
|
+
let totalFilesRolledBack = 0;
|
|
191
|
+
// Rollback each snapshot in reverse chronological order
|
|
192
|
+
for (const snapshot of snapshotsToRollback) {
|
|
193
|
+
for (const backup of snapshot.metadata.backups) {
|
|
194
|
+
try {
|
|
195
|
+
if (backup.existed && backup.content !== null) {
|
|
196
|
+
// Restore original file content
|
|
197
|
+
await fs.writeFile(backup.path, backup.content, 'utf-8');
|
|
198
|
+
totalFilesRolledBack++;
|
|
199
|
+
}
|
|
200
|
+
else if (!backup.existed) {
|
|
201
|
+
// Delete file that was created
|
|
202
|
+
try {
|
|
203
|
+
await fs.unlink(backup.path);
|
|
204
|
+
totalFilesRolledBack++;
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
// File may not exist, ignore
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
console.error(`Failed to restore file ${backup.path}:`, error);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return totalFilesRolledBack;
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
console.error('Failed to rollback to message index:', error);
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
160
223
|
/**
|
|
161
224
|
* Clear all snapshots for a session
|
|
162
225
|
*/
|
|
@@ -382,7 +382,12 @@ export async function executeMCPTool(toolName, args) {
|
|
|
382
382
|
case 'create':
|
|
383
383
|
return await filesystemService.createFile(args.filePath, args.content, args.createDirectories);
|
|
384
384
|
case 'delete':
|
|
385
|
-
|
|
385
|
+
// Support both filePath (legacy) and filePaths (new) parameters
|
|
386
|
+
const pathsToDelete = args.filePaths || args.filePath;
|
|
387
|
+
if (!pathsToDelete) {
|
|
388
|
+
throw new Error('Missing required parameter: filePath or filePaths');
|
|
389
|
+
}
|
|
390
|
+
return await filesystemService.deleteFile(pathsToDelete);
|
|
386
391
|
case 'list':
|
|
387
392
|
return await filesystemService.listFiles(args.dirPath);
|
|
388
393
|
case 'exists':
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 重试工具函数
|
|
3
|
+
* 提供统一的重试机制用于所有 AI 请求
|
|
4
|
+
* - 支持5次重试
|
|
5
|
+
* - 延时递增策略 (1s, 2s, 4s, 8s, 16s)
|
|
6
|
+
* - 支持 AbortSignal 中断
|
|
7
|
+
*/
|
|
8
|
+
export interface RetryOptions {
|
|
9
|
+
maxRetries?: number;
|
|
10
|
+
baseDelay?: number;
|
|
11
|
+
onRetry?: (error: Error, attempt: number, nextDelay: number) => void;
|
|
12
|
+
abortSignal?: AbortSignal;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 包装异步函数,提供重试机制
|
|
16
|
+
*/
|
|
17
|
+
export declare function withRetry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
18
|
+
/**
|
|
19
|
+
* 包装异步生成器函数,提供重试机制
|
|
20
|
+
* 注意:如果生成器已经开始产生数据,则不会重试
|
|
21
|
+
*/
|
|
22
|
+
export declare function withRetryGenerator<T>(fn: () => AsyncGenerator<T, void, unknown>, options?: RetryOptions): AsyncGenerator<T, void, unknown>;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 重试工具函数
|
|
3
|
+
* 提供统一的重试机制用于所有 AI 请求
|
|
4
|
+
* - 支持5次重试
|
|
5
|
+
* - 延时递增策略 (1s, 2s, 4s, 8s, 16s)
|
|
6
|
+
* - 支持 AbortSignal 中断
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* 延时函数,支持 AbortSignal 中断
|
|
10
|
+
*/
|
|
11
|
+
async function delay(ms, abortSignal) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
if (abortSignal?.aborted) {
|
|
14
|
+
reject(new Error('Aborted'));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const timer = setTimeout(() => {
|
|
18
|
+
cleanup();
|
|
19
|
+
resolve();
|
|
20
|
+
}, ms);
|
|
21
|
+
const abortHandler = () => {
|
|
22
|
+
cleanup();
|
|
23
|
+
reject(new Error('Aborted'));
|
|
24
|
+
};
|
|
25
|
+
const cleanup = () => {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
abortSignal?.removeEventListener('abort', abortHandler);
|
|
28
|
+
};
|
|
29
|
+
abortSignal?.addEventListener('abort', abortHandler);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 判断错误是否可重试
|
|
34
|
+
*/
|
|
35
|
+
function isRetriableError(error) {
|
|
36
|
+
const errorMessage = error.message.toLowerCase();
|
|
37
|
+
// 网络错误
|
|
38
|
+
if (errorMessage.includes('network') ||
|
|
39
|
+
errorMessage.includes('econnrefused') ||
|
|
40
|
+
errorMessage.includes('econnreset') ||
|
|
41
|
+
errorMessage.includes('etimedout') ||
|
|
42
|
+
errorMessage.includes('timeout')) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
// Rate limit errors
|
|
46
|
+
if (errorMessage.includes('rate limit') ||
|
|
47
|
+
errorMessage.includes('too many requests') ||
|
|
48
|
+
errorMessage.includes('429')) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
// Server errors (5xx)
|
|
52
|
+
if (errorMessage.includes('500') ||
|
|
53
|
+
errorMessage.includes('502') ||
|
|
54
|
+
errorMessage.includes('503') ||
|
|
55
|
+
errorMessage.includes('504') ||
|
|
56
|
+
errorMessage.includes('internal server error') ||
|
|
57
|
+
errorMessage.includes('bad gateway') ||
|
|
58
|
+
errorMessage.includes('service unavailable') ||
|
|
59
|
+
errorMessage.includes('gateway timeout')) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
// Temporary service unavailable
|
|
63
|
+
if (errorMessage.includes('overloaded') ||
|
|
64
|
+
errorMessage.includes('unavailable')) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 包装异步函数,提供重试机制
|
|
71
|
+
*/
|
|
72
|
+
export async function withRetry(fn, options = {}) {
|
|
73
|
+
const { maxRetries = 5, baseDelay = 1000, onRetry, abortSignal } = options;
|
|
74
|
+
let lastError = null;
|
|
75
|
+
let attempt = 0;
|
|
76
|
+
while (attempt <= maxRetries) {
|
|
77
|
+
// 检查是否已中断
|
|
78
|
+
if (abortSignal?.aborted) {
|
|
79
|
+
throw new Error('Request aborted');
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
// 尝试执行函数
|
|
83
|
+
return await fn();
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
lastError = error;
|
|
87
|
+
// 如果是 AbortError,立即退出
|
|
88
|
+
if (lastError.name === 'AbortError' || lastError.message === 'Aborted') {
|
|
89
|
+
throw lastError;
|
|
90
|
+
}
|
|
91
|
+
// 如果已达到最大重试次数,抛出错误
|
|
92
|
+
if (attempt >= maxRetries) {
|
|
93
|
+
throw lastError;
|
|
94
|
+
}
|
|
95
|
+
// 检查错误是否可重试
|
|
96
|
+
if (!isRetriableError(lastError)) {
|
|
97
|
+
throw lastError;
|
|
98
|
+
}
|
|
99
|
+
// 计算下次重试的延时(指数退避:1s, 2s, 4s, 8s, 16s)
|
|
100
|
+
const nextDelay = baseDelay * Math.pow(2, attempt);
|
|
101
|
+
// 调用重试回调
|
|
102
|
+
if (onRetry) {
|
|
103
|
+
onRetry(lastError, attempt + 1, nextDelay);
|
|
104
|
+
}
|
|
105
|
+
// 等待后重试
|
|
106
|
+
try {
|
|
107
|
+
await delay(nextDelay, abortSignal);
|
|
108
|
+
}
|
|
109
|
+
catch (delayError) {
|
|
110
|
+
// 延时过程中被中断
|
|
111
|
+
throw new Error('Request aborted');
|
|
112
|
+
}
|
|
113
|
+
attempt++;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// 不应该到达这里
|
|
117
|
+
throw lastError || new Error('Retry failed');
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 包装异步生成器函数,提供重试机制
|
|
121
|
+
* 注意:如果生成器已经开始产生数据,则不会重试
|
|
122
|
+
*/
|
|
123
|
+
export async function* withRetryGenerator(fn, options = {}) {
|
|
124
|
+
const { maxRetries = 5, baseDelay = 1000, onRetry, abortSignal } = options;
|
|
125
|
+
let lastError = null;
|
|
126
|
+
let attempt = 0;
|
|
127
|
+
let hasYielded = false; // 标记是否已经产生过数据
|
|
128
|
+
while (attempt <= maxRetries) {
|
|
129
|
+
// 检查是否已中断
|
|
130
|
+
if (abortSignal?.aborted) {
|
|
131
|
+
throw new Error('Request aborted');
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
// 尝试执行生成器
|
|
135
|
+
const generator = fn();
|
|
136
|
+
for await (const chunk of generator) {
|
|
137
|
+
hasYielded = true; // 标记已产生数据
|
|
138
|
+
yield chunk;
|
|
139
|
+
}
|
|
140
|
+
// 成功完成
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
lastError = error;
|
|
145
|
+
// 如果是 AbortError,立即退出
|
|
146
|
+
if (lastError.name === 'AbortError' || lastError.message === 'Aborted') {
|
|
147
|
+
throw lastError;
|
|
148
|
+
}
|
|
149
|
+
// 如果已经产生过数据,不再重试(避免重复数据)
|
|
150
|
+
if (hasYielded) {
|
|
151
|
+
throw lastError;
|
|
152
|
+
}
|
|
153
|
+
// 如果已达到最大重试次数,抛出错误
|
|
154
|
+
if (attempt >= maxRetries) {
|
|
155
|
+
throw lastError;
|
|
156
|
+
}
|
|
157
|
+
// 检查错误是否可重试
|
|
158
|
+
if (!isRetriableError(lastError)) {
|
|
159
|
+
throw lastError;
|
|
160
|
+
}
|
|
161
|
+
// 计算下次重试的延时(指数退避:1s, 2s, 4s, 8s, 16s)
|
|
162
|
+
const nextDelay = baseDelay * Math.pow(2, attempt);
|
|
163
|
+
// 调用重试回调
|
|
164
|
+
if (onRetry) {
|
|
165
|
+
onRetry(lastError, attempt + 1, nextDelay);
|
|
166
|
+
}
|
|
167
|
+
// 等待后重试
|
|
168
|
+
try {
|
|
169
|
+
await delay(nextDelay, abortSignal);
|
|
170
|
+
}
|
|
171
|
+
catch (delayError) {
|
|
172
|
+
// 延时过程中被中断
|
|
173
|
+
throw new Error('Request aborted');
|
|
174
|
+
}
|
|
175
|
+
attempt++;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// 不应该到达这里
|
|
179
|
+
throw lastError || new Error('Retry failed');
|
|
180
|
+
}
|
|
@@ -4,33 +4,31 @@ import { formatToolCallMessage } from './messageFormatter.js';
|
|
|
4
4
|
*/
|
|
5
5
|
export function convertSessionMessagesToUI(sessionMessages) {
|
|
6
6
|
const uiMessages = [];
|
|
7
|
+
// First pass: build a map of tool_call_id to tool results
|
|
8
|
+
const toolResultsMap = new Map();
|
|
9
|
+
for (const msg of sessionMessages) {
|
|
10
|
+
if (msg.role === 'tool' && msg.tool_call_id) {
|
|
11
|
+
toolResultsMap.set(msg.tool_call_id, msg.content);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
7
14
|
for (const msg of sessionMessages) {
|
|
8
15
|
// Skip system messages
|
|
9
16
|
if (msg.role === 'system')
|
|
10
17
|
continue;
|
|
11
|
-
//
|
|
12
|
-
if (msg.role === 'tool')
|
|
13
|
-
const isError = msg.content.startsWith('Error:');
|
|
14
|
-
const statusIcon = isError ? '✗' : '✓';
|
|
15
|
-
const statusText = isError ? `\n └─ ${msg.content}` : '';
|
|
16
|
-
const toolName = msg.tool_call_id || 'unknown-tool';
|
|
17
|
-
uiMessages.push({
|
|
18
|
-
role: 'assistant',
|
|
19
|
-
content: `${statusIcon} ${toolName}${statusText}`,
|
|
20
|
-
streaming: false,
|
|
21
|
-
toolResult: !isError ? msg.content : undefined
|
|
22
|
-
});
|
|
18
|
+
// Skip tool role messages (we'll attach them to tool calls)
|
|
19
|
+
if (msg.role === 'tool')
|
|
23
20
|
continue;
|
|
24
|
-
}
|
|
25
21
|
// Handle user and assistant messages
|
|
26
22
|
const uiMessage = {
|
|
27
23
|
role: msg.role,
|
|
28
24
|
content: msg.content,
|
|
29
25
|
streaming: false,
|
|
30
|
-
images: msg.images
|
|
26
|
+
images: msg.images,
|
|
31
27
|
};
|
|
32
28
|
// If assistant message has tool_calls, expand to show each tool call
|
|
33
|
-
if (msg.role === 'assistant' &&
|
|
29
|
+
if (msg.role === 'assistant' &&
|
|
30
|
+
msg.tool_calls &&
|
|
31
|
+
msg.tool_calls.length > 0) {
|
|
34
32
|
for (const toolCall of msg.tool_calls) {
|
|
35
33
|
const toolDisplay = formatToolCallMessage(toolCall);
|
|
36
34
|
let toolArgs;
|
|
@@ -40,16 +38,81 @@ export function convertSessionMessagesToUI(sessionMessages) {
|
|
|
40
38
|
catch (e) {
|
|
41
39
|
toolArgs = {};
|
|
42
40
|
}
|
|
41
|
+
// Get the tool result for this tool call
|
|
42
|
+
const toolResult = toolResultsMap.get(toolCall.id);
|
|
43
|
+
const isError = toolResult?.startsWith('Error:') || false;
|
|
44
|
+
// For filesystem-edit, try to extract diff data from result
|
|
45
|
+
let editDiffData;
|
|
46
|
+
if (toolCall.function.name === 'filesystem-edit' &&
|
|
47
|
+
toolResult &&
|
|
48
|
+
!isError) {
|
|
49
|
+
try {
|
|
50
|
+
const resultData = JSON.parse(toolResult);
|
|
51
|
+
if (resultData.oldContent && resultData.newContent) {
|
|
52
|
+
editDiffData = {
|
|
53
|
+
oldContent: resultData.oldContent,
|
|
54
|
+
newContent: resultData.newContent,
|
|
55
|
+
filename: toolArgs.filePath,
|
|
56
|
+
};
|
|
57
|
+
// Merge diff data into toolArgs for DiffViewer
|
|
58
|
+
toolArgs.oldContent = resultData.oldContent;
|
|
59
|
+
toolArgs.newContent = resultData.newContent;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
// If parsing fails, just show regular result
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// For terminal-execute, try to extract terminal result data
|
|
67
|
+
let terminalResultData;
|
|
68
|
+
if (toolCall.function.name === 'terminal-execute' &&
|
|
69
|
+
toolResult &&
|
|
70
|
+
!isError) {
|
|
71
|
+
try {
|
|
72
|
+
const resultData = JSON.parse(toolResult);
|
|
73
|
+
if (resultData.stdout !== undefined ||
|
|
74
|
+
resultData.stderr !== undefined) {
|
|
75
|
+
terminalResultData = {
|
|
76
|
+
stdout: resultData.stdout,
|
|
77
|
+
stderr: resultData.stderr,
|
|
78
|
+
exitCode: resultData.exitCode,
|
|
79
|
+
command: toolArgs.command,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
// If parsing fails, just show regular result
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Create tool call message
|
|
43
88
|
uiMessages.push({
|
|
44
89
|
role: 'assistant',
|
|
45
90
|
content: `⚡ ${toolDisplay.toolName}`,
|
|
46
91
|
streaming: false,
|
|
47
92
|
toolCall: {
|
|
48
93
|
name: toolCall.function.name,
|
|
49
|
-
arguments: toolArgs
|
|
94
|
+
arguments: toolArgs,
|
|
50
95
|
},
|
|
51
|
-
toolDisplay
|
|
96
|
+
toolDisplay,
|
|
52
97
|
});
|
|
98
|
+
// Create tool result message
|
|
99
|
+
if (toolResult) {
|
|
100
|
+
const statusIcon = isError ? '✗' : '✓';
|
|
101
|
+
const statusText = isError ? `\n └─ ${toolResult}` : '';
|
|
102
|
+
uiMessages.push({
|
|
103
|
+
role: 'assistant',
|
|
104
|
+
content: `${statusIcon} ${toolCall.function.name}${statusText}`,
|
|
105
|
+
streaming: false,
|
|
106
|
+
toolResult: !isError ? toolResult : undefined,
|
|
107
|
+
toolCall: editDiffData || terminalResultData
|
|
108
|
+
? {
|
|
109
|
+
name: toolCall.function.name,
|
|
110
|
+
arguments: toolArgs,
|
|
111
|
+
}
|
|
112
|
+
: undefined,
|
|
113
|
+
terminalResult: terminalResultData,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
53
116
|
}
|
|
54
117
|
}
|
|
55
118
|
else {
|