@logictan/dsh-imagegen 2.0.1 → 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/README.md +1 -1
- package/lib/client.js +120 -118
- package/lib/client.js.map +1 -1
- package/lib/index.js +280 -31
- package/package.json +19 -19
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
|
-
...
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
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:
|
|
3445
|
+
apiKey: keyMirror.get(channelKeyRef(channel.id)) ?? ""
|
|
3222
3446
|
})),
|
|
3223
3447
|
defaultChannelId,
|
|
3224
3448
|
promptApiUrl: typeof value.promptApiUrl === "string" ? value.promptApiUrl.trim() : "",
|
|
3225
|
-
promptApiKey:
|
|
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
|
-
|
|
3557
|
+
refreshAllKeys().then(sync);
|
|
3320
3558
|
},
|
|
3321
|
-
onChange:
|
|
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.
|
|
4
|
+
"version": "2.0.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -33,23 +33,23 @@
|
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
36
|
-
"@deepseek-ai/dsh-api-remotes": "0.
|
|
37
|
-
"@deepseek-ai/dsh-api-session-controller": "0.
|
|
38
|
-
"@deepseek-ai/dsh-attachment": "0.
|
|
39
|
-
"@deepseek-ai/dsh-client-connection": "0.
|
|
40
|
-
"@deepseek-ai/dsh-client-locale": "0.
|
|
41
|
-
"@deepseek-ai/dsh-client-store": "0.
|
|
42
|
-
"@deepseek-ai/dsh-client-ui-conversation": "0.
|
|
43
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.
|
|
44
|
-
"@deepseek-ai/dsh-client-ui-renderer": "0.
|
|
45
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.
|
|
46
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.
|
|
47
|
-
"@deepseek-ai/dsh-commands": "0.
|
|
48
|
-
"@deepseek-ai/dsh-host-webserver": "0.
|
|
49
|
-
"@deepseek-ai/dsh-llm": "0.
|
|
50
|
-
"@deepseek-ai/dsh-settings": "0.
|
|
51
|
-
"@deepseek-ai/dsh-system-prompt": "0.
|
|
52
|
-
"@deepseek-ai/dsh-tools": "0.
|
|
36
|
+
"@deepseek-ai/dsh-api-remotes": "^0.2.0-rc.2",
|
|
37
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.2.0-rc.2",
|
|
38
|
+
"@deepseek-ai/dsh-attachment": "^0.2.0-rc.2",
|
|
39
|
+
"@deepseek-ai/dsh-client-connection": "^0.2.0-rc.2",
|
|
40
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.0-rc.2",
|
|
41
|
+
"@deepseek-ai/dsh-client-store": "^0.2.0-rc.2",
|
|
42
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.2.0-rc.2",
|
|
43
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.2.0-rc.2",
|
|
44
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.2",
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh-commands": "^0.2.0-rc.2",
|
|
48
|
+
"@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.2",
|
|
49
|
+
"@deepseek-ai/dsh-llm": "^0.2.0-rc.2",
|
|
50
|
+
"@deepseek-ai/dsh-settings": "^0.2.0-rc.2",
|
|
51
|
+
"@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.2",
|
|
52
|
+
"@deepseek-ai/dsh-tools": "^0.2.0-rc.2",
|
|
53
53
|
"@types/node": "^22.20.0",
|
|
54
54
|
"@types/react": "~18.3.1",
|
|
55
55
|
"@types/react-dom": "^18.3.5",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"tsdown": "0.22.2",
|
|
61
61
|
"typescript": "~5.7.2",
|
|
62
62
|
"@deepseek-ai/schemastery": "^3.18.4",
|
|
63
|
-
"@deepseek-ai/dsh-credentials": "0.
|
|
63
|
+
"@deepseek-ai/dsh-credentials": "^0.2.0-rc.2"
|
|
64
64
|
},
|
|
65
65
|
"files": [
|
|
66
66
|
"lib",
|