@jeik/prompt-rewriter
v1.1.0
Published
按 YAML 模板,在**消息进 AI 之前**和**回复发出之前**做结构化包裹,并可让 AI 给回复**打标记**,配合钉钉连接器精准认定最终答案。
Readme
prompt-rewriter (YAML)
按 YAML 模板,在消息进 AI 之前和回复发出之前做结构化包裹,并可让 AI 给回复打标记,配合钉钉连接器精准认定最终答案。
三件事:
- 入站包裹 —— 把用户原文塞进你的模板(如先加一层"权限审查"约束),再交给 AI。
- 出站包裹 —— 把 AI 的最终回复塞进你的模板(如加免责声明 / 署名后缀)。
- 打标记(默认关,安装向导引导开启)—— 注入指令让 AI 给过程段加
[-process-]、最终答案加[-final-],钉钉卡片据此精准认定最终答案。
安装
方式一:npm(推荐)
插件已发布到 npm。用自带的安装向导,一条命令完成「装插件 + 开启所需 hook 权限」:
npx -y @jeik/prompt-rewriter install向导会问是否开启打标记功能(默认关);开启时再问注入到系统提示词还是用户指令,并自动写入所需的 allowPromptInjection 权限、在 ~/.openclaw/prompt-rewriter/config.yaml 生成带注释示例的配置(marker 段按选择启用)。最后提示重启。
只想装、之后自己配权限(见下方"必须的配置"):
openclaw plugins install @jeik/prompt-rewriter --force
--force即为覆盖更新,升级时同样用它,无需先卸载。
方式二:本地构建产物(开发 / 离线)
npm install && npm run build && npm pack # → jeik-prompt-rewriter-1.0.0.tgz
openclaw plugins install ./jeik-prompt-rewriter-1.0.0.tgz --force之后
# 写规则配置(可选,不写也能跑——默认只注入打标记指令):~/.openclaw/prompt-rewriter/config.yaml
openclaw gateway restart必须的配置(否则入站包裹/打标记不生效)
入站钩子 before_prompt_build 属于 prompt-injection 类,必须在 openclaw 主配置里显式放行:
"plugins": {
"entries": {
"prompt-rewriter": {
"enabled": true,
"hooks": {
"allowPromptInjection": true // ← 入站包裹 + 打标记指令,缺它直接被网关拦掉
}
}
}
}出站包裹用的
reply_payload_sending钩子不需要额外开关。 排错时若日志出现typed hook "before_prompt_build" blocked by ... allowPromptInjection=false,就是这里没配对。
热重载
config.yaml 按文件 mtime 缓存:改规则 / markers 后,下一轮消息即生效,无需 gateway restart。
- 解析失败时保留上一份有效配置(避免写到一半读到坏 YAML 把规则抹掉)。
- 装插件、改
allowPromptInjection等主配置后仍需重启网关。
配置文件 ~/.openclaw/prompt-rewriter/config.yaml
# 打标记(默认【关闭】;不写 markers 块或不设 enabled 即关闭)
markers:
enabled: true # 唯一开关,默认 false;需显式 true 才注入"打标记"指令(安装向导可引导写入)
inject: system # user=用户指令 / system=系统提示(默认 system)
# instruction: | # 可选:覆盖下面的默认注入文案(自定义时务必保留固定标记 [-process-] / [-final-])
# 【输出格式要求 / 必须遵守】
# 1. 你在思考、查资料、执行中间步骤时输出的每一段过程性内容,【末尾】加标记:[-process-]
# 2. 你的最终答案(给用户看的正式回复)【末尾】加标记:[-final-]
# 3. 整轮对话中 [-final-] 只能出现一次,且必须在最终答案的最末尾。
# 4. 标记只放在末尾、原样输出即可,不要放在开头、不要解释这些标记,也不要把它们包进代码块。
# 包裹规则(可多条)
rules:
- name: "回复加免责声明" # 可省,日志里好认
placeholder: "【agent_reply】" # 必填,模板里要被替换的标记
where: reply # prompt / reply / both,默认 both
enabled: true # 可省,默认 true
inject: user # 仅入站生效:user=用户指令(默认) / system=系统提示
template: | # 必填,必须包含 placeholder
【agent_reply】
---该内容由 AI 生成,请注意辨别字段
| 字段 | 必填 | 说明 |
|---|---|---|
| placeholder | ✅ | 模板里被替换成实际内容的标记,如 【user_message】、【agent_reply】 |
| template | ✅ | 完整模板,必须包含 placeholder;出现几次替换几次 |
| where | | prompt(入站)/ reply(出站)/ both(默认) |
| inject | | 注入通道,仅入站生效:user=用户指令(默认)/ system=系统提示 |
| name | | 备注名,出错/日志里能认出 |
| enabled | | 默认 true,设 false 临时停用 |
where 精确含义:
prompt—— 网关收到渠道消息后、送进 AI 之前。用event.prompt(用户整段原文)填模板。reply—— AI 最终回复生成后、发到渠道之前。用回复正文填模板(仅kind==="final")。both—— 两端都套(默认)。
入站只取第一条命中的 prompt 规则;出站只取第一条命中的 reply 规则。
注入通道:用户指令 vs 系统提示(inject)
入站注入分两条通道,本质都是"进 AI 之前的提示词注入",区别在塞到哪一层:
| inject | 叫法 | 注入到 | 适合 |
|---|---|---|---|
| user(默认) | 用户指令 | 用户消息层(prependContext) | 跟当前这条用户消息强相关的包装,如"把用户需求包成带约束的指令" |
| system | 系统提示 | 系统提示层(prependSystemContext,可被 provider 缓存省 token) | 每轮都一样的静态约束,如打标记格式、全局人设 |
- prompt 模板默认
user(用户指令);markers 默认system(系统提示)。 - 同一通道有多段(如 prompt 模板 + markers 都设成 system),按出现顺序用空行拼接。
inject对出站reply规则无意义(出站是改投递文本,不分系统/用户层),会被忽略。
工作原理(重要:回复"两条流")
OpenClaw 把 AI 回复分成两条互不同步的流,这决定了出站包裹的可见范围:
| 流 | 谁显示它 | 本插件怎么处理 |
|---|---|---|
| 持久化消息 | webchat 控制台、历史记录 | 不碰(避免把模板写进上下文污染后续轮次) |
| 投递 payload | 各渠道实际发送的内容 | reply_payload_sending 包裹(不污染历史) |
后果(属正常现象,非 bug):
- webchat 控制台看不到出站模板——它显示的是持久化原文。这是有意为之:控制台展示模型真实输出。
- 钉钉等渠道能看到——渠道发的是投递 payload。
钉钉连接器用自定义 dispatcher,核心不会自动跑
reply_payload_sending;配套的钉钉连接器修复版会在投递时显式调用本插件的出站钩子,所以钉钉能正确套上模板。用其它标准渠道(如 Telegram)则由核心自动触发。
钩子一览:
| 时机 | 钩子 | 作用 | 需要的开关 |
|---|---|---|---|
| 入站 | before_prompt_build | prompt 模板(prependContext)+ 打标记指令(prependSystemContext) | allowPromptInjection: true |
| 出站 | reply_payload_sending | reply 模板包裹投递文本(kind==="final") | 无 |
打标记 + 钉钉精准渲染
痛点:AI 常分多段输出(思考过程 + 最终答案),到达顺序会乱,钉钉卡片可能把中间段当成最终、提前定稿。
方案:
- 本插件(默认关,开启后)注入指令,让 AI 给过程段加
[-process-]、最终答案加[-final-]。 - 钉钉连接器修复版据此:
- 带标记 → 只认
[-final-]后的内容当最终答案,其余当过程; - 不带标记 → 兜底用 openclaw 的
isReasoning标签选"最近一段非思考的正式答案",不再靠时序猜; - 标记对用户不可见(渲染前剥离);
- AI 漏标
[-final-]→ 自动退回兜底,绝不卡空白; - 选定最终答案后再套上你的 reply 固定模板。
- 带标记 → 只认
不想用标记:markers.enabled: false,或干脆不把指令喂给 AI(连接器检测不到标记就自动走兜底)。
示例
见 examples/:
| 文件 | 用途 |
|---|---|
| 01-permission-gate.yaml | 入站包一层权限审查约束 |
| 02-reply-wrap.yaml | 出站给回复加前缀/后缀 |
| 03-bilingual-prompt.yaml | 同段原文在模板里出现多次(演示全部替换) |
| 04-both-sides.yaml | where: both 两端都管 |
cp examples/02-reply-wrap.yaml ~/.openclaw/prompt-rewriter/config.yaml
openclaw gateway restart测试与验证
插件每个钩子、每个分支都打中文日志(前缀 [prompt-rewriter])。装好发条消息后:
grep prompt-rewriter /tmp/openclaw/openclaw-*.log | tail -30应能看到:
- 启动:
插件加载完成,共 N 条规则,打标记=开 - 入站:
入站钩子 before_prompt_build 触发…→规则「…」生效,注入 N 字到 prompt 前/注入打标记系统指令(N 字) - 出站:
出站钩子 reply_payload_sending 触发:kind=final…→规则「…」生效,回复已套模板
钉钉连接器侧(确认走了哪条认定):
[DingTalk][closeStreaming] 最终答案来源=marker[-final-] / 非reasoning答案 / accumulatedText兜底排错
| 现象 | 原因 / 处理 |
|---|---|
| 入站不生效,日志 blocked by ... allowPromptInjection=false | 主配置 plugins.entries.prompt-rewriter.hooks.allowPromptInjection 没设 true |
| 改了源码不生效 | 要 npm run build → 重新 install --force → 重启;网关跑的是装进 extensions/ 的构建产物 |
| webchat 看不到出站模板 | 正常——webchat 显示持久化原文,模板在投递层。看渠道实际发送内容 |
| 钉钉回复没套上模板 | 需安装配套的钉钉连接器修复版(它在投递时显式跑本插件出站钩子) |
| AI 不打标记 | 确认 allowPromptInjection: true(指令走入站钩子);连接器会自动走兜底,仍能稳定 |
| 回复变成"回显用户原话" | 旧版误用了 before_agent_reply(拦截钩子)。当前版本已改用 reply_payload_sending,升级即可 |
开发
npm install
npm run build # esbuild → dist/index.js
npm pack # → jeik-prompt-rewriter-1.0.0.tgz源码:
- index.ts —— 钩子注册(入站 / 出站 / 打标记注入)
- src/config.ts —— 读 / 校验 yaml
- src/rewriter.ts —— 规则类型、模板渲染、标记常量与默认指令
