kie-ai-cli 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.
@@ -0,0 +1,1025 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import readline from 'readline';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import { getChatCompletion, sendChatMessage } from '../api.js';
7
+ import { buildCodeContext, buildCodeAgentSystemPrompt, createCodeSessionId, buildDefaultRoutingConfig, initCodeWorkspace, initCodeWorkspaceProfile, isDangerousCommand, listCodeSessions, loadCodeModelMetrics, loadCodeSession, loadCodeWorkspaceProfile, recordCodeModelMetric, removeWeightedModel, saveCodeModelMetrics, saveCodeWorkspaceConfig, selectModelFromRouting, parseAgentResponse, truncateCommandOutput, buildCodeSystemPrompt, buildCodeUserPrompt, loadCodeWorkspaceConfig, resolveCodeRoute, saveCodeSession, upsertWeightedModel, trimConversation, } from '../code.js';
8
+ import { isAbortError } from '../kie-http.js';
9
+ import { formatModelLabel, resolveChatModel, resolveModelPickInput, promptSelectChatModel, } from '../models.js';
10
+ import { initRenderer, marked } from '../utils/renderer.js';
11
+ function parsePositiveInt(raw, fallback) {
12
+ const n = Number.parseInt(raw, 10);
13
+ if (Number.isNaN(n) || n <= 0)
14
+ return fallback;
15
+ return n;
16
+ }
17
+ function printCodeReplHelp() {
18
+ console.log(chalk.cyan('\nCode 模式可用指令:\n'));
19
+ const rows = [
20
+ ['/help', '显示帮助'],
21
+ ['/clear', '清空当前 code 会话上下文'],
22
+ ['/cwd', '显示当前 code 工作目录'],
23
+ ['/init', '在当前工作目录生成 .kie/code.json'],
24
+ ['/model [id|序号]', '查看或切换当前会话模型'],
25
+ ['/route [auto|quality|balanced|fast]', '查看或设置路由策略'],
26
+ ['/models [plan|execute|fix]', '交互式配置分阶段模型池与权重(亦可 add/rm/strategy)'],
27
+ ['/approve on|off', '设置命令执行是否自动确认'],
28
+ ['/sessions [id|序号]', '列出或切换 code 会话'],
29
+ ['/files', '查看当前注入的文件列表'],
30
+ ['/file <路径>', '追加文件到上下文'],
31
+ ['/run <命令>', '在 code 工作目录执行命令并注入输出'],
32
+ ['/tree on|off', '开关项目结构注入'],
33
+ ['/status', '查看当前 code 配置'],
34
+ ['exit / quit', '退出 code 对话'],
35
+ ];
36
+ for (const [cmd, desc] of rows) {
37
+ console.log(` ${chalk.white(cmd.padEnd(18))} ${chalk.gray(desc)}`);
38
+ }
39
+ console.log();
40
+ }
41
+ async function askForConfirmation(question) {
42
+ if (!process.stdin.isTTY)
43
+ return false;
44
+ const rl = readline.createInterface({
45
+ input: process.stdin,
46
+ output: process.stdout,
47
+ });
48
+ return new Promise((resolve) => {
49
+ rl.question(question, (answer) => {
50
+ rl.close();
51
+ const normalized = answer.trim().toLowerCase();
52
+ resolve(normalized === 'y' || normalized === 'yes');
53
+ });
54
+ });
55
+ }
56
+ function parsePhase(input) {
57
+ const normalized = input.trim().toLowerCase();
58
+ if (normalized === 'plan' || normalized === 'execute' || normalized === 'fix') {
59
+ return normalized;
60
+ }
61
+ return null;
62
+ }
63
+ function inferFallbackCommands(task) {
64
+ const commands = [];
65
+ const normalized = task.toLowerCase();
66
+ const quotedPath = task.match(/["']([^"']+\.[a-zA-Z0-9]+)["']/)?.[1] ||
67
+ task.match(/([A-Za-z0-9_./\\-]+\.[A-Za-z0-9]+)/)?.[1];
68
+ if (quotedPath && /(读取|查看|分析|read|inspect|summar)/i.test(task)) {
69
+ if (process.platform === 'win32') {
70
+ commands.push(`type "${quotedPath.replace(/\//g, '\\')}"`);
71
+ }
72
+ else {
73
+ commands.push(`cat "${quotedPath}"`);
74
+ }
75
+ }
76
+ if (commands.length === 0 && /(读取|查看|分析|代码|文件|module|模块)/i.test(task)) {
77
+ commands.push(process.platform === 'win32' ? 'dir' : 'ls');
78
+ }
79
+ if (commands.length === 0 && /(test|测试|lint|构建|build)/i.test(normalized)) {
80
+ commands.push('npm run test');
81
+ }
82
+ return commands.slice(0, 2);
83
+ }
84
+ function printRoutingTable(routing, routeMode, metrics) {
85
+ console.log(chalk.cyan(`\n当前路由: ${routeMode} | strategy: ${routing.strategy}`));
86
+ const phases = ['plan', 'execute', 'fix'];
87
+ phases.forEach((phase) => {
88
+ console.log(chalk.yellow(`\n[${phase}]`));
89
+ routing.phases[phase].forEach((entry, idx) => {
90
+ const m = metrics.metrics[entry.model];
91
+ const successRate = !m || m.runs === 0 ? '-' : `${Math.round((m.success / m.runs) * 100)}%`;
92
+ const latency = !m || m.runs === 0 ? '-' : `${m.avgLatencyMs}ms`;
93
+ console.log(` ${idx + 1}. ${entry.model} w=${entry.weight} success=${successRate} latency=${latency}`);
94
+ });
95
+ });
96
+ console.log();
97
+ }
98
+ export async function codeAction(task, options, config, readStdin) {
99
+ const apiKey = config.get('apiKey');
100
+ const baseUrl = config.get('baseUrl') || 'https://api.kie.ai';
101
+ const codeCwd = path.resolve(process.cwd(), options.cwd || '.');
102
+ if (!fs.existsSync(codeCwd) || !fs.statSync(codeCwd).isDirectory()) {
103
+ console.log(chalk.red(`❌ 无效目录: ${codeCwd}`));
104
+ process.exit(1);
105
+ }
106
+ const rawArgv = process.argv.slice(2);
107
+ const hasNoTree = rawArgv.includes('--no-tree');
108
+ const hasTreeLimit = rawArgv.some((a) => a === '--tree-limit' || a.startsWith('--tree-limit='));
109
+ const hasFileMaxChars = rawArgv.some((a) => a === '--file-max-chars' || a.startsWith('--file-max-chars='));
110
+ const workspace = await loadCodeWorkspaceConfig(codeCwd);
111
+ let workspaceProfile = await loadCodeWorkspaceProfile(codeCwd);
112
+ let modelMetrics = await loadCodeModelMetrics(codeCwd);
113
+ let includeTree = hasNoTree ? false : (workspace?.includeTree ?? options.tree);
114
+ let treeLimit = hasTreeLimit
115
+ ? parsePositiveInt(options.treeLimit, 120)
116
+ : (workspace?.treeLimit ?? parsePositiveInt(options.treeLimit, 120));
117
+ let fileMaxChars = hasFileMaxChars
118
+ ? parsePositiveInt(options.fileMaxChars, 8000)
119
+ : (workspace?.fileMaxChars ?? parsePositiveInt(options.fileMaxChars, 8000));
120
+ let codeModel = resolveChatModel(config, {
121
+ cli: options.model || workspace?.defaultModel,
122
+ });
123
+ let routeMode = resolveCodeRoute(options.route || workspace?.defaultRoute);
124
+ let routingConfig = workspace?.routing || buildDefaultRoutingConfig(codeModel);
125
+ const maxAgentSteps = parsePositiveInt(options.maxSteps, 25);
126
+ const useAgentMode = !options.chat;
127
+ let autoApprove = Boolean(options.yes);
128
+ let activeSessionId = options.newSession
129
+ ? createCodeSessionId()
130
+ : options.session || 'default';
131
+ if (options.init) {
132
+ const initResult = await initCodeWorkspace(codeCwd, {
133
+ includeTree,
134
+ treeLimit,
135
+ fileMaxChars,
136
+ defaultModel: codeModel,
137
+ defaultRoute: routeMode,
138
+ routing: routingConfig,
139
+ });
140
+ await saveCodeWorkspaceConfig(codeCwd, initResult.config);
141
+ const profileInit = await initCodeWorkspaceProfile(codeCwd);
142
+ workspaceProfile = profileInit.profile;
143
+ routingConfig = initResult.config.routing || buildDefaultRoutingConfig(codeModel);
144
+ modelMetrics = await loadCodeModelMetrics(codeCwd);
145
+ if (initResult.created) {
146
+ console.log(chalk.green(`✅ code 工作区初始化完成: ${initResult.configPath}`));
147
+ }
148
+ else {
149
+ console.log(chalk.yellow(`ℹ️ 已存在配置,未覆盖: ${initResult.configPath}`));
150
+ }
151
+ console.log(chalk.green(`✅ 工作区画像已刷新: ${profileInit.profilePath}`));
152
+ const p = profileInit.profile;
153
+ console.log(chalk.cyan('\n📋 工作区分析结果:'));
154
+ if (p.projectName)
155
+ console.log(` 项目名称: ${chalk.bold(p.projectName)}${p.projectDescription ? chalk.gray(` — ${p.projectDescription}`) : ''}`);
156
+ console.log(` 包管理器: ${p.packageManager}`);
157
+ if (p.gitBranch)
158
+ console.log(` Git 分支: ${p.gitBranch}`);
159
+ if (p.nodeVersion)
160
+ console.log(` Node 版本: ${p.nodeVersion}`);
161
+ if (p.frameworks?.length)
162
+ console.log(` 技术栈: ${chalk.yellow(p.frameworks.join(', '))}`);
163
+ if (p.buildTool)
164
+ console.log(` 构建工具: ${p.buildTool}`);
165
+ if (p.testFramework)
166
+ console.log(` 测试框架: ${p.testFramework}`);
167
+ if (p.entryPoints?.length)
168
+ console.log(` 入口文件: ${p.entryPoints.join(', ')}`);
169
+ console.log(` Scripts: ${p.scripts.join(', ') || '(无)'}`);
170
+ console.log(` 语言分布: ${p.languageStats.slice(0, 6).map(l => `${l.ext}(${l.count})`).join(', ')}`);
171
+ console.log(` 目录结构: ${p.topLevelDirs.join(', ')}`);
172
+ if (p.configFiles?.length)
173
+ console.log(` 配置文件: ${p.configFiles.join(', ')}`);
174
+ if (p.dependencies?.length)
175
+ console.log(` 依赖(${p.dependencies.length}): ${p.dependencies.slice(0, 12).join(', ')}${p.dependencies.length > 12 ? ' ...' : ''}`);
176
+ if (p.devDependencies?.length)
177
+ console.log(` 开发依赖(${p.devDependencies.length}): ${p.devDependencies.slice(0, 8).join(', ')}${p.devDependencies.length > 8 ? ' ...' : ''}`);
178
+ if (p.keyFiles?.length)
179
+ console.log(` 关键文件: ${p.keyFiles.join(', ')}`);
180
+ if (p.totalFiles)
181
+ console.log(` 文件总数: ${p.totalFiles}`);
182
+ console.log();
183
+ }
184
+ if (options.init && !task) {
185
+ return;
186
+ }
187
+ const piped = await readStdin();
188
+ let finalTask = '';
189
+ if (piped && task)
190
+ finalTask = `${piped}\n\n${task}`;
191
+ else if (piped)
192
+ finalTask = piped;
193
+ else if (task)
194
+ finalTask = task;
195
+ if (!apiKey) {
196
+ console.log(chalk.red('❌ 缺少必要配置,请先运行以下命令配置 API Key:'));
197
+ console.log(chalk.yellow('kie config --set-api-key <KEY>'));
198
+ process.exit(1);
199
+ }
200
+ const loadedSession = await loadCodeSession(codeCwd, activeSessionId);
201
+ if (loadedSession && !options.newSession) {
202
+ codeModel = options.model || loadedSession.model || codeModel;
203
+ routeMode = resolveCodeRoute(options.route || loadedSession.route || routeMode);
204
+ autoApprove = Boolean(options.yes || loadedSession.autoApprove);
205
+ }
206
+ const persistWorkspaceConfig = async () => {
207
+ await saveCodeWorkspaceConfig(codeCwd, {
208
+ version: workspace?.version || 1,
209
+ createdAt: workspace?.createdAt || new Date().toISOString(),
210
+ root: codeCwd,
211
+ includeTree,
212
+ treeLimit,
213
+ fileMaxChars,
214
+ defaultModel: codeModel,
215
+ defaultRoute: routeMode,
216
+ routing: routingConfig,
217
+ });
218
+ };
219
+ const extraFiles = loadedSession ? [...loadedSession.extraFiles, ...options.file] : [...options.file];
220
+ const conversation = loadedSession?.conversation || [];
221
+ let activeAbort = null;
222
+ let activeChildProcess = null;
223
+ let interrupted = false;
224
+ const normalizeCommandForPlatform = (cmd) => {
225
+ if (process.platform !== 'win32')
226
+ return cmd;
227
+ const trimmed = cmd.trim();
228
+ if (/^cat\s+/i.test(trimmed)) {
229
+ return trimmed.replace(/^cat\s+/i, 'type ');
230
+ }
231
+ if (/^pwd$/i.test(trimmed)) {
232
+ return 'cd';
233
+ }
234
+ return cmd;
235
+ };
236
+ const persistCodeSession = async () => {
237
+ await saveCodeSession(codeCwd, {
238
+ id: activeSessionId,
239
+ updatedAt: new Date().toISOString(),
240
+ conversation,
241
+ extraFiles,
242
+ model: codeModel,
243
+ route: routeMode,
244
+ autoApprove,
245
+ });
246
+ };
247
+ const renderSessionList = async () => {
248
+ const sessions = await listCodeSessions(codeCwd);
249
+ if (sessions.length === 0) {
250
+ console.log(chalk.gray('暂无 code 会话。\n'));
251
+ return sessions;
252
+ }
253
+ console.log(chalk.cyan('\nCode 会话列表:'));
254
+ sessions.forEach((session, idx) => {
255
+ const current = session.id === activeSessionId ? chalk.green(' (当前)') : '';
256
+ const updated = session.updatedAt ? session.updatedAt.slice(0, 19).replace('T', ' ') : '-';
257
+ console.log(` ${idx + 1}. ${session.id}${current} ${chalk.gray(`消息 ${session.messageCount},更新 ${updated}`)}`);
258
+ });
259
+ console.log();
260
+ return sessions;
261
+ };
262
+ const switchSession = async (targetId) => {
263
+ const loaded = await loadCodeSession(codeCwd, targetId);
264
+ activeSessionId = targetId;
265
+ conversation.length = 0;
266
+ extraFiles.length = 0;
267
+ if (loaded) {
268
+ conversation.push(...loaded.conversation);
269
+ extraFiles.push(...loaded.extraFiles);
270
+ codeModel = loaded.model || codeModel;
271
+ routeMode = resolveCodeRoute(loaded.route || routeMode);
272
+ autoApprove = loaded.autoApprove;
273
+ }
274
+ await persistCodeSession();
275
+ console.log(chalk.green(`✅ 已切换 code 会话: ${activeSessionId}\n`));
276
+ };
277
+ if (!loadedSession) {
278
+ await persistCodeSession();
279
+ }
280
+ const COMMAND_TIMEOUT_MS = 30_000;
281
+ const executeCommandIntoContext = async (cmd) => {
282
+ const actualCmd = normalizeCommandForPlatform(cmd);
283
+ const separator = chalk.gray('─'.repeat(Math.min(process.stdout.columns || 50, 60)));
284
+ try {
285
+ console.log(chalk.bgGray.white(' ▸ ') + ' ' + chalk.dim(actualCmd));
286
+ const { exec } = await import('child_process');
287
+ let stdoutData = '';
288
+ let stderrData = '';
289
+ const p = exec(actualCmd, { cwd: codeCwd, encoding: 'utf-8' });
290
+ activeChildProcess = p;
291
+ p.stdout?.on('data', (d) => { stdoutData += d; process.stdout.write(chalk.dim(d)); });
292
+ p.stderr?.on('data', (d) => { stderrData += d; process.stderr.write(chalk.yellow(d)); });
293
+ await Promise.race([
294
+ new Promise((resolve, reject) => {
295
+ p.on('close', (code) => {
296
+ if (code === 0)
297
+ resolve();
298
+ else
299
+ reject(new Error(`exit code ${code}\n${stderrData}`));
300
+ });
301
+ p.on('error', reject);
302
+ }),
303
+ new Promise((_, reject) => setTimeout(() => {
304
+ p.kill();
305
+ reject(new Error(`命令执行超时 (${COMMAND_TIMEOUT_MS / 1000}s),已终止`));
306
+ }, COMMAND_TIMEOUT_MS)),
307
+ ]);
308
+ activeChildProcess = null;
309
+ const rawOutput = stdoutData.trim();
310
+ const output = truncateCommandOutput(rawOutput);
311
+ conversation.push({
312
+ role: 'user',
313
+ content: [
314
+ `[System] Command executed in '${codeCwd}': ${actualCmd}`,
315
+ '',
316
+ 'stdout:',
317
+ output || '(empty)',
318
+ ].join('\n'),
319
+ });
320
+ console.log(chalk.green(' ✓ 执行成功,输出已注入上下文'));
321
+ console.log(separator);
322
+ }
323
+ catch (err) {
324
+ activeChildProcess = null;
325
+ const errOut = truncateCommandOutput(err?.stderr || err?.message || 'unknown error');
326
+ conversation.push({
327
+ role: 'user',
328
+ content: [
329
+ `[System] Command FAILED in '${codeCwd}': ${actualCmd}`,
330
+ '',
331
+ 'stderr:',
332
+ errOut,
333
+ ].join('\n'),
334
+ });
335
+ console.log(chalk.red(` ✗ 执行失败`));
336
+ console.log(separator);
337
+ }
338
+ await persistCodeSession();
339
+ };
340
+ for (const cmd of options.run) {
341
+ if (cmd.trim())
342
+ await executeCommandIntoContext(cmd.trim());
343
+ }
344
+ const pickModelForPhase = (phase) => selectModelFromRouting(routeMode, codeModel, phase, routingConfig, modelMetrics).model;
345
+ const runCodeTurn = async (userTask) => {
346
+ const context = await buildCodeContext({
347
+ cwd: codeCwd,
348
+ includeFiles: extraFiles,
349
+ includeTree,
350
+ treeMaxEntries: treeLimit,
351
+ maxFileChars: fileMaxChars,
352
+ });
353
+ const userPrompt = [`【代码工作目录】\n${codeCwd}`, buildCodeUserPrompt(userTask, context, workspaceProfile)].join('\n\n');
354
+ const modelForTurn = pickModelForPhase('execute');
355
+ const contextMessages = trimConversation(conversation);
356
+ const messages = [
357
+ { role: 'system', content: buildCodeSystemPrompt(workspaceProfile) },
358
+ ...contextMessages,
359
+ { role: 'user', content: userPrompt },
360
+ ];
361
+ const ac = new AbortController();
362
+ activeAbort = ac;
363
+ const startedAt = Date.now();
364
+ const spinner = ora({
365
+ text: `Code Agent 思考中 (${modelForTurn})...`,
366
+ discardStdin: true,
367
+ }).start();
368
+ let wroteHeader = false;
369
+ let fullResponse = '';
370
+ try {
371
+ await sendChatMessage({
372
+ apiKey,
373
+ baseUrl,
374
+ model: modelForTurn,
375
+ messages,
376
+ signal: ac.signal,
377
+ onChunk: (text) => {
378
+ if (ac.signal.aborted || !text)
379
+ return;
380
+ if (!wroteHeader) {
381
+ spinner.stop();
382
+ process.stdout.write(chalk.green.bold('\nKie Code: '));
383
+ wroteHeader = true;
384
+ }
385
+ process.stdout.write(chalk.white(text));
386
+ fullResponse += text;
387
+ },
388
+ });
389
+ spinner.stop();
390
+ process.stdout.write('\n\n');
391
+ conversation.push({ role: 'user', content: userTask });
392
+ conversation.push({ role: 'assistant', content: fullResponse || '(空回复)' });
393
+ recordCodeModelMetric(modelMetrics, modelForTurn, true, Date.now() - startedAt);
394
+ await saveCodeModelMetrics(codeCwd, modelMetrics);
395
+ await persistCodeSession();
396
+ }
397
+ catch (error) {
398
+ recordCodeModelMetric(modelMetrics, modelForTurn, false, Date.now() - startedAt);
399
+ await saveCodeModelMetrics(codeCwd, modelMetrics);
400
+ throw error;
401
+ }
402
+ finally {
403
+ activeAbort = null;
404
+ }
405
+ };
406
+ const runCodeAgentLoop = async (initialTask, allowInteractiveConfirm) => {
407
+ let currentTask = initialTask;
408
+ // Build full context once — subsequent steps rely on conversation history
409
+ const fullContext = await buildCodeContext({
410
+ cwd: codeCwd,
411
+ includeFiles: extraFiles,
412
+ includeTree,
413
+ treeMaxEntries: treeLimit,
414
+ maxFileChars: fileMaxChars,
415
+ });
416
+ for (let step = 1; step <= maxAgentSteps; step += 1) {
417
+ if (interrupted) {
418
+ console.log(chalk.yellow('\n⚠️ 已中断 agent 执行。'));
419
+ return;
420
+ }
421
+ const userPrompt = step === 1
422
+ ? [
423
+ `【代码工作目录】\n${codeCwd}`,
424
+ `【当前步骤】\n第 ${step} / ${maxAgentSteps} 轮`,
425
+ buildCodeUserPrompt(currentTask, fullContext, workspaceProfile),
426
+ ].join('\n\n')
427
+ : [
428
+ `【代码工作目录】\n${codeCwd}`,
429
+ `【当前步骤】\n第 ${step} / ${maxAgentSteps} 轮`,
430
+ currentTask,
431
+ ].join('\n\n');
432
+ const modelForLoop = pickModelForPhase(step >= 2 ? 'fix' : 'plan');
433
+ const contextMessages = trimConversation(conversation);
434
+ const messages = [
435
+ { role: 'system', content: buildCodeAgentSystemPrompt(workspaceProfile) },
436
+ ...contextMessages,
437
+ { role: 'user', content: userPrompt },
438
+ ];
439
+ const ac = new AbortController();
440
+ activeAbort = ac;
441
+ const startedAt = Date.now();
442
+ const spinner = ora({
443
+ text: `Code Agent 执行中(第 ${step} 轮 / ${maxAgentSteps},${modelForLoop})...`,
444
+ discardStdin: true,
445
+ }).start();
446
+ let rawResponse = '';
447
+ try {
448
+ rawResponse = await getChatCompletion({
449
+ apiKey,
450
+ baseUrl,
451
+ model: modelForLoop,
452
+ messages,
453
+ signal: ac.signal,
454
+ });
455
+ recordCodeModelMetric(modelMetrics, modelForLoop, true, Date.now() - startedAt);
456
+ await saveCodeModelMetrics(codeCwd, modelMetrics);
457
+ }
458
+ catch (error) {
459
+ recordCodeModelMetric(modelMetrics, modelForLoop, false, Date.now() - startedAt);
460
+ await saveCodeModelMetrics(codeCwd, modelMetrics);
461
+ throw error;
462
+ }
463
+ finally {
464
+ spinner.stop();
465
+ activeAbort = null;
466
+ }
467
+ const parsed = parseAgentResponse(rawResponse);
468
+ let effectiveParsed = parsed;
469
+ let assistantForHistory = rawResponse || '(空回复)';
470
+ if (!effectiveParsed.done && effectiveParsed.commands.length === 0) {
471
+ const fixPrompt = [
472
+ '你上一次回复没有提供可执行命令,导致 agent 无法继续。',
473
+ '请仅返回严格标签格式:<assistant_reply>...</assistant_reply><commands>...</commands><done>true|false</done>',
474
+ '若任务未完成,commands 至少给 1 条可执行命令(例如查看文件、运行测试)。',
475
+ `当前任务: ${currentTask}`,
476
+ `你上一次回复: ${rawResponse ? rawResponse.slice(0, 800) : '(空)'}`,
477
+ ].join('\n');
478
+ // Use a lighter model for format correction to save tokens/time
479
+ const fixModel = pickModelForPhase('fix');
480
+ const fixMessages = trimConversation(conversation);
481
+ const corrected = await getChatCompletion({
482
+ apiKey,
483
+ baseUrl,
484
+ model: fixModel,
485
+ messages: [
486
+ { role: 'system', content: buildCodeAgentSystemPrompt(workspaceProfile) },
487
+ ...fixMessages,
488
+ { role: 'user', content: fixPrompt },
489
+ ],
490
+ });
491
+ effectiveParsed = parseAgentResponse(corrected);
492
+ if (!effectiveParsed.reply || effectiveParsed.reply === '(无文本回复)') {
493
+ effectiveParsed.reply = '已请求模型按可执行格式重试。';
494
+ }
495
+ assistantForHistory = corrected || '(空回复)';
496
+ }
497
+ conversation.push({ role: 'user', content: currentTask });
498
+ conversation.push({ role: 'assistant', content: assistantForHistory });
499
+ await persistCodeSession();
500
+ console.log(chalk.green.bold('\nKie Code Agent:'));
501
+ initRenderer();
502
+ const renderedReply = marked.parse(effectiveParsed.reply || '(无文本回复)');
503
+ process.stdout.write(renderedReply.trimEnd() + '\n');
504
+ if (effectiveParsed.done && effectiveParsed.commands.length === 0) {
505
+ console.log(chalk.green(`✅ Agent 已在第 ${step} 轮完成任务。`));
506
+ return;
507
+ }
508
+ if (effectiveParsed.commands.length === 0) {
509
+ const fallbackCommands = inferFallbackCommands(currentTask);
510
+ if (fallbackCommands.length === 0) {
511
+ console.log(chalk.yellow('⚠️ 模型仍未返回可执行命令,建议 /model 切换更强模型后重试。'));
512
+ return;
513
+ }
514
+ console.log(chalk.yellow(`⚠️ 模型未给命令,已启用兜底命令: ${fallbackCommands.join(' ; ')}`));
515
+ effectiveParsed = {
516
+ ...effectiveParsed,
517
+ done: false,
518
+ commands: fallbackCommands,
519
+ };
520
+ }
521
+ for (const cmd of effectiveParsed.commands) {
522
+ if (isDangerousCommand(cmd)) {
523
+ console.log(chalk.red(`⛔ 已拦截高风险命令: ${cmd}`));
524
+ conversation.push({
525
+ role: 'user',
526
+ content: `[System] Dangerous command blocked and NOT executed: ${cmd}`,
527
+ });
528
+ continue;
529
+ }
530
+ let approved = autoApprove;
531
+ if (!approved && allowInteractiveConfirm) {
532
+ approved = await askForConfirmation(chalk.yellow(`执行命令? ${cmd} (y/N): `));
533
+ }
534
+ if (!approved) {
535
+ console.log(chalk.yellow(`⏭️ 已跳过命令: ${cmd}`));
536
+ conversation.push({
537
+ role: 'user',
538
+ content: `[System] Command skipped by user confirmation policy: ${cmd}`,
539
+ });
540
+ await persistCodeSession();
541
+ continue;
542
+ }
543
+ await executeCommandIntoContext(cmd);
544
+ }
545
+ currentTask =
546
+ `原始任务: ${initialTask}\n\n请基于上方命令的执行结果继续推进。若已全部完成且验证通过,输出 done=true。`;
547
+ }
548
+ console.log(chalk.yellow(`⚠️ 已达到最大轮数 ${maxAgentSteps},请根据当前结果决定是否继续。`));
549
+ };
550
+ if (finalTask.trim()) {
551
+ const onSigint = () => {
552
+ console.log(chalk.yellow('\n正在取消…'));
553
+ interrupted = true;
554
+ activeAbort?.abort();
555
+ if (activeChildProcess) {
556
+ try {
557
+ activeChildProcess.kill();
558
+ }
559
+ catch { }
560
+ activeChildProcess = null;
561
+ }
562
+ };
563
+ process.on('SIGINT', onSigint);
564
+ try {
565
+ if (useAgentMode) {
566
+ await runCodeAgentLoop(finalTask, true);
567
+ }
568
+ else {
569
+ await runCodeTurn(finalTask);
570
+ }
571
+ }
572
+ catch (error) {
573
+ if (isAbortError(error)) {
574
+ console.log(chalk.yellow('\n(已取消)\n'));
575
+ process.exit(130);
576
+ }
577
+ const msg = error instanceof Error ? error.message : String(error);
578
+ console.error(chalk.red(`\n❌ ${msg}\n`));
579
+ process.exit(1);
580
+ }
581
+ finally {
582
+ process.off('SIGINT', onSigint);
583
+ }
584
+ return;
585
+ }
586
+ console.log(chalk.cyan('=================================================='));
587
+ console.log(chalk.cyan(' 🧠 Kie Code 工作台(交互模式)'));
588
+ console.log(chalk.cyan('=================================================='));
589
+ console.log(chalk.gray(`会话: ${activeSessionId}`));
590
+ console.log(chalk.gray(`工作目录: ${codeCwd}`));
591
+ console.log(chalk.gray(`模型: ${formatModelLabel(codeModel)}`));
592
+ console.log(chalk.gray(`路由: ${routeMode} (${routingConfig.strategy})`));
593
+ console.log(chalk.gray(`模式: ${useAgentMode ? 'agent 自动执行' : 'code 对话'}`));
594
+ if (useAgentMode && !autoApprove) {
595
+ console.log(chalk.yellow('提示: 当前为逐条确认命令执行(可 /approve on 开启自动确认)。'));
596
+ }
597
+ if (workspace) {
598
+ console.log(chalk.gray(`工作区配置: tree=${workspace.includeTree ? 'on' : 'off'}, treeLimit=${workspace.treeLimit}, fileMaxChars=${workspace.fileMaxChars}`));
599
+ }
600
+ else {
601
+ console.log(chalk.gray('未检测到 .kie/code.json,可输入 /init 初始化'));
602
+ }
603
+ if (workspaceProfile) {
604
+ const parts = [`pm=${workspaceProfile.packageManager}`];
605
+ if (workspaceProfile.frameworks?.length)
606
+ parts.push(`stack=${workspaceProfile.frameworks.join(',')}`);
607
+ if (workspaceProfile.buildTool)
608
+ parts.push(`build=${workspaceProfile.buildTool}`);
609
+ if (workspaceProfile.testFramework)
610
+ parts.push(`test=${workspaceProfile.testFramework}`);
611
+ parts.push(`files=${workspaceProfile.totalFiles || '?'}`);
612
+ console.log(chalk.gray(`工作区画像: ${parts.join(', ')}`));
613
+ }
614
+ console.log(chalk.gray('输入 /help 查看指令;/sessions 管理会话;exit 退出\n'));
615
+ return new Promise((resolve) => {
616
+ const startREPL = () => {
617
+ const rl = readline.createInterface({
618
+ input: process.stdin,
619
+ output: process.stdout,
620
+ terminal: true,
621
+ prompt: 'kie:code › ',
622
+ });
623
+ let exiting = false;
624
+ let running = false;
625
+ rl.on('close', () => {
626
+ if (exiting)
627
+ resolve();
628
+ });
629
+ rl.on('SIGINT', () => {
630
+ if (activeAbort) {
631
+ activeAbort.abort();
632
+ return;
633
+ }
634
+ if (activeChildProcess) {
635
+ try {
636
+ activeChildProcess.kill();
637
+ }
638
+ catch { }
639
+ activeChildProcess = null;
640
+ console.log(chalk.yellow('\n✘ 已终止命令执行'));
641
+ interrupted = true;
642
+ return;
643
+ }
644
+ exiting = true;
645
+ console.log(chalk.gray('\n👋 已退出 code 对话'));
646
+ rl.close();
647
+ });
648
+ const showPrompt = () => rl.prompt();
649
+ rl.on('line', async (line) => {
650
+ let input = line.trim();
651
+ if (/^kie\s+code\b/i.test(input)) {
652
+ input = input.replace(/^kie\s+code\b/i, '').trim();
653
+ input = input.replace(/^-y\b/i, '').trim();
654
+ if (!input) {
655
+ console.log(chalk.gray('提示:你已在 code REPL 内,直接输入任务即可,无需再写 kie code。\n'));
656
+ showPrompt();
657
+ return;
658
+ }
659
+ }
660
+ if (!input) {
661
+ showPrompt();
662
+ return;
663
+ }
664
+ if (running) {
665
+ console.log(chalk.gray('请等待当前请求完成(Ctrl+C 可取消)\n'));
666
+ showPrompt();
667
+ return;
668
+ }
669
+ if (input === 'exit' || input === 'quit') {
670
+ exiting = true;
671
+ console.log(chalk.gray('👋 已退出 code 对话'));
672
+ rl.close();
673
+ return;
674
+ }
675
+ if (input === '/help') {
676
+ printCodeReplHelp();
677
+ showPrompt();
678
+ return;
679
+ }
680
+ if (input === '/clear') {
681
+ conversation.length = 0;
682
+ await persistCodeSession();
683
+ console.log(chalk.green('✅ code 会话上下文已清空。\n'));
684
+ showPrompt();
685
+ return;
686
+ }
687
+ if (input === '/cwd') {
688
+ console.log(chalk.cyan(`${codeCwd}\n`));
689
+ showPrompt();
690
+ return;
691
+ }
692
+ if (input === '/files') {
693
+ if (extraFiles.length === 0) {
694
+ console.log(chalk.gray('当前没有附加文件。\n'));
695
+ }
696
+ else {
697
+ console.log(chalk.cyan('\n附加文件列表:'));
698
+ extraFiles.forEach((f, i) => console.log(` ${i + 1}. ${f}`));
699
+ console.log();
700
+ }
701
+ showPrompt();
702
+ return;
703
+ }
704
+ if (input === '/model') {
705
+ exiting = false;
706
+ rl.close();
707
+ setTimeout(async () => {
708
+ try {
709
+ const picked = await promptSelectChatModel(codeModel, '选择切换当前会话默认模型');
710
+ if (picked) {
711
+ codeModel = picked;
712
+ await persistWorkspaceConfig();
713
+ await persistCodeSession();
714
+ console.log(chalk.green(`\n✅ 已切换模型: ${formatModelLabel(codeModel)}\n`));
715
+ }
716
+ }
717
+ catch (err) {
718
+ console.log(chalk.red(`\n❌ 模型切换已取消或出错: ${err.message || err}\n`));
719
+ }
720
+ finally {
721
+ startREPL();
722
+ }
723
+ }, 100);
724
+ return;
725
+ }
726
+ if (input.startsWith('/model ')) {
727
+ const picked = resolveModelPickInput(input.slice(7).trim());
728
+ if (!picked) {
729
+ console.log(chalk.gray('已取消模型切换。\n'));
730
+ }
731
+ else {
732
+ codeModel = picked;
733
+ await persistWorkspaceConfig();
734
+ await persistCodeSession();
735
+ console.log(chalk.green(`✅ 已切换模型: ${formatModelLabel(codeModel)}\n`));
736
+ }
737
+ showPrompt();
738
+ return;
739
+ }
740
+ if (input === '/route') {
741
+ console.log(chalk.cyan(`当前路由: ${routeMode}\n`));
742
+ showPrompt();
743
+ return;
744
+ }
745
+ if (input.startsWith('/route ')) {
746
+ routeMode = resolveCodeRoute(input.slice(7).trim());
747
+ await persistWorkspaceConfig();
748
+ await persistCodeSession();
749
+ console.log(chalk.green(`✅ 已设置路由: ${routeMode} (${routingConfig.strategy})\n`));
750
+ showPrompt();
751
+ return;
752
+ }
753
+ if (input === '/models') {
754
+ printRoutingTable(routingConfig, routeMode, modelMetrics);
755
+ showPrompt();
756
+ return;
757
+ }
758
+ const phaseMatch = input.match(/^\/models\s+(plan|execute|fix)$/i);
759
+ if (phaseMatch) {
760
+ const phase = phaseMatch[1].toLowerCase();
761
+ exiting = false;
762
+ rl.close();
763
+ setTimeout(async () => {
764
+ try {
765
+ const pickedModel = await promptSelectChatModel(routingConfig.phases[phase]?.[0]?.model || codeModel, `选择要在 [${phase}] 阶段加入的 Chat 模型`);
766
+ if (pickedModel) {
767
+ const { default: select } = await import('@inquirer/select');
768
+ const weight = await select({
769
+ message: `设置模型 ${pickedModel} 在 [${phase}] 阶段的权重 (权重越高,被选中的概率越大)`,
770
+ choices: [
771
+ { name: '1 (默认)', value: 1 },
772
+ { name: '2', value: 2 },
773
+ { name: '3 (推荐偏好)', value: 3 },
774
+ { name: '5 (高优先级)', value: 5 },
775
+ { name: '10 (极高优先级)', value: 10 },
776
+ ],
777
+ });
778
+ routingConfig = upsertWeightedModel(routingConfig, phase, pickedModel, weight);
779
+ await persistWorkspaceConfig();
780
+ console.log(chalk.green(`\n✅ 已成功配置: phase=${phase}, model=${pickedModel}, weight=${weight}\n`));
781
+ }
782
+ }
783
+ catch (err) {
784
+ console.log(chalk.red(`\n❌ 选择取消或发生错误: ${err.message || err}\n`));
785
+ }
786
+ finally {
787
+ startREPL();
788
+ }
789
+ }, 100);
790
+ return;
791
+ }
792
+ if (input.startsWith('/models strategy ')) {
793
+ const strategy = input.slice('/models strategy '.length).trim().toLowerCase();
794
+ if (strategy !== 'argmax' && strategy !== 'weighted-random') {
795
+ console.log(chalk.red('❌ strategy 仅支持 argmax 或 weighted-random\n'));
796
+ showPrompt();
797
+ return;
798
+ }
799
+ routingConfig = { ...routingConfig, strategy };
800
+ await persistWorkspaceConfig();
801
+ console.log(chalk.green(`✅ 已设置路由策略: ${strategy}\n`));
802
+ showPrompt();
803
+ return;
804
+ }
805
+ if (input.startsWith('/models add ')) {
806
+ const tokens = input.slice('/models add '.length).trim().split(/\s+/);
807
+ const phase = parsePhase(tokens[0] || '');
808
+ const model = tokens[1] || '';
809
+ const weight = parsePositiveInt(tokens[2] || '1', 1);
810
+ if (!phase || !model) {
811
+ console.log(chalk.red('❌ 用法: /models add <plan|execute|fix> <model> [weight]\n'));
812
+ showPrompt();
813
+ return;
814
+ }
815
+ routingConfig = upsertWeightedModel(routingConfig, phase, model, weight);
816
+ await persistWorkspaceConfig();
817
+ console.log(chalk.green(`✅ 已添加模型: phase=${phase}, model=${model}, weight=${weight}\n`));
818
+ showPrompt();
819
+ return;
820
+ }
821
+ if (input.startsWith('/models rm ')) {
822
+ const tokens = input.slice('/models rm '.length).trim().split(/\s+/);
823
+ const phase = parsePhase(tokens[0] || '');
824
+ const model = tokens[1] || '';
825
+ if (!phase || !model) {
826
+ console.log(chalk.red('❌ 用法: /models rm <plan|execute|fix> <model>\n'));
827
+ showPrompt();
828
+ return;
829
+ }
830
+ routingConfig = removeWeightedModel(routingConfig, phase, model);
831
+ await persistWorkspaceConfig();
832
+ console.log(chalk.green(`✅ 已移除模型: phase=${phase}, model=${model}\n`));
833
+ showPrompt();
834
+ return;
835
+ }
836
+ if (input === '/approve on') {
837
+ autoApprove = true;
838
+ await persistCodeSession();
839
+ console.log(chalk.green('✅ 已开启自动确认执行命令。\n'));
840
+ showPrompt();
841
+ return;
842
+ }
843
+ if (input === '/approve off') {
844
+ autoApprove = false;
845
+ await persistCodeSession();
846
+ console.log(chalk.green('✅ 已关闭自动确认,改为逐条确认。\n'));
847
+ showPrompt();
848
+ return;
849
+ }
850
+ if (input === '/sessions') {
851
+ await renderSessionList();
852
+ showPrompt();
853
+ return;
854
+ }
855
+ if (input.startsWith('/sessions ')) {
856
+ const arg = input.slice(10).trim();
857
+ const sessions = await renderSessionList();
858
+ let picked = arg;
859
+ const n = Number.parseInt(arg, 10);
860
+ if (!Number.isNaN(n) && n >= 1 && n <= sessions.length) {
861
+ picked = sessions[n - 1].id;
862
+ }
863
+ if (!picked) {
864
+ console.log(chalk.gray('已取消。\n'));
865
+ }
866
+ else if (picked === activeSessionId) {
867
+ console.log(chalk.gray('已在当前会话。\n'));
868
+ }
869
+ else {
870
+ await switchSession(picked);
871
+ }
872
+ showPrompt();
873
+ return;
874
+ }
875
+ if (input === '/init') {
876
+ const initResult = await initCodeWorkspace(codeCwd, {
877
+ includeTree,
878
+ treeLimit,
879
+ fileMaxChars,
880
+ defaultModel: codeModel,
881
+ defaultRoute: routeMode,
882
+ });
883
+ const profileInit = await initCodeWorkspaceProfile(codeCwd);
884
+ workspaceProfile = profileInit.profile;
885
+ if (initResult.created) {
886
+ console.log(chalk.green(`✅ 初始化完成: ${initResult.configPath}\n`));
887
+ }
888
+ else {
889
+ console.log(chalk.yellow(`ℹ️ 已存在: ${initResult.configPath}\n`));
890
+ }
891
+ console.log(chalk.green(`✅ 工作区画像已刷新: ${profileInit.profilePath}\n`));
892
+ showPrompt();
893
+ return;
894
+ }
895
+ if (input === '/status') {
896
+ console.log(chalk.cyan('\n当前 code 状态:'));
897
+ console.log(` session: ${activeSessionId}`);
898
+ console.log(` cwd: ${codeCwd}`);
899
+ console.log(` model: ${codeModel}`);
900
+ console.log(` route: ${routeMode} (${routingConfig.strategy})`);
901
+ console.log(` autoApprove: ${autoApprove ? 'on' : 'off'}`);
902
+ console.log(` mode: ${useAgentMode ? 'agent' : 'chat'}`);
903
+ console.log(` tree: ${includeTree ? 'on' : 'off'} (limit=${treeLimit})`);
904
+ console.log(` fileMaxChars: ${fileMaxChars}`);
905
+ console.log(` files: ${extraFiles.length}`);
906
+ console.log(` metricsModels: ${Object.keys(modelMetrics.metrics).length}`);
907
+ console.log();
908
+ showPrompt();
909
+ return;
910
+ }
911
+ if (input === '/tree on') {
912
+ includeTree = true;
913
+ await persistWorkspaceConfig();
914
+ await persistCodeSession();
915
+ console.log(chalk.green('✅ 已开启项目结构注入。\n'));
916
+ showPrompt();
917
+ return;
918
+ }
919
+ if (input === '/tree off') {
920
+ includeTree = false;
921
+ await persistWorkspaceConfig();
922
+ await persistCodeSession();
923
+ console.log(chalk.green('✅ 已关闭项目结构注入。\n'));
924
+ showPrompt();
925
+ return;
926
+ }
927
+ if (input.startsWith('/file ')) {
928
+ const raw = input.slice(6).trim();
929
+ const filePath = (raw.startsWith('"') && raw.endsWith('"')) ||
930
+ (raw.startsWith("'") && raw.endsWith("'"))
931
+ ? raw.slice(1, -1)
932
+ : raw;
933
+ if (!filePath) {
934
+ console.log(chalk.yellow('⚠️ 请提供文件路径。\n'));
935
+ showPrompt();
936
+ return;
937
+ }
938
+ try {
939
+ const fullPath = path.resolve(codeCwd, filePath);
940
+ if (!fs.existsSync(fullPath)) {
941
+ console.log(chalk.red(`❌ 文件不存在: ${fullPath}\n`));
942
+ }
943
+ else {
944
+ if (!extraFiles.includes(filePath)) {
945
+ extraFiles.push(filePath);
946
+ await persistCodeSession();
947
+ console.log(chalk.green(`✅ 已添加文件: ${filePath}\n`));
948
+ }
949
+ else {
950
+ console.log(chalk.gray(`ℹ️ 文件已在列表中: ${filePath}\n`));
951
+ }
952
+ }
953
+ }
954
+ catch (err) {
955
+ console.log(chalk.red(`❌ 错误: ${err.message}\n`));
956
+ }
957
+ showPrompt();
958
+ return;
959
+ }
960
+ if (input.startsWith('/run ')) {
961
+ const cmd = input.slice(5).trim();
962
+ if (!cmd) {
963
+ console.log(chalk.yellow('⚠️ 请提供要执行的命令。\n'));
964
+ }
965
+ else {
966
+ await executeCommandIntoContext(cmd);
967
+ }
968
+ showPrompt();
969
+ return;
970
+ }
971
+ running = true;
972
+ rl.pause();
973
+ try {
974
+ if (useAgentMode) {
975
+ await runCodeAgentLoop(input, true);
976
+ }
977
+ else {
978
+ await runCodeTurn(input);
979
+ }
980
+ }
981
+ catch (error) {
982
+ if (isAbortError(error)) {
983
+ console.log(chalk.yellow('\n(已取消)\n'));
984
+ }
985
+ else {
986
+ const msg = error instanceof Error ? error.message : String(error);
987
+ console.log(chalk.red(`\n❌ ${msg}\n`));
988
+ }
989
+ }
990
+ finally {
991
+ running = false;
992
+ interrupted = false;
993
+ rl.resume();
994
+ showPrompt();
995
+ }
996
+ });
997
+ showPrompt();
998
+ };
999
+ startREPL();
1000
+ });
1001
+ }
1002
+ export function registerCodeCommand(program, config, readStdin) {
1003
+ program
1004
+ .command('code')
1005
+ .description('代码模式:支持单次任务、初始化与交互式对话')
1006
+ .argument('[task]', '代码任务描述(不传则进入交互模式)')
1007
+ .option('-m, --model <model>', '指定代码模型')
1008
+ .option('--cwd <dir>', '代码工作目录')
1009
+ .option('--init', '初始化当前 code 工作目录')
1010
+ .option('--run <cmd>', '先执行命令并注入上下文', (val, prev) => [...prev, val], [])
1011
+ .option('--agent', '启用自动执行代理循环')
1012
+ .option('--chat', '切换为纯对话模式')
1013
+ .option('--route <mode>', '模型路由')
1014
+ .option('--max-steps <n>', 'agent 最大执行轮数', '25')
1015
+ .option('-y, --yes', 'agent 模式自动确认执行命令')
1016
+ .option('--session <id>', '使用指定 code 会话')
1017
+ .option('--new-session', '创建新的 code 会话')
1018
+ .option('-f, --file <path>', '附加文件内容', (val, prev) => [...prev, val], [])
1019
+ .option('--no-tree', '不附带项目结构摘要')
1020
+ .option('--tree-limit <n>', '项目结构最大条目数', '120')
1021
+ .option('--file-max-chars <n>', '每个附加文件最大字符数', '8000')
1022
+ .action(async (task, options) => {
1023
+ await codeAction(task, options, config, readStdin);
1024
+ });
1025
+ }