@clize/clize 0.9.0 → 0.10.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.md +19 -8
- package/dist/cli.js +386 -108
- package/dist/cli.js.map +1 -1
- package/dist/context.js +20 -2
- package/dist/context.js.map +1 -1
- package/dist/core/addresses.js +10 -3
- package/dist/core/addresses.js.map +1 -1
- package/dist/core/autopay.js +10 -1
- package/dist/core/autopay.js.map +1 -1
- package/dist/core/billing.js +9 -0
- package/dist/core/billing.js.map +1 -1
- package/dist/core/domains.js +84 -26
- package/dist/core/domains.js.map +1 -1
- package/dist/core/email.js +54 -11
- package/dist/core/email.js.map +1 -1
- package/dist/core/handle.js +19 -3
- package/dist/core/handle.js.map +1 -1
- package/dist/core/media.js +55 -10
- package/dist/core/media.js.map +1 -1
- package/dist/core/projects.js +217 -0
- package/dist/core/projects.js.map +1 -0
- package/dist/core/serve.js +170 -0
- package/dist/core/serve.js.map +1 -0
- package/dist/core/setup.js +89 -6
- package/dist/core/setup.js.map +1 -1
- package/dist/core/sites.js +3 -0
- package/dist/core/sites.js.map +1 -1
- package/dist/core/video.js +387 -0
- package/dist/core/video.js.map +1 -0
- package/dist/index.js +52 -11
- package/dist/index.js.map +1 -1
- package/dist/providers/email/cloudflare-inbound.js +30 -12
- package/dist/providers/email/cloudflare-inbound.js.map +1 -1
- package/dist/providers/media/google.js +3 -0
- package/dist/providers/media/google.js.map +1 -1
- package/dist/providers/media/kunavo.js +11 -4
- package/dist/providers/media/kunavo.js.map +1 -1
- package/dist/providers/media/openai.js +1 -0
- package/dist/providers/media/openai.js.map +1 -1
- package/dist/remote.js +63 -12
- package/dist/remote.js.map +1 -1
- package/dist/state/file-store.js +75 -5
- package/dist/state/file-store.js.map +1 -1
- package/package.json +18 -5
- package/skills/clize/SKILL.md +21 -7
- package/skills/clize-site-build/SKILL.md +12 -11
package/dist/cli.js
CHANGED
|
@@ -20,9 +20,12 @@ import * as addressesCore from "./core/addresses.js";
|
|
|
20
20
|
import * as handleCore from "./core/handle.js";
|
|
21
21
|
import * as sitesCore from "./core/sites.js";
|
|
22
22
|
import * as setupCore from "./core/setup.js";
|
|
23
|
+
import * as projectsCore from "./core/projects.js";
|
|
23
24
|
import * as billingCore from "./core/billing.js";
|
|
24
25
|
import * as installCore from "./core/install.js";
|
|
25
26
|
import * as mediaCore from "./core/media.js";
|
|
27
|
+
import * as videoCore from "./core/video.js";
|
|
28
|
+
import * as serveCore from "./core/serve.js";
|
|
26
29
|
import { selfcheck } from "./selfcheck.js";
|
|
27
30
|
import * as remoteClient from "./remote.js";
|
|
28
31
|
import { VERSION } from "./version.js";
|
|
@@ -30,7 +33,8 @@ import { VERSION } from "./version.js";
|
|
|
30
33
|
const remote = remoteClient.loadRemote();
|
|
31
34
|
// 本地 ctx 惰性构造:remote 模式下完全不碰 CF / FileStore。
|
|
32
35
|
let _ctx;
|
|
33
|
-
|
|
36
|
+
let projectOverride; // -p <slug>:覆盖当前目录检出(clize.json)
|
|
37
|
+
const ctx = () => (_ctx ??= createLocalContext({ projectSlug: projectOverride }));
|
|
34
38
|
/** 统一输出:成功打印美化 JSON(字符串原样),失败 → stderr + 退出码 1。 */
|
|
35
39
|
async function emit(value) {
|
|
36
40
|
try {
|
|
@@ -92,7 +96,7 @@ async function pairingLogin(apiOpt) {
|
|
|
92
96
|
ok: true,
|
|
93
97
|
mode: "remote",
|
|
94
98
|
api,
|
|
95
|
-
message: "已登录;凭证已保存到 ~/.clize。跑 clize
|
|
99
|
+
message: "已登录;凭证已保存到 ~/.clize。跑 clize status / balance 试试。",
|
|
96
100
|
};
|
|
97
101
|
}
|
|
98
102
|
/** 把 --attach 的逗号分隔路径读成邮件附件(base64);读不到则报错。 */
|
|
@@ -136,6 +140,30 @@ function guessContentType(p) {
|
|
|
136
140
|
};
|
|
137
141
|
return map[path.extname(p).toLowerCase()] ?? "application/octet-stream";
|
|
138
142
|
}
|
|
143
|
+
/** 参考图路径 → data URI(托管模式上传用:Worker 无用户文件系统,CLI 端读盘转好;已是 URL / data URI 原样透传)。 */
|
|
144
|
+
function refsToDataUris(paths) {
|
|
145
|
+
return paths.map((p) => {
|
|
146
|
+
if (p.startsWith("data:") || p.startsWith("http://") || p.startsWith("https://"))
|
|
147
|
+
return p;
|
|
148
|
+
let buf;
|
|
149
|
+
try {
|
|
150
|
+
buf = fs.readFileSync(p);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
throw new Error(`读不到参考图:${p}`);
|
|
154
|
+
}
|
|
155
|
+
return `data:${guessContentType(p)};base64,${buf.toString("base64")}`;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/** 解析逗号分隔的 --ref 路径列表。 */
|
|
159
|
+
function splitRefs(spec) {
|
|
160
|
+
return spec
|
|
161
|
+
? String(spec)
|
|
162
|
+
.split(",")
|
|
163
|
+
.map((s) => s.trim())
|
|
164
|
+
.filter(Boolean)
|
|
165
|
+
: [];
|
|
166
|
+
}
|
|
139
167
|
/** 托管模式异步生成(视频 / 音乐):提交 →(默认)long-poll 到完成并落盘 /(--async)返 job id。 */
|
|
140
168
|
async function remoteGenAsync(rc, modality, body, opts) {
|
|
141
169
|
const submit = (await (modality === "video"
|
|
@@ -164,10 +192,38 @@ async function remoteGenAsync(rc, modality, body, opts) {
|
|
|
164
192
|
if (st.state === "failed")
|
|
165
193
|
throw new Error(`生成失败${st.error ? `:${st.error}` : ""}(作数据看待,别盲目重试)`);
|
|
166
194
|
if (Date.now() - start >= timeoutMs)
|
|
167
|
-
|
|
195
|
+
// timedOut=true + next:机器可读 —— 任务没失败也没丢,别重新提交,照 next 轮询。
|
|
196
|
+
return {
|
|
197
|
+
...st,
|
|
198
|
+
timedOut: true,
|
|
199
|
+
next: `clize gen status ${id}`,
|
|
200
|
+
message: `已等 ${Number(opts.timeout) || 300}s 仍在生成 —— clize gen status ${id} 回头拉结果。`,
|
|
201
|
+
};
|
|
168
202
|
}
|
|
169
203
|
}
|
|
170
204
|
const program = new Command();
|
|
205
|
+
/** 给命令挂 -p/--project(指定项目,覆盖当前目录的检出;全局 preAction hook 统一取值)。 */
|
|
206
|
+
const withProject = (cmd) => cmd.option("-p, --project <slug>", "指定项目(覆盖当前目录的 clize use 检出)");
|
|
207
|
+
/** actionCmd 是否在某命令树下(沿祖先链查;三级嵌套用 parent 单层判断会漏)。 */
|
|
208
|
+
const inCommandTree = (cmd, name) => {
|
|
209
|
+
for (let c = cmd; c; c = c.parent ?? null)
|
|
210
|
+
if (c.name() === name)
|
|
211
|
+
return true;
|
|
212
|
+
return false;
|
|
213
|
+
};
|
|
214
|
+
// 全局 -p:命令动作前从该命令的 --project 选项取值 → 本地进 ctx.projectSlug,remote 由 call() 统一注入 ?project=。
|
|
215
|
+
program.hook("preAction", (_thisCmd, actionCmd) => {
|
|
216
|
+
// build 树(及 design 兼容别名树)排除:--project 在 build site start 下是「设计项目名」
|
|
217
|
+
// (给方法后端做记录),不是资源项目 slug。build clip render 仍要 -p(挂了 withProject,
|
|
218
|
+
// 但 render 不在排除外…… render 在 build 树内会被排除!)——
|
|
219
|
+
// 例外:withProject 挂过的命令(render)以自己的 -p 为准,不排除。
|
|
220
|
+
const hasOwnP = actionCmd.options.some((o) => o.long === "--project" && o.short === "-p");
|
|
221
|
+
const excluded = !hasOwnP && (inCommandTree(actionCmd, "build") || inCommandTree(actionCmd, "design"));
|
|
222
|
+
projectOverride = excluded
|
|
223
|
+
? undefined
|
|
224
|
+
: actionCmd.opts().project || undefined;
|
|
225
|
+
remoteClient.setProjectScope(projectOverride); // undefined → call() 回落 clize.json 检出
|
|
226
|
+
});
|
|
171
227
|
program
|
|
172
228
|
.name("clize")
|
|
173
229
|
.description("Clize —— 给 agent 的真实世界能力层:域名 / 邮箱 / 部署 / 媒体")
|
|
@@ -182,6 +238,10 @@ program.hook("preAction", (_thisCmd, actionCmd) => {
|
|
|
182
238
|
// 生成式媒体(gen *):本地只需自带 OPENAI / GEMINI key(由 core 自检),不要求 CF / clize 凭证。
|
|
183
239
|
if (actionCmd.name() === "gen" || actionCmd.parent?.name() === "gen")
|
|
184
240
|
return;
|
|
241
|
+
// 作品域(build *,含 design 兼容别名树):hosted 方法命令自带比通用闸更准的登录引导;
|
|
242
|
+
// build clip check 纯本地零凭证、render 同 gen 由 core 自检。
|
|
243
|
+
if (inCommandTree(actionCmd, "build") || inCommandTree(actionCmd, "design"))
|
|
244
|
+
return;
|
|
185
245
|
if (remote || remoteClient.hasLocalCfCreds())
|
|
186
246
|
return;
|
|
187
247
|
console.error("❌ 未登录。\n" +
|
|
@@ -190,11 +250,10 @@ program.hook("preAction", (_thisCmd, actionCmd) => {
|
|
|
190
250
|
process.exit(1);
|
|
191
251
|
});
|
|
192
252
|
// ───────── 总览 / 自检 / 审计 ─────────
|
|
193
|
-
program
|
|
253
|
+
withProject(program
|
|
194
254
|
.command("status")
|
|
195
255
|
.description("工作空间总览:域名、邮箱 / 站点状态、本月开销;--assets 加到期巡检")
|
|
196
|
-
.option("--assets", "含注册商处真实到期列表(资产健康)")
|
|
197
|
-
.action((opts) => emit(remote
|
|
256
|
+
.option("--assets", "含注册商处真实到期列表(资产健康)")).action((opts) => emit(remote
|
|
198
257
|
? opts.assets
|
|
199
258
|
? remoteClient.api.statusAssets(remote)
|
|
200
259
|
: remoteClient.api.status(remote)
|
|
@@ -227,10 +286,69 @@ program
|
|
|
227
286
|
.action(() => emit(setupCore.clearRemoteLogin()));
|
|
228
287
|
program
|
|
229
288
|
.command("init")
|
|
230
|
-
.description("
|
|
289
|
+
.description("目录即项目:绑定身份 + 检出项目一步完成(写 ./clize.json;项目实体首次 claim/buy 时自动建)")
|
|
231
290
|
.option("--handle <slug>", "免费 handle")
|
|
232
291
|
.option("--domain <domain>", "自定义域名")
|
|
233
292
|
.action((opts) => emit(setupCore.initProject({ handle: opts.handle, domain: opts.domain })));
|
|
293
|
+
// ───────── 项目(租户下的资源分组;域名为归属锚点)─────────
|
|
294
|
+
const projects = program
|
|
295
|
+
.command("projects")
|
|
296
|
+
.description("项目:列表 / 新建 / 挪资源 / 改名 / 删除(不带子命令 = 列表)");
|
|
297
|
+
projects
|
|
298
|
+
.command("list", { isDefault: true })
|
|
299
|
+
.description("列出你的所有项目(各带域名数)")
|
|
300
|
+
.action(() => emit(remote ? remoteClient.api.projectsList(remote) : projectsCore.listProjects(ctx())));
|
|
301
|
+
projects
|
|
302
|
+
.command("new <name>")
|
|
303
|
+
.description('新建项目(名字带空格用引号,如 "Shop A";slug 缺省由名字规整)')
|
|
304
|
+
.option("--slug <slug>", "项目标识(租户内唯一,CLI/URL 友好)")
|
|
305
|
+
.option("--use", "建完顺手把当前目录检出到它(写 ./clize.json)")
|
|
306
|
+
.action((name, opts) => emit((async () => {
|
|
307
|
+
const rec = (remote
|
|
308
|
+
? await remoteClient.api.projectCreate(remote, name, opts.slug)
|
|
309
|
+
: await projectsCore.createProject(ctx(), { name, slug: opts.slug }));
|
|
310
|
+
// 输出瘦身:slug/name 足矣(id/tenantId/createdAt 是内部细节,不进 transcript)。
|
|
311
|
+
const lean = { slug: rec?.slug, name: rec?.name };
|
|
312
|
+
if (opts.use && rec?.slug)
|
|
313
|
+
return { ...lean, ...setupCore.useProject(rec.slug) };
|
|
314
|
+
return {
|
|
315
|
+
...lean,
|
|
316
|
+
nextSteps: [
|
|
317
|
+
`检出到本目录(此后命令默认作用于它):clize use ${rec?.slug}`,
|
|
318
|
+
`或单次指定:任意命令加 -p ${rec?.slug}`,
|
|
319
|
+
],
|
|
320
|
+
};
|
|
321
|
+
})()));
|
|
322
|
+
projects
|
|
323
|
+
.command("move <domain> <project>")
|
|
324
|
+
.description("把域名挪进某项目(它的邮箱地址跟着走;媒体资产不动)")
|
|
325
|
+
.action((d, p) => emit(remote ? remoteClient.api.projectMove(remote, d, p) : projectsCore.moveDomain(ctx(), d, p)));
|
|
326
|
+
projects
|
|
327
|
+
.command("rename <slug>")
|
|
328
|
+
.description("改项目显示名 / 标识(default 不能改标识)")
|
|
329
|
+
.option("--name <name>", "新显示名")
|
|
330
|
+
.option("--slug <newSlug>", "新标识(检出过旧标识的目录要重新 clize use)")
|
|
331
|
+
.action((slug, opts) => emit(remote
|
|
332
|
+
? remoteClient.api.projectRename(remote, slug, { name: opts.name, slug: opts.slug })
|
|
333
|
+
: projectsCore.renameProject(ctx(), slug, { name: opts.name, slug: opts.slug })));
|
|
334
|
+
projects
|
|
335
|
+
.command("rm <slug>")
|
|
336
|
+
.description("删项目(default 不可删;非空项目必须 --into 指定资源去向,绝不级联删资源)")
|
|
337
|
+
.option("--into <project>", "把项目下资源(域名 + 媒体资产)归并到哪个项目,如 default")
|
|
338
|
+
.action((slug, opts) => emit(remote
|
|
339
|
+
? remoteClient.api.projectRemove(remote, slug, opts.into)
|
|
340
|
+
: projectsCore.removeProject(ctx(), slug, { into: opts.into })));
|
|
341
|
+
program
|
|
342
|
+
.command("use <project>")
|
|
343
|
+
.description("把当前目录切到某个项目(此后本目录的命令默认作用于它;写 ./clize.json)")
|
|
344
|
+
.action((project) => emit((async () => {
|
|
345
|
+
const list = (remote
|
|
346
|
+
? await remoteClient.api.projectsList(remote)
|
|
347
|
+
: await projectsCore.listProjects(ctx()));
|
|
348
|
+
if (!Array.isArray(list) || !list.some((p) => p.slug === project))
|
|
349
|
+
throw new Error(`没有叫 "${project}" 的项目;先看看有哪些:clize projects(新建:clize projects new "<名字>")`);
|
|
350
|
+
return setupCore.useProject(project);
|
|
351
|
+
})()));
|
|
234
352
|
program
|
|
235
353
|
.command("install")
|
|
236
354
|
.description("把 clize 接进本机 coding agent(Claude Code / Codex):默认只铺 skill;加 --mcp 才另登记 MCP server")
|
|
@@ -254,11 +372,10 @@ program
|
|
|
254
372
|
.command("check [domain]")
|
|
255
373
|
.description("连通性自检:验证凭证 / 账户 / 域名查价(只读,不花钱)")
|
|
256
374
|
.action((domain) => remote ? emit(remoteClient.check(remote)) : selfcheck(ctx(), domain));
|
|
257
|
-
program
|
|
375
|
+
withProject(program
|
|
258
376
|
.command("audit")
|
|
259
|
-
.description("查花钱 / 对外动作的审计记录")
|
|
260
|
-
.option("-n, --limit <n>", "返回条数", "20")
|
|
261
|
-
.action((opts) => emit(remote
|
|
377
|
+
.description("查花钱 / 对外动作的审计记录(检出项目时只看该项目的花钱动作)")
|
|
378
|
+
.option("-n, --limit <n>", "返回条数", "20")).action((opts) => emit(remote
|
|
262
379
|
? remoteClient.api.audit(remote, Number(opts.limit))
|
|
263
380
|
: domainsCore.auditLog(ctx(), Number(opts.limit))));
|
|
264
381
|
program
|
|
@@ -390,15 +507,13 @@ billing
|
|
|
390
507
|
return emit(Promise.reject(new Error("指定 --on / --off / --amount 之一。")));
|
|
391
508
|
return emit(remoteClient.api.billingAutopay(remote, patch));
|
|
392
509
|
});
|
|
393
|
-
program
|
|
510
|
+
withProject(program
|
|
394
511
|
.command("context [address]")
|
|
395
|
-
.description("会话开头 rehydrate:某地址的 身份+知识+操作规约 → 进 agent 的 system prompt(无地址=列地址)")
|
|
396
|
-
|
|
397
|
-
program
|
|
512
|
+
.description("会话开头 rehydrate:某地址的 身份+知识+操作规约 → 进 agent 的 system prompt(无地址=列地址)")).action((address) => emit(remote ? remoteClient.api.context(remote, address) : addressesCore.addressContext(ctx(), address)));
|
|
513
|
+
withProject(program
|
|
398
514
|
.command("claim <slug>")
|
|
399
|
-
.description("占一个免费 handle <slug>.clize.app(先到先得;默认自动开 support@
|
|
400
|
-
.option("--no-email", "只占名,不自动开邮箱")
|
|
401
|
-
.action((slug, opts) => emit(remote
|
|
515
|
+
.description("占一个免费 handle <slug>.clize.app(先到先得;默认自动开 support@ 收信;归当前检出项目)")
|
|
516
|
+
.option("--no-email", "只占名,不自动开邮箱")).action((slug, opts) => emit(remote
|
|
402
517
|
? remoteClient.api.claim(remote, slug)
|
|
403
518
|
: handleCore.claimHandle(ctx(), slug, { provisionEmail: opts.email })));
|
|
404
519
|
program
|
|
@@ -422,12 +537,11 @@ domain
|
|
|
422
537
|
supportedTlds: domainsCore.SUPPORTED_TLDS,
|
|
423
538
|
note: "Cloudflare 注册商 beta;.co / .io / .ai / .consulting / .agency 等暂不支持。",
|
|
424
539
|
}));
|
|
425
|
-
domain
|
|
540
|
+
withProject(domain
|
|
426
541
|
.command("buy <domain>")
|
|
427
|
-
.description("注册域名(💰 花钱:必须 --confirm
|
|
542
|
+
.description("注册域名(💰 花钱:必须 --confirm;缺则只返报价;归当前检出项目)")
|
|
428
543
|
.option("--confirm", "确认花钱注册")
|
|
429
|
-
.option("--no-auto-renew", "关闭自动续费(默认开,防过期)")
|
|
430
|
-
.action((d, opts) => {
|
|
544
|
+
.option("--no-auto-renew", "关闭自动续费(默认开,防过期)")).action((d, opts) => {
|
|
431
545
|
if (remote)
|
|
432
546
|
return emit(remoteClient.api.domainBuy(remote, d, !!opts.confirm, opts.autoRenew));
|
|
433
547
|
if (!opts.confirm) {
|
|
@@ -438,23 +552,17 @@ domain
|
|
|
438
552
|
}
|
|
439
553
|
return emit(domainsCore.registerDomain(ctx(), d, { autoRenew: opts.autoRenew }));
|
|
440
554
|
});
|
|
441
|
-
domain
|
|
442
|
-
|
|
443
|
-
.description("接入已有域名(迁 NS 托管,幂等)")
|
|
444
|
-
.action((d) => emit(remote ? remoteClient.api.domainImport(remote, d) : domainsCore.importDomain(ctx(), d)));
|
|
445
|
-
domain
|
|
446
|
-
.command("list")
|
|
447
|
-
.description("列出本租户的域名")
|
|
448
|
-
.action(() => emit(remote ? remoteClient.api.domainList(remote) : domainsCore.listDomains(ctx())));
|
|
555
|
+
withProject(domain.command("import <domain>").description("接入已有域名(迁 NS 托管,幂等;新接入归当前检出项目)")).action((d) => emit(remote ? remoteClient.api.domainImport(remote, d) : domainsCore.importDomain(ctx(), d)));
|
|
556
|
+
withProject(domain.command("list").description("列出本租户的域名(检出项目时只列该项目)")).action(() => emit(remote ? remoteClient.api.domainList(remote) : domainsCore.listDomains(ctx())));
|
|
449
557
|
// ───────── 邮箱 ─────────
|
|
450
558
|
const email = program.command("email").description("邮箱:配置 / 发信 / 收信");
|
|
451
559
|
email
|
|
452
560
|
.command("setup <domain>")
|
|
453
561
|
.description("配自定义域收发链路(MX / SPF + 发信域名验证)")
|
|
454
562
|
.action((d) => emit(remote ? remoteClient.api.emailSetup(remote, d) : emailCore.setupEmail(ctx(), d)));
|
|
455
|
-
email
|
|
563
|
+
withProject(email
|
|
456
564
|
.command("send")
|
|
457
|
-
.description("发信(📨 身份对外硬闸:默认只出草稿,加 --confirm 才真发)")
|
|
565
|
+
.description("发信(📨 身份对外硬闸:默认只出草稿,加 --confirm 才真发)"))
|
|
458
566
|
.requiredOption("--to <addr>", "收件人(逗号分隔多个)")
|
|
459
567
|
.requiredOption("--subject <s>", "主题")
|
|
460
568
|
.option("--from <addr>", "发件人(默认项目地址)")
|
|
@@ -462,23 +570,30 @@ email
|
|
|
462
570
|
.option("--html <h>", "HTML 正文")
|
|
463
571
|
.option("--attach <paths>", "附件路径(逗号分隔多个;如 gen image 生成的图)")
|
|
464
572
|
.option("--confirm", "确认发送(缺省只返回草稿供人核)")
|
|
573
|
+
.option("--allow-duplicate", "越过防重发闸(10 分钟内同收件人同主题默认拒发,防重试重复发信)")
|
|
465
574
|
.action((opts) => {
|
|
466
575
|
const to = String(opts.to).includes(",")
|
|
467
576
|
? String(opts.to).split(",").map((s) => s.trim())
|
|
468
577
|
: opts.to;
|
|
469
578
|
const attachments = loadAttachments(opts.attach);
|
|
470
|
-
// 托管模式:from
|
|
579
|
+
// 托管模式:from 缺省同样从本地 clize.json 推导(CLI 在用户机器上跑,读得到);
|
|
580
|
+
// confirm 透传给后端裁决草稿/真发。
|
|
471
581
|
if (remote) {
|
|
472
|
-
|
|
473
|
-
|
|
582
|
+
const rSender = opts.from
|
|
583
|
+
? { from: opts.from, replyTo: undefined }
|
|
584
|
+
: setupCore.projectSender();
|
|
585
|
+
if (!rSender?.from)
|
|
586
|
+
return emit(Promise.reject(new Error("需 --from <地址>,或先 `clize init --handle <slug>` 绑定项目以自动带出发件人")));
|
|
474
587
|
return emit(remoteClient.api.emailSend(remote, {
|
|
475
|
-
from:
|
|
588
|
+
from: rSender.from,
|
|
476
589
|
to,
|
|
477
590
|
subject: opts.subject,
|
|
478
591
|
text: opts.text,
|
|
479
592
|
html: opts.html,
|
|
593
|
+
replyTo: rSender.replyTo,
|
|
480
594
|
attachments,
|
|
481
595
|
confirm: !!opts.confirm,
|
|
596
|
+
allowDuplicate: !!opts.allowDuplicate,
|
|
482
597
|
}));
|
|
483
598
|
}
|
|
484
599
|
// from 缺省时从项目(clize.json)带出发件人 + 回信地址
|
|
@@ -503,7 +618,7 @@ email
|
|
|
503
618
|
},
|
|
504
619
|
message: "📨 这是草稿,还没发。把它给用户看、确认后加 --confirm 才真发:clize email send … --confirm",
|
|
505
620
|
});
|
|
506
|
-
return emit(emailCore.sendEmail(ctx(), msg));
|
|
621
|
+
return emit(emailCore.sendEmail(ctx(), msg, { allowDuplicate: !!opts.allowDuplicate }));
|
|
507
622
|
});
|
|
508
623
|
email
|
|
509
624
|
.command("inbox <domain>")
|
|
@@ -525,9 +640,16 @@ email
|
|
|
525
640
|
.description("读单封邮件全文")
|
|
526
641
|
.action((d, id) => emit(remote ? remoteClient.api.emailShow(remote, d, id) : emailCore.getMessage(ctx(), d, id)));
|
|
527
642
|
email
|
|
528
|
-
.command("thread <
|
|
529
|
-
.description("
|
|
530
|
-
.action((
|
|
643
|
+
.command("thread <contactOrDomain> [contact]")
|
|
644
|
+
.description("读与某联系人的往来。init 过的目录单参即可:email thread <对方地址>;双参 = <收信域> <对方地址>")
|
|
645
|
+
.action((a, b) => {
|
|
646
|
+
// 单参:a=联系人,收信域从 clize.json 推导(与 deploy 免 --domain、send 免 --from 同一红利)。
|
|
647
|
+
const domain = b ? a : setupCore.projectDomain();
|
|
648
|
+
const contact = b ?? a;
|
|
649
|
+
if (!domain)
|
|
650
|
+
return emit(Promise.reject(new Error("没绑定收信域:clize email thread <domain> <contact>,或先 clize init 绑定本目录后单参使用")));
|
|
651
|
+
return emit(remote ? remoteClient.api.emailThread(remote, domain, contact) : emailCore.emailThread(ctx(), domain, contact));
|
|
652
|
+
});
|
|
531
653
|
email
|
|
532
654
|
.command("inbox-setup <address>")
|
|
533
655
|
.description("开启 agent 自持收信(Email Worker 接住入站,存收件箱)")
|
|
@@ -577,18 +699,16 @@ address
|
|
|
577
699
|
.action((addr) => emit(remote
|
|
578
700
|
? remoteClient.api.emailAddressRemove(remote, addr)
|
|
579
701
|
: addressesCore.removeAddress(ctx(), addr)));
|
|
580
|
-
email
|
|
581
|
-
.command("list")
|
|
582
|
-
.description("列出所有地址 + tag")
|
|
583
|
-
.action(() => emit(remote ? remoteClient.api.emailListAddresses(remote) : addressesCore.listAddresses(ctx())));
|
|
702
|
+
withProject(email.command("list").description("列出所有地址 + tag(检出项目时只列该项目域名下的)")).action(() => emit(remote ? remoteClient.api.emailListAddresses(remote) : addressesCore.listAddresses(ctx())));
|
|
584
703
|
// ───────── 部署 ─────────
|
|
585
|
-
program
|
|
704
|
+
withProject(program
|
|
586
705
|
.command("deploy <path>")
|
|
587
706
|
.description("部署:目录 + <slug>.clize.app → 免费 handle 多文件站;或 <名> --html 内联单页")
|
|
588
707
|
.option("--domain <domain>", "目标域(<slug>.clize.app 走免费 handle)")
|
|
589
708
|
.option("--html <html>", "内联 HTML(单页 worker,不读目录)")
|
|
590
|
-
.option("--html-file <file>", "从文件读 HTML")
|
|
591
|
-
.
|
|
709
|
+
.option("--html-file <file>", "从文件读 HTML")).action((p, opts) => {
|
|
710
|
+
// 缺 --domain 时从 ./clize.json 推导(clize init 绑过的目录免显式传参,与 email send 推 from 对称)。
|
|
711
|
+
const domain = opts.domain ?? setupCore.projectDomain();
|
|
592
712
|
if (remote) {
|
|
593
713
|
let dir = false;
|
|
594
714
|
try {
|
|
@@ -597,38 +717,71 @@ program
|
|
|
597
717
|
catch {
|
|
598
718
|
/* 非路径 */
|
|
599
719
|
}
|
|
600
|
-
if (!dir
|
|
601
|
-
return emit(Promise.reject(new Error(
|
|
720
|
+
if (!dir)
|
|
721
|
+
return emit(Promise.reject(new Error(`${p} 不是存在的目录 —— 先生成站点文件再 deploy(托管模式只支持目录部署;--html 内联单页暂仅本地模式)`)));
|
|
722
|
+
if (!domain)
|
|
723
|
+
return emit(Promise.reject(new Error("缺目标域:clize deploy <目录> --domain <已 claim 的 host>,或先 clize init --handle <slug> 绑定本目录免传 --domain")));
|
|
602
724
|
const files = sitesCore.readDirFiles(p);
|
|
603
725
|
const bytes = files.reduce((n, f) => n + Math.ceil(f.base64.length * 0.75), 0);
|
|
604
726
|
if (bytes > 25 * 1024 * 1024)
|
|
605
727
|
return emit(Promise.reject(new Error(`站点约 ${(bytes / 1048576).toFixed(1)}MB,超过托管部署上限 25MB;请精简`)));
|
|
606
|
-
return emit(remoteClient.api.deploy(remote,
|
|
728
|
+
return emit(remoteClient.api.deploy(remote, domain, files));
|
|
607
729
|
}
|
|
608
730
|
let isDir = false;
|
|
609
731
|
try {
|
|
610
732
|
isDir = fs.statSync(p).isDirectory();
|
|
611
733
|
}
|
|
612
734
|
catch {
|
|
613
|
-
/*
|
|
735
|
+
/* 非路径:仅显式 --html/--html-file 时才允许当 worker 名走内联(否则下面报"目录不存在") */
|
|
614
736
|
}
|
|
615
737
|
if (isDir) {
|
|
616
|
-
if (!
|
|
617
|
-
return emit(Promise.reject(new Error("目录(多文件)部署需要 --domain <域名>(需已 claim / buy / import)")));
|
|
618
|
-
return emit(sitesCore.deployToHandle(ctx(),
|
|
738
|
+
if (!domain)
|
|
739
|
+
return emit(Promise.reject(new Error("目录(多文件)部署需要 --domain <域名>(需已 claim / buy / import),或先 clize init 绑定本目录")));
|
|
740
|
+
return emit(sitesCore.deployToHandle(ctx(), domain, p));
|
|
619
741
|
}
|
|
742
|
+
// 路径不存在且没给内联 HTML:大概率是站点还没生成 / 路径打错 —— 直接说清,
|
|
743
|
+
// 绝不带着错参数去打 CF(那会报回毫不相干的 workers.dev 错误,误导排查方向)。
|
|
744
|
+
if (!opts.html && !opts.htmlFile)
|
|
745
|
+
return emit(Promise.reject(new Error(`${p} 不是存在的目录 —— 先生成站点文件再 deploy;内联单页要显式 --html "<...>" 或 --html-file <path>`)));
|
|
620
746
|
const html = opts.htmlFile ? fs.readFileSync(opts.htmlFile, "utf8") : opts.html;
|
|
621
747
|
return emit(siteCore.deploySite(ctx(), { workerName: p, html, domain: opts.domain }));
|
|
622
748
|
});
|
|
749
|
+
// ───────── 本地预览(serve:deploy 的本地对偶)─────────
|
|
750
|
+
// 起一个支持 Range + 媒体不灌 no-store 的静态 server —— 预览含 <video> 的站点 / 片子用它,
|
|
751
|
+
// 别 `python -m http.server`(不支持 Range:Safari 不播、不能 seek)。只伺服本地路径、不碰项目资源,故不包 withProject。
|
|
752
|
+
program
|
|
753
|
+
.command("serve [path]")
|
|
754
|
+
.description("本地预览:起支持 Range·媒体不灌 no-store 的静态 server(预览含视频的站点 / 片子用它,别 python -m http.server)")
|
|
755
|
+
.option("--port <n>", "端口(被占用自动顺延)", "8000")
|
|
756
|
+
.option("--open", "起好后用默认浏览器打开(给人看;agent 不用)")
|
|
757
|
+
.action(async (pathArg, opts) => {
|
|
758
|
+
const root = pathArg
|
|
759
|
+
? path.resolve(pathArg)
|
|
760
|
+
: fs.existsSync("site")
|
|
761
|
+
? path.resolve("site")
|
|
762
|
+
: process.cwd();
|
|
763
|
+
try {
|
|
764
|
+
const h = await serveCore.serve(root, { port: Number(opts.port) || 8000 });
|
|
765
|
+
process.stdout.write(`clize serve · ${h.url} (root: ${h.root})\n` +
|
|
766
|
+
`支持 Range·媒体不灌 no-store —— <video> 能 seek、Safari 能播。Ctrl-C 停止。\n`);
|
|
767
|
+
if (opts.open)
|
|
768
|
+
openBrowser(h.url);
|
|
769
|
+
await new Promise(() => { }); // 常驻:阻塞到 Ctrl-C
|
|
770
|
+
}
|
|
771
|
+
catch (e) {
|
|
772
|
+
process.stderr.write(`serve 失败:${e.message}\n`);
|
|
773
|
+
process.exitCode = 1;
|
|
774
|
+
}
|
|
775
|
+
});
|
|
623
776
|
// ───────── 生成式媒体(gen:图 / 视频 / 音乐)─────────
|
|
624
777
|
// G1 = 本地算力 + 你自带的 model key,与 clize 登录模式无关(remote 登录也在本地跑、写本地盘);
|
|
625
778
|
// 托管(clize 代付 + R2 托管)是 G2,所以这里不按 remote 分流。
|
|
626
779
|
const gen = program
|
|
627
780
|
.command("gen")
|
|
628
|
-
.description("生成式媒体:你写 prompt,clize 产字节落盘(图 / 视频 /
|
|
629
|
-
gen
|
|
781
|
+
.description("生成式媒体:你写 prompt,clize 产字节落盘(图 / 视频 / 音乐;蓝图批量 = check + render)");
|
|
782
|
+
withProject(gen
|
|
630
783
|
.command("image <prompt>")
|
|
631
|
-
.description("文生图 / 图生图(💰:--confirm 才生成,缺则只报价;可先 gen budget 预批免逐次确认)")
|
|
784
|
+
.description("文生图 / 图生图(💰:--confirm 才生成,缺则只报价;可先 gen budget 预批免逐次确认)"))
|
|
632
785
|
.option("--model <m>", "模型:gpt-image-2(OpenAI)| nano-banana-2(Google)", "gpt-image-2")
|
|
633
786
|
.option("--n <k>", "出几张", "1")
|
|
634
787
|
.option("--size <s>", "尺寸,如 1024x1024")
|
|
@@ -664,11 +817,10 @@ gen
|
|
|
664
817
|
out: opts.out,
|
|
665
818
|
}, { confirm: !!opts.confirm }));
|
|
666
819
|
});
|
|
667
|
-
gen
|
|
820
|
+
withProject(gen
|
|
668
821
|
.command("list")
|
|
669
|
-
.description("列已生成资产(id / 模型 / prompt / 路径 /
|
|
670
|
-
.option("--modality <m>", "只看某模态:image | video | music")
|
|
671
|
-
.action((opts) => emit(remote ? remoteClient.api.genList(remote, opts.modality) : mediaCore.listAssets(ctx(), opts.modality)));
|
|
822
|
+
.description("列已生成资产(id / 模型 / prompt / 路径 / 花费;检出项目时只列该项目)")
|
|
823
|
+
.option("--modality <m>", "只看某模态:image | video | music")).action((opts) => emit(remote ? remoteClient.api.genList(remote, opts.modality) : mediaCore.listAssets(ctx(), opts.modality)));
|
|
672
824
|
gen
|
|
673
825
|
.command("show <id>")
|
|
674
826
|
.description("看单个生成任务 / 资产全貌")
|
|
@@ -683,12 +835,13 @@ gen
|
|
|
683
835
|
const amount = raw != null && raw !== "" && !Number.isNaN(Number(raw)) ? Number(raw) : undefined;
|
|
684
836
|
return emit(amount != null ? mediaCore.setGenBudget(ctx(), amount) : mediaCore.genBudget(ctx()));
|
|
685
837
|
});
|
|
686
|
-
gen
|
|
838
|
+
withProject(gen
|
|
687
839
|
.command("video <prompt>")
|
|
688
|
-
.description("文生视频 / 图生视频(长任务:默认等到完成,--async 后台返 job id)")
|
|
840
|
+
.description("文生视频 / 图生视频(长任务:默认等到完成,--async 后台返 job id)"))
|
|
689
841
|
.option("--model <m>", "模型(默认 veo)", "veo")
|
|
690
842
|
.option("--from-image <path>", "首帧图(图生视频)")
|
|
691
|
-
.option("--
|
|
843
|
+
.option("--ref <paths>", "参考图(逗号分隔多张:角色图+场景图保跨镜一致性;veo 上限 3 张)")
|
|
844
|
+
.option("--duration <sec>", "时长(秒;veo 忽略 —— 时长模型自定、单次 ≤8s)")
|
|
692
845
|
.option("--aspect <r>", "画幅,如 16:9")
|
|
693
846
|
.option("--out <path>", "落盘路径(--wait 时;--async 走默认 ./clize-assets)")
|
|
694
847
|
.option("--async", "后台生成,只返 job id(默认等到完成)")
|
|
@@ -696,28 +849,30 @@ gen
|
|
|
696
849
|
.option("--confirm", "确认花钱生成")
|
|
697
850
|
.action((prompt, opts) => {
|
|
698
851
|
if (remote) {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
return emit(remoteGenAsync(remote, "video", {
|
|
852
|
+
// 托管:参考图在 CLI 端读盘转 data URI 上传(首帧排第一,与本地 core 同序)。
|
|
853
|
+
const paths = [...new Set([opts.fromImage, ...splitRefs(opts.ref)].filter(Boolean))];
|
|
854
|
+
return emit((async () => remoteGenAsync(remote, "video", {
|
|
702
855
|
prompt,
|
|
703
856
|
model: opts.model,
|
|
857
|
+
refs: paths.length ? refsToDataUris(paths) : undefined,
|
|
704
858
|
durationSec: opts.duration ? Number(opts.duration) : undefined,
|
|
705
859
|
aspect: opts.aspect,
|
|
706
860
|
confirm: !!opts.confirm,
|
|
707
|
-
}, { confirm: !!opts.confirm, async: !!opts.async, out: opts.out, timeout: opts.timeout }));
|
|
861
|
+
}, { confirm: !!opts.confirm, async: !!opts.async, out: opts.out, timeout: opts.timeout }))());
|
|
708
862
|
}
|
|
709
863
|
return emit(mediaCore.genVideo(ctx(), {
|
|
710
864
|
prompt,
|
|
711
865
|
model: opts.model,
|
|
712
866
|
fromImage: opts.fromImage,
|
|
867
|
+
refs: splitRefs(opts.ref),
|
|
713
868
|
durationSec: opts.duration ? Number(opts.duration) : undefined,
|
|
714
869
|
aspect: opts.aspect,
|
|
715
870
|
out: opts.out,
|
|
716
871
|
}, { confirm: !!opts.confirm, async: !!opts.async, timeoutSec: Number(opts.timeout) }));
|
|
717
872
|
});
|
|
718
|
-
gen
|
|
873
|
+
withProject(gen
|
|
719
874
|
.command("music <prompt>")
|
|
720
|
-
.description("文生音乐(长任务:默认等到完成,--async 后台返 job id)")
|
|
875
|
+
.description("文生音乐(长任务:默认等到完成,--async 后台返 job id)"))
|
|
721
876
|
.option("--model <m>", "模型(默认 suno)", "suno")
|
|
722
877
|
.option("--instrumental", "纯器乐(无人声)")
|
|
723
878
|
.option("--duration <sec>", "时长(秒)")
|
|
@@ -743,10 +898,7 @@ gen
|
|
|
743
898
|
out: opts.out,
|
|
744
899
|
}, { confirm: !!opts.confirm, async: !!opts.async, timeoutSec: Number(opts.timeout) }));
|
|
745
900
|
});
|
|
746
|
-
gen
|
|
747
|
-
.command("jobs")
|
|
748
|
-
.description("列生成任务(running 置顶;断会话后重连长任务用)")
|
|
749
|
-
.action(() => emit(remote ? remoteClient.api.genJobs(remote) : mediaCore.genJobs(ctx())));
|
|
901
|
+
withProject(gen.command("jobs").description("列生成任务(running 置顶;断会话后重连长任务用;检出项目时只列该项目)")).action(() => emit(remote ? remoteClient.api.genJobs(remote) : mediaCore.genJobs(ctx())));
|
|
750
902
|
gen
|
|
751
903
|
.command("status <id>")
|
|
752
904
|
.description("查单任务:running 则向 provider 轮询一次(可能就此完成落盘)")
|
|
@@ -759,43 +911,169 @@ gen
|
|
|
759
911
|
.command("rm <id>")
|
|
760
912
|
.description("删任务记录 + 本地文件")
|
|
761
913
|
.action((id) => emit(remote ? remoteClient.api.genRm(remote, id) : mediaCore.genRm(ctx(), id)));
|
|
762
|
-
// ─────────
|
|
763
|
-
|
|
914
|
+
// ───────── build:作品域(端到端做一个作品;方法 + 校验免费,成品步骤花钱带闸)─────────
|
|
915
|
+
// taxonomy:gen = 原子生成 + 任务池(引擎),build = 作品编排门面(方法 → 你创作 → 校验 → 成品)。
|
|
916
|
+
// 第 N 种作品 = build 加子域(方法三件套 + 该作品特有的校验/成品命令),顶级命令面零膨胀。
|
|
917
|
+
const hostedBuild = (fn) => emit(remote
|
|
764
918
|
? fn(remote)
|
|
765
|
-
: Promise.reject(new Error("clize
|
|
766
|
-
const
|
|
767
|
-
.command("
|
|
768
|
-
.description("
|
|
769
|
-
design
|
|
919
|
+
: Promise.reject(new Error("clize build 的方法库需登录托管模式:先 `clize login`(作品方法 + 风格库在 clize 云端,只发登录用户)")));
|
|
920
|
+
const build = program
|
|
921
|
+
.command("build")
|
|
922
|
+
.description("做一个作品(端到端):build site = 站点/UI · build clip = 短片;方法/校验免费,成品步骤 💰");
|
|
923
|
+
/** 站点/UI 的 7 条方法命令构建器:build site(正式位)与裸 design(0.7.8 兼容别名,help 隐藏)各灌一套。 */
|
|
924
|
+
function siteBuildCommands() {
|
|
925
|
+
const start = new Command("start")
|
|
926
|
+
.argument("<brief...>")
|
|
927
|
+
.description("开工:返回工作流(7 步 + 反 slop)+ 风格目录 + 针对 brief 的推荐")
|
|
928
|
+
.option("--project <name>", "项目名")
|
|
929
|
+
.action((brief, opts) => hostedBuild((rc) => remoteClient.api.buildSiteStart(rc, brief.join(" "), opts.project)));
|
|
930
|
+
const recommend = new Command("recommend")
|
|
931
|
+
.argument("<brief...>")
|
|
932
|
+
.description("按 brief 推荐:风格 + 调色板 + 字体对 + 反模式")
|
|
933
|
+
.option("--project <name>", "项目名")
|
|
934
|
+
.action((brief, opts) => hostedBuild((rc) => remoteClient.api.buildSiteRecommend(rc, brief.join(" "), opts.project)));
|
|
935
|
+
const list = new Command("list")
|
|
936
|
+
.description("列出全部风格宪法(slug + 一句话 best-for)")
|
|
937
|
+
.action(() => hostedBuild((rc) => remoteClient.api.buildSiteList(rc)));
|
|
938
|
+
const get = new Command("get")
|
|
939
|
+
.argument("<slug>")
|
|
940
|
+
.description("取某风格的完整宪法(色 / 字 / 形 / 签名手法)+ imagery 配图契约 + 调色板 / 字体对 + DESIGN.md 模板")
|
|
941
|
+
.action((slug) => hostedBuild((rc) => remoteClient.api.buildSiteGet(rc, slug)));
|
|
942
|
+
const stack = new Command("stack")
|
|
943
|
+
.argument("<stack>")
|
|
944
|
+
.argument("[query...]")
|
|
945
|
+
.description("取某技术栈实现指南(React/Next/SwiftUI/Flutter/shadcn… 惯用法 + 行为规则;跨栈 UI 指南)")
|
|
946
|
+
.action((stackName, query) => hostedBuild((rc) => remoteClient.api.buildSiteStack(rc, stackName, (query || []).join(" "))));
|
|
947
|
+
const search = new Command("search")
|
|
948
|
+
.argument("<query...>")
|
|
949
|
+
.description("查 UUPM 库(ux/color/chart/landing/product/typography/icons…;默认自动判域)")
|
|
950
|
+
.option("--domain <d>", "限定域")
|
|
951
|
+
.action((query, opts) => hostedBuild((rc) => remoteClient.api.buildSiteSearch(rc, query.join(" "), opts.domain)));
|
|
952
|
+
const review = new Command("review")
|
|
953
|
+
.description("取合并自审清单(anti-slop + DESIGN.md 保真 + UX 行为)")
|
|
954
|
+
.action(() => hostedBuild((rc) => remoteClient.api.buildSiteReview(rc)));
|
|
955
|
+
return [start, recommend, list, get, stack, search, review];
|
|
956
|
+
}
|
|
957
|
+
const buildSite = build
|
|
958
|
+
.command("site")
|
|
959
|
+
.description("站点/UI 作品:start 取方法+推荐 → 你写码 → review 自审 → deploy 上线(成品步骤复用顶级 deploy)");
|
|
960
|
+
for (const c of siteBuildCommands())
|
|
961
|
+
buildSite.addCommand(c);
|
|
962
|
+
// 兼容别名:0.7.8 起发布的 clize design start… 继续工作;help 不展示(正式位 build site)。
|
|
963
|
+
const designAlias = new Command("design");
|
|
964
|
+
for (const c of siteBuildCommands())
|
|
965
|
+
designAlias.addCommand(c);
|
|
966
|
+
program.addCommand(designAlias, { hidden: true });
|
|
967
|
+
/** 读蓝图 JSON 文件(build clip check / render 共用)。 */
|
|
968
|
+
function readBlueprint(file) {
|
|
969
|
+
let raw;
|
|
970
|
+
try {
|
|
971
|
+
raw = fs.readFileSync(file, "utf8");
|
|
972
|
+
}
|
|
973
|
+
catch {
|
|
974
|
+
throw new Error(`读不到蓝图文件:${file}`);
|
|
975
|
+
}
|
|
976
|
+
try {
|
|
977
|
+
return JSON.parse(raw);
|
|
978
|
+
}
|
|
979
|
+
catch (e) {
|
|
980
|
+
throw new Error(`蓝图不是合法 JSON:${e instanceof Error ? e.message : String(e)}`);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
/** render 后端(本地):探价走 provider.quote;提交/轮询走 core/media(任务进 gen jobs 体系)。 */
|
|
984
|
+
function localRenderBackend() {
|
|
985
|
+
return {
|
|
986
|
+
quote: (p) => mediaCore.quoteVideo(ctx(), p),
|
|
987
|
+
submit: async (p) => {
|
|
988
|
+
const r = (await mediaCore.genVideo(ctx(), { ...p }, { confirm: true, async: true }));
|
|
989
|
+
if (!r.id)
|
|
990
|
+
throw new Error(`提交未返回任务 id${r.message ? `:${String(r.message)}` : ""}`);
|
|
991
|
+
return { id: String(r.id), state: String(r.state ?? "running") };
|
|
992
|
+
},
|
|
993
|
+
poll: async (id, outPath) => {
|
|
994
|
+
const r = (await mediaCore.genJobStatus(ctx(), id));
|
|
995
|
+
if (r.state === "succeeded") {
|
|
996
|
+
const src = r.files?.[0]?.path;
|
|
997
|
+
if (src && path.resolve(src) !== path.resolve(outPath)) {
|
|
998
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
999
|
+
fs.copyFileSync(src, outPath);
|
|
1000
|
+
}
|
|
1001
|
+
return { state: "succeeded" };
|
|
1002
|
+
}
|
|
1003
|
+
if (r.state === "failed")
|
|
1004
|
+
return { state: "failed", error: r.error ? String(r.error) : undefined };
|
|
1005
|
+
return { state: "running" };
|
|
1006
|
+
},
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
/** render 后端(托管):参考图逐镜转 data URI 上传;confirm=false 探价是后端契约(永不生成)。 */
|
|
1010
|
+
function remoteRenderBackend(rc) {
|
|
1011
|
+
const body = (p, confirm) => ({
|
|
1012
|
+
prompt: p.prompt,
|
|
1013
|
+
model: p.model,
|
|
1014
|
+
refs: p.refs?.length ? refsToDataUris(p.refs) : undefined,
|
|
1015
|
+
durationSec: p.durationSec,
|
|
1016
|
+
aspect: p.aspect,
|
|
1017
|
+
confirm,
|
|
1018
|
+
});
|
|
1019
|
+
return {
|
|
1020
|
+
quote: async (p) => {
|
|
1021
|
+
const r = (await remoteClient.api.genVideo(rc, body(p, false)));
|
|
1022
|
+
const est = r?.quote?.estUsd;
|
|
1023
|
+
if (typeof est !== "number")
|
|
1024
|
+
throw new Error("探价未返回 quote(后端响应异常)");
|
|
1025
|
+
return est;
|
|
1026
|
+
},
|
|
1027
|
+
submit: async (p) => {
|
|
1028
|
+
const r = (await remoteClient.api.genVideo(rc, body(p, true)));
|
|
1029
|
+
if (!r.id)
|
|
1030
|
+
throw new Error(`提交未返回任务 id${r.message ? `:${String(r.message)}` : ""}`);
|
|
1031
|
+
return { id: String(r.id), state: String(r.state ?? "running") };
|
|
1032
|
+
},
|
|
1033
|
+
poll: async (id, outPath) => {
|
|
1034
|
+
const r = (await remoteClient.api.genStatus(rc, id));
|
|
1035
|
+
if (r.state === "succeeded") {
|
|
1036
|
+
await mediaCore.materializeRemote(r, outPath);
|
|
1037
|
+
return { state: "succeeded" };
|
|
1038
|
+
}
|
|
1039
|
+
if (r.state === "failed")
|
|
1040
|
+
return { state: "failed", error: r.error ? String(r.error) : undefined };
|
|
1041
|
+
return { state: "running" };
|
|
1042
|
+
},
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
// build clip:短片作品(蓝图制)—— 方法 + 校验 + 成品一条龙;render 的任务进 gen 任务池(gen jobs/status 可查)。
|
|
1046
|
+
const buildClip = build
|
|
1047
|
+
.command("clip")
|
|
1048
|
+
.description("短片作品(蓝图制):start 取方法+风格 → 你写蓝图 → check 校验(免费)→ render 成片(💰;素材级单镜用 gen video)");
|
|
1049
|
+
buildClip
|
|
770
1050
|
.command("start <brief...>")
|
|
771
|
-
.description("开工:返回方法(4 阶段 +
|
|
772
|
-
.
|
|
773
|
-
|
|
774
|
-
design
|
|
775
|
-
.command("recommend <brief...>")
|
|
776
|
-
.description("按 brief 推荐:风格 + 调色板 + 字体对 + 反模式")
|
|
777
|
-
.option("--project <name>", "项目名")
|
|
778
|
-
.action((brief, opts) => hostedDesign((rc) => remoteClient.api.designRecommend(rc, brief.join(" "), opts.project)));
|
|
779
|
-
design
|
|
1051
|
+
.description("开工:返回方法(4 阶段 + 蓝图 schema + 反穿帮清单)+ 风格目录 + 针对 brief 的推荐")
|
|
1052
|
+
.action((brief) => hostedBuild((rc) => remoteClient.api.buildClipStart(rc, brief.join(" "))));
|
|
1053
|
+
buildClip
|
|
780
1054
|
.command("list")
|
|
781
|
-
.description("
|
|
782
|
-
.action(() =>
|
|
783
|
-
|
|
1055
|
+
.description("列出全部视频风格(id + 一句话适用场景)")
|
|
1056
|
+
.action(() => hostedBuild((rc) => remoteClient.api.buildClipList(rc)));
|
|
1057
|
+
buildClip
|
|
784
1058
|
.command("get <slug>")
|
|
785
|
-
.description("
|
|
786
|
-
.action((slug) =>
|
|
787
|
-
|
|
788
|
-
.command("
|
|
789
|
-
.description("
|
|
790
|
-
.action((
|
|
791
|
-
|
|
792
|
-
.command("
|
|
793
|
-
.description("
|
|
794
|
-
.option("--
|
|
795
|
-
.
|
|
796
|
-
|
|
797
|
-
.
|
|
798
|
-
.
|
|
799
|
-
|
|
1059
|
+
.description("取某风格完整包(资产图模板 + 视频视觉词汇 + strict 后端替换行)")
|
|
1060
|
+
.action((slug) => hostedBuild((rc) => remoteClient.api.buildClipGet(rc, slug)));
|
|
1061
|
+
buildClip
|
|
1062
|
+
.command("check <blueprint>")
|
|
1063
|
+
.description("蓝图机械校验(本地、免费、零凭证):防穿帮 lint + 台词覆盖 + 时长公式 + 结构;error 清零才 render")
|
|
1064
|
+
.action((file) => emit((async () => videoCore.checkBlueprint(readBlueprint(file)))()));
|
|
1065
|
+
withProject(buildClip
|
|
1066
|
+
.command("render <blueprint>")
|
|
1067
|
+
.description("按蓝图批量成片(💰:报价合计一次 --confirm;断点续传,重跑同命令只补未完成镜头;任务在 gen jobs 可查)"))
|
|
1068
|
+
.option("--candidates <n>", "每镜候选数(真人镜头建议 2-3 挑最好)", "1")
|
|
1069
|
+
.option("--out-dir <dir>", "输出目录(默认 clize-video/<title>)")
|
|
1070
|
+
.option("--timeout <sec>", "轮询总时限(秒)", "1800")
|
|
1071
|
+
.option("--confirm", "确认花钱生成(报价见无 --confirm 的输出)")
|
|
1072
|
+
.action((file, opts) => emit(videoCore.renderBlueprint(remote ? remoteRenderBackend(remote) : localRenderBackend(), readBlueprint(file), {
|
|
1073
|
+
confirm: !!opts.confirm,
|
|
1074
|
+
candidates: Number(opts.candidates) || 1,
|
|
1075
|
+
outDir: opts.outDir,
|
|
1076
|
+
timeoutSec: Number(opts.timeout) || 1800,
|
|
1077
|
+
})));
|
|
800
1078
|
program.parseAsync(process.argv);
|
|
801
1079
|
//# sourceMappingURL=cli.js.map
|