snow-ai 0.3.6 → 0.3.7
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/agents/reviewAgent.d.ts +50 -0
- package/dist/agents/reviewAgent.js +264 -0
- package/dist/api/anthropic.js +104 -71
- package/dist/api/chat.d.ts +1 -1
- package/dist/api/chat.js +60 -41
- package/dist/api/gemini.js +97 -57
- package/dist/api/responses.d.ts +9 -1
- package/dist/api/responses.js +110 -70
- package/dist/api/systemPrompt.d.ts +1 -1
- package/dist/api/systemPrompt.js +36 -7
- package/dist/api/types.d.ts +8 -0
- package/dist/hooks/useCommandHandler.d.ts +1 -0
- package/dist/hooks/useCommandHandler.js +44 -1
- package/dist/hooks/useCommandPanel.js +13 -0
- package/dist/hooks/useConversation.d.ts +4 -1
- package/dist/hooks/useConversation.js +48 -6
- package/dist/hooks/useKeyboardInput.js +19 -0
- package/dist/hooks/useTerminalFocus.js +13 -3
- package/dist/mcp/aceCodeSearch.d.ts +2 -76
- package/dist/mcp/aceCodeSearch.js +31 -467
- package/dist/mcp/bash.d.ts +1 -8
- package/dist/mcp/bash.js +20 -40
- package/dist/mcp/filesystem.d.ts +3 -68
- package/dist/mcp/filesystem.js +32 -348
- package/dist/mcp/ideDiagnostics.js +2 -4
- package/dist/mcp/todo.d.ts +1 -17
- package/dist/mcp/todo.js +11 -15
- package/dist/mcp/types/aceCodeSearch.types.d.ts +92 -0
- package/dist/mcp/types/aceCodeSearch.types.js +4 -0
- package/dist/mcp/types/bash.types.d.ts +13 -0
- package/dist/mcp/types/bash.types.js +4 -0
- package/dist/mcp/types/filesystem.types.d.ts +44 -0
- package/dist/mcp/types/filesystem.types.js +4 -0
- package/dist/mcp/types/todo.types.d.ts +27 -0
- package/dist/mcp/types/todo.types.js +4 -0
- package/dist/mcp/types/websearch.types.d.ts +30 -0
- package/dist/mcp/types/websearch.types.js +4 -0
- package/dist/mcp/utils/aceCodeSearch/filesystem.utils.d.ts +34 -0
- package/dist/mcp/utils/aceCodeSearch/filesystem.utils.js +146 -0
- package/dist/mcp/utils/aceCodeSearch/language.utils.d.ts +14 -0
- package/dist/mcp/utils/aceCodeSearch/language.utils.js +99 -0
- package/dist/mcp/utils/aceCodeSearch/search.utils.d.ts +31 -0
- package/dist/mcp/utils/aceCodeSearch/search.utils.js +136 -0
- package/dist/mcp/utils/aceCodeSearch/symbol.utils.d.ts +20 -0
- package/dist/mcp/utils/aceCodeSearch/symbol.utils.js +141 -0
- package/dist/mcp/utils/bash/security.utils.d.ts +20 -0
- package/dist/mcp/utils/bash/security.utils.js +34 -0
- package/dist/mcp/utils/filesystem/code-analysis.utils.d.ts +18 -0
- package/dist/mcp/utils/filesystem/code-analysis.utils.js +165 -0
- package/dist/mcp/utils/filesystem/match-finder.utils.d.ts +16 -0
- package/dist/mcp/utils/filesystem/match-finder.utils.js +85 -0
- package/dist/mcp/utils/filesystem/similarity.utils.d.ts +22 -0
- package/dist/mcp/utils/filesystem/similarity.utils.js +75 -0
- package/dist/mcp/utils/todo/date.utils.d.ts +9 -0
- package/dist/mcp/utils/todo/date.utils.js +14 -0
- package/dist/mcp/utils/websearch/browser.utils.d.ts +8 -0
- package/dist/mcp/utils/websearch/browser.utils.js +58 -0
- package/dist/mcp/utils/websearch/text.utils.d.ts +16 -0
- package/dist/mcp/utils/websearch/text.utils.js +39 -0
- package/dist/mcp/websearch.d.ts +1 -31
- package/dist/mcp/websearch.js +21 -97
- package/dist/ui/components/ChatInput.d.ts +2 -1
- package/dist/ui/components/ChatInput.js +10 -3
- package/dist/ui/components/MarkdownRenderer.d.ts +1 -2
- package/dist/ui/components/MarkdownRenderer.js +16 -153
- package/dist/ui/components/MessageList.js +4 -4
- package/dist/ui/components/SessionListScreen.js +37 -17
- package/dist/ui/components/ToolResultPreview.js +6 -6
- package/dist/ui/components/UsagePanel.d.ts +2 -0
- package/dist/ui/components/UsagePanel.js +360 -0
- package/dist/ui/pages/ChatScreen.d.ts +4 -0
- package/dist/ui/pages/ChatScreen.js +70 -30
- package/dist/ui/pages/ConfigScreen.js +23 -19
- package/dist/ui/pages/HeadlessModeScreen.js +2 -4
- package/dist/ui/pages/SubAgentConfigScreen.js +17 -17
- package/dist/ui/pages/SystemPromptConfigScreen.js +7 -6
- package/dist/utils/commandExecutor.d.ts +3 -3
- package/dist/utils/commandExecutor.js +4 -4
- package/dist/utils/commands/home.d.ts +2 -0
- package/dist/utils/commands/home.js +12 -0
- package/dist/utils/commands/review.d.ts +2 -0
- package/dist/utils/commands/review.js +81 -0
- package/dist/utils/commands/role.d.ts +2 -0
- package/dist/utils/commands/role.js +37 -0
- package/dist/utils/commands/usage.d.ts +2 -0
- package/dist/utils/commands/usage.js +12 -0
- package/dist/utils/contextCompressor.js +99 -367
- package/dist/utils/fileUtils.js +3 -3
- package/dist/utils/mcpToolsManager.js +12 -12
- package/dist/utils/proxyUtils.d.ts +15 -0
- package/dist/utils/proxyUtils.js +50 -0
- package/dist/utils/retryUtils.d.ts +27 -0
- package/dist/utils/retryUtils.js +114 -2
- package/dist/utils/sessionManager.d.ts +2 -5
- package/dist/utils/sessionManager.js +16 -83
- package/dist/utils/terminal.js +4 -3
- package/dist/utils/usageLogger.d.ts +11 -0
- package/dist/utils/usageLogger.js +99 -0
- package/package.json +3 -7
- package/dist/agents/summaryAgent.d.ts +0 -31
- package/dist/agents/summaryAgent.js +0 -256
|
@@ -9,7 +9,7 @@ import { mcpTools as aceCodeSearchTools } from '../mcp/aceCodeSearch.js';
|
|
|
9
9
|
import { mcpTools as websearchTools } from '../mcp/websearch.js';
|
|
10
10
|
import { mcpTools as ideDiagnosticsTools } from '../mcp/ideDiagnostics.js';
|
|
11
11
|
import { TodoService } from '../mcp/todo.js';
|
|
12
|
-
import { getMCPTools as getSubAgentTools, subAgentService } from '../mcp/subagent.js';
|
|
12
|
+
import { getMCPTools as getSubAgentTools, subAgentService, } from '../mcp/subagent.js';
|
|
13
13
|
import { sessionManager } from './sessionManager.js';
|
|
14
14
|
import { logger } from './logger.js';
|
|
15
15
|
import { resourceMonitor } from './resourceMonitor.js';
|
|
@@ -69,7 +69,7 @@ async function refreshToolsCache() {
|
|
|
69
69
|
const servicesInfo = [];
|
|
70
70
|
// Add built-in filesystem tools (always available)
|
|
71
71
|
const filesystemServiceTools = filesystemTools.map(tool => ({
|
|
72
|
-
name: tool.name.replace('
|
|
72
|
+
name: tool.name.replace('filesystem-', ''),
|
|
73
73
|
description: tool.description,
|
|
74
74
|
inputSchema: tool.inputSchema,
|
|
75
75
|
}));
|
|
@@ -83,7 +83,7 @@ async function refreshToolsCache() {
|
|
|
83
83
|
allTools.push({
|
|
84
84
|
type: 'function',
|
|
85
85
|
function: {
|
|
86
|
-
name:
|
|
86
|
+
name: tool.name,
|
|
87
87
|
description: tool.description,
|
|
88
88
|
parameters: tool.inputSchema,
|
|
89
89
|
},
|
|
@@ -91,7 +91,7 @@ async function refreshToolsCache() {
|
|
|
91
91
|
}
|
|
92
92
|
// Add built-in terminal tools (always available)
|
|
93
93
|
const terminalServiceTools = terminalTools.map(tool => ({
|
|
94
|
-
name: tool.name.replace('
|
|
94
|
+
name: tool.name.replace('terminal-', ''),
|
|
95
95
|
description: tool.description,
|
|
96
96
|
inputSchema: tool.inputSchema,
|
|
97
97
|
}));
|
|
@@ -105,7 +105,7 @@ async function refreshToolsCache() {
|
|
|
105
105
|
allTools.push({
|
|
106
106
|
type: 'function',
|
|
107
107
|
function: {
|
|
108
|
-
name:
|
|
108
|
+
name: tool.name,
|
|
109
109
|
description: tool.description,
|
|
110
110
|
parameters: tool.inputSchema,
|
|
111
111
|
},
|
|
@@ -137,7 +137,7 @@ async function refreshToolsCache() {
|
|
|
137
137
|
}
|
|
138
138
|
// Add built-in ACE Code Search tools (always available)
|
|
139
139
|
const aceServiceTools = aceCodeSearchTools.map(tool => ({
|
|
140
|
-
name: tool.name.replace('
|
|
140
|
+
name: tool.name.replace('ace-', ''),
|
|
141
141
|
description: tool.description,
|
|
142
142
|
inputSchema: tool.inputSchema,
|
|
143
143
|
}));
|
|
@@ -151,7 +151,7 @@ async function refreshToolsCache() {
|
|
|
151
151
|
allTools.push({
|
|
152
152
|
type: 'function',
|
|
153
153
|
function: {
|
|
154
|
-
name:
|
|
154
|
+
name: tool.name,
|
|
155
155
|
description: tool.description,
|
|
156
156
|
parameters: tool.inputSchema,
|
|
157
157
|
},
|
|
@@ -159,7 +159,7 @@ async function refreshToolsCache() {
|
|
|
159
159
|
}
|
|
160
160
|
// Add built-in Web Search tools (always available)
|
|
161
161
|
const websearchServiceTools = websearchTools.map(tool => ({
|
|
162
|
-
name: tool.name.replace('
|
|
162
|
+
name: tool.name.replace('websearch-', ''),
|
|
163
163
|
description: tool.description,
|
|
164
164
|
inputSchema: tool.inputSchema,
|
|
165
165
|
}));
|
|
@@ -173,7 +173,7 @@ async function refreshToolsCache() {
|
|
|
173
173
|
allTools.push({
|
|
174
174
|
type: 'function',
|
|
175
175
|
function: {
|
|
176
|
-
name:
|
|
176
|
+
name: tool.name,
|
|
177
177
|
description: tool.description,
|
|
178
178
|
parameters: tool.inputSchema,
|
|
179
179
|
},
|
|
@@ -181,7 +181,7 @@ async function refreshToolsCache() {
|
|
|
181
181
|
}
|
|
182
182
|
// Add built-in IDE Diagnostics tools (always available)
|
|
183
183
|
const ideDiagnosticsServiceTools = ideDiagnosticsTools.map(tool => ({
|
|
184
|
-
name: tool.name.replace('
|
|
184
|
+
name: tool.name.replace('ide-', ''),
|
|
185
185
|
description: tool.description,
|
|
186
186
|
inputSchema: tool.inputSchema,
|
|
187
187
|
}));
|
|
@@ -195,7 +195,7 @@ async function refreshToolsCache() {
|
|
|
195
195
|
allTools.push({
|
|
196
196
|
type: 'function',
|
|
197
197
|
function: {
|
|
198
|
-
name:
|
|
198
|
+
name: tool.name,
|
|
199
199
|
description: tool.description,
|
|
200
200
|
parameters: tool.inputSchema,
|
|
201
201
|
},
|
|
@@ -598,7 +598,7 @@ export async function executeMCPTool(toolName, args, abortSignal, onTokenUpdate)
|
|
|
598
598
|
}
|
|
599
599
|
if (serviceName === 'todo') {
|
|
600
600
|
// Handle built-in TODO tools (no connection needed)
|
|
601
|
-
return await todoService.executeTool(
|
|
601
|
+
return await todoService.executeTool(actualToolName, args);
|
|
602
602
|
}
|
|
603
603
|
else if (serviceName === 'filesystem') {
|
|
604
604
|
// Handle built-in filesystem tools (no connection needed)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Agent as HttpAgent } from 'http';
|
|
2
|
+
import type { Agent as HttpsAgent } from 'https';
|
|
3
|
+
/**
|
|
4
|
+
* 创建代理 Agent(如果启用了代理)
|
|
5
|
+
* @param targetUrl - 目标 URL,用于判断是否使用 HTTPS
|
|
6
|
+
* @returns HTTP/HTTPS Agent,如果未启用代理则返回 undefined
|
|
7
|
+
*/
|
|
8
|
+
export declare function createProxyAgent(targetUrl: string): HttpAgent | HttpsAgent | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* 为 fetch 请求添加代理支持
|
|
11
|
+
* @param url - 请求 URL
|
|
12
|
+
* @param options - fetch 选项
|
|
13
|
+
* @returns 添加了代理支持的 fetch 选项
|
|
14
|
+
*/
|
|
15
|
+
export declare function addProxyToFetchOptions(url: string, options?: RequestInit): RequestInit;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getProxyConfig } from './apiConfig.js';
|
|
2
|
+
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
3
|
+
import { HttpProxyAgent } from 'http-proxy-agent';
|
|
4
|
+
/**
|
|
5
|
+
* 创建代理 Agent(如果启用了代理)
|
|
6
|
+
* @param targetUrl - 目标 URL,用于判断是否使用 HTTPS
|
|
7
|
+
* @returns HTTP/HTTPS Agent,如果未启用代理则返回 undefined
|
|
8
|
+
*/
|
|
9
|
+
export function createProxyAgent(targetUrl) {
|
|
10
|
+
const proxyConfig = getProxyConfig();
|
|
11
|
+
// 如果代理未启用,直接返回 undefined
|
|
12
|
+
if (!proxyConfig.enabled) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
// 构建代理 URL
|
|
16
|
+
const proxyUrl = `http://127.0.0.1:${proxyConfig.port}`;
|
|
17
|
+
// 根据目标 URL 协议选择合适的代理 Agent
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL(targetUrl);
|
|
20
|
+
if (url.protocol === 'https:') {
|
|
21
|
+
return new HttpsProxyAgent(proxyUrl);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
return new HttpProxyAgent(proxyUrl);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
// URL 解析失败,默认使用 HTTPS
|
|
29
|
+
return new HttpsProxyAgent(proxyUrl);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 为 fetch 请求添加代理支持
|
|
34
|
+
* @param url - 请求 URL
|
|
35
|
+
* @param options - fetch 选项
|
|
36
|
+
* @returns 添加了代理支持的 fetch 选项
|
|
37
|
+
*/
|
|
38
|
+
export function addProxyToFetchOptions(url, options = {}) {
|
|
39
|
+
const agent = createProxyAgent(url);
|
|
40
|
+
if (!agent) {
|
|
41
|
+
return options;
|
|
42
|
+
}
|
|
43
|
+
// 添加 agent 到 fetch 选项
|
|
44
|
+
// 注意:Node.js 的 fetch 支持 dispatcher 选项
|
|
45
|
+
return {
|
|
46
|
+
...options,
|
|
47
|
+
// @ts-ignore - Node.js fetch 支持 agent
|
|
48
|
+
agent,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -20,3 +20,30 @@ export declare function withRetry<T>(fn: () => Promise<T>, options?: RetryOption
|
|
|
20
20
|
* 注意:如果生成器已经开始产生数据,则不会重试
|
|
21
21
|
*/
|
|
22
22
|
export declare function withRetryGenerator<T>(fn: () => AsyncGenerator<T, void, unknown>, options?: RetryOptions): AsyncGenerator<T, void, unknown>;
|
|
23
|
+
/**
|
|
24
|
+
* JSON 解析结果
|
|
25
|
+
*/
|
|
26
|
+
export interface JsonParseResult<T = any> {
|
|
27
|
+
success: boolean;
|
|
28
|
+
data?: T;
|
|
29
|
+
error?: Error;
|
|
30
|
+
wasFixed?: boolean;
|
|
31
|
+
originalJson?: string;
|
|
32
|
+
fixedJson?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 尝试解析 JSON,如果失败则尝试修复常见的 JSON 错误
|
|
36
|
+
* @param jsonString - 要解析的 JSON 字符串
|
|
37
|
+
* @param options - 配置选项
|
|
38
|
+
* @returns 解析结果
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseJsonWithFix<T = any>(jsonString: string, options?: {
|
|
41
|
+
/** 是否在修复成功时记录警告 */
|
|
42
|
+
logWarning?: boolean;
|
|
43
|
+
/** 是否在修复失败时记录错误 */
|
|
44
|
+
logError?: boolean;
|
|
45
|
+
/** 工具名称(用于日志) */
|
|
46
|
+
toolName?: string;
|
|
47
|
+
/** 失败时的回退值 */
|
|
48
|
+
fallbackValue?: T;
|
|
49
|
+
}): JsonParseResult<T>;
|
package/dist/utils/retryUtils.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* - 延时递增策略 (1s, 2s, 4s, 8s, 16s)
|
|
6
6
|
* - 支持 AbortSignal 中断
|
|
7
7
|
*/
|
|
8
|
+
import { logger } from './logger.js';
|
|
8
9
|
/**
|
|
9
10
|
* 延时函数,支持 AbortSignal 中断
|
|
10
11
|
*/
|
|
@@ -48,8 +49,11 @@ function isRetriableError(error) {
|
|
|
48
49
|
errorMessage.includes('429')) {
|
|
49
50
|
return true;
|
|
50
51
|
}
|
|
51
|
-
// Server errors
|
|
52
|
-
if (errorMessage.includes('
|
|
52
|
+
// Server errors
|
|
53
|
+
if (errorMessage.includes('400') ||
|
|
54
|
+
errorMessage.includes('403') ||
|
|
55
|
+
errorMessage.includes('405') ||
|
|
56
|
+
errorMessage.includes('500') ||
|
|
53
57
|
errorMessage.includes('502') ||
|
|
54
58
|
errorMessage.includes('503') ||
|
|
55
59
|
errorMessage.includes('504') ||
|
|
@@ -189,3 +193,111 @@ export async function* withRetryGenerator(fn, options = {}) {
|
|
|
189
193
|
// 不应该到达这里
|
|
190
194
|
throw lastError || new Error('Retry failed');
|
|
191
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* 尝试解析 JSON,如果失败则尝试修复常见的 JSON 错误
|
|
198
|
+
* @param jsonString - 要解析的 JSON 字符串
|
|
199
|
+
* @param options - 配置选项
|
|
200
|
+
* @returns 解析结果
|
|
201
|
+
*/
|
|
202
|
+
export function parseJsonWithFix(jsonString, options = {}) {
|
|
203
|
+
const { logWarning = true, logError = true, toolName = 'unknown', fallbackValue, } = options;
|
|
204
|
+
// 首先尝试直接解析
|
|
205
|
+
try {
|
|
206
|
+
const data = JSON.parse(jsonString);
|
|
207
|
+
return { success: true, data };
|
|
208
|
+
}
|
|
209
|
+
catch (originalError) {
|
|
210
|
+
// 解析失败,尝试修复
|
|
211
|
+
let fixedJson = jsonString;
|
|
212
|
+
let wasFixed = false;
|
|
213
|
+
// Fix 1: 移除格式错误的模式,如 "endLine":685 ": ""
|
|
214
|
+
// 处理值后面有额外冒号和引号的情况
|
|
215
|
+
const malformedPattern = /(\"[\w]+\"\s*:\s*[^,}\]]+)\s*\":\s*\"[^\"]*\"/g;
|
|
216
|
+
if (malformedPattern.test(fixedJson)) {
|
|
217
|
+
fixedJson = fixedJson.replace(malformedPattern, '$1');
|
|
218
|
+
wasFixed = true;
|
|
219
|
+
}
|
|
220
|
+
// Fix 2: 移除闭合括号前的尾随逗号
|
|
221
|
+
if (/,(\s*[}\]])/.test(fixedJson)) {
|
|
222
|
+
fixedJson = fixedJson.replace(/,(\s*[}\]])/g, '$1');
|
|
223
|
+
wasFixed = true;
|
|
224
|
+
}
|
|
225
|
+
// Fix 3: 修复属性名缺少引号的问题
|
|
226
|
+
if (/{\s*\w+\s*:/.test(fixedJson)) {
|
|
227
|
+
fixedJson = fixedJson.replace(/{\s*(\w+)\s*:/g, '{"$1":');
|
|
228
|
+
fixedJson = fixedJson.replace(/,\s*(\w+)\s*:/g, ',"$1":');
|
|
229
|
+
wasFixed = true;
|
|
230
|
+
}
|
|
231
|
+
// Fix 4: 添加缺失的闭合括号
|
|
232
|
+
const openBraces = (fixedJson.match(/{/g) || []).length;
|
|
233
|
+
const closeBraces = (fixedJson.match(/}/g) || []).length;
|
|
234
|
+
const openBrackets = (fixedJson.match(/\[/g) || []).length;
|
|
235
|
+
const closeBrackets = (fixedJson.match(/\]/g) || []).length;
|
|
236
|
+
if (openBraces > closeBraces) {
|
|
237
|
+
fixedJson += '}'.repeat(openBraces - closeBraces);
|
|
238
|
+
wasFixed = true;
|
|
239
|
+
}
|
|
240
|
+
if (openBrackets > closeBrackets) {
|
|
241
|
+
fixedJson += ']'.repeat(openBrackets - closeBrackets);
|
|
242
|
+
wasFixed = true;
|
|
243
|
+
}
|
|
244
|
+
// Fix 5: 移除多余的闭合括号
|
|
245
|
+
if (closeBraces > openBraces) {
|
|
246
|
+
const extraBraces = closeBraces - openBraces;
|
|
247
|
+
for (let i = 0; i < extraBraces; i++) {
|
|
248
|
+
fixedJson = fixedJson.replace(/}([^}]*)$/, '$1');
|
|
249
|
+
}
|
|
250
|
+
wasFixed = true;
|
|
251
|
+
}
|
|
252
|
+
if (closeBrackets > openBrackets) {
|
|
253
|
+
const extraBrackets = closeBrackets - openBrackets;
|
|
254
|
+
for (let i = 0; i < extraBrackets; i++) {
|
|
255
|
+
fixedJson = fixedJson.replace(/\]([^\]]*)$/, '$1');
|
|
256
|
+
}
|
|
257
|
+
wasFixed = true;
|
|
258
|
+
}
|
|
259
|
+
// 尝试解析修复后的 JSON
|
|
260
|
+
try {
|
|
261
|
+
const data = JSON.parse(fixedJson);
|
|
262
|
+
if (wasFixed && logWarning) {
|
|
263
|
+
logger.warn(`Warning: Fixed malformed JSON for ${toolName}`);
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
success: true,
|
|
267
|
+
data,
|
|
268
|
+
wasFixed,
|
|
269
|
+
originalJson: jsonString,
|
|
270
|
+
fixedJson,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
catch (fixError) {
|
|
274
|
+
// 修复失败
|
|
275
|
+
if (logError) {
|
|
276
|
+
logger.error(`Error: Failed to parse JSON for ${toolName}`);
|
|
277
|
+
logger.error(`Original: ${jsonString}`);
|
|
278
|
+
if (wasFixed) {
|
|
279
|
+
logger.error(`After fixes: ${fixedJson}`);
|
|
280
|
+
}
|
|
281
|
+
logger.error(`Parse error: ${fixError instanceof Error ? fixError.message : 'Unknown'}`);
|
|
282
|
+
}
|
|
283
|
+
// 如果提供了回退值,使用回退值
|
|
284
|
+
if (fallbackValue !== undefined) {
|
|
285
|
+
return {
|
|
286
|
+
success: false,
|
|
287
|
+
data: fallbackValue,
|
|
288
|
+
error: fixError instanceof Error ? fixError : new Error(String(fixError)),
|
|
289
|
+
wasFixed,
|
|
290
|
+
originalJson: jsonString,
|
|
291
|
+
fixedJson: wasFixed ? fixedJson : undefined,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
success: false,
|
|
296
|
+
error: fixError instanceof Error ? fixError : new Error(String(fixError)),
|
|
297
|
+
wasFixed,
|
|
298
|
+
originalJson: jsonString,
|
|
299
|
+
fixedJson: wasFixed ? fixedJson : undefined,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -22,17 +22,14 @@ export interface SessionListItem {
|
|
|
22
22
|
declare class SessionManager {
|
|
23
23
|
private readonly sessionsDir;
|
|
24
24
|
private currentSession;
|
|
25
|
-
private summaryAbortController;
|
|
26
|
-
private summaryTimeoutId;
|
|
27
25
|
constructor();
|
|
28
26
|
private ensureSessionsDir;
|
|
29
27
|
private getSessionPath;
|
|
30
28
|
private formatDateForFolder;
|
|
31
29
|
/**
|
|
32
|
-
*
|
|
33
|
-
* This prevents wasted resources and race conditions
|
|
30
|
+
* Clean title by removing newlines and extra spaces
|
|
34
31
|
*/
|
|
35
|
-
private
|
|
32
|
+
private cleanTitle;
|
|
36
33
|
createNewSession(): Promise<Session>;
|
|
37
34
|
saveSession(session: Session): Promise<void>;
|
|
38
35
|
loadSession(sessionId: string): Promise<Session | null>;
|
|
@@ -2,8 +2,8 @@ import fs from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import { randomUUID } from 'crypto';
|
|
5
|
-
import { summaryAgent } from '../agents/summaryAgent.js';
|
|
6
5
|
import { getTodoService } from './mcpToolsManager.js';
|
|
6
|
+
import { logger } from './logger.js';
|
|
7
7
|
class SessionManager {
|
|
8
8
|
constructor() {
|
|
9
9
|
Object.defineProperty(this, "sessionsDir", {
|
|
@@ -18,18 +18,6 @@ class SessionManager {
|
|
|
18
18
|
writable: true,
|
|
19
19
|
value: null
|
|
20
20
|
});
|
|
21
|
-
Object.defineProperty(this, "summaryAbortController", {
|
|
22
|
-
enumerable: true,
|
|
23
|
-
configurable: true,
|
|
24
|
-
writable: true,
|
|
25
|
-
value: null
|
|
26
|
-
});
|
|
27
|
-
Object.defineProperty(this, "summaryTimeoutId", {
|
|
28
|
-
enumerable: true,
|
|
29
|
-
configurable: true,
|
|
30
|
-
writable: true,
|
|
31
|
-
value: null
|
|
32
|
-
});
|
|
33
21
|
this.sessionsDir = path.join(os.homedir(), '.snow', 'sessions');
|
|
34
22
|
}
|
|
35
23
|
async ensureSessionsDir(date) {
|
|
@@ -58,18 +46,13 @@ class SessionManager {
|
|
|
58
46
|
return `${year}-${month}-${day}`;
|
|
59
47
|
}
|
|
60
48
|
/**
|
|
61
|
-
*
|
|
62
|
-
* This prevents wasted resources and race conditions
|
|
49
|
+
* Clean title by removing newlines and extra spaces
|
|
63
50
|
*/
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (this.summaryTimeoutId) {
|
|
70
|
-
clearTimeout(this.summaryTimeoutId);
|
|
71
|
-
this.summaryTimeoutId = null;
|
|
72
|
-
}
|
|
51
|
+
cleanTitle(title) {
|
|
52
|
+
return title
|
|
53
|
+
.replace(/[\r\n]+/g, ' ') // Replace newlines with space
|
|
54
|
+
.replace(/\s+/g, ' ') // Replace multiple spaces with single space
|
|
55
|
+
.trim(); // Remove leading/trailing spaces
|
|
73
56
|
}
|
|
74
57
|
async createNewSession() {
|
|
75
58
|
await this.ensureSessionsDir(new Date());
|
|
@@ -165,7 +148,7 @@ class SessionManager {
|
|
|
165
148
|
const session = JSON.parse(data);
|
|
166
149
|
sessions.push({
|
|
167
150
|
id: session.id,
|
|
168
|
-
title: session.title,
|
|
151
|
+
title: this.cleanTitle(session.title),
|
|
169
152
|
summary: session.summary,
|
|
170
153
|
createdAt: session.createdAt,
|
|
171
154
|
updatedAt: session.updatedAt,
|
|
@@ -196,7 +179,7 @@ class SessionManager {
|
|
|
196
179
|
const session = JSON.parse(data);
|
|
197
180
|
sessions.push({
|
|
198
181
|
id: session.id,
|
|
199
|
-
title: session.title,
|
|
182
|
+
title: this.cleanTitle(session.title),
|
|
200
183
|
summary: session.summary,
|
|
201
184
|
createdAt: session.createdAt,
|
|
202
185
|
updatedAt: session.updatedAt,
|
|
@@ -260,59 +243,13 @@ class SessionManager {
|
|
|
260
243
|
this.currentSession.messages.push(message);
|
|
261
244
|
this.currentSession.messageCount = this.currentSession.messages.length;
|
|
262
245
|
this.currentSession.updatedAt = Date.now();
|
|
263
|
-
// Generate summary from first user message
|
|
246
|
+
// Generate simple title and summary from first user message
|
|
264
247
|
if (this.currentSession.messageCount === 1 && message.role === 'user') {
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
this.
|
|
270
|
-
// Create new AbortController for this summary generation
|
|
271
|
-
this.summaryAbortController = new AbortController();
|
|
272
|
-
const currentSessionId = this.currentSession.id;
|
|
273
|
-
const abortSignal = this.summaryAbortController.signal;
|
|
274
|
-
// Set timeout to cancel summary generation after 30 seconds (防呆机制)
|
|
275
|
-
this.summaryTimeoutId = setTimeout(() => {
|
|
276
|
-
if (this.summaryAbortController) {
|
|
277
|
-
console.warn('Summary generation timeout after 30s, aborting...');
|
|
278
|
-
this.summaryAbortController.abort();
|
|
279
|
-
this.summaryAbortController = null;
|
|
280
|
-
}
|
|
281
|
-
}, 30000);
|
|
282
|
-
// Generate better summary in parallel (non-blocking)
|
|
283
|
-
// This won't delay the main conversation flow
|
|
284
|
-
summaryAgent
|
|
285
|
-
.generateSummary(message.content, abortSignal)
|
|
286
|
-
.then(summary => {
|
|
287
|
-
// 防呆检查:确保会话没有被切换,且仍然是第一条消息
|
|
288
|
-
if (this.currentSession &&
|
|
289
|
-
this.currentSession.id === currentSessionId &&
|
|
290
|
-
this.currentSession.messageCount === 1) {
|
|
291
|
-
// Only update if this is still the first message in the same session
|
|
292
|
-
this.currentSession.title = summary;
|
|
293
|
-
this.currentSession.summary = summary;
|
|
294
|
-
this.saveSession(this.currentSession).catch(error => {
|
|
295
|
-
console.error('Failed to save session with generated summary:', error);
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
// Clean up
|
|
299
|
-
this.cancelOngoingSummaryGeneration();
|
|
300
|
-
})
|
|
301
|
-
.catch(error => {
|
|
302
|
-
// Clean up on error
|
|
303
|
-
this.cancelOngoingSummaryGeneration();
|
|
304
|
-
// Silently fail if aborted (expected behavior)
|
|
305
|
-
if (error.name === 'AbortError' || abortSignal.aborted) {
|
|
306
|
-
console.log('Summary generation cancelled (expected)');
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
// Log other errors - we already have a fallback title/summary
|
|
310
|
-
console.warn('Summary generation failed, using fallback:', error);
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
else if (this.currentSession.messageCount > 1) {
|
|
314
|
-
// 防呆机制:如果不是第一条消息,取消任何正在进行的摘要生成
|
|
315
|
-
this.cancelOngoingSummaryGeneration();
|
|
248
|
+
// Use first 50 chars as title, first 100 chars as summary
|
|
249
|
+
const title = message.content.slice(0, 50) + (message.content.length > 50 ? '...' : '');
|
|
250
|
+
const summary = message.content.slice(0, 100) + (message.content.length > 100 ? '...' : '');
|
|
251
|
+
this.currentSession.title = this.cleanTitle(title);
|
|
252
|
+
this.currentSession.summary = this.cleanTitle(summary);
|
|
316
253
|
}
|
|
317
254
|
await this.saveSession(this.currentSession);
|
|
318
255
|
}
|
|
@@ -320,13 +257,9 @@ class SessionManager {
|
|
|
320
257
|
return this.currentSession;
|
|
321
258
|
}
|
|
322
259
|
setCurrentSession(session) {
|
|
323
|
-
// 防呆机制:切换会话时取消正在进行的摘要生成
|
|
324
|
-
this.cancelOngoingSummaryGeneration();
|
|
325
260
|
this.currentSession = session;
|
|
326
261
|
}
|
|
327
262
|
clearCurrentSession() {
|
|
328
|
-
// 防呆机制:清除会话时取消正在进行的摘要生成
|
|
329
|
-
this.cancelOngoingSummaryGeneration();
|
|
330
263
|
this.currentSession = null;
|
|
331
264
|
}
|
|
332
265
|
async deleteSession(sessionId) {
|
|
@@ -374,7 +307,7 @@ class SessionManager {
|
|
|
374
307
|
}
|
|
375
308
|
catch (error) {
|
|
376
309
|
// TODO删除失败不影响会话删除结果
|
|
377
|
-
|
|
310
|
+
logger.warn(`Failed to delete TODO list for session ${sessionId}:`, error);
|
|
378
311
|
}
|
|
379
312
|
}
|
|
380
313
|
return sessionDeleted;
|
package/dist/utils/terminal.js
CHANGED
|
@@ -6,7 +6,8 @@ export function resetTerminal(stream) {
|
|
|
6
6
|
// RIS (Reset to Initial State) clears scrollback and resets terminal modes
|
|
7
7
|
target.write('\x1bc');
|
|
8
8
|
target.write('\x1B[3J\x1B[2J\x1B[H');
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// This
|
|
9
|
+
// Re-enable focus reporting immediately after terminal reset
|
|
10
|
+
// The RIS command (\x1bc) disables focus reporting, so we must re-enable it
|
|
11
|
+
// This ensures focus state tracking continues to work after /clear command
|
|
12
|
+
target.write('\x1b[?1004h');
|
|
12
13
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Save usage data to file system
|
|
3
|
+
* This is called directly from API layers to ensure all usage is tracked
|
|
4
|
+
*/
|
|
5
|
+
export declare function saveUsageToFile(model: string, usage: {
|
|
6
|
+
prompt_tokens?: number;
|
|
7
|
+
completion_tokens?: number;
|
|
8
|
+
cache_creation_input_tokens?: number;
|
|
9
|
+
cache_read_input_tokens?: number;
|
|
10
|
+
cached_tokens?: number;
|
|
11
|
+
}): void;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
|
5
|
+
// 队列来避免并发写入冲突
|
|
6
|
+
let writeQueue = Promise.resolve();
|
|
7
|
+
async function getActiveProfile() {
|
|
8
|
+
try {
|
|
9
|
+
const homeDir = os.homedir();
|
|
10
|
+
const profilePath = path.join(homeDir, '.snow', 'active-profile.txt');
|
|
11
|
+
const profileName = await fs.readFile(profilePath, 'utf-8');
|
|
12
|
+
return profileName.trim();
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
return 'default';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function getUsageDir() {
|
|
19
|
+
const homeDir = os.homedir();
|
|
20
|
+
const snowDir = path.join(homeDir, '.snow', 'usage');
|
|
21
|
+
const today = new Date().toISOString().split('T')[0] || ''; // YYYY-MM-DD
|
|
22
|
+
const dateDir = path.join(snowDir, today);
|
|
23
|
+
// 确保目录存在
|
|
24
|
+
try {
|
|
25
|
+
await fs.mkdir(dateDir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
// 目录可能已存在,忽略错误
|
|
29
|
+
}
|
|
30
|
+
return dateDir;
|
|
31
|
+
}
|
|
32
|
+
async function getCurrentLogFile(dateDir) {
|
|
33
|
+
try {
|
|
34
|
+
const files = (await fs.readdir(dateDir)).filter(f => f.startsWith('usage-') && f.endsWith('.jsonl'));
|
|
35
|
+
if (files.length === 0) {
|
|
36
|
+
return path.join(dateDir, 'usage-001.jsonl');
|
|
37
|
+
}
|
|
38
|
+
// 按文件名排序,获取最新的文件
|
|
39
|
+
files.sort();
|
|
40
|
+
const latestFileName = files[files.length - 1];
|
|
41
|
+
if (!latestFileName) {
|
|
42
|
+
return path.join(dateDir, 'usage-001.jsonl');
|
|
43
|
+
}
|
|
44
|
+
const latestFile = path.join(dateDir, latestFileName);
|
|
45
|
+
// 检查文件大小
|
|
46
|
+
const stats = await fs.stat(latestFile);
|
|
47
|
+
if (stats.size >= MAX_FILE_SIZE) {
|
|
48
|
+
// 创建新文件
|
|
49
|
+
const match = latestFileName.match(/usage-(\d+)\.jsonl/);
|
|
50
|
+
const nextNum = match && match[1] ? parseInt(match[1]) + 1 : 1;
|
|
51
|
+
return path.join(dateDir, `usage-${String(nextNum).padStart(3, '0')}.jsonl`);
|
|
52
|
+
}
|
|
53
|
+
return latestFile;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
// 如果目录不存在或读取失败,返回默认文件名
|
|
57
|
+
return path.join(dateDir, 'usage-001.jsonl');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Save usage data to file system
|
|
62
|
+
* This is called directly from API layers to ensure all usage is tracked
|
|
63
|
+
*/
|
|
64
|
+
export function saveUsageToFile(model, usage) {
|
|
65
|
+
// Add to write queue to avoid concurrent writes
|
|
66
|
+
writeQueue = writeQueue
|
|
67
|
+
.then(async () => {
|
|
68
|
+
try {
|
|
69
|
+
const profileName = await getActiveProfile();
|
|
70
|
+
const dateDir = await getUsageDir();
|
|
71
|
+
const logFile = await getCurrentLogFile(dateDir);
|
|
72
|
+
// Extract cache tokens (different API formats)
|
|
73
|
+
const cacheReadTokens = usage.cache_read_input_tokens ?? usage.cached_tokens;
|
|
74
|
+
// Only save non-sensitive data: model name, profile, and token counts
|
|
75
|
+
const record = {
|
|
76
|
+
model,
|
|
77
|
+
profileName,
|
|
78
|
+
inputTokens: usage.prompt_tokens || 0,
|
|
79
|
+
outputTokens: usage.completion_tokens || 0,
|
|
80
|
+
...(usage.cache_creation_input_tokens !== undefined && {
|
|
81
|
+
cacheCreationInputTokens: usage.cache_creation_input_tokens,
|
|
82
|
+
}),
|
|
83
|
+
...(cacheReadTokens !== undefined && {
|
|
84
|
+
cacheReadInputTokens: cacheReadTokens,
|
|
85
|
+
}),
|
|
86
|
+
timestamp: new Date().toISOString(),
|
|
87
|
+
};
|
|
88
|
+
// Append to file (JSONL format: one JSON object per line)
|
|
89
|
+
const line = JSON.stringify(record) + '\n';
|
|
90
|
+
await fs.appendFile(logFile, line, 'utf-8');
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.error('Failed to save usage data:', error);
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
.catch(error => {
|
|
97
|
+
console.error('Usage persistence queue error:', error);
|
|
98
|
+
});
|
|
99
|
+
}
|