dsh-agnes-create 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,783 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-agnes-create",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ const React = require("react");
8
+
9
+ const ID = "dsh-agnes-create";
10
+ const CSS_ID = "dsh-agnes-create";
11
+ const SIZE_OPTIONS = ["1K", "2K", "3K", "4K"];
12
+ const RATIO_OPTIONS = ["1:1", "3:4", "4:3", "16:9", "9:16", "2:3", "3:2", "21:9"];
13
+ // 视频:Flash 固定 720P,seconds 是字符串 "4"–"12"
14
+ const VIDEO_SECONDS = ["4", "5", "6", "7", "8", "9", "10", "11", "12"];
15
+ const VIDEO_RATIOS = ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16"];
16
+ const HISTORY_MAX = 8;
17
+ const POLL_MS = 6000;
18
+ const VIDEO_DEADLINE_MS = 15 * 60 * 1000;
19
+
20
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
21
+
22
+ /* ---------- 样式 ---------- */
23
+ const CSS = `
24
+ .agn-root { display: flex; flex-direction: column; gap: 12px; padding: 20px 24px; height: 100%;
25
+ box-sizing: border-box; overflow: auto; font-size: 13px; }
26
+ .agn-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
27
+ .agn-title { font-size: 16px; font-weight: 600; color: var(--dsw-alias-label-primary, inherit); }
28
+ .agn-meta { font-size: 12px; color: var(--dsw-alias-label-secondary, #888); }
29
+ .agn-card { border: 1px solid var(--dsw-alias-border-l2, #e3e3e3); border-radius: 10px;
30
+ padding: 12px 14px; display: flex; flex-direction: column; gap: 10px; }
31
+ .agn-label { font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-secondary, #666); }
32
+ .agn-textarea { width: 100%; box-sizing: border-box; min-height: 84px; resize: vertical;
33
+ font: inherit; line-height: 1.6; padding: 8px 10px; border-radius: 8px;
34
+ color: var(--dsw-alias-label-primary, inherit);
35
+ background: var(--dsw-alias-bg-layer-2, #fafafa);
36
+ border: 1px solid var(--dsw-alias-border-l2, #ddd); outline: none; }
37
+ .agn-textarea:focus { border-color: var(--dsw-alias-state-business-primary, #4b7bec); }
38
+ .agn-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
39
+ .agn-select { font: inherit; padding: 5px 8px; border-radius: 8px; cursor: pointer;
40
+ color: var(--dsw-alias-label-primary, inherit);
41
+ background: var(--dsw-alias-bg-layer-2, #fafafa);
42
+ border: 1px solid var(--dsw-alias-border-l2, #ddd); }
43
+ .agn-btn { font: inherit; font-weight: 600; padding: 7px 18px; border-radius: 8px; cursor: pointer;
44
+ border: 1px solid var(--dsw-alias-button-primary-fill, #4b7bec);
45
+ background: var(--dsw-alias-button-primary-fill, #4b7bec);
46
+ color: var(--dsw-alias-label-primary-foreground, #fff); }
47
+ .agn-btn:disabled { opacity: .45; cursor: not-allowed; }
48
+ .agn-err { color: var(--dsw-alias-state-error-primary, #d33); white-space: pre-wrap;
49
+ word-break: break-word; font-size: 12px; }
50
+ .agn-ok { color: var(--dsw-alias-state-success-primary, #2a2); font-size: 12px; }
51
+
52
+ /* 图片/视频 模式切换 */
53
+ .agn-modes { display: flex; gap: 6px; }
54
+ .agn-mode { font: inherit; font-size: 12px; padding: 4px 14px; border-radius: 999px;
55
+ cursor: pointer; border: 1px solid var(--dsw-alias-border-l2, #ddd);
56
+ background: transparent; color: var(--dsw-alias-label-secondary, #666); }
57
+ .agn-mode-on { background: var(--dsw-alias-button-primary-fill, #4b7bec);
58
+ border-color: transparent;
59
+ color: var(--dsw-alias-label-primary-foreground, #fff); }
60
+ .agn-mode:disabled { opacity: .45; cursor: not-allowed; }
61
+
62
+ /* 进度条 */
63
+ .agn-bar { height: 6px; border-radius: 3px; overflow: hidden;
64
+ background: var(--dsw-alias-bg-layer-2, #eee); }
65
+ .agn-bar-fill { height: 100%; width: 0;
66
+ background: var(--dsw-alias-button-primary-fill, #4b7bec);
67
+ transition: width .3s ease; }
68
+
69
+ /* 媒体展示区 */
70
+ .agn-shot { border-radius: 10px; border: 1px solid var(--dsw-alias-border-l2, #e3e3e3);
71
+ background: var(--dsw-alias-bg-layer-2, #fafafa); overflow: hidden;
72
+ display: flex; align-items: center; justify-content: center; min-height: 120px; }
73
+ .agn-shot img, .agn-shot video { display: block; max-width: 100%; height: auto; }
74
+ .agn-shot-big { max-height: 62vh; }
75
+ .agn-shot-big img, .agn-shot-big video { max-height: 62vh; object-fit: contain; }
76
+ .agn-shot-msg { padding: 18px; font-size: 12px; text-align: center; line-height: 1.7;
77
+ color: var(--dsw-alias-label-secondary, #888); }
78
+
79
+ /* 历史缩略图 */
80
+ .agn-hist { display: flex; flex-direction: column; gap: 6px; }
81
+ .agn-hist-strip { display: flex; gap: 8px; flex-wrap: wrap; }
82
+ .agn-thumb { position: relative; width: 84px; height: 84px; border-radius: 8px; padding: 0;
83
+ overflow: hidden; cursor: pointer; flex: none;
84
+ border: 2px solid var(--dsw-alias-border-l2, #e3e3e3);
85
+ background: var(--dsw-alias-bg-layer-2, #fafafa); }
86
+ .agn-thumb-on { border-color: var(--dsw-alias-state-business-primary, #4b7bec); }
87
+ .agn-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
88
+ .agn-thumb-play { position: absolute; inset: 0; display: flex; align-items: center;
89
+ justify-content: center; font-size: 22px; line-height: 1;
90
+ color: #fff; background: rgba(0, 0, 0, .3); }
91
+ .agn-thumb-cap { position: absolute; left: 0; right: 0; bottom: 0; font-size: 10px;
92
+ text-align: center; padding: 1px 0;
93
+ color: var(--dsw-alias-label-primary-foreground, #fff);
94
+ background: rgba(0, 0, 0, .45); }
95
+
96
+ /* 设置卡片 */
97
+ .agn-field { display: flex; flex-direction: column; gap: 4px; }
98
+ .agn-input { font: inherit; font-size: 12px; padding: 6px 9px; border-radius: 8px;
99
+ box-sizing: border-box; width: 100%; outline: none;
100
+ color: var(--dsw-alias-label-primary, inherit);
101
+ background: var(--dsw-alias-bg-layer-2, #fafafa);
102
+ border: 1px solid var(--dsw-alias-border-l2, #ddd);
103
+ font-family: var(--ds-font-family-code, ui-monospace, monospace); }
104
+ .agn-input:focus { border-color: var(--dsw-alias-state-business-primary, #4b7bec); }
105
+ .agn-input:disabled { opacity: .5; }
106
+ .agn-note { font-size: 11px; color: var(--dsw-alias-label-secondary, #888); }
107
+ .agn-okline { font-size: 12px; color: var(--dsw-alias-state-success-primary, #2a2); }
108
+
109
+ .agn-hint { font-size: 12px; color: var(--dsw-alias-label-secondary, #888); line-height: 1.6; }
110
+ .agn-code { font-family: var(--ds-font-family-code, ui-monospace, monospace); font-size: 11px;
111
+ word-break: break-all; }
112
+ `;
113
+
114
+ function ensureCss() {
115
+ const tags = document.querySelectorAll('style[data-plugin-css="' + CSS_ID + '"]');
116
+ const style = tags[0] || document.createElement("style");
117
+ for (let i = 1; i < tags.length; i += 1) tags[i].remove();
118
+ style.dataset.plugin = "dsh-agnes-create";
119
+ style.dataset.pluginCss = CSS_ID;
120
+ style.textContent = CSS;
121
+ if (!style.isConnected) document.head.appendChild(style);
122
+ }
123
+
124
+ function srcOf(name) {
125
+ return "/agnes/media?name=" + encodeURIComponent(name);
126
+ }
127
+
128
+ /* ==========================================================================
129
+ 状态仓库(放组件外面:main slot 切换会卸载组件,状态必须独立存活)
130
+ ========================================================================== */
131
+ const GLOBAL_KEY = "__dshAgnesCreateStore";
132
+
133
+ function createStore() {
134
+ const listeners = new Set();
135
+ const state = {
136
+ prompt: "",
137
+ mode: "image", // image | video
138
+ size: "1K",
139
+ ratio: "1:1",
140
+ vSeconds: "5",
141
+ vRatio: "16:9",
142
+ busy: false,
143
+ error: "",
144
+ result: null,
145
+ history: [], // 最新在前,元素带 kind: 'image' | 'video'
146
+ startedAt: 0,
147
+ elapsed: 0,
148
+ vProgress: 0,
149
+ vStatus: "",
150
+ videoId: null,
151
+ health: null,
152
+ healthRequested: false,
153
+
154
+ /* ---- 可配置项:Key 文件路径 / 输出目录 ---- */
155
+ cfg: null, // 生效配置 { keyFile, outDir }
156
+ cfgDefaults: null,
157
+ cfgStored: null,
158
+ cfgDraft: { keyFile: "", outDir: "" },
159
+ cfgOpen: false,
160
+ cfgSaving: false,
161
+ cfgError: "",
162
+ cfgNotice: "",
163
+ };
164
+
165
+ const snapshot = () => ({ ...state, history: state.history.slice() });
166
+ const notify = () => {
167
+ for (const fn of Array.from(listeners)) {
168
+ try { fn(); } catch { /* 单个订阅者出错不拖垮其它 */ }
169
+ }
170
+ };
171
+ const set = (patch) => {
172
+ Object.assign(state, patch);
173
+ notify();
174
+ };
175
+
176
+ let timer = null;
177
+ let runToken = 0;
178
+
179
+ const stopTimer = () => {
180
+ if (timer !== null) { clearInterval(timer); timer = null; }
181
+ };
182
+ const startTimer = () => {
183
+ stopTimer();
184
+ timer = setInterval(() => {
185
+ if (state.busy) set({ elapsed: (Date.now() - state.startedAt) / 1000 });
186
+ }, 1000);
187
+ };
188
+
189
+ const pushResult = (entry) => {
190
+ set({ result: entry, history: [entry, ...state.history].slice(0, HISTORY_MAX) });
191
+ };
192
+
193
+ async function loadHealth() {
194
+ if (state.healthRequested) return;
195
+ state.healthRequested = true;
196
+ try {
197
+ const res = await fetch("/agnes/health");
198
+ set({ health: await res.json() });
199
+ } catch {
200
+ set({ health: { ok: false, error: "宿主路由不可达" } });
201
+ }
202
+ }
203
+
204
+ /* ---------- 配置读写 ---------- */
205
+ async function loadConfig() {
206
+ try {
207
+ const res = await fetch("/agnes/config");
208
+ // 宿主半还没重启时这条路由不存在:给出可操作的提示,而不是一句“读取失败”
209
+ if (res.status === 404) {
210
+ set({ cfgError: "配置接口尚未就绪:需要完全重启应用(只重载窗口不够)" });
211
+ return;
212
+ }
213
+ const data = await res.json();
214
+ if (!data.ok) throw new Error(data.error || "读取配置失败");
215
+ set({
216
+ cfg: data.config,
217
+ cfgDefaults: data.defaults,
218
+ cfgStored: data.stored,
219
+ cfgDraft: { keyFile: data.config.keyFile, outDir: data.config.outDir },
220
+ cfgError: "",
221
+ });
222
+ } catch (e) {
223
+ set({ cfgError: e && e.message ? e.message : String(e) });
224
+ }
225
+ }
226
+
227
+ async function saveConfig() {
228
+ if (state.cfgSaving) return;
229
+ set({ cfgSaving: true, cfgError: "", cfgNotice: "" });
230
+ try {
231
+ const res = await fetch("/agnes/config", {
232
+ method: "POST",
233
+ headers: { "content-type": "application/json" },
234
+ body: JSON.stringify({
235
+ keyFile: state.cfgDraft.keyFile,
236
+ outDir: state.cfgDraft.outDir,
237
+ }),
238
+ });
239
+ if (res.status === 404) throw new Error("配置接口尚未就绪:需要完全重启应用(只重载窗口不够)");
240
+ const data = await res.json();
241
+ if (!data.ok) throw new Error(data.error || "保存失败");
242
+ set({
243
+ cfg: data.config,
244
+ cfgDefaults: data.defaults,
245
+ cfgStored: data.stored,
246
+ cfgDraft: { keyFile: data.config.keyFile, outDir: data.config.outDir },
247
+ cfgNotice: "已保存",
248
+ });
249
+ // 路径变了,key 是否存在/目录是否可写都要重新算
250
+ try {
251
+ set({ health: await (await fetch("/agnes/health")).json() });
252
+ } catch { /* 健康信息刷新失败不影响保存结果 */ }
253
+ } catch (e) {
254
+ set({ cfgError: e && e.message ? e.message : String(e) });
255
+ } finally {
256
+ set({ cfgSaving: false });
257
+ }
258
+ }
259
+
260
+ /* ---------- 图片:一次请求就出结果 ---------- */
261
+ async function runImage(token, prompt) {
262
+ const res = await fetch("/agnes/generate", {
263
+ method: "POST",
264
+ headers: { "content-type": "application/json" },
265
+ body: JSON.stringify({ prompt, size: state.size, ratio: state.ratio }),
266
+ });
267
+ const data = await res.json();
268
+ if (!data.ok) throw new Error(data.error || "生成失败");
269
+ if (token !== runToken) return;
270
+ pushResult({ ...data, kind: "image", wallSec: ((Date.now() - state.startedAt) / 1000).toFixed(1) });
271
+ }
272
+
273
+ /* ---------- 视频:提交 → 轮询 → 下载完成 ---------- */
274
+ async function runVideo(token, prompt) {
275
+ set({ vStatus: "提交中…", vProgress: 0, videoId: null });
276
+
277
+ const subRes = await fetch("/agnes/video", {
278
+ method: "POST",
279
+ headers: { "content-type": "application/json" },
280
+ body: JSON.stringify({ prompt, seconds: state.vSeconds, ratio: state.vRatio }),
281
+ });
282
+ const sub = await subRes.json();
283
+ if (!sub.ok) throw new Error(sub.error || "提交失败");
284
+ if (token !== runToken) return;
285
+
286
+ set({ videoId: sub.videoId, vStatus: "排队中…" });
287
+ const deadline = Date.now() + VIDEO_DEADLINE_MS;
288
+
289
+ let transientFails = 0;
290
+ for (;;) {
291
+ await sleep(POLL_MS);
292
+ if (token !== runToken) return;
293
+
294
+ let st;
295
+ try {
296
+ const r = await fetch("/agnes/video?id=" + encodeURIComponent(sub.videoId));
297
+ st = await r.json();
298
+ } catch (e) {
299
+ st = { ok: false, error: "网络错误:" + (e && e.message ? e.message : String(e)) };
300
+ }
301
+
302
+ if (!st.ok) {
303
+ const msg = String(st.error || "查询任务失败");
304
+ // 429/503 只是限流,任务还在服务端跑:退避后继续,别把整个任务判死。
305
+ // 实测提交后立刻查询就会 429,这条路径是必需的。
306
+ if (/\b429\b|\b503\b|频繁|队列已满|网络错误|timeout/i.test(msg)) {
307
+ transientFails += 1;
308
+ if (transientFails > 8) throw new Error(msg);
309
+ set({ vStatus: "限流重试中…" });
310
+ await sleep(Math.min(30000, 6000 * transientFails));
311
+ continue;
312
+ }
313
+ throw new Error(msg);
314
+ }
315
+
316
+ transientFails = 0;
317
+ if (token !== runToken) return;
318
+
319
+ set({
320
+ vStatus: st.status,
321
+ vProgress: typeof st.progress === "number" ? st.progress : 0,
322
+ });
323
+
324
+ if (st.status === "completed") {
325
+ pushResult({
326
+ kind: "video",
327
+ filename: st.filename,
328
+ path: st.path,
329
+ bytes: st.bytes,
330
+ size: "720P",
331
+ ratio: state.vRatio,
332
+ seconds: st.seconds || state.vSeconds,
333
+ transport: "异步任务",
334
+ wallSec: ((Date.now() - state.startedAt) / 1000).toFixed(1),
335
+ });
336
+ return;
337
+ }
338
+ if (st.status === "failed") {
339
+ const err = st.error;
340
+ throw new Error(typeof err === "string" ? err : (err && err.message) || "视频生成失败");
341
+ }
342
+ if (Date.now() > deadline) throw new Error("等待超时(15 分钟)");
343
+ }
344
+ }
345
+
346
+ async function run() {
347
+ if (state.busy) return;
348
+ const prompt = state.prompt.trim();
349
+ if (prompt === "") return;
350
+
351
+ const token = ++runToken;
352
+ const mode = state.mode;
353
+ set({
354
+ busy: true, error: "", startedAt: Date.now(), elapsed: 0,
355
+ vProgress: 0, vStatus: mode === "video" ? "提交中…" : "",
356
+ });
357
+ startTimer();
358
+
359
+ try {
360
+ if (mode === "video") await runVideo(token, prompt);
361
+ else await runImage(token, prompt);
362
+ } catch (e) {
363
+ if (token === runToken) set({ error: e && e.message ? e.message : String(e) });
364
+ } finally {
365
+ if (token === runToken) {
366
+ stopTimer();
367
+ set({ busy: false, elapsed: (Date.now() - state.startedAt) / 1000 });
368
+ }
369
+ }
370
+ }
371
+
372
+ return {
373
+ get: snapshot,
374
+ raw: () => state,
375
+ subscribe(fn) {
376
+ listeners.add(fn);
377
+ return () => listeners.delete(fn);
378
+ },
379
+ set,
380
+ loadHealth,
381
+ loadConfig,
382
+ saveConfig,
383
+ setCfgDraft(field, value) {
384
+ set({ cfgDraft: { ...state.cfgDraft, [field]: value }, cfgNotice: "" });
385
+ },
386
+ toggleCfg() { set({ cfgOpen: !state.cfgOpen }); },
387
+ run,
388
+ /** 切换主预览到历史里的某一张 */
389
+ show(entry) { set({ result: entry }); },
390
+ /**
391
+ * 清空界面上的生成结果。只清显示状态:不删磁盘文件、不动提示词和参数。
392
+ * 进行中的请求不清(否则结果回来又会写一遍,反而更乱)。
393
+ */
394
+ clear() {
395
+ if (state.busy) return;
396
+ set({ result: null, history: [], error: "" });
397
+ },
398
+ dispose() {
399
+ stopTimer();
400
+ runToken += 1;
401
+ listeners.clear();
402
+ },
403
+ };
404
+ }
405
+
406
+ function getStore() {
407
+ let s = globalThis[GLOBAL_KEY];
408
+ if (!s) { s = createStore(); globalThis[GLOBAL_KEY] = s; }
409
+ return s;
410
+ }
411
+
412
+ /* ---------- 媒体显示:视频直接用 <video>,图片带加载兜底 ---------- */
413
+ function MediaBox({ entry, big }) {
414
+ const isVideo = entry !== null && entry.kind === "video";
415
+ const name = entry === null ? null : entry.filename;
416
+
417
+ const [src, setSrc] = React.useState(name === null ? null : srcOf(name));
418
+ const [failed, setFailed] = React.useState(false);
419
+ const [triedBlob, setTriedBlob] = React.useState(false);
420
+ const objUrlRef = React.useRef(null);
421
+
422
+ React.useEffect(() => {
423
+ setSrc(name === null ? null : srcOf(name));
424
+ setFailed(false);
425
+ setTriedBlob(false);
426
+ return () => {
427
+ if (objUrlRef.current !== null) {
428
+ try { URL.revokeObjectURL(objUrlRef.current); } catch { /* ignore */ }
429
+ objUrlRef.current = null;
430
+ }
431
+ };
432
+ }, [name]);
433
+
434
+ if (entry === null || name === null) {
435
+ return React.createElement("div", { className: "agn-shot" },
436
+ React.createElement("div", { className: "agn-shot-msg" }, "还没有生成结果"));
437
+ }
438
+
439
+ const onError = async () => {
440
+ if (isVideo || triedBlob) { setFailed(true); return; }
441
+ setTriedBlob(true);
442
+ try {
443
+ const res = await fetch(srcOf(name));
444
+ if (!res.ok) throw new Error("HTTP " + res.status);
445
+ const buf = await res.arrayBuffer();
446
+ if (buf.byteLength === 0) throw new Error("0 字节");
447
+ const url = URL.createObjectURL(new Blob([buf], { type: "image/png" }));
448
+ objUrlRef.current = url;
449
+ setSrc(url);
450
+ } catch {
451
+ setFailed(true);
452
+ }
453
+ };
454
+
455
+ if (failed) {
456
+ return React.createElement("div", { className: "agn-shot" },
457
+ React.createElement("div", { className: "agn-shot-msg" },
458
+ "已生成,但界面加载失败。\n文件路径:",
459
+ React.createElement("span", { className: "agn-code" }, entry.path || name)));
460
+ }
461
+
462
+ return React.createElement("div", { className: "agn-shot" + (big ? " agn-shot-big" : "") },
463
+ isVideo
464
+ ? React.createElement("video", {
465
+ src,
466
+ controls: true,
467
+ playsInline: true,
468
+ preload: "metadata",
469
+ onError: () => setFailed(true),
470
+ })
471
+ : React.createElement("img", { src, alt: "生成结果", onError })
472
+ );
473
+ }
474
+
475
+ /* ---------- 侧栏图标 ---------- */
476
+ function Glyph(props) {
477
+ const size = typeof props.size === "number" ? props.size : 18;
478
+ return React.createElement(
479
+ "svg",
480
+ {
481
+ width: size, height: size, viewBox: "0 0 24 24", fill: "none",
482
+ stroke: "currentColor", strokeWidth: props.active ? 2 : 1.7,
483
+ strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true",
484
+ },
485
+ React.createElement("rect", { key: "b", x: 2.5, y: 6.5, width: 19, height: 14, rx: 3 }),
486
+ React.createElement("circle", { key: "c", cx: 12, cy: 13.5, r: 4 }),
487
+ React.createElement("path", { key: "t", d: "M8.5 6.5 9.6 4.2h4.8l1.1 2.3" })
488
+ );
489
+ }
490
+
491
+ /* ---------- 主页面 ---------- */
492
+ function Panel() {
493
+ const store = getStore();
494
+ const [s, setSnap] = React.useState(() => store.get());
495
+
496
+ React.useEffect(() => {
497
+ setSnap(store.get());
498
+ const off = store.subscribe(() => setSnap(store.get()));
499
+ return () => { off(); };
500
+ }, []);
501
+
502
+ React.useEffect(() => { store.loadHealth(); store.loadConfig(); }, []);
503
+
504
+ const busy = s.busy;
505
+ const isVideo = s.mode === "video";
506
+ const key = s.health && s.health.key ? s.health.key : null;
507
+ const keyBad = key !== null && key.present !== true;
508
+ const shown = s.result;
509
+
510
+ const onKeyDown = (e) => {
511
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); store.run(); }
512
+ };
513
+
514
+ const buttonLabel = busy
515
+ ? (isVideo ? "生成视频… " : "生成中… ") + (s.elapsed > 0 ? s.elapsed.toFixed(0) + "s" : "")
516
+ : (isVideo ? "生成视频" : "生成");
517
+
518
+ const modeBtn = (mode, text) => React.createElement("button", {
519
+ key: mode,
520
+ type: "button",
521
+ className: "agn-mode" + (s.mode === mode ? " agn-mode-on" : ""),
522
+ disabled: busy,
523
+ onClick: () => store.set({ mode }),
524
+ }, text);
525
+
526
+ /* ---- 参数区:图片走档位+宽高比,视频走秒数+画幅(720P 固定) ---- */
527
+ const params = isVideo
528
+ ? [
529
+ React.createElement("span", { key: "l1", className: "agn-label" }, "时长"),
530
+ React.createElement("select", {
531
+ key: "s1", className: "agn-select", value: s.vSeconds, disabled: busy,
532
+ onChange: (e) => store.set({ vSeconds: e.target.value }),
533
+ }, VIDEO_SECONDS.map((x) => React.createElement("option", { key: x, value: x }, x + " 秒"))),
534
+ React.createElement("span", { key: "l2", className: "agn-label" }, "画幅"),
535
+ React.createElement("select", {
536
+ key: "s2", className: "agn-select", value: s.vRatio, disabled: busy,
537
+ onChange: (e) => store.set({ vRatio: e.target.value }),
538
+ }, VIDEO_RATIOS.map((x) => React.createElement("option", { key: x, value: x }, x))),
539
+ React.createElement("span", { key: "m", className: "agn-meta" }, "720P"),
540
+ ]
541
+ : [
542
+ React.createElement("span", { key: "l1", className: "agn-label" }, "尺寸"),
543
+ React.createElement("select", {
544
+ key: "s1", className: "agn-select", value: s.size, disabled: busy,
545
+ onChange: (e) => store.set({ size: e.target.value }),
546
+ }, SIZE_OPTIONS.map((x) => React.createElement("option", { key: x, value: x }, x))),
547
+ React.createElement("span", { key: "l2", className: "agn-label" }, "比例"),
548
+ React.createElement("select", {
549
+ key: "s2", className: "agn-select", value: s.ratio, disabled: busy,
550
+ onChange: (e) => store.set({ ratio: e.target.value }),
551
+ }, RATIO_OPTIONS.map((x) => React.createElement("option", { key: x, value: x }, x))),
552
+ ];
553
+
554
+ const resultLabel = shown === null
555
+ ? "生成结果(还没有)"
556
+ : (shown.kind === "video" ? "生成结果 · 视频" : "生成结果 · 图片");
557
+
558
+ return React.createElement("div", { className: "agn-root" },
559
+ React.createElement("div", { className: "agn-head" },
560
+ React.createElement("span", { className: "agn-title" }, "Agnes 创作"),
561
+ React.createElement("span", { className: "agn-meta" },
562
+ isVideo
563
+ ? (s.health && s.health.videoModel ? s.health.videoModel : "agnes-video-2.5-flash")
564
+ : (s.health && s.health.model ? s.health.model : "agnes-image-2.5-flash"))
565
+ ),
566
+
567
+ React.createElement("div", { className: "agn-card" },
568
+ React.createElement("div", { className: "agn-modes" },
569
+ modeBtn("image", "图片"),
570
+ modeBtn("video", "视频")
571
+ ),
572
+ React.createElement("div", { className: "agn-label" }, "提示词"),
573
+ React.createElement("textarea", {
574
+ className: "agn-textarea",
575
+ value: s.prompt,
576
+ placeholder: isVideo
577
+ ? "描述镜头与动作:主体 + 动作 + 镜头运动 + 光线氛围…\n(Cmd/Ctrl + Enter 生成)"
578
+ : "描述你想要的画面:主体 + 场景 + 风格 + 光照 + 构图 + 质量…\n(Cmd/Ctrl + Enter 生成)",
579
+ onChange: (e) => store.set({ prompt: e.target.value }),
580
+ onKeyDown,
581
+ disabled: busy,
582
+ }),
583
+ React.createElement("div", { className: "agn-row" },
584
+ params,
585
+ React.createElement("button", {
586
+ className: "agn-btn", type: "button",
587
+ disabled: busy || s.prompt.trim() === "" || keyBad,
588
+ onClick: () => store.run(),
589
+ }, buttonLabel)
590
+ ),
591
+
592
+ /* 视频是异步任务:给出阶段与百分比,避免只看到一个跳动的秒数 */
593
+ busy && isVideo
594
+ ? React.createElement("div", null,
595
+ React.createElement("div", { className: "agn-bar" },
596
+ React.createElement("div", {
597
+ className: "agn-bar-fill",
598
+ style: { width: Math.max(3, Math.min(100, s.vProgress)) + "%" },
599
+ })),
600
+ React.createElement("div", { className: "agn-hint" },
601
+ (s.vStatus || "排队中…") + " " + Math.max(0, Math.round(s.vProgress)) + "%"
602
+ + (s.videoId ? " 任务 " + String(s.videoId).slice(0, 18) : ""))
603
+ )
604
+ : null,
605
+
606
+ keyBad
607
+ ? React.createElement("div", { className: "agn-err" },
608
+ "未找到 Key:" + (key.file || "") + " —— 把 Key 写进该文件后可立即使用")
609
+ : null,
610
+ s.error !== "" ? React.createElement("div", { className: "agn-err" }, s.error) : null,
611
+ busy && !isVideo
612
+ ? React.createElement("div", { className: "agn-hint" },
613
+ "正在生成:可切到别的面板,回来仍会接着显示结果(请求不会中断)")
614
+ : null
615
+ ),
616
+
617
+ /* ---- 结果区 ---- */
618
+ React.createElement("div", { className: "agn-card" },
619
+ React.createElement("div", { className: "agn-card-head" },
620
+ React.createElement("div", { className: "agn-label" }, resultLabel),
621
+ React.createElement("button", {
622
+ type: "button",
623
+ className: "agn-btn-ghost",
624
+ disabled: busy || (s.result === null && s.history.length === 0),
625
+ title: "清空界面上的生成结果显示(不会删除已保存到桌面的文件)",
626
+ onClick: () => store.clear(),
627
+ }, "清空")
628
+ ),
629
+
630
+ shown !== null
631
+ ? React.createElement(MediaBox, { entry: shown, big: true })
632
+ : React.createElement("div", { className: "agn-shot" },
633
+ React.createElement("div", { className: "agn-shot-msg" },
634
+ busy ? (isVideo ? "正在生成视频…" : "正在生成图片…")
635
+ : "输入提示词后点「生成」,结果会显示在这里")),
636
+
637
+ shown !== null
638
+ ? React.createElement("div", { className: "agn-hint" },
639
+ React.createElement("span", null,
640
+ shown.filename + " · " + (shown.bytes / 1024 / 1024 >= 1
641
+ ? (shown.bytes / 1024 / 1024).toFixed(2) + " MB"
642
+ : (shown.bytes / 1024).toFixed(0) + " KB")
643
+ + " · " + shown.size + "/" + shown.ratio
644
+ + (shown.seconds ? " · " + shown.seconds + " 秒" : "")
645
+ + " · 用时 " + shown.elapsedSec + "s"),
646
+ React.createElement("br"),
647
+ React.createElement("span", null, "已保存到 "),
648
+ React.createElement("span", { className: "agn-code" }, shown.path || ""))
649
+ : null
650
+ ),
651
+
652
+ /* ---- 历史 ---- */
653
+ s.history.length > 1
654
+ ? React.createElement("div", { className: "agn-card agn-hist" },
655
+ React.createElement("div", { className: "agn-label" }, "最近生成"),
656
+ React.createElement("div", { className: "agn-hist-strip" },
657
+ s.history.map((h, i) => React.createElement("button", {
658
+ key: h.filename + "-" + i,
659
+ type: "button",
660
+ className: "agn-thumb" + (shown && shown.filename === h.filename ? " agn-thumb-on" : ""),
661
+ title: h.filename,
662
+ onClick: () => store.show(h),
663
+ },
664
+ h.kind === "video"
665
+ ? React.createElement("div", { className: "agn-thumb-play" }, "▶")
666
+ : React.createElement("img", { src: srcOf(h.filename), alt: h.filename, loading: "lazy" }),
667
+ React.createElement("span", { className: "agn-thumb-cap" },
668
+ h.kind === "video"
669
+ ? (h.seconds ? h.seconds + "s" : "视频")
670
+ : (h.bytes / 1024).toFixed(0) + "KB")
671
+ ))
672
+ )
673
+ )
674
+ : null,
675
+
676
+ /* ---- 设置:Key 文件路径 / 输出目录 ---- */
677
+ React.createElement("div", { className: "agn-card" },
678
+ React.createElement("div", { className: "agn-card-head" },
679
+ React.createElement("div", { className: "agn-label" }, "设置"),
680
+ React.createElement("button", {
681
+ type: "button", className: "agn-btn-ghost", disabled: s.cfgSaving,
682
+ onClick: () => store.toggleCfg(),
683
+ }, s.cfgOpen ? "收起" : "展开")
684
+ ),
685
+
686
+ s.cfgOpen
687
+ ? React.createElement("div", { className: "agn-field" },
688
+ React.createElement("div", { className: "agn-label" }, "Key 文件路径"),
689
+ React.createElement("input", {
690
+ className: "agn-input", type: "text", spellCheck: false,
691
+ value: s.cfgDraft.keyFile,
692
+ placeholder: s.cfgDefaults ? s.cfgDefaults.keyFile : "~/.agnes-key.txt",
693
+ disabled: s.cfgSaving,
694
+ onChange: (e) => store.setCfgDraft("keyFile", e.target.value),
695
+ }),
696
+ React.createElement("div", { className: "agn-note" },
697
+ "只放 key 本身,别带 Bearer 前缀。留空 = 默认 ",
698
+ s.cfgDefaults ? s.cfgDefaults.keyFile : "~/.agnes-key.txt")
699
+ )
700
+ : null,
701
+
702
+ s.cfgOpen
703
+ ? React.createElement("div", { className: "agn-field" },
704
+ React.createElement("div", { className: "agn-label" }, "输出目录"),
705
+ React.createElement("input", {
706
+ className: "agn-input", type: "text", spellCheck: false,
707
+ value: s.cfgDraft.outDir,
708
+ placeholder: s.cfgDefaults ? s.cfgDefaults.outDir : "~/Desktop/Agnes出图",
709
+ disabled: s.cfgSaving,
710
+ onChange: (e) => store.setCfgDraft("outDir", e.target.value),
711
+ }),
712
+ React.createElement("div", { className: "agn-note" },
713
+ "图片和视频都存这里,目录不存在会自动创建。留空 = 默认 ",
714
+ s.cfgDefaults ? s.cfgDefaults.outDir : "~/Desktop/Agnes出图")
715
+ )
716
+ : null,
717
+
718
+ s.cfgOpen
719
+ ? React.createElement("div", { className: "agn-row" },
720
+ React.createElement("button", {
721
+ type: "button", className: "agn-btn", disabled: s.cfgSaving,
722
+ onClick: () => store.saveConfig(),
723
+ }, s.cfgSaving ? "保存中…" : "保存"),
724
+ React.createElement("button", {
725
+ type: "button", className: "agn-btn-ghost", disabled: s.cfgSaving,
726
+ title: "把两个输入框清空,保存后即回退到默认路径",
727
+ onClick: () => {
728
+ store.setCfgDraft("keyFile", "");
729
+ store.setCfgDraft("outDir", "");
730
+ },
731
+ }, "填回默认"),
732
+ s.cfgNotice !== "" ? React.createElement("span", { className: "agn-okline" }, s.cfgNotice) : null
733
+ )
734
+ : null,
735
+
736
+ s.cfgError !== "" ? React.createElement("div", { className: "agn-err" }, s.cfgError) : null,
737
+
738
+ s.cfgOpen
739
+ ? React.createElement("div", { className: "agn-note" },
740
+ "保存到 ",
741
+ (s.health && s.health.configFile) || "~/.dsh/agnes-image/config.json")
742
+ : null
743
+ ),
744
+
745
+ React.createElement("div", { className: "agn-hint" },
746
+ s.health === null
747
+ ? "正在读取插件状态…"
748
+ : s.health.ok
749
+ ? React.createElement("span", null,
750
+ "输出目录 ",
751
+ React.createElement("span", { className: "agn-code" }, s.health.outDir || ""),
752
+ " Key:",
753
+ React.createElement("span", { className: "agn-code" },
754
+ key && key.present ? key.masked : "未就绪"))
755
+ : "插件宿主半未就绪:" + (s.health.error || "未知错误")
756
+ )
757
+ );
758
+ }
759
+
760
+ /* ---------- 注册 ---------- */
761
+ function apply(ctx) {
762
+ ensureCss();
763
+
764
+ const slots = ctx.get("slots");
765
+ if (slots === undefined) return;
766
+
767
+ slots.inject("main", () => slots.register(
768
+ { name: "main", key: ID },
769
+ () => React.createElement(Panel)
770
+ ));
771
+
772
+ slots.inject("sidebar.panellist", () => slots.register(
773
+ { name: "sidebar.panellist", id: ID, order: 25, label: () => "Agnes创作" },
774
+ (props) => React.createElement(Glyph, props)
775
+ ));
776
+ }
777
+
778
+ exports.apply = apply;
779
+ exports.inject = ["slots"];
780
+ exports.__internals = { getStore, createStore, srcOf };
781
+ return module.exports;
782
+ },
783
+ });