zcode-pro 0.20.2 → 1.0.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 +8 -5
- package/README.md +7 -4
- package/dist/inject.js +1433 -434
- package/package.json +1 -1
- package/src/host/helper.mjs +147 -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 +101 -18
- 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/session-switch.js +6 -2
- package/src/inject/features/settings-dialog.js +88 -59
- package/src/inject/features/styles.js +8 -1
- package/src/inject/features/title-gen.js +41 -0
- 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 +384 -27
- package/src/inject/index.js +3 -2
- package/src/inject/ui.js +20 -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 = '0.
|
|
20
|
+
const VERSION = '1.0.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)) {
|
|
@@ -338,6 +342,14 @@ export function startHelper({ port, token, dataRoot, state, agentsFile = default
|
|
|
338
342
|
if (body && typeof body === 'object' && body.titleGen && typeof body.titleGen === 'object') {
|
|
339
343
|
current.titleGen = mergeTitleGenConfig({ ...current.titleGen, ...body.titleGen });
|
|
340
344
|
}
|
|
345
|
+
// 一次性迁移标记(如别名迁移弹窗的“暂不迁移”):按项合并,只接受布尔值
|
|
346
|
+
if (body && typeof body === 'object' && body.migrations && typeof body.migrations === 'object') {
|
|
347
|
+
const patch = {};
|
|
348
|
+
for (const [k, v] of Object.entries(body.migrations)) {
|
|
349
|
+
if (typeof v === 'boolean') patch[k] = v;
|
|
350
|
+
}
|
|
351
|
+
current.migrations = { ...(current.migrations || {}), ...patch };
|
|
352
|
+
}
|
|
341
353
|
saveConfig(configFile, current);
|
|
342
354
|
json(res, 200, { ok: true, config: current });
|
|
343
355
|
return;
|
|
@@ -383,6 +395,16 @@ export function startHelper({ port, token, dataRoot, state, agentsFile = default
|
|
|
383
395
|
json(res, ...writeAgents(body, agentsFile));
|
|
384
396
|
return;
|
|
385
397
|
}
|
|
398
|
+
if (req.method === 'GET' && url.pathname === '/projects-v2') {
|
|
399
|
+
// 虚拟项目(3.16.1 起 projects/project_roots 表):别名迁移弹窗用 roots
|
|
400
|
+
// 区分“目录已并入其他项目”与“项目已删除”。读不到时 available=false,
|
|
401
|
+
// 注入端降级为纯 DOM 检测
|
|
402
|
+
const data = await listVirtualProjects(dataRoot);
|
|
403
|
+
json(res, 200, data
|
|
404
|
+
? { ok: true, available: true, ...data }
|
|
405
|
+
: { ok: true, available: false, projects: [], deletedRoots: [] });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
386
408
|
if (req.method === 'GET' && url.pathname === '/projects') {
|
|
387
409
|
const settings = readSettings(settingsFile);
|
|
388
410
|
const projects = (settings.recentProjects || []).map((p) => {
|
|
@@ -1106,7 +1128,9 @@ export function startHelper({ port, token, dataRoot, state, agentsFile = default
|
|
|
1106
1128
|
// —— zcode-plugins 市场更新:读状态(带 30 分钟节流的市场同步)与执行更新 ————
|
|
1107
1129
|
if (req.method === 'GET' && url.pathname === '/plugins/status') {
|
|
1108
1130
|
await ensureZcodePluginsMarketplace();
|
|
1109
|
-
|
|
1131
|
+
// 有更新任务在执行或排队时跳过这里的节流同步(任务本身会同步市场并刷新
|
|
1132
|
+
// 时间戳),避免两处同时跑 marketplace update
|
|
1133
|
+
if (!pluginsUpdateJob && Date.now() - lastMarketplaceSync > 30 * 60 * 1000) {
|
|
1110
1134
|
await runZcodeCli(['plugins', 'marketplace', 'update', ZCODE_PLUGINS_MARKETPLACE], 60000);
|
|
1111
1135
|
lastMarketplaceSync = Date.now();
|
|
1112
1136
|
}
|
|
@@ -1120,40 +1144,8 @@ export function startHelper({ port, token, dataRoot, state, agentsFile = default
|
|
|
1120
1144
|
}
|
|
1121
1145
|
if (req.method === 'POST' && url.pathname === '/plugins/update') {
|
|
1122
1146
|
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 });
|
|
1147
|
+
const result = await enqueuePluginsUpdate(body);
|
|
1148
|
+
json(res, result.status, result.payload);
|
|
1157
1149
|
return;
|
|
1158
1150
|
}
|
|
1159
1151
|
json(res, 404, { ok: false, error: 'not found' });
|
|
@@ -1386,16 +1378,17 @@ async function runSettingsSync({ direction, dataRoot, configFile, config, proxyU
|
|
|
1386
1378
|
}
|
|
1387
1379
|
|
|
1388
1380
|
// 上传:每组独立一个云端文件(zcodepro.json / model.json,始终最新一版),勾选几组
|
|
1389
|
-
//
|
|
1390
|
-
//
|
|
1391
|
-
//
|
|
1392
|
-
//
|
|
1381
|
+
// 写几个。aliases 不再上传——3.16.1 起别名功能已停用(官方“编辑项目”改名),
|
|
1382
|
+
// 且各机器项目路径不同,同步别名只会互相带来无效路径。同一份内容再写一个带
|
|
1383
|
+
// 时间戳的历史版本文件,供下载时任选版本恢复(旧版 zcode-pro 仍读固定名);
|
|
1384
|
+
// 历史版本最多保留 historyLimit 个(同一时间戳的各组文件算一个),上传成功后
|
|
1385
|
+
// 自动删除更早的
|
|
1393
1386
|
async function runSyncPush({ dataRoot, configFile, config, sync, proxyUrl }) {
|
|
1394
1387
|
const keep = Number.isInteger(sync.historyLimit) ? sync.historyLimit : defaultConfig().sync.historyLimit;
|
|
1395
1388
|
const docs = {};
|
|
1396
1389
|
if (sync.include.zcodepro) {
|
|
1397
|
-
// sync
|
|
1398
|
-
const { sync: _s, version: _v, ...rest } = config;
|
|
1390
|
+
// sync(本机的同步配置与凭据)、version(本机 helper 版本)与 aliases 不外发
|
|
1391
|
+
const { sync: _s, version: _v, aliases: _a, ...rest } = config;
|
|
1399
1392
|
docs.zcodepro = rest;
|
|
1400
1393
|
}
|
|
1401
1394
|
if (sync.include.model) {
|
|
@@ -1419,11 +1412,6 @@ async function runSyncPush({ dataRoot, configFile, config, sync, proxyUrl }) {
|
|
|
1419
1412
|
for (const kind of Object.keys(docs)) {
|
|
1420
1413
|
const remote = await fetchSyncDoc(sync, proxyUrl, kind);
|
|
1421
1414
|
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
1415
|
const doc = { app: 'zcode-pro', kind, format: 1, savedAt: stamp.toISOString(), data: docs[kind] };
|
|
1428
1416
|
const docPath = join(dir, SYNC_FILES[kind]);
|
|
1429
1417
|
writeFileSync(docPath, JSON.stringify(doc, null, 2), 'utf8');
|
|
@@ -1457,8 +1445,9 @@ async function runSyncPush({ dataRoot, configFile, config, sync, proxyUrl }) {
|
|
|
1457
1445
|
}
|
|
1458
1446
|
|
|
1459
1447
|
// 下载:把云端所选组应用到本机(各组独立文件,勾选几组读几组,可只覆盖模型设置)。
|
|
1460
|
-
// 覆盖前留 .zcodepro-backup 备份;zcodepro
|
|
1461
|
-
|
|
1448
|
+
// 覆盖前留 .zcodepro-backup 备份;zcodepro 组覆盖功能/样式/代理,别名不受影响
|
|
1449
|
+
//(3.16.1 起别名已停用,且云端备份里的别名多是其他机器的路径);sync 配置与
|
|
1450
|
+
// 凭据始终保留本机的
|
|
1462
1451
|
async function runSyncPull({ dataRoot, configFile, config, sync, proxyUrl }) {
|
|
1463
1452
|
// 先把勾选组的云端数据全部读到(任何一组读取失败都不动本机文件),再逐组应用
|
|
1464
1453
|
const docs = {};
|
|
@@ -1476,8 +1465,9 @@ async function runSyncPull({ dataRoot, configFile, config, sync, proxyUrl }) {
|
|
|
1476
1465
|
}
|
|
1477
1466
|
|
|
1478
1467
|
// 把云端读到的各组数据落到本机(runSyncPull 与按文件的历史版本导入共用):
|
|
1479
|
-
// zcodepro 组覆盖功能/样式/代理(sync
|
|
1480
|
-
|
|
1468
|
+
// zcodepro 组覆盖功能/样式/代理(sync 配置与凭据保留本机的),别名不导入
|
|
1469
|
+
//(云端备份可能出自别的机器,路径在本机无效),model 组覆盖 v2/config.json 的
|
|
1470
|
+
// provider 与 provider_config.json;覆盖前都留备份
|
|
1481
1471
|
function applySyncDocs({ docs, dataRoot, configFile, config, sync }) {
|
|
1482
1472
|
const applied = [];
|
|
1483
1473
|
const backupFile = (p) => {
|
|
@@ -1494,10 +1484,8 @@ function applySyncDocs({ docs, dataRoot, configFile, config, sync }) {
|
|
|
1494
1484
|
const next = loadConfig(configFile);
|
|
1495
1485
|
applyFeaturesPartial(next.features, rz.features);
|
|
1496
1486
|
next.styles = applyStylesPartial(next.styles, rz.styles);
|
|
1497
|
-
//
|
|
1498
|
-
//
|
|
1499
|
-
const remoteAliases = sanitizeAliases(rz.aliases);
|
|
1500
|
-
if (remoteAliases) next.aliases = mergeAliases(next.aliases, remoteAliases);
|
|
1487
|
+
// 别名保持本机的原样:云端备份里的别名可能出自其他机器(路径在本机无效),
|
|
1488
|
+
// 且 3.16.1 起别名功能已停用,不再导入
|
|
1501
1489
|
if (typeof rz.proxy === 'string') {
|
|
1502
1490
|
const v = rz.proxy.trim();
|
|
1503
1491
|
if (v === '' || /^https?:\/\/\S+:\d+$/.test(v)) next.proxy = v;
|
|
@@ -3056,7 +3044,8 @@ async function runHeadroomHook(args, timeoutMs = 30000) {
|
|
|
3056
3044
|
});
|
|
3057
3045
|
}
|
|
3058
3046
|
|
|
3059
|
-
// 解析 ensure-proxy.sh status 输出为结构化字段(字段名以脚本 hr_status
|
|
3047
|
+
// 解析 ensure-proxy(.sh/.mjs) status 输出为结构化字段(字段名以脚本 hr_status 的输出为准;
|
|
3048
|
+
// headroom 插件 1.3.0+ 多实例模式由 hr-multi 输出同名字段,另附 instance: 行)。
|
|
3060
3049
|
// 导出以便测试脚本直接驱动。
|
|
3061
3050
|
export function parseHeadroomStatus(text) {
|
|
3062
3051
|
const raw = String(text || '');
|
|
@@ -3066,15 +3055,23 @@ export function parseHeadroomStatus(text) {
|
|
|
3066
3055
|
};
|
|
3067
3056
|
const proxy = line('proxy') || '';
|
|
3068
3057
|
const ps = line('power-save-auto-cpu') || '';
|
|
3058
|
+
// 多实例模式的实例行:instance: <端口> <格式> <上游> <up|down> <供应商列表>
|
|
3059
|
+
const instances = [];
|
|
3060
|
+
for (const m of raw.matchAll(/^instance: (\d+) (\S+) (\S+) (up|down) ?(.*)$/gm)) {
|
|
3061
|
+
instances.push({ port: Number(m[1]), format: m[2], upstream: m[3], up: m[4] === 'up', providers: m[5] || '' });
|
|
3062
|
+
}
|
|
3069
3063
|
return {
|
|
3070
3064
|
up: /^up\b/.test(proxy),
|
|
3071
3065
|
port: Number(/(\d+)\)?$/.exec(proxy)?.[1]) || null,
|
|
3072
3066
|
managed: line('managed-by-plugin'),
|
|
3067
|
+
multi: instances.length > 0 || /^instances: \d+$/m.test(raw),
|
|
3068
|
+
instances,
|
|
3073
3069
|
backend: line('current-backend'),
|
|
3074
3070
|
desiredBackend: line('desired-backend'),
|
|
3075
3071
|
powerMode: line('power-mode'),
|
|
3076
3072
|
saverDetect: line('saver-detect'),
|
|
3077
|
-
|
|
3073
|
+
// watcher 行存在才是真状态;多实例模式不输出该行时为 null(面板显示 —)
|
|
3074
|
+
watcherRunning: /^watcher: /m.test(raw) ? /^watcher: running/m.test(raw) : null,
|
|
3078
3075
|
saverHint: (raw.split('\n').find((l) => l.startsWith('注意:')) || '').trim() || null,
|
|
3079
3076
|
powerSaveMode: (/^(battery|saver|off)/.exec(ps) || [])[1] || null,
|
|
3080
3077
|
watchInterval: Number(/\(interval (\d+)s\)/.exec(ps)?.[1]) || null,
|
|
@@ -3221,6 +3218,38 @@ function runZcodeCli(subArgs, timeoutMs = 120000, scope = 'plugins') {
|
|
|
3221
3218
|
return runNodeScript([bundle, ...subArgs], timeoutMs, scope);
|
|
3222
3219
|
}
|
|
3223
3220
|
|
|
3221
|
+
// 简单版本号比较(a<b 返回 -1,相等 0,a>b 返回 1):主版本按点分段,
|
|
3222
|
+
// 数字段按数值比、非数字段按字符串比;主版本相同时带 pre-release 后缀
|
|
3223
|
+
// (如 1.7.0-beta)低于正式版,后缀内部同样按段比较;空版本视为最旧
|
|
3224
|
+
// (已装版本未知时当作可更新)
|
|
3225
|
+
function cmpVersion(a, b) {
|
|
3226
|
+
const cmpSegs = (A, B) => {
|
|
3227
|
+
for (let i = 0; i < Math.max(A.length, B.length); i++) {
|
|
3228
|
+
const x = A[i] || '';
|
|
3229
|
+
const y = B[i] || '';
|
|
3230
|
+
const nx = /^\d+$/.test(x) ? Number(x) : null;
|
|
3231
|
+
const ny = /^\d+$/.test(y) ? Number(y) : null;
|
|
3232
|
+
if (nx !== null && ny !== null) {
|
|
3233
|
+
if (nx !== ny) return nx < ny ? -1 : 1;
|
|
3234
|
+
} else if (x !== y) {
|
|
3235
|
+
return x < y ? -1 : 1;
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
return 0;
|
|
3239
|
+
};
|
|
3240
|
+
const parse = (v) => {
|
|
3241
|
+
const s = String(v || '');
|
|
3242
|
+
const i = s.indexOf('-');
|
|
3243
|
+
return { pre: i >= 0 ? s.slice(i + 1) : null, core: (i >= 0 ? s.slice(0, i) : s).split('.') };
|
|
3244
|
+
};
|
|
3245
|
+
const A = parse(a), B = parse(b);
|
|
3246
|
+
const c = cmpSegs(A.core, B.core);
|
|
3247
|
+
if (c !== 0) return c;
|
|
3248
|
+
if ((A.pre === null) !== (B.pre === null)) return A.pre === null ? 1 : -1;
|
|
3249
|
+
if (A.pre !== B.pre) return cmpSegs(A.pre.split('.'), B.pre.split('.'));
|
|
3250
|
+
return 0;
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3224
3253
|
// 比对市场清单与已装清单:每个插件的已装版本与最新版本。导出以便测试。
|
|
3225
3254
|
export function pluginUpdateStatus() {
|
|
3226
3255
|
const installedRaw = readJsonFile(join(cliPluginsDir(), 'installed_plugins.json'));
|
|
@@ -3234,7 +3263,8 @@ export function pluginUpdateStatus() {
|
|
|
3234
3263
|
name: e.name,
|
|
3235
3264
|
latest: e.version || '',
|
|
3236
3265
|
installed: mine.get(e.name) || null,
|
|
3237
|
-
|
|
3266
|
+
// 只有市场版本更高才算可更新:本地高于市场(市场回退/降级)不提示、不自动降级
|
|
3267
|
+
hasUpdate: mine.has(e.name) && cmpVersion(mine.get(e.name), e.version || '') < 0,
|
|
3238
3268
|
}));
|
|
3239
3269
|
return { plugins, updates: plugins.filter((e) => e.hasUpdate) };
|
|
3240
3270
|
}
|
|
@@ -3254,6 +3284,63 @@ async function ensureZcodePluginsMarketplace() {
|
|
|
3254
3284
|
return { ok: r.ok, output: r.output, error: r.error };
|
|
3255
3285
|
}
|
|
3256
3286
|
|
|
3287
|
+
// /plugins/update 并发控制:启动自动更新、打开设置弹窗时的自动触发、手动按钮、
|
|
3288
|
+
// 各插件面板的单插件操作都打到这个端点,而 CLI 同时改同一个插件目录会互相破坏。
|
|
3289
|
+
// 同目标(全量或同一插件名)的并发请求共用同一次执行结果;
|
|
3290
|
+
// 不同目标的请求排队串行,后到的等前一个结束再跑
|
|
3291
|
+
let pluginsUpdateJob = null; // { only, promise }:正在执行或排队的任务
|
|
3292
|
+
let pluginsUpdateQueue = Promise.resolve();
|
|
3293
|
+
|
|
3294
|
+
async function runPluginsUpdateJob(body) {
|
|
3295
|
+
// name 指定时只处理该插件:已装且有更新则更新,未装则安装(各插件面板的单插件操作)
|
|
3296
|
+
const only = typeof body?.name === 'string' && body.name.trim() ? body.name.trim() : null;
|
|
3297
|
+
const steps = [];
|
|
3298
|
+
const added = await ensureZcodePluginsMarketplace();
|
|
3299
|
+
if (added) steps.push({ step: 'marketplace add', ok: added.ok, output: added.output || added.error });
|
|
3300
|
+
const sync = await runZcodeCli(['plugins', 'marketplace', 'update', ZCODE_PLUGINS_MARKETPLACE], 60000);
|
|
3301
|
+
lastMarketplaceSync = Date.now();
|
|
3302
|
+
steps.push({ step: 'marketplace update', ok: sync.ok, output: sync.output });
|
|
3303
|
+
if (!sync.ok && sync.code !== 'cli-not-found') {
|
|
3304
|
+
return { status: 500, payload: { ok: false, code: 'plugins-update', error: sync.error || sync.output || 'marketplace update 失败', steps } };
|
|
3305
|
+
}
|
|
3306
|
+
const status = pluginUpdateStatus();
|
|
3307
|
+
if (!status) return { status: 200, payload: { ok: sync.ok, updated: 0, steps } };
|
|
3308
|
+
let updated = 0;
|
|
3309
|
+
for (const p of status.plugins) {
|
|
3310
|
+
if (only && p.name !== only) continue;
|
|
3311
|
+
if (p.installed && p.hasUpdate) {
|
|
3312
|
+
const r = await runZcodeCli(['plugins', 'update', `${p.name}@${ZCODE_PLUGINS_MARKETPLACE}`], 120000);
|
|
3313
|
+
steps.push({ step: `update ${p.name}`, ok: r.ok, output: r.output });
|
|
3314
|
+
if (r.ok) updated += 1;
|
|
3315
|
+
} else if (!p.installed && only) {
|
|
3316
|
+
// 安装只走单插件入口(name 指定=各面板的安装卡):
|
|
3317
|
+
// 全量“检查并更新”/启动自动更新只升级已装插件——卸载必须被尊重,
|
|
3318
|
+
// 未装插件一律由用户在各 tab 显式点安装
|
|
3319
|
+
const r = await runZcodeCli(['plugins', 'install', `${p.name}@${ZCODE_PLUGINS_MARKETPLACE}`], 120000);
|
|
3320
|
+
steps.push({ step: `install ${p.name}`, ok: r.ok, output: r.output });
|
|
3321
|
+
if (r.ok) updated += 1;
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
return { status: 200, payload: { ok: steps.every((s) => s.ok), updated, steps, ...pluginUpdateStatus() } };
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
function enqueuePluginsUpdate(body) {
|
|
3328
|
+
const only = typeof body?.name === 'string' && body.name.trim() ? body.name.trim() : null;
|
|
3329
|
+
if (pluginsUpdateJob && pluginsUpdateJob.only === only) return pluginsUpdateJob.promise;
|
|
3330
|
+
const prev = pluginsUpdateQueue;
|
|
3331
|
+
const promise = (async () => {
|
|
3332
|
+
await prev;
|
|
3333
|
+
try {
|
|
3334
|
+
return await runPluginsUpdateJob(body);
|
|
3335
|
+
} finally {
|
|
3336
|
+
if (pluginsUpdateJob && pluginsUpdateJob.promise === promise) pluginsUpdateJob = null;
|
|
3337
|
+
}
|
|
3338
|
+
})();
|
|
3339
|
+
pluginsUpdateQueue = promise.then(() => undefined, () => undefined);
|
|
3340
|
+
pluginsUpdateJob = { only, promise };
|
|
3341
|
+
return promise;
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3257
3344
|
// 读全局提示词(~/.zcode/AGENTS.md);文件不存在视为空内容。
|
|
3258
3345
|
export function readAgents(agentsFile) {
|
|
3259
3346
|
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
|
+
}
|