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 +117 -107
- package/package.json +1 -1
- package/src/host/helper.mjs +175 -10
- package/src/host/taskIndex.mjs +9 -4
- package/src/inject/core.js +6 -6
- package/src/inject/features/settings-dialog.js +37 -28
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\
|
|
258
|
-
visionBaseUrl: "\u63A5\u53E3\u5730\u5740
|
|
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
|
|
623
|
-
visionBaseUrl: "
|
|
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: ["
|
|
4129
|
+
chain: ["session"],
|
|
4130
4130
|
proxies: [
|
|
4131
|
-
{ name: "
|
|
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:
|
|
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) =>
|
|
4283
|
-
|
|
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: "
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
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-
|
|
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
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
)
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
placeholder:
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
p.
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
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
|
-
}
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
5286
|
+
{ class: "mt-2 rounded-xl border border-border p-2" },
|
|
5282
5287
|
sampleInput,
|
|
5283
|
-
|
|
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
package/src/host/helper.mjs
CHANGED
|
@@ -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.
|
|
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 (!
|
|
2160
|
-
|
|
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,
|
|
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,
|
package/src/host/taskIndex.mjs
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
}
|
package/src/inject/core.js
CHANGED
|
@@ -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: '
|
|
266
|
-
visionBaseUrl: '接口地址
|
|
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
|
|
632
|
-
visionBaseUrl: '
|
|
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
|
-
//
|
|
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: ['
|
|
1220
|
+
chain: ['session'],
|
|
1220
1221
|
proxies: [
|
|
1221
|
-
{ name: '
|
|
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:
|
|
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) =>
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
2211
|
-
|
|
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
|
};
|