closer-code 1.0.0

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 (100) hide show
  1. package/.env.example +83 -0
  2. package/API_GUIDE.md +1411 -0
  3. package/AUTO_MKDIR_IMPROVEMENT.md +354 -0
  4. package/CLAUDE.md +55 -0
  5. package/CTRL_C_EXPERIMENT.md +90 -0
  6. package/PROJECT_CLEANUP_SUMMARY.md +121 -0
  7. package/README.md +686 -0
  8. package/cloco.md +51 -0
  9. package/config.example.json +116 -0
  10. package/dist/bash-runner.js +128 -0
  11. package/dist/batch-cli.js +20736 -0
  12. package/dist/closer-cli.js +21190 -0
  13. package/dist/index.js +31228 -0
  14. package/docs/EXPORT_COMMAND.md +152 -0
  15. package/docs/FILE_NAMING_IMPROVEMENT.md +168 -0
  16. package/docs/GLOBAL_CONFIG.md +128 -0
  17. package/docs/LONG_MESSAGE_DISPLAY_FIX.md +202 -0
  18. package/docs/PROJECT_HISTORY_ISOLATION.md +315 -0
  19. package/docs/QUICK_START_HISTORY.md +207 -0
  20. package/docs/TASK_PROGRESS_FEATURE.md +190 -0
  21. package/docs/THINKING_CONTENT_RESEARCH.md +267 -0
  22. package/docs/THINKING_FEATURE.md +187 -0
  23. package/docs/THINKING_IMPROVEMENT_COMPARISON.md +193 -0
  24. package/docs/THINKING_OPTIMIZATION_SUMMARY.md +242 -0
  25. package/docs/UI_IMPROVEMENTS_2025-01-18.md +256 -0
  26. package/docs/WHY_THINKING_SHORT.md +201 -0
  27. package/package.json +49 -0
  28. package/scenarios/README.md +234 -0
  29. package/scenarios/run-all-scenarios.js +342 -0
  30. package/scenarios/scenario1-batch-converter.js +247 -0
  31. package/scenarios/scenario2-code-analyzer.js +375 -0
  32. package/scenarios/scenario3-doc-generator.js +371 -0
  33. package/scenarios/scenario4-log-analyzer.js +496 -0
  34. package/scenarios/scenario5-tdd-helper.js +681 -0
  35. package/src/ai-client-legacy.js +171 -0
  36. package/src/ai-client.js +221 -0
  37. package/src/bash-runner.js +148 -0
  38. package/src/batch-cli.js +327 -0
  39. package/src/cli.jsx +166 -0
  40. package/src/closer-cli.jsx +1103 -0
  41. package/src/closer-cli.jsx.backup +948 -0
  42. package/src/commands/batch.js +62 -0
  43. package/src/commands/chat.js +10 -0
  44. package/src/commands/config.js +154 -0
  45. package/src/commands/help.js +76 -0
  46. package/src/commands/history.js +192 -0
  47. package/src/commands/setup.js +17 -0
  48. package/src/commands/upgrade.js +101 -0
  49. package/src/commands/workflow-tests.js +125 -0
  50. package/src/config.js +343 -0
  51. package/src/conversation.js +962 -0
  52. package/src/git-helper.js +349 -0
  53. package/src/index.js +88 -0
  54. package/src/logger.js +347 -0
  55. package/src/plan.js +193 -0
  56. package/src/planner.js +397 -0
  57. package/src/search.js +195 -0
  58. package/src/setup.js +147 -0
  59. package/src/shortcuts.js +269 -0
  60. package/src/snippets.js +430 -0
  61. package/src/test-modules.js +118 -0
  62. package/src/tools.js +398 -0
  63. package/src/utils/cli.js +124 -0
  64. package/src/utils/validator.js +184 -0
  65. package/src/utils/version.js +33 -0
  66. package/src/utils/workflow-test.js +271 -0
  67. package/src/utils/workflow.js +268 -0
  68. package/test/demo-file-naming.js +92 -0
  69. package/test/demo-thinking.js +124 -0
  70. package/test/final-verification-report.md +303 -0
  71. package/test/research-thinking.js +130 -0
  72. package/test/test-auto-mkdir.js +123 -0
  73. package/test/test-e2e-empty-dir.md +108 -0
  74. package/test/test-export-logic.js +119 -0
  75. package/test/test-global-cloco.js +126 -0
  76. package/test/test-history-isolation.js +291 -0
  77. package/test/test-improved-thinking.js +43 -0
  78. package/test/test-long-message.js +65 -0
  79. package/test/test-plan-functionality.js +95 -0
  80. package/test/test-real-scenario.js +216 -0
  81. package/test/test-thinking-display.js +65 -0
  82. package/test/ui-verification-test.js +203 -0
  83. package/test/verify-history-isolation.sh +71 -0
  84. package/test/verify-thinking.js +339 -0
  85. package/test/workflows/empty-dir-creation.md +51 -0
  86. package/test/workflows/inventor/ascii-teacup.js +199 -0
  87. package/test/workflows/inventor/ascii-teacup.mjs +199 -0
  88. package/test/workflows/inventor/ascii_apple.hs +84 -0
  89. package/test/workflows/inventor/ascii_apple.py +91 -0
  90. package/test/workflows/inventor/cloco.md +3 -0
  91. package/test/workflows/longtalk/cloco.md +19 -0
  92. package/test/workflows/longtalk/emoji_500.txt +63 -0
  93. package/test/workflows/longtalk/emoji_list.txt +20 -0
  94. package/test/workflows/programmer/adder.md +33 -0
  95. package/test/workflows/programmer/expect.md +2 -0
  96. package/test/workflows/programmer/prompt.md +3 -0
  97. package/test/workflows/test-empty-dir-creation.js +113 -0
  98. package/test-ctrl-c.jsx +126 -0
  99. package/test-manual-file-creation.js +151 -0
  100. package/winfix.md +3 -0
@@ -0,0 +1,962 @@
1
+ /**
2
+ * 对话管理器(使用 SDK)
3
+ *
4
+ * 使用 @anthropic-ai/sdk 的 toolRunner 自动处理工具调用循环
5
+ * 优势:
6
+ * - 无需手工解析工具调用
7
+ * - 无需清理工具调用标记
8
+ * - 无需复杂的正则表达式
9
+ * - 代码量减少 80%
10
+ */
11
+
12
+ import Anthropic from '@anthropic-ai/sdk';
13
+ import { createAIClient } from './ai-client.js';
14
+ import { setToolExecutorContext, getToolDefinitions } from './tools.js';
15
+ import { loadHistory, saveHistory, loadMemory } from './config.js';
16
+ import { Plan, PlanType, PlanStatus, StepStatus } from './plan.js';
17
+ import {
18
+ initLogger,
19
+ logConfig,
20
+ logUserMessage,
21
+ logAIRequest,
22
+ logAIResponse,
23
+ logAIError,
24
+ logToolCall,
25
+ logSessionSummary
26
+ } from './logger.js';
27
+
28
+ // 消息类型
29
+ export const MessageType = {
30
+ USER: 'user',
31
+ ASSISTANT: 'assistant',
32
+ TOOL: 'tool',
33
+ SYSTEM: 'system',
34
+ ERROR: 'error'
35
+ };
36
+
37
+ // Workflow 测试模式的前置提示词(添加到每轮对话开头)
38
+ // SDK 版本无需特殊格式说明,工具调用自动处理
39
+ export const WORKFLOW_PROMPT_PREFIX = `
40
+ 【Workflow 测试模式】
41
+ 你现在处于 workflow 测试模式。这个测试分为两轮:
42
+ **第1轮(执行任务)**:
43
+ - 你会收到一个任务描述
44
+ - 你必须使用工具完成任务(如 readFile、writeFile、bash 等)
45
+ - 不能只说"我会做",而要立即调用工具
46
+ - 必须实际创建/修改文件,而不仅仅是输出代码
47
+ **第2轮(验证结果)**:
48
+ - 你会收到验收标准
49
+ - 使用工具验证结果是否符合预期
50
+ - 验证完成后,如果结果完全符合预期,你必须回复:WORKFLOW TEST AS EXPECTED
51
+ - 如果结果不符合预期,请详细说明哪些方面不符合
52
+ 重要:你必须使用工具来实际操作文件和系统,而不是仅仅输出代码或描述。
53
+ `;
54
+
55
+ // Workflow 测试模式的系统提示词
56
+ const WORKFLOW_SYSTEM_PROMPT = `
57
+ ## Workflow 测试模式
58
+ 你当前处于 workflow 测试模式。这是一个两轮对话测试:
59
+ 1. **执行任务阶段(第1轮)**:
60
+ - 你会收到一个任务描述
61
+ - 必须使用工具完成该任务(readFile、writeFile、bash 等)
62
+ - 必须实际创建/修改文件,不能只输出代码
63
+ - 任务完成后对话会中止
64
+ 2. **验证阶段(第2轮)**:
65
+ - 你会收到验收标准
66
+ - 使用工具验证结果是否符合预期
67
+ - 验证完成后对话会中止
68
+ **验证结果回复格式**:
69
+ - 如果结果完全符合预期:必须回复 "WORKFLOW TEST AS EXPECTED"
70
+ - 如果结果不符合预期:详细说明哪些方面不符合
71
+ **关键规则**:
72
+ - 第1轮必须使用工具实际操作文件
73
+ - 第2轮验证完成后必须给出明确的验收结论
74
+ `;
75
+
76
+ /**
77
+ * 对话会话(使用 SDK)
78
+ */
79
+ export class Conversation {
80
+ constructor(config, workflowTest = false) {
81
+ this.config = config;
82
+ this.workflowTest = workflowTest;
83
+ this.messages = [];
84
+ this.currentPlan = null;
85
+ this.isProcessing = false;
86
+
87
+ // 初始化工具执行器上下文
88
+ setToolExecutorContext(config);
89
+ }
90
+
91
+ /**
92
+ * 初始化对话
93
+ */
94
+ async initialize() {
95
+ // 初始化日志
96
+ await initLogger();
97
+ await logConfig(this.config);
98
+
99
+ // 加载历史
100
+ const history = loadHistory();
101
+ // SDK 不接受 role: 'tool' 的消息,只保留 user 和 assistant 消息
102
+ this.messages = history
103
+ .filter(msg => msg.role === 'user' || msg.role === 'assistant')
104
+ .map(msg => ({
105
+ role: msg.role,
106
+ content: msg.content
107
+ }));
108
+
109
+ // 构建系统提示(现在是异步的)
110
+ await this.buildSystemPrompt();
111
+ return this;
112
+ }
113
+
114
+ /**
115
+ * 构建系统提示
116
+ */
117
+ async buildSystemPrompt() {
118
+ const memory = loadMemory();
119
+ const projectKey = this.config.behavior.workingDir || 'default';
120
+ const projectInfo = memory.projects?.[projectKey];
121
+
122
+ // 读取全局 cloco.md 文件内容
123
+ let globalClocoContent = '';
124
+ try {
125
+ const fs = await import('fs/promises');
126
+ const path = await import('path');
127
+ const os = await import('os');
128
+
129
+ // 获取用户主目录
130
+ const homeDir = os.homedir();
131
+ const globalClocoPath = path.join(homeDir, '.closer-code', 'cloco.md');
132
+
133
+ globalClocoContent = await fs.readFile(globalClocoPath, 'utf-8');
134
+ console.log('✅ 已加载全局行为规范: ~/.closer-code/cloco.md');
135
+ } catch (error) {
136
+ // 全局配置不存在是正常情况,不报错
137
+ if (error.code !== 'ENOENT') {
138
+ console.error('读取全局 cloco.md 失败:', error.message);
139
+ }
140
+ }
141
+
142
+ // 读取项目级 cloco.md 文件内容
143
+ let projectClocoContent = '';
144
+ try {
145
+ const fs = await import('fs/promises');
146
+ const path = await import('path');
147
+ const clocoPath = path.join(process.cwd(), 'cloco.md');
148
+ projectClocoContent = await fs.readFile(clocoPath, 'utf-8');
149
+ console.log('✅ 已加载项目行为规范: ./cloco.md');
150
+ } catch (error) {
151
+ // 项目配置不存在是正常情况,不报错
152
+ if (error.code !== 'ENOENT') {
153
+ console.error('读取项目 cloco.md 失败:', error.message);
154
+ }
155
+ }
156
+
157
+ // SDK 版本的系统提示 - 移除了工具调用格式的说明
158
+ // SDK 会自动处理工具调用,无需告诉 AI 特殊格式
159
+ this.systemPrompt = `You are Closer, an AI programming assistant designed to help developers with coding tasks, debugging, and project management.
160
+
161
+ ## Tool Use Requirements (CRITICAL)
162
+ **YOU MUST USE TOOLS TO EXECUTE ACTIONS.** This is not optional.
163
+ - When user asks you to "show", "list", "check", "see", "view" directory contents → **MUST** call bash tool with "ls" or "dir" command
164
+ - When user asks about files → **MUST** call readFile, searchFiles, or searchCode tools
165
+ - When user asks to run commands/tests → **MUST** call bash tool
166
+ - When user asks to make changes → **MUST** call writeFile or editFile tools
167
+
168
+ **DO NOT** just say "I'll check", "Let me see", "I'll look into it" - **IMMEDIATELY CALL THE APPROPRIATE TOOL**.
169
+
170
+ Examples of CORRECT behavior:
171
+ - User: "What's in this directory?" → You: Immediately call bash tool
172
+ - User: "Show me the config" → You: Immediately call readFile tool
173
+ - User: "Run the tests" → You: Immediately call bash tool
174
+
175
+ ## Error Handling and Self-Correction (CRITICAL) 🆕
176
+
177
+ **When a tool returns an error, you MUST analyze and attempt to fix it.** Do not give up after the first failure.
178
+
179
+ ### Error Analysis Process
180
+ 1. **Read the error message carefully** - Look for error codes like ENOENT, EACCES, etc.
181
+ 2. **Identify the root cause** - Understand why the operation failed
182
+ 3. **Devise a solution** - Determine what needs to be fixed
183
+ 4. **Execute the fix** - Use appropriate tools to resolve the issue
184
+ 5. **Retry the original operation** - Attempt the failed operation again
185
+
186
+ ### Common Error Patterns
187
+
188
+ #### Directory Not Found (ENOENT)
189
+ **Error**: "Parent directory does not exist"
190
+ **Solution**: Create the directory first
191
+ \`\`\`javascript
192
+ // Example error response:
193
+ {
194
+ "success": false,
195
+ "error": "ENOENT",
196
+ "suggestion": "Create it first using: bash tool with 'mkdir -p chapters/'"
197
+ }
198
+
199
+ // Your response:
200
+ 1. Call bash tool: "mkdir -p chapters/"
201
+ 2. Retry writeFile with the original path
202
+ \`\`\`
203
+
204
+ #### Permission Denied (EACCES)
205
+ **Error**: "Permission denied"
206
+ **Solution**: Check permissions or use a different location
207
+
208
+ #### File Not Found for Editing
209
+ **Error**: "Old text not found in file"
210
+ **Solution**: Use readFile to check the actual content first, then adjust the oldText
211
+
212
+ ### Retry Strategy
213
+ - **Maximum retries**: 3 attempts per operation
214
+ - **Wait time**: No delay needed for tool operations
215
+ - **Different approach**: If the same fix fails twice, try an alternative solution
216
+
217
+ ### Example: Self-Correction in Action
218
+
219
+ **User Request**: "Create a file at src/components/Button.tsx"
220
+
221
+ **Attempt 1** (fails):
222
+ \`\`\`
223
+ You: Call writeFile with "src/components/Button.tsx"
224
+ Tool: {"success": false, "error": "ENOENT", "suggestion": "mkdir -p src/components/"}
225
+ \`\`\`
226
+
227
+ **Your Analysis**:
228
+ - Error: ENOENT means directory doesn't exist
229
+ - Root cause: src/components/ directory is missing
230
+ - Solution: Create the directory first
231
+
232
+ **Attempt 2** (fix):
233
+ \`\`\`
234
+ You: Call bash with "mkdir -p src/components/"
235
+ Tool: {"success": true}
236
+ \`\`\`
237
+
238
+ **Attempt 3** (retry):
239
+ \`\`\`
240
+ You: Call writeFile with "src/components/Button.tsx"
241
+ Tool: {"success": true, "path": ".../src/components/Button.tsx"}
242
+ \`\`\`
243
+
244
+ **Result**: ✅ Success through self-correction!
245
+
246
+ ### Important Notes
247
+ - **Always read error suggestions** - Tools often provide hints on how to fix errors
248
+ - **Be persistent** - Up to 3 retries are acceptable for complex operations
249
+ - **Learn from errors** - If a pattern emerges, adapt your approach
250
+ - **Ask for help if needed** - After 3 failed attempts, explain the issue to the user
251
+
252
+ **Remember**: Errors are opportunities to demonstrate problem-solving skills. Analyze, fix, retry!
253
+
254
+ ## Planning and Documentation Behavior (CRITICAL)
255
+ **YOU MUST DOCUMENT YOUR PLANNING PROCESS.** When analyzing complex tasks or projects:
256
+ 1. **Save planning documents to .closer_plan directory**
257
+ - Copy relevant .md files that inform your understanding
258
+ - Document your analysis process and findings
259
+ - Keep track of context files you've reviewed
260
+ 2. **Why this matters:**
261
+ - Creates a traceable record of your thought process
262
+ - Helps maintain context across sessions
263
+ - Enables better project understanding over time
264
+ 3. **When to do this:**
265
+ - Before starting complex multi-step tasks
266
+ - When analyzing project architecture
267
+ - When reviewing documentation for context
268
+ - Before making significant changes
269
+
270
+ **DO NOT** skip this step for complex tasks. It's essential for maintaining project intelligence.
271
+
272
+ ## Multi-Step Task Execution Guide
273
+ When users request complex tasks that require multiple tool calls, you MUST complete ALL steps before providing a summary.
274
+
275
+ ### Task: "Read the entire project" / "Analyze the whole project" / "Read all the code"
276
+ **Required Steps (Do ALL of them):**
277
+ 1. List the src/ directory to see all source files
278
+ 2. Read README.md, package.json, and config files to understand the project
279
+ 3. **Read ALL source code files** (.js, .jsx, .ts, .tsx) in src/ directory
280
+ 4. Analyze the code architecture, module relationships, and data flow
281
+ 5. Identify performance bottlenecks, security issues, or design problems
282
+ 6. Provide a comprehensive summary including:
283
+ - Project purpose and functionality
284
+ - Technical architecture
285
+ - Code quality assessment
286
+ - Performance concerns
287
+ - Design issues or improvements
288
+
289
+ ### Task: "Search for X in the codebase"
290
+ **Required Steps:**
291
+ 1. Use searchFiles to find relevant files
292
+ 2. Use searchCode to search within file contents
293
+ 3. Read the matching files to understand context
294
+ 4. Provide specific findings with file names and line numbers
295
+
296
+ ## Your Capabilities
297
+ You have access to tools that allow you to:
298
+ - **bash**: Execute bash commands (ls, cat, grep, npm, git, etc.)
299
+ - **readFile**: Read file contents
300
+ - **writeFile**: Create or modify files
301
+ - **editFile**: Replace text in files
302
+ - **searchFiles**: Find files by pattern
303
+ - **searchCode**: Search within file contents
304
+
305
+ ## Current Context
306
+ Working Directory: ${this.config.behavior.workingDir}
307
+ Available Tools: ${this.config.tools.enabled.join(', ')}
308
+ ${projectInfo ? `
309
+ ## Project Patterns
310
+ This is a familiar project. Remember these patterns:
311
+ ${JSON.stringify(projectInfo.patterns, null, 2)}
312
+ ` : ''}
313
+
314
+ ## Behavior Configuration
315
+ - Auto Plan: ${this.config.behavior.autoPlan ? 'Enabled' : 'Disabled'}
316
+ - Auto Execute: ${this.config.behavior.autoExecute ? 'Enabled (low-risk operations only)' : 'Disabled'}
317
+ - Confirm Destructive: ${this.config.behavior.confirmDestructive ? 'Enabled' : 'Disabled'}
318
+
319
+ **Remember: Use tools proactively. Complete ALL steps of multi-step tasks before reporting results.**
320
+
321
+ ${globalClocoContent ? `
322
+ ## 📋 Global Behavior Guidelines (CRITICAL)
323
+ **The following global guidelines from ~/.closer-code/cloco.md are EXTREMELY IMPORTANT and MUST be followed:**
324
+
325
+ ${globalClocoContent}
326
+
327
+ **These global guidelines take precedence over general instructions. Follow them carefully**
328
+ ` : ''}
329
+
330
+ ${projectClocoContent ? `
331
+ ## 📋 Project Behavior Guidelines (CRITICAL)
332
+ **The following project-specific guidelines from ./cloco.md are EXTREMELY IMPORTANT and MUST be followed:**
333
+
334
+ ${projectClocoContent}
335
+
336
+ **These project guidelines take precedence over general instructions. Follow them carefully**
337
+ ` : ''}
338
+
339
+ ${!globalClocoContent && !projectClocoContent ? `
340
+ ## 📋 Behavior Guidelines
341
+ No custom behavior guidelines found. You can add them by:
342
+ - Creating ~/.closer-code/cloco.md for global guidelines
343
+ - Creating ./cloco.md for project-specific guidelines
344
+ ` : ''}`
345
+ + (this.workflowTest ? WORKFLOW_SYSTEM_PROMPT : '');
346
+ }
347
+
348
+ /**
349
+ * 发送消息并获取响应(使用 SDK,手动处理工具调用循环以支持进度)
350
+ *
351
+ * 工具调用循环:
352
+ * 1. 发送消息给 AI
353
+ * 2. 如果 AI 调用工具,执行工具并发送结果回 AI
354
+ * 3. 重复直到 AI 完成响应
355
+ */
356
+ async sendMessage(userMessage, onProgress = null, options = {}) {
357
+ if (this.isProcessing) {
358
+ throw new Error('Already processing a message');
359
+ }
360
+ this.isProcessing = true;
361
+
362
+ try {
363
+ // 记录用户消息
364
+ await logUserMessage(userMessage);
365
+
366
+ // 添加用户消息
367
+ this.messages.push({
368
+ role: MessageType.USER,
369
+ content: userMessage
370
+ });
371
+
372
+ // 获取 AI 客户端
373
+ const aiClient = createAIClient(this.config);
374
+
375
+ // 获取工具定义(使用 Zod 工具)
376
+ const tools = getToolDefinitions(this.config.tools.enabled);
377
+
378
+ // 手动处理工具调用循环 - 全部使用流式 API
379
+ let currentMessages = [...this.messages];
380
+ let fullTextContent = '';
381
+ let hasToolCalls = false;
382
+
383
+ await logAIRequest(currentMessages, { system: this.systemPrompt, tools: tools.map(t => t.name) });
384
+
385
+ // 工具调用循环
386
+ while (true) {
387
+ // 使用流式 API 发送消息
388
+ const response = await aiClient.chatStream(
389
+ currentMessages,
390
+ {
391
+ system: this.systemPrompt,
392
+ tools: tools,
393
+ temperature: 0.7,
394
+ thinking: process.env.CLOSER_THINKING_ENABLED !== '0' ? { type: 'enabled', budget_tokens: 20000 } : { type: 'disabled' }
395
+ },
396
+ (chunk) => {
397
+ // 处理流式事件
398
+ if (chunk.type === 'thinking') {
399
+ if (typeof onProgress === 'function') {
400
+ onProgress({
401
+ type: 'thinking',
402
+ delta: chunk.delta, // 增量内容
403
+ snapshot: chunk.snapshot // 完整快照
404
+ });
405
+ }
406
+ } else if (chunk.type === 'signature') {
407
+ if (typeof onProgress === 'function') {
408
+ onProgress({
409
+ type: 'thinking_signature',
410
+ signature: chunk.signature
411
+ });
412
+ }
413
+ } else if (chunk.type === 'text') {
414
+ // 真正的流式文本
415
+ if (typeof onProgress === 'function') {
416
+ onProgress({
417
+ type: 'token',
418
+ content: chunk.delta
419
+ });
420
+ }
421
+ } else if (chunk.type === 'content_block_start') {
422
+ // 检测到工具调用块开始
423
+ if (chunk.blockType === 'tool_use') {
424
+ if (typeof onProgress === 'function') {
425
+ onProgress({
426
+ type: 'tool_use_start',
427
+ toolName: chunk.block.name,
428
+ toolId: chunk.block.id
429
+ });
430
+ }
431
+ }
432
+ }
433
+ }
434
+ );
435
+
436
+ // 检查是否有工具调用
437
+ const toolUseBlocks = response.content.filter(block => block.type === 'tool_use');
438
+
439
+ if (toolUseBlocks.length === 0) {
440
+ // 没有工具调用,提取最终文本内容(用于保存历史)
441
+ fullTextContent = response.content
442
+ .filter(block => block.type === 'text')
443
+ .map(block => block.text)
444
+ .join('\n');
445
+
446
+ // 更新消息历史
447
+ this.messages.push({
448
+ role: MessageType.ASSISTANT,
449
+ content: response.content
450
+ });
451
+ break;
452
+ }
453
+
454
+ hasToolCalls = true;
455
+
456
+ // 添加助手响应(包含工具调用)到消息历史
457
+ this.messages.push({
458
+ role: MessageType.ASSISTANT,
459
+ content: response.content
460
+ });
461
+
462
+ // 处理工具调用
463
+ for (const block of toolUseBlocks) {
464
+ if (typeof onProgress === 'function') {
465
+ onProgress({
466
+ type: 'tool_start',
467
+ tool: block.name,
468
+ input: block.input
469
+ });
470
+ }
471
+
472
+ // 检测 AI Planning
473
+ const detectedPlan = this.detectAIPlanning(block.name, block.input);
474
+ if (detectedPlan && typeof onProgress === 'function') {
475
+ onProgress({
476
+ type: 'plan_created',
477
+ plan: detectedPlan
478
+ });
479
+ }
480
+
481
+ // 查找对应的 betaZodTool
482
+ const tool = tools.find(t => t.name === block.name);
483
+ if (!tool) {
484
+ throw new Error(`Tool ${block.name} not found`);
485
+ }
486
+
487
+ // 执行工具
488
+ const result = await tool.run(block.input);
489
+
490
+ // 记录工具调用
491
+ await logToolCall(block.name, block.input, result);
492
+
493
+ // 更新 AI Planning 步骤
494
+ const planUpdated = this.updateAIPlanningStep(block.name, result);
495
+ if (planUpdated && typeof onProgress === 'function') {
496
+ onProgress({
497
+ type: 'plan_progress',
498
+ plan: this.currentPlan
499
+ });
500
+ }
501
+
502
+ if (typeof onProgress === 'function') {
503
+ onProgress({
504
+ type: 'tool_complete',
505
+ tool: block.name,
506
+ result: JSON.parse(result)
507
+ });
508
+ }
509
+
510
+ // 添加工具结果到 currentMessages(用于下一次 AI 请求)
511
+ currentMessages.push({
512
+ role: MessageType.ASSISTANT,
513
+ content: response.content
514
+ });
515
+
516
+ currentMessages.push({
517
+ role: 'user',
518
+ content: [{
519
+ type: 'tool_result',
520
+ tool_use_id: block.id,
521
+ content: result
522
+ }]
523
+ });
524
+
525
+ // 同时添加到 this.messages(用于保存历史)
526
+ this.messages.push({
527
+ role: 'user',
528
+ content: [{
529
+ type: 'tool_result',
530
+ tool_use_id: block.id,
531
+ content: result
532
+ }]
533
+ });
534
+ }
535
+ }
536
+
537
+ // 提取最终文本内容
538
+ const lastMessage = this.messages[this.messages.length - 1];
539
+ let textContent = fullTextContent;
540
+
541
+ if (!textContent && lastMessage?.content) {
542
+ // 先提取 thinking 相关内容(包括 thinking 和 redacted_thinking)
543
+ const thinkingBlocks = lastMessage.content.filter(block =>
544
+ block.type === 'thinking' || block.type === 'redacted_thinking'
545
+ );
546
+
547
+ if (thinkingBlocks.length > 0 && typeof onProgress === 'function') {
548
+ for (const block of thinkingBlocks) {
549
+ if (block.type === 'thinking') {
550
+ onProgress({
551
+ type: 'thinking',
552
+ content: block.thinking,
553
+ signature: block.signature
554
+ });
555
+ } else if (block.type === 'redacted_thinking') {
556
+ onProgress({
557
+ type: 'thinking_redacted',
558
+ content: block.data
559
+ });
560
+ }
561
+ }
562
+ }
563
+
564
+ // 再提取文本内容
565
+ textContent = lastMessage.content
566
+ .filter(block => block.type === 'text')
567
+ .map(block => block.text)
568
+ .join('\n') || '';
569
+ }
570
+
571
+ // 记录 AI 响应
572
+ await logAIResponse({ content: [{ type: 'text', text: textContent }] });
573
+
574
+ // 保存历史
575
+ saveHistory(this.messages);
576
+
577
+ return {
578
+ content: textContent,
579
+ toolCalls: hasToolCalls ? ['executed'] : []
580
+ };
581
+ } catch (error) {
582
+ await logAIError(error);
583
+ throw error;
584
+ } finally {
585
+ this.isProcessing = false;
586
+ }
587
+ }
588
+
589
+ /**
590
+ * 发送消息(流式响应)
591
+ * 注意:toolRunner 不支持流式,所以这里使用普通的 chatStream
592
+ */
593
+ async sendMessageStream(userMessage, onProgress = null) {
594
+ if (this.isProcessing) {
595
+ throw new Error('Already processing a message');
596
+ }
597
+ this.isProcessing = true;
598
+
599
+ try {
600
+ // 记录用户消息
601
+ await logUserMessage(userMessage);
602
+
603
+ // 添加用户消息
604
+ this.messages.push({
605
+ role: MessageType.USER,
606
+ content: userMessage
607
+ });
608
+
609
+ // 获取 AI 客户端
610
+ const aiClient = createAIClient(this.config);
611
+
612
+ // 获取工具定义
613
+ const tools = getToolDefinitions(this.config.tools.enabled);
614
+
615
+ // 流式响应处理
616
+ let fullResponse = {
617
+ role: 'assistant',
618
+ content: []
619
+ };
620
+
621
+ await logAIRequest(this.messages, { system: this.systemPrompt });
622
+
623
+ await aiClient.chatStream(
624
+ this.messages,
625
+ {
626
+ system: this.systemPrompt,
627
+ tools: tools,
628
+ thinking: options.thinking || (process.env.CLOSER_THINKING_ENABLED !== '0' ? { type: 'enabled', budget_tokens: 20000 } : { type: 'disabled' })
629
+ },
630
+ (chunk) => {
631
+ // 处理 thinking 事件(使用 SDK 事件监听器 API)
632
+ if (chunk.type === 'thinking') {
633
+ if (typeof onProgress === 'function') {
634
+ onProgress({
635
+ type: 'thinking',
636
+ content: chunk.delta, // 增量内容
637
+ snapshot: chunk.snapshot // 完整快照
638
+ });
639
+ }
640
+ }
641
+ // 处理 signature 事件(thinking 签名)
642
+ else if (chunk.type === 'signature') {
643
+ if (typeof onProgress === 'function') {
644
+ onProgress({
645
+ type: 'thinking_signature',
646
+ signature: chunk.signature
647
+ });
648
+ }
649
+ }
650
+ // 处理文本事件
651
+ else if (chunk.type === 'text') {
652
+ if (typeof onProgress === 'function') {
653
+ onProgress({
654
+ type: 'token',
655
+ content: chunk.delta
656
+ });
657
+ }
658
+ }
659
+ }
660
+ );
661
+
662
+ return fullResponse;
663
+ } catch (error) {
664
+ await logAIError(error);
665
+ throw error;
666
+ } finally {
667
+ this.isProcessing = false;
668
+ }
669
+ }
670
+
671
+ /**
672
+ * 清除对话历史
673
+ */
674
+ clearHistory() {
675
+ this.messages = [];
676
+ saveHistory([]);
677
+ }
678
+
679
+ /**
680
+ * 获取对话摘要
681
+ */
682
+ getSummary() {
683
+ return {
684
+ messageCount: this.messages.length,
685
+ hasPlan: !!this.currentPlan,
686
+ planStatus: this.currentPlan?.status,
687
+ lastMessage: this.messages[this.messages.length - 1]
688
+ };
689
+ }
690
+
691
+ /**
692
+ * 导出对话
693
+ */
694
+ export() {
695
+ return {
696
+ messages: this.messages,
697
+ plan: this.currentPlan,
698
+ summary: this.getSummary()
699
+ };
700
+ }
701
+
702
+ /**
703
+ * 导入对话
704
+ */
705
+ import(data) {
706
+ this.messages = data.messages || [];
707
+ this.currentPlan = data.plan || null;
708
+ saveHistory(this.messages);
709
+ }
710
+
711
+ /**
712
+ * 创建计划
713
+ */
714
+ createPlan(description, type = PlanType.AUTO) {
715
+ const plan = new Plan(description, type);
716
+ this.currentPlan = plan;
717
+ return plan;
718
+ }
719
+
720
+ /**
721
+ * 执行计划(/plan 命令)
722
+ */
723
+ async planAndExecute(taskDescription, onProgress) {
724
+ try {
725
+ // 创建计划
726
+ const plan = this.createPlan(taskDescription, PlanType.COMMAND);
727
+
728
+ if (typeof onProgress === 'function') {
729
+ onProgress({
730
+ type: 'plan_created',
731
+ plan
732
+ });
733
+ }
734
+
735
+ // 让 AI 分析任务并生成步骤
736
+ plan.start();
737
+
738
+ // 添加分析步骤
739
+ const analysisStep = plan.addStep('分析任务需求');
740
+ plan.updateStep(analysisStep.id, StepStatus.IN_PROGRESS);
741
+
742
+ // 发送任务给 AI,让它生成执行步骤
743
+ const prompt = `请分析以下任务,并生成详细的执行步骤列表。每个步骤应该是一个具体的、可执行的操作。
744
+
745
+ 任务:${taskDescription}
746
+
747
+ 请以 JSON 格式返回步骤列表,格式如下:
748
+ [
749
+ {"description": "步骤1描述"},
750
+ {"description": "步骤2描述"},
751
+ ...
752
+ ]
753
+
754
+ 只返回 JSON,不要其他内容。`;
755
+
756
+ const analysis = await this.sendMessage(prompt);
757
+
758
+ // 解析 AI 返回的步骤
759
+ let steps = [];
760
+ try {
761
+ // 尝试从响应中提取 JSON
762
+ const jsonMatch = analysis.content.match(/\[[\s\S]*\]/);
763
+ if (jsonMatch) {
764
+ steps = JSON.parse(jsonMatch[0]);
765
+ }
766
+ } catch (error) {
767
+ console.error('Failed to parse steps:', error);
768
+ // 如果解析失败,使用默认步骤
769
+ steps = [{ description: taskDescription }];
770
+ }
771
+
772
+ plan.updateStep(analysisStep.id, StepStatus.COMPLETED);
773
+
774
+ // 添加解析出的步骤
775
+ steps.forEach(step => {
776
+ plan.addStep(step.description);
777
+ });
778
+
779
+ if (typeof onProgress === 'function') {
780
+ onProgress({
781
+ type: 'plan_ready',
782
+ plan
783
+ });
784
+ }
785
+
786
+ // 逐步执行
787
+ for (const step of plan.steps) {
788
+ if (step.status === StepStatus.PENDING) {
789
+ plan.updateStep(step.id, StepStatus.IN_PROGRESS);
790
+
791
+ if (typeof onProgress === 'function') {
792
+ onProgress({
793
+ type: 'step_start',
794
+ plan,
795
+ step
796
+ });
797
+ }
798
+
799
+ // 让 AI 执行这个步骤
800
+ try {
801
+ const result = await this.sendMessage(`执行步骤:${step.description}`);
802
+ plan.updateStep(step.id, StepStatus.COMPLETED, result);
803
+
804
+ if (typeof onProgress === 'function') {
805
+ onProgress({
806
+ type: 'step_complete',
807
+ plan,
808
+ step
809
+ });
810
+ }
811
+ } catch (error) {
812
+ plan.updateStep(step.id, StepStatus.FAILED, error.message);
813
+
814
+ if (typeof onProgress === 'function') {
815
+ onProgress({
816
+ type: 'step_failed',
817
+ plan,
818
+ step,
819
+ error
820
+ });
821
+ }
822
+
823
+ // 失败后停止执行
824
+ plan.fail(error.message);
825
+ break;
826
+ }
827
+ }
828
+ }
829
+
830
+ return {
831
+ success: plan.status !== PlanStatus.FAILED,
832
+ plan
833
+ };
834
+ } catch (error) {
835
+ console.error('planAndExecute error:', error);
836
+ if (this.currentPlan) {
837
+ this.currentPlan.fail(error.message);
838
+ }
839
+ throw error;
840
+ }
841
+ }
842
+
843
+ /**
844
+ * 学习项目模式(/learn 命令)
845
+ */
846
+ async learnProject() {
847
+ const plan = this.createPlan('学习项目模式和代码结构', PlanType.COMMAND);
848
+ plan.start();
849
+
850
+ // 添加学习步骤
851
+ const steps = [
852
+ '读取项目配置文件 (package.json, README.md)',
853
+ '分析源代码目录结构',
854
+ '识别主要模块和依赖关系',
855
+ '总结项目模式和最佳实践'
856
+ ];
857
+
858
+ steps.forEach(desc => plan.addStep(desc));
859
+
860
+ // 执行学习
861
+ for (const step of plan.steps) {
862
+ step.status = StepStatus.IN_PROGRESS;
863
+
864
+ try {
865
+ // 根据步骤描述执行相应的操作
866
+ if (step.description.includes('package.json')) {
867
+ await this.sendMessage('读取并分析 package.json 文件');
868
+ } else if (step.description.includes('README')) {
869
+ await this.sendMessage('读取并分析 README.md 文件');
870
+ } else if (step.description.includes('目录结构')) {
871
+ await this.sendMessage('列出并分析项目的目录结构');
872
+ } else if (step.description.includes('模块')) {
873
+ await this.sendMessage('分析项目的主要模块和依赖关系');
874
+ } else if (step.description.includes('总结')) {
875
+ await this.sendMessage('总结这个项目的模式和最佳实践');
876
+ }
877
+
878
+ step.status = StepStatus.COMPLETED;
879
+ } catch (error) {
880
+ step.status = StepStatus.FAILED;
881
+ step.result = error.message;
882
+ }
883
+ }
884
+
885
+ plan.complete();
886
+
887
+ return {
888
+ success: true,
889
+ plan
890
+ };
891
+ }
892
+
893
+ /**
894
+ * 检测并创建 AI Planning(自动检测)
895
+ */
896
+ detectAIPlanning(toolName, toolInput) {
897
+ // 检测是否在写入 .closer_plan/ 目录
898
+ if (toolName === 'writeFile' && toolInput.filePath) {
899
+ const filePath = toolInput.filePath;
900
+ if (filePath.includes('.closer_plan/') || filePath.includes('.closer_plan\\')) {
901
+ // 提取文件名作为任务描述
902
+ const fileName = filePath.split('/').pop().split('\\').pop();
903
+ const description = `AI Planning: ${fileName.replace('.md', '')}`;
904
+
905
+ // 如果当前没有 plan,或者 plan 类型不匹配,创建新的
906
+ if (!this.currentPlan || this.currentPlan.type !== PlanType.AUTO) {
907
+ const plan = this.createPlan(description, PlanType.AUTO);
908
+ plan.start();
909
+
910
+ // 添加步骤
911
+ plan.addStep('分析任务需求');
912
+ plan.addStep('执行操作');
913
+ plan.addStep('生成规划文档');
914
+
915
+ // 标记第一个步骤为进行中
916
+ const firstStep = plan.steps[0];
917
+ plan.updateStep(firstStep.id, StepStatus.IN_PROGRESS);
918
+
919
+ return plan;
920
+ }
921
+ }
922
+ }
923
+
924
+ return null;
925
+ }
926
+
927
+ /**
928
+ * 更新 AI Planning 步骤
929
+ */
930
+ updateAIPlanningStep(toolName, result) {
931
+ if (this.currentPlan && this.currentPlan.type === PlanType.AUTO) {
932
+ const currentStep = this.currentPlan.getCurrentStep();
933
+ if (currentStep) {
934
+ // 标记当前步骤完成
935
+ this.currentPlan.updateStep(currentStep.id, StepStatus.COMPLETED, result);
936
+
937
+ // 开始下一个步骤
938
+ const nextStep = this.currentPlan.getNextStep();
939
+ if (nextStep) {
940
+ this.currentPlan.updateStep(nextStep.id, StepStatus.IN_PROGRESS);
941
+ } else {
942
+ // 所有步骤完成
943
+ this.currentPlan.complete();
944
+ }
945
+
946
+ return true;
947
+ }
948
+ }
949
+ return false;
950
+ }
951
+ }
952
+
953
+ /**
954
+ * 创建对话会话(使用 SDK)
955
+ * @param {Object} config - 配置对象
956
+ * @param {boolean} workflowTest - 是否为 workflow 测试模式
957
+ */
958
+ export async function createConversation(config, workflowTest = false) {
959
+ const conversation = new Conversation(config, workflowTest);
960
+ await conversation.initialize();
961
+ return conversation;
962
+ }