@sanqianx/project-knowledge 5.0.16 → 5.0.17-rc.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.
@@ -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
  ## 验收记录
@@ -99,3 +105,15 @@ Leader 独立复测关键链路。源码、npm 候选、正式安装和真实模
99
105
  - 原生 Hook 是否需要再次审核取决于实际定义哈希,不由版本号猜测。rc.6 审核后 rc.8 的哈希未变、原生仍为 trusted;确实变化才请求用户审核,不绕过信任。
100
106
 
101
107
  本轮的会话、提交、源码回归、npm 下载和真实维护结果见 [5.0.16 验收记录](release-5.0.16-acceptance.md)。操作手册与版本放行结果分开维护。
108
+
109
+ ## 网页响应性与历史修复(2026-10-15)
110
+
111
+ 本批继续采用 Leader 关键开发、真实 GLM-5.3 独立故障审查:Workbench `sess-8155764fec1e`,外壳 `sess-68d4d397c7f5`。GLM 只写指定测试和审查报告,不改生产实现、不发布、不改用户配置。Leader 独立复测测试文件与真实安装包,不将报告摘要当作成功凭证。
112
+
113
+ - 缓存 TTL 的确定性测试使用注入时钟;20ms 真实时间在完整回归负载下会过期,不应靠增大等待掩盖测试竞态。
114
+ - 原生 SDK 已发出最终 result,不代表异步迭代器必定立即 EOF。旧工作台的独立审查会话曾在最终报告后仍显示 running;Leader 核对报告完整后显式中止清理。新实现以 result 为本轮边界并在 finally 关闭自己的 query,另用真实 npm 工作台任务验证自然 idle,不能只补假流单测。
115
+ - 浏览器滚动默认动作是异步的;滚轮后同一时刻读取 scrollTop 可能仍为旧值。要同时验证向上意图、后续内容增长和稳定阅读位置,不能只取输入后的单帧。
116
+ - 新增异步发现后要审查诊断与 runtime 旁路,确保没有保留同步命令查找,也不能为了消除阻塞而丢掉注册表安装路径。
117
+ - 多标签连接测试使用八个真实浏览器标签页;Node WebSocket 测试只证明协议行为,不能代替浏览器同源连接限制的验收。
118
+
119
+ 本批完整记录见 [5.0.17 验收记录](release-5.0.17-acceptance.md)。
@@ -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
 
@@ -0,0 +1,74 @@
1
+ # 5.0.17 网页加载、刷新与完整历史验收
2
+
3
+ ## 发布状态
4
+
5
+ 2026-10-15:候选验收和最终复核进行中,正式 `latest` 仍为 `5.0.16`。本文件不得被解读为正式版已发布。Electron、Windows 安装包和窄屏矩阵均不在本次范围;用户报告故障的另一台电脑尚需更新后回验。
6
+
7
+ ## 交付范围
8
+
9
+ - 首屏按区域独立加载、普通读取 3 秒超时,失败保留已有有效数据。
10
+ - Agent 异步扫描最多两个并发,单探测 5 秒、整次 12 秒,手动刷新绕过缓存。
11
+ - 嵌入工作台只初始化一次,切页、项目和重连通过已有消息/快照恢复,不重设 iframe 地址。
12
+ - 每标签一个同源 WebSocket,合并工作台、引擎及日志;独立模块保留 SSE。
13
+ - 引擎服务归属与项目接线分离,失败项目独立恢复;未完成安全接线的项目不执行写入。
14
+ - 运行摘要分页,完整提交证据延迟读取;配置指纹合并下发并防止模块重启后的迟到回执污染缓存。
15
+ - 工作中向上滚动立即暂停跟随;新增持久化完整会话历史与向前分页,旧 Claude 原生历史尽可能只读恢复。
16
+
17
+ 旧会话若从未持久化且原生文件已经不存在,不能凭空恢复;页面明确显示历史不完整。新增完整归档不再依赖 5000 条驱动事件缓存。
18
+
19
+ ## 源码及独立审查
20
+
21
+ | 范围 | 结果与证据 |
22
+ |---|---|
23
+ | Workbench 首批 | `c0ad1f4439cf9ddd81f1277924dd6a01ddd212aa`,400 项通过;134 个 JS 文件为后续补测后的数量,首批为 133 |
24
+ | 引擎 | `10c940a2425daaf27d24500c6f2ba21b79cb91d4`,185 通过、1 项既有跳过,不把跳过计为通过 |
25
+ | 外壳首批 | `17bab6279035b3120a4dfbbe998f7e7a53772094`,backend-only 133 通过;补 configuration-generation 后最终 134 项全部通过(233968ms);浏览器测试排除不计通过,见下方真实桌面验收 |
26
+ | GLM-5.3 | Workbench 13 项定向审查;外壳 14 项当前通过,冻结基线 9 项预期失败。真实会话和报告位置见操作手册 |
27
+ | 诊断补漏 | `98258aa`:runtime 改走异步解析,doctor 与扫描共用 ZCode 注册表后备发现;新增三项先红后绿,Workbench 全套 403 通过 |
28
+ | 启动补漏 | `cbba73a`:GLM 同时指出任务与命令发现的同步 PATH 后备;改为一次异步解析,保留兼容同步导出,扫描中止信号传入子进程;新增先红后绿,Workbench 全套 404 通过 |
29
+
30
+ GLM 报告分别保存在两个仓库的 `.local-test-data/ui-refresh-2026-10-15/`,不是生产知识文档。Leader 独立执行安装、真实模型和浏览器验收。
31
+
32
+ ## 首个候选 5.0.17-rc.1
33
+
34
+ 官方独立缓存收据:`D:\SanQian.Xu\.pk-acceptance-temp\kb-registry-product-ErheAk\registry-installation.json`。
35
+
36
+ - 主包提交 `17bab6279035b3120a4dfbbe998f7e7a53772094`。
37
+ - integrity:`sha512-QdcreZSakB55UyBDJ1rCmULxwOxem5qmQYVWFDYXH+yzp9qceqJl+EsOeq2qOMLKW1oUIPPucB+oMWiRO5Vjdw==`。
38
+ - 精确模块:Workbench `0.13.2-rc.14`、vector-hub `1.0.8-rc.8`、bridge `0.7.2`、vendored engine `0.2.7-rc.10`。
39
+ - 包内四模块哈希及依赖官方 integrity 通过,不依赖兄弟源码运行。
40
+ - 四个真实受管模块退出后守护恢复、项目投影保留、CLI 大写 STOP 定向停止、所有受管子进程正常退出通过。证据在收据根目录 `acceptance-data/installed-*-acceptance.json`。
41
+
42
+ ### 真实模型与向量
43
+
44
+ 隔离环境:`D:\SanQian.Xu\.pk-acceptance-temp\智脑桌面验收-6PYtN8`,原供应商配置只读,隔离凭据目录受 ACL 保护。使用真实 MiniMax 对话与 embo-01,不使用模型/embedding 替身。
45
+
46
+ `real-agent-rc1-refresh-acceptance.json`:
47
+
48
+ - 单提交 `efb58c0cbe8b48dd5132b7ea1931b30d884a4d73` 晋升、索引及 commit 引用通过。
49
+ - 两项目提交 `334f69b08fb759e4cbfca3d8398a02ea74eff5ea`、`21706feebe3fb695ac1b9444bc2d9078cafbe994` 均完成。
50
+ - 分析区间分别为 12:23:29.417–12:23:54.088 UTC、12:23:30.428–12:24:03.577 UTC,确实重叠,不以两个排队状态冒充并发。
51
+ - 浏览器通过包内 Workbench 发送真实任务,会话 `sess-98c93fa8072c` 返回知识原文及对应 commit;两轮流式回答后自然 idle,不需手动中止。
52
+
53
+ ### 桌面浏览器
54
+
55
+ 真实 Edge 正常桌面视口 1436×756,无尺寸覆盖、无横向溢出。
56
+
57
+ - 源码受控五服务环境:八个真实标签页均加载工作台,每页 1 条 WebSocket / 2 个订阅;无同源六连接堵塞。
58
+ - npm 候选真实模型环境:上下滚动、输出期间暂停跟随、切页保留草稿、断网恢复、引用保持、单连接复用通过。滚动后内容高度由 2233 增至 4495,阅读位置仍停留较早内容,而非被拉到底部。
59
+ - 真实回答明确引用 `Knowledge has one writer: the engine promotion path. Preserve native workbench streaming and keep the product simple.`,不只匹配提交号。
60
+ - 截图:`project-knowledge-base/.local-test-data/ui-refresh-2026-10-15/rc1-real-workbench.jpg`。
61
+
62
+ ## 性能与保留的首次失败
63
+
64
+ - 一个模块挂起时,聚合项目读取从约 12038ms 降至 2509ms;模块健康读取约 2014ms,均为本机受控测量,不是所有设备保证。
65
+ - 四个真实慢 CLI 子进程、最多两个并发时,轻量 HTTP P95 约 2.35–12.84ms;不再同步阻塞整个后台。
66
+ - 1000 个运行的摘要只返回 50 条且小于 10KB,详情仍保留证据。
67
+ - 初次候选 registry 尚在异步处理中时下载校验因 next 仍旧版本失败;等待 registry 可见后使用新的独立缓存下载通过,不覆盖原失败。
68
+ - 原全套回归暴露配置同步过程中知识根变化未再次下发;已修复合并任务的 dirty 循环,重测通过。
69
+ - GLM 的 TTL 20ms 墙钟测试在负载下偶发失效,改为注入时钟并全套复跑;不将测试竞态当产品回归。
70
+ - 诊断复核发现 runtime 同步旁路与 registry-only ZCode 遗漏;首个候选不晋升 latest,补测试和修复后重新验收。
71
+
72
+ ## 最终放行
73
+
74
+ 待补:最终候选的官方收据、正式包收据及安装版重测结果。未填写之前不报告正式交付完成。