@logictan/dsh-imagegen 2.0.2 → 2.0.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/lib/index.js CHANGED
@@ -1,9 +1,65 @@
1
+ import "@deepseek-ai/cordis";
1
2
  import * as settingsModule from "@deepseek-ai/dsh-settings";
2
3
  import { SettingsConflictError } from "@deepseek-ai/dsh-settings";
3
4
  import z from "@deepseek-ai/schemastery";
4
- import "@deepseek-ai/cordis";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import { defineTool } from "@deepseek-ai/dsh-tools";
7
+ //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-brand@0.2.0-rc.2_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-brand/lib/index.js
8
+ /**
9
+ * Duplicate-install-safe nominal primitive helpers.
10
+ *
11
+ * A brand makes structurally identical strings or numbers non-interchangeable
12
+ * at the type level: a `SessionId` cannot be passed where a `ToolCallId` is
13
+ * expected, and an event sequence cannot be passed as a log offset. Comparison,
14
+ * logging, and serialization retain the underlying primitive behavior.
15
+ *
16
+ * This package owns no concrete domain value and keeps no runtime identity or mutable
17
+ * state, so independently installed copies produce interchangeable values.
18
+ *
19
+ * @module @deepseek-ai/dsh-brand
20
+ */
21
+ /**
22
+ * Apply a compile-time string brand without changing the value.
23
+ * @param value - string admitted by the domain that owns the target brand.
24
+ * @returns the same string with the requested compile-time brand.
25
+ */
26
+ function brandString(value) {
27
+ return value;
28
+ }
29
+ //#endregion
30
+ //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-credentials@0.2.0-rc.2_@deepseek-ai+cordis@4.0.4_@deepseek-ai+dsh-inva_13783eb73d2217c5a4a149b781a9cc49/node_modules/@deepseek-ai/dsh-credentials/lib/index.js
31
+ /**
32
+ * Service Definition for the credential-reference capability seam (`ctx.credentials`). Settings and composition files carry
33
+ * *references* to secrets — environment-variable names — while providers own
34
+ * the actual values and their storage. Consumers resolve a reference once per
35
+ * operation, so a changed credential reaches the next operation without any
36
+ * plugin restart, and configuration surfaces describe a reference without
37
+ * ever seeing its value.
38
+ * @module @deepseek-ai/dsh-credentials
39
+ */
40
+ const REF_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
41
+ /**
42
+ * Brand a raw string as a {@link CredentialRef}.
43
+ * @param value - candidate reference; a POSIX shell identifier such as `DEEPSEEK_API_KEY`.
44
+ * @returns the branded reference.
45
+ */
46
+ function credentialRef(value) {
47
+ if (!isCredentialRefName(value)) throw new TypeError(`credential ref "${value}" must match ${String(REF_PATTERN)}`);
48
+ return brandString(value);
49
+ }
50
+ /**
51
+ * Whether a raw string could name a reference at all. Consumers that receive
52
+ * environment-variable names from somewhere else — a provider library's own
53
+ * ambient discovery, a hook payload — ask this before resolving, because a name
54
+ * outside the grammar has no reference to miss and should read as "not set"
55
+ * rather than as a thrown error.
56
+ * @param value - candidate reference.
57
+ * @returns true when {@link credentialRef} would accept it.
58
+ */
59
+ function isCredentialRefName(value) {
60
+ return REF_PATTERN.test(value);
61
+ }
62
+ //#endregion
7
63
  //#region src/settings-compat.ts
8
64
  const compatModule = settingsModule;
9
65
  /** Strip volatile markers for the pre-0.1.7 settings providers. */
@@ -1697,6 +1753,55 @@ function subscriptionReferences(request) {
1697
1753
  });
1698
1754
  }
1699
1755
  //#endregion
1756
+ //#region src/credential-refs.ts
1757
+ /**
1758
+ * The mapping from credential-backed settings paths to credential refs.
1759
+ *
1760
+ * This is a pure module on purpose. The settings bridge (`routes.ts`) must
1761
+ * classify a submitted path as credential-backed *before* it decides whether to
1762
+ * forward it to the settings seam, and it must reach the same verdict on a host
1763
+ * that composes no credential provider — otherwise a key write would fall
1764
+ * through to the settings document, which is exactly the plaintext leak this
1765
+ * mapping exists to prevent. So the mapping is a pure function that needs no
1766
+ * provider, and `routes.ts` can import it without a cycle back into `index.ts`.
1767
+ *
1768
+ * `role('secret')` is NOT protection: it redacts a value from settings *reads*,
1769
+ * while the plaintext still lands in `cordis.patch.yml`, which the config-sync
1770
+ * `plugins` adapter exports verbatim.
1771
+ */
1772
+ /** Credential reference holding the prompt-enhancement endpoint's API key. */
1773
+ const PROMPT_KEY_REF = "DSH_IMAGEGEN_PROMPT_KEY";
1774
+ /**
1775
+ * The credential ref holding one channel's API key.
1776
+ *
1777
+ * The channel id is folded into the ref name because refs must match
1778
+ * `/^[A-Za-z_][A-Za-z0-9_]*$/` — a channel id is usually a UUID, whose hyphens
1779
+ * are outside that grammar. The fold is lossy in principle (two ids differing
1780
+ * only in punctuation collide), but ids are generated UUIDs, so the
1781
+ * transformation is injective over the values actually in use.
1782
+ *
1783
+ * @param channelId - the channel's stable id.
1784
+ * @returns the credential reference name.
1785
+ */
1786
+ function channelKeyRef(channelId) {
1787
+ return `DSH_IMAGEGEN_CHANNEL_${channelId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
1788
+ }
1789
+ /**
1790
+ * The credential ref backing a settings path, or `undefined` when the path is an
1791
+ * ordinary config field that belongs in the settings document.
1792
+ *
1793
+ * Only the two key-bearing families are diverted. The legacy flat `apiKey` field
1794
+ * is deliberately NOT diverted: it is a plaintext field being retired, so its
1795
+ * `unset` must reach the settings seam to actually delete the stored value.
1796
+ *
1797
+ * @param path - a settings path op's path segments.
1798
+ * @returns the credential ref, or `undefined` for a config field.
1799
+ */
1800
+ function credentialRefForPath(path) {
1801
+ if (path.length === 1 && path[0] === "promptApiKey") return PROMPT_KEY_REF;
1802
+ if (path.length === 2 && path[0] === "channelSecrets" && path[1] !== void 0 && path[1] !== "") return channelKeyRef(path[1]);
1803
+ }
1804
+ //#endregion
1700
1805
  //#region src/presets.ts
1701
1806
  /**
1702
1807
  * Built-in provider catalog (presets). A preset is an official or well-known
@@ -1951,17 +2056,18 @@ function imageDataUrl$1(value) {
1951
2056
  };
1952
2057
  }
1953
2058
  /** Project one settings descriptor onto the bridge wire view. */
1954
- function toView(descriptor, namespace = String(descriptor.ns)) {
2059
+ function toView(descriptor, namespace = String(descriptor.ns), extraSecrets = []) {
2060
+ const secrets = [...(descriptor.secrets ?? []).map((secret) => ({
2061
+ path: [...secret.path],
2062
+ set: secret.set
2063
+ })), ...extraSecrets];
1955
2064
  return {
1956
2065
  ns: namespace,
1957
2066
  schema: descriptor.schema,
1958
2067
  value: descriptor.value,
1959
2068
  ...descriptor.base === void 0 ? {} : { base: descriptor.base },
1960
2069
  ...descriptor.user === void 0 ? {} : { user: descriptor.user },
1961
- ...descriptor.secrets === void 0 ? {} : { secrets: descriptor.secrets.map((secret) => ({
1962
- path: [...secret.path],
1963
- set: secret.set
1964
- })) },
2070
+ ...secrets.length === 0 ? {} : { secrets },
1965
2071
  revision: descriptor.revision
1966
2072
  };
1967
2073
  }
@@ -1985,6 +2091,80 @@ function failureOf(error) {
1985
2091
  message: error instanceof Error ? error.message : String(error)
1986
2092
  };
1987
2093
  }
2094
+ /** The path of a raw settings op, or `undefined` when it carries no string path. */
2095
+ function settingsOpPath(op) {
2096
+ if (typeof op !== "object" || op === null) return void 0;
2097
+ const path = op.path;
2098
+ if (!Array.isArray(path)) return void 0;
2099
+ return path.every((segment) => typeof segment === "string") ? path : void 0;
2100
+ }
2101
+ /**
2102
+ * Split submitted settings ops into config-field ops and credential ops.
2103
+ *
2104
+ * A credential path is never forwarded to the settings seam: the namespace root
2105
+ * is volatile, so a `set` there would be accepted and would persist the
2106
+ * plaintext key into `cordis.patch.yml` — the exact leak this split exists to
2107
+ * prevent. An empty (or whitespace-only) value decodes as a removal because the
2108
+ * credential store refuses to hold an empty string.
2109
+ *
2110
+ * @param ops - the raw ops from the bridge request body.
2111
+ * @returns the ops to forward to the settings seam, and the credential writes.
2112
+ * @throws {TypeError} when a credential path carries an undecodable op.
2113
+ */
2114
+ function splitCredentialOps(ops) {
2115
+ const settingsOps = [];
2116
+ const credentialOps = [];
2117
+ for (const op of ops) {
2118
+ const path = settingsOpPath(op);
2119
+ const ref = path === void 0 ? void 0 : credentialRefForPath(path);
2120
+ if (path === void 0 || ref === void 0) {
2121
+ settingsOps.push(op);
2122
+ continue;
2123
+ }
2124
+ const kind = op.op;
2125
+ if (kind === "unset") {
2126
+ credentialOps.push({
2127
+ ref,
2128
+ value: void 0
2129
+ });
2130
+ continue;
2131
+ }
2132
+ const raw = op.value;
2133
+ if (kind !== "set" || typeof raw !== "string") throw new TypeError(`settings op for credential field "${path.join(".")}" must be a string set or an unset`);
2134
+ const value = raw.trim();
2135
+ credentialOps.push({
2136
+ ref,
2137
+ value: value === "" ? void 0 : value
2138
+ });
2139
+ }
2140
+ return {
2141
+ settingsOps,
2142
+ credentialOps
2143
+ };
2144
+ }
2145
+ /**
2146
+ * The credential-backed secret entries the redacted view must carry.
2147
+ *
2148
+ * The client never receives a key value, so "is a key held?" is answered from
2149
+ * this sidecar. The schema no longer declares the key fields, so these entries
2150
+ * are synthesized from the credential store rather than walked off the schema.
2151
+ *
2152
+ * @param deps - the route dependencies.
2153
+ * @returns one entry per credential-backed path.
2154
+ */
2155
+ async function credentialSecrets(deps) {
2156
+ const credentials = deps.credentials?.();
2157
+ if (credentials === void 0) return [];
2158
+ const paths = [["promptApiKey"]];
2159
+ for (const channel of deps.resolveChannels?.().channels ?? []) paths.push(["channelSecrets", channel.id]);
2160
+ return Promise.all(paths.map(async (path) => {
2161
+ const ref = credentialRefForPath(path);
2162
+ return {
2163
+ path,
2164
+ set: ref === void 0 ? false : await credentials.holds(ref)
2165
+ };
2166
+ }));
2167
+ }
1988
2168
  /** Validate a submitted generation request (the browser's generate payload). */
1989
2169
  function parseGenerateRequest(body) {
1990
2170
  const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
@@ -2276,7 +2456,7 @@ function makeRoutes(deps) {
2276
2456
  writeJson(res, 200, {
2277
2457
  ok: true,
2278
2458
  value: {
2279
- namespaces: descriptor === void 0 ? [] : [toView(descriptor, IMAGEGEN_SETTINGS_NAMESPACE)],
2459
+ namespaces: descriptor === void 0 ? [] : [toView(descriptor, IMAGEGEN_SETTINGS_NAMESPACE, await credentialSecrets(deps))],
2280
2460
  writable: deps.settings.writable !== false
2281
2461
  }
2282
2462
  });
@@ -2305,8 +2485,27 @@ function makeRoutes(deps) {
2305
2485
  return;
2306
2486
  }
2307
2487
  const expectedRevision = typeof body.expectedRevision === "number" ? body.expectedRevision : void 0;
2488
+ let settingsOps;
2489
+ let credentialOps;
2308
2490
  try {
2309
- await deps.settings.mutate(settingsNamespace, body.ops, expectedRevision);
2491
+ ({settingsOps, credentialOps} = splitCredentialOps(body.ops));
2492
+ } catch (error) {
2493
+ writeJson(res, 200, failureOf(error));
2494
+ return;
2495
+ }
2496
+ const credentials = deps.credentials?.();
2497
+ if (credentialOps.length > 0 && credentials === void 0) {
2498
+ writeJson(res, 200, {
2499
+ ok: false,
2500
+ code: "settings-rejected",
2501
+ message: "this host composes no credential provider, so an API key cannot be stored"
2502
+ });
2503
+ return;
2504
+ }
2505
+ try {
2506
+ if (settingsOps.length > 0) await deps.settings.mutate(settingsNamespace, settingsOps, expectedRevision);
2507
+ for (const credential of credentialOps) if (credential.value === void 0) await credentials?.unset(credential.ref);
2508
+ else await credentials?.set(credential.ref, credential.value);
2310
2509
  } catch (error) {
2311
2510
  writeJson(res, 200, failureOf(error));
2312
2511
  return;
@@ -2322,7 +2521,7 @@ function makeRoutes(deps) {
2322
2521
  }
2323
2522
  writeJson(res, 200, {
2324
2523
  ok: true,
2325
- value: toView(descriptor, IMAGEGEN_SETTINGS_NAMESPACE)
2524
+ value: toView(descriptor, IMAGEGEN_SETTINGS_NAMESPACE, await credentialSecrets(deps))
2326
2525
  });
2327
2526
  }
2328
2527
  },
@@ -3116,13 +3315,10 @@ const Config = z.object({
3116
3315
  id: z.string()
3117
3316
  })).default([])
3118
3317
  })).default([]),
3119
- channelSecrets: z.dict(z.string().role("secret")).default({}),
3120
3318
  defaultChannelId: z.string().default(""),
3121
3319
  promptApiUrl: z.string().default(""),
3122
- promptApiKey: z.string().role("secret").default(""),
3123
3320
  promptModel: z.string().default(""),
3124
3321
  apiUrl: z.string().default(""),
3125
- apiKey: z.string().role("secret").default(""),
3126
3322
  imageModels: z.array(z.string()).default([])
3127
3323
  }).volatile();
3128
3324
  /** Schema defaults, re-read for hand-built contexts (the loader applies them normally). */
@@ -3132,7 +3328,7 @@ const DEFAULT_ALLOW_AGENT_IMAGE_GENERATION = true;
3132
3328
  /** Order of the announcement section within the tool-guidance band. */
3133
3329
  const SECTION_ORDER = 150;
3134
3330
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
3135
- const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):通过「渠道」对接 OpenAI 兼容图像生成 API(每个渠道 = 一个 API 端点 + 各自的模型目录),支持文生图(/images/generations)与图生图(/images/edits,上传参考图,grok-imagine 模型按官方 JSON image_url 协议发送,nanobanana 系列按 aspect_ratio / image_size 参数协议发送;seedream 系列统一走 /images/generations,参考图以 JSON image 数组发送;智谱 `glm-image` 使用官方 `/api/paas/v4/images/generations`,当前仅支持文生图;qwen-image 系列使用阿里云 DashScope 原生接口(api_url 填 https://dashscope.aliyuncs.com/api/v1,不支持 OpenAI 兼容模式,该渠道不可复用于提示词增强,尺寸自动映射为宽*高)。MiniMax `image-01` 使用 MiniMax 原生 `/image_generation` 接口(api_url 填 https://api.minimax.io/v1 或国内站 https://api.minimaxi.com/v1,支持 1:1/16:9/4:3/3:2/2:3/3:4/9:16/21:9 宽高比,一次最多 9 张;图生图为单张 subject_reference 主体参考(保持人物/主体一致,非像素级局部编辑);其 /models 只列聊天模型,图片模型需用预设目录)。API 地址与密钥在 GUI 设置中按渠道配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回。模型只能使用用户在各渠道配置目录中的模型;检测模型时会过滤聊天、Embedding 等非图片模型,但模型出现在 /models 中仍不等于其网关原生支持生图协议,遇到 Qwen、MiniMax、Gemini 等非 OpenAI 生图协议时应如实说明上游兼容性。Agent 可直接调用 `generate_image` 提交文生图,也可用 `edit_image` 图生图;默认保持工具调用等待直到任务完成,完成图片显示在工具调用对应的左侧结果区域,模型收到状态和附件引用,不会额外伪造用户消息。用户也可以使用 `/edit_image <修改描述>`,命令会直接读取当前对话最近图片并调用插件图片模型,不经过对话模型的图片能力检查。若明确需要后台执行,可传 `wait_for_completion: false`,之后再用 `get_image_generation_task` 查询;不要反复轮询。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务。用户提到「生图 / 绘画 / 生成图片 / 文生图 / 图生图」时即指本插件,请据此协作。";
3331
+ const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):通过「渠道」对接 OpenAI 兼容图像生成 API(每个渠道 = 一个 API 端点 + 各自的模型目录),支持文生图(/images/generations)与图生图(/images/edits,上传参考图,grok-imagine 模型按官方 JSON image_url 协议发送,nanobanana 系列按 aspect_ratio / image_size 参数协议发送;seedream 系列统一走 /images/generations,参考图以 JSON image 数组发送;智谱 `glm-image` 使用官方 `/api/paas/v4/images/generations`,当前仅支持文生图;qwen-image 系列使用阿里云 DashScope 原生接口(api_url 填 https://dashscope.aliyuncs.com/api/v1,不支持 OpenAI 兼容模式,该渠道不可复用于提示词增强,尺寸自动映射为宽*高)。MiniMax `image-01` 使用 MiniMax 原生 `/image_generation` 接口(api_url 填 https://api.minimax.io/v1 或国内站 https://api.minimaxi.com/v1,支持 1:1/16:9/4:3/3:2/2:3/3:4/9:16/21:9 宽高比,一次最多 9 张;图生图为单张 subject_reference 主体参考(保持人物/主体一致,非像素级局部编辑);其 /models 只列聊天模型,图片模型需用预设目录)。API 地址与密钥在 GUI 设置中按渠道配置,密钥存于 $DSH_HOME/.credentials.yaml 的 DSH_IMAGEGEN_CHANNEL_* 引用,不写入插件配置行;生成请求由本地宿主代理转发,结果以 base64 返回。模型只能使用用户在各渠道配置目录中的模型;检测模型时会过滤聊天、Embedding 等非图片模型,但模型出现在 /models 中仍不等于其网关原生支持生图协议,遇到 Qwen、MiniMax、Gemini 等非 OpenAI 生图协议时应如实说明上游兼容性。Agent 可直接调用 `generate_image` 提交文生图,也可用 `edit_image` 图生图;默认保持工具调用等待直到任务完成,完成图片显示在工具调用对应的左侧结果区域,模型收到状态和附件引用,不会额外伪造用户消息。用户也可以使用 `/edit_image <修改描述>`,命令会直接读取当前对话最近图片并调用插件图片模型,不经过对话模型的图片能力检查。若明确需要后台执行,可传 `wait_for_completion: false`,之后再用 `get_image_generation_task` 查询;不要反复轮询。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在 $DSH_HOME/.credentials.yaml;参考图会发送至所配置的 API 服务。用户提到「生图 / 绘画 / 生成图片 / 文生图 / 图生图」时即指本插件,请据此协作。";
3136
3332
  /** Append the live channel × model table so an Agent can honor user choices. */
3137
3333
  function guidanceFor(channels, defaultChannelId) {
3138
3334
  if (channels.length === 0) return `${IMAGEGEN_GUIDANCE} 尚未配置任何渠道:请先在「设置 → 插件 → AI 生图」添加渠道并填写 API 地址与密钥。`;
@@ -3184,28 +3380,56 @@ function normalizeChannels(value) {
3184
3380
  */
3185
3381
  function apply(ctx, config) {
3186
3382
  let current = () => config?.get() ?? {};
3383
+ /**
3384
+ * Synchronous mirror of the credential values this plugin needs.
3385
+ *
3386
+ * `resolve()` is synchronous and is called from route handlers, the Agent
3387
+ * tools and the runtime, while the credential seam resolves asynchronously.
3388
+ * Rather than making every call site async, the values are read once into
3389
+ * this map and refreshed on every credential change: the seam emits
3390
+ * `credentials/reference-updated` after a write commits and after each file
3391
+ * reload, so the mirror tracks the store without polling.
3392
+ *
3393
+ * A missing provider leaves the map empty, which reads as "no key" — the same
3394
+ * outcome as an unset ref, and it keeps the plugin mountable on a deployment
3395
+ * that composes no credential provider.
3396
+ */
3397
+ const keyMirror = /* @__PURE__ */ new Map();
3398
+ const credentials = () => ctx.get("credentials");
3399
+ /** Re-read one reference into the mirror; a missing provider or ref clears it. */
3400
+ const refreshKey = async (ref) => {
3401
+ const provider = credentials();
3402
+ if (provider === void 0) {
3403
+ keyMirror.delete(ref);
3404
+ return;
3405
+ }
3406
+ const value = (await provider.resolve(credentialRef(ref)))?.value ?? "";
3407
+ if (value === "") keyMirror.delete(ref);
3408
+ else keyMirror.set(ref, value);
3409
+ };
3410
+ /** Re-read every reference this plugin can name (all channels + the prompt key). */
3411
+ const refreshAllKeys = async () => {
3412
+ const refs = /* @__PURE__ */ new Set([PROMPT_KEY_REF]);
3413
+ for (const channel of normalizeChannels(current()?.channels)) refs.add(channelKeyRef(channel.id));
3414
+ await Promise.all([...refs].map((ref) => refreshKey(ref)));
3415
+ };
3187
3416
  const resolve = () => {
3188
3417
  const value = current() ?? {};
3189
3418
  let channels = normalizeChannels(value.channels);
3190
- const secrets = { ...value.channelSecrets ?? {} };
3191
3419
  if (channels.length === 0) {
3192
3420
  const legacyUrl = typeof value.apiUrl === "string" ? value.apiUrl.trim() : "";
3193
3421
  const legacyModels = Array.isArray(value.imageModels) ? value.imageModels.filter((model) => typeof model === "string" && model.trim() !== "").map((model) => ({
3194
3422
  alias: model.trim(),
3195
3423
  id: model.trim()
3196
3424
  })) : [];
3197
- if (legacyUrl !== "" || legacyModels.length > 0) {
3198
- channels = [{
3199
- id: "default",
3200
- preset: "",
3201
- name: "默认渠道",
3202
- apiUrl: legacyUrl,
3203
- apiUrlFull: false,
3204
- models: legacyModels
3205
- }];
3206
- const legacyKey = typeof value.apiKey === "string" ? value.apiKey.trim() : "";
3207
- if (legacyKey !== "") secrets["default"] = legacyKey;
3208
- }
3425
+ if (legacyUrl !== "" || legacyModels.length > 0) channels = [{
3426
+ id: "default",
3427
+ preset: "",
3428
+ name: "默认渠道",
3429
+ apiUrl: legacyUrl,
3430
+ apiUrlFull: false,
3431
+ models: legacyModels
3432
+ }];
3209
3433
  }
3210
3434
  const named = channels.map((channel) => ({
3211
3435
  ...channel,
@@ -3218,11 +3442,11 @@ function apply(ctx, config) {
3218
3442
  allowAgentImageGeneration: value.allowAgentImageGeneration ?? DEFAULT_ALLOW_AGENT_IMAGE_GENERATION,
3219
3443
  channels: named.map((channel) => ({
3220
3444
  ...channel,
3221
- apiKey: typeof secrets[channel.id] === "string" ? secrets[channel.id] : ""
3445
+ apiKey: keyMirror.get(channelKeyRef(channel.id)) ?? ""
3222
3446
  })),
3223
3447
  defaultChannelId,
3224
3448
  promptApiUrl: typeof value.promptApiUrl === "string" ? value.promptApiUrl.trim() : "",
3225
- promptApiKey: typeof value.promptApiKey === "string" ? value.promptApiKey.trim() : "",
3449
+ promptApiKey: keyMirror.get("DSH_IMAGEGEN_PROMPT_KEY") ?? "",
3226
3450
  promptModel: typeof value.promptModel === "string" ? value.promptModel.trim() : ""
3227
3451
  };
3228
3452
  };
@@ -3237,9 +3461,23 @@ function apply(ctx, config) {
3237
3461
  const pendingConversationImages = /* @__PURE__ */ new Map();
3238
3462
  ctx.inject(["settings", "attachments"], (sctx) => {
3239
3463
  const seam = sctx.get("settings");
3464
+ const credentialsSeam = () => {
3465
+ const provider = credentials();
3466
+ if (provider === void 0) return void 0;
3467
+ return {
3468
+ holds: async (ref) => (await provider.describe(credentialRef(ref))).configured,
3469
+ set: async (ref, value) => {
3470
+ await provider.set(credentialRef(ref), value);
3471
+ },
3472
+ unset: async (ref) => {
3473
+ await provider.unset(credentialRef(ref));
3474
+ }
3475
+ };
3476
+ };
3240
3477
  sctx.effect(() => {
3241
3478
  const disposers = makeRoutes({
3242
3479
  settings: seam,
3480
+ credentials: credentialsSeam,
3243
3481
  resolve: () => {
3244
3482
  const value = resolve();
3245
3483
  const channel = value.channels.find((candidate) => candidate.id === value.defaultChannelId) ?? value.channels[0];
@@ -3316,11 +3554,22 @@ function apply(ctx, config) {
3316
3554
  installSettingsSectionCompat(ctx, ImageGenSettingsNamespace, Config, config?.get() ?? {}, {
3317
3555
  setSource: (source) => {
3318
3556
  current = source;
3319
- sync();
3557
+ refreshAllKeys().then(sync);
3320
3558
  },
3321
- onChange: sync
3559
+ onChange: () => {
3560
+ refreshAllKeys().then(sync);
3561
+ }
3562
+ });
3563
+ ctx.inject(["credentials"], (cctx) => {
3564
+ cctx.effect(() => {
3565
+ const dispose = cctx.on("credentials/reference-updated", () => {
3566
+ refreshAllKeys().then(sync);
3567
+ });
3568
+ refreshAllKeys().then(sync);
3569
+ return dispose;
3570
+ }, "dsh-imagegen: credential mirror");
3322
3571
  });
3323
3572
  sync();
3324
3573
  }
3325
3574
  //#endregion
3326
- export { Config, IMAGEGEN_GUIDANCE, ImageGenError, ImageGenSettingsNamespace, ImageGenerationRuntime, apply, generateImage, inject, latestSessionImage, makeRoutes, name, promptCharLimit, registerAgentImageTools, registerEditImageCommand };
3575
+ export { Config, IMAGEGEN_GUIDANCE, ImageGenError, ImageGenSettingsNamespace, ImageGenerationRuntime, PROMPT_KEY_REF, apply, channelKeyRef, generateImage, inject, latestSessionImage, makeRoutes, name, promptCharLimit, registerAgentImageTools, registerEditImageCommand };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@logictan/dsh-imagegen",
3
3
  "description": "Chat-invoked AI image generation for the dsh web GUI: text-to-image and image-to-image through configurable provider channels (gpt-image-2 / grok-imagine-image / nanobanana series / seedream-5.0-pro / dall-e-3, with native xAI Grok Imagine, Google Nano Banana and ByteDance Seedream request shaping) and per-channel model catalogs.",
4
- "version": "2.0.2",
4
+ "version": "2.0.3",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {