@mindscraft/branch-video-agent-cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Branch Video Agent CLI
2
2
 
3
- `@mindscraft/branch-video-agent-cli` 是一个可脱离当前代码仓库使用的发布版 CLI,用于通过 Bearer agent token 调用 branch-video 与 AIHub agent API。
3
+ `@mindscraft/branch-video-agent-cli` 是一个可脱离当前代码仓库使用的发布版 CLI,用于通过 Bearer agent token 调用 branch-video、AIHub workflow AI Gateway agent API。
4
4
 
5
5
  ## 安装与执行
6
6
 
@@ -34,6 +34,8 @@ branch-video-agent --help
34
34
  - `AIHUB_AGENT_TOKEN`
35
35
  - `BRANCH_VIDEO_AGENT_BASE_URL`
36
36
 
37
+ AI Gateway 调用同样只传 `AIHUB_AGENT_TOKEN`;服务端使用该 token 记录上的独立 `AiGatewayKey`,不能复用 AIHub `projectKey`。
38
+
37
39
  最小调用示例:
38
40
 
39
41
  ```powershell
@@ -46,6 +48,21 @@ branch-video-agent --help
46
48
  '@ | npm exec --yes --package=@mindscraft/branch-video-agent-cli branch-video-agent -- project list
47
49
  ```
48
50
 
51
+ AIHub 导入公共库示例:
52
+
53
+ ```powershell
54
+ @'
55
+ {
56
+ "global_context_str": "{\"project_title\":\"AIHub 导入项目\"}",
57
+ "script_body_str": "{\"project_title\":\"AIHub 导入项目\"}",
58
+ "result_script_str": "[]",
59
+ "workflow_run_id": "00000000-0000-4000-8000-000000000000"
60
+ }
61
+ '@ | npm exec --yes --package=@mindscraft/branch-video-agent-cli branch-video-agent -- project import-aihub
62
+ ```
63
+
64
+ 该命令与其他 `project` / `version` / `production` 命令一样,统一使用 `AIHUB_AGENT_TOKEN` Bearer 鉴权;服务端会兼容旧的 `X-AIHub-Import-Token` 头,但 CLI 不再单独管理第二套 token。
65
+
49
66
  托管互动方案提交示例:
50
67
 
51
68
  ```powershell
@@ -64,6 +81,65 @@ branch-video-agent --help
64
81
  '@ | npm exec --yes --package=@mindscraft/branch-video-agent-cli branch-video-agent -- preset upsert
65
82
  ```
66
83
 
84
+ 角色资产库查询示例:
85
+
86
+ ```powershell
87
+ @'
88
+ {
89
+ "query": "喵星人 科学老师 实验服",
90
+ "tags": ["喵星人IP", "科学"],
91
+ "limit": 10
92
+ }
93
+ '@ | npm exec --yes --package=@mindscraft/branch-video-agent-cli branch-video-agent -- character search
94
+ ```
95
+
96
+ 角色资产入库示例:
97
+
98
+ ```powershell
99
+ @'
100
+ {
101
+ "externalId": "workflow-run-123:meow_teacher",
102
+ "name": "喵星科学老师",
103
+ "description": "穿白色实验服的喵星人科学老师,适合科学实验讲解。",
104
+ "tags": ["喵星人IP", "科学", "老师"],
105
+ "referenceImages": [{ "url": "https://example.com/meow-teacher.png" }],
106
+ "actions": [
107
+ {
108
+ "actionKey": "speaking",
109
+ "mediaType": "video",
110
+ "url": "https://example.com/meow-teacher-speaking.mp4"
111
+ }
112
+ ]
113
+ }
114
+ '@ | npm exec --yes --package=@mindscraft/branch-video-agent-cli branch-video-agent -- character create
115
+ ```
116
+
117
+ `character search` 只返回已经人工添加 `accepted_for_reuse` 标签的角色资产;新入库资产默认未验收,需要在角色资产库页面验收后才会被 Agent 二次复用。
118
+
119
+ AI Gateway 图片生成示例:
120
+
121
+ ```powershell
122
+ @'
123
+ {
124
+ "model": "doubao-seedream-5-0-260128",
125
+ "prompt": "3D 卡通风格,穿白大褂的喵星人科学老师在实验室讲解电路"
126
+ }
127
+ '@ | npm exec --yes --package=@mindscraft/branch-video-agent-cli branch-video-agent -- ai-gateway image-generate -- --raw
128
+ ```
129
+
130
+ AI Gateway 视频生成并等待示例:
131
+
132
+ ```powershell
133
+ @'
134
+ {
135
+ "model": "doubao-seedance-2-0-260128",
136
+ "input": {
137
+ "prompt": "镜头推进到实验台,卡通猫老师点亮一个小灯泡"
138
+ }
139
+ }
140
+ '@ | npm exec --yes --package=@mindscraft/branch-video-agent-cli branch-video-agent -- ai-gateway video-create -- --wait --raw
141
+ ```
142
+
67
143
  ## 在仓库内构建发布包
68
144
 
69
145
  从仓库根目录执行:
@@ -97,4 +173,4 @@ npm publish
97
173
  发布前请先确认两件事:
98
174
 
99
175
  1. 你在 npmjs 上拥有当前包名对应的 scope 权限;如果没有 `@nd` scope,需要先改包名,例如改成你实际拥有的 scope,或改成非 scope 包名。
100
- 2. 如果要让中国大陆用户安装体验更稳定,建议仍以 npmjs 作为唯一发布源,再让用户通过 `npmmirror` 安装;不要把 GitHub Packages 当作主发布仓库,因为安装门槛和网络可达性都更差。
176
+ 2. 如果要让中国大陆用户安装体验更稳定,建议仍以 npmjs 作为唯一发布源,再让用户通过 `npmmirror` 安装;不要把 GitHub Packages 当作主发布仓库,因为安装门槛和网络可达性都更差。
@@ -0,0 +1,2 @@
1
+ import type { CommandHandler } from '../lib/types.js';
2
+ export declare const aiGatewayCommands: Record<string, CommandHandler>;
@@ -0,0 +1,181 @@
1
+ import { CliCommandError } from '../lib/errors.js';
2
+ import { getOptionalString, getRequiredString, omitKeys, pickDefined } from '../lib/payload.js';
3
+ const gatewaySuccessStatuses = new Set(['succeeded', 'success', 'done']);
4
+ const gatewayPendingStatuses = new Set(['queued', 'running', 'pending', 'text_success', 'first_success']);
5
+ function ensureObject(value, fieldName) {
6
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
7
+ throw new CliCommandError(`${fieldName} must be a JSON object`, 'VALIDATION_ERROR');
8
+ }
9
+ return value;
10
+ }
11
+ function normalizeOperation(value) {
12
+ return value.trim().replace(/^\/+/, '');
13
+ }
14
+ function extractTaskId(value) {
15
+ const record = value && typeof value === 'object' ? value : {};
16
+ const candidates = [
17
+ record.id,
18
+ record.taskId,
19
+ record.task_id,
20
+ record.data?.id,
21
+ record.data?.taskId,
22
+ record.data?.task_id,
23
+ record.data?.response?.id,
24
+ record.data?.response?.taskId,
25
+ record.data?.response?.task_id,
26
+ ];
27
+ const taskId = candidates.find((candidate) => typeof candidate === 'string' && candidate.trim());
28
+ if (!taskId) {
29
+ throw new CliCommandError('AI Gateway response did not include a task id', 'VALIDATION_ERROR', value);
30
+ }
31
+ return String(taskId).trim();
32
+ }
33
+ function extractStatus(value) {
34
+ const record = value && typeof value === 'object' ? value : {};
35
+ const status = record.data?.status ?? record.data?.successFlag ?? record.status;
36
+ return typeof status === 'string' ? status.trim().toLowerCase() : '';
37
+ }
38
+ function extractFailureMessage(value) {
39
+ const record = value && typeof value === 'object' ? value : {};
40
+ return String(record.error?.message
41
+ || record.data?.errorMessage
42
+ || record.data?.error?.message
43
+ || record.error
44
+ || 'AI Gateway task failed');
45
+ }
46
+ async function waitForGatewayTask(context, submit, poll) {
47
+ const startedAt = context.runtime.now();
48
+ while (context.runtime.now() - startedAt <= context.flags.timeoutMs) {
49
+ const statusResult = await poll();
50
+ const status = extractStatus(statusResult.data);
51
+ if (gatewaySuccessStatuses.has(status)) {
52
+ return {
53
+ data: {
54
+ submit,
55
+ status: statusResult.data,
56
+ },
57
+ requestId: statusResult.requestId,
58
+ };
59
+ }
60
+ if (status && !gatewayPendingStatuses.has(status)) {
61
+ throw new CliCommandError(extractFailureMessage(statusResult.data), 'RUN_FAILED', { submit, status: statusResult.data }, statusResult.requestId || null);
62
+ }
63
+ await context.runtime.sleep(context.flags.pollIntervalMs);
64
+ }
65
+ throw new CliCommandError('AI Gateway wait timed out', 'TIMEOUT', { submit });
66
+ }
67
+ function normalizeImageEditPayload(payload) {
68
+ if (payload.fields || payload.files)
69
+ return payload;
70
+ const files = {};
71
+ if (payload.image !== undefined)
72
+ files.image = payload.image;
73
+ if (payload.mask !== undefined)
74
+ files.mask = payload.mask;
75
+ return {
76
+ fields: omitKeys(payload, ['image', 'mask']),
77
+ files,
78
+ };
79
+ }
80
+ function inferMusicQueryOperation(operation) {
81
+ switch (operation) {
82
+ case 'vocal-removal':
83
+ return 'vocal-removal';
84
+ case 'wav':
85
+ return 'wav';
86
+ case 'lyrics':
87
+ return 'lyrics';
88
+ case 'mp4':
89
+ return 'mp4';
90
+ default:
91
+ return 'query';
92
+ }
93
+ }
94
+ async function getMusicStatus(context, operation, id) {
95
+ return context.client.request(`/api/agent/ai-gateway/music/${encodeURIComponent(operation)}`, {
96
+ method: 'GET',
97
+ query: { id },
98
+ });
99
+ }
100
+ export const aiGatewayCommands = {
101
+ capabilities: async ({ client }) => {
102
+ const result = await client.request('/api/agent/ai-gateway/capabilities', {
103
+ method: 'GET',
104
+ });
105
+ return { data: result.data, requestId: result.requestId };
106
+ },
107
+ request: async ({ client, payload }) => {
108
+ const path = getRequiredString(payload, 'path');
109
+ const method = getOptionalString(payload, 'method') || 'POST';
110
+ const result = await client.request('/api/agent/ai-gateway/request', {
111
+ method: 'POST',
112
+ body: {
113
+ method,
114
+ path,
115
+ query: payload.query === undefined ? undefined : ensureObject(payload.query, 'query'),
116
+ headers: payload.headers === undefined ? undefined : ensureObject(payload.headers, 'headers'),
117
+ body: payload.body,
118
+ },
119
+ });
120
+ return { data: result.data, requestId: result.requestId };
121
+ },
122
+ 'image-generate': async ({ client, payload }) => {
123
+ const result = await client.request('/api/agent/ai-gateway/images/generations', {
124
+ method: 'POST',
125
+ body: payload,
126
+ });
127
+ return { data: result.data, requestId: result.requestId };
128
+ },
129
+ 'image-edit': async ({ client, payload }) => {
130
+ const result = await client.request('/api/agent/ai-gateway/images/edits', {
131
+ method: 'POST',
132
+ body: normalizeImageEditPayload(payload),
133
+ });
134
+ return { data: result.data, requestId: result.requestId };
135
+ },
136
+ 'video-create': async (context) => {
137
+ const result = await context.client.request('/api/agent/ai-gateway/predictions', {
138
+ method: 'POST',
139
+ body: context.payload,
140
+ });
141
+ if (!context.flags.wait) {
142
+ return { data: result.data, requestId: result.requestId };
143
+ }
144
+ const taskId = extractTaskId(result.data);
145
+ const query = pickDefined(context.payload, ['model']);
146
+ return waitForGatewayTask(context, result.data, () => context.client.request(`/api/agent/ai-gateway/predictions/${encodeURIComponent(taskId)}`, { method: 'GET', query }));
147
+ },
148
+ 'video-get': async ({ client, payload }) => {
149
+ const taskId = getRequiredString(payload, 'taskId');
150
+ const result = await client.request(`/api/agent/ai-gateway/predictions/${encodeURIComponent(taskId)}`, {
151
+ method: 'GET',
152
+ query: pickDefined(payload, ['model']),
153
+ });
154
+ return { data: result.data, requestId: result.requestId };
155
+ },
156
+ 'music-submit': async (context) => {
157
+ const operation = normalizeOperation(getRequiredString(context.payload, 'operation'));
158
+ const body = context.payload.body === undefined
159
+ ? omitKeys(context.payload, ['operation', 'queryOperation'])
160
+ : ensureObject(context.payload.body, 'body');
161
+ const result = await context.client.request(`/api/agent/ai-gateway/music/${encodeURIComponent(operation)}`, {
162
+ method: 'POST',
163
+ body,
164
+ });
165
+ if (!context.flags.wait) {
166
+ return { data: result.data, requestId: result.requestId };
167
+ }
168
+ const taskId = extractTaskId(result.data);
169
+ const queryOperation = normalizeOperation(getOptionalString(context.payload, 'queryOperation') || inferMusicQueryOperation(operation));
170
+ return waitForGatewayTask(context, result.data, () => getMusicStatus(context, queryOperation, taskId));
171
+ },
172
+ 'music-get': async ({ client, payload }) => {
173
+ const id = getRequiredString(payload, 'id');
174
+ const operation = normalizeOperation(getOptionalString(payload, 'operation') || 'query');
175
+ const result = await client.request(`/api/agent/ai-gateway/music/${encodeURIComponent(operation)}`, {
176
+ method: 'GET',
177
+ query: { id },
178
+ });
179
+ return { data: result.data, requestId: result.requestId };
180
+ },
181
+ };
@@ -0,0 +1,2 @@
1
+ import type { CommandHandler } from '../lib/types.js';
2
+ export declare const characterCommands: Record<string, CommandHandler>;
@@ -0,0 +1,48 @@
1
+ import { getRequiredString, pickDefined } from '../lib/payload.js';
2
+ const ACCEPTED_FOR_REUSE_TAG = 'accepted_for_reuse';
3
+ function normalizeTags(value) {
4
+ const raw = Array.isArray(value)
5
+ ? value
6
+ : typeof value === 'string'
7
+ ? value.split(',')
8
+ : [];
9
+ return raw
10
+ .map((item) => String(item || '').trim())
11
+ .filter(Boolean)
12
+ .filter((item, index, list) => list.indexOf(item) === index);
13
+ }
14
+ function tagsForReuseSearch(value) {
15
+ return [...normalizeTags(value), ACCEPTED_FOR_REUSE_TAG]
16
+ .filter((item, index, list) => list.indexOf(item) === index);
17
+ }
18
+ export const characterCommands = {
19
+ search: async ({ client, payload }) => {
20
+ const tags = tagsForReuseSearch(payload.tags);
21
+ const result = await client.request('/api/branch-video/character-assets', {
22
+ method: 'GET',
23
+ query: {
24
+ ...pickDefined(payload, ['query', 'page', 'limit']),
25
+ tags: tags.join(','),
26
+ },
27
+ });
28
+ return { data: result.data, requestId: result.requestId };
29
+ },
30
+ create: async ({ client, payload }) => {
31
+ const externalId = getRequiredString(payload, 'externalId');
32
+ const result = await client.request('/api/branch-video/character-assets', {
33
+ method: 'POST',
34
+ body: {
35
+ ...payload,
36
+ externalId,
37
+ },
38
+ });
39
+ return { data: result.data, requestId: result.requestId };
40
+ },
41
+ get: async ({ client, payload }) => {
42
+ const id = getRequiredString(payload, 'id');
43
+ const result = await client.request(`/api/branch-video/character-assets/${encodeURIComponent(id)}`, {
44
+ method: 'GET',
45
+ });
46
+ return { data: result.data, requestId: result.requestId };
47
+ },
48
+ };
@@ -21,6 +21,13 @@ export const projectCommands = {
21
21
  });
22
22
  return { data: result.data, requestId: result.requestId };
23
23
  },
24
+ 'import-aihub': async ({ client, payload }) => {
25
+ const result = await client.request('/api/branch-video/import/aihub', {
26
+ method: 'POST',
27
+ body: payload,
28
+ });
29
+ return { data: result.data, requestId: result.requestId };
30
+ },
24
31
  update: async ({ client, payload }) => {
25
32
  const projectId = getRequiredString(payload, 'projectId');
26
33
  const result = await client.request(`/api/branch-video/${encodeURIComponent(projectId)}`, {
package/dist/index.js CHANGED
@@ -3,6 +3,8 @@ import { productionCommands } from './commands/production.js';
3
3
  import { projectCommands } from './commands/project.js';
4
4
  import { versionCommands } from './commands/version.js';
5
5
  import { workflowCommands } from './commands/workflow.js';
6
+ import { characterCommands } from './commands/character.js';
7
+ import { aiGatewayCommands } from './commands/aiGateway.js';
6
8
  import { CliCommandError } from './lib/errors.js';
7
9
  import { getCliHelpText, parseCliArgs, resolveCliConfig } from './lib/flags.js';
8
10
  import { CliHttpClient } from './lib/http.js';
@@ -14,6 +16,8 @@ const commandRegistry = {
14
16
  project: projectCommands,
15
17
  version: versionCommands,
16
18
  production: productionCommands,
19
+ character: characterCommands,
20
+ 'ai-gateway': aiGatewayCommands,
17
21
  };
18
22
  function normalizeError(error) {
19
23
  if (error instanceof CliCommandError) {
package/dist/lib/flags.js CHANGED
@@ -41,6 +41,8 @@ export function parseCliArgs(argv) {
41
41
  };
42
42
  for (let index = 0; index < argv.length; index += 1) {
43
43
  const arg = argv[index];
44
+ if (arg === '--')
45
+ continue;
44
46
  if (!arg.startsWith('--')) {
45
47
  positionals.push(arg);
46
48
  continue;
@@ -93,9 +95,11 @@ export function getCliHelpText() {
93
95
  'Groups:',
94
96
  ' preset list | upsert',
95
97
  ' workflow run | status | outputs',
96
- ' project list | get | create | update | delete',
98
+ ' project list | get | create | import-aihub | update | delete',
97
99
  ' version list | get | publish',
98
100
  ' production schema | start | status | retry',
101
+ ' character search | create | get',
102
+ ' ai-gateway capabilities | request | image-generate | image-edit | video-create | video-get | music-submit | music-get',
99
103
  '',
100
104
  'Config precedence:',
101
105
  ' AIHUB_AGENT_TOKEN / BRANCH_VIDEO_AGENT_BASE_URL > stdin JSON or --token-file > command flags',
package/dist/lib/http.js CHANGED
@@ -14,6 +14,7 @@ function buildUrl(baseUrl, path, query) {
14
14
  function extractRequestId(response, parsed) {
15
15
  return response.headers.get('x-request-id')
16
16
  || response.headers.get('x-correlation-id')
17
+ || response.headers.get('x-ai-gateway-request-id')
17
18
  || (typeof parsed?.requestId === 'string' ? parsed.requestId : null);
18
19
  }
19
20
  export class CliHttpClient {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindscraft/branch-video-agent-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Published CLI for branch-video and AIHub agent APIs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,4 +33,4 @@
33
33
  "prepack": "npm run build",
34
34
  "smoke:help": "npm run build && node dist/bin.js --help"
35
35
  }
36
- }
36
+ }