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,61 @@
1
+ import chalk from 'chalk';
2
+ import { CONFIG_KEY_IMAGE_MODEL, formatModelLabel, getChatModel, getImageModel, maskSecret, printChatModels, promptSelectChatModel, setChatModel, } from '../models.js';
3
+ import { getImageAssistModel, setImageAssistModel, } from '../image-assist.js';
4
+ import { getActiveSessionId, listSessions, printSessionList, } from '../session.js';
5
+ export async function configAction(options, config) {
6
+ if (options.setApiKey) {
7
+ config.set('apiKey', options.setApiKey);
8
+ console.log(chalk.green('✅ API Key 配置已保存。'));
9
+ }
10
+ if (options.setBaseUrl) {
11
+ config.set('baseUrl', options.setBaseUrl);
12
+ console.log(chalk.green('✅ Base URL 配置已保存。'));
13
+ }
14
+ if (options.setModel) {
15
+ setChatModel(config, options.setModel);
16
+ console.log(chalk.green(`✅ 默认对话模型已设为: ${options.setModel}`));
17
+ }
18
+ if (options.setImageModel) {
19
+ config.set(CONFIG_KEY_IMAGE_MODEL, options.setImageModel);
20
+ console.log(chalk.green(`✅ 默认图片模型已设为: ${options.setImageModel}`));
21
+ }
22
+ if (options.setImageAssistModel) {
23
+ setImageAssistModel(config, options.setImageAssistModel);
24
+ console.log(chalk.green(`✅ 图片助手模型已设为: ${formatModelLabel(options.setImageAssistModel)}`));
25
+ }
26
+ if (!options.setApiKey &&
27
+ !options.setBaseUrl &&
28
+ !options.setModel &&
29
+ !options.setImageModel &&
30
+ !options.setImageAssistModel) {
31
+ const apiKey = config.get('apiKey');
32
+ console.log(chalk.yellow('当前配置信息:'));
33
+ console.log(chalk.cyan(`API Key: ${apiKey ? maskSecret(apiKey) : '未设置'}`));
34
+ console.log(chalk.cyan(`Base URL: ${config.get('baseUrl') || 'https://api.kie.ai (默认)'}`));
35
+ console.log(chalk.cyan(`对话模型: ${formatModelLabel(getChatModel(config))}`));
36
+ console.log(chalk.cyan(`图片模型: ${getImageModel(config)}`));
37
+ console.log(chalk.cyan(`图片助手: ${formatModelLabel(getImageAssistModel(config))}`));
38
+ }
39
+ }
40
+ export async function sessionsAction(config) {
41
+ const activeId = getActiveSessionId(config);
42
+ printSessionList(listSessions(config), activeId);
43
+ console.log(chalk.gray(`切换会话: kie chat --resume <id> 或交互中 /sessions\n`));
44
+ }
45
+ export async function modelsAction(options, config) {
46
+ const current = getChatModel(config);
47
+ if (!process.stdin.isTTY || options.list) {
48
+ printChatModels(current);
49
+ return;
50
+ }
51
+ const picked = await promptSelectChatModel(current);
52
+ if (options.set) {
53
+ setChatModel(config, picked);
54
+ console.log(chalk.green(`\n✅ 默认模型已设为: ${formatModelLabel(picked)}`));
55
+ }
56
+ else {
57
+ console.log(chalk.cyan(`\n已选择: ${formatModelLabel(picked)}`));
58
+ console.log(chalk.gray(`保存为默认: kie config --set-model ${picked}`));
59
+ console.log(chalk.gray(`本次对话使用: kie chat --model ${picked}`));
60
+ }
61
+ }
@@ -0,0 +1,102 @@
1
+ import chalk from 'chalk';
2
+ import { buildImageTaskInput, printImageModels, promptSelectImageModel, resolveImageModel, setImageModel, } from '../image-models.js';
3
+ import { createJob } from '../job.js';
4
+ import { runImageRepl } from '../image-repl.js';
5
+ import { waitImageTask, } from '../image.js';
6
+ import { getImageModel } from '../models.js';
7
+ import { kieUploadFile } from '../api.js';
8
+ export async function imageModelsAction(options, config) {
9
+ const current = getImageModel(config);
10
+ if (!process.stdin.isTTY || options.list) {
11
+ printImageModels(current);
12
+ return;
13
+ }
14
+ const picked = await promptSelectImageModel(current);
15
+ if (options.set) {
16
+ setImageModel(config, picked);
17
+ console.log(chalk.green(`\n✅ 默认图片模型已设为: ${picked}`));
18
+ }
19
+ else {
20
+ console.log(chalk.cyan(`\n已选择: ${picked}`));
21
+ console.log(chalk.gray(`保存为默认: kie config --set-image-model ${picked}`));
22
+ console.log(chalk.gray(`本次生成: kie image -m ${picked} "提示词"`));
23
+ }
24
+ }
25
+ export async function imageAction(prompt, options, config) {
26
+ const apiKey = config.get('apiKey');
27
+ const baseUrl = config.get('baseUrl') || 'https://api.kie.ai';
28
+ if (!apiKey) {
29
+ console.log(chalk.red('❌ 缺少必要配置,请先运行以下命令配置 API Key:'));
30
+ console.log(chalk.yellow('kie config --set-api-key <KEY>'));
31
+ process.exit(1);
32
+ }
33
+ if (!prompt) {
34
+ await runImageRepl({ config, apiKey, baseUrl });
35
+ return;
36
+ }
37
+ const ora = (await import('ora')).default;
38
+ const model = resolveImageModel(config, options.model);
39
+ // Pre-process local reference images and upload if needed
40
+ let imageUrls = undefined;
41
+ if (options.imageUrl) {
42
+ imageUrls = [];
43
+ const fs = await import('fs');
44
+ const path = await import('path');
45
+ const rawUrls = Array.isArray(options.imageUrl) ? options.imageUrl : [options.imageUrl];
46
+ for (const item of rawUrls) {
47
+ if (item.startsWith('http://') || item.startsWith('https://')) {
48
+ imageUrls.push(item);
49
+ }
50
+ else {
51
+ const resolvedPath = path.resolve(item);
52
+ if (fs.existsSync(resolvedPath)) {
53
+ const uploadSpinner = ora(`正在上传本地参考图: ${item}...`).start();
54
+ try {
55
+ const trimmedBaseUrl = baseUrl.replace(/\/+$/, '');
56
+ const uploadUrl = `${trimmedBaseUrl}/api/file-stream-upload`;
57
+ const uploadRes = await kieUploadFile(uploadUrl, {
58
+ apiKey,
59
+ filePath: resolvedPath,
60
+ });
61
+ if (uploadRes.success) {
62
+ uploadSpinner.succeed(chalk.green(`本地参考图 ${item} 上传成功!`));
63
+ imageUrls.push(uploadRes.data.downloadUrl);
64
+ }
65
+ else {
66
+ uploadSpinner.fail(chalk.red(`本地参考图 ${item} 上传失败: ${uploadRes.msg}`));
67
+ throw new Error(`参考图上传失败: ${uploadRes.msg}`);
68
+ }
69
+ }
70
+ catch (uploadErr) {
71
+ uploadSpinner.fail(chalk.red(`本地参考图 ${item} 上传出错: ${uploadErr.message}`));
72
+ throw uploadErr;
73
+ }
74
+ }
75
+ else {
76
+ throw new Error(`图片路径既不是合法的 URL,也不是存在的本地文件: ${item}`);
77
+ }
78
+ }
79
+ }
80
+ }
81
+ const spinner = ora(`正在提交图片生成任务 (${model})...`).start();
82
+ try {
83
+ const input = buildImageTaskInput(model, {
84
+ prompt,
85
+ aspectRatio: options.ratio,
86
+ quality: options.quality,
87
+ imageUrls,
88
+ seed: options.seed ? parseInt(options.seed, 10) : undefined,
89
+ });
90
+ const taskId = await createJob({ apiKey, baseUrl, model, input });
91
+ spinner.succeed(chalk.green(`任务提交成功!Job ID: ${taskId}`));
92
+ if (options.wait !== false) {
93
+ await waitImageTask({ apiKey, baseUrl, taskId, download: options.download });
94
+ }
95
+ else {
96
+ console.log(chalk.gray(`查询进度: kie job ${taskId}`));
97
+ }
98
+ }
99
+ catch (err) {
100
+ spinner.fail(chalk.red(`任务提交失败: ${err.message}`));
101
+ }
102
+ }
@@ -0,0 +1,25 @@
1
+ import chalk from 'chalk';
2
+ import { queryImageTask, printJobStatus } from '../image.js';
3
+ export async function jobAction(jobId, options, config) {
4
+ const apiKey = config.get('apiKey');
5
+ const baseUrl = config.get('baseUrl') || 'https://api.kie.ai';
6
+ if (!apiKey) {
7
+ console.log(chalk.red('❌ 缺少必要配置,请运行 kie config --set-api-key 设置'));
8
+ process.exit(1);
9
+ }
10
+ const ora = (await import('ora')).default;
11
+ const spinner = ora(`正在查询任务 ${jobId}...`).start();
12
+ try {
13
+ const status = await queryImageTask(apiKey, baseUrl, jobId);
14
+ spinner.stop();
15
+ if (options.json) {
16
+ console.log(JSON.stringify(status, null, 2));
17
+ }
18
+ else {
19
+ printJobStatus(status, jobId);
20
+ }
21
+ }
22
+ catch (err) {
23
+ spinner.fail(chalk.red(`查询失败: ${err.message}`));
24
+ }
25
+ }
@@ -0,0 +1,41 @@
1
+ import chalk from 'chalk';
2
+ import { kieUploadFile } from '../api.js';
3
+ export async function uploadAction(filepath, options, config) {
4
+ const apiKey = config.get('apiKey');
5
+ const baseUrl = config.get('baseUrl') || 'https://api.kie.ai';
6
+ if (!apiKey) {
7
+ console.log(chalk.red('❌ 缺少必要配置,请运行 kie config --set-api-key 设置'));
8
+ process.exit(1);
9
+ }
10
+ const ora = (await import('ora')).default;
11
+ const spinner = ora(`正在上传文件: ${filepath}...`).start();
12
+ try {
13
+ const trimmedBaseUrl = baseUrl.replace(/\/+$/, '');
14
+ const uploadUrl = `${trimmedBaseUrl}/api/file-stream-upload`;
15
+ const response = await kieUploadFile(uploadUrl, {
16
+ apiKey,
17
+ filePath: filepath,
18
+ uploadPath: options.path,
19
+ });
20
+ if (response.success) {
21
+ spinner.succeed(chalk.green('🎉 文件上传成功!'));
22
+ console.log();
23
+ console.log(chalk.cyan('=================================================='));
24
+ console.log(` 📂 ${chalk.bold('Kie 临时文件上传服务')}`);
25
+ console.log(chalk.cyan('=================================================='));
26
+ console.log(` ${chalk.white.bold('文件名:')} ${chalk.gray(response.data.fileName)}`);
27
+ console.log(` ${chalk.white.bold('大小:')} ${chalk.gray((response.data.fileSize / 1024).toFixed(2))} KB`);
28
+ console.log(` ${chalk.white.bold('MIME类型:')} ${chalk.gray(response.data.mimeType)}`);
29
+ console.log(` ${chalk.white.bold('上传路径:')} ${chalk.gray(response.data.filePath)}`);
30
+ console.log(` ${chalk.white.bold('下载链接:')} ${chalk.yellow.bold(response.data.downloadUrl)}`);
31
+ console.log(chalk.cyan('=================================================='));
32
+ console.log();
33
+ }
34
+ else {
35
+ spinner.fail(chalk.red(`上传失败: ${response.msg || '未知服务器错误'}`));
36
+ }
37
+ }
38
+ catch (err) {
39
+ spinner.fail(chalk.red(`上传出错: ${err.message}`));
40
+ }
41
+ }
package/dist/editor.js ADDED
@@ -0,0 +1,38 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { spawnSync } from 'child_process';
5
+ /** 用系统编辑器打开临时文件,返回用户保存后的正文(trim 后) */
6
+ export function readMultilineFromEditor(initialContent = '') {
7
+ const tmpFile = path.join(os.tmpdir(), `kie-chat-${Date.now()}.md`);
8
+ fs.writeFileSync(tmpFile, initialContent, 'utf-8');
9
+ const editor = process.env.EDITOR || process.env.VISUAL;
10
+ let result;
11
+ if (editor) {
12
+ const parts = editor.split(/\s+/).filter(Boolean);
13
+ result = spawnSync(parts[0], [...parts.slice(1), tmpFile], { stdio: 'inherit' });
14
+ }
15
+ else if (process.platform === 'win32') {
16
+ result = spawnSync('cmd', ['/c', 'start', '/wait', 'notepad', tmpFile], {
17
+ stdio: 'inherit',
18
+ shell: true,
19
+ });
20
+ }
21
+ else {
22
+ result = spawnSync('nano', [tmpFile], { stdio: 'inherit' });
23
+ }
24
+ try {
25
+ if (result.error || (result.status !== null && result.status !== 0)) {
26
+ return null;
27
+ }
28
+ return fs.readFileSync(tmpFile, 'utf-8').trim();
29
+ }
30
+ finally {
31
+ try {
32
+ fs.unlinkSync(tmpFile);
33
+ }
34
+ catch {
35
+ /* ignore */
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,79 @@
1
+ import { findImageModel, formatImageModelLabel } from './image-models.js';
2
+ export const CONFIG_KEY_IMAGE_ASSIST_MODEL = 'imageAssistModel';
3
+ /** 默认用 chat 同款里偏快的模型,可在 REPL /config 中改为任意 chat 模型 */
4
+ export const DEFAULT_IMAGE_ASSIST_MODEL = 'claude-haiku-4-5';
5
+ export function getImageAssistModel(config) {
6
+ return config.get(CONFIG_KEY_IMAGE_ASSIST_MODEL) || DEFAULT_IMAGE_ASSIST_MODEL;
7
+ }
8
+ export function setImageAssistModel(config, modelId) {
9
+ config.set(CONFIG_KEY_IMAGE_ASSIST_MODEL, modelId.trim());
10
+ }
11
+ export function resolveImageAssistModel(config, override) {
12
+ return override?.trim() || getImageAssistModel(config);
13
+ }
14
+ export function buildAssistSystemPrompt(generationModelId, draft) {
15
+ const genInfo = findImageModel(generationModelId);
16
+ const kind = genInfo?.kind === 'image-to-image' ? '图生图(需 input_urls)' : '文生图';
17
+ const ratios = genInfo?.aspectRatios.join(', ') ?? 'auto';
18
+ const qualityNote = genInfo?.supportsQuality
19
+ ? '支持 quality: medium | high'
20
+ : '不支持 quality 参数';
21
+ return [
22
+ '你是 Kie CLI 的出图提示词助手,帮助用户把想法改写成「图像生成 API」可用的英文绘图 prompt。',
23
+ '你不能真正生成图片;用户满意后会执行 /gen 提交到出图模型。',
24
+ '',
25
+ '工作语境(重要):',
26
+ '- 用户多为插画、概念艺术、动漫/游戏立绘、电商图等合法创作场景',
27
+ '- 将角色理解为虚构作品中的成年或年龄未明示的 stylized 角色;用 "young adult woman/man"、"anime-style female character" 等表述,避免 implying minors',
28
+ '- 你的职责是改写构图、风格、光影、服装、镜头语言,而不是审查用户是否该出图',
29
+ '- 仅当请求明确涉及未成年人性化、真实人物非法内容时,才简短拒绝,并仍须输出 DRAFT: (keep)',
30
+ '',
31
+ `当前生成模型: ${generationModelId} (${formatImageModelLabel(generationModelId)})`,
32
+ `类型: ${kind};宽高比可选: ${ratios};${qualityNote}`,
33
+ '',
34
+ `当前草稿 prompt: ${draft.prompt || '(空)'}`,
35
+ `参考图数量: ${draft.imageUrls.length}`,
36
+ `aspect_ratio: ${draft.aspectRatio ?? '(未设)'}`,
37
+ `quality: ${draft.quality ?? '(未设)'}`,
38
+ '',
39
+ '回复规则(必须全部遵守):',
40
+ '1) 用中文简要说明(2-5 句)',
41
+ '2) 最后一行必须是: DRAFT: <内容> —— 缺少此行视为格式错误',
42
+ '3) 需要更新草稿时: DRAFT: 一行英文 prompt(仅 prompt,无引号、无 markdown)',
43
+ '4) 无需改草稿、仅回答问题、或拒绝请求时: DRAFT: (keep)',
44
+ '5) 即使用户描述含中文或敏感词,也应尽量给出合规的英文视觉描述,而非只拒绝不写 DRAFT',
45
+ ].join('\n');
46
+ }
47
+ /** 助手是否像在拒绝服务(未给 DRAFT 时用于提示用户) */
48
+ export function looksLikeAssistRefusal(text) {
49
+ return /无法帮助|不能帮助|无法协助|超出.*范围|I can'?t help|cannot help|I'?m unable/i.test(text);
50
+ }
51
+ /** 从助手回复中解析 DRAFT 行 */
52
+ export function parseDraftFromAssistReply(text) {
53
+ const lines = text.split('\n');
54
+ let draftLineIdx = -1;
55
+ for (let i = lines.length - 1; i >= 0; i--) {
56
+ const t = lines[i].trim();
57
+ if (t.toUpperCase().startsWith('DRAFT:')) {
58
+ draftLineIdx = i;
59
+ break;
60
+ }
61
+ }
62
+ if (draftLineIdx === -1) {
63
+ return { displayText: text.trim(), draftPrompt: null, keepDraft: false };
64
+ }
65
+ const draftRaw = lines[draftLineIdx].trim().replace(/^DRAFT:\s*/i, '').trim();
66
+ const displayLines = [...lines.slice(0, draftLineIdx), ...lines.slice(draftLineIdx + 1)];
67
+ const displayText = displayLines.join('\n').trim();
68
+ if (!draftRaw || draftRaw === '(keep)' || draftRaw.toLowerCase() === 'keep') {
69
+ return { displayText, draftPrompt: null, keepDraft: true };
70
+ }
71
+ return { displayText, draftPrompt: draftRaw, keepDraft: false };
72
+ }
73
+ export function ensureAssistSystemMessage(messages, systemContent) {
74
+ const first = messages[0];
75
+ if (first?.role === 'system') {
76
+ return [{ role: 'system', content: systemContent }, ...messages.slice(1)];
77
+ }
78
+ return [{ role: 'system', content: systemContent }, ...messages];
79
+ }