zcode-pro 0.20.0 → 0.20.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.
package/dist/inject.js CHANGED
@@ -59,6 +59,7 @@
59
59
  proxyScopeSyncDesc: "WebDAV \u4E0A\u4F20\u4E0E\u4E0B\u8F7D\u8D70\u4EE3\u7406\u3002\u9ED8\u8BA4\u5173\u95ED\uFF1A\u575A\u679C\u4E91\u7B49\u56FD\u5185\u7F51\u76D8\u76F4\u8FDE\u5373\u53EF\uFF0C\u4EE3\u7406\u505C\u4E86\u540C\u6B65\u4F1A\u8DDF\u7740\u5931\u8D25\u3002",
60
60
  tabSync: "\u540C\u6B65",
61
61
  tabDebug: "\u8C03\u8BD5",
62
+ testNoSessionHint: "\u9009\u62E9\u201C\u8DDF\u968F\u5F53\u524D\u4F1A\u8BDD[\u5F53\u524D\u6A21\u578B]\u201D\u65F6\uFF0C\u9009\u62E9\u4E0A\u6B21\u4F1A\u8BDD\u4E2D\u4F7F\u7528\u7684\u4F9B\u5E94\u5546[\u6A21\u578B]",
62
63
  syncDesc: "\u624B\u52A8\u628A ZCode Pro \u8BBE\u7F6E\u4E0E\u6A21\u578B\u8BBE\u7F6E\u5907\u4EFD\u5230 WebDAV\uFF0C\u6216\u5728\u65B0\u7535\u8111\u4E0A\u6062\u590D\u3002\u4E24\u9879\u5404\u81EA\u72EC\u7ACB\u6210\u6587\u4EF6\uFF0C\u53EF\u53EA\u4E0A\u4F20\u6216\u53EA\u8986\u76D6\u5176\u4E2D\u4E00\u9879\uFF1B\u6BCF\u6B21\u4E0A\u4F20\u4F1A\u989D\u5916\u4FDD\u7559\u4E00\u4EFD\u5E26\u65F6\u95F4\u6233\u7684\u5386\u53F2\u7248\u672C\uFF08\u4FDD\u7559\u591A\u5C11\u4E2A\u89C1\u201C\u5386\u53F2\u7248\u672C\u201D\u4E00\u884C\uFF0C\u4E0A\u4F20\u540E\u81EA\u52A8\u5220\u9664\u8D85\u51FA\u7684\u6700\u65E7\u7248\u672C\uFF09\uFF0C\u4E0B\u8F7D\u65F6\u4EFB\u9009\u4E00\u4E2A\u7248\u672C\u6062\u590D\uFF1B\u4EC5\u5728\u70B9\u51FB\u6309\u94AE\u65F6\u4F20\u8F93\uFF0C\u4E0D\u505A\u81EA\u52A8\u540C\u6B65\u3002",
63
64
  syncUrlPlaceholder: "https://dav.jianguoyun.com/dav/",
64
65
  syncDir: "\u4E0A\u4F20\u76EE\u5F55",
@@ -254,11 +255,10 @@
254
255
  visionSkipMinutes: "\u8DF3\u8FC7\u5206\u949F\u6570",
255
256
  visionSkipMinutesHint: "\u8DF3\u8FC7\u591A\u4E45\u540E\u81EA\u52A8\u6062\u590D\u5C1D\u8BD5\uFF1B\u671F\u95F4\u4EFB\u4E00\u6B21\u6210\u529F\u5373\u6E05\u96F6\u8BA1\u6570",
256
257
  visionResetChain: "\u6062\u590D\u9ED8\u8BA4\u94FE",
257
- visionResetChainConfirm: "\u5C06\u94FE\u6A21\u5F0F\u4E0E\u4EE3\u7406\u5217\u8868\u6062\u590D\u4E3A\u9ED8\u8BA4\u4E24\u7EA7\uFF08glm-session \u2192 glm-flash\uFF09\uFF0C\u5F53\u524D\u5BF9\u4EE3\u7406\u7684\u4FEE\u6539\u4F1A\u4E22\u5931\u3002\u7EE7\u7EED\uFF1F",
258
- visionBaseUrl: "\u63A5\u53E3\u5730\u5740 baseUrl",
258
+ visionResetChainConfirm: "\u5C06\u94FE\u6A21\u5F0F\u4E0E\u4EE3\u7406\u5217\u8868\u6062\u590D\u4E3A\u9ED8\u8BA4\u5355\u7EA7\uFF08session\uFF1A\u8DDF\u968F\u4F1A\u8BDD\u4E0E\u5176\u5F53\u524D\u6A21\u578B\uFF09\uFF0C\u5F53\u524D\u5BF9\u4EE3\u7406\u7684\u4FEE\u6539\u4F1A\u4E22\u5931\u3002\u7EE7\u7EED\uFF1F",
259
+ visionBaseUrl: "\u63A5\u53E3\u5730\u5740",
259
260
  visionModel: "\u6A21\u578B",
260
261
  visionApiKey: "API Key",
261
- visionApiKeyHint: "\u7559\u7A7A\u81EA\u52A8\u53D6 GLM \u8BA2\u9605 key",
262
262
  visionPrompt: "\u8BC6\u522B\u63D0\u793A\u8BCD\uFF08pipeline \u540E\u7EED\u6B65\u53EF\u7528 {prev} \u5F15\u7528\u4E0A\u4E00\u6B65\uFF09",
263
263
  visionTest: "\u6D4B\u8BD5",
264
264
  visionTesting: "\u6D4B\u8BD5\u4E2D\u2026",
@@ -424,6 +424,7 @@
424
424
  proxyScopeSyncDesc: "WebDAV upload and download go through the proxy. Off by default: domestic WebDAV providers work fine direct, and a stopped proxy breaks sync.",
425
425
  tabSync: "Sync",
426
426
  tabDebug: "Debug",
427
+ testNoSessionHint: "With \u201Cfollow current session [current model]\u201D selected, the provider [model] of the last session is used.",
427
428
  syncDesc: 'Manually back up ZCode Pro settings and model settings to WebDAV, or restore them on a new computer. The two items are stored as separate files, so you can upload or overwrite either one alone; every upload also keeps a timestamped history version (how many to keep is set in the "History" row; older ones are removed automatically after upload) and download lets you pick any of them. Data moves only when you click, never automatically.',
428
429
  syncUrlPlaceholder: "https://dav.example.com/dav/",
429
430
  syncDir: "Folder",
@@ -619,11 +620,10 @@
619
620
  visionSkipMinutes: "Skip minutes",
620
621
  visionSkipMinutesHint: "How long a skipped proxy rests before being retried; any success resets the counter",
621
622
  visionResetChain: "Reset default chain",
622
- visionResetChainConfirm: "Reset chain mode and the proxy list to the default 2-level chain (glm-session \u2192 glm-flash)? Current proxy edits will be lost. Continue?",
623
- visionBaseUrl: "Base URL",
623
+ visionResetChainConfirm: "Reset chain mode and the proxy list to the default single-level chain (session: follow session and its current model)? Current proxy edits will be lost. Continue?",
624
+ visionBaseUrl: "API URL",
624
625
  visionModel: "Model",
625
626
  visionApiKey: "API Key",
626
- visionApiKeyHint: "Leave empty to reuse the GLM subscription key",
627
627
  visionPrompt: "Recognition prompt ({prev} = previous step in pipeline)",
628
628
  visionTest: "Test",
629
629
  visionTesting: "Testing\u2026",
@@ -4126,16 +4126,15 @@
4126
4126
  const VISION_DEFAULT_CFG = {
4127
4127
  enabled: true,
4128
4128
  chainMode: "fallback",
4129
- chain: ["glm-session", "glm-flash"],
4129
+ chain: ["session"],
4130
4130
  proxies: [
4131
- { name: "glm-session", useProvider: "session", model: "glm-5.3-flash", prompt: VISION_DEFAULT_PROMPT },
4132
- { name: "glm-flash", baseUrl: "https://open.bigmodel.cn/api/anthropic", model: "glm-5.3-flash", apiKey: "", format: "anthropic", prompt: VISION_DEFAULT_PROMPT }
4131
+ { name: "session", useProvider: "session", model: "", prompt: VISION_DEFAULT_PROMPT }
4133
4132
  ],
4134
4133
  pollMs: 3e3,
4135
4134
  apiTimeoutMs: 12e4,
4136
4135
  // 视觉上游冷启动可能近 2 分钟,与插件 DEFAULT_CONFIG 一致
4137
4136
  compressThresholdKB: 1024,
4138
- skipAfterFailures: 4,
4137
+ skipAfterFailures: 2,
4139
4138
  skipMinutes: 30,
4140
4139
  forceIntercept: true
4141
4140
  };
@@ -4279,26 +4278,22 @@
4279
4278
  n
4280
4279
  );
4281
4280
  };
4282
- const cards = visionCfg.proxies.map((p, i) => h(
4283
- "div",
4284
- { class: "mt-2 rounded-xl border border-border p-2" },
4285
- h(
4281
+ const cards = visionCfg.proxies.map((p, i) => {
4282
+ const manualConnCard = isManualConn(p.useProvider, titleGenProviders) ? [h(
4286
4283
  "div",
4287
- { class: "flex flex-wrap items-center gap-2" },
4288
- h("span", { class: "shrink-0 rounded-md bg-surface px-1.5 py-0.5 text-ui-xs tabular-nums text-foreground-subtle" }, String(i + 1)),
4289
- // 名称输入框用 flex-1 占剩余宽度,与序号同行;textIn 的 w-full 会把序号挤成单独一行
4290
- (() => {
4291
- const n = h("input", { type: "text", value: p.name || "", placeholder: L.visionName, class: visionInputCls + " min-w-0 flex-1" });
4292
- n.addEventListener("input", () => {
4293
- p.name = n.value;
4294
- saveVisionSoon();
4295
- });
4296
- return n;
4297
- })(),
4298
- (() => {
4284
+ { class: "mt-2 grid grid-cols-1 gap-2 rounded-xl border border-border p-2 sm:grid-cols-3" },
4285
+ field(L.visionBaseUrl, textIn(p.baseUrl, (v) => {
4286
+ p.baseUrl = v;
4287
+ saveVisionSoon();
4288
+ })),
4289
+ field(L.visionApiKey, textIn(p.apiKey, (v) => {
4290
+ p.apiKey = v;
4291
+ saveVisionSoon();
4292
+ }, { type: "password" })),
4293
+ field(L.titleGenFormat, (() => {
4299
4294
  const sel = h(
4300
4295
  "select",
4301
- { class: "h-7 shrink-0 rounded-lg border border-border bg-input px-1.5 text-ui-xs text-foreground outline-none" },
4296
+ { class: "h-8 w-full rounded-lg border border-border bg-input px-2 text-ui-sm text-foreground outline-none" },
4302
4297
  h("option", { value: "anthropic" }, "anthropic"),
4303
4298
  h("option", { value: "openai" }, "openai")
4304
4299
  );
@@ -4308,78 +4303,85 @@
4308
4303
  void persistVision();
4309
4304
  });
4310
4305
  return sel;
4311
- })(),
4312
- smallBtn(L.visionMoveUp, () => {
4313
- if (i <= 0) return;
4314
- visionCfg.proxies.splice(i - 1, 0, visionCfg.proxies.splice(i, 1)[0]);
4315
- void persistVision();
4316
- renderVision();
4317
- }, i === 0 ? "pointer-events-none opacity-40" : ""),
4318
- smallBtn(L.visionMoveDown, () => {
4319
- if (i >= visionCfg.proxies.length - 1) return;
4320
- visionCfg.proxies.splice(i + 1, 0, visionCfg.proxies.splice(i, 1)[0]);
4321
- void persistVision();
4322
- renderVision();
4323
- }, i === visionCfg.proxies.length - 1 ? "pointer-events-none opacity-40" : ""),
4324
- smallBtn(L.visionRemove, () => {
4325
- visionCfg.proxies.splice(i, 1);
4326
- void persistVision();
4327
- renderVision();
4328
- })
4329
- ),
4330
- h(
4331
- "div",
4332
- { class: "mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2" },
4333
- // 供应商与模型走 provider-fields 共用逻辑:跟随会话时模型下拉为
4334
- // 全部供应商模型汇总,具体供应商为其模型列表;已删 id 标注“已删除”
4335
- field(L.visionUseProvider, comboField(
4336
- p.useProvider || "",
4337
- providerOptionsOf(titleGenProviders, L),
4338
- (v, src) => {
4339
- p.useProvider = v;
4340
- saveVisionSoon();
4341
- if (src === "select") renderVision();
4342
- },
4343
- { placeholder: L.titleGenProviderSession, staleLabel: L.titleGenProviderDeleted }
4344
- )),
4345
- // 跟随会话时模型下拉头部有“跟随当前会话当前模型”(值为空,
4346
- // 插件 ≥1.6.1 在调用时解析为会话当前模型);具体供应商为其模型列表
4347
- field(L.visionModel, comboField(
4348
- p.model || "",
4349
- modelOptionsOf(p.useProvider, titleGenProviders, {
4350
- followSession: (p.useProvider || "").trim() === "session",
4351
- followSessionLabel: L.titleGenModelFollowSession
4352
- }),
4353
- (v) => {
4354
- p.model = v;
4355
- saveVisionSoon();
4356
- },
4357
- {
4358
- placeholder: modelPlaceholderOf(p.useProvider, L),
4359
- emptyLabel: (p.useProvider || "").trim() === "session" ? L.titleGenModelFollowSession : L.titleGenModelEmpty
4360
- }
4361
- )),
4362
- ...isManualConn(p.useProvider, titleGenProviders) ? [
4363
- field(L.visionBaseUrl, textIn(p.baseUrl, (v) => {
4364
- p.baseUrl = v;
4365
- saveVisionSoon();
4366
- })),
4367
- field(L.visionApiKey + " \xB7 " + L.visionApiKeyHint, textIn(p.apiKey, (v) => {
4368
- p.apiKey = v;
4306
+ })())
4307
+ )] : [];
4308
+ return h("div", { class: "mt-2 rounded-xl border border-border p-2" }, [
4309
+ h(
4310
+ "div",
4311
+ { class: "flex flex-wrap items-center gap-2" },
4312
+ h("span", { class: "shrink-0 rounded-md bg-surface px-1.5 py-0.5 text-ui-xs tabular-nums text-foreground-subtle" }, String(i + 1)),
4313
+ // 名称输入框用 flex-1 占剩余宽度,与序号同行;textIn 的 w-full 会把序号挤成单独一行
4314
+ (() => {
4315
+ const n = h("input", { type: "text", value: p.name || "", placeholder: L.visionName, class: visionInputCls + " min-w-0 flex-1" });
4316
+ n.addEventListener("input", () => {
4317
+ p.name = n.value;
4318
+ saveVisionSoon();
4319
+ });
4320
+ return n;
4321
+ })(),
4322
+ smallBtn(L.visionMoveUp, () => {
4323
+ if (i <= 0) return;
4324
+ visionCfg.proxies.splice(i - 1, 0, visionCfg.proxies.splice(i, 1)[0]);
4325
+ void persistVision();
4326
+ renderVision();
4327
+ }, i === 0 ? "pointer-events-none opacity-40" : ""),
4328
+ smallBtn(L.visionMoveDown, () => {
4329
+ if (i >= visionCfg.proxies.length - 1) return;
4330
+ visionCfg.proxies.splice(i + 1, 0, visionCfg.proxies.splice(i, 1)[0]);
4331
+ void persistVision();
4332
+ renderVision();
4333
+ }, i === visionCfg.proxies.length - 1 ? "pointer-events-none opacity-40" : ""),
4334
+ smallBtn(L.visionRemove, () => {
4335
+ visionCfg.proxies.splice(i, 1);
4336
+ void persistVision();
4337
+ renderVision();
4338
+ })
4339
+ ),
4340
+ h(
4341
+ "div",
4342
+ { class: "mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2" },
4343
+ // 供应商与模型走 provider-fields 共用逻辑:跟随会话时模型下拉为
4344
+ // 全部供应商模型汇总,具体供应商为其模型列表;已删 id 标注“已删除”
4345
+ field(L.visionUseProvider, comboField(
4346
+ p.useProvider || "",
4347
+ providerOptionsOf(titleGenProviders, L),
4348
+ (v, src) => {
4349
+ p.useProvider = v;
4350
+ saveVisionSoon();
4351
+ if (src === "select") renderVision();
4352
+ },
4353
+ { placeholder: L.titleGenProviderSession, staleLabel: L.titleGenProviderDeleted }
4354
+ )),
4355
+ // 跟随会话时模型下拉头部有“跟随当前会话当前模型”(值为空,
4356
+ // 插件 ≥1.6.1 在调用时解析为会话当前模型);具体供应商为其模型列表
4357
+ field(L.visionModel, comboField(
4358
+ p.model || "",
4359
+ modelOptionsOf(p.useProvider, titleGenProviders, {
4360
+ followSession: (p.useProvider || "").trim() === "session",
4361
+ followSessionLabel: L.titleGenModelFollowSession
4362
+ }),
4363
+ (v) => {
4364
+ p.model = v;
4365
+ saveVisionSoon();
4366
+ },
4367
+ {
4368
+ placeholder: modelPlaceholderOf(p.useProvider, L),
4369
+ emptyLabel: (p.useProvider || "").trim() === "session" ? L.titleGenModelFollowSession : L.titleGenModelEmpty
4370
+ }
4371
+ ))
4372
+ ),
4373
+ ...manualConnCard,
4374
+ (() => {
4375
+ const n = h("textarea", { class: "zcodepro-textarea zcodepro-textarea-sm", rows: "3", placeholder: L.visionPrompt });
4376
+ n.value = p.prompt || "";
4377
+ n.addEventListener("input", () => {
4378
+ p.prompt = n.value;
4369
4379
  saveVisionSoon();
4370
- }, { type: "password" }))
4371
- ] : []
4372
- ),
4373
- (() => {
4374
- const n = h("textarea", { class: "zcodepro-textarea zcodepro-textarea-sm", rows: "3", placeholder: L.visionPrompt });
4375
- n.value = p.prompt || "";
4376
- n.addEventListener("input", () => {
4377
- p.prompt = n.value;
4378
- saveVisionSoon();
4379
- });
4380
- return field(L.visionPrompt, n);
4381
- })()
4382
- ));
4380
+ });
4381
+ return field(L.visionPrompt, n);
4382
+ })()
4383
+ ]);
4384
+ });
4383
4385
  let testBtn;
4384
4386
  testBtn = smallBtn(L.visionTest, async () => {
4385
4387
  testBtn.disabled = true;
@@ -4390,7 +4392,6 @@
4390
4392
  testBtn.disabled = false;
4391
4393
  testBtn.textContent = L.visionTest;
4392
4394
  visionTestPre.textContent = res.output || res.error || "";
4393
- if (!res.ok) showToast(L.visionTestFailed + ": " + errText(res), "error");
4394
4395
  });
4395
4396
  paneVision.replaceChildren(
4396
4397
  h("p", { class: "text-ui-sm/relaxed text-foreground-subtle" }, L.visionDesc),
@@ -4455,7 +4456,7 @@
4455
4456
  n
4456
4457
  );
4457
4458
  })(),
4458
- numIn(visionCfg.skipAfterFailures, { min: 0, dflt: 4, title: L.visionSkipAfterFailuresHint, label: L.visionSkipAfterFailures, commit: (v) => {
4459
+ numIn(visionCfg.skipAfterFailures, { min: 0, dflt: 2, title: L.visionSkipAfterFailuresHint, label: L.visionSkipAfterFailures, commit: (v) => {
4459
4460
  visionCfg.skipAfterFailures = v;
4460
4461
  } }),
4461
4462
  numIn(visionCfg.skipMinutes, { min: 1, dflt: 30, title: L.visionSkipMinutesHint, label: L.visionSkipMinutes, commit: (v) => {
@@ -4470,7 +4471,7 @@
4470
4471
  "div",
4471
4472
  { class: "flex items-center gap-1.5" },
4472
4473
  smallBtn("+ " + L.visionAddProxy, () => {
4473
- visionCfg.proxies.push({ name: "", baseUrl: "https://open.bigmodel.cn/api/anthropic", model: "glm-5.3-flash", apiKey: "", format: "anthropic", prompt: "" });
4474
+ visionCfg.proxies.push({ name: "", baseUrl: "https://open.bigmodel.cn/api/anthropic", model: "glm-5.3-flash", apiKey: "", format: "anthropic", prompt: VISION_DEFAULT_PROMPT });
4474
4475
  renderVision();
4475
4476
  }),
4476
4477
  smallBtn(L.visionResetChain, () => {
@@ -4481,7 +4482,12 @@
4481
4482
  renderVision();
4482
4483
  })
4483
4484
  ),
4484
- testBtn
4485
+ h(
4486
+ "div",
4487
+ { class: "flex min-w-0 items-center justify-end gap-2" },
4488
+ h("span", { class: "min-w-0 text-right text-ui-xs/relaxed text-foreground-subtle" }, L.testNoSessionHint),
4489
+ testBtn
4490
+ )
4485
4491
  ),
4486
4492
  visionTestPre
4487
4493
  );
@@ -5231,11 +5237,9 @@
5231
5237
  testBtn.textContent = L.titleGenTest;
5232
5238
  if (res && res.ok) {
5233
5239
  testState.textContent = L.titleGenTestOk + res.title;
5234
- showToast(res.title, "success");
5235
5240
  } else {
5236
5241
  testState.textContent = res && errText(res) || L.titleGenTestFailed;
5237
5242
  testState.className = "min-w-0 flex-1 truncate text-ui-xs/relaxed text-destructive";
5238
- showToast(L.titleGenTestFailed + ": " + (res ? errText(res) : ""), "error");
5239
5243
  }
5240
5244
  }, "shrink-0");
5241
5245
  paneTitleGen.replaceChildren(
@@ -5275,12 +5279,18 @@
5275
5279
  ),
5276
5280
  promptArea
5277
5281
  ),
5278
- // 测试
5282
+ // 测试(两行:输入框整行;说明 + 按钮一行。结果只显示在本区下方,
5283
+ // 不弹窗;从没对话过时的引导见 testState 错误文案)
5279
5284
  h(
5280
5285
  "div",
5281
- { class: "mt-2 flex items-center gap-2 rounded-xl border border-border p-2" },
5286
+ { class: "mt-2 rounded-xl border border-border p-2" },
5282
5287
  sampleInput,
5283
- testBtn
5288
+ h(
5289
+ "div",
5290
+ { class: "mt-2 flex items-center justify-between gap-2" },
5291
+ h("span", { class: "min-w-0 text-ui-xs/relaxed text-foreground-subtle" }, L.testNoSessionHint),
5292
+ testBtn
5293
+ )
5284
5294
  ),
5285
5295
  testState
5286
5296
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zcode-pro",
3
- "version": "0.20.0",
3
+ "version": "0.20.2",
4
4
  "description": "ZCode Pro — 为 ZCode 桌面版增加界面增强(不修改客户端文件,通过 ZCode Pro 快捷方式启动即可),核心功能:项目自定义别名",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,10 +4,11 @@
4
4
 
5
5
  import { createServer } from 'node:http';
6
6
  import { execFile, spawn, spawnSync } from 'node:child_process';
7
+ import { createDecipheriv, createHash } from 'node:crypto';
7
8
  import { appendFileSync, chmodSync, copyFileSync, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
8
9
  import { mkdtemp } from 'node:fs/promises'; // 注意:fs 的 mkdtemp 是回调版,await 它会把 undefined 当回调
9
10
  import { basename, delimiter, dirname, isAbsolute, join, resolve, sep } from 'node:path';
10
- import { homedir, tmpdir } from 'node:os';
11
+ import { homedir, tmpdir, userInfo } from 'node:os';
11
12
  import { fileURLToPath, pathToFileURL } from 'node:url';
12
13
  import { readSettings, writeSettingsAtomic, remapSettingsPaths, isProjectOpenInTabs } from './settings.mjs';
13
14
  import { taskIndexPath, probeTaskIndexWritable, remapTaskIndexPaths, taskIndexDriverAvailable, listOverriddenTitles, listTaskWorkspaceMap, setTaskTitleIfNotOverridden, setCustomSessionTitle, isTaskTitleOverridden, reconcileCustomTitles, getTaskProviderModel } from './taskIndex.mjs';
@@ -16,7 +17,7 @@ import { reorderWorkspaceTasks, reorderGroupMembers } from './taskOrder.mjs';
16
17
  import { webdavRequest, webdavPutWithMkcol, webdavHttpText, parseWebdavListing, findCurl } from './webdav.mjs';
17
18
  import { resolveZcodeExecutable } from './paths.mjs';
18
19
 
19
- const VERSION = '0.20.0';
20
+ const VERSION = '0.20.2';
20
21
 
21
22
  // 全局提示词固定在用户主目录:官方加载器按 HOME/USERPROFILE 拼 .zcode/AGENTS.md,
22
23
  // 不读 ZCODE_DATA_BASE_DIR(数据根迁走时全局指令仍在原位)。
@@ -2005,6 +2006,17 @@ function titleGenProviderDisplayName(id, name, planKinds = {}) {
2005
2006
  return (name && String(name).trim()) || id;
2006
2007
  }
2007
2008
 
2009
+ // headroom 压缩路由(/hr-join)注册表:供应商接入后 baseUrl 统一指向 127.0.0.1:8787,
2010
+ // 地址不再带协议线索,真实格式(anthropic/openai)记录在 ~/.zcode/headroom.json 的
2011
+ // providers 里;按供应商 id 查,查不到返回空串。同一查表逻辑在 zcode-plugins 的
2012
+ // vision-lib.mjs(headroomFormatOf)与 hr-join 接入规则各有一份,改 providers
2013
+ // 结构时三处同改
2014
+ function headroomFormatOf(headroomProviders, providerId) {
2015
+ const hit = (headroomProviders || []).find((p) => p && p.id === providerId);
2016
+ const f = String(hit?.format || '').toLowerCase();
2017
+ return f === 'anthropic' || f === 'openai' ? f : '';
2018
+ }
2019
+
2008
2020
  // 供应商条目(智能起名与视觉代理共用),列表范围与 ZCode 界面的供应商列表
2009
2021
  // 保持一致:
2010
2022
  // - 内置套餐(builtin:*):有地址、密钥才列出(无密钥的 helper 无法在会话外
@@ -2013,13 +2025,16 @@ function titleGenProviderDisplayName(id, name, planKinds = {}) {
2013
2025
  // 自定义列表就来自它),provider 表里不在其中的残留条目(已弃用的中转等)
2014
2026
  // 不列出;
2015
2027
  // - 协议:api.type / kind 缺失时按地址推断(含 anthropic → anthropic,含
2016
- // openai 或以 /v\d 结尾 → openai),两个都推不出才放弃该条目。
2028
+ // openai 或以 /v\d 结尾 → openai);地址推不出(典型:指向 headroom 压缩路由
2029
+ // 127.0.0.1:8787)时查上面的注册表,仍没有才放弃该条目。
2017
2030
  // 两张来源表同 id 合并(模型并集、原始名收进别名)。models 为该供应商的
2018
2031
  // 可选模型(下拉用,可为空)。导出以便测试
2019
2032
  export function listTitleGenProviders() {
2020
2033
  const out = [];
2021
2034
  const byId = new Map();
2022
2035
  const planKinds = codingPlanKindByFamily();
2036
+ // 注册表读一次,供下方各条目的协议判定兜底复用(文件不存在时为空数组)
2037
+ const headroomProviders = (readJsonFile(join(homedir(), '.zcode', 'headroom.json')) || {}).providers || [];
2023
2038
  const add = (id, { name, aliases = [], kind, baseUrl, apiKey, models = [] }) => {
2024
2039
  baseUrl = typeof baseUrl === 'string' ? baseUrl.trim() : '';
2025
2040
  apiKey = typeof apiKey === 'string' ? apiKey.trim() : '';
@@ -2027,6 +2042,7 @@ export function listTitleGenProviders() {
2027
2042
  let format = /anthropic/i.test(String(kind)) ? 'anthropic' : /openai/i.test(String(kind)) ? 'openai' : '';
2028
2043
  if (!format) format = /anthropic/i.test(baseUrl) ? 'anthropic'
2029
2044
  : (/openai/i.test(baseUrl) || /\/v\d+$/.test(baseUrl)) ? 'openai' : '';
2045
+ if (!format) format = headroomFormatOf(headroomProviders, id);
2030
2046
  if (!format) return;
2031
2047
  models = models.filter(Boolean);
2032
2048
  const known = byId.get(id);
@@ -2153,16 +2169,165 @@ export async function generateTitleWith(cfg, rawText, dataRoot = '', sessId = ''
2153
2169
  return title;
2154
2170
  }
2155
2171
 
2172
+ // 供应商 id 的匹配归一化(导出以便测试):会话索引里登录账号供应商带 account:
2173
+ // 前缀(如 account:bigmodel-individual-coding-plan),供应商列表里同名条目带
2174
+ // builtin: 前缀(builtin:bigmodel-coding-plan),个人/团队是同一端点——剥前缀、
2175
+ // 抹掉 individual/team 差异后按同一家匹配
2176
+ export function normalizeProviderIdForMatch(id) {
2177
+ return String(id || '')
2178
+ .replace(/^(account|builtin):/, '')
2179
+ .replace(/-(individual|team)-/, '-');
2180
+ }
2181
+
2182
+ // —— 登录账号供应商(Coding Plan)直连凭据的换取 ——
2183
+ // Coding Plan 的直连密钥不是静态保存的,官方桌面/CLI 用 OAuth access_token
2184
+ // 实时换“项目 token”。这里复刻同款流程(协议读自官方 project-access-token-
2185
+ // client.ts),让只登录、从未配置过供应商的机器也能直连起名:
2186
+ // ① 解密 v2/credentials.json 里的 oauth:<family>:access_token——加密格式
2187
+ // enc:v1:<iv>.<authTag>.<cipher>(AES-256-GCM),密钥推导与官方
2188
+ // credential-cipher.ts 一致(ZCODE_CREDENTIAL_SECRET 环境变量优先,否则
2189
+ // sha256("zcode-credential-fallback:平台:主目录:用户名")),同机同用户可解;
2190
+ // ② GET getCustomerInfo 定位个人(非团队)项目——优先名称含“默认机构/
2191
+ // 默认项目”的条目,否则取第一个;
2192
+ // ③ 在项目的 api_keys 里找官方建的 zcode-api-key(没有则创建);
2193
+ // ④ POST …/access_tokens 换项目 token(临期前进程内缓存复用)。
2194
+ // 管理接口的鉴权:bigmodel 用原始 token、zai 带 Bearer 前缀;签发接口两家
2195
+ // 都用 Bearer——与官方实现一致
2196
+ const ACCOUNT_FAMILY_BASE = {
2197
+ bigmodel: { manageOrigin: 'https://bigmodel.cn', apiBase: 'https://open.bigmodel.cn/api/anthropic' },
2198
+ zai: { manageOrigin: 'https://api.z.ai', apiBase: 'https://api.z.ai/api/anthropic' },
2199
+ };
2200
+ const accountTokenCache = new Map(); // family → { token, expiresAt }
2201
+
2202
+ // 解密 credentials.json 的 enc:v1: 值(非加密值原样返回;失败返回空串)
2203
+ export function decryptCredentialValue(value) {
2204
+ const s = String(value || '');
2205
+ if (!s.startsWith('enc:v1:')) return s;
2206
+ const [ivRaw, tagRaw, ctRaw] = s.slice('enc:v1:'.length).split('.');
2207
+ if (!ivRaw || !tagRaw || !ctRaw) return '';
2208
+ try {
2209
+ const secret = (process.env.ZCODE_CREDENTIAL_SECRET || '').trim()
2210
+ || `zcode-credential-fallback:${process.platform}:${homedir()}:${userInfo().username}`;
2211
+ const key = createHash('sha256').update(secret).digest();
2212
+ const d = createDecipheriv('aes-256-gcm', key, Buffer.from(ivRaw, 'base64url'));
2213
+ d.setAuthTag(Buffer.from(tagRaw, 'base64url'));
2214
+ return Buffer.concat([d.update(Buffer.from(ctRaw, 'base64url')), d.final()]).toString('utf8');
2215
+ } catch { return ''; }
2216
+ }
2217
+
2218
+ // 换取登录账号的项目 token;成功返回 { token, apiBase, expiresAt }
2219
+ export async function exchangeAccountProjectToken(family, dataRoot) {
2220
+ const conf = ACCOUNT_FAMILY_BASE[family];
2221
+ if (!conf) throw new Error(`不支持的供应商族 ${family}`);
2222
+ const cached = accountTokenCache.get(family);
2223
+ if (cached && Date.now() < cached.expiresAt - 5 * 60 * 1000) {
2224
+ return { token: cached.token, apiBase: conf.apiBase, expiresAt: cached.expiresAt };
2225
+ }
2226
+ const creds = readJsonFile(join(dataRoot, 'v2', 'credentials.json'));
2227
+ const loginToken = (creds && typeof creds === 'object')
2228
+ ? decryptCredentialValue(creds[`oauth:${family}:access_token`]) : '';
2229
+ if (!loginToken) throw new Error(`credentials.json 里没有可解的 ${family} 登录令牌(未登录或已过期)`);
2230
+
2231
+ // BIGMODEL_API_BASE_URL:官方同款测试环境覆盖,顺带让协议可注入测试
2232
+ const origin = (family === 'bigmodel' && (process.env.BIGMODEL_API_BASE_URL || '').trim())
2233
+ || conf.manageOrigin;
2234
+ // 管理接口沿用旧鉴权格式(bigmodel 原始 token / zai Bearer);签发接口用 Bearer
2235
+ const manageHeaders = {
2236
+ 'content-type': 'application/json',
2237
+ authorization: family === 'zai' ? `Bearer ${loginToken}` : loginToken,
2238
+ };
2239
+ const api = async (path, init = {}) => {
2240
+ const ctrl = new AbortController();
2241
+ const timer = setTimeout(() => ctrl.abort(), 20000);
2242
+ try {
2243
+ const resp = await fetch(origin + path, { ...init, signal: ctrl.signal, headers: manageHeaders });
2244
+ const doc = await resp.json().catch(() => null);
2245
+ const data = (doc && [200, 0, '200', '0'].includes(doc.code)) ? doc.data : null;
2246
+ if (data === null) throw new Error(`接口 ${path} 返回异常(${resp.status})`);
2247
+ return data;
2248
+ } finally { clearTimeout(timer); }
2249
+ };
2250
+
2251
+ // ① 客户信息 → ② 定位个人项目(projectType "2" 是团队项目,个人密钥不能挂在团队下)
2252
+ const cust = await api('/api/biz/customer/getCustomerInfo');
2253
+ const orgs = (Array.isArray(cust?.organizations) ? cust.organizations : [])
2254
+ .map((o) => ({ id: String(o?.organizationId || '').trim(), name: String(o?.organizationName || ''),
2255
+ projects: (Array.isArray(o?.projects) ? o.projects : [])
2256
+ .filter((p) => String(p?.projectType ?? '').trim() !== '2' && String(p?.projectId || '').trim()) }));
2257
+ const org = orgs.find((o) => o.id && o.projects.length && o.name.includes('默认机构')) || orgs.find((o) => o.id && o.projects.length);
2258
+ const project = org && (org.projects.find((p) => String(p?.projectName || '').includes('默认项目')) || org.projects[0]);
2259
+ if (!org || !project) throw new Error('账号下没有可用的个人项目(未订阅 Coding Plan?)');
2260
+
2261
+ // ③ 找官方建的 zcode-api-key,没有就建一个
2262
+ const keysUrl = `/api/biz/v1/organization/${encodeURIComponent(org.id)}/projects/${encodeURIComponent(project.projectId)}/api_keys`;
2263
+ const keys = await api(keysUrl);
2264
+ const keyEntry = (Array.isArray(keys) ? keys : []).find((k) => k && typeof k === 'object' && k.name === 'zcode-api-key')
2265
+ || await api(keysUrl, { method: 'POST', body: JSON.stringify({ name: 'zcode-api-key', usageScene: 1 }) });
2266
+ const apiKeyId = keyEntry && String(keyEntry.apiKey || '').trim();
2267
+ if (!apiKeyId) throw new Error('未能定位 Coding Plan 的 API Key');
2268
+
2269
+ // ④ 签发项目 token(此接口用 Bearer 鉴权;超时与 api() 同款)
2270
+ const issueCtrl = new AbortController();
2271
+ const issueTimer = setTimeout(() => issueCtrl.abort(), 20000);
2272
+ let issueResp;
2273
+ try {
2274
+ issueResp = await fetch(`${origin}${keysUrl}/${encodeURIComponent(apiKeyId)}/access_tokens`, {
2275
+ method: 'POST',
2276
+ signal: issueCtrl.signal,
2277
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${loginToken}` },
2278
+ body: JSON.stringify({ clientType: 'zcode', clientVersion: VERSION }),
2279
+ });
2280
+ } finally { clearTimeout(issueTimer); }
2281
+ const issue = await issueResp.json().catch(() => null);
2282
+ const issueData = (issue && [200, 0, '200', '0'].includes(issue.code)) ? issue.data : null;
2283
+ if (!issueData) throw new Error(`签发项目 token 失败(${issueResp.status})`);
2284
+ // enable 语义与官方一致:旧版接口不回该字段(实测回的是同义的 enabled),
2285
+ // 缺失视为开启,仅显式 false 才算未开启
2286
+ if (issueData.enable === false || !issueData.accessToken) throw new Error('项目 token 未开启(enable=false)');
2287
+ accountTokenCache.set(family, { token: issueData.accessToken, expiresAt: Number(issueData.expiresAt) || 0 });
2288
+ return { token: issueData.accessToken, apiBase: conf.apiBase, expiresAt: Number(issueData.expiresAt) || 0 };
2289
+ }
2290
+
2156
2291
  // 直连前的连接解析(见 generateTitleWith 的取值说明);解析不出抛明确错误
2157
2292
  async function resolveTitleGenConnection(cfg, providerId, model, dataRoot, sessId) {
2158
2293
  if (providerId === 'session') {
2159
- if (!sessId || !dataRoot) {
2160
- throw new Error('跟随当前会话需要会话信息(测试按钮请选择具体供应商或“不跟随”)');
2294
+ if (!dataRoot) throw new Error('跟随当前会话需要会话信息');
2295
+ // sessId 为空(设置面板测试按钮,没有会话上下文)时取任务索引里最近更新
2296
+ // 的会话——与视觉代理测试按钮同款语义
2297
+ const task = await getTaskProviderModel(dataRoot, sessId || '');
2298
+ if (!task || !task.providerId) {
2299
+ throw new Error(sessId ? '索引中查不到该会话的供应商信息' : '任务索引里没有会话记录(“跟随当前会话”取上次会话的供应商/模型):先在任意会话里发过消息,或改选具体供应商再测试');
2300
+ }
2301
+ const list = listTitleGenProviders();
2302
+ const entry = list.find((p) => p.id === task.providerId)
2303
+ || list.find((p) => normalizeProviderIdForMatch(p.id) === normalizeProviderIdForMatch(task.providerId));
2304
+ if (!entry) {
2305
+ // 登录账号供应商:本机没有物化密钥表(从未保存过供应商配置的机器不生成
2306
+ // v2/config.json)时按官方同款流程换取项目 token 直连(见下方实现)
2307
+ if (task.providerId.startsWith('account:')) {
2308
+ if (/start-plan/.test(task.providerId)) {
2309
+ throw new Error('会话用的是 Start Plan 登录供应商,暂不支持自动直连:请在此改选具体供应商或不跟随(自填地址)');
2310
+ }
2311
+ const family = task.providerId.includes(':zai') ? 'zai' : 'bigmodel';
2312
+ let accountConn;
2313
+ try {
2314
+ accountConn = await exchangeAccountProjectToken(family, dataRoot);
2315
+ } catch (err) {
2316
+ throw new Error(`登录账号供应商取直连密钥失败(${err?.message || err}):请在 ZCode 里重新登录,或在此改选具体供应商 / 不跟随`);
2317
+ }
2318
+ const useAccountModel = model || task.modelId;
2319
+ if (!useAccountModel) throw new Error('未能确定会话当前模型,请手动填写模型');
2320
+ return {
2321
+ format: 'anthropic',
2322
+ baseUrl: accountConn.apiBase,
2323
+ apiKey: accountConn.token,
2324
+ // 官方目录的模型名是大写 GLM- 前缀;会话索引里可能是小写(不同供应商
2325
+ // 记法不一),登录端点按目录写法规整
2326
+ model: /^glm-\d/.test(useAccountModel) ? useAccountModel.replace(/^glm/, 'GLM') : useAccountModel,
2327
+ };
2328
+ }
2329
+ throw new Error(`会话供应商 ${task.providerId} 无法在会话外直连(无密钥或协议未知)`);
2161
2330
  }
2162
- const task = await getTaskProviderModel(dataRoot, sessId);
2163
- if (!task || !task.providerId) throw new Error('索引中查不到该会话的供应商信息');
2164
- const entry = listTitleGenProviders().find((p) => p.id === task.providerId);
2165
- if (!entry) throw new Error(`会话供应商 ${task.providerId} 无法在会话外直连(无密钥或协议未知)`);
2166
2331
  const useModel = model || task.modelId;
2167
2332
  if (!useModel) throw new Error('未能确定会话当前模型,请手动填写模型');
2168
2333
  return { format: entry.format, baseUrl: entry.baseUrl, apiKey: entry.apiKey, model: useModel };
@@ -2236,7 +2401,7 @@ export function validateVisionConfig(raw) {
2236
2401
  apiTimeoutMs: clampInt(raw.apiTimeoutMs, 1000, 600000, 120000),
2237
2402
  compressThresholdKB: clampInt(raw.compressThresholdKB, 0, 102400, 1024),
2238
2403
  // 连续失败跳过(0 = 不跳过);跳过时长(分钟)
2239
- skipAfterFailures: clampInt(raw.skipAfterFailures, 0, 100, 4),
2404
+ skipAfterFailures: clampInt(raw.skipAfterFailures, 0, 100, 2),
2240
2405
  skipMinutes: clampInt(raw.skipMinutes, 1, 10080, 30),
2241
2406
  // 主模型能看图时是否也拦截识别(false = 跳过;flash 主模型始终不触发,见插件)
2242
2407
  forceIntercept: raw.forceIntercept !== false,
@@ -286,7 +286,9 @@ function isBusyError(err) {
286
286
  // 读某会话记录的模型信息(智能起名“跟随当前会话”用)。model 列存
287
287
  // “<供应商id>/<模型id>”(如 be0a90cb…/glm-5.3),拆开后返回
288
288
  // { providerId, modelId };没有斜线时 providerId 退回 provider 列的值。
289
- // 会话不存在返回 null;读取失败按 null 处理(调用方换明确报错)
289
+ // 会话不存在返回 null;读取失败按 null 处理(调用方换明确报错)。
290
+ // sessId 为空 = 取最近更新的会话(设置面板的测试按钮没有会话上下文,
291
+ // 与视觉代理的测试同款语义)
290
292
  export async function getTaskProviderModel(dataRoot, sessId) {
291
293
  const driver = await loadDriver();
292
294
  if (!driver) return null;
@@ -296,7 +298,7 @@ export async function getTaskProviderModel(dataRoot, sessId) {
296
298
  // model 列早十几秒(任务索引由桌面同步器延迟写;新会话头十几秒里它还是
297
299
  // 空值,起名第一次调用会因此解析到不存在的模型,实测 400 模型不存在)
298
300
  const usageFile = join(dataRoot, 'cli', 'db', 'db.sqlite');
299
- if (existsSync(usageFile)) {
301
+ if (existsSync(usageFile) && sessId) {
300
302
  try {
301
303
  if (driver.kind === 'node') {
302
304
  const db = new driver.DatabaseSync(usageFile, { readOnly: true });
@@ -318,16 +320,19 @@ export async function getTaskProviderModel(dataRoot, sessId) {
318
320
  const dbFile = taskIndexPath(dataRoot);
319
321
  if (!existsSync(dbFile)) return null;
320
322
  let row = null;
323
+ const where = sessId
324
+ ? `WHERE task_id = ${esc(sessId)} AND deleted = 0`
325
+ : 'WHERE deleted = 0 AND model != \'\' ORDER BY updated_at DESC LIMIT 1';
321
326
  try {
322
327
  if (driver.kind === 'node') {
323
328
  const db = new driver.DatabaseSync(dbFile);
324
329
  try {
325
330
  db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
326
- row = db.prepare('SELECT provider, model FROM tasks WHERE task_id = ? AND deleted = 0').get(sessId) || null;
331
+ row = db.prepare(`SELECT provider, model FROM tasks ${where}`).get() || null;
327
332
  } finally { db.close(); }
328
333
  } else {
329
334
  const r = runCli(dbFile,
330
- `SELECT coalesce(json_object('provider', provider, 'model', model), 'null') FROM tasks WHERE task_id = ${esc(sessId)} AND deleted = 0;`);
335
+ `SELECT coalesce(json_object('provider', provider, 'model', model), 'null') FROM (SELECT provider, model FROM tasks ${where});`);
331
336
  const parsed = JSON.parse((r.stdout.trim() || 'null'));
332
337
  row = parsed && typeof parsed === 'object' ? parsed : null;
333
338
  }
@@ -67,6 +67,7 @@ const zh = {
67
67
  proxyScopeSyncDesc: 'WebDAV 上传与下载走代理。默认关闭:坚果云等国内网盘直连即可,代理停了同步会跟着失败。',
68
68
  tabSync: '同步',
69
69
  tabDebug: '调试',
70
+ testNoSessionHint: '选择“跟随当前会话[当前模型]”时,选择上次会话中使用的供应商[模型]',
70
71
  syncDesc: '手动把 ZCode Pro 设置与模型设置备份到 WebDAV,或在新电脑上恢复。两项各自独立成文件,可只上传或只覆盖其中一项;每次上传会额外保留一份带时间戳的历史版本(保留多少个见“历史版本”一行,上传后自动删除超出的最旧版本),下载时任选一个版本恢复;仅在点击按钮时传输,不做自动同步。',
71
72
  syncUrlPlaceholder: 'https://dav.jianguoyun.com/dav/',
72
73
  syncDir: '上传目录',
@@ -262,11 +263,10 @@ const zh = {
262
263
  visionSkipMinutes: '跳过分钟数',
263
264
  visionSkipMinutesHint: '跳过多久后自动恢复尝试;期间任一次成功即清零计数',
264
265
  visionResetChain: '恢复默认链',
265
- visionResetChainConfirm: '将链模式与代理列表恢复为默认两级(glm-session → glm-flash),当前对代理的修改会丢失。继续?',
266
- visionBaseUrl: '接口地址 baseUrl',
266
+ visionResetChainConfirm: '将链模式与代理列表恢复为默认单级(session:跟随会话与其当前模型),当前对代理的修改会丢失。继续?',
267
+ visionBaseUrl: '接口地址',
267
268
  visionModel: '模型',
268
269
  visionApiKey: 'API Key',
269
- visionApiKeyHint: '留空自动取 GLM 订阅 key',
270
270
  visionPrompt: '识别提示词(pipeline 后续步可用 {prev} 引用上一步)',
271
271
  visionTest: '测试',
272
272
  visionTesting: '测试中…',
@@ -433,6 +433,7 @@ const en = {
433
433
  proxyScopeSyncDesc: 'WebDAV upload and download go through the proxy. Off by default: domestic WebDAV providers work fine direct, and a stopped proxy breaks sync.',
434
434
  tabSync: 'Sync',
435
435
  tabDebug: 'Debug',
436
+ testNoSessionHint: 'With “follow current session [current model]” selected, the provider [model] of the last session is used.',
436
437
  syncDesc: 'Manually back up ZCode Pro settings and model settings to WebDAV, or restore them on a new computer. The two items are stored as separate files, so you can upload or overwrite either one alone; every upload also keeps a timestamped history version (how many to keep is set in the "History" row; older ones are removed automatically after upload) and download lets you pick any of them. Data moves only when you click, never automatically.',
437
438
  syncUrlPlaceholder: 'https://dav.example.com/dav/',
438
439
  syncDir: 'Folder',
@@ -628,11 +629,10 @@ const en = {
628
629
  visionSkipMinutes: 'Skip minutes',
629
630
  visionSkipMinutesHint: 'How long a skipped proxy rests before being retried; any success resets the counter',
630
631
  visionResetChain: 'Reset default chain',
631
- visionResetChainConfirm: 'Reset chain mode and the proxy list to the default 2-level chain (glm-session → glm-flash)? Current proxy edits will be lost. Continue?',
632
- visionBaseUrl: 'Base URL',
632
+ visionResetChainConfirm: 'Reset chain mode and the proxy list to the default single-level chain (session: follow session and its current model)? Current proxy edits will be lost. Continue?',
633
+ visionBaseUrl: 'API URL',
633
634
  visionModel: 'Model',
634
635
  visionApiKey: 'API Key',
635
- visionApiKeyHint: 'Leave empty to reuse the GLM subscription key',
636
636
  visionPrompt: 'Recognition prompt ({prev} = previous step in pipeline)',
637
637
  visionTest: 'Test',
638
638
  visionTesting: 'Testing…',
@@ -1210,21 +1210,21 @@ export function openSettingsDialog() {
1210
1210
  // “视觉代理”:编辑 ~/.zcode/zcode-vision.json(zcode-vision 插件与 /vision-* 命令共用同一文件)。
1211
1211
  // 代理列表顺序即执行链;结构性改动(开关/模式/排序/删增)即时保存,文本输入防抖保存。
1212
1212
  // 注意:保存时链按全部代理重建——若用 /vision-chain 配过子集链,会被这里覆盖(两套入口语义如此,面板以列表为准)。
1213
- // 默认两级链与 zcode-vision 插件 DEFAULT_CONFIG 保持一致:glm-session(跟随会话)→ glm-flash(GLM 订阅直连)。
1213
+ // 默认单级链与 zcode-vision 插件 DEFAULT_CONFIG 保持一致:session(跟随会话供应商、
1214
+ // 模型留空=会话当前模型)。需要兜底级可手动加 GLM 订阅直连。
1214
1215
  // 赠送额度级 trust-build 暂不进默认:ZCode 网关不放行无人值守调用(ADR-0002)。
1215
1216
  const VISION_DEFAULT_PROMPT = '请详细描述这张图片的全部内容。若是界面或图表截图,请先把所有错误、警告、异常状态逐字引用出来(含完整原文),再描述整体布局、文字与关键数据。';
1216
1217
  const VISION_DEFAULT_CFG = {
1217
1218
  enabled: true,
1218
1219
  chainMode: 'fallback',
1219
- chain: ['glm-session', 'glm-flash'],
1220
+ chain: ['session'],
1220
1221
  proxies: [
1221
- { name: 'glm-session', useProvider: 'session', model: 'glm-5.3-flash', prompt: VISION_DEFAULT_PROMPT },
1222
- { name: 'glm-flash', baseUrl: 'https://open.bigmodel.cn/api/anthropic', model: 'glm-5.3-flash', apiKey: '', format: 'anthropic', prompt: VISION_DEFAULT_PROMPT },
1222
+ { name: 'session', useProvider: 'session', model: '', prompt: VISION_DEFAULT_PROMPT },
1223
1223
  ],
1224
1224
  pollMs: 3000,
1225
1225
  apiTimeoutMs: 120000, // 视觉上游冷启动可能近 2 分钟,与插件 DEFAULT_CONFIG 一致
1226
1226
  compressThresholdKB: 1024,
1227
- skipAfterFailures: 4,
1227
+ skipAfterFailures: 2,
1228
1228
  skipMinutes: 30,
1229
1229
  forceIntercept: true,
1230
1230
  };
@@ -1343,7 +1343,23 @@ export function openSettingsDialog() {
1343
1343
  };
1344
1344
  // 接口地址 / API Key 是否显示走 provider-fields 的共用规则
1345
1345
  //(isManualConn:不跟随或供应商已删才显示,连接其余由跟随对象负责)
1346
- const cards = visionCfg.proxies.map((p, i) => h('div', { class: 'mt-2 rounded-xl border border-border p-2' },
1346
+ const cards = visionCfg.proxies.map((p, i) => {
1347
+ // 不跟随(或供应商已删)才显示的手动连接行:接口地址 + API Key + 协议
1348
+ // 三栏一行(与智能起名同款);协议跟随供应商解析,平时不显示
1349
+ const manualConnCard = isManualConn(p.useProvider, titleGenProviders)
1350
+ ? [h('div', { class: 'mt-2 grid grid-cols-1 gap-2 rounded-xl border border-border p-2 sm:grid-cols-3' },
1351
+ field(L.visionBaseUrl, textIn(p.baseUrl, (v) => { p.baseUrl = v; saveVisionSoon(); })),
1352
+ field(L.visionApiKey, textIn(p.apiKey, (v) => { p.apiKey = v; saveVisionSoon(); }, { type: 'password' })),
1353
+ field(L.titleGenFormat, (() => {
1354
+ const sel = h('select', { class: 'h-8 w-full rounded-lg border border-border bg-input px-2 text-ui-sm text-foreground outline-none' },
1355
+ h('option', { value: 'anthropic' }, 'anthropic'),
1356
+ h('option', { value: 'openai' }, 'openai'));
1357
+ sel.value = p.format === 'openai' ? 'openai' : 'anthropic';
1358
+ sel.addEventListener('change', () => { p.format = sel.value; void persistVision(); });
1359
+ return sel;
1360
+ })()))]
1361
+ : [];
1362
+ return h('div', { class: 'mt-2 rounded-xl border border-border p-2' }, [
1347
1363
  h('div', { class: 'flex flex-wrap items-center gap-2' },
1348
1364
  h('span', { class: 'shrink-0 rounded-md bg-surface px-1.5 py-0.5 text-ui-xs tabular-nums text-foreground-subtle' }, String(i + 1)),
1349
1365
  // 名称输入框用 flex-1 占剩余宽度,与序号同行;textIn 的 w-full 会把序号挤成单独一行
@@ -1352,14 +1368,6 @@ export function openSettingsDialog() {
1352
1368
  n.addEventListener('input', () => { p.name = n.value; saveVisionSoon(); });
1353
1369
  return n;
1354
1370
  })(),
1355
- (() => {
1356
- const sel = h('select', { class: 'h-7 shrink-0 rounded-lg border border-border bg-input px-1.5 text-ui-xs text-foreground outline-none' },
1357
- h('option', { value: 'anthropic' }, 'anthropic'),
1358
- h('option', { value: 'openai' }, 'openai'));
1359
- sel.value = p.format === 'openai' ? 'openai' : 'anthropic';
1360
- sel.addEventListener('change', () => { p.format = sel.value; void persistVision(); });
1361
- return sel;
1362
- })(),
1363
1371
  smallBtn(L.visionMoveUp, () => {
1364
1372
  if (i <= 0) return;
1365
1373
  visionCfg.proxies.splice(i - 1, 0, visionCfg.proxies.splice(i, 1)[0]);
@@ -1398,18 +1406,16 @@ export function openSettingsDialog() {
1398
1406
  emptyLabel: (p.useProvider || '').trim() === 'session'
1399
1407
  ? L.titleGenModelFollowSession : L.titleGenModelEmpty,
1400
1408
  })),
1401
- ...(isManualConn(p.useProvider, titleGenProviders) ? [
1402
- field(L.visionBaseUrl, textIn(p.baseUrl, (v) => { p.baseUrl = v; saveVisionSoon(); })),
1403
- field(L.visionApiKey + ' · ' + L.visionApiKeyHint, textIn(p.apiKey, (v) => { p.apiKey = v; saveVisionSoon(); }, { type: 'password' })),
1404
- ] : []),
1405
1409
  ),
1410
+ ...manualConnCard,
1406
1411
  (() => {
1407
1412
  const n = h('textarea', { class: 'zcodepro-textarea zcodepro-textarea-sm', rows: '3', placeholder: L.visionPrompt });
1408
1413
  n.value = p.prompt || '';
1409
1414
  n.addEventListener('input', () => { p.prompt = n.value; saveVisionSoon(); });
1410
1415
  return field(L.visionPrompt, n);
1411
1416
  })(),
1412
- ));
1417
+ ]);
1418
+ });
1413
1419
  let testBtn;
1414
1420
  testBtn = smallBtn(L.visionTest, async () => {
1415
1421
  testBtn.disabled = true;
@@ -1420,7 +1426,6 @@ export function openSettingsDialog() {
1420
1426
  testBtn.disabled = false;
1421
1427
  testBtn.textContent = L.visionTest;
1422
1428
  visionTestPre.textContent = res.output || res.error || '';
1423
- if (!res.ok) showToast(L.visionTestFailed + ': ' + errText(res), 'error');
1424
1429
  });
1425
1430
  paneVision.replaceChildren(
1426
1431
  h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.visionDesc),
@@ -1461,7 +1466,7 @@ export function openSettingsDialog() {
1461
1466
  return h('label', { class: 'flex shrink-0 items-center gap-1.5', title: L.visionCompressKBHint },
1462
1467
  h('span', { class: 'text-ui-xs font-medium text-foreground-subtle' }, L.visionCompressKB), n);
1463
1468
  })(),
1464
- numIn(visionCfg.skipAfterFailures, { min: 0, dflt: 4, title: L.visionSkipAfterFailuresHint, label: L.visionSkipAfterFailures, commit: (v) => { visionCfg.skipAfterFailures = v; } }),
1469
+ numIn(visionCfg.skipAfterFailures, { min: 0, dflt: 2, title: L.visionSkipAfterFailuresHint, label: L.visionSkipAfterFailures, commit: (v) => { visionCfg.skipAfterFailures = v; } }),
1465
1470
  numIn(visionCfg.skipMinutes, { min: 1, dflt: 30, title: L.visionSkipMinutesHint, label: L.visionSkipMinutes, commit: (v) => { visionCfg.skipMinutes = v; } }),
1466
1471
  ),
1467
1472
  ...cards,
@@ -1469,7 +1474,7 @@ export function openSettingsDialog() {
1469
1474
  h('div', { class: 'flex items-center gap-1.5' },
1470
1475
  smallBtn('+ ' + L.visionAddProxy, () => {
1471
1476
  // 名称留空时 helper 校验会拒绝保存;等用户输入名称后防抖保存
1472
- visionCfg.proxies.push({ name: '', baseUrl: 'https://open.bigmodel.cn/api/anthropic', model: 'glm-5.3-flash', apiKey: '', format: 'anthropic', prompt: '' });
1477
+ visionCfg.proxies.push({ name: '', baseUrl: 'https://open.bigmodel.cn/api/anthropic', model: 'glm-5.3-flash', apiKey: '', format: 'anthropic', prompt: VISION_DEFAULT_PROMPT });
1473
1478
  renderVision();
1474
1479
  }),
1475
1480
  smallBtn(L.visionResetChain, () => {
@@ -1479,7 +1484,9 @@ export function openSettingsDialog() {
1479
1484
  void persistVision();
1480
1485
  renderVision();
1481
1486
  })),
1482
- testBtn),
1487
+ h('div', { class: 'flex min-w-0 items-center justify-end gap-2' },
1488
+ h('span', { class: 'min-w-0 text-right text-ui-xs/relaxed text-foreground-subtle' }, L.testNoSessionHint),
1489
+ testBtn)),
1483
1490
  visionTestPre,
1484
1491
  );
1485
1492
  };
@@ -2174,11 +2181,9 @@ export function openSettingsDialog() {
2174
2181
  testBtn.textContent = L.titleGenTest;
2175
2182
  if (res && res.ok) {
2176
2183
  testState.textContent = L.titleGenTestOk + res.title;
2177
- showToast(res.title, 'success');
2178
2184
  } else {
2179
2185
  testState.textContent = (res && errText(res)) || L.titleGenTestFailed;
2180
2186
  testState.className = 'min-w-0 flex-1 truncate text-ui-xs/relaxed text-destructive';
2181
- showToast(L.titleGenTestFailed + ': ' + (res ? errText(res) : ''), 'error');
2182
2187
  }
2183
2188
  }, 'shrink-0');
2184
2189
  paneTitleGen.replaceChildren(
@@ -2206,9 +2211,13 @@ export function openSettingsDialog() {
2206
2211
  promptArea.focus();
2207
2212
  })),
2208
2213
  promptArea),
2209
- // 测试
2210
- h('div', { class: 'mt-2 flex items-center gap-2 rounded-xl border border-border p-2' },
2211
- sampleInput, testBtn),
2214
+ // 测试(两行:输入框整行;说明 + 按钮一行。结果只显示在本区下方,
2215
+ // 不弹窗;从没对话过时的引导见 testState 错误文案)
2216
+ h('div', { class: 'mt-2 rounded-xl border border-border p-2' },
2217
+ sampleInput,
2218
+ h('div', { class: 'mt-2 flex items-center justify-between gap-2' },
2219
+ h('span', { class: 'min-w-0 text-ui-xs/relaxed text-foreground-subtle' }, L.testNoSessionHint),
2220
+ testBtn)),
2212
2221
  testState,
2213
2222
  );
2214
2223
  };