zcode-pro 0.17.1 → 0.18.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 +6 -3
- package/README.md +6 -3
- package/dist/inject.js +795 -105
- package/package.json +1 -1
- package/src/host/helper.mjs +794 -125
- package/src/host/main.mjs +44 -3
- package/src/host/paths.mjs +32 -10
- package/src/host/pickFolder.mjs +48 -3
- package/src/host/setup.mjs +35 -10
- package/src/host/webdav.mjs +128 -12
- package/src/inject/core.js +140 -32
- package/src/inject/features/plugin-updates.js +1 -1
- package/src/inject/features/settings-dialog.js +597 -70
- package/src/inject/features/styles.js +15 -1
- package/src/inject/index.js +12 -2
- package/src/inject/ui.js +3 -2
|
@@ -15,12 +15,18 @@ function openExternal(url) {
|
|
|
15
15
|
// 检查更新 → 升级(helper 后台任务,轮询进度,输出取最后一行、超长省略中间)→
|
|
16
16
|
// 进行中可停止;关弹窗不中断升级,重开恢复显示。endpoint 为 /headroom 或 /rtk,
|
|
17
17
|
// metaRefresh 在升级完成后刷新各面板自己的版本行与配置。
|
|
18
|
-
function makeUpgradeControls({ endpoint, metaRefresh }) {
|
|
18
|
+
function makeUpgradeControls({ endpoint, metaRefresh, idleText }) {
|
|
19
19
|
const L = t();
|
|
20
20
|
const state = h('span', { class: 'min-w-0 flex-1 truncate text-left text-ui-xs/relaxed text-foreground-subtle' });
|
|
21
|
-
// 失败原因在状态行直显红字(toast 只有 5
|
|
21
|
+
// 失败原因在状态行直显红字(toast 只有 5 秒且易被弹窗遮挡)。
|
|
22
|
+
// idleText:空闲(无检查/升级活动)时该槽位显示调用方给的文本——rtk/headroom
|
|
23
|
+
// 面板用它显示本体位置;一旦有活动(检查中/下载进度/结果),活动文本顶替位置
|
|
24
|
+
const hasIdle = () => typeof idleText === 'function';
|
|
22
25
|
const setState = (text, kind = '') => {
|
|
23
|
-
|
|
26
|
+
const shown = text || (hasIdle() ? (idleText() || '') : '');
|
|
27
|
+
// 「 · 」前缀:与前面的版本行无缝拼成「插件 x · rtk y · 状态/路径」一行;
|
|
28
|
+
// 错误信息不加前缀——它是独立提示,拼在版本行后会读成版本的一部分
|
|
29
|
+
state.textContent = shown ? (kind === 'error' ? shown : ` · ${shown}`) : '';
|
|
24
30
|
state.className = 'min-w-0 flex-1 truncate text-left text-ui-xs/relaxed '
|
|
25
31
|
+ (kind === 'error' ? 'text-destructive' : 'text-foreground-subtle');
|
|
26
32
|
};
|
|
@@ -129,6 +135,8 @@ function makeUpgradeControls({ endpoint, metaRefresh }) {
|
|
|
129
135
|
const res = await rpc(endpoint + '/upgrade');
|
|
130
136
|
if (res.ok && res.upgrade && res.upgrade.running) pollStart();
|
|
131
137
|
})();
|
|
138
|
+
// 初始化为空闲态(有 idleText 时即显示位置等空闲文本)
|
|
139
|
+
setState('');
|
|
132
140
|
return { btn, state, stop: stopPoll };
|
|
133
141
|
}
|
|
134
142
|
|
|
@@ -323,7 +331,7 @@ export function openSettingsDialog() {
|
|
|
323
331
|
const runPluginsUpdate = async () => {
|
|
324
332
|
pluginsBtn.disabled = true;
|
|
325
333
|
pluginsStatusLine.textContent = L.pluginsUpdating;
|
|
326
|
-
const res = await rpc('/plugins/update', { method: 'POST', body: {
|
|
334
|
+
const res = await rpc('/plugins/update', { method: 'POST', body: {} });
|
|
327
335
|
pluginsBtn.disabled = false;
|
|
328
336
|
if (!res.ok) {
|
|
329
337
|
pluginsStatusLine.textContent = '';
|
|
@@ -376,7 +384,8 @@ export function openSettingsDialog() {
|
|
|
376
384
|
const paneVision = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
377
385
|
const paneRtk = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
378
386
|
const paneHeadroom = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
379
|
-
const
|
|
387
|
+
const paneDebug = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
388
|
+
const panes = { features: paneFeatures, styles: paneStyles, perf: panePerf, agents: paneAgents, proxy: paneProxy, sync: paneSync, vision: paneVision, headroom: paneHeadroom, rtk: paneRtk, debug: paneDebug };
|
|
380
389
|
const tabDefs = [
|
|
381
390
|
['features', L.tabFeatures],
|
|
382
391
|
['styles', L.tabStyles],
|
|
@@ -387,6 +396,9 @@ export function openSettingsDialog() {
|
|
|
387
396
|
['vision', L.tabVision],
|
|
388
397
|
['headroom', L.tabHeadroom],
|
|
389
398
|
['rtk', L.tabRtk],
|
|
399
|
+
// “调试”标签页只对开发模式生效的用户显示(配置开关或 ZCODEPRO_DEV=1),
|
|
400
|
+
// 普通用户不可见
|
|
401
|
+
...(config.devMode === true || config.devActive === true ? [['debug', L.tabDebug]] : []),
|
|
390
402
|
];
|
|
391
403
|
const tablist = h('div', { role: 'tablist', 'aria-orientation': 'horizontal', class: 'zcodepro-tablist mt-4' });
|
|
392
404
|
const renderTabs = () => tablist.replaceChildren(...tabDefs.map(([id, label]) =>
|
|
@@ -483,6 +495,7 @@ export function openSettingsDialog() {
|
|
|
483
495
|
styleCell(L.rowGapName, L.rowGapDesc, 'rowGap'),
|
|
484
496
|
styleCell(L.userLineHeightName, L.userLineHeightDesc, 'userLineHeight', { min: 1, max: 3, step: 0.05, unit: 'x' }),
|
|
485
497
|
styleCell(L.lineHeightName, L.lineHeightDesc, 'lineHeight', { min: 1, max: 3, step: 0.05, unit: 'x' }),
|
|
498
|
+
styleCell(L.thinkingLineHeightName, L.thinkingLineHeightDesc, 'thinkingLineHeight', { min: 1, max: 3, step: 0.05, unit: 'x' }),
|
|
486
499
|
styleCell(L.codeLineHeightName, L.codeLineHeightDesc, 'codeLineHeight', { min: 1, max: 3, step: 0.05, unit: 'x' }),
|
|
487
500
|
styleCell(L.listSpacingName, L.listSpacingDesc, 'listSpacing'),
|
|
488
501
|
styleCell(L.listItemSpacingName, L.listItemSpacingDesc, 'listItemSpacing'),
|
|
@@ -635,6 +648,12 @@ export function openSettingsDialog() {
|
|
|
635
648
|
const proxyTestBtn = btnSmall(L.proxyTest, () => { void runProxyTest(); }, 'shrink-0');
|
|
636
649
|
const runProxyTest = async () => {
|
|
637
650
|
if (proxyTesting) return;
|
|
651
|
+
// 空地址不发请求(等网络超时才报「检测失败」的体验太差),立即提示
|
|
652
|
+
if (!proxyInput.value.trim()) {
|
|
653
|
+
proxyTestState.className = 'mt-2 text-ui-xs/relaxed text-amber-500';
|
|
654
|
+
proxyTestState.textContent = L.proxyTestEmpty;
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
638
657
|
proxyTesting = true;
|
|
639
658
|
proxyTestBtn.disabled = true;
|
|
640
659
|
proxyTestBtn.textContent = L.proxyTesting;
|
|
@@ -655,6 +674,39 @@ export function openSettingsDialog() {
|
|
|
655
674
|
proxyTestState.className = 'mt-2 text-ui-xs/relaxed ' + (allOk ? 'text-foreground-subtle' : 'text-amber-500');
|
|
656
675
|
proxyTestState.textContent = `${res.proxy}:${parts.join(' · ')}`;
|
|
657
676
|
};
|
|
677
|
+
// 代理生效范围(即时保存,每个开关只改自己那项):插件市场更新与插件依赖
|
|
678
|
+
// 软件默认开,同步默认关——国内网盘直连即可,代理停了同步会跟着失败
|
|
679
|
+
const persistProxyScope = async (partial) => {
|
|
680
|
+
const res = await rpc('/config', { method: 'POST', body: { proxyScopes: partial } });
|
|
681
|
+
if (!res.ok) {
|
|
682
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
if (res.config && res.config.proxyScopes) config.proxyScopes = res.config.proxyScopes;
|
|
686
|
+
return true;
|
|
687
|
+
};
|
|
688
|
+
const proxyScopeWrap = h('div');
|
|
689
|
+
const renderProxyScopes = () => {
|
|
690
|
+
const sc = config.proxyScopes || {};
|
|
691
|
+
proxyScopeWrap.replaceChildren(
|
|
692
|
+
settingRow(L.proxyScopePlugins, L.proxyScopePluginsDesc, sc.plugins !== false, async () => {
|
|
693
|
+
const next = !(sc.plugins !== false);
|
|
694
|
+
if (await persistProxyScope({ plugins: next })) sc.plugins = next;
|
|
695
|
+
renderProxyScopes();
|
|
696
|
+
}),
|
|
697
|
+
settingRow(L.proxyScopeDeps, L.proxyScopeDepsDesc, sc.deps !== false, async () => {
|
|
698
|
+
const next = !(sc.deps !== false);
|
|
699
|
+
if (await persistProxyScope({ deps: next })) sc.deps = next;
|
|
700
|
+
renderProxyScopes();
|
|
701
|
+
}),
|
|
702
|
+
settingRow(L.proxyScopeSync, L.proxyScopeSyncDesc, sc.sync === true, async () => {
|
|
703
|
+
const next = !(sc.sync === true);
|
|
704
|
+
if (await persistProxyScope({ sync: next })) sc.sync = next;
|
|
705
|
+
renderProxyScopes();
|
|
706
|
+
}),
|
|
707
|
+
);
|
|
708
|
+
};
|
|
709
|
+
renderProxyScopes();
|
|
658
710
|
paneProxy.append(
|
|
659
711
|
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.proxyDesc),
|
|
660
712
|
h('div', { class: 'mt-3 flex items-center gap-2' },
|
|
@@ -663,6 +715,7 @@ export function openSettingsDialog() {
|
|
|
663
715
|
proxyTestBtn),
|
|
664
716
|
h('p', { class: 'mt-2 text-ui-xs/relaxed text-foreground-subtle' }, L.proxyHint),
|
|
665
717
|
proxyTestState,
|
|
718
|
+
h('div', { class: 'mt-3 divide-y divide-border rounded-xl border border-border' }, proxyScopeWrap),
|
|
666
719
|
);
|
|
667
720
|
|
|
668
721
|
// “性能”:空闲项目内存回收(开关 + 两个时长)。idleReclaimStartupMinutes
|
|
@@ -712,8 +765,10 @@ export function openSettingsDialog() {
|
|
|
712
765
|
|
|
713
766
|
// “同步”:把 ZCode Pro 设置与模型设置手动备份到 WebDAV / 从 WebDAV 恢复
|
|
714
767
|
// (交互参考 Tampermonkey:URL/登录/密码 + 手动触发,无自动同步)。仅 WebDAV
|
|
715
|
-
//
|
|
716
|
-
//
|
|
768
|
+
// 一种后端且本就只在点击时传输,不设“类型/禁用”下拉。表单显式保存(旁有
|
|
769
|
+
// 「测试」用当前表单值验证连通);同步内容开关即时保存;下载先弹云端版本
|
|
770
|
+
// 列表(固定名 = 最新 + 带时间戳的历史),任选一个导入,导入会覆盖本机、
|
|
771
|
+
// 先弹二次确认
|
|
717
772
|
const savedSync = (config.sync && typeof config.sync === 'object') ? config.sync
|
|
718
773
|
: { url: '', dir: 'zcode-pro', login: '', password: '', include: { zcodepro: true, model: true }, lastPushAt: null, lastPullAt: null };
|
|
719
774
|
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 +816,44 @@ export function openSettingsDialog() {
|
|
|
761
816
|
syncCheckDirty();
|
|
762
817
|
}
|
|
763
818
|
};
|
|
819
|
+
// 「测试」:用表单当前值(先测再存)验证 WebDAV:连接与凭据 → 上传目录
|
|
820
|
+
// (缺则自动建,等同上传时的行为)→ 文件列表;结果仿“代理”标签页的检测,
|
|
821
|
+
// 一行显示各步耗时与云端备份文件数
|
|
822
|
+
const syncTestState = h('p', { class: 'mt-2 text-ui-xs/relaxed text-foreground-subtle' });
|
|
823
|
+
let syncTesting = false;
|
|
824
|
+
const syncTestBtn = btnSmall(L.syncTest, () => { void runSyncTest(); }, 'shrink-0');
|
|
825
|
+
const runSyncTest = async () => {
|
|
826
|
+
if (syncTesting) return;
|
|
827
|
+
// 没填 URL 不发请求(等超时才报“测试失败”的体验太差),立即提示
|
|
828
|
+
if (!syncUrlInput.value.trim()) {
|
|
829
|
+
syncTestState.className = 'mt-2 text-ui-xs/relaxed text-amber-500';
|
|
830
|
+
syncTestState.textContent = L.syncTestEmpty;
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
syncTesting = true;
|
|
834
|
+
syncTestBtn.disabled = true;
|
|
835
|
+
syncTestBtn.textContent = L.syncTesting;
|
|
836
|
+
syncTestState.textContent = '';
|
|
837
|
+
syncTestState.className = 'mt-2 text-ui-xs/relaxed text-foreground-subtle';
|
|
838
|
+
const res = await rpc('/sync/test', { method: 'POST', body: { sync: syncFormValues() } });
|
|
839
|
+
syncTesting = false;
|
|
840
|
+
syncTestBtn.disabled = false;
|
|
841
|
+
syncTestBtn.textContent = L.syncTest;
|
|
842
|
+
if (!res.ok) {
|
|
843
|
+
syncTestState.className = 'mt-2 text-ui-xs/relaxed text-destructive';
|
|
844
|
+
syncTestState.textContent = L.syncTestFailed + ': ' + errText(res);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
const stepName = { connect: L.syncTestStepConnect, dir: L.syncTestStepDir, list: L.syncTestStepList };
|
|
848
|
+
const parts = (res.targets || []).map((t) =>
|
|
849
|
+
`${stepName[t.step] || t.step} ${t.ok ? '✓' + (t.ms != null ? ' ' + t.ms + 'ms' : '') : '✗ ' + (t.error || t.httpCode || '')}`.trim());
|
|
850
|
+
const allOk = (res.targets || []).length > 0 && (res.targets || []).every((t) => t.ok);
|
|
851
|
+
let host = syncUrlInput.value.trim();
|
|
852
|
+
try { host = new URL(host).host; } catch { /* 非法输入保留原文 */ }
|
|
853
|
+
syncTestState.className = 'mt-2 text-ui-xs/relaxed ' + (allOk ? 'text-foreground-subtle' : 'text-amber-500');
|
|
854
|
+
syncTestState.textContent = `${host}:${parts.join(' · ')}`
|
|
855
|
+
+ (allOk && res.fileCount != null ? `(${L.syncTestFilesFound.replace('{n}', res.fileCount)})` : '');
|
|
856
|
+
};
|
|
764
857
|
// 坚果云推荐行:外链经宿主 openExternal 打开系统浏览器(无该能力时只出纯文字)
|
|
765
858
|
const JIANGUO_HELP = 'https://help.jianguoyun.com/?p=2064';
|
|
766
859
|
const syncRecLine = h('p', { class: 'mt-1 text-ui-xs/relaxed text-foreground-subtle' });
|
|
@@ -798,61 +891,199 @@ export function openSettingsDialog() {
|
|
|
798
891
|
);
|
|
799
892
|
};
|
|
800
893
|
renderSyncInclude();
|
|
801
|
-
// 上次操作时间 +
|
|
894
|
+
// 上次操作时间 + 上传/下载按钮;上传固定名与历史版本双写,下载弹云端版本列表
|
|
802
895
|
const fmtSyncTime = (ts) => (ts ? new Date(ts).toLocaleString() : L.syncNever);
|
|
803
896
|
const syncStatusLine = h('div', { class: 'text-ui-xs/relaxed text-foreground-subtle' });
|
|
804
897
|
const renderSyncStatus = (s) => {
|
|
805
898
|
syncStatusLine.textContent = `${L.syncLastPush} ${fmtSyncTime(s.lastPushAt)} · ${L.syncLastPull} ${fmtSyncTime(s.lastPullAt)}`;
|
|
806
899
|
};
|
|
807
900
|
renderSyncStatus(savedSync);
|
|
808
|
-
const syncUploadBtn = btnSmall(L.syncUpload, () => { void
|
|
809
|
-
const syncDownloadBtn = btnSmall(L.syncDownload, () => {
|
|
810
|
-
|
|
901
|
+
const syncUploadBtn = btnSmall(L.syncUpload, () => { void runSyncPush(); }, 'shrink-0', 'primary');
|
|
902
|
+
const syncDownloadBtn = btnSmall(L.syncDownload, () => { openSyncVersionPicker(); }, 'shrink-0');
|
|
903
|
+
// 「从云端下载」先弹版本列表(备份管理器的样子):列出云端目录里的备份文件
|
|
904
|
+
// (固定名 = 最新一版 + 带时间戳的历史),每行可导入到本机 / 从云端删除 /
|
|
905
|
+
// 另存到本机磁盘;导入按文件名前缀决定覆盖哪一项,不受“同步内容”勾选影响
|
|
906
|
+
const openSyncVersionPicker = () => {
|
|
811
907
|
openDialog({
|
|
812
|
-
title: L.
|
|
813
|
-
description: L.
|
|
814
|
-
width: '
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
)
|
|
821
|
-
|
|
908
|
+
title: L.syncPickTitle,
|
|
909
|
+
description: L.syncPickDesc,
|
|
910
|
+
width: 'max-w-2xl',
|
|
911
|
+
dismissOnOutside: false,
|
|
912
|
+
onMount: async ({ body: pickBody, content, close: closePicker }) => {
|
|
913
|
+
// 标题栏 ✕(设置弹窗同款);h2 也是拖拽把手,✕ 上要拦住 mousedown 冒泡
|
|
914
|
+
const titleEl = content && content.firstElementChild;
|
|
915
|
+
if (titleEl && titleEl.tagName === 'H2') {
|
|
916
|
+
titleEl.classList.add('flex', 'w-full', 'items-center', 'justify-between');
|
|
917
|
+
titleEl.replaceChildren(
|
|
918
|
+
h('span', { class: 'flex min-w-0 items-center' }, L.syncPickTitle),
|
|
919
|
+
h('button', {
|
|
920
|
+
type: 'button',
|
|
921
|
+
class: 'zcodepro-close',
|
|
922
|
+
title: L.close,
|
|
923
|
+
'aria-label': L.close,
|
|
924
|
+
onMousedown: (e) => { e.stopPropagation(); },
|
|
925
|
+
onClick: () => { closePicker(); },
|
|
926
|
+
}, '✕'),
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
const listWrap = h('div', { class: 'space-y-1' });
|
|
930
|
+
pickBody.append(listWrap);
|
|
931
|
+
const fmtSize = (n) => {
|
|
932
|
+
if (!Number.isFinite(n) || n <= 0) return '';
|
|
933
|
+
return n >= 1048576 ? (n / 1048576).toFixed(2) + ' MB' : Math.max(1, Math.round(n / 1024)) + ' KB';
|
|
934
|
+
};
|
|
935
|
+
const setStatus = (el) => { listWrap.replaceChildren(el); };
|
|
936
|
+
const setRowBusy = (btns, busy) => { for (const b of btns) b.disabled = busy; };
|
|
937
|
+
const versionRow = (f) => {
|
|
938
|
+
// 固定名(zcodepro.json / model.json)不含时间戳段,始终是最新一版;
|
|
939
|
+
// 归属哪项同步内容由分组标题说明,行内不再重复
|
|
940
|
+
const isLatest = !/-\d{4}-\d{2}-\d{2}T/.test(f.name);
|
|
941
|
+
const importBtn = btnSmall(L.syncVerImport, () => { confirmImport(f, rowBtns, importBtn); }, 'shrink-0');
|
|
942
|
+
const deleteBtn = btnSmall(L.syncVerDelete, () => { confirmDelete(f, rowBtns, isLatest); }, 'shrink-0');
|
|
943
|
+
const saveBtn = btnSmall(L.syncVerSave, () => { void exportVersion(f, rowBtns, saveBtn); }, 'shrink-0');
|
|
944
|
+
const rowBtns = [importBtn, deleteBtn, saveBtn];
|
|
945
|
+
return h('div', { class: 'flex items-center gap-3 rounded-xl border border-border px-3 py-2' },
|
|
946
|
+
h('div', { class: 'min-w-0 flex-1' },
|
|
947
|
+
h('div', { class: 'flex min-w-0 items-center gap-1.5' },
|
|
948
|
+
h('span', { class: 'truncate font-mono text-ui-xs text-foreground', title: f.name }, f.name),
|
|
949
|
+
...(isLatest ? [h('span', { class: 'shrink-0 rounded border border-border px-1.5 text-ui-xs text-foreground-subtle' }, L.syncVerLatest)] : [])),
|
|
950
|
+
h('div', { class: 'mt-0.5 text-ui-xs/relaxed text-foreground-subtle' },
|
|
951
|
+
[fmtSize(f.size), f.lastModified ? new Date(f.lastModified).toLocaleString() : ''].filter(Boolean).join(' · '))),
|
|
952
|
+
h('div', { class: 'flex shrink-0 items-center gap-1.5' }, importBtn, deleteBtn, saveBtn));
|
|
953
|
+
};
|
|
954
|
+
const loadList = async () => {
|
|
955
|
+
setStatus(h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.syncPickLoading));
|
|
956
|
+
const res = await rpc('/sync/versions', { method: 'POST', body: {} });
|
|
957
|
+
if (!res.ok) {
|
|
958
|
+
setStatus(h('p', { class: 'text-ui-sm text-destructive' }, L.failed + ': ' + errText(res)));
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const files = res.files || [];
|
|
962
|
+
if (!files.length) {
|
|
963
|
+
setStatus(h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.syncPickEmpty));
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
// 按同步内容分组排列:组标题下固定名(最新)放最上面,历史版本按
|
|
967
|
+
// 时间新→旧往下——两组文件多了也不会按时间交错混排
|
|
968
|
+
const parts = [];
|
|
969
|
+
for (const [prefix, label] of [['zcodepro', L.syncIncludeZcp], ['model', L.syncIncludeModel]]) {
|
|
970
|
+
const group = files.filter((f) => f.name === `${prefix}.json` || f.name.startsWith(`${prefix}-`));
|
|
971
|
+
if (!group.length) continue;
|
|
972
|
+
const latest = group.find((f) => f.name === `${prefix}.json`);
|
|
973
|
+
const history = group.filter((f) => f !== latest)
|
|
974
|
+
.sort((a, b) => ((b.lastModified || 0) - (a.lastModified || 0)) || b.name.localeCompare(a.name));
|
|
975
|
+
parts.push(h('div', { class: 'px-1 pt-2 text-ui-xs font-medium text-foreground-subtle' },
|
|
976
|
+
`${label}${isZhLocale() ? '(' : ' ('}${group.length}${isZhLocale() ? ')' : ')'}`));
|
|
977
|
+
for (const f of [latest, ...history]) {
|
|
978
|
+
if (f) parts.push(versionRow(f));
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
listWrap.replaceChildren(...parts);
|
|
982
|
+
};
|
|
983
|
+
// 导入沿用原有的覆盖确认文案,并注明选中的是哪个文件
|
|
984
|
+
const confirmImport = (f, rowBtns, importBtn) => {
|
|
985
|
+
openDialog({
|
|
986
|
+
title: L.syncConfirmTitle,
|
|
987
|
+
width: 'sm:max-w-md',
|
|
988
|
+
onMount: ({ body: confirmBody, close: closeConfirm }) => {
|
|
989
|
+
confirmBody.append(
|
|
990
|
+
h('p', { class: 'mb-2 truncate font-mono text-ui-xs text-foreground-subtle', title: f.name }, f.name),
|
|
991
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.syncConfirmDesc),
|
|
992
|
+
dialogFooter(
|
|
993
|
+
btnSecondary(L.cancel, () => closeConfirm()),
|
|
994
|
+
btnPrimary(L.syncConfirmGo, () => { closeConfirm(); void importVersion(f, rowBtns, importBtn); }, 'min-w-24'),
|
|
995
|
+
),
|
|
996
|
+
);
|
|
997
|
+
},
|
|
998
|
+
});
|
|
999
|
+
};
|
|
1000
|
+
const importVersion = async (f, rowBtns, importBtn) => {
|
|
1001
|
+
setRowBusy(rowBtns, true);
|
|
1002
|
+
importBtn.textContent = L.syncVerImporting;
|
|
1003
|
+
const res = await rpc('/sync/version/apply', { method: 'POST', body: { file: f.name } });
|
|
1004
|
+
setRowBusy(rowBtns, false);
|
|
1005
|
+
importBtn.textContent = L.syncVerImport;
|
|
1006
|
+
if (!res.ok) {
|
|
1007
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
closePicker();
|
|
1011
|
+
renderSyncStatus(res.sync || {});
|
|
1012
|
+
// 该版本已落到本机:刷新弹窗内开关显示,并让常驻功能立即按新配置生效
|
|
1013
|
+
clearConfigCache();
|
|
1014
|
+
window.dispatchEvent(new CustomEvent('zcodepro:config-changed'));
|
|
1015
|
+
Object.assign(config, await getConfig(true));
|
|
1016
|
+
refreshRows();
|
|
1017
|
+
renderToolbarSwitch();
|
|
1018
|
+
const names = (res.applied || [])
|
|
1019
|
+
.map((k) => (k === 'model' ? L.syncIncludeModel : L.syncIncludeZcp))
|
|
1020
|
+
.join(isZhLocale() ? '、' : ', ');
|
|
1021
|
+
showToast(names ? `${L.syncDownloaded}(${names})` : L.syncDownloaded, 'success');
|
|
1022
|
+
};
|
|
1023
|
+
const confirmDelete = (f, rowBtns, isLatest) => {
|
|
1024
|
+
openDialog({
|
|
1025
|
+
title: L.syncVerDeleteTitle,
|
|
1026
|
+
width: 'sm:max-w-md',
|
|
1027
|
+
onMount: ({ body: confirmBody, close: closeConfirm }) => {
|
|
1028
|
+
confirmBody.append(
|
|
1029
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' },
|
|
1030
|
+
L.syncVerDeleteDesc,
|
|
1031
|
+
h('span', { class: 'font-mono' }, f.name)),
|
|
1032
|
+
// 删“最新”文件会让旧版 zcode-pro 失去下载入口,多提醒一句
|
|
1033
|
+
...(isLatest ? [h('p', { class: 'mt-2 text-ui-xs/relaxed text-amber-500' }, L.syncVerDeleteLatestNote)] : []),
|
|
1034
|
+
dialogFooter(
|
|
1035
|
+
btnSecondary(L.cancel, () => closeConfirm()),
|
|
1036
|
+
btnPrimary(L.syncVerDelete, () => { closeConfirm(); void deleteVersion(f, rowBtns); }, 'min-w-24'),
|
|
1037
|
+
),
|
|
1038
|
+
);
|
|
1039
|
+
},
|
|
1040
|
+
});
|
|
1041
|
+
};
|
|
1042
|
+
const deleteVersion = async (f, rowBtns) => {
|
|
1043
|
+
setRowBusy(rowBtns, true);
|
|
1044
|
+
const res = await rpc('/sync/version/delete', { method: 'POST', body: { file: f.name } });
|
|
1045
|
+
setRowBusy(rowBtns, false);
|
|
1046
|
+
if (!res.ok) {
|
|
1047
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
showToast(L.syncVerDeleted, 'success');
|
|
1051
|
+
void loadList();
|
|
1052
|
+
};
|
|
1053
|
+
const exportVersion = async (f, rowBtns, saveBtn) => {
|
|
1054
|
+
setRowBusy(rowBtns, true);
|
|
1055
|
+
saveBtn.textContent = L.syncVerSaving;
|
|
1056
|
+
// 宿主会弹系统目录选择器选保存位置;用户取消时不作提示
|
|
1057
|
+
const res = await rpc('/sync/version/export', { method: 'POST', body: { file: f.name } });
|
|
1058
|
+
setRowBusy(rowBtns, false);
|
|
1059
|
+
saveBtn.textContent = L.syncVerSave;
|
|
1060
|
+
if (!res.ok) {
|
|
1061
|
+
if (res.code !== 'export-cancelled') showToast(L.failed + ': ' + errText(res), 'error');
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
showToast(`${L.syncVerSaved} ${res.path}`, 'success');
|
|
1065
|
+
};
|
|
1066
|
+
void loadList();
|
|
822
1067
|
},
|
|
823
1068
|
});
|
|
824
1069
|
};
|
|
825
|
-
|
|
1070
|
+
// 上传:云端固定名(最新)与时间戳历史版本由服务端一并写入云端;下载期间按钮也
|
|
1071
|
+
// 禁用(列表在变,选了也会过期)
|
|
1072
|
+
const runSyncPush = async () => {
|
|
826
1073
|
syncUploadBtn.disabled = true;
|
|
827
1074
|
syncDownloadBtn.disabled = true;
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const res = await rpc('/sync/run', { method: 'POST', body: { direction } });
|
|
1075
|
+
const idleText = syncUploadBtn.textContent;
|
|
1076
|
+
syncUploadBtn.textContent = L.syncUploading;
|
|
1077
|
+
const res = await rpc('/sync/run', { method: 'POST', body: { direction: 'push' } });
|
|
832
1078
|
syncUploadBtn.disabled = false;
|
|
833
1079
|
syncDownloadBtn.disabled = false;
|
|
834
|
-
|
|
1080
|
+
syncUploadBtn.textContent = idleText;
|
|
835
1081
|
if (!res.ok) {
|
|
836
1082
|
showToast(L.failed + ': ' + errText(res), 'error');
|
|
837
1083
|
return;
|
|
838
1084
|
}
|
|
839
1085
|
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
|
-
}
|
|
1086
|
+
showToast(L.syncUploaded, 'success');
|
|
856
1087
|
};
|
|
857
1088
|
paneSync.append(
|
|
858
1089
|
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.syncDesc),
|
|
@@ -865,7 +1096,8 @@ export function openSettingsDialog() {
|
|
|
865
1096
|
h('div', { class: 'min-w-0' },
|
|
866
1097
|
h('p', { class: 'text-ui-xs/relaxed text-foreground-subtle' }, L.syncFormHint),
|
|
867
1098
|
syncRecLine),
|
|
868
|
-
syncSaveBtn)),
|
|
1099
|
+
h('div', { class: 'flex shrink-0 items-center gap-1.5' }, syncSaveBtn, syncTestBtn))),
|
|
1100
|
+
syncTestState,
|
|
869
1101
|
h('div', { class: 'mt-2 divide-y divide-border rounded-xl border border-border' }, syncIncludeWrap),
|
|
870
1102
|
h('div', { class: 'mt-2 flex items-center justify-between gap-2 rounded-xl border border-border p-2' },
|
|
871
1103
|
h('div', { class: 'min-w-0' }, syncStatusLine),
|
|
@@ -873,6 +1105,48 @@ export function openSettingsDialog() {
|
|
|
873
1105
|
h('p', { class: 'mt-2 text-ui-xs/relaxed text-foreground-subtle' }, L.syncHint),
|
|
874
1106
|
);
|
|
875
1107
|
|
|
1108
|
+
// “调试”:开发模式开关 + 重载按钮。重载 = 构建 → 换新启动器进程(新 helper
|
|
1109
|
+
// 代码与新注入脚本)→ 各 ZCode 窗口检测到换代自动整页刷新重新注入;
|
|
1110
|
+
// ZCode 本体与所有会话不受影响,不用重启 ZCode
|
|
1111
|
+
const persistDevMode = async (v) => {
|
|
1112
|
+
const res = await rpc('/config', { method: 'POST', body: { devMode: v } });
|
|
1113
|
+
if (!res.ok) {
|
|
1114
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
if (res.config) config.devMode = res.config.devMode;
|
|
1118
|
+
return true;
|
|
1119
|
+
};
|
|
1120
|
+
const devReloadBtn = btnSmall(L.devReload, () => { void runDevReload(); }, 'shrink-0', 'primary');
|
|
1121
|
+
const runDevReload = async () => {
|
|
1122
|
+
devReloadBtn.disabled = true;
|
|
1123
|
+
devReloadBtn.textContent = L.devReloading;
|
|
1124
|
+
// 旧进程应答后即退出,fetch 可能中断,成败都进入下面的兜底刷新
|
|
1125
|
+
try { await rpc('/dev/reload', { method: 'POST', body: {} }); } catch { /* ignore */ }
|
|
1126
|
+
// 兜底:正常情况下新启动器注入时本页会自动刷新;2 秒没动静(交接失败)
|
|
1127
|
+
// 就自己刷新,宁可多刷一次也不能停在旧代码 + 失效 token 上
|
|
1128
|
+
setTimeout(() => { try { location.reload(); } catch { /* ignore */ } }, 2000);
|
|
1129
|
+
};
|
|
1130
|
+
const devModeWrap = h('div');
|
|
1131
|
+
const renderDevPane = () => {
|
|
1132
|
+
const dev = config.devMode === true;
|
|
1133
|
+
devModeWrap.replaceChildren(
|
|
1134
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.devModeDesc),
|
|
1135
|
+
h('div', { class: 'mt-3 divide-y divide-border rounded-xl border border-border' },
|
|
1136
|
+
settingRow(L.devMode, L.devModeRowDesc, dev, async () => {
|
|
1137
|
+
if (await persistDevMode(!dev)) renderDevPane();
|
|
1138
|
+
}),
|
|
1139
|
+
...(dev ? [h('div', { class: 'flex items-center justify-between gap-2 px-2 py-1.5' },
|
|
1140
|
+
h('div', { class: 'min-w-0' },
|
|
1141
|
+
h('div', { class: 'text-ui-sm font-medium text-foreground' }, L.devReload),
|
|
1142
|
+
h('div', { class: 'mt-0.5 text-ui-xs/relaxed text-foreground-subtle' }, L.devReloadDesc)),
|
|
1143
|
+
devReloadBtn)] : []),
|
|
1144
|
+
),
|
|
1145
|
+
);
|
|
1146
|
+
};
|
|
1147
|
+
renderDevPane();
|
|
1148
|
+
paneDebug.append(devModeWrap);
|
|
1149
|
+
|
|
876
1150
|
// “视觉代理”:编辑 ~/.zcode/zcode-vision.json(zcode-vision 插件与 /vision-* 命令共用同一文件)。
|
|
877
1151
|
// 代理列表顺序即执行链;结构性改动(开关/模式/排序/删增)即时保存,文本输入防抖保存。
|
|
878
1152
|
// 注意:保存时链按全部代理重建——若用 /vision-chain 配过子集链,会被这里覆盖(两套入口语义如此,面板以列表为准)。
|
|
@@ -915,7 +1189,55 @@ export function openSettingsDialog() {
|
|
|
915
1189
|
style: 'display:none',
|
|
916
1190
|
});
|
|
917
1191
|
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';
|
|
1192
|
+
// 未装插件时的一键安装卡(与 Headroom 面板同款交互):helper 单插件安装,
|
|
1193
|
+
// 装完 onInstalled 原地恢复面板。rtk/vision 面板共用。
|
|
1194
|
+
const pluginInstallCard = (pluginName, onInstalled) => {
|
|
1195
|
+
const installBtn = btnSmall(L.headroomInstall, () => { void runInstall(); }, 'shrink-0');
|
|
1196
|
+
const installState = h('div', { class: 'mt-0.5 truncate text-ui-xs/relaxed text-foreground-subtle' });
|
|
1197
|
+
const runInstall = async () => {
|
|
1198
|
+
installBtn.disabled = true;
|
|
1199
|
+
installBtn.textContent = L.headroomInstalling;
|
|
1200
|
+
installState.textContent = '';
|
|
1201
|
+
const res = await rpc('/plugins/update', { method: 'POST', body: { name: pluginName } });
|
|
1202
|
+
if (!res.ok) {
|
|
1203
|
+
installBtn.disabled = false;
|
|
1204
|
+
installBtn.textContent = L.headroomInstall;
|
|
1205
|
+
showToast(L.pluginsUpdateFailed + ': ' + errText(res), 'error');
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
if (onInstalled) await onInstalled();
|
|
1209
|
+
showToast(L.pluginInstallDone.replaceAll('{name}', pluginName), 'success');
|
|
1210
|
+
};
|
|
1211
|
+
return h('div', { class: 'mt-3 flex items-center justify-between gap-2 rounded-xl border border-border p-2' },
|
|
1212
|
+
h('div', { class: 'min-w-0' },
|
|
1213
|
+
h('div', { class: 'text-ui-sm font-medium text-foreground' }, L.pluginInstallCardTitle.replaceAll('{name}', pluginName)),
|
|
1214
|
+
h('div', { class: 'mt-0.5 text-ui-xs/relaxed text-foreground-subtle' }, L.pluginInstallCardDesc),
|
|
1215
|
+
installState),
|
|
1216
|
+
installBtn);
|
|
1217
|
+
};
|
|
1218
|
+
|
|
918
1219
|
const renderVision = () => {
|
|
1220
|
+
if (!visionRes.ok) {
|
|
1221
|
+
paneVision.replaceChildren(
|
|
1222
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.visionDesc),
|
|
1223
|
+
h('p', { class: 'mt-2 text-ui-sm text-destructive' }, L.visionLoadFailed + ': ' + errText(visionRes)),
|
|
1224
|
+
);
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
// 插件未装优先显示安装卡——判断只看钩子(visionRes.hook),与配置文件是否存在无关:
|
|
1228
|
+
// 配置是钩子消费的,没钩子就没功能;老用户卸载插件后 ~/.zcode/zcode-vision.json 仍在,
|
|
1229
|
+
// 若按配置存在与否分流会误入完整配置界面而毫无「未安装」提示
|
|
1230
|
+
if (!visionRes.hook) {
|
|
1231
|
+
paneVision.replaceChildren(
|
|
1232
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.visionDesc),
|
|
1233
|
+
pluginInstallCard('zcode-vision', async () => {
|
|
1234
|
+
const meta = await rpc('/vision');
|
|
1235
|
+
if (meta.ok) Object.assign(visionRes, meta);
|
|
1236
|
+
renderVision();
|
|
1237
|
+
}),
|
|
1238
|
+
);
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
919
1241
|
if (!visionCfg) {
|
|
920
1242
|
const initBtn = btnSmall(L.visionAddProxy, async () => {
|
|
921
1243
|
const res = await rpc('/vision', { method: 'POST', body: { config: VISION_DEFAULT_CFG } });
|
|
@@ -925,6 +1247,10 @@ export function openSettingsDialog() {
|
|
|
925
1247
|
paneVision.replaceChildren(
|
|
926
1248
|
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.visionDesc),
|
|
927
1249
|
...(visionRes.ok ? [] : [h('p', { class: 'mt-2 text-ui-sm text-destructive' }, L.visionLoadFailed + ': ' + errText(visionRes))]),
|
|
1250
|
+
// 信息卡(仅版本行;配置未建,启用开关随配置出现)
|
|
1251
|
+
h('div', { class: 'mt-3 rounded-xl border border-border p-2' },
|
|
1252
|
+
h('div', { class: 'flex flex-wrap items-center gap-2' },
|
|
1253
|
+
h('span', { class: 'shrink-0 whitespace-nowrap text-ui-xs/relaxed text-foreground-subtle' }, `插件 ${visionRes.pluginVersion || '—'}`))),
|
|
928
1254
|
h('div', { class: 'mt-3 flex justify-end' }, initBtn),
|
|
929
1255
|
);
|
|
930
1256
|
return;
|
|
@@ -1046,17 +1372,22 @@ export function openSettingsDialog() {
|
|
|
1046
1372
|
});
|
|
1047
1373
|
paneVision.replaceChildren(
|
|
1048
1374
|
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.visionDesc),
|
|
1049
|
-
//
|
|
1375
|
+
// 信息卡:插件版本 + 启用图片识别开关(与 rtk/headroom 面板同款式)
|
|
1376
|
+
h('div', { class: 'mt-3 rounded-xl border border-border p-2' },
|
|
1377
|
+
h('div', { class: 'flex flex-wrap items-center gap-2' },
|
|
1378
|
+
h('span', { class: 'shrink-0 whitespace-nowrap text-ui-xs/relaxed text-foreground-subtle' }, `插件 ${visionRes.pluginVersion || '—'}`),
|
|
1379
|
+
h('div', { class: 'ml-auto' },
|
|
1380
|
+
settingRow(L.visionEnabled, '', visionCfg.enabled !== false, async () => {
|
|
1381
|
+
visionCfg.enabled = !(visionCfg.enabled !== false);
|
|
1382
|
+
await persistVision();
|
|
1383
|
+
renderVision();
|
|
1384
|
+
})))),
|
|
1385
|
+
// 链模式 + 强制拦截开关合到一行(启用开关已上移信息卡)
|
|
1050
1386
|
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
1387
|
h('span', { class: 'ml-1.5 shrink-0 text-ui-sm font-medium text-foreground' }, L.visionMode),
|
|
1052
1388
|
modeBtn('fallback', L.visionModeFallback, L.visionModeFallbackDesc),
|
|
1053
1389
|
modeBtn('pipeline', L.visionModePipeline, L.visionModePipelineDesc),
|
|
1054
1390
|
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
1391
|
settingRow(L.visionForceIntercept, '', visionCfg.forceIntercept !== false, async () => {
|
|
1061
1392
|
visionCfg.forceIntercept = !(visionCfg.forceIntercept !== false);
|
|
1062
1393
|
await persistVision();
|
|
@@ -1122,15 +1453,20 @@ export function openSettingsDialog() {
|
|
|
1122
1453
|
return res.ok;
|
|
1123
1454
|
};
|
|
1124
1455
|
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
|
|
1456
|
+
// rtk 本体(GitHub Releases 下载替换)的版本行与升级控件,渲染函数外创建一次。
|
|
1457
|
+
// 信息卡单行:版本行 + 弹性状态槽(空闲显示本体位置,检查更新/升级时被活动
|
|
1458
|
+
// 日志顶替,见 makeUpgradeControls 的 idleText)+ 检查更新按钮 + 启用开关
|
|
1126
1459
|
const rtkVersionLine = h('span', { class: 'shrink-0 whitespace-nowrap text-ui-xs/relaxed text-foreground-subtle' });
|
|
1460
|
+
let rtkLocText = '';
|
|
1127
1461
|
const renderRtkVersion = (meta) => {
|
|
1128
|
-
const v = meta.version || 'rtk';
|
|
1129
|
-
rtkVersionLine.textContent = meta.
|
|
1462
|
+
const v = String(meta.version || '').replace(/^rtk\s+/i, '').trim();
|
|
1463
|
+
rtkVersionLine.textContent = `插件 ${meta.pluginVersion || '—'}${v ? ` · rtk ${v}` : ''}`;
|
|
1464
|
+
rtkLocText = meta.binPath || '';
|
|
1130
1465
|
};
|
|
1131
1466
|
renderRtkVersion(rtkRes);
|
|
1132
1467
|
const rtkUp = makeUpgradeControls({
|
|
1133
1468
|
endpoint: '/rtk',
|
|
1469
|
+
idleText: () => rtkLocText,
|
|
1134
1470
|
metaRefresh: async () => {
|
|
1135
1471
|
const meta = await rpc('/rtk');
|
|
1136
1472
|
if (meta.ok) {
|
|
@@ -1143,15 +1479,50 @@ export function openSettingsDialog() {
|
|
|
1143
1479
|
rtkUpgradePollStop = rtkUp.stop;
|
|
1144
1480
|
// 版本/路径/检查更新 + 启用 rtk 压缩开关合到一行(开关靠右,顶部已有简介不带描述)
|
|
1145
1481
|
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,
|
|
1482
|
+
const rtkVersionCard = h('div', { class: 'mt-3 flex flex-wrap items-center gap-2 rounded-xl border border-border p-2' },
|
|
1483
|
+
rtkVersionLine, rtkUp.state, rtkUp.btn,
|
|
1484
|
+
h('div', { class: 'ml-auto shrink-0' }, rtkSwitchRow));
|
|
1148
1485
|
const rtkBody = h('div');
|
|
1149
|
-
//
|
|
1486
|
+
// 插件未装时的一键安装行(本体未装另有提示行),装完原地撤下
|
|
1487
|
+
const rtkPluginRow = h('div');
|
|
1488
|
+
const renderRtkPluginRow = () => {
|
|
1489
|
+
rtkPluginRow.replaceChildren(...(rtkRes.hook ? [] : [
|
|
1490
|
+
pluginInstallCard('rtk', async () => {
|
|
1491
|
+
const meta = await rpc('/rtk');
|
|
1492
|
+
if (meta.ok) {
|
|
1493
|
+
Object.assign(rtkRes, meta);
|
|
1494
|
+
// 版本行 span 与 rtkCfg 都是弹窗打开时的未装态快照,装完必须重算/重建
|
|
1495
|
+
renderRtkVersion(meta);
|
|
1496
|
+
// rtkCfg 整体重建(未装态时内置清单为空,不重建则白名单区块渲染旧空清单)
|
|
1497
|
+
rtkCfg = {
|
|
1498
|
+
installed: meta.installed !== false,
|
|
1499
|
+
version: meta.version || '',
|
|
1500
|
+
binPath: meta.binPath || '',
|
|
1501
|
+
mode: meta.mode === 'off' ? 'off' : 'hint',
|
|
1502
|
+
whitelist: Array.isArray(meta.whitelist) ? [...meta.whitelist] : [],
|
|
1503
|
+
builtinGit: Array.isArray(meta.builtinGit) ? [...meta.builtinGit] : [],
|
|
1504
|
+
builtinPlain: Array.isArray(meta.builtinPlain) ? [...meta.builtinPlain] : [],
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
renderRtkPluginRow();
|
|
1508
|
+
renderRtk();
|
|
1509
|
+
}),
|
|
1510
|
+
]));
|
|
1511
|
+
};
|
|
1512
|
+
renderRtkPluginRow();
|
|
1513
|
+
// 简介置顶(与 Headroom 面板一致),其下插件安装卡,再下版本卡(含启用 rtk 压缩开关),再下白名单
|
|
1150
1514
|
paneRtk.append(
|
|
1151
1515
|
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.rtkDesc),
|
|
1516
|
+
rtkPluginRow,
|
|
1152
1517
|
rtkVersionCard,
|
|
1153
1518
|
rtkBody);
|
|
1154
1519
|
const renderRtk = () => {
|
|
1520
|
+
// 插件未装:只显示安装卡,隐藏版本卡与全部设置项(与 vision/headroom 未装时一致)
|
|
1521
|
+
if (rtkRes.ok && !rtkRes.hook) {
|
|
1522
|
+
rtkVersionCard.style.display = 'none';
|
|
1523
|
+
rtkBody.replaceChildren();
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1155
1526
|
rtkVersionCard.style.display = rtkCfg && rtkCfg.installed ? '' : 'none';
|
|
1156
1527
|
if (!rtkCfg) {
|
|
1157
1528
|
rtkBody.replaceChildren(
|
|
@@ -1195,7 +1566,45 @@ export function openSettingsDialog() {
|
|
|
1195
1566
|
rtkBody.replaceChildren(
|
|
1196
1567
|
...(rtkCfg.installed
|
|
1197
1568
|
? []
|
|
1198
|
-
: [h('
|
|
1569
|
+
: [h('div', { class: 'flex items-center gap-2' },
|
|
1570
|
+
h('p', { class: 'min-w-0 flex-1 text-ui-sm text-amber-500' }, L.rtkNotInstalled),
|
|
1571
|
+
// 一键安装本体:与“检查更新”同一条 helper 升级管线(未装则装到 ~/.local/bin)
|
|
1572
|
+
(() => {
|
|
1573
|
+
const b = btnSmall(L.rtkInstallBin, () => { void run(); }, 'shrink-0');
|
|
1574
|
+
const run = async () => {
|
|
1575
|
+
b.disabled = true;
|
|
1576
|
+
b.textContent = L.headroomInstalling;
|
|
1577
|
+
const res = await rpc('/rtk', { method: 'POST', body: { upgrade: true } });
|
|
1578
|
+
if (!res.ok) {
|
|
1579
|
+
b.disabled = false;
|
|
1580
|
+
b.textContent = L.rtkInstallBin;
|
|
1581
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
const done = await new Promise((resolve) => {
|
|
1585
|
+
const iv = setInterval(async () => {
|
|
1586
|
+
const s = await rpc('/rtk/upgrade');
|
|
1587
|
+
if (s.ok && s.upgrade && !s.upgrade.running) { clearInterval(iv); resolve(s.upgrade); }
|
|
1588
|
+
}, 1200);
|
|
1589
|
+
// 超时也要 resolve:否则按钮永久停在“安装中…”
|
|
1590
|
+
setTimeout(() => { clearInterval(iv); resolve(null); }, 10 * 60 * 1000);
|
|
1591
|
+
});
|
|
1592
|
+
if (done && !done.error && done.version) {
|
|
1593
|
+
const meta = await rpc('/rtk');
|
|
1594
|
+
if (meta.ok) {
|
|
1595
|
+
Object.assign(rtkRes, meta);
|
|
1596
|
+
rtkCfg = { ...rtkCfg, installed: meta.installed !== false, version: meta.version || '', binPath: meta.binPath || '' };
|
|
1597
|
+
}
|
|
1598
|
+
showToast(L.rtkBinInstalled, 'success');
|
|
1599
|
+
renderRtk();
|
|
1600
|
+
} else {
|
|
1601
|
+
b.disabled = false;
|
|
1602
|
+
b.textContent = L.rtkInstallBin;
|
|
1603
|
+
showToast((done && done.error) || L.headroomInstallRetry, 'error');
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
return b;
|
|
1607
|
+
})())]),
|
|
1199
1608
|
h('div', { class: 'mt-2 rounded-xl border border-border p-2' },
|
|
1200
1609
|
h('div', { class: 'text-ui-sm font-medium text-foreground' }, L.rtkWhitelistTitle),
|
|
1201
1610
|
h('p', { class: 'mt-1 text-ui-xs/relaxed text-foreground-subtle' }, L.rtkWhitelistDesc),
|
|
@@ -1243,15 +1652,18 @@ export function openSettingsDialog() {
|
|
|
1243
1652
|
// 可整面重渲染:未装插件时显示安装卡,安装成功后原地恢复完整面板。
|
|
1244
1653
|
// 版本行与本体升级控件(pip 通道)在渲染函数外创建一次,重渲染时复用同一组节点
|
|
1245
1654
|
const hrVersionLine = h('span', { class: 'shrink-0 whitespace-nowrap text-ui-xs/relaxed text-foreground-subtle' });
|
|
1655
|
+
let hrLocText = '';
|
|
1656
|
+
// 启用开关(与 rtk 面板同款,版本卡右上角):开 = ZCode 供应商走 headroom,
|
|
1657
|
+
// 关 = 直连上游;只改供应商地址,不动代理进程(headroom 可被其他程序共用)
|
|
1658
|
+
const hrSwitchRow = h('div');
|
|
1246
1659
|
const renderHrVersion = (meta) => {
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
if (meta.version) parts.push(`${L.headroomVersionBin} ${meta.version}`);
|
|
1250
|
-
hrVersionLine.textContent = parts.length ? parts.join(' · ') : `${L.headroomVersionPlugin} —`;
|
|
1660
|
+
hrVersionLine.textContent = `插件 ${meta.pluginVersion || '—'}${meta.version ? ` · headroom ${meta.version}` : ''}`;
|
|
1661
|
+
hrLocText = meta.binPath || '';
|
|
1251
1662
|
};
|
|
1252
1663
|
renderHrVersion(headroomRes);
|
|
1253
1664
|
const hrUp = makeUpgradeControls({
|
|
1254
1665
|
endpoint: '/headroom',
|
|
1666
|
+
idleText: () => hrLocText,
|
|
1255
1667
|
metaRefresh: async () => {
|
|
1256
1668
|
const meta = await rpc('/headroom');
|
|
1257
1669
|
if (meta.ok) {
|
|
@@ -1278,7 +1690,7 @@ export function openSettingsDialog() {
|
|
|
1278
1690
|
installBtn.disabled = true;
|
|
1279
1691
|
installBtn.textContent = L.headroomInstalling;
|
|
1280
1692
|
installState.textContent = '';
|
|
1281
|
-
const res = await rpc('/plugins/update', { method: 'POST', body: { name: 'headroom'
|
|
1693
|
+
const res = await rpc('/plugins/update', { method: 'POST', body: { name: 'headroom' } });
|
|
1282
1694
|
if (!res.ok) {
|
|
1283
1695
|
installBtn.disabled = false;
|
|
1284
1696
|
installBtn.textContent = L.headroomInstall;
|
|
@@ -1289,6 +1701,8 @@ export function openSettingsDialog() {
|
|
|
1289
1701
|
if (meta.ok) Object.assign(headroomRes, meta);
|
|
1290
1702
|
if (headroomRes.hook) {
|
|
1291
1703
|
showToast(L.headroomInstalled, 'success');
|
|
1704
|
+
// 版本行 span 在弹窗打开时按未装态算过一次,装完必须重算
|
|
1705
|
+
renderHrVersion(headroomRes);
|
|
1292
1706
|
renderHeadroomPane();
|
|
1293
1707
|
} else {
|
|
1294
1708
|
installBtn.disabled = false;
|
|
@@ -1312,16 +1726,65 @@ export function openSettingsDialog() {
|
|
|
1312
1726
|
const hrRun = async (reqBody, warn) => {
|
|
1313
1727
|
if (hrBusy) return false;
|
|
1314
1728
|
hrBusy = true;
|
|
1315
|
-
const
|
|
1316
|
-
|
|
1729
|
+
const action = reqBody && reqBody.action;
|
|
1730
|
+
const isStart = action === 'start' || action === 'restart';
|
|
1731
|
+
const isStop = action === 'stop';
|
|
1732
|
+
// 生命周期按钮:点击瞬间就显示等待提示 + 计时(不等 RPC 返回——
|
|
1733
|
+
// RPC 里 helper 要杀/拉进程,期间 3-10 秒界面不能毫无反应)
|
|
1734
|
+
// RPC 先发出(不等返回——期间界面要显示等待反馈)
|
|
1735
|
+
const rpcDone = rpc('/headroom', { method: 'POST', body: reqBody });
|
|
1736
|
+
let timerIv = null;
|
|
1737
|
+
if (isStart || isStop) {
|
|
1738
|
+
const wantUp = isStart;
|
|
1739
|
+
const msg = action === 'restart' ? L.hrRestarting : isStart ? L.hrStarting : L.hrStopping;
|
|
1740
|
+
const doneMsg = isStart ? L.hrStarted : L.hrStopped;
|
|
1741
|
+
const timeoutMs = isStart ? 90 * 1000 : 30 * 1000;
|
|
1742
|
+
const t0 = Date.now();
|
|
1743
|
+
const timerSpan = h('span', { class: 'text-ui-sm text-foreground-subtle/70' }, msg + ' (0s)');
|
|
1744
|
+
hrStatusLine.replaceChildren(timerSpan);
|
|
1745
|
+
timerIv = setInterval(() => {
|
|
1746
|
+
timerSpan.textContent = msg + ` (${Math.round((Date.now() - t0) / 1000)}s)`;
|
|
1747
|
+
}, 1000);
|
|
1748
|
+
// RPC 完成后进入轮询(RPC 执行实际的 stop/start,可能要 3-10 秒)。
|
|
1749
|
+
// 轮询出结果前 hrBusy 一直占着:期间再点开关或按钮直接忽略,否则
|
|
1750
|
+
// 每次点击都会重发 start、把计时和启动流程从头来过
|
|
1751
|
+
void (async () => {
|
|
1752
|
+
const kick = await rpcDone;
|
|
1753
|
+
if (!kick.ok) return; // 失败由主流程提示并复位
|
|
1754
|
+
while (Date.now() - t0 < timeoutMs) {
|
|
1755
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
1756
|
+
const st = await rpc('/headroom/status');
|
|
1757
|
+
if (!st.ok) continue;
|
|
1758
|
+
const up = !!(st.status && st.status.up);
|
|
1759
|
+
if (up === wantUp) {
|
|
1760
|
+
clearInterval(timerIv);
|
|
1761
|
+
hrBusy = false;
|
|
1762
|
+
await hrRefresh();
|
|
1763
|
+
showToast(doneMsg, isStart ? 'success' : undefined);
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
clearInterval(timerIv);
|
|
1768
|
+
hrBusy = false;
|
|
1769
|
+
if (isStart) showToast(L.hrStartTimeout, 'error');
|
|
1770
|
+
void hrRefresh();
|
|
1771
|
+
})();
|
|
1772
|
+
}
|
|
1773
|
+
const res = await rpcDone;
|
|
1317
1774
|
if (!res.ok) {
|
|
1775
|
+
if (timerIv) clearInterval(timerIv);
|
|
1776
|
+
hrBusy = false;
|
|
1318
1777
|
showToast(L.failed + ': ' + errText(res), 'error');
|
|
1778
|
+
void hrRefresh();
|
|
1319
1779
|
return false;
|
|
1320
1780
|
}
|
|
1321
1781
|
if (res.config) Object.assign(hrCfg, res.config);
|
|
1322
1782
|
if (warn) showToast(warn);
|
|
1323
|
-
// backend/power
|
|
1324
|
-
|
|
1783
|
+
// 非生命周期动作(backend/power):稍等后刷一次
|
|
1784
|
+
if (!isStart && !isStop) {
|
|
1785
|
+
hrBusy = false;
|
|
1786
|
+
setTimeout(() => { void hrRefresh(); }, 1200);
|
|
1787
|
+
}
|
|
1325
1788
|
return true;
|
|
1326
1789
|
};
|
|
1327
1790
|
// 状态值本地化:钩子输出的是 auto/cpu/ac/ok 等内部值,面板显示成人话
|
|
@@ -1360,6 +1823,23 @@ export function openSettingsDialog() {
|
|
|
1360
1823
|
hrHintLine.textContent = s.saverHint || '';
|
|
1361
1824
|
hrHintLine.style.display = s.saverHint ? '' : 'none';
|
|
1362
1825
|
};
|
|
1826
|
+
// 启用开关不走 hrRun:与代理进程无关,点击即时生效,也不受生命周期等待锁影响
|
|
1827
|
+
const renderHrUsageSwitch = () => {
|
|
1828
|
+
const usageOn = !!(headroomRes.usage && headroomRes.usage.enabled);
|
|
1829
|
+
hrSwitchRow.replaceChildren(
|
|
1830
|
+
settingRow(L.pluginEnable, '', usageOn, async () => {
|
|
1831
|
+
const next = !usageOn;
|
|
1832
|
+
const res = await rpc('/headroom', { method: 'POST', body: { usage: next } });
|
|
1833
|
+
if (!res.ok) {
|
|
1834
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
if (res.usage) headroomRes.usage = res.usage;
|
|
1838
|
+
renderHrUsageSwitch();
|
|
1839
|
+
if (next && !res.changed && !(res.usage && res.usage.enabled)) showToast(L.hrUsageNone);
|
|
1840
|
+
}));
|
|
1841
|
+
};
|
|
1842
|
+
renderHrUsageSwitch();
|
|
1363
1843
|
const hrRefreshBtn = btnSmall(L.headroomRefresh, () => { void hrRefresh(); });
|
|
1364
1844
|
const hrStartBtn = btnSmall(L.headroomStart, () => { void hrRun({ action: 'start' }); });
|
|
1365
1845
|
const hrRestartBtn = btnSmall(L.headroomRestart, () => { void hrRun({ action: 'restart' }); });
|
|
@@ -1453,14 +1933,60 @@ export function openSettingsDialog() {
|
|
|
1453
1933
|
};
|
|
1454
1934
|
updateHrPwrHint();
|
|
1455
1935
|
|
|
1936
|
+
// 本体未装:一键安装(pip 全新安装通道,helper 同一条升级任务管线);
|
|
1937
|
+
// 装完刷新 meta 并整面重渲染(版本行、本体状态、代理控制全部就位)
|
|
1938
|
+
const hrInstallBinBtn = (() => {
|
|
1939
|
+
const b = btnSmall(L.hrInstallBin, () => { void run(); }, 'shrink-0');
|
|
1940
|
+
let busy = false;
|
|
1941
|
+
const run = async () => {
|
|
1942
|
+
if (busy) return;
|
|
1943
|
+
busy = true;
|
|
1944
|
+
b.disabled = true;
|
|
1945
|
+
b.textContent = L.headroomInstalling;
|
|
1946
|
+
const res = await rpc('/headroom', { method: 'POST', body: { upgrade: true } });
|
|
1947
|
+
if (!res.ok) {
|
|
1948
|
+
b.disabled = false;
|
|
1949
|
+
b.textContent = L.hrInstallBin;
|
|
1950
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
1951
|
+
busy = false;
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
// pip 下载大包可持续数分钟,轮询后台任务到结束(超时 resolve,避免按钮卡死)
|
|
1955
|
+
const done = await new Promise((resolve) => {
|
|
1956
|
+
const iv = setInterval(async () => {
|
|
1957
|
+
const s = await rpc('/headroom/upgrade');
|
|
1958
|
+
if (s.ok && s.upgrade && !s.upgrade.running) { clearInterval(iv); resolve(s.upgrade); }
|
|
1959
|
+
}, 2000);
|
|
1960
|
+
setTimeout(() => { clearInterval(iv); resolve(null); }, 12 * 60 * 1000);
|
|
1961
|
+
});
|
|
1962
|
+
busy = false;
|
|
1963
|
+
if (done && !done.error && done.version) {
|
|
1964
|
+
const meta = await rpc('/headroom');
|
|
1965
|
+
if (meta.ok) {
|
|
1966
|
+
renderHrVersion(meta);
|
|
1967
|
+
Object.assign(headroomRes, meta);
|
|
1968
|
+
}
|
|
1969
|
+
showToast(L.hrBinInstalled, 'success');
|
|
1970
|
+
renderHeadroomPane();
|
|
1971
|
+
} else {
|
|
1972
|
+
b.disabled = false;
|
|
1973
|
+
b.textContent = L.hrInstallBin;
|
|
1974
|
+
showToast((done && done.error) || L.headroomInstallRetry, 'error');
|
|
1975
|
+
}
|
|
1976
|
+
};
|
|
1977
|
+
return b;
|
|
1978
|
+
})();
|
|
1979
|
+
|
|
1456
1980
|
paneHeadroom.append(
|
|
1457
1981
|
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.headroomDesc),
|
|
1458
1982
|
...(headroomRes.installed === false
|
|
1459
|
-
? [h('
|
|
1983
|
+
? [h('div', { class: 'mt-2 flex items-center gap-2' },
|
|
1984
|
+
h('p', { class: 'min-w-0 flex-1 text-ui-sm text-amber-500' }, L.headroomBinMissing),
|
|
1985
|
+
hrInstallBinBtn)]
|
|
1460
1986
|
: []),
|
|
1461
|
-
h('div', { class: 'mt-3 flex items-center
|
|
1462
|
-
|
|
1463
|
-
|
|
1987
|
+
h('div', { class: 'mt-3 flex flex-wrap items-center gap-2 rounded-xl border border-border p-2' },
|
|
1988
|
+
hrVersionLine, hrUp.state, hrUp.btn,
|
|
1989
|
+
h('div', { class: 'ml-auto shrink-0' }, hrSwitchRow)),
|
|
1464
1990
|
h('div', { class: 'mt-2 rounded-xl border border-border p-2' },
|
|
1465
1991
|
h('div', { class: 'flex flex-wrap items-center justify-between gap-2' },
|
|
1466
1992
|
h('span', { class: 'text-ui-sm font-medium text-foreground' }, L.headroomStatusTitle),
|
|
@@ -1497,6 +2023,7 @@ export function openSettingsDialog() {
|
|
|
1497
2023
|
paneVision,
|
|
1498
2024
|
paneHeadroom,
|
|
1499
2025
|
paneRtk,
|
|
2026
|
+
paneDebug,
|
|
1500
2027
|
);
|
|
1501
2028
|
},
|
|
1502
2029
|
});
|