@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,662 @@
1
+ import { createStudioController, createLocalApi, uiError } from './controller.js';
2
+ import { describeFields, modelsFor, runLabel, runDiagnosticText, runRecoveryText, safeOutputUrl, compactLocation, fieldDisplayLabel, fieldPlacement } from './model.js';
3
+ import { outputPresentation, canQueryRun } from './save-view.js';
4
+ import { messageForCode } from './messages.js';
5
+ import { receiptPresentation } from './save-model.js';
6
+ import { assetPresentation, assetFileLink } from './asset-model.js';
7
+ const el = (id) => document.getElementById(id);
8
+ const button = (id) => el(id);
9
+ function text(id, value) { el(id).textContent = value; }
10
+ function show(id, visible) { el(id).hidden = !visible; }
11
+ function node(tag, content, className) {
12
+ const n = document.createElement(tag);
13
+ if (content !== undefined)
14
+ n.textContent = content;
15
+ if (className)
16
+ n.className = className;
17
+ return n;
18
+ }
19
+ function errorText(error) { return error ? `${messageForCode(error.code, error.message)} (${error.code})` : ''; }
20
+ function disabled(id, value, reason) {
21
+ const control = el(id);
22
+ control.disabled = value;
23
+ let hint = document.getElementById(id + '-reason');
24
+ if (!hint) {
25
+ hint = node('small', undefined, 'disabled-reason');
26
+ hint.id = id + '-reason';
27
+ control.after(hint);
28
+ }
29
+ hint.hidden = !value;
30
+ hint.textContent = value ? reason : '';
31
+ if (value) {
32
+ control.title = reason;
33
+ control.setAttribute('aria-describedby', hint.id);
34
+ }
35
+ else {
36
+ control.removeAttribute('title');
37
+ control.removeAttribute('aria-describedby');
38
+ }
39
+ }
40
+ const api = createLocalApi(fetch, () => controller.expireSession());
41
+ let view = 'create', fieldSignature = '', selectSignature = '', runSignature = '', assetsSignature = '', currentSignature = '', uploading = false;
42
+ let canvasDirty = false, canvasLoading = false;
43
+ let previousAssetsStatus = 'idle';
44
+ let canvasHost;
45
+ function canvasEnvironment(state) {
46
+ return { api, onOpenRuns: () => {
47
+ const navigate = () => { view = 'tasks'; void controller.refreshCloud(); render(controller.state); };
48
+ if (canvasHost)
49
+ canvasHost.requestLeave(navigate);
50
+ else
51
+ navigate();
52
+ }, onDirty: (value) => { canvasDirty = value; }, active: view === 'canvas' && !state.sessionExpired, configured: !!state.local.value?.storage.configured && !state.sessionExpired, connected: state.connected,
53
+ models: state.catalog.status === 'ready' ? state.catalog.value ?? [] : [], assets: (state.assets.value?.assets ?? []).map(a => ({ id: a.assetId, name: a.displayName, mediaType: a.mediaType.startsWith('image/') ? 'image' : 'video' })), assetError: state.assets.status === 'error' ? errorText(state.assets.error) : undefined,
54
+ theme: document.documentElement.dataset.theme === 'light' ? 'light' : 'dark', nonce: document.querySelector('meta[name="studio-style-nonce"]')?.content ?? '' };
55
+ }
56
+ function renderCanvas(state) {
57
+ document.querySelector('.workspace')?.classList.toggle('canvas-mode', view === 'canvas');
58
+ if (canvasHost) {
59
+ canvasHost.update(canvasEnvironment(state));
60
+ return;
61
+ }
62
+ if (view !== 'canvas' || canvasLoading || state.sessionExpired)
63
+ return;
64
+ canvasLoading = true;
65
+ const modulePath = '/studio/canvas/editor.js';
66
+ void import(modulePath).then(module => { canvasHost = module.mountStudioCanvas(el('canvas-view'), canvasEnvironment(controller.state)); }).catch(error => { canvasLoading = false; controller.reportError(error); });
67
+ }
68
+ const dialog = el('confirm-dialog');
69
+ let dialogRunId;
70
+ const controller = createStudioController({ api, onChange: render });
71
+ const observed = new Set();
72
+ const outputObserver = new IntersectionObserver(entries => {
73
+ for (const entry of entries) {
74
+ const id = entry.target.dataset.saveOperation;
75
+ const bounds = entry.target.getBoundingClientRect(), header = document.querySelector('.runtime-header').getBoundingClientRect();
76
+ if (id)
77
+ controller.markSaveVisible(id, entry.target.isConnected && entry.isIntersecting && entry.intersectionRatio > 0 && bounds.height > 0 && bounds.bottom > header.bottom && bounds.top < innerHeight);
78
+ }
79
+ });
80
+ function observeOutputs() {
81
+ outputObserver.disconnect();
82
+ const next = new Set();
83
+ for (const row of document.querySelectorAll('[data-save-operation]')) {
84
+ const bounds = row.getBoundingClientRect(), id = row.dataset.saveOperation;
85
+ if (bounds.height > 0) {
86
+ next.add(id);
87
+ outputObserver.observe(row);
88
+ if (bounds.bottom <= 0 || bounds.top >= innerHeight)
89
+ controller.markSaveVisible(id, false);
90
+ }
91
+ }
92
+ // Replacing a row to show "checking" must not itself turn that same visible
93
+ // row off-screen, cancel its queued lazy read, and trigger a render/read loop.
94
+ for (const id of observed)
95
+ if (!next.has(id))
96
+ controller.markSaveVisible(id, false);
97
+ observed.clear();
98
+ for (const id of next)
99
+ observed.add(id);
100
+ }
101
+ const observedAssets = new Set();
102
+ const assetObserver = new IntersectionObserver(entries => {
103
+ for (const entry of entries) {
104
+ const row = entry.target, bounds = row.getBoundingClientRect(), header = document.querySelector('.runtime-header').getBoundingClientRect();
105
+ if (row.dataset.assetId)
106
+ controller.markAssetVisible(row.dataset.assetId, row.isConnected && entry.isIntersecting && entry.intersectionRatio > 0 && bounds.height > 0 && bounds.bottom > header.bottom && bounds.top < innerHeight);
107
+ }
108
+ });
109
+ function observeAssets() {
110
+ assetObserver.disconnect();
111
+ const next = new Set();
112
+ for (const row of document.querySelectorAll('.asset-card[data-asset-id]')) {
113
+ const bounds = row.getBoundingClientRect(), id = row.dataset.assetId;
114
+ if (bounds.height > 0) {
115
+ next.add(id);
116
+ assetObserver.observe(row);
117
+ if (bounds.bottom <= 0 || bounds.top >= innerHeight)
118
+ controller.markAssetVisible(id, false);
119
+ }
120
+ }
121
+ for (const id of observedAssets)
122
+ if (!next.has(id))
123
+ controller.markAssetVisible(id, false);
124
+ observedAssets.clear();
125
+ for (const id of next)
126
+ observedAssets.add(id);
127
+ }
128
+ function renderFields(state) {
129
+ const draft = state.edits[state.mode], models = modelsFor(state.catalog.value ?? [], state.mode);
130
+ const model = models.find(m => m.id === draft.modelId);
131
+ const signature = JSON.stringify([state.mode, state.catalog.status, models.map(m => [m.id, m.displayName, m.name]), draft.modelId]);
132
+ const select = el('model');
133
+ if (selectSignature !== signature) {
134
+ selectSignature = signature;
135
+ select.replaceChildren();
136
+ const empty = node('option', models.length ? '请选择模型' : state.connected ? '没有可用模型或目录尚未就绪' : '连接云端后加载目录');
137
+ empty.value = '';
138
+ select.append(empty);
139
+ for (const m of models) {
140
+ const option = node('option', `${m.displayName ?? m.name ?? m.id} · ${m.id}`);
141
+ option.value = m.id;
142
+ select.append(option);
143
+ }
144
+ if (draft.modelId && !model) {
145
+ const unavailable = node('option', `${draft.modelId}(当前目录不可用)`);
146
+ unavailable.value = draft.modelId;
147
+ select.append(unavailable);
148
+ }
149
+ select.value = draft.modelId;
150
+ }
151
+ disabled('model', state.catalog.status !== 'ready' || state.submitBusy, state.submitBusy ? '正在提交,请等待结果。' : !state.connected ? '先连接云端,再选择模型;当前输入会保留。' : state.catalog.status === 'loading' ? '目录读取中;保留上次模型和输入。' : '目录未就绪,请刷新目录后再选择。');
152
+ const fields = model ? describeFields(model) : [{ key: 'prompt', title: '描述你的想法', kind: 'text', spec: { type: 'string' }, required: false }];
153
+ for (const key of Object.keys(draft.fields))
154
+ if (!fields.some(f => f.key === key))
155
+ fields.push({ key, title: key + '(目录未声明,清空后才能报价)', kind: 'text', spec: {}, required: false });
156
+ const unmatched = fields.filter(f => (f.kind === 'enum' || f.kind === 'boolean') && draft.fields[f.key] && !(f.options ?? [{ value: 'true' }, { value: 'false' }]).some(o => o.value === draft.fields[f.key])).map(f => [f.key, draft.fields[f.key]]);
157
+ const shape = JSON.stringify([state.mode, draft.modelId, fields, unmatched]);
158
+ if (fieldSignature !== shape) {
159
+ fieldSignature = shape;
160
+ el('prompt-fields').replaceChildren();
161
+ el('parameter-fields').replaceChildren();
162
+ el('advanced-fields').replaceChildren();
163
+ for (const [index, field] of fields.entries()) {
164
+ const wrapper = node('div', undefined, 'field'), label = node('label', fieldDisplayLabel(field));
165
+ const id = 'field-' + index;
166
+ label.htmlFor = id;
167
+ let control;
168
+ if (field.kind === 'enum' || field.kind === 'boolean') {
169
+ control = node('select');
170
+ const empty = node('option', '留空 · 由服务按目录处理');
171
+ empty.value = '';
172
+ control.append(empty);
173
+ for (const option of field.options ?? [{ value: 'true', label: '是' }, { value: 'false', label: '否' }]) {
174
+ const item = node('option', option.label);
175
+ item.value = option.value;
176
+ control.append(item);
177
+ }
178
+ if (unmatched.some(([key]) => key === field.key)) {
179
+ const item = node('option', '已保存值 ' + draft.fields[field.key] + '(需重新选择)');
180
+ item.value = draft.fields[field.key];
181
+ control.append(item);
182
+ }
183
+ }
184
+ else if (field.kind === 'json' || field.kind === 'text')
185
+ control = node('textarea');
186
+ else {
187
+ control = node('input');
188
+ control.type = 'text';
189
+ if (field.kind === 'number')
190
+ control.inputMode = 'decimal';
191
+ }
192
+ control.id = id;
193
+ control.dataset.field = field.key;
194
+ control.autocomplete = 'off';
195
+ if (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) {
196
+ control.maxLength = 128 * 1024;
197
+ control.placeholder = field.kind === 'json' ? '按目录填写 JSON,或留空' : field.kind === 'text' ? '按目录说明填写内容' : '留空,由服务按目录处理';
198
+ }
199
+ control.addEventListener(control instanceof HTMLSelectElement ? 'change' : 'input', () => controller.setField(field.key, control.value));
200
+ wrapper.append(label, control);
201
+ if (field.defaultHint !== undefined)
202
+ wrapper.append(node('small', `目录默认:${field.defaultHint}(未填写时由服务按目录默认值写入报价)`));
203
+ if (field.kind === 'json')
204
+ wrapper.append(node('small', 'JSON 参数;只按目录契约使用公开 URL,不填写本机路径。'));
205
+ if (model && !field.labelKnown)
206
+ wrapper.append(node('small', '目录未提供中文名;保留原参数键,不推测含义。'));
207
+ if (!Object.hasOwn(model?.inputSchema.properties ?? { prompt: {} }, field.key))
208
+ wrapper.append(node('small', `当前目录未声明键“${field.key}”;清空后才能报价。`));
209
+ const placement = fieldPlacement(field);
210
+ el(placement === 'advanced' ? 'advanced-fields' : placement === 'input' ? 'prompt-fields' : 'parameter-fields').append(wrapper);
211
+ }
212
+ show('advanced-parameters', fields.some(f => fieldPlacement(f) === 'advanced'));
213
+ }
214
+ for (const input of document.querySelectorAll('[data-field]')) {
215
+ const value = draft.fields[input.dataset.field] ?? '';
216
+ if (input.value !== value)
217
+ input.value = value;
218
+ input.disabled = state.submitBusy;
219
+ }
220
+ }
221
+ function renderRun(run, allowRefresh) {
222
+ const card = node('article', undefined, 'run-card');
223
+ card.append(node('strong', run.modelId), node('p', runLabel(run)));
224
+ card.append(node('p', `报价 ${run.quote.credits} 算力 · 有效至 ${new Date(run.quote.expiresAt).toLocaleString()} · ${run.runId}`, 'run-meta'));
225
+ if (run.job)
226
+ card.append(node('p', '任务 ID:' + run.job.jobId, 'run-meta'));
227
+ const diagnostic = runDiagnosticText(run), recovery = runRecoveryText(run);
228
+ if (diagnostic)
229
+ card.append(node('p', diagnostic, 'error'));
230
+ if (recovery)
231
+ card.append(node('p', recovery, 'hint'));
232
+ const links = node('div', undefined, 'run-links');
233
+ for (const [index, output] of (run.job?.outputs ?? []).entries()) {
234
+ const url = safeOutputUrl(output.url);
235
+ if (!url)
236
+ continue;
237
+ const a = node('a', `在新窗口查看云端输出 ${index + 1}(云端链接)`);
238
+ a.href = url;
239
+ a.target = '_blank';
240
+ a.rel = 'noopener noreferrer';
241
+ links.append(a);
242
+ }
243
+ card.append(links);
244
+ if (run.job) {
245
+ const list = controller.state.saves.outputs[run.runId];
246
+ const area = node('section', undefined, 'output-list');
247
+ area.dataset.run = run.runId;
248
+ const status = node('p', list?.status === 'loading' ? '正在读取本机输出记录…' : list?.status === 'error' ? errorText(list.error) : list?.status === 'ready' ? '' : '输出记录尚未读取;云端链接不代表已保存到本机。', list?.status === 'error' ? 'error' : 'hint');
249
+ status.setAttribute('role', list?.status === 'error' ? 'alert' : 'status');
250
+ area.append(status);
251
+ if (list?.value && list.status !== 'ready')
252
+ area.append(node('p', '以下保留上次读取的记录,当前结果尚未更新。', 'hint'));
253
+ for (const output of list?.value?.items ?? []) {
254
+ const presentation = outputPresentation(output, controller.state.connected);
255
+ const targetId = output.localOutputId ?? output.save?.localOutputId;
256
+ const target = targetId ? controller.state.saves.targets[targetId] : undefined;
257
+ const operationId = target?.operationId ?? output.saveOperationId;
258
+ const observation = operationId ? controller.state.saves.operations[operationId] : undefined;
259
+ const save = observation?.value ?? output.save;
260
+ const receiptView = save ? receiptPresentation(save, observation?.status === 'error') : undefined;
261
+ const row = node('article', undefined, 'output-record');
262
+ row.dataset.outputId = output.localOutputId ?? '';
263
+ row.dataset.identityReason = output.identity.reason ?? '';
264
+ if (operationId)
265
+ row.dataset.saveOperation = operationId;
266
+ if (save)
267
+ row.dataset.saveState = save.state;
268
+ row.append(node('strong', `输出 ${output.displayOrdinal}`), node('p', receiptView?.status ?? (target?.status === 'submitting' ? '正在确认保存请求' : target?.status === 'unknown' ? '保存结果尚未确认' : presentation.status), 'save-status'));
269
+ if (save && receiptView) {
270
+ row.append(node('p', receiptView.stage, 'hint'));
271
+ if (receiptView.progress)
272
+ row.append(node('p', receiptView.progress, 'save-progress'));
273
+ if (receiptView.error)
274
+ row.append(node('p', receiptView.error, 'error'));
275
+ if (save.checkedAt)
276
+ row.append(node('p', '上次核验:' + new Date(save.checkedAt).toLocaleString(), 'hint'));
277
+ if (save.asset) {
278
+ const details = node('details'), summary = node('summary', save.asset.relativePath + ' · 展开完整路径');
279
+ const path = node('code', save.asset.absolutePath, 'saved-path'), copy = node('button', '复制路径', 'quiet'), copyStatus = node('small', '', 'hint');
280
+ copy.onclick = () => { void navigator.clipboard.writeText(save.asset.absolutePath).then(() => { copyStatus.textContent = '已复制路径'; }).catch(() => { copyStatus.textContent = '浏览器未允许复制,请展开后手动复制。'; }); };
281
+ details.append(summary, path, copy, copyStatus);
282
+ row.append(details);
283
+ row.append(node('p', '保存根:' + (controller.state.saves.binding.value?.root?.absolutePath ?? '尚未确认'), 'saved-path hint'));
284
+ }
285
+ if (observation?.notice)
286
+ row.append(node('p', observation.notice, 'hint'));
287
+ if (observation?.error)
288
+ row.append(node('p', errorText(observation.error), 'error'));
289
+ const check = node('button', '核对保存状态', 'quiet');
290
+ check.disabled = observation?.status === 'loading';
291
+ check.onclick = () => { void controller.readSave(save.operationId, { manual: true }); };
292
+ row.append(check);
293
+ if (check.disabled)
294
+ row.append(node('small', '正在核对原保存记录,请稍候。', 'disabled-reason'));
295
+ if (receiptView.canRetry && observation?.status === 'ready') {
296
+ const retry = node('button', '重试原保存', 'quiet');
297
+ retry.onclick = () => { void controller.retrySave(save.operationId); };
298
+ row.append(retry, node('p', '沿用原保存,不会重新生成或扣费', 'hint'));
299
+ }
300
+ }
301
+ else if (targetId) {
302
+ if (target?.error)
303
+ row.append(node('p', errorText(target.error), 'error'));
304
+ if (target?.status === 'unknown' || target?.checking) {
305
+ const check = node('button', '核对保存状态', 'quiet');
306
+ check.disabled = !!target.checking;
307
+ check.onclick = () => { void controller.checkOutput(run.runId, targetId); };
308
+ row.append(check);
309
+ if (check.disabled)
310
+ row.append(node('small', '正在核对原输出是否已受理,请稍候。', 'disabled-reason'));
311
+ }
312
+ else if ((!target || target.status === 'idle') && output.acceptance === 'NEVER_ACCEPTED' && output.saveAllowed) {
313
+ const saveButton = node('button', '保存到本机', 'primary');
314
+ saveButton.onclick = () => { void controller.saveOutput(run.runId, targetId); };
315
+ row.append(saveButton);
316
+ }
317
+ }
318
+ if (presentation.explanation)
319
+ row.append(node('p', presentation.explanation, 'hint'));
320
+ if (presentation.blocker && (!save || output.saveBlockedCode !== 'SAVE_OPERATION_EXISTS'))
321
+ row.append(node('p', presentation.blocker, 'hint'));
322
+ if (output.identity.reason === 'LEGACY_AWAITING_FRESH_GET') {
323
+ const refresh = node('button', '刷新任务以核实输出', 'quiet');
324
+ refresh.disabled = !presentation.canRefresh || controller.state.refreshing;
325
+ refresh.onclick = () => { void controller.refreshRun(run.runId); };
326
+ row.append(refresh);
327
+ if (refresh.disabled)
328
+ row.append(node('small', controller.state.refreshing ? '正在查询,请稍候。' : '先连接云端,才能从原连接核实输出身份。', 'disabled-reason'));
329
+ }
330
+ area.append(row);
331
+ }
332
+ if (list?.status === 'ready' && !list.value?.items.length)
333
+ area.append(node('p', '本次快照没有输出记录。', 'hint'));
334
+ const read = node('button', list?.status === 'ready' ? '重读输出列表' : '读取输出列表', 'quiet');
335
+ read.disabled = list?.status === 'loading' || controller.state.saves.binding.status === 'loading';
336
+ read.onclick = () => { void controller.readOutputs(run.runId); };
337
+ area.append(read);
338
+ if (read.disabled)
339
+ area.append(node('small', '本机记录读取中,请稍候。', 'disabled-reason'));
340
+ card.append(area);
341
+ }
342
+ if (allowRefresh && run.job && canQueryRun(controller.state.saves.outputs[run.runId]?.value?.items)) {
343
+ const b = node('button', '查询最新状态', 'quiet');
344
+ b.type = 'button';
345
+ b.disabled = controller.state.refreshing || !controller.state.connected;
346
+ b.onclick = () => { void controller.refreshRun(run.runId); };
347
+ card.append(b);
348
+ if (b.disabled) {
349
+ const reason = controller.state.refreshing ? '正在查询,请稍候。' : '先连接云端,再查询该任务。';
350
+ b.title = reason;
351
+ card.append(node('small', reason, 'disabled-reason'));
352
+ }
353
+ }
354
+ return card;
355
+ }
356
+ function resourceMessage(resource, empty) { return resource.status === 'loading' ? '正在读取…' : resource.status === 'error' ? errorText(resource.error) : empty; }
357
+ function createAssetCard(asset) {
358
+ const card = node('article', undefined, 'asset-card'), src = '/studio/api/assets/' + encodeURIComponent(asset.assetId) + '/content';
359
+ card.dataset.assetId = asset.assetId;
360
+ const preview = asset.mediaType.startsWith('image/') ? node('img') : node('video');
361
+ preview.src = src;
362
+ if (preview instanceof HTMLImageElement) {
363
+ preview.alt = asset.displayName;
364
+ preview.loading = 'lazy';
365
+ }
366
+ else {
367
+ preview.controls = true;
368
+ preview.preload = 'metadata';
369
+ }
370
+ const previewError = node('p', '预览暂不可用,文件状态以下方核对结果为准。', 'asset-preview-error hint');
371
+ previewError.hidden = true;
372
+ preview.onerror = () => {
373
+ preview.hidden = true;
374
+ previewError.hidden = false;
375
+ };
376
+ const name = node('p', asset.displayName), bytes = node('p', `${asset.byteLength.toLocaleString()} 字节`, 'muted'), status = node('p', '', 'asset-integrity-status'), date = node('p', '', 'hint'), notice = node('p', '', 'hint'), error = node('p', '', 'error');
377
+ status.setAttribute('role', 'status');
378
+ error.setAttribute('role', 'alert');
379
+ const check = node('button', '核对文件状态', 'quiet'), reason = node('small', '正在核对该文件,请稍候。', 'disabled-reason');
380
+ // Keep the keyboard target while loading: aria-disabled plus the controller's
381
+ // in-flight guard prevents duplicate work without detaching or blurring it.
382
+ check.onclick = () => {
383
+ if (controller.state.assetChecks[asset.assetId]?.status !== 'loading')
384
+ void controller.readAsset(asset.assetId, { manual: true });
385
+ };
386
+ const link = node('a', '打开本机文件');
387
+ link.href = src;
388
+ link.target = '_blank';
389
+ link.rel = 'noopener';
390
+ const linkReason = node('p', '', 'asset-file-link-reason hint');
391
+ card.append(preview, previewError, name, bytes, status, date, notice, error, check, reason, link, linkReason);
392
+ return { card, status, date, notice, error, check, reason, link, linkReason };
393
+ }
394
+ const assetCards = new Map();
395
+ function renderAssets(assets) {
396
+ const container = el('assets'), ids = new Set(assets.map(a => a.assetId));
397
+ for (const [id, entry] of assetCards)
398
+ if (!ids.has(id)) {
399
+ entry.card.remove();
400
+ assetCards.delete(id);
401
+ }
402
+ for (const child of [...container.children])
403
+ if (!(child instanceof HTMLElement) || !child.dataset.assetId)
404
+ child.remove();
405
+ let cursor = container.firstElementChild;
406
+ for (const asset of assets) {
407
+ let entry = assetCards.get(asset.assetId);
408
+ if (!entry) {
409
+ entry = createAssetCard(asset);
410
+ assetCards.set(asset.assetId, entry);
411
+ }
412
+ const observation = controller.state.assetChecks[asset.assetId], current = observation?.value ?? asset;
413
+ entry.card.dataset.file = current.integrity?.file ?? '';
414
+ entry.card.dataset.integrityCheck = current.integrity?.integrityCheck ?? '';
415
+ entry.status.textContent = assetPresentation(current, observation?.status === 'error' || controller.state.assets.status === 'error').status;
416
+ const fileLink = assetFileLink(current);
417
+ entry.link.hidden = !fileLink.canOpen;
418
+ if (fileLink.canOpen)
419
+ entry.link.href = '/studio/api/assets/' + encodeURIComponent(asset.assetId) + '/content';
420
+ else
421
+ entry.link.removeAttribute('href');
422
+ entry.linkReason.textContent = fileLink.reason;
423
+ entry.linkReason.hidden = !fileLink.reason;
424
+ entry.date.textContent = current.integrity?.checkedAt ? '上次核验:' + new Date(current.integrity.checkedAt).toLocaleString() : '';
425
+ entry.date.hidden = !entry.date.textContent;
426
+ entry.notice.textContent = observation?.notice ?? '';
427
+ entry.notice.hidden = !entry.notice.textContent;
428
+ entry.error.textContent = observation?.error ? errorText(observation.error) : '';
429
+ entry.error.hidden = !entry.error.textContent;
430
+ const loading = observation?.status === 'loading';
431
+ entry.check.setAttribute('aria-disabled', String(loading));
432
+ entry.reason.hidden = !loading;
433
+ if (entry.card === cursor)
434
+ cursor = cursor.nextElementSibling;
435
+ else
436
+ container.insertBefore(entry.card, cursor);
437
+ }
438
+ }
439
+ function render(state) {
440
+ const storage = state.local.value?.storage, path = storage?.outputDir;
441
+ text('local-status', state.local.status === 'ready' ? '本机已授权' : state.local.status === 'error' ? '本机授权状态不可用' : '正在核对本机授权…');
442
+ show('storage-location', !!storage?.configured && !!path);
443
+ text('location-short', path ? compactLocation(path) : '');
444
+ text('location', path ?? '');
445
+ const storageNotice = state.local.status === 'loading' || state.local.status === 'idle' ? '正在读取保存位置…' : state.local.status === 'error' ? '保存位置未确认;当前编辑保留。' : '临时编辑 · 不能落盘';
446
+ show('storage-warning', !storage?.configured || !path);
447
+ text('storage-warning', storageNotice);
448
+ text('gateway', state.connecting ? '正在连接云端…' : state.connected ? '云端已连接' : state.error ? '云端未连接 · 请检查连接状态' : '云端未连接');
449
+ text('gateway-detail', state.connected ? state.gateway ?? '' : '离线可编辑;云端生成需要连接。');
450
+ el('gateway').title = state.connected ? state.gateway ?? '' : '离线可编辑;云端生成需要连接。';
451
+ el('storage-warning').title = state.local.status === 'ready' ? '临时编辑 · 不能落盘。请用 studio --output-dir <绝对目录> 重新启动。' : storageNotice;
452
+ const error = state.error ?? state.local.error;
453
+ show('error', !!error);
454
+ text('error', errorText(error));
455
+ show('notice', view === 'create' && !!state.notice);
456
+ text('notice', state.notice);
457
+ const balance = state.balance.value;
458
+ text('balance', state.balance.status === 'ready' && balance ? `可用 ${balance.availableCredits.toLocaleString()} · 预占 ${balance.reservedCredits.toLocaleString()}` : state.balance.status === 'error' ? '算力读取失败 · ' + state.balance.error?.code : state.balance.status === 'loading' ? '正在读取算力…' : '算力未知');
459
+ show('connect', !state.connected);
460
+ if (state.connected) {
461
+ if (document.getElementById('connect-reason'))
462
+ show('connect-reason', false);
463
+ }
464
+ else {
465
+ disabled('connect', state.connecting || state.submitBusy || state.local.status !== 'ready', state.connecting ? '连接进行中,请稍候。' : state.submitBusy ? '正在提交,请等待结果。' : '先完成本机授权;必要时重新打开启动链接。');
466
+ text('connect', state.connecting ? '连接中…' : '连接云端');
467
+ }
468
+ show('disconnect', state.connected);
469
+ disabled('disconnect', state.submitBusy, '正在提交,请等待结果再断开。');
470
+ text('title', state.mode === 'image' ? '图像创作' : '视频创作');
471
+ text('crumb', view === 'create' ? state.mode === 'image' ? '图像创作' : '视频创作' : view === 'tasks' ? '生成任务' : view === 'canvas' ? '本机画布' : '本机素材');
472
+ for (const b of document.querySelectorAll('[data-view]')) {
473
+ const selected = b.dataset.view === view && (view !== 'create' || b.dataset.mode === state.mode);
474
+ b.classList.toggle('selected', selected);
475
+ if (selected)
476
+ b.setAttribute('aria-current', 'page');
477
+ else
478
+ b.removeAttribute('aria-current');
479
+ }
480
+ for (const section of ['create', 'tasks', 'assets', 'canvas'])
481
+ show(section + '-view', view === section);
482
+ renderCanvas(state);
483
+ renderFields(state);
484
+ text('catalog-status', state.catalog.status === 'ready' ? `${modelsFor(state.catalog.value, state.mode).length} 个可用模型 · 参数按目录` : resourceMessage(state.catalog, '尚未连接云端目录'));
485
+ disabled('reload-cloud', !state.connected || state.refreshing, state.refreshing ? '正在刷新,请稍候;当前输入保留。' : '先连接云端。');
486
+ disabled('refresh-tasks', !state.connected || state.refreshing, state.refreshing ? '正在读取任务,请稍候。' : '连接云端后才能读取该连接的任务记录。');
487
+ text('draft-status', !storage?.configured ? '临时编辑 · 未落盘' : state.dirty[state.mode] ? '有未保存编辑' : state.drafts.status === 'loading' ? '正在读取本机草稿…' : state.drafts.status === 'error' ? '草稿读取失败 · 当前编辑保留' : state.drafts.value?.drafts[state.mode] ? '草稿已保存' : '尚无本机草稿');
488
+ disabled('save-draft', state.saving || state.drafts.status !== 'ready', state.saving ? '草稿正在保存,请稍候。' : !storage?.configured ? '未设置保存目录,临时编辑不能落盘。' : state.drafts.status === 'loading' ? '正在读取草稿版本,完成后可保存。' : '请先读取最新草稿版本,再保存当前编辑。');
489
+ text('save-draft', state.saving ? '保存中…' : '保存草稿');
490
+ const draftError = state.drafts.error;
491
+ show('draft-error', !!draftError);
492
+ text('draft-error', errorText(draftError));
493
+ show('reload-draft', !!draftError || state.error?.code === 'STUDIO_DRAFT_CONFLICT');
494
+ const currentModel = modelsFor(state.catalog.value ?? [], state.mode).some(m => m.id === state.edits[state.mode].modelId);
495
+ disabled('quote', !state.connected || !storage?.configured || state.catalog.status !== 'ready' || !currentModel || state.quoteBusy || state.submitBusy, state.submitBusy ? '正在提交,请等待结果。' : state.quoteBusy ? '正在报价,请稍候。' : !storage?.configured ? '先通过 studio --output-dir 设置保存目录;页面不能代选目录。' : !state.connected ? '先连接云端,当前草稿不会丢失。' : state.catalog.status !== 'ready' ? '目录尚未就绪,请先刷新目录。' : '请选择当前目录中的模型。');
496
+ text('quote', state.quoteBusy ? '正在报价…' : '查看报价');
497
+ const current = JSON.stringify([state.quote, state.refreshing, state.connected, state.saves]);
498
+ if (currentSignature !== current) {
499
+ currentSignature = current;
500
+ const target = el('current-run');
501
+ target.replaceChildren();
502
+ target.className = state.quote ? '' : 'empty';
503
+ if (state.quote)
504
+ target.append(renderRun(state.quote, true));
505
+ else
506
+ target.textContent = '填写输入并选择模型后查看报价。';
507
+ }
508
+ const quoted = state.quote?.state === 'QUOTED';
509
+ show('review-quote', quoted);
510
+ disabled('review-quote', state.submitBusy || !state.connected, state.submitBusy ? '正在提交,请等待结果。' : '当前云连接不可用,请先核对连接状态。');
511
+ show('review-quote-reason', quoted && (state.submitBusy || !state.connected));
512
+ if (dialog.open && (!quoted || state.quote?.runId !== dialogRunId))
513
+ dialog.close();
514
+ const runsKey = JSON.stringify([state.runs, state.refreshing, state.connected, state.saves]);
515
+ const binding = state.saves.binding;
516
+ text('save-binding-status', binding.status === 'ready' ? binding.value?.root ? '输出保存根(服务已确认)' : messageForCode('SAVE_OUTPUT_ROOT_REQUIRED', '') : binding.status === 'error' ? errorText(binding.error) : binding.status === 'loading' ? '正在核对本机保存连接…' : '保存连接尚未核对');
517
+ el('save-binding-status').dataset.status = binding.status;
518
+ el('save-binding-status').setAttribute('role', binding.status === 'error' ? 'alert' : 'status');
519
+ text('save-binding-path', binding.status === 'ready' ? binding.value?.root?.absolutePath ?? '' : '');
520
+ show('save-binding-path', !!binding.value?.root && binding.status === 'ready');
521
+ disabled('rebind-saves', binding.status === 'loading' || state.local.status !== 'ready', state.local.status !== 'ready' ? '先完成本机授权;必要时请从 CLI 重新打开 Studio。' : '保存连接核对中,请稍候。');
522
+ const tasksNotice = state.runs.status === 'loading' ? '正在读取任务记录;已有记录保留。' : state.runs.status === 'error' ? errorText(state.runs.error) : '';
523
+ text('tasks-status', tasksNotice);
524
+ show('tasks-status', !!tasksNotice);
525
+ el('tasks-status').dataset.status = state.runs.status;
526
+ if (runSignature !== runsKey) {
527
+ runSignature = runsKey;
528
+ const target = el('tasks');
529
+ target.replaceChildren();
530
+ if (state.runs.value?.length)
531
+ for (const run of state.runs.value.slice().reverse())
532
+ target.append(renderRun(run, true));
533
+ else
534
+ target.append(node('p', state.runs.status === 'ready' ? '当前连接还没有本机任务记录。' : !state.connected ? '连接云端后读取当前连接的任务记录。' : '列表尚未就绪,请等待或重试读取。', 'empty'));
535
+ }
536
+ const assetsKey = JSON.stringify([state.assets, state.assetChecks]);
537
+ if (state.assets.status === 'loading' && previousAssetsStatus !== 'loading')
538
+ text('asset-message', '');
539
+ previousAssetsStatus = state.assets.status;
540
+ const assetsNotice = state.assets.status === 'loading' ? state.assets.value ? '正在更新素材列表,以下为上次读取的素材。' : '正在读取素材列表…' : state.assets.status === 'error' ? '素材读取失败;' + (state.assets.value ? '以下保留上次读取的数据。' : '尚未取得列表,不能确定是否有素材。') + ' ' + errorText(state.assets.error) : '';
541
+ text('assets-status', assetsNotice);
542
+ show('assets-status', !!assetsNotice);
543
+ el('assets-status').dataset.status = state.assets.status;
544
+ el('assets-status').setAttribute('role', state.assets.status === 'error' ? 'alert' : 'status');
545
+ if (assetsSignature !== assetsKey) {
546
+ assetsSignature = assetsKey;
547
+ if (state.assets.value?.assets.length)
548
+ renderAssets(state.assets.value.assets);
549
+ else {
550
+ el('assets').replaceChildren(node('p', state.assets.status === 'ready' ? '还没有本机素材。可导入已有文件;云端结果不会自动保存。' : state.assets.value ? '上次读取没有素材;当前列表尚未更新。' : '列表未就绪,请等待或重试读取。', 'empty'));
551
+ }
552
+ }
553
+ const issues = state.assets.value?.recoveryIssues.length ?? 0;
554
+ show('issues', issues > 0);
555
+ text('issues', `${issues} 项素材需要恢复检查,原文件已保留。`);
556
+ disabled('save-asset-button', uploading || !storage?.configured, uploading ? '本机文件正在保存,请稍候。' : '先设置保存目录,才能导入文件。');
557
+ disabled('refresh-assets', state.assets.status === 'loading', '素材列表正在读取,重复读取已合并。');
558
+ if (state.sessionExpired) {
559
+ text('asset-message', '');
560
+ text('local-status', '本机会话已失效');
561
+ text('error', '请从 CLI 重新打开 Studio');
562
+ show('error', true);
563
+ for (const control of document.querySelectorAll('button,input,select,textarea')) {
564
+ if (control.id === 'theme' || control.dataset.view)
565
+ continue;
566
+ control.disabled = true;
567
+ control.title = '请从 CLI 重新打开 Studio';
568
+ }
569
+ for (const media of document.querySelectorAll('[src^="/studio/api/"]'))
570
+ media.removeAttribute('src');
571
+ for (const link of document.querySelectorAll('a[href^="/studio/api/"]'))
572
+ link.removeAttribute('href');
573
+ for (const reason of document.querySelectorAll('[id$="-reason"],.disabled-reason'))
574
+ reason.textContent = '请从 CLI 重新打开 Studio';
575
+ }
576
+ observeOutputs();
577
+ observeAssets();
578
+ }
579
+ for (const b of document.querySelectorAll('[data-view]'))
580
+ b.onclick = () => {
581
+ const navigate = () => { if (view === 'assets' && b.dataset.view !== 'assets')
582
+ text('asset-message', ''); view = b.dataset.view; if (b.dataset.mode === 'image' || b.dataset.mode === 'video')
583
+ controller.setMode(b.dataset.mode); if (view === 'assets')
584
+ void controller.refreshAssets(); render(controller.state); };
585
+ if (view === 'canvas' && b.dataset.view !== 'canvas' && canvasHost)
586
+ canvasHost.requestLeave(navigate);
587
+ else
588
+ navigate();
589
+ };
590
+ button('theme').onclick = () => { document.documentElement.dataset.theme = document.documentElement.dataset.theme === 'light' ? 'dark' : 'light'; renderCanvas(controller.state); };
591
+ button('connect').onclick = () => { void controller.connect(); };
592
+ button('disconnect').onclick = () => { void controller.disconnect(); };
593
+ el('model').onchange = e => controller.selectModel(e.target.value);
594
+ button('save-draft').onclick = () => { void controller.saveDraft(); };
595
+ button('reload-draft').onclick = () => { void controller.rereadDrafts(); };
596
+ button('quote').onclick = () => { void controller.quote(); };
597
+ button('reload-cloud').onclick = button('refresh-tasks').onclick = () => { void controller.refreshCloud(); };
598
+ button('refresh-assets').onclick = () => { void controller.refreshAssets(); };
599
+ button('rebind-saves').onclick = () => { void controller.bindSaves(); };
600
+ button('review-quote').onclick = () => {
601
+ const run = controller.state.quote;
602
+ if (!run || run.state !== 'QUOTED')
603
+ return;
604
+ if (Date.parse(run.quote.expiresAt) <= Date.now()) {
605
+ controller.reportError({ code: 'STUDIO_QUOTE_EXPIRED', message: '报价已过期,请重新报价。' });
606
+ return;
607
+ }
608
+ dialogRunId = run.runId;
609
+ text('confirm-summary', `模型 ${run.modelId};本次报价 ${run.quote.credits} 算力;有效至 ${new Date(run.quote.expiresAt).toLocaleString()}。`);
610
+ dialog.showModal();
611
+ };
612
+ button('cancel-confirm').onclick = () => dialog.close();
613
+ button('confirm').onclick = () => { dialog.close(); if (controller.state.quote?.runId === dialogRunId)
614
+ void controller.confirm(); };
615
+ el('save-asset').onsubmit = async (e) => {
616
+ e.preventDefault();
617
+ if (uploading)
618
+ return;
619
+ const file = el('file').files?.[0];
620
+ if (!file) {
621
+ text('asset-message', '请先选择一个文件。');
622
+ return;
623
+ }
624
+ uploading = true;
625
+ render(controller.state);
626
+ text('asset-message', '正在保存本机文件…');
627
+ try {
628
+ const r = await api.request('/studio/api/assets', { method: 'POST', headers: { 'Content-Type': file.type, 'X-Studio-Display-Name': encodeURIComponent(file.name) }, body: file });
629
+ const value = await r.json();
630
+ if (controller.state.sessionExpired)
631
+ throw { code: 'STUDIO_SESSION_REQUIRED', message: '请从 CLI 重新打开 Studio' };
632
+ if (!r.ok)
633
+ throw Object.assign(new Error(value.error?.message || '保存未成功。'), { code: value.error?.code });
634
+ el('file').value = '';
635
+ await controller.refreshAssets({ afterWrite: true });
636
+ text('asset-message', '');
637
+ if (view === 'assets' && controller.state.assets.status === 'ready')
638
+ text('asset-message', '已保存到本机目录。');
639
+ }
640
+ catch (error) {
641
+ text('asset-message', errorText(uiError(error)));
642
+ }
643
+ finally {
644
+ uploading = false;
645
+ render(controller.state);
646
+ }
647
+ };
648
+ window.addEventListener('beforeunload', event => { if (canvasDirty || Object.values(controller.state.dirty).some(Boolean) || controller.state.submitBusy || controller.state.saving) {
649
+ event.preventDefault();
650
+ event.returnValue = '';
651
+ } });
652
+ document.addEventListener('visibilitychange', () => controller.setSavePageVisible(!document.hidden));
653
+ window.addEventListener('pagehide', () => { outputObserver.disconnect(); assetObserver.disconnect(); controller.disposeSaves(); });
654
+ (async () => {
655
+ controller.setSavePageVisible(!document.hidden);
656
+ const launch = new URLSearchParams(location.hash.slice(1)).get('launch');
657
+ history.replaceState(null, '', '/studio/');
658
+ if (launch)
659
+ await api('session/bootstrap', { launch });
660
+ await controller.init();
661
+ })().catch(error => controller.reportError(error));
662
+ //# sourceMappingURL=app.js.map