@ohos-cpf/3rdloop 0.0.4 → 0.0.5

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 (43) hide show
  1. package/README.md +119 -128
  2. package/lib/cli.js +25 -0
  3. package/lib/serve.js +268 -0
  4. package/lib/update.js +46 -6
  5. package/lib/web-ext.js +454 -0
  6. package/lib/web.js +664 -0
  7. package/package.json +2 -1
  8. package/vendor/Server/Skills/arkts-code-use/SKILL.md +270 -0
  9. package/vendor/Server/Skills/arkts-code-use/assets/TEMPLATES.md +367 -0
  10. package/vendor/Server/Skills/arkts-code-use/references/API_VERIFICATION.md +144 -0
  11. package/vendor/Server/Skills/arkts-code-use/references/ARKTS_RULES.md +240 -0
  12. package/vendor/Server/Skills/arkts-code-use/references/CODE_PATTERNS.md +431 -0
  13. package/vendor/Server/Skills/arkts-code-use/references/SYNTAX_CHECK_GUIDE.md +164 -0
  14. package/vendor/Server/Skills/arkts-code-use/scripts/verify-arkts.cjs +428 -0
  15. package/vendor/Server/Skills/gitcode-repo-fork/SKILL.md +310 -0
  16. package/vendor/Server/Skills/gitcode-repo-fork/assets/FORK_REPORT_TEMPLATE.md +113 -0
  17. package/vendor/Server/Skills/gitcode-repo-fork/references/FORK_DECISION_GUIDE.md +124 -0
  18. package/vendor/Server/Skills/gitcode-repo-fork/references/GITCODE_FORK_API.md +95 -0
  19. package/vendor/Server/Skills/gitcode-repo-fork/scripts/gitcode-fork.cjs +285 -0
  20. package/vendor/VERSION +3 -3
  21. package/web/css/arktslibrarycheck.css +322 -0
  22. package/web/css/codecheck.css +464 -0
  23. package/web/css/flutterlibrarycheck.css +322 -0
  24. package/web/css/knowledge.css +332 -0
  25. package/web/css/loop.css +578 -0
  26. package/web/css/md-reader.css +240 -0
  27. package/web/css/rnlibrarycheck.css +322 -0
  28. package/web/css/theme.css +702 -0
  29. package/web/index.html +713 -0
  30. package/web/js/arktslibrarycheck.js +1413 -0
  31. package/web/js/codecheck.js +1039 -0
  32. package/web/js/flutterlibrarycheck.js +1364 -0
  33. package/web/js/health.js +69 -0
  34. package/web/js/knowledge.js +358 -0
  35. package/web/js/loop.js +1102 -0
  36. package/web/js/md-reader.js +435 -0
  37. package/web/js/navigation.js +238 -0
  38. package/web/js/rnlibrarycheck.js +1378 -0
  39. package/web/js/stats.js +110 -0
  40. package/web/js/theme.js +46 -0
  41. package/web/js/utils.js +228 -0
  42. package/web/knowledge.html +146 -0
  43. package/web/loop.html +219 -0
@@ -0,0 +1,435 @@
1
+ /* ════════════════════════════════════════════════════════════════════
2
+ * MarkdownReader — 独立 Markdown 渲染器组件
3
+ * ════════════════════════════════════════════════════════════════════
4
+ *
5
+ * 高内聚低耦合设计:
6
+ * - 自包含的 Markdown → HTML 渲染引擎(零外部依赖)
7
+ * - 支持标题、段落、代码块(行内+多语言)、表格、列表、引用、链接、
8
+ * 图片、分隔线、粗体/斜体/删除线/行内代码
9
+ * - 通过 MarkdownReader.open(title, markdownText) 调用
10
+ * - 不依赖任何外部库或全局状态,仅创建自己的 DOM overlay
11
+ *
12
+ * 用法:
13
+ * MarkdownReader.open('stepId1_loop0_result_0.md', markdownContent)
14
+ * MarkdownReader.close()
15
+ */
16
+ 'use strict';
17
+
18
+ var MarkdownReader = (function() {
19
+
20
+ // ─── 私有: DOM 元素引用 ──────────────────────────────────────────
21
+ var _overlay = null;
22
+ var _titleEl = null;
23
+ var _bodyEl = null;
24
+ var _toggleRawBtn = null;
25
+ var _isRawMode = false;
26
+ var _currentMarkdown = '';
27
+
28
+ // ─── 私有: 初始化 DOM (惰性创建,首次调用时构建) ────────────────
29
+ function _ensureDOM() {
30
+ if (_overlay) return;
31
+
32
+ _overlay = document.createElement('div');
33
+ _overlay.className = 'md-reader-overlay';
34
+
35
+ var dialog = document.createElement('div');
36
+ dialog.className = 'md-reader-dialog';
37
+
38
+ // Header
39
+ var header = document.createElement('div');
40
+ header.className = 'md-reader-header';
41
+
42
+ _titleEl = document.createElement('h3');
43
+ _titleEl.className = 'md-reader-title';
44
+
45
+ var headerRight = document.createElement('div');
46
+ headerRight.className = 'md-reader-header-right';
47
+
48
+ _toggleRawBtn = document.createElement('button');
49
+ _toggleRawBtn.className = 'md-reader-btn-raw';
50
+ _toggleRawBtn.textContent = '📄 源码';
51
+ _toggleRawBtn.title = '切换源码/渲染视图';
52
+ _toggleRawBtn.onclick = _toggleRaw;
53
+
54
+ var closeBtn = document.createElement('button');
55
+ closeBtn.className = 'md-reader-close';
56
+ closeBtn.textContent = '✕';
57
+ closeBtn.title = '关闭';
58
+ closeBtn.onclick = MarkdownReader.close;
59
+
60
+ headerRight.appendChild(_toggleRawBtn);
61
+ headerRight.appendChild(closeBtn);
62
+ header.appendChild(_titleEl);
63
+ header.appendChild(headerRight);
64
+
65
+ // Body
66
+ _bodyEl = document.createElement('div');
67
+ _bodyEl.className = 'md-reader-body';
68
+
69
+ dialog.appendChild(header);
70
+ dialog.appendChild(_bodyEl);
71
+ _overlay.appendChild(dialog);
72
+
73
+ // 点击遮罩关闭
74
+ _overlay.addEventListener('click', function(e) {
75
+ if (e.target === _overlay) MarkdownReader.close();
76
+ });
77
+
78
+ // ESC 关闭
79
+ document.addEventListener('keydown', function(e) {
80
+ if (e.key === 'Escape' && _overlay && _overlay.classList.contains('show')) {
81
+ MarkdownReader.close();
82
+ }
83
+ });
84
+
85
+ document.body.appendChild(_overlay);
86
+ }
87
+
88
+ // ─── 私有: 切换源码/渲染模式 ─────────────────────────────────────
89
+ function _toggleRaw() {
90
+ _isRawMode = !_isRawMode;
91
+ _toggleRawBtn.textContent = _isRawMode ? '🎨 渲染' : '📄 源码';
92
+ _renderContent();
93
+ }
94
+
95
+ function _renderContent() {
96
+ if (_isRawMode) {
97
+ _bodyEl.innerHTML = '<pre class="md-reader-raw">' + _escapeHtml(_currentMarkdown) + '</pre>';
98
+ } else {
99
+ _bodyEl.innerHTML = _renderMarkdown(_currentMarkdown);
100
+ _postProcess();
101
+ }
102
+ }
103
+
104
+ // ─── 私有: HTML 转义 ─────────────────────────────────────────────
105
+ function _escapeHtml(text) {
106
+ if (!text) return '';
107
+ var d = document.createElement('div');
108
+ d.textContent = String(text);
109
+ return d.innerHTML;
110
+ }
111
+
112
+ // ════════════════════════════════════════════════════════════════
113
+ // Markdown 渲染引擎
114
+ // ════════════════════════════════════════════════════════════════
115
+
116
+ function _renderMarkdown(md) {
117
+ if (!md) return '<p class="md-muted">暂无内容</p>';
118
+ md = String(md);
119
+ // 行尾归一化:CRLF / 单独 CR 统一为 LF,避免 split('\n') 后行尾残留 \r
120
+ // 导致无序列表项正则 ^(\s*)([-*+])\s+(.*)$ 匹配失败(. 不匹配 \r)而死循环
121
+ md = md.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
122
+
123
+ // 1. 提取代码块(保护内容不被后续处理破坏)
124
+ var codeBlocks = [];
125
+ md = md.replace(/```(\w*)\n?([\s\S]*?)```/g, function(match, lang, code) {
126
+ var idx = codeBlocks.length;
127
+ codeBlocks.push({ lang: lang || '', code: code.replace(/\n$/, '') });
128
+ return '\u0000CODEBLOCK' + idx + '\u0000';
129
+ });
130
+
131
+ // 2. HTML 转义剩余文本
132
+ md = _escapeHtml(md);
133
+
134
+ // 3. 提取表格(在行处理前)
135
+ md = _processTables(md);
136
+
137
+ // 4. 按行处理块级元素
138
+ var lines = md.split('\n');
139
+ var html = [];
140
+ var i = 0;
141
+
142
+ while (i < lines.length) {
143
+ var line = lines[i];
144
+
145
+ // 代码块占位符(单独一行)
146
+ var cbMatch = line.match(/^\u0000CODEBLOCK(\d+)\u0000$/);
147
+ if (cbMatch) {
148
+ var cb = codeBlocks[Number(cbMatch[1])];
149
+ html.push(_renderCodeBlock(cb.code, cb.lang));
150
+ i++;
151
+ continue;
152
+ }
153
+
154
+ // 标题 h1-h6
155
+ var hMatch = line.match(/^(#{1,6})\s+(.*)$/);
156
+ if (hMatch) {
157
+ var level = hMatch[1].length;
158
+ var text = _renderInline(hMatch[2]);
159
+ html.push('<h' + level + ' class="md-h md-h' + level + '">' + text + '</h' + level + '>');
160
+ i++;
161
+ continue;
162
+ }
163
+
164
+ // 分隔线
165
+ if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
166
+ html.push('<hr class="md-hr">');
167
+ i++;
168
+ continue;
169
+ }
170
+
171
+ // 引用块(连续 > 开头)
172
+ if (/^>\s?/.test(line)) {
173
+ var quoteLines = [];
174
+ while (i < lines.length && /^>\s?/.test(lines[i])) {
175
+ quoteLines.push(lines[i].replace(/^>\s?/, ''));
176
+ i++;
177
+ }
178
+ html.push('<blockquote class="md-quote">' + _renderInline(quoteLines.join('\n')) + '</blockquote>');
179
+ continue;
180
+ }
181
+
182
+ // 无序列表(- * +)
183
+ if (/^(\s*)[-*+]\s+/.test(line)) {
184
+ var ulItems = [];
185
+ var ulIndent = 0;
186
+ while (i < lines.length) {
187
+ var ulMatch = lines[i].match(/^(\s*)([-*+])\s+(.*)$/);
188
+ if (ulMatch) {
189
+ var indent = ulMatch[1].length;
190
+ if (ulItems.length === 0) ulIndent = indent;
191
+ if (indent === ulIndent) {
192
+ ulItems.push({ text: ulMatch[3], sub: [] });
193
+ } else if (indent > ulIndent && ulItems.length > 0) {
194
+ ulItems[ulItems.length - 1].sub.push(ulMatch[3]);
195
+ }
196
+ i++;
197
+ } else if (lines[i].trim() === '' && i + 1 < lines.length && /^(\s*)[-*+]\s+/.test(lines[i + 1])) {
198
+ i++; // 列表内空行
199
+ } else {
200
+ break;
201
+ }
202
+ }
203
+ html.push('<ul class="md-ul">' + ulItems.map(function(item) {
204
+ var sub = item.sub.length > 0
205
+ ? '<ul class="md-ul md-ul-sub">' + item.sub.map(function(s) { return '<li>' + _renderInline(s) + '</li>'; }).join('') + '</ul>'
206
+ : '';
207
+ return '<li>' + _renderInline(item.text) + sub + '</li>';
208
+ }).join('') + '</ul>');
209
+ continue;
210
+ }
211
+
212
+ // 有序列表(1. 2. 等)
213
+ if (/^\d+\.\s+/.test(line)) {
214
+ var olItems = [];
215
+ while (i < lines.length && /^\d+\.\s+/.test(lines[i])) {
216
+ olItems.push(lines[i].replace(/^\d+\.\s+/, ''));
217
+ i++;
218
+ }
219
+ html.push('<ol class="md-ol">' + olItems.map(function(t) { return '<li>' + _renderInline(t) + '</li>'; }).join('') + '</ol>');
220
+ continue;
221
+ }
222
+
223
+ // 空行
224
+ if (line.trim() === '') {
225
+ i++;
226
+ continue;
227
+ }
228
+
229
+ // 普通段落(连续非空非特殊行合并)
230
+ var paraLines = [];
231
+ while (i < lines.length
232
+ && lines[i].trim() !== ''
233
+ && !/^#{1,6}\s/.test(lines[i])
234
+ && !/^>\s?/.test(lines[i])
235
+ && !/^[-*+]\s+/.test(lines[i])
236
+ && !/^\d+\.\s+/.test(lines[i])
237
+ && !/^(-{3,}|\*{3,}|_{3,})\s*$/.test(lines[i])
238
+ && !/^\u0000CODEBLOCK/.test(lines[i])
239
+ && !/^\|/.test(lines[i])
240
+ ) {
241
+ // 检查是否是代码块占位符
242
+ var inlineCb = lines[i].match(/^\u0000CODEBLOCK(\d+)\u0000$/);
243
+ if (inlineCb) {
244
+ if (paraLines.length > 0) break;
245
+ var cb2 = codeBlocks[Number(inlineCb[1])];
246
+ html.push(_renderCodeBlock(cb2.code, cb2.lang));
247
+ i++;
248
+ paraLines = null;
249
+ break;
250
+ }
251
+ paraLines.push(lines[i]);
252
+ i++;
253
+ }
254
+ if (paraLines && paraLines.length > 0) {
255
+ html.push('<p class="md-p">' + _renderInline(paraLines.join(' ')) + '</p>');
256
+ }
257
+ }
258
+
259
+ // 5. 还原代码块占位符(如果上面未处理到)
260
+ var result = html.join('\n');
261
+ result = result.replace(/\u0000CODEBLOCK(\d+)\u0000/g, function(match, idx) {
262
+ var cb = codeBlocks[Number(idx)];
263
+ return _renderCodeBlock(cb.code, cb.lang);
264
+ });
265
+
266
+ return result;
267
+ }
268
+
269
+ // ─── 表格处理 ─────────────────────────────────────────────────────
270
+ function _processTables(text) {
271
+ var lines = text.split('\n');
272
+ var result = [];
273
+ var i = 0;
274
+
275
+ while (i < lines.length) {
276
+ if (/^\|/.test(lines[i]) && i + 1 < lines.length && /^\|[\s:|-]+\|/.test(lines[i + 1])) {
277
+ // 收集表格行
278
+ var tableLines = [];
279
+ while (i < lines.length && /^\|/.test(lines[i])) {
280
+ tableLines.push(lines[i]);
281
+ i++;
282
+ }
283
+ result.push(_renderTable(tableLines));
284
+ } else {
285
+ result.push(lines[i]);
286
+ i++;
287
+ }
288
+ }
289
+
290
+ return result.join('\n');
291
+ }
292
+
293
+ function _renderTable(tableLines) {
294
+ if (tableLines.length < 2) return tableLines.join('\n');
295
+
296
+ var parseRow = function(line) {
297
+ return line.replace(/^\|/, '').replace(/\|$/, '').split('|').map(function(c) { return c.trim(); });
298
+ };
299
+
300
+ var headers = parseRow(tableLines[0]);
301
+ var aligns = parseRow(tableLines[1]).map(function(spec) {
302
+ if (/^:/.test(spec) && /:$/.test(spec)) return 'center';
303
+ if (/:$/.test(spec)) return 'right';
304
+ return 'left';
305
+ });
306
+
307
+ var bodyRows = tableLines.slice(2).map(parseRow);
308
+
309
+ var html = '<div class="md-table-wrap"><table class="md-table"><thead><tr>';
310
+ headers.forEach(function(h, idx) {
311
+ html += '<th style="text-align:' + (aligns[idx] || 'left') + '">' + _renderInline(h) + '</th>';
312
+ });
313
+ html += '</tr></thead><tbody>';
314
+ bodyRows.forEach(function(row) {
315
+ html += '<tr>';
316
+ row.forEach(function(cell, idx) {
317
+ html += '<td style="text-align:' + (aligns[idx] || 'left') + '">' + _renderInline(cell) + '</td>';
318
+ });
319
+ html += '</tr>';
320
+ });
321
+ html += '</tbody></table></div>';
322
+
323
+ return html;
324
+ }
325
+
326
+ // ─── 代码块渲染 ───────────────────────────────────────────────────
327
+ function _renderCodeBlock(code, lang) {
328
+ var langLabel = lang ? '<span class="md-code-lang">' + _escapeHtml(lang) + '</span>' : '';
329
+ var copyBtn = '<button class="md-code-copy" data-code="' + encodeURIComponent(code) + '">复制</button>';
330
+ return '<div class="md-code-block">' +
331
+ '<div class="md-code-header">' + langLabel + copyBtn + '</div>' +
332
+ '<pre><code>' + _escapeHtml(code) + '</code></pre>' +
333
+ '</div>';
334
+ }
335
+
336
+ // ─── 行内元素渲染 ─────────────────────────────────────────────────
337
+ function _renderInline(text) {
338
+ if (!text) return '';
339
+
340
+ // 行内代码(先处理,保护内容)
341
+ var inlineCodes = [];
342
+ text = text.replace(/`([^`]+)`/g, function(match, code) {
343
+ var idx = inlineCodes.length;
344
+ inlineCodes.push(code);
345
+ return '\u0001INLINECODE' + idx + '\u0001';
346
+ });
347
+
348
+ // 图片 ![alt](url)
349
+ text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, function(match, alt, url) {
350
+ return '<img class="md-img" src="' + _escapeAttr(url) + '" alt="' + _escapeAttr(alt) + '" loading="lazy">';
351
+ });
352
+
353
+ // 链接 [text](url)
354
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(match, linkText, url) {
355
+ return '<a class="md-link" href="' + _escapeAttr(url) + '" target="_blank" rel="noopener">' + _renderInline(linkText) + '</a>';
356
+ });
357
+
358
+ // 粗体+斜体 ***text***
359
+ text = text.replace(/\*\*\*([^*]+)\*\*\*/g, '<strong><em>$1</em></strong>');
360
+ // 粗体 **text**
361
+ text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
362
+ // 斜体 *text*
363
+ text = text.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>');
364
+ // 删除线 ~~text~~
365
+ text = text.replace(/~~([^~]+)~~/g, '<del>$1</del>');
366
+
367
+ // 还原行内代码
368
+ text = text.replace(/\u0001INLINECODE(\d+)\u0001/g, function(match, idx) {
369
+ return '<code class="md-inline-code">' + _escapeHtml(inlineCodes[Number(idx)]) + '</code>';
370
+ });
371
+
372
+ return text;
373
+ }
374
+
375
+ function _escapeAttr(text) {
376
+ return String(text).replace(/"/g, '&quot;').replace(/'/g, '&#39;');
377
+ }
378
+
379
+ // ─── 后处理: 代码复制按钮事件绑定 ─────────────────────────────────
380
+ function _postProcess() {
381
+ if (!_bodyEl) return;
382
+ var copyBtns = _bodyEl.querySelectorAll('.md-code-copy');
383
+ copyBtns.forEach(function(btn) {
384
+ btn.onclick = function() {
385
+ var code = decodeURIComponent(btn.getAttribute('data-code'));
386
+ try {
387
+ navigator.clipboard.writeText(code);
388
+ btn.textContent = '✓ 已复制';
389
+ setTimeout(function() { btn.textContent = '复制'; }, 2000);
390
+ } catch(e) {
391
+ btn.textContent = '✗ 失败';
392
+ setTimeout(function() { btn.textContent = '复制'; }, 2000);
393
+ }
394
+ };
395
+ });
396
+ }
397
+
398
+ // ════════════════════════════════════════════════════════════════
399
+ // 公开 API
400
+ // ════════════════════════════════════════════════════════════════
401
+
402
+ return {
403
+ /**
404
+ * 打开 Markdown 阅读器,渲染指定内容。
405
+ * @param {string} title - 标题(通常为文件名)
406
+ * @param {string} markdown - Markdown 文本
407
+ */
408
+ open: function(title, markdown) {
409
+ _ensureDOM();
410
+ _titleEl.textContent = title || 'Markdown 阅读器';
411
+ _currentMarkdown = markdown || '';
412
+ _isRawMode = false;
413
+ _toggleRawBtn.textContent = '📄 源码';
414
+ _renderContent();
415
+ _overlay.classList.add('show');
416
+ },
417
+
418
+ /**
419
+ * 关闭阅读器。
420
+ */
421
+ close: function() {
422
+ if (_overlay) _overlay.classList.remove('show');
423
+ },
424
+
425
+ /**
426
+ * 仅渲染 Markdown 为 HTML 字符串(不打开弹窗)。
427
+ * 供外部需要嵌入渲染结果的场景使用。
428
+ * @param {string} markdown
429
+ * @returns {string} HTML 字符串
430
+ */
431
+ render: function(markdown) {
432
+ return _renderMarkdown(markdown);
433
+ }
434
+ };
435
+ })();
@@ -0,0 +1,238 @@
1
+ /* ════════════════════════════════════════════════════════════════════
2
+ * navigation.js — 视图模式切换 + 功能面板导航 + URL Hash 持久化
3
+ *
4
+ * 两层结构:
5
+ * 1. ViewMode(顶栏切换器):功能工作台 ↔ 循环控制台 ↔ 知识管理
6
+ * - loop 模式:隐藏左侧 nav,循环控制台独占全宽
7
+ * - knowledge 模式:隐藏左侧 nav,知识管理独占全宽
8
+ * - features 模式:显示左侧 nav,选择功能面板操作
9
+ * 2. Nav(左侧导航):功能面板切换 + 懒加载初始化
10
+ *
11
+ * URL Hash 规则:
12
+ * - #loop → 循环控制台
13
+ * - #knowledge → 知识管理
14
+ * - #p-codecheck → 功能工作台 / PR 代码检视
15
+ * - 其他 #p-xxx → 功能工作台 / 对应面板
16
+ * ════════════════════════════════════════════════════════════════════ */
17
+ 'use strict';
18
+
19
+ /* ─── 视图模式管理 ──────────────────────────────────────────────────── */
20
+ var ViewMode = {
21
+ _current: 'features',
22
+ _loopInited: false,
23
+ _knowledgeInited: false,
24
+
25
+ /**
26
+ * 切换视图模式
27
+ * @param {string} mode - 'loop' | 'knowledge' | 'features'
28
+ */
29
+ set: function (mode) {
30
+ this._current = mode;
31
+ document.querySelector('.layout').setAttribute('data-view', mode);
32
+
33
+ // 更新切换器按钮激活态
34
+ document.querySelectorAll('.view-btn').forEach(function (btn) {
35
+ btn.classList.toggle('active', btn.dataset.view === mode);
36
+ });
37
+
38
+ if (mode === 'loop') {
39
+ // 切到循环控制台时,隐藏所有功能面板(移除 .active),避免与 loop 面板同时可见
40
+ document.querySelectorAll('#main > .panel:not(#p-loop):not(#p-knowledge)').forEach(function (p) {
41
+ p.classList.remove('active');
42
+ });
43
+ // 懒加载初始化循环控制台
44
+ if (!this._loopInited && typeof initLoopPanel === 'function') {
45
+ this._loopInited = true;
46
+ initLoopPanel();
47
+ }
48
+ history.replaceState(null, '', '#loop');
49
+ } else if (mode === 'knowledge') {
50
+ // 切到知识管理时,隐藏所有功能面板(移除 .active),避免与知识面板同时可见
51
+ document.querySelectorAll('#main > .panel:not(#p-loop):not(#p-knowledge)').forEach(function (p) {
52
+ p.classList.remove('active');
53
+ });
54
+ // 懒加载初始化知识管理面板
55
+ if (!this._knowledgeInited && typeof initKnowledgePanel === 'function') {
56
+ this._knowledgeInited = true;
57
+ initKnowledgePanel();
58
+ }
59
+ history.replaceState(null, '', '#knowledge');
60
+ } else {
61
+ // 切到功能工作台时,确保有一个功能面板处于激活态
62
+ var hasActive = document.querySelector('#main > .panel:not(#p-loop):not(#p-knowledge).active');
63
+ if (!hasActive) {
64
+ Nav.switchTo('p-codecheck');
65
+ }
66
+ }
67
+
68
+ localStorage.setItem('view-mode', mode);
69
+ },
70
+
71
+ /** 初始化视图模式 */
72
+ init: function () {
73
+ // 绑定切换器按钮
74
+ document.querySelectorAll('.view-btn').forEach(function (btn) {
75
+ btn.addEventListener('click', function () {
76
+ ViewMode.set(btn.dataset.view);
77
+ });
78
+ });
79
+
80
+ // 恢复状态:URL Hash > localStorage > 默认 features
81
+ var hash = location.hash.slice(1);
82
+ if (hash === 'loop') {
83
+ this.set('loop');
84
+ } else if (hash === 'knowledge' || hash === 'p-knowledge') {
85
+ this.set('knowledge');
86
+ } else if (hash && hash.startsWith('p-')) {
87
+ // hash 指向某个功能面板 → 切到 features 模式并激活该面板
88
+ var target = document.getElementById(hash);
89
+ if (target && target.classList.contains('panel') && hash !== 'p-loop' && hash !== 'p-knowledge') {
90
+ this.set('features');
91
+ Nav.switchTo(hash);
92
+ return;
93
+ }
94
+ this.set('features');
95
+ } else {
96
+ // 无 hash,查 localStorage
97
+ var saved = localStorage.getItem('view-mode');
98
+ if (saved === 'loop') {
99
+ this.set('loop');
100
+ } else if (saved === 'knowledge') {
101
+ this.set('knowledge');
102
+ } else {
103
+ this.set('features');
104
+ }
105
+ }
106
+ }
107
+ };
108
+
109
+ /* ─── 功能面板导航 ──────────────────────────────────────────────────── */
110
+ var Nav = {
111
+ /** 面板初始化注册表:panelId → initFn(只执行一次) */
112
+ _initMap: {},
113
+ _initialized: {},
114
+
115
+ /**
116
+ * 注册面板初始化函数
117
+ * @param {string} panelId - 面板 DOM ID
118
+ * @param {function} initFn - 初始化函数(首次激活时调用)
119
+ */
120
+ register: function (panelId, initFn) {
121
+ this._initMap[panelId] = initFn;
122
+ },
123
+
124
+ /**
125
+ * 切换到指定功能面板(不影响循环控制台)
126
+ * @param {string} panelId
127
+ */
128
+ switchTo: function (panelId) {
129
+ // 更新 nav-item 激活态
130
+ document.querySelectorAll('.nav-item').forEach(function (n) {
131
+ n.classList.toggle('active', n.dataset.panel === panelId);
132
+ });
133
+
134
+ // 显示/隐藏功能面板(排除 #p-loop / #p-knowledge,它们由 ViewMode 控制)
135
+ document.querySelectorAll('#main > .panel:not(#p-loop):not(#p-knowledge)').forEach(function (p) {
136
+ p.classList.toggle('active', p.id === panelId);
137
+ });
138
+
139
+ // 懒加载初始化
140
+ if (!this._initialized[panelId] && this._initMap[panelId]) {
141
+ this._initialized[panelId] = true;
142
+ try {
143
+ this._initMap[panelId]();
144
+ } catch (e) {
145
+ console.error('[Nav] 面板初始化失败 [' + panelId + ']:', e);
146
+ }
147
+ }
148
+
149
+ // URL Hash 持久化(功能面板的 hash 以 p- 开头)
150
+ history.replaceState(null, '', '#' + panelId);
151
+ },
152
+
153
+ /** 初始化功能面板导航 */
154
+ init: function () {
155
+ var self = this;
156
+
157
+ // 绑定 nav-item 点击事件
158
+ document.querySelectorAll('.nav-item').forEach(function (el) {
159
+ el.addEventListener('click', function () {
160
+ if (el.classList.contains('nav-item-disabled')) {
161
+ toast('info', '该功能即将支持,敬请期待');
162
+ return;
163
+ }
164
+ var panelId = el.dataset.panel;
165
+ if (panelId) self.switchTo(panelId);
166
+ });
167
+ });
168
+ }
169
+ };
170
+
171
+ /* ─── 扩展功能导航(3rdloop web 扩展注册表) ──────────────────────────
172
+ * 从 /api/ext-registry 拉取已挂载扩展(tag/issue/prcheck 等二级目录功能),
173
+ * 动态渲染导航项,点击跳转对应挂载路径(独立页面,浏览器返回键回到工作台)。
174
+ * 端点不存在(如纯静态部署/旧版 Web/server.js)或无扩展时静默隐藏整个分区。
175
+ * ──────────────────────────────────────────────────────────────────── */
176
+ var ExtNav = {
177
+ init: function () {
178
+ fetch('/api/ext-registry')
179
+ .then(function (res) { return res.ok ? res.json() : null; })
180
+ .then(function (payload) {
181
+ var exts = payload && payload.data;
182
+ if (!exts || !exts.length) return;
183
+
184
+ var host = document.getElementById('nav-ext-section');
185
+ if (!host) return;
186
+
187
+ exts.sort(function (a, b) {
188
+ return (a.order || 100) - (b.order || 100) ||
189
+ String(a.name).localeCompare(String(b.name));
190
+ });
191
+
192
+ exts.forEach(function (ext) {
193
+ if (!ext.mount) return;
194
+ var item = document.createElement('div');
195
+ item.className = 'nav-item nav-item-ext';
196
+ item.title = ext.description || ext.title || ext.name;
197
+
198
+ var badge = document.createElement('span');
199
+ badge.className = 'method m-post';
200
+ badge.textContent = 'EXT';
201
+ item.appendChild(badge);
202
+ item.appendChild(document.createTextNode(' ' + (ext.title || ext.name)));
203
+
204
+ item.addEventListener('click', function () {
205
+ window.location.href = ext.mount;
206
+ });
207
+ host.appendChild(item);
208
+ });
209
+
210
+ host.style.display = '';
211
+ })
212
+ .catch(function () { /* 扩展服务不可用(纯静态部署)时静默隐藏 */ });
213
+ }
214
+ };
215
+
216
+ window.Nav = Nav;
217
+ window.ViewMode = ViewMode;
218
+
219
+ /* ─── 页面加载入口 ─────────────────────────────────────────────────── */
220
+ window.addEventListener('load', function () {
221
+ // 1. 主题(最先执行,避免闪烁)
222
+ Theme.init();
223
+
224
+ // 2. 健康检查(异步,不阻塞)
225
+ Health.startPolling();
226
+
227
+ // 3. 统计计数(异步,不阻塞)
228
+ Stats.init();
229
+
230
+ // 4. 功能面板导航绑定
231
+ Nav.init();
232
+
233
+ // 5. 视图模式初始化(恢复 hash / localStorage,触发懒加载)
234
+ ViewMode.init();
235
+
236
+ // 6. 扩展功能导航(异步,3rdloop web 部署时渲染 tag/issue/prcheck 入口)
237
+ ExtNav.init();
238
+ });