@sanqianx/project-knowledge 4.8.0 → 5.0.0
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 +19 -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/module-model-configuration-test.js +2 -1
- 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/hook-manager.js +1 -1
- package/_site/lib/integration-installer.js +2 -1
- package/_site/lib/module-bridge.js +6 -16
- package/_site/lib/product-data.js +24 -0
- package/_site/lib/project-lifecycle-service.js +18 -2
- package/_site/lib/server-app.js +11 -22
- package/docs/README.zh-CN.md +8 -4
- package/docs/fresh-v5-start.md +28 -0
- package/module-runtime-manifest.json +2 -2
- package/package.json +2 -1
- 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 -1
- package/ui/product.js +41 -15
- 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
|
@@ -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}`,
|
|
@@ -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 };
|
|
@@ -163,6 +163,8 @@ class ProjectLifecycleService {
|
|
|
163
163
|
// Legacy v1 hook: import will replace it; that's the migration
|
|
164
164
|
// path (T03 case 2), not a conflict.
|
|
165
165
|
checks.hook = { ok: true, reason: 'legacy-v1' };
|
|
166
|
+
} else if (hookStatus.kbManaged && hookStatus.projectId !== input.projectId) {
|
|
167
|
+
checks.hook = { ok: true, replacementProjectId: hookStatus.projectId };
|
|
166
168
|
} else {
|
|
167
169
|
checks.hook = { ok: true };
|
|
168
170
|
}
|
|
@@ -354,7 +356,15 @@ class ProjectLifecycleService {
|
|
|
354
356
|
journal.created.metadata = this.layout.getProjectMetadataDir(projectId);
|
|
355
357
|
journal.phase = 'metadata-created';
|
|
356
358
|
this.writeJournal(journal);
|
|
357
|
-
const
|
|
359
|
+
const priorHook = this.hookManager.readHookStatus({ repoPath: git.repoPath });
|
|
360
|
+
if (priorHook.kbManaged && priorHook.projectId !== projectId) {
|
|
361
|
+
if (input.replaceManagedHookProjectId !== priorHook.projectId || this.registryStore.readDisplaySnapshot(priorHook.projectId)) throw new DomainError('HOOK_CONFLICT', '目录中留有另一项目的知识库 Hook。请确认替换旧托管 Hook 后重新导入。', { status: 409 });
|
|
362
|
+
journal.replacedHook = { path: priorHook.hookPath, content: fs.readFileSync(priorHook.hookPath, 'utf8') };
|
|
363
|
+
this.writeJournal(journal);
|
|
364
|
+
}
|
|
365
|
+
journal.created.hookPath = priorHook.hookPath;
|
|
366
|
+
this.writeJournal(journal);
|
|
367
|
+
const hook = this.hookManager.installHook({ repoPath: git.repoPath, projectId, triggerScriptPath: this.triggerScriptPath, dataDir: this.layout.getDataDir(), ...this.bridgeAdapter?.hookBinding?.(), replaceManagedHookProjectId: input.replaceManagedHookProjectId });
|
|
358
368
|
journal.created.hookPath = hook.hookPath;
|
|
359
369
|
journal.phase = 'hook-verified';
|
|
360
370
|
this.writeJournal(journal);
|
|
@@ -419,7 +429,13 @@ class ProjectLifecycleService {
|
|
|
419
429
|
try { await this.registryStore.remove(journal.projectId); } catch (error) { warnings.push(`registry:${error.message}`); }
|
|
420
430
|
}
|
|
421
431
|
if (journal.created.hookPath && journal.projectId && journal.repoPath) {
|
|
422
|
-
try {
|
|
432
|
+
try {
|
|
433
|
+
const unchanged = journal.replacedHook && fs.existsSync(journal.replacedHook.path) && fs.readFileSync(journal.replacedHook.path, 'utf8') === journal.replacedHook.content;
|
|
434
|
+
if (!unchanged) {
|
|
435
|
+
this.hookManager.uninstallHook({ repoPath: journal.repoPath, projectId: journal.projectId });
|
|
436
|
+
if (journal.replacedHook) AtomicFile.writeFileAtomic(journal.replacedHook.path, journal.replacedHook.content, { mode: 0o755 });
|
|
437
|
+
}
|
|
438
|
+
} catch (error) { warnings.push(`hook:${error.message}`); }
|
|
423
439
|
}
|
|
424
440
|
if (journal.created.metadata && fs.existsSync(journal.created.metadata)) {
|
|
425
441
|
try { fs.rmSync(journal.created.metadata, { recursive: true, force: true }); } catch (error) { warnings.push(`metadata:${error.message}`); }
|
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');
|
|
@@ -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 {
|
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>/
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# v5 从头开始
|
|
2
|
+
|
|
3
|
+
本版按用户确认不再迁移以前的知识库数据。旧文件留在原位置,但新版不会读取它们;没有对用户代码仓库或旧知识目录执行删除。
|
|
4
|
+
|
|
5
|
+
## 更新
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
project-knowledge stop
|
|
9
|
+
npm install -g @sanqianx/project-knowledge@latest --registry=https://registry.npmjs.org/
|
|
10
|
+
project-knowledge-integrations update
|
|
11
|
+
project-knowledge
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
需要 Node ≥22 和 Git。默认使用 `~/.project-knowledge-v5`,四模块的托管数据也在此目录内。若环境变量 `KB_DATA_DIR` 仍指向旧目录,请取消该设置或改为新的空目录。不要把旧 `projects.json`、设置或引擎状态复制进新目录。
|
|
15
|
+
|
|
16
|
+
在“系统状态”重新配置模型与后台默认模型、embedding 和知识根目录,然后在“领域与项目”导入代码目录及新的空知识目录。原代码目录无需移动。若残留旧知识库托管 Hook,导入预检会要求明确确认替换;不会覆盖第三方 Hook。导入建立 HEAD 基线,不分析已有历史。
|
|
17
|
+
|
|
18
|
+
如果模块是自己独立启动的旧进程,请在它原来的终端中停止后重启新版外壳。外壳不会擅自杀死其他服务;版本或运行期鉴权不一致时显示具体连接阶段,不接管旧服务数据。
|
|
19
|
+
|
|
20
|
+
## 解绑与恢复
|
|
21
|
+
|
|
22
|
+
“解绑并保留数据”从项目列表移除登记、取消未开始任务并卸下本项目托管 Hook,保留代码、知识、会话及证据。实际分析或索引写入尚未结束时仍禁止解绑,防止丢失写入状态。队列本身不再被当成正在运行。
|
|
23
|
+
|
|
24
|
+
删除的是旧版本兼容路径,不是新版的故障恢复能力:v5 自身的重启恢复、提交去重、并发限制、失败重试和索引修复仍有效。`upgrade-recovery` 不再存在。
|
|
25
|
+
|
|
26
|
+
## 回归范围
|
|
27
|
+
|
|
28
|
+
旧格式迁移专用测试随旧实现删除,替换为旧目录拒绝且原字节不变、空状态启动、连接失败诊断与恢复、旧托管 Hook 的显式替换和回滚测试。真实提交链路继续由引擎 Git/Hook/journal/晋升/索引回归和五服务网页回归覆盖。模型替身和真实模型的结果分开记录;旧版本真实调用记录不计为本版通过。
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "project-knowledge/module-content/v1",
|
|
3
3
|
"productFiles": {
|
|
4
|
-
"ui/index.html": "
|
|
5
|
-
"ui/product.js": "
|
|
4
|
+
"ui/index.html": "fd79551591729f6874c95089e479d46f208ab3b45cf45ba21eff1f6967757f7d",
|
|
5
|
+
"ui/product.js": "d5fca94df54075d598f271e01ea41ac9cce3a9847ea4f246e4f20c47daed43ed",
|
|
6
6
|
"ui/product.css": "1e68f087cec85a69f4497492d31fd1dd8712cf5176b0c721ce765de36ce606e1"
|
|
7
7
|
},
|
|
8
8
|
"modules": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanqianx/project-knowledge",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "Knowledge base manager with Git integration, AI-driven analysis, and bilingual (zh-CN/en-US) knowledge output",
|
|
5
5
|
"main": "_site/server.js",
|
|
6
6
|
"scripts": {
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"docs/project-registry-schema.md",
|
|
50
50
|
"docs/team-knowledge-mode-a-plan.md",
|
|
51
51
|
"docs/testing-strategy.md",
|
|
52
|
+
"docs/fresh-v5-start.md",
|
|
52
53
|
"README.md",
|
|
53
54
|
"NOTICE",
|
|
54
55
|
"CHANGELOG.md"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "project-knowledge",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "5.0.0",
|
|
5
5
|
"description": "Read durable project decisions and implementation history from Project Knowledge.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "SanQianX",
|
package/ui/index.html
CHANGED
|
@@ -54,9 +54,10 @@
|
|
|
54
54
|
<p id="embeddingFeedback" class="muted" role="status"></p>
|
|
55
55
|
<div class="dialog-actions"><button type="button" id="disableEmbedding">停用向量模型</button><button type="button" data-close="embeddingDialog">取消</button><button type="submit" class="primary">保存配置</button></div>
|
|
56
56
|
</form></dialog>
|
|
57
|
-
<dialog id="importDialog"><form id="importForm"><div class="dialog-heading"><h2>导入项目</h2><button type="button" data-close="importDialog" aria-label="关闭">×</button></div><p class="muted"
|
|
57
|
+
<dialog id="importDialog"><form id="importForm"><div class="dialog-heading"><h2>导入项目</h2><button type="button" data-close="importDialog" aria-label="关闭">×</button></div><p class="muted">绑定代码与新的空知识目录。只建立跟踪基线,不读取旧知识、不分析全部历史。</p><label>代码目录<div class="input-with-action"><input name="localPath" id="importCode" required placeholder="D:\SanQian.Xu\my-project"><button type="button" id="pickCode">选择</button></div></label><label>项目名称<input name="displayName" id="importName" required maxlength="200"></label><label>知识目录<div class="input-with-action"><input name="knowledgePath" id="importKnowledge" required placeholder="D:\SanQian.Xu\knowledge\my-project"><button type="button" id="pickKnowledge">选择</button></div></label><label>所属领域<select name="domainId" id="importDomain"></select></label><label id="gitInitChoice" class="checkbox" hidden><input type="checkbox" name="confirmGitInit">这个目录不是 Git 仓库,我确认初始化 Git。</label><label id="replaceHookChoice" class="checkbox" hidden><input type="checkbox" name="confirmReplaceHook">目录留有旧知识库的托管 Hook,我确认替换为新项目接线。不会覆盖第三方 Hook。</label><div id="preflightStatus" class="feedback" role="status">填写目录后检查导入条件。</div><button type="button" id="setupImportRoot" hidden>设置知识根目录</button><div class="dialog-actions"><button type="button" data-close="importDialog">取消</button><button type="submit" class="primary" id="confirmImport" disabled>导入项目</button></div></form></dialog>
|
|
58
58
|
<dialog id="domainDialog"><form id="domainForm"><div class="dialog-heading"><h2>创建领域</h2><button type="button" data-close="domainDialog" aria-label="关闭">×</button></div><label>领域名称<input name="name" required maxlength="100" placeholder="例如:开发工具"></label><p class="muted">领域用于共享相关项目的知识,不是新的代码项目。</p><div class="dialog-actions"><button type="button" data-close="domainDialog">取消</button><button class="primary" type="submit">创建领域</button></div></form></dialog>
|
|
59
59
|
<dialog id="memoryDialog"><form id="memoryForm"><div class="dialog-heading"><h2>编辑记忆</h2><button type="button" data-close="memoryDialog" aria-label="关闭">×</button></div><label>内容<textarea id="memoryText" maxlength="2000" required rows="5"></textarea></label><p id="memoryEvidence" class="muted"></p><div class="dialog-actions"><button type="button" data-close="memoryDialog">取消</button><button type="submit" class="primary">保存修改</button></div></form></dialog>
|
|
60
|
+
<dialog id="unbindDialog"><form id="unbindForm"><div class="dialog-heading"><h2>解绑项目</h2><button type="button" data-close="unbindDialog" aria-label="关闭">×</button></div><p id="unbindProjectName"></p><p class="muted">项目将从列表移除,取消尚未开始的维护任务。代码、知识文件、提交证据和会话历史仍保留。正在执行的知识写入结束前不会解绑。</p><p id="unbindFeedback" class="feedback" role="status"></p><div class="dialog-actions"><button type="button" data-close="unbindDialog">取消</button><button class="primary" type="submit" id="confirmUnbind">解绑并保留数据</button></div></form></dialog>
|
|
60
61
|
<dialog id="referenceDialog" class="reference-dialog"><div class="dialog-heading"><h2 id="referenceTitle">知识引用</h2><div class="actions"><button id="copyReference">复制引用</button><button data-close="referenceDialog" aria-label="关闭">×</button></div></div><p id="referenceMeta" class="muted"></p><pre id="referenceContent"></pre></dialog>
|
|
61
62
|
<dialog id="engineDialog" class="engine-dialog"><div class="dialog-heading"><h2>知识引擎诊断</h2><button data-close="engineDialog" aria-label="关闭">×</button></div><iframe id="engineFrame" title="知识引擎诊断" referrerpolicy="same-origin"></iframe></dialog>
|
|
62
63
|
<div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
|
package/ui/product.js
CHANGED
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
const domainName = id => state.brain.domains.find(d => d.id === id)?.name || '未分组';
|
|
17
17
|
const projectName = id => state.projects.find(p => p.projectId === id)?.name || id;
|
|
18
18
|
const time = value => value ? new Date(value).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '—';
|
|
19
|
-
const label = run => ({ queued: '排队', running: '维护中', succeeded: '已更新 · 可检索', 'index-pending': '知识已更新 · 索引待修复', failed: '维护失败' })[run.state] || run.state;
|
|
19
|
+
const label = run => ({ queued: '排队', running: '维护中', succeeded: '已更新 · 可检索', 'index-pending': '知识已更新 · 索引待修复', failed: '维护失败', cancelled: '已取消' })[run.state] || run.state;
|
|
20
20
|
const badge = (text, status = '') => `<span class="status ${esc(status)}">${esc(text)}</span>`;
|
|
21
|
-
let toastTimer, refreshTimer, preflightTimer, editingMemoryId, logStream, logEpoch = 0, referenceEpoch = 0, snapshotEpoch = 0, resumeImport = false;
|
|
21
|
+
let toastTimer, refreshTimer, preflightTimer, editingMemoryId, logStream, logEpoch = 0, referenceEpoch = 0, snapshotEpoch = 0, registryEpoch = 0, registryPromise = null, resumeImport = false, unbindProjectId = '', unbinding = false;
|
|
22
22
|
async function api(url, options = {}) {
|
|
23
23
|
const { allowNotReady, ...init } = options;
|
|
24
24
|
if (init.body && typeof init.body !== 'string') init.body = JSON.stringify(init.body);
|
|
@@ -149,23 +149,29 @@
|
|
|
149
149
|
stream.addEventListener('progress', () => { if (state.stream === stream) scheduleRefresh(); });
|
|
150
150
|
stream.onerror = () => { if (state.stream === stream) $('activityConnection').textContent = '连接中断,正在恢复快照'; };
|
|
151
151
|
}
|
|
152
|
-
|
|
153
|
-
if (!navigator.onLine
|
|
154
|
-
|
|
152
|
+
function refreshRegistry(force = false) {
|
|
153
|
+
if (!navigator.onLine) return Promise.resolve();
|
|
154
|
+
if (registryPromise && !force) return registryPromise;
|
|
155
|
+
const epoch = ++registryEpoch;
|
|
156
|
+
const job = (async () => {
|
|
155
157
|
try {
|
|
156
158
|
const [brain, projects, health] = await Promise.all([api('/api/brain'), api('/api/projects/aggregated'), api('/api/modules/health')]);
|
|
159
|
+
if (epoch !== registryEpoch) return;
|
|
157
160
|
state.brain = brain.brain; state.root = brain.knowledgeRoot; state.projects = projects.projects; state.health = health.modules; state.ownership = health.ownership;
|
|
158
161
|
if (!activeProject()) { state.projectId = state.projects[0]?.projectId || ''; state.epoch++; Object.keys(frames).forEach(frameContext); }
|
|
159
162
|
renderProjectOptions(); renderSystem(); connectProgress(); await refreshSnapshot(); setView(state.view);
|
|
160
163
|
} catch (error) { toast(error.message); }
|
|
161
|
-
finally {
|
|
164
|
+
finally { if (epoch === registryEpoch) { registryPromise = null; connectProgress(); } }
|
|
165
|
+
})();
|
|
166
|
+
registryPromise = job;
|
|
167
|
+
return job;
|
|
162
168
|
}
|
|
163
169
|
function renderSystem() {
|
|
164
170
|
const down = Object.keys(modules).filter(key => !state.health[key]?.up);
|
|
165
171
|
const pending = state.ownership?.owner === 'engine' && !state.ownership.ready;
|
|
166
172
|
$('systemDot').className = `dot ${down.length || pending ? 'issue' : 'up'}`;
|
|
167
|
-
$('systemTitle').textContent = pending ? '
|
|
168
|
-
$('systemDetail').textContent = pending ? state.ownership.message || '
|
|
173
|
+
$('systemTitle').textContent = pending ? '知识引擎连接待恢复' : down.length ? '部分服务需要处理' : '系统连接正常';
|
|
174
|
+
$('systemDetail').textContent = pending ? `${state.ownership.message || '服务恢复后自动重试。'}${state.ownership.error ? ` · ${state.ownership.error.step} (${state.ownership.error.code})` : ''}` : down.length ? down.map(k => modules[k]).join('、') : '本机知识服务已连接';
|
|
169
175
|
$('moduleHealth').innerHTML = Object.entries(modules).map(([key, name]) => `<div class="row"><div><b>${esc(name)}</b><small>${esc(state.health[key]?.url || '')}</small></div>${badge(state.health[key]?.up ? '在线' : '离线', state.health[key]?.up ? 'up' : 'issue')}</div>`).join('');
|
|
170
176
|
if (!navigator.onLine) {
|
|
171
177
|
$('systemDot').className = 'dot issue'; $('systemTitle').textContent = '网页连接中断';
|
|
@@ -267,9 +273,9 @@
|
|
|
267
273
|
$('importForm').reset(); state.importId = `project-${crypto.randomUUID()}`; state.preflight = null; state.preflightEpoch++;
|
|
268
274
|
$('importDomain').innerHTML = '<option value="">未分组</option>' + state.brain.domains.map(d => `<option value="${esc(d.id)}">${esc(d.name)}</option>`).join('');
|
|
269
275
|
$('importDomain').value = activeProject()?.domainId || '';
|
|
270
|
-
$('gitInitChoice').hidden = true; $('confirmImport').disabled = true; $('preflightStatus').textContent = '填写目录后检查导入条件。'; $('importDialog').showModal();
|
|
276
|
+
$('gitInitChoice').hidden = true; $('replaceHookChoice').hidden = true; $('confirmImport').disabled = true; $('preflightStatus').textContent = '填写目录后检查导入条件。'; $('importDialog').showModal();
|
|
271
277
|
}
|
|
272
|
-
function importInput() { return { projectId: state.importId, brainId: state.brain.id, domainId: $('importDomain').value || null, localPath: $('importCode').value.trim(), knowledgePath: $('importKnowledge').value.trim(), displayName: $('importName').value.trim(), knowledgeLanguage: 'zh-CN', confirmGitInit: $('importForm').elements.confirmGitInit.checked }; }
|
|
278
|
+
function importInput() { return { projectId: state.importId, brainId: state.brain.id, domainId: $('importDomain').value || null, localPath: $('importCode').value.trim(), knowledgePath: $('importKnowledge').value.trim(), displayName: $('importName').value.trim(), knowledgeLanguage: 'zh-CN', confirmGitInit: $('importForm').elements.confirmGitInit.checked, replaceManagedHookProjectId: $('importForm').elements.confirmReplaceHook.checked ? state.preflight?.checks.hook?.replacementProjectId : undefined }; }
|
|
273
279
|
const problemLabels = { KNOWLEDGE_ROOT_MISSING: '请在系统状态中设置知识根目录。', AI_PROFILE_REQUIRED: '请配置可用模型;后台默认模型在系统状态中设置。', DUPLICATE_PROJECT: '此项目已导入,请打开已有项目。', HOOK_CONFLICT: '存在第三方 Hook,不会覆盖。请先检查冲突。', PATH_MISSING: '代码目录不存在。', PATH_REQUIRED: '请选择代码目录。' };
|
|
274
280
|
async function checkImport() {
|
|
275
281
|
const epoch = ++state.preflightEpoch; $('confirmImport').disabled = true;
|
|
@@ -279,12 +285,12 @@
|
|
|
279
285
|
try {
|
|
280
286
|
const result = await api('/api/projects/preflight-import', { method: 'POST', body: importInput(), allowNotReady: true });
|
|
281
287
|
if (epoch !== state.preflightEpoch || !$('importDialog').open) return;
|
|
282
|
-
state.preflight = result; $('gitInitChoice').hidden = !result.plannedGitInit;
|
|
288
|
+
state.preflight = result; $('gitInitChoice').hidden = !result.plannedGitInit; $('replaceHookChoice').hidden = !result.checks.hook?.replacementProjectId;
|
|
283
289
|
const messages = result.problems.map(p => problemLabels[p.code] || p.message);
|
|
284
290
|
$('setupImportRoot').hidden = !result.problems.some(p => p.code === 'KNOWLEDGE_ROOT_MISSING');
|
|
285
291
|
$('preflightStatus').textContent = result.ready ? result.plannedGitInit ? '目录检查通过。请确认初始化 Git;不分析历史。' : '检查通过。只建立当前提交的跟踪基线,不分析历史。' : [...new Set(messages)].join('\n');
|
|
286
292
|
$('preflightStatus').classList.toggle('issue', !result.ready);
|
|
287
|
-
$('confirmImport').disabled = !result.ready || result.plannedGitInit && !$('importForm').elements.confirmGitInit.checked || !$('importKnowledge').value.trim();
|
|
293
|
+
$('confirmImport').disabled = !result.ready || result.plannedGitInit && !$('importForm').elements.confirmGitInit.checked || result.checks.hook?.replacementProjectId && !$('importForm').elements.confirmReplaceHook.checked || !$('importKnowledge').value.trim();
|
|
288
294
|
} catch (error) { if (epoch === state.preflightEpoch) $('preflightStatus').textContent = error.message; }
|
|
289
295
|
}
|
|
290
296
|
document.addEventListener('click', e => {
|
|
@@ -292,7 +298,7 @@
|
|
|
292
298
|
const execute = async () => {
|
|
293
299
|
if (button.dataset.view) setView(button.dataset.view);
|
|
294
300
|
if (button.dataset.go) { if (button.dataset.go === 'memory') state.memoryTab = 'candidate'; setView(button.dataset.go); }
|
|
295
|
-
if (button.dataset.close) $(button.dataset.close).close();
|
|
301
|
+
if (button.dataset.close && !(button.dataset.close === 'unbindDialog' && unbinding)) $(button.dataset.close).close();
|
|
296
302
|
if (button.dataset.selectProject) selectProject(button.dataset.selectProject);
|
|
297
303
|
if (button.dataset.scope) { state.scope = button.dataset.scope; all('[data-scope]').forEach(b => b.classList.toggle('active', b.dataset.scope === state.scope)); post('vectorHub', { type: 'kb:scope', scope: state.scope }); }
|
|
298
304
|
if (button.dataset.memoryTab) { state.memoryTab = button.dataset.memoryTab; renderMemories(); }
|
|
@@ -300,7 +306,7 @@
|
|
|
300
306
|
if (button.dataset.action === 'import') openImport();
|
|
301
307
|
if (button.dataset.action === 'repair') await repair();
|
|
302
308
|
if (button.dataset.action === 'system') await openSystem();
|
|
303
|
-
if (button.dataset.action === 'unbind') { const p = activeProject(); if (p &&
|
|
309
|
+
if (button.dataset.action === 'unbind') { const p = activeProject(); if (p && !unbinding) { unbindProjectId = p.projectId; $('unbindProjectName').textContent = `解绑「${p.name}」?`; $('unbindFeedback').textContent = ''; $('unbindDialog').showModal(); } }
|
|
304
310
|
if (button.dataset.retryRun) { await api(`/api/engine/runs/${encodeURIComponent(button.dataset.retryRun)}/retry`, { method: 'POST', body: {} }); await refreshSnapshot(); }
|
|
305
311
|
if (button.dataset.contextIndex !== undefined) await openReference(state.contexts.get(state.projectId).references[Number(button.dataset.contextIndex)]);
|
|
306
312
|
if (button.dataset.memoryAction) {
|
|
@@ -355,12 +361,32 @@
|
|
|
355
361
|
on('createDomain', 'click', () => { $('domainForm').reset(); $('domainDialog').showModal(); });
|
|
356
362
|
on('domainForm', 'submit', async e => { e.preventDefault(); await api('/api/domains', { method: 'POST', body: Object.fromEntries(new FormData(e.target)) }); $('domainDialog').close(); await refreshRegistry(); });
|
|
357
363
|
on('memoryForm', 'submit', async e => { e.preventDefault(); await api(`/api/engine/memories/${encodeURIComponent(editingMemoryId)}`, { method: 'PATCH', body: { text: $('memoryText').value } }); $('memoryDialog').close(); await refreshSnapshot(); });
|
|
364
|
+
on('unbindForm', 'submit', async e => {
|
|
365
|
+
e.preventDefault(); if (!unbindProjectId || unbinding) return;
|
|
366
|
+
const projectId = unbindProjectId;
|
|
367
|
+
unbinding = true; $('confirmUnbind').disabled = true; $('confirmUnbind').textContent = '正在解绑…';
|
|
368
|
+
$('unbindFeedback').textContent = '正在停止项目接线并移除登记,磁盘文件不会删除。';
|
|
369
|
+
try {
|
|
370
|
+
await api(`/api/projects/${encodeURIComponent(projectId)}`, { method: 'DELETE', body: { deleteKnowledge: false } });
|
|
371
|
+
state.contexts.delete(projectId);
|
|
372
|
+
state.projects = state.projects.filter(project => project.projectId !== projectId);
|
|
373
|
+
if (state.projectId === projectId) { state.projectId = state.projects[0]?.projectId || ''; state.epoch++; }
|
|
374
|
+
renderProjectOptions(); render(); Object.keys(frames).forEach(frameContext);
|
|
375
|
+
$('unbindDialog').close(); unbindProjectId = '';
|
|
376
|
+
// A late read started before DELETE cannot restore the removed project.
|
|
377
|
+
await refreshRegistry(true); localStorage.setItem('kb.project', state.projectId);
|
|
378
|
+
Object.keys(frames).forEach(frameContext);
|
|
379
|
+
toast('项目已从列表移除,代码、知识和会话仍保留。');
|
|
380
|
+
} catch (error) { $('unbindFeedback').textContent = error.message; }
|
|
381
|
+
finally { unbinding = false; $('confirmUnbind').disabled = false; $('confirmUnbind').textContent = '解绑并保留数据'; }
|
|
382
|
+
});
|
|
383
|
+
on('unbindDialog', 'cancel', e => { if (unbinding) e.preventDefault(); });
|
|
358
384
|
on('importForm', 'input', e => {
|
|
359
385
|
if (e.target.id === 'importCode') { const name = e.target.value.trim().split(/[\\/]/).filter(Boolean).pop() || ''; $('importName').value = name; if (state.root) $('importKnowledge').value = `${state.root.replace(/[\\/]+$/, '')}/${name}-${state.importId.slice(-6)}`; }
|
|
360
386
|
clearTimeout(preflightTimer); $('confirmImport').disabled = true; preflightTimer = setTimeout(checkImport, 250);
|
|
361
387
|
});
|
|
362
388
|
on('importForm', 'submit', async e => {
|
|
363
|
-
e.preventDefault(); if (!state.preflight?.ready || state.preflight.plannedGitInit && !$('importForm').elements.confirmGitInit.checked) return;
|
|
389
|
+
e.preventDefault(); if (!state.preflight?.ready || state.preflight.plannedGitInit && !$('importForm').elements.confirmGitInit.checked || state.preflight.checks.hook?.replacementProjectId && !$('importForm').elements.confirmReplaceHook.checked) return;
|
|
364
390
|
$('confirmImport').disabled = true; $('confirmImport').textContent = '正在登记…';
|
|
365
391
|
try { const result = await api('/api/projects/import', { method: 'POST', body: importInput() }); state.projectId = result.projectId; state.epoch++; $('importDialog').close(); await refreshRegistry(); Object.keys(frames).forEach(frameContext); setView('projects'); toast(Object.values(result.modules).every(m => m.ok) ? '项目已导入,四个模块已连接。' : '项目已导入,部分接线待恢复。'); }
|
|
366
392
|
finally { $('confirmImport').textContent = '导入项目'; if ($('importDialog').open) checkImport(); }
|
package/vendor-manifest.json
CHANGED
|
@@ -3,19 +3,18 @@
|
|
|
3
3
|
"modules": {
|
|
4
4
|
"knowledge-engine": {
|
|
5
5
|
"name": "@sanqianx/ai-coding-knowledge-engine",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.2.0",
|
|
7
7
|
"root": "_modules/knowledge-engine",
|
|
8
8
|
"files": {
|
|
9
|
-
"README.md": "
|
|
10
|
-
"package.json": "
|
|
9
|
+
"README.md": "c12b9bc9aa49dce6eaad0e6f3fd611466ab1ef6b17d2bdfb69a6dab6a42ab5ed",
|
|
10
|
+
"package.json": "6740fa3dec72ebae49fa903a4b5f8ddc2adf52d11ba9f7c4bc1f36a603636578",
|
|
11
11
|
"src/analysis-contract.js": "90354d1e9b4eff74fdd9d47e097cdb5d47944785c2be66811f64e5958c94d8e7",
|
|
12
12
|
"src/analysis-worker.js": "a105cabe6e1573b2d986b1e7a5aaf615e2fb53524156fc8e4e2c0a71f2c97e55",
|
|
13
13
|
"src/bin.js": "22208907431d0f3a971a669c7e5eb44b3b7af9bbb36fd7ba17c3b90c4288bd70",
|
|
14
14
|
"src/bridge-client.js": "ae0dba21e4286694bcd9e064ca36564ec997969ae1eb1ce4f8a6665e447cbffe",
|
|
15
|
-
"src/coordinator.js": "
|
|
15
|
+
"src/coordinator.js": "e85f1f604d2090bd466244d73cf8e1862e0cbe436e62d4a2f7193afd2fac7843",
|
|
16
16
|
"src/headless-analyzer.js": "2473b28d4bf06088abbcafa21d491560a9ca0ab100024c912a36bc6f981d8576",
|
|
17
17
|
"src/memories.js": "0a8667e6daba99fa5022393951a28e96cdadf98af85b6af9a6be96c7f4e896fc",
|
|
18
|
-
"src/migration.js": "b018871727f3277e4a24350fef1911fbb89e2158578339210d88efc2e309be32",
|
|
19
18
|
"src/retrieval.js": "acbf76bb93fd13dac391c2e918891a56ace869db5583d2a5994641b6782496d6",
|
|
20
19
|
"src/runtime/README.md": "9be676eca76a7857b34d7357194e6f0ee733c22e61d962d53984715dbec7f673",
|
|
21
20
|
"src/runtime/atomic-file.js": "9846b4b69db5ded811acf19a1181ef93615d93efaa542b858e552a7f3124ad23",
|
|
@@ -35,10 +34,10 @@
|
|
|
35
34
|
"src/runtime/layout.js": "c48fa1db62e5df1edefe13218e470c5ceea4ef2781293512c2c18e5c2ba0c7e9",
|
|
36
35
|
"src/runtime/markdown-knowledge-indexer.js": "44a76a9adefad07e78f77b1106b10d6c1cc7873d76362df4c546892b21f3939c",
|
|
37
36
|
"src/runtime/scanner.js": "d99fa8d74a6fbbaae75ca49f62127e14cf9e48b1404e4b4d597ef39df5441410",
|
|
38
|
-
"src/server.js": "
|
|
39
|
-
"src/store.js": "
|
|
37
|
+
"src/server.js": "e7f0b07b7a8cf2623690b9d2d23492f2c95737dc974adfeea049610a4b5ded55",
|
|
38
|
+
"src/store.js": "71f0417c2c1e42b89429e3dee1adff7b2c09e86682cb3f4843ce6d9e5aafd7ec",
|
|
40
39
|
"ui/app.css": "7ad370c269e60a7b5ec907d81a607b2d16bd07b2d7c22a81f102dab51eeb2889",
|
|
41
|
-
"ui/app.js": "
|
|
40
|
+
"ui/app.js": "91ee6d8c31712e6af46de2eecbd72bbc5236c06b7cd055800831e7d57f84a0be",
|
|
42
41
|
"ui/index.html": "ce3565cfefd4838b11d37a91a906ef26ca5a1834844fe3f7c897b56851a3c418"
|
|
43
42
|
}
|
|
44
43
|
}
|