claude-coder 1.9.0 → 1.9.2
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/README.md +214 -214
- package/bin/cli.js +155 -155
- package/package.json +55 -55
- package/recipes/_shared/roles/developer.md +11 -11
- package/recipes/_shared/roles/product.md +12 -12
- package/recipes/_shared/roles/tester.md +12 -12
- package/recipes/_shared/test/report-format.md +86 -86
- package/recipes/backend/base.md +27 -27
- package/recipes/backend/components/auth.md +18 -18
- package/recipes/backend/components/crud-api.md +18 -18
- package/recipes/backend/components/file-service.md +15 -15
- package/recipes/backend/manifest.json +20 -20
- package/recipes/backend/test/api-test.md +25 -25
- package/recipes/console/base.md +37 -37
- package/recipes/console/components/modal-form.md +20 -20
- package/recipes/console/components/pagination.md +17 -17
- package/recipes/console/components/search.md +17 -17
- package/recipes/console/components/table-list.md +18 -18
- package/recipes/console/components/tabs.md +14 -14
- package/recipes/console/components/tree.md +15 -15
- package/recipes/console/components/upload.md +15 -15
- package/recipes/console/manifest.json +24 -24
- package/recipes/console/test/crud-e2e.md +47 -47
- package/recipes/h5/base.md +26 -26
- package/recipes/h5/components/animation.md +11 -11
- package/recipes/h5/components/countdown.md +11 -11
- package/recipes/h5/components/share.md +11 -11
- package/recipes/h5/components/swiper.md +11 -11
- package/recipes/h5/manifest.json +21 -21
- package/recipes/h5/test/h5-e2e.md +20 -20
- package/src/commands/auth.js +420 -362
- package/src/commands/setup-modules/helpers.js +100 -100
- package/src/commands/setup-modules/index.js +25 -25
- package/src/commands/setup-modules/mcp.js +115 -115
- package/src/commands/setup-modules/provider.js +260 -260
- package/src/commands/setup-modules/safety.js +47 -47
- package/src/commands/setup-modules/simplify.js +52 -52
- package/src/commands/setup.js +172 -172
- package/src/common/assets.js +245 -245
- package/src/common/config.js +125 -125
- package/src/common/constants.js +55 -55
- package/src/common/indicator.js +260 -260
- package/src/common/interaction.js +170 -170
- package/src/common/logging.js +77 -77
- package/src/common/sdk.js +50 -50
- package/src/common/tasks.js +88 -88
- package/src/common/utils.js +213 -213
- package/src/core/coding.js +33 -33
- package/src/core/go.js +264 -264
- package/src/core/hooks.js +500 -500
- package/src/core/init.js +166 -165
- package/src/core/plan.js +188 -187
- package/src/core/prompts.js +247 -247
- package/src/core/repair.js +36 -36
- package/src/core/runner.js +471 -458
- package/src/core/scan.js +93 -93
- package/src/core/session.js +280 -271
- package/src/core/simplify.js +74 -74
- package/src/core/state.js +105 -105
- package/src/index.js +76 -76
- package/templates/bash-process.md +12 -12
- package/templates/codingSystem.md +65 -65
- package/templates/codingUser.md +17 -17
- package/templates/coreProtocol.md +29 -29
- package/templates/goSystem.md +130 -130
- package/templates/guidance.json +72 -72
- package/templates/planSystem.md +78 -78
- package/templates/planUser.md +8 -8
- package/templates/requirements.example.md +57 -57
- package/templates/scanSystem.md +120 -120
- package/templates/scanUser.md +10 -10
- package/templates/test_rule.md +194 -194
- package/templates/web-testing.md +17 -17
- package/types/index.d.ts +217 -217
package/src/core/coding.js
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { buildSystemPrompt, buildCodingContext } = require('./prompts');
|
|
4
|
-
const { Session } = require('./session');
|
|
5
|
-
const { log } = require('../common/config');
|
|
6
|
-
|
|
7
|
-
async function executeCoding(config, sessionNum, opts = {}) {
|
|
8
|
-
const taskId = opts.taskId || 'unknown';
|
|
9
|
-
const dateStr = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
|
|
10
|
-
|
|
11
|
-
return Session.run('coding', config, {
|
|
12
|
-
sessionNum,
|
|
13
|
-
logFileName: `${taskId}_session_${sessionNum}_${dateStr}.log`,
|
|
14
|
-
label: `coding task=${taskId}`,
|
|
15
|
-
|
|
16
|
-
async execute(session) {
|
|
17
|
-
const prompt = buildCodingContext(sessionNum, opts);
|
|
18
|
-
const queryOpts = session.buildQueryOptions(opts);
|
|
19
|
-
queryOpts.systemPrompt = buildSystemPrompt('coding');
|
|
20
|
-
queryOpts.disallowedTools = ['askUserQuestion'];
|
|
21
|
-
|
|
22
|
-
const { subtype, cost, usage } = await session.runQuery(prompt, queryOpts);
|
|
23
|
-
|
|
24
|
-
if (subtype && subtype !== 'success' && subtype !== 'unknown') {
|
|
25
|
-
log('warn', `session 结束原因: ${subtype}`);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return { cost, tokenUsage: usage, subtype: subtype || 'unknown' };
|
|
29
|
-
},
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
module.exports = { executeCoding };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { buildSystemPrompt, buildCodingContext } = require('./prompts');
|
|
4
|
+
const { Session } = require('./session');
|
|
5
|
+
const { log } = require('../common/config');
|
|
6
|
+
|
|
7
|
+
async function executeCoding(config, sessionNum, opts = {}) {
|
|
8
|
+
const taskId = opts.taskId || 'unknown';
|
|
9
|
+
const dateStr = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
|
|
10
|
+
|
|
11
|
+
return Session.run('coding', config, {
|
|
12
|
+
sessionNum,
|
|
13
|
+
logFileName: `${taskId}_session_${sessionNum}_${dateStr}.log`,
|
|
14
|
+
label: `coding task=${taskId}`,
|
|
15
|
+
|
|
16
|
+
async execute(session) {
|
|
17
|
+
const prompt = buildCodingContext(sessionNum, opts);
|
|
18
|
+
const queryOpts = session.buildQueryOptions(opts);
|
|
19
|
+
queryOpts.systemPrompt = buildSystemPrompt('coding');
|
|
20
|
+
queryOpts.disallowedTools = ['askUserQuestion'];
|
|
21
|
+
|
|
22
|
+
const { subtype, cost, usage } = await session.runQuery(prompt, queryOpts);
|
|
23
|
+
|
|
24
|
+
if (subtype && subtype !== 'success' && subtype !== 'unknown') {
|
|
25
|
+
log('warn', `session 结束原因: ${subtype}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { cost, tokenUsage: usage, subtype: subtype || 'unknown' };
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { executeCoding };
|
package/src/core/go.js
CHANGED
|
@@ -1,264 +1,264 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const readline = require('readline');
|
|
6
|
-
const { buildSystemPrompt } = require('./prompts');
|
|
7
|
-
const { log } = require('../common/config');
|
|
8
|
-
const { assets } = require('../common/assets');
|
|
9
|
-
const { extractResultText } = require('../common/logging');
|
|
10
|
-
const { loadState, saveState } = require('./state');
|
|
11
|
-
const { Session } = require('./session');
|
|
12
|
-
|
|
13
|
-
const GO_DIR_NAME = 'go';
|
|
14
|
-
|
|
15
|
-
// ─── Go State (harness_state.json → go section) ──────────
|
|
16
|
-
|
|
17
|
-
function loadGoState() {
|
|
18
|
-
const state = loadState();
|
|
19
|
-
return state.go || {};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function saveGoState(goData) {
|
|
23
|
-
const state = loadState();
|
|
24
|
-
state.go = { ...state.go, ...goData };
|
|
25
|
-
saveState(state);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// ─── Prompt Builder ───────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
function buildGoPrompt(instruction, opts = {}) {
|
|
31
|
-
const recipesAbsPath = assets.recipesDir();
|
|
32
|
-
const goState = loadGoState();
|
|
33
|
-
|
|
34
|
-
const inputSection = opts.reqFile
|
|
35
|
-
? `用户需求文件路径: ${opts.reqFile}\n先读取该文件了解用户需求。`
|
|
36
|
-
: instruction
|
|
37
|
-
? `用户需求:\n${instruction}`
|
|
38
|
-
: '用户未提供需求,使用对话模式收集。';
|
|
39
|
-
|
|
40
|
-
const modeSection = (instruction || opts.reqFile)
|
|
41
|
-
? '【自动模式】用户已提供需求,直接分析并组装方案,不要提问。'
|
|
42
|
-
: '【对话模式】使用 askUserQuestion 工具,按协议中的顺序向用户提问收集需求。';
|
|
43
|
-
|
|
44
|
-
let memorySection = '';
|
|
45
|
-
if (goState.lastDomain || goState.lastFile) {
|
|
46
|
-
const parts = [];
|
|
47
|
-
if (goState.lastDomain) parts.push(`上次领域: ${goState.lastDomain}`);
|
|
48
|
-
if (goState.lastComponents) parts.push(`上次组件: ${goState.lastComponents.join(', ')}`);
|
|
49
|
-
if (goState.lastTimestamp) parts.push(`时间: ${goState.lastTimestamp}`);
|
|
50
|
-
memorySection = `上次使用记录(仅供参考):${parts.join(' | ')}`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return [
|
|
54
|
-
inputSection,
|
|
55
|
-
'',
|
|
56
|
-
modeSection,
|
|
57
|
-
'',
|
|
58
|
-
`食谱目录绝对路径: ${recipesAbsPath}`,
|
|
59
|
-
'',
|
|
60
|
-
memorySection,
|
|
61
|
-
].filter(Boolean).join('\n');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ─── Content Extraction ──────────────────────────────────
|
|
65
|
-
|
|
66
|
-
function extractGoContent(collected) {
|
|
67
|
-
let fullText = '';
|
|
68
|
-
for (const msg of collected) {
|
|
69
|
-
if (msg.type === 'assistant' && msg.message?.content) {
|
|
70
|
-
for (const block of msg.message.content) {
|
|
71
|
-
if (block.type === 'text' && block.text) fullText += block.text;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const match = fullText.match(/GO_CONTENT_START\s*\n([\s\S]*?)\nGO_CONTENT_END/);
|
|
77
|
-
if (match) return match[1].trim();
|
|
78
|
-
|
|
79
|
-
const resultText = extractResultText(collected);
|
|
80
|
-
if (resultText) {
|
|
81
|
-
const m = resultText.match(/GO_CONTENT_START\s*\n([\s\S]*?)\nGO_CONTENT_END/);
|
|
82
|
-
if (m) return m[1].trim();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function extractDomainFromContent(content) {
|
|
89
|
-
if (!content) return null;
|
|
90
|
-
const match = content.match(/##\s*开发领域\s*\n\s*(\S+)/);
|
|
91
|
-
if (match) {
|
|
92
|
-
const name = match[1];
|
|
93
|
-
const domainMap = { '管理后台': 'console', 'H5': 'h5', '后端': 'backend' };
|
|
94
|
-
for (const [key, val] of Object.entries(domainMap)) {
|
|
95
|
-
if (name.includes(key)) return val;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function extractComponentsFromContent(content) {
|
|
102
|
-
if (!content) return [];
|
|
103
|
-
const section = content.match(/##\s*功能组件\s*\n([\s\S]*?)(?=\n##|$)/);
|
|
104
|
-
if (!section) return [];
|
|
105
|
-
const items = section[1].match(/[-*]\s+\*\*(.+?)\*\*/g) || [];
|
|
106
|
-
return items.map(i => i.replace(/[-*]\s+\*\*|\*\*/g, '').trim());
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ─── Preview & Confirm ───────────────────────────────────
|
|
110
|
-
|
|
111
|
-
async function previewAndConfirm(content) {
|
|
112
|
-
const lines = content.split('\n');
|
|
113
|
-
const previewLines = Math.min(lines.length, 25);
|
|
114
|
-
|
|
115
|
-
console.log('');
|
|
116
|
-
console.log('┌─ 需求方案预览 ────────────────────────────────┐');
|
|
117
|
-
for (let i = 0; i < previewLines; i++) {
|
|
118
|
-
const line = lines[i].length > 52 ? lines[i].slice(0, 49) + '...' : lines[i];
|
|
119
|
-
console.log(`│ ${line.padEnd(52)}│`);
|
|
120
|
-
}
|
|
121
|
-
if (lines.length > previewLines) {
|
|
122
|
-
const msg = `... 共 ${lines.length} 行,完整内容将写入文件`;
|
|
123
|
-
console.log(`│ ${msg.padEnd(52)}│`);
|
|
124
|
-
}
|
|
125
|
-
console.log('└───────────────────────────────────────────────┘');
|
|
126
|
-
|
|
127
|
-
if (!process.stdin.isTTY) return { confirmed: true, supplement: '' };
|
|
128
|
-
|
|
129
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
130
|
-
return new Promise(resolve => {
|
|
131
|
-
rl.question('\n有什么要补充的?(直接回车确认 / 输入补充内容 / 输入 cancel 取消)\n> ', answer => {
|
|
132
|
-
rl.close();
|
|
133
|
-
const trimmed = answer.trim();
|
|
134
|
-
if (trimmed.toLowerCase() === 'cancel') {
|
|
135
|
-
resolve({ confirmed: false, supplement: '' });
|
|
136
|
-
} else {
|
|
137
|
-
resolve({ confirmed: true, supplement: trimmed });
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async function promptProceedToPlan() {
|
|
144
|
-
if (!process.stdin.isTTY) return true;
|
|
145
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
146
|
-
return new Promise(resolve => {
|
|
147
|
-
rl.question('是否继续生成计划并分解任务?(y/n) ', answer => {
|
|
148
|
-
rl.close();
|
|
149
|
-
resolve(/^[Yy]/.test(answer.trim()));
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ─── Go Output ────────────────────────────────────────────
|
|
155
|
-
|
|
156
|
-
function ensureGoDir() {
|
|
157
|
-
const goDir = path.join(assets.projectRoot, '.claude-coder', GO_DIR_NAME);
|
|
158
|
-
if (!fs.existsSync(goDir)) fs.mkdirSync(goDir, { recursive: true });
|
|
159
|
-
return goDir;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function writeGoFile(content) {
|
|
163
|
-
const goDir = ensureGoDir();
|
|
164
|
-
const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
|
|
165
|
-
const fileName = `go_${ts}.md`;
|
|
166
|
-
const filePath = path.join(goDir, fileName);
|
|
167
|
-
fs.writeFileSync(filePath, content, 'utf8');
|
|
168
|
-
return filePath;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// ─── Main Entry ──────────────────────────────────────────
|
|
172
|
-
|
|
173
|
-
async function executeGo(config, input, opts = {}) {
|
|
174
|
-
const instruction = input || '';
|
|
175
|
-
|
|
176
|
-
if (opts.reset) {
|
|
177
|
-
saveGoState({});
|
|
178
|
-
log('ok', 'Go 记忆已重置');
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (opts.reqFile) {
|
|
183
|
-
log('info', `需求文件: ${opts.reqFile}`);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const isAutoMode = !!(instruction || opts.reqFile);
|
|
187
|
-
const mode = isAutoMode ? '自动' : '对话';
|
|
188
|
-
log('info', `Go 模式: ${mode}`);
|
|
189
|
-
|
|
190
|
-
const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
|
|
191
|
-
|
|
192
|
-
const result = await Session.run('go', config, {
|
|
193
|
-
logFileName: `go_${ts}.log`,
|
|
194
|
-
label: isAutoMode ? 'go_auto' : 'go_dialogue',
|
|
195
|
-
|
|
196
|
-
async execute(session) {
|
|
197
|
-
log('info', isAutoMode ? '正在分析需求并组装方案...' : '正在启动对话式需求收集...');
|
|
198
|
-
|
|
199
|
-
const prompt = buildGoPrompt(instruction, opts);
|
|
200
|
-
const queryOpts = session.buildQueryOptions(opts);
|
|
201
|
-
queryOpts.systemPrompt = buildSystemPrompt('go');
|
|
202
|
-
queryOpts.permissionMode = 'plan';
|
|
203
|
-
|
|
204
|
-
if (isAutoMode) {
|
|
205
|
-
queryOpts.disallowedTools = ['askUserQuestion'];
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const { messages } = await session.runQuery(prompt, queryOpts);
|
|
209
|
-
const content = extractGoContent(messages);
|
|
210
|
-
|
|
211
|
-
return { content, collected: messages };
|
|
212
|
-
},
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
if (!result.content) {
|
|
216
|
-
log('error', '无法从 AI 输出中提取方案内容');
|
|
217
|
-
log('info', '请检查日志文件了解详情');
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const { confirmed, supplement } = await previewAndConfirm(result.content);
|
|
222
|
-
if (!confirmed) {
|
|
223
|
-
log('info', '已取消');
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
let finalContent = result.content;
|
|
228
|
-
if (supplement) {
|
|
229
|
-
finalContent += `\n\n## 补充要求\n\n${supplement}`;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const filePath = writeGoFile(finalContent);
|
|
233
|
-
log('ok', `方案已保存: ${filePath}`);
|
|
234
|
-
|
|
235
|
-
const domain = extractDomainFromContent(finalContent);
|
|
236
|
-
const components = extractComponentsFromContent(finalContent);
|
|
237
|
-
const history = (loadGoState().history || []).slice(-9);
|
|
238
|
-
history.push({
|
|
239
|
-
timestamp: new Date().toISOString(),
|
|
240
|
-
requirement: instruction || opts.reqFile || '(对话收集)',
|
|
241
|
-
file: filePath,
|
|
242
|
-
domain,
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
saveGoState({
|
|
246
|
-
lastFile: filePath,
|
|
247
|
-
lastDomain: domain,
|
|
248
|
-
lastComponents: components,
|
|
249
|
-
lastTimestamp: new Date().toISOString(),
|
|
250
|
-
history,
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
console.log('');
|
|
254
|
-
const shouldPlan = await promptProceedToPlan();
|
|
255
|
-
if (shouldPlan) {
|
|
256
|
-
log('info', '开始生成计划并分解任务...');
|
|
257
|
-
const { executePlan } = require('./plan');
|
|
258
|
-
await executePlan(config, '', { reqFile: filePath });
|
|
259
|
-
} else {
|
|
260
|
-
log('info', `方案已保存,稍后可使用: claude-coder plan -r ${filePath}`);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
module.exports = { executeGo };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const { buildSystemPrompt } = require('./prompts');
|
|
7
|
+
const { log } = require('../common/config');
|
|
8
|
+
const { assets } = require('../common/assets');
|
|
9
|
+
const { extractResultText } = require('../common/logging');
|
|
10
|
+
const { loadState, saveState } = require('./state');
|
|
11
|
+
const { Session } = require('./session');
|
|
12
|
+
|
|
13
|
+
const GO_DIR_NAME = 'go';
|
|
14
|
+
|
|
15
|
+
// ─── Go State (harness_state.json → go section) ──────────
|
|
16
|
+
|
|
17
|
+
function loadGoState() {
|
|
18
|
+
const state = loadState();
|
|
19
|
+
return state.go || {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function saveGoState(goData) {
|
|
23
|
+
const state = loadState();
|
|
24
|
+
state.go = { ...state.go, ...goData };
|
|
25
|
+
saveState(state);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── Prompt Builder ───────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
function buildGoPrompt(instruction, opts = {}) {
|
|
31
|
+
const recipesAbsPath = assets.recipesDir();
|
|
32
|
+
const goState = loadGoState();
|
|
33
|
+
|
|
34
|
+
const inputSection = opts.reqFile
|
|
35
|
+
? `用户需求文件路径: ${opts.reqFile}\n先读取该文件了解用户需求。`
|
|
36
|
+
: instruction
|
|
37
|
+
? `用户需求:\n${instruction}`
|
|
38
|
+
: '用户未提供需求,使用对话模式收集。';
|
|
39
|
+
|
|
40
|
+
const modeSection = (instruction || opts.reqFile)
|
|
41
|
+
? '【自动模式】用户已提供需求,直接分析并组装方案,不要提问。'
|
|
42
|
+
: '【对话模式】使用 askUserQuestion 工具,按协议中的顺序向用户提问收集需求。';
|
|
43
|
+
|
|
44
|
+
let memorySection = '';
|
|
45
|
+
if (goState.lastDomain || goState.lastFile) {
|
|
46
|
+
const parts = [];
|
|
47
|
+
if (goState.lastDomain) parts.push(`上次领域: ${goState.lastDomain}`);
|
|
48
|
+
if (goState.lastComponents) parts.push(`上次组件: ${goState.lastComponents.join(', ')}`);
|
|
49
|
+
if (goState.lastTimestamp) parts.push(`时间: ${goState.lastTimestamp}`);
|
|
50
|
+
memorySection = `上次使用记录(仅供参考):${parts.join(' | ')}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return [
|
|
54
|
+
inputSection,
|
|
55
|
+
'',
|
|
56
|
+
modeSection,
|
|
57
|
+
'',
|
|
58
|
+
`食谱目录绝对路径: ${recipesAbsPath}`,
|
|
59
|
+
'',
|
|
60
|
+
memorySection,
|
|
61
|
+
].filter(Boolean).join('\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Content Extraction ──────────────────────────────────
|
|
65
|
+
|
|
66
|
+
function extractGoContent(collected) {
|
|
67
|
+
let fullText = '';
|
|
68
|
+
for (const msg of collected) {
|
|
69
|
+
if (msg.type === 'assistant' && msg.message?.content) {
|
|
70
|
+
for (const block of msg.message.content) {
|
|
71
|
+
if (block.type === 'text' && block.text) fullText += block.text;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const match = fullText.match(/GO_CONTENT_START\s*\n([\s\S]*?)\nGO_CONTENT_END/);
|
|
77
|
+
if (match) return match[1].trim();
|
|
78
|
+
|
|
79
|
+
const resultText = extractResultText(collected);
|
|
80
|
+
if (resultText) {
|
|
81
|
+
const m = resultText.match(/GO_CONTENT_START\s*\n([\s\S]*?)\nGO_CONTENT_END/);
|
|
82
|
+
if (m) return m[1].trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function extractDomainFromContent(content) {
|
|
89
|
+
if (!content) return null;
|
|
90
|
+
const match = content.match(/##\s*开发领域\s*\n\s*(\S+)/);
|
|
91
|
+
if (match) {
|
|
92
|
+
const name = match[1];
|
|
93
|
+
const domainMap = { '管理后台': 'console', 'H5': 'h5', '后端': 'backend' };
|
|
94
|
+
for (const [key, val] of Object.entries(domainMap)) {
|
|
95
|
+
if (name.includes(key)) return val;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function extractComponentsFromContent(content) {
|
|
102
|
+
if (!content) return [];
|
|
103
|
+
const section = content.match(/##\s*功能组件\s*\n([\s\S]*?)(?=\n##|$)/);
|
|
104
|
+
if (!section) return [];
|
|
105
|
+
const items = section[1].match(/[-*]\s+\*\*(.+?)\*\*/g) || [];
|
|
106
|
+
return items.map(i => i.replace(/[-*]\s+\*\*|\*\*/g, '').trim());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── Preview & Confirm ───────────────────────────────────
|
|
110
|
+
|
|
111
|
+
async function previewAndConfirm(content) {
|
|
112
|
+
const lines = content.split('\n');
|
|
113
|
+
const previewLines = Math.min(lines.length, 25);
|
|
114
|
+
|
|
115
|
+
console.log('');
|
|
116
|
+
console.log('┌─ 需求方案预览 ────────────────────────────────┐');
|
|
117
|
+
for (let i = 0; i < previewLines; i++) {
|
|
118
|
+
const line = lines[i].length > 52 ? lines[i].slice(0, 49) + '...' : lines[i];
|
|
119
|
+
console.log(`│ ${line.padEnd(52)}│`);
|
|
120
|
+
}
|
|
121
|
+
if (lines.length > previewLines) {
|
|
122
|
+
const msg = `... 共 ${lines.length} 行,完整内容将写入文件`;
|
|
123
|
+
console.log(`│ ${msg.padEnd(52)}│`);
|
|
124
|
+
}
|
|
125
|
+
console.log('└───────────────────────────────────────────────┘');
|
|
126
|
+
|
|
127
|
+
if (!process.stdin.isTTY) return { confirmed: true, supplement: '' };
|
|
128
|
+
|
|
129
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
130
|
+
return new Promise(resolve => {
|
|
131
|
+
rl.question('\n有什么要补充的?(直接回车确认 / 输入补充内容 / 输入 cancel 取消)\n> ', answer => {
|
|
132
|
+
rl.close();
|
|
133
|
+
const trimmed = answer.trim();
|
|
134
|
+
if (trimmed.toLowerCase() === 'cancel') {
|
|
135
|
+
resolve({ confirmed: false, supplement: '' });
|
|
136
|
+
} else {
|
|
137
|
+
resolve({ confirmed: true, supplement: trimmed });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function promptProceedToPlan() {
|
|
144
|
+
if (!process.stdin.isTTY) return true;
|
|
145
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
146
|
+
return new Promise(resolve => {
|
|
147
|
+
rl.question('是否继续生成计划并分解任务?(y/n) ', answer => {
|
|
148
|
+
rl.close();
|
|
149
|
+
resolve(/^[Yy]/.test(answer.trim()));
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── Go Output ────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
function ensureGoDir() {
|
|
157
|
+
const goDir = path.join(assets.projectRoot, '.claude-coder', GO_DIR_NAME);
|
|
158
|
+
if (!fs.existsSync(goDir)) fs.mkdirSync(goDir, { recursive: true });
|
|
159
|
+
return goDir;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function writeGoFile(content) {
|
|
163
|
+
const goDir = ensureGoDir();
|
|
164
|
+
const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
|
|
165
|
+
const fileName = `go_${ts}.md`;
|
|
166
|
+
const filePath = path.join(goDir, fileName);
|
|
167
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
168
|
+
return filePath;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─── Main Entry ──────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
async function executeGo(config, input, opts = {}) {
|
|
174
|
+
const instruction = input || '';
|
|
175
|
+
|
|
176
|
+
if (opts.reset) {
|
|
177
|
+
saveGoState({});
|
|
178
|
+
log('ok', 'Go 记忆已重置');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (opts.reqFile) {
|
|
183
|
+
log('info', `需求文件: ${opts.reqFile}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const isAutoMode = !!(instruction || opts.reqFile);
|
|
187
|
+
const mode = isAutoMode ? '自动' : '对话';
|
|
188
|
+
log('info', `Go 模式: ${mode}`);
|
|
189
|
+
|
|
190
|
+
const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
|
|
191
|
+
|
|
192
|
+
const result = await Session.run('go', config, {
|
|
193
|
+
logFileName: `go_${ts}.log`,
|
|
194
|
+
label: isAutoMode ? 'go_auto' : 'go_dialogue',
|
|
195
|
+
|
|
196
|
+
async execute(session) {
|
|
197
|
+
log('info', isAutoMode ? '正在分析需求并组装方案...' : '正在启动对话式需求收集...');
|
|
198
|
+
|
|
199
|
+
const prompt = buildGoPrompt(instruction, opts);
|
|
200
|
+
const queryOpts = session.buildQueryOptions(opts);
|
|
201
|
+
queryOpts.systemPrompt = buildSystemPrompt('go');
|
|
202
|
+
queryOpts.permissionMode = 'plan';
|
|
203
|
+
|
|
204
|
+
if (isAutoMode) {
|
|
205
|
+
queryOpts.disallowedTools = ['askUserQuestion'];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const { messages } = await session.runQuery(prompt, queryOpts);
|
|
209
|
+
const content = extractGoContent(messages);
|
|
210
|
+
|
|
211
|
+
return { content, collected: messages };
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (!result.content) {
|
|
216
|
+
log('error', '无法从 AI 输出中提取方案内容');
|
|
217
|
+
log('info', '请检查日志文件了解详情');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const { confirmed, supplement } = await previewAndConfirm(result.content);
|
|
222
|
+
if (!confirmed) {
|
|
223
|
+
log('info', '已取消');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let finalContent = result.content;
|
|
228
|
+
if (supplement) {
|
|
229
|
+
finalContent += `\n\n## 补充要求\n\n${supplement}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const filePath = writeGoFile(finalContent);
|
|
233
|
+
log('ok', `方案已保存: ${filePath}`);
|
|
234
|
+
|
|
235
|
+
const domain = extractDomainFromContent(finalContent);
|
|
236
|
+
const components = extractComponentsFromContent(finalContent);
|
|
237
|
+
const history = (loadGoState().history || []).slice(-9);
|
|
238
|
+
history.push({
|
|
239
|
+
timestamp: new Date().toISOString(),
|
|
240
|
+
requirement: instruction || opts.reqFile || '(对话收集)',
|
|
241
|
+
file: filePath,
|
|
242
|
+
domain,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
saveGoState({
|
|
246
|
+
lastFile: filePath,
|
|
247
|
+
lastDomain: domain,
|
|
248
|
+
lastComponents: components,
|
|
249
|
+
lastTimestamp: new Date().toISOString(),
|
|
250
|
+
history,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
console.log('');
|
|
254
|
+
const shouldPlan = await promptProceedToPlan();
|
|
255
|
+
if (shouldPlan) {
|
|
256
|
+
log('info', '开始生成计划并分解任务...');
|
|
257
|
+
const { executePlan } = require('./plan');
|
|
258
|
+
await executePlan(config, '', { reqFile: filePath });
|
|
259
|
+
} else {
|
|
260
|
+
log('info', `方案已保存,稍后可使用: claude-coder plan -r ${filePath}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = { executeGo };
|