@ninkch/dsh-infinite-canvas 0.18.1 → 0.18.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.
@@ -39,19 +39,31 @@ B:……
39
39
  面板文字只允许 ≤4 字短标签(面板名 / 表情名 / 颜色名)。同一资产在所有分格中外观**完全一致**——
40
40
  **已连接参考图时,参考图形象是唯一事实**,逐格严格复制;未连接参考图时逐格复用设定集外观关键词。
41
41
 
42
- ### 角色设定图(16:9 横版——主图与三视图占大版面)
43
- - **版面主体(约三分之二)给画面**:中央大幅全身立绘主图(正面站姿),主图下方一排
44
- **三视图(正面 / 侧面 / 背面 全身)**——三视图是必须项,后续视频要靠它保证侧背面的准确形态。
45
- - **右侧窄栏做纯视觉辅助(不渲染文字段落)**:
46
- - 脸部特征:一格正面特写(或正 / 侧两小格);
47
- - 表情条:一行 4–6 个小头像,每格下方只标表情名(两字,按角色性格取舍,如 温柔 / 惊讶 / 坚定);
48
- - 细节特写(可选):1–2 格(项圈 / 挂牌 / 发饰),格内无文字。
49
- - **禁止**:姓名 / 性别 / 年龄等属性文本、服饰说明文字、旁白或诗句——这些属于设定集,
50
- 画进图里必成乱码,且会挤出角色形象的一致性注意力。
51
- - 所有分格中同一角色外观完全一致(以参考图形象为准逐格复制)。
52
- - 提示词结构示例:「角色人设图(16:9 横版设定集排版):中央大幅全身立绘,主图下方三视图一排
53
- (正面/侧面/背面全身);右侧窄栏:脸部特写一格、表情小头像一排(每格下方只标两字表情名);
54
- 画面中不出现任何成段文字。」
42
+ ### 角色设定图(16:9 横版——按「六段式」结构写提示词,实测出图最稳)
43
+
44
+ 提示词按以下六段组织,**方括号段名照写**(模型对显式分段结构的遵循度显著更好):
45
+
46
+ - **【总起】**一句话交代:16:9 横版专业设定集排版、深色简洁背景、风格基调(如 写实电影感)、
47
+ 同一角色在所有分格中外观完全一致。
48
+ - **【身份锚】**声明参考图是同一角色的真实照片:严格保持脸型 / 眼型 / 耳形 / 毛量 / 口鼻比例 / 体型,
49
+ 只改变姿势与视角;**忽略照片里的临时配饰**(狗绳、挂牌等),配饰一律按提示词文字描述绘制。
50
+ 绑定几张参考图就写几个 [图N]。
51
+ - **【主体】**中央大幅全身立绘(正面站姿):体型写量化并加反例(如「约 3 公斤、肩高 20 厘米;
52
+ 不是中型犬,不是萨摩耶/银狐/柴犬」——易混淆品种必须显式排除)、毛色毛发、五官、四肢尾巴,
53
+ 以及固定配饰的完整描述。
54
+ - **【主图下方·三视图(必须项)】**正面/侧面/背面三格全身站姿——同一角色只换角度,
55
+ 交代侧背形态(后续视频一致性靠它)。
56
+ - **【右侧窄栏·自上而下分块;每块内容全图只出现一次】**
57
+ ① 脸部特写一格 + 一行 ≤12 字的五官短标注;
58
+ ② 配饰格(有固定配饰时):正 / 背两张小图并排,可刻短字,并注明「该物在本图内只出现这一处」;
59
+ ③ 表情格:6 个头部小头像分两行(每行 3 个),每个下方只标两字表情名。
60
+ - **【文字规则】**图内文字极少:只保留两三个字的短标签,黑色细体、笔画清楚、字要写大;
61
+ 不写任何大标题、说明段或长句。
62
+ - **【画面白名单】**明确写出「画面中只有这一只角色和上述分格内容;没有人物、没有其他动物、
63
+ 没有乐器、没有文字水印」——反幻觉白名单实测能压住乱入元素。
64
+
65
+ 要点:**每块内容全图只出现一次**(配饰在配饰格画过,主图描述就不再重复强调它);
66
+ 反例约束有效——易混淆的品种 / 风格务必写进【主体】的排除句。
55
67
 
56
68
  ### 道具设定图(横竖皆可——按道具特性选,不限制)
57
69
  - **画幅选择**:长条形/单件道具(剑、箫、杖)竖版更能展示完整形态;
@@ -141,8 +141,10 @@ description: 用无限画布(Infinite Canvas)做短剧/视频创作时使用
141
141
  (命名规范「角色-<名> 设定图」/「场景-<名> 设定图」/「道具-<名> 设定图」),
142
142
  **prompt 必须按 `canvas-craft`「资产设定图版式」一节的排版模板写详细版式提示词,
143
143
  禁止只写一句三视图/外观关键词**——信息量不足,撑不起后续视频的角色一致性。要点(完整模板见 canvas-craft):
144
- - **角色设定图**(16:9 横版):**主图 + 三视图一排(正/侧/背全身,必须项)占大版面(约 2/3)**;
145
- 右窄栏纯视觉辅助(脸部特写一格 / 表情条一行小头像,每格只标两字表情名);细节格可选
144
+ - **角色设定图**(16:9 横版):按 canvas-craft 的「六段式」结构写——【总起】(版式/背景/风格/
145
+ 全格一致)→【身份锚】(参考图=唯一形象事实,忽略照片配饰)→【主体】(量化体型+反例排除)→
146
+ 【三视图·必须项】→【右侧窄栏:脸部特写 / 配饰格 / 表情两行×3,每格只标两字标签,
147
+ 每块内容全图只出现一次】→【文字规则 + 画面白名单】
146
148
  - **道具设定图**(横竖皆可,按道具特性选):顶部道具名大标题(≤6 字);中央主图(斜 45° 悬浮+氛围光);
147
149
  细节特写 2–3 格(格内无文字);侧栏侧视图;底部圆形色卡 3–5 枚(色名 ≤3 字)
148
150
  - **场景设定图**(主图+环绕小图):一张主场景大图占主体,周边环绕补充小图(多角度 / 其他时间段 / 细节格,3–6 张各配 ≤6 字短标注)——大致参考不强约束
package/client.js CHANGED
@@ -1377,7 +1377,10 @@ window.__ModuleLoader__.load({
1377
1377
  }
1378
1378
  return out;
1379
1379
  }
1380
- function saveSettings(s) { lsSet(LS_SETTINGS, s); }
1380
+ function saveSettings(s) {
1381
+ lsSet(LS_SETTINGS, s);
1382
+ notify(); // 设置即改即存(无保存按钮),但必须通知画布重渲染——否则节点上的模型下拉/星标一直是旧快照
1383
+ }
1381
1384
 
1382
1385
  // ---------- 供应商 + 模型(图片/视频):扁平视图与解析 ----------
1383
1386
  let legacyImageApiRefMap = {}; // 旧扁平配置 id → 'providerId|modelId'(节点字段懒迁移用)
@@ -2154,10 +2157,21 @@ window.__ModuleLoader__.load({
2154
2157
  let base = s.baseUrl.replace(/\/+$/, '').replace(/\/images\/generations$/, '');
2155
2158
  if (!base.replace(/^https?:\/\//i, '').includes('/')) base = base + '/v1';
2156
2159
  const refs = refBlobs || [];
2157
- const auth = s.apiKey ? { Authorization: 'Bearer ' + s.apiKey } : {};
2158
- const extra = parseExtraParams(s.extra); // 厂商专属参数(留空不发送)
2160
+ const auth = s.apiKey ? { Authorization: 'Bearer ' + String(s.apiKey).trim() } : {};
2161
+ // 节点级扩展参数(手动参数框)与供应商扩展参数合并(节点优先)
2162
+ const nodeExtra = String((opts && opts.nodeExtra) || '').trim();
2163
+ let extraSrc = String(s.extra || '');
2164
+ if (nodeExtra) {
2165
+ try {
2166
+ extraSrc = JSON.stringify({ ...(extraSrc.trim() ? JSON.parse(extraSrc) : {}), ...JSON.parse(nodeExtra) });
2167
+ } catch (e) { throw new Error('节点扩展参数需为 JSON 对象:' + ((e && e.message) || e)); }
2168
+ }
2169
+ const extra = parseExtraParams(extraSrc); // 厂商专属参数(留空不发送)
2159
2170
  // 优云智算(compshare)任务式:Qwen-Image 异步任务(提交 → 轮询 → output_url 下载)
2160
- if (s.mode === 'compshare') return await compshareImageTask(prompt, refs, s, opts);
2171
+ if (s.mode === 'compshare') return await compshareImageTask(prompt, refs, { ...s, extra: extraSrc }, opts);
2172
+ // 协议自动路由:优云智算的图片 API 只有任务式(/image/v1/tasks)——误配成通用模式
2173
+ // 会在 /v1/images/generations 上拿到 401 "invalid api key"(该面不存在),按域名自动切任务式
2174
+ try { if (/compshare\.cn$/i.test(new URL(base).hostname)) return await compshareImageTask(prompt, refs, { ...s, extra: extraSrc }, opts); } catch { /* URL 异常由后续流程报错 */ }
2161
2175
  const upstreamTimeout = Number(s.timeoutMs) > 0 ? Number(s.timeoutMs) : 120000; // 商汤等同步接口 20s+,默认 120s
2162
2176
  // 参考图编辑模式:multipart 上传到 /images/edits(OpenAI gpt-image-1 兼容)
2163
2177
  if (refs.length && s.mode === 'edits') {
@@ -2229,12 +2243,20 @@ window.__ModuleLoader__.load({
2229
2243
  // 参考图走三步上传(upload_urls → PUT 签名直传 → assets 登记)换 asset_id;
2230
2244
  // 登记结果缓存在画布资产元数据(compAssetIdCache):同一张 CAS 资产不重复上传。
2231
2245
  const compAssetIdCache = new Map(); // 画布资产 id → 优云 asset_id
2232
- /** 参考图上传前置:统一 MIME/扩展名并测真实像素尺寸(平台校验上传宽高必须与图片一致)。 */
2246
+ /** 参考图上传前置:魔数嗅探真实编码(平台仅收 JPG/PNG/WebP,且校验声明 MIME 与实际字节一致——
2247
+ * 改后缀的 AVIF/HEIC 会在登记时报 "Params [Width/Height] not available")+ 测真实像素尺寸。 */
2233
2248
  async function imageBlobMeta(input) {
2234
2249
  const blob = input.blob || input;
2235
- const type = (blob.type || 'image/png').toLowerCase();
2236
- const mime = /jpeg|jpg/.test(type) ? 'image/jpeg' : /webp/.test(type) ? 'image/webp' : 'image/png';
2237
- const ext = mime === 'image/jpeg' ? 'jpg' : mime === 'image/webp' ? 'webp' : 'png';
2250
+ const head = new Uint8Array(await blob.slice(0, 16).arrayBuffer());
2251
+ const isPng = head[0] === 0x89 && head[1] === 0x50 && head[2] === 0x4e && head[3] === 0x47;
2252
+ const isJpg = head[0] === 0xff && head[1] === 0xd8 && head[2] === 0xff;
2253
+ const isWebp = head[0] === 0x52 && head[1] === 0x49 && head[2] === 0x46 && head[3] === 0x46
2254
+ && head[8] === 0x57 && head[9] === 0x45 && head[10] === 0x42 && head[11] === 0x50;
2255
+ if (!isPng && !isJpg && !isWebp) {
2256
+ throw new Error('参考图编码不受支持:平台仅接受 JPG/PNG/WebP 原生编码(当前文件的实际字节不是这三类,常见于改后缀的 AVIF/HEIC)——请转成 JPG/PNG 后重新挂参考图');
2257
+ }
2258
+ const mime = isPng ? 'image/png' : isJpg ? 'image/jpeg' : 'image/webp';
2259
+ const ext = isPng ? 'png' : isJpg ? 'jpg' : 'webp';
2238
2260
  let width = 0;
2239
2261
  let height = 0;
2240
2262
  try {
@@ -2243,7 +2265,7 @@ window.__ModuleLoader__.load({
2243
2265
  height = bmp.height;
2244
2266
  try { bmp.close && bmp.close(); } catch { /* 忽略 */ }
2245
2267
  } catch { /* 测量失败交由下方显式报错 */ }
2246
- if (!width || !height) throw new Error('参考图尺寸测量失败(仅支持 PNG/JPG/WebP)');
2268
+ if (!width || !height) throw new Error('参考图尺寸测量失败');
2247
2269
  return { blob, mime, ext, width, height };
2248
2270
  }
2249
2271
  async function compshareImageTask(prompt, refs, s, opts) {
@@ -2251,17 +2273,17 @@ window.__ModuleLoader__.load({
2251
2273
  let base = String(s.baseUrl || '').trim().replace(/\/+$/, '');
2252
2274
  base = base.replace(/\/image\/v1$/i, ''); // 粘贴了完整路径前缀也兼容
2253
2275
  if (!base) throw new Error('未配置图片 API 地址(优云智算:https://cp.compshare.cn)');
2254
- const headers = { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: 'Bearer ' + (s.apiKey || '') };
2276
+ const headers = { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: 'Bearer ' + String(s.apiKey || '').trim() };
2255
2277
  const extra = parseExtraParams(s.extra); // 透传:optimize_prompt / use_context_ir 等任务级字段
2256
2278
  const api = async (method, path, body, idem) => {
2257
2279
  const h = { ...headers };
2258
2280
  if (idem) h['Idempotency-Key'] = idem;
2259
2281
  const r = await fetchViaProxy(base + path, { method, headers: h, ...(body !== undefined ? { body } : {}), timeoutMs: 120000, label: '优云图片 ' + method });
2260
- if (!r.ok) throw new Error('优云图片 API HTTP ' + r.status + ':' + r.text.slice(0, 300));
2261
- let j = null; try { j = JSON.parse(r.text); } catch { throw new Error('优云图片 API 响应非 JSON:' + r.text.slice(0, 200)); }
2282
+ if (!r.ok) throw new Error(`优云图片 API ${method} ${path} HTTP ${r.status}:` + r.text.slice(0, 300));
2283
+ let j = null; try { j = JSON.parse(r.text); } catch { throw new Error(`优云图片 API ${method} ${path} 响应非 JSON:` + r.text.slice(0, 200)); }
2262
2284
  // 网关业务错误可能 HTTP 200:RetCode≠0 或 {type:'error'} 都算失败
2263
2285
  if ((j.RetCode != null && Number(j.RetCode) !== 0) || j.type === 'error') {
2264
- throw new Error('优云图片 API 失败:' + ((j.error && j.error.message) || j.Message || JSON.stringify(j).slice(0, 200)));
2286
+ throw new Error(`优云图片 API ${method} ${path} 失败:` + ((j.error && j.error.message) || j.Message || JSON.stringify(j).slice(0, 200)));
2265
2287
  }
2266
2288
  return j;
2267
2289
  };
@@ -2821,7 +2843,7 @@ window.__ModuleLoader__.load({
2821
2843
  const ex = (() => { try { return parseExtraParams(s.extra) || {}; } catch (e) { throw new Error('扩展参数配置有误:' + ((e && e.message) || e)); } })();
2822
2844
  // 路径风格:优云智算网关挂 /minimax/v2/...,MiniMax 官方裸 /v2/...;扩展参数 pathPrefix 可显式覆盖
2823
2845
  const prefix = String(ex.pathPrefix || (/compshare/i.test(base) ? '/minimax' : ''));
2824
- const headers = { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: 'Bearer ' + (s.apiKey || '') };
2846
+ const headers = { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: 'Bearer ' + String(s.apiKey || '').trim() };
2825
2847
  // 分辨率大小写敏感(480P/768P/1080P/2K/4K):画布的 720P 语义等同原生 768P,显式映射不依赖厂商静默降级
2826
2848
  const RESO = { '480P': '480P', '720P': '768P', '768P': '768P', '1080P': '1080P', '2K': '2K', '4K': '4K' };
2827
2849
  const resolution = String(ex.resolution || RESO[String(o.resolution || '').toUpperCase()] || '768P');
@@ -3066,6 +3088,14 @@ window.__ModuleLoader__.load({
3066
3088
  };
3067
3089
  async function apiVideo(prompt, frameBlob, s, opts) {
3068
3090
  const o = opts || {};
3091
+ // 节点级扩展参数(手动参数框)与供应商扩展参数合并(节点优先),各 runner 的 mergeExtra 自动生效
3092
+ const nodeExtra = String(o.nodeExtra || '').trim();
3093
+ if (nodeExtra) {
3094
+ try {
3095
+ const baseExtra = String(s.extra || '').trim();
3096
+ s = { ...s, extra: JSON.stringify({ ...(baseExtra ? JSON.parse(baseExtra) : {}), ...JSON.parse(nodeExtra) }) };
3097
+ } catch (e) { throw new Error('节点扩展参数需为 JSON 对象:' + ((e && e.message) || e)); }
3098
+ }
3069
3099
  // 衔接模式:auto=尾帧降级为参考图(保留实体参考图);hard=硬首帧(放弃参考图,画面绝对承接);
3070
3100
  // off=忽略首帧。网关禁止 first_frame 与 reference_image 混用,故 hard 模式必须放弃参考图。
3071
3101
  if (o.frameMode === 'off') frameBlob = null;
@@ -5362,7 +5392,7 @@ window.__ModuleLoader__.load({
5362
5392
  // 模型解析:节点选择优先,未指定路由到星标默认模型;都没有 = 明确报错(不再静默走演示模式)
5363
5393
  const mcfg = resolveImageModelCfg(settings, nodeImageModelRef(n));
5364
5394
  if (!mcfg) throw new Error(T('errNoImageModel'));
5365
- const blob = await apiImage(p, metas, mcfg, { size: n.data.size, ratio: n.data.ratio });
5395
+ const blob = await apiImage(p, metas, mcfg, { size: n.data.size, ratio: n.data.ratio, nodeExtra: n.data.imgExtra });
5366
5396
  const rec = await saveAssetRecord(S.activeId, 'image', blob, {
5367
5397
  name: assetNameFor(n, entities, 'image'),
5368
5398
  prompt: stripMentionMarks(prompt), effectivePrompt: p,
@@ -5385,6 +5415,7 @@ window.__ModuleLoader__.load({
5385
5415
  frameMode: n.data.frameMode || 'auto', // auto=尾帧降级为参考图 / hard=硬首帧(放弃参考图) / off=忽略首帧
5386
5416
  refs: refs.map((r) => r.blob),
5387
5417
  refNames: refs.map((r) => r.entity || r.name || ''), // 跨镜衔接标注用(图2..N 的实体名)
5418
+ nodeExtra: n.data.vidExtra, // 节点级扩展参数(手动参数框,如 {"use_context_ir":true})
5388
5419
  // 任务提交成功即持久化 task id:轮询中断/页面关闭后仍可凭 id 回查结果(钱花了就必须能找回)
5389
5420
  onTask: (tid, qurl) => {
5390
5421
  update((s) => { const nd = s.graph.nodes[n.id]; if (nd) { nd.data.taskId = String(tid || ''); nd.data.taskQueryUrl = String(qurl || ''); } });
@@ -6311,6 +6342,17 @@ window.__ModuleLoader__.load({
6311
6342
  ],
6312
6343
  h('div', { className: 'dshic-flex' }),
6313
6344
  ),
6345
+ h('input', {
6346
+ key: 'nextra', className: 'dshic-input', value: (kind === 'video' ? n.data.vidExtra : n.data.imgExtra) || '',
6347
+ placeholder: '扩展参数 JSON,如 {"use_context_ir":true}',
6348
+ title: '节点级扩展参数:合并进供应商扩展参数,节点值优先',
6349
+ onChange: (e) => update((s) => {
6350
+ const nd = s.graph.nodes[n.id];
6351
+ if (!nd) return;
6352
+ if (kind === 'video') nd.data.vidExtra = e.target.value; else nd.data.imgExtra = e.target.value;
6353
+ }),
6354
+ onPointerDown: (e) => e.stopPropagation(),
6355
+ }),
6314
6356
  );
6315
6357
  }
6316
6358
 
@@ -6573,6 +6615,36 @@ window.__ModuleLoader__.load({
6573
6615
  })();
6574
6616
  return () => { cancelled = true; };
6575
6617
  }, [kind, n.data.assetId, n.data.label, S.assetsTick]);
6618
+ // 历史条自愈:已被删除的资产(渲染成 … 的幽灵格)自动从节点 history 剔除;
6619
+ // 服务端不可达时跳过本轮(宁可不修剪,绝不误删有效历史)
6620
+ useEffect(() => {
6621
+ const histList = n.data.history || [];
6622
+ if (!histList.length) return;
6623
+ let ok = true;
6624
+ (async () => {
6625
+ let alive = null;
6626
+ if (S.store) {
6627
+ const r = await hostRpc('assetsGet', { ids: histList.map((x) => x.assetId) }).catch(() => null);
6628
+ if (!r) return;
6629
+ alive = new Set((r.items || []).map((x) => x.id));
6630
+ } else {
6631
+ const recs = await Promise.all(histList.map((x) => idbGetRaw(x.assetId).catch(() => null)));
6632
+ alive = new Set(histList.filter((_, i) => recs[i]).map((x) => x.assetId));
6633
+ }
6634
+ if (!ok) return;
6635
+ const dead = histList.filter((x) => !alive.has(x.assetId)).map((x) => x.assetId);
6636
+ if (!dead.length) return;
6637
+ update((s) => {
6638
+ const nd = s.graph.nodes[n.id];
6639
+ if (!nd || !Array.isArray(nd.data.history)) return;
6640
+ const next = nd.data.history.filter((x) => !dead.includes(x.assetId));
6641
+ if (next.length === nd.data.history.length) return;
6642
+ nd.data.history = next;
6643
+ if (nd.data.assetId && dead.includes(nd.data.assetId)) nd.data.assetId = next.length ? next[next.length - 1].assetId : null;
6644
+ });
6645
+ })();
6646
+ return () => { ok = false; };
6647
+ }, [n.id, S.assetsTick, S.store]);
6576
6648
  const promptEdge = S.graph.edges.find((x) => x.toNode === n.id && x.toPort === 'prompt');
6577
6649
  const promptSrc = promptEdge && S.graph.nodes[promptEdge.fromNode];
6578
6650
  const fromStoryboard = !!(promptSrc && promptSrc.type === 'storyboard');
@@ -8043,7 +8115,11 @@ window.__ModuleLoader__.load({
8043
8115
  assetMem.delete(a.id);
8044
8116
  update((s) => {
8045
8117
  for (const nd of Object.values(s.graph.nodes)) {
8046
- if (nd.data && nd.data.assetId === a.id) { nd.data.assetId = null; nd.data.status = 'idle'; }
8118
+ const d = nd.data;
8119
+ if (!d) continue;
8120
+ if (d.assetId === a.id) { d.assetId = null; d.status = 'idle'; }
8121
+ if (d.lastAssetId === a.id) d.lastAssetId = null; // 尾帧引用同步清理
8122
+ if (Array.isArray(d.history) && d.history.some((x) => x.assetId === a.id)) d.history = d.history.filter((x) => x.assetId !== a.id); // 历史条同步清理
8047
8123
  }
8048
8124
  s.assetsTick++;
8049
8125
  });
@@ -8356,12 +8432,12 @@ window.__ModuleLoader__.load({
8356
8432
  }
8357
8433
 
8358
8434
  /** 图片/视频模型多配置列表:名称 / 地址 / Key / 模型(/模式),可增删、设默认。 */
8359
- function Field({ label, value, onChange, placeholder, password }) {
8435
+ function Field({ label, value, onChange, placeholder, password, tip }) {
8360
8436
  // 敏感字段(API Key):默认脱敏,点击小眼睛切换明文显示
8361
8437
  const [reveal, setReveal] = useState(false);
8362
8438
  if (!password) {
8363
8439
  return h('div', { className: 'dshic-field' },
8364
- h('span', { className: 'dshic-field-label' }, label),
8440
+ h('span', { className: 'dshic-field-label' }, label, tip ? InfoTip(tip) : null),
8365
8441
  h('input', {
8366
8442
  className: 'dshic-input', type: 'text', value, placeholder: placeholder || '',
8367
8443
  onChange: (e) => onChange(e.target.value),
@@ -8451,14 +8527,15 @@ window.__ModuleLoader__.load({
8451
8527
  ),
8452
8528
  open ? [
8453
8529
  kind === 'video' ? h('div', { key: 'vendor', className: 'dshic-field' },
8454
- h('span', { className: 'dshic-field-label' }, T('vendorLabel')),
8530
+ h('div', { className: 'dshic-row' },
8531
+ h('span', { className: 'dshic-field-label' }, T('vendorLabel')),
8532
+ InfoTip((VIDEO_VENDORS[detectVideoVendor(p)] || {}).note || '')),
8455
8533
  h('select', {
8456
8534
  className: 'dshic-select', value: detectVideoVendor(p),
8457
8535
  onPointerDown: (e) => e.stopPropagation(),
8458
8536
  onChange: (e) => { const v = e.target.value; const vd = VIDEO_VENDORS[v]; patchProvider(p.id, 'vendor', v); if (vd && vd.base && !p[urlKey]) patchProvider(p.id, urlKey, vd.base); },
8459
8537
  },
8460
8538
  Object.entries(VIDEO_VENDORS).map(([id, vd]) => h('option', { key: id, value: id }, vd.label))),
8461
- h('div', { className: 'dshic-hint' }, (VIDEO_VENDORS[detectVideoVendor(p)] || {}).note || ''),
8462
8539
  ) : null,
8463
8540
  h(Field, { key: 'u', label: T('baseUrl'), value: p[urlKey] || '', onChange: (v) => patchProvider(p.id, urlKey, v), placeholder: kind === 'video' ? ((VIDEO_VENDORS[detectVideoVendor(p)] || {}).base || urlPh) : urlPh }),
8464
8541
  h(Field, { key: 'k', label: T('apiKey'), value: p.apiKey || '', onChange: (v) => patchProvider(p.id, 'apiKey', v), placeholder: kind === 'video' ? ((VIDEO_VENDORS[detectVideoVendor(p)] || {}).keyPh || 'sk-…') : 'sk-…', password: true }),
@@ -8477,9 +8554,8 @@ window.__ModuleLoader__.load({
8477
8554
  h('input', { type: 'checkbox', checked: p.generateAudio === true, onChange: (e) => patchProvider(p.id, 'generateAudio', e.target.checked), onPointerDown: (e) => e.stopPropagation() }),
8478
8555
  h('span', {}, T('vidAudioOpt'))) : null,
8479
8556
  ],
8480
- (kind === 'image' || detectVideoVendor(p) === 'openai' || detectVideoVendor(p) === 'seedance') ? [
8481
- h(Field, { key: 'extra', label: T('cfgExtra'), value: p.extra || '', onChange: (v) => patchProvider(p.id, 'extra', v), placeholder: '{"watermark":false,"prompt_extend":false}' }),
8482
- h('div', { className: 'dshic-hint' }, T('cfgExtraHint')),
8557
+ (kind === 'image' || kind === 'video') ? [
8558
+ h(Field, { key: 'extra', label: T('cfgExtra'), value: p.extra || '', onChange: (v) => patchProvider(p.id, 'extra', v), placeholder: '{"watermark":false,"prompt_extend":false}', tip: T('cfgExtraHint') }),
8483
8559
  ] : null,
8484
8560
  h('div', { className: 'dshic-row', style: { marginTop: 4 } },
8485
8561
  h('span', { className: 'dshic-field-label', style: { flex: 1, minWidth: 0 } }, T('modelsLabel')),
@@ -8640,10 +8716,7 @@ window.__ModuleLoader__.load({
8640
8716
  h('span', {}, T('seedanceWatermark')))
8641
8717
  : null,
8642
8718
  (kind === 'image' || kind === 'video')
8643
- ? h(Field, { key: 'extra', label: T('cfgExtra'), value: cfg.extra || '', onChange: (v) => patch(cfg.id, 'extra', v), placeholder: '{"watermark":false,"prompt_extend":false}' })
8644
- : null,
8645
- (kind === 'image' || kind === 'video')
8646
- ? h('div', { className: 'dshic-hint' }, T('cfgExtraHint'))
8719
+ ? h(Field, { key: 'extra', label: T('cfgExtra'), value: cfg.extra || '', onChange: (v) => patch(cfg.id, 'extra', v), placeholder: '{"watermark":false,"prompt_extend":false}', tip: T('cfgExtraHint') })
8647
8720
  : null,
8648
8721
  kind === 'tts'
8649
8722
  ? h(Field, { key: 'voice', label: T('voice'), value: cfg.voice || '', onChange: (v) => patch(cfg.id, 'voice', v), placeholder: 'alloy' })
package/index.js CHANGED
@@ -1117,7 +1117,13 @@ async function fetchProxyForward(reqBody) {
1117
1117
  const url = String((reqBody && reqBody.url) || '');
1118
1118
  if (!/^https?:\/\//i.test(url)) throw new Error('需要 http(s) 接口 url');
1119
1119
  const method = String((reqBody && reqBody.method) || 'POST').toUpperCase();
1120
- const headers = { ...((reqBody && reqBody.headers && typeof reqBody.headers === 'object') ? reqBody.headers : {}) };
1120
+ // 头部统一小写:客户端与默认补值可能各自携带不同大小写的同名头(Content-Type 与 content-type),
1121
+ // 混用会作为重复头转发,严格网关(优云智算)直接 400 "Content-Type must be application/json"
1122
+ const headers = {};
1123
+ for (const [hk, hv] of Object.entries((reqBody && reqBody.headers && typeof reqBody.headers === 'object') ? reqBody.headers : {})) {
1124
+ if (hv === undefined || hv === null) continue;
1125
+ headers[String(hk).toLowerCase()] = String(hv);
1126
+ }
1121
1127
  let body;
1122
1128
  if (method !== 'GET' && method !== 'HEAD') {
1123
1129
  if (reqBody && reqBody.bodyRaw != null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ninkch/dsh-infinite-canvas",
3
- "version": "0.18.1",
3
+ "version": "0.18.3",
4
4
  "description": "Node-based short-drama studio for DeepSeek Harness: script → storyboard → assets → shots → voiced, subtitled final cut on an infinite canvas.",
5
5
  "keywords": [
6
6
  "dsh",