fix(console): 评审/归档卡片的 markdown 渲染成 HTML
内联评审闸卡片与归档详情此前把 agent 产出的 markdown(plan/spec/ operations)直接塞进 <pre> 当纯文本,满屏裸 ## / - / 反引号未渲染。 改为在 ui_kit 层用 window.MaestroRichDoc(marked + DOMPurify 净化) 渲染,与 TaskTree 既有写法一致;保留 280px 限高滚动与容器样式。 - GateSection.jsx: GateCard doc 改为 undefined,children 内对非 attention 闸用 MaestroRichDoc 渲染 a.doc;删除未引用的 reportBox 死代码 - ArchiveSection.jsx: doc() 助手由 <pre> 改为 MaestroRichDoc 容器, SPEC/OPERATIONS 两块随之正确渲染 - 安全:两处均走 RichDoc 的 DOMPurify 净化,挡住 same-origin 存储型 XSS; 不绕过 sanitize 直接注入 HTML。CDN 不可用时回退纯文本不崩 - 不改 GateCard.jsx(避免底层组件反向依赖 ui_kit 全局 + 触发 bundle 重编) - DiffBody 的 <pre> 渲染 git diff(非 markdown)保持不动 类似但未改:console_mobile/MobileViews.jsx 为独立非在线 demo,其 index.html 未加载 RichDoc/marked/DOMPurify,修复需整套引入,超出本次范围。 tsk_sp0DwZ2Kc319 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -126,7 +126,7 @@ function ArchiveModal({ item, t, onClose }) {
|
|||||||
const d = item.detail || {};
|
const d = item.detail || {};
|
||||||
const label = (text) => <div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', margin: '14px 0 4px' }}>{text}</div>;
|
const label = (text) => <div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', margin: '14px 0 4px' }}>{text}</div>;
|
||||||
const doc = (text) => (
|
const doc = (text) => (
|
||||||
<pre style={{ background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderLeft: '2px solid var(--green-dim)', borderRadius: 4, padding: '10px 12px', fontFamily: 'var(--mono)', fontSize: 12.5, lineHeight: 1.7, whiteSpace: 'pre-wrap', wordBreak: 'break-word', margin: 0, color: 'var(--ink)' }}>{text}</pre>
|
<div style={{ background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderLeft: '2px solid var(--green-dim)', borderRadius: 4, padding: '10px 12px' }}><window.MaestroRichDoc md={text} /></div>
|
||||||
);
|
);
|
||||||
const kvGrid = { fontSize: 12, display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '3px 12px', margin: 0 };
|
const kvGrid = { fontSize: 12, display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '3px 12px', margin: 0 };
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -181,7 +181,6 @@ function GatePreview({ item, t, onDecide, onClose }) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
const secLabel = (txt) => <div style={{ fontSize: 10.5, fontWeight: 700, color: 'var(--muted)', letterSpacing: '.18em', margin: '18px 0 8px' }}>{txt}</div>;
|
const secLabel = (txt) => <div style={{ fontSize: 10.5, fontWeight: 700, color: 'var(--muted)', letterSpacing: '.18em', margin: '18px 0 8px' }}>{txt}</div>;
|
||||||
const reportBox = (txt) => <div style={{ background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderRadius: 4, padding: '12px 16px', fontSize: 12.5, lineHeight: 1.7, whiteSpace: 'pre-wrap', wordBreak: 'break-word', color: 'var(--ink)' }}>{txt}</div>;
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'fixed', inset: 0, zIndex: 1100, display: 'grid', placeItems: 'center' }}>
|
<div style={{ position: 'fixed', inset: 0, zIndex: 1100, display: 'grid', placeItems: 'center' }}>
|
||||||
<div onClick={onClose} style={{ position: 'absolute', inset: 0, background: 'rgba(4,6,5,.86)', backdropFilter: 'blur(3px)' }}></div>
|
<div onClick={onClose} style={{ position: 'absolute', inset: 0, background: 'rgba(4,6,5,.86)', backdropFilter: 'blur(3px)' }}></div>
|
||||||
@@ -291,7 +290,7 @@ function GateSection({ approvals, onDecide, taskOps, onTakeover, t }) {
|
|||||||
const isFolded = folded.has(a.id);
|
const isFolded = folded.has(a.id);
|
||||||
return (
|
return (
|
||||||
<div key={a.id} style={{ marginBottom: 12 }}>
|
<div key={a.id} style={{ marginBottom: 12 }}>
|
||||||
<GateCard gate={a.gate} kindLabel={a.gate === 'attention' ? t.gateNeedsHuman : t.gates[a.gate]} title={a.title} meta={a.meta} docLabel={a.docLabel} doc={a.gate === 'attention' ? undefined : a.doc}
|
<GateCard gate={a.gate} kindLabel={a.gate === 'attention' ? t.gateNeedsHuman : t.gates[a.gate]} title={a.title} meta={a.meta} docLabel={a.docLabel} doc={undefined}
|
||||||
collapsed={isFolded} foldable onToggleFold={() => toggleFold(a.id)}
|
collapsed={isFolded} foldable onToggleFold={() => toggleFold(a.id)}
|
||||||
foldTitle={isFolded ? t.gateExpand : t.gateCollapse}
|
foldTitle={isFolded ? t.gateExpand : t.gateCollapse}
|
||||||
onHeaderDoubleClick={a.gate === 'attention' ? undefined : () => setPreview(a)} headerTitle={a.gate === 'attention' ? null : t.gateDblTip}
|
onHeaderDoubleClick={a.gate === 'attention' ? undefined : () => setPreview(a)} headerTitle={a.gate === 'attention' ? null : t.gateDblTip}
|
||||||
@@ -320,15 +319,24 @@ function GateSection({ approvals, onDecide, taskOps, onTakeover, t }) {
|
|||||||
}>
|
}>
|
||||||
{a.gate === 'attention' ? (
|
{a.gate === 'attention' ? (
|
||||||
<ReviewReports taskId={a.taskId} fallback={a.doc} t={t} />
|
<ReviewReports taskId={a.taskId} fallback={a.doc} t={t} />
|
||||||
) : rejecting === a.id ? (
|
) : (
|
||||||
<div style={{ display: 'flex', gap: 10, alignItems: 'flex-start', marginTop: 10 }}>
|
<React.Fragment>
|
||||||
<Textarea danger placeholder={t.rejectPlaceholder} minHeight={56} value={reason} onChange={setReason} />
|
{a.doc ? (
|
||||||
<Button variant="reject" disabled={!reason.trim()}
|
<div style={{ background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderLeft: '2px solid var(--green-dim)', borderRadius: 'var(--radius-sm, 4px)', padding: '10px 12px', maxHeight: 280, overflow: 'auto' }}>
|
||||||
onClick={() => { onDecide(a, 'reject', reason); setRejecting(null); setReason(''); }}>
|
<window.MaestroRichDoc md={a.doc} />
|
||||||
{t.confirmReject}
|
</div>
|
||||||
</Button>
|
) : null}
|
||||||
</div>
|
{rejecting === a.id ? (
|
||||||
) : null}
|
<div style={{ display: 'flex', gap: 10, alignItems: 'flex-start', marginTop: 10 }}>
|
||||||
|
<Textarea danger placeholder={t.rejectPlaceholder} minHeight={56} value={reason} onChange={setReason} />
|
||||||
|
<Button variant="reject" disabled={!reason.trim()}
|
||||||
|
onClick={() => { onDecide(a, 'reject', reason); setRejecting(null); setReason(''); }}>
|
||||||
|
{t.confirmReject}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
</GateCard>
|
</GateCard>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user