@aswless_854771076/ai_short_studio_cli 0.1.43 → 0.1.45
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 +33 -8
- package/dist/auth/session-cache.d.ts +20 -0
- package/dist/auth/session-cache.js +76 -0
- package/dist/auth/session-manager.d.ts +5 -0
- package/dist/auth/session-manager.js +40 -13
- package/dist/command-helpers.d.ts +1 -0
- package/dist/command-helpers.js +12 -0
- package/dist/commands/canvas/apply.d.ts +2 -0
- package/dist/commands/canvas/apply.js +20 -2
- package/dist/commands/canvas/inspect.d.ts +19 -0
- package/dist/commands/canvas/inspect.js +36 -0
- package/dist/commands/canvas/node/run.d.ts +3 -1
- package/dist/commands/canvas/node/run.js +33 -7
- package/dist/commands/canvas/node/update-many.d.ts +13 -0
- package/dist/commands/canvas/node/update-many.js +48 -0
- package/dist/commands/canvas/node/update.d.ts +5 -0
- package/dist/commands/canvas/node/update.js +46 -2
- package/dist/commands/task/wait.d.ts +3 -1
- package/dist/commands/task/wait.js +19 -5
- package/dist/config/profiles.d.ts +1 -0
- package/dist/config/profiles.js +1 -1
- package/dist/domain/vvicat-api.d.ts +20 -0
- package/dist/domain/vvicat-api.js +116 -13
- package/dist/runtime.js +4 -1
- package/dist/skill/skill-manager.d.ts +13 -13
- package/dist/skill/skill-manager.js +1 -1
- package/package.json +2 -1
- package/scripts/postinstall.mjs +2 -2
- package/skills/humanizer/LICENSE +21 -0
- package/skills/humanizer/SKILL.md +412 -0
- package/skills/using-vvicat-ai-short-studio-cli/SKILL.md +21 -8
- package/skills/using-vvicat-ai-short-studio-cli/references/commands.md +17 -6
package/README.md
CHANGED
|
@@ -11,9 +11,9 @@ ai-short-studio project list --json
|
|
|
11
11
|
ai-short-studio canvas node types --json
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
npm 包内置 `using-vvicat-ai-short-studio-cli` Agent Skill。安装或升级 CLI 时,`postinstall`
|
|
14
|
+
npm 包内置 `using-vvicat-ai-short-studio-cli`、`short-drama` 和 `humanizer` 三个 Agent Skill。安装或升级 CLI 时,`postinstall` 会自动把它们安装或更新到 Codex;用户修改过或不由 CLI 管理的副本不会被覆盖。设置 `VVICAT_SKIP_SKILL_INSTALL=1` 可关闭自动安装。
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
`skill install/status/update` 默认管理全部内置 Skill,也可用 `--skill short-drama` 或 `--skill humanizer` 单独管理。某个 Skill 被用户修改时会独立跳过,不影响其他 Skill。剧本先用 `short-drama` 创作和复核,再用 `humanizer` 审计、去除 AI 味,最后回到 `short-drama` 检查剧情连贯性;操作 VVICAT 使用主 CLI Skill。
|
|
17
17
|
|
|
18
18
|
邮箱注册和登录从 stdin 读取密码:
|
|
19
19
|
|
|
@@ -28,7 +28,7 @@ Agent 代操作时,用户已在当前会话明确提供的密码只能直接
|
|
|
28
28
|
|
|
29
29
|
注册确认链接使用当前 profile 的 `baseUrl` 和 `--locale`,指向同域的 `/{locale}/auth/callback`。API 与网页需保持同域,并在 Supabase Redirect URLs 中允许各环境的 `/**`;Site URL 只作为未显式传入回调时的生产兜底。
|
|
30
30
|
|
|
31
|
-
CLI 只把 refresh token
|
|
31
|
+
CLI 只把 refresh token 保存到系统钥匙串。短期 access token 会写入权限为 `0600` 的本机 session 缓存;同一 profile 的并发进程通过跨进程锁共享刷新结果,只有缓存缺失或确实需要刷新时才访问钥匙串,从而避免 Agent 批量操作反复触发 macOS Keychain。退出登录会清除该缓存,CI 可用 `VVICAT_ACCESS_TOKEN` 直接注入临时令牌。未配置服务地址时默认连接 `https://ai-short-studio.vvicat.dev`,`--base-url`、`VVICAT_BASE_URL` 和当前 profile 可依次覆盖。首次登录会从服务端 bootstrap 取得 Supabase 公共配置,也可用 `VVICAT_SUPABASE_URL`、`VVICAT_SUPABASE_PUBLISHABLE_KEY` 显式覆盖。Google 登录在 Windows 上直接调用系统 URL Handler,确保带 `&` 的 PKCE 授权参数完整传给浏览器。服务地址和非敏感 profile 写入系统配置目录;切换环境时必须使用独立的 `--profile`,避免复用旧认证。
|
|
32
32
|
|
|
33
33
|
创作前运行 `ai-short-studio preflight --json`。CLI 会通过公开的 `/api/v1/bootstrap` 检查服务连通性、取得 Supabase 公共配置、读取 npm `latest` 稳定版本并默认更新当前全局包,再检查登录态和文本、人物、场景、故事版、视频模型配置。npm registry 暂时不可用时服务端使用配置版本降级。更新后必须按输出重跑;明确不允许全局写入时使用 `--no-update`。
|
|
34
34
|
|
|
@@ -37,12 +37,12 @@ CLI 只把 refresh token 保存到系统钥匙串,access token 仅驻留内存
|
|
|
37
37
|
- `ai-short-studio preflight`:创作前版本、连通性、认证和模型配置检查。
|
|
38
38
|
- `ai-short-studio project`:无限画布项目增删改查。
|
|
39
39
|
- `ai-short-studio canvas settings get|set`:读取或原子更新项目配置。
|
|
40
|
-
- `ai-short-studio canvas get|apply
|
|
41
|
-
- `ai-short-studio canvas node`:节点类型/schema
|
|
40
|
+
- `ai-short-studio canvas get|inspect|apply`:读取画布、一次聚合查询常用状态,或执行低层图补丁。
|
|
41
|
+
- `ai-short-studio canvas node`:节点类型/schema 查询、字段级或批量节点更新、批量执行。
|
|
42
42
|
- `ai-short-studio canvas edge`:连线读写。
|
|
43
43
|
- `ai-short-studio canvas continuity analyze`:视频生成前分析镜头依赖,可修复已具备源视频的首尾帧连线。
|
|
44
44
|
- `ai-short-studio canvas asset`:资产列表、上传、版本选择和下载。
|
|
45
|
-
- `ai-short-studio task
|
|
45
|
+
- `ai-short-studio task`:异步任务读取、批量等待和取消。
|
|
46
46
|
- `ai-short-studio config`:模型、provider、默认模型、能力默认值和并发配置。
|
|
47
47
|
|
|
48
48
|
```bash
|
|
@@ -80,7 +80,20 @@ ai-short-studio canvas settings set --project "$PROJECT_ID" --file settings.json
|
|
|
80
80
|
}
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
`null` 或空字符串会清除项目覆盖并恢复继承。设置 `artStyle` 会把当前关联的 `visualBible` 保存为项目快照,画风库后续变化不会反向更新项目;同一请求显式传入 `visualBible` 时以显式值为准。所有者和管理员可写,协作者只读;409 冲突后重新 `settings get` 并携带新 `expectedVersion` 重试。`canvas apply`
|
|
83
|
+
`null` 或空字符串会清除项目覆盖并恢复继承。设置 `artStyle` 会把当前关联的 `visualBible` 保存为项目快照,画风库后续变化不会反向更新项目;同一请求显式传入 `visualBible` 时以显式值为准。所有者和管理员可写,协作者只读;409 冲突后重新 `settings get` 并携带新 `expectedVersion` 重试。`canvas apply` 仍用于节点、连线、删除和视口等低层画布图补丁;同阶段有多个可合并图变更时默认一次批量提交并统一回读。已经读取画布时传入 `--canvas-id` 与 `--expected-version`,可避免保存前再请求一次全图。批量入口不可用或合法批量请求仍因粒度失败时才回退单条命令。Pexels 继续使用 `project provider get|set|test`。
|
|
84
|
+
|
|
85
|
+
常用审计优先使用一次聚合查询,不必临时编写脚本或逐条请求:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
ai-short-studio canvas inspect --project "$PROJECT_ID" --include summary --include nodes --include edges --include assets --include settings --include node-types --json
|
|
89
|
+
ai-short-studio canvas inspect --project "$PROJECT_ID" --kind storyboard-image --status stale --operation image-resource --json
|
|
90
|
+
ai-short-studio canvas node update <nodeId> --project "$PROJECT_ID" --set 'gridSize=9' --unset legacyField --json
|
|
91
|
+
ai-short-studio canvas node update-many --project "$PROJECT_ID" --file updates.json --json
|
|
92
|
+
ai-short-studio canvas node run --project "$PROJECT_ID" --node <node1> --node <node2> --concurrency 4 --wait --json
|
|
93
|
+
ai-short-studio task wait --task <task1> --task <task2> --concurrency 4 --json
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`canvas node update` 的 `--set key=<JSON>`、`--unset key`、`--title`、`--x` 和 `--y` 只修改指定字段;旧 `--config` 仍兼容。`update-many` 在一次画布版本校验中原子更新多个节点。结构化生成资源仍必须使用 `edit-resource` 或 `edit-shot`,不能通过字段更新改写 artifact。批量执行与等待默认并发为 4、上限为 16;Agent 应优先单进程批处理,避免为每个节点启动一个 CLI 进程。
|
|
84
97
|
|
|
85
98
|
使用 `canvas node edit-shot <nodeId> --project <projectId> --file shot.json` 编辑镜头。命令会追加并选中不可变的人工 DOCUMENT 版本,不允许修改 `shotKey`、`shotIndex` 或来源身份,旧版本继续可选。
|
|
86
99
|
|
|
@@ -96,7 +109,7 @@ CLI 通过 `/api/v1/bootstrap` 完成版本、连通性与 Supabase 公共配置
|
|
|
96
109
|
|
|
97
110
|
专业 Markdown 剧本可在 `storyboard-breakdown` 配置中显式使用 `storyboardPipelineVersion: 2`、`sourceFormat: screenplay-markdown.v1`、`targetLanguage: en|ar`、`platform: TikTok` 和 `aspectRatio: 9:16`。CLI 通过现有通用节点配置写入这些字段。该模式只生成分镜文本和镜头资产,不自动运行声音、TTS、图片、音频或视频节点。
|
|
98
111
|
|
|
99
|
-
标准短剧制作先用随包安装的 `short-drama`
|
|
112
|
+
标准短剧制作先用随包安装的 `short-drama` 生成和复核专业剧本,再用 `humanizer` 审计模板化台词、虚浮修辞与机械节奏,随后回到 `short-drama` 检查剧情连贯性(已有专业剧本直接复用)。Humanizer 只润色表达,不改剧情事实、人物关系、专有名词、数字、时间线、世界观规则、场次结构或拍摄标记。剧本确认后再选择目标集数、创建或复用项目,并只建立目标集实际出现的人物、场景、道具和可选音色设定。拆镜前必须完成资产—剧情匹配审计;拆镜后逐镜全量检查重复、无效、缺失、冲突、穿帮、连续性、道具交互朝向、语言和来源身份。P0/P1 未清零时不得进入图片、声音或视频生产。
|
|
100
113
|
|
|
101
114
|
`character-visual-design` schema 暴露 `candidateCount`,范围为 1~3,默认值为 1。CLI 按实时 schema 校验和写入;多个视觉描述仍分别进入独立三视图和图片资产链。
|
|
102
115
|
|
|
@@ -116,6 +129,17 @@ CLI 通过 `/api/v1/bootstrap` 完成版本、连通性与 Supabase 公共配置
|
|
|
116
129
|
|
|
117
130
|
配套 Agent Skill 位于 `skills/using-vvicat-ai-short-studio-cli`,随 npm 包发布。安装或更新 CLI 时会自动安装到 Codex Skill 目录;已被用户修改或非 CLI 托管的目录不会被覆盖。
|
|
118
131
|
|
|
132
|
+
## 本地真实验收
|
|
133
|
+
|
|
134
|
+
CLI 开发完成后运行以下命令:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
cd packages/ai_short_studio_cli
|
|
138
|
+
npm run test:local-e2e
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
该命令先构建真实 CLI,再启动隔离的本地 mock API,通过多个 `bin/run.js` 子进程验证聚合查询、字段级与多节点更新、`canvas apply` 免预读、批量执行/等待和旧位置参数兼容;同时启动 8 个独立认证进程,确认共享 session 缓存只触发一次模拟 Keychain 读取和一次刷新。测试不连接生产服务、不调用模型,所有配置、令牌、画布和任务数据均在系统临时目录中创建并在结束时清理。
|
|
142
|
+
|
|
119
143
|
标准创作中,用户未指定时角色、道具、场景/背景和分镜图统一使用 `16:9`;视频按镜头目标时长向上取 5、10、15 秒生成档位,为剪辑保留余量。每批生成后 Agent 必须重新读取任务与资产、核对最终配置并实际检查预览,完成自我审计后才能继续或交付。
|
|
120
144
|
|
|
121
145
|
```bash
|
|
@@ -123,6 +147,7 @@ ai-short-studio skill install --target codex
|
|
|
123
147
|
ai-short-studio skill status --target codex
|
|
124
148
|
ai-short-studio skill update --target codex
|
|
125
149
|
ai-short-studio skill update --target codex --skill short-drama --json
|
|
150
|
+
ai-short-studio skill update --target codex --skill humanizer --json
|
|
126
151
|
```
|
|
127
152
|
|
|
128
153
|
手动恢复或安装到其他 Agent 时,可使用 `--target agents`;自定义全部内置 Skills 的父目录使用 `--root <path>`。旧版 `--directory <path>` 保留为兼容入口,只把主 CLI Skill 精确安装到该目录。自动安装对应使用 `VVICAT_SKILLS_ROOT`;旧变量 `VVICAT_SKILL_DIRECTORY` 仍保持主 Skill 精确目录语义。CLI 会记录版本和 SHA-256;已安装 Skill 被用户修改后,更新默认停止,只有显式 `--force` 才覆盖。自动安装可通过 `VVICAT_SKIP_SKILL_INSTALL=1` 跳过。
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface CachedAccessToken {
|
|
2
|
+
accessToken: string;
|
|
3
|
+
expiresAt: number;
|
|
4
|
+
}
|
|
5
|
+
export interface AccessTokenCache {
|
|
6
|
+
delete(profile: string): Promise<void>;
|
|
7
|
+
get(profile: string): Promise<CachedAccessToken | null>;
|
|
8
|
+
set(profile: string, session: CachedAccessToken): Promise<void>;
|
|
9
|
+
withRefreshLock<T>(profile: string, action: () => Promise<T>): Promise<T>;
|
|
10
|
+
}
|
|
11
|
+
export declare class FileAccessTokenCache implements AccessTokenCache {
|
|
12
|
+
private readonly directory;
|
|
13
|
+
constructor(directory: string);
|
|
14
|
+
delete(profile: string): Promise<void>;
|
|
15
|
+
get(profile: string): Promise<CachedAccessToken | null>;
|
|
16
|
+
set(profile: string, session: CachedAccessToken): Promise<void>;
|
|
17
|
+
withRefreshLock<T>(profile: string, action: () => Promise<T>): Promise<T>;
|
|
18
|
+
private tokenPath;
|
|
19
|
+
private lockPath;
|
|
20
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { chmod, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const LOCK_STALE_MS = 30_000;
|
|
5
|
+
const LOCK_WAIT_MS = 50;
|
|
6
|
+
export class FileAccessTokenCache {
|
|
7
|
+
directory;
|
|
8
|
+
constructor(directory) {
|
|
9
|
+
this.directory = directory;
|
|
10
|
+
}
|
|
11
|
+
async delete(profile) {
|
|
12
|
+
await rm(this.tokenPath(profile), { force: true });
|
|
13
|
+
}
|
|
14
|
+
async get(profile) {
|
|
15
|
+
try {
|
|
16
|
+
const value = JSON.parse(await readFile(this.tokenPath(profile), 'utf8'));
|
|
17
|
+
if (typeof value.accessToken !== 'string' || typeof value.expiresAt !== 'number')
|
|
18
|
+
return null;
|
|
19
|
+
return { accessToken: value.accessToken, expiresAt: value.expiresAt };
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error.code === 'ENOENT')
|
|
23
|
+
return null;
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async set(profile, session) {
|
|
28
|
+
await mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
29
|
+
await chmod(this.directory, 0o700);
|
|
30
|
+
const path = this.tokenPath(profile);
|
|
31
|
+
const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
32
|
+
try {
|
|
33
|
+
await writeFile(temporaryPath, `${JSON.stringify(session)}\n`, { mode: 0o600 });
|
|
34
|
+
await rename(temporaryPath, path);
|
|
35
|
+
await chmod(path, 0o600);
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
await rm(temporaryPath, { force: true });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async withRefreshLock(profile, action) {
|
|
42
|
+
await mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
43
|
+
await chmod(this.directory, 0o700);
|
|
44
|
+
const path = this.lockPath(profile);
|
|
45
|
+
while (true) {
|
|
46
|
+
try {
|
|
47
|
+
await mkdir(path, { mode: 0o700 });
|
|
48
|
+
try {
|
|
49
|
+
return await action();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
await rm(path, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error.code !== 'EEXIST')
|
|
57
|
+
throw error;
|
|
58
|
+
const lockStat = await stat(path).catch(() => null);
|
|
59
|
+
if (lockStat && Date.now() - lockStat.mtimeMs > LOCK_STALE_MS) {
|
|
60
|
+
await rm(path, { recursive: true, force: true });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
await new Promise((resolve) => setTimeout(resolve, LOCK_WAIT_MS));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
tokenPath(profile) {
|
|
68
|
+
return join(this.directory, `${profileKey(profile)}.json`);
|
|
69
|
+
}
|
|
70
|
+
lockPath(profile) {
|
|
71
|
+
return join(this.directory, `${profileKey(profile)}.lock`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function profileKey(profile) {
|
|
75
|
+
return createHash('sha256').update(profile).digest('hex').slice(0, 24);
|
|
76
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AccessTokenCache } from './session-cache.js';
|
|
1
2
|
export interface SessionTokens {
|
|
2
3
|
accessToken: string;
|
|
3
4
|
refreshToken: string;
|
|
@@ -26,7 +27,9 @@ export declare class SupabaseAuthProvider implements AuthProvider {
|
|
|
26
27
|
}
|
|
27
28
|
export declare class SessionManager {
|
|
28
29
|
private current;
|
|
30
|
+
private lastReturnedAccessToken;
|
|
29
31
|
private refreshPromise;
|
|
32
|
+
private readonly cache?;
|
|
30
33
|
private readonly credentials;
|
|
31
34
|
private readonly environment;
|
|
32
35
|
private readonly profile;
|
|
@@ -35,6 +38,7 @@ export declare class SessionManager {
|
|
|
35
38
|
profile: string;
|
|
36
39
|
credentials: CredentialStore;
|
|
37
40
|
provider: AuthProvider;
|
|
41
|
+
cache?: AccessTokenCache;
|
|
38
42
|
environment?: NodeJS.ProcessEnv;
|
|
39
43
|
});
|
|
40
44
|
accessToken(): Promise<string>;
|
|
@@ -44,4 +48,5 @@ export declare class SessionManager {
|
|
|
44
48
|
refreshAccessToken(): Promise<string>;
|
|
45
49
|
logout(): Promise<void>;
|
|
46
50
|
private refresh;
|
|
51
|
+
private remember;
|
|
47
52
|
}
|
|
@@ -49,7 +49,9 @@ function sessionTokens(session) {
|
|
|
49
49
|
}
|
|
50
50
|
export class SessionManager {
|
|
51
51
|
current = null;
|
|
52
|
+
lastReturnedAccessToken = null;
|
|
52
53
|
refreshPromise = null;
|
|
54
|
+
cache;
|
|
53
55
|
credentials;
|
|
54
56
|
environment;
|
|
55
57
|
profile;
|
|
@@ -58,15 +60,19 @@ export class SessionManager {
|
|
|
58
60
|
this.profile = input.profile;
|
|
59
61
|
this.credentials = input.credentials;
|
|
60
62
|
this.provider = input.provider;
|
|
63
|
+
this.cache = input.cache;
|
|
61
64
|
this.environment = input.environment ?? process.env;
|
|
62
65
|
}
|
|
63
66
|
async accessToken() {
|
|
64
67
|
if (this.environment.VVICAT_ACCESS_TOKEN)
|
|
65
|
-
return this.environment.VVICAT_ACCESS_TOKEN;
|
|
66
|
-
if (this.current && this.current
|
|
67
|
-
return this.current.accessToken;
|
|
68
|
+
return this.remember(this.environment.VVICAT_ACCESS_TOKEN);
|
|
69
|
+
if (this.current && isFresh(this.current))
|
|
70
|
+
return this.remember(this.current.accessToken);
|
|
71
|
+
const cached = await this.cache?.get(this.profile);
|
|
72
|
+
if (cached && isFresh(cached))
|
|
73
|
+
return this.remember(cached.accessToken);
|
|
68
74
|
if (!this.refreshPromise) {
|
|
69
|
-
this.refreshPromise = this.refresh().finally(() => {
|
|
75
|
+
this.refreshPromise = this.refresh(false).finally(() => {
|
|
70
76
|
this.refreshPromise = null;
|
|
71
77
|
});
|
|
72
78
|
}
|
|
@@ -75,23 +81,26 @@ export class SessionManager {
|
|
|
75
81
|
async login(email, password) {
|
|
76
82
|
this.current = await this.provider.login(email, password);
|
|
77
83
|
await this.credentials.set(this.profile, this.current.refreshToken);
|
|
84
|
+
await this.cache?.set(this.profile, this.current);
|
|
78
85
|
}
|
|
79
86
|
async loginWithGoogle() {
|
|
80
87
|
this.current = await this.provider.loginWithGoogle();
|
|
81
88
|
await this.credentials.set(this.profile, this.current.refreshToken);
|
|
89
|
+
await this.cache?.set(this.profile, this.current);
|
|
82
90
|
}
|
|
83
91
|
async signup(email, password, emailRedirectTo) {
|
|
84
92
|
this.current = await this.provider.signup(email, password, emailRedirectTo);
|
|
85
93
|
if (!this.current)
|
|
86
94
|
return false;
|
|
87
95
|
await this.credentials.set(this.profile, this.current.refreshToken);
|
|
96
|
+
await this.cache?.set(this.profile, this.current);
|
|
88
97
|
return true;
|
|
89
98
|
}
|
|
90
99
|
async refreshAccessToken() {
|
|
91
100
|
if (this.environment.VVICAT_ACCESS_TOKEN)
|
|
92
|
-
return this.environment.VVICAT_ACCESS_TOKEN;
|
|
101
|
+
return this.remember(this.environment.VVICAT_ACCESS_TOKEN);
|
|
93
102
|
if (!this.refreshPromise) {
|
|
94
|
-
this.refreshPromise = this.refresh().finally(() => {
|
|
103
|
+
this.refreshPromise = this.refresh(true).finally(() => {
|
|
95
104
|
this.refreshPromise = null;
|
|
96
105
|
});
|
|
97
106
|
}
|
|
@@ -103,15 +112,33 @@ export class SessionManager {
|
|
|
103
112
|
if (accessToken)
|
|
104
113
|
await this.provider.logout(accessToken);
|
|
105
114
|
await this.credentials.delete(this.profile);
|
|
115
|
+
await this.cache?.delete(this.profile);
|
|
106
116
|
}
|
|
107
117
|
this.current = null;
|
|
108
118
|
}
|
|
109
|
-
async refresh() {
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
async refresh(force) {
|
|
120
|
+
const action = async () => {
|
|
121
|
+
const cached = await this.cache?.get(this.profile);
|
|
122
|
+
if (cached && isFresh(cached) && (!force || cached.accessToken !== this.lastReturnedAccessToken)) {
|
|
123
|
+
return this.remember(cached.accessToken);
|
|
124
|
+
}
|
|
125
|
+
const refreshToken = await this.credentials.get(this.profile);
|
|
126
|
+
if (!refreshToken)
|
|
127
|
+
throw new Error('尚未登录,请先运行 ai-short-studio auth login');
|
|
128
|
+
this.current = await this.provider.refresh(refreshToken);
|
|
129
|
+
if (this.current.refreshToken !== refreshToken) {
|
|
130
|
+
await this.credentials.set(this.profile, this.current.refreshToken);
|
|
131
|
+
}
|
|
132
|
+
await this.cache?.set(this.profile, this.current);
|
|
133
|
+
return this.remember(this.current.accessToken);
|
|
134
|
+
};
|
|
135
|
+
return this.cache ? this.cache.withRefreshLock(this.profile, action) : action();
|
|
136
|
+
}
|
|
137
|
+
remember(accessToken) {
|
|
138
|
+
this.lastReturnedAccessToken = accessToken;
|
|
139
|
+
return accessToken;
|
|
116
140
|
}
|
|
117
141
|
}
|
|
142
|
+
function isFresh(session) {
|
|
143
|
+
return session.expiresAt > Date.now() + 60_000;
|
|
144
|
+
}
|
|
@@ -8,3 +8,4 @@ export declare function commandApi(flags: Parameters<typeof createRuntime>[0]):
|
|
|
8
8
|
client: import("./client/api-client.js").ApiClient;
|
|
9
9
|
}>;
|
|
10
10
|
export declare function readJsonInput(file?: string): Promise<Record<string, unknown>>;
|
|
11
|
+
export declare function mapConcurrent<T, R>(values: T[], concurrency: number, transform: (value: T, index: number) => Promise<R>): Promise<R[]>;
|
package/dist/command-helpers.js
CHANGED
|
@@ -14,6 +14,18 @@ export async function readJsonInput(file) {
|
|
|
14
14
|
throw new Error('输入必须是 JSON 对象');
|
|
15
15
|
return value;
|
|
16
16
|
}
|
|
17
|
+
export async function mapConcurrent(values, concurrency, transform) {
|
|
18
|
+
const results = new Array(values.length);
|
|
19
|
+
let cursor = 0;
|
|
20
|
+
const workers = Array.from({ length: Math.min(Math.max(1, concurrency), values.length) }, async () => {
|
|
21
|
+
while (cursor < values.length) {
|
|
22
|
+
const index = cursor++;
|
|
23
|
+
results[index] = await transform(values[index], index);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
await Promise.all(workers);
|
|
27
|
+
return results;
|
|
28
|
+
}
|
|
17
29
|
async function readStdin() {
|
|
18
30
|
if (process.stdin.isTTY)
|
|
19
31
|
throw new Error('请使用 --file 或通过 stdin 传入 JSON');
|
|
@@ -3,6 +3,8 @@ export default class CanvasApply extends BaseCommand {
|
|
|
3
3
|
static flags: {
|
|
4
4
|
project: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
5
5
|
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
|
+
'canvas-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
'expected-version': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
8
|
'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
9
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
10
|
locale: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -2,6 +2,24 @@ import { Flags } from '@oclif/core';
|
|
|
2
2
|
import { BaseCommand } from '../../base-command.js';
|
|
3
3
|
import { commandApi, projectFlag, readJsonInput } from '../../command-helpers.js';
|
|
4
4
|
export default class CanvasApply extends BaseCommand {
|
|
5
|
-
static flags = {
|
|
6
|
-
|
|
5
|
+
static flags = {
|
|
6
|
+
...BaseCommand.baseFlags,
|
|
7
|
+
project: projectFlag,
|
|
8
|
+
file: Flags.string({ description: 'patch JSON 文件;省略时读取 stdin' }),
|
|
9
|
+
'canvas-id': Flags.string({ description: '已知画布 ID;与 expected-version 同时提供时跳过预读' }),
|
|
10
|
+
'expected-version': Flags.integer({ description: '最近一次读取的画布版本;与 canvas-id 同时提供时跳过预读' }),
|
|
11
|
+
};
|
|
12
|
+
async run() {
|
|
13
|
+
const { flags } = await this.parse(CanvasApply);
|
|
14
|
+
const { api } = await commandApi(flags);
|
|
15
|
+
const input = await readJsonInput(flags.file);
|
|
16
|
+
const canvasId = flags['canvas-id'] ?? (typeof input.canvasId === 'string' ? input.canvasId : undefined);
|
|
17
|
+
const expectedVersion = flags['expected-version'] ?? (typeof input.expectedVersion === 'number' ? input.expectedVersion : undefined);
|
|
18
|
+
const canvas = canvasId && expectedVersion !== undefined ? null : await api.canvas(flags.project);
|
|
19
|
+
this.print(await api.patchCanvas(flags.project, {
|
|
20
|
+
...input,
|
|
21
|
+
canvasId: canvasId ?? canvas.id,
|
|
22
|
+
expectedVersion: expectedVersion ?? canvas.version,
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
7
25
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
export default class CanvasInspect extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static flags: {
|
|
5
|
+
project: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
|
+
include: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
kind: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
status: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
operation: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
'resource-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
'producer-node': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
locale: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../base-command.js';
|
|
3
|
+
import { commandApi, projectFlag } from '../../command-helpers.js';
|
|
4
|
+
const includeOptions = ['summary', 'nodes', 'edges', 'assets', 'settings', 'node-types'];
|
|
5
|
+
export default class CanvasInspect extends BaseCommand {
|
|
6
|
+
static description = '一次读取并筛选画布节点、连线、资产、设置与节点目录';
|
|
7
|
+
static flags = {
|
|
8
|
+
...BaseCommand.baseFlags,
|
|
9
|
+
project: projectFlag,
|
|
10
|
+
include: Flags.string({
|
|
11
|
+
description: '返回内容,可重复指定',
|
|
12
|
+
multiple: true,
|
|
13
|
+
options: [...includeOptions],
|
|
14
|
+
default: ['summary', 'nodes', 'edges'],
|
|
15
|
+
}),
|
|
16
|
+
kind: Flags.string({ description: '节点 kind,可重复指定', multiple: true }),
|
|
17
|
+
status: Flags.string({ description: '节点状态,可重复指定', multiple: true }),
|
|
18
|
+
search: Flags.string({ description: '按节点 ID、标题、资源名称或 resourceKey 搜索' }),
|
|
19
|
+
operation: Flags.string({ description: '按 artifact.operation 筛选' }),
|
|
20
|
+
'resource-type': Flags.string({ description: '按 artifact.resourceType 筛选' }),
|
|
21
|
+
'producer-node': Flags.string({ description: '按 artifact.producerNodeId 筛选' }),
|
|
22
|
+
};
|
|
23
|
+
async run() {
|
|
24
|
+
const { flags } = await this.parse(CanvasInspect);
|
|
25
|
+
const { api } = await commandApi(flags);
|
|
26
|
+
this.print(await api.inspectCanvas(flags.project, {
|
|
27
|
+
includes: flags.include,
|
|
28
|
+
kinds: flags.kind,
|
|
29
|
+
statuses: flags.status,
|
|
30
|
+
search: flags.search,
|
|
31
|
+
operation: flags.operation,
|
|
32
|
+
resourceType: flags['resource-type'],
|
|
33
|
+
producerNodeId: flags['producer-node'],
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { BaseCommand } from '../../../base-command.js';
|
|
2
2
|
export default class CanvasNodeRun extends BaseCommand {
|
|
3
3
|
static args: {
|
|
4
|
-
nodeId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
4
|
+
nodeId: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
5
|
};
|
|
6
6
|
static flags: {
|
|
7
7
|
project: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
node: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
concurrency: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
10
|
wait: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
11
|
interval: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
12
|
'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,11 +1,37 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import { BaseCommand } from '../../../base-command.js';
|
|
3
|
-
import { commandApi, projectFlag } from '../../../command-helpers.js';
|
|
3
|
+
import { commandApi, mapConcurrent, projectFlag } from '../../../command-helpers.js';
|
|
4
4
|
export default class CanvasNodeRun extends BaseCommand {
|
|
5
|
-
static args = { nodeId: Args.string({ required:
|
|
6
|
-
static flags = {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
static args = { nodeId: Args.string({ required: false }) };
|
|
6
|
+
static flags = {
|
|
7
|
+
...BaseCommand.baseFlags,
|
|
8
|
+
project: projectFlag,
|
|
9
|
+
node: Flags.string({ description: '节点 ID,可重复指定以批量运行', multiple: true }),
|
|
10
|
+
concurrency: Flags.integer({ description: '批量提交并发数', default: 4, min: 1, max: 16 }),
|
|
11
|
+
wait: Flags.boolean({ default: false }),
|
|
12
|
+
interval: Flags.integer({ default: 1000 }),
|
|
13
|
+
};
|
|
14
|
+
async run() {
|
|
15
|
+
const { args, flags } = await this.parse(CanvasNodeRun);
|
|
16
|
+
const nodeIds = [...new Set([...(args.nodeId ? [args.nodeId] : []), ...(flags.node ?? [])])];
|
|
17
|
+
if (!nodeIds.length)
|
|
18
|
+
throw new Error('请提供 nodeId 或至少一个 --node');
|
|
19
|
+
const { api } = await commandApi(flags);
|
|
20
|
+
const submissions = await mapConcurrent(nodeIds, flags.concurrency, async (nodeId) => ({
|
|
21
|
+
nodeId,
|
|
22
|
+
result: await api.runNode(flags.project, nodeId),
|
|
23
|
+
}));
|
|
24
|
+
if (!flags.wait) {
|
|
25
|
+
this.print(nodeIds.length === 1 ? submissions[0].result : { submissions });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const tasks = submissions.filter((item) => item.result.task?.id);
|
|
29
|
+
const completed = await mapConcurrent(tasks, flags.concurrency, async (item) => ({
|
|
30
|
+
nodeId: item.nodeId,
|
|
31
|
+
task: await api.waitTask(item.result.task.id, flags.interval),
|
|
32
|
+
}));
|
|
33
|
+
this.print(nodeIds.length === 1 ? completed[0]?.task ?? submissions[0].result : { submissions, tasks: completed });
|
|
34
|
+
if (completed.some((item) => item.task.status !== 'completed'))
|
|
35
|
+
this.error('部分任务未成功完成', { exit: 8 });
|
|
36
|
+
}
|
|
11
37
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BaseCommand } from '../../../base-command.js';
|
|
2
|
+
export default class CanvasNodeUpdateMany extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static flags: {
|
|
5
|
+
project: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
|
+
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
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,48 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../../base-command.js';
|
|
3
|
+
import { commandApi, projectFlag, readJsonInput } from '../../../command-helpers.js';
|
|
4
|
+
export default class CanvasNodeUpdateMany extends BaseCommand {
|
|
5
|
+
static description = '一次读取 schema 并原子更新多个节点字段';
|
|
6
|
+
static flags = {
|
|
7
|
+
...BaseCommand.baseFlags,
|
|
8
|
+
project: projectFlag,
|
|
9
|
+
file: Flags.string({ description: '包含 updates 数组的 JSON 文件;省略时读取 stdin' }),
|
|
10
|
+
};
|
|
11
|
+
async run() {
|
|
12
|
+
const { flags } = await this.parse(CanvasNodeUpdateMany);
|
|
13
|
+
const input = await readJsonInput(flags.file);
|
|
14
|
+
if (!Array.isArray(input.updates))
|
|
15
|
+
throw new Error('输入必须包含 updates 数组');
|
|
16
|
+
const updates = input.updates.map(readUpdate);
|
|
17
|
+
const { api } = await commandApi(flags);
|
|
18
|
+
this.print(await api.updateNodes(flags.project, updates));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function readUpdate(value) {
|
|
22
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
23
|
+
throw new Error('updates 每一项必须是对象');
|
|
24
|
+
const update = value;
|
|
25
|
+
if (typeof update.nodeId !== 'string' || !update.nodeId)
|
|
26
|
+
throw new Error('updates.nodeId 不能为空');
|
|
27
|
+
if (update.set !== undefined && (!update.set || typeof update.set !== 'object' || Array.isArray(update.set))) {
|
|
28
|
+
throw new Error(`节点 ${update.nodeId} 的 set 必须是对象`);
|
|
29
|
+
}
|
|
30
|
+
if (update.unset !== undefined && (!Array.isArray(update.unset) || update.unset.some((field) => typeof field !== 'string'))) {
|
|
31
|
+
throw new Error(`节点 ${update.nodeId} 的 unset 必须是字符串数组`);
|
|
32
|
+
}
|
|
33
|
+
for (const coordinate of ['x', 'y']) {
|
|
34
|
+
if (update[coordinate] !== undefined && (typeof update[coordinate] !== 'number' || !Number.isFinite(update[coordinate]))) {
|
|
35
|
+
throw new Error(`节点 ${update.nodeId} 的 ${coordinate} 必须是数字`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (update.title !== undefined && typeof update.title !== 'string')
|
|
39
|
+
throw new Error(`节点 ${update.nodeId} 的 title 必须是字符串`);
|
|
40
|
+
return {
|
|
41
|
+
nodeId: update.nodeId,
|
|
42
|
+
set: update.set,
|
|
43
|
+
unset: update.unset,
|
|
44
|
+
title: update.title,
|
|
45
|
+
x: update.x,
|
|
46
|
+
y: update.y,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -6,6 +6,11 @@ export default class CanvasNodeUpdate extends BaseCommand {
|
|
|
6
6
|
static flags: {
|
|
7
7
|
project: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
8
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
set: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
unset: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
title: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
x: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
y: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
14
|
'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
15
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
16
|
locale: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|