zcode-pro 0.19.0 → 0.20.0

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.
@@ -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
- // 「 · 」前缀:与前面的版本行无缝拼成「插件 x · rtk y · 状态/路径」一行;
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
- const syncInputCls = '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';
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
- h('span', { class: 'w-20 shrink-0 text-ui-sm font-medium text-foreground' }, labelText), node);
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
- // 「测试」:用表单当前值(先测再存)验证 WebDAV:连接与凭据 → 上传目录
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
- syncFormRow(L.syncDir, syncDirInput),
1126
- syncFormRow(L.syncLogin, syncLoginInput),
1127
- syncFormRow(L.syncPassword, syncPasswordInput),
1128
- h('div', { class: 'flex items-start justify-between gap-2 px-2 py-1.5' },
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),
@@ -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),
@@ -1347,18 +1374,18 @@ export function openSettingsDialog() {
1347
1374
  ),
1348
1375
  h('div', { class: 'mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2' },
1349
1376
  // 供应商与模型走 provider-fields 共用逻辑:跟随会话时模型下拉为
1350
- // 全部供应商模型汇总,具体供应商为其模型列表;已删 id 标注「已删除」
1377
+ // 全部供应商模型汇总,具体供应商为其模型列表;已删 id 标注“已删除”
1351
1378
  field(L.visionUseProvider, comboField(p.useProvider || '',
1352
1379
  providerOptionsOf(titleGenProviders, L),
1353
1380
  (v, src) => {
1354
1381
  p.useProvider = v;
1355
1382
  saveVisionSoon();
1356
1383
  // 下拉切换后重渲染整个面板:模型下拉选项随新供应商变化,接口
1357
- // 地址/API Key 字段按「不跟随或供应商已删才显示」重新显隐
1384
+ // 地址/API Key 字段按“不跟随或供应商已删才显示”重新显隐
1358
1385
  if (src === 'select') renderVision();
1359
1386
  },
1360
1387
  { placeholder: L.titleGenProviderSession, staleLabel: L.titleGenProviderDeleted })),
1361
- // 跟随会话时模型下拉头部有「跟随当前会话当前模型」(值为空,
1388
+ // 跟随会话时模型下拉头部有“跟随当前会话当前模型”(值为空,
1362
1389
  // 插件 ≥1.6.1 在调用时解析为会话当前模型);具体供应商为其模型列表
1363
1390
  field(L.visionModel, comboField(p.model || '',
1364
1391
  modelOptionsOf(p.useProvider, titleGenProviders, {
@@ -2069,7 +2096,7 @@ export function openSettingsDialog() {
2069
2096
  h('span', { class: 'mb-1 block text-ui-xs font-medium text-foreground-subtle' }, labelText), node);
2070
2097
  // 供应商 / 模型 / 地址密钥共用 provider-fields 的规则(详见该模块说明)。
2071
2098
  // 换供应商后重渲染:模型下拉换成新供应商的模型、地址字段按模式显隐;
2072
- // 手动输入过的模型保留(不在新列表里时显示为「手动输入」项,是否改选
2099
+ // 手动输入过的模型保留(不在新列表里时显示为“手动输入”项,是否改选
2073
2100
  // 由用户决定)
2074
2101
  const showInlineConn = isManualConn(titleGenCfg.provider, titleGenProviders);
2075
2102
  const providerCombo = comboField(
@@ -2117,7 +2144,7 @@ export function openSettingsDialog() {
2117
2144
  field(L.titleGenBaseUrl, textIn(titleGenCfg.baseUrl, (v) => { titleGenCfg.baseUrl = v; saveTitleGenSoon(); }, { placeholder: 'https://…' })),
2118
2145
  field(L.titleGenApiKey, textIn(titleGenCfg.apiKey, (v) => { titleGenCfg.apiKey = v; saveTitleGenSoon(); }, { type: 'password' })),
2119
2146
  field(L.titleGenFormat, formatSel));
2120
- // 提示词:留空 = 内置默认;「查看默认提示词」把默认值载入编辑框供修改
2147
+ // 提示词:留空 = 内置默认;“查看默认提示词”把默认值载入编辑框供修改
2121
2148
  const promptArea = h('textarea', {
2122
2149
  class: 'zcodepro-textarea zcodepro-textarea-sm', rows: '8',
2123
2150
  placeholder: L.titleGenPromptPlaceholder,
@@ -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
- // 「思考行高」管“已工作/工作中 N 秒”过程视图的行距(读取/编辑/查阅/终端等
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
- const res = await rpc('/title-gen/run', { method: 'POST', body: { sessId, text: texts.join('\n') } }).catch(() => null);
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
- // 展开态判定等本批渲染全部落地后再做(aria-expanded 可能与行移除分两次提交)
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
  // 假装选中第一项而配置里其实没选