@clize/clize 0.10.1 → 0.12.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 (47) hide show
  1. package/README.md +9 -11
  2. package/dist/cli.js +207 -373
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.js +1 -12
  5. package/dist/config.js.map +1 -1
  6. package/dist/context.js +32 -50
  7. package/dist/context.js.map +1 -1
  8. package/dist/core/billing.js +21 -17
  9. package/dist/core/billing.js.map +1 -1
  10. package/dist/core/credentials.js +30 -0
  11. package/dist/core/credentials.js.map +1 -0
  12. package/dist/core/dns.js +27 -1
  13. package/dist/core/dns.js.map +1 -1
  14. package/dist/core/domains.js +134 -19
  15. package/dist/core/domains.js.map +1 -1
  16. package/dist/core/reconcile.js +127 -0
  17. package/dist/core/reconcile.js.map +1 -0
  18. package/dist/core/setup.js +3 -26
  19. package/dist/core/setup.js.map +1 -1
  20. package/dist/core/sites.js +72 -20
  21. package/dist/core/sites.js.map +1 -1
  22. package/dist/index.js +54 -90
  23. package/dist/index.js.map +1 -1
  24. package/dist/lib/cloudflare.js +4 -0
  25. package/dist/lib/cloudflare.js.map +1 -1
  26. package/dist/lib/crypto.js +57 -0
  27. package/dist/lib/crypto.js.map +1 -0
  28. package/dist/lib/vercel.js +67 -0
  29. package/dist/lib/vercel.js.map +1 -0
  30. package/dist/lib/zone.js +12 -0
  31. package/dist/lib/zone.js.map +1 -1
  32. package/dist/providers/dns/cloudflare.js +8 -0
  33. package/dist/providers/dns/cloudflare.js.map +1 -1
  34. package/dist/providers/index.js +15 -3
  35. package/dist/providers/index.js.map +1 -1
  36. package/dist/providers/registrar/cloudflare.js +12 -22
  37. package/dist/providers/registrar/cloudflare.js.map +1 -1
  38. package/dist/providers/registrar/routing.js +98 -0
  39. package/dist/providers/registrar/routing.js.map +1 -0
  40. package/dist/providers/registrar/vercel.js +134 -0
  41. package/dist/providers/registrar/vercel.js.map +1 -0
  42. package/dist/remote.js +20 -8
  43. package/dist/remote.js.map +1 -1
  44. package/dist/state/file-store.js +7 -0
  45. package/dist/state/file-store.js.map +1 -1
  46. package/package.json +1 -1
  47. package/skills/clize-site-debug/SKILL.md +80 -0
package/dist/cli.js CHANGED
@@ -1,40 +1,34 @@
1
1
  #!/usr/bin/env node
2
2
  // ============================================================
3
- // clize CLI —— 两种模式共用一套命令面:
4
- // • 本地(self-host):自带 Cloudflare token,命令跑本地 core,直连你自己的 CF。
5
- // • 托管(remote):配置里有 CLIZE_API_KEY(由 `clize login --api` 写),命令改走
6
- // HTTP clize 控制面后端;用户永不接触 CF。后端已就绪的命令见 remote.ts;
7
- // 其余命令在 remote 模式给友好报错。
8
- // 判定:remote.loadRemote() 非空 = 托管模式。
3
+ // clize CLI —— 纯托管瘦客户端:
4
+ // • CLI 不含 core 业务逻辑、不直连 CF/Vercel/OpenAI;所有真实动作经 clize 控制面 worker
5
+ // • 登录态 = 配置/env 里有 CLIZE_API_KEY(由 `clize login` 写)。未登录则需凭证的命令引导去登录。
6
+ // 客户端仅保留「读盘 / 落盘 / 本地 lint / 本地预览」这类不碰云的活儿
7
+ // (站点文件读取、知识文件读取、媒体落盘、蓝图校验、serve)。
9
8
  // ============================================================
10
9
  import fs from "node:fs";
11
10
  import path from "node:path";
12
11
  import { spawn } from "node:child_process";
13
12
  import { Command } from "commander";
14
- import { createLocalContext } from "./context.js";
15
13
  import * as githubClient from "./github.js";
16
- import * as domainsCore from "./core/domains.js";
17
- import * as emailCore from "./core/email.js";
18
- import * as siteCore from "./core/site.js";
19
14
  import * as addressesCore from "./core/addresses.js";
20
- import * as handleCore from "./core/handle.js";
21
15
  import * as sitesCore from "./core/sites.js";
22
16
  import * as setupCore from "./core/setup.js";
23
- import * as projectsCore from "./core/projects.js";
24
- import * as billingCore from "./core/billing.js";
25
17
  import * as installCore from "./core/install.js";
26
18
  import * as mediaCore from "./core/media.js";
27
19
  import * as videoCore from "./core/video.js";
28
20
  import * as serveCore from "./core/serve.js";
29
- import { selfcheck } from "./selfcheck.js";
30
21
  import * as remoteClient from "./remote.js";
31
22
  import { VERSION } from "./version.js";
32
- // 托管模式探测(配置里有 CLIZE_API_KEY 即启用)。
23
+ // 托管模式探测(配置里有 CLIZE_API_KEY 即登录态)。
33
24
  const remote = remoteClient.loadRemote();
34
- // 本地 ctx 惰性构造:remote 模式下完全不碰 CF / FileStore。
35
- let _ctx;
36
25
  let projectOverride; // -p <slug>:覆盖当前目录检出(clize.json)
37
- const ctx = () => (_ctx ??= createLocalContext({ projectSlug: projectOverride }));
26
+ /** 取登录态 RemoteConfig;未登录直接报错引导登录(纯托管:无凭证就没法干活) */
27
+ function rc() {
28
+ if (!remote)
29
+ throw new Error("未登录:先 clize login(网页用 GitHub / Google / 邮箱授权)");
30
+ return remote;
31
+ }
38
32
  /** 统一输出:成功打印美化 JSON(字符串原样),失败 → stderr + 退出码 1。 */
39
33
  async function emit(value) {
40
34
  try {
@@ -165,10 +159,10 @@ function splitRefs(spec) {
165
159
  : [];
166
160
  }
167
161
  /** 托管模式异步生成(视频 / 音乐):提交 →(默认)long-poll 到完成并落盘 /(--async)返 job id。 */
168
- async function remoteGenAsync(rc, modality, body, opts) {
162
+ async function remoteGenAsync(rcfg, modality, body, opts) {
169
163
  const submit = (await (modality === "video"
170
- ? remoteClient.api.genVideo(rc, body)
171
- : remoteClient.api.genMusic(rc, body)));
164
+ ? remoteClient.api.genVideo(rcfg, body)
165
+ : remoteClient.api.genMusic(rcfg, body)));
172
166
  if (!opts.confirm)
173
167
  return submit; // 只报价
174
168
  if (submit.state !== "running" || !submit.id)
@@ -182,7 +176,7 @@ async function remoteGenAsync(rc, modality, body, opts) {
182
176
  await new Promise((r) => setTimeout(r, 6000));
183
177
  let st;
184
178
  try {
185
- st = (await remoteClient.api.genStatus(rc, id));
179
+ st = (await remoteClient.api.genStatus(rcfg, id));
186
180
  }
187
181
  catch {
188
182
  continue; // 轮询偶发出错:不判失败,继续等到超时
@@ -211,7 +205,7 @@ const inCommandTree = (cmd, name) => {
211
205
  return true;
212
206
  return false;
213
207
  };
214
- // 全局 -p:命令动作前从该命令的 --project 选项取值 → 本地进 ctx.projectSlug,remote call() 统一注入 ?project=。
208
+ // 全局 -p:命令动作前从该命令的 --project 选项取值 → remote call() 统一注入 ?project=。
215
209
  program.hook("preAction", (_thisCmd, actionCmd) => {
216
210
  // build 树(及 design 兼容别名树)排除:--project 在 build site start 下是「设计项目名」
217
211
  // (给方法后端做记录),不是资源项目 slug。build clip render 仍要 -p(挂了 withProject,
@@ -229,37 +223,27 @@ program
229
223
  .description("Clize —— 给 agent 的真实世界能力层:域名 / 邮箱 / 部署 / 媒体")
230
224
  .version(VERSION)
231
225
  .showHelpAfterError();
232
- // 「未登录」闸:既没有 clize key(托管)也没有 Cloudflare 凭证(自带-CF)时,
233
- // 任何需要凭证的命令都直接引导去 `clize login`,而不是吐底层 Cloudflare 错误。
226
+ // 「未登录」闸:没有 clize key 时,任何需要凭证的命令都引导去 `clize login`(纯托管:动作经控制面)
234
227
  const NO_AUTH_CMDS = new Set(["login", "signup", "init", "install", "update", "help"]);
235
228
  program.hook("preAction", (_thisCmd, actionCmd) => {
236
229
  if (NO_AUTH_CMDS.has(actionCmd.name()))
237
230
  return;
238
- // 生成式媒体(gen *):本地只需自带 OPENAI / GEMINI key(由 core 自检),不要求 CF / clize 凭证。
239
- if (actionCmd.name() === "gen" || actionCmd.parent?.name() === "gen")
240
- return;
241
231
  // 作品域(build *,含 design 兼容别名树):hosted 方法命令自带比通用闸更准的登录引导;
242
- // build clip check 纯本地零凭证、render 同 gen 由 core 自检。
232
+ // build clip check 纯本地零凭证(蓝图机械校验,不碰云)。
243
233
  if (inCommandTree(actionCmd, "build") || inCommandTree(actionCmd, "design"))
244
234
  return;
245
- if (remote || remoteClient.hasLocalCfCreds())
235
+ if (remote)
246
236
  return;
247
237
  console.error("❌ 未登录。\n" +
248
- " 登录即用(免配任何 key):clize login # GitHub 授权,资源跑在 clize 托管\n" +
249
- " (已有 Cloudflare 想自托管?进阶:clize login --help)");
238
+ " 登录即用(免配任何 key):clize login # GitHub / Google / 邮箱授权,资源跑在 clize 托管\n" +
239
+ " CI / headless:clize login --token <clize key> --api <url>");
250
240
  process.exit(1);
251
241
  });
252
242
  // ───────── 总览 / 自检 / 审计 ─────────
253
243
  withProject(program
254
244
  .command("status")
255
245
  .description("工作空间总览:域名、邮箱 / 站点状态、本月开销;--assets 加到期巡检")
256
- .option("--assets", "含注册商处真实到期列表(资产健康)")).action((opts) => emit(remote
257
- ? opts.assets
258
- ? remoteClient.api.statusAssets(remote)
259
- : remoteClient.api.status(remote)
260
- : opts.assets
261
- ? domainsCore.assetsHealth(ctx())
262
- : domainsCore.workspaceStatus(ctx())));
246
+ .option("--assets", "含注册商处真实到期列表(资产健康)")).action((opts) => emit(opts.assets ? remoteClient.api.statusAssets(rc()) : remoteClient.api.status(rc())));
263
247
  program
264
248
  .command("signup")
265
249
  .description("注册一个 clize 托管账户:开租户 + 发 API key(随后用 clize login --api 登录)")
@@ -269,15 +253,12 @@ program
269
253
  program
270
254
  .command("login")
271
255
  .description("登录:默认在网页用 GitHub / Google / 邮箱任一授权(托管);--github 走 device flow;--token 手动")
272
- .option("--token <token>", "手动凭证(托管=clize key;本地=Cloudflare API token)")
256
+ .option("--token <token>", "手动塞 clize key(CI / headless)")
273
257
  .option("--github", "走 GitHub device flow(只认 GitHub、不想开网页时的捷径)")
274
258
  .option("--api <url>", "托管控制面地址")
275
- .option("--account <id>", "账户 id(本地模式:Cloudflare Account ID)")
276
259
  .action((opts) => {
277
260
  if (opts.token)
278
- return emit(opts.api
279
- ? setupCore.saveRemoteLogin(opts.api, opts.token)
280
- : setupCore.saveLogin(opts.token, opts.account));
261
+ return emit(setupCore.saveRemoteLogin(opts.api ?? remoteClient.DEFAULT_API_URL, opts.token));
281
262
  return emit(opts.github ? githubLogin(opts.api) : pairingLogin(opts.api));
282
263
  });
283
264
  program
@@ -297,16 +278,14 @@ const projects = program
297
278
  projects
298
279
  .command("list", { isDefault: true })
299
280
  .description("列出你的所有项目(各带域名数)")
300
- .action(() => emit(remote ? remoteClient.api.projectsList(remote) : projectsCore.listProjects(ctx())));
281
+ .action(() => emit(remoteClient.api.projectsList(rc())));
301
282
  projects
302
283
  .command("new <name>")
303
284
  .description('新建项目(名字带空格用引号,如 "Shop A";slug 缺省由名字规整)')
304
285
  .option("--slug <slug>", "项目标识(租户内唯一,CLI/URL 友好)")
305
286
  .option("--use", "建完顺手把当前目录检出到它(写 ./clize.json)")
306
287
  .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 }));
288
+ const rec = (await remoteClient.api.projectCreate(rc(), name, opts.slug));
310
289
  // 输出瘦身:slug/name 足矣(id/tenantId/createdAt 是内部细节,不进 transcript)。
311
290
  const lean = { slug: rec?.slug, name: rec?.name };
312
291
  if (opts.use && rec?.slug)
@@ -322,29 +301,23 @@ projects
322
301
  projects
323
302
  .command("move <domain> <project>")
324
303
  .description("把域名挪进某项目(它的邮箱地址跟着走;媒体资产不动)")
325
- .action((d, p) => emit(remote ? remoteClient.api.projectMove(remote, d, p) : projectsCore.moveDomain(ctx(), d, p)));
304
+ .action((d, p) => emit(remoteClient.api.projectMove(rc(), d, p)));
326
305
  projects
327
306
  .command("rename <slug>")
328
307
  .description("改项目显示名 / 标识(default 不能改标识)")
329
308
  .option("--name <name>", "新显示名")
330
309
  .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 })));
310
+ .action((slug, opts) => emit(remoteClient.api.projectRename(rc(), slug, { name: opts.name, slug: opts.slug })));
334
311
  projects
335
312
  .command("rm <slug>")
336
313
  .description("删项目(default 不可删;非空项目必须 --into 指定资源去向,绝不级联删资源)")
337
314
  .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 })));
315
+ .action((slug, opts) => emit(remoteClient.api.projectRemove(rc(), slug, opts.into)));
341
316
  program
342
317
  .command("use <project>")
343
318
  .description("把当前目录切到某个项目(此后本目录的命令默认作用于它;写 ./clize.json)")
344
319
  .action((project) => emit((async () => {
345
- const list = (remote
346
- ? await remoteClient.api.projectsList(remote)
347
- : await projectsCore.listProjects(ctx()));
320
+ const list = (await remoteClient.api.projectsList(rc()));
348
321
  if (!Array.isArray(list) || !list.some((p) => p.slug === project))
349
322
  throw new Error(`没有叫 "${project}" 的项目;先看看有哪些:clize projects(新建:clize projects new "<名字>")`);
350
323
  return setupCore.useProject(project);
@@ -371,69 +344,55 @@ program
371
344
  program
372
345
  .command("check [domain]")
373
346
  .description("连通性自检:验证凭证 / 账户 / 域名查价(只读,不花钱)")
374
- .action((domain) => remote ? emit(remoteClient.check(remote)) : selfcheck(ctx(), domain));
347
+ .action(() => emit(remoteClient.check(rc())));
375
348
  withProject(program
376
349
  .command("audit")
377
350
  .description("查花钱 / 对外动作的审计记录(检出项目时只看该项目的花钱动作)")
378
- .option("-n, --limit <n>", "返回条数", "20")).action((opts) => emit(remote
379
- ? remoteClient.api.audit(remote, Number(opts.limit))
380
- : domainsCore.auditLog(ctx(), Number(opts.limit))));
351
+ .option("-n, --limit <n>", "返回条数", "20")).action((opts) => emit(remoteClient.api.audit(rc(), Number(opts.limit))));
381
352
  program
382
353
  .command("balance")
383
- .description("查 clize 余额 + 最近流水(托管模式;本地自带-CF 模式无余额概念)")
384
- .action(() => emit(remote ? remoteClient.api.balance(remote) : billingCore.balance(ctx())));
354
+ .description("查 clize 余额 + 流水")
355
+ .action(() => emit(remoteClient.api.balance(rc())));
385
356
  program
386
357
  .command("recharge")
387
- .description("充值 clize 余额(托管模式,最低 $25):自动打开 Stripe 付款页,付完余额自动到账")
358
+ .description("充值 clize 余额(最低 $25):自动打开 Stripe 付款页,付完余额自动到账")
388
359
  .requiredOption("--amount <usd>", "充值金额(美元)")
389
360
  .option("--no-open", "不自动打开浏览器,只返回付款链接(headless / agent 用)")
390
- .action((opts) => {
391
- if (!remote)
392
- return emit(Promise.reject(new Error("充值仅托管模式可用;本地自带-CF 模式费用直接走你的 Cloudflare 账户。")));
393
- return emit((async () => {
394
- const r = (await remoteClient.api.rechargeSession(remote, Number(opts.amount)));
395
- const opened = opts.open !== false && !!r.checkoutUrl;
396
- if (opened)
397
- openBrowser(r.checkoutUrl);
398
- return {
399
- ...r,
400
- opened,
401
- message: opened
402
- ? "已在浏览器打开付款页;没弹出就手动打开上面的 checkoutUrl。付完 `clize balance` 查看。"
403
- : "在浏览器打开 checkoutUrl 完成支付;付完 `clize balance` 查看。",
404
- };
405
- })());
406
- });
361
+ .action((opts) => emit((async () => {
362
+ const r = (await remoteClient.api.rechargeSession(rc(), Number(opts.amount)));
363
+ const opened = opts.open !== false && !!r.checkoutUrl;
364
+ if (opened)
365
+ openBrowser(r.checkoutUrl);
366
+ return {
367
+ ...r,
368
+ opened,
369
+ message: opened
370
+ ? "已在浏览器打开付款页;没弹出就手动打开上面的 checkoutUrl。付完 `clize balance` 查看。"
371
+ : "在浏览器打开 checkoutUrl 完成支付;付完 `clize balance` 查看。",
372
+ };
373
+ })()));
407
374
  const pay = program.command("pay").description("代客收费(托管):连接你的 Stripe,向你的客户收款");
408
375
  pay
409
376
  .command("connect")
410
377
  .description("连接你的 Stripe 账户(开浏览器授权;之后即可向客户收款)")
411
378
  .option("--no-open", "不自动打开浏览器,只返回授权链接(headless / agent 用)")
412
- .action((opts) => {
413
- if (!remote)
414
- return emit(Promise.reject(new Error("代客收费仅托管模式可用;先 clize login。")));
415
- return emit((async () => {
416
- const r = (await remoteClient.api.connectStart(remote));
417
- const opened = opts.open !== false && !!r.authorizeUrl;
418
- if (opened)
419
- openBrowser(r.authorizeUrl);
420
- return {
421
- ...r,
422
- opened,
423
- message: opened
424
- ? "已在浏览器打开 Stripe 授权页;授权后回来跑 `clize pay status`。"
425
- : "在浏览器打开 authorizeUrl 授权;完成后 `clize pay status` 查看。",
426
- };
427
- })());
428
- });
379
+ .action((opts) => emit((async () => {
380
+ const r = (await remoteClient.api.connectStart(rc()));
381
+ const opened = opts.open !== false && !!r.authorizeUrl;
382
+ if (opened)
383
+ openBrowser(r.authorizeUrl);
384
+ return {
385
+ ...r,
386
+ opened,
387
+ message: opened
388
+ ? "已在浏览器打开 Stripe 授权页;授权后回来跑 `clize pay status`。"
389
+ : "在浏览器打开 authorizeUrl 授权;完成后 `clize pay status` 查看。",
390
+ };
391
+ })()));
429
392
  pay
430
393
  .command("status")
431
394
  .description("查看你的收款账户状态(是否已连接 / 可收款)")
432
- .action(() => {
433
- if (!remote)
434
- return emit(Promise.reject(new Error("代客收费仅托管模式可用;先 clize login。")));
435
- return emit(remoteClient.api.connectStatus(remote));
436
- });
395
+ .action(() => emit(remoteClient.api.connectStatus(rc())));
437
396
  pay
438
397
  .command("link")
439
398
  .description("生成一条收款链接,发给你的客户付款(--mode direct=进你的 Stripe / balance=进 clize 余额)")
@@ -441,25 +400,17 @@ pay
441
400
  .option("--mode <mode>", "direct=进你的 Stripe(默认,需先 connect)| balance=进 clize 余额", "direct")
442
401
  .option("--to <ref>", "客户标识(邮箱 / 备注,仅记录展示)")
443
402
  .option("--for <purpose>", "收款事由(发票号 / 订单 / 询价 id)")
444
- .action((opts) => {
445
- if (!remote)
446
- return emit(Promise.reject(new Error("代客收费仅托管模式可用;先 clize login。")));
447
- return emit(remoteClient.api.payLink(remote, {
448
- amountUsd: Number(opts.amount),
449
- mode: opts.mode,
450
- customerRef: opts.to,
451
- purpose: opts.for,
452
- }));
453
- });
403
+ .action((opts) => emit(remoteClient.api.payLink(rc(), {
404
+ amountUsd: Number(opts.amount),
405
+ mode: opts.mode,
406
+ customerRef: opts.to,
407
+ purpose: opts.for,
408
+ })));
454
409
  pay
455
410
  .command("list")
456
411
  .description("列出你开出的收款单(最新在前)")
457
412
  .option("--limit <n>", "条数(默认 20)")
458
- .action((opts) => {
459
- if (!remote)
460
- return emit(Promise.reject(new Error("代客收费仅托管模式可用;先 clize login。")));
461
- return emit(remoteClient.api.payList(remote, opts.limit ? Number(opts.limit) : undefined));
462
- });
413
+ .action((opts) => emit(remoteClient.api.payList(rc(), opts.limit ? Number(opts.limit) : undefined)));
463
414
  const billing = program.command("billing").description("计费:绑卡 / 自动续充(托管模式)");
464
415
  billing
465
416
  .command("card")
@@ -467,12 +418,10 @@ billing
467
418
  .option("--remove", "移除已绑卡(同时关闭自动续充)")
468
419
  .option("--no-open", "不自动打开浏览器,只返回绑卡链接(headless / agent 用)")
469
420
  .action((opts) => {
470
- if (!remote)
471
- return emit(Promise.reject(new Error("自动续充仅托管模式可用;本地自带-CF 模式费用直接走你的 Cloudflare 账户。")));
472
421
  if (opts.remove)
473
- return emit(remoteClient.api.billingCardRemove(remote));
422
+ return emit(remoteClient.api.billingCardRemove(rc()));
474
423
  return emit((async () => {
475
- const r = (await remoteClient.api.billingSetupSession(remote));
424
+ const r = (await remoteClient.api.billingSetupSession(rc()));
476
425
  const opened = opts.open !== false && !!r.checkoutUrl;
477
426
  if (opened)
478
427
  openBrowser(r.checkoutUrl);
@@ -492,8 +441,6 @@ billing
492
441
  .option("--off", "关闭自动续充")
493
442
  .option("--amount <usd>", "每次自动续充额(美元,>=最低充值额)")
494
443
  .action((opts) => {
495
- if (!remote)
496
- return emit(Promise.reject(new Error("自动续充仅托管模式可用;本地自带-CF 模式费用直接走你的 Cloudflare 账户。")));
497
444
  if (opts.on && opts.off)
498
445
  return emit(Promise.reject(new Error("--on 和 --off 不能同时用。")));
499
446
  const patch = {};
@@ -505,21 +452,19 @@ billing
505
452
  patch.amountUsd = Number(opts.amount);
506
453
  if (patch.enabled === undefined && patch.amountUsd === undefined)
507
454
  return emit(Promise.reject(new Error("指定 --on / --off / --amount 之一。")));
508
- return emit(remoteClient.api.billingAutopay(remote, patch));
455
+ return emit(remoteClient.api.billingAutopay(rc(), patch));
509
456
  });
510
457
  withProject(program
511
458
  .command("context [address]")
512
- .description("会话开头 rehydrate:某地址的 身份+知识+操作规约 → 进 agent 的 system prompt(无地址=列地址)")).action((address) => emit(remote ? remoteClient.api.context(remote, address) : addressesCore.addressContext(ctx(), address)));
459
+ .description("会话开头 rehydrate:某地址的 身份+知识+操作规约 → 进 agent 的 system prompt(无地址=列地址)")).action((address) => emit(remoteClient.api.context(rc(), address)));
513
460
  withProject(program
514
461
  .command("claim <slug>")
515
462
  .description("占一个免费 handle <slug>.clize.app(先到先得;默认自动开 support@ 收信;归当前检出项目)")
516
- .option("--no-email", "只占名,不自动开邮箱")).action((slug, opts) => emit(remote
517
- ? remoteClient.api.claim(remote, slug)
518
- : handleCore.claimHandle(ctx(), slug, { provisionEmail: opts.email })));
463
+ .option("--no-email", "只占名,不自动开邮箱")).action((slug) => emit(remoteClient.api.claim(rc(), slug)));
519
464
  program
520
465
  .command("release <slug>")
521
466
  .description("释放自己的免费 handle(删 DNS + 身份,腾出名字;best-effort 拆站点绑定)")
522
- .action((slug) => emit(remote ? remoteClient.api.release(remote, slug) : handleCore.releaseHandle(ctx(), slug)));
467
+ .action((slug) => emit(remoteClient.api.release(rc(), slug)));
523
468
  // ───────── 域名 ─────────
524
469
  const domain = program.command("domain").description("域名:查可注册 / 买 / 接入 / 列表");
525
470
  domain
@@ -528,38 +473,63 @@ domain
528
473
  .option("--tld <tld>", "顶级域(query 不含点时用)", "com")
529
474
  .action((query, opts) => {
530
475
  const domains = query.map((q) => (q.includes(".") ? q : `${q}.${opts.tld}`));
531
- return emit(remote ? remoteClient.api.domainSearch(remote, domains) : domainsCore.searchDomains(ctx(), domains));
476
+ return emit(remoteClient.api.domainSearch(rc(), domains));
532
477
  });
533
478
  domain
534
479
  .command("tlds")
535
- .description("列出 domain search 支持的后缀(其余返回 extension_not_supported_via_api)")
536
- .action(() => emit({
537
- supportedTlds: domainsCore.SUPPORTED_TLDS,
538
- note: "Cloudflare 注册商 beta;.co / .io / .ai / .consulting / .agency 等暂不支持。",
539
- }));
480
+ .description("列出可注册后缀(CF 直注 + Vercel 补充;按 TLD 自动选注册商)")
481
+ .action(() => emit(remoteClient.api.listSupportedTlds(rc())));
540
482
  withProject(domain
541
483
  .command("buy <domain>")
542
484
  .description("注册域名(💰 花钱:必须 --confirm;缺则只返报价;归当前检出项目)")
543
485
  .option("--confirm", "确认花钱注册")
544
- .option("--no-auto-renew", "关闭自动续费(默认开,防过期)")).action((d, opts) => {
545
- if (remote)
546
- return emit(remoteClient.api.domainBuy(remote, d, !!opts.confirm, opts.autoRenew));
547
- if (!opts.confirm) {
548
- return emit(domainsCore.searchDomains(ctx(), [d]).then((quote) => ({
549
- quote,
550
- message: `这是报价。确认后加 --confirm 才会真注册:clize domain buy ${d} --confirm`,
551
- })));
552
- }
553
- return emit(domainsCore.registerDomain(ctx(), d, { autoRenew: opts.autoRenew }));
554
- });
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())));
486
+ .option("--no-auto-renew", "关闭自动续费(默认开,防过期)")).action((d, opts) => emit(remoteClient.api.domainBuy(rc(), d, !!opts.confirm, opts.autoRenew)));
487
+ withProject(domain.command("import <domain>").description("接入已有域名(迁 NS 托管,幂等;新接入归当前检出项目)")).action((d) => emit(remoteClient.api.domainImport(rc(), d)));
488
+ withProject(domain.command("list").description("列出本租户的域名(检出项目时只列该项目)")).action(() => emit(remoteClient.api.domainList(rc())));
489
+ domain
490
+ .command("ns <domain> [nameservers...]")
491
+ .description("查 / 改 NS 委派(不带 ns = 查现状;clize 注册的 Vercel 域可指 CF 或指走、CF 注册锁 CF、外部域引导手动)")
492
+ .action((d, nameservers) => emit(nameservers.length
493
+ ? remoteClient.api.domainSetNameservers(rc(), d, nameservers)
494
+ : remoteClient.api.domainNameservers(rc(), d)));
495
+ // ───────── DNS 记录 ─────────
496
+ const dns = program
497
+ .command("dns")
498
+ .description("DNS 记录:在 NS 已托管 CF 的域上增删改 A/CNAME/MX/TXT…");
499
+ dns
500
+ .command("list <domain>")
501
+ .description("列出该域在 Cloudflare 上的全部 DNS 记录")
502
+ .action((d) => emit(remoteClient.api.dnsList(rc(), d)));
503
+ dns
504
+ .command("set <domain>")
505
+ .description("加 / 改一条记录(同 type+name 已存在则覆盖)")
506
+ .requiredOption("--type <type>", "记录类型:A | AAAA | CNAME | MX | TXT | …")
507
+ .requiredOption("--name <name>", "记录名(@ = apex,或 www / mail.example.com)")
508
+ .requiredOption("--content <content>", "记录值(A=IP,CNAME=目标域,MX=邮件服务器,TXT=文本…)")
509
+ .option("--ttl <sec>", "TTL 秒(默认 auto)")
510
+ .option("--priority <n>", "优先级(MX 用)")
511
+ .option("--proxied", "经 Cloudflare 代理(橙云;A/CNAME 适用)")
512
+ .action((d, opts) => emit(remoteClient.api.dnsSet(rc(), {
513
+ domain: d,
514
+ type: opts.type,
515
+ name: opts.name,
516
+ content: opts.content,
517
+ ttl: opts.ttl ? Number(opts.ttl) : undefined,
518
+ priority: opts.priority ? Number(opts.priority) : undefined,
519
+ proxied: opts.proxied || undefined,
520
+ })));
521
+ dns
522
+ .command("rm <domain>")
523
+ .description("删一条记录(按 type+name 定位;幂等)")
524
+ .requiredOption("--type <type>", "记录类型")
525
+ .requiredOption("--name <name>", "记录名")
526
+ .action((d, opts) => emit(remoteClient.api.dnsRm(rc(), d, opts.type, opts.name)));
557
527
  // ───────── 邮箱 ─────────
558
528
  const email = program.command("email").description("邮箱:配置 / 发信 / 收信");
559
529
  email
560
530
  .command("setup <domain>")
561
531
  .description("配自定义域收发链路(MX / SPF + 发信域名验证)")
562
- .action((d) => emit(remote ? remoteClient.api.emailSetup(remote, d) : emailCore.setupEmail(ctx(), d)));
532
+ .action((d) => emit(remoteClient.api.emailSetup(rc(), d)));
563
533
  withProject(email
564
534
  .command("send")
565
535
  .description("发信(📨 身份对外硬闸:默认只出草稿,加 --confirm 才真发)"))
@@ -576,31 +546,13 @@ withProject(email
576
546
  ? String(opts.to).split(",").map((s) => s.trim())
577
547
  : opts.to;
578
548
  const attachments = loadAttachments(opts.attach);
579
- // 托管模式:from 缺省同样从本地 clize.json 推导(CLI 在用户机器上跑,读得到);
580
- // confirm 透传给后端裁决草稿/真发。
581
- if (remote) {
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>` 绑定项目以自动带出发件人")));
587
- return emit(remoteClient.api.emailSend(remote, {
588
- from: rSender.from,
589
- to,
590
- subject: opts.subject,
591
- text: opts.text,
592
- html: opts.html,
593
- replyTo: rSender.replyTo,
594
- attachments,
595
- confirm: !!opts.confirm,
596
- allowDuplicate: !!opts.allowDuplicate,
597
- }));
598
- }
599
- // from 缺省时从项目(clize.json)带出发件人 + 回信地址
600
- const sender = opts.from ? { from: opts.from, replyTo: undefined } : setupCore.projectSender();
549
+ // from 缺省同样从本地 clize.json 推导(CLI 在用户机器上跑,读得到);confirm 透传给后端裁决草稿/真发。
550
+ const sender = opts.from
551
+ ? { from: opts.from, replyTo: undefined }
552
+ : setupCore.projectSender();
601
553
  if (!sender?.from)
602
554
  return emit(Promise.reject(new Error("需 --from <地址>,或先 `clize init --handle <slug>` 绑定项目以自动带出发件人")));
603
- const msg = {
555
+ return emit(remoteClient.api.emailSend(rc(), {
604
556
  from: sender.from,
605
557
  to,
606
558
  subject: opts.subject,
@@ -608,17 +560,9 @@ withProject(email
608
560
  html: opts.html,
609
561
  replyTo: sender.replyTo,
610
562
  attachments,
611
- };
612
- if (!opts.confirm)
613
- return emit({
614
- // 草稿里把附件 base64 折叠成「文件名 (类型, ~大小)」摘要,不污染 transcript。
615
- draft: {
616
- ...msg,
617
- attachments: attachments?.map((a) => `${a.filename} (${a.contentType ?? "?"}, ~${Math.round((a.content.length * 0.75) / 1024)}KB)`),
618
- },
619
- message: "📨 这是草稿,还没发。把它给用户看、确认后加 --confirm 才真发:clize email send … --confirm",
620
- });
621
- return emit(emailCore.sendEmail(ctx(), msg, { allowDuplicate: !!opts.allowDuplicate }));
563
+ confirm: !!opts.confirm,
564
+ allowDuplicate: !!opts.allowDuplicate,
565
+ }));
622
566
  });
623
567
  email
624
568
  .command("inbox <domain>")
@@ -626,19 +570,13 @@ email
626
570
  .option("-n, --limit <n>", "返回条数", "20")
627
571
  .option("--wait-for <pattern>", "轮询等待直到出现 subject/from/正文 含该串的信(如验证码)")
628
572
  .option("--timeout <sec>", "等待上限秒数(配 --wait-for)", "120")
629
- .action((d, opts) => emit(remote
630
- ? opts.waitFor
631
- ? remoteClient.waitForInboxRemote(remote, d, opts.waitFor, {
632
- timeoutSec: Number(opts.timeout),
633
- })
634
- : remoteClient.api.emailInbox(remote, d, Number(opts.limit))
635
- : opts.waitFor
636
- ? emailCore.waitForInbox(ctx(), d, opts.waitFor, { timeoutSec: Number(opts.timeout) })
637
- : emailCore.checkInbox(ctx(), d, Number(opts.limit))));
573
+ .action((d, opts) => emit(opts.waitFor
574
+ ? remoteClient.waitForInboxRemote(rc(), d, opts.waitFor, { timeoutSec: Number(opts.timeout) })
575
+ : remoteClient.api.emailInbox(rc(), d, Number(opts.limit))));
638
576
  email
639
577
  .command("show <domain> <id>")
640
578
  .description("读单封邮件全文")
641
- .action((d, id) => emit(remote ? remoteClient.api.emailShow(remote, d, id) : emailCore.getMessage(ctx(), d, id)));
579
+ .action((d, id) => emit(remoteClient.api.emailShow(rc(), d, id)));
642
580
  email
643
581
  .command("thread <contactOrDomain> [contact]")
644
582
  .description("读与某联系人的往来。init 过的目录单参即可:email thread <对方地址>;双参 = <收信域> <对方地址>")
@@ -648,25 +586,21 @@ email
648
586
  const contact = b ?? a;
649
587
  if (!domain)
650
588
  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));
589
+ return emit(remoteClient.api.emailThread(rc(), domain, contact));
652
590
  });
653
591
  email
654
592
  .command("inbox-setup <address>")
655
593
  .description("开启 agent 自持收信(Email Worker 接住入站,存收件箱)")
656
594
  .option("--forward <to>", "同时转发到个人邮箱")
657
- .action((address, opts) => emit(remote
658
- ? remoteClient.api.emailInboxSetup(remote, address, opts.forward)
659
- : emailCore.setupInbox(ctx(), address, opts.forward)));
595
+ .action((address, opts) => emit(remoteClient.api.emailInboxSetup(rc(), address, opts.forward)));
660
596
  email
661
597
  .command("route <from> <to>")
662
598
  .description("配转发规则:发往 from 的邮件转发到 to")
663
- .action((from, to) => emit(remote ? remoteClient.api.emailRoute(remote, from, to) : emailCore.routeEmail(ctx(), from, to)));
599
+ .action((from, to) => emit(remoteClient.api.emailRoute(rc(), from, to)));
664
600
  email
665
601
  .command("webhook <address> <url>")
666
602
  .description("配置入站推送:来信时 POST 信封到 url(与 status pull 并存)")
667
- .action((address, url) => emit(remote
668
- ? remoteClient.api.emailWebhook(remote, address, url)
669
- : emailCore.setEmailWebhook(ctx(), address, url)));
603
+ .action((address, url) => emit(remoteClient.api.emailWebhook(rc(), address, url)));
670
604
  // 地址:tag + knowledge(客服等场景;无 build / 角色 / 记忆)
671
605
  const address = email.command("address").description("邮箱地址:加 / 改(tag + knowledge)");
672
606
  address
@@ -674,77 +608,49 @@ address
674
608
  .description("加一个地址(--tag 分类;--knowledge 挂静态知识,作答时进 system prompt)")
675
609
  .option("--tag <t>", "标签,如 客服")
676
610
  .option("--knowledge <path>", "知识文件或目录")
677
- .action((addr, opts) => emit(remote
678
- ? remoteClient.api.emailAddressAdd(remote, {
679
- address: addr,
680
- tag: opts.tag,
681
- knowledge: opts.knowledge ? addressesCore.readKnowledge(opts.knowledge) : undefined,
682
- })
683
- : addressesCore.addAddress(ctx(), addr, { tag: opts.tag, knowledgePath: opts.knowledge })));
611
+ .action((addr, opts) => emit(remoteClient.api.emailAddressAdd(rc(), {
612
+ address: addr,
613
+ tag: opts.tag,
614
+ knowledge: opts.knowledge ? addressesCore.readKnowledge(opts.knowledge) : undefined,
615
+ })));
684
616
  address
685
617
  .command("update <addr>")
686
618
  .description("改地址的 tag / knowledge(未传的不动)")
687
619
  .option("--tag <t>", "标签")
688
620
  .option("--knowledge <path>", "知识文件或目录")
689
- .action((addr, opts) => emit(remote
690
- ? remoteClient.api.emailAddressUpdate(remote, {
691
- address: addr,
692
- tag: opts.tag,
693
- knowledge: opts.knowledge ? addressesCore.readKnowledge(opts.knowledge) : undefined,
694
- })
695
- : addressesCore.updateAddress(ctx(), addr, { tag: opts.tag, knowledgePath: opts.knowledge })));
621
+ .action((addr, opts) => emit(remoteClient.api.emailAddressUpdate(rc(), {
622
+ address: addr,
623
+ tag: opts.tag,
624
+ knowledge: opts.knowledge ? addressesCore.readKnowledge(opts.knowledge) : undefined,
625
+ })));
696
626
  address
697
627
  .command("remove <addr>")
698
628
  .description("删一个地址配置(收件箱历史邮件不动)")
699
- .action((addr) => emit(remote
700
- ? remoteClient.api.emailAddressRemove(remote, addr)
701
- : addressesCore.removeAddress(ctx(), addr)));
702
- withProject(email.command("list").description("列出所有地址 + tag(检出项目时只列该项目域名下的)")).action(() => emit(remote ? remoteClient.api.emailListAddresses(remote) : addressesCore.listAddresses(ctx())));
629
+ .action((addr) => emit(remoteClient.api.emailAddressRemove(rc(), addr)));
630
+ withProject(email.command("list").description("列出所有地址 + tag(检出项目时只列该项目域名下的)")).action(() => emit(remoteClient.api.emailListAddresses(rc())));
703
631
  // ───────── 部署 ─────────
704
632
  withProject(program
705
633
  .command("deploy <path>")
706
- .description("部署:目录 + <slug>.clize.app → 免费 handle 多文件站;或 <名> --html 内联单页")
707
- .option("--domain <domain>", "目标域(<slug>.clize.app 走免费 handle)")
708
- .option("--html <html>", "内联 HTML(单页 worker,不读目录)")
709
- .option("--html-file <file>", "从文件读 HTML")).action((p, opts) => {
634
+ .description("部署:目录 + <slug>.clize.app → 免费 handle 多文件站")
635
+ .option("--domain <domain>", "目标域(<slug>.clize.app 走免费 handle)")).action((p, opts) => {
710
636
  // 缺 --domain 时从 ./clize.json 推导(clize init 绑过的目录免显式传参,与 email send 推 from 对称)。
711
637
  const domain = opts.domain ?? setupCore.projectDomain();
712
- if (remote) {
713
- let dir = false;
714
- try {
715
- dir = fs.statSync(p).isDirectory();
716
- }
717
- catch {
718
- /* 非路径 */
719
- }
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")));
724
- const files = sitesCore.readDirFiles(p);
725
- const bytes = files.reduce((n, f) => n + Math.ceil(f.base64.length * 0.75), 0);
726
- if (bytes > 25 * 1024 * 1024)
727
- return emit(Promise.reject(new Error(`站点约 ${(bytes / 1048576).toFixed(1)}MB,超过托管部署上限 25MB;请精简`)));
728
- return emit(remoteClient.api.deploy(remote, domain, files));
729
- }
730
- let isDir = false;
638
+ let dir = false;
731
639
  try {
732
- isDir = fs.statSync(p).isDirectory();
640
+ dir = fs.statSync(p).isDirectory();
733
641
  }
734
642
  catch {
735
- /* 非路径:仅显式 --html/--html-file 时才允许当 worker 名走内联(否则下面报"目录不存在") */
736
- }
737
- if (isDir) {
738
- if (!domain)
739
- return emit(Promise.reject(new Error("目录(多文件)部署需要 --domain <域名>(需已 claim / buy / import),或先 clize init 绑定本目录")));
740
- return emit(sitesCore.deployToHandle(ctx(), domain, p));
643
+ /* 非路径 */
741
644
  }
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>`)));
746
- const html = opts.htmlFile ? fs.readFileSync(opts.htmlFile, "utf8") : opts.html;
747
- return emit(siteCore.deploySite(ctx(), { workerName: p, html, domain: opts.domain }));
645
+ if (!dir)
646
+ return emit(Promise.reject(new Error(`${p} 不是存在的目录 —— 先生成站点文件再 deploy(托管部署支持目录部署)`)));
647
+ if (!domain)
648
+ return emit(Promise.reject(new Error("缺目标域:clize deploy <目录> --domain <已 claim host>,或先 clize init --handle <slug> 绑定本目录免传 --domain")));
649
+ const files = sitesCore.readDirFiles(p);
650
+ const bytes = files.reduce((n, f) => n + Math.ceil(f.base64.length * 0.75), 0);
651
+ if (bytes > 25 * 1024 * 1024)
652
+ return emit(Promise.reject(new Error(`站点约 ${(bytes / 1048576).toFixed(1)}MB,超过托管部署上限 25MB;请精简`)));
653
+ return emit(remoteClient.api.deploy(rc(), domain, files));
748
654
  });
749
655
  // ───────── 本地预览(serve:deploy 的本地对偶)─────────
750
656
  // 起一个支持 Range + 媒体不灌 no-store 的静态 server —— 预览含 <video> 的站点 / 片子用它,
@@ -774,8 +680,7 @@ program
774
680
  }
775
681
  });
776
682
  // ───────── 生成式媒体(gen:图 / 视频 / 音乐)─────────
777
- // G1 = 本地算力 + 你自带的 model key,与 clize 登录模式无关(remote 登录也在本地跑、写本地盘);
778
- // 托管(clize 代付 + R2 托管)是 G2,所以这里不按 remote 分流。
683
+ // 纯托管:平台代付 + 字节回 CLI 落盘(G2);提交 / 报价 / 轮询全经控制面,CLI 只负责读参考图、落盘成品。
779
684
  const gen = program
780
685
  .command("gen")
781
686
  .description("生成式媒体:你写 prompt,clize 产字节落盘(图 / 视频 / 音乐;蓝图批量 = check + render)");
@@ -790,51 +695,30 @@ withProject(gen
790
695
  .option("--out <path>", "落盘路径(默认 ./clize-assets/<id>_<i>.png)")
791
696
  .option("--confirm", "确认花钱生成")
792
697
  .action((prompt, opts) => {
793
- if (remote) {
794
- if (opts.ref || opts.mask)
795
- return emit(Promise.reject(new Error("托管模式暂不支持参考图 / 蒙版(--ref/--mask);用纯文生图,或切本地模式。")));
796
- return emit(remoteClient.api
797
- .genImage(remote, {
798
- prompt,
799
- model: opts.model,
800
- n: Number(opts.n),
801
- size: opts.size,
802
- confirm: !!opts.confirm,
803
- })
804
- .then((r) => mediaCore.materializeRemote(r, opts.out)));
805
- }
806
- return emit(mediaCore.genImage(ctx(), {
698
+ if (opts.ref || opts.mask)
699
+ return emit(Promise.reject(new Error("托管模式暂不支持参考图 / 蒙版(--ref/--mask);用纯文生图。")));
700
+ return emit(remoteClient.api
701
+ .genImage(rc(), {
807
702
  prompt,
808
703
  model: opts.model,
809
704
  n: Number(opts.n),
810
705
  size: opts.size,
811
- refs: opts.ref
812
- ? String(opts.ref)
813
- .split(",")
814
- .map((s) => s.trim())
815
- : undefined,
816
- mask: opts.mask,
817
- out: opts.out,
818
- }, { confirm: !!opts.confirm }));
706
+ confirm: !!opts.confirm,
707
+ })
708
+ .then((r) => mediaCore.materializeRemote(r, opts.out)));
819
709
  });
820
710
  withProject(gen
821
711
  .command("list")
822
712
  .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)));
713
+ .option("--modality <m>", "只看某模态:image | video | music")).action((opts) => emit(remoteClient.api.genList(rc(), opts.modality)));
824
714
  gen
825
715
  .command("show <id>")
826
716
  .description("看单个生成任务 / 资产全貌")
827
- .action((id) => emit(remote ? remoteClient.api.genShow(remote, id) : mediaCore.getAsset(ctx(), id)));
717
+ .action((id) => emit(remoteClient.api.genShow(rc(), id)));
828
718
  gen
829
719
  .command("budget [set] [usd]")
830
720
  .description("设 / 查生成预批额度:额度内免逐次 --confirm(clize gen budget set 2.00 / clize gen budget)")
831
- .action((setOrUsd, usd) => {
832
- if (remote)
833
- return emit(Promise.reject(new Error("gen budget 托管模式暂未支持(二期);托管下每次生成用 --confirm 逐次确认即可。")));
834
- const raw = setOrUsd === "set" ? usd : setOrUsd;
835
- const amount = raw != null && raw !== "" && !Number.isNaN(Number(raw)) ? Number(raw) : undefined;
836
- return emit(amount != null ? mediaCore.setGenBudget(ctx(), amount) : mediaCore.genBudget(ctx()));
837
- });
721
+ .action(() => emit(Promise.reject(new Error("gen budget 托管模式暂未支持(二期);托管下每次生成用 --confirm 逐次确认即可。"))));
838
722
  withProject(gen
839
723
  .command("video <prompt>")
840
724
  .description("文生视频 / 图生视频(长任务:默认等到完成,--async 后台返 job id)"))
@@ -848,27 +732,16 @@ withProject(gen
848
732
  .option("--timeout <sec>", "默认等待上限(秒)", "300")
849
733
  .option("--confirm", "确认花钱生成")
850
734
  .action((prompt, opts) => {
851
- if (remote) {
852
- // 托管:参考图在 CLI 端读盘转 data URI 上传(首帧排第一,与本地 core 同序)
853
- const paths = [...new Set([opts.fromImage, ...splitRefs(opts.ref)].filter(Boolean))];
854
- return emit((async () => remoteGenAsync(remote, "video", {
855
- prompt,
856
- model: opts.model,
857
- refs: paths.length ? refsToDataUris(paths) : undefined,
858
- durationSec: opts.duration ? Number(opts.duration) : undefined,
859
- aspect: opts.aspect,
860
- confirm: !!opts.confirm,
861
- }, { confirm: !!opts.confirm, async: !!opts.async, out: opts.out, timeout: opts.timeout }))());
862
- }
863
- return emit(mediaCore.genVideo(ctx(), {
735
+ // 托管:参考图在 CLI 端读盘转 data URI 上传(首帧排第一,与后端同序)
736
+ const paths = [...new Set([opts.fromImage, ...splitRefs(opts.ref)].filter(Boolean))];
737
+ return emit((async () => remoteGenAsync(rc(), "video", {
864
738
  prompt,
865
739
  model: opts.model,
866
- fromImage: opts.fromImage,
867
- refs: splitRefs(opts.ref),
740
+ refs: paths.length ? refsToDataUris(paths) : undefined,
868
741
  durationSec: opts.duration ? Number(opts.duration) : undefined,
869
742
  aspect: opts.aspect,
870
- out: opts.out,
871
- }, { confirm: !!opts.confirm, async: !!opts.async, timeoutSec: Number(opts.timeout) }));
743
+ confirm: !!opts.confirm,
744
+ }, { confirm: !!opts.confirm, async: !!opts.async, out: opts.out, timeout: opts.timeout }))());
872
745
  });
873
746
  withProject(gen
874
747
  .command("music <prompt>")
@@ -880,37 +753,24 @@ withProject(gen
880
753
  .option("--async", "后台生成,只返 job id(默认等到完成)")
881
754
  .option("--timeout <sec>", "默认等待上限(秒)", "300")
882
755
  .option("--confirm", "确认花钱生成")
883
- .action((prompt, opts) => {
884
- if (remote) {
885
- return emit(remoteGenAsync(remote, "music", {
886
- prompt,
887
- model: opts.model,
888
- instrumental: !!opts.instrumental,
889
- durationSec: opts.duration ? Number(opts.duration) : undefined,
890
- confirm: !!opts.confirm,
891
- }, { confirm: !!opts.confirm, async: !!opts.async, out: opts.out, timeout: opts.timeout }));
892
- }
893
- return emit(mediaCore.genMusic(ctx(), {
894
- prompt,
895
- model: opts.model,
896
- instrumental: !!opts.instrumental,
897
- durationSec: opts.duration ? Number(opts.duration) : undefined,
898
- out: opts.out,
899
- }, { confirm: !!opts.confirm, async: !!opts.async, timeoutSec: Number(opts.timeout) }));
900
- });
901
- withProject(gen.command("jobs").description("列生成任务(running 置顶;断会话后重连长任务用;检出项目时只列该项目)")).action(() => emit(remote ? remoteClient.api.genJobs(remote) : mediaCore.genJobs(ctx())));
756
+ .action((prompt, opts) => emit(remoteGenAsync(rc(), "music", {
757
+ prompt,
758
+ model: opts.model,
759
+ instrumental: !!opts.instrumental,
760
+ durationSec: opts.duration ? Number(opts.duration) : undefined,
761
+ confirm: !!opts.confirm,
762
+ }, { confirm: !!opts.confirm, async: !!opts.async, out: opts.out, timeout: opts.timeout })));
763
+ withProject(gen.command("jobs").description("列生成任务(running 置顶;断会话后重连长任务用;检出项目时只列该项目)")).action(() => emit(remoteClient.api.genJobs(rc())));
902
764
  gen
903
765
  .command("status <id>")
904
766
  .description("查单任务:running 则向 provider 轮询一次(可能就此完成落盘)")
905
- .action((id) => emit(remote
906
- ? remoteClient.api
907
- .genStatus(remote, id)
908
- .then((r) => mediaCore.materializeRemote(r, undefined))
909
- : mediaCore.genJobStatus(ctx(), id)));
767
+ .action((id) => emit(remoteClient.api
768
+ .genStatus(rc(), id)
769
+ .then((r) => mediaCore.materializeRemote(r, undefined))));
910
770
  gen
911
771
  .command("rm <id>")
912
772
  .description("删任务记录 + 本地文件")
913
- .action((id) => emit(remote ? remoteClient.api.genRm(remote, id) : mediaCore.genRm(ctx(), id)));
773
+ .action((id) => emit(remoteClient.api.genRm(rc(), id)));
914
774
  // ───────── build:作品域(端到端做一个作品;方法 + 校验免费,成品步骤花钱带闸)─────────
915
775
  // taxonomy:gen = 原子生成 + 任务池(引擎),build = 作品编排门面(方法 → 你创作 → 校验 → 成品)。
916
776
  // 第 N 种作品 = build 加子域(方法三件套 + 该作品特有的校验/成品命令),顶级命令面零膨胀。
@@ -926,32 +786,32 @@ function siteBuildCommands() {
926
786
  .argument("<brief...>")
927
787
  .description("开工:返回工作流(7 步 + 反 slop)+ 风格目录 + 针对 brief 的推荐")
928
788
  .option("--project <name>", "项目名")
929
- .action((brief, opts) => hostedBuild((rc) => remoteClient.api.buildSiteStart(rc, brief.join(" "), opts.project)));
789
+ .action((brief, opts) => hostedBuild((rcfg) => remoteClient.api.buildSiteStart(rcfg, brief.join(" "), opts.project)));
930
790
  const recommend = new Command("recommend")
931
791
  .argument("<brief...>")
932
792
  .description("按 brief 推荐:风格 + 调色板 + 字体对 + 反模式")
933
793
  .option("--project <name>", "项目名")
934
- .action((brief, opts) => hostedBuild((rc) => remoteClient.api.buildSiteRecommend(rc, brief.join(" "), opts.project)));
794
+ .action((brief, opts) => hostedBuild((rcfg) => remoteClient.api.buildSiteRecommend(rcfg, brief.join(" "), opts.project)));
935
795
  const list = new Command("list")
936
796
  .description("列出全部风格宪法(slug + 一句话 best-for)")
937
- .action(() => hostedBuild((rc) => remoteClient.api.buildSiteList(rc)));
797
+ .action(() => hostedBuild((rcfg) => remoteClient.api.buildSiteList(rcfg)));
938
798
  const get = new Command("get")
939
799
  .argument("<slug>")
940
800
  .description("取某风格的完整宪法(色 / 字 / 形 / 签名手法)+ imagery 配图契约 + 调色板 / 字体对 + DESIGN.md 模板")
941
- .action((slug) => hostedBuild((rc) => remoteClient.api.buildSiteGet(rc, slug)));
801
+ .action((slug) => hostedBuild((rcfg) => remoteClient.api.buildSiteGet(rcfg, slug)));
942
802
  const stack = new Command("stack")
943
803
  .argument("<stack>")
944
804
  .argument("[query...]")
945
805
  .description("取某技术栈实现指南(React/Next/SwiftUI/Flutter/shadcn… 惯用法 + 行为规则;跨栈 UI 指南)")
946
- .action((stackName, query) => hostedBuild((rc) => remoteClient.api.buildSiteStack(rc, stackName, (query || []).join(" "))));
806
+ .action((stackName, query) => hostedBuild((rcfg) => remoteClient.api.buildSiteStack(rcfg, stackName, (query || []).join(" "))));
947
807
  const search = new Command("search")
948
808
  .argument("<query...>")
949
809
  .description("查 UUPM 库(ux/color/chart/landing/product/typography/icons…;默认自动判域)")
950
810
  .option("--domain <d>", "限定域")
951
- .action((query, opts) => hostedBuild((rc) => remoteClient.api.buildSiteSearch(rc, query.join(" "), opts.domain)));
811
+ .action((query, opts) => hostedBuild((rcfg) => remoteClient.api.buildSiteSearch(rcfg, query.join(" "), opts.domain)));
952
812
  const review = new Command("review")
953
813
  .description("取合并自审清单(anti-slop + DESIGN.md 保真 + UX 行为)")
954
- .action(() => hostedBuild((rc) => remoteClient.api.buildSiteReview(rc)));
814
+ .action(() => hostedBuild((rcfg) => remoteClient.api.buildSiteReview(rcfg)));
955
815
  return [start, recommend, list, get, stack, search, review];
956
816
  }
957
817
  const buildSite = build
@@ -980,34 +840,8 @@ function readBlueprint(file) {
980
840
  throw new Error(`蓝图不是合法 JSON:${e instanceof Error ? e.message : String(e)}`);
981
841
  }
982
842
  }
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
843
  /** render 后端(托管):参考图逐镜转 data URI 上传;confirm=false 探价是后端契约(永不生成)。 */
1010
- function remoteRenderBackend(rc) {
844
+ function remoteRenderBackend(rcfg) {
1011
845
  const body = (p, confirm) => ({
1012
846
  prompt: p.prompt,
1013
847
  model: p.model,
@@ -1018,20 +852,20 @@ function remoteRenderBackend(rc) {
1018
852
  });
1019
853
  return {
1020
854
  quote: async (p) => {
1021
- const r = (await remoteClient.api.genVideo(rc, body(p, false)));
855
+ const r = (await remoteClient.api.genVideo(rcfg, body(p, false)));
1022
856
  const est = r?.quote?.estUsd;
1023
857
  if (typeof est !== "number")
1024
858
  throw new Error("探价未返回 quote(后端响应异常)");
1025
859
  return est;
1026
860
  },
1027
861
  submit: async (p) => {
1028
- const r = (await remoteClient.api.genVideo(rc, body(p, true)));
862
+ const r = (await remoteClient.api.genVideo(rcfg, body(p, true)));
1029
863
  if (!r.id)
1030
864
  throw new Error(`提交未返回任务 id${r.message ? `:${String(r.message)}` : ""}`);
1031
865
  return { id: String(r.id), state: String(r.state ?? "running") };
1032
866
  },
1033
867
  poll: async (id, outPath) => {
1034
- const r = (await remoteClient.api.genStatus(rc, id));
868
+ const r = (await remoteClient.api.genStatus(rcfg, id));
1035
869
  if (r.state === "succeeded") {
1036
870
  await mediaCore.materializeRemote(r, outPath);
1037
871
  return { state: "succeeded" };
@@ -1049,15 +883,15 @@ const buildClip = build
1049
883
  buildClip
1050
884
  .command("start <brief...>")
1051
885
  .description("开工:返回方法(4 阶段 + 蓝图 schema + 反穿帮清单)+ 风格目录 + 针对 brief 的推荐")
1052
- .action((brief) => hostedBuild((rc) => remoteClient.api.buildClipStart(rc, brief.join(" "))));
886
+ .action((brief) => hostedBuild((rcfg) => remoteClient.api.buildClipStart(rcfg, brief.join(" "))));
1053
887
  buildClip
1054
888
  .command("list")
1055
889
  .description("列出全部视频风格(id + 一句话适用场景)")
1056
- .action(() => hostedBuild((rc) => remoteClient.api.buildClipList(rc)));
890
+ .action(() => hostedBuild((rcfg) => remoteClient.api.buildClipList(rcfg)));
1057
891
  buildClip
1058
892
  .command("get <slug>")
1059
893
  .description("取某风格完整包(资产图模板 + 视频视觉词汇 + strict 后端替换行)")
1060
- .action((slug) => hostedBuild((rc) => remoteClient.api.buildClipGet(rc, slug)));
894
+ .action((slug) => hostedBuild((rcfg) => remoteClient.api.buildClipGet(rcfg, slug)));
1061
895
  buildClip
1062
896
  .command("check <blueprint>")
1063
897
  .description("蓝图机械校验(本地、免费、零凭证):防穿帮 lint + 台词覆盖 + 时长公式 + 结构;error 清零才 render")
@@ -1069,7 +903,7 @@ withProject(buildClip
1069
903
  .option("--out-dir <dir>", "输出目录(默认 clize-video/<title>)")
1070
904
  .option("--timeout <sec>", "轮询总时限(秒)", "1800")
1071
905
  .option("--confirm", "确认花钱生成(报价见无 --confirm 的输出)")
1072
- .action((file, opts) => emit(videoCore.renderBlueprint(remote ? remoteRenderBackend(remote) : localRenderBackend(), readBlueprint(file), {
906
+ .action((file, opts) => emit(videoCore.renderBlueprint(remoteRenderBackend(rc()), readBlueprint(file), {
1073
907
  confirm: !!opts.confirm,
1074
908
  candidates: Number(opts.candidates) || 1,
1075
909
  outDir: opts.outDir,