mingdao-harness 0.2.6 → 0.2.8

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,201 @@
1
+ // 纯前端工具(零状态,仅依赖 DOM/标准 API):选择器、转义、Markdown 渲染、格式化。
2
+ // 从 app.js 拆分(v0.2.8 C2):这些函数被消息渲染/轨迹/设置等多处复用,独立成模块便于维护与测试。
3
+
4
+ export const $ = (s) => document.querySelector(s);
5
+
6
+ export function esc(s) {
7
+ return String(s ?? '')
8
+ .replace(/&/g, '&')
9
+ .replace(/</g, '&lt;')
10
+ .replace(/>/g, '&gt;')
11
+ .replace(/"/g, '&quot;')
12
+ .replace(/'/g, '&#39;');
13
+ }
14
+
15
+ export function highlight(code, lang) {
16
+ const kws = new Set(
17
+ 'const let var function return if else for while import from export class new await async try catch throw null undefined true false def elif raise with as lambda None True False self echo exit if then else fi done local function'.split(
18
+ ' '
19
+ )
20
+ );
21
+ const re = /(\/\/[^\n]*|#[^\n]*|"(?:\\.|[^"\\])*"|'[^']*'|`[^`]*`|\b\d[\d._]*\b|[A-Za-z_$][\w$]*)/g;
22
+ return esc(code).replace(re, (tok) => {
23
+ if (tok.startsWith('//') || tok.startsWith('#')) return '<span class="hl-c">' + tok + '</span>';
24
+ if (/^["'`]/.test(tok)) return '<span class="hl-s">' + tok + '</span>';
25
+ if (/^\d/.test(tok)) return '<span class="hl-n">' + tok + '</span>';
26
+ if (kws.has(tok)) return '<span class="hl-kw">' + tok + '</span>';
27
+ return tok;
28
+ });
29
+ }
30
+
31
+ export function renderMarkdown(text) {
32
+ // 块级解析(美化:加粗标题 / 段落 / 真列表 / 引用 / 分割线,参照 DeepSeek-Harness 会话排版)
33
+ const lines = String(text).split('\n');
34
+ let out = '';
35
+ let code = null;
36
+ let codeLang = '';
37
+ const flushCode = () => {
38
+ if (code !== null) {
39
+ out +=
40
+ '<div class="codeblock"><div class="cb-banner"><span class="cb-lang"><span class="cb-dot"></span>' +
41
+ (esc(codeLang) || 'code') +
42
+ '</span><span class="cb-copy" style="cursor:pointer">复制</span></div><pre><code class="lang-' +
43
+ esc(codeLang) +
44
+ '">' +
45
+ highlight(code.join('\n'), codeLang) +
46
+ '</code></pre></div>';
47
+ code = null;
48
+ }
49
+ };
50
+ let para = [];
51
+ let list = null; // {type:'ul'|'ol', items:[]}
52
+ const flushPara = () => {
53
+ if (para.length) {
54
+ out += '<p>' + para.join('<br>') + '</p>';
55
+ para = [];
56
+ }
57
+ };
58
+ const flushList = () => {
59
+ if (list) {
60
+ out += '<' + list.type + '>' + list.items.map((i) => '<li>' + i + '</li>').join('') + '</' + list.type + '>';
61
+ list = null;
62
+ }
63
+ };
64
+ // 行内元素(审计 P1-3:内容与 codeLang 均经 esc 转义防 XSS)
65
+ const inline = (l) => {
66
+ let s = esc(l);
67
+ s = s
68
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
69
+ .replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>')
70
+ .replace(/\*([^*\s][^*]*)\*/g, '<i>$1</i>')
71
+ .replace(/\[([^\]]+)\]\((https?:\/\/[^\s'"<>)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
72
+ return s;
73
+ };
74
+ for (const line of lines) {
75
+ const t = line.trim();
76
+ if (t.startsWith('```')) {
77
+ if (code === null) {
78
+ flushList();
79
+ flushPara();
80
+ code = [];
81
+ codeLang = t.slice(3).trim().split(/\s+/)[0] || '';
82
+ } else flushCode();
83
+ continue;
84
+ }
85
+ if (code !== null) {
86
+ code.push(line);
87
+ continue;
88
+ }
89
+ if (t === '') {
90
+ flushList();
91
+ flushPara();
92
+ continue;
93
+ }
94
+ const h = t.match(/^(#{1,6})\s+(.*)$/);
95
+ if (h) {
96
+ flushList();
97
+ flushPara();
98
+ const level = Math.min(h[1].length, 4);
99
+ out += '<h' + level + '>' + inline(h[2]) + '</h' + level + '>';
100
+ continue;
101
+ }
102
+ const ul = t.match(/^([-*+])\s+(.*)$/);
103
+ if (ul) {
104
+ flushPara();
105
+ if (!list || list.type !== 'ul') {
106
+ flushList();
107
+ list = { type: 'ul', items: [] };
108
+ }
109
+ list.items.push(inline(ul[2]));
110
+ continue;
111
+ }
112
+ const ol = t.match(/^(\d+)[.)]\s+(.*)$/);
113
+ if (ol) {
114
+ flushPara();
115
+ if (!list || list.type !== 'ol') {
116
+ flushList();
117
+ list = { type: 'ol', items: [] };
118
+ }
119
+ list.items.push(inline(ol[2]));
120
+ continue;
121
+ }
122
+ if (/^>\s?/.test(t)) {
123
+ flushList();
124
+ flushPara();
125
+ out += '<blockquote>' + inline(t.replace(/^>\s?/, '')) + '</blockquote>';
126
+ continue;
127
+ }
128
+ if (/^(-{3,}|\*{3,})$/.test(t)) {
129
+ flushList();
130
+ flushPara();
131
+ out += '<hr>';
132
+ continue;
133
+ }
134
+ flushList();
135
+ para.push(inline(t));
136
+ }
137
+ flushCode();
138
+ flushList();
139
+ flushPara();
140
+ return out;
141
+ }
142
+
143
+ export function scrollBottom() {
144
+ const sc = document.querySelector('main');
145
+ if (sc) sc.scrollTop = sc.scrollHeight;
146
+ }
147
+
148
+ export function expandableBody(previewHtml, fullHtml) {
149
+ const b = document.createElement('div');
150
+ b.className = 'body';
151
+ const prev = document.createElement('div');
152
+ const full = document.createElement('div');
153
+ prev.innerHTML = previewHtml;
154
+ full.innerHTML = fullHtml;
155
+ full.style.display = 'none';
156
+ const btn = document.createElement('button');
157
+ btn.textContent = '展开全文';
158
+ btn.style.cssText = 'padding:1px 8px;font-size:11px;margin-top:4px';
159
+ btn.onclick = () => {
160
+ const open = full.style.display !== 'none';
161
+ full.style.display = open ? 'none' : 'block';
162
+ btn.textContent = open ? '展开全文' : '收起';
163
+ scrollBottom();
164
+ };
165
+ b.appendChild(prev);
166
+ b.appendChild(full);
167
+ b.appendChild(btn);
168
+ return b;
169
+ }
170
+
171
+ export function resultText(r) {
172
+ if (r == null || r === undefined) return '';
173
+ if (typeof r === 'string') return r;
174
+ if (r.output) return String(r.output);
175
+ if (r.stdout || r.stderr) return String(r.stdout || '') + String(r.stderr || '');
176
+ return JSON.stringify(r);
177
+ }
178
+
179
+ export function truncText(t, n) {
180
+ t = String(t || '');
181
+ return t.length > n ? t.slice(0, n) + '\n…(截断,共 ' + t.length + ' 字)' : t;
182
+ }
183
+
184
+ export function fmtDur(ms) {
185
+ if (!ms) return '0s';
186
+ const sec = Math.round(ms / 1000);
187
+ const m = Math.floor(sec / 60);
188
+ return m > 0 ? m + 'm' + String(sec % 60).padStart(2, '0') + 's' : sec + 's';
189
+ }
190
+
191
+ export function fmtTok(n) {
192
+ if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
193
+ if (n >= 1e6) return (n / 1e6).toFixed(0) + 'M';
194
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
195
+ return String(n);
196
+ }
197
+
198
+ export function fmtT(n) {
199
+ n = Number(n || 0);
200
+ return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n);
201
+ }