引入正文安全子集并收紧呈现创作口径,避免本局任意脚本美化。
创作只选壳与分区,渲染与提示共用同一份白名单语法。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -63,11 +63,11 @@
|
||||
叙事指南与故事推进(工序编排)正文:一份全文(纲领/遣词/笔墨/禁忌/推进等);**整份**双挂转述与主世界层。artifact=`设计.叙事指南与故事推进`。
|
||||
对话落盘拆成两份:`设计.叙事指南`(遣词/笔墨/禁忌/写法档)与 `设计.故事推进`(推进 + 用户输入用法);各自双挂,条件独立落盘。
|
||||
回复呈现:每轮终稿给用户看什么(正文/字数/日期/变量);artifact=`设计.监控栏`(旧称设计监控栏/状态栏)。
|
||||
正文组成:把回复呈现清单展示给用户(选壳、美化,可写 CSS/HTML/JS);artifact=`设计.正文组成`。见 `docs/play-presentation-shells.md`。
|
||||
正文组成:把回复呈现清单展示给用户(选壳、分区、【正文安全子集】);artifact=`设计.正文组成`。见 `docs/play-presentation-shells.md`。
|
||||
变量设计:真值 + **维护语句** + **Data映射索引**(指向具体实例)+ side_effects。
|
||||
变量控制上下文:各槽视野 + **旁观汇总**;mount 含主世界层与旁观维护。
|
||||
生成规则:合同挂主世界层 + 旁观(旁观读 rule_id/格式/必要性,描写长文可 summary)。
|
||||
开场白与开场变量:每条开场 = meta(用户角色+初值)+ 正文(present.v1,壳与正文组成一致);可经 opening-generator 落库。
|
||||
开场白与开场变量:开场正文(present.v1,壳与正文组成一致)+ 开场变量初值;用户代入名片优先「主角设定」。可经 opening-generator 落库。
|
||||
|
||||
|
||||
具体实例正文:`rule_id` + `records[]`(始终数组;单层,键随规则;条数服从规则「一次生成」);自评=合规模(只按规则执行)。
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
> 上游:游玩管线 `gm → perspective? → narrator → auditor`。
|
||||
> 创作挂钩:`设计.正文组成`(怎么展示)/ `设计.监控栏`(每轮看什么,技能名「回复呈现」);终稿 tag 通常 `输出.用户展示`。
|
||||
> 各壳介绍与规格:`skills/dialogue/world-simulator/present-shells/`(正文组成执行时动态注入)。
|
||||
> **创作定位:按回复呈现清单选壳并美化,可写 CSS/HTML/JS。**
|
||||
> **创作定位:按回复呈现清单选壳并分区;正文语法见【正文安全子集】(程序动态装载)。**
|
||||
> 日常折叠/表格走 Markdown 安全子集;一般不写本局任意 CSS/HTML/JS。
|
||||
> 预览:`/shells.html`
|
||||
|
||||
## 1. 创作怎么做(权威口径)
|
||||
|
||||
@@ -122,7 +122,7 @@ Progressive 投影 本轮主世界层只看见「当前档」;未
|
||||
| **生成规则 / 具体实例** | 分档表 schema 与行数据(如好感→性格);生成后由变量设计索引 |
|
||||
| **正文组成** | 隐藏段定界(供解析维护语句);用户可见块序 |
|
||||
| **设计监控栏** | 用户可见的会变字段子集 |
|
||||
| **开场白与开场变量** | 开场正文(present.v1,跟正文组成)+ 与开场同真相的初值 / 用户角色 |
|
||||
| **开场白与开场变量** | 开场正文(present.v1,跟正文组成)+ 与开场同真相的初值;用户角色优先「主角设定」 |
|
||||
| **游玩拓扑 / 上下文投影排序 / 细化终稿** | 固定槽;扁平投影序;tables 收成 |
|
||||
|
||||
编排口诀:体验与机制 → 要不要真值 → 映射表用生成规则/实例 → 维护语句与旁观汇总 → 开场同真相 → 收成。
|
||||
|
||||
@@ -172,7 +172,7 @@ recipes/world-simulator|expand-assistant/recipe.yaml
|
||||
| 能力 | id | 状态 |
|
||||
|------|-----|------|
|
||||
| 美学纲领与交互范式 | `aesthetics-interaction` | **范例已写**;前端 mosaic 视图 |
|
||||
| 主角设定 | `protagonist` | **已写**:有产物则挂在当前选用的用户角色卡下(不进全局列表);关掉这局就不加载 |
|
||||
| 主角设定 | `protagonist` | **已写**:代入名片(名字/背景/特殊设定);挂在当前选用的用户角色卡下;关掉这局就不加载 |
|
||||
| 实现机制 | `mechanism` | **已写**:`context-fragment.v1`(支撑点正文 + 自评 + 追问);`feeds: gm`;**专用卡已接** |
|
||||
| 舞台骨架 | `world-blueprint` | **已写**:`context-fragment.v1`;社会结构 + 世界状况;**专用卡已接** |
|
||||
| 生成规则 | `generation-rules` | **已写**:`context-fragment.v1`;方向池优先、元素池仅封闭集合;可反复;**〔先验产物〕**;规划字段 `target`;挂 gm+auditor;**专用卡已接** |
|
||||
@@ -183,10 +183,10 @@ recipes/world-simulator|expand-assistant/recipe.yaml
|
||||
| 拓扑图谱 | `topology` | 必须生成且不适合走生成规则→实例时,用拓扑结构写出(升级路径、地图、人物关系等) |
|
||||
| 回复呈现 | `status-bar` | **已写**:每轮终稿看什么(正文/字数/日期/变量);旧称设计监控栏 |
|
||||
| 随机范围整理 | `random-range` | **已写**:汇总检定/对抗等随机项与范围;正文组成之前;替代机遇裁定槽 |
|
||||
| 正文组成 | `reply-format` | **已写**:把回复呈现清单展示出来;选壳+美化,可写 CSS/HTML/JS;可接隐藏备用随机区 |
|
||||
| 正文组成 | `reply-format` | **已写**:选壳+分区;正文语法【正文安全子集】动态注入;可接隐藏备用随机区 |
|
||||
| 变量设计与更新规则 | `variable-design` | **已写**;专用/结构化卡 |
|
||||
| 变量控制上下文 | `variable-context` | **已写**:旁观汇总;mount 含 auditor;通用 fragment 卡 |
|
||||
| 开场白与开场变量 | `opening-setup` | **已写**:每条 = meta(初值/用户角色)+ present.v1 正文(壳跟正文组成);**〔收口〕**选定后落库并保存定稿 |
|
||||
| 开场白与开场变量 | `opening-setup` | **已写**:每条 = meta(初值)+ present.v1 正文;用户角色优先主角设定;**〔收口〕**选定后落库并保存定稿 |
|
||||
|
||||
共用收成(池内保留,按需):
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# 正文 Markdown 安全子集(程序动态装载进「正文组成」)
|
||||
# 实现:web/markdown.js · 先转义再解析;禁止裸 HTML / 任意 CSS / JS
|
||||
version: 1
|
||||
intro: >
|
||||
用户开启 Markdown 渲染时,游玩正文(及壳区域主读)只吃下列语法。
|
||||
美化靠程序预制 class(.md-*),不要写本局任意 CSS/HTML/JS。
|
||||
目录可增条目;技能 prompt 勿背死清单。
|
||||
features:
|
||||
- id: paragraph
|
||||
name: 段落与换行
|
||||
syntax: 空行分段;段内换行保留
|
||||
- id: heading
|
||||
name: 标题
|
||||
syntax: "# ~ ###### 标题"
|
||||
- id: emphasis
|
||||
name: 强调
|
||||
syntax: "**粗** *斜* ~~删~~ `行内代码`"
|
||||
- id: link
|
||||
name: 链接
|
||||
syntax: "[文案](https://…)(仅 http/https)"
|
||||
- id: list
|
||||
name: 列表
|
||||
syntax: '- 项 / 1. 项'
|
||||
- id: quote
|
||||
name: 引用
|
||||
syntax: '> 引用行'
|
||||
- id: fence
|
||||
name: 代码块
|
||||
syntax: '三反引号围栏代码块'
|
||||
- id: hr
|
||||
name: 分隔线
|
||||
syntax: '---'
|
||||
- id: table
|
||||
name: 表格
|
||||
syntax: |
|
||||
| 列甲 | 列乙 |
|
||||
| --- | --- |
|
||||
| 值1 | 值2 |
|
||||
- id: details
|
||||
name: 折叠
|
||||
syntax: |
|
||||
:::details 标题
|
||||
折叠正文(可含上表其它语法)
|
||||
:::
|
||||
默认展开::::details open 标题
|
||||
restrictions:
|
||||
- 禁止裸 HTML 标签、事件属性、script
|
||||
- 禁止本局任意 CSS / JS(壳微调 tone_chrome 等另走呈现壳)
|
||||
- 链接协议仅 http / https
|
||||
@@ -1,7 +1,7 @@
|
||||
# 正文组成
|
||||
|
||||
> 技能文档。程序只切割下方 **fence 块**;`##` 标题仅供人读。
|
||||
> **创作定位:把「回复呈现」已钉的内容展示给用户**(选壳、美化排版,可写 CSS/HTML/JS)。壳介绍与规格写在各壳自己的文档,程序动态装载。
|
||||
> **创作定位:把「回复呈现」已钉的内容展示给用户**(选壳、分区、用安全子集美化)。壳与【正文安全子集】由程序动态装载。
|
||||
> 产物外壳:`docs/context-fragment-design.md`。
|
||||
> 旧称:设计回复格式。
|
||||
|
||||
@@ -12,7 +12,7 @@ name: 正文组成
|
||||
id: reply-format
|
||||
artifact: 设计.正文组成
|
||||
declaration: >
|
||||
把「回复呈现」已钉的内容展示给用户:选壳、美化排版,可写 CSS/HTML/JS;有「随机范围整理」则把隐藏备用随机区接到消息;排在二者之后、开场白之前
|
||||
把「回复呈现」已钉的内容展示给用户:选壳、分区、按【正文安全子集】做折叠/表格等美化;有「随机范围整理」则把隐藏备用随机区接到消息;排在二者之后、开场白之前
|
||||
when: |
|
||||
已有「回复呈现」(或等价:每轮终稿看什么已钉)后排。几乎每局都要。
|
||||
若本局有随机需求:须已有「随机范围整理」(或确认无随机)后再排本步。
|
||||
@@ -23,7 +23,7 @@ when_not: |
|
||||
不要排在开场白之后。几乎不跳过。
|
||||
只谈文风遣词 → 「叙事指南」(工序编排:「叙事指南与故事推进」)。只改呈现区域与内容规则(字数/选项槽/监控键)→ 「回复呈现」。
|
||||
boundary: |
|
||||
本技能:前端展示——从【可选呈现壳】选壳、把回复呈现的各区域落到版式、美化排版(可含 CSS/HTML/JS);若有随机范围表,预留/接到隐藏备用随机区(用户看不见、发给 AI 仍在上下文)。
|
||||
本技能:前端展示——从【可选呈现壳】选壳、把回复呈现的各区域落到版式;正文语法以【正文安全子集】为准(折叠、表格等)。
|
||||
回复呈现:必选/可选区域 + 各区简单内容规则——本步不重议清单与规则,只负责怎么显示。
|
||||
随机范围整理:要插哪些数、范围是什么——本步不改范围表,只负责怎么藏进消息。
|
||||
叙事指南:文风。变量设计:真值规则。开场白:第一屏——本步先定展示。
|
||||
@@ -33,11 +33,11 @@ feeds: narrator
|
||||
## opening
|
||||
|
||||
```opening
|
||||
本局把「回复呈现」里已钉的内容展示给用户(选壳、美化排版)。壳清单由程序动态装载,不要背死几种。
|
||||
本局把「回复呈现」里已钉的内容展示给用户(选壳、分区、安全子集美化)。壳与安全子集由程序动态装载,不要背死。
|
||||
|
||||
1. 对照【可选呈现壳】:哪一个最适合放下已钉的那些东西?
|
||||
2. 各项落到壳的哪一区?显示名叫什么?
|
||||
3. 要不要额外的 CSS/HTML/JS 来美化(没有就说没有)?
|
||||
2. 各项落到壳的哪一区?显示名叫什么?渲染提示用纯文本还是 Markdown?
|
||||
3. 需要折叠/表格等时,对照【正文安全子集】(一般够用;html/css/js 默认空)。
|
||||
4. 定稿时会用前面情节架空一幕灌进壳里,专供你看版式/美化(内容随便,不当正史)。
|
||||
```
|
||||
|
||||
@@ -46,17 +46,18 @@ feeds: narrator
|
||||
```task
|
||||
你正在执行「正文组成」。产物必须是 **context-fragment.v1** JSON,写入「设计.正文组成」。
|
||||
|
||||
本步 = **前端展示**:按「回复呈现」的清单,从【可选呈现壳】里选一个,把内容落到区域并美化排版。可写本局需要的 CSS/HTML/JS。
|
||||
禁止重议「看什么、正文多少字」;禁止自造目录没有的 shell_id。
|
||||
本步 = **前端展示**:按「回复呈现」的清单,从【可选呈现壳】里选一个,把内容落到区域。
|
||||
正文美化优先【正文安全子集】(用户开 Markdown 渲染时生效)。日常折叠、表格等走子集即可;「渲染」里 html/css/js 默认空字符串。
|
||||
禁止重议「看什么、正文多少字」;禁止自造目录没有的 shell_id;禁止发明子集没有的语法。
|
||||
|
||||
执行顺序:
|
||||
1. 读「回复呈现」与美学/叙事:清单是权威;不重开「看什么」问卷。
|
||||
2. **选 shell_id**(必须是【可选呈现壳】里的 id);说明为何这个壳放得下清单里的项。
|
||||
3. 可见块:把清单各项挂到该壳已有区域;写显示名与职责。
|
||||
4. 需要时写「渲染」:本局 CSS/HTML/JS 片段(美化排版,不是新后端协议)。
|
||||
3. 可见块:把清单各项挂到该壳已有区域;写显示名与职责;渲染提示选「纯文本」或「Markdown」。
|
||||
4. 「渲染」html/css/js:默认全空;仅当用户点名要超出安全子集的交互时再议(本步通常不写)。
|
||||
5. 隐藏段(若清单要求):定界与用途。
|
||||
6. 填「呈现壳.微调」;分区必须与壳一致。
|
||||
7. **示例**:用前面已定情节架空一幕灌进 `present.v1`,专供验收版式/美化;字数以回复呈现为准,本步不重钉篇幅。
|
||||
7. **示例**:用前面已定情节架空一幕灌进 `present.v1`;body 可演示子集语法(如折叠/表格);字数以回复呈现为准。
|
||||
8. 自评 + 追问;禁止写 context `order`。
|
||||
|
||||
summary:`正文组成 · 适配 {shell_id} · …`。
|
||||
@@ -66,14 +67,14 @@ summary:`正文组成 · 适配 {shell_id} · …`。
|
||||
|
||||
```principles
|
||||
1. 创作 = 把「回复呈现」的清单展示出来
|
||||
选壳、分区、显示名、CSS/HTML/JS 都为那份清单服务。本步验收重点是「看起来对不对」。
|
||||
选壳、分区、显示名、安全子集语法都为那份清单服务。本步验收重点是「看起来对不对」。
|
||||
「按意见修改」:以最新产物为底;用户要求按先后都有效,冲突时以较新的为准。用户说整体可以、或只提美化 → 不换 shell_id。换壳必须用户点名。
|
||||
|
||||
2. 清单权威在「回复呈现」
|
||||
正文篇幅、要不要日期、展示哪些变量,以那份产物为准。本步只决定怎么显示。
|
||||
|
||||
3. 壳是底,渲染可加
|
||||
从【可选呈现壳】选底。需要时写本局 CSS/HTML/JS 做美化;不要发明新的后端协议或目录没有的壳。
|
||||
3. 壳是底,子集是语法
|
||||
从【可选呈现壳】选底;Markdown 区只使用【正文安全子集】。不要发明新后端协议、目录没有的壳、或子集没有的语法。
|
||||
|
||||
4. 隐藏段服务维护,不污染阅读
|
||||
|
||||
@@ -90,8 +91,8 @@ summary:`正文组成 · 适配 {shell_id} · …`。
|
||||
```probe
|
||||
每轮 1~2 点。
|
||||
|
||||
优先:壳能否放下回复呈现的各项;分区与显示名;要不要 CSS/HTML/JS。
|
||||
不追问:重议字数/变量清单、事件池 schema、文风遣词、示例文笔好坏。
|
||||
优先:壳能否放下回复呈现的各项;分区与显示名;渲染提示纯文本还是 Markdown;要不要子集里的折叠/表格。
|
||||
不追问:重议字数/变量清单、事件池 schema、文风遣词、示例文笔好坏、本局任意 CSS/JS。
|
||||
```
|
||||
|
||||
## output
|
||||
@@ -100,46 +101,38 @@ summary:`正文组成 · 适配 {shell_id} · …`。
|
||||
{
|
||||
"schema": "context-fragment.v1",
|
||||
"技能": "正文组成",
|
||||
"brief": "一句话:壳 + 主要块序",
|
||||
"brief": "一句话:选了哪壳、关键区怎么挂",
|
||||
"mount": ["narrator"],
|
||||
"稳变": "stable",
|
||||
"正文": {
|
||||
"依据的体验与呈现": [
|
||||
"从回复呈现继承的清单 + 美学/叙事呈现要点"
|
||||
],
|
||||
"依据的体验与呈现": {
|
||||
"回复呈现要点": "从设计.监控栏摘:必选区 + 字数等",
|
||||
"美学或叙事可映到版式的一句": "可空"
|
||||
},
|
||||
"呈现壳": {
|
||||
"shell_id": "必须与【可选呈现壳】某一 id 一致",
|
||||
"为何选它": "一句话",
|
||||
"shell_id": "必须是【可选呈现壳】里的 id",
|
||||
"为何选它": "对照回复呈现要同时看见什么",
|
||||
"微调": {
|
||||
"tone_chrome": "messenger|terminal|book|default",
|
||||
"show_suggested_actions": true,
|
||||
"monitor_fields": [],
|
||||
"block_labels": {},
|
||||
"show_suggested_actions": true,
|
||||
"tone_chrome": "default|messenger|book|terminal",
|
||||
"empty_states": {}
|
||||
}
|
||||
},
|
||||
"版式隐喻": "给人读的比喻;程序以 shell_id 为准",
|
||||
"版式隐喻": "可空;一句气质,不是新壳",
|
||||
"可见块": [
|
||||
{
|
||||
"块id": "英文 kebab 或中文短名,同局稳定",
|
||||
"区域": "monitor|header|body|footer|aside",
|
||||
"显示名": "用户可理解的标题;可无标题则空",
|
||||
"职责": "这块给用户什么信息",
|
||||
"顺序": 1,
|
||||
"可空": true,
|
||||
"填充方": "叙事转述|程序拼装|回复呈现变量|其它",
|
||||
"内容形态": "散文|短列表|键值行|其它",
|
||||
"示例": "可选:该块一句占位(总预览见正文.示例)"
|
||||
"块id": "与回复呈现一致的短名",
|
||||
"挂到区域": "monitor|header|body|footer|aside",
|
||||
"显示名": "可空",
|
||||
"职责": "一句",
|
||||
"来源提示": "转述填|程序填|混合"
|
||||
}
|
||||
],
|
||||
"呈现清单落实": {
|
||||
"依据的回复呈现": "brief 或要点",
|
||||
"各项落到": [
|
||||
{
|
||||
"清单项": "正文|日期|某变量|建议行动|…",
|
||||
"区域": "monitor|header|body|footer|aside|hidden",
|
||||
"显示名": "可空"
|
||||
}
|
||||
]
|
||||
"监控栏锚点": {
|
||||
"启用": true,
|
||||
"字段": ["与回复呈现一致;无则 []"]
|
||||
},
|
||||
"隐藏段": [
|
||||
{
|
||||
@@ -159,20 +152,20 @@ summary:`正文组成 · 适配 {shell_id} · …`。
|
||||
"区域id": "monitor|header|body|footer|aside",
|
||||
"对应块id": ["…"],
|
||||
"意图": "固定顶栏|主滚动|折叠文末|…",
|
||||
"渲染提示": "纯文本|Markdown|自定义"
|
||||
"渲染提示": "纯文本|Markdown"
|
||||
}
|
||||
],
|
||||
"渲染": {
|
||||
"html": "可选:本局结构片段;无则空字符串",
|
||||
"css": "可选:本局样式;无则空字符串",
|
||||
"js": "可选:本局交互;无则空字符串"
|
||||
"html": "",
|
||||
"css": "",
|
||||
"js": ""
|
||||
},
|
||||
"说明": "无分区则写「单流渲染」(通常 prose)"
|
||||
"说明": "Markdown 区语法见【正文安全子集】;渲染三字段默认空。无分区则写「单流渲染」(通常 prose)"
|
||||
},
|
||||
"拼装与终稿": {
|
||||
"终稿tag": "通常 输出.用户展示",
|
||||
"拼装方": "叙事转述一次写出各块|程序按块拼接|混合",
|
||||
"推荐灌数": "分块壳优先 present.v1;prose 可为纯 Markdown",
|
||||
"推荐灌数": "分块壳优先 present.v1;prose 可为纯 Markdown(安全子集)",
|
||||
"单轮正文篇幅": "以回复呈现的字数为准,本步不重钉",
|
||||
"与正文关系": "转述读主世界「输出.用户展示」再填各块;可补残稿与版式,不改剧情;隐藏段可供旁观解析维护语句"
|
||||
},
|
||||
@@ -186,7 +179,7 @@ summary:`正文组成 · 适配 {shell_id} · …`。
|
||||
"blocks": {
|
||||
"monitor": "若该壳有 monitor:短键值;否则省略",
|
||||
"header": "若有则短字;无则省略",
|
||||
"body": "架空主读文(短段即可,勿写成章)",
|
||||
"body": "架空主读文;需要时可演示 :::details / 表格(短段即可)",
|
||||
"aside": "若有则短线索/备注;无则省略",
|
||||
"footer": "若有则可空或一句"
|
||||
},
|
||||
@@ -211,7 +204,7 @@ summary:`正文组成 · 适配 {shell_id} · …`。
|
||||
{
|
||||
"名": "可实现",
|
||||
"分数": 8,
|
||||
"说明": "shell_id 是否在【可选呈现壳】内;分区与隐藏段是否可交给转述/程序"
|
||||
"说明": "shell_id 是否在【可选呈现壳】内;Markdown 是否只用【正文安全子集】"
|
||||
},
|
||||
{
|
||||
"名": "版式可感",
|
||||
@@ -241,7 +234,7 @@ summary:`正文组成 · 适配 {shell_id} · …`。
|
||||
3. `可见块` 至少 1 条(通常含 body);禁止自造区域 id / 目录没有的壳。
|
||||
4. 正文须含 `示例`:`性质` 标明非正史;`灌数` 为 `present.v1`,`shell` 与本步 shell_id 一致;只填该壳已有区域。
|
||||
5. 示例内容可糙:只为验收壳与美化;禁止当开场白/正史;禁止为示例新造设定。
|
||||
6. 禁止程序 API schema、context `order`。需要美化时写本局 CSS/HTML/JS,不要另起后端协议。
|
||||
6. 禁止程序 API schema、context `order`。Markdown 区只使用【正文安全子集】;渲染 html/css/js 默认空。
|
||||
7. 自评:可见完备 / 清单落实 / 可实现 / 版式可感。
|
||||
8. summary:`正文组成 · 适配 {shell_id} · {brief 缩略}`。
|
||||
9. 兼容旧称产物 tag「设计.回复格式」。
|
||||
@@ -252,7 +245,7 @@ summary:`正文组成 · 适配 {shell_id} · …`。
|
||||
- [ ] 是把「回复呈现」的清单展示出来,而不是重议看什么?
|
||||
- [ ] shell_id 在【可选呈现壳】内?可见块未超出该壳分区?
|
||||
- [ ] 清单各项已落到区域?字数未在本步另定一套?
|
||||
- [ ] 隐藏段(若有)定界清楚?CSS/HTML/JS 只服务本局展示?
|
||||
- [ ] Markdown 区语法落在【正文安全子集】内?渲染三字段默认空?
|
||||
- [ ] 有示例灌数(present.v1),壳一致、各区撑开,且标明非正史?
|
||||
- [ ] 未把示例写成开场白或打磨成长文?
|
||||
```
|
||||
@@ -261,13 +254,15 @@ summary:`正文组成 · 适配 {shell_id} · …`。
|
||||
|
||||
```examples
|
||||
好:
|
||||
- 回复呈现要正文+好感:选 chat_monitor,好感进 monitor,正文进 body;可加短 CSS 美化芯片。
|
||||
- 回复呈现只要正文:选 prose;渲染可空。
|
||||
- 回复呈现要选项:选 choice_dock,示例 meta 带占位行动。
|
||||
- 回复呈现要正文+好感:选 chat_monitor,好感进 monitor,正文进 body;渲染提示 Markdown。
|
||||
- 需要折叠说明:body 用 :::details 标题 … :::(见安全子集)。
|
||||
- 需要对照表:body 用 Markdown 表格。
|
||||
- 回复呈现只要正文:选 prose;渲染三字段空。
|
||||
|
||||
坏:
|
||||
- 本步重钉「正文 300 字」而回复呈现已经写了 800~1500。
|
||||
- 不写 shell_id,只写「要好看的 UI」。
|
||||
- 示例写成第一章正史,或为写好看新造未定设定。
|
||||
- 示例 shell 与呈现壳不一致,或灌了壳没有的区域。
|
||||
- 写本局任意 CSS/JS,却只是要折叠或表格。
|
||||
```
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 正文 Markdown 安全子集目录。
|
||||
* 正文组成执行时注入【正文安全子集】,与 web/markdown.js 实现对照维护。
|
||||
*/
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
|
||||
const DEFAULT_SKILLS_ROOT = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"../../skills",
|
||||
);
|
||||
|
||||
export const MARKDOWN_SAFE_SUBSET_FILENAME =
|
||||
"markdown-safe-subset/catalog.yaml";
|
||||
|
||||
export type MarkdownSafeFeature = {
|
||||
id: string;
|
||||
name: string;
|
||||
syntax: string;
|
||||
};
|
||||
|
||||
export type MarkdownSafeSubsetCatalog = {
|
||||
version: number;
|
||||
intro: string;
|
||||
features: MarkdownSafeFeature[];
|
||||
restrictions: string[];
|
||||
};
|
||||
|
||||
function asString(v: unknown): string {
|
||||
return typeof v === "string" ? v.trim() : "";
|
||||
}
|
||||
|
||||
export function parseMarkdownSafeSubsetCatalog(
|
||||
raw: string,
|
||||
): MarkdownSafeSubsetCatalog | null {
|
||||
let doc: unknown;
|
||||
try {
|
||||
doc = parseYaml(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
|
||||
const row = doc as Record<string, unknown>;
|
||||
const featuresRaw = row.features;
|
||||
if (!Array.isArray(featuresRaw)) return null;
|
||||
const features: MarkdownSafeFeature[] = [];
|
||||
for (const item of featuresRaw) {
|
||||
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
||||
const f = item as Record<string, unknown>;
|
||||
const id = asString(f.id);
|
||||
const name = asString(f.name);
|
||||
const syntax = asString(f.syntax);
|
||||
if (!id || !name || !syntax) continue;
|
||||
features.push({ id, name, syntax });
|
||||
}
|
||||
if (!features.length) return null;
|
||||
const restrictions: string[] = [];
|
||||
if (Array.isArray(row.restrictions)) {
|
||||
for (const r of row.restrictions) {
|
||||
const s = asString(r);
|
||||
if (s) restrictions.push(s);
|
||||
}
|
||||
}
|
||||
const version =
|
||||
typeof row.version === "number" && Number.isFinite(row.version)
|
||||
? row.version
|
||||
: 1;
|
||||
return {
|
||||
version,
|
||||
intro: asString(row.intro),
|
||||
features,
|
||||
restrictions,
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadMarkdownSafeSubsetCatalog(
|
||||
skillPackRoot: string,
|
||||
skillsRoot = DEFAULT_SKILLS_ROOT,
|
||||
): Promise<MarkdownSafeSubsetCatalog | null> {
|
||||
const catalogPath = path.join(
|
||||
skillsRoot,
|
||||
skillPackRoot,
|
||||
MARKDOWN_SAFE_SUBSET_FILENAME,
|
||||
);
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await readFile(catalogPath, "utf8");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return parseMarkdownSafeSubsetCatalog(raw);
|
||||
}
|
||||
|
||||
export function formatMarkdownSafeSubsetForPrompt(
|
||||
catalog: MarkdownSafeSubsetCatalog,
|
||||
): string {
|
||||
const lines = [
|
||||
"## 【正文安全子集】程序从仓库动态装载",
|
||||
"",
|
||||
catalog.intro ||
|
||||
"用户开启 Markdown 渲染时,正文只吃下列语法;美化靠程序 class,勿写本局任意 CSS/HTML/JS。",
|
||||
"",
|
||||
];
|
||||
for (const f of catalog.features) {
|
||||
lines.push(`- **${f.name}**(\`${f.id}\`)`);
|
||||
const syn = f.syntax.trim();
|
||||
if (syn.includes("\n")) {
|
||||
lines.push("", "```", syn, "```", "");
|
||||
} else {
|
||||
lines.push(` \`${syn}\``);
|
||||
}
|
||||
}
|
||||
if (catalog.restrictions.length) {
|
||||
lines.push("", "硬限制:");
|
||||
for (const r of catalog.restrictions) {
|
||||
lines.push(`- ${r}`);
|
||||
}
|
||||
}
|
||||
lines.push(
|
||||
"",
|
||||
"区域「渲染提示」选 Markdown 时,只使用上表;日常折叠/表格走子集即可,一般不必填「渲染」里的 html/css/js。",
|
||||
);
|
||||
return lines.join("\n").trim();
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { renderMarkdownToHtml } from "../web/markdown.js";
|
||||
|
||||
describe("markdown safe subset render", () => {
|
||||
it("renders GFM tables", () => {
|
||||
const html = renderMarkdownToHtml(
|
||||
["| 甲 | 乙 |", "| --- | --- |", "| 1 | **粗** |"].join("\n"),
|
||||
);
|
||||
expect(html).toContain('<table class="md-table">');
|
||||
expect(html).toContain("<th>");
|
||||
expect(html).toContain("<td>");
|
||||
expect(html).toContain("<strong>粗</strong>");
|
||||
expect(html).not.toContain("| 甲 |");
|
||||
});
|
||||
|
||||
it("renders details folds", () => {
|
||||
const html = renderMarkdownToHtml(
|
||||
[":::details 规则说明", "里面有 *斜体*", ":::"].join("\n"),
|
||||
);
|
||||
expect(html).toContain('<details class="md-details">');
|
||||
expect(html).toContain('<summary class="md-summary">');
|
||||
expect(html).toContain("规则说明");
|
||||
expect(html).toContain("<em>斜体</em>");
|
||||
});
|
||||
|
||||
it("supports open details", () => {
|
||||
const html = renderMarkdownToHtml(
|
||||
[":::details open 已展开", "内容", ":::"].join("\n"),
|
||||
);
|
||||
expect(html).toContain("<details class=\"md-details\" open>");
|
||||
});
|
||||
|
||||
it("escapes raw html tags", () => {
|
||||
const html = renderMarkdownToHtml("<script>alert(1)</script>");
|
||||
expect(html).not.toContain("<script>");
|
||||
expect(html).toContain("<script>");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
formatMarkdownSafeSubsetForPrompt,
|
||||
loadMarkdownSafeSubsetCatalog,
|
||||
parseMarkdownSafeSubsetCatalog,
|
||||
} from "../src/skills/markdown-safe-subset.js";
|
||||
|
||||
const PACK = "dialogue/world-simulator";
|
||||
|
||||
describe("markdown safe subset catalog", () => {
|
||||
it("parses features from yaml", () => {
|
||||
const cat = parseMarkdownSafeSubsetCatalog(`
|
||||
version: 1
|
||||
intro: 测试简介
|
||||
features:
|
||||
- id: table
|
||||
name: 表格
|
||||
syntax: "| a | b |"
|
||||
- id: details
|
||||
name: 折叠
|
||||
syntax: |
|
||||
:::details 标题
|
||||
内容
|
||||
:::
|
||||
restrictions:
|
||||
- 禁止 script
|
||||
`);
|
||||
expect(cat?.features.map((f) => f.id)).toEqual(["table", "details"]);
|
||||
expect(cat?.restrictions).toEqual(["禁止 script"]);
|
||||
});
|
||||
|
||||
it("loads pack catalog with table and details", async () => {
|
||||
const cat = await loadMarkdownSafeSubsetCatalog(PACK);
|
||||
expect(cat).toBeTruthy();
|
||||
const ids = cat!.features.map((f) => f.id);
|
||||
expect(ids).toContain("table");
|
||||
expect(ids).toContain("details");
|
||||
expect(ids).toContain("heading");
|
||||
});
|
||||
|
||||
it("formats injection block for reply-format", async () => {
|
||||
const cat = await loadMarkdownSafeSubsetCatalog(PACK);
|
||||
const block = formatMarkdownSafeSubsetForPrompt(cat!);
|
||||
expect(block).toContain("【正文安全子集】");
|
||||
expect(block).toContain("动态装载");
|
||||
expect(block).toContain("`table`");
|
||||
expect(block).toContain("`details`");
|
||||
expect(block).toContain(":::details");
|
||||
});
|
||||
});
|
||||
+77
-2
@@ -1,4 +1,6 @@
|
||||
/** Optional Markdown display — escaped first, then a small safe subset. */
|
||||
/** Optional Markdown display — escaped first, then a small safe subset.
|
||||
* Feature list (prompt injection): skills/.../markdown-safe-subset/catalog.yaml
|
||||
*/
|
||||
|
||||
const STORAGE_KEY = "wa-markdown-render";
|
||||
|
||||
@@ -36,7 +38,21 @@ function esc(s) {
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
/** Safe subset: fences, inline code, headings, emphasis, links, lists, quotes, hr, breaks. */
|
||||
function isTableSeparator(line) {
|
||||
const t = String(line ?? "").trim();
|
||||
if (!t.includes("-")) return false;
|
||||
// GFM: | --- | :---: | ---: |
|
||||
return /^\|?(\s*:?-{3,}:?\s*\|)+\s*:?-{3,}:?\s*\|?\s*$/.test(t);
|
||||
}
|
||||
|
||||
function splitTableRow(line) {
|
||||
let s = String(line ?? "").trim();
|
||||
if (s.startsWith("|")) s = s.slice(1);
|
||||
if (s.endsWith("|")) s = s.slice(0, -1);
|
||||
return s.split("|").map((c) => c.trim());
|
||||
}
|
||||
|
||||
/** Safe subset: fences, details, tables, headings, emphasis, links, lists, quotes, hr. */
|
||||
export function renderMarkdownToHtml(raw) {
|
||||
const text = String(raw ?? "");
|
||||
if (!text) return "";
|
||||
@@ -92,6 +108,65 @@ export function renderMarkdownToHtml(raw) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const detailsStart = line.match(/^:::details(?:\s+(open))?\s+(.+?)\s*$/);
|
||||
if (detailsStart) {
|
||||
flushPara();
|
||||
flushList();
|
||||
const openAttr = detailsStart[1] ? " open" : "";
|
||||
const summary = detailsStart[2];
|
||||
const bodyLines = [];
|
||||
i += 1;
|
||||
while (i < lines.length && !/^:::\s*$/.test(lines[i])) {
|
||||
bodyLines.push(lines[i]);
|
||||
i += 1;
|
||||
}
|
||||
if (i < lines.length) i += 1;
|
||||
const inner = renderMarkdownToHtml(bodyLines.join("\n"));
|
||||
out.push(
|
||||
`<details class="md-details"${openAttr}><summary class="md-summary">${inlineMarkdown(
|
||||
summary,
|
||||
)}</summary><div class="md-details-body">${inner || ""}</div></details>`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
line.includes("|") &&
|
||||
i + 1 < lines.length &&
|
||||
isTableSeparator(lines[i + 1])
|
||||
) {
|
||||
flushPara();
|
||||
flushList();
|
||||
const headers = splitTableRow(line);
|
||||
i += 2;
|
||||
const rows = [];
|
||||
while (i < lines.length && lines[i].includes("|") && lines[i].trim()) {
|
||||
if (isTableSeparator(lines[i])) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
rows.push(splitTableRow(lines[i]));
|
||||
i += 1;
|
||||
}
|
||||
const thead = `<thead><tr>${headers
|
||||
.map((h) => `<th>${inlineMarkdown(h)}</th>`)
|
||||
.join("")}</tr></thead>`;
|
||||
const tbody = rows.length
|
||||
? `<tbody>${rows
|
||||
.map(
|
||||
(cells) =>
|
||||
`<tr>${headers
|
||||
.map((_, idx) => `<td>${inlineMarkdown(cells[idx] ?? "")}</td>`)
|
||||
.join("")}</tr>`,
|
||||
)
|
||||
.join("")}</tbody>`
|
||||
: "";
|
||||
out.push(
|
||||
`<div class="md-table-wrap"><table class="md-table">${thead}${tbody}</table></div>`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^\s*([-*_])\1{2,}\s*$/.test(line)) {
|
||||
flushPara();
|
||||
flushList();
|
||||
|
||||
Reference in New Issue
Block a user