zcode-pro 0.19.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/bin/zcode-pro.mjs +32 -9
- package/dist/inject.js +114 -22
- package/package.json +1 -1
- package/src/host/helper.mjs +167 -27
- package/src/host/main.mjs +8 -4
- package/src/host/setup.mjs +45 -42
- package/src/host/taskIndex.mjs +178 -3
- package/src/inject/core.js +17 -9
- package/src/inject/features/settings-dialog.js +48 -21
- package/src/inject/features/styles.js +1 -1
- package/src/inject/features/task-order.js +1 -1
- package/src/inject/features/title-gen.js +10 -1
- package/src/inject/features/title-lock.js +59 -1
- package/src/inject/features/ws-running.js +1 -1
- package/src/inject/provider-fields.js +2 -2
package/src/host/helper.mjs
CHANGED
|
@@ -4,19 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
import { createServer } from 'node:http';
|
|
6
6
|
import { execFile, spawn, spawnSync } from 'node:child_process';
|
|
7
|
-
import { chmodSync, copyFileSync, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { appendFileSync, chmodSync, copyFileSync, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import { mkdtemp } from 'node:fs/promises'; // 注意:fs 的 mkdtemp 是回调版,await 它会把 undefined 当回调
|
|
9
9
|
import { basename, delimiter, dirname, isAbsolute, join, resolve, sep } from 'node:path';
|
|
10
10
|
import { homedir, tmpdir } from 'node:os';
|
|
11
11
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
12
12
|
import { readSettings, writeSettingsAtomic, remapSettingsPaths, isProjectOpenInTabs } from './settings.mjs';
|
|
13
|
-
import { taskIndexPath, probeTaskIndexWritable, remapTaskIndexPaths, taskIndexDriverAvailable, listOverriddenTitles, listTaskWorkspaceMap, setTaskTitleIfNotOverridden, getTaskProviderModel } from './taskIndex.mjs';
|
|
13
|
+
import { taskIndexPath, probeTaskIndexWritable, remapTaskIndexPaths, taskIndexDriverAvailable, listOverriddenTitles, listTaskWorkspaceMap, setTaskTitleIfNotOverridden, setCustomSessionTitle, isTaskTitleOverridden, reconcileCustomTitles, getTaskProviderModel } from './taskIndex.mjs';
|
|
14
14
|
import { pickFolderSystem } from './pickFolder.mjs';
|
|
15
15
|
import { reorderWorkspaceTasks, reorderGroupMembers } from './taskOrder.mjs';
|
|
16
16
|
import { webdavRequest, webdavPutWithMkcol, webdavHttpText, parseWebdavListing, findCurl } from './webdav.mjs';
|
|
17
17
|
import { resolveZcodeExecutable } from './paths.mjs';
|
|
18
18
|
|
|
19
|
-
const VERSION = '0.
|
|
19
|
+
const VERSION = '0.20.0';
|
|
20
20
|
|
|
21
21
|
// 全局提示词固定在用户主目录:官方加载器按 HOME/USERPROFILE 拼 .zcode/AGENTS.md,
|
|
22
22
|
// 不读 ZCODE_DATA_BASE_DIR(数据根迁走时全局指令仍在原位)。
|
|
@@ -104,6 +104,7 @@ export function defaultConfig() {
|
|
|
104
104
|
//(用会话当前模型);prompt 留空 = 内置默认提示词
|
|
105
105
|
titleGen: {
|
|
106
106
|
enabled: false,
|
|
107
|
+
customWritten: {},
|
|
107
108
|
provider: 'session',
|
|
108
109
|
model: 'glm-5.3-flash',
|
|
109
110
|
prompt: '',
|
|
@@ -118,14 +119,16 @@ export function defaultConfig() {
|
|
|
118
119
|
// 软件(headroom 与 rtk)的安装、检查更新与升级;sync = WebDAV 同步。同步默认
|
|
119
120
|
// 关闭——坚果云等国内网盘通常直连即可,代理停了同步会跟着失败
|
|
120
121
|
proxyScopes: { plugins: true, deps: true, sync: false },
|
|
121
|
-
//
|
|
122
|
+
// 开发模式(设置弹窗“调试”标签页):开启后出现“重载”按钮,可在不重启
|
|
122
123
|
// ZCode 的情况下加载新代码(构建 → 换新启动器进程 → 各窗口自动刷新重注入)
|
|
123
124
|
devMode: false,
|
|
124
125
|
// 设置同步(设置弹窗“同步”标签页,交互参考 Tampermonkey 的手动同步):
|
|
125
126
|
// 仅 WebDAV、仅在点击按钮时传输(type 字段预留多后端扩展,当前恒为 webdav)。
|
|
126
127
|
// url 为 WebDAV 服务器地址;dir 为其下的上传目录(默认 zcode-pro,避免直接
|
|
127
128
|
// 写进网盘根目录);include 选择同步内容;凭据只存本机、不同步出去;
|
|
128
|
-
// lastPushAt/lastPullAt
|
|
129
|
+
// lastPushAt/lastPullAt 只记录本机操作时间;historyLimit 为云端历史版本保留
|
|
130
|
+
// 个数(同一次上传的各组文件算一个,上传成功后自动删除更早的;0 = 不留历史,
|
|
131
|
+
// 只有固定名最新版)。
|
|
129
132
|
sync: {
|
|
130
133
|
type: 'webdav',
|
|
131
134
|
url: '',
|
|
@@ -133,6 +136,7 @@ export function defaultConfig() {
|
|
|
133
136
|
login: '',
|
|
134
137
|
password: '',
|
|
135
138
|
include: { zcodepro: true, model: true },
|
|
139
|
+
historyLimit: 10,
|
|
136
140
|
lastPushAt: null,
|
|
137
141
|
lastPullAt: null,
|
|
138
142
|
},
|
|
@@ -246,6 +250,20 @@ function proxyUrlFor(scope) {
|
|
|
246
250
|
return activeProxyScopes[scope] ? activeProxyUrl : '';
|
|
247
251
|
}
|
|
248
252
|
|
|
253
|
+
// 智能起名诊断日志:追加到 <dataRoot>/zcodepro.log(与启动诊断同文件),超 512KB
|
|
254
|
+
// 轮转 .old。起名过程注入侧静默(失败不打扰用户),出问题时靠这里回查
|
|
255
|
+
let titleGenLogRotated = false;
|
|
256
|
+
function titleGenLog(dataRoot, line) {
|
|
257
|
+
try {
|
|
258
|
+
const file = join(dataRoot, 'zcodepro.log');
|
|
259
|
+
if (!titleGenLogRotated && existsSync(file) && statSync(file).size > 512 * 1024) {
|
|
260
|
+
renameSync(file, file + '.old');
|
|
261
|
+
titleGenLogRotated = true;
|
|
262
|
+
}
|
|
263
|
+
appendFileSync(file, `[${new Date().toISOString()}] title-gen ${line}\n`);
|
|
264
|
+
} catch { /* 日志失败不影响功能 */ }
|
|
265
|
+
}
|
|
266
|
+
|
|
249
267
|
export function startHelper({ port, token, dataRoot, state, agentsFile = defaultAgentsFile() }) {
|
|
250
268
|
const configFile = join(dataRoot, 'zcodepro.json');
|
|
251
269
|
const settingsFile = join(dataRoot, 'v2', 'setting.json');
|
|
@@ -418,6 +436,13 @@ export function startHelper({ port, token, dataRoot, state, agentsFile = default
|
|
|
418
436
|
// 注入侧据此把被自动标题覆盖的侧栏行钉回重命名值;响应带开关,
|
|
419
437
|
// 注入侧按需拉取这一个端点即可(无轮询)
|
|
420
438
|
if (req.method === 'GET' && url.pathname === '/pinned-titles') {
|
|
439
|
+
// 先自动校正:桌面端打开会话时会把运行时缓存的旧标题写回任务索引(实测),
|
|
440
|
+
// 对我们写过且 CLI custom 仍是所写标题的会话,把索引改回来再下发
|
|
441
|
+
const config0 = loadConfig(configFile);
|
|
442
|
+
if (config0.titleGen.customWritten && Object.keys(config0.titleGen.customWritten).length) {
|
|
443
|
+
const repaired = await reconcileCustomTitles(dataRoot, config0.titleGen.customWritten);
|
|
444
|
+
if (repaired.length) titleGenLog(dataRoot, `reconciled sess=${repaired.map((s) => s.slice(5, 13)).join(',')}`);
|
|
445
|
+
}
|
|
421
446
|
const r = await listOverriddenTitles(dataRoot);
|
|
422
447
|
if (r.error) {
|
|
423
448
|
json(res, 500, { ok: false, code: r.code, error: r.error });
|
|
@@ -488,9 +513,43 @@ export function startHelper({ port, token, dataRoot, state, agentsFile = default
|
|
|
488
513
|
return;
|
|
489
514
|
}
|
|
490
515
|
const sessId = typeof body?.sessId === 'string' && /^sess_[A-Za-z0-9._-]+$/.test(body.sessId) ? body.sessId : '';
|
|
491
|
-
|
|
516
|
+
let title;
|
|
517
|
+
try {
|
|
518
|
+
title = await generateTitleWith(cfg, text, dataRoot, sessId);
|
|
519
|
+
} catch (err) {
|
|
520
|
+
titleGenLog(dataRoot, `generate-failed sess=${sessId.slice(0, 13)} err=${err?.message || err}`);
|
|
521
|
+
throw err; // 交给外层统一 500
|
|
522
|
+
}
|
|
492
523
|
let written = false;
|
|
524
|
+
let store = ''; // CLI 会话库写入结果:'' 成功 | skipped 原因 | 'error'
|
|
493
525
|
if (sessId && body.test !== true) {
|
|
526
|
+
// 已被手动重命名(或前次起名已写过)就整体跳过——手动重命名永远优先,
|
|
527
|
+
// 也避免起名晚到的 CLI 写入盖掉用户的 custom 标题
|
|
528
|
+
if (await isTaskTitleOverridden(dataRoot, sessId)) {
|
|
529
|
+
json(res, 200, { ok: true, title, written: false, store: 'skipped: overridden' });
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
// 双写,顺序不能反:先写 CLI 会话库(title_source=custom,权威层——
|
|
533
|
+
// 官方自动起名看到 custom 会放弃,桌面同步器会把 custom 标题带着
|
|
534
|
+
// overridden=1 送回任务索引),再写任务索引(展示层,立即上屏)。
|
|
535
|
+
// 只写任务索引时,官方运行时排队的自动标题会在几秒后把它覆盖(实测)。
|
|
536
|
+
// CLI 库不可写(旧版无 title_source 列 / 无该会话行)不阻断:退回
|
|
537
|
+
// 单写任务索引的旧行为;任务索引随后失败也不回滚 CLI——同步器会补
|
|
538
|
+
for (let attempt = 0; ; attempt++) {
|
|
539
|
+
try {
|
|
540
|
+
const r = await setCustomSessionTitle(dataRoot, sessId, title);
|
|
541
|
+
store = r.written ? '' : (r.skipped || 'skipped');
|
|
542
|
+
break;
|
|
543
|
+
} catch (err) {
|
|
544
|
+
if (err.code === 'index-busy' && attempt < 2) {
|
|
545
|
+
await new Promise((r) => setTimeout(r, 1200));
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
store = 'error: ' + (err?.message || err);
|
|
549
|
+
titleGenLog(dataRoot, `cli-store-error sess=${sessId.slice(0, 13)} err=${err?.message || err}`);
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
494
553
|
// 写库撞上宿主写入时稍候重试(起名不急这几秒)
|
|
495
554
|
for (let attempt = 0; ; attempt++) {
|
|
496
555
|
try {
|
|
@@ -502,12 +561,19 @@ export function startHelper({ port, token, dataRoot, state, agentsFile = default
|
|
|
502
561
|
await new Promise((r) => setTimeout(r, 1200));
|
|
503
562
|
continue;
|
|
504
563
|
}
|
|
564
|
+
titleGenLog(dataRoot, `index-error sess=${sessId.slice(0, 13)} err=${err?.message || err}`);
|
|
505
565
|
json(res, 500, { ok: false, code: err.code || 'index-error', error: err.message });
|
|
506
566
|
return;
|
|
507
567
|
}
|
|
508
568
|
}
|
|
509
569
|
}
|
|
510
|
-
|
|
570
|
+
// CLI 库写成功了才登记回查记录(reconcileCustomTitles 只认这些记录校正)
|
|
571
|
+
if (sessId && store === '') {
|
|
572
|
+
const c = loadConfig(configFile);
|
|
573
|
+
c.titleGen.customWritten = { ...(c.titleGen.customWritten || {}), [sessId]: { title, at: Date.now() } };
|
|
574
|
+
saveConfig(configFile, c);
|
|
575
|
+
}
|
|
576
|
+
json(res, 200, { ok: true, title, written, store });
|
|
511
577
|
return;
|
|
512
578
|
}
|
|
513
579
|
// —— zcode-vision 插件(图片视觉代理):设置弹窗“视觉代理”标签页读写 ————
|
|
@@ -910,7 +976,7 @@ export function startHelper({ port, token, dataRoot, state, agentsFile = default
|
|
|
910
976
|
json(res, r.status || 500, { ok: false, code: r.code || 'sync-run', error: r.error });
|
|
911
977
|
return;
|
|
912
978
|
}
|
|
913
|
-
json(res, 200, { ok: true, direction, applied: r.applied, sync: loadConfig(configFile).sync });
|
|
979
|
+
json(res, 200, { ok: true, direction, applied: r.applied, ...(typeof r.pruned === 'number' ? { pruned: r.pruned } : {}), sync: loadConfig(configFile).sync });
|
|
914
980
|
return;
|
|
915
981
|
}
|
|
916
982
|
// —— 同步连通性测试(“测试”按钮):用表单当前值逐步验证(可先测再存),不写入配置文件 ——
|
|
@@ -1076,7 +1142,7 @@ export function startHelper({ port, token, dataRoot, state, agentsFile = default
|
|
|
1076
1142
|
if (r.ok) updated += 1;
|
|
1077
1143
|
} else if (!p.installed && only) {
|
|
1078
1144
|
// 安装只走单插件入口(name 指定=各面板的安装卡):
|
|
1079
|
-
//
|
|
1145
|
+
// 全量“检查并更新”/启动自动更新只升级已装插件——卸载必须被尊重,
|
|
1080
1146
|
// 未装插件一律由用户在各 tab 显式点安装
|
|
1081
1147
|
const r = await runZcodeCli(['plugins', 'install', `${p.name}@${ZCODE_PLUGINS_MARKETPLACE}`], 120000);
|
|
1082
1148
|
steps.push({ step: `install ${p.name}`, ok: r.ok, output: r.output });
|
|
@@ -1143,6 +1209,18 @@ function mergeTitleGenConfig(saved) {
|
|
|
1143
1209
|
// 旧版配置的 'cli'(登录账号单独成模式)统一迁移为 'session':
|
|
1144
1210
|
// 登录账号的套餐已在供应商列表里,跟随会话即覆盖原语义
|
|
1145
1211
|
const provider = typeof s.provider === 'string' ? s.provider.trim().slice(0, 200) : d.provider;
|
|
1212
|
+
// customWritten:起名功能写过的 { sessId → { title, at } },自动校正只认这些
|
|
1213
|
+
// 记录(见 reconcileCustomTitles)。只收合法会话 id 与短字符串,超 7 天/500 条的丢弃
|
|
1214
|
+
const written = {};
|
|
1215
|
+
const cutoff = Date.now() - 7 * 24 * 3600 * 1000;
|
|
1216
|
+
let kept = 0;
|
|
1217
|
+
for (const [k, v] of Object.entries((s.customWritten && typeof s.customWritten === 'object' && !Array.isArray(s.customWritten)) ? s.customWritten : {})) {
|
|
1218
|
+
if (kept >= 500 || !/^sess_[A-Za-z0-9._-]+$/.test(k)) continue;
|
|
1219
|
+
if (!v || typeof v !== 'object' || typeof v.title !== 'string' || v.title.length > 200) continue;
|
|
1220
|
+
if (!Number.isFinite(v.at) || v.at < cutoff) continue;
|
|
1221
|
+
written[k] = { title: v.title, at: v.at };
|
|
1222
|
+
kept++;
|
|
1223
|
+
}
|
|
1146
1224
|
return {
|
|
1147
1225
|
enabled: s.enabled === true,
|
|
1148
1226
|
provider: provider === 'cli' ? 'session' : provider,
|
|
@@ -1151,6 +1229,7 @@ function mergeTitleGenConfig(saved) {
|
|
|
1151
1229
|
baseUrl: typeof s.baseUrl === 'string' ? s.baseUrl.trim().slice(0, 500) : d.baseUrl,
|
|
1152
1230
|
apiKey: typeof s.apiKey === 'string' ? s.apiKey.trim().slice(0, 500) : d.apiKey,
|
|
1153
1231
|
format: s.format === 'openai' ? 'openai' : s.format === 'anthropic' ? 'anthropic' : '',
|
|
1232
|
+
customWritten: written,
|
|
1154
1233
|
};
|
|
1155
1234
|
}
|
|
1156
1235
|
|
|
@@ -1179,13 +1258,16 @@ function mergeSyncConfig(saved) {
|
|
|
1179
1258
|
login: typeof saved.login === 'string' ? saved.login.slice(0, 300) : '',
|
|
1180
1259
|
password: typeof saved.password === 'string' ? saved.password.slice(0, 300) : '',
|
|
1181
1260
|
include: { zcodepro: inc.zcodepro !== false, model: inc.model !== false },
|
|
1261
|
+
historyLimit: Number.isInteger(saved.historyLimit) && saved.historyLimit >= 0 && saved.historyLimit <= 100
|
|
1262
|
+
? saved.historyLimit : d.historyLimit,
|
|
1182
1263
|
lastPushAt: Number.isFinite(saved.lastPushAt) ? saved.lastPushAt : null,
|
|
1183
1264
|
lastPullAt: Number.isFinite(saved.lastPullAt) ? saved.lastPullAt : null,
|
|
1184
1265
|
};
|
|
1185
1266
|
}
|
|
1186
1267
|
|
|
1187
1268
|
// POST /sync 表单校验:url/login/password/dir 全量替换(dir 缺省时不动已有值)。
|
|
1188
|
-
// URL 必填,指向 WebDAV 服务器(可含路径前缀);dir
|
|
1269
|
+
// URL 必填,指向 WebDAV 服务器(可含路径前缀);dir 为其下的上传目录,可多级;
|
|
1270
|
+
// historyLimit 可选(缺省时不动已有值),0–100 的整数
|
|
1189
1271
|
function validateSyncForm(raw) {
|
|
1190
1272
|
const url = typeof raw.url === 'string' ? raw.url.trim() : '';
|
|
1191
1273
|
const login = typeof raw.login === 'string' ? raw.login.trim() : '';
|
|
@@ -1206,6 +1288,12 @@ function validateSyncForm(raw) {
|
|
|
1206
1288
|
}
|
|
1207
1289
|
out.dir = dir;
|
|
1208
1290
|
}
|
|
1291
|
+
if (raw.historyLimit !== undefined && raw.historyLimit !== null) {
|
|
1292
|
+
if (!Number.isInteger(raw.historyLimit) || raw.historyLimit < 0 || raw.historyLimit > 100) {
|
|
1293
|
+
return { error: '历史版本保留个数需为 0–100 的整数' };
|
|
1294
|
+
}
|
|
1295
|
+
out.historyLimit = raw.historyLimit;
|
|
1296
|
+
}
|
|
1209
1297
|
return { sync: out };
|
|
1210
1298
|
}
|
|
1211
1299
|
|
|
@@ -1299,8 +1387,10 @@ async function runSettingsSync({ direction, dataRoot, configFile, config, proxyU
|
|
|
1299
1387
|
// 上传:每组独立一个云端文件(zcodepro.json / model.json,始终最新一版),勾选几组
|
|
1300
1388
|
// 写几个;写前预读云端内容,别名与本机的合并后再写(合并规则见 mergeAliases),
|
|
1301
1389
|
// 其余内容整份覆盖。同一份内容再写一个带时间戳的历史版本文件,供下载时任选版本
|
|
1302
|
-
// 恢复(旧版 zcode-pro
|
|
1390
|
+
// 恢复(旧版 zcode-pro 仍读固定名);历史版本最多保留 historyLimit 个(同一时间戳
|
|
1391
|
+
// 的各组文件算一个),上传成功后自动删除更早的
|
|
1303
1392
|
async function runSyncPush({ dataRoot, configFile, config, sync, proxyUrl }) {
|
|
1393
|
+
const keep = Number.isInteger(sync.historyLimit) ? sync.historyLimit : defaultConfig().sync.historyLimit;
|
|
1304
1394
|
const docs = {};
|
|
1305
1395
|
if (sync.include.zcodepro) {
|
|
1306
1396
|
// sync(本机的同步配置与凭据)与 version(本机 helper 版本)不外发
|
|
@@ -1345,10 +1435,13 @@ async function runSyncPush({ dataRoot, configFile, config, sync, proxyUrl }) {
|
|
|
1345
1435
|
if (put.httpCode < 200 || put.httpCode >= 300) {
|
|
1346
1436
|
return { code: 'sync-request', status: 502, error: webdavHttpText(put.httpCode) };
|
|
1347
1437
|
}
|
|
1348
|
-
|
|
1349
|
-
if (
|
|
1350
|
-
|
|
1351
|
-
return { code: 'sync-request', status: 502, error:
|
|
1438
|
+
// 历史版本只在保留个数 > 0 时写(0 = 只留固定名最新版,由上传后清理收尾)
|
|
1439
|
+
if (keep > 0) {
|
|
1440
|
+
const putVersion = await putUrl(syncVersionName(kind, stamp));
|
|
1441
|
+
if (!putVersion.ok) return { code: 'sync-request', status: 502, error: putVersion.error };
|
|
1442
|
+
if (putVersion.httpCode < 200 || putVersion.httpCode >= 300) {
|
|
1443
|
+
return { code: 'sync-request', status: 502, error: webdavHttpText(putVersion.httpCode) };
|
|
1444
|
+
}
|
|
1352
1445
|
}
|
|
1353
1446
|
pushed.push(kind);
|
|
1354
1447
|
}
|
|
@@ -1357,7 +1450,9 @@ async function runSyncPush({ dataRoot, configFile, config, sync, proxyUrl }) {
|
|
|
1357
1450
|
}
|
|
1358
1451
|
sync.lastPushAt = Date.now();
|
|
1359
1452
|
saveConfig(configFile, config);
|
|
1360
|
-
|
|
1453
|
+
// 上传成功后清理超出保留个数的旧历史版本(best-effort,失败不影响上传结果)
|
|
1454
|
+
const pruned = await pruneSyncHistory(sync, proxyUrl, keep);
|
|
1455
|
+
return { applied: pushed, pruned };
|
|
1361
1456
|
}
|
|
1362
1457
|
|
|
1363
1458
|
// 下载:把云端所选组应用到本机(各组独立文件,勾选几组读几组,可只覆盖模型设置)。
|
|
@@ -1500,6 +1595,39 @@ async function listSyncVersions(sync, proxyUrl) {
|
|
|
1500
1595
|
return { files };
|
|
1501
1596
|
}
|
|
1502
1597
|
|
|
1598
|
+
// 从云端文件名列表选出待删除的历史版本(导出以便测试脚本直接驱动):带时间戳的
|
|
1599
|
+
// 文件按时间戳段分组——同一次上传的各组文件共用一个时间戳,算“一个”;时间戳
|
|
1600
|
+
// 段定宽零填充,字典序即时间序。保留最近 keep 个,返回更早的各组文件名;
|
|
1601
|
+
// 固定名(zcodepro.json / model.json)和不相干的文件名不会出现在结果里
|
|
1602
|
+
export function syncHistoryStaleNames(names, keep) {
|
|
1603
|
+
const groups = new Map();
|
|
1604
|
+
for (const name of names) {
|
|
1605
|
+
const ts = (String(name).match(/^(?:zcodepro|model)(-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)\.json$/) || [])[1];
|
|
1606
|
+
if (!ts) continue;
|
|
1607
|
+
if (!groups.has(ts)) groups.set(ts, []);
|
|
1608
|
+
groups.get(ts).push(name);
|
|
1609
|
+
}
|
|
1610
|
+
// 时间戳段按新 → 旧排,slice(keep) 剩下的即超出保留个数的更早各组
|
|
1611
|
+
return [...groups.keys()].sort((a, b) => b.localeCompare(a)).slice(Math.max(0, keep)).flatMap((ts) => groups.get(ts));
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// 上传后清理云端目录里超出保留个数的历史版本:列出文件,删掉 syncHistoryStaleNames
|
|
1615
|
+
// 归出的旧文件。best-effort——列目录或单个删除失败只跳过(下次上传再清),不抛错;
|
|
1616
|
+
// 返回本次删掉的文件数
|
|
1617
|
+
async function pruneSyncHistory(sync, proxyUrl, keep) {
|
|
1618
|
+
const list = await listSyncVersions(sync, proxyUrl);
|
|
1619
|
+
if (list.error) return 0;
|
|
1620
|
+
const stale = syncHistoryStaleNames(list.files.map((f) => f.name), keep);
|
|
1621
|
+
let removed = 0;
|
|
1622
|
+
for (const name of stale) {
|
|
1623
|
+
const r = await webdavRequest({ method: 'DELETE', url: syncFileUrlByName(sync, name), login: sync.login, password: sync.password, proxyUrl, timeoutMs: 30000 });
|
|
1624
|
+
if (!r.ok) continue; // 网络失败:跳过该文件
|
|
1625
|
+
if (r.httpCode !== 404 && (r.httpCode < 200 || r.httpCode >= 300)) continue; // 404 = 已不在,视为已清
|
|
1626
|
+
removed++;
|
|
1627
|
+
}
|
|
1628
|
+
return removed;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1503
1631
|
// 别名表清洗(同步下载用):只收 路径字符串 → 短字符串 的条目,数量限幅
|
|
1504
1632
|
function sanitizeAliases(raw) {
|
|
1505
1633
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
@@ -1607,7 +1735,7 @@ export async function scanIdleBusy(paths, dataRoot) {
|
|
|
1607
1735
|
return SCAN_OFF;
|
|
1608
1736
|
}
|
|
1609
1737
|
|
|
1610
|
-
// Linux
|
|
1738
|
+
// Linux:直接读取 /proc(comm 定位 CLI、cwd 定位项目、task/<pid>/children 拿子进程)
|
|
1611
1739
|
function scanIdleBusyLinux(paths) {
|
|
1612
1740
|
let pids;
|
|
1613
1741
|
try { pids = readdirSync('/proc').filter((d) => /^\d+$/.test(d)); }
|
|
@@ -1794,7 +1922,7 @@ function sanitizeFontStack(v) {
|
|
|
1794
1922
|
return s;
|
|
1795
1923
|
}
|
|
1796
1924
|
|
|
1797
|
-
//
|
|
1925
|
+
// 视觉代理“跟随供应商”下拉取数:与智能起名共用同一套供应商列表和过滤规则
|
|
1798
1926
|
//(见 listTitleGenProviders),只回 id/名称/别名,不含密钥。
|
|
1799
1927
|
// 导出以便测试脚本直接驱动。
|
|
1800
1928
|
export function listVisionProviders() {
|
|
@@ -1853,7 +1981,7 @@ const TITLE_GEN_TIMEOUT_MS = 60000; // 单次模型调用超时(与官方起
|
|
|
1853
1981
|
//(coding-plan 与 start-plan 都叫 "Coding Plan"),界面里按套餐类型显示。
|
|
1854
1982
|
// coding-plan 的个人/团队是动态的:从桌面设置(v2/setting.json 的
|
|
1855
1983
|
// providerFamilyConnectionSelections)读当前登录的连接方式(kind 含
|
|
1856
|
-
// individual → 个人、team →
|
|
1984
|
+
// individual → 个人、team → 团队),读不到时退回“个人/团队”
|
|
1857
1985
|
function codingPlanKindByFamily() {
|
|
1858
1986
|
const s = readJsonFile(join(homedir(), '.zcode', 'v2', 'setting.json'));
|
|
1859
1987
|
const sel = (s && s.providerFamilyConnectionSelections && typeof s.providerFamilyConnectionSelections === 'object')
|
|
@@ -1962,7 +2090,7 @@ async function callAnthropicText(baseUrl, apiKey, model, content) {
|
|
|
1962
2090
|
body: JSON.stringify({ model, max_tokens: 2048, messages: [{ role: 'user', content }] }),
|
|
1963
2091
|
});
|
|
1964
2092
|
const body = await resp.text();
|
|
1965
|
-
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${body.slice(0, 300)}`);
|
|
2093
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status} [model=${model}]: ${body.slice(0, 300)}`);
|
|
1966
2094
|
const doc = JSON.parse(body);
|
|
1967
2095
|
const text = Array.isArray(doc.content)
|
|
1968
2096
|
? doc.content.filter((b) => b && b.type === 'text').map((b) => b.text || '').join('') : '';
|
|
@@ -1988,7 +2116,7 @@ async function callOpenAiText(baseUrl, apiKey, model, content) {
|
|
|
1988
2116
|
body: JSON.stringify({ model, max_tokens: 2048, messages: [{ role: 'user', content }] }),
|
|
1989
2117
|
});
|
|
1990
2118
|
const body = await resp.text();
|
|
1991
|
-
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${body.slice(0, 300)}`);
|
|
2119
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status} [model=${model}]: ${body.slice(0, 300)}`);
|
|
1992
2120
|
const doc = JSON.parse(body);
|
|
1993
2121
|
const text = doc.choices?.[0]?.message?.content || '';
|
|
1994
2122
|
if (typeof text !== 'string' || !text.trim()) throw new Error('响应中没有文本内容');
|
|
@@ -2008,7 +2136,12 @@ async function callOpenAiText(baseUrl, apiKey, model, content) {
|
|
|
2008
2136
|
export async function generateTitleWith(cfg, rawText, dataRoot = '', sessId = '') {
|
|
2009
2137
|
const text = String(rawText || '').trim().replace(/\s+/g, ' ').slice(0, TITLE_GEN_MAX_TEXT);
|
|
2010
2138
|
const prompt = (cfg.prompt && cfg.prompt.trim()) || titleGenDefaultPrompt();
|
|
2011
|
-
|
|
2139
|
+
// 注入当前日期(上海时区):起名模型是无状态的,自定义提示词里要求
|
|
2140
|
+
// “MMDD 取对话创建日期”时它无从得知,只能留字面量 MMDD
|
|
2141
|
+
const now = new Date(Date.now() + 8 * 3600 * 1000);
|
|
2142
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
2143
|
+
const dateLine = `当前日期(Asia/Shanghai):${now.getUTCFullYear()}-${pad(now.getUTCMonth() + 1)}-${pad(now.getUTCDate())}(MMDD 即 ${pad(now.getUTCMonth() + 1)}${pad(now.getUTCDate())})`;
|
|
2144
|
+
const userContent = `${prompt}\n\n${dateLine}\n\n会话的用户消息:\n${text}`;
|
|
2012
2145
|
const providerId = (cfg.provider || '').trim();
|
|
2013
2146
|
const model = (cfg.model || '').trim();
|
|
2014
2147
|
const conn = await resolveTitleGenConnection(cfg, providerId, model, dataRoot, sessId);
|
|
@@ -2056,12 +2189,19 @@ async function resolveTitleGenConnection(cfg, providerId, model, dataRoot, sessI
|
|
|
2056
2189
|
}
|
|
2057
2190
|
|
|
2058
2191
|
// 清洗模型输出:去思考块/代码围栏/首尾引号/序号/末尾标点,取首个非空行并限长。
|
|
2059
|
-
// 模型可能沿用官方起名的习惯回 JSON 对象,此时取其 title
|
|
2192
|
+
// 模型可能沿用官方起名的习惯回 JSON 对象,此时取其 title 字段。自定义提示词常让
|
|
2193
|
+
// 模型“先说明再给标题”(如“无法直接修改会话标题……建议手动设置:”后另起一行
|
|
2194
|
+
// 【MMDD|类型|主题】),此时首个非空行是说明而非标题——优先取独立成行的
|
|
2195
|
+
// 【】/[] 包裹标题行(允许行首 markdown 标题符)。导出以便测试
|
|
2060
2196
|
export function cleanGeneratedTitle(text) {
|
|
2061
2197
|
let t = String(text || '').replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
|
|
2062
|
-
//
|
|
2198
|
+
// 整段包在代码围栏里时先去掉围栏标记(先取行会把“```json”当成标题内容)
|
|
2063
2199
|
t = t.replace(/^```[a-zA-Z]*\s*\n?/, '').replace(/\n?```\s*$/, '').trim();
|
|
2064
|
-
|
|
2200
|
+
const lines = t.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
2201
|
+
// 标题行判定前剥掉 markdown 外壳(## 标题符、**加粗**、*斜体*、_下划线_、~~删除线~~)
|
|
2202
|
+
const unwrap = (s) => s.replace(/^#{1,4}\s*/, '').replace(/^[*_~]+/, '').replace(/[*_~]+$/, '').trim();
|
|
2203
|
+
const bracketed = lines.find((s) => /^(?:【[^】]{1,60}】|\[[^\]\n]{1,60}\])$/.test(unwrap(s)));
|
|
2204
|
+
let line = bracketed ? unwrap(bracketed) : (lines[0] || '');
|
|
2065
2205
|
line = line.replace(/^```[a-zA-Z]*\s*/, '').replace(/```\s*$/, '');
|
|
2066
2206
|
try {
|
|
2067
2207
|
const doc = JSON.parse(line);
|
|
@@ -2411,7 +2551,7 @@ async function ensureUserPathEntry(dir) {
|
|
|
2411
2551
|
}
|
|
2412
2552
|
|
|
2413
2553
|
// 在插件缓存里找市场插件的钩子脚本:只认 installed_plugins.json 登记的 installPath。
|
|
2414
|
-
//
|
|
2554
|
+
// 不能按目录扫描:卸载只删“当前指向”的版本目录,缓存里的历史版本目录会残留
|
|
2415
2555
|
// (升级留旧版、GitHub/本地源先后装过等),扫目录会把已卸载的插件误判为已装,
|
|
2416
2556
|
// 面板就不显示安装卡了。本地开发流(install-plugin-local.sh)同样会更新安装记录,兼容。
|
|
2417
2557
|
// 注意只认正式市场 duanluan-zcode-plugins:本地临时市场(如 duanluan-local)装的副本不在查找范围,
|
|
@@ -2430,7 +2570,7 @@ function findVisionHook() {
|
|
|
2430
2570
|
return findMarketplaceHook('zcode-vision', join('hooks', 'vision-hook.mjs'));
|
|
2431
2571
|
}
|
|
2432
2572
|
|
|
2433
|
-
// 读市场插件在 installed_plugins.json
|
|
2573
|
+
// 读市场插件在 installed_plugins.json 里登记的版本(面板信息卡显示“插件 x.y.z”用)
|
|
2434
2574
|
function installedPluginVersion(pluginName) {
|
|
2435
2575
|
const installedRaw = readJsonFile(join(cliPluginsDir(), 'installed_plugins.json'));
|
|
2436
2576
|
const rec = (installedRaw?.plugins || []).find((p) => p?.name === pluginName && p?.marketplace === ZCODE_PLUGINS_MARKETPLACE);
|
package/src/host/main.mjs
CHANGED
|
@@ -17,11 +17,15 @@ import { runSetup } from './setup.mjs';
|
|
|
17
17
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
18
18
|
const INJECT_BUNDLE = resolve(here, '..', '..', 'dist', 'inject.js');
|
|
19
19
|
|
|
20
|
-
// Windows 控制台中文乱码适配:输出为 UTF-8 字节,旧版 conhost/
|
|
21
|
-
|
|
22
|
-
//
|
|
20
|
+
// Windows 控制台中文乱码适配:输出为 UTF-8 字节,旧版 conhost/cmd 的默认代码页
|
|
21
|
+
//(936/GBK)按 GBK 解码会显示乱码。检测到非 UTF-8 代码页时切到 65001——只影响
|
|
22
|
+
// 当前控制台会话(重开终端即恢复)。不能用 stdout.isTTY 单独作前提:npm 全局
|
|
23
|
+
// 入口(bin/zcode-pro.mjs)spawn ZCode 运行时跑 cli.mjs,子进程 stdout 经
|
|
24
|
+
// stdio:'inherit' 是管道(isTTY=false),但控制台乱码照样发生(0.18/0.19 用户
|
|
25
|
+
// 实拍);此时 stdin 仍连着终端,故用 stdout 或 stdin 任一 isTTY 判定。真正的
|
|
26
|
+
// 重定向场景(stdout 与 stdin 都不是 TTY)不干预。
|
|
23
27
|
// 放在模块顶层:cli.mjs 经 import main.mjs 进入,setup 等子命令的输出同样受益。
|
|
24
|
-
if (process.platform === 'win32' && process.stdout.isTTY) {
|
|
28
|
+
if (process.platform === 'win32' && (process.stdout.isTTY || process.stdin.isTTY)) {
|
|
25
29
|
try {
|
|
26
30
|
const cp = execSync('chcp.com', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true }).match(/(\d+)/)?.[1];
|
|
27
31
|
if (cp && cp !== '65001') execSync('chcp.com 65001', { stdio: 'ignore', windowsHide: true });
|