@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/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
- })();