zcode-pro 0.17.2 → 0.19.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.
- package/README.en.md +1 -0
- package/README.md +1 -0
- package/dist/inject.js +1368 -216
- package/package.json +1 -1
- package/src/host/helper.mjs +1248 -156
- package/src/host/main.mjs +11 -0
- package/src/host/pickFolder.mjs +48 -3
- package/src/host/setup.mjs +34 -9
- package/src/host/taskIndex.mjs +83 -0
- package/src/host/webdav.mjs +128 -12
- package/src/inject/core.js +190 -42
- package/src/inject/features/file-menu.js +9 -2
- package/src/inject/features/plugin-updates.js +1 -1
- package/src/inject/features/settings-dialog.js +821 -118
- package/src/inject/features/styles.js +15 -1
- package/src/inject/features/title-gen.js +169 -0
- package/src/inject/features/title-lock.js +14 -0
- package/src/inject/features/toolbar-icons.js +38 -4
- package/src/inject/index.js +14 -2
- package/src/inject/provider-fields.js +71 -0
- package/src/inject/ui.js +23 -7
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// 顶部标签页切换(视觉参考侧栏“分组/项目”切换);配置写入 helper(~/.zcode/zcodepro.json)。
|
|
3
3
|
import { h, t, rpc, getConfig, clearConfigCache, errText, HELPER_URL, isZhLocale } from '../core.js';
|
|
4
4
|
import { openDialog, dialogFooter, btnPrimary, btnSecondary, btnSmall, settingRow, ensureStyle, showToast, numberField, unitField } from '../ui.js';
|
|
5
|
+
import { comboField, providerOptionsOf, modelOptionsOf, modelPlaceholderOf, isManualConn } from '../provider-fields.js';
|
|
5
6
|
import { refreshAliases } from './alias.js';
|
|
6
7
|
import { applyStyles, STYLE_DEFAULTS, showStyleHighlight, hideStyleHighlight } from './styles.js';
|
|
7
8
|
import { copyToClipboard, hCopyIcon } from './copy-path.js';
|
|
@@ -15,12 +16,18 @@ function openExternal(url) {
|
|
|
15
16
|
// 检查更新 → 升级(helper 后台任务,轮询进度,输出取最后一行、超长省略中间)→
|
|
16
17
|
// 进行中可停止;关弹窗不中断升级,重开恢复显示。endpoint 为 /headroom 或 /rtk,
|
|
17
18
|
// metaRefresh 在升级完成后刷新各面板自己的版本行与配置。
|
|
18
|
-
function makeUpgradeControls({ endpoint, metaRefresh }) {
|
|
19
|
+
function makeUpgradeControls({ endpoint, metaRefresh, idleText }) {
|
|
19
20
|
const L = t();
|
|
20
21
|
const state = h('span', { class: 'min-w-0 flex-1 truncate text-left text-ui-xs/relaxed text-foreground-subtle' });
|
|
21
|
-
// 失败原因在状态行直显红字(toast 只有 5
|
|
22
|
+
// 失败原因在状态行直显红字(toast 只有 5 秒且易被弹窗遮挡)。
|
|
23
|
+
// idleText:空闲(无检查/升级活动)时该槽位显示调用方给的文本——rtk/headroom
|
|
24
|
+
// 面板用它显示本体位置;一旦有活动(检查中/下载进度/结果),活动文本顶替位置
|
|
25
|
+
const hasIdle = () => typeof idleText === 'function';
|
|
22
26
|
const setState = (text, kind = '') => {
|
|
23
|
-
|
|
27
|
+
const shown = text || (hasIdle() ? (idleText() || '') : '');
|
|
28
|
+
// 「 · 」前缀:与前面的版本行无缝拼成「插件 x · rtk y · 状态/路径」一行;
|
|
29
|
+
// 错误信息不加前缀——它是独立提示,拼在版本行后会读成版本的一部分
|
|
30
|
+
state.textContent = shown ? (kind === 'error' ? shown : ` · ${shown}`) : '';
|
|
24
31
|
state.className = 'min-w-0 flex-1 truncate text-left text-ui-xs/relaxed '
|
|
25
32
|
+ (kind === 'error' ? 'text-destructive' : 'text-foreground-subtle');
|
|
26
33
|
};
|
|
@@ -129,6 +136,8 @@ function makeUpgradeControls({ endpoint, metaRefresh }) {
|
|
|
129
136
|
const res = await rpc(endpoint + '/upgrade');
|
|
130
137
|
if (res.ok && res.upgrade && res.upgrade.running) pollStart();
|
|
131
138
|
})();
|
|
139
|
+
// 初始化为空闲态(有 idleText 时即显示位置等空闲文本)
|
|
140
|
+
setState('');
|
|
132
141
|
return { btn, state, stop: stopPoll };
|
|
133
142
|
}
|
|
134
143
|
|
|
@@ -141,8 +150,10 @@ export function openSettingsDialog() {
|
|
|
141
150
|
openDialog({
|
|
142
151
|
title: L.settingsTitle,
|
|
143
152
|
// 宽度类必须用宿主样式表已有的工具类(注入的类名不会生成 CSS):
|
|
144
|
-
// sm:max-w-3xl 不存在会失去约束拉满全屏;max-w-
|
|
145
|
-
|
|
153
|
+
// sm:max-w-3xl 不存在会失去约束拉满全屏;max-w-3xl=48rem 存在——英文界面
|
|
154
|
+
// 标签栏较长,42rem 会挤出一块空白并触发横向滚动
|
|
155
|
+
width: 'max-w-3xl',
|
|
156
|
+
bodyClass: 'mt-1',
|
|
146
157
|
overlay: 'none',
|
|
147
158
|
draggable: true,
|
|
148
159
|
posKey: 'settings',
|
|
@@ -194,7 +205,11 @@ export function openSettingsDialog() {
|
|
|
194
205
|
const config = await getConfig(true);
|
|
195
206
|
const agentsRes = await rpc('/agents');
|
|
196
207
|
const visionRes = await rpc('/vision');
|
|
197
|
-
const
|
|
208
|
+
const titleGenRes = await rpc('/title-gen');
|
|
209
|
+
const titleGenProvidersRes = await rpc('/title-gen/providers');
|
|
210
|
+
// 供应商表(含各供应商可选模型):视觉代理的模型下拉也用这份
|
|
211
|
+
const titleGenProviders = titleGenProvidersRes.ok && Array.isArray(titleGenProvidersRes.providers)
|
|
212
|
+
? titleGenProvidersRes.providers : [];
|
|
198
213
|
const rtkRes = await rpc('/rtk');
|
|
199
214
|
const headroomRes = await rpc('/headroom');
|
|
200
215
|
|
|
@@ -217,7 +232,7 @@ export function openSettingsDialog() {
|
|
|
217
232
|
'data-zcodepro-status': '1',
|
|
218
233
|
},
|
|
219
234
|
h('span', { class: 'inline-block size-2 rounded-full bg-emerald-500' }),
|
|
220
|
-
h('span', { class: 'text-foreground-subtle/70' }, `${
|
|
235
|
+
h('span', { class: 'text-foreground-subtle/70' }, `${health.version} · ${HELPER_URL}`)
|
|
221
236
|
);
|
|
222
237
|
|
|
223
238
|
const rows = h('div', { class: 'divide-y divide-border rounded-xl border border-border' });
|
|
@@ -323,7 +338,7 @@ export function openSettingsDialog() {
|
|
|
323
338
|
const runPluginsUpdate = async () => {
|
|
324
339
|
pluginsBtn.disabled = true;
|
|
325
340
|
pluginsStatusLine.textContent = L.pluginsUpdating;
|
|
326
|
-
const res = await rpc('/plugins/update', { method: 'POST', body: {
|
|
341
|
+
const res = await rpc('/plugins/update', { method: 'POST', body: {} });
|
|
327
342
|
pluginsBtn.disabled = false;
|
|
328
343
|
if (!res.ok) {
|
|
329
344
|
pluginsStatusLine.textContent = '';
|
|
@@ -370,25 +385,30 @@ export function openSettingsDialog() {
|
|
|
370
385
|
);
|
|
371
386
|
const paneStyles = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
372
387
|
const panePerf = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
373
|
-
const paneAgents = h('div', { role: 'tabpanel', class: 'mt-
|
|
388
|
+
const paneAgents = h('div', { role: 'tabpanel', class: 'mt-2', style: 'display:none' });
|
|
374
389
|
const paneProxy = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
375
390
|
const paneSync = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
376
391
|
const paneVision = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
377
392
|
const paneRtk = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
378
393
|
const paneHeadroom = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
379
|
-
const
|
|
394
|
+
const paneTitleGen = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
395
|
+
const paneDebug = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
396
|
+
const panes = { features: paneFeatures, styles: paneStyles, perf: panePerf, agents: paneAgents, proxy: paneProxy, sync: paneSync, vision: paneVision, headroom: paneHeadroom, rtk: paneRtk, debug: paneDebug };
|
|
380
397
|
const tabDefs = [
|
|
381
398
|
['features', L.tabFeatures],
|
|
399
|
+
['agents', L.tabBehavior],
|
|
382
400
|
['styles', L.tabStyles],
|
|
383
401
|
['perf', L.tabPerformance],
|
|
384
|
-
['agents', L.tabAgents],
|
|
385
402
|
['proxy', L.tabProxy],
|
|
386
403
|
['sync', L.tabSync],
|
|
387
404
|
['vision', L.tabVision],
|
|
388
405
|
['headroom', L.tabHeadroom],
|
|
389
406
|
['rtk', L.tabRtk],
|
|
407
|
+
// “调试”标签页只对开发模式生效的用户显示(配置开关或 ZCODEPRO_DEV=1),
|
|
408
|
+
// 普通用户不可见
|
|
409
|
+
...(config.devMode === true || config.devActive === true ? [['debug', L.tabDebug]] : []),
|
|
390
410
|
];
|
|
391
|
-
const tablist = h('div', { role: 'tablist', 'aria-orientation': 'horizontal', class: 'zcodepro-tablist mt-
|
|
411
|
+
const tablist = h('div', { role: 'tablist', 'aria-orientation': 'horizontal', class: 'zcodepro-tablist mt-1' });
|
|
392
412
|
const renderTabs = () => tablist.replaceChildren(...tabDefs.map(([id, label]) =>
|
|
393
413
|
h('button', {
|
|
394
414
|
type: 'button', role: 'tab', class: 'zcodepro-tab',
|
|
@@ -483,6 +503,7 @@ export function openSettingsDialog() {
|
|
|
483
503
|
styleCell(L.rowGapName, L.rowGapDesc, 'rowGap'),
|
|
484
504
|
styleCell(L.userLineHeightName, L.userLineHeightDesc, 'userLineHeight', { min: 1, max: 3, step: 0.05, unit: 'x' }),
|
|
485
505
|
styleCell(L.lineHeightName, L.lineHeightDesc, 'lineHeight', { min: 1, max: 3, step: 0.05, unit: 'x' }),
|
|
506
|
+
styleCell(L.thinkingLineHeightName, L.thinkingLineHeightDesc, 'thinkingLineHeight', { min: 1, max: 3, step: 0.05, unit: 'x' }),
|
|
486
507
|
styleCell(L.codeLineHeightName, L.codeLineHeightDesc, 'codeLineHeight', { min: 1, max: 3, step: 0.05, unit: 'x' }),
|
|
487
508
|
styleCell(L.listSpacingName, L.listSpacingDesc, 'listSpacing'),
|
|
488
509
|
styleCell(L.listItemSpacingName, L.listItemSpacingDesc, 'listItemSpacing'),
|
|
@@ -568,7 +589,7 @@ export function openSettingsDialog() {
|
|
|
568
589
|
// “全局提示词”:编辑 ~/.zcode/AGENTS.md(helper /agents 端点读写)。
|
|
569
590
|
// 大段文本不做即存即生效——显式保存;未修改时保存按钮禁用
|
|
570
591
|
const agentsArea = h('textarea', {
|
|
571
|
-
class: 'zcodepro-textarea',
|
|
592
|
+
class: 'zcodepro-textarea zcodepro-textarea-lg',
|
|
572
593
|
placeholder: L.agentsPlaceholder,
|
|
573
594
|
spellcheck: 'false',
|
|
574
595
|
});
|
|
@@ -597,7 +618,11 @@ export function openSettingsDialog() {
|
|
|
597
618
|
agentsSaveBtn.disabled = false;
|
|
598
619
|
}
|
|
599
620
|
};
|
|
600
|
-
|
|
621
|
+
// “行为”标签页:二级标签切换「全局提示词 / 任务名」。全局提示词编辑
|
|
622
|
+
// ~/.zcode/AGENTS.md;任务名(智能起名)面板复用 paneTitleGen(原本是
|
|
623
|
+
// 一级标签页,收进来后不再单独出现在顶部标签栏)
|
|
624
|
+
const agentsPromptPane = h('div', { class: 'mt-4' });
|
|
625
|
+
agentsPromptPane.append(
|
|
601
626
|
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.agentsDesc),
|
|
602
627
|
...(agentsFailed
|
|
603
628
|
? [h('p', { class: 'mt-2 text-ui-sm text-destructive' }, L.agentsLoadFailed + ': ' + errText(agentsRes))]
|
|
@@ -605,6 +630,27 @@ export function openSettingsDialog() {
|
|
|
605
630
|
h('div', { class: 'mt-3' }, agentsArea),
|
|
606
631
|
h('div', { class: 'mt-3 flex justify-end' }, agentsSaveBtn),
|
|
607
632
|
);
|
|
633
|
+
let agentsSubTab = 'prompt';
|
|
634
|
+
const agentsSubPanes = { prompt: agentsPromptPane, titleGen: paneTitleGen };
|
|
635
|
+
const agentsSubList = h('div', { role: 'tablist', 'aria-orientation': 'horizontal', class: 'zcodepro-tablist' });
|
|
636
|
+
const renderAgentsSubTabs = () => {
|
|
637
|
+
agentsSubList.replaceChildren(...[
|
|
638
|
+
['prompt', L.tabAgents],
|
|
639
|
+
['titleGen', L.tabTitleGen],
|
|
640
|
+
].map(([id, label]) => h('button', {
|
|
641
|
+
type: 'button', role: 'tab', class: 'zcodepro-tab',
|
|
642
|
+
'aria-selected': String(agentsSubTab === id),
|
|
643
|
+
'data-state': agentsSubTab === id ? 'active' : 'inactive',
|
|
644
|
+
onClick: () => {
|
|
645
|
+
agentsSubTab = id;
|
|
646
|
+
for (const [pid, pane] of Object.entries(agentsSubPanes)) pane.style.display = pid === id ? '' : 'none';
|
|
647
|
+
renderAgentsSubTabs();
|
|
648
|
+
},
|
|
649
|
+
}, label)));
|
|
650
|
+
};
|
|
651
|
+
for (const [pid, pane] of Object.entries(agentsSubPanes)) pane.style.display = pid === agentsSubTab ? '' : 'none';
|
|
652
|
+
renderAgentsSubTabs();
|
|
653
|
+
paneAgents.append(agentsSubList, agentsPromptPane, paneTitleGen);
|
|
608
654
|
|
|
609
655
|
// “代理”:为 helper 发起的网络访问(插件市场更新/安装、headroom 本体升级)设
|
|
610
656
|
// HTTP 代理;保存即生效(helper 子进程环境变量注入),不影响 ZCode 应用与模型请求。
|
|
@@ -635,6 +681,12 @@ export function openSettingsDialog() {
|
|
|
635
681
|
const proxyTestBtn = btnSmall(L.proxyTest, () => { void runProxyTest(); }, 'shrink-0');
|
|
636
682
|
const runProxyTest = async () => {
|
|
637
683
|
if (proxyTesting) return;
|
|
684
|
+
// 空地址不发请求(等网络超时才报「检测失败」的体验太差),立即提示
|
|
685
|
+
if (!proxyInput.value.trim()) {
|
|
686
|
+
proxyTestState.className = 'mt-2 text-ui-xs/relaxed text-amber-500';
|
|
687
|
+
proxyTestState.textContent = L.proxyTestEmpty;
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
638
690
|
proxyTesting = true;
|
|
639
691
|
proxyTestBtn.disabled = true;
|
|
640
692
|
proxyTestBtn.textContent = L.proxyTesting;
|
|
@@ -655,6 +707,39 @@ export function openSettingsDialog() {
|
|
|
655
707
|
proxyTestState.className = 'mt-2 text-ui-xs/relaxed ' + (allOk ? 'text-foreground-subtle' : 'text-amber-500');
|
|
656
708
|
proxyTestState.textContent = `${res.proxy}:${parts.join(' · ')}`;
|
|
657
709
|
};
|
|
710
|
+
// 代理生效范围(即时保存,每个开关只改自己那项):插件市场更新与插件依赖
|
|
711
|
+
// 软件默认开,同步默认关——国内网盘直连即可,代理停了同步会跟着失败
|
|
712
|
+
const persistProxyScope = async (partial) => {
|
|
713
|
+
const res = await rpc('/config', { method: 'POST', body: { proxyScopes: partial } });
|
|
714
|
+
if (!res.ok) {
|
|
715
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
if (res.config && res.config.proxyScopes) config.proxyScopes = res.config.proxyScopes;
|
|
719
|
+
return true;
|
|
720
|
+
};
|
|
721
|
+
const proxyScopeWrap = h('div');
|
|
722
|
+
const renderProxyScopes = () => {
|
|
723
|
+
const sc = config.proxyScopes || {};
|
|
724
|
+
proxyScopeWrap.replaceChildren(
|
|
725
|
+
settingRow(L.proxyScopePlugins, L.proxyScopePluginsDesc, sc.plugins !== false, async () => {
|
|
726
|
+
const next = !(sc.plugins !== false);
|
|
727
|
+
if (await persistProxyScope({ plugins: next })) sc.plugins = next;
|
|
728
|
+
renderProxyScopes();
|
|
729
|
+
}),
|
|
730
|
+
settingRow(L.proxyScopeDeps, L.proxyScopeDepsDesc, sc.deps !== false, async () => {
|
|
731
|
+
const next = !(sc.deps !== false);
|
|
732
|
+
if (await persistProxyScope({ deps: next })) sc.deps = next;
|
|
733
|
+
renderProxyScopes();
|
|
734
|
+
}),
|
|
735
|
+
settingRow(L.proxyScopeSync, L.proxyScopeSyncDesc, sc.sync === true, async () => {
|
|
736
|
+
const next = !(sc.sync === true);
|
|
737
|
+
if (await persistProxyScope({ sync: next })) sc.sync = next;
|
|
738
|
+
renderProxyScopes();
|
|
739
|
+
}),
|
|
740
|
+
);
|
|
741
|
+
};
|
|
742
|
+
renderProxyScopes();
|
|
658
743
|
paneProxy.append(
|
|
659
744
|
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.proxyDesc),
|
|
660
745
|
h('div', { class: 'mt-3 flex items-center gap-2' },
|
|
@@ -663,6 +748,7 @@ export function openSettingsDialog() {
|
|
|
663
748
|
proxyTestBtn),
|
|
664
749
|
h('p', { class: 'mt-2 text-ui-xs/relaxed text-foreground-subtle' }, L.proxyHint),
|
|
665
750
|
proxyTestState,
|
|
751
|
+
h('div', { class: 'mt-3 divide-y divide-border rounded-xl border border-border' }, proxyScopeWrap),
|
|
666
752
|
);
|
|
667
753
|
|
|
668
754
|
// “性能”:空闲项目内存回收(开关 + 两个时长)。idleReclaimStartupMinutes
|
|
@@ -712,8 +798,10 @@ export function openSettingsDialog() {
|
|
|
712
798
|
|
|
713
799
|
// “同步”:把 ZCode Pro 设置与模型设置手动备份到 WebDAV / 从 WebDAV 恢复
|
|
714
800
|
// (交互参考 Tampermonkey:URL/登录/密码 + 手动触发,无自动同步)。仅 WebDAV
|
|
715
|
-
//
|
|
716
|
-
//
|
|
801
|
+
// 一种后端且本就只在点击时传输,不设“类型/禁用”下拉。表单显式保存(旁有
|
|
802
|
+
// 「测试」用当前表单值验证连通);同步内容开关即时保存;下载先弹云端版本
|
|
803
|
+
// 列表(固定名 = 最新 + 带时间戳的历史),任选一个导入,导入会覆盖本机、
|
|
804
|
+
// 先弹二次确认
|
|
717
805
|
const savedSync = (config.sync && typeof config.sync === 'object') ? config.sync
|
|
718
806
|
: { url: '', dir: 'zcode-pro', login: '', password: '', include: { zcodepro: true, model: true }, lastPushAt: null, lastPullAt: null };
|
|
719
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';
|
|
@@ -761,6 +849,44 @@ export function openSettingsDialog() {
|
|
|
761
849
|
syncCheckDirty();
|
|
762
850
|
}
|
|
763
851
|
};
|
|
852
|
+
// 「测试」:用表单当前值(先测再存)验证 WebDAV:连接与凭据 → 上传目录
|
|
853
|
+
// (缺则自动建,等同上传时的行为)→ 文件列表;结果仿“代理”标签页的检测,
|
|
854
|
+
// 一行显示各步耗时与云端备份文件数
|
|
855
|
+
const syncTestState = h('p', { class: 'mt-2 text-ui-xs/relaxed text-foreground-subtle' });
|
|
856
|
+
let syncTesting = false;
|
|
857
|
+
const syncTestBtn = btnSmall(L.syncTest, () => { void runSyncTest(); }, 'shrink-0');
|
|
858
|
+
const runSyncTest = async () => {
|
|
859
|
+
if (syncTesting) return;
|
|
860
|
+
// 没填 URL 不发请求(等超时才报“测试失败”的体验太差),立即提示
|
|
861
|
+
if (!syncUrlInput.value.trim()) {
|
|
862
|
+
syncTestState.className = 'mt-2 text-ui-xs/relaxed text-amber-500';
|
|
863
|
+
syncTestState.textContent = L.syncTestEmpty;
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
syncTesting = true;
|
|
867
|
+
syncTestBtn.disabled = true;
|
|
868
|
+
syncTestBtn.textContent = L.syncTesting;
|
|
869
|
+
syncTestState.textContent = '';
|
|
870
|
+
syncTestState.className = 'mt-2 text-ui-xs/relaxed text-foreground-subtle';
|
|
871
|
+
const res = await rpc('/sync/test', { method: 'POST', body: { sync: syncFormValues() } });
|
|
872
|
+
syncTesting = false;
|
|
873
|
+
syncTestBtn.disabled = false;
|
|
874
|
+
syncTestBtn.textContent = L.syncTest;
|
|
875
|
+
if (!res.ok) {
|
|
876
|
+
syncTestState.className = 'mt-2 text-ui-xs/relaxed text-destructive';
|
|
877
|
+
syncTestState.textContent = L.syncTestFailed + ': ' + errText(res);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const stepName = { connect: L.syncTestStepConnect, dir: L.syncTestStepDir, list: L.syncTestStepList };
|
|
881
|
+
const parts = (res.targets || []).map((t) =>
|
|
882
|
+
`${stepName[t.step] || t.step} ${t.ok ? '✓' + (t.ms != null ? ' ' + t.ms + 'ms' : '') : '✗ ' + (t.error || t.httpCode || '')}`.trim());
|
|
883
|
+
const allOk = (res.targets || []).length > 0 && (res.targets || []).every((t) => t.ok);
|
|
884
|
+
let host = syncUrlInput.value.trim();
|
|
885
|
+
try { host = new URL(host).host; } catch { /* 非法输入保留原文 */ }
|
|
886
|
+
syncTestState.className = 'mt-2 text-ui-xs/relaxed ' + (allOk ? 'text-foreground-subtle' : 'text-amber-500');
|
|
887
|
+
syncTestState.textContent = `${host}:${parts.join(' · ')}`
|
|
888
|
+
+ (allOk && res.fileCount != null ? `(${L.syncTestFilesFound.replace('{n}', res.fileCount)})` : '');
|
|
889
|
+
};
|
|
764
890
|
// 坚果云推荐行:外链经宿主 openExternal 打开系统浏览器(无该能力时只出纯文字)
|
|
765
891
|
const JIANGUO_HELP = 'https://help.jianguoyun.com/?p=2064';
|
|
766
892
|
const syncRecLine = h('p', { class: 'mt-1 text-ui-xs/relaxed text-foreground-subtle' });
|
|
@@ -798,61 +924,199 @@ export function openSettingsDialog() {
|
|
|
798
924
|
);
|
|
799
925
|
};
|
|
800
926
|
renderSyncInclude();
|
|
801
|
-
// 上次操作时间 +
|
|
927
|
+
// 上次操作时间 + 上传/下载按钮;上传固定名与历史版本双写,下载弹云端版本列表
|
|
802
928
|
const fmtSyncTime = (ts) => (ts ? new Date(ts).toLocaleString() : L.syncNever);
|
|
803
929
|
const syncStatusLine = h('div', { class: 'text-ui-xs/relaxed text-foreground-subtle' });
|
|
804
930
|
const renderSyncStatus = (s) => {
|
|
805
931
|
syncStatusLine.textContent = `${L.syncLastPush} ${fmtSyncTime(s.lastPushAt)} · ${L.syncLastPull} ${fmtSyncTime(s.lastPullAt)}`;
|
|
806
932
|
};
|
|
807
933
|
renderSyncStatus(savedSync);
|
|
808
|
-
const syncUploadBtn = btnSmall(L.syncUpload, () => { void
|
|
809
|
-
const syncDownloadBtn = btnSmall(L.syncDownload, () => {
|
|
810
|
-
|
|
934
|
+
const syncUploadBtn = btnSmall(L.syncUpload, () => { void runSyncPush(); }, 'shrink-0', 'primary');
|
|
935
|
+
const syncDownloadBtn = btnSmall(L.syncDownload, () => { openSyncVersionPicker(); }, 'shrink-0');
|
|
936
|
+
// 「从云端下载」先弹版本列表(备份管理器的样子):列出云端目录里的备份文件
|
|
937
|
+
// (固定名 = 最新一版 + 带时间戳的历史),每行可导入到本机 / 从云端删除 /
|
|
938
|
+
// 另存到本机磁盘;导入按文件名前缀决定覆盖哪一项,不受“同步内容”勾选影响
|
|
939
|
+
const openSyncVersionPicker = () => {
|
|
811
940
|
openDialog({
|
|
812
|
-
title: L.
|
|
813
|
-
description: L.
|
|
814
|
-
width: '
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
)
|
|
821
|
-
|
|
941
|
+
title: L.syncPickTitle,
|
|
942
|
+
description: L.syncPickDesc,
|
|
943
|
+
width: 'max-w-2xl',
|
|
944
|
+
dismissOnOutside: false,
|
|
945
|
+
onMount: async ({ body: pickBody, content, close: closePicker }) => {
|
|
946
|
+
// 标题栏 ✕(设置弹窗同款);h2 也是拖拽把手,✕ 上要拦住 mousedown 冒泡
|
|
947
|
+
const titleEl = content && content.firstElementChild;
|
|
948
|
+
if (titleEl && titleEl.tagName === 'H2') {
|
|
949
|
+
titleEl.classList.add('flex', 'w-full', 'items-center', 'justify-between');
|
|
950
|
+
titleEl.replaceChildren(
|
|
951
|
+
h('span', { class: 'flex min-w-0 items-center' }, L.syncPickTitle),
|
|
952
|
+
h('button', {
|
|
953
|
+
type: 'button',
|
|
954
|
+
class: 'zcodepro-close',
|
|
955
|
+
title: L.close,
|
|
956
|
+
'aria-label': L.close,
|
|
957
|
+
onMousedown: (e) => { e.stopPropagation(); },
|
|
958
|
+
onClick: () => { closePicker(); },
|
|
959
|
+
}, '✕'),
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
const listWrap = h('div', { class: 'space-y-1' });
|
|
963
|
+
pickBody.append(listWrap);
|
|
964
|
+
const fmtSize = (n) => {
|
|
965
|
+
if (!Number.isFinite(n) || n <= 0) return '';
|
|
966
|
+
return n >= 1048576 ? (n / 1048576).toFixed(2) + ' MB' : Math.max(1, Math.round(n / 1024)) + ' KB';
|
|
967
|
+
};
|
|
968
|
+
const setStatus = (el) => { listWrap.replaceChildren(el); };
|
|
969
|
+
const setRowBusy = (btns, busy) => { for (const b of btns) b.disabled = busy; };
|
|
970
|
+
const versionRow = (f) => {
|
|
971
|
+
// 固定名(zcodepro.json / model.json)不含时间戳段,始终是最新一版;
|
|
972
|
+
// 归属哪项同步内容由分组标题说明,行内不再重复
|
|
973
|
+
const isLatest = !/-\d{4}-\d{2}-\d{2}T/.test(f.name);
|
|
974
|
+
const importBtn = btnSmall(L.syncVerImport, () => { confirmImport(f, rowBtns, importBtn); }, 'shrink-0');
|
|
975
|
+
const deleteBtn = btnSmall(L.syncVerDelete, () => { confirmDelete(f, rowBtns, isLatest); }, 'shrink-0');
|
|
976
|
+
const saveBtn = btnSmall(L.syncVerSave, () => { void exportVersion(f, rowBtns, saveBtn); }, 'shrink-0');
|
|
977
|
+
const rowBtns = [importBtn, deleteBtn, saveBtn];
|
|
978
|
+
return h('div', { class: 'flex items-center gap-3 rounded-xl border border-border px-3 py-2' },
|
|
979
|
+
h('div', { class: 'min-w-0 flex-1' },
|
|
980
|
+
h('div', { class: 'flex min-w-0 items-center gap-1.5' },
|
|
981
|
+
h('span', { class: 'truncate font-mono text-ui-xs text-foreground', title: f.name }, f.name),
|
|
982
|
+
...(isLatest ? [h('span', { class: 'shrink-0 rounded border border-border px-1.5 text-ui-xs text-foreground-subtle' }, L.syncVerLatest)] : [])),
|
|
983
|
+
h('div', { class: 'mt-0.5 text-ui-xs/relaxed text-foreground-subtle' },
|
|
984
|
+
[fmtSize(f.size), f.lastModified ? new Date(f.lastModified).toLocaleString() : ''].filter(Boolean).join(' · '))),
|
|
985
|
+
h('div', { class: 'flex shrink-0 items-center gap-1.5' }, importBtn, deleteBtn, saveBtn));
|
|
986
|
+
};
|
|
987
|
+
const loadList = async () => {
|
|
988
|
+
setStatus(h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.syncPickLoading));
|
|
989
|
+
const res = await rpc('/sync/versions', { method: 'POST', body: {} });
|
|
990
|
+
if (!res.ok) {
|
|
991
|
+
setStatus(h('p', { class: 'text-ui-sm text-destructive' }, L.failed + ': ' + errText(res)));
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const files = res.files || [];
|
|
995
|
+
if (!files.length) {
|
|
996
|
+
setStatus(h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.syncPickEmpty));
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
// 按同步内容分组排列:组标题下固定名(最新)放最上面,历史版本按
|
|
1000
|
+
// 时间新→旧往下——两组文件多了也不会按时间交错混排
|
|
1001
|
+
const parts = [];
|
|
1002
|
+
for (const [prefix, label] of [['zcodepro', L.syncIncludeZcp], ['model', L.syncIncludeModel]]) {
|
|
1003
|
+
const group = files.filter((f) => f.name === `${prefix}.json` || f.name.startsWith(`${prefix}-`));
|
|
1004
|
+
if (!group.length) continue;
|
|
1005
|
+
const latest = group.find((f) => f.name === `${prefix}.json`);
|
|
1006
|
+
const history = group.filter((f) => f !== latest)
|
|
1007
|
+
.sort((a, b) => ((b.lastModified || 0) - (a.lastModified || 0)) || b.name.localeCompare(a.name));
|
|
1008
|
+
parts.push(h('div', { class: 'px-1 pt-2 text-ui-xs font-medium text-foreground-subtle' },
|
|
1009
|
+
`${label}${isZhLocale() ? '(' : ' ('}${group.length}${isZhLocale() ? ')' : ')'}`));
|
|
1010
|
+
for (const f of [latest, ...history]) {
|
|
1011
|
+
if (f) parts.push(versionRow(f));
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
listWrap.replaceChildren(...parts);
|
|
1015
|
+
};
|
|
1016
|
+
// 导入沿用原有的覆盖确认文案,并注明选中的是哪个文件
|
|
1017
|
+
const confirmImport = (f, rowBtns, importBtn) => {
|
|
1018
|
+
openDialog({
|
|
1019
|
+
title: L.syncConfirmTitle,
|
|
1020
|
+
width: 'sm:max-w-md',
|
|
1021
|
+
onMount: ({ body: confirmBody, close: closeConfirm }) => {
|
|
1022
|
+
confirmBody.append(
|
|
1023
|
+
h('p', { class: 'mb-2 truncate font-mono text-ui-xs text-foreground-subtle', title: f.name }, f.name),
|
|
1024
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.syncConfirmDesc),
|
|
1025
|
+
dialogFooter(
|
|
1026
|
+
btnSecondary(L.cancel, () => closeConfirm()),
|
|
1027
|
+
btnPrimary(L.syncConfirmGo, () => { closeConfirm(); void importVersion(f, rowBtns, importBtn); }, 'min-w-24'),
|
|
1028
|
+
),
|
|
1029
|
+
);
|
|
1030
|
+
},
|
|
1031
|
+
});
|
|
1032
|
+
};
|
|
1033
|
+
const importVersion = async (f, rowBtns, importBtn) => {
|
|
1034
|
+
setRowBusy(rowBtns, true);
|
|
1035
|
+
importBtn.textContent = L.syncVerImporting;
|
|
1036
|
+
const res = await rpc('/sync/version/apply', { method: 'POST', body: { file: f.name } });
|
|
1037
|
+
setRowBusy(rowBtns, false);
|
|
1038
|
+
importBtn.textContent = L.syncVerImport;
|
|
1039
|
+
if (!res.ok) {
|
|
1040
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
closePicker();
|
|
1044
|
+
renderSyncStatus(res.sync || {});
|
|
1045
|
+
// 该版本已落到本机:刷新弹窗内开关显示,并让常驻功能立即按新配置生效
|
|
1046
|
+
clearConfigCache();
|
|
1047
|
+
window.dispatchEvent(new CustomEvent('zcodepro:config-changed'));
|
|
1048
|
+
Object.assign(config, await getConfig(true));
|
|
1049
|
+
refreshRows();
|
|
1050
|
+
renderToolbarSwitch();
|
|
1051
|
+
const names = (res.applied || [])
|
|
1052
|
+
.map((k) => (k === 'model' ? L.syncIncludeModel : L.syncIncludeZcp))
|
|
1053
|
+
.join(isZhLocale() ? '、' : ', ');
|
|
1054
|
+
showToast(names ? `${L.syncDownloaded}(${names})` : L.syncDownloaded, 'success');
|
|
1055
|
+
};
|
|
1056
|
+
const confirmDelete = (f, rowBtns, isLatest) => {
|
|
1057
|
+
openDialog({
|
|
1058
|
+
title: L.syncVerDeleteTitle,
|
|
1059
|
+
width: 'sm:max-w-md',
|
|
1060
|
+
onMount: ({ body: confirmBody, close: closeConfirm }) => {
|
|
1061
|
+
confirmBody.append(
|
|
1062
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' },
|
|
1063
|
+
L.syncVerDeleteDesc,
|
|
1064
|
+
h('span', { class: 'font-mono' }, f.name)),
|
|
1065
|
+
// 删“最新”文件会让旧版 zcode-pro 失去下载入口,多提醒一句
|
|
1066
|
+
...(isLatest ? [h('p', { class: 'mt-2 text-ui-xs/relaxed text-amber-500' }, L.syncVerDeleteLatestNote)] : []),
|
|
1067
|
+
dialogFooter(
|
|
1068
|
+
btnSecondary(L.cancel, () => closeConfirm()),
|
|
1069
|
+
btnPrimary(L.syncVerDelete, () => { closeConfirm(); void deleteVersion(f, rowBtns); }, 'min-w-24'),
|
|
1070
|
+
),
|
|
1071
|
+
);
|
|
1072
|
+
},
|
|
1073
|
+
});
|
|
1074
|
+
};
|
|
1075
|
+
const deleteVersion = async (f, rowBtns) => {
|
|
1076
|
+
setRowBusy(rowBtns, true);
|
|
1077
|
+
const res = await rpc('/sync/version/delete', { method: 'POST', body: { file: f.name } });
|
|
1078
|
+
setRowBusy(rowBtns, false);
|
|
1079
|
+
if (!res.ok) {
|
|
1080
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
showToast(L.syncVerDeleted, 'success');
|
|
1084
|
+
void loadList();
|
|
1085
|
+
};
|
|
1086
|
+
const exportVersion = async (f, rowBtns, saveBtn) => {
|
|
1087
|
+
setRowBusy(rowBtns, true);
|
|
1088
|
+
saveBtn.textContent = L.syncVerSaving;
|
|
1089
|
+
// 宿主会弹系统目录选择器选保存位置;用户取消时不作提示
|
|
1090
|
+
const res = await rpc('/sync/version/export', { method: 'POST', body: { file: f.name } });
|
|
1091
|
+
setRowBusy(rowBtns, false);
|
|
1092
|
+
saveBtn.textContent = L.syncVerSave;
|
|
1093
|
+
if (!res.ok) {
|
|
1094
|
+
if (res.code !== 'export-cancelled') showToast(L.failed + ': ' + errText(res), 'error');
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
showToast(`${L.syncVerSaved} ${res.path}`, 'success');
|
|
1098
|
+
};
|
|
1099
|
+
void loadList();
|
|
822
1100
|
},
|
|
823
1101
|
});
|
|
824
1102
|
};
|
|
825
|
-
|
|
1103
|
+
// 上传:云端固定名(最新)与时间戳历史版本由服务端一并写入云端;下载期间按钮也
|
|
1104
|
+
// 禁用(列表在变,选了也会过期)
|
|
1105
|
+
const runSyncPush = async () => {
|
|
826
1106
|
syncUploadBtn.disabled = true;
|
|
827
1107
|
syncDownloadBtn.disabled = true;
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const res = await rpc('/sync/run', { method: 'POST', body: { direction } });
|
|
1108
|
+
const idleText = syncUploadBtn.textContent;
|
|
1109
|
+
syncUploadBtn.textContent = L.syncUploading;
|
|
1110
|
+
const res = await rpc('/sync/run', { method: 'POST', body: { direction: 'push' } });
|
|
832
1111
|
syncUploadBtn.disabled = false;
|
|
833
1112
|
syncDownloadBtn.disabled = false;
|
|
834
|
-
|
|
1113
|
+
syncUploadBtn.textContent = idleText;
|
|
835
1114
|
if (!res.ok) {
|
|
836
1115
|
showToast(L.failed + ': ' + errText(res), 'error');
|
|
837
1116
|
return;
|
|
838
1117
|
}
|
|
839
1118
|
renderSyncStatus(res.sync || {});
|
|
840
|
-
|
|
841
|
-
// 云端配置已落到本机:刷新弹窗内开关显示,并让常驻功能立即按新配置生效
|
|
842
|
-
clearConfigCache();
|
|
843
|
-
window.dispatchEvent(new CustomEvent('zcodepro:config-changed'));
|
|
844
|
-
Object.assign(config, await getConfig(true));
|
|
845
|
-
refreshRows();
|
|
846
|
-
renderToolbarSwitch();
|
|
847
|
-
}
|
|
848
|
-
// 下载 toast 标明本次覆盖了哪几项(两项独立成文件,可能只覆盖了一项)
|
|
849
|
-
if (direction === 'push') showToast(L.syncUploaded, 'success');
|
|
850
|
-
else {
|
|
851
|
-
const names = (res.applied || [])
|
|
852
|
-
.map((k) => (k === 'model' ? L.syncIncludeModel : L.syncIncludeZcp))
|
|
853
|
-
.join(isZhLocale() ? '、' : ', ');
|
|
854
|
-
showToast(names ? `${L.syncDownloaded}(${names})` : L.syncDownloaded, 'success');
|
|
855
|
-
}
|
|
1119
|
+
showToast(L.syncUploaded, 'success');
|
|
856
1120
|
};
|
|
857
1121
|
paneSync.append(
|
|
858
1122
|
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.syncDesc),
|
|
@@ -865,7 +1129,8 @@ export function openSettingsDialog() {
|
|
|
865
1129
|
h('div', { class: 'min-w-0' },
|
|
866
1130
|
h('p', { class: 'text-ui-xs/relaxed text-foreground-subtle' }, L.syncFormHint),
|
|
867
1131
|
syncRecLine),
|
|
868
|
-
syncSaveBtn)),
|
|
1132
|
+
h('div', { class: 'flex shrink-0 items-center gap-1.5' }, syncSaveBtn, syncTestBtn))),
|
|
1133
|
+
syncTestState,
|
|
869
1134
|
h('div', { class: 'mt-2 divide-y divide-border rounded-xl border border-border' }, syncIncludeWrap),
|
|
870
1135
|
h('div', { class: 'mt-2 flex items-center justify-between gap-2 rounded-xl border border-border p-2' },
|
|
871
1136
|
h('div', { class: 'min-w-0' }, syncStatusLine),
|
|
@@ -873,6 +1138,48 @@ export function openSettingsDialog() {
|
|
|
873
1138
|
h('p', { class: 'mt-2 text-ui-xs/relaxed text-foreground-subtle' }, L.syncHint),
|
|
874
1139
|
);
|
|
875
1140
|
|
|
1141
|
+
// “调试”:开发模式开关 + 重载按钮。重载 = 构建 → 换新启动器进程(新 helper
|
|
1142
|
+
// 代码与新注入脚本)→ 各 ZCode 窗口检测到换代自动整页刷新重新注入;
|
|
1143
|
+
// ZCode 本体与所有会话不受影响,不用重启 ZCode
|
|
1144
|
+
const persistDevMode = async (v) => {
|
|
1145
|
+
const res = await rpc('/config', { method: 'POST', body: { devMode: v } });
|
|
1146
|
+
if (!res.ok) {
|
|
1147
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
if (res.config) config.devMode = res.config.devMode;
|
|
1151
|
+
return true;
|
|
1152
|
+
};
|
|
1153
|
+
const devReloadBtn = btnSmall(L.devReload, () => { void runDevReload(); }, 'shrink-0', 'primary');
|
|
1154
|
+
const runDevReload = async () => {
|
|
1155
|
+
devReloadBtn.disabled = true;
|
|
1156
|
+
devReloadBtn.textContent = L.devReloading;
|
|
1157
|
+
// 旧进程应答后即退出,fetch 可能中断,成败都进入下面的兜底刷新
|
|
1158
|
+
try { await rpc('/dev/reload', { method: 'POST', body: {} }); } catch { /* ignore */ }
|
|
1159
|
+
// 兜底:正常情况下新启动器注入时本页会自动刷新;2 秒没动静(交接失败)
|
|
1160
|
+
// 就自己刷新,宁可多刷一次也不能停在旧代码 + 失效 token 上
|
|
1161
|
+
setTimeout(() => { try { location.reload(); } catch { /* ignore */ } }, 2000);
|
|
1162
|
+
};
|
|
1163
|
+
const devModeWrap = h('div');
|
|
1164
|
+
const renderDevPane = () => {
|
|
1165
|
+
const dev = config.devMode === true;
|
|
1166
|
+
devModeWrap.replaceChildren(
|
|
1167
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.devModeDesc),
|
|
1168
|
+
h('div', { class: 'mt-3 divide-y divide-border rounded-xl border border-border' },
|
|
1169
|
+
settingRow(L.devMode, L.devModeRowDesc, dev, async () => {
|
|
1170
|
+
if (await persistDevMode(!dev)) renderDevPane();
|
|
1171
|
+
}),
|
|
1172
|
+
...(dev ? [h('div', { class: 'flex items-center justify-between gap-2 px-2 py-1.5' },
|
|
1173
|
+
h('div', { class: 'min-w-0' },
|
|
1174
|
+
h('div', { class: 'text-ui-sm font-medium text-foreground' }, L.devReload),
|
|
1175
|
+
h('div', { class: 'mt-0.5 text-ui-xs/relaxed text-foreground-subtle' }, L.devReloadDesc)),
|
|
1176
|
+
devReloadBtn)] : []),
|
|
1177
|
+
),
|
|
1178
|
+
);
|
|
1179
|
+
};
|
|
1180
|
+
renderDevPane();
|
|
1181
|
+
paneDebug.append(devModeWrap);
|
|
1182
|
+
|
|
876
1183
|
// “视觉代理”:编辑 ~/.zcode/zcode-vision.json(zcode-vision 插件与 /vision-* 命令共用同一文件)。
|
|
877
1184
|
// 代理列表顺序即执行链;结构性改动(开关/模式/排序/删增)即时保存,文本输入防抖保存。
|
|
878
1185
|
// 注意:保存时链按全部代理重建——若用 /vision-chain 配过子集链,会被这里覆盖(两套入口语义如此,面板以列表为准)。
|
|
@@ -894,8 +1201,6 @@ export function openSettingsDialog() {
|
|
|
894
1201
|
skipMinutes: 30,
|
|
895
1202
|
forceIntercept: true,
|
|
896
1203
|
};
|
|
897
|
-
// 供应商下拉数据:合并 zcode 两张供应商表(config.json 的 provider 与 provider_config.json 的 providerRules)
|
|
898
|
-
const visionProviders = visionProvidersRes.ok && Array.isArray(visionProvidersRes.providers) ? visionProvidersRes.providers : [];
|
|
899
1204
|
let visionCfg = visionRes.ok && visionRes.config && typeof visionRes.config === 'object'
|
|
900
1205
|
? structuredClone(visionRes.config)
|
|
901
1206
|
: null;
|
|
@@ -915,7 +1220,55 @@ export function openSettingsDialog() {
|
|
|
915
1220
|
style: 'display:none',
|
|
916
1221
|
});
|
|
917
1222
|
const visionInputCls = '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';
|
|
1223
|
+
// 未装插件时的一键安装卡(与 Headroom 面板同款交互):helper 单插件安装,
|
|
1224
|
+
// 装完 onInstalled 原地恢复面板。rtk/vision 面板共用。
|
|
1225
|
+
const pluginInstallCard = (pluginName, onInstalled) => {
|
|
1226
|
+
const installBtn = btnSmall(L.headroomInstall, () => { void runInstall(); }, 'shrink-0');
|
|
1227
|
+
const installState = h('div', { class: 'mt-0.5 truncate text-ui-xs/relaxed text-foreground-subtle' });
|
|
1228
|
+
const runInstall = async () => {
|
|
1229
|
+
installBtn.disabled = true;
|
|
1230
|
+
installBtn.textContent = L.headroomInstalling;
|
|
1231
|
+
installState.textContent = '';
|
|
1232
|
+
const res = await rpc('/plugins/update', { method: 'POST', body: { name: pluginName } });
|
|
1233
|
+
if (!res.ok) {
|
|
1234
|
+
installBtn.disabled = false;
|
|
1235
|
+
installBtn.textContent = L.headroomInstall;
|
|
1236
|
+
showToast(L.pluginsUpdateFailed + ': ' + errText(res), 'error');
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
if (onInstalled) await onInstalled();
|
|
1240
|
+
showToast(L.pluginInstallDone.replaceAll('{name}', pluginName), 'success');
|
|
1241
|
+
};
|
|
1242
|
+
return h('div', { class: 'mt-3 flex items-center justify-between gap-2 rounded-xl border border-border p-2' },
|
|
1243
|
+
h('div', { class: 'min-w-0' },
|
|
1244
|
+
h('div', { class: 'text-ui-sm font-medium text-foreground' }, L.pluginInstallCardTitle.replaceAll('{name}', pluginName)),
|
|
1245
|
+
h('div', { class: 'mt-0.5 text-ui-xs/relaxed text-foreground-subtle' }, L.pluginInstallCardDesc),
|
|
1246
|
+
installState),
|
|
1247
|
+
installBtn);
|
|
1248
|
+
};
|
|
1249
|
+
|
|
918
1250
|
const renderVision = () => {
|
|
1251
|
+
if (!visionRes.ok) {
|
|
1252
|
+
paneVision.replaceChildren(
|
|
1253
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.visionDesc),
|
|
1254
|
+
h('p', { class: 'mt-2 text-ui-sm text-destructive' }, L.visionLoadFailed + ': ' + errText(visionRes)),
|
|
1255
|
+
);
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
// 插件未装优先显示安装卡——判断只看钩子(visionRes.hook),与配置文件是否存在无关:
|
|
1259
|
+
// 配置是钩子消费的,没钩子就没功能;老用户卸载插件后 ~/.zcode/zcode-vision.json 仍在,
|
|
1260
|
+
// 若按配置存在与否分流会误入完整配置界面而毫无「未安装」提示
|
|
1261
|
+
if (!visionRes.hook) {
|
|
1262
|
+
paneVision.replaceChildren(
|
|
1263
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.visionDesc),
|
|
1264
|
+
pluginInstallCard('zcode-vision', async () => {
|
|
1265
|
+
const meta = await rpc('/vision');
|
|
1266
|
+
if (meta.ok) Object.assign(visionRes, meta);
|
|
1267
|
+
renderVision();
|
|
1268
|
+
}),
|
|
1269
|
+
);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
919
1272
|
if (!visionCfg) {
|
|
920
1273
|
const initBtn = btnSmall(L.visionAddProxy, async () => {
|
|
921
1274
|
const res = await rpc('/vision', { method: 'POST', body: { config: VISION_DEFAULT_CFG } });
|
|
@@ -925,6 +1278,10 @@ export function openSettingsDialog() {
|
|
|
925
1278
|
paneVision.replaceChildren(
|
|
926
1279
|
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.visionDesc),
|
|
927
1280
|
...(visionRes.ok ? [] : [h('p', { class: 'mt-2 text-ui-sm text-destructive' }, L.visionLoadFailed + ': ' + errText(visionRes))]),
|
|
1281
|
+
// 信息卡(仅版本行;配置未建,启用开关随配置出现)
|
|
1282
|
+
h('div', { class: 'mt-3 rounded-xl border border-border p-2' },
|
|
1283
|
+
h('div', { class: 'flex flex-wrap items-center gap-2' },
|
|
1284
|
+
h('span', { class: 'shrink-0 whitespace-nowrap text-ui-xs/relaxed text-foreground-subtle' }, `插件 ${visionRes.pluginVersion || '—'}`))),
|
|
928
1285
|
h('div', { class: 'mt-3 flex justify-end' }, initBtn),
|
|
929
1286
|
);
|
|
930
1287
|
return;
|
|
@@ -957,6 +1314,8 @@ export function openSettingsDialog() {
|
|
|
957
1314
|
return h('label', { class: 'flex shrink-0 items-center gap-1.5', title },
|
|
958
1315
|
h('span', { class: 'text-ui-xs font-medium text-foreground-subtle' }, label), n);
|
|
959
1316
|
};
|
|
1317
|
+
// 接口地址 / API Key 是否显示走 provider-fields 的共用规则
|
|
1318
|
+
//(isManualConn:不跟随或供应商已删才显示,连接其余由跟随对象负责)
|
|
960
1319
|
const cards = visionCfg.proxies.map((p, i) => h('div', { class: 'mt-2 rounded-xl border border-border p-2' },
|
|
961
1320
|
h('div', { class: 'flex flex-wrap items-center gap-2' },
|
|
962
1321
|
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)),
|
|
@@ -987,43 +1346,35 @@ export function openSettingsDialog() {
|
|
|
987
1346
|
smallBtn(L.visionRemove, () => { visionCfg.proxies.splice(i, 1); void persistVision(); renderVision(); }),
|
|
988
1347
|
),
|
|
989
1348
|
h('div', { class: 'mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2' },
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|| visionProviders.some((pr) => pr.id === v || pr.name === v || (pr.aliases || []).includes(v));
|
|
997
|
-
const sel = h('select', { class: visionInputCls });
|
|
998
|
-
const syncSelect = () => {
|
|
999
|
-
const cur = (p.useProvider || '').trim();
|
|
1000
|
-
const opts = [h('option', { value: '' }, L.visionUseProviderNone), h('option', { value: 'session' }, L.visionUseProviderSession)];
|
|
1001
|
-
for (const pr of visionProviders) {
|
|
1002
|
-
opts.push(h('option', { value: pr.id }, pr.name && pr.name !== pr.id ? `${pr.name}(${pr.id})` : pr.id));
|
|
1003
|
-
}
|
|
1004
|
-
if (cur && !opts.some((o) => o.value === cur)) opts.push(h('option', { value: cur }, `${cur}(${L.visionUseProvider})`));
|
|
1005
|
-
sel.replaceChildren(...opts);
|
|
1006
|
-
sel.value = cur;
|
|
1007
|
-
};
|
|
1008
|
-
sel.addEventListener('change', () => {
|
|
1009
|
-
input.value = sel.value;
|
|
1010
|
-
p.useProvider = sel.value.trim();
|
|
1011
|
-
warn.style.display = 'none';
|
|
1349
|
+
// 供应商与模型走 provider-fields 共用逻辑:跟随会话时模型下拉为
|
|
1350
|
+
// 全部供应商模型汇总,具体供应商为其模型列表;已删 id 标注「已删除」
|
|
1351
|
+
field(L.visionUseProvider, comboField(p.useProvider || '',
|
|
1352
|
+
providerOptionsOf(titleGenProviders, L),
|
|
1353
|
+
(v, src) => {
|
|
1354
|
+
p.useProvider = v;
|
|
1012
1355
|
saveVisionSoon();
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1356
|
+
// 下拉切换后重渲染整个面板:模型下拉选项随新供应商变化,接口
|
|
1357
|
+
// 地址/API Key 字段按「不跟随或供应商已删才显示」重新显隐
|
|
1358
|
+
if (src === 'select') renderVision();
|
|
1359
|
+
},
|
|
1360
|
+
{ placeholder: L.titleGenProviderSession, staleLabel: L.titleGenProviderDeleted })),
|
|
1361
|
+
// 跟随会话时模型下拉头部有「跟随当前会话当前模型」(值为空,
|
|
1362
|
+
// 插件 ≥1.6.1 在调用时解析为会话当前模型);具体供应商为其模型列表
|
|
1363
|
+
field(L.visionModel, comboField(p.model || '',
|
|
1364
|
+
modelOptionsOf(p.useProvider, titleGenProviders, {
|
|
1365
|
+
followSession: (p.useProvider || '').trim() === 'session',
|
|
1366
|
+
followSessionLabel: L.titleGenModelFollowSession,
|
|
1367
|
+
}),
|
|
1368
|
+
(v) => { p.model = v; saveVisionSoon(); },
|
|
1369
|
+
{
|
|
1370
|
+
placeholder: modelPlaceholderOf(p.useProvider, L),
|
|
1371
|
+
emptyLabel: (p.useProvider || '').trim() === 'session'
|
|
1372
|
+
? L.titleGenModelFollowSession : L.titleGenModelEmpty,
|
|
1373
|
+
})),
|
|
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
|
+
] : []),
|
|
1027
1378
|
),
|
|
1028
1379
|
(() => {
|
|
1029
1380
|
const n = h('textarea', { class: 'zcodepro-textarea zcodepro-textarea-sm', rows: '3', placeholder: L.visionPrompt });
|
|
@@ -1046,17 +1397,22 @@ export function openSettingsDialog() {
|
|
|
1046
1397
|
});
|
|
1047
1398
|
paneVision.replaceChildren(
|
|
1048
1399
|
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.visionDesc),
|
|
1049
|
-
//
|
|
1400
|
+
// 信息卡:插件版本 + 启用图片识别开关(与 rtk/headroom 面板同款式)
|
|
1401
|
+
h('div', { class: 'mt-3 rounded-xl border border-border p-2' },
|
|
1402
|
+
h('div', { class: 'flex flex-wrap items-center gap-2' },
|
|
1403
|
+
h('span', { class: 'shrink-0 whitespace-nowrap text-ui-xs/relaxed text-foreground-subtle' }, `插件 ${visionRes.pluginVersion || '—'}`),
|
|
1404
|
+
h('div', { class: 'ml-auto' },
|
|
1405
|
+
settingRow(L.visionEnabled, '', visionCfg.enabled !== false, async () => {
|
|
1406
|
+
visionCfg.enabled = !(visionCfg.enabled !== false);
|
|
1407
|
+
await persistVision();
|
|
1408
|
+
renderVision();
|
|
1409
|
+
})))),
|
|
1410
|
+
// 链模式 + 强制拦截开关合到一行(启用开关已上移信息卡)
|
|
1050
1411
|
h('div', { class: 'mt-3 flex flex-wrap items-center gap-x-4 gap-y-1.5 rounded-xl border border-border p-1' },
|
|
1051
1412
|
h('span', { class: 'ml-1.5 shrink-0 text-ui-sm font-medium text-foreground' }, L.visionMode),
|
|
1052
1413
|
modeBtn('fallback', L.visionModeFallback, L.visionModeFallbackDesc),
|
|
1053
1414
|
modeBtn('pipeline', L.visionModePipeline, L.visionModePipelineDesc),
|
|
1054
1415
|
h('div', { class: 'ml-auto flex items-center gap-4' },
|
|
1055
|
-
settingRow(L.visionEnabled, '', visionCfg.enabled !== false, async () => {
|
|
1056
|
-
visionCfg.enabled = !(visionCfg.enabled !== false);
|
|
1057
|
-
await persistVision();
|
|
1058
|
-
renderVision();
|
|
1059
|
-
}),
|
|
1060
1416
|
settingRow(L.visionForceIntercept, '', visionCfg.forceIntercept !== false, async () => {
|
|
1061
1417
|
visionCfg.forceIntercept = !(visionCfg.forceIntercept !== false);
|
|
1062
1418
|
await persistVision();
|
|
@@ -1122,15 +1478,20 @@ export function openSettingsDialog() {
|
|
|
1122
1478
|
return res.ok;
|
|
1123
1479
|
};
|
|
1124
1480
|
const rtkInputCls = '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';
|
|
1125
|
-
// rtk 本体(GitHub Releases
|
|
1481
|
+
// rtk 本体(GitHub Releases 下载替换)的版本行与升级控件,渲染函数外创建一次。
|
|
1482
|
+
// 信息卡单行:版本行 + 弹性状态槽(空闲显示本体位置,检查更新/升级时被活动
|
|
1483
|
+
// 日志顶替,见 makeUpgradeControls 的 idleText)+ 检查更新按钮 + 启用开关
|
|
1126
1484
|
const rtkVersionLine = h('span', { class: 'shrink-0 whitespace-nowrap text-ui-xs/relaxed text-foreground-subtle' });
|
|
1485
|
+
let rtkLocText = '';
|
|
1127
1486
|
const renderRtkVersion = (meta) => {
|
|
1128
|
-
const v = meta.version || 'rtk';
|
|
1129
|
-
rtkVersionLine.textContent = meta.
|
|
1487
|
+
const v = String(meta.version || '').replace(/^rtk\s+/i, '').trim();
|
|
1488
|
+
rtkVersionLine.textContent = `插件 ${meta.pluginVersion || '—'}${v ? ` · rtk ${v}` : ''}`;
|
|
1489
|
+
rtkLocText = meta.binPath || '';
|
|
1130
1490
|
};
|
|
1131
1491
|
renderRtkVersion(rtkRes);
|
|
1132
1492
|
const rtkUp = makeUpgradeControls({
|
|
1133
1493
|
endpoint: '/rtk',
|
|
1494
|
+
idleText: () => rtkLocText,
|
|
1134
1495
|
metaRefresh: async () => {
|
|
1135
1496
|
const meta = await rpc('/rtk');
|
|
1136
1497
|
if (meta.ok) {
|
|
@@ -1143,15 +1504,50 @@ export function openSettingsDialog() {
|
|
|
1143
1504
|
rtkUpgradePollStop = rtkUp.stop;
|
|
1144
1505
|
// 版本/路径/检查更新 + 启用 rtk 压缩开关合到一行(开关靠右,顶部已有简介不带描述)
|
|
1145
1506
|
const rtkSwitchRow = h('div');
|
|
1146
|
-
const rtkVersionCard = h('div', { class: 'mt-3 flex items-center gap-2 rounded-xl border border-border p-2' },
|
|
1147
|
-
rtkVersionLine, rtkUp.state, rtkUp.btn,
|
|
1507
|
+
const rtkVersionCard = h('div', { class: 'mt-3 flex flex-wrap items-center gap-2 rounded-xl border border-border p-2' },
|
|
1508
|
+
rtkVersionLine, rtkUp.state, rtkUp.btn,
|
|
1509
|
+
h('div', { class: 'ml-auto shrink-0' }, rtkSwitchRow));
|
|
1148
1510
|
const rtkBody = h('div');
|
|
1149
|
-
//
|
|
1511
|
+
// 插件未装时的一键安装行(本体未装另有提示行),装完原地撤下
|
|
1512
|
+
const rtkPluginRow = h('div');
|
|
1513
|
+
const renderRtkPluginRow = () => {
|
|
1514
|
+
rtkPluginRow.replaceChildren(...(rtkRes.hook ? [] : [
|
|
1515
|
+
pluginInstallCard('rtk', async () => {
|
|
1516
|
+
const meta = await rpc('/rtk');
|
|
1517
|
+
if (meta.ok) {
|
|
1518
|
+
Object.assign(rtkRes, meta);
|
|
1519
|
+
// 版本行 span 与 rtkCfg 都是弹窗打开时的未装态快照,装完必须重算/重建
|
|
1520
|
+
renderRtkVersion(meta);
|
|
1521
|
+
// rtkCfg 整体重建(未装态时内置清单为空,不重建则白名单区块渲染旧空清单)
|
|
1522
|
+
rtkCfg = {
|
|
1523
|
+
installed: meta.installed !== false,
|
|
1524
|
+
version: meta.version || '',
|
|
1525
|
+
binPath: meta.binPath || '',
|
|
1526
|
+
mode: meta.mode === 'off' ? 'off' : 'hint',
|
|
1527
|
+
whitelist: Array.isArray(meta.whitelist) ? [...meta.whitelist] : [],
|
|
1528
|
+
builtinGit: Array.isArray(meta.builtinGit) ? [...meta.builtinGit] : [],
|
|
1529
|
+
builtinPlain: Array.isArray(meta.builtinPlain) ? [...meta.builtinPlain] : [],
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
renderRtkPluginRow();
|
|
1533
|
+
renderRtk();
|
|
1534
|
+
}),
|
|
1535
|
+
]));
|
|
1536
|
+
};
|
|
1537
|
+
renderRtkPluginRow();
|
|
1538
|
+
// 简介置顶(与 Headroom 面板一致),其下插件安装卡,再下版本卡(含启用 rtk 压缩开关),再下白名单
|
|
1150
1539
|
paneRtk.append(
|
|
1151
1540
|
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.rtkDesc),
|
|
1541
|
+
rtkPluginRow,
|
|
1152
1542
|
rtkVersionCard,
|
|
1153
1543
|
rtkBody);
|
|
1154
1544
|
const renderRtk = () => {
|
|
1545
|
+
// 插件未装:只显示安装卡,隐藏版本卡与全部设置项(与 vision/headroom 未装时一致)
|
|
1546
|
+
if (rtkRes.ok && !rtkRes.hook) {
|
|
1547
|
+
rtkVersionCard.style.display = 'none';
|
|
1548
|
+
rtkBody.replaceChildren();
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1155
1551
|
rtkVersionCard.style.display = rtkCfg && rtkCfg.installed ? '' : 'none';
|
|
1156
1552
|
if (!rtkCfg) {
|
|
1157
1553
|
rtkBody.replaceChildren(
|
|
@@ -1195,7 +1591,45 @@ export function openSettingsDialog() {
|
|
|
1195
1591
|
rtkBody.replaceChildren(
|
|
1196
1592
|
...(rtkCfg.installed
|
|
1197
1593
|
? []
|
|
1198
|
-
: [h('
|
|
1594
|
+
: [h('div', { class: 'flex items-center gap-2' },
|
|
1595
|
+
h('p', { class: 'min-w-0 flex-1 text-ui-sm text-amber-500' }, L.rtkNotInstalled),
|
|
1596
|
+
// 一键安装本体:与“检查更新”同一条 helper 升级管线(未装则装到 ~/.local/bin)
|
|
1597
|
+
(() => {
|
|
1598
|
+
const b = btnSmall(L.rtkInstallBin, () => { void run(); }, 'shrink-0');
|
|
1599
|
+
const run = async () => {
|
|
1600
|
+
b.disabled = true;
|
|
1601
|
+
b.textContent = L.headroomInstalling;
|
|
1602
|
+
const res = await rpc('/rtk', { method: 'POST', body: { upgrade: true } });
|
|
1603
|
+
if (!res.ok) {
|
|
1604
|
+
b.disabled = false;
|
|
1605
|
+
b.textContent = L.rtkInstallBin;
|
|
1606
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
const done = await new Promise((resolve) => {
|
|
1610
|
+
const iv = setInterval(async () => {
|
|
1611
|
+
const s = await rpc('/rtk/upgrade');
|
|
1612
|
+
if (s.ok && s.upgrade && !s.upgrade.running) { clearInterval(iv); resolve(s.upgrade); }
|
|
1613
|
+
}, 1200);
|
|
1614
|
+
// 超时也要 resolve:否则按钮永久停在“安装中…”
|
|
1615
|
+
setTimeout(() => { clearInterval(iv); resolve(null); }, 10 * 60 * 1000);
|
|
1616
|
+
});
|
|
1617
|
+
if (done && !done.error && done.version) {
|
|
1618
|
+
const meta = await rpc('/rtk');
|
|
1619
|
+
if (meta.ok) {
|
|
1620
|
+
Object.assign(rtkRes, meta);
|
|
1621
|
+
rtkCfg = { ...rtkCfg, installed: meta.installed !== false, version: meta.version || '', binPath: meta.binPath || '' };
|
|
1622
|
+
}
|
|
1623
|
+
showToast(L.rtkBinInstalled, 'success');
|
|
1624
|
+
renderRtk();
|
|
1625
|
+
} else {
|
|
1626
|
+
b.disabled = false;
|
|
1627
|
+
b.textContent = L.rtkInstallBin;
|
|
1628
|
+
showToast((done && done.error) || L.headroomInstallRetry, 'error');
|
|
1629
|
+
}
|
|
1630
|
+
};
|
|
1631
|
+
return b;
|
|
1632
|
+
})())]),
|
|
1199
1633
|
h('div', { class: 'mt-2 rounded-xl border border-border p-2' },
|
|
1200
1634
|
h('div', { class: 'text-ui-sm font-medium text-foreground' }, L.rtkWhitelistTitle),
|
|
1201
1635
|
h('p', { class: 'mt-1 text-ui-xs/relaxed text-foreground-subtle' }, L.rtkWhitelistDesc),
|
|
@@ -1243,15 +1677,18 @@ export function openSettingsDialog() {
|
|
|
1243
1677
|
// 可整面重渲染:未装插件时显示安装卡,安装成功后原地恢复完整面板。
|
|
1244
1678
|
// 版本行与本体升级控件(pip 通道)在渲染函数外创建一次,重渲染时复用同一组节点
|
|
1245
1679
|
const hrVersionLine = h('span', { class: 'shrink-0 whitespace-nowrap text-ui-xs/relaxed text-foreground-subtle' });
|
|
1680
|
+
let hrLocText = '';
|
|
1681
|
+
// 启用开关(与 rtk 面板同款,版本卡右上角):开 = ZCode 供应商走 headroom,
|
|
1682
|
+
// 关 = 直连上游;只改供应商地址,不动代理进程(headroom 可被其他程序共用)
|
|
1683
|
+
const hrSwitchRow = h('div');
|
|
1246
1684
|
const renderHrVersion = (meta) => {
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
if (meta.version) parts.push(`${L.headroomVersionBin} ${meta.version}`);
|
|
1250
|
-
hrVersionLine.textContent = parts.length ? parts.join(' · ') : `${L.headroomVersionPlugin} —`;
|
|
1685
|
+
hrVersionLine.textContent = `插件 ${meta.pluginVersion || '—'}${meta.version ? ` · headroom ${meta.version}` : ''}`;
|
|
1686
|
+
hrLocText = meta.binPath || '';
|
|
1251
1687
|
};
|
|
1252
1688
|
renderHrVersion(headroomRes);
|
|
1253
1689
|
const hrUp = makeUpgradeControls({
|
|
1254
1690
|
endpoint: '/headroom',
|
|
1691
|
+
idleText: () => hrLocText,
|
|
1255
1692
|
metaRefresh: async () => {
|
|
1256
1693
|
const meta = await rpc('/headroom');
|
|
1257
1694
|
if (meta.ok) {
|
|
@@ -1278,7 +1715,7 @@ export function openSettingsDialog() {
|
|
|
1278
1715
|
installBtn.disabled = true;
|
|
1279
1716
|
installBtn.textContent = L.headroomInstalling;
|
|
1280
1717
|
installState.textContent = '';
|
|
1281
|
-
const res = await rpc('/plugins/update', { method: 'POST', body: { name: 'headroom'
|
|
1718
|
+
const res = await rpc('/plugins/update', { method: 'POST', body: { name: 'headroom' } });
|
|
1282
1719
|
if (!res.ok) {
|
|
1283
1720
|
installBtn.disabled = false;
|
|
1284
1721
|
installBtn.textContent = L.headroomInstall;
|
|
@@ -1289,6 +1726,8 @@ export function openSettingsDialog() {
|
|
|
1289
1726
|
if (meta.ok) Object.assign(headroomRes, meta);
|
|
1290
1727
|
if (headroomRes.hook) {
|
|
1291
1728
|
showToast(L.headroomInstalled, 'success');
|
|
1729
|
+
// 版本行 span 在弹窗打开时按未装态算过一次,装完必须重算
|
|
1730
|
+
renderHrVersion(headroomRes);
|
|
1292
1731
|
renderHeadroomPane();
|
|
1293
1732
|
} else {
|
|
1294
1733
|
installBtn.disabled = false;
|
|
@@ -1312,16 +1751,65 @@ export function openSettingsDialog() {
|
|
|
1312
1751
|
const hrRun = async (reqBody, warn) => {
|
|
1313
1752
|
if (hrBusy) return false;
|
|
1314
1753
|
hrBusy = true;
|
|
1315
|
-
const
|
|
1316
|
-
|
|
1754
|
+
const action = reqBody && reqBody.action;
|
|
1755
|
+
const isStart = action === 'start' || action === 'restart';
|
|
1756
|
+
const isStop = action === 'stop';
|
|
1757
|
+
// 生命周期按钮:点击瞬间就显示等待提示 + 计时(不等 RPC 返回——
|
|
1758
|
+
// RPC 里 helper 要杀/拉进程,期间 3-10 秒界面不能毫无反应)
|
|
1759
|
+
// RPC 先发出(不等返回——期间界面要显示等待反馈)
|
|
1760
|
+
const rpcDone = rpc('/headroom', { method: 'POST', body: reqBody });
|
|
1761
|
+
let timerIv = null;
|
|
1762
|
+
if (isStart || isStop) {
|
|
1763
|
+
const wantUp = isStart;
|
|
1764
|
+
const msg = action === 'restart' ? L.hrRestarting : isStart ? L.hrStarting : L.hrStopping;
|
|
1765
|
+
const doneMsg = isStart ? L.hrStarted : L.hrStopped;
|
|
1766
|
+
const timeoutMs = isStart ? 90 * 1000 : 30 * 1000;
|
|
1767
|
+
const t0 = Date.now();
|
|
1768
|
+
const timerSpan = h('span', { class: 'text-ui-sm text-foreground-subtle/70' }, msg + ' (0s)');
|
|
1769
|
+
hrStatusLine.replaceChildren(timerSpan);
|
|
1770
|
+
timerIv = setInterval(() => {
|
|
1771
|
+
timerSpan.textContent = msg + ` (${Math.round((Date.now() - t0) / 1000)}s)`;
|
|
1772
|
+
}, 1000);
|
|
1773
|
+
// RPC 完成后进入轮询(RPC 执行实际的 stop/start,可能要 3-10 秒)。
|
|
1774
|
+
// 轮询出结果前 hrBusy 一直占着:期间再点开关或按钮直接忽略,否则
|
|
1775
|
+
// 每次点击都会重发 start、把计时和启动流程从头来过
|
|
1776
|
+
void (async () => {
|
|
1777
|
+
const kick = await rpcDone;
|
|
1778
|
+
if (!kick.ok) return; // 失败由主流程提示并复位
|
|
1779
|
+
while (Date.now() - t0 < timeoutMs) {
|
|
1780
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
1781
|
+
const st = await rpc('/headroom/status');
|
|
1782
|
+
if (!st.ok) continue;
|
|
1783
|
+
const up = !!(st.status && st.status.up);
|
|
1784
|
+
if (up === wantUp) {
|
|
1785
|
+
clearInterval(timerIv);
|
|
1786
|
+
hrBusy = false;
|
|
1787
|
+
await hrRefresh();
|
|
1788
|
+
showToast(doneMsg, isStart ? 'success' : undefined);
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
clearInterval(timerIv);
|
|
1793
|
+
hrBusy = false;
|
|
1794
|
+
if (isStart) showToast(L.hrStartTimeout, 'error');
|
|
1795
|
+
void hrRefresh();
|
|
1796
|
+
})();
|
|
1797
|
+
}
|
|
1798
|
+
const res = await rpcDone;
|
|
1317
1799
|
if (!res.ok) {
|
|
1800
|
+
if (timerIv) clearInterval(timerIv);
|
|
1801
|
+
hrBusy = false;
|
|
1318
1802
|
showToast(L.failed + ': ' + errText(res), 'error');
|
|
1803
|
+
void hrRefresh();
|
|
1319
1804
|
return false;
|
|
1320
1805
|
}
|
|
1321
1806
|
if (res.config) Object.assign(hrCfg, res.config);
|
|
1322
1807
|
if (warn) showToast(warn);
|
|
1323
|
-
// backend/power
|
|
1324
|
-
|
|
1808
|
+
// 非生命周期动作(backend/power):稍等后刷一次
|
|
1809
|
+
if (!isStart && !isStop) {
|
|
1810
|
+
hrBusy = false;
|
|
1811
|
+
setTimeout(() => { void hrRefresh(); }, 1200);
|
|
1812
|
+
}
|
|
1325
1813
|
return true;
|
|
1326
1814
|
};
|
|
1327
1815
|
// 状态值本地化:钩子输出的是 auto/cpu/ac/ok 等内部值,面板显示成人话
|
|
@@ -1360,6 +1848,23 @@ export function openSettingsDialog() {
|
|
|
1360
1848
|
hrHintLine.textContent = s.saverHint || '';
|
|
1361
1849
|
hrHintLine.style.display = s.saverHint ? '' : 'none';
|
|
1362
1850
|
};
|
|
1851
|
+
// 启用开关不走 hrRun:与代理进程无关,点击即时生效,也不受生命周期等待锁影响
|
|
1852
|
+
const renderHrUsageSwitch = () => {
|
|
1853
|
+
const usageOn = !!(headroomRes.usage && headroomRes.usage.enabled);
|
|
1854
|
+
hrSwitchRow.replaceChildren(
|
|
1855
|
+
settingRow(L.pluginEnable, '', usageOn, async () => {
|
|
1856
|
+
const next = !usageOn;
|
|
1857
|
+
const res = await rpc('/headroom', { method: 'POST', body: { usage: next } });
|
|
1858
|
+
if (!res.ok) {
|
|
1859
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
if (res.usage) headroomRes.usage = res.usage;
|
|
1863
|
+
renderHrUsageSwitch();
|
|
1864
|
+
if (next && !res.changed && !(res.usage && res.usage.enabled)) showToast(L.hrUsageNone);
|
|
1865
|
+
}));
|
|
1866
|
+
};
|
|
1867
|
+
renderHrUsageSwitch();
|
|
1363
1868
|
const hrRefreshBtn = btnSmall(L.headroomRefresh, () => { void hrRefresh(); });
|
|
1364
1869
|
const hrStartBtn = btnSmall(L.headroomStart, () => { void hrRun({ action: 'start' }); });
|
|
1365
1870
|
const hrRestartBtn = btnSmall(L.headroomRestart, () => { void hrRun({ action: 'restart' }); });
|
|
@@ -1453,14 +1958,60 @@ export function openSettingsDialog() {
|
|
|
1453
1958
|
};
|
|
1454
1959
|
updateHrPwrHint();
|
|
1455
1960
|
|
|
1961
|
+
// 本体未装:一键安装(pip 全新安装通道,helper 同一条升级任务管线);
|
|
1962
|
+
// 装完刷新 meta 并整面重渲染(版本行、本体状态、代理控制全部就位)
|
|
1963
|
+
const hrInstallBinBtn = (() => {
|
|
1964
|
+
const b = btnSmall(L.hrInstallBin, () => { void run(); }, 'shrink-0');
|
|
1965
|
+
let busy = false;
|
|
1966
|
+
const run = async () => {
|
|
1967
|
+
if (busy) return;
|
|
1968
|
+
busy = true;
|
|
1969
|
+
b.disabled = true;
|
|
1970
|
+
b.textContent = L.headroomInstalling;
|
|
1971
|
+
const res = await rpc('/headroom', { method: 'POST', body: { upgrade: true } });
|
|
1972
|
+
if (!res.ok) {
|
|
1973
|
+
b.disabled = false;
|
|
1974
|
+
b.textContent = L.hrInstallBin;
|
|
1975
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
1976
|
+
busy = false;
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
// pip 下载大包可持续数分钟,轮询后台任务到结束(超时 resolve,避免按钮卡死)
|
|
1980
|
+
const done = await new Promise((resolve) => {
|
|
1981
|
+
const iv = setInterval(async () => {
|
|
1982
|
+
const s = await rpc('/headroom/upgrade');
|
|
1983
|
+
if (s.ok && s.upgrade && !s.upgrade.running) { clearInterval(iv); resolve(s.upgrade); }
|
|
1984
|
+
}, 2000);
|
|
1985
|
+
setTimeout(() => { clearInterval(iv); resolve(null); }, 12 * 60 * 1000);
|
|
1986
|
+
});
|
|
1987
|
+
busy = false;
|
|
1988
|
+
if (done && !done.error && done.version) {
|
|
1989
|
+
const meta = await rpc('/headroom');
|
|
1990
|
+
if (meta.ok) {
|
|
1991
|
+
renderHrVersion(meta);
|
|
1992
|
+
Object.assign(headroomRes, meta);
|
|
1993
|
+
}
|
|
1994
|
+
showToast(L.hrBinInstalled, 'success');
|
|
1995
|
+
renderHeadroomPane();
|
|
1996
|
+
} else {
|
|
1997
|
+
b.disabled = false;
|
|
1998
|
+
b.textContent = L.hrInstallBin;
|
|
1999
|
+
showToast((done && done.error) || L.headroomInstallRetry, 'error');
|
|
2000
|
+
}
|
|
2001
|
+
};
|
|
2002
|
+
return b;
|
|
2003
|
+
})();
|
|
2004
|
+
|
|
1456
2005
|
paneHeadroom.append(
|
|
1457
2006
|
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.headroomDesc),
|
|
1458
2007
|
...(headroomRes.installed === false
|
|
1459
|
-
? [h('
|
|
2008
|
+
? [h('div', { class: 'mt-2 flex items-center gap-2' },
|
|
2009
|
+
h('p', { class: 'min-w-0 flex-1 text-ui-sm text-amber-500' }, L.headroomBinMissing),
|
|
2010
|
+
hrInstallBinBtn)]
|
|
1460
2011
|
: []),
|
|
1461
|
-
h('div', { class: 'mt-3 flex items-center
|
|
1462
|
-
|
|
1463
|
-
|
|
2012
|
+
h('div', { class: 'mt-3 flex flex-wrap items-center gap-2 rounded-xl border border-border p-2' },
|
|
2013
|
+
hrVersionLine, hrUp.state, hrUp.btn,
|
|
2014
|
+
h('div', { class: 'ml-auto shrink-0' }, hrSwitchRow)),
|
|
1464
2015
|
h('div', { class: 'mt-2 rounded-xl border border-border p-2' },
|
|
1465
2016
|
h('div', { class: 'flex flex-wrap items-center justify-between gap-2' },
|
|
1466
2017
|
h('span', { class: 'text-ui-sm font-medium text-foreground' }, L.headroomStatusTitle),
|
|
@@ -1485,18 +2036,170 @@ export function openSettingsDialog() {
|
|
|
1485
2036
|
};
|
|
1486
2037
|
renderHeadroomPane();
|
|
1487
2038
|
|
|
2039
|
+
// “任务名提示词”(位于“行为”标签页的二级标签):新会话开始作答时 →
|
|
2040
|
+
// 按自定义供应商/提示词生成标题并按手动重命名写库。配置读写走 /title-gen
|
|
2041
|
+
//(存 zcodepro.json);供应商下拉数据走 /title-gen/providers(与视觉代理
|
|
2042
|
+
// 同源,但只列 helper 能直连调用的条目)。
|
|
2043
|
+
// 结构性改动(开关/供应商/协议)即时保存,提示词与文本输入防抖保存。
|
|
2044
|
+
let titleGenCfg = titleGenRes.ok && titleGenRes.titleGen && typeof titleGenRes.titleGen === 'object'
|
|
2045
|
+
? structuredClone(titleGenRes.titleGen) : null;
|
|
2046
|
+
const titleGenDefaultPrompt = titleGenRes.ok && typeof titleGenRes.defaultPrompt === 'string'
|
|
2047
|
+
? titleGenRes.defaultPrompt : '';
|
|
2048
|
+
let titleGenSaveTimer = null;
|
|
2049
|
+
const persistTitleGen = async () => {
|
|
2050
|
+
const res = await rpc('/title-gen', { method: 'POST', body: { titleGen: titleGenCfg } });
|
|
2051
|
+
clearConfigCache();
|
|
2052
|
+
if (!res.ok) showToast(L.failed + ': ' + errText(res), 'error');
|
|
2053
|
+
return res.ok;
|
|
2054
|
+
};
|
|
2055
|
+
const saveTitleGenSoon = () => {
|
|
2056
|
+
clearTimeout(titleGenSaveTimer);
|
|
2057
|
+
titleGenSaveTimer = setTimeout(() => { void persistTitleGen(); }, 500);
|
|
2058
|
+
};
|
|
2059
|
+
const renderTitleGenPane = () => {
|
|
2060
|
+
if (!titleGenCfg) {
|
|
2061
|
+
paneTitleGen.replaceChildren(
|
|
2062
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.titleGenShortDesc),
|
|
2063
|
+
h('p', { class: 'mt-2 text-ui-sm text-destructive' }, L.visionLoadFailed + ': ' + errText(titleGenRes)),
|
|
2064
|
+
);
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
const inputCls = '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';
|
|
2068
|
+
const field = (labelText, node) => h('label', { class: 'block min-w-0' },
|
|
2069
|
+
h('span', { class: 'mb-1 block text-ui-xs font-medium text-foreground-subtle' }, labelText), node);
|
|
2070
|
+
// 供应商 / 模型 / 地址密钥共用 provider-fields 的规则(详见该模块说明)。
|
|
2071
|
+
// 换供应商后重渲染:模型下拉换成新供应商的模型、地址字段按模式显隐;
|
|
2072
|
+
// 手动输入过的模型保留(不在新列表里时显示为「手动输入」项,是否改选
|
|
2073
|
+
// 由用户决定)
|
|
2074
|
+
const showInlineConn = isManualConn(titleGenCfg.provider, titleGenProviders);
|
|
2075
|
+
const providerCombo = comboField(
|
|
2076
|
+
titleGenCfg.provider || '',
|
|
2077
|
+
providerOptionsOf(titleGenProviders, L),
|
|
2078
|
+
(v, src) => {
|
|
2079
|
+
titleGenCfg.provider = v;
|
|
2080
|
+
if (src === 'select') { void persistTitleGen(); renderTitleGenPane(); }
|
|
2081
|
+
else saveTitleGenSoon();
|
|
2082
|
+
},
|
|
2083
|
+
{ placeholder: L.titleGenProviderSession, staleLabel: L.titleGenProviderDeleted });
|
|
2084
|
+
const modelCombo = comboField(
|
|
2085
|
+
titleGenCfg.model || '',
|
|
2086
|
+
// 跟随会话时选项表头部有“跟随当前会话当前模型”(值为空,生成时用
|
|
2087
|
+
// 会话当前模型),选了具体模型后也能再切回它
|
|
2088
|
+
modelOptionsOf(titleGenCfg.provider, titleGenProviders, {
|
|
2089
|
+
followSession: true,
|
|
2090
|
+
followSessionLabel: L.titleGenModelFollowSession,
|
|
2091
|
+
}),
|
|
2092
|
+
(v) => { titleGenCfg.model = v; saveTitleGenSoon(); },
|
|
2093
|
+
{
|
|
2094
|
+
placeholder: modelPlaceholderOf(titleGenCfg.provider, L),
|
|
2095
|
+
emptyLabel: (titleGenCfg.provider || '').trim() === 'session'
|
|
2096
|
+
? L.titleGenModelFollowSession : L.titleGenModelEmpty,
|
|
2097
|
+
});
|
|
2098
|
+
// 接口地址 / API Key / 协议(显隐见上方 showInlineConn):不跟随时由
|
|
2099
|
+
// 用户填写;供应商被删时展示保存的副本,仍可直连或改填
|
|
2100
|
+
const textIn = (value, onInput, { type = 'text', placeholder = '' } = {}) => {
|
|
2101
|
+
const n = h('input', { type, value: value || '', placeholder, class: inputCls });
|
|
2102
|
+
n.addEventListener('input', () => onInput(n.value));
|
|
2103
|
+
return n;
|
|
2104
|
+
};
|
|
2105
|
+
const formatSel = h('select', { class: inputCls },
|
|
2106
|
+
h('option', { value: 'anthropic' }, 'anthropic'),
|
|
2107
|
+
h('option', { value: 'openai' }, 'openai'));
|
|
2108
|
+
formatSel.value = titleGenCfg.format === 'openai' ? 'openai' : 'anthropic';
|
|
2109
|
+
formatSel.addEventListener('change', () => {
|
|
2110
|
+
titleGenCfg.format = formatSel.value;
|
|
2111
|
+
void persistTitleGen();
|
|
2112
|
+
});
|
|
2113
|
+
const inlineConnCard = h('div', {
|
|
2114
|
+
class: 'mt-2 grid grid-cols-1 gap-2 rounded-xl border border-border p-2 sm:grid-cols-3',
|
|
2115
|
+
style: showInlineConn ? '' : 'display:none',
|
|
2116
|
+
},
|
|
2117
|
+
field(L.titleGenBaseUrl, textIn(titleGenCfg.baseUrl, (v) => { titleGenCfg.baseUrl = v; saveTitleGenSoon(); }, { placeholder: 'https://…' })),
|
|
2118
|
+
field(L.titleGenApiKey, textIn(titleGenCfg.apiKey, (v) => { titleGenCfg.apiKey = v; saveTitleGenSoon(); }, { type: 'password' })),
|
|
2119
|
+
field(L.titleGenFormat, formatSel));
|
|
2120
|
+
// 提示词:留空 = 内置默认;「查看默认提示词」把默认值载入编辑框供修改
|
|
2121
|
+
const promptArea = h('textarea', {
|
|
2122
|
+
class: 'zcodepro-textarea zcodepro-textarea-sm', rows: '8',
|
|
2123
|
+
placeholder: L.titleGenPromptPlaceholder,
|
|
2124
|
+
});
|
|
2125
|
+
promptArea.value = titleGenCfg.prompt || '';
|
|
2126
|
+
promptArea.addEventListener('input', () => { titleGenCfg.prompt = promptArea.value; saveTitleGenSoon(); });
|
|
2127
|
+
// 测试:用示例首条消息实际生成一次(不写库),验证供应商/模型/提示词
|
|
2128
|
+
// 连通;示例文本随界面语言取中英文版
|
|
2129
|
+
const sampleText = isZhLocale()
|
|
2130
|
+
? '帮我看下这个报错是什么原因:NullPointerException at com.example.OrderService.calculate(OrderService.java:87)'
|
|
2131
|
+
: 'Why does my Spring Boot app throw NullPointerException intermittently in OrderService.calculate?';
|
|
2132
|
+
const sampleInput = h('input', {
|
|
2133
|
+
type: 'text', class: inputCls + ' min-w-0 flex-1',
|
|
2134
|
+
value: sampleText,
|
|
2135
|
+
});
|
|
2136
|
+
let testBtn;
|
|
2137
|
+
const testState = h('span', { class: 'min-w-0 flex-1 truncate text-ui-xs/relaxed text-foreground-subtle' });
|
|
2138
|
+
testBtn = btnSmall(L.titleGenTest, async () => {
|
|
2139
|
+
testBtn.disabled = true;
|
|
2140
|
+
testBtn.textContent = L.titleGenTesting;
|
|
2141
|
+
testState.textContent = L.titleGenTesting;
|
|
2142
|
+
const res = await rpc('/title-gen/run', {
|
|
2143
|
+
method: 'POST',
|
|
2144
|
+
body: { text: sampleInput.value, test: true },
|
|
2145
|
+
});
|
|
2146
|
+
testBtn.disabled = false;
|
|
2147
|
+
testBtn.textContent = L.titleGenTest;
|
|
2148
|
+
if (res && res.ok) {
|
|
2149
|
+
testState.textContent = L.titleGenTestOk + res.title;
|
|
2150
|
+
showToast(res.title, 'success');
|
|
2151
|
+
} else {
|
|
2152
|
+
testState.textContent = (res && errText(res)) || L.titleGenTestFailed;
|
|
2153
|
+
testState.className = 'min-w-0 flex-1 truncate text-ui-xs/relaxed text-destructive';
|
|
2154
|
+
showToast(L.titleGenTestFailed + ': ' + (res ? errText(res) : ''), 'error');
|
|
2155
|
+
}
|
|
2156
|
+
}, 'shrink-0');
|
|
2157
|
+
paneTitleGen.replaceChildren(
|
|
2158
|
+
// 信息卡:一句话简介在左,启用开关右对齐
|
|
2159
|
+
h('div', { class: 'mt-3 flex items-center justify-between gap-2 rounded-xl border border-border p-2' },
|
|
2160
|
+
h('span', { class: 'min-w-0 text-ui-sm/relaxed text-foreground-subtle' }, L.titleGenShortDesc),
|
|
2161
|
+
h('div', { class: 'shrink-0' }, settingRow(L.titleGenEnabled, '', titleGenCfg.enabled === true, async () => {
|
|
2162
|
+
titleGenCfg.enabled = !(titleGenCfg.enabled === true);
|
|
2163
|
+
await persistTitleGen();
|
|
2164
|
+
renderTitleGenPane();
|
|
2165
|
+
}))),
|
|
2166
|
+
// 供应商 + 模型
|
|
2167
|
+
h('div', { class: 'mt-2 grid grid-cols-1 gap-2 rounded-xl border border-border p-2 sm:grid-cols-2' },
|
|
2168
|
+
field(L.titleGenProvider, providerCombo),
|
|
2169
|
+
field(L.titleGenModel, modelCombo)),
|
|
2170
|
+
inlineConnCard,
|
|
2171
|
+
// 提示词 + 载入默认
|
|
2172
|
+
h('div', { class: 'mt-2 rounded-xl border border-border p-2' },
|
|
2173
|
+
h('div', { class: 'mb-1 flex items-center justify-between gap-2' },
|
|
2174
|
+
h('span', { class: 'text-ui-xs font-medium text-foreground-subtle' }, L.titleGenPrompt),
|
|
2175
|
+
btnSmall(L.titleGenPromptReset, () => {
|
|
2176
|
+
promptArea.value = titleGenDefaultPrompt;
|
|
2177
|
+
titleGenCfg.prompt = titleGenDefaultPrompt;
|
|
2178
|
+
void persistTitleGen();
|
|
2179
|
+
promptArea.focus();
|
|
2180
|
+
})),
|
|
2181
|
+
promptArea),
|
|
2182
|
+
// 测试
|
|
2183
|
+
h('div', { class: 'mt-2 flex items-center gap-2 rounded-xl border border-border p-2' },
|
|
2184
|
+
sampleInput, testBtn),
|
|
2185
|
+
testState,
|
|
2186
|
+
);
|
|
2187
|
+
};
|
|
2188
|
+
renderTitleGenPane();
|
|
2189
|
+
|
|
1488
2190
|
body.append(
|
|
1489
2191
|
statusLine,
|
|
1490
2192
|
tablist,
|
|
1491
2193
|
paneFeatures,
|
|
2194
|
+
paneAgents,
|
|
1492
2195
|
paneStyles,
|
|
1493
2196
|
panePerf,
|
|
1494
|
-
paneAgents,
|
|
1495
2197
|
paneProxy,
|
|
1496
2198
|
paneSync,
|
|
1497
2199
|
paneVision,
|
|
1498
2200
|
paneHeadroom,
|
|
1499
2201
|
paneRtk,
|
|
2202
|
+
paneDebug,
|
|
1500
2203
|
);
|
|
1501
2204
|
},
|
|
1502
2205
|
});
|