@dickpy/dsh-imagegen 1.5.2 → 1.5.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 +2 -1
- package/lib/client.js +808 -323
- package/lib/client.js.map +1 -1
- package/lib/index.js +166 -2
- package/package.json +1 -1
- package/src/client/ImageGenPanel.tsx +5 -0
- package/src/client/SettingsCard.tsx +9 -3
- package/src/client/helpers.ts +71 -33
- package/src/client/index.ts +59 -7
- package/src/client/locales.ts +385 -2
- package/src/client/use-language.ts +14 -0
- package/src/engine.ts +148 -0
- package/src/index.ts +1 -1
- package/src/model-catalog.ts +10 -1
- package/src/presets.ts +15 -0
- package/src/protocol.ts +1 -1
package/lib/index.js
CHANGED
|
@@ -40,7 +40,7 @@ function installSettingsSectionCompat(ctx, ns, schema, entry, hooks) {
|
|
|
40
40
|
/** Settings namespace this plugin owns (host settings seam + bridge). */
|
|
41
41
|
const IMAGEGEN_SETTINGS_NAMESPACE = "dsh-imagegen";
|
|
42
42
|
/** Published package version shared by the host updater and the client UI. */
|
|
43
|
-
const PLUGIN_VERSION = "1.5.
|
|
43
|
+
const PLUGIN_VERSION = "1.5.3";
|
|
44
44
|
/** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
|
|
45
45
|
const SETTINGS_API = {
|
|
46
46
|
describe: "/api/dsh-imagegen/settings/describe",
|
|
@@ -206,6 +206,14 @@ const ENTRIES = {
|
|
|
206
206
|
supportsEdit: false,
|
|
207
207
|
supportsAspectRatio: false,
|
|
208
208
|
qualityTiers: ["HD"]
|
|
209
|
+
},
|
|
210
|
+
qwen: {
|
|
211
|
+
label: "qwen-image",
|
|
212
|
+
labelZh: "千问图像",
|
|
213
|
+
known: true,
|
|
214
|
+
supportsEdit: true,
|
|
215
|
+
supportsAspectRatio: true,
|
|
216
|
+
qualityTiers: ["auto"]
|
|
209
217
|
}
|
|
210
218
|
};
|
|
211
219
|
/** Official Gemini image ids served by Nano Banana gateways. */
|
|
@@ -244,6 +252,10 @@ function describeModel(model) {
|
|
|
244
252
|
family: "zhipu",
|
|
245
253
|
...ENTRIES.zhipu
|
|
246
254
|
};
|
|
255
|
+
if (/^qwen-image(?:[-_.]|$)/i.test(id)) return {
|
|
256
|
+
family: "qwen",
|
|
257
|
+
...ENTRIES.qwen
|
|
258
|
+
};
|
|
247
259
|
return {
|
|
248
260
|
family: "unknown",
|
|
249
261
|
label: "unknown",
|
|
@@ -471,6 +483,12 @@ function isSeedream(model) {
|
|
|
471
483
|
function isZhipuImage(model) {
|
|
472
484
|
return modelFamily(model) === "zhipu";
|
|
473
485
|
}
|
|
486
|
+
/** Whether the model is Alibaba Qwen-Image, which speaks the DashScope native
|
|
487
|
+
* multimodal-generation contract (NOT OpenAI-compatible): a chat-style
|
|
488
|
+
* messages body, `宽*高` pixel sizes, and image URLs in the reply content. */
|
|
489
|
+
function isQwenImage(model) {
|
|
490
|
+
return modelFamily(model) === "qwen";
|
|
491
|
+
}
|
|
474
492
|
function isGlmImage(model) {
|
|
475
493
|
return /^glm-image(?:-|$)/i.test(model.trim());
|
|
476
494
|
}
|
|
@@ -483,6 +501,38 @@ function seedreamSize(quality) {
|
|
|
483
501
|
if (quality === "1k") return "1K";
|
|
484
502
|
return "2K";
|
|
485
503
|
}
|
|
504
|
+
/** The panel's aspect ratios mapped to Qwen-Image's `宽*高` pixel sizes.
|
|
505
|
+
* The classic series (qwen-image / -plus / -max) documents this fixed list;
|
|
506
|
+
* 2.0 / 3.0-series models accept any size within their pixel budget and
|
|
507
|
+
* recommend the larger set. */
|
|
508
|
+
const QWEN_SIZE_CLASSIC = {
|
|
509
|
+
"16:9": "1664*928",
|
|
510
|
+
"21:9": "1664*928",
|
|
511
|
+
"4:3": "1472*1104",
|
|
512
|
+
"3:2": "1472*1104",
|
|
513
|
+
"1:1": "1328*1328",
|
|
514
|
+
"3:4": "1104*1472",
|
|
515
|
+
"2:3": "1104*1472",
|
|
516
|
+
"9:16": "928*1664"
|
|
517
|
+
};
|
|
518
|
+
const QWEN_SIZE_HD = {
|
|
519
|
+
"16:9": "2688*1536",
|
|
520
|
+
"21:9": "2688*1536",
|
|
521
|
+
"4:3": "2368*1728",
|
|
522
|
+
"3:2": "2368*1728",
|
|
523
|
+
"1:1": "2048*2048",
|
|
524
|
+
"3:4": "1728*2368",
|
|
525
|
+
"2:3": "1728*2368",
|
|
526
|
+
"9:16": "1536*2688"
|
|
527
|
+
};
|
|
528
|
+
/** Versioned ids (qwen-image-2.0 / -3.0-pro / …) take the large size set. */
|
|
529
|
+
function isVersionedQwenImage(model) {
|
|
530
|
+
return /^qwen-image-\d+\.\d/i.test(model.trim());
|
|
531
|
+
}
|
|
532
|
+
function qwenSize(model, ratio) {
|
|
533
|
+
if (ratio === "" || ratio === "auto") return void 0;
|
|
534
|
+
return (isVersionedQwenImage(model) ? QWEN_SIZE_HD : QWEN_SIZE_CLASSIC)[ratio];
|
|
535
|
+
}
|
|
486
536
|
/** The panel's aspect ratios mapped to the closest OpenAI pixel size
|
|
487
537
|
* (gpt-image-2 / generic OpenAI-compatible endpoints). */
|
|
488
538
|
const OPENAI_SIZE_BY_RATIO = {
|
|
@@ -759,6 +809,83 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
|
|
|
759
809
|
}));
|
|
760
810
|
}
|
|
761
811
|
/**
|
|
812
|
+
* Qwen-Image (DashScope native multimodal-generation): one chat-style request
|
|
813
|
+
* carries the prompt (plus the reference image for edit mode) and answers
|
|
814
|
+
* synchronously with image URLs in the reply content. The versioned series
|
|
815
|
+
* batches natively (n ≤ 6; the panel caps at 4), the classic series is
|
|
816
|
+
* single-image per call.
|
|
817
|
+
*/
|
|
818
|
+
async function generateQwenImage(baseUrl, upstream, request, options) {
|
|
819
|
+
const model = wireModel(request);
|
|
820
|
+
const content = [];
|
|
821
|
+
if (request.mode === "edit") {
|
|
822
|
+
if (typeof request.image !== "string" || request.image === "") throw new ImageGenError("图生图需要上传参考图片", "edit-image-missing");
|
|
823
|
+
const parsed = parseDataUrl(request.image);
|
|
824
|
+
if (parsed === void 0) throw new ImageGenError("参考图片格式无效", "edit-image-invalid");
|
|
825
|
+
if (Buffer.from(parsed.base64, "base64").byteLength > MAX_EDIT_IMAGE_BYTES) throw new ImageGenError("参考图片超过 10MB 上限", "edit-image-too-large");
|
|
826
|
+
content.push({ image: request.image });
|
|
827
|
+
}
|
|
828
|
+
content.push({ text: request.prompt });
|
|
829
|
+
const count = isVersionedQwenImage(model) ? clampCount(request.n) : 1;
|
|
830
|
+
const size = qwenSize(model, request.size);
|
|
831
|
+
const body = {
|
|
832
|
+
model,
|
|
833
|
+
input: { messages: [{
|
|
834
|
+
role: "user",
|
|
835
|
+
content
|
|
836
|
+
}] },
|
|
837
|
+
parameters: {
|
|
838
|
+
...size !== void 0 ? { size } : {},
|
|
839
|
+
...count > 1 ? { n: count } : {}
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
const budget = requestSignal(options.signal, UPSTREAM_TIMEOUT_MS);
|
|
843
|
+
let response;
|
|
844
|
+
try {
|
|
845
|
+
response = await fetch(`${baseUrl}/services/aigc/multimodal-generation/generation`, {
|
|
846
|
+
method: "POST",
|
|
847
|
+
headers: {
|
|
848
|
+
authorization: `Bearer ${upstream.apiKey.trim()}`,
|
|
849
|
+
"content-type": "application/json"
|
|
850
|
+
},
|
|
851
|
+
body: JSON.stringify(body),
|
|
852
|
+
signal: budget.signal
|
|
853
|
+
});
|
|
854
|
+
} catch (error) {
|
|
855
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
856
|
+
if (/aborter/i.test(message) || /timeout/i.test(message)) throw new ImageGenError("上游接口响应超时(240 秒)", "upstream-timeout");
|
|
857
|
+
throw new ImageGenError(`无法连接上游接口:${message}`, "upstream-unreachable");
|
|
858
|
+
} finally {
|
|
859
|
+
budget.dispose();
|
|
860
|
+
}
|
|
861
|
+
let payload;
|
|
862
|
+
try {
|
|
863
|
+
payload = await response.json();
|
|
864
|
+
} catch {
|
|
865
|
+
throw new ImageGenError(`上游接口返回了非 JSON 响应(HTTP ${response.status})`, "upstream-invalid");
|
|
866
|
+
}
|
|
867
|
+
if (!response.ok || payload === null || typeof payload !== "object") throw new ImageGenError(upstreamMessage(payload, response.status), "upstream-rejected");
|
|
868
|
+
const output = payload.output;
|
|
869
|
+
const choices = output !== void 0 && Array.isArray(output.choices) ? output.choices : [];
|
|
870
|
+
const urls = [];
|
|
871
|
+
for (const choice of choices) {
|
|
872
|
+
const message = choice !== null && typeof choice === "object" ? choice.message : void 0;
|
|
873
|
+
const items = message !== null && typeof message === "object" && Array.isArray(message.content) ? message.content : [];
|
|
874
|
+
for (const item of items) if (item !== null && typeof item === "object") {
|
|
875
|
+
const image = item.image;
|
|
876
|
+
if (typeof image === "string" && image !== "") urls.push(image);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (urls.length === 0) throw new ImageGenError("上游响应缺少图片内容", "upstream-empty");
|
|
880
|
+
return { images: await Promise.all(urls.map(async (url) => {
|
|
881
|
+
const normalized = await normalizeItem({ url }, upstream);
|
|
882
|
+
return {
|
|
883
|
+
b64: normalized.b64,
|
|
884
|
+
mime: normalized.mime
|
|
885
|
+
};
|
|
886
|
+
})) };
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
762
889
|
* Forward one generate request to the configured endpoint. The requested image
|
|
763
890
|
* count is satisfied with N parallel single-image requests (the `n` batch
|
|
764
891
|
* parameter is never sent, because Responses-API-based gateways reject it as
|
|
@@ -768,6 +895,7 @@ async function generateImage(upstream, request, options = {}) {
|
|
|
768
895
|
const baseUrl = upstream.apiUrl.trim().replace(/\/+$/, "");
|
|
769
896
|
if (baseUrl === "") throw new ImageGenError("api_url 未配置:请先在「设置 → 插件 → 可配置」中填写", "config-missing");
|
|
770
897
|
if (upstream.apiKey.trim() === "") throw new ImageGenError("api_key 未配置:请先在「设置 → 插件 → 可配置」中填写", "config-missing");
|
|
898
|
+
if (isQwenImage(wireModel(request))) return generateQwenImage(baseUrl, upstream, request, options);
|
|
771
899
|
if (request.mode === "edit" && isZhipuImage(wireModel(request))) throw new ImageGenError("智谱 GLM-Image 当前仅支持文生图,请切换到文生图模式或选择支持图生图的模型", "edit-unsupported");
|
|
772
900
|
const params = effectiveParams(request);
|
|
773
901
|
const count = effectiveCount(request);
|
|
@@ -2001,6 +2129,42 @@ const IMAGE_PRESETS = [
|
|
|
2001
2129
|
id: "glm-image"
|
|
2002
2130
|
}]
|
|
2003
2131
|
},
|
|
2132
|
+
{
|
|
2133
|
+
id: "aliyun-dashscope-qwen",
|
|
2134
|
+
name: "阿里云百炼(Qwen-Image)",
|
|
2135
|
+
apiUrl: "https://dashscope.aliyuncs.com/api/v1",
|
|
2136
|
+
hint: "阿里云百炼 DashScope 原生接口:通义千问 Qwen-Image 系列(该渠道不可复用于提示词增强)",
|
|
2137
|
+
models: [
|
|
2138
|
+
{
|
|
2139
|
+
alias: "qwen-image-3.0-pro",
|
|
2140
|
+
id: "qwen-image-3.0-pro"
|
|
2141
|
+
},
|
|
2142
|
+
{
|
|
2143
|
+
alias: "qwen-image-3.0",
|
|
2144
|
+
id: "qwen-image-3.0"
|
|
2145
|
+
},
|
|
2146
|
+
{
|
|
2147
|
+
alias: "qwen-image-2.0-pro",
|
|
2148
|
+
id: "qwen-image-2.0-pro"
|
|
2149
|
+
},
|
|
2150
|
+
{
|
|
2151
|
+
alias: "qwen-image-2.0",
|
|
2152
|
+
id: "qwen-image-2.0"
|
|
2153
|
+
},
|
|
2154
|
+
{
|
|
2155
|
+
alias: "qwen-image-max",
|
|
2156
|
+
id: "qwen-image-max"
|
|
2157
|
+
},
|
|
2158
|
+
{
|
|
2159
|
+
alias: "qwen-image-plus",
|
|
2160
|
+
id: "qwen-image-plus"
|
|
2161
|
+
},
|
|
2162
|
+
{
|
|
2163
|
+
alias: "qwen-image",
|
|
2164
|
+
id: "qwen-image"
|
|
2165
|
+
}
|
|
2166
|
+
]
|
|
2167
|
+
},
|
|
2004
2168
|
{
|
|
2005
2169
|
id: "xai-grok",
|
|
2006
2170
|
name: "xAI(Grok)",
|
|
@@ -3881,7 +4045,7 @@ const DEFAULT_ALLOW_AGENT_IMAGE_GENERATION = true;
|
|
|
3881
4045
|
/** Order of the announcement section within the tool-guidance band. */
|
|
3882
4046
|
const SECTION_ORDER = 150;
|
|
3883
4047
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
3884
|
-
const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「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
|
|
4048
|
+
const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「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 兼容模式,该渠道不可复用于提示词增强,尺寸自动映射为宽*高)。API 地址与密钥在 GUI 设置中按渠道配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载。模型只能使用用户在各渠道配置目录中的模型;检测模型时会过滤聊天、Embedding 等非图片模型,但模型出现在 /models 中仍不等于其网关原生支持生图协议,遇到 Qwen、Gemini 等非 OpenAI 生图协议时应如实说明上游兼容性。可一键把满意的图片加入「画廊」。内置「提示词模板库」(面板提示词框左下角「模板库」按钮):多来源标签页(精选案例库 / 沧河案例库,后续可扩展),打包 awesome-gpt-image-2 的数百条提示词案例,可搜索、筛选、收藏(星标,宿主持久化)与复用;各来源列表独立刷新,宿主每 12 小时后台自动同步一次。Agent 可直接调用 `generate_image` 提交文生图,也可用 `edit_image` 图生图;默认保持工具调用等待直到任务完成,完成图片显示在工具调用对应的左侧结果区域,模型收到状态和附件引用,不会额外伪造用户消息。用户也可以使用 `/edit_image <修改描述>`,命令会直接读取当前对话最近图片并调用插件图片模型,不经过对话模型的图片能力检查。若明确需要后台执行,可传 `wait_for_completion: false`,之后再用 `get_image_generation_task` 查询;不要反复轮询。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务;模板库在线刷新与参考图首次加载需要访问对应来源站点(vibeui.top / gpt-image2.canghe.ai)。用户提到「生图 / 绘画 / 生成图片 / 文生图 / 图生图 / 画廊 / 提示词模板」时即指本插件,请据此协作。";
|
|
3885
4049
|
/** Append the live channel × model table so an Agent can honor user choices. */
|
|
3886
4050
|
function guidanceFor(channels, defaultChannelId) {
|
|
3887
4051
|
if (channels.length === 0) return `${IMAGEGEN_GUIDANCE} 尚未配置任何渠道:请先在「设置 → 插件 → AI 生图」添加渠道并填写 API 地址与密钥。`;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dickpy/dsh-imagegen",
|
|
3
3
|
"description": "AI image generation plugin 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), with per-channel model catalogs and a New Session / Image Generation tab entry opening a three-column studio beside the native conversation.",
|
|
4
|
-
"version": "1.5.
|
|
4
|
+
"version": "1.5.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -18,6 +18,7 @@ import type { ImageGenApi } from './api.ts'
|
|
|
18
18
|
import { errorMessage, tt } from './helpers.ts'
|
|
19
19
|
import { TemplateLibrary } from './TemplateLibrary.tsx'
|
|
20
20
|
import { InspirationGallery } from './InspirationGallery.tsx'
|
|
21
|
+
import { useImageGenLanguageTick } from './use-language.ts'
|
|
21
22
|
import type { EcommerceRefRole, GeneratedImage, GenerateMode, GenerateRequest, GenerationTask, GenerationTaskStatus, HistoryEntry, HistoryImageRef, ProductSetDraft, ProductSetSlot, UpdateInfo } from '../protocol.ts'
|
|
22
23
|
import { AGENT_IMAGE_API } from '../protocol.ts'
|
|
23
24
|
import type { ImageGenConfig, ImageGenScope } from './settings-scope.ts'
|
|
@@ -440,6 +441,10 @@ export function ImageGenPanel(props: {
|
|
|
440
441
|
}) {
|
|
441
442
|
const { api, scope, sessions, conversation } = props
|
|
442
443
|
const config = useConfig(scope)
|
|
444
|
+
// The plugin language follows the DSH interface (bridged from ctx.locale);
|
|
445
|
+
// this tick re-renders the tree so every tt() switches live — the template
|
|
446
|
+
// library and the inspiration wall render inside this tree.
|
|
447
|
+
useImageGenLanguageTick()
|
|
443
448
|
const enabled = config?.enabled ?? true
|
|
444
449
|
// Channel-aware model options: the panel lists every configured alias
|
|
445
450
|
// (default channel first); legacy flat fields remain the upgrade fallback.
|
|
@@ -21,6 +21,8 @@ import type { ImageGenScope } from './settings-scope.ts'
|
|
|
21
21
|
import { describeModel } from '../model-catalog.ts'
|
|
22
22
|
import { IMAGE_MODEL_API, PRESETS_API, PROMPT_ENHANCE_API, USAGE_API, type ModelMapping, type PresetProviderView } from '../protocol.ts'
|
|
23
23
|
import type { ImageGenKey } from './locales.ts'
|
|
24
|
+
import { tt, type TranslateValues } from './helpers.ts'
|
|
25
|
+
import { useImageGenLanguageTick } from './use-language.ts'
|
|
24
26
|
import css from './settings-card.module.css'
|
|
25
27
|
|
|
26
28
|
/** The global (non-channel) fields this card's staged form edits. */
|
|
@@ -127,7 +129,11 @@ interface UsageCounters {
|
|
|
127
129
|
* @returns the card, or nothing while the namespace is still loading.
|
|
128
130
|
*/
|
|
129
131
|
export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
|
|
130
|
-
|
|
132
|
+
// The card renders through the plugin's own dictionary so the uiLanguage
|
|
133
|
+
// override applies here too — the host-locale props.t would only follow the
|
|
134
|
+
// DSH interface language.
|
|
135
|
+
const t = tt
|
|
136
|
+
useImageGenLanguageTick()
|
|
131
137
|
const state = props.useImageGenSettingsCard(snapshot => snapshot)
|
|
132
138
|
const [open, setOpen] = useState(false)
|
|
133
139
|
// Global-section local states (prompt enhancement etc.).
|
|
@@ -445,8 +451,8 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
|
|
|
445
451
|
offLabel={t('settings.off')}
|
|
446
452
|
{...fieldProps}
|
|
447
453
|
{...state.allowAgentImageGeneration}
|
|
448
|
-
onEdit={(text) => { props.edit('
|
|
449
|
-
onReset={() => { props.resetField('
|
|
454
|
+
onEdit={(text) => { props.edit('enabled', text) }}
|
|
455
|
+
onReset={() => { props.resetField('enabled') }}
|
|
450
456
|
/>
|
|
451
457
|
</div> : null}
|
|
452
458
|
<div className={css.footer}>
|
package/src/client/helpers.ts
CHANGED
|
@@ -1,33 +1,71 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared panel helpers: the active-dictionary pick
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Shared panel helpers: the active-dictionary pick bound to the dsh-imagegen
|
|
3
|
+
* interpolator, the plugin locale that follows the DSH interface language
|
|
4
|
+
* (bridged in client/index.ts from ctx.locale — the plugin ships zh / en / ru
|
|
5
|
+
* and registers itself as a DSH language pack for Русский), plus a small
|
|
6
|
+
* error-message extractor. All copy stays in the locale dictionaries.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { en, ru, zh, type ImageGenKey } from './locales.ts'
|
|
10
|
+
|
|
11
|
+
/** Template values accepted by the interpolator. */
|
|
12
|
+
export type TranslateValues = Record<string, string | number>
|
|
13
|
+
|
|
14
|
+
/** Languages with a shipped dictionary. */
|
|
15
|
+
export type ImageGenLanguage = 'zh' | 'en' | 'ru'
|
|
16
|
+
|
|
17
|
+
const DICTIONARIES: Record<ImageGenLanguage, Record<string, string>> = { zh, en, ru }
|
|
18
|
+
|
|
19
|
+
/** The active DSH locale mapped onto our dictionary (module-level, one value per app). */
|
|
20
|
+
let activeLocale: ImageGenLanguage = 'zh'
|
|
21
|
+
|
|
22
|
+
/** Bumped on every locale change; useSyncExternalStore version. */
|
|
23
|
+
let languageVersion = 0
|
|
24
|
+
|
|
25
|
+
const languageListeners = new Set<() => void>()
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Adopt the DSH interface language. Unknown ids (future language packs)
|
|
29
|
+
* resolve to English — the same per-key fallback convention the host locale
|
|
30
|
+
* chain uses.
|
|
31
|
+
*/
|
|
32
|
+
export function applyHostLocale(id: unknown): void {
|
|
33
|
+
const next: ImageGenLanguage = id === 'zh' || id === 'ru' ? id : 'en'
|
|
34
|
+
if (next === activeLocale) return
|
|
35
|
+
activeLocale = next
|
|
36
|
+
languageVersion += 1
|
|
37
|
+
for (const listener of [...languageListeners]) listener()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Monotonic version of the active locale (external-store snapshot). */
|
|
41
|
+
export function getImageGenLanguageVersion(): number {
|
|
42
|
+
return languageVersion
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Observe locale changes; returns the unsubscriber. */
|
|
46
|
+
export function subscribeImageGenLanguage(listener: () => void): () => void {
|
|
47
|
+
languageListeners.add(listener)
|
|
48
|
+
return () => { languageListeners.delete(listener) }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Active dictionary for the current DSH language. */
|
|
52
|
+
export function dictionary(): Record<string, string> {
|
|
53
|
+
return DICTIONARIES[activeLocale]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Translate a key with optional {name} template params (current language). */
|
|
57
|
+
export function tt(key: ImageGenKey, values?: TranslateValues): string {
|
|
58
|
+
const text = dictionary()[key] ?? key
|
|
59
|
+
if (values === undefined) return text
|
|
60
|
+
let rendered = text
|
|
61
|
+
for (const [name, value] of Object.entries(values)) {
|
|
62
|
+
rendered = rendered.replaceAll(`{${name}}`, String(value))
|
|
63
|
+
}
|
|
64
|
+
return rendered
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Human-readable error text from an unknown thrown value. */
|
|
68
|
+
export function errorMessage(error: unknown): string {
|
|
69
|
+
if (error instanceof Error) return error.message
|
|
70
|
+
return String(error)
|
|
71
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -11,18 +11,18 @@
|
|
|
11
11
|
* whole boot when a plugin apply throws, and an external plugin must not take
|
|
12
12
|
* the GUI down.
|
|
13
13
|
*/
|
|
14
|
-
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
|
15
|
-
import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client'
|
|
14
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
|
15
|
+
import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client'
|
|
16
16
|
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
|
17
17
|
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
|
|
18
|
-
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
19
|
-
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
|
20
|
-
// Type-only: pulls the LocaleNamespaceMap merge table.
|
|
18
|
+
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
19
|
+
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
|
20
|
+
// Type-only: pulls the LocaleNamespaceMap merge table.
|
|
21
21
|
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
22
22
|
import { ImageGenApi } from './api.ts'
|
|
23
23
|
import { ImageGenController } from './controller.ts'
|
|
24
|
-
import { tt } from './helpers.ts'
|
|
25
|
-
import { en, zh, type ImageGenKey } from './locales.ts'
|
|
24
|
+
import { tt, applyHostLocale } from './helpers.ts'
|
|
25
|
+
import { en, ru, zh, type ImageGenKey } from './locales.ts'
|
|
26
26
|
import { mountPanel } from './mount.tsx'
|
|
27
27
|
import { mountSidebarEntry } from './sidebar-entry.ts'
|
|
28
28
|
import { ImageGenSettingsCard, ImageGenSettingsCardController } from './SettingsCard.tsx'
|
|
@@ -68,7 +68,39 @@ export const inject = ['slots', 'locale', 'connection', 'sessions', 'conversatio
|
|
|
68
68
|
* @param ctx - client root context (services: slots, locale, connection).
|
|
69
69
|
*/
|
|
70
70
|
export function apply(ctx: ClientContext): void {
|
|
71
|
+
// The host locale service only knows zh/en dictionaries (its type is
|
|
72
|
+
// fixed); ru rides the untyped single-locale registration instead.
|
|
71
73
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-imagegen: dictionaries')
|
|
74
|
+
// Russian ships as a language pack: the dictionary lands in this plugin's
|
|
75
|
+
// namespace, and Русский joins the shared DSH language catalog (Settings →
|
|
76
|
+
// General → Language) with per-key fallback to English for host copy.
|
|
77
|
+
// Registration order/aggregation between register(NS, 'ru', …) and
|
|
78
|
+
// addLanguage differs across host builds and a duplicate throws — these
|
|
79
|
+
// surfaces must degrade silently, never fail the GUI boot.
|
|
80
|
+
ctx.effect(() => {
|
|
81
|
+
try {
|
|
82
|
+
return ctx.locale.register(NS, 'ru', ru)
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.warn('[dsh-imagegen] ru dictionary not registered:', error)
|
|
85
|
+
return () => {}
|
|
86
|
+
}
|
|
87
|
+
}, 'dsh-imagegen: ru dictionary')
|
|
88
|
+
ctx.effect(() => {
|
|
89
|
+
try {
|
|
90
|
+
if (ctx.locale.getLocale().locales.some(locale => locale.id === 'ru')) return () => {}
|
|
91
|
+
return ctx.locale.addLanguage({ id: 'ru', label: 'Русский', fallback: 'en' })
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.warn('[dsh-imagegen] ru language not added to the catalog:', error)
|
|
94
|
+
return () => {}
|
|
95
|
+
}
|
|
96
|
+
}, 'dsh-imagegen: ru language pack')
|
|
97
|
+
// Every plugin surface renders through tt(); bridge DSH locale switches
|
|
98
|
+
// into it so the whole plugin follows the interface language.
|
|
99
|
+
ctx.effect(() => {
|
|
100
|
+
const applyLocale = (): void => { applyHostLocale(ctx.locale.getLocale().active) }
|
|
101
|
+
applyLocale()
|
|
102
|
+
return ctx.locale.subscribe(applyLocale)
|
|
103
|
+
}, 'dsh-imagegen: follow host locale')
|
|
72
104
|
registerImageToolviews(ctx)
|
|
73
105
|
|
|
74
106
|
const connection = ctx.get('connection') as ConnectionHandle | undefined
|
|
@@ -119,6 +151,26 @@ export function apply(ctx: ClientContext): void {
|
|
|
119
151
|
tt('entry.tooltip'),
|
|
120
152
|
))
|
|
121
153
|
disposers.push(mountPanel(controller, api, scope, { sessions, conversation }))
|
|
154
|
+
// The imperative sidebar tabs render their labels once; relabel them on
|
|
155
|
+
// every DSH language switch so the entry follows the interface too.
|
|
156
|
+
disposers.push(ctx.locale.subscribe(() => {
|
|
157
|
+
const root = document.querySelector('[data-dsh-imagegen-sidebar-root]')
|
|
158
|
+
if (root === null) return
|
|
159
|
+
const labels: Array<[string, string, string]> = [
|
|
160
|
+
['new-session', tt('entry.newSession'), tt('entry.newSessionTooltip')],
|
|
161
|
+
['image', tt('entry.image'), tt('entry.tooltip')],
|
|
162
|
+
]
|
|
163
|
+
for (const [tab, label, tooltip] of labels) {
|
|
164
|
+
const button = root.querySelector<HTMLButtonElement>(`[data-dsh-imagegen-tab="${tab}"]`)
|
|
165
|
+
if (button === null) continue
|
|
166
|
+
button.setAttribute('aria-label', label)
|
|
167
|
+
button.setAttribute('title', tooltip)
|
|
168
|
+
const labelSpan = button.querySelector('span:nth-child(2)')
|
|
169
|
+
if (labelSpan !== null) labelSpan.textContent = label
|
|
170
|
+
}
|
|
171
|
+
const tablist = root.querySelector<HTMLDivElement>('[role="tablist"][data-dsh-imagegen-session-tabs]')
|
|
172
|
+
tablist?.setAttribute('aria-label', tt('entry.tooltip'))
|
|
173
|
+
}))
|
|
122
174
|
} catch (error) {
|
|
123
175
|
// DOM failures degrade the studio, never the GUI.
|
|
124
176
|
console.warn('[dsh-imagegen] mount failed:', error)
|