在微信里对话、监控并审批你的 DSH 智能体
30.2/100
综合分
1
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/9/20 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
自动兼容检查有告警:未发布到 npm registry,仅可从源码安装。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add PRTS168/dsh-wechat-suite该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:需留意
是什么
dsh 原生插件 · vision
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
更新放缓:最近一次提交在 50 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
⚠︎ 实装验证未通过(unknown · 2026/11/3) ——可能是验证环境差异,装前建议到 GitHub 仓库确认最近更新与 issue。
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查⚠ 装前注意未发布到 npm registry,仅可从源码安装
展开查看检查明细 ↓
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✗npm 包@dsh-cowork/chatnode-wechat(未发布到 npm,仅可源码安装)
✓Node 引擎要求 >=22 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明
验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/7 13:15:39
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
可执行外部命令读写/删除本地文件读取剪贴板或密钥环境变量发起外部网络请求
查看扫描证据(共 128 处,展示前 91 条)
- 读取本地文件lib/gateway/index.js:363
plaintext = await readFile(filePath); - 读取本地文件lib/gateway/index.js:496
plaintext = await readFile(filePath); - 写入本地文件lib/qq/index.js:114
mkdirSync(dirname(file), { recursive: true }); - 写入本地文件lib/qq/index.js:115
writeFileSync(file, JSON.stringify({ at: new Date().toISOString(), ...patch }, null, 2), 'utf8'); - 写入本地文件lib/node/problems.js:171
mkdirSync(dirname(this.file), { recursive: true }); - 写入本地文件lib/node/problems.js:173
appendFileSync(this.file, `${line}\n`, 'utf8'); - 读取本地文件lib/node/problems.js:186
if (!existsSync(this.file)) - 读取本地文件lib/node/problems.js:188
if (statSync(this.file).size < PROBLEM_LOG_LIMIT_BYTES) - 写入本地文件lib/node/problems.js:190
renameSync(this.file, `${this.file}.1`); - 读取本地文件lib/node/inbound.js:260
if (!existsSync(path)) - 读取本地文件lib/node/inbound.js:262
if (statSync(path).size < limit) - 写入本地文件lib/node/inbound.js:264
renameSync(path, `${path}.1`); - 写入本地文件lib/node/inbound.js:324
await mkdir(dir, { recursive: true }); - 写入本地文件lib/node/inbound.js:332
await writeFile(absPath, downloaded.bytes); - 写入本地文件lib/node/inbound.js:391
await appendFile(log, `${new Date().toISOString()} ${absPath}: ${reason}\n`); - 写入本地文件lib/node/inbound.js:514
await mkdir(dir, { recursive: true }); - 写入本地文件lib/node/inbound.js:535
await writeFile(absPath, downloaded.bytes); - 发起网络请求lib/qq/client.js:168
const raw = await this.request(QQ_TOKEN_PATH, { - 发起网络请求lib/qq/client.js:183
const raw = await this.request(QQ_GATEWAY_PATH, { - 发起网络请求lib/qq/client.js:200
const raw = await this.request(qqC2CMessagesPath(userOpenid), { - 读取本地文件lib/node/reminders.js:135
const raw = await readFile(this.file, 'utf8'); - 写入本地文件lib/node/reminders.js:172
await mkdir(join(this.file, '..'), { recursive: true }); - 写入本地文件lib/node/reminders.js:182
await writeFile(temp, payload, 'utf8'); - 写入本地文件lib/node/reminders.js:183
await rename(temp, this.file); - 删除本地文件lib/node/reminders.js:188
await unlink(temp); - 读取本地文件lib/node/morning.js:189
const raw = await readFile(this.file, 'utf8'); - 写入本地文件lib/node/morning.js:221
await mkdir(join(this.file, '..'), { recursive: true }); - 写入本地文件lib/node/morning.js:227
await writeFile(this.file, JSON.stringify(this.config, null, 2), 'utf8'); - 读取本地文件lib/node/memory.js:86
return readFileSync(file, 'utf8'); - 读取本地文件lib/node/memory.js:95
readFileSync(file); - 读取本地文件lib/node/memory.js:110
if (existsSync(file)) - 写入本地文件lib/node/memory.js:113
mkdirSync(dirname(file), { recursive: true }); - 写入本地文件lib/node/memory.js:114
writeFileSync(file, MEMORY_SEED, 'utf8'); - 写入本地文件lib/node/memory.js:148
mkdirSync(dirname(file), { recursive: true }); - 写入本地文件lib/node/memory.js:151
copyFileSync(file, `${file}.bak-${stamp()}`); - 写入本地文件lib/node/memory.js:158
writeFileSync(temp, text, 'utf8'); - 写入本地文件lib/node/memory.js:159
renameSync(temp, file); - 删除本地文件lib/node/memory.js:164
rmSync(temp, { force: true }); - 读取本地文件lib/node/memory.js:186
if (!existsSync(file)) - 读取本地文件lib/node/memory.js:188
if (statSync(file).size < limit) - 读取本地文件lib/node/memory.js:210
for (const name of readdirSync(dir)) { - 删除本地文件lib/node/memory.js:226
rmSync(join(dir, row.name), { force: true }); - 发起网络请求lib/node/image-gen.js:32
const response = await fetch(`${baseUrl}/images/generations`, { - 发起网络请求lib/node/image-gen.js:55
const dl = await fetch(url, { signal: dlController.signal }); - 写入本地文件lib/node/image-gen.js:68
await mkdir(dir, { recursive: true }).catch(() => { }); - 写入本地文件lib/node/image-gen.js:71
await writeFile(absPath, bytes); - 发起网络请求lib/node/tts.js:26
const response = await fetch(`${baseUrl}/audio/speech`, { - 发起网络请求lib/node/ocr.js:49
const response = await fetch(`${baseUrl}/chat/completions`, { - 发起网络请求lib/node/stt.js:54
const response = await fetch(`${baseUrl}/audio/transcriptions`, { - 读取本地文件lib/node/context-report.js:36
const doc = JSON.parse(readFileSync(file, 'utf8')); - 读取本地文件lib/node/context-report.js:60
const doc = JSON.parse(readFileSync(file, 'utf8')); - 读取本地文件lib/node/context-report.js:79
const text = readFileSync(file, 'utf8'); - 读取本地文件lib/node/approvals.js:33
if (!existsSync(path)) - 读取本地文件lib/node/approvals.js:35
if (statSync(path).size < limit) - 写入本地文件lib/node/approvals.js:37
renameSync(path, `${path}.1`); - 写入本地文件lib/node/approvals.js:65
appendFileSync(path, `${new Date().toISOString()} ${line}\n`, 'utf8'); - 写入本地文件lib/node/admin-control.js:107
mkdirSync(queueDir, { recursive: true }); - 写入本地文件lib/node/admin-control.js:108
mkdirSync(doneDir, { recursive: true }); - 读取本地文件lib/node/admin-control.js:118
names = readdirSync(queueDir).filter((name) => name.endsWith('.json')).sort(); - 读取本地文件lib/node/admin-control.js:128
command = JSON.parse(readFileSync(file, 'utf8')); - 写入本地文件lib/node/admin-control.js:133
renameSync(file, join(doneDir, `${name}.bad`)); - 写入本地文件lib/node/admin-control.js:155
writeFileSync(join(doneDir, `${name}.result.json`), JSON.stringify({ at: new Date().toISOString(), command, ...result }, null, 2)); - 删除本地文件lib/node/admin-control.js:163
rmSync(file, { force: true }); - 读取本地文件admin/server.ts:197
const parsed = parsePatch(readFileSync(patchFor(profileName), 'utf8')) - 读取密钥环境变量admin/server.ts:217
if (process.env.WECHAT_ADMIN_TOKEN) return process.env.WECHAT_ADMIN_TOKEN - 读取本地文件admin/server.ts:219
const existing = readFileSync(TOKEN_FILE, 'utf8').trim() - 写入本地文件admin/server.ts:224
writeFileSync(TOKEN_FILE, minted, { mode: 0o600 }) - 读取本地文件admin/server.ts:313
const buf = readFileSync(path) - 读取本地文件admin/server.ts:333
const doc = JSON.parse(readFileSync(file, 'utf8')) as { record?: { rows?: Record<string, { val?: unknown }> } } - 读取本地文件admin/server.ts:355
try { dirs = readdirSync(root) } catch { return out } - 读取本地文件admin/server.ts:359
try { sessions = readdirSync(cwdPath) } catch { continue } - 写入本地文件admin/server.ts:477
mkdirSync(queueDir(p), { recursive: true }) - 写入本地文件admin/server.ts:479
writeFileSync(join(queueDir(p), name), JSON.stringify(command, null, 2)) - 删除本地文件admin/server.ts:537
rmSync(projection, { force: true }) - 写入本地文件admin/server.ts:548
mkdirSync(trashDir(), { recursive: true }) - 写入本地文件admin/server.ts:549
renameSync(found, target) - 写入本地文件admin/server.ts:554
if (existsSync(projection)) renameSync(projection, join(target, 'projection.json')) - 删除本地文件admin/server.ts:1110
else if (!before.exists) rmSync(patchFor(p), { force: true }) - 读取本地文件lib/node/patch-config.js:243
const text = await readFile(file, 'utf8'); - 读取本地文件lib/node/patch-config.js:326
text = await readFile(file, 'utf8'); - 写入本地文件lib/node/patch-config.js:531
await mkdir(dirname(file), { recursive: true }).catch(() => { }); - 写入本地文件lib/node/patch-config.js:535
await copyFile(file, backup); - 写入本地文件lib/node/patch-config.js:544
await writeFile(file, finalLines.join('\n') + '\n', 'utf8'); - 写入本地文件lib/node/silk.js:26
writeFileSync(mp3Path, Buffer.from(mp3)); - 执行外部命令固定命令lib/node/silk.js:28
const ffmpeg = spawnSync('ffmpeg', ['-y', '-i', mp3Path, '-ar', '24000', '-ac', '1', '-sample_fmt', 's16', wavPath], { encoding: 'utf8' }); - 写入本地文件lib/node/silk.js:40
writeFileSync(scriptPath, script, 'utf8'); - 执行外部命令固定命令lib/node/silk.js:41
const py = spawnSync('python', [scriptPath], { encoding: 'utf8' }); - 读取本地文件lib/node/silk.js:45
return readFileSync(silkPath); - 删除本地文件递归lib/node/silk.js:48
rmSync(dir, { recursive: true, force: true }); - 读取本地文件lib/node/silk.js:53
readFileSync(p); - 发起网络请求lib/qq/binding.js:39
const response = await fetch(url, {
扫描于 2026/11/4(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/dsh-agent@deepseek-ai/dsh-credentials@deepseek-ai/dsh-llm@deepseek-ai/dsh-session@deepseek-ai/dsh-session-title@deepseek-ai/dsh-tools@deepseek-ai/dsh-user-approval@deepseek-ai/schemastery@deepseek-ai/dsh-agent-presets@deepseek-ai/dsh-credentials-local@deepseek-ai/dsh-session-projection给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
功能亮点
- 微信内对话智能体
- 监控与审批
- 双向多类型消息
在微信里通过 clawbot iLink 网关与 DSH(DeepSeek Harness)智能体聊天、监控并审批:双向文字/图片/语音/文件/视频、原生视觉或 OCR、上下文轮换策略、提醒,外加独立管理控制台。
README
由 DeepSeek 最新模型翻译生成在微信里与你的 DSH agent 对话、监控、审批。
双向文字 · 图片 · 语音 · 文件 · 视频,走腾讯 clawbot iLink 网关 —— 不需要公网 IP、不需要端口映射、不需要打开浏览器。
Release
License
Tests
Node
DSH
Platform
这是什么 · 60 秒上手 ·功能 ·配置 ·命令 ·管理台 ·记忆 ·更新日志 ·
🧭 这是什么
一个 DeepSeek Harness bundle,通过腾讯 clawbot iLink 网关(ilinkai.weixin.qq.com,即腾讯自家微信机器人客户端所用的协议)把 DSH profile 接到微信个人账号 —— 这条用法不受官方支持。
展开完整文档(README 剩余 1 节)
在微信里与你的 DSH agent 对话、监控、审批。
双向文字 · 图片 · 语音 · 文件 · 视频,走腾讯 clawbot iLink 网关 —— 不需要公网 IP、不需要端口映射、不需要打开浏览器。
Release
License
Tests
Node
DSH
Platform
这是什么 · 60 秒上手 ·功能 ·配置 ·命令 ·管理台 ·记忆 ·更新日志 ·
🧭 这是什么
一个 DeepSeek Harness bundle,通过腾讯 clawbot iLink 网关(ilinkai.weixin.qq.com,即腾讯自家微信机器人客户端所用的协议)把 DSH profile 接到微信个人账号 —— 这条用法不受官方支持。
你(微信) ⇄ iLink ⇄ wechat-gateway ⇄ wechat-conversation-node ⇄ DSH agent 会话
| 插件 | 职责 |
| wechat-gateway(WechatGateway) | iLink 服务(ctx.wechat):扫码登录、鉴权长轮询、断线重连/退避、发送重试 + 限流熔断、正在输入指示、加密 CDN 媒体下载/上传、入站去重 |
| wechat-conversation-node | 微信 ⇄ DSH 桥:白名单闸门、会话定位、命令、上下文轮换、多模态媒体(OCR/STT/TTS/生图/文件/视频)、提醒与早安天气、摘要式出站、审批、灯控 |
⚡ 60 秒上手
前置:Node ≥ 22、pnpm、一个专用微信号、一个 DSH profile。装好后三步:
dsh plugin --profile add github:PRTS168/dsh-wechat-suite # 1 装进 profile
pnpm login # 2 扫码,写入微信凭据
pnpm setup # 3 填白名单等占位项
然后重启 dsh web,在微信里给这个号发条消息就该有回复。其它安装方式(Release tarball、从 checkout 构建)与分步说明见快速开始;卡住了先看独立管理台的体检——它每一项失败都会告诉你卡在哪。
[!WARNING]
一个账号一个轮询者。 iLink 每个 bot token 只允许一个鉴权轮询者:同一微信账号同时跑第二个本实例(或任何其他 iLink 客户端)会导致 HTTP 403 与消息丢失。请为桥准备一个专用微信号,并把它当作可弃置账号 —— 腾讯随时可能限制它。
[!IMPORTANT]
有两样东西必须填。 allowFrom 白名单,以及 WEIXIN_BOT_TOKEN / WEIXIN_ACCOUNT_ID / WEIXIN_BASE_URL 凭据。缺了它们,桥会安全地保持空闲 —— 它绝不会把白名单外的消息喂给模型。
所有 都是要你自己填的占位符;人设 preset 也需自备(把 agentPreset 指向你放在 $DSH_HOME/.agent-presets// 的 preset)。
✨ v5.0 有什么新东西
- QQ 是第二个平台,不是一个分支 —— 同一座桥现在同时支持微信与 QQ。平台接缝(src/platform/index.ts)是唯一知道"这条会话属于哪条渠道"的地方,两个平台各自拥有自己的白名单、记忆文件、问题台账与会话命名空间。两条渠道的对话永远不共用上下文,也不会互相镜像转发。
- 一个控制台管两个平台 —— 管理台可以在同一个网址下服务多个 profile(--profiles wechat,qq)并切换。控制台读的每一个路径都由平台 id 推出,所以"名叫 web、实际服务微信"的 profile 能找到 wechat-memory/MEMORY.md 与 wechat- 会话,而不是对着一屋子数据渲染空页面。
- 控制台再也走不出自己的目录 —— ?platform= 以前不经校验就进了路径拼接,构造一个名字就能读写 profiles/ 之外的 cordis.patch.yml。现在收口到一处校验,并有回归测试钉住"必须拒绝"。
- 改前端立刻生效 —— admin/index.html 每次请求都读盘,改完刷新即可,不用重启控制台。
- 保存不再把 profile 写坏 —— YAML 标量按字段 kind 决定是否加引号:QQ AppID 这类 "1234567890" 会保持字符串,不会被写成数字、导致下次启动被 schema 拒绝。
上一版的重头戏(网关传输自愈、长轮询窗口封顶、长回合进度提前)见 v4.1 发行说明。逐条变更:CHANGELOG.md · 更早的版本:releases/
🚀 快速开始
前置:Node ≥ 22、pnpm、一个专用微信账号、一个 DSH profile。
一键安装
dsh plugin … add 就是在 profile 里跑一次 pnpm add,所以 pnpm 支持的源都能用 ——
git 仓库、Release tarball、npm 包名、本地路径:
直接从仓库装(不需要 npm 发布)
dsh plugin --profile add github:PRTS168/dsh-wechat-suite
从 Release tarball 装
dsh plugin --profile add \
https://github.com/PRTS168/dsh-wechat-suite/releases/download/v4.0/dsh-cowork-chatnode-wechat-4.0.0.tgz
发布到 npm 之后按包名装
dsh plugin --profile add
装完直接跳到下面「配对微信」。从 checkout 构建的路径同样可用:
1. 从 checkout 安装
git clone https://github.com/PRTS168/dsh-wechat-suite.git
cd dsh-wechat-suite
pnpm install && pnpm build
dsh plugin --profile add .
2. 配对微信(打印二维码链接,微信扫码确认)
pnpm login # 写入 WEIXIN_BOT_TOKEN / WEIXIN_ACCOUNT_ID / WEIXIN_BASE_URL
3. 填齐剩余占位项(先备份,再改写 profile)
pnpm setup # 交互
或非交互;siliconflowKey 一次填充 OCR / 生图 / STT / TTS
pnpm setup --yes --set allowFrom=@im.wechat --set siliconflowKey=sk-...
然后重启 dsh web,给机器人发一条微信消息。想同时开管理台:
node admin/server.ts # Windows 上也可用 admin/start-admin.bat
→ http://127.0.0.1:8790/ (令牌写入 admin/.admin-token)
🎯 功能
| 方面 | 你会得到什么 |
| 消息 | 双向文字(回复发送前做微信化排版:标题 → 【】、代码块去围栏并缩进、表格去线);入站消息带时间戳的定界信封,模型因此分得清「真实用户回合」与「它自己写过的东西」 |
| 识图 | 多模态路由直接收到真正的 image 块,纯文本路由回落到 DeepSeek-OCR 文本 + 路径;imageInput: auto\|native\|ocr 与 /识图 运行时切换;generate_image(Kolors)出站 |
| 语音 | 入站语音自动转写(XingChenASR 或微信自带转写);agent 用 speak 开口说话(CosyVoice2 克隆音色),mp3 附件送达 |
| 文件与视频 | 入站文档/视频解密落盘 mediaDir 并保留原文件名;wechat_send_file / wechat_send_video 发回(可播放的 mp4/mov 附件 —— iLink 无原生视频气泡) |
| 会话 | /sessions /use /new /stop /status,重启后自动 resume 最近的 wechat- 会话;用 wechat- 前缀与网页 GUI 会话硬隔离 |
| 上下文 | contextPolicy 按轮数 / 上下文压力 / token 预算 / 空闲时长轮换,附免费交接摘要;管理台一键切换方案 |
| 控制 | /model 与 /perm 两步菜单;/yes /no(或裸 1 / 2)审批;/开灯 /关灯 或 /gear /off /low /mid /high 灯控(control_esp32_light) |
| 主动 | set_reminder(按联系人隔离、JSON 持久化、停机后补发)与每日 /早安 天气摘要(Open-Meteo,零 LLM 成本) |
| 邮件 | send_email 通过配置好的隐式 TLS SMTP 账号发送纯文本邮件 |
| 不刷屏 | 摘要式出站:长回合第一条心跳在约 20 秒时发出,之后每 digestIntervalSec 一条,回复按 maxMessageChars 分块限速,回合结束只在出错/中止/截断时提示 |
[!TIP]
/早安 test 回一句 fetch failed? 基本都是本地/系统代理把 api.open-meteo.com 拦掉了。桥会自动改用直连重试一次;两次都失败时,报错会写出真实原因(ENOTFOUND / ECONNREFUSED / TLS)。
🧠 长期记忆
跨会话的事实文件 ${DSH_HOME}/wechat-memory/MEMORY.md,五个小节 + 一个作废台账:
关于主人
- 主人有两个邮箱:主 primary@example.com、副 secondary@example.com
偏好与习惯常用设备与环境待办与承诺重要决定已过期
- 主人在示例市(已作废)
- 怎么进去的:① 主人说"记一下…"时模型调用 remember_fact 立刻落盘; ② 每天定点(默认 04:30)自动整理一次当天的主人原话。两条路都走同一套写入:原子替换 + 先备份 + 4000 字上限 + 每次改动一行审计(memory-log.md)。
- 怎么出来的:作为背景资料贴在用户消息围栏外面注入 —— 人设的硬规则是 "只有围栏里的才算主人发言",所以记忆永远不会被误当成命令。文件没有事实时不注入。
- 不会被忘掉:换会话、重启、压缩都带着;压缩发生后桥会主动在下一条消息重新注入一次。
- 用户可以随时看/改:/memory 看,文件是纯 Markdown,用记事本改完下一次注入即生效。
[!NOTE]
写入是有边界的:只记主人本人明确说过、且长期有效的事实(称呼、城市、作息、偏好、设备与路径、承诺、决定)。密码/token/API key、一次性安排、闲聊情绪、模型自己说过的话都不写;单条上限 300 字(超了会被截断,并在回执里写明),整份文件上限 4000 字(超了整笔拒绝,不会悄悄丢)。
⚙️ 配置
profile patch(cordis.patch.yml) plugins: dsh-chatnode-wechat:
allowFrom: ["@im.wechat"] # 硬白名单,必填,无默认值
digestIntervalSec: 300 # 回合中心跳间隔;第一条约 20 秒后就发,之后按这个间隔 approvalTimeoutSec: 600 # 审批超时 → 默认拒绝 maxMessageChars: 2000 # 微信单条气泡上限(协议限制) sendChunkDelayMs: 1500 # 出站气泡间隔限速 imageInput: auto # auto | native | ocr contextPolicy: '{"scheme":"manual"}' # 见 releases/v0.3.0-release-notes.md imageInputModel: amd/DeepSeek-V4-Flash-Vision-Exp # 图片专用视觉路由 agentPreset: wechat # 可选:人设 preset(在仓库之外) agentProvider / agentModel: ... # 微信 agent 的模型路由 esp32BaseUrl: http://:80 # 灯控(可选;没有内置默认地址,要用就得填)
---- 媒体助手(全部可选;未配时对应能力优雅降级)---- ocrApiKey / ocrModel: deepseek-ai/DeepSeek-OCR / ocrBaseUrl imageGenApiKey / imageGenModel: Kwai-Kolors/Kolors / imageGenDir sttApiKey / sttModel: XingChenAGI/XingChenASR-V3.2-Ultra ttsApiKey / ttsModel: FunAudioLLM/CosyVoice2-0.5B / ttsVoice: speech: mediaDir: # 入站媒体落盘(默认 $DSH_HOME/attachments/wechat) reminderFile / morningFile:
---- 邮件(可选;host + 用户名 + 密码三项齐全才会真正发得出去)---- smtpHost: smtp.example.com smtpPort: 465 # 隐式 TLS smtpUsername: you@example.com # 同时作为发件人地址 smtpPassword: smtpFromName:
---- 长期记忆(可选,全部有默认值)---- memoryFile: $DSH_HOME/wechat-memory/MEMORY.md memoryInjectEvery: 10 # 每 N 条消息重新注入一次记忆 memoryConsolidateTime: "04:30" # 每天整理时间;留空 = 不自动整理 problemFile: $DSH_HOME/wechat-problems.log # 问题台账
---- 网关调优(可选)---- 以下为网关调优键;longPollTimeoutMs 同时是服务端建议长轮询窗口的上限 longPollTimeoutMs / apiTimeoutMs / pollIdleDelayMs / retryDelayMs backoffDelayMs / maxConsecutiveFailures / sessionExpiredPauseMs sendChunkRetries / sendChunkRetryDelayMs rateLimitCircuitOpenMs / rateLimitCircuitWindowMs / rateLimitCircuitThreshold
allowCdnHosts: [...] # 媒体下载的 SSRF 白名单
allowFrom 必填且没有宽松默认值:缺失会启动失败;白名单外的消息只记日志、直接忽略,永远不会喂给模型。
[!TIP]
上面每一项都能在管理台里改(新手模式只显示常用的几项,高级模式列出全部)。
以前有一类坑:某些键只声明在会话节点侧,宿主校验 profile patch 时会静默丢弃,于是"填了等于没填"(send_email 报"SMTP 未配置"就是这么来的)。现在 bundle schema / 转发列表 / 节点 schema / 管理台字段四份键集必须相等,并有 config-surface 测试守护。
原生识图 vs OCR —— 以及「声明才是开关」这个坑
| 模式 | 模型拿到什么 | 何时使用 |
| native | 真正的 image 内容块(模型自己看图) | 路由模型声明了 image 输入 |
| ocr | 【OCR 识别结果】 文本 + 文件路径 | 路由模型是纯文本,或没有任何路由声明支持图片 |
- auto(默认)—— 先解析 agent 实际聊天用的路由,向 llm.listModels() 询问它的 inputModalities,声明了 image 就发图片块。若聊天路由是纯文本,auto 会在其他已注册路由里找声明支持图片的(也可以用 imageInputModel 钉死一个),否则走 OCR。
- native —— 总是尝试发图片块。未声明支持图片的路由仍会被试一次(端点可能实际接受图片却没广告出来);一旦被拒,该路由会被抑制 3 小时,后续图片直接走 OCR。
- ocr —— 总是走文本路径。文档、截图这类场景更便宜,往往也更准。
声明才是开关。 harness 在请求上游就按模型声明的 inputModalities 闸门拦图片 ——声明成纯文本,图片根本不会被发送。而 DeepSeek 适配器内置的模型目录早于 4.1 多模态(把 deepseek-v4-flash 记为纯文本,唯一声明 image 的 deepseek-v4-flash-vision-exp 又已下线),所以要显式声明:
llm-deepseek:
models:
- id: deepseek-flash
inputModalities: [text, image]
这个 models: 列表是整体替换插件内置目录、不是追加 —— 要把你实际路由到的每个 id 都列上(若你的 profile 路由到旧别名 deepseek-v4-flash,也要一并列出,否则那条路由不再解析)。
/识图 可查看并运行时切换模式(auto / native / ocr),覆盖状态持续到 dsh web
重启。两种模式下入站图片都保留 [微信图片] 前缀,会话记录可回放、agent 可重读文件。
/识图 # 当前模式 + 路由模型 /识图 native # 强制图片块 /识图 ocr # 强制 OCR 文本
🎛️ 命令与工具
命令(微信里发送)
| 命令 | 作用 |
| (普通文字 / 图片 / 语音 / 文件 / 视频)* | 路由到当前 agent |
| /sessions | 编号会话列表(仅 wechat-,最近优先) |
| /use N | 切换活动会话 |
| /new | 新建 agent+会话并开工 |
| /stop | 取消当前任务 |
| /status | agent 状态 + 会话摘要 + 网关健康(在线 / 重连 / 暂停 / 已停止) |
| /context | 上下文占用、模型窗口、组成、压缩触发点、压缩后原样保留多少、记忆条数 |
| /memory | 查看长期记忆;/memory now 立刻整理一次 |
| /problems | 最近被记下来的问题(/problems clear 清空列表,日志保留) |
| /send | 发送一张本地图片给当前联系人 |
| /model | 两步切换模型(列表 → 选数字) |
| /perm | 两步切换权限预设(列表 → 选数字) |
| /识图 [auto\|native\|ocr] | 图片识别模式;不带参数则报告当前模式与路由模型 |
| /早安 on\|off\|status\|test\|HH:MM(别名 /morning) | 早安天气摘要 |
| /开灯 /开灯1\|2\|3 /关灯 | ESP32 灯控(3 档高 / 1·2 档低中 / 关) |
| /gear /off /low /mid /high | 同一实现的设备原生词表(查询 / 关 / 低 / 中 / 高) |
| /yes /no(仅一条待确认时也可 1/2) | 回答权限请求 |
| /help | 命令列表 |
Agent 工具(供模型调用)
| 工具 | 用途 |
| wechat_send_image(path) | 发送本地图片给联系人 |
| wechat_send_file(path) | 发送任意本地文件给联系人 |
| wechat_send_video(path) | 发送本地视频(可播放的 mp4/mov 附件) |
| generate_image(prompt) | 文生图(Kolors)并发送 |
| speak(text) | 克隆音色 TTS,以 mp3 附件发送 |
| send_email(to, subject, body) | 通过已配置 SMTP 账号发纯文本邮件 |
| control_esp32_light(mode) | 局域网灯的 query / off / low / mid / high |
| set_reminder(text, inMinutes\|atTime) | 定时提醒(按联系人) |
| list_reminders() | 查看待办提醒 |
| cancel_reminder(id) | 取消提醒 |
| remember_fact(text, section?, replaces?) | 把主人说过的事实立刻写进长期记忆(可选 replaces 取代旧事实) |
✅ 审批
微信没有按钮,权限请求渲染为编号文本提示、聊天内回答:
#1 需要你的确认工具: bash 原因: run a destructive command 回复 /yes 同意,/no 拒绝(仅一条待确认时也可回复 1/2) 10 分钟内未回复将自动拒绝
/yes 授予 allowed-once;/no 拒绝;超时回退到 DSH 默认拒绝。桥只回答当前由微信驱动的 agent 的请求,其余沿 answerer 链继续委托。
🖥️ 独立管理台
node admin/server.ts [--port 8790](Windows 上可用 admin/start-admin.bat)启动一个
仅回环的 HTTP 管理台,它不属于 DSH 插件树 —— 不会影响 profile 启动,停掉它也不会停掉桥。
页面有两个模式,左上角切换(记住选择):
| 模式 | 面向 | 内容 |
| 新手模式 | 不熟配置的人 | 只留必须懂的几项、全程大白话:谁能跟我说话(白名单)/ 用哪个大脑(模型下拉)/ 它记不记得住(记忆 + 每天整理时间)/ 聊久了怎么办(两个选择),外加"出问题了"页直接看日志原文;顶部一行体检结论 |
| 高级模式 | 要全部控制的人 | 全部配置键(分组)/ provider 与图片路由 / 记忆与问题日志路径 / 6 种上下文方案 + 参数微调 + 原始 JSON / 对话列表与转录 / 配置备份与一键回滚 / 完整环境自检 |
新手模式 —— 一眼看清"现在能不能用",红点会直接说缺什么、去哪儿填:
高级模式 —— 全部配置项按「必填 / 可选」分组,密钥字段掩码显示:
两张截图都来自演示实例,配置值、路径与人设均为占位数据。
| 接口 | 作用 |
| GET /api/state | 配置值(密钥掩码)、字段定义、上下文方案、会话列表 |
| GET /api/health | 8 项体检:配置文件 / 白名单 / 人设 / 微信凭据 / 媒体 Key / 长期记忆 / 最近问题 / 聊天模型 |
| GET /api/problems · GET /api/memory | 问题台账原文 · 长期记忆原文 |
| GET /api/models | 从 settings.yaml 读出真实模型列表(含第三方中转名) |
| GET /api/backups · POST /api/rollback | 配置备份列表(按时间)· 回滚(回滚前再存一份当前配置) |
| POST /api/config · POST /api/scheme | 写 patch(只提交改动项)· 切换上下文方案 |
| GET /api/conversations · /api/transcript · POST /api/session/ | 会话列表 / 转录 / 新建 / 遗忘(可恢复) |
安全姿态 —— 只绑定 127.0.0.1 · 令牌在首次启动时写入 admin/.admin-token (可用 WECHAT_ADMIN_TOKEN 覆盖)· 每次 API 调用都要带它 · 改动类调用还要带 x-wechat-admin: 1 守卫头(跨站页面无法伪造)· Host 必须是回环地址(DNS rebinding 页面因此够不到它)· 配置文件读不出来时拒绝写入(避免把别的插件条目一起覆盖掉)·数字字段服务端二次校验(写坏一个整数字段会让整个 profile 起不来)。会话命令落在磁盘队列($DSH_HOME/wechat-admin/queue/),由桥在 ≤2 秒内执行;管理台还能看到桥最近的执行回报。
令牌文件已被 git 忽略。
[!NOTE]
v0.3.0 起不再有网页 GUI 内的设置页 —— config-api 插件行已移除(见 releases/v0.3.0-release-notes.md)。请改 cordis.patch.yml 或用这个管理台。
🧪 开发
pnpm install
pnpm build # src/ → lib/(tsc)
pnpm typecheck
pnpm test # node --test test/.test.ts —— 255 项,无需微信
pnpm smoke # 真机手动冒烟
pnpm setup # 交互式配置向导
- test/fake-ilink-server.ts 实现 iLink 端点(长轮询、sendmessage、sendtyping、getconfig、扫码登录、加密 CDN 下载),回放 test/fixtures/inbound.ndjson;入站→会话→出站全链路可离线跑在 CI(.github/workflows/ci.yml)。
- 覆盖范围包括:网关去重(message id 与载荷指纹)、入站路由 / 媒体失败路径 / 消息信封、命令面、审批桥、上下文轮换策略(含 token 预算的多级回退链)、灯控、两个宿主版本下的 preset 兼容,以及 boot-safety.test.ts —— 它把「绝不产生未处理 rejection」这条曾导致宿主崩溃的性质钉死在测试里。
- 诚实标注的盲区(暂无单测):OCR 成功/失败分支、语音下载→ASR 全流程、媒体上行(fake 服务器无 /upload)、/send、/help、重启 resume,以及原生图片块本身(vision.test.ts 用 stub 目录覆盖模式判定,attachments.saveImage 只在真机上跑)。真机冒烟覆盖主路径。
- 测试分布(255 项):memory 36 · node 33 · context-policy 21 · gateway 18 · light 15 · commands 11 · problems 10 · vision 10 · markdown 9 · morning 9 · approvals 9 · outbound-guard 8 · context-report 8 · robustness 8 · patch-config 7 · inbound-media 6 · dedup 6 · resume 5 · config-surface 5 · user-message-envelope 5 · reminders 4 · email 4 · picker 4 · packaging 3 · boot-safety 1
- v4.0 新增的守护型测试:config-surface(配置面四份键集必须相等 —— 就是它挡住了 "SMTP 配置被宿主丢弃"那一类 bug)、problems(台账去重/限流/轮转)、robustness (拒写、原子写、备份按时间裁剪、日志轮转)、packaging(发布包白名单不能把 .admin-token 与日志扫进去)、outbound-guard(用真实事故原文做用例)。
⚠️ 已知限制
- 仅供参考:只在一套环境实测过(2026-09),不代表开箱即用;协议细节逆向自既有的 iLink 客户端,仓库内为合成样本。
- 出站语音/视频以文件附件(mp3/mp4)送达,不是原生气泡(iLink 限制);silk.ts 与
gateway.sendVoice() 已备但暂无调用方(silk 转码还需外部 ffmpeg + pilk)。
- 部分命令回执仍带 emoji,未按禁 Emoji 人设清理。
- 生成的图片/语音累积在 mediaDir/generated,暂无自动清理。
- 微信 silk 编码语音的 STT 待真机验证(m4a 已实测)。
- 当前只面向 1:1 文字聊天;群消息按设计忽略(MVP)。
- rotate-pressure 的上下文尺寸是「事件序列化字符数」这一代理值,不是模型 token 数;要精确计数请用 rotate-tokens(依赖宿主暴露会话投影)。
🛡️ 风险
| 风险 | 对策 |
| iLink 独占锁 —— 同一 token 两个轮询者 → 403 + 丢消息 | 专用账号;遇 403 大声报错并停止轮询 |
| 非官方网关可能限制账号 | 使用可弃置的专用账号;README 明说 |
| DSH v0.1 变更 | 已验证两个宿主版本(见 v0.3.0 发行说明);可选项用 ctx.get();boot 安全测试 |
| 未处理的 rejection 杀死宿主 | 全部异步入口保证 resolve;调用点 .catch();热重载压迫测试 |
| 协议细节未见于公开文档 | 报文格式从既有 iLink 客户端归纳;仓库内为合成样本 |
| 运行时在仓库根落盘含凭据的文件 | client-config.json / account.json / admin/.admin-token 均已 git 忽略 |
📚 版本历史
v5.0 —— QQ 接入为第二平台 · 一个控制台管两边 · 控制台路径护栏
- 双平台,一座桥:微信与 QQ,各自拥有白名单、记忆文件、问题台账与会话命名空间。按平台的接缝决定一个回合属于哪条渠道,答复永远回到它来的那条渠道。
- 多平台管理台:同一个网址服务多个 profile 并可切换;控制台读的每个文件都按平台 id 定位,会话、记忆、台账都能显示出来,而不是渲染空页面。
- 安全:控制台的 ?platform= 在变成路径之前先校验 —— 未知名字与穿越写法一律 4xx,并有回归测试钉住;profile 名单也在启动时校验。
- 修复:字符串型数字值(如 QQ AppID)不再被写成裸 YAML 数字,那个问题会让保存过的 profile 下次启动直接失败。
- 修复:问题台账与控制台的命令队列改为按平台 id 定位,不再按 profile 名。
- 294 条离线单元测试。
见 releases/v5.0-release-notes.md。
v4.1 —— 网关传输自愈 · 长轮询窗口封顶 · 长回合进度提前
- 传输层自愈:连接层出问题时,后续请求自动改走全新连接(node:http、agent: false),不再需要重启进程才能恢复 —— 起因是一次连续 18 分钟发不出消息的事故。
- 长轮询窗口封顶:服务端的 longpolling_timeout_ms 不再能无限顶掉配置值,取
min(服务端建议, longPollTimeoutMs),两个数字都写进日志。
- 长回合第一条进度约 20 秒就发(原来等满 digestIntervalSec,默认 300 秒)。
- 修复:Linux CI 上必红的 robustness 用例(chmod 000 后回读前没恢复权限);补齐灯控地址的破坏性变更说明(esp32BaseUrl 不再有内置默认值)。
- 离线单测 261 项(原 255 项)。
见 releases/v4.1-release-notes.md。
v4.0 —— 上下文管理大改 · Web 管理台重写 · 长期记忆
- 上下文:默认一个会话到底;/context 显示真实占用与压缩触发点;建议把压缩参数写成 thresholdRatio 0.65 / retainRatio 0.25(避开"只留最后一条消息"的溢出路径);压缩后自动重新注入记忆。
- 长期记忆:MEMORY.md + 每天 04:30 整理 + remember_fact 工具 + /memory。
- 管理台:新手 / 高级双模式,8 项体检,备份与一键回滚,写操作守卫头真正生效。
- 问题台账:wechat-problems.log + /problems + /status 网关健康,告警限流。
- 修复:配置被宿主静默丢弃(send_email 报"SMTP 未配置"的真因)、模型冒充主人写话、工具调用步骤被误报成空回复、心跳定时器泄漏、热重载 403、定时器 NaN 忙循环等。
- 离线单测 255 项(原 167 项)。
见 releases/v4.0-release-notes.md。
v0.3.1 —— 天气与命令面修复
- 天气推送报出真实失败原因,并直连重试(不受宿主代理影响)。
- 裸 1 / 2 恢复回答权限请求;/yes /no 不再自称「未知命令」。
- 老灯控词表 /gear /off /low /mid /high 恢复,且同样不怕代理。
- 离线单测 167 项(原 143 项)。
见 releases/v0.3.1-release-notes.md。
v0.3.0 —— 稳定性、上下文生命周期、独立管理台
上下文轮换方案、宿主稳定性加固、故障可见性与独立管理台 —— 详见 releases/v0.3.0-release-notes.md。
- 离线单测 143 项(原 86 项)。
v0.2.2 —— 适配 DeepSeek 4.1 多模态与原生图片输入
- 路由模型声明图片输入时,入站图片以真正的 image 块送达;否则回落 DeepSeek-OCR 文本。
imageInput 与 /识图 选择策略;被拒过的路由抑制 3 小时。
- 「声明才是开关」(见「配置 → 原生识图 vs OCR」):models: 片段是整体替换插件目录,不是追加。
- send_email(隐式 TLS SMTP);client-config.json / account.json 加入 .gitignore。
v0.2.1 —— 网页管理页与人设编辑
- 设置 → 插件 →「微信桥配置」:浏览器内管理全部占位项,密钥脱敏、带时间戳备份;同一页面还能编辑各 agent preset 的人设正文。(v0.3.0 已移除,见 releases/v0.3.0-release-notes.md。)
v0.2.0 —— /perm、双向文件与视频
- /perm 两步权限预设切换器,以及 pnpm setup 配置向导。
- 入站文件/视频解密落盘 mediaDir 并保留原文件名;wechat_send_file / wechat_send_video 发回本地文件与视频。
- 仓库内人设残留清除。
完整变更历史见 CHANGELOG.md · 全部发行说明见 releases/
🗺️ 路线图
- 下一步 —— 群聊(显式开启,高风险)、多账号、与 hermes-agent / OpenClaw 共存的共享轮询代理。
- 后续 —— 复用 node/ 层的企业微信 / 钉钉 / 飞书 bundle。
🙏 致谢
- 协议参考 —— iLink 报文细节归纳自既有的微信机器人客户端(hermes-agent 与 OpenClaw);合成样本在 test/fixtures/inbound.ndjson(不含真实账号数据),CI 因此无需真账号。
- 平台 —— DeepSeek Harness 及其 Cordis 插件模型(cordis、schemastery)。
- 可选媒体能力所用服务 —— SiliconFlow(DeepSeek-OCR、Kwai-Kolors、XingChenASR、 CosyVoice2)与 Open-Meteo(早安天气)。
- 感谢 —— 本 bundle 所依赖的 DeepSeek Harness 与 Cordis 插件生态。
📄 许可协议
MIT —— 见 LICENSE。预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
PRTS168/dsh-wechat-suite
在微信里对话、监控并审批你的 DSH 智能体
相关插件 · 视觉能力
智能体工作台Devin-AXIS/iPolloWork
企业级、本地优先的智能体工作台,面向个人和智能体团队——一个工作区,容纳多个智能体引擎;一套统一体系,管理插件和技能;支…
★ 7,364已汉化DSH
72
开源设计工作台nexu-io/open-design
OpenDesign:首个协作式 AI 设计智能体工作区
★ 105,709已汉化DSH
71.8
GPT 图像提示词库freestylefly/awesome-gpt-image-2
Prompt as Code | GPT Image 2 / 2.5 提示词与案例库,530+ 个案例、20+…
★ 34,534已汉化DSH
71.5
多智能体编码工作区spacering-net/codeg
多智能体协作的 AI 编程工作区:汇总 Claude Code、Codex、OpenCode、Pi、Grok Build 等来源的会话。支持桌面应用、自托管服务器或 Docker。
★ 4,319已汉化DSH
71.5
AI 用量桌面小部件Javis603/token-monitor
EN | 简 | 繁 | KO | JA | PT
★ 3,533已汉化DSH
71.1
架构图生成器tt-a1i/archify
把你想理解、规划或分享的事,变成可以互动的可视化作品。
★ 93,105已汉化DSH
71.1
这是你的仓库?认领条目