dsh-agnes-create 0.10.3 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,10 +53,10 @@ Key 从 [Agnes AI 平台](https://platform.agnes-ai.com) 获取。端点默认
53
53
 
54
54
  1. 侧边栏点 **Agnes创作**
55
55
  2. 顶部选择 **图片** 或 **视频**
56
- 3. 输入提示词,`Cmd/Ctrl + Enter` 或点按钮生成;提示词上方有 6 个风格预设(写实摄影 / 水彩 / 3D 渲染 / 海报 / 夜景霓虹 / 极简),点一下追加到末尾,不会冲掉已写的主体
56
+ 3. 输入提示词,`Cmd/Ctrl + Enter` 或点按钮生成;提示词上方有 6 个内置风格预设(写实摄影 / 水彩 / 3D 渲染 / 海报 / 夜景霓虹 / 极简),点一下追加到末尾,不会冲掉已写的主体;也可以把自己的常用提示词存成预设,或者点 **扩写** 让模型把那句简短描述补全
57
57
  4. 想放弃等待就点生成按钮旁的 **取消**(只是停止本地等待,服务端任务可能仍在跑)
58
58
  5. 结果直接显示在面板里;图片用 `<img>`、视频用 `<video>` 播放(支持拖动进度条)
59
- 6. 结果下方三个按钮:**复制路径**(写到剪贴板)、**在访达中显示**(定位到文件)、**下载**(另存一份到「下载」目录);如果结果本身是图片,还会多出 **设为首帧** / **设为尾帧**,一键把它变成下一段视频的关键帧
59
+ 6. 结果下方四个按钮:**复制路径**(写到剪贴板)、**在访达中显示**(定位到文件)、**下载**(另存一份到「下载」目录)、**新窗口打开**(在浏览器新标签页里看原图 / 播放原视频);如果结果本身是图片,还会多出 **设为首帧** / **设为尾帧**,一键把它变成下一段视频的关键帧
60
60
  7. 生成过的内容记在面板底部「最近生成」里,最多 24 条,落盘到 `~/.dsh/agnes-create/history.json`,重启应用也不丢
61
61
 
62
62
  提示词建议按「主体 + 场景 + 动作/镜头 + 光线 + 风格 + 构图」组织,例如:
@@ -152,6 +152,17 @@ Key 从 [Agnes AI 平台](https://platform.agnes-ai.com) 获取。端点默认
152
152
  - 内置清单:图片 `agnes-image-2.5-flash` / `agnes-image-2.1-flash`,视频 `agnes-video-2.5-flash` / `agnes-video-2.5`
153
153
  - 只有 1 个可选时仍然显示为纯文本,不弹空下拉
154
154
 
155
+ ### 自定义提示词预设与扩写
156
+
157
+ 提示词上方那排 chip 里,除了 6 个内置风格,还能放自己的:
158
+
159
+ - **+ 存为预设** —— 把当前输入的整段提示词存下来。会展开一个输入框让你取名字(留空则取提示词前 10 个字),回车或点「保存」即存,存完立刻出现在 chip 里
160
+ - 点自己的预设 = 追加到当前提示词末尾(和内置预设的拼接规则一样:先去掉尾部标点,再用「,」连接),不会覆盖已经写好的主体
161
+ - 预设 chip 右侧的 **×** 删除它,点了就生效,没有二次确认
162
+ - **扩写** —— 把一句话变成一段完整的画面描述:图片模式补主体外观、环境、光线、镜头与构图;视频模式补动作推进、镜头运动、景别与氛围。扩写结果直接替换输入框内容,多花一次文本模型的调用(约 160 tokens,几分钱)
163
+
164
+ 预设落盘到 `~/.dsh/agnes-create/presets.json`,重启应用不丢;最多 40 条,名称截到 20 字、内容截到 600 字,同名再存会覆盖并置顶。
165
+
155
166
  ### 在对话里直接出图 / 出视频
156
167
 
157
168
  除了侧边栏面板,插件还把三个模型工具交给宿主,所以你直接对 agent 说「画一只坐在窗台上的橘猫」它就能出图:
package/lib/agnes.js CHANGED
@@ -266,6 +266,67 @@ export async function clearHistory() {
266
266
  return { entries: [], file: HISTORY_FILE };
267
267
  }
268
268
 
269
+ /* ==========================================================================
270
+ 自定义提示词预设
271
+ --------------------------------------------------------------------------
272
+ 内置的 6 个风格 chip 写死在客户端(纯展示,不用存);用户自己存的走这里,
273
+ 落到 ~/.dsh/agnes-create/presets.json —— 跨会话、重启都还在。
274
+ 同名视为覆盖(存两次同一名字不会出现两条)。
275
+ ========================================================================== */
276
+
277
+ export const PRESETS_FILE = join(CONFIG_DIR, 'presets.json');
278
+ export const PRESET_MAX = 40;
279
+ export const PRESET_LABEL_MAX = 20;
280
+ export const PRESET_TEXT_MAX = 600;
281
+
282
+ function sanitizePreset(raw) {
283
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) return null;
284
+ const label = typeof raw.label === 'string' ? raw.label.trim().replace(/\s+/g, ' ') : '';
285
+ const text = typeof raw.text === 'string' ? raw.text.trim() : '';
286
+ if (label === '' || text === '') return null;
287
+ return { label: label.slice(0, PRESET_LABEL_MAX), text: text.slice(0, PRESET_TEXT_MAX) };
288
+ }
289
+
290
+ export async function readPresets() {
291
+ try {
292
+ const raw = JSON.parse(await readFile(PRESETS_FILE, 'utf8'));
293
+ const list = Array.isArray(raw) ? raw : (Array.isArray(raw?.presets) ? raw.presets : []);
294
+ const seen = new Set();
295
+ const presets = [];
296
+ for (const item of list) {
297
+ const clean = sanitizePreset(item);
298
+ if (clean === null || seen.has(clean.label)) continue;
299
+ seen.add(clean.label);
300
+ presets.push(clean);
301
+ }
302
+ return { presets: presets.slice(0, PRESET_MAX), file: PRESETS_FILE };
303
+ } catch {
304
+ return { presets: [], file: PRESETS_FILE };
305
+ }
306
+ }
307
+
308
+ /** 存一条预设(同名覆盖并置顶)。返回最新列表。 */
309
+ export async function savePreset(entry) {
310
+ const clean = sanitizePreset(entry);
311
+ if (clean === null) throw new Error('预设需要名称和内容');
312
+ const { presets } = await readPresets();
313
+ const next = [clean, ...presets.filter((p) => p.label !== clean.label)].slice(0, PRESET_MAX);
314
+ await mkdir(CONFIG_DIR, { recursive: true });
315
+ await writeFile(PRESETS_FILE, JSON.stringify(next, null, 2) + '\n', 'utf8');
316
+ return { presets: next, file: PRESETS_FILE };
317
+ }
318
+
319
+ /** 删一条预设。名字不存在时也返回成功(幂等),列表不变。 */
320
+ export async function removePreset(label) {
321
+ const name = typeof label === 'string' ? label.trim() : '';
322
+ if (name === '') throw new Error('缺少预设名称');
323
+ const { presets } = await readPresets();
324
+ const next = presets.filter((p) => p.label !== name);
325
+ await mkdir(CONFIG_DIR, { recursive: true });
326
+ await writeFile(PRESETS_FILE, JSON.stringify(next, null, 2) + '\n', 'utf8');
327
+ return { presets: next, file: PRESETS_FILE };
328
+ }
329
+
269
330
  /* ==========================================================================
270
331
  视频任务(落盘 + 宿主侧轮询)
271
332
  --------------------------------------------------------------------------
@@ -356,6 +417,78 @@ export async function listModels({ baseUrl = DEFAULT_BASE, key } = {}) {
356
417
  };
357
418
  }
358
419
 
420
+ /* ==========================================================================
421
+ 提示词扩写(借上游文本模型)
422
+ --------------------------------------------------------------------------
423
+ 实测契约(2026-11-12):POST /v1/chat/completions,OpenAI 兼容;
424
+ model=agnes-2.5-flash 可用,正文在 choices[0].message.content,约 160 tokens/次。
425
+ 图片与视频的扩写侧重不同,所以按 kind 换系统提示。
426
+ ========================================================================== */
427
+
428
+ export const EXPAND_MODEL = 'agnes-2.5-flash';
429
+ export const EXPAND_MAX_INPUT = 1000;
430
+ export const EXPAND_MAX_OUTPUT = 1200;
431
+ export const EXPAND_TIMEOUT_MS = 60_000;
432
+
433
+ const EXPAND_SYSTEM_IMAGE =
434
+ '你是图像提示词工程师。把用户给的简短想法扩写成一段具体的画面描述:'
435
+ + '补上主体外观与动作、环境细节、光线方向与质感、镜头视角与构图。'
436
+ + '只输出这一段中文描述,不要解释,不要分点,不要加引号,不要写「提示词:」这类前缀。';
437
+
438
+ const EXPAND_SYSTEM_VIDEO =
439
+ '你是视频提示词工程师。把用户给的简短想法扩写成一段可执行的视频描述:'
440
+ + '包含主体动作的推进、镜头运动(推拉摇移或固定)、景别变化、光线与氛围,'
441
+ + '时长按 5 秒左右的节奏来写,不要出现分镜编号或时间码。'
442
+ + '只输出这一段中文描述,不要解释,不要分点,不要加引号。';
443
+
444
+ /** 把一句话扩写成一段提示词。kind: 'image' | 'video'。 */
445
+ export async function expandPrompt(prompt, { kind = 'image', model = EXPAND_MODEL, key, baseUrl = DEFAULT_BASE } = {}) {
446
+ const text = typeof prompt === 'string' ? prompt.trim() : '';
447
+ if (text === '') throw new Error('先写一句想画什么,再来扩写');
448
+ if (text.length > EXPAND_MAX_INPUT) throw new Error(`提示词太长(上限 ${EXPAND_MAX_INPUT} 字)`);
449
+ const k = key ?? (await readKey());
450
+ if (!k) throw new Error('没有找到 API Key,先确认 Key 文件路径');
451
+
452
+ const system = kind === 'video' ? EXPAND_SYSTEM_VIDEO : EXPAND_SYSTEM_IMAGE;
453
+ let res;
454
+ try {
455
+ res = await fetch(`${baseUrl.replace(/\/+$/, '')}/chat/completions`, {
456
+ method: 'POST',
457
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${k}` },
458
+ body: JSON.stringify({
459
+ model,
460
+ messages: [
461
+ { role: 'system', content: system },
462
+ { role: 'user', content: text },
463
+ ],
464
+ max_tokens: 400,
465
+ }),
466
+ signal: AbortSignal.timeout(EXPAND_TIMEOUT_MS),
467
+ });
468
+ } catch (e) {
469
+ throw new Error(`连不上上游:${e && e.message ? e.message : String(e)}`);
470
+ }
471
+ if (res.status === 401 || res.status === 403) throw new Error('Key 无效或已过期(HTTP ' + res.status + ')');
472
+ if (res.status === 429) throw new Error('上游限流(HTTP 429),稍后再试');
473
+ if (!res.ok) {
474
+ const body = await res.text().catch(() => '');
475
+ throw new Error(`上游返回 HTTP ${res.status}${body ? ':' + body.slice(0, 120) : ''}`);
476
+ }
477
+
478
+ const payload = await res.json().catch(() => null);
479
+ const content = payload && payload.choices && payload.choices[0]
480
+ ? payload.choices[0].message && payload.choices[0].message.content
481
+ : '';
482
+ // 有的模型会自带一对引号或收尾换行,剥掉再返回
483
+ const out = (typeof content === 'string' ? content : '')
484
+ .trim()
485
+ .replace(/^[「“"'`]+/, '')
486
+ .replace(/[」”"'`]+$/, '')
487
+ .trim();
488
+ if (out === '') throw new Error('上游没有返回内容,再试一次');
489
+ return { prompt: out.slice(0, EXPAND_MAX_OUTPUT), model, usage: (payload && payload.usage) || null };
490
+ }
491
+
359
492
  export async function readKey(file) {
360
493
  const target = file ?? (await resolvePaths()).keyFile;
361
494
  let raw;
package/lib/client.js CHANGED
@@ -218,6 +218,27 @@ window.__ModuleLoader__.load({
218
218
  .agn-preset:hover:not(:disabled) { border-color: var(--dsw-alias-state-business-primary, #4b7bec);
219
219
  color: var(--dsw-alias-state-business-primary, #4b7bec); }
220
220
  .agn-preset:disabled { opacity: .45; cursor: not-allowed; }
221
+ /* 我的预设:名字 + 删除叉,包在一个 span 里 */
222
+ .agn-preset-user { display: inline-flex; align-items: stretch; }
223
+ .agn-preset-mine { border-top-right-radius: 0; border-bottom-right-radius: 0;
224
+ border-right: none; }
225
+ .agn-preset-x { font: inherit; font-size: 11px; line-height: 1.4; padding: 2px 7px;
226
+ border-radius: 0 999px 999px 0; cursor: pointer;
227
+ color: var(--dsw-alias-label-secondary, #888); background: transparent;
228
+ border: 1px solid var(--dsw-alias-border-l2, #ddd); }
229
+ .agn-preset-x:hover:not(:disabled) { color: var(--dsw-alias-state-error-primary, #e5484d);
230
+ border-color: var(--dsw-alias-state-error-primary, #e5484d); }
231
+ .agn-preset-x:disabled { opacity: .45; cursor: not-allowed; }
232
+ .agn-preset-add { border-style: dashed; }
233
+ .agn-preset-expand { color: var(--dsw-alias-state-business-primary, #4b7bec);
234
+ border-color: var(--dsw-alias-state-business-primary, #4b7bec); }
235
+ .agn-preset-form { display: flex; gap: 6px; align-items: center; margin-bottom: 6px; }
236
+ .agn-input-sm { flex: 1 1 auto; min-width: 0; font: inherit; font-size: 12px;
237
+ padding: 3px 8px; border-radius: 6px;
238
+ color: var(--dsw-alias-label-primary, #222); background: transparent;
239
+ border: 1px solid var(--dsw-alias-border-l2, #ddd); }
240
+ .agn-act-link { text-decoration: none; color: inherit; display: inline-flex;
241
+ align-items: center; }
221
242
 
222
243
  /* 视频关键帧(图生视频) */
223
244
  .agn-frames { display: flex; flex-direction: column; gap: 8px; }
@@ -443,6 +464,14 @@ window.__ModuleLoader__.load({
443
464
  libraryBusy: false,
444
465
  libraryError: "",
445
466
  copiedKey: "", // 刚刚复制了哪一条的提示词(用于显示「已复制」)
467
+
468
+ /* ---- 自定义提示词预设(宿主落盘) + 扩写 ---- */
469
+ presets: [], // [{ label, text }]
470
+ presetsLoaded: false,
471
+ presetFormOpen: false, // 「存为预设」的小表单
472
+ presetLabel: "",
473
+ presetBusy: false,
474
+ expandBusy: false,
446
475
  };
447
476
 
448
477
  const snapshot = () => ({ ...state, history: state.history.slice() });
@@ -483,6 +512,94 @@ window.__ModuleLoader__.load({
483
512
  void refreshLibrary();
484
513
  };
485
514
 
515
+ /* ---------- 自定义提示词预设(宿主落盘) ---------- */
516
+ async function loadPresets() {
517
+ try {
518
+ const res = await fetch("/agnes/presets");
519
+ if (!res.ok) { set({ presetsLoaded: true }); return; }
520
+ const data = await res.json();
521
+ if (!data || data.ok !== true) { set({ presetsLoaded: true }); return; }
522
+ set({ presets: Array.isArray(data.presets) ? data.presets : [], presetsLoaded: true });
523
+ } catch {
524
+ set({ presetsLoaded: true });
525
+ }
526
+ }
527
+
528
+ const presetText = (preset) => (
529
+ state.prompt.trim() === ""
530
+ ? preset.text
531
+ : state.prompt.replace(/[\s,,;;。.]+$/, "") + "," + preset.text
532
+ );
533
+
534
+ async function saveCurrentAsPreset() {
535
+ if (state.presetBusy) return;
536
+ const text = state.prompt.trim();
537
+ if (text === "") { set({ notice: "先写提示词,再存成预设" }); return; }
538
+ const label = (state.presetLabel.trim() || text.slice(0, 10)).trim();
539
+ set({ presetBusy: true });
540
+ try {
541
+ const res = await fetch("/agnes/presets", {
542
+ method: "POST",
543
+ headers: { "content-type": "application/json" },
544
+ body: JSON.stringify({ label, text }),
545
+ });
546
+ const data = await res.json().catch(() => null);
547
+ if (!res.ok || !data || data.ok !== true) {
548
+ throw new Error((data && data.error) || ("HTTP " + res.status));
549
+ }
550
+ set({
551
+ presets: Array.isArray(data.presets) ? data.presets : state.presets,
552
+ presetBusy: false, presetFormOpen: false, presetLabel: "",
553
+ notice: "已存为预设「" + label + "」",
554
+ });
555
+ } catch (e) {
556
+ set({ presetBusy: false, notice: "存预设失败:" + (e && e.message ? e.message : String(e)) });
557
+ }
558
+ }
559
+
560
+ async function deletePreset(label) {
561
+ if (state.presetBusy) return;
562
+ set({ presetBusy: true });
563
+ try {
564
+ const res = await fetch("/agnes/presets?label=" + encodeURIComponent(label), { method: "DELETE" });
565
+ const data = await res.json().catch(() => null);
566
+ if (!res.ok || !data || data.ok !== true) {
567
+ throw new Error((data && data.error) || ("HTTP " + res.status));
568
+ }
569
+ set({
570
+ presets: Array.isArray(data.presets) ? data.presets : [],
571
+ presetBusy: false,
572
+ notice: "已删除预设「" + label + "」",
573
+ });
574
+ } catch (e) {
575
+ set({ presetBusy: false, notice: "删除预设失败:" + (e && e.message ? e.message : String(e)) });
576
+ }
577
+ }
578
+
579
+ /** 让上游文本模型(agnes-2.5-flash)把这一句扩写成一段描述。 */
580
+ async function expandPrompt() {
581
+ if (state.expandBusy) return;
582
+ const text = state.prompt.trim();
583
+ if (text === "") { set({ notice: "先写一句想画什么,再来扩写" }); return; }
584
+ set({ expandBusy: true, notice: "" });
585
+ try {
586
+ const res = await fetch("/agnes/expand", {
587
+ method: "POST",
588
+ headers: { "content-type": "application/json" },
589
+ body: JSON.stringify({ prompt: text, kind: state.mode === "video" ? "video" : "image" }),
590
+ });
591
+ const data = await res.json().catch(() => null);
592
+ if (!res.ok || !data || data.ok !== true) {
593
+ throw new Error((data && data.error) || ("HTTP " + res.status));
594
+ }
595
+ const next = typeof data.prompt === "string" ? data.prompt.trim() : "";
596
+ if (next === "") throw new Error("上游没有返回内容");
597
+ set({ prompt: next, expandBusy: false, notice: "已扩写(" + (data.model || "agnes-2.5-flash") + ")" });
598
+ } catch (e) {
599
+ set({ expandBusy: false, notice: "扩写失败:" + (e && e.message ? e.message : String(e)) });
600
+ }
601
+ }
602
+
486
603
  /* ---------- 历史(宿主持久化) ---------- */
487
604
  async function loadHistory() {
488
605
  try {
@@ -1330,6 +1447,15 @@ window.__ModuleLoader__.load({
1330
1447
  downloadResult,
1331
1448
  cancel,
1332
1449
  copyPrompt,
1450
+ /* ---- 自定义预设 / 扩写 ---- */
1451
+ loadPresets,
1452
+ saveCurrentAsPreset,
1453
+ deletePreset,
1454
+ expandPrompt,
1455
+ /** 追加内置或自定义预设到提示词末尾 */
1456
+ appendPreset(preset) { set({ prompt: presetText(preset) }); },
1457
+ setPresetLabel(value) { set({ presetLabel: value }); },
1458
+ togglePresetForm() { set({ presetFormOpen: !state.presetFormOpen, presetLabel: "" }); },
1333
1459
  /* ---- 输出目录清单 ---- */
1334
1460
  loadLibrary,
1335
1461
  toggleLibrary,
@@ -1472,6 +1598,7 @@ window.__ModuleLoader__.load({
1472
1598
  store.loadHealth();
1473
1599
  store.loadConfig();
1474
1600
  store.loadHistory();
1601
+ store.loadPresets();
1475
1602
  // 上次没跑完的视频任务:宿主还在推进,接回进度
1476
1603
  store.resumeTasks();
1477
1604
  }, []);
@@ -1848,18 +1975,67 @@ window.__ModuleLoader__.load({
1848
1975
  React.createElement("div", { className: "agn-label" }, "提示词"),
1849
1976
  React.createElement("div", { className: "agn-presets" },
1850
1977
  PROMPT_PRESETS.map((preset) => React.createElement("button", {
1851
- key: preset.label,
1978
+ key: "builtin-" + preset.label,
1852
1979
  type: "button",
1853
1980
  className: "agn-preset",
1854
1981
  disabled: busy,
1855
1982
  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))
1983
+ onClick: () => store.appendPreset(preset),
1984
+ }, preset.label)),
1985
+ s.presets.map((preset) => React.createElement("span", {
1986
+ key: "mine-" + preset.label,
1987
+ className: "agn-preset-user",
1988
+ },
1989
+ React.createElement("button", {
1990
+ type: "button",
1991
+ className: "agn-preset agn-preset-mine",
1992
+ disabled: busy,
1993
+ title: "我的预设,点一下追加:" + preset.text,
1994
+ onClick: () => store.appendPreset(preset),
1995
+ }, preset.label),
1996
+ React.createElement("button", {
1997
+ type: "button",
1998
+ className: "agn-preset-x",
1999
+ disabled: busy || s.presetBusy,
2000
+ title: "删除这个预设",
2001
+ onClick: () => store.deletePreset(preset.label),
2002
+ }, "×")
2003
+ )),
2004
+ React.createElement("button", {
2005
+ type: "button",
2006
+ className: "agn-preset agn-preset-add",
2007
+ disabled: busy || s.prompt.trim() === "" || s.presetBusy,
2008
+ title: "把当前提示词存成可复用的预设",
2009
+ onClick: () => store.togglePresetForm(),
2010
+ }, s.presetFormOpen ? "取消保存" : "+ 存为预设"),
2011
+ React.createElement("button", {
2012
+ type: "button",
2013
+ className: "agn-preset agn-preset-expand",
2014
+ disabled: busy || s.expandBusy || s.prompt.trim() === "",
2015
+ title: "用 agnes-2.5-flash 把这一句扩写成一段具体描述",
2016
+ onClick: () => store.expandPrompt(),
2017
+ }, s.expandBusy ? "扩写中…" : "扩写")
1862
2018
  ),
2019
+ s.presetFormOpen
2020
+ ? React.createElement("div", { className: "agn-preset-form" },
2021
+ React.createElement("input", {
2022
+ className: "agn-input agn-input-sm",
2023
+ value: s.presetLabel,
2024
+ placeholder: "给这个预设起个名字(留空则取提示词前 10 个字)",
2025
+ maxLength: 20,
2026
+ disabled: s.presetBusy,
2027
+ onChange: (e) => store.setPresetLabel(e.target.value),
2028
+ onKeyDown: (e) => {
2029
+ if (e.key === "Enter") { e.preventDefault(); store.saveCurrentAsPreset(); }
2030
+ },
2031
+ }),
2032
+ React.createElement("button", {
2033
+ type: "button", className: "agn-btn",
2034
+ disabled: s.presetBusy,
2035
+ onClick: () => store.saveCurrentAsPreset(),
2036
+ }, s.presetBusy ? "保存中…" : "保存")
2037
+ )
2038
+ : null,
1863
2039
  React.createElement("textarea", {
1864
2040
  className: "agn-textarea",
1865
2041
  value: s.prompt,
@@ -1977,6 +2153,13 @@ window.__ModuleLoader__.load({
1977
2153
  title: "另存一份到「下载」目录",
1978
2154
  onClick: () => store.downloadResult(),
1979
2155
  }, "下载"),
2156
+ React.createElement("a", {
2157
+ className: "agn-act agn-act-link",
2158
+ href: srcOf(shown.filename),
2159
+ target: "_blank",
2160
+ rel: "noopener noreferrer",
2161
+ title: "用浏览器新标签页打开原文件",
2162
+ }, "新窗口打开"),
1980
2163
  shown.kind === "image"
1981
2164
  ? React.createElement("button", {
1982
2165
  type: "button", className: "agn-act",
package/lib/index.js CHANGED
@@ -17,6 +17,14 @@
17
17
  * POST /agnes/history 追加一条(生成完成后由前端调用)
18
18
  * DELETE /agnes/history 清空
19
19
  *
20
+ * 自定义提示词预设(宿主落盘,跨会话/重启都在)
21
+ * GET /agnes/presets 读回全部
22
+ * POST /agnes/presets { label, text } → 存一条(同名覆盖并置顶)
23
+ * DELETE /agnes/presets ?label=… → 删一条
24
+ *
25
+ * 提示词扩写(借上游文本模型 agnes-2.5-flash)
26
+ * POST /agnes/expand { prompt, kind } → { prompt } 扩写后的描述
27
+ *
20
28
  * 结果收尾
21
29
  * POST /agnes/reveal { name } → 在访达里显示该文件(只能是输出目录内的文件)
22
30
  *
@@ -40,6 +48,8 @@ import {
40
48
  listLibrary, fileExists,
41
49
  listModels, IMAGE_MODELS, VIDEO_MODELS,
42
50
  readTasks, writeTasks, TASK_ACTIVE, TASK_MAX,
51
+ readPresets, savePreset, removePreset, PRESET_MAX,
52
+ expandPrompt, EXPAND_MODEL,
43
53
  } from './agnes.js';
44
54
 
45
55
  export const name = 'dsh-agnes-create';
@@ -668,7 +678,7 @@ export function apply(ctx) {
668
678
  path: '/agnes/video',
669
679
  handler: async (req, res) => {
670
680
  try {
671
- const url = new URL(req.url, 'http://dsh.local');
681
+ const url = new URL(req.url, 'https://dsh.local');
672
682
 
673
683
  // POST = 提交任务
674
684
  if (req.method === 'POST') {
@@ -790,7 +800,7 @@ export function apply(ctx) {
790
800
  handler: async (req, res) => {
791
801
  try {
792
802
  if (req.method !== 'GET') return sendJson(res, { ok: false, error: '需要 GET' }, 405);
793
- const url = new URL(req.url, 'http://dsh.local');
803
+ const url = new URL(req.url, 'https://dsh.local');
794
804
  const id = url.searchParams.get('id') || '';
795
805
  const task = tasks.get(id);
796
806
  if (task === undefined) return sendJson(res, { ok: false, error: '任务不存在' }, 404);
@@ -872,6 +882,50 @@ export function apply(ctx) {
872
882
  },
873
883
  });
874
884
 
885
+ /* ---------- 自定义提示词预设 ---------- */
886
+ addRoute(webServer, {
887
+ kind: 'exact',
888
+ path: '/agnes/presets',
889
+ handler: async (req, res) => {
890
+ try {
891
+ if (req.method === 'GET') {
892
+ const { presets, file } = await readPresets();
893
+ return sendJson(res, { ok: true, presets, file, max: PRESET_MAX });
894
+ }
895
+ if (req.method === 'POST') {
896
+ const body = await readJson(req);
897
+ const { presets } = await savePreset({ label: body.label, text: body.text });
898
+ return sendJson(res, { ok: true, presets, max: PRESET_MAX });
899
+ }
900
+ if (req.method === 'DELETE') {
901
+ const label = new URL(req.url, 'https://dsh.local').searchParams.get('label') || '';
902
+ const { presets } = await removePreset(label);
903
+ return sendJson(res, { ok: true, presets, max: PRESET_MAX });
904
+ }
905
+ return sendJson(res, { ok: false, error: '需要 GET / POST / DELETE' }, 405);
906
+ } catch (e) {
907
+ sendJson(res, { ok: false, error: errMessage(e) }, 400);
908
+ }
909
+ },
910
+ });
911
+
912
+ /* ---------- 提示词扩写 ---------- */
913
+ addRoute(webServer, {
914
+ kind: 'exact',
915
+ path: '/agnes/expand',
916
+ handler: async (req, res) => {
917
+ try {
918
+ if (req.method !== 'POST') return sendJson(res, { ok: false, error: '需要 POST' }, 405);
919
+ const body = await readJson(req);
920
+ const kind = body.kind === 'video' ? 'video' : 'image';
921
+ const { prompt, model } = await expandPrompt(body.prompt, { kind });
922
+ sendJson(res, { ok: true, prompt, model: model || EXPAND_MODEL });
923
+ } catch (e) {
924
+ sendJson(res, { ok: false, error: errMessage(e) }, 400);
925
+ }
926
+ },
927
+ });
928
+
875
929
  /* ---------- 输出目录清单:把已经存在的图/视频捞回界面 ---------- */
876
930
  addRoute(webServer, {
877
931
  kind: 'exact',
@@ -912,7 +966,7 @@ export function apply(ctx) {
912
966
  return sendJson(res, { ok: false, error: '文件不在输出目录内' }, 400);
913
967
  }
914
968
  await new Promise((resolve, reject) => {
915
- execFile('open', ['-R', abs], (err) => (err ? reject(err) : resolve()));
969
+ execFile('/usr/bin/open', ['-R', abs], (err) => (err ? reject(err) : resolve()));
916
970
  });
917
971
  sendJson(res, { ok: true, path: abs });
918
972
  } catch (e) {
@@ -924,7 +978,7 @@ export function apply(ctx) {
924
978
  /* ---------- 媒体(图片 + 视频) ---------- */
925
979
  const mediaHandler = async (req, res) => {
926
980
  try {
927
- const name = new URL(req.url, 'http://dsh.local').searchParams.get('name') || '';
981
+ const name = new URL(req.url, 'https://dsh.local').searchParams.get('name') || '';
928
982
  const { buf, mime } = await readMedia(name);
929
983
  sendMedia(req, res, buf, mime);
930
984
  } catch (e) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-agnes-create",
3
- "version": "0.10.3",
4
- "description": "Agnes 创作:在 DeepSeek Harness 里出图、出视频,也能让 agent 在对话里直接调用。图片支持文生图 / 图生图 / 多图合成(agnes-image-2.5-flash);视频支持文生 / 首尾帧关键帧 / 参考图三种模式与 seed(agnes-video-2.5-flash,720P,4–12 秒)。侧边栏面板 + 模型工具双通道,视频任务由宿主推进、刷新或重启可接回;提示词写进图片(PNG iTXt / JPEG COM)悬停可看可复制;可浏览输出目录、历史 24 条、模型可切换并测试连接;标题栏的模型名可直接点开切换。",
3
+ "version": "0.11.1",
4
+ "description": "Agnes 创作:在 DeepSeek Harness 里出图、出视频,也能让 agent 在对话里直接调用。图片支持文生图 / 图生图 / 多图合成(agnes-image-2.5-flash);视频支持文生 / 首尾帧关键帧 / 参考图三种模式与 seed(agnes-video-2.5-flash,720P,4–12 秒)。侧边栏面板 + 模型工具双通道,视频任务由宿主推进、刷新或重启可接回;提示词写进图片(PNG iTXt / JPEG COM)悬停可看可复制;可浏览输出目录、历史 24 条、模型可切换并测试连接;标题栏的模型名可直接点开切换;提示词可存成自定义预设并一键扩写。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {