zcode-pro 0.20.3 → 1.1.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 +9 -6
- package/README.md +8 -5
- package/dist/inject.js +2658 -1318
- package/package.json +1 -1
- package/src/host/helper.mjs +156 -60
- package/src/host/main.mjs +7 -4
- package/src/host/taskIndex.mjs +97 -0
- package/src/host/version.mjs +51 -0
- package/src/inject/core.js +177 -24
- package/src/inject/features/alias-migration.js +470 -0
- package/src/inject/features/alias.js +6 -1
- package/src/inject/features/idle-reclaim.js +23 -48
- package/src/inject/features/pinned-collapse.js +4 -3
- package/src/inject/features/project-menu.js +209 -19
- package/src/inject/features/sandbox-reclaim.js +121 -0
- package/src/inject/features/session-switch.js +164 -25
- package/src/inject/features/settings-dialog.js +225 -61
- package/src/inject/features/styles.js +8 -1
- package/src/inject/features/title-lock.js +20 -2
- package/src/inject/features/toolbar-icons.js +85 -45
- package/src/inject/features/ws-rows.js +62 -0
- package/src/inject/features/ws-running.js +391 -27
- package/src/inject/index.js +3 -2
- package/src/inject/ui.js +34 -11
- package/src/inject/features/pinned-expand.js +0 -142
package/package.json
CHANGED
package/src/host/helper.mjs
CHANGED
|
@@ -11,13 +11,13 @@ import { basename, delimiter, dirname, isAbsolute, join, resolve, sep } from 'no
|
|
|
11
11
|
import { homedir, tmpdir, userInfo } from 'node:os';
|
|
12
12
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
13
13
|
import { readSettings, writeSettingsAtomic, remapSettingsPaths, isProjectOpenInTabs } from './settings.mjs';
|
|
14
|
-
import { taskIndexPath, probeTaskIndexWritable, remapTaskIndexPaths, taskIndexDriverAvailable, listOverriddenTitles, listTaskWorkspaceMap, setTaskTitleIfNotOverridden, setCustomSessionTitle, isTaskTitleOverridden, reconcileCustomTitles, getTaskProviderModel } from './taskIndex.mjs';
|
|
14
|
+
import { taskIndexPath, probeTaskIndexWritable, remapTaskIndexPaths, taskIndexDriverAvailable, listOverriddenTitles, listTaskWorkspaceMap, setTaskTitleIfNotOverridden, setCustomSessionTitle, isTaskTitleOverridden, reconcileCustomTitles, getTaskProviderModel, listVirtualProjects } from './taskIndex.mjs';
|
|
15
15
|
import { pickFolderSystem } from './pickFolder.mjs';
|
|
16
16
|
import { reorderWorkspaceTasks, reorderGroupMembers } from './taskOrder.mjs';
|
|
17
17
|
import { webdavRequest, webdavPutWithMkcol, webdavHttpText, parseWebdavListing, findCurl } from './webdav.mjs';
|
|
18
18
|
import { resolveZcodeExecutable } from './paths.mjs';
|
|
19
19
|
|
|
20
|
-
const VERSION = '
|
|
20
|
+
const VERSION = '1.1.0';
|
|
21
21
|
|
|
22
22
|
// 全局提示词固定在用户主目录:官方加载器按 HOME/USERPROFILE 拼 .zcode/AGENTS.md,
|
|
23
23
|
// 不读 ZCODE_DATA_BASE_DIR(数据根迁走时全局指令仍在原位)。
|
|
@@ -56,7 +56,6 @@ export function defaultConfig() {
|
|
|
56
56
|
projectMenu: true, // 项目菜单增强:自定义别名 + 切换文件夹 + 打开文件夹 + 复制路径(含侧栏别名渲染)
|
|
57
57
|
taskOrder: true, // 侧边栏会话拖动排序持久化
|
|
58
58
|
fileActions: true, // 文件菜单:默认应用打开 / 打开所在目录
|
|
59
|
-
pinnedKeepCollapsed: false, // 点击置顶会话保持项目折叠(实验性:会先展开再缩起,有闪烁)
|
|
60
59
|
autoUpdatePlugins: false, // 启动时自动更新已装的 zcode-plugins 插件(含安装市场里新增的)
|
|
61
60
|
sessionSwitch: true, // 会话快捷切换(alt+z 上次会话;按住 alt x/c 弹窗导航,类 alt+tab)
|
|
62
61
|
titleLock: true, // 会话名锁定:手动重命名过的会话不被运行时推送的自动标题覆盖(侧栏钉回索引库中的名字)
|
|
@@ -324,6 +323,11 @@ export function startHelper({ port, token, dataRoot, state, agentsFile = default
|
|
|
324
323
|
current.devMode = body.devMode;
|
|
325
324
|
}
|
|
326
325
|
if (body && typeof body === 'object' && body.features && typeof body.features === 'object') {
|
|
326
|
+
// 会话名锁定开关的每次变化都落日志:它被意外关闭两次(页面里是关、
|
|
327
|
+
// 配置文件是开,来源不明),有记录才能查到是谁改的
|
|
328
|
+
if (typeof body.features.titleLock === 'boolean' && body.features.titleLock !== (loadConfig(configFile).features.titleLock !== false)) {
|
|
329
|
+
titleGenLog(dataRoot, `titleLock ${body.features.titleLock ? 'on' : 'off'}(此前为 ${!body.features.titleLock ? 'on' : 'off'})`);
|
|
330
|
+
}
|
|
327
331
|
applyFeaturesPartial(current.features, body.features);
|
|
328
332
|
}
|
|
329
333
|
if (body && typeof body === 'object' && typeof body.idleReclaimMinutes === 'number' && Number.isFinite(body.idleReclaimMinutes)) {
|
|
@@ -335,9 +339,26 @@ export function startHelper({ port, token, dataRoot, state, agentsFile = default
|
|
|
335
339
|
if (body && typeof body === 'object' && body.styles && typeof body.styles === 'object') {
|
|
336
340
|
current.styles = applyStylesPartial(current.styles, body.styles);
|
|
337
341
|
}
|
|
342
|
+
// 会话快捷切换的键位(设置 → 快捷键里修改):键名固定,值限单个字母或数字
|
|
343
|
+
if (body && typeof body === 'object' && body.shortcuts && typeof body.shortcuts === 'object') {
|
|
344
|
+
const patch = {};
|
|
345
|
+
for (const k of ['switchLast', 'switchPrev', 'switchNext', 'switchRecent', 'switchAttention']) {
|
|
346
|
+
const v = body.shortcuts[k];
|
|
347
|
+
if (typeof v === 'string' && /^[a-z0-9]$/.test(v)) patch[k] = v;
|
|
348
|
+
}
|
|
349
|
+
current.shortcuts = { ...(current.shortcuts || {}), ...patch };
|
|
350
|
+
}
|
|
338
351
|
if (body && typeof body === 'object' && body.titleGen && typeof body.titleGen === 'object') {
|
|
339
352
|
current.titleGen = mergeTitleGenConfig({ ...current.titleGen, ...body.titleGen });
|
|
340
353
|
}
|
|
354
|
+
// 一次性迁移标记(如别名迁移弹窗的“暂不迁移”):按项合并,只接受布尔值
|
|
355
|
+
if (body && typeof body === 'object' && body.migrations && typeof body.migrations === 'object') {
|
|
356
|
+
const patch = {};
|
|
357
|
+
for (const [k, v] of Object.entries(body.migrations)) {
|
|
358
|
+
if (typeof v === 'boolean') patch[k] = v;
|
|
359
|
+
}
|
|
360
|
+
current.migrations = { ...(current.migrations || {}), ...patch };
|
|
361
|
+
}
|
|
341
362
|
saveConfig(configFile, current);
|
|
342
363
|
json(res, 200, { ok: true, config: current });
|
|
343
364
|
return;
|
|
@@ -383,6 +404,16 @@ export function startHelper({ port, token, dataRoot, state, agentsFile = default
|
|
|
383
404
|
json(res, ...writeAgents(body, agentsFile));
|
|
384
405
|
return;
|
|
385
406
|
}
|
|
407
|
+
if (req.method === 'GET' && url.pathname === '/projects-v2') {
|
|
408
|
+
// 虚拟项目(3.16.1 起 projects/project_roots 表):别名迁移弹窗用 roots
|
|
409
|
+
// 区分“目录已并入其他项目”与“项目已删除”。读不到时 available=false,
|
|
410
|
+
// 注入端降级为纯 DOM 检测
|
|
411
|
+
const data = await listVirtualProjects(dataRoot);
|
|
412
|
+
json(res, 200, data
|
|
413
|
+
? { ok: true, available: true, ...data }
|
|
414
|
+
: { ok: true, available: false, projects: [], deletedRoots: [] });
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
386
417
|
if (req.method === 'GET' && url.pathname === '/projects') {
|
|
387
418
|
const settings = readSettings(settingsFile);
|
|
388
419
|
const projects = (settings.recentProjects || []).map((p) => {
|
|
@@ -1106,7 +1137,9 @@ export function startHelper({ port, token, dataRoot, state, agentsFile = default
|
|
|
1106
1137
|
// —— zcode-plugins 市场更新:读状态(带 30 分钟节流的市场同步)与执行更新 ————
|
|
1107
1138
|
if (req.method === 'GET' && url.pathname === '/plugins/status') {
|
|
1108
1139
|
await ensureZcodePluginsMarketplace();
|
|
1109
|
-
|
|
1140
|
+
// 有更新任务在执行或排队时跳过这里的节流同步(任务本身会同步市场并刷新
|
|
1141
|
+
// 时间戳),避免两处同时跑 marketplace update
|
|
1142
|
+
if (!pluginsUpdateJob && Date.now() - lastMarketplaceSync > 30 * 60 * 1000) {
|
|
1110
1143
|
await runZcodeCli(['plugins', 'marketplace', 'update', ZCODE_PLUGINS_MARKETPLACE], 60000);
|
|
1111
1144
|
lastMarketplaceSync = Date.now();
|
|
1112
1145
|
}
|
|
@@ -1120,40 +1153,8 @@ export function startHelper({ port, token, dataRoot, state, agentsFile = default
|
|
|
1120
1153
|
}
|
|
1121
1154
|
if (req.method === 'POST' && url.pathname === '/plugins/update') {
|
|
1122
1155
|
const body = await readBody(req);
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
const steps = [];
|
|
1126
|
-
const added = await ensureZcodePluginsMarketplace();
|
|
1127
|
-
if (added) steps.push({ step: 'marketplace add', ok: added.ok, output: added.output || added.error });
|
|
1128
|
-
const sync = await runZcodeCli(['plugins', 'marketplace', 'update', ZCODE_PLUGINS_MARKETPLACE], 60000);
|
|
1129
|
-
lastMarketplaceSync = Date.now();
|
|
1130
|
-
steps.push({ step: 'marketplace update', ok: sync.ok, output: sync.output });
|
|
1131
|
-
if (!sync.ok && sync.code !== 'cli-not-found') {
|
|
1132
|
-
json(res, 500, { ok: false, code: 'plugins-update', error: sync.error || sync.output || 'marketplace update 失败', steps });
|
|
1133
|
-
return;
|
|
1134
|
-
}
|
|
1135
|
-
const status = pluginUpdateStatus();
|
|
1136
|
-
if (status) {
|
|
1137
|
-
let updated = 0;
|
|
1138
|
-
for (const p of status.plugins) {
|
|
1139
|
-
if (only && p.name !== only) continue;
|
|
1140
|
-
if (p.installed && p.hasUpdate) {
|
|
1141
|
-
const r = await runZcodeCli(['plugins', 'update', `${p.name}@${ZCODE_PLUGINS_MARKETPLACE}`], 120000);
|
|
1142
|
-
steps.push({ step: `update ${p.name}`, ok: r.ok, output: r.output });
|
|
1143
|
-
if (r.ok) updated += 1;
|
|
1144
|
-
} else if (!p.installed && only) {
|
|
1145
|
-
// 安装只走单插件入口(name 指定=各面板的安装卡):
|
|
1146
|
-
// 全量“检查并更新”/启动自动更新只升级已装插件——卸载必须被尊重,
|
|
1147
|
-
// 未装插件一律由用户在各 tab 显式点安装
|
|
1148
|
-
const r = await runZcodeCli(['plugins', 'install', `${p.name}@${ZCODE_PLUGINS_MARKETPLACE}`], 120000);
|
|
1149
|
-
steps.push({ step: `install ${p.name}`, ok: r.ok, output: r.output });
|
|
1150
|
-
if (r.ok) updated += 1;
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
json(res, 200, { ok: steps.every((s) => s.ok), updated, steps, ...pluginUpdateStatus() });
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
json(res, 200, { ok: sync.ok, updated: 0, steps });
|
|
1156
|
+
const result = await enqueuePluginsUpdate(body);
|
|
1157
|
+
json(res, result.status, result.payload);
|
|
1157
1158
|
return;
|
|
1158
1159
|
}
|
|
1159
1160
|
json(res, 404, { ok: false, error: 'not found' });
|
|
@@ -1386,16 +1387,17 @@ async function runSettingsSync({ direction, dataRoot, configFile, config, proxyU
|
|
|
1386
1387
|
}
|
|
1387
1388
|
|
|
1388
1389
|
// 上传:每组独立一个云端文件(zcodepro.json / model.json,始终最新一版),勾选几组
|
|
1389
|
-
//
|
|
1390
|
-
//
|
|
1391
|
-
//
|
|
1392
|
-
//
|
|
1390
|
+
// 写几个。aliases 不再上传——3.16.1 起别名功能已停用(官方“编辑项目”改名),
|
|
1391
|
+
// 且各机器项目路径不同,同步别名只会互相带来无效路径。同一份内容再写一个带
|
|
1392
|
+
// 时间戳的历史版本文件,供下载时任选版本恢复(旧版 zcode-pro 仍读固定名);
|
|
1393
|
+
// 历史版本最多保留 historyLimit 个(同一时间戳的各组文件算一个),上传成功后
|
|
1394
|
+
// 自动删除更早的
|
|
1393
1395
|
async function runSyncPush({ dataRoot, configFile, config, sync, proxyUrl }) {
|
|
1394
1396
|
const keep = Number.isInteger(sync.historyLimit) ? sync.historyLimit : defaultConfig().sync.historyLimit;
|
|
1395
1397
|
const docs = {};
|
|
1396
1398
|
if (sync.include.zcodepro) {
|
|
1397
|
-
// sync
|
|
1398
|
-
const { sync: _s, version: _v, ...rest } = config;
|
|
1399
|
+
// sync(本机的同步配置与凭据)、version(本机 helper 版本)与 aliases 不外发
|
|
1400
|
+
const { sync: _s, version: _v, aliases: _a, ...rest } = config;
|
|
1399
1401
|
docs.zcodepro = rest;
|
|
1400
1402
|
}
|
|
1401
1403
|
if (sync.include.model) {
|
|
@@ -1419,11 +1421,6 @@ async function runSyncPush({ dataRoot, configFile, config, sync, proxyUrl }) {
|
|
|
1419
1421
|
for (const kind of Object.keys(docs)) {
|
|
1420
1422
|
const remote = await fetchSyncDoc(sync, proxyUrl, kind);
|
|
1421
1423
|
if (remote.error) return remote;
|
|
1422
|
-
// 别名先与云端合并:本机没配过的条目从云端带上,别让这次上传把别的机器配的别名顶掉
|
|
1423
|
-
if (kind === 'zcodepro' && remote.doc) {
|
|
1424
|
-
const remoteAliases = sanitizeAliases(remote.doc.data?.aliases);
|
|
1425
|
-
if (remoteAliases) docs.zcodepro.aliases = mergeAliases(docs.zcodepro.aliases, remoteAliases);
|
|
1426
|
-
}
|
|
1427
1424
|
const doc = { app: 'zcode-pro', kind, format: 1, savedAt: stamp.toISOString(), data: docs[kind] };
|
|
1428
1425
|
const docPath = join(dir, SYNC_FILES[kind]);
|
|
1429
1426
|
writeFileSync(docPath, JSON.stringify(doc, null, 2), 'utf8');
|
|
@@ -1457,8 +1454,9 @@ async function runSyncPush({ dataRoot, configFile, config, sync, proxyUrl }) {
|
|
|
1457
1454
|
}
|
|
1458
1455
|
|
|
1459
1456
|
// 下载:把云端所选组应用到本机(各组独立文件,勾选几组读几组,可只覆盖模型设置)。
|
|
1460
|
-
// 覆盖前留 .zcodepro-backup 备份;zcodepro
|
|
1461
|
-
|
|
1457
|
+
// 覆盖前留 .zcodepro-backup 备份;zcodepro 组覆盖功能/样式/代理,别名不受影响
|
|
1458
|
+
//(3.16.1 起别名已停用,且云端备份里的别名多是其他机器的路径);sync 配置与
|
|
1459
|
+
// 凭据始终保留本机的
|
|
1462
1460
|
async function runSyncPull({ dataRoot, configFile, config, sync, proxyUrl }) {
|
|
1463
1461
|
// 先把勾选组的云端数据全部读到(任何一组读取失败都不动本机文件),再逐组应用
|
|
1464
1462
|
const docs = {};
|
|
@@ -1476,8 +1474,9 @@ async function runSyncPull({ dataRoot, configFile, config, sync, proxyUrl }) {
|
|
|
1476
1474
|
}
|
|
1477
1475
|
|
|
1478
1476
|
// 把云端读到的各组数据落到本机(runSyncPull 与按文件的历史版本导入共用):
|
|
1479
|
-
// zcodepro 组覆盖功能/样式/代理(sync
|
|
1480
|
-
|
|
1477
|
+
// zcodepro 组覆盖功能/样式/代理(sync 配置与凭据保留本机的),别名不导入
|
|
1478
|
+
//(云端备份可能出自别的机器,路径在本机无效),model 组覆盖 v2/config.json 的
|
|
1479
|
+
// provider 与 provider_config.json;覆盖前都留备份
|
|
1481
1480
|
function applySyncDocs({ docs, dataRoot, configFile, config, sync }) {
|
|
1482
1481
|
const applied = [];
|
|
1483
1482
|
const backupFile = (p) => {
|
|
@@ -1494,10 +1493,8 @@ function applySyncDocs({ docs, dataRoot, configFile, config, sync }) {
|
|
|
1494
1493
|
const next = loadConfig(configFile);
|
|
1495
1494
|
applyFeaturesPartial(next.features, rz.features);
|
|
1496
1495
|
next.styles = applyStylesPartial(next.styles, rz.styles);
|
|
1497
|
-
//
|
|
1498
|
-
//
|
|
1499
|
-
const remoteAliases = sanitizeAliases(rz.aliases);
|
|
1500
|
-
if (remoteAliases) next.aliases = mergeAliases(next.aliases, remoteAliases);
|
|
1496
|
+
// 别名保持本机的原样:云端备份里的别名可能出自其他机器(路径在本机无效),
|
|
1497
|
+
// 且 3.16.1 起别名功能已停用,不再导入
|
|
1501
1498
|
if (typeof rz.proxy === 'string') {
|
|
1502
1499
|
const v = rz.proxy.trim();
|
|
1503
1500
|
if (v === '' || /^https?:\/\/\S+:\d+$/.test(v)) next.proxy = v;
|
|
@@ -3056,7 +3053,8 @@ async function runHeadroomHook(args, timeoutMs = 30000) {
|
|
|
3056
3053
|
});
|
|
3057
3054
|
}
|
|
3058
3055
|
|
|
3059
|
-
// 解析 ensure-proxy.sh status 输出为结构化字段(字段名以脚本 hr_status
|
|
3056
|
+
// 解析 ensure-proxy(.sh/.mjs) status 输出为结构化字段(字段名以脚本 hr_status 的输出为准;
|
|
3057
|
+
// headroom 插件 1.3.0+ 多实例模式由 hr-multi 输出同名字段,另附 instance: 行)。
|
|
3060
3058
|
// 导出以便测试脚本直接驱动。
|
|
3061
3059
|
export function parseHeadroomStatus(text) {
|
|
3062
3060
|
const raw = String(text || '');
|
|
@@ -3066,15 +3064,23 @@ export function parseHeadroomStatus(text) {
|
|
|
3066
3064
|
};
|
|
3067
3065
|
const proxy = line('proxy') || '';
|
|
3068
3066
|
const ps = line('power-save-auto-cpu') || '';
|
|
3067
|
+
// 多实例模式的实例行:instance: <端口> <格式> <上游> <up|down> <供应商列表>
|
|
3068
|
+
const instances = [];
|
|
3069
|
+
for (const m of raw.matchAll(/^instance: (\d+) (\S+) (\S+) (up|down) ?(.*)$/gm)) {
|
|
3070
|
+
instances.push({ port: Number(m[1]), format: m[2], upstream: m[3], up: m[4] === 'up', providers: m[5] || '' });
|
|
3071
|
+
}
|
|
3069
3072
|
return {
|
|
3070
3073
|
up: /^up\b/.test(proxy),
|
|
3071
3074
|
port: Number(/(\d+)\)?$/.exec(proxy)?.[1]) || null,
|
|
3072
3075
|
managed: line('managed-by-plugin'),
|
|
3076
|
+
multi: instances.length > 0 || /^instances: \d+$/m.test(raw),
|
|
3077
|
+
instances,
|
|
3073
3078
|
backend: line('current-backend'),
|
|
3074
3079
|
desiredBackend: line('desired-backend'),
|
|
3075
3080
|
powerMode: line('power-mode'),
|
|
3076
3081
|
saverDetect: line('saver-detect'),
|
|
3077
|
-
|
|
3082
|
+
// watcher 行存在才是真状态;多实例模式不输出该行时为 null(面板显示 —)
|
|
3083
|
+
watcherRunning: /^watcher: /m.test(raw) ? /^watcher: running/m.test(raw) : null,
|
|
3078
3084
|
saverHint: (raw.split('\n').find((l) => l.startsWith('注意:')) || '').trim() || null,
|
|
3079
3085
|
powerSaveMode: (/^(battery|saver|off)/.exec(ps) || [])[1] || null,
|
|
3080
3086
|
watchInterval: Number(/\(interval (\d+)s\)/.exec(ps)?.[1]) || null,
|
|
@@ -3221,6 +3227,38 @@ function runZcodeCli(subArgs, timeoutMs = 120000, scope = 'plugins') {
|
|
|
3221
3227
|
return runNodeScript([bundle, ...subArgs], timeoutMs, scope);
|
|
3222
3228
|
}
|
|
3223
3229
|
|
|
3230
|
+
// 简单版本号比较(a<b 返回 -1,相等 0,a>b 返回 1):主版本按点分段,
|
|
3231
|
+
// 数字段按数值比、非数字段按字符串比;主版本相同时带 pre-release 后缀
|
|
3232
|
+
// (如 1.7.0-beta)低于正式版,后缀内部同样按段比较;空版本视为最旧
|
|
3233
|
+
// (已装版本未知时当作可更新)
|
|
3234
|
+
function cmpVersion(a, b) {
|
|
3235
|
+
const cmpSegs = (A, B) => {
|
|
3236
|
+
for (let i = 0; i < Math.max(A.length, B.length); i++) {
|
|
3237
|
+
const x = A[i] || '';
|
|
3238
|
+
const y = B[i] || '';
|
|
3239
|
+
const nx = /^\d+$/.test(x) ? Number(x) : null;
|
|
3240
|
+
const ny = /^\d+$/.test(y) ? Number(y) : null;
|
|
3241
|
+
if (nx !== null && ny !== null) {
|
|
3242
|
+
if (nx !== ny) return nx < ny ? -1 : 1;
|
|
3243
|
+
} else if (x !== y) {
|
|
3244
|
+
return x < y ? -1 : 1;
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
return 0;
|
|
3248
|
+
};
|
|
3249
|
+
const parse = (v) => {
|
|
3250
|
+
const s = String(v || '');
|
|
3251
|
+
const i = s.indexOf('-');
|
|
3252
|
+
return { pre: i >= 0 ? s.slice(i + 1) : null, core: (i >= 0 ? s.slice(0, i) : s).split('.') };
|
|
3253
|
+
};
|
|
3254
|
+
const A = parse(a), B = parse(b);
|
|
3255
|
+
const c = cmpSegs(A.core, B.core);
|
|
3256
|
+
if (c !== 0) return c;
|
|
3257
|
+
if ((A.pre === null) !== (B.pre === null)) return A.pre === null ? 1 : -1;
|
|
3258
|
+
if (A.pre !== B.pre) return cmpSegs(A.pre.split('.'), B.pre.split('.'));
|
|
3259
|
+
return 0;
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3224
3262
|
// 比对市场清单与已装清单:每个插件的已装版本与最新版本。导出以便测试。
|
|
3225
3263
|
export function pluginUpdateStatus() {
|
|
3226
3264
|
const installedRaw = readJsonFile(join(cliPluginsDir(), 'installed_plugins.json'));
|
|
@@ -3234,7 +3272,8 @@ export function pluginUpdateStatus() {
|
|
|
3234
3272
|
name: e.name,
|
|
3235
3273
|
latest: e.version || '',
|
|
3236
3274
|
installed: mine.get(e.name) || null,
|
|
3237
|
-
|
|
3275
|
+
// 只有市场版本更高才算可更新:本地高于市场(市场回退/降级)不提示、不自动降级
|
|
3276
|
+
hasUpdate: mine.has(e.name) && cmpVersion(mine.get(e.name), e.version || '') < 0,
|
|
3238
3277
|
}));
|
|
3239
3278
|
return { plugins, updates: plugins.filter((e) => e.hasUpdate) };
|
|
3240
3279
|
}
|
|
@@ -3254,6 +3293,63 @@ async function ensureZcodePluginsMarketplace() {
|
|
|
3254
3293
|
return { ok: r.ok, output: r.output, error: r.error };
|
|
3255
3294
|
}
|
|
3256
3295
|
|
|
3296
|
+
// /plugins/update 并发控制:启动自动更新、打开设置弹窗时的自动触发、手动按钮、
|
|
3297
|
+
// 各插件面板的单插件操作都打到这个端点,而 CLI 同时改同一个插件目录会互相破坏。
|
|
3298
|
+
// 同目标(全量或同一插件名)的并发请求共用同一次执行结果;
|
|
3299
|
+
// 不同目标的请求排队串行,后到的等前一个结束再跑
|
|
3300
|
+
let pluginsUpdateJob = null; // { only, promise }:正在执行或排队的任务
|
|
3301
|
+
let pluginsUpdateQueue = Promise.resolve();
|
|
3302
|
+
|
|
3303
|
+
async function runPluginsUpdateJob(body) {
|
|
3304
|
+
// name 指定时只处理该插件:已装且有更新则更新,未装则安装(各插件面板的单插件操作)
|
|
3305
|
+
const only = typeof body?.name === 'string' && body.name.trim() ? body.name.trim() : null;
|
|
3306
|
+
const steps = [];
|
|
3307
|
+
const added = await ensureZcodePluginsMarketplace();
|
|
3308
|
+
if (added) steps.push({ step: 'marketplace add', ok: added.ok, output: added.output || added.error });
|
|
3309
|
+
const sync = await runZcodeCli(['plugins', 'marketplace', 'update', ZCODE_PLUGINS_MARKETPLACE], 60000);
|
|
3310
|
+
lastMarketplaceSync = Date.now();
|
|
3311
|
+
steps.push({ step: 'marketplace update', ok: sync.ok, output: sync.output });
|
|
3312
|
+
if (!sync.ok && sync.code !== 'cli-not-found') {
|
|
3313
|
+
return { status: 500, payload: { ok: false, code: 'plugins-update', error: sync.error || sync.output || 'marketplace update 失败', steps } };
|
|
3314
|
+
}
|
|
3315
|
+
const status = pluginUpdateStatus();
|
|
3316
|
+
if (!status) return { status: 200, payload: { ok: sync.ok, updated: 0, steps } };
|
|
3317
|
+
let updated = 0;
|
|
3318
|
+
for (const p of status.plugins) {
|
|
3319
|
+
if (only && p.name !== only) continue;
|
|
3320
|
+
if (p.installed && p.hasUpdate) {
|
|
3321
|
+
const r = await runZcodeCli(['plugins', 'update', `${p.name}@${ZCODE_PLUGINS_MARKETPLACE}`], 120000);
|
|
3322
|
+
steps.push({ step: `update ${p.name}`, ok: r.ok, output: r.output });
|
|
3323
|
+
if (r.ok) updated += 1;
|
|
3324
|
+
} else if (!p.installed && only) {
|
|
3325
|
+
// 安装只走单插件入口(name 指定=各面板的安装卡):
|
|
3326
|
+
// 全量“检查并更新”/启动自动更新只升级已装插件——卸载必须被尊重,
|
|
3327
|
+
// 未装插件一律由用户在各 tab 显式点安装
|
|
3328
|
+
const r = await runZcodeCli(['plugins', 'install', `${p.name}@${ZCODE_PLUGINS_MARKETPLACE}`], 120000);
|
|
3329
|
+
steps.push({ step: `install ${p.name}`, ok: r.ok, output: r.output });
|
|
3330
|
+
if (r.ok) updated += 1;
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
return { status: 200, payload: { ok: steps.every((s) => s.ok), updated, steps, ...pluginUpdateStatus() } };
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
function enqueuePluginsUpdate(body) {
|
|
3337
|
+
const only = typeof body?.name === 'string' && body.name.trim() ? body.name.trim() : null;
|
|
3338
|
+
if (pluginsUpdateJob && pluginsUpdateJob.only === only) return pluginsUpdateJob.promise;
|
|
3339
|
+
const prev = pluginsUpdateQueue;
|
|
3340
|
+
const promise = (async () => {
|
|
3341
|
+
await prev;
|
|
3342
|
+
try {
|
|
3343
|
+
return await runPluginsUpdateJob(body);
|
|
3344
|
+
} finally {
|
|
3345
|
+
if (pluginsUpdateJob && pluginsUpdateJob.promise === promise) pluginsUpdateJob = null;
|
|
3346
|
+
}
|
|
3347
|
+
})();
|
|
3348
|
+
pluginsUpdateQueue = promise.then(() => undefined, () => undefined);
|
|
3349
|
+
pluginsUpdateJob = { only, promise };
|
|
3350
|
+
return promise;
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3257
3353
|
// 读全局提示词(~/.zcode/AGENTS.md);文件不存在视为空内容。
|
|
3258
3354
|
export function readAgents(agentsFile) {
|
|
3259
3355
|
try {
|
package/src/host/main.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
import { randomBytes } from 'node:crypto';
|
|
12
12
|
import { resolveZcodeExecutable, resolveDataRootDir } from './paths.mjs';
|
|
13
|
+
import { readZcodeVersion } from './version.mjs';
|
|
13
14
|
import { CdpConnection, fetchBrowserWsUrl } from './cdp.mjs';
|
|
14
15
|
import { startHelper } from './helper.mjs';
|
|
15
16
|
import { runSetup } from './setup.mjs';
|
|
@@ -154,12 +155,12 @@ async function waitForCdp(port, timeoutMs) {
|
|
|
154
155
|
return false;
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
async function buildInjectSource(token, helperPort, features) {
|
|
158
|
+
async function buildInjectSource(token, helperPort, features, zcodeVersion) {
|
|
158
159
|
if (!existsSync(INJECT_BUNDLE)) {
|
|
159
160
|
throw new Error('缺少注入脚本 dist/inject.js,请先运行 npm run build');
|
|
160
161
|
}
|
|
161
162
|
const bundle = readFileSync(INJECT_BUNDLE, 'utf8');
|
|
162
|
-
const bootstrap = `window.__ZCODEPRO__={helperUrl:'http://127.0.0.1:${helperPort}',token:${JSON.stringify(token)},features:${JSON.stringify(features)}};`;
|
|
163
|
+
const bootstrap = `window.__ZCODEPRO__={helperUrl:'http://127.0.0.1:${helperPort}',token:${JSON.stringify(token)},features:${JSON.stringify(features)},zcodeVersion:${JSON.stringify(zcodeVersion || '')}};`;
|
|
163
164
|
return bootstrap + '\n' + bundle;
|
|
164
165
|
}
|
|
165
166
|
|
|
@@ -189,6 +190,8 @@ export async function run(argv) {
|
|
|
189
190
|
throw err;
|
|
190
191
|
}
|
|
191
192
|
winLog(`启动: ZCode=${exe}, CDP 端口 ${args.cdpPort}`);
|
|
193
|
+
const zcodeVersion = readZcodeVersion(exe);
|
|
194
|
+
if (args.verbose) console.log(`[zcodepro] ZCode 版本: ${zcodeVersion || '未知(app.asar 读取失败)'}`);
|
|
192
195
|
const dataRoot = resolveDataRootDir();
|
|
193
196
|
const token = randomBytes(16).toString('hex');
|
|
194
197
|
const state = { injectedPages: 0 };
|
|
@@ -220,7 +223,7 @@ export async function run(argv) {
|
|
|
220
223
|
}
|
|
221
224
|
|
|
222
225
|
// 2. 启动辅助服务
|
|
223
|
-
const server = await startHelper({ port: args.helperPort, token, dataRoot, state });
|
|
226
|
+
const server = await startHelper({ port: args.helperPort, token, dataRoot, state, zcodeVersion });
|
|
224
227
|
console.log(`[zcodepro] 辅助服务已就绪: http://127.0.0.1:${args.helperPort} (数据目录 ${dataRoot})`);
|
|
225
228
|
|
|
226
229
|
// 3. 连接 CDP 并注入(断线自动重连)
|
|
@@ -236,7 +239,7 @@ export async function run(argv) {
|
|
|
236
239
|
server.close();
|
|
237
240
|
process.exit(1);
|
|
238
241
|
}
|
|
239
|
-
const source = await buildInjectSource(token, args.helperPort, healthRes.features || {});
|
|
242
|
+
const source = await buildInjectSource(token, args.helperPort, healthRes.features || {}, zcodeVersion);
|
|
240
243
|
|
|
241
244
|
async function attachConnection() {
|
|
242
245
|
const conn = new CdpConnection(await fetchBrowserWsUrl(args.cdpPort));
|
package/src/host/taskIndex.mjs
CHANGED
|
@@ -172,6 +172,103 @@ export async function remapTaskIndexPaths(dataRoot, oldPath, newPath) {
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
// 虚拟项目清单(projects + project_roots,只读):别名迁移弹窗用它判断每条别名的
|
|
176
|
+
// 目标现状——路径是某项目的附加目录(3.16.1 多目录合并)还是已删除的项目。
|
|
177
|
+
// 返回 { projects: [{ name, roots: [{ path, primary }] }], deletedRoots: [path] };
|
|
178
|
+
// 读不到(库缺失/无驱动/查询失败)返回 null,调用方降级为纯 DOM 检测
|
|
179
|
+
export async function listVirtualProjects(dataRoot) {
|
|
180
|
+
const dbFile = taskIndexPath(dataRoot);
|
|
181
|
+
if (!existsSync(dbFile)) return null;
|
|
182
|
+
const driver = await loadDriver();
|
|
183
|
+
if (!driver) return null;
|
|
184
|
+
const SQL_ACTIVE = `SELECT p.project_id AS pid, p.name AS name, r.root_path AS path, r.is_primary, p.name_overridden
|
|
185
|
+
FROM projects p JOIN project_roots r ON r.project_id = p.project_id
|
|
186
|
+
WHERE p.deleted = 0 AND (p.host_id = 'local' OR p.host_id IS NULL)`;
|
|
187
|
+
const SQL_DELETED = `SELECT r.root_path AS path
|
|
188
|
+
FROM projects p JOIN project_roots r ON r.project_id = p.project_id
|
|
189
|
+
WHERE p.deleted = 1 AND (p.host_id = 'local' OR p.host_id IS NULL)`;
|
|
190
|
+
// CLI 驱动用制表符分隔行(项目名含引号/逗号都安全;名字含制表符的极端情况不处理)
|
|
191
|
+
const TAB = "char(9)";
|
|
192
|
+
try {
|
|
193
|
+
let activeRows;
|
|
194
|
+
let deletedRows;
|
|
195
|
+
if (driver.kind === 'node') {
|
|
196
|
+
const db = new driver.DatabaseSync(dbFile, { readOnly: true });
|
|
197
|
+
try {
|
|
198
|
+
activeRows = db.prepare(SQL_ACTIVE).all();
|
|
199
|
+
deletedRows = db.prepare(SQL_DELETED).all();
|
|
200
|
+
} finally { db.close(); }
|
|
201
|
+
} else {
|
|
202
|
+
const act = runCli(dbFile,
|
|
203
|
+
`SELECT p.project_id || ${TAB} || p.name || ${TAB} || r.root_path || ${TAB} || r.is_primary || ${TAB} || p.name_overridden${SQL_ACTIVE.slice(SQL_ACTIVE.indexOf(' FROM'))};`).stdout;
|
|
204
|
+
activeRows = act.split('\n').filter(Boolean).map((l) => {
|
|
205
|
+
const [pid, name, path, primary, overridden] = l.split('\t');
|
|
206
|
+
return { pid, name, path, primary, name_overridden: overridden };
|
|
207
|
+
});
|
|
208
|
+
const del = runCli(dbFile,
|
|
209
|
+
`SELECT r.root_path FROM projects p JOIN project_roots r ON r.project_id = p.project_id
|
|
210
|
+
WHERE p.deleted = 1 AND (p.host_id = 'local' OR p.host_id IS NULL);`).stdout;
|
|
211
|
+
deletedRows = del.split('\n').filter(Boolean).map((p) => ({ path: p }));
|
|
212
|
+
}
|
|
213
|
+
const byId = new Map();
|
|
214
|
+
for (const row of activeRows) {
|
|
215
|
+
const key = String(row.pid || row.name || '');
|
|
216
|
+
let proj = byId.get(key);
|
|
217
|
+
if (!proj) {
|
|
218
|
+
proj = { id: String(row.pid || ''), name: String(row.name || ''), roots: [], overridden: Number(row.name_overridden) === 1 };
|
|
219
|
+
byId.set(key, proj);
|
|
220
|
+
}
|
|
221
|
+
proj.roots.push({ path: String(row.path || ''), primary: Number(row.is_primary) === 1 });
|
|
222
|
+
}
|
|
223
|
+
// 全部出现过的任务工作区(含非项目工作区:draft/独立任务等,它们的任务
|
|
224
|
+
// 显示在侧栏“任务”分区):供分区头运行判定把活跃任务按归属分类
|
|
225
|
+
let allPaths = [];
|
|
226
|
+
try {
|
|
227
|
+
if (driver.kind === 'node') {
|
|
228
|
+
const db = new driver.DatabaseSync(dbFile, { readOnly: true });
|
|
229
|
+
try { allPaths = db.prepare('SELECT DISTINCT workspace_path FROM tasks').all().map((r) => String(r.workspace_path || '')); } finally { db.close(); }
|
|
230
|
+
} else {
|
|
231
|
+
allPaths = runCli(dbFile, 'SELECT DISTINCT workspace_path FROM tasks;').stdout.split('\n').filter(Boolean);
|
|
232
|
+
}
|
|
233
|
+
} catch { /* 读不到就不带,调用方只有项目路径 */ }
|
|
234
|
+
return {
|
|
235
|
+
projects: [...byId.values()],
|
|
236
|
+
deletedRoots: deletedRows.map((r) => String(r.path || '')),
|
|
237
|
+
allPaths,
|
|
238
|
+
};
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 批量查会话终态:task_id → task_status。分区头运行判定用它剔除官方 syncer
|
|
245
|
+
// 卡滞的报告(会话已完成但内存态仍报活跃时,任务行 loader 会一直卡转)。
|
|
246
|
+
// 读不到(库缺失/无驱动)返回 null,调用方按未确认处理
|
|
247
|
+
export async function listTaskStatuses(dataRoot, ids) {
|
|
248
|
+
const out = {};
|
|
249
|
+
const dbFile = taskIndexPath(dataRoot);
|
|
250
|
+
if (!existsSync(dbFile)) return null;
|
|
251
|
+
const driver = await loadDriver();
|
|
252
|
+
if (!driver) return null;
|
|
253
|
+
const esc = (v) => "'" + String(v).replace(/'/g, "''") + "'";
|
|
254
|
+
const inList = ids.map((id) => esc(id)).join(', ');
|
|
255
|
+
const sql = `SELECT task_id, task_status, updated_at FROM tasks WHERE task_id IN (${inList})`;
|
|
256
|
+
try {
|
|
257
|
+
let rows;
|
|
258
|
+
if (driver.kind === 'node') {
|
|
259
|
+
const db = new driver.DatabaseSync(dbFile, { readOnly: true });
|
|
260
|
+
try { rows = db.prepare(sql).all(); } finally { db.close(); }
|
|
261
|
+
} else {
|
|
262
|
+
rows = runCli(dbFile, sql + ';').stdout.split('\n').filter(Boolean)
|
|
263
|
+
.map((l) => { const [id, st, at] = l.split('|'); return { task_id: id, task_status: st, updated_at: at }; });
|
|
264
|
+
}
|
|
265
|
+
for (const r of rows) out[String(r.task_id)] = { status: String(r.task_status || ''), updatedAt: Number(r.updated_at) || 0 };
|
|
266
|
+
return out;
|
|
267
|
+
} catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
175
272
|
// 已重命名会话的标题表:task_id → title(title_overridden = 1 且未删除)。
|
|
176
273
|
// 只读单条 SELECT,不开事务;库不存在或没有 SQLite 支持时返回空表,
|
|
177
274
|
// 调用方(会话名锁定)自然降级为不干预。
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// 读取已安装 ZCode 的版本号:从可执行文件旁边的 resources/app.asar(macOS 为
|
|
2
|
+
// Contents/Resources/app.asar)头部解析内嵌 package.json 的 version 字段。
|
|
3
|
+
// asar 结构:偏移 0–15 为头部尺寸描述,偏移 12 处的 u32 是头部 JSON 长度,
|
|
4
|
+
// JSON 紧跟在偏移 16;files["package.json"] 的 offset 相对 16+jsonLen,且实测
|
|
5
|
+
// 存在 4 字节对齐补位,因此多读几字节、从首个 '{' 开始截取再解析。
|
|
6
|
+
// 读取失败一律返回空串:调用方把空串当作“版本未知”,不启用按版本的分支。
|
|
7
|
+
import { closeSync, openSync, readSync } from 'node:fs';
|
|
8
|
+
import { dirname, join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
// 本启动器常以 ELECTRON_RUN_AS_NODE 方式复用 ZCode 二进制运行,该模式下 Electron
|
|
11
|
+
// 的 fs 仍带 asar 补丁:.asar 路径被当作归档目录而非普通文件,按字节偏移读头部会
|
|
12
|
+
// 全部落空(实测 3.16.1 下读到空串)。noAsar 开关(Electron 官方 API)关闭该拦截;
|
|
13
|
+
// 纯 Node 运行时没有这个属性,赋值无副作用
|
|
14
|
+
process.noAsar = true;
|
|
15
|
+
|
|
16
|
+
export function zcodeAsarPath(exePath) {
|
|
17
|
+
if (!exePath) return '';
|
|
18
|
+
// Electron 约定:Linux/Windows 资源目录在可执行文件旁;macOS 在 Contents/Resources
|
|
19
|
+
const dir = process.platform === 'darwin'
|
|
20
|
+
? join(dirname(exePath), '..', 'Resources')
|
|
21
|
+
: join(dirname(exePath), 'resources');
|
|
22
|
+
return join(dir, 'app.asar');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function readZcodeVersion(exePath) {
|
|
26
|
+
let fd = 0;
|
|
27
|
+
try {
|
|
28
|
+
fd = openSync(zcodeAsarPath(exePath), 'r');
|
|
29
|
+
const head = Buffer.alloc(16);
|
|
30
|
+
if (readSync(fd, head, 0, 16, 0) !== 16) return '';
|
|
31
|
+
const jsonLen = head.readUInt32LE(12);
|
|
32
|
+
// 头部异常大小说明不是预期的 asar(或文件损坏),直接放弃
|
|
33
|
+
if (jsonLen <= 0 || jsonLen > 64 * 1024 * 1024) return '';
|
|
34
|
+
const hdr = Buffer.alloc(jsonLen);
|
|
35
|
+
if (readSync(fd, hdr, 0, jsonLen, 16) !== jsonLen) return '';
|
|
36
|
+
const entry = JSON.parse(hdr.toString('utf8')).files?.['package.json'];
|
|
37
|
+
if (!entry || entry.unpacked || typeof entry.offset !== 'string') return '';
|
|
38
|
+
const base = 16 + jsonLen + Number(entry.offset);
|
|
39
|
+
const buf = Buffer.alloc(entry.size + 8);
|
|
40
|
+
readSync(fd, buf, 0, buf.length, base);
|
|
41
|
+
const text = buf.toString('utf8');
|
|
42
|
+
const start = text.indexOf('{');
|
|
43
|
+
if (start < 0) return '';
|
|
44
|
+
const version = JSON.parse(text.slice(start, start + entry.size)).version;
|
|
45
|
+
return typeof version === 'string' ? version : '';
|
|
46
|
+
} catch {
|
|
47
|
+
return '';
|
|
48
|
+
} finally {
|
|
49
|
+
if (fd) { try { closeSync(fd); } catch { /* ignore */ } }
|
|
50
|
+
}
|
|
51
|
+
}
|