@sanqianx/project-knowledge 4.7.0 → 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 (192) hide show
  1. package/.agents/plugins/marketplace.json +17 -17
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/CHANGELOG.md +817 -799
  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/chat-claudecodeui-match-test.js +92 -92
  46. package/_site/_test/claude-executable-discovery-test.js +62 -62
  47. package/_site/_test/claude-workbench-test.js +170 -170
  48. package/_site/_test/codex-conversation-projection-test.js +2 -1
  49. package/_site/_test/commit-conversation-binding-test.js +95 -95
  50. package/_site/_test/commit-evidence-test.js +110 -110
  51. package/_site/_test/conversation-api-test.js +109 -109
  52. package/_site/_test/conversation-store-test.js +136 -136
  53. package/_site/_test/data-dir-migration-test.js +57 -57
  54. package/_site/_test/desktop-browser-compat-test.js +28 -28
  55. package/_site/_test/desktop-hook-runtime-regression-test.js +204 -204
  56. package/_site/_test/explicit-commit-processor-test.js +37 -37
  57. package/_site/_test/fixtures/make-git-repos.js +12 -1
  58. package/_site/_test/folder-picker-output-test.js +19 -19
  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/legacy-forward-compat-test.js +257 -257
  75. package/_site/_test/legacy-project-upgrade-e2e-test.js +363 -363
  76. package/_site/_test/logging-api-test.js +89 -89
  77. package/_site/_test/logging-sse-no-gap-test.js +88 -88
  78. package/_site/_test/logging-ui-test.js +106 -104
  79. package/_site/_test/markdown-delta-overlay-test.js +85 -85
  80. package/_site/_test/markdown-maintenance-api-test.js +75 -75
  81. package/_site/_test/mcp-server-test.js +149 -149
  82. package/_site/_test/module-artifact-integrity-test.js +27 -0
  83. package/_site/_test/module-boundary-test.js +30 -0
  84. package/_site/_test/module-bridge-eventbridge-test.js +7 -0
  85. package/_site/_test/module-model-configuration-test.js +43 -0
  86. package/_site/_test/module-process-lifecycle-test.js +24 -0
  87. package/_site/_test/module-stream-proxy-test.js +34 -0
  88. package/_site/_test/non-release-ci-test.js +34 -34
  89. package/_site/_test/offline-boundary-isolation-test.js +32 -32
  90. package/_site/_test/p0-data-migration-characterization-test.js +37 -37
  91. package/_site/_test/p0-e2e-gate-test.js +347 -347
  92. package/_site/_test/packaged-ui-smoke-test.js +11 -9
  93. package/_site/_test/path-consistency-test.js +151 -151
  94. package/_site/_test/pending-sweep-test.js +9 -9
  95. package/_site/_test/post-commit-automation-test.js +87 -87
  96. package/_site/_test/product-diagnostics-ui-test.js +75 -0
  97. package/_site/_test/product-import-ui-test.js +92 -0
  98. package/_site/_test/project-delete-recovery-test.js +64 -64
  99. package/_site/_test/project-goal-editor-test.js +144 -144
  100. package/_site/_test/project-layout-v2-migration-test.js +127 -127
  101. package/_site/_test/project-lifecycle-transaction-test.js +106 -106
  102. package/_site/_test/prompt-settings-test.js +115 -115
  103. package/_site/_test/protected-architecture-gate-test.js +122 -122
  104. package/_site/_test/refactor-characterization-test.js +36 -36
  105. package/_site/_test/release-version-sync-test.js +1 -1
  106. package/_site/_test/requirement-recorder-test.js +173 -173
  107. package/_site/_test/run-all-tests.js +159 -156
  108. package/_site/_test/server-runtime-migration-safety-test.js +27 -27
  109. package/_site/_test/shared-contracts-test.js +58 -58
  110. package/_site/_test/startup-analysis-disabled-test.js +10 -10
  111. package/_site/_test/storage-foundation-test.js +78 -78
  112. package/_site/_test/structured-logger-test.js +101 -101
  113. package/_site/_test/tracking-start-test.js +108 -108
  114. package/_site/_test/v4122-upgrade-data-contract-test.js +56 -56
  115. package/_site/_test/workbench-permission-test.js +117 -117
  116. package/_site/_test/workspace-ui-contract-test.js +28 -43
  117. package/_site/lib/ai-profile-resolver.js +78 -78
  118. package/_site/lib/ai-workspace.js +101 -101
  119. package/_site/lib/automation-config.js +140 -140
  120. package/_site/lib/bridge-adapter.js +1 -0
  121. package/_site/lib/bridge-consumer-service.js +402 -402
  122. package/_site/lib/claude-cli-runner.js +1510 -1510
  123. package/_site/lib/commit-conversation-binder.js +234 -234
  124. package/_site/lib/commit-processing-ledger.js +27 -27
  125. package/_site/lib/commit-prompt.js +202 -202
  126. package/_site/lib/commit-reconciler.js +406 -406
  127. package/_site/lib/contracts.js +284 -279
  128. package/_site/lib/conversation-query-service.js +144 -144
  129. package/_site/lib/conversation-store.js +385 -385
  130. package/_site/lib/data-dir.js +74 -74
  131. package/_site/lib/data-state-classifier.js +40 -40
  132. package/_site/lib/engine-cutover.js +97 -0
  133. package/_site/lib/evidence-bundle.js +228 -228
  134. package/_site/lib/folder-picker-output.js +21 -21
  135. package/_site/lib/git-runner.js +62 -62
  136. package/_site/lib/github-team-store.js +1172 -1172
  137. package/_site/lib/hook-manager.js +203 -202
  138. package/_site/lib/index-service.js +173 -173
  139. package/_site/lib/integration-installer.js +907 -907
  140. package/_site/lib/kb-framework.js +176 -176
  141. package/_site/lib/kb-validator.js +124 -124
  142. package/_site/lib/knowledge-promotion.js +435 -435
  143. package/_site/lib/knowledge-retrieval-service.js +401 -401
  144. package/_site/lib/knowledge-tool-runtime.js +359 -359
  145. package/_site/lib/legacy-data-manifest.js +26 -26
  146. package/_site/lib/llm-client.js +161 -161
  147. package/_site/lib/markdown-knowledge-indexer.js +230 -230
  148. package/_site/lib/migration-service.js +442 -442
  149. package/_site/lib/module-bridge.js +198 -33
  150. package/_site/lib/post-commit-automation.js +87 -87
  151. package/_site/lib/project-lifecycle-service.js +519 -497
  152. package/_site/lib/project-store.js +268 -262
  153. package/_site/lib/requirement-recorder.js +278 -278
  154. package/_site/lib/runtime-endpoint.js +155 -155
  155. package/_site/lib/scanner.js +298 -298
  156. package/_site/lib/server-app.js +1590 -1520
  157. package/_site/lib/settings-store.js +133 -113
  158. package/_site/lib/storage-layout.js +162 -162
  159. package/_site/lib/structured-logger.js +574 -574
  160. package/_site/scripts/folder-picker.ps1 +156 -156
  161. package/_site/scripts/hook-trigger.js +157 -156
  162. package/_site/scripts/install-module-candidates.js +69 -0
  163. package/_site/scripts/pack-module-candidates.js +72 -0
  164. package/_site/scripts/sync-release-version.js +2 -2
  165. package/_site/scripts/vendor-modules.js +47 -0
  166. package/_site/scripts/verify-module-runtime.js +45 -0
  167. package/_site/scripts/verify-product-runtime.js +34 -0
  168. package/bin/project-knowledge-mcp.js +194 -194
  169. package/docs/README.zh-CN.md +34 -25
  170. package/docs/project-registry-schema.md +22 -22
  171. package/docs/testing-strategy.md +33 -33
  172. package/module-runtime-manifest.json +220 -0
  173. package/package.json +13 -9
  174. package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
  175. package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
  176. package/plugins/project-knowledge/.mcp.json +1 -1
  177. package/plugins/project-knowledge/opencode/project-knowledge.md +3 -3
  178. package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -28
  179. package/templates/project-readme.md +34 -34
  180. package/ui/favicon.svg +38 -38
  181. package/ui/index.html +51 -149
  182. package/ui/product.css +9 -0
  183. package/ui/product.js +386 -0
  184. package/vendor-manifest.json +46 -0
  185. package/_site/_test/import-ui-flow-test.js +0 -171
  186. package/_site/_test/project-control-panel-task14-test.js +0 -63
  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/ui/app.css +0 -58
  191. package/ui/app.js +0 -607
  192. 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
- })();