Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
cf5c89b
build(feishu): bump @larksuiteoapi/node-sdk to ^1.61.1 for registerApp
mackt Jun 2, 2026
fd56619
feat(feishu): add isBotMentioned helper for group @bot gating
mackt Jun 2, 2026
ea15541
feat(feishu): add fetchBotOpenId for group mention detection
mackt Jun 2, 2026
d7332c3
feat(feishu): add legacy authorizedOpenIds migration helper
mackt Jun 2, 2026
6c0f508
feat(feishu): migrate access control to SDK gate for DM and group
mackt Jun 2, 2026
160fc1b
feat(feishu): add one-click app creation login with config.patch writ…
mackt Jun 2, 2026
cd62b64
docs(feishu): document SDK access gate and one-click login flow
mackt Jun 2, 2026
9d5256b
refactor(feishu): address review — migrate disabled accounts, cover r…
mackt Jun 2, 2026
23dc815
build(feishu): depend on @marswave/cola-plugin-sdk 0.0.3-beta.0
mackt Jun 2, 2026
48ed0a0
feat(feishu): add group-context builder and watermark tracker
mackt Jun 2, 2026
c5e054d
fix(feishu): unwrap language-keyed post content in group context
mackt Jun 2, 2026
71fec51
feat(feishu): fetch group history window via List Messages API
mackt Jun 2, 2026
6bbceee
refactor(feishu): drop unnecessary message.list param cast
mackt Jun 2, 2026
bea72a3
feat(feishu): prepend fetched group context on @-mention
mackt Jun 2, 2026
dd335e6
docs(feishu): note GroupContextTracker is not concurrency-guarded
mackt Jun 2, 2026
b6cdc28
docs(feishu): document group-context fetch and required scope
mackt Jun 2, 2026
71efca9
style(feishu): apply oxfmt to group-context wiring
mackt Jun 2, 2026
6dd2995
test(feishu): cover group-context watermark round-trip across two @me…
mackt Jun 2, 2026
080c37c
feat(feishu): gate group chat behind groupEnabled, reply '暂不支持群聊' whe…
mackt Jun 4, 2026
c4f4ad5
feat(telegram): gate group chat behind groupEnabled, reply '暂不支持群聊' w…
mackt Jun 4, 2026
9396c72
test(telegram): cover @mention of another user not triggering the gro…
mackt Jun 4, 2026
1ec2fde
feat(feishu): implement auth.disconnect to clear credentials (enables…
mackt Jun 4, 2026
f095ebf
refactor(channel): shorten group-disabled notice to "暂不支持群聊"
mackt Jun 4, 2026
c8e26e7
refactor(feishu): hide groupEnabled toggle from config UI
mackt Jun 4, 2026
d8be803
refactor(telegram): expose only bot token and allowed chat IDs in con…
mackt Jun 4, 2026
7b4b84c
chore(plugins): use dev plugin sdk
mackt Jun 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 84 additions & 53 deletions plugins/feishu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

- 接收飞书/Lark 的文本、图片、文件和表情回应事件。
- 从 Cola 向飞书/Lark 发送文本、图片、文件、Markdown 和表情回应消息。
- 默认仅支持私聊。群聊默认关闭,被 @机器人时回复「暂不支持群聊」;可通过配置 `groupEnabled` 开启。
- 开启群聊后:需 @机器人触发、按群授信(`cola channel allow-group`),并自动补全「上次回复以来」的群聊上下文(见[群聊上下文](#群聊上下文))。
- 支持 `cola channel login feishu` 扫码一键创建应用并自动回写凭据。
- 固定使用 WebSocket 长连接接收事件,不需要公网服务器。
- 支持通过 `accounts` 配置多个账号。

Expand All @@ -19,21 +22,23 @@

## 配置流程

### 方式一:一键创建飞书智能体应用(推荐)
### 方式一:扫码一键创建(推荐)

飞书开放平台提供了“一键创建飞书智能体应用”能力,适合把 Cola 接入飞书。创建成功后会
直接返回 `App ID` `App Secret`,并预置智能体常用的机器人能力和权限
插件内置了飞书开放平台的“一键创建飞书智能体应用”流程:扫码即可创建应用,凭据由插件
通过 SDK 自动回写到配置,无需手动复制 `App ID` / `App Secret`。

1. 打开 [飞书开放平台应用启动器](https://open.feishu.cn/page/launcher)。
2. 按页面提示扫码或登录飞书。
3. 选择创建新应用,或选择一个已有应用继续配置。
4. 创建完成后复制 `App ID` 和 `App Secret`。
5. 在 Cola 插件商店安装 Feishu 插件。
6. 打开 Cola 里的 Feishu 插件设置。
7. 填入 `appId` 和 `appSecret`。
8. `domain` 选择 `feishu`;如果你使用国际版 Lark,选择 `lark`。
9. 在 `authorizedOpenIds` 中填入允许使用 Cola 的飞书用户 `open_id`,多个值用逗号分隔。如果不知道获取方式,可以在保存 `appId` 和 `appSecret` 后见[获取用户 open_id](#获取用户-open_id)。
10. 保存设置,并按 Cola 提示重启或重载 gateway。
1. 在 Cola 插件商店安装 Feishu 插件。
2. 运行:

```text
cola channel login feishu
```

3. 用飞书扫描终端里的二维码并确认。
4. 创建成功后,插件会自动把 `appId`、`appSecret` 和 `domain` 写入 `accounts.default`
并重载 gateway,扫码用户也会被自动授信,可立即给机器人发消息。

之后再为其他用户或群授信,见[访问授信](#访问授信谁能使用-cola)。

### 方式二:手动创建飞书自建应用

Expand Down Expand Up @@ -115,54 +120,79 @@
2. 打开 Feishu 插件设置。
3. 填入 `appId` 和 `appSecret`。
4. `domain` 选择 `feishu` 或 `lark`。
5. 在 `authorizedOpenIds` 中填入允许使用 Cola 的飞书用户 `open_id`,多个值用逗号分隔
6. 保存设置,并按 Cola 提示重启或重载 gateway
5. 保存设置,并按 Cola 提示重启或重载 gateway
6. 给需要使用 Cola 的用户或群授信,见[访问授信](#访问授信谁能使用-cola)

Feishu 插件固定使用 WebSocket 长连接模式,不需要公网 HTTP 回调地址。

#### 7. 把机器人加入会话

群聊中,打开群设置,进入 `群机器人`,添加你创建的机器人。很多飞书群聊配置下,用户需要
@机器人 才会触发 Cola 回复。
群聊中,打开群设置,进入 `群机器人`,添加你创建的机器人。群聊里必须 @机器人 才会触发
Cola 回复,且该群需要先被授信(见下文)

单聊中,直接在飞书里搜索机器人名称并发起会话。如果你的 Cola 部署启用了私聊身份配对,
需要先把发送者的 `open_id` 加入 `authorizedOpenIds`,保存配置并重载 gateway 后,再让
该用户给机器人发一条消息完成绑定。
单聊中,直接在飞书里搜索机器人名称并发起会话。发送者需要先被授信,未授信用户发消息时,
插件会回复一段包含其 `open_id` 和授信命令的提示。

## 获取用户 open_id
## 访问授信(谁能使用 Cola)

谁能通过飞书驱动 Cola 由 Cola 的访问网关统一管理,使用 `cola channel` 命令授信,不再
通过插件配置里的名单。授信对象有两类:

- **私聊**:按发送者 `open_id` 授信。
- **群聊**:按群 `chat_id` 授信整群,且群里必须 @机器人 才会触发。

```text
cola channel allow feishu <open_id> # 授信一个私聊用户
cola channel revoke feishu <open_id> # 取消用户授信
cola channel allow-group feishu <chat_id> # 授信整个群
cola channel revoke-group feishu <chat_id> # 取消群授信
cola channel allowlist feishu # 查看已授信的用户和群
```

`authorizedOpenIds` 需要填写飞书/Lark 用户的 `open_id`,通常以 `ou_` 开头。
### 怎么拿到 open_id / chat_id

最简单的方式是先让需要使用 Cola 的用户给机器人发一条消息。插件收到
未授权用户的消息时,会直接回复一段配对提示,里面包含发送者的 `open_id`,例如:
最简单的方式是让对方先触发一次。未授信的用户私聊机器人、或在群里 @机器人时,插件会回复
一段提示,里面已经带好待执行的命令,例如:

````text
Cola Feishu: access not configured.
你还没有被授信,无法使用 Cola。
请管理员执行:
```
cola channel allow feishu ou_xxx
```
````

Your Feishu open_id:
群聊里则是:

````text
这个群还没有被授信,无法使用 Cola。
请管理员执行:
```
ou_xxx
cola channel allow-group feishu oc_xxx
```
````

复制其中的 `ou_xxx`,填入 `authorizedOpenIds`,保存设置并重载 gateway 后,让该用户再给
机器人发一条消息即可自动完成绑定
把命令复制到终端执行即可。同一目标的提示有冷却时间,不会刷屏;未 @机器人的群消息会被
直接忽略,不打扰群成员

如果需要从日志排查,也可以搜索新版本插件的提示:
> 从旧版本升级:插件启动时会自动把遗留的 `authorizedOpenIds` 迁移成授信绑定,原有用户
> 不需要重新授信。

```text
[plugin:feishu] Skipping Feishu message from unauthorized sender ou_xxx
```
## 群聊上下文

如果你使用的是旧版本插件,也可能看到 host 层日志:
> 仅在配置 `groupEnabled: true` 开启群聊后生效。群聊默认关闭,此时群里 @机器人只会收到「暂不支持群聊」提示。

```text
[plugin:feishu] Ignoring message from unbound sender: ou_xxx
```
群聊里机器人默认只会收到「@它」的消息,看不到群里其他人的发言,被 @ 时容易缺少上下文。
为此,当机器人在已授信的群里被 @ 时,插件会通过飞书「获取会话历史消息」接口拉取该群
**自机器人上次回复以来**的最近消息,拼成一段「仅供参考」的上下文,连同当前这条消息一起
交给 Cola,让回复能理解大家在讨论什么。

如果你在飞书开放平台调试事件,也可以从 `im.message.receive_v1` 事件 payload 的
`sender.sender_id.open_id` 字段获取。管理员或开发者也可以通过飞书开放平台的用户 ID
查询工具/API 获取用户的 `open_id`。
- **无需额外权限**:复用已申请的 `im:message` / `im:message:readonly`,前提是机器人已在该群里。
- **作用范围**:仅群聊、仅被 @ 时触发;单聊不受影响。整群共享一段上下文,按群累计。
- **失败不影响回复**:拉取历史失败时只记一条告警日志,机器人照常回复,只是少了上下文。
- **当前限制**:上下文里的图片/文件等只显示占位符(如 `[图片]`),发言人以 `open_id` 标注;
服务重启后首次被 @ 会改为拉取最近若干条。被动接收群里全部消息(需敏感权限
`im:message.group_msg`)作为后续增强,暂未启用。

## 测试

Expand All @@ -186,12 +216,15 @@ ou_xxx

## 配置字段

| 字段 | 必需 | 默认值 | 说明 |
| ------------------- | ---- | -------- | ------------------------------------------------------------------ |
| `appId` | 是 | | 飞书/Lark 机器人应用 ID,例如 `cli_xxx`。 |
| `appSecret` | 是 | | 机器人应用密钥。请作为 secret 保存。 |
| `domain` | 否 | `feishu` | 国内飞书用 `feishu`,国际版 Lark 用 `lark`。 |
| `authorizedOpenIds` | 否 | | 允许绑定到 Cola 的发送者 `open_id`,多个值用逗号、空格或换行分隔。 |
| 字段 | 必需 | 默认值 | 说明 |
| -------------- | ---- | -------- | -------------------------------------------------------- |
| `appId` | 是 | | 飞书/Lark 机器人应用 ID,例如 `cli_xxx`。 |
| `appSecret` | 是 | | 机器人应用密钥。请作为 secret 保存。 |
| `domain` | 否 | `feishu` | 国内飞书用 `feishu`,国际版 Lark 用 `lark`。 |
| `groupEnabled` | 否 | `false` | 是否启用群聊。关闭时群里 @机器人只回复「暂不支持群聊」。 |

谁能使用 Cola 不再通过配置字段控制,改用 `cola channel allow[-group]` 授信,见
[访问授信](#访问授信谁能使用-cola)。

多账号高级配置可以直接使用 `accounts` 对象:

Expand All @@ -201,8 +234,7 @@ ou_xxx
"default": {
"appId": "cli_xxx",
"appSecret": "secret",
"domain": "feishu",
"authorizedOpenIds": "ou_xxx,ou_yyy"
"domain": "feishu"
}
}
}
Expand All @@ -227,11 +259,10 @@ ou_xxx
- 应用已经发布,并且发送消息的用户在可用范围内。
- 机器人已经加入目标会话。
- 权限包含收消息和以机器人身份发消息的 scope。
- 群聊里是否需要 @机器人。
- 发送者的 `open_id` 是否已加入 `authorizedOpenIds`。
- 保存配置并重载 gateway 后,是否让该 `open_id` 发过消息触发自动绑定。
- 未授权用户是否收到了 `Cola Feishu: access not configured.` 提示;如果没有,请检查
`im:message:send_as_bot` 权限。
- 群聊里是否 @机器人;该群是否已用 `cola channel allow-group feishu <chat_id>` 授信。
- 私聊发送者是否已用 `cola channel allow feishu <open_id>` 授信。可用
`cola channel allowlist feishu` 查看当前授信列表。
- 未授信用户是否收到了带授信命令的提示;如果没有,请检查 `im:message:send_as_bot` 权限。

### 图片或文件失败

Expand Down
4 changes: 2 additions & 2 deletions plugins/feishu/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@larksuiteoapi/node-sdk": "^1.60.0",
"@marswave/cola-plugin-sdk": "^0.0.1"
"@larksuiteoapi/node-sdk": "^1.61.1",
"@marswave/cola-plugin-sdk": "0.0.4-dev.202606041251.c8b2bed3"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bump the published plugin version for these changes

This commit changes the Feishu plugin's runtime behavior and dependencies while the package still declares version 0.1.2; scripts/build-registry.ts builds the release URL from {id}-{version}.tar.gz, so publishing this under the same version would either overwrite an immutable tarball or leave clients/caches unable to distinguish the new SDK-gated build from the old one. Please bump the affected plugin package versions before release; the same issue applies to the Telegram package changed in this commit.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add a Cola compatibility gate for the new SDK APIs

This line moves Feishu onto the new dev SDK and the same commit starts using host features such as auth, unauthorizedHint, conversation, and mentionedBot, but the manifest still has no cola.plugin.minColaVersion for scripts/build-registry.ts to publish. Older Cola installs will therefore see this build as compatible and may load a plugin that depends on SDK/runtime hooks they do not implement instead of being blocked by the upgrade gate; add the required minColaVersion to the affected Feishu and Telegram manifests.

Useful? React with 👍 / 👎.

},
"devDependencies": {
"@types/node": "^22.0.0",
Expand Down
21 changes: 21 additions & 0 deletions plugins/feishu/src/api/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as lark from "@larksuiteoapi/node-sdk";
import type { PluginLogger } from "@marswave/cola-plugin-sdk";
import type { FeishuDomain, FeishuAccountConfig } from "./types.js";

function resolveDomain(domain: FeishuDomain | undefined): lark.Domain | string {
Expand Down Expand Up @@ -61,6 +62,26 @@ export function createEventDispatcher(config: FeishuAccountConfig): lark.EventDi
});
}

/**
* Fetch the bot's own open_id (used to detect @bot mentions in group chats).
* Best-effort: returns undefined on failure so group gating degrades gracefully.
*/
export async function fetchBotOpenId(
client: lark.Client,
logger: PluginLogger,
): Promise<string | undefined> {
try {
const res = (await client.request({
method: "GET",
url: "/open-apis/bot/v3/info",
})) as { bot?: { open_id?: string } };
return res?.bot?.open_id;
} catch (err) {
logger.warn("Failed to fetch Feishu bot open_id (group @mention detection disabled)", err);
return undefined;
}
}

export function getLarkClient(accountId: string): lark.Client | undefined {
return clientCache.get(accountId)?.client;
}
Expand Down
8 changes: 7 additions & 1 deletion plugins/feishu/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export type FeishuAccountConfig = {
appId: string;
appSecret: string;
domain?: FeishuDomain;
authorizedOpenIds?: string | string[];
encryptKey?: string;
verificationToken?: string;
enabled?: boolean;
Expand All @@ -15,6 +14,13 @@ export type FeishuAccountConfig = {
export type FeishuPluginConfig = {
pluginDir?: string;
accounts?: Record<string, FeishuAccountConfig>;
/** Enable group chat. When false (default), @mentions in groups get a "not supported" reply. */
groupEnabled?: boolean;
};

/** Pre-SDK-gate account config that may still carry a legacy authorizedOpenIds allowlist. */
export type LegacyFeishuAccountConfig = Partial<FeishuAccountConfig> & {
authorizedOpenIds?: string | string[];
};

export type ResolvedAccount = {
Expand Down
44 changes: 0 additions & 44 deletions plugins/feishu/src/auth/authorized-open-ids.ts

This file was deleted.

50 changes: 50 additions & 0 deletions plugins/feishu/src/auth/legacy-allowlist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { PluginLogger, PluginRuntime } from "@marswave/cola-plugin-sdk";
import type { LegacyFeishuAccountConfig } from "../api/types.js";

/**
* Parse a legacy `authorizedOpenIds` config value (comma/space separated string
* or array) into a set of open_ids. Retained only to migrate pre-SDK-gate config.
*/
export function parseAuthorizedOpenIds(value: unknown): Set<string> {
if (Array.isArray(value)) {
return new Set(
value.filter((item): item is string => isNonEmptyString(item)).map((s) => s.trim()),
);
}

if (typeof value !== "string") return new Set();

return new Set(
value
.split(/[\s,]+/)
.map((item) => item.trim())
.filter(Boolean),
);
}

/**
* One-time migration: bind every legacy `authorizedOpenIds` entry as an identity
* binding so existing authorized users keep access under the SDK access gate.
* Idempotent — re-binding an already-bound sender is a no-op on the host side.
*/
export async function migrateLegacyAllowlist(
accounts: Iterable<LegacyFeishuAccountConfig>,
identity: PluginRuntime["identity"],
logger: PluginLogger,
): Promise<void> {
const seen = new Set<string>();
for (const acct of accounts) {
for (const openId of parseAuthorizedOpenIds(acct.authorizedOpenIds)) {
if (seen.has(openId)) continue;
seen.add(openId);
await identity.bind(openId);
}
}
if (seen.size > 0) {
logger.info(`Migrated ${seen.size} legacy authorizedOpenId(s) to identity bindings`);
}
}

function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
Loading
Loading