@sanqianx/project-knowledge 4.6.1 → 4.8.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 (198) hide show
  1. package/.agents/plugins/marketplace.json +17 -17
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/CHANGELOG.md +817 -780
  4. package/README.md +48 -36
  5. package/_modules/knowledge-engine/README.md +88 -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 +255 -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/migration.js +88 -0
  15. package/_modules/knowledge-engine/src/retrieval.js +116 -0
  16. package/_modules/knowledge-engine/src/runtime/README.md +7 -0
  17. package/_modules/knowledge-engine/src/runtime/atomic-file.js +204 -0
  18. package/_modules/knowledge-engine/src/runtime/automation-config.js +140 -0
  19. package/_modules/knowledge-engine/src/runtime/commit-conversation-binder.js +249 -0
  20. package/_modules/knowledge-engine/src/runtime/commit-processing-ledger.js +26 -0
  21. package/_modules/knowledge-engine/src/runtime/commit-prompt.js +202 -0
  22. package/_modules/knowledge-engine/src/runtime/commit-reconciler.js +432 -0
  23. package/_modules/knowledge-engine/src/runtime/content-hash.js +4 -0
  24. package/_modules/knowledge-engine/src/runtime/contracts.js +279 -0
  25. package/_modules/knowledge-engine/src/runtime/conversation-exclusions.js +204 -0
  26. package/_modules/knowledge-engine/src/runtime/conversation-store.js +387 -0
  27. package/_modules/knowledge-engine/src/runtime/evidence-bundle.js +228 -0
  28. package/_modules/knowledge-engine/src/runtime/git-runner.js +62 -0
  29. package/_modules/knowledge-engine/src/runtime/knowledge-promotion.js +444 -0
  30. package/_modules/knowledge-engine/src/runtime/knowledge-retrieval-service.js +402 -0
  31. package/_modules/knowledge-engine/src/runtime/layout.js +44 -0
  32. package/_modules/knowledge-engine/src/runtime/markdown-knowledge-indexer.js +230 -0
  33. package/_modules/knowledge-engine/src/runtime/scanner.js +298 -0
  34. package/_modules/knowledge-engine/src/server.js +155 -0
  35. package/_modules/knowledge-engine/src/store.js +239 -0
  36. package/_modules/knowledge-engine/ui/app.css +172 -0
  37. package/_modules/knowledge-engine/ui/app.js +297 -0
  38. package/_modules/knowledge-engine/ui/index.html +95 -0
  39. package/_site/README.md +30 -30
  40. package/_site/_test/ai-profile-resolver-test.js +137 -137
  41. package/_site/_test/automation-queue-test.js +14 -14
  42. package/_site/_test/automation-ui-test.js +40 -40
  43. package/_site/_test/background-task-registry-test.js +43 -43
  44. package/_site/_test/baseline-schema-test.js +91 -91
  45. package/_site/_test/bridge-adapter-test.js +1 -1
  46. package/_site/_test/bridge-consumer-service-test.js +1 -1
  47. package/_site/_test/chat-claudecodeui-match-test.js +92 -92
  48. package/_site/_test/claude-executable-discovery-test.js +62 -62
  49. package/_site/_test/claude-workbench-test.js +170 -170
  50. package/_site/_test/client-capture-restart-e2e-test.js +4 -4
  51. package/_site/_test/codex-conversation-projection-test.js +2 -1
  52. package/_site/_test/commit-boundary-freeze-test.js +1 -1
  53. package/_site/_test/commit-conversation-binding-test.js +95 -95
  54. package/_site/_test/commit-evidence-test.js +110 -110
  55. package/_site/_test/conversation-api-test.js +109 -109
  56. package/_site/_test/conversation-store-test.js +136 -136
  57. package/_site/_test/cross-repo-isolation-e2e-test.js +2 -2
  58. package/_site/_test/data-dir-migration-test.js +57 -57
  59. package/_site/_test/desktop-browser-compat-test.js +28 -28
  60. package/_site/_test/desktop-hook-runtime-regression-test.js +204 -204
  61. package/_site/_test/explicit-commit-processor-test.js +37 -37
  62. package/_site/_test/fixtures/make-git-repos.js +12 -1
  63. package/_site/_test/folder-picker-output-test.js +19 -19
  64. package/_site/_test/full-integration-e2e-test.js +165 -165
  65. package/_site/_test/git-validation-test.js +2 -2
  66. package/_site/_test/hook-runtime-endpoint-test.js +88 -88
  67. package/_site/_test/hook-status-repair-api-test.js +152 -152
  68. package/_site/_test/hook-trigger-test.js +85 -85
  69. package/_site/_test/import-preflight-api-test.js +234 -233
  70. package/_site/_test/index-writer-concurrency-test.js +135 -135
  71. package/_site/_test/integration-adapters-test.js +171 -171
  72. package/_site/_test/integration-capture-setup-test.js +1 -1
  73. package/_site/_test/integration-surface-coverage-test.js +164 -164
  74. package/_site/_test/knowledge-language-control-test.js +170 -170
  75. package/_site/_test/knowledge-promotion-recovery-test.js +260 -260
  76. package/_site/_test/knowledge-query-test.js +54 -54
  77. package/_site/_test/knowledge-retrieval-service-test.js +77 -77
  78. package/_site/_test/knowledge-storage-startup-test.js +52 -52
  79. package/_site/_test/knowledge-store-logs-supervision-test.js +89 -89
  80. package/_site/_test/legacy-forward-compat-test.js +257 -257
  81. package/_site/_test/legacy-project-upgrade-e2e-test.js +363 -363
  82. package/_site/_test/logging-api-test.js +89 -89
  83. package/_site/_test/logging-sse-no-gap-test.js +88 -88
  84. package/_site/_test/logging-ui-test.js +106 -104
  85. package/_site/_test/markdown-delta-overlay-test.js +85 -85
  86. package/_site/_test/markdown-maintenance-api-test.js +75 -75
  87. package/_site/_test/mcp-server-test.js +149 -149
  88. package/_site/_test/module-artifact-integrity-test.js +27 -0
  89. package/_site/_test/module-boundary-test.js +30 -0
  90. package/_site/_test/module-bridge-eventbridge-test.js +7 -0
  91. package/_site/_test/module-model-configuration-test.js +43 -0
  92. package/_site/_test/module-process-lifecycle-test.js +24 -0
  93. package/_site/_test/module-stream-proxy-test.js +34 -0
  94. package/_site/_test/non-release-ci-test.js +34 -34
  95. package/_site/_test/offline-boundary-isolation-test.js +32 -32
  96. package/_site/_test/p0-data-migration-characterization-test.js +37 -37
  97. package/_site/_test/p0-e2e-gate-test.js +347 -347
  98. package/_site/_test/packaged-ui-smoke-test.js +19 -10
  99. package/_site/_test/path-consistency-test.js +151 -151
  100. package/_site/_test/pending-sweep-test.js +9 -9
  101. package/_site/_test/post-commit-automation-test.js +87 -87
  102. package/_site/_test/product-diagnostics-ui-test.js +75 -0
  103. package/_site/_test/product-import-ui-test.js +92 -0
  104. package/_site/_test/project-delete-recovery-test.js +64 -64
  105. package/_site/_test/project-goal-editor-test.js +144 -144
  106. package/_site/_test/project-layout-v2-migration-test.js +127 -127
  107. package/_site/_test/project-lifecycle-transaction-test.js +106 -106
  108. package/_site/_test/prompt-settings-test.js +115 -115
  109. package/_site/_test/protected-architecture-gate-test.js +122 -122
  110. package/_site/_test/refactor-characterization-test.js +36 -36
  111. package/_site/_test/release-version-sync-test.js +1 -1
  112. package/_site/_test/requirement-recorder-test.js +173 -173
  113. package/_site/_test/run-all-tests.js +159 -156
  114. package/_site/_test/server-runtime-migration-safety-test.js +27 -27
  115. package/_site/_test/shared-contracts-test.js +58 -58
  116. package/_site/_test/startup-analysis-disabled-test.js +10 -10
  117. package/_site/_test/storage-foundation-test.js +78 -78
  118. package/_site/_test/structured-logger-test.js +101 -101
  119. package/_site/_test/tracking-start-test.js +108 -108
  120. package/_site/_test/v4122-upgrade-data-contract-test.js +56 -56
  121. package/_site/_test/workbench-permission-test.js +117 -117
  122. package/_site/_test/workspace-ui-contract-test.js +28 -43
  123. package/_site/lib/ai-profile-resolver.js +78 -78
  124. package/_site/lib/ai-workspace.js +101 -101
  125. package/_site/lib/automation-config.js +140 -140
  126. package/_site/lib/bridge-adapter.js +5 -1
  127. package/_site/lib/bridge-consumer-service.js +402 -402
  128. package/_site/lib/claude-cli-runner.js +1510 -1510
  129. package/_site/lib/commit-conversation-binder.js +234 -234
  130. package/_site/lib/commit-processing-ledger.js +27 -27
  131. package/_site/lib/commit-prompt.js +202 -202
  132. package/_site/lib/commit-reconciler.js +406 -406
  133. package/_site/lib/contracts.js +284 -279
  134. package/_site/lib/conversation-query-service.js +144 -144
  135. package/_site/lib/conversation-store.js +385 -385
  136. package/_site/lib/data-dir.js +74 -74
  137. package/_site/lib/data-state-classifier.js +40 -40
  138. package/_site/lib/engine-cutover.js +97 -0
  139. package/_site/lib/evidence-bundle.js +228 -228
  140. package/_site/lib/folder-picker-output.js +21 -21
  141. package/_site/lib/git-runner.js +62 -62
  142. package/_site/lib/github-team-store.js +1172 -1172
  143. package/_site/lib/hook-manager.js +203 -202
  144. package/_site/lib/index-service.js +173 -173
  145. package/_site/lib/integration-installer.js +907 -907
  146. package/_site/lib/kb-framework.js +176 -176
  147. package/_site/lib/kb-validator.js +124 -124
  148. package/_site/lib/knowledge-promotion.js +435 -435
  149. package/_site/lib/knowledge-retrieval-service.js +401 -401
  150. package/_site/lib/knowledge-tool-runtime.js +359 -359
  151. package/_site/lib/legacy-data-manifest.js +26 -26
  152. package/_site/lib/llm-client.js +161 -161
  153. package/_site/lib/markdown-knowledge-indexer.js +230 -230
  154. package/_site/lib/migration-service.js +442 -442
  155. package/_site/lib/module-bridge.js +198 -33
  156. package/_site/lib/post-commit-automation.js +87 -87
  157. package/_site/lib/project-lifecycle-service.js +519 -497
  158. package/_site/lib/project-store.js +268 -262
  159. package/_site/lib/requirement-recorder.js +278 -278
  160. package/_site/lib/runtime-endpoint.js +155 -155
  161. package/_site/lib/scanner.js +298 -298
  162. package/_site/lib/server-app.js +1590 -1520
  163. package/_site/lib/settings-store.js +133 -113
  164. package/_site/lib/storage-layout.js +162 -162
  165. package/_site/lib/structured-logger.js +574 -574
  166. package/_site/scripts/folder-picker.ps1 +156 -156
  167. package/_site/scripts/hook-trigger.js +157 -156
  168. package/_site/scripts/install-module-candidates.js +69 -0
  169. package/_site/scripts/pack-module-candidates.js +72 -0
  170. package/_site/scripts/sync-release-version.js +2 -2
  171. package/_site/scripts/vendor-modules.js +47 -0
  172. package/_site/scripts/verify-module-runtime.js +45 -0
  173. package/_site/scripts/verify-product-runtime.js +34 -0
  174. package/bin/project-knowledge-mcp.js +194 -194
  175. package/docs/README.zh-CN.md +34 -25
  176. package/docs/project-registry-schema.md +22 -22
  177. package/docs/testing-strategy.md +33 -33
  178. package/module-runtime-manifest.json +220 -0
  179. package/package.json +13 -10
  180. package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
  181. package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
  182. package/plugins/project-knowledge/.mcp.json +1 -1
  183. package/plugins/project-knowledge/opencode/project-knowledge.md +3 -3
  184. package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -28
  185. package/templates/project-readme.md +34 -34
  186. package/ui/favicon.svg +38 -38
  187. package/ui/index.html +51 -149
  188. package/ui/product.css +9 -0
  189. package/ui/product.js +386 -0
  190. package/vendor-manifest.json +46 -0
  191. package/_site/_test/import-ui-flow-test.js +0 -171
  192. package/_site/_test/project-control-panel-task14-test.js +0 -63
  193. package/_site/_test/task15-20-ui-flow-test.js +0 -148
  194. package/_site/_test/ui-i18n-toggle-test.js +0 -114
  195. package/_site/_test/ui-smoke-test.js +0 -73
  196. package/ui/app.css +0 -58
  197. package/ui/app.js +0 -607
  198. package/ui/i18n.js +0 -146
package/ui/app.js DELETED
@@ -1,607 +0,0 @@
1
- (() => {
2
- 'use strict';
3
- // Control Center shell: 连接与投影 —— 功能本体在模块(terminal :5760 / vector-hub :8787 / 会话浏览 :8790)。
4
- const $ = id => document.getElementById(id);
5
- const i18nApi = window.I18N || {};
6
- const i18n = typeof i18nApi.t === 'function' ? i18nApi.t : key => key;
7
- const setLanguage = typeof i18nApi.setLanguage === 'function' ? i18nApi.setLanguage : () => {};
8
- const activeLanguage = typeof i18nApi.activeLanguage === 'function' ? i18nApi.activeLanguage : () => 'zh-CN';
9
- const state = {
10
- projects: [], activeProjectId: '', view: 'terminal', settings: 'logs', settingsOpen: false,
11
- logs: [], logStream: null, logCursor: '', newLogs: 0,
12
- messageCount: 0, pendingDeleteId: '',
13
- modules: { terminal: { url: 'http://127.0.0.1:5760', up: false }, vectorHub: { url: 'http://127.0.0.1:8787', up: false }, eventBridge: { url: 'http://127.0.0.1:8790', up: false } },
14
- sessionsByProject: new Map(), // projectId -> [{sessionId,title,agentId,updatedAt}]
15
- fold: { term: false, vh: false, eb: false }, // false = 该模块项目列表折叠(默认)
16
- linkRetries: { term: 0, vh: 0 }, // kb:select-project 竞态假阴性的有界重试计数
17
- };
18
- const today = () => { const d = new Date(), pad = n => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; };
19
- const api = async (url, options = {}) => {
20
- const response = await fetch(url, { ...options, headers: options.body ? { 'content-type': 'application/json', ...(options.headers || {}) } : options.headers });
21
- const text = await response.text();
22
- const body = text ? JSON.parse(text) : {};
23
- if (!response.ok) throw Object.assign(new Error(body.error?.message || `请求失败 (${response.status})`), { body, status: response.status });
24
- return body;
25
- };
26
- const activeProject = () => state.projects.find(project => project.projectId === state.activeProjectId) || null;
27
- const setText = (node, value) => { node.textContent = value == null ? '' : String(value); };
28
- const option = (value, label) => { const node = document.createElement('option'); node.value = value; node.textContent = label; return node; };
29
- const formatTime = iso => { const d = new Date(iso); return Number.isNaN(d.getTime()) ? '—' : d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); };
30
- async function copyText(value) {
31
- const text = String(value == null ? '' : value);
32
- if (navigator.clipboard?.writeText) { try { await navigator.clipboard.writeText(text); return; } catch {} }
33
- const input = document.createElement('textarea'); input.value = text; input.setAttribute('readonly', ''); input.style.position = 'fixed'; input.style.opacity = '0'; document.body.append(input); input.select(); document.execCommand('copy'); input.remove();
34
- }
35
-
36
- /* ===================== 嵌入协议(壳 ↔ 模块) ===================== */
37
- const FRAME_OF = { term: 'fr-5760', vh: 'fr-8787', eb: 'fr-8790' };
38
- // 模块 kb:ready/kb:projects 消息里的 app 标识 → 壳侧的模块键
39
- const APP_KEY_OF = { 'agent-terminal': 'term', 'vector-hub': 'vh', 'event-bridge': 'eb' };
40
- // 每个模块的联动状态 chip(会话浏览暂无联动协议,chip 缺省)
41
- const CHIP_OF = { term: 'sync-5760', vh: 'sync-8787', eb: null };
42
- function postTo(key, msg) {
43
- const frame = $(FRAME_OF[key]);
44
- if (frame && frame.contentWindow) frame.contentWindow.postMessage(msg, '*');
45
- }
46
- function syncFold(key) { postTo(key, { type: 'kb:sidebar', visible: state.fold[key] }); }
47
- function renderFoldIcons() {
48
- document.querySelectorAll('.nav-fold').forEach(node => {
49
- const on = state.fold[node.dataset.fold];
50
- node.classList.toggle('on', on);
51
- node.title = `模块项目列表:${on ? '已展开,点击折叠' : '已折叠,点击展开'}`;
52
- });
53
- }
54
- function linkProjectToModules(project) {
55
- if (!project) return;
56
- postTo('term', { type: 'kb:select-project', project: project.repoPath || project.projectId });
57
- postTo('vh', { type: 'kb:select-project', project: (project.modules.vectorHub && project.modules.vectorHub.projectName) || project.projectId });
58
- }
59
- window.addEventListener('message', ev => {
60
- const data = ev.data;
61
- if (!data || typeof data !== 'object') return;
62
- const moduleKey = APP_KEY_OF[data.app] || (data.app ? null : 'term');
63
- if (data.type === 'kb:ready') {
64
- if (!moduleKey) return;
65
- setTimeout(() => { syncFold(moduleKey); linkProjectToModules(activeProject()); }, 150);
66
- } else if (data.type === 'kb:projects') {
67
- const chip = moduleKey ? $(CHIP_OF[moduleKey]) : null;
68
- const project = activeProject();
69
- if (!chip || !project) return;
70
- if (data.ok) {
71
- state.linkRetries[moduleKey] = 0;
72
- chip.textContent = `联动: ${project.displayName}`;
73
- chip.style.color = 'var(--good)'; chip.title = '';
74
- } else {
75
- chip.textContent = `未登记: ${project.displayName}`;
76
- chip.style.color = 'var(--warnText)';
77
- chip.title = '该模块还没有这个项目——在导入项目时勾选登记即可建立联动';
78
- // 模块项目清单未就绪时 kb:ready 触发的首轮选中会扑空(假阴性):
79
- // 有界重试补发,真未登记则几次后停下,chip 保持如实提示
80
- if ((state.linkRetries[moduleKey] || 0) < 4) {
81
- state.linkRetries[moduleKey] = (state.linkRetries[moduleKey] || 0) + 1;
82
- const delay = [800, 2500, 6000, 10000][state.linkRetries[moduleKey] - 1] || 10000;
83
- setTimeout(() => { if (activeProject()) linkProjectToModules(activeProject()); }, delay);
84
- }
85
- }
86
- } else if (data.type === 'kb:sessions') {
87
- if (!data.ok) return;
88
- state.sessionsByProject.set(data.project, data.sessions || []);
89
- renderProjectSessions(data.project);
90
- } else if (data.type === 'kb:session' && data.ok) {
91
- showView('terminal');
92
- }
93
- });
94
-
95
- function renderBrandStatus() {
96
- setText($('brand-root'), state.projects.length ? `${state.projects.length} 个项目 · 终端${state.modules.terminal.up ? '✓' : '×'} 检索${state.modules.vectorHub.up ? '✓' : '×'} 会话${state.modules.eventBridge.up ? '✓' : '×'}` : '暂无项目');
97
- }
98
- function probeModules() {
99
- for (const [key, id] of [['term', 'st-5760'], ['vh', 'st-8787'], ['bridge', 'st-8790']]) {
100
- const status = $(id);
101
- if (!status) continue;
102
- const info = key === 'term' ? state.modules.terminal : key === 'vh' ? state.modules.vectorHub : state.modules.eventBridge;
103
- let attempts = 0;
104
- const attempt = () => {
105
- fetch(info.url + '/', { mode: 'no-cors', cache: 'no-store' })
106
- .then(() => {
107
- info.up = true;
108
- status.classList.remove('down'); status.classList.add('ok');
109
- status.querySelector('.st-text').textContent = `已连接 ${info.url.replace(/^https?:\/\//, '')}`;
110
- renderBrandStatus();
111
- })
112
- .catch(() => {
113
- info.up = false;
114
- // 模块服务随壳自启,冷启动(node 起进程 + 服务监听)可能慢于首轮
115
- // 探测:有界退避重试,避免一次性误报"未检测到"并停在那直到手动刷新
116
- attempts += 1;
117
- if (attempts <= 10) {
118
- status.querySelector('.st-text').textContent = `连接中 ${info.url.replace(/^https?:\/\//, '')}…`;
119
- setTimeout(attempt, [500, 1000, 2000, 3000][Math.min(attempts - 1, 3)]);
120
- return;
121
- }
122
- status.classList.add('down');
123
- status.querySelector('.st-text').textContent = `未检测到 ${info.url.replace(/^https?:\/\//, '')} — 界面将显示为空白`;
124
- renderBrandStatus();
125
- });
126
- };
127
- attempt();
128
- }
129
- }
130
- function mountEmbeds() {
131
- const termUrl = state.modules.terminal.url, vhUrl = state.modules.vectorHub.url, ebUrl = state.modules.eventBridge.url;
132
- $('fr-5760').src = `${termUrl}/agent-terminal/?embed=1`;
133
- $('fr-8787').src = `${vhUrl}/?embed=1`;
134
- $('fr-8790').src = `${ebUrl}/?embed=1`;
135
- $('open-5760').href = `${termUrl}/agent-terminal/`;
136
- $('open-8787').href = `${vhUrl}/`;
137
- $('open-8790').href = `${ebUrl}/`;
138
- }
139
-
140
- /* ===================== 项目栏(聚合投影) ===================== */
141
- function projectDotClass(project) {
142
- if (project.projectId === state.activeProjectId) return 'dot claude';
143
- if (!project.modules.terminal.registered && !project.modules.vectorHub.registered && !project.modules.eventBridge.registered) return 'dot idle';
144
- return 'dot good';
145
- }
146
- function renderProjects() {
147
- const list = $('project-list');
148
- list.replaceChildren();
149
- $('mobile-project').replaceChildren();
150
- $('logs-project').replaceChildren(option('', '全部项目'));
151
- for (const project of state.projects) {
152
- const wrap = document.createElement('div'); wrap.className = 'project-item'; wrap.dataset.projectId = project.projectId;
153
- const card = document.createElement('button');
154
- card.type = 'button';
155
- card.className = `project-card${project.projectId === state.activeProjectId ? ' active' : ''}`;
156
- card.dataset.projectId = project.projectId;
157
- const line = document.createElement('div'); line.className = 'project-line';
158
- const main = document.createElement('div');
159
- const name = document.createElement('div'); name.className = 'project-name'; name.textContent = project.displayName;
160
- const badges = document.createElement('div'); badges.className = 'module-badges';
161
- const termBadge = document.createElement('span'); termBadge.className = `mbadge${project.modules.terminal.registered ? ' on' : ''}`; termBadge.textContent = '端'; termBadge.title = project.modules.terminal.registered ? '已登记到 Agent 终端' : '未登记到 Agent 终端';
162
- const vhBadge = document.createElement('span'); vhBadge.className = `mbadge${project.modules.vectorHub.registered ? ' on' : ''}`; vhBadge.textContent = '检'; vhBadge.title = project.modules.vectorHub.registered ? '已登记到 vector-hub' : '未登记到 vector-hub';
163
- const ebBadge = document.createElement('span'); ebBadge.className = `mbadge${project.modules.eventBridge && project.modules.eventBridge.registered ? ' on' : ''}`; ebBadge.textContent = '桥'; ebBadge.title = project.modules.eventBridge && project.modules.eventBridge.registered ? '已登记到会话浏览(ai-coding-event-bridge)' : '未登记到会话浏览(ai-coding-event-bridge)';
164
- badges.append(termBadge, vhBadge, ebBadge);
165
- main.append(name, badges);
166
- const caret = document.createElement('span'); caret.className = 'proj-caret'; caret.textContent = '▸'; caret.title = '历史会话';
167
- const dot = document.createElement('span'); dot.className = projectDotClass(project);
168
- line.append(main, dot, caret);
169
- const repo = document.createElement('div'); repo.className = 'project-path'; repo.textContent = project.repoPath || '';
170
- card.append(line, repo);
171
- card.addEventListener('click', event => { if (event.target.closest('.proj-caret')) return; selectProject(project.projectId); });
172
- caret.addEventListener('click', event => {
173
- event.stopPropagation();
174
- const sessions = wrap.querySelector('.proj-sessions');
175
- const expanded = !sessions.hidden;
176
- sessions.hidden = expanded;
177
- caret.classList.toggle('open', !expanded);
178
- if (!expanded && !state.sessionsByProject.has(project.repoPath || project.projectId)) {
179
- sessions.replaceChildren(sessionPlaceholder('正在向终端拉取会话…'));
180
- // 拉取超时兜底:终端未连接/未登记时不至于永远停在"正在拉取"
181
- const key = project.repoPath || project.projectId;
182
- setTimeout(() => {
183
- if (!state.sessionsByProject.has(key)) {
184
- const host = sessions.querySelector('.sess-empty');
185
- if (host) host.textContent = '未能获取会话(终端未连接或该项目未登记到终端)';
186
- }
187
- }, 8000);
188
- postTo('term', { type: 'kb:list-sessions', project: key });
189
- }
190
- });
191
- card.addEventListener('contextmenu', event => {
192
- event.preventDefault();
193
- openContextmenu(project.projectId, event.clientX, event.clientY);
194
- });
195
- const sessions = document.createElement('div'); sessions.className = 'proj-sessions'; sessions.hidden = true;
196
- wrap.append(card, sessions);
197
- list.append(wrap);
198
- $('mobile-project').append(option(project.projectId, project.displayName));
199
- $('logs-project').append(option(project.projectId, project.displayName));
200
- }
201
- $('mobile-project').value = state.activeProjectId;
202
- updateProjectContext();
203
- }
204
- function sessionPlaceholder(text) { const div = document.createElement('div'); div.className = 'sess-empty'; div.textContent = text; return div; }
205
- function renderProjectSessions(projectKey) {
206
- const wrap = [...document.querySelectorAll('.project-item')].find(node => {
207
- const project = state.projects.find(item => item.projectId === node.dataset.projectId);
208
- return project && (project.repoPath || project.projectId) === projectKey;
209
- });
210
- if (!wrap) return;
211
- const host = wrap.querySelector('.proj-sessions');
212
- const sessions = state.sessionsByProject.get(projectKey) || [];
213
- host.replaceChildren();
214
- if (!sessions.length) { host.append(sessionPlaceholder('该模块还没有此项目的会话')); return; }
215
- for (const session of sessions) {
216
- const row = document.createElement('button'); row.type = 'button'; row.className = 'sess-row';
217
- row.dataset.sessionId = session.sessionId;
218
- const title = document.createElement('span'); title.className = 'sess-title'; title.textContent = session.title || session.sessionId;
219
- const agent = document.createElement('span'); agent.className = 'sess-agent'; agent.textContent = session.agentId || '';
220
- row.append(title, agent);
221
- row.addEventListener('click', () => {
222
- postTo('term', { type: 'kb:open-session', sessionId: session.sessionId });
223
- showView('terminal');
224
- });
225
- host.append(row);
226
- }
227
- }
228
- function updateProjectContext() {
229
- const project = activeProject();
230
- setText($('page-title'), ({ terminal: 'Agent 终端', search: '知识检索', bridge: '会话浏览', import: '导入项目' })[state.view] || 'Agent 终端');
231
- linkProjectToModules(project);
232
- }
233
- function selectProject(projectId) {
234
- state.activeProjectId = projectId;
235
- renderProjects();
236
- }
237
- const viewTitles = { terminal: 'Agent 终端', search: '知识检索', bridge: '会话浏览', import: '导入项目' };
238
- function showView(view) {
239
- state.view = view;
240
- document.querySelectorAll('.view').forEach(node => node.classList.toggle('active', node.id === `view-${view}`));
241
- document.querySelectorAll('[data-go]').forEach(node => node.classList.toggle('active', node.dataset.go === view));
242
- setText($('page-title'), viewTitles[view] || view);
243
- hideContextmenu();
244
- }
245
- async function loadState() {
246
- const aggregate = await api('/api/projects/aggregated');
247
- state.projects = (aggregate.projects || []).map(project => ({
248
- projectId: project.projectId,
249
- displayName: project.name || project.projectId,
250
- repoPath: project.workspacePath || '',
251
- knowledgePath: project.knowledgePath || '',
252
- modules: project.modules || { terminal: {}, vectorHub: {}, eventBridge: {} },
253
- }));
254
- if (!state.activeProjectId || !state.projects.some(project => project.projectId === state.activeProjectId)) state.activeProjectId = state.projects[0]?.projectId || '';
255
- renderProjects();
256
- }
257
- async function refreshModulesHealth() {
258
- try {
259
- const body = await api('/api/modules/health');
260
- state.modules.terminal = { ...state.modules.terminal, ...body.modules.terminal };
261
- state.modules.vectorHub = { ...state.modules.vectorHub, ...body.modules.vectorHub };
262
- state.modules.eventBridge = { ...state.modules.eventBridge, ...body.modules.eventBridge };
263
- } catch { /* server down; probes keep their state */ }
264
- }
265
-
266
- /* ===================== 导入:只触发两个模块的登记(壳不自建设置面) ===================== */
267
- async function submitImport(event) {
268
- event.preventDefault();
269
- const notice = $('import-notice') || (() => { const n = document.createElement('div'); n.id = 'import-notice'; n.className = 'notice'; n.hidden = true; document.querySelector('#import-form').appendChild(n); return n; })();
270
- const payload = buildImportPayload();
271
- try {
272
- const body = await api('/api/projects/import', { method: 'POST', body: JSON.stringify(payload) });
273
- const projectId = body.projectId || body.project?.projectId || '';
274
- renderModuleRegistration(body.modules);
275
- notice.hidden = false; notice.className = 'notice'; notice.textContent = '项目已导入并登记到模块。';
276
- await loadState(); selectProject(projectId); showView('terminal'); event.target.reset(); renderFoldIcons();
277
- } catch (error) { notice.hidden = false; notice.className = 'notice error'; notice.textContent = error.message; }
278
- }
279
- function renderModuleRegistration(modules) {
280
- const node = $('pv-modules');
281
- if (!modules) { setText(node, '待登记'); return; }
282
- const term = modules.terminal && modules.terminal.ok ? '终端 ✓' : modules.terminal && modules.terminal.status === 'pending' ? '终端 待登记' : '终端 ✗';
283
- const vh = modules.vectorHub && modules.vectorHub.ok ? '检索 ✓' : modules.vectorHub && modules.vectorHub.status === 'pending' ? '检索 待登记' : '检索 ✗';
284
- const eb = modules.eventBridge && modules.eventBridge.ok ? '会话 ✓' : modules.eventBridge && modules.eventBridge.status === 'pending' ? '会话 待登记' : '会话 ✗';
285
- setText(node, `${term} · ${vh} · ${eb}`);
286
- }
287
- function buildImportPayload() {
288
- // 语言/配置走后端默认(zh-CN / 终端 profile 唯一源)——壳上不再暴露
289
- return {
290
- localPath: $('import-path').value,
291
- knowledgePath: $('import-knowledge-path').value.trim() || null,
292
- };
293
- }
294
-
295
- let preflightSeq = 0;
296
- let preflightTimer = null;
297
- function schedulePreflight() {
298
- if (preflightTimer) clearTimeout(preflightTimer);
299
- preflightTimer = setTimeout(runPreflight, 250);
300
- }
301
- async function runPreflight() {
302
- const seq = ++preflightSeq;
303
- const submit = $('import-submit');
304
- const path = $('import-path').value.trim();
305
- if (!path) {
306
- submit.disabled = true;
307
- setText($('pv-git-status'), '等待路径输入');
308
- setText($('pv-knowledge-root'), $('import-knowledge-path').value.trim() || '—');
309
- setText($('pv-modules'), '待登记');
310
- $('import-preflight').hidden = true;
311
- $('import-errors').hidden = true;
312
- $('import-auto-init').hidden = true;
313
- return;
314
- }
315
- try {
316
- const result = await api('/api/projects/preflight-import', { method: 'POST', body: JSON.stringify(buildImportPayload()) });
317
- if (seq !== preflightSeq) return;
318
- renderPreflight(result);
319
- } catch (error) {
320
- if (seq !== preflightSeq) return;
321
- submit.disabled = true;
322
- const err = $('import-errors');
323
- err.hidden = false;
324
- err.textContent = `Preflight 失败:${error.message}`;
325
- }
326
- }
327
- function renderPreflight(result) {
328
- const submit = $('import-submit');
329
- submit.disabled = !result.ready;
330
- setText($('pv-knowledge-root'), $('import-knowledge-path').value.trim() || (result.effective && result.effective.knowledgeRoot) || '—');
331
- const gitCheck = result.checks && result.checks.git;
332
- if (gitCheck && gitCheck.ok) {
333
- const desc = gitCheck.emptyRepo ? 'Git 仓库(空仓库)' : gitCheck.headCommit ? `Git 仓库 @ ${gitCheck.headCommit.slice(0, 7)}` : 'Git 仓库';
334
- setText($('pv-git-status'), desc);
335
- } else if (gitCheck && gitCheck.plannedInit) setText($('pv-git-status'), '非 Git 目录(将自动初始化)');
336
- else setText($('pv-git-status'), '路径无效');
337
- $('import-auto-init').hidden = !result.plannedGitInit;
338
- const preNotice = $('import-preflight');
339
- if (result.ready) { preNotice.hidden = false; preNotice.className = 'notice'; preNotice.textContent = '前置条件就绪,可以导入。'; }
340
- else preNotice.hidden = true;
341
- const errNotice = $('import-errors');
342
- if (result.problems && result.problems.length) {
343
- errNotice.hidden = false; errNotice.className = 'notice error'; errNotice.replaceChildren();
344
- const strong = document.createElement('strong'); strong.textContent = '导入前需要处理:'; errNotice.append(strong);
345
- const list = document.createElement('ul'); list.style.margin = '6px 0 0 18px'; list.style.padding = '0';
346
- for (const problem of result.problems) {
347
- const li = document.createElement('li');
348
- li.textContent = `${problem.message} [${problem.code}]`;
349
- if (problem.action) { const hint = document.createElement('span'); hint.className = 'muted'; hint.style.marginLeft = '6px'; hint.textContent = `操作:${problem.action}`; li.append(hint); }
350
- list.append(li);
351
- }
352
- errNotice.append(list);
353
- } else { errNotice.hidden = true; errNotice.textContent = ''; }
354
- }
355
-
356
- /* ===================== 日志(连接层运行记录) ===================== */
357
- function logParams(includeCursor = false) {
358
- const params = new URLSearchParams(); const date = $('logs-date').value || today();
359
- params.set('from', date); params.set('to', date); params.set('pageSize', $('logs-limit').value || '500');
360
- if ($('logs-project').value) params.set('projectId', $('logs-project').value);
361
- if ($('logs-scope').value === 'important') params.set('levels', 'info,warn,error,fatal');
362
- if ($('logs-scope').value === 'errors') params.set('levels', 'warn,error,fatal');
363
- if ($('logs-search').value.trim()) params.set('q', $('logs-search').value.trim());
364
- if (includeCursor && state.logCursor) params.set('streamCursor', state.logCursor);
365
- return params;
366
- }
367
- let loadLogsSeq = 0; // 连续触发加载时丢弃过期响应,避免清空后的列表被旧渲染覆盖或重复开流
368
- async function loadLogs() {
369
- const seq = ++loadLogsSeq;
370
- stopLogStream(); $('logs-date').value = $('logs-date').value || today();
371
- $('record-list').replaceChildren(); $('record-empty').style.display = 'none';
372
- setText($('logs-summary'), '正在读取运行记录…');
373
- try {
374
- const body = await api(`/api/logs?${logParams()}`);
375
- if (seq !== loadLogsSeq) return;
376
- state.logs = [...(body.entries || [])].reverse(); state.logCursor = body.streamCursor || ''; state.newLogs = 0; renderLogs();
377
- requestAnimationFrame(() => { const list = $('record-list'); list.scrollTop = list.scrollHeight; });
378
- if ($('logs-date').value === today()) startLogStream();
379
- } catch (error) {
380
- if (seq !== loadLogsSeq) return;
381
- state.logs = []; renderLogs(); $('record-empty').style.display = 'block';
382
- $('record-empty').textContent = error.message; setText($('logs-summary'), '读取失败');
383
- }
384
- }
385
- function createRecordEntry(record) {
386
- const entry = document.createElement('div');
387
- entry.className = 'record-entry'; entry.dataset.logId = record.id; entry.dataset.level = record.level;
388
- const row = document.createElement('button'); row.type = 'button'; row.className = 'record-row';
389
- const main = document.createElement('span'); main.className = 'record-main';
390
- const title = document.createElement('span'); title.className = 'record-title'; title.textContent = record.message;
391
- const meta = document.createElement('span'); meta.className = 'record-meta';
392
- const parts = [
393
- ['record-project', record.projectDisplayName || record.projectId || '系统'],
394
- ['record-category', record.component || record.event || ''],
395
- ['record-duration', record.durationMs != null ? `${record.durationMs.toLocaleString()} ms` : ''],
396
- ];
397
- parts.forEach(([className, value], index) => {
398
- if (!value) return;
399
- if (index) { const sep = document.createElement('span'); sep.className = 'record-meta-sep'; sep.textContent = '·'; meta.append(sep); }
400
- const node = document.createElement('span'); node.className = className; node.textContent = value; meta.append(node);
401
- });
402
- main.append(title, meta);
403
- const time = document.createElement('span'); time.className = 'record-time'; time.textContent = formatTime(record.ts);
404
- row.append(main, time);
405
- const detail = document.createElement('div'); detail.className = 'record-detail';
406
- const grid = document.createElement('div'); grid.className = 'record-detail-grid';
407
- for (const [key, value] of [
408
- ['level', String(record.level || '').toUpperCase()], ['component', record.component || '—'], ['event', record.event || '—'],
409
- ['operationId', record.operationId || '—'], ['runId', record.runId || '—'], ['commit', record.commitSha || '—'],
410
- ['phase', record.phase || '—'], ['attempt', String(record.attempt || 0)], ['durationMs', record.durationMs == null ? '—' : String(record.durationMs)],
411
- ]) {
412
- const span = document.createElement('span'); const b = document.createElement('b'); b.textContent = `${key}:`;
413
- span.append(b, document.createTextNode(` ${value}`)); grid.append(span);
414
- }
415
- const message = document.createElement('div'); message.className = 'record-detail-message'; message.textContent = record.message;
416
- const actions = document.createElement('div'); actions.className = 'record-detail-actions';
417
- const copy = document.createElement('button'); copy.type = 'button'; copy.className = 'btn'; copy.textContent = '复制详情';
418
- copy.addEventListener('click', event => { event.stopPropagation(); copyText(message.textContent); });
419
- actions.append(copy);
420
- detail.append(grid, message, actions);
421
- row.addEventListener('click', () => entry.classList.toggle('open'));
422
- entry.append(row, detail);
423
- return entry;
424
- }
425
- function renderLogs() {
426
- const list = $('record-list'); const nearBottom = list.scrollHeight - list.scrollTop - list.clientHeight < 48;
427
- const frag = document.createDocumentFragment();
428
- for (const record of state.logs) frag.append(createRecordEntry(record));
429
- list.replaceChildren(frag);
430
- $('record-empty').style.display = state.logs.length ? 'none' : 'block';
431
- const limit = Number($('logs-limit').value || 500);
432
- setText($('logs-summary'), `已加载 ${state.logs.length.toLocaleString()} 条 · 显示上限 ${limit.toLocaleString()} 条`);
433
- if (nearBottom) requestAnimationFrame(() => { list.scrollTop = list.scrollHeight; });
434
- updateNewLogButton();
435
- }
436
- function appendLog(record) {
437
- if (state.logs.some(item => item.id === record.id)) return;
438
- const list = $('record-list'); const atBottom = list.scrollHeight - list.scrollTop - list.clientHeight < 48;
439
- state.logs.push(record);
440
- const limit = Number($('logs-limit').value || 500);
441
- if (state.logs.length > limit) {
442
- state.logs.splice(0, state.logs.length - limit);
443
- while (list.children.length > state.logs.length) list.firstElementChild.remove();
444
- }
445
- $('record-empty').style.display = 'none';
446
- list.append(createRecordEntry(record));
447
- setText($('logs-summary'), `已加载 ${state.logs.length.toLocaleString()} 条 · 显示上限 ${limit.toLocaleString()} 条`);
448
- if (!atBottom) state.newLogs += 1;
449
- updateNewLogButton();
450
- if (atBottom) requestAnimationFrame(() => { list.scrollTop = list.scrollHeight; });
451
- }
452
- function updateNewLogButton() { const button = $('new-records'); button.hidden = !state.newLogs; button.textContent = `有 ${state.newLogs} 条新记录`; }
453
- function startLogStream() {
454
- if (!state.logCursor) return;
455
- const stream = new EventSource(`/api/logs/stream?${logParams(true)}`); state.logStream = stream;
456
- const warning = $('logs-connection');
457
- stream.addEventListener('logs/appended', event => { try { appendLog(JSON.parse(event.data).record); } catch {} });
458
- stream.addEventListener('logs/ready', () => warning.classList.remove('show'));
459
- stream.onerror = () => warning.classList.add('show');
460
- }
461
- function stopLogStream() {
462
- if (state.logStream) { state.logStream.close(); state.logStream = null; }
463
- $('logs-connection').classList.remove('show');
464
- }
465
- async function exportLogs() {
466
- const response = await fetch(`/api/logs/export?${logParams()}`); if (!response.ok) throw new Error(`导出失败 (${response.status})`);
467
- const blob = await response.blob(); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `project-knowledge-logs-${$('logs-date').value || today()}.jsonl`; document.body.append(link); link.click(); link.remove(); setTimeout(() => URL.revokeObjectURL(url), 0);
468
- }
469
-
470
- function toggleTheme() {
471
- const next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
472
- document.documentElement.dataset.theme = next; localStorage.setItem('pk-theme', next);
473
- setText($('theme-button'), next === 'dark' ? '浅色模式' : '深色模式');
474
- }
475
-
476
- /* ===================== 设置抽屉(日志) ===================== */
477
- const settingsTitles = {
478
- logs: ['日志', '查看系统和项目的运行记录;异常会自动突出显示。'],
479
- };
480
- function openSettings(section = 'logs') { state.settingsOpen = true; $('settings-backdrop').classList.add('show'); $('settings-drawer').classList.add('show'); showSettings(section); }
481
- function closeSettings() { state.settingsOpen = false; $('settings-backdrop').classList.remove('show'); $('settings-drawer').classList.remove('show'); stopLogStream(); }
482
- function showSettings(section) {
483
- state.settings = section;
484
- document.querySelectorAll('[data-settings]').forEach(node => node.classList.toggle('active', node.dataset.settings === section));
485
- document.querySelectorAll('.settings-section').forEach(node => node.classList.toggle('active', node.id === `settings-${section}`));
486
- const [title, help] = settingsTitles[section] || ['', ''];
487
- setText($('settings-title'), title); setText($('settings-help'), help);
488
- if (section === 'logs') loadLogs(); else stopLogStream();
489
- }
490
-
491
- /* ===================== 右键管理与删除 ===================== */
492
- function openContextmenu(projectId, x, y) {
493
- state.pendingDeleteId = projectId;
494
- setText($('context-id'), projectId);
495
- const menu = $('context-menu');
496
- menu.classList.add('show');
497
- menu.style.left = Math.min(x, window.innerWidth - 210) + 'px';
498
- menu.style.top = Math.min(y, window.innerHeight - 110) + 'px';
499
- }
500
- function hideContextmenu() { $('context-menu').classList.remove('show'); }
501
- function openDeleteDialog(projectId) {
502
- const project = state.projects.find(item => item.projectId === projectId);
503
- if (!project) return;
504
- state.pendingDeleteId = projectId;
505
- setText($('delete-copy'), `确认移除“${project.displayName}”?默认保留知识目录与会话记录。`);
506
- $('delete-knowledge').checked = false; $('delete-confirm').value = ''; $('delete-confirm-field').hidden = true; $('delete-error').hidden = true;
507
- $('delete-dialog').showModal();
508
- }
509
-
510
- /* ===================== 事件绑定 ===================== */
511
- document.querySelectorAll('[data-go]').forEach(node => node.addEventListener('click', () => showView(node.dataset.go)));
512
- document.querySelectorAll('[data-settings]').forEach(node => node.addEventListener('click', () => showSettings(node.dataset.settings)));
513
- for (const id of ['open-settings', 'mobile-settings']) $(id).addEventListener('click', () => openSettings());
514
- $('close-settings').addEventListener('click', closeSettings); $('settings-backdrop').addEventListener('click', closeSettings);
515
- $('theme-button').addEventListener('click', toggleTheme);
516
- $('mobile-project').addEventListener('change', event => selectProject(event.target.value));
517
- document.querySelectorAll('.nav-fold').forEach(node => node.addEventListener('click', event => {
518
- event.stopPropagation();
519
- const key = node.dataset.fold;
520
- state.fold[key] = !state.fold[key];
521
- renderFoldIcons(); syncFold(key);
522
- }));
523
- const uiLang = $('ui-language');
524
- if (uiLang) {
525
- uiLang.value = activeLanguage();
526
- uiLang.addEventListener('change', event => { setLanguage(event.target.value); renderI18nLabels(); });
527
- }
528
- window.__PK_I18N_ONCHANGE__ = () => { if (uiLang) uiLang.value = activeLanguage(); renderI18nLabels(); };
529
- function renderI18nLabels() {
530
- document.querySelectorAll('[data-i18n]').forEach(node => {
531
- const key = node.dataset.i18n;
532
- if (key) node.textContent = i18n(key);
533
- });
534
- if ($('import-path') && $('import-path').placeholder) $('import-path').placeholder = i18n('app.import.path.placeholder');
535
- }
536
- $('import-form').addEventListener('submit', submitImport);
537
- for (const id of ['import-path', 'import-knowledge-path']) {
538
- $(id).addEventListener('input', schedulePreflight);
539
- $(id).addEventListener('change', schedulePreflight);
540
- }
541
- // 目录选择:优先桌面端原生选择器;Web 模式经代理调用模块自带的原生选择器
542
- //(① 用终端的 pick-folder,② 用 vector-hub 的)。两者都返回 {path},取消为 path:null。
543
- async function pickFolderVia(endpoint) {
544
- if (window.screenX !== undefined) {
545
- const win = `${Math.round(window.screenX)},${Math.round(window.screenY)},${Math.round(window.outerWidth)},${Math.round(window.outerHeight)}`;
546
- endpoint += (endpoint.includes('?') ? '&' : '?') + 'win=' + encodeURIComponent(win);
547
- }
548
- const response = await fetch(endpoint);
549
- if (response.status === 501) throw new Error('此环境不支持目录选择,请手动输入路径。');
550
- if (!response.ok) throw new Error(`目录选择失败 (${response.status})`);
551
- return response.json();
552
- }
553
- function bindFolderPicker(buttonId, inputId, endpoint) {
554
- $(buttonId).addEventListener('click', async () => {
555
- const err = $('import-errors');
556
- try {
557
- $(buttonId).disabled = true;
558
- const desktop = window.projectKnowledgeDesktop;
559
- const result = desktop && typeof desktop.pickFolder === 'function'
560
- ? await desktop.pickFolder()
561
- : await pickFolderVia(endpoint);
562
- if (result && result.path) { $(inputId).value = result.path; schedulePreflight(); }
563
- } catch (error) {
564
- err.hidden = false; err.className = 'notice error';
565
- err.textContent = error.message;
566
- } finally {
567
- $(buttonId).disabled = false;
568
- }
569
- });
570
- }
571
- bindFolderPicker('import-pick-folder', 'import-path', '/api/terminal/system/pick-folder');
572
- bindFolderPicker('import-pick-knowledge', 'import-knowledge-path', '/api/vectorhub/system/pick-folder');
573
- const importReset = $('import-reset');
574
- if (importReset) importReset.addEventListener('click', () => setTimeout(schedulePreflight, 0));
575
- let logTimer; for (const id of ['logs-date', 'logs-project', 'logs-scope', 'logs-limit']) $(id).addEventListener('change', loadLogs);
576
- $('logs-search').addEventListener('input', () => { clearTimeout(logTimer); logTimer = setTimeout(loadLogs, 250); });
577
- $('logs-export').addEventListener('click', () => exportLogs().catch(error => { const warning = $('logs-connection'); warning.classList.add('show'); warning.textContent = error.message; }));
578
- $('new-records').addEventListener('click', () => { state.newLogs = 0; updateNewLogButton(); const list = $('record-list'); list.scrollTop = list.scrollHeight; });
579
- $('remove-project').addEventListener('click', () => { hideContextmenu(); openDeleteDialog(state.pendingDeleteId); });
580
- document.addEventListener('click', event => { if (!event.target.closest('.context-menu') && !event.target.closest('.project-card')) hideContextmenu(); });
581
- $('delete-knowledge').addEventListener('change', event => { $('delete-confirm-field').hidden = !event.target.checked; });
582
- $('delete-cancel').addEventListener('click', () => $('delete-dialog').close());
583
- $('delete-submit').addEventListener('click', async () => {
584
- const projectId = state.pendingDeleteId || activeProject()?.projectId; if (!projectId) return;
585
- try {
586
- // 先解除模块登记(keepSessions:本地会话记录与知识目录全部保留)
587
- await api(`/api/projects/${encodeURIComponent(projectId)}/module-remove`, { method: 'POST' }).catch(() => {});
588
- await api(`/api/projects/${encodeURIComponent(projectId)}`, { method: 'DELETE', body: JSON.stringify({ deleteKnowledge: $('delete-knowledge').checked, confirmationToken: $('delete-confirm').value }) });
589
- $('delete-dialog').close(); state.pendingDeleteId = '';
590
- await loadState();
591
- } catch (error) { $('delete-error').hidden = false; $('delete-error').textContent = error.message; }
592
- });
593
-
594
- const storedTheme = localStorage.getItem('pk-theme') || 'light';
595
- document.documentElement.dataset.theme = storedTheme;
596
- setText($('theme-button'), storedTheme === 'dark' ? '浅色模式' : '深色模式');
597
- $('logs-date').value = today();
598
- window.__PK_APP__ = { getState: () => ({ projects: state.projects.length, activeProjectId: state.activeProjectId, view: state.view, settings: state.settings, settingsOpen: state.settingsOpen, logCount: state.logs.length, newLogs: state.newLogs, modules: state.modules, fold: state.fold }), openSettings, showSettings, loadLogs, selectProject, showView };
599
- renderFoldIcons();
600
- (async () => {
601
- await refreshModulesHealth();
602
- mountEmbeds();
603
- probeModules();
604
- await loadState().catch(error => setText($('brand-root'), error.message));
605
- renderBrandStatus();
606
- })();
607
- })();