packages/dsh-remote-web-ui
zhu1090093659/dsh-web手机/PC 远程操控 dsh web 工作区:扫码配对、令牌门控通道、SSE 实时同步,提供移动端与完整桌面 GUI 两种远程形态。
给同事的运维请求通道——同事在飞书私聊里提的事先排进串行队列,由已配对的管理员在审批卡上放行后才进入那条共享的 DSH 会话(带注入的人设执行),回答回到提交人。
给同事的运维请求通道:同事在飞书私聊机器人提一件事,你在审批卡上点「同意」后,由你指定的那条 DSH 会话带着你配好的工具与人设去办,结果以卡片回到提交人的飞书(Markdown 正常渲染)。不需要公网入口。
不适用:想给每个同事各自一条独立会话、各自积累上下文的场景——那是 per-user 的 IM 桥,不是这个插件。
dsh plugin --profile web add dsh-feishu-assistant
装完重启 dsh web。
DSH 版本:当前
0.2.0需要 DSH0.1.7-rc.1及以上。 还在 DSH 0.1.5 上的,装最后一个支持它的版本:dsh plugin --profile web add dsh-feishu-assistant@0.1.4
装的时候如果收尾报
ERR_PNPM_IGNORED_BUILDS: protobufjs,插件其实没装上。@larksuiteoapi/node-sdk的依赖里有protobufjs,它带 postinstall 脚本,pnpm 默认不跑;dsh 把 pnpm 的非零退出当成整体失败,于是没把插件登记进 profile。先放行再装一次:# ~/.dsh/profiles/<profile>/pnpm-workspace.yaml allowBuilds: protobufjs: true
im:message.p2p_msg:readonly —— 接收私聊消息im:message:send_as_bot —— 以机器人身份发消息;非管理员请求跑完后的合并转发(im/v1/messages/merge_forward)也用它,不需要另开权限。缺这条时合并转发会失败,只记日志cardkit:card:write —— 回答卡片(建卡片实体、整块更新组件正文)。不加也能跑,只是回答退回"每段一张普通卡片"contact:contact.base:readonly —— 读通讯录基本信息:接口级,加了接口才调得通(控制台里按「通讯录」搜)。不加也能跑,卡片上会退回显示 open_idcontact:user.base:readonly —— 用户基本信息字段:字段级,接口通了之后靠它响应里才带姓名(控制台里按「用户基本信息」搜)。只加前一条的话,接口返回成功但姓名是空的im:message.urgent —— 应用内加急:审批卡片发出去时顺手把这条消息标成「急」。不加也能跑,只是少了这一档提醒im:message.urgent:sms —— 短信加急:审批卡片超过 5 分钟没人处理时升级一次。不加也能跑,只是升级那一步会失败并记日志contact/v3/users 只返回权限范围内的用户,范围外的照样取不到名字,接口会报 41050 no user authorityim.message.receive_v1(收到消息)card.action.trigger(审批卡片的按钮点击)打开 设置 → 「飞书AI助理」,四项(另有一个「企微提醒(可选)」区块,见下面「企微提醒(可选)」):
| 项 | 填什么 | 存哪 |
|---|---|---|
| App ID / App Secret | 第 1 步抄下来的 | $DSH_HOME/.credentials.yaml |
| 目标会话 | 见下面「选一条会话」 | 当前 Profile 的插件配置 |
| 人设文件 | 上传一份本地 md/txt;不上传就等于这个模式没开 | 当前 Profile 的插件配置 |
| 管理员 | 不手填,用配对码绑 | 当前 Profile 的插件配置 |
凭据在 .credentials.yaml 里的引用名是 FEISHU_ASSISTANT_APP_ID / FEISHU_ASSISTANT_APP_SECRET。
凭据写入后不再回显;被环境变量或 .env 遮蔽时,页面上显示为只读。
必须把 DSH 权限设成「完全权限」:
飞书来的消息是无人值守执行的——你在手机上,应答不了 DSH 的权限审批。而 DSH 的审批是 fail-close 的:没有应答方就直接拒绝。权限不放开的后果是,飞书发来的请求一碰到写文件、跑命令就失败,而你在手机上只看到一句报错,不知道为什么。
两处都要设,缺一处都不行:
| 哪里 | 设成什么 | 管什么 |
|---|---|---|
| 设置 → 通用 → 权限 | 完全权限 | 以后新建会话的默认档位 |
在会话里打 /permission | 完全权限 | 当前这条会话的档位 |
顺序有讲究:先改通用设置、再建目标会话,新会话就直接是完全权限;要是会话已经建好了,改默认值不会回头生效,得进去用 /permission 补一次。
附带后果:开了完全权限,等于机器人能在这台机器上做任何事。所以**「管理员」那一项别漏**——它决定谁能跟机器人说话。
选一条会话:
建议专开一条会话给飞书用,别跟自己写代码的会话共用:飞书来的消息会进同一条会话历史。
绑管理员:点「生成配对码」,把 8 位口令私聊发给机器人,发送者即被设为管理员。口令 8 位、10 分钟有效、用一次即废。存下来的是发送者的 open_id;手工在补丁层填 managerId 时也必须填 open_id,否则审批卡片、加急和合并转发都用不了。
在飞书里私聊机器人发一句话。管理员的消息直接进队列;回答以卡片回。设置页「状态」区能看到长连接、目标会话、人设三块的状态。
「飞书AI助理模式」这个能力跟插件一起装,但人设内容不由插件提供:上传一份本地 md/txt 就行。
feishu-assistant.persona),插件不会动你的原文件写什么随意。典型内容是助手的身份、语气,以及"正文直接用 Markdown"这类格式约定。
飞书私聊文本
├─ 配对码 → 把发送者设为管理员,回执
├─ 人设内容为空 → 管理员收到指路;其他人静默丢弃
├─ 管理员 → 直接进请求队列
└─ 其他人 → 发审批卡片给管理员;同意后进请求队列,拒绝则回执
(待审批最多 20 条;发卡时顺手应用内加急,超过 5 分钟没人处理升级一次短信加急;15 分钟过期,作废时回消息告知)
(整轮跑完后把原话和回答合并转发给管理员)
turn/end 结束才注入下一条message_id 去重(lru-cache,记最近 200 条),重投只记一条日志agent.whenIdle())再注入——这样它就是下一轮,"这一轮跑完了"的判定必然落在自己身上,不会和界面里的轮次交错。等待期间请求人已经在卡片上看到「正在处理」assistant/message,所以每产出一条就更新一次那张卡片(上面那条);退回普通卡片时才是每段发一条、超过 3000 字按换行分片。DSH 另有 token 级的 agent/assistant-stream(网页界面用它逐字渲染),插件没有接cardkit.cardElement.update),客户端立刻显示。不用卡片流式接口:那是给逐字生成准备的,客户端会按打字机慢慢打,一段几百字要打好几秒,收尾还会把没打完的一次性刷出来("打几个字突然整段蹦出")。正文接近卡片上限(24 KB,飞书硬上限 30 KB)会自动翻页再接一张卡cardkit:card:write 权限或接口失败时,退回"每段一张普通卡片"的老行为;卡片中途失败会把没上屏的内容补发成普通消息,不丢内容sessionController.resolveAgent() 把会话拉起来ask_user_question)时会往飞书回一条提示——那个问题只在 DSH 界面上等人回答,飞书看不到也答不了;提示里会建议用人设禁止它提问、改用回答正文im/v1/messages/merge_forward 合并转发给管理员;这一轮没有产出时不转;转发失败只记日志,不影响请求人设置页改的都会即时生效(applies: 'live'),不需要重启。
补丁层也可以给初始值:
- id: feishu-assistant
name: dsh-feishu-assistant
config:
sessionId: ''
managerId: ''
persona: ''
personaName: ''
wecomEnabled: false
wecomCorpId: ''
wecomAgentId: 0 # 手动填应用 AgentId
wecomToUser: ''
插件只主动发消息,不接收企微事件,所以不提供 webhook。
设置页怎么填
| 项 | 填什么 | 存哪 |
|---|---|---|
| 启用 | 开关,默认关 | 插件配置 |
| 企业 ID | 企微的 CorpID | 插件配置 |
| AgentId | 应用的 AgentId,手填,不设默认;0 或缺失都算未配置 | 插件配置 |
| 接收人 UserId | 消息发给谁 | 插件配置 |
| Secret | 应用的 Secret,保存后不回显 | .credentials.yaml 的 FEISHU_ASSISTANT_WECOM_SECRET |
开关关着,或企业 ID / AgentId / 接收人 / Secret 任一缺失,发送直接跳过,不发请求。
企微后台的前置条件
企业微信可能要求先配置「可信域名」或「接收消息服务器 URL」,才允许保存「企业可信 IP」。这是企微后台的流程,本插件不提供 webhook,也不会替你准备公网入口。两种走法:
/webhook,注意新应用的 Token / EncodingAESKey 要和该服务配置的一致。另外,自建应用的服务端 API 受「企业可信 IP」限制:运行 dsh 的机器出口公网 IP 要加进去。家宽/办公网 IP 会变,变了会报 60020,需要重新加,或改走固定出口。
纯发提醒不需要接收企微消息,但这两个前置条件仍然要满足。
什么时候发
非管理员请求跑完、这一轮有产出时发:原话 + 回答。不依赖飞书侧是否发送成功——飞书发送整个失败时,企微提醒照样会发。开关关着或配置不全时不发。
发送器在 lib/wecom.js,也挂在 ctx 的 feishuWecomSender 上,其他插件可以用 ctx.get('feishuWecomSender') 取。
实现
照 el-melloi-classroom 的 wecom:gettoken → 按 corpId + agentId 缓存 token(提前 60 秒过期)→ message/send,正文按 2048 UTF-8 字节切分。
npm test # 等价于 node test/all.mjs
九个用例、128 条断言,全部用假飞书 SDK(不打真接口、不需要凭据),涉及消息链路的都跑真实的 apply():
| 用例 | 覆盖 |
|---|---|
outbound-check | 出站返回值语义:卡片成功 / 被拒退回文本 / 没有收件人不发 |
answer-card-check | 回答卡片:整块替换、翻页、失败补发、收尾不多调接口 |
plugin-routing-check | 轮次归属:飞书请求回原消息、外部轮次走私聊、反问提示 |
idle-hold-check | 注入前等会话空闲:忙时不注入、被插队继续等、前提被破坏时告警 |
broken-session-check | 会话打不开:第一次闪一下、之后不再白开卡片、修好后恢复 |
inbound-dedupe-check | 重复投递只答一次 |
forward-check | 合并转发:翻页消息 ID、开卡失败走普通消息、转发被拒不影响收尾、企微提醒触发 |
wecom-check | 企微提醒:开关/配置不全/Secret 缺失不发、token 复用与 corpId 变更、2048 字节切分、接收人覆盖、errcode 非 0 |
settings-check | 设置页后端:AgentId 解析、快照不返回 Secret、只存企微 Secret 不重连、注册 feishuWecomSender、没有 webServer 时仍加载 |
测试放在 test/,不随 npm 包发布(package.json 的 files 只有 lib 和 cordis.patch.yml)。
/ 命令inject 里只有 agents、settings、credentials 三个硬依赖,缺一个插件就不会加载。三个都是官方包(@deepseek-ai/dsh-agent / dsh-settings / dsh-credentials),随 dsh-base 走,任何 profile 都有。
webServer 是可选的:
配置手写在当前 profile 的 cordis.patch.yml:
- id: feishu-assistant
config:
sessionId: '...'
managerId: 'ou_...' # 管理员 open_id,没有配对码,直接填
persona: |
你是……
personaName: 'AGENTS.md'
wecomEnabled: true
wecomCorpId: 'ww...'
wecomAgentId: 1000008
wecomToUser: '...'
Secret 写在 $DSH_HOME/.credentials.yaml 的 refs 下:FEISHU_ASSISTANT_APP_ID、FEISHU_ASSISTANT_APP_SECRET、FEISHU_ASSISTANT_WECOM_SECRET。
配对码依赖设置页;没有设置页时把管理员 open_id 直接填进 managerId。
见 CHANGELOG.md。