Chuyển đến nội dung chính

dsh-send-to-feishu

Đã xác minh

@liaozhi/dsh-send-to-feishu · v0.5.1 · MIT · Giao diện web

DeepSeek Harness plugin that sends messages to Feishu: host + client halves, HMR development, npm/GitHub distribution.

Cài đặt

dsh plugin add @liaozhi/dsh-send-to-feishu

Xác nhận layer đã áp bằng dsh --profile default --dump-config — xem hướng dẫn cài plugin.

Mã nguồn

Phát hành lên npm mà không có repository công khai. Hãy kiểm tra nội dung package trước khi cài.

Thẻ

Readme

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。

使用本模板

  1. 复制本目录(或点 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、Typert package 字段与端点描述符 id、invariant 伴随件名;
    • 前缀:src/version-gate.ts 错误前缀、DSH_<前缀>_STRICT 环境变量名、 __DSH_<前缀>_DISABLED__ 全局标记名(src/shared/disabled-flag.ts);
    • 路径:cordis.dev.yml 的 root 绝对路径与顶部用法注释(按脚本自身位置 自动写回,不假设"包名 == 目录名")。

    脚本支持重复改名(基线始终取自 package.json),不依赖任何模板名常量。

  2. 手工改名(仅当脚本不可用):设 <当前包名> = package.json 的 name, <当前前缀> = 去掉 @scope/ 后全大写、-/. 转 _。先在全局把 <当前前缀> → 新前缀、<当前包名> → 新包名,再按下表逐项核对:

    位置 改成你的
    package.json 的 name 你的包名(如 @you/dsh-foo)
    cordis.patch.yml 的 id / name 同上
    tsdown.config.ts banner 的 id + CSS 虚拟模块前缀 同上
    src/index.ts 的 export const name + Typert package: 字段 同上(cordis 插件名,漏改 ⇒ 身份错位)
    src/client/index.ts 的 export const name + 两处 slot id 同上
    src/remote.ts 的端点描述符 id(<当前包名>#…) 同上(Typert 关联,漏改 ⇒ RPC 失败)
    src/invariant.ts 的 PACKAGE_NAME / name 同上(伴生插件名)
    src/version-gate.ts 错误前缀、DSH_<前缀>_STRICT 错误前缀变量名 新前缀 + _STRICT
    src/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 步)
  3. 安装依赖:pnpm install(全部来自 npm,会自动跑 prepare 完成首次构建)。

  4. 验证改名: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-hmr stat 轮询 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/ 内容变化才走热替换。
  • --patch overlay 不在运行时监听列表里,改 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 构建