@jesonliu/lark-claudecode-bridge 1.4.0 → 1.4.2
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 +551 -102
- 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 +19 -2
- package/dist/gateway/progress-card.js +71 -8
- package/dist/gateway/progress-card.js.map +1 -1
- package/dist/index.js +95 -52
- package/dist/index.js.map +1 -1
- package/dist/memory-installer.d.ts +53 -2
- package/dist/memory-installer.js +294 -67
- package/dist/memory-installer.js.map +1 -1
- package/dist/memory.d.ts +22 -2
- package/dist/memory.js +123 -26
- 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 +25 -5
- package/dist/web/memory-api.js.map +1 -1
- package/dist/web/memory-users-api.d.ts +106 -0
- package/dist/web/memory-users-api.js +534 -0
- package/dist/web/memory-users-api.js.map +1 -0
- package/dist/web/server.js +88 -5
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
|
@@ -17,6 +17,8 @@ export interface InstallState {
|
|
|
17
17
|
steps: InstallStep[];
|
|
18
18
|
}
|
|
19
19
|
export declare function getInstallState(): InstallState;
|
|
20
|
+
/** 受管进程最近输出尾部(失败文案「最近输出:…」的数据源;exported 便于单测验证截断语义) */
|
|
21
|
+
export declare function managedRecentOutput(maxLines?: number): string;
|
|
20
22
|
/** 停受管实例(页面 stop / LCB shutdown / 重装前):幂等,未运行为 no-op */
|
|
21
23
|
export declare function stopManagedServer(): void;
|
|
22
24
|
/** LCB 优雅关闭钩子注册点(index.ts shutdown 调用) */
|
|
@@ -30,7 +32,10 @@ export declare function resolveServerBin(uv: string): Promise<string | null>;
|
|
|
30
32
|
/** LCB 记忆管理域(嵌入式数据全收敛于此:卸载即整目录删) */
|
|
31
33
|
export declare const MEMORY_MANAGED_DIR: string;
|
|
32
34
|
export declare const MANAGED_OV_CONF: string;
|
|
33
|
-
/** 渲染 ov.conf(纯函数 exported 便于单测)。rootKey 已有则复用(重装不换 key
|
|
35
|
+
/** 渲染 ov.conf(纯函数 exported 便于单测)。rootKey 已有则复用(重装不换 key,否则插件侧缓存/用户侧配置全部失效)。
|
|
36
|
+
* P2 双端点拆分:embedding.dense 与 vlm 各自独立——向量块显式配置(embeddingApiBase/embeddingApiKey)
|
|
37
|
+
* 优先,未设置回退主块(apiBase/apiKey),既有单份配置零迁移即可用;跨厂商组合(vlm=glm +
|
|
38
|
+
* embedding=openai 系)填向量块两字段即可。 */
|
|
34
39
|
export declare function renderOvConf(model: MemoryModelConfig, opts: {
|
|
35
40
|
workspaceDir: string;
|
|
36
41
|
port: number;
|
|
@@ -76,11 +81,19 @@ export declare function classifyClientConf(paths: {
|
|
|
76
81
|
* net listen 成功 ≠ 端口空闲——已有 OpenViking 服务在监听时 listen 可能误报空闲;探活有
|
|
77
82
|
* 响应 = 确定被占)。候选端口同时两者皆过才采用。 */
|
|
78
83
|
export declare function findFreePort(startPort?: number, tries?: number): Promise<number>;
|
|
84
|
+
/** launchManagedAndWait 的结果:ready=就绪;crashed=进程已放弃(崩溃超限/被停);二者皆否 = 慢启动 */
|
|
85
|
+
export interface LaunchOutcome {
|
|
86
|
+
ready: boolean;
|
|
87
|
+
crashed: boolean;
|
|
88
|
+
detail: string;
|
|
89
|
+
port: number;
|
|
90
|
+
}
|
|
79
91
|
/**
|
|
80
92
|
* 启动流水线(kind='start',后台执行 + 与安装同款 stepper 轮询展示):LCB 已接管 OpenViking,
|
|
81
93
|
* 「启动」就应当是接管的启动——不只拉进程,还要把记忆链路整段拉就绪:
|
|
82
94
|
* ① 检查服务与配置(bin / ov.conf 定位,缺失即 failed + 指引改走「检查修复 / 一键安装」)
|
|
83
|
-
* ② 拉起并探活(受管拉起 +
|
|
95
|
+
* ② 拉起并探活(受管拉起 + P0 预算 120s;预算耗尽且进程存活 → 转后台续等(UI 显示
|
|
96
|
+
* 「启动中(已等 N 秒)」),就绪自动续跑 ③;已在运行则 skipped,幂等重跑)
|
|
84
97
|
* ③ 接入记忆插件(CLI 市场渠道真实安装:已登记启用 → skipped;缺失 → marketplace add/install/enable
|
|
85
98
|
* 三步幂等安装,插件页可见可管理;CLI 失败回退 zip 下载自管副本)
|
|
86
99
|
* 完成后无须再点「检查修复」——启动结束即完整接管态。任何一步失败即停(同安装流水线语义)。
|
|
@@ -88,6 +101,38 @@ export declare function findFreePort(startPort?: number, tries?: number): Promis
|
|
|
88
101
|
export declare function runStartPipeline(opts: {
|
|
89
102
|
configPath: string;
|
|
90
103
|
}): Promise<InstallState>;
|
|
104
|
+
/**
|
|
105
|
+
* 自举结果(控制台分步提示 + 配置页状态区展示共用)。ready=false 一律**不阻断 LCB**:
|
|
106
|
+
* 记忆是增强能力,桥接器本体必须在任何记忆故障下照常服务(与「记忆不可用不阻断机器人」同基调)。
|
|
107
|
+
* hint 是给用户的下一步指引——自举失败时页面/控制台直接展示「去哪手动起」,不必自己猜。
|
|
108
|
+
*/
|
|
109
|
+
export interface MemoryAutoStartReport {
|
|
110
|
+
at: string;
|
|
111
|
+
trigger: 'lcb-start';
|
|
112
|
+
ready: boolean;
|
|
113
|
+
/** 本次是否真的拉起了进程(false=已在跑/远程模式/未安装/失败) */
|
|
114
|
+
started: boolean;
|
|
115
|
+
detail: string;
|
|
116
|
+
hint?: string;
|
|
117
|
+
}
|
|
118
|
+
/** 最近一次启动自举结果(配置页状态区展示源;从未自举过则 undefined) */
|
|
119
|
+
export declare function getMemoryAutoStartReport(): MemoryAutoStartReport | undefined;
|
|
120
|
+
/**
|
|
121
|
+
* 启动自举:LCB 启动时确保受管 OpenViking 服务在跑(embedded + 本机回环才代管)。
|
|
122
|
+
* 设计约束(三条,缺一不可):
|
|
123
|
+
* ① **绝不阻断启动**:任何失败/超时都只产出 report(ready=false + hint),由调用方打印提示;
|
|
124
|
+
* ② **有界等待**:前台最多等 foregroundMs(默认 60s),超时但进程存活 → 转后台续等
|
|
125
|
+
* (waitSlowStart),既不空等也不杀进程;崩溃 → 立刻失败并附最近输出;
|
|
126
|
+
* ③ **幂等**:已在跑(含外部自管服务占用同一端口且健康)直接 skipped,重启 LCB 无副作用。
|
|
127
|
+
* 不碰插件接入(stepPlugin 属安装/启动流水线语义):插件缺失由配置页「检查修复」补齐,
|
|
128
|
+
* 启动自举只负责「服务在不在」这一件事,避免启动路径被 npm/marketplace 安装拖住。
|
|
129
|
+
*/
|
|
130
|
+
export declare function ensureMemoryServiceRunning(opts: {
|
|
131
|
+
endpoint: string;
|
|
132
|
+
mode?: string;
|
|
133
|
+
trigger?: 'lcb-start';
|
|
134
|
+
foregroundMs?: number;
|
|
135
|
+
}): Promise<MemoryAutoStartReport>;
|
|
91
136
|
/** 记忆插件定位结果:path 恒为真实可加载目录(.claude-plugin/plugin.json 在)。
|
|
92
137
|
* where 标注渠道——marketplace=CLI 安装且 enabledPlugins 启用(SDK 自动发现,无须显式引用);
|
|
93
138
|
* explicit=config.yaml apps[].plugins 显式引用(运行时实际加载源);managed=LCB 自管副本
|
|
@@ -165,10 +210,16 @@ export interface InstallOptions {
|
|
|
165
210
|
model?: MemoryModelConfig;
|
|
166
211
|
/** 服务 pin 版本(memory.version ?? DEFAULT_OPENVIKING_PIN) */
|
|
167
212
|
pin?: string;
|
|
213
|
+
/** P1 保存即生效(「立即生效」按钮):受管实例探活通过也强制重启——模型配置保存后
|
|
214
|
+
* ov.conf 已重渲染,但运行中的旧进程不会热加载,必须重启才能应用新模型 */
|
|
215
|
+
forceRestart?: boolean;
|
|
168
216
|
}
|
|
169
217
|
/**
|
|
170
218
|
* 一键安装流水线(幂等锁:running 中重复调用直接返回当前状态)。
|
|
171
219
|
* 任何一步失败即停(状态留 failed + detail),页面可整链重跑——已就绪步骤自动 skipped。
|
|
220
|
+
* P0 慢启动语义:拉起步骤预算耗尽但进程存活时,流水线保持 running、转后台续等(UI 实时显示
|
|
221
|
+
* 「启动中(已等 N 秒)」),就绪后自动续跑 ⑤⑥ 并收尾——调用方(HTTP 层)此刻即返回,
|
|
222
|
+
* 前端经 /api/memory/install/status 轮询看到全过程。
|
|
172
223
|
*/
|
|
173
224
|
export declare function runInstall(opts: InstallOptions): Promise<InstallState>;
|
|
174
225
|
export {};
|
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, memoryEndpoint } from './memory.js';
|
|
20
|
+
import { probeMemoryHealth, MEMORY_PLUGIN_NAME, memoryAccount, 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: {
|
|
@@ -310,7 +327,8 @@ function isLcbClientConfShape(v) {
|
|
|
310
327
|
* 值:保证直连场景 identity 非空 → 插件判 trusted header 身份(dev 模式服务可用);留空会被
|
|
311
328
|
* 判成 api_key 模式而 401 */
|
|
312
329
|
export function renderOvcliConf(memory) {
|
|
313
|
-
|
|
330
|
+
const account = memoryAccount(memory);
|
|
331
|
+
return { url: memoryEndpoint(memory), account, user: account };
|
|
314
332
|
}
|
|
315
333
|
/** 写/刷新客户端配置(幂等可重跑)。决策表:
|
|
316
334
|
* ① EXTERNAL_OV_CONF 存在 → 'skipped-external'(~/.openviking 域归用户自管,整体不碰;
|
|
@@ -401,40 +419,102 @@ export async function findFreePort(startPort = 1933, tries = 20) {
|
|
|
401
419
|
throw new Error(`${startPort}-${startPort + tries - 1} 端口段均被占用`);
|
|
402
420
|
}
|
|
403
421
|
/** 拉起受管实例并探活等待就绪(安装流水线第 4 步与「启动服务」按钮共用)。
|
|
404
|
-
*
|
|
405
|
-
|
|
422
|
+
* P0 启动可靠性(记忆系统最终方案 v1.1 §五):
|
|
423
|
+
* - 探活预算 30 → 120 轮(首装冷启动最坏路径:OS 缓存全冷 + Defender 实时扫描 + 重依赖加载,
|
|
424
|
+
* 常超 30s——旧预算 + 超时即杀是「第一次必败」竞态的根因);
|
|
425
|
+
* - 超时不杀进程:预算耗尽且进程仍活 → 返回 { ready:false, crashed:false },调用方转后台续等
|
|
426
|
+
* (waitSlowStart),UI 显示「启动中(已等 N 秒)」而非红叉,就绪后自动续跑后续步骤;
|
|
427
|
+
* - 进程退出(崩溃 3 次放弃 / 被停止)→ { ready:false, crashed:true },文案带最近真实输出。 */
|
|
428
|
+
const PROBE_BUDGET_ROUNDS = 120;
|
|
429
|
+
/** 慢启动后台续等窗口(预算耗尽且进程存活时;10 分钟仍未见就绪才放弃并停止进程,防僵尸占端口) */
|
|
430
|
+
const SLOW_START_MAX_MS = 600_000;
|
|
431
|
+
async function launchManagedAndWait(bin, confPath, port, external = false, opts = {}) {
|
|
432
|
+
const budget = opts.budgetRounds ?? PROBE_BUDGET_ROUNDS;
|
|
406
433
|
spawnManaged(bin, confPath, port);
|
|
407
|
-
for (let i =
|
|
434
|
+
for (let i = 1; i <= budget; i++) {
|
|
408
435
|
await new Promise((r) => setTimeout(r, 1000));
|
|
409
436
|
const h = await probeMemoryHealth(`http://127.0.0.1:${port}`, 1500);
|
|
410
437
|
if (h.healthy) {
|
|
411
|
-
return {
|
|
438
|
+
return {
|
|
439
|
+
ready: true, crashed: false, port,
|
|
440
|
+
detail: `服务已就绪:http://127.0.0.1:${port}${external ? '(外部配置 ~/.openviking/ov.conf,LCB 仅代为拉起、不改写)' : ''}${i > 15 ? `(冷启动 ${i} 秒)` : ''}`,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
if (!managed) {
|
|
444
|
+
// 崩溃自动重启 3 次放弃 / 被主动停止:managed 已清。文案带最近输出(stdout+stderr 合流),
|
|
445
|
+
// 不再误导为「模型配置问题」——真实原因看输出(方案 §五-P0)
|
|
446
|
+
return { ready: false, crashed: true, port, detail: `服务进程已退出(等待 ${i} 秒)。最近输出:\n${managedRecentOutput() || '(无输出)'}` };
|
|
412
447
|
}
|
|
448
|
+
}
|
|
449
|
+
if (managed) {
|
|
450
|
+
// 预算耗尽但进程存活:绝不杀进程(调用方转后台续等)
|
|
451
|
+
return { ready: false, crashed: false, port, detail: `探活预算(${budget} 秒)内未见就绪,服务进程仍在运行,转后台继续等待` };
|
|
452
|
+
}
|
|
453
|
+
return { ready: false, crashed: true, port, detail: `服务进程在等待期间退出。最近输出:\n${managedRecentOutput() || '(无输出)'}` };
|
|
454
|
+
}
|
|
455
|
+
/** P0 慢启动后台续等:预算耗尽但进程存活时,后台每 2s 探活;onTick 上报累计已等秒数(UI 实时
|
|
456
|
+
* 「启动中(已等 N 秒)」)。就绪 → ok;进程放弃(崩溃 3 次/被停止)或窗口耗尽 → !ok——窗口
|
|
457
|
+
* 耗尽时停止未就绪进程(防僵尸占端口),文案如实说明并附最近输出。 */
|
|
458
|
+
async function waitSlowStart(port, onTick) {
|
|
459
|
+
const begin = Date.now();
|
|
460
|
+
while (Date.now() - begin < SLOW_START_MAX_MS) {
|
|
461
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
462
|
+
const elapsed = PROBE_BUDGET_ROUNDS + Math.round((Date.now() - begin) / 1000);
|
|
463
|
+
const h = await probeMemoryHealth(`http://127.0.0.1:${port}`, 1500);
|
|
464
|
+
if (h.healthy)
|
|
465
|
+
return { ok: true, detail: `服务已就绪(累计等待 ${elapsed} 秒):http://127.0.0.1:${port}` };
|
|
413
466
|
if (!managed)
|
|
414
|
-
|
|
467
|
+
return { ok: false, detail: `服务进程已退出(累计等待 ${elapsed} 秒)。最近输出:\n${managedRecentOutput() || '(无输出)'}` };
|
|
468
|
+
onTick(elapsed);
|
|
415
469
|
}
|
|
416
|
-
const
|
|
470
|
+
const detail = `服务启动超时(已等 ${PROBE_BUDGET_ROUNDS + Math.round(SLOW_START_MAX_MS / 1000)} 秒仍未见就绪),已停止该进程。最近输出:\n${managedRecentOutput() || '(无输出)'}`;
|
|
417
471
|
stopManagedServer();
|
|
418
|
-
|
|
472
|
+
return { ok: false, detail };
|
|
473
|
+
}
|
|
474
|
+
/** 流水线统一收尾:err 时把 running 步骤置 failed,无论成败都关闭 running 态(后台续跑路径共用) */
|
|
475
|
+
function finalizeInstallState(steps, err) {
|
|
476
|
+
const failed = steps.find((x) => x.status === 'running');
|
|
477
|
+
if (err && failed) {
|
|
478
|
+
failed.status = 'failed';
|
|
479
|
+
failed.detail = err instanceof Error ? err.message : String(err);
|
|
480
|
+
}
|
|
481
|
+
installState.running = false;
|
|
482
|
+
installState.finishedAt = new Date().toISOString();
|
|
419
483
|
}
|
|
420
|
-
async function stepStart(bin, confPath) {
|
|
484
|
+
async function stepStart(bin, confPath, opts = {}) {
|
|
421
485
|
const port = readConfPort(confPath);
|
|
422
|
-
//
|
|
486
|
+
// 已有受管实例:探活通即跳过(幂等重跑);forceRestart(P1 保存即生效)则重启以应用新配置
|
|
423
487
|
if (managed && !managed.stopped) {
|
|
424
488
|
const h = await probeMemoryHealth(`http://127.0.0.1:${managed.port}`, 2000);
|
|
425
|
-
if (h.healthy)
|
|
426
|
-
return { skipped: true, detail: `受管实例已在运行(127.0.0.1:${managed.port})`, port: managed.port };
|
|
489
|
+
if (h.healthy && !opts.forceRestart) {
|
|
490
|
+
return { skipped: true, ready: true, crashed: false, detail: `受管实例已在运行(127.0.0.1:${managed.port})`, port: managed.port };
|
|
491
|
+
}
|
|
427
492
|
stopManagedServer();
|
|
493
|
+
await new Promise((r) => setTimeout(r, 1000)); // 端口释放窗口(TIME_WAIT)
|
|
428
494
|
}
|
|
429
495
|
const r = await launchManagedAndWait(bin, confPath, port);
|
|
430
|
-
return {
|
|
496
|
+
return { ...r, skipped: false };
|
|
431
497
|
}
|
|
432
498
|
// ============ 单独启动(「启动服务」按钮 → LCB 接管式启动流水线) ============
|
|
499
|
+
/** 启动流水线尾部(③插件接入 + 回写):正常路径与慢启动后台续跑共用 */
|
|
500
|
+
async function finishStartTail(opts, port, external, set) {
|
|
501
|
+
set('plugin', 'running');
|
|
502
|
+
const pl = await stepPlugin({
|
|
503
|
+
endpoint: `http://127.0.0.1:${port}`,
|
|
504
|
+
account: await readMemoryAccount(opts.configPath),
|
|
505
|
+
});
|
|
506
|
+
set('plugin', pl.skipped ? 'skipped' : 'done', pl.detail);
|
|
507
|
+
if (pl.writeback) {
|
|
508
|
+
// 插件新装/调和才回写:受管场景顺带对齐 endpoint/mode;外部场景不动用户配置(只补插件引用)
|
|
509
|
+
writeBackConfig(opts.configPath, external ? {} : { endpoint: `http://127.0.0.1:${port}`, mode: 'embedded' }, pl.writeback.pluginPath, pl.writeback.opts);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
433
512
|
/**
|
|
434
513
|
* 启动流水线(kind='start',后台执行 + 与安装同款 stepper 轮询展示):LCB 已接管 OpenViking,
|
|
435
514
|
* 「启动」就应当是接管的启动——不只拉进程,还要把记忆链路整段拉就绪:
|
|
436
515
|
* ① 检查服务与配置(bin / ov.conf 定位,缺失即 failed + 指引改走「检查修复 / 一键安装」)
|
|
437
|
-
* ② 拉起并探活(受管拉起 +
|
|
516
|
+
* ② 拉起并探活(受管拉起 + P0 预算 120s;预算耗尽且进程存活 → 转后台续等(UI 显示
|
|
517
|
+
* 「启动中(已等 N 秒)」),就绪自动续跑 ③;已在运行则 skipped,幂等重跑)
|
|
438
518
|
* ③ 接入记忆插件(CLI 市场渠道真实安装:已登记启用 → skipped;缺失 → marketplace add/install/enable
|
|
439
519
|
* 三步幂等安装,插件页可见可管理;CLI 失败回退 zip 下载自管副本)
|
|
440
520
|
* 完成后无须再点「检查修复」——启动结束即完整接管态。任何一步失败即停(同安装流水线语义)。
|
|
@@ -479,42 +559,148 @@ export async function runStartPipeline(opts) {
|
|
|
479
559
|
}
|
|
480
560
|
if (needLaunch) {
|
|
481
561
|
const r = await launchManagedAndWait(bin, confPath, readConfPort(confPath), external);
|
|
562
|
+
if (!r.ready) {
|
|
563
|
+
if (r.crashed) {
|
|
564
|
+
set('launch', 'failed', r.detail);
|
|
565
|
+
throw new Error(r.detail);
|
|
566
|
+
}
|
|
567
|
+
// P0 慢启动:进程存活 → 不杀不红叉,保持流水线 running 转后台续等,就绪自动续跑 ③
|
|
568
|
+
set('launch', 'running', `启动中(已等 ${PROBE_BUDGET_ROUNDS} 秒)…`);
|
|
569
|
+
void (async () => {
|
|
570
|
+
const slow = await waitSlowStart(r.port, (s) => set('launch', 'running', `启动中(已等 ${s} 秒)…`));
|
|
571
|
+
if (!slow.ok) {
|
|
572
|
+
set('launch', 'failed', slow.detail);
|
|
573
|
+
finalizeInstallState(steps);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
set('launch', 'done', slow.detail);
|
|
577
|
+
try {
|
|
578
|
+
await finishStartTail(opts, r.port, external, set);
|
|
579
|
+
finalizeInstallState(steps);
|
|
580
|
+
}
|
|
581
|
+
catch (e) {
|
|
582
|
+
finalizeInstallState(steps, e);
|
|
583
|
+
}
|
|
584
|
+
})();
|
|
585
|
+
return installState; // running 保持 true:前端持续轮询看实时秒数
|
|
586
|
+
}
|
|
482
587
|
port = r.port;
|
|
483
588
|
set('launch', 'done', r.detail);
|
|
484
589
|
}
|
|
485
590
|
// ③ 插件接入(启动完整性的另一半:插件缺失时记忆照样不工作——补齐并回写显式引用;
|
|
486
591
|
// client 传 endpoint/account:顺带落客户端接入配置 ovcli.conf,外部模式由 ensure 自守卫跳过)
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
endpoint: `http://127.0.0.1:${port ?? 1933}`,
|
|
490
|
-
account: await memoryAccount(opts.configPath),
|
|
491
|
-
});
|
|
492
|
-
set('plugin', pl.skipped ? 'skipped' : 'done', pl.detail);
|
|
493
|
-
if (pl.writeback) {
|
|
494
|
-
// 插件新装/调和才回写:受管场景顺带对齐 endpoint/mode;外部场景不动用户配置(只补插件引用)
|
|
495
|
-
writeBackConfig(opts.configPath, external ? {} : { endpoint: `http://127.0.0.1:${port ?? 1933}`, mode: 'embedded' }, pl.writeback.pluginPath, pl.writeback.opts);
|
|
496
|
-
}
|
|
497
|
-
installState.running = false;
|
|
498
|
-
installState.finishedAt = new Date().toISOString();
|
|
592
|
+
await finishStartTail(opts, port ?? 1933, external, set);
|
|
593
|
+
finalizeInstallState(steps);
|
|
499
594
|
return installState;
|
|
500
595
|
}
|
|
501
596
|
catch (e) {
|
|
502
|
-
|
|
503
|
-
installState.finishedAt = new Date().toISOString();
|
|
504
|
-
const failed = steps.find((x) => x.status === 'running');
|
|
505
|
-
if (failed) {
|
|
506
|
-
failed.status = 'failed';
|
|
507
|
-
failed.detail = e instanceof Error ? e.message : String(e);
|
|
508
|
-
}
|
|
597
|
+
finalizeInstallState(steps, e);
|
|
509
598
|
return installState;
|
|
510
599
|
}
|
|
511
600
|
}
|
|
601
|
+
let autoStartReport;
|
|
602
|
+
/** 最近一次启动自举结果(配置页状态区展示源;从未自举过则 undefined) */
|
|
603
|
+
export function getMemoryAutoStartReport() {
|
|
604
|
+
return autoStartReport;
|
|
605
|
+
}
|
|
606
|
+
/** endpoint 是否指向本机回环(远程服务 LCB 不代管拉起) */
|
|
607
|
+
function isLoopbackEndpoint(endpoint) {
|
|
608
|
+
try {
|
|
609
|
+
const h = new URL(endpoint).hostname;
|
|
610
|
+
return h === '127.0.0.1' || h === 'localhost' || h === '::1' || h === '0.0.0.0';
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* 启动自举:LCB 启动时确保受管 OpenViking 服务在跑(embedded + 本机回环才代管)。
|
|
618
|
+
* 设计约束(三条,缺一不可):
|
|
619
|
+
* ① **绝不阻断启动**:任何失败/超时都只产出 report(ready=false + hint),由调用方打印提示;
|
|
620
|
+
* ② **有界等待**:前台最多等 foregroundMs(默认 60s),超时但进程存活 → 转后台续等
|
|
621
|
+
* (waitSlowStart),既不空等也不杀进程;崩溃 → 立刻失败并附最近输出;
|
|
622
|
+
* ③ **幂等**:已在跑(含外部自管服务占用同一端口且健康)直接 skipped,重启 LCB 无副作用。
|
|
623
|
+
* 不碰插件接入(stepPlugin 属安装/启动流水线语义):插件缺失由配置页「检查修复」补齐,
|
|
624
|
+
* 启动自举只负责「服务在不在」这一件事,避免启动路径被 npm/marketplace 安装拖住。
|
|
625
|
+
*/
|
|
626
|
+
export async function ensureMemoryServiceRunning(opts) {
|
|
627
|
+
const trigger = opts.trigger ?? 'lcb-start';
|
|
628
|
+
const foregroundMs = opts.foregroundMs ?? 60_000;
|
|
629
|
+
const finish = (r) => {
|
|
630
|
+
autoStartReport = { at: new Date().toISOString(), trigger, ...r };
|
|
631
|
+
return autoStartReport;
|
|
632
|
+
};
|
|
633
|
+
// 远程模式:只探活不代管(拉起别人的服务是越权)
|
|
634
|
+
if ((opts.mode ?? 'embedded') !== 'embedded' || !isLoopbackEndpoint(opts.endpoint)) {
|
|
635
|
+
const h = await probeMemoryHealth(opts.endpoint, 2000);
|
|
636
|
+
return finish({
|
|
637
|
+
ready: h.healthy,
|
|
638
|
+
started: false,
|
|
639
|
+
detail: h.healthy
|
|
640
|
+
? `远程记忆服务正常:${opts.endpoint}`
|
|
641
|
+
: `远程记忆服务不可达:${h.detail ?? '未知原因'}(LCB 不代管拉起外部服务)`,
|
|
642
|
+
...(h.healthy ? {} : { hint: '请自行确认远程记忆服务已启动;本机地址与端口见 config.yaml 的 memory.endpoint' }),
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
// 已健康(含用户手工起的外部实例)→ 幂等跳过
|
|
646
|
+
const before = await probeMemoryHealth(opts.endpoint, 2000);
|
|
647
|
+
if (before.healthy) {
|
|
648
|
+
return finish({ ready: true, started: false, detail: `服务已在运行:${opts.endpoint}${before.authMode ? `(${before.authMode} 模式)` : ''}` });
|
|
649
|
+
}
|
|
650
|
+
// 定位二进制与配置(与启动流水线 ① 同款判定,缺任一即给「检查修复」指引)
|
|
651
|
+
const uv = await detectUv();
|
|
652
|
+
const bin = uv ? await resolveServerBin(uv) : null;
|
|
653
|
+
if (!bin) {
|
|
654
|
+
return finish({
|
|
655
|
+
ready: false, started: false,
|
|
656
|
+
detail: `服务未运行,且未检测到 OpenViking 运行时(openviking-server):${before.detail ?? '端口无监听'}`,
|
|
657
|
+
hint: '请到配置页「记忆增强 → 检查修复」补装运行时与服务',
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
const confPath = existsSync(MANAGED_OV_CONF) ? MANAGED_OV_CONF
|
|
661
|
+
: existsSync(EXTERNAL_OV_CONF) ? EXTERNAL_OV_CONF : undefined;
|
|
662
|
+
if (!confPath) {
|
|
663
|
+
return finish({
|
|
664
|
+
ready: false, started: false,
|
|
665
|
+
detail: `服务未运行,且未找到服务配置(ov.conf):${before.detail ?? '端口无监听'}`,
|
|
666
|
+
hint: '请到配置页「记忆增强」填好模型三件套并保存(生成 ov.conf),再点「检查修复」',
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
const external = confPath === EXTERNAL_OV_CONF;
|
|
670
|
+
const port = readConfPort(confPath);
|
|
671
|
+
// 受管实例仍在但探不通(假活)→ 先回收再拉起,避免端口占用导致新进程秒退
|
|
672
|
+
if (managed && !managed.stopped) {
|
|
673
|
+
stopManagedServer();
|
|
674
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
675
|
+
}
|
|
676
|
+
const r = await launchManagedAndWait(bin, confPath, port, external, { budgetRounds: Math.max(5, Math.ceil(foregroundMs / 1000)) });
|
|
677
|
+
if (r.ready) {
|
|
678
|
+
return finish({
|
|
679
|
+
ready: true, started: true,
|
|
680
|
+
detail: `已拉起受管记忆服务:http://127.0.0.1:${port}${external ? '(外部 ov.conf,LCB 仅代为拉起、不改写)' : ''}`,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
if (r.crashed) {
|
|
684
|
+
return finish({
|
|
685
|
+
ready: false, started: true,
|
|
686
|
+
detail: `自动拉起失败(服务进程已退出):${r.detail}`,
|
|
687
|
+
hint: '请到配置页「记忆增强」检查模型三件套与 ov.conf,或点「检查修复」查看完整安装流水线',
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
// 慢启动:进程活着只是还没就绪 → 后台续等,不红叉、不杀进程;后续签发由调用方重试兜上
|
|
691
|
+
void waitSlowStart(r.port, () => { });
|
|
692
|
+
return finish({
|
|
693
|
+
ready: false, started: true,
|
|
694
|
+
detail: `记忆服务正在启动(冷启动超过 ${Math.round(foregroundMs / 1000)} 秒,进程仍在运行,已转入后台等待)`,
|
|
695
|
+
hint: '通常 1-3 分钟内就绪;若长时间未起,请到配置页「记忆增强」查看受管服务输出',
|
|
696
|
+
});
|
|
697
|
+
}
|
|
512
698
|
function spawnManaged(bin, confPath, port) {
|
|
513
699
|
const proc = spawn(bin, ['--config', confPath, '--host', '127.0.0.1', '--port', String(port)], {
|
|
514
700
|
windowsHide: true,
|
|
515
701
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
516
702
|
});
|
|
517
|
-
managed = { proc, port, restarts: 0, stopped: false, confPath, startedAt: new Date().toISOString() };
|
|
703
|
+
managed = { proc, port, restarts: 0, stopped: false, confPath, startedAt: new Date().toISOString(), output: [] };
|
|
518
704
|
proc.on('exit', () => {
|
|
519
705
|
if (!managed || managed.proc !== proc)
|
|
520
706
|
return;
|
|
@@ -542,10 +728,25 @@ function spawnManaged(bin, confPath, port) {
|
|
|
542
728
|
managed = undefined;
|
|
543
729
|
}
|
|
544
730
|
});
|
|
731
|
+
// P0:stdout 原先只 pipe 不消费——缓冲写满会阻塞子进程且信息全丢;现转发控制台并入环形缓冲
|
|
732
|
+
//(stderr 同款),失败文案「最近输出:…」即来源于此
|
|
733
|
+
proc.stdout?.on('data', (d) => {
|
|
734
|
+
for (const raw of String(d).split(/\r?\n/)) {
|
|
735
|
+
const line = raw.trim();
|
|
736
|
+
if (!line)
|
|
737
|
+
continue;
|
|
738
|
+
pushManagedOutput(line);
|
|
739
|
+
console.log('[memory-server]', line.slice(0, 300));
|
|
740
|
+
}
|
|
741
|
+
});
|
|
545
742
|
proc.stderr?.on('data', (d) => {
|
|
546
|
-
const
|
|
547
|
-
|
|
743
|
+
for (const raw of String(d).split(/\r?\n/)) {
|
|
744
|
+
const line = raw.trim();
|
|
745
|
+
if (!line)
|
|
746
|
+
continue;
|
|
747
|
+
pushManagedOutput(line);
|
|
548
748
|
console.warn('[memory-server]', line.slice(0, 300));
|
|
749
|
+
}
|
|
549
750
|
});
|
|
550
751
|
}
|
|
551
752
|
// ============ 步骤 5:插件 ============
|
|
@@ -920,9 +1121,9 @@ export function writeBackConfig(configPath, wb, pluginPath, opts = {}) {
|
|
|
920
1121
|
writeAtomic(configPath, doc.toString({ lineWidth: 0 }));
|
|
921
1122
|
}
|
|
922
1123
|
// ============ 主流水线 ============
|
|
923
|
-
/** 读 config.yaml 的 memory.account(懒加载;损坏配置降级 undefined →
|
|
1124
|
+
/** 读 config.yaml 的 memory.account(懒加载;损坏配置降级 undefined → 调用方取缺省 default)。
|
|
924
1125
|
* 两条流水线原本不读 config.yaml(只有 configPath),stepPlugin 落 ovcli.conf 时需要它 */
|
|
925
|
-
async function
|
|
1126
|
+
async function readMemoryAccount(configPath) {
|
|
926
1127
|
try {
|
|
927
1128
|
const { loadConfig } = await import('./config.js');
|
|
928
1129
|
return loadConfig(configPath).memory?.account;
|
|
@@ -931,9 +1132,30 @@ async function memoryAccount(configPath) {
|
|
|
931
1132
|
return undefined;
|
|
932
1133
|
}
|
|
933
1134
|
}
|
|
1135
|
+
/** 安装流水线尾部(⑤插件接入 + ⑥回写 config.yaml):正常路径与慢启动后台续跑共用 */
|
|
1136
|
+
async function finishInstallTail(opts, port, firstInstallKey, set) {
|
|
1137
|
+
// ⑤ 插件(client 传 endpoint/account:顺带落客户端接入配置 ovcli.conf,§6.7 第 4 步)
|
|
1138
|
+
set('plugin', 'running');
|
|
1139
|
+
const pl = await stepPlugin({
|
|
1140
|
+
endpoint: `http://127.0.0.1:${port}`,
|
|
1141
|
+
account: await readMemoryAccount(opts.configPath),
|
|
1142
|
+
});
|
|
1143
|
+
set('plugin', pl.skipped ? 'skipped' : 'done', pl.detail);
|
|
1144
|
+
// ⑥ 回写 config.yaml——全链成功才落盘(半成品不污染用户配置):endpoint / mode=embedded /
|
|
1145
|
+
// 首装生成的 root_api_key 回写 memory.api_key(复用已有 key 不写)+ 插件引用回写(CLI 渠道为
|
|
1146
|
+
// 调和模式、zip 兜底为补齐模式,选项由 stepPlugin 携带)
|
|
1147
|
+
writeBackConfig(opts.configPath, {
|
|
1148
|
+
endpoint: `http://127.0.0.1:${port}`,
|
|
1149
|
+
mode: 'embedded',
|
|
1150
|
+
...(firstInstallKey ? { apiKey: firstInstallKey } : {}),
|
|
1151
|
+
}, pl.writeback?.pluginPath, pl.writeback?.opts);
|
|
1152
|
+
}
|
|
934
1153
|
/**
|
|
935
1154
|
* 一键安装流水线(幂等锁:running 中重复调用直接返回当前状态)。
|
|
936
1155
|
* 任何一步失败即停(状态留 failed + detail),页面可整链重跑——已就绪步骤自动 skipped。
|
|
1156
|
+
* P0 慢启动语义:拉起步骤预算耗尽但进程存活时,流水线保持 running、转后台续等(UI 实时显示
|
|
1157
|
+
* 「启动中(已等 N 秒)」),就绪后自动续跑 ⑤⑥ 并收尾——调用方(HTTP 层)此刻即返回,
|
|
1158
|
+
* 前端经 /api/memory/install/status 轮询看到全过程。
|
|
937
1159
|
*/
|
|
938
1160
|
export async function runInstall(opts) {
|
|
939
1161
|
if (installState.running)
|
|
@@ -1000,35 +1222,40 @@ export async function runInstall(opts) {
|
|
|
1000
1222
|
}
|
|
1001
1223
|
// ④ 拉起
|
|
1002
1224
|
set('start', 'running');
|
|
1003
|
-
const st = await stepStart(svc.bin, MANAGED_OV_CONF);
|
|
1225
|
+
const st = await stepStart(svc.bin, MANAGED_OV_CONF, { forceRestart: opts.forceRestart });
|
|
1226
|
+
if (!st.ready) {
|
|
1227
|
+
if (st.crashed) {
|
|
1228
|
+
set('start', 'failed', st.detail);
|
|
1229
|
+
throw new Error(st.detail);
|
|
1230
|
+
}
|
|
1231
|
+
// P0 慢启动:进程存活 → 不杀不红叉,保持流水线 running 转后台续等,就绪自动续跑 ⑤⑥
|
|
1232
|
+
set('start', 'running', `启动中(已等 ${PROBE_BUDGET_ROUNDS} 秒)…`);
|
|
1233
|
+
void (async () => {
|
|
1234
|
+
const slow = await waitSlowStart(st.port, (s) => set('start', 'running', `启动中(已等 ${s} 秒)…`));
|
|
1235
|
+
if (!slow.ok) {
|
|
1236
|
+
set('start', 'failed', slow.detail);
|
|
1237
|
+
finalizeInstallState(steps);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
set('start', 'done', slow.detail);
|
|
1241
|
+
try {
|
|
1242
|
+
await finishInstallTail(opts, st.port, firstInstallKey, set);
|
|
1243
|
+
finalizeInstallState(steps);
|
|
1244
|
+
}
|
|
1245
|
+
catch (e) {
|
|
1246
|
+
finalizeInstallState(steps, e);
|
|
1247
|
+
}
|
|
1248
|
+
})();
|
|
1249
|
+
return installState; // running 保持 true:前端持续轮询看实时秒数
|
|
1250
|
+
}
|
|
1004
1251
|
set('start', st.skipped ? 'skipped' : 'done', st.detail);
|
|
1005
|
-
//
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
endpoint: `http://127.0.0.1:${st.port}`,
|
|
1009
|
-
account: await memoryAccount(opts.configPath),
|
|
1010
|
-
});
|
|
1011
|
-
set('plugin', pl.skipped ? 'skipped' : 'done', pl.detail);
|
|
1012
|
-
// ⑥ 回写 config.yaml——全链成功才落盘(半成品不污染用户配置):endpoint / mode=embedded /
|
|
1013
|
-
// 首装生成的 root_api_key 回写 memory.api_key(复用已有 key 不写)+ 插件引用回写(CLI 渠道为
|
|
1014
|
-
// 调和模式、zip 兜底为补齐模式,选项由 stepPlugin 携带)
|
|
1015
|
-
writeBackConfig(opts.configPath, {
|
|
1016
|
-
endpoint: `http://127.0.0.1:${st.port}`,
|
|
1017
|
-
mode: 'embedded',
|
|
1018
|
-
...(firstInstallKey ? { apiKey: firstInstallKey } : {}),
|
|
1019
|
-
}, pl.writeback?.pluginPath, pl.writeback?.opts);
|
|
1020
|
-
installState.running = false;
|
|
1021
|
-
installState.finishedAt = new Date().toISOString();
|
|
1252
|
+
// ⑤⑥ 插件接入 + 回写(尾部提取:与慢启动后台续跑共用)
|
|
1253
|
+
await finishInstallTail(opts, st.port, firstInstallKey, set);
|
|
1254
|
+
finalizeInstallState(steps);
|
|
1022
1255
|
return installState;
|
|
1023
1256
|
}
|
|
1024
1257
|
catch (e) {
|
|
1025
|
-
|
|
1026
|
-
installState.finishedAt = new Date().toISOString();
|
|
1027
|
-
const failed = steps.find((x) => x.status === 'running');
|
|
1028
|
-
if (failed) {
|
|
1029
|
-
failed.status = 'failed';
|
|
1030
|
-
failed.detail = e instanceof Error ? e.message : String(e);
|
|
1031
|
-
}
|
|
1258
|
+
finalizeInstallState(steps, e);
|
|
1032
1259
|
return installState;
|
|
1033
1260
|
}
|
|
1034
1261
|
}
|