@sanqianx/project-knowledge 5.0.17 → 5.0.18-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.
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+ // Startup-event integration repair. No timers, no polling: the same failure
3
+ // cause will not heal inside one run, so a failed repair is surfaced once and
4
+ // never retried automatically. The next backend start is a new event and gets
5
+ // one fresh attempt; the only in-run retry is the explicit user action.
6
+
7
+ const { readIntent } = require('./integration-intent');
8
+
9
+ class IntegrationAutoRepair {
10
+ constructor({ dataDir, createManager, logger } = {}) {
11
+ if (typeof createManager !== 'function') throw new Error('IntegrationAutoRepair requires a createManager function.');
12
+ this.dataDir = dataDir;
13
+ this.createManager = createManager;
14
+ this.logger = logger || { info: () => {}, warn: () => {}, error: () => {} };
15
+ this.attempted = new Set();
16
+ }
17
+
18
+ async _repairOne(client, manager) {
19
+ const summary = await manager.installAll({ clients: [client] });
20
+ const capture = summary?.clients?.find(entry => entry.client === client)?.developmentCapture;
21
+ return { state: capture?.ok === true ? 'repaired' : 'failed', at: new Date().toISOString(), error: capture?.ok === true ? null : (capture?.detail || 'installAll did not report success.') };
22
+ }
23
+
24
+ async _attempt(client, options = {}) {
25
+ const force = options.force === true;
26
+ if (!force && this.attempted.has(client)) return null;
27
+ if (!force) this.attempted.add(client);
28
+ let result;
29
+ try {
30
+ result = await this._repairOne(client, this.createManager());
31
+ } catch (error) {
32
+ result = { state: 'failed', at: new Date().toISOString(), error: error.message };
33
+ }
34
+ this.results ||= {};
35
+ this.results[client] = result;
36
+ this.logger[result.state === 'repaired' ? 'info' : 'warn'](
37
+ `integrations.auto_repair_${result.state === 'repaired' ? 'succeeded' : 'failed'}`,
38
+ result.state === 'repaired' ? '启动时自动修复了 Agent 接入。' : '启动自动修复失败,已停止本周期自动重试。',
39
+ { context: { client, error: result.error } },
40
+ );
41
+ return result;
42
+ }
43
+
44
+ async runOnce() {
45
+ const intent = readIntent(this.dataDir);
46
+ if (!intent) return this.status();
47
+ const status = await this.createManager().statusAll({});
48
+ for (const entry of status?.clients || []) {
49
+ if (intent.clients[entry.client] !== 'connected') continue;
50
+ if (entry.developmentCapture?.installed === true) continue;
51
+ if (this.attempted.has(entry.client)) continue;
52
+ await this._attempt(entry.client);
53
+ }
54
+ return this.status();
55
+ }
56
+
57
+ async retry(client) {
58
+ return this._attempt(client, { force: true });
59
+ }
60
+
61
+ // A later successful manual install (switch on / retry repair) clears the
62
+ // startup failure so the badge stops reporting a stale error.
63
+ markSucceeded(clients) {
64
+ this.results ||= {};
65
+ for (const client of clients || []) {
66
+ if (this.results[client]?.state === 'failed') this.results[client] = { state: 'repaired', at: new Date().toISOString(), error: null };
67
+ }
68
+ }
69
+
70
+ status() {
71
+ return { ...(this.results || {}) };
72
+ }
73
+ }
74
+
75
+ module.exports = { IntegrationAutoRepair };
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+ // User intent for external agent integrations.
3
+ //
4
+ // The install state alone cannot distinguish "broken, should heal" from
5
+ // "deliberately removed, must stay away": both look like hooks-absent. This
6
+ // store records the user's switch position per client so the startup repair
7
+ // event only ever touches clients the user actually connected.
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const SCHEMA = 'project-knowledge/integration-intent/v1';
13
+ const FILENAME = 'integration-intent.json';
14
+ const STATES = new Set(['connected', 'disconnected']);
15
+
16
+ function intentPath(dataDir) {
17
+ return path.join(path.resolve(dataDir), FILENAME);
18
+ }
19
+
20
+ function normalize(value) {
21
+ if (!value || typeof value !== 'object' || value.schema !== SCHEMA) return null;
22
+ const clients = {};
23
+ for (const [client, state] of Object.entries(value.clients || {})) {
24
+ if (STATES.has(state)) clients[client] = state;
25
+ }
26
+ return { schema: SCHEMA, clients };
27
+ }
28
+
29
+ function readIntent(dataDir) {
30
+ try {
31
+ return normalize(JSON.parse(fs.readFileSync(intentPath(dataDir), 'utf-8')));
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ function writeIntent(dataDir, clients) {
38
+ const target = intentPath(dataDir);
39
+ const temp = `${target}.${process.pid}.tmp`;
40
+ fs.mkdirSync(path.dirname(target), { recursive: true });
41
+ fs.writeFileSync(temp, JSON.stringify({ schema: SCHEMA, clients }, null, 2) + '\n', 'utf-8');
42
+ try {
43
+ fs.renameSync(temp, target);
44
+ } catch {
45
+ // Windows cannot always rename over an existing file.
46
+ try { fs.unlinkSync(target); } catch {}
47
+ fs.renameSync(temp, target);
48
+ }
49
+ return { schema: SCHEMA, clients };
50
+ }
51
+
52
+ function markIntent(dataDir, client, state) {
53
+ if (!STATES.has(state)) throw new Error(`invalid integration intent state: ${state}`);
54
+ const current = readIntent(dataDir);
55
+ const clients = { ...(current?.clients || {}), [client]: state };
56
+ return writeIntent(dataDir, clients);
57
+ }
58
+
59
+ // Migration: fill entries only for clients the file has never seen AND whose
60
+ // status scan has settled — a mid-flight scan reports installed as undefined,
61
+ // which must never be recorded as a removal decision. A client whose hooks
62
+ // are installed counts as connected (standalone install or pre-upgrade
63
+ // state); settled-but-absent hooks count as disconnected — the store is never
64
+ // allowed to rewrite a decision the user already made.
65
+ function bootstrapIntent(dataDir, clientsStatus) {
66
+ const current = readIntent(dataDir);
67
+ const clients = { ...(current?.clients || {}) };
68
+ let changed = false;
69
+ for (const entry of clientsStatus || []) {
70
+ if (!entry || !entry.client || entry.client in clients) continue;
71
+ if (entry.developmentCapture?.state === 'checking' || entry.developmentCapture?.installed === undefined) continue;
72
+ clients[entry.client] = entry.developmentCapture?.installed === true ? 'connected' : 'disconnected';
73
+ changed = true;
74
+ }
75
+ if (!changed && current) return current;
76
+ return writeIntent(dataDir, clients);
77
+ }
78
+
79
+ // Aggregate badge state for the single-switch UI.
80
+ function summarizeClient(entry, repair) {
81
+ if (entry.developmentCapture?.state === 'checking' || entry.knowledgeIntegration?.state === 'checking') return 'checking';
82
+ if (!(entry.available || entry.configFound)) return 'not-detected';
83
+ if (entry.developmentCapture?.installed === true) return 'connected';
84
+ if (repair?.[entry.client]?.state === 'failed') return 'error';
85
+ return 'disconnected';
86
+ }
87
+
88
+ module.exports = { SCHEMA, FILENAME, intentPath, readIntent, markIntent, bootstrapIntent, summarizeClient };
@@ -12,9 +12,14 @@
12
12
  * - module removal with keepSessions semantics
13
13
  *
14
14
  * Configuration (env, loopback defaults):
15
- * KB_TERMINAL_URL default http://127.0.0.1:5760
16
- * KB_VECTORHUB_URL default http://127.0.0.1:8787
17
- * KB_EVENTBRIDGE_URL default http://127.0.0.1:8790
15
+ * KB_TERMINAL_URL default http://127.0.0.1:5760 (managed: dynamic when busy)
16
+ * KB_VECTORHUB_URL default http://127.0.0.1:8787 (managed: dynamic when busy)
17
+ * KB_EVENTBRIDGE_URL default http://127.0.0.1:8790 (managed: dynamic when busy)
18
+ * KB_KNOWLEDGE_ENGINE_URL default http://127.0.0.1:5790 (managed: dynamic when busy)
19
+ * An explicitly injected URL is treated as an external service and never
20
+ * rewritten. Managed modules resolve a final URL before spawn: healthy
21
+ * identity match reuses the port, otherwise the next free port from
22
+ * preferred +0..+19 is taken; all busy fails loudly (NO_FREE_MODULE_PORT).
18
23
  * KB_MODULES_AUTOSTART '0' to stop spawning the vendored module services
19
24
  * (they start with the shell by default)
20
25
  * KB_TERMINAL_COMMAND explicit node command line for the terminal service
@@ -23,8 +28,10 @@
23
28
  */
24
29
 
25
30
  const { spawn, spawnSync } = require('child_process');
31
+ const net = require('net');
26
32
  const path = require('path');
27
33
  const { StorageLayout } = require('./storage-layout');
34
+ const { resolveModuleUrl } = require('./module-port-resolver');
28
35
  const fs = require('fs');
29
36
  const crypto = require('crypto');
30
37
  const { once } = require('events');
@@ -83,6 +90,40 @@ function defaultEventBridgeUrl() {
83
90
 
84
91
  function defaultEngineUrl() { return String(process.env.KB_KNOWLEDGE_ENGINE_URL || 'http://127.0.0.1:5790').replace(/\/+$/, ''); }
85
92
 
93
+ // Health identity markers: an HTTP 200 alone can come from any foreign
94
+ // service that grabbed the port. Only a body that matches the product
95
+ // counts as "our module already running" (reuse); anything else is
96
+ // foreign and the resolver moves to another port instead of crash-looping.
97
+ const MODULE_IDENTITY = {
98
+ terminal: body => body && typeof body === 'object' && body.status === 'ok' && typeof body.apiVersion === 'string',
99
+ vectorHub: body => body && typeof body === 'object' && Array.isArray(body.projects),
100
+ eventBridge: body => body && typeof body === 'object' && typeof body.registeredProjects === 'number',
101
+ engine: body => body && typeof body === 'object' && body.name === 'knowledge-engine',
102
+ };
103
+
104
+ // kind -> bridge URL property, explicit env override, spawn spec name
105
+ const MODULE_URL_TARGETS = [
106
+ { kind: 'terminal', prop: 'terminalUrl', env: 'KB_TERMINAL_URL', specName: 'terminal' },
107
+ { kind: 'vectorHub', prop: 'vectorHubUrl', env: 'KB_VECTORHUB_URL', specName: 'vector-hub' },
108
+ { kind: 'eventBridge', prop: 'eventBridgeUrl', env: 'KB_EVENTBRIDGE_URL', specName: 'event-bridge' },
109
+ { kind: 'engine', prop: 'engineUrl', env: 'KB_KNOWLEDGE_ENGINE_URL', specName: 'knowledge-engine' },
110
+ ];
111
+
112
+ function specNameToKind(specName) {
113
+ return MODULE_URL_TARGETS.find(target => target.specName === specName)?.kind || null;
114
+ }
115
+
116
+ function tcpPortFree(host, port) {
117
+ return new Promise(resolve => {
118
+ const tester = net.createServer();
119
+ let done = false;
120
+ const finish = free => { if (done) return; done = true; tester.close(() => resolve(free)); };
121
+ tester.once('error', () => finish(false));
122
+ tester.once('listening', () => finish(true));
123
+ tester.listen(port, host);
124
+ });
125
+ }
126
+
86
127
  class ModuleBridge {
87
128
  constructor(options = {}) {
88
129
  this.logger = options.logger || { info: () => {}, warn: () => {}, error: () => {} };
@@ -90,6 +131,15 @@ class ModuleBridge {
90
131
  this.vectorHubUrl = options.vectorHubUrl || defaultVectorHubUrl();
91
132
  this.eventBridgeUrl = options.eventBridgeUrl || defaultEventBridgeUrl();
92
133
  this.engineUrl = options.engineUrl || defaultEngineUrl();
134
+ // Explicitly injected URLs (option or env) mean an external service;
135
+ // only managed defaults participate in dynamic port resolution.
136
+ this.explicitModuleUrls = {
137
+ terminal: Boolean(options.terminalUrl || process.env.KB_TERMINAL_URL),
138
+ vectorHub: Boolean(options.vectorHubUrl || process.env.KB_VECTORHUB_URL),
139
+ eventBridge: Boolean(options.eventBridgeUrl || process.env.KB_EVENTBRIDGE_URL),
140
+ engine: Boolean(options.engineUrl || process.env.KB_KNOWLEDGE_ENGINE_URL),
141
+ };
142
+ this.moduleUrlSources = { terminal: 'pending', vectorHub: 'pending', eventBridge: 'pending', engine: 'pending' };
93
143
  this.runtimeToken = options.runtimeToken || crypto.randomBytes(32).toString('hex');
94
144
  this.registryStore = options.registryStore || null;
95
145
  this.projectStore = options.projectStore || null;
@@ -303,23 +353,43 @@ class ModuleBridge {
303
353
  };
304
354
  }
305
355
 
306
- async _probe(base) {
307
- const route = base === this.terminalUrl ? '/api/claude-workbench/v1/health' : '/api/health';
308
- try { const response = await this._fetch(`${base}${route}`, { timeoutMs: 2000 }); return response.ok; } catch { return false; }
356
+ /**
357
+ * Classify what is listening on a module URL.
358
+ * 'module' HTTP 2xx + product identity marker -> safe to reuse
359
+ * 'foreign' HTTP responded but identity mismatch -> never reuse
360
+ * 'unreachable' no HTTP answer (free port or non-HTTP listener)
361
+ */
362
+ async _probeClass(base, kind) {
363
+ const route = kind === 'terminal' ? '/api/claude-workbench/v1/health' : '/api/health';
364
+ const marker = MODULE_IDENTITY[kind];
365
+ if (!marker) return 'unreachable';
366
+ try {
367
+ const response = await this._fetch(`${base}${route}`, { timeoutMs: 2000 });
368
+ if (!response.ok) return 'unreachable';
369
+ let body;
370
+ try { body = await response.json(); } catch { return 'foreign'; }
371
+ return marker(body) ? 'module' : 'foreign';
372
+ } catch { return 'unreachable'; }
373
+ }
374
+
375
+ async _probe(base, kind) {
376
+ const resolvedKind = kind || (base === this.terminalUrl ? 'terminal' : null);
377
+ if (!resolvedKind) return false;
378
+ return (await this._probeClass(base, resolvedKind)) === 'module';
309
379
  }
310
380
 
311
381
  async modulesHealth() {
312
382
  const [terminal, vectorHub, eventBridge, engine] = await Promise.all([
313
- this._probe(this.terminalUrl),
314
- this._probe(this.vectorHubUrl),
315
- this._probe(this.eventBridgeUrl),
316
- this._probe(this.engineUrl),
383
+ this._probe(this.terminalUrl, 'terminal'),
384
+ this._probe(this.vectorHubUrl, 'vectorHub'),
385
+ this._probe(this.eventBridgeUrl, 'eventBridge'),
386
+ this._probe(this.engineUrl, 'engine'),
317
387
  ]);
318
388
  return { ok: true, modules: {
319
- terminal: { url: this.terminalUrl, up: terminal },
320
- vectorHub: { url: this.vectorHubUrl, up: vectorHub },
321
- eventBridge: { url: this.eventBridgeUrl, up: eventBridge },
322
- engine: { url: this.engineUrl, up: engine },
389
+ terminal: { url: this.terminalUrl, up: terminal, source: this.moduleUrlSources.terminal },
390
+ vectorHub: { url: this.vectorHubUrl, up: vectorHub, source: this.moduleUrlSources.vectorHub },
391
+ eventBridge: { url: this.eventBridgeUrl, up: eventBridge, source: this.moduleUrlSources.eventBridge },
392
+ engine: { url: this.engineUrl, up: engine, source: this.moduleUrlSources.engine },
323
393
  } };
324
394
  }
325
395
 
@@ -617,8 +687,10 @@ class ModuleBridge {
617
687
  */
618
688
  async startSupervised() {
619
689
  if (String(process.env.KB_MODULES_AUTOSTART || '') === '0') return;
690
+ await this._resolveManagedUrls();
620
691
  await Promise.all(this._supervisedSpecs().map(async spec => {
621
- if (await this._probe(spec.url)) {
692
+ const kind = specNameToKind(spec.name);
693
+ if (kind && await this._probeClass(spec.url, kind) === 'module') {
622
694
  this.logger.info('modules.supervisor', 'Module service already up; not spawning.', { context: { name: spec.name, url: spec.url } });
623
695
  this.onReady?.(spec.name); return;
624
696
  }
@@ -626,6 +698,31 @@ class ModuleBridge {
626
698
  }));
627
699
  }
628
700
 
701
+ /**
702
+ * Resolve final managed URLs BEFORE any spec is built or a process is
703
+ * spawned, so inter-module env wiring (engine URL, bridge URL, ...) uses
704
+ * the final dynamic ports. Sequential on purpose: parallel scans could
705
+ * race two kinds into the same overlapping candidate range
706
+ * (8787..8806 vs 8790..8809).
707
+ */
708
+ async _resolveManagedUrls() {
709
+ for (const target of MODULE_URL_TARGETS) {
710
+ const preferredUrl = this[target.prop];
711
+ const resolved = await resolveModuleUrl({
712
+ kind: target.kind,
713
+ preferredUrl,
714
+ explicit: this.explicitModuleUrls[target.kind],
715
+ portFree: port => tcpPortFree(urlHost(preferredUrl), port),
716
+ probe: url => this._probeClass(url, target.kind),
717
+ });
718
+ this[target.prop] = resolved.url;
719
+ this.moduleUrlSources[target.kind] = resolved.source;
720
+ if (resolved.source !== 'external') {
721
+ this.logger.info('modules.port_resolution', 'Module URL resolved.', { context: { kind: target.kind, source: resolved.source, url: resolved.url, port: resolved.port, offset: resolved.offset } });
722
+ }
723
+ }
724
+ }
725
+
629
726
  _supervisedSpecs() {
630
727
  const specs = [];
631
728
  // C2 minimal wiring: the terminal service receives the non-secret product
@@ -740,7 +837,8 @@ class ModuleBridge {
740
837
  let attemptsReady = 0;
741
838
  const checkReady = async () => {
742
839
  if (this.stopped || child.exitCode !== null || ++attemptsReady > 20) return;
743
- if (await this._probe(spec.url)) { this.resetRuntimeConfiguration(); this.onReady?.(spec.name); return; }
840
+ const kind = specNameToKind(spec.name);
841
+ if (kind && await this._probeClass(spec.url, kind) === 'module') { this.resetRuntimeConfiguration(); this.onReady?.(spec.name); return; }
744
842
  const timer = setTimeout(() => { this.restartTimers.delete(timer); checkReady(); }, 500);
745
843
  this.restartTimers ||= new Set(); this.restartTimers.add(timer);
746
844
  };
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ // Dynamic module URL resolution.
4
+ //
5
+ // Managed modules used to bind fixed default ports (terminal 5760,
6
+ // vector-hub 8787, event-bridge 8790, engine 5790). When a foreign process
7
+ // occupied one of those ports the module crash-looped and the feature stayed
8
+ // degraded. This resolver picks the final URL BEFORE any module is spawned:
9
+ //
10
+ // explicit env/option URL -> external service, untouched (legacy behavior)
11
+ // healthy identity match -> reused (standalone module keeps running)
12
+ // otherwise -> first free port from preferred +0..+19
13
+ // all busy -> NO_FREE_MODULE_PORT (loud, never silent)
14
+
15
+ function invalidUrl(value) {
16
+ return Object.assign(new Error(`Invalid module URL: ${value}`), { code: 'INVALID_MODULE_URL' });
17
+ }
18
+
19
+ function parsePreferredUrl(value) {
20
+ let parsed;
21
+ try { parsed = new URL(String(value)); } catch { throw invalidUrl(value); }
22
+ const port = Number(parsed.port);
23
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw invalidUrl(value);
24
+ return { parsed, port };
25
+ }
26
+
27
+ function rebuildUrl(parsed, port) {
28
+ const host = parsed.hostname.includes(':') ? `[${parsed.hostname}]` : parsed.hostname;
29
+ return `${parsed.protocol}//${host}:${port}`;
30
+ }
31
+
32
+ /**
33
+ * @param {object} options
34
+ * @param {string} options.kind module kind, used in errors/logs only
35
+ * @param {string} options.preferredUrl default URL with an explicit port
36
+ * @param {boolean} options.explicit true when the URL was injected explicitly
37
+ * @param {(port: number) => Promise<boolean>} options.portFree TCP bind probe
38
+ * @param {(url: string) => Promise<'module'|'foreign'|'unreachable'>} options.probe
39
+ * @param {number} [options.scanRange] candidate count starting at the preferred port
40
+ * @returns {Promise<{url: string, source: 'external'|'reused'|'dynamic', port: number, offset: number}>}
41
+ */
42
+ async function resolveModuleUrl({ kind, preferredUrl, explicit, portFree, probe, scanRange = 20 }) {
43
+ const { parsed, port } = parsePreferredUrl(preferredUrl);
44
+ if (explicit) return { url: preferredUrl, source: 'external', port, offset: 0 };
45
+ if (await probe(preferredUrl) === 'module') return { url: preferredUrl, source: 'reused', port, offset: 0 };
46
+ const range = Math.max(1, Number.isInteger(scanRange) ? scanRange : 20);
47
+ for (let offset = 0; offset < range && port + offset <= 65535; offset++) {
48
+ const candidate = port + offset;
49
+ if (await portFree(candidate)) {
50
+ return { url: rebuildUrl(parsed, candidate), source: 'dynamic', port: candidate, offset };
51
+ }
52
+ }
53
+ const last = Math.min(port + range - 1, 65535);
54
+ throw Object.assign(new Error(`No free port found for ${kind} in ${port}-${last}.`), { code: 'NO_FREE_MODULE_PORT' });
55
+ }
56
+
57
+ module.exports = { resolveModuleUrl };
@@ -1304,6 +1304,7 @@ function createRequestHandler(runtime, options = {}) {
1304
1304
  // Integration and Development Capture per client, with separate status.
1305
1305
  if (method === 'GET' && pathname === '/api/integrations/status') {
1306
1306
  runtime.integrationStatusScan ||= new (require('./integration-status-scan').IntegrationStatusScan)();
1307
+ const intentStore = require('./integration-intent');
1307
1308
  const managerOptions = { ...options.integrationOptions, rootDir: runtime.rootDir, logger: runtime.logger, operationId: requestOperationId, bridgeHomeDir: runtime.bridgeAdapter.hookBinding().bridgeHomeDir };
1308
1309
  const projectId = url.searchParams.get('projectId');
1309
1310
  if (projectId && !runtime.registryStore.listIds().includes(projectId)) throw new DomainError('PROJECT_NOT_FOUND', '当前项目尚未登记。', { status: 404 });
@@ -1311,6 +1312,14 @@ function createRequestHandler(runtime, options = {}) {
1311
1312
  const requested = url.searchParams.get('clients');
1312
1313
  const clients = requested ? requested.split(',').filter(Boolean).map(require('./integration-installer').normalizeClient) : undefined;
1313
1314
  const summary = await runtime.integrationStatusScan.read(managerOptions, { captureOnly: url.searchParams.get('captureOnly') === '1', cwd, clients }, { force: url.searchParams.get('refresh') === '1' });
1315
+ const repair = runtime.integrationAutoRepair?.status() || null;
1316
+ const intent = intentStore.bootstrapIntent(runtime.dataPath, summary.clients);
1317
+ summary.clients = summary.clients.map(entry => ({
1318
+ ...entry,
1319
+ intent: intent.clients[entry.client] || 'disconnected',
1320
+ summary: intentStore.summarizeClient(entry, repair),
1321
+ repair: repair?.[entry.client] || null,
1322
+ }));
1314
1323
  return send(res, 200, { ok: true, ...summary });
1315
1324
  }
1316
1325
  if (method === 'POST' && pathname === '/api/integrations/install') {
@@ -1327,6 +1336,13 @@ function createRequestHandler(runtime, options = {}) {
1327
1336
  scope: body.scope || 'user',
1328
1337
  });
1329
1338
  const complete = summary.bridge.ok && summary.clients.every(entry => (!entry.knowledgeIntegration || entry.knowledgeIntegration.ok) && (!entry.developmentCapture || entry.developmentCapture.ok));
1339
+ if (complete) {
1340
+ const intentStore = require('./integration-intent');
1341
+ for (const entry of summary.clients) {
1342
+ if (entry.developmentCapture?.ok || entry.knowledgeIntegration?.ok) intentStore.markIntent(runtime.dataPath, entry.client, 'connected');
1343
+ }
1344
+ runtime.integrationAutoRepair?.markSucceeded?.(summary.clients.map(entry => entry.client));
1345
+ }
1330
1346
  await runtime.logger[complete ? 'info' : 'error'](`integrations.install_${complete ? 'completed' : 'failed'}`, complete ? 'Agent 接入完成。' : 'Agent 接入未完成,请查看各工具的失败原因。', { operationId: requestOperationId, context: { clients: summary.clients.map(entry => entry.client) } });
1331
1347
  return send(res, 200, { ok: true, complete, operationId: requestOperationId, ...summary });
1332
1348
  }
@@ -1342,7 +1358,13 @@ function createRequestHandler(runtime, options = {}) {
1342
1358
  : undefined;
1343
1359
  const knowledgeResults = body.captureOnly === true ? [] : await manager.operate('uninstall', clients && clients.length ? clients : manager.detectClients(), { scope: body.scope || 'user' });
1344
1360
  const captureResults = body.knowledgeOnly === true ? [] : await manager.operateCapture('uninstall', clients && clients.length ? clients : manager.detectClients(), {});
1345
- return send(res, 200, { ok: true, complete: [...knowledgeResults, ...captureResults].every(entry => entry.ok), operationId: requestOperationId, clients: knowledgeResults, capture: captureResults });
1361
+ const complete = [...knowledgeResults, ...captureResults].every(entry => entry.ok);
1362
+ if (complete) {
1363
+ const intentStore = require('./integration-intent');
1364
+ const removed = new Set([...knowledgeResults, ...captureResults].map(entry => entry.client));
1365
+ for (const client of removed) intentStore.markIntent(runtime.dataPath, client, 'disconnected');
1366
+ }
1367
+ return send(res, 200, { ok: true, complete, operationId: requestOperationId, clients: knowledgeResults, capture: captureResults });
1346
1368
  }
1347
1369
 
1348
1370
  if (method === 'GET' && pathname === '/api/ai-profiles') {
@@ -1532,6 +1554,26 @@ async function startServer(options = {}) {
1532
1554
  runtime.logger.warn('modules.supervisor_failed', 'Module supervisor failed to start.', { error });
1533
1555
  }
1534
1556
 
1557
+ // Startup-event integration repair: one attempt per connected-but-broken
1558
+ // client, failures surfaced and never retried in this run. No timers.
1559
+ try {
1560
+ runtime.integrationAutoRepair = new (require('./integration-auto-repair').IntegrationAutoRepair)({
1561
+ dataDir: runtime.dataPath,
1562
+ logger: runtime.logger,
1563
+ createManager: () => new IntegrationManager({
1564
+ ...options.integrationOptions,
1565
+ rootDir: runtime.rootDir,
1566
+ logger: runtime.logger,
1567
+ bridgeHomeDir: runtime.bridgeAdapter.hookBinding().bridgeHomeDir,
1568
+ }),
1569
+ });
1570
+ runtime.integrationAutoRepair.runOnce().catch(error => {
1571
+ runtime.logger.warn('integrations.auto_repair_skipped', '启动自动修复未完成。', { error });
1572
+ });
1573
+ } catch (error) {
1574
+ runtime.logger.warn('integrations.auto_repair_unavailable', '启动自动修复不可用。', { error });
1575
+ }
1576
+
1535
1577
  let stopping = null;
1536
1578
  const stop = async reason => {
1537
1579
  if (stopping) return stopping;
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## 发布状态
4
4
 
5
- 2026-10-15:最终候选 `5.0.17-rc.3` 验收通过,正在准备正式 `5.0.17` 的独立下载验收,`latest` 仍为 `5.0.16`。本文件不得被解读为正式版已放行。Electron、Windows 安装包和窄屏矩阵均不在本次范围;用户报告故障的另一台电脑尚需更新后回验。npm 内文档是打包时快照,正式包自己的验收结果在测试完成后追加到仓库当前文档。
5
+ 2026-10-15:最终候选 `5.0.17-rc.3` 和正式 `5.0.17` 均已独立下载安装验收通过,官方 npm 的 `latest`、`next` 均为 `5.0.17`,`npm view ...@latest version --prefer-online` 已再次确认。Electron、Windows 安装包和窄屏矩阵不在本次范围;用户报告故障的另一台电脑尚需更新后回验。npm 内文档是打包时快照,正式包自己的验收结果在测试完成后追加到仓库当前文档,未重发或修改同版本 npm 包。
6
6
 
7
7
  ## 交付范围
8
8
 
@@ -96,4 +96,22 @@ GLM 报告分别保存在两个仓库的 `.local-test-data/ui-refresh-2026-10-15
96
96
 
97
97
  ### 正式包
98
98
 
99
- 待补:正式包自己的官方收据、安装版重测及 latest 晋升结果。未填写之前不报告正式交付完成。
99
+ 官方全新安装收据:`D:\SanQian.Xu\.pk-acceptance-temp\kb-registry-product-Vn5JOh\registry-installation.json`,源码提交 `7fac75f63bca0ca9d4c125cff8029c6e8822a4c1`,integrity `sha512-HhFfsEDF4UbZlbSmPyBbnAgvKGd2tgXmhPDXW3H5VGNRGmiN7DfXA2fmwVvqOr/Fe59d4KoQsrdEsOIpsmNJ5Q==`。Workbench `0.13.2-rc.17`、vector-hub `1.0.8-rc.8`、bridge `0.7.2`、vendored engine `0.2.7-rc.10`;包内内容和官方模块 integrity 均核对通过。
100
+
101
+ | 正式验收项 | 实际结果 |
102
+ |---|---|
103
+ | 源码冻结复跑 | 正式提交的 Node 24 外壳 134/134,215568ms;Node 22 134/134 记录保留。Workbench 双 Node 405,engine 双 Node 185 通过/1 跳过;未修改模块也补跑 vector-hub 73、bridge 30,均通过。浏览器自动脚本排除不计通过,真实浏览器另列 |
104
+ | 真进程生命周期 | 正式安装目录 `acceptance-data` 下的 startup/restart/shutdown JSON:四模块启动、独立退出后的守护恢复、大写 STOP、四个子进程正常关闭均通过 |
105
+ | 真实提交闭环 | `智脑桌面验收-E0OL29/real-agent-formal-refresh-acceptance.json`:单提交 `2769908ce00b6b8aeab178e83b6f1bbca0c16871`,双项目 `7f1c331d`、`f5870071` 均晋升、索引并可检索;并行分析区间实际重叠 |
106
+ | 真实向量 | 正式引擎的语义查询包含 10 个校验向量片段、`index:vector-hub` 命中和对应正式提交,不以文本降级冒充向量成功 |
107
+ | 真网页和模型 | 会话 `sess-001c5cbec432`,真实 MiniMax-M3.1-Flash-Preview,76 个文本增量,回答逐字引用新知识原文和完整提交;最后自然 idle、持久化 turn-end,通过后端独立核验 |
108
+ | 刷新与滚轮 | 工作中向上滚动后切到活动页,活动自动显示真实维护“已更新·可检索”;返回工作台内容增长至 2075,阅读位置仍为 0,未发送草稿保留,iframe 只加载一次 |
109
+ | 网络恢复 | 正式页面断网显示连接中断,恢复后重新读取状态,草稿、会话和引用仍存在;仍为 1 WebSocket / 2 订阅,桌面 1436×730、无横向溢出、浏览器控制台无错误 |
110
+ | 完整历史 | 独立替身环境 `智脑桌面验收-TIFVtg`,正式包后端新建 25 轮,分页 20+5;真实网页向上翻阅后 DOM 同时包含第 01 与第 25 轮,共 25 轮。仅此长历史项使用替身,不混入真实模型结果 |
111
+ | 正式发布 | 正式包先发 next,以上验收完成后才执行 dist-tag latest;第一次 npm view 仍命中旧元数据,保留该观察。dist-tag 官方端点随后已为 5.0.17,最后 npm view @latest 也确认为 5.0.17 |
112
+
113
+ 最终证据副本集中在 `project-knowledge-base/.local-test-data/ui-refresh-2026-10-15/`:`formal-registry-installation.json`、`formal-installed-{startup,restart,shutdown}.json`、`formal-real-commits.json`、`formal-real-workbench.json`、`formal-complete-history.json`、`node24-formal-source-pass.md`、`node22-final-pass.md` 与 `glm-final-review.md`。两张正式桌面截图为 `formal-real-workbench.jpg`、`formal-complete-history.jpg`。
114
+
115
+ 相关源码均已推送远程 Git;本轮未更新或重启原 5757 用户实例(仍为 5.0.16),没有修改用户知识、历史、凭据、原生 Hook 信任或第三方通知。请在实际出现故障的电脑按日常方式 stop、安装官方 @latest、检查版本并启动。Windows 若独立 IDE 的 MCP 持有原生库导致 EBUSY,应先退出相应会话后重试,不删除原生文件或清空数据。
116
+
117
+ 本轮网页范围验收完成,不等于对其他设备、私有语料或未保存过的旧历史作无缺陷保证。Electron 与 macOS 原生安装未实施、不计为通过。
@@ -1,24 +1,24 @@
1
1
  {
2
2
  "schema": "project-knowledge/module-content/v1",
3
3
  "productFiles": {
4
- "ui/index.html": "1ad9cd83d1727e98b90391c1ab5ab9b99b5812d1ab9e9262bf1e8f3b49e3331d",
5
- "ui/product.js": "1a5cee1cdebaffe097cef4f908bc7e595b16734a08f7495ed0d52b9b81137f5e",
6
- "ui/product.css": "0c31d4937780b3a277689b4619208ad0e5dc59a84d7e27fc16b5f1815d59b47c",
4
+ "ui/index.html": "737b8f5c745270797950c5975f458a13ffe1ea738f921e814bfbbe5e56813fed",
5
+ "ui/product.js": "da7ea12a394212766cb0994fe0026ab2cccbba866063cdcc4c1662573e3c6dfa",
6
+ "ui/product.css": "4f300b08c086a939831d7ee69df01f23641457c0d606af29e9d4ce8a6d28bc9d",
7
7
  "ui/favicon.svg": "c48be946b224112ebb0238b44263f7a61bda31e531afa895c2e9801c0ff24f32",
8
8
  "plugins/project-knowledge/hooks/claude.json": "d61510327e3e1facbb4c645228e9f50cad05a88b33d80c4c22e5ba9f8b9f5359",
9
9
  "plugins/project-knowledge/hooks/codex.json": "216e03c07295606ef6ebb2c4f669ad0d790b5e7c4f3f34600488033db75e4239",
10
10
  "plugins/project-knowledge/scripts/context-hook.cjs": "da666153ad5723592cce42d25337ae649f7d93e43fab76b3c6d1263933de063f",
11
11
  "plugins/project-knowledge/scripts/mcp-launcher.cjs": "7c68830b5c73eb53ef030756a4de33987f7eab6b1a6cc15602835aac3f954893",
12
- "plugins/project-knowledge/.claude-plugin/plugin.json": "0cc5384cb0c368e79550077587eb64b69a57bcd401af545be2b7c97dae3c13d0",
13
- "plugins/project-knowledge/.codex-plugin/plugin.json": "6968e066cc84b2d91034b1279d90c256415548bc9518fe62ae7f2055ef8e013a",
14
- "plugins/project-knowledge/.mcp.json": "63dab0b2484b49dce2ab4565595e030a7c88e4196bc35a54d14445ff24399921",
12
+ "plugins/project-knowledge/.claude-plugin/plugin.json": "ad8cf459af8242236bb71c4a7e98a24b015415404bb44aedb920fbe0fef943ec",
13
+ "plugins/project-knowledge/.codex-plugin/plugin.json": "ee91251067ce65c54001e915c3960672a595045cf6db4f7c3d56ab7101eff4c0",
14
+ "plugins/project-knowledge/.mcp.json": "9bc8e766dbe8893726fef90e9f909cdc04ae22d25e85b26e517ead72ff69d771",
15
15
  "ui/live-client.js": "0d81fa894c629bb839e1931fc3c5ebad4d831aae8232d6ce8c0f542e9e3559e8"
16
16
  },
17
17
  "modules": {
18
18
  "terminal": {
19
19
  "name": "@sanqianx/claude-ai-workbench",
20
20
  "installName": "claude-ai-workbench",
21
- "version": "0.13.2-rc.17",
21
+ "version": "0.13.2-rc.18",
22
22
  "files": {
23
23
  "LICENSE": "c1b9df1275e769f3dbab000d1e457a2d4b0f28eb5da6c77e48dc37eeba202ed7",
24
24
  "README.md": "43938c8a64397d340dcf633edc40a7cea7dd8a95b5f82cf612b2c15b66c380b6",
@@ -35,20 +35,20 @@
35
35
  "apps/minimal-test-host/README.md": "fa73136f4ee3865c2d91eb97b2e8c03679aa4688e44c1e1c8ec6e27d604b0de5",
36
36
  "docs/sdk/FILE_LAYOUT.md": "17882f96505c8161bfb66876684b9d2d3f50021a111b8b844502d3cb4e97870a",
37
37
  "docs/sdk/INTEGRATION.md": "d4c0c80d2d2fc610205102d703a429bfaa20f9f3a48f1718f5bab02ee90138df",
38
- "docs/sdk/openapi.yaml": "7c7dd11f5b261cf92cd4d64f8b1eeddd56b79f2617fa7236107dc185a02d2ce3",
38
+ "docs/sdk/openapi.yaml": "ece89afdd4e1a58f01f7e8db6581cee91baf2c918d68208e86200ff5abe0ac9d",
39
39
  "docs/sdk/README.md": "7ab591d39c36a3b84cfdf487ff567040ec453975f3ef0fba86dc822094667b61",
40
- "package.json": "20169c89b949300b9f3ba325b989f6a7aabf1a11097e31f898094eb5956863d5",
40
+ "package.json": "cfa3c5e1ca1f5dd4727fd96b3639f4b989a135451549f970047a72dd588be646",
41
41
  "packages/client/index.d.ts": "587a66845052dc5cc03c4f41d678808140ea258fdf914be1c6dd2f6c07c1aff1",
42
42
  "packages/client/index.js": "6c7abce75086b68c18b9fdab372eff8b70d4bb81da6e7e0f46e4af57de0c523d",
43
- "packages/client/package.json": "0b1a9727003087a25b66e5d99a860785d9fcd5d008dbfc514547d791b2cb9e46",
43
+ "packages/client/package.json": "eb65b7e8d9dd1fe21c86b04830e83b1d86ed4f69b3dbf04428b33efb125ab6be",
44
44
  "packages/client/README.md": "342dff9b1dcbc85fccc9b980ed3e4e455aa71133af1d0805b8431ea2ac564ef8",
45
45
  "packages/contracts/index.d.ts": "109ed9559b23f92b3840c484b9dd7f293a46866a3937a52e631e52692bb41927",
46
46
  "packages/contracts/index.js": "5eeb195d9c4cc54dab064510684ae1350d325fbaf47ff824dad4a04a5bb67d11",
47
- "packages/contracts/package.json": "b79a3fdd09c635b2645ecab11ab6e789d1883aeac4f13d35dc11f4f09947808d",
47
+ "packages/contracts/package.json": "cffc5c24c523871b52e4e6b1ab0d3d7b81cfd11c3e1f53f14d681874eaee8d4b",
48
48
  "packages/contracts/README.md": "44d83a3cc508cec6c5c20dfa1f2c08d5ea03e3afb2853639fdc0a9cc8eb5b9c8",
49
49
  "packages/core/index.d.ts": "c95b24a13f8aeeb128497833cf298790563baca6036ea5d29a9d8daff53199d7",
50
50
  "packages/core/index.js": "825640d0edc7dd130f22da1f580d2cda355566a40b0b36e42e5f38909605bbfa",
51
- "packages/core/package.json": "271a20f55ca486cd9a788b857381d8997c96de17029516bacd1dee4a3f1a3573",
51
+ "packages/core/package.json": "935f642db83942faecd1c9276c0f48365f968a6b17c4fd768d93272db891dbcc",
52
52
  "packages/core/README.md": "e222c546104a168f1038d95c6c4efb8e5603edd6145ab2cab79cab977fb710e5",
53
53
  "packages/server/bin/agent-terminal-server.js": "043dff4d76e3fd213b666a55e581ed1e72f9e5b8de6cb798fbf42bd39ed26861",
54
54
  "packages/server/bin/claude-workbench-server.js": "b03c730b6d86faf6974afea26c74109d59b1bbc1ecc656c1d6270258708694ac",
@@ -67,10 +67,10 @@
67
67
  "packages/server/lib/context-resolver.js": "d3060bfe35fb184e521847fc0900cf6114f8ebf3120ce13ed39dfc224af54660",
68
68
  "packages/server/lib/conversation-capture.js": "9f3ee29003eaea4778dc1e4b7e23e868f2ba0268bd43db07317acba62f48a094",
69
69
  "packages/server/lib/diagnostics.js": "b45687fcc07f8baeb33ffd08efdc875494d4e3205887514fdfd7e1ce6370753b",
70
- "packages/server/lib/drivers/cli-json-runner.js": "23704b5ea76564f5200d4088f53afd67a7fd03f17e98f7ebaeebd40a1fc4f2ac",
71
- "packages/server/lib/drivers/codex-driver.js": "74368620dd3357268228ea2cf9a5c1318e3fc3e24849869d4d91c9698d554652",
72
- "packages/server/lib/drivers/opencode-driver.js": "7155d0fcd091ac5ff9ec9275b6694a34321e7d16a5305da4adf080c50d054c0d",
73
- "packages/server/lib/drivers/zcode-driver.js": "bf8fb9d49346aa51207565fb7838ef29b667544119e9e24ec115f3de0e7820f2",
70
+ "packages/server/lib/drivers/cli-json-runner.js": "3802a9066bd78062c40441d43b23bf97333ec742f877c59f4426e4cb118e8e54",
71
+ "packages/server/lib/drivers/codex-driver.js": "a9fb31e5a3a34d24f976e275546721f968e7a8af901019c6d5fab10ebfa5b9e4",
72
+ "packages/server/lib/drivers/opencode-driver.js": "b7e0a338b32c2616fa3ce1fa438be5abc9794218eb1744b3a6eac877122a8c28",
73
+ "packages/server/lib/drivers/zcode-driver.js": "cb25b9d84e1bb8fb2b12aa4add656af2448c090cd99210e9dca9f55657764504",
74
74
  "packages/server/lib/drivers/zcode-profile-runtime.js": "77c5ac83551cc424375918863d4a31039984492c3a1c987a615a072ac7cb1f14",
75
75
  "packages/server/lib/event-stream.js": "3e6cf39b4599a3b1c9897da31d73c28b288a3d57f7a177ed90618de91977d3c4",
76
76
  "packages/server/lib/folder-picker.js": "70f55df999262b90433f2b4251aa69dd70a23b55a84756b5f1d24dbeacfbda31",
@@ -84,8 +84,8 @@
84
84
  "packages/server/lib/session-history.js": "7836167666d8032b9894a241ca42befa33fd034d7c9bd894a6399f57f31a2906",
85
85
  "packages/server/lib/system-doctor.js": "d098a046f425e90ef25209be69d78beefdfd9c2af92f08068a9bfc52053a8f7d",
86
86
  "packages/server/lib/ui-preferences-store.js": "e108cb22e7cc1f3438704913d5401e01de2c0e717eb2abd3bdc4727209393819",
87
- "packages/server/lib/workbench-service.js": "a9cea8a68bee8dd93a705c54d747794fbd47a3694d6194ce739e6030e62da2b6",
88
- "packages/server/package.json": "d59b110305fd9e428d8e3d8240d955ee2d84a1af7ad923d7f19df05392e7b912",
87
+ "packages/server/lib/workbench-service.js": "12477e3556b4c3d49adc3b69c0783f79335d87fbfc727ba5a19c598e2a4118d4",
88
+ "packages/server/package.json": "bf4e1c055e6115c17fe5b3636ac477bd430176aa9650ce5e1013862fae6342c8",
89
89
  "packages/server/README.md": "a34cfaa4f4e859008b44e2be8e1aafb56e97fe83e6129c968a8562945fa843f3",
90
90
  "packages/ui/ai-profile-settings.d.ts": "0551b6c6eda6d18ea89a976daddf01b2161195f4874fe2d67a57cd9ecc6ce643",
91
91
  "packages/ui/ai-profile-settings.js": "e92bbac67a1199d473e4e44b32d1cc76c5b6c79306d4fef9d9067a1bb6ec42ca",
@@ -93,7 +93,7 @@
93
93
  "packages/ui/claude-terminal.js": "96bd217a4a7e4792ae3fbaaf46249ecee13b0f4e01fc6d25a1b8b22d8f8fe00c",
94
94
  "packages/ui/index.d.ts": "e547ab39640fe7519fb367b78442dc8a2c51b2efd17b66f8b0a003b154b36ead",
95
95
  "packages/ui/index.js": "f4394aabd395938f5c3fe587e430471d4033e83945a9b9c7e0149ad4040982c9",
96
- "packages/ui/package.json": "6bc87d45f6df2e1d8de5784dfd5a99b473d35eff72590da026d3bf8fefcccc18",
96
+ "packages/ui/package.json": "f08f2d43068ae70dd8740744f414d45eb3968f4724668ebdbc7d4b1b672d67e2",
97
97
  "packages/ui/README.md": "793c4a9c286fee23f39062a1976ca3afe9d2816dcdb55e156ba940e1ef514f89",
98
98
  "packages/ui/theme.css": "8f03a081272a7db11b196f7a70d0fafb1f8d954778f329f1ba53d110d9d5fd98",
99
99
  "src/assets/claude-prompts.json": "cb111f2e0a45afe8a31c251a504d1ab5375966ed61291660e61fd2efd77f6d9f",