@sanqianx/project-knowledge 5.0.16 → 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,6 +86,12 @@ 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
  ## 验收记录
@@ -2,7 +2,36 @@
2
2
 
3
3
  ## 当前结论
4
4
 
5
- `5.0.16-rc.8` 的 30/30 核心门禁及二次开发闭环已通过,现冻结正式包 `5.0.16`,仅发布 next 后重新下载验收;正式包尚未验收,不更新 latest(仍为 5.0.14)。保留 rc.6/rc.7 首失败,不用候选通过冒充正式通过。
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 符号链接场景不计为自动通过。
6
35
 
7
36
  ### rc.8 候选验收
8
37