zcode-pro 0.19.0 → 0.20.1
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/bin/zcode-pro.mjs +32 -9
- package/dist/inject.js +229 -127
- package/package.json +1 -1
- package/src/host/helper.mjs +325 -35
- package/src/host/main.mjs +8 -4
- package/src/host/setup.mjs +45 -42
- package/src/host/taskIndex.mjs +186 -6
- package/src/inject/core.js +23 -15
- package/src/inject/features/settings-dialog.js +85 -49
- package/src/inject/features/styles.js +1 -1
- package/src/inject/features/task-order.js +1 -1
- package/src/inject/features/title-gen.js +10 -1
- package/src/inject/features/title-lock.js +59 -1
- package/src/inject/features/ws-running.js +1 -1
- package/src/inject/provider-fields.js +2 -2
|
@@ -25,7 +25,7 @@ function makeUpgradeControls({ endpoint, metaRefresh, idleText }) {
|
|
|
25
25
|
const hasIdle = () => typeof idleText === 'function';
|
|
26
26
|
const setState = (text, kind = '') => {
|
|
27
27
|
const shown = text || (hasIdle() ? (idleText() || '') : '');
|
|
28
|
-
//
|
|
28
|
+
// “ · ”前缀:与前面的版本行直接拼成“插件 x · rtk y · 状态/路径”一行;
|
|
29
29
|
// 错误信息不加前缀——它是独立提示,拼在版本行后会读成版本的一部分
|
|
30
30
|
state.textContent = shown ? (kind === 'error' ? shown : ` · ${shown}`) : '';
|
|
31
31
|
state.className = 'min-w-0 flex-1 truncate text-left text-ui-xs/relaxed '
|
|
@@ -618,7 +618,7 @@ export function openSettingsDialog() {
|
|
|
618
618
|
agentsSaveBtn.disabled = false;
|
|
619
619
|
}
|
|
620
620
|
};
|
|
621
|
-
//
|
|
621
|
+
// “行为”标签页:二级标签切换“全局提示词 / 任务名”。全局提示词编辑
|
|
622
622
|
// ~/.zcode/AGENTS.md;任务名(智能起名)面板复用 paneTitleGen(原本是
|
|
623
623
|
// 一级标签页,收进来后不再单独出现在顶部标签栏)
|
|
624
624
|
const agentsPromptPane = h('div', { class: 'mt-4' });
|
|
@@ -681,7 +681,7 @@ export function openSettingsDialog() {
|
|
|
681
681
|
const proxyTestBtn = btnSmall(L.proxyTest, () => { void runProxyTest(); }, 'shrink-0');
|
|
682
682
|
const runProxyTest = async () => {
|
|
683
683
|
if (proxyTesting) return;
|
|
684
|
-
//
|
|
684
|
+
// 空地址不发请求(等网络超时才报“检测失败”的体验太差),立即提示
|
|
685
685
|
if (!proxyInput.value.trim()) {
|
|
686
686
|
proxyTestState.className = 'mt-2 text-ui-xs/relaxed text-amber-500';
|
|
687
687
|
proxyTestState.textContent = L.proxyTestEmpty;
|
|
@@ -799,19 +799,37 @@ export function openSettingsDialog() {
|
|
|
799
799
|
// “同步”:把 ZCode Pro 设置与模型设置手动备份到 WebDAV / 从 WebDAV 恢复
|
|
800
800
|
// (交互参考 Tampermonkey:URL/登录/密码 + 手动触发,无自动同步)。仅 WebDAV
|
|
801
801
|
// 一种后端且本就只在点击时传输,不设“类型/禁用”下拉。表单显式保存(旁有
|
|
802
|
-
//
|
|
802
|
+
// “测试”用当前表单值验证连通);同步内容开关即时保存;下载先弹云端版本
|
|
803
803
|
// 列表(固定名 = 最新 + 带时间戳的历史),任选一个导入,导入会覆盖本机、
|
|
804
804
|
// 先弹二次确认
|
|
805
805
|
const savedSync = (config.sync && typeof config.sync === 'object') ? config.sync
|
|
806
806
|
: { url: '', dir: 'zcode-pro', login: '', password: '', include: { zcodepro: true, model: true }, lastPushAt: null, lastPullAt: null };
|
|
807
|
-
|
|
807
|
+
// 宽度分开拼:定宽控件(历史版本数字框)不能带 w-full——与 w-20 同用时
|
|
808
|
+
// w-full 在样式表里靠后、会胜出,把整行撑爆
|
|
809
|
+
const syncInputBase = 'h-8 rounded-lg border border-border bg-input px-2.5 text-ui-sm text-foreground outline-none transition-shadow placeholder:text-foreground-subtle focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40';
|
|
810
|
+
const syncInputCls = syncInputBase + ' w-full';
|
|
811
|
+
const syncLabel = (text) => h('span', { class: 'w-20 shrink-0 text-ui-sm font-medium text-foreground' }, text);
|
|
808
812
|
const syncFormRow = (labelText, node) => h('div', { class: 'flex items-center gap-3 px-2 py-1.5' },
|
|
809
|
-
|
|
813
|
+
syncLabel(labelText), node);
|
|
810
814
|
const syncUrlInput = h('input', { type: 'text', value: savedSync.url || '', placeholder: L.syncUrlPlaceholder, spellcheck: 'false', autocomplete: 'off', class: syncInputCls + ' min-w-0 flex-1' });
|
|
811
815
|
// 上传目录:拼在 URL 之后的文件夹(默认 zcode-pro,避免直接写进网盘根目录),可多级
|
|
812
816
|
const syncDirInput = h('input', { type: 'text', value: savedSync.dir || 'zcode-pro', placeholder: L.syncDirPlaceholder, spellcheck: 'false', autocomplete: 'off', class: syncInputCls + ' min-w-0 flex-1' });
|
|
813
817
|
const syncLoginInput = h('input', { type: 'text', value: savedSync.login || '', spellcheck: 'false', autocomplete: 'off', class: syncInputCls + ' min-w-0 flex-1' });
|
|
814
818
|
const syncPasswordInput = h('input', { type: 'password', value: savedSync.password || '', autocomplete: 'new-password', class: syncInputCls + ' min-w-0 flex-1' });
|
|
819
|
+
// 历史版本保留个数:上传成功后自动删除云端更早的历史版本(固定名最新版不受
|
|
820
|
+
// 影响);0 = 不留历史版本。非法输入按默认 10 提交(服务端还会再校验)
|
|
821
|
+
const syncHistoryInput = h('input', {
|
|
822
|
+
type: 'number', min: '0', max: '100', step: '1',
|
|
823
|
+
value: Number.isInteger(savedSync.historyLimit) ? savedSync.historyLimit : 10,
|
|
824
|
+
title: L.syncHistoryLimitHint, class: syncInputBase + ' w-20 shrink-0 text-center',
|
|
825
|
+
});
|
|
826
|
+
const syncHistoryValue = () => {
|
|
827
|
+
// 清空按默认 10 提交,不当成 0——0 会清掉云端全部历史版,误清空代价太大
|
|
828
|
+
const s = syncHistoryInput.value.trim();
|
|
829
|
+
if (!s) return 10;
|
|
830
|
+
const n = Number(s);
|
|
831
|
+
return Number.isFinite(n) ? Math.min(100, Math.max(0, Math.round(n))) : 10;
|
|
832
|
+
};
|
|
815
833
|
let syncFormOriginal = null;
|
|
816
834
|
const syncSaveBtn = btnSmall(L.syncSave, () => { void saveSyncForm(); }, 'shrink-0', 'primary');
|
|
817
835
|
const syncFormValues = () => ({
|
|
@@ -819,15 +837,17 @@ export function openSettingsDialog() {
|
|
|
819
837
|
dir: syncDirInput.value.trim(),
|
|
820
838
|
login: syncLoginInput.value.trim(),
|
|
821
839
|
password: syncPasswordInput.value,
|
|
840
|
+
historyLimit: syncHistoryValue(),
|
|
822
841
|
});
|
|
823
842
|
const syncCheckDirty = () => {
|
|
824
843
|
const c = syncFormValues();
|
|
825
844
|
syncSaveBtn.disabled = c.url === syncFormOriginal.url && c.dir === syncFormOriginal.dir
|
|
826
|
-
&& c.login === syncFormOriginal.login && c.password === syncFormOriginal.password
|
|
845
|
+
&& c.login === syncFormOriginal.login && c.password === syncFormOriginal.password
|
|
846
|
+
&& c.historyLimit === syncFormOriginal.historyLimit;
|
|
827
847
|
};
|
|
828
848
|
syncFormOriginal = syncFormValues();
|
|
829
849
|
syncSaveBtn.disabled = true;
|
|
830
|
-
for (const el of [syncUrlInput, syncDirInput, syncLoginInput, syncPasswordInput]) {
|
|
850
|
+
for (const el of [syncUrlInput, syncDirInput, syncLoginInput, syncPasswordInput, syncHistoryInput]) {
|
|
831
851
|
el.addEventListener('input', syncCheckDirty);
|
|
832
852
|
}
|
|
833
853
|
const saveSyncForm = async () => {
|
|
@@ -838,18 +858,20 @@ export function openSettingsDialog() {
|
|
|
838
858
|
url: res.sync.url,
|
|
839
859
|
dir: res.sync.dir,
|
|
840
860
|
login: res.sync.login, password: res.sync.password,
|
|
861
|
+
historyLimit: Number.isInteger(res.sync.historyLimit) ? res.sync.historyLimit : 10,
|
|
841
862
|
};
|
|
842
863
|
syncUrlInput.value = res.sync.url;
|
|
843
864
|
syncDirInput.value = res.sync.dir;
|
|
844
865
|
syncLoginInput.value = res.sync.login;
|
|
845
866
|
syncPasswordInput.value = res.sync.password;
|
|
867
|
+
syncHistoryInput.value = syncFormOriginal.historyLimit;
|
|
846
868
|
showToast(L.syncSaved);
|
|
847
869
|
} else {
|
|
848
870
|
showToast(L.failed + ': ' + errText(res), 'error');
|
|
849
871
|
syncCheckDirty();
|
|
850
872
|
}
|
|
851
873
|
};
|
|
852
|
-
//
|
|
874
|
+
// “测试”:用表单当前值(先测再存)验证 WebDAV:连接与凭据 → 上传目录
|
|
853
875
|
// (缺则自动建,等同上传时的行为)→ 文件列表;结果仿“代理”标签页的检测,
|
|
854
876
|
// 一行显示各步耗时与云端备份文件数
|
|
855
877
|
const syncTestState = h('p', { class: 'mt-2 text-ui-xs/relaxed text-foreground-subtle' });
|
|
@@ -933,7 +955,7 @@ export function openSettingsDialog() {
|
|
|
933
955
|
renderSyncStatus(savedSync);
|
|
934
956
|
const syncUploadBtn = btnSmall(L.syncUpload, () => { void runSyncPush(); }, 'shrink-0', 'primary');
|
|
935
957
|
const syncDownloadBtn = btnSmall(L.syncDownload, () => { openSyncVersionPicker(); }, 'shrink-0');
|
|
936
|
-
//
|
|
958
|
+
// “从云端下载”先弹版本列表(备份管理器的样子):列出云端目录里的备份文件
|
|
937
959
|
// (固定名 = 最新一版 + 带时间戳的历史),每行可导入到本机 / 从云端删除 /
|
|
938
960
|
// 另存到本机磁盘;导入按文件名前缀决定覆盖哪一项,不受“同步内容”勾选影响
|
|
939
961
|
const openSyncVersionPicker = () => {
|
|
@@ -1116,16 +1138,21 @@ export function openSettingsDialog() {
|
|
|
1116
1138
|
return;
|
|
1117
1139
|
}
|
|
1118
1140
|
renderSyncStatus(res.sync || {});
|
|
1119
|
-
showToast(L.syncUploaded, 'success');
|
|
1141
|
+
showToast(res.pruned ? L.syncUploaded + (isZhLocale() ? ';' : '; ') + L.syncPruned.replace('{n}', res.pruned) : L.syncUploaded, 'success');
|
|
1120
1142
|
};
|
|
1121
1143
|
paneSync.append(
|
|
1122
1144
|
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.syncDesc),
|
|
1123
1145
|
h('div', { class: 'mt-3 rounded-xl border border-border p-1' },
|
|
1124
1146
|
syncFormRow('URL', syncUrlInput),
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1147
|
+
// 登录与密码一行、上传目录与历史版本一行:短字段成对排,少占纵向空间
|
|
1148
|
+
h('div', { class: 'flex items-center gap-3 px-2 py-1.5' },
|
|
1149
|
+
syncLabel(L.syncLogin), syncLoginInput,
|
|
1150
|
+
syncLabel(L.syncPassword), syncPasswordInput),
|
|
1151
|
+
h('div', { class: 'flex items-center gap-3 px-2 py-1.5' },
|
|
1152
|
+
syncLabel(L.syncDir), syncDirInput,
|
|
1153
|
+
syncLabel(L.syncHistoryLimit), syncHistoryInput,
|
|
1154
|
+
h('span', { class: 'shrink-0 text-ui-xs text-foreground-subtle' }, L.syncHistoryLimitUnit)),
|
|
1155
|
+
h('div', { class: 'flex items-center justify-between gap-2 px-2 py-1.5' },
|
|
1129
1156
|
h('div', { class: 'min-w-0' },
|
|
1130
1157
|
h('p', { class: 'text-ui-xs/relaxed text-foreground-subtle' }, L.syncFormHint),
|
|
1131
1158
|
syncRecLine),
|
|
@@ -1183,21 +1210,21 @@ export function openSettingsDialog() {
|
|
|
1183
1210
|
// “视觉代理”:编辑 ~/.zcode/zcode-vision.json(zcode-vision 插件与 /vision-* 命令共用同一文件)。
|
|
1184
1211
|
// 代理列表顺序即执行链;结构性改动(开关/模式/排序/删增)即时保存,文本输入防抖保存。
|
|
1185
1212
|
// 注意:保存时链按全部代理重建——若用 /vision-chain 配过子集链,会被这里覆盖(两套入口语义如此,面板以列表为准)。
|
|
1186
|
-
//
|
|
1213
|
+
// 默认单级链与 zcode-vision 插件 DEFAULT_CONFIG 保持一致:session(跟随会话供应商、
|
|
1214
|
+
// 模型留空=会话当前模型)。需要兜底级可手动加 GLM 订阅直连。
|
|
1187
1215
|
// 赠送额度级 trust-build 暂不进默认:ZCode 网关不放行无人值守调用(ADR-0002)。
|
|
1188
1216
|
const VISION_DEFAULT_PROMPT = '请详细描述这张图片的全部内容。若是界面或图表截图,请先把所有错误、警告、异常状态逐字引用出来(含完整原文),再描述整体布局、文字与关键数据。';
|
|
1189
1217
|
const VISION_DEFAULT_CFG = {
|
|
1190
1218
|
enabled: true,
|
|
1191
1219
|
chainMode: 'fallback',
|
|
1192
|
-
chain: ['
|
|
1220
|
+
chain: ['session'],
|
|
1193
1221
|
proxies: [
|
|
1194
|
-
{ name: '
|
|
1195
|
-
{ 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 },
|
|
1196
1223
|
],
|
|
1197
1224
|
pollMs: 3000,
|
|
1198
1225
|
apiTimeoutMs: 120000, // 视觉上游冷启动可能近 2 分钟,与插件 DEFAULT_CONFIG 一致
|
|
1199
1226
|
compressThresholdKB: 1024,
|
|
1200
|
-
skipAfterFailures:
|
|
1227
|
+
skipAfterFailures: 2,
|
|
1201
1228
|
skipMinutes: 30,
|
|
1202
1229
|
forceIntercept: true,
|
|
1203
1230
|
};
|
|
@@ -1257,7 +1284,7 @@ export function openSettingsDialog() {
|
|
|
1257
1284
|
}
|
|
1258
1285
|
// 插件未装优先显示安装卡——判断只看钩子(visionRes.hook),与配置文件是否存在无关:
|
|
1259
1286
|
// 配置是钩子消费的,没钩子就没功能;老用户卸载插件后 ~/.zcode/zcode-vision.json 仍在,
|
|
1260
|
-
//
|
|
1287
|
+
// 若按配置存在与否分流会误入完整配置界面而毫无“未安装”提示
|
|
1261
1288
|
if (!visionRes.hook) {
|
|
1262
1289
|
paneVision.replaceChildren(
|
|
1263
1290
|
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.visionDesc),
|
|
@@ -1316,7 +1343,23 @@ export function openSettingsDialog() {
|
|
|
1316
1343
|
};
|
|
1317
1344
|
// 接口地址 / API Key 是否显示走 provider-fields 的共用规则
|
|
1318
1345
|
//(isManualConn:不跟随或供应商已删才显示,连接其余由跟随对象负责)
|
|
1319
|
-
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' }, [
|
|
1320
1363
|
h('div', { class: 'flex flex-wrap items-center gap-2' },
|
|
1321
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)),
|
|
1322
1365
|
// 名称输入框用 flex-1 占剩余宽度,与序号同行;textIn 的 w-full 会把序号挤成单独一行
|
|
@@ -1325,14 +1368,6 @@ export function openSettingsDialog() {
|
|
|
1325
1368
|
n.addEventListener('input', () => { p.name = n.value; saveVisionSoon(); });
|
|
1326
1369
|
return n;
|
|
1327
1370
|
})(),
|
|
1328
|
-
(() => {
|
|
1329
|
-
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' },
|
|
1330
|
-
h('option', { value: 'anthropic' }, 'anthropic'),
|
|
1331
|
-
h('option', { value: 'openai' }, 'openai'));
|
|
1332
|
-
sel.value = p.format === 'openai' ? 'openai' : 'anthropic';
|
|
1333
|
-
sel.addEventListener('change', () => { p.format = sel.value; void persistVision(); });
|
|
1334
|
-
return sel;
|
|
1335
|
-
})(),
|
|
1336
1371
|
smallBtn(L.visionMoveUp, () => {
|
|
1337
1372
|
if (i <= 0) return;
|
|
1338
1373
|
visionCfg.proxies.splice(i - 1, 0, visionCfg.proxies.splice(i, 1)[0]);
|
|
@@ -1347,18 +1382,18 @@ export function openSettingsDialog() {
|
|
|
1347
1382
|
),
|
|
1348
1383
|
h('div', { class: 'mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2' },
|
|
1349
1384
|
// 供应商与模型走 provider-fields 共用逻辑:跟随会话时模型下拉为
|
|
1350
|
-
// 全部供应商模型汇总,具体供应商为其模型列表;已删 id
|
|
1385
|
+
// 全部供应商模型汇总,具体供应商为其模型列表;已删 id 标注“已删除”
|
|
1351
1386
|
field(L.visionUseProvider, comboField(p.useProvider || '',
|
|
1352
1387
|
providerOptionsOf(titleGenProviders, L),
|
|
1353
1388
|
(v, src) => {
|
|
1354
1389
|
p.useProvider = v;
|
|
1355
1390
|
saveVisionSoon();
|
|
1356
1391
|
// 下拉切换后重渲染整个面板:模型下拉选项随新供应商变化,接口
|
|
1357
|
-
// 地址/API Key
|
|
1392
|
+
// 地址/API Key 字段按“不跟随或供应商已删才显示”重新显隐
|
|
1358
1393
|
if (src === 'select') renderVision();
|
|
1359
1394
|
},
|
|
1360
1395
|
{ placeholder: L.titleGenProviderSession, staleLabel: L.titleGenProviderDeleted })),
|
|
1361
|
-
//
|
|
1396
|
+
// 跟随会话时模型下拉头部有“跟随当前会话当前模型”(值为空,
|
|
1362
1397
|
// 插件 ≥1.6.1 在调用时解析为会话当前模型);具体供应商为其模型列表
|
|
1363
1398
|
field(L.visionModel, comboField(p.model || '',
|
|
1364
1399
|
modelOptionsOf(p.useProvider, titleGenProviders, {
|
|
@@ -1371,18 +1406,16 @@ export function openSettingsDialog() {
|
|
|
1371
1406
|
emptyLabel: (p.useProvider || '').trim() === 'session'
|
|
1372
1407
|
? L.titleGenModelFollowSession : L.titleGenModelEmpty,
|
|
1373
1408
|
})),
|
|
1374
|
-
...(isManualConn(p.useProvider, titleGenProviders) ? [
|
|
1375
|
-
field(L.visionBaseUrl, textIn(p.baseUrl, (v) => { p.baseUrl = v; saveVisionSoon(); })),
|
|
1376
|
-
field(L.visionApiKey + ' · ' + L.visionApiKeyHint, textIn(p.apiKey, (v) => { p.apiKey = v; saveVisionSoon(); }, { type: 'password' })),
|
|
1377
|
-
] : []),
|
|
1378
1409
|
),
|
|
1410
|
+
...manualConnCard,
|
|
1379
1411
|
(() => {
|
|
1380
1412
|
const n = h('textarea', { class: 'zcodepro-textarea zcodepro-textarea-sm', rows: '3', placeholder: L.visionPrompt });
|
|
1381
1413
|
n.value = p.prompt || '';
|
|
1382
1414
|
n.addEventListener('input', () => { p.prompt = n.value; saveVisionSoon(); });
|
|
1383
1415
|
return field(L.visionPrompt, n);
|
|
1384
1416
|
})(),
|
|
1385
|
-
|
|
1417
|
+
]);
|
|
1418
|
+
});
|
|
1386
1419
|
let testBtn;
|
|
1387
1420
|
testBtn = smallBtn(L.visionTest, async () => {
|
|
1388
1421
|
testBtn.disabled = true;
|
|
@@ -1393,7 +1426,6 @@ export function openSettingsDialog() {
|
|
|
1393
1426
|
testBtn.disabled = false;
|
|
1394
1427
|
testBtn.textContent = L.visionTest;
|
|
1395
1428
|
visionTestPre.textContent = res.output || res.error || '';
|
|
1396
|
-
if (!res.ok) showToast(L.visionTestFailed + ': ' + errText(res), 'error');
|
|
1397
1429
|
});
|
|
1398
1430
|
paneVision.replaceChildren(
|
|
1399
1431
|
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.visionDesc),
|
|
@@ -1434,7 +1466,7 @@ export function openSettingsDialog() {
|
|
|
1434
1466
|
return h('label', { class: 'flex shrink-0 items-center gap-1.5', title: L.visionCompressKBHint },
|
|
1435
1467
|
h('span', { class: 'text-ui-xs font-medium text-foreground-subtle' }, L.visionCompressKB), n);
|
|
1436
1468
|
})(),
|
|
1437
|
-
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; } }),
|
|
1438
1470
|
numIn(visionCfg.skipMinutes, { min: 1, dflt: 30, title: L.visionSkipMinutesHint, label: L.visionSkipMinutes, commit: (v) => { visionCfg.skipMinutes = v; } }),
|
|
1439
1471
|
),
|
|
1440
1472
|
...cards,
|
|
@@ -1442,7 +1474,7 @@ export function openSettingsDialog() {
|
|
|
1442
1474
|
h('div', { class: 'flex items-center gap-1.5' },
|
|
1443
1475
|
smallBtn('+ ' + L.visionAddProxy, () => {
|
|
1444
1476
|
// 名称留空时 helper 校验会拒绝保存;等用户输入名称后防抖保存
|
|
1445
|
-
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 });
|
|
1446
1478
|
renderVision();
|
|
1447
1479
|
}),
|
|
1448
1480
|
smallBtn(L.visionResetChain, () => {
|
|
@@ -1452,7 +1484,9 @@ export function openSettingsDialog() {
|
|
|
1452
1484
|
void persistVision();
|
|
1453
1485
|
renderVision();
|
|
1454
1486
|
})),
|
|
1455
|
-
|
|
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)),
|
|
1456
1490
|
visionTestPre,
|
|
1457
1491
|
);
|
|
1458
1492
|
};
|
|
@@ -2069,7 +2103,7 @@ export function openSettingsDialog() {
|
|
|
2069
2103
|
h('span', { class: 'mb-1 block text-ui-xs font-medium text-foreground-subtle' }, labelText), node);
|
|
2070
2104
|
// 供应商 / 模型 / 地址密钥共用 provider-fields 的规则(详见该模块说明)。
|
|
2071
2105
|
// 换供应商后重渲染:模型下拉换成新供应商的模型、地址字段按模式显隐;
|
|
2072
|
-
//
|
|
2106
|
+
// 手动输入过的模型保留(不在新列表里时显示为“手动输入”项,是否改选
|
|
2073
2107
|
// 由用户决定)
|
|
2074
2108
|
const showInlineConn = isManualConn(titleGenCfg.provider, titleGenProviders);
|
|
2075
2109
|
const providerCombo = comboField(
|
|
@@ -2117,7 +2151,7 @@ export function openSettingsDialog() {
|
|
|
2117
2151
|
field(L.titleGenBaseUrl, textIn(titleGenCfg.baseUrl, (v) => { titleGenCfg.baseUrl = v; saveTitleGenSoon(); }, { placeholder: 'https://…' })),
|
|
2118
2152
|
field(L.titleGenApiKey, textIn(titleGenCfg.apiKey, (v) => { titleGenCfg.apiKey = v; saveTitleGenSoon(); }, { type: 'password' })),
|
|
2119
2153
|
field(L.titleGenFormat, formatSel));
|
|
2120
|
-
// 提示词:留空 =
|
|
2154
|
+
// 提示词:留空 = 内置默认;“查看默认提示词”把默认值载入编辑框供修改
|
|
2121
2155
|
const promptArea = h('textarea', {
|
|
2122
2156
|
class: 'zcodepro-textarea zcodepro-textarea-sm', rows: '8',
|
|
2123
2157
|
placeholder: L.titleGenPromptPlaceholder,
|
|
@@ -2147,11 +2181,9 @@ export function openSettingsDialog() {
|
|
|
2147
2181
|
testBtn.textContent = L.titleGenTest;
|
|
2148
2182
|
if (res && res.ok) {
|
|
2149
2183
|
testState.textContent = L.titleGenTestOk + res.title;
|
|
2150
|
-
showToast(res.title, 'success');
|
|
2151
2184
|
} else {
|
|
2152
2185
|
testState.textContent = (res && errText(res)) || L.titleGenTestFailed;
|
|
2153
2186
|
testState.className = 'min-w-0 flex-1 truncate text-ui-xs/relaxed text-destructive';
|
|
2154
|
-
showToast(L.titleGenTestFailed + ': ' + (res ? errText(res) : ''), 'error');
|
|
2155
2187
|
}
|
|
2156
2188
|
}, 'shrink-0');
|
|
2157
2189
|
paneTitleGen.replaceChildren(
|
|
@@ -2179,9 +2211,13 @@ export function openSettingsDialog() {
|
|
|
2179
2211
|
promptArea.focus();
|
|
2180
2212
|
})),
|
|
2181
2213
|
promptArea),
|
|
2182
|
-
//
|
|
2183
|
-
|
|
2184
|
-
|
|
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)),
|
|
2185
2221
|
testState,
|
|
2186
2222
|
);
|
|
2187
2223
|
};
|
|
@@ -140,7 +140,7 @@ function buildCss(styles) {
|
|
|
140
140
|
}
|
|
141
141
|
const tlh = styles.thinkingLineHeight;
|
|
142
142
|
if (typeof tlh === 'number' && Number.isFinite(tlh) && tlh >= 0.8) {
|
|
143
|
-
//
|
|
143
|
+
// “思考行高”管“已工作/工作中 N 秒”过程视图的行距(读取/编辑/查阅/终端等
|
|
144
144
|
// 条目行、流式思考预览,默认 1.5):行高挂在历史内容容器上由各行继承;
|
|
145
145
|
// 过程里的叙述正文走 .space-y-4(回答行高,选择器更具体压过这里的继承)。
|
|
146
146
|
// 单独展开的“思考·持续了 N 秒”块内内容不在此设置范围内
|
|
@@ -29,7 +29,7 @@ export function startTaskOrderWatcher() {
|
|
|
29
29
|
if (!row || row.getAttribute('data-task-item-key') === dragKey) return;
|
|
30
30
|
e.preventDefault();
|
|
31
31
|
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
|
32
|
-
// 实时预览:把拖动行移动到目标行前/后(拖动期间 React
|
|
32
|
+
// 实时预览:把拖动行移动到目标行前/后(拖动期间 React 不重渲染;写入数据库后刷新重建)
|
|
33
33
|
const rect = row.getBoundingClientRect();
|
|
34
34
|
const before = e.clientY < rect.top + rect.height / 2;
|
|
35
35
|
const src = [...row.parentElement.querySelectorAll('[data-task-item-key]')]
|
|
@@ -111,7 +111,16 @@ async function onNewTaskRow(key) {
|
|
|
111
111
|
} finally { convObserver.disconnect(); }
|
|
112
112
|
if (!texts.length) return;
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
// 失败静默但重试(模型调用偶发失败不应让本次会话永远失去标题):5 秒一次
|
|
115
|
+
// 错开瞬时抖动;20 秒一次兜住“会话刚建的几秒内索引模型信息未写定”
|
|
116
|
+
//(实测新会话 8 秒时解析到的模型不存在,稍后即正常)
|
|
117
|
+
const callTitleGen = () => rpc('/title-gen/run', { method: 'POST', body: { sessId, text: texts.join('\n') } }).catch(() => null);
|
|
118
|
+
let res = await callTitleGen();
|
|
119
|
+
for (const delay of [5000, 20000]) {
|
|
120
|
+
if (res && res.ok) break;
|
|
121
|
+
await sleep(delay);
|
|
122
|
+
res = await callTitleGen();
|
|
123
|
+
}
|
|
115
124
|
// written=false 常见于用户已抢先手动重命名——尊重手动命名,不提示
|
|
116
125
|
if (res && res.ok && res.written && res.title) {
|
|
117
126
|
setRowTitle(key, res.title);
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
// 用户刚完成的重命名(新名先上屏、写库在毫秒级)此时表已更新成新名,
|
|
12
12
|
// 不会把新名改回旧名;拉表失败则本轮不干预(缓存可能过时);
|
|
13
13
|
// - 行内有输入框(正在重命名)不干预;回写后再复核一次,纠正数据库写入
|
|
14
|
-
//
|
|
14
|
+
// 导致的误回写;
|
|
15
|
+
// - 右键“重命名任务”的弹窗预填应用内部状态里的标题(被自动标题覆盖过的
|
|
16
|
+
// 会话即旧名),在弹窗里直接点确认会把旧名写回成手动重命名、撤销锁定——
|
|
17
|
+
// 点中重命名菜单项后把预填值换成锁定名兜住这个口子(见 prefillRenameDialog)。
|
|
15
18
|
// 扫描事件驱动(任务行相关变动去抖 150ms;characterData 必须订阅——React
|
|
16
19
|
// 改标题文本走 nodeValue,只产生 characterData 变动)+ 低频兜底轮询。
|
|
17
20
|
import { rpc } from '../core.js';
|
|
@@ -216,18 +219,73 @@ export function refreshPinnedTitles() {
|
|
|
216
219
|
return fetchPinned().then((m) => { if (m) scan(); });
|
|
217
220
|
}
|
|
218
221
|
|
|
222
|
+
// —— 重命名弹窗预填锁定名 ——
|
|
223
|
+
// 官方“重命名任务”弹窗([role=dialog] 内的输入框)预填应用内部状态里的标题,
|
|
224
|
+
// 被自动标题覆盖过的会话即旧名;用户在弹窗里不做修改直接确认,会把旧名当成
|
|
225
|
+
// 手动重命名写库、悄悄撤销锁定。右键任务行记下会话、点到“重命名”菜单项后才
|
|
226
|
+
// 武装预填(右键菜单里“分组”“反馈问题”等也会弹输入框,只按右键窗口判定会
|
|
227
|
+
// 误写它们);且只改已有预填值的输入框——官方重命名弹窗必带当前标题,反馈、
|
|
228
|
+
// 新建分组这类空白输入框不碰
|
|
229
|
+
const RENAME_CTX_VALID_MS = 15000; // 点中重命名菜单项到弹窗出现的有效窗口
|
|
230
|
+
let renameCtx = null; // { key, at, armed } 最近右键的任务行
|
|
231
|
+
const renamePrefilled = new WeakSet(); // 已预填过的输入框,不重复处理、不与输入争抢
|
|
232
|
+
|
|
233
|
+
// 原位写值并让 React 受控输入接住:直接 el.value = x 不派发事件,框架状态
|
|
234
|
+
// 会把值改回原样;用原型 setter 写入再派发 input 事件(实测有效)
|
|
235
|
+
function setNativeValue(el, text) {
|
|
236
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
237
|
+
Object.getOwnPropertyDescriptor(el.constructor.prototype, 'value').set.call(el, text);
|
|
238
|
+
} else {
|
|
239
|
+
el.textContent = text;
|
|
240
|
+
}
|
|
241
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function prefillRenameDialog() {
|
|
245
|
+
if (!renameCtx || !renameCtx.armed || Date.now() - renameCtx.at > RENAME_CTX_VALID_MS) { renameCtx = null; return; }
|
|
246
|
+
if (!enabled || !pinned) return;
|
|
247
|
+
const want = pinned.get(sessIdOf(renameCtx.key));
|
|
248
|
+
if (want === undefined) return; // 该会话没重命名过:弹窗显示的就是官方值,不动
|
|
249
|
+
for (const el of document.querySelectorAll('[role="dialog"] input, [role="dialog"] textarea, [role="dialog"] [contenteditable="true"]')) {
|
|
250
|
+
if (renamePrefilled.has(el) || !el.getClientRects().length) continue;
|
|
251
|
+
if (el.closest(TASK_SEL) || el.closest('.zcodepro-overlay')) continue;
|
|
252
|
+
const cur = String((el.value ?? el.textContent) ?? '').trim();
|
|
253
|
+
if (!cur) continue; // 空白输入框不是重命名弹窗(反馈/新建分组等),不碰
|
|
254
|
+
renamePrefilled.add(el);
|
|
255
|
+
if (cur !== String(want).trim()) setNativeValue(el, String(want));
|
|
256
|
+
renameCtx = null; // 一次重命名只预填最先出现的非空输入框
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
219
261
|
export function startTitleLock() {
|
|
220
262
|
if (installed || typeof document === 'undefined') return;
|
|
221
263
|
installed = true;
|
|
222
264
|
void bootstrap();
|
|
223
265
|
|
|
224
266
|
new MutationObserver((muts) => {
|
|
267
|
+
if (renameCtx) prefillRenameDialog();
|
|
225
268
|
if (!taskRowMutations(muts)) return;
|
|
226
269
|
refreshMapSoon();
|
|
227
270
|
scheduleScan();
|
|
228
271
|
})
|
|
229
272
|
.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'], characterData: true });
|
|
230
273
|
|
|
274
|
+
// 右键记下目标会话(捕获阶段,菜单未弹出前);点中“重命名”菜单项才武装预填
|
|
275
|
+
//(从点菜单项起重新计有效窗口)。右键空白处则清掉,避免误改无关弹窗
|
|
276
|
+
document.addEventListener('contextmenu', (e) => {
|
|
277
|
+
const row = e.target instanceof Element && e.target.closest(TASK_SEL);
|
|
278
|
+
renameCtx = row ? { key: row.getAttribute('data-task-item-key') || '', at: 0, armed: false } : null;
|
|
279
|
+
}, true);
|
|
280
|
+
document.addEventListener('click', (e) => {
|
|
281
|
+
if (!renameCtx || renameCtx.armed) return;
|
|
282
|
+
const item = e.target instanceof Element && e.target.closest('[role="menuitem"], [data-radix-collection-item]');
|
|
283
|
+
if (item && /重命名|rename/i.test(item.textContent || '')) {
|
|
284
|
+
renameCtx.armed = true;
|
|
285
|
+
renameCtx.at = Date.now();
|
|
286
|
+
}
|
|
287
|
+
}, true);
|
|
288
|
+
|
|
231
289
|
// 启动期取表失败(helper 未就绪/索引暂不可读)时退避重试,成功即停
|
|
232
290
|
scheduleBootRetry();
|
|
233
291
|
|
|
@@ -145,7 +145,7 @@ function handleRemovals(muts) {
|
|
|
145
145
|
})
|
|
146
146
|
.filter(Boolean);
|
|
147
147
|
if (!infos.length) return;
|
|
148
|
-
//
|
|
148
|
+
// 展开态判定等本批渲染全部完成后再做(aria-expanded 可能与行移除分两次提交)
|
|
149
149
|
setTimeout(() => {
|
|
150
150
|
for (const info of infos) {
|
|
151
151
|
const row = findWsRow(info.ws);
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
// 设置面板共用,后续其他面板直接复用这里的组件与规则。供应商取值约定:
|
|
3
3
|
// - '':不跟随(自填接口地址 + API Key + 协议直连);
|
|
4
4
|
// - 'session':跟随当前会话(连接由会话侧解析,不存地址密钥);
|
|
5
|
-
// - 其余:供应商 id;已删的 id
|
|
5
|
+
// - 其余:供应商 id;已删的 id 保留为“已删除”选项,调用方靠保存的地址/
|
|
6
6
|
// 密钥副本继续直连。
|
|
7
7
|
import { h } from './core.js';
|
|
8
8
|
|
|
9
9
|
export const PROVIDER_FIELD_CLS = 'h-8 w-full rounded-lg border border-border bg-input px-2.5 text-ui-sm text-foreground outline-none transition-shadow placeholder:text-foreground-subtle focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40';
|
|
10
10
|
|
|
11
|
-
//
|
|
11
|
+
// “下拉 + 手输”组合(供应商与模型选择共用):下拉选择列表项,也可以直接
|
|
12
12
|
// 输入;当前值不在列表里(条目被删或手输 id)时保留为带 staleLabel 标注的
|
|
13
13
|
// 选项,不清空、不跳值;当前值为空时插入一个 emptyLabel 空选项,避免下拉框
|
|
14
14
|
// 假装选中第一项而配置里其实没选
|