|
@@ -47,6 +47,64 @@
|
|
|
|
|
|
|
|
function ymdToDash(s) { return /^\d{8}$/.test(String(s)) ? `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}` : String(s || ''); }
|
|
function ymdToDash(s) { return /^\d{8}$/.test(String(s)) ? `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}` : String(s || ''); }
|
|
|
|
|
|
|
|
|
|
+ /** 极简安全 Markdown:先整体 HTML 转义,再做受限转换(标题/列表/表格/代码块/引用/加粗/链接)。
|
|
|
|
|
+ * 不引入外部依赖;链接仅放行 http(s)。 */
|
|
|
|
|
+ function renderMarkdown(src) {
|
|
|
|
|
+ const lines = String(src || '').replace(/\r\n?/g, '\n').split('\n');
|
|
|
|
|
+ const inline = (s) => esc(s)
|
|
|
|
|
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
|
|
|
+ .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
|
|
|
+ .replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
|
|
|
|
|
+ const out = [];
|
|
|
|
|
+ let list = null;
|
|
|
|
|
+ let para = [];
|
|
|
|
|
+ const closeList = () => { if (list) { out.push(`</${list}>`); list = null; } };
|
|
|
|
|
+ const closePara = () => { if (para.length) { out.push(`<p>${para.map(inline).join('<br>')}</p>`); para = []; } };
|
|
|
|
|
+ let i = 0;
|
|
|
|
|
+ while (i < lines.length) {
|
|
|
|
|
+ const line = lines[i];
|
|
|
|
|
+ if (/^```/.test(line)) {
|
|
|
|
|
+ closePara(); closeList();
|
|
|
|
|
+ const buf = [];
|
|
|
|
|
+ i += 1;
|
|
|
|
|
+ while (i < lines.length && !/^```\s*$/.test(lines[i])) { buf.push(lines[i]); i += 1; }
|
|
|
|
|
+ i += 1;
|
|
|
|
|
+ out.push(`<pre class="md-code">${esc(buf.join('\n'))}</pre>`);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (/^\s*\|.*\|\s*$/.test(line) && i + 1 < lines.length && /^\s*\|[\s:|-]+\|\s*$/.test(lines[i + 1])) {
|
|
|
|
|
+ closePara(); closeList();
|
|
|
|
|
+ const cells = (l) => l.trim().replace(/^\||\|$/g, '').split('|').map((c) => c.trim());
|
|
|
|
|
+ const head = cells(line);
|
|
|
|
|
+ i += 2;
|
|
|
|
|
+ const rows = [];
|
|
|
|
|
+ while (i < lines.length && /^\s*\|.*\|\s*$/.test(lines[i])) { rows.push(cells(lines[i])); i += 1; }
|
|
|
|
|
+ out.push(`<table class="md-table"><thead><tr>${head.map((h) => `<th>${inline(h)}</th>`).join('')}</tr></thead><tbody>${rows.map((r) => `<tr>${r.map((c) => `<td>${inline(c)}</td>`).join('')}</tr>`).join('')}</tbody></table>`);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ const h = line.match(/^(#{1,4})\s+(.*)$/);
|
|
|
|
|
+ if (h) { closePara(); closeList(); const lv = h[1].length + 2; out.push(`<h${lv}>${inline(h[2])}</h${lv}>`); i += 1; continue; }
|
|
|
|
|
+ if (/^\s*-{3,}\s*$/.test(line)) { closePara(); closeList(); out.push('<hr>'); i += 1; continue; }
|
|
|
|
|
+ const q = line.match(/^\s*>\s?(.*)$/);
|
|
|
|
|
+ if (q) { closePara(); closeList(); out.push(`<blockquote>${inline(q[1])}</blockquote>`); i += 1; continue; }
|
|
|
|
|
+ const ul = line.match(/^\s*[-*]\s+(.*)$/);
|
|
|
|
|
+ const ol = line.match(/^\s*\d+[.、]\s+(.*)$/);
|
|
|
|
|
+ if (ul || ol) {
|
|
|
|
|
+ closePara();
|
|
|
|
|
+ const want = ul ? 'ul' : 'ol';
|
|
|
|
|
+ if (list !== want) { closeList(); out.push(`<${want} class="md-list">`); list = want; }
|
|
|
|
|
+ out.push(`<li>${inline((ul || ol)[1])}</li>`);
|
|
|
|
|
+ i += 1;
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!line.trim()) { closePara(); closeList(); i += 1; continue; }
|
|
|
|
|
+ para.push(line);
|
|
|
|
|
+ i += 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ closePara(); closeList();
|
|
|
|
|
+ return out.join('');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
function fmtDuration(ms) { return ms == null || ms === '' ? '' : (ms >= 1000 ? (ms / 1000).toFixed(1) + 's' : `${ms}ms`); }
|
|
function fmtDuration(ms) { return ms == null || ms === '' ? '' : (ms >= 1000 ? (ms / 1000).toFixed(1) + 's' : `${ms}ms`); }
|
|
|
function fmtBytes(n) { return n == null ? '' : (n >= 1024 ? (n / 1024).toFixed(1) + ' KB' : `${n} B`); }
|
|
function fmtBytes(n) { return n == null ? '' : (n >= 1024 ? (n / 1024).toFixed(1) + ' KB' : `${n} B`); }
|
|
|
function fmtTokens(n) { return n == null ? '—' : (n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n)); }
|
|
function fmtTokens(n) { return n == null ? '—' : (n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n)); }
|
|
@@ -412,7 +470,7 @@
|
|
|
if (step.tokensIn != null) bits.push(`${fmtTokens(step.tokensIn)} in / ${fmtTokens(step.tokensOut)} out`);
|
|
if (step.tokensIn != null) bits.push(`${fmtTokens(step.tokensIn)} in / ${fmtTokens(step.tokensOut)} out`);
|
|
|
steps.push(`<div class="ask-step">
|
|
steps.push(`<div class="ask-step">
|
|
|
<div class="ask-step-head"><span class="ask-dot done"></span><b>思考${step.turn ? ` · Turn ${step.turn}` : ''}</b></div>
|
|
<div class="ask-step-head"><span class="ask-dot done"></span><b>思考${step.turn ? ` · Turn ${step.turn}` : ''}</b></div>
|
|
|
- <div class="ask-step-meta">${bits.filter(Boolean).join(' · ')}</div>
|
|
|
|
|
|
|
+ <div class="ask-step-meta">${bits.filter(Boolean).map((b) => `<span class="ask-chip">${esc(String(b))}</span>`).join('')}</div>
|
|
|
${step.text ? `<div class="ask-step-body">${esc(step.text)}</div>` : ''}
|
|
${step.text ? `<div class="ask-step-body">${esc(step.text)}</div>` : ''}
|
|
|
</div>`);
|
|
</div>`);
|
|
|
} else if (step.type === 'tool') {
|
|
} else if (step.type === 'tool') {
|
|
@@ -421,7 +479,7 @@
|
|
|
if (step.rows != null) bits.push(`${step.rows} 行`);
|
|
if (step.rows != null) bits.push(`${step.rows} 行`);
|
|
|
steps.push(`<div class="ask-step">
|
|
steps.push(`<div class="ask-step">
|
|
|
<div class="ask-step-head"><span class="ask-dot ${step.ok ? 'done' : 'failed'}"></span><b>${esc(label)}</b></div>
|
|
<div class="ask-step-head"><span class="ask-dot ${step.ok ? 'done' : 'failed'}"></span><b>${esc(label)}</b></div>
|
|
|
- <div class="ask-step-meta">${bits.filter(Boolean).join(' · ')}</div>
|
|
|
|
|
|
|
+ <div class="ask-step-meta">${bits.filter(Boolean).map((b) => `<span class="ask-chip">${esc(String(b))}</span>`).join('')}</div>
|
|
|
<details class="ask-tool"><summary>参数与结果</summary>
|
|
<details class="ask-tool"><summary>参数与结果</summary>
|
|
|
<pre class="ask-pre">参数:${esc(JSON.stringify(step.args || {}))}
|
|
<pre class="ask-pre">参数:${esc(JSON.stringify(step.args || {}))}
|
|
|
|
|
|
|
@@ -457,7 +515,7 @@ ${esc(step.summary || step.error || '')}</pre>
|
|
|
|
|
|
|
|
const result = rec.result || null;
|
|
const result = rec.result || null;
|
|
|
if (result && result.answer) {
|
|
if (result && result.answer) {
|
|
|
- parts.push(`<div class="ask-answer">${esc(result.answer)}</div>`);
|
|
|
|
|
|
|
+ parts.push(`<div class="ask-answer">${renderMarkdown(result.answer)}</div>`);
|
|
|
const chart = result.chart;
|
|
const chart = result.chart;
|
|
|
if (chart && chart.type && chart.type !== 'none') {
|
|
if (chart && chart.type && chart.type !== 'none') {
|
|
|
parts.push(`<div class="ask-chart" data-run="${esc(rec.runId)}"></div>`);
|
|
parts.push(`<div class="ask-chart" data-run="${esc(rec.runId)}"></div>`);
|