@sanqianx/project-knowledge 4.7.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.
Files changed (196) hide show
  1. package/.agents/plugins/marketplace.json +17 -17
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/CHANGELOG.md +836 -799
  4. package/README.md +58 -41
  5. package/_modules/knowledge-engine/README.md +91 -0
  6. package/_modules/knowledge-engine/package.json +38 -0
  7. package/_modules/knowledge-engine/src/analysis-contract.js +25 -0
  8. package/_modules/knowledge-engine/src/analysis-worker.js +54 -0
  9. package/_modules/knowledge-engine/src/bin.js +52 -0
  10. package/_modules/knowledge-engine/src/bridge-client.js +24 -0
  11. package/_modules/knowledge-engine/src/coordinator.js +263 -0
  12. package/_modules/knowledge-engine/src/headless-analyzer.js +75 -0
  13. package/_modules/knowledge-engine/src/memories.js +85 -0
  14. package/_modules/knowledge-engine/src/retrieval.js +116 -0
  15. package/_modules/knowledge-engine/src/runtime/README.md +7 -0
  16. package/_modules/knowledge-engine/src/runtime/atomic-file.js +204 -0
  17. package/_modules/knowledge-engine/src/runtime/automation-config.js +140 -0
  18. package/_modules/knowledge-engine/src/runtime/commit-conversation-binder.js +249 -0
  19. package/_modules/knowledge-engine/src/runtime/commit-processing-ledger.js +26 -0
  20. package/_modules/knowledge-engine/src/runtime/commit-prompt.js +202 -0
  21. package/_modules/knowledge-engine/src/runtime/commit-reconciler.js +432 -0
  22. package/_modules/knowledge-engine/src/runtime/content-hash.js +4 -0
  23. package/_modules/knowledge-engine/src/runtime/contracts.js +279 -0
  24. package/_modules/knowledge-engine/src/runtime/conversation-exclusions.js +204 -0
  25. package/_modules/knowledge-engine/src/runtime/conversation-store.js +387 -0
  26. package/_modules/knowledge-engine/src/runtime/evidence-bundle.js +228 -0
  27. package/_modules/knowledge-engine/src/runtime/git-runner.js +62 -0
  28. package/_modules/knowledge-engine/src/runtime/knowledge-promotion.js +444 -0
  29. package/_modules/knowledge-engine/src/runtime/knowledge-retrieval-service.js +402 -0
  30. package/_modules/knowledge-engine/src/runtime/layout.js +44 -0
  31. package/_modules/knowledge-engine/src/runtime/markdown-knowledge-indexer.js +230 -0
  32. package/_modules/knowledge-engine/src/runtime/scanner.js +298 -0
  33. package/_modules/knowledge-engine/src/server.js +154 -0
  34. package/_modules/knowledge-engine/src/store.js +236 -0
  35. package/_modules/knowledge-engine/ui/app.css +172 -0
  36. package/_modules/knowledge-engine/ui/app.js +297 -0
  37. package/_modules/knowledge-engine/ui/index.html +95 -0
  38. package/_site/README.md +30 -30
  39. package/_site/_test/ai-profile-resolver-test.js +137 -137
  40. package/_site/_test/automation-queue-test.js +14 -14
  41. package/_site/_test/automation-ui-test.js +40 -40
  42. package/_site/_test/background-task-registry-test.js +43 -43
  43. package/_site/_test/baseline-schema-test.js +91 -91
  44. package/_site/_test/chat-claudecodeui-match-test.js +92 -92
  45. package/_site/_test/claude-executable-discovery-test.js +62 -62
  46. package/_site/_test/claude-workbench-test.js +170 -170
  47. package/_site/_test/codex-conversation-projection-test.js +2 -1
  48. package/_site/_test/commit-conversation-binding-test.js +95 -95
  49. package/_site/_test/commit-evidence-test.js +110 -110
  50. package/_site/_test/conversation-api-test.js +109 -109
  51. package/_site/_test/conversation-store-test.js +136 -136
  52. package/_site/_test/desktop-browser-compat-test.js +28 -28
  53. package/_site/_test/desktop-hook-runtime-regression-test.js +204 -204
  54. package/_site/_test/explicit-commit-processor-test.js +37 -37
  55. package/_site/_test/fixtures/make-git-repos.js +12 -1
  56. package/_site/_test/folder-picker-output-test.js +19 -19
  57. package/_site/_test/fresh-hook-reimport-test.js +53 -0
  58. package/_site/_test/fresh-product-data-test.js +42 -0
  59. package/_site/_test/full-integration-e2e-test.js +165 -165
  60. package/_site/_test/git-validation-test.js +2 -2
  61. package/_site/_test/hook-runtime-endpoint-test.js +88 -88
  62. package/_site/_test/hook-status-repair-api-test.js +152 -152
  63. package/_site/_test/hook-trigger-test.js +85 -85
  64. package/_site/_test/import-preflight-api-test.js +234 -233
  65. package/_site/_test/index-writer-concurrency-test.js +135 -135
  66. package/_site/_test/integration-adapters-test.js +171 -171
  67. package/_site/_test/integration-surface-coverage-test.js +164 -164
  68. package/_site/_test/knowledge-language-control-test.js +170 -170
  69. package/_site/_test/knowledge-promotion-recovery-test.js +260 -260
  70. package/_site/_test/knowledge-query-test.js +54 -54
  71. package/_site/_test/knowledge-retrieval-service-test.js +77 -77
  72. package/_site/_test/knowledge-storage-startup-test.js +52 -52
  73. package/_site/_test/knowledge-store-logs-supervision-test.js +89 -89
  74. package/_site/_test/logging-api-test.js +89 -89
  75. package/_site/_test/logging-sse-no-gap-test.js +88 -88
  76. package/_site/_test/logging-ui-test.js +106 -104
  77. package/_site/_test/markdown-delta-overlay-test.js +85 -85
  78. package/_site/_test/markdown-maintenance-api-test.js +75 -75
  79. package/_site/_test/mcp-server-test.js +149 -149
  80. package/_site/_test/module-artifact-integrity-test.js +27 -0
  81. package/_site/_test/module-boundary-test.js +30 -0
  82. package/_site/_test/module-bridge-eventbridge-test.js +7 -0
  83. package/_site/_test/module-model-configuration-test.js +44 -0
  84. package/_site/_test/module-process-lifecycle-test.js +24 -0
  85. package/_site/_test/module-stream-proxy-test.js +34 -0
  86. package/_site/_test/non-release-ci-test.js +34 -34
  87. package/_site/_test/offline-boundary-isolation-test.js +32 -32
  88. package/_site/_test/packaged-ui-smoke-test.js +11 -9
  89. package/_site/_test/path-consistency-test.js +151 -151
  90. package/_site/_test/pending-sweep-test.js +9 -9
  91. package/_site/_test/post-commit-automation-test.js +87 -87
  92. package/_site/_test/product-diagnostics-ui-test.js +75 -0
  93. package/_site/_test/product-import-ui-test.js +92 -0
  94. package/_site/_test/project-delete-recovery-test.js +64 -64
  95. package/_site/_test/project-goal-editor-test.js +144 -144
  96. package/_site/_test/project-lifecycle-transaction-test.js +106 -106
  97. package/_site/_test/prompt-settings-test.js +115 -115
  98. package/_site/_test/protected-architecture-gate-test.js +122 -122
  99. package/_site/_test/refactor-characterization-test.js +36 -36
  100. package/_site/_test/release-version-sync-test.js +1 -1
  101. package/_site/_test/requirement-recorder-test.js +173 -173
  102. package/_site/_test/run-all-tests.js +159 -156
  103. package/_site/_test/shared-contracts-test.js +58 -58
  104. package/_site/_test/startup-analysis-disabled-test.js +10 -10
  105. package/_site/_test/storage-foundation-test.js +78 -78
  106. package/_site/_test/structured-logger-test.js +101 -101
  107. package/_site/_test/tracking-start-test.js +109 -108
  108. package/_site/_test/workbench-permission-test.js +117 -117
  109. package/_site/_test/workspace-ui-contract-test.js +28 -43
  110. package/_site/lib/ai-profile-resolver.js +78 -78
  111. package/_site/lib/ai-workspace.js +101 -101
  112. package/_site/lib/automation-config.js +140 -140
  113. package/_site/lib/bridge-adapter.js +1 -0
  114. package/_site/lib/bridge-consumer-service.js +402 -402
  115. package/_site/lib/claude-cli-runner.js +1510 -1510
  116. package/_site/lib/commit-conversation-binder.js +234 -234
  117. package/_site/lib/commit-processing-ledger.js +27 -27
  118. package/_site/lib/commit-prompt.js +202 -202
  119. package/_site/lib/commit-reconciler.js +406 -406
  120. package/_site/lib/contracts.js +284 -279
  121. package/_site/lib/conversation-query-service.js +144 -144
  122. package/_site/lib/conversation-store.js +385 -385
  123. package/_site/lib/data-dir.js +6 -74
  124. package/_site/lib/engine-connection.js +67 -0
  125. package/_site/lib/evidence-bundle.js +228 -228
  126. package/_site/lib/folder-picker-output.js +21 -21
  127. package/_site/lib/git-runner.js +62 -62
  128. package/_site/lib/github-team-store.js +1172 -1172
  129. package/_site/lib/hook-manager.js +203 -202
  130. package/_site/lib/index-service.js +173 -173
  131. package/_site/lib/integration-installer.js +908 -907
  132. package/_site/lib/kb-framework.js +176 -176
  133. package/_site/lib/kb-validator.js +124 -124
  134. package/_site/lib/knowledge-promotion.js +435 -435
  135. package/_site/lib/knowledge-retrieval-service.js +401 -401
  136. package/_site/lib/knowledge-tool-runtime.js +359 -359
  137. package/_site/lib/llm-client.js +161 -161
  138. package/_site/lib/markdown-knowledge-indexer.js +230 -230
  139. package/_site/lib/module-bridge.js +191 -36
  140. package/_site/lib/post-commit-automation.js +87 -87
  141. package/_site/lib/product-data.js +24 -0
  142. package/_site/lib/project-lifecycle-service.js +535 -497
  143. package/_site/lib/project-store.js +268 -262
  144. package/_site/lib/requirement-recorder.js +278 -278
  145. package/_site/lib/runtime-endpoint.js +155 -155
  146. package/_site/lib/scanner.js +298 -298
  147. package/_site/lib/server-app.js +1579 -1520
  148. package/_site/lib/settings-store.js +133 -113
  149. package/_site/lib/storage-layout.js +162 -162
  150. package/_site/lib/structured-logger.js +574 -574
  151. package/_site/scripts/folder-picker.ps1 +156 -156
  152. package/_site/scripts/hook-trigger.js +157 -156
  153. package/_site/scripts/install-module-candidates.js +69 -0
  154. package/_site/scripts/pack-module-candidates.js +72 -0
  155. package/_site/scripts/sync-release-version.js +2 -2
  156. package/_site/scripts/vendor-modules.js +47 -0
  157. package/_site/scripts/verify-module-runtime.js +45 -0
  158. package/_site/scripts/verify-product-runtime.js +34 -0
  159. package/bin/project-knowledge-mcp.js +194 -194
  160. package/docs/README.zh-CN.md +42 -29
  161. package/docs/fresh-v5-start.md +28 -0
  162. package/docs/project-registry-schema.md +22 -22
  163. package/docs/testing-strategy.md +33 -33
  164. package/module-runtime-manifest.json +220 -0
  165. package/package.json +14 -9
  166. package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
  167. package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
  168. package/plugins/project-knowledge/.mcp.json +1 -1
  169. package/plugins/project-knowledge/opencode/project-knowledge.md +3 -3
  170. package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -28
  171. package/templates/project-readme.md +34 -34
  172. package/ui/favicon.svg +38 -38
  173. package/ui/index.html +52 -149
  174. package/ui/product.css +9 -0
  175. package/ui/product.js +412 -0
  176. package/vendor-manifest.json +45 -0
  177. package/_site/_test/data-dir-migration-test.js +0 -77
  178. package/_site/_test/import-ui-flow-test.js +0 -171
  179. package/_site/_test/knowledge-migration-test.js +0 -75
  180. package/_site/_test/legacy-forward-compat-test.js +0 -257
  181. package/_site/_test/legacy-project-upgrade-e2e-test.js +0 -363
  182. package/_site/_test/p0-data-migration-characterization-test.js +0 -37
  183. package/_site/_test/p0-e2e-gate-test.js +0 -347
  184. package/_site/_test/project-control-panel-task14-test.js +0 -63
  185. package/_site/_test/project-layout-v2-migration-test.js +0 -127
  186. package/_site/_test/server-runtime-migration-safety-test.js +0 -27
  187. package/_site/_test/task15-20-ui-flow-test.js +0 -148
  188. package/_site/_test/ui-i18n-toggle-test.js +0 -114
  189. package/_site/_test/ui-smoke-test.js +0 -73
  190. package/_site/_test/v4122-upgrade-data-contract-test.js +0 -56
  191. package/_site/lib/data-state-classifier.js +0 -40
  192. package/_site/lib/legacy-data-manifest.js +0 -26
  193. package/_site/lib/migration-service.js +0 -442
  194. package/ui/app.css +0 -58
  195. package/ui/app.js +0 -607
  196. package/ui/i18n.js +0 -146
package/ui/product.js ADDED
@@ -0,0 +1,412 @@
1
+ (() => {
2
+ 'use strict';
3
+ const $ = id => document.getElementById(id);
4
+ const all = selector => [...document.querySelectorAll(selector)];
5
+ const titles = { home: '智脑首页', work: '开始工作', search: '知识检索', memory: '长期记忆', projects: '领域与项目', activity: '活动记录' };
6
+ const modules = { terminal: 'Agent 工作台', vectorHub: '知识索引', eventBridge: '开发事件', engine: '知识引擎' };
7
+ const frames = {
8
+ terminal: { id: 'workFrame', url: '/modules/terminal/agent-terminal/?embed=1&apiBase=/api/terminal', view: 'work', ready: false },
9
+ vectorHub: { id: 'vectorFrame', url: '/modules/vectorhub/?embed=1&apiBase=/api/vectorhub&engineBase=/api/engine', view: 'search', ready: false },
10
+ eventBridge: { id: 'bridgeFrame', url: '/modules/eventbridge/?embed=1&apiBase=/api/eventbridge', view: 'activity', ready: false },
11
+ engine: { id: 'engineFrame', url: '/modules/engine/?embed=1&apiBase=/api/engine', ready: false },
12
+ };
13
+ const state = { brain: { id: 'brain-default', name: '我的智脑', domains: [] }, root: '', projects: [], projectId: localStorage.getItem('kb.project') || '', view: 'home', scope: 'project', memoryTab: 'confirmed', activityTab: 'updates', health: {}, snapshot: { projects: [], runs: [], events: [], memories: [] }, engineAvailable: false, epoch: 0, contexts: new Map(), stream: null, refreshing: false, importId: '', preflight: null, preflightEpoch: 0 };
14
+ const esc = value => String(value ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
15
+ const activeProject = () => state.projects.find(p => p.projectId === state.projectId);
16
+ const domainName = id => state.brain.domains.find(d => d.id === id)?.name || '未分组';
17
+ const projectName = id => state.projects.find(p => p.projectId === id)?.name || id;
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: '维护失败', cancelled: '已取消' })[run.state] || run.state;
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, registryEpoch = 0, registryPromise = null, resumeImport = false, unbindProjectId = '', unbinding = false;
22
+ async function api(url, options = {}) {
23
+ const { allowNotReady, ...init } = options;
24
+ if (init.body && typeof init.body !== 'string') init.body = JSON.stringify(init.body);
25
+ if (init.body) init.headers = { 'Content-Type': 'application/json', ...init.headers };
26
+ const response = await fetch(url, init);
27
+ const data = await response.json();
28
+ if (!response.ok || data.ok === false && !allowNotReady) throw Object.assign(new Error(data.error?.message || data.message || (typeof data.error === 'string' ? data.error : `请求失败 (${response.status})`)), { code: data.error?.code || data.error });
29
+ return data;
30
+ }
31
+ function toast(text) { $('toast').textContent = text; $('toast').hidden = false; clearTimeout(toastTimer); toastTimer = setTimeout(() => { $('toast').hidden = true; }, 4000); }
32
+ function on(id, event, callback) { $(id).addEventListener(event, e => Promise.resolve(callback(e)).catch(error => toast(error.message))); }
33
+ function post(key, message) { const frame = frames[key]; if (frame.ready) $(frame.id).contentWindow.postMessage(message, location.origin); }
34
+ function initializeFrame(key) { const frame = frames[key]; if (!frame.initialized) { $(frame.id).src = frame.url; frame.initialized = true; } }
35
+ function frameVisible(key) { return key === 'engine' ? $('engineDialog').open : state.view === frames[key].view && (key !== 'eventBridge' || state.activityTab === 'sessions'); }
36
+ function frameContext(key) {
37
+ const project = activeProject();
38
+ const frame = frames[key];
39
+ const ref = key === 'terminal' ? project?.modules.terminal.projectId || project?.workspacePath : key === 'eventBridge' ? project?.modules.eventBridge.projectId || project?.workspacePath : project?.projectId;
40
+ frame.requestId = `${state.epoch}:${key}`;
41
+ post(key, { type: 'kb:theme', theme: document.documentElement.dataset.theme });
42
+ post(key, { type: 'kb:scope', scope: state.scope });
43
+ post(key, { type: 'kb:sidebar', visible: key === 'terminal' });
44
+ post(key, { type: 'kb:select-project', project: ref || '', projectId: project?.projectId || '', requestId: frame.requestId });
45
+ post(key, { type: 'kb:visibility', visible: frameVisible(key) });
46
+ }
47
+ function setView(view) {
48
+ if (!titles[view]) view = 'home';
49
+ state.view = view;
50
+ history.replaceState(null, '', `#${view}`);
51
+ all('[data-view]').forEach(button => button.classList.toggle('active', button.dataset.view === view));
52
+ all('.view').forEach(section => section.classList.toggle('active', section.id === `view-${view}`));
53
+ $('pageTitle').textContent = titles[view];
54
+ if (view === 'work' && activeProject()) initializeFrame('terminal');
55
+ if (view === 'search' && activeProject()) initializeFrame('vectorHub');
56
+ if (view === 'activity' && state.activityTab === 'sessions' && activeProject()) initializeFrame('eventBridge');
57
+ Object.keys(frames).forEach(key => post(key, { type: 'kb:visibility', visible: frameVisible(key) }));
58
+ render();
59
+ }
60
+ function selectProject(id) {
61
+ if (id === state.projectId && activeProject()) return;
62
+ state.projectId = id; state.epoch++;
63
+ referenceEpoch++; $('referenceDialog').close();
64
+ if ($('logsDialog').open) loadLogs().catch(error => toast(error.message));
65
+ localStorage.setItem('kb.project', id);
66
+ $('projectSelect').value = id;
67
+ Object.keys(frames).forEach(frameContext);
68
+ render(); setView(state.view);
69
+ }
70
+ function renderProjectOptions() {
71
+ $('projectSelect').innerHTML = state.projects.length ? state.projects.map(p => `<option value="${esc(p.projectId)}">${esc(p.name)}</option>`).join('') : '<option value="">尚未导入项目</option>';
72
+ $('projectSelect').disabled = !state.projects.length;
73
+ $('projectSelect').value = state.projectId;
74
+ }
75
+ function render() {
76
+ const project = activeProject();
77
+ $('brainLabel').textContent = state.brain.name; $('homeTitle').textContent = state.brain.name;
78
+ $('breadcrumb').textContent = project ? `${state.brain.name} / ${domainName(project.domainId)} / ${project.name}` : state.brain.name;
79
+ $('workEmpty').hidden = !!project; $('workLayout').hidden = !project;
80
+ $('searchEmpty').hidden = !!project; $('vectorFrame').parentElement.hidden = !project;
81
+ const candidates = state.snapshot.memories.filter(m => m.status === 'candidate');
82
+ $('memoryCount').textContent = candidates.length; $('memoryCount').hidden = !candidates.length; $('candidateCount').textContent = candidates.length || '';
83
+ if (state.view === 'home') renderHome(candidates);
84
+ if (state.view === 'projects') renderProjects();
85
+ if (state.view === 'memory') renderMemories();
86
+ if (state.view === 'activity') renderActivity();
87
+ renderContext();
88
+ }
89
+ function renderHome(candidates) {
90
+ if (!state.projects.length) { $('homeContent').innerHTML = '<div class="empty"><h2>从第一个项目开始</h2><p>选择代码目录和知识目录,保留项目原有文件。</p><button class="primary" data-action="import">导入第一个项目</button></div>'; return; }
91
+ const runs = state.snapshot.runs.filter(r => !state.projectId || r.projectId === state.projectId);
92
+ const issues = runs.filter(r => ['failed', 'index-pending'].includes(r.state));
93
+ const scopeIssues = (state.snapshot.sharedScopes || []).filter(s => s.brainId === state.brain.id && s.index?.status === 'pending');
94
+ const projectRows = state.projects.map(p => `<div class="row"><div><b>${esc(p.name)}</b><small>${esc(domainName(p.domainId))}</small></div>${badge(Object.values(p.modules).every(m => m.registered) ? '已连接' : '接线待恢复', Object.values(p.modules).every(m => m.registered) ? 'up' : 'issue')}<button data-select-project="${esc(p.projectId)}">打开</button></div>`).join('');
95
+ const updates = runs.slice(0, 5).map(r => `<div class="row"><time>${time(r.startedAt)}</time><div><b>${esc(projectName(r.projectId))}</b><small>${esc(r.commitSha.slice(0, 8))} · ${esc(label(r))}</small></div></div>`).join('') || '<p>尚无维护记录。导入时不会分析历史提交。</p>';
96
+ $('homeContent').innerHTML = `<div class="grid2"><div class="stack"><section class="panel"><h2>领域与项目</h2>${projectRows}</section><section class="panel"><h2>最近知识更新</h2>${state.engineAvailable ? updates : '<p>知识引擎暂时不可用,恢复后显示真实记录。</p>'}</section></div><div class="stack"><section class="panel"><h2>待确认记忆</h2>${candidates.length ? candidates.slice(0, 3).map(m => `<div class="row"><div><b>${esc(m.text)}</b><small>${esc(projectName(m.evidence.projectId))} · ${esc(m.evidence.commitSha.slice(0, 8))}</small></div></div>`).join('') + '<button data-go="memory">查看候选箱</button>' : '<p>没有待确认记忆。智脑不会自动把项目行为当作你的偏好。</p>'}</section><section class="panel"><h2>需要处理</h2>${issues.length ? issues.slice(0, 3).map(r => `<div class="row"><div><b>${esc(projectName(r.projectId))}</b><small>${esc(r.error?.message || label(r))}</small></div></div>`).join('') + '<button data-go="activity">查看运行详情</button>' : scopeIssues.length ? '' : '<p>没有待处理的维护异常。</p>'}${scopeIssues.map(s => `<div class="row"><div><b>${esc(s.name)}</b><small>${esc(s.index.message)}</small></div></div>`).join('')}${scopeIssues.length ? '<button data-action="system">检查索引连接</button>' : ''}${!state.engineAvailable ? '<p>知识引擎离线,请检查系统状态。</p>' : ''}</section></div></div>`;
97
+ }
98
+ function renderProjects() {
99
+ const group = (id, name) => `<h3>${esc(name)}</h3>${state.projects.filter(p => (p.domainId || '') === id).map(p => `<button class="${p.projectId === state.projectId ? 'active' : ''}" data-select-project="${esc(p.projectId)}">${esc(p.name)}</button>`).join('') || '<small>暂无项目</small>'}`;
100
+ $('projectTree').innerHTML = `<h2>${esc(state.brain.name)}</h2>${state.brain.domains.map(d => group(d.id, d.name)).join('')}${group('', '未分组')}`;
101
+ const project = activeProject();
102
+ if (!project) { $('projectDetail').innerHTML = '<div class="empty"><h2>尚未导入项目</h2><button class="primary" data-action="import">导入第一个项目</button></div>'; return; }
103
+ $('projectDetail').innerHTML = `<h2>${esc(project.name)}</h2><div class="detail-label">代码目录</div><div class="detail-path">${esc(project.workspacePath)}</div><div class="detail-label">知识目录</div><div class="detail-path">${esc(project.knowledgePath)}</div><label>所属领域<select id="projectDomain"><option value="">未分组</option>${state.brain.domains.map(d => `<option value="${esc(d.id)}" ${d.id === project.domainId ? 'selected' : ''}>${esc(d.name)}</option>`).join('')}</select></label><h3>项目连接</h3>${Object.entries(modules).map(([key, name]) => `<div class="row"><div>${esc(name)}</div>${badge(project.modules[key]?.registered ? '已连接' : '待恢复', project.modules[key]?.registered ? 'up' : 'issue')}</div>`).join('')}<div class="actions"><button class="primary" data-go="work">开始工作</button><button data-action="repair">修复接线</button><button data-action="unbind">解绑并保留数据</button></div>`;
104
+ }
105
+ function renderMemories() {
106
+ all('[data-memory-tab]').forEach(button => button.classList.toggle('active', button.dataset.memoryTab === state.memoryTab));
107
+ const memories = state.snapshot.memories.filter(m => m.status === state.memoryTab);
108
+ $('memoryContent').innerHTML = !state.engineAvailable ? '<div class="empty">知识引擎暂时不可用。未读取到的记忆不会显示为已确认。</div>' : memories.length ? `<div class="memory-grid">${memories.map(m => `<article class="memory-card"><p>${esc(m.text)}</p><small>${esc(projectName(m.evidence.projectId))} · commit ${esc(m.evidence.commitSha.slice(0, 8))}<br>${esc(m.evidence.entryId)}</small><div class="actions"><button data-memory-action="evidence" data-memory-id="${esc(m.id)}">查看依据</button><button data-memory-action="edit" data-memory-id="${esc(m.id)}">编辑</button>${m.status === 'candidate' ? `<button class="primary" data-memory-action="confirm" data-memory-id="${esc(m.id)}">确认</button><button data-memory-action="ignore" data-memory-id="${esc(m.id)}">忽略</button>` : badge('已确认', 'up')}</div></article>`).join('')}</div>` : `<div class="empty">${state.memoryTab === 'confirmed' ? '还没有已确认的长期记忆。' : '候选箱为空。维护产物中的有据偏好会进入这里,等待你的确认。'}</div>`;
109
+ }
110
+ function renderActivity() {
111
+ all('[data-activity-tab]').forEach(button => button.classList.toggle('active', button.dataset.activityTab === state.activityTab));
112
+ $('activityUpdates').hidden = state.activityTab !== 'updates'; $('activitySessions').hidden = state.activityTab !== 'sessions';
113
+ const runs = state.snapshot.runs.filter(r => r.projectId === state.projectId);
114
+ const open = new Set(all('.run[open]').map(d => d.dataset.runId));
115
+ $('activityUpdates').innerHTML = state.engineAvailable ? runs.map(r => `<details class="run" data-run-id="${esc(r.runId)}" ${open.has(r.runId) ? 'open' : ''}><summary><time>${time(r.startedAt)}</time><strong>${esc(projectName(r.projectId))} · ${esc(r.commitSha.slice(0, 8))}</strong>${badge(label(r), ['failed', 'index-pending'].includes(r.state) ? 'issue' : r.state === 'succeeded' ? 'up' : '')}</summary><div><p>提交:${esc(r.commitSha)}<br>阶段:${esc(r.phase)} · 入口:${esc(r.trigger)}<br>知识晋升:${time(r.promotedAt)} · 索引:${esc(r.indexState)}</p>${r.error ? `<p class="error">${esc(r.error.message)} (${esc(r.error.code)})</p>` : ''}${(r.staging?.operations || []).map(op => `<div class="operation">${esc(op.operation)} · ${esc(op.path)}<br>${esc(op.reason)}</div>`).join('')}${['failed', 'index-pending'].includes(r.state) ? `<button data-retry-run="${esc(r.runId)}">重试失败阶段</button>` : ''}</div></details>`).join('') || '<div class="empty">当前项目还没有知识维护记录。</div>' : '<div class="empty">知识引擎离线。记录仍保留在本机,连接恢复后自动读取。</div>';
116
+ }
117
+ function renderContext() {
118
+ const saved = state.contexts.get(state.projectId);
119
+ const context = saved?.status === 'not-retrieved' ? null : saved;
120
+ $('contextScope').textContent = context ? ({ ready: '已检索知识', 'file-fallback': '索引暂不可用,已检索原文件', unavailable: '知识服务不可用,未注入上下文', unregistered: '项目尚未登记知识库', disabled: '未启用知识连接' })[context.status] || '本次任务上下文' : '任务发送前自动检索';
121
+ $('contextContent').classList.toggle('small-empty', !context?.references?.length);
122
+ const seen = new Set();
123
+ const references = (context?.references || []).map((r, index) => ({ ...r, index })).filter(r => { const key = `${r.spaceId}:${r.projectId}:${r.entryId}:${r.commitSha || ''}`; if (seen.has(key)) return false; seen.add(key); return true; });
124
+ $('contextContent').classList.toggle('empty', !references.length);
125
+ $('contextContent').innerHTML = references.length ? references.map(r => `<button class="reference" data-context-index="${r.index}"><b>${esc(r.title)}</b><small>${esc(projectName(r.projectId))} / ${esc(r.entryId)}${r.commitSha ? ` · ${esc(r.commitSha.slice(0, 8))}` : ''}</small><p>${esc(r.text.slice(0, 180))}</p></button>`).join('') : context ? '本次任务未找到可引用的知识。' : '发送任务后显示实际检索到的项目事实、领域经验和已确认记忆。';
126
+ }
127
+ async function openReference(reference) {
128
+ const id = state.projectId;
129
+ const epoch = ++referenceEpoch;
130
+ $('referenceTitle').textContent = reference.title || reference.entryId;
131
+ $('referenceMeta').textContent = `${projectName(reference.projectId)} / ${reference.entryId}${reference.commitSha ? ` · commit ${reference.commitSha}` : ''}`;
132
+ $('referenceContent').textContent = reference.text || '读取知识文件…'; $('referenceDialog').showModal();
133
+ if (reference.entryId) {
134
+ const result = await api('/api/engine/get', { method: 'POST', body: { projectId: state.projects.some(p => p.projectId === reference.projectId) ? reference.projectId : id, spaceId: reference.spaceId, entryId: reference.entryId } });
135
+ if (epoch === referenceEpoch && id === state.projectId && $('referenceDialog').open) $('referenceContent').textContent = result.chunks.map(c => c.chunk_text).join('\n');
136
+ }
137
+ }
138
+ async function refreshSnapshot() {
139
+ const epoch = ++snapshotEpoch;
140
+ try { const snapshot = await api('/api/engine/snapshot'); if (epoch !== snapshotEpoch || !navigator.onLine) return; state.snapshot = snapshot; state.engineAvailable = true; }
141
+ catch { if (epoch !== snapshotEpoch) return; state.engineAvailable = false; }
142
+ render();
143
+ }
144
+ function scheduleRefresh() { if (!navigator.onLine) return; clearTimeout(refreshTimer); refreshTimer = setTimeout(() => { refreshSnapshot(); post('eventBridge', { type: 'kb:refresh' }); }, 160); }
145
+ function connectProgress() {
146
+ if (!navigator.onLine || !state.health.engine?.up || state.stream) return;
147
+ const stream = new EventSource('/api/engine/progress'); state.stream = stream;
148
+ stream.addEventListener('snapshot', () => { if (state.stream !== stream) return; $('activityConnection').textContent = '已连接实时更新'; scheduleRefresh(); });
149
+ stream.addEventListener('progress', () => { if (state.stream === stream) scheduleRefresh(); });
150
+ stream.onerror = () => { if (state.stream === stream) $('activityConnection').textContent = '连接中断,正在恢复快照'; };
151
+ }
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 () => {
157
+ try {
158
+ const [brain, projects, health] = await Promise.all([api('/api/brain'), api('/api/projects/aggregated'), api('/api/modules/health')]);
159
+ if (epoch !== registryEpoch) return;
160
+ state.brain = brain.brain; state.root = brain.knowledgeRoot; state.projects = projects.projects; state.health = health.modules; state.ownership = health.ownership;
161
+ if (!activeProject()) { state.projectId = state.projects[0]?.projectId || ''; state.epoch++; Object.keys(frames).forEach(frameContext); }
162
+ renderProjectOptions(); renderSystem(); connectProgress(); await refreshSnapshot(); setView(state.view);
163
+ } catch (error) { toast(error.message); }
164
+ finally { if (epoch === registryEpoch) { registryPromise = null; connectProgress(); } }
165
+ })();
166
+ registryPromise = job;
167
+ return job;
168
+ }
169
+ function renderSystem() {
170
+ const down = Object.keys(modules).filter(key => !state.health[key]?.up);
171
+ const pending = state.ownership?.owner === 'engine' && !state.ownership.ready;
172
+ $('systemDot').className = `dot ${down.length || pending ? 'issue' : 'up'}`;
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('、') : '本机知识服务已连接';
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('');
176
+ if (!navigator.onLine) {
177
+ $('systemDot').className = 'dot issue'; $('systemTitle').textContent = '网页连接中断';
178
+ $('systemDetail').textContent = '恢复连接后自动读取状态,后台维护不受影响。';
179
+ $('moduleHealth').innerHTML = '<p>当前网页无法确认服务状态,连接恢复后重新读取。</p>';
180
+ }
181
+ }
182
+ async function openSystem() {
183
+ $('systemDrawer').hidden = false; $('drawerBackdrop').hidden = false;
184
+ $('brainName').value = state.brain.name; $('knowledgeRoot').value = state.root;
185
+ await loadMaintenance();
186
+ }
187
+ async function loadLogs() {
188
+ const epoch = ++logEpoch;
189
+ logStream?.close(); logStream = null;
190
+ const params = new URLSearchParams({ pageSize: '100', q: $('logsSearch').value.trim() });
191
+ if (state.projectId) params.set('projectId', state.projectId);
192
+ const result = await api(`/api/logs?${params}`);
193
+ if (epoch !== logEpoch || !$('logsDialog').open) return;
194
+ $('logsDialog').dataset.streamCursor = result.streamCursor || '';
195
+ $('logsContent').replaceChildren();
196
+ for (const record of [...(result.entries || [])].reverse()) appendLog(record);
197
+ $('logsStatus').textContent = `${(result.entries || []).length} 条诊断记录 · 当前项目`;
198
+ if (!$('logsSearch').value.trim()) {
199
+ const streamParams = new URLSearchParams({ streamCursor: result.streamCursor || '' });
200
+ if (state.projectId) streamParams.set('projectId', state.projectId);
201
+ logStream = new EventSource(`/api/logs/stream?${streamParams}`);
202
+ logStream.addEventListener('logs/appended', event => { if (epoch === logEpoch && $('logsDialog').open) appendLog(JSON.parse(event.data).record); });
203
+ }
204
+ }
205
+ function appendLog(record) {
206
+ const row = document.createElement('details'); row.className = 'log-entry';
207
+ row.dataset.level = record.level;
208
+ const summary = document.createElement('summary'); summary.className = 'row';
209
+ const date = document.createElement('time'); date.textContent = time(record.ts || record.at);
210
+ const text = document.createElement('pre'); text.textContent = record.message || '';
211
+ const detail = document.createElement('pre'); detail.className = 'log-detail';
212
+ detail.textContent = JSON.stringify(record, null, 2);
213
+ summary.append(date, text); row.append(summary, detail); $('logsContent').append(row);
214
+ while ($('logsContent').childElementCount > 100) $('logsContent').firstElementChild.remove();
215
+ }
216
+ function closeSystem() {
217
+ $('systemDrawer').hidden = true; $('drawerBackdrop').hidden = true;
218
+ if (resumeImport) {
219
+ resumeImport = false;
220
+ $('importDialog').showModal();
221
+ checkImport().catch(error => toast(error.message));
222
+ }
223
+ }
224
+ async function loadMaintenance() {
225
+ try {
226
+ const [profiles, saved] = await Promise.all([api('/api/terminal/profiles'), api('/api/terminal/maintenance-default')]);
227
+ const list = Array.isArray(profiles) ? profiles : profiles.profiles || [];
228
+ $('maintenanceModel').innerHTML = '<option value="">尚未选择后台默认模型</option>' + list.filter(p => p.enabled && p.credentialConfigured).map(p => `<option value="${esc(p.id)}">${esc(p.name)} · ${esc(p.models.default)}</option>`).join('');
229
+ $('maintenanceModel').value = saved.profileId || '';
230
+ $('configurationStatus').textContent = saved.profileId ? '此默认模型仅用于后台维护。' : '先配置供应商,再选择后台默认模型。';
231
+ const embedding = await api('/api/terminal/embedding-config');
232
+ $('embeddingStatus').textContent = embedding.configured ? `已配置 · ${embedding.config.model}` : '未配置,使用文本检索降级。';
233
+ const capture = (await api('/api/terminal/health')).conversationCapture;
234
+ $('captureStatus').textContent = !capture ? '工作台对话接线尚未启用。' : capture.error ? `工作台对话待恢复 · ${capture.pending} 条事件保留在本机,请检查接线。` : capture.pending ? `工作台对话正在同步 · ${capture.pending} 条待写入。` : capture.enabled ? '工作台对话自动保存到开发会话记录。' : '工作台对话捕获已关闭。';
235
+ } catch { $('maintenanceModel').innerHTML = '<option value="">工作台暂时不可用</option>'; $('configurationStatus').textContent = '无法读取模型配置,请恢复工作台服务。'; }
236
+ }
237
+ async function openEmbedding() {
238
+ const [profiles, embedding] = await Promise.all([api('/api/terminal/profiles'), api('/api/terminal/embedding-config')]);
239
+ const list = Array.isArray(profiles) ? profiles : profiles.profiles || [];
240
+ $('embeddingProfile').innerHTML = '<option value="">独立凭据</option>' + list.filter(p => p.enabled && p.credentialConfigured).map(p => `<option value="${esc(p.id)}">${esc(p.name)}</option>`).join('');
241
+ const saved = embedding.config || {};
242
+ $('embeddingProfile').value = saved.profileId || '';
243
+ $('embeddingProvider').value = saved.provider || 'minimax';
244
+ $('embeddingModel').value = saved.model || 'embo-01';
245
+ $('embeddingEndpoint').value = saved.endpoint || '';
246
+ $('embeddingSecret').value = '';
247
+ $('embeddingSecretLabel').hidden = !!saved.profileId;
248
+ $('embeddingFeedback').textContent = embedding.configured ? '配置已保存。留空凭据不会覆盖现有密钥。' : '向量检索尚未配置。';
249
+ closeSystem(); $('embeddingDialog').showModal();
250
+ }
251
+ async function saveEmbedding(enabled = true) {
252
+ const body = enabled ? { provider: $('embeddingProvider').value, model: $('embeddingModel').value, endpoint: $('embeddingEndpoint').value, profileId: $('embeddingProfile').value || null, secret: $('embeddingSecret').value } : { enabled: false };
253
+ await api('/api/terminal/embedding-config', { method: 'PUT', body });
254
+ $('embeddingSecret').value = '';
255
+ const result = await api('/api/modules/configure', { method: 'POST', body: {} });
256
+ $('embeddingFeedback').textContent = !enabled ? '已停用向量模型,知识文件保留。' : result.configuration.embedding ? '已保存并同步到向量检索服务。' : '配置已保存,向量服务待恢复。请检查模块连接。';
257
+ await loadMaintenance();
258
+ }
259
+ async function repair() {
260
+ const project = activeProject(); if (!project) return toast('请先导入项目。');
261
+ const result = await api(`/api/projects/${encodeURIComponent(project.projectId)}/module-register`, { method: 'POST', body: {} });
262
+ const pending = Object.entries(result.modules).filter(([, v]) => !v.ok).map(([key]) => modules[key]);
263
+ $('wiringFeedback').textContent = pending.length ? `项目已登记,待恢复:${pending.join('、')}` : '四个模块已连接。';
264
+ toast($('wiringFeedback').textContent); await refreshRegistry(); Object.keys(frames).forEach(frameContext);
265
+ }
266
+ async function pick(target) {
267
+ const result = await api('/api/terminal/system/pick-folder');
268
+ if (result.cancelled || !result.path) return;
269
+ $(target).value = result.path; $(target).dispatchEvent(new Event('input', { bubbles: true }));
270
+ }
271
+ function openImport() {
272
+ resumeImport = false; $('setupImportRoot').hidden = true;
273
+ $('importForm').reset(); state.importId = `project-${crypto.randomUUID()}`; state.preflight = null; state.preflightEpoch++;
274
+ $('importDomain').innerHTML = '<option value="">未分组</option>' + state.brain.domains.map(d => `<option value="${esc(d.id)}">${esc(d.name)}</option>`).join('');
275
+ $('importDomain').value = activeProject()?.domainId || '';
276
+ $('gitInitChoice').hidden = true; $('replaceHookChoice').hidden = true; $('confirmImport').disabled = true; $('preflightStatus').textContent = '填写目录后检查导入条件。'; $('importDialog').showModal();
277
+ }
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 }; }
279
+ const problemLabels = { KNOWLEDGE_ROOT_MISSING: '请在系统状态中设置知识根目录。', AI_PROFILE_REQUIRED: '请配置可用模型;后台默认模型在系统状态中设置。', DUPLICATE_PROJECT: '此项目已导入,请打开已有项目。', HOOK_CONFLICT: '存在第三方 Hook,不会覆盖。请先检查冲突。', PATH_MISSING: '代码目录不存在。', PATH_REQUIRED: '请选择代码目录。' };
280
+ async function checkImport() {
281
+ const epoch = ++state.preflightEpoch; $('confirmImport').disabled = true;
282
+ $('setupImportRoot').hidden = true;
283
+ if (!$('importCode').value.trim()) return;
284
+ $('preflightStatus').textContent = '正在检查目录、仓库与接线条件…';
285
+ try {
286
+ const result = await api('/api/projects/preflight-import', { method: 'POST', body: importInput(), allowNotReady: true });
287
+ if (epoch !== state.preflightEpoch || !$('importDialog').open) return;
288
+ state.preflight = result; $('gitInitChoice').hidden = !result.plannedGitInit; $('replaceHookChoice').hidden = !result.checks.hook?.replacementProjectId;
289
+ const messages = result.problems.map(p => problemLabels[p.code] || p.message);
290
+ $('setupImportRoot').hidden = !result.problems.some(p => p.code === 'KNOWLEDGE_ROOT_MISSING');
291
+ $('preflightStatus').textContent = result.ready ? result.plannedGitInit ? '目录检查通过。请确认初始化 Git;不分析历史。' : '检查通过。只建立当前提交的跟踪基线,不分析历史。' : [...new Set(messages)].join('\n');
292
+ $('preflightStatus').classList.toggle('issue', !result.ready);
293
+ $('confirmImport').disabled = !result.ready || result.plannedGitInit && !$('importForm').elements.confirmGitInit.checked || result.checks.hook?.replacementProjectId && !$('importForm').elements.confirmReplaceHook.checked || !$('importKnowledge').value.trim();
294
+ } catch (error) { if (epoch === state.preflightEpoch) $('preflightStatus').textContent = error.message; }
295
+ }
296
+ document.addEventListener('click', e => {
297
+ const button = e.target.closest('button'); if (!button) return;
298
+ const execute = async () => {
299
+ if (button.dataset.view) setView(button.dataset.view);
300
+ if (button.dataset.go) { if (button.dataset.go === 'memory') state.memoryTab = 'candidate'; setView(button.dataset.go); }
301
+ if (button.dataset.close && !(button.dataset.close === 'unbindDialog' && unbinding)) $(button.dataset.close).close();
302
+ if (button.dataset.selectProject) selectProject(button.dataset.selectProject);
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 }); }
304
+ if (button.dataset.memoryTab) { state.memoryTab = button.dataset.memoryTab; renderMemories(); }
305
+ if (button.dataset.activityTab) { state.activityTab = button.dataset.activityTab; setView('activity'); post('eventBridge', { type: 'kb:visibility', visible: state.activityTab === 'sessions' }); post('eventBridge', { type: 'kb:refresh' }); }
306
+ if (button.dataset.action === 'import') openImport();
307
+ if (button.dataset.action === 'repair') await repair();
308
+ if (button.dataset.action === 'system') await openSystem();
309
+ if (button.dataset.action === 'unbind') { const p = activeProject(); if (p && !unbinding) { unbindProjectId = p.projectId; $('unbindProjectName').textContent = `解绑「${p.name}」?`; $('unbindFeedback').textContent = ''; $('unbindDialog').showModal(); } }
310
+ if (button.dataset.retryRun) { await api(`/api/engine/runs/${encodeURIComponent(button.dataset.retryRun)}/retry`, { method: 'POST', body: {} }); await refreshSnapshot(); }
311
+ if (button.dataset.contextIndex !== undefined) await openReference(state.contexts.get(state.projectId).references[Number(button.dataset.contextIndex)]);
312
+ if (button.dataset.memoryAction) {
313
+ const memory = state.snapshot.memories.find(m => m.id === button.dataset.memoryId); if (!memory) return;
314
+ const action = button.dataset.memoryAction;
315
+ if (action === 'evidence') await openReference({ ...memory.evidence, text: memory.evidence.quote, title: '记忆来源依据' });
316
+ else if (action === 'edit') { editingMemoryId = memory.id; $('memoryText').value = memory.text; $('memoryEvidence').textContent = `${projectName(memory.evidence.projectId)} · ${memory.evidence.commitSha.slice(0, 8)} / ${memory.evidence.entryId}`; $('memoryDialog').showModal(); }
317
+ else { await api(`/api/engine/memories/${encodeURIComponent(memory.id)}`, { method: 'PATCH', body: { status: action === 'confirm' ? 'confirmed' : 'ignored' } }); await refreshSnapshot(); }
318
+ }
319
+ };
320
+ execute().catch(error => toast(error.message));
321
+ });
322
+ document.addEventListener('change', e => { if (e.target.id === 'projectDomain') api(`/api/projects/${encodeURIComponent(state.projectId)}`, { method: 'PATCH', body: { domainId: e.target.value || null } }).then(refreshRegistry).catch(error => toast(error.message)); });
323
+ window.addEventListener('message', event => {
324
+ if (event.origin !== location.origin || !event.data || typeof event.data !== 'object') return;
325
+ const key = Object.keys(frames).find(k => $(frames[k].id).contentWindow === event.source); if (!key) return;
326
+ const message = event.data;
327
+ if (message.type === 'kb:ready') { frames[key].ready = true; frameContext(key); if (frames[key].openSettings) { post(key, { type: 'kb:open-settings' }); frames[key].openSettings = false; } }
328
+ if (message.requestId && message.requestId !== frames[key].requestId) return;
329
+ if (message.type === 'kb:knowledge' && key === 'terminal') {
330
+ const project = state.projects.find(p => p.modules.terminal.projectId === message.projectId);
331
+ if (project) { state.contexts.set(project.projectId, message.knowledge); if (project.projectId === state.projectId) renderContext(); }
332
+ }
333
+ });
334
+ on('projectSelect', 'change', e => selectProject(e.target.value));
335
+ on('themeToggle', 'click', () => { document.documentElement.dataset.theme = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'; localStorage.setItem('kb.theme', document.documentElement.dataset.theme); Object.keys(frames).forEach(key => post(key, { type: 'kb:theme', theme: document.documentElement.dataset.theme })); });
336
+ on('openSystem', 'click', openSystem); on('closeSystem', 'click', closeSystem); on('drawerBackdrop', 'click', closeSystem);
337
+ on('repairWiring', 'click', repair);
338
+ on('configureProviders', 'click', () => { closeSystem(); setView('work'); initializeFrame('terminal'); if (frames.terminal.ready) post('terminal', { type: 'kb:open-settings' }); else frames.terminal.openSettings = true; });
339
+ on('saveMaintenance', 'click', async () => { await api('/api/terminal/maintenance-default', { method: 'PUT', body: { profileId: $('maintenanceModel').value || null } }); const result = await api('/api/modules/configure', { method: 'POST', body: {} }); $('configurationStatus').textContent = result.configuration.model ? '后台默认模型已保存并同步。' : '配置已保存,后台运行配置尚未同步,请检查模块连接。'; });
340
+ on('openEmbedding', 'click', openEmbedding);
341
+ on('embeddingProfile', 'change', () => { $('embeddingSecretLabel').hidden = !!$('embeddingProfile').value; });
342
+ on('embeddingProvider', 'change', () => { $('embeddingModel').value = $('embeddingProvider').value === 'minimax' ? 'embo-01' : 'text-embedding-3-small'; });
343
+ on('embeddingForm', 'submit', async event => { event.preventDefault(); await saveEmbedding(); });
344
+ on('disableEmbedding', 'click', () => saveEmbedding(false));
345
+ on('embeddingDialog', 'close', () => { $('embeddingSecret').value = ''; });
346
+ on('saveBrainSettings', 'click', async () => { await api('/api/brain', { method: 'PATCH', body: { name: $('brainName').value } }); await api('/api/settings', { method: 'PATCH', body: { knowledge: { rootPath: $('knowledgeRoot').value } } }); toast('设置已保存,已有知识未移动。'); await refreshRegistry(); if (resumeImport) closeSystem(); });
347
+ on('setupImportRoot', 'click', async () => { $('importDialog').close(); resumeImport = true; await openSystem(); $('knowledgeRoot').focus(); });
348
+ on('pickRoot', 'click', () => pick('knowledgeRoot')); on('pickCode', 'click', () => pick('importCode')); on('pickKnowledge', 'click', () => pick('importKnowledge'));
349
+ on('openEngine', 'click', () => { closeSystem(); initializeFrame('engine'); $('engineDialog').showModal(); post('engine', { type: 'kb:visibility', visible: true }); });
350
+ on('engineDialog', 'close', () => post('engine', { type: 'kb:visibility', visible: false }));
351
+ on('openLogs', 'click', async () => { closeSystem(); $('logsDialog').showModal(); await loadLogs(); });
352
+ on('logsDialog', 'close', () => { logEpoch++; logStream?.close(); logStream = null; });
353
+ on('copyReference', 'click', async () => {
354
+ const text = $('referenceMeta').textContent + '\n\n' + $('referenceContent').textContent;
355
+ try { await navigator.clipboard.writeText(text); }
356
+ catch { const field = document.createElement('textarea'); field.value = text; document.body.append(field); field.select(); const copied = document.execCommand('copy'); field.remove(); if (!copied) throw new Error('复制失败,请选择引用内容手动复制。'); }
357
+ toast('已复制知识与来源。');
358
+ });
359
+ on('loadLogs', 'click', loadLogs);
360
+ on('exportLogs', 'click', async () => { const params = new URLSearchParams({ q: $('logsSearch').value.trim() }); if (state.projectId) params.set('projectId', state.projectId); const response = await fetch(`/api/logs/export?${params}`); if (!response.ok) throw new Error('诊断导出失败。'); const blob = await response.blob(); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = 'knowledge-diagnostics.jsonl'; link.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); });
361
+ on('createDomain', 'click', () => { $('domainForm').reset(); $('domainDialog').showModal(); });
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(); });
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(); });
384
+ on('importForm', 'input', e => {
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)}`; }
386
+ clearTimeout(preflightTimer); $('confirmImport').disabled = true; preflightTimer = setTimeout(checkImport, 250);
387
+ });
388
+ on('importForm', 'submit', async e => {
389
+ e.preventDefault(); if (!state.preflight?.ready || state.preflight.plannedGitInit && !$('importForm').elements.confirmGitInit.checked || state.preflight.checks.hook?.replacementProjectId && !$('importForm').elements.confirmReplaceHook.checked) return;
390
+ $('confirmImport').disabled = true; $('confirmImport').textContent = '正在登记…';
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) ? '项目已导入,四个模块已连接。' : '项目已导入,部分接线待恢复。'); }
392
+ finally { $('confirmImport').textContent = '导入项目'; if ($('importDialog').open) checkImport(); }
393
+ });
394
+ window.addEventListener('hashchange', () => setView(location.hash.slice(1)));
395
+ window.addEventListener('offline', () => {
396
+ snapshotEpoch++; clearTimeout(refreshTimer);
397
+ state.stream?.close(); state.stream = null; logStream?.close(); logStream = null; logEpoch++;
398
+ state.engineAvailable = false; $('activityConnection').textContent = '连接中断,恢复后自动读取快照';
399
+ renderSystem(); render();
400
+ });
401
+ window.addEventListener('online', () => {
402
+ $('activityConnection').textContent = '正在恢复快照'; refreshRegistry();
403
+ if ($('logsDialog').open) loadLogs().catch(error => toast(error.message));
404
+ });
405
+ window.addEventListener('beforeunload', () => { state.stream?.close(); logStream?.close(); });
406
+ document.addEventListener('keydown', e => { if (e.key === 'Escape') closeSystem(); });
407
+ document.documentElement.dataset.theme = localStorage.getItem('kb.theme') === 'dark' ? 'dark' : 'light';
408
+ all('[data-scope]').forEach(button => button.classList.toggle('active', button.dataset.scope === state.scope));
409
+ state.view = titles[location.hash.slice(1)] ? location.hash.slice(1) : 'home';
410
+ setView(state.view); refreshRegistry();
411
+ setInterval(() => { if (!document.hidden) refreshRegistry(); }, 30000);
412
+ })();
@@ -0,0 +1,45 @@
1
+ {
2
+ "schema": "project-knowledge/vendor-runtime/v1",
3
+ "modules": {
4
+ "knowledge-engine": {
5
+ "name": "@sanqianx/ai-coding-knowledge-engine",
6
+ "version": "0.2.0",
7
+ "root": "_modules/knowledge-engine",
8
+ "files": {
9
+ "README.md": "c12b9bc9aa49dce6eaad0e6f3fd611466ab1ef6b17d2bdfb69a6dab6a42ab5ed",
10
+ "package.json": "6740fa3dec72ebae49fa903a4b5f8ddc2adf52d11ba9f7c4bc1f36a603636578",
11
+ "src/analysis-contract.js": "90354d1e9b4eff74fdd9d47e097cdb5d47944785c2be66811f64e5958c94d8e7",
12
+ "src/analysis-worker.js": "a105cabe6e1573b2d986b1e7a5aaf615e2fb53524156fc8e4e2c0a71f2c97e55",
13
+ "src/bin.js": "22208907431d0f3a971a669c7e5eb44b3b7af9bbb36fd7ba17c3b90c4288bd70",
14
+ "src/bridge-client.js": "ae0dba21e4286694bcd9e064ca36564ec997969ae1eb1ce4f8a6665e447cbffe",
15
+ "src/coordinator.js": "e85f1f604d2090bd466244d73cf8e1862e0cbe436e62d4a2f7193afd2fac7843",
16
+ "src/headless-analyzer.js": "2473b28d4bf06088abbcafa21d491560a9ca0ab100024c912a36bc6f981d8576",
17
+ "src/memories.js": "0a8667e6daba99fa5022393951a28e96cdadf98af85b6af9a6be96c7f4e896fc",
18
+ "src/retrieval.js": "acbf76bb93fd13dac391c2e918891a56ace869db5583d2a5994641b6782496d6",
19
+ "src/runtime/README.md": "9be676eca76a7857b34d7357194e6f0ee733c22e61d962d53984715dbec7f673",
20
+ "src/runtime/atomic-file.js": "9846b4b69db5ded811acf19a1181ef93615d93efaa542b858e552a7f3124ad23",
21
+ "src/runtime/automation-config.js": "6498c5397be12b155829740595a1553d4dcd6eaa7db6ced3ef88fdc60218f00e",
22
+ "src/runtime/commit-conversation-binder.js": "4f5a7a13c3dfac3765ad70e54de351f5d81e0ed4b2b2d7ecb90ef77cb29e0c15",
23
+ "src/runtime/commit-processing-ledger.js": "455aa1ac4e212c0b63d9df99e49aa94703cd24145e41aa9794f16274b2ec2e18",
24
+ "src/runtime/commit-prompt.js": "11856175a405c00514add6a540324a0efe3d7e5dd872c9e8a0b77f9f40abc551",
25
+ "src/runtime/commit-reconciler.js": "27a894ac7588701fc56e19811cbf266664357e8ac9beb9ffc4eccfc1f8e75b46",
26
+ "src/runtime/content-hash.js": "201ee7dcc054310f14097a00058cf9b28fa913367eef616e0fdd8facbf3c6c41",
27
+ "src/runtime/contracts.js": "567fbace85ad0534fb0119f5c0e57d73acc0b76cc799af93d778c5ecbade53df",
28
+ "src/runtime/conversation-exclusions.js": "ef8545034b862d384fb460140490bb531b58d94992f5e2222ebaaa079b44fa63",
29
+ "src/runtime/conversation-store.js": "8ea0135588e40dfe0bab0288220a0fb481a470855558ae38b8274d57ce55fc39",
30
+ "src/runtime/evidence-bundle.js": "33d4614849d5e1d69aba8b75c3a43c18b7b6f94e5c78654c82ef631331cc94cc",
31
+ "src/runtime/git-runner.js": "b74fe197bd4dcf3e70635ef7b1f2615edf965c1d9a3ffe5406aabd41b76d74a9",
32
+ "src/runtime/knowledge-promotion.js": "c24aba505caf11cdc8faba763beffc65956c1330ef28ed5fbd6ae16eac640fa6",
33
+ "src/runtime/knowledge-retrieval-service.js": "75444f495a9ad0720af0e57b8d27d7ac1325c78a768f4f68ebd86ebc2524945e",
34
+ "src/runtime/layout.js": "c48fa1db62e5df1edefe13218e470c5ceea4ef2781293512c2c18e5c2ba0c7e9",
35
+ "src/runtime/markdown-knowledge-indexer.js": "44a76a9adefad07e78f77b1106b10d6c1cc7873d76362df4c546892b21f3939c",
36
+ "src/runtime/scanner.js": "d99fa8d74a6fbbaae75ca49f62127e14cf9e48b1404e4b4d597ef39df5441410",
37
+ "src/server.js": "e7f0b07b7a8cf2623690b9d2d23492f2c95737dc974adfeea049610a4b5ded55",
38
+ "src/store.js": "71f0417c2c1e42b89429e3dee1adff7b2c09e86682cb3f4843ce6d9e5aafd7ec",
39
+ "ui/app.css": "7ad370c269e60a7b5ec907d81a607b2d16bd07b2d7c22a81f102dab51eeb2889",
40
+ "ui/app.js": "91ee6d8c31712e6af46de2eecbd72bbc5236c06b7cd055800831e7d57f84a0be",
41
+ "ui/index.html": "ce3565cfefd4838b11d37a91a906ef26ca5a1834844fe3f7c897b56851a3c418"
42
+ }
43
+ }
44
+ }
45
+ }
@@ -1,77 +0,0 @@
1
- const assert = require('assert');
2
- const fs = require('fs');
3
- const os = require('os');
4
- const path = require('path');
5
- const dataDir = require('../lib/data-dir');
6
- const { LEGACY_ASSETS } = require('../lib/legacy-data-manifest');
7
-
8
- function temp(name) { return fs.mkdtempSync(path.join(os.tmpdir(), `pk-data-dir-${name}-`)); }
9
- function clean(dir) { fs.rmSync(dir, { recursive: true, force: true }); }
10
- function write(file, value = '{}') { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, value); }
11
-
12
- (() => {
13
- // Resolution must be diagnostic-safe: resolving never creates the candidate.
14
- const missing = path.join(temp('resolve'), 'not-created');
15
- try {
16
- assert.equal(dataDir.resolveDataDirPath({ dataDir: missing }), path.resolve(missing));
17
- assert.equal(fs.existsSync(missing), false);
18
- dataDir.ensureDataDir(missing);
19
- assert.equal(fs.existsSync(missing), true);
20
- } finally { clean(path.dirname(missing)); }
21
-
22
- const legacy = temp('legacy');
23
- const target = temp('target');
24
- const saved = process.env.KB_DATA_DIR;
25
- try {
26
- write(path.join(legacy, 'projects.json'), JSON.stringify({ old: { repoPath: 'C:/legacy' } }));
27
- write(path.join(legacy, 'ai-profiles.json'), JSON.stringify({ profiles: [{ id: 'primary', apiKey: 'test-secret' }] }));
28
- write(path.join(legacy, 'knowledge-store.json'), JSON.stringify({ rootPath: 'C:/knowledge' }));
29
- write(path.join(legacy, 'embedding-config.json'), JSON.stringify({ modelId: 'model', localModelPath: 'C:/models', localFilesOnly: true }));
30
- write(path.join(legacy, 'knowledge-scopes.json'), JSON.stringify({ scopes: ['p'] }));
31
- write(path.join(legacy, 'models', 'nested', 'model.onnx'), 'model-bytes');
32
- // An empty target registry is non-authoritative and must not suppress legacy data.
33
- write(path.join(target, 'projects.json'), '{}');
34
- process.env.KB_DATA_DIR = target;
35
- dataDir._resetCache();
36
- const result = dataDir.migrateFromLegacy({ legacyRoot: legacy });
37
- assert.equal(result.ok, true);
38
- assert.equal(result.migrated, true);
39
- assert.equal(JSON.parse(fs.readFileSync(path.join(target, 'projects.json'))).old.repoPath, 'C:/legacy');
40
- assert.equal(JSON.parse(fs.readFileSync(path.join(target, 'ai-profiles.json'))).profiles.length, 1);
41
- assert.equal(JSON.parse(fs.readFileSync(path.join(target, 'embedding-config.json'))).modelId, 'model');
42
- assert.equal(fs.readFileSync(path.join(target, 'models', 'nested', 'model.onnx'), 'utf8'), 'model-bytes');
43
- assert(LEGACY_ASSETS.some(asset => asset.source === 'embedding-config.json'));
44
- assert(LEGACY_ASSETS.some(asset => asset.source === 'models'));
45
- } finally { process.env.KB_DATA_DIR = saved; dataDir._resetCache(); clean(legacy); clean(target); }
46
-
47
- const source = temp('conflict-source');
48
- const destination = temp('conflict-destination');
49
- try {
50
- write(path.join(source, 'projects.json'), JSON.stringify({ old: { repoPath: 'C:/old' } }));
51
- write(path.join(destination, 'projects.json'), JSON.stringify({ newer: { repoPath: 'C:/new' } }));
52
- process.env.KB_DATA_DIR = destination;
53
- dataDir._resetCache();
54
- const result = dataDir.migrateFromLegacy({ legacyRoot: source });
55
- assert.equal(result.ok, false);
56
- assert.equal(result.requiresManualRecovery, true);
57
- assert.equal(JSON.parse(fs.readFileSync(path.join(destination, 'projects.json'))).newer.repoPath, 'C:/new');
58
- } finally { process.env.KB_DATA_DIR = saved; dataDir._resetCache(); clean(source); clean(destination); }
59
-
60
- // The installed package can contain empty scaffolding directories (for
61
- // example projects/) while the user data directory contains real projects.
62
- // Empty package scaffolding is not a legacy source and must not block startup.
63
- const packageRoot = temp('empty-package-root');
64
- const userData = temp('existing-user-data');
65
- try {
66
- fs.mkdirSync(path.join(packageRoot, 'projects'), { recursive: true });
67
- write(path.join(userData, 'projects', 'project-real', 'config.json'), JSON.stringify({ id: 'project-real' }));
68
- process.env.KB_DATA_DIR = userData;
69
- dataDir._resetCache();
70
- const result = dataDir.migrateFromLegacy({ legacyRoot: packageRoot });
71
- assert.equal(result.ok, true, 'empty package scaffolding must not be a migration conflict');
72
- assert.equal(result.migrated, false);
73
- assert.equal(result.reason, 'no legacy assets found');
74
- assert.equal(fs.existsSync(path.join(userData, 'projects', 'project-real', 'config.json')), true);
75
- } finally { process.env.KB_DATA_DIR = saved; dataDir._resetCache(); clean(packageRoot); clean(userData); }
76
- console.log('data-dir-migration-test PASS');
77
- })();