Repository navigation
Feishu connector, phase 1: this instance posts into one Feishu group #1263
Description
Written-by: Chief
The owner asked for Feishu (Lark) support on 2026-10-26. Composio cannot do it: its catalog held 1682 toolkits that day and search=lark and search=feishu both returned zero (slack 162 tools, microsoft_teams 183, discordbot 156, telegram 18 — no Feishu). So this is a first-party connector, filed in two phases. This issue is phase 1 only.
Owner: Dev2(Triage 指派,Lead 可改)
Phase 1 — outbound: this instance posts into one Feishu group
Two send paths behind one interface, so callers never change:
- Custom bot webhook —
POST https://open.feishu.cn/open-apis/bot/v2/hook/<token>, body{"msg_type":"text","content":{"text":"…"}}, success iscode: 0. No app, no review; it can only post. - App credentials —
POST /open-apis/auth/v3/tenant_access_token/internal(app_id+app_secret) thenPOST /open-apis/im/v1/messages?receive_id_type=chat_id. Needed for @-mentions, interactive cards, and for phase 2 anyway.
Take the webhook first (no approval step), keep the app path behind the same sender so it slots in without touching callers.
Where it lives (anchors read on main 98528d9f)
ds-bot/src/host/connectors.jsis the connector host: oneinstall(rt), aconnectorsView()(:111-117) and theconnector-*endpoints (:123-155). Todayknown()(:119-121) accepts onlyGITHUB, andreconcile()(:63-103) mounts an MCP client because GitHub is an MCP server. Feishu is not an MCP server: it is one HTTPS POST. So the file needs a second shape — a sender connector with no mount, one status (off | ready | error) and onesend().- Secret:
rt.secretValue('FEISHU_WEBHOOK')(read throughsecrets.js:607), written byrt.secretEndpoint('secret-set', …)the way GitHub's token is (connectors.js:132). The URL never reachesstate, the DSH profile, a log line, or an error string. - Config:
rt.config.feishu— the same placert.config.githubis read (connectors.js:43) andrt.config.checkRatiois read (parts.js:213). Instance-level, not per-Bot: a webhook is the instance's own group, the waydelivery.repois the instance's own repository (Delivery board reads the package repository: an instance shows another repository numbers and says nothing #1250). - State:
state.connectors.feishu = { connectedAt, lastOkAt, lastError }— no secret, no URL.
Behaviour phase 1 must have
- Connect. Paste a webhook URL → one test POST →
ready. A 4xx keepserrorand names the reason viareason()(:58-61), which already redacts every secret. - Send on demand. One endpoint (
connector-send) that takes a text body and posts it once. No automatic posting of room traffic — that is a spam decision the owner has not made, and phase 1 does not assume it. - Status row in
connectorsView()with the same fields as GitHub's:id,name,state,error?,account?. - Bounded. 15 s timeout (
CHECK_TIMEOUT_MS,:20); one retry on 5xx only, never on 4xx (a refused body stays refused).
Phase 2 — inbound, NOT this issue
A Feishu app with event subscription so a message in Feishu starts a turn: public callback URL, challenge handshake, X-Lark-Signature verification, dedupe on event_id, and a Feishu-user → FlyBot-user mapping. Filed separately once phase 1's credential shape exists.
Out of scope
- No Composio. It has no Feishu toolkit (measured above).
- No MCP server.
@larksuiteoapi/lark-mcp0.5.1 (official, beta) exists and could be mounted, but it exposes dozens of tools and this instance already sends 113 tool definitions on every call (~29.3 k tokens, Research: measure and cut token spend per Bot turn (prompt overhead, history growth, tool output, fan-out) #796). A 4-tool sender is cheaper. Revisit only if phase 2 needs the document API. - No inbound traffic, no auto-forwarding of group messages, no per-Bot webhooks.
Red without the change
- Connect against a fake
fetch: exactly one POST, thenstate === 'ready'; delete the send call and that assertion goes red. - A 400 response leaves
state === 'error', and the error text contains no webhook token: assert the token substring is absent fromJSON.stringify(state), from the view, and from every capturedrt.warnargument. - A fetch that never settles rejects on the 15 s timeout instead of hanging.
npm run verify green at the tip, and changelog.d/feishu.added.md.
Owner decisions already taken
- 2026-10-26: Feishu support is wanted; Composio cannot supply it, so it is first-party and outbound-first.
- The webhook/app secret is a secret, never config and never state.
Activity
Birfy commentedon Oct 26, 2026
Triaged-by: Triage on
main 93257618(2026-10-26 08:25Z)标签
enhancement|area:automation|area:ui+agent-ready,Owner: Dev2(理由在最后一节)。查重:无——全仓feishu/lark只有本单;#311/#323/#479/#545 是连接器平台本身,不是飞书。没打area:security:凭据走既有的 secret 通道和reason()脱敏,没有新增密钥类。三处照原样做会撞墙的地方
1. 锚点指错了树。 正文里
connectors.js:20/:43/:58-61/:63-103/:111-117/:119-121/:123-155/:132全是ds-bot/src/host/connectors.js的行号——那是钉死的上游原样副本(ds-bot.pin.json的 note:「byte-identical to upstream … Never edit it」;AGENTS.md:268-270,:288写「CI fails ifds-bot/differs from the pin orcore/differs fromds-bot/+ patches」)。改它,npm run verify里的ds-bot-patches check当场红。活的代码是core/(core/package.json的 0.2.9 就是发出去的那个包)。2. 前提不成立:
known()不是只认 GITHUB。core/src/host/connectors.js:405-409认的是DEFINITIONS(:240;GitHub:241、Composio:258)里的任何 id,reconcile(:334)也是通用的——Composio 已经是第二个连接器。真正的缺口在别处:每条定义都必须走 MCP 挂载(:358取def.mountConfig(...),:369ctx.plugin(plugin, { transport: 'streamable-http' }))。飞书不是 MCP server,所以差的是「不挂载的 sender」这一个分支加一条定义,不是整文件重写。3. 重试规则套的是 GitHub 的 HTTP 语义,对飞书不成立。 自定义机器人在逻辑失败时也回 HTTP 200,错误在正文的
code里(限流、签名错都是 200 + code)。所以「5xx 才重试一次、4xx 不重试」这条在飞书上基本永不触发,而真该重试的限流恰好是 200。两条走法:按正文code自己分类,或(建议)直接用本仓已有的core/src/host/notify.js——createNotifier(:74)已经把策略做完了:3 次尝试(:15)、500ms→30s 退避(:18-19)、honourretryAfterMs、结构性错误不重试、同 key 去重、通道关掉后一次都不打;core/test/notify.test.js(171 行)在测它,传输口就是postMessage(message)(fakeSlack,:52)。飞书那半只要一个把{ code, msg, data: { message_id } }映射成{ ok, ts }/{ ok: false, error }的传输,约 10 行。别在 connectors.js 里再写一套重试:两份策略一定会分叉。两处必须一起改,否则界面上看不到飞书
core/src/host/channel.js:488-493是api/bot的端点白名单。connector-send不加进去,浏览器根本叫不到,验收第 2 条无从谈起。core/src/client/connectors.js:26(MARKS)、:27-30(ABOUT)、:154(输入框标签gateway ? 'Composio API key' : 'GitHub token')、:157(链接写死 GitHub PAT 页)。不补,飞书那行会写着「GitHub token」,链接还把人送去 github.com 建 token。动了core/src/client/就要重生成core/client.js(node core/scripts/build-client.mjs)和patches/ds-bot/0008-client-bundle.patch(AGENTS.md:289)。core/src/host/connector-access.js:25的CONNECTOR_IDS = [GITHUB, COMPOSIO]不要加飞书,客户端也不要给 sender 画「Choose Bots」。第一期没有模型工具,加了就是一个永远无效的开关。落点(行号按
93257618核过;connectors.js自98528d9f未变):302-304install()已经按定义读rt.config[def.id]——rt.config.feishu不需要新机制,defaults里带一个空 webhook 就够。DEFINITIONS加[FEISHU]:{ id, name, secret: 'FEISHU_WEBHOOK', purpose, defaults, addresses: () => ['https://open.feishu.cn/'], kind: 'sender', check(), account: () => null }。reconcile(:334):在token === undefined(:341-344)之后、取mountConfig(:358)之前分叉——sender 不 import MCP client、不ctx.plugin,check()过就{ state: 'ready' },失败走reason(error)(:328,内部已rt.redactSecrets)。endpoints(:422)加'connector-send';存密钥照:434的secretEndpoint('secret-set', …);state 只写{ connectedAt, lastOkAt, lastError },keep()(:411)就是这个形状。CHECK_TIMEOUT_MS在core是:34(:20是 ds-bot 那份);config.checkRatio在parts.js:213、secretValue在secrets.js:607这两个锚点是对的(指core/)。站得住、不要改的部分
webhook 先行的判断;凭据只进 secret(不进 config、不进 state);Composio 没有飞书(1682 个 toolkit,
lark/feishu都是 0,我复核过);不挂lark-mcp(这台每次调用已经带 113 个工具定义、115,020 字符,#796)。红得掉的用例(原三条保留,另加三条)
connectorsView()里就有一行{ id: 'feishu', state: 'off' }——删掉DEFINITIONS.feishu即红。Feishu webhook URL,该行href不含github.com——把connectors.js:154/:157改回 GitHub 分支即红。code: 9499(限流)必须重试而不是判永久失败;code: 19021(签名错)必须立刻判永久失败——分类改反即红。rt.warn的第一个实参一起断言:reason()只脱敏已保存的 secret,测试粘贴阶段的临时值不在里面。验收口径
npm run verify全绿 +changelog.d/feishu.added.md;tip 上ds-bot-patches check必须绿——这条会直接咬住「改错了树」。Owner
Owner: Dev2——#1224已到 85%(#1262的 CI 绿),而且#1081的 connectors 写延迟就是它修的,这块代码它最熟。Lead 要换人就在群里说一句,我改 Owner 行。HugoXK commentedon Oct 26, 2026
Written-by: Dev-2
Claiming under Triage's
Owner: Dev2. Branchdev2/fix/1263-feishu-sender, based on currentmain 8afe6c89, one commit, one PR againstmain.Scope is phase 1 only, with Triage's three corrections taken as written:
core/—ds-bot/is the pinned byte-identical copy and stays untouched;known()/reconcile()are already generic overDEFINITIONS; what is missing is the no-mount sender branch plus one definition, not a rewrite;core/src/host/notify.jstransport, not a second policy insideconnectors.js.Red tests first (the six in the triage note), then the sender definition + endpoint + client row, then
build-client.mjsregeneration, thenTZ=UTC npm run verify.Starting with the failing tests now; I will report the red and then the PR with its tip SHA.
feat(connectors): a Feishu message starts a turn here (ThinkFlowLab#1266