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.
- package/README.en.md +36 -0
- package/README.md +503 -0
- package/dist/api.js +29 -0
- package/dist/code.js +937 -0
- package/dist/commands/chat.js +552 -0
- package/dist/commands/code-command.js +1025 -0
- package/dist/commands/config.js +61 -0
- package/dist/commands/image.js +102 -0
- package/dist/commands/job.js +25 -0
- package/dist/commands/upload.js +41 -0
- package/dist/editor.js +38 -0
- package/dist/image-assist.js +79 -0
- package/dist/image-models.js +336 -0
- package/dist/image-repl.js +525 -0
- package/dist/image-session.js +75 -0
- package/dist/image-templates.js +76 -0
- package/dist/image.js +233 -0
- package/dist/index.js +135 -0
- package/dist/job.js +121 -0
- package/dist/kie-http.js +182 -0
- package/dist/models.js +223 -0
- package/dist/providers/claude.js +63 -0
- package/dist/providers/codex.js +59 -0
- package/dist/providers/index.js +14 -0
- package/dist/providers/openai.js +64 -0
- package/dist/session.js +235 -0
- package/dist/types.js +1 -0
- package/dist/utils/renderer.js +19 -0
- package/package.json +38 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import readline from 'readline';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { sendChatMessage } from './api.js';
|
|
6
|
+
import { readMultilineFromEditor } from './editor.js';
|
|
7
|
+
import { buildAssistSystemPrompt, ensureAssistSystemMessage, looksLikeAssistRefusal, parseDraftFromAssistReply, resolveImageAssistModel, setImageAssistModel, } from './image-assist.js';
|
|
8
|
+
import { formatImageModelLabel, printImageModelPickerMenu, resolveImageModel, resolveImageModelPick, setImageModel, } from './image-models.js';
|
|
9
|
+
import { runImageGeneration } from './image.js';
|
|
10
|
+
import { applyTemplate, printTemplateList, resolveTemplatePick, } from './image-templates.js';
|
|
11
|
+
import { createImageSessionId, emptyDraft, getActiveImageSessionId, loadImageSession, saveImageSession, setActiveImageSessionId, } from './image-session.js';
|
|
12
|
+
import { isAbortError } from './kie-http.js';
|
|
13
|
+
import { formatModelLabel, printModelPickerMenu, resolveModelPickInput, } from './models.js';
|
|
14
|
+
const IMAGE_SLASH_ALIASES = {
|
|
15
|
+
'/h': '/help',
|
|
16
|
+
'/g': '/gen',
|
|
17
|
+
'/t': '/tpl',
|
|
18
|
+
'/a': '/assist',
|
|
19
|
+
};
|
|
20
|
+
function normalizeSlashInput(input) {
|
|
21
|
+
const space = input.indexOf(' ');
|
|
22
|
+
const cmd = (space === -1 ? input : input.slice(0, space)).toLowerCase();
|
|
23
|
+
const alias = IMAGE_SLASH_ALIASES[cmd];
|
|
24
|
+
if (!alias)
|
|
25
|
+
return input;
|
|
26
|
+
return alias + (space === -1 ? '' : input.slice(space));
|
|
27
|
+
}
|
|
28
|
+
function printImageReplHelp() {
|
|
29
|
+
console.log(chalk.cyan('\nImage 工作台指令:\n'));
|
|
30
|
+
const rows = [
|
|
31
|
+
['/help', '显示帮助'],
|
|
32
|
+
['/new', '新建 image 会话'],
|
|
33
|
+
['/model [id|序号]', '切换图片生成模型'],
|
|
34
|
+
['/assist [id|序号]', '切换提示词助手模型(与 kie chat 同款列表)'],
|
|
35
|
+
['/draft', '查看当前草稿'],
|
|
36
|
+
['/set <prompt>', '直接写入草稿(不经助手,可写中文)'],
|
|
37
|
+
['/system', '查看助手系统提示词(当前会话上下文)'],
|
|
38
|
+
['/tpl [id] [主体]', '模板列表或应用内置模板'],
|
|
39
|
+
['/optimize [说明]', '优化当前草稿'],
|
|
40
|
+
['/image-url <url>', '添加图生图参考图'],
|
|
41
|
+
['/ratio <1:1|2:3|3:2|auto>', '设置宽高比'],
|
|
42
|
+
['/quality <medium|high>', '设置质量(支持的模型)'],
|
|
43
|
+
['/gen', '按草稿提交生成并下载'],
|
|
44
|
+
['/clear', '清空草稿与助手对话'],
|
|
45
|
+
['/edit', '用编辑器编辑草稿 prompt'],
|
|
46
|
+
['直接输入', '与助手对话,自动更新 DRAFT'],
|
|
47
|
+
['Ctrl+C', '取消生成;空闲时连按两次退出'],
|
|
48
|
+
['exit / quit', '退出并保存'],
|
|
49
|
+
];
|
|
50
|
+
for (const [cmd, desc] of rows) {
|
|
51
|
+
console.log(` ${chalk.white(cmd.padEnd(24))} ${chalk.gray(desc)}`);
|
|
52
|
+
}
|
|
53
|
+
console.log();
|
|
54
|
+
}
|
|
55
|
+
function printDraftBar(session) {
|
|
56
|
+
const d = session.draft;
|
|
57
|
+
const promptPreview = d.prompt.length > 60 ? `${d.prompt.slice(0, 57)}...` : d.prompt || '(空)';
|
|
58
|
+
console.log(chalk.gray(`\n 草稿: ${chalk.white(promptPreview)} | 生成: ${formatImageModelLabel(session.generationModel)} | 助手: ${formatModelLabel(session.assistModel)} | 参考图: ${d.imageUrls.length} | 比例: ${d.aspectRatio ?? '-'} | /gen 出图\n`));
|
|
59
|
+
}
|
|
60
|
+
export async function runImageRepl(options) {
|
|
61
|
+
const { config, apiKey, baseUrl } = options;
|
|
62
|
+
let sessionId = getActiveImageSessionId(config);
|
|
63
|
+
let session = loadImageSession(config, sessionId);
|
|
64
|
+
if (!session.generationModel) {
|
|
65
|
+
session.generationModel = resolveImageModel(config);
|
|
66
|
+
}
|
|
67
|
+
if (!session.assistModel) {
|
|
68
|
+
session.assistModel = resolveImageAssistModel(config);
|
|
69
|
+
}
|
|
70
|
+
const persist = () => saveImageSession(config, session);
|
|
71
|
+
console.log(chalk.magenta('=================================================='));
|
|
72
|
+
console.log(chalk.magenta(' 🎨 Kie 出图工作台 (助手润色 → /gen 生成) '));
|
|
73
|
+
console.log(chalk.magenta('=================================================='));
|
|
74
|
+
console.log(chalk.gray(`会话: ${sessionId}`));
|
|
75
|
+
printDraftBar(session);
|
|
76
|
+
console.log(chalk.gray('输入 /help 查看指令 | /tpl 模板 | 退出: exit 或连按两次 Ctrl+C\n' +
|
|
77
|
+
'说明: 退出后回到 PS/CMD 是正常的;草稿保存在 image-sessions/,再运行 kie image 可继续\n'));
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
let exiting = false;
|
|
80
|
+
let isProcessing = false;
|
|
81
|
+
let replMode = 'chat';
|
|
82
|
+
let multilineBuffer = [];
|
|
83
|
+
let activeAbort = null;
|
|
84
|
+
let lastIdleSigintAt = 0;
|
|
85
|
+
const PROMPT = '(Image) > ';
|
|
86
|
+
const PROMPT_PICK = '序号 > ';
|
|
87
|
+
const PROMPT_MULTILINE = '... > ';
|
|
88
|
+
const rl = readline.createInterface({
|
|
89
|
+
input: process.stdin,
|
|
90
|
+
output: process.stdout,
|
|
91
|
+
terminal: true,
|
|
92
|
+
prompt: PROMPT,
|
|
93
|
+
});
|
|
94
|
+
const showPrompt = () => rl.prompt();
|
|
95
|
+
const resetInputMode = () => {
|
|
96
|
+
replMode = 'chat';
|
|
97
|
+
multilineBuffer = [];
|
|
98
|
+
rl.setPrompt(PROMPT);
|
|
99
|
+
};
|
|
100
|
+
const runAssist = async (userText, opts) => {
|
|
101
|
+
const systemContent = buildAssistSystemPrompt(session.generationModel, session.draft);
|
|
102
|
+
const userContent = opts?.optimizeOnly
|
|
103
|
+
? `请优化当前草稿的英文绘图 prompt。${userText ? `补充要求: ${userText}` : ''}`
|
|
104
|
+
: userText;
|
|
105
|
+
session.assistMessages.push({ role: 'user', content: userContent });
|
|
106
|
+
session.assistMessages = ensureAssistSystemMessage(session.assistMessages, systemContent);
|
|
107
|
+
const spinner = ora({
|
|
108
|
+
text: `助手思考中 (${session.assistModel})...`,
|
|
109
|
+
discardStdin: false,
|
|
110
|
+
}).start();
|
|
111
|
+
let full = '';
|
|
112
|
+
let wroteHeader = false;
|
|
113
|
+
const ac = new AbortController();
|
|
114
|
+
activeAbort = ac;
|
|
115
|
+
try {
|
|
116
|
+
await sendChatMessage({
|
|
117
|
+
apiKey,
|
|
118
|
+
baseUrl,
|
|
119
|
+
model: session.assistModel,
|
|
120
|
+
messages: session.assistMessages,
|
|
121
|
+
signal: ac.signal,
|
|
122
|
+
onChunk: (text) => {
|
|
123
|
+
if (!text || ac.signal.aborted)
|
|
124
|
+
return;
|
|
125
|
+
if (!wroteHeader) {
|
|
126
|
+
spinner.stop();
|
|
127
|
+
process.stdout.write(chalk.blue.bold('\n助手: '));
|
|
128
|
+
wroteHeader = true;
|
|
129
|
+
}
|
|
130
|
+
process.stdout.write(chalk.white(text));
|
|
131
|
+
full += text;
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
if (!wroteHeader)
|
|
135
|
+
spinner.stop();
|
|
136
|
+
else
|
|
137
|
+
console.log('\n');
|
|
138
|
+
session.assistMessages.push({ role: 'assistant', content: full });
|
|
139
|
+
const parsed = parseDraftFromAssistReply(full);
|
|
140
|
+
if (parsed.displayText) {
|
|
141
|
+
/* already streamed */
|
|
142
|
+
}
|
|
143
|
+
if (parsed.draftPrompt) {
|
|
144
|
+
session.draft.prompt = parsed.draftPrompt;
|
|
145
|
+
console.log(chalk.green('✅ 草稿已更新\n'));
|
|
146
|
+
}
|
|
147
|
+
else if (parsed.keepDraft) {
|
|
148
|
+
console.log(chalk.gray('(草稿保持不变)\n'));
|
|
149
|
+
}
|
|
150
|
+
else if (looksLikeAssistRefusal(full)) {
|
|
151
|
+
console.log(chalk.yellow('⚠️ 助手未改写草稿(可能触发了模型安全策略)。可尝试:\n' +
|
|
152
|
+
' · 换说法(如 anime-style young adult female character)\n' +
|
|
153
|
+
' · /assist 换模型(如 Sonnet)\n' +
|
|
154
|
+
' · /set <描述> 直接写入草稿,或 /edit 编辑\n'));
|
|
155
|
+
}
|
|
156
|
+
else if (parsed.displayText) {
|
|
157
|
+
console.log(chalk.gray('(未解析到 DRAFT 行,草稿未变更;可用 /set 直接写入)\n'));
|
|
158
|
+
}
|
|
159
|
+
persist();
|
|
160
|
+
printDraftBar(session);
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
spinner.stop();
|
|
164
|
+
const last = session.assistMessages[session.assistMessages.length - 1];
|
|
165
|
+
if (last?.role === 'user' && last.content === userContent) {
|
|
166
|
+
session.assistMessages.pop();
|
|
167
|
+
}
|
|
168
|
+
if (isAbortError(err)) {
|
|
169
|
+
console.log(chalk.yellow('\n(已取消)\n'));
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
173
|
+
console.error(chalk.red(`\n❌ ${msg}\n`));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
activeAbort = null;
|
|
178
|
+
isProcessing = false;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const runGenerate = async () => {
|
|
182
|
+
if (!session.draft.prompt.trim()) {
|
|
183
|
+
console.log(chalk.yellow('⚠️ 草稿为空,请先描述画面或 /tpl 应用模板。\n'));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
isProcessing = true;
|
|
187
|
+
const ac = new AbortController();
|
|
188
|
+
activeAbort = ac;
|
|
189
|
+
try {
|
|
190
|
+
const result = await runImageGeneration({
|
|
191
|
+
config,
|
|
192
|
+
apiKey,
|
|
193
|
+
baseUrl,
|
|
194
|
+
prompt: session.draft.prompt,
|
|
195
|
+
model: session.generationModel,
|
|
196
|
+
aspectRatio: session.draft.aspectRatio,
|
|
197
|
+
quality: session.draft.quality,
|
|
198
|
+
imageUrls: session.draft.imageUrls,
|
|
199
|
+
outputDir: path.join(process.cwd(), 'kie-image'),
|
|
200
|
+
download: true,
|
|
201
|
+
signal: ac.signal,
|
|
202
|
+
});
|
|
203
|
+
session.tasks.push({
|
|
204
|
+
taskId: result.taskId,
|
|
205
|
+
prompt: session.draft.prompt,
|
|
206
|
+
model: session.generationModel,
|
|
207
|
+
createdAt: new Date().toISOString(),
|
|
208
|
+
savedPaths: result.savedPaths,
|
|
209
|
+
});
|
|
210
|
+
persist();
|
|
211
|
+
printDraftBar(session);
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
if (!isAbortError(err)) {
|
|
215
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
216
|
+
console.error(chalk.red(`\n❌ ${msg}\n`));
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
console.log(chalk.yellow('\n(已取消)\n'));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
activeAbort = null;
|
|
224
|
+
isProcessing = false;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
const leaveRepl = (reason) => {
|
|
228
|
+
if (exiting)
|
|
229
|
+
return;
|
|
230
|
+
exiting = true;
|
|
231
|
+
persist();
|
|
232
|
+
if (reason === 'sigint') {
|
|
233
|
+
console.log(chalk.gray(`\n👋 已退出 Image 工作台(草稿已保存,会话 ${sessionId})`));
|
|
234
|
+
}
|
|
235
|
+
else if (reason === 'exit') {
|
|
236
|
+
console.log(chalk.gray(`\n👋 再见!草稿已保存 (${sessionId})`));
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
console.log(chalk.gray('\n工作台已关闭(输入流结束)'));
|
|
240
|
+
}
|
|
241
|
+
console.log(chalk.gray('重新进入: kie image\n'));
|
|
242
|
+
rl.close();
|
|
243
|
+
};
|
|
244
|
+
rl.on('close', () => {
|
|
245
|
+
if (exiting)
|
|
246
|
+
resolve();
|
|
247
|
+
});
|
|
248
|
+
rl.on('SIGINT', () => {
|
|
249
|
+
if (activeAbort) {
|
|
250
|
+
activeAbort.abort();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (replMode !== 'chat') {
|
|
254
|
+
resetInputMode();
|
|
255
|
+
console.log(chalk.gray('\n已取消。\n'));
|
|
256
|
+
showPrompt();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const now = Date.now();
|
|
260
|
+
if (now - lastIdleSigintAt < 1500) {
|
|
261
|
+
leaveRepl('sigint');
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
lastIdleSigintAt = now;
|
|
265
|
+
console.log(chalk.gray('\n再按一次 Ctrl+C 将退出工作台(回到 PS/CMD)\n'));
|
|
266
|
+
showPrompt();
|
|
267
|
+
});
|
|
268
|
+
rl.on('line', async (line) => {
|
|
269
|
+
const input = normalizeSlashInput(line.trim());
|
|
270
|
+
if (isProcessing) {
|
|
271
|
+
if (input)
|
|
272
|
+
console.log(chalk.gray('\n请等待完成(Ctrl+C 可取消)...\n'));
|
|
273
|
+
showPrompt();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (replMode === 'pick_gen_model') {
|
|
277
|
+
replMode = 'chat';
|
|
278
|
+
rl.setPrompt(PROMPT);
|
|
279
|
+
const picked = resolveImageModelPick(input);
|
|
280
|
+
if (picked) {
|
|
281
|
+
session.generationModel = picked;
|
|
282
|
+
setImageModel(config, picked);
|
|
283
|
+
console.log(chalk.green(`✅ 生成模型: ${formatImageModelLabel(picked)}\n`));
|
|
284
|
+
persist();
|
|
285
|
+
}
|
|
286
|
+
else
|
|
287
|
+
console.log(chalk.gray('已取消。\n'));
|
|
288
|
+
printDraftBar(session);
|
|
289
|
+
showPrompt();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (replMode === 'pick_assist_model') {
|
|
293
|
+
replMode = 'chat';
|
|
294
|
+
rl.setPrompt(PROMPT);
|
|
295
|
+
const picked = resolveModelPickInput(input);
|
|
296
|
+
if (picked) {
|
|
297
|
+
session.assistModel = picked;
|
|
298
|
+
setImageAssistModel(config, picked);
|
|
299
|
+
console.log(chalk.green(`✅ 助手模型: ${formatModelLabel(picked)}\n`));
|
|
300
|
+
persist();
|
|
301
|
+
}
|
|
302
|
+
else
|
|
303
|
+
console.log(chalk.gray('已取消。\n'));
|
|
304
|
+
showPrompt();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (replMode === 'chat' && input.endsWith('\\') && input.length > 1) {
|
|
308
|
+
multilineBuffer = [input.slice(0, -1)];
|
|
309
|
+
replMode = 'multiline';
|
|
310
|
+
rl.setPrompt(PROMPT_MULTILINE);
|
|
311
|
+
console.log(chalk.gray('多行模式:空行发送\n'));
|
|
312
|
+
showPrompt();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (replMode === 'multiline') {
|
|
316
|
+
if (!input) {
|
|
317
|
+
const text = multilineBuffer.join('\n').trim();
|
|
318
|
+
resetInputMode();
|
|
319
|
+
if (!text) {
|
|
320
|
+
showPrompt();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
isProcessing = true;
|
|
324
|
+
await runAssist(text);
|
|
325
|
+
showPrompt();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
multilineBuffer.push(input.endsWith('\\') ? input.slice(0, -1) : input);
|
|
329
|
+
showPrompt();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (!input) {
|
|
333
|
+
showPrompt();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
|
|
337
|
+
leaveRepl('exit');
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (input === '/help') {
|
|
341
|
+
printImageReplHelp();
|
|
342
|
+
showPrompt();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (input === '/new') {
|
|
346
|
+
persist();
|
|
347
|
+
sessionId = createImageSessionId();
|
|
348
|
+
setActiveImageSessionId(config, sessionId);
|
|
349
|
+
session = loadImageSession(config, sessionId);
|
|
350
|
+
session.generationModel = resolveImageModel(config);
|
|
351
|
+
session.assistModel = resolveImageAssistModel(config);
|
|
352
|
+
console.log(chalk.green(`✅ 新会话: ${sessionId}\n`));
|
|
353
|
+
printDraftBar(session);
|
|
354
|
+
showPrompt();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (input === '/draft') {
|
|
358
|
+
console.log(chalk.cyan('\n当前草稿:\n'));
|
|
359
|
+
console.log(chalk.white(session.draft.prompt || '(空)'));
|
|
360
|
+
if (session.draft.imageUrls.length) {
|
|
361
|
+
console.log(chalk.gray('\n参考图:'));
|
|
362
|
+
session.draft.imageUrls.forEach((u) => console.log(chalk.cyan(` ${u}`)));
|
|
363
|
+
}
|
|
364
|
+
console.log();
|
|
365
|
+
showPrompt();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (input.startsWith('/set ')) {
|
|
369
|
+
const text = input.slice(5).trim();
|
|
370
|
+
if (!text) {
|
|
371
|
+
console.log(chalk.yellow('用法: /set <提示词>\n'));
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
session.draft.prompt = text;
|
|
375
|
+
persist();
|
|
376
|
+
console.log(chalk.green('✅ 已直接写入草稿(未经助手)\n'));
|
|
377
|
+
printDraftBar(session);
|
|
378
|
+
}
|
|
379
|
+
showPrompt();
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (input === '/system') {
|
|
383
|
+
console.log(chalk.cyan('\n--- 助手系统提示词(动态,含当前草稿)---\n'));
|
|
384
|
+
console.log(chalk.gray(buildAssistSystemPrompt(session.generationModel, session.draft)));
|
|
385
|
+
console.log();
|
|
386
|
+
showPrompt();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (input === '/clear') {
|
|
390
|
+
session.draft = emptyDraft();
|
|
391
|
+
session.assistMessages = [];
|
|
392
|
+
persist();
|
|
393
|
+
console.log(chalk.green('✅ 已清空草稿与助手对话。\n'));
|
|
394
|
+
showPrompt();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (input === '/model' || input.startsWith('/model ')) {
|
|
398
|
+
const arg = input.slice(6).trim();
|
|
399
|
+
if (!arg) {
|
|
400
|
+
printImageModelPickerMenu(session.generationModel);
|
|
401
|
+
replMode = 'pick_gen_model';
|
|
402
|
+
rl.setPrompt(PROMPT_PICK);
|
|
403
|
+
showPrompt();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
session.generationModel = arg;
|
|
407
|
+
setImageModel(config, arg);
|
|
408
|
+
console.log(chalk.green(`✅ 生成模型: ${formatImageModelLabel(arg)}\n`));
|
|
409
|
+
persist();
|
|
410
|
+
printDraftBar(session);
|
|
411
|
+
showPrompt();
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (input === '/assist' || input.startsWith('/assist ')) {
|
|
415
|
+
const arg = input.slice(7).trim();
|
|
416
|
+
if (!arg) {
|
|
417
|
+
printModelPickerMenu(session.assistModel);
|
|
418
|
+
replMode = 'pick_assist_model';
|
|
419
|
+
rl.setPrompt(PROMPT_PICK);
|
|
420
|
+
showPrompt();
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
session.assistModel = arg;
|
|
424
|
+
setImageAssistModel(config, arg);
|
|
425
|
+
console.log(chalk.green(`✅ 助手模型: ${formatModelLabel(arg)}\n`));
|
|
426
|
+
persist();
|
|
427
|
+
showPrompt();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (input === '/tpl' || input.startsWith('/tpl ')) {
|
|
431
|
+
const rest = input.slice(4).trim();
|
|
432
|
+
if (!rest) {
|
|
433
|
+
printTemplateList();
|
|
434
|
+
showPrompt();
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const parts = rest.split(/\s+/);
|
|
438
|
+
const tplId = resolveTemplatePick(parts[0]) ?? parts[0];
|
|
439
|
+
const subject = parts.slice(1).join(' ');
|
|
440
|
+
try {
|
|
441
|
+
const { template, draft } = applyTemplate(tplId, subject);
|
|
442
|
+
session.draft.prompt = draft.prompt ?? session.draft.prompt;
|
|
443
|
+
if (draft.aspectRatio)
|
|
444
|
+
session.draft.aspectRatio = draft.aspectRatio;
|
|
445
|
+
if (draft.quality)
|
|
446
|
+
session.draft.quality = draft.quality;
|
|
447
|
+
if (template.suggestedModel) {
|
|
448
|
+
console.log(chalk.gray(`提示: 此模板建议使用 ${template.suggestedModel},可用 /model 切换`));
|
|
449
|
+
}
|
|
450
|
+
persist();
|
|
451
|
+
console.log(chalk.green(`✅ 已应用模板「${template.label}」\n`));
|
|
452
|
+
printDraftBar(session);
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
456
|
+
console.log(chalk.red(`❌ ${msg}\n`));
|
|
457
|
+
}
|
|
458
|
+
showPrompt();
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (input === '/optimize' || input.startsWith('/optimize ')) {
|
|
462
|
+
const hint = input.slice(9).trim();
|
|
463
|
+
isProcessing = true;
|
|
464
|
+
await runAssist(hint || '请让 prompt 更具体、更可生成', { optimizeOnly: true });
|
|
465
|
+
showPrompt();
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (input.startsWith('/image-url ')) {
|
|
469
|
+
const url = input.slice(10).trim();
|
|
470
|
+
if (url) {
|
|
471
|
+
session.draft.imageUrls.push(url);
|
|
472
|
+
persist();
|
|
473
|
+
console.log(chalk.green(`✅ 已添加参考图 (${session.draft.imageUrls.length})\n`));
|
|
474
|
+
}
|
|
475
|
+
showPrompt();
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (input.startsWith('/ratio ')) {
|
|
479
|
+
session.draft.aspectRatio = input.slice(6).trim();
|
|
480
|
+
persist();
|
|
481
|
+
console.log(chalk.green(`✅ aspect_ratio = ${session.draft.aspectRatio}\n`));
|
|
482
|
+
showPrompt();
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (input.startsWith('/quality ')) {
|
|
486
|
+
const q = input.slice(8).trim();
|
|
487
|
+
if (q === 'medium' || q === 'high') {
|
|
488
|
+
session.draft.quality = q;
|
|
489
|
+
persist();
|
|
490
|
+
console.log(chalk.green(`✅ quality = ${q}\n`));
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
console.log(chalk.red('❌ 仅支持 medium 或 high\n'));
|
|
494
|
+
}
|
|
495
|
+
showPrompt();
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (input === '/edit') {
|
|
499
|
+
const edited = readMultilineFromEditor(session.draft.prompt);
|
|
500
|
+
if (edited !== null) {
|
|
501
|
+
session.draft.prompt = edited;
|
|
502
|
+
persist();
|
|
503
|
+
console.log(chalk.green('✅ 草稿已更新\n'));
|
|
504
|
+
printDraftBar(session);
|
|
505
|
+
}
|
|
506
|
+
showPrompt();
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (input === '/gen') {
|
|
510
|
+
await runGenerate();
|
|
511
|
+
showPrompt();
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (input.startsWith('/')) {
|
|
515
|
+
console.log(chalk.yellow(`❌ 未知指令,输入 /help\n`));
|
|
516
|
+
showPrompt();
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
isProcessing = true;
|
|
520
|
+
await runAssist(input);
|
|
521
|
+
showPrompt();
|
|
522
|
+
});
|
|
523
|
+
showPrompt();
|
|
524
|
+
});
|
|
525
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const DEFAULT_IMAGE_SESSION_ID = 'image-default';
|
|
4
|
+
export function getImageSessionsDir(config) {
|
|
5
|
+
const dir = path.join(path.dirname(config.path), 'image-sessions');
|
|
6
|
+
if (!fs.existsSync(dir)) {
|
|
7
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
return dir;
|
|
10
|
+
}
|
|
11
|
+
function sessionPath(config, sessionId) {
|
|
12
|
+
const safeId = sessionId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
13
|
+
return path.join(getImageSessionsDir(config), `${safeId}.json`);
|
|
14
|
+
}
|
|
15
|
+
export function getActiveImageSessionId(config) {
|
|
16
|
+
return config.get('activeImageSessionId') || DEFAULT_IMAGE_SESSION_ID;
|
|
17
|
+
}
|
|
18
|
+
export function setActiveImageSessionId(config, sessionId) {
|
|
19
|
+
config.set('activeImageSessionId', sessionId);
|
|
20
|
+
}
|
|
21
|
+
export function createImageSessionId() {
|
|
22
|
+
return `image-${Date.now()}`;
|
|
23
|
+
}
|
|
24
|
+
export function emptyDraft() {
|
|
25
|
+
return { prompt: '', imageUrls: [] };
|
|
26
|
+
}
|
|
27
|
+
export function loadImageSession(config, sessionId) {
|
|
28
|
+
const filePath = sessionPath(config, sessionId);
|
|
29
|
+
if (!fs.existsSync(filePath)) {
|
|
30
|
+
return {
|
|
31
|
+
id: sessionId,
|
|
32
|
+
updatedAt: new Date().toISOString(),
|
|
33
|
+
generationModel: '',
|
|
34
|
+
assistModel: '',
|
|
35
|
+
draft: emptyDraft(),
|
|
36
|
+
assistMessages: [],
|
|
37
|
+
tasks: [],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
42
|
+
return {
|
|
43
|
+
id: raw.id || sessionId,
|
|
44
|
+
updatedAt: raw.updatedAt || '',
|
|
45
|
+
generationModel: raw.generationModel || '',
|
|
46
|
+
assistModel: raw.assistModel || '',
|
|
47
|
+
draft: {
|
|
48
|
+
prompt: raw.draft?.prompt ?? '',
|
|
49
|
+
imageUrls: Array.isArray(raw.draft?.imageUrls) ? raw.draft.imageUrls : [],
|
|
50
|
+
aspectRatio: raw.draft?.aspectRatio,
|
|
51
|
+
quality: raw.draft?.quality,
|
|
52
|
+
},
|
|
53
|
+
assistMessages: Array.isArray(raw.assistMessages) ? raw.assistMessages : [],
|
|
54
|
+
tasks: Array.isArray(raw.tasks) ? raw.tasks : [],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return {
|
|
59
|
+
id: sessionId,
|
|
60
|
+
updatedAt: new Date().toISOString(),
|
|
61
|
+
generationModel: '',
|
|
62
|
+
assistModel: '',
|
|
63
|
+
draft: emptyDraft(),
|
|
64
|
+
assistMessages: [],
|
|
65
|
+
tasks: [],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function saveImageSession(config, session) {
|
|
70
|
+
const data = {
|
|
71
|
+
...session,
|
|
72
|
+
updatedAt: new Date().toISOString(),
|
|
73
|
+
};
|
|
74
|
+
fs.writeFileSync(sessionPath(config, session.id), JSON.stringify(data, null, 2), 'utf-8');
|
|
75
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export const BUILTIN_IMAGE_TEMPLATES = [
|
|
3
|
+
{
|
|
4
|
+
id: 'portrait',
|
|
5
|
+
label: '人像摄影',
|
|
6
|
+
description: '写实棚拍人像,柔和布光',
|
|
7
|
+
aspectRatio: '2:3',
|
|
8
|
+
quality: 'high',
|
|
9
|
+
prompt: 'Professional studio portrait of {{subject}}, soft key light, shallow depth of field, 85mm lens, natural skin texture, neutral background, photorealistic, high detail',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: 'product',
|
|
13
|
+
label: '白底商品图',
|
|
14
|
+
description: '电商白底产品照',
|
|
15
|
+
aspectRatio: '1:1',
|
|
16
|
+
quality: 'medium',
|
|
17
|
+
prompt: 'Professional product photography of {{subject}} on pure white background, soft studio lighting, centered composition, sharp focus, commercial e-commerce style, no text, no watermark',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'cyberpunk',
|
|
21
|
+
label: '赛博朋克场景',
|
|
22
|
+
description: '霓虹夜景、电影感',
|
|
23
|
+
aspectRatio: '3:2',
|
|
24
|
+
prompt: 'Cinematic cyberpunk scene of {{subject}}, neon lights, rain-soaked streets, volumetric fog, high contrast, detailed environment, 35mm film look, moody atmosphere',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'logo',
|
|
28
|
+
label: '简约 Logo',
|
|
29
|
+
description: '扁平矢量风格标志',
|
|
30
|
+
aspectRatio: '1:1',
|
|
31
|
+
prompt: 'Minimal flat vector logo design for {{subject}}, simple geometric shapes, limited color palette, clean lines, white background, modern brand identity, no photorealism',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'i2i-edit',
|
|
35
|
+
label: '图生图编辑',
|
|
36
|
+
description: '在参考图基础上按描述修改',
|
|
37
|
+
aspectRatio: 'auto',
|
|
38
|
+
suggestedModel: 'gpt-image-2-image-to-image',
|
|
39
|
+
prompt: 'Edit the input image: {{subject}}. Preserve the main subject likeness and composition where appropriate. Match lighting and color temperature to the original. Photorealistic integration, no pasted-on look.',
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
export function findTemplate(id) {
|
|
43
|
+
const key = id.trim().toLowerCase();
|
|
44
|
+
return BUILTIN_IMAGE_TEMPLATES.find((t) => t.id === key || t.id.toLowerCase() === key);
|
|
45
|
+
}
|
|
46
|
+
export function applyTemplate(templateId, subject) {
|
|
47
|
+
const template = findTemplate(templateId);
|
|
48
|
+
if (!template) {
|
|
49
|
+
throw new Error(`未知模板: ${templateId},使用 /tpl 查看列表`);
|
|
50
|
+
}
|
|
51
|
+
const sub = subject.trim() || 'the subject';
|
|
52
|
+
const prompt = template.prompt.replace(/\{\{subject\}\}/g, sub);
|
|
53
|
+
const draft = {
|
|
54
|
+
prompt,
|
|
55
|
+
aspectRatio: template.aspectRatio,
|
|
56
|
+
quality: template.quality,
|
|
57
|
+
};
|
|
58
|
+
return { template, draft };
|
|
59
|
+
}
|
|
60
|
+
export function printTemplateList() {
|
|
61
|
+
console.log(chalk.cyan('\n内置提示词模板:\n'));
|
|
62
|
+
BUILTIN_IMAGE_TEMPLATES.forEach((t, i) => {
|
|
63
|
+
console.log(` ${chalk.cyan(String(i + 1).padStart(2))}. ${chalk.bold(t.id.padEnd(12))} ${t.label} — ${chalk.dim(t.description)}`);
|
|
64
|
+
});
|
|
65
|
+
console.log(chalk.gray('\n用法: /tpl <id> [主体描述] 例: /tpl product 无线蓝牙耳机\n'));
|
|
66
|
+
}
|
|
67
|
+
export function resolveTemplatePick(input) {
|
|
68
|
+
const trimmed = input.trim();
|
|
69
|
+
if (!trimmed || trimmed === '0')
|
|
70
|
+
return null;
|
|
71
|
+
const n = parseInt(trimmed, 10);
|
|
72
|
+
if (!Number.isNaN(n) && n >= 1 && n <= BUILTIN_IMAGE_TEMPLATES.length) {
|
|
73
|
+
return BUILTIN_IMAGE_TEMPLATES[n - 1].id;
|
|
74
|
+
}
|
|
75
|
+
return trimmed;
|
|
76
|
+
}
|