zcode-pro 0.16.0__py3-none-any.whl
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.
- zcode_pro/__init__.py +3 -0
- zcode_pro/__main__.py +4 -0
- zcode_pro/app/cli.mjs +9 -0
- zcode_pro/app/dist/inject.js +5821 -0
- zcode_pro/app/src/host/cdp.mjs +126 -0
- zcode_pro/app/src/host/helper.mjs +2134 -0
- zcode_pro/app/src/host/main.mjs +262 -0
- zcode_pro/app/src/host/paths.mjs +73 -0
- zcode_pro/app/src/host/pickFolder.mjs +152 -0
- zcode_pro/app/src/host/settings.mjs +61 -0
- zcode_pro/app/src/host/taskIndex.mjs +255 -0
- zcode_pro/app/src/host/taskOrder.mjs +117 -0
- zcode_pro/app/src/host/webdav.mjs +105 -0
- zcode_pro/app/src/inject/core.js +751 -0
- zcode_pro/app/src/inject/features/alias.js +211 -0
- zcode_pro/app/src/inject/features/copy-path.js +78 -0
- zcode_pro/app/src/inject/features/file-menu.js +147 -0
- zcode_pro/app/src/inject/features/idle-reclaim.js +179 -0
- zcode_pro/app/src/inject/features/image-menu.js +215 -0
- zcode_pro/app/src/inject/features/open-folder.js +43 -0
- zcode_pro/app/src/inject/features/pinned-collapse.js +132 -0
- zcode_pro/app/src/inject/features/pinned-expand.js +142 -0
- zcode_pro/app/src/inject/features/plugin-updates.js +28 -0
- zcode_pro/app/src/inject/features/project-menu.js +58 -0
- zcode_pro/app/src/inject/features/relocate-dialog.js +156 -0
- zcode_pro/app/src/inject/features/session-switch.js +244 -0
- zcode_pro/app/src/inject/features/settings-dialog.js +1503 -0
- zcode_pro/app/src/inject/features/settings-entry.js +21 -0
- zcode_pro/app/src/inject/features/styles.js +311 -0
- zcode_pro/app/src/inject/features/task-order.js +78 -0
- zcode_pro/app/src/inject/features/title-lock.js +242 -0
- zcode_pro/app/src/inject/features/toolbar-icons.js +243 -0
- zcode_pro/app/src/inject/features/ws-running.js +472 -0
- zcode_pro/app/src/inject/index.js +55 -0
- zcode_pro/app/src/inject/ui.js +734 -0
- zcode_pro/cli.py +71 -0
- zcode_pro-0.16.0.dist-info/METADATA +45 -0
- zcode_pro-0.16.0.dist-info/RECORD +41 -0
- zcode_pro-0.16.0.dist-info/WHEEL +4 -0
- zcode_pro-0.16.0.dist-info/entry_points.txt +2 -0
- zcode_pro-0.16.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,1503 @@
|
|
|
1
|
+
// “ZCode Pro 增强设置”弹窗:功能开关 + 样式调整 + 运行状态。
|
|
2
|
+
// 顶部标签页切换(视觉参考侧栏“分组/项目”切换);配置写入 helper(~/.zcode/zcodepro.json)。
|
|
3
|
+
import { h, t, rpc, getConfig, clearConfigCache, errText, HELPER_URL, isZhLocale } from '../core.js';
|
|
4
|
+
import { openDialog, dialogFooter, btnPrimary, btnSecondary, btnSmall, settingRow, ensureStyle, showToast, numberField, unitField } from '../ui.js';
|
|
5
|
+
import { refreshAliases } from './alias.js';
|
|
6
|
+
import { applyStyles, STYLE_DEFAULTS, showStyleHighlight, hideStyleHighlight } from './styles.js';
|
|
7
|
+
import { copyToClipboard, hCopyIcon } from './copy-path.js';
|
|
8
|
+
|
|
9
|
+
// 调宿主桥在系统浏览器打开外链;宿主没暴露该能力时静默忽略
|
|
10
|
+
function openExternal(url) {
|
|
11
|
+
try { void window.zcode.openExternal(url); } catch { /* ignore */ }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 本体升级交互(Headroom 的 pip 与 rtk 的 GitHub Releases 共用):
|
|
15
|
+
// 检查更新 → 升级(helper 后台任务,轮询进度,输出取最后一行、超长省略中间)→
|
|
16
|
+
// 进行中可停止;关弹窗不中断升级,重开恢复显示。endpoint 为 /headroom 或 /rtk,
|
|
17
|
+
// metaRefresh 在升级完成后刷新各面板自己的版本行与配置。
|
|
18
|
+
function makeUpgradeControls({ endpoint, metaRefresh }) {
|
|
19
|
+
const L = t();
|
|
20
|
+
const state = h('span', { class: 'min-w-0 flex-1 truncate text-left text-ui-xs/relaxed text-foreground-subtle' });
|
|
21
|
+
// 失败原因在状态行直显红字(toast 只有 5 秒且易被弹窗遮挡)
|
|
22
|
+
const setState = (text, kind = '') => {
|
|
23
|
+
state.textContent = text;
|
|
24
|
+
state.className = 'min-w-0 flex-1 truncate text-left text-ui-xs/relaxed '
|
|
25
|
+
+ (kind === 'error' ? 'text-destructive' : 'text-foreground-subtle');
|
|
26
|
+
};
|
|
27
|
+
let latest = null; // 检查到新版本时记录最新版号,按钮随即转为“升级”
|
|
28
|
+
let busy = false;
|
|
29
|
+
let timer = null;
|
|
30
|
+
const stopPoll = () => { if (timer) { clearInterval(timer); timer = null; } };
|
|
31
|
+
const lastLine = (out) => {
|
|
32
|
+
const lines = String(out || '').split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
33
|
+
return lines[lines.length - 1] || '';
|
|
34
|
+
};
|
|
35
|
+
// 进度行只留一行:先去掉工具自带的结尾省略号,超长截中间——
|
|
36
|
+
// 头部(动作+包名)和尾部(大小/百分比)信息量最大,配合样式 truncate 不挤压版本行
|
|
37
|
+
const short = (line) => {
|
|
38
|
+
const v = line.replace(/[.…]{3,}\s*$/, '').trimEnd();
|
|
39
|
+
if (v.length <= 80) return v;
|
|
40
|
+
return v.slice(0, 40) + '…' + v.slice(-39);
|
|
41
|
+
};
|
|
42
|
+
const btn = btnSmall(L.upgradeCheck, () => { void runCheck(); }, 'shrink-0');
|
|
43
|
+
const runCancel = async () => {
|
|
44
|
+
const res = await rpc(endpoint, { method: 'POST', body: { cancelUpgrade: true } });
|
|
45
|
+
if (!res.ok) showToast(L.failed + ': ' + errText(res), 'error');
|
|
46
|
+
// 收尾交给轮询:任务结束后 tick 会恢复按钮并提示“已停止升级”
|
|
47
|
+
};
|
|
48
|
+
const runUpgrade = async () => {
|
|
49
|
+
if (busy) return;
|
|
50
|
+
busy = true;
|
|
51
|
+
const res = await rpc(endpoint, { method: 'POST', body: { upgrade: true } });
|
|
52
|
+
busy = false;
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
btn.textContent = L.upgradeNow;
|
|
55
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
pollStart();
|
|
59
|
+
};
|
|
60
|
+
const runCheck = async () => {
|
|
61
|
+
if (busy) return;
|
|
62
|
+
if (timer) { void runCancel(); return; } // 升级进行中:按钮此时是“停止升级”
|
|
63
|
+
if (latest) { void runUpgrade(); return; }
|
|
64
|
+
busy = true;
|
|
65
|
+
btn.disabled = true;
|
|
66
|
+
setState(L.upgradeChecking);
|
|
67
|
+
const res = await rpc(endpoint, { method: 'POST', body: { checkUpdate: true } });
|
|
68
|
+
busy = false;
|
|
69
|
+
btn.disabled = false;
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
setState(L.upgradeCheckFailed, 'error');
|
|
72
|
+
showToast(L.upgradeCheckFailed + ': ' + errText(res), 'error');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (res.upToDate) {
|
|
76
|
+
latest = null;
|
|
77
|
+
setState(L.upgradeUpToDate.replaceAll('{v}', res.current || ''));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
latest = res.latest;
|
|
81
|
+
setState(L.upgradeFound.replaceAll('{v}', res.latest || ''));
|
|
82
|
+
btn.textContent = L.upgradeNow;
|
|
83
|
+
};
|
|
84
|
+
const tick = async () => {
|
|
85
|
+
const res = await rpc(endpoint + '/upgrade');
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
stopPoll();
|
|
88
|
+
latest = null;
|
|
89
|
+
busy = false;
|
|
90
|
+
btn.disabled = false;
|
|
91
|
+
btn.textContent = L.upgradeCheck;
|
|
92
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const snap = res.upgrade || {};
|
|
96
|
+
if (snap.running) {
|
|
97
|
+
setState(short(lastLine(snap.output)) || L.upgradeRunning);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
stopPoll();
|
|
101
|
+
latest = null;
|
|
102
|
+
busy = false;
|
|
103
|
+
btn.disabled = false;
|
|
104
|
+
btn.textContent = L.upgradeCheck;
|
|
105
|
+
if (snap.canceled) {
|
|
106
|
+
setState(L.upgradeStopped);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (snap.error) {
|
|
110
|
+
setState(String(snap.error).split('\n')[0], 'error');
|
|
111
|
+
showToast(L.failed + ': ' + String(snap.error).split('\n')[0], 'error');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
setState(snap.version ? L.upgradeDone.replaceAll('{v}', snap.version) : L.upgradeFinished);
|
|
115
|
+
showToast(state.textContent, 'success');
|
|
116
|
+
await metaRefresh();
|
|
117
|
+
};
|
|
118
|
+
const pollStart = () => {
|
|
119
|
+
stopPoll();
|
|
120
|
+
// 升级中按钮转为“停止升级”,保持可点(点击即中止)
|
|
121
|
+
btn.disabled = false;
|
|
122
|
+
btn.textContent = L.upgradeStop;
|
|
123
|
+
setState(L.upgradeRunning);
|
|
124
|
+
timer = setInterval(() => { void tick(); }, 1500);
|
|
125
|
+
void tick();
|
|
126
|
+
};
|
|
127
|
+
// 打开弹窗时若升级仍在后台跑,恢复进度显示(关闭弹窗不会中断升级)
|
|
128
|
+
void (async () => {
|
|
129
|
+
const res = await rpc(endpoint + '/upgrade');
|
|
130
|
+
if (res.ok && res.upgrade && res.upgrade.running) pollStart();
|
|
131
|
+
})();
|
|
132
|
+
return { btn, state, stop: stopPoll };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function openSettingsDialog() {
|
|
136
|
+
ensureStyle();
|
|
137
|
+
const L = t();
|
|
138
|
+
// Headroom / rtk 升级进度轮询的停止句柄:面板代码在 onMount 里赋值,关弹窗时停掉
|
|
139
|
+
let hrUpgradePollStop = null;
|
|
140
|
+
let rtkUpgradePollStop = null;
|
|
141
|
+
openDialog({
|
|
142
|
+
title: L.settingsTitle,
|
|
143
|
+
// 宽度类必须用宿主样式表已有的工具类(注入的类名不会生成 CSS):
|
|
144
|
+
// sm:max-w-3xl 不存在会失去约束拉满全屏;max-w-2xl=42rem 存在且尺寸合适
|
|
145
|
+
width: 'max-w-2xl',
|
|
146
|
+
overlay: 'none',
|
|
147
|
+
draggable: true,
|
|
148
|
+
posKey: 'settings',
|
|
149
|
+
dismissOnOutside: false,
|
|
150
|
+
onClose: () => {
|
|
151
|
+
if (hrUpgradePollStop) hrUpgradePollStop();
|
|
152
|
+
if (rtkUpgradePollStop) rtkUpgradePollStop();
|
|
153
|
+
},
|
|
154
|
+
onMount: async ({ body, close, content }) => {
|
|
155
|
+
// 标题行:左侧“ZCode Pro”点击跳转 GitHub 仓库;右上角 X 关闭弹窗(替代底部按钮)。
|
|
156
|
+
// h2 仍是拖拽把手——X 上阻止 mousedown 冒泡,避免点关闭时误触发拖拽
|
|
157
|
+
const titleEl = content && content.firstElementChild;
|
|
158
|
+
if (titleEl && titleEl.tagName === 'H2') {
|
|
159
|
+
titleEl.classList.add('flex', 'w-full', 'items-center', 'justify-between');
|
|
160
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
161
|
+
const mkIcon = (paths, cls) => {
|
|
162
|
+
const svg = h('svg', { viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
|
|
163
|
+
'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', class: cls });
|
|
164
|
+
for (const d of paths) {
|
|
165
|
+
const p2 = document.createElementNS(ns, 'path');
|
|
166
|
+
p2.setAttribute('d', d);
|
|
167
|
+
svg.append(p2);
|
|
168
|
+
}
|
|
169
|
+
return svg;
|
|
170
|
+
};
|
|
171
|
+
const titleLeft = h('span', { class: 'flex min-w-0 items-center' }, L.settingsTitle);
|
|
172
|
+
if (typeof window.zcode?.openExternal === 'function') {
|
|
173
|
+
const REPO_URL = 'https://github.com/duanluan/zcode-pro';
|
|
174
|
+
titleLeft.replaceChildren(
|
|
175
|
+
h('span', {
|
|
176
|
+
class: 'cursor-pointer underline-offset-4 hover:underline',
|
|
177
|
+
title: REPO_URL,
|
|
178
|
+
onClick: () => openExternal(REPO_URL),
|
|
179
|
+
}, L.settingsTitle),
|
|
180
|
+
mkIcon(['M15 3h6v6', 'M10 14 21 3', 'M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6'], 'ml-1 inline-block size-4 text-foreground-subtle'));
|
|
181
|
+
}
|
|
182
|
+
// 关闭按钮用文字字形 + 自有样式(.zcodepro-close):SVG 描边在本应用内不可见
|
|
183
|
+
const closeX = h('button', {
|
|
184
|
+
type: 'button',
|
|
185
|
+
class: 'zcodepro-close',
|
|
186
|
+
title: L.close,
|
|
187
|
+
'aria-label': L.close,
|
|
188
|
+
onMousedown: (e) => { e.stopPropagation(); },
|
|
189
|
+
onClick: () => { close(); },
|
|
190
|
+
}, '✕');
|
|
191
|
+
titleEl.replaceChildren(titleLeft, closeX);
|
|
192
|
+
}
|
|
193
|
+
const health = await rpc('/health');
|
|
194
|
+
const config = await getConfig(true);
|
|
195
|
+
const agentsRes = await rpc('/agents');
|
|
196
|
+
const visionRes = await rpc('/vision');
|
|
197
|
+
const visionProvidersRes = await rpc('/vision/providers');
|
|
198
|
+
const rtkRes = await rpc('/rtk');
|
|
199
|
+
const headroomRes = await rpc('/headroom');
|
|
200
|
+
|
|
201
|
+
const setFeature = async (key, value) => {
|
|
202
|
+
const res = await rpc('/config', { method: 'POST', body: { features: { [key]: value } } });
|
|
203
|
+
clearConfigCache();
|
|
204
|
+
// 通知常驻功能(如侧栏菜单并入顶栏)立即按新配置启用/停用
|
|
205
|
+
window.dispatchEvent(new CustomEvent('zcodepro:config-changed'));
|
|
206
|
+
if (!res.ok) {
|
|
207
|
+
body.querySelector('[data-zcodepro-status]').replaceChildren(
|
|
208
|
+
h('span', { class: 'text-ui-sm text-destructive' }, L.failed + ': ' + errText(res))
|
|
209
|
+
);
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
return true;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const statusLine = h('div', {
|
|
216
|
+
class: 'flex items-center gap-2 text-ui-sm text-foreground-subtle',
|
|
217
|
+
'data-zcodepro-status': '1',
|
|
218
|
+
},
|
|
219
|
+
h('span', { class: 'inline-block size-2 rounded-full bg-emerald-500' }),
|
|
220
|
+
h('span', { class: 'text-foreground-subtle/70' }, `${L.version} ${health.version} · ${HELPER_URL}`)
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const rows = h('div', { class: 'divide-y divide-border rounded-xl border border-border' });
|
|
224
|
+
const refreshRows = () => {
|
|
225
|
+
const f = config.features || {};
|
|
226
|
+
rows.replaceChildren(
|
|
227
|
+
settingRow(L.featureProjectMenu, L.featureProjectMenuDesc, f.projectMenu !== false, async () => {
|
|
228
|
+
const next = !(f.projectMenu !== false);
|
|
229
|
+
if (await setFeature('projectMenu', next)) f.projectMenu = next;
|
|
230
|
+
refreshRows();
|
|
231
|
+
// 关闭后立即还原真实名称;开启则按表重新渲染
|
|
232
|
+
await refreshAliases();
|
|
233
|
+
}),
|
|
234
|
+
settingRow(L.featureTaskOrder, L.featureTaskOrderDesc, f.taskOrder !== false, async () => {
|
|
235
|
+
const next = !(f.taskOrder !== false);
|
|
236
|
+
if (await setFeature('taskOrder', next)) f.taskOrder = next;
|
|
237
|
+
refreshRows();
|
|
238
|
+
}),
|
|
239
|
+
settingRow(L.featureSessionSwitch, L.featureSessionSwitchDesc, f.sessionSwitch !== false, async () => {
|
|
240
|
+
const next = !(f.sessionSwitch !== false);
|
|
241
|
+
if (await setFeature('sessionSwitch', next)) f.sessionSwitch = next;
|
|
242
|
+
refreshRows();
|
|
243
|
+
}),
|
|
244
|
+
settingRow(L.featureTitleLock, L.featureTitleLockDesc, f.titleLock !== false, async () => {
|
|
245
|
+
const next = !(f.titleLock !== false);
|
|
246
|
+
if (await setFeature('titleLock', next)) f.titleLock = next;
|
|
247
|
+
refreshRows();
|
|
248
|
+
}),
|
|
249
|
+
settingRow(L.featurePinnedCollapse, L.featurePinnedCollapseDesc, f.pinnedCollapse !== false, async () => {
|
|
250
|
+
const next = !(f.pinnedCollapse !== false);
|
|
251
|
+
if (await setFeature('pinnedCollapse', next)) f.pinnedCollapse = next;
|
|
252
|
+
refreshRows();
|
|
253
|
+
}),
|
|
254
|
+
settingRow(L.featureFileActions, L.featureFileActionsDesc, f.fileActions !== false, async () => {
|
|
255
|
+
const next = !(f.fileActions !== false);
|
|
256
|
+
if (await setFeature('fileActions', next)) f.fileActions = next;
|
|
257
|
+
refreshRows();
|
|
258
|
+
}),
|
|
259
|
+
settingRow(L.featureImageCopy, L.featureImageCopyDesc, f.imageCopy !== false, async () => {
|
|
260
|
+
const next = !(f.imageCopy !== false);
|
|
261
|
+
if (await setFeature('imageCopy', next)) f.imageCopy = next;
|
|
262
|
+
refreshRows();
|
|
263
|
+
}),
|
|
264
|
+
settingRow(L.featureWsRunning, L.featureWsRunningDesc, f.wsRunningSpin !== false, async () => {
|
|
265
|
+
const next = !(f.wsRunningSpin !== false);
|
|
266
|
+
if (await setFeature('wsRunningSpin', next)) f.wsRunningSpin = next;
|
|
267
|
+
refreshRows();
|
|
268
|
+
}),
|
|
269
|
+
settingRow(L.featurePinnedExpand, L.featurePinnedExpandDesc, f.pinnedKeepCollapsed !== false, async () => {
|
|
270
|
+
const next = !(f.pinnedKeepCollapsed !== false);
|
|
271
|
+
if (await setFeature('pinnedKeepCollapsed', next)) f.pinnedKeepCollapsed = next;
|
|
272
|
+
refreshRows();
|
|
273
|
+
}),
|
|
274
|
+
settingRow(L.featureAutoUpdatePlugins, L.featureAutoUpdatePluginsDesc, f.autoUpdatePlugins === true, async () => {
|
|
275
|
+
const next = !(f.autoUpdatePlugins === true);
|
|
276
|
+
if (await setFeature('autoUpdatePlugins', next)) f.autoUpdatePlugins = next;
|
|
277
|
+
refreshRows();
|
|
278
|
+
}),
|
|
279
|
+
);
|
|
280
|
+
};
|
|
281
|
+
refreshRows();
|
|
282
|
+
|
|
283
|
+
// 底部交流卡片:QQ 群 + 微信群,同一行两列。
|
|
284
|
+
// QQ 群点击用宿主 openExternal 打开系统浏览器(宿主没该能力时不出 QQ 卡);
|
|
285
|
+
// 微信群点击把微信号复制到剪贴板,不依赖宿主能力
|
|
286
|
+
const WECHAT_ID = 'ai4only';
|
|
287
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
288
|
+
const extIcon = () => {
|
|
289
|
+
const icon = h('svg', { viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
|
|
290
|
+
'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round',
|
|
291
|
+
class: 'size-4 shrink-0 text-foreground-subtle' });
|
|
292
|
+
for (const d of ['M15 3h6v6', 'M10 14 21 3', 'M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6']) {
|
|
293
|
+
const p2 = document.createElementNS(ns, 'path');
|
|
294
|
+
p2.setAttribute('d', d);
|
|
295
|
+
icon.append(p2);
|
|
296
|
+
}
|
|
297
|
+
return icon;
|
|
298
|
+
};
|
|
299
|
+
const communityCard = (title, desc, icon, onClick) => h('div', {
|
|
300
|
+
class: 'flex cursor-pointer items-center justify-between gap-2 rounded-xl border border-border p-2 transition-colors hover:bg-surface-hover',
|
|
301
|
+
onClick,
|
|
302
|
+
},
|
|
303
|
+
h('div', { class: 'min-w-0' },
|
|
304
|
+
h('div', { class: 'truncate text-ui-sm font-medium text-foreground' }, title),
|
|
305
|
+
h('div', { class: 'mt-0.5 truncate text-ui-xs/relaxed text-foreground-subtle' }, desc)),
|
|
306
|
+
icon());
|
|
307
|
+
const communityCards = h('div', { class: 'mt-3 grid grid-cols-2 gap-2' },
|
|
308
|
+
...(typeof window !== 'undefined' && typeof window.zcode?.openExternal === 'function'
|
|
309
|
+
? [communityCard(L.qqGroupTitle, L.qqGroupDesc, extIcon, () => openExternal('https://qm.qq.com/q/WXuISJK3ug'))]
|
|
310
|
+
: []),
|
|
311
|
+
communityCard(L.wechatGroupTitle, L.wechatGroupDesc.replaceAll('{id}', WECHAT_ID),
|
|
312
|
+
() => hCopyIcon('size-4 shrink-0 text-foreground-subtle'),
|
|
313
|
+
async () => {
|
|
314
|
+
if (await copyToClipboard(WECHAT_ID)) showToast(L.wechatIdCopied);
|
|
315
|
+
}));
|
|
316
|
+
|
|
317
|
+
// 标签页切换:功能(现有内容)/ 样式调整 / 全局提示词 / 视觉代理
|
|
318
|
+
let activeTab = 'features';
|
|
319
|
+
// 插件更新卡片:显示 duanluan-zcode-plugins 市场状态,一键更新(helper /plugins/* 端点);
|
|
320
|
+
// 状态异步加载,打开弹窗不等待
|
|
321
|
+
const pluginsStatusLine = h('span', { class: 'text-ui-xs/relaxed text-foreground-subtle' }, '…');
|
|
322
|
+
const pluginsBtn = btnSmall(L.pluginsCheckNow, () => { void runPluginsUpdate(); });
|
|
323
|
+
const runPluginsUpdate = async () => {
|
|
324
|
+
pluginsBtn.disabled = true;
|
|
325
|
+
pluginsStatusLine.textContent = L.pluginsUpdating;
|
|
326
|
+
const res = await rpc('/plugins/update', { method: 'POST', body: { installMissing: true } });
|
|
327
|
+
pluginsBtn.disabled = false;
|
|
328
|
+
if (!res.ok) {
|
|
329
|
+
pluginsStatusLine.textContent = '';
|
|
330
|
+
showToast(L.pluginsUpdateFailed + ': ' + errText(res), 'error');
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
pluginsStatusLine.textContent = res.updated > 0
|
|
334
|
+
? L.pluginsUpdatedDone.replaceAll('{n}', String(res.updated))
|
|
335
|
+
: L.pluginsUpToDate;
|
|
336
|
+
showToast(pluginsStatusLine.textContent, 'success');
|
|
337
|
+
};
|
|
338
|
+
void (async () => {
|
|
339
|
+
const res = await rpc('/plugins/status');
|
|
340
|
+
if (!res.ok) { pluginsStatusLine.textContent = ''; return; }
|
|
341
|
+
pluginsStatusLine.textContent = (res.updates || []).length > 0
|
|
342
|
+
? L.pluginsUpdatesFound.replaceAll('{n}', String(res.updates.length))
|
|
343
|
+
: L.pluginsUpToDate;
|
|
344
|
+
})();
|
|
345
|
+
// 标题中的 zcode-plugins 可点击跳转插件市场仓库(依赖宿主 openExternal 打开系统浏览器)
|
|
346
|
+
const PLUGINS_REPO_URL = 'https://github.com/duanluan/zcode-plugins';
|
|
347
|
+
const repoName = 'zcode-plugins';
|
|
348
|
+
const titleParts = L.pluginsUpdateTitle.split(repoName);
|
|
349
|
+
const pluginsTitle = h('div', { class: 'truncate text-ui-sm font-medium text-foreground' });
|
|
350
|
+
if (titleParts.length === 2 && typeof window.zcode?.openExternal === 'function') {
|
|
351
|
+
pluginsTitle.append(titleParts[0],
|
|
352
|
+
h('span', {
|
|
353
|
+
class: 'cursor-pointer underline-offset-4 hover:underline',
|
|
354
|
+
title: PLUGINS_REPO_URL,
|
|
355
|
+
onClick: () => openExternal(PLUGINS_REPO_URL),
|
|
356
|
+
}, repoName),
|
|
357
|
+
titleParts[1]);
|
|
358
|
+
} else {
|
|
359
|
+
pluginsTitle.append(L.pluginsUpdateTitle);
|
|
360
|
+
}
|
|
361
|
+
const pluginsCard = h('div', { class: 'mt-3 flex items-center justify-between gap-2 rounded-xl border border-border p-2' },
|
|
362
|
+
h('div', { class: 'min-w-0' },
|
|
363
|
+
pluginsTitle,
|
|
364
|
+
h('div', { class: 'mt-0.5 truncate text-ui-xs/relaxed text-foreground-subtle' }, pluginsStatusLine)),
|
|
365
|
+
pluginsBtn);
|
|
366
|
+
const paneFeatures = h('div', { role: 'tabpanel', class: 'mt-4' },
|
|
367
|
+
h('div', {}, rows),
|
|
368
|
+
pluginsCard,
|
|
369
|
+
communityCards,
|
|
370
|
+
);
|
|
371
|
+
const paneStyles = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
372
|
+
const panePerf = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
373
|
+
const paneAgents = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
374
|
+
const paneProxy = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
375
|
+
const paneSync = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
376
|
+
const paneVision = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
377
|
+
const paneRtk = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
378
|
+
const paneHeadroom = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
379
|
+
const panes = { features: paneFeatures, styles: paneStyles, perf: panePerf, agents: paneAgents, proxy: paneProxy, sync: paneSync, vision: paneVision, headroom: paneHeadroom, rtk: paneRtk };
|
|
380
|
+
const tabDefs = [
|
|
381
|
+
['features', L.tabFeatures],
|
|
382
|
+
['styles', L.tabStyles],
|
|
383
|
+
['perf', L.tabPerformance],
|
|
384
|
+
['agents', L.tabAgents],
|
|
385
|
+
['proxy', L.tabProxy],
|
|
386
|
+
['sync', L.tabSync],
|
|
387
|
+
['vision', L.tabVision],
|
|
388
|
+
['headroom', L.tabHeadroom],
|
|
389
|
+
['rtk', L.tabRtk],
|
|
390
|
+
];
|
|
391
|
+
const tablist = h('div', { role: 'tablist', 'aria-orientation': 'horizontal', class: 'zcodepro-tablist mt-4' });
|
|
392
|
+
const renderTabs = () => tablist.replaceChildren(...tabDefs.map(([id, label]) =>
|
|
393
|
+
h('button', {
|
|
394
|
+
type: 'button', role: 'tab', class: 'zcodepro-tab',
|
|
395
|
+
'aria-selected': String(activeTab === id),
|
|
396
|
+
'data-state': activeTab === id ? 'active' : 'inactive',
|
|
397
|
+
onClick: () => switchTab(id),
|
|
398
|
+
}, label)));
|
|
399
|
+
const switchTab = (name) => {
|
|
400
|
+
activeTab = name;
|
|
401
|
+
for (const [id, pane] of Object.entries(panes)) pane.style.display = id === name ? '' : 'none';
|
|
402
|
+
renderTabs();
|
|
403
|
+
};
|
|
404
|
+
renderTabs();
|
|
405
|
+
|
|
406
|
+
// “样式调整”:一行两项、相关项同行;无描述文字,悬停名称显示 tip;
|
|
407
|
+
// 数字框滚轮/手输调节,改完即存即生效
|
|
408
|
+
const savedStyles = config.styles || {};
|
|
409
|
+
let saveTimer = null;
|
|
410
|
+
const persistStyles = (partial) => {
|
|
411
|
+
clearTimeout(saveTimer);
|
|
412
|
+
saveTimer = setTimeout(async () => {
|
|
413
|
+
const res = await rpc('/config', { method: 'POST', body: { styles: partial } });
|
|
414
|
+
clearConfigCache();
|
|
415
|
+
if (res.ok) applyStyles((res.config && res.config.styles) || savedStyles);
|
|
416
|
+
else showToast(L.failed + ': ' + errText(res), 'error');
|
|
417
|
+
}, 150);
|
|
418
|
+
};
|
|
419
|
+
// 聚焦预览接线:聚焦标出影响位置(取输入框当前值,打开弹窗后调过的值
|
|
420
|
+
// 不在 savedStyles 快照里),调值提交时黄条随新值实时变宽变窄
|
|
421
|
+
const bindHighlight = (field, key) => {
|
|
422
|
+
field.el.addEventListener('focus', () => showStyleHighlight(key, field.get()));
|
|
423
|
+
field.el.addEventListener('blur', () => hideStyleHighlight());
|
|
424
|
+
};
|
|
425
|
+
const styleCell = (name, tip, key, { min = 0, max = 48, step = 1, unit = 'px' } = {}) => {
|
|
426
|
+
const field = numberField({
|
|
427
|
+
value: typeof savedStyles[key] === 'number' ? savedStyles[key] : null,
|
|
428
|
+
fallback: STYLE_DEFAULTS[key],
|
|
429
|
+
min, max, step,
|
|
430
|
+
onCommit: (v) => {
|
|
431
|
+
persistStyles({ [key]: v });
|
|
432
|
+
showStyleHighlight(key, v);
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
bindHighlight(field, key);
|
|
436
|
+
return {
|
|
437
|
+
field,
|
|
438
|
+
el: h('div', { class: 'flex items-center justify-between gap-2 p-2' },
|
|
439
|
+
h('span', { class: 'min-w-0 truncate text-ui-sm font-medium text-foreground', title: tip }, name),
|
|
440
|
+
h('span', { class: 'flex shrink-0 items-center gap-1' }, field.el,
|
|
441
|
+
h('span', { class: 'w-3 text-ui-xs text-foreground-subtle' }, unit))),
|
|
442
|
+
};
|
|
443
|
+
};
|
|
444
|
+
// 内容宽度:数值+单位同框(px/%),交互参考 gemini-pro。
|
|
445
|
+
// 默认值不是固定数——应用按窗口宽度自适应,打开弹窗时实测当前内容列宽度作为默认显示。
|
|
446
|
+
const column = document.querySelector('[data-v4-timeline-content-column]');
|
|
447
|
+
const currentWidth = column ? Math.round(column.getBoundingClientRect().width) : 0;
|
|
448
|
+
const widthField = unitField({
|
|
449
|
+
value: savedStyles.contentWidth || null,
|
|
450
|
+
fallback: { value: currentWidth > 0 ? currentWidth : 1152, unit: 'px' },
|
|
451
|
+
onCommit: (v) => persistStyles({ contentWidth: v }),
|
|
452
|
+
});
|
|
453
|
+
bindHighlight(widthField, 'contentWidth');
|
|
454
|
+
const widthCell = {
|
|
455
|
+
field: widthField,
|
|
456
|
+
el: h('div', { class: 'flex items-center justify-between gap-2 p-2' },
|
|
457
|
+
h('span', { class: 'min-w-0 truncate text-ui-sm font-medium text-foreground', title: L.contentWidthDesc }, L.contentWidthName),
|
|
458
|
+
widthField.el),
|
|
459
|
+
};
|
|
460
|
+
// 单元格上下/左右边距:一行两个输入框(先上下后左右)
|
|
461
|
+
const cellPadField = (key) => numberField({
|
|
462
|
+
value: typeof savedStyles[key] === 'number' ? savedStyles[key] : null,
|
|
463
|
+
fallback: STYLE_DEFAULTS[key],
|
|
464
|
+
min: 0, max: 24, step: 1,
|
|
465
|
+
onCommit: (v) => { persistStyles({ [key]: v }); showStyleHighlight(key, v); },
|
|
466
|
+
});
|
|
467
|
+
const cellPadV = cellPadField('tableCellPaddingV');
|
|
468
|
+
const cellPadH = cellPadField('tableCellPaddingH');
|
|
469
|
+
bindHighlight(cellPadV, 'tableCellPaddingV');
|
|
470
|
+
bindHighlight(cellPadH, 'tableCellPaddingH');
|
|
471
|
+
const cellPadCell = {
|
|
472
|
+
field: { reset() { cellPadV.reset(); cellPadH.reset(); } },
|
|
473
|
+
el: h('div', { class: 'flex items-center justify-between gap-2 p-2' },
|
|
474
|
+
h('span', { class: 'min-w-0 truncate text-ui-sm font-medium text-foreground', title: L.tableCellPaddingDesc }, L.tableCellPaddingName),
|
|
475
|
+
h('span', { class: 'flex shrink-0 items-center gap-1' },
|
|
476
|
+
cellPadV.el, h('span', { class: 'text-ui-xs text-foreground-subtle' }, '/'),
|
|
477
|
+
cellPadH.el, h('span', { class: 'w-3 text-ui-xs text-foreground-subtle' }, 'px'))),
|
|
478
|
+
};
|
|
479
|
+
const cells = [
|
|
480
|
+
styleCell(L.sidebarProjectSpacingName, L.sidebarProjectSpacingDesc, 'sidebarProjectSpacing', { max: 24 }),
|
|
481
|
+
styleCell(L.sidebarTaskSpacingName, L.sidebarTaskSpacingDesc, 'sidebarTaskSpacing', { max: 24 }),
|
|
482
|
+
widthCell,
|
|
483
|
+
styleCell(L.rowGapName, L.rowGapDesc, 'rowGap'),
|
|
484
|
+
styleCell(L.userLineHeightName, L.userLineHeightDesc, 'userLineHeight', { min: 1, max: 3, step: 0.05, unit: 'x' }),
|
|
485
|
+
styleCell(L.lineHeightName, L.lineHeightDesc, 'lineHeight', { min: 1, max: 3, step: 0.05, unit: 'x' }),
|
|
486
|
+
styleCell(L.codeLineHeightName, L.codeLineHeightDesc, 'codeLineHeight', { min: 1, max: 3, step: 0.05, unit: 'x' }),
|
|
487
|
+
styleCell(L.listSpacingName, L.listSpacingDesc, 'listSpacing'),
|
|
488
|
+
styleCell(L.listItemSpacingName, L.listItemSpacingDesc, 'listItemSpacing'),
|
|
489
|
+
styleCell(L.quoteCodeSpacingName, L.quoteCodeSpacingDesc, 'quoteCodeSpacing'),
|
|
490
|
+
styleCell(L.tableSpacingName, L.tableSpacingDesc, 'tableSpacing'),
|
|
491
|
+
cellPadCell,
|
|
492
|
+
];
|
|
493
|
+
// “侧栏菜单并入顶栏”开关放在样式页(属界面布局调整,存仍是 features 配置);
|
|
494
|
+
// 切换后重渲本行让开关状态即时反映
|
|
495
|
+
const toolbarSwitchWrap = h('div');
|
|
496
|
+
const renderToolbarSwitch = () => {
|
|
497
|
+
const f = config.features || {};
|
|
498
|
+
toolbarSwitchWrap.replaceChildren(settingRow(L.featureToolbarIcons, L.featureToolbarIconsDesc, f.toolbarIcons !== false, async () => {
|
|
499
|
+
const next = !(f.toolbarIcons !== false);
|
|
500
|
+
if (await setFeature('toolbarIcons', next)) f.toolbarIcons = next;
|
|
501
|
+
renderToolbarSwitch();
|
|
502
|
+
}));
|
|
503
|
+
};
|
|
504
|
+
// 字体设置:界面/提问/回答三个下拉框(打开弹窗时异步拉系统字体列表,
|
|
505
|
+
// 拉不到回退常用清单);选中即保存即生效,选“默认”恢复跟随应用。
|
|
506
|
+
// 不接聚焦高亮:字体变化肉眼可辨,无需标出受影响区域
|
|
507
|
+
const fontSelectCls = 'h-8 w-full min-w-0 flex-1 rounded-lg border border-border bg-input px-2.5 text-ui-sm text-foreground outline-none transition-shadow focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40';
|
|
508
|
+
// queryLocalFonts 不可用/被拒时的回退清单(常用中西文字体)
|
|
509
|
+
const FONT_FALLBACK = ['霞鹜文楷 屏幕阅读版', '霞鹜新晰黑 屏幕阅读版', 'Noto Sans CJK SC', 'Noto Serif CJK SC', 'Noto Sans Mono CJK SC', '文泉驿微米黑', '文泉驿等宽微米黑', 'JetBrains Mono', 'Fira Code', 'Cascadia Mono', 'DejaVu Sans', 'DejaVu Serif', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Menlo', 'SF Mono', 'PingFang SC', 'Microsoft YaHei', 'SimSun', 'KaiTi', 'FangSong'];
|
|
510
|
+
const fontCell = (name, tip, key) => {
|
|
511
|
+
const saved = typeof savedStyles[key] === 'string' ? savedStyles[key].trim() : '';
|
|
512
|
+
const sel = h('select', { class: fontSelectCls, title: tip },
|
|
513
|
+
h('option', { value: '' }, L.defaultValue));
|
|
514
|
+
// 配置里存过的值(旧版手输的多字体栈等不在列表中的)单列一项,保证回显与生效一致
|
|
515
|
+
if (saved) sel.append(h('option', { value: saved }, saved));
|
|
516
|
+
sel.value = saved;
|
|
517
|
+
sel.addEventListener('change', () => persistStyles({ [key]: sel.value || null }));
|
|
518
|
+
return {
|
|
519
|
+
field: { reset() { sel.value = ''; } },
|
|
520
|
+
el: h('div', { class: 'flex items-center justify-between gap-2 p-2' },
|
|
521
|
+
h('span', { class: 'w-24 shrink-0 truncate text-ui-sm font-medium text-foreground', title: tip }, name),
|
|
522
|
+
sel),
|
|
523
|
+
setOptions(families) {
|
|
524
|
+
const cur = sel.value;
|
|
525
|
+
const opts = [h('option', { value: '' }, L.defaultValue)];
|
|
526
|
+
if (cur && !families.includes(cur)) opts.push(h('option', { value: cur }, cur));
|
|
527
|
+
for (const fam of families) {
|
|
528
|
+
const opt = h('option', { value: fam }, fam);
|
|
529
|
+
// 选项文字用自身字体渲染,选中前即可预览字形
|
|
530
|
+
opt.style.fontFamily = fam;
|
|
531
|
+
opts.push(opt);
|
|
532
|
+
}
|
|
533
|
+
sel.replaceChildren(...opts);
|
|
534
|
+
sel.value = cur;
|
|
535
|
+
},
|
|
536
|
+
};
|
|
537
|
+
};
|
|
538
|
+
const fontCells = [
|
|
539
|
+
fontCell(L.uiFontName, L.uiFontDesc, 'uiFont'),
|
|
540
|
+
fontCell(L.userFontName, L.userFontDesc, 'userFont'),
|
|
541
|
+
fontCell(L.assistantFontName, L.assistantFontDesc, 'assistantFont'),
|
|
542
|
+
];
|
|
543
|
+
void (async () => {
|
|
544
|
+
let families = null;
|
|
545
|
+
try {
|
|
546
|
+
if (typeof window.queryLocalFonts === 'function') {
|
|
547
|
+
const fonts = await window.queryLocalFonts();
|
|
548
|
+
families = [...new Set(fonts.map((f) => f.family))].sort((a, b) => a.localeCompare(b, 'zh'));
|
|
549
|
+
}
|
|
550
|
+
} catch { /* 权限被拒等:走回退清单 */ }
|
|
551
|
+
const list = ['sans-serif', 'serif', 'monospace', ...(families || FONT_FALLBACK)];
|
|
552
|
+
for (const c of fontCells) c.setOptions(list);
|
|
553
|
+
})();
|
|
554
|
+
renderToolbarSwitch();
|
|
555
|
+
paneStyles.append(
|
|
556
|
+
toolbarSwitchWrap,
|
|
557
|
+
h('div', { class: 'mt-2 flex flex-col gap-0.5 rounded-xl border border-border p-1' },
|
|
558
|
+
...fontCells.map((c) => h('div', { class: 'rounded-lg transition-colors hover:bg-surface-hover' }, c.el))),
|
|
559
|
+
h('div', { class: 'mt-2 grid grid-cols-2 gap-2 rounded-xl border border-border p-1' },
|
|
560
|
+
...cells.map((c) => h('div', { class: 'rounded-lg transition-colors hover:bg-surface-hover' }, c.el))),
|
|
561
|
+
h('div', { class: 'mt-2 flex justify-end' },
|
|
562
|
+
btnSmall(L.resetDefault, () => {
|
|
563
|
+
for (const c of [...cells, ...fontCells]) c.field.reset();
|
|
564
|
+
persistStyles(Object.fromEntries(Object.keys(STYLE_DEFAULTS).map((k) => [k, null])));
|
|
565
|
+
})),
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
// “全局提示词”:编辑 ~/.zcode/AGENTS.md(helper /agents 端点读写)。
|
|
569
|
+
// 大段文本不做即存即生效——显式保存;未修改时保存按钮禁用
|
|
570
|
+
const agentsArea = h('textarea', {
|
|
571
|
+
class: 'zcodepro-textarea',
|
|
572
|
+
placeholder: L.agentsPlaceholder,
|
|
573
|
+
spellcheck: 'false',
|
|
574
|
+
});
|
|
575
|
+
const agentsSaveBtn = btnSmall(L.agentsSave, () => { void saveAgents(); }, '', 'primary');
|
|
576
|
+
let agentsOriginal = '';
|
|
577
|
+
let agentsFailed = false;
|
|
578
|
+
if (agentsRes.ok) {
|
|
579
|
+
agentsOriginal = agentsRes.content || '';
|
|
580
|
+
agentsArea.value = agentsOriginal;
|
|
581
|
+
} else {
|
|
582
|
+
agentsFailed = true;
|
|
583
|
+
agentsArea.disabled = true;
|
|
584
|
+
}
|
|
585
|
+
agentsSaveBtn.disabled = true;
|
|
586
|
+
agentsArea.addEventListener('input', () => {
|
|
587
|
+
agentsSaveBtn.disabled = agentsFailed || agentsArea.value === agentsOriginal;
|
|
588
|
+
});
|
|
589
|
+
const saveAgents = async () => {
|
|
590
|
+
agentsSaveBtn.disabled = true;
|
|
591
|
+
const res = await rpc('/agents', { method: 'POST', body: { content: agentsArea.value } });
|
|
592
|
+
if (res.ok) {
|
|
593
|
+
agentsOriginal = typeof res.content === 'string' ? res.content : agentsArea.value;
|
|
594
|
+
showToast(L.agentsSaved);
|
|
595
|
+
} else {
|
|
596
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
597
|
+
agentsSaveBtn.disabled = false;
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
paneAgents.append(
|
|
601
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.agentsDesc),
|
|
602
|
+
...(agentsFailed
|
|
603
|
+
? [h('p', { class: 'mt-2 text-ui-sm text-destructive' }, L.agentsLoadFailed + ': ' + errText(agentsRes))]
|
|
604
|
+
: []),
|
|
605
|
+
h('div', { class: 'mt-3' }, agentsArea),
|
|
606
|
+
h('div', { class: 'mt-3 flex justify-end' }, agentsSaveBtn),
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
// “代理”:为 helper 发起的网络访问(插件市场更新/安装、headroom 本体升级)设
|
|
610
|
+
// HTTP 代理;保存即生效(helper 子进程环境变量注入),不影响 ZCode 应用与模型请求。
|
|
611
|
+
// “检测”经 curl -x 走代理访问 GitHub / PyPI,测输入框当前的地址(可先测再存)
|
|
612
|
+
const savedProxy = typeof config.proxy === 'string' ? config.proxy : '';
|
|
613
|
+
const proxyInputCls = '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';
|
|
614
|
+
const proxyInput = h('input', { type: 'text', value: savedProxy, placeholder: L.proxyPlaceholder, spellcheck: 'false', class: proxyInputCls + ' min-w-0 flex-1' });
|
|
615
|
+
const proxySaveBtn = btnSmall(L.proxySave, () => { void saveProxy(); }, 'shrink-0', 'primary');
|
|
616
|
+
let proxyOriginal = savedProxy;
|
|
617
|
+
proxySaveBtn.disabled = true;
|
|
618
|
+
proxyInput.addEventListener('input', () => {
|
|
619
|
+
proxySaveBtn.disabled = proxyInput.value.trim() === proxyOriginal;
|
|
620
|
+
});
|
|
621
|
+
const saveProxy = async () => {
|
|
622
|
+
proxySaveBtn.disabled = true;
|
|
623
|
+
const res = await rpc('/config', { method: 'POST', body: { proxy: proxyInput.value.trim() } });
|
|
624
|
+
if (res.ok) {
|
|
625
|
+
proxyOriginal = res.config && typeof res.config.proxy === 'string' ? res.config.proxy : proxyInput.value.trim();
|
|
626
|
+
proxyInput.value = proxyOriginal;
|
|
627
|
+
showToast(L.proxySaved);
|
|
628
|
+
} else {
|
|
629
|
+
proxySaveBtn.disabled = false;
|
|
630
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
const proxyTestState = h('p', { class: 'mt-2 text-ui-xs/relaxed text-foreground-subtle' });
|
|
634
|
+
let proxyTesting = false;
|
|
635
|
+
const proxyTestBtn = btnSmall(L.proxyTest, () => { void runProxyTest(); }, 'shrink-0');
|
|
636
|
+
const runProxyTest = async () => {
|
|
637
|
+
if (proxyTesting) return;
|
|
638
|
+
proxyTesting = true;
|
|
639
|
+
proxyTestBtn.disabled = true;
|
|
640
|
+
proxyTestBtn.textContent = L.proxyTesting;
|
|
641
|
+
proxyTestState.textContent = '';
|
|
642
|
+
proxyTestState.className = 'mt-2 text-ui-xs/relaxed text-foreground-subtle';
|
|
643
|
+
const res = await rpc('/proxy/test', { method: 'POST', body: { proxy: proxyInput.value.trim() } });
|
|
644
|
+
proxyTesting = false;
|
|
645
|
+
proxyTestBtn.disabled = false;
|
|
646
|
+
proxyTestBtn.textContent = L.proxyTest;
|
|
647
|
+
if (!res.ok) {
|
|
648
|
+
proxyTestState.className = 'mt-2 text-ui-xs/relaxed text-destructive';
|
|
649
|
+
proxyTestState.textContent = L.proxyTestFailed + ': ' + errText(res);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const parts = (res.targets || []).map((t) =>
|
|
653
|
+
`${t.name} ${t.ok ? '✓ ' + (t.ms != null ? t.ms + 'ms' : '') : '✗ ' + (t.error || t.httpCode || '')}`.trim());
|
|
654
|
+
const allOk = (res.targets || []).length > 0 && (res.targets || []).every((t) => t.ok);
|
|
655
|
+
proxyTestState.className = 'mt-2 text-ui-xs/relaxed ' + (allOk ? 'text-foreground-subtle' : 'text-amber-500');
|
|
656
|
+
proxyTestState.textContent = `${res.proxy}:${parts.join(' · ')}`;
|
|
657
|
+
};
|
|
658
|
+
paneProxy.append(
|
|
659
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.proxyDesc),
|
|
660
|
+
h('div', { class: 'mt-3 flex items-center gap-2' },
|
|
661
|
+
proxyInput,
|
|
662
|
+
proxySaveBtn,
|
|
663
|
+
proxyTestBtn),
|
|
664
|
+
h('p', { class: 'mt-2 text-ui-xs/relaxed text-foreground-subtle' }, L.proxyHint),
|
|
665
|
+
proxyTestState,
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
// “性能”:空闲项目内存回收(开关 + 两个时长)。idleReclaimStartupMinutes
|
|
669
|
+
// 允许 0(启动后立即回收一次),取值不能用“|| 默认值”的写法回退
|
|
670
|
+
const perfToggleWrap = h('div');
|
|
671
|
+
const renderPerfToggle = () => {
|
|
672
|
+
const f = config.features || {};
|
|
673
|
+
perfToggleWrap.replaceChildren(
|
|
674
|
+
settingRow(L.featureIdleReclaim, L.featureIdleReclaimDesc, f.idleReclaim !== false, async () => {
|
|
675
|
+
const next = !(f.idleReclaim !== false);
|
|
676
|
+
if (await setFeature('idleReclaim', next)) f.idleReclaim = next;
|
|
677
|
+
renderPerfToggle();
|
|
678
|
+
}),
|
|
679
|
+
);
|
|
680
|
+
};
|
|
681
|
+
renderPerfToggle();
|
|
682
|
+
const perfNumberRow = (labelText, tip, key, fallback, min) => {
|
|
683
|
+
const v = Number(config[key]);
|
|
684
|
+
return h('div', { class: 'flex items-center justify-between gap-2 px-2 py-1.5' },
|
|
685
|
+
h('div', { class: 'min-w-0' },
|
|
686
|
+
h('div', { class: 'text-ui-sm font-medium text-foreground' }, labelText),
|
|
687
|
+
...(tip ? [h('div', { class: 'mt-0.5 text-ui-xs/relaxed text-foreground-subtle' }, tip)] : [])),
|
|
688
|
+
h('div', { class: 'flex shrink-0 items-center gap-1' },
|
|
689
|
+
numberField({
|
|
690
|
+
value: Number.isFinite(v) ? v : fallback,
|
|
691
|
+
fallback,
|
|
692
|
+
min,
|
|
693
|
+
max: 1440,
|
|
694
|
+
step: 1,
|
|
695
|
+
onCommit: async (n) => {
|
|
696
|
+
const res = await rpc('/config', { method: 'POST', body: { [key]: n } });
|
|
697
|
+
if (res.ok && res.config) {
|
|
698
|
+
config[key] = res.config[key];
|
|
699
|
+
clearConfigCache();
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
}).el,
|
|
703
|
+
h('span', { class: 'text-ui-xs text-foreground-subtle' }, L.idleReclaimUnit)));
|
|
704
|
+
};
|
|
705
|
+
panePerf.append(
|
|
706
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.perfDesc),
|
|
707
|
+
h('div', { class: 'mt-3 divide-y divide-border rounded-xl border border-border' },
|
|
708
|
+
perfToggleWrap,
|
|
709
|
+
perfNumberRow(L.idleReclaimMinutesLabel, null, 'idleReclaimMinutes', 30, 1),
|
|
710
|
+
perfNumberRow(L.idleReclaimStartupLabel, L.idleReclaimStartupHint, 'idleReclaimStartupMinutes', 5, 0)),
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
// “同步”:把 ZCode Pro 设置与模型设置手动备份到 WebDAV / 从 WebDAV 恢复
|
|
714
|
+
// (交互参考 Tampermonkey:URL/登录/密码 + 手动触发,无自动同步)。仅 WebDAV
|
|
715
|
+
// 一种后端且本就只在点击时传输,不设“类型/禁用”下拉。表单显式保存;
|
|
716
|
+
// 同步内容开关即时保存;下载会覆盖本机,先弹二次确认
|
|
717
|
+
const savedSync = (config.sync && typeof config.sync === 'object') ? config.sync
|
|
718
|
+
: { url: '', dir: 'zcode-pro', login: '', password: '', include: { zcodepro: true, model: true }, lastPushAt: null, lastPullAt: null };
|
|
719
|
+
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';
|
|
720
|
+
const syncFormRow = (labelText, node) => h('div', { class: 'flex items-center gap-3 px-2 py-1.5' },
|
|
721
|
+
h('span', { class: 'w-20 shrink-0 text-ui-sm font-medium text-foreground' }, labelText), node);
|
|
722
|
+
const syncUrlInput = h('input', { type: 'text', value: savedSync.url || '', placeholder: L.syncUrlPlaceholder, spellcheck: 'false', autocomplete: 'off', class: syncInputCls + ' min-w-0 flex-1' });
|
|
723
|
+
// 上传目录:拼在 URL 之后的文件夹(默认 zcode-pro,避免直接写进网盘根目录),可多级
|
|
724
|
+
const syncDirInput = h('input', { type: 'text', value: savedSync.dir || 'zcode-pro', placeholder: L.syncDirPlaceholder, spellcheck: 'false', autocomplete: 'off', class: syncInputCls + ' min-w-0 flex-1' });
|
|
725
|
+
const syncLoginInput = h('input', { type: 'text', value: savedSync.login || '', spellcheck: 'false', autocomplete: 'off', class: syncInputCls + ' min-w-0 flex-1' });
|
|
726
|
+
const syncPasswordInput = h('input', { type: 'password', value: savedSync.password || '', autocomplete: 'new-password', class: syncInputCls + ' min-w-0 flex-1' });
|
|
727
|
+
let syncFormOriginal = null;
|
|
728
|
+
const syncSaveBtn = btnSmall(L.syncSave, () => { void saveSyncForm(); }, 'shrink-0', 'primary');
|
|
729
|
+
const syncFormValues = () => ({
|
|
730
|
+
url: syncUrlInput.value.trim(),
|
|
731
|
+
dir: syncDirInput.value.trim(),
|
|
732
|
+
login: syncLoginInput.value.trim(),
|
|
733
|
+
password: syncPasswordInput.value,
|
|
734
|
+
});
|
|
735
|
+
const syncCheckDirty = () => {
|
|
736
|
+
const c = syncFormValues();
|
|
737
|
+
syncSaveBtn.disabled = c.url === syncFormOriginal.url && c.dir === syncFormOriginal.dir
|
|
738
|
+
&& c.login === syncFormOriginal.login && c.password === syncFormOriginal.password;
|
|
739
|
+
};
|
|
740
|
+
syncFormOriginal = syncFormValues();
|
|
741
|
+
syncSaveBtn.disabled = true;
|
|
742
|
+
for (const el of [syncUrlInput, syncDirInput, syncLoginInput, syncPasswordInput]) {
|
|
743
|
+
el.addEventListener('input', syncCheckDirty);
|
|
744
|
+
}
|
|
745
|
+
const saveSyncForm = async () => {
|
|
746
|
+
syncSaveBtn.disabled = true;
|
|
747
|
+
const res = await rpc('/sync', { method: 'POST', body: { sync: syncFormValues() } });
|
|
748
|
+
if (res.ok) {
|
|
749
|
+
syncFormOriginal = {
|
|
750
|
+
url: res.sync.url,
|
|
751
|
+
dir: res.sync.dir,
|
|
752
|
+
login: res.sync.login, password: res.sync.password,
|
|
753
|
+
};
|
|
754
|
+
syncUrlInput.value = res.sync.url;
|
|
755
|
+
syncDirInput.value = res.sync.dir;
|
|
756
|
+
syncLoginInput.value = res.sync.login;
|
|
757
|
+
syncPasswordInput.value = res.sync.password;
|
|
758
|
+
showToast(L.syncSaved);
|
|
759
|
+
} else {
|
|
760
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
761
|
+
syncCheckDirty();
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
// 坚果云推荐行:外链经宿主 openExternal 打开系统浏览器(无该能力时只出纯文字)
|
|
765
|
+
const JIANGUO_HELP = 'https://help.jianguoyun.com/?p=2064';
|
|
766
|
+
const syncRecLine = h('p', { class: 'mt-1 text-ui-xs/relaxed text-foreground-subtle' });
|
|
767
|
+
if (typeof window.zcode?.openExternal === 'function') {
|
|
768
|
+
syncRecLine.append(
|
|
769
|
+
h('span', {
|
|
770
|
+
class: 'cursor-pointer underline-offset-4 hover:underline',
|
|
771
|
+
title: JIANGUO_HELP,
|
|
772
|
+
onClick: () => openExternal(JIANGUO_HELP),
|
|
773
|
+
}, L.syncRecLink),
|
|
774
|
+
L.syncRecSuffix);
|
|
775
|
+
} else {
|
|
776
|
+
syncRecLine.textContent = L.syncRecLink + L.syncRecSuffix;
|
|
777
|
+
}
|
|
778
|
+
// 同步内容开关(即时保存,上传/下载都按这里的勾选执行)
|
|
779
|
+
const persistSyncInclude = async (partial) => {
|
|
780
|
+
const res = await rpc('/sync', { method: 'POST', body: { include: partial } });
|
|
781
|
+
if (!res.ok) showToast(L.failed + ': ' + errText(res), 'error');
|
|
782
|
+
return res.ok;
|
|
783
|
+
};
|
|
784
|
+
const syncIncludeWrap = h('div');
|
|
785
|
+
const renderSyncInclude = () => {
|
|
786
|
+
const inc = savedSync.include || {};
|
|
787
|
+
syncIncludeWrap.replaceChildren(
|
|
788
|
+
settingRow(L.syncIncludeZcp, L.syncIncludeZcpDesc, inc.zcodepro !== false, async () => {
|
|
789
|
+
const next = !(inc.zcodepro !== false);
|
|
790
|
+
if (await persistSyncInclude({ zcodepro: next })) inc.zcodepro = next;
|
|
791
|
+
renderSyncInclude();
|
|
792
|
+
}),
|
|
793
|
+
settingRow(L.syncIncludeModel, L.syncIncludeModelDesc, inc.model !== false, async () => {
|
|
794
|
+
const next = !(inc.model !== false);
|
|
795
|
+
if (await persistSyncInclude({ model: next })) inc.model = next;
|
|
796
|
+
renderSyncInclude();
|
|
797
|
+
}),
|
|
798
|
+
);
|
|
799
|
+
};
|
|
800
|
+
renderSyncInclude();
|
|
801
|
+
// 上次操作时间 + 上传/下载按钮;下载覆盖本机,需二次确认
|
|
802
|
+
const fmtSyncTime = (ts) => (ts ? new Date(ts).toLocaleString() : L.syncNever);
|
|
803
|
+
const syncStatusLine = h('div', { class: 'text-ui-xs/relaxed text-foreground-subtle' });
|
|
804
|
+
const renderSyncStatus = (s) => {
|
|
805
|
+
syncStatusLine.textContent = `${L.syncLastPush} ${fmtSyncTime(s.lastPushAt)} · ${L.syncLastPull} ${fmtSyncTime(s.lastPullAt)}`;
|
|
806
|
+
};
|
|
807
|
+
renderSyncStatus(savedSync);
|
|
808
|
+
const syncUploadBtn = btnSmall(L.syncUpload, () => { void runSyncDirection('push'); }, 'shrink-0', 'primary');
|
|
809
|
+
const syncDownloadBtn = btnSmall(L.syncDownload, () => { confirmSyncDownload(); }, 'shrink-0');
|
|
810
|
+
const confirmSyncDownload = () => {
|
|
811
|
+
openDialog({
|
|
812
|
+
title: L.syncConfirmTitle,
|
|
813
|
+
description: L.syncConfirmDesc,
|
|
814
|
+
width: 'sm:max-w-md',
|
|
815
|
+
onMount: ({ body: confirmBody, close: closeConfirm }) => {
|
|
816
|
+
confirmBody.append(
|
|
817
|
+
dialogFooter(
|
|
818
|
+
btnSecondary(L.cancel, () => closeConfirm()),
|
|
819
|
+
btnPrimary(L.syncConfirmGo, () => { closeConfirm(); void runSyncDirection('pull'); }, 'min-w-24'),
|
|
820
|
+
),
|
|
821
|
+
);
|
|
822
|
+
},
|
|
823
|
+
});
|
|
824
|
+
};
|
|
825
|
+
const runSyncDirection = async (direction) => {
|
|
826
|
+
syncUploadBtn.disabled = true;
|
|
827
|
+
syncDownloadBtn.disabled = true;
|
|
828
|
+
const btn = direction === 'push' ? syncUploadBtn : syncDownloadBtn;
|
|
829
|
+
const idleText = btn.textContent;
|
|
830
|
+
btn.textContent = direction === 'push' ? L.syncUploading : L.syncDownloading;
|
|
831
|
+
const res = await rpc('/sync/run', { method: 'POST', body: { direction } });
|
|
832
|
+
syncUploadBtn.disabled = false;
|
|
833
|
+
syncDownloadBtn.disabled = false;
|
|
834
|
+
btn.textContent = idleText;
|
|
835
|
+
if (!res.ok) {
|
|
836
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
renderSyncStatus(res.sync || {});
|
|
840
|
+
if (direction === 'pull') {
|
|
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
|
+
}
|
|
856
|
+
};
|
|
857
|
+
paneSync.append(
|
|
858
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.syncDesc),
|
|
859
|
+
h('div', { class: 'mt-3 rounded-xl border border-border p-1' },
|
|
860
|
+
syncFormRow('URL', syncUrlInput),
|
|
861
|
+
syncFormRow(L.syncDir, syncDirInput),
|
|
862
|
+
syncFormRow(L.syncLogin, syncLoginInput),
|
|
863
|
+
syncFormRow(L.syncPassword, syncPasswordInput),
|
|
864
|
+
h('div', { class: 'flex items-start justify-between gap-2 px-2 py-1.5' },
|
|
865
|
+
h('div', { class: 'min-w-0' },
|
|
866
|
+
h('p', { class: 'text-ui-xs/relaxed text-foreground-subtle' }, L.syncFormHint),
|
|
867
|
+
syncRecLine),
|
|
868
|
+
syncSaveBtn)),
|
|
869
|
+
h('div', { class: 'mt-2 divide-y divide-border rounded-xl border border-border' }, syncIncludeWrap),
|
|
870
|
+
h('div', { class: 'mt-2 flex items-center justify-between gap-2 rounded-xl border border-border p-2' },
|
|
871
|
+
h('div', { class: 'min-w-0' }, syncStatusLine),
|
|
872
|
+
h('div', { class: 'flex shrink-0 items-center gap-1.5' }, syncUploadBtn, syncDownloadBtn)),
|
|
873
|
+
h('p', { class: 'mt-2 text-ui-xs/relaxed text-foreground-subtle' }, L.syncHint),
|
|
874
|
+
);
|
|
875
|
+
|
|
876
|
+
// “视觉代理”:编辑 ~/.zcode/zcode-vision.json(zcode-vision 插件与 /vision-* 命令共用同一文件)。
|
|
877
|
+
// 代理列表顺序即执行链;结构性改动(开关/模式/排序/删增)即时保存,文本输入防抖保存。
|
|
878
|
+
// 注意:保存时链按全部代理重建——若用 /vision-chain 配过子集链,会被这里覆盖(两套入口语义如此,面板以列表为准)。
|
|
879
|
+
// 默认两级链与 zcode-vision 插件 DEFAULT_CONFIG 保持一致:glm-session(跟随会话)→ glm-flash(GLM 订阅直连)。
|
|
880
|
+
// 赠送额度级 trust-build 暂不进默认:ZCode 网关不放行无人值守调用(ADR-0002)。
|
|
881
|
+
const VISION_DEFAULT_PROMPT = '请详细描述这张图片的全部内容。若是界面或图表截图,请先把所有错误、警告、异常状态逐字引用出来(含完整原文),再描述整体布局、文字与关键数据。';
|
|
882
|
+
const VISION_DEFAULT_CFG = {
|
|
883
|
+
enabled: true,
|
|
884
|
+
chainMode: 'fallback',
|
|
885
|
+
chain: ['glm-session', 'glm-flash'],
|
|
886
|
+
proxies: [
|
|
887
|
+
{ name: 'glm-session', useProvider: 'session', model: 'glm-5.3-flash', prompt: VISION_DEFAULT_PROMPT },
|
|
888
|
+
{ name: 'glm-flash', baseUrl: 'https://open.bigmodel.cn/api/anthropic', model: 'glm-5.3-flash', apiKey: '', format: 'anthropic', prompt: VISION_DEFAULT_PROMPT },
|
|
889
|
+
],
|
|
890
|
+
pollMs: 3000,
|
|
891
|
+
apiTimeoutMs: 120000, // 视觉上游冷启动可能近 2 分钟,与插件 DEFAULT_CONFIG 一致
|
|
892
|
+
compressThresholdKB: 1024,
|
|
893
|
+
skipAfterFailures: 4,
|
|
894
|
+
skipMinutes: 30,
|
|
895
|
+
forceIntercept: true,
|
|
896
|
+
};
|
|
897
|
+
// 供应商下拉数据:合并 zcode 两张供应商表(config.json 的 provider 与 provider_config.json 的 providerRules)
|
|
898
|
+
const visionProviders = visionProvidersRes.ok && Array.isArray(visionProvidersRes.providers) ? visionProvidersRes.providers : [];
|
|
899
|
+
let visionCfg = visionRes.ok && visionRes.config && typeof visionRes.config === 'object'
|
|
900
|
+
? structuredClone(visionRes.config)
|
|
901
|
+
: null;
|
|
902
|
+
let visionSaveTimer = null;
|
|
903
|
+
const persistVision = async () => {
|
|
904
|
+
visionCfg.chain = visionCfg.proxies.map((p) => (p.name || '').trim()).filter(Boolean);
|
|
905
|
+
const res = await rpc('/vision', { method: 'POST', body: { config: visionCfg } });
|
|
906
|
+
if (!res.ok) showToast(L.failed + ': ' + errText(res), 'error');
|
|
907
|
+
return res.ok;
|
|
908
|
+
};
|
|
909
|
+
const saveVisionSoon = () => {
|
|
910
|
+
clearTimeout(visionSaveTimer);
|
|
911
|
+
visionSaveTimer = setTimeout(() => { void persistVision(); }, 500);
|
|
912
|
+
};
|
|
913
|
+
const visionTestPre = h('pre', {
|
|
914
|
+
class: 'mt-2 max-h-56 overflow-auto whitespace-pre-wrap rounded-lg border border-border bg-surface p-2 text-ui-xs text-foreground-subtle',
|
|
915
|
+
style: 'display:none',
|
|
916
|
+
});
|
|
917
|
+
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';
|
|
918
|
+
const renderVision = () => {
|
|
919
|
+
if (!visionCfg) {
|
|
920
|
+
const initBtn = btnSmall(L.visionAddProxy, async () => {
|
|
921
|
+
const res = await rpc('/vision', { method: 'POST', body: { config: VISION_DEFAULT_CFG } });
|
|
922
|
+
if (res.ok) { visionCfg = structuredClone(res.config); renderVision(); }
|
|
923
|
+
else showToast(L.failed + ': ' + errText(res), 'error');
|
|
924
|
+
});
|
|
925
|
+
paneVision.replaceChildren(
|
|
926
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.visionDesc),
|
|
927
|
+
...(visionRes.ok ? [] : [h('p', { class: 'mt-2 text-ui-sm text-destructive' }, L.visionLoadFailed + ': ' + errText(visionRes))]),
|
|
928
|
+
h('div', { class: 'mt-3 flex justify-end' }, initBtn),
|
|
929
|
+
);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
const field = (labelText, node) => h('label', { class: 'block min-w-0' },
|
|
933
|
+
h('span', { class: 'mb-1 block text-ui-xs font-medium text-foreground-subtle' }, labelText), node);
|
|
934
|
+
const smallBtn = (text, onClick, extra = '') => btnSmall(text, onClick, extra);
|
|
935
|
+
const modeBtn = (id, label, desc) => h('button', {
|
|
936
|
+
type: 'button',
|
|
937
|
+
class: 'zcodepro-tab',
|
|
938
|
+
'data-state': visionCfg.chainMode === id ? 'active' : 'inactive',
|
|
939
|
+
title: desc,
|
|
940
|
+
onClick: () => { visionCfg.chainMode = id; void persistVision(); renderVision(); },
|
|
941
|
+
}, label);
|
|
942
|
+
const textIn = (value, onInput, { type = 'text', placeholder = '' } = {}) => {
|
|
943
|
+
const n = h('input', { type, value: value || '', placeholder, class: visionInputCls });
|
|
944
|
+
n.addEventListener('input', () => onInput(n.value));
|
|
945
|
+
return n;
|
|
946
|
+
};
|
|
947
|
+
const numIn = (value, { min, dflt, title, label, commit }) => {
|
|
948
|
+
const n = h('input', { type: 'number', min: String(min), step: '1', title,
|
|
949
|
+
class: 'h-7 w-16 rounded-lg border border-border bg-input px-2 text-right text-ui-xs tabular-nums text-foreground outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40' });
|
|
950
|
+
n.value = String(Number.isFinite(value) ? value : dflt);
|
|
951
|
+
n.addEventListener('change', () => {
|
|
952
|
+
const v = Math.max(min, Math.round(Number(n.value) || 0));
|
|
953
|
+
n.value = String(v);
|
|
954
|
+
commit(v);
|
|
955
|
+
void persistVision();
|
|
956
|
+
});
|
|
957
|
+
return h('label', { class: 'flex shrink-0 items-center gap-1.5', title },
|
|
958
|
+
h('span', { class: 'text-ui-xs font-medium text-foreground-subtle' }, label), n);
|
|
959
|
+
};
|
|
960
|
+
const cards = visionCfg.proxies.map((p, i) => h('div', { class: 'mt-2 rounded-xl border border-border p-2' },
|
|
961
|
+
h('div', { class: 'flex flex-wrap items-center gap-2' },
|
|
962
|
+
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)),
|
|
963
|
+
// 名称输入框用 flex-1 占剩余宽度,与序号同行;textIn 的 w-full 会把序号挤成单独一行
|
|
964
|
+
(() => {
|
|
965
|
+
const n = h('input', { type: 'text', value: p.name || '', placeholder: L.visionName, class: visionInputCls + ' min-w-0 flex-1' });
|
|
966
|
+
n.addEventListener('input', () => { p.name = n.value; saveVisionSoon(); });
|
|
967
|
+
return n;
|
|
968
|
+
})(),
|
|
969
|
+
(() => {
|
|
970
|
+
const sel = h('select', { class: 'h-7 shrink-0 rounded-lg border border-border bg-input px-1.5 text-ui-xs text-foreground outline-none' },
|
|
971
|
+
h('option', { value: 'anthropic' }, 'anthropic'),
|
|
972
|
+
h('option', { value: 'openai' }, 'openai'));
|
|
973
|
+
sel.value = p.format === 'openai' ? 'openai' : 'anthropic';
|
|
974
|
+
sel.addEventListener('change', () => { p.format = sel.value; void persistVision(); });
|
|
975
|
+
return sel;
|
|
976
|
+
})(),
|
|
977
|
+
smallBtn(L.visionMoveUp, () => {
|
|
978
|
+
if (i <= 0) return;
|
|
979
|
+
visionCfg.proxies.splice(i - 1, 0, visionCfg.proxies.splice(i, 1)[0]);
|
|
980
|
+
void persistVision(); renderVision();
|
|
981
|
+
}, i === 0 ? 'pointer-events-none opacity-40' : ''),
|
|
982
|
+
smallBtn(L.visionMoveDown, () => {
|
|
983
|
+
if (i >= visionCfg.proxies.length - 1) return;
|
|
984
|
+
visionCfg.proxies.splice(i + 1, 0, visionCfg.proxies.splice(i, 1)[0]);
|
|
985
|
+
void persistVision(); renderVision();
|
|
986
|
+
}, i === visionCfg.proxies.length - 1 ? 'pointer-events-none opacity-40' : ''),
|
|
987
|
+
smallBtn(L.visionRemove, () => { visionCfg.proxies.splice(i, 1); void persistVision(); renderVision(); }),
|
|
988
|
+
),
|
|
989
|
+
h('div', { class: 'mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2' },
|
|
990
|
+
field(L.visionUseProvider, (() => {
|
|
991
|
+
// 组合框:下拉(不跟随/session/全部供应商)+ 手动输入;手输失焦校验,未知供应商给提示
|
|
992
|
+
const wrap = h('div', { class: 'flex flex-col gap-1' });
|
|
993
|
+
const input = h('input', { type: 'text', value: p.useProvider || '', placeholder: L.visionUseProviderHint, class: visionInputCls });
|
|
994
|
+
const warn = h('p', { class: 'text-ui-xs text-destructive', style: 'display:none' }, L.visionUseProviderUnknown);
|
|
995
|
+
const isKnown = (v) => !v || v === 'session'
|
|
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';
|
|
1012
|
+
saveVisionSoon();
|
|
1013
|
+
});
|
|
1014
|
+
input.addEventListener('input', () => { p.useProvider = input.value.trim(); saveVisionSoon(); });
|
|
1015
|
+
input.addEventListener('blur', () => {
|
|
1016
|
+
p.useProvider = input.value.trim();
|
|
1017
|
+
warn.style.display = isKnown(p.useProvider) ? 'none' : '';
|
|
1018
|
+
syncSelect();
|
|
1019
|
+
});
|
|
1020
|
+
syncSelect();
|
|
1021
|
+
wrap.append(sel, input, warn);
|
|
1022
|
+
return wrap;
|
|
1023
|
+
})()),
|
|
1024
|
+
field(L.visionModel, textIn(p.model, (v) => { p.model = v; saveVisionSoon(); })),
|
|
1025
|
+
field(L.visionBaseUrl, textIn(p.baseUrl, (v) => { p.baseUrl = v; saveVisionSoon(); })),
|
|
1026
|
+
field(L.visionApiKey + ' · ' + L.visionApiKeyHint, textIn(p.apiKey, (v) => { p.apiKey = v; saveVisionSoon(); }, { type: 'password' })),
|
|
1027
|
+
),
|
|
1028
|
+
(() => {
|
|
1029
|
+
const n = h('textarea', { class: 'zcodepro-textarea zcodepro-textarea-sm', rows: '3', placeholder: L.visionPrompt });
|
|
1030
|
+
n.value = p.prompt || '';
|
|
1031
|
+
n.addEventListener('input', () => { p.prompt = n.value; saveVisionSoon(); });
|
|
1032
|
+
return field(L.visionPrompt, n);
|
|
1033
|
+
})(),
|
|
1034
|
+
));
|
|
1035
|
+
let testBtn;
|
|
1036
|
+
testBtn = smallBtn(L.visionTest, async () => {
|
|
1037
|
+
testBtn.disabled = true;
|
|
1038
|
+
testBtn.textContent = L.visionTesting;
|
|
1039
|
+
visionTestPre.style.display = '';
|
|
1040
|
+
visionTestPre.textContent = L.visionTesting;
|
|
1041
|
+
const res = await rpc('/vision/test', { method: 'POST', body: {} });
|
|
1042
|
+
testBtn.disabled = false;
|
|
1043
|
+
testBtn.textContent = L.visionTest;
|
|
1044
|
+
visionTestPre.textContent = res.output || res.error || '';
|
|
1045
|
+
if (!res.ok) showToast(L.visionTestFailed + ': ' + errText(res), 'error');
|
|
1046
|
+
});
|
|
1047
|
+
paneVision.replaceChildren(
|
|
1048
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.visionDesc),
|
|
1049
|
+
// 链模式 + 两个开关合到一行(顶部已有简介,开关不再单占一块、不带描述)
|
|
1050
|
+
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
|
+
h('span', { class: 'ml-1.5 shrink-0 text-ui-sm font-medium text-foreground' }, L.visionMode),
|
|
1052
|
+
modeBtn('fallback', L.visionModeFallback, L.visionModeFallbackDesc),
|
|
1053
|
+
modeBtn('pipeline', L.visionModePipeline, L.visionModePipelineDesc),
|
|
1054
|
+
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
|
+
settingRow(L.visionForceIntercept, '', visionCfg.forceIntercept !== false, async () => {
|
|
1061
|
+
visionCfg.forceIntercept = !(visionCfg.forceIntercept !== false);
|
|
1062
|
+
await persistVision();
|
|
1063
|
+
renderVision();
|
|
1064
|
+
}),
|
|
1065
|
+
),
|
|
1066
|
+
),
|
|
1067
|
+
// 压缩阈值 / 连续失败次数 / 跳过分钟数 三个数字一行
|
|
1068
|
+
h('div', { class: 'mt-2 flex flex-wrap items-center gap-x-4 gap-y-1.5 rounded-xl border border-border p-1' },
|
|
1069
|
+
(() => {
|
|
1070
|
+
const n = h('input', { type: 'number', min: '0', step: '128', title: L.visionCompressKBHint,
|
|
1071
|
+
class: 'h-7 w-16 rounded-lg border border-border bg-input px-2 text-right text-ui-xs tabular-nums text-foreground outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40' });
|
|
1072
|
+
n.value = String(Number.isFinite(visionCfg.compressThresholdKB) ? visionCfg.compressThresholdKB : 1024);
|
|
1073
|
+
n.addEventListener('change', () => {
|
|
1074
|
+
visionCfg.compressThresholdKB = Math.max(0, Math.round(Number(n.value) || 0));
|
|
1075
|
+
n.value = String(visionCfg.compressThresholdKB);
|
|
1076
|
+
void persistVision();
|
|
1077
|
+
});
|
|
1078
|
+
return h('label', { class: 'flex shrink-0 items-center gap-1.5', title: L.visionCompressKBHint },
|
|
1079
|
+
h('span', { class: 'text-ui-xs font-medium text-foreground-subtle' }, L.visionCompressKB), n);
|
|
1080
|
+
})(),
|
|
1081
|
+
numIn(visionCfg.skipAfterFailures, { min: 0, dflt: 4, title: L.visionSkipAfterFailuresHint, label: L.visionSkipAfterFailures, commit: (v) => { visionCfg.skipAfterFailures = v; } }),
|
|
1082
|
+
numIn(visionCfg.skipMinutes, { min: 1, dflt: 30, title: L.visionSkipMinutesHint, label: L.visionSkipMinutes, commit: (v) => { visionCfg.skipMinutes = v; } }),
|
|
1083
|
+
),
|
|
1084
|
+
...cards,
|
|
1085
|
+
h('div', { class: 'mt-2 flex items-center justify-between' },
|
|
1086
|
+
h('div', { class: 'flex items-center gap-1.5' },
|
|
1087
|
+
smallBtn('+ ' + L.visionAddProxy, () => {
|
|
1088
|
+
// 名称留空时 helper 校验会拒绝保存;等用户输入名称后防抖保存
|
|
1089
|
+
visionCfg.proxies.push({ name: '', baseUrl: 'https://open.bigmodel.cn/api/anthropic', model: 'glm-5.3-flash', apiKey: '', format: 'anthropic', prompt: '' });
|
|
1090
|
+
renderVision();
|
|
1091
|
+
}),
|
|
1092
|
+
smallBtn(L.visionResetChain, () => {
|
|
1093
|
+
if (!window.confirm(L.visionResetChainConfirm)) return;
|
|
1094
|
+
visionCfg.chainMode = VISION_DEFAULT_CFG.chainMode;
|
|
1095
|
+
visionCfg.proxies = structuredClone(VISION_DEFAULT_CFG.proxies);
|
|
1096
|
+
void persistVision();
|
|
1097
|
+
renderVision();
|
|
1098
|
+
})),
|
|
1099
|
+
testBtn),
|
|
1100
|
+
visionTestPre,
|
|
1101
|
+
);
|
|
1102
|
+
};
|
|
1103
|
+
renderVision();
|
|
1104
|
+
|
|
1105
|
+
// “rtk 压缩”:编辑 rtk 插件状态(~/.zcode-rtk 的 mode 与 whitelist,
|
|
1106
|
+
// /rtk-* 命令编辑同一文件)。开关与白名单增删即时保存,钩子每次执行都重读文件。
|
|
1107
|
+
let rtkCfg = rtkRes.ok
|
|
1108
|
+
? {
|
|
1109
|
+
installed: rtkRes.installed !== false,
|
|
1110
|
+
version: rtkRes.version || '',
|
|
1111
|
+
binPath: rtkRes.binPath || '',
|
|
1112
|
+
mode: rtkRes.mode === 'off' ? 'off' : 'hint',
|
|
1113
|
+
whitelist: Array.isArray(rtkRes.whitelist) ? [...rtkRes.whitelist] : [],
|
|
1114
|
+
// 钩子内置放行清单(只读展示)
|
|
1115
|
+
builtinGit: Array.isArray(rtkRes.builtinGit) ? [...rtkRes.builtinGit] : [],
|
|
1116
|
+
builtinPlain: Array.isArray(rtkRes.builtinPlain) ? [...rtkRes.builtinPlain] : [],
|
|
1117
|
+
}
|
|
1118
|
+
: null;
|
|
1119
|
+
const persistRtk = async (partial) => {
|
|
1120
|
+
const res = await rpc('/rtk', { method: 'POST', body: partial });
|
|
1121
|
+
if (!res.ok) showToast(L.failed + ': ' + errText(res), 'error');
|
|
1122
|
+
return res.ok;
|
|
1123
|
+
};
|
|
1124
|
+
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 下载替换)的版本行与升级控件,渲染函数外创建一次
|
|
1126
|
+
const rtkVersionLine = h('span', { class: 'shrink-0 whitespace-nowrap text-ui-xs/relaxed text-foreground-subtle' });
|
|
1127
|
+
const renderRtkVersion = (meta) => {
|
|
1128
|
+
const v = meta.version || 'rtk';
|
|
1129
|
+
rtkVersionLine.textContent = meta.binPath ? `${v} · ${meta.binPath}` : v;
|
|
1130
|
+
};
|
|
1131
|
+
renderRtkVersion(rtkRes);
|
|
1132
|
+
const rtkUp = makeUpgradeControls({
|
|
1133
|
+
endpoint: '/rtk',
|
|
1134
|
+
metaRefresh: async () => {
|
|
1135
|
+
const meta = await rpc('/rtk');
|
|
1136
|
+
if (meta.ok) {
|
|
1137
|
+
// 初始读取失败时 rtkCfg 为 null,此时只刷新版本行
|
|
1138
|
+
if (rtkCfg) Object.assign(rtkCfg, meta);
|
|
1139
|
+
renderRtkVersion(meta);
|
|
1140
|
+
}
|
|
1141
|
+
},
|
|
1142
|
+
});
|
|
1143
|
+
rtkUpgradePollStop = rtkUp.stop;
|
|
1144
|
+
// 版本/路径/检查更新 + 启用 rtk 压缩开关合到一行(开关靠右,顶部已有简介不带描述)
|
|
1145
|
+
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, rtkSwitchRow);
|
|
1148
|
+
const rtkBody = h('div');
|
|
1149
|
+
// 简介置顶(与 Headroom 面板一致),其下版本卡(含启用 rtk 压缩开关),再下白名单
|
|
1150
|
+
paneRtk.append(
|
|
1151
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.rtkDesc),
|
|
1152
|
+
rtkVersionCard,
|
|
1153
|
+
rtkBody);
|
|
1154
|
+
const renderRtk = () => {
|
|
1155
|
+
rtkVersionCard.style.display = rtkCfg && rtkCfg.installed ? '' : 'none';
|
|
1156
|
+
if (!rtkCfg) {
|
|
1157
|
+
rtkBody.replaceChildren(
|
|
1158
|
+
h('p', { class: 'text-ui-sm text-destructive' }, L.rtkLoadFailed + ': ' + errText(rtkRes)),
|
|
1159
|
+
);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
const input = h('input', { type: 'text', placeholder: L.rtkWhitelistPlaceholder, class: rtkInputCls + ' min-w-0 flex-1' });
|
|
1163
|
+
const addEntry = async () => {
|
|
1164
|
+
const v = input.value.trim();
|
|
1165
|
+
if (!v) return;
|
|
1166
|
+
if (!/^(git:)?[A-Za-z0-9._-]+$/.test(v) || rtkCfg.whitelist.includes(v)) {
|
|
1167
|
+
showToast(L.rtkWhitelistInvalid, 'error');
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
if (await persistRtk({ whitelist: [...rtkCfg.whitelist, v] })) {
|
|
1171
|
+
rtkCfg.whitelist.push(v);
|
|
1172
|
+
renderRtk();
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
input.addEventListener('keydown', (e) => {
|
|
1176
|
+
if (e.key === 'Enter') { e.preventDefault(); void addEntry(); }
|
|
1177
|
+
});
|
|
1178
|
+
const removeEntry = (entry) => {
|
|
1179
|
+
void (async () => {
|
|
1180
|
+
const next = rtkCfg.whitelist.filter((x) => x !== entry);
|
|
1181
|
+
if (await persistRtk({ whitelist: next })) {
|
|
1182
|
+
rtkCfg.whitelist = next;
|
|
1183
|
+
renderRtk();
|
|
1184
|
+
}
|
|
1185
|
+
})();
|
|
1186
|
+
};
|
|
1187
|
+
rtkSwitchRow.replaceChildren(
|
|
1188
|
+
settingRow(L.rtkEnabled, '', rtkCfg.mode === 'hint', async () => {
|
|
1189
|
+
const next = rtkCfg.mode === 'hint' ? 'off' : 'hint';
|
|
1190
|
+
if (await persistRtk({ mode: next })) {
|
|
1191
|
+
rtkCfg.mode = next;
|
|
1192
|
+
renderRtk();
|
|
1193
|
+
}
|
|
1194
|
+
}));
|
|
1195
|
+
rtkBody.replaceChildren(
|
|
1196
|
+
...(rtkCfg.installed
|
|
1197
|
+
? []
|
|
1198
|
+
: [h('p', { class: 'text-ui-sm text-amber-500' }, L.rtkNotInstalled)]),
|
|
1199
|
+
h('div', { class: 'mt-2 rounded-xl border border-border p-2' },
|
|
1200
|
+
h('div', { class: 'text-ui-sm font-medium text-foreground' }, L.rtkWhitelistTitle),
|
|
1201
|
+
h('p', { class: 'mt-1 text-ui-xs/relaxed text-foreground-subtle' }, L.rtkWhitelistDesc),
|
|
1202
|
+
...((rtkCfg.builtinGit.length > 0 || rtkCfg.builtinPlain.length > 0)
|
|
1203
|
+
? [
|
|
1204
|
+
h('div', { class: 'mt-2 text-ui-xs font-medium text-foreground-subtle' }, L.rtkBuiltinLabel),
|
|
1205
|
+
h('div', { class: 'mt-1 flex flex-wrap items-center gap-1' },
|
|
1206
|
+
...rtkCfg.builtinGit.map((g) => `git:${g}`).concat(rtkCfg.builtinPlain)
|
|
1207
|
+
.map((entry) => h('span', {
|
|
1208
|
+
class: 'inline-flex items-center rounded-md border border-border bg-surface px-1.5 py-0.5 text-ui-xs text-foreground-subtle/80',
|
|
1209
|
+
}, entry))),
|
|
1210
|
+
]
|
|
1211
|
+
: []),
|
|
1212
|
+
h('div', { class: 'mt-2 flex flex-wrap items-center gap-1.5' },
|
|
1213
|
+
...rtkCfg.whitelist.map((entry) => h('span', {
|
|
1214
|
+
class: 'inline-flex items-center gap-1 rounded-md border border-border bg-surface px-1.5 py-0.5 text-ui-xs text-foreground',
|
|
1215
|
+
}, entry,
|
|
1216
|
+
h('button', {
|
|
1217
|
+
type: 'button', class: 'text-foreground-subtle hover:text-destructive', title: L.visionRemove,
|
|
1218
|
+
onClick: () => removeEntry(entry),
|
|
1219
|
+
}, '×'))),
|
|
1220
|
+
...(rtkCfg.whitelist.length === 0
|
|
1221
|
+
? [h('span', { class: 'text-ui-xs text-foreground-subtle/70' }, L.defaultValue)]
|
|
1222
|
+
: [])),
|
|
1223
|
+
h('div', { class: 'mt-2 flex items-center gap-2' },
|
|
1224
|
+
input,
|
|
1225
|
+
btnSmall(L.rtkWhitelistAdd, () => { void addEntry(); }, 'shrink-0'),
|
|
1226
|
+
btnSmall(L.rtkWhitelistClear, () => {
|
|
1227
|
+
void (async () => {
|
|
1228
|
+
if (rtkCfg.whitelist.length === 0) return;
|
|
1229
|
+
if (await persistRtk({ whitelist: [] })) {
|
|
1230
|
+
rtkCfg.whitelist = [];
|
|
1231
|
+
renderRtk();
|
|
1232
|
+
showToast(L.rtkWhitelistCleared);
|
|
1233
|
+
}
|
|
1234
|
+
})();
|
|
1235
|
+
}, 'shrink-0'))),
|
|
1236
|
+
);
|
|
1237
|
+
};
|
|
1238
|
+
renderRtk();
|
|
1239
|
+
|
|
1240
|
+
// “Headroom”:编辑 headroom 插件配置(~/.zcode/headroom.json,
|
|
1241
|
+
// /hr-* 命令与钩子共用同一文件)。压缩设备/省电切换经 helper 调钩子动作改
|
|
1242
|
+
// (立即生效;切换设备会重启代理);监视间隔直写配置,监视器下一轮巡检生效。
|
|
1243
|
+
// 可整面重渲染:未装插件时显示安装卡,安装成功后原地恢复完整面板。
|
|
1244
|
+
// 版本行与本体升级控件(pip 通道)在渲染函数外创建一次,重渲染时复用同一组节点
|
|
1245
|
+
const hrVersionLine = h('span', { class: 'shrink-0 whitespace-nowrap text-ui-xs/relaxed text-foreground-subtle' });
|
|
1246
|
+
const renderHrVersion = (meta) => {
|
|
1247
|
+
const parts = [];
|
|
1248
|
+
if (meta.pluginVersion) parts.push(`${L.headroomVersionPlugin} ${meta.pluginVersion}`);
|
|
1249
|
+
if (meta.version) parts.push(`${L.headroomVersionBin} ${meta.version}`);
|
|
1250
|
+
hrVersionLine.textContent = parts.length ? parts.join(' · ') : `${L.headroomVersionPlugin} —`;
|
|
1251
|
+
};
|
|
1252
|
+
renderHrVersion(headroomRes);
|
|
1253
|
+
const hrUp = makeUpgradeControls({
|
|
1254
|
+
endpoint: '/headroom',
|
|
1255
|
+
metaRefresh: async () => {
|
|
1256
|
+
const meta = await rpc('/headroom');
|
|
1257
|
+
if (meta.ok) {
|
|
1258
|
+
renderHrVersion(meta);
|
|
1259
|
+
Object.assign(headroomRes, meta);
|
|
1260
|
+
}
|
|
1261
|
+
},
|
|
1262
|
+
});
|
|
1263
|
+
hrUpgradePollStop = hrUp.stop;
|
|
1264
|
+
const renderHeadroomPane = () => {
|
|
1265
|
+
paneHeadroom.replaceChildren();
|
|
1266
|
+
if (!headroomRes.ok || !headroomRes.config) {
|
|
1267
|
+
paneHeadroom.append(
|
|
1268
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.headroomDesc),
|
|
1269
|
+
h('p', { class: 'mt-2 text-ui-sm text-destructive' }, L.headroomLoadFailed + ': ' + errText(headroomRes)),
|
|
1270
|
+
);
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
if (!headroomRes.hook) {
|
|
1274
|
+
// 未装插件:安装卡一键从市场安装(helper 单插件安装),装完原地恢复面板
|
|
1275
|
+
const installBtn = btnSmall(L.headroomInstall, () => { void runInstall(); }, 'shrink-0');
|
|
1276
|
+
const installState = h('div', { class: 'mt-0.5 truncate text-ui-xs/relaxed text-foreground-subtle' });
|
|
1277
|
+
const runInstall = async () => {
|
|
1278
|
+
installBtn.disabled = true;
|
|
1279
|
+
installBtn.textContent = L.headroomInstalling;
|
|
1280
|
+
installState.textContent = '';
|
|
1281
|
+
const res = await rpc('/plugins/update', { method: 'POST', body: { name: 'headroom', installMissing: true } });
|
|
1282
|
+
if (!res.ok) {
|
|
1283
|
+
installBtn.disabled = false;
|
|
1284
|
+
installBtn.textContent = L.headroomInstall;
|
|
1285
|
+
showToast(L.pluginsUpdateFailed + ': ' + errText(res), 'error');
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
const meta = await rpc('/headroom');
|
|
1289
|
+
if (meta.ok) Object.assign(headroomRes, meta);
|
|
1290
|
+
if (headroomRes.hook) {
|
|
1291
|
+
showToast(L.headroomInstalled, 'success');
|
|
1292
|
+
renderHeadroomPane();
|
|
1293
|
+
} else {
|
|
1294
|
+
installBtn.disabled = false;
|
|
1295
|
+
installBtn.textContent = L.headroomInstall;
|
|
1296
|
+
installState.textContent = L.headroomInstallRetry;
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
paneHeadroom.append(
|
|
1300
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.headroomDesc),
|
|
1301
|
+
h('div', { class: 'mt-3 flex items-center justify-between gap-2 rounded-xl border border-border p-2' },
|
|
1302
|
+
h('div', { class: 'min-w-0' },
|
|
1303
|
+
h('div', { class: 'text-ui-sm font-medium text-foreground' }, L.headroomInstallTitle),
|
|
1304
|
+
h('div', { class: 'mt-0.5 text-ui-xs/relaxed text-foreground-subtle' }, L.headroomInstallDesc),
|
|
1305
|
+
installState),
|
|
1306
|
+
installBtn),
|
|
1307
|
+
);
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
const hrCfg = { ...headroomRes.config };
|
|
1311
|
+
let hrBusy = false;
|
|
1312
|
+
const hrRun = async (reqBody, warn) => {
|
|
1313
|
+
if (hrBusy) return false;
|
|
1314
|
+
hrBusy = true;
|
|
1315
|
+
const res = await rpc('/headroom', { method: 'POST', body: reqBody });
|
|
1316
|
+
hrBusy = false;
|
|
1317
|
+
if (!res.ok) {
|
|
1318
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
1319
|
+
return false;
|
|
1320
|
+
}
|
|
1321
|
+
if (res.config) Object.assign(hrCfg, res.config);
|
|
1322
|
+
if (warn) showToast(warn);
|
|
1323
|
+
// backend/power 动作会重启代理,稍等其就绪再刷状态,避免闪现“未运行”
|
|
1324
|
+
setTimeout(() => { void hrRefresh(); }, 1200);
|
|
1325
|
+
return true;
|
|
1326
|
+
};
|
|
1327
|
+
// 状态值本地化:钩子输出的是 auto/cpu/ac/ok 等内部值,面板显示成人话
|
|
1328
|
+
const hrDevLabel = (v) => (v == null ? '—' : v === 'auto' ? L.hrValAuto : v === 'cpu' ? 'CPU' : v);
|
|
1329
|
+
const HR_PM = { ac: L.hrPmAc, battery: L.hrPmBattery, saver: L.hrPmSaver, 'saver+battery': L.hrPmSaverBat };
|
|
1330
|
+
const HR_DETECT = { ok: L.hrDetectOk, missing: L.hrDetectMissing, broken: L.hrDetectBroken };
|
|
1331
|
+
const hrStatusLine = h('div', { class: 'flex items-center gap-2' },
|
|
1332
|
+
h('span', { class: 'text-ui-sm text-foreground-subtle/70' }, '…'));
|
|
1333
|
+
const hrFacts = h('div', { class: 'mt-1.5 grid grid-cols-2 gap-x-4 gap-y-1 sm:grid-cols-3' });
|
|
1334
|
+
const hrHintLine = h('p', { class: 'mt-1.5 text-ui-xs/relaxed text-amber-500', style: 'display:none' });
|
|
1335
|
+
const hrRefresh = async () => {
|
|
1336
|
+
const res = await rpc('/headroom/status');
|
|
1337
|
+
if (!res.ok) {
|
|
1338
|
+
hrStatusLine.replaceChildren(
|
|
1339
|
+
h('span', { class: 'text-ui-sm text-destructive' }, L.headroomStatusLoadFailed + ': ' + errText(res)));
|
|
1340
|
+
hrFacts.replaceChildren();
|
|
1341
|
+
hrHintLine.style.display = 'none';
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
const s = res.status || {};
|
|
1345
|
+
hrStatusLine.replaceChildren(
|
|
1346
|
+
h('span', { class: 'inline-block size-2 shrink-0 rounded-full ' + (s.up ? 'bg-emerald-500' : 'bg-red-400') }),
|
|
1347
|
+
h('span', { class: 'text-ui-sm font-medium text-foreground' }, s.up ? L.headroomProxyUp : L.headroomProxyDown),
|
|
1348
|
+
...(s.port ? [h('span', { class: 'text-ui-xs text-foreground-subtle' }, `127.0.0.1:${s.port}`)] : []),
|
|
1349
|
+
);
|
|
1350
|
+
const fact = (label, value) => h('div', { class: 'min-w-0 truncate text-ui-xs text-foreground-subtle' },
|
|
1351
|
+
h('span', { class: 'text-foreground-subtle/70' }, label + ':'),
|
|
1352
|
+
String(value ?? '—'));
|
|
1353
|
+
hrFacts.replaceChildren(
|
|
1354
|
+
fact(L.headroomCurrentBackend, hrDevLabel(s.backend)),
|
|
1355
|
+
fact(L.headroomDesiredBackend, hrDevLabel(s.desiredBackend)),
|
|
1356
|
+
fact(L.headroomPowerState, HR_PM[s.powerMode] || s.powerMode),
|
|
1357
|
+
fact(L.headroomWatcher, s.watcherRunning ? L.headroomWatcherOn : L.headroomWatcherOff),
|
|
1358
|
+
fact(L.headroomSaverDetect, HR_DETECT[s.saverDetect] || s.saverDetect),
|
|
1359
|
+
);
|
|
1360
|
+
hrHintLine.textContent = s.saverHint || '';
|
|
1361
|
+
hrHintLine.style.display = s.saverHint ? '' : 'none';
|
|
1362
|
+
};
|
|
1363
|
+
const hrRefreshBtn = btnSmall(L.headroomRefresh, () => { void hrRefresh(); });
|
|
1364
|
+
const hrStartBtn = btnSmall(L.headroomStart, () => { void hrRun({ action: 'start' }); });
|
|
1365
|
+
const hrRestartBtn = btnSmall(L.headroomRestart, () => { void hrRun({ action: 'restart' }); });
|
|
1366
|
+
const hrStopBtn = btnSmall(L.headroomStopAction, () => {
|
|
1367
|
+
// 停止会断开指向该代理的供应商连接,需二次确认(与“切换文件夹”同款确认弹窗)
|
|
1368
|
+
openDialog({
|
|
1369
|
+
title: L.headroomStopTitle,
|
|
1370
|
+
description: L.headroomStopDesc,
|
|
1371
|
+
width: 'sm:max-w-md',
|
|
1372
|
+
onMount: ({ body: confirmBody, close: closeConfirm }) => {
|
|
1373
|
+
confirmBody.append(
|
|
1374
|
+
dialogFooter(
|
|
1375
|
+
btnSecondary(L.cancel, () => closeConfirm()),
|
|
1376
|
+
btnPrimary(L.headroomStopAction, () => {
|
|
1377
|
+
closeConfirm();
|
|
1378
|
+
void hrRun({ action: 'stop' }, L.headroomStoppedWarn);
|
|
1379
|
+
}, 'min-w-24'),
|
|
1380
|
+
),
|
|
1381
|
+
);
|
|
1382
|
+
},
|
|
1383
|
+
});
|
|
1384
|
+
});
|
|
1385
|
+
void hrRefresh();
|
|
1386
|
+
|
|
1387
|
+
|
|
1388
|
+
// 压缩后端:下拉选择(auto/cpu,配置为原生值时追加显示当前值)
|
|
1389
|
+
const hrBackendSel = h('select', { class: 'h-7 shrink-0 rounded-lg border border-border bg-input px-1.5 text-ui-xs text-foreground outline-none' });
|
|
1390
|
+
const hrBackendOpts = [
|
|
1391
|
+
['auto', L.headroomBackendAuto],
|
|
1392
|
+
['cpu', L.headroomBackendCpu],
|
|
1393
|
+
];
|
|
1394
|
+
if (!['auto', 'cpu'].includes(hrCfg.kompressBackend)) {
|
|
1395
|
+
hrBackendOpts.push([hrCfg.kompressBackend, hrCfg.kompressBackend + L.headroomBackendNative]);
|
|
1396
|
+
}
|
|
1397
|
+
for (const [v, label] of hrBackendOpts) hrBackendSel.append(h('option', { value: v }, label));
|
|
1398
|
+
hrBackendSel.value = hrCfg.kompressBackend;
|
|
1399
|
+
hrBackendSel.addEventListener('change', () => {
|
|
1400
|
+
const v = hrBackendSel.value;
|
|
1401
|
+
void (async () => {
|
|
1402
|
+
if (await hrRun({ backend: v })) updateHrPwrHint();
|
|
1403
|
+
else hrBackendSel.value = hrCfg.kompressBackend;
|
|
1404
|
+
})();
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
// 省电自动切换:三态胶囊(同“链模式”的标签按钮),与监视间隔同一行
|
|
1408
|
+
const hrPowerDefs = [
|
|
1409
|
+
['off', L.headroomPowerOff, L.headroomPowerOffDesc],
|
|
1410
|
+
['battery', L.headroomPowerBattery, L.headroomPowerBatteryDesc],
|
|
1411
|
+
['saver', L.headroomPowerSaver, L.headroomPowerSaverDesc],
|
|
1412
|
+
];
|
|
1413
|
+
const hrPowerBox = h('div', { class: 'flex min-w-0 flex-wrap items-center gap-1.5' });
|
|
1414
|
+
const renderHrPower = () => hrPowerBox.replaceChildren(
|
|
1415
|
+
h('span', { class: 'shrink-0 text-ui-sm font-medium text-foreground' }, L.headroomPower),
|
|
1416
|
+
...hrPowerDefs.map(([id, label, desc]) => h('button', {
|
|
1417
|
+
type: 'button',
|
|
1418
|
+
class: 'zcodepro-tab',
|
|
1419
|
+
'data-state': hrCfg.powerSaveCpu === id ? 'active' : 'inactive',
|
|
1420
|
+
title: desc,
|
|
1421
|
+
onClick: () => {
|
|
1422
|
+
void (async () => {
|
|
1423
|
+
if (await hrRun({ power: id })) renderHrPower();
|
|
1424
|
+
})();
|
|
1425
|
+
},
|
|
1426
|
+
}, label)));
|
|
1427
|
+
renderHrPower();
|
|
1428
|
+
|
|
1429
|
+
// 监视间隔:数字框(滚轮/手输),保存后从下一轮巡检起生效
|
|
1430
|
+
const hrIntervalField = numberField({
|
|
1431
|
+
value: hrCfg.powerWatchInterval,
|
|
1432
|
+
fallback: 60,
|
|
1433
|
+
min: 10,
|
|
1434
|
+
max: 3600,
|
|
1435
|
+
step: 5,
|
|
1436
|
+
onCommit: (v) => {
|
|
1437
|
+
void (async () => {
|
|
1438
|
+
const res = await rpc('/headroom', { method: 'POST', body: { interval: v } });
|
|
1439
|
+
if (res.ok && res.config) Object.assign(hrCfg, res.config);
|
|
1440
|
+
else {
|
|
1441
|
+
showToast(L.failed + ': ' + errText(res), 'error');
|
|
1442
|
+
hrIntervalField.set(hrCfg.powerWatchInterval);
|
|
1443
|
+
}
|
|
1444
|
+
})();
|
|
1445
|
+
},
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
// 压缩设备固定为 CPU 时,省电切换没有可切换的空间(规则命中也还是 CPU)
|
|
1449
|
+
const hrPwrHint = h('p', { class: 'px-2.5 pb-1.5 text-ui-xs/relaxed text-foreground-subtle', style: 'display:none' },
|
|
1450
|
+
L.hrCpuPinnedHint);
|
|
1451
|
+
const updateHrPwrHint = () => {
|
|
1452
|
+
hrPwrHint.style.display = hrCfg.kompressBackend === 'cpu' ? '' : 'none';
|
|
1453
|
+
};
|
|
1454
|
+
updateHrPwrHint();
|
|
1455
|
+
|
|
1456
|
+
paneHeadroom.append(
|
|
1457
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.headroomDesc),
|
|
1458
|
+
...(headroomRes.installed === false
|
|
1459
|
+
? [h('p', { class: 'mt-2 text-ui-sm text-amber-500' }, L.headroomBinMissing)]
|
|
1460
|
+
: []),
|
|
1461
|
+
h('div', { class: 'mt-3 flex items-center justify-between gap-2 rounded-xl border border-border p-2' },
|
|
1462
|
+
h('div', { class: 'flex min-w-0 items-center gap-2' }, hrVersionLine, hrUp.state),
|
|
1463
|
+
hrUp.btn),
|
|
1464
|
+
h('div', { class: 'mt-2 rounded-xl border border-border p-2' },
|
|
1465
|
+
h('div', { class: 'flex flex-wrap items-center justify-between gap-2' },
|
|
1466
|
+
h('span', { class: 'text-ui-sm font-medium text-foreground' }, L.headroomStatusTitle),
|
|
1467
|
+
h('div', { class: 'flex items-center gap-1.5' }, hrRefreshBtn, hrStartBtn, hrRestartBtn, hrStopBtn)),
|
|
1468
|
+
hrStatusLine,
|
|
1469
|
+
hrFacts,
|
|
1470
|
+
hrHintLine),
|
|
1471
|
+
h('div', {
|
|
1472
|
+
class: 'mt-2 rounded-xl border border-border p-1',
|
|
1473
|
+
},
|
|
1474
|
+
h('div', { class: 'flex items-center justify-between gap-2 rounded-lg p-2 transition-colors hover:bg-surface-hover' },
|
|
1475
|
+
h('span', { class: 'min-w-0 truncate text-ui-sm font-medium text-foreground', title: L.headroomBackendDesc }, L.headroomBackend),
|
|
1476
|
+
hrBackendSel),
|
|
1477
|
+
h('div', { class: 'flex flex-wrap items-center gap-x-3 gap-y-1.5 rounded-lg p-1.5 transition-colors hover:bg-surface-hover' },
|
|
1478
|
+
hrPowerBox,
|
|
1479
|
+
h('div', { class: 'ml-auto flex shrink-0 items-center gap-1.5' },
|
|
1480
|
+
h('span', { class: 'text-ui-xs font-medium text-foreground-subtle', title: L.headroomIntervalDesc }, L.headroomInterval),
|
|
1481
|
+
hrIntervalField.el,
|
|
1482
|
+
h('span', { class: 'w-4 text-ui-xs text-foreground-subtle' }, L.headroomSecUnit))),
|
|
1483
|
+
hrPwrHint),
|
|
1484
|
+
);
|
|
1485
|
+
};
|
|
1486
|
+
renderHeadroomPane();
|
|
1487
|
+
|
|
1488
|
+
body.append(
|
|
1489
|
+
statusLine,
|
|
1490
|
+
tablist,
|
|
1491
|
+
paneFeatures,
|
|
1492
|
+
paneStyles,
|
|
1493
|
+
panePerf,
|
|
1494
|
+
paneAgents,
|
|
1495
|
+
paneProxy,
|
|
1496
|
+
paneSync,
|
|
1497
|
+
paneVision,
|
|
1498
|
+
paneHeadroom,
|
|
1499
|
+
paneRtk,
|
|
1500
|
+
);
|
|
1501
|
+
},
|
|
1502
|
+
});
|
|
1503
|
+
}
|