dsh-agnes-create 0.4.1 → 0.10.3
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/README.md +78 -5
- package/lib/agnes.js +545 -14
- package/lib/client.js +1164 -62
- package/lib/index.js +625 -13
- package/package.json +7 -3
package/lib/client.js
CHANGED
|
@@ -13,16 +13,34 @@ window.__ModuleLoader__.load({
|
|
|
13
13
|
// 视频:Flash 固定 720P,seconds 是字符串 "4"–"12"
|
|
14
14
|
const VIDEO_SECONDS = ["4", "5", "6", "7", "8", "9", "10", "11", "12"];
|
|
15
15
|
const VIDEO_RATIOS = ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16"];
|
|
16
|
-
|
|
16
|
+
/* 与宿主 agnes.js 的 HISTORY_MAX 保持一致:首屏先按 24 条渲染,
|
|
17
|
+
loadHistory() 回来后以宿主返回的 max 为准 */
|
|
18
|
+
const HISTORY_MAX = 24;
|
|
17
19
|
const POLL_MS = 6000;
|
|
18
20
|
const VIDEO_DEADLINE_MS = 15 * 60 * 1000;
|
|
19
21
|
// 图生图:一次最多 8 张参考图(与宿主 MAX_REF_IMAGES 保持一致)
|
|
20
22
|
const MAX_REFS = 8;
|
|
23
|
+
|
|
24
|
+
/** 视频 reference 模式的参考图上限(上游:images 最多 5 张) */
|
|
25
|
+
const MAX_VREFS = 5;
|
|
21
26
|
// 发给上游前先下采样:长边 1280 足够模型识别,base64 也压得住
|
|
22
27
|
const REF_MAX_EDGE = 1280;
|
|
23
28
|
const REF_QUALITY = 0.88;
|
|
24
29
|
const REF_PNG_KEEP_BYTES = 800 * 1024;
|
|
25
30
|
|
|
31
|
+
/**
|
|
32
|
+
* 提示词预设:点一下把风格描述追加到当前提示词末尾。
|
|
33
|
+
* 刻意写成「可叠加的风格后缀」,而不是整段替换——用户已经写好的主体不会被冲掉。
|
|
34
|
+
*/
|
|
35
|
+
const PROMPT_PRESETS = [
|
|
36
|
+
{ label: "写实摄影", text: "写实摄影风格,自然光,浅景深,35mm 镜头,细节丰富" },
|
|
37
|
+
{ label: "水彩", text: "水彩画风格,纸纹质感,柔和晕染,留白构图" },
|
|
38
|
+
{ label: "3D 渲染", text: "3D 渲染风格,柔光棚拍,材质清晰,背景简洁" },
|
|
39
|
+
{ label: "海报", text: "平面海报构图,大面积留白,主体突出,配色克制" },
|
|
40
|
+
{ label: "夜景霓虹", text: "夜景霓虹灯光,湿润地面反光,氛围浓烈" },
|
|
41
|
+
{ label: "极简", text: "极简构图,单一主体,纯净背景,柔和渐变光" },
|
|
42
|
+
];
|
|
43
|
+
|
|
26
44
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
27
45
|
|
|
28
46
|
/* ---------- 样式 ---------- */
|
|
@@ -32,6 +50,13 @@ window.__ModuleLoader__.load({
|
|
|
32
50
|
.agn-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
|
|
33
51
|
.agn-title { font-size: 16px; font-weight: 600; color: var(--dsw-alias-label-primary, inherit); }
|
|
34
52
|
.agn-meta { font-size: 12px; color: var(--dsw-alias-label-secondary, #888); }
|
|
53
|
+
/* 标题栏里的模型下拉:长得像一行说明文字,但可以点开换模型 */
|
|
54
|
+
.agn-model { font: inherit; font-size: 12px; line-height: 1.4; padding: 1px 4px;
|
|
55
|
+
max-width: 220px; cursor: pointer; border-radius: 6px;
|
|
56
|
+
color: var(--dsw-alias-label-secondary, #888); background: transparent;
|
|
57
|
+
border: 1px solid transparent; }
|
|
58
|
+
.agn-model:hover:not(:disabled) { border-color: var(--dsw-alias-border-l2, #ddd); }
|
|
59
|
+
.agn-model:disabled { opacity: .6; cursor: not-allowed; }
|
|
35
60
|
.agn-card { border: 1px solid var(--dsw-alias-border-l2, #e3e3e3); border-radius: 10px;
|
|
36
61
|
padding: 12px 14px; display: flex; flex-direction: column; gap: 10px; }
|
|
37
62
|
.agn-label { font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-secondary, #666); }
|
|
@@ -73,7 +98,8 @@ window.__ModuleLoader__.load({
|
|
|
73
98
|
transition: width .3s ease; }
|
|
74
99
|
|
|
75
100
|
/* 媒体展示区 */
|
|
76
|
-
.agn-shot {
|
|
101
|
+
.agn-shot { position: relative; border-radius: 10px;
|
|
102
|
+
border: 1px solid var(--dsw-alias-border-l2, #e3e3e3);
|
|
77
103
|
background: var(--dsw-alias-bg-layer-2, #fafafa); overflow: hidden;
|
|
78
104
|
display: flex; align-items: center; justify-content: center; min-height: 120px; }
|
|
79
105
|
.agn-shot img, .agn-shot video { display: block; max-width: 100%; height: auto; }
|
|
@@ -82,6 +108,39 @@ window.__ModuleLoader__.load({
|
|
|
82
108
|
.agn-shot-msg { padding: 18px; font-size: 12px; text-align: center; line-height: 1.7;
|
|
83
109
|
color: var(--dsw-alias-label-secondary, #888); }
|
|
84
110
|
|
|
111
|
+
/* 悬停浮现的提示词:贴在媒体底部,默认透明,鼠标进来才出现 */
|
|
112
|
+
.agn-prompt { position: absolute; left: 0; right: 0; bottom: 0;
|
|
113
|
+
display: flex; align-items: flex-end; gap: 8px; padding: 8px 10px;
|
|
114
|
+
background: linear-gradient(to top, rgba(0,0,0,.62), rgba(0,0,0,0));
|
|
115
|
+
opacity: 0; transition: opacity .16s ease; pointer-events: none; }
|
|
116
|
+
.agn-shot:hover .agn-prompt { opacity: 1; pointer-events: auto; }
|
|
117
|
+
.agn-prompt-text { flex: 1 1 auto; min-width: 0; font-size: 11px; line-height: 1.5;
|
|
118
|
+
color: #fff; display: -webkit-box; -webkit-line-clamp: 3;
|
|
119
|
+
-webkit-box-orient: vertical; overflow: hidden; word-break: break-word; }
|
|
120
|
+
.agn-prompt-copy { flex: none; font: inherit; font-size: 11px; line-height: 1.3;
|
|
121
|
+
padding: 2px 8px; border-radius: 5px; cursor: pointer;
|
|
122
|
+
color: #fff; background: rgba(255,255,255,.18);
|
|
123
|
+
border: 1px solid rgba(255,255,255,.45); }
|
|
124
|
+
.agn-prompt-copy:hover { background: rgba(255,255,255,.3); }
|
|
125
|
+
|
|
126
|
+
/* 输出目录清单 */
|
|
127
|
+
.agn-lib { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }
|
|
128
|
+
.agn-lib-tabs { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
|
|
129
|
+
.agn-lib-grid { display: flex; gap: 8px; flex-wrap: wrap;
|
|
130
|
+
max-height: 300px; overflow-y: auto; padding: 2px; }
|
|
131
|
+
.agn-lib-item { position: relative; width: 84px; height: 84px; border-radius: 8px;
|
|
132
|
+
padding: 0; overflow: hidden; cursor: pointer; flex: none;
|
|
133
|
+
border: 2px solid var(--dsw-alias-border-l2, #e3e3e3);
|
|
134
|
+
background: var(--dsw-alias-bg-layer-2, #fafafa); }
|
|
135
|
+
.agn-lib-item:hover { border-color: var(--dsw-alias-state-business-primary, #4b7bec); }
|
|
136
|
+
.agn-lib-item img, .agn-lib-item video { width: 100%; height: 100%;
|
|
137
|
+
object-fit: cover; display: block; pointer-events: none; }
|
|
138
|
+
.agn-lib-badge { position: absolute; left: 4px; bottom: 4px; font-size: 10px;
|
|
139
|
+
line-height: 1.4; padding: 0 5px; border-radius: 4px;
|
|
140
|
+
color: #fff; background: rgba(0,0,0,.55); }
|
|
141
|
+
.agn-lib-empty { font-size: 12px; line-height: 1.6;
|
|
142
|
+
color: var(--dsw-alias-label-secondary, #888); }
|
|
143
|
+
|
|
85
144
|
/* 历史缩略图 */
|
|
86
145
|
.agn-hist { display: flex; flex-direction: column; gap: 6px; }
|
|
87
146
|
.agn-hist-strip { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
@@ -90,10 +149,15 @@ window.__ModuleLoader__.load({
|
|
|
90
149
|
border: 2px solid var(--dsw-alias-border-l2, #e3e3e3);
|
|
91
150
|
background: var(--dsw-alias-bg-layer-2, #fafafa); }
|
|
92
151
|
.agn-thumb-on { border-color: var(--dsw-alias-state-business-primary, #4b7bec); }
|
|
93
|
-
|
|
152
|
+
/* 缩略图里放 <video> 取首帧;pointer-events: none 让点击继续落到外层按钮上
|
|
153
|
+
(video 属于可交互内容,套在 button 里会吃掉点击) */
|
|
154
|
+
.agn-thumb img, .agn-thumb video { width: 100%; height: 100%;
|
|
155
|
+
object-fit: cover; display: block; pointer-events: none; }
|
|
156
|
+
/* ▶ 只是角标,压在首帧上;别挡点击,也别把首帧盖住 */
|
|
94
157
|
.agn-thumb-play { position: absolute; inset: 0; display: flex; align-items: center;
|
|
95
|
-
justify-content: center; font-size:
|
|
96
|
-
color: #fff;
|
|
158
|
+
justify-content: center; font-size: 20px; line-height: 1;
|
|
159
|
+
color: #fff; text-shadow: 0 1px 4px rgba(0, 0, 0, .75);
|
|
160
|
+
pointer-events: none; background: rgba(0, 0, 0, .12); }
|
|
97
161
|
.agn-thumb-cap { position: absolute; left: 0; right: 0; bottom: 0; font-size: 10px;
|
|
98
162
|
text-align: center; padding: 1px 0;
|
|
99
163
|
color: var(--dsw-alias-label-primary-foreground, #fff);
|
|
@@ -144,6 +208,57 @@ window.__ModuleLoader__.load({
|
|
|
144
208
|
.agn-tag { font-size: 11px; padding: 2px 8px; border-radius: 999px;
|
|
145
209
|
background: var(--dsw-alias-bg-layer-2, #f1f1f1);
|
|
146
210
|
color: var(--dsw-alias-label-secondary, #555); }
|
|
211
|
+
|
|
212
|
+
/* 提示词预设 */
|
|
213
|
+
.agn-presets { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px; }
|
|
214
|
+
.agn-preset { font: inherit; font-size: 11px; line-height: 1.4; padding: 2px 9px;
|
|
215
|
+
border-radius: 999px; cursor: pointer;
|
|
216
|
+
color: var(--dsw-alias-label-secondary, #555); background: transparent;
|
|
217
|
+
border: 1px solid var(--dsw-alias-border-l2, #ddd); }
|
|
218
|
+
.agn-preset:hover:not(:disabled) { border-color: var(--dsw-alias-state-business-primary, #4b7bec);
|
|
219
|
+
color: var(--dsw-alias-state-business-primary, #4b7bec); }
|
|
220
|
+
.agn-preset:disabled { opacity: .45; cursor: not-allowed; }
|
|
221
|
+
|
|
222
|
+
/* 视频关键帧(图生视频) */
|
|
223
|
+
.agn-frames { display: flex; flex-direction: column; gap: 8px; }
|
|
224
|
+
.agn-frames-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-start; }
|
|
225
|
+
.agn-frame { display: flex; flex-direction: column; gap: 4px; flex: none; }
|
|
226
|
+
.agn-frame-cap { font-size: 11px; color: var(--dsw-alias-label-secondary, #888); }
|
|
227
|
+
.agn-frame-box { position: relative; width: 72px; height: 72px; border-radius: 8px;
|
|
228
|
+
overflow: hidden; border: 1px solid var(--dsw-alias-border-l2, #e3e3e3);
|
|
229
|
+
background: var(--dsw-alias-bg-layer-2, #fafafa); }
|
|
230
|
+
.agn-frame-box img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
231
|
+
.agn-frame-add { width: 72px; height: 72px; border-radius: 8px; cursor: pointer;
|
|
232
|
+
font: inherit; font-size: 11px; line-height: 1.35; padding: 4px;
|
|
233
|
+
color: var(--dsw-alias-label-secondary, #666); background: transparent;
|
|
234
|
+
border: 1px dashed var(--dsw-alias-border-l2, #ccc); }
|
|
235
|
+
.agn-frame-add:disabled { opacity: .45; cursor: not-allowed; }
|
|
236
|
+
/* 「+ 结果」:从已生成的图片里挑一张当关键帧 */
|
|
237
|
+
.agn-frame-from { width: 72px; font: inherit; font-size: 11px; line-height: 1.4;
|
|
238
|
+
padding: 2px 0; border-radius: 6px; cursor: pointer;
|
|
239
|
+
color: var(--dsw-alias-label-secondary, #555); background: transparent;
|
|
240
|
+
border: 1px solid var(--dsw-alias-border-l2, #ddd); }
|
|
241
|
+
.agn-frame-from:hover:not(:disabled) {
|
|
242
|
+
border-color: var(--dsw-alias-state-business-primary, #4b7bec);
|
|
243
|
+
color: var(--dsw-alias-state-business-primary, #4b7bec); }
|
|
244
|
+
.agn-frame-from:disabled { opacity: .45; cursor: not-allowed; }
|
|
245
|
+
.agn-frame .agn-ref-pick { margin-top: 2px; }
|
|
246
|
+
.agn-seed { width: 72px; }
|
|
247
|
+
|
|
248
|
+
/* 结果收尾动作 */
|
|
249
|
+
.agn-acts { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
|
|
250
|
+
.agn-act { font: inherit; font-size: 11px; line-height: 1.4; padding: 3px 10px;
|
|
251
|
+
border-radius: 6px; cursor: pointer;
|
|
252
|
+
color: var(--dsw-alias-label-secondary, #555); background: transparent;
|
|
253
|
+
border: 1px solid var(--dsw-alias-border-l2, #ddd); }
|
|
254
|
+
.agn-act:hover { border-color: var(--dsw-alias-state-business-primary, #4b7bec);
|
|
255
|
+
color: var(--dsw-alias-state-business-primary, #4b7bec); }
|
|
256
|
+
.agn-act-on { border-color: var(--dsw-alias-state-business-primary, #4b7bec);
|
|
257
|
+
color: var(--dsw-alias-state-business-primary, #4b7bec); }
|
|
258
|
+
/* 「测试连接」的结果行:和按钮之间留一点距离 */
|
|
259
|
+
.agn-check { margin-top: 6px; }
|
|
260
|
+
.agn-notice { font-size: 12px; color: var(--dsw-alias-state-success-primary, #2a2);
|
|
261
|
+
line-height: 1.6; }
|
|
147
262
|
`;
|
|
148
263
|
|
|
149
264
|
function ensureCss() {
|
|
@@ -160,6 +275,25 @@ window.__ModuleLoader__.load({
|
|
|
160
275
|
return "/agnes/media?name=" + encodeURIComponent(name);
|
|
161
276
|
}
|
|
162
277
|
|
|
278
|
+
/* 「+ 结果」的候选:最近生成的图片 + 当前结果区那张(按文件名去重)。
|
|
279
|
+
从「输出目录」点出来的图只进了结果区、没进历史,只翻历史会漏掉它,
|
|
280
|
+
于是刚在眼前看到的那张图反而选不进参考图 / 关键帧。 */
|
|
281
|
+
function pickableImagesOf(state) {
|
|
282
|
+
const history = (Array.isArray(state.history) ? state.history : []).filter((h) => h.kind === "image");
|
|
283
|
+
const cur = state.result;
|
|
284
|
+
if (cur === null || cur === undefined || cur.kind !== "image") return history;
|
|
285
|
+
if (typeof cur.filename !== "string" || cur.filename === "") return history;
|
|
286
|
+
if (history.some((h) => h.filename === cur.filename)) return history;
|
|
287
|
+
return [cur].concat(history);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/* 视频生成方式:宿主回来的 mode 只可能是这三种之一,其余一律当纯文本 */
|
|
291
|
+
function modeOf(mode) {
|
|
292
|
+
if (mode === "keyframe") return "keyframe";
|
|
293
|
+
if (mode === "reference") return "reference";
|
|
294
|
+
return "text";
|
|
295
|
+
}
|
|
296
|
+
|
|
163
297
|
/* ==========================================================================
|
|
164
298
|
参考图预处理(图生图)
|
|
165
299
|
--------------------------------------------------------------------------
|
|
@@ -261,6 +395,7 @@ window.__ModuleLoader__.load({
|
|
|
261
395
|
vProgress: 0,
|
|
262
396
|
vStatus: "",
|
|
263
397
|
videoId: null,
|
|
398
|
+
vTaskId: null,
|
|
264
399
|
health: null,
|
|
265
400
|
healthRequested: false,
|
|
266
401
|
|
|
@@ -268,17 +403,46 @@ window.__ModuleLoader__.load({
|
|
|
268
403
|
cfg: null, // 生效配置 { keyFile, outDir }
|
|
269
404
|
cfgDefaults: null,
|
|
270
405
|
cfgStored: null,
|
|
271
|
-
cfgDraft: { keyFile: "", outDir: "" },
|
|
406
|
+
cfgDraft: { keyFile: "", outDir: "", imageModel: "", videoModel: "" },
|
|
272
407
|
cfgOpen: false,
|
|
273
408
|
cfgSaving: false,
|
|
274
409
|
cfgError: "",
|
|
275
410
|
cfgNotice: "",
|
|
411
|
+
checking: false, // 正在测连通性
|
|
412
|
+
checkResult: null, // { ok, text, detail }
|
|
413
|
+
upModels: null, // 连接测试拿到的上游真实模型清单 { image: [], video: [] }
|
|
276
414
|
|
|
277
415
|
/* ---- 图生图参考图 ---- */
|
|
278
416
|
refs: [], // { id, name, src(dataURI), bytes, from: 'local' | 'result' }
|
|
279
417
|
refsBusy: false,
|
|
280
418
|
refsError: "",
|
|
281
419
|
refPickOpen: false,
|
|
420
|
+
/* ---- 视频参考图(reference 模式,最多 5 张) ---- */
|
|
421
|
+
vRefs: [],
|
|
422
|
+
vRefsBusy: false,
|
|
423
|
+
vRefsError: "",
|
|
424
|
+
vRefPickOpen: false,
|
|
425
|
+
|
|
426
|
+
/* ---- 视频关键帧(可选):本地图直接当首帧 / 尾帧 ---- */
|
|
427
|
+
vFirst: null, // { src(dataURI), name, bytes }
|
|
428
|
+
vLast: null,
|
|
429
|
+
vSeed: "", // 空串 = 不传 seed
|
|
430
|
+
frameBusy: false, // 正在压缩关键帧图片
|
|
431
|
+
framePick: "", // '' | 'first' | 'last':本地文件选择器的目标
|
|
432
|
+
framePickFrom: "", // '' | 'first' | 'last':生成结果选择面板的目标
|
|
433
|
+
|
|
434
|
+
/* ---- 历史:宿主落盘,刷新/重启后还在 ---- */
|
|
435
|
+
historyMax: HISTORY_MAX,
|
|
436
|
+
historyLoaded: false,
|
|
437
|
+
notice: "", // 一次性操作反馈(复制路径、打开访达…)
|
|
438
|
+
|
|
439
|
+
/* ---- 输出目录清单:把磁盘上已有的图/视频捞回来看 ---- */
|
|
440
|
+
library: null, // null = 还没加载过;[] = 加载过但是空
|
|
441
|
+
libraryOpen: false,
|
|
442
|
+
libraryTab: "image", // 'image' | 'video'
|
|
443
|
+
libraryBusy: false,
|
|
444
|
+
libraryError: "",
|
|
445
|
+
copiedKey: "", // 刚刚复制了哪一条的提示词(用于显示「已复制」)
|
|
282
446
|
};
|
|
283
447
|
|
|
284
448
|
const snapshot = () => ({ ...state, history: state.history.slice() });
|
|
@@ -312,9 +476,60 @@ window.__ModuleLoader__.load({
|
|
|
312
476
|
};
|
|
313
477
|
|
|
314
478
|
const pushResult = (entry) => {
|
|
315
|
-
set({ result: entry, history: [entry, ...state.history].slice(0,
|
|
479
|
+
set({ result: entry, history: [entry, ...state.history].slice(0, state.historyMax) });
|
|
480
|
+
// 顺手落盘:/agnes/history 是唯一真相,写失败也不影响本次展示
|
|
481
|
+
void appendHistory(entry);
|
|
482
|
+
// 新文件已经写进输出目录了,目录清单开着的话跟着变(没加载过就不去打扰)
|
|
483
|
+
void refreshLibrary();
|
|
316
484
|
};
|
|
317
485
|
|
|
486
|
+
/* ---------- 历史(宿主持久化) ---------- */
|
|
487
|
+
async function loadHistory() {
|
|
488
|
+
try {
|
|
489
|
+
const res = await fetch("/agnes/history");
|
|
490
|
+
if (!res.ok) {
|
|
491
|
+
// 旧版宿主没有这条路由:保持空历史即可,不要报错吓人
|
|
492
|
+
set({ historyLoaded: true });
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const data = await res.json();
|
|
496
|
+
if (!data || data.ok !== true) {
|
|
497
|
+
set({ historyLoaded: true });
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const entries = Array.isArray(data.entries) ? data.entries : [];
|
|
501
|
+
const max = typeof data.max === "number" && data.max > 0 ? data.max : HISTORY_MAX;
|
|
502
|
+
update((prev) => ({
|
|
503
|
+
history: entries,
|
|
504
|
+
historyMax: max,
|
|
505
|
+
historyLoaded: true,
|
|
506
|
+
// 主预览默认显示最新一条;已经有结果时不动它
|
|
507
|
+
result: prev.result === null ? (entries[0] || null) : prev.result,
|
|
508
|
+
}));
|
|
509
|
+
} catch {
|
|
510
|
+
set({ historyLoaded: true });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function appendHistory(entry) {
|
|
515
|
+
try {
|
|
516
|
+
const res = await fetch("/agnes/history", {
|
|
517
|
+
method: "POST",
|
|
518
|
+
headers: { "content-type": "application/json" },
|
|
519
|
+
body: JSON.stringify(entry),
|
|
520
|
+
});
|
|
521
|
+
if (!res.ok) return;
|
|
522
|
+
const data = await res.json();
|
|
523
|
+
if (!data || data.ok !== true) return;
|
|
524
|
+
// 只有宿主确实回了列表才覆盖本地:宁可用旧列表,也不要把界面清空
|
|
525
|
+
if (!Array.isArray(data.entries)) return;
|
|
526
|
+
update((prev) => ({
|
|
527
|
+
history: data.entries,
|
|
528
|
+
historyMax: typeof data.max === "number" && data.max > 0 ? data.max : prev.historyMax,
|
|
529
|
+
}));
|
|
530
|
+
} catch { /* 落盘失败不影响当前结果展示 */ }
|
|
531
|
+
}
|
|
532
|
+
|
|
318
533
|
async function loadHealth() {
|
|
319
534
|
if (state.healthRequested) return;
|
|
320
535
|
state.healthRequested = true;
|
|
@@ -341,7 +556,12 @@ window.__ModuleLoader__.load({
|
|
|
341
556
|
cfg: data.config,
|
|
342
557
|
cfgDefaults: data.defaults,
|
|
343
558
|
cfgStored: data.stored,
|
|
344
|
-
cfgDraft: {
|
|
559
|
+
cfgDraft: {
|
|
560
|
+
keyFile: data.config.keyFile,
|
|
561
|
+
outDir: data.config.outDir,
|
|
562
|
+
imageModel: data.config.imageModel || "",
|
|
563
|
+
videoModel: data.config.videoModel || "",
|
|
564
|
+
},
|
|
345
565
|
cfgError: "",
|
|
346
566
|
});
|
|
347
567
|
} catch (e) {
|
|
@@ -359,6 +579,9 @@ window.__ModuleLoader__.load({
|
|
|
359
579
|
body: JSON.stringify({
|
|
360
580
|
keyFile: state.cfgDraft.keyFile,
|
|
361
581
|
outDir: state.cfgDraft.outDir,
|
|
582
|
+
// 空串 = 回退到内置默认模型
|
|
583
|
+
imageModel: state.cfgDraft.imageModel,
|
|
584
|
+
videoModel: state.cfgDraft.videoModel,
|
|
362
585
|
}),
|
|
363
586
|
});
|
|
364
587
|
if (res.status === 404) throw new Error("配置接口尚未就绪:需要完全重启应用(只重载窗口不够)");
|
|
@@ -368,7 +591,12 @@ window.__ModuleLoader__.load({
|
|
|
368
591
|
cfg: data.config,
|
|
369
592
|
cfgDefaults: data.defaults,
|
|
370
593
|
cfgStored: data.stored,
|
|
371
|
-
cfgDraft: {
|
|
594
|
+
cfgDraft: {
|
|
595
|
+
keyFile: data.config.keyFile,
|
|
596
|
+
outDir: data.config.outDir,
|
|
597
|
+
imageModel: data.config.imageModel || "",
|
|
598
|
+
videoModel: data.config.videoModel || "",
|
|
599
|
+
},
|
|
372
600
|
cfgNotice: "已保存",
|
|
373
601
|
});
|
|
374
602
|
// 路径变了,key 是否存在/目录是否可写都要重新算
|
|
@@ -382,6 +610,43 @@ window.__ModuleLoader__.load({
|
|
|
382
610
|
}
|
|
383
611
|
}
|
|
384
612
|
|
|
613
|
+
/** 测一下 Key 能不能连上游:顺便把上游真实的模型名单拉回来 */
|
|
614
|
+
async function checkKey() {
|
|
615
|
+
if (state.checking) return;
|
|
616
|
+
set({ checking: true, checkResult: null, cfgError: "" });
|
|
617
|
+
try {
|
|
618
|
+
const res = await fetch("/agnes/models");
|
|
619
|
+
const data = await res.json().catch(() => ({}));
|
|
620
|
+
if (res.status === 404) {
|
|
621
|
+
throw new Error("模型探测接口尚未就绪:需要完全重启应用(只重载窗口不够)");
|
|
622
|
+
}
|
|
623
|
+
if (!res.ok || !data || data.ok !== true) {
|
|
624
|
+
throw new Error((data && data.error) || ("探测失败:HTTP " + res.status));
|
|
625
|
+
}
|
|
626
|
+
const up = data.upstream || {};
|
|
627
|
+
const imgs = Array.isArray(up.image) ? up.image.length : 0;
|
|
628
|
+
const vids = Array.isArray(up.video) ? up.video.length : 0;
|
|
629
|
+
set({
|
|
630
|
+
checking: false,
|
|
631
|
+
// 把上游真实清单留下:顶部的模型下拉优先用它,比内置清单更可信
|
|
632
|
+
upModels: {
|
|
633
|
+
image: Array.isArray(up.image) ? up.image : [],
|
|
634
|
+
video: Array.isArray(up.video) ? up.video : [],
|
|
635
|
+
},
|
|
636
|
+
checkResult: {
|
|
637
|
+
ok: true,
|
|
638
|
+
text: "连接正常:上游可用 " + imgs + " 个图片模型、" + vids + " 个视频模型",
|
|
639
|
+
detail: ""
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
} catch (e) {
|
|
643
|
+
set({
|
|
644
|
+
checking: false,
|
|
645
|
+
checkResult: { ok: false, text: e && e.message ? e.message : String(e), detail: "" },
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
385
650
|
/* ---------- 图片:一次请求就出结果 ---------- */
|
|
386
651
|
async function runImage(token, prompt) {
|
|
387
652
|
const refs = state.refs;
|
|
@@ -400,7 +665,7 @@ window.__ModuleLoader__.load({
|
|
|
400
665
|
const data = await res.json();
|
|
401
666
|
if (!data.ok) throw new Error(data.error || "生成失败");
|
|
402
667
|
if (token !== runToken) return;
|
|
403
|
-
pushResult({ ...data, kind: "image", wallSec: ((Date.now() - state.startedAt) / 1000).toFixed(1) });
|
|
668
|
+
pushResult({ ...data, kind: "image", prompt, wallSec: ((Date.now() - state.startedAt) / 1000).toFixed(1) });
|
|
404
669
|
}
|
|
405
670
|
|
|
406
671
|
/* ---------- 参考图(图生图) ---------- */
|
|
@@ -519,44 +784,339 @@ window.__ModuleLoader__.load({
|
|
|
519
784
|
set({ refPickOpen: !state.refPickOpen, refsError: "" });
|
|
520
785
|
}
|
|
521
786
|
|
|
522
|
-
/* ----------
|
|
523
|
-
|
|
524
|
-
set({ vStatus: "提交中…", vProgress: 0, videoId: null });
|
|
787
|
+
/* ---------- 视频参考图(reference 模式,≤5 张) ---------- */
|
|
788
|
+
const vRefsFull = () => state.vRefs.length >= MAX_VREFS;
|
|
525
789
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
790
|
+
function addVRefsFromFiles(files) {
|
|
791
|
+
const list = Array.from(files || []).filter(
|
|
792
|
+
(f) => f && String(f.type || "").startsWith("image/")
|
|
793
|
+
);
|
|
794
|
+
if (list.length === 0) {
|
|
795
|
+
set({ vRefsError: "没有可用的图片文件(需要 PNG / JPEG / WebP / GIF)" });
|
|
796
|
+
return Promise.resolve();
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return queueRefOp(async () => {
|
|
800
|
+
set({ vRefsBusy: true, vRefsError: "" });
|
|
801
|
+
const added = [];
|
|
802
|
+
const failed = [];
|
|
803
|
+
try {
|
|
804
|
+
for (const file of list) {
|
|
805
|
+
if (state.vRefs.length + added.length >= MAX_VREFS) {
|
|
806
|
+
failed.push("超出 " + MAX_VREFS + " 张上限");
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
try {
|
|
810
|
+
const r = await shrinkImage(file);
|
|
811
|
+
added.push({
|
|
812
|
+
id: nextRefId(),
|
|
813
|
+
name: file.name || "本地图片",
|
|
814
|
+
src: r.src,
|
|
815
|
+
bytes: r.bytes,
|
|
816
|
+
from: "local",
|
|
817
|
+
});
|
|
818
|
+
} catch (e) {
|
|
819
|
+
failed.push((file.name || "图片") + ":" + (e && e.message ? e.message : String(e)));
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (added.length > 0) {
|
|
823
|
+
update((prev) => ({ vRefs: prev.vRefs.concat(added) }));
|
|
824
|
+
}
|
|
825
|
+
set({
|
|
826
|
+
vRefsError: failed.length > 0 ? failed.join(";") : "",
|
|
827
|
+
vRefPickOpen: false,
|
|
828
|
+
});
|
|
829
|
+
} finally {
|
|
830
|
+
set({ vRefsBusy: false });
|
|
831
|
+
}
|
|
530
832
|
});
|
|
531
|
-
|
|
532
|
-
if (!sub.ok) throw new Error(sub.error || "提交失败");
|
|
533
|
-
if (token !== runToken) return;
|
|
833
|
+
}
|
|
534
834
|
|
|
535
|
-
|
|
536
|
-
|
|
835
|
+
function addVRefFromResult(entry) {
|
|
836
|
+
if (!entry || entry.kind !== "image" || !entry.filename) return Promise.resolve();
|
|
837
|
+
if (vRefsFull()) {
|
|
838
|
+
set({ vRefsError: "参考图最多 " + MAX_VREFS + " 张" });
|
|
839
|
+
return Promise.resolve();
|
|
840
|
+
}
|
|
537
841
|
|
|
842
|
+
return queueRefOp(async () => {
|
|
843
|
+
set({ vRefsBusy: true, vRefsError: "" });
|
|
844
|
+
try {
|
|
845
|
+
const res = await fetch(srcOf(entry.filename));
|
|
846
|
+
if (!res.ok) throw new Error("读取生成结果失败:HTTP " + res.status);
|
|
847
|
+
const blob = await res.blob();
|
|
848
|
+
const r = await shrinkImage(blob);
|
|
849
|
+
update((prev) => ({
|
|
850
|
+
vRefs: prev.vRefs.concat([{
|
|
851
|
+
id: nextRefId(),
|
|
852
|
+
name: entry.filename,
|
|
853
|
+
src: r.src,
|
|
854
|
+
bytes: r.bytes,
|
|
855
|
+
from: "result",
|
|
856
|
+
}]),
|
|
857
|
+
vRefPickOpen: false,
|
|
858
|
+
}));
|
|
859
|
+
} catch (e) {
|
|
860
|
+
set({ vRefsError: e && e.message ? e.message : String(e) });
|
|
861
|
+
} finally {
|
|
862
|
+
set({ vRefsBusy: false });
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function removeVRef(id) {
|
|
868
|
+
if (state.busy) return;
|
|
869
|
+
update((prev) => ({ vRefs: prev.vRefs.filter((r) => r.id !== id), vRefsError: "" }));
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function clearVRefs() {
|
|
873
|
+
if (state.busy) return;
|
|
874
|
+
set({ vRefs: [], vRefsError: "", vRefPickOpen: false });
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function toggleVRefPick() {
|
|
878
|
+
if (state.busy) return;
|
|
879
|
+
set({ vRefPickOpen: !state.vRefPickOpen, vRefsError: "" });
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/* ---------- 视频关键帧(首帧 / 尾帧,本地图直接当帧) ---------- */
|
|
883
|
+
const frameField = (which) => (which === "last" ? "vLast" : "vFirst");
|
|
884
|
+
|
|
885
|
+
function pickFrame(which) {
|
|
886
|
+
if (state.busy || state.frameBusy) return;
|
|
887
|
+
set({ framePick: which, error: "" });
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
async function setFrameFromFile(which, file) {
|
|
891
|
+
set({ frameBusy: true, error: "" });
|
|
892
|
+
try {
|
|
893
|
+
// 和参考图走同一套下采样:长边 1280、PNG 优先,既省上行又不会糊
|
|
894
|
+
const r = await shrinkImage(file);
|
|
895
|
+
set({
|
|
896
|
+
[frameField(which)]: { src: r.src, name: file.name || "关键帧", bytes: r.bytes },
|
|
897
|
+
framePick: "",
|
|
898
|
+
});
|
|
899
|
+
} catch (e) {
|
|
900
|
+
set({ framePick: "", error: e && e.message ? e.message : String(e) });
|
|
901
|
+
} finally {
|
|
902
|
+
set({ frameBusy: false });
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function clearFrame(which) {
|
|
907
|
+
if (state.busy) return;
|
|
908
|
+
set({ [frameField(which)]: null, framePickFrom: "" });
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/* 把「生成结果」里的图直接当关键帧:和本地图完全同一条链路(blob → 下采样 → data URI) */
|
|
912
|
+
function toggleFramePick(which) {
|
|
913
|
+
if (state.busy || state.frameBusy) return;
|
|
914
|
+
set({ framePickFrom: state.framePickFrom === which ? "" : which, error: "" });
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
async function setFrameFromResult(which, entry) {
|
|
918
|
+
if (!entry || entry.kind !== "image" || !entry.filename) return;
|
|
919
|
+
if (state.busy || state.frameBusy) return;
|
|
920
|
+
set({ frameBusy: true, error: "" });
|
|
921
|
+
try {
|
|
922
|
+
const res = await fetch(srcOf(entry.filename));
|
|
923
|
+
if (!res.ok) throw new Error("读取生成结果失败:HTTP " + res.status);
|
|
924
|
+
const blob = await res.blob();
|
|
925
|
+
const r = await shrinkImage(blob);
|
|
926
|
+
set({
|
|
927
|
+
[frameField(which)]: { src: r.src, name: entry.filename, bytes: r.bytes },
|
|
928
|
+
framePickFrom: "",
|
|
929
|
+
});
|
|
930
|
+
} catch (e) {
|
|
931
|
+
set({ framePickFrom: "", error: e && e.message ? e.message : String(e) });
|
|
932
|
+
} finally {
|
|
933
|
+
set({ frameBusy: false });
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/* 结果区的一键设置:只在「结果本身是图片」时有意义 */
|
|
938
|
+
function useResultAsFrame(which) {
|
|
939
|
+
const cur = state.result;
|
|
940
|
+
if (cur === null || cur.kind !== "image") return;
|
|
941
|
+
return setFrameFromResult(which, cur);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/* ---------- 结果收尾动作:复制路径 / 访达显示 / 下载 ---------- */
|
|
945
|
+
async function copyPath() {
|
|
946
|
+
const cur = state.result;
|
|
947
|
+
if (cur === null) return;
|
|
948
|
+
const text = cur.path || "";
|
|
949
|
+
try {
|
|
950
|
+
await navigator.clipboard.writeText(text);
|
|
951
|
+
set({ notice: "路径已复制" });
|
|
952
|
+
} catch {
|
|
953
|
+
set({ notice: "复制失败,可手动复制:" + text });
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
async function reveal() {
|
|
958
|
+
const cur = state.result;
|
|
959
|
+
if (cur === null) return;
|
|
960
|
+
try {
|
|
961
|
+
const res = await fetch("/agnes/reveal", {
|
|
962
|
+
method: "POST",
|
|
963
|
+
headers: { "content-type": "application/json" },
|
|
964
|
+
body: JSON.stringify({ name: cur.filename }),
|
|
965
|
+
});
|
|
966
|
+
const data = await res.json().catch(() => ({}));
|
|
967
|
+
set({
|
|
968
|
+
notice: data && data.ok === true
|
|
969
|
+
? "已在访达中显示"
|
|
970
|
+
: "打开失败:" + ((data && data.error) || "HTTP " + res.status),
|
|
971
|
+
});
|
|
972
|
+
} catch (e) {
|
|
973
|
+
set({ notice: "打开失败:" + (e && e.message ? e.message : String(e)) });
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function downloadResult() {
|
|
978
|
+
const cur = state.result;
|
|
979
|
+
if (cur === null) return;
|
|
980
|
+
const a = document.createElement("a");
|
|
981
|
+
a.href = srcOf(cur.filename);
|
|
982
|
+
a.download = cur.filename;
|
|
983
|
+
document.body.appendChild(a);
|
|
984
|
+
a.click();
|
|
985
|
+
a.remove();
|
|
986
|
+
set({ notice: "已开始下载" });
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/** 停掉等待:让在途的轮询/请求结果失效(宿主的任务照常跑,文件会正常落盘) */
|
|
990
|
+
function cancel() {
|
|
991
|
+
if (!state.busy) return;
|
|
992
|
+
const hadTask = state.vTaskId !== null;
|
|
993
|
+
runToken += 1;
|
|
994
|
+
stopTimer();
|
|
995
|
+
set({
|
|
996
|
+
busy: false, vStatus: "", vProgress: 0,
|
|
997
|
+
notice: hadTask
|
|
998
|
+
? "已停止跟踪;宿主会继续推进,刷新面板还能接回进度,文件也会照常落盘"
|
|
999
|
+
: "已停止等待;服务端任务可能仍在跑",
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/* ---------- 输出目录:把磁盘上已经有的图/视频捞回来 ---------- */
|
|
1004
|
+
async function copyPrompt(text) {
|
|
1005
|
+
const t = typeof text === "string" ? text.trim() : "";
|
|
1006
|
+
if (t === "") return;
|
|
1007
|
+
try {
|
|
1008
|
+
await navigator.clipboard.writeText(t);
|
|
1009
|
+
set({ notice: "提示词已复制", copiedKey: t });
|
|
1010
|
+
} catch {
|
|
1011
|
+
set({ notice: "复制失败,可手动选中复制" });
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
async function loadLibrary() {
|
|
1016
|
+
if (state.libraryBusy) return;
|
|
1017
|
+
set({ libraryBusy: true, libraryError: "" });
|
|
1018
|
+
try {
|
|
1019
|
+
const res = await fetch("/agnes/library");
|
|
1020
|
+
const data = await res.json().catch(() => ({}));
|
|
1021
|
+
if (!res.ok || !data || data.ok !== true) {
|
|
1022
|
+
throw new Error((data && data.error) || "HTTP " + res.status);
|
|
1023
|
+
}
|
|
1024
|
+
const files = Array.isArray(data.files) ? data.files : [];
|
|
1025
|
+
set({
|
|
1026
|
+
library: files,
|
|
1027
|
+
libraryOpen: true,
|
|
1028
|
+
libraryBusy: false,
|
|
1029
|
+
libraryError: "",
|
|
1030
|
+
});
|
|
1031
|
+
const images = files.filter((f) => f.kind === "image").length;
|
|
1032
|
+
if (images === 0) {
|
|
1033
|
+
const videos = files.filter((f) => f.kind === "video").length;
|
|
1034
|
+
if (videos > 0) set({ libraryTab: "video" });
|
|
1035
|
+
}
|
|
1036
|
+
} catch (e) {
|
|
1037
|
+
set({
|
|
1038
|
+
libraryBusy: false,
|
|
1039
|
+
libraryOpen: true,
|
|
1040
|
+
libraryError: e && e.message ? e.message : String(e),
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* 静默刷新输出目录清单:生成完成后新文件已经落盘,列表要跟着更新。
|
|
1047
|
+
* 只在清单已经加载过时才拉(没展开过就别偷偷把面板撑开),失败保留旧清单。
|
|
1048
|
+
*/
|
|
1049
|
+
async function refreshLibrary() {
|
|
1050
|
+
if (state.library === null || state.libraryBusy) return;
|
|
1051
|
+
try {
|
|
1052
|
+
const res = await fetch("/agnes/library");
|
|
1053
|
+
const data = await res.json().catch(() => ({}));
|
|
1054
|
+
if (!res.ok || !data || data.ok !== true) return;
|
|
1055
|
+
if (!Array.isArray(data.files)) return;
|
|
1056
|
+
update(() => ({ library: data.files, libraryError: "" }));
|
|
1057
|
+
} catch {
|
|
1058
|
+
// 刷新失败保留旧清单,不打扰用户
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function toggleLibrary() {
|
|
1063
|
+
if (state.libraryOpen) {
|
|
1064
|
+
set({ libraryOpen: false });
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
// 没加载过就先拉一次;已经有清单则直接展开
|
|
1068
|
+
if (state.library === null) return loadLibrary();
|
|
1069
|
+
set({ libraryOpen: true });
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function setLibraryTab(tab) {
|
|
1073
|
+
set({ libraryTab: tab === "video" ? "video" : "image" });
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/** 点磁盘上的文件:直接放进结果区看,不经过生成流程 */
|
|
1077
|
+
function showLibraryFile(file) {
|
|
1078
|
+
if (!file || !file.name) return;
|
|
1079
|
+
if (state.busy) return;
|
|
1080
|
+
set({
|
|
1081
|
+
result: {
|
|
1082
|
+
kind: file.kind === "video" ? "video" : "image",
|
|
1083
|
+
filename: file.name,
|
|
1084
|
+
path: file.path || "",
|
|
1085
|
+
bytes: file.bytes || 0,
|
|
1086
|
+
prompt: typeof file.prompt === "string" ? file.prompt : "",
|
|
1087
|
+
fromDisk: true,
|
|
1088
|
+
},
|
|
1089
|
+
error: "",
|
|
1090
|
+
notice: "",
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/* ---------- 视频:交给宿主任务队列,面板只负责看进度 ---------- */
|
|
1095
|
+
// 提交后 video_id 由宿主保管:关面板、刷新页面、重启应用都不影响任务继续推进。
|
|
1096
|
+
async function watchTask(token, taskId, fallback) {
|
|
1097
|
+
const deadline = Date.now() + VIDEO_DEADLINE_MS;
|
|
538
1098
|
let transientFails = 0;
|
|
1099
|
+
|
|
539
1100
|
for (;;) {
|
|
540
1101
|
await sleep(POLL_MS);
|
|
541
1102
|
if (token !== runToken) return;
|
|
542
1103
|
|
|
543
1104
|
let st;
|
|
544
1105
|
try {
|
|
545
|
-
const r = await fetch("/agnes/
|
|
546
|
-
st = await r.json();
|
|
1106
|
+
const r = await fetch("/agnes/task?id=" + encodeURIComponent(taskId));
|
|
1107
|
+
st = r.ok ? await r.json() : { ok: false, error: "查询任务失败:HTTP " + r.status };
|
|
547
1108
|
} catch (e) {
|
|
548
1109
|
st = { ok: false, error: "网络错误:" + (e && e.message ? e.message : String(e)) };
|
|
549
1110
|
}
|
|
550
1111
|
|
|
551
1112
|
if (!st.ok) {
|
|
552
1113
|
const msg = String(st.error || "查询任务失败");
|
|
553
|
-
//
|
|
554
|
-
// 实测提交后立刻查询就会 429,这条路径是必需的。
|
|
1114
|
+
// 宿主自己会重试上游;这里只在本地接口抖动时容忍几次
|
|
555
1115
|
if (/\b429\b|\b503\b|频繁|队列已满|网络错误|timeout/i.test(msg)) {
|
|
556
1116
|
transientFails += 1;
|
|
557
1117
|
if (transientFails > 8) throw new Error(msg);
|
|
558
|
-
set({ vStatus: "
|
|
559
|
-
await sleep(Math.min(30000,
|
|
1118
|
+
set({ vStatus: "重试中…" });
|
|
1119
|
+
await sleep(Math.min(30000, 3000 * transientFails));
|
|
560
1120
|
continue;
|
|
561
1121
|
}
|
|
562
1122
|
throw new Error(msg);
|
|
@@ -565,30 +1125,126 @@ window.__ModuleLoader__.load({
|
|
|
565
1125
|
transientFails = 0;
|
|
566
1126
|
if (token !== runToken) return;
|
|
567
1127
|
|
|
1128
|
+
const task = st.task || {};
|
|
568
1129
|
set({
|
|
569
|
-
vStatus:
|
|
570
|
-
vProgress: typeof
|
|
1130
|
+
vStatus: task.status ? String(task.status) : "",
|
|
1131
|
+
vProgress: typeof task.progress === "number" ? task.progress : 0,
|
|
1132
|
+
videoId: task.videoId || fallback.videoId || null,
|
|
571
1133
|
});
|
|
572
1134
|
|
|
573
|
-
if (
|
|
1135
|
+
if (task.status === "completed") {
|
|
574
1136
|
pushResult({
|
|
575
1137
|
kind: "video",
|
|
576
|
-
filename:
|
|
577
|
-
path:
|
|
578
|
-
bytes:
|
|
1138
|
+
filename: task.filename,
|
|
1139
|
+
path: task.path,
|
|
1140
|
+
bytes: task.bytes,
|
|
579
1141
|
size: "720P",
|
|
580
|
-
ratio:
|
|
581
|
-
seconds:
|
|
582
|
-
transport: "
|
|
1142
|
+
ratio: task.ratio || fallback.ratio,
|
|
1143
|
+
seconds: task.seconds || fallback.seconds,
|
|
1144
|
+
transport: "宿主任务",
|
|
1145
|
+
mode: modeOf(task.mode || fallback.mode),
|
|
1146
|
+
seed: task.seed === null || task.seed === undefined ? undefined : task.seed,
|
|
1147
|
+
prompt: task.prompt || fallback.prompt,
|
|
583
1148
|
wallSec: ((Date.now() - state.startedAt) / 1000).toFixed(1),
|
|
584
1149
|
});
|
|
585
1150
|
return;
|
|
586
1151
|
}
|
|
587
|
-
if (
|
|
588
|
-
|
|
589
|
-
|
|
1152
|
+
if (task.status === "failed") throw new Error(task.error || "视频生成失败");
|
|
1153
|
+
if (task.status === "cancelled") throw new Error("任务已取消");
|
|
1154
|
+
if (Date.now() > deadline) {
|
|
1155
|
+
throw new Error("等待超时(15 分钟);宿主仍在推进,稍后刷新还能看到结果");
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
async function runVideo(token, prompt) {
|
|
1161
|
+
set({ vStatus: "提交中…", vProgress: 0, videoId: null, vTaskId: null });
|
|
1162
|
+
|
|
1163
|
+
const seedText = String(state.vSeed).trim();
|
|
1164
|
+
const seedValue = seedText === "" ? null : Number(seedText);
|
|
1165
|
+
if (seedValue !== null && !(Number.isInteger(seedValue) && seedValue >= 0)) {
|
|
1166
|
+
throw new Error("seed 需要非负整数(也可以留空)");
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// 关键帧直接发 data URI:上游已实测接受 base64,本地图片不用先传图床
|
|
1170
|
+
const subRes = await fetch("/agnes/tasks", {
|
|
1171
|
+
method: "POST",
|
|
1172
|
+
headers: { "content-type": "application/json" },
|
|
1173
|
+
body: JSON.stringify({
|
|
1174
|
+
prompt,
|
|
1175
|
+
seconds: state.vSeconds,
|
|
1176
|
+
ratio: state.vRatio,
|
|
1177
|
+
seed: seedValue,
|
|
1178
|
+
firstFrame: state.vFirst !== null ? state.vFirst.src : null,
|
|
1179
|
+
lastFrame: state.vLast !== null ? state.vLast.src : null,
|
|
1180
|
+
// 参考图(reference 模式):有首/尾帧时上游会用 keyframe,参考图自动让位
|
|
1181
|
+
referenceImages: state.vRefs.map((r) => r.src),
|
|
1182
|
+
}),
|
|
1183
|
+
});
|
|
1184
|
+
const sub = await subRes.json();
|
|
1185
|
+
if (!sub.ok) throw new Error(sub.error || "提交失败");
|
|
1186
|
+
if (token !== runToken) return;
|
|
1187
|
+
|
|
1188
|
+
const task = sub.task || {};
|
|
1189
|
+
if (!task.id) throw new Error("宿主没有返回任务号");
|
|
1190
|
+
set({ videoId: task.videoId || null, vTaskId: task.id, vStatus: "排队中…" });
|
|
1191
|
+
|
|
1192
|
+
await watchTask(token, task.id, {
|
|
1193
|
+
prompt,
|
|
1194
|
+
ratio: state.vRatio,
|
|
1195
|
+
seconds: state.vSeconds,
|
|
1196
|
+
mode: task.mode,
|
|
1197
|
+
videoId: task.videoId,
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/* 面板重新挂载时把宿主上还在跑的任务接回来:刷新页面不会让进度消失 */
|
|
1202
|
+
async function resumeTasks() {
|
|
1203
|
+
if (state.busy) return;
|
|
1204
|
+
try {
|
|
1205
|
+
const r = await fetch("/agnes/tasks");
|
|
1206
|
+
if (!r.ok) return;
|
|
1207
|
+
const data = await r.json();
|
|
1208
|
+
if (!data || data.ok !== true || !Array.isArray(data.tasks)) return;
|
|
1209
|
+
const active = data.tasks.find((t) => t.status === "queued" || t.status === "running");
|
|
1210
|
+
if (!active || state.busy) return;
|
|
1211
|
+
|
|
1212
|
+
const token = ++runToken;
|
|
1213
|
+
const created = Date.parse(active.createdAt);
|
|
1214
|
+
set({
|
|
1215
|
+
mode: "video",
|
|
1216
|
+
busy: true,
|
|
1217
|
+
error: "",
|
|
1218
|
+
notice: "",
|
|
1219
|
+
startedAt: Number.isFinite(created) ? created : Date.now(),
|
|
1220
|
+
elapsed: 0,
|
|
1221
|
+
vFirst: null,
|
|
1222
|
+
vLast: null,
|
|
1223
|
+
videoId: active.videoId || null,
|
|
1224
|
+
vTaskId: active.id,
|
|
1225
|
+
vStatus: active.status,
|
|
1226
|
+
vProgress: typeof active.progress === "number" ? active.progress : 0,
|
|
1227
|
+
prompt: state.prompt.trim() === "" ? String(active.prompt || "") : state.prompt,
|
|
1228
|
+
});
|
|
1229
|
+
startTimer();
|
|
1230
|
+
try {
|
|
1231
|
+
await watchTask(token, active.id, {
|
|
1232
|
+
prompt: active.prompt,
|
|
1233
|
+
ratio: active.ratio || state.vRatio,
|
|
1234
|
+
seconds: active.seconds || state.vSeconds,
|
|
1235
|
+
mode: active.mode,
|
|
1236
|
+
videoId: active.videoId,
|
|
1237
|
+
});
|
|
1238
|
+
} catch (e) {
|
|
1239
|
+
if (token === runToken) set({ error: e && e.message ? e.message : String(e) });
|
|
1240
|
+
} finally {
|
|
1241
|
+
if (token === runToken) {
|
|
1242
|
+
stopTimer();
|
|
1243
|
+
set({ busy: false, elapsed: (Date.now() - state.startedAt) / 1000 });
|
|
1244
|
+
}
|
|
590
1245
|
}
|
|
591
|
-
|
|
1246
|
+
} catch {
|
|
1247
|
+
// 旧宿主没有 /agnes/tasks:静默跳过,不打扰用户
|
|
592
1248
|
}
|
|
593
1249
|
}
|
|
594
1250
|
|
|
@@ -628,11 +1284,24 @@ window.__ModuleLoader__.load({
|
|
|
628
1284
|
set,
|
|
629
1285
|
loadHealth,
|
|
630
1286
|
loadConfig,
|
|
1287
|
+
loadHistory,
|
|
631
1288
|
saveConfig,
|
|
632
1289
|
setCfgDraft(field, value) {
|
|
633
1290
|
set({ cfgDraft: { ...state.cfgDraft, [field]: value }, cfgNotice: "" });
|
|
634
1291
|
},
|
|
1292
|
+
|
|
1293
|
+
/* 顶部标题栏直接换模型:先写进草稿,再立刻落盘(不然刷新就回退了) */
|
|
1294
|
+
async setModel(kind, value) {
|
|
1295
|
+
if (state.busy || state.cfgSaving) return;
|
|
1296
|
+
const field = kind === "video" ? "videoModel" : "imageModel";
|
|
1297
|
+
set({ cfgDraft: { ...state.cfgDraft, [field]: value }, cfgNotice: "", cfgError: "" });
|
|
1298
|
+
await saveConfig();
|
|
1299
|
+
if (state.cfgError === "") {
|
|
1300
|
+
set({ notice: "已切换模型:" + (value === "" ? "内置默认" : value) });
|
|
1301
|
+
}
|
|
1302
|
+
},
|
|
635
1303
|
toggleCfg() { set({ cfgOpen: !state.cfgOpen }); },
|
|
1304
|
+
checkKey,
|
|
636
1305
|
run,
|
|
637
1306
|
/* ---- 参考图动作 ---- */
|
|
638
1307
|
addRefsFromFiles,
|
|
@@ -640,16 +1309,45 @@ window.__ModuleLoader__.load({
|
|
|
640
1309
|
removeRef,
|
|
641
1310
|
clearRefs,
|
|
642
1311
|
toggleRefPick,
|
|
1312
|
+
/* ---- 视频参考图(reference 模式,≤5 张) ---- */
|
|
1313
|
+
addVRefsFromFiles,
|
|
1314
|
+
addVRefFromResult,
|
|
1315
|
+
removeVRef,
|
|
1316
|
+
clearVRefs,
|
|
1317
|
+
toggleVRefPick,
|
|
1318
|
+
/* ---- 视频关键帧 ---- */
|
|
1319
|
+
pickFrame,
|
|
1320
|
+
setFrameFromFile,
|
|
1321
|
+
clearFrame,
|
|
1322
|
+
toggleFramePick,
|
|
1323
|
+
setFrameFromResult,
|
|
1324
|
+
useResultAsFrame,
|
|
1325
|
+
/* ---- 视频任务(宿主侧推进,刷新可接回) ---- */
|
|
1326
|
+
resumeTasks,
|
|
1327
|
+
/* ---- 结果收尾 ---- */
|
|
1328
|
+
copyPath,
|
|
1329
|
+
reveal,
|
|
1330
|
+
downloadResult,
|
|
1331
|
+
cancel,
|
|
1332
|
+
copyPrompt,
|
|
1333
|
+
/* ---- 输出目录清单 ---- */
|
|
1334
|
+
loadLibrary,
|
|
1335
|
+
toggleLibrary,
|
|
1336
|
+
setLibraryTab,
|
|
1337
|
+
showLibraryFile,
|
|
643
1338
|
maxRefs: MAX_REFS,
|
|
1339
|
+
maxVRefs: MAX_VREFS,
|
|
644
1340
|
/** 切换主预览到历史里的某一张 */
|
|
645
1341
|
show(entry) { set({ result: entry }); },
|
|
646
1342
|
/**
|
|
647
|
-
*
|
|
648
|
-
*
|
|
1343
|
+
* 清空界面上的生成结果。删的是界面状态和宿主持久化的历史记录,
|
|
1344
|
+
* 不删磁盘文件、不动提示词和参数。进行中的请求不清。
|
|
649
1345
|
*/
|
|
650
1346
|
clear() {
|
|
651
1347
|
if (state.busy) return;
|
|
652
|
-
set({ result: null, history: [], error: "" });
|
|
1348
|
+
set({ result: null, history: [], error: "", notice: "" });
|
|
1349
|
+
// 宿主是历史的真源;这里的 DELETE 失败也不该打断界面
|
|
1350
|
+
void fetch("/agnes/history", { method: "DELETE" }).catch(() => {});
|
|
653
1351
|
},
|
|
654
1352
|
dispose() {
|
|
655
1353
|
stopTimer();
|
|
@@ -666,7 +1364,7 @@ window.__ModuleLoader__.load({
|
|
|
666
1364
|
}
|
|
667
1365
|
|
|
668
1366
|
/* ---------- 媒体显示:视频直接用 <video>,图片带加载兜底 ---------- */
|
|
669
|
-
function MediaBox({ entry, big }) {
|
|
1367
|
+
function MediaBox({ entry, big, onCopyPrompt }) {
|
|
670
1368
|
const isVideo = entry !== null && entry.kind === "video";
|
|
671
1369
|
const name = entry === null ? null : entry.filename;
|
|
672
1370
|
|
|
@@ -715,6 +1413,8 @@ window.__ModuleLoader__.load({
|
|
|
715
1413
|
React.createElement("span", { className: "agn-code" }, entry.path || name)));
|
|
716
1414
|
}
|
|
717
1415
|
|
|
1416
|
+
const promptText = typeof entry.prompt === "string" ? entry.prompt.trim() : "";
|
|
1417
|
+
|
|
718
1418
|
return React.createElement("div", { className: "agn-shot" + (big ? " agn-shot-big" : "") },
|
|
719
1419
|
isVideo
|
|
720
1420
|
? React.createElement("video", {
|
|
@@ -724,7 +1424,20 @@ window.__ModuleLoader__.load({
|
|
|
724
1424
|
preload: "metadata",
|
|
725
1425
|
onError: () => setFailed(true),
|
|
726
1426
|
})
|
|
727
|
-
: React.createElement("img", { src, alt: "生成结果", onError })
|
|
1427
|
+
: React.createElement("img", { src, alt: "生成结果", onError }),
|
|
1428
|
+
/* 鼠标移上去才浮出来的一层:显示当初的提示词,带一键复制 */
|
|
1429
|
+
promptText === ""
|
|
1430
|
+
? null
|
|
1431
|
+
: React.createElement("div", { className: "agn-prompt" },
|
|
1432
|
+
React.createElement("div", { className: "agn-prompt-text", title: promptText }, promptText),
|
|
1433
|
+
React.createElement("button", {
|
|
1434
|
+
type: "button", className: "agn-prompt-copy",
|
|
1435
|
+
title: "复制这段提示词",
|
|
1436
|
+
onClick: (e) => {
|
|
1437
|
+
e.stopPropagation();
|
|
1438
|
+
if (typeof onCopyPrompt === "function") onCopyPrompt(promptText);
|
|
1439
|
+
},
|
|
1440
|
+
}, "复制"))
|
|
728
1441
|
);
|
|
729
1442
|
}
|
|
730
1443
|
|
|
@@ -755,7 +1468,13 @@ window.__ModuleLoader__.load({
|
|
|
755
1468
|
return () => { off(); };
|
|
756
1469
|
}, []);
|
|
757
1470
|
|
|
758
|
-
React.useEffect(() => {
|
|
1471
|
+
React.useEffect(() => {
|
|
1472
|
+
store.loadHealth();
|
|
1473
|
+
store.loadConfig();
|
|
1474
|
+
store.loadHistory();
|
|
1475
|
+
// 上次没跑完的视频任务:宿主还在推进,接回进度
|
|
1476
|
+
store.resumeTasks();
|
|
1477
|
+
}, []);
|
|
759
1478
|
|
|
760
1479
|
const busy = s.busy;
|
|
761
1480
|
const isVideo = s.mode === "video";
|
|
@@ -764,8 +1483,16 @@ window.__ModuleLoader__.load({
|
|
|
764
1483
|
const shown = s.result;
|
|
765
1484
|
const hasRefs = s.refs.length > 0;
|
|
766
1485
|
const refsFull = s.refs.length >= MAX_REFS;
|
|
1486
|
+
const vRefsFull = s.vRefs.length >= MAX_VREFS;
|
|
767
1487
|
const imageHistory = s.history.filter((h) => h.kind === "image");
|
|
1488
|
+
const pickableImages = pickableImagesOf(s);
|
|
1489
|
+
const libraryAll = s.library || [];
|
|
1490
|
+
const libraryImages = libraryAll.filter((f) => f.kind !== "video");
|
|
1491
|
+
const libraryVideos = libraryAll.filter((f) => f.kind === "video");
|
|
1492
|
+
const libraryShown = s.libraryTab === "video" ? libraryVideos : libraryImages;
|
|
768
1493
|
const fileInput = React.useRef(null);
|
|
1494
|
+
const frameInput = React.useRef(null);
|
|
1495
|
+
const vRefInput = React.useRef(null);
|
|
769
1496
|
|
|
770
1497
|
const onKeyDown = (e) => {
|
|
771
1498
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); store.run(); }
|
|
@@ -797,6 +1524,13 @@ window.__ModuleLoader__.load({
|
|
|
797
1524
|
onChange: (e) => store.set({ vRatio: e.target.value }),
|
|
798
1525
|
}, VIDEO_RATIOS.map((x) => React.createElement("option", { key: x, value: x }, x))),
|
|
799
1526
|
React.createElement("span", { key: "m", className: "agn-meta" }, "720P"),
|
|
1527
|
+
React.createElement("span", { key: "l3", className: "agn-label" }, "种子"),
|
|
1528
|
+
React.createElement("input", {
|
|
1529
|
+
key: "s3", type: "text", inputMode: "numeric", className: "agn-seed",
|
|
1530
|
+
value: s.vSeed, disabled: busy, placeholder: "随机",
|
|
1531
|
+
title: "固定 seed 可让同一提示词出片更稳定;留空由服务端随机",
|
|
1532
|
+
onChange: (e) => store.set({ vSeed: e.target.value }),
|
|
1533
|
+
}),
|
|
800
1534
|
]
|
|
801
1535
|
: [
|
|
802
1536
|
React.createElement("span", { key: "l1", className: "agn-label" }, "尺寸"),
|
|
@@ -815,6 +1549,57 @@ window.__ModuleLoader__.load({
|
|
|
815
1549
|
? "生成结果(还没有)"
|
|
816
1550
|
: (shown.kind === "video" ? "生成结果 · 视频" : "生成结果 · 图片");
|
|
817
1551
|
|
|
1552
|
+
/* 视频缩略图:放一个真的 <video>,浏览器会解码出首帧当封面。
|
|
1553
|
+
#t=0.1 是媒体片段定位,避免第一帧偶尔是黑场;muted + playsInline
|
|
1554
|
+
保证不会被自动播放策略拦掉,也不会突然出声。 */
|
|
1555
|
+
const videoThumb = (name) => React.createElement("video", {
|
|
1556
|
+
src: srcOf(name) + "#t=0.1",
|
|
1557
|
+
preload: "metadata",
|
|
1558
|
+
muted: true,
|
|
1559
|
+
playsInline: true,
|
|
1560
|
+
tabIndex: -1,
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
/* 关键帧小格:有图显示缩略图 + ×,无图显示「+ 本地」;
|
|
1564
|
+
下方固定一行「+ 结果」,从已经生成过的图片里挑一张 */
|
|
1565
|
+
const frameSlot = (which, label, cur) => React.createElement("div", { className: "agn-frame" },
|
|
1566
|
+
React.createElement("div", { className: "agn-frame-cap" }, label),
|
|
1567
|
+
cur !== null
|
|
1568
|
+
? React.createElement("div", { className: "agn-frame-box" },
|
|
1569
|
+
React.createElement("img", { src: cur.src, alt: label }),
|
|
1570
|
+
React.createElement("button", {
|
|
1571
|
+
type: "button", className: "agn-ref-del", title: "移除" + label,
|
|
1572
|
+
disabled: busy,
|
|
1573
|
+
onClick: () => store.clearFrame(which),
|
|
1574
|
+
}, "×"))
|
|
1575
|
+
: React.createElement("button", {
|
|
1576
|
+
type: "button", className: "agn-frame-add",
|
|
1577
|
+
disabled: busy || s.frameBusy,
|
|
1578
|
+
title: "从本地磁盘选择" + label + "图片(PNG / JPEG / WebP / GIF)",
|
|
1579
|
+
onClick: () => {
|
|
1580
|
+
store.pickFrame(which);
|
|
1581
|
+
const el = frameInput.current;
|
|
1582
|
+
if (el !== null) el.click();
|
|
1583
|
+
},
|
|
1584
|
+
}, "+ 本地"),
|
|
1585
|
+
React.createElement("button", {
|
|
1586
|
+
type: "button", className: "agn-frame-from",
|
|
1587
|
+
disabled: busy || s.frameBusy || pickableImages.length === 0,
|
|
1588
|
+
title: pickableImages.length === 0
|
|
1589
|
+
? "还没有生成过图片,先出一张图再来选"
|
|
1590
|
+
: "从生成结果里挑一张当" + label,
|
|
1591
|
+
onClick: () => store.toggleFramePick(which),
|
|
1592
|
+
}, s.framePickFrom === which ? "收起" : "+ 结果"),
|
|
1593
|
+
s.framePickFrom === which
|
|
1594
|
+
? React.createElement("div", { className: "agn-ref-pick agn-frame-pick" },
|
|
1595
|
+
pickableImages.map((h) => React.createElement("button", {
|
|
1596
|
+
key: h.filename, type: "button", title: h.filename,
|
|
1597
|
+
disabled: busy || s.frameBusy,
|
|
1598
|
+
onClick: () => store.setFrameFromResult(which, h),
|
|
1599
|
+
}, React.createElement("img", { src: srcOf(h.filename), alt: h.filename }))))
|
|
1600
|
+
: null
|
|
1601
|
+
);
|
|
1602
|
+
|
|
818
1603
|
return React.createElement("div", { className: "agn-root" },
|
|
819
1604
|
/* 隐藏的文件选择器:由「+ 本地图片」按钮触发,支持多选 */
|
|
820
1605
|
React.createElement("input", {
|
|
@@ -835,12 +1620,65 @@ window.__ModuleLoader__.load({
|
|
|
835
1620
|
},
|
|
836
1621
|
}),
|
|
837
1622
|
|
|
1623
|
+
/* 隐藏的关键帧选择器:首帧 / 尾帧共用,靠 store.framePick 区分目标 */
|
|
1624
|
+
React.createElement("input", {
|
|
1625
|
+
ref: frameInput,
|
|
1626
|
+
type: "file",
|
|
1627
|
+
accept: "image/png,image/jpeg,image/webp,image/gif",
|
|
1628
|
+
style: { display: "none" },
|
|
1629
|
+
onChange: (e) => {
|
|
1630
|
+
const el = e.target;
|
|
1631
|
+
const file = el.files && el.files[0];
|
|
1632
|
+
const which = s.framePick === "last" ? "last" : "first";
|
|
1633
|
+
Promise.resolve(store.setFrameFromFile(which, file))
|
|
1634
|
+
.catch(() => {})
|
|
1635
|
+
.finally(() => { try { el.value = ""; } catch { /* input 可能已卸载 */ } });
|
|
1636
|
+
},
|
|
1637
|
+
}),
|
|
1638
|
+
|
|
1639
|
+
/* 隐藏的视频参考图选择器(reference 模式,最多 5 张) */
|
|
1640
|
+
React.createElement("input", {
|
|
1641
|
+
ref: vRefInput,
|
|
1642
|
+
type: "file",
|
|
1643
|
+
accept: "image/png,image/jpeg,image/webp,image/gif",
|
|
1644
|
+
multiple: true,
|
|
1645
|
+
style: { display: "none" },
|
|
1646
|
+
onChange: (e) => {
|
|
1647
|
+
const el = e.target;
|
|
1648
|
+
const files = el.files;
|
|
1649
|
+
Promise.resolve(store.addVRefsFromFiles(files))
|
|
1650
|
+
.catch(() => {})
|
|
1651
|
+
.finally(() => { try { el.value = ""; } catch { /* input 可能已卸载 */ } });
|
|
1652
|
+
},
|
|
1653
|
+
}),
|
|
1654
|
+
|
|
838
1655
|
React.createElement("div", { className: "agn-head" },
|
|
839
1656
|
React.createElement("span", { className: "agn-title" }, "Agnes 创作"),
|
|
840
|
-
|
|
841
|
-
isVideo
|
|
842
|
-
|
|
843
|
-
|
|
1657
|
+
(() => {
|
|
1658
|
+
const kind = isVideo ? "video" : "image";
|
|
1659
|
+
const cur = isVideo
|
|
1660
|
+
? (s.cfgDraft.videoModel || (s.health && s.health.videoModel) || "agnes-video-2.5-flash")
|
|
1661
|
+
: (s.cfgDraft.imageModel || (s.health && s.health.model) || "agnes-image-2.5-flash");
|
|
1662
|
+
/* 选项优先用连接测试拿到的上游真实清单,没有就退回内置清单 */
|
|
1663
|
+
const fromUp = s.upModels && Array.isArray(s.upModels[kind]) ? s.upModels[kind] : null;
|
|
1664
|
+
const fromHealth = s.health && s.health.models && Array.isArray(s.health.models[kind])
|
|
1665
|
+
? s.health.models[kind]
|
|
1666
|
+
: [];
|
|
1667
|
+
let list = fromUp !== null && fromUp.length > 0 ? fromUp : fromHealth;
|
|
1668
|
+
if (list.length < 2) {
|
|
1669
|
+
/* 只有一个模型时给下拉没有意义,直接显示名字 */
|
|
1670
|
+
return React.createElement("span", { className: "agn-meta" }, cur);
|
|
1671
|
+
}
|
|
1672
|
+
list = list.slice();
|
|
1673
|
+
if (list.indexOf(cur) === -1) list.unshift(cur);
|
|
1674
|
+
return React.createElement("select", {
|
|
1675
|
+
className: "agn-model",
|
|
1676
|
+
value: cur,
|
|
1677
|
+
disabled: busy || s.cfgSaving,
|
|
1678
|
+
title: "切换" + (isVideo ? "视频" : "图片") + "模型(立刻保存到配置)",
|
|
1679
|
+
onChange: (e) => store.setModel(kind, e.target.value),
|
|
1680
|
+
}, list.map((m) => React.createElement("option", { key: m, value: m }, m)));
|
|
1681
|
+
})()
|
|
844
1682
|
),
|
|
845
1683
|
|
|
846
1684
|
React.createElement("div", { className: "agn-card" },
|
|
@@ -849,6 +1687,22 @@ window.__ModuleLoader__.load({
|
|
|
849
1687
|
modeBtn("video", "视频")
|
|
850
1688
|
),
|
|
851
1689
|
|
|
1690
|
+
/* ---- 视频关键帧(图生视频):本地图片直接当首帧/尾帧,留空即纯文生视频 ---- */
|
|
1691
|
+
isVideo
|
|
1692
|
+
? React.createElement("div", { className: "agn-frames" },
|
|
1693
|
+
React.createElement("div", { className: "agn-ref-head" },
|
|
1694
|
+
React.createElement("span", { className: "agn-label" }, "关键帧"),
|
|
1695
|
+
React.createElement("span", { className: "agn-hint" },
|
|
1696
|
+
s.vFirst !== null || s.vLast !== null ? "图生视频" : "留空 = 纯文生视频"),
|
|
1697
|
+
s.frameBusy ? React.createElement("span", { className: "agn-hint" }, "处理中…") : null
|
|
1698
|
+
),
|
|
1699
|
+
React.createElement("div", { className: "agn-frames-row" },
|
|
1700
|
+
frameSlot("first", "首帧", s.vFirst),
|
|
1701
|
+
frameSlot("last", "尾帧", s.vLast)
|
|
1702
|
+
)
|
|
1703
|
+
)
|
|
1704
|
+
: null,
|
|
1705
|
+
|
|
852
1706
|
/* ---- 参考图(图生图 / 多图合成):本地文件 + 生成结果两种来源 ---- */
|
|
853
1707
|
!isVideo
|
|
854
1708
|
? React.createElement("div", { className: "agn-refs" },
|
|
@@ -890,8 +1744,8 @@ window.__ModuleLoader__.load({
|
|
|
890
1744
|
}, "+ 本地图片"),
|
|
891
1745
|
React.createElement("button", {
|
|
892
1746
|
type: "button", className: "agn-ref-add",
|
|
893
|
-
disabled: busy || refsFull ||
|
|
894
|
-
title:
|
|
1747
|
+
disabled: busy || refsFull || pickableImages.length === 0,
|
|
1748
|
+
title: pickableImages.length === 0 ? "还没有生成过图片" : "从「生成结果」里挑一张",
|
|
895
1749
|
onClick: () => store.toggleRefPick(),
|
|
896
1750
|
}, "+ 生成结果")
|
|
897
1751
|
),
|
|
@@ -899,9 +1753,9 @@ window.__ModuleLoader__.load({
|
|
|
899
1753
|
/* 从生成结果挑图:只列图片(视频不能当参考图) */
|
|
900
1754
|
s.refPickOpen
|
|
901
1755
|
? React.createElement("div", { className: "agn-ref-pick" },
|
|
902
|
-
|
|
1756
|
+
pickableImages.length === 0
|
|
903
1757
|
? React.createElement("span", { className: "agn-hint" }, "还没有生成过图片")
|
|
904
|
-
:
|
|
1758
|
+
: pickableImages.map((h) => React.createElement("button", {
|
|
905
1759
|
key: "pick-" + h.filename,
|
|
906
1760
|
type: "button",
|
|
907
1761
|
title: h.filename,
|
|
@@ -921,7 +1775,91 @@ window.__ModuleLoader__.load({
|
|
|
921
1775
|
)
|
|
922
1776
|
: null,
|
|
923
1777
|
|
|
1778
|
+
/* ---- 视频参考图(reference 模式,≤5 张):给风格 / 主体参考,不是首帧 ---- */
|
|
1779
|
+
isVideo
|
|
1780
|
+
? React.createElement("div", { className: "agn-refs" },
|
|
1781
|
+
React.createElement("div", { className: "agn-ref-head" },
|
|
1782
|
+
React.createElement("span", { className: "agn-label" }, "参考图"),
|
|
1783
|
+
React.createElement("span", { className: "agn-tag" },
|
|
1784
|
+
s.vRefs.length + " / " + MAX_VREFS),
|
|
1785
|
+
s.vRefsBusy ? React.createElement("span", { className: "agn-hint" }, "处理中…") : null,
|
|
1786
|
+
s.vRefs.length > 0
|
|
1787
|
+
? React.createElement("button", {
|
|
1788
|
+
type: "button", className: "agn-btn-ghost", disabled: busy,
|
|
1789
|
+
onClick: () => store.clearVRefs(),
|
|
1790
|
+
}, "清空参考图")
|
|
1791
|
+
: null
|
|
1792
|
+
),
|
|
1793
|
+
React.createElement("div", { className: "agn-ref-strip" },
|
|
1794
|
+
s.vRefs.map((r) => React.createElement("div", {
|
|
1795
|
+
key: r.id,
|
|
1796
|
+
className: "agn-ref",
|
|
1797
|
+
title: r.name + " · " + (r.bytes / 1024).toFixed(0) + " KB · " +
|
|
1798
|
+
(r.from === "result" ? "来自生成结果" : "来自本地"),
|
|
1799
|
+
},
|
|
1800
|
+
React.createElement("img", { src: r.src, alt: r.name }),
|
|
1801
|
+
React.createElement("button", {
|
|
1802
|
+
type: "button", className: "agn-ref-del",
|
|
1803
|
+
title: "移除这张参考图",
|
|
1804
|
+
disabled: busy,
|
|
1805
|
+
onClick: () => store.removeVRef(r.id),
|
|
1806
|
+
}, "×")
|
|
1807
|
+
)),
|
|
1808
|
+
React.createElement("button", {
|
|
1809
|
+
type: "button", className: "agn-ref-add",
|
|
1810
|
+
disabled: busy || vRefsFull,
|
|
1811
|
+
title: "从本地磁盘选择一个或多个图片(PNG / JPEG / WebP / GIF)",
|
|
1812
|
+
onClick: () => {
|
|
1813
|
+
const el = vRefInput.current;
|
|
1814
|
+
if (el !== null) el.click();
|
|
1815
|
+
},
|
|
1816
|
+
}, "+ 本地"),
|
|
1817
|
+
React.createElement("button", {
|
|
1818
|
+
type: "button", className: "agn-ref-add",
|
|
1819
|
+
disabled: busy || vRefsFull || pickableImages.length === 0,
|
|
1820
|
+
title: pickableImages.length === 0 ? "还没有生成过图片" : "从「生成结果」里挑一张",
|
|
1821
|
+
onClick: () => store.toggleVRefPick(),
|
|
1822
|
+
}, "+ 结果")
|
|
1823
|
+
),
|
|
1824
|
+
s.vRefPickOpen
|
|
1825
|
+
? React.createElement("div", { className: "agn-ref-pick" },
|
|
1826
|
+
pickableImages.length === 0
|
|
1827
|
+
? React.createElement("span", { className: "agn-hint" }, "还没有生成过图片")
|
|
1828
|
+
: pickableImages.map((h) => React.createElement("button", {
|
|
1829
|
+
key: "vpick-" + h.filename,
|
|
1830
|
+
type: "button",
|
|
1831
|
+
title: h.filename,
|
|
1832
|
+
onClick: () => store.addVRefFromResult(h),
|
|
1833
|
+
}, React.createElement("img", {
|
|
1834
|
+
src: srcOf(h.filename), alt: h.filename, loading: "lazy",
|
|
1835
|
+
})))
|
|
1836
|
+
)
|
|
1837
|
+
: null,
|
|
1838
|
+
React.createElement("div", { className: "agn-note" },
|
|
1839
|
+
s.vRefs.length > 0
|
|
1840
|
+
? (s.vFirst !== null || s.vLast !== null
|
|
1841
|
+
? "已给关键帧,本次按首尾帧生成,参考图不生效"
|
|
1842
|
+
: "将按参考图生成(" + s.vRefs.length + " 张)——参考风格 / 主体 / 构图")
|
|
1843
|
+
: "可选:给上游 1–5 张参考图;留空 = 纯文本生视频"),
|
|
1844
|
+
s.vRefsError !== "" ? React.createElement("div", { className: "agn-err" }, s.vRefsError) : null
|
|
1845
|
+
)
|
|
1846
|
+
: null,
|
|
1847
|
+
|
|
924
1848
|
React.createElement("div", { className: "agn-label" }, "提示词"),
|
|
1849
|
+
React.createElement("div", { className: "agn-presets" },
|
|
1850
|
+
PROMPT_PRESETS.map((preset) => React.createElement("button", {
|
|
1851
|
+
key: preset.label,
|
|
1852
|
+
type: "button",
|
|
1853
|
+
className: "agn-preset",
|
|
1854
|
+
disabled: busy,
|
|
1855
|
+
title: "追加到提示词末尾:" + preset.text,
|
|
1856
|
+
onClick: () => store.set({
|
|
1857
|
+
prompt: s.prompt.trim() === ""
|
|
1858
|
+
? preset.text
|
|
1859
|
+
: s.prompt.replace(/[\s,,;;。.]+$/, "") + "," + preset.text,
|
|
1860
|
+
}),
|
|
1861
|
+
}, preset.label))
|
|
1862
|
+
),
|
|
925
1863
|
React.createElement("textarea", {
|
|
926
1864
|
className: "agn-textarea",
|
|
927
1865
|
value: s.prompt,
|
|
@@ -938,7 +1876,17 @@ window.__ModuleLoader__.load({
|
|
|
938
1876
|
className: "agn-btn", type: "button",
|
|
939
1877
|
disabled: busy || s.refsBusy || s.prompt.trim() === "" || keyBad,
|
|
940
1878
|
onClick: () => store.run(),
|
|
941
|
-
}, buttonLabel)
|
|
1879
|
+
}, buttonLabel),
|
|
1880
|
+
/* 视频是异步任务,等待可能很长:给出真正的「停止等待」而不是让用户干等 */
|
|
1881
|
+
busy
|
|
1882
|
+
? React.createElement("button", {
|
|
1883
|
+
className: "agn-btn-ghost", type: "button",
|
|
1884
|
+
title: isVideo
|
|
1885
|
+
? "停止等待(服务端任务可能仍在跑,已生成的文件不会丢)"
|
|
1886
|
+
: "放弃这次等待(请求已发出,结果可能仍会落盘)",
|
|
1887
|
+
onClick: () => store.cancel(),
|
|
1888
|
+
}, "取消")
|
|
1889
|
+
: null
|
|
942
1890
|
),
|
|
943
1891
|
|
|
944
1892
|
/* 视频是异步任务:给出阶段与百分比,避免只看到一个跳动的秒数 */
|
|
@@ -960,6 +1908,7 @@ window.__ModuleLoader__.load({
|
|
|
960
1908
|
"未找到 Key:" + (key.file || "") + " —— 把 Key 写进该文件后可立即使用")
|
|
961
1909
|
: null,
|
|
962
1910
|
s.error !== "" ? React.createElement("div", { className: "agn-err" }, s.error) : null,
|
|
1911
|
+
s.notice !== "" ? React.createElement("div", { className: "agn-notice" }, s.notice) : null,
|
|
963
1912
|
busy && !isVideo
|
|
964
1913
|
? React.createElement("div", { className: "agn-hint" },
|
|
965
1914
|
"正在生成:可切到别的面板,回来仍会接着显示结果(请求不会中断)")
|
|
@@ -970,6 +1919,12 @@ window.__ModuleLoader__.load({
|
|
|
970
1919
|
React.createElement("div", { className: "agn-card" },
|
|
971
1920
|
React.createElement("div", { className: "agn-card-head" },
|
|
972
1921
|
React.createElement("div", { className: "agn-label" }, resultLabel),
|
|
1922
|
+
React.createElement("button", {
|
|
1923
|
+
type: "button",
|
|
1924
|
+
className: "agn-btn-ghost",
|
|
1925
|
+
title: "把输出目录里已有的图片 / 视频列出来,点一下就能显示在上方",
|
|
1926
|
+
onClick: () => store.toggleLibrary(),
|
|
1927
|
+
}, s.libraryOpen ? "收起目录" : "输出目录"),
|
|
973
1928
|
React.createElement("button", {
|
|
974
1929
|
type: "button",
|
|
975
1930
|
className: "agn-btn-ghost",
|
|
@@ -980,7 +1935,7 @@ window.__ModuleLoader__.load({
|
|
|
980
1935
|
),
|
|
981
1936
|
|
|
982
1937
|
shown !== null
|
|
983
|
-
? React.createElement(MediaBox, { entry: shown, big: true })
|
|
1938
|
+
? React.createElement(MediaBox, { entry: shown, big: true, onCopyPrompt: (t) => store.copyPrompt(t) })
|
|
984
1939
|
: React.createElement("div", { className: "agn-shot" },
|
|
985
1940
|
React.createElement("div", { className: "agn-shot-msg" },
|
|
986
1941
|
busy ? (isVideo ? "正在生成视频…" : "正在生成图片…")
|
|
@@ -989,19 +1944,117 @@ window.__ModuleLoader__.load({
|
|
|
989
1944
|
shown !== null
|
|
990
1945
|
? React.createElement("div", { className: "agn-hint" },
|
|
991
1946
|
React.createElement("span", null,
|
|
992
|
-
shown.
|
|
1947
|
+
(shown.fromDisk ? "来自输出目录 · " : "")
|
|
1948
|
+
+ shown.filename + " · " + (shown.bytes / 1024 / 1024 >= 1
|
|
993
1949
|
? (shown.bytes / 1024 / 1024).toFixed(2) + " MB"
|
|
994
1950
|
: (shown.bytes / 1024).toFixed(0) + " KB")
|
|
995
1951
|
+ " · " + shown.size + "/" + shown.ratio
|
|
996
1952
|
+ (shown.seconds ? " · " + shown.seconds + " 秒" : "")
|
|
1953
|
+
+ (shown.mode === "keyframe" ? " · 图生视频" : "")
|
|
1954
|
+
+ (shown.mode === "reference" ? " · 参考生视频" : "")
|
|
997
1955
|
+ (shown.mode === "img2img" ? " · 图生图 " + (shown.refCount || 0) + " 张参考" : "")
|
|
998
|
-
+ " ·
|
|
1956
|
+
+ (shown.seed !== undefined && shown.seed !== null ? " · seed " + shown.seed : "")
|
|
1957
|
+
+ " · 用时 " + (shown.elapsedSec || shown.wallSec) + "s"),
|
|
999
1958
|
React.createElement("br"),
|
|
1000
1959
|
React.createElement("span", null, "已保存到 "),
|
|
1001
1960
|
React.createElement("span", { className: "agn-code" }, shown.path || ""))
|
|
1961
|
+
: null,
|
|
1962
|
+
|
|
1963
|
+
shown !== null
|
|
1964
|
+
? React.createElement("div", { className: "agn-acts" },
|
|
1965
|
+
React.createElement("button", {
|
|
1966
|
+
type: "button", className: "agn-act",
|
|
1967
|
+
title: "把文件路径复制到剪贴板",
|
|
1968
|
+
onClick: () => store.copyPath(),
|
|
1969
|
+
}, "复制路径"),
|
|
1970
|
+
React.createElement("button", {
|
|
1971
|
+
type: "button", className: "agn-act",
|
|
1972
|
+
title: "在访达中显示这个文件",
|
|
1973
|
+
onClick: () => store.reveal(),
|
|
1974
|
+
}, "在访达中显示"),
|
|
1975
|
+
React.createElement("button", {
|
|
1976
|
+
type: "button", className: "agn-act",
|
|
1977
|
+
title: "另存一份到「下载」目录",
|
|
1978
|
+
onClick: () => store.downloadResult(),
|
|
1979
|
+
}, "下载"),
|
|
1980
|
+
shown.kind === "image"
|
|
1981
|
+
? React.createElement("button", {
|
|
1982
|
+
type: "button", className: "agn-act",
|
|
1983
|
+
disabled: busy || s.frameBusy,
|
|
1984
|
+
title: "把这张图设为视频首帧(切到视频模式即可看到)",
|
|
1985
|
+
onClick: () => store.useResultAsFrame("first"),
|
|
1986
|
+
}, "设为首帧")
|
|
1987
|
+
: null,
|
|
1988
|
+
shown.kind === "image"
|
|
1989
|
+
? React.createElement("button", {
|
|
1990
|
+
type: "button", className: "agn-act",
|
|
1991
|
+
disabled: busy || s.frameBusy,
|
|
1992
|
+
title: "把这张图设为视频尾帧(切到视频模式即可看到)",
|
|
1993
|
+
onClick: () => store.useResultAsFrame("last"),
|
|
1994
|
+
}, "设为尾帧")
|
|
1995
|
+
: null)
|
|
1002
1996
|
: null
|
|
1003
1997
|
),
|
|
1004
1998
|
|
|
1999
|
+
/* ---- 输出目录:把磁盘上已有的图 / 视频捞回来看 ---- */
|
|
2000
|
+
s.libraryOpen
|
|
2001
|
+
? React.createElement("div", { className: "agn-card" },
|
|
2002
|
+
React.createElement("div", { className: "agn-card-head" },
|
|
2003
|
+
React.createElement("div", { className: "agn-label" }, "输出目录"),
|
|
2004
|
+
React.createElement("button", {
|
|
2005
|
+
type: "button", className: "agn-btn-ghost",
|
|
2006
|
+
disabled: s.libraryBusy,
|
|
2007
|
+
title: "重新读一遍输出目录(新生成的文件会自动出现在这里)",
|
|
2008
|
+
onClick: () => store.loadLibrary(),
|
|
2009
|
+
}, s.libraryBusy ? "读取中…" : "刷新")
|
|
2010
|
+
),
|
|
2011
|
+
React.createElement("div", { className: "agn-hint" },
|
|
2012
|
+
"点缩略图会把它显示到上方结果区,并带上当初写进图片里的提示词"),
|
|
2013
|
+
React.createElement("div", { className: "agn-lib" },
|
|
2014
|
+
React.createElement("div", { className: "agn-lib-tabs" },
|
|
2015
|
+
React.createElement("button", {
|
|
2016
|
+
type: "button",
|
|
2017
|
+
className: "agn-act" + (s.libraryTab === "image" ? " agn-act-on" : ""),
|
|
2018
|
+
onClick: () => store.setLibraryTab("image"),
|
|
2019
|
+
}, "图片 " + libraryImages.length),
|
|
2020
|
+
React.createElement("button", {
|
|
2021
|
+
type: "button",
|
|
2022
|
+
className: "agn-act" + (s.libraryTab === "video" ? " agn-act-on" : ""),
|
|
2023
|
+
onClick: () => store.setLibraryTab("video"),
|
|
2024
|
+
}, "视频 " + libraryVideos.length)
|
|
2025
|
+
),
|
|
2026
|
+
s.libraryError !== ""
|
|
2027
|
+
? React.createElement("div", { className: "agn-err" }, s.libraryError)
|
|
2028
|
+
: null,
|
|
2029
|
+
libraryShown.length === 0
|
|
2030
|
+
? React.createElement("div", { className: "agn-lib-empty" },
|
|
2031
|
+
s.libraryBusy
|
|
2032
|
+
? "正在读取输出目录…"
|
|
2033
|
+
: "这个目录里还没有" + (s.libraryTab === "video" ? "视频" : "图片"))
|
|
2034
|
+
: React.createElement("div", { className: "agn-lib-grid" },
|
|
2035
|
+
libraryShown.map((f) => React.createElement("button", {
|
|
2036
|
+
key: f.name,
|
|
2037
|
+
type: "button",
|
|
2038
|
+
className: "agn-lib-item" + (shown && shown.filename === f.name ? " agn-thumb-on" : ""),
|
|
2039
|
+
title: (f.prompt ? f.prompt + "\n" : "") + f.name,
|
|
2040
|
+
disabled: busy,
|
|
2041
|
+
onClick: () => store.showLibraryFile(f),
|
|
2042
|
+
},
|
|
2043
|
+
f.kind === "video"
|
|
2044
|
+
? videoThumb(f.name)
|
|
2045
|
+
: React.createElement("img", { src: srcOf(f.name), alt: f.name, loading: "lazy" }),
|
|
2046
|
+
f.kind === "video"
|
|
2047
|
+
? React.createElement("div", { className: "agn-thumb-play" }, "▶")
|
|
2048
|
+
: null,
|
|
2049
|
+
f.kind === "video"
|
|
2050
|
+
? React.createElement("span", { className: "agn-lib-badge" }, "视频")
|
|
2051
|
+
: null
|
|
2052
|
+
))
|
|
2053
|
+
)
|
|
2054
|
+
)
|
|
2055
|
+
)
|
|
2056
|
+
: null,
|
|
2057
|
+
|
|
1005
2058
|
/* ---- 历史 ---- */
|
|
1006
2059
|
s.history.length > 1
|
|
1007
2060
|
? React.createElement("div", { className: "agn-card agn-hist" },
|
|
@@ -1015,8 +2068,11 @@ window.__ModuleLoader__.load({
|
|
|
1015
2068
|
onClick: () => store.show(h),
|
|
1016
2069
|
},
|
|
1017
2070
|
h.kind === "video"
|
|
1018
|
-
?
|
|
2071
|
+
? videoThumb(h.filename)
|
|
1019
2072
|
: React.createElement("img", { src: srcOf(h.filename), alt: h.filename, loading: "lazy" }),
|
|
2073
|
+
h.kind === "video"
|
|
2074
|
+
? React.createElement("div", { className: "agn-thumb-play" }, "▶")
|
|
2075
|
+
: null,
|
|
1020
2076
|
React.createElement("span", { className: "agn-thumb-cap" },
|
|
1021
2077
|
h.kind === "video"
|
|
1022
2078
|
? (h.seconds ? h.seconds + "s" : "视频")
|
|
@@ -1068,6 +2124,52 @@ window.__ModuleLoader__.load({
|
|
|
1068
2124
|
)
|
|
1069
2125
|
: null,
|
|
1070
2126
|
|
|
2127
|
+
s.cfgOpen
|
|
2128
|
+
? React.createElement("div", { className: "agn-field" },
|
|
2129
|
+
React.createElement("div", { className: "agn-label" }, "图片模型"),
|
|
2130
|
+
React.createElement("select", {
|
|
2131
|
+
className: "agn-input", value: s.cfgDraft.imageModel, disabled: s.cfgSaving,
|
|
2132
|
+
onChange: (e) => store.setCfgDraft("imageModel", e.target.value),
|
|
2133
|
+
},
|
|
2134
|
+
React.createElement("option", { value: "" }, "默认(" + ((s.health && s.health.model) || "agnes-image-2.5-flash") + ")"),
|
|
2135
|
+
((s.health && s.health.models && s.health.models.image) || []).map((m) =>
|
|
2136
|
+
React.createElement("option", { key: m, value: m }, m))
|
|
2137
|
+
),
|
|
2138
|
+
React.createElement("div", { className: "agn-note" }, "只列出当前上游真实存在的图片模型")
|
|
2139
|
+
)
|
|
2140
|
+
: null,
|
|
2141
|
+
|
|
2142
|
+
s.cfgOpen
|
|
2143
|
+
? React.createElement("div", { className: "agn-field" },
|
|
2144
|
+
React.createElement("div", { className: "agn-label" }, "视频模型"),
|
|
2145
|
+
React.createElement("select", {
|
|
2146
|
+
className: "agn-input", value: s.cfgDraft.videoModel, disabled: s.cfgSaving,
|
|
2147
|
+
onChange: (e) => store.setCfgDraft("videoModel", e.target.value),
|
|
2148
|
+
},
|
|
2149
|
+
React.createElement("option", { value: "" }, "默认(" + ((s.health && s.health.videoModel) || "agnes-video-2.5-flash") + ")"),
|
|
2150
|
+
((s.health && s.health.models && s.health.models.video) || []).map((m) =>
|
|
2151
|
+
React.createElement("option", { key: m, value: m }, m))
|
|
2152
|
+
),
|
|
2153
|
+
React.createElement("div", { className: "agn-note" }, "换模型后新提交的任务才会用新模型")
|
|
2154
|
+
)
|
|
2155
|
+
: null,
|
|
2156
|
+
|
|
2157
|
+
s.cfgOpen
|
|
2158
|
+
? React.createElement("div", { className: "agn-field" },
|
|
2159
|
+
React.createElement("button", {
|
|
2160
|
+
type: "button", className: "agn-btn-ghost", disabled: s.checking,
|
|
2161
|
+
title: "用当前 Key 请求一次上游模型清单,确认 Key 能用、网络通",
|
|
2162
|
+
onClick: () => store.checkKey(),
|
|
2163
|
+
}, s.checking ? "连接中…" : "测试连接"),
|
|
2164
|
+
s.checkResult !== null
|
|
2165
|
+
? React.createElement("div", {
|
|
2166
|
+
// 别给 style 传字符串:React 会抛错并卸载整块面板(白屏)
|
|
2167
|
+
className: (s.checkResult.ok ? "agn-okline" : "agn-err") + " agn-check",
|
|
2168
|
+
}, s.checkResult.text)
|
|
2169
|
+
: null
|
|
2170
|
+
)
|
|
2171
|
+
: null,
|
|
2172
|
+
|
|
1071
2173
|
s.cfgOpen
|
|
1072
2174
|
? React.createElement("div", { className: "agn-row" },
|
|
1073
2175
|
React.createElement("button", {
|
|
@@ -1091,7 +2193,7 @@ window.__ModuleLoader__.load({
|
|
|
1091
2193
|
s.cfgOpen
|
|
1092
2194
|
? React.createElement("div", { className: "agn-note" },
|
|
1093
2195
|
"保存到 ",
|
|
1094
|
-
(s.health && s.health.configFile) || "~/.dsh/agnes-
|
|
2196
|
+
(s.health && s.health.configFile) || "~/.dsh/agnes-create/config.json")
|
|
1095
2197
|
: null
|
|
1096
2198
|
),
|
|
1097
2199
|
|
|
@@ -1130,7 +2232,7 @@ window.__ModuleLoader__.load({
|
|
|
1130
2232
|
|
|
1131
2233
|
exports.apply = apply;
|
|
1132
2234
|
exports.inject = ["slots"];
|
|
1133
|
-
exports.__internals = { getStore, createStore, srcOf, shrinkImage, MAX_REFS };
|
|
2235
|
+
exports.__internals = { pickableImagesOf, getStore, createStore, srcOf, shrinkImage, MAX_REFS };
|
|
1134
2236
|
return module.exports;
|
|
1135
2237
|
},
|
|
1136
2238
|
});
|