@ninkch/dsh-infinite-canvas 0.18.0 → 0.18.2

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.
@@ -34,34 +34,34 @@ B:……
34
34
 
35
35
  ## 二·五、资产设定图版式(角色/道具/场景的专业排版,拒绝简单三视图)
36
36
 
37
- 简单三视图信息量不足——设定图要做成**专业设定集排版**:文字信息 + 主图 + 特写 + 表情/色卡
38
- 分区呈现,一图读懂资产的全部视觉设定。所有面板文字一律**简体中文、清晰可读**;
39
- 同一资产在所有分格中外观**完全一致**(发型/服装/配色逐格复用设定集关键词)。
37
+ 简单三视图信息量不足——设定图要做成**专业设定集排版**:主图 + 特写 + 表情/色卡分区呈现,
38
+ 一图读懂资产的全部视觉设定。**版面里绝不渲染成段文字**:文字渲染型模型画长文本必然乱码(实测),
39
+ 面板文字只允许 ≤4 字短标签(面板名 / 表情名 / 颜色名)。同一资产在所有分格中外观**完全一致**——
40
+ **已连接参考图时,参考图形象是唯一事实**,逐格严格复制;未连接参考图时逐格复用设定集外观关键词。
40
41
 
41
42
  ### 角色设定图(16:9 横版——主图与三视图占大版面)
42
43
  - **版面主体(约三分之二)给画面**:中央大幅全身立绘主图(正面站姿),主图下方一排
43
44
  **三视图(正面 / 侧面 / 背面 全身)**——三视图是必须项,后续视频要靠它保证侧背面的准确形态。
44
- - **右侧窄栏做辅助信息(压缩排布)**:
45
- - 信息条:姓名 / 性别 / 年龄 / 身份,3–4 行小字;
46
- - 脸部特征:一格正面特写(或正/侧两小格)+ 一两行五官标注;
47
- - 服饰特征:2–3 行服装要点;
48
- - 表情条:一行 4–6 个小头像,逐个标注表情名(按角色性格取舍,如 温柔 / 惊讶 / 坚定)。
49
- - **细节格(可选)**:2–4 个小格——发饰 / 佩饰 / 纹样特写,各配一行小字。
50
- - 所有分格中同一角色外观完全一致(发型 / 服装 / 配色逐格复用设定集关键词);
51
- 面板文字一律简体中文清晰可读。
45
+ - **右侧窄栏做纯视觉辅助(不渲染文字段落)**:
46
+ - 脸部特征:一格正面特写(或正 / 侧两小格);
47
+ - 表情条:一行 4–6 个小头像,每格下方只标表情名(两字,按角色性格取舍,如 温柔 / 惊讶 / 坚定);
48
+ - 细节特写(可选):1–2 格(项圈 / 挂牌 / 发饰),格内无文字。
49
+ - **禁止**:姓名 / 性别 / 年龄等属性文本、服饰说明文字、旁白或诗句——这些属于设定集,
50
+ 画进图里必成乱码,且会挤出角色形象的一致性注意力。
51
+ - 所有分格中同一角色外观完全一致(以参考图形象为准逐格复制)。
52
52
  - 提示词结构示例:「角色人设图(16:9 横版设定集排版):中央大幅全身立绘,主图下方三视图一排
53
- (正面/侧面/背面全身);右侧窄栏:姓名性别年龄小字信息、一格脸部特写、一行表情小头像;
54
- 面板文字全部写进提示词,模型才能排出完整版式。」
53
+ (正面/侧面/背面全身);右侧窄栏:脸部特写一格、表情小头像一排(每格下方只标两字表情名);
54
+ 画面中不出现任何成段文字。」
55
55
 
56
56
  ### 道具设定图(横竖皆可——按道具特性选,不限制)
57
57
  - **画幅选择**:长条形/单件道具(剑、箫、杖)竖版更能展示完整形态;
58
58
  组合道具、带场景感或横向结构的道具用横版能并列更多细节与色卡——排版逻辑不变,画幅自由。
59
- - **顶部**:道具名大标题 + 一行类别副标题(如「火属性上古仙兵」)。
59
+ - **顶部**:道具名大标题(≤6 字)。
60
60
  - **中央主图**:道具斜 45° 悬浮展示,带氛围光/粒子效果,占画面主体。
61
- - **细节特写 2–3 格**:材质纹理、关键部件(如「箫身细节:赤玉雕金」),各配一行小字。
62
- - **侧栏侧视图**:同一道具的正侧面完整形态,标题「侧视图」。
63
- - **底部颜色参考**:3–5 个圆形色卡,每个下方标注颜色名(如「赤红 / 熔金 / 绛紫」)。
64
- - 点缀一两句道具铭文/短诗式文案(如「一簫起,万火归墟」)。
61
+ - **细节特写 2–3 格**:材质纹理、关键部件,格内无文字。
62
+ - **侧栏侧视图**:同一道具的正侧面完整形态。
63
+ - **底部颜色参考**:3–5 个圆形色卡,每个下方标注颜色名(≤3 字,如「赤红 / 熔金 / 绛紫」)。
64
+ - **禁止**:铭文 / 短诗 / 说明文字渲染(易成乱码)。
65
65
 
66
66
  ### 场景设定图(主场景图 + 环绕补充小图)
67
67
  - **主场景图占画面主体**(全景建立镜头,交代空间结构与光线方向)——不要切成几张等大的长条格,
@@ -70,9 +70,10 @@ B:……
70
70
  - 右侧一列:**多角度图**(俯瞰、贴地、第一人称视线等其他机位)+ **其他时间图**(夜景 / 雨雾 /
71
71
  逆光等不同时间段或天气下的同一场景);
72
72
  - 底部一排:**细节格**(标志物 / 入口 / 材质纹理 / 关键道具在场景中的位置)。
73
- - 每张小图下方一行标注(机位 / 时间段 / 内容,如「入口 · 夜」「雨中 · 逆光」)。
73
+ - 每张小图下方一行**短标注(≤6 字**,如「入口·夜」「雨中·逆光」)。
74
74
  - 上述布局是大致参考、不强制;所有格子的光线方向、天气、色调须与主图统一
75
75
  (展示不同时间段时在标注中说明)。
76
+ - **禁止**:成段说明文字。
76
77
 
77
78
  ### 画面感要点
78
79
  - 用一个**具体的可视细节**替代形容词:不写"他很悲伤",写"他把没送出的信折了三次,塞回口袋"。
@@ -142,12 +142,11 @@ description: 用无限画布(Infinite Canvas)做短剧/视频创作时使用
142
142
  **prompt 必须按 `canvas-craft`「资产设定图版式」一节的排版模板写详细版式提示词,
143
143
  禁止只写一句三视图/外观关键词**——信息量不足,撑不起后续视频的角色一致性。要点(完整模板见 canvas-craft):
144
144
  - **角色设定图**(16:9 横版):**主图 + 三视图一排(正/侧/背全身,必须项)占大版面(约 2/3)**;
145
- 右窄栏压缩辅助信息(信息条 3–4 行小字 / 脸部特写一格 / 服饰要点 2–3 行 / 表情条一行 4–6 个小头像);
146
- 细节格可选;所有分格中同一角色外观完全一致
147
- - **道具设定图**(横竖皆可,按道具特性选):顶部道具名大标题+类别副标题;中央主图(斜 45° 悬浮+氛围光);
148
- 材质/部件细节特写带名称小字;侧栏侧视图;底部圆形色卡 3–5 枚带颜色名
149
- - **场景设定图**(主图+环绕小图):一张主场景大图占主体,周边环绕补充小图(多角度 / 其他时间段 / 细节格,3–6 张各配标注)——大致参考不强约束
150
- - 面板文字一律简体中文且清晰可读;设定集外观关键词逐格复用,保证跨格一致
145
+ 右窄栏纯视觉辅助(脸部特写一格 / 表情条一行小头像,每格只标两字表情名);细节格可选
146
+ - **道具设定图**(横竖皆可,按道具特性选):顶部道具名大标题(≤6 字);中央主图(斜 45° 悬浮+氛围光);
147
+ 细节特写 2–3 格(格内无文字);侧栏侧视图;底部圆形色卡 3–5 枚(色名 ≤3 字)
148
+ - **场景设定图**(主图+环绕小图):一张主场景大图占主体,周边环绕补充小图(多角度 / 其他时间段 / 细节格,3–6 张各配 ≤6 字短标注)——大致参考不强约束
149
+ - **面板内绝不渲染成段文字**(长文本必乱码):只允许 ≤4 字短标签;已连接参考图时参考图形象是唯一事实,逐格严格复制
151
150
  - 需要补连任意两节点时用 `canvas_connect({ projectId, fromNode, toNode })`(端口类型必须匹配;
152
151
  连分镜表时系统按语义自动选口:剧本/文本 → `script`,分镜表 → `prev`,不用手写端口)
153
152
  - **布局乱了/节点重叠**:`canvas_relayout({ projectId })` 一键重排——横向数据流决定列
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'(节点字段懒迁移用)
@@ -1600,18 +1603,21 @@ window.__ModuleLoader__.load({
1600
1603
  const rest = nm.slice(pref.length);
1601
1604
  return rest === '' || !/[\p{L}\p{N}]/u.test(rest[0]);
1602
1605
  }
1603
- /** 生成用提示词:@标记去掉 + [图N] 绑定 + 命中实体的设定描述。 */
1604
- function buildEffectivePrompt(prompt, refs, entities, bible) {
1606
+ /** 生成用提示词:@标记去掉 + [图N] 绑定 + 命中实体的设定描述。
1607
+ * light=true(图片生成专用):只保留风格行与 [图N] 名称绑定——参考图本身就是外观事实源,
1608
+ * 内联设定全文会稀释参考图约束,且文字渲染型模型(Qwen-Image)会把追加文本直接画进画面(用户实测:
1609
+ * 设定集全文被渲染成画面右下角的乱码文字块、剧情词凭空生成道具)。视频路径不用 light(不渲染文字,上下文有益)。 */
1610
+ function buildEffectivePrompt(prompt, refs, entities, bible, light) {
1605
1611
  const zh = effLang() !== 'en';
1606
1612
  const bind = refs.map((r, i) => {
1607
1613
  if (r.entity) {
1608
1614
  const ent = matchBibleEntity(r.entity, bible);
1609
- const desc = ent ? ent.desc : '';
1610
- return `[图${i + 1}] ${entityLabelOf(ent ? ent.kind : 'prop')} ${r.entity}${desc ? '(' + desc + ')' : ''}`;
1615
+ const desc = light ? '' : ((ent && ent.desc) || '');
1616
+ return `[图${i + 1}] ${entityLabelOf((ent || {}).kind || 'prop')} ${r.entity}${desc ? '(' + desc + ')' : ''}`;
1611
1617
  }
1612
1618
  return `[图${i + 1}] ${catLabel(r.category)}:${r.name}`;
1613
1619
  }).join(';');
1614
- const entLines = entities.map((e) => `${entityLabelOf(e.kind)} ${e.name}:${e.desc}`);
1620
+ const entLines = light ? [] : entities.map((e) => `${entityLabelOf(e.kind)} ${e.name}:${e.desc}`);
1615
1621
  return [
1616
1622
  (bible.style || '').trim() ? (zh ? '风格:' : 'Style: ') + bible.style.trim() : '',
1617
1623
  bind ? (zh ? '参考图 → ' : 'Refs -> ') + bind : '',
@@ -2151,10 +2157,21 @@ window.__ModuleLoader__.load({
2151
2157
  let base = s.baseUrl.replace(/\/+$/, '').replace(/\/images\/generations$/, '');
2152
2158
  if (!base.replace(/^https?:\/\//i, '').includes('/')) base = base + '/v1';
2153
2159
  const refs = refBlobs || [];
2154
- const auth = s.apiKey ? { Authorization: 'Bearer ' + s.apiKey } : {};
2155
- 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); // 厂商专属参数(留空不发送)
2156
2170
  // 优云智算(compshare)任务式:Qwen-Image 异步任务(提交 → 轮询 → output_url 下载)
2157
- 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 异常由后续流程报错 */ }
2158
2175
  const upstreamTimeout = Number(s.timeoutMs) > 0 ? Number(s.timeoutMs) : 120000; // 商汤等同步接口 20s+,默认 120s
2159
2176
  // 参考图编辑模式:multipart 上传到 /images/edits(OpenAI gpt-image-1 兼容)
2160
2177
  if (refs.length && s.mode === 'edits') {
@@ -2248,7 +2265,7 @@ window.__ModuleLoader__.load({
2248
2265
  let base = String(s.baseUrl || '').trim().replace(/\/+$/, '');
2249
2266
  base = base.replace(/\/image\/v1$/i, ''); // 粘贴了完整路径前缀也兼容
2250
2267
  if (!base) throw new Error('未配置图片 API 地址(优云智算:https://cp.compshare.cn)');
2251
- const headers = { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: 'Bearer ' + (s.apiKey || '') };
2268
+ const headers = { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: 'Bearer ' + String(s.apiKey || '').trim() };
2252
2269
  const extra = parseExtraParams(s.extra); // 透传:optimize_prompt / use_context_ir 等任务级字段
2253
2270
  const api = async (method, path, body, idem) => {
2254
2271
  const h = { ...headers };
@@ -2818,7 +2835,7 @@ window.__ModuleLoader__.load({
2818
2835
  const ex = (() => { try { return parseExtraParams(s.extra) || {}; } catch (e) { throw new Error('扩展参数配置有误:' + ((e && e.message) || e)); } })();
2819
2836
  // 路径风格:优云智算网关挂 /minimax/v2/...,MiniMax 官方裸 /v2/...;扩展参数 pathPrefix 可显式覆盖
2820
2837
  const prefix = String(ex.pathPrefix || (/compshare/i.test(base) ? '/minimax' : ''));
2821
- const headers = { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: 'Bearer ' + (s.apiKey || '') };
2838
+ const headers = { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: 'Bearer ' + String(s.apiKey || '').trim() };
2822
2839
  // 分辨率大小写敏感(480P/768P/1080P/2K/4K):画布的 720P 语义等同原生 768P,显式映射不依赖厂商静默降级
2823
2840
  const RESO = { '480P': '480P', '720P': '768P', '768P': '768P', '1080P': '1080P', '2K': '2K', '4K': '4K' };
2824
2841
  const resolution = String(ex.resolution || RESO[String(o.resolution || '').toUpperCase()] || '768P');
@@ -3063,6 +3080,14 @@ window.__ModuleLoader__.load({
3063
3080
  };
3064
3081
  async function apiVideo(prompt, frameBlob, s, opts) {
3065
3082
  const o = opts || {};
3083
+ // 节点级扩展参数(手动参数框)与供应商扩展参数合并(节点优先),各 runner 的 mergeExtra 自动生效
3084
+ const nodeExtra = String(o.nodeExtra || '').trim();
3085
+ if (nodeExtra) {
3086
+ try {
3087
+ const baseExtra = String(s.extra || '').trim();
3088
+ s = { ...s, extra: JSON.stringify({ ...(baseExtra ? JSON.parse(baseExtra) : {}), ...JSON.parse(nodeExtra) }) };
3089
+ } catch (e) { throw new Error('节点扩展参数需为 JSON 对象:' + ((e && e.message) || e)); }
3090
+ }
3066
3091
  // 衔接模式:auto=尾帧降级为参考图(保留实体参考图);hard=硬首帧(放弃参考图,画面绝对承接);
3067
3092
  // off=忽略首帧。网关禁止 first_frame 与 reference_image 混用,故 hard 模式必须放弃参考图。
3068
3093
  if (o.frameMode === 'off') frameBlob = null;
@@ -5355,11 +5380,11 @@ window.__ModuleLoader__.load({
5355
5380
  // 参考图 = 连线(用户顺序)+ @引用命中的设定集资产(按出现顺序)
5356
5381
  const { refs, entities } = await collectRefs(n, prompt, bible);
5357
5382
  const metas = refs.map((r) => ({ blob: r.blob, name: r.name, category: r.category, assetId: r.assetId }));
5358
- const p = buildEffectivePrompt(prompt, refs, entities, bible);
5383
+ const p = buildEffectivePrompt(prompt, refs, entities, bible, true); // light:图片不内联设定全文(防文字渲染污染 + 参考图约束稀释)
5359
5384
  // 模型解析:节点选择优先,未指定路由到星标默认模型;都没有 = 明确报错(不再静默走演示模式)
5360
5385
  const mcfg = resolveImageModelCfg(settings, nodeImageModelRef(n));
5361
5386
  if (!mcfg) throw new Error(T('errNoImageModel'));
5362
- const blob = await apiImage(p, metas, mcfg, { size: n.data.size, ratio: n.data.ratio });
5387
+ const blob = await apiImage(p, metas, mcfg, { size: n.data.size, ratio: n.data.ratio, nodeExtra: n.data.imgExtra });
5363
5388
  const rec = await saveAssetRecord(S.activeId, 'image', blob, {
5364
5389
  name: assetNameFor(n, entities, 'image'),
5365
5390
  prompt: stripMentionMarks(prompt), effectivePrompt: p,
@@ -5382,6 +5407,7 @@ window.__ModuleLoader__.load({
5382
5407
  frameMode: n.data.frameMode || 'auto', // auto=尾帧降级为参考图 / hard=硬首帧(放弃参考图) / off=忽略首帧
5383
5408
  refs: refs.map((r) => r.blob),
5384
5409
  refNames: refs.map((r) => r.entity || r.name || ''), // 跨镜衔接标注用(图2..N 的实体名)
5410
+ nodeExtra: n.data.vidExtra, // 节点级扩展参数(手动参数框,如 {"use_context_ir":true})
5385
5411
  // 任务提交成功即持久化 task id:轮询中断/页面关闭后仍可凭 id 回查结果(钱花了就必须能找回)
5386
5412
  onTask: (tid, qurl) => {
5387
5413
  update((s) => { const nd = s.graph.nodes[n.id]; if (nd) { nd.data.taskId = String(tid || ''); nd.data.taskQueryUrl = String(qurl || ''); } });
@@ -6308,6 +6334,17 @@ window.__ModuleLoader__.load({
6308
6334
  ],
6309
6335
  h('div', { className: 'dshic-flex' }),
6310
6336
  ),
6337
+ h('input', {
6338
+ key: 'nextra', className: 'dshic-input', value: (kind === 'video' ? n.data.vidExtra : n.data.imgExtra) || '',
6339
+ placeholder: '扩展参数 JSON,如 {"use_context_ir":true}',
6340
+ title: '节点级扩展参数:合并进供应商扩展参数,节点值优先',
6341
+ onChange: (e) => update((s) => {
6342
+ const nd = s.graph.nodes[n.id];
6343
+ if (!nd) return;
6344
+ if (kind === 'video') nd.data.vidExtra = e.target.value; else nd.data.imgExtra = e.target.value;
6345
+ }),
6346
+ onPointerDown: (e) => e.stopPropagation(),
6347
+ }),
6311
6348
  );
6312
6349
  }
6313
6350
 
@@ -6570,6 +6607,36 @@ window.__ModuleLoader__.load({
6570
6607
  })();
6571
6608
  return () => { cancelled = true; };
6572
6609
  }, [kind, n.data.assetId, n.data.label, S.assetsTick]);
6610
+ // 历史条自愈:已被删除的资产(渲染成 … 的幽灵格)自动从节点 history 剔除;
6611
+ // 服务端不可达时跳过本轮(宁可不修剪,绝不误删有效历史)
6612
+ useEffect(() => {
6613
+ const histList = n.data.history || [];
6614
+ if (!histList.length) return;
6615
+ let ok = true;
6616
+ (async () => {
6617
+ let alive = null;
6618
+ if (S.store) {
6619
+ const r = await hostRpc('assetsGet', { ids: histList.map((x) => x.assetId) }).catch(() => null);
6620
+ if (!r) return;
6621
+ alive = new Set((r.items || []).map((x) => x.id));
6622
+ } else {
6623
+ const recs = await Promise.all(histList.map((x) => idbGetRaw(x.assetId).catch(() => null)));
6624
+ alive = new Set(histList.filter((_, i) => recs[i]).map((x) => x.assetId));
6625
+ }
6626
+ if (!ok) return;
6627
+ const dead = histList.filter((x) => !alive.has(x.assetId)).map((x) => x.assetId);
6628
+ if (!dead.length) return;
6629
+ update((s) => {
6630
+ const nd = s.graph.nodes[n.id];
6631
+ if (!nd || !Array.isArray(nd.data.history)) return;
6632
+ const next = nd.data.history.filter((x) => !dead.includes(x.assetId));
6633
+ if (next.length === nd.data.history.length) return;
6634
+ nd.data.history = next;
6635
+ if (nd.data.assetId && dead.includes(nd.data.assetId)) nd.data.assetId = next.length ? next[next.length - 1].assetId : null;
6636
+ });
6637
+ })();
6638
+ return () => { ok = false; };
6639
+ }, [n.id, S.assetsTick, S.store]);
6573
6640
  const promptEdge = S.graph.edges.find((x) => x.toNode === n.id && x.toPort === 'prompt');
6574
6641
  const promptSrc = promptEdge && S.graph.nodes[promptEdge.fromNode];
6575
6642
  const fromStoryboard = !!(promptSrc && promptSrc.type === 'storyboard');
@@ -8040,7 +8107,11 @@ window.__ModuleLoader__.load({
8040
8107
  assetMem.delete(a.id);
8041
8108
  update((s) => {
8042
8109
  for (const nd of Object.values(s.graph.nodes)) {
8043
- if (nd.data && nd.data.assetId === a.id) { nd.data.assetId = null; nd.data.status = 'idle'; }
8110
+ const d = nd.data;
8111
+ if (!d) continue;
8112
+ if (d.assetId === a.id) { d.assetId = null; d.status = 'idle'; }
8113
+ if (d.lastAssetId === a.id) d.lastAssetId = null; // 尾帧引用同步清理
8114
+ if (Array.isArray(d.history) && d.history.some((x) => x.assetId === a.id)) d.history = d.history.filter((x) => x.assetId !== a.id); // 历史条同步清理
8044
8115
  }
8045
8116
  s.assetsTick++;
8046
8117
  });
@@ -8353,12 +8424,12 @@ window.__ModuleLoader__.load({
8353
8424
  }
8354
8425
 
8355
8426
  /** 图片/视频模型多配置列表:名称 / 地址 / Key / 模型(/模式),可增删、设默认。 */
8356
- function Field({ label, value, onChange, placeholder, password }) {
8427
+ function Field({ label, value, onChange, placeholder, password, tip }) {
8357
8428
  // 敏感字段(API Key):默认脱敏,点击小眼睛切换明文显示
8358
8429
  const [reveal, setReveal] = useState(false);
8359
8430
  if (!password) {
8360
8431
  return h('div', { className: 'dshic-field' },
8361
- h('span', { className: 'dshic-field-label' }, label),
8432
+ h('span', { className: 'dshic-field-label' }, label, tip ? InfoTip(tip) : null),
8362
8433
  h('input', {
8363
8434
  className: 'dshic-input', type: 'text', value, placeholder: placeholder || '',
8364
8435
  onChange: (e) => onChange(e.target.value),
@@ -8448,14 +8519,15 @@ window.__ModuleLoader__.load({
8448
8519
  ),
8449
8520
  open ? [
8450
8521
  kind === 'video' ? h('div', { key: 'vendor', className: 'dshic-field' },
8451
- h('span', { className: 'dshic-field-label' }, T('vendorLabel')),
8522
+ h('div', { className: 'dshic-row' },
8523
+ h('span', { className: 'dshic-field-label' }, T('vendorLabel')),
8524
+ InfoTip((VIDEO_VENDORS[detectVideoVendor(p)] || {}).note || '')),
8452
8525
  h('select', {
8453
8526
  className: 'dshic-select', value: detectVideoVendor(p),
8454
8527
  onPointerDown: (e) => e.stopPropagation(),
8455
8528
  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); },
8456
8529
  },
8457
8530
  Object.entries(VIDEO_VENDORS).map(([id, vd]) => h('option', { key: id, value: id }, vd.label))),
8458
- h('div', { className: 'dshic-hint' }, (VIDEO_VENDORS[detectVideoVendor(p)] || {}).note || ''),
8459
8531
  ) : null,
8460
8532
  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 }),
8461
8533
  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 }),
@@ -8475,8 +8547,7 @@ window.__ModuleLoader__.load({
8475
8547
  h('span', {}, T('vidAudioOpt'))) : null,
8476
8548
  ],
8477
8549
  (kind === 'image' || detectVideoVendor(p) === 'openai' || detectVideoVendor(p) === 'seedance') ? [
8478
- h(Field, { key: 'extra', label: T('cfgExtra'), value: p.extra || '', onChange: (v) => patchProvider(p.id, 'extra', v), placeholder: '{"watermark":false,"prompt_extend":false}' }),
8479
- h('div', { className: 'dshic-hint' }, T('cfgExtraHint')),
8550
+ 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') }),
8480
8551
  ] : null,
8481
8552
  h('div', { className: 'dshic-row', style: { marginTop: 4 } },
8482
8553
  h('span', { className: 'dshic-field-label', style: { flex: 1, minWidth: 0 } }, T('modelsLabel')),
@@ -8637,10 +8708,7 @@ window.__ModuleLoader__.load({
8637
8708
  h('span', {}, T('seedanceWatermark')))
8638
8709
  : null,
8639
8710
  (kind === 'image' || kind === 'video')
8640
- ? h(Field, { key: 'extra', label: T('cfgExtra'), value: cfg.extra || '', onChange: (v) => patch(cfg.id, 'extra', v), placeholder: '{"watermark":false,"prompt_extend":false}' })
8641
- : null,
8642
- (kind === 'image' || kind === 'video')
8643
- ? h('div', { className: 'dshic-hint' }, T('cfgExtraHint'))
8711
+ ? 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') })
8644
8712
  : null,
8645
8713
  kind === 'tts'
8646
8714
  ? 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.0",
3
+ "version": "0.18.2",
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",