@aswless_854771076/ai_short_studio_cli 0.1.64 → 0.1.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +35 -2
  2. package/dist/client/api-client.js +19 -2
  3. package/dist/commands/canvas/asset/select.d.ts +2 -1
  4. package/dist/commands/canvas/asset/select.js +22 -3
  5. package/dist/commands/canvas/edge/list.js +4 -1
  6. package/dist/commands/canvas/node/list.d.ts +4 -0
  7. package/dist/commands/canvas/node/list.js +43 -2
  8. package/dist/commands/canvas/node/run.d.ts +3 -0
  9. package/dist/commands/canvas/node/run.js +103 -5
  10. package/dist/commands/config/ai-provider/capabilities.d.ts +26 -0
  11. package/dist/commands/config/ai-provider/capabilities.js +90 -0
  12. package/dist/commands/config/ai-provider/get.d.ts +21 -0
  13. package/dist/commands/config/ai-provider/get.js +21 -0
  14. package/dist/commands/config/ai-provider/list.d.ts +20 -0
  15. package/dist/commands/config/ai-provider/list.js +19 -0
  16. package/dist/commands/config/ai-provider/models.d.ts +21 -0
  17. package/dist/commands/config/ai-provider/models.js +21 -0
  18. package/dist/commands/config/ai-provider/set.d.ts +40 -0
  19. package/dist/commands/config/ai-provider/set.js +65 -0
  20. package/dist/commands/config/ai-provider/test.d.ts +24 -0
  21. package/dist/commands/config/ai-provider/test.js +24 -0
  22. package/dist/commands/config/set.d.ts +1 -0
  23. package/dist/commands/config/set.js +55 -2
  24. package/dist/commands/task/wait.d.ts +1 -0
  25. package/dist/commands/task/wait.js +6 -1
  26. package/dist/domain/ai-provider-capabilities.d.ts +109 -0
  27. package/dist/domain/ai-provider-capabilities.js +180 -0
  28. package/dist/domain/ai-provider-command-helpers.d.ts +27 -0
  29. package/dist/domain/ai-provider-command-helpers.js +55 -0
  30. package/dist/domain/vvicat-api.d.ts +42 -2
  31. package/dist/domain/vvicat-api.js +56 -3
  32. package/dist/preflight.js +9 -0
  33. package/package.json +1 -1
  34. package/skills/using-vvicat-ai-short-studio-cli/SKILL.md +15 -1
  35. package/skills/using-vvicat-ai-short-studio-cli/references/commands/project.md +20 -1
  36. package/skills/using-vvicat-ai-short-studio-cli/references/project-and-canvas.md +1 -1
@@ -0,0 +1,40 @@
1
+ import { BaseCommand } from '../../../base-command.js';
2
+ /**
3
+ * vvicat config ai-provider set <providerId> [...flags]
4
+ *
5
+ * 写入 provider 配置。两类使用方式:
6
+ *
7
+ * 1. 命令行 flag(最常见):
8
+ * vvicat config ai-provider set comfyui \
9
+ * --base-url http://127.0.0.1:8188 \
10
+ * --enabled true \
11
+ * --protocol comfyui-official
12
+ *
13
+ * 2. JSON 文件(复杂场景,如 openai-compat 协议全套字段):
14
+ * vvicat config ai-provider set openai --file openai-config.json
15
+ * cat openai-config.json | vvicat config ai-provider set openai --file -
16
+ *
17
+ * `--file` 模式时:JSON 内容会作为完整 body 提交,要求含 `type` + `name`;CLI 不再叠加 flag。
18
+ */
19
+ export default class ConfigAiProviderSet extends BaseCommand {
20
+ static description: string;
21
+ static args: {
22
+ providerId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
23
+ };
24
+ static flags: {
25
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
26
+ 'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
27
+ 'api-mode': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
28
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
29
+ protocol: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
30
+ enabled: import("@oclif/core/interfaces").BooleanFlag<boolean>;
31
+ 'sort-order': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
32
+ file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
33
+ create: import("@oclif/core/interfaces").BooleanFlag<boolean>;
34
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
35
+ locale: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
36
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
37
+ episode: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
38
+ };
39
+ run(): Promise<void>;
40
+ }
@@ -0,0 +1,65 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base-command.js';
3
+ import { commandApi, readJsonInput } from '../../../command-helpers.js';
4
+ import { composeBodyFromFlags } from '../../../domain/ai-provider-command-helpers.js';
5
+ /**
6
+ * vvicat config ai-provider set <providerId> [...flags]
7
+ *
8
+ * 写入 provider 配置。两类使用方式:
9
+ *
10
+ * 1. 命令行 flag(最常见):
11
+ * vvicat config ai-provider set comfyui \
12
+ * --base-url http://127.0.0.1:8188 \
13
+ * --enabled true \
14
+ * --protocol comfyui-official
15
+ *
16
+ * 2. JSON 文件(复杂场景,如 openai-compat 协议全套字段):
17
+ * vvicat config ai-provider set openai --file openai-config.json
18
+ * cat openai-config.json | vvicat config ai-provider set openai --file -
19
+ *
20
+ * `--file` 模式时:JSON 内容会作为完整 body 提交,要求含 `type` + `name`;CLI 不再叠加 flag。
21
+ */
22
+ export default class ConfigAiProviderSet extends BaseCommand {
23
+ static description = '配置 AI provider 的 baseUrl / apiKey / 协议 / enabled 等字段';
24
+ static args = {
25
+ providerId: Args.string({ required: true, description: 'provider ID' }),
26
+ };
27
+ static flags = {
28
+ ...BaseCommand.baseFlags,
29
+ 'base-url': Flags.string({ description: 'provider 服务地址(如 http://127.0.0.1:8188)' }),
30
+ 'api-key': Flags.string({ description: 'provider API key(仅写入;不会回显)' }),
31
+ 'api-mode': Flags.string({ description: 'apiMode(gateway 协议族字段,如 openai / comfyui-official)' }),
32
+ name: Flags.string({ description: 'provider 展示名(默认沿用 providerId)' }),
33
+ protocol: Flags.string({ description: 'protocolMode(openai / comfyui-official / runninghub)' }),
34
+ enabled: Flags.boolean({ description: 'true 启用;false 禁用', allowNo: true }),
35
+ 'sort-order': Flags.integer({ description: '排序权重(数字越小越靠前)' }),
36
+ file: Flags.string({ description: '完整 JSON body;省略时从 stdin 读取' }),
37
+ create: Flags.boolean({ description: '强制走 POST 创建(默认 PATCH 404 时自动回退到 create)', default: false }),
38
+ };
39
+ async run() {
40
+ const { args, flags } = await this.parse(ConfigAiProviderSet);
41
+ const { api } = await commandApi(flags);
42
+ const body = flags.file !== undefined
43
+ ? await readJsonInput(flags.file === '-' ? undefined : flags.file)
44
+ : composeBodyFromFlags(args.providerId, flags);
45
+ if (typeof body.type !== 'string' || !body.type.trim()) {
46
+ this.error('set 请求 body 必须包含 type 字段(provider 类型,如 comfyui / openai)');
47
+ }
48
+ if (flags.create) {
49
+ this.print(await api.createAiProvider(body));
50
+ return;
51
+ }
52
+ try {
53
+ this.print(await api.updateAiProvider(args.providerId, body));
54
+ }
55
+ catch (error) {
56
+ // PATCH 404 → 不存在,自动回退到 create(更友好的 UX)
57
+ const status = error.status;
58
+ if (status === 404) {
59
+ this.print(await api.createAiProvider(body));
60
+ return;
61
+ }
62
+ throw error;
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,24 @@
1
+ import { BaseCommand } from '../../../base-command.js';
2
+ /**
3
+ * vvicat config ai-provider test <providerId>
4
+ *
5
+ * 触发服务端连通性探测:
6
+ * - comfyui: GET baseUrl/system_stats
7
+ * - 其他 OpenAI 兼容: GET baseUrl/models 带 Bearer token
8
+ *
9
+ * 返回 { success, steps, durationMs }。CLI 原样转发。
10
+ */
11
+ export default class ConfigAiProviderTest extends BaseCommand {
12
+ static description: string;
13
+ static args: {
14
+ providerId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
15
+ };
16
+ static flags: {
17
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
+ locale: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
20
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
+ episode: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
22
+ };
23
+ run(): Promise<void>;
24
+ }
@@ -0,0 +1,24 @@
1
+ import { Args } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base-command.js';
3
+ import { commandApi } from '../../../command-helpers.js';
4
+ /**
5
+ * vvicat config ai-provider test <providerId>
6
+ *
7
+ * 触发服务端连通性探测:
8
+ * - comfyui: GET baseUrl/system_stats
9
+ * - 其他 OpenAI 兼容: GET baseUrl/models 带 Bearer token
10
+ *
11
+ * 返回 { success, steps, durationMs }。CLI 原样转发。
12
+ */
13
+ export default class ConfigAiProviderTest extends BaseCommand {
14
+ static description = '触发服务端连通性探测(GET /models 或 /system_stats)';
15
+ static args = {
16
+ providerId: Args.string({ required: true, description: 'provider ID' }),
17
+ };
18
+ static flags = BaseCommand.baseFlags;
19
+ async run() {
20
+ const { args, flags } = await this.parse(ConfigAiProviderTest);
21
+ const { api } = await commandApi(flags);
22
+ this.print(await api.testAiProvider(args.providerId));
23
+ }
24
+ }
@@ -2,6 +2,7 @@ import { BaseCommand } from '../../base-command.js';
2
2
  export default class ConfigSet extends BaseCommand {
3
3
  static flags: {
4
4
  file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
5
+ verify: import("@oclif/core/interfaces").BooleanFlag<boolean>;
5
6
  'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
7
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
8
  locale: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
@@ -1,7 +1,60 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { BaseCommand } from '../../base-command.js';
3
3
  import { commandApi, readJsonInput } from '../../command-helpers.js';
4
+ // 服务端 GET /api/user/api-config 会自动"修复"无效的 defaultModels;客户端拿到
5
+ // success=true 但实际配置可能被覆盖。--verify 在 set 成功后立刻 GET 一次,
6
+ // 输出"用户请求值 vs 服务端实际值" diff,让 agent 能看到真相。
7
+ // 不影响默认行为;只是给 agent 一个验证工具。
4
8
  export default class ConfigSet extends BaseCommand {
5
- static flags = { ...BaseCommand.baseFlags, file: Flags.string({ description: '配置 JSON;省略时读取 stdin' }) };
6
- async run() { const { flags } = await this.parse(ConfigSet); const { api } = await commandApi(flags); this.print(await api.updateConfig(await readJsonInput(flags.file))); }
9
+ static flags = {
10
+ ...BaseCommand.baseFlags,
11
+ file: Flags.string({ description: '配置 JSON;省略时读取 stdin' }),
12
+ verify: Flags.boolean({
13
+ description: 'set 成功后立刻 GET 一次,diff 用户请求值 vs 服务端实际值。默认 false。',
14
+ default: false,
15
+ }),
16
+ };
17
+ async run() {
18
+ const { flags } = await this.parse(ConfigSet);
19
+ const { api } = await commandApi(flags);
20
+ const requested = await readJsonInput(flags.file);
21
+ const result = await api.updateConfig(requested);
22
+ if (!flags.verify) {
23
+ this.print(result);
24
+ return;
25
+ }
26
+ // --verify:再 GET 一次,把 effective vs requested diff 出来。
27
+ // 服务端会修复 invalid defaultModels / capabilityDefaults,所以 effective
28
+ // 可能 ≠ requested。这里**只递归遍历 requested 的 keys**,避免 effective
29
+ // 中多出的 models / providers / capabilityDefaults 等造成噪声。
30
+ const effective = (await api.config());
31
+ const diff = diffJsonRequested(requested, effective, '');
32
+ this.print({ requested, effective, diff });
33
+ }
34
+ }
35
+ // 只递归遍历 requested 的 keys;effective 中多余 keys 不输出。
36
+ // 这样客户端能直接看到"用户请求的字段"与"服务端实际值"的差异,不会被服务端
37
+ // 补充的默认值 / 自动修复字段污染 diff。
38
+ function diffJsonRequested(requested, effective, prefix) {
39
+ const out = [];
40
+ for (const key of Object.keys(requested ?? {})) {
41
+ const path = prefix ? `${prefix}.${key}` : key;
42
+ const r = (requested ?? {})[key];
43
+ const e = (effective ?? {})[key];
44
+ if (r === undefined)
45
+ continue;
46
+ if (e === undefined) {
47
+ out.push(`${path}: requested=${JSON.stringify(r)}, effective=<missing>`);
48
+ }
49
+ else if (isPlainObject(r) && isPlainObject(e)) {
50
+ out.push(...diffJsonRequested(r, e, path));
51
+ }
52
+ else if (JSON.stringify(r) !== JSON.stringify(e)) {
53
+ out.push(`${path}: requested=${JSON.stringify(r)}, effective=${JSON.stringify(e)}`);
54
+ }
55
+ }
56
+ return out;
57
+ }
58
+ function isPlainObject(value) {
59
+ return !!value && typeof value === 'object' && !Array.isArray(value);
7
60
  }
@@ -7,6 +7,7 @@ export default class TaskWait extends BaseCommand {
7
7
  task: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
8
  concurrency: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
9
9
  interval: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'max-wait-ms': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
10
11
  'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
12
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
13
  locale: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
@@ -8,6 +8,10 @@ export default class TaskWait extends BaseCommand {
8
8
  task: Flags.string({ description: '任务 ID,可重复指定以批量等待', multiple: true }),
9
9
  concurrency: Flags.integer({ description: '批量轮询并发数', default: 4, min: 1, max: 16 }),
10
10
  interval: Flags.integer({ default: 1000 }),
11
+ 'max-wait-ms': Flags.integer({
12
+ description: '单次 waitTask 最大等待毫秒数;0 表示不超时(保留旧行为)。',
13
+ default: 0,
14
+ }),
11
15
  };
12
16
  async run() {
13
17
  const { args, flags } = await this.parse(TaskWait);
@@ -15,7 +19,8 @@ export default class TaskWait extends BaseCommand {
15
19
  if (!taskIds.length)
16
20
  throw new Error('请提供 taskId 或至少一个 --task');
17
21
  const { api } = await commandApi(flags);
18
- const tasks = await mapConcurrent(taskIds, flags.concurrency, async (taskId) => await api.waitTask(taskId, flags.interval));
22
+ const maxWaitMs = flags['max-wait-ms'] > 0 ? flags['max-wait-ms'] : Number.POSITIVE_INFINITY;
23
+ const tasks = await mapConcurrent(taskIds, flags.concurrency, async (taskId) => await api.waitTask(taskId, flags.interval, { maxWaitMs }));
19
24
  this.print(tasks.length === 1 ? tasks[0] : { tasks });
20
25
  if (tasks.some((task) => task.status !== 'completed'))
21
26
  this.error('部分任务未成功完成', { exit: 8 });
@@ -0,0 +1,109 @@
1
+ /**
2
+ * vvicat config ai-provider capabilities 命令的聚合视图:
3
+ * 把服务端返回的 providers / catalog / user-config / reachability
4
+ * 合并成一张 agent 可读的"我能跑什么 / 缺什么"结构。
5
+ *
6
+ * 设计目标:
7
+ * - 单一入口;agent 一次拿到所有决策所需信息。
8
+ * - 畸形输入不抛错(catalogModels / defaultModels 可能因为网络/服务端 bug 缺数据)。
9
+ * - 字段命名稳定(生产数据契约);JSON 字段不要随意改名。
10
+ */
11
+ export type ReachabilityState = 'ok' | 'failed' | 'skipped' | 'unknown';
12
+ export interface ProviderCapabilityRow {
13
+ id: string;
14
+ name: string;
15
+ type: string;
16
+ enabled: boolean;
17
+ baseUrl: string | null;
18
+ apiKeyMask: string | null;
19
+ hasApiKey: boolean;
20
+ protocolMode: string | null;
21
+ lastTestResult: 'pass' | 'fail' | null;
22
+ lastResponseMs: number | null;
23
+ upstreamModels?: unknown[];
24
+ upstreamModelsCount: number;
25
+ upstreamModelsSyncedAt: string | null;
26
+ reachability: ReachabilityState;
27
+ reachabilityDetail?: string;
28
+ }
29
+ export interface ModelCapabilityRow {
30
+ modelKey: string;
31
+ provider: string;
32
+ type: 'image' | 'video' | 'audio' | 'llm' | 'tts';
33
+ enabled: boolean;
34
+ capabilityOptions: Record<string, unknown>;
35
+ }
36
+ export interface MissingPiece {
37
+ capability: string;
38
+ modelKey?: string;
39
+ reason: string;
40
+ fixHint?: string;
41
+ }
42
+ export interface CapabilitiesReport {
43
+ generatedAt: string;
44
+ baseUrl: string;
45
+ summary: {
46
+ totalProviders: number;
47
+ enabledProviders: number;
48
+ reachableProviders: number;
49
+ catalogModelCount: number;
50
+ capabilityCount: number;
51
+ };
52
+ providers: ProviderCapabilityRow[];
53
+ capabilities: {
54
+ imageModel: ModelCapabilityRow[];
55
+ videoModel: ModelCapabilityRow[];
56
+ audioModel: ModelCapabilityRow[];
57
+ analysisModel: ModelCapabilityRow[];
58
+ characterModel: ModelCapabilityRow[];
59
+ locationModel: ModelCapabilityRow[];
60
+ storyboardModel: ModelCapabilityRow[];
61
+ };
62
+ userDefaults: {
63
+ imageModel: string | null;
64
+ videoModel: string | null;
65
+ audioModel: string | null;
66
+ analysisModel: string | null;
67
+ characterModel: string | null;
68
+ locationModel: string | null;
69
+ storyboardModel: string | null;
70
+ };
71
+ reachability: Record<string, {
72
+ ok: boolean;
73
+ detail?: string;
74
+ durationMs?: number;
75
+ }>;
76
+ missingPieces: MissingPiece[];
77
+ }
78
+ export interface ProviderCapabilitiesInput {
79
+ baseUrl: string;
80
+ providers: Array<{
81
+ id: string;
82
+ name: string;
83
+ type: string;
84
+ enabled: boolean;
85
+ baseUrl: string | null;
86
+ apiKeyMask: string | null;
87
+ hasApiKey: boolean;
88
+ protocolMode: string | null;
89
+ lastTestResult: 'pass' | 'fail' | null;
90
+ lastResponseMs: number | null;
91
+ upstreamModels?: unknown[];
92
+ upstreamModelsSyncedAt: string | null;
93
+ }>;
94
+ catalogModels: Array<{
95
+ modelKey: string;
96
+ provider: string;
97
+ type: 'image' | 'video' | 'audio' | 'llm' | 'tts';
98
+ enabled?: boolean;
99
+ capabilities?: Record<string, Record<string, unknown>>;
100
+ }> | null;
101
+ defaultModels: Record<string, string> | null;
102
+ capabilityDefaults: Record<string, Record<string, unknown>> | null;
103
+ reachability: Record<string, {
104
+ ok: boolean;
105
+ detail?: string;
106
+ durationMs?: number;
107
+ }>;
108
+ }
109
+ export declare function buildCapabilitiesReport(input: ProviderCapabilitiesInput): CapabilitiesReport;
@@ -0,0 +1,180 @@
1
+ /**
2
+ * vvicat config ai-provider capabilities 命令的聚合视图:
3
+ * 把服务端返回的 providers / catalog / user-config / reachability
4
+ * 合并成一张 agent 可读的"我能跑什么 / 缺什么"结构。
5
+ *
6
+ * 设计目标:
7
+ * - 单一入口;agent 一次拿到所有决策所需信息。
8
+ * - 畸形输入不抛错(catalogModels / defaultModels 可能因为网络/服务端 bug 缺数据)。
9
+ * - 字段命名稳定(生产数据契约);JSON 字段不要随意改名。
10
+ */
11
+ function isPlainObject(value) {
12
+ return !!value && typeof value === 'object' && !Array.isArray(value);
13
+ }
14
+ function mapCapabilityBucket(type) {
15
+ if (type === 'llm')
16
+ return 'analysisModel';
17
+ if (type === 'image')
18
+ return 'imageModel';
19
+ if (type === 'video')
20
+ return 'videoModel';
21
+ if (type === 'tts')
22
+ return 'audioModel';
23
+ return 'audioModel';
24
+ }
25
+ function resolveReachability(provider, sample) {
26
+ if (!provider.enabled)
27
+ return 'skipped';
28
+ if (!sample)
29
+ return 'unknown';
30
+ return sample.ok ? 'ok' : 'failed';
31
+ }
32
+ export function buildCapabilitiesReport(input) {
33
+ const catalogModels = Array.isArray(input.catalogModels) ? input.catalogModels : [];
34
+ const defaultModels = isPlainObject(input.defaultModels) ? input.defaultModels : {};
35
+ const capabilityDefaults = isPlainObject(input.capabilityDefaults) ? input.capabilityDefaults : {};
36
+ // 1) providers 表(含 reachability 状态)
37
+ const providerIds = new Set(input.providers.map((p) => p.id));
38
+ const providers = input.providers.map((p) => {
39
+ const sample = input.reachability[p.id];
40
+ return {
41
+ id: p.id,
42
+ name: p.name,
43
+ type: p.type,
44
+ enabled: p.enabled,
45
+ baseUrl: p.baseUrl,
46
+ apiKeyMask: p.apiKeyMask,
47
+ hasApiKey: p.hasApiKey,
48
+ protocolMode: p.protocolMode,
49
+ lastTestResult: p.lastTestResult,
50
+ lastResponseMs: p.lastResponseMs,
51
+ upstreamModelsCount: Array.isArray(p.upstreamModels) ? p.upstreamModels.length : 0,
52
+ upstreamModelsSyncedAt: p.upstreamModelsSyncedAt,
53
+ reachability: resolveReachability(p, sample),
54
+ reachabilityDetail: sample?.detail,
55
+ };
56
+ });
57
+ // 2) capability buckets
58
+ const buckets = {
59
+ imageModel: [],
60
+ videoModel: [],
61
+ audioModel: [],
62
+ analysisModel: [],
63
+ characterModel: [],
64
+ locationModel: [],
65
+ storyboardModel: [],
66
+ };
67
+ for (const model of catalogModels) {
68
+ if (!model || typeof model.modelKey !== 'string' || typeof model.provider !== 'string')
69
+ continue;
70
+ if (model.enabled === false)
71
+ continue;
72
+ const bucket = mapCapabilityBucket(model.type);
73
+ const row = {
74
+ modelKey: model.modelKey,
75
+ provider: model.provider,
76
+ type: model.type,
77
+ enabled: model.enabled !== false,
78
+ capabilityOptions: extractCapabilityOptions(model.capabilities, model.type),
79
+ };
80
+ buckets[bucket].push(row);
81
+ }
82
+ // character/location/storyboard 都是 image 类,复用 imageModel 列表
83
+ buckets.characterModel = buckets.imageModel;
84
+ buckets.locationModel = buckets.imageModel;
85
+ buckets.storyboardModel = buckets.imageModel;
86
+ // 3) userDefaults + missingPieces
87
+ const missingPieces = [];
88
+ const userDefaults = {
89
+ imageModel: typeof defaultModels.imageModel === 'string' ? defaultModels.imageModel : null,
90
+ videoModel: typeof defaultModels.videoModel === 'string' ? defaultModels.videoModel : null,
91
+ audioModel: typeof defaultModels.audioModel === 'string' ? defaultModels.audioModel : null,
92
+ analysisModel: typeof defaultModels.analysisModel === 'string' ? defaultModels.analysisModel : null,
93
+ characterModel: typeof defaultModels.characterModel === 'string' ? defaultModels.characterModel : null,
94
+ locationModel: typeof defaultModels.locationModel === 'string' ? defaultModels.locationModel : null,
95
+ storyboardModel: typeof defaultModels.storyboardModel === 'string' ? defaultModels.storyboardModel : null,
96
+ };
97
+ for (const field of ['analysisModel', 'characterModel', 'locationModel', 'storyboardModel', 'videoModel']) {
98
+ const key = userDefaults[field];
99
+ if (!key) {
100
+ missingPieces.push({ capability: field, reason: `defaultModels.${field} 未配置`, fixHint: `运行 vvicat config set 设置 defaultModels.${field}` });
101
+ continue;
102
+ }
103
+ const providerKey = key.split('::')[0];
104
+ if (providerKey && !providerIds.has(providerKey)) {
105
+ missingPieces.push({
106
+ capability: field,
107
+ modelKey: key,
108
+ reason: `defaultModels.${field}=${key} 引用了未注册的 provider '${providerKey}'`,
109
+ fixHint: `运行 vvicat config ai-provider list 查看可用 provider,或 vvicat config ai-provider set ${providerKey} ...`,
110
+ });
111
+ }
112
+ }
113
+ // 检查每个 defaultModelKey 在 catalog 中是否 enabled
114
+ for (const [capability, modelKey] of Object.entries(userDefaults)) {
115
+ if (!modelKey)
116
+ continue;
117
+ const catalogEntry = catalogModels.find((m) => m.modelKey === modelKey);
118
+ if (catalogEntry && catalogEntry.enabled === false) {
119
+ missingPieces.push({
120
+ capability,
121
+ modelKey,
122
+ reason: `catalog 中 ${modelKey} 已禁用`,
123
+ fixHint: '在服务端启用该模型或换一个 defaultModel',
124
+ });
125
+ }
126
+ }
127
+ // capabilityDefaults 关键字段检查:videoModel 缺 duration 时给 agent 提示
128
+ for (const [capability, modelKey] of Object.entries(userDefaults)) {
129
+ if (!modelKey)
130
+ continue;
131
+ const catalogEntry = catalogModels.find((m) => m.modelKey === modelKey);
132
+ if (!catalogEntry)
133
+ continue;
134
+ if (catalogEntry.type !== 'video')
135
+ continue;
136
+ const defaults = capabilityDefaults[modelKey];
137
+ if (!defaults || !('duration' in defaults)) {
138
+ missingPieces.push({
139
+ capability,
140
+ modelKey,
141
+ reason: `capabilityDefaults.${modelKey} 缺 duration 字段`,
142
+ fixHint: `运行 vvicat config set 设置 capabilityDefaults.${modelKey}.duration(如 10)`,
143
+ });
144
+ }
145
+ }
146
+ const enabledProviders = providers.filter((p) => p.enabled).length;
147
+ const reachableProviders = providers.filter((p) => p.reachability === 'ok').length;
148
+ const capabilityCount = Object.values({
149
+ imageModel: buckets.imageModel,
150
+ videoModel: buckets.videoModel,
151
+ audioModel: buckets.audioModel,
152
+ analysisModel: buckets.analysisModel,
153
+ }).filter((bucket) => bucket.length > 0).length;
154
+ return {
155
+ generatedAt: new Date().toISOString(),
156
+ baseUrl: input.baseUrl,
157
+ summary: {
158
+ totalProviders: providers.length,
159
+ enabledProviders,
160
+ reachableProviders,
161
+ catalogModelCount: catalogModels.length,
162
+ capabilityCount,
163
+ },
164
+ providers,
165
+ capabilities: buckets,
166
+ userDefaults,
167
+ reachability: input.reachability,
168
+ missingPieces,
169
+ };
170
+ }
171
+ function extractCapabilityOptions(caps, type) {
172
+ if (!isPlainObject(caps))
173
+ return {};
174
+ const namespace = isPlainObject(caps[type]) ? caps[type] : {};
175
+ const out = {};
176
+ for (const [key, value] of Object.entries(namespace)) {
177
+ out[key] = value;
178
+ }
179
+ return out;
180
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * vvicat config ai-provider ... 命令的纯逻辑 helper。
3
+ *
4
+ * 单测入口在这里(_ 开头让 oclif 不把它当作命令加载)。
5
+ * 真实命令文件 import 这里的 helper 即可。
6
+ */
7
+ export interface SetFlags {
8
+ 'base-url'?: string;
9
+ 'api-key'?: string;
10
+ 'api-mode'?: string;
11
+ name?: string;
12
+ protocol?: string;
13
+ enabled?: boolean;
14
+ 'sort-order'?: number;
15
+ }
16
+ export declare function composeBodyFromFlags(providerId: string, flags: SetFlags): Record<string, unknown>;
17
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
18
+ export interface CatalogModelRow {
19
+ modelKey: string;
20
+ provider: string;
21
+ type: 'image' | 'video' | 'audio' | 'llm' | 'tts';
22
+ enabled?: boolean;
23
+ capabilities?: Record<string, Record<string, unknown>>;
24
+ }
25
+ export declare function extractCatalogModels(config: unknown): CatalogModelRow[];
26
+ export declare function extractDefaultModels(config: unknown): Record<string, string>;
27
+ export declare function extractCapabilityDefaults(config: unknown): Record<string, Record<string, unknown>>;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * vvicat config ai-provider ... 命令的纯逻辑 helper。
3
+ *
4
+ * 单测入口在这里(_ 开头让 oclif 不把它当作命令加载)。
5
+ * 真实命令文件 import 这里的 helper 即可。
6
+ */
7
+ export function composeBodyFromFlags(providerId, flags) {
8
+ const body = {
9
+ type: providerId,
10
+ name: flags.name?.trim() || providerId,
11
+ enabled: flags.enabled === true,
12
+ };
13
+ if (typeof flags['base-url'] === 'string' && flags['base-url'].trim())
14
+ body.baseUrl = flags['base-url'].trim();
15
+ if (typeof flags['api-key'] === 'string' && flags['api-key'])
16
+ body.apiKey = flags['api-key'];
17
+ if (typeof flags['api-mode'] === 'string' && flags['api-mode'])
18
+ body.apiMode = flags['api-mode'];
19
+ if (typeof flags.protocol === 'string' && flags.protocol)
20
+ body.protocolMode = flags.protocol;
21
+ if (typeof flags['sort-order'] === 'number')
22
+ body.sortOrder = flags['sort-order'];
23
+ return body;
24
+ }
25
+ export function isRecord(value) {
26
+ return !!value && typeof value === 'object' && !Array.isArray(value);
27
+ }
28
+ export function extractCatalogModels(config) {
29
+ if (!isRecord(config))
30
+ return [];
31
+ const models = config.models;
32
+ if (!Array.isArray(models))
33
+ return [];
34
+ return models.filter((m) => isRecord(m) && typeof m.modelKey === 'string' && typeof m.provider === 'string' && typeof m.type === 'string');
35
+ }
36
+ export function extractDefaultModels(config) {
37
+ if (!isRecord(config))
38
+ return {};
39
+ const value = isRecord(config.defaultModels) ? config.defaultModels : {};
40
+ const out = {};
41
+ for (const [k, v] of Object.entries(value))
42
+ if (typeof v === 'string')
43
+ out[k] = v;
44
+ return out;
45
+ }
46
+ export function extractCapabilityDefaults(config) {
47
+ if (!isRecord(config))
48
+ return {};
49
+ const value = isRecord(config.capabilityDefaults) ? config.capabilityDefaults : {};
50
+ const out = {};
51
+ for (const [k, v] of Object.entries(value))
52
+ if (isRecord(v))
53
+ out[k] = v;
54
+ return out;
55
+ }