@sanqianx/project-knowledge 5.0.16-rc.8 → 5.0.17-rc.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.
@@ -1,9 +1,17 @@
1
1
  const { DomainError } = require('./contracts');
2
2
  const { execGit } = require('./git-runner');
3
3
 
4
- async function connectEngine(runtime) {
5
- if (runtime.analysisOwner === 'legacy' || runtime.engineConnecting) return;
6
- runtime.engineConnecting = true;
4
+ function connectEngine(runtime) {
5
+ if (runtime.analysisOwner === 'legacy') return Promise.resolve();
6
+ if (runtime.engineConnecting) { runtime.engineReconnectRequested = true; return runtime.engineConnecting; }
7
+ const job = connectEngineOnce(runtime).finally(() => {
8
+ runtime.engineConnecting = null;
9
+ if (runtime.engineReconnectRequested && !runtime.engineConnectionClosed) { runtime.engineReconnectRequested = false; void connectEngine(runtime); }
10
+ });
11
+ runtime.engineConnecting = job;
12
+ return job;
13
+ }
14
+ async function connectEngineOnce(runtime) {
7
15
  let step = 'engine-health';
8
16
  let projectId = '';
9
17
  let httpStatus = 0;
@@ -21,24 +29,36 @@ async function connectEngine(runtime) {
21
29
  httpStatus = auth.status;
22
30
  if (!auth.ok) throw new Error('RUNTIME_AUTH_REQUIRED');
23
31
  bridge.engineCompatible = true;
32
+ if (engine.executionEnabled === false) { bridge.resetRuntimeConfiguration?.(); runtime.projectConnections?.clear(); }
24
33
  step = 'project-wiring';
25
- for (const id of runtime.registryStore.listIds()) {
26
- projectId = id;
34
+ const remote = bridge._listEngineProjects ? await bridge._listEngineProjects() : [];
35
+ runtime.projectConnections ||= new Map();
36
+ const enabled = [], pending = [];
37
+ let cursor = 0;
38
+ const ids = runtime.registryStore.listIds();
39
+ const wire = async () => { while (cursor < ids.length) {
40
+ const id = ids[cursor++];
27
41
  if (!runtime.registryStore.readDisplaySnapshot(id)) continue;
28
42
  let config;
29
43
  try { config = runtime.projectStore.readConfig(id); }
30
- catch (error) { if (!runtime.registryStore.readDisplaySnapshot(id)) continue; throw error; }
44
+ catch { pending.push({ projectId: id, code: 'PROJECT_CONFIG_UNAVAILABLE' }); continue; }
45
+ const fingerprint = JSON.stringify(config);
46
+ if (runtime.projectConnections.get(id) === fingerprint && remote.some(p => p.projectId === id && p.repoPath === config.repoPath && p.knowledgePath === config.knowledgePath)) { enabled.push(id); continue; }
47
+ try {
31
48
  const modules = await bridge.registerProjectLinks({ projectId: id, name: config.displayName, workspacePath: config.repoPath, knowledgePath: config.knowledgePath });
32
- if (!modules.engine.ok || !modules.eventBridge.ok) throw new Error('PROJECT_WIRING_PENDING');
33
- }
49
+ if (!modules.engine.ok || !modules.eventBridge.ok) { pending.push({ projectId: id, code: 'PROJECT_WIRING_PENDING' }); continue; }
50
+ runtime.projectConnections.set(id, fingerprint); enabled.push(id);
51
+ } catch { pending.push({ projectId: id, code: 'PROJECT_WIRING_PENDING' }); }
52
+ } };
53
+ await Promise.all([wire(), wire()]);
34
54
  projectId = '';
35
55
  step = 'runtime-configuration';
36
56
  await bridge.syncEngineConfiguration();
37
57
  step = 'engine-activation';
38
- const activated = await bridge._fetch(`${bridge.engineUrl}/api/control`, { method: 'POST', headers, body: JSON.stringify({ enabled: true }) });
58
+ const activated = await bridge._fetch(`${bridge.engineUrl}/api/control`, { method: 'POST', headers, body: JSON.stringify({ enabled: true, projectIds: enabled }) });
39
59
  httpStatus = activated.status;
40
60
  if (!activated.ok) throw new Error(activated.status === 403 ? 'RUNTIME_AUTH_REQUIRED' : 'ENGINE_ACTIVATION_FAILED');
41
- runtime.ownership = { owner: 'engine', ready: true };
61
+ runtime.ownership = { owner: 'engine', ready: true, ...(pending.length ? { pendingProjects: pending, message: '部分项目接线待恢复,其他已连接项目可正常维护。' } : {}) };
42
62
  } catch (error) {
43
63
  const codes = ['ENGINE_UNAVAILABLE', 'ENGINE_VERSION_MISMATCH', 'PROJECT_WIRING_PENDING', 'RUNTIME_AUTH_REQUIRED', 'ENGINE_ACTIVATION_FAILED'];
44
64
  const code = codes.includes(error.message) ? error.message : 'ENGINE_CONNECTION_FAILED';
@@ -52,7 +72,7 @@ async function connectEngine(runtime) {
52
72
  const previous = runtime.ownership?.error;
53
73
  runtime.ownership = { owner: 'engine', ready: false, message, error: { code, step, httpStatus, ...(projectId ? { projectId } : {}) } };
54
74
  if (previous?.code !== code || previous?.step !== step || (previous?.projectId || '') !== projectId) await runtime.logger.warn('engine.connection_pending', message, { component: 'engine-connection', phase: step, projectId, context: { code, httpStatus } });
55
- } finally { runtime.engineConnecting = false; }
75
+ }
56
76
  }
57
77
 
58
78
  async function forwardCommitToBridge(runtime, event) {
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+ const { WebSocketServer } = require('ws');
3
+ const { TextDecoder } = require('node:util');
4
+
5
+ function attachLiveGateway(server, runtime) {
6
+ const wss = new WebSocketServer({ noServer: true, maxPayload: 8192 });
7
+ function target(route) {
8
+ const url = new URL(route, 'http://local');
9
+ if (url.origin !== 'http://local' || !route.startsWith('/') || route.length > 2048) throw new Error('INVALID_ROUTE');
10
+ if ([...url.searchParams.keys()].some(k => !['after', 'projectId', 'level', 'component', 'event', 'q', 'operationId', 'runId', 'commitSha', 'limit', 'summary', 'jobId', 'since', 'streamCursor'].includes(k))) throw new Error('INVALID_QUERY');
11
+ const bridge = runtime.moduleBridge;
12
+ if (url.pathname === '/api/engine/progress') {
13
+ if (bridge.engineCompatible === false) throw new Error('ENGINE_CONNECTION_PENDING');
14
+ return `${bridge.engineUrl}/api/progress${url.search}`;
15
+ }
16
+ if (/^\/api\/terminal\/sessions\/[a-zA-Z0-9_-]+\/events$/.test(url.pathname)) return bridge._terminalApi(url.pathname) + url.search;
17
+ if (url.pathname === '/api/logs/stream') return `http://${runtime.liveAddress}${route}`;
18
+ throw new Error('INVALID_ROUTE');
19
+ }
20
+ server.on('upgrade', (req, socket, head) => {
21
+ const local = ['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(req.socket.remoteAddress);
22
+ let hostname; try { hostname = new URL(`http://${req.headers.host}`).hostname; } catch { socket.destroy(); return; }
23
+ if (req.url !== '/api/live' || !local || !['127.0.0.1', 'localhost', '[::1]'].includes(hostname) || req.headers.origin !== `http://${req.headers.host}`) { socket.destroy(); return; }
24
+ wss.handleUpgrade(req, socket, head, ws => wss.emit('connection', ws));
25
+ });
26
+ wss.on('connection', ws => {
27
+ const subscriptions = new Map();
28
+ const send = payload => {
29
+ if (ws.readyState !== 1) return;
30
+ if (ws.bufferedAmount > 1024 * 1024) { ws.close(1013, 'slow consumer'); return; }
31
+ ws.send(JSON.stringify(payload));
32
+ };
33
+ const stop = id => { subscriptions.get(id)?.abort(); subscriptions.delete(id); };
34
+ ws.on('message', async bytes => {
35
+ let message, controller;
36
+ try {
37
+ message = JSON.parse(String(bytes));
38
+ if (typeof message?.id !== 'string' || !/^[a-zA-Z0-9_-]{1,80}$/.test(message.id)) throw new Error('INVALID_SUBSCRIPTION');
39
+ if (message.type === 'unsubscribe') { stop(message.id); return; }
40
+ if (message.type !== 'subscribe' || subscriptions.size >= 8) throw new Error('INVALID_SUBSCRIPTION');
41
+ const url = target(message.route); stop(message.id);
42
+ controller = new AbortController(); subscriptions.set(message.id, controller);
43
+ const deadline = setTimeout(() => controller.abort(), 3000);
44
+ let upstream;
45
+ try { upstream = await fetch(url, { signal: controller.signal, headers: message.after ? { 'Last-Event-ID': String(message.after).slice(0, 128) } : {} }); }
46
+ finally { clearTimeout(deadline); }
47
+ if (!upstream.ok || !upstream.headers.get('content-type')?.includes('text/event-stream')) throw new Error('UPSTREAM_UNAVAILABLE');
48
+ send({ id: message.id, type: 'open' });
49
+ const decoder = new TextDecoder(); let buffer = '';
50
+ for await (const chunk of upstream.body) {
51
+ buffer += decoder.decode(chunk, { stream: true });
52
+ if (buffer.length > 2 * 1024 * 1024) throw new Error('EVENT_TOO_LARGE');
53
+ let boundary;
54
+ while ((boundary = /\r?\n\r?\n/.exec(buffer))) {
55
+ const block = buffer.slice(0, boundary.index); buffer = buffer.slice(boundary.index + boundary[0].length);
56
+ let event = 'message', data = [], cursor;
57
+ for (const line of block.split(/\r?\n/)) {
58
+ const colon = line.indexOf(':'), key = colon < 0 ? line : line.slice(0, colon), value = colon < 0 ? '' : line.slice(colon + 1).replace(/^ /, '');
59
+ if (key === 'event') event = value; else if (key === 'data') data.push(value); else if (key === 'id') cursor = value;
60
+ }
61
+ if (data.length && subscriptions.get(message.id) === controller) send({ type: 'event', id: message.id, event, data: data.join('\n'), cursor });
62
+ }
63
+ }
64
+ if (!controller.signal.aborted) throw new Error('UPSTREAM_CLOSED');
65
+ } catch {
66
+ if (controller) { if (subscriptions.get(message.id) === controller) { stop(message.id); send({ type: 'error', id: message.id, code: 'STREAM_UNAVAILABLE' }); } }
67
+ else send({ type: 'error', id: message?.id, code: 'INVALID_SUBSCRIPTION' });
68
+ }
69
+ });
70
+ const heartbeat = setInterval(() => { if (ws.isAlive === false) return ws.terminate(); ws.isAlive = false; ws.ping(); }, 30000);
71
+ ws.on('pong', () => { ws.isAlive = true; });
72
+ ws.on('close', () => { clearInterval(heartbeat); for (const id of subscriptions.keys()) stop(id); });
73
+ });
74
+ return { close() { for (const ws of wss.clients) ws.terminate(); wss.close(); } };
75
+ }
76
+ module.exports = { attachLiveGateway };
@@ -218,7 +218,7 @@ class ModuleBridge {
218
218
 
219
219
  async _listTerminalProjects() {
220
220
  try {
221
- const response = await this._fetch(`${this.terminalUrl}/api/claude-workbench/v1/projects`);
221
+ const response = await this._fetch(`${this.terminalUrl}/api/claude-workbench/v1/projects`, { timeoutMs: 2500 });
222
222
  if (!response.ok) return [];
223
223
  const payload = await response.json();
224
224
  const list = Array.isArray(payload) ? payload : payload.projects;
@@ -228,7 +228,7 @@ class ModuleBridge {
228
228
 
229
229
  async _listVectorHubProjects() {
230
230
  try {
231
- const response = await this._fetch(`${this.vectorHubUrl}/api/projects`);
231
+ const response = await this._fetch(`${this.vectorHubUrl}/api/projects`, { timeoutMs: 2500 });
232
232
  if (!response.ok) return [];
233
233
  const payload = await response.json();
234
234
  return Array.isArray(payload.projects) ? payload.projects : [];
@@ -237,7 +237,7 @@ class ModuleBridge {
237
237
 
238
238
  async _listEventBridgeProjects() {
239
239
  try {
240
- const response = await this._fetch(`${this.eventBridgeUrl}/api/projects`);
240
+ const response = await this._fetch(`${this.eventBridgeUrl}/api/projects`, { timeoutMs: 2500 });
241
241
  if (!response.ok) return [];
242
242
  const payload = await response.json();
243
243
  return Array.isArray(payload.projects) ? payload.projects : [];
@@ -247,7 +247,7 @@ class ModuleBridge {
247
247
  async _listEngineProjects() {
248
248
  if (this.engineCompatible === false) return [];
249
249
  try {
250
- const response = await this._fetch(`${this.engineUrl}/api/projects`);
250
+ const response = await this._fetch(`${this.engineUrl}/api/projects`, { timeoutMs: 2500 });
251
251
  if (!response.ok) return [];
252
252
  const payload = await response.json();
253
253
  return Array.isArray(payload.projects) ? payload.projects : [];
@@ -256,6 +256,7 @@ class ModuleBridge {
256
256
 
257
257
  /** Aggregated projection for the shell sidebar: pkb registry is the spine. */
258
258
  async aggregatedProjects() {
259
+ const healthJob = this.modulesHealth();
259
260
  const [terminalProjects, vectorHubProjects, eventBridgeProjects, engineProjects] = await Promise.all([
260
261
  this._listTerminalProjects(),
261
262
  this._listVectorHubProjects(),
@@ -295,12 +296,10 @@ class ModuleBridge {
295
296
  },
296
297
  };
297
298
  });
299
+ const health = (await healthJob).modules;
298
300
  return {
299
301
  projects,
300
- terminalAvailable: terminalProjects.length > 0 || await this._probe(this.terminalUrl),
301
- vectorHubAvailable: vectorHubProjects.length > 0 || await this._probe(this.vectorHubUrl),
302
- eventBridgeAvailable: eventBridgeProjects.length > 0 || await this._probe(this.eventBridgeUrl),
303
- engineAvailable: engineProjects.length > 0 || await this._probe(this.engineUrl),
302
+ terminalAvailable: health.terminal.up, vectorHubAvailable: health.vectorHub.up, eventBridgeAvailable: health.eventBridge.up, engineAvailable: health.engine.up,
304
303
  };
305
304
  }
306
305
 
@@ -324,14 +323,39 @@ class ModuleBridge {
324
323
  } };
325
324
  }
326
325
 
327
- syncEngineConfiguration() {
328
- if (this.configSyncPromise) return this.configSyncPromise.catch(() => undefined).then(() => this.syncEngineConfiguration());
329
- const pending = this._syncEngineConfiguration().finally(() => { if (this.configSyncPromise === pending) this.configSyncPromise = null; });
326
+ syncEngineConfiguration({ force = false } = {}) {
327
+ const signature = () => { try { return crypto.createHash('sha256').update(JSON.stringify(this.settingsStore?.read() || {})).digest('hex'); } catch { return ''; } };
328
+ if (this.configSyncPromise) {
329
+ if (force || signature() !== this.configSyncSettings) this.configSyncDirty = true;
330
+ return this.configSyncPromise;
331
+ }
332
+ const pending = (async () => {
333
+ let result;
334
+ do { this.configSyncDirty = false; this.configSyncSettings = signature(); result = await this._syncEngineConfiguration(); } while (this.configSyncDirty && !this.stopped);
335
+ return result;
336
+ })().finally(() => { if (this.configSyncPromise === pending) this.configSyncPromise = null; });
330
337
  this.configSyncPromise = pending;
331
338
  return pending;
332
339
  }
333
340
 
341
+ resetRuntimeConfiguration() {
342
+ this.configFingerprints = {};
343
+ this.configGeneration = (this.configGeneration || 0) + 1;
344
+ if (this.configSyncPromise) this.configSyncDirty = true;
345
+ }
346
+
334
347
  async _syncEngineConfiguration() {
348
+ const applyConfiguration = async (key, url, body) => {
349
+ this.configFingerprints ||= {};
350
+ const generation = this.configGeneration;
351
+ const fingerprint = crypto.createHash('sha256').update(JSON.stringify(body)).digest('hex');
352
+ if (this.configFingerprints[key]?.fingerprint === fingerprint) return this.configFingerprints[key].result;
353
+ const response = await this._fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.runtimeToken}` }, body: JSON.stringify(body) });
354
+ const result = { ok: response.ok, data: response.ok ? await response.json().catch(() => ({})) : null };
355
+ if (result.ok && this.configGeneration === generation) this.configFingerprints[key] = { fingerprint, result };
356
+ else delete this.configFingerprints[key];
357
+ return result;
358
+ };
335
359
  const dataDir = this.dataDir || require('./data-dir').getDataDir();
336
360
  let indexPath = null;
337
361
  try { indexPath = new StorageLayout({ dataDir }).getVectorRootPath(this.settingsStore?.read()); }
@@ -344,7 +368,7 @@ class ModuleBridge {
344
368
  if (settings.knowledge.rootPath) {
345
369
  const brain = settings.brain;
346
370
  const root = path.join(settings.knowledge.rootPath, '_shared');
347
- const response = await this._fetch(`${this.engineUrl}/api/snapshot`);
371
+ const response = await this._fetch(`${this.engineUrl}/api/snapshot?summary=1&limit=1`);
348
372
  if (!response.ok) throw new DomainError('ENGINE_UNAVAILABLE', '共享知识接线暂时不可用。');
349
373
  const prior = (await response.json()).sharedScopes || [];
350
374
  // Root settings govern new directories and derived indexes, never move
@@ -355,7 +379,7 @@ class ModuleBridge {
355
379
  return item.knowledgePath || existing?.knowledgePath || path.join(root, item.id);
356
380
  };
357
381
  const scopes = [{ id: brain.id, kind: 'brain', brainId: brain.id, name: brain.name, knowledgePath: binding(brain, 'brain') }, ...brain.domains.map(d => ({ id: d.id, kind: 'domain', brainId: brain.id, domainId: d.id, name: d.name, knowledgePath: binding(d, 'domain') }))];
358
- const applied = await this._fetch(`${this.engineUrl}/api/scopes`, { method: 'POST', headers, body: JSON.stringify({ scopes }) });
382
+ const applied = await applyConfiguration('scopes', `${this.engineUrl}/api/scopes`, { scopes });
359
383
  if (!applied.ok) throw new DomainError('SHARED_SCOPE_APPLY_FAILED', '共享目录接线未完成。');
360
384
  await this.settingsStore.bindSharedKnowledgePaths?.(scopes);
361
385
  status.scopes = true; status.scopeStatus = { status: 'ready' };
@@ -372,7 +396,7 @@ class ModuleBridge {
372
396
  const response = await this._fetch(`${this.terminalUrl}/api/claude-workbench/v1/internal/maintenance-config`, { method: 'POST', headers, body: '{}' });
373
397
  if (response.ok) profile = await response.json();
374
398
  } catch { /* Missing configuration must clear a previously resolved key. */ }
375
- const response = await this._fetch(`${this.engineUrl}/api/runtime-config`, { method: 'POST', headers, body: JSON.stringify({ projectId: 'default', profile }) });
399
+ const response = await applyConfiguration('model', `${this.engineUrl}/api/runtime-config`, { projectId: 'default', profile });
376
400
  status.model = response.ok && !!profile;
377
401
  } catch { /* Status projection only: never log resolved profiles or raw errors. */ }
378
402
  try {
@@ -381,10 +405,10 @@ class ModuleBridge {
381
405
  const resolved = await this._fetch(`${this.terminalUrl}/api/claude-workbench/v1/internal/embedding-config`, { method: 'POST', headers, body: '{}' });
382
406
  if (resolved.ok) settings = await resolved.json();
383
407
  } catch { /* Revocation wins over a previously cached runtime key. */ }
384
- const applied = await this._fetch(`${this.vectorHubUrl}/api/internal/runtime-config`, { method: 'POST', headers, body: JSON.stringify({ settings, ...(indexPath ? { storageRoot: indexPath } : {}) }) });
408
+ const applied = await applyConfiguration('embedding', `${this.vectorHubUrl}/api/internal/runtime-config`, { settings, ...(indexPath ? { storageRoot: indexPath } : {}) });
385
409
  status.embedding = applied.ok && !!settings;
386
410
  if (applied.ok) {
387
- const result = await applied.json();
411
+ const result = applied.data;
388
412
  status.indexStorage.actualPath = result.rootPath || null;
389
413
  status.indexStorage.status = result.rootPath && path.resolve(result.rootPath) === indexPath ? 'ready' : 'pending';
390
414
  }
@@ -593,13 +617,13 @@ class ModuleBridge {
593
617
  */
594
618
  async startSupervised() {
595
619
  if (String(process.env.KB_MODULES_AUTOSTART || '') === '0') return;
596
- for (const spec of this._supervisedSpecs()) {
620
+ await Promise.all(this._supervisedSpecs().map(async spec => {
597
621
  if (await this._probe(spec.url)) {
598
622
  this.logger.info('modules.supervisor', 'Module service already up; not spawning.', { context: { name: spec.name, url: spec.url } });
599
- continue;
623
+ this.onReady?.(spec.name); return;
600
624
  }
601
625
  this._spawnSupervised(spec);
602
- }
626
+ }));
603
627
  }
604
628
 
605
629
  _supervisedSpecs() {
@@ -713,6 +737,14 @@ class ModuleBridge {
713
737
  });
714
738
  this.supervised.push(child);
715
739
  this.logger.info('modules.supervisor', 'Module service spawned.', { context: { name: spec.name, pid: child.pid } });
740
+ let attemptsReady = 0;
741
+ const checkReady = async () => {
742
+ if (this.stopped || child.exitCode !== null || ++attemptsReady > 20) return;
743
+ if (await this._probe(spec.url)) { this.resetRuntimeConfiguration(); this.onReady?.(spec.name); return; }
744
+ const timer = setTimeout(() => { this.restartTimers.delete(timer); checkReady(); }, 500);
745
+ this.restartTimers ||= new Set(); this.restartTimers.add(timer);
746
+ };
747
+ void checkReady();
716
748
  }
717
749
 
718
750
  async stopSupervised() {
@@ -916,8 +916,10 @@ function createRequestHandler(runtime, options = {}) {
916
916
  }
917
917
  if (method === 'POST' && pathname === '/api/modules/configure') {
918
918
  await readJsonBody(req);
919
+ runtime.moduleBridge.resetRuntimeConfiguration();
920
+ runtime.projectConnections?.clear();
919
921
  await connectEngine(runtime);
920
- return send(res, 200, { ok: true, configuration: await runtime.moduleBridge.syncEngineConfiguration() });
922
+ return send(res, 200, { ok: true, configuration: await runtime.moduleBridge.syncEngineConfiguration({ force: true }) });
921
923
  }
922
924
  if (method === 'GET' && pathname === '/api/brain') {
923
925
  const settings = runtime.settingsStore.read();
@@ -986,6 +988,7 @@ function createRequestHandler(runtime, options = {}) {
986
988
  return send(res, 200, { ok: true, settings: { ...settings, ai: publicAiProfilesConfig(settings.ai) }, configuration });
987
989
  }
988
990
 
991
+ if (method === 'GET' && pathname === '/api/projects/projection') return send(res, 200, { ok: true, projects: listProjectViews(runtime.registryStore, runtime.projectStore).map(p => ({ projectId: p.projectId, name: p.config.displayName, workspacePath: p.config.repoPath, knowledgePath: p.config.knowledgePath, brainId: p.config.brainId, domainId: p.config.domainId, modules: Object.fromEntries(['terminal', 'vectorHub', 'eventBridge', 'engine'].map(key => { const module = p.state.modules?.[key] || {}; return [key, { projectId: module.projectId, projectName: module.projectName, status: module.status, registered: module.ok === true && module.status === 'registered' }]; })) })) });
989
992
  if (method === 'GET' && pathname === '/api/projects') return send(res, 200, { ok: true, projects: listProjectViews(runtime.registryStore, runtime.projectStore) });
990
993
  // T06: pure preflight endpoint — never mutates state. The UI calls
991
994
  // this on path pick / language change / profile change to keep the
@@ -1011,6 +1014,7 @@ function createRequestHandler(runtime, options = {}) {
1011
1014
  workspacePath: String(config.repoPath || body.path || ''),
1012
1015
  knowledgePath: String(body.knowledgePath || config.knowledgePath || ''),
1013
1016
  });
1017
+ void connectEngine(runtime);
1014
1018
  return send(res, 201, { ...result, modules: moduleResult, project: projectPublicView(result.projectId, runtime.projectStore) });
1015
1019
  }
1016
1020
  const projectMatch = pathname.match(/^\/api\/projects\/([^/]+)$/);
@@ -1486,6 +1490,8 @@ async function startServer(options = {}) {
1486
1490
  startup = await initializeRuntime(runtime);
1487
1491
  const handler = createRequestHandler(runtime, { ...options, port, host });
1488
1492
  server = http.createServer(handler);
1493
+ runtime.liveGateway = require('./live-gateway').attachLiveGateway(server, runtime);
1494
+ runtime.liveAddress = `${host}:${port}`;
1489
1495
  await new Promise((resolve, reject) => {
1490
1496
  server.once('error', reject);
1491
1497
  server.listen(port, host, resolve);
@@ -1513,7 +1519,8 @@ async function startServer(options = {}) {
1513
1519
  maintenanceTimer.unref?.();
1514
1520
  const configurationTimer = setInterval(() => { connectEngine(runtime).catch(() => {}); }, 30000);
1515
1521
  configurationTimer.unref?.();
1516
- runtime.moduleBridge.syncEngineConfiguration().catch(() => {});
1522
+ let readyTimer;
1523
+ runtime.moduleBridge.onReady = () => { clearTimeout(readyTimer); readyTimer = setTimeout(() => { runtime.projectConnections?.clear(); connectEngine(runtime).catch(() => {}); }, 100); };
1517
1524
  connectEngine(runtime).catch(() => {});
1518
1525
 
1519
1526
  // T3.5 进程编排:拉起包内自带的模块服务并守护(默认开启,KB_MODULES_AUTOSTART=0 关闭)
@@ -1532,6 +1539,9 @@ async function startServer(options = {}) {
1532
1539
  runtime.integrationStatusScan?.close();
1533
1540
  clearInterval(maintenanceTimer);
1534
1541
  clearInterval(configurationTimer);
1542
+ clearTimeout(readyTimer); runtime.moduleBridge.onReady = null;
1543
+ runtime.engineConnectionClosed = true;
1544
+ runtime.liveGateway?.close();
1535
1545
  await runtime.logger.info('server.shutdown_started', 'Server shutdown started.', { phase: 'shutdown', context: { reason } });
1536
1546
  const forceConnections = setTimeout(() => server.closeAllConnections?.(), Number(process.env.KB_SERVER_CLOSE_GRACE_MS || 2000));
1537
1547
  await new Promise(resolve => server.close(resolve));
@@ -53,7 +53,7 @@ function pack(sources) {
53
53
  process.stdout.write(`Packed ${key} ${pkg.version}: ${result.files.length} files\n`);
54
54
  }
55
55
  const manifestPath = path.join(output, 'module-candidates.json'); AtomicFile.writeJsonAtomic(manifestPath, manifest);
56
- const productFiles = Object.fromEntries(['ui/index.html', 'ui/product.js', 'ui/product.css', 'ui/favicon.svg'].map(file => [file, hash(fs.readFileSync(path.join(root, file)))]));
56
+ const productFiles = Object.fromEntries(['ui/index.html', 'ui/product.js', 'ui/live-client.js', 'ui/product.css', 'ui/favicon.svg'].map(file => [file, hash(fs.readFileSync(path.join(root, file)))]));
57
57
  AtomicFile.writeJsonAtomic(path.join(root, 'module-runtime-manifest.json'), { schema: 'project-knowledge/module-content/v1', productFiles, modules: Object.fromEntries(Object.entries(manifest.modules).map(([key, module]) => [key, { name: module.name, installName: module.installName, version: module.version, files: module.files }])) });
58
58
  process.stdout.write(`MODULE_CANDIDATES ${manifestPath}\n`);
59
59
  return manifestPath;
@@ -3,6 +3,7 @@ const fs = require('node:fs');
3
3
  const path = require('node:path');
4
4
  const crypto = require('node:crypto');
5
5
  const { createRequire } = require('node:module');
6
+ const { execFileSync } = require('node:child_process');
6
7
  const { safeRelative } = require('./pack-module-candidates');
7
8
  const AtomicFile = require('../lib/atomic-file');
8
9
  const root = path.resolve(__dirname, '../..');
@@ -18,8 +19,10 @@ for (const module of Object.values(manifest.modules)) {
18
19
  const expected = pkg.dependencies[module.installName];
19
20
  if (installed.name !== module.name || ![installed.version, `npm:${installed.name}@${installed.version}`].includes(expected)) throw new Error('Install the exact registry module version before content synchronization.');
20
21
  module.version = installed.version;
21
- for (const relative of Object.keys(module.files)) { if (!safeRelative(relative)) throw new Error('Unsafe content path'); const target = path.join(directory, relative); if (fs.lstatSync(target).isSymbolicLink()) throw new Error('Unsafe content symlink'); module.files[relative] = digest(target); }
22
+ const npmCli = process.env.npm_execpath || path.join(path.dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js');
23
+ const packed = JSON.parse(execFileSync(process.execPath, [npmCli, 'pack', '--dry-run', '--ignore-scripts', '--json'], { cwd: directory, windowsHide: true, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }))[0];
24
+ module.files = Object.fromEntries(packed.files.map(entry => { const relative = entry.path; if (!safeRelative(relative)) throw new Error('Unsafe content path'); const target = path.join(directory, relative); if (fs.lstatSync(target).isSymbolicLink()) throw new Error('Unsafe content symlink'); return [relative, digest(target)]; }));
22
25
  }
23
- for (const relative of ['ui/index.html', 'ui/product.js', 'ui/product.css', 'ui/favicon.svg', 'plugins/project-knowledge/hooks/claude.json', 'plugins/project-knowledge/hooks/codex.json', 'plugins/project-knowledge/scripts/context-hook.cjs', 'plugins/project-knowledge/scripts/mcp-launcher.cjs', 'plugins/project-knowledge/.claude-plugin/plugin.json', 'plugins/project-knowledge/.codex-plugin/plugin.json', 'plugins/project-knowledge/.mcp.json']) manifest.productFiles[relative] = digest(path.join(root, relative));
26
+ for (const relative of ['ui/index.html', 'ui/product.js', 'ui/live-client.js', 'ui/product.css', 'ui/favicon.svg', 'plugins/project-knowledge/hooks/claude.json', 'plugins/project-knowledge/hooks/codex.json', 'plugins/project-knowledge/scripts/context-hook.cjs', 'plugins/project-knowledge/scripts/mcp-launcher.cjs', 'plugins/project-knowledge/.claude-plugin/plugin.json', 'plugins/project-knowledge/.codex-plugin/plugin.json', 'plugins/project-knowledge/.mcp.json']) manifest.productFiles[relative] = digest(path.join(root, relative));
24
27
  AtomicFile.writeJsonAtomic(file, manifest);
25
28
  console.log('Updated content hashes from exact installed modules and reviewed product assets; this is not release acceptance.');
@@ -9,6 +9,7 @@ function verifyProductRuntime(coreRoot, resolveFrom = path.join(coreRoot, 'packa
9
9
  if (manifest.schema !== 'project-knowledge/module-content/v1' || Object.keys(manifest.modules).length !== 3) throw new Error('Incomplete module content manifest');
10
10
  for (const file of ['ui/index.html', 'ui/product.js', 'ui/product.css', 'ui/favicon.svg', 'plugins/project-knowledge/hooks/claude.json', 'plugins/project-knowledge/hooks/codex.json', 'plugins/project-knowledge/scripts/context-hook.cjs', 'plugins/project-knowledge/scripts/mcp-launcher.cjs', 'plugins/project-knowledge/.claude-plugin/plugin.json', 'plugins/project-knowledge/.codex-plugin/plugin.json', 'plugins/project-knowledge/.mcp.json']) if (!manifest.productFiles?.[file]) throw new Error(`Product asset missing from content manifest: ${file}`);
11
11
  const resolve = createRequire(resolveFrom);
12
+ if (!manifest.productFiles['ui/live-client.js']) throw new Error('Product live transport missing from content manifest');
12
13
  for (const [relative, expected] of Object.entries(manifest.productFiles || {})) {
13
14
  if (!safeRelative(relative) || crypto.createHash('sha256').update(fs.readFileSync(path.join(coreRoot, relative))).digest('hex') !== expected) throw new Error(`Product UI content mismatch: ${relative}`);
14
15
  }
@@ -86,8 +86,22 @@ Leader 独立复测关键链路。源码、npm 候选、正式安装和真实模
86
86
 
87
87
  安装、配置修复、重启和发布需单独的操作合同及受保护备份。候选只发布 `next`;正式包验收通过后才更新 `latest`。用户仍须亲自审查确实变化的原生 Hook,不写入或绕过信任。
88
88
 
89
+ 正式包也必须使用自己的全新 npm 收据和隔离配置;复用已付费向量不等于复用旧插件市场。若手工传错 fixture,外层设置了新收据也不能替旧版本背书,先核对环境文件的实际包路径和版本,错误采样明确作废。对话中的独立审查结果不替代 Leader 读取真实事件、文件与工具结果。
90
+
91
+ 提交标识查询可能合法返回 `markdown-hybrid-fallback`:它只说明返回行未采用向量通道,不应以一个后端标签否认实际检索。验收须结构化匹配项目、完整提交、服务来源、成功 tool-id 和写前顺序,并独立检查真实非空向量和语义查询;不能把文本命中当向量成功。模型注释中的正常换行也不应被误判为缺少 SHA,需保留稳定反例与复测。
92
+
93
+ Windows 的 npm 全局更新还需确认独立 MCP 客户端已释放原生库。主服务停止不代表 IDE 自己启动的 MCP 子进程已退出。本轮通过进程模块只读检查定位到本会话的本产品 MCP,而非批量终止 Node;校验精确入口、父进程和 DLL 后仅关闭该子进程,再重做完整保真升级。不能据此终止来源不明或被安全检查禁止停止的旧实例。启动后等待四模块及引擎 API 就绪,不把网页端口在线当作所有功能就绪。
94
+
89
95
  临时工作树不得通过目录联接共用需要保留的 `node_modules` 后直接递归清理。本轮清理带联接的工作树后,主源码依赖目录为空;因此清理前必须枚举链接、核对真实目标并先安全解除联接本身,不能让递归操作越出临时目录。依赖恢复按原锁文件执行,保留首失败,不以恢复后的结果覆盖环境事故。
90
96
 
91
97
  ## 验收记录
92
98
 
99
+ 本轮补充的操作约束:
100
+
101
+ - 一条独立任务优先使用新建短上下文会话;只需执行固定脚本时,不追加长篇设计或重复创建任务实体。长命令由实际进程结果证明,GLM 总结不是测试凭证。
102
+ - 从 Workbench 内再启动隔离产品时,同时设置该 fixture 的 `KB_DATA_DIR` 和 `KB_KNOWLEDGE_DATA_DIR`;不能继承宿主登记层。Q24 的首个二次开发因此查错登记层,保留首失败并修正验收启动器后才重测。
103
+ - 真实 UI 要检查任务原文事实是否送达、实际回答是否引用它,不能只检查提交号。rc.6/rc.7 的 SHA-only 成功曾掩盖正文遗漏,后续门禁补齐该反例。
104
+ - GLM 对安装包缺文件或别名的结论必须核对真实文件与哈希。本轮曾从无词法命中推断缺别名,Leader 的字节证据反证后已撤回;旧 Node REPL 的模块缓存也不能当作新运行时证据,关键核验使用新进程。
105
+ - 原生 Hook 是否需要再次审核取决于实际定义哈希,不由版本号猜测。rc.6 审核后 rc.8 的哈希未变、原生仍为 trusted;确实变化才请求用户审核,不绕过信任。
106
+
93
107
  本轮的会话、提交、源码回归、npm 下载和真实维护结果见 [5.0.16 验收记录](release-5.0.16-acceptance.md)。操作手册与版本放行结果分开维护。
@@ -2,7 +2,59 @@
2
2
 
3
3
  ## 当前结论
4
4
 
5
- 正在准备 `5.0.16-rc.8`,纳入引擎 `0.2.7-rc.9` 的短范围去重与有界完整提交证据修复,其他三个模块保持已锁定版本。源码修复已通过双 Node 回归;新安装包仍须复测,不能将 rc.6/rc.7 的结果记为 rc.8 通过。latest 仍为 5.0.14。
5
+ 正式 `5.0.16` 已通过官方独立下载、30/30 核心门禁、真实二次开发闭环、本机保留数据升级及日常 latest 重装。npm 的 latest 与 next 均为 5.0.16,本机也运行正式 npm 包;保留候选与正式包首失败,不用候选通过冒充正式通过。
6
+
7
+ ### 正式包验收
8
+
9
+ 官方收据:`kb-registry-product-9bwo5b/registry-installation.json`,sourceCommit `3a2740b6467b8d26fa05cb494c85e396132b136b`。主包 integrity 为 `sha512-VztZ6sJAg6Th0B+Rn4prfjfIfyG6JxNFe/sEUIe2++xI5niP7+q1icqWplLMf3R48Ix554PWh0yKc8e7TgET6g==`;精确模块版本与下方 rc.8 表一致。正式包与候选的收据、运行结果分别绑定,未将旧同名报告计入正式通过。
10
+
11
+ | 范围 | 正式包证据与结果 |
12
+ |---|---|
13
+ | 源码 | 冻结外壳 3a2740b 在 Node 22/24 各 139/139;引擎测试辅助代码最终 f1248a6 各 182 通过、1 项 Windows 符号链接权限跳过,40 个发布运行文件与 vendored 7c6812f 完全相同;其余模块使用精确版本、精确提交的源码回归 |
14
+ | 安装版与故障 | GLM-5.3 独立运行 Node 22/24 正常网页、一次 profiles 503 恢复、故障与生命周期八项,全部通过;测试替身仅用于确定性故障,不冒称真实模型 |
15
+ | 真模型十项 | `scoped-memories/run-1792038371463/report.json`,fixture fcKCDT,真实 MiniMax 候选提取、分级确认编辑、范围隔离、UI/API/CLI/MCP、流式、单提交与跨项目重叠、重启保真 10/10 |
16
+ | 质量与性能 | 20 条固定留出问题 Recall@5=1、MRR=0.9667、拒答准确率=1;8 项目/1000 文档热查询 P95=699.9054ms,四次真实查询 embedding,1536 维非空索引;复用付费文档向量,无重建冒充 |
17
+ | 原生送达 | Claude Code、ZCode 真实用户配置通过;Codex Hook 原生 trusted、MCP 实际 initialize 为 5.0.16,首次真实 MiniMax 请求含当前事实及已确认产品记忆;OpenCode 实际 CLI 检索与引用通过,不宣称自动前置注入 |
18
+ | 工作台 | 真实网页发送、增量流式、最终持久化和右侧引用通过;回答实际引用单写者英文原文及完整提交,不只验证 SHA。1920×1080 截图已人工核对 |
19
+ | 连续开发 | `retrieval-formal-live-development24-1792038616093.json`:分析期间再提交、保留未提交草稿、同项目 FIFO、三次真实自动维护与页面自动刷新通过 |
20
+ | 二次开发 | `retrieval-formal-secondary24-1792039028998.json`:原任务真实 search/get 先于 Write,代码及测试通过,随后真实 Hook、冻结对话、晋升、索引、再次检索通过;恢复验收未手工改写原任务文件、引用或事件 |
21
+ | 本机保真升级 | `retrieval-formal-user-upgrade24-1792039474426.json`,备份 `user-upgrade-1792039475645`;原 7 项目、知识、历史、配置和记忆保留,593 个索引文件字节不变,实际位置仍为 `D:\SanQian.Xu\knowledge\.indexes`;全局运行文件与官方收据逐文件相同 |
22
+ | 本机接线 | 正式全局安装后的公开接线操作 `op-69195d8fbbc84e49b54875134ed2727f` 四客户端完成;临时验收目录的市场来源及运行绑定已切回全局 npm 包,第三方 Hook、通知和用户原生信任保留 |
23
+ | 真实用户环境 | `retrieval-formal-user-native24-1792039600245.json`:在实际 knowledge-engine 仓库、实际用户插件下,未提醒检索的普通只读任务,其首次真实模型请求收到本项目知识;工作树不变,不以受控 fixture 代替此项 |
24
+ | 日常 latest 安装 | `retrieval-formal-user-latest24-1792039770873.json`,详细记录 `daily-latest-1792039770931/result.json`:真实 stop、npm 官方 @latest 全局安装、版本检查、启动、受管进程退出、四模块恢复、原有数据与 593 个索引文件保真、官方包逐字节核对均通过,耗时 62.4 秒 |
25
+
26
+ 正式核心汇总为 `knowledge-engine/.agent-state/acceptance/system-regression/retrieval-release-5.0.16.json`。30/30 与本机升级、日常安装、真实用户补验分别记录,不互相冒充。最终交付回执为同目录 `retrieval-system-5.0.16-final.json`,还会核对本说明提交后的实际自动维护、知识文件、索引和再次检索,以及 daily 重启后的真实原生读取。
27
+
28
+ #### 正式阶段保留的首失败
29
+
30
+ - 离线插件案例首次复用 rc.8 的隔离市场目录,因与正式包来源路径不同被保护逻辑拒绝;改用本轮全新 fcKCDT 隔离目录后,真实 Claude CLI 在 GitHub 不通条件下安装与幂等检查通过。一次 Leader 手工补验误指旧 rc.5 fixture(1792038460071)不计正式证据,正确正式补验为 1792038543299。
31
+ - 二次开发首失败 1792038714956:实际 seq18 查询、21 成功返回、108/111 get 成功、139 才 Write;旧测试错误要求 SHA 查询必须带向量命中标签,且不能解析跨 `//` 注释换行的完整 SHA。结构化项目/来源/完整提交/匹配 tool-id/写前顺序校验及解析回归已补齐,文本事实不冒称向量成功。原任务同文件恢复后通过,未重造原成功记录。重复无活动子进程的 teardown 也有独立回归,不能借此放行真正被中断的任务。修复仅在测试辅助代码(3eb54b4、f1248a6),未改正式包运行文件;GLM 独立复核 10 项定向测试通过。
32
+ - 本机首次 npm 升级 1792039147807 失败为 Windows `EBUSY`:本会话独立 MCP 子进程持有全局包的 LanceDB 原生文件。只在校验精确命令路径、父进程及 DLL 后关闭本产品 MCP 子进程,未关闭 Codex 应用或无关 Node;旧包逐文件完整性通过后恢复旧服务,再重做受保护升级。冷启动时两次过早读取引擎 API 的失败也保留;重试前已等待四模块归属及引擎 API 真正就绪。
33
+
34
+ Windows 日常 npm 更新前应先断开仍连接本产品 MCP 的编程会话;`stop` 管理主服务及其子进程,不意味着独立 IDE 的 MCP 连接已经关闭。遇到 EBUSY 不清空数据、不删除原生文件、不批量结束 Node。另一台电脑及其私有语料、本轮未操作的原生目录弹窗、不具备权限的 Windows 符号链接场景不计为自动通过。
35
+
36
+ ### rc.8 候选验收
37
+
38
+ | 范围 | 实测结果 |
39
+ |---|---|
40
+ | 官方安装 | `kb-registry-product-0DPXUm/registry-installation.json`;5.0.16-rc.8,sourceCommit `2576e1f0b98fbfdd34183feeb01539a2024922fe`;305 个产品文件及包内四模块内容核对通过,独立缓存下载,无兄弟源码回退 |
41
+ | 精确模块 | Workbench 0.13.2-rc.13、vector-hub 1.0.8-rc.8、bridge 0.7.2、引擎 0.2.7-rc.9(vendored commit `7c6812ffab8320c2d835e7825abe275507d6b960`) |
42
+ | 源码 | Node 22/24:外壳各 139;引擎各 181 通过、1 项 Windows 符号链接权限跳过;未修改的已锁定 Workbench 365、vector 73、bridge 30 维持对应提交证据 |
43
+ | 安装桌面及故障 | 两个 Node 版本的正常页面、首次 profiles 503 后恢复、故障与生命周期共八项通过,均绑定本次 receipt;仅 1920×1080 桌面 |
44
+ | 真实模型 | MiniMax 真实提取、分级记忆确认编辑、实际流式、1536 维非空向量、单提交、双项目实际重叠和重启保真 10/10(Q22,fixture CiDegJ) |
45
+ | 检索质量/性能 | 20 条固定留出问题 Recall@5=1、MRR=0.9667、拒答准确率=1;8 项目/1000 文档热查询 P95=679.7145ms,四次真实查询 embedding;不代表私有语料普遍保证 |
46
+ | 原文送达 | npm 版 topic-context 与真实网页发送都包含策略原文及完整 SHA,真实回答引用原句;原始 9aec 失败语料另用官方安装包引擎库验证,知识不改写、两条记忆加事实 3601 字符,明确不将库级补验冒充原始 HTTP/UI 复测 |
47
+ | 原生客户端 | Claude Code、Codex、ZCode 使用实际用户接线,首次模型请求含当前事实与已确认产品记忆;真实代码/回答采用记忆。Codex MCP 版本为 rc.8、Hook 原生 trusted;OpenCode 实际 CLI 搜索及引用通过,不宣称自动前置注入 |
48
+ | 二次开发 | rc8-final-fixed:实际搜索和 get → 引用后写代码/运行测试 → commit `0ec95835f58b1a1b934955fb5cfaa1811fd3b817` → Hook/冻结对话/晋升/索引/再检索均通过;`retrieval-rc8-secondary24-1792037130380.json` |
49
+ | 本机升级 | 受保护备份 `user-upgrade-1792037257474`,从官方 npm 升级 rc.8(PID 36460);原有 7 项目、知识、历史、记忆、配置保留,587 个付费索引文件字节不变,实际仍位于 `D:\SanQian.Xu\knowledge\.indexes`;四客户端修复操作 `op-0d5a7b0ef39c46779432e73cbf357f25` 完成 |
50
+
51
+ 候选汇总为 `knowledge-engine/.agent-state/acceptance/system-regression/retrieval-release-5.0.16-rc.8.json`。源码、受控故障、真实模型和 npm 安装身份分别保留,非公开的凭据配置不进入交付记录。
52
+
53
+ Q24 首次二次开发失败保留(`retrieval-rc8-secondary24-1792036733022.json`):GLM 启动的验收进程继承宿主 `KB_KNOWLEDGE_DATA_DIR`,CLI 在另一登记层返回 PROJECT_NOT_FOUND,随后超时;不是产品门禁通过。生产 supervisor 已设置正确映射,验收启动器补同一映射及稳定红/绿回归(测试提交 b255f9d),重跑新的 rc8-final-fixed 轮次后通过;未修改产品包、未重造首次成功记录。
54
+
55
+ 用户首次口头回执与实际 trust 状态不符时暂停模型测试;用户实际审核后,rc.6 与 rc.8 原生哈希同为 a51a3b6f…,rc.8 保持 trusted,无需按版本号重复审核。信任是否失效以原生接口为准,不代写记录或使用绕过参数。
56
+
57
+ ### rc.8 修复过程与保留失败
6
58
 
7
59
  rc.7 已官方下载(`kb-registry-product-50eztE/registry-installation.json`,sourceCommit `043e7156fa55005e27f014e98179d5db110e59bf`),双 Node 外壳各 139 项、安装版八项和真实 MiniMax 记忆/并发十项通过。原始 `topic-context` 已通过,但加强的真实网页问法仍失败:返回了完整事实段落及其首句,随后交叉引用继续挤掉 Patch;失败收据 `retrieval-rc7-workbench24-1792024781524.json` 保留,不升级 latest。
8
60