@sanqianx/project-knowledge 4.8.0 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +32 -0
- package/README.md +11 -6
- package/_modules/knowledge-engine/README.md +9 -6
- package/_modules/knowledge-engine/package.json +1 -1
- package/_modules/knowledge-engine/src/coordinator.js +11 -3
- package/_modules/knowledge-engine/src/server.js +9 -10
- package/_modules/knowledge-engine/src/store.js +9 -12
- package/_modules/knowledge-engine/ui/app.js +1 -1
- package/_site/_test/fresh-hook-reimport-test.js +53 -0
- package/_site/_test/fresh-product-data-test.js +42 -0
- package/_site/_test/git-path-compatibility-test.js +80 -0
- package/_site/_test/module-model-configuration-test.js +2 -1
- package/_site/_test/product-diagnostics-ui-test.js +17 -3
- package/_site/_test/product-import-ui-test.js +12 -0
- package/_site/_test/tracking-start-test.js +1 -0
- package/_site/lib/data-dir.js +13 -81
- package/_site/lib/engine-connection.js +67 -0
- package/_site/lib/git-path.js +14 -0
- package/_site/lib/hook-manager.js +5 -5
- package/_site/lib/integration-installer.js +2 -1
- package/_site/lib/module-bridge.js +6 -16
- package/_site/lib/post-commit-automation.js +1 -1
- package/_site/lib/product-data.js +24 -0
- package/_site/lib/project-lifecycle-service.js +29 -8
- package/_site/lib/requirement-recorder.js +2 -2
- package/_site/lib/server-app.js +13 -24
- package/_site/scripts/hook-trigger.js +3 -2
- package/docs/README.zh-CN.md +8 -4
- package/docs/fresh-v5-start.md +34 -0
- package/module-runtime-manifest.json +5 -5
- package/package.json +3 -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 +3 -2
- package/ui/product.js +57 -22
- package/vendor-manifest.json +7 -8
- package/_modules/knowledge-engine/src/migration.js +0 -88
- package/_site/_test/data-dir-migration-test.js +0 -77
- package/_site/_test/knowledge-migration-test.js +0 -75
- package/_site/_test/legacy-forward-compat-test.js +0 -257
- package/_site/_test/legacy-project-upgrade-e2e-test.js +0 -363
- package/_site/_test/p0-data-migration-characterization-test.js +0 -37
- package/_site/_test/p0-e2e-gate-test.js +0 -347
- package/_site/_test/project-layout-v2-migration-test.js +0 -127
- package/_site/_test/server-runtime-migration-safety-test.js +0 -27
- package/_site/_test/v4122-upgrade-data-contract-test.js +0 -56
- package/_site/lib/data-state-classifier.js +0 -40
- package/_site/lib/engine-cutover.js +0 -97
- package/_site/lib/legacy-data-manifest.js +0 -26
- package/_site/lib/migration-service.js +0 -442
package/_site/lib/data-dir.js
CHANGED
|
@@ -1,81 +1,13 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
function ensureDataDir(dataDir = resolveDataDirPath()) { fs.mkdirSync(dataDir, { recursive: true }); return path.resolve(dataDir); }
|
|
16
|
-
function getDataDir() { if (!_resolved) _resolved = resolveDataDirPath(); return _resolved; }
|
|
17
|
-
function _resetCache() { _resolved = null; }
|
|
18
|
-
function fileDigest(filePath) { return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex'); }
|
|
19
|
-
function directoryDigest(root) {
|
|
20
|
-
const hash = crypto.createHash('sha256');
|
|
21
|
-
const walk = current => {
|
|
22
|
-
for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
23
|
-
const filePath = path.join(current, entry.name);
|
|
24
|
-
const relative = path.relative(root, filePath).replace(/\\/g, '/');
|
|
25
|
-
hash.update(`${entry.isDirectory() ? 'd' : 'f'}:${relative}\0`);
|
|
26
|
-
if (entry.isDirectory()) walk(filePath); else if (entry.isFile()) hash.update(fs.readFileSync(filePath));
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
walk(root); return hash.digest('hex');
|
|
30
|
-
}
|
|
31
|
-
function isEmptyAsset(filePath, asset) {
|
|
32
|
-
if (!fs.existsSync(filePath)) return true;
|
|
33
|
-
if (asset.kind === 'dir') return fs.readdirSync(filePath).length === 0;
|
|
34
|
-
if (fs.statSync(filePath).size === 0) return true;
|
|
35
|
-
if (!filePath.endsWith('.json')) return false;
|
|
36
|
-
try { const raw = fs.readFileSync(filePath, 'utf8'); return isEmptyJson(JSON.parse(raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw)); } catch { return false; }
|
|
37
|
-
}
|
|
38
|
-
function assetsEqual(source, target, asset) { return asset.kind === 'dir' ? directoryDigest(source) === directoryDigest(target) : fileDigest(source) === fileDigest(target); }
|
|
39
|
-
function copyDir(source, target) {
|
|
40
|
-
fs.mkdirSync(target, { recursive: true });
|
|
41
|
-
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
42
|
-
const from = path.join(source, entry.name); const to = path.join(target, entry.name);
|
|
43
|
-
if (entry.isDirectory()) copyDir(from, to); else if (entry.isFile()) fs.copyFileSync(from, to);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function migrateFromLegacy({ legacyRoot, logger } = {}) {
|
|
48
|
-
if (process.env.KB_SKIP_MIGRATION === '1') return { ok: true, migrated: false, reason: 'skipped via KB_SKIP_MIGRATION=1' };
|
|
49
|
-
if (!legacyRoot || !fs.existsSync(legacyRoot)) return { ok: true, migrated: false, reason: legacyRoot ? 'legacy root does not exist' : 'no legacy root provided' };
|
|
50
|
-
const dataDir = getDataDir();
|
|
51
|
-
if (path.resolve(legacyRoot) === dataDir) return { ok: true, migrated: false, reason: 'legacy root equals data dir' };
|
|
52
|
-
// Packaged application roots may include empty scaffolding such as
|
|
53
|
-
// `projects/`. It is not user data and must never conflict with populated
|
|
54
|
-
// runtime data during startup relocation.
|
|
55
|
-
const sources = LEGACY_ASSETS.filter(asset => {
|
|
56
|
-
const source = assetPath(legacyRoot, asset);
|
|
57
|
-
return fs.existsSync(source) && !isEmptyAsset(source, asset);
|
|
58
|
-
});
|
|
59
|
-
if (!sources.length) return { ok: true, migrated: false, reason: 'no legacy assets found' };
|
|
60
|
-
const conflicts = [];
|
|
61
|
-
for (const asset of sources) {
|
|
62
|
-
const source = assetPath(legacyRoot, asset); const target = assetPath(dataDir, asset, 'target');
|
|
63
|
-
if (fs.existsSync(target) && !isEmptyAsset(target, asset) && !assetsEqual(source, target, asset)) conflicts.push(asset.target);
|
|
64
|
-
}
|
|
65
|
-
if (conflicts.length) return { ok: false, migrated: false, requiresManualRecovery: true, reason: 'legacy-data-conflict', conflicts, source: legacyRoot, target: dataDir };
|
|
66
|
-
ensureDataDir(dataDir);
|
|
67
|
-
const result = { ok: true, migrated: false, files: 0, dirs: 0, source: legacyRoot, target: dataDir };
|
|
68
|
-
try {
|
|
69
|
-
for (const asset of sources) {
|
|
70
|
-
const source = assetPath(legacyRoot, asset); const target = assetPath(dataDir, asset, 'target');
|
|
71
|
-
if (fs.existsSync(target) && assetsEqual(source, target, asset)) continue;
|
|
72
|
-
if (asset.kind === 'dir') { if (fs.existsSync(target)) fs.rmSync(target, { recursive: true, force: true }); copyDir(source, target); result.dirs++; }
|
|
73
|
-
else { fs.mkdirSync(path.dirname(target), { recursive: true }); fs.copyFileSync(source, target); result.files++; }
|
|
74
|
-
}
|
|
75
|
-
result.migrated = result.files > 0 || result.dirs > 0;
|
|
76
|
-
if (result.migrated && logger) logger(`migrated runtime data to ${dataDir} (${result.files} files, ${result.dirs} dirs from ${legacyRoot})`);
|
|
77
|
-
return result;
|
|
78
|
-
} catch (error) { return { ...result, ok: false, error: `failed to relocate legacy runtime data: ${error.message}` }; }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
module.exports = { resolveDataDirPath, ensureDataDir, getDataDir, migrateFromLegacy, LEGACY_ASSETS, _resetCache };
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
let resolved = null;
|
|
6
|
+
function resolveDataDirPath(options = {}) {
|
|
7
|
+
const configured = options.dataDir === undefined ? process.env.KB_DATA_DIR : options.dataDir;
|
|
8
|
+
return path.resolve(configured || path.join(options.homeDir || os.homedir(), '.project-knowledge-v5'));
|
|
9
|
+
}
|
|
10
|
+
function ensureDataDir(dataDir = resolveDataDirPath()) { fs.mkdirSync(dataDir, { recursive: true }); return path.resolve(dataDir); }
|
|
11
|
+
function getDataDir() { if (!resolved) resolved = resolveDataDirPath(); return resolved; }
|
|
12
|
+
function _resetCache() { resolved = null; }
|
|
13
|
+
module.exports = { resolveDataDirPath, ensureDataDir, getDataDir, _resetCache };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const { DomainError } = require('./contracts');
|
|
2
|
+
const { execGit } = require('./git-runner');
|
|
3
|
+
|
|
4
|
+
async function connectEngine(runtime) {
|
|
5
|
+
if (runtime.analysisOwner === 'legacy' || runtime.engineConnecting) return;
|
|
6
|
+
runtime.engineConnecting = true;
|
|
7
|
+
let step = 'engine-health';
|
|
8
|
+
let projectId = '';
|
|
9
|
+
let httpStatus = 0;
|
|
10
|
+
try {
|
|
11
|
+
const bridge = runtime.moduleBridge;
|
|
12
|
+
if (bridge.engineCompatible === undefined) bridge.engineCompatible = false;
|
|
13
|
+
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${bridge.runtimeToken}` };
|
|
14
|
+
const health = await bridge._fetch(`${bridge.engineUrl}/api/health`);
|
|
15
|
+
httpStatus = health.status;
|
|
16
|
+
if (!health.ok) throw new Error('ENGINE_UNAVAILABLE');
|
|
17
|
+
const engine = await health.json();
|
|
18
|
+
if (engine.name !== 'knowledge-engine' || engine.dataSchemaVersion !== 3) throw new Error('ENGINE_VERSION_MISMATCH');
|
|
19
|
+
step = 'runtime-authentication';
|
|
20
|
+
const auth = await bridge._fetch(`${bridge.engineUrl}/api/runtime-auth`, { headers });
|
|
21
|
+
httpStatus = auth.status;
|
|
22
|
+
if (!auth.ok) throw new Error('RUNTIME_AUTH_REQUIRED');
|
|
23
|
+
bridge.engineCompatible = true;
|
|
24
|
+
step = 'project-wiring';
|
|
25
|
+
for (const id of runtime.registryStore.listIds()) {
|
|
26
|
+
projectId = id;
|
|
27
|
+
const config = runtime.projectStore.readConfig(id);
|
|
28
|
+
const modules = await bridge.registerProjectLinks({ projectId: id, name: config.displayName, workspacePath: config.repoPath, knowledgePath: config.knowledgePath });
|
|
29
|
+
if (!modules.engine.ok || !modules.eventBridge.ok) throw new Error('PROJECT_WIRING_PENDING');
|
|
30
|
+
}
|
|
31
|
+
projectId = '';
|
|
32
|
+
step = 'runtime-configuration';
|
|
33
|
+
await bridge.syncEngineConfiguration();
|
|
34
|
+
step = 'engine-activation';
|
|
35
|
+
const activated = await bridge._fetch(`${bridge.engineUrl}/api/control`, { method: 'POST', headers, body: JSON.stringify({ enabled: true }) });
|
|
36
|
+
httpStatus = activated.status;
|
|
37
|
+
if (!activated.ok) throw new Error(activated.status === 403 ? 'RUNTIME_AUTH_REQUIRED' : 'ENGINE_ACTIVATION_FAILED');
|
|
38
|
+
runtime.ownership = { owner: 'engine', ready: true };
|
|
39
|
+
} catch (error) {
|
|
40
|
+
const codes = ['ENGINE_UNAVAILABLE', 'ENGINE_VERSION_MISMATCH', 'PROJECT_WIRING_PENDING', 'RUNTIME_AUTH_REQUIRED', 'ENGINE_ACTIVATION_FAILED'];
|
|
41
|
+
const code = codes.includes(error.message) ? error.message : 'ENGINE_CONNECTION_FAILED';
|
|
42
|
+
if (['ENGINE_VERSION_MISMATCH', 'RUNTIME_AUTH_REQUIRED'].includes(code)) runtime.moduleBridge.engineCompatible = false;
|
|
43
|
+
const messages = {
|
|
44
|
+
ENGINE_VERSION_MISMATCH: '检测到旧版知识引擎,请停止旧版服务后重新启动新版。',
|
|
45
|
+
RUNTIME_AUTH_REQUIRED: '引擎服务与外壳鉴权不一致,请停止旧版服务后重新启动新版。',
|
|
46
|
+
PROJECT_WIRING_PENDING: '项目接线未完成,请检查模块连接。',
|
|
47
|
+
};
|
|
48
|
+
const message = messages[code] || '知识引擎连接未完成,服务恢复后自动重试。';
|
|
49
|
+
const previous = runtime.ownership?.error;
|
|
50
|
+
runtime.ownership = { owner: 'engine', ready: false, message, error: { code, step, httpStatus, ...(projectId ? { projectId } : {}) } };
|
|
51
|
+
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 } });
|
|
52
|
+
} finally { runtime.engineConnecting = false; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function forwardCommitToBridge(runtime, event) {
|
|
56
|
+
const config = runtime.projectStore.readConfig(event.projectId);
|
|
57
|
+
if (!runtime.layout.pathsEqual(config.repoPath, event.repoRoot) || !/^[a-f0-9]{40}$/i.test(event.head || '')) throw new DomainError('INVALID_ARGUMENT', '提交通知与项目绑定不匹配。');
|
|
58
|
+
const exists = await execGit(config.repoPath, ['cat-file', '-e', `${event.head}^{commit}`]);
|
|
59
|
+
if (!exists.ok) throw new DomainError('INVALID_ARGUMENT', '提交不属于当前仓库。');
|
|
60
|
+
const identity = await runtime.bridgeAdapter.resolveRepoIdentity(config.repoPath);
|
|
61
|
+
const result = await runtime.bridgeAdapter.appendCommitBoundary({ projectId: event.projectId, repoIdentity: identity.repoIdentity, commitSha: event.head, branch: event.branch || null, operationId: event.operationId });
|
|
62
|
+
if (result.status !== 'captured') throw new DomainError('MODULE_UNREACHABLE', '事件记录失败,请恢复事件模块后重试。', { status: 503 });
|
|
63
|
+
runtime.moduleBridge._fetch(`${runtime.moduleBridge.engineUrl}/api/bridge/notify`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }).catch(() => {});
|
|
64
|
+
return { journalSequence: result.boundary?.journalSequence || result.boundary?.sequence };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { connectEngine, forwardCommitToBridge };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { DomainError } = require('./contracts');
|
|
3
|
+
|
|
4
|
+
function resolveGitPath(cwd, output) {
|
|
5
|
+
const value = String(output || '').trim();
|
|
6
|
+
// rev-parse may echo unsupported flags with exit code 0. Never let a
|
|
7
|
+
// multi-line response become a filesystem path or a repository identity.
|
|
8
|
+
if (!value || /[\r\n\0]/.test(value) || value.startsWith('--')) {
|
|
9
|
+
throw new DomainError('INVALID_ARGUMENT', 'Git 返回了无效的目录路径,请检查 Git 安装。', { status: 400 });
|
|
10
|
+
}
|
|
11
|
+
return path.resolve(cwd, value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { resolveGitPath };
|
|
@@ -3,6 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const { spawnSync } = require('child_process');
|
|
4
4
|
const AtomicFile = require('./atomic-file');
|
|
5
5
|
const { DomainError, validateProjectId } = require('./contracts');
|
|
6
|
+
const { resolveGitPath } = require('./git-path');
|
|
6
7
|
|
|
7
8
|
const HOOK_NAME = 'post-commit';
|
|
8
9
|
const HOOK_MARKER = '# PROJECT-KNOWLEDGE-HOOK ';
|
|
@@ -28,13 +29,12 @@ function runGit(repoPath, args) {
|
|
|
28
29
|
function resolveGitHooksDir(_gitDir, repoPath) {
|
|
29
30
|
if (!repoPath) throw new DomainError('INVALID_ARGUMENT', 'repoPath is required.');
|
|
30
31
|
const absolute = path.resolve(repoPath);
|
|
31
|
-
const hooksPath = runGit(absolute, ['rev-parse', '--
|
|
32
|
-
|
|
33
|
-
return path.resolve(absolute, hooksPath);
|
|
32
|
+
const hooksPath = runGit(absolute, ['rev-parse', '--git-path', 'hooks']);
|
|
33
|
+
return resolveGitPath(absolute, hooksPath);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function repoGitDir(repoPath) {
|
|
37
|
-
try { return runGit(path.resolve(repoPath), ['rev-parse', '--
|
|
37
|
+
try { return resolveGitPath(path.resolve(repoPath), runGit(path.resolve(repoPath), ['rev-parse', '--git-dir'])); }
|
|
38
38
|
catch { return null; }
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -111,7 +111,7 @@ function installHook(options = {}) {
|
|
|
111
111
|
// refusing — otherwise import promises a migration the installer
|
|
112
112
|
// rejects for every repo hooked by an older KB generation.
|
|
113
113
|
}
|
|
114
|
-
if (marker && marker.projectId !== projectId) {
|
|
114
|
+
if (marker && marker.projectId !== projectId && options.replaceManagedHookProjectId !== marker.projectId) {
|
|
115
115
|
throw new DomainError('HOOK_CONFLICT', 'The managed hook belongs to another project.', { status: 409 });
|
|
116
116
|
}
|
|
117
117
|
if (existing === body) return { ok: true, installed: true, updated: false, hookPath, projectId, managedVersion: MANAGED_VERSION };
|
|
@@ -6,6 +6,7 @@ const { spawnSync } = require('child_process');
|
|
|
6
6
|
const crossSpawn = require('cross-spawn');
|
|
7
7
|
const { applyEdits, modify, parse } = require('jsonc-parser');
|
|
8
8
|
const packageInfo = require('../../package.json');
|
|
9
|
+
const { resolveDataDirPath } = require('./data-dir');
|
|
9
10
|
|
|
10
11
|
// Fail-open module resolution: a missing/unloadable Bridge package must
|
|
11
12
|
// degrade Development Capture status, never crash server startup.
|
|
@@ -327,7 +328,7 @@ class IntegrationManager {
|
|
|
327
328
|
this.bridgeConsumerName = options.bridgeConsumerName || DEFAULT_BRIDGE_CONSUMER;
|
|
328
329
|
this.bridgeHomeDir = options.bridgeHomeDir
|
|
329
330
|
|| this.env.AI_CODING_EVENT_BRIDGE_HOME
|
|
330
|
-
|| path.join(this.homeDir, '
|
|
331
|
+
|| path.join(resolveDataDirPath({ dataDir: this.env.KB_DATA_DIR || '', homeDir: this.homeDir }), 'events');
|
|
331
332
|
this.claudeSettingsFile = options.claudeSettingsFile || '';
|
|
332
333
|
this.codexConfigFile = options.codexConfigFile || '';
|
|
333
334
|
this.openCodePluginsDir = options.openCodePluginsDir || '';
|
|
@@ -186,6 +186,7 @@ class ModuleBridge {
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
proxyEngine(req, res, pathname, search) {
|
|
189
|
+
if (this.engineCompatible === false) throw new DomainError('ENGINE_CONNECTION_PENDING', '知识引擎连接尚未确认,请查看系统状态;不会读取旧服务中的知识。', { status: 503 });
|
|
189
190
|
return this.proxy(req, res, `${this.engineUrl}/api/${pathname.replace(/^\/api\/engine\/?/, '')}${search || ''}`);
|
|
190
191
|
}
|
|
191
192
|
|
|
@@ -230,6 +231,7 @@ class ModuleBridge {
|
|
|
230
231
|
}
|
|
231
232
|
|
|
232
233
|
async _listEngineProjects() {
|
|
234
|
+
if (this.engineCompatible === false) return [];
|
|
233
235
|
try {
|
|
234
236
|
const response = await this._fetch(`${this.engineUrl}/api/projects`);
|
|
235
237
|
if (!response.ok) return [];
|
|
@@ -333,16 +335,6 @@ class ModuleBridge {
|
|
|
333
335
|
const response = await this._fetch(`${this.engineUrl}/api/runtime-config`, { method: 'POST', headers, body: JSON.stringify({ projectId: 'default', profile }) });
|
|
334
336
|
status.model = response.ok && !!profile;
|
|
335
337
|
} catch { /* Status projection only: never log resolved profiles or raw errors. */ }
|
|
336
|
-
try {
|
|
337
|
-
const legacyResponse = await this._fetch(`${this.vectorHubUrl}/api/internal/embedding-config`, { method: 'POST', headers, body: '{}' });
|
|
338
|
-
if (legacyResponse.ok) {
|
|
339
|
-
const legacy = await legacyResponse.json();
|
|
340
|
-
if (legacy.settings && /^embedding-[a-f0-9]{64}$/.test(legacy.migrationId || '')) {
|
|
341
|
-
const imported = await this._fetch(`${this.terminalUrl}/api/claude-workbench/v1/internal/import-embedding`, { method: 'POST', headers, body: JSON.stringify(legacy) });
|
|
342
|
-
if (imported.ok) await this._fetch(`${this.vectorHubUrl}/api/internal/retire-embedding`, { method: 'POST', headers, body: JSON.stringify({ migrationId: legacy.migrationId }) });
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
} catch { /* Failed migration retains the module's protected configuration. */ }
|
|
346
338
|
try {
|
|
347
339
|
let settings = null;
|
|
348
340
|
try {
|
|
@@ -462,7 +454,7 @@ class ModuleBridge {
|
|
|
462
454
|
if (!requireEngine) result.engine = { ok: true, status: 'not-owner' };
|
|
463
455
|
else {
|
|
464
456
|
const response = await this._fetch(`${this.engineUrl}/api/projects/${encodeURIComponent(projectId)}`, { method: 'DELETE' });
|
|
465
|
-
if (response.status === 409) throw new DomainError('PROJECT_BUSY', '
|
|
457
|
+
if (response.status === 409) throw new DomainError('PROJECT_BUSY', '此项目正在执行知识维护,请等待写入结束后再解绑。未开始的排队任务会随解绑取消。', { status: 409, retryable: true });
|
|
466
458
|
result.engine = { ok: response.ok || response.status === 404, status: response.ok || response.status === 404 ? 'removed' : 'pending' };
|
|
467
459
|
}
|
|
468
460
|
} catch (error) { if (error.code === 'PROJECT_BUSY') throw error; result.engine = { ok: false, status: 'pending' }; }
|
|
@@ -549,7 +541,7 @@ class ModuleBridge {
|
|
|
549
541
|
name: 'terminal',
|
|
550
542
|
command: `node ${JSON.stringify(terminalEntry)}`,
|
|
551
543
|
url: this.terminalUrl,
|
|
552
|
-
env: { AGENT_TERMINAL_PORT: urlPort(this.terminalUrl, 5760), AGENT_TERMINAL_HOST: urlHost(this.terminalUrl), KB_WORKBENCH_RUNTIME_TOKEN: this.runtimeToken, KB_KNOWLEDGE_ENGINE_URL: this.engineUrl, KB_EVENT_BRIDGE_URL: this.eventBridgeUrl },
|
|
544
|
+
env: { AGENT_TERMINAL_DATA_DIR: path.join(this.dataDir || require('./data-dir').getDataDir(), 'workbench'), AGENT_TERMINAL_PORT: urlPort(this.terminalUrl, 5760), AGENT_TERMINAL_HOST: urlHost(this.terminalUrl), KB_WORKBENCH_RUNTIME_TOKEN: this.runtimeToken, KB_KNOWLEDGE_ENGINE_URL: this.engineUrl, KB_EVENT_BRIDGE_URL: this.eventBridgeUrl },
|
|
553
545
|
});
|
|
554
546
|
} else {
|
|
555
547
|
this.logger.warn('modules.supervisor', 'claude-ai-workbench runtime not installed; expecting an external service.', {});
|
|
@@ -572,7 +564,7 @@ class ModuleBridge {
|
|
|
572
564
|
name: 'vector-hub',
|
|
573
565
|
command: `node ${JSON.stringify(vectorHubEntry)} serve --host ${urlHost(this.vectorHubUrl)} --port ${urlPort(this.vectorHubUrl, 8787)} --root ${JSON.stringify(root)}`,
|
|
574
566
|
url: this.vectorHubUrl,
|
|
575
|
-
env: { VECTOR_HUB_ROOT: root, VECTOR_HUB_CONFIG:
|
|
567
|
+
env: { VECTOR_HUB_ROOT: root, VECTOR_HUB_CONFIG: path.join(root, 'config.json'), VECTOR_HUB_LEGACY_CONFIG: path.join(root, 'no-legacy-config.json'), KB_VECTOR_MANAGED: '1', KB_VECTOR_RUNTIME_TOKEN: this.runtimeToken },
|
|
576
568
|
});
|
|
577
569
|
}
|
|
578
570
|
}
|
|
@@ -589,9 +581,7 @@ class ModuleBridge {
|
|
|
589
581
|
if (!eventBridgeEntry) {
|
|
590
582
|
this.logger.warn('modules.supervisor', '@sanqianx/ai-coding-event-bridge-console runtime not installed; expecting an external service.', {});
|
|
591
583
|
} else {
|
|
592
|
-
const homeArgs = process.env.AI_CODING_EVENT_BRIDGE_HOME
|
|
593
|
-
? ` --home ${JSON.stringify(process.env.AI_CODING_EVENT_BRIDGE_HOME)}`
|
|
594
|
-
: '';
|
|
584
|
+
const homeArgs = ` --home ${JSON.stringify(process.env.AI_CODING_EVENT_BRIDGE_HOME || path.join(this.dataDir || require('./data-dir').getDataDir(), 'events'))}`;
|
|
595
585
|
specs.push({
|
|
596
586
|
name: 'event-bridge',
|
|
597
587
|
command: `node ${JSON.stringify(eventBridgeEntry)} serve --host ${urlHost(this.eventBridgeUrl)} --port ${urlPort(this.eventBridgeUrl, 8790)}${homeArgs}`,
|
|
@@ -27,7 +27,7 @@ async function handlePostCommitEvent(event = {}, deps = {}) {
|
|
|
27
27
|
if (!resolved.registryStore.readDisplaySnapshot(projectId)) throw new DomainError('PROJECT_NOT_FOUND', 'Hook projectId is not registered.', { status: 404 });
|
|
28
28
|
const config = resolved.projectStore.readConfig(projectId);
|
|
29
29
|
const runtimeRoot = path.resolve(runGit(path.resolve(event.repoRoot), ['rev-parse', '--show-toplevel']).stdout);
|
|
30
|
-
const commonDir = path.resolve(runGit(runtimeRoot, ['rev-parse', '--
|
|
30
|
+
const commonDir = path.resolve(runtimeRoot, runGit(runtimeRoot, ['rev-parse', '--git-common-dir']).stdout);
|
|
31
31
|
if (config.repoIdentity && config.repoIdentity.commonDir
|
|
32
32
|
&& !resolved.layout.pathsEqual(config.repoIdentity.commonDir, commonDir)) {
|
|
33
33
|
if (fs.existsSync(config.repoPath)) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const AtomicFile = require('./atomic-file');
|
|
4
|
+
const { DomainError } = require('./contracts');
|
|
5
|
+
|
|
6
|
+
const DATA_SCHEMA = 'project-knowledge/fresh-product-data/v1';
|
|
7
|
+
|
|
8
|
+
function initializeProductData(dataPath) {
|
|
9
|
+
const marker = path.join(dataPath, 'product-data.json');
|
|
10
|
+
if (fs.existsSync(marker)) {
|
|
11
|
+
const value = AtomicFile.readJsonStrict(marker);
|
|
12
|
+
if (value.schema !== DATA_SCHEMA) throw new DomainError('SCHEMA_UNSUPPORTED', '数据目录格式不受支持,请使用新的空目录重新导入项目。', { status: 409 });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
// Old registries and unfinished claims must never become fresh product work.
|
|
16
|
+
const oldData = ['settings.json', 'projects.json', 'projects', 'runtime', 'knowledge-engine', 'workbench', 'vectorhub', 'events', 'knowledge-store.json', 'ai-profiles.json', 'embedding-config.json'].some(name => {
|
|
17
|
+
const file = path.join(dataPath, name);
|
|
18
|
+
return fs.existsSync(file) && (!fs.statSync(file).isDirectory() || fs.readdirSync(file).length > 0);
|
|
19
|
+
});
|
|
20
|
+
if (oldData) throw new DomainError('SCHEMA_UNSUPPORTED', '不再迁移旧版知识库数据。请取消旧 KB_DATA_DIR 设置,或指定新的空数据目录,然后重新导入项目。', { status: 409 });
|
|
21
|
+
AtomicFile.writeJsonAtomic(marker, { schema: DATA_SCHEMA, createdAt: new Date().toISOString() });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { initializeProductData, DATA_SCHEMA };
|
|
@@ -6,6 +6,7 @@ const { DomainError, createId } = require('./contracts');
|
|
|
6
6
|
const AtomicFile = require('./atomic-file');
|
|
7
7
|
const hookManagerDefault = require('./hook-manager');
|
|
8
8
|
const { resolveEffectiveAiProfile } = require('./ai-profile-resolver');
|
|
9
|
+
const { resolveGitPath } = require('./git-path');
|
|
9
10
|
|
|
10
11
|
function slugify(value) {
|
|
11
12
|
const slug = String(value || '').trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
@@ -86,12 +87,12 @@ class ProjectLifecycleService {
|
|
|
86
87
|
checks.git = { ok: false, status: 'non-git', plannedInit: true, message: 'Directory is not a Git repository. Import will run `git init` before proceeding.' };
|
|
87
88
|
} else {
|
|
88
89
|
const head = runGit(localPath, ['rev-parse', '--verify', 'HEAD'], { allowFailure: true });
|
|
89
|
-
const commonDir = runGit(localPath, ['rev-parse', '--
|
|
90
|
+
const commonDir = runGit(localPath, ['rev-parse', '--git-common-dir']);
|
|
90
91
|
const branch = runGit(localPath, ['branch', '--show-current'], { allowFailure: true });
|
|
91
92
|
repoInspection = {
|
|
92
93
|
ok: true,
|
|
93
94
|
repoPath: top.ok ? path.resolve(top.stdout) : path.resolve(localPath),
|
|
94
|
-
commonDir: commonDir.ok ?
|
|
95
|
+
commonDir: commonDir.ok ? resolveGitPath(localPath, commonDir.stdout) : '',
|
|
95
96
|
headCommit: head.ok ? head.stdout : null,
|
|
96
97
|
branch: branch.ok ? branch.stdout : '',
|
|
97
98
|
emptyRepo: !head.ok,
|
|
@@ -163,6 +164,8 @@ class ProjectLifecycleService {
|
|
|
163
164
|
// Legacy v1 hook: import will replace it; that's the migration
|
|
164
165
|
// path (T03 case 2), not a conflict.
|
|
165
166
|
checks.hook = { ok: true, reason: 'legacy-v1' };
|
|
167
|
+
} else if (hookStatus.kbManaged && hookStatus.projectId !== input.projectId) {
|
|
168
|
+
checks.hook = { ok: true, replacementProjectId: hookStatus.projectId };
|
|
166
169
|
} else {
|
|
167
170
|
checks.hook = { ok: true };
|
|
168
171
|
}
|
|
@@ -268,9 +271,9 @@ class ProjectLifecycleService {
|
|
|
268
271
|
journal.gitInitialized = initialized.ok;
|
|
269
272
|
}
|
|
270
273
|
const top = runGit(repoPath, ['rev-parse', '--show-toplevel']).stdout;
|
|
271
|
-
const commonDir = runGit(repoPath, ['rev-parse', '--
|
|
274
|
+
const commonDir = runGit(repoPath, ['rev-parse', '--git-common-dir']).stdout;
|
|
272
275
|
const head = runGit(repoPath, ['rev-parse', '--verify', 'HEAD'], { allowFailure: true });
|
|
273
|
-
return { repoPath: path.resolve(top), commonDir:
|
|
276
|
+
return { repoPath: path.resolve(top), commonDir: resolveGitPath(repoPath, commonDir), head: head.ok ? head.stdout : null };
|
|
274
277
|
}
|
|
275
278
|
|
|
276
279
|
async importProject(input = {}) {
|
|
@@ -354,7 +357,15 @@ class ProjectLifecycleService {
|
|
|
354
357
|
journal.created.metadata = this.layout.getProjectMetadataDir(projectId);
|
|
355
358
|
journal.phase = 'metadata-created';
|
|
356
359
|
this.writeJournal(journal);
|
|
357
|
-
const
|
|
360
|
+
const priorHook = this.hookManager.readHookStatus({ repoPath: git.repoPath });
|
|
361
|
+
if (priorHook.kbManaged && priorHook.projectId !== projectId) {
|
|
362
|
+
if (input.replaceManagedHookProjectId !== priorHook.projectId || this.registryStore.readDisplaySnapshot(priorHook.projectId)) throw new DomainError('HOOK_CONFLICT', '目录中留有另一项目的知识库 Hook。请确认替换旧托管 Hook 后重新导入。', { status: 409 });
|
|
363
|
+
journal.replacedHook = { path: priorHook.hookPath, content: fs.readFileSync(priorHook.hookPath, 'utf8') };
|
|
364
|
+
this.writeJournal(journal);
|
|
365
|
+
}
|
|
366
|
+
journal.created.hookPath = priorHook.hookPath;
|
|
367
|
+
this.writeJournal(journal);
|
|
368
|
+
const hook = this.hookManager.installHook({ repoPath: git.repoPath, projectId, triggerScriptPath: this.triggerScriptPath, dataDir: this.layout.getDataDir(), ...this.bridgeAdapter?.hookBinding?.(), replaceManagedHookProjectId: input.replaceManagedHookProjectId });
|
|
358
369
|
journal.created.hookPath = hook.hookPath;
|
|
359
370
|
journal.phase = 'hook-verified';
|
|
360
371
|
this.writeJournal(journal);
|
|
@@ -366,14 +377,18 @@ class ProjectLifecycleService {
|
|
|
366
377
|
await log.info?.('project.import.completed', 'Project import completed.', { projectId, phase: 'registry-commit', durationMs: Date.now() - Date.parse(journal.startedAt) });
|
|
367
378
|
return { ok: true, operationId, projectId, config: created.config, state: created.state, hook, gitInitialized: journal.gitInitialized === true };
|
|
368
379
|
} catch (error) {
|
|
380
|
+
journal.failurePhase = journal.phase;
|
|
369
381
|
const rollbackWarnings = await this.rollbackImport(journal);
|
|
370
382
|
journal.phase = 'failed';
|
|
371
383
|
journal.failedAt = new Date().toISOString();
|
|
372
384
|
journal.error = { code: error.code || '', message: String(error.message || error) };
|
|
373
385
|
journal.rollbackWarnings = rollbackWarnings;
|
|
374
386
|
this.writeJournal(journal);
|
|
375
|
-
await log.error?.('project.import.failed', 'Project import failed.', { projectId: journal.projectId || '', phase: journal.
|
|
376
|
-
if (error instanceof DomainError)
|
|
387
|
+
await log.error?.('project.import.failed', 'Project import failed.', { projectId: journal.projectId || '', phase: journal.failurePhase, error, context: { rollbackWarnings } });
|
|
388
|
+
if (error instanceof DomainError) {
|
|
389
|
+
if (!error.operationId) error.operationId = operationId;
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
377
392
|
throw new DomainError('INVALID_ARGUMENT', 'Project import failed.', { status: 500, operationId, cause: error, details: { rollbackWarnings } });
|
|
378
393
|
}
|
|
379
394
|
}
|
|
@@ -419,7 +434,13 @@ class ProjectLifecycleService {
|
|
|
419
434
|
try { await this.registryStore.remove(journal.projectId); } catch (error) { warnings.push(`registry:${error.message}`); }
|
|
420
435
|
}
|
|
421
436
|
if (journal.created.hookPath && journal.projectId && journal.repoPath) {
|
|
422
|
-
try {
|
|
437
|
+
try {
|
|
438
|
+
const unchanged = journal.replacedHook && fs.existsSync(journal.replacedHook.path) && fs.readFileSync(journal.replacedHook.path, 'utf8') === journal.replacedHook.content;
|
|
439
|
+
if (!unchanged) {
|
|
440
|
+
this.hookManager.uninstallHook({ repoPath: journal.repoPath, projectId: journal.projectId });
|
|
441
|
+
if (journal.replacedHook) AtomicFile.writeFileAtomic(journal.replacedHook.path, journal.replacedHook.content, { mode: 0o755 });
|
|
442
|
+
}
|
|
443
|
+
} catch (error) { warnings.push(`hook:${error.message}`); }
|
|
423
444
|
}
|
|
424
445
|
if (journal.created.metadata && fs.existsSync(journal.created.metadata)) {
|
|
425
446
|
try { fs.rmSync(journal.created.metadata, { recursive: true, force: true }); } catch (error) { warnings.push(`metadata:${error.message}`); }
|
|
@@ -98,9 +98,9 @@ class GitReader {
|
|
|
98
98
|
const top = topResult.status === 0 && String(topResult.stdout || '').trim()
|
|
99
99
|
? path.resolve(String(topResult.stdout).trim())
|
|
100
100
|
: null;
|
|
101
|
-
const commonResult = this.run(repoPath, ['rev-parse', '--
|
|
101
|
+
const commonResult = this.run(repoPath, ['rev-parse', '--git-common-dir']);
|
|
102
102
|
const commonDir = commonResult.status === 0 && String(commonResult.stdout || '').trim()
|
|
103
|
-
? path.resolve(String(commonResult.stdout).trim())
|
|
103
|
+
? path.resolve(repoPath, String(commonResult.stdout).trim())
|
|
104
104
|
: null;
|
|
105
105
|
let repoIdentity = null;
|
|
106
106
|
if (top && bridgeIdentityModule) {
|
package/_site/lib/server-app.js
CHANGED
|
@@ -11,7 +11,6 @@ const { StorageLayout } = require('./storage-layout');
|
|
|
11
11
|
const { SettingsStore } = require('./settings-store');
|
|
12
12
|
const { ProjectRegistryStore } = require('./project-registry-store');
|
|
13
13
|
const { ProjectStore } = require('./project-store');
|
|
14
|
-
const { MigrationService } = require('./migration-service');
|
|
15
14
|
const { ProjectLifecycleService } = require('./project-lifecycle-service');
|
|
16
15
|
const { RequirementRecorder } = require('./requirement-recorder');
|
|
17
16
|
const { ConversationStore } = require('./conversation-store');
|
|
@@ -25,7 +24,8 @@ const { handlePostCommitEvent, recoverOrphanedClaims } = require('./post-commit-
|
|
|
25
24
|
const { KnowledgePromotionService, hashBuffer } = require('./knowledge-promotion');
|
|
26
25
|
const { resolveEffectiveAiProfile } = require('./ai-profile-resolver');
|
|
27
26
|
const { ModuleBridge } = require('./module-bridge');
|
|
28
|
-
const {
|
|
27
|
+
const { connectEngine, forwardCommitToBridge } = require('./engine-connection');
|
|
28
|
+
const { initializeProductData } = require('./product-data');
|
|
29
29
|
const { IndexService } = require('./index-service');
|
|
30
30
|
const { KnowledgeDatabase } = require('./knowledge-db');
|
|
31
31
|
const { LocalEmbeddingService } = require('./embedding-service');
|
|
@@ -234,12 +234,12 @@ async function inspectGitRepository(repoPath) {
|
|
|
234
234
|
execGit(root, ['rev-parse', '--verify', 'HEAD']),
|
|
235
235
|
execGit(root, ['branch', '--show-current']),
|
|
236
236
|
execGit(root, ['status', '--porcelain=v1', '--untracked-files=normal']),
|
|
237
|
-
execGit(root, ['rev-parse', '--
|
|
237
|
+
execGit(root, ['rev-parse', '--git-common-dir']),
|
|
238
238
|
]);
|
|
239
239
|
return {
|
|
240
240
|
repoStatus: head.ok ? 'ok' : 'empty',
|
|
241
241
|
repoPath: top.ok ? path.resolve(top.stdout.trim()) : root,
|
|
242
|
-
commonDir: commonDir.ok ? path.resolve(commonDir.stdout.trim()) : '',
|
|
242
|
+
commonDir: commonDir.ok ? path.resolve(root, commonDir.stdout.trim()) : '',
|
|
243
243
|
headCommit: head.ok ? head.stdout.trim() : null,
|
|
244
244
|
branch: branch.ok ? branch.stdout.trim() : '',
|
|
245
245
|
dirty: status.ok && Boolean(status.stdout.trim()),
|
|
@@ -442,6 +442,7 @@ class RuntimeKnowledgeAnalyzer {
|
|
|
442
442
|
function createRuntime(options = {}) {
|
|
443
443
|
const rootDir = path.resolve(options.rootDir || path.resolve(__dirname, '..', '..'));
|
|
444
444
|
const dataPath = path.resolve(options.dataDir || dataDir.getDataDir());
|
|
445
|
+
if (options.analysisOwner === 'engine') initializeProductData(dataPath);
|
|
445
446
|
const layout = options.layout || new StorageLayout({ dataDir: dataPath });
|
|
446
447
|
const settingsStore = options.settingsStore || new SettingsStore({ layout });
|
|
447
448
|
const registryStore = options.registryStore || new ProjectRegistryStore({ layout });
|
|
@@ -466,7 +467,7 @@ function createRuntime(options = {}) {
|
|
|
466
467
|
secretsProvider,
|
|
467
468
|
});
|
|
468
469
|
const activeTasks = new Map();
|
|
469
|
-
const bridgeAdapter = options.bridgeAdapter || new BridgeAdapter({ dataDir: dataPath });
|
|
470
|
+
const bridgeAdapter = options.bridgeAdapter || new BridgeAdapter({ dataDir: dataPath, ...(options.analysisOwner === 'engine' ? { bridgeHomeDir: process.env.AI_CODING_EVENT_BRIDGE_HOME || path.join(dataPath, 'events') } : {}) });
|
|
470
471
|
const conversationStore = options.conversationStore || new ConversationStore({ layout, projectStore, logger });
|
|
471
472
|
const bridgeConsumerService = options.bridgeConsumerService || new BridgeConsumerService({
|
|
472
473
|
bridgeAdapter,
|
|
@@ -505,12 +506,11 @@ function createRuntime(options = {}) {
|
|
|
505
506
|
isProjectBusy: projectId => isProjectBusy(projectId, projectStore, activeTasks),
|
|
506
507
|
bridgeAdapter,
|
|
507
508
|
});
|
|
508
|
-
const migrationService = options.migrationService || new MigrationService({ layout, legacyDataDir: dataPath, logger });
|
|
509
509
|
const moduleBridge = options.moduleBridge || new ModuleBridge({ registryStore, projectStore, settingsStore, logger, dataDir: dataPath });
|
|
510
510
|
return {
|
|
511
511
|
analysisOwner: options.analysisOwner || 'legacy', ownership: { owner: options.analysisOwner || 'legacy', ready: false },
|
|
512
512
|
rootDir, dataPath, layout, settingsStore, registryStore, projectStore, logger, logRepository,
|
|
513
|
-
activeTasks, bridgeAdapter, bridgeConsumerService, conversationStore, conversationQuery, indexAdapter, indexService, promotionService, reconciler, lifecycleService, requirementRecorder, knowledgeRuntime,
|
|
513
|
+
activeTasks, bridgeAdapter, bridgeConsumerService, conversationStore, conversationQuery, indexAdapter, indexService, promotionService, reconciler, lifecycleService, requirementRecorder, knowledgeRuntime, moduleBridge,
|
|
514
514
|
};
|
|
515
515
|
}
|
|
516
516
|
|
|
@@ -648,22 +648,11 @@ async function reconcileLegacyHookMigration(runtime, projectId, config, state) {
|
|
|
648
648
|
}
|
|
649
649
|
|
|
650
650
|
async function initializeRuntime(runtime) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
// relocation previously aborted startup on conflict, bricking fresh
|
|
654
|
-
// machines whose npm package dir carried stale v4.1.x-era files.
|
|
655
|
-
await runtime.logger.info('server.startup_started', 'Server startup began.', { phase: 'migration' });
|
|
656
|
-
const migration = await runtime.migrationService.migrateIfNeeded();
|
|
657
|
-
if (!migration.ok || migration.requiresManualRecovery) {
|
|
658
|
-
throw new DomainError('MIGRATION_FAILED', 'Runtime data migration did not complete safely.', {
|
|
659
|
-
status: 500,
|
|
660
|
-
details: { reason: migration.reason || (migration.error && migration.error.code) || '' },
|
|
661
|
-
});
|
|
662
|
-
}
|
|
651
|
+
await runtime.logger.info('server.startup_started', 'Server startup began with fresh product data.', { phase: 'initialization' });
|
|
652
|
+
const migration = { ok: true, migrated: false, reason: 'fresh-data-only' };
|
|
663
653
|
dataDir.ensureDataDir(runtime.dataPath);
|
|
664
654
|
await runtime.settingsStore.initialize();
|
|
665
655
|
await runtime.registryStore.initialize();
|
|
666
|
-
if (runtime.analysisOwner !== 'legacy') await prepareEngineCutover(runtime);
|
|
667
656
|
const recoveredPromotions = runtime.analysisOwner === 'legacy' ? await runtime.promotionService.recoverAll() : [];
|
|
668
657
|
// Promotion recovery runs first because it can legitimately finish and clear
|
|
669
658
|
// a claim. Any claim left after that belonged to the previous process. Make
|
|
@@ -904,7 +893,7 @@ function createRequestHandler(runtime, options = {}) {
|
|
|
904
893
|
const moduleUi = pathname.match(/^\/modules\/(terminal|vectorhub|eventbridge|engine)\/(.*)$/);
|
|
905
894
|
if (moduleUi && method === 'GET') return runtime.moduleBridge.proxyModuleUi(req, res, moduleUi[1], moduleUi[2], url.search);
|
|
906
895
|
if (pathname.startsWith('/api/engine/') || pathname === '/api/engine') {
|
|
907
|
-
if (['/api/engine/runtime-config', '/api/engine/scopes', '/api/engine/control', '/api/engine/migrate'].includes(pathname)) throw new DomainError('FORBIDDEN', 'Runtime configuration is not exposed to browsers.', { status: 403 });
|
|
896
|
+
if (['/api/engine/runtime-config', '/api/engine/runtime-auth', '/api/engine/scopes', '/api/engine/control', '/api/engine/migrate'].includes(pathname)) throw new DomainError('FORBIDDEN', 'Runtime configuration is not exposed to browsers.', { status: 403 });
|
|
908
897
|
return runtime.moduleBridge.proxyEngine(req, res, pathname, url.search);
|
|
909
898
|
}
|
|
910
899
|
|
|
@@ -926,7 +915,7 @@ function createRequestHandler(runtime, options = {}) {
|
|
|
926
915
|
}
|
|
927
916
|
if (method === 'POST' && pathname === '/api/modules/configure') {
|
|
928
917
|
await readJsonBody(req);
|
|
929
|
-
await
|
|
918
|
+
await connectEngine(runtime);
|
|
930
919
|
return send(res, 200, { ok: true, configuration: await runtime.moduleBridge.syncEngineConfiguration() });
|
|
931
920
|
}
|
|
932
921
|
if (method === 'GET' && pathname === '/api/brain') {
|
|
@@ -1506,10 +1495,10 @@ async function startServer(options = {}) {
|
|
|
1506
1495
|
if (runtime.analysisOwner === 'legacy') runtime.indexService.retryDirtyProjects().catch(error => runtime.logger.error('index.retry_failed', 'Index retry failed.', { error }));
|
|
1507
1496
|
}, Number(process.env.KB_MAINTENANCE_INTERVAL_MS || 60 * 60 * 1000));
|
|
1508
1497
|
maintenanceTimer.unref?.();
|
|
1509
|
-
const configurationTimer = setInterval(() => {
|
|
1498
|
+
const configurationTimer = setInterval(() => { connectEngine(runtime).catch(() => {}); }, 30000);
|
|
1510
1499
|
configurationTimer.unref?.();
|
|
1511
1500
|
runtime.moduleBridge.syncEngineConfiguration().catch(() => {});
|
|
1512
|
-
|
|
1501
|
+
connectEngine(runtime).catch(() => {});
|
|
1513
1502
|
|
|
1514
1503
|
// T3.5 进程编排:拉起包内自带的模块服务并守护(默认开启,KB_MODULES_AUTOSTART=0 关闭)
|
|
1515
1504
|
try {
|
|
@@ -8,6 +8,7 @@ const { readLiveEndpoint } = require('../lib/runtime-endpoint');
|
|
|
8
8
|
const { StorageLayout } = require('../lib/storage-layout');
|
|
9
9
|
const { Logger } = require('../lib/structured-logger');
|
|
10
10
|
const { BridgeAdapter } = require('../lib/bridge-adapter');
|
|
11
|
+
const { resolveGitPath } = require('../lib/git-path');
|
|
11
12
|
|
|
12
13
|
let bridgeIdentityModule = null;
|
|
13
14
|
try {
|
|
@@ -96,11 +97,11 @@ function post(body) {
|
|
|
96
97
|
if (toplevel) {
|
|
97
98
|
let workspaceRoot = path.resolve(toplevel);
|
|
98
99
|
try { workspaceRoot = fs.realpathSync.native(workspaceRoot) || workspaceRoot; } catch { workspaceRoot = path.resolve(toplevel); }
|
|
99
|
-
const commonDir = git(['rev-parse', '--
|
|
100
|
+
const commonDir = git(['rev-parse', '--git-common-dir']);
|
|
100
101
|
const remoteRaw = git(['config', '--get', 'remote.origin.url']);
|
|
101
102
|
repoIdentity = bridgeIdentityModule.buildRepoIdentityV1({
|
|
102
103
|
workspaceRoot,
|
|
103
|
-
commonDir: commonDir ?
|
|
104
|
+
commonDir: commonDir ? resolveGitPath(repoRoot, commonDir) : null,
|
|
104
105
|
remote: remoteRaw ? bridgeIdentityModule.normalizeGitUrl(remoteRaw) : null,
|
|
105
106
|
});
|
|
106
107
|
}
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
## 安装与运行
|
|
7
7
|
|
|
8
|
+
v5 不再兼容或迁移旧知识库数据。停止旧版后重新设置模型、知识根目录并导入
|
|
9
|
+
项目;默认新数据目录为 `~/.project-knowledge-v5`。旧文件保留但不读取。
|
|
10
|
+
详见 [从头开始](fresh-v5-start.md)。
|
|
11
|
+
|
|
8
12
|
新机器完整安装只需三条命令:
|
|
9
13
|
|
|
10
14
|
```bash
|
|
@@ -23,8 +27,8 @@ project-knowledge
|
|
|
23
27
|
Git 工作区里的 Agent 对话捕捉进已导入项目的「开发对话」。
|
|
24
28
|
同时注册 host 级 `project-knowledge` Bridge consumer。全程无需打开任何
|
|
25
29
|
客户端 UI;第三方 hooks 与配置完整保留;Codex notify 冲突只上报不覆盖。
|
|
26
|
-
3. `project-knowledge`
|
|
27
|
-
|
|
30
|
+
3. `project-knowledge` 启动全新的 v5 本地服务。重新配置模型、知识根目录并导入
|
|
31
|
+
项目,不迁移旧登记、会话、知识、索引或未完成任务。
|
|
28
32
|
|
|
29
33
|
注意事项:
|
|
30
34
|
|
|
@@ -99,12 +103,12 @@ project-knowledge-kb history --project <projectId> --json
|
|
|
99
103
|
|
|
100
104
|
## 存储合同
|
|
101
105
|
|
|
102
|
-
内部数据默认在 `~/.project-knowledge/`,可用 `KB_DATA_DIR` 修改。用户必须先设置
|
|
106
|
+
内部数据默认在 `~/.project-knowledge-v5/`,可用 `KB_DATA_DIR` 修改。用户必须先设置
|
|
103
107
|
global knowledge root,再导入项目;它只影响未来导入。每个项目导入时固化绝对
|
|
104
108
|
knowledgePath,修改 global root 不会移动已有项目。
|
|
105
109
|
|
|
106
110
|
```text
|
|
107
|
-
~/.project-knowledge/
|
|
111
|
+
~/.project-knowledge-v5/
|
|
108
112
|
├── settings.json
|
|
109
113
|
├── projects.json # 仅 ID、顺序、最小显示快照
|
|
110
114
|
├── projects/<projectId>/
|