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/setup.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// zcode-pro setup:本机集成设置入口(npm / pip 等包管理器安装不写系统目录,
|
|
2
|
-
//
|
|
2
|
+
// 需要菜单入口等集成时显式运行本子命令)。按“设置项”组织(id / 标题 /
|
|
3
3
|
// install / uninstall),以后新增设置项往 items 里加即可。
|
|
4
4
|
// setup 不依赖 ZCode 运行,在 main.run 里先于 ZCode 定位处理。
|
|
5
5
|
//
|
|
@@ -291,17 +291,17 @@ function plistContent(hasIcon) {
|
|
|
291
291
|
|
|
292
292
|
const shortcutItem = {
|
|
293
293
|
id: 'desktop-shortcut',
|
|
294
|
-
title: '
|
|
294
|
+
title: 'Shortcut "ZCode Pro" (app/start menu, icon reuses ZCode)',
|
|
295
295
|
supported: (plat) => SUPPORTED.includes(plat),
|
|
296
296
|
|
|
297
297
|
install(plat, opts = {}) {
|
|
298
298
|
const cmd = commandInPath(plat);
|
|
299
299
|
if (!cmd) {
|
|
300
|
-
throw new Error('
|
|
300
|
+
throw new Error('zcode-pro command not found in PATH, cannot create shortcut; check your install and retry');
|
|
301
301
|
}
|
|
302
302
|
if (!sameInstall(cmd, plat)) {
|
|
303
|
-
throw new Error(`
|
|
304
|
-
'
|
|
303
|
+
throw new Error(`the zcode-pro in PATH (${cmd}) is not part of this installation, ` +
|
|
304
|
+
'skipped so the shortcut will not point at an old version; manage shortcuts with that installation instead');
|
|
305
305
|
}
|
|
306
306
|
if (plat === 'linux') return this.installLinux(cmd, opts);
|
|
307
307
|
if (plat === 'win32') return this.installWindows(cmd, opts);
|
|
@@ -312,7 +312,7 @@ const shortcutItem = {
|
|
|
312
312
|
// 系统级(AUR 等)已有同名条目时不再建用户级:desktop 文件 id 相同时用户级
|
|
313
313
|
// 会遮蔽系统级(项目既有约定),残留后表现为菜单一直运行另一份
|
|
314
314
|
if (existsSync('/usr/share/applications/zcode-pro.desktop')) {
|
|
315
|
-
return { skipped: '
|
|
315
|
+
return { skipped: 'system-level shortcut detected (e.g. AUR); a user entry would shadow it, skipped' };
|
|
316
316
|
}
|
|
317
317
|
const appsDir = applicationsDir();
|
|
318
318
|
const target = join(appsDir, DESKTOP_FILE);
|
|
@@ -320,21 +320,21 @@ const shortcutItem = {
|
|
|
320
320
|
if (existsSync(target)) {
|
|
321
321
|
const cur = readFileSync(target, 'utf-8');
|
|
322
322
|
if (!cur.includes(SETUP_MARK)) {
|
|
323
|
-
return { skipped: `${target}
|
|
323
|
+
return { skipped: `${target} exists and was not created by setup (e.g. install.sh), not overwritten` };
|
|
324
324
|
}
|
|
325
325
|
// 早期版本写过 0644 的条目:即使内容无需重写也补上执行位(原因见 writeDesktopEntry)
|
|
326
326
|
chmodSync(target, 0o755);
|
|
327
327
|
const exec = (cur.match(/^Exec=(.+)$/m) || [])[1] || '';
|
|
328
328
|
if (exec.includes(cmd)) {
|
|
329
|
-
parts.push('
|
|
329
|
+
parts.push('app-menu entry already exists and points to the current command, nothing to do');
|
|
330
330
|
} else {
|
|
331
331
|
writeDesktopEntry(target, cmd);
|
|
332
|
-
parts.push(
|
|
332
|
+
parts.push(`updated ${target} (Exec=${cmd})`);
|
|
333
333
|
}
|
|
334
334
|
} else {
|
|
335
335
|
mkdirSync(appsDir, { recursive: true });
|
|
336
336
|
writeDesktopEntry(target, cmd);
|
|
337
|
-
parts.push(
|
|
337
|
+
parts.push(`written ${target} (Exec=${cmd})`);
|
|
338
338
|
}
|
|
339
339
|
try { execFileSync('update-desktop-database', [appsDir], { stdio: 'ignore' }); } catch { /* 无该命令时忽略 */ }
|
|
340
340
|
// 桌面条目独立判断(不因菜单条目幂等而短路)
|
|
@@ -347,13 +347,13 @@ const shortcutItem = {
|
|
|
347
347
|
// GNOME 的桌面(Nautilus)额外要求 trusted 元数据才显示“允许启动”;
|
|
348
348
|
// 其他环境无 gio / 写不进元数据时忽略
|
|
349
349
|
try { execFileSync('gio', ['set', dTarget, 'metadata::trusted', 'true'], { stdio: 'ignore' }); } catch { /* 非 GNOME 忽略 */ }
|
|
350
|
-
parts.push(
|
|
350
|
+
parts.push(`desktop ${dTarget}`);
|
|
351
351
|
} else {
|
|
352
|
-
parts.push('
|
|
352
|
+
parts.push('desktop entry exists and was not created by setup, not overwritten');
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
|
-
if (parts.length === 1 && parts[0].startsWith('
|
|
356
|
-
return { detail: parts.join('
|
|
355
|
+
if (parts.length === 1 && parts[0].startsWith('app-menu entry already exists')) return { skipped: parts[0] };
|
|
356
|
+
return { detail: parts.join('; ') };
|
|
357
357
|
},
|
|
358
358
|
|
|
359
359
|
installWindows(cmd, opts = {}) {
|
|
@@ -367,9 +367,9 @@ const shortcutItem = {
|
|
|
367
367
|
const plan = windowsShortcutPlan(cmd, !!opts.desktop, wshOk, !menuForeign);
|
|
368
368
|
if (menuForeign && (!opts.desktop || deskForeign)) {
|
|
369
369
|
return { skipped: [
|
|
370
|
-
menuForeign ?
|
|
371
|
-
deskForeign ?
|
|
372
|
-
].filter(Boolean).join('
|
|
370
|
+
menuForeign ? `${plan.lnkPath} exists (created elsewhere), not overwritten` : '',
|
|
371
|
+
deskForeign ? `desktop ${deskPath} exists (created elsewhere), not overwritten` : '',
|
|
372
|
+
].filter(Boolean).join('; ') };
|
|
373
373
|
}
|
|
374
374
|
if (wshOk) {
|
|
375
375
|
mkdirSync(plan.vbsDir, { recursive: true });
|
|
@@ -377,14 +377,14 @@ const shortcutItem = {
|
|
|
377
377
|
}
|
|
378
378
|
runPowerShell(plan.psScript);
|
|
379
379
|
const how = wshOk
|
|
380
|
-
?
|
|
381
|
-
: '
|
|
380
|
+
? `silent launch via ${plan.vbsPath}`
|
|
381
|
+
: 'WSH unavailable on this machine; minimized cmd launch (console flashes briefly on start)';
|
|
382
382
|
const parts = [];
|
|
383
383
|
parts.push(menuForeign
|
|
384
|
-
?
|
|
385
|
-
:
|
|
386
|
-
if (opts.desktop) parts.push(deskForeign ?
|
|
387
|
-
return { detail: `${parts.join('
|
|
384
|
+
? `start menu ${plan.lnkPath} exists (created elsewhere), not overwritten`
|
|
385
|
+
: `created ${plan.lnkPath}`);
|
|
386
|
+
if (opts.desktop) parts.push(deskForeign ? `desktop ${deskPath} exists (created elsewhere), not overwritten` : 'desktop shortcut created');
|
|
387
|
+
return { detail: `${parts.join('; ')} (${how})` };
|
|
388
388
|
},
|
|
389
389
|
|
|
390
390
|
installMac(cmd, opts = {}) {
|
|
@@ -392,7 +392,7 @@ const shortcutItem = {
|
|
|
392
392
|
const contents = join(app, 'Contents');
|
|
393
393
|
const marker = join(contents, 'zcodepro-setup-marker');
|
|
394
394
|
if (existsSync(app) && !existsSync(marker)) {
|
|
395
|
-
return { skipped: `${app}
|
|
395
|
+
return { skipped: `${app} exists and was not created by setup, not overwritten` };
|
|
396
396
|
}
|
|
397
397
|
let iconCopied = false;
|
|
398
398
|
for (const src of [join('/', 'Applications', 'ZCode.app', 'Contents', 'Resources', 'app.icns'),
|
|
@@ -411,15 +411,15 @@ const shortcutItem = {
|
|
|
411
411
|
chmodSync(launcher, 0o755);
|
|
412
412
|
writeFileSync(join(contents, 'Info.plist'), plistContent(iconCopied));
|
|
413
413
|
writeFileSync(marker, '');
|
|
414
|
-
let detail =
|
|
414
|
+
let detail = `created ${app} (launcher -> ${cmd}${iconCopied ? ', icon copied' : ''})`;
|
|
415
415
|
if (opts.desktop) {
|
|
416
416
|
const link = macDesktopLink();
|
|
417
417
|
if (existsSync(link)) {
|
|
418
|
-
detail += '
|
|
418
|
+
detail += '; desktop entry with the same name exists, not overwritten';
|
|
419
419
|
} else {
|
|
420
420
|
try {
|
|
421
421
|
symlinkSync(app, link);
|
|
422
|
-
detail +=
|
|
422
|
+
detail += `; desktop ${link}`;
|
|
423
423
|
} catch { /* 建链接失败仅省略桌面入口 */ }
|
|
424
424
|
}
|
|
425
425
|
}
|
|
@@ -445,16 +445,16 @@ const shortcutItem = {
|
|
|
445
445
|
rmSync(dTarget);
|
|
446
446
|
removed.push(dTarget);
|
|
447
447
|
}
|
|
448
|
-
if (!removed.length) return { skipped: '
|
|
448
|
+
if (!removed.length) return { skipped: 'no setup-created shortcuts found' };
|
|
449
449
|
try { execFileSync('update-desktop-database', [applicationsDir()], { stdio: 'ignore' }); } catch { /* 无该命令时忽略 */ }
|
|
450
|
-
return { detail:
|
|
450
|
+
return { detail: `removed ${removed.join(', ')}` };
|
|
451
451
|
},
|
|
452
452
|
|
|
453
453
|
uninstallWindows() {
|
|
454
454
|
const paths = windowsPaths();
|
|
455
455
|
const hadVbs = existsSync(paths.vbsPath);
|
|
456
456
|
const hadLnk = existsSync(paths.lnkPath);
|
|
457
|
-
if (!hadVbs && !hadLnk) return { skipped: '
|
|
457
|
+
if (!hadVbs && !hadLnk) return { skipped: 'no setup-created shortcuts found' };
|
|
458
458
|
// .lnk 可能已被 install.ps1 等重建并指向它自己:只有 Arguments 带本功能归属
|
|
459
459
|
// 标记才删;校验失败也保留 .lnk(宁可漏删不误删)
|
|
460
460
|
let lnkRemoved = false;
|
|
@@ -488,29 +488,29 @@ const shortcutItem = {
|
|
|
488
488
|
vbsCleaned = true;
|
|
489
489
|
} catch {
|
|
490
490
|
vbsCleaned = !existsSync(paths.vbsPath);
|
|
491
|
-
if (!vbsCleaned) console.error(`
|
|
491
|
+
if (!vbsCleaned) console.error(` (${paths.vbsDir} could not be deleted (may be in use); remove it manually later)`);
|
|
492
492
|
}
|
|
493
493
|
}
|
|
494
494
|
}
|
|
495
495
|
const parts = [];
|
|
496
496
|
if (hadLnk) {
|
|
497
|
-
parts.push(lnkRemoved ? '
|
|
497
|
+
parts.push(lnkRemoved ? 'start-menu/desktop shortcut' : verified ? 'shortcut not created by setup, kept' : 'shortcut ownership check failed, .lnk kept');
|
|
498
498
|
}
|
|
499
|
-
if (hadVbs) parts.push(vbsCleaned ? '
|
|
500
|
-
return { detail:
|
|
499
|
+
if (hadVbs) parts.push(vbsCleaned ? 'generated launcher script' : 'launcher script (delete failed)');
|
|
500
|
+
return { detail: `removed: ${parts.join(', ')}` };
|
|
501
501
|
},
|
|
502
502
|
|
|
503
503
|
uninstallMac() {
|
|
504
504
|
const app = macAppBundle();
|
|
505
505
|
const marker = join(app, 'Contents', 'zcodepro-setup-marker');
|
|
506
|
-
if (!existsSync(marker)) return { skipped: '
|
|
506
|
+
if (!existsSync(marker)) return { skipped: 'no setup-created app bundle found' };
|
|
507
507
|
// setup --desktop 建的桌面入口一并清理(只删指向本应用包的链接)
|
|
508
508
|
const link = macDesktopLink();
|
|
509
509
|
try {
|
|
510
510
|
if (existsSync(link) && realpathSync(link) === realpathSync(app)) rmSync(link);
|
|
511
511
|
} catch { /* 链接失效则忽略 */ }
|
|
512
512
|
rmSync(app, { recursive: true, force: true });
|
|
513
|
-
return { detail:
|
|
513
|
+
return { detail: `removed ${app}` };
|
|
514
514
|
},
|
|
515
515
|
};
|
|
516
516
|
|
|
@@ -532,22 +532,25 @@ function rmSyncWithRetry(target, tries = 4) {
|
|
|
532
532
|
const items = [shortcutItem];
|
|
533
533
|
|
|
534
534
|
export async function runSetup({ uninstall = false, platform: plat = platform(), desktop = false } = {}) {
|
|
535
|
-
|
|
535
|
+
// 输出全用 ASCII 英文:setup 常在 GBK 代码页的 cmd/PowerShell 里跑,中文经
|
|
536
|
+
// 管道输出时极易乱码(chcp 适配也不是所有入口都来得及生效);ASCII 文案
|
|
537
|
+
// 在任何代码页下都稳定显示
|
|
538
|
+
console.log(`[zcodepro] setup ${uninstall ? 'remove' : 'install'} local integration${desktop && !uninstall ? ' (with desktop)' : ''}:`);
|
|
536
539
|
let failed = 0;
|
|
537
540
|
for (const item of items) {
|
|
538
541
|
if (!item.supported(plat)) {
|
|
539
|
-
console.log(`
|
|
542
|
+
console.log(` - ${item.title}: not supported on ${plat}, skipped`);
|
|
540
543
|
continue;
|
|
541
544
|
}
|
|
542
545
|
try {
|
|
543
546
|
const res = uninstall ? item.uninstall(plat) : item.install(plat, { desktop });
|
|
544
|
-
if (res && res.skipped) console.log(`
|
|
545
|
-
else console.log(`
|
|
547
|
+
if (res && res.skipped) console.log(` - ${item.title}: ${res.skipped}`);
|
|
548
|
+
else console.log(` [OK] ${item.title}: ${res && res.detail ? res.detail : 'done'}`);
|
|
546
549
|
} catch (err) {
|
|
547
550
|
failed++;
|
|
548
|
-
console.error(`
|
|
551
|
+
console.error(` [FAIL] ${item.title}: ${err?.message || err}`);
|
|
549
552
|
}
|
|
550
553
|
}
|
|
551
|
-
console.log(uninstall ? '' : '
|
|
554
|
+
console.log(uninstall ? '' : ' undo: zcode-pro setup --uninstall');
|
|
552
555
|
if (failed) process.exitCode = 1;
|
|
553
556
|
}
|
package/src/host/taskIndex.mjs
CHANGED
|
@@ -206,6 +206,28 @@ export async function listOverriddenTitles(dataRoot) {
|
|
|
206
206
|
return { titles };
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
// 智能起名的前置判定:会话是否已被手动重命名(或已被起名写过)。双写前先查它,
|
|
210
|
+
// 已重命名就整体跳过——避免起名晚到的写入盖掉用户的 custom 标题。读取失败按
|
|
211
|
+
// 未重命名处理(后续写入自带原子守卫兜底)
|
|
212
|
+
export async function isTaskTitleOverridden(dataRoot, sessId) {
|
|
213
|
+
const dbFile = taskIndexPath(dataRoot);
|
|
214
|
+
if (!existsSync(dbFile)) return false;
|
|
215
|
+
const driver = await loadDriver();
|
|
216
|
+
if (!driver) return false;
|
|
217
|
+
try {
|
|
218
|
+
if (driver.kind === 'node') {
|
|
219
|
+
const db = new driver.DatabaseSync(dbFile, { readOnly: true });
|
|
220
|
+
try {
|
|
221
|
+
const r = db.prepare('SELECT title_overridden FROM tasks WHERE task_id = ? AND deleted = 0').get(sessId);
|
|
222
|
+
return r?.title_overridden === 1;
|
|
223
|
+
} finally { db.close(); }
|
|
224
|
+
}
|
|
225
|
+
const esc = (v) => "'" + String(v).replace(/'/g, "''") + "'";
|
|
226
|
+
const r = runCli(dbFile, `SELECT title_overridden FROM tasks WHERE task_id = ${esc(sessId)} AND deleted = 0;`);
|
|
227
|
+
return /^1/m.test(r.stdout);
|
|
228
|
+
} catch { return false; }
|
|
229
|
+
}
|
|
230
|
+
|
|
209
231
|
// 智能起名写入:把生成标题按“手动重命名”存入索引(title_overridden = 1),
|
|
210
232
|
// 之后官方自动标题不会再覆盖这条记录。仅在会话未删除、且从未被手动重命名过
|
|
211
233
|
// (title_overridden = 0)时写入——手动重命名永远优先。返回 { written, title };
|
|
@@ -266,10 +288,35 @@ function isBusyError(err) {
|
|
|
266
288
|
// { providerId, modelId };没有斜线时 providerId 退回 provider 列的值。
|
|
267
289
|
// 会话不存在返回 null;读取失败按 null 处理(调用方换明确报错)
|
|
268
290
|
export async function getTaskProviderModel(dataRoot, sessId) {
|
|
269
|
-
const dbFile = taskIndexPath(dataRoot);
|
|
270
|
-
if (!existsSync(dbFile)) return null;
|
|
271
291
|
const driver = await loadDriver();
|
|
272
292
|
if (!driver) return null;
|
|
293
|
+
const esc = (v) => "'" + String(v).replace(/'/g, "''") + "'";
|
|
294
|
+
|
|
295
|
+
// 优先读 CLI 会话库的 model_usage:首个 turn 开始时即写入,比任务索引的
|
|
296
|
+
// model 列早十几秒(任务索引由桌面同步器延迟写;新会话头十几秒里它还是
|
|
297
|
+
// 空值,起名第一次调用会因此解析到不存在的模型,实测 400 模型不存在)
|
|
298
|
+
const usageFile = join(dataRoot, 'cli', 'db', 'db.sqlite');
|
|
299
|
+
if (existsSync(usageFile)) {
|
|
300
|
+
try {
|
|
301
|
+
if (driver.kind === 'node') {
|
|
302
|
+
const db = new driver.DatabaseSync(usageFile, { readOnly: true });
|
|
303
|
+
try {
|
|
304
|
+
const r = db.prepare(
|
|
305
|
+
"SELECT provider_id, model_id FROM model_usage WHERE session_id = ? AND provider_id IS NOT NULL AND provider_id != '' ORDER BY started_at DESC LIMIT 1"
|
|
306
|
+
).get(sessId);
|
|
307
|
+
if (r && r.model_id) return { providerId: r.provider_id, modelId: r.model_id };
|
|
308
|
+
} finally { db.close(); }
|
|
309
|
+
} else {
|
|
310
|
+
const r = runCli(usageFile,
|
|
311
|
+
`SELECT coalesce(json_object('provider_id', provider_id, 'model_id', model_id), 'null') FROM (SELECT provider_id, model_id FROM model_usage WHERE session_id = ${esc(sessId)} AND provider_id IS NOT NULL AND provider_id != '' ORDER BY started_at DESC LIMIT 1);`);
|
|
312
|
+
const parsed = JSON.parse(r.stdout.trim() || 'null');
|
|
313
|
+
if (parsed && parsed.model_id) return { providerId: parsed.provider_id, modelId: parsed.model_id };
|
|
314
|
+
}
|
|
315
|
+
} catch { /* 表不存在(旧版 CLI)等:走任务索引兜底 */ }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const dbFile = taskIndexPath(dataRoot);
|
|
319
|
+
if (!existsSync(dbFile)) return null;
|
|
273
320
|
let row = null;
|
|
274
321
|
try {
|
|
275
322
|
if (driver.kind === 'node') {
|
|
@@ -279,7 +326,6 @@ export async function getTaskProviderModel(dataRoot, sessId) {
|
|
|
279
326
|
row = db.prepare('SELECT provider, model FROM tasks WHERE task_id = ? AND deleted = 0').get(sessId) || null;
|
|
280
327
|
} finally { db.close(); }
|
|
281
328
|
} else {
|
|
282
|
-
const esc = (v) => "'" + String(v).replace(/'/g, "''") + "'";
|
|
283
329
|
const r = runCli(dbFile,
|
|
284
330
|
`SELECT coalesce(json_object('provider', provider, 'model', model), 'null') FROM tasks WHERE task_id = ${esc(sessId)} AND deleted = 0;`);
|
|
285
331
|
const parsed = JSON.parse((r.stdout.trim() || 'null'));
|
|
@@ -336,3 +382,132 @@ function runCli(dbFile, script) {
|
|
|
336
382
|
if (r.status !== 0) throw new Error('sqlite3 CLI 执行失败: ' + (r.stderr || r.stdout || ('exit ' + r.status)));
|
|
337
383
|
return r;
|
|
338
384
|
}
|
|
385
|
+
|
|
386
|
+
// CLI 会话库(~/.zcode/cli/db/db.sqlite)的会话标题权威写入:title + title_source=
|
|
387
|
+
// 'custom'(官方 setCustomSessionTitle 同款语义)。custom 后官方运行时彻底知情:
|
|
388
|
+
// 自动起名写回后的自动校正:桌面端打开会话时会把运行时内存缓存的旧标题写回
|
|
389
|
+
// 任务索引(实测点开会话的瞬间发生,运行时不知道我们直写过 CLI 库),索引里的
|
|
390
|
+
// 标题因此被改回旧值。CLI 库的 custom 标题才是权威——对“我们写过、且 CLI 标题
|
|
391
|
+
// 仍等于我们所写”的会话,索引标题若不同就改回 CLI 值。只认自己写过的记录:
|
|
392
|
+
// 官方手动改名(renameTask)两库同步更新不会分歧;其 v4 同步失败时索引比 CLI 新
|
|
393
|
+
// (历史会话场景),不认记录就绝不会误回滚用户的手动改名。
|
|
394
|
+
// writtenMap: { sessId → { title, at } };返回本次校正的会话数(失败静默,下次再试)
|
|
395
|
+
export async function reconcileCustomTitles(dataRoot, writtenMap) {
|
|
396
|
+
const repaired = [];
|
|
397
|
+
if (!writtenMap || typeof writtenMap !== 'object') return repaired;
|
|
398
|
+
const driver = await loadDriver();
|
|
399
|
+
if (!driver) return repaired;
|
|
400
|
+
const esc = (v) => "'" + String(v).replace(/'/g, "''") + "'";
|
|
401
|
+
const cliFile = join(dataRoot, 'cli', 'db', 'db.sqlite');
|
|
402
|
+
const idxFile = taskIndexPath(dataRoot);
|
|
403
|
+
if (!existsSync(cliFile) || !existsSync(idxFile)) return repaired;
|
|
404
|
+
|
|
405
|
+
const readCli = async (sessId) => {
|
|
406
|
+
if (driver.kind === 'node') {
|
|
407
|
+
const db = new driver.DatabaseSync(cliFile, { readOnly: true });
|
|
408
|
+
try {
|
|
409
|
+
return db.prepare("SELECT title, title_source FROM session WHERE id = ?").get(sessId) || null;
|
|
410
|
+
} finally { db.close(); }
|
|
411
|
+
}
|
|
412
|
+
const r = runCli(cliFile, `SELECT coalesce(json_object('title', title, 'title_source', title_source), 'null') FROM session WHERE id = ${esc(sessId)};`);
|
|
413
|
+
const parsed = JSON.parse(r.stdout.trim() || 'null');
|
|
414
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
for (const [sessId, rec] of Object.entries(writtenMap)) {
|
|
418
|
+
if (!/^sess_[A-Za-z0-9._-]+$/.test(sessId) || !rec || typeof rec.title !== 'string') continue;
|
|
419
|
+
let cli;
|
|
420
|
+
try { cli = await readCli(sessId); } catch { continue; }
|
|
421
|
+
// CLI 已不是我们所写的 custom 标题:别人改过(手动改名/官方),不碰
|
|
422
|
+
if (!cli || cli.title_source !== 'custom' || cli.title !== rec.title) continue;
|
|
423
|
+
try {
|
|
424
|
+
if (driver.kind === 'node') {
|
|
425
|
+
const db = new driver.DatabaseSync(idxFile);
|
|
426
|
+
try {
|
|
427
|
+
db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
|
|
428
|
+
db.exec('BEGIN IMMEDIATE');
|
|
429
|
+
try {
|
|
430
|
+
const r = db.prepare(
|
|
431
|
+
'UPDATE tasks SET title = ? WHERE task_id = ? AND deleted = 0 AND title_overridden = 1 AND title != ?'
|
|
432
|
+
).run(rec.title, sessId, rec.title);
|
|
433
|
+
db.exec('COMMIT');
|
|
434
|
+
if (r.changes > 0) repaired.push(sessId);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
try { db.exec('ROLLBACK'); } catch { /* 已回滚 */ }
|
|
437
|
+
throw err;
|
|
438
|
+
}
|
|
439
|
+
} finally { db.close(); }
|
|
440
|
+
} else {
|
|
441
|
+
const r = runCli(idxFile,
|
|
442
|
+
'BEGIN IMMEDIATE;\n'
|
|
443
|
+
+ `UPDATE tasks SET title = ${esc(rec.title)} WHERE task_id = ${esc(sessId)} AND deleted = 0 AND title_overridden = 1 AND title != ${esc(rec.title)};\n`
|
|
444
|
+
+ 'SELECT changes();\nCOMMIT;');
|
|
445
|
+
if (Number(r.stdout.trim()) > 0) repaired.push(sessId);
|
|
446
|
+
}
|
|
447
|
+
} catch { /* 占用等:下次再校正 */ }
|
|
448
|
+
}
|
|
449
|
+
return repaired;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// 自动起名在写回前重读时看到 custom 会放弃(session-title.ts 的 custom_title 短路),
|
|
453
|
+
// 桌面同步器(zcodeTaskIndexSyncer)会把 custom 标题带着 titleOverridden=1 写回
|
|
454
|
+
// 任务索引——单写任务索引被官方排队的自动标题覆盖的竞态(实测 14 秒后覆盖)由此
|
|
455
|
+
// 消除。time_title_updated/time_updated 与官方 updateSession 一致(title 变更即刷新,
|
|
456
|
+
// time_updated 只增不减);title_message_id 保留原值。旧版 CLI 库没有 title_source
|
|
457
|
+
// 列时返回 skipped(缺 custom 语义,写了也守不住,退回单写任务索引的旧行为)。
|
|
458
|
+
// 库被占用时抛 index-busy(与任务索引写入同款重试语义)。
|
|
459
|
+
export async function setCustomSessionTitle(dataRoot, sessId, title) {
|
|
460
|
+
const dbFile = join(dataRoot, 'cli', 'db', 'db.sqlite');
|
|
461
|
+
if (!existsSync(dbFile)) return { written: false, skipped: 'no-cli-db' };
|
|
462
|
+
const driver = await loadDriver();
|
|
463
|
+
if (!driver) {
|
|
464
|
+
const err = new Error('缺少 SQLite 支持(需要 Node ≥23.4 或 sqlite3 命令)');
|
|
465
|
+
err.code = 'index-error';
|
|
466
|
+
throw err;
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
if (driver.kind === 'node') {
|
|
470
|
+
const db = new driver.DatabaseSync(dbFile);
|
|
471
|
+
try {
|
|
472
|
+
const cols = db.prepare("PRAGMA table_info('session')").all();
|
|
473
|
+
if (!cols.some((c) => c.name === 'title_source')) return { written: false, skipped: 'no-title-source' };
|
|
474
|
+
db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
|
|
475
|
+
db.exec('BEGIN IMMEDIATE');
|
|
476
|
+
try {
|
|
477
|
+
const now = Date.now();
|
|
478
|
+
const r = db.prepare(
|
|
479
|
+
"UPDATE session SET title = ?, title_source = 'custom', time_title_updated = ?, time_updated = max(time_updated, ?) WHERE id = ?"
|
|
480
|
+
).run(title, now, now, sessId);
|
|
481
|
+
db.exec('COMMIT');
|
|
482
|
+
return { written: r.changes > 0, skipped: r.changes > 0 ? '' : 'no-session-row' };
|
|
483
|
+
} catch (err) {
|
|
484
|
+
try { db.exec('ROLLBACK'); } catch { /* 已回滚/连接失效 */ }
|
|
485
|
+
throw err;
|
|
486
|
+
}
|
|
487
|
+
} finally {
|
|
488
|
+
db.close();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const esc = (v) => "'" + String(v).replace(/'/g, "''") + "'";
|
|
492
|
+
const cols = runCli(dbFile, "PRAGMA table_info('session');").stdout;
|
|
493
|
+
if (!/title_source/.test(cols)) return { written: false, skipped: 'no-title-source' };
|
|
494
|
+
const now = Date.now();
|
|
495
|
+
const r = runCli(dbFile,
|
|
496
|
+
'BEGIN IMMEDIATE;\n'
|
|
497
|
+
+ `UPDATE session SET title = ${esc(title)}, title_source = 'custom',`
|
|
498
|
+
+ ` time_title_updated = ${now}, time_updated = max(time_updated, ${now})`
|
|
499
|
+
+ ` WHERE id = ${esc(sessId)};\n`
|
|
500
|
+
+ 'SELECT changes();\nCOMMIT;');
|
|
501
|
+
const changed = Number(r.stdout.trim()) > 0;
|
|
502
|
+
return { written: changed, skipped: changed ? '' : 'no-session-row' };
|
|
503
|
+
} catch (err) {
|
|
504
|
+
if (isBusyError(err)) {
|
|
505
|
+
const e = new Error('CLI 会话库正被占用(ZCode 可能正在写入)');
|
|
506
|
+
e.code = 'index-busy';
|
|
507
|
+
throw e;
|
|
508
|
+
}
|
|
509
|
+
const e = new Error('写入 CLI 会话标题失败: ' + (err?.message || err));
|
|
510
|
+
e.code = 'index-error';
|
|
511
|
+
throw e;
|
|
512
|
+
}
|
|
513
|
+
}
|
package/src/inject/core.js
CHANGED
|
@@ -67,12 +67,16 @@ const zh = {
|
|
|
67
67
|
proxyScopeSyncDesc: 'WebDAV 上传与下载走代理。默认关闭:坚果云等国内网盘直连即可,代理停了同步会跟着失败。',
|
|
68
68
|
tabSync: '同步',
|
|
69
69
|
tabDebug: '调试',
|
|
70
|
-
syncDesc: '手动把 ZCode Pro 设置与模型设置备份到 WebDAV
|
|
70
|
+
syncDesc: '手动把 ZCode Pro 设置与模型设置备份到 WebDAV,或在新电脑上恢复。两项各自独立成文件,可只上传或只覆盖其中一项;每次上传会额外保留一份带时间戳的历史版本(保留多少个见“历史版本”一行,上传后自动删除超出的最旧版本),下载时任选一个版本恢复;仅在点击按钮时传输,不做自动同步。',
|
|
71
71
|
syncUrlPlaceholder: 'https://dav.jianguoyun.com/dav/',
|
|
72
72
|
syncDir: '上传目录',
|
|
73
73
|
syncDirPlaceholder: 'zcode-pro',
|
|
74
74
|
syncLogin: '登录',
|
|
75
75
|
syncPassword: '密码',
|
|
76
|
+
syncHistoryLimit: '历史版本',
|
|
77
|
+
syncHistoryLimitUnit: '个',
|
|
78
|
+
syncHistoryLimitHint: '云端最多保留最近几次上传留下的历史版本,上传成功后自动删除更早的;固定名最新版不受影响;0 = 不保留历史版本',
|
|
79
|
+
syncPruned: '已清理 {n} 个更早的历史版本',
|
|
76
80
|
syncSave: '保存',
|
|
77
81
|
syncSaved: '同步配置已保存',
|
|
78
82
|
syncFormHint: '同步文件存到“上传目录”(默认 zcode-pro,可多级,自动创建):zcodepro.json 与 model.json 始终是最新一版,旁边另保留带时间戳的历史版本。',
|
|
@@ -93,8 +97,8 @@ const zh = {
|
|
|
93
97
|
syncConfirmTitle: '从云端下载',
|
|
94
98
|
syncConfirmDesc: '将用所选文件覆盖本机对应的一项(覆盖前会在原文件旁留下 .zcodepro-backup 备份);本机的同步配置与登录凭据不受影响。模型设置恢复后建议重启 ZCode 生效。确认导入?',
|
|
95
99
|
syncConfirmGo: '确认导入',
|
|
96
|
-
syncPickDesc: '
|
|
97
|
-
syncVerDeleteLatestNote: '注意:这是“最新”文件,旧版 zcode-pro
|
|
100
|
+
syncPickDesc: '固定名文件始终是最新一版,带时间戳的是每次上传留下的历史版本;“导入”用所选文件覆盖本机对应的一项。',
|
|
101
|
+
syncVerDeleteLatestNote: '注意:这是“最新”文件,旧版 zcode-pro 的“从云端下载”依赖它,删除后旧版机器将无法下载;带时间戳的历史版本不受影响。',
|
|
98
102
|
syncHint: '同步登录凭据仅保存在本机 zcodepro.json;模型设置包含自定义供应商的 API Key,只会上传到你自己的 WebDAV。',
|
|
99
103
|
syncTest: '测试',
|
|
100
104
|
syncTesting: '测试中…',
|
|
@@ -118,8 +122,8 @@ const zh = {
|
|
|
118
122
|
syncVerDeleted: '已删除云端备份',
|
|
119
123
|
syncVerSaved: '已保存到',
|
|
120
124
|
devMode: '开发模式',
|
|
121
|
-
devModeRowDesc: '
|
|
122
|
-
devModeDesc: '开发调试用:改完 zcode-pro 代码后不必重启 ZCode
|
|
125
|
+
devModeRowDesc: '开启后出现“重载 ZCode Pro”按钮;也可给启动器设环境变量 ZCODEPRO_DEV=1 启用。此标签页仅在开发模式生效时显示。',
|
|
126
|
+
devModeDesc: '开发调试用:改完 zcode-pro 代码后不必重启 ZCode,点“重载 ZCode Pro”即可加载新代码。流程为自动构建(需在源码目录运行)→ 以新进程重启 ZCode Pro(本地助手与注入脚本都换新)→ 所有 ZCode 窗口自动刷新并重新注入;ZCode 本体与已打开的会话不受影响。',
|
|
123
127
|
devReload: '重载 ZCode Pro',
|
|
124
128
|
devReloading: '重载中…',
|
|
125
129
|
devReloadDesc: '点击后约 2-3 秒内所有 ZCode 窗口自动刷新一次并加载新脚本。',
|
|
@@ -140,7 +144,7 @@ const zh = {
|
|
|
140
144
|
lineHeightName: '回答行高',
|
|
141
145
|
lineHeightDesc: '回答正文的行高(倍数)。',
|
|
142
146
|
thinkingLineHeightName: '思考行高',
|
|
143
|
-
thinkingLineHeightDesc: '
|
|
147
|
+
thinkingLineHeightDesc: '“已工作/工作中”过程记录(读取/编辑/查阅/终端条目与思考内容)的行高(倍数)。',
|
|
144
148
|
userLineHeightName: '提问行高',
|
|
145
149
|
userLineHeightDesc: '提问内容的行高(倍数)。',
|
|
146
150
|
contentWidthName: '内容宽度',
|
|
@@ -429,12 +433,16 @@ const en = {
|
|
|
429
433
|
proxyScopeSyncDesc: 'WebDAV upload and download go through the proxy. Off by default: domestic WebDAV providers work fine direct, and a stopped proxy breaks sync.',
|
|
430
434
|
tabSync: 'Sync',
|
|
431
435
|
tabDebug: 'Debug',
|
|
432
|
-
syncDesc: 'Manually back up ZCode Pro settings and model settings to WebDAV, or restore them on a new computer. The two items are stored as separate files, so you can upload or overwrite either one alone; every upload also keeps a timestamped history version and download lets you pick any of them. Data moves only when you click, never automatically.',
|
|
436
|
+
syncDesc: 'Manually back up ZCode Pro settings and model settings to WebDAV, or restore them on a new computer. The two items are stored as separate files, so you can upload or overwrite either one alone; every upload also keeps a timestamped history version (how many to keep is set in the "History" row; older ones are removed automatically after upload) and download lets you pick any of them. Data moves only when you click, never automatically.',
|
|
433
437
|
syncUrlPlaceholder: 'https://dav.example.com/dav/',
|
|
434
|
-
syncDir: '
|
|
438
|
+
syncDir: 'Folder',
|
|
435
439
|
syncDirPlaceholder: 'zcode-pro',
|
|
436
440
|
syncLogin: 'Login',
|
|
437
441
|
syncPassword: 'Password',
|
|
442
|
+
syncHistoryLimit: 'History',
|
|
443
|
+
syncHistoryLimitUnit: 'versions',
|
|
444
|
+
syncHistoryLimitHint: 'How many recent uploads keep their history versions on the server; older ones are removed automatically after a successful upload. The fixed-name latest file is never removed. 0 = keep no history versions.',
|
|
445
|
+
syncPruned: 'removed {n} older history versions',
|
|
438
446
|
syncSave: 'Save',
|
|
439
447
|
syncSaved: 'Sync settings saved.',
|
|
440
448
|
syncFormHint: 'Sync files go inside the folder (default zcode-pro, multi-level allowed, created automatically): zcodepro.json and model.json are always the latest version; timestamped history versions are kept alongside.',
|
|
@@ -895,7 +903,7 @@ export function observeRadixPopups(onPopup) {
|
|
|
895
903
|
}
|
|
896
904
|
// 兜底:直接观察 role=menu 的 content。延迟一帧再回调——Radix 偶尔分两帧渲染
|
|
897
905
|
// (先挂 menu 容器、下一帧才填菜单项),立即回调时菜单项为空,调用方找不到
|
|
898
|
-
//
|
|
906
|
+
// 参照的菜单项就永远漏注入(菜单项填入不会再产生 role=menu 节点添加事件)
|
|
899
907
|
if (node.getAttribute && node.getAttribute('role') === 'menu' && !seen.has(node)) {
|
|
900
908
|
seen.add(node);
|
|
901
909
|
requestAnimationFrame(() => {
|