- JavaScript 99.2%
- PowerShell 0.8%
| lib | ||
| tests | ||
| .gitignore | ||
| install.ps1 | ||
| package.json | ||
| README.md | ||
dsh-llm-hiagent
原生 DeepSeek Harness LLM 提供方插件,把同济 HiAgent(会话式智能体 API)映射为 DSH 的
llm 服务适配器。不需要单独的 Python 代理进程——转换逻辑全部在进程内完成。
适配自 liquidhelium/tongji-hiagent-proxy
(原为 OpenClaw 设计的 FastAPI 代理),核心机制相同,但以原生 LlmAdapter 实现:
- 会话方式(默认:每次新建会话 + 完整上下文):默认
noReuse: true时,不复用 HiAgent 会话——每次请求都新建一个AppConversationID,把完整对话历史(含[user]/[assistant]/[tool:...]段落)作为只读上下文一次性注入首条消息,彻底摆脱 服务端 ~3 轮滑动窗口对记忆的锁死,也不依赖hiagent-conversations.json映射。需要旧式 复用时设noReuse: false(见「记忆与滑动窗口」)。 - 会话映射(
noReuse: false旧模式):按消息哈希(到首个 assistant 轮为止)复用 HiAgent 的AppConversationID,映射持久化到$DSH_HOME/hiagent-conversations.json。 - 工具调用:模型使用 DeepSeek 原生标准
<│DSML│tool_calls>格式。每轮刻意注入 DSML 工具调用格式指令(## Tool calling format+ 完整 DSML 示例块),并在重发的历史里把 assistant 的工具调用序列化为原生 DSML(toolCallsToDsml)而非 OpenAI wire JSON—— 这样历史本身就成为可供模仿的 DSML 教师样例,避免模型改去模仿 wire JSON 数组。模型输出 DSML 块后由parseDsmlToolCalls解析为 DSH 的tool-call块。每轮重注入工具 schema 与 DSML 指令是因为 HiAgent 会话有内置滑动窗口(实测只保留最近约 3 轮),首轮注入的内容 会滚出窗口。 - 思考(
think_message事件):v2 规范下思考由独立think_message增量下发,插件按事件 类型路由,无条件进入 DSH 的reasoning块;message增量才是可见text。不依赖 start/end 段落状态机(服务端可能在思考中途错误插入 end/message_start,依赖状态机会泄漏思考到可见文本)。 请求侧不回传 reasoning(HiAgent 会话记忆自带)。 - 真流式:HiAgent 网关在收到
Accept: text/event-stream头时会把底层 Dify SSE 流增量冲刷 (实测首字节 ~1s、逐 token 事件);插件实时渲染可见文本与思考,并实时遮蔽DSML 工具区工具区域,结束时做权威解析。作者注:流式 masker 只做 DSML 屏蔽,不再做任何 think 标签剥离——思考只来自think_message事件,parseAnswer也直接解析原始 DSML 调用。 - DSML 解析与截断恢复:解析器只认 DeepSeek 原生
<│DSML│tool_calls>,并把string="true|false"正确还原为字符串/JSON 参数;不注入任何自定义[TOOL_CALL_START]格式。针对服务端在 think 后随机吞掉首帧开头字节的问题,解析器还支持残缺开标签的 relaxed 恢复。 - 上下文研究结论(实测):内部模型为 ~128k 上下文级别(>~20 万 token 的提示会返回"大模型
接口调用出错,请联系管理员"作为答案,~14 万 token 正常);
contextWindow已按此声明为 128000,DSH 不再每两三轮就压缩。会话滑动窗口约 3 轮,输入 token 数不随会话无限增长。 - 会话轮换续接:复用会话超过
maxReuseTurns(即插件设置里的「对话轮数保留」,默认 100、最大 100)轮后,插件会新建一个 HiAgent 会话并处理当前请求。这样重置了服务端 ~3 轮的滑动窗口,且输入规模始终有界(0 关闭轮换)。 - 首条消息限流:新建 HiAgent 会话时只发送最近
maxInitialMessages(默认 120)条消息、 估算 token 不超过maxInitialTokens(默认 100000)、字符不超过maxInitialChars(默认 800000),避免把 DSH 超长历史一次性塞给 HiAgent 导致上下文超限。正常会话远低于这些上限; 各值设 0 即关闭对应限制。
安装
-
把本包放进 profile 的 node_modules(本机已就位):
~/.dsh/profiles/node_modules/dsh-llm-hiagent/ -
在
~/.dsh/profiles/web/cordis.patch.yml加入一行(本机已加):- insert: - id: llm-hiagent name: dsh-llm-hiagent config: apiKey: <hiagent-api-key> # 或 apiKeyEnv: HIAGENT_API_KEY baseURL: https://agent.tongji.edu.cn/api/proxy/api/v1/ injectSystemPrompt: true # agent 未内置 prompt 时为 true modelId: hiagent displayName: HiAgent (Tongji) contextWindow: 128000 maxTokens: 4096 -
重启 dsh(
Ctrl+C后重新运行启动命令),然后在模型选择里选 HiAgent (Tongji)。
配置界面
- 设置 → 插件 → 可配置:有一张 "HiAgent (Tongji)" 卡片(client 半实现,
lib/client.js),可编辑 模型列表(每个模型绑定一个 API Key,对应一个 HiAgent agent)+ 共享配置,写入settings.yaml的llm-hiagent:段,实时生效。每个模型行的 API Key 是写入型字段:填了新值才写、留空保持不变 (设置线路上密钥只写不读),所以后续修改其他字段不会清掉已配置的 Key。 - 也可以直接编辑
settings.yaml的llm-hiagent:段(组合层配置为基线,用户层覆盖)。 - 提示:宿主代码与首次新增的 client 半需要重启才加载(client-modules 图在插件重新激活时
增量扫描);之后修改
lib/client.js刷新页面即可生效。
多模型
HiAgent 每个发布出去的 agent 有独立 API Key,底层模型可能不同。配置 models 列表即可:
llm-hiagent:
models:
- id: hiagent-glm # 模型选择器里显示为 hiagent/glm
name: HiAgent GLM
contextWindow: 128000
maxTokens: 4096
maxReuseTurns: 50 # 可选:该模型自己的对话轮数保留(覆盖顶层,不设则用顶层)
noReuse: false # 可选:该模型关闭"每次新建会话"(覆盖顶层;不设则继承顶层)
- id: hiagent-ds
name: HiAgent DeepSeek
contextWindow: 128000
maxTokens: 8192
modelKeys: # 每个模型的独立 API Key(写入型,UI 里填)
hiagent-glm: <agent1-key> # 该 agent 的 API Key
hiagent-ds: <agent2-key>
- 每个模型的 Key 放在顶层
modelKeys字典里(按模型 id),而不是models[i].apiKey。原因: 配置线路上密钥只写不读(redacted),若把 Key 塞在models数组里,任何一次整表保存都会把已配置的 Key 静默清掉(旧 bug 的根因)。modelKeys逐条写入,互不影响。旧式models[i].apiKey仍被读取 (向后兼容),但modelKeys优先。 - 每个模型的
apiKey/modelKeys可留空 → 继承顶层apiKey(或apiKeyEnv解析)。 - 会话映射按模型隔离(会话 key 带模型盐),不同 agent 的对话不会串。
- 不配置
models时退化为顶层字段定义的单个默认模型(向后兼容)。
记忆与滑动窗口
HiAgent 会话有内置滑动窗口(实测只保留最近约 3 轮),而插件为了复用会话会把单次请求只发增量——
这样模型能看到的记忆被锁死在约 3 轮内(会话越长越明显)。默认的 noReuse: true 模式直接绕开
这个问题:每次请求新建 HiAgent 会话,并把 DSH 侧完整的历史消息序列化进首条消息作为只读上下文,
模型每次都能看到全部记忆,不受服务端窗口限制;代价是每次都要重发完整上下文(token 成本上升)。
noReuse: false 时回到旧式复用:把单次请求只发增量,通过 maxReuseTurns(默认 100,即「对话轮数
保留」)控制复用轮数,达到上限后新建 HiAgent 会话以重置滑动窗口。每个模型可通过
models[i].maxReuseTurns、models[i].noReuse 单独覆盖。
会话复用 key 把 DSH 会话标识(options.sessionId) 并入盐:会话 key = hash(模型盐 + sessionId +
消息前缀)。DSH 的 fork 会给子会话铸造新 id,因此 fork 出的分支即使与前缀消息相同,也会映射到
独立的 HiAgent 会话,不会与父分支串上下文(否则两个分支共享同一个 AppConversationID,模型
会看到另一个分支的记忆——实测工具调用结果错位、上下文重放的根源)。同一会话内 sessionId 稳定,
复用行为不变;无 sessionId 的一次性调用回退到纯消息哈希。
配置项
| 字段 | 默认 | 说明 |
|---|---|---|
apiKey |
— | 默认模型/回退的 HiAgent API 密钥(直接内联) |
apiKeyEnv |
— | 密钥的环境变量/凭据引用(与 apiKey 二选一,优先 apiKey) |
models |
— | 模型列表,每项 {id, name, apiKeyEnv, contextWindow, maxTokens, maxReuseTurns}(Key 见 modelKeys;maxReuseTurns 可选覆盖顶层) |
modelKeys |
— | 按模型 id 的独立 API Key 字典(写入型;优先于 models[i].apiKey 与顶层 apiKey) |
maxReuseTurns |
100 |
复用会话达到该轮数后新建 HiAgent 会话(即「对话轮数保留」,重置滑动窗口;0=关闭轮换;最大 100)。仅 noReuse: false 时生效 |
noReuse |
true |
每次新建会话:不复用 HiAgent 会话,每次请求新建并把完整历史作为只读上下文发入首条消息(消除 ~3 轮窗口锁死;代价是每次重发完整上下文)。false 恢复旧式复用 |
baseURL |
https://agent.tongji.edu.cn/api/proxy/api/v1/ |
必须以 / 结尾(端点名是拼接的) |
userId |
default_user |
HiAgent 用户 ID |
injectSystemPrompt |
true |
agent 未内置系统提示时,向首条消息注入 DSH 系统提示 + 工具 schema |
modelId |
hiagent |
未配置 models 时的默认模型 id |
displayName |
HiAgent (Tongji) |
选择器里的显示名 |
contextWindow |
128000 |
内部模型实测上下文(~128k 级;>20 万 token 会报错) |
maxTokens |
4096 |
元数据:输出上限(HiAgent 实际不限制) |
maxInitialMessages |
120 |
新建会话时最多发送的历史消息数(防御超限;0=不限) |
maxInitialTokens |
100000 |
新建会话时首条提示的 token 估算上限(CJK≈1 字/token;0=不限) |
maxInitialChars |
800000 |
新建会话时首条提示的字符上限兜底(0=不限) |
conversationStorePath |
$DSH_HOME/hiagent-conversations.json |
会话映射持久化文件 |
已知坑(适配时的关键点)
- BASE_URL 必须以
/结尾,否则create_conversation拼成...v1create_conversation404。 - 系统提示会很大。DSH 的系统提示(含全部工具 schema)可能上万 token,而 OpenClaw 版声明
的上下文只有 16000。插件把 DeepSeek 原生 DSML 工具协议和工具列表放在 system 段最前面,
即使 HiAgent 截断长首条消息,模型也看得到工具协议。若实际遇到截断,调大
contextWindow。 - 工具结果必须以
[tool:<callId>]段回传。DSH 的工具结果消息是role:"user"+tool-result块,不能按普通 user 文本处理,否则模型收不到结果只能瞎编(实测踩过)。 - DSML 解析与思考互不干扰:思考由独立的
think_message事件进入reasoning块;可见message增量直接解析 DSML 调用,两者来自不同事件通道,无需担心思考内容被当成工具参数。 - 不再做 think 标签剥离:旧版靠
splitThinkTags结构感知剥离thinking/<thinking>标签。 v2 规范下思考以独立think_message增量下发,流式 masker 与parseAnswer都不再剥离任何 think 标签——思考只来自事件路由,避免误剥模型想展示的标签字面量。 - 流式思考按事件类型路由,不依赖段落状态机:v2 规范下思考以独立
think_message增量下发, 插件按事件类型无条件送入reasoning;message增量才是可见text。绝不依赖 start/end 段落 状态机(服务端可能在思考中途错误插入 end/message_start,状态机会泄漏思考到正文)。无旧通道回退。 parseDsmlToolCalls与 masker 大小写不敏感 + 代码感知 + 截断恢复:DSML 标签匹配 大小写不敏感(masker 与权威解析必须一致,否则实时显示与最终块错位),并跳过代码区域内的 字面示例。解析器只认tool_calls块内的invoke,正文里的格式教学示例不会被误判。string="true"按字符串原样取;string="false"按 JSON 解析,裸控制字符由normalizeArguments宽容处理。- masker 保留 markdown 代码定界符(反引号/围栏):
TagMasker只把`与```当作代码区域的定界符来跟踪(区域内标签保持惰性),定界符本身是字面文本,必须照常输出。 旧实现吞掉开头的反引号/围栏,已修复:masker 输出定界符;emitResultTail/synthesizeReplyBlocks优先取权威content(仅当 content 为空时回退流式visibleText,如工具调用旁白)。 - 服务端会随机吞掉首帧开头字节:
tool_calls开标签的第一帧可能被网关丢掉开头 K 字节 (实测 K=0~13),导致开标签残缺或完全消失、DSML 严格块解析失败。 对策(已实施):parseDsmlToolCalls先做严格块解析;严格找不到时进入 relaxed 恢复—— 直接扫描invoke,只要后面存在tool_calls闭合,或 invoke 前存在残缺的开标签后缀 (如tool_calls>、calls>),就把它解析为真实 tool call。这样即使丢了开标签前缀也能恢复。 - 模型不一定会按协议输出:同一个 HiAgent agent 可能随机拒答或改用纯文本。这是模型侧行为, 代理层只能保证提示词完整。若频繁发生,考虑换 agent / 调 prompt。
- 会话复用的 key 必须两端一致:保存时用「请求消息 + 合成回复块」,下次请求用「历史里的
实际块」——序列化必须确定(
serializeContent固定了块字段),否则每轮都新建会话。 - reasoning 能力不声明:模型未声明
reasoningEfforts,选择器不会提供 effort 选项,请求 也不带 thinking 参数;思考由think_message事件路由成 reasoning 块,不做请求侧 think 标签回传。 - 「模型无法理解你的问题或回复内容为空」作为失败触发 DSH 重试:HiAgent 有时以「正常完成」
方式返回这句兜底失败文案(而非真实回答)。适配器在解析最终回答时,若整段文本「精确匹配」
(两端 trim 后)内置的
HIAGENT_EMPTY_REPLY_MARKERS列表,就把它标记为 DSH 原生错误码EMPTY_RESPONSE抛出。由于EMPTY_RESPONSE在 dsh-llm-retry 的默认可重试码集合内, DSH 会按重试策略(默认最多 2 次、指数退避)自动重新发起该模型请求。列表按精确整串匹配, 不做宽容/子串匹配以免误伤用户恰好问出相同句子的正常回答;遇到新的失败变体,向该数组追加即可。
DSML 截断恢复的设计
服务端在 think 后可能随机吞掉 <│DSML│tool_calls> 开头的 K 字节(实测 K=0~13)。为了在
不引入自定义格式的前提下恢复工具调用,解析器采用两层策略:
- 严格模式:只匹配完整的
<│DSML│tool_calls>...</│DSML│tool_calls>块,并跳过 markdown 代码区域,避免正文里的教学示例被误判。 - relaxed 恢复:严格模式没有命中时,直接扫描
<│DSML│invoke>。只要 invoke 后面存在</│DSML│tool_calls>闭合标签,或 invoke 前面存在残缺的开标签后缀(tool_calls>、calls>等),就判定为真实 DSML 工具调用并解析。 - 不做“纯 JSON 兜底”,避免把正文中的 JSON 误判成工具调用。
实测验证:think 后丢前缀时,只要 <│DSML│invoke> 和闭合 </│DSML│tool_calls> 仍幸存,
工具调用即可恢复解析。
测试
node test_adapter.mjs # 直接驱动 HiAgentAdapter(真实 API)
node test_fullstack.mjs # 独立 Cordis 应用 + 真实 LlmRuntime + 插件(真实 API)
源码目录 C:\code\hiagent\dsh-llm-hiagent,已同步到
C:\Users\helium\.dsh\profiles\node_modules\dsh-llm-hiagent。