@sanqianx/project-knowledge 5.0.9-rc.1 → 5.0.9-rc.10
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 +96 -0
- package/README.md +27 -4
- package/_modules/knowledge-engine/README.md +4 -1
- package/_modules/knowledge-engine/package.json +1 -1
- package/_modules/knowledge-engine/src/headless-analyzer.js +3 -1
- package/_modules/knowledge-engine/src/knowledge-targets.js +46 -0
- package/_modules/knowledge-engine/src/retrieval.js +7 -5
- package/_site/_test/fixtures/make-git-repos.js +1 -9
- package/_site/_test/knowledge-engine-client-test.js +105 -0
- package/_site/_test/sessions-stream-test.js +3 -2
- package/_site/_test/shared-scope-root-binding-test.js +67 -0
- package/_site/_test/simple-import-test.js +3 -2
- package/_site/_test/team-knowledge-store-test.js +3 -2
- package/_site/_test/test-server-cleanup-test.js +31 -0
- package/_site/_test/vector-storage-root-test.js +52 -0
- package/_site/_test/workspace-ui-contract-test.js +1 -0
- package/_site/lib/contracts.js +5 -0
- package/_site/lib/engine-knowledge-client.js +72 -0
- package/_site/lib/knowledge-tool-runtime.js +7 -0
- package/_site/lib/module-bridge.js +44 -11
- package/_site/lib/server-app.js +3 -3
- package/_site/lib/settings-store.js +20 -0
- package/_site/lib/storage-layout.js +4 -0
- package/bin/project-knowledge-kb.js +4 -2
- package/bin/project-knowledge-mcp.js +7 -1
- package/module-runtime-manifest.json +40 -38
- package/package.json +3 -3
- 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 +2 -2
- package/ui/product.css +1 -0
- package/ui/product.js +12 -1
- package/vendor-manifest.json +6 -5
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { StorageLayout } = require('../lib/storage-layout');
|
|
5
|
+
const { ModuleBridge } = require('../lib/module-bridge');
|
|
6
|
+
const root = path.resolve('D:/knowledge storage 中文');
|
|
7
|
+
const layout = new StorageLayout({ dataDir: path.resolve('D:/metadata') });
|
|
8
|
+
assert.equal(layout.getVectorRootPath({ knowledge: { rootPath: root } }), path.join(root, '.indexes'));
|
|
9
|
+
assert.equal(layout.getVectorRootPath({ knowledge: { rootPath: '' } }), path.join(layout.getDataDir(), 'vectorhub'));
|
|
10
|
+
const settingsStore = { read: () => ({ knowledge: { rootPath: root }, brain: { id: 'brain-default', domains: [] } }) };
|
|
11
|
+
const bridge = new ModuleBridge({ settingsStore, dataDir: layout.getDataDir() });
|
|
12
|
+
const spec = bridge._supervisedSpecs().find(spec => spec.name === 'vector-hub');
|
|
13
|
+
assert.equal(spec.env.VECTOR_HUB_ROOT, path.join(root, '.indexes'));
|
|
14
|
+
assert.equal(spec.env.VECTOR_HUB_CONFIG, path.join(layout.getDataDir(), 'vectorhub', 'config.json'));
|
|
15
|
+
const calls = [];
|
|
16
|
+
bridge._fetch = async (url, init) => { const body = JSON.parse(init.body); calls.push({ url, body }); return { ok: true, json: async () => url.includes('/api/internal/runtime-config') ? { ok: true, rootPath: path.join(root, '.indexes') } : {} }; };
|
|
17
|
+
bridge.syncEngineConfiguration().then(async status => {
|
|
18
|
+
assert.equal(calls.find(call => call.url.includes('/api/internal/runtime-config')).body.storageRoot, path.join(root, '.indexes'));
|
|
19
|
+
assert.equal(status.indexStorage.status, 'ready');
|
|
20
|
+
let currentRoot = root, release, started;
|
|
21
|
+
const entered = new Promise(resolve => { started = resolve; });
|
|
22
|
+
const held = new Promise(resolve => { release = resolve; });
|
|
23
|
+
const queued = new ModuleBridge({ dataDir: layout.getDataDir(), settingsStore: { read: () => ({ knowledge: { rootPath: currentRoot }, brain: { id: 'brain-default', domains: [] } }) } });
|
|
24
|
+
const applied = [];
|
|
25
|
+
queued._fetch = async (url, init) => {
|
|
26
|
+
const body = JSON.parse(init.body);
|
|
27
|
+
if (url.endsWith('/api/internal/runtime-config')) {
|
|
28
|
+
applied.push(body.storageRoot);
|
|
29
|
+
if (applied.length === 1) { started(); await held; }
|
|
30
|
+
return { ok: true, json: async () => ({ rootPath: body.storageRoot }) };
|
|
31
|
+
}
|
|
32
|
+
return { ok: true, json: async () => null };
|
|
33
|
+
};
|
|
34
|
+
const first = queued.syncEngineConfiguration(); await entered;
|
|
35
|
+
currentRoot = path.join(root, 'new root');
|
|
36
|
+
const second = queued.syncEngineConfiguration(); release();
|
|
37
|
+
await first;
|
|
38
|
+
const latest = await second;
|
|
39
|
+
assert.deepEqual(applied, [path.join(root, '.indexes'), path.join(currentRoot, '.indexes')]);
|
|
40
|
+
assert.equal(latest.indexStorage.actualPath, path.join(currentRoot, '.indexes'));
|
|
41
|
+
assert.equal(latest.indexStorage.status, 'ready');
|
|
42
|
+
const broken = new ModuleBridge({ dataDir: layout.getDataDir(), settingsStore: { read: () => { throw new Error('corrupt settings'); } } });
|
|
43
|
+
let revoked;
|
|
44
|
+
broken._fetch = async (url, init) => {
|
|
45
|
+
if (url.endsWith('/api/internal/runtime-config')) revoked = JSON.parse(init.body);
|
|
46
|
+
return { ok: false };
|
|
47
|
+
};
|
|
48
|
+
const unavailable = await broken.syncEngineConfiguration();
|
|
49
|
+
assert.deepEqual(revoked, { settings: null });
|
|
50
|
+
assert.equal(unavailable.indexStorage.status, 'pending');
|
|
51
|
+
console.log('vector-storage-root-test PASS');
|
|
52
|
+
}).catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -11,6 +11,7 @@ for (const view of ['home', 'work', 'search', 'memory', 'projects', 'activity'])
|
|
|
11
11
|
assert.ok(html.includes(`id="view-${view}"`));
|
|
12
12
|
}
|
|
13
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');
|
|
14
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);
|
|
15
16
|
assert.ok(html.includes('data-action="import"') && html.includes('不分析全部历史'));
|
|
16
17
|
assert.ok(!html.includes('srcdoc') && !/data-simulate|data-analyze|data-install-hook/.test(html));
|
package/_site/lib/contracts.js
CHANGED
|
@@ -71,6 +71,11 @@ const ERROR_CODES = Object.freeze([
|
|
|
71
71
|
'KNOWLEDGE_WRITE_MOVED',
|
|
72
72
|
'MODEL_CONFIG_MOVED',
|
|
73
73
|
'MODULE_UNREACHABLE',
|
|
74
|
+
'ENGINE_UNAVAILABLE',
|
|
75
|
+
'ENGINE_IDENTITY_MISMATCH',
|
|
76
|
+
'ENGINE_INVALID_RESPONSE',
|
|
77
|
+
'ENGINE_REQUEST_FAILED',
|
|
78
|
+
'ENGINE_URL_INVALID',
|
|
74
79
|
]);
|
|
75
80
|
|
|
76
81
|
const IMMUTABLE_PROJECT_FIELDS = Object.freeze([
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { DomainError } = require('./contracts');
|
|
3
|
+
const endpoint = require('./runtime-endpoint');
|
|
4
|
+
const control = require('./runtime-control');
|
|
5
|
+
|
|
6
|
+
class EngineKnowledgeClient {
|
|
7
|
+
constructor({ layout, engineUrl, fetchImpl = fetch, timeoutMs = 20000 }) {
|
|
8
|
+
this.layout = layout;
|
|
9
|
+
this.engineUrl = engineUrl || process.env.KB_KNOWLEDGE_ENGINE_URL;
|
|
10
|
+
this.fetch = fetchImpl;
|
|
11
|
+
this.timeoutMs = timeoutMs;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async json(url, options = {}) {
|
|
15
|
+
let response;
|
|
16
|
+
try { response = await this.fetch(url, { ...options, redirect: 'error', signal: AbortSignal.timeout(this.timeoutMs) }); }
|
|
17
|
+
catch { throw new DomainError('ENGINE_UNAVAILABLE', '知识引擎连接失败或超时,请在系统状态中检查服务。', { status: 503 }); }
|
|
18
|
+
let payload;
|
|
19
|
+
try {
|
|
20
|
+
const reader = response.body.getReader();
|
|
21
|
+
const chunks = []; let size = 0;
|
|
22
|
+
try {
|
|
23
|
+
while (true) {
|
|
24
|
+
const part = await reader.read(); if (part.done) break;
|
|
25
|
+
size += part.value.byteLength;
|
|
26
|
+
if (size > 2 * 1024 * 1024) { await reader.cancel(); throw new Error('response limit'); }
|
|
27
|
+
chunks.push(Buffer.from(part.value));
|
|
28
|
+
}
|
|
29
|
+
} finally { reader.releaseLock(); }
|
|
30
|
+
payload = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
31
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) throw new Error('invalid envelope');
|
|
32
|
+
} catch { throw new DomainError('ENGINE_INVALID_RESPONSE', '知识引擎返回了无效响应。', { status: 502 }); }
|
|
33
|
+
if (!response.ok || payload.ok === false) {
|
|
34
|
+
const code = /^[A-Z][A-Z0-9_]{0,79}$/.test(payload.error?.code || '') ? payload.error.code : 'ENGINE_REQUEST_FAILED';
|
|
35
|
+
throw new DomainError(code, '知识引擎未完成读取,请检查项目接线或请求参数。', { status: response.ok ? 502 : response.status });
|
|
36
|
+
}
|
|
37
|
+
return payload;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async target(project) {
|
|
41
|
+
if (this.engineUrl) {
|
|
42
|
+
let url; try { url = new URL(this.engineUrl); } catch {}
|
|
43
|
+
if (!url || !['http:', 'https:'].includes(url.protocol) || !endpoint.isLoopbackHost(url.hostname.replace(/^\[|\]$/g, '')) || url.username || url.password || url.search || url.hash || !['', '/'].includes(url.pathname)) throw new DomainError('ENGINE_URL_INVALID', '知识引擎地址必须是无凭据的本机服务地址。');
|
|
44
|
+
const health = await this.json(new URL('/api/health', url));
|
|
45
|
+
const registered = await this.json(new URL('/api/projects', url));
|
|
46
|
+
const match = registered.projects?.find(item => item.projectId === project.projectId && item.enabled !== false);
|
|
47
|
+
if (health.name !== 'knowledge-engine' || health.dataSchemaVersion !== 3 || !match || !this.layout.pathsEqual(match.repoPath, project.repoPath) || !this.layout.pathsEqual(match.knowledgePath, project.knowledgePath)) throw new DomainError('ENGINE_IDENTITY_MISMATCH', '知识引擎与当前项目目录绑定不匹配。', { status: 409 });
|
|
48
|
+
return { url, prefix: '/api' };
|
|
49
|
+
}
|
|
50
|
+
const live = endpoint.readLiveEndpoint(this.layout.getDataDir(), { removeStale: false });
|
|
51
|
+
if (!live) throw new DomainError('ENGINE_UNAVAILABLE', '知识库服务尚未运行,请先启动 project-knowledge。', { status: 503 });
|
|
52
|
+
const url = new URL(`http://${live.host.includes(':') ? `[${live.host}]` : live.host}:${live.port}`);
|
|
53
|
+
const identity = await this.json(new URL('/api/runtime/identity', url));
|
|
54
|
+
if (identity.schema !== control.SCHEMA || identity.name !== 'project-knowledge' || identity.pid !== live.pid || identity.port !== live.port || identity.startedAt !== live.startedAt || !identity.dataDir || !this.layout.pathsEqual(identity.dataDir, this.layout.getDataDir())) throw new DomainError('ENGINE_IDENTITY_MISMATCH', '知识库实例与当前数据目录不匹配,已拒绝读取。', { status: 409 });
|
|
55
|
+
return { url, prefix: '/api/engine' };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async request(action, input, project) {
|
|
59
|
+
if (!['search', 'ask', 'get', 'history'].includes(action)) throw new DomainError('INVALID_ARGUMENT', '仅支持知识读取操作。');
|
|
60
|
+
if (input.scope !== undefined && !['project', 'domain', 'brain'].includes(input.scope)) throw new DomainError('INVALID_ARGUMENT', '知识范围必须是项目、领域或智脑。');
|
|
61
|
+
const target = await this.target(project);
|
|
62
|
+
// Forward only the read contract, never profiles, credentials or context objects.
|
|
63
|
+
const body = { projectId: project.projectId };
|
|
64
|
+
for (const key of ['query', 'limit', 'entryId', 'entry', 'scope', 'spaceId']) if (input[key] !== undefined) body[key] = input[key];
|
|
65
|
+
if (body.spaceId === undefined && input.space !== undefined) body.spaceId = input.space;
|
|
66
|
+
const result = await this.json(new URL(`${target.prefix}/${action}`, target.url), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
67
|
+
const valid = action === 'get' ? Array.isArray(result.chunks) : Array.isArray(result.results);
|
|
68
|
+
if (result.ok !== true || !valid || (action === 'ask' && (typeof result.answer !== 'string' || !Array.isArray(result.citations)))) throw new DomainError('ENGINE_INVALID_RESPONSE', '知识引擎返回了无效读取结果。', { status: 502 });
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
module.exports = { EngineKnowledgeClient };
|
|
@@ -12,6 +12,7 @@ const { EMBEDDING_DIMENSIONS } = require('./knowledge-schema');
|
|
|
12
12
|
const { DomainError } = require('./contracts');
|
|
13
13
|
const { KnowledgeRetrievalService } = require('./knowledge-retrieval-service');
|
|
14
14
|
const { ConversationStore } = require('./conversation-store');
|
|
15
|
+
const { EngineKnowledgeClient } = require('./engine-knowledge-client');
|
|
15
16
|
|
|
16
17
|
function gitRoot(candidate) {
|
|
17
18
|
const resolved = path.resolve(candidate || process.cwd());
|
|
@@ -97,6 +98,7 @@ class KnowledgeToolRuntime {
|
|
|
97
98
|
this.settingsStore = options.settingsStore || new SettingsStore({ layout: this.layout });
|
|
98
99
|
this.registryStore = options.registryStore || new ProjectRegistryStore({ layout: this.layout });
|
|
99
100
|
this.projectStore = options.projectStore || new ProjectStore({ layout: this.layout });
|
|
101
|
+
this.engineClient = options.engineClient || new EngineKnowledgeClient({ layout: this.layout, engineUrl: options.engineUrl, fetchImpl: options.fetchImpl });
|
|
100
102
|
this.database = null;
|
|
101
103
|
this.embedder = null;
|
|
102
104
|
this.conversationStore = options.conversationStore || new ConversationStore({
|
|
@@ -279,10 +281,13 @@ class KnowledgeToolRuntime {
|
|
|
279
281
|
|
|
280
282
|
async search(input = {}) {
|
|
281
283
|
const project = this.resolveProject(input);
|
|
284
|
+
if (this.projectStore.readState(project.projectId).modules?.engine) return this.engineClient.request('search', input, project);
|
|
282
285
|
return this.retrievalService.search({ ...input, project });
|
|
283
286
|
}
|
|
284
287
|
|
|
285
288
|
async ask(input = {}) {
|
|
289
|
+
const project = this.resolveProject(input);
|
|
290
|
+
if (this.projectStore.readState(project.projectId).modules?.engine) return this.engineClient.request('ask', input, project);
|
|
286
291
|
const searched = await this.search(input);
|
|
287
292
|
const citations = searched.results.slice(0, 5).map((result, index) => ({
|
|
288
293
|
index: index + 1,
|
|
@@ -300,6 +305,7 @@ class KnowledgeToolRuntime {
|
|
|
300
305
|
|
|
301
306
|
async get(input = {}) {
|
|
302
307
|
const project = this.resolveProject(input);
|
|
308
|
+
if (this.projectStore.readState(project.projectId).modules?.engine) return this.engineClient.request('get', input, project);
|
|
303
309
|
const entryId = String(input.entryId || input.entry || '').trim().replace(/\\/g, '/');
|
|
304
310
|
if (!entryId) throw new DomainError('INVALID_ARGUMENT', 'entry is required.');
|
|
305
311
|
const target = path.resolve(project.knowledgePath, ...entryId.split('/'));
|
|
@@ -321,6 +327,7 @@ class KnowledgeToolRuntime {
|
|
|
321
327
|
|
|
322
328
|
async history(input = {}) {
|
|
323
329
|
const project = this.resolveProject(input);
|
|
330
|
+
if (this.projectStore.readState(project.projectId).modules?.engine) return this.engineClient.request('history', input, project);
|
|
324
331
|
const changesRoot = path.join(project.knowledgePath, 'changes');
|
|
325
332
|
const results = markdownFiles(project.knowledgePath).filter(file => this.layout.isPathInside(changesRoot, file, { realpath: true })).map(file => {
|
|
326
333
|
const text = fs.readFileSync(file, 'utf8');
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
const { spawn, spawnSync } = require('child_process');
|
|
26
26
|
const path = require('path');
|
|
27
|
+
const { StorageLayout } = require('./storage-layout');
|
|
27
28
|
const fs = require('fs');
|
|
28
29
|
const crypto = require('crypto');
|
|
29
30
|
const { once } = require('events');
|
|
@@ -310,10 +311,19 @@ class ModuleBridge {
|
|
|
310
311
|
} };
|
|
311
312
|
}
|
|
312
313
|
|
|
313
|
-
|
|
314
|
-
if (this.
|
|
315
|
-
this.
|
|
316
|
-
|
|
314
|
+
syncEngineConfiguration() {
|
|
315
|
+
if (this.configSyncPromise) return this.configSyncPromise.catch(() => undefined).then(() => this.syncEngineConfiguration());
|
|
316
|
+
const pending = this._syncEngineConfiguration().finally(() => { if (this.configSyncPromise === pending) this.configSyncPromise = null; });
|
|
317
|
+
this.configSyncPromise = pending;
|
|
318
|
+
return pending;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async _syncEngineConfiguration() {
|
|
322
|
+
const dataDir = this.dataDir || require('./data-dir').getDataDir();
|
|
323
|
+
let indexPath = null;
|
|
324
|
+
try { indexPath = new StorageLayout({ dataDir }).getVectorRootPath(this.settingsStore?.read()); }
|
|
325
|
+
catch { /* Corrupt settings must not switch storage or prevent credential revocation. */ }
|
|
326
|
+
const status = { scopes: false, scopeStatus: { status: 'pending' }, model: false, embedding: false, indexStorage: { expectedPath: indexPath, actualPath: null, status: 'pending' } };
|
|
317
327
|
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.runtimeToken}` };
|
|
318
328
|
try {
|
|
319
329
|
if (this.settingsStore) {
|
|
@@ -321,11 +331,28 @@ class ModuleBridge {
|
|
|
321
331
|
if (settings.knowledge.rootPath) {
|
|
322
332
|
const brain = settings.brain;
|
|
323
333
|
const root = path.join(settings.knowledge.rootPath, '_shared');
|
|
324
|
-
const
|
|
325
|
-
|
|
334
|
+
const response = await this._fetch(`${this.engineUrl}/api/snapshot`);
|
|
335
|
+
if (!response.ok) throw new DomainError('ENGINE_UNAVAILABLE', '共享知识接线暂时不可用。');
|
|
336
|
+
const prior = (await response.json()).sharedScopes || [];
|
|
337
|
+
// Root settings govern new directories and derived indexes, never move
|
|
338
|
+
// existing knowledge. Retain engine-acknowledged bindings in the spine.
|
|
339
|
+
const binding = (item, kind) => {
|
|
340
|
+
const existing = prior.find(scope => scope.id === item.id && scope.kind === kind && scope.brainId === brain.id);
|
|
341
|
+
if (existing && !path.isAbsolute(existing.knowledgePath || '')) throw new DomainError('DATA_CORRUPT', '共享目录绑定损坏。');
|
|
342
|
+
return item.knowledgePath || existing?.knowledgePath || path.join(root, item.id);
|
|
343
|
+
};
|
|
344
|
+
const scopes = [{ id: brain.id, kind: 'brain', brainId: brain.id, name: brain.name, knowledgePath: binding(brain, 'brain') }, ...brain.domains.map(d => ({ id: d.id, kind: 'domain', brainId: brain.id, domainId: d.id, name: d.name, knowledgePath: binding(d, 'domain') }))];
|
|
345
|
+
const applied = await this._fetch(`${this.engineUrl}/api/scopes`, { method: 'POST', headers, body: JSON.stringify({ scopes }) });
|
|
346
|
+
if (!applied.ok) throw new DomainError('SHARED_SCOPE_APPLY_FAILED', '共享目录接线未完成。');
|
|
347
|
+
await this.settingsStore.bindSharedKnowledgePaths?.(scopes);
|
|
348
|
+
status.scopes = true; status.scopeStatus = { status: 'ready' };
|
|
326
349
|
}
|
|
327
350
|
}
|
|
328
|
-
} catch {
|
|
351
|
+
} catch (error) {
|
|
352
|
+
status.scopeStatus = { status: 'pending', code: ['DATA_CORRUPT', 'IMMUTABLE_FIELD', 'ENGINE_UNAVAILABLE', 'SHARED_SCOPE_APPLY_FAILED'].includes(error.code) ? error.code : 'ENGINE_UNAVAILABLE' };
|
|
353
|
+
this.logger.warn('modules.scopes.pending', 'Shared knowledge wiring is pending; existing data is retained.', { context: { code: status.scopeStatus.code } });
|
|
354
|
+
// A scope failure must not prevent revoking a stale model key.
|
|
355
|
+
}
|
|
329
356
|
try {
|
|
330
357
|
let profile = null;
|
|
331
358
|
try {
|
|
@@ -341,10 +368,15 @@ class ModuleBridge {
|
|
|
341
368
|
const resolved = await this._fetch(`${this.terminalUrl}/api/claude-workbench/v1/internal/embedding-config`, { method: 'POST', headers, body: '{}' });
|
|
342
369
|
if (resolved.ok) settings = await resolved.json();
|
|
343
370
|
} catch { /* Revocation wins over a previously cached runtime key. */ }
|
|
344
|
-
const applied = await this._fetch(`${this.vectorHubUrl}/api/internal/runtime-config`, { method: 'POST', headers, body: JSON.stringify({ settings }) });
|
|
371
|
+
const applied = await this._fetch(`${this.vectorHubUrl}/api/internal/runtime-config`, { method: 'POST', headers, body: JSON.stringify({ settings, ...(indexPath ? { storageRoot: indexPath } : {}) }) });
|
|
345
372
|
status.embedding = applied.ok && !!settings;
|
|
373
|
+
if (applied.ok) {
|
|
374
|
+
const result = await applied.json();
|
|
375
|
+
status.indexStorage.actualPath = result.rootPath || null;
|
|
376
|
+
status.indexStorage.status = result.rootPath && path.resolve(result.rootPath) === indexPath ? 'ready' : 'pending';
|
|
377
|
+
}
|
|
346
378
|
} catch { /* Public status contains no resolved configuration. */ }
|
|
347
|
-
this.
|
|
379
|
+
this.configStatus = status;
|
|
348
380
|
return status;
|
|
349
381
|
}
|
|
350
382
|
|
|
@@ -559,12 +591,13 @@ class ModuleBridge {
|
|
|
559
591
|
this.logger.warn('modules.supervisor', '@sanqianx/vector-hub runtime not installed; expecting an external service.', {});
|
|
560
592
|
} else {
|
|
561
593
|
const dataDir = this.dataDir || require('./data-dir').getDataDir();
|
|
562
|
-
const
|
|
594
|
+
const configRoot = path.join(dataDir, 'vectorhub');
|
|
595
|
+
const root = new StorageLayout({ dataDir }).getVectorRootPath(this.settingsStore?.read());
|
|
563
596
|
specs.push({
|
|
564
597
|
name: 'vector-hub',
|
|
565
598
|
command: `node ${JSON.stringify(vectorHubEntry)} serve --host ${urlHost(this.vectorHubUrl)} --port ${urlPort(this.vectorHubUrl, 8787)} --root ${JSON.stringify(root)}`,
|
|
566
599
|
url: this.vectorHubUrl,
|
|
567
|
-
env: { VECTOR_HUB_ROOT: root, VECTOR_HUB_CONFIG: path.join(
|
|
600
|
+
env: { VECTOR_HUB_ROOT: root, VECTOR_HUB_CONFIG: path.join(configRoot, 'config.json'), VECTOR_HUB_LEGACY_CONFIG: path.join(configRoot, 'no-legacy-config.json'), KB_VECTOR_MANAGED: '1', KB_VECTOR_RUNTIME_TOKEN: this.runtimeToken },
|
|
568
601
|
});
|
|
569
602
|
}
|
|
570
603
|
}
|
package/_site/lib/server-app.js
CHANGED
|
@@ -921,7 +921,7 @@ function createRequestHandler(runtime, options = {}) {
|
|
|
921
921
|
}
|
|
922
922
|
if (method === 'GET' && pathname === '/api/brain') {
|
|
923
923
|
const settings = runtime.settingsStore.read();
|
|
924
|
-
return send(res, 200, { ok: true, brain: settings.brain, knowledgeRoot: settings.knowledge.rootPath, configuration: runtime.moduleBridge.configStatus || null });
|
|
924
|
+
return send(res, 200, { ok: true, brain: settings.brain, knowledgeRoot: settings.knowledge.rootPath, indexStoragePath: runtime.layout.getVectorRootPath(settings), configuration: runtime.moduleBridge.configStatus || null });
|
|
925
925
|
}
|
|
926
926
|
if (method === 'PATCH' && pathname === '/api/brain') {
|
|
927
927
|
const body = await readJsonBody(req);
|
|
@@ -982,8 +982,8 @@ function createRequestHandler(runtime, options = {}) {
|
|
|
982
982
|
if (body.logging) throw new DomainError('IMMUTABLE_FIELD', 'Logging storage and capture policy are not user settings.', { status: 409 });
|
|
983
983
|
if (!Object.keys(patch).length) throw new DomainError('INVALID_ARGUMENT', 'Only the knowledge root is mutable on this route.');
|
|
984
984
|
const settings = await runtime.settingsStore.updatePatch(patch);
|
|
985
|
-
await runtime.moduleBridge.syncEngineConfiguration();
|
|
986
|
-
return send(res, 200, { ok: true, settings: { ...settings, ai: publicAiProfilesConfig(settings.ai) } });
|
|
985
|
+
const configuration = await runtime.moduleBridge.syncEngineConfiguration();
|
|
986
|
+
return send(res, 200, { ok: true, settings: { ...settings, ai: publicAiProfilesConfig(settings.ai) }, configuration });
|
|
987
987
|
}
|
|
988
988
|
|
|
989
989
|
if (method === 'GET' && pathname === '/api/projects') return send(res, 200, { ok: true, projects: listProjectViews(runtime.registryStore, runtime.projectStore) });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { SCHEMAS, DomainError, publicAiProfilesConfig } = require('./contracts');
|
|
2
2
|
const AtomicFile = require('./atomic-file');
|
|
3
3
|
const { StorageLayout } = require('./storage-layout');
|
|
4
|
+
const path = require('node:path');
|
|
4
5
|
|
|
5
6
|
function defaultSettings() {
|
|
6
7
|
return {
|
|
@@ -43,9 +44,12 @@ function validateSettings(settings) {
|
|
|
43
44
|
}
|
|
44
45
|
settings.brain = settings.brain || { id: 'brain-default', name: '我的智脑', domains: [] };
|
|
45
46
|
if (settings.brain.id !== 'brain-default' || typeof settings.brain.name !== 'string' || !settings.brain.name.trim() || settings.brain.name.length > 100 || !Array.isArray(settings.brain.domains)) throw new DomainError('DATA_CORRUPT', 'Brain metadata is invalid.', { status: 500 });
|
|
47
|
+
const validBinding = value => value === undefined || typeof value === 'string' && path.isAbsolute(value);
|
|
48
|
+
if (!validBinding(settings.brain.knowledgePath)) throw new DomainError('DATA_CORRUPT', 'Brain knowledge binding is invalid.', { status: 500 });
|
|
46
49
|
const ids = new Set();
|
|
47
50
|
for (const domain of settings.brain.domains) {
|
|
48
51
|
if (!domain || !/^domain-[a-z0-9-]+$/.test(domain.id) || ids.has(domain.id) || typeof domain.name !== 'string' || !domain.name.trim() || domain.name.length > 100) throw new DomainError('DATA_CORRUPT', 'Domain metadata is invalid.', { status: 500 });
|
|
52
|
+
if (!validBinding(domain.knowledgePath)) throw new DomainError('DATA_CORRUPT', 'Domain knowledge binding is invalid.', { status: 500 });
|
|
49
53
|
ids.add(domain.id);
|
|
50
54
|
}
|
|
51
55
|
const logging = settings.logging || {};
|
|
@@ -92,6 +96,7 @@ class SettingsStore {
|
|
|
92
96
|
|
|
93
97
|
async updatePatch(patch) {
|
|
94
98
|
return this.atomic.withFileLock(this.lockPath, async () => {
|
|
99
|
+
if (patch?.brain && (Object.prototype.hasOwnProperty.call(patch.brain, 'knowledgePath') || patch.brain.domains?.some?.(domain => Object.prototype.hasOwnProperty.call(domain, 'knowledgePath')))) throw new DomainError('INVALID_ARGUMENT', '共享知识目录绑定只允许受控接线写入。');
|
|
95
100
|
if (patch && patch.logging && typeof patch.logging === 'object') {
|
|
96
101
|
const unknown = Object.keys(patch.logging).find(key => key !== 'levels');
|
|
97
102
|
if (unknown) throw new DomainError('INVALID_ARGUMENT', `Logging setting is read-only or unsupported: ${unknown}.`, { details: { field: unknown } });
|
|
@@ -120,6 +125,21 @@ class SettingsStore {
|
|
|
120
125
|
});
|
|
121
126
|
}
|
|
122
127
|
|
|
128
|
+
async bindSharedKnowledgePaths(scopes) {
|
|
129
|
+
return this.atomic.withFileLock(this.lockPath, async () => {
|
|
130
|
+
const settings = this.read();
|
|
131
|
+
let changed = false;
|
|
132
|
+
for (const scope of scopes) {
|
|
133
|
+
const target = scope.kind === 'brain' && scope.id === settings.brain.id ? settings.brain : settings.brain.domains.find(domain => domain.id === scope.id);
|
|
134
|
+
if (!target || !path.isAbsolute(scope.knowledgePath || '')) throw new DomainError('INVALID_ARGUMENT', '共享知识绑定无效。');
|
|
135
|
+
if (target.knowledgePath && !this.layout.pathsEqual(target.knowledgePath, scope.knowledgePath)) throw new DomainError('IMMUTABLE_FIELD', '共享知识目录不能隐式移动。', { status: 409 });
|
|
136
|
+
if (!target.knowledgePath) { target.knowledgePath = path.resolve(scope.knowledgePath); changed = true; }
|
|
137
|
+
}
|
|
138
|
+
if (changed) { settings.updatedAt = new Date().toISOString(); validateSettings(settings); this.atomic.writeJsonAtomic(this.filePath, settings); }
|
|
139
|
+
return settings;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
123
143
|
readPublicView(options = {}) {
|
|
124
144
|
const settings = this.read(options);
|
|
125
145
|
return {
|
|
@@ -49,6 +49,10 @@ class StorageLayout {
|
|
|
49
49
|
getRegistryLockPath() { return path.join(this.dataDir, 'runtime', 'locks', 'projects.lock'); }
|
|
50
50
|
|
|
51
51
|
getIndexPath() { return path.join(this.dataDir, 'index', 'knowledge.lancedb'); }
|
|
52
|
+
getVectorRootPath(settings) {
|
|
53
|
+
const root = settings?.knowledge?.rootPath;
|
|
54
|
+
return root ? path.join(path.resolve(root), '.indexes') : path.join(this.dataDir, 'vectorhub');
|
|
55
|
+
}
|
|
52
56
|
getCachePath(...segments) { return path.join(this.dataDir, 'cache', ...segments); }
|
|
53
57
|
getRuntimePath(...segments) { return path.join(this.dataDir, 'runtime', ...segments); }
|
|
54
58
|
getRecoveryPath(...segments) { return path.join(this.dataDir, 'recovery', ...segments); }
|
|
@@ -25,7 +25,9 @@ Read-only local knowledge tools:
|
|
|
25
25
|
project-knowledge-kb get --project <projectId-or-legacy-slug> --entry <path>
|
|
26
26
|
project-knowledge-kb history --project <projectId-or-legacy-slug> [--limit 20]
|
|
27
27
|
|
|
28
|
-
Add --
|
|
28
|
+
Add --scope project|domain|brain for a search range, --space <id> for a cited
|
|
29
|
+
shared entry, and --json for machine-readable output. These commands never
|
|
30
|
+
modify knowledge or project configuration.\n`);
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
async function main(argv = process.argv.slice(2), options = {}) {
|
|
@@ -38,7 +40,7 @@ async function main(argv = process.argv.slice(2), options = {}) {
|
|
|
38
40
|
if (!args.project) throw new Error('--project is required');
|
|
39
41
|
const runtime = options.runtime || new KnowledgeToolRuntime(options);
|
|
40
42
|
try {
|
|
41
|
-
const input = { project: args.project, query: args.query, limit: args.limit, entry: args.entry };
|
|
43
|
+
const input = { project: args.project, query: args.query, limit: args.limit, entry: args.entry, scope: args.scope, spaceId: args.spaceId || args.space };
|
|
42
44
|
let result;
|
|
43
45
|
if (args.command === 'search') result = await runtime.search(input);
|
|
44
46
|
else if (args.command === 'ask') result = await runtime.ask(input);
|
|
@@ -11,6 +11,7 @@ const { KnowledgeToolRuntime } = require('../_site/lib/knowledge-tool-runtime');
|
|
|
11
11
|
const { serializeErrorEnvelope, createId } = require('../_site/lib/contracts');
|
|
12
12
|
|
|
13
13
|
const SERVER_NAME = 'project-knowledge';
|
|
14
|
+
const SCOPE = { type: 'string', enum: ['project', 'domain', 'brain'], description: 'Knowledge range: current project (default), its domain, or its brain.' };
|
|
14
15
|
const SERVER_INSTRUCTIONS = `Project Knowledge is the durable source of prior project decisions and implementation history. Knowledge content is read-only; the only write operation appends an explicit supplemental Prompt event to the shared conversation truth.
|
|
15
16
|
|
|
16
17
|
At the start of work in a registered Git repository, and before answering questions about prior work or implementing a non-trivial change:
|
|
@@ -56,7 +57,7 @@ const TOOLS = [
|
|
|
56
57
|
},
|
|
57
58
|
{
|
|
58
59
|
name: 'project_knowledge_search',
|
|
59
|
-
description: 'Search durable
|
|
60
|
+
description: 'Search the same durable knowledge and commit citations as the product UI, within the selected project, domain or brain. This tool is read-only.',
|
|
60
61
|
inputSchema: {
|
|
61
62
|
type: 'object',
|
|
62
63
|
required: ['query'],
|
|
@@ -66,6 +67,7 @@ const TOOLS = [
|
|
|
66
67
|
projectId: { type: 'string', description: 'Stable projectId returned by project_knowledge_resolve.' },
|
|
67
68
|
project: { type: 'string', description: 'Optional registered project slug.' },
|
|
68
69
|
limit: { type: 'integer', minimum: 1, maximum: 30, default: 8 },
|
|
70
|
+
scope: SCOPE,
|
|
69
71
|
},
|
|
70
72
|
additionalProperties: false,
|
|
71
73
|
},
|
|
@@ -82,6 +84,7 @@ const TOOLS = [
|
|
|
82
84
|
projectId: { type: 'string' },
|
|
83
85
|
project: { type: 'string' },
|
|
84
86
|
limit: { type: 'integer', minimum: 1, maximum: 30, default: 8 },
|
|
87
|
+
scope: SCOPE,
|
|
85
88
|
},
|
|
86
89
|
additionalProperties: false,
|
|
87
90
|
},
|
|
@@ -95,6 +98,8 @@ const TOOLS = [
|
|
|
95
98
|
properties: {
|
|
96
99
|
entry: { type: 'string', minLength: 1, description: 'Entry identifier returned by search.' },
|
|
97
100
|
space: { type: 'string', description: 'Optional scoped knowledge space identifier.' },
|
|
101
|
+
spaceId: { type: 'string', description: 'Knowledge space identifier returned by a citation; alias of space.' },
|
|
102
|
+
scope: SCOPE,
|
|
98
103
|
repoPath: { type: 'string' },
|
|
99
104
|
projectId: { type: 'string' },
|
|
100
105
|
project: { type: 'string' },
|
|
@@ -112,6 +117,7 @@ const TOOLS = [
|
|
|
112
117
|
projectId: { type: 'string' },
|
|
113
118
|
project: { type: 'string' },
|
|
114
119
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
|
|
120
|
+
scope: SCOPE,
|
|
115
121
|
},
|
|
116
122
|
additionalProperties: false,
|
|
117
123
|
},
|