snow-ai 0.1.12 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/chat.d.ts +65 -2
- package/dist/api/chat.js +299 -16
- package/dist/api/responses.d.ts +52 -0
- package/dist/api/responses.js +541 -0
- package/dist/api/systemPrompt.d.ts +4 -0
- package/dist/api/systemPrompt.js +43 -0
- package/dist/app.js +15 -4
- package/dist/cli.js +17 -1
- package/dist/hooks/useConversation.d.ts +32 -0
- package/dist/hooks/useConversation.js +403 -0
- package/dist/hooks/useGlobalNavigation.d.ts +6 -0
- package/dist/hooks/useGlobalNavigation.js +15 -0
- package/dist/hooks/useSessionManagement.d.ts +10 -0
- package/dist/hooks/useSessionManagement.js +43 -0
- package/dist/hooks/useSessionSave.d.ts +8 -0
- package/dist/hooks/useSessionSave.js +52 -0
- package/dist/hooks/useToolConfirmation.d.ts +18 -0
- package/dist/hooks/useToolConfirmation.js +49 -0
- package/dist/mcp/bash.d.ts +57 -0
- package/dist/mcp/bash.js +138 -0
- package/dist/mcp/filesystem.d.ts +307 -0
- package/dist/mcp/filesystem.js +520 -0
- package/dist/mcp/todo.d.ts +55 -0
- package/dist/mcp/todo.js +329 -0
- package/dist/test/logger-test.d.ts +1 -0
- package/dist/test/logger-test.js +7 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/ui/components/ChatInput.d.ts +15 -2
- package/dist/ui/components/ChatInput.js +445 -59
- package/dist/ui/components/CommandPanel.d.ts +2 -2
- package/dist/ui/components/CommandPanel.js +11 -7
- package/dist/ui/components/DiffViewer.d.ts +9 -0
- package/dist/ui/components/DiffViewer.js +93 -0
- package/dist/ui/components/FileList.d.ts +14 -0
- package/dist/ui/components/FileList.js +131 -0
- package/dist/ui/components/MCPInfoPanel.d.ts +2 -0
- package/dist/ui/components/MCPInfoPanel.js +74 -0
- package/dist/ui/components/MCPInfoScreen.d.ts +7 -0
- package/dist/ui/components/MCPInfoScreen.js +27 -0
- package/dist/ui/components/MarkdownRenderer.d.ts +7 -0
- package/dist/ui/components/MarkdownRenderer.js +110 -0
- package/dist/ui/components/Menu.d.ts +5 -2
- package/dist/ui/components/Menu.js +60 -9
- package/dist/ui/components/MessageList.d.ts +30 -2
- package/dist/ui/components/MessageList.js +64 -12
- package/dist/ui/components/PendingMessages.js +1 -1
- package/dist/ui/components/ScrollableSelectInput.d.ts +29 -0
- package/dist/ui/components/ScrollableSelectInput.js +157 -0
- package/dist/ui/components/SessionListScreen.d.ts +7 -0
- package/dist/ui/components/SessionListScreen.js +196 -0
- package/dist/ui/components/SessionListScreenWrapper.d.ts +7 -0
- package/dist/ui/components/SessionListScreenWrapper.js +14 -0
- package/dist/ui/components/TodoTree.d.ts +15 -0
- package/dist/ui/components/TodoTree.js +60 -0
- package/dist/ui/components/ToolConfirmation.d.ts +8 -0
- package/dist/ui/components/ToolConfirmation.js +38 -0
- package/dist/ui/components/ToolResultPreview.d.ts +12 -0
- package/dist/ui/components/ToolResultPreview.js +115 -0
- package/dist/ui/pages/ChatScreen.d.ts +4 -0
- package/dist/ui/pages/ChatScreen.js +385 -196
- package/dist/ui/pages/MCPConfigScreen.d.ts +6 -0
- package/dist/ui/pages/MCPConfigScreen.js +55 -0
- package/dist/ui/pages/ModelConfigScreen.js +73 -12
- package/dist/ui/pages/WelcomeScreen.js +17 -11
- package/dist/utils/apiConfig.d.ts +12 -0
- package/dist/utils/apiConfig.js +95 -9
- package/dist/utils/commandExecutor.d.ts +2 -1
- package/dist/utils/commands/init.d.ts +2 -0
- package/dist/utils/commands/init.js +93 -0
- package/dist/utils/commands/mcp.d.ts +2 -0
- package/dist/utils/commands/mcp.js +12 -0
- package/dist/utils/commands/resume.d.ts +2 -0
- package/dist/utils/commands/resume.js +12 -0
- package/dist/utils/commands/yolo.d.ts +2 -0
- package/dist/utils/commands/yolo.js +12 -0
- package/dist/utils/fileUtils.d.ts +44 -0
- package/dist/utils/fileUtils.js +222 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/logger.d.ts +31 -0
- package/dist/utils/logger.js +97 -0
- package/dist/utils/mcpToolsManager.d.ts +47 -0
- package/dist/utils/mcpToolsManager.js +476 -0
- package/dist/utils/messageFormatter.d.ts +12 -0
- package/dist/utils/messageFormatter.js +32 -0
- package/dist/utils/sessionConverter.d.ts +6 -0
- package/dist/utils/sessionConverter.js +61 -0
- package/dist/utils/sessionManager.d.ts +39 -0
- package/dist/utils/sessionManager.js +141 -0
- package/dist/utils/textBuffer.d.ts +36 -7
- package/dist/utils/textBuffer.js +265 -179
- package/dist/utils/todoPreprocessor.d.ts +5 -0
- package/dist/utils/todoPreprocessor.js +19 -0
- package/dist/utils/toolExecutor.d.ts +21 -0
- package/dist/utils/toolExecutor.js +28 -0
- package/package.json +12 -3
- package/readme.md +2 -2
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { getOpenAiConfig } from '../utils/apiConfig.js';
|
|
3
|
+
import { executeMCPTool } from '../utils/mcpToolsManager.js';
|
|
4
|
+
import { SYSTEM_PROMPT } from './systemPrompt.js';
|
|
5
|
+
/**
|
|
6
|
+
* 确保 schema 符合 Responses API 的要求:
|
|
7
|
+
* 1. additionalProperties: false
|
|
8
|
+
* 2. 保持原有的 required 数组(不修改)
|
|
9
|
+
*/
|
|
10
|
+
function ensureStrictSchema(schema) {
|
|
11
|
+
if (!schema) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
// 深拷贝 schema
|
|
15
|
+
const strictSchema = JSON.parse(JSON.stringify(schema));
|
|
16
|
+
if (strictSchema.type === 'object') {
|
|
17
|
+
// 添加 additionalProperties: false
|
|
18
|
+
strictSchema.additionalProperties = false;
|
|
19
|
+
// 递归处理嵌套的 object 属性
|
|
20
|
+
if (strictSchema.properties) {
|
|
21
|
+
for (const key of Object.keys(strictSchema.properties)) {
|
|
22
|
+
const prop = strictSchema.properties[key];
|
|
23
|
+
// 递归处理嵌套的 object
|
|
24
|
+
if (prop.type === 'object' || (Array.isArray(prop.type) && prop.type.includes('object'))) {
|
|
25
|
+
if (!('additionalProperties' in prop)) {
|
|
26
|
+
prop.additionalProperties = false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// 如果 properties 为空且有 required 字段,删除它
|
|
32
|
+
if (strictSchema.properties && Object.keys(strictSchema.properties).length === 0 && strictSchema.required) {
|
|
33
|
+
delete strictSchema.required;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return strictSchema;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 转换 Chat Completions 格式的工具为 Responses API 格式
|
|
40
|
+
* Chat Completions: {type: 'function', function: {name, description, parameters}}
|
|
41
|
+
* Responses API: {type: 'function', name, description, parameters, strict}
|
|
42
|
+
*/
|
|
43
|
+
function convertToolsForResponses(tools) {
|
|
44
|
+
if (!tools || tools.length === 0) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
return tools.map(tool => ({
|
|
48
|
+
type: 'function',
|
|
49
|
+
name: tool.function.name,
|
|
50
|
+
description: tool.function.description,
|
|
51
|
+
parameters: ensureStrictSchema(tool.function.parameters),
|
|
52
|
+
strict: false
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
let openaiClient = null;
|
|
56
|
+
function getOpenAIClient() {
|
|
57
|
+
if (!openaiClient) {
|
|
58
|
+
const config = getOpenAiConfig();
|
|
59
|
+
if (!config.apiKey || !config.baseUrl) {
|
|
60
|
+
throw new Error('OpenAI API configuration is incomplete. Please configure API settings first.');
|
|
61
|
+
}
|
|
62
|
+
openaiClient = new OpenAI({
|
|
63
|
+
apiKey: config.apiKey,
|
|
64
|
+
baseURL: config.baseUrl,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return openaiClient;
|
|
68
|
+
}
|
|
69
|
+
export function resetOpenAIClient() {
|
|
70
|
+
openaiClient = null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 转换消息格式为 Responses API 的 input 格式
|
|
74
|
+
* Responses API 的 input 格式:
|
|
75
|
+
* 1. 支持 user, assistant 角色消息,使用 type: "message" 包裹
|
|
76
|
+
* 2. 工具调用在 assistant 中表示为 function_call 类型的 item
|
|
77
|
+
* 3. 工具结果使用 function_call_output 类型
|
|
78
|
+
*
|
|
79
|
+
* 注意:Responses API 使用 instructions 字段代替 system 消息
|
|
80
|
+
* 优化:使用 type: "message" 包裹以提高缓存命中率
|
|
81
|
+
*/
|
|
82
|
+
function convertToResponseInput(messages) {
|
|
83
|
+
const result = [];
|
|
84
|
+
for (const msg of messages) {
|
|
85
|
+
if (!msg)
|
|
86
|
+
continue;
|
|
87
|
+
// 跳过 system 消息(在 createResponse 中使用 instructions 字段)
|
|
88
|
+
if (msg.role === 'system') {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// 用户消息:content 必须是数组格式,使用 type: "message" 包裹
|
|
92
|
+
if (msg.role === 'user') {
|
|
93
|
+
const contentParts = [];
|
|
94
|
+
// 添加文本内容
|
|
95
|
+
if (msg.content) {
|
|
96
|
+
contentParts.push({
|
|
97
|
+
type: 'input_text',
|
|
98
|
+
text: msg.content
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// 添加图片内容
|
|
102
|
+
if (msg.images && msg.images.length > 0) {
|
|
103
|
+
for (const image of msg.images) {
|
|
104
|
+
contentParts.push({
|
|
105
|
+
type: 'input_image',
|
|
106
|
+
image_url: image.data
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
result.push({
|
|
111
|
+
type: 'message',
|
|
112
|
+
role: 'user',
|
|
113
|
+
content: contentParts
|
|
114
|
+
});
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// Assistant 消息(带工具调用)
|
|
118
|
+
// 在 Responses API 中,需要将工具调用转换为 function_call 类型的独立项
|
|
119
|
+
if (msg.role === 'assistant' && msg.tool_calls && msg.tool_calls.length > 0) {
|
|
120
|
+
// 添加 assistant 文本内容(如果有)
|
|
121
|
+
if (msg.content) {
|
|
122
|
+
result.push({
|
|
123
|
+
type: 'message',
|
|
124
|
+
role: 'assistant',
|
|
125
|
+
content: [{
|
|
126
|
+
type: 'output_text',
|
|
127
|
+
text: msg.content
|
|
128
|
+
}]
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// 为每个工具调用添加 function_call 项
|
|
132
|
+
for (const toolCall of msg.tool_calls) {
|
|
133
|
+
result.push({
|
|
134
|
+
type: 'function_call',
|
|
135
|
+
call_id: toolCall.id,
|
|
136
|
+
name: toolCall.function.name,
|
|
137
|
+
arguments: toolCall.function.arguments
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
// Assistant 消息(纯文本)
|
|
143
|
+
if (msg.role === 'assistant') {
|
|
144
|
+
result.push({
|
|
145
|
+
type: 'message',
|
|
146
|
+
role: 'assistant',
|
|
147
|
+
content: [{
|
|
148
|
+
type: 'output_text',
|
|
149
|
+
text: msg.content || ''
|
|
150
|
+
}]
|
|
151
|
+
});
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
// Tool 消息:转换为 function_call_output
|
|
155
|
+
if (msg.role === 'tool' && msg.tool_call_id) {
|
|
156
|
+
result.push({
|
|
157
|
+
type: 'function_call_output',
|
|
158
|
+
call_id: msg.tool_call_id,
|
|
159
|
+
output: msg.content
|
|
160
|
+
});
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 使用 Responses API 创建响应(非流式,带自动工具调用)
|
|
168
|
+
*/
|
|
169
|
+
export async function createResponse(options) {
|
|
170
|
+
const client = getOpenAIClient();
|
|
171
|
+
let messages = [...options.messages];
|
|
172
|
+
// 提取系统提示词(如果存在)
|
|
173
|
+
let systemInstructions = SYSTEM_PROMPT;
|
|
174
|
+
const firstMessage = messages[0];
|
|
175
|
+
if (firstMessage?.role === 'system') {
|
|
176
|
+
systemInstructions = firstMessage.content;
|
|
177
|
+
messages = messages.slice(1); // 移除系统消息
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
// 使用 Responses API
|
|
181
|
+
while (true) {
|
|
182
|
+
const requestPayload = {
|
|
183
|
+
model: options.model,
|
|
184
|
+
instructions: systemInstructions, // Responses API 使用 instructions 代替 system 消息
|
|
185
|
+
input: convertToResponseInput(messages),
|
|
186
|
+
tools: convertToolsForResponses(options.tools),
|
|
187
|
+
tool_choice: options.tool_choice,
|
|
188
|
+
reasoning: options.reasoning || { summary: 'auto', effort: 'high' },
|
|
189
|
+
store: options.store ?? false, // 默认不存储对话历史,提高缓存命中
|
|
190
|
+
include: options.include || ['reasoning.encrypted_content'], // 包含加密推理内容
|
|
191
|
+
prompt_cache_key: options.prompt_cache_key, // 缓存键(可选)
|
|
192
|
+
};
|
|
193
|
+
const response = await client.responses.create(requestPayload);
|
|
194
|
+
// 提取响应 - Responses API 返回 output 数组
|
|
195
|
+
const output = response.output;
|
|
196
|
+
if (!output || output.length === 0) {
|
|
197
|
+
throw new Error('No output from AI');
|
|
198
|
+
}
|
|
199
|
+
// 获取最后一条消息(通常是 assistant 的响应)
|
|
200
|
+
const lastMessage = output[output.length - 1];
|
|
201
|
+
// 添加 assistant 消息到对话
|
|
202
|
+
messages.push({
|
|
203
|
+
role: 'assistant',
|
|
204
|
+
content: lastMessage.content || '',
|
|
205
|
+
tool_calls: lastMessage.tool_calls
|
|
206
|
+
});
|
|
207
|
+
// 检查是否有工具调用
|
|
208
|
+
if (lastMessage.tool_calls && lastMessage.tool_calls.length > 0) {
|
|
209
|
+
// 执行每个工具调用
|
|
210
|
+
for (const toolCall of lastMessage.tool_calls) {
|
|
211
|
+
if (toolCall.type === 'function') {
|
|
212
|
+
try {
|
|
213
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
214
|
+
const result = await executeMCPTool(toolCall.function.name, args);
|
|
215
|
+
// 添加工具结果到对话
|
|
216
|
+
messages.push({
|
|
217
|
+
role: 'tool',
|
|
218
|
+
content: JSON.stringify(result),
|
|
219
|
+
tool_call_id: toolCall.id
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
// 添加错误结果到对话
|
|
224
|
+
messages.push({
|
|
225
|
+
role: 'tool',
|
|
226
|
+
content: `Error: ${error instanceof Error ? error.message : 'Tool execution failed'}`,
|
|
227
|
+
tool_call_id: toolCall.id
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// 继续对话获取工具结果后的响应
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
// 没有工具调用,返回内容
|
|
236
|
+
return lastMessage.content || '';
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
if (error instanceof Error) {
|
|
241
|
+
// 检查是否是 API 网关不支持 Responses API
|
|
242
|
+
if (error.message.includes('Panic detected') ||
|
|
243
|
+
error.message.includes('nil pointer') ||
|
|
244
|
+
error.message.includes('404') ||
|
|
245
|
+
error.message.includes('not found')) {
|
|
246
|
+
throw new Error('Response creation failed: Your API endpoint does not support the Responses API. ' +
|
|
247
|
+
'Please switch to "Chat Completions" method in API settings, or use an OpenAI-compatible endpoint that supports Responses API.');
|
|
248
|
+
}
|
|
249
|
+
throw new Error(`Response creation failed: ${error.message}`);
|
|
250
|
+
}
|
|
251
|
+
throw new Error('Response creation failed: Unknown error');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* 使用 Responses API 创建流式响应(带自动工具调用)
|
|
256
|
+
*/
|
|
257
|
+
export async function* createStreamingResponse(options, abortSignal) {
|
|
258
|
+
const client = getOpenAIClient();
|
|
259
|
+
// 提取系统提示词(如果存在)
|
|
260
|
+
let systemInstructions = SYSTEM_PROMPT;
|
|
261
|
+
let messages = [...options.messages];
|
|
262
|
+
const firstMessage = messages[0];
|
|
263
|
+
if (firstMessage?.role === 'system') {
|
|
264
|
+
systemInstructions = firstMessage.content;
|
|
265
|
+
messages = messages.slice(1); // 移除系统消息
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
const requestInput = convertToResponseInput(messages);
|
|
269
|
+
const requestPayload = {
|
|
270
|
+
model: options.model,
|
|
271
|
+
instructions: systemInstructions, // Responses API 使用 instructions 代替 system 消息
|
|
272
|
+
input: requestInput,
|
|
273
|
+
stream: true,
|
|
274
|
+
tools: convertToolsForResponses(options.tools),
|
|
275
|
+
tool_choice: options.tool_choice,
|
|
276
|
+
reasoning: options.reasoning || { summary: 'auto', effort: 'high' },
|
|
277
|
+
store: options.store ?? false, // 默认不存储对话历史,提高缓存命中
|
|
278
|
+
include: options.include || ['reasoning.encrypted_content'], // 包含加密推理内容
|
|
279
|
+
prompt_cache_key: options.prompt_cache_key, // 缓存键(可选)
|
|
280
|
+
};
|
|
281
|
+
const stream = await client.responses.create(requestPayload, {
|
|
282
|
+
signal: abortSignal,
|
|
283
|
+
});
|
|
284
|
+
let contentBuffer = '';
|
|
285
|
+
let toolCallsBuffer = {};
|
|
286
|
+
let hasToolCalls = false;
|
|
287
|
+
let currentFunctionCallId = null;
|
|
288
|
+
let usageData;
|
|
289
|
+
for await (const chunk of stream) {
|
|
290
|
+
if (abortSignal?.aborted) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
// Responses API 使用 SSE 事件格式
|
|
294
|
+
const eventType = chunk.type;
|
|
295
|
+
// 根据事件类型处理
|
|
296
|
+
if (eventType === 'response.created' || eventType === 'response.in_progress') {
|
|
297
|
+
// 响应创建/进行中 - 忽略
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
else if (eventType === 'response.output_item.added') {
|
|
301
|
+
// 新输出项添加
|
|
302
|
+
const item = chunk.item;
|
|
303
|
+
if (item?.type === 'reasoning') {
|
|
304
|
+
// 推理摘要开始 - 忽略
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
else if (item?.type === 'message') {
|
|
308
|
+
// 消息开始 - 忽略
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
else if (item?.type === 'function_call') {
|
|
312
|
+
// 工具调用开始
|
|
313
|
+
hasToolCalls = true;
|
|
314
|
+
const callId = item.call_id || item.id;
|
|
315
|
+
currentFunctionCallId = callId;
|
|
316
|
+
toolCallsBuffer[callId] = {
|
|
317
|
+
id: callId,
|
|
318
|
+
type: 'function',
|
|
319
|
+
function: {
|
|
320
|
+
name: item.name || '',
|
|
321
|
+
arguments: ''
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
else if (eventType === 'response.function_call_arguments.delta') {
|
|
329
|
+
// 工具调用参数增量
|
|
330
|
+
const delta = chunk.delta;
|
|
331
|
+
if (delta && currentFunctionCallId) {
|
|
332
|
+
toolCallsBuffer[currentFunctionCallId].function.arguments += delta;
|
|
333
|
+
// 发送 delta 用于 token 计数
|
|
334
|
+
yield {
|
|
335
|
+
type: 'tool_call_delta',
|
|
336
|
+
delta: delta
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else if (eventType === 'response.function_call_arguments.done') {
|
|
341
|
+
// 工具调用参数完成
|
|
342
|
+
const itemId = chunk.item_id;
|
|
343
|
+
const args = chunk.arguments;
|
|
344
|
+
if (itemId && toolCallsBuffer[itemId]) {
|
|
345
|
+
toolCallsBuffer[itemId].function.arguments = args;
|
|
346
|
+
}
|
|
347
|
+
currentFunctionCallId = null;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
else if (eventType === 'response.output_item.done') {
|
|
351
|
+
// 输出项完成
|
|
352
|
+
const item = chunk.item;
|
|
353
|
+
if (item?.type === 'function_call') {
|
|
354
|
+
// 确保工具调用信息完整
|
|
355
|
+
const callId = item.call_id || item.id;
|
|
356
|
+
if (toolCallsBuffer[callId]) {
|
|
357
|
+
toolCallsBuffer[callId].function.name = item.name;
|
|
358
|
+
toolCallsBuffer[callId].function.arguments = item.arguments;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
else if (eventType === 'response.content_part.added') {
|
|
364
|
+
// 内容部分添加 - 忽略
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
else if (eventType === 'response.reasoning_summary_text.delta') {
|
|
368
|
+
// 推理摘要增量更新(仅用于 token 计数,不包含在响应内容中)
|
|
369
|
+
const delta = chunk.delta;
|
|
370
|
+
if (delta) {
|
|
371
|
+
yield {
|
|
372
|
+
type: 'reasoning_delta',
|
|
373
|
+
delta: delta
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
else if (eventType === 'response.output_text.delta') {
|
|
378
|
+
// 文本增量更新
|
|
379
|
+
const delta = chunk.delta;
|
|
380
|
+
if (delta) {
|
|
381
|
+
contentBuffer += delta;
|
|
382
|
+
yield {
|
|
383
|
+
type: 'content',
|
|
384
|
+
content: delta
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
else if (eventType === 'response.output_text.done') {
|
|
389
|
+
// 文本输出完成 - 忽略
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
else if (eventType === 'response.content_part.done') {
|
|
393
|
+
// 内容部分完成 - 忽略
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
else if (eventType === 'response.completed') {
|
|
397
|
+
// 响应完全完成 - 从 response 对象中提取 usage
|
|
398
|
+
if (chunk.response && chunk.response.usage) {
|
|
399
|
+
usageData = {
|
|
400
|
+
prompt_tokens: chunk.response.usage.input_tokens || 0,
|
|
401
|
+
completion_tokens: chunk.response.usage.output_tokens || 0,
|
|
402
|
+
total_tokens: chunk.response.usage.total_tokens || 0
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
else if (eventType === 'response.failed' || eventType === 'response.cancelled') {
|
|
408
|
+
// 响应失败或取消
|
|
409
|
+
const error = chunk.error;
|
|
410
|
+
if (error) {
|
|
411
|
+
throw new Error(`Response failed: ${error.message || 'Unknown error'}`);
|
|
412
|
+
}
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// 如果有工具调用,返回它们
|
|
417
|
+
if (hasToolCalls) {
|
|
418
|
+
yield {
|
|
419
|
+
type: 'tool_calls',
|
|
420
|
+
tool_calls: Object.values(toolCallsBuffer)
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
// Yield usage information if available
|
|
424
|
+
if (usageData) {
|
|
425
|
+
yield {
|
|
426
|
+
type: 'usage',
|
|
427
|
+
usage: usageData
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
// 发送完成信号
|
|
431
|
+
yield {
|
|
432
|
+
type: 'done'
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (error instanceof Error) {
|
|
440
|
+
// 检查是否是 API 网关不支持 Responses API
|
|
441
|
+
if (error.message.includes('Panic detected') ||
|
|
442
|
+
error.message.includes('nil pointer') ||
|
|
443
|
+
error.message.includes('404') ||
|
|
444
|
+
error.message.includes('not found')) {
|
|
445
|
+
throw new Error('Streaming response creation failed: Your API endpoint does not support the Responses API. ' +
|
|
446
|
+
'Please switch to "Chat Completions" method in API settings, or use an OpenAI-compatible endpoint that supports Responses API (OpenAI official API, or compatible providers).');
|
|
447
|
+
}
|
|
448
|
+
throw new Error(`Streaming response creation failed: ${error.message}`);
|
|
449
|
+
}
|
|
450
|
+
throw new Error('Streaming response creation failed: Unknown error');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* 使用 Responses API 创建响应(限制工具调用轮数)
|
|
455
|
+
*/
|
|
456
|
+
export async function createResponseWithTools(options, maxToolRounds = 5) {
|
|
457
|
+
const client = getOpenAIClient();
|
|
458
|
+
let messages = [...options.messages];
|
|
459
|
+
let allToolCalls = [];
|
|
460
|
+
let rounds = 0;
|
|
461
|
+
// 提取系统提示词(如果存在)
|
|
462
|
+
let systemInstructions = SYSTEM_PROMPT;
|
|
463
|
+
const firstMessage = messages[0];
|
|
464
|
+
if (firstMessage?.role === 'system') {
|
|
465
|
+
systemInstructions = firstMessage.content;
|
|
466
|
+
messages = messages.slice(1); // 移除系统消息
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
while (rounds < maxToolRounds) {
|
|
470
|
+
const response = await client.responses.create({
|
|
471
|
+
model: options.model,
|
|
472
|
+
instructions: systemInstructions, // Responses API 使用 instructions 代替 system 消息
|
|
473
|
+
input: convertToResponseInput(messages),
|
|
474
|
+
tools: convertToolsForResponses(options.tools),
|
|
475
|
+
tool_choice: options.tool_choice,
|
|
476
|
+
reasoning: options.reasoning || { summary: 'auto', effort: 'high' },
|
|
477
|
+
store: options.store ?? false, // 默认不存储对话历史,提高缓存命中
|
|
478
|
+
include: options.include || ['reasoning.encrypted_content'], // 包含加密推理内容
|
|
479
|
+
prompt_cache_key: options.prompt_cache_key, // 缓存键(可选)
|
|
480
|
+
});
|
|
481
|
+
const output = response.output;
|
|
482
|
+
if (!output || output.length === 0) {
|
|
483
|
+
throw new Error('No output from AI');
|
|
484
|
+
}
|
|
485
|
+
const lastMessage = output[output.length - 1];
|
|
486
|
+
// 添加 assistant 消息
|
|
487
|
+
messages.push({
|
|
488
|
+
role: 'assistant',
|
|
489
|
+
content: lastMessage.content || '',
|
|
490
|
+
tool_calls: lastMessage.tool_calls
|
|
491
|
+
});
|
|
492
|
+
// 检查工具调用
|
|
493
|
+
if (lastMessage.tool_calls && lastMessage.tool_calls.length > 0) {
|
|
494
|
+
allToolCalls.push(...lastMessage.tool_calls);
|
|
495
|
+
// 执行工具调用
|
|
496
|
+
for (const toolCall of lastMessage.tool_calls) {
|
|
497
|
+
if (toolCall.type === 'function') {
|
|
498
|
+
try {
|
|
499
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
500
|
+
const result = await executeMCPTool(toolCall.function.name, args);
|
|
501
|
+
messages.push({
|
|
502
|
+
role: 'tool',
|
|
503
|
+
content: JSON.stringify(result),
|
|
504
|
+
tool_call_id: toolCall.id
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
messages.push({
|
|
509
|
+
role: 'tool',
|
|
510
|
+
content: `Error: ${error instanceof Error ? error.message : 'Tool execution failed'}`,
|
|
511
|
+
tool_call_id: toolCall.id
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
rounds++;
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
// 没有工具调用,返回结果
|
|
520
|
+
return {
|
|
521
|
+
content: lastMessage.content || '',
|
|
522
|
+
toolCalls: allToolCalls
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
throw new Error(`Maximum tool calling rounds (${maxToolRounds}) exceeded`);
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
if (error instanceof Error) {
|
|
529
|
+
// 检查是否是 API 网关不支持 Responses API
|
|
530
|
+
if (error.message.includes('Panic detected') ||
|
|
531
|
+
error.message.includes('nil pointer') ||
|
|
532
|
+
error.message.includes('404') ||
|
|
533
|
+
error.message.includes('not found')) {
|
|
534
|
+
throw new Error('Response creation with tools failed: Your API endpoint does not support the Responses API. ' +
|
|
535
|
+
'Please switch to "Chat Completions" method in API settings.');
|
|
536
|
+
}
|
|
537
|
+
throw new Error(`Response creation with tools failed: ${error.message}`);
|
|
538
|
+
}
|
|
539
|
+
throw new Error('Response creation with tools failed: Unknown error');
|
|
540
|
+
}
|
|
541
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System prompt configuration for Snow AI CLI
|
|
3
|
+
*/
|
|
4
|
+
export declare const SYSTEM_PROMPT = "You are Snow AI CLI, an intelligent command-line assistant designed to help users with their tasks efficiently.\n\nYour capabilities:\n- Answer technical questions and provide programming guidance\n- Execute MCP (Model Context Protocol) tools for file operations and system tasks\n- Run terminal commands using terminal-execute tool\n- Understand file references (using @ symbol)\n- Provide clear, accurate, and well-structured responses\n\n**Project Documentation:**\n- The current project may have a SNOW.md file in the root directory\n- SNOW.md contains project overview, architecture, tech stack, and development guidelines\n- You should read SNOW.md (if it exists) to understand the project context before making changes\n- If SNOW.md doesn't exist, you can still complete user requests without it - it's an optional helper document\n- You can generate or update SNOW.md using the /init command\n\nAvailable built-in tools:\n1. **Filesystem tools** (filesystem-*):\n - filesystem-read: Read file contents with line range\n - filesystem-create: Create new files\n - filesystem-edit: Edit existing files with diff preview\n - filesystem-delete: Delete files\n - filesystem-list: List directory contents\n - filesystem-search: Search for code patterns across files\n - filesystem-exists: Check if file/directory exists\n - filesystem-info: Get file metadata\n\n2. **Terminal execution** (terminal-execute):\n - Run commands exactly as typed in terminal\n - Examples: \"npm -v\", \"git status\", \"node index.js\"\n\n3. **TODO tools** (todo-*):\n - Track task progress with todo-create, todo-update, todo-add\n - Mark tasks completed immediately after finishing\n\nJust type the command as you would in terminal. That's it.\n\nError handling:\n- If command fails, check the error and try alternatives\n- Use filesystem tools when terminal commands don't work";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System prompt configuration for Snow AI CLI
|
|
3
|
+
*/
|
|
4
|
+
export const SYSTEM_PROMPT = `You are Snow AI CLI, an intelligent command-line assistant designed to help users with their tasks efficiently.
|
|
5
|
+
|
|
6
|
+
Your capabilities:
|
|
7
|
+
- Answer technical questions and provide programming guidance
|
|
8
|
+
- Execute MCP (Model Context Protocol) tools for file operations and system tasks
|
|
9
|
+
- Run terminal commands using terminal-execute tool
|
|
10
|
+
- Understand file references (using @ symbol)
|
|
11
|
+
- Provide clear, accurate, and well-structured responses
|
|
12
|
+
|
|
13
|
+
**Project Documentation:**
|
|
14
|
+
- The current project may have a SNOW.md file in the root directory
|
|
15
|
+
- SNOW.md contains project overview, architecture, tech stack, and development guidelines
|
|
16
|
+
- You should read SNOW.md (if it exists) to understand the project context before making changes
|
|
17
|
+
- If SNOW.md doesn't exist, you can still complete user requests without it - it's an optional helper document
|
|
18
|
+
- You can generate or update SNOW.md using the /init command
|
|
19
|
+
|
|
20
|
+
Available built-in tools:
|
|
21
|
+
1. **Filesystem tools** (filesystem-*):
|
|
22
|
+
- filesystem-read: Read file contents with line range
|
|
23
|
+
- filesystem-create: Create new files
|
|
24
|
+
- filesystem-edit: Edit existing files with diff preview
|
|
25
|
+
- filesystem-delete: Delete files
|
|
26
|
+
- filesystem-list: List directory contents
|
|
27
|
+
- filesystem-search: Search for code patterns across files
|
|
28
|
+
- filesystem-exists: Check if file/directory exists
|
|
29
|
+
- filesystem-info: Get file metadata
|
|
30
|
+
|
|
31
|
+
2. **Terminal execution** (terminal-execute):
|
|
32
|
+
- Run commands exactly as typed in terminal
|
|
33
|
+
- Examples: "npm -v", "git status", "node index.js"
|
|
34
|
+
|
|
35
|
+
3. **TODO tools** (todo-*):
|
|
36
|
+
- Track task progress with todo-create, todo-update, todo-add
|
|
37
|
+
- Mark tasks completed immediately after finishing
|
|
38
|
+
|
|
39
|
+
Just type the command as you would in terminal. That's it.
|
|
40
|
+
|
|
41
|
+
Error handling:
|
|
42
|
+
- If command fails, check the error and try alternatives
|
|
43
|
+
- Use filesystem tools when terminal commands don't work`;
|
package/dist/app.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { Alert } from '@inkjs/ui';
|
|
4
4
|
import WelcomeScreen from './ui/pages/WelcomeScreen.js';
|
|
5
5
|
import ApiConfigScreen from './ui/pages/ApiConfigScreen.js';
|
|
6
6
|
import ModelConfigScreen from './ui/pages/ModelConfigScreen.js';
|
|
7
|
+
import MCPConfigScreen from './ui/pages/MCPConfigScreen.js';
|
|
7
8
|
import ChatScreen from './ui/pages/ChatScreen.js';
|
|
8
9
|
import { useGlobalExit } from './hooks/useGlobalExit.js';
|
|
10
|
+
import { onNavigate } from './hooks/useGlobalNavigation.js';
|
|
9
11
|
export default function App({ version }) {
|
|
10
12
|
const [currentView, setCurrentView] = useState('welcome');
|
|
11
13
|
const [exitNotification, setExitNotification] = useState({
|
|
@@ -14,8 +16,15 @@ export default function App({ version }) {
|
|
|
14
16
|
});
|
|
15
17
|
// Global exit handler
|
|
16
18
|
useGlobalExit(setExitNotification);
|
|
19
|
+
// Global navigation handler
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const unsubscribe = onNavigate((event) => {
|
|
22
|
+
setCurrentView(event.destination);
|
|
23
|
+
});
|
|
24
|
+
return unsubscribe;
|
|
25
|
+
}, []);
|
|
17
26
|
const handleMenuSelect = (value) => {
|
|
18
|
-
if (value === 'chat' || value === 'settings' || value === 'config' || value === 'models') {
|
|
27
|
+
if (value === 'chat' || value === 'settings' || value === 'config' || value === 'models' || value === 'mcp') {
|
|
19
28
|
setCurrentView(value);
|
|
20
29
|
}
|
|
21
30
|
else if (value === 'exit') {
|
|
@@ -36,12 +45,14 @@ export default function App({ version }) {
|
|
|
36
45
|
return (React.createElement(ApiConfigScreen, { onBack: () => setCurrentView('welcome'), onSave: () => setCurrentView('welcome') }));
|
|
37
46
|
case 'models':
|
|
38
47
|
return (React.createElement(ModelConfigScreen, { onBack: () => setCurrentView('welcome'), onSave: () => setCurrentView('welcome') }));
|
|
48
|
+
case 'mcp':
|
|
49
|
+
return (React.createElement(MCPConfigScreen, { onBack: () => setCurrentView('welcome'), onSave: () => setCurrentView('welcome') }));
|
|
39
50
|
default:
|
|
40
51
|
return (React.createElement(WelcomeScreen, { version: version, onMenuSelect: handleMenuSelect }));
|
|
41
52
|
}
|
|
42
53
|
};
|
|
43
|
-
return (React.createElement(Box, { flexDirection: "column"
|
|
54
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
44
55
|
renderView(),
|
|
45
|
-
exitNotification.show && (React.createElement(Box, {
|
|
56
|
+
exitNotification.show && (React.createElement(Box, { paddingX: 1 },
|
|
46
57
|
React.createElement(Alert, { variant: "warning" }, exitNotification.message)))));
|
|
47
58
|
}
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import meow from 'meow';
|
|
|
5
5
|
import App from './app.js';
|
|
6
6
|
const cli = meow(`
|
|
7
7
|
Usage
|
|
8
|
-
$
|
|
8
|
+
$ snow
|
|
9
9
|
|
|
10
10
|
Options
|
|
11
11
|
--help Show help
|
|
@@ -14,6 +14,22 @@ const cli = meow(`
|
|
|
14
14
|
importMeta: import.meta,
|
|
15
15
|
flags: {},
|
|
16
16
|
});
|
|
17
|
+
// Disable bracketed paste mode on startup
|
|
18
|
+
process.stdout.write('\x1b[?2004l');
|
|
19
|
+
// Re-enable on exit to avoid polluting parent shell
|
|
20
|
+
const cleanup = () => {
|
|
21
|
+
process.stdout.write('\x1b[?2004l');
|
|
22
|
+
};
|
|
23
|
+
process.on('exit', cleanup);
|
|
24
|
+
process.on('SIGINT', () => {
|
|
25
|
+
cleanup();
|
|
26
|
+
process.exit(0);
|
|
27
|
+
});
|
|
28
|
+
process.on('SIGTERM', () => {
|
|
29
|
+
cleanup();
|
|
30
|
+
process.exit(0);
|
|
31
|
+
});
|
|
17
32
|
render(React.createElement(App, { version: cli.pkg.version }), {
|
|
18
33
|
exitOnCtrlC: false,
|
|
34
|
+
patchConsole: true,
|
|
19
35
|
});
|