dsh-cyber-im-avatar
IM stand-in seat for DeepSeek Harness: a sidebar panel polls Feishu/Lark, WeCom and DingTalk (switched on from the panel) through the platform CLI you are already logged in to, and wakes the DSH session you bound when a new message arrives, so that session reads it and replies as you — drafts are sh
Install
dsh plugin --profile web add dsh-cyber-im-avatar
Sandbox test results
| Stage | Result |
|---|---|
| 1 install | ✓ |
| 2 list | ✓ |
| 3 mount | ✓ |
| 4 boot | — |
Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).
README
dsh-cyber-im-avatar · IM 分身壳
把仓库根目录那套 skill(SKILL.md + scripts/)做成 DeepSeek Harness 里的常驻壳:
侧边栏一个席位,点开是每日关注、值守实例、服务对象(群工作本 / 人物档案)与手动发送。
分工(spec 0004):插件只当壳,skill 是唯一的脑子。
层 · 干什么
壳(本插件) · 按节奏拉取(耳朵)、命中唤醒绑定 DSH 会话(嘴)、消息展示与手动发送(脸)
skill · 闭环协议、平台差异、铁律、故障自愈
agent 会话 · 被唤醒后全权处理:自己调 IM CLI 拉上下文、调研、起草、发送
cyber-im-avatar/dsh-plugin/ ├─ package.json # dsh.bundle(patch) + dsh.client(platform/inject) ├─ cordis.patch.yml # 把 bundle 插进 web 组合 ├─ lib/index.js # 宿主半:同源路由 + token + 装 skill + 注入 DSH 服务 ├─ lib/app.js # /api/* 路由表与参数校验 ├─ lib/state.js # state.json:配置 / 值守名单 / 最近 10 条指针 ├─ lib/poller.js # 轮询、分类、命中后唤醒(待回派生供计数与详情) ├─ lib/turns.js # live turn:resolveAgent / followup / 等 turn/end / 取消 ├─ lib/im.js # 平台适配:lark-cli / wecom-cli 封装 + @我 判定 ├─ lib/profiles.js # 画像:人物档案(单聊一份)与群工作本;旧私聊工作本整文件搬迁 ├─ lib/workspaces.js # 工作区列表(读 ~/.dsh/storages/workspace.json) ├─ lib/sessions.js # DSH 会话目录路径换算(只读) ├─ lib/client.js # 构建产物(内联了 web/panel.js) └─ web/panel.js # 面板 UI 的唯一真源(原生 DOM,无框架)
这个插件不在本机开任何端口。 面板数据走 DSH 自己的 Web 服务(同源),
后端就是宿主进程里的这些 ESM 模块;整台机器上唯一的监听 socket 是 DSH 那个。
没有 Python 服务、没有 stdio 管道、没有 SQLite、没有 dsh headless 子进程。
装
# 需要一个 DSH(web profile)。已发到 npm,直接装包名: dsh plugin --profile web add dsh-cyber-im-avatar dsh web # 重启后侧边栏底部出现「IM 值守」 # 或者装本地目录(改代码时用这个) dsh plugin --profile web add ~/cyber-im-avatar/dsh-plugin
想让它出现在 DSH 的「插件 → 发现」里可搜可点,把市场索引加进去:
https://raw.githubusercontent.com/buger-gif/cyber-im-avatar/main/dsh-plugin/market/index.json
前置:lark-cli / wecom-cli 已安装并登录;要用钉钉再装 dws(npm i -g dingtalk-workspace-cli)。
钉钉默认不启用:在面板「体检」页点钉钉卡片的「启用」,或加入一个钉钉会话值守,才会开始轮询。
skill 不用单独装:插件首次启动会把
cyber-im-avatar 装到 ~/.agents/skills/cyber-im-avatar/(npm 包自带副本;本地
link 安装读仓库根目录)。已经手装过同一份的不会被覆盖,只打一行提示。
起 DSH 建议用仓库里的脚本,别直接 node bin.js web &:
./scripts/start-dsh.sh # 已在跑就只打印地址(幂等) ./scripts/start-dsh.sh --restart # 先停再起
它做两件事:让 DSH 进独立会话(关掉终端也不会被连带关闭),并清掉代理变量与
NODE_OPTIONS 注入项。token 落到 ~/.dsh/web-token(600)。
命中 → 唤醒 → 同会话续跑
值守会话来了新消息(私聊消息 / 群 @我;私聊可切「只盯 @我」,见下),插件在宿主里拿绑定的 DSH 会话跑一轮
live turn。每次唤醒的载荷都只追加本次新消息:
1. 新会话首轮首行 /cyber-im-avatar —— DSH 的 user invocation,直接把 skill 正文注入
这一步(闭环、平台差异、铁律、故障自愈都在 skill 里,prompt 不再抄);同一轮另带
平台/对象、chat_id、运行形态与记忆文件路径(单聊:人物档案一份;群:会话工作本
与本批发言人的人物档案路径,≤5、去重、@我优先)与本次新消息;
2. 已有会话的后续唤醒只发本次新消息行(外加本会话还没给过的人的档案行;
同一人在同一条 DSH 会话里只给一次路径),不再重复平台、
chat_id、运行形态、工作本路径或媒体说明——这些已经在同一 session 的历史里。
插件不注入画像正文、不解析 agent 回复、不代写画像;agent 按首轮载荷里的路径自己读、
自己改:单聊只有对方的人物档案一份;群才是「工作本写本群规矩 + 人物档案写人的评价」。
换会话或重置会话会重新带 /cyber-im-avatar 入口。进程被杀后 state.json 的
prompt_session 与 recent 还在,下一轮继续追加 pending/新消息,不重建完整载荷。
富媒体在唤醒时取回:只对本批待回消息下载,不动历史。图片用 DSH
attachments.admitPromptContent() 变成 image block,模型直接可见;文件/语音/视频
落 ~/.im-standin/media/ 并在 prompt 给本地路径;sticker 只标 [表情]。卡片、合并
转发、位置、名片、系统消息等未适配类型,以及下载失败的消息,才在 prompt 标“可用 CLI 取”。
单批最多 4 张图 / 合计 10MB、单条 20 秒;模型不支持图片或附件服务缺失时降级为
文本 + 本地路径,不阻断 turn。
命中回执:followup 被接受后,插件用 lark-cli im reactions create 给本批 @ 你的消息点一个 Get 表情(不是新消息);只认群 @ 与私聊「只认 @」模式的 dm,仅飞书,失败不影响 turn,设置页可关(dsh.ack_reaction)。
agent 在这一轮里自己用 lark-cli 拉上下文、去绑定工作区核实事实(只读)、
起草并发送或等你在会话里确认——它有多少工具就用多少,插件不封顶。
壳内不挂监听:轮询、水位、唤醒都是插件的事。agent 不运行
listen_cli/bootstrap.py,也不重挂监听——壳内沙箱是workspace-write,
~/.im-standin 在工作区外,写不了;也不需要写。
- 同一通 IM 会话 = 一个长期复用的 DSH 会话(
chats.dsh_session),跨命中、
跨重启累积历史;在 DSH 侧边栏能看到、能点开接着聊。
- 在飞 turn 可打断:同一会话又来了新消息 →
agent.cancel旧 turn →
同 session 重起,任务里注明「上一轮被新消息打断,以最新上下文为准」。
- 批次合并:同一轮 poll 拉到多条新消息只起一个 turn,不让 agent 被连发消息叫醒 N 次。
- 并发安全:就是宿主里的同一个 agent,DSH UI 打开它不再抢
session.lock;
另一个 DSH 宿主持有写锁时才报 session_locked_other_host。
- 失败重喊:turn 失败/超时后,积压还在就下一轮再喊;不自动连环重试。
- 重开会话:实例卡片或会话详情点「重开会话」→ 打断在飞 turn、解绑当前会话,
值守中(未屏蔽)立刻在新会话里跑一轮开场——不用等下一轮命中,@-only 私聊也不会看起来
像没建会话。旧会话保留在侧边栏,只是不再续聊。闲置超过阈值也会自动换,见下。
单轮守时:dsh.timeout(默认 600 秒)是停滞阈值——本轮有工具/模型活动就重置计时,真卡死才取消;dsh.turn_cap_sec(默认 3600 秒,0 = 不限)从轮次开始兜总时长。长任务不会被「单轮只能跑 10 分钟」的墙钟切掉。
模型与会话都走 DSH
不需要在这儿再配一遍模型。插件在宿主进程内拿 DSH 的 sessionController /
sessions,模型、网关、凭据全部由 DSH 自己解析:
要什么 · 从哪读
会话 / agent · ctx.sessionController.resolveAgent() 或 create()
模型 / 网关 / 凭据 · DSH 自己的 profile 配置与凭据库,插件不读、不存、不回传
工作区 · ctx.workspaceRegistry + ~/.dsh/storages/workspace.json(只读展示)
所以「DSH 里能正常对话」=「值守能干活」。插件没有第二份 key、没有 headless profile、
没有起草模型设置。
实例的生命周期
一通值守会话 = 一个分身实例 = 一个 DSH 会话。插件管四件事:
事 · 行为
建 · 首个命中时 create({workspaceId · cwd}),DSH 自动把它归属到工作区
续 · 复用;历史与工具调用都落在同一个 session-… 里;一段连续闲置最多跑一轮值守心跳(dsh.warm_min)
断 · 新消息到达时 agent.cancel 在飞 turn,同 session 重起
换 · 手动「重开会话」(解绑后立刻起开场轮)、工作区漂移、会话丢失,或闲置超过第二个保温周期(约 2 × dsh.warm_min)后的下一条消息;旧会话留在侧边栏
实例卡片显示绑定会话、累计轮数(保温轮不计)、最近一次 turn 是否成功与上次保温结果。
保温与会话到期
- 值守心跳(
dsh.warm_min,默认 55 分钟):一段连续闲置达到这个间隔,轮询器在已绑定
会话里起一轮很轻的维护——续上下文缓存,并按 skill 规则整理载荷列出的文件(单聊一份
人物档案;群工作本 + 最多 3 份档案),不扫目录。它不计入「N 轮」、不推进闲置时钟;真实
消息到达会立刻打断它,详情页显示上次保温结果。
- 只保温一次:再过一个同样长的周期(默认约 110 分钟)仍没有真实消息,就停止保温;此后
的第一条消息到来时先解绑旧会话,在新会话里带完整首轮载荷处理。旧会话不删。手动重开、
换工作区、会话丢失也会在详情里写明换会话的原因。
dsh.warm_min=0同时关闭保温与自动换会话;想让它活得更久就调大间隔。面板「设置 →
会话生命周期」可改。
会话所在 cwd 与绑定的工作区不一致时(换了工作区),DSH 会拒用旧会话,
插件会丢掉旧 id 在正确工作区重开,并在实例卡上提示「已自动新建」。
显式工作区目录存在但未登记进 DSH 时,仍先恢复旧会话,不会每轮白开新的。
拉取节奏:定时 / 按需 / 手动
路径 · 什么时候发生 · 拉什么
值守(快车道) · 默认每 60 秒(poll.fast_sec,0 = 关) · 值守中的私聊,以及关注程度不是「不盯」的群
发现(慢车道) · 默认每 10 分钟(可调,下限 10 分钟,上限 240) · whoami + 刷会话列表(发现新会话),再补拉一轮候选
按需 · 点开某个会话时 · 在线拉 30 条;群聊平时完全不进快车道,只有勾了才进
手动 · 顶部「同步」、会话里的「拉一次」、「设置 → 立即拉一轮」 · 不受间隔约束;同一会话 20 秒冷却,连点不会重复打接口
面板每 60 秒刷新一次状态(值守状态点 / 处理中),和拉取节奏是两件事。
刷新不会吞掉正在输入的内容:搜索、企微手动加入、详情发送框与设置页数字框都有内存草稿;
接口失败时保留上一份数据并给「重试」,屏蔽 / 清理待回 / 运行中移出会先确认。
为什么要给群单独一套规则:群消息量远大于私聊,绝大
…(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::14,最后更新 2026-11-06