@coralai/sps-cli 0.65.75 → 0.65.77

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 (103) hide show
  1. package/dist/console/index.d.ts.map +1 -1
  2. package/dist/console/index.js +6 -0
  3. package/dist/console/index.js.map +1 -1
  4. package/dist/console/lib/artPaths.d.ts +4 -4
  5. package/dist/console/lib/artPaths.d.ts.map +1 -1
  6. package/dist/console/lib/artPaths.js +24 -16
  7. package/dist/console/lib/artPaths.js.map +1 -1
  8. package/dist/console/lib/artRunRequest.d.ts +46 -0
  9. package/dist/console/lib/artRunRequest.d.ts.map +1 -0
  10. package/dist/console/lib/artRunRequest.js +107 -0
  11. package/dist/console/lib/artRunRequest.js.map +1 -0
  12. package/dist/console/routes/art.d.ts.map +1 -1
  13. package/dist/console/routes/art.js +65 -47
  14. package/dist/console/routes/art.js.map +1 -1
  15. package/dist/console/routes/artPipeline.d.ts +16 -0
  16. package/dist/console/routes/artPipeline.d.ts.map +1 -0
  17. package/dist/console/routes/artPipeline.js +126 -0
  18. package/dist/console/routes/artPipeline.js.map +1 -0
  19. package/dist/core/art/artConcurrency.d.ts +63 -0
  20. package/dist/core/art/artConcurrency.d.ts.map +1 -0
  21. package/dist/core/art/artConcurrency.js +63 -0
  22. package/dist/core/art/artConcurrency.js.map +1 -0
  23. package/dist/core/art/artRunLauncher.d.ts +6 -0
  24. package/dist/core/art/artRunLauncher.d.ts.map +1 -0
  25. package/dist/core/art/artRunLauncher.js +91 -0
  26. package/dist/core/art/artRunLauncher.js.map +1 -0
  27. package/dist/core/art/artRunService.d.ts +115 -0
  28. package/dist/core/art/artRunService.d.ts.map +1 -0
  29. package/dist/core/art/artRunService.js +183 -0
  30. package/dist/core/art/artRunService.js.map +1 -0
  31. package/dist/core/art/assetLocator.d.ts +33 -0
  32. package/dist/core/art/assetLocator.d.ts.map +1 -0
  33. package/dist/core/art/assetLocator.js +61 -0
  34. package/dist/core/art/assetLocator.js.map +1 -0
  35. package/dist/providers/art/artDriver.d.ts +71 -0
  36. package/dist/providers/art/artDriver.d.ts.map +1 -0
  37. package/dist/providers/art/artDriver.js +119 -0
  38. package/dist/providers/art/artDriver.js.map +1 -0
  39. package/dist/providers/art/artEvents.d.ts +23 -0
  40. package/dist/providers/art/artEvents.d.ts.map +1 -0
  41. package/dist/providers/art/artEvents.js +40 -0
  42. package/dist/providers/art/artEvents.js.map +1 -0
  43. package/dist/providers/art/artGen.d.ts +259 -0
  44. package/dist/providers/art/artGen.d.ts.map +1 -0
  45. package/dist/providers/art/artGen.js +1303 -0
  46. package/dist/providers/art/artGen.js.map +1 -0
  47. package/dist/providers/art/artPrompt.d.ts +42 -0
  48. package/dist/providers/art/artPrompt.d.ts.map +1 -0
  49. package/dist/providers/art/artPrompt.js +41 -0
  50. package/dist/providers/art/artPrompt.js.map +1 -0
  51. package/dist/providers/art/artRunChild.d.ts +3 -0
  52. package/dist/providers/art/artRunChild.d.ts.map +1 -0
  53. package/dist/providers/art/artRunChild.js +153 -0
  54. package/dist/providers/art/artRunChild.js.map +1 -0
  55. package/dist/providers/art/atomicWrite.d.ts +16 -0
  56. package/dist/providers/art/atomicWrite.d.ts.map +1 -0
  57. package/dist/providers/art/atomicWrite.js +62 -0
  58. package/dist/providers/art/atomicWrite.js.map +1 -0
  59. package/dist/providers/art/cancelMarker.d.ts +9 -0
  60. package/dist/providers/art/cancelMarker.d.ts.map +1 -0
  61. package/dist/providers/art/cancelMarker.js +60 -0
  62. package/dist/providers/art/cancelMarker.js.map +1 -0
  63. package/dist/providers/art/composeAsset.d.ts +52 -0
  64. package/dist/providers/art/composeAsset.d.ts.map +1 -0
  65. package/dist/providers/art/composeAsset.js +73 -0
  66. package/dist/providers/art/composeAsset.js.map +1 -0
  67. package/dist/providers/art/imageSize.d.ts +31 -0
  68. package/dist/providers/art/imageSize.d.ts.map +1 -0
  69. package/dist/providers/art/imageSize.js +92 -0
  70. package/dist/providers/art/imageSize.js.map +1 -0
  71. package/dist/providers/art/jobContext.d.ts +25 -0
  72. package/dist/providers/art/jobContext.d.ts.map +1 -0
  73. package/dist/providers/art/jobContext.js +5 -0
  74. package/dist/providers/art/jobContext.js.map +1 -0
  75. package/dist/providers/art/rateLimitRetry.d.ts +25 -0
  76. package/dist/providers/art/rateLimitRetry.d.ts.map +1 -0
  77. package/dist/providers/art/rateLimitRetry.js +40 -0
  78. package/dist/providers/art/rateLimitRetry.js.map +1 -0
  79. package/dist/providers/art/uiKit.d.ts +61 -0
  80. package/dist/providers/art/uiKit.d.ts.map +1 -0
  81. package/dist/providers/art/uiKit.js +331 -0
  82. package/dist/providers/art/uiKit.js.map +1 -0
  83. package/dist/providers/art/visionJudge.d.ts +26 -0
  84. package/dist/providers/art/visionJudge.d.ts.map +1 -0
  85. package/dist/providers/art/visionJudge.js +127 -0
  86. package/dist/providers/art/visionJudge.js.map +1 -0
  87. package/dist/providers/art/withinDir.d.ts +2 -0
  88. package/dist/providers/art/withinDir.d.ts.map +1 -0
  89. package/dist/providers/art/withinDir.js +19 -0
  90. package/dist/providers/art/withinDir.js.map +1 -0
  91. package/dist/providers/media/gatewayMediaProviders.d.ts +58 -0
  92. package/dist/providers/media/gatewayMediaProviders.d.ts.map +1 -0
  93. package/dist/providers/media/gatewayMediaProviders.js +46 -0
  94. package/dist/providers/media/gatewayMediaProviders.js.map +1 -0
  95. package/dist/providers/media/generateAsset.d.ts +9 -0
  96. package/dist/providers/media/generateAsset.d.ts.map +1 -1
  97. package/dist/providers/media/generateAsset.js +7 -1
  98. package/dist/providers/media/generateAsset.js.map +1 -1
  99. package/dist/providers/media/mediaProviders.d.ts +31 -1
  100. package/dist/providers/media/mediaProviders.d.ts.map +1 -1
  101. package/dist/providers/media/mediaProviders.js +33 -1
  102. package/dist/providers/media/mediaProviders.js.map +1 -1
  103. package/package.json +1 -1
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @module core/art/artRunLauncher
3
+ * @description 真正把 run 丢进**子进程**的那段(1.4)+ `UV_THREADPOOL_SIZE`(1.5)。
4
+ * @layer core
5
+ * @boundedContext art
6
+ *
7
+ * 🔴 **凭据走 stdin,不走 argv、也不走 env。**
8
+ * argv 在 `ps` 里对同机任何用户可见;env 在 `/proc/<pid>/environ` 里对同用户的任何进程可见。
9
+ * 子进程的活里带着 jobId 与工作区标识,**唯一不外泄的通道是管道**。
10
+ * (今天这份 task 里其实没有 token —— 但通道要在**没有 token 的时候**就选对,
11
+ * 否则将来加上去的人不会回头改传法。)
12
+ *
13
+ * 🔴 **结果靠 stdout 上的一行 `SPS_ART_RESULT <json>`,不靠退出码。**
14
+ * 退出码只有"0 / 非 0",而 `partial`(半套资产,已生成的保留)既不是成功也不是失败。
15
+ * **没有结果行 = `lost`** —— 与 worker 死活判定同一个判据(没有终态消息就是 lost,
16
+ * 不去猜"大概是成功了")。
17
+ */
18
+ import { spawn } from 'node:child_process';
19
+ import { access } from 'node:fs/promises';
20
+ import { fileURLToPath } from 'node:url';
21
+ import { dirname, join } from 'node:path';
22
+ import { ART_ASSET_CONCURRENCY_MAX } from './artConcurrency.js';
23
+ /** 结果行前缀 —— 子进程的 stdout 上可能还有别的输出(算子的 warn 等),靠它挑出结果。 */
24
+ export const ART_RESULT_PREFIX = 'SPS_ART_RESULT ';
25
+ /** 子进程入口:与本文件同一棵编译产物里的 `providers/art/artRunChild.js`。 */
26
+ function childScript() {
27
+ // 与 `agentDaemon` 定位 daemon 脚本同一手法:从自己的位置推,不依赖 cwd / argv[1]
28
+ //(进程管理器会改写 argv[1] —— pm2 fork 实测踩过)。
29
+ const here = dirname(fileURLToPath(import.meta.url)); // <dist>/core/art
30
+ return join(here, '..', '..', 'providers', 'art', 'artRunChild.js');
31
+ }
32
+ function parseResult(stdout) {
33
+ const line = stdout
34
+ .split('\n')
35
+ .reverse()
36
+ .find((l) => l.startsWith(ART_RESULT_PREFIX));
37
+ if (!line)
38
+ return undefined;
39
+ try {
40
+ return JSON.parse(line.slice(ART_RESULT_PREFIX.length));
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ }
46
+ /** 生产用 launcher。测试注入自己的(见 artRunService.test)。 */
47
+ export const spawnArtRun = async (task) => {
48
+ const script = childScript();
49
+ await access(script).catch(() => {
50
+ throw new Error(`美术子进程入口缺失:${script}(dist 没编译全?先 npm run build)`);
51
+ });
52
+ // ══════════════════════════════════════════════════════════════════
53
+ // 🔴 线程池大小与资产并发**写在同一段代码里、用同一个常量**(1.5)。
54
+ // UV_THREADPOOL_SIZE 只在**进程启动、池首次使用之前**有效:
55
+ // 池是懒初始化的,**定型后改 process.env 静默无效**(不报错,只是不生效)。
56
+ // ⇒ 只能在这里、spawn 的那一刻给。放 systemd 会影响无关的一切,
57
+ // 而且**读代码的人看不见它**。
58
+ // ══════════════════════════════════════════════════════════════════
59
+ const assetConcurrency = ART_ASSET_CONCURRENCY_MAX;
60
+ return await new Promise((resolve) => {
61
+ // `--run` 是**显式**启动旗标(同 daemon 的 `--serve`):子进程不靠 argv[1] 推断自己该不该跑。
62
+ const child = spawn(process.execPath, [script, '--run'], {
63
+ env: { ...process.env, UV_THREADPOOL_SIZE: String(assetConcurrency) },
64
+ stdio: ['pipe', 'pipe', 'pipe'],
65
+ });
66
+ let out = '';
67
+ let err = '';
68
+ child.stdout.on('data', (d) => { out += d.toString(); });
69
+ child.stderr.on('data', (d) => { err += d.toString(); });
70
+ child.on('error', (e) => {
71
+ resolve({ status: 'failed', total: 0, ok: 0, skipped: 0, failed: 0, error: e.message });
72
+ });
73
+ child.on('close', (code) => {
74
+ const r = parseResult(out);
75
+ if (r)
76
+ return resolve(r);
77
+ // 🔴 没有结果行 = lost。**不按退出码猜** —— 退出码 0 也可能是它在写结果前被 OOM 掉了。
78
+ resolve({
79
+ status: 'lost',
80
+ total: 0,
81
+ ok: 0,
82
+ skipped: 0,
83
+ failed: 0,
84
+ error: `子进程未给出结果(exit ${code ?? '?'})${err ? `:${err.slice(-400)}` : ''}`,
85
+ });
86
+ });
87
+ // 活从 stdin 进去(见模块头)。
88
+ child.stdin.end(JSON.stringify(task));
89
+ });
90
+ };
91
+ //# sourceMappingURL=artRunLauncher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artRunLauncher.js","sourceRoot":"","sources":["../../../src/core/art/artRunLauncher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,115 @@
1
+ import type { ArtPrompts } from '../../providers/art/artPrompt.js';
2
+ import { ART_RUN_LIMITS } from './artConcurrency.js';
3
+ import { type ProjectLocator } from './assetLocator.js';
4
+ /** run 的状态机(契约 1.7):queued → running → succeeded/partial/failed/lost;cancelling → cancelled。 */
5
+ export type ArtRunState = 'queued' | 'running' | 'succeeded' | 'partial' | 'failed' | 'cancelling' | 'cancelled' | 'lost';
6
+ /**
7
+ * 交给子进程的活。
8
+ *
9
+ * 🔴 **带凭据**(1.7b,2026-08-12 补)。在此之前这里注释着「没有凭据字段」,
10
+ * 而 `parseArtRunRequest` 收下的 `gatewayToken`/`gatewayBaseUrl` 到路由那层就被丢掉了 ——
11
+ * 子进程于是回落到全局 provider,**归因落平台账且不报错**(设计 studio `docs/39` §十三)。
12
+ *
13
+ * 🔴 **凭据只走 stdin**:本结构被 `artRunLauncher` 序列化后经管道喂给子进程。
14
+ * 不许改成 argv(`ps` 里全机可见)或 env(`/proc/<pid>/environ` 里同用户可见)——
15
+ * `artRunLauncher` 头注在还没有 token 的时候就把通道选对了,这次只是把 token 放进去。
16
+ * ⚠️ 因此**本结构不可整体进日志**。
17
+ */
18
+ export interface ArtChildTask {
19
+ runId: string;
20
+ locator: ProjectLocator;
21
+ jobId: string;
22
+ manifestFile: string;
23
+ artPrompts: ArtPrompts;
24
+ /** 该 attempt 的网关 token(自带归因:租户 + jobId)。🔴 禁止落日志。 */
25
+ gatewayToken: string;
26
+ gatewayBaseUrl: string;
27
+ /** 可选;缺省时由 coral 网关按控制面默认决定(见 `gatewayMediaProviders`)。 */
28
+ imageModel?: string;
29
+ }
30
+ /**
31
+ * 「跑完了,但一张都没生成」的**合法**原因(3.5)。
32
+ *
33
+ * 🔴 存在的理由:`succeeded · total 0` 今天有三个来源,而它们的处置完全不同 ——
34
+ * 「这项目没有美术需求」是**正常**,「指错了 projectId」是**调用方的错**(已在路由层 422 拦掉),
35
+ * 「manifest 坏了」是**故障**(走 failed)。**留成裸的 succeeded/0 = 三者不可分。**
36
+ * ⚠️ 只在 `total === 0` 且 `status === 'succeeded'` 时出现;正常跑完不带这个字段。
37
+ */
38
+ export type ArtEmptyReason = 'no-manifest' | 'no-assets';
39
+ /** 子进程的产出(它在 stdout 上给的结果行)。 */
40
+ export interface ArtChildResult {
41
+ status: 'succeeded' | 'partial' | 'failed' | 'lost';
42
+ total: number;
43
+ ok: number;
44
+ skipped: number;
45
+ failed: number;
46
+ error?: string;
47
+ /** 见 {@link ArtEmptyReason}。**可选新增字段**:老消费方不认它就当没有(契约只增不改)。 */
48
+ reason?: ArtEmptyReason;
49
+ }
50
+ export interface ArtRunView {
51
+ runId: string;
52
+ status: ArtRunState;
53
+ total?: number;
54
+ ok?: number;
55
+ skipped?: number;
56
+ failed?: number;
57
+ error?: string;
58
+ /** 「跑完了但一张都没生成」的合法原因(3.5)——见 {@link ArtEmptyReason}。 */
59
+ reason?: ArtEmptyReason;
60
+ queuedAt: string;
61
+ startedAt?: string;
62
+ endedAt?: string;
63
+ }
64
+ export type StartResult = {
65
+ ok: true;
66
+ runId: string;
67
+ status: 'running' | 'queued';
68
+ } | {
69
+ ok: false;
70
+ reason: 'busy';
71
+ retryAfterS: number;
72
+ running: number;
73
+ queued: number;
74
+ };
75
+ /** 启动一个子进程跑这份活;resolve = 子进程结束。**不 reject** —— 死法也是一种结果(lost)。 */
76
+ export type ArtRunLauncher = (task: ArtChildTask) => Promise<ArtChildResult>;
77
+ export interface ArtRunServiceOpts {
78
+ launch: ArtRunLauncher;
79
+ limits?: Partial<typeof ART_RUN_LIMITS>;
80
+ now?: () => Date;
81
+ newRunId?: () => string;
82
+ }
83
+ export declare class ArtRunService {
84
+ private readonly runs;
85
+ private readonly queue;
86
+ private readonly launch;
87
+ private readonly limits;
88
+ private readonly now;
89
+ private readonly newRunId;
90
+ constructor(opts: ArtRunServiceOpts);
91
+ /**
92
+ * 收一个 run。有空位就直接跑;没空位**排队**;队列也满 → `busy`(调用方回 429 + Retry-After)。
93
+ *
94
+ * 🔴 **配额只有两层:全局 + 租户,不做项目级。** 项目级是产品策略(谁能同时建几个),
95
+ * 而这道闸是**资源保护**(机器扛得住几个)—— 两件事混在一个闸里,调其中一个必然误伤另一个。
96
+ */
97
+ start(task: Omit<ArtChildTask, 'runId'>): StartResult;
98
+ get(runId: string): ArtRunView | undefined;
99
+ /**
100
+ * 取消。**幂等**;终态调用是 no-op(不报错 —— 重复点停止不该变成错误)。
101
+ *
102
+ * 两种情形要分开:
103
+ * - **还在排队**:没有子进程可读标记 ⇒ 直接判 `cancelled` 并出队(它一行都没跑过)
104
+ * - **正在跑**:写标记 → `cancelling`;真正落到 `cancelled` 由子进程的终态决定
105
+ */
106
+ cancel(runId: string): ArtRunView | undefined;
107
+ /** 当前在跑的数量(给闸用,也给 /api 观测用)。 */
108
+ countRunning(tenantId?: string): number;
109
+ queuedCount(): number;
110
+ private begin;
111
+ private finish;
112
+ /** 有空位就放一个排队的进去(FIFO;租户配额可能让队首过不去 → 顺延给下一个)。 */
113
+ private pump;
114
+ }
115
+ //# sourceMappingURL=artRunService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artRunService.d.ts","sourceRoot":"","sources":["../../../src/core/art/artRunService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,183 @@
1
+ /**
2
+ * @module core/art/artRunService
3
+ * @description 美术 run 的**管理面**:并发闸(1.8)· 子进程调度(1.4)· 取消 · 状态查询。
4
+ * @layer core
5
+ * @boundedContext art
6
+ *
7
+ * ══════════════════════════════════════════════════════════════════════════
8
+ * 🔴 **run 跑在子进程里,不在 console 进程里**(1.4)。三个理由,只有第一个是新的:
9
+ * ```
10
+ * ① 图处理吃满 libuv 线程池,而池是**全进程共享**的 ⇒ 在 console 里跑
11
+ * 会把「美术慢」变成「sps 整体变慢」(连 /api/health 探针都跟着卡)
12
+ * ② UV_THREADPOOL_SIZE **只能在进程启动前设**(见 artConcurrency)⇒ 必须有个新进程
13
+ * ③ 移交件里大量同步 fs 是**对的**(它本来就跑在 CLI 里)—— 换个宿主它才变错。
14
+ * 与其把 1200 行改成异步,不如**换回它对的那个宿主**。
15
+ * ```
16
+ * ⇒ 并发因此变成一件可数的事:**数子进程**,不猜内存、不猜线程。
17
+ *
18
+ * 🔴 **取消绝不发信号**:只写取消标记(`cancelMarker`,存在即信号),
19
+ * 子进程在**取下一个资产之前**读它。SIGTERM 落在哪一行不可控 ——
20
+ * 半张图落盘后,"文件存在就跳过"的幂等会把它当成已完成,**取消一次就永久污染一个资产**。
21
+ *
22
+ * ⚠️ **注册表是内存的**:console 重启后在跑的 run 变成孤儿(它们仍会把图写完,
23
+ * 但查不到状态、也取消不了)。与 studio 2.4 的判断一致 —— 持久化要解决的是
24
+ * "重启后还能取消",而那件事今天没有消费方在等。
25
+ * ══════════════════════════════════════════════════════════════════════════
26
+ */
27
+ import { randomUUID } from 'node:crypto';
28
+ import { requestCancel } from '../../providers/art/cancelMarker.js';
29
+ import { ART_RUN_LIMITS } from './artConcurrency.js';
30
+ import { resolveProjectDir } from './assetLocator.js';
31
+ export class ArtRunService {
32
+ runs = new Map();
33
+ queue = [];
34
+ launch;
35
+ limits;
36
+ now;
37
+ newRunId;
38
+ constructor(opts) {
39
+ this.launch = opts.launch;
40
+ this.limits = { ...ART_RUN_LIMITS, ...opts.limits };
41
+ this.now = opts.now ?? (() => new Date());
42
+ this.newRunId = opts.newRunId ?? (() => randomUUID());
43
+ }
44
+ /**
45
+ * 收一个 run。有空位就直接跑;没空位**排队**;队列也满 → `busy`(调用方回 429 + Retry-After)。
46
+ *
47
+ * 🔴 **配额只有两层:全局 + 租户,不做项目级。** 项目级是产品策略(谁能同时建几个),
48
+ * 而这道闸是**资源保护**(机器扛得住几个)—— 两件事混在一个闸里,调其中一个必然误伤另一个。
49
+ */
50
+ start(task) {
51
+ const runningTotal = this.countRunning();
52
+ const runningTenant = this.countRunning(task.locator.tenantId);
53
+ const canRunNow = runningTotal < this.limits.maxRuns && runningTenant < this.limits.maxRunsPerTenant;
54
+ if (!canRunNow && this.queue.length >= this.limits.maxQueued) {
55
+ // 🔴 不无限积压:无限队列只是把"现在失败"变成"很久以后失败",
56
+ // 而调用方在那之前拿不到任何可操作的信号。
57
+ return {
58
+ ok: false,
59
+ reason: 'busy',
60
+ retryAfterS: this.limits.retryAfterS,
61
+ running: runningTotal,
62
+ queued: this.queue.length,
63
+ };
64
+ }
65
+ const runId = this.newRunId();
66
+ const at = this.now().toISOString();
67
+ const entry = {
68
+ task: { ...task, runId },
69
+ cancelRequested: false,
70
+ view: { runId, status: canRunNow ? 'running' : 'queued', queuedAt: at },
71
+ };
72
+ this.runs.set(runId, entry);
73
+ if (canRunNow) {
74
+ this.begin(entry);
75
+ }
76
+ else {
77
+ this.queue.push(runId);
78
+ }
79
+ return { ok: true, runId, status: entry.view.status };
80
+ }
81
+ get(runId) {
82
+ return this.runs.get(runId)?.view;
83
+ }
84
+ /**
85
+ * 取消。**幂等**;终态调用是 no-op(不报错 —— 重复点停止不该变成错误)。
86
+ *
87
+ * 两种情形要分开:
88
+ * - **还在排队**:没有子进程可读标记 ⇒ 直接判 `cancelled` 并出队(它一行都没跑过)
89
+ * - **正在跑**:写标记 → `cancelling`;真正落到 `cancelled` 由子进程的终态决定
90
+ */
91
+ cancel(runId) {
92
+ const e = this.runs.get(runId);
93
+ if (!e)
94
+ return undefined;
95
+ if (isTerminal(e.view.status))
96
+ return e.view;
97
+ if (e.view.status === 'queued') {
98
+ const i = this.queue.indexOf(runId);
99
+ if (i >= 0)
100
+ this.queue.splice(i, 1);
101
+ e.cancelRequested = true;
102
+ e.view.status = 'cancelled';
103
+ e.view.endedAt = this.now().toISOString();
104
+ return e.view;
105
+ }
106
+ // 🔴 只写标记,**绝不发信号**(见模块头)。
107
+ requestCancel(resolveProjectDir(e.task.locator), runId);
108
+ e.cancelRequested = true;
109
+ e.view.status = 'cancelling';
110
+ return e.view;
111
+ }
112
+ /** 当前在跑的数量(给闸用,也给 /api 观测用)。 */
113
+ countRunning(tenantId) {
114
+ let n = 0;
115
+ for (const e of this.runs.values()) {
116
+ if (e.view.status !== 'running' && e.view.status !== 'cancelling')
117
+ continue;
118
+ if (tenantId && e.task.locator.tenantId !== tenantId)
119
+ continue;
120
+ n++;
121
+ }
122
+ return n;
123
+ }
124
+ queuedCount() {
125
+ return this.queue.length;
126
+ }
127
+ begin(e) {
128
+ e.view.status = 'running';
129
+ e.view.startedAt = this.now().toISOString();
130
+ void this.launch(e.task)
131
+ .then((r) => this.finish(e, r))
132
+ .catch((err) => {
133
+ // launcher 自己炸了(spawn 失败等)—— 与"子进程没给结果"是两件事,分开记。
134
+ this.finish(e, {
135
+ status: 'failed',
136
+ total: 0,
137
+ ok: 0,
138
+ skipped: 0,
139
+ failed: 0,
140
+ error: err instanceof Error ? err.message : String(err),
141
+ });
142
+ });
143
+ }
144
+ finish(e, r) {
145
+ e.view.total = r.total;
146
+ e.view.ok = r.ok;
147
+ e.view.skipped = r.skipped;
148
+ e.view.failed = r.failed;
149
+ if (r.error)
150
+ e.view.error = r.error;
151
+ if (r.reason)
152
+ e.view.reason = r.reason;
153
+ // 取消过 ⇒ 终态是 cancelled(**不是 failed**):半套资产是有效产物,已生成的保留。
154
+ // ⚠️ 但**子进程没给结果**(lost)时不覆盖成 cancelled —— 那会把"不知道发生了什么"
155
+ // 说成"是我们停的",而这两者的排查方向完全不同。
156
+ e.view.status = r.status === 'lost' ? 'lost' : e.cancelRequested ? 'cancelled' : r.status;
157
+ e.view.endedAt = this.now().toISOString();
158
+ this.pump();
159
+ }
160
+ /** 有空位就放一个排队的进去(FIFO;租户配额可能让队首过不去 → 顺延给下一个)。 */
161
+ pump() {
162
+ for (let i = 0; i < this.queue.length;) {
163
+ if (this.countRunning() >= this.limits.maxRuns)
164
+ return;
165
+ const id = this.queue[i];
166
+ const e = this.runs.get(id);
167
+ if (!e) {
168
+ this.queue.splice(i, 1);
169
+ continue;
170
+ }
171
+ if (this.countRunning(e.task.locator.tenantId) >= this.limits.maxRunsPerTenant) {
172
+ i++; // 这个租户满了,看下一个 —— 否则一个占满配额的租户会把**别人**堵在队首后面
173
+ continue;
174
+ }
175
+ this.queue.splice(i, 1);
176
+ this.begin(e);
177
+ }
178
+ }
179
+ }
180
+ function isTerminal(s) {
181
+ return s === 'succeeded' || s === 'partial' || s === 'failed' || s === 'cancelled' || s === 'lost';
182
+ }
183
+ //# sourceMappingURL=artRunService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artRunService.js","sourceRoot":"","sources":["../../../src/core/art/artRunService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /** 一个项目的逻辑标识。**没有路径字段,这是有意的。** */
2
+ export interface ProjectLocator {
3
+ /** 联邦租户 id(不含 `t` 前缀,前缀由本模块加)。 */
4
+ tenantId: string;
5
+ /** 产品 id:`gameforge` / `createexperiments` / `appforge` / `video` / `coralplatform` …
6
+ * ⚠️ 由调用方给 —— 它知道自己是哪个产品,而 sps **不该**有一张产品表去猜。
7
+ * #22 改名时调用方传新值即可,本模块一行不用动。 */
8
+ productId: string;
9
+ /** 项目 id(studio 的 `projects.id`,如 `game-gms8s9219`)。 */
10
+ projectId: string;
11
+ }
12
+ /** 租户 namespace:`t<tenantId>`(与 studio `resolveRepoRoot` 及既有磁盘布局一致)。 */
13
+ export declare function tenantNamespace(tenantId: string): string;
14
+ /**
15
+ * 项目工作区目录 = `<WORKSPACES_ROOT>/t<tenantId>/<productId>/<projectId>`。
16
+ *
17
+ * 逐段校验后由 `workspaceDir` 拼在受管根下;任一段非法即抛(delivery 层转 422)。
18
+ */
19
+ export declare function resolveProjectDir(loc: ProjectLocator): string;
20
+ /** 资产目录 —— 与 studio 侧 `assetsDir(ctx)` 同一约定(`<repoDir>/assets`)。 */
21
+ export declare function resolveAssetsDir(loc: ProjectLocator): string;
22
+ /**
23
+ * 单个资产的最终落点。
24
+ *
25
+ * ⚠️ 扩展名固定 `.png`,**与 studio 的 `assetPath` 一致**(它把扩展名钉死后再拼)——
26
+ * 那条约定正是 atlas 的 `meta.image` 能对上图的原因(见 art-http-surface §pack-atlas)。
27
+ * 这里保持同一约定,**不要"顺手支持任意扩展名"**:一旦 `id` 和文件名脱钩,
28
+ * `meta.image` 就会指向不存在的文件,而 atlas 结构合法、只在引擎加载时才炸。
29
+ */
30
+ export declare function resolveAssetPath(loc: ProjectLocator, assetId: string): string;
31
+ /** sps 在工作区里的自留地(token / 取消标记等都落这)。 */
32
+ export declare function resolveSpsDir(loc: ProjectLocator): string;
33
+ //# sourceMappingURL=assetLocator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assetLocator.d.ts","sourceRoot":"","sources":["../../../src/core/art/assetLocator.ts"],"names":[],"mappings":"AA4BA,kCAAkC;AAClC,MAAM,WAAW,cAAc;IAC7B,kCAAkC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB;;oCAEgC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,CAG7D;AAED,oEAAoE;AACpE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,CAE5D;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAG7E;AAED,uCAAuC;AACvC,wBAAgB,aAAa,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,CAEzD"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @module core/art/assetLocator
3
+ * @description 逻辑标识 → 物理路径的**正向解析**(美术流水线 1.6)。
4
+ * @layer core
5
+ * @boundedContext art
6
+ *
7
+ * 契约:调用方给 `{tenantId, productId, projectId}`,**sps 自己拼路径**。
8
+ * 调用方**永远不传路径**,sps **永远不从路径反推**租户/产品/项目。
9
+ *
10
+ * 🔴 **为什么是"正向查"而不是"解析字符串"**:
11
+ * 早先给美术算子面定的是「`in`/`out` 当不透明路径,不解析、不推断」——
12
+ * 那条**没有被推翻**。两条是同一个判据的两面:
13
+ * > **不从路径反推标识,而是持有标识正向拼路径。**
14
+ * 反推(`split('/')` 认产品名)会在工作区改名那天**静默错**;正向拼则一行都不用改。
15
+ *
16
+ * ✅ **映射是"拼出来"的,不是"存下来"的** —— 这一点很重要:
17
+ * 设计答复里曾担心「sps 从此持有 tenant→路径映射,它就成了一份必须被治理的状态
18
+ * (谁写、怎么迁、租户删除时怎么办)」。**结论是不需要**:落点规则本身是组合式的
19
+ * (`<root>/t<tenant>/<product>/<project>`),所以它是**纯函数,零状态,零治理**。
20
+ *
21
+ * 🔴 **安全性来自构造,不来自检查**:每一段都过 `validateSegment`(禁 `/` `\` `.` `..`
22
+ * 空串与非法字符),再由 sps 拼在受管根之下 —— 所以**结果不可能落到根外**,
23
+ * 它在结构上就不是一个"任意位置写文件"的原语。
24
+ * 这与 `managedWorkspaceDir` 的既有理由一致:**命名权可以给调用方,落点权不行。**
25
+ */
26
+ import { join } from 'node:path';
27
+ import { validateSegment, workspaceDir } from '../workspacePaths.js';
28
+ /** 租户 namespace:`t<tenantId>`(与 studio `resolveRepoRoot` 及既有磁盘布局一致)。 */
29
+ export function tenantNamespace(tenantId) {
30
+ return `t${validateSegment(tenantId)}`;
31
+ }
32
+ /**
33
+ * 项目工作区目录 = `<WORKSPACES_ROOT>/t<tenantId>/<productId>/<projectId>`。
34
+ *
35
+ * 逐段校验后由 `workspaceDir` 拼在受管根下;任一段非法即抛(delivery 层转 422)。
36
+ */
37
+ export function resolveProjectDir(loc) {
38
+ const ns = `${tenantNamespace(loc.tenantId)}/${validateSegment(loc.productId)}`;
39
+ return workspaceDir(ns, loc.projectId);
40
+ }
41
+ /** 资产目录 —— 与 studio 侧 `assetsDir(ctx)` 同一约定(`<repoDir>/assets`)。 */
42
+ export function resolveAssetsDir(loc) {
43
+ return join(resolveProjectDir(loc), 'assets');
44
+ }
45
+ /**
46
+ * 单个资产的最终落点。
47
+ *
48
+ * ⚠️ 扩展名固定 `.png`,**与 studio 的 `assetPath` 一致**(它把扩展名钉死后再拼)——
49
+ * 那条约定正是 atlas 的 `meta.image` 能对上图的原因(见 art-http-surface §pack-atlas)。
50
+ * 这里保持同一约定,**不要"顺手支持任意扩展名"**:一旦 `id` 和文件名脱钩,
51
+ * `meta.image` 就会指向不存在的文件,而 atlas 结构合法、只在引擎加载时才炸。
52
+ */
53
+ export function resolveAssetPath(loc, assetId) {
54
+ const id = validateSegment(assetId.replace(/\.(png|jpe?g|webp)$/i, ''));
55
+ return join(resolveAssetsDir(loc), `${id}.png`);
56
+ }
57
+ /** sps 在工作区里的自留地(token / 取消标记等都落这)。 */
58
+ export function resolveSpsDir(loc) {
59
+ return join(resolveProjectDir(loc), '.sps');
60
+ }
61
+ //# sourceMappingURL=assetLocator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assetLocator.js","sourceRoot":"","sources":["../../../src/core/art/assetLocator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAcrE,wEAAwE;AACxE,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,OAAO,IAAI,eAAe,CAAC,QAAQ,CAAC,EAAE,CAAC;AACzC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAmB;IACnD,MAAM,EAAE,GAAG,GAAG,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;IAChF,OAAO,YAAY,CAAC,EAAE,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC;AACzC,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,gBAAgB,CAAC,GAAmB;IAClD,OAAO,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,CAAC;AAChD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAmB,EAAE,OAAe;IACnE,MAAM,EAAE,GAAG,eAAe,CAAC,OAAO,CAAC,OAAO,CAAC,sBAAsB,EAAE,EAAE,CAAC,CAAC,CAAC;IACxE,OAAO,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;AAClD,CAAC;AAED,uCAAuC;AACvC,MAAM,UAAU,aAAa,CAAC,GAAmB;IAC/C,OAAO,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;AAC9C,CAAC"}
@@ -0,0 +1,71 @@
1
+ import { type ProjectLocator } from '../../core/art/assetLocator.js';
2
+ /** 资产类型派发。 */
3
+ export type AssetKind = 'visual' | 'audio';
4
+ /**
5
+ * 🔴 **这一格是暂时的**(与 studio `assetKind` 同判据)。Eddy 已定:音频后续同样由 sps 做成工具产出。
6
+ *
7
+ * 所以这里刻意只有**一个类型别名 + 一行判据** —— **不建策略注册表 / 能力路由 / 可插拔 dispatcher**。
8
+ * 将来接音频时是**补一个 case**,不是改结构。
9
+ *
10
+ * ⚠️ 同理:遍历**全部**资产、对暂不支持的类型**明确跳过并回报**,
11
+ * 而不是 `filter(isVisual)` 把音频从视野里抹掉 ——
12
+ * > **「我只做视觉」和「我目前只支持视觉」是两句话:后者加音频时补一个 case,前者要改结构。**
13
+ * 它顺带兜住"上游拆派发漏了某个 audio 资产"的情况:**回报"跳过了 N 个",而不是静默当作全做完。**
14
+ */
15
+ export declare function assetKind(a: {
16
+ type?: string;
17
+ }): AssetKind;
18
+ export interface ArtAssetLike {
19
+ id: string;
20
+ type?: string;
21
+ desc?: string;
22
+ }
23
+ export interface AssetOutcome {
24
+ id: string;
25
+ /** `done` 已产出 · `skipped` 本版不支持该类型 · `failed` 处理失败 · `cancelled` 取消后未开工 */
26
+ state: 'done' | 'skipped' | 'failed' | 'cancelled';
27
+ reason?: string;
28
+ }
29
+ export interface ArtRunOutcome {
30
+ /** 🔴 终态 `partial` 覆盖中断三来源(用户取消 / 额度耗尽 / 崩溃),**不是 `failed`** —— 半套资产是有效产物。 */
31
+ status: 'succeeded' | 'partial' | 'failed';
32
+ total: number;
33
+ ok: number;
34
+ /** **跳过的要出现在结果里**,否则调用方无从知道"这些没做"。 */
35
+ skipped: AssetOutcome[];
36
+ failed: AssetOutcome[];
37
+ outcomes: AssetOutcome[];
38
+ }
39
+ export interface ArtDriverDeps {
40
+ /** 单个视觉资产的生成 + 后处理。移交件在此接入。 */
41
+ processAsset: (asset: ArtAssetLike, ctx: {
42
+ assetsDir: string;
43
+ projectDir: string;
44
+ }) => Promise<void>;
45
+ /** 里程碑事件(1.12)。**只给 {key, params},绝不给成品文案** —— i18n 字典与渲染都在调用方。 */
46
+ emit?: (key: string, params: Record<string, unknown>) => void;
47
+ }
48
+ /** 读 manifest。读不到 → null(空跑,不抛:没有 manifest 不是错误,是没活干)。 */
49
+ export declare function readArtManifest(projectDir: string): {
50
+ assets: ArtAssetLike[];
51
+ } | null;
52
+ /**
53
+ * 跑一轮美术流水线。
54
+ *
55
+ * 🔴 **取消只在资产边界问一次** —— 半张图停下会留坏文件,而"文件存在就跳过"的幂等
56
+ * 会把它当成已完成:**取消一次就永久污染一个资产,而且不报错**。
57
+ * (崩溃/OOM/硬杀不经过这里 —— 那条靠 `atomicWrite`,两层都要。)
58
+ */
59
+ export declare function runArtPipeline(locator: ProjectLocator, runId: string, deps: ArtDriverDeps): Promise<ArtRunOutcome>;
60
+ /**
61
+ * 逐资产结果 → run 终态。**只此一份**。
62
+ *
63
+ * 🔴 抽出来是因为它有了**第二个调用方**(`artRunChild`:子进程里跑的是移交件的
64
+ * `generateGameArt`,而终态语义在这里)。同一条"什么算 partial"的规则写两遍,
65
+ * 迟早会在一边被改 —— 而两份规则的分歧**不会有任何报错**,只会让同一次运行
66
+ * 在"运行状态"和"事件流"里显示成两回事。
67
+ *
68
+ * 🔴 中断 → `partial`,**不是 `failed`**:已生成的半套资产保留不回滚(删掉等于把已花的钱扔了)。
69
+ */
70
+ export declare function summarizeRun(outcomes: AssetOutcome[], total: number, interrupted: boolean): ArtRunOutcome;
71
+ //# sourceMappingURL=artDriver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artDriver.d.ts","sourceRoot":"","sources":["../../../src/providers/art/artDriver.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAuC,KAAK,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAE1G,cAAc;AACd,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE3C;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAEzD;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,2EAA2E;IAC3E,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,WAAW,CAAC;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,8EAA8E;IAC9E,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,sCAAsC;IACtC,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,QAAQ,EAAE,YAAY,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,+BAA+B;IAC/B,YAAY,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrG,mEAAmE;IACnE,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAC/D;AAED,0DAA0D;AAC1D,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,YAAY,EAAE,CAAA;CAAE,GAAG,IAAI,CASrF;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,aAAa,CAAC,CA8CxB;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,GAAG,aAAa,CAYzG"}