koishi-plugin-chatluna-dsh-adapter
把 DeepSeek Harness(DSH) 接进 ChatLuna。默认是「工具模式」:普通聊天仍然走你原来的模型、原来的预设、原来的 chatluna-character;只有你在消息里带上关键词时,模型才会看到一个 harness 工具,把任务交给 Harness 执行,结果再回到原模型/角色管线,由 ChatLuna 渲染到 QQ。
也保留「模型适配器模式」:把 ChatLuna 的模型直接选成 dsh/deepseek-harness,让 Harness 当对话主脑。两种模式可以共存,但通常只需要工具模式。
从 0.3.1 起,每个会话有独立工作区:部署者(trustedUsers)使用基础 workspace 和全部工具;其他私聊使用 private/<QQ号>;群聊使用 groups/<群号>(也可按成员再分)。访客可以正常用 Harness,但越界操作不会自动批准。
⚠️ 如果你同时用
chatluna-character请在
chatluna-character里关闭 「是否启用<think>标签」(配置项toolCallReplyThinkTag,默认是开启的)。开启时,它的
character_reply工具会强制要求think字段。deepseek-flash这类模型有时会漏掉,随后报:
missing character_reply tool call- 或
invalid character_reply tool call ... required property "think"这时 Harness 其实已经执行完了(日志里有
harness task done),失败的是角色插件生成最终回复。关闭该选项后不再要求think;如果一定要保留 think 标签,就换更强的 tool-calling 模型,并在预设/作者注里明确要求每次character_reply都带"think": "..."。
两种模式
工具模式(默认,推荐)
QQ / NapCat
│
▼
Koishi ──► ChatLuna(原模型 / 原预设 / chatluna-character)
│
│ 当前消息含 #harness
▼
harness 工具(本插件)
│ ACP over stdio,每次调用一个 dsh 进程
▼
DeepSeek Harness
├─ pwsh / fs / web / skill / subagent / workflow ...
└─ 返回任务结果文本
│
▼
原模型继续生成(需要时再调 character_reply)
│
▼
ChatLuna renderer ──► QQ
关键点:
- 不改模型。ChatLuna 和
chatluna-character继续用你原来的模型;character_reply仍由原模型生成。 - 默认不经过 Harness。只有最后一条用户消息包含
triggerKeywords(默认#harness、harness、用harness、调用harness)时,harness工具才会被授权给这一轮。 - 关键词门禁不依赖工具 selector。
chatluna-character传给selector(history)的历史不含当前消息,所以本插件用一个前置 Koishi middleware 按会话“武装”工具,再由authorization(session)检查。这样角色插件也能正确触发。 - 工具由 DSH 执行。模型只负责决定“要不要调用”和“任务描述是什么”;文件、命令、网页、子代理都在 DSH 内完成。
- 工作区按会话隔离。DSH 进程的
cwd和sandbox-policy.workspaceRoot都指向该会话的文件夹;trustedUsers永远用基础workspace,所以即使人在群里,也按“部署者”处理。 - 访客能干活,但不能越界。访客仍可使用 shell/fs/web 等工具,但 DSH 的“离开工作区”审批会被拒绝;只有 owner 的审批会放行。
模型适配器模式(可选)
ChatLuna 预设(已渲染的 SystemMessage)
│
▼
本适配器(平台 dsh / 模型 deepseek-harness)
│ ACP stdio + 临时 cordis.patch.yml
▼
DeepSeek Harness(persona = ChatLuna 预设)
│
▼
ChatLuna renderer ──► QQ
适合“整个机器人的人格就是 Harness”的场景。代价是 ChatLuna 的工具不会自动变成 DSH 工具,且每轮要启动一个 DSH 进程。工具模式没有这些问题。
安装
发布到 npm 后,在 Koishi 实例目录(例如 C:\Users\<你>\AppData\Roaming\Koishi\Desktop\data\instances\default)直接安装:
npm install koishi-plugin-chatluna-dsh-adapter
Yarn 4(Berry):
corepack yarn add koishi-plugin-chatluna-dsh-adapter
也可以在 Koishi 控制台的「插件市场」里搜索 chatluna-dsh-adapter 安装。
开发阶段或离线安装可以用 tgz。Yarn 4 必须写成 包名@file:路径:
corepack yarn add "koishi-plugin-chatluna-dsh-adapter@file:./koishi-plugin-chatluna-dsh-adapter-0.3.1.tgz"
如果 Yarn 报 YN0027 / YN0035,或者把路径当成 npm 包名去 registry 找,就是这个格式问题。也可以直接用 npm:
npm install "./koishi-plugin-chatluna-dsh-adapter-0.3.1.tgz"
然后在 Koishi 控制台启用 chatluna-dsh-adapter。不需要去 ChatLuna 或 chatluna-character 里改模型。
配置
| 配置项 | 默认值 | 说明 |
|---|---|---|
executable | dsh | DSH 可执行文件。桌面版填 F:\kunfox\deepseek\DeepSeek Harness.exe。 |
cliPath | 空 | 桌面版的 cli.js 路径;使用 PATH 里的 dsh 时留空。 |
profile | acp | DSH profile。 |
dshHome | 空 | DSH_HOME。留空继承 Koishi 进程环境;Windows 默认是 C:\Users\<你>\.dsh。 |
workspace | 空 | 基础工作区;留空用 <koishi baseDir>/data/chatluna-dsh。owner 直接用这里,访客用它的子目录。 |
groupWorkspaceMode | shared | shared = 每个群一个共享文件夹;per-user = 每个群成员在群文件夹下再分自己的子目录。 |
maxWorkspaceSizeMB | 0 | 访客私聊/群文件夹的软上限(MB),0 = 不限。超过后会中止当前 Harness 任务。建议 256–512。 |
ownerWorkspaceSizeMB | 0 | owner 基础工作区的软上限(MB),0 = 不限。 |
workspaceQuotaCheckIntervalMs | 1500 | 大小检查间隔(毫秒)。调小更及时,但大目录会更耗 I/O。 |
model | 空 | DSH 模型,可填 deepseek-flash、deepseek-account/deepseek-flash 或 ["deepseek-account","deepseek-flash"]。适配器会按 DSH 会话暴露的模型选项自动解析;留空用默认路由(缺凭据时会失败)。 |
toolPermission | trusted | allow = 所有人可用工具;trusted = owner 用基础工作区+全部工具,访客按 guestToolAccess;reject = 所有人禁用工具。 |
guestToolAccess | true | 非 owner 是否也能使用工具;他们只能在自己的私聊/群文件夹里操作。 |
trustedUsers | [] | 完全信任的 Koishi userId:基础工作区 + 全部工具;在群里也按 owner 处理。 |
trustedGuilds | [] | 允许使用 Harness 的群 guildId;不授予基础工作区,成员仍用群文件夹。 |
requestTimeout | 180000 | 单次 DSH 调用超时(毫秒)。 |
contextSize | 128000 | 仅模型适配器模式使用:向 ChatLuna 声明的上下文长度。 |
toolEnabled | true | 是否注册 harness ChatLuna 工具。 |
toolName | harness | 注册给 ChatLuna 的工具名。 |
triggerKeywords | ["#harness","harness","用harness","调用harness"] | 只有最后一条用户消息包含其中之一,才授权工具。 |
toolSystemPrompt | 空 | Harness 任务执行器的系统提示词;留空用内置的简洁执行器提示。 |
characterThinkTagReminder | true | 仅用于在配置页显示 chatluna-character 的 <think> 兼容提醒,不影响运行。 |
debug | false | 输出关键词匹配、授权、任务耗时等日志。 |
桌面版示例
executable: F:\kunfox\deepseek\DeepSeek Harness.exe
cliPath: F:\kunfox\deepseek\resources\app.asar\dsh\node_modules\@deepseek-ai\dsh-desktop-host\lib\cli.js
profile: acp
dshHome: C:\Users\MECHREV\.dsh
workspace: D:\koishi-dsh-workspace
model: deepseek-flash
toolPermission: trusted
guestToolAccess: true
groupWorkspaceMode: shared
trustedUsers:
- '你的QQ号'
trustedGuilds: []
requestTimeout: 300000
debug: true
executable 和 cliPath 必须成对:
- 桌面版:
executable指DeepSeek Harness.exe,cliPath指...\dsh-desktop-host\lib\cli.js。 - 命令行版:
executable指...\runtime\cli\bin\dsh.cmd,cliPath留空。
Koishi 进程的 PATH 通常没有 dsh,所以不要只填 executable: dsh 就指望它能找到。
用法
在 QQ 里正常聊天即可。需要 Harness 时,在消息里带关键词:
#harness 看一下 D:\koishi-dsh-workspace 里有哪些文件
harness 帮我查一下今天的日期,用 pwsh
用harness 把 https://httpbin.org/uuid 的内容抓下来
模型会把这个任务写成 harness 工具调用;插件启动 DSH 执行,把结果作为工具输出交回模型。chatluna-character 随后照常生成 character_reply,所以角色人格、分段、图片/语音渲染都不变。
没有关键词的消息不会看到 harness 工具,正常走原模型。
关于 chatluna-character
chatluna-character 的 experimentalToolCallReply 要求模型最终返回 character_reply 工具调用。工具模式下,这个调用仍由原模型生成,Harness 只是一个额外的普通工具,因此不会再出现 missing character_reply tool call。
前提:
chatluna-character的toolCalling保持开启;- 原模型支持 tool calling;
- 不要把
chatluna-character的模型改成dsh/deepseek-harness(那是模型适配器模式,适配器只声明TextInput,不会产生character_reply)。
如果仍报错,把 debug 打开,确认日志里出现了 harness authorization allowed=true,并且 chatluna-character 自己没有把模型切到 dsh。
文件夹位置与大小限制
以 workspace: D:\koishi-dsh-workspace 为例:
| 会话 | 实际目录 |
|---|---|
owner(trustedUsers) | D:\koishi-dsh-workspace 本身 |
| 私聊访客 | D:\koishi-dsh-workspace\private\<QQ号> |
群聊(shared,默认) | D:\koishi-dsh-workspace\groups\<群号> |
群聊(per-user) | D:\koishi-dsh-workspace\groups\<群号>\<QQ号> |
private / groups 都是第一次有对应会话调用 Harness 时才创建;所以现在只看到 private、没有 groups 是正常的。
大小限制在配置里:
maxWorkspaceSizeMB: 512 # 访客私聊/群文件夹上限,0 = 不限
ownerWorkspaceSizeMB: 0 # 你自己的基础工作区上限,0 = 不限
workspaceQuotaCheckIntervalMs: 1500
实现方式:任务开始前先量一次目录;任务运行中每隔 workspaceQuotaCheckIntervalMs 递归统计文件大小;超过上限就中止 DSH 进程并报 harness workspace quota exceeded。该限制只作用于工具模式的 runTask;模型适配器模式不经过这里。
注意这是软保护,不是 NTFS 硬配额:
- 单个超大文件可能在两次检查之间短暂超限;任务会被中止,但已经写下的部分文件不会自动删除,需要你手动清理。
- 它只统计当前会话文件夹,不统计 DSH 进程在文件夹外创建的临时文件。
- 需要真正的硬上限,应该给 Koishi 用独立 Windows 账户并配 NTFS 配额/FSRM,或用固定大小的 VHD;插件本身做不到硬隔离。
查看当前占用:
Get-ChildItem -Recurse D:\koishi-dsh-workspace |
Measure-Object Length -Sum |
Select-Object Count,Sum
安全与隔离
- owner(
trustedUsers):使用基础workspace,拥有全部工具;在群里触发也按 owner 处理。只把自己和完全信任的人放进去。 - 私聊访客:工作区是
<workspace>/private/<QQ号>。第一次调用时自动创建。 - 群聊访客:工作区是
<workspace>/groups/<群号>;同一群成员共用这个文件夹(groupWorkspaceMode: shared)。如果不想让群友互相看到文件,改成per-user。 - 访客工具:默认
guestToolAccess: true,所以访客也能用 shell/fs/web 等工具。DSH 会把写入/删除限制在各自文件夹,进程cwd也在那里;但读取不受 DSH sandbox 限制(见下面的“读隔离”)。 toolPermission: reject会禁用所有工具,只保留聊天;allow对访客仍只给文件夹,不会给基础工作区。- 读隔离(重要):DSH 当前只围栏写/删除与 cwd,不围栏读。访客进程与 Koishi 使用同一个 Windows 账户,因此能读取该账户可读的任何文件,包括基础
workspace、用户目录和 DSH 凭据。不要把密钥、.env、个人资料放在同一账户可读的位置;公开机器人应先设guestToolAccess: false,或把 Koishi 放进独立 Windows 账户/虚拟机。真正的读隔离需要受限 MCP 文件工具或 AppContainer,当前版本尚未实现。 - 不要把基础
workspace指向含密钥、.env、个人资料的目录。owner 能访问整个基础工作区;访客的写入被限制,但读取仍受同一账户权限支配。 - 群共享文件夹的副作用:群友可以读写别人留下的文件;需要隐私时用
per-user,或只让 owner 使用。 - 访客可以发起耗时任务,目前没有每用户限流;公开群建议配合 Koishi 的频率限制或只开
trustedGuilds。 - 插件不接触 API Key;DSH 使用
dshHome下已保存的凭据。
扩展接口
插件在 Koishi 上暴露 ctx.dshHarness,给以后单独的 Harness 配置插件使用:
ctx.dshHarness.version // 2
ctx.dshHarness.toolPluginIds // DSH profile 里可被策略开关的工具插件 id
ctx.dshHarness.config // 当前配置
ctx.dshHarness.isOwner(session)
ctx.dshHarness.isTrusted(session) // 兼容别名,语义同 isOwner
ctx.dshHarness.resolveScope(session)
// { kind: 'owner'|'private'|'group', workspace, allowTools,
// approvePermissions, fullAccess, label, ... }
await ctx.dshHarness.runTask(task, { session, signal })
const off = ctx.dshHarness.registerPermissionProvider(async (session, task) => {
// 返回 false 拒绝;true/undefined 放行
})
const off2 = ctx.dshHarness.registerPostProcessor(async (text, context) => {
// context.scope 含本次工作区;可以改写 Harness 返回的文本
return text
})
const off3 = ctx.dshHarness.registerScopeResolver((session, base) => {
// 返回 { workspace, allowTools };访客的 workspace 不允许逃出 base
return null
})
const off4 = ctx.dshHarness.registerToolPolicy((session, scope) => {
// 未来 DSH 插件可在这里按私聊/群限制可用工具
return { enabledToolIds: ['tool-fs', 'tool-web'] }
// 或 { disabledToolIds: ['tool-pwsh'] }
})
ctx.dshHarness.registerTool('my_tool', myChatLunaTool)
registerScopeResolver在默认工作区计算之后执行,可覆盖workspace/allowTools;访客返回base之外的绝对路径会抛错,owner 不受限。registerToolPolicy可返回enabledToolIds(白名单)或disabledToolIds(黑名单);它影响的是 DSH profile 里的工具插件,不是 ChatLuna 工具。registerPermissionProvider在每次任务前执行,任一返回false就抛错;registerPostProcessor在结果返回前执行;registerTool直接转给ChatLunaPlugin.registerTool。- 这些接口是 v2,后续尽量只增不改。
已知限制
- 不是逐 token 流式:DSH 的 ACP 只在消息提交时发
agent_message_chunk,所以 ChatLuna 会先显示“正在输入”,然后一次性收到整段工具结果。 - 每次调用启动一个 DSH 进程:实测一次简单任务约 5–17 秒(取决于工具和模型);首次运行 profile 可能触发依赖安装,会更慢。
- 关键词只看用户消息:由前置 middleware 判断
session.content;引用、转发里出现关键词也可能触发。 - 工具模式需要原模型支持 tool calling。如果原模型不支持,
harness工具不会被调用。 - ChatLuna 的工具不会自动变成 DSH 工具,反之亦然;两边工具是分开的。
- 群文件夹默认共享:同群成员会看到同一个
groups/<群号>下的文件;需要隔离时设groupWorkspaceMode: per-user。 - 隔离依赖 DSH sandbox:它只限制写入/删除与 cwd,不限制读取,也不是完整容器;对恶意用户仍建议独立账户/虚拟机。
- 目前没有每用户并发/频率限制:ChatLuna 的全局并发限制仍生效,但单个用户连续触发会连续启动 DSH 进程;公开机器人建议在 Koishi 侧再加限流。
{{char}}不会自动映射(仅模型适配器模式相关):ChatLuna 变量表里是{{name}}和{{user}},未识别的{{char}}会渲染成空串。- 目前只在 Windows + DSH 0.2.1-rc.5 + ChatLuna 1.4.4 + chatluna-character 0.0.235 上验证。
排错
| 现象 | 处理 |
|---|---|
missing character_reply tool call | 先确认 chatluna-character 的模型不是 dsh/deepseek-harness;工具模式下保持原模型,toolCalling 开启。若日志里已经有 harness task done,多半是下面的 think 问题。 |
invalid character_reply tool call ... required property "think" | 在 chatluna-character 关闭「是否启用 <think> 标签」(toolCallReplyThinkTag: false),或换更强模型并在预设中强制带 "think": ""。 |
dsh exited with code 1,stderr 是 'dsh' 不是内部或外部命令 | 填桌面版 executable + cliPath,或 dsh.cmd 的完整路径。 |
no API key for provider route "deepseek-official" | 设 model 为有凭据的模型,如 deepseek-flash 或 deepseek-account/deepseek-flash。 |
unknown model option: deepseek-flash | 0.2.1 已支持裸模型名;若仍出现,说明 DSH 的模型列表里没有该模型,填 deepseek-account/deepseek-flash 或先跑 dsh --profile acp --dump-config 确认。 |
模型完全不知道有 harness 工具 | 打开 debug,看日志有没有 harness authorization allowed=true;确认消息里关键词、toolEnabled、toolPermission、trusted 列表。 |
| 每次普通聊天也都调用 Harness | 检查是否把 triggerKeywords 设得太宽(例如只写 h),或 toolPermission: allow 且模型过于主动;把关键词改成更明确的 #harness。 |
Yarn 安装报 YN0027/YN0035 | 用 包名@file:./xxx.tgz,或改用 npm。 |
模型列表里没有 dsh | 只影响模型适配器模式;工具模式不需要选 dsh 模型。确认插件已启用、ChatLuna 已加载。 |
验证记录
- 工具模式真实 DSH:消息
#harness 现在几点?,Harness 执行Get-Date -Format o,返回2026-11-03T18:42:57.0117758+08:00,耗时约 5.9 秒;无关键词时授权为false。 - 模型适配器模式:注入 persona 后模型回答
I am TestPersona, and my color is teal.,stopReason: end_turn。 - 非可信用户写文件被禁用工具插件阻止;可信用户可正常创建文件。
- 0.3.0 隔离测试:访客私聊
Get-Location返回D:\koishi-dsh-workspace\private\guest-1;尝试写D:\koishi-dsh-workspace\escape-guest.txt被 sandbox/approval 拒绝,文件未创建。 - 0.3.0 owner 测试:
trustedUsers用户在群里仍使用基础D:\koishi-dsh-workspace,Get-Date正常返回。 - 0.3.0 读侧测试:访客能读取基础 workspace 里的测试文件;这不是 bug,而是 DSH sandbox 只围栏写/删除的已知限制(见“读隔离”)。
- 0.3.1 配额测试:1 MB 上限下让 Harness 写 2 MB 文件,任务返回
harness workspace quota exceeded;文件已写入 2 MB,说明轮询式配额会拦截任务但不能阻止瞬时大文件,需要手动清理。
License
MIT