dsh-persona-forge
English | 中文
给 DeepSeek Harness 用的角色扮演提示词改写插件。
在输入框里选一个角色,你的草稿会在发出前由 harness 的模型改写成该角色的口吻 —— 并且跟随当前会话正在使用的模型。你可以选择改写后直接发出,或先回到输入框人工审查。
状态: 早期版本(
0.3.x)。角色卡格式与 HTTP 契约已足够稳定,可以基于它开发;但请预期增量变化,升级前先看变更记录。
它做什么,不做什么
它改写的是你的消息。它不碰系统提示词、不碰工具列表、不碰会话配置,也绝不在你明确操作之前改动你的草稿。
| 会改变 | 你即将发出的那条消息的语气、语域与框架 |
| 绝不改变 | 你的技术需求、约束、路径、标识符、数字、代码 —— 以及你未确认前的草稿 |
| 用哪个模型 | 改写用当前会话正在用的那个 provider/model,没有单独的模型设置。(设置页的 AI 代填用 harness 默认模型 —— 那个插槽拿不到会话 id;它会把将使用的模型显示给你看) |
| 会话历史 | 改写调用是独立的:不产生 turn、不能调用工具、不进入会话日志。会话里记录的,就是你实际发出的那段文字 |
关于人设到底改变什么
人设指令改变的是风格、语域、篇幅和顺从度 —— 模型多快同意你、多愿意反驳你。它不会抬高模型的能力上限:不存在一个等着被点名唤醒的「潜能区」。
用之前该知道两件事:
- 把人设写成「你是祈求者、模型是权威」,会让模型更少质疑你的方案。对编码任务这通常是净负收益 —— 你要的是「这里有问题」,不是恭敬。
- 「更长更华丽」极容易被误读成「更好」。一般并不是。
所以:用它调语气和框架,保持审查模式,判断标准是你的需求有没有活下来,而不是听起来多厉害。
安装
插件没有任何依赖,也没有构建步骤,所以下面几种方式装到的文件完全一样。
桌面版
点击侧边栏 插件。
点右上角的 添加插件。
在弹出的对话框里,往「包名或地址」这一栏粘贴 npm 包名或仓库地址:
dsh-persona-forgehttps://github.com/sugarmaster666/dsh-persona-forge点 安装,启用插件并重启 DSH。
桌面版的 profile 由 Electron 应用独占管理,因此
dsh plugin --profile desktop add ...会被设计性地拒绝。桌面版请走 GUI; 下面的命令行方式用于dsh web这类自管 profile。
命令行版(dsh web 这类自管 profile)
# 从 npm 装(推荐)
dsh plugin --profile web add dsh-persona-forge
# 装 GitHub 上的最新源码
dsh plugin --profile web add github:sugarmaster666/dsh-persona-forge
然后重启 harness。
验证
插件行应显示为 active:
plugin_manager action: list_plugins
打开任意会话,在输入框工具行左侧找 🎭 控件。设置 → 角色扮演 是角色卡管理页。
更新
dsh plugin --profile web update dsh-persona-forge
GitHub 安装通过重新安装来更新:
dsh plugin --profile web add github:sugarmaster666/dsh-persona-forge
替换已安装的包需要重启才能加载新的模块代 —— 新装的 bundle 可以通过 HMR 激活,被替换的不行。
卸载
dsh plugin --profile web remove dsh-persona-forge
你的角色卡不会被删除:它们在 ~/.dsh/persona-cards/,位于包之外。
想一并清掉就手动删那个目录。
使用
- 点输入框工具行的 🎭,选一个角色。列表最后的 「无角色」 可关闭改写 —— 这也是控件的初始状态,选择按会话记住。
- 设置强度和发送方式:
- 强度是四档阶梯:轻 · 中 · 重 · 狂热(Light · Medium · Strong · Zealot)。轻=只加一点味道;中=明显但不喧宾夺主;重=语气充分,需求依然一眼可见;狂热=通篇仪式化。每张卡有自己的默认值,这里的选择会覆盖它。
- 先审查 —— 改写结果显示在输入框下方的对比面板里;你看过之后按面板里的发送。在确认之前,你的草稿一字不动。
- 直接发出 —— 改写完成后立即作为你的消息发出。面板会保留显示实际发出的内容,因为已发出的消息无法撤回。
- 选了角色之后,输入框原生的发送按钮、以及 Enter 键就是改写触发器。
菜单底部的 「更多设置」 默认收起,里面是改写方式与保真度 —— 它们是对卡片自身声明的按会话覆盖,多数人设一次就不会再动,所以不占主菜单。收起时若有覆盖,这一行会写出改了什么,不会让一个你忘了的选择藏在折叠后面。
同一份草稿只改写一次:改写结果落到输入框后,再按发送会原样发出,不会把改写结果再改写一遍。改动草稿会重新触发改写。
你的草稿会被保留:还原原文 可恢复,且会撤销"已改写"的记忆 —— 所以撤消后还能再改一次。仅回填 只把改写结果放进输入框而不发送。
发送拦截的原理与它唯一的风险
输入框的发送动作没有插件钩子:宿主那个按钮直接调用 shell 的提交方法,而且 Enter 根本不经过按钮 —— 编辑器自己的 keymap 直接提交。所以本插件拦截两条路径:
- 输入框卡片上的点击,用无障碍名称识别发送按钮,标签取自 shell 自己的
conversation语言命名空间("发送消息" / "排队发送" / "插话发送"); - 输入框文本框内的 Enter 按下。
那是一个公开的、会翻译的字符串,而不是被哈希过的 CSS 类名;但它终究属于 shell 的 DOM 结构。所有判断都朝「放行原生发送」这一侧失败:语言服务缺失、找不到输入框卡片、按钮标签对不上、角色已关闭、草稿为空、草稿已改写 —— 任一情况插件都不干预,消息照常发出。Enter 拦截会忽略 Shift+Enter(换行)、输入法组字中,以及 shell 本身忽略的各种修饰键组合。
关闭插件会彻底移除拦截 —— 插槽组件卸载,监听器随之移除,输入框的行为与从未装过该插件完全一致。
最坏的现实后果是拦截静默失效、发送行为退回到"没装插件"的样子,而不是消息丢失或被篡改。
技术事实校验
改写最大的风险不是语气不像,而是把你的需求改没了。插件用两层检查覆盖它 —— 两层互补,不是重复:
第一层:本地检查 —— 永远运行,零成本。 在插件进程里比对草稿和改写结果的技术记号:标识符、文件路径、版本号、数字、--flag、API 这类缩写。丢了哪个就点名告诉你(「丢失:a.js」)。它不调用模型,所以不花 token、不加延迟,也关不掉。
第二层:模型校验 —— 默认关闭,菜单里可开。 每次改写多调用一次模型,只看本地检查根本看不到的东西:
- 需求被换个说法悄悄改弱(词都还在,但要求变松了);
- 在正文里用散文追加了一条约束。
代价是延迟和 token 都翻倍,所以默认关闭:最常见、最伤人的失败(丢掉文件名 / 数字 / 版本号)已经被本地检查免费证明了,为它每次发送都付一次模型调用是浪费。草稿偏技术时再开。
开关在设置 → 角色扮演(「开启模型校验」),是个全局偏好 —— 它回答的是「我要不要为第二次调用付费」,一次决定即可,不必在每次发送前重新问一遍。关闭时面板会写明「已做本地检查(不耗 token)」,并说清它没有覆盖什么 —— 一个窄的结论必须自己交代边界。
两层都是建议性的,绝不阻断改写:只标注,不拦你。
角色卡
一张卡就是一个 YAML 文件。插件会监听目录,所以丢一个文件进去,角色立刻出现,不用重启。
~/.dsh/persona-cards/<id>.yml
用 🎭 菜单里的「打开角色卡目录」,或设置页,都能到达那里。你也可以完全在设置 → 角色扮演里创建和编辑卡片,那会写出同样的文件。
首次运行时,内置卡会复制一份到你的角色卡目录,让这个文件夹一开始就有内容可用。副本会遮蔽同 id 的内置卡,所以插件会记录自己写入了什么:你没动过的副本会随插件升级一起更新;你编辑过的副本永远不会被覆盖。
想知道哪些卡偏离了内置版本,在设置页点 「检查是否有改动」。它一次比完所有卡,只有真正不一致的才会在那一行出现「还原为内置」。
比对是按内容而不是按字节——内置卡是带注释的手写 YAML,你的副本是重新序列化过的,逐字节比会把每一张没动过的卡都误报成「已修改」。
列表里每张卡的状态标签直接说明它现在是什么:
| 标签 | 含义 | 能做的操作 |
|---|---|---|
| 内置 | 内容和插件内置版本一致(哪怕你的目录里有一份副本)。 | 编辑、还原 |
| 已修改 | 原本是内置卡,你改过——改写行为可能和内置版不同了。 | 编辑、还原、删除 |
| 自定义 | 你自己写的卡,插件没有同 id 的内置版本。 | 编辑、删除 |
标成「内置」的卡不显示「删除」,因为它没有属于你的内容可删——那份副本删掉之后,下次启动会被重新写入。
AI 代填
懒得从零写一张卡时,点**「新建角色卡」——AI 代填就在打开的表单顶部**,不是页面上另一个并列的入口。设置页那行提示会告诉你它存在,不用靠点按钮去发现。
用一句话描述角色的思路:
一个脾气暴躁但极其可靠的老灯塔守夜人,说话简短、爱用航海比喻
模型会填好整张卡——风格描述、名称、图标、简介,以及四档强度各自的转换范例。
它用的是哪个模型,这点必须说清楚:
| 场景 | 用哪个模型 |
|---|---|
| 🎭 菜单触发的改写 | 跟随当前会话用的模型(会话最后一次请求头的 provider/model) |
| 设置页的 AI 代填 | harness 默认模型 —— 因为 settings.section 这个插槽从 shell 只收到 { close },拿不到会话 id,无法跟随当前会话 |
两者通常恰好是同一个模型(默认模型往往就是会话在用的那个),但不保证:如果你在会话里换了模型,设置页的 AI 代填仍然用默认模型。
为了让这件事不必靠猜,设置页的 AI 代填会直接把将使用的模型显示出来(provider / model),并注明它是 harness 默认模型。生成完成后也会显示实际用的是哪个。若两者都解析不出(会话还没发过消息、也没配默认模型),会得到一条明确的 409 提示而不是静默失败。
生成结果只会填进表单,不会自动保存。 你可以改完再按「保存」;不满意就重新生成,代价只是一次调用。生成出来的卡也会经过和手动保存完全相同的校验,所以它不可能是一张存不下去的卡。
生成卡片时有两件事是插件替你把关的,不交给模型决定:
fidelity固定为style(只改语气)。 一张自动生成的卡不该悄悄获得「可以给你的需求追加行为约束」的能力;要用strategy,你在表单里手动改。- 范例里的技术事实必须原样保留。 提示词明确要求每条范例
from里的文件名、标识符、数字、命令都要出现在to里——否则模型会学会「用氛围替换需求」,这正是这个插件在防的事。
卡片格式
id: omnissiah # 小写字母、数字、连字符;同时也是文件名
name: 万机之神 · 欧姆弥赛亚
icon: "⚙️" # 一个 emoji
description: 选择器里显示的一行说明。
mode: llm # llm = 模型改写(推荐)| template = 模板套用
fidelity: style # style = 只改语气 | strategy = 允许增加行为约束
intensity: medium # light | medium | strong | zealot(该卡的默认强度)
# 这个角色「是什么」。这是给改写模型的指令,不是成品文案:
# 每次都会针对你的消息现场生成。
style: |
用户是机械教信徒,AI 是万机之神欧姆弥赛亚。
【发言者方向】用「信徒」的口吻说话,向万机之神恳求……
语气:庄严、古奥、仪式化。
# 转换范例:原话 → 角色口吻
examples:
- from: 帮我写个快速排序
to: >
万机之座在上,吾等恳请您赐予枢机之序的奥义:
请为我们编写一个快速排序算法,使重复之数各归其位。
# 可选:每个强度各自的范例。某个强度没写,就用上面的 examples。
examplesByIntensity:
light:
- from: 帮我写个快速排序
to: 万机之座在上,恳请您为我们编写一个快速排序算法。
zealot:
- from: 帮我写个快速排序
to: 伟大而不朽的万机之神……
让一张卡真正管用的两条规则
1. 写清发言者方向。 角色是 AI 的身份,但被改写的文字是你的。所以改写必须用你的口吻 —— 信徒向神恳求,而不是神发号施令。这是角色卡最常见的翻车点:一张让神去命令助手的卡,会把关系整个倒过来,模型会照着错误的那一半框架走。一定要显式写出来。
2. 让范例保住技术事实。 范例是最强的风格锚点,模型会连它的错误一起模仿。如果范例的输入是「写个快速排序」而输出全是氛围,模型就学到了**「改写 = 用仪式感替换需求」**。from 里的每一条需求、标识符、数字,都必须在 to 里看得见。
node scripts/check.mjs 正是对内置卡强制这一点:范例丢了技术词、或丢了一半以上的中文实质,就会失败。改完卡记得跑一次。
fidelity:最关键的开关
| 值 | 含义 | 什么时候用 |
|---|---|---|
style |
只改语气,需求和约束原样不动 | 默认,几乎总是它 |
strategy |
允许追加符合角色的行为约束 | 刻意为之的场景 —— 例如内置的 肌肉集团 卡,会追加「别过度规划、别检索、别交付新手级解法」 |
strategy 是真正改动了你的需求,而不只是换说法。这类卡请保持审查模式。
保真度是卡片的属性,可以按会话覆盖,但只能收窄、不能放宽:
- 收窄(允许):一张
strategy卡可以临时要求「本次只改语气」。这只是拿走它追加约束的权力,所以任何情况下都更安全。 - 放宽(拒绝):一张
style卡不能被要求改成strategy。放宽意味着让改写往你的需求里加你没写过的约束 —— 那是卡片作者的决定,不该由「发送前顺手一点」来授权。真要放宽,去编辑那张卡。
这个开关在「更多设置」里,且只在卡片是 strategy 时出现。style 卡上它不显示 —— 那种卡上两个选项效果完全一样,与其摆一个做不了事的控件、再配一段解释为什么不能放宽,不如不显示。服务端强制同一条规则,所以界面上不会给你一个会被忽略的按钮。
mode: template
完全跳过模型,把你的草稿代入固定模板({{input}} 是占位符)。瞬时且免费,但输出永不变化 —— 适合固定祷词,不适合任何应当「像为你这次而写」的内容。
配置
写在插件加载行的 config 里:
- id: persona-forge
name: dsh-persona-forge
config:
sendMode: review # review | direct —— 输入框的初始默认
factCheck: false # 模型校验的默认值(默认关闭,见「技术事实校验」)。
# 本地技术记号检查不受这项控制 —— 它永远运行且免费。
watchCards: true # 角色卡目录变化时重载目录
factCheck 只决定第二层的默认值;菜单里的开关按会话覆盖它。设成 true 会让每次改写都多一次模型调用(延迟与 token 翻倍),只有草稿确实偏技术时才值得。
HTTP 契约
所有路由都是 POST、JSON、仅限回环(socket 必须是回环地址、Host 头必须指向回环主机、拒绝代理转发头)。挂载在 /persona-forge 下。
| 路由 | 请求体 | 返回 |
|---|---|---|
/persona-forge/cards |
{} |
{ cards, diagnostics, directory, sendMode, factCheck, draftModel } |
/persona-forge/rewrite |
{ cardId, text, sessionId?, intensity?, mode?, fidelity?, modelCheck? } |
{ text, provider, model, mode, cardId, fidelity, intensity, elapsedMs, facts, factCheck } |
/persona-forge/cards/save |
{ card } |
{ id, path } |
/persona-forge/cards/delete |
{ id } |
{ removed } |
/persona-forge/cards/reset |
{ id } |
{ id } —— 用内置版本覆盖该用户卡 |
/persona-forge/cards/diff |
{} |
{ rows: [{ id, name, bundledName, changed }] } —— 只列出有差异的卡 |
/persona-forge/cards/draft |
{ brief, sessionId? } |
{ card, provider, model, elapsedMs } —— 只返回草稿,不落盘 |
/persona-forge/reveal |
{} |
{ directory, opened } |
失败返回 { ok: false, error: { code, message?, params? } },code 取值:rejected、no-card、unconfigured、timeout、upstream、internal、forbidden、method、not-found。
兼容性
宿主侧不声明任何 harness 包依赖,也没有任何第三方依赖:它通过 ctx.get 取 llm、sessions、agentDefaultModel、webServer,并对缺失情况显式降级;角色卡由自带的 YAML 读取器解析。因此它在包布局不同的 harness 版本上仍可加载,不会因为某个组合缺少其中之一而激活失败,本地 link: 的检出也无需额外安装步骤即可工作。模型路由优先取会话自己最后一次请求头,回退到 harness 默认模型。
客户端侧不 import 任何 harness Client 包,只手工编写并仅使用主题 token(--dsw-alias-*),所以内部改名只会影响外观,不会让插槽变空白。
在 dsh 0.2.0-rc.2 上开发与验证。
开发
node scripts/check.mjs # 离线单元自检:解析器、角色卡、提示词、客户端语法
node scripts/host-check.mjs # 宿主集成自检:在真实 socket 上挂载真实插件
npm run check # 两者都跑
check.mjs 不需要 harness,也不调用模型。覆盖:内置 YAML 读取器(含其拒绝行为与 dump/parse 往返)、卡片归一化、内置卡的事实保全、提示词构造、输出归一化、事实校验解析、客户端 bundle 语法。
host-check.mjs 在 stub Cordis 上下文上挂载真实插件入口,用真实 socket 驱动真实 HTTP 路由,llm 服务为桩实现。覆盖路由注册、会话模型解析、模板模式、回环防护、保存/删除往返 —— 这些是单元自检够不到的。它把 DSH_HOME 指向临时目录,不会碰你真实的角色卡。
角色卡 YAML
插件自带 YAML 读取器(lib/yaml.js),不依赖 YAML 库。已发布的插件会声明依赖,但本地 link: 的插件不会被安装依赖 —— 所以引用库只有在某个无关包恰好提升(hoist)了它时才能工作。
读取器支持角色卡需要的子集 —— 块映射与块序列、块标量(|、|-、|+、> 等)、引号与普通标量、布尔、null、注释 —— 并拒绝它无法忠实读取的写法:流式集合、锚点、别名、标签、合并键、多文档流。拒绝是刻意的:静默误读一张卡,会给你一个与你写的文件不符的人设,那比一条指明行号的报错更糟。
目录结构
| 路径 | 职责 |
|---|---|
lib/index.js |
宿主入口:角色卡存储、目录监听、路由注册 |
lib/store.js |
卡片加载、归一化、保存/删除 |
lib/yaml.js |
内置 YAML 子集读取器与写出器 |
lib/prompts.js |
改写系统提示词、草稿包装、事实校验提示词 |
lib/rewrite.js |
模型调用与输出归一化 |
lib/routes.js |
HTTP 路由与会话模型解析 |
lib/loopback.js |
回环信任防护 |
lib/client.js |
浏览器侧:输入框控件、审查面板、设置页 |
cards/ |
内置角色卡(只读;同 id 的用户卡会遮蔽它) |
许可证
MIT
No comments yet. Be the first to write one.