# 一张卡,把回答排成文章 > dsh-rich-cards 只做一件事:把回答末尾的那段 JSON,变成你愿意读完的版式。 对话里的长回答本质上是一篇文档:结论、证据、数据、时间线全混在一段 markdown 里,读者得自己找重点。卡片做的是先把这段文字拆成有名字的部分,再决定每一部分怎么排。 ## 它解决的不是美观问题 排版是副产品。被固定下来的是结构:先给一句话结论,再给证据,最后才展开细节。模型为了填满这些格子,不得不先把话说清楚——版式反过来约束了写作。 | | | | --- | --- | | 块类型 | 12 | | 布局 | 2(`card` / `article`)| | 代码量 | 1,273 行(宿主 318 + 客户端 955)| | 运行时依赖 | 0 | ## 两条传送带 插件历史上走过两条把数据送到渲染器的路,现在只剩第二条。 - **工具通道(已下线)**:注册一个 `rich_card` 工具,参数本身就是 JSON。写坏一个引号,整个回合以 `MALFORMED_RESPONSE` 结束——回答没了。 - **围栏通道(现行)**:模型在回答末尾写一个 ` ```card ` 代码块,正文是 JSON。渲染失败只留下一段源码,回答照常成立。 选后者不是因为它更优雅,是因为失败代价小一个数量级。 ## 一次回答的旅程 1. 宿主端往系统提示里加一段写作约定(`order: 34`),**不注册任何工具**。 2. 模型在回答末尾写一个 `card` 围栏块,正文是 JSON。 3. 浏览器端用 `MutationObserver` 扫到这段代码块,`parseFence` 解析成功,就在原位挂一张卡片。 4. 解析失败就原样留着,读者看到的是源码,不是坏掉的页面。 拆代码块的壳有讲究。渲染器的结构大致是: ```html
代码块 · 复制
…
``` 所以客户端不逐层藏标签:先用 `pre.closest('.md-code-block')` 找到整块,把卡片插到它后面,确认插入之后再 `display: none` 把整块替换掉。类名带哈希会随构建变化,`data-code-block-banner` 这类属性不会,因此认壳时优先用 data 属性,找不到再退回「藏抬头 + 压平外壳」的兜底。 ## 刻意做的取舍 - **fail open**:宁可丑,不可断。任何解析失败都退回源码。 - **不注册工具**:工具参数在流里就要过 `JSON.parse`,一处错误整轮死。 - **一张回答一张卡**:位置只在末尾,不跟正文抢注意力。 - **配图有预算**:两三次查不到就不配图,也不在正文里交代为什么没配——缺图不是新闻。 ## 已知边界 - 一张回答只渲染最后一张卡。 - 卡片固定在回答末尾,插不进段落中间。 - 依赖代码块结构;渲染器大改结构时要重新认壳(认壳逻辑有兜底,但兜底不是承诺)。 ## 版本与时间线 | 日期 | 事件 | | --- | --- | | 10-20 | 第一版:工具传参,卡片挂在回答末尾 | | 10-21 | 换成围栏块,工具下线,写坏不再中断整轮 | | 10-21 | 拆壳:认出 `md-code-block`,整块换掉 | | 10-22 | 1.2.4:按 data 属性找壳,换主题也不塌 | | 10-22 | 提交收录:awesome-dsh-plugin PR #8560 | ## 安装 ```sh dsh plugin --profile <你的 profile> add link:/path/to/dsh-rich-cards ``` 装完刷新窗口。接口只有系统提示里那一段约定:没有配置项,没有设置面板,也没有工具。