dsh-send-to-feishu
已验证@liaozhi/dsh-send-to-feishu · v0.5.1 · MIT · Web 界面
DeepSeek Harness plugin that sends messages to Feishu: host + client halves, HMR development, npm/GitHub distribution.
安装
dsh plugin add @liaozhi/dsh-send-to-feishu 用 dsh --profile default --dump-config 确认 layer 已生效 —— 参见安装指南。
源码
发布到 npm 但没有公开仓库。安装前请检查包内容。
标签
说明文档
dsh-send-to-feishu
DeepSeek Harness 插件:把会话里的文字或文件发送到飞书群机器人。
- 设置面板「飞书机器人」:扫码创建应用机器人(
registerApp,OAuth 2.0 Device Authorization Grant,确认页确认即生效,无需手动进开发者后台),或粘贴 App ID + App Secret 绑定已有应用;支持接入多个机器人,每个机器人可独立选择目标群; - 工具
send-to-feishu:模型调用它向机器人发送文字消息或文件(targets数组指定目标机器人名称,省略则全部发送);没有接入任何机器人时工具不注册进会话(门控,见src/gate.ts); - 插件自更新:设置面板「插件更新」区块一键检查 npm 最新版并更新——host 半三级定位安装 profile(包根上溯 / link 开发副本按 realpath 反查 / 启动 argv 候选),先经
dsh plugin list --json校验官方通道可见,再执行官方命令dsh plugin --profile <name> add <pkg>@<latest>(见src/self-update.ts);client 半经 client-hmr 就地热替换,host 半重启 DeepSeek Harness 后生效;link 开发副本同样可更新(官方 add 会把开发链接替换为 registry 版本); - 设计与调研结论见
docs/research.md。
使用本模板
复制本目录(或点 GitHub 的 "Use this template"),然后改名——插件身份散布在 15+ 个位点,且部分藏在运行路径里(cordis 插件名、Typert
package字段、 端点描述符 id、invariant 伴随件名、__ModuleLoader__id)。漏改不报错, 只表现为"装上但功能不认",所以优先用脚本:pnpm rename <你的包名> # 例:pnpm rename @you/dsh-foo pnpm rename <你的包名> --dry-run # 先预览将改哪些文件,不写入scripts/rename.mjs以package.json的当前name为基线,一次性替换全部身份 位点并做全仓库残留检查:- 包名:
package.json、cordis.patch.yml的 id/name、tsdown banner 的__ModuleLoader__id + CSS 虚拟模块前缀、host/client 入口的export const name、Typertpackage字段与端点描述符 id、invariant 伴随件名; - 前缀:
src/version-gate.ts错误前缀、DSH_<前缀>_STRICT环境变量名、__DSH_<前缀>_DISABLED__全局标记名(src/shared/disabled-flag.ts); - 路径:
cordis.dev.yml的 root 绝对路径与顶部用法注释(按脚本自身位置 自动写回,不假设"包名 == 目录名")。
脚本支持重复改名(基线始终取自
package.json),不依赖任何模板名常量。- 包名:
手工改名(仅当脚本不可用):设
<当前包名>=package.json的name,<当前前缀>= 去掉@scope/后全大写、-/.转_。先在全局把<当前前缀>→ 新前缀、<当前包名>→ 新包名,再按下表逐项核对:位置 改成你的 package.json的name你的包名(如 @you/dsh-foo)cordis.patch.yml的id/name同上 tsdown.config.tsbanner 的id+ CSS 虚拟模块前缀同上 src/index.ts的export const name+ Typertpackage:字段同上(cordis 插件名,漏改 ⇒ 身份错位) src/client/index.ts的export const name+ 两处 slotid同上 src/remote.ts的端点描述符id(<当前包名>#…)同上(Typert 关联,漏改 ⇒ RPC 失败) src/invariant.ts的PACKAGE_NAME/name同上(伴生插件名) src/version-gate.ts错误前缀、DSH_<前缀>_STRICT错误前缀变量名新前缀 + _STRICTsrc/shared/disabled-flag.ts的__DSH_<前缀>_DISABLED__变量名__<新前缀>_DISABLED__cordis.dev.yml的 root 绝对路径(root 列表 + 顶部用法注释)本模板在你机器上的 lib/路径src/client/locales.ts的nav/title(slot label 来自这里)你的 UI 标识(如 "Meme 生成") LICENSE版权行你的名字 其余: README.md/AGENTS.md/ 示例文案里出现的旧名同上(残留检查见第 4 步) 安装依赖:
pnpm install(全部来自 npm,会自动跑prepare完成首次构建)。验证改名:
pnpm typecheck && pnpm build通过,且全仓库无旧身份串(改名脚本已 自动做残留检查;手工改名则用git grep复核)。若该目录此前已dsh plugin --profile web add安装过,改名后重跑一次add。
开发(HMR)
一次安装进 profile(client half 的发现依赖包名解析,必须安装而非 --patch 插路径):
# 安装了 dsh CLI:
dsh plugin --profile web add <本模板目录的绝对路径>
# 或用 harness 源码仓库:
cd <deepseek-harness 仓库> && pnpm dsh plugin --profile web add <本模板目录的绝对路径>
每个开发会话两个终端:
# 终端 1:构建监视器(tsc --watch + tsdown --watch,持续重写 lib/)
pnpm dev
# 终端 2:harness(叠加开发 overlay,重新启用 HMR 并监听 lib/)
dsh web --patch <本模板目录>/cordis.dev.yml --no-open
# 或源码仓库里:pnpm dsh web --patch ... --no-open
然后:
- 改
src/下的 host 半 → tsc 重写lib/index.js→ cordis-plugin-hmr 重载插件 → 终端打印新的host half loaded日志。在设置面板接入机器人后,让 agent 调用send-to-feishu工具验证。 - 改
src/client/下的 client 半 → tsdown 重写lib/client.js(<100ms)→ host 的 client-hmr 轮询发现 → SSE 广播 → 浏览器不刷新页面就地更新。打开http://127.0.0.1:3080→ 设置 → "飞书机器人"面板可见。
无浏览器验证 client 链:curl -N http://127.0.0.1:3080/plugins/events,改 client 源码会实时收到 data: {"type":"rebuilt","id":"dsh-send-to-feishu",...}。
安装版 dsh 注意:cordis-plugin-hmr 需要读 Node 内部模块加载器。若启动报
--expose-internals is required for HMR,用NODE_OPTIONS=--expose-internals dsh web ...启动即可(从源码运行的pnpm dsh自带 tsx,无此问题)。
依赖版本与追新
harness 处于 0.1 rc 快速迭代期,要点:
- peer 与 dev 同一下限:dev = 编译所用版本,peer = 运行时最低兼容声明(不强制 harness 升级,但旧环境装插件会得到 unmet peer 警告)。官方规则:every peer has a matching development range。
- 范围写法:用
^0.1.5-rc.1(= 已实测的最低版本,peer 与 dev 写同一个;当前窗口与追新记录见docs/compat-plan-0.1.5-rc.2.md与docs/compat-plan-0.1.5-rc.3.md)。不要用latest/*(dist-tag 异常/停在老线,反而拿到最旧),也不要用精确钉死(pnpm add默认写死,永不移动)。注意 semver 预发布规则:caret 只匹配同 [major,minor,patch] 元组的更高预发布号,所以^0.1.5-rc.1恰好覆盖{0.1.5-rc.1, 0.1.5-rc.2, 0.1.5-rc.3}——同 minor 线内追新(如 rc.1 → rc.2 → rc.3)只抬 MAX,peer/dev 依赖字段一行不动;跨预发布线(如 0.1.6-alpha.2)caret 不匹配,才必须 OR 双臂。 - harness 版本门禁:插件自带
src/version-gate.ts,要求 harness 版本落在[MIN_HARNESS_VERSION, MAX_HARNESS_VERSION]窗口内(当前为[0.1.5-rc.1, 0.1.5-rc.3];MIN = 代码实际使用的 API 面要求的最低版本,与 peer 下限对应,保持已实测的最低版本 0.1.5-rc.1;MAX = 已验证兼容的最新 tag,随bump:deps验证后上调,本次追新只抬 MAX 到 0.1.5-rc.3,peer/dev 仍是^0.1.5-rc.1)。dsh plugin add只是 pnpm 转发器,安装期没有插件版本检查钩子,所以门禁放在插件 apply 时执行:以 harness CLI 入口(process.argv[1])为锚点,向上找最近的package.json并校验包名为@deepseek-ai/dsh(CLI 包本体,dsh --version同源;不能读@deepseek-ai/dsh-tools等内嵌依赖当 harness 版本——CLI 包对内嵌依赖是 caret 语义,实装版本可能高于 CLI 本体,实测 rc.1 的 CLI 内嵌 rc.2 的 dsh-tools,读它会误判软禁用;也不能以插件自身为锚点——devDependencies 里的旧版开发副本会让解析永远命中插件自己的 node_modules,门禁静默失效)。落在窗口外默认软禁用:醒目错误日志 + 插件不注册任何业务能力,不影响 dsh web 启动(Loader/app-boot 对插件抛错零容忍,抛错 = 整个 harness exit(1));DSH_SEND_TO_FEISHU_STRICT=1恢复抛错 fail-loud。软禁用时 host 半经webserver/index-inject向页面注入__DSH_SEND_TO_FEISHU_DISABLED__标记(fiber-bound,卸载即撤、无残留),client 半读到后改挂"已停用"说明面板并展示支持版本窗口(src/shared/disabled-flag.ts是两侧共享的标记契约)。
追新流程:
pnpm bump:deps # ① 查最新 ② 抬 peer/dev 下限 ③ pnpm install
pnpm typecheck && pnpm build # 红 = 上游破坏性变更,按报错修
脚本 scripts/bump-deps.mjs 只抬当前 minor 线内的最新 rc(跨线 0.2.x 是破坏性变更,需手动改下限)。
- 不用
pnpm update:它会剥掉 rc 包的^(实测),且不碰 peer 侧。 - 跨线升级(0.2.x):同样抬下限到
^0.2.0-rc.x,peer/dev 一起。 - 锁文件会钉住首次安装版本:想每次克隆都最新就删掉它,否则按上面流程主动抬下限。
发布到 npm
npm login # 首次
pnpm publish # prepack 会自动完成构建
files 白名单只带运行必需的产物(lib/index.js、lib/invariant.js、lib/client.js、cordis.patch.yml),发布前建议 pnpm pack 检查内容。用户安装:
dsh plugin --profile web add dsh-send-to-feishu
上传到 GitHub
直接 push 源码即可(不要提交 lib/,它在 .gitignore 里)。本模板带了 prepare 脚本:用户从 git 安装时 pnpm 会用它现场构建产物(自包含,只依赖 npm 上的公开包)。
用户侧安装分两步(pnpm ≥10 的构建许可要求):
# 1. 首次 add 会被 pnpm 拒绝构建脚本,按提示把包名加进 profile 的
# pnpm-workspace.yaml(~/.dsh/profiles/<profile>/pnpm-workspace.yaml):
# allowBuilds:
# dsh-send-to-feishu: true
# 2. 重新执行
dsh plugin --profile web add github:<owner>/dsh-send-to-feishu#<commit-sha>
构建许可 = 允许安装时执行该包的代码,提醒你的用户只对他们信任的包开启,并建议 pin commit。 不想让用户配许可的话,也可以发 npm 或提供
pnpm pack产物(tarball 安装不跑构建)。
原理速记
- 一切都是 Cordis 插件;通过
ctx注册的都是可逆 effect,卸载自动清理——这是 HMR 热替换的前提。 - Host 半 HMR:
@deepseek-ai/cordis-plugin-hmr(dsh-base 已挂载,web 模式默认禁用,cordis.dev.yml按 id 重启用)监听构建产物,沿 Node 模块图重载受影响的插件条目。 - Client 半 HMR:
dsh.client声明让modules服务把包扫进window.__DSH_BOOT__并以/plugins/<id>/client.js供给;client-hmrstat 轮询 bundle 变化 → SSE 广播 → 浏览器按"invalidate → prefetch → 卸载旧 fiber → 物化新工厂"原地热替换。触发源是任何重写lib/client.js的进程,本模板用tsdown --watch。 - 分发形态即开发形态:
exports['.']恒指向lib/index.js,npm 包、git 安装、本地开发走同一条加载路径,没有"源码能跑、装上就挂"的落差。 - 依赖规则:
@deepseek-ai/*一律进peerDependencies(运行时由 harness 安装环境提供;dsh profile 的 pnpm 工作区是autoInstallPeers: false+ 安装目录兜底解析,不会为你的 peer 链去 npm 拉包),普通第三方库进dependencies,构建工具进devDependencies。不要把@deepseek-ai/*放进dependencies。 - HMR 只覆盖 link 开发的场景:cordis-plugin-hmr 沿 Node 模块图追踪时排除路径含
/node_modules/的模块,所以 npm/git 安装进 profile 的实体副本不参与热重载(它们是给最终用户的不可变产物);dsh plugin add <本地目录>是符号链接,realpath 后跳出 node_modules,才能被追踪。 dsh.client声明是包元数据,扫描缓存永不过期:改它要重启 harness;lib/内容变化才走热替换。--patchoverlay 不在运行时监听列表里,改cordis.dev.yml需重启;profile 的cordis.patch.yml和$DSH_HOME/cordis.patch.yml才是热监听的。
已知坑(模板已绕过,建议反馈上游)
Windows 下 cordis-plugin-hmr 默认的 ignored 含 **/.*,而 hmr 用 picomatch 匹配 path.relative() 的反斜杠结果时反斜杠被当转义符,..\.. 前缀会误命中 **/.*——base 目录之外的监听 root 被静默吞掉。cordis.dev.yml 用 ignored: [] 绕行。上游正解:匹配前归一化分隔符(或 picomatch windows: true)。
文件清单
├── src/index.ts # host half 入口(版本门禁 + settings 注册 + Typert 端点注册 + 门控组装)
├── src/spec.ts # settings schema(bots 列表,appSecret 为 secret 角色)
├── src/feishu-api.ts # 飞书 OpenAPI 薄客户端(token 缓存 / 发文字 / 上传文件 / 群列表 / verify)
├── src/registration.ts # 扫码创建应用机器人的注册状态机(registerApp,RFC 8628)
├── src/self-update.ts # 插件自更新(registry 检查 + 定位 profile + pnpm add)
├── src/service.ts # FeishuBackend(feishu/* RPC 面:列表/注册/绑定/验证/选群)
├── src/typert.ts # feishu/* 端点调用描述符表(第三方安全形态)
├── src/gate.ts # 业务门控(无机器人不注册工具/prompt)
├── src/tools/send-to-feishu.ts # send-to-feishu 工具(text/filePath + targets 路由)
├── src/shared/contract.ts # host/client 共享 wire 契约(脱敏投影 + 结果包络)
├── src/shared/disabled-flag.ts # host→client 的软禁用标记契约(全局变量名 + 载荷形状)
├── src/version-gate.ts # harness 版本门禁(软禁用语义 + CLI 锚点解析 + semver 比较)
├── src/invariant.ts # 官方 invariant 伴随件(每包必有)
├── src/css-modules.d.ts # CSS Modules 导入声明(*.module.css)
├── src/client/index.ts # client half 入口(词典/样式/槽位注册组装 + 停用标记分支,无 JSX)
├── src/client/locales.ts # zh/en 词典(所有 UI 文案走 locale key)
├── src/client/api.ts # 浏览器 → host 的 RPC 调用(ctx.connection.rpc)
├── src/client/useFeishuPanel.ts # 面板状态机(列表/扫码轮询/手动绑定/选群)
├── src/client/FeishuSection.tsx # 设置面板组件(纯渲染)
├── src/client/FeishuDisabledSection.tsx # 版本不兼容"已停用"说明面板
├── src/client/FeishuSection.module.css # 面板样式(CSS Modules + --dsw 设计令牌)
├── docs/research.md # 调研报告(飞书 API 边界 + DSH 机制 + 设计决策)
├── scripts/dev.mjs # pnpm dev:并行两个构建监视器
├── scripts/rename.mjs # pnpm rename <包名>:一键替换全部身份位点 + 残留检查
├── tsdown.config.ts # client bundle 构建(CJS 工厂 + 基线外部化 + CSS Modules 内联)
├── tsconfig.json # 全量类型检查(pnpm typecheck)
├── tsconfig.build.json # host 半构建(lib/index.js + lib/invariant.js)
├── cordis.patch.yml # bundle 层:安装时插入插件行
├── cordis.dev.yml # 开发 overlay:重启用 HMR 并监听 lib/
└── package.json # dsh.bundle + dsh.client 双清单,prepare/prepack 构建