@aswless_854771076/ai_short_studio_cli 0.1.12 → 0.1.13

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 CHANGED
@@ -12,12 +12,19 @@ ai-short-studio project list --json
12
12
  ai-short-studio canvas node types --json
13
13
  ```
14
14
 
15
- 邮箱登录从 stdin 读取密码:
15
+ 邮箱注册和登录从 stdin 读取密码:
16
16
 
17
17
  ```bash
18
+ ai-short-studio auth signup --email user@example.com --password-stdin
18
19
  ai-short-studio auth login --email user@example.com --password-stdin
19
20
  ```
20
21
 
22
+ Agent 代操作时,用户已在当前会话明确提供的密码只能直接写入正在等待的隐藏密码提示,不得复述、拼入 shell 命令、落盘或跨会话保存。
23
+
24
+ 注册响应中的 `confirmationRequired` 表示是否需要先完成邮件验证。Supabase 直接返回会话时,CLI 会立即登录并把 refresh token 保存到系统钥匙串;需要邮件验证时不会写入空凭据,验证后再执行登录。
25
+
26
+ 注册确认链接使用当前 profile 的 `baseUrl` 和 `--locale`,指向同域的 `/{locale}/auth/callback`。API 与网页需保持同域,并在 Supabase Redirect URLs 中允许各环境的 `/**`;Site URL 只作为未显式传入回调时的生产兜底。
27
+
21
28
  CLI 只把 refresh token 保存到系统钥匙串,access token 仅驻留内存并在到期前自动刷新。首次登录会从服务端 bootstrap 取得 Supabase 公共配置,也可用 `VVICAT_SUPABASE_URL`、`VVICAT_SUPABASE_PUBLISHABLE_KEY` 显式覆盖。服务地址和非敏感 profile 写入系统配置目录;切换环境时必须使用独立的 `--profile`,避免复用旧认证。
22
29
 
23
30
  创作前运行 `ai-short-studio preflight --json`。CLI 会通过公开的 `/api/v1/bootstrap` 检查服务连通性、取得 Supabase 公共配置、读取 npm `latest` 稳定版本并默认更新当前全局包,再检查登录态和文本、人物、场景、故事版、视频模型配置。npm registry 暂时不可用时服务端使用配置版本降级。更新后必须按输出重跑;明确不允许全局写入时使用 `--no-update`。
@@ -13,6 +13,7 @@ export interface AuthProvider {
13
13
  loginWithGoogle(): Promise<SessionTokens>;
14
14
  logout(accessToken: string): Promise<void>;
15
15
  refresh(refreshToken: string): Promise<SessionTokens>;
16
+ signup(email: string, password: string, emailRedirectTo: string): Promise<SessionTokens | null>;
16
17
  }
17
18
  export declare class SupabaseAuthProvider implements AuthProvider {
18
19
  private readonly client;
@@ -21,6 +22,7 @@ export declare class SupabaseAuthProvider implements AuthProvider {
21
22
  logout(_accessToken: string): Promise<void>;
22
23
  loginWithGoogle(): Promise<SessionTokens>;
23
24
  refresh(refreshToken: string): Promise<SessionTokens>;
25
+ signup(email: string, password: string, emailRedirectTo: string): Promise<SessionTokens | null>;
24
26
  }
25
27
  export declare class SessionManager {
26
28
  private current;
@@ -38,6 +40,7 @@ export declare class SessionManager {
38
40
  accessToken(): Promise<string>;
39
41
  login(email: string, password: string): Promise<void>;
40
42
  loginWithGoogle(): Promise<void>;
43
+ signup(email: string, password: string, emailRedirectTo: string): Promise<boolean>;
41
44
  refreshAccessToken(): Promise<string>;
42
45
  logout(): Promise<void>;
43
46
  private refresh;
@@ -27,6 +27,16 @@ export class SupabaseAuthProvider {
27
27
  throw error ?? new Error('刷新响应未包含会话');
28
28
  return sessionTokens(data.session);
29
29
  }
30
+ async signup(email, password, emailRedirectTo) {
31
+ const { data, error } = await this.client.auth.signUp({
32
+ email,
33
+ password,
34
+ options: { emailRedirectTo },
35
+ });
36
+ if (error || !data.user)
37
+ throw error ?? new Error('注册响应未包含用户');
38
+ return data.session ? sessionTokens(data.session) : null;
39
+ }
30
40
  }
31
41
  function sessionTokens(session) {
32
42
  if (!session.refresh_token || !session.expires_at)
@@ -70,6 +80,13 @@ export class SessionManager {
70
80
  this.current = await this.provider.loginWithGoogle();
71
81
  await this.credentials.set(this.profile, this.current.refreshToken);
72
82
  }
83
+ async signup(email, password, emailRedirectTo) {
84
+ this.current = await this.provider.signup(email, password, emailRedirectTo);
85
+ if (!this.current)
86
+ return false;
87
+ await this.credentials.set(this.profile, this.current.refreshToken);
88
+ return true;
89
+ }
73
90
  async refreshAccessToken() {
74
91
  if (this.environment.VVICAT_ACCESS_TOKEN)
75
92
  return this.environment.VVICAT_ACCESS_TOKEN;
@@ -1,7 +1,7 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { BaseCommand } from '../../base-command.js';
3
3
  import { createRuntime } from '../../runtime.js';
4
- import { promptLine, promptPassword } from '../../terminal/prompt.js';
4
+ import { promptLine, promptPassword, readStdinText } from '../../terminal/prompt.js';
5
5
  export default class AuthLogin extends BaseCommand {
6
6
  static description = '登录并将 refresh token 保存到系统钥匙串';
7
7
  static flags = {
@@ -20,15 +20,9 @@ export default class AuthLogin extends BaseCommand {
20
20
  }
21
21
  else {
22
22
  const email = flags.email || await promptLine('邮箱: ');
23
- const password = flags['password-stdin'] ? await readStdin() : await promptPassword('密码: ');
23
+ const password = flags['password-stdin'] ? await readStdinText() : await promptPassword('密码: ');
24
24
  await runtime.session.login(email, password.trimEnd());
25
25
  }
26
26
  this.print({ authenticated: true, profile: runtime.profile.name });
27
27
  }
28
28
  }
29
- async function readStdin() {
30
- const chunks = [];
31
- for await (const chunk of process.stdin)
32
- chunks.push(Buffer.from(chunk));
33
- return Buffer.concat(chunks).toString('utf8');
34
- }
@@ -0,0 +1,16 @@
1
+ import { BaseCommand } from '../../base-command.js';
2
+ export declare function buildSignupRedirectUrl(baseUrl: string, locale: string): string;
3
+ export default class AuthSignup extends BaseCommand {
4
+ static description: string;
5
+ static flags: {
6
+ email: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ 'password-stdin': import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ 'supabase-publishable-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ 'supabase-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ locale: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ };
15
+ run(): Promise<void>;
16
+ }
@@ -0,0 +1,35 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../base-command.js';
3
+ import { createRuntime } from '../../runtime.js';
4
+ import { promptLine, promptPassword, readStdinText } from '../../terminal/prompt.js';
5
+ export function buildSignupRedirectUrl(baseUrl, locale) {
6
+ return new URL(`/${locale}/auth/callback`, baseUrl).toString();
7
+ }
8
+ export default class AuthSignup extends BaseCommand {
9
+ static description = '使用邮箱和密码注册账号';
10
+ static flags = {
11
+ ...BaseCommand.baseFlags,
12
+ email: Flags.string({ description: '注册邮箱' }),
13
+ 'password-stdin': Flags.boolean({ description: '从标准输入读取密码', default: false }),
14
+ 'supabase-publishable-key': Flags.string({ description: 'Supabase publishable key', env: 'VVICAT_SUPABASE_PUBLISHABLE_KEY' }),
15
+ 'supabase-url': Flags.string({ description: 'Supabase URL', env: 'VVICAT_SUPABASE_URL' }),
16
+ };
17
+ async run() {
18
+ const { flags } = await this.parse(AuthSignup);
19
+ const runtime = await createRuntime(flags);
20
+ const email = (flags.email || await promptLine('邮箱: ')).trim();
21
+ const password = flags['password-stdin'] ? await readStdinText() : await promptPassword('密码: ');
22
+ const normalizedPassword = password.trimEnd();
23
+ if (!email)
24
+ throw new Error('邮箱不能为空');
25
+ if (!normalizedPassword)
26
+ throw new Error('密码不能为空');
27
+ const authenticated = await runtime.session.signup(email, normalizedPassword, buildSignupRedirectUrl(runtime.profile.baseUrl, flags.locale));
28
+ this.print({
29
+ registrationAccepted: true,
30
+ authenticated,
31
+ confirmationRequired: !authenticated,
32
+ profile: runtime.profile.name,
33
+ });
34
+ }
35
+ }
@@ -1,4 +1,5 @@
1
1
  import { BaseCommand } from '../../../base-command.js';
2
+ export declare function mimeTypeForUpload(filename: string): string;
2
3
  export default class CanvasAssetUpload extends BaseCommand {
3
4
  static args: {
4
5
  file: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
@@ -1,10 +1,28 @@
1
- import { basename } from 'node:path';
1
+ import { basename, extname } from 'node:path';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { Args } from '@oclif/core';
4
4
  import { BaseCommand } from '../../../base-command.js';
5
5
  import { commandApi, projectFlag } from '../../../command-helpers.js';
6
+ const MIME_BY_EXTENSION = {
7
+ '.png': 'image/png',
8
+ '.jpg': 'image/jpeg',
9
+ '.jpeg': 'image/jpeg',
10
+ '.webp': 'image/webp',
11
+ '.gif': 'image/gif',
12
+ '.mp4': 'video/mp4',
13
+ '.mov': 'video/quicktime',
14
+ '.webm': 'video/webm',
15
+ '.mp3': 'audio/mpeg',
16
+ '.wav': 'audio/wav',
17
+ '.m4a': 'audio/mp4',
18
+ '.aac': 'audio/aac',
19
+ '.ogg': 'audio/ogg',
20
+ };
21
+ export function mimeTypeForUpload(filename) {
22
+ return MIME_BY_EXTENSION[extname(filename).toLowerCase()] || 'application/octet-stream';
23
+ }
6
24
  export default class CanvasAssetUpload extends BaseCommand {
7
25
  static args = { file: Args.string({ required: true }) };
8
26
  static flags = { ...BaseCommand.baseFlags, project: projectFlag };
9
- async run() { const { args, flags } = await this.parse(CanvasAssetUpload); const bytes = await readFile(args.file); const form = new FormData(); form.set('file', new Blob([bytes]), basename(args.file)); const { api } = await commandApi(flags); this.print(await api.uploadAsset(flags.project, form)); }
27
+ async run() { const { args, flags } = await this.parse(CanvasAssetUpload); const bytes = await readFile(args.file); const form = new FormData(); form.set('file', new Blob([bytes], { type: mimeTypeForUpload(args.file) }), basename(args.file)); const { api } = await commandApi(flags); this.print(await api.uploadAsset(flags.project, form)); }
10
28
  }
@@ -0,0 +1,13 @@
1
+ import { BaseCommand } from '../../../base-command.js';
2
+ export default class ProjectProviderGet extends BaseCommand {
3
+ static args: {
4
+ projectId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static flags: {
7
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ locale: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
@@ -0,0 +1,12 @@
1
+ import { Args } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base-command.js';
3
+ import { commandApi } from '../../../command-helpers.js';
4
+ export default class ProjectProviderGet extends BaseCommand {
5
+ static args = { projectId: Args.string({ required: true }) };
6
+ static flags = BaseCommand.baseFlags;
7
+ async run() {
8
+ const { args, flags } = await this.parse(ProjectProviderGet);
9
+ const { api } = await commandApi(flags);
10
+ this.print(await api.projectMaterialProvider(args.projectId));
11
+ }
12
+ }
@@ -0,0 +1,14 @@
1
+ import { BaseCommand } from '../../../base-command.js';
2
+ export default class ProjectProviderSet extends BaseCommand {
3
+ static args: {
4
+ projectId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static flags: {
7
+ file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ locale: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ };
13
+ run(): Promise<void>;
14
+ }
@@ -0,0 +1,18 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base-command.js';
3
+ import { commandApi, readJsonInput } from '../../../command-helpers.js';
4
+ export default class ProjectProviderSet extends BaseCommand {
5
+ static args = { projectId: Args.string({ required: true }) };
6
+ static flags = { ...BaseCommand.baseFlags, file: Flags.string({ description: '配置 JSON;省略时读取 stdin' }) };
7
+ async run() {
8
+ const { args, flags } = await this.parse(ProjectProviderSet);
9
+ const input = await readJsonInput(flags.file);
10
+ if (typeof input.enabled !== 'boolean')
11
+ this.error('配置必须包含 boolean 类型的 enabled');
12
+ const { api } = await commandApi(flags);
13
+ this.print(await api.updateProjectMaterialProvider(args.projectId, {
14
+ enabled: input.enabled,
15
+ ...(typeof input.apiKey === 'string' ? { apiKey: input.apiKey } : {}),
16
+ }));
17
+ }
18
+ }
@@ -0,0 +1,13 @@
1
+ import { BaseCommand } from '../../../base-command.js';
2
+ export default class ProjectProviderTest extends BaseCommand {
3
+ static args: {
4
+ projectId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static flags: {
7
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ locale: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
@@ -0,0 +1,12 @@
1
+ import { Args } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base-command.js';
3
+ import { commandApi } from '../../../command-helpers.js';
4
+ export default class ProjectProviderTest extends BaseCommand {
5
+ static args = { projectId: Args.string({ required: true }) };
6
+ static flags = BaseCommand.baseFlags;
7
+ async run() {
8
+ const { args, flags } = await this.parse(ProjectProviderTest);
9
+ const { api } = await commandApi(flags);
10
+ this.print(await api.testProjectMaterialProvider(args.projectId));
11
+ }
12
+ }
@@ -37,6 +37,12 @@ export declare class VvicatApi {
37
37
  name?: string;
38
38
  description?: string | null;
39
39
  }): Promise<JsonObject>;
40
+ projectMaterialProvider(projectId: string): Promise<JsonObject>;
41
+ updateProjectMaterialProvider(projectId: string, input: {
42
+ apiKey?: string;
43
+ enabled: boolean;
44
+ }): Promise<JsonObject>;
45
+ testProjectMaterialProvider(projectId: string): Promise<JsonObject>;
40
46
  deleteProject(projectId: string): Promise<JsonObject>;
41
47
  canvas(projectId: string): Promise<CanvasDto>;
42
48
  listNodes(projectId: string): Promise<CanvasNode[]>;
@@ -19,6 +19,15 @@ export class VvicatApi {
19
19
  updateProject(projectId, input) {
20
20
  return this.client.request(`/api/projects/${projectId}`, { method: 'PATCH', body: input });
21
21
  }
22
+ projectMaterialProvider(projectId) {
23
+ return this.client.request(`/api/projects/${projectId}/material-providers/pexels`);
24
+ }
25
+ updateProjectMaterialProvider(projectId, input) {
26
+ return this.client.request(`/api/projects/${projectId}/material-providers/pexels`, { method: 'PUT', body: input });
27
+ }
28
+ testProjectMaterialProvider(projectId) {
29
+ return this.client.request(`/api/projects/${projectId}/material-providers/pexels`, { method: 'POST' });
30
+ }
22
31
  deleteProject(projectId) {
23
32
  return this.client.request(`/api/projects/${projectId}`, { method: 'DELETE' });
24
33
  }
@@ -1,2 +1,3 @@
1
1
  export declare function promptLine(label: string): Promise<string>;
2
2
  export declare function promptPassword(label: string): Promise<string>;
3
+ export declare function readStdinText(): Promise<string>;
@@ -33,17 +33,23 @@ export async function promptPassword(label) {
33
33
  resolve(value);
34
34
  };
35
35
  const onData = (chunk) => {
36
- const text = chunk.toString('utf8');
37
- if (text === '\u0003')
38
- return finish(new Error('已取消'));
39
- if (text === '\r' || text === '\n')
40
- return finish();
41
- if (text === '\u007f') {
42
- value = value.slice(0, -1);
43
- return;
36
+ for (const text of chunk.toString('utf8')) {
37
+ if (text === '\u0003')
38
+ return finish(new Error('已取消'));
39
+ if (text === '\r' || text === '\n')
40
+ return finish();
41
+ if (text === '\u007f')
42
+ value = value.slice(0, -1);
43
+ else
44
+ value += text;
44
45
  }
45
- value += text;
46
46
  };
47
47
  process.stdin.on('data', onData);
48
48
  });
49
49
  }
50
+ export async function readStdinText() {
51
+ const chunks = [];
52
+ for await (const chunk of process.stdin)
53
+ chunks.push(Buffer.from(chunk));
54
+ return Buffer.concat(chunks).toString('utf8');
55
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aswless_854771076/ai_short_studio_cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "VVICAT 无限画布项目命令行工具",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -7,29 +7,41 @@ description: Use when an agent needs to inspect or operate VVICAT infinite-canva
7
7
 
8
8
  ## 核心原则
9
9
 
10
- 把 `ai-short-studio` 当作机器可读的远程控制接口。创作全程只要目标、素材含义、平台、语言、画幅、时长、风格、人物、场景、道具、模型、节点 schema、候选版本、付费范围或覆盖影响存在未知、缺失或矛盾,就合并列出最少必要问题并询问用户;得到答复前只做只读检查,不用示例值、默认值或自行推断替用户做创作决定。
10
+ 把 `ai-short-studio` 当作机器可读的远程控制接口。创作全程只要目标、素材含义、平台、语言、画幅、时长、风格、人物、场景、道具、模型、节点 schema、候选版本、付费范围或覆盖影响存在未知、缺失或矛盾,就合并列出最少必要问题并询问用户;得到答复前只做只读检查,不用示例值、默认值或自行推断替用户做创作决定。新建或继续任何无限画布项目时,项目配置是创作节点之前的硬门禁:配置写入并回读验证通过前,不得创建或执行任何创作节点。
11
11
 
12
12
  ## 初始化检查
13
13
 
14
14
  创建项目或修改任何资源前,按顺序执行:
15
15
 
16
16
  1. 运行 `ai-short-studio preflight --json`。它通过 API base URL 检查连通性和 CLI 最新稳定版,默认发现更新就更新当前全局包;`restartRequired` 为真时必须重跑。连不上时提醒用户检查 `--base-url`、`VVICAT_BASE_URL` 或 profile,不猜地址。
17
- 2. 未登录时执行 `ai-short-studio auth login --google`;CLI 会从服务端取得 Supabase 公共配置。登录后重跑 preflight。`ready` 为 false 时,把 `missing` 一次性告知用户并询问如何补齐;新配置模型首选公司的 [StarRouter](https://starrouter.io/),账号开通联系邱茂。
18
- 3. 运行 `ai-short-studio skill status --target codex --json`;非 current 时询问后更新,用户修改或非托管目录不得擅自 `--force` 覆盖。
17
+ 2. 未登录且已有账号时执行 `ai-short-studio auth login --google` 或邮箱登录;没有账号且用户明确要求注册时,先核对 profile 的 `baseUrl` 确实指向目标环境,再使用 `auth signup --email <email> --password-stdin`。CLI 会从服务端取得 Supabase 公共配置,并把同域的 `/{locale}/auth/callback` 显式设为注册确认回调。注册返回 `confirmationRequired: true` 时,等待用户完成邮件验证后再登录。登录后重跑 preflight。`ready` 为 false 时,把 `missing` 一次性告知用户并询问如何补齐;新配置模型首选公司的 [StarRouter](https://starrouter.io/),账号开通联系邱茂。
18
+ 3. 运行账户级只读命令 `ai-short-studio config get --json`,取得账户默认模型及可用 provider;此步骤只用于计算继承后的有效模型,不得用 `config set` 改账户配置来完成项目配置。
19
19
  4. 运行 `ai-short-studio canvas node types --json`,按实时 `inputSchema`、`outputSchema`、`configSchema` 和 `defaultConfig` 确定能力与必填项。
20
- 5. 运行 `project list/get` `canvas get`,确认 `INFINITE_CANVAS`、目标项目、现有节点、资产 selected version 和本次创作参数。任何缺口仍按核心原则询问。
20
+ 5. 运行 `project list` 判断目标是新项目还是既有项目。新项目按“确认名称和用途 → `project create` → `project get` + `canvas get`”继续;既有项目直接运行 `project get` + `canvas get`。这些项目与画布读取只用于确认 `INFINITE_CANVAS`、当前 `settings`、现有节点、资产 selected version 和本次创作参数;不得要求尚未创建的新项目先执行 `project get` 或 `canvas get`。任何缺口仍按核心原则询问。
21
+ 6. 运行 `ai-short-studio skill status --target codex --json`;非 current 时询问后更新,用户修改或非托管目录不得擅自 `--force` 覆盖。
22
+ 7. Meme 流程需要外部背景时,运行 `project provider get <projectId> --json` 检查 Pexels 状态;只在用户明确提供 Key 并授权写入后,才通过安全 stdin 执行 `project provider set`,随后运行 `project provider test`。
23
+
24
+ ## 项目配置门禁
25
+
26
+ 1. 向用户展示项目名称与用途,并逐项确认以下画布设置的“当前值、继承后的有效值、建议值、来源/继承关系”:`aspectRatio`、`artStyle`、`visualBible`、`storyboardImageType`、`analysisModel`、`imageModel`、`editModel`、`videoModel`、`audioModel`。模型既可写为项目覆盖,也可由用户明确确认继承账户默认;无论哪种方式,都必须列明每个所需模型最终生效的具体模型,并确认该模型当前可用。缺失、冲突或继承结果不明确时先一次性询问。
27
+ 2. `storyboardGridSize` 不是统一创作默认值,只在无法按节点设置 `gridSize` 的异常回退场景中展示并取得用户确认。正常流程仍按镜头复杂度逐个设置节点 `gridSize`,不得因项目 settings 已有该字段而套用统一格数。
28
+ 3. 新项目按初始化顺序确认名称和用途、创建项目并取得项目与画布;既有项目开始创作前也必须完成本门禁,名称或用途需要更新时只用 `project update` 修改用户确认的项目字段。用途可记录在项目描述中,但描述不构成画布 settings。不得用账户级 `config set`、项目 `description` 或节点 `prompt`/`config` 代替画布 `settings`。
29
+ 4. 配置写入窗口内必须避免其他客户端或用户并发修改项目 settings;无法确保时停在配置阶段,请用户协调后再继续。执行 `canvas apply` 前立即再运行一次 `canvas get`,以这次读取的完整 settings 为基线重新合并,仅改变用户确认字段并原样保留其余字段;如果相对用户刚确认的快照有任何字段变化,先展示变化字段并重新确认,再用一次 `canvas apply` 写入完整目标 settings。
30
+ 5. 写入后立即再次执行 `canvas get`,把回读的完整 settings(包括应原样保留的字段)与本次写入的完整目标 settings 做整体比较。只有两者完全一致、继承后的所需模型明确且可用时,才可创建或执行创作节点。写入或回读失败、字段被权限规则忽略、结果不一致时,停在配置阶段,不触发任何付费任务。
21
31
 
22
32
  ## 标准创作流程
23
33
 
24
34
  1. 明确原始故事或剧本、目标受众与平台、语言、画幅、总时长与镜头时长、风格参考、人物/场景/道具约束,以及交付是否包含剧本、设定、分镜图、视频和音频;缺项先问。
25
- 2. 完成 preflight、节点目录、模型配置与项目检查。新建项目先确认名称和用途;既有项目优先复用已确认的节点与 selected version。
26
- 3. 原文需要改编时执行 `novel-to-script`。分镜拆解前必须先完成人物、场景、道具等本次故事涉及的素材生成;每个生成任务都 `task wait` 到成功终态,并让用户确认后为每项素材设置 selected version。任一素材未生成成功、未确认或没有 selected version 时,禁止执行 `storyboard-breakdown`,先询问用户并补齐。
35
+ 2. 完成 preflight、账户配置读取和节点目录检查后,新项目按“project list → 确认名称/用途 → project create → project get + canvas get”继续,既有项目按“project list/get → canvas get”继续;再按“项目配置门禁”逐项确认、写入并回读完整画布 settings。两种项目都必须在任何创作节点之前通过门禁;既有项目只修改用户确认字段,并优先复用已确认的节点与 selected version。
36
+ 3. 原文需要改编时执行 `novel-to-script`。分镜拆解前必须先完成人物、场景、道具等本次故事涉及的素材生成;互不依赖的素材节点应同批执行并收集真实任务 ID,再并发 `task wait`,不得等待一个完成后才提交下一个;存在上游依赖时按依赖顺序分批执行。每个任务都必须到成功终态,并让用户确认后为每项素材设置 selected version。任一素材未生成成功、未确认或没有 selected version 时,禁止执行 `storyboard-breakdown`,先询问用户并补齐。
27
37
  4. 按实时节点 schema,把已确认的剧本节点,以及每个人物、场景、道具等相关素材节点,逐项连接到 `storyboard-breakdown` 对应输入 handle(通常为 `script`、`characters`、`locations`、`props`)。执行前重新读取画布和连线,确认本次故事涉及的素材节点无遗漏;即使服务端把素材输入标为可选,标准创作流程也不得省略。任一对应连线缺失时禁止拆解,先补线;素材范围不清楚时先询问用户。全部门禁满足后才执行并等待。成功后刷新画布,读取服务端自动创建的 `storyboard-shot` 和 `storyboard-image`,不得重复手工创建。
28
- 5. 生成分镜图前,用 `canvas asset get/list` 取得人物、场景、道具等已选版本,必要时下载,并把实际图片或可访问预览展示给用户;只提供资产 ID、文件路径或文字说明不算展示。用户明确确认素材与选版后,优先建议选择 `imageLayout: storyboard`:它适合连续叙事审阅并保留格序、镜号、景别、机位和运镜。也要说明 `grid` 与 `single` 可选;最终由用户确认布局、镜头范围、参考资产、模型与费用范围。未展示或未确认时禁止生成分镜图。
38
+ 5. 生成分镜图前,用 `canvas asset get/list` 取得人物、场景、道具等已选版本,必要时下载,并把实际图片或可访问预览展示给用户;只提供资产 ID、文件路径或文字说明不算展示。用户明确确认素材与选版后,逐镜头评估需要独立呈现的关键状态(起势、动作或表情变化、人物与道具关系变化、机位或视角变化、空间调度和结果),把 `gridSize` 设为实时 schema 允许值中能够完整表达这些状态的最小值:单一静态状态用 1 格,一般连续变化优先 4 格,多阶段动作或复杂调度再用 6/9 格,只有确实存在更多不可合并状态时才用 12 格以上;不得全项目固定同一格数,也不得只按镜头时长决定。向用户列出“镜号、复杂度依据、建议格数”并确认;连续叙事优先建议 `imageLayout: storyboard`,同时说明 `grid` 与 `single` 可选,最终由用户确认布局、镜头范围、参考资产、模型与费用范围。未展示或未确认时禁止生成分镜图。
29
39
  6. 执行分镜图后检查任务终态、候选资产与 selected version,并把实际分镜图或可访问预览展示给用户。用户明确确认分镜图以及需要制作的视频镜头后,才能刷新画布并复用服务端自动创建的 `storyboard-video-prompt`;提示词再经用户确认后执行自动创建的 `video-generate`,音频按交付范围执行。未展示或未确认分镜图时禁止生成视频。
30
- 7. 每次写入前重新读取画布版本;优先用专用 node/edge 命令,批量变更才用 `canvas apply`。每次执行都读取真实任务 ID 并 `task wait`,不用固定 sleep。
40
+ 7. 配置门禁固定用一次 `canvas apply` 写入合并后的 settings;通过门禁后的节点与连线写入,仍需在每次写入前重新读取画布版本,优先用专用 node/edge 命令,批量变更才用 `canvas apply`。每次执行都读取真实任务 ID 并 `task wait`,不用固定 sleep。
31
41
  8. 交付前确认 selected version 与下载目录,记录项目、节点、任务和资产 ID、失败项与未决项。临时项目在所有结束路径删除;既有项目绝不擅自删除。
32
42
 
43
+ Meme 推荐链路为 `text.text → meme-scene-analysis.story`,运行后把自动生成的计划资源连接到 `meme-material-search.plan`,再把素材包资源连接到 `cat-meme-video.materials`;原故事仍连接 `cat-meme-video.story`,计划资源可同时连接 `cat-meme-video.plan`。每一步完成后刷新画布,使用服务端自动创建的资源节点,不能伪造或重复创建资源。`cat-meme-video` 的 `config` 需按实时 schema 确认 `aspectRatio`(`1:1`、`9:16` 或 `16:9`)与 `resolution`(`720p` 或 `1080p`);它们是节点输出参数,不替代项目配置门禁。
44
+
33
45
  详细命令按需读取 [references/commands.md](references/commands.md),或运行 `ai-short-studio <topic> --help`。
34
46
 
35
47
  ## AI 剪辑联动
@@ -42,9 +54,13 @@ description: Use when an agent needs to inspect or operate VVICAT infinite-canva
42
54
 
43
55
  - 不把密码、access token、refresh token 或 API Key 放入参数、日志、Skill 或仓库。
44
56
  - 密码登录使用 `printf`/管道以外的安全 stdin 来源;自动化环境使用秘密管理器注入 stdin。
57
+ - 邮箱注册同样只通过交互式隐藏输入或安全 stdin 传递密码,不把密码放入命令参数、日志或仓库。
58
+ - 用户已在当前会话明确提供密码时,可将其直接写入正在等待的交互式隐藏密码提示;不得复述密码、拼入 shell 命令、写入临时文件或跨会话保存。
59
+ - 注册环境通过 profile `baseUrl` 区分;Supabase Redirect URLs 应预先允许各环境,禁止为单次本地注册临时修改全局 Site URL。
45
60
  - `config get` 只应返回 `hasApiKey`,不得依赖或要求服务端回显密钥。
61
+ - `project provider get` 只返回配置状态。`project provider set` 的 JSON 必须来自安全 stdin 或不入库的临时秘密文件,禁止把 Key 写进命令参数、仓库文件或会回显的 shell 历史。
46
62
  - 删除用户既有项目、覆盖完整画布或修改模型配置前,先明确目标和影响;没有授权就停在读取或生成变更计划。
47
- - 对版本冲突重新读取画布后重算变更,不盲目重试旧的 `expectedVersion`。
63
+ - `canvas apply` 提交的版本字段不能替代项目 settings 的排他写入窗口;无法协调其他客户端或用户暂停修改时,停在配置阶段。
48
64
 
49
65
  ## 示例
50
66
 
@@ -61,10 +77,14 @@ ai-short-studio canvas node list --project "$PROJECT_ID" --json
61
77
 
62
78
  - 节点类型写死:先读 `canvas node types`,服务端目录才是事实来源。
63
79
  - 配置不全仍继续:先做初始化检查;模型、项目或用户意图有缺口时先询问。
80
+ - 把项目配置写到错误位置:画幅、画风、视觉圣经、故事版类型和模型必须通过 `canvas apply` 写入项目 settings;账户级 `config set`、项目描述或节点 prompt/config 都不能替代。
81
+ - 写完 settings 就创建节点:必须 `canvas get` 回读完全一致,并确认继承后的最终有效模型可用;失败或冲突时停在配置阶段,不触发付费任务。
64
82
  - CLI 有更新仍用旧进程继续:更新后必须重跑 preflight。
83
+ - 注册成功就假定已登录:`confirmationRequired: true` 表示需要先完成邮件验证,再执行 `auth login`。
65
84
  - 自动创建的下游节点重复添加:任务完成后先刷新画布并复用真实节点 ID。
66
85
  - 素材未完成或未连线就拆分镜:人物、场景、道具等相关素材必须任务成功、已确认 selected version,并逐项连接到 `storyboard-breakdown` 对应输入后才能执行。
67
86
  - 未展示产物就继续:生成分镜图前展示并确认素材,生成视频前展示并确认分镜图;只报告 ID 或路径不能代替预览与用户确认。
87
+ - 所有镜头沿用同一故事版格数:必须按每个镜头的关键状态与调度复杂度分别设置最小够用的 `gridSize`,并在执行前让用户确认。
68
88
  - 涉及剪辑却自行设计:时间线、字幕、转场、音频编排或渲染必须先使用 `remotion-best-practices`。
69
89
  - 只检查任务创建响应:必须 `task wait` 并检查终态和错误字段。
70
90
  - 下载列表中的任意 URL:先确认资产的 selected version,再下载。
@@ -26,6 +26,7 @@ ai-short-studio skill update --target codex --json
26
26
  ## 认证
27
27
 
28
28
  ```bash
29
+ ai-short-studio auth signup --email user@example.com --password-stdin --base-url <url> --supabase-url <url> --supabase-publishable-key <key>
29
30
  ai-short-studio auth login --google --base-url <url> --supabase-url <url> --supabase-publishable-key <key>
30
31
  ai-short-studio auth login --email user@example.com --password-stdin --base-url <url> --supabase-url <url> --supabase-publishable-key <key>
31
32
  ai-short-studio auth status --json
@@ -33,6 +34,8 @@ ai-short-studio auth logout
33
34
  ```
34
35
 
35
36
  Google 登录会打开浏览器并通过本机环回地址完成 PKCE 回调。
37
+ 邮箱注册的密码只从隐藏输入或 stdin 读取。`confirmationRequired: true` 时需先完成邮件验证再登录;为 `false` 时 CLI 已保存会话。
38
+ 邮箱注册确认链接由当前 profile 的 `baseUrl` 和 `--locale` 生成,格式为同域的 `/{locale}/auth/callback`。Supabase Redirect URLs 必须允许对应环境的 `/**`,无需为本地或测试注册修改全局 Site URL。
36
39
 
37
40
  ## 项目
38
41
 
@@ -46,6 +49,16 @@ ai-short-studio project delete <projectId> --yes --json
46
49
 
47
50
  创建操作固定写入 `INFINITE_CANVAS`;列表也只返回该类型。
48
51
 
52
+ ### 项目素材供应商
53
+
54
+ ```bash
55
+ ai-short-studio project provider get <projectId> --json
56
+ ai-short-studio project provider set <projectId> --json
57
+ ai-short-studio project provider test <projectId> --json
58
+ ```
59
+
60
+ `provider set` 从 stdin 读取 `{ "enabled": true, "apiKey": "..." }`,也可用 `--file`。真实 Key 只能来自安全 stdin 或秘密管理器;不得把它写入命令参数、仓库文件或文档。省略 `apiKey` 会保留已保存的 Key,只更新启用状态。读取命令只返回遮罩状态。
61
+
49
62
  ## 画布和节点
50
63
 
51
64
  ```bash
@@ -60,7 +73,9 @@ ai-short-studio canvas node delete <nodeId> --project <projectId> --yes --json
60
73
  ai-short-studio canvas node run <nodeId> --project <projectId> --json
61
74
  ```
62
75
 
63
- `canvas apply` 的 JSON 可以通过 `--file` 读取;省略文件时从 stdin 读取。提交批量变更时携带最近读取到的 `canvasId` 与 `expectedVersion`。
76
+ `canvas apply` 的 JSON 可以通过 `--file` 读取;省略文件时从 stdin 读取。CLI 会在提交前读取当前画布版本并携带 `canvasId` 与 `expectedVersion`,但这不为 settings 提供可靠的条件写保护。项目配置补丁使用顶层 `settings`,其值是从 `canvas get` 当前 settings 合并用户确认字段后得到的完整对象,不是只含变更字段的局部对象;写入期间仍需避免其他客户端或用户并发修改项目 settings。
77
+
78
+ 现有 `visualBible` 必须从 `canvas get` 原样保留;需要修改时,必须使用项目或服务端返回的完整 `VisualBibleV1`。禁止写入空对象或占位对象,也不要自行编造视觉圣经结构。`storyboardGridSize` 仅在节点级 `gridSize` 无法使用的异常回退中经用户确认后写入;正常流程继续按镜头复杂度设置各节点的 `gridSize`。
64
79
 
65
80
  ## 连线
66
81
 
@@ -72,6 +87,8 @@ ai-short-studio canvas edge delete <edgeId> --project <projectId> --yes --json
72
87
 
73
88
  handle 名称来自节点类型目录中的输入输出 schema,不要自行推断。
74
89
 
90
+ Meme 推荐连线为:`text.text → meme-scene-analysis.story`、自动生成的计划资源 `.text → meme-material-search.plan`、素材包资源 `.text → cat-meme-video.materials`、原故事 `.text → cat-meme-video.story`,以及可选的计划资源 `.text → cat-meme-video.plan`。分析或检索任务成功后先刷新画布,取得服务端自动创建的真实资源节点 ID。`cat-meme-video` 的 `config` 支持 `aspectRatio: "1:1" | "9:16" | "16:9"` 和 `resolution: "720p" | "1080p"`;创建或更新节点前仍以实时 schema 为准。
91
+
75
92
  ## 资产与下载
76
93
 
77
94
  ```bash
@@ -82,6 +99,8 @@ ai-short-studio canvas asset select-version <assetId> --version <versionId> --pr
82
99
  ai-short-studio canvas asset download <assetId> --project <projectId> --output <directory>
83
100
  ```
84
101
 
102
+ `canvas asset upload` 会按文件扩展名为常见图片、视频和音频设置 MIME 类型;未知扩展名使用 `application/octet-stream`。
103
+
85
104
  ## 异步任务
86
105
 
87
106
  ```bash
@@ -97,14 +116,18 @@ ai-short-studio config get --json
97
116
  ai-short-studio config set --file <config.json> --json
98
117
  ```
99
118
 
100
- `config set` 接受 `models`、`providers`、`defaultModels`、`capabilityDefaults`、`workflowConcurrency` 的部分对象。provider 的 `apiKey` 缺省时保留现有密钥,空字符串表示清除;敏感配置文件不要提交到版本库。
119
+ `config set` 接受 `models`、`providers`、`defaultModels`、`capabilityDefaults`、`workflowConcurrency` 的部分对象。provider 的 `apiKey` 缺省时保留现有密钥,空字符串表示清除;敏感配置文件不要提交到版本库。标准创作启动只执行只读的 `config get` 来计算账户默认模型的继承结果;不得用账户级 `config set` 代替项目的 canvas settings。
101
120
 
102
121
  ## 标准创作命令顺序
103
122
 
104
- 先只读执行 `preflight` → `canvas node types` → `project list/get` → `canvas get` → `canvas node list` `canvas asset list`。确认用户意图和配置后,才执行 node add/edge add/node run,并对每个返回任务使用 `task wait`。
123
+ 先只读执行 `preflight` → `config get` → `canvas node types` → `project list`。新项目随后按“确认名称/用途 → `project create` → `project get` + `canvas get`”继续,不要求创建前读取不存在的项目或画布;既有项目按“`project get` `canvas get`”继续。现有节点和资产也只读检查。
124
+
125
+ 取得画布后,向用户展示项目名称/用途,并逐项确认 `aspectRatio`、`artStyle`、`visualBible`、`storyboardImageType`、`analysisModel`、`imageModel`、`editModel`、`videoModel`、`audioModel` 的当前值、继承后的有效值、建议值与来源;模型可项目覆盖,也可由用户明确确认继承账户默认,但必须明确最终生效且可用的具体模型。`storyboardGridSize` 仅在异常回退时确认。
126
+
127
+ 项目名称和用途先由用户确认;新项目用 `project create` 保存,既有项目需要变更时仅用 `project update` 修改用户确认的项目字段,用途可记录在 description 中。配置写入窗口内必须避免其他客户端或用户并发修改项目 settings;无法确保时停下并请用户协调。`canvas apply` 前立即执行一次 `canvas get`,以最新完整 settings 为基线重新合并;相对用户刚确认的快照有字段变化时,先展示变化字段并重新确认,再写入一次。随后用 `canvas get` 回读,必须把完整 settings(包括原样保留字段)与写入目标整体比较;只有完全一致且所需模型可用,才执行 node add/edge add/node run。写入、回读或模型校验失败时停在配置阶段,不触发付费任务。账户级 `config set`、项目 `description` 和节点 `prompt`/`config` 都不能替代项目 settings。
105
128
 
106
- 执行 `storyboard-breakdown` 前,必须先完成本次故事涉及的全部人物、场景、道具等素材节点:逐个等待生成任务成功,用 `canvas asset get/list` 核对并在用户确认后设置 selected version。随后依据实时 schema,用 `canvas edge add` 将剧本节点和每个相关素材节点分别连接到 `storyboard-breakdown` 对应输入 handle,再用 `canvas get` 与 `canvas edge list` 核对无遗漏。任一素材任务未成功、selected version 为空或对应连线缺失时,都不得执行分镜拆解;素材范围不清楚时先询问用户。即使服务端 schema 把人物、场景或道具输入标为可选,标准创作流程也不能跳过本次故事实际涉及且已确认使用的素材。
129
+ 执行 `storyboard-breakdown` 前,必须先完成本次故事涉及的全部人物、场景、道具等素材节点:互不依赖的节点先同批执行并收集真实任务 ID,再并发调用 `task wait`;有上游依赖的节点按依赖顺序分批执行,不得为了等待单个任务而串行提交其他独立素材。全部任务成功后,用 `canvas asset get/list` 核对并在用户确认后设置 selected version。随后依据实时 schema,用 `canvas edge add` 将剧本节点和每个相关素材节点分别连接到 `storyboard-breakdown` 对应输入 handle,再用 `canvas get` 与 `canvas edge list` 核对无遗漏。任一素材任务未成功、selected version 为空或对应连线缺失时,都不得执行分镜拆解;素材范围不清楚时先询问用户。即使服务端 schema 把人物、场景或道具输入标为可选,标准创作流程也不能跳过本次故事实际涉及且已确认使用的素材。
107
130
 
108
- 生成 `storyboard-image` 前,用 `canvas asset get/list` 取得素材 selected version,必要时通过 `canvas asset download` 下载,并向用户实际展示人物、场景、道具等预览;用户确认后才可执行。生成视频前,同样取得并展示实际分镜图或可访问预览,确认分镜图和视频镜头范围后才可执行 `storyboard-video-prompt` / `video-generate`。仅输出资产 ID、URL、文件路径或成功状态不能代替展示;任一确认缺失时停止并询问。
131
+ 生成 `storyboard-image` 前,用 `canvas asset get/list` 取得素材 selected version,必要时通过 `canvas asset download` 下载,并向用户实际展示人物、场景、道具等预览;用户确认后才可执行。逐镜头根据需要独立呈现的动作、表情、主体关系、视角和空间调度状态选择最小够用的 `gridSize`,不得沿用统一格数或仅按时长推算;先用实时节点 schema 核对允许值,再向用户列出“镜号、复杂度依据、建议格数”,确认后逐个更新对应 `storyboard-image` 节点。生成视频前,同样取得并展示实际分镜图或可访问预览,确认分镜图和视频镜头范围后才可执行 `storyboard-video-prompt` / `video-generate`。仅输出资产 ID、URL、文件路径或成功状态不能代替展示;任一确认缺失时停止并询问。
109
132
 
110
133
  `storyboard-breakdown`、`storyboard-image` 和 `storyboard-video-prompt` 成功后会由服务端物化下游节点。每步完成后先重新 `canvas get` 或 `canvas node list` 获取真实 ID,禁止照示例伪造 ID 或重复建节点。