@aswless_854771076/ai_short_studio_cli 0.1.5 → 0.1.7

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
@@ -18,10 +18,13 @@ ai-short-studio canvas node types --json
18
18
  ai-short-studio auth login --email user@example.com --password-stdin
19
19
  ```
20
20
 
21
- CLI 只把 refresh token 保存到系统钥匙串,access token 仅驻留内存并在到期前自动刷新。首次登录通过参数或 `VVICAT_SUPABASE_URL`、`VVICAT_SUPABASE_PUBLISHABLE_KEY` 提供 Supabase 公共配置。服务地址和非敏感 profile 写入系统配置目录;可使用 `--profile`、`--base-url` 或 `VVICAT_BASE_URL` 切换环境。
21
+ CLI 只把 refresh token 保存到系统钥匙串,access token 仅驻留内存并在到期前自动刷新。首次登录会从服务端 bootstrap 取得 Supabase 公共配置,也可用 `VVICAT_SUPABASE_URL`、`VVICAT_SUPABASE_PUBLISHABLE_KEY` 显式覆盖。服务地址和非敏感 profile 写入系统配置目录;切换环境时必须使用独立的 `--profile`,避免复用旧认证。
22
+
23
+ 创作前运行 `ai-short-studio preflight --json`。CLI 会通过公开的 `/api/v1/bootstrap` 检查服务连通性、取得 Supabase 公共配置、比较稳定版本并默认更新当前全局包,再检查登录态和文本、人物、场景、故事版、视频模型配置。更新后必须按输出重跑;明确不允许全局写入时使用 `--no-update`。
22
24
 
23
25
  ## 命令范围
24
26
 
27
+ - `ai-short-studio preflight`:创作前版本、连通性、认证和模型配置检查。
25
28
  - `ai-short-studio project`:无限画布项目增删改查。
26
29
  - `ai-short-studio canvas get|apply`:读取或批量更新画布。
27
30
  - `ai-short-studio canvas node`:节点类型/schema 查询、节点读写、执行。
@@ -36,13 +39,13 @@ ai-short-studio canvas edge delete <edgeId> --project <projectId> --yes --json
36
39
  ai-short-studio canvas asset select-version <assetId> --project <projectId> --version <versionId> --json
37
40
  ```
38
41
 
39
- CLI 直接使用现有 `/api/projects`、`/api/canvas/node-types`、项目画布与资产路由,不要求服务端额外提供版本化包装接口。
42
+ CLI 通过 `/api/v1/bootstrap` 完成版本、连通性与 Supabase 公共配置预检,并直接使用现有 `/api/projects`、`/api/canvas/node-types`、项目画布与资产路由。
40
43
 
41
44
  运行 `ai-short-studio <topic> --help` 查看精确参数。可执行命令使用完整产品名 `ai-short-studio`,同时避免与其他 CLI 冲突。所有结构化响应保持 JSON;API 错误也返回稳定的 `error.code`、`status`、`retryable` 和 `details`。
42
45
 
43
46
  退出码:`0` 成功,`3` 未认证,`4` 不存在,`5` 禁止或冲突,`6` 参数错误,`7` 其他错误。
44
47
 
45
- 配套 Agent Skill 位于 `skills/using-vvicat-cli`,随 npm 包发布。安装或更新 CLI 时会自动安装到 Codex Skill 目录;已被用户修改或非 CLI 托管的目录不会被覆盖。
48
+ 配套 Agent Skill 位于 `skills/using-vvicat-ai-short-studio-cli`,随 npm 包发布。安装或更新 CLI 时会自动安装到 Codex Skill 目录;已被用户修改或非 CLI 托管的目录不会被覆盖。
46
49
 
47
50
  ```bash
48
51
  ai-short-studio skill install --target codex
@@ -0,0 +1,12 @@
1
+ import { BaseCommand } from '../base-command.js';
2
+ export default class Preflight extends BaseCommand {
3
+ static description: string;
4
+ static flags: {
5
+ update: import("@oclif/core/interfaces").BooleanFlag<boolean>;
6
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ locale: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,40 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../base-command.js';
3
+ import { ProfileStore } from '../config/profiles.js';
4
+ import { VvicatApi } from '../domain/vvicat-api.js';
5
+ import { createRuntime } from '../runtime.js';
6
+ import { compareStableVersions, fetchBootstrap, inspectCreationConfig, readCurrentPackageIdentity, updateCliPackage, } from '../preflight.js';
7
+ export default class Preflight extends BaseCommand {
8
+ static description = '创作前检查 CLI、服务连接、认证和模型配置';
9
+ static flags = {
10
+ ...BaseCommand.baseFlags,
11
+ update: Flags.boolean({ description: '发现新版本时更新全局 CLI', default: true, allowNo: true }),
12
+ };
13
+ async run() {
14
+ const { flags } = await this.parse(Preflight);
15
+ const profiles = new ProfileStore();
16
+ const selected = flags.profile ? await profiles.get(flags.profile) : await profiles.current();
17
+ const baseUrl = (flags['base-url'] || process.env.VVICAT_BASE_URL || selected?.baseUrl || '').replace(/\/+$/, '');
18
+ if (!baseUrl)
19
+ this.error('缺少服务地址,请传入 --base-url 或设置 VVICAT_BASE_URL');
20
+ const bootstrap = await fetchBootstrap(baseUrl);
21
+ const current = await readCurrentPackageIdentity();
22
+ const hasUpdate = compareStableVersions(current.version, bootstrap.cli.latestVersion) < 0;
23
+ const version = flags.update
24
+ ? updateCliPackage({ current, latest: bootstrap.cli })
25
+ : {
26
+ updated: false,
27
+ restartRequired: false,
28
+ currentVersion: current.version,
29
+ latestVersion: bootstrap.cli.latestVersion,
30
+ };
31
+ if (version.restartRequired) {
32
+ this.print({ connected: true, version, ready: false, nextAction: '重新运行 ai-short-studio preflight --json' });
33
+ return;
34
+ }
35
+ const runtime = await createRuntime(flags);
36
+ await runtime.client.request('/api/auth/session');
37
+ const readiness = inspectCreationConfig(await new VvicatApi(runtime.client).config());
38
+ this.print({ connected: true, authenticated: true, updateAvailable: hasUpdate, version, ...readiness });
39
+ }
40
+ }
@@ -0,0 +1,46 @@
1
+ export interface BootstrapConfig {
2
+ cli: {
3
+ packageName: string;
4
+ latestVersion: string;
5
+ };
6
+ supabase: {
7
+ url: string;
8
+ publishableKey: string;
9
+ };
10
+ }
11
+ type Fetcher = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
12
+ type PackageRunner = (command: string, args: string[], options: {
13
+ shell: false;
14
+ timeout: number;
15
+ encoding: 'utf8';
16
+ }) => {
17
+ status: number | null;
18
+ error?: Error;
19
+ };
20
+ export declare function fetchBootstrap(baseUrl: string, fetcher?: Fetcher): Promise<BootstrapConfig>;
21
+ export declare function compareStableVersions(left: string, right: string): number;
22
+ export declare function readCurrentPackageIdentity(): Promise<{
23
+ name: string;
24
+ version: string;
25
+ }>;
26
+ export declare function updateCliPackage(input: {
27
+ current: {
28
+ name: string;
29
+ version: string;
30
+ };
31
+ latest: {
32
+ packageName: string;
33
+ latestVersion: string;
34
+ };
35
+ run?: PackageRunner;
36
+ }): {
37
+ updated: boolean;
38
+ restartRequired: boolean;
39
+ currentVersion: string;
40
+ latestVersion: string;
41
+ };
42
+ export declare function inspectCreationConfig(config: unknown): {
43
+ ready: boolean;
44
+ missing: string[];
45
+ };
46
+ export {};
@@ -0,0 +1,140 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readFile } from 'node:fs/promises';
3
+ const STABLE_SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
4
+ const REQUIRED_CREATION_MODELS = {
5
+ analysisModel: 'llm',
6
+ characterModel: 'image',
7
+ locationModel: 'image',
8
+ storyboardModel: 'image',
9
+ videoModel: 'video',
10
+ };
11
+ export async function fetchBootstrap(baseUrl, fetcher = fetch) {
12
+ const normalizedBaseUrl = baseUrl.replace(/\/+$/, '');
13
+ let response;
14
+ try {
15
+ response = await fetcher(`${normalizedBaseUrl}/api/v1/bootstrap`, {
16
+ headers: { accept: 'application/json' },
17
+ signal: AbortSignal.timeout(10_000),
18
+ });
19
+ }
20
+ catch {
21
+ throw new Error(`无法连接 VVICAT 服务,请检查 API base URL:${normalizedBaseUrl}`);
22
+ }
23
+ if (!response.ok) {
24
+ throw new Error(`VVICAT 服务预检失败:HTTP ${response.status};请检查 API base URL:${normalizedBaseUrl}`);
25
+ }
26
+ const payload = await response.json().catch(() => null);
27
+ const data = payload?.data;
28
+ const cli = data?.cli;
29
+ const supabase = data?.supabase;
30
+ if (payload?.ok !== true
31
+ || typeof cli?.packageName !== 'string'
32
+ || typeof cli.latestVersion !== 'string'
33
+ || typeof supabase?.url !== 'string'
34
+ || typeof supabase.publishableKey !== 'string')
35
+ throw new Error('VVICAT 服务返回的 bootstrap 配置无效');
36
+ compareStableVersions(cli.latestVersion, cli.latestVersion);
37
+ return {
38
+ cli: { packageName: cli.packageName, latestVersion: cli.latestVersion },
39
+ supabase: { url: supabase.url, publishableKey: supabase.publishableKey },
40
+ };
41
+ }
42
+ export function compareStableVersions(left, right) {
43
+ if (!STABLE_SEMVER.test(left) || !STABLE_SEMVER.test(right))
44
+ throw new Error('CLI 版本必须是稳定 SemVer');
45
+ const a = left.split('.').map(Number);
46
+ const b = right.split('.').map(Number);
47
+ for (let index = 0; index < 3; index += 1) {
48
+ if (a[index] !== b[index])
49
+ return a[index] - b[index];
50
+ }
51
+ return 0;
52
+ }
53
+ export async function readCurrentPackageIdentity() {
54
+ const manifest = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
55
+ if (typeof manifest.name !== 'string' || typeof manifest.version !== 'string')
56
+ throw new Error('当前 CLI 包清单无效');
57
+ return { name: manifest.name, version: manifest.version };
58
+ }
59
+ export function updateCliPackage(input) {
60
+ if (input.current.name !== input.latest.packageName)
61
+ throw new Error('服务端返回的 CLI 包名与当前安装包不一致');
62
+ const comparison = compareStableVersions(input.current.version, input.latest.latestVersion);
63
+ const result = {
64
+ updated: false,
65
+ restartRequired: false,
66
+ currentVersion: input.current.version,
67
+ latestVersion: input.latest.latestVersion,
68
+ };
69
+ if (comparison >= 0)
70
+ return result;
71
+ const run = input.run || ((command, args, options) => {
72
+ const child = spawnSync(command, args, options);
73
+ return { status: child.status, error: child.error };
74
+ });
75
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
76
+ const child = run(npm, [
77
+ 'install',
78
+ '--global',
79
+ `${input.current.name}@${input.latest.latestVersion}`,
80
+ '--prefer-online',
81
+ '--no-audit',
82
+ '--no-fund',
83
+ ], { shell: false, timeout: 120_000, encoding: 'utf8' });
84
+ if (child.error || child.status !== 0)
85
+ throw new Error('CLI 自动更新失败,请检查 npm 全局安装权限后重试');
86
+ return { ...result, updated: true, restartRequired: true };
87
+ }
88
+ export function inspectCreationConfig(config) {
89
+ const value = isRecord(config) ? config : {};
90
+ const providers = Array.isArray(value.providers) ? value.providers.filter(isRecord) : [];
91
+ const models = Array.isArray(value.models) ? value.models.filter(isRecord) : [];
92
+ const defaults = isRecord(value.defaultModels) ? value.defaultModels : {};
93
+ const capabilityDefaults = isRecord(value.capabilityDefaults) ? value.capabilityDefaults : {};
94
+ const missing = [];
95
+ const addMissing = (field) => { if (!missing.includes(field))
96
+ missing.push(field); };
97
+ for (const [field, expectedType] of Object.entries(REQUIRED_CREATION_MODELS)) {
98
+ const modelKey = defaults[field];
99
+ if (typeof modelKey !== 'string' || !modelKey.trim()) {
100
+ addMissing(`defaultModels.${field}`);
101
+ continue;
102
+ }
103
+ const model = models.find((item) => item.modelKey === modelKey && item.enabled !== false);
104
+ if (!model || model.type !== expectedType) {
105
+ addMissing(`models.${modelKey}`);
106
+ continue;
107
+ }
108
+ const provider = providers.find((item) => item.id === model.provider);
109
+ if (!provider || provider.hasApiKey !== true)
110
+ addMissing(`providers.${String(model.provider)}.apiKey`);
111
+ inspectModelCapabilities(model, modelKey, expectedType, capabilityDefaults, addMissing);
112
+ }
113
+ if (!isRecord(value.capabilityDefaults))
114
+ addMissing('capabilityDefaults');
115
+ return { ready: missing.length === 0, missing };
116
+ }
117
+ function inspectModelCapabilities(model, modelKey, modelType, defaults, addMissing) {
118
+ const capabilities = isRecord(model.capabilities) ? model.capabilities : {};
119
+ const namespace = isRecord(capabilities[modelType]) ? capabilities[modelType] : {};
120
+ const selection = isRecord(defaults[modelKey]) ? defaults[modelKey] : {};
121
+ for (const [optionName, rawOptions] of Object.entries(namespace)) {
122
+ if (!optionName.endsWith('Options') || !Array.isArray(rawOptions) || rawOptions.length === 0)
123
+ continue;
124
+ const field = optionName.slice(0, -'Options'.length);
125
+ const selected = selection[field];
126
+ if (selected === undefined) {
127
+ if (modelType === 'image' && field === 'resolution')
128
+ continue;
129
+ if (modelType === 'video' && field === 'generateAudio' && rawOptions.includes(false))
130
+ continue;
131
+ addMissing(`capabilityDefaults.${modelKey}.${field}`);
132
+ }
133
+ else if (!rawOptions.includes(selected)) {
134
+ addMissing(`capabilityDefaults.${modelKey}.${field}`);
135
+ }
136
+ }
137
+ }
138
+ function isRecord(value) {
139
+ return !!value && typeof value === 'object' && !Array.isArray(value);
140
+ }
package/dist/runtime.js CHANGED
@@ -2,17 +2,29 @@ import { ApiClient } from './client/api-client.js';
2
2
  import { KeyringCredentialStore } from './auth/credential-store.js';
3
3
  import { SessionManager, SupabaseAuthProvider } from './auth/session-manager.js';
4
4
  import { ProfileStore } from './config/profiles.js';
5
+ import { fetchBootstrap } from './preflight.js';
5
6
  export async function createRuntime(flags) {
6
7
  const profiles = new ProfileStore();
7
8
  const selected = flags.profile ? await profiles.get(flags.profile) : await profiles.current();
8
9
  const baseUrl = (flags['base-url'] || process.env.VVICAT_BASE_URL || selected?.baseUrl || '').replace(/\/+$/, '');
9
10
  if (!baseUrl)
10
11
  throw new Error('缺少服务地址,请传入 --base-url 或设置 VVICAT_BASE_URL');
11
- const profile = ensureAuthConfig({
12
+ if (selected?.baseUrl && selected.baseUrl.replace(/\/+$/, '') !== baseUrl) {
13
+ throw new Error('API base URL 与当前 profile 不一致;请为新服务使用独立的 --profile,避免复用旧环境认证');
14
+ }
15
+ let profile = resolveAuthConfig({
12
16
  ...selected,
13
17
  name: flags.profile || selected?.name || 'default',
14
18
  baseUrl,
15
19
  }, flags);
20
+ if (!profile.supabaseUrl || !profile.supabasePublishableKey) {
21
+ const bootstrap = await fetchBootstrap(baseUrl);
22
+ profile = {
23
+ ...profile,
24
+ supabaseUrl: profile.supabaseUrl || bootstrap.supabase.url,
25
+ supabasePublishableKey: profile.supabasePublishableKey || bootstrap.supabase.publishableKey,
26
+ };
27
+ }
16
28
  await profiles.save(profile);
17
29
  const session = new SessionManager({
18
30
  profile: profile.name,
@@ -25,7 +37,7 @@ export async function createRuntime(flags) {
25
37
  client: new ApiClient({ baseUrl, session, locale: flags.locale }),
26
38
  };
27
39
  }
28
- function ensureAuthConfig(profile, flags) {
40
+ function resolveAuthConfig(profile, flags) {
29
41
  const supabaseUrl = flags['supabase-url']
30
42
  || process.env.VVICAT_SUPABASE_URL
31
43
  || process.env.NEXT_PUBLIC_SUPABASE_URL
@@ -35,9 +47,6 @@ function ensureAuthConfig(profile, flags) {
35
47
  || process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY
36
48
  || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
37
49
  || profile.supabasePublishableKey;
38
- if (!supabaseUrl || !supabasePublishableKey) {
39
- throw new Error('缺少 Supabase 公共配置;登录时传入 --supabase-url 和 --supabase-publishable-key,或设置对应的 VVICAT_ 环境变量');
40
- }
41
50
  return {
42
51
  ...profile,
43
52
  supabaseUrl,
@@ -3,14 +3,14 @@ import { cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:
3
3
  import { homedir } from 'node:os';
4
4
  import { dirname, join, relative } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
- const BUNDLED_SKILL = fileURLToPath(new URL('../../skills/using-vvicat-cli', import.meta.url));
6
+ const BUNDLED_SKILL = fileURLToPath(new URL('../../skills/using-vvicat-ai-short-studio-cli', import.meta.url));
7
7
  const PACKAGE_JSON = fileURLToPath(new URL('../../package.json', import.meta.url));
8
8
  const MANIFEST = '.vvicat-skill.json';
9
9
  export function bundledSkillPath() {
10
10
  return BUNDLED_SKILL;
11
11
  }
12
12
  export function defaultSkillTarget(target) {
13
- return join(homedir(), target === 'codex' ? '.codex' : '.agents', 'skills', 'using-vvicat-cli');
13
+ return join(homedir(), target === 'codex' ? '.codex' : '.agents', 'skills', 'using-vvicat-ai-short-studio-cli');
14
14
  }
15
15
  export async function skillStatus(target) {
16
16
  const packageVersion = await readPackageVersion();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aswless_854771076/ai_short_studio_cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "VVICAT 无限画布项目命令行工具",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: using-vvicat-ai-short-studio-cli
3
+ description: Use when an agent needs to inspect or operate VVICAT infinite-canvas projects, nodes, edges, assets, tasks, or model configuration through the ai-short-studio CLI.
4
+ ---
5
+
6
+ # 使用 VVICAT AI Short Studio CLI
7
+
8
+ ## 核心原则
9
+
10
+ 把 `ai-short-studio` 当作机器可读的远程控制接口。创作全程只要目标、素材含义、平台、语言、画幅、时长、风格、人物、场景、道具、模型、节点 schema、候选版本、付费范围或覆盖影响存在未知、缺失或矛盾,就合并列出最少必要问题并询问用户;得到答复前只做只读检查,不用示例值、默认值或自行推断替用户做创作决定。
11
+
12
+ ## 初始化检查
13
+
14
+ 创建项目或修改任何资源前,按顺序执行:
15
+
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` 覆盖。
19
+ 4. 运行 `ai-short-studio canvas node types --json`,按实时 `inputSchema`、`outputSchema`、`configSchema` 和 `defaultConfig` 确定能力与必填项。
20
+ 5. 运行 `project list/get` 与 `canvas get`,确认 `INFINITE_CANVAS`、目标项目、现有节点、资产 selected version 和本次创作参数。任何缺口仍按核心原则询问。
21
+
22
+ ## 标准创作流程
23
+
24
+ 1. 明确原始故事或剧本、目标受众与平台、语言、画幅、总时长与镜头时长、风格参考、人物/场景/道具约束,以及交付是否包含剧本、设定、分镜图、视频和音频;缺项先问。
25
+ 2. 完成 preflight、节点目录、模型配置与项目检查。新建项目先确认名称和用途;既有项目优先复用已确认的节点与 selected version。
26
+ 3. 原文需要改编时执行 `novel-to-script`。分镜拆解前必须先完成人物、场景、道具等本次故事涉及的素材生成;每个生成任务都 `task wait` 到成功终态,并让用户确认后为每项素材设置 selected version。任一素材未生成成功、未确认或没有 selected version 时,禁止执行 `storyboard-breakdown`,先询问用户并补齐。
27
+ 4. 按实时节点 schema,把已确认的剧本节点,以及每个人物、场景、道具等相关素材节点,逐项连接到 `storyboard-breakdown` 对应输入 handle(通常为 `script`、`characters`、`locations`、`props`)。执行前重新读取画布和连线,确认本次故事涉及的素材节点无遗漏;即使服务端把素材输入标为可选,标准创作流程也不得省略。任一对应连线缺失时禁止拆解,先补线;素材范围不清楚时先询问用户。全部门禁满足后才执行并等待。成功后刷新画布,读取服务端自动创建的 `storyboard-shot` 和 `storyboard-image`,不得重复手工创建。
28
+ 5. 生成分镜图前,用 `canvas asset get/list` 取得人物、场景、道具等已选版本,必要时下载,并把实际图片或可访问预览展示给用户;只提供资产 ID、文件路径或文字说明不算展示。用户明确确认素材与选版后,优先建议选择 `imageLayout: storyboard`:它适合连续叙事审阅并保留格序、镜号、景别、机位和运镜。也要说明 `grid` 与 `single` 可选;最终由用户确认布局、镜头范围、参考资产、模型与费用范围。未展示或未确认时禁止生成分镜图。
29
+ 6. 执行分镜图后检查任务终态、候选资产与 selected version,并把实际分镜图或可访问预览展示给用户。用户明确确认分镜图以及需要制作的视频镜头后,才能刷新画布并复用服务端自动创建的 `storyboard-video-prompt`;提示词再经用户确认后执行自动创建的 `video-generate`,音频按交付范围执行。未展示或未确认分镜图时禁止生成视频。
30
+ 7. 每次写入前重新读取画布版本;优先用专用 node/edge 命令,批量变更才用 `canvas apply`。每次执行都读取真实任务 ID 并 `task wait`,不用固定 sleep。
31
+ 8. 交付前确认 selected version 与下载目录,记录项目、节点、任务和资产 ID、失败项与未决项。临时项目在所有结束路径删除;既有项目绝不擅自删除。
32
+
33
+ 详细命令按需读取 [references/commands.md](references/commands.md),或运行 `ai-short-studio <topic> --help`。
34
+
35
+ ## 安全边界
36
+
37
+ - 不把密码、access token、refresh token 或 API Key 放入参数、日志、Skill 或仓库。
38
+ - 密码登录使用 `printf`/管道以外的安全 stdin 来源;自动化环境使用秘密管理器注入 stdin。
39
+ - `config get` 只应返回 `hasApiKey`,不得依赖或要求服务端回显密钥。
40
+ - 删除用户既有项目、覆盖完整画布或修改模型配置前,先明确目标和影响;没有授权就停在读取或生成变更计划。
41
+ - 对版本冲突重新读取画布后重算变更,不盲目重试旧的 `expectedVersion`。
42
+
43
+ ## 示例
44
+
45
+ ```bash
46
+ ai-short-studio canvas node types --json
47
+ ai-short-studio project create --name "agent-draft-20260722" --json
48
+ ai-short-studio canvas node add text --project "$PROJECT_ID" --config ./node.json --json
49
+ ai-short-studio canvas node list --project "$PROJECT_ID" --json
50
+ ```
51
+
52
+ 从 JSON 输出中取得真实 ID;示例中的环境变量仅表示调用方已安全解析并保存结果。
53
+
54
+ ## 常见错误
55
+
56
+ - 节点类型写死:先读 `canvas node types`,服务端目录才是事实来源。
57
+ - 配置不全仍继续:先做初始化检查;模型、项目或用户意图有缺口时先询问。
58
+ - CLI 有更新仍用旧进程继续:更新后必须重跑 preflight。
59
+ - 自动创建的下游节点重复添加:任务完成后先刷新画布并复用真实节点 ID。
60
+ - 素材未完成或未连线就拆分镜:人物、场景、道具等相关素材必须任务成功、已确认 selected version,并逐项连接到 `storyboard-breakdown` 对应输入后才能执行。
61
+ - 未展示产物就继续:生成分镜图前展示并确认素材,生成视频前展示并确认分镜图;只报告 ID 或路径不能代替预览与用户确认。
62
+ - 只检查任务创建响应:必须 `task wait` 并检查终态和错误字段。
63
+ - 下载列表中的任意 URL:先确认资产的 selected version,再下载。
64
+ - 测试创建资源但未清理:用调用方的 `finally`/trap 删除唯一前缀项目。
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "使用 VVICAT AI Short Studio CLI"
3
+ short_description: "通过命令行安全操作 VVICAT 无限画布项目与产物"
4
+ default_prompt: "使用 $using-vvicat-ai-short-studio-cli 安全地完成 VVICAT 无限画布项目、节点、任务、产物或模型配置操作。"
@@ -2,6 +2,16 @@
2
2
 
3
3
  所有命令支持 `--base-url`、`--profile`、`--locale zh|en` 和 `--json`。默认档案保存服务地址与默认项目;refresh token 仅保存在系统钥匙串。
4
4
 
5
+ ## 创作前预检
6
+
7
+ ```bash
8
+ ai-short-studio --version
9
+ ai-short-studio preflight --json
10
+ ai-short-studio preflight --no-update --json
11
+ ```
12
+
13
+ `preflight` 通过 `/api/v1/bootstrap` 检查 API base URL 连通性、取得 Supabase 公共配置、比较服务端推荐的稳定 CLI 版本,并检查认证和标准创作所需模型。默认发现新版本会更新当前全局 npm 包;更新后按 `nextAction` 重跑。`--no-update` 仅用于用户明确禁止全局写入时。
14
+
5
15
  ## Skill 管理
6
16
 
7
17
  ```bash
@@ -88,3 +98,13 @@ ai-short-studio config set --file <config.json> --json
88
98
  ```
89
99
 
90
100
  `config set` 接受 `models`、`providers`、`defaultModels`、`capabilityDefaults`、`workflowConcurrency` 的部分对象。provider 的 `apiKey` 缺省时保留现有密钥,空字符串表示清除;敏感配置文件不要提交到版本库。
101
+
102
+ ## 标准创作命令顺序
103
+
104
+ 先只读执行 `preflight` → `canvas node types` → `project list/get` → `canvas get` → `canvas node list` → `canvas asset list`。确认用户意图和配置后,才执行 node add/edge add/node run,并对每个返回任务使用 `task wait`。
105
+
106
+ 执行 `storyboard-breakdown` 前,必须先完成本次故事涉及的全部人物、场景、道具等素材节点:逐个等待生成任务成功,用 `canvas asset get/list` 核对并在用户确认后设置 selected version。随后依据实时 schema,用 `canvas edge add` 将剧本节点和每个相关素材节点分别连接到 `storyboard-breakdown` 对应输入 handle,再用 `canvas get` 与 `canvas edge list` 核对无遗漏。任一素材任务未成功、selected version 为空或对应连线缺失时,都不得执行分镜拆解;素材范围不清楚时先询问用户。即使服务端 schema 把人物、场景或道具输入标为可选,标准创作流程也不能跳过本次故事实际涉及且已确认使用的素材。
107
+
108
+ 生成 `storyboard-image` 前,用 `canvas asset get/list` 取得素材 selected version,必要时通过 `canvas asset download` 下载,并向用户实际展示人物、场景、道具等预览;用户确认后才可执行。生成视频前,同样取得并展示实际分镜图或可访问预览,确认分镜图和视频镜头范围后才可执行 `storyboard-video-prompt` / `video-generate`。仅输出资产 ID、URL、文件路径或成功状态不能代替展示;任一确认缺失时停止并询问。
109
+
110
+ `storyboard-breakdown`、`storyboard-image` 和 `storyboard-video-prompt` 成功后会由服务端物化下游节点。每步完成后先重新 `canvas get` 或 `canvas node list` 获取真实 ID,禁止照示例伪造 ID 或重复建节点。
@@ -1,59 +0,0 @@
1
- ---
2
- name: using-vvicat-cli
3
- description: Use when an agent needs to inspect or operate VVICAT infinite-canvas projects, nodes, edges, assets, tasks, or model configuration through the ai-short-studio CLI.
4
- ---
5
-
6
- # 使用 VVICAT CLI
7
-
8
- ## 核心原则
9
-
10
- 把 `ai-short-studio` 当作机器可读的远程控制接口。认证后首先拉取服务端节点目录,先了解当前有哪些功能节点,再检查模型和项目配置。任何必要配置不完整或信息不确定时,先询问用户,不猜测、不执行写操作。
11
-
12
- ## 初始化检查
13
-
14
- 认证成功后、创建项目或修改任何资源前,按顺序执行:
15
-
16
- 1. 运行 `ai-short-studio canvas node types --json`,检查节点列表及各节点的 `inputSchema`、`outputSchema`、`configSchema` 和 `defaultConfig`,据此确定可用功能与必填项。
17
- 2. 运行 `ai-short-studio config get --json`,检查本次会用到的 `providers`、`models`、`defaultModels` 和 `capabilityDefaults`,包括 provider 的 `hasApiKey` 是否为真。需要新配置模型时,首选公司的 [StarRouter](https://starrouter.io/);需开通账号时联系邱茂。
18
- 3. 运行 `ai-short-studio project list --json`。已有项目再运行 `ai-short-studio project get <projectId> --json` 和 `ai-short-studio canvas get --project <projectId> --json`,检查项目类型、目标项目、画布现状及节点必需输入;新项目则确认名称、用途和节点所需业务参数。
19
- 4. 上述配置缺失、相互矛盾、无法判定是否适用,或用户意图存在任何不确定时,合并列出缺口并询问用户;得到答复前停在只读检查。
20
-
21
- ## 工作流
22
-
23
- 1. 运行 `ai-short-studio auth status --json`。首次登录通过参数或 `VVICAT_BASE_URL`、`VVICAT_SUPABASE_URL`、`VVICAT_SUPABASE_PUBLISHABLE_KEY` 提供服务和 Supabase 公共配置,再运行 `ai-short-studio auth login --google`;非交互环境才通过 stdin 传密码。
24
- npm 安装或更新 CLI 时会自动同步 Codex Skill。先运行 `ai-short-studio skill status --json`;`current` 为 false 时,说明自动同步可能因用户修改或非托管目录被跳过,再询问是否执行 `ai-short-studio skill update`。
25
- 2. 完成上述初始化检查。确认目标项目是 `INFINITE_CANVAS`;创建测试项目时使用唯一前缀,并记录项目 ID。
26
- 3. 严格按节点目录返回的 schema 和默认配置构造数据。
27
- 4. 写入前运行 `ai-short-studio canvas get --project <id> --json` 获取当前结构;优先使用专用 node/edge 命令,批量变更才用 `canvas apply`。
28
- 5. 执行节点后读取返回的任务 ID,并用 `ai-short-studio task wait <taskId> --json` 等待终态。不要使用固定时长 sleep。
29
- 6. 下载前读取资产及所选版本,再使用 `canvas asset download` 写入明确目录。
30
- 7. 对临时项目始终在结束路径中执行 `ai-short-studio project delete <id> --yes`,包括命令失败时。
31
-
32
- 详细命令按需读取 [references/commands.md](references/commands.md),或运行 `ai-short-studio <topic> --help`。
33
-
34
- ## 安全边界
35
-
36
- - 不把密码、access token、refresh token 或 API Key 放入参数、日志、Skill 或仓库。
37
- - 密码登录使用 `printf`/管道以外的安全 stdin 来源;自动化环境使用秘密管理器注入 stdin。
38
- - `config get` 只应返回 `hasApiKey`,不得依赖或要求服务端回显密钥。
39
- - 删除用户既有项目、覆盖完整画布或修改模型配置前,先明确目标和影响;没有授权就停在读取或生成变更计划。
40
- - 对版本冲突重新读取画布后重算变更,不盲目重试旧的 `expectedVersion`。
41
-
42
- ## 示例
43
-
44
- ```bash
45
- ai-short-studio canvas node types --json
46
- ai-short-studio project create --name "agent-draft-20260722" --json
47
- ai-short-studio canvas node add text --project "$PROJECT_ID" --config ./node.json --json
48
- ai-short-studio canvas node list --project "$PROJECT_ID" --json
49
- ```
50
-
51
- 从 JSON 输出中取得真实 ID;示例中的环境变量仅表示调用方已安全解析并保存结果。
52
-
53
- ## 常见错误
54
-
55
- - 节点类型写死:先读 `canvas node types`,服务端目录才是事实来源。
56
- - 配置不全仍继续:先做初始化检查;模型、项目或用户意图有缺口时先询问。
57
- - 只检查任务创建响应:必须 `task wait` 并检查终态和错误字段。
58
- - 下载列表中的任意 URL:先确认资产的 selected version,再下载。
59
- - 测试创建资源但未清理:用调用方的 `finally`/trap 删除唯一前缀项目。
@@ -1,4 +0,0 @@
1
- interface:
2
- display_name: "使用 VVICAT CLI"
3
- short_description: "通过命令行安全操作 VVICAT 无限画布项目与产物"
4
- default_prompt: "使用 $using-vvicat-cli 安全地完成 VVICAT 无限画布项目、节点、任务、产物或模型配置操作。"