closer-code 1.0.0 → 1.0.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.
Files changed (99) hide show
  1. package/.closer-code.example.json +32 -0
  2. package/DUAL_OPTIMIZATION_COMPLETE.md +293 -0
  3. package/README.md +167 -557
  4. package/README_OPENAI.md +163 -0
  5. package/THINKING_THROTTLING_OPTIMIZATION.md +244 -0
  6. package/THROTTLING_1_5S_OPTIMIZATION.md +401 -0
  7. package/TOOLS_IMPROVEMENTS_SUMMARY.md +273 -0
  8. package/cloco.md +5 -1
  9. package/config.example.json +15 -94
  10. package/config.mcp.example.json +81 -0
  11. package/dist/bash-runner.js +5 -126
  12. package/dist/batch-cli.js +286 -20658
  13. package/dist/closer-cli.js +329 -21135
  14. package/dist/index.js +308 -31036
  15. package/docs/ANTHROPIC_TOOL_ERROR_HANDLING.md +220 -0
  16. package/docs/BUILD_COMMANDS.md +79 -0
  17. package/docs/CTRL_Z_SUPPORT.md +189 -0
  18. package/docs/DEEPSEEK_R1_INTEGRATION.md +427 -0
  19. package/docs/FIX_OPENAI_TOOL_ERROR_HANDLING.md +375 -0
  20. package/docs/FIX_OPENAI_TOOL_RESULT.md +198 -0
  21. package/docs/INPUT_ENHANCEMENTS.md +192 -0
  22. package/docs/MCP_IMPLEMENTATION_SUMMARY.md +428 -0
  23. package/docs/MCP_INTEGRATION.md +418 -0
  24. package/docs/MCP_QUICKSTART.md +299 -0
  25. package/docs/MCP_README.md +166 -0
  26. package/docs/MINIFY_BUILD.md +180 -0
  27. package/docs/MULTILINE_INPUT_FEATURE.md +119 -0
  28. package/docs/OPENAI_CLIENT.md +258 -0
  29. package/docs/PROJECT_LOCAL_CONFIG.md +471 -0
  30. package/docs/PROJECT_LOCAL_CONFIG_SUMMARY.md +407 -0
  31. package/docs/REFACTOR_CONVERSATION.md +306 -0
  32. package/docs/REGION_EDIT_DESIGN.md +475 -0
  33. package/docs/SIGNAL_HANDLING.md +171 -0
  34. package/docs/STREAM_UPDATE_THROTTLE.md +273 -0
  35. package/docs/TOOLS_REFACTOR_PLAN.md +520 -0
  36. package/ds_r1.md +249 -0
  37. package/examples/abort-fence-example.js +294 -0
  38. package/package.json +18 -4
  39. package/src/ai-client-legacy.js +6 -1
  40. package/src/ai-client-openai.js +672 -0
  41. package/src/ai-client.js +30 -13
  42. package/src/closer-cli.jsx +450 -162
  43. package/src/components/fullscreen-conversation.jsx +157 -0
  44. package/src/components/ink-text-input/index.jsx +324 -0
  45. package/src/components/multiline-text-input.jsx +614 -0
  46. package/src/components/progress-bar.jsx +135 -0
  47. package/src/components/tool-detail-view.jsx +82 -0
  48. package/src/components/tool-renderers/bash-renderer.jsx +197 -0
  49. package/src/components/tool-renderers/file-edit-renderer.jsx +247 -0
  50. package/src/components/tool-renderers/file-read-renderer.jsx +261 -0
  51. package/src/components/tool-renderers/file-write-renderer.jsx +222 -0
  52. package/src/components/tool-renderers/index.jsx +178 -0
  53. package/src/components/tool-renderers/list-renderer.jsx +274 -0
  54. package/src/components/tool-renderers/search-renderer.jsx +248 -0
  55. package/src/config.js +182 -20
  56. package/src/conversation/abort-fence.js +158 -0
  57. package/src/conversation/core.js +377 -0
  58. package/src/conversation/index.js +33 -0
  59. package/src/conversation/mcp-integration.js +96 -0
  60. package/src/conversation/plan-manager.js +295 -0
  61. package/src/conversation/stream-handler.js +154 -0
  62. package/src/conversation/tool-executor.js +264 -0
  63. package/src/conversation.js +23 -958
  64. package/src/hooks/use-throttled-state.js +158 -0
  65. package/src/input/enhanced-input.jsx +268 -0
  66. package/src/input/history.js +342 -0
  67. package/src/logger.js +20 -0
  68. package/src/mcp/client.js +275 -0
  69. package/src/mcp/tools-adapter.js +149 -0
  70. package/src/planner.js +18 -5
  71. package/src/prompt-builder.js +159 -0
  72. package/src/tools.js +457 -25
  73. package/src/utils/json-parser.js +231 -0
  74. package/src/utils/json-repair.js +146 -0
  75. package/src/utils/platform.js +259 -0
  76. package/test/test-ctrl-bf.js +121 -0
  77. package/test/test-deepseek-reasoning.js +118 -0
  78. package/test/test-history-navigation.js +80 -0
  79. package/test/test-input-fix.js +105 -0
  80. package/test/test-input-history.js +98 -0
  81. package/test/test-mcp.js +115 -0
  82. package/test/test-openai-client.js +152 -0
  83. package/test/test-openai-tool-result.js +199 -0
  84. package/test/test-project-config.js +106 -0
  85. package/test/test-shortcuts.js +79 -0
  86. package/test/test-stream-throttle.js +124 -0
  87. package/test/test-tool-error-handling.js +95 -0
  88. package/test/verify-input-fix.sh +35 -0
  89. package/test-abort-fence.js +263 -0
  90. package/test-abort-fix.js +54 -0
  91. package/test-abort-new-conversation.js +75 -0
  92. package/test-ctrl-z.js +54 -0
  93. package/test-file-read.js +105 -0
  94. package/test-tool-display.js +127 -0
  95. package/src/closer-cli.jsx.backup +0 -948
  96. package/test/workflows/longtalk/cloco.md +0 -19
  97. package/test/workflows/longtalk/emoji_500.txt +0 -63
  98. package/test/workflows/longtalk/emoji_list.txt +0 -20
  99. package/test-ctrl-c.jsx +0 -126
@@ -0,0 +1,672 @@
1
+
2
+ /**
3
+ * OpenAI AI 客户端模块 - 使用 @openai/agents
4
+ *
5
+ * 与 Anthropic SDK 的对应关系:
6
+ * - Anthropic: betaZodTool -> OpenAI: tool
7
+ * - Anthropic: toolRunner -> OpenAI: run
8
+ * - Anthropic: stream.on -> OpenAI: (暂无流式事件 API)
9
+ * - Anthropic: client.messages.create -> OpenAI: Agent + run
10
+ *
11
+ * 注意:@openai/agents 包提供了更高级的抽象,
12
+ * 但为了保持与现有代码的兼容性,我们需要适配到统一的接口。
13
+ */
14
+
15
+ import { Agent, run, tool } from '@openai/agents';
16
+ import { z } from 'zod';
17
+ import OpenAI from 'openai';
18
+ import { safeJSONParse } from './utils/json-repair.js';
19
+
20
+ /**
21
+ * OpenAI 客户端(使用 @openai/agents SDK)
22
+ */
23
+ export class OpenAIClient {
24
+ constructor(config) {
25
+ this.apiKey = config.apiKey;
26
+ this.baseURL = config.baseURL || 'https://api.openai.com/v1';
27
+ this.model = config.model || 'gpt-4o';
28
+ this.maxTokens = config.maxTokens || 8192;
29
+
30
+ // 创建 OpenAI 客户端实例
31
+ this.client = new OpenAI({
32
+ apiKey: this.apiKey,
33
+ baseURL: this.baseURL
34
+ });
35
+
36
+ // 存储当前 agent
37
+ this.currentAgent = null;
38
+
39
+ // DeepSeek-R1 Reasoning 支持
40
+ this.isDeepSeekReasoner = this.model.includes('deepseek-reasoner') ||
41
+ config.enableReasoning === true;
42
+ this.reasoningContent = ''; // 累积的推理内容
43
+ }
44
+
45
+ /**
46
+ * 发送消息(非流式)
47
+ * 使用 @openai/agents 的 Agent 和 run
48
+ *
49
+ * @param {Array} messages - 消息数组
50
+ * @param {Object} options - 选项(system, tools, temperature)
51
+ * @returns {Promise} API 响应
52
+ */
53
+ async chat(messages, options = {}) {
54
+ const system = options.system || 'You are a helpful AI programming assistant.';
55
+ const tools = options.tools || [];
56
+ const temperature = options.temperature ?? 0.7;
57
+
58
+ // 转换消息格式(Anthropic -> OpenAI),需要特殊处理 tool_result
59
+ const formattedMessages = [];
60
+
61
+ // 遍历消息并转换
62
+ for (const message of messages) {
63
+ const converted = this._convertMessageFormat(message);
64
+
65
+ // 检查是否包含 tool_result(需要拆分为多个 role: 'tool' 消息)
66
+ if (Array.isArray(message.content)) {
67
+ const toolResultBlocks = message.content.filter(block => block.type === 'tool_result');
68
+ const textBlocks = message.content.filter(block => block.type === 'text');
69
+
70
+ if (toolResultBlocks.length > 0) {
71
+ // 每个工具结果作为独立的 role: 'tool' 消息
72
+ for (const block of toolResultBlocks) {
73
+ formattedMessages.push({
74
+ role: 'tool',
75
+ tool_call_id: block.tool_use_id,
76
+ content: typeof block.content === 'string' ? block.content : JSON.stringify(block.content)
77
+ });
78
+ }
79
+
80
+ // 如果有文本内容,添加为 user 消息
81
+ if (textBlocks.length > 0) {
82
+ const textContent = textBlocks.map(block => block.text).join('\n');
83
+ formattedMessages.push({
84
+ role: message.role,
85
+ content: textContent
86
+ });
87
+ }
88
+ } else {
89
+ // 没有 tool_result,直接添加转换后的消息
90
+ formattedMessages.push(converted);
91
+ }
92
+ } else {
93
+ // 不是数组,直接添加
94
+ formattedMessages.push(converted);
95
+ }
96
+ }
97
+
98
+ // 如果有工具,需要转换为 OpenAI agents 的 tool 格式
99
+ const openaiTools = this._convertTools(tools);
100
+
101
+ // 创建 Agent
102
+ const agent = new Agent({
103
+ name: 'Assistant',
104
+ instructions: system,
105
+ tools: openaiTools,
106
+ temperature: temperature
107
+ });
108
+
109
+ // 将消息转换为输入(简单拼接)
110
+ const input = this._messagesToInput(messages);
111
+
112
+ // 使用 run 函数执行
113
+ const result = await run(agent, input, {
114
+ maxTurns: 10 // 限制最大轮次
115
+ });
116
+
117
+ // 转换响应格式(OpenAI -> Anthropic)
118
+ return this._convertResponseToAnthropic(result);
119
+ }
120
+
121
+ /**
122
+ * 发送消息(流式)
123
+ * 注意:@openai/agents 目前不提供与 Anthropic SDK 相同的流式事件 API
124
+ * 这里使用 OpenAI SDK 的原生流式 API
125
+ *
126
+ * @param {Array} messages - 消息数组
127
+ * @param {Object} options - 选项
128
+ * @param {Function} onChunk - 流式回调函数
129
+ * @returns {Promise} 最终消息
130
+ */
131
+ async chatStream(messages, options = {}, onChunk) {
132
+ const system = options.system || 'You are a helpful AI programming assistant.';
133
+ const tools = options.tools || [];
134
+ const temperature = options.temperature ?? 0.7;
135
+
136
+ // 转换消息格式,需要特殊处理 tool_result
137
+ const formattedMessages = [
138
+ { role: 'system', content: system }
139
+ ];
140
+
141
+ // 遍历消息并转换
142
+ for (const message of messages) {
143
+ const converted = this._convertMessageFormat(message);
144
+
145
+ // 检查是否包含 tool_result(需要拆分为多个 role: 'tool' 消息)
146
+ if (Array.isArray(message.content)) {
147
+ const toolResultBlocks = message.content.filter(block => block.type === 'tool_result');
148
+ const textBlocks = message.content.filter(block => block.type === 'text');
149
+
150
+ if (toolResultBlocks.length > 0) {
151
+ // 每个工具结果作为独立的 role: 'tool' 消息
152
+ for (const block of toolResultBlocks) {
153
+ formattedMessages.push({
154
+ role: 'tool',
155
+ tool_call_id: block.tool_use_id,
156
+ content: typeof block.content === 'string' ? block.content : JSON.stringify(block.content)
157
+ });
158
+ }
159
+
160
+ // 如果有文本内容,添加为 user 消息
161
+ if (textBlocks.length > 0) {
162
+ const textContent = textBlocks.map(block => block.text).join('\n');
163
+ formattedMessages.push({
164
+ role: message.role,
165
+ content: textContent
166
+ });
167
+ }
168
+ } else {
169
+ // 没有 tool_result,直接添加转换后的消息
170
+ formattedMessages.push(converted);
171
+ }
172
+ } else {
173
+ // 不是数组,直接添加
174
+ formattedMessages.push(converted);
175
+ }
176
+ }
177
+
178
+ // 转换工具格式
179
+ const openaiTools = this._convertTools(tools);
180
+
181
+ // 构建 API 请求参数
182
+ const apiParams = {
183
+ model: this.model,
184
+ messages: formattedMessages,
185
+ tools: openaiTools.length > 0 ? openaiTools : undefined,
186
+ temperature: temperature,
187
+ max_tokens: this.maxTokens,
188
+ stream: true
189
+ };
190
+
191
+ // DeepSeek-R1 Reasoning: 添加 thinking 参数
192
+ if (this.isDeepSeekReasoner && options.thinking?.type === 'enabled') {
193
+ apiParams.extra_body = {
194
+ thinking: { type: 'enabled' }
195
+ };
196
+ }
197
+
198
+ // 使用 OpenAI SDK 的原生流式 API
199
+ const stream = await this.client.chat.completions.create(apiParams);
200
+
201
+ let fullResponse = {
202
+ role: 'assistant',
203
+ content: [],
204
+ model: this.model
205
+ };
206
+
207
+ let currentToolCalls = [];
208
+ let accumulatedText = '';
209
+ let accumulatedReasoning = '';
210
+
211
+ try {
212
+ for await (const chunk of stream) {
213
+ try {
214
+ const delta = chunk.choices[0]?.delta;
215
+
216
+ if (!delta) continue;
217
+
218
+ // 处理 DeepSeek-R1 的 reasoning_content
219
+ if (delta.reasoning_content) {
220
+ accumulatedReasoning += delta.reasoning_content;
221
+
222
+ if (typeof onChunk === 'function') {
223
+ onChunk({
224
+ type: 'reasoning',
225
+ delta: delta.reasoning_content,
226
+ snapshot: accumulatedReasoning
227
+ });
228
+ }
229
+ }
230
+
231
+ // 处理文本内容
232
+ if (delta.content) {
233
+ accumulatedText += delta.content;
234
+
235
+ if (typeof onChunk === 'function') {
236
+ onChunk({
237
+ type: 'text',
238
+ delta: delta.content,
239
+ snapshot: accumulatedText
240
+ });
241
+ }
242
+ }
243
+
244
+ // 处理工具调用
245
+ if (delta.tool_calls) {
246
+ for (const toolCall of delta.tool_calls) {
247
+ if (toolCall.index !== undefined) {
248
+ if (!currentToolCalls[toolCall.index]) {
249
+ currentToolCalls[toolCall.index] = {
250
+ id: toolCall.id,
251
+ type: 'function',
252
+ function: {
253
+ name: toolCall.function?.name || '',
254
+ arguments: toolCall.function?.arguments || ''
255
+ }
256
+ };
257
+ } else {
258
+ // 累积参数
259
+ if (toolCall.function?.arguments) {
260
+ currentToolCalls[toolCall.index].function.arguments +=
261
+ toolCall.function.arguments;
262
+ }
263
+ }
264
+ }
265
+ }
266
+ }
267
+
268
+ // 检查是否完成
269
+ if (chunk.choices[0]?.finish_reason === 'stop' ||
270
+ chunk.choices[0]?.finish_reason === 'tool_calls') {
271
+ break;
272
+ }
273
+ } catch (chunkError) {
274
+ console.error('[OpenAI Stream Chunk Error]:', chunkError.message);
275
+ // 继续处理下一个 chunk,不中断整个流
276
+ continue;
277
+ }
278
+ }
279
+ } catch (streamError) {
280
+ console.error('[OpenAI Stream Error]:', streamError.message);
281
+ console.error('[OpenAI Stream Error Type]:', streamError.constructor.name);
282
+
283
+ // 如果是网络错误或流中断,仍然返回已累积的内容
284
+ if (streamError.name === 'AbortError' || streamError.name === 'NetworkError') {
285
+ console.warn('[OpenAI Stream] Stream interrupted, returning accumulated content');
286
+ } else {
287
+ // 其他错误也尝试返回已累积的内容
288
+ console.warn('[OpenAI Stream] Error occurred, returning accumulated content');
289
+ }
290
+ // 不抛出异常,继续处理已累积的内容
291
+ }
292
+
293
+ // 构建响应内容
294
+ if (accumulatedReasoning) {
295
+ // DeepSeek-R1 的 reasoning_content
296
+ fullResponse.content.push({
297
+ type: 'reasoning',
298
+ text: accumulatedReasoning
299
+ });
300
+
301
+ // 保存推理内容用于后续工具调用
302
+ this.reasoningContent = accumulatedReasoning;
303
+ }
304
+
305
+ if (accumulatedText) {
306
+ fullResponse.content.push({
307
+ type: 'text',
308
+ text: accumulatedText
309
+ });
310
+ }
311
+
312
+ if (currentToolCalls.length > 0) {
313
+ for (const toolCall of currentToolCalls) {
314
+ // 使用 jsonrepair 解析工具参数
315
+ const input = safeJSONParse(toolCall.function.arguments, {
316
+ fallback: {},
317
+ silent: false
318
+ });
319
+
320
+ // 检查是否解析失败
321
+ // 情况1: 返回的不是对象(如字符串、数字等)
322
+ const isNotObject = typeof input !== 'object' || input === null;
323
+ // 情况2: 返回的是空对象
324
+ const isEmptyObject = typeof input === 'object' && input !== null && Object.keys(input).length === 0;
325
+ // 原始参数是否为空
326
+ const isArgsEmpty = toolCall.function.arguments.trim() === '' ||
327
+ toolCall.function.arguments.trim() === '{}';
328
+
329
+ // 如果返回的不是对象,且原始参数不为空,则认为解析失败
330
+ // 如果返回的是空对象,且原始参数不是空JSON,则认为解析失败
331
+ const parseError = (isNotObject && !isArgsEmpty) || (isEmptyObject && !isArgsEmpty);
332
+
333
+ fullResponse.content.push({
334
+ type: 'tool_use',
335
+ id: toolCall.id,
336
+ name: toolCall.function.name,
337
+ input,
338
+ parseError
339
+ });
340
+ }
341
+ }
342
+
343
+ // DeepSeek-R1: 保存完整的响应消息(包含 reasoning_content)
344
+ // 用于下一轮工具调用
345
+ if (this.isDeepSeekReasoner) {
346
+ fullResponse.reasoning_content = accumulatedReasoning || '';
347
+ fullResponse.raw_content = accumulatedText || '';
348
+ }
349
+
350
+ return fullResponse;
351
+ }
352
+
353
+ /**
354
+ * 使用 tools 自动处理工具调用循环
355
+ * 对应 Anthropic 的 toolRunner
356
+ *
357
+ * @param {Array} messages - 消息数组
358
+ * @param {Array} tools - 工具数组(使用 Zod 定义的 tool)
359
+ * @param {Object} options - 选项
360
+ * @returns {Promise} 最终消息(所有工具调用完成后)
361
+ */
362
+ async chatWithTools(messages, tools, options = {}) {
363
+ const system = options.system || 'You are a helpful AI programming assistant.';
364
+ const temperature = options.temperature ?? 0.7;
365
+
366
+ // 转换工具格式(Anthropic betaZodTool -> OpenAI tool)
367
+ const openaiTools = tools.map(tool => {
368
+ // betaZodTool 有 name, description, input_schema, run
369
+ // OpenAI tool 需要:name, description, parameters (Zod schema), execute
370
+ return {
371
+ name: tool.name,
372
+ description: tool.description,
373
+ parameters: tool.input_schema, // Zod schema
374
+ execute: tool.run
375
+ };
376
+ });
377
+
378
+ // 创建 Agent
379
+ const agent = new Agent({
380
+ name: 'Assistant',
381
+ instructions: system,
382
+ tools: openaiTools,
383
+ temperature: temperature
384
+ });
385
+
386
+ // 将消息转换为输入
387
+ const input = this._messagesToInput(messages);
388
+
389
+ // 使用 run 函数执行(自动处理工具调用循环)
390
+ const result = await run(agent, input, {
391
+ maxTurns: 10
392
+ });
393
+
394
+ // 转换响应格式
395
+ return this._convertResponseToAnthropic(result);
396
+ }
397
+
398
+ /**
399
+ * 获取消息的 token 计数(使用 OpenAI SDK 的 tokenizer)
400
+ * @param {Array} messages - 消息数组
401
+ * @returns {Promise} Token 计数
402
+ */
403
+ async countTokens(messages) {
404
+ // OpenAI SDK 没有内置的 countTokens 方法
405
+ // 使用简单的估算:1 token ≈ 4 characters
406
+ const text = JSON.stringify(messages);
407
+ return Math.ceil(text.length / 4);
408
+ }
409
+
410
+ /**
411
+ * 清除历史消息中的 reasoning_content(DeepSeek-R1 特性)
412
+ *
413
+ * DeepSeek-R1 要求:
414
+ * - 同一轮的工具调用中:保留 reasoning_content
415
+ * - 新一轮对话开始时:清除 reasoning_content 以节省带宽
416
+ *
417
+ * @param {Array} messages - 消息数组
418
+ * @returns {Array} 清除后的消息数组
419
+ */
420
+ clearReasoningContent(messages) {
421
+ if (!this.isDeepSeekReasoner) {
422
+ return messages; // 非 DeepSeek-R1 模型,无需处理
423
+ }
424
+
425
+ // 清除每条消息中的 reasoning_content
426
+ return messages.map(message => {
427
+ if (message.reasoning_content !== undefined) {
428
+ // 创建新消息对象,不包含 reasoning_content
429
+ const { reasoning_content, ...messageWithoutReasoning } = message;
430
+ return messageWithoutReasoning;
431
+ }
432
+ return message;
433
+ });
434
+ }
435
+
436
+ /**
437
+ * 保留当前轮的 reasoning_content(DeepSeek-R1 特性)
438
+ *
439
+ * 用于在同一轮的工具调用中继续传递 reasoning_content
440
+ *
441
+ * @param {Array} messages - 消息数组
442
+ * @param {string} reasoningContent - 当前轮的推理内容
443
+ * @returns {Array} 添加了 reasoning_content 的消息数组
444
+ */
445
+ appendCurrentReasoning(messages, reasoningContent) {
446
+ if (!this.isDeepSeekReasoner || !reasoningContent) {
447
+ return messages; // 非 DeepSeek-R1 或无推理内容,无需处理
448
+ }
449
+
450
+ // 在最后一条 assistant 消息中添加 reasoning_content
451
+ const lastMessage = messages[messages.length - 1];
452
+ if (lastMessage && lastMessage.role === 'assistant') {
453
+ lastMessage.reasoning_content = reasoningContent;
454
+ }
455
+
456
+ return messages;
457
+ }
458
+
459
+ /**
460
+ * 转换消息格式(Anthropic -> OpenAI)
461
+ * @private
462
+ */
463
+ _convertMessageFormat(message) {
464
+ // Anthropic: { role, content } where content can be string or array
465
+ // OpenAI: { role, content } or { role, tool_calls } or { role: 'tool', tool_call_id, content }
466
+
467
+ if (typeof message.content === 'string') {
468
+ return {
469
+ role: message.role,
470
+ content: message.content
471
+ };
472
+ }
473
+
474
+ // 处理 content 为数组的情况
475
+ if (Array.isArray(message.content)) {
476
+ // 检查是否包含 tool_use(assistant 的工具调用)
477
+ const toolUseBlocks = message.content.filter(block => block.type === 'tool_use');
478
+ const textBlocks = message.content.filter(block => block.type === 'text');
479
+
480
+ if (toolUseBlocks.length > 0) {
481
+ // OpenAI 格式:assistant 消息使用 tool_calls 字段
482
+ // 同时保留文本内容(如果有)
483
+ const result = {
484
+ role: message.role,
485
+ tool_calls: toolUseBlocks.map(block => ({
486
+ id: block.id,
487
+ type: 'function',
488
+ function: {
489
+ name: block.name,
490
+ arguments: JSON.stringify(block.input)
491
+ }
492
+ }))
493
+ };
494
+
495
+ // 如果有文本内容,添加 content 字段
496
+ if (textBlocks.length > 0) {
497
+ result.content = textBlocks.map(block => block.text).join('\n');
498
+ }
499
+
500
+ // DeepSeek-R1: 保留 reasoning_content 字段
501
+ if (this.isDeepSeekReasoner && message.reasoning_content !== undefined) {
502
+ result.reasoning_content = message.reasoning_content;
503
+ }
504
+
505
+ return result;
506
+ }
507
+
508
+ // 检查是否包含 tool_result(user 的工具结果)
509
+ const toolResultBlocks = message.content.filter(block => block.type === 'tool_result');
510
+
511
+ if (toolResultBlocks.length > 0) {
512
+ // OpenAI 格式:每个 tool_result 应该是一个独立的 role: 'tool' 消息
513
+ // 但由于这个函数返回单个消息,我们需要特殊处理
514
+ // 如果只有一个 tool_result,返回标准格式
515
+ if (toolResultBlocks.length === 1 && !textBlocks.length) {
516
+ const block = toolResultBlocks[0];
517
+ return {
518
+ role: 'tool',
519
+ tool_call_id: block.tool_use_id,
520
+ content: typeof block.content === 'string' ? block.content : JSON.stringify(block.content)
521
+ };
522
+ }
523
+
524
+ // 如果有多个 tool_result 或同时有文本,返回 user 消息格式
525
+ // 注意:这种情况下,调用方需要将这个消息拆分为多个消息
526
+ return {
527
+ role: message.role,
528
+ content: toolResultBlocks.map(block => ({
529
+ type: 'text',
530
+ text: `[Tool Result for ${block.tool_use_id}]: ${typeof block.content === 'string' ? block.content : JSON.stringify(block.content)}`
531
+ }))
532
+ };
533
+ }
534
+
535
+ // 普通 text 内容
536
+ const textParts = message.content
537
+ .filter(block => block.type === 'text')
538
+ .map(block => block.text)
539
+ .join('\n');
540
+
541
+ return {
542
+ role: message.role,
543
+ content: textParts
544
+ };
545
+ }
546
+
547
+ return {
548
+ role: message.role,
549
+ content: ''
550
+ };
551
+ }
552
+
553
+ /**
554
+ * 转换工具格式(Anthropic betaZodTool -> OpenAI tool/function)
555
+ * @private
556
+ */
557
+ _convertTools(anthropicTools) {
558
+ return anthropicTools.map(tool => ({
559
+ type: 'function',
560
+ function: {
561
+ name: tool.name,
562
+ description: tool.description,
563
+ parameters: tool.input_schema // Zod schema
564
+ }
565
+ }));
566
+ }
567
+
568
+ /**
569
+ * 将消息数组转换为输入字符串
570
+ * @private
571
+ */
572
+ _messagesToInput(messages) {
573
+ // 简单实现:提取所有文本内容
574
+ return messages
575
+ .map(m => {
576
+ if (typeof m.content === 'string') {
577
+ return `${m.role}: ${m.content}`;
578
+ }
579
+ if (Array.isArray(m.content)) {
580
+ // 提取文本内容
581
+ const textBlocks = m.content.filter(block => block.type === 'text');
582
+ const text = textBlocks.map(block => block.text).join('\n');
583
+
584
+ // 提取工具结果
585
+ const toolResultBlocks = m.content.filter(block => block.type === 'tool_result');
586
+ const toolResults = toolResultBlocks.map(block => {
587
+ const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
588
+ return `[Tool Result: ${content}]`;
589
+ }).join('\n');
590
+
591
+ // 组合文本和工具结果
592
+ const combined = [text, toolResults].filter(s => s).join('\n');
593
+ return `${m.role}: ${combined}`;
594
+ }
595
+ return '';
596
+ })
597
+ .join('\n\n');
598
+ }
599
+
600
+ /**
601
+ * 转换响应格式(OpenAI -> Anthropic)
602
+ * @private
603
+ */
604
+ _convertResponseToAnthropic(result) {
605
+ // OpenAI agents 返回: { finalOutput, ... }
606
+ // 我们需要转换为 Anthropic 格式: { role, content, ... }
607
+
608
+ const textContent = result.finalOutput || '';
609
+
610
+ return {
611
+ role: 'assistant',
612
+ content: [
613
+ {
614
+ type: 'text',
615
+ text: textContent
616
+ }
617
+ ],
618
+ model: this.model,
619
+ finishReason: 'stop'
620
+ };
621
+ }
622
+ }
623
+
624
+ /**
625
+ * 创建兼容的工具定义
626
+ * 将现有的 betaZodTool 转换为 OpenAI agents 的 tool 格式
627
+ */
628
+ export function createOpenAITool(anthropicTool) {
629
+ return tool({
630
+ name: anthropicTool.name,
631
+ description: anthropicTool.description,
632
+ parameters: anthropicTool.input_schema,
633
+ execute: async (input) => {
634
+ try {
635
+ // 调用原始工具的 run 方法
636
+ const result = await anthropicTool.run(input);
637
+
638
+ // 使用 jsonrepair 解析 JSON
639
+ const parsed = safeJSONParse(result, {
640
+ fallback: null,
641
+ silent: true
642
+ });
643
+
644
+ // 如果解析失败,检查结果是否已经是对象
645
+ if (parsed === null) {
646
+ if (typeof result === 'object') {
647
+ return result;
648
+ }
649
+ // 返回原始字符串
650
+ return { result: result };
651
+ }
652
+
653
+ return parsed;
654
+ } catch (error) {
655
+ // 工具执行失败,返回错误信息
656
+ console.error(`[OpenAI Tool Execution Error] ${anthropicTool.name}:`, error.message);
657
+ return {
658
+ success: false,
659
+ error: error.message,
660
+ errorType: error.constructor.name
661
+ };
662
+ }
663
+ }
664
+ });
665
+ }
666
+
667
+ /**
668
+ * 批量转换工具
669
+ */
670
+ export function convertToolsToOpenAI(anthropicTools) {
671
+ return anthropicTools.map(createOpenAITool);
672
+ }