@sanqianx/project-knowledge 5.0.20 → 5.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +12 -1
- package/_site/_test/agent-capture-mcp-ui-test.js +2 -1
- package/_site/_test/agent-capture-ui-test.js +2 -1
- package/_site/_test/fixtures/slow-startup-integration.cjs +12 -0
- package/_site/_test/fixtures/startup-repair-worker.cjs +13 -0
- package/_site/_test/integration-auto-repair-test.js +21 -0
- package/_site/_test/knowledge-panel-lifecycle-test.js +154 -0
- package/_site/_test/knowledge-panel-ui-contract-test.js +110 -112
- package/_site/_test/startup-integration-isolation-test.js +59 -0
- package/_site/_test/startup-integration-repair-test.js +43 -0
- package/_site/_test/workspace-ui-contract-test.js +31 -31
- package/_site/_test/zcode-capture-integration-test.js +4 -0
- package/_site/lib/integration-auto-repair.js +5 -3
- package/_site/lib/integration-repair-worker.js +16 -0
- package/_site/lib/server-app.js +8 -5
- package/_site/lib/startup-integration-repair.js +77 -0
- package/docs/fresh-v5-start.md +2 -0
- package/module-runtime-manifest.json +128 -126
- package/package.json +2 -2
- package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.mcp.json +1 -1
- package/ui/index.html +1 -1
- package/ui/knowledge-panel.html +11 -14
- package/ui/product.css +2 -10
- package/ui/product.js +73 -56
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { StartupIntegrationRepair } = require('../lib/startup-integration-repair');
|
|
7
|
+
const { markIntent } = require('../lib/integration-intent');
|
|
8
|
+
const { isProcessAlive } = require('../lib/runtime-endpoint');
|
|
9
|
+
const workerFile = path.join(__dirname, 'fixtures/startup-repair-worker.cjs');
|
|
10
|
+
async function main() {
|
|
11
|
+
const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-startup-repair-empty-'));
|
|
12
|
+
markIntent(empty, 'codex', 'disconnected');
|
|
13
|
+
const idle = new StartupIntegrationRepair({ dataDir: empty, workerFile: 'does-not-exist' });
|
|
14
|
+
assert.deepEqual(await idle.runOnce(), {}); assert.equal(idle.child, null); await idle.close();
|
|
15
|
+
for (const mode of ['normal', 'manual', 'crash', 'hang']) {
|
|
16
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-startup-repair-'));
|
|
17
|
+
markIntent(dataDir, 'claude', 'connected'); markIntent(dataDir, 'codex', 'connected');
|
|
18
|
+
markIntent(dataDir, 'zcode', 'disconnected');
|
|
19
|
+
const logs = [];
|
|
20
|
+
const repair = new StartupIntegrationRepair({ dataDir, managerOptions: { homeDir: mode }, workerFile,
|
|
21
|
+
timeoutMs: mode === 'hang' ? 150 : 3000, logger: { warn: (...args) => logs.push(args) } });
|
|
22
|
+
try {
|
|
23
|
+
const result = repair.runOnce();
|
|
24
|
+
assert.equal(repair.runOnce(), result, 'one startup event must not spawn duplicate repair jobs');
|
|
25
|
+
if (mode === 'manual') repair.markSucceeded(['claude']);
|
|
26
|
+
const status = await result;
|
|
27
|
+
assert.equal(status.zcode, undefined, 'disconnected clients must not acquire repair state');
|
|
28
|
+
if (mode === 'normal') {
|
|
29
|
+
assert.equal(status.claude.state, 'failed'); assert.equal(status.codex.state, 'repaired');
|
|
30
|
+
repair.markSucceeded(['claude']); assert.equal(repair.status().claude.state, 'repaired');
|
|
31
|
+
} else if (mode === 'manual') {
|
|
32
|
+
assert.equal(status.claude, undefined, 'late startup failure must not overwrite a manual install');
|
|
33
|
+
assert.equal(status.codex.state, 'repaired');
|
|
34
|
+
} else {
|
|
35
|
+
assert.equal(status.claude.state, 'failed'); assert.equal(status.codex.state, 'failed');
|
|
36
|
+
assert.equal(logs.length, 1, 'worker failure must be reported without automatic retries');
|
|
37
|
+
}
|
|
38
|
+
} finally { await repair.close(); }
|
|
39
|
+
assert.equal(isProcessAlive(repair.child.pid), false, 'owned worker must be reaped on close');
|
|
40
|
+
}
|
|
41
|
+
console.log('Startup repair worker PASS: intent isolation, one attempt, manual success precedence, timeout, crash and owned cleanup');
|
|
42
|
+
}
|
|
43
|
+
main().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const assert = require('node:assert/strict');
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
const root = path.resolve(__dirname, '../..');
|
|
6
|
-
const html = fs.readFileSync(path.join(root, 'ui/index.html'), 'utf8');
|
|
7
|
-
const script = fs.readFileSync(path.join(root, 'ui/product.js'), 'utf8');
|
|
8
|
-
const css = fs.readFileSync(path.join(root, 'ui/product.css'), 'utf8');
|
|
9
|
-
for (const view of ['home', 'work', 'search', 'memory', 'projects', 'activity']) {
|
|
10
|
-
assert.equal((html.match(new RegExp(`data-view="${view}"`, 'g')) || []).length, 1);
|
|
11
|
-
assert.ok(html.includes(`id="view-${view}"`));
|
|
12
|
-
}
|
|
13
|
-
assert.equal((html.match(/data-view=/g) || []).length, 6, 'diagnostics must not become another primary product');
|
|
14
|
-
assert.match(html, /<select\b[^>]*id="projectSelect"[^>]*\bdisabled\b/, 'project selection is disabled until the initial registry snapshot has loaded');
|
|
15
|
-
for (const id of ['workFrame', 'vectorFrame', 'bridgeFrame', 'engineFrame', 'importDialog', 'importCode', 'importKnowledge', 'importDomain', 'setupImportRoot', 'createDomain', 'logsDialog', 'systemDrawer', 'copyReference']) assert.equal((html.match(new RegExp(`id="${id}"`, 'g')) || []).length, 1);
|
|
16
|
-
assert.ok(html.includes('data-action="import"') && html.includes('不分析全部历史'));
|
|
17
|
-
assert.ok(!html.includes('srcdoc') && !/data-simulate|data-analyze|data-install-hook/.test(html));
|
|
18
|
-
assert.equal((script.match(/\.src = /g) || []).length, 1, 'frames initialize once, not on project/theme/page changes');
|
|
19
|
-
for (const contract of ['kb:theme', 'kb:select-project', 'kb:scope', 'kb:knowledge', 'kb:ready']) assert.ok(script.includes(contract));
|
|
20
|
-
assert.ok(script.includes('event.origin !== location.origin') && script.includes('contentWindow === event.source'), 'module messages must be from the expected frame and origin');
|
|
21
|
-
assert.ok(script.includes('state.epoch++') && script.includes('referenceEpoch') && script.includes('logEpoch'), 'late project/reference/log feedback must be isolated');
|
|
22
|
-
assert.match(script, /on\('domainForm'[\s\S]*?await refreshRegistry\(true\)/, 'domain writes must invalidate pre-mutation reads');
|
|
23
|
-
assert.match(script, /registered = true;[\s\S]*?await refreshRegistry\(true\)/, 'import must not reuse a pre-import snapshot');
|
|
24
|
-
assert.ok(script.includes(
|
|
25
|
-
assert.ok(script.includes('text.textContent = record.message') && script.includes("'logs/appended'"), 'diagnostics stay inert and stream live');
|
|
26
|
-
assert.ok(script.includes('URL.revokeObjectURL(url)') && script.includes('logStream?.close()'));
|
|
27
|
-
assert.ok(script.includes('resumeImport = true') && script.includes("$('importDialog').showModal()") && script.includes("p.code === 'KNOWLEDGE_ROOT_MISSING'"), 'first import can return from setup without resetting its input');
|
|
28
|
-
assert.ok(css.includes('height:100%') && css.includes('.view.module-view{padding:0;overflow:hidden}') && css.includes('overflow-wrap:anywhere'));
|
|
29
|
-
assert.ok(css.includes('html[data-theme=dark]') && css.includes('prefers-reduced-motion'));
|
|
30
|
-
assert.doesNotThrow(() => new Function(script));
|
|
31
|
-
console.log('workspace UI contract test PASS (source contract; not browser acceptance)');
|
|
1
|
+
'use strict';
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const root = path.resolve(__dirname, '../..');
|
|
6
|
+
const html = fs.readFileSync(path.join(root, 'ui/index.html'), 'utf8');
|
|
7
|
+
const script = fs.readFileSync(path.join(root, 'ui/product.js'), 'utf8');
|
|
8
|
+
const css = fs.readFileSync(path.join(root, 'ui/product.css'), 'utf8');
|
|
9
|
+
for (const view of ['home', 'work', 'search', 'memory', 'projects', 'activity']) {
|
|
10
|
+
assert.equal((html.match(new RegExp(`data-view="${view}"`, 'g')) || []).length, 1);
|
|
11
|
+
assert.ok(html.includes(`id="view-${view}"`));
|
|
12
|
+
}
|
|
13
|
+
assert.equal((html.match(/data-view=/g) || []).length, 6, 'diagnostics must not become another primary product');
|
|
14
|
+
assert.match(html, /<select\b[^>]*id="projectSelect"[^>]*\bdisabled\b/, 'project selection is disabled until the initial registry snapshot has loaded');
|
|
15
|
+
for (const id of ['workFrame', 'vectorFrame', 'bridgeFrame', 'engineFrame', 'importDialog', 'importCode', 'importKnowledge', 'importDomain', 'setupImportRoot', 'createDomain', 'logsDialog', 'systemDrawer', 'copyReference']) assert.equal((html.match(new RegExp(`id="${id}"`, 'g')) || []).length, 1);
|
|
16
|
+
assert.ok(html.includes('data-action="import"') && html.includes('不分析全部历史'));
|
|
17
|
+
assert.ok(!html.includes('srcdoc') && !/data-simulate|data-analyze|data-install-hook/.test(html));
|
|
18
|
+
assert.equal((script.match(/\.src = /g) || []).length, 1, 'frames initialize once, not on project/theme/page changes');
|
|
19
|
+
for (const contract of ['kb:theme', 'kb:select-project', 'kb:scope', 'kb:knowledge', 'kb:ready']) assert.ok(script.includes(contract));
|
|
20
|
+
assert.ok(script.includes('event.origin !== location.origin') && script.includes('contentWindow === event.source'), 'module messages must be from the expected frame and origin');
|
|
21
|
+
assert.ok(script.includes('state.epoch++') && script.includes('referenceEpoch') && script.includes('logEpoch'), 'late project/reference/log feedback must be isolated');
|
|
22
|
+
assert.match(script, /on\('domainForm'[\s\S]*?await refreshRegistry\(true\)/, 'domain writes must invalidate pre-mutation reads');
|
|
23
|
+
assert.match(script, /registered = true;[\s\S]*?await refreshRegistry\(true\)/, 'import must not reuse a pre-import snapshot');
|
|
24
|
+
assert.ok(script.includes("title: String(r.title || r.entryId || '')") && script.includes('esc(m.text)') && script.includes('esc(project.name)'));
|
|
25
|
+
assert.ok(script.includes('text.textContent = record.message') && script.includes("'logs/appended'"), 'diagnostics stay inert and stream live');
|
|
26
|
+
assert.ok(script.includes('URL.revokeObjectURL(url)') && script.includes('logStream?.close()'));
|
|
27
|
+
assert.ok(script.includes('resumeImport = true') && script.includes("$('importDialog').showModal()") && script.includes("p.code === 'KNOWLEDGE_ROOT_MISSING'"), 'first import can return from setup without resetting its input');
|
|
28
|
+
assert.ok(css.includes('height:100%') && css.includes('.view.module-view{padding:0;overflow:hidden}') && css.includes('overflow-wrap:anywhere'));
|
|
29
|
+
assert.ok(css.includes('html[data-theme=dark]') && css.includes('prefers-reduced-motion'));
|
|
30
|
+
assert.doesNotThrow(() => new Function(script));
|
|
31
|
+
console.log('workspace UI contract test PASS (source contract; not browser acceptance)');
|
|
@@ -59,6 +59,8 @@ const { runCli } = require('../../bin/project-knowledge-integrations');
|
|
|
59
59
|
assert.equal(fs.readFileSync(configFile, 'utf8'), untouched);
|
|
60
60
|
|
|
61
61
|
const runtime = createRuntime({ dataDir: layout.getDataDir(), logger, analysisOwner: 'engine', bridgeAdapter: new BridgeAdapter({ dataDir: layout.getDataDir(), bridgeHomeDir }) });
|
|
62
|
+
let startupRepairStops = 0;
|
|
63
|
+
runtime.integrationAutoRepair = { close: async () => { startupRepairStops++; }, status: () => ({}) };
|
|
62
64
|
server = http.createServer(createRequestHandler(runtime, { integrationOptions: options }));
|
|
63
65
|
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
64
66
|
const base = `http://127.0.0.1:${server.address().port}`;
|
|
@@ -68,6 +70,7 @@ const { runCli } = require('../../bin/project-knowledge-integrations');
|
|
|
68
70
|
return response.json();
|
|
69
71
|
};
|
|
70
72
|
const apiInstall = await request('/api/integrations/install', { clients: ['zcode'], captureOnly: true });
|
|
73
|
+
assert.equal(startupRepairStops, 1, 'manual install cancels concurrent startup repair');
|
|
71
74
|
assert.equal(apiInstall.complete, true);
|
|
72
75
|
assert(apiInstall.operationId);
|
|
73
76
|
const apiStatus = await request('/api/integrations/status');
|
|
@@ -82,6 +85,7 @@ const { runCli } = require('../../bin/project-knowledge-integrations');
|
|
|
82
85
|
assert(errors.some(entry => entry.operationId === failed.operationId && entry.context.client === 'zcode' && entry.error.message.includes('JSON')), 'actual failed phase and operation ID are persisted');
|
|
83
86
|
fs.writeFileSync(configFile, JSON.stringify(twice));
|
|
84
87
|
const removed = await request('/api/integrations/uninstall', { clients: ['zcode'], captureOnly: true });
|
|
88
|
+
assert.equal(startupRepairStops, 3, 'manual uninstall cancels startup repair before removing client hooks');
|
|
85
89
|
assert.equal(removed.complete, true);
|
|
86
90
|
assert.deepEqual(removed.clients, [], 'capture-only removal does not invoke knowledge plugin operations');
|
|
87
91
|
assert.equal(removed.capture[0].state, 'removed');
|
|
@@ -44,10 +44,12 @@ class IntegrationAutoRepair {
|
|
|
44
44
|
async runOnce() {
|
|
45
45
|
const intent = readIntent(this.dataDir);
|
|
46
46
|
if (!intent) return this.status();
|
|
47
|
-
const
|
|
47
|
+
const clients = Object.entries(intent.clients).filter(([, state]) => state === 'connected').map(([client]) => client);
|
|
48
|
+
if (!clients.length) return this.status();
|
|
49
|
+
const status = await this.createManager().statusAll({ clients });
|
|
48
50
|
for (const entry of status?.clients || []) {
|
|
49
|
-
if (
|
|
50
|
-
if (entry.developmentCapture?.installed
|
|
51
|
+
if (readIntent(this.dataDir)?.clients[entry.client] !== 'connected') continue;
|
|
52
|
+
if (entry.developmentCapture?.installed !== false) continue;
|
|
51
53
|
if (this.attempted.has(entry.client)) continue;
|
|
52
54
|
await this._attempt(entry.client);
|
|
53
55
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { IntegrationManager } = require('./integration-installer');
|
|
3
|
+
const { IntegrationAutoRepair } = require('./integration-auto-repair');
|
|
4
|
+
process.once('message', async input => {
|
|
5
|
+
const logger = Object.fromEntries(['info', 'warn', 'error'].map(level => [level, (...args) => {
|
|
6
|
+
if (process.connected) process.send({ kind: 'log', level, args });
|
|
7
|
+
}]));
|
|
8
|
+
try {
|
|
9
|
+
const repair = new IntegrationAutoRepair({ dataDir: input.dataDir, logger,
|
|
10
|
+
createManager: () => new IntegrationManager({ ...input.managerOptions, logger }) });
|
|
11
|
+
const results = await repair.runOnce();
|
|
12
|
+
process.send({ ok: true, results }, () => process.disconnect());
|
|
13
|
+
} catch {
|
|
14
|
+
process.send({ ok: false }, () => process.disconnect());
|
|
15
|
+
}
|
|
16
|
+
});
|
package/_site/lib/server-app.js
CHANGED
|
@@ -1332,6 +1332,7 @@ function createRequestHandler(runtime, options = {}) {
|
|
|
1332
1332
|
if (method === 'POST' && pathname === '/api/integrations/install') {
|
|
1333
1333
|
runtime.integrationStatusScan?.invalidate();
|
|
1334
1334
|
const body = await readJsonBody(req);
|
|
1335
|
+
await runtime.integrationAutoRepair?.close();
|
|
1335
1336
|
const manager = new IntegrationManager({ ...options.integrationOptions, rootDir: runtime.rootDir, logger: runtime.logger, operationId: requestOperationId, bridgeHomeDir: runtime.bridgeAdapter.hookBinding().bridgeHomeDir });
|
|
1336
1337
|
const clients = Array.isArray(body.clients) && body.clients.length
|
|
1337
1338
|
? body.clients.map(client => String(client))
|
|
@@ -1355,6 +1356,7 @@ function createRequestHandler(runtime, options = {}) {
|
|
|
1355
1356
|
}
|
|
1356
1357
|
if (method === 'POST' && pathname === '/api/integrations/uninstall') {
|
|
1357
1358
|
const body = await readJsonBody(req);
|
|
1359
|
+
await runtime.integrationAutoRepair?.close();
|
|
1358
1360
|
const manager = new IntegrationManager({ ...options.integrationOptions, rootDir: runtime.rootDir, logger: runtime.logger, operationId: requestOperationId, bridgeHomeDir: runtime.bridgeAdapter.hookBinding().bridgeHomeDir });
|
|
1359
1361
|
if (body.disableAllCapture === true) {
|
|
1360
1362
|
const summary = await manager.disableAllCapture({});
|
|
@@ -1563,17 +1565,17 @@ async function startServer(options = {}) {
|
|
|
1563
1565
|
}
|
|
1564
1566
|
|
|
1565
1567
|
// Startup-event integration repair: one attempt per connected-but-broken
|
|
1566
|
-
// client, failures surfaced and never retried in this run.
|
|
1568
|
+
// client, failures surfaced and never retried in this run. Native checks
|
|
1569
|
+
// run in an owned worker so they cannot block HTTP or launcher readiness.
|
|
1567
1570
|
try {
|
|
1568
|
-
runtime.integrationAutoRepair = new (require('./integration-
|
|
1571
|
+
runtime.integrationAutoRepair = new (require('./startup-integration-repair').StartupIntegrationRepair)({
|
|
1569
1572
|
dataDir: runtime.dataPath,
|
|
1570
1573
|
logger: runtime.logger,
|
|
1571
|
-
|
|
1574
|
+
managerOptions: {
|
|
1572
1575
|
...options.integrationOptions,
|
|
1573
1576
|
rootDir: runtime.rootDir,
|
|
1574
|
-
logger: runtime.logger,
|
|
1575
1577
|
bridgeHomeDir: runtime.bridgeAdapter.hookBinding().bridgeHomeDir,
|
|
1576
|
-
}
|
|
1578
|
+
},
|
|
1577
1579
|
});
|
|
1578
1580
|
runtime.integrationAutoRepair.runOnce().catch(error => {
|
|
1579
1581
|
runtime.logger.warn('integrations.auto_repair_skipped', '启动自动修复未完成。', { error });
|
|
@@ -1587,6 +1589,7 @@ async function startServer(options = {}) {
|
|
|
1587
1589
|
if (stopping) return stopping;
|
|
1588
1590
|
stopping = (async () => {
|
|
1589
1591
|
runtime.integrationStatusScan?.close();
|
|
1592
|
+
await runtime.integrationAutoRepair?.close();
|
|
1590
1593
|
clearInterval(maintenanceTimer);
|
|
1591
1594
|
clearInterval(configurationTimer);
|
|
1592
1595
|
clearTimeout(readyTimer); runtime.moduleBridge.onReady = null;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { fork, spawn } = require('node:child_process');
|
|
4
|
+
const { readIntent } = require('./integration-intent');
|
|
5
|
+
|
|
6
|
+
// Native client commands can block for minutes. Keep the whole startup
|
|
7
|
+
// status/repair operation outside the HTTP and launcher readiness process.
|
|
8
|
+
class StartupIntegrationRepair {
|
|
9
|
+
constructor({ dataDir, managerOptions = {}, logger, workerFile, timeoutMs = 120000 } = {}) {
|
|
10
|
+
this.dataDir = dataDir; this.managerOptions = managerOptions; this.logger = logger;
|
|
11
|
+
this.workerFile = workerFile || path.join(__dirname, 'integration-repair-worker.js');
|
|
12
|
+
this.timeoutMs = timeoutMs; this.results = {}; this.manualSuccess = new Set();
|
|
13
|
+
this.closed = false; this.promise = null; this.child = null;
|
|
14
|
+
}
|
|
15
|
+
status() { return { ...this.results }; }
|
|
16
|
+
markSucceeded(clients) {
|
|
17
|
+
for (const client of clients || []) {
|
|
18
|
+
this.manualSuccess.add(client);
|
|
19
|
+
if (this.results[client]?.state === 'failed') this.results[client] = { state: 'repaired', at: new Date().toISOString(), error: null };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
runOnce() {
|
|
23
|
+
if (this.promise) return this.promise;
|
|
24
|
+
const clients = Object.entries(readIntent(this.dataDir)?.clients || {}).filter(([, state]) => state === 'connected').map(([client]) => client);
|
|
25
|
+
if (this.closed || !clients.length) return Promise.resolve(this.status());
|
|
26
|
+
this.promise = new Promise(resolve => {
|
|
27
|
+
const child = fork(this.workerFile, [], { stdio: ['ignore', 'ignore', 'ignore', 'ipc'], windowsHide: true, detached: process.platform !== 'win32' });
|
|
28
|
+
this.child = child;
|
|
29
|
+
let settled = false;
|
|
30
|
+
const finish = (results, code) => {
|
|
31
|
+
if (settled) return; settled = true; clearTimeout(timer);
|
|
32
|
+
if (!this.closed) {
|
|
33
|
+
for (const [client, result] of Object.entries(results || {})) {
|
|
34
|
+
if (clients.includes(client) && !this.manualSuccess.has(client)) this.results[client] = result;
|
|
35
|
+
}
|
|
36
|
+
if (code) {
|
|
37
|
+
for (const client of clients) if (!this.manualSuccess.has(client)) this.results[client] = { state: 'failed', at: new Date().toISOString(), error: '启动接入检查未完成,请在 Agent 接入中重试。' };
|
|
38
|
+
Promise.resolve(this.logger?.warn?.('integrations.auto_repair_skipped', '启动接入检查未完成,主界面继续可用。', { context: { code, clients } })).catch(() => {});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
resolve(this.status());
|
|
42
|
+
};
|
|
43
|
+
const timer = setTimeout(() => { void this.terminate().catch(() => {}); finish(null, 'INTEGRATION_REPAIR_TIMEOUT'); }, this.timeoutMs);
|
|
44
|
+
child.on('message', message => {
|
|
45
|
+
if (message?.kind === 'log') {
|
|
46
|
+
if (['info', 'warn', 'error'].includes(message.level) && Array.isArray(message.args) && /^integrations?\./.test(message.args[0] || '')) {
|
|
47
|
+
Promise.resolve(this.logger?.[message.level]?.(...message.args)).catch(() => {});
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
finish(message?.results, message?.ok ? null : 'INTEGRATION_REPAIR_FAILED');
|
|
52
|
+
});
|
|
53
|
+
child.once('error', () => finish(null, 'INTEGRATION_REPAIR_SPAWN_FAILED'));
|
|
54
|
+
child.once('exit', () => finish(null, this.closed ? null : 'INTEGRATION_REPAIR_EXITED'));
|
|
55
|
+
const allowed = ['rootDir', 'homeDir', 'version', 'packageSpec', 'marketplaceSource', 'codexMarketplaceSource', 'bridgeHomeDir', 'bridgeConsumerName', 'claudeSettingsFile', 'codexConfigFile', 'zcodeConfigFile', 'openCodePluginsDir', 'env'];
|
|
56
|
+
child.send({ dataDir: this.dataDir, managerOptions: Object.fromEntries(allowed.filter(key => this.managerOptions[key] !== undefined).map(key => [key, this.managerOptions[key]])) }, error => { if (error) finish(null, 'INTEGRATION_REPAIR_SEND_FAILED'); });
|
|
57
|
+
});
|
|
58
|
+
return this.promise;
|
|
59
|
+
}
|
|
60
|
+
terminate() {
|
|
61
|
+
const child = this.child;
|
|
62
|
+
if (!child?.pid || child.exitCode !== null || child.signalCode !== null) return Promise.resolve();
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const timer = setTimeout(() => reject(new Error('启动接入检查进程尚未停止。')), 5000);
|
|
65
|
+
child.once('exit', () => { clearTimeout(timer); resolve(); });
|
|
66
|
+
if (process.platform === 'win32') {
|
|
67
|
+
// Only this live worker and its native probe children are owned here.
|
|
68
|
+
const killer = spawn('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
|
|
69
|
+
killer.once('error', () => child.kill());
|
|
70
|
+
} else {
|
|
71
|
+
try { process.kill(-child.pid, 'SIGTERM'); } catch { child.kill('SIGTERM'); }
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async close() { this.closed = true; await this.terminate(); }
|
|
76
|
+
}
|
|
77
|
+
module.exports = { StartupIntegrationRepair };
|
package/docs/fresh-v5-start.md
CHANGED
|
@@ -15,6 +15,8 @@ project-knowledge
|
|
|
15
15
|
|
|
16
16
|
5.0.6 修复首次启动被未登记的旧工作台会话误拦截:仅有日志、默认提示词及孤立会话的目录可以创建空的新登记,孤立会话保留原字节但不会恢复或触发任务。实际旧项目、设置、引擎状态或维护任务仍拒绝迁移,错误列出具体目录和冲突项。后台启动命令等待 HTTP 就绪后才报成功;启动失败直接在终端显示原因并返回非零退出码,不提前打开浏览器。
|
|
17
17
|
|
|
18
|
+
5.0.22 将启动时的 Agent 接入检查和自动修复放入独立进程,慢客户端命令不再占住后台就绪通知、网页或实例控制。检查失败可在 Agent 接入中重试,停止服务会同时结束自有检查进程;无需清空数据。若仍提示启动未完成,请同时保留 `project-knowledge --version`、`project-knowledge status` 和报错所指 `launcher.log` 的末尾记录,以区分进程失败与慢启动。
|
|
19
|
+
|
|
18
20
|
在“系统状态”重新配置模型与后台默认模型、embedding 和知识根目录,然后在“领域与项目”导入代码目录及新的空知识目录。原代码目录无需移动。若残留旧知识库托管 Hook,导入预检会要求明确确认替换;不会覆盖第三方 Hook。导入建立 HEAD 基线,不分析已有历史。
|
|
19
21
|
|
|
20
22
|
如果模块是自己独立启动的旧进程,请在它原来的终端中停止后重启新版外壳。外壳不会擅自杀死其他服务;版本或运行期鉴权不一致时显示具体连接阶段,不接管旧服务数据。
|