@lumenkey/cli 0.3.1 → 0.4.0-rc.1

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 (182) hide show
  1. package/README.md +36 -2
  2. package/dist/cli/copied.js +86 -85
  3. package/dist/cli/index.js +56 -44
  4. package/dist/cli/render.js +3 -2
  5. package/dist/core/asset-stream.d.ts +16 -0
  6. package/dist/core/asset-stream.js +64 -0
  7. package/dist/core/atomic-config-file.js +8 -7
  8. package/dist/core/chinese-messages.d.ts +4 -0
  9. package/dist/core/chinese-messages.js +907 -0
  10. package/dist/core/chinese-script.d.ts +7 -0
  11. package/dist/core/chinese-script.js +28 -0
  12. package/dist/core/client.js +2 -1
  13. package/dist/core/config-dir.js +2 -1
  14. package/dist/core/config.js +7 -6
  15. package/dist/core/download-root.d.ts +5 -0
  16. package/dist/core/download-root.js +64 -0
  17. package/dist/core/download.js +15 -12
  18. package/dist/core/generation-input.js +3 -2
  19. package/dist/core/generation.d.ts +8 -1
  20. package/dist/core/generation.js +13 -10
  21. package/dist/core/job-display.js +7 -6
  22. package/dist/core/locale.js +163 -148
  23. package/dist/core/outcome.js +6 -4
  24. package/dist/core/output-path.d.ts +3 -1
  25. package/dist/core/output-path.js +5 -2
  26. package/dist/core/params.js +26 -25
  27. package/dist/core/profiles.js +5 -4
  28. package/dist/core/set-values.d.ts +0 -1
  29. package/dist/core/set-values.js +7 -7
  30. package/dist/core/submission-journal.js +31 -16
  31. package/dist/core/version.js +2 -3
  32. package/dist/mcp/server.js +30 -29
  33. package/dist/studio/asset-integrity-hash.d.ts +28 -0
  34. package/dist/studio/asset-integrity-hash.js +93 -0
  35. package/dist/studio/asset-integrity-queue.d.ts +24 -0
  36. package/dist/studio/asset-integrity-queue.js +94 -0
  37. package/dist/studio/asset-store.d.ts +52 -0
  38. package/dist/studio/asset-store.js +352 -0
  39. package/dist/studio/canvas/LICENSES.txt +1590 -0
  40. package/dist/studio/canvas/dependencies.json +862 -0
  41. package/dist/studio/canvas/director-source-manifest.json +47 -0
  42. package/dist/studio/canvas/editor.css +239 -0
  43. package/dist/studio/canvas/editor.js +4613 -0
  44. package/dist/studio/canvas/integration-source-manifest.json +384 -0
  45. package/dist/studio/canvas/source-manifest.json +240 -0
  46. package/dist/studio/canvas-store.d.ts +1254 -0
  47. package/dist/studio/canvas-store.js +205 -0
  48. package/dist/studio/capture-placement.d.ts +11 -0
  49. package/dist/studio/capture-placement.js +85 -0
  50. package/dist/studio/cloud-contract.d.ts +128 -0
  51. package/dist/studio/cloud-contract.js +229 -0
  52. package/dist/studio/cloud-service.d.ts +178 -0
  53. package/dist/studio/cloud-service.js +202 -0
  54. package/dist/studio/director-captures.d.ts +59 -0
  55. package/dist/studio/director-captures.js +141 -0
  56. package/dist/studio/director-contract.d.ts +1489 -0
  57. package/dist/studio/director-contract.js +288 -0
  58. package/dist/studio/director-png.d.ts +6 -0
  59. package/dist/studio/director-png.js +41 -0
  60. package/dist/studio/director-relations.d.ts +40 -0
  61. package/dist/studio/director-relations.js +103 -0
  62. package/dist/studio/document-authority.d.ts +7 -0
  63. package/dist/studio/document-authority.js +50 -0
  64. package/dist/studio/draft-store.d.ts +95 -0
  65. package/dist/studio/draft-store.js +100 -0
  66. package/dist/studio/errors.d.ts +85 -0
  67. package/dist/studio/errors.js +94 -0
  68. package/dist/studio/launch.d.ts +5 -0
  69. package/dist/studio/launch.js +31 -0
  70. package/dist/studio/media.d.ts +9 -0
  71. package/dist/studio/media.js +50 -0
  72. package/dist/studio/output-save-contract.d.ts +1321 -0
  73. package/dist/studio/output-save-contract.js +26 -0
  74. package/dist/studio/output-save-journal.d.ts +971 -0
  75. package/dist/studio/output-save-journal.js +287 -0
  76. package/dist/studio/output-save-recovery.d.ts +18 -0
  77. package/dist/studio/output-save-recovery.js +161 -0
  78. package/dist/studio/output-save-service.d.ts +436 -0
  79. package/dist/studio/output-save-service.js +251 -0
  80. package/dist/studio/output-save-worker.d.ts +40 -0
  81. package/dist/studio/output-save-worker.js +360 -0
  82. package/dist/studio/paths.d.ts +6 -0
  83. package/dist/studio/paths.js +68 -0
  84. package/dist/studio/run-journal.d.ts +993 -0
  85. package/dist/studio/run-journal.js +125 -0
  86. package/dist/studio/server.d.ts +17 -0
  87. package/dist/studio/server.js +440 -0
  88. package/dist/studio/session.d.ts +6 -0
  89. package/dist/studio/session.js +26 -0
  90. package/dist/studio/shell.d.ts +3 -0
  91. package/dist/studio/shell.js +23 -0
  92. package/dist/studio/style.d.ts +2 -0
  93. package/dist/studio/style.js +29 -0
  94. package/dist/studio/types.d.ts +26 -0
  95. package/dist/studio/types.js +2 -0
  96. package/dist/studio/web/app.d.ts +2 -0
  97. package/dist/studio/web/app.js +662 -0
  98. package/dist/studio/web/asset-controller.d.ts +37 -0
  99. package/dist/studio/web/asset-controller.js +191 -0
  100. package/dist/studio/web/asset-model.d.ts +29 -0
  101. package/dist/studio/web/asset-model.js +51 -0
  102. package/dist/studio/web/controller.d.ts +113 -0
  103. package/dist/studio/web/controller.js +401 -0
  104. package/dist/studio/web/messages.d.ts +2 -0
  105. package/dist/studio/web/messages.js +74 -0
  106. package/dist/studio/web/model.d.ts +75 -0
  107. package/dist/studio/web/model.js +182 -0
  108. package/dist/studio/web/save-controller.d.ts +85 -0
  109. package/dist/studio/web/save-controller.js +495 -0
  110. package/dist/studio/web/save-model.d.ts +63 -0
  111. package/dist/studio/web/save-model.js +64 -0
  112. package/dist/studio/web/save-queue.d.ts +7 -0
  113. package/dist/studio/web/save-queue.js +62 -0
  114. package/dist/studio/web/save-view.d.ts +9 -0
  115. package/dist/studio/web/save-view.js +19 -0
  116. package/dist/studio/writer-lock.d.ts +5 -0
  117. package/dist/studio/writer-lock.js +88 -0
  118. package/package.json +11 -5
  119. package/dist/cli/copied.d.ts.map +0 -1
  120. package/dist/cli/copied.js.map +0 -1
  121. package/dist/cli/index.d.ts.map +0 -1
  122. package/dist/cli/index.js.map +0 -1
  123. package/dist/cli/render.d.ts.map +0 -1
  124. package/dist/cli/render.js.map +0 -1
  125. package/dist/core/atomic-config-file.d.ts.map +0 -1
  126. package/dist/core/atomic-config-file.js.map +0 -1
  127. package/dist/core/chat.d.ts.map +0 -1
  128. package/dist/core/chat.js.map +0 -1
  129. package/dist/core/client.d.ts.map +0 -1
  130. package/dist/core/client.js.map +0 -1
  131. package/dist/core/config-dir.d.ts.map +0 -1
  132. package/dist/core/config-dir.js.map +0 -1
  133. package/dist/core/config-error.d.ts.map +0 -1
  134. package/dist/core/config-error.js.map +0 -1
  135. package/dist/core/config.d.ts.map +0 -1
  136. package/dist/core/config.js.map +0 -1
  137. package/dist/core/credit-cap.d.ts.map +0 -1
  138. package/dist/core/credit-cap.js.map +0 -1
  139. package/dist/core/download.d.ts.map +0 -1
  140. package/dist/core/download.js.map +0 -1
  141. package/dist/core/error-result.d.ts.map +0 -1
  142. package/dist/core/error-result.js.map +0 -1
  143. package/dist/core/explain.d.ts.map +0 -1
  144. package/dist/core/explain.js.map +0 -1
  145. package/dist/core/generation-input.d.ts.map +0 -1
  146. package/dist/core/generation-input.js.map +0 -1
  147. package/dist/core/generation-result.d.ts.map +0 -1
  148. package/dist/core/generation-result.js.map +0 -1
  149. package/dist/core/generation.d.ts.map +0 -1
  150. package/dist/core/generation.js.map +0 -1
  151. package/dist/core/job-display.d.ts.map +0 -1
  152. package/dist/core/job-display.js.map +0 -1
  153. package/dist/core/locale.d.ts.map +0 -1
  154. package/dist/core/locale.js.map +0 -1
  155. package/dist/core/origin.d.ts.map +0 -1
  156. package/dist/core/origin.js.map +0 -1
  157. package/dist/core/outcome.d.ts.map +0 -1
  158. package/dist/core/outcome.js.map +0 -1
  159. package/dist/core/output-path.d.ts.map +0 -1
  160. package/dist/core/output-path.js.map +0 -1
  161. package/dist/core/params.d.ts.map +0 -1
  162. package/dist/core/params.js.map +0 -1
  163. package/dist/core/poller.d.ts.map +0 -1
  164. package/dist/core/poller.js.map +0 -1
  165. package/dist/core/profiles.d.ts.map +0 -1
  166. package/dist/core/profiles.js.map +0 -1
  167. package/dist/core/set-values.d.ts.map +0 -1
  168. package/dist/core/set-values.js.map +0 -1
  169. package/dist/core/submission-journal.d.ts.map +0 -1
  170. package/dist/core/submission-journal.js.map +0 -1
  171. package/dist/core/types.d.ts.map +0 -1
  172. package/dist/core/types.js.map +0 -1
  173. package/dist/core/version.d.ts.map +0 -1
  174. package/dist/core/version.js.map +0 -1
  175. package/dist/core/wait.d.ts.map +0 -1
  176. package/dist/core/wait.js.map +0 -1
  177. package/dist/index.d.ts.map +0 -1
  178. package/dist/index.js.map +0 -1
  179. package/dist/mcp/render.d.ts.map +0 -1
  180. package/dist/mcp/render.js.map +0 -1
  181. package/dist/mcp/server.d.ts.map +0 -1
  182. package/dist/mcp/server.js.map +0 -1
@@ -0,0 +1,495 @@
1
+ import { uiError } from './controller.js';
2
+ import { parseReceipt, mergeReceipt, pollDelay, backoffDelay, pendingDelay, isSaving, invalidSaveResponse } from './save-model.js';
3
+ import { createSaveQueue } from './save-queue.js';
4
+ const id = (v) => typeof v === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v);
5
+ const record = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
6
+ const integer = (v) => typeof v === 'number' && Number.isSafeInteger(v) && v >= 0;
7
+ function sameSnapshot(a, b) {
8
+ if (a === b)
9
+ return true;
10
+ if (Array.isArray(a) && Array.isArray(b))
11
+ return a.length === b.length && a.every((v, i) => sameSnapshot(v, b[i]));
12
+ return record(a) && record(b) && Object.keys(a).length === Object.keys(b).length && Object.keys(a).every(key => Object.hasOwn(b, key) && sameSnapshot(a[key], b[key]));
13
+ }
14
+ const reasons = ['LEGACY_AWAITING_FRESH_GET', 'JOB_UNAVAILABLE', 'NO_VERIFIABLE_OUTPUTS', 'IDENTITY_CONFLICT'];
15
+ const blockers = ['SAVE_OUTPUT_ROOT_REQUIRED', 'SAVE_ROOT_NOT_ALLOWED', 'SAVE_OUTPUT_IDENTITY_UNVERIFIED', 'SAVE_OUTPUT_IDENTITY_CONFLICT', 'SAVE_JOB_NOT_TERMINAL', 'SAVE_MEDIA_UNSUPPORTED', 'SAVE_SOURCE_UNAVAILABLE', 'SAVE_OPERATION_EXISTS'];
16
+ function invalid() { throw Object.assign(new Error('本机回复不符合页面契约。'), { code: 'STUDIO_RESPONSE_INVALID' }); }
17
+ function isScope(value) { return record(value) && (value.rootId === null || id(value.rootId)) && id(value.connectionId); }
18
+ export function parseBinding(value) {
19
+ if (!record(value) || !isScope(value.scope))
20
+ return invalid();
21
+ if (value.root === null && value.scope.rootId === null && value.code === 'SAVE_OUTPUT_ROOT_REQUIRED')
22
+ return value;
23
+ if (record(value.root) && typeof value.root.absolutePath === 'string' && value.root.absolutePath.length > 0 && id(value.scope.rootId) && value.code === null)
24
+ return value;
25
+ return invalid();
26
+ }
27
+ export function parseOutputs(value, scope, runId) {
28
+ if (!record(value) || !isScope(value.scope) || value.scope.rootId !== scope.rootId || value.scope.connectionId !== scope.connectionId || !integer(value.revision) || !Array.isArray(value.items))
29
+ return invalid();
30
+ for (const item of value.items) {
31
+ if (!record(item) || item.runId !== runId || !id(item.jobId) || !integer(item.displayOrdinal) || item.displayOrdinal < 1 ||
32
+ !(item.outputIndex === null || integer(item.outputIndex)) || !(item.localOutputId === null || id(item.localOutputId)) || !(item.gatewayAssetId === null || id(item.gatewayAssetId)) ||
33
+ ![null, 'image', 'video', 'audio'].includes(item.outputType) || !(item.mediaType === null || typeof item.mediaType === 'string') || !record(item.identity) ||
34
+ !(item.identity.status === 'VERIFIED' && item.identity.reason === null && id(item.localOutputId) && id(item.gatewayAssetId) || item.identity.status === 'UNVERIFIED' && reasons.includes(item.identity.reason) && item.localOutputId === null && item.gatewayAssetId === null) ||
35
+ !['ACCEPTED', 'NEVER_ACCEPTED', 'UNKNOWN'].includes(item.acceptance) || typeof item.saveAllowed !== 'boolean' ||
36
+ !(item.saveAllowed ? item.saveBlockedCode === null : blockers.includes(item.saveBlockedCode)))
37
+ return invalid();
38
+ if (item.save === null) {
39
+ if (item.saveOperationId !== null || item.acceptance === 'ACCEPTED')
40
+ return invalid();
41
+ }
42
+ else {
43
+ const save = parseReceipt(item.save);
44
+ if (item.saveOperationId !== save.operationId || item.acceptance !== 'ACCEPTED' || item.saveAllowed || save.runId !== runId || save.jobId !== item.jobId || item.localOutputId !== null && save.localOutputId !== item.localOutputId || item.gatewayAssetId !== null && save.gatewayAssetId !== item.gatewayAssetId)
45
+ return invalid();
46
+ }
47
+ }
48
+ return value;
49
+ }
50
+ export function createSaveController(options) {
51
+ const state = { binding: { status: 'idle' }, outputs: {}, operations: {}, targets: {} };
52
+ const clock = options.clock ?? { now: Date.now, setTimeout: (fn, ms) => setTimeout(fn, ms), clearTimeout: (handle) => clearTimeout(handle) };
53
+ const uuid = options.uuid ?? (() => crypto.randomUUID()), queue = options.queue ?? createSaveQueue();
54
+ let follows = new Map(), visible = new Set(), lazy = new Set(), pageVisible = true, timer;
55
+ const mutations = new Map();
56
+ const skipped = Symbol('paused'), expiredRead = Symbol('pending-expired'), cappedRead = Symbol('follow-limit');
57
+ let epoch = 0, binding;
58
+ let reads = new Map();
59
+ const emit = () => options.onChange?.();
60
+ const call = (key, priority, task) => queue.run(`save:${epoch}:${key}`, priority, task);
61
+ function stopTimer() { if (timer !== undefined)
62
+ clock.clearTimeout(timer); timer = undefined; }
63
+ function invalidate() { epoch++; queue.reset('save:'); stopTimer(); follows = new Map(); visible = new Set(); lazy = new Set(); binding = undefined; reads = new Map(); mutations.clear(); state.binding = { status: 'idle' }; state.outputs = {}; state.operations = {}; state.targets = {}; emit(); }
64
+ function bind() {
65
+ if (binding)
66
+ return binding;
67
+ invalidate();
68
+ const started = epoch;
69
+ state.binding = { status: 'loading' };
70
+ emit();
71
+ const pending = (async () => {
72
+ try {
73
+ const value = parseBinding(await call('bind', 1, () => options.api('saves/bind', {})));
74
+ if (started === epoch)
75
+ state.binding = { status: 'ready', value };
76
+ }
77
+ catch (error) {
78
+ if (started === epoch)
79
+ state.binding = { status: 'error', error: uiError(error) };
80
+ }
81
+ finally {
82
+ if (started === epoch) {
83
+ binding = undefined;
84
+ emit();
85
+ }
86
+ }
87
+ })();
88
+ binding = pending;
89
+ return pending;
90
+ }
91
+ function readOutputs(runId, retry = true) {
92
+ if (state.binding.status === 'idle' || state.binding.status === 'loading') {
93
+ const waiting = binding ?? bind(), waitingEpoch = epoch;
94
+ return waiting.then(() => waitingEpoch === epoch ? readOutputs(runId, retry) : undefined);
95
+ }
96
+ const shared = reads.get(runId);
97
+ if (shared)
98
+ return shared;
99
+ const registry = reads;
100
+ const pending = (async () => {
101
+ const started = epoch, bound = state.binding.value;
102
+ if (state.binding.status !== 'ready' || !bound?.root) {
103
+ state.outputs[runId] = { status: 'error', error: state.binding.error ?? uiError({ code: 'SAVE_OUTPUT_ROOT_REQUIRED', message: '尚未设置可用保存目录。' }) };
104
+ emit();
105
+ return;
106
+ }
107
+ const previous = state.outputs[runId]?.value;
108
+ state.outputs[runId] = { status: 'loading', ...(previous ? { value: previous } : {}) };
109
+ emit();
110
+ try {
111
+ const value = parseOutputs(await call(`list:${runId}`, 1, () => options.api(`runs/${runId}/outputs`)), bound.scope, runId);
112
+ if (started === epoch) {
113
+ state.outputs[runId] = { status: 'ready', value };
114
+ for (const output of value.items)
115
+ if (output.save)
116
+ rememberReceipt(output.save);
117
+ }
118
+ }
119
+ catch (error) {
120
+ if (started !== epoch)
121
+ return;
122
+ if (retry && uiError(error).code === 'SAVE_BINDING_REQUIRED') {
123
+ const recovery = bind(), recoveryEpoch = epoch;
124
+ await recovery;
125
+ if (recoveryEpoch === epoch)
126
+ await readOutputs(runId, false);
127
+ return;
128
+ }
129
+ // A refused authority read must not keep displaying old saveAllowed or
130
+ // NEVER_ACCEPTED facts as if they belonged to the current connection.
131
+ state.outputs[runId] = { status: 'error', error: uiError(error) };
132
+ }
133
+ finally {
134
+ if (started === epoch)
135
+ emit();
136
+ }
137
+ })().finally(() => { if (registry.get(runId) === pending)
138
+ registry.delete(runId); });
139
+ registry.set(runId, pending);
140
+ return pending;
141
+ }
142
+ function context(id) { let value = follows.get(id); if (!value) {
143
+ value = { follows: 0, failures: 0 };
144
+ follows.set(id, value);
145
+ } return value; }
146
+ function rememberReceipt(next) {
147
+ const incoming = parseReceipt(next), before = state.operations[incoming.operationId]?.value;
148
+ if (before && incoming.revision < before.revision)
149
+ return before;
150
+ const save = mergeReceipt(before, incoming);
151
+ state.operations[save.operationId] = { status: 'ready', value: save };
152
+ state.targets[save.localOutputId] = { status: 'known', operationId: save.operationId };
153
+ const follow = context(save.operationId);
154
+ if (isSaving(save)) {
155
+ if (!before || !isSaving(before))
156
+ follow.activeSince = clock.now();
157
+ follow.next ??= clock.now() + pollDelay(clock.now() - (follow.activeSince ?? clock.now()));
158
+ }
159
+ else {
160
+ follow.activeSince = undefined;
161
+ if (save.integrityCheck === 'IDLE')
162
+ follow.next = undefined;
163
+ }
164
+ arm();
165
+ emit();
166
+ return save;
167
+ }
168
+ function scopeResponse(value) {
169
+ const bound = state.binding.value;
170
+ if (!record(value) || !bound || !isScope(value.scope) || value.scope.rootId !== bound.scope.rootId || value.scope.connectionId !== bound.scope.connectionId || !integer(value.revision))
171
+ return invalidSaveResponse();
172
+ }
173
+ function receiptResponse(value, id) {
174
+ scopeResponse(value);
175
+ const save = parseReceipt(value.save);
176
+ if (id && save.operationId !== id)
177
+ return invalidSaveResponse();
178
+ return save;
179
+ }
180
+ function transient(error) {
181
+ const status = error?.status ?? uiError(error).httpStatus;
182
+ return status === 503 || status === undefined && (!(error?.code) || uiError(error).code === 'STUDIO_REQUEST_FAILED');
183
+ }
184
+ const cancelled = Symbol('old-epoch');
185
+ async function readBound(route, key, priority, adoptEpoch) {
186
+ const started = epoch;
187
+ try {
188
+ return await call(key, priority, () => options.api(route));
189
+ }
190
+ catch (error) {
191
+ if (started !== epoch)
192
+ return cancelled;
193
+ if (uiError(error).code !== 'SAVE_BINDING_REQUIRED')
194
+ throw error;
195
+ const recovery = bind(), recovering = epoch;
196
+ await recovery;
197
+ if (recovering !== epoch)
198
+ return cancelled;
199
+ adoptEpoch(recovering);
200
+ if (state.binding.status !== 'ready')
201
+ throw state.binding.error;
202
+ // This is a GET only. A second REQUIRED/MISMATCH is returned, never looped.
203
+ return call(key, priority, () => options.api(route));
204
+ }
205
+ }
206
+ function expirePending(id, follow) {
207
+ if (follow.pendingSince !== undefined && clock.now() >= follow.pendingSince + 120_000) {
208
+ follow.next = undefined;
209
+ if (state.operations[id])
210
+ state.operations[id].notice = follow.failures ? '暂不可核对 · 核对保存状态' : '核验尚未完成 · 核对保存状态';
211
+ queue.cancel(`save:${epoch}:operation:${id}`, expiredRead);
212
+ return true;
213
+ }
214
+ return false;
215
+ }
216
+ function arm() {
217
+ stopTimer();
218
+ if (!pageVisible)
219
+ return;
220
+ const deadlines = [];
221
+ for (const [id, follow] of follows) {
222
+ if (expirePending(id, follow))
223
+ continue;
224
+ if (follow.busy) {
225
+ if (follow.pendingSince !== undefined)
226
+ deadlines.push(follow.pendingSince + 120_000);
227
+ continue;
228
+ }
229
+ if (follow.next !== undefined)
230
+ deadlines.push(follow.pendingSince === undefined ? follow.next : Math.min(follow.next, follow.pendingSince + 120_000));
231
+ }
232
+ if (deadlines.length)
233
+ timer = clock.setTimeout(tick, Math.max(0, Math.min(...deadlines) - clock.now()));
234
+ }
235
+ function tick() {
236
+ timer = undefined;
237
+ if (!pageVisible)
238
+ return;
239
+ for (const [id, follow] of follows) {
240
+ if (expirePending(id, follow) || follow.busy)
241
+ continue;
242
+ if (follow.next !== undefined && follow.next <= clock.now())
243
+ void readOperation(id, { automatic: true });
244
+ }
245
+ arm();
246
+ emit();
247
+ }
248
+ function readOperation(id, readOptions = {}) {
249
+ let follow = context(id);
250
+ if (follow.busy)
251
+ return follow.busy;
252
+ const current = state.operations[id]?.value;
253
+ if (!current || state.binding.status !== 'ready')
254
+ return Promise.resolve();
255
+ let started = epoch;
256
+ const originalNext = follow.next;
257
+ follow.next = undefined;
258
+ if (readOptions.manual) {
259
+ follow.pendingSince = undefined;
260
+ follow.follows = 0;
261
+ follow.failures = 0;
262
+ }
263
+ const previousObservation = state.operations[id];
264
+ const loadingObservation = { ...previousObservation, status: 'loading' };
265
+ state.operations[id] = loadingObservation;
266
+ emit();
267
+ function acceptRead(incoming) {
268
+ const held = state.operations[id]?.value;
269
+ if (held && incoming.revision < held.revision) {
270
+ if (state.operations[id] === loadingObservation)
271
+ state.operations[id] = previousObservation;
272
+ follow.next ??= originalNext;
273
+ return;
274
+ }
275
+ const save = rememberReceipt(incoming);
276
+ follow.failures = 0;
277
+ if (isSaving(save))
278
+ follow.next = clock.now() + pollDelay(clock.now() - (follow.activeSince ?? clock.now()));
279
+ else if (save.state === 'SAVED' && save.integrityCheck === 'PENDING') {
280
+ follow.pendingSince ??= clock.now();
281
+ if (!expirePending(id, follow))
282
+ follow.next = follow.follows < 13 ? clock.now() + pendingDelay(follow.follows) : follow.pendingSince + 120_000;
283
+ }
284
+ else {
285
+ follow.next = undefined;
286
+ follow.pendingSince = undefined;
287
+ }
288
+ }
289
+ const pending = (async () => {
290
+ try {
291
+ const value = await call(`operation:${id}`, isSaving(current) ? 0 : readOptions.manual ? 1 : 2, async () => {
292
+ if (started !== epoch || !pageVisible || readOptions.lazy && !visible.has(id))
293
+ return skipped;
294
+ if (expirePending(id, follow))
295
+ return expiredRead;
296
+ if (readOptions.automatic && follow.pendingSince !== undefined && follow.follows >= 13 && !isSaving(current))
297
+ return cappedRead;
298
+ if (readOptions.lazy)
299
+ lazy.add(id);
300
+ if (readOptions.automatic && follow.pendingSince !== undefined && !isSaving(current))
301
+ follow.follows++;
302
+ return options.api(`saves/${id}`);
303
+ });
304
+ if (started !== epoch)
305
+ return;
306
+ if (value === skipped || value === expiredRead || value === cappedRead) {
307
+ if (value === skipped)
308
+ follow.next = originalNext;
309
+ if (value === cappedRead)
310
+ follow.next = follow.pendingSince + 120_000;
311
+ if (state.operations[id] === loadingObservation)
312
+ state.operations[id] = { ...previousObservation, ...(state.operations[id].notice ? { notice: state.operations[id].notice } : {}) };
313
+ return;
314
+ }
315
+ acceptRead(receiptResponse(value, id));
316
+ }
317
+ catch (error) {
318
+ if (started !== epoch)
319
+ return;
320
+ if (uiError(error).code === 'SAVE_BINDING_REQUIRED') {
321
+ try {
322
+ const recovery = bind(), recovering = epoch;
323
+ await recovery;
324
+ if (recovering !== epoch)
325
+ return;
326
+ started = recovering;
327
+ follow = context(id);
328
+ if (state.binding.status !== 'ready')
329
+ throw state.binding.error;
330
+ const save = receiptResponse(await call(`operation:${id}`, 1, () => options.api(`saves/${id}`)), id);
331
+ if (started === epoch) {
332
+ acceptRead(save);
333
+ // Rebinding removed old-scope rows. Re-read the authorized list so
334
+ // the recovered receipt remains reachable in the actual page.
335
+ await readOutputs(save.runId, false);
336
+ }
337
+ }
338
+ catch (retryError) {
339
+ if (started === epoch)
340
+ state.operations[id] = { status: 'error', error: uiError(retryError), notice: '保存结果尚未确认 · 核对保存状态' };
341
+ }
342
+ return;
343
+ }
344
+ state.operations[id] = { ...state.operations[id], status: 'error', error: uiError(error), notice: '保存结果尚未确认 · 核对保存状态' };
345
+ if (transient(error) && (isSaving(current) || follow.pendingSince !== undefined) && !expirePending(id, follow))
346
+ follow.next = clock.now() + backoffDelay(follow.failures++);
347
+ else
348
+ follow.next = undefined;
349
+ }
350
+ finally {
351
+ if (started === epoch) {
352
+ follow.busy = undefined;
353
+ arm();
354
+ emit();
355
+ }
356
+ }
357
+ })();
358
+ follow.busy = pending;
359
+ return pending;
360
+ }
361
+ function markVisible(id, onScreen) {
362
+ if (onScreen)
363
+ visible.add(id);
364
+ else
365
+ visible.delete(id);
366
+ if (pageVisible && onScreen && !lazy.has(id) && state.operations[id]?.value?.state === 'SAVED')
367
+ void readOperation(id, { lazy: true });
368
+ }
369
+ function setPageVisible(value) {
370
+ pageVisible = value;
371
+ if (value)
372
+ for (const id of visible)
373
+ if (!lazy.has(id) && state.operations[id]?.value?.state === 'SAVED')
374
+ void readOperation(id, { lazy: true });
375
+ arm();
376
+ emit();
377
+ }
378
+ function checkOutput(runId, outputId) {
379
+ const key = `${epoch}:check:${outputId}`, shared = mutations.get(key);
380
+ if (shared)
381
+ return shared;
382
+ let started = epoch;
383
+ state.targets[outputId] = { ...state.targets[outputId] ?? { status: 'unknown' }, checking: true };
384
+ emit();
385
+ const pending = (async () => {
386
+ try {
387
+ const value = await readBound(`runs/${runId}/outputs/${outputId}/save`, state.targets[outputId]?.operationId ? `operation:${state.targets[outputId].operationId}` : key, 1, value => { started = value; });
388
+ if (value === cancelled || started !== epoch)
389
+ return;
390
+ scopeResponse(value);
391
+ const parsed = parseOutputs({ scope: value.scope, revision: value.revision, items: [value.output] }, value.scope, runId).items[0];
392
+ if ((parsed.localOutputId ?? parsed.save?.localOutputId) !== outputId)
393
+ return invalidSaveResponse();
394
+ if (value.acceptance !== parsed.acceptance || (value.save === null) !== (parsed.save === null))
395
+ return invalidSaveResponse();
396
+ if (parsed.save) {
397
+ const save = parseReceipt(value.save);
398
+ if (!sameSnapshot(save, parsed.save))
399
+ return invalidSaveResponse();
400
+ rememberReceipt(save);
401
+ }
402
+ else if (parsed.acceptance === 'NEVER_ACCEPTED' && parsed.saveAllowed)
403
+ state.targets[outputId] = { status: 'idle' };
404
+ else
405
+ state.targets[outputId] = { status: 'unknown' };
406
+ const list = state.outputs[runId]?.value;
407
+ if (list)
408
+ list.items = list.items.map(item => (item.localOutputId ?? item.save?.localOutputId) === outputId ? parsed : item);
409
+ }
410
+ catch (error) {
411
+ if (started === epoch)
412
+ state.targets[outputId] = { status: 'unknown', error: uiError(error) };
413
+ }
414
+ finally {
415
+ mutations.delete(key);
416
+ if (started === epoch) {
417
+ if (state.targets[outputId])
418
+ state.targets[outputId].checking = false;
419
+ emit();
420
+ }
421
+ }
422
+ })();
423
+ mutations.set(key, pending);
424
+ return pending;
425
+ }
426
+ function saveOutput(runId, outputId) {
427
+ const key = `${epoch}:save:${outputId}`, shared = mutations.get(key);
428
+ if (shared)
429
+ return shared;
430
+ const output = state.outputs[runId]?.value?.items.find(o => o.localOutputId === outputId), target = state.targets[outputId];
431
+ if (state.binding.status !== 'ready' || !output || output.acceptance !== 'NEVER_ACCEPTED' || !output.saveAllowed || target && target.status !== 'idle')
432
+ return Promise.resolve();
433
+ const started = epoch;
434
+ state.targets[outputId] = { status: 'submitting' };
435
+ emit();
436
+ const pending = (async () => {
437
+ try {
438
+ const value = await call(key, 1, () => options.api(`runs/${runId}/outputs/${outputId}/save`, { requestId: uuid() }));
439
+ if (started !== epoch)
440
+ return;
441
+ const save = receiptResponse(value);
442
+ if (save.localOutputId !== outputId || save.runId !== runId)
443
+ return invalidSaveResponse();
444
+ rememberReceipt(save);
445
+ }
446
+ catch (error) {
447
+ if (started !== epoch)
448
+ return;
449
+ state.targets[outputId] = { status: 'unknown', error: uiError(error) };
450
+ emit();
451
+ await checkOutput(runId, outputId);
452
+ }
453
+ finally {
454
+ mutations.delete(key);
455
+ if (started === epoch)
456
+ emit();
457
+ }
458
+ })();
459
+ mutations.set(key, pending);
460
+ return pending;
461
+ }
462
+ function retryOperation(id) {
463
+ const key = `${epoch}:retry:${id}`, shared = mutations.get(key);
464
+ if (shared)
465
+ return shared;
466
+ const save = state.operations[id]?.value;
467
+ if (!save?.safeRetry || !['FAILED', 'RECOVERY_REQUIRED'].includes(save.state) || state.operations[id]?.status !== 'ready' || context(id).busy)
468
+ return Promise.resolve();
469
+ const started = epoch;
470
+ state.operations[id] = { ...state.operations[id], status: 'loading' };
471
+ emit();
472
+ const pending = (async () => {
473
+ try {
474
+ const value = await call(`operation:${id}`, 1, () => options.api(`saves/${id}/retry`, { expectedRevision: save.revision }));
475
+ if (started === epoch)
476
+ rememberReceipt(receiptResponse(value, id));
477
+ }
478
+ catch (error) {
479
+ if (started === epoch) {
480
+ state.operations[id] = { ...state.operations[id], status: 'error', error: uiError(error), notice: '保存结果尚未确认 · 核对保存状态' };
481
+ await readOperation(id, { manual: true });
482
+ }
483
+ }
484
+ finally {
485
+ mutations.delete(key);
486
+ if (started === epoch)
487
+ emit();
488
+ }
489
+ })();
490
+ mutations.set(key, pending);
491
+ return pending;
492
+ }
493
+ return { state, invalidate, bind, readOutputs, rememberReceipt, readOperation, checkOutput, saveOutput, retryOperation, markVisible, setPageVisible, dispose: invalidate };
494
+ }
495
+ //# sourceMappingURL=save-controller.js.map
@@ -0,0 +1,63 @@
1
+ export type SaveStage = 'QUEUED' | 'DOWNLOADING' | 'FILE_READY' | 'REGISTERED' | 'LINKED' | 'RECONCILING';
2
+ export interface SaveReceipt {
3
+ operationId: string;
4
+ revision: number;
5
+ localOutputId: string;
6
+ runId: string;
7
+ jobId: string;
8
+ gatewayAssetId: string;
9
+ state: 'ACCEPTED' | 'SAVING' | 'SAVED' | 'FAILED' | 'RECOVERY_REQUIRED' | 'BLOCKED';
10
+ stage: SaveStage;
11
+ attempt: number;
12
+ facts: {
13
+ file: 'NOT_CHECKED' | 'ABSENT' | 'PARTIAL' | 'VERIFIED' | 'MISSING' | 'MODIFIED' | 'UNSAFE' | 'UNKNOWN';
14
+ registration: 'ABSENT' | 'PRESENT' | 'UNKNOWN';
15
+ relation: 'ABSENT' | 'PRESENT' | 'UNKNOWN';
16
+ };
17
+ asset: {
18
+ assetId: string;
19
+ displayName: string;
20
+ mediaType: string;
21
+ byteLength: number;
22
+ sha256: string;
23
+ relativePath: string;
24
+ absolutePath: string;
25
+ contentUrl: string;
26
+ } | null;
27
+ progress: {
28
+ receivedBytes: number;
29
+ totalBytes: number | null;
30
+ } | null;
31
+ error: {
32
+ code: string;
33
+ message: string;
34
+ type: string;
35
+ httpStatus?: number;
36
+ retryable: boolean;
37
+ mayAlreadyBeSaved: boolean | null;
38
+ } | null;
39
+ retryable: boolean;
40
+ safeRetry: boolean;
41
+ mayAlreadyBeSaved: boolean | null;
42
+ checkedAt: string | null;
43
+ integrityCheck: 'IDLE' | 'PENDING';
44
+ createdAt: string;
45
+ updatedAt: string;
46
+ }
47
+ export declare const isRecord: (v: unknown) => v is Record<string, unknown>;
48
+ export declare const isId: (v: unknown) => v is string;
49
+ export declare function invalidSaveResponse(): never;
50
+ export declare function parseReceipt(v: unknown): SaveReceipt;
51
+ export declare function mergeReceipt(before: SaveReceipt | undefined, next: SaveReceipt): SaveReceipt;
52
+ export declare const pollDelay: (elapsed: number) => 1000 | 5000 | 2000;
53
+ export declare const backoffDelay: (failures: number) => number;
54
+ export declare const pendingDelay: (read: number) => 5000 | 10000 | 2000;
55
+ export declare const isSaving: (save: SaveReceipt) => boolean;
56
+ export declare function receiptPresentation(save: SaveReceipt, observationFailed?: boolean): {
57
+ status: string;
58
+ progress: string;
59
+ canRetry: boolean;
60
+ error: string;
61
+ stage: "排队中" | "接收文件" | "文件已写入" | "素材已登记" | "记录已关联" | "核对原保存记录";
62
+ };
63
+ //# sourceMappingURL=save-model.d.ts.map
@@ -0,0 +1,64 @@
1
+ import { messageForCode } from './messages.js';
2
+ export const isRecord = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
3
+ export const isId = (v) => typeof v === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v);
4
+ const integer = (v) => typeof v === 'number' && Number.isSafeInteger(v) && v >= 0;
5
+ const string = (v) => typeof v === 'string' && v.length > 0;
6
+ const date = (v) => string(v) && Number.isFinite(Date.parse(v));
7
+ const nullableBoolean = (v) => v === null || typeof v === 'boolean';
8
+ export function invalidSaveResponse() { throw Object.assign(new Error('保存回复不符合页面契约。'), { code: 'STUDIO_RESPONSE_INVALID' }); }
9
+ export function parseReceipt(v) {
10
+ if (!isRecord(v) || !['operationId', 'localOutputId', 'runId', 'jobId', 'gatewayAssetId'].every(k => isId(v[k])) || !integer(v.revision) || v.revision < 1 || !integer(v.attempt) ||
11
+ !['ACCEPTED', 'SAVING', 'SAVED', 'FAILED', 'RECOVERY_REQUIRED', 'BLOCKED'].includes(v.state) || !['QUEUED', 'DOWNLOADING', 'FILE_READY', 'REGISTERED', 'LINKED', 'RECONCILING'].includes(v.stage) ||
12
+ !isRecord(v.facts) || !['NOT_CHECKED', 'ABSENT', 'PARTIAL', 'VERIFIED', 'MISSING', 'MODIFIED', 'UNSAFE', 'UNKNOWN'].includes(v.facts.file) || !['ABSENT', 'PRESENT', 'UNKNOWN'].includes(v.facts.registration) || !['ABSENT', 'PRESENT', 'UNKNOWN'].includes(v.facts.relation) ||
13
+ typeof v.retryable !== 'boolean' || typeof v.safeRetry !== 'boolean' || !nullableBoolean(v.mayAlreadyBeSaved) || !(v.checkedAt === null || date(v.checkedAt)) || !['IDLE', 'PENDING'].includes(v.integrityCheck) || !date(v.createdAt) || !date(v.updatedAt))
14
+ return invalidSaveResponse();
15
+ if (v.progress !== null && (!isRecord(v.progress) || !integer(v.progress.receivedBytes) || !(v.progress.totalBytes === null || integer(v.progress.totalBytes) && v.progress.totalBytes > 0)))
16
+ return invalidSaveResponse();
17
+ if (v.error !== null && (!isRecord(v.error) || !string(v.error.code) || typeof v.error.message !== 'string' || !string(v.error.type) || typeof v.error.retryable !== 'boolean' || !nullableBoolean(v.error.mayAlreadyBeSaved) || v.error.httpStatus !== undefined && (!integer(v.error.httpStatus) || v.error.httpStatus < 400 || v.error.httpStatus > 599)))
18
+ return invalidSaveResponse();
19
+ if (v.asset !== null && (!isRecord(v.asset) || !isId(v.asset.assetId) || !['displayName', 'relativePath', 'absolutePath', 'contentUrl'].every(k => string(v.asset[k])) || !['image/png', 'image/jpeg', 'image/webp', 'video/mp4', 'video/webm'].includes(v.asset.mediaType) || !integer(v.asset.byteLength) || v.asset.byteLength < 1 || typeof v.asset.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(v.asset.sha256)))
20
+ return invalidSaveResponse();
21
+ return v;
22
+ }
23
+ export function mergeReceipt(before, next) {
24
+ if (!before)
25
+ return next;
26
+ for (const key of ['operationId', 'localOutputId', 'runId', 'jobId', 'gatewayAssetId', 'createdAt'])
27
+ if (before[key] !== next[key])
28
+ return invalidSaveResponse();
29
+ if (next.revision < before.revision)
30
+ return before;
31
+ if (next.revision > before.revision)
32
+ return next;
33
+ if (next.attempt !== before.attempt || next.state !== before.state || next.stage !== before.stage)
34
+ return invalidSaveResponse();
35
+ const progress = next.progress && before.progress ? { receivedBytes: Math.max(next.progress.receivedBytes, before.progress.receivedBytes), totalBytes: next.progress.totalBytes ?? before.progress.totalBytes } : next.progress ?? before.progress;
36
+ return { ...next, progress };
37
+ }
38
+ export const pollDelay = (elapsed) => elapsed < 10_000 ? 1000 : elapsed < 60_000 ? 2000 : 5000;
39
+ export const backoffDelay = (failures) => [2000, 4000, 8000, 16000, 30000][Math.min(failures, 4)];
40
+ export const pendingDelay = (read) => read === 0 ? 2000 : read === 1 ? 5000 : 10000;
41
+ export const isSaving = (save) => save.state === 'ACCEPTED' || save.state === 'SAVING';
42
+ export function receiptPresentation(save, observationFailed = false) {
43
+ let status;
44
+ if (isSaving(save))
45
+ status = ['FILE_READY', 'REGISTERED'].includes(save.stage) ? '文件已写入,正在核对记录' : save.state === 'ACCEPTED' ? '等待保存到本机' : '正在保存到本机';
46
+ else if (save.state === 'SAVED') {
47
+ if (['MISSING', 'MODIFIED', 'UNSAFE'].includes(save.facts.file))
48
+ status = '曾保存,文件现已' + { MISSING: '缺失', MODIFIED: '变更', UNSAFE: '不安全' }[save.facts.file];
49
+ else if (save.integrityCheck === 'PENDING')
50
+ status = '曾保存,正在检查';
51
+ else if (save.facts.file === 'VERIFIED' && save.facts.registration === 'PRESENT' && save.facts.relation === 'PRESENT' && save.checkedAt)
52
+ status = '已保存到本机(当前已核验)';
53
+ else
54
+ status = save.facts.file === 'NOT_CHECKED' ? '已保存 · 尚未核验' : '曾保存,当前状态未确认';
55
+ }
56
+ else
57
+ status = save.state === 'FAILED' ? '未保存成功' : save.state === 'BLOCKED' ? '需要人工处理' : '文件保存需要恢复';
58
+ if (observationFailed)
59
+ status = save.state === 'SAVED' ? '曾保存,当前状态未确认' : '保存结果尚未确认';
60
+ const progress = !isSaving(save) ? '' : save.progress === null ? '正在保存,进度暂不可用' : save.progress.totalBytes === null ? `已接收 ${save.progress.receivedBytes.toLocaleString()} 字节` : `已接收 ${save.progress.receivedBytes.toLocaleString()} / ${save.progress.totalBytes.toLocaleString()} 字节`;
61
+ const interrupted = !save.error && ['FAILED', 'RECOVERY_REQUIRED'].includes(save.state) ? '保存中断' + (save.safeRetry ? ',可重试原保存' : ',请核对原保存记录') : '';
62
+ return { status, progress, canRetry: save.safeRetry && ['FAILED', 'RECOVERY_REQUIRED'].includes(save.state), error: save.error ? messageForCode(save.error.code, save.error.message) + ` (${save.error.code})` : interrupted, stage: { QUEUED: '排队中', DOWNLOADING: '接收文件', FILE_READY: '文件已写入', REGISTERED: '素材已登记', LINKED: '记录已关联', RECONCILING: '核对原保存记录' }[save.stage] };
63
+ }
64
+ //# sourceMappingURL=save-model.js.map
@@ -0,0 +1,7 @@
1
+ export declare function createSaveQueue(): {
2
+ has: (key: string) => boolean;
3
+ cancel(key: string, value: unknown): void;
4
+ run<T>(key: string, priority: number, task: () => Promise<T>): Promise<T>;
5
+ reset(prefix?: string): void;
6
+ };
7
+ //# sourceMappingURL=save-queue.d.ts.map