pdd-skills 3.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 (261) hide show
  1. package/README.md +1478 -0
  2. package/bin/pdd.js +354 -0
  3. package/config/bpmn-rules.yaml +166 -0
  4. package/config/checkstyle.xml +105 -0
  5. package/config/eslint.config.js +48 -0
  6. package/config/pmd.xml +91 -0
  7. package/config/prd-rules.yaml +113 -0
  8. package/config/ruff.toml +45 -0
  9. package/config/sqlfluff.cfg +82 -0
  10. package/hooks/hook-executor.js +332 -0
  11. package/index.js +43 -0
  12. package/lib/api-routes.js +750 -0
  13. package/lib/api-server.js +408 -0
  14. package/lib/cache/cache-config.js +209 -0
  15. package/lib/cache/system-cache.js +852 -0
  16. package/lib/config-manager.js +373 -0
  17. package/lib/generate.js +528 -0
  18. package/lib/grpc/grpc-routes.js +1134 -0
  19. package/lib/grpc/grpc-server.js +912 -0
  20. package/lib/grpc/proto-definitions.js +1033 -0
  21. package/lib/init.js +172 -0
  22. package/lib/iteration/auto-fixer.js +1025 -0
  23. package/lib/iteration/auto-reviewer.js +923 -0
  24. package/lib/iteration/controller.js +577 -0
  25. package/lib/list.js +130 -0
  26. package/lib/mcp-server.js +548 -0
  27. package/lib/openclaw/api-integration.js +535 -0
  28. package/lib/openclaw/cli-integration.js +567 -0
  29. package/lib/openclaw/data-sync.js +845 -0
  30. package/lib/openclaw/openclaw-adapter.js +783 -0
  31. package/lib/plugin/example-plugins/code-stats/index.js +332 -0
  32. package/lib/plugin/example-plugins/code-stats/plugin.json +1 -0
  33. package/lib/plugin/example-plugins/custom-linter/index.js +472 -0
  34. package/lib/plugin/example-plugins/custom-linter/plugin.json +1 -0
  35. package/lib/plugin/example-plugins/hello-world/index.js +86 -0
  36. package/lib/plugin/example-plugins/hello-world/plugin.json +1 -0
  37. package/lib/plugin/plugin-manager.js +655 -0
  38. package/lib/plugin/plugin-sdk.js +565 -0
  39. package/lib/plugin/sandbox.js +627 -0
  40. package/lib/quality/rules/maintainability.js +418 -0
  41. package/lib/quality/rules/performance.js +498 -0
  42. package/lib/quality/rules/readability.js +441 -0
  43. package/lib/quality/rules/robustness.js +504 -0
  44. package/lib/quality/rules/security.js +444 -0
  45. package/lib/quality/scorer.js +576 -0
  46. package/lib/report.js +669 -0
  47. package/lib/sdk-base.js +301 -0
  48. package/lib/sdk-js.js +446 -0
  49. package/lib/sdk-python/README.md +546 -0
  50. package/lib/sdk-python/examples/basic_usage.py +450 -0
  51. package/lib/sdk-python/pdd_sdk/__init__.py +180 -0
  52. package/lib/sdk-python/pdd_sdk/client.py +1170 -0
  53. package/lib/sdk-python/pdd_sdk/events.py +423 -0
  54. package/lib/sdk-python/pdd_sdk/exceptions.py +158 -0
  55. package/lib/sdk-python/pdd_sdk/models.py +518 -0
  56. package/lib/sdk-python/pdd_sdk/utils.py +759 -0
  57. package/lib/token/budget-alert.js +367 -0
  58. package/lib/token/budget-manager.js +485 -0
  59. package/lib/update.js +54 -0
  60. package/lib/utils/logger.js +88 -0
  61. package/lib/verify.js +741 -0
  62. package/lib/version.js +52 -0
  63. package/lib/vm/README.md +102 -0
  64. package/lib/vm/dashboard/api-routes.js +669 -0
  65. package/lib/vm/dashboard/server.js +391 -0
  66. package/lib/vm/dashboard/sse.js +358 -0
  67. package/lib/vm/dashboard/static/css/dashboard.css +1378 -0
  68. package/lib/vm/dashboard/static/index.html +118 -0
  69. package/lib/vm/dashboard/static/js/app.js +949 -0
  70. package/lib/vm/dashboard/static/js/charts.js +913 -0
  71. package/lib/vm/dashboard/static/js/kanban-view.js +1053 -0
  72. package/lib/vm/dashboard/static/js/pipeline-view.js +463 -0
  73. package/lib/vm/dashboard/static/js/quality-view.js +598 -0
  74. package/lib/vm/dashboard/static/js/system-view.js +1021 -0
  75. package/lib/vm/data-provider.js +1191 -0
  76. package/lib/vm/event-bus.js +402 -0
  77. package/lib/vm/hooks/extract-hook.js +307 -0
  78. package/lib/vm/hooks/generate-hook.js +374 -0
  79. package/lib/vm/hooks/hook-interface.js +458 -0
  80. package/lib/vm/hooks/report-hook.js +331 -0
  81. package/lib/vm/hooks/verify-hook.js +454 -0
  82. package/lib/vm/models.js +1003 -0
  83. package/lib/vm/reconciler.js +855 -0
  84. package/lib/vm/scanner.js +988 -0
  85. package/lib/vm/state-schema.js +955 -0
  86. package/lib/vm/state-store.js +733 -0
  87. package/lib/vm/tui/components/card.js +339 -0
  88. package/lib/vm/tui/components/progress-bar.js +368 -0
  89. package/lib/vm/tui/components/sparkline.js +327 -0
  90. package/lib/vm/tui/components/status-light.js +294 -0
  91. package/lib/vm/tui/components/table.js +370 -0
  92. package/lib/vm/tui/input.js +335 -0
  93. package/lib/vm/tui/renderer.js +548 -0
  94. package/lib/vm/tui/screens/kanban-screen.js +397 -0
  95. package/lib/vm/tui/screens/overview-screen.js +357 -0
  96. package/lib/vm/tui/screens/quality-screen.js +336 -0
  97. package/lib/vm/tui/screens/system-screen.js +379 -0
  98. package/lib/vm/tui/tui.js +805 -0
  99. package/package.json +1 -0
  100. package/scripts/cso-analyzer.js +198 -0
  101. package/scripts/eval-runner.js +359 -0
  102. package/scripts/i18n-checker.js +109 -0
  103. package/scripts/linter/activiti-linter.js +272 -0
  104. package/scripts/linter/prd-linter.js +162 -0
  105. package/scripts/linter/report-generator.js +207 -0
  106. package/scripts/linter/run-linters.js +285 -0
  107. package/scripts/linter/sql-linter.js +166 -0
  108. package/scripts/token-analyzer.js +162 -0
  109. package/scripts/vm-test.js +180 -0
  110. package/skills/core/official-doc-writer/LICENSE +21 -0
  111. package/skills/core/official-doc-writer/README.md +232 -0
  112. package/skills/core/official-doc-writer/SKILL.md +475 -0
  113. package/skills/core/official-doc-writer/_meta.json +1 -0
  114. package/skills/core/official-doc-writer/document_generator.py +580 -0
  115. package/skills/core/official-doc-writer/evals/default-evals.json +1 -0
  116. package/skills/core/official-doc-writer/examples.md +150 -0
  117. package/skills/core/official-doc-writer/fonts/FONTS_LIST.md +45 -0
  118. package/skills/core/official-doc-writer/fonts/README.md +141 -0
  119. package/skills/core/official-doc-writer/fonts/SIMFANG.TTF +0 -0
  120. package/skills/core/official-doc-writer/fonts/SIMHEI.TTF +0 -0
  121. package/skills/core/official-doc-writer/fonts/SIMKAI.TTF +0 -0
  122. package/skills/core/official-doc-writer/fonts/SIMSUN.TTC +0 -0
  123. package/skills/core/official-doc-writer/fonts//346/226/271/346/255/243/345/260/217/346/240/207/345/256/213GBK.TTF +0 -0
  124. package/skills/core/official-doc-writer/references/GBT_9704-2012_/345/205/232/346/224/277/346/234/272/345/205/263/345/205/254/346/226/207/346/240/274/345/274/217.md +422 -0
  125. package/skills/core/official-doc-writer/scripts/__pycache__/generate_official_doc.cpython-313.pyc +0 -0
  126. package/skills/core/official-doc-writer/scripts/dialog_manager.py +564 -0
  127. package/skills/core/official-doc-writer/scripts/generate_official_doc.py +252 -0
  128. package/skills/core/official-doc-writer/scripts/install_fonts.py +390 -0
  129. package/skills/core/official-doc-writer/scripts/smart_prompts.py +363 -0
  130. package/skills/core/pdd-ba/SKILL.md +305 -0
  131. package/skills/core/pdd-ba/_meta.json +1 -0
  132. package/skills/core/pdd-ba/evals/default-evals.json +1 -0
  133. package/skills/core/pdd-code-reviewer/SKILL.md +378 -0
  134. package/skills/core/pdd-code-reviewer/_meta.json +1 -0
  135. package/skills/core/pdd-code-reviewer/evals/default-evals.json +1 -0
  136. package/skills/core/pdd-doc-change/SKILL.md +350 -0
  137. package/skills/core/pdd-doc-change/_meta.json +1 -0
  138. package/skills/core/pdd-doc-change/evals/default-evals.json +1 -0
  139. package/skills/core/pdd-doc-gardener/SKILL.md +248 -0
  140. package/skills/core/pdd-doc-gardener/_meta.json +1 -0
  141. package/skills/core/pdd-doc-gardener/evals/default-evals.json +1 -0
  142. package/skills/core/pdd-entropy-reduction/SKILL.md +360 -0
  143. package/skills/core/pdd-entropy-reduction/_meta.json +1 -0
  144. package/skills/core/pdd-entropy-reduction/evals/default-evals.json +1 -0
  145. package/skills/core/pdd-entropy-reduction/references/entropy-report-template.md +287 -0
  146. package/skills/core/pdd-entropy-reduction/references/golden-principles.md +573 -0
  147. package/skills/core/pdd-entropy-reduction/scripts/entropy_scan.py +712 -0
  148. package/skills/core/pdd-extract-features/SKILL.md +320 -0
  149. package/skills/core/pdd-extract-features/_meta.json +1 -0
  150. package/skills/core/pdd-extract-features/evals/default-evals.json +1 -0
  151. package/skills/core/pdd-generate-spec/SKILL.md +418 -0
  152. package/skills/core/pdd-generate-spec/_meta.json +1 -0
  153. package/skills/core/pdd-generate-spec/evals/default-evals.json +1 -0
  154. package/skills/core/pdd-implement-feature/SKILL.md +332 -0
  155. package/skills/core/pdd-implement-feature/_meta.json +1 -0
  156. package/skills/core/pdd-implement-feature/evals/default-evals.json +1 -0
  157. package/skills/core/pdd-main/SKILL.md +540 -0
  158. package/skills/core/pdd-main/_meta.json +1 -0
  159. package/skills/core/pdd-main/evals/default-evals.json +1 -0
  160. package/skills/core/pdd-main/evals/evals.json +215 -0
  161. package/skills/core/pdd-verify-feature/SKILL.md +474 -0
  162. package/skills/core/pdd-verify-feature/_meta.json +1 -0
  163. package/skills/core/pdd-verify-feature/evals/default-evals.json +1 -0
  164. package/skills/core/pdd-vm/evals/default-evals.json +1 -0
  165. package/skills/core/traffic-accident-assessor/LICENSE +29 -0
  166. package/skills/core/traffic-accident-assessor/SKILL.md +439 -0
  167. package/skills/core/traffic-accident-assessor/evals/evals.json +1 -0
  168. package/skills/core/traffic-accident-assessor/references/accident-types.md +369 -0
  169. package/skills/core/traffic-accident-assessor/references/liability-rules.md +287 -0
  170. package/skills/core/traffic-accident-assessor/references/traffic-laws.md +226 -0
  171. package/skills/core/traffic-accident-assessor/references//351/253/230/345/260/224/345/244/253/350/257/264/346/230/216/344/271/246.pdf +32576 -106
  172. package/skills/core/traffic-accident-assessor/scripts/generate_official_statement.py +588 -0
  173. package/skills/core/traffic-accident-assessor/scripts/generate_report.py +495 -0
  174. package/skills/core/traffic-accident-assessor/scripts/generate_statement.py +528 -0
  175. package/skills/core/traffic-accident-assessor.zip +0 -0
  176. package/skills/entropy/expert-arch-enforcer/SKILL.md +292 -0
  177. package/skills/entropy/expert-arch-enforcer/_meta.json +1 -0
  178. package/skills/entropy/expert-arch-enforcer/evals/default-evals.json +1 -0
  179. package/skills/entropy/expert-auto-refactor/SKILL.md +327 -0
  180. package/skills/entropy/expert-auto-refactor/_meta.json +1 -0
  181. package/skills/entropy/expert-auto-refactor/evals/default-evals.json +1 -0
  182. package/skills/entropy/expert-code-quality/SKILL.md +468 -0
  183. package/skills/entropy/expert-code-quality/_meta.json +1 -0
  184. package/skills/entropy/expert-code-quality/evals/default-evals.json +1 -0
  185. package/skills/entropy/expert-code-quality/evals/evals.json +109 -0
  186. package/skills/entropy/expert-code-quality/references/code-smells.md +605 -0
  187. package/skills/entropy/expert-code-quality/references/design-patterns.md +1111 -0
  188. package/skills/entropy/expert-code-quality/references/refactoring-catalog.md +1281 -0
  189. package/skills/entropy/expert-code-quality/references/solid-principles.md +524 -0
  190. package/skills/entropy/expert-entropy-auditor/SKILL.md +276 -0
  191. package/skills/entropy/expert-entropy-auditor/_meta.json +1 -0
  192. package/skills/entropy/expert-entropy-auditor/evals/default-evals.json +1 -0
  193. package/skills/expert/expert-activiti/SKILL.md +497 -0
  194. package/skills/expert/expert-activiti/_meta.json +1 -0
  195. package/skills/expert/expert-mysql/SKILL.md +832 -0
  196. package/skills/expert/expert-mysql/_meta.json +1 -0
  197. package/skills/expert/expert-performance/SKILL.md +379 -0
  198. package/skills/expert/expert-performance/_meta.json +1 -0
  199. package/skills/expert/expert-performance/evals/default-evals.json +1 -0
  200. package/skills/expert/expert-ruoyi/SKILL.md +472 -0
  201. package/skills/expert/expert-ruoyi/_meta.json +1 -0
  202. package/skills/expert/expert-security/SKILL.md +1341 -0
  203. package/skills/expert/expert-security/_meta.json +1 -0
  204. package/skills/expert/expert-security/evals/default-evals.json +1 -0
  205. package/skills/expert/software-architect/SKILL.md +350 -0
  206. package/skills/expert/software-architect/_meta.json +1 -0
  207. package/skills/expert/software-engineer/SKILL.md +437 -0
  208. package/skills/expert/software-engineer/_meta.json +1 -0
  209. package/skills/expert/software-engineer/architecture.md +130 -0
  210. package/skills/expert/software-engineer/patterns.md +151 -0
  211. package/skills/expert/software-engineer/testing.md +135 -0
  212. package/skills/expert/system-architect/SKILL.md +628 -0
  213. package/skills/expert/system-architect/_meta.json +1 -0
  214. package/skills/expert/system-architect/assets/templates/ARCHITECTURE.md +25 -0
  215. package/skills/expert/system-architect/assets/templates/README.md +44 -0
  216. package/skills/expert/system-architect/references/js-ts-standards.md +18 -0
  217. package/skills/expert/system-architect/references/python-standards.md +19 -0
  218. package/skills/expert/system-architect/references/scaffolding.md +61 -0
  219. package/skills/expert/system-architect/references/security-checklist.md +21 -0
  220. package/skills/openspec/openspec-apply-change/SKILL.md +156 -0
  221. package/skills/openspec/openspec-apply-change/_meta.json +1 -0
  222. package/skills/openspec/openspec-archive-change/SKILL.md +114 -0
  223. package/skills/openspec/openspec-archive-change/_meta.json +1 -0
  224. package/skills/openspec/openspec-bulk-archive-change/SKILL.md +246 -0
  225. package/skills/openspec/openspec-bulk-archive-change/_meta.json +1 -0
  226. package/skills/openspec/openspec-continue-change/SKILL.md +118 -0
  227. package/skills/openspec/openspec-continue-change/_meta.json +1 -0
  228. package/skills/openspec/openspec-explore/SKILL.md +288 -0
  229. package/skills/openspec/openspec-explore/_meta.json +1 -0
  230. package/skills/openspec/openspec-ff-change/SKILL.md +101 -0
  231. package/skills/openspec/openspec-ff-change/_meta.json +1 -0
  232. package/skills/openspec/openspec-new-change/SKILL.md +74 -0
  233. package/skills/openspec/openspec-new-change/_meta.json +1 -0
  234. package/skills/openspec/openspec-onboard/SKILL.md +554 -0
  235. package/skills/openspec/openspec-onboard/_meta.json +1 -0
  236. package/skills/openspec/openspec-sync-specs/SKILL.md +138 -0
  237. package/skills/openspec/openspec-sync-specs/_meta.json +1 -0
  238. package/skills/openspec/openspec-verify-change/SKILL.md +168 -0
  239. package/skills/openspec/openspec-verify-change/_meta.json +1 -0
  240. package/skills/pr/pdd-multi-review/SKILL.md +534 -0
  241. package/skills/pr/pdd-multi-review/_meta.json +1 -0
  242. package/skills/pr/pdd-pr-batch/SKILL.md +303 -0
  243. package/skills/pr/pdd-pr-batch/_meta.json +1 -0
  244. package/skills/pr/pdd-pr-create/SKILL.md +344 -0
  245. package/skills/pr/pdd-pr-create/_meta.json +1 -0
  246. package/skills/pr/pdd-pr-merge/SKILL.md +286 -0
  247. package/skills/pr/pdd-pr-merge/_meta.json +1 -0
  248. package/skills/pr/pdd-pr-review/SKILL.md +217 -0
  249. package/skills/pr/pdd-pr-review/_meta.json +1 -0
  250. package/skills/pr/pdd-task-manager/SKILL.md +636 -0
  251. package/skills/pr/pdd-task-manager/_meta.json +1 -0
  252. package/skills/pr/pdd-template-engine/SKILL.md +306 -0
  253. package/skills/pr/pdd-template-engine/_meta.json +1 -0
  254. package/templates/behavior-shaping/iron-law-template.md +87 -0
  255. package/templates/behavior-shaping/rationalization-template.md +62 -0
  256. package/templates/behavior-shaping/red-flags-template.md +70 -0
  257. package/templates/bilingual-template.md +139 -0
  258. package/templates/config/default.yaml +47 -0
  259. package/templates/project/default/README.md +31 -0
  260. package/templates/project/frontend/README.md +46 -0
  261. package/templates/project/java/README.md +48 -0
@@ -0,0 +1,1021 @@
1
+ /**
2
+ * SystemView - 系统管理与资产视图 (Panel 4)
3
+ * 展示服务状态、项目资产浏览器、操作按钮、活动日志
4
+ *
5
+ * @module SystemView
6
+ * @version 1.0.0
7
+ * @requires App
8
+ */
9
+
10
+ const SystemView = {
11
+ name: 'system',
12
+
13
+ // 操作日志
14
+ activityLog: [],
15
+
16
+ // 是否暂停日志滚动
17
+ paused: false,
18
+
19
+ // 当前操作状态(防止重复点击)
20
+ operationInProgress: false,
21
+
22
+ /**
23
+ * 初始化视图
24
+ */
25
+ init() {
26
+ App.registerView(this.name, this);
27
+ this.bindActions();
28
+ console.log('[SystemView] Initialized');
29
+ },
30
+
31
+ /**
32
+ * 视图显示时调用
33
+ */
34
+ onShow() {
35
+ this.render();
36
+ },
37
+
38
+ /**
39
+ * SSE事件处理 - 记录活动日志
40
+ * @param {string} type - 事件类型
41
+ * @param {*} data - 事件数据
42
+ */
43
+ onEvent(type, data) {
44
+ this.addActivityLog(type, data);
45
+ },
46
+
47
+ /**
48
+ * 绑定操作按钮事件(使用事件委托)
49
+ */
50
+ bindActions() {
51
+ document.addEventListener('click', async (e) => {
52
+ // 全量重新扫描
53
+ if (e.target.matches('#btn-scan') || e.target.closest('#btn-scan')) {
54
+ await this.actionScan(e.target);
55
+ }
56
+
57
+ // 导入报告
58
+ if (e.target.matches('#btn-import') || e.target.closest('#btn-import')) {
59
+ await this.actionImportReport();
60
+ }
61
+
62
+ // 导出JSON
63
+ if (e.target.matches('#btn-export-json') || e.target.closest('#btn-export-json')) {
64
+ await this.actionExport('json');
65
+ }
66
+
67
+ // 导出Markdown
68
+ if (e.target.matches('#btn-export-md') || e.target.closest('#btn-export-md')) {
69
+ await this.actionExport('md');
70
+ }
71
+
72
+ // 导出CSV
73
+ if (e.target.matches('#btn-export-csv') || e.target.closest('#btn-export-csv')) {
74
+ await this.actionExport('csv');
75
+ }
76
+
77
+ // 清理缓存
78
+ if (e.target.matches('#btn-clear-cache') || e.target.closest('#btn-clear-cache')) {
79
+ await this.actionClearCache();
80
+ }
81
+
82
+ // 暂停/恢复日志
83
+ if (e.target.matches('#btn-toggle-log')) {
84
+ this.paused = !this.paused;
85
+ e.target.textContent = this.paused ? '▶️ 恢复' : '⏸️ 暂停';
86
+ e.target.classList.toggle('btn-warning', this.paused);
87
+ e.target.classList.toggle('btn-outline', !this.paused);
88
+ }
89
+ });
90
+ },
91
+
92
+ /**
93
+ * 主渲染方法
94
+ */
95
+ async render() {
96
+ const container = document.getElementById('view-system');
97
+ if (!container) return;
98
+
99
+ container.innerHTML = `
100
+ <div class="system-view">
101
+ <!-- 上半部分: grid-2 -->
102
+ <div class="grid-2 system-top">
103
+ ${this._renderServiceStatus()}
104
+ ${this._renderAssetBrowser()}
105
+ </div>
106
+
107
+ <!-- 下半部分 -->
108
+ ${this._renderActionButtons()}
109
+ ${this._renderActivityLog()}
110
+ </div>
111
+ `;
112
+
113
+ // 加载服务状态数据
114
+ this._loadSystemHealth();
115
+
116
+ // 滚动日志到底部
117
+ this._scrollLogToBottom();
118
+ },
119
+
120
+ // ==================== 服务状态面板 ====================
121
+
122
+ /**
123
+ * 渲染服务状态面板容器
124
+ * @returns {string} HTML
125
+ */
126
+ _renderServiceStatus() {
127
+ return `
128
+ <div class="card service-status-card">
129
+ <div class="card-header">
130
+ <h3>🖥️ 服务状态</h3>
131
+ <span class="text-muted">系统健康检查</span>
132
+ </div>
133
+ <div class="card-body" id="service-status-content">
134
+ <div class="loading-spinner-small"></div>
135
+ <p class="text-muted">正在检测服务状态...</p>
136
+ </div>
137
+ </div>
138
+ `;
139
+ },
140
+
141
+ /**
142
+ * 加载并渲染服务健康状态
143
+ */
144
+ async _loadSystemHealth() {
145
+ try {
146
+ const response = await fetch('/api/system');
147
+ if (!response.ok) throw new Error('获取系统状态失败');
148
+
149
+ const data = await response.json();
150
+ const health = data.health || data;
151
+
152
+ this._renderServiceStatusContent(health);
153
+
154
+ // 记录到日志
155
+ this.addActivityLog('health_check', { status: health.overall?.status || 'unknown' });
156
+
157
+ } catch (error) {
158
+ console.error('[SystemView] Failed to load health:', error);
159
+ document.getElementById('service-status-content').innerHTML = `
160
+ <div class="text-danger">
161
+ <p>⚠️ 无法连接到系统</p>
162
+ <small>${error.message}</small>
163
+ </div>
164
+ `;
165
+ }
166
+ },
167
+
168
+ /**
169
+ * 渲染服务状态详情
170
+ * @param {Object} health - 健康状态数据
171
+ */
172
+ _renderServiceStatusContent(health) {
173
+ const container = document.getElementById('service-status-content');
174
+ if (!container) return;
175
+
176
+ const services = [
177
+ { key: 'api', name: 'API Server', icon: '🌐' },
178
+ { key: 'mcp', name: 'MCP Server', icon: '🔌' },
179
+ { key: 'grpc', name: 'gRPC Server', icon: '📡' },
180
+ { key: 'openclaw', name: 'OpenClaw', icon: '🦞' }
181
+ ];
182
+
183
+ container.innerHTML = `
184
+ <div class="services-grid">
185
+ ${services.map(svc => {
186
+ const svcHealth = health.services?.[svc.key] || {};
187
+ const status = svcHealth.status || 'unknown';
188
+ const latency = svcHealth.latency || 0;
189
+ const uptime = svcHealth.uptime || 0;
190
+
191
+ return `
192
+ <div class="service-item status-${status}">
193
+ <div class="service-icon">${svc.icon}</div>
194
+ <div class="service-info">
195
+ <div class="service-name">${svc.name}</div>
196
+ <div class="status-light status-${status}" title="${status}"></div>
197
+ </div>
198
+ <div class="service-metrics">
199
+ <div class="metric">
200
+ <span class="metric-label">延迟</span>
201
+ <span class="metric-value">${latency > 0 ? `${latency}ms` : '-'}</span>
202
+ </div>
203
+ <div class="metric">
204
+ <span class="metric-label">运行时间</span>
205
+ <span class="metric-value">${this._formatUptime(uptime)}</span>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ `;
210
+ }).join('')}
211
+ </div>
212
+
213
+ <!-- 插件列表 -->
214
+ ${health.plugins && health.plugins.length > 0 ? `
215
+ <div class="plugins-section mt-3">
216
+ <h5>已加载插件 (${health.plugins.length})</h5>
217
+ <div class="plugins-grid">
218
+ ${health.plugins.map(plugin => `
219
+ <div class="plugin-card">
220
+ <div class="plugin-name">${plugin.name || plugin.id}</div>
221
+ <div class="plugin-version">v${plugin.version || '0.0.0'}</div>
222
+ <div class="plugin-status status-dot status-${plugin.status || 'active'}"></div>
223
+ <button class="btn btn-xs btn-outline plugin-action-btn"
224
+ onclick="SystemView._handlePluginAction('${plugin.id}', '${plugin.status}')"
225
+ ${this.operationInProgress ? 'disabled' : ''}>
226
+ ${plugin.status === 'loaded' || plugin.status === 'active' ? '卸载' : '加载'}
227
+ </button>
228
+ </div>
229
+ `).join('')}
230
+ </div>
231
+ </div>
232
+ ` : ''}
233
+ `;
234
+ },
235
+
236
+ // ==================== 项目资产浏览器 ====================
237
+
238
+ /**
239
+ * 渲染资产浏览器容器
240
+ * @returns {string} HTML
241
+ */
242
+ _renderAssetBrowser() {
243
+ return `
244
+ <div class="card asset-browser-card">
245
+ <div class="card-header">
246
+ <h3>📁 项目资产浏览器</h3>
247
+ <button class="btn btn-xs btn-outline" onclick="SystemView._refreshAssets()">🔄 刷新</button>
248
+ </div>
249
+ <div class="card-body asset-browser" id="asset-browser-content">
250
+ ${this._renderAssetTreeSkeleton()}
251
+ </div>
252
+ </div>
253
+ `;
254
+ },
255
+
256
+ /**
257
+ * 渲染资产树骨架屏
258
+ * @returns {string} HTML
259
+ */
260
+ _renderAssetTreeSkeleton() {
261
+ return `
262
+ <div class="asset-columns">
263
+ <div class="asset-column">
264
+ <div class="asset-folder" data-folder="dev-specs">
265
+ <span class="folder-icon">📁</span>
266
+ <span class="folder-name">dev-specs/</span>
267
+ <span class="folder-count">-</span>
268
+ </div>
269
+ <div class="folder-content collapsed" id="folder-dev-specs"></div>
270
+ </div>
271
+
272
+ <div class="asset-column">
273
+ <div class="asset-folder" data-folder="src">
274
+ <span class="folder-icon">📁</span>
275
+ <span class="folder-name">src/</span>
276
+ <span class="folder-count">-</span>
277
+ </div>
278
+ <div class="folder-content collapsed" id="folder-src"></div>
279
+ </div>
280
+
281
+ <div class="asset-column">
282
+ <div class="asset-folder" data-folder="reports">
283
+ <span class="folder-icon">📁</span>
284
+ <span class="folder-name">reports/</span>
285
+ <span class="folder-count">-</span>
286
+ </div>
287
+ <div class="folder-content collapsed" id="folder-reports"></div>
288
+ </div>
289
+ </div>
290
+ `;
291
+ },
292
+
293
+ /**
294
+ * 刷新资产数据
295
+ */
296
+ async _refreshAssets() {
297
+ // 这里可以从API获取实际的文件列表
298
+ // 目前使用模拟数据或从App.state获取
299
+ const features = App.state.features || [];
300
+
301
+ // 构建模拟的资产数据
302
+ const assets = {
303
+ 'dev-specs': (features || []).filter(f => f.artifacts?.spec).map(f => ({
304
+ name: `${f.id}.md`,
305
+ size: f.artifacts.spec.size || Math.round(Math.random() * 5000) + 1000,
306
+ modifiedAt: f.artifacts.spec.generatedAt || f.updatedAt,
307
+ type: 'markdown'
308
+ })),
309
+ 'src': (features || []).filter(f => f.artifacts?.code).map(f => ({
310
+ name: f.artifacts.code.path || `feature-${f.id}/`,
311
+ files: f.artifacts.code.fileCount || Math.floor(Math.random() * 10) + 1,
312
+ loc: f.artifacts.code.loc || Math.floor(Math.random() * 1000) + 100,
313
+ languages: f.artifacts.code.languages || {},
314
+ modifiedAt: f.updatedAt,
315
+ type: 'directory'
316
+ })),
317
+ 'reports': (features || []).filter(f => f.artifacts?.report).map(f => ({
318
+ name: `${f.id}-report.md`,
319
+ type: 'report',
320
+ size: f.artifacts.report.size || Math.round(Math.random() * 3000) + 500,
321
+ generatedAt: f.artifacts.report.generatedAt || f.updatedAt
322
+ }))
323
+ };
324
+
325
+ this._renderAssetData(assets);
326
+ },
327
+
328
+ /**
329
+ * 渲染资产数据
330
+ * @param {Object} assets - 资产数据
331
+ */
332
+ _renderAssetData(assets) {
333
+ // dev-specs 文件列表
334
+ const specsContainer = document.getElementById('folder-dev-specs');
335
+ if (specsContainer) {
336
+ const specs = assets['dev-specs'] || [];
337
+ specsContainer.previousElementSibling.querySelector('.folder-count').textContent = specs.length;
338
+ specsContainer.innerHTML = specs.length > 0 ? specs.map(file => `
339
+ <div class="file-item file-markdown" onclick="SystemView._previewFile('${file.name}', '${file.type}')">
340
+ <span class="file-icon">📄</span>
341
+ <span class="file-name" title="${file.name}">${this._truncate(file.name, 20)}</span>
342
+ <span class="file-size">${this._formatBytes(file.size)}</span>
343
+ <span class="file-time">${this._formatDate(file.modifiedAt)}</span>
344
+ </div>
345
+ `).join('') : '<div class="empty-folder">空目录</div>';
346
+ }
347
+
348
+ // src 目录统计
349
+ const srcContainer = document.getElementById('folder-src');
350
+ if (srcContainer) {
351
+ const srcItems = assets['src'] || [];
352
+ srcContainer.previousElementSibling.querySelector('.folder-count').textContent =
353
+ `${srcItems.length} 个模块`;
354
+
355
+ let totalFiles = 0;
356
+ let totalLoc = 0;
357
+ const allLanguages = {};
358
+
359
+ srcItems.forEach(item => {
360
+ totalFiles += item.files || 0;
361
+ totalLoc += item.loc || 0;
362
+ Object.entries(item.languages || {}).forEach(([lang, stats]) => {
363
+ allLanguages[lang] = (allLanguages[lang] || 0) + (stats.loc || 0);
364
+ });
365
+ });
366
+
367
+ srcContainer.innerHTML = srcItems.length > 0 ? `
368
+ <div class="src-summary">
369
+ <div class="summary-stat">
370
+ <strong>${totalFiles}</strong> 个文件
371
+ </div>
372
+ <div class="summary-stat">
373
+ <strong>${this._formatNumber(totalLoc)}</strong> 行代码
374
+ </div>
375
+ </div>
376
+ ${Object.keys(allLanguages).length > 0 ? `
377
+ <div class="language-summary">
378
+ ${Object.entries(allLanguages).map(([lang, loc]) => `
379
+ <span class="lang-tag" style="background-color: ${this._getLanguageColor(lang)}20; color: ${this._getLanguageColor(lang)}">
380
+ ${lang}: ${this._formatNumber(loc)} LOC
381
+ </span>
382
+ `).join('')}
383
+ </div>
384
+ ` : ''}
385
+ <div class="module-list">
386
+ ${srcItems.map(item => `
387
+ <div class="file-item file-directory" onclick="SystemView._previewFile('${item.name}', '${item.type}')">
388
+ <span class="file-icon">📂</span>
389
+ <span class="file-name">${item.name}</span>
390
+ <span class="file-meta">${item.files}f / ${this._formatNumber(item.loc)}loc</span>
391
+ </div>
392
+ `).join('')}
393
+ </div>
394
+ ` : '<div class="empty-folder">无源代码</div>';
395
+ }
396
+
397
+ // reports 报告列表
398
+ const reportsContainer = document.getElementById('folder-reports');
399
+ if (reportsContainer) {
400
+ const reports = assets['reports'] || [];
401
+ reportsContainer.previousElementSibling.querySelector('.folder-count').textContent = reports.length;
402
+ reportsContainer.innerHTML = reports.length > 0 ? reports.map(report => `
403
+ <div class="file-item file-report" onclick="SystemView._previewFile('${report.name}', '${report.type}')">
404
+ <span class="file-icon">📊</span>
405
+ <span class="file-name" title="${report.name}">${this._truncate(report.name, 20)}</span>
406
+ <span class="file-type badge badge-outline">${report.type}</span>
407
+ <span class="file-size">${this._formatBytes(report.size)}</span>
408
+ <span class="file-time">${this._formatDate(report.generatedAt)}</span>
409
+ </div>
410
+ `).join('') : '<div class="empty-folder">无报告</div>';
411
+ }
412
+
413
+ // 绑定文件夹展开/折叠事件
414
+ document.querySelectorAll('.asset-folder').forEach(folder => {
415
+ folder.addEventListener('click', () => {
416
+ const folderName = folder.dataset.folder;
417
+ const content = document.getElementById(`folder-${folderName}`);
418
+ if (content) {
419
+ content.classList.toggle('collapsed');
420
+ folder.classList.toggle('expanded');
421
+ }
422
+ });
423
+ });
424
+ },
425
+
426
+ /**
427
+ * 预览文件内容(简单Modal)
428
+ * @param {string} fileName - 文件名
429
+ * @param {string} fileType - 文件类型
430
+ */
431
+ async _previewFile(fileName, fileType) {
432
+ App.showModal(`
433
+ <div class="file-preview-modal">
434
+ <div class="preview-header">
435
+ <h4>📄 ${fileName}</h4>
436
+ <button class="btn btn-sm btn-outline" onclick="App.closeModal()">关闭</button>
437
+ </div>
438
+ <div class="preview-body">
439
+ <div class="loading-spinner-small"></div>
440
+ <p class="text-muted">正在加载文件内容...</p>
441
+ </div>
442
+ </div>
443
+ `);
444
+
445
+ // 尝试获取文件内容
446
+ try {
447
+ const response = await fetch(`/api/file?name=${encodeURIComponent(fileName)}&type=${fileType}`);
448
+ if (response.ok) {
449
+ const data = await response.json();
450
+ setTimeout(() => {
451
+ const previewBody = document.querySelector('.preview-body');
452
+ if (previewBody) {
453
+ previewBody.innerHTML = `<pre class="code-preview"><code>${this._escapeHtml(data.content || '(空文件)')}</code></pre>`;
454
+ }
455
+ }, 200);
456
+ } else {
457
+ throw new Error('无法加载文件');
458
+ }
459
+ } catch (error) {
460
+ setTimeout(() => {
461
+ const previewBody = document.querySelector('.preview-body');
462
+ if (previewBody) {
463
+ previewBody.innerHTML = `<p class="text-warning">⚠️ ${error.message}</p>`;
464
+ }
465
+ }, 200);
466
+ }
467
+ },
468
+
469
+ // ==================== 操作按钮区 ====================
470
+
471
+ /**
472
+ * 渲染操作按钮区
473
+ * @returns {string} HTML
474
+ */
475
+ _renderActionButtons() {
476
+ return `
477
+ <div class="card action-buttons-card">
478
+ <div class="card-header">
479
+ <h3>⚡ 操作中心</h3>
480
+ </div>
481
+ <div class="card-body">
482
+ <div class="action-buttons-row">
483
+ <button id="btn-scan" class="btn btn-primary" title="全量重新扫描所有功能点" ${this.operationInProgress ? 'disabled' : ''}>
484
+ 🔄 全量重新扫描
485
+ </button>
486
+ <button id="btn-import" class="btn btn-secondary" title="导入外部验证报告" ${this.operationInProgress ? 'disabled' : ''}>
487
+ 📥 导入报告
488
+ </button>
489
+ <button id="btn-export-json" class="btn btn-outline" title="导出JSON格式数据" ${this.operationInProgress ? 'disabled' : ''}>
490
+ 💾 导出 JSON
491
+ </button>
492
+ <button id="btn-export-md" class="btn btn-outline" title="导出Markdown格式报告" ${this.operationInProgress ? 'disabled' : ''}>
493
+ 📝 导出 MD
494
+ </button>
495
+ <button id="btn-export-csv" class="btn btn-outline" title="导出CSV格式表格" ${this.operationInProgress ? 'disabled' : ''}>
496
+ 📊 导出 CSV
497
+ </button>
498
+ <button id="btn-clear-cache" class="btn btn-danger" title="清理系统缓存(谨慎操作)" ${this.operationInProgress ? 'disabled' : ''}>
499
+ 🗑️ 清理缓存
500
+ </button>
501
+ </div>
502
+ <div class="action-hint text-muted mt-2">
503
+ <small>提示: 部分操作需要确认,请仔细阅读提示信息</small>
504
+ </div>
505
+ </div>
506
+ </div>
507
+ `;
508
+ },
509
+
510
+ // ==================== 日志与活动流 ====================
511
+
512
+ /**
513
+ * 渲染活动日志容器
514
+ * @returns {string} HTML
515
+ */
516
+ _renderActivityLog() {
517
+ return `
518
+ <div class="card activity-log-card">
519
+ <div class="card-header">
520
+ <h3>📋 操作日志与活动流</h3>
521
+ <div class="log-controls">
522
+ <button id="btn-toggle-log" class="btn btn-xs btn-outline">⏸️ 暂停</button>
523
+ <button class="btn btn-xs btn-outline" onclick="SystemView.activityLog = []; SystemView._renderLogEntries();">
524
+ 🗑️ 清空
525
+ </button>
526
+ </div>
527
+ </div>
528
+ <div class="card-body">
529
+ <div class="activity-log-container" id="activity-log-container">
530
+ <table class="data-table log-table">
531
+ <thead>
532
+ <tr>
533
+ <th width="140">时间戳</th>
534
+ <th width="120">操作类型</th>
535
+ <th width="80">结果</th>
536
+ <th>详情</th>
537
+ </tr>
538
+ </thead>
539
+ <tbody id="log-entries-body">
540
+ ${this.activityLog.length > 0 ?
541
+ this.activityLog.slice(0, 50).map(entry => this._renderLogRow(entry)).join('') :
542
+ '<tr><td colspan="4" class="text-center text-muted py-3">暂无日志记录,操作将自动记录在此处</td></tr>'
543
+ }
544
+ </tbody>
545
+ </table>
546
+ </div>
547
+ </div>
548
+ </div>
549
+ `;
550
+ },
551
+
552
+ /**
553
+ * 渲染单行日志
554
+ * @param {Object} entry - 日志条目
555
+ * @returns {string} HTML
556
+ */
557
+ _renderLogRow(entry) {
558
+ const typeColors = {
559
+ stage_change: '#3498db',
560
+ quality_update: '#27ae60',
561
+ token_threshold: '#f39c12',
562
+ data_refreshed: '#9b59b6',
563
+ health_check: '#1abc9c',
564
+ scan: '#3498db',
565
+ export: '#2ecc71',
566
+ import: '#e67e22',
567
+ clear_cache: '#e74c3c',
568
+ error: '#e74c3c'
569
+ };
570
+
571
+ const color = typeColors[entry.type] || '#95a5a6';
572
+ const isSuccess = !entry.result || entry.result === 'success';
573
+
574
+ return `
575
+ <tr class="log-entry ${isSuccess ? '' : 'log-error'}">
576
+ <td><code class="log-time">${entry.time}</code></td>
577
+ <td>
578
+ <span class="badge badge-outline" style="border-color: ${color}; color: ${color}">
579
+ ${this._formatEventType(entry.type)}
580
+ </span>
581
+ </td>
582
+ <td>
583
+ <span class="status-dot status-${isSuccess ? 'success' : 'error'}"></span>
584
+ </td>
585
+ <td class="log-detail" title="${entry.data}">${this._truncate(entry.data, 80)}</td>
586
+ </tr>
587
+ `;
588
+ },
589
+
590
+ /**
591
+ * 格式化事件类型显示名称
592
+ * @param {string} type - 事件类型
593
+ * @returns {string}
594
+ */
595
+ _formatEventType(type) {
596
+ const names = {
597
+ stage_change: '阶段变更',
598
+ quality_update: '质量更新',
599
+ token_threshold: 'Token预警',
600
+ data_refreshed: '数据刷新',
601
+ health_check: '健康检查',
602
+ scan: '全量扫描',
603
+ export: '数据导出',
604
+ import: '报告导入',
605
+ clear_cache: '清理缓存',
606
+ error: '错误'
607
+ };
608
+ return names[type] || type;
609
+ },
610
+
611
+ /**
612
+ * 添加活动日志条目
613
+ * @param {string} type - 事件类型
614
+ * @param {*} data - 事件数据
615
+ */
616
+ addActivityLog(type, data) {
617
+ if (this.paused) return;
618
+
619
+ const entry = {
620
+ time: new Date().toLocaleTimeString('zh-CN', { hour12: false }),
621
+ type: type,
622
+ result: 'success',
623
+ data: JSON.stringify(data).slice(0, 150)
624
+ };
625
+
626
+ this.activityLog.unshift(entry);
627
+
628
+ // 保持最多200条
629
+ if (this.activityLog.length > 200) {
630
+ this.activityLog.pop();
631
+ }
632
+
633
+ // 实时插入DOM(如果容器存在)
634
+ this._appendLogRow(entry);
635
+ },
636
+
637
+ /**
638
+ * 追加一行日志到DOM
639
+ * @param {Object} entry - 日志条目
640
+ */
641
+ _appendLogRow(entry) {
642
+ const tbody = document.getElementById('log-entries-body');
643
+ if (!tbody) return;
644
+
645
+ // 如果是第一条且是占位符,先清空
646
+ if (tbody.children.length === 1 && tbody.children[0].querySelector('.text-muted')) {
647
+ tbody.innerHTML = '';
648
+ }
649
+
650
+ // 在顶部插入新行
651
+ const tempDiv = document.createElement('div');
652
+ tempDiv.innerHTML = this._renderLogRow(entry);
653
+ const newRow = tempDiv.firstElementChild;
654
+
655
+ tbody.insertBefore(newRow, tbody.firstChild);
656
+
657
+ // 限制显示50行
658
+ while (tbody.children.length > 50) {
659
+ tbody.removeChild(tbody.lastChild);
660
+ }
661
+
662
+ // 自动滚动到底部
663
+ this._scrollLogToBottom();
664
+ },
665
+
666
+ /**
667
+ * 重新渲染所有日志条目
668
+ */
669
+ _renderLogEntries() {
670
+ const tbody = document.getElementById('log-entries-body');
671
+ if (!tbody) return;
672
+
673
+ tbody.innerHTML = this.activityLog.length > 0 ?
674
+ this.activityLog.slice(0, 50).map(entry => this._renderLogRow(entry)).join('') :
675
+ '<tr><td colspan="4" class="text-center text-muted py-3">暂无日志记录</td></tr>';
676
+ },
677
+
678
+ /**
679
+ * 滚动日志到底部
680
+ */
681
+ _scrollLogToBottom() {
682
+ const container = document.getElementById('activity-log-container');
683
+ if (container && !this.paused) {
684
+ container.scrollTop = container.scrollHeight;
685
+ }
686
+ },
687
+
688
+ // ==================== 操作方法 ====================
689
+
690
+ /**
691
+ * 执行全量重新扫描
692
+ * @param {HTMLElement} button - 触发按钮
693
+ */
694
+ async actionScan(button) {
695
+ if (this.operationInProgress) return;
696
+
697
+ // 确认对话框
698
+ const confirmed = confirm(
699
+ '确定要执行全量重新扫描吗?\n\n' +
700
+ '这将:\n' +
701
+ '- 重新扫描所有规格文件\n' +
702
+ '- 重新分析源代码\n' +
703
+ '- 更新所有功能点状态\n\n' +
704
+ '预计耗时: 30秒 ~ 2分钟'
705
+ );
706
+
707
+ if (!confirmed) return;
708
+
709
+ this.operationInProgress = true;
710
+ button.disabled = true;
711
+ button.innerHTML = '⏳ 扫描中...';
712
+
713
+ this.addActivityLog('scan', { action: 'start' });
714
+
715
+ try {
716
+ const response = await fetch('/api/refresh', { method: 'POST' });
717
+ const result = await response.json();
718
+
719
+ if (result.success) {
720
+ this.addActivityLog('scan', { action: 'complete', featuresScanned: result.count || 0 });
721
+ alert(`✅ 扫描完成!\n共扫描 ${result.count || 0} 个功能点`);
722
+
723
+ // 刷新数据
724
+ await App.fetchInitialData();
725
+ this.render();
726
+ } else {
727
+ throw new Error(result.error || '扫描失败');
728
+ }
729
+
730
+ } catch (error) {
731
+ console.error('[SystemView] Scan error:', error);
732
+ this.addActivityLog('error', { message: error.message });
733
+ alert(`❌ 扫描失败: ${error.message}`);
734
+ } finally {
735
+ this.operationInProgress = false;
736
+ button.disabled = false;
737
+ button.innerHTML = '🔄 全量重新扫描';
738
+ }
739
+ },
740
+
741
+ /**
742
+ * 执行导入报告
743
+ */
744
+ async actionImportReport() {
745
+ if (this.operationInProgress) return;
746
+
747
+ // 创建文件输入
748
+ const input = document.createElement('input');
749
+ input.type = 'file';
750
+ input.accept = '.md,.json,.csv';
751
+ input.multiple = true;
752
+
753
+ input.onchange = async (e) => {
754
+ const files = Array.from(e.target.files);
755
+ if (files.length === 0) return;
756
+
757
+ const confirmed = confirm(
758
+ `确定要导入 ${files.length} 个报告文件吗?\n\n` +
759
+ files.map(f => `- ${f.name} (${this._formatBytes(f.size)})`).join('\n')
760
+ );
761
+
762
+ if (!confirmed) return;
763
+
764
+ this.operationInProgress = true;
765
+ this.addActivityLog('import', { files: files.map(f => f.name), count: files.length });
766
+
767
+ try {
768
+ const formData = new FormData();
769
+ files.forEach(file => formData.append('reports', file));
770
+
771
+ const response = await fetch('/api/import', {
772
+ method: 'POST',
773
+ body: formData
774
+ });
775
+
776
+ const result = await response.json();
777
+
778
+ if (result.success) {
779
+ this.addActivityLog('import', { ...result, status: 'success' });
780
+ alert(`✅ 导入成功!\n共导入 ${result.imported || files.length} 个报告`);
781
+ await App.fetchInitialData();
782
+ this.render();
783
+ } else {
784
+ throw new Error(result.error || '导入失败');
785
+ }
786
+
787
+ } catch (error) {
788
+ console.error('[SystemView] Import error:', error);
789
+ this.addActivityLog('error', { message: error.message });
790
+ alert(`❌ 导入失败: ${error.message}`);
791
+ } finally {
792
+ this.operationInProgress = false;
793
+ }
794
+ };
795
+
796
+ input.click();
797
+ },
798
+
799
+ /**
800
+ * 执行数据导出
801
+ * @param {string} format - 导出格式 (json|md|csv)
802
+ */
803
+ async actionExport(format) {
804
+ if (this.operationInProgress) return;
805
+
806
+ this.operationInProgress = true;
807
+ this.addActivityLog('export', { format, action: 'start' });
808
+
809
+ try {
810
+ const response = await fetch(`/api/export?format=${format}`);
811
+
812
+ if (!response.ok) throw new Error('导出失败');
813
+
814
+ // 获取文件名
815
+ const contentDisposition = response.headers.get('content-disposition');
816
+ let filename = `pdd-export.${format}`;
817
+ if (contentDisposition) {
818
+ const match = contentDisposition.match(/filename="?([^"]+)"?/);
819
+ if (match) filename = match[1];
820
+ }
821
+
822
+ // 下载文件
823
+ const blob = await response.blob();
824
+ const url = window.URL.createObjectURL(blob);
825
+ const a = document.createElement('a');
826
+ a.href = url;
827
+ a.download = filename;
828
+ document.body.appendChild(a);
829
+ a.click();
830
+ document.body.removeChild(a);
831
+ window.URL.revokeObjectURL(url);
832
+
833
+ this.addActivityLog('export', { format, status: 'success', filename });
834
+ console.log(`[SystemView] Exported: ${filename}`);
835
+
836
+ } catch (error) {
837
+ console.error('[SystemView] Export error:', error);
838
+ this.addActivityLog('error', { message: error.message, context: `export:${format}` });
839
+ alert(`❌ 导出失败: ${error.message}`);
840
+ } finally {
841
+ this.operationInProgress = false;
842
+ }
843
+ },
844
+
845
+ /**
846
+ * 执行清理缓存
847
+ */
848
+ async actionClearCache() {
849
+ if (this.operationInProgress) return;
850
+
851
+ // 二次确认(危险操作)
852
+ const confirmed = confirm(
853
+ '⚠️ 警告: 您确定要清理系统缓存吗?\n\n' +
854
+ '这将清除:\n' +
855
+ '- L1/L2/L3 所有层级缓存\n' +
856
+ '- 已编译的中间结果\n' +
857
+ '- 临时生成的文件\n\n' +
858
+ '注意: 此操作不可撤销!'
859
+ );
860
+
861
+ if (!confirmed) return;
862
+
863
+ // 第三次确认(最终确认)
864
+ const finalConfirm = prompt('请输入 "CONFIRM" 以确认清理缓存:');
865
+ if (finalConfirm !== 'CONFIRM') {
866
+ alert('已取消操作');
867
+ return;
868
+ }
869
+
870
+ this.operationInProgress = true;
871
+ this.addActivityLog('clear_cache', { action: 'start' });
872
+
873
+ try {
874
+ const response = await fetch('/api/cache/clear', { method: 'POST' });
875
+ const result = await response.json();
876
+
877
+ if (result.success) {
878
+ this.addActivityLog('clear_cache', { ...result, status: 'success' });
879
+ alert(`✅ 缓存清理完成!\n释放空间: ${this._formatBytes(result.freed || 0)}`);
880
+ this.render(); // 刷新视图以更新缓存统计
881
+ } else {
882
+ throw new Error(result.error || '清理失败');
883
+ }
884
+
885
+ } catch (error) {
886
+ console.error('[SystemView] Clear cache error:', error);
887
+ this.addActivityLog('error', { message: error.message });
888
+ alert(`❌ 清理失败: ${error.message}`);
889
+ } finally {
890
+ this.operationInProgress = false;
891
+ }
892
+ },
893
+
894
+ /**
895
+ * 处理插件操作(加载/卸载)
896
+ * @param {string} pluginId - 插件ID
897
+ * @param {string} currentStatus - 当前状态
898
+ */
899
+ async _handlePluginAction(pluginId, currentStatus) {
900
+ const action = currentStatus === 'loaded' || currentStatus === 'active' ? 'unload' : 'load';
901
+ const confirmed = confirm(`确定要${action === 'unload' ? '卸载' : '加载'}插件 "${pluginId}" 吗?`);
902
+
903
+ if (!confirmed) return;
904
+
905
+ try {
906
+ const response = await fetch(`/api/plugins/${pluginId}/${action}`, { method: 'POST' });
907
+ const result = await response.json();
908
+
909
+ if (result.success) {
910
+ this.addActivityLog('plugin_action', { pluginId, action, status: 'success' });
911
+ alert(`✅ 插件${action === 'unload' ? '卸载' : '加载'}成功`);
912
+ this._loadSystemHealth(); // 刷新服务状态
913
+ } else {
914
+ throw new Error(result.error || '操作失败');
915
+ }
916
+
917
+ } catch (error) {
918
+ this.addActivityLog('error', { message: error.message, context: `plugin:${action}` });
919
+ alert(`❌ 操作失败: ${error.message}`);
920
+ }
921
+ },
922
+
923
+ // ==================== 工具方法 ====================
924
+
925
+ /**
926
+ * 格式化运行时间
927
+ * @param {number} seconds - 秒数
928
+ * @returns {string}
929
+ */
930
+ _formatUptime(seconds) {
931
+ if (!seconds) return '-';
932
+ const hours = Math.floor(seconds / 3600);
933
+ const minutes = Math.floor((seconds % 3600) / 60);
934
+ if (hours > 0) return `${hours}h ${minutes}m`;
935
+ return `${minutes}m`;
936
+ },
937
+
938
+ /**
939
+ * 格式化数字(千位分隔符)
940
+ * @param {number} num
941
+ * @returns {string}
942
+ */
943
+ _formatNumber(num) {
944
+ if (!num && num !== 0) return '0';
945
+ return Math.round(num).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
946
+ },
947
+
948
+ /**
949
+ * 格式化字节数
950
+ * @param {number} bytes
951
+ * @returns {string}
952
+ */
953
+ _formatBytes(bytes) {
954
+ if (!bytes) return '0 B';
955
+ const units = ['B', 'KB', 'MB', 'GB'];
956
+ let i = 0;
957
+ while (bytes >= 1024 && i < units.length - 1) {
958
+ bytes /= 1024;
959
+ i++;
960
+ }
961
+ return `${bytes.toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
962
+ },
963
+
964
+ /**
965
+ * 格式化日期
966
+ * @param {string} dateStr
967
+ * @returns {string}
968
+ */
969
+ _formatDate(dateStr) {
970
+ if (!dateStr) return '-';
971
+ try {
972
+ return new Date(dateStr).toLocaleDateString('zh-CN');
973
+ } catch (e) {
974
+ return dateStr;
975
+ }
976
+ },
977
+
978
+ /**
979
+ * 截断文本
980
+ * @param {string} text
981
+ * @param {number} maxLen
982
+ * @returns {string}
983
+ */
984
+ _truncate(text, maxLen = 50) {
985
+ if (!text) return '';
986
+ return text.length > maxLen ? text.substring(0, maxLen) + '...' : text;
987
+ },
988
+
989
+ /**
990
+ * HTML转义
991
+ * @param {string} text
992
+ * @returns {string}
993
+ */
994
+ _escapeHtml(text) {
995
+ if (!text) return '';
996
+ const div = document.createElement('div');
997
+ div.textContent = text;
998
+ return div.innerHTML;
999
+ },
1000
+
1001
+ /**
1002
+ * 获取语言颜色
1003
+ * @param {string} lang
1004
+ * @returns {string}
1005
+ */
1006
+ _getLanguageColor(lang) {
1007
+ const colors = {
1008
+ javascript: '#f7df1e', typescript: '#3178c6', python: '#3776ab',
1009
+ java: '#007396', go: '#00add8', rust: '#dea584',
1010
+ cpp: '#00599c', c: '#a8b9cc', ruby: '#cc342d',
1011
+ php: '#777bb4', swift: '#f05138', kotlin: '#7f52ff'
1012
+ };
1013
+ return colors[lang.toLowerCase()] || '#95a5a6';
1014
+ }
1015
+ };
1016
+
1017
+ // 初始化
1018
+ SystemView.init();
1019
+
1020
+ // 导出全局使用
1021
+ window.SystemView = SystemView;