Skip to content

dsh-feishu-assistant

Verified

dsh-feishu-assistant · v0.1.2 · MIT · Web UI

飞书上的 AI 助手:在飞书里私聊机器人,就是跟你指定的那条 DSH 会话对话,回答以卡片回到飞书(Markdown 正常渲染),不需要公网入口。四个特点:配对码把发送者绑成管理员,机器人只服务指定账号;非管理员请求走审批卡片,同意/拒绝都校验点击人身份;请求串行排队,前一条整轮 turn 跑完才注入下一条;目标会话启动时主动 resume,不用先有人在界面上点开。

Install

dsh plugin add dsh-feishu-assistant

Confirm the layer applied with dsh --profile default --dump-config — see the install guide.

Source

Published to npm without a public repository. Inspect the package contents before installing.

Tags

Creators

Readme

dsh-feishu-assistant

飞书上的 AI 助手:在飞书里私聊机器人,就是跟你指定的那条 DSH 会话对话——助手的回答以卡片回到飞书,Markdown 正常渲染。不需要公网入口。

dsh plugin --profile web add dsh-feishu-assistant

装完重启 dsh web

做一个飞书AI助理(从零到能聊天)

第 1 步:飞书开放平台建应用

  1. 飞书开放平台 → 创建企业自建应用
  2. 「凭证与基础信息」里抄下 App IDApp Secret
  3. 「添加应用能力」里开启机器人
  4. 「权限管理」里加上三条权限:
    • im:message.p2p_msg:readonly —— 接收私聊消息
    • im:message:send_as_bot —— 以机器人身份发消息
    • cardkit:card:write —— 回答卡片(建卡片实体、整块更新组件正文)。不加也能跑,只是回答退回"每段一张普通卡片"
  5. 「事件与回调」里把订阅方式设为长连接,然后添加:
    • 事件 im.message.receive_v1(收到消息)
    • 回调 card.action.trigger(审批卡片的按钮点击)
  6. 「版本管理与发布」里创建版本并申请发布 —— 企业自建应用要管理员通过后才对用户生效。

第 2 步:DSH 里装上并配好

打开 设置 → 「飞书AI助理」,四项:

填什么 存哪
App ID / App Secret 第 1 步抄下来的 $DSH_HOME/.credentials.yaml
目标会话 见下面「选一条会话」 $DSH_HOME/settings.yaml
人设文件 上传一份本地 md/txt;不上传就等于这个模式没开 $DSH_HOME/settings.yaml
管理员 不手填,用配对码绑 $DSH_HOME/settings.yaml

凭据写入后不再回显;被环境变量或 .env 遮蔽时,页面上显示为只读。

选一条会话

  1. 在 Web UI 里新建一条会话;
  2. 随便发一句话让它落盘 —— 有日志的会话才会出现在「目标会话」下拉里;
  3. 到 设置 → 「飞书AI助理」→「目标会话」,选中它。

建议专开一条会话给飞书用,别跟自己写代码的会话共用:飞书来的消息会进同一条会话历史。

绑管理员:点「生成配对码」,把 8 位口令私聊发给机器人,发送者即被设为管理员。口令 8 位、10 分钟有效、用一次即废。

第 3 步:验证

在飞书里私聊机器人发一句话。管理员的消息直接进队列;回答以卡片回。设置页「状态」区能看到长连接、目标会话、人设三块的状态。

人设文件

「飞书AI助理模式」这个能力跟插件一起装,但人设内容不由插件提供:上传一份本地 md/txt 就行。

  • 上传后内容是插件自己配置里的一份副本$DSH_HOME/settings.yamlfeishu-assistant.persona),插件不会动你的原文件
  • 上限 64 KB;空文件或超限,设置页直接拒绝
  • 内容为空时飞书消息不处理(管理员收到一句指路,其他人静默丢弃,审批卡片也不发)
  • 内容会盖掉该会话原本的个人设定
  • 上传后不用重启、也不用新建会话:下一条飞书消息就是新人设
  • 代价:上传之后再改原文件,插件不会跟过来,要重新上传一次

写什么随意。典型内容是助手的身份、语气,以及"正文直接用 Markdown"这类格式约定。

行为

飞书私聊文本
  ├─ 配对码            → 把发送者设为管理员,回执
  ├─ 人设内容为空      → 管理员收到指路;其他人静默丢弃
  ├─ 管理员            → 直接进请求队列
  └─ 其他人            → 发审批卡片给管理员;同意后进请求队列,拒绝则回执
                          (待审批最多 20 条、30 分钟过期,作废时回消息告知)
  • 请求队列串行:先进先出,前一条整轮 turn/end 结束才注入下一条
  • 重复投递只答一次:飞书长连接是"至少一次"投递,断线重连可能把同一条消息再送一遍;按 message_id 去重(lru-cache,记最近 200 条),重投只记一条日志
  • 注入前等目标会话空下来:目标会话是共享的,DSH 界面里那一轮可能正在跑。飞书这条会先挂在队列里,等会话真空下来(当前轮跑完、没有排队输入,靠 agent.whenIdle())再注入——这样它就是下一轮,"这一轮跑完了"的判定必然落在自己身上,不会和界面里的轮次交错。等待期间请求人已经在卡片上看到「正在处理」
  • 回写粒度是 step:插件只消费已提交的 assistant/message,所以每产出一条就更新一次那张卡片(上面那条);退回普通卡片时才是每段发一条、超过 3000 字按换行分片。DSH 另有 token 级的 agent/assistant-stream(网页界面用它逐字渲染),插件没有
  • 回答走一张卡片,整块替换:文本消息不渲染 Markdown,所以回答用卡片回;而且一条回答只用一张卡——先发一张写着「正在处理」的卡,之后每段把 markdown 组件整块换掉cardkit.cardElement.update),客户端立刻显示不用卡片流式接口:那是给逐字生成准备的,客户端会按打字机慢慢打,一段几百字要打好几秒,收尾还会把没打完的一次性刷出来("打几个字突然整段蹦出")。正文超过卡片上限(30 KB)会自动翻页再接一张卡
  • 卡片开不出来就退回普通卡片:没 cardkit:card:write 权限或接口失败时,退回"每段一张普通卡片"的老行为;卡片中途失败会把没上屏的内容补发成普通消息,不丢内容
  • 回写重试:单条失败重试 3 次(500ms / 1000ms 退避)
  • 直发有回执:管理员私聊发来的请求先回一句「正在处理」再跑,长回答期间不会让你以为它死了(审批通过的请求人由审批流程回同一句)
  • 没产出会说明:这一轮结束时一个字都没产出(例如跑到一半出错)就回一条失败文案,不再静默;管理员还会看到具体失败原因
  • 不是飞书发起的轮次也推给你:目标会话是共享的——你可能在 DSH 界面上继续问同一件事。插件按「这一轮里有没有自己注入的那条消息」认领轮次:不是它发起的,就把回答以卡片主动私聊推给管理员,而不是静默丢掉(也没有可回复的飞书消息,所以用主动私聊)
  • 会话主动打开:启动、换目标会话、收到消息时都会 sessionController.resolveAgent() 把会话拉起来
  • 会话失效有反馈:目标会话打不开(例如被删掉)时,设置页「状态」区会出现红色的「会话错误」并写明原因;同一条失败还会主动私聊告知管理员一次,恢复后再坏会重新告知。确认打不开之后的后续消息不再白开一张卡片(直接回一句报错);请求照常入队,会话恢复后照常处理——那一轮回答先以普通卡片回来,下一轮恢复成回答卡片
  • 人设有状态:设置页「状态」区显示「飞书AI助理模式 已导入 <文件名>(N / 65536 字节)」,没上传则显示未启用
  • 助手反问会提示:助手调用提问工具(ask_user_question)时会往飞书回一条提示——那个问题只在 DSH 界面上等人回答,飞书看不到也答不了;提示里会建议用人设禁止它提问、改用回答正文
  • 审批人校验:卡片回调校验点击人的 open_id / user_id / union_id,非管理员点不了

配置项

设置页改的都会即时生效(applies: 'live'),不需要重启。

补丁层也可以给初始值:

- id: feishu-assistant
  name: dsh-feishu-assistant
  config:
    sessionId: ''
    managerId: ''
    persona: ''

怎么验证

npm test          # 等价于 node test/all.mjs

六个用例、79 条断言,全部用假飞书 SDK 跑真实的 apply()(不打真接口、不需要凭据):

用例 覆盖
outbound-check 出站返回值语义:卡片成功 / 被拒退回文本 / 没有收件人不发
answer-card-check 回答卡片:整块替换、翻页、失败补发、收尾不多调接口
plugin-routing-check 轮次归属:飞书请求回原消息、外部轮次走私聊、反问提示
idle-hold-check 注入前等会话空闲:忙时不注入、被插队继续等、前提被破坏时告警
broken-session-check 会话打不开:第一次闪一下、之后不再白开卡片、修好后恢复
inbound-dedupe-check 重复投递只答一次

测试放在 test/不随 npm 包发布package.jsonfiles 只有 libcordis.patch.yml)。

已知边界

  • 只处理私聊文本消息:群聊、图片、文件、富文本、语音一律忽略
  • 目标会话必须能被本进程 resume;被另一个进程占着时会失败(DSH 会话是单进程独占的)
  • 待审批缓存是内存态,DSH 重启即丢
  • 回答卡片用的是卡片 JSON 2.0,要求飞书客户端 7.20 及以上:低于 7.20 时卡片正文会显示成"升级提示"占位图
  • 目标会话忙着(界面里那一轮正在跑)时,飞书请求要等它跑完才开始;等待时间等于那一轮的时长。等待期间请求人的卡片显示「正在处理」
  • 回答卡片是普通卡片(非流式),没有「生成中」状态,生成期间也能转发
  • 每段正文是整块替换、不是逐字流式:答案会在每个 step 结束时成段出现(这是有意的,见上)
  • 没有发送者限流;没有 / 命令

依赖的 DSH 服务

agentssettingscredentialswebServer 列在 inject 里,缺一个插件就不会加载。

四个都是官方包(@deepseek-ai/dsh-agent / dsh-settings / dsh-credentials / dsh-host-webserver)。前三个随 dsh-base 走,任何 profile 都有;webServer 只有 dsh-web-app 提供,所以:

这个插件只能在 web profile 里用。 装进 acpheadlesssdk 之类的 profile 时,因为 webServer 不存在,插件会静默不加载——不报错,就是没反应。安装时 --profile web 不要省。