DeepSeek Harness Plugin Hub

Publish and manage complete Harness Profiles. Discover Plugins for your next setup.

Explore

PluginsPresetsDocsNews

Community

Publish a pluginContactReport an issue

Resources

Plugin Hub on GitHubDeepSeek HarnessSystem statusPrivacy notice
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

Independent and unofficial. Not affiliated with, authorized by, or endorsed by DeepSeek.

Feishu Cui — DSH Plugin for DeepSeek Harness
← Plugins
F

dsh-feishu-cui

Feishu Cui

Remote DSH without handling operations yourself: use 飞书 private chat as the operations interface (CUI), with messages and cards delivered through a 飞书 bot long connection

The plugin will be installed here. Keep web if you are unsure.

npx -y @deepseek-ai/dsh plugin --profile web add github:wanjiaju3108/dsh-feishu-cui#9f63098f07ba76f0b768c38230219a99e8125e2c
READMECompatibilityVersions

Compatibility and provenance

Feishu Cui is published as dsh-feishu-cui and currently resolves to version 0.2.2. The Hub verifies its manifest and preserves the exact installation source for reproducible installs.

DSH compatibility
*
Runtime surfaces
web
Release source
github
Registry updated
9/25/2026

Versions

0.2.2stable
9/25/2026
0.2.1stable
9/24/2026

Related plugins

Loading related plugins…

Latest
0.2.2
DSH
*
HMR
Process restart
Tree shaking
Safe tree shaking not declared
Unpacked size
Unavailable
Files
Unavailable
Surface
web
License
MIT
Source
github
GitHub
★ 0
Weekly downloads
0
Last push
9/25/2026
View source ↗
README badge

Click the badge to copy Markdown for your README.

Do you maintain this Plugin?Claim benefit · Priority security scan

Verify the GitHub repository declared in package.json to manage this listing. After you claim it, Hub will prioritize a security scan of the current version and publish the result when it passes.

Claim this Plugin →
Report an issue
DeepSeek Harness Plugin Hub
ProfilesPluginsCategoriesNewsDocsSign inManage Profiles
ProfilesPluginsCategoriesNewsDocsSign in

Related plugins

More verified plugins in integrations-communication.

Acp App@deepseek-ai/dsh-acp-appThe dsh ACP profile bundle: automation-only JSON-RPC stdio and process lifecycle over dsh-baseRemote Web Ui@linxin666/dsh-remote-web-uiScan-to-pair remote access for the dsh web GUI that shares one official interface: a QR beside the settings button pairs phones and PCs into the same Web GUI (a portrait-touch adaptation layer for phones, full desktop on PCs) through one-time tokens and rUniver Officedsh-univer-officeDSH × Univer integration with a bundled collaboration Gateway and Viewer: inline previews, live floating Worktree windows, and session-end review actions in DeepSeek Harness.DSCODE@toddzheng024/dscode-bundleA complete DeepSeek coding agent with persistent shell, Ultra collaboration and automatic permission review.

README

dsh-feishu-cui

免运维的远程 DSH:一套完整的远程使用方案:拿飞书私聊当操作界面(CUI)——装一个插件、填一对凭据、在飞书里配对一次,之后在手机上就能用 DSH;常用操作都是卡片和菜单里点,不用像 TUI 那样记命令:发消息排进当前会话的队列、回答回到你那条消息上,工作区 / 会话(切换与改名)/ 模型 / 推理深度 / 权限 / 余额都在飞书里点,agent 反问与工具审批也在飞书答。

往下看:这是什么 · 怎么用 · 边界与约定 · 代码结构 · 用例脉络 · 看日志 · 术语表 · 还没做

这是什么

一套远程使用 DSH 的入口:拿飞书私聊当操作界面(CUI)——提问、切会话、切模型与权限、打断、答反问、批工具都在飞书里做。执行面在宿主:真正跑命令、改文件的是 DSH 里的 agent,沙箱与审批策略由 DSH 决定。它由两半组成——宿主半边的插件(lib/,跑在 dsh web 里)和浏览器半边的设置页(lib/settings/client.js,挂在 DSH 的 Web 界面上)。

它替你省掉的运维(这是它跟「自己搭一套远程访问」最大的区别):

  • 不用公网入口:不需要公网 IP / 域名 / HTTPS 证书 / 反向代理。
  • 不用内网穿透:不需要 frp / ngrok / tmate 这类东西。
  • 不用开端口、改防火墙 / 安全组:机器人往飞书出站建一条长连接,网络侧什么都不用动。
  • 不用自建中转服务:消息走飞书官方通道(机器人长连接 + 卡片回调)。
  • 不用写客户端:飞书本身就是客户端——私聊收回答,卡片做选择,菜单做入口。
  • 不用自己搓鉴权:配对码一次性绑定 owner,机器人只服务他一个人。
  • 不用盯着断线:自动重连 + 存活看门狗(见长连接的存活与恢复)。
  • 不用额外起服务:它就是 dsh web 的一个插件,装完跟着宿主加载。
  • 不用搭服务端:服务端就是飞书。要自己动手的只有开发者后台那几步——建一个自建应用、加 3 项订阅(两个事件 + 卡片回调)、补 2 条权限、配 8 个菜单项、发一次版本(见飞书那边)。

跟 TUI 比,省的是「记」:终端里每一步都要敲命令、记参数、盯滚动;这里工作区 / 会话 / 模型 / 推理深度 / 权限 / 余额各是一张卡,切会话、改名、打断、答反问、批审批都在飞书的消息和按钮里完成,回答做成卡片回到你发消息的那条上。

能做什么

  • 私聊发一句话 → 排进「当前会话」的队列(不打断正在跑的那一轮),回答回到那条消息上:卡片跟着状态走——被取走后是「处理中」(带「停止」按钮),刚进队列还没被取走是「排队中」(带「撤回」按钮),之后逐段把正文接上去,跑完收尾成「已完成」/「已停止」/「处理失败」。正文超过卡片体积上限(30KB)就换成「任务失败」+ 一句「去网页端看」。
  • 菜单八项:申请配对、工作区列表、会话列表(含「新建」)、会话重命名、模型、推理深度、权限、余额。
  • agent 反问(ask_user_question)与工具审批(工具要授权)都在飞书弹卡:谁触发的谁答。
  • 别处(网页端 / CLI)改了当前会话的模型 / 权限 / 会话名 → 主动私聊通知 owner 一张「模型已修改」/「权限已修改」/「会话名已修改」卡。
  • 长连接自愈:休眠 / 半开连接有存活看门狗(见长连接的存活与恢复)。

不做什么(能力边界,详细取舍见边界与约定)

  • 只服务 owner 一个人:别人发消息、点菜单一律回同一句「CUI会话未匹配」,且不区分「还没绑定」和「不是 owner」。
  • 只认私聊、只认文本:图片 / 文件 / 附件永不支持。
  • 只认「飞书当前会话」:别的会话怎么聊都不回答、也不推送。
  • 反问只接「有选项、且不是多选」的题;多选题与全靠打字的题让给网页端(让路之前先回一句「这题飞书答不了」)。
  • 模型 / 推理深度 / 权限都是会话级设置,只作用于当前会话。
  • 卡片上不能自由输入文字(只有配对填配对码、会话重命名填名字这两张卡能输)。

前置条件

  • 本机已装 DSH(dsh 与 dsh plugin 可用),profile 用 web——设置页挂在它的 Web 服务器上。
  • 一个自己的飞书自建应用(要能开长连接),并且只给自己用:开发者后台的可用范围只勾自己。
  • 别跟 dsh-feishu-assistant 共用同一个飞书应用:长连接是集群模式、不广播,同 App 会互相抢事件。凭据引用名也各用各的(这个插件读 FEISHU_CUI_APP_ID / FEISHU_CUI_APP_SECRET)。

依赖的宿主能力(缺了哪个,对应功能就自动退化,并在日志里说明原因)

credentials、settings、webServer、sessionController(投喂 / 打断 / 撤排队项 / 模型目录 / 会话句柄)这四项写在 inject 里;另外按需取 sessionQuery(列会话)、workspaceRegistry(读工作区)、permissionPresets(读权限预设)、userQuestions 与 approval(两条要人答的 waterfall)、sessionProjections(读当前模型)。卡片按飞书卡片 JSON 2.0 写。

怎么用

安装与前置

dsh plugin --profile web add dsh-feishu-cui

装完重启 dsh web。装在别的 profile 上就把 --profile 换成那个名字;本机改这个插件自己的代码时,用 link: 指到仓库目录也一样。

DSH 版本:当前 0.2.x 需要 DSH 0.1.7-rc.1 及以上——设置走的是 0.1.7 起的插件条目 config(带 volatile 那几个字段)。 还在 DSH 0.1.5 上的,装最后一个支持它的版本:

dsh plugin --profile web add dsh-feishu-cui@0.1.0

装的时候如果收尾报 ERR_PNPM_IGNORED_BUILDS: protobufjs,插件其实没装上。 @larksuiteoapi/node-sdk 的依赖里有 protobufjs,它带 postinstall 脚本,pnpm 默认不跑;dsh 把 pnpm 的非零退出当成整体失败,于是没把插件登记进 profile。先放行再装一次:

# ~/.dsh/profiles/<profile>/pnpm-workspace.yaml
allowBuilds:
  protobufjs: true

插件分两半:宿主半边 lib/index.js 跟着 dsh web 加载;浏览器半边是设置页,插件装上后在 DSH 的 Web 界面里出现「飞书CUI会话」分区。

改完代码要生效得重启 dsh web——插件只在启动时加载一次。

飞书那边

开发者后台 → 你的自建应用:

  • 事件与回调的订阅方式选 长连接;
  • 事件加 im.message.receive_v1、application.bot.menu_v6;
  • 卡片回调加 card.action.trigger;
  • 权限补 im:message.p2p_msg:readonly、im:message:send_as_bot;
  • 可用范围只勾自己——机器人只对你一个人开放,别放给部门或全员。

机器人自定义菜单:下面这套是推荐配置,不是硬要求。

硬约束只有三条,少一条就不工作:

  • 子菜单都选 推送事件;
  • event_key 必须和下表完全一致(插件按它分流);
  • 父菜单只当容器、不配事件——配了它会推一个插件不认识的 event_key:点了没反应(它不执行动作,也不回话),只有日志里多一句「这个菜单项还没有去处」。

至于分几个父菜单、叫什么名字、谁挂在谁下面、子菜单怎么排序,插件一概不看(它只认叶子的 event_key),怎么分都行;展示形式也随你。飞书后台的顺序是先建子菜单、再建父菜单。

下面这套是现在后台里的实际配置,可以照抄:

父菜单子菜单(从上到下)event_key点了会怎样
会话设置模型model发「模型」卡
推理深度effort发「推理深度」卡
重命名session-rename发一张输入框卡片,填新名字(改的是当前会话)
权限permission发「权限」卡
导航会话列表sessions发「选择会话」卡(底部多一个「新建」)
工作区列表workspaces发「选择工作区」卡
操作查询余额balance发「账户余额」卡
申请配对pairing发一张输入框卡片,让你填配对码(见下一节)

「模型」和「推理深度」是两张独立的卡,所以两个菜单项都要配;工作区 / 会话 / 模型 / 推理深度 / 权限各是一张卡带一组选项,一个菜单项就够,不需要为每个选项各配一项。权限那三个预设(read-only / workspace-write / danger-full-access)配不配都行,配了就也打开权限卡,只是冗余。

  • 创建版本并发布,菜单生效要等 5 分钟左右。

机器人只对 owner 一个人开放。 可用范围里只勾你自己,这是第一道闸;插件这层是第二道: 除了配对,别人发消息、点菜单一律只收到「CUI会话未匹配」,卡片上的操作也只回一句同文案的提示—— 不静默、不执行任何动作,而且不区分「还没绑定」和「不是 owner」(对外不暴露绑定状态)。 所以聊天里不会提示你去配对,配对指路只在设置页和本 README 里。

别和 dsh-feishu-assistant 共用同一个飞书应用:长连接是集群模式、不广播,同 App 会互相抢事件。

插件里配对

在飞书里点菜单「申请配对」,机器人发一张输入框卡片,卡片上写着「设置页里有一串配对码,把它填到下面这个输入框里。」打开 DSH 的设置页 →「飞书CUI会话」,那里显示这串码;把它填进卡片、点「确定」,填的那个人就成为 owner。

  • 配对码 10 分钟有效、只能用一次;已经绑过就不再生成(再点菜单「申请配对」只会收到「存在绑定会话,无法申请」)。
  • 设置页的状态快照里有:长连接是否建立、当前绑定的 user、在册的配对码、两项凭据配没配。页面不轮询,随时点「刷新」重新读一次。
  • 换人要先解绑:设置页点「解绑」→ 清掉绑定的 user、在册的配对码一并作废;同时给原 owner 私聊发一张「已解绑」的卡(发不出去只记日志,不影响解绑)。之后回飞书重新点「申请配对」拿新码。 解绑不掐正在跑的那一轮、也不撤排队里的消息:那一轮照旧跑完、回答卡也会走完,但原 owner 之后发消息、点卡片就只会收到「CUI会话未匹配」。
  • 凭据(App ID / App Secret)也在同一页填:保存后不再回显,存进 $DSH_HOME/.credentials.yaml,保存即按新凭据重建长连接。

日常用法

  • 私聊机器人发消息(owner 专用):回你一张卡片(回复在你发的那条消息上),之后这一轮每产出一段就把那张卡整张换掉,跑完收尾。 上屏的只有正文(助手消息里的 text 块):思考、工具调用、工具结果都不上屏——思考一轮能写几万字、工具结果常常是整份文件,一张卡片装不下。想看完整过程在网页端看。 这句话同时排进当前会话的队列,不打断正在跑的那一轮;队列不设上限。 卡片的时机:进队列时先不开卡;被这一轮取走就是「处理中」;排了一小会儿(100ms)还没被取走,就先开一张「排队中」。不做兜底:两种信号一条都没来就不开卡。 跑完了却没有正文,卡片上补一句「这一轮没有可显示的正文」,不会一直挂在「处理中」。 「停止」:打断当前这一轮(跟网页端「停止生成」同一件事,走会话控制器的取消)——只停正在跑的这轮,已经在队列里排着的不受影响。点完先变「正在停止」,这一轮真正收尾时把已产出的正文留着、标题改成「已停止」。按钮只出现在飞书自己发起的那一轮。 「撤回」:把还没开跑的那条从队列里撤掉(走宿主的排队项移除),卡片换成「对话已取消」。 正文装不进一张卡片(超过 30KB)就不再往上接:卡片换成「任务失败」+「回答内容过多,卡片无法全部展示,请到网页端查看」。 还没选会话时回一句「还没有当前会话」;不是文本的消息回一句「只支持文本消息」(图片 / 附件永不支持)。 投喂走宿主的会话控制器(mode: 'queue'),不是自己往会话日志里塞事件——模型可用性、会话是否还在这些校验都交给宿主。
  • 会话列表里只列当前工作区的会话,摘要写「本工作区有 N 个会话」;一次都没跑过的空会话(没标题那种)不列。底部「新建」会在当前工作区里开一个新会话并切过去。
  • 会话重命名改的是当前会话:点菜单发一张输入框卡片,填新名字、点「确定」;名字由宿主归一化(去掉转义序列与控制字符、空白压成一个、超长按字节截断),卡片先换成「已请求:会话改名为【X】」,宿主要是没收下就另发一张卡写「会话改名失败」;真改成了由宿主那条事件发一张「会话名已修改」。
  • 有活没干完时不让换会话、换工作区:还有回答卡在册(排队中 / 处理中 / 正在停止)时,那两张卡的「确定」「新建」会被挡下来,回一句「有正在进行的任务,无法切换会话 / 工作区」。

主动通知

  • 当前会话的模型(连同推理深度)、权限或会话名被改了,就往飞书发一张卡:标题「模型已修改」/「权限已修改」/「会话名已修改」,正文「模型改成【X】」「推理深度改成【X】」「权限改成【X】」「会话名改成【X】」。
  • 不管是谁改的:宿主那三条事件(model/selection、permission/preset、session/title)里没有「谁改的」这个信息,所以网页端改、CLI 改、以及你自己从飞书那几张设置卡改,都会收到这张卡。 从飞书改的那一次,它正好就是这次请求的回应:设置卡先变成「已请求:…」,紧接着这张「…已修改」的新卡到。
  • 会话名那条事件要挑一下:自动起的标题(模型生成的、兜底的)也走它,只有 source.kind 是 user(人手动改名)才通知。
  • 一次权限变更只发一张:预设名变了才通知,跟着变的沙箱模式与审批策略不单独报(它们就是预设的内容)。

长连接的存活与恢复

  • 存活看门狗:发出去的 ping 15 秒没收到任何回帧就判定连接死了,拆掉重连。SDK 默认不开这个看门狗,不开的代价很隐蔽:Mac 休眠再回来、或者 NAT 悄悄掐掉连接之后 TCP 是半开的,没有 FIN/RST,socket 层永不报错——连接看着还是「已连接」,而飞书事件从此一条都收不到。
  • 自动重连:SDK 自己重连(日志里是「飞书长连接断开,开始重连」→「飞书长连接已建立」)。断开这段时间的消息飞书会补推,恢复之后可能一次到一批。
  • 迟到的消息直接丢:事件时间比本机时间早 3 秒以上的,不回执也不投喂(补推来的旧消息不能当成新话)。判据是本机时钟,机器时间快 3 秒以上会把所有消息都判成迟到。
  • 同一条消息只处理一次:按飞书消息 ID 记账,最近 200 条(重投只发生在断线重连前后,够用)。

自检与排查

设置页「飞书CUI会话」那一块就能看大半:长连接是否建立、绑的是谁、在册的配对码、凭据配没配。页面不轮询,点「刷新」。

常见症状:

症状大概是什么
发消息没任何反应长连接断了(看日志里「飞书长连接断开」);或者飞书后台的事件订阅没配
菜单点了没反应,日志里写「这个菜单项还没有去处:…」飞书后台那一项的 event_key 和上表不一致;或者你给父菜单配了事件(父菜单只当容器,不该推事件)
回「CUI会话未匹配」还没配对,或者发消息的人不是 owner
回「还没有当前会话」先去菜单里选一个会话
回「只支持文本消息」发的是图片 / 文件 / 附件(永不支持)
卡片点了只回「这张卡片已失效…」这张卡已经被新卡顶掉,或者已经处理过了
设置卡点完只留「已请求:…」这是设计:请求交出去了,结果一律另发一张卡——成功是宿主那条事件发的「…已修改」,失败是「…失败」那张
反问卡没弹到飞书这一轮不是飞书发起的(让给网页端了);或者题目是多选 / 没选项

边界与约定

  • 谁触发的谁答:只有飞书这边发起、而且正在跑的那一轮,反问卡和审批卡才会弹到飞书;网页端发起的轮次一律让给网页端的 UI(不然人坐在网页那边会干等)。判据是运行期那个「正在跑的那一轮是哪条飞书消息」的槽。
  • 子代理问不到人:两条路都被宿主挡着——反问那边,被别人拥有的子代理一问就抛 DELEGATED_CALLER;审批那边,子会话的审批策略在派发时被钉成 never,需要审批的操作当场被拒。所以子代理的反问 / 审批根本走不到飞书。
  • 会话级设置,下一次请求生效:模型、推理深度、权限都是写进当前会话的,不影响别的会话;改完从下一次提问开始按新的来。
  • 设置卡点完只留「已请求」:模型、推理深度、权限、会话名这四张卡都一样——点「确定」之后卡片变「已请求:…」(校验过了、请求交出去了),后台才真正提交;结果一律另发一张卡:交成了由宿主那条事件发「…已修改」,没交成发一张写失败原因的卡。确定那张卡此后不再动,一直停在「已请求」。宿主那条会话名事件还带着自动起的标题(模型生成的和兜底的),这里只认人手动改名(source.kind 是 user)那一种,不然每开一个新会话都要通知一次。
  • 换会话 / 换工作区要有活没干完:有回答卡在册时挡下来(见日常用法)。
  • 卡片体积:一张卡上限 30KB,回答卡到顶就换成「任务失败」;选项卡、反问卡在发之前也量一次,装不下的让给网页端。
  • 反问只接单选且有选项:多选题、没有选项的题,飞书这边先回一句「这题飞书答不了(没有选项或者是多选题),去网页端答吧」,再把请求让给网页端。
  • 反问可以不答:一道题都没选也能点「确定」,没答的题按跳过交回(selected: [],跟网页端「跳过本题」交回的形状一样);点完换成的那张文字卡上,没答的题写「已跳过」。
  • 空会话不列:一次都没跑过(0 轮)的会话不进会话列表——它没有标题,列出来只能显示一串会话 ID。读不到运行期投影时照样列(宁可多列不可漏列)。
  • 在册的选项类卡片只有一张:全插件同一时刻只认一张;新发一张就把上一张作废(换成「…已取消」)。回答卡、反问卡、审批卡不在这张名册里,各自按自己的键在册。

代码结构

按「谁跟谁说话」分层,上层依赖下层,反过来不依赖:

dsh-feishu-cui/
├── package.json        三条脚本:check(对每个模块 node --check)、test(回归用例)、prepublishOnly(发布前跑前两条)
├── README.md           装 / 配 / 用法 / 通知 / 休眠 / 边界 / 结构 / 脉络 / 日志 / 术语
├── CHANGELOG.md        每一版改了什么
├── test/               22 个文件  2285 行   回归用例:不联网;单元那份用假出站,端到端那份真起插件(假 SDK + 假宿主)
└── lib/
    ├── index.js              1 个文件   164 行   装配:造对象、接线、生命周期
    ├── init.js               1 个文件    26 行   启动时的初始化:把当前会话缓存填上
    ├── notices.js            1 个文件    73 行   给绑定的人发卡:连上通告、解绑
    ├── cache/                7 个文件   245 行   需要跨文件读写的运行期状态
    ├── common/               4 个文件   427 行   文案总表、飞书事件、CUI 事件、凭据引用名
    ├── driving/              6 个文件   559 行   判定与分派(飞书那头 / 宿主那头)
    ├── handler/              19 个文件  2353 行   干活:菜单、卡片、回答、反问、审批、通知
    ├── infra/                13 个文件  1220 行   跟外面打交道:飞书出站、宿主服务、插件配置、本机防休眠
    ├── settings/             4 个文件   750 行   设置页(四条回环路由 + 浏览器半边)
    ├── transport/            6 个文件   284 行   连接:飞书长连接 / REST,宿主事件订阅、waterfall
    └── ui/                   6 个文件   501 行   卡片长什么样(只出 JSON)

各层干什么:

  • lib/index.js:唯一的入口。apply() 里按顺序造出所有对象、把依赖递进去(transport → push → 各 handler → 路由 → 准入 → 两条宿主事件订阅 + 两条 waterfall 订阅),注册设置页,最后按凭据建连。不写业务。
  • init.js:启动时那一步初始化。读配置里配的当前会话,问宿主还在不在(session.js 的 hasSession),把结论写进 cache/current-session.js;之后上层读 readSettings() 拿到的就是这个缓存,不再问宿主。
  • notices.js:给绑定的那个人发卡。连接状态变了发一张「dsh 已连接,当前会话为【…】」;解绑时把 user 清掉、在册的配对码一起作废,再给原 user 发一张「已解绑」。
  • transport/:门外那一段。feishu/ 是飞书侧(websocket-client.js 长连接与心跳看门狗、http-client.js REST、transport.js 把两个客户端一起持有、换凭据时整组重建);host/ 是宿主侧(session-events.js 订 session/event、agent-events.js 订收件箱三条、waterfall.js 订要人答的两条 waterfall)。
  • driving/:判定和分派。feishu/ 把飞书原始事件转成 CUI 事件(receiver.js)、判准入(admission.js:去重、迟到、鉴权、只认文本、有没有当前会话)、按事件分给处理函数(router.js);host/ 对宿主事件做同样三件事,router.js 只把要盯的那几种事件分给 handler/host/。
  • handler/:干活的地方。feishu/ 是菜单和卡片点出来的(七个菜单各一张卡 + option-card-flow.js、input-card-flow.js 两套共用骨架 + deferred-submit.js 两套共用的后台提交 + message.js 投喂 + pairing.js 配对 + session-rename.js 会话重命名 + warn.js 判定没过时回话);host/ 是宿主推过来的(waterfall.js 是反问与审批共用的骨架 + answer.js 回答卡、question.js 反问卡、approval.js 审批卡、settings-watch.js 设置变更通知)。
  • infra/:跟外面的接口。feishu/push.js 发出站(发卡 / 换卡,失败重试);host/ 是宿主服务的封装(会话、工作区、模型目录、权限、余额,以及按名字借服务的取用口);plugin/ 是插件自己的配置与凭据(走宿主的 settings / credentials 服务);system/sleep-guard.js 是插件自己起本机进程那块——持有一个 caffeinate -s,插件跑着就防休眠。
  • ui/:只造卡片 JSON。六种:text-card.js(只有正文 / 带标题栏两种)、option-card.js(选项卡:确定|取消,和确定|新建|取消两种)、input-card.js(输入框)、answer-card.js(带一个按钮的回答卡)、approval-card.js(允许|拒绝)、(共用件)。给人看的字一律不在这里。

依赖方向:driving/ 认 handler/ 和 ui/(只为拿卡片类型与判定原因那几个常量),handler/ 能 import ui/、infra/、cache/、common/(文案在 common/copy.js),ui/ 只 import ui/,transport/ 只认 common/(事件名与凭据引用名),收到的东西交给装配时递进来的回调。

改完跑两条:npm run check(每个模块过一遍 node --check)、npm test(跑 test/ 下的 18 个用例,全部不联网)。用例分两档:*-check.mjs 里的单元那份只造要测的那几个对象(假出站、假会话目录);端到端那份(connection / no-credentials / inbound / answer-card / waterfall / settings-routes)用 harness.mjs 的 startPlugin() 真调一遍 apply(),把上下文、宿主服务和飞书 SDK 都换成假的(fake-lark.mjs 配 lark-hooks.mjs 顶掉那个 SDK)。

用例脉络

A. 入站:飞书 → 会话

transport/feishu/websocket-client.js(长连接收到私聊消息)→ driving/feishu/receiver.js(转成 CUI 事件:正文、消息 ID、操作者)→ driving/feishu/admission.js(过期丢弃 → 查重记账 → 是不是 owner → 是不是文本 → 有没有当前会话)→ driving/feishu/router.js → handler/feishu/message.js → infra/host/session.js(prompt)→ 宿主的会话控制器(mode: 'queue')。

投喂进去的那条随后会被宿主取走:transport/host/agent-events.js(agent/inbox/inserted / claimed / discarded)→ driving/host/receiver.js → driving/host/router.js → handler/host/answer.js(开卡 / 改标题 / 撤回),并把「这一轮是哪条飞书消息」记进 cache/running-turn.js。

B. 出站:会话 → 飞书

transport/host/session-events.js(session/event 那条 firehose)→ driving/host/receiver.js(assistant/message 里只取 text 块当正文;turn/end 里取结束原因)→ driving/host/router.js → handler/host/answer.js → ui/answer-card.js(有按钮那几张)/ ui/text-card.js(终态)→ infra/feishu/push.js(sendCard / patchCard)。

C. 菜单与五张选择卡

菜单:websocket-client.js(application.bot.menu_v6)→ driving/feishu/receiver.js(tag = 飞书的 event_key)→ admission.js(配对放行,其余只给 owner)→ router.js → 七个 handler(sessions / session-rename / workspaces / model / effort / permission / balance,外加 pairing)→ ui/option-card.js / ui/input-card.js / ui/text-card.js → push.sendCard,消息 ID 记进 cache/pending-cards.js。

卡片回调(card.action.trigger)→ admission.admitCard(是不是本人;换会话、换工作区那两个按钮还要看有没有活没干完)→ router.js → handler/feishu/option-card-flow.js(点一行只选中 → 重画;确定 → 交给各家的 onConfirm;取消 / 没选就确定 → 按取消)或 input-card-flow.js(重命名那张卡:确定 → 交给 onSubmit)→ infra/host/{session,workspace,models,permissions}.js → 宿主。四张设置卡都先回「已请求」,后台提交(handler/feishu/deferred-submit.js)——交成了由宿主那边的事件发「…已修改」(handler/host/settings-watch.js),没交成由这里另发一张写失败原因的卡。

D. 要人来答的两条口

反问:transport/host/waterfall.js(user-questions/request,带 prepend 抢在网页端前面)→ handler/host/question.js(四道检查:是当前会话、这一轮是飞书发起的、题目画得出来、卡片发得出去;发 ui/option-card.js 的卡)→ 卡片回调 → 点「确定」交回 { answers }(没答的题是空选择);取消拒 ASK_CANCELLED;这一轮被停掉作废并拒 ASK_ABORTED。

审批:同一条 transport(approval/request)→ handler/host/approval.js(同样四道检查,发 ui/approval-card.js)→ 卡片回调 → 交回 allowed-once / rejected;这一轮被停掉交回 cancelled 并把卡片收成「审批已结束」。批完那张换成 ui/text-card.js 的带标题栏卡片,正文照旧留着。

E. 生命周期、换人与换会话

启动:lib/index.js(apply)→ infra/plugin/config.js(注册 feishu-cui 那个设置命名空间)+ infra/plugin/credentials.js(凭据存储)→ init.js(把当前会话缓存填上)→ transport/feishu/transport.js(按凭据建连)→ notices.js(连上给 owner 发一条「dsh 已连接,当前会话为【…】」);apply 里同时按开关起 infra/system/sleep-guard.js(防休眠)。

换会话 / 换工作区:菜单 → handler/feishu/sessions.js / workspaces.js → infra/host/session.js(create / open)或 infra/host/workspace.js → 写回 feishu-cui 那几个设置键。

换人(解绑):设置页 → settings/panel.js(/dsh-feishu-cui/user/unbind)→ notices.js 的 unbindUser(先把 userId 读出来 → 清 userId + 作废配对码 → 给原 user 发一张 ui/text-card.js 的「已解绑」卡,这一发送不等着发完)。

F. 设置页(浏览器半边)

settings/panel.js 挂四条只判回环与 JSON 的路由(settings/http.js)→ settings/client.js 在 DSH 设置面板里注册「飞书CUI会话」分区:填凭据、看状态与配对码、点解绑。

G. 主动通知

session/event 里的 model/selection / permission/preset / session/title → driving/host/receiver.js(把事件载荷一起带出来)→ driving/host/router.js → handler/host/settings-watch.js(拼标题与正文;会话名那条只认人手动改名)→ ui/text-card.js(带标题栏)→ push.sendCard。

看日志

插件不自己写日志文件:infra/plugin/log-exporter.js 把这一路日志按级别交给 stdout / stderr,由启动 dsh web 的那一层落盘(用 supervisor 跑的话就是它的日志)。

排查时值得搜的几行:

  • 已订阅 session/event、已订阅 agent/inbox/...、已订阅 user-questions/request、已订阅 approval/request——插件加载时各订了什么;
  • 飞书长连接已建立 / 飞书长连接断开——连接状态;
  • 已把消息交给会话 <会话 ID>——投喂成功;
  • 回答卡片已开出 / 画回答卡片(…,N 字)——这一轮的卡片在长;
  • 反问卡片已发出 / 反问卡片:… 第 N/M 题选了【…】 / … 被人取消——反问那条口;
  • 审批卡片已发出 / 审批卡片:… allowed-once——审批那条口;
  • 会话设置被改了,通知绑定的人:…——主动通知;
  • 这批题飞书答不了、这一轮不是飞书这边发起来的,让给网页端——让路的原因。

术语表

说法指什么
owner配对绑定的那个人,也就是设置里那个 userId;机器人只服务他
会话宿主里的一个 DSH 会话;插件同一时刻只认「当前会话」那一个
一轮宿主的一次问答(turn/start 到 turn/end),对应一条飞书消息
CUI(conversational user interface)拿「人跟系统来回对话」当界面的那一类形态,跟 GUI(图形界面)、TUI(终端界面)并列;名字出自维基百科 Conversational user interface 这个词条。飞书私聊这一套就是:发消息提问、点卡片选择、机器人回话
ChatOps在聊天软件里操作一个系统的通行叫法,最初是 GitHub 内部这么叫、后来运维圈写开了(CMU SEI、Rapid7 都有文章)。这个插件干的就是它,差别是 ChatOps 一般指「敲命令触发脚本」,这里是「问一个会思考的 agent」
CUI 事件common/cui-event-schema.js 定的六个字段:event / tag / time / content / messageId / operatorId
卡片类型(tag)发卡时写进按钮 value 的 tag,卡片回调按它路由;菜单项的 event_key 是同类东西
在册内存里还记着的一张卡片(cache/):不在册的卡片上的操作一律不执行,只回一句话
让路(next())waterfall 上的说法:不接这个请求,交给排在后面的回答者(网页端)
回答卡一轮回答那张卡(回复在你发的那条消息上),标题从「排队中」到终态
选项卡若干组选项行 + 底部按钮:会话 / 工作区 / 模型 / 推理深度 / 权限这五张,以及反问卡
反问卡agent 调 ask_user_question 时弹的卡(user-questions/request)
审批卡工具要授权时弹的卡(approval/request)
文字卡只有正文(buildTextCard)或带标题栏(buildHeaderTextCard)的纯提示卡,回执和终态都用它

还没做

  • 反问里的多选题与全靠打字的题(让给网页端);卡片上的自由输入(只有配对填码、会话重命名填名字这两张卡用)。
  • 卡片翻页(超 30KB 就只提示去网页端看)。

更新日志

见 CHANGELOG.md。

card.js
  • cache/:需要跨文件读写的运行期状态,只在内存里——在册的卡片、正在跑的那一轮、处理过的飞书消息、配对码、设置句柄、铸号。各 handler 自己私有那份(回答卡的定时器与改动队列、在册的提问、在册的审批)留在各自工厂里,不在这里。
  • common/:各层共用的形状、常量与文案——CUI 事件(cui-event-schema.js)、飞书事件名(feishu-event.js)、凭据引用名(credential-refs.js)、给人看的文案总表(copy.js)。
  • settings/:设置页。四条回环路由(state / credentials / user/unbind / sleep-guard,只判回环和 JSON,不做身份校验)+ 浏览器半边。