museav-cli 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/.github/workflows/ci.yml +19 -0
  2. package/.github/workflows/publish.yml +79 -0
  3. package/AGENTS.md +90 -0
  4. package/CHANGELOG.md +49 -0
  5. package/LICENSE +21 -0
  6. package/README.md +490 -0
  7. package/SECURITY.md +11 -0
  8. package/dist/client.d.ts +387 -0
  9. package/dist/client.js +372 -0
  10. package/dist/commands/assets.d.ts +14 -0
  11. package/dist/commands/assets.js +30 -0
  12. package/dist/commands/balance.d.ts +3 -0
  13. package/dist/commands/balance.js +11 -0
  14. package/dist/commands/bind-feishu.d.ts +3 -0
  15. package/dist/commands/bind-feishu.js +63 -0
  16. package/dist/commands/gen.d.ts +16 -0
  17. package/dist/commands/gen.js +88 -0
  18. package/dist/commands/image-to-template.d.ts +24 -0
  19. package/dist/commands/image-to-template.js +111 -0
  20. package/dist/commands/jobs.d.ts +11 -0
  21. package/dist/commands/jobs.js +27 -0
  22. package/dist/commands/login.d.ts +3 -0
  23. package/dist/commands/login.js +67 -0
  24. package/dist/commands/models.d.ts +3 -0
  25. package/dist/commands/models.js +9 -0
  26. package/dist/commands/products.d.ts +9 -0
  27. package/dist/commands/products.js +16 -0
  28. package/dist/commands/reverse.d.ts +3 -0
  29. package/dist/commands/reverse.js +16 -0
  30. package/dist/commands/skills.d.ts +5 -0
  31. package/dist/commands/skills.js +22 -0
  32. package/dist/commands/templates.d.ts +28 -0
  33. package/dist/commands/templates.js +79 -0
  34. package/dist/commands/upload.d.ts +9 -0
  35. package/dist/commands/upload.js +8 -0
  36. package/dist/commands/video-templates.d.ts +21 -0
  37. package/dist/commands/video-templates.js +71 -0
  38. package/dist/commands/welcome.d.ts +6 -0
  39. package/dist/commands/welcome.js +93 -0
  40. package/dist/commands/whoami.d.ts +3 -0
  41. package/dist/commands/whoami.js +14 -0
  42. package/dist/config.d.ts +31 -0
  43. package/dist/config.js +93 -0
  44. package/dist/index.d.ts +2 -0
  45. package/dist/index.js +292 -0
  46. package/dist/tenant-client.d.ts +31 -0
  47. package/dist/tenant-client.js +84 -0
  48. package/package.json +55 -0
  49. package/src/client.ts +636 -0
  50. package/src/commands/assets.ts +47 -0
  51. package/src/commands/balance.ts +12 -0
  52. package/src/commands/bind-feishu.ts +77 -0
  53. package/src/commands/gen.ts +111 -0
  54. package/src/commands/image-to-template.ts +150 -0
  55. package/src/commands/jobs.ts +37 -0
  56. package/src/commands/login.ts +80 -0
  57. package/src/commands/models.ts +12 -0
  58. package/src/commands/products.ts +38 -0
  59. package/src/commands/reverse.ts +21 -0
  60. package/src/commands/skills.ts +29 -0
  61. package/src/commands/templates.ts +98 -0
  62. package/src/commands/upload.ts +18 -0
  63. package/src/commands/video-templates.ts +89 -0
  64. package/src/commands/welcome.ts +108 -0
  65. package/src/commands/whoami.ts +19 -0
  66. package/src/config.ts +114 -0
  67. package/src/index.ts +318 -0
  68. package/src/tenant-client.ts +90 -0
  69. package/src/types/update-notifier.d.ts +31 -0
  70. package/tsconfig.json +19 -0
@@ -0,0 +1,111 @@
1
+ import { isAsyncJob } from '../client.js';
2
+ const STEP_MARK = { running: '🔄', ok: '✅', fail: '❌' };
3
+ export async function imageToTemplate(client, input, opts = {}) {
4
+ const isUrl = /^https?:\/\//.test(input);
5
+ const createTemplate = opts.create !== false;
6
+ // --variables 收窄白名单:逗号分隔。中台只认它那套通用语义 key(title/subject/location…),
7
+ // 传了白名单外的会被忽略并在 template_warnings 里说明,全都不合法才 400。
8
+ const variables = opts.variables
9
+ ? opts.variables.split(',').map((s) => s.trim()).filter(Boolean)
10
+ : undefined;
11
+ if (opts.variables && !variables?.length) {
12
+ throw new Error('--variables 给了但解析不出任何变量名,格式是逗号分隔,如 --variables title,subject,location');
13
+ }
14
+ let variableLabels;
15
+ if (opts.labels) {
16
+ try {
17
+ variableLabels = JSON.parse(opts.labels);
18
+ }
19
+ catch {
20
+ throw new Error(`--labels 必须是合法 JSON 对象,如 '{"subject":"艺人","location":"城市"}',收到: ${opts.labels}`);
21
+ }
22
+ }
23
+ // 模板元信息全是可选的,一个都没给就整个不传,让中台自己生成 slug / 中文名
24
+ const template = opts.name || opts.slug || opts.category
25
+ ? { zh_name: opts.name, slug: opts.slug, category: opts.category }
26
+ : undefined;
27
+ if (template && !createTemplate) {
28
+ process.stderr.write('⚠ --no-create 只看草稿,--name / --slug / --category 不会生效\n');
29
+ }
30
+ process.stderr.write(`${createTemplate ? '图生模板' : '图生模板草稿(--no-create,不建模板)'}: ${input}\n`);
31
+ const submitted = await client.imageToTemplate({
32
+ file: isUrl ? undefined : input,
33
+ imageUrl: isUrl ? input : undefined,
34
+ variables,
35
+ variableLabels,
36
+ createTemplate,
37
+ template,
38
+ async: opts.async,
39
+ });
40
+ let result;
41
+ if (isAsyncJob(submitted)) {
42
+ process.stderr.write(`任务已提交: ${submitted.jobId}\n`);
43
+ // 阶段名去重打印:中台每轮返回的是完整 steps 数组,不过滤的话每 3 秒把同样的阶段重刷一遍
44
+ const printed = new Set();
45
+ result = await client.waitImageToTemplate(submitted.jobId, (steps) => {
46
+ for (const s of steps) {
47
+ const key = `${s.name}:${s.status}`;
48
+ if (printed.has(key))
49
+ continue;
50
+ printed.add(key);
51
+ const cost = s.ms != null ? ` (${(s.ms / 1000).toFixed(1)}s)` : '';
52
+ process.stderr.write(` ${STEP_MARK[s.status] || '·'} ${s.name}${cost}\n`);
53
+ }
54
+ });
55
+ }
56
+ else {
57
+ result = submitted;
58
+ }
59
+ printResult(result, createTemplate);
60
+ }
61
+ function printResult(r, createTemplate) {
62
+ process.stderr.write(`\n✅ 读图完成\n`);
63
+ process.stderr.write(`风格: ${r.zh_name || '-'} 分类: ${r.image_category || '-'} 比例: ${r.aspect_ratio}\n`);
64
+ if (r.style_tags?.length)
65
+ process.stderr.write(`标签: ${r.style_tags.join(', ')}\n`);
66
+ const layers = r.text_layers || [];
67
+ if (layers.length) {
68
+ process.stderr.write(`\n文字层(${layers.length} 处):\n`);
69
+ for (const l of layers) {
70
+ const varTag = l.is_variable ? `{${l.variable}}` : '固定';
71
+ process.stderr.write(` ${(l.role || '-').padEnd(12)} ${varTag.padEnd(14)} ${(l.position || '').padEnd(12)} ${String(l.content || '').slice(0, 30)}\n`);
72
+ }
73
+ }
74
+ if (r.prompt_template) {
75
+ process.stderr.write(`\n提示词模具:\n ${r.prompt_template}\n`);
76
+ process.stderr.write(`变量: ${fieldSummary(r.fields)}\n`);
77
+ }
78
+ else {
79
+ // 中台的降级口径:文字层/变量化/建模板任一步失败都只降级,读图结果照常返回。
80
+ // 所以这里不抛错——上面那段读图结果是真的,用户可以照常拿去出图。
81
+ process.stderr.write(`\n⚠ 没能做出提示词模具${r.template_error ? `:${r.template_error}` : ''}\n`);
82
+ process.stderr.write(` 读图结果仍然有效,可直接用下面的 prompt 出图\n`);
83
+ }
84
+ for (const w of r.template_warnings || [])
85
+ process.stderr.write(`⚠ ${w}\n`);
86
+ if (r.template) {
87
+ process.stderr.write(`\n✅ 模板已建:${r.template.id}\n`);
88
+ process.stderr.write(` slug: ${r.template.slug} 名称: ${r.template.zh_name}\n`);
89
+ process.stderr.write(` 归属:${r.template.tenant_id ? '当前租户(其他租户看不到)' : '平台共享(所有租户可见)'}\n`);
90
+ if (r.template.reference_image)
91
+ process.stderr.write(` 参考图(已焊进模板): ${r.template.reference_image}\n`);
92
+ const example = r.fields?.length ? ` --fields '{"${r.fields[0].key}":"..."}'` : '';
93
+ process.stderr.write(`\n出图: museav gen --template ${r.template.id}${example}\n`);
94
+ // stdout 只出模板 id,跟 templates create 一致,便于脚本链式使用
95
+ console.log(r.template.id);
96
+ return;
97
+ }
98
+ if (createTemplate && r.template_error) {
99
+ process.stderr.write(`\n❌ 模板没建成:${r.template_error}\n`);
100
+ }
101
+ else if (!createTemplate) {
102
+ process.stderr.write(`\n这是草稿(--no-create)。满意的话去掉 --no-create 重跑一次即可建成模板。\n`);
103
+ }
104
+ // 没建成模板时 stdout 给完整草稿 JSON——脚本还能拿它自己调 POST /api/templates
105
+ console.log(JSON.stringify(r));
106
+ }
107
+ function fieldSummary(fields) {
108
+ if (!fields?.length)
109
+ return '(无)';
110
+ return fields.map((f) => (f.label && f.label !== f.key ? `${f.key}(${f.label})` : f.key)).join(', ');
111
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * museav jobs —— 查自己名下的出图工作流
3
+ *
4
+ * 范围自动跟着鉴权凭证走:个人 login 只看自己的记录;租户 apiKey 看自己业务下的全部记录。
5
+ * 不需要额外传租户/用户 id——你拿的是谁的凭证,就是谁的数据。
6
+ */
7
+ import type { StudioClient, Job } from '../client.js';
8
+ export declare function jobs(client: StudioClient, opts: {
9
+ limit?: string;
10
+ status?: Job['status'];
11
+ }): Promise<void>;
@@ -0,0 +1,27 @@
1
+ const STATUS_MARK = {
2
+ pending: '⏳',
3
+ processing: '🔄',
4
+ done: '✅',
5
+ failed: '❌',
6
+ };
7
+ export async function jobs(client, opts) {
8
+ const limit = opts.limit ? Number(opts.limit) : 20;
9
+ const list = await client.listJobs({ limit, status: opts.status });
10
+ if (!list.length) {
11
+ process.stderr.write('没有找到工作流记录\n');
12
+ console.log('[]');
13
+ return;
14
+ }
15
+ process.stderr.write(`最近 ${list.length} 条工作流:\n\n`);
16
+ for (const j of list) {
17
+ const mark = STATUS_MARK[j.status] || '?';
18
+ const time = j.created_at ? new Date(j.created_at).toLocaleString() : '?';
19
+ process.stderr.write(`${mark} ${j.id} ${j.status.padEnd(10)} ${(j.model || '?').padEnd(16)} ${time}\n`);
20
+ if (j.cdn_url)
21
+ process.stderr.write(` → ${j.cdn_url}\n`);
22
+ if (j.status === 'failed' && j.error)
23
+ process.stderr.write(` error: ${j.error}\n`);
24
+ }
25
+ // stdout 输出完整 JSON,供脚本消费
26
+ console.log(JSON.stringify(list));
27
+ }
@@ -0,0 +1,3 @@
1
+ export declare function login(opts: {
2
+ baseUrl?: string;
3
+ }): Promise<void>;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * museav login —— 设备授权登录(RFC 8628)
3
+ *
4
+ * 流程:CLI 调 /cli-auth/start 拿验证码 → 用户浏览器打开审批页授权
5
+ * → CLI 轮询 /cli-auth/poll 拿 JWT → 存到 ~/.museav.json
6
+ *
7
+ * 之后所有命令用 Bearer JWT 调 API,权限/额度与网页端一致。
8
+ */
9
+ import { saveConfig, DEFAULT_BASE_URL } from '../config.js';
10
+ import { printWelcome } from './welcome.js';
11
+ export async function login(opts) {
12
+ const baseUrl = (opts.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
13
+ // 1. 发起设备授权
14
+ process.stderr.write('正在发起授权...\n');
15
+ const startResp = await fetch(`${baseUrl}/api/cli-auth/start`, { method: 'POST' });
16
+ if (!startResp.ok) {
17
+ throw new Error(`发起授权失败: HTTP ${startResp.status}`);
18
+ }
19
+ const start = await startResp.json();
20
+ // 2. 展示验证码和链接,等用户去浏览器授权
21
+ process.stderr.write('\n');
22
+ process.stderr.write('┌──────────────────────────────────────────────────┐\n');
23
+ process.stderr.write('│ │\n');
24
+ process.stderr.write(`│ 验证码: ${start.user_code.padEnd(34)} │\n`);
25
+ process.stderr.write('│ │\n');
26
+ process.stderr.write('│ 请在浏览器打开以下地址,登录并批准授权: │\n');
27
+ process.stderr.write('│ │\n');
28
+ process.stderr.write('│ ' + start.verification_uri + '\n');
29
+ process.stderr.write('│ │\n');
30
+ process.stderr.write('└──────────────────────────────────────────────────┘\n');
31
+ process.stderr.write('\n等待授权完成...(可随时 Ctrl+C 取消)\n');
32
+ // 3. 轮询直到拿到 token
33
+ const interval = (start.interval || 3) * 1000;
34
+ const maxAttempts = Math.floor((start.expires_in || 600) * 1000 / interval);
35
+ for (let i = 0; i < maxAttempts; i++) {
36
+ await sleep(interval);
37
+ const pollResp = await fetch(`${baseUrl}/api/cli-auth/poll`, {
38
+ method: 'POST',
39
+ headers: { 'Content-Type': 'application/json' },
40
+ body: JSON.stringify({ device_code: start.device_code }),
41
+ });
42
+ if (!pollResp.ok)
43
+ continue;
44
+ const poll = await pollResp.json();
45
+ if (poll.status === 'approved' && poll.token) {
46
+ saveConfig({ baseUrl, token: poll.token });
47
+ process.stderr.write('\n✅ 登录成功!token 已保存到 ~/.museav.json\n');
48
+ await printWelcome(baseUrl, { token: poll.token });
49
+ return;
50
+ }
51
+ if (poll.status === 'expired') {
52
+ throw new Error('授权已过期,请重新运行 museav login');
53
+ }
54
+ // 服务端目前只有 pending → approved 或过期两条路,没有"拒绝"这个动作
55
+ // (浏览器审批页没有拒绝按钮,不批准就是不动,最终走 expired)。
56
+ // 这里仍保留未知状态的兜底,万一以后服务端加了新状态,报错文案不会文不对题。
57
+ if (poll.status !== 'pending') {
58
+ throw new Error(`授权失败: ${poll.error || poll.status || '未知错误'}`);
59
+ }
60
+ // pending:继续等
61
+ process.stderr.write('.');
62
+ }
63
+ throw new Error('等待授权超时,请重新运行 museav login');
64
+ }
65
+ function sleep(ms) {
66
+ return new Promise((r) => setTimeout(r, ms));
67
+ }
@@ -0,0 +1,3 @@
1
+ /** museav models —— 查可用模型列表 */
2
+ import type { StudioClient } from '../client.js';
3
+ export declare function models(client: StudioClient): Promise<void>;
@@ -0,0 +1,9 @@
1
+ export async function models(client) {
2
+ const list = await client.models();
3
+ process.stderr.write(`可用模型(${list.length} 个):\n`);
4
+ for (const m of list) {
5
+ process.stderr.write(` ${m.value.padEnd(28)} ${m.label}\n`);
6
+ }
7
+ // stdout 输出 value 列表(便于脚本解析)
8
+ console.log(list.map((m) => m.value).join('\n'));
9
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * museav products —— 查所属租户自己的产品目录。
3
+ *
4
+ * 数据不在 Studio 中台,在租户自己的后台(见 ../tenant-client.ts 顶部注释)。
5
+ * 不是每个租户都有"产品"这个概念——比如好易美是演唱会海报/票务业务,没有实体商品,
6
+ * 它的后台没有开通这个端点,调用会报错,这是预期行为,不是 bug。
7
+ */
8
+ import type { TenantClient } from '../tenant-client.js';
9
+ export declare function products(client: TenantClient): Promise<void>;
@@ -0,0 +1,16 @@
1
+ export async function products(client) {
2
+ const list = await client.get('tenant-products');
3
+ if (!Array.isArray(list) || !list.length) {
4
+ process.stderr.write('该租户暂无产品数据\n');
5
+ console.log('[]');
6
+ return;
7
+ }
8
+ process.stderr.write(`产品(${list.length} 个):\n`);
9
+ for (const p of list) {
10
+ const imgCount = (p.images?.length || 0) + (p.cover_image_url ? 1 : 0);
11
+ process.stderr.write(` ${String(p.id).padEnd(38)} ${(p.name || '').padEnd(20)} ${(p.series || '').padEnd(12)} ${(p.status || '').padEnd(10)} 图${imgCount}张\n`);
12
+ }
13
+ process.stderr.write(`\n配合出图: museav gen --template <模板id> --fields '{"...":"..."}'(参考图 URL 从上面产品的 cover_image_url / images 里挑)\n`);
14
+ // stdout 输出完整 JSON,方便 agent/脚本挑参考图字段
15
+ console.log(JSON.stringify(list));
16
+ }
@@ -0,0 +1,3 @@
1
+ /** museav reverse —— 图片逆向(SCULPT 六要素反推 prompt) */
2
+ import type { StudioClient } from '../client.js';
3
+ export declare function reverse(client: StudioClient, input: string): Promise<void>;
@@ -0,0 +1,16 @@
1
+ export async function reverse(client, input) {
2
+ // 输入是文件路径还是 URL
3
+ const isUrl = /^https?:\/\//.test(input);
4
+ const result = await client.reverse(isUrl ? { imageUrl: input } : { file: input });
5
+ process.stderr.write(`✅ 逆向完成\n\n`);
6
+ process.stderr.write(`风格: ${result.zh_name || '-'} 比例: ${result.aspect_ratio}\n`);
7
+ process.stderr.write(`标签: ${result.style_tags.join(', ')}\n\n`);
8
+ process.stderr.write(`SCULPT 六要素:\n`);
9
+ for (const [k, v] of Object.entries(result.sculpt)) {
10
+ process.stderr.write(` ${k}: ${v}\n`);
11
+ }
12
+ process.stderr.write(`\n英文 prompt:\n ${result.prompt}\n`);
13
+ process.stderr.write(`中文 prompt:\n ${result.prompt_cn}\n`);
14
+ // stdout 输出英文 prompt(便于管道直接喂给 gen)
15
+ console.log(result.prompt);
16
+ }
@@ -0,0 +1,5 @@
1
+ /** museav skills —— 查可用技能(私有 + 所属租户专属模板 + 公共库) */
2
+ import type { StudioClient } from '../client.js';
3
+ export declare function skills(client: StudioClient, opts?: {
4
+ genre?: string;
5
+ }): Promise<void>;
@@ -0,0 +1,22 @@
1
+ export async function skills(client, opts = {}) {
2
+ let list = await client.skills();
3
+ if (opts.genre) {
4
+ const kw = opts.genre.toLowerCase();
5
+ list = list.filter((s) => (s.genre || '').toLowerCase().includes(kw));
6
+ }
7
+ if (!list.length) {
8
+ process.stderr.write(opts.genre ? `没有匹配「${opts.genre}」的技能\n` : '没有可用技能\n');
9
+ return;
10
+ }
11
+ // 归属标记:自己建的 / 所属租户的专属模板 / 公共库——同名时服务端按这个顺序取
12
+ const tag = (s) => (s.private ? '[私有]' : s.agency ? '[专属]' : '');
13
+ process.stderr.write(`可用技能(${list.length} 个):\n`);
14
+ for (const s of list) {
15
+ const flag = s.ref_required ? '需垫图' : '';
16
+ process.stderr.write(` ${s.slug.padEnd(30)} ${(s.zh_name || '').padEnd(16)} ${(s.genre || '').padEnd(10)} ${(s.ratio || '').padEnd(6)} ${flag.padEnd(6)} ${tag(s)}\n`);
17
+ }
18
+ process.stderr.write(`\n出图: museav gen --skill <技能名> --input "一句业务描述"\n`);
19
+ process.stderr.write(`标「需垫图」的必须加 --ref ./图.jpg,否则服务端会拒绝\n`);
20
+ // stdout 只出 slug,便于脚本与 agent 解析
21
+ console.log(list.map((s) => s.slug).join('\n'));
22
+ }
@@ -0,0 +1,28 @@
1
+ /** museav templates —— 查可用图片/文字模板(自己租户建的 + 平台共享的)
2
+ * --type image|article 可过滤(中台 templates 表同时装两种,不传则都列并标注类型) */
3
+ import type { StudioClient } from '../client.js';
4
+ export declare function templates(client: StudioClient, opts?: {
5
+ category?: string;
6
+ type?: string;
7
+ }): Promise<void>;
8
+ interface CreateTemplateOpts {
9
+ name: string;
10
+ prompt: string;
11
+ category?: string;
12
+ ratio?: string;
13
+ description?: string;
14
+ model?: string;
15
+ quality?: string;
16
+ fields?: string;
17
+ type?: string;
18
+ }
19
+ /**
20
+ * museav templates create —— 新建图片模板。
21
+ *
22
+ * 归属不用自己传:服务端根据鉴权身份自动决定——租户 apiKey 建的自动归该租户
23
+ * (其他租户看不到),平台管理员 JWT 建的是 tenant_id=null 的平台共享模板,
24
+ * 个人账号(无租户、非管理员)会被服务端拒绝。CLI 这里不做额外判断,直接把
25
+ * 服务端返回的结果(含真实归属)打印出来。
26
+ */
27
+ export declare function createTemplate(client: StudioClient, opts: CreateTemplateOpts): Promise<void>;
28
+ export {};
@@ -0,0 +1,79 @@
1
+ export async function templates(client, opts = {}) {
2
+ let list = await client.templates((opts.type === 'image' || opts.type === 'article') ? opts.type : undefined);
3
+ if (opts.category) {
4
+ const kw = opts.category.toLowerCase();
5
+ list = list.filter((t) => (t.category || '').toLowerCase().includes(kw));
6
+ }
7
+ if (!list.length) {
8
+ process.stderr.write(opts.category ? `没有匹配「${opts.category}」的模板\n` : '没有可用模板\n');
9
+ return;
10
+ }
11
+ const tag = (t) => (t.tenant_id ? '' : '[平台]');
12
+ const typeTag = (t) => (t.template_type === 'article' ? '[文字]' : t.template_type === 'image' ? '[图片]' : '');
13
+ process.stderr.write(`可用模板(${list.length} 个):\n`);
14
+ for (const t of list) {
15
+ const cfg = t.generation_configs?.find((c) => c.is_default) || t.generation_configs?.[0];
16
+ const fields = cfg?.params_json?.fields || [];
17
+ const fieldHint = fields.length ? `字段:${fields.map((f) => f.key).join(',')}` : '';
18
+ process.stderr.write(` ${t.id.padEnd(38)} ${(t.zh_name || '').padEnd(16)} ${(t.category || '').padEnd(10)} ${(t.ratio || '').padEnd(6)} ${typeTag(t).padEnd(8)} ${fieldHint.padEnd(20)} ${tag(t)}\n`);
19
+ }
20
+ process.stderr.write(`\n出图: museav gen --template <模板id> [--fields '{"key":"值"}']\n`);
21
+ process.stderr.write(`按类型过滤: museav templates --type image|article\n`);
22
+ // stdout 只出 id,便于脚本与 agent 解析
23
+ console.log(list.map((t) => t.id).join('\n'));
24
+ }
25
+ /**
26
+ * museav templates create —— 新建图片模板。
27
+ *
28
+ * 归属不用自己传:服务端根据鉴权身份自动决定——租户 apiKey 建的自动归该租户
29
+ * (其他租户看不到),平台管理员 JWT 建的是 tenant_id=null 的平台共享模板,
30
+ * 个人账号(无租户、非管理员)会被服务端拒绝。CLI 这里不做额外判断,直接把
31
+ * 服务端返回的结果(含真实归属)打印出来。
32
+ */
33
+ export async function createTemplate(client, opts) {
34
+ if (!opts.name?.trim())
35
+ throw new Error('--name 必填');
36
+ if (!opts.prompt?.trim())
37
+ throw new Error('--prompt 必填,占位符用 {key} 形式,如 "{artist} 在 {city} 的演唱会海报"');
38
+ let fields;
39
+ if (opts.fields) {
40
+ try {
41
+ fields = JSON.parse(opts.fields);
42
+ }
43
+ catch {
44
+ throw new Error('--fields 必须是合法 JSON 数组,如 \'[{"key":"artist","label":"艺人名"}]\'');
45
+ }
46
+ }
47
+ else {
48
+ // 不传 --fields 就自动从 --prompt 里的 {key} 占位符提取,label 先等于 key,
49
+ // 想要更友好的中文标签可以自己传 --fields 覆盖
50
+ const keys = Array.from(new Set(Array.from(opts.prompt.matchAll(/\{(\w+)\}/g), (m) => m[1])));
51
+ fields = keys.map((key) => ({ key, label: key }));
52
+ }
53
+ const type = opts.type === 'article' ? 'article' : 'image';
54
+ const row = await client.createTemplate({
55
+ zh_name: opts.name,
56
+ category: opts.category,
57
+ ratio: opts.ratio,
58
+ description: opts.description,
59
+ template_type: type,
60
+ generation_configs: [
61
+ {
62
+ model: opts.model || 'gpt-image-2',
63
+ prompt_template: opts.prompt,
64
+ quality: opts.quality,
65
+ // 契约要求 fields 在 config 顶层(服务端 validateConfig 读 cfg.fields)
66
+ fields: fields.length ? fields : undefined,
67
+ is_default: true,
68
+ },
69
+ ],
70
+ });
71
+ process.stderr.write(`✅ ${type === 'article' ? '文字' : '图片'}模板已建:${row.id}\n`);
72
+ process.stderr.write(`归属:${row.tenant_id ? '当前租户(其他租户看不到)' : '平台共享(所有租户可见)'}\n`);
73
+ if (fields.length)
74
+ process.stderr.write(`占位符字段: ${fields.map((f) => f.key).join(', ')}\n`);
75
+ const fieldExample = fields.length ? ` --fields '{"${fields[0].key}":"..."}'` : '';
76
+ process.stderr.write(`\n出图: museav gen --template ${row.id}${fieldExample}\n`);
77
+ // stdout 只出新建的模板 id,便于脚本链式使用
78
+ console.log(row.id);
79
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * museav upload —— 上传素材到中台图库,stdout 输出公网直链。
3
+ *
4
+ * 走 POST /api/upload-ref:图片 / 音频 / 视频都收,类型按**字节魔数**判定(中台不信
5
+ * 客户端声明的 MIME),分类型限大小——图片 8MB / 音频 20MB / 视频 50MB。
6
+ * 拿到的 URL 可以直接喂给 gen --ref / gen --video --image,也能给 reverse 当图片 URL。
7
+ */
8
+ import type { StudioClient } from '../client.js';
9
+ export declare function upload(client: StudioClient, filePath: string): Promise<void>;
@@ -0,0 +1,8 @@
1
+ const KIND_LABEL = { image: '图片', audio: '音频', video: '视频' };
2
+ export async function upload(client, filePath) {
3
+ process.stderr.write(`上传 ${filePath} ...\n`);
4
+ const { url, media_type, mime } = await client.uploadRef(filePath);
5
+ const kind = media_type ? `${KIND_LABEL[media_type] || media_type}${mime ? ` · ${mime}` : ''}` : '';
6
+ process.stderr.write(`✅ 上传成功${kind ? `(${kind})` : ''}\n`);
7
+ console.log(url);
8
+ }
@@ -0,0 +1,21 @@
1
+ /** museav video-templates —— 查可用视频模板(配合 gen --video --template) */
2
+ import type { StudioClient } from '../client.js';
3
+ export declare function videoTemplates(client: StudioClient, opts?: {
4
+ category?: string;
5
+ }): Promise<void>;
6
+ interface CreateVideoTemplateOpts {
7
+ name: string;
8
+ prompt: string;
9
+ category?: string;
10
+ description?: string;
11
+ model?: string;
12
+ duration?: string;
13
+ ratio?: string;
14
+ sampleVideo?: string;
15
+ sampleCover?: string;
16
+ }
17
+ /** museav video-templates create —— 新建视频模板。
18
+ * 视频模板字段跟图片不同:模型/时长/比例在 generation_configs 每项里(服务端契约)。
19
+ * 归属跟图片模板一样由服务端根据鉴权身份自动决定。 */
20
+ export declare function createVideoTemplate(client: StudioClient, opts: CreateVideoTemplateOpts): Promise<void>;
21
+ export {};
@@ -0,0 +1,71 @@
1
+ export async function videoTemplates(client, opts = {}) {
2
+ let list = await client.videoTemplates();
3
+ if (opts.category) {
4
+ const kw = opts.category.toLowerCase();
5
+ list = list.filter((t) => (t.category || '').toLowerCase().includes(kw));
6
+ }
7
+ if (!list.length) {
8
+ process.stderr.write(opts.category ? `没有匹配「${opts.category}」的视频模板\n` : '没有可用视频模板\n');
9
+ return;
10
+ }
11
+ const tag = (t) => (t.tenant_id ? '' : '[平台]');
12
+ process.stderr.write(`可用视频模板(${list.length} 个):\n`);
13
+ for (const t of list) {
14
+ const cfg = t.generation_configs?.find((c) => c.is_default) || t.generation_configs?.[0];
15
+ const modelHint = cfg?.model ? `模型:${cfg.model}` : '';
16
+ const ratioHint = t.ratio || '';
17
+ const fieldCount = cfg?.params_json?.fields?.length || 0;
18
+ const fieldHint = fieldCount ? `字段:${cfg.params_json.fields.map((f) => f.key).join(',')}` : '';
19
+ const sampleHint = t.sample_video_url ? '有参考视频' : '';
20
+ process.stderr.write(` ${t.id.padEnd(38)} ${(t.zh_name || '').padEnd(20)} ${(t.category || '').padEnd(10)} ${ratioHint.padEnd(6)} ${modelHint.padEnd(30)} ${fieldHint.padEnd(24)} ${sampleHint.padEnd(10)} ${tag(t)}\n`);
21
+ if (t.sample_video_url) {
22
+ process.stderr.write(` 参考视频: ${t.sample_video_url}\n`);
23
+ }
24
+ }
25
+ process.stderr.write(`\n出视频: museav gen --video --template <模板id> [--fields '{"key":"值"}']\n`);
26
+ // stdout 只出 id 和参考视频 URL(tab 分隔),便于脚本与 agent 解析
27
+ console.log(list.map((t) => t.sample_video_url ? `${t.id}\t${t.sample_video_url}` : t.id).join('\n'));
28
+ }
29
+ /** museav video-templates create —— 新建视频模板。
30
+ * 视频模板字段跟图片不同:模型/时长/比例在 generation_configs 每项里(服务端契约)。
31
+ * 归属跟图片模板一样由服务端根据鉴权身份自动决定。 */
32
+ export async function createVideoTemplate(client, opts) {
33
+ if (!opts.name?.trim())
34
+ throw new Error('--name 必填');
35
+ if (!opts.prompt?.trim())
36
+ throw new Error('--prompt 必填,占位符用 {key} 形式,如 "{product} 在 {scene} 中展示"');
37
+ // 占位符必须声明 fields(中台 validateConfig 硬校验:prompt 里有 {key} 但没 fields 会被拒)
38
+ const keys = Array.from(new Set(Array.from(opts.prompt.matchAll(/\{(\w+)\}/g), (m) => m[1])));
39
+ const fields = keys.map((key) => ({ key, label: key }));
40
+ const cfg = {
41
+ model: opts.model || 'seedance-2',
42
+ prompt_template: opts.prompt,
43
+ is_default: true,
44
+ };
45
+ if (fields.length)
46
+ cfg.fields = fields;
47
+ if (opts.duration) {
48
+ const d = Number(opts.duration);
49
+ if (!Number.isFinite(d) || d < 4 || d > 15)
50
+ throw new Error('--duration 必须是 4-15 之间的数字(秒)');
51
+ cfg.duration = d;
52
+ }
53
+ if (opts.ratio)
54
+ cfg.aspect_ratio = opts.ratio;
55
+ const row = await client.createVideoTemplate({
56
+ zh_name: opts.name,
57
+ category: opts.category,
58
+ description: opts.description,
59
+ sample_video_url: opts.sampleVideo || null,
60
+ sample_cover_image: opts.sampleCover || null,
61
+ generation_configs: [cfg],
62
+ });
63
+ process.stderr.write(`✅ 视频模板已建:${row.id}\n`);
64
+ process.stderr.write(`归属:${row.tenant_id ? '当前租户(其他租户看不到)' : '平台共享(所有租户可见)'}\n`);
65
+ process.stderr.write(`模型: ${cfg.model} 时长: ${cfg.duration || '模板默认'} 比例: ${cfg.aspect_ratio || '模板默认'}\n`);
66
+ if (fields.length)
67
+ process.stderr.write(`占位符字段: ${fields.map((f) => f.key).join(', ')}\n`);
68
+ process.stderr.write(`\n出视频: museav gen --video --template ${row.id}\n`);
69
+ // stdout 只出新建的模板 id,便于脚本链式使用
70
+ console.log(row.id);
71
+ }
@@ -0,0 +1,6 @@
1
+ export interface WelcomeCred {
2
+ token?: string;
3
+ apiKey?: string;
4
+ }
5
+ /** 打印身份欢迎。识别失败时给兜底问候,不影响 CLI 继续使用。 */
6
+ export declare function printWelcome(baseUrl: string, cred: WelcomeCred): Promise<void>;
@@ -0,0 +1,93 @@
1
+ import { StudioClient } from '../client.js';
2
+ /** 终端显示宽度:ASCII=1,中文等宽字符=2(终端等宽字体下中文占 2 格) */
3
+ function displayWidth(s) {
4
+ return [...s].reduce((w, ch) => w + (ch.charCodeAt(0) > 255 ? 2 : 1), 0);
5
+ }
6
+ function box(lines) {
7
+ const width = Math.max(...lines.map(displayWidth)) + 4;
8
+ const border = '┌' + '─'.repeat(width) + '┐';
9
+ const bottom = '└' + '─'.repeat(width) + '┘';
10
+ return [
11
+ border,
12
+ ...lines.map((l) => `│ ${l}${' '.repeat(width - displayWidth(l))} │`),
13
+ bottom,
14
+ ].join('\n');
15
+ }
16
+ /** 打印身份欢迎。识别失败时给兜底问候,不影响 CLI 继续使用。 */
17
+ export async function printWelcome(baseUrl, cred) {
18
+ let me = null;
19
+ try {
20
+ me = await new StudioClient({ baseUrl, ...cred }).me();
21
+ }
22
+ catch {
23
+ me = null;
24
+ }
25
+ if (me?.identity === 'tenant') {
26
+ // ── 系统接入方(apiKey 模式)──
27
+ // 「租户」是内部技术称谓,对客户不暴露:客户视角是「我们的系统接入了
28
+ // MUSE AV AI 创作平台」,我们是服务提供方,不是把对方当租客。
29
+ const t = me.tenant;
30
+ const tname = t?.nickname || t?.name || '贵方系统';
31
+ process.stderr.write(`👋 欢迎,${tname}!MUSE AV AI 创作平台已为你的系统接入创作能力\n`);
32
+ process.stderr.write(` 接入方:${tname}(系统级 API Key,为你的业务后台提供图片/视频创作)\n`);
33
+ }
34
+ else if (me) {
35
+ // ── 个人身份 ──
36
+ const name = me.nickname || me.email;
37
+ let identity = '平台用户';
38
+ let greeting = '欢迎使用 MUSE AV 创作中台';
39
+ if (me.role === 'superadmin') {
40
+ identity = '平台超级管理员';
41
+ greeting = '欢迎回来,山鬼映画!平台归你管,出了事找你本人 😄';
42
+ }
43
+ else if (me.role === 'admin') {
44
+ identity = '平台管理员';
45
+ greeting = `欢迎回来,${name}!`;
46
+ }
47
+ else if (me.brand) {
48
+ // 接入方业务系统的成员(如好易美员工账户):
49
+ // 身份表达 = 我是「XX」的人,MUSE AV 为「XX」提供创作能力。
50
+ // 不用「租户/租户成员」这类内部词,客户不该有被出租的感觉。
51
+ const biz = me.brand.name;
52
+ identity = `${biz}成员`;
53
+ greeting = `欢迎,${name}!MUSE AV 为「${biz}」提供 AI 创作能力,你的创作工作台已就绪`;
54
+ }
55
+ else {
56
+ greeting = `欢迎,${name}!`;
57
+ }
58
+ const quota = me.generation_remaining != null ? `${me.generation_remaining} 次` : '不限';
59
+ process.stderr.write(`👋 ${greeting}\n`);
60
+ process.stderr.write(` 账户:${me.email}(${identity})\n`);
61
+ if (me.gen_done != null)
62
+ process.stderr.write(` 已出图 ${me.gen_done} 张 · 剩余额度 ${quota}\n`);
63
+ // 已绑定飞书:让客户知道缪斯 agent 在飞书里能认出他(未绑定不提示,绑定说明区有引导)
64
+ if (me.feishu_open_id)
65
+ process.stderr.write(' 飞书:已绑定 ✓(缪斯 agent 在飞书群里能认出你)\n');
66
+ }
67
+ else {
68
+ process.stderr.write('👋 欢迎使用 MUSE AV 创作中台\n');
69
+ }
70
+ process.stderr.write('\n' + box([
71
+ '📖 怎么用(常用命令)',
72
+ ' museav gen --prompt "英文提示词" 自由出图',
73
+ ' museav gen --skill <技能> --input "描述" 技能出图',
74
+ ' museav gen --template <id> --fields \'{..}\' 模板出图',
75
+ ' museav image-to-template <图> 一张图做成可复用模板',
76
+ ' museav skills / templates / reverse 查技能/模板/读图',
77
+ ' museav jobs / whoami 记录 / 身份',
78
+ ' museav bind-feishu 绑定飞书',
79
+ ]) + '\n');
80
+ process.stderr.write('\n🔗 找我 / 支持我:\n');
81
+ process.stderr.write(' · GitHub 给项目点个 ⭐ → https://github.com/webkubor/museav-cli\n');
82
+ process.stderr.write(' · 小红书「山鬼映画」(东方电影美学)→ https://www.xiaohongshu.com/user/profile/5c3c1581000000000501835d\n');
83
+ process.stderr.write('\n🎁 我的其他作品(GitHub 上给它们点个 ⭐ 就是最大的支持):\n');
84
+ process.stderr.write(' · typora-Bloom-theme(Typora 写作主题,★89)→ github.com/webkubor/typora-Bloom-theme\n');
85
+ process.stderr.write(' · voice-editor(本地中文 TTS 工作台)→ github.com/webkubor/voice-editor\n');
86
+ process.stderr.write(' · kyvault(本地加密密钥管理 CLI)→ github.com/webkubor/kyvault\n');
87
+ process.stderr.write(' · wechat-chat-gen(高仿真微信聊天截图生成器)→ github.com/webkubor/wechat-chat-gen\n');
88
+ process.stderr.write(' · knowledge-pdf-kit(Markdown → PDF/长图)→ github.com/webkubor/knowledge-pdf-kit\n');
89
+ process.stderr.write('\nℹ️ 绑定说明:\n');
90
+ process.stderr.write(' · 出图 / 技能 / 模板 / 逆向等全部创作功能【不需要】绑定飞书\n');
91
+ process.stderr.write(' · 绑定飞书(bind-feishu)只影响:让 agent 在飞书里认出你的身份\n');
92
+ process.stderr.write('\n现在就可以开始:museav gen --prompt "一只猫"\n');
93
+ }