zcode-pro 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +1 -0
- package/README.md +1 -0
- package/bin/zcode-pro.mjs +32 -9
- package/dist/inject.js +684 -130
- package/package.json +1 -1
- package/src/host/helper.mjs +613 -50
- package/src/host/main.mjs +8 -4
- package/src/host/setup.mjs +45 -42
- package/src/host/taskIndex.mjs +258 -0
- package/src/inject/core.js +67 -19
- package/src/inject/features/file-menu.js +9 -2
- package/src/inject/features/settings-dialog.js +266 -63
- package/src/inject/features/styles.js +1 -1
- package/src/inject/features/task-order.js +1 -1
- package/src/inject/features/title-gen.js +178 -0
- package/src/inject/features/title-lock.js +73 -1
- package/src/inject/features/toolbar-icons.js +38 -4
- package/src/inject/features/ws-running.js +1 -1
- package/src/inject/index.js +2 -0
- package/src/inject/provider-fields.js +71 -0
- package/src/inject/ui.js +20 -5
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// 顶部标签页切换(视觉参考侧栏“分组/项目”切换);配置写入 helper(~/.zcode/zcodepro.json)。
|
|
3
3
|
import { h, t, rpc, getConfig, clearConfigCache, errText, HELPER_URL, isZhLocale } from '../core.js';
|
|
4
4
|
import { openDialog, dialogFooter, btnPrimary, btnSecondary, btnSmall, settingRow, ensureStyle, showToast, numberField, unitField } from '../ui.js';
|
|
5
|
+
import { comboField, providerOptionsOf, modelOptionsOf, modelPlaceholderOf, isManualConn } from '../provider-fields.js';
|
|
5
6
|
import { refreshAliases } from './alias.js';
|
|
6
7
|
import { applyStyles, STYLE_DEFAULTS, showStyleHighlight, hideStyleHighlight } from './styles.js';
|
|
7
8
|
import { copyToClipboard, hCopyIcon } from './copy-path.js';
|
|
@@ -24,7 +25,7 @@ function makeUpgradeControls({ endpoint, metaRefresh, idleText }) {
|
|
|
24
25
|
const hasIdle = () => typeof idleText === 'function';
|
|
25
26
|
const setState = (text, kind = '') => {
|
|
26
27
|
const shown = text || (hasIdle() ? (idleText() || '') : '');
|
|
27
|
-
//
|
|
28
|
+
// “ · ”前缀:与前面的版本行直接拼成“插件 x · rtk y · 状态/路径”一行;
|
|
28
29
|
// 错误信息不加前缀——它是独立提示,拼在版本行后会读成版本的一部分
|
|
29
30
|
state.textContent = shown ? (kind === 'error' ? shown : ` · ${shown}`) : '';
|
|
30
31
|
state.className = 'min-w-0 flex-1 truncate text-left text-ui-xs/relaxed '
|
|
@@ -149,8 +150,10 @@ export function openSettingsDialog() {
|
|
|
149
150
|
openDialog({
|
|
150
151
|
title: L.settingsTitle,
|
|
151
152
|
// 宽度类必须用宿主样式表已有的工具类(注入的类名不会生成 CSS):
|
|
152
|
-
// sm:max-w-3xl 不存在会失去约束拉满全屏;max-w-
|
|
153
|
-
|
|
153
|
+
// sm:max-w-3xl 不存在会失去约束拉满全屏;max-w-3xl=48rem 存在——英文界面
|
|
154
|
+
// 标签栏较长,42rem 会挤出一块空白并触发横向滚动
|
|
155
|
+
width: 'max-w-3xl',
|
|
156
|
+
bodyClass: 'mt-1',
|
|
154
157
|
overlay: 'none',
|
|
155
158
|
draggable: true,
|
|
156
159
|
posKey: 'settings',
|
|
@@ -202,7 +205,11 @@ export function openSettingsDialog() {
|
|
|
202
205
|
const config = await getConfig(true);
|
|
203
206
|
const agentsRes = await rpc('/agents');
|
|
204
207
|
const visionRes = await rpc('/vision');
|
|
205
|
-
const
|
|
208
|
+
const titleGenRes = await rpc('/title-gen');
|
|
209
|
+
const titleGenProvidersRes = await rpc('/title-gen/providers');
|
|
210
|
+
// 供应商表(含各供应商可选模型):视觉代理的模型下拉也用这份
|
|
211
|
+
const titleGenProviders = titleGenProvidersRes.ok && Array.isArray(titleGenProvidersRes.providers)
|
|
212
|
+
? titleGenProvidersRes.providers : [];
|
|
206
213
|
const rtkRes = await rpc('/rtk');
|
|
207
214
|
const headroomRes = await rpc('/headroom');
|
|
208
215
|
|
|
@@ -225,7 +232,7 @@ export function openSettingsDialog() {
|
|
|
225
232
|
'data-zcodepro-status': '1',
|
|
226
233
|
},
|
|
227
234
|
h('span', { class: 'inline-block size-2 rounded-full bg-emerald-500' }),
|
|
228
|
-
h('span', { class: 'text-foreground-subtle/70' }, `${
|
|
235
|
+
h('span', { class: 'text-foreground-subtle/70' }, `${health.version} · ${HELPER_URL}`)
|
|
229
236
|
);
|
|
230
237
|
|
|
231
238
|
const rows = h('div', { class: 'divide-y divide-border rounded-xl border border-border' });
|
|
@@ -378,19 +385,20 @@ export function openSettingsDialog() {
|
|
|
378
385
|
);
|
|
379
386
|
const paneStyles = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
380
387
|
const panePerf = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
381
|
-
const paneAgents = h('div', { role: 'tabpanel', class: 'mt-
|
|
388
|
+
const paneAgents = h('div', { role: 'tabpanel', class: 'mt-2', style: 'display:none' });
|
|
382
389
|
const paneProxy = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
383
390
|
const paneSync = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
384
391
|
const paneVision = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
385
392
|
const paneRtk = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
386
393
|
const paneHeadroom = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
394
|
+
const paneTitleGen = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
387
395
|
const paneDebug = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
388
396
|
const panes = { features: paneFeatures, styles: paneStyles, perf: panePerf, agents: paneAgents, proxy: paneProxy, sync: paneSync, vision: paneVision, headroom: paneHeadroom, rtk: paneRtk, debug: paneDebug };
|
|
389
397
|
const tabDefs = [
|
|
390
398
|
['features', L.tabFeatures],
|
|
399
|
+
['agents', L.tabBehavior],
|
|
391
400
|
['styles', L.tabStyles],
|
|
392
401
|
['perf', L.tabPerformance],
|
|
393
|
-
['agents', L.tabAgents],
|
|
394
402
|
['proxy', L.tabProxy],
|
|
395
403
|
['sync', L.tabSync],
|
|
396
404
|
['vision', L.tabVision],
|
|
@@ -400,7 +408,7 @@ export function openSettingsDialog() {
|
|
|
400
408
|
// 普通用户不可见
|
|
401
409
|
...(config.devMode === true || config.devActive === true ? [['debug', L.tabDebug]] : []),
|
|
402
410
|
];
|
|
403
|
-
const tablist = h('div', { role: 'tablist', 'aria-orientation': 'horizontal', class: 'zcodepro-tablist mt-
|
|
411
|
+
const tablist = h('div', { role: 'tablist', 'aria-orientation': 'horizontal', class: 'zcodepro-tablist mt-1' });
|
|
404
412
|
const renderTabs = () => tablist.replaceChildren(...tabDefs.map(([id, label]) =>
|
|
405
413
|
h('button', {
|
|
406
414
|
type: 'button', role: 'tab', class: 'zcodepro-tab',
|
|
@@ -581,7 +589,7 @@ export function openSettingsDialog() {
|
|
|
581
589
|
// “全局提示词”:编辑 ~/.zcode/AGENTS.md(helper /agents 端点读写)。
|
|
582
590
|
// 大段文本不做即存即生效——显式保存;未修改时保存按钮禁用
|
|
583
591
|
const agentsArea = h('textarea', {
|
|
584
|
-
class: 'zcodepro-textarea',
|
|
592
|
+
class: 'zcodepro-textarea zcodepro-textarea-lg',
|
|
585
593
|
placeholder: L.agentsPlaceholder,
|
|
586
594
|
spellcheck: 'false',
|
|
587
595
|
});
|
|
@@ -610,7 +618,11 @@ export function openSettingsDialog() {
|
|
|
610
618
|
agentsSaveBtn.disabled = false;
|
|
611
619
|
}
|
|
612
620
|
};
|
|
613
|
-
|
|
621
|
+
// “行为”标签页:二级标签切换“全局提示词 / 任务名”。全局提示词编辑
|
|
622
|
+
// ~/.zcode/AGENTS.md;任务名(智能起名)面板复用 paneTitleGen(原本是
|
|
623
|
+
// 一级标签页,收进来后不再单独出现在顶部标签栏)
|
|
624
|
+
const agentsPromptPane = h('div', { class: 'mt-4' });
|
|
625
|
+
agentsPromptPane.append(
|
|
614
626
|
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.agentsDesc),
|
|
615
627
|
...(agentsFailed
|
|
616
628
|
? [h('p', { class: 'mt-2 text-ui-sm text-destructive' }, L.agentsLoadFailed + ': ' + errText(agentsRes))]
|
|
@@ -618,6 +630,27 @@ export function openSettingsDialog() {
|
|
|
618
630
|
h('div', { class: 'mt-3' }, agentsArea),
|
|
619
631
|
h('div', { class: 'mt-3 flex justify-end' }, agentsSaveBtn),
|
|
620
632
|
);
|
|
633
|
+
let agentsSubTab = 'prompt';
|
|
634
|
+
const agentsSubPanes = { prompt: agentsPromptPane, titleGen: paneTitleGen };
|
|
635
|
+
const agentsSubList = h('div', { role: 'tablist', 'aria-orientation': 'horizontal', class: 'zcodepro-tablist' });
|
|
636
|
+
const renderAgentsSubTabs = () => {
|
|
637
|
+
agentsSubList.replaceChildren(...[
|
|
638
|
+
['prompt', L.tabAgents],
|
|
639
|
+
['titleGen', L.tabTitleGen],
|
|
640
|
+
].map(([id, label]) => h('button', {
|
|
641
|
+
type: 'button', role: 'tab', class: 'zcodepro-tab',
|
|
642
|
+
'aria-selected': String(agentsSubTab === id),
|
|
643
|
+
'data-state': agentsSubTab === id ? 'active' : 'inactive',
|
|
644
|
+
onClick: () => {
|
|
645
|
+
agentsSubTab = id;
|
|
646
|
+
for (const [pid, pane] of Object.entries(agentsSubPanes)) pane.style.display = pid === id ? '' : 'none';
|
|
647
|
+
renderAgentsSubTabs();
|
|
648
|
+
},
|
|
649
|
+
}, label)));
|
|
650
|
+
};
|
|
651
|
+
for (const [pid, pane] of Object.entries(agentsSubPanes)) pane.style.display = pid === agentsSubTab ? '' : 'none';
|
|
652
|
+
renderAgentsSubTabs();
|
|
653
|
+
paneAgents.append(agentsSubList, agentsPromptPane, paneTitleGen);
|
|
621
654
|
|
|
622
655
|
// “代理”:为 helper 发起的网络访问(插件市场更新/安装、headroom 本体升级)设
|
|
623
656
|
// HTTP 代理;保存即生效(helper 子进程环境变量注入),不影响 ZCode 应用与模型请求。
|
|
@@ -648,7 +681,7 @@ export function openSettingsDialog() {
|
|
|
648
681
|
const proxyTestBtn = btnSmall(L.proxyTest, () => { void runProxyTest(); }, 'shrink-0');
|
|
649
682
|
const runProxyTest = async () => {
|
|
650
683
|
if (proxyTesting) return;
|
|
651
|
-
//
|
|
684
|
+
// 空地址不发请求(等网络超时才报“检测失败”的体验太差),立即提示
|
|
652
685
|
if (!proxyInput.value.trim()) {
|
|
653
686
|
proxyTestState.className = 'mt-2 text-ui-xs/relaxed text-amber-500';
|
|
654
687
|
proxyTestState.textContent = L.proxyTestEmpty;
|
|
@@ -766,19 +799,37 @@ export function openSettingsDialog() {
|
|
|
766
799
|
// “同步”:把 ZCode Pro 设置与模型设置手动备份到 WebDAV / 从 WebDAV 恢复
|
|
767
800
|
// (交互参考 Tampermonkey:URL/登录/密码 + 手动触发,无自动同步)。仅 WebDAV
|
|
768
801
|
// 一种后端且本就只在点击时传输,不设“类型/禁用”下拉。表单显式保存(旁有
|
|
769
|
-
//
|
|
802
|
+
// “测试”用当前表单值验证连通);同步内容开关即时保存;下载先弹云端版本
|
|
770
803
|
// 列表(固定名 = 最新 + 带时间戳的历史),任选一个导入,导入会覆盖本机、
|
|
771
804
|
// 先弹二次确认
|
|
772
805
|
const savedSync = (config.sync && typeof config.sync === 'object') ? config.sync
|
|
773
806
|
: { url: '', dir: 'zcode-pro', login: '', password: '', include: { zcodepro: true, model: true }, lastPushAt: null, lastPullAt: null };
|
|
774
|
-
|
|
807
|
+
// 宽度分开拼:定宽控件(历史版本数字框)不能带 w-full——与 w-20 同用时
|
|
808
|
+
// w-full 在样式表里靠后、会胜出,把整行撑爆
|
|
809
|
+
const syncInputBase = 'h-8 rounded-lg border border-border bg-input px-2.5 text-ui-sm text-foreground outline-none transition-shadow placeholder:text-foreground-subtle focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40';
|
|
810
|
+
const syncInputCls = syncInputBase + ' w-full';
|
|
811
|
+
const syncLabel = (text) => h('span', { class: 'w-20 shrink-0 text-ui-sm font-medium text-foreground' }, text);
|
|
775
812
|
const syncFormRow = (labelText, node) => h('div', { class: 'flex items-center gap-3 px-2 py-1.5' },
|
|
776
|
-
|
|
813
|
+
syncLabel(labelText), node);
|
|
777
814
|
const syncUrlInput = h('input', { type: 'text', value: savedSync.url || '', placeholder: L.syncUrlPlaceholder, spellcheck: 'false', autocomplete: 'off', class: syncInputCls + ' min-w-0 flex-1' });
|
|
778
815
|
// 上传目录:拼在 URL 之后的文件夹(默认 zcode-pro,避免直接写进网盘根目录),可多级
|
|
779
816
|
const syncDirInput = h('input', { type: 'text', value: savedSync.dir || 'zcode-pro', placeholder: L.syncDirPlaceholder, spellcheck: 'false', autocomplete: 'off', class: syncInputCls + ' min-w-0 flex-1' });
|
|
780
817
|
const syncLoginInput = h('input', { type: 'text', value: savedSync.login || '', spellcheck: 'false', autocomplete: 'off', class: syncInputCls + ' min-w-0 flex-1' });
|
|
781
818
|
const syncPasswordInput = h('input', { type: 'password', value: savedSync.password || '', autocomplete: 'new-password', class: syncInputCls + ' min-w-0 flex-1' });
|
|
819
|
+
// 历史版本保留个数:上传成功后自动删除云端更早的历史版本(固定名最新版不受
|
|
820
|
+
// 影响);0 = 不留历史版本。非法输入按默认 10 提交(服务端还会再校验)
|
|
821
|
+
const syncHistoryInput = h('input', {
|
|
822
|
+
type: 'number', min: '0', max: '100', step: '1',
|
|
823
|
+
value: Number.isInteger(savedSync.historyLimit) ? savedSync.historyLimit : 10,
|
|
824
|
+
title: L.syncHistoryLimitHint, class: syncInputBase + ' w-20 shrink-0 text-center',
|
|
825
|
+
});
|
|
826
|
+
const syncHistoryValue = () => {
|
|
827
|
+
// 清空按默认 10 提交,不当成 0——0 会清掉云端全部历史版,误清空代价太大
|
|
828
|
+
const s = syncHistoryInput.value.trim();
|
|
829
|
+
if (!s) return 10;
|
|
830
|
+
const n = Number(s);
|
|
831
|
+
return Number.isFinite(n) ? Math.min(100, Math.max(0, Math.round(n))) : 10;
|
|
832
|
+
};
|
|
782
833
|
let syncFormOriginal = null;
|
|
783
834
|
const syncSaveBtn = btnSmall(L.syncSave, () => { void saveSyncForm(); }, 'shrink-0', 'primary');
|
|
784
835
|
const syncFormValues = () => ({
|
|
@@ -786,15 +837,17 @@ export function openSettingsDialog() {
|
|
|
786
837
|
dir: syncDirInput.value.trim(),
|
|
787
838
|
login: syncLoginInput.value.trim(),
|
|
788
839
|
password: syncPasswordInput.value,
|
|
840
|
+
historyLimit: syncHistoryValue(),
|
|
789
841
|
});
|
|
790
842
|
const syncCheckDirty = () => {
|
|
791
843
|
const c = syncFormValues();
|
|
792
844
|
syncSaveBtn.disabled = c.url === syncFormOriginal.url && c.dir === syncFormOriginal.dir
|
|
793
|
-
&& c.login === syncFormOriginal.login && c.password === syncFormOriginal.password
|
|
845
|
+
&& c.login === syncFormOriginal.login && c.password === syncFormOriginal.password
|
|
846
|
+
&& c.historyLimit === syncFormOriginal.historyLimit;
|
|
794
847
|
};
|
|
795
848
|
syncFormOriginal = syncFormValues();
|
|
796
849
|
syncSaveBtn.disabled = true;
|
|
797
|
-
for (const el of [syncUrlInput, syncDirInput, syncLoginInput, syncPasswordInput]) {
|
|
850
|
+
for (const el of [syncUrlInput, syncDirInput, syncLoginInput, syncPasswordInput, syncHistoryInput]) {
|
|
798
851
|
el.addEventListener('input', syncCheckDirty);
|
|
799
852
|
}
|
|
800
853
|
const saveSyncForm = async () => {
|
|
@@ -805,18 +858,20 @@ export function openSettingsDialog() {
|
|
|
805
858
|
url: res.sync.url,
|
|
806
859
|
dir: res.sync.dir,
|
|
807
860
|
login: res.sync.login, password: res.sync.password,
|
|
861
|
+
historyLimit: Number.isInteger(res.sync.historyLimit) ? res.sync.historyLimit : 10,
|
|
808
862
|
};
|
|
809
863
|
syncUrlInput.value = res.sync.url;
|
|
810
864
|
syncDirInput.value = res.sync.dir;
|
|
811
865
|
syncLoginInput.value = res.sync.login;
|
|
812
866
|
syncPasswordInput.value = res.sync.password;
|
|
867
|
+
syncHistoryInput.value = syncFormOriginal.historyLimit;
|
|
813
868
|
showToast(L.syncSaved);
|
|
814
869
|
} else {
|
|
815
870
|
showToast(L.failed + ': ' + errText(res), 'error');
|
|
816
871
|
syncCheckDirty();
|
|
817
872
|
}
|
|
818
873
|
};
|
|
819
|
-
//
|
|
874
|
+
// “测试”:用表单当前值(先测再存)验证 WebDAV:连接与凭据 → 上传目录
|
|
820
875
|
// (缺则自动建,等同上传时的行为)→ 文件列表;结果仿“代理”标签页的检测,
|
|
821
876
|
// 一行显示各步耗时与云端备份文件数
|
|
822
877
|
const syncTestState = h('p', { class: 'mt-2 text-ui-xs/relaxed text-foreground-subtle' });
|
|
@@ -900,7 +955,7 @@ export function openSettingsDialog() {
|
|
|
900
955
|
renderSyncStatus(savedSync);
|
|
901
956
|
const syncUploadBtn = btnSmall(L.syncUpload, () => { void runSyncPush(); }, 'shrink-0', 'primary');
|
|
902
957
|
const syncDownloadBtn = btnSmall(L.syncDownload, () => { openSyncVersionPicker(); }, 'shrink-0');
|
|
903
|
-
//
|
|
958
|
+
// “从云端下载”先弹版本列表(备份管理器的样子):列出云端目录里的备份文件
|
|
904
959
|
// (固定名 = 最新一版 + 带时间戳的历史),每行可导入到本机 / 从云端删除 /
|
|
905
960
|
// 另存到本机磁盘;导入按文件名前缀决定覆盖哪一项,不受“同步内容”勾选影响
|
|
906
961
|
const openSyncVersionPicker = () => {
|
|
@@ -1083,16 +1138,21 @@ export function openSettingsDialog() {
|
|
|
1083
1138
|
return;
|
|
1084
1139
|
}
|
|
1085
1140
|
renderSyncStatus(res.sync || {});
|
|
1086
|
-
showToast(L.syncUploaded, 'success');
|
|
1141
|
+
showToast(res.pruned ? L.syncUploaded + (isZhLocale() ? ';' : '; ') + L.syncPruned.replace('{n}', res.pruned) : L.syncUploaded, 'success');
|
|
1087
1142
|
};
|
|
1088
1143
|
paneSync.append(
|
|
1089
1144
|
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.syncDesc),
|
|
1090
1145
|
h('div', { class: 'mt-3 rounded-xl border border-border p-1' },
|
|
1091
1146
|
syncFormRow('URL', syncUrlInput),
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1147
|
+
// 登录与密码一行、上传目录与历史版本一行:短字段成对排,少占纵向空间
|
|
1148
|
+
h('div', { class: 'flex items-center gap-3 px-2 py-1.5' },
|
|
1149
|
+
syncLabel(L.syncLogin), syncLoginInput,
|
|
1150
|
+
syncLabel(L.syncPassword), syncPasswordInput),
|
|
1151
|
+
h('div', { class: 'flex items-center gap-3 px-2 py-1.5' },
|
|
1152
|
+
syncLabel(L.syncDir), syncDirInput,
|
|
1153
|
+
syncLabel(L.syncHistoryLimit), syncHistoryInput,
|
|
1154
|
+
h('span', { class: 'shrink-0 text-ui-xs text-foreground-subtle' }, L.syncHistoryLimitUnit)),
|
|
1155
|
+
h('div', { class: 'flex items-center justify-between gap-2 px-2 py-1.5' },
|
|
1096
1156
|
h('div', { class: 'min-w-0' },
|
|
1097
1157
|
h('p', { class: 'text-ui-xs/relaxed text-foreground-subtle' }, L.syncFormHint),
|
|
1098
1158
|
syncRecLine),
|
|
@@ -1168,8 +1228,6 @@ export function openSettingsDialog() {
|
|
|
1168
1228
|
skipMinutes: 30,
|
|
1169
1229
|
forceIntercept: true,
|
|
1170
1230
|
};
|
|
1171
|
-
// 供应商下拉数据:合并 zcode 两张供应商表(config.json 的 provider 与 provider_config.json 的 providerRules)
|
|
1172
|
-
const visionProviders = visionProvidersRes.ok && Array.isArray(visionProvidersRes.providers) ? visionProvidersRes.providers : [];
|
|
1173
1231
|
let visionCfg = visionRes.ok && visionRes.config && typeof visionRes.config === 'object'
|
|
1174
1232
|
? structuredClone(visionRes.config)
|
|
1175
1233
|
: null;
|
|
@@ -1226,7 +1284,7 @@ export function openSettingsDialog() {
|
|
|
1226
1284
|
}
|
|
1227
1285
|
// 插件未装优先显示安装卡——判断只看钩子(visionRes.hook),与配置文件是否存在无关:
|
|
1228
1286
|
// 配置是钩子消费的,没钩子就没功能;老用户卸载插件后 ~/.zcode/zcode-vision.json 仍在,
|
|
1229
|
-
//
|
|
1287
|
+
// 若按配置存在与否分流会误入完整配置界面而毫无“未安装”提示
|
|
1230
1288
|
if (!visionRes.hook) {
|
|
1231
1289
|
paneVision.replaceChildren(
|
|
1232
1290
|
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.visionDesc),
|
|
@@ -1283,6 +1341,8 @@ export function openSettingsDialog() {
|
|
|
1283
1341
|
return h('label', { class: 'flex shrink-0 items-center gap-1.5', title },
|
|
1284
1342
|
h('span', { class: 'text-ui-xs font-medium text-foreground-subtle' }, label), n);
|
|
1285
1343
|
};
|
|
1344
|
+
// 接口地址 / API Key 是否显示走 provider-fields 的共用规则
|
|
1345
|
+
//(isManualConn:不跟随或供应商已删才显示,连接其余由跟随对象负责)
|
|
1286
1346
|
const cards = visionCfg.proxies.map((p, i) => h('div', { class: 'mt-2 rounded-xl border border-border p-2' },
|
|
1287
1347
|
h('div', { class: 'flex flex-wrap items-center gap-2' },
|
|
1288
1348
|
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)),
|
|
@@ -1313,43 +1373,35 @@ export function openSettingsDialog() {
|
|
|
1313
1373
|
smallBtn(L.visionRemove, () => { visionCfg.proxies.splice(i, 1); void persistVision(); renderVision(); }),
|
|
1314
1374
|
),
|
|
1315
1375
|
h('div', { class: 'mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2' },
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|| visionProviders.some((pr) => pr.id === v || pr.name === v || (pr.aliases || []).includes(v));
|
|
1323
|
-
const sel = h('select', { class: visionInputCls });
|
|
1324
|
-
const syncSelect = () => {
|
|
1325
|
-
const cur = (p.useProvider || '').trim();
|
|
1326
|
-
const opts = [h('option', { value: '' }, L.visionUseProviderNone), h('option', { value: 'session' }, L.visionUseProviderSession)];
|
|
1327
|
-
for (const pr of visionProviders) {
|
|
1328
|
-
opts.push(h('option', { value: pr.id }, pr.name && pr.name !== pr.id ? `${pr.name}(${pr.id})` : pr.id));
|
|
1329
|
-
}
|
|
1330
|
-
if (cur && !opts.some((o) => o.value === cur)) opts.push(h('option', { value: cur }, `${cur}(${L.visionUseProvider})`));
|
|
1331
|
-
sel.replaceChildren(...opts);
|
|
1332
|
-
sel.value = cur;
|
|
1333
|
-
};
|
|
1334
|
-
sel.addEventListener('change', () => {
|
|
1335
|
-
input.value = sel.value;
|
|
1336
|
-
p.useProvider = sel.value.trim();
|
|
1337
|
-
warn.style.display = 'none';
|
|
1376
|
+
// 供应商与模型走 provider-fields 共用逻辑:跟随会话时模型下拉为
|
|
1377
|
+
// 全部供应商模型汇总,具体供应商为其模型列表;已删 id 标注“已删除”
|
|
1378
|
+
field(L.visionUseProvider, comboField(p.useProvider || '',
|
|
1379
|
+
providerOptionsOf(titleGenProviders, L),
|
|
1380
|
+
(v, src) => {
|
|
1381
|
+
p.useProvider = v;
|
|
1338
1382
|
saveVisionSoon();
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1383
|
+
// 下拉切换后重渲染整个面板:模型下拉选项随新供应商变化,接口
|
|
1384
|
+
// 地址/API Key 字段按“不跟随或供应商已删才显示”重新显隐
|
|
1385
|
+
if (src === 'select') renderVision();
|
|
1386
|
+
},
|
|
1387
|
+
{ placeholder: L.titleGenProviderSession, staleLabel: L.titleGenProviderDeleted })),
|
|
1388
|
+
// 跟随会话时模型下拉头部有“跟随当前会话当前模型”(值为空,
|
|
1389
|
+
// 插件 ≥1.6.1 在调用时解析为会话当前模型);具体供应商为其模型列表
|
|
1390
|
+
field(L.visionModel, comboField(p.model || '',
|
|
1391
|
+
modelOptionsOf(p.useProvider, titleGenProviders, {
|
|
1392
|
+
followSession: (p.useProvider || '').trim() === 'session',
|
|
1393
|
+
followSessionLabel: L.titleGenModelFollowSession,
|
|
1394
|
+
}),
|
|
1395
|
+
(v) => { p.model = v; saveVisionSoon(); },
|
|
1396
|
+
{
|
|
1397
|
+
placeholder: modelPlaceholderOf(p.useProvider, L),
|
|
1398
|
+
emptyLabel: (p.useProvider || '').trim() === 'session'
|
|
1399
|
+
? L.titleGenModelFollowSession : L.titleGenModelEmpty,
|
|
1400
|
+
})),
|
|
1401
|
+
...(isManualConn(p.useProvider, titleGenProviders) ? [
|
|
1402
|
+
field(L.visionBaseUrl, textIn(p.baseUrl, (v) => { p.baseUrl = v; saveVisionSoon(); })),
|
|
1403
|
+
field(L.visionApiKey + ' · ' + L.visionApiKeyHint, textIn(p.apiKey, (v) => { p.apiKey = v; saveVisionSoon(); }, { type: 'password' })),
|
|
1404
|
+
] : []),
|
|
1353
1405
|
),
|
|
1354
1406
|
(() => {
|
|
1355
1407
|
const n = h('textarea', { class: 'zcodepro-textarea zcodepro-textarea-sm', rows: '3', placeholder: L.visionPrompt });
|
|
@@ -2011,13 +2063,164 @@ export function openSettingsDialog() {
|
|
|
2011
2063
|
};
|
|
2012
2064
|
renderHeadroomPane();
|
|
2013
2065
|
|
|
2066
|
+
// “任务名提示词”(位于“行为”标签页的二级标签):新会话开始作答时 →
|
|
2067
|
+
// 按自定义供应商/提示词生成标题并按手动重命名写库。配置读写走 /title-gen
|
|
2068
|
+
//(存 zcodepro.json);供应商下拉数据走 /title-gen/providers(与视觉代理
|
|
2069
|
+
// 同源,但只列 helper 能直连调用的条目)。
|
|
2070
|
+
// 结构性改动(开关/供应商/协议)即时保存,提示词与文本输入防抖保存。
|
|
2071
|
+
let titleGenCfg = titleGenRes.ok && titleGenRes.titleGen && typeof titleGenRes.titleGen === 'object'
|
|
2072
|
+
? structuredClone(titleGenRes.titleGen) : null;
|
|
2073
|
+
const titleGenDefaultPrompt = titleGenRes.ok && typeof titleGenRes.defaultPrompt === 'string'
|
|
2074
|
+
? titleGenRes.defaultPrompt : '';
|
|
2075
|
+
let titleGenSaveTimer = null;
|
|
2076
|
+
const persistTitleGen = async () => {
|
|
2077
|
+
const res = await rpc('/title-gen', { method: 'POST', body: { titleGen: titleGenCfg } });
|
|
2078
|
+
clearConfigCache();
|
|
2079
|
+
if (!res.ok) showToast(L.failed + ': ' + errText(res), 'error');
|
|
2080
|
+
return res.ok;
|
|
2081
|
+
};
|
|
2082
|
+
const saveTitleGenSoon = () => {
|
|
2083
|
+
clearTimeout(titleGenSaveTimer);
|
|
2084
|
+
titleGenSaveTimer = setTimeout(() => { void persistTitleGen(); }, 500);
|
|
2085
|
+
};
|
|
2086
|
+
const renderTitleGenPane = () => {
|
|
2087
|
+
if (!titleGenCfg) {
|
|
2088
|
+
paneTitleGen.replaceChildren(
|
|
2089
|
+
h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.titleGenShortDesc),
|
|
2090
|
+
h('p', { class: 'mt-2 text-ui-sm text-destructive' }, L.visionLoadFailed + ': ' + errText(titleGenRes)),
|
|
2091
|
+
);
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
const inputCls = 'h-8 w-full rounded-lg border border-border bg-input px-2.5 text-ui-sm text-foreground outline-none transition-shadow placeholder:text-foreground-subtle focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40';
|
|
2095
|
+
const field = (labelText, node) => h('label', { class: 'block min-w-0' },
|
|
2096
|
+
h('span', { class: 'mb-1 block text-ui-xs font-medium text-foreground-subtle' }, labelText), node);
|
|
2097
|
+
// 供应商 / 模型 / 地址密钥共用 provider-fields 的规则(详见该模块说明)。
|
|
2098
|
+
// 换供应商后重渲染:模型下拉换成新供应商的模型、地址字段按模式显隐;
|
|
2099
|
+
// 手动输入过的模型保留(不在新列表里时显示为“手动输入”项,是否改选
|
|
2100
|
+
// 由用户决定)
|
|
2101
|
+
const showInlineConn = isManualConn(titleGenCfg.provider, titleGenProviders);
|
|
2102
|
+
const providerCombo = comboField(
|
|
2103
|
+
titleGenCfg.provider || '',
|
|
2104
|
+
providerOptionsOf(titleGenProviders, L),
|
|
2105
|
+
(v, src) => {
|
|
2106
|
+
titleGenCfg.provider = v;
|
|
2107
|
+
if (src === 'select') { void persistTitleGen(); renderTitleGenPane(); }
|
|
2108
|
+
else saveTitleGenSoon();
|
|
2109
|
+
},
|
|
2110
|
+
{ placeholder: L.titleGenProviderSession, staleLabel: L.titleGenProviderDeleted });
|
|
2111
|
+
const modelCombo = comboField(
|
|
2112
|
+
titleGenCfg.model || '',
|
|
2113
|
+
// 跟随会话时选项表头部有“跟随当前会话当前模型”(值为空,生成时用
|
|
2114
|
+
// 会话当前模型),选了具体模型后也能再切回它
|
|
2115
|
+
modelOptionsOf(titleGenCfg.provider, titleGenProviders, {
|
|
2116
|
+
followSession: true,
|
|
2117
|
+
followSessionLabel: L.titleGenModelFollowSession,
|
|
2118
|
+
}),
|
|
2119
|
+
(v) => { titleGenCfg.model = v; saveTitleGenSoon(); },
|
|
2120
|
+
{
|
|
2121
|
+
placeholder: modelPlaceholderOf(titleGenCfg.provider, L),
|
|
2122
|
+
emptyLabel: (titleGenCfg.provider || '').trim() === 'session'
|
|
2123
|
+
? L.titleGenModelFollowSession : L.titleGenModelEmpty,
|
|
2124
|
+
});
|
|
2125
|
+
// 接口地址 / API Key / 协议(显隐见上方 showInlineConn):不跟随时由
|
|
2126
|
+
// 用户填写;供应商被删时展示保存的副本,仍可直连或改填
|
|
2127
|
+
const textIn = (value, onInput, { type = 'text', placeholder = '' } = {}) => {
|
|
2128
|
+
const n = h('input', { type, value: value || '', placeholder, class: inputCls });
|
|
2129
|
+
n.addEventListener('input', () => onInput(n.value));
|
|
2130
|
+
return n;
|
|
2131
|
+
};
|
|
2132
|
+
const formatSel = h('select', { class: inputCls },
|
|
2133
|
+
h('option', { value: 'anthropic' }, 'anthropic'),
|
|
2134
|
+
h('option', { value: 'openai' }, 'openai'));
|
|
2135
|
+
formatSel.value = titleGenCfg.format === 'openai' ? 'openai' : 'anthropic';
|
|
2136
|
+
formatSel.addEventListener('change', () => {
|
|
2137
|
+
titleGenCfg.format = formatSel.value;
|
|
2138
|
+
void persistTitleGen();
|
|
2139
|
+
});
|
|
2140
|
+
const inlineConnCard = h('div', {
|
|
2141
|
+
class: 'mt-2 grid grid-cols-1 gap-2 rounded-xl border border-border p-2 sm:grid-cols-3',
|
|
2142
|
+
style: showInlineConn ? '' : 'display:none',
|
|
2143
|
+
},
|
|
2144
|
+
field(L.titleGenBaseUrl, textIn(titleGenCfg.baseUrl, (v) => { titleGenCfg.baseUrl = v; saveTitleGenSoon(); }, { placeholder: 'https://…' })),
|
|
2145
|
+
field(L.titleGenApiKey, textIn(titleGenCfg.apiKey, (v) => { titleGenCfg.apiKey = v; saveTitleGenSoon(); }, { type: 'password' })),
|
|
2146
|
+
field(L.titleGenFormat, formatSel));
|
|
2147
|
+
// 提示词:留空 = 内置默认;“查看默认提示词”把默认值载入编辑框供修改
|
|
2148
|
+
const promptArea = h('textarea', {
|
|
2149
|
+
class: 'zcodepro-textarea zcodepro-textarea-sm', rows: '8',
|
|
2150
|
+
placeholder: L.titleGenPromptPlaceholder,
|
|
2151
|
+
});
|
|
2152
|
+
promptArea.value = titleGenCfg.prompt || '';
|
|
2153
|
+
promptArea.addEventListener('input', () => { titleGenCfg.prompt = promptArea.value; saveTitleGenSoon(); });
|
|
2154
|
+
// 测试:用示例首条消息实际生成一次(不写库),验证供应商/模型/提示词
|
|
2155
|
+
// 连通;示例文本随界面语言取中英文版
|
|
2156
|
+
const sampleText = isZhLocale()
|
|
2157
|
+
? '帮我看下这个报错是什么原因:NullPointerException at com.example.OrderService.calculate(OrderService.java:87)'
|
|
2158
|
+
: 'Why does my Spring Boot app throw NullPointerException intermittently in OrderService.calculate?';
|
|
2159
|
+
const sampleInput = h('input', {
|
|
2160
|
+
type: 'text', class: inputCls + ' min-w-0 flex-1',
|
|
2161
|
+
value: sampleText,
|
|
2162
|
+
});
|
|
2163
|
+
let testBtn;
|
|
2164
|
+
const testState = h('span', { class: 'min-w-0 flex-1 truncate text-ui-xs/relaxed text-foreground-subtle' });
|
|
2165
|
+
testBtn = btnSmall(L.titleGenTest, async () => {
|
|
2166
|
+
testBtn.disabled = true;
|
|
2167
|
+
testBtn.textContent = L.titleGenTesting;
|
|
2168
|
+
testState.textContent = L.titleGenTesting;
|
|
2169
|
+
const res = await rpc('/title-gen/run', {
|
|
2170
|
+
method: 'POST',
|
|
2171
|
+
body: { text: sampleInput.value, test: true },
|
|
2172
|
+
});
|
|
2173
|
+
testBtn.disabled = false;
|
|
2174
|
+
testBtn.textContent = L.titleGenTest;
|
|
2175
|
+
if (res && res.ok) {
|
|
2176
|
+
testState.textContent = L.titleGenTestOk + res.title;
|
|
2177
|
+
showToast(res.title, 'success');
|
|
2178
|
+
} else {
|
|
2179
|
+
testState.textContent = (res && errText(res)) || L.titleGenTestFailed;
|
|
2180
|
+
testState.className = 'min-w-0 flex-1 truncate text-ui-xs/relaxed text-destructive';
|
|
2181
|
+
showToast(L.titleGenTestFailed + ': ' + (res ? errText(res) : ''), 'error');
|
|
2182
|
+
}
|
|
2183
|
+
}, 'shrink-0');
|
|
2184
|
+
paneTitleGen.replaceChildren(
|
|
2185
|
+
// 信息卡:一句话简介在左,启用开关右对齐
|
|
2186
|
+
h('div', { class: 'mt-3 flex items-center justify-between gap-2 rounded-xl border border-border p-2' },
|
|
2187
|
+
h('span', { class: 'min-w-0 text-ui-sm/relaxed text-foreground-subtle' }, L.titleGenShortDesc),
|
|
2188
|
+
h('div', { class: 'shrink-0' }, settingRow(L.titleGenEnabled, '', titleGenCfg.enabled === true, async () => {
|
|
2189
|
+
titleGenCfg.enabled = !(titleGenCfg.enabled === true);
|
|
2190
|
+
await persistTitleGen();
|
|
2191
|
+
renderTitleGenPane();
|
|
2192
|
+
}))),
|
|
2193
|
+
// 供应商 + 模型
|
|
2194
|
+
h('div', { class: 'mt-2 grid grid-cols-1 gap-2 rounded-xl border border-border p-2 sm:grid-cols-2' },
|
|
2195
|
+
field(L.titleGenProvider, providerCombo),
|
|
2196
|
+
field(L.titleGenModel, modelCombo)),
|
|
2197
|
+
inlineConnCard,
|
|
2198
|
+
// 提示词 + 载入默认
|
|
2199
|
+
h('div', { class: 'mt-2 rounded-xl border border-border p-2' },
|
|
2200
|
+
h('div', { class: 'mb-1 flex items-center justify-between gap-2' },
|
|
2201
|
+
h('span', { class: 'text-ui-xs font-medium text-foreground-subtle' }, L.titleGenPrompt),
|
|
2202
|
+
btnSmall(L.titleGenPromptReset, () => {
|
|
2203
|
+
promptArea.value = titleGenDefaultPrompt;
|
|
2204
|
+
titleGenCfg.prompt = titleGenDefaultPrompt;
|
|
2205
|
+
void persistTitleGen();
|
|
2206
|
+
promptArea.focus();
|
|
2207
|
+
})),
|
|
2208
|
+
promptArea),
|
|
2209
|
+
// 测试
|
|
2210
|
+
h('div', { class: 'mt-2 flex items-center gap-2 rounded-xl border border-border p-2' },
|
|
2211
|
+
sampleInput, testBtn),
|
|
2212
|
+
testState,
|
|
2213
|
+
);
|
|
2214
|
+
};
|
|
2215
|
+
renderTitleGenPane();
|
|
2216
|
+
|
|
2014
2217
|
body.append(
|
|
2015
2218
|
statusLine,
|
|
2016
2219
|
tablist,
|
|
2017
2220
|
paneFeatures,
|
|
2221
|
+
paneAgents,
|
|
2018
2222
|
paneStyles,
|
|
2019
2223
|
panePerf,
|
|
2020
|
-
paneAgents,
|
|
2021
2224
|
paneProxy,
|
|
2022
2225
|
paneSync,
|
|
2023
2226
|
paneVision,
|
|
@@ -140,7 +140,7 @@ function buildCss(styles) {
|
|
|
140
140
|
}
|
|
141
141
|
const tlh = styles.thinkingLineHeight;
|
|
142
142
|
if (typeof tlh === 'number' && Number.isFinite(tlh) && tlh >= 0.8) {
|
|
143
|
-
//
|
|
143
|
+
// “思考行高”管“已工作/工作中 N 秒”过程视图的行距(读取/编辑/查阅/终端等
|
|
144
144
|
// 条目行、流式思考预览,默认 1.5):行高挂在历史内容容器上由各行继承;
|
|
145
145
|
// 过程里的叙述正文走 .space-y-4(回答行高,选择器更具体压过这里的继承)。
|
|
146
146
|
// 单独展开的“思考·持续了 N 秒”块内内容不在此设置范围内
|
|
@@ -29,7 +29,7 @@ export function startTaskOrderWatcher() {
|
|
|
29
29
|
if (!row || row.getAttribute('data-task-item-key') === dragKey) return;
|
|
30
30
|
e.preventDefault();
|
|
31
31
|
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
|
32
|
-
// 实时预览:把拖动行移动到目标行前/后(拖动期间 React
|
|
32
|
+
// 实时预览:把拖动行移动到目标行前/后(拖动期间 React 不重渲染;写入数据库后刷新重建)
|
|
33
33
|
const rect = row.getBoundingClientRect();
|
|
34
34
|
const before = e.clientY < rect.top + rect.height / 2;
|
|
35
35
|
const src = [...row.parentElement.querySelectorAll('[data-task-item-key]')]
|