@jesonliu/lark-claudecode-bridge 1.3.8 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/web/js/pages/memory.js +450 -104
- package/config.example.yaml +19 -0
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/gateway/card-builder.js +3 -2
- package/dist/gateway/card-builder.js.map +1 -1
- package/dist/gateway/progress-card.d.ts +6 -2
- package/dist/gateway/progress-card.js +8 -4
- package/dist/gateway/progress-card.js.map +1 -1
- package/dist/index.js +39 -39
- package/dist/index.js.map +1 -1
- package/dist/memory-installer.d.ts +60 -5
- package/dist/memory-installer.js +318 -63
- package/dist/memory-installer.js.map +1 -1
- package/dist/memory.d.ts +11 -2
- package/dist/memory.js +15 -2
- package/dist/memory.js.map +1 -1
- package/dist/types.d.ts +17 -3
- package/dist/util/file-tracker.d.ts +2 -7
- package/dist/util/file-tracker.js +2 -24
- package/dist/util/file-tracker.js.map +1 -1
- package/dist/web/config-api.js +15 -8
- package/dist/web/config-api.js.map +1 -1
- package/dist/web/memory-api.d.ts +10 -2
- package/dist/web/memory-api.js +34 -6
- package/dist/web/memory-api.js.map +1 -1
- package/dist/web/memory-users-api.d.ts +76 -0
- package/dist/web/memory-users-api.js +461 -0
- package/dist/web/memory-users-api.js.map +1 -0
- package/dist/web/server.js +78 -5
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
package/dist/memory-installer.js
CHANGED
|
@@ -17,7 +17,7 @@ import { access } from 'node:fs/promises';
|
|
|
17
17
|
import { tmpdir } from 'node:os';
|
|
18
18
|
import { dirname, join } from 'node:path';
|
|
19
19
|
import { parseDocument } from 'yaml';
|
|
20
|
-
import { probeMemoryHealth, MEMORY_PLUGIN_NAME } from './memory.js';
|
|
20
|
+
import { probeMemoryHealth, MEMORY_PLUGIN_NAME, memoryEndpoint } from './memory.js';
|
|
21
21
|
/** 服务默认 pin 版本(实测环境;memory.version 可覆盖) */
|
|
22
22
|
export const DEFAULT_OPENVIKING_PIN = '0.5.0';
|
|
23
23
|
/** 插件源(市场清单 git-subdir 事实):主仓库 examples 子目录 */
|
|
@@ -50,6 +50,20 @@ const START_STEPS = () => [
|
|
|
50
50
|
let installState = { kind: 'install', running: false, steps: FRESH_STEPS() };
|
|
51
51
|
export function getInstallState() { return installState; }
|
|
52
52
|
let managed;
|
|
53
|
+
/** 受管进程最近输出环形缓冲上限(行;P0:失败文案带真实输出而非笼统指引) */
|
|
54
|
+
const MANAGED_OUTPUT_CAP = 40;
|
|
55
|
+
/** stdout/stderr 行入环形缓冲(spawnManaged 的两个 data 回调共用) */
|
|
56
|
+
function pushManagedOutput(line) {
|
|
57
|
+
if (!managed)
|
|
58
|
+
return;
|
|
59
|
+
managed.output.push(line.slice(0, 300));
|
|
60
|
+
if (managed.output.length > MANAGED_OUTPUT_CAP)
|
|
61
|
+
managed.output.shift();
|
|
62
|
+
}
|
|
63
|
+
/** 受管进程最近输出尾部(失败文案「最近输出:…」的数据源;exported 便于单测验证截断语义) */
|
|
64
|
+
export function managedRecentOutput(maxLines = 12) {
|
|
65
|
+
return (managed?.output ?? []).slice(-maxLines).join('\n');
|
|
66
|
+
}
|
|
53
67
|
/** 停受管实例(页面 stop / LCB shutdown / 重装前):幂等,未运行为 no-op */
|
|
54
68
|
export function stopManagedServer() {
|
|
55
69
|
if (!managed)
|
|
@@ -237,7 +251,10 @@ export const MANAGED_OV_CONF = join(MEMORY_MANAGED_DIR, 'ov.conf');
|
|
|
237
251
|
function vlmProvider(p) {
|
|
238
252
|
return p === 'glm' ? 'glm' : 'openai';
|
|
239
253
|
}
|
|
240
|
-
/** 渲染 ov.conf(纯函数 exported 便于单测)。rootKey 已有则复用(重装不换 key
|
|
254
|
+
/** 渲染 ov.conf(纯函数 exported 便于单测)。rootKey 已有则复用(重装不换 key,否则插件侧缓存/用户侧配置全部失效)。
|
|
255
|
+
* P2 双端点拆分:embedding.dense 与 vlm 各自独立——向量块显式配置(embeddingApiBase/embeddingApiKey)
|
|
256
|
+
* 优先,未设置回退主块(apiBase/apiKey),既有单份配置零迁移即可用;跨厂商组合(vlm=glm +
|
|
257
|
+
* embedding=openai 系)填向量块两字段即可。 */
|
|
241
258
|
export function renderOvConf(model, opts) {
|
|
242
259
|
const rootApiKey = opts.rootApiKey ?? randomBytes(24).toString('hex');
|
|
243
260
|
const conf = {
|
|
@@ -246,8 +263,8 @@ export function renderOvConf(model, opts) {
|
|
|
246
263
|
dense: {
|
|
247
264
|
provider: 'openai',
|
|
248
265
|
model: model.embeddingModel ?? '',
|
|
249
|
-
api_key: model.apiKey ?? '',
|
|
250
|
-
api_base: model.apiBase ?? '',
|
|
266
|
+
api_key: model.embeddingApiKey ?? model.apiKey ?? '',
|
|
267
|
+
api_base: model.embeddingApiBase ?? model.apiBase ?? '',
|
|
251
268
|
},
|
|
252
269
|
},
|
|
253
270
|
vlm: {
|
|
@@ -288,6 +305,97 @@ function readConfPort(confPath) {
|
|
|
288
305
|
return 1933;
|
|
289
306
|
}
|
|
290
307
|
}
|
|
308
|
+
// ============ 客户端接入配置(ovcli.conf,§6.7 第 4 步落地) ============
|
|
309
|
+
// 插件 isPluginEnabled() 的第 3 优先级判据 + 连接信息兜底层(env > ovcli.conf > ov.conf,
|
|
310
|
+
// 插件 scripts/shared/credentials.mjs 实测口径)。LCB 只写「无 key 骨架」:key 永远走 env
|
|
311
|
+
// 下发(§6.6),绝不落盘到 ~/.openviking/ 域。全新机器一键安装后即启用插件,用户零手工。
|
|
312
|
+
/** 客户端配置文件位置(与 EXTERNAL_OV_CONF 同款 home 解析口径) */
|
|
313
|
+
export const OVCLI_CONF = join(String(process.env.USERPROFILE ?? process.env.HOME ?? ''), '.openviking', 'ovcli.conf');
|
|
314
|
+
/** LCB 骨架形状签名:恰好 {url, account, user} 三个 string 键。这是「文件归 LCB 所有」的判定
|
|
315
|
+
* 依据——插件 setup 向导 / `ov login` 恒写 api_key(含 api_key 的文件永不与本形状同形),
|
|
316
|
+
* 且形状判定与端点/账号现值解耦:重装 findFreePort 换端口后旧骨架仍被识别为 LCB 所有
|
|
317
|
+
* (这是 ensure 能刷新、cleanup 能删中的关键;勿改成内容比对,否则端口漂移后清理永不触发) */
|
|
318
|
+
function isLcbClientConfShape(v) {
|
|
319
|
+
if (!v || typeof v !== 'object' || Array.isArray(v))
|
|
320
|
+
return false;
|
|
321
|
+
const o = v;
|
|
322
|
+
const keys = Object.keys(o);
|
|
323
|
+
return keys.length === 3 && ['url', 'account', 'user'].every((k) => typeof o[k] === 'string')
|
|
324
|
+
&& keys.every((k) => ['url', 'account', 'user'].includes(k));
|
|
325
|
+
}
|
|
326
|
+
/** 渲染 ovcli.conf(纯函数 exported 便于单测)。刻意不含 api_key / plugin 段;user 取 account
|
|
327
|
+
* 值:保证直连场景 identity 非空 → 插件判 trusted header 身份(dev 模式服务可用);留空会被
|
|
328
|
+
* 判成 api_key 模式而 401 */
|
|
329
|
+
export function renderOvcliConf(memory) {
|
|
330
|
+
return { url: memoryEndpoint(memory), account: memory.account ?? 'lcb', user: memory.account ?? 'lcb' };
|
|
331
|
+
}
|
|
332
|
+
/** 写/刷新客户端配置(幂等可重跑)。决策表:
|
|
333
|
+
* ① EXTERNAL_OV_CONF 存在 → 'skipped-external'(~/.openviking 域归用户自管,整体不碰;
|
|
334
|
+
* 外部 ov.conf 本身就满足插件启用判定,LCB 会话另有 env 通道兜底)
|
|
335
|
+
* ② 文件缺失 → mkdir recursive + writeAtomic(渲染值) → 'written'
|
|
336
|
+
* ③ 文件存在且为 LCB 骨架:内容与渲染值全等 → 'kept'(幂等零写);否则重写 → 'updated'
|
|
337
|
+
* (端口漂移/账号改名/remote↔embedded 切换后的自愈路径)
|
|
338
|
+
* ④ 其余(含 api_key 的用户手配、plugin 段、损坏 JSON)→ 'skipped-foreign'(尊重不动)
|
|
339
|
+
* dirOverride 供单测注入临时目录(ov.conf/ovcli.conf 一并重定向,摆脱模块常量的 home 依赖) */
|
|
340
|
+
export function ensureClientCliConf(memory, dirOverride) {
|
|
341
|
+
const ovConf = dirOverride ? join(dirOverride, 'ov.conf') : EXTERNAL_OV_CONF;
|
|
342
|
+
const ovcli = dirOverride ? join(dirOverride, 'ovcli.conf') : OVCLI_CONF;
|
|
343
|
+
if (existsSync(ovConf))
|
|
344
|
+
return 'skipped-external';
|
|
345
|
+
const next = renderOvcliConf(memory);
|
|
346
|
+
let cur;
|
|
347
|
+
try {
|
|
348
|
+
cur = JSON.parse(readFileSync(ovcli, 'utf8'));
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
cur = undefined;
|
|
352
|
+
}
|
|
353
|
+
if (cur === undefined && !existsSync(ovcli)) {
|
|
354
|
+
writeAtomic(ovcli, JSON.stringify(next, null, 2));
|
|
355
|
+
return 'written';
|
|
356
|
+
}
|
|
357
|
+
if (isLcbClientConfShape(cur)) {
|
|
358
|
+
if (JSON.stringify(cur) === JSON.stringify(next))
|
|
359
|
+
return 'kept';
|
|
360
|
+
writeAtomic(ovcli, JSON.stringify(next, null, 2));
|
|
361
|
+
return 'updated';
|
|
362
|
+
}
|
|
363
|
+
return 'skipped-foreign';
|
|
364
|
+
}
|
|
365
|
+
/** 关记忆的对称清理:仅当解析后命中 LCB 骨架形状才删(返回是否删除)。刻意不做内容匹配
|
|
366
|
+
* (见 isLcbClientConfShape 注释);损坏/手配文件一律保留 */
|
|
367
|
+
export function cleanupClientCliConf(dirOverride) {
|
|
368
|
+
const ovcli = dirOverride ? join(dirOverride, 'ovcli.conf') : OVCLI_CONF;
|
|
369
|
+
let cur;
|
|
370
|
+
try {
|
|
371
|
+
cur = JSON.parse(readFileSync(ovcli, 'utf8'));
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
if (!isLcbClientConfShape(cur))
|
|
377
|
+
return false;
|
|
378
|
+
rmSync(ovcli, { force: true });
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
/** 状态页分类(exported 便于 memory-api 与单测复用;路径入参可注入摆脱 home 依赖)。
|
|
382
|
+
* external 优先:外部 ov.conf 存在即整个 ~/.openviking 域归用户(与 ensure 的守卫口径一致) */
|
|
383
|
+
export function classifyClientConf(paths) {
|
|
384
|
+
if (existsSync(paths.ovConf))
|
|
385
|
+
return { state: 'external', path: paths.ovConf };
|
|
386
|
+
let cur;
|
|
387
|
+
try {
|
|
388
|
+
cur = JSON.parse(readFileSync(paths.ovcli, 'utf8'));
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
cur = undefined;
|
|
392
|
+
}
|
|
393
|
+
if (isLcbClientConfShape(cur))
|
|
394
|
+
return { state: 'lcb-managed', path: paths.ovcli };
|
|
395
|
+
if (existsSync(paths.ovcli))
|
|
396
|
+
return { state: 'user-managed', path: paths.ovcli };
|
|
397
|
+
return { state: 'missing', path: paths.ovcli };
|
|
398
|
+
}
|
|
291
399
|
// ============ 步骤 4:受管拉起 ============
|
|
292
400
|
/** 找空闲端口:从 1933 起「net 试探 + HTTP 探活」双重确认(Windows SO_REUSEADDR 允许抢绑,
|
|
293
401
|
* net listen 成功 ≠ 端口空闲——已有 OpenViking 服务在监听时 listen 可能误报空闲;探活有
|
|
@@ -310,40 +418,101 @@ export async function findFreePort(startPort = 1933, tries = 20) {
|
|
|
310
418
|
throw new Error(`${startPort}-${startPort + tries - 1} 端口段均被占用`);
|
|
311
419
|
}
|
|
312
420
|
/** 拉起受管实例并探活等待就绪(安装流水线第 4 步与「启动服务」按钮共用)。
|
|
313
|
-
*
|
|
421
|
+
* P0 启动可靠性(记忆系统最终方案 v1.1 §五):
|
|
422
|
+
* - 探活预算 30 → 120 轮(首装冷启动最坏路径:OS 缓存全冷 + Defender 实时扫描 + 重依赖加载,
|
|
423
|
+
* 常超 30s——旧预算 + 超时即杀是「第一次必败」竞态的根因);
|
|
424
|
+
* - 超时不杀进程:预算耗尽且进程仍活 → 返回 { ready:false, crashed:false },调用方转后台续等
|
|
425
|
+
* (waitSlowStart),UI 显示「启动中(已等 N 秒)」而非红叉,就绪后自动续跑后续步骤;
|
|
426
|
+
* - 进程退出(崩溃 3 次放弃 / 被停止)→ { ready:false, crashed:true },文案带最近真实输出。 */
|
|
427
|
+
const PROBE_BUDGET_ROUNDS = 120;
|
|
428
|
+
/** 慢启动后台续等窗口(预算耗尽且进程存活时;10 分钟仍未见就绪才放弃并停止进程,防僵尸占端口) */
|
|
429
|
+
const SLOW_START_MAX_MS = 600_000;
|
|
314
430
|
async function launchManagedAndWait(bin, confPath, port, external = false) {
|
|
315
431
|
spawnManaged(bin, confPath, port);
|
|
316
|
-
for (let i =
|
|
432
|
+
for (let i = 1; i <= PROBE_BUDGET_ROUNDS; i++) {
|
|
317
433
|
await new Promise((r) => setTimeout(r, 1000));
|
|
318
434
|
const h = await probeMemoryHealth(`http://127.0.0.1:${port}`, 1500);
|
|
319
435
|
if (h.healthy) {
|
|
320
|
-
return {
|
|
436
|
+
return {
|
|
437
|
+
ready: true, crashed: false, port,
|
|
438
|
+
detail: `服务已就绪:http://127.0.0.1:${port}${external ? '(外部配置 ~/.openviking/ov.conf,LCB 仅代为拉起、不改写)' : ''}${i > 15 ? `(冷启动 ${i} 秒)` : ''}`,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
if (!managed) {
|
|
442
|
+
// 崩溃自动重启 3 次放弃 / 被主动停止:managed 已清。文案带最近输出(stdout+stderr 合流),
|
|
443
|
+
// 不再误导为「模型配置问题」——真实原因看输出(方案 §五-P0)
|
|
444
|
+
return { ready: false, crashed: true, port, detail: `服务进程已退出(等待 ${i} 秒)。最近输出:\n${managedRecentOutput() || '(无输出)'}` };
|
|
321
445
|
}
|
|
446
|
+
}
|
|
447
|
+
if (managed) {
|
|
448
|
+
// 预算耗尽但进程存活:绝不杀进程(调用方转后台续等)
|
|
449
|
+
return { ready: false, crashed: false, port, detail: `探活预算(${PROBE_BUDGET_ROUNDS} 秒)内未见就绪,服务进程仍在运行,转后台继续等待` };
|
|
450
|
+
}
|
|
451
|
+
return { ready: false, crashed: true, port, detail: `服务进程在等待期间退出。最近输出:\n${managedRecentOutput() || '(无输出)'}` };
|
|
452
|
+
}
|
|
453
|
+
/** P0 慢启动后台续等:预算耗尽但进程存活时,后台每 2s 探活;onTick 上报累计已等秒数(UI 实时
|
|
454
|
+
* 「启动中(已等 N 秒)」)。就绪 → ok;进程放弃(崩溃 3 次/被停止)或窗口耗尽 → !ok——窗口
|
|
455
|
+
* 耗尽时停止未就绪进程(防僵尸占端口),文案如实说明并附最近输出。 */
|
|
456
|
+
async function waitSlowStart(port, onTick) {
|
|
457
|
+
const begin = Date.now();
|
|
458
|
+
while (Date.now() - begin < SLOW_START_MAX_MS) {
|
|
459
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
460
|
+
const elapsed = PROBE_BUDGET_ROUNDS + Math.round((Date.now() - begin) / 1000);
|
|
461
|
+
const h = await probeMemoryHealth(`http://127.0.0.1:${port}`, 1500);
|
|
462
|
+
if (h.healthy)
|
|
463
|
+
return { ok: true, detail: `服务已就绪(累计等待 ${elapsed} 秒):http://127.0.0.1:${port}` };
|
|
322
464
|
if (!managed)
|
|
323
|
-
|
|
465
|
+
return { ok: false, detail: `服务进程已退出(累计等待 ${elapsed} 秒)。最近输出:\n${managedRecentOutput() || '(无输出)'}` };
|
|
466
|
+
onTick(elapsed);
|
|
324
467
|
}
|
|
325
|
-
const
|
|
468
|
+
const detail = `服务启动超时(已等 ${PROBE_BUDGET_ROUNDS + Math.round(SLOW_START_MAX_MS / 1000)} 秒仍未见就绪),已停止该进程。最近输出:\n${managedRecentOutput() || '(无输出)'}`;
|
|
326
469
|
stopManagedServer();
|
|
327
|
-
|
|
470
|
+
return { ok: false, detail };
|
|
328
471
|
}
|
|
329
|
-
|
|
472
|
+
/** 流水线统一收尾:err 时把 running 步骤置 failed,无论成败都关闭 running 态(后台续跑路径共用) */
|
|
473
|
+
function finalizeInstallState(steps, err) {
|
|
474
|
+
const failed = steps.find((x) => x.status === 'running');
|
|
475
|
+
if (err && failed) {
|
|
476
|
+
failed.status = 'failed';
|
|
477
|
+
failed.detail = err instanceof Error ? err.message : String(err);
|
|
478
|
+
}
|
|
479
|
+
installState.running = false;
|
|
480
|
+
installState.finishedAt = new Date().toISOString();
|
|
481
|
+
}
|
|
482
|
+
async function stepStart(bin, confPath, opts = {}) {
|
|
330
483
|
const port = readConfPort(confPath);
|
|
331
|
-
//
|
|
484
|
+
// 已有受管实例:探活通即跳过(幂等重跑);forceRestart(P1 保存即生效)则重启以应用新配置
|
|
332
485
|
if (managed && !managed.stopped) {
|
|
333
486
|
const h = await probeMemoryHealth(`http://127.0.0.1:${managed.port}`, 2000);
|
|
334
|
-
if (h.healthy)
|
|
335
|
-
return { skipped: true, detail: `受管实例已在运行(127.0.0.1:${managed.port})`, port: managed.port };
|
|
487
|
+
if (h.healthy && !opts.forceRestart) {
|
|
488
|
+
return { skipped: true, ready: true, crashed: false, detail: `受管实例已在运行(127.0.0.1:${managed.port})`, port: managed.port };
|
|
489
|
+
}
|
|
336
490
|
stopManagedServer();
|
|
491
|
+
await new Promise((r) => setTimeout(r, 1000)); // 端口释放窗口(TIME_WAIT)
|
|
337
492
|
}
|
|
338
493
|
const r = await launchManagedAndWait(bin, confPath, port);
|
|
339
|
-
return {
|
|
494
|
+
return { ...r, skipped: false };
|
|
340
495
|
}
|
|
341
496
|
// ============ 单独启动(「启动服务」按钮 → LCB 接管式启动流水线) ============
|
|
497
|
+
/** 启动流水线尾部(③插件接入 + 回写):正常路径与慢启动后台续跑共用 */
|
|
498
|
+
async function finishStartTail(opts, port, external, set) {
|
|
499
|
+
set('plugin', 'running');
|
|
500
|
+
const pl = await stepPlugin({
|
|
501
|
+
endpoint: `http://127.0.0.1:${port}`,
|
|
502
|
+
account: await memoryAccount(opts.configPath),
|
|
503
|
+
});
|
|
504
|
+
set('plugin', pl.skipped ? 'skipped' : 'done', pl.detail);
|
|
505
|
+
if (pl.writeback) {
|
|
506
|
+
// 插件新装/调和才回写:受管场景顺带对齐 endpoint/mode;外部场景不动用户配置(只补插件引用)
|
|
507
|
+
writeBackConfig(opts.configPath, external ? {} : { endpoint: `http://127.0.0.1:${port}`, mode: 'embedded' }, pl.writeback.pluginPath, pl.writeback.opts);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
342
510
|
/**
|
|
343
511
|
* 启动流水线(kind='start',后台执行 + 与安装同款 stepper 轮询展示):LCB 已接管 OpenViking,
|
|
344
512
|
* 「启动」就应当是接管的启动——不只拉进程,还要把记忆链路整段拉就绪:
|
|
345
513
|
* ① 检查服务与配置(bin / ov.conf 定位,缺失即 failed + 指引改走「检查修复 / 一键安装」)
|
|
346
|
-
* ② 拉起并探活(受管拉起 +
|
|
514
|
+
* ② 拉起并探活(受管拉起 + P0 预算 120s;预算耗尽且进程存活 → 转后台续等(UI 显示
|
|
515
|
+
* 「启动中(已等 N 秒)」),就绪自动续跑 ③;已在运行则 skipped,幂等重跑)
|
|
347
516
|
* ③ 接入记忆插件(CLI 市场渠道真实安装:已登记启用 → skipped;缺失 → marketplace add/install/enable
|
|
348
517
|
* 三步幂等安装,插件页可见可管理;CLI 失败回退 zip 下载自管副本)
|
|
349
518
|
* 完成后无须再点「检查修复」——启动结束即完整接管态。任何一步失败即停(同安装流水线语义)。
|
|
@@ -388,29 +557,42 @@ export async function runStartPipeline(opts) {
|
|
|
388
557
|
}
|
|
389
558
|
if (needLaunch) {
|
|
390
559
|
const r = await launchManagedAndWait(bin, confPath, readConfPort(confPath), external);
|
|
560
|
+
if (!r.ready) {
|
|
561
|
+
if (r.crashed) {
|
|
562
|
+
set('launch', 'failed', r.detail);
|
|
563
|
+
throw new Error(r.detail);
|
|
564
|
+
}
|
|
565
|
+
// P0 慢启动:进程存活 → 不杀不红叉,保持流水线 running 转后台续等,就绪自动续跑 ③
|
|
566
|
+
set('launch', 'running', `启动中(已等 ${PROBE_BUDGET_ROUNDS} 秒)…`);
|
|
567
|
+
void (async () => {
|
|
568
|
+
const slow = await waitSlowStart(r.port, (s) => set('launch', 'running', `启动中(已等 ${s} 秒)…`));
|
|
569
|
+
if (!slow.ok) {
|
|
570
|
+
set('launch', 'failed', slow.detail);
|
|
571
|
+
finalizeInstallState(steps);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
set('launch', 'done', slow.detail);
|
|
575
|
+
try {
|
|
576
|
+
await finishStartTail(opts, r.port, external, set);
|
|
577
|
+
finalizeInstallState(steps);
|
|
578
|
+
}
|
|
579
|
+
catch (e) {
|
|
580
|
+
finalizeInstallState(steps, e);
|
|
581
|
+
}
|
|
582
|
+
})();
|
|
583
|
+
return installState; // running 保持 true:前端持续轮询看实时秒数
|
|
584
|
+
}
|
|
391
585
|
port = r.port;
|
|
392
586
|
set('launch', 'done', r.detail);
|
|
393
587
|
}
|
|
394
|
-
// ③
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (pl.writeback) {
|
|
399
|
-
// 插件新装/调和才回写:受管场景顺带对齐 endpoint/mode;外部场景不动用户配置(只补插件引用)
|
|
400
|
-
writeBackConfig(opts.configPath, external ? {} : { endpoint: `http://127.0.0.1:${port ?? 1933}`, mode: 'embedded' }, pl.writeback.pluginPath, pl.writeback.opts);
|
|
401
|
-
}
|
|
402
|
-
installState.running = false;
|
|
403
|
-
installState.finishedAt = new Date().toISOString();
|
|
588
|
+
// ③ 插件接入(启动完整性的另一半:插件缺失时记忆照样不工作——补齐并回写显式引用;
|
|
589
|
+
// client 传 endpoint/account:顺带落客户端接入配置 ovcli.conf,外部模式由 ensure 自守卫跳过)
|
|
590
|
+
await finishStartTail(opts, port ?? 1933, external, set);
|
|
591
|
+
finalizeInstallState(steps);
|
|
404
592
|
return installState;
|
|
405
593
|
}
|
|
406
594
|
catch (e) {
|
|
407
|
-
|
|
408
|
-
installState.finishedAt = new Date().toISOString();
|
|
409
|
-
const failed = steps.find((x) => x.status === 'running');
|
|
410
|
-
if (failed) {
|
|
411
|
-
failed.status = 'failed';
|
|
412
|
-
failed.detail = e instanceof Error ? e.message : String(e);
|
|
413
|
-
}
|
|
595
|
+
finalizeInstallState(steps, e);
|
|
414
596
|
return installState;
|
|
415
597
|
}
|
|
416
598
|
}
|
|
@@ -419,7 +601,7 @@ function spawnManaged(bin, confPath, port) {
|
|
|
419
601
|
windowsHide: true,
|
|
420
602
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
421
603
|
});
|
|
422
|
-
managed = { proc, port, restarts: 0, stopped: false, confPath, startedAt: new Date().toISOString() };
|
|
604
|
+
managed = { proc, port, restarts: 0, stopped: false, confPath, startedAt: new Date().toISOString(), output: [] };
|
|
423
605
|
proc.on('exit', () => {
|
|
424
606
|
if (!managed || managed.proc !== proc)
|
|
425
607
|
return;
|
|
@@ -447,10 +629,25 @@ function spawnManaged(bin, confPath, port) {
|
|
|
447
629
|
managed = undefined;
|
|
448
630
|
}
|
|
449
631
|
});
|
|
632
|
+
// P0:stdout 原先只 pipe 不消费——缓冲写满会阻塞子进程且信息全丢;现转发控制台并入环形缓冲
|
|
633
|
+
//(stderr 同款),失败文案「最近输出:…」即来源于此
|
|
634
|
+
proc.stdout?.on('data', (d) => {
|
|
635
|
+
for (const raw of String(d).split(/\r?\n/)) {
|
|
636
|
+
const line = raw.trim();
|
|
637
|
+
if (!line)
|
|
638
|
+
continue;
|
|
639
|
+
pushManagedOutput(line);
|
|
640
|
+
console.log('[memory-server]', line.slice(0, 300));
|
|
641
|
+
}
|
|
642
|
+
});
|
|
450
643
|
proc.stderr?.on('data', (d) => {
|
|
451
|
-
const
|
|
452
|
-
|
|
644
|
+
for (const raw of String(d).split(/\r?\n/)) {
|
|
645
|
+
const line = raw.trim();
|
|
646
|
+
if (!line)
|
|
647
|
+
continue;
|
|
648
|
+
pushManagedOutput(line);
|
|
453
649
|
console.warn('[memory-server]', line.slice(0, 300));
|
|
650
|
+
}
|
|
454
651
|
});
|
|
455
652
|
}
|
|
456
653
|
// ============ 步骤 5:插件 ============
|
|
@@ -639,8 +836,22 @@ async function cliInstallViaMarketplace(claudeDir, dirs) {
|
|
|
639
836
|
: { detail: `安装动作已完成但未能自动发现 ${MARKETPLACE_KEY}(installed_plugins/enabledPlugins 未生效,请反馈)` };
|
|
640
837
|
}
|
|
641
838
|
/** 接入记忆插件(安装流水线第 5 步/启动流水线第 3 步共用;exported 便于运维脚本与单测直调):
|
|
642
|
-
* ① CLI marketplace 双登记命中 → skipped;② CLI 真实安装(幂等);③ zip 兜底 + CLI
|
|
643
|
-
|
|
839
|
+
* ① CLI marketplace 双登记命中 → skipped;② CLI 真实安装(幂等);③ zip 兜底 + CLI 同构登记。
|
|
840
|
+
* client 传入时(endpoint/account)同时落客户端接入配置 ovcli.conf(§6.7 第 4 步:插件启用判定
|
|
841
|
+
* 的文件兜底层;写入失败只 warn 不阻塞插件接入——LCB 会话另有 env 通道兜底) */
|
|
842
|
+
export async function stepPlugin(client) {
|
|
843
|
+
// 客户端配置先落(三个 return 分支都要携带:①分支是高频提前 return——「已装但无文件」的
|
|
844
|
+
// 存量机器最常见形态,放函数体后段会漏掉)
|
|
845
|
+
let clientConf;
|
|
846
|
+
if (client) {
|
|
847
|
+
try {
|
|
848
|
+
clientConf = ensureClientCliConf(client);
|
|
849
|
+
}
|
|
850
|
+
catch (e) {
|
|
851
|
+
console.warn('[memory-installer] ovcli.conf 写入失败(不阻塞插件接入):', e instanceof Error ? e.message : e);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
const detailSuffix = clientConf ? `;客户端配置 ${clientConf}` : '';
|
|
644
855
|
const { loadConfig } = await import('./config.js');
|
|
645
856
|
const { DEFAULT_CLAUDE_DIR, resolveClaudeDir } = await import('./claude-config.js');
|
|
646
857
|
let claudeDir = DEFAULT_CLAUDE_DIR;
|
|
@@ -659,7 +870,8 @@ export async function stepPlugin() {
|
|
|
659
870
|
if (hit)
|
|
660
871
|
return {
|
|
661
872
|
skipped: true,
|
|
662
|
-
detail: `已安装(CLI marketplace 已启用):${hit.path}`,
|
|
873
|
+
detail: `已安装(CLI marketplace 已启用):${hit.path}${detailSuffix}`,
|
|
874
|
+
clientConf,
|
|
663
875
|
writeback: {
|
|
664
876
|
pluginPath: hit.path,
|
|
665
877
|
opts: { reconcileOnly: true, artifactRoots: [...dirs.map((x) => join(x, 'plugins', 'cache')), MANAGED_PLUGIN_ROOT] },
|
|
@@ -675,7 +887,8 @@ export async function stepPlugin() {
|
|
|
675
887
|
if (cli.installPath) {
|
|
676
888
|
return {
|
|
677
889
|
skipped: false,
|
|
678
|
-
detail: `${cli.detail}
|
|
890
|
+
detail: `${cli.detail}(插件页可见,可启停/更新)${detailSuffix}`,
|
|
891
|
+
clientConf,
|
|
679
892
|
writeback: {
|
|
680
893
|
pluginPath: cli.installPath,
|
|
681
894
|
opts: { reconcileOnly: true, artifactRoots: [...dirs.map((d) => join(d, 'plugins', 'cache')), MANAGED_PLUGIN_ROOT] },
|
|
@@ -721,7 +934,8 @@ export async function stepPlugin() {
|
|
|
721
934
|
invalidatePluginCache(claudeDir);
|
|
722
935
|
return {
|
|
723
936
|
skipped: false,
|
|
724
|
-
detail: `已安装 v${version}(CLI 同构登记)→ ${dest}
|
|
937
|
+
detail: `已安装 v${version}(CLI 同构登记)→ ${dest}(插件页可见,可启停/更新)${detailSuffix}`,
|
|
938
|
+
clientConf,
|
|
725
939
|
writeback: {
|
|
726
940
|
pluginPath: dest,
|
|
727
941
|
opts: { reconcileOnly: true, artifactRoots: [...dirs.map((d) => join(d, 'plugins', 'cache')), MANAGED_PLUGIN_ROOT] },
|
|
@@ -807,9 +1021,42 @@ export function writeBackConfig(configPath, wb, pluginPath, opts = {}) {
|
|
|
807
1021
|
return;
|
|
808
1022
|
writeAtomic(configPath, doc.toString({ lineWidth: 0 }));
|
|
809
1023
|
}
|
|
1024
|
+
// ============ 主流水线 ============
|
|
1025
|
+
/** 读 config.yaml 的 memory.account(懒加载;损坏配置降级 undefined → 渲染取缺省 'lcb')。
|
|
1026
|
+
* 两条流水线原本不读 config.yaml(只有 configPath),stepPlugin 落 ovcli.conf 时需要它 */
|
|
1027
|
+
async function memoryAccount(configPath) {
|
|
1028
|
+
try {
|
|
1029
|
+
const { loadConfig } = await import('./config.js');
|
|
1030
|
+
return loadConfig(configPath).memory?.account;
|
|
1031
|
+
}
|
|
1032
|
+
catch {
|
|
1033
|
+
return undefined;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
/** 安装流水线尾部(⑤插件接入 + ⑥回写 config.yaml):正常路径与慢启动后台续跑共用 */
|
|
1037
|
+
async function finishInstallTail(opts, port, firstInstallKey, set) {
|
|
1038
|
+
// ⑤ 插件(client 传 endpoint/account:顺带落客户端接入配置 ovcli.conf,§6.7 第 4 步)
|
|
1039
|
+
set('plugin', 'running');
|
|
1040
|
+
const pl = await stepPlugin({
|
|
1041
|
+
endpoint: `http://127.0.0.1:${port}`,
|
|
1042
|
+
account: await memoryAccount(opts.configPath),
|
|
1043
|
+
});
|
|
1044
|
+
set('plugin', pl.skipped ? 'skipped' : 'done', pl.detail);
|
|
1045
|
+
// ⑥ 回写 config.yaml——全链成功才落盘(半成品不污染用户配置):endpoint / mode=embedded /
|
|
1046
|
+
// 首装生成的 root_api_key 回写 memory.api_key(复用已有 key 不写)+ 插件引用回写(CLI 渠道为
|
|
1047
|
+
// 调和模式、zip 兜底为补齐模式,选项由 stepPlugin 携带)
|
|
1048
|
+
writeBackConfig(opts.configPath, {
|
|
1049
|
+
endpoint: `http://127.0.0.1:${port}`,
|
|
1050
|
+
mode: 'embedded',
|
|
1051
|
+
...(firstInstallKey ? { apiKey: firstInstallKey } : {}),
|
|
1052
|
+
}, pl.writeback?.pluginPath, pl.writeback?.opts);
|
|
1053
|
+
}
|
|
810
1054
|
/**
|
|
811
1055
|
* 一键安装流水线(幂等锁:running 中重复调用直接返回当前状态)。
|
|
812
1056
|
* 任何一步失败即停(状态留 failed + detail),页面可整链重跑——已就绪步骤自动 skipped。
|
|
1057
|
+
* P0 慢启动语义:拉起步骤预算耗尽但进程存活时,流水线保持 running、转后台续等(UI 实时显示
|
|
1058
|
+
* 「启动中(已等 N 秒)」),就绪后自动续跑 ⑤⑥ 并收尾——调用方(HTTP 层)此刻即返回,
|
|
1059
|
+
* 前端经 /api/memory/install/status 轮询看到全过程。
|
|
813
1060
|
*/
|
|
814
1061
|
export async function runInstall(opts) {
|
|
815
1062
|
if (installState.running)
|
|
@@ -876,32 +1123,40 @@ export async function runInstall(opts) {
|
|
|
876
1123
|
}
|
|
877
1124
|
// ④ 拉起
|
|
878
1125
|
set('start', 'running');
|
|
879
|
-
const st = await stepStart(svc.bin, MANAGED_OV_CONF);
|
|
1126
|
+
const st = await stepStart(svc.bin, MANAGED_OV_CONF, { forceRestart: opts.forceRestart });
|
|
1127
|
+
if (!st.ready) {
|
|
1128
|
+
if (st.crashed) {
|
|
1129
|
+
set('start', 'failed', st.detail);
|
|
1130
|
+
throw new Error(st.detail);
|
|
1131
|
+
}
|
|
1132
|
+
// P0 慢启动:进程存活 → 不杀不红叉,保持流水线 running 转后台续等,就绪自动续跑 ⑤⑥
|
|
1133
|
+
set('start', 'running', `启动中(已等 ${PROBE_BUDGET_ROUNDS} 秒)…`);
|
|
1134
|
+
void (async () => {
|
|
1135
|
+
const slow = await waitSlowStart(st.port, (s) => set('start', 'running', `启动中(已等 ${s} 秒)…`));
|
|
1136
|
+
if (!slow.ok) {
|
|
1137
|
+
set('start', 'failed', slow.detail);
|
|
1138
|
+
finalizeInstallState(steps);
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
set('start', 'done', slow.detail);
|
|
1142
|
+
try {
|
|
1143
|
+
await finishInstallTail(opts, st.port, firstInstallKey, set);
|
|
1144
|
+
finalizeInstallState(steps);
|
|
1145
|
+
}
|
|
1146
|
+
catch (e) {
|
|
1147
|
+
finalizeInstallState(steps, e);
|
|
1148
|
+
}
|
|
1149
|
+
})();
|
|
1150
|
+
return installState; // running 保持 true:前端持续轮询看实时秒数
|
|
1151
|
+
}
|
|
880
1152
|
set('start', st.skipped ? 'skipped' : 'done', st.detail);
|
|
881
|
-
//
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
set('plugin', pl.skipped ? 'skipped' : 'done', pl.detail);
|
|
885
|
-
// ⑥ 回写 config.yaml——全链成功才落盘(半成品不污染用户配置):endpoint / mode=embedded /
|
|
886
|
-
// 首装生成的 root_api_key 回写 memory.api_key(复用已有 key 不写)+ 插件引用回写(CLI 渠道为
|
|
887
|
-
// 调和模式、zip 兜底为补齐模式,选项由 stepPlugin 携带)
|
|
888
|
-
writeBackConfig(opts.configPath, {
|
|
889
|
-
endpoint: `http://127.0.0.1:${st.port}`,
|
|
890
|
-
mode: 'embedded',
|
|
891
|
-
...(firstInstallKey ? { apiKey: firstInstallKey } : {}),
|
|
892
|
-
}, pl.writeback?.pluginPath, pl.writeback?.opts);
|
|
893
|
-
installState.running = false;
|
|
894
|
-
installState.finishedAt = new Date().toISOString();
|
|
1153
|
+
// ⑤⑥ 插件接入 + 回写(尾部提取:与慢启动后台续跑共用)
|
|
1154
|
+
await finishInstallTail(opts, st.port, firstInstallKey, set);
|
|
1155
|
+
finalizeInstallState(steps);
|
|
895
1156
|
return installState;
|
|
896
1157
|
}
|
|
897
1158
|
catch (e) {
|
|
898
|
-
|
|
899
|
-
installState.finishedAt = new Date().toISOString();
|
|
900
|
-
const failed = steps.find((x) => x.status === 'running');
|
|
901
|
-
if (failed) {
|
|
902
|
-
failed.status = 'failed';
|
|
903
|
-
failed.detail = e instanceof Error ? e.message : String(e);
|
|
904
|
-
}
|
|
1159
|
+
finalizeInstallState(steps, e);
|
|
905
1160
|
return installState;
|
|
906
1161
|
}
|
|
907
1162
|
}
|