zcode-pro 1.3.1 → 1.4.1

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.
@@ -6,7 +6,7 @@
6
6
  // ZCODEPRO_SYNC_FORCE_FETCH=1 可强制走 fetch(排障/测试用)。
7
7
  import { spawn, spawnSync } from 'node:child_process';
8
8
  import { mkdtemp } from 'node:fs/promises'; // fs 的 mkdtemp 是回调版,await 它会把 undefined 当回调
9
- import { readFileSync, rmSync } from 'node:fs';
9
+ import { readFileSync, rmSync, writeFileSync } from 'node:fs';
10
10
  import { tmpdir } from 'node:os';
11
11
  import { join } from 'node:path';
12
12
 
@@ -88,15 +88,17 @@ function fetchErrText(err) {
88
88
  // curl 不可用时的兜底实现:返回值约定与 curl 版一致({ ok, httpCode, body, error,
89
89
  // redirectUrl },HTTP 4xx/5xx 也算 ok)。凭据走 Authorization 头;proxyUrl 在这条
90
90
  // 路径不生效(fetch 不读代理环境变量,调用方仍照传以免改签名);redirect 不跟随
91
- // (对齐 curl 不带 -L),302 的目标地址放 redirectUrl
92
- async function fetchWebdavRequest({ method, url, login = '', password = '', uploadFile = null, timeoutMs = 45000, headers = [] }) {
91
+ //(对齐 curl 不带 -L),302 的目标地址放 redirectUrl。
92
+ // downloadFile:GET 二进制落到文件(背景图库同步下载用),此时 body 为空串。
93
+ // contentType:PUT 的 Content-Type,缺省 application/json(历史行为),null = 不带
94
+ async function fetchWebdavRequest({ method, url, login = '', password = '', uploadFile = null, timeoutMs = 45000, headers = [], contentType, downloadFile = null }) {
93
95
  // curl 路径的 headers 是 ["Depth: 1"] 字符串数组,fetch 要对象:按第一个冒号拆开
94
96
  const hdrs = {};
95
97
  for (const h of headers) {
96
98
  const i = h.indexOf(':');
97
99
  if (i > 0) hdrs[h.slice(0, i).trim()] = h.slice(i + 1).trim();
98
100
  }
99
- if (method === 'PUT') hdrs['Content-Type'] = 'application/json';
101
+ if (method === 'PUT') hdrs['Content-Type'] = contentType !== undefined ? contentType : 'application/json';
100
102
  if (login || password) hdrs['Authorization'] = 'Basic ' + Buffer.from(`${login}:${password}`).toString('base64');
101
103
  try {
102
104
  const res = await fetch(url, {
@@ -106,6 +108,10 @@ async function fetchWebdavRequest({ method, url, login = '', password = '', uplo
106
108
  signal: AbortSignal.timeout(Math.max(10000, timeoutMs)),
107
109
  body: method === 'PUT' && uploadFile ? readFileSync(uploadFile) : undefined,
108
110
  });
111
+ if (downloadFile) {
112
+ writeFileSync(downloadFile, Buffer.from(await res.arrayBuffer()));
113
+ return { ok: true, httpCode: res.status, body: '', error: '', redirectUrl: res.headers.get('location') || '' };
114
+ }
109
115
  const body = await res.text().catch(() => '');
110
116
  return { ok: true, httpCode: res.status, body, error: '', redirectUrl: res.headers.get('location') || '' };
111
117
  } catch (err) {
@@ -118,20 +124,22 @@ async function fetchWebdavRequest({ method, url, login = '', password = '', uplo
118
124
  // 返回 { ok, httpCode, body, error, redirectUrl }:ok 表示 curl 本身执行成功
119
125
  // (HTTP 4xx/5xx 也算 ok,由调用方看 httpCode 分支);error 为中文原因;
120
126
  // redirectUrl 为重定向目标(请求不跟随重定向,302 时才有值)。
121
- export async function webdavRequest({ method, url, login = '', password = '', uploadFile = null, proxyUrl = '', timeoutMs = 45000, headers = [] }) {
127
+ export async function webdavRequest({ method, url, login = '', password = '', uploadFile = null, proxyUrl = '', timeoutMs = 45000, headers = [], contentType, downloadFile = null }) {
122
128
  const curl = findCurl();
123
- if (!curl) return fetchWebdavRequest({ method, url, login, password, uploadFile, timeoutMs, headers });
129
+ if (!curl) return fetchWebdavRequest({ method, url, login, password, uploadFile, timeoutMs, headers, contentType, downloadFile });
124
130
  const dir = await mkdtemp(join(tmpdir(), 'zcodepro-webdav-'));
125
131
  const bodyFile = join(dir, 'body');
126
132
  try {
127
133
  const args = ['-sS', '--connect-timeout', '10', '--max-time', String(Math.max(10, Math.ceil(timeoutMs / 1000))),
128
- '-w', '%{http_code} %{redirect_url}', '-o', bodyFile, '--config', '-'];
134
+ '-w', '%{http_code} %{redirect_url}', '-o', downloadFile || bodyFile, '--config', '-'];
129
135
  for (const hd of headers) {
130
136
  // 头只能是这里拼好的固定字符串(Depth: 1 等),带换行的一律丢弃
131
137
  if (typeof hd === 'string' && hd && !/[\r\n]/.test(hd)) args.push('-H', hd);
132
138
  }
133
139
  if (method === 'PUT' && uploadFile) {
134
- args.push('-H', 'Content-Type: application/json', '-T', uploadFile);
140
+ const ct = contentType !== undefined ? contentType : 'application/json';
141
+ if (ct) args.push('-H', 'Content-Type: ' + ct);
142
+ args.push('-T', uploadFile);
135
143
  } else {
136
144
  args.push('-X', method);
137
145
  }
@@ -171,9 +179,9 @@ export async function webdavRequest({ method, url, login = '', password = '', up
171
179
 
172
180
  // PUT 前确保云端目录存在:409(父目录缺失)时逐段 MKCOL 再重试一次。
173
181
  // 已存在的目录 MKCOL 多半回 405,忽略即可——这里是尽力而为,最终成败仍看 PUT。
174
- export async function webdavPutWithMkcol({ url, login, password, uploadFile, proxyUrl, timeoutMs }) {
182
+ export async function webdavPutWithMkcol({ url, login, password, uploadFile, proxyUrl, timeoutMs, contentType }) {
175
183
  const auth = { login, password, proxyUrl };
176
- const first = await webdavRequest({ method: 'PUT', url, uploadFile, ...auth, timeoutMs });
184
+ const first = await webdavRequest({ method: 'PUT', url, uploadFile, contentType, ...auth, timeoutMs });
177
185
  if (!first.ok || (first.httpCode >= 200 && first.httpCode < 300) || first.httpCode !== 409) return first;
178
186
  try {
179
187
  const u = new URL(url);
@@ -185,7 +193,7 @@ export async function webdavPutWithMkcol({ url, login, password, uploadFile, pro
185
193
  await webdavRequest({ method: 'MKCOL', url: cur, ...auth, timeoutMs: 15000 });
186
194
  }
187
195
  } catch { /* URL 解析失败:直接重试原 PUT */ }
188
- return webdavRequest({ method: 'PUT', url, uploadFile, ...auth, timeoutMs });
196
+ return webdavRequest({ method: 'PUT', url, uploadFile, contentType, ...auth, timeoutMs });
189
197
  }
190
198
 
191
199
  // 解析 PROPFIND(Depth: 1)的 multistatus 响应,只取文件条目:各网盘的命名空间
@@ -9,6 +9,7 @@ export const HELPER_URL = BOOT.helperUrl || 'http://127.0.0.1:47889';
9
9
  function liveBoot() {
10
10
  return (typeof window !== 'undefined' && window.__ZCODEPRO__) || BOOT;
11
11
  }
12
+ export { liveBoot };
12
13
 
13
14
  export function rpc(path, options = {}) {
14
15
  const boot = liveBoot();
@@ -24,17 +25,28 @@ export function rpc(path, options = {}) {
24
25
  .catch((err) => ({ ok: false, error: String(err && err.message || err) }));
25
26
  }
26
27
 
27
- // 配置缓存:菜单每次打开时拉取,保证设置弹窗里的开关即时生效
28
+ // 配置缓存:菜单每次打开时拉取,保证设置弹窗里的开关即时生效。
29
+ // 并发合并:config-changed 事件的多个监听方会同时 getConfig(true)(绕过
30
+ // 缓存),不合并时一次保存触发 N 个并发全量拉取(每个都是 helper 读盘+序列化)
28
31
  let configCache = { value: null, at: 0 };
32
+ let configInflight = null;
29
33
  export async function getConfig(force = false) {
30
34
  const now = Date.now();
31
35
  if (!force && configCache.value && now - configCache.at < 3000) return configCache.value;
32
- const res = await rpc('/config');
33
- if (res.ok && res.config) {
34
- configCache = { value: res.config, at: now };
35
- return res.config;
36
- }
37
- return configCache.value || { features: {} };
36
+ if (configInflight) return configInflight;
37
+ configInflight = (async () => {
38
+ try {
39
+ const res = await rpc('/config');
40
+ if (res.ok && res.config) {
41
+ configCache = { value: res.config, at: Date.now() };
42
+ return res.config;
43
+ }
44
+ return configCache.value || { features: {} };
45
+ } finally {
46
+ configInflight = null;
47
+ }
48
+ })();
49
+ return configInflight;
38
50
  }
39
51
  export function clearConfigCache() { configCache = { value: null, at: 0 }; }
40
52
 
@@ -53,7 +65,7 @@ export const DEFAULT_SHORTCUTS = {
53
65
  export const STATUS_SESSION_IDS = new Set(['ctx', 'sessionCost', 'questions']);
54
66
  export const STATUS_GLOBAL_IDS = new Set(['todayTokens', 'todayCost', 'totalTokens', 'totalCost', 'tasks']);
55
67
 
56
- // 状态栏统计项的默认清单(顺序即默认显示顺序);设置 → 样式 → 状态栏里
68
+ // 状态栏统计项的默认清单(顺序即默认显示顺序);设置 → 外观 → 状态栏里
57
69
  // 可调顺序与显隐,配置存在 config.statusBar.items(整表覆盖)
58
70
  // 每轮回答统计项(turn-usage.js):回答末尾标注的三段,各自可显隐(顺序固定)
59
71
  export const DEFAULT_TURN_ITEMS = [
@@ -106,7 +118,7 @@ export function isNewProjectSidebar() {
106
118
  const zh = {
107
119
  settingsTitle: 'ZCode Pro',
108
120
  tabFeatures: '功能',
109
- tabStyles: '样式',
121
+ tabStyles: '外观',
110
122
  tabProxy: '代理',
111
123
  tabBehavior: '行为',
112
124
  tabAgents: '全局提示词',
@@ -187,8 +199,6 @@ const zh = {
187
199
  syncVerDeleteDesc: '将删除云端的这份备份,不可恢复:',
188
200
  syncVerDeleted: '已删除云端备份',
189
201
  syncVerSaved: '已保存到',
190
- devMode: '开发模式',
191
- devModeRowDesc: '开启后出现“重载 ZCode Pro”按钮;也可给启动器设环境变量 ZCODEPRO_DEV=1 启用。此标签页仅在开发模式生效时显示。',
192
202
  devModeDesc: '开发调试用:改完 zcode-pro 代码后不必重启 ZCode,点“重载 ZCode Pro”即可加载新代码。流程为自动构建(需在源码目录运行)→ 以新进程重启 ZCode Pro(本地助手与注入脚本都换新)→ 所有 ZCode 窗口自动刷新并重新注入;ZCode 本体与已打开的会话不受影响。',
193
203
  devReload: '重载 ZCode Pro',
194
204
  devReloading: '重载中…',
@@ -296,13 +306,65 @@ const zh = {
296
306
  featureNewtabPanelsDesc: '官方“新标签页”只列官方面板与推荐的官方插件,此功能把第三方市场插件(headroom、rtk 等)的面板卡片加进去,点击在侧边面板打开。',
297
307
  ntpDescSuffix: '插件面板',
298
308
  ntpNeedSession: '先打开一个任务会话,再从新标签页打开插件面板',
299
- featureStatusBarDesc: '主界面底部右下角显示统计项:上下文占用、会话/今日/累计 token、任务数等;各项的顺序与显隐在“样式 → 状态栏”里调整。',
300
- tabStylesPane: '样式调整',
309
+ featureStatusBarDesc: '提问输入框右下角和每轮回答最后显示统计项。',
310
+ tabStylesPane: '样式',
301
311
  tabStatusPane: '状态栏',
312
+ themeColorsPane: '主题',
313
+ themeBgRegions: '分区背景',
314
+ themeBgRegionFollow: '无背景',
315
+ themeBgRegionLeft: '左侧边栏',
316
+ themeBgRegionCenter: '中间区域',
317
+ themeBgRegionRight: '右侧边栏',
318
+ themeBgPane: '背景图',
319
+ themeSkinTitle: '皮肤',
320
+ themeSkinDesc: '知名编辑器主题配色,自动匹配 ZCode 的浅色 / 深色模式;默认保持 ZCode 原样,自定义在下方自行调色。',
321
+ themeSkinCustom: '自定义',
322
+ themeCustomTitle: '自定义主题',
323
+ themeCustomHint: '改颜色即实时生效(自动切到自定义皮肤);浅色、深色各配一次,选好 5 个颜色即可生成整套界面配色。',
324
+ themeCustomLight: '浅色',
325
+ themeCustomDark: '深色',
326
+ themeCustomBg: '背景',
327
+ themeCustomCard: '卡片',
328
+ themeCustomFg: '正文',
329
+ themeCustomMuted: '次级文字',
330
+ themeCustomAccent: '强调色',
331
+ themeBgDesc: '背景来源可选本地图库、文件夹(定时轮换)或网址(定时刷新),左栏 / 中栏 / 右栏可分区独立设置;侧栏、会话流、输入框透出背景,弹窗菜单保持不透明保可读。本地图库随 WebDAV 同步上传(覆盖式,不留历史版本)。',
332
+ themeBgModeNone: '无背景',
333
+ themeBgModeLibrary: '本地图库',
334
+ themeBgModeFolder: '文件夹',
335
+ themeBgModeUrl: '网址',
336
+ themeBgLibrary: '我的图库',
337
+ themeBgUpload: '上传图片',
338
+ themeBgEmpty: '还没有上传图片,支持 png / jpg / webp / gif / bmp / avif,单张 10MB 以内',
339
+ themeBgDelete: '删除',
340
+ themeBgNoneHint: '选择一种背景来源;文件夹可定时轮换,网址可配合随机图 API 定时刷新。',
341
+ themeBgFolderPick: '浏览',
342
+ themeBgFolderPickTitle: '选择背景图文件夹',
343
+ themeBgFolderHint: '选择一个装壁纸的文件夹,图片原地读取不复制。',
344
+ themeBgFolderRandom: '随机顺序',
345
+ themeBgIntervalHint: '切换间隔(分钟)',
346
+ themeBgIntervalUnit: '分钟,0 = 不切换',
347
+ themeBgFolderCount: '文件夹里有 {n} 张图片,定时轮换中。',
348
+ themeBgFolderMissing: '文件夹不存在或里面没有图片。',
349
+ themeBgUrlHint: '定时刷新适合随机图 API(每次请求返回不同图);刷新时带缓存参数避免命中缓存。',
350
+ themeBgUrlIntervalHint: '刷新间隔(分钟)',
351
+ themeBgUrlInvalid: '网址需以 http(s):// 开头',
352
+ themeBgFit: '缩放方式',
353
+ themeBgFitCover: '填满',
354
+ themeBgFitContain: '适应',
355
+ themeBgFitFill: '拉伸',
356
+ themeBgFitTile: '平铺',
357
+ themeBgMask: '遮罩不透明度',
358
+ themeBgMaskHint: '数值越高,图片越向主题背景色淡化,文字对比越清晰',
359
+ themeBgMaskHint2: '图片向主题背景色淡化的程度',
360
+ themeBgBlur: '图片模糊',
361
+ themeBgPanel: '面板不透明度',
362
+ themeBgPanelHint: '侧栏、面板与标签栏的不透明程度。越低,透出的图片越多。',
363
+ themeBgDeletedActive: '当前背景已删除,已自动停用背景',
302
364
  sbItemCtx: '上下文占用',
303
365
  sbItemQuestions: '提问次数',
304
366
  sbMaster: '启用统计',
305
- sbMasterDesc: '总开关:关闭后底部状态栏与每轮回答的用量标注一起停用。',
367
+ sbMasterDesc: '总开关:关闭后输入框右下角的统计行与每轮回答的用量标注一起停用。',
306
368
  sbGroupSession: '会话统计(跟随当前会话)',
307
369
  sbGroupGlobal: '总统计',
308
370
  sbGroupTurn: '每轮统计(回答末尾)',
@@ -515,7 +577,7 @@ const zh = {
515
577
  aliasMigrateDesc: '当前 ZCode 已支持修改项目名称(项目“更多”菜单 → 编辑项目),自定义别名功能已停用。可把已保存的别名写入官方项目名称,勾选要迁移的项目;迁移只改项目名称,磁盘目录不受影响。',
516
578
  aliasMigrateItemHint: '迁移后将作为项目名称保存(可随时在“编辑项目”中再改)。仅无标记的条目可勾选;已迁移、已并入、项目已删除、本机不存在(旧机器路径)以及项目已重命名的都不可勾选——要改这些项目的名字,直接用项目“更多”菜单里的“编辑项目”。',
517
579
  aliasMigrateStateDone: '已迁移',
518
- aliasMigrateStateMerged: '已并入「{name}」',
580
+ aliasMigrateStateMerged: '已并入“{name}”',
519
581
  aliasMigrateStateDeleted: '项目已删除',
520
582
  aliasMigrateStateMissing: '本机不存在',
521
583
  aliasMigrateStateRenamed: '项目已重命名',
@@ -563,7 +625,7 @@ const zh = {
563
625
  const en = {
564
626
  settingsTitle: 'ZCode Pro',
565
627
  tabFeatures: 'Features',
566
- tabStyles: 'Styles',
628
+ tabStyles: 'Appearance',
567
629
  tabProxy: 'Proxy',
568
630
  tabBehavior: 'Behavior',
569
631
  tabAgents: 'Global Prompt',
@@ -644,8 +706,6 @@ const en = {
644
706
  syncVerDeleteDesc: 'This backup will be deleted from the server and cannot be recovered: ',
645
707
  syncVerDeleted: 'Backup deleted from the server.',
646
708
  syncVerSaved: 'Saved to',
647
- devMode: 'Developer mode',
648
- devModeRowDesc: 'Shows the "Reload ZCode Pro" button; can also be enabled with the ZCODEPRO_DEV=1 environment variable. This tab is visible only while developer mode is active.',
649
709
  devModeDesc: 'For development: after changing zcode-pro code, click "Reload ZCode Pro" instead of restarting ZCode. It builds automatically (when running from the source repo), restarts the ZCode Pro launcher process (fresh helper and injected script), and every ZCode window refreshes and re-injects. The ZCode app itself and all sessions stay untouched.',
650
710
  devReload: 'Reload ZCode Pro',
651
711
  devReloading: 'Reloading…',
@@ -753,13 +813,65 @@ const en = {
753
813
  featureNewtabPanelsDesc: "The built-in \"new tab page\" only lists built-in panels and recommended official plugins; this adds cards for third-party marketplace plugin panels (headroom, rtk, …) that open in the side pane.",
754
814
  ntpDescSuffix: 'plugin panel',
755
815
  ntpNeedSession: 'Open a task session first, then open plugin panels from the new tab page',
756
- featureStatusBarDesc: 'Shows statistics at the bottom-right of the main view: context usage, session/today/total tokens, task count, etc.; reorder or hide each item under "Styles → Status bar".',
816
+ featureStatusBarDesc: 'Shows statistics at the bottom-right of the input box and at the end of each answer.',
757
817
  tabStylesPane: 'Styles',
758
818
  tabStatusPane: 'Status bar',
819
+ themeColorsPane: 'Theme',
820
+ themeBgRegions: 'Regional backgrounds',
821
+ themeBgRegionFollow: 'None',
822
+ themeBgRegionLeft: 'Left sidebar',
823
+ themeBgRegionCenter: 'Center area',
824
+ themeBgRegionRight: 'Right sidebar',
825
+ themeBgPane: 'Background',
826
+ themeSkinTitle: 'Skins',
827
+ themeSkinDesc: 'Famous editor theme palettes, auto-matching ZCode\u2019s light / dark mode; "Default" keeps ZCode as-is, "Custom" is tuned below.',
828
+ themeSkinCustom: 'Custom',
829
+ themeCustomTitle: 'Custom theme',
830
+ themeCustomHint: 'Color changes apply live (auto-switches to the custom skin); set light and dark separately — pick 5 colors and the whole palette is derived.',
831
+ themeCustomLight: 'Light',
832
+ themeCustomDark: 'Dark',
833
+ themeCustomBg: 'Background',
834
+ themeCustomCard: 'Card',
835
+ themeCustomFg: 'Text',
836
+ themeCustomMuted: 'Muted text',
837
+ themeCustomAccent: 'Accent',
838
+ themeBgDesc: 'Sources: local library, folder (rotate on a timer) or URL (refresh on a timer); left / center / right regions can each have their own. Sidebar, chat and composer reveal the background while menus and dialogs stay opaque. The library uploads with the zcode-pro sync group (overwrite, no history versions).',
839
+ themeBgModeNone: 'None',
840
+ themeBgModeLibrary: 'Library',
841
+ themeBgModeFolder: 'Folder',
842
+ themeBgModeUrl: 'URL',
843
+ themeBgLibrary: 'My library',
844
+ themeBgUpload: 'Upload',
845
+ themeBgEmpty: 'No images yet. png / jpg / webp / gif / bmp / avif, up to 10MB each',
846
+ themeBgDelete: 'Delete',
847
+ themeBgNoneHint: 'Pick a background source; folders rotate on a timer, URLs refresh on a timer for random-image APIs.',
848
+ themeBgFolderPick: 'Browse',
849
+ themeBgFolderPickTitle: 'Choose a wallpaper folder',
850
+ themeBgFolderHint: 'Pick a folder of wallpapers; images are read in place, not copied.',
851
+ themeBgFolderRandom: 'Random order',
852
+ themeBgIntervalHint: 'Switch interval (minutes)',
853
+ themeBgIntervalUnit: 'min, 0 = off',
854
+ themeBgFolderCount: '{n} images in the folder, rotating on the timer.',
855
+ themeBgFolderMissing: 'Folder missing or contains no images.',
856
+ themeBgUrlHint: 'Timed refresh suits random-image APIs (each request returns a new image); a cache-buster is appended on refresh.',
857
+ themeBgUrlIntervalHint: 'Refresh interval (minutes)',
858
+ themeBgUrlInvalid: 'URL must start with http(s)://',
859
+ themeBgFit: 'Fit',
860
+ themeBgFitCover: 'Fill',
861
+ themeBgFitContain: 'Fit',
862
+ themeBgFitFill: 'Stretch',
863
+ themeBgFitTile: 'Tile',
864
+ themeBgMask: 'Mask opacity',
865
+ themeBgMaskHint: 'Higher values fade the image toward the theme background color for clearer text',
866
+ themeBgMaskHint2: 'How much the image fades toward the theme background color',
867
+ themeBgBlur: 'Image blur',
868
+ themeBgPanel: 'Panel opacity',
869
+ themeBgPanelHint: 'Opacity of sidebar, panels and tab bar. Lower reveals more of the image.',
870
+ themeBgDeletedActive: 'Active background was deleted; background disabled',
759
871
  sbItemCtx: 'Context usage',
760
872
  sbItemQuestions: 'Questions asked',
761
873
  sbMaster: 'Enable statistics',
762
- sbMasterDesc: 'Master switch: turning this off disables both the status bar and per-turn labels.',
874
+ sbMasterDesc: 'Master switch: turning this off disables both the input-box statistics and per-turn labels.',
763
875
  sbGroupSession: 'Session statistics (follows current session)',
764
876
  sbGroupGlobal: 'Global statistics',
765
877
  sbGroupTurn: 'Per-turn statistics (answer rows)',