hiwork-im
v0.2.0
Published
HiWork 助理插件:把企业微信智能机器人接进桌面端,用手机远程指挥本机 HiWork 干活,结果回到原聊天。
Readme
hiwork-im(HiWork 助理)
把企业微信智能机器人接到你电脑上的 HiWork:在手机上发一句话,本机的 HiWork 干活,结果回到原来的聊天里。
面向的是「人不在电脑前」的场景——开会、通勤、出差时,用手机把电脑上的活派出去。
- 设计文档:
hiwork-desktop/docs/superpowers/specs/2026-09-22-hiwork-im-design.md - 实施计划:
hiwork-desktop/docs/superpowers/plans/2026-09-23-hiwork-im-m0.md
M0 能做什么
| 能力 | 说明 | | --- | --- | | 扫码接入 | 在企业微信里扫一下,机器人就接到了本机(不用抄任何字段) | | 手动填入 | 从管理后台复制 Bot ID 与 Secret 填两个框(扫码不可用时的常驻退路) | | 远程干活 | 私聊机器人 → 本机的助理会话执行 → 答复回到聊天 | | 主动投递 | 助理可以主动发消息给你(会话里的 Agent 工具 / 助理页 / 定时任务) | | 助理页 | 中央「助理」,分历史会话与配置两个分区(下方;历史在前、也是默认分区) | | 设置区 | 设置 → 助理:与助理页同源的紧凑视图 |
助理页的两个分区:
顺序与默认:历史会话在前、且是默认分区(打开助理页第一眼要回答的是「它替我干了什么」,
配置是一次性设完就很少再动的东西)。默认页签必须是最左边那个,否则打开时高亮落在第二个上,
看着像「自动选错了」——tests/client-view.spec.tsx 有一条用例盯着这个不变量。
| 分区 | 内容 |
| --- | --- |
| 配置 | 接入状态(连接态 / 绑定人 / 工作区 / 生效权限 / 最近活动)、默认模型(选哪条线路的哪个模型,只影响助理下一次执行)、目录权限、工作目录(默认 <DSH_HOME>/assistant,改后要新建助理会话才生效——会话的工作目录在创建时就写死了;页面会明说并给「在新目录新建助理会话」按钮)、单次任务时长上限、助理人设与自定义指令、以及「发一条」「解绑」 |
| 历史会话 | 运行记录(时间、状态、耗时、答复摘要),可跳回原生会话看全过程;支持删除(单条即点即删、勾选批量、清空——后两者有 inline 二次确认)。删的只是这份索引,原生会话里的完整过程不受影响;进行中的记录删不掉(回合收尾时会被写回,所以 UI 直接不给入口) |
接入状态在配置分区里,不在页级:它回答的是「这台电脑上的助理接进来没有」,而绑定、 权限、模型、工作目录都是围绕它的设置;放在页级会让两个分区都为它让位,也没法跟着分区收起。 只要拿到了快照就渲染分区(已绑定 / 未绑定 / 通道被关掉都是同一套骨架)——少渲染一组标签条 的代价是绑定/解绑那一刻整个页面换形状,用户会以为自己点错了地方。
两处口径写在 src/config.ts 顶部:「未设置」≠「设成默认值」、非法值不静默降级、
fail closed(一次请求里任一项不合法就整次拒绝,不写存储)。
M0 不做:群聊、文件回传、卡片交互、审批卡、多通道(微信个人号/飞书)、跨设备会话切换。 理由见设计文档 §11。
安装(尚未发布到 npm)
本插件还没发布,装法是把打包产物挂进 DSH profile。
# 1) 打包
pnpm install && pnpm build && pnpm pack # 产出 hiwork-im-<版本>.tgz
# 2) 装进 profile(CLI 的 ~/.dsh/profiles/web、桌面端的 ~/.hiwork/profiles/<name>,两个都要装)
# 推荐:解包 + rsync —— 桌面端运行中也能安全更新,不重算依赖树、不动 lockfile
rm -rf /tmp/hiwork-im-pkg && mkdir -p /tmp/hiwork-im-pkg
tar -xzf hiwork-im-0.1.0.tgz -C /tmp/hiwork-im-pkg
rsync -a --delete /tmp/hiwork-im-pkg/package/ ~/.dsh/profiles/web/node_modules/hiwork-im/
rsync -a --delete /tmp/hiwork-im-pkg/package/ ~/.hiwork/profiles/<name>/node_modules/hiwork-im/
md5 -q ~/.hiwork/profiles/<name>/node_modules/hiwork-im/lib/index.js # 与本地 lib/index.js 比对,别只看版本号pnpm add file:*.tgz 也能用,但有两个坑(都实测过):必须用桌面端自带的 pnpm 11,它在
/Applications/HiWork.app/Contents/Resources/node/pnpm-package/bin/pnpm.cjs(用随包的 node 跑),
不在 dsh-runtime/node_modules/.bin/ 下;用系统 pnpm 10 会报 UNEXPECTED_STORE。
另外同名版本不会重装,先 rm -rf <profile>/node_modules/hiwork-im 再 add。
装完在 profile 的 package.json 里把 hiwork-im 加进 dsh.profile.bundles,重启即生效。
用法
- 打开中央「助理」→ 点扫码接入(或手动填入)。扫码后机器人就绑好了,状态卡变成「已连接」。
- 在企业微信里找到这个机器人,发一句话,例如:
帮我看看工作区里有哪些文件。 - 本机 HiWork 执行,答复回到聊天。点助理页里的「查看会话」可以看完整过程。
解绑:助理页 → 解绑(二次确认)。解绑只断开渠道与清除凭据,不删除助理会话的历史。
安全模型(请读完再用)
- 凭据单向:机器人 Secret 只落在 DSH 凭据域(
ctx.credentials)。存储域、日志、错误文案、 以及给浏览器的一切响应里都不会出现它——出站要过一道禁止键剥离(secret/secretRef/token…), 凭据引用名本身也由 botId 哈希派生(引用名会出现在诊断输出里,塞明文等于泄漏一半)。 - 来源校验:只响应绑定者本人的私聊。群聊、陌生人、以及 senderId 与绑定记录不符的消息一律静默忽略。
绑定记录里会记下授权人的渠道 id(
ownerPeerId)用于这条校验。 - 远程任务自动执行:与定时任务同理,IM 派发的回合没有人坐在电脑前点确认,因此回合内会设置
权限预设并把审批策略设为
never(否则每个工具调用都会卡在确认上,而你不在电脑前)。 这是本插件最大的能力面——被绑定的机器人等同于「能在这台电脑上派活的人」。 请把机器人当成你自己的凭据来保管:不要把它拉进群、不要转发扫码入口给别人; 怀疑泄漏时立刻解绑(解绑会同时清掉凭据)。 - 不给浏览器真值:Web 半边不发任何 HTTP、不读环境变量,一切经 loopback RPC;
它拿到的快照里只有掩码(
wow_ab…cd)。
开发
pnpm install
pnpm typecheck # tsc --noEmit(strict + exactOptionalPropertyTypes + noUncheckedIndexedAccess)
pnpm build # host → lib/index.js(自包含),client → lib/client.js
pnpm test # vitest
pnpm verify # typecheck → build → test(提交前跑这个)
pnpm pack # 产出 tgz- 仓库约定:分支
dev(remote 名origin);注释、文案、commit 全中文。 - 两个半边:
src/**(Host)与src/client/**(Web),各自独立打包。 client 半边对官方包只做 type-only import,运行时只 import react 与本地模块。 - 冻结契约:
src/protocol.ts是唯一事实源(RPC 频道与端点、feature/设置区座位、存储域与表名、 错误码)。改它等于改契约,要同步 client 侧常量与对撞测试。 - 凭据纪律:任何新增的 RPC 响应都要过
stripForbidden;任何新增的日志都不得打印 secret。
测试要「有牙」
守卫类断言写完必须做一次变异验证——把实现改坏,确认用例真的变红。本仓库已实测过的三条:
| 变异 | 期望结果 |
| --- | --- |
| resolveBinding 在凭据缺失时回退到 env | tests/credential.spec.ts 变红 |
| 幂等记录写在发送之前 | tests/delivery.spec.ts 变红 |
| 短路 stripForbidden | tests/rpc.spec.ts 变红 |
| 配置里时长上下界判断恒假 | tests/config.spec.ts / tests/service-config.spec.ts 变红 |
| 保存配置时跳过「目录是否存在」检查 | tests/service-config.spec.ts 变红 |
| 选目录时不看能力形态(拿 ok 当通过) | 同上(只看 ok 看不出来:能力对象上没有 pick,会变成 internal——所以要断言错误码) |
| 换助理会话时不清「会话实际用的目录」 | 同上(第一次写这条断言时是假绿:那个字段本来就没值) |
| 配置的模型被宿主默认覆盖 | tests/turn.spec.ts 变红 |
| 去掉 agent.ctx 这条人设取法 | tests/turn.spec.ts 变红 |
| 分区容器去掉 flex: 1(页面根有界且不滚) | tests/client-css-classes.spec.ts 变红 |
| 配置分区去掉 overflow-x: hidden | 同上 |
| 表单控件去掉 box-sizing: border-box | 同上 |
**假件必须照真件建模,宽窄两个方向都会骗人。**本轮实测踩到的两类:
| 假件失真 | 后果 |
| --- | --- |
| 假 Host 的 setup 收到 undefined(真机收到带 systemPrompt 的真 ctx) | 全部人设用例都在走真机上返回 undefined 的那条兜底,绿着放过一个真机不生效的路径 |
| 假 systemPrompt 重名注册不抛错、撤销不摘段落 | 「先撤旧的再注册」的顺序错了也测不出来 |
对照:假件的 mount 现在会把提示词服务装到 agentCtx 上、假 agent 自带 ctx 且与传给 setup 的是同一个对象(真机实测 agent.ctx === agentCtx)、重名注册会抛、撤销会摘段落。
遇到「测试全绿但真机不生效」,先怀疑假件比真件窄还是宽,再怀疑实现。
版式问题的判据在真引擎里,不在单测里。「内容重叠 / 没有滚动条」这类事 jsdom 测不出来
(它没有排版引擎),只能起真页面量几何:.hiwork-im-tabpanel > .hiwork-im-stack 的
clientHeight vs scrollHeight、每个卡片列的「直接子元素覆盖范围」vs「它自己的盒子」
(差 > 0 就是压到下一块上了)。两处都量过再改,改完再量一次。
单测能钉住的是规则本身(tests/client-css-classes.spec.ts 的「分区骨架与滚动」一组):
flex: 1 / overflow-y: auto / overflow-x: hidden / flex: none 这几条,
以及「凡 width: 100% 且带横向内边距或边框的规则都必须 border-box」——
最后这条是实测出来的:.hiwork-im-input 少 border-box 会比容器宽 22px,
把外层顶出横向溢出,多出一条横向滚动条。
做法:改一处 → pnpm exec vitest run <spec> 看是否变红 → 改回来再跑一次确认回绿。
(注意:pnpm exec vitest run ... | tail 的退出码是 tail 的,判定要看输出里的
Tests N failed,或者写成 ... > /tmp/out.txt 2>&1; echo $?。)
目录
src/
protocol.ts 冻结契约(频道/端点/座位/存储/错误码)
types.ts 跨半边结构契约
context.ts 对 DSH 运行时的结构端口(窄化类型,便于造假件)
store.ts 存储域 hiwork_im:settings / runs / deliveries
credential.ts 凭据层(secret 的唯一落点、掩码、引用名派生)
config.ts 助理配置的解析与校验(纯函数,三条口径:未设置≠默认 / 不降级 / fail closed)
persona.ts 助理人设(注册进助理会话自己的系统提示词作用域,不进用户消息)
turn.ts 无人值守回合(agents.create → followup → whenIdle → 摘要)
delivery.ts 主动投递(幂等)
rpc.ts loopback RPC 分发 + 出站禁止键剥离
service.ts 装配与生命周期
channel/types.ts 渠道抽象(M1 的第二通道实现同一接口)
channel/wecom/ 企业微信智能机器人(长连接 / 扫码 / 归一化 / 本地状态)
client/ Web 半边(助理页 + 设置区)
tests/ vitest(含假 Host:tests/support.ts)