@wonanut/dsh-agent-lab 0.0.0-stage → 0.2.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.
@@ -0,0 +1,576 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ * Agent 实验室 · 80-source.js — 源码浏览器
3
+ *
4
+ * 数据来自构建时注入的全局 SOURCES:
5
+ * [{ id, name, role, code }]
6
+ * 左侧文件列表(名称 + 角色),右侧代码区(行号 / 等宽 / 横向滚动 /
7
+ * 一键复制 / 轻量语法高亮)。高亮只负责“上色”,渲染文本与原始源码
8
+ * 完全一致;复制永远复制原始 code。
9
+ *
10
+ * 组件由 90-page.js 直接调用:
11
+ * function SourcePage(props)
12
+ * 可选 props:
13
+ * sources 覆盖 SOURCES(仅调试/测试用)
14
+ * initialId 初始选中的文件 id
15
+ *
16
+ * 约束:普通 JS,无 import/export/JSX;React / e / hooks 由外层提供。
17
+ * 所有 class 使用 al-src- 前缀;颜色优先使用 --dsw-alias-* token。
18
+ * ───────────────────────────────────────────────────────────── */
19
+
20
+ const SRC_CSS = `
21
+ .al-src-page {
22
+ --al-src-bg: var(--dsw-alias-bg-base, #ffffff);
23
+ --al-src-panel: var(--dsw-alias-bg-layer-1, #ffffff);
24
+ --al-src-panel-2: var(--dsw-alias-bg-layer-2, #f6f7f9);
25
+ --al-src-panel-3: var(--dsw-alias-bg-layer-3, #eceff3);
26
+ --al-src-text: var(--dsw-alias-label-primary, #171a21);
27
+ --al-src-sub: var(--dsw-alias-label-secondary, #5b6472);
28
+ --al-src-ter: var(--dsw-alias-label-tertiary, #8b93a1);
29
+ --al-src-border: var(--dsw-alias-border-l1, rgba(15, 23, 42, 0.1));
30
+ --al-src-border-2: var(--dsw-alias-border-l2, rgba(15, 23, 42, 0.18));
31
+ --al-src-brand: var(--dsw-alias-brand-primary, #4d6bfe);
32
+ --al-src-tint: color-mix(in srgb, var(--al-src-brand) 11%, transparent);
33
+ --al-src-brand-text: color-mix(in srgb, var(--al-src-brand) 76%, var(--al-src-text));
34
+ --al-src-code-bg: var(--al-src-panel-2);
35
+ --al-src-gutter-bg: var(--al-src-panel-3);
36
+ --al-src-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
37
+ --al-src-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
38
+ --al-src-tok-comment: var(--al-src-ter);
39
+ --al-src-tok-string: color-mix(in srgb, #1a7f37 55%, var(--al-src-text));
40
+ --al-src-tok-template: color-mix(in srgb, #116329 48%, var(--al-src-text));
41
+ --al-src-tok-keyword: color-mix(in srgb, #8250df 55%, var(--al-src-text));
42
+ --al-src-tok-number: color-mix(in srgb, #bc4c00 55%, var(--al-src-text));
43
+ --al-src-tok-builtin: color-mix(in srgb, #0969da 48%, var(--al-src-text));
44
+ box-sizing: border-box;
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: 14px;
48
+ width: 100%;
49
+ height: 100%;
50
+ min-height: 0;
51
+ padding: 4px 4px 12px;
52
+ color: var(--al-src-text);
53
+ font-family: var(--al-src-sans);
54
+ font-size: 14px;
55
+ line-height: 1.6;
56
+ -webkit-font-smoothing: antialiased;
57
+ }
58
+ .al-src-page * { box-sizing: border-box; }
59
+
60
+ /* ── 头部 ── */
61
+ .al-src-head { flex: none; padding: 14px 2px 2px; }
62
+ .al-src-eyebrow { font-family: var(--al-src-mono); font-size: 11px; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; color: var(--al-src-ter); }
63
+ .al-src-title { margin: 8px 0 10px; color: var(--al-src-text); font-size: clamp(21px, 2.6vw, 28px); line-height: 1.28; font-weight: 700; letter-spacing: -0.01em; }
64
+ .al-src-lead { margin: 0; max-width: 84ch; color: var(--al-src-sub); font-size: 13.5px; line-height: 1.8; }
65
+ .al-src-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 13px; }
66
+ .al-src-chip { display: inline-flex; align-items: center; gap: 7px; padding: 3px 10px; border: 1px solid var(--al-src-border); border-radius: 999px; background: var(--al-src-panel-2); color: var(--al-src-sub); font-size: 12px; line-height: 1.6; }
67
+ .al-src-chip-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--al-src-brand); opacity: 0.85; }
68
+
69
+ /* ── 主体:文件列表 + 代码区 ── */
70
+ .al-src-work { flex: 1 1 auto; display: grid; grid-template-columns: 268px minmax(0, 1fr); grid-template-rows: minmax(0, 1fr); gap: 12px; min-height: 0; }
71
+ .al-src-side { display: flex; flex-direction: column; min-height: 0; overflow: hidden; border: 1px solid var(--al-src-border); border-radius: 12px; background: var(--al-src-panel); }
72
+ .al-src-side-head { flex: none; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--al-src-border); background: var(--al-src-panel-2); }
73
+ .al-src-side-title { color: var(--al-src-sub); font-size: 11.5px; font-weight: 600; letter-spacing: 0.04em; }
74
+ .al-src-side-count { color: var(--al-src-ter); font-family: var(--al-src-mono); font-size: 10.5px; }
75
+ .al-src-files { flex: 1 1 auto; min-height: 120px; overflow-y: auto; padding: 6px; display: flex; flex-direction: column; gap: 2px; }
76
+ .al-src-file { appearance: none; display: flex; flex-direction: column; gap: 2px; width: 100%; padding: 7px 9px; border: 1px solid transparent; border-radius: 8px; background: transparent; color: var(--al-src-sub); font: inherit; text-align: left; cursor: pointer; }
77
+ .al-src-file:hover { background: var(--al-src-panel-2); }
78
+ .al-src-file.is-active { border-color: color-mix(in srgb, var(--al-src-brand) 26%, transparent); background: var(--al-src-tint); }
79
+ .al-src-file:focus-visible { outline: 2px solid var(--al-src-brand); outline-offset: 1px; }
80
+ .al-src-file-top { display: flex; align-items: center; gap: 7px; }
81
+ .al-src-file-no { flex: none; width: 18px; color: var(--al-src-ter); font-family: var(--al-src-mono); font-size: 10px; }
82
+ .al-src-file-name { color: var(--al-src-text); font-family: var(--al-src-mono); font-size: 12px; font-weight: 600; word-break: break-all; }
83
+ .al-src-file.is-active .al-src-file-name { color: var(--al-src-brand-text); }
84
+ .al-src-file-role { padding-left: 25px; color: var(--al-src-ter); font-size: 11px; line-height: 1.5; }
85
+ .al-src-file-meta { padding-left: 25px; color: var(--al-src-ter); font-family: var(--al-src-mono); font-size: 10px; }
86
+
87
+ .al-src-main { display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow: hidden; border: 1px solid var(--al-src-border); border-radius: 12px; background: var(--al-src-panel); }
88
+ .al-src-toolbar { flex: none; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; padding: 9px 12px; border-bottom: 1px solid var(--al-src-border); background: var(--al-src-panel-2); }
89
+ .al-src-toolbar-name { color: var(--al-src-text); font-family: var(--al-src-mono); font-size: 12px; font-weight: 600; }
90
+ .al-src-toolbar-role { color: var(--al-src-ter); font-size: 11.5px; }
91
+ .al-src-toolbar-spacer { flex: 1 1 auto; }
92
+ .al-src-meta { color: var(--al-src-ter); font-family: var(--al-src-mono); font-size: 10.5px; white-space: nowrap; }
93
+ .al-src-copy { appearance: none; display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px; border: 1px solid var(--al-src-border-2); border-radius: 7px; background: var(--al-src-panel); color: var(--al-src-sub); font: inherit; font-size: 11.5px; cursor: pointer; white-space: nowrap; }
94
+ .al-src-copy:hover { border-color: var(--al-src-brand); color: var(--al-src-text); }
95
+ .al-src-copy:focus-visible { outline: 2px solid var(--al-src-brand); outline-offset: 1px; }
96
+ .al-src-copy.is-done { border-color: color-mix(in srgb, #2da44e 45%, var(--al-src-border)); color: var(--al-src-tok-string); }
97
+ .al-src-copy.is-fail { border-color: color-mix(in srgb, #f85149 45%, var(--al-src-border)); color: color-mix(in srgb, #f85149 70%, var(--al-src-text)); }
98
+ .al-src-copy-glyph { font-family: var(--al-src-mono); font-size: 12px; line-height: 1; }
99
+
100
+ /* ── 代码区 ── */
101
+ .al-src-scroll { flex: 1 1 auto; min-height: 240px; max-height: min(72vh, 900px); overflow: auto; background: var(--al-src-code-bg); }
102
+ .al-src-body { display: flex; align-items: flex-start; min-width: max-content; }
103
+ .al-src-gutter { position: sticky; left: 0; z-index: 2; flex: none; padding: 12px 10px 12px 12px; border-right: 1px solid var(--al-src-border); background: var(--al-src-gutter-bg); user-select: none; }
104
+ .al-src-ln { height: 20px; min-width: 2.8em; color: var(--al-src-ter); font-family: var(--al-src-mono); font-size: 12px; line-height: 20px; text-align: right; font-variant-numeric: tabular-nums; }
105
+ .al-src-pre { flex: none; margin: 0; padding: 12px 18px 12px 14px; color: var(--al-src-text); font-family: var(--al-src-mono); font-size: 12px; line-height: 20px; tab-size: 2; }
106
+ .al-src-line { height: 20px; line-height: 20px; white-space: pre; }
107
+ .al-src-tok-comment { color: var(--al-src-tok-comment); font-style: italic; }
108
+ .al-src-tok-string { color: var(--al-src-tok-string); }
109
+ .al-src-tok-template { color: var(--al-src-tok-template); }
110
+ .al-src-tok-keyword { color: var(--al-src-tok-keyword); font-weight: 600; }
111
+ .al-src-tok-number { color: var(--al-src-tok-number); }
112
+ .al-src-tok-builtin { color: var(--al-src-tok-builtin); }
113
+ .al-src-tok-plain { color: var(--al-src-text); }
114
+
115
+ /* ── 空状态 / 页脚 ── */
116
+ .al-src-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; min-height: 240px; padding: 40px 24px; color: var(--al-src-sub); text-align: center; font-size: 12.5px; line-height: 1.75; }
117
+ .al-src-empty-title { color: var(--al-src-text); font-size: 13.5px; font-weight: 650; }
118
+ .al-src-empty-code { padding: 2px 6px; border: 1px solid var(--al-src-border); border-radius: 5px; background: var(--al-src-panel-2); color: var(--al-src-text); font-family: var(--al-src-mono); font-size: 11.5px; }
119
+ .al-src-hint { flex: none; margin: 0; padding: 0 2px; color: var(--al-src-ter); font-size: 11.5px; line-height: 1.6; }
120
+
121
+ /* ── 响应式 ── */
122
+ @media (max-width: 880px) {
123
+ .al-src-work { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
124
+ .al-src-side { max-height: 200px; }
125
+ .al-src-scroll { max-height: min(64vh, 720px); }
126
+ }
127
+ @media (max-width: 560px) {
128
+ .al-src-title { font-size: 20px; }
129
+ .al-src-page { gap: 10px; }
130
+ }
131
+ @media (prefers-reduced-motion: reduce) {
132
+ .al-src-page * { transition: none !important; }
133
+ }
134
+ `;
135
+
136
+ /* ── 语法高亮:轻量 tokenizer ────────────────────────────────
137
+ * 只识别 注释 / 字符串 / 模板字符串 / 关键字 / 内置对象 / 数字。
138
+ * 关键约束:所有 token 首尾相接覆盖整个源码,渲染出去的文本与原始
139
+ * 源码逐字符一致;跨行块注释与多行模板字符串按 '\n' 切开后逐行上色。
140
+ */
141
+
142
+ const SRC_KEYWORDS = new Set([
143
+ 'const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while',
144
+ 'do', 'switch', 'case', 'break', 'continue', 'new', 'typeof', 'instanceof',
145
+ 'this', 'class', 'extends', 'super', 'async', 'await', 'try', 'catch',
146
+ 'finally', 'throw', 'delete', 'in', 'of', 'void', 'yield', 'static',
147
+ 'import', 'export', 'from', 'default', 'null', 'undefined', 'true', 'false',
148
+ ]);
149
+
150
+ const SRC_BUILTINS = new Set([
151
+ 'React', 'JSON', 'Math', 'Object', 'Array', 'String', 'Number', 'Boolean',
152
+ 'Promise', 'Set', 'Map', 'Symbol', 'Date', 'Error', 'console', 'globalThis',
153
+ 'window', 'document', 'navigator', 'localStorage', 'parseInt', 'parseFloat',
154
+ 'isNaN', 'setTimeout', 'clearTimeout', 'requestAnimationFrame',
155
+ ]);
156
+
157
+ function sourceIsDigit(ch) {
158
+ return ch >= '0' && ch <= '9';
159
+ }
160
+
161
+ function sourceIsIdentStart(ch) {
162
+ return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$';
163
+ }
164
+
165
+ function sourceIsIdentPart(ch) {
166
+ return sourceIsIdentStart(ch) || sourceIsDigit(ch);
167
+ }
168
+
169
+ function sourceTokenize(code) {
170
+ const tokens = [];
171
+ const length = code.length;
172
+ let index = 0;
173
+
174
+ while (index < length) {
175
+ const ch = code[index];
176
+ const next = code[index + 1];
177
+
178
+ // 块注释:可能跨行
179
+ if (ch === '/' && next === '*') {
180
+ const closing = code.indexOf('*/', index + 2);
181
+ const end = closing < 0 ? length : closing + 2;
182
+ tokens.push({ type: 'comment', text: code.slice(index, end) });
183
+ index = end;
184
+ continue;
185
+ }
186
+
187
+ // 行注释
188
+ if (ch === '/' && next === '/') {
189
+ const closing = code.indexOf('\n', index + 2);
190
+ const end = closing < 0 ? length : closing;
191
+ tokens.push({ type: 'comment', text: code.slice(index, end) });
192
+ index = end;
193
+ continue;
194
+ }
195
+
196
+ // 单/双引号字符串(不允许跨行;未闭合时到行尾为止)
197
+ if (ch === '"' || ch === "'") {
198
+ let cursor = index + 1;
199
+ while (cursor < length) {
200
+ const current = code[cursor];
201
+ if (current === '\\') {
202
+ cursor += 2;
203
+ continue;
204
+ }
205
+ if (current === '\n') break;
206
+ if (current === ch) {
207
+ cursor += 1;
208
+ break;
209
+ }
210
+ cursor += 1;
211
+ }
212
+ if (cursor > length) cursor = length;
213
+ tokens.push({ type: 'string', text: code.slice(index, cursor) });
214
+ index = cursor;
215
+ continue;
216
+ }
217
+
218
+ // 模板字符串:允许跨行,跳过 ${...} 内部直到配对,转义字符整体跳过
219
+ if (ch === '`') {
220
+ let cursor = index + 1;
221
+ let depth = 0;
222
+ while (cursor < length) {
223
+ const current = code[cursor];
224
+ if (current === '\\') {
225
+ cursor += 2;
226
+ continue;
227
+ }
228
+ if (current === '`' && depth === 0) {
229
+ cursor += 1;
230
+ break;
231
+ }
232
+ if (current === '$' && code[cursor + 1] === '{') {
233
+ depth += 1;
234
+ cursor += 2;
235
+ continue;
236
+ }
237
+ if (current === '{' && depth > 0) {
238
+ depth += 1;
239
+ cursor += 1;
240
+ continue;
241
+ }
242
+ if (current === '}' && depth > 0) {
243
+ depth -= 1;
244
+ cursor += 1;
245
+ continue;
246
+ }
247
+ cursor += 1;
248
+ }
249
+ if (cursor > length) cursor = length;
250
+ tokens.push({ type: 'template', text: code.slice(index, cursor) });
251
+ index = cursor;
252
+ continue;
253
+ }
254
+
255
+ // 数字:十进制 / 十六进制 / 二进制 / 八进制 / 小数 / 指数
256
+ if (sourceIsDigit(ch)) {
257
+ let cursor = index + 1;
258
+ if (ch === '0' && (code[index + 1] === 'x' || code[index + 1] === 'X')) {
259
+ cursor = index + 2;
260
+ while (cursor < length && /[0-9a-fA-F_]/.test(code[cursor])) cursor += 1;
261
+ } else if (ch === '0' && (code[index + 1] === 'b' || code[index + 1] === 'B')) {
262
+ cursor = index + 2;
263
+ while (cursor < length && (code[cursor] === '0' || code[cursor] === '1' || code[cursor] === '_')) cursor += 1;
264
+ } else if (ch === '0' && (code[index + 1] === 'o' || code[index + 1] === 'O')) {
265
+ cursor = index + 2;
266
+ while (cursor < length && /[0-7_]/.test(code[cursor])) cursor += 1;
267
+ } else {
268
+ while (cursor < length && (sourceIsDigit(code[cursor]) || code[cursor] === '_')) cursor += 1;
269
+ if (code[cursor] === '.') {
270
+ cursor += 1;
271
+ while (cursor < length && (sourceIsDigit(code[cursor]) || code[cursor] === '_')) cursor += 1;
272
+ }
273
+ if (code[cursor] === 'e' || code[cursor] === 'E') {
274
+ cursor += 1;
275
+ if (code[cursor] === '+' || code[cursor] === '-') cursor += 1;
276
+ while (cursor < length && sourceIsDigit(code[cursor])) cursor += 1;
277
+ }
278
+ }
279
+ tokens.push({ type: 'number', text: code.slice(index, cursor) });
280
+ index = cursor;
281
+ continue;
282
+ }
283
+
284
+ // 标识符 / 关键字
285
+ if (sourceIsIdentStart(ch)) {
286
+ let cursor = index + 1;
287
+ while (cursor < length && sourceIsIdentPart(code[cursor])) cursor += 1;
288
+ const word = code.slice(index, cursor);
289
+ const type = SRC_KEYWORDS.has(word) ? 'keyword' : (SRC_BUILTINS.has(word) ? 'builtin' : 'plain');
290
+ tokens.push({ type: type, text: word });
291
+ index = cursor;
292
+ continue;
293
+ }
294
+
295
+ // 其余字符:成段累积,避免产生太多单字符 span
296
+ let cursor = index + 1;
297
+ while (cursor < length) {
298
+ const current = code[cursor];
299
+ if (current === '/' && (code[cursor + 1] === '*' || code[cursor + 1] === '/')) break;
300
+ if (current === '"' || current === "'" || current === '`') break;
301
+ if (sourceIsDigit(current) || sourceIsIdentStart(current)) break;
302
+ cursor += 1;
303
+ }
304
+ tokens.push({ type: 'plain', text: code.slice(index, cursor) });
305
+ index = cursor;
306
+ }
307
+
308
+ return tokens;
309
+ }
310
+
311
+ /** 把 token 流按换行切成“行 → token 片段”,供逐行渲染。 */
312
+ function sourceHighlightLines(code) {
313
+ const tokens = sourceTokenize(code);
314
+ const lines = [[]];
315
+
316
+ for (let t = 0; t < tokens.length; t += 1) {
317
+ const token = tokens[t];
318
+ const parts = token.text.split('\n');
319
+ for (let p = 0; p < parts.length; p += 1) {
320
+ if (p > 0) lines.push([]);
321
+ if (parts[p].length > 0) {
322
+ lines[lines.length - 1].push({ type: token.type, text: parts[p] });
323
+ }
324
+ }
325
+ }
326
+
327
+ // 源码以换行结尾时,最后会多出一个空行:展示层去掉它,行号更自然
328
+ if (lines.length > 1 && lines[lines.length - 1].length === 0) {
329
+ lines.pop();
330
+ }
331
+
332
+ return lines;
333
+ }
334
+
335
+ /* ── 小工具 ─────────────────────────────────────────────── */
336
+
337
+ function sourceFormatBytes(size) {
338
+ const bytes = Number(size) || 0;
339
+ if (bytes < 1024) return bytes + ' B';
340
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
341
+ return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
342
+ }
343
+
344
+ function sourceLineCount(text) {
345
+ const value = String(text || '');
346
+ if (value.length === 0) return 1;
347
+ return value.split('\n').length;
348
+ }
349
+
350
+ function sourcePad2(value) {
351
+ return value < 10 ? '0' + value : String(value);
352
+ }
353
+
354
+ function sourceChip(props) {
355
+ return e('span', { className: 'al-src-chip' },
356
+ e('span', { className: 'al-src-chip-dot' }),
357
+ props.children
358
+ );
359
+ }
360
+
361
+ /* ── 文件列表 ───────────────────────────────────────────── */
362
+
363
+ function SourceFileList(props) {
364
+ const list = props.list || [];
365
+ const activeIndex = props.activeIndex;
366
+ const onSelect = props.onSelect;
367
+
368
+ if (list.length === 0) {
369
+ return e('aside', { className: 'al-src-side' },
370
+ e('div', { className: 'al-src-side-head' },
371
+ e('span', { className: 'al-src-side-title' }, '源文件'),
372
+ e('span', { className: 'al-src-side-count' }, '0 个')
373
+ ),
374
+ e('div', { className: 'al-src-files' })
375
+ );
376
+ }
377
+
378
+ return e('aside', { className: 'al-src-side' },
379
+ e('div', { className: 'al-src-side-head' },
380
+ e('span', { className: 'al-src-side-title' }, '源文件'),
381
+ e('span', { className: 'al-src-side-count' }, list.length + ' 个 · 构建快照')
382
+ ),
383
+ e('div', { className: 'al-src-files' }, list.map(function (item, index) {
384
+ const active = index === activeIndex;
385
+ const name = (item && item.name) || (item && item.id) || 'unknown.js';
386
+ const code = (item && item.code) || '';
387
+ return e('button', {
388
+ key: (item && item.id) || index,
389
+ type: 'button',
390
+ className: 'al-src-file' + (active ? ' is-active' : ''),
391
+ 'aria-current': active ? 'true' : undefined,
392
+ title: (item && item.role) || name,
393
+ onClick: function () { onSelect(index); },
394
+ },
395
+ e('span', { className: 'al-src-file-top' },
396
+ e('span', { className: 'al-src-file-no' }, sourcePad2(index + 1)),
397
+ e('span', { className: 'al-src-file-name' }, name)
398
+ ),
399
+ item && item.role ? e('span', { className: 'al-src-file-role' }, item.role) : null,
400
+ e('span', { className: 'al-src-file-meta' },
401
+ sourceLineCount(code) + ' 行 · ' + sourceFormatBytes(code.length)
402
+ )
403
+ );
404
+ }))
405
+ );
406
+ }
407
+
408
+ /* ── 代码区 ─────────────────────────────────────────────── */
409
+
410
+ function SourceCodeBody(props) {
411
+ const lines = props.lines || [];
412
+
413
+ return e('div', { className: 'al-src-body' },
414
+ e('div', { className: 'al-src-gutter', 'aria-hidden': 'true' },
415
+ lines.map(function (_, index) {
416
+ return e('div', { key: index, className: 'al-src-ln' }, String(index + 1));
417
+ })
418
+ ),
419
+ e('div', { className: 'al-src-pre' },
420
+ lines.map(function (tokens, lineIndex) {
421
+ const children = tokens.length === 0
422
+ ? '\u00a0'
423
+ : tokens.map(function (token, tokenIndex) {
424
+ return e('span', {
425
+ key: tokenIndex,
426
+ className: 'al-src-tok al-src-tok-' + token.type,
427
+ }, token.text);
428
+ });
429
+ return e('div', { key: lineIndex, className: 'al-src-line' }, children);
430
+ })
431
+ )
432
+ );
433
+ }
434
+
435
+ /* ── 页面组件 ───────────────────────────────────────────── */
436
+
437
+ function SourcePage(props) {
438
+ const options = props || {};
439
+ const injected = typeof SOURCES !== 'undefined' && Array.isArray(SOURCES) ? SOURCES : [];
440
+ const list = Array.isArray(options.sources) ? options.sources : injected;
441
+ const hasSources = list.length > 0;
442
+
443
+ const [activeIndex, setActiveIndex] = useState(function () {
444
+ const preferred = options.initialId;
445
+ if (preferred != null) {
446
+ for (let i = 0; i < list.length; i += 1) {
447
+ if (list[i] && list[i].id === preferred) return i;
448
+ }
449
+ }
450
+ return 0;
451
+ });
452
+
453
+ const [copyState, setCopyState] = useState('');
454
+ const copyTimer = useRef(null);
455
+
456
+ useEffect(function () {
457
+ return function () {
458
+ if (copyTimer.current) {
459
+ clearTimeout(copyTimer.current);
460
+ copyTimer.current = null;
461
+ }
462
+ };
463
+ }, []);
464
+
465
+ const active = list[activeIndex] || list[0] || null;
466
+ const code = active ? String(active.code || '') : '';
467
+ const lineCount = code.length === 0 ? 0 : sourceLineCount(code);
468
+
469
+ const lines = useMemo(function () {
470
+ return code.length === 0 ? [] : sourceHighlightLines(code);
471
+ }, [code]);
472
+
473
+ const totals = useMemo(function () {
474
+ let bytes = 0;
475
+ let linesCount = 0;
476
+ list.forEach(function (item) {
477
+ const text = String((item && item.code) || '');
478
+ bytes += text.length;
479
+ linesCount += sourceLineCount(text);
480
+ });
481
+ return { files: list.length, bytes: bytes, lines: linesCount };
482
+ }, [list]);
483
+
484
+ const totalBytes = code.length;
485
+
486
+ const copyCode = useCallback(function () {
487
+ if (!code) return;
488
+ const write = function () {
489
+ try {
490
+ const nav = typeof globalThis !== 'undefined' ? globalThis.navigator : null;
491
+ if (nav && nav.clipboard && typeof nav.clipboard.writeText === 'function') {
492
+ return nav.clipboard.writeText(code).then(function () { return true; }).catch(function () { return false; });
493
+ }
494
+ } catch (error) {
495
+ /* 剪贴板不可用:走失败态提示,绝不抛错影响页面 */
496
+ }
497
+ return Promise.resolve(false);
498
+ };
499
+ write().then(function (ok) {
500
+ setCopyState(ok ? 'done' : 'fail');
501
+ if (copyTimer.current) clearTimeout(copyTimer.current);
502
+ copyTimer.current = setTimeout(function () { setCopyState(''); }, 1800);
503
+ });
504
+ }, [code]);
505
+
506
+ function handleSelect(index) {
507
+ setActiveIndex(index);
508
+ setCopyState('');
509
+ }
510
+
511
+ return e('div', { className: 'al-src-page' },
512
+ e('style', { dangerouslySetInnerHTML: { __html: SRC_CSS } }),
513
+
514
+ e('header', { className: 'al-src-head' },
515
+ e('div', { className: 'al-src-eyebrow' }, 'AGENT LAB · 源码'),
516
+ e('h1', { className: 'al-src-title' }, '这就是当前页面正在运行的代码'),
517
+ e('p', { className: 'al-src-lead' },
518
+ '构建时 build.mjs 把 src/*.js 的内容作为 JSON 字符串注入 SOURCES;DSH 加载的 client.js、你在「体验」和「原理」页看到的界面,全部由下面这些文件生成。左边选文件,右边读代码 —— 没有隐藏,也没有压缩混淆。'
519
+ ),
520
+ e('div', { className: 'al-src-chips' },
521
+ sourceChip({ children: (hasSources ? totals.files : 0) + ' 个源文件' }),
522
+ sourceChip({ children: sourceFormatBytes(hasSources ? totals.bytes : 0) }),
523
+ sourceChip({ children: '纯 JavaScript · 无外部依赖' }),
524
+ sourceChip({ children: '构建快照 · 与 client.js 一致' })
525
+ )
526
+ ),
527
+
528
+ e('div', { className: 'al-src-work' },
529
+ e(SourceFileList, {
530
+ list: list,
531
+ activeIndex: list.indexOf(active),
532
+ onSelect: handleSelect,
533
+ }),
534
+
535
+ e('section', { className: 'al-src-main' },
536
+ e('div', { className: 'al-src-toolbar' },
537
+ e('span', { className: 'al-src-toolbar-name' }, active ? ((active.name || active.id) || 'unknown.js') : '无文件'),
538
+ active && active.role ? e('span', { className: 'al-src-toolbar-role' }, active.role) : null,
539
+ e('span', { className: 'al-src-toolbar-spacer' }),
540
+ active ? e('span', { className: 'al-src-meta' },
541
+ lineCount + ' 行 · ' + sourceFormatBytes(totalBytes)
542
+ ) : null,
543
+ active ? e('button', {
544
+ type: 'button',
545
+ className: 'al-src-copy' + (copyState === 'done' ? ' is-done' : copyState === 'fail' ? ' is-fail' : ''),
546
+ title: '复制当前文件的全部源码',
547
+ onClick: copyCode,
548
+ },
549
+ e('span', { className: 'al-src-copy-glyph' }, copyState === 'done' ? '✓' : copyState === 'fail' ? '!' : '⧉'),
550
+ e('span', null, copyState === 'done' ? '已复制' : copyState === 'fail' ? '复制失败' : '复制源码')
551
+ ) : null
552
+ ),
553
+
554
+ e('div', { className: 'al-src-scroll' },
555
+ active
556
+ ? e(SourceCodeBody, { lines: lines })
557
+ : e('div', { className: 'al-src-empty' },
558
+ e('span', { className: 'al-src-empty-title' }, '没有检测到内嵌源码'),
559
+ e('span', null,
560
+ '全局 SOURCES 没有被注入或为空。这通常意味着直接打开了 src 里的源文件,而不是构建后的 client.js。'
561
+ ),
562
+ e('span', null,
563
+ '运行 ',
564
+ e('code', { className: 'al-src-empty-code' }, 'node build.mjs'),
565
+ ' 重新构建,然后刷新页面即可。'
566
+ )
567
+ )
568
+ )
569
+ )
570
+ ),
571
+
572
+ e('p', { className: 'al-src-hint' },
573
+ 'SOURCES 是构建时快照:修改 src 里的文件后需要重新运行 node build.mjs,本页才会显示新代码。语法高亮是插件内自写的轻量 tokenizer(注释 / 字符串 / 模板字符串 / 关键字 / 数字),复制按钮复制的始终是原始源码。'
574
+ )
575
+ );
576
+ }