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
package/src/logger.js ADDED
@@ -0,0 +1,347 @@
1
+ /**
2
+ * 调试日志模块
3
+ */
4
+
5
+ import fs from 'fs/promises';
6
+ import path from 'path';
7
+ import { homedir } from 'os';
8
+
9
+ const LOG_DIR = path.join(homedir(), '.closer-code/logs');
10
+ const DEBUG_ENABLED = process.env.CLOSER_DEBUG_LOG === '1';
11
+
12
+ let logFile = null;
13
+ let logStream = null;
14
+
15
+ /**
16
+ * 初始化日志
17
+ */
18
+ export async function initLogger() {
19
+ if (!DEBUG_ENABLED) return;
20
+
21
+ try {
22
+ await fs.mkdir(LOG_DIR, { recursive: true });
23
+
24
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
25
+ logFile = path.join(LOG_DIR, `closer_debug_log_${timestamp}.log`);
26
+
27
+ logStream = await fs.open(logFile, 'a');
28
+
29
+ await writeLog('=== Closer Code Debug Log Started ===');
30
+ await writeLog(`Log file: ${logFile}`);
31
+ await writeLog(`CLOSER_DEBUG_LOG: ${process.env.CLOSER_DEBUG_LOG}`);
32
+ await writeLog(`Node.js: ${process.version}`);
33
+ await writeLog(`Platform: ${process.platform}`);
34
+ await writeLog(`Working Directory: ${process.cwd()}`);
35
+ await writeLog('');
36
+
37
+ console.error(`[DEBUG] Logging enabled. Log file: ${logFile}`);
38
+
39
+ } catch (error) {
40
+ console.error('Failed to initialize logger:', error.message);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * 写入日志
46
+ */
47
+ async function writeLog(message) {
48
+ if (!DEBUG_ENABLED || !logStream) return;
49
+
50
+ const timestamp = new Date().toISOString();
51
+ const logMessage = `[${timestamp}] ${message}\n`;
52
+
53
+ try {
54
+ await logStream.write(logMessage);
55
+ } catch (error) {
56
+ console.error('Failed to write log:', error.message);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * 格式化对象为日志字符串
62
+ */
63
+ function formatObject(obj, indent = 0) {
64
+ const spaces = ' '.repeat(indent);
65
+
66
+ if (obj === null) return 'null';
67
+ if (obj === undefined) return 'undefined';
68
+ if (typeof obj !== 'object') return String(obj);
69
+
70
+ if (Array.isArray(obj)) {
71
+ if (obj.length === 0) return '[]';
72
+ const items = obj.slice(0, 5).map(item => formatObject(item, indent + 1));
73
+ const truncated = obj.length > 5 ? `\n${spaces}... (${obj.length} items total)` : '';
74
+ return `[\n${spaces}${items.join(',\n' + spaces)}${truncated}\n${spaces.slice(0, -2)}]`;
75
+ }
76
+
77
+ const keys = Object.keys(obj);
78
+ if (keys.length === 0) return '{}';
79
+
80
+ const entries = keys.slice(0, 10).map(key => {
81
+ const value = obj[key];
82
+ if (typeof value === 'object' && value !== null) {
83
+ return `${key}: ${formatObject(value, indent + 1)}`;
84
+ }
85
+ return `${key}: ${JSON.stringify(value)}`;
86
+ });
87
+
88
+ const truncated = keys.length > 10 ? `\n${spaces}... (${keys.length} keys total)` : '';
89
+
90
+ return `{\n${spaces}${entries.join(',\n' + spaces)}${truncated}\n${spaces.slice(0, -2)}}`;
91
+ }
92
+
93
+ /**
94
+ * 记录配置
95
+ */
96
+ export async function logConfig(config) {
97
+ if (!DEBUG_ENABLED) return;
98
+
99
+ await writeLog('--- Configuration ---');
100
+ await writeLog(`AI Provider: ${config.ai.provider}`);
101
+
102
+ if (config.ai.anthropic) {
103
+ await writeLog(`Anthropic API Key: ${config.ai.anthropic.apiKey ? '***' + config.ai.anthropic.apiKey.slice(-4) : 'NOT SET'}`);
104
+ await writeLog(`Anthropic Base URL: ${config.ai.anthropic.baseURL}`);
105
+ await writeLog(`Anthropic Model: ${config.ai.anthropic.model}`);
106
+ await writeLog(`Anthropic Max Tokens: ${config.ai.anthropic.maxTokens}`);
107
+ }
108
+
109
+ if (config.ai.openai) {
110
+ await writeLog(`OpenAI API Key: ${config.ai.openai.apiKey ? '***' + config.ai.openai.apiKey.slice(-4) : 'NOT SET'}`);
111
+ await writeLog(`OpenAI Base URL: ${config.ai.openai.baseURL}`);
112
+ await writeLog(`OpenAI Model: ${config.ai.openai.model}`);
113
+ await writeLog(`OpenAI Max Tokens: ${config.ai.openai.maxTokens}`);
114
+ }
115
+
116
+ await writeLog(`Auto Plan: ${config.behavior.autoPlan}`);
117
+ await writeLog(`Auto Execute: ${config.behavior.autoExecute}`);
118
+ await writeLog(`Working Dir: ${config.behavior.workingDir}`);
119
+ await writeLog(`Enabled Tools: ${config.tools.enabled.join(', ')}`);
120
+ await writeLog('');
121
+ }
122
+
123
+ /**
124
+ * 记录用户消息
125
+ */
126
+ export async function logUserMessage(message) {
127
+ if (!DEBUG_ENABLED) return;
128
+
129
+ await writeLog('--- User Message ---');
130
+ await writeLog(`Content: ${message}`);
131
+ await writeLog(`Length: ${message.length} characters`);
132
+ await writeLog('');
133
+ }
134
+
135
+ /**
136
+ * 记录 AI 请求
137
+ */
138
+ export async function logAIRequest(messages, options) {
139
+ if (!DEBUG_ENABLED) return;
140
+
141
+ await writeLog('--- AI Request ---');
142
+ await writeLog(`Message Count: ${messages.length}`);
143
+ await writeLog(`Temperature: ${options.temperature}`);
144
+ await writeLog(`Max Tokens: ${options.maxTokens || 'N/A'}`);
145
+ await writeLog(`Tool Count: ${options.tools?.length || 0}`);
146
+
147
+ if (options.system) {
148
+ await writeLog(`System Prompt (first 500 chars):\n${options.system.slice(0, 500)}...`);
149
+ }
150
+
151
+ if (options.tools) {
152
+ await writeLog('Available Tools:');
153
+ for (const tool of options.tools) {
154
+ await writeLog(` - ${tool.name}: ${tool.description?.slice(0, 80)}...`);
155
+ }
156
+ }
157
+
158
+ await writeLog('Messages:');
159
+ for (let i = 0; i < messages.length; i++) {
160
+ const msg = messages[i];
161
+ await writeLog(` [${i}] Role: ${msg.role}`);
162
+
163
+ if (msg.content) {
164
+ const content = typeof msg.content === 'string'
165
+ ? msg.content
166
+ : JSON.stringify(msg.content).slice(0, 200);
167
+ await writeLog(` Content: ${content}...`);
168
+ }
169
+
170
+ if (msg.toolCalls) {
171
+ await writeLog(` Tool Calls: ${msg.toolCalls.length}`);
172
+ }
173
+ }
174
+
175
+ await writeLog('');
176
+ }
177
+
178
+ /**
179
+ * 记录流式响应开始
180
+ */
181
+ export async function logStreamStart() {
182
+ if (!DEBUG_ENABLED) return;
183
+
184
+ await writeLog('--- Stream Response Started ---');
185
+ }
186
+
187
+ /**
188
+ * 记录流式响应数据块
189
+ */
190
+ export async function logStreamChunk(chunk) {
191
+ if (!DEBUG_ENABLED) return;
192
+
193
+ await writeLog(`Stream Chunk Type: ${chunk.type}`);
194
+
195
+ if (chunk.type === 'content_block_start') {
196
+ await writeLog(` Block Type: ${chunk.content_block?.type}`);
197
+ if (chunk.content_block?.type === 'tool_use') {
198
+ await writeLog(` Tool Name: ${chunk.content_block.name}`);
199
+ await writeLog(` Tool ID: ${chunk.content_block.id}`);
200
+ }
201
+ }
202
+
203
+ if (chunk.type === 'content_block_delta') {
204
+ if (chunk.delta?.text) {
205
+ await writeLog(` Delta Text: ${chunk.delta?.text || '(empty)'}`);
206
+ await writeLog(` Delta Length: ${chunk.delta?.text?.length || 0}`);
207
+ }
208
+ if (chunk.delta?.partial_json) {
209
+ await writeLog(` Partial JSON: ${chunk.delta.partial_json.slice(0, 100)}...`);
210
+ }
211
+ }
212
+
213
+ if (chunk.type === 'content_block_stop') {
214
+ await writeLog(` Block Type: ${chunk.content_block?.type}`);
215
+ }
216
+
217
+ if (chunk.type === 'message_delta') {
218
+ await writeLog(` Stop Reason: ${chunk.delta?.stop_reason}`);
219
+ }
220
+
221
+ if (chunk.type === 'message_stop') {
222
+ await writeLog(` Message Stop`);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * 记录流式响应结束
228
+ */
229
+ export async function logStreamEnd(fullResponse, toolCalls) {
230
+ if (!DEBUG_ENABLED) return;
231
+
232
+ await writeLog('--- Stream Response Ended ---');
233
+ await writeLog(`Total Response Length: ${fullResponse.length} characters`);
234
+ await writeLog(`Tool Calls Found: ${toolCalls.length}`);
235
+
236
+ if (toolCalls.length > 0) {
237
+ await writeLog('Tool Calls:');
238
+ for (const tc of toolCalls) {
239
+ await writeLog(` - ${tc.name}`);
240
+ await writeLog(` Input: ${JSON.stringify(tc.input).slice(0, 200)}...`);
241
+ }
242
+ }
243
+
244
+ await writeLog(`Full Response (first 500 chars):\n${fullResponse.slice(0, 500)}${fullResponse.length > 500 ? '...' : ''}`);
245
+ await writeLog('');
246
+ }
247
+
248
+ /**
249
+ * 记录工具调用
250
+ */
251
+ export async function logToolCall(toolName, input, result) {
252
+ if (!DEBUG_ENABLED) return;
253
+
254
+ await writeLog('--- Tool Call ---');
255
+ await writeLog(`Tool: ${toolName}`);
256
+ await writeLog(`Input: ${JSON.stringify(input).slice(0, 300)}...`);
257
+ await writeLog(`Success: ${result.success}`);
258
+
259
+ if (result.success) {
260
+ await writeLog(`Result Data: ${JSON.stringify(result.data).slice(0, 300)}...`);
261
+ } else {
262
+ await writeLog(`Error: ${result.error}`);
263
+ }
264
+
265
+ await writeLog('');
266
+ }
267
+
268
+ /**
269
+ * 记录 AI 错误
270
+ */
271
+ export async function logAIError(error) {
272
+ if (!DEBUG_ENABLED) return;
273
+
274
+ await writeLog('--- AI Error ---');
275
+ await writeLog(`Error: ${error.message}`);
276
+ await writeLog(`Stack: ${error.stack}`);
277
+ await writeLog('');
278
+ }
279
+
280
+ /**
281
+ * 记录非流式响应
282
+ */
283
+ export async function logAIResponse(response) {
284
+ if (!DEBUG_ENABLED) return;
285
+
286
+ await writeLog('--- AI Response ---');
287
+ await writeLog(`Model: ${response.model}`);
288
+ await writeLog(`Stop Reason: ${response.stopReason || response.finishReason}`);
289
+ await writeLog(`Content Blocks: ${response.content?.length || 0}`);
290
+
291
+ if (response.content) {
292
+ let fullText = '';
293
+ for (const block of response.content) {
294
+ if (block.type === 'text') {
295
+ fullText += block.text;
296
+ }
297
+ }
298
+ await writeLog(`Response Length: ${fullText.length} characters`);
299
+ await writeLog(`Response (first 500 chars):\n${fullText.slice(0, 500)}${fullText.length > 500 ? '...' : ''}`);
300
+
301
+ const toolUses = response.content.filter(b => b.type === 'tool_use');
302
+ if (toolUses.length > 0) {
303
+ await writeLog(`Tool Uses: ${toolUses.length}`);
304
+ for (const tu of toolUses) {
305
+ await writeLog(` - ${tu.name}: ${JSON.stringify(tu.input).slice(0, 100)}...`);
306
+ }
307
+ }
308
+ }
309
+
310
+ await writeLog('');
311
+ }
312
+
313
+ /**
314
+ * 记录会话摘要
315
+ */
316
+ export async function logSessionSummary(conversation) {
317
+ if (!DEBUG_ENABLED) return;
318
+
319
+ await writeLog('--- Session Summary ---');
320
+ const summary = conversation.getSummary();
321
+ await writeLog(formatObject(summary));
322
+ await writeLog('');
323
+
324
+ await writeLog('=== Closer Code Debug Log Ended ===');
325
+ await closeLogger();
326
+ }
327
+
328
+ /**
329
+ * 关闭日志
330
+ */
331
+ export async function closeLogger() {
332
+ if (!DEBUG_ENABLED || !logStream) return;
333
+
334
+ try {
335
+ await logStream.close();
336
+ console.error(`\n[DEBUG] Log saved to: ${logFile}`);
337
+ } catch (error) {
338
+ console.error('Failed to close logger:', error.message);
339
+ }
340
+ }
341
+
342
+ /**
343
+ * 获取日志文件路径
344
+ */
345
+ export function getLogFile() {
346
+ return logFile;
347
+ }
package/src/plan.js ADDED
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Plan 类 - 任务计划管理
3
+ */
4
+
5
+ import crypto from 'crypto';
6
+
7
+ /**
8
+ * 生成唯一ID
9
+ */
10
+ function generateId() {
11
+ return crypto.randomBytes(16).toString('hex');
12
+ }
13
+
14
+ /**
15
+ * Plan 状态
16
+ */
17
+ export const PlanStatus = {
18
+ PENDING: 'pending',
19
+ IN_PROGRESS: 'in_progress',
20
+ COMPLETED: 'completed',
21
+ FAILED: 'failed'
22
+ };
23
+
24
+ /**
25
+ * Step 状态
26
+ */
27
+ export const StepStatus = {
28
+ PENDING: 'pending',
29
+ IN_PROGRESS: 'in_progress',
30
+ COMPLETED: 'completed',
31
+ FAILED: 'failed'
32
+ };
33
+
34
+ /**
35
+ * Plan 类型
36
+ */
37
+ export const PlanType = {
38
+ COMMAND: 'command', // /plan 命令创建
39
+ AUTO: 'auto' // AI 自动创建
40
+ };
41
+
42
+ /**
43
+ * Plan 类
44
+ */
45
+ export class Plan {
46
+ constructor(description, type = PlanType.AUTO) {
47
+ this.id = generateId();
48
+ this.type = type;
49
+ this.description = description;
50
+ this.status = PlanStatus.PENDING;
51
+ this.steps = [];
52
+ this.createdAt = Date.now();
53
+ this.updatedAt = Date.now();
54
+ this.metadata = {};
55
+ }
56
+
57
+ /**
58
+ * 添加步骤
59
+ */
60
+ addStep(description) {
61
+ const step = {
62
+ id: generateId(),
63
+ description,
64
+ status: StepStatus.PENDING,
65
+ result: null
66
+ };
67
+ this.steps.push(step);
68
+ this.updatedAt = Date.now();
69
+ return step;
70
+ }
71
+
72
+ /**
73
+ * 开始执行计划
74
+ */
75
+ start() {
76
+ this.status = PlanStatus.IN_PROGRESS;
77
+ this.updatedAt = Date.now();
78
+ }
79
+
80
+ /**
81
+ * 完成计划
82
+ */
83
+ complete() {
84
+ this.status = PlanStatus.COMPLETED;
85
+ this.updatedAt = Date.now();
86
+ }
87
+
88
+ /**
89
+ * 标记计划失败
90
+ */
91
+ fail(error) {
92
+ this.status = PlanStatus.FAILED;
93
+ this.metadata.error = error;
94
+ this.updatedAt = Date.now();
95
+ }
96
+
97
+ /**
98
+ * 更新步骤状态
99
+ */
100
+ updateStep(stepId, status, result = null) {
101
+ const step = this.steps.find(s => s.id === stepId);
102
+ if (step) {
103
+ step.status = status;
104
+ if (result !== null) {
105
+ step.result = result;
106
+ }
107
+ this.updatedAt = Date.now();
108
+
109
+ // 如果所有步骤都完成,标记计划为完成
110
+ if (this.steps.every(s => s.status === StepStatus.COMPLETED)) {
111
+ this.complete();
112
+ }
113
+ }
114
+ return step;
115
+ }
116
+
117
+ /**
118
+ * 获取当前正在执行的步骤
119
+ */
120
+ getCurrentStep() {
121
+ return this.steps.find(s => s.status === StepStatus.IN_PROGRESS);
122
+ }
123
+
124
+ /**
125
+ * 获取下一个待执行的步骤
126
+ */
127
+ getNextStep() {
128
+ return this.steps.find(s => s.status === StepStatus.PENDING);
129
+ }
130
+
131
+ /**
132
+ * 获取进度信息
133
+ */
134
+ getProgress() {
135
+ const total = this.steps.length;
136
+ const completed = this.steps.filter(s => s.status === StepStatus.COMPLETED).length;
137
+ const inProgress = this.steps.filter(s => s.status === StepStatus.IN_PROGRESS).length;
138
+ const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
139
+
140
+ return {
141
+ total,
142
+ completed,
143
+ inProgress,
144
+ percentage,
145
+ failed: this.steps.filter(s => s.status === StepStatus.FAILED).length
146
+ };
147
+ }
148
+
149
+ /**
150
+ * 获取简略的进度摘要
151
+ */
152
+ getSummary() {
153
+ const progress = this.getProgress();
154
+ const statusEmoji = {
155
+ [PlanStatus.PENDING]: '⏳',
156
+ [PlanStatus.IN_PROGRESS]: '⚙️',
157
+ [PlanStatus.COMPLETED]: '✅',
158
+ [PlanStatus.FAILED]: '❌'
159
+ };
160
+
161
+ return `${statusEmoji[this.status]} ${progress.completed}/${progress.total} (${progress.percentage}%)`;
162
+ }
163
+
164
+ /**
165
+ * 转换为简单对象(用于序列化)
166
+ */
167
+ toJSON() {
168
+ return {
169
+ id: this.id,
170
+ type: this.type,
171
+ description: this.description,
172
+ status: this.status,
173
+ steps: this.steps,
174
+ createdAt: this.createdAt,
175
+ updatedAt: this.updatedAt,
176
+ metadata: this.metadata
177
+ };
178
+ }
179
+
180
+ /**
181
+ * 从简单对象创建 Plan 实例
182
+ */
183
+ static fromJSON(data) {
184
+ const plan = new Plan(data.description, data.type);
185
+ plan.id = data.id;
186
+ plan.status = data.status;
187
+ plan.steps = data.steps;
188
+ plan.createdAt = data.createdAt;
189
+ plan.updatedAt = data.updatedAt;
190
+ plan.metadata = data.metadata || {};
191
+ return plan;
192
+ }
193
+ }