Add lark extension for Feishu/Lark integration
This commit is contained in:
parent
9025da2296
commit
aeac1220f2
336
extensions/lark/SKILL.md
Normal file
336
extensions/lark/SKILL.md
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
---
|
||||||
|
name: lark-mcp
|
||||||
|
description: 使用飞书/Lark官方 MCP 工具操作飞书平台。支持:文档读取、多维表格操作、任务管理、日程创建、群消息读取、IM消息发送等。当用户需要操作飞书文档、表格、任务、日程或消息时使用此 skill。
|
||||||
|
metadata: {"moltbot":{"emoji":"📋","requires":{"bins":["npx"]}}}
|
||||||
|
---
|
||||||
|
|
||||||
|
# Lark MCP 飞书操作工具
|
||||||
|
|
||||||
|
使用飞书官方 `@larksuiteoapi/lark-mcp` MCP 工具操作飞书平台。
|
||||||
|
|
||||||
|
GitHub: https://github.com/larksuite/lark-openapi-mcp
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
### 1. 用户授权登录(访问用户数据必须)
|
||||||
|
|
||||||
|
如果上下文中有 `UserAccessToken`,可以直接使用。否则需要先登录:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx -y @larksuiteoapi/lark-mcp login -a <APP_ID> -s <APP_SECRET> --domain https://open.larksuite.com
|
||||||
|
```
|
||||||
|
|
||||||
|
登录后会保存 token,后续调用会自动使用。
|
||||||
|
|
||||||
|
### 2. MCP 工具调用方式
|
||||||
|
|
||||||
|
使用 mcporter 调用 lark-mcp 工具:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看所有可用工具
|
||||||
|
mcporter list lark-mcp --schema
|
||||||
|
|
||||||
|
# 调用工具
|
||||||
|
mcporter call lark-mcp.<tool_name> param1=value1 param2=value2 --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文档操作 (Docx)
|
||||||
|
|
||||||
|
### 获取文档内容
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 获取文档元信息
|
||||||
|
mcporter call lark-mcp.docx.v1.document.get document_id=<doc_id> --output json
|
||||||
|
|
||||||
|
# 获取文档纯文本内容
|
||||||
|
mcporter call lark-mcp.docx.v1.document.raw_content document_id=<doc_id> --output json
|
||||||
|
|
||||||
|
# 获取文档块列表
|
||||||
|
mcporter call lark-mcp.docx.v1.document_block.list document_id=<doc_id> --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 文档 ID 获取方式
|
||||||
|
|
||||||
|
从文档 URL 提取:`https://xxx.larksuite.com/docx/ABC123` → document_id = `ABC123`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 多维表格 (Bitable)
|
||||||
|
|
||||||
|
### 获取表格信息
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 获取多维表格元信息
|
||||||
|
mcporter call lark-mcp.bitable.v1.app.get app_token=<app_token> --output json
|
||||||
|
|
||||||
|
# 列出所有数据表
|
||||||
|
mcporter call lark-mcp.bitable.v1.app_table.list app_token=<app_token> --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 读取表格记录
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 获取记录列表
|
||||||
|
mcporter call lark-mcp.bitable.v1.app_table_record.list \
|
||||||
|
app_token=<app_token> \
|
||||||
|
table_id=<table_id> \
|
||||||
|
--output json
|
||||||
|
|
||||||
|
# 获取单条记录
|
||||||
|
mcporter call lark-mcp.bitable.v1.app_table_record.get \
|
||||||
|
app_token=<app_token> \
|
||||||
|
table_id=<table_id> \
|
||||||
|
record_id=<record_id> \
|
||||||
|
--output json
|
||||||
|
|
||||||
|
# 搜索记录(带筛选条件)
|
||||||
|
mcporter call lark-mcp.bitable.v1.app_table_record.search \
|
||||||
|
app_token=<app_token> \
|
||||||
|
table_id=<table_id> \
|
||||||
|
--args '{"filter":{"conjunction":"and","conditions":[{"field_name":"状态","operator":"is","value":["进行中"]}]}}' \
|
||||||
|
--output json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 写入表格记录
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建记录
|
||||||
|
mcporter call lark-mcp.bitable.v1.app_table_record.create \
|
||||||
|
app_token=<app_token> \
|
||||||
|
table_id=<table_id> \
|
||||||
|
--args '{"fields":{"标题":"新任务","状态":"待处理","负责人":[{"id":"ou_xxx"}]}}' \
|
||||||
|
--output json
|
||||||
|
|
||||||
|
# 更新记录
|
||||||
|
mcporter call lark-mcp.bitable.v1.app_table_record.update \
|
||||||
|
app_token=<app_token> \
|
||||||
|
table_id=<table_id> \
|
||||||
|
record_id=<record_id> \
|
||||||
|
--args '{"fields":{"状态":"已完成"}}' \
|
||||||
|
--output json
|
||||||
|
|
||||||
|
# 删除记录
|
||||||
|
mcporter call lark-mcp.bitable.v1.app_table_record.delete \
|
||||||
|
app_token=<app_token> \
|
||||||
|
table_id=<table_id> \
|
||||||
|
record_id=<record_id> \
|
||||||
|
--output json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bitable Token 获取方式
|
||||||
|
|
||||||
|
从 URL 提取:`https://xxx.larksuite.com/base/ABC123` → app_token = `ABC123`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务管理 (Task)
|
||||||
|
|
||||||
|
### 创建任务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcporter call lark-mcp.task.v2.task.create \
|
||||||
|
--args '{
|
||||||
|
"summary": "任务标题",
|
||||||
|
"description": "任务描述",
|
||||||
|
"due": {"timestamp": "1735689600"},
|
||||||
|
"members": [{"id": "ou_xxx", "role": "assignee"}]
|
||||||
|
}' \
|
||||||
|
--output json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查询任务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 获取任务详情
|
||||||
|
mcporter call lark-mcp.task.v2.task.get task_guid=<task_id> --output json
|
||||||
|
|
||||||
|
# 列出任务
|
||||||
|
mcporter call lark-mcp.task.v2.task.list --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 更新任务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcporter call lark-mcp.task.v2.task.patch \
|
||||||
|
task_guid=<task_id> \
|
||||||
|
--args '{"task":{"summary":"更新后的标题","completed_at":"1735689600"}}' \
|
||||||
|
--output json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 日程管理 (Calendar)
|
||||||
|
|
||||||
|
### 获取日历列表
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcporter call lark-mcp.calendar.v4.calendar.list --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 创建日程
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcporter call lark-mcp.calendar.v4.calendar_event.create \
|
||||||
|
calendar_id=<calendar_id> \
|
||||||
|
--args '{
|
||||||
|
"summary": "会议标题",
|
||||||
|
"description": "会议描述",
|
||||||
|
"start_time": {"timestamp": "1735689600"},
|
||||||
|
"end_time": {"timestamp": "1735693200"},
|
||||||
|
"attendee_ability": "can_modify_event",
|
||||||
|
"attendees": [{"type": "user", "user_id": "ou_xxx"}]
|
||||||
|
}' \
|
||||||
|
--output json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查询日程
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 获取日程详情
|
||||||
|
mcporter call lark-mcp.calendar.v4.calendar_event.get \
|
||||||
|
calendar_id=<calendar_id> \
|
||||||
|
event_id=<event_id> \
|
||||||
|
--output json
|
||||||
|
|
||||||
|
# 列出日程
|
||||||
|
mcporter call lark-mcp.calendar.v4.calendar_event.list \
|
||||||
|
calendar_id=<calendar_id> \
|
||||||
|
start_time=<start_timestamp> \
|
||||||
|
end_time=<end_timestamp> \
|
||||||
|
--output json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用预设日历工具
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用默认日历预设(更简单)
|
||||||
|
mcporter call lark-mcp.preset.calendar.default.<action> ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## IM 消息操作
|
||||||
|
|
||||||
|
### 发送消息
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 发送文本消息到群聊
|
||||||
|
mcporter call lark-mcp.im.v1.message.create \
|
||||||
|
receive_id_type=chat_id \
|
||||||
|
--args '{
|
||||||
|
"receive_id": "<chat_id>",
|
||||||
|
"msg_type": "text",
|
||||||
|
"content": "{\"text\":\"Hello from MCP!\"}"
|
||||||
|
}' \
|
||||||
|
--output json
|
||||||
|
|
||||||
|
# 发送富文本消息
|
||||||
|
mcporter call lark-mcp.im.v1.message.create \
|
||||||
|
receive_id_type=chat_id \
|
||||||
|
--args '{
|
||||||
|
"receive_id": "<chat_id>",
|
||||||
|
"msg_type": "post",
|
||||||
|
"content": "{\"zh_cn\":{\"title\":\"标题\",\"content\":[[{\"tag\":\"text\",\"text\":\"内容\"}]]}}"
|
||||||
|
}' \
|
||||||
|
--output json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 读取群消息
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 获取群聊消息列表
|
||||||
|
mcporter call lark-mcp.im.v1.message.list \
|
||||||
|
container_id_type=chat \
|
||||||
|
container_id=<chat_id> \
|
||||||
|
--output json
|
||||||
|
|
||||||
|
# 获取单条消息
|
||||||
|
mcporter call lark-mcp.im.v1.message.get message_id=<message_id> --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 群聊管理
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 获取群聊列表
|
||||||
|
mcporter call lark-mcp.im.v1.chat.list --output json
|
||||||
|
|
||||||
|
# 获取群聊信息
|
||||||
|
mcporter call lark-mcp.im.v1.chat.get chat_id=<chat_id> --output json
|
||||||
|
|
||||||
|
# 获取群成员
|
||||||
|
mcporter call lark-mcp.im.v1.chat_members.get chat_id=<chat_id> --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 知识库 (Wiki)
|
||||||
|
|
||||||
|
### 获取知识库信息
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 列出知识空间
|
||||||
|
mcporter call lark-mcp.wiki.v2.space.list --output json
|
||||||
|
|
||||||
|
# 获取知识空间节点
|
||||||
|
mcporter call lark-mcp.wiki.v2.space_node.list space_id=<space_id> --output json
|
||||||
|
|
||||||
|
# 获取节点信息
|
||||||
|
mcporter call lark-mcp.wiki.v2.space_node.get token=<node_token> --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 用户与通讯录
|
||||||
|
|
||||||
|
### 获取用户信息
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 通过 open_id 获取用户
|
||||||
|
mcporter call lark-mcp.contact.v3.user.get \
|
||||||
|
user_id=<open_id> \
|
||||||
|
user_id_type=open_id \
|
||||||
|
--output json
|
||||||
|
|
||||||
|
# 搜索用户
|
||||||
|
mcporter call lark-mcp.contact.v3.user.batch_get_id \
|
||||||
|
--args '{"emails":["user@example.com"]}' \
|
||||||
|
--output json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常用预设工具
|
||||||
|
|
||||||
|
lark-mcp 提供了一些预设工具集,使用更简单:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 日历预设
|
||||||
|
preset.calendar.default
|
||||||
|
|
||||||
|
# IM 预设
|
||||||
|
preset.im.default
|
||||||
|
|
||||||
|
# 文档预设
|
||||||
|
preset.docx.default
|
||||||
|
```
|
||||||
|
|
||||||
|
启用预设工具需要在 MCP 配置中指定,例如:
|
||||||
|
`-t preset.calendar.default,preset.im.default`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **权限要求**:确保飞书应用已申请对应 API 的权限
|
||||||
|
2. **用户授权**:访问用户私有数据(文档、日程等)需要 user_access_token
|
||||||
|
3. **ID 类型**:user_id 有多种类型(open_id, union_id, user_id),注意指定 `user_id_type`
|
||||||
|
4. **时间戳**:时间参数使用 Unix 时间戳(秒)
|
||||||
|
5. **JSON 参数**:复杂参数使用 `--args '{...}'` 传递 JSON
|
||||||
|
|
||||||
|
## 错误排查
|
||||||
|
|
||||||
|
- **99991663**: 权限不足,检查应用权限配置
|
||||||
|
- **99991668**: token 过期,重新登录授权
|
||||||
|
- **99991400**: 参数错误,检查参数格式
|
||||||
11
extensions/lark/clawdbot.plugin.json
Normal file
11
extensions/lark/clawdbot.plugin.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"id": "lark",
|
||||||
|
"channels": [
|
||||||
|
"lark"
|
||||||
|
],
|
||||||
|
"configSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
extensions/lark/index.ts
Normal file
18
extensions/lark/index.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
||||||
|
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
|
import { larkPlugin } from "./src/channel.js";
|
||||||
|
import { setLarkRuntime } from "./src/runtime.js";
|
||||||
|
|
||||||
|
const plugin = {
|
||||||
|
id: "lark",
|
||||||
|
name: "Lark",
|
||||||
|
description: "Lark (Feishu) channel plugin",
|
||||||
|
configSchema: emptyPluginConfigSchema(),
|
||||||
|
register(api: ClawdbotPluginApi) {
|
||||||
|
setLarkRuntime(api.runtime);
|
||||||
|
api.registerChannel({ plugin: larkPlugin });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default plugin;
|
||||||
582
extensions/lark/package-lock.json
generated
Normal file
582
extensions/lark/package-lock.json
generated
Normal file
@ -0,0 +1,582 @@
|
|||||||
|
{
|
||||||
|
"name": "@clawdbot/lark",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@clawdbot/lark",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"dependencies": {
|
||||||
|
"@larksuiteoapi/node-sdk": "^1.37.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@larksuiteoapi/node-sdk": {
|
||||||
|
"version": "1.56.1",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/@larksuiteoapi/node-sdk/-/node-sdk-1.56.1.tgz",
|
||||||
|
"integrity": "sha512-/ixtyJnWOmcupKgDXz+6G6qTLMi3cNrR+LGOuq2PMwcJ6hhXTUJNyAF+ADY7ah9OoeDniGU/UJwMb2gqKdxwcA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "0.27.2",
|
||||||
|
"lodash.identity": "^3.0.0",
|
||||||
|
"lodash.merge": "^4.6.2",
|
||||||
|
"lodash.pickby": "^4.6.0",
|
||||||
|
"protobufjs": "^7.2.6",
|
||||||
|
"qs": "^6.13.0",
|
||||||
|
"ws": "^8.16.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/aspromise": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/base64": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/codegen": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/eventemitter": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/fetch": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@protobufjs/aspromise": "^1.1.1",
|
||||||
|
"@protobufjs/inquire": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/float": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/@protobufjs/float/-/float-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/inquire": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/path": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/@protobufjs/path/-/path-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/pool": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/utf8": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "25.0.10",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/@types/node/-/node-25.0.10.tgz",
|
||||||
|
"integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~7.16.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "0.27.2",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/axios/-/axios-0.27.2.tgz",
|
||||||
|
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.14.9",
|
||||||
|
"form-data": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bound": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/call-bound/-/call-bound-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"get-intrinsic": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-set-tostringtag": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.6",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.11",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.5",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/form-data/-/form-data-4.0.5.tgz",
|
||||||
|
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lodash.identity": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/lodash.identity/-/lodash.identity-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-AupTIzdLQxJS5wIYUQlgGyk2XRTfGXA+MCghDHqZk0pzUNYvd3EESS6dkChNauNYVIutcb0dfHw1ri9Q1yPV8Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.merge": {
|
||||||
|
"version": "4.6.2",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
|
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.pickby": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/lodash.pickby/-/lodash.pickby-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/long": {
|
||||||
|
"version": "5.3.2",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/long/-/long-5.3.2.tgz",
|
||||||
|
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/object-inspect": {
|
||||||
|
"version": "1.13.4",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/protobufjs": {
|
||||||
|
"version": "7.5.4",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/protobufjs/-/protobufjs-7.5.4.tgz",
|
||||||
|
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@protobufjs/aspromise": "^1.1.2",
|
||||||
|
"@protobufjs/base64": "^1.1.2",
|
||||||
|
"@protobufjs/codegen": "^2.0.4",
|
||||||
|
"@protobufjs/eventemitter": "^1.1.0",
|
||||||
|
"@protobufjs/fetch": "^1.1.0",
|
||||||
|
"@protobufjs/float": "^1.0.2",
|
||||||
|
"@protobufjs/inquire": "^1.1.0",
|
||||||
|
"@protobufjs/path": "^1.1.2",
|
||||||
|
"@protobufjs/pool": "^1.1.0",
|
||||||
|
"@protobufjs/utf8": "^1.1.0",
|
||||||
|
"@types/node": ">=13.7.0",
|
||||||
|
"long": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qs": {
|
||||||
|
"version": "6.14.1",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/qs/-/qs-6.14.1.tgz",
|
||||||
|
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-list": "^1.0.0",
|
||||||
|
"side-channel-map": "^1.0.1",
|
||||||
|
"side-channel-weakmap": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-list": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-map": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-weakmap": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-map": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "7.16.0",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
|
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.19.0",
|
||||||
|
"resolved": "http://mirrors.tencentyun.com/npm/ws/-/ws-8.19.0.tgz",
|
||||||
|
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
extensions/lark/package.json
Normal file
17
extensions/lark/package.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "@clawdbot/lark",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"description": "Clawdbot Lark (Feishu) channel plugin",
|
||||||
|
"clawdbot": {
|
||||||
|
"extensions": [
|
||||||
|
"./index.ts"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@larksuiteoapi/node-sdk": "^1.37.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
73
extensions/lark/readme.md
Normal file
73
extensions/lark/readme.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
clawdbot config set channels.lark.enabled true
|
||||||
|
clawdbot config set channels.lark.appId "cli_a9fc5173f1b8ded0"
|
||||||
|
clawdbot config set channels.lark.appSecret "zg3K9mVcKMcGdDBZTrf9Ybr3lKa2YFN6"
|
||||||
|
|
||||||
|
# Lark bot openID: Mentioned Only Response
|
||||||
|
clawdbot config set channels.lark.botOpenId "ou_60fbc6f87749c6b6f7816136d1d816b6"
|
||||||
|
|
||||||
|
|
||||||
|
## Cloudflare构建Lark Webhook链接:
|
||||||
|
cloudflared tunnel login
|
||||||
|
cloudflared tunnel create lark-webhook
|
||||||
|
|
||||||
|
# 每次都会变,临时方案:
|
||||||
|
cloudflared tunnel --url http://localhost:9000
|
||||||
|
|
||||||
|
|
||||||
|
# Oauth Lark:
|
||||||
|
https://warming-evanescence-tone-unions.trycloudflare.com/oauth/callback
|
||||||
|
|
||||||
|
clawdbot config set channels.lark.oauthRedirectUri "https://warming-evanescence-tone-unions.trycloudflare.com/oauth/callback"
|
||||||
|
|
||||||
|
# Oauth权限:
|
||||||
|
clawdbot config set channels.lark.oauthScope "docx:document wiki:wiki:readonly drive:drive:readonly bitable:app"
|
||||||
|
|
||||||
|
# Skills:
|
||||||
|
clawdbot config set 'skills.load.extraDirs' '["~/.clawdbot/skills"]'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Architecture:
|
||||||
|
Deploy: Tencent Cloud (Kaze大管家)
|
||||||
|
|
||||||
|
Clawdbot:
|
||||||
|
1. Skills:
|
||||||
|
mcporter -> Lark-MCP
|
||||||
|
Larkmcp Skills;
|
||||||
|
|
||||||
|
|
||||||
|
Webhook: Lark开发者平台(Bot App),回调 ->
|
||||||
|
|
||||||
|
接受信息:群里面有人at Bot -> (回调) https://warming-evanescence-tone-unions.trycloudflare.com/webhook (CloudFlare Tunnel)
|
||||||
|
-> 转发到 Tencent Cloud 43.162.107.61 (本机)9000端口 -> Lark Extension -> Clawdbot Channel
|
||||||
|
-> Agent 操作
|
||||||
|
Agent操作:
|
||||||
|
通过Lark-MCP:(https://github.com/larksuite/lark-openapi-mcp)
|
||||||
|
1. 读写群消息;
|
||||||
|
2. 读写文档;多维表格;
|
||||||
|
.....
|
||||||
|
|
||||||
|
发送信息:
|
||||||
|
Lark-MCP -> 发送信息:发到群里面;
|
||||||
|
|
||||||
|
|
||||||
|
改动点:
|
||||||
|
1. Fork了一份 -> 增加了extension下的lark
|
||||||
|
/home/ubuntu/kaze_moltbot/moltbot/extensions/lark
|
||||||
|
|
||||||
|
运行的是预装Clawdbot:
|
||||||
|
读指定路径下的extension:
|
||||||
|
|
||||||
|
2. 增加了lark-mcp skill:(SLILL.md)
|
||||||
|
/home/ubuntu/.clawdbot/skills/lark-mcp/SKILL.md
|
||||||
|
|
||||||
|
|
||||||
|
完全重新配置:
|
||||||
|
1. 先Tencent Cloud开一台预装了Clawdbot金属机
|
||||||
|
2. 增加我的这些改动点;
|
||||||
|
extension: lark
|
||||||
|
skill: lark-mcp
|
||||||
|
3. 在lark开发者平台上,创建新的Bot App,拿到App ID,App secret,权限配置好;
|
||||||
|
4. 运行clawdbot:
|
||||||
|
让他去支持:Lark-MCP:(https://github.com/larksuite/lark-openapi-mcp)
|
||||||
|
|
||||||
141
extensions/lark/src/accounts.ts
Normal file
141
extensions/lark/src/accounts.ts
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||||
|
import type { LarkChannelConfig, ResolvedLarkAccount, LarkAccountConfig } from "./types.js";
|
||||||
|
|
||||||
|
export const DEFAULT_ACCOUNT_ID = "default";
|
||||||
|
|
||||||
|
// Get lark channel config from clawdbot config
|
||||||
|
function getLarkConfig(cfg: ClawdbotConfig): LarkChannelConfig | undefined {
|
||||||
|
return cfg.channels?.lark as LarkChannelConfig | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all configured lark account IDs
|
||||||
|
export function listLarkAccountIds(cfg: ClawdbotConfig): string[] {
|
||||||
|
const larkConfig = getLarkConfig(cfg);
|
||||||
|
if (!larkConfig) return [];
|
||||||
|
|
||||||
|
const accountIds = new Set<string>();
|
||||||
|
|
||||||
|
// Check if base config has credentials (default account)
|
||||||
|
if (larkConfig.appId || process.env.LARK_APP_ID) {
|
||||||
|
accountIds.add(DEFAULT_ACCOUNT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add named accounts
|
||||||
|
if (larkConfig.accounts) {
|
||||||
|
for (const accountId of Object.keys(larkConfig.accounts)) {
|
||||||
|
accountIds.add(accountId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(accountIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve default account ID
|
||||||
|
export function resolveDefaultLarkAccountId(cfg: ClawdbotConfig): string {
|
||||||
|
const accountIds = listLarkAccountIds(cfg);
|
||||||
|
return accountIds.length > 0 ? accountIds[0] : DEFAULT_ACCOUNT_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize account ID
|
||||||
|
export function normalizeAccountId(accountId?: string): string {
|
||||||
|
const trimmed = accountId?.trim().toLowerCase() ?? "";
|
||||||
|
if (!trimmed || trimmed === "default") return DEFAULT_ACCOUNT_ID;
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve a specific lark account
|
||||||
|
export function resolveLarkAccount(params: {
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
accountId?: string;
|
||||||
|
}): ResolvedLarkAccount {
|
||||||
|
const { cfg, accountId } = params;
|
||||||
|
const resolvedAccountId = normalizeAccountId(accountId);
|
||||||
|
const larkConfig = getLarkConfig(cfg);
|
||||||
|
|
||||||
|
// Check for named account first
|
||||||
|
const accountConfig =
|
||||||
|
resolvedAccountId !== DEFAULT_ACCOUNT_ID
|
||||||
|
? larkConfig?.accounts?.[resolvedAccountId]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Resolve credentials with fallback chain: account config -> base config -> env
|
||||||
|
const appIdFromAccount = accountConfig?.appId?.trim();
|
||||||
|
const appIdFromBase = larkConfig?.appId?.trim();
|
||||||
|
const appIdFromEnv = process.env.LARK_APP_ID?.trim();
|
||||||
|
|
||||||
|
const appSecretFromAccount = accountConfig?.appSecret?.trim();
|
||||||
|
const appSecretFromBase = larkConfig?.appSecret?.trim();
|
||||||
|
const appSecretFromEnv = process.env.LARK_APP_SECRET?.trim();
|
||||||
|
|
||||||
|
const appId = appIdFromAccount || appIdFromBase || appIdFromEnv || "";
|
||||||
|
const appSecret = appSecretFromAccount || appSecretFromBase || appSecretFromEnv || "";
|
||||||
|
|
||||||
|
// Determine source of credentials
|
||||||
|
let appIdSource: "config" | "env" | "none" = "none";
|
||||||
|
if (appIdFromAccount || appIdFromBase) {
|
||||||
|
appIdSource = "config";
|
||||||
|
} else if (appIdFromEnv) {
|
||||||
|
appIdSource = "env";
|
||||||
|
}
|
||||||
|
|
||||||
|
let appSecretSource: "config" | "env" | "none" = "none";
|
||||||
|
if (appSecretFromAccount || appSecretFromBase) {
|
||||||
|
appSecretSource = "config";
|
||||||
|
} else if (appSecretFromEnv) {
|
||||||
|
appSecretSource = "env";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve other config values
|
||||||
|
const encryptKey =
|
||||||
|
accountConfig?.encryptKey?.trim() ||
|
||||||
|
larkConfig?.encryptKey?.trim() ||
|
||||||
|
process.env.LARK_ENCRYPT_KEY?.trim();
|
||||||
|
|
||||||
|
const verificationToken =
|
||||||
|
accountConfig?.verificationToken?.trim() ||
|
||||||
|
larkConfig?.verificationToken?.trim() ||
|
||||||
|
process.env.LARK_VERIFICATION_TOKEN?.trim();
|
||||||
|
|
||||||
|
const botOpenId =
|
||||||
|
accountConfig?.botOpenId?.trim() ||
|
||||||
|
larkConfig?.botOpenId?.trim() ||
|
||||||
|
process.env.LARK_BOT_OPEN_ID?.trim();
|
||||||
|
|
||||||
|
// Determine if enabled
|
||||||
|
const enabled =
|
||||||
|
resolvedAccountId !== DEFAULT_ACCOUNT_ID
|
||||||
|
? accountConfig?.enabled !== false
|
||||||
|
: larkConfig?.enabled !== false;
|
||||||
|
|
||||||
|
// Build merged config for this account
|
||||||
|
const mergedConfig: LarkAccountConfig = {
|
||||||
|
...larkConfig,
|
||||||
|
...accountConfig,
|
||||||
|
appId,
|
||||||
|
appSecret,
|
||||||
|
encryptKey,
|
||||||
|
verificationToken,
|
||||||
|
botOpenId,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
accountId: resolvedAccountId,
|
||||||
|
name: accountConfig?.name || (resolvedAccountId === DEFAULT_ACCOUNT_ID ? undefined : resolvedAccountId),
|
||||||
|
enabled,
|
||||||
|
appId,
|
||||||
|
appSecret,
|
||||||
|
encryptKey,
|
||||||
|
verificationToken,
|
||||||
|
appIdSource,
|
||||||
|
appSecretSource,
|
||||||
|
config: mergedConfig,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all enabled lark accounts
|
||||||
|
export function listEnabledLarkAccounts(cfg: ClawdbotConfig): ResolvedLarkAccount[] {
|
||||||
|
const accountIds = listLarkAccountIds(cfg);
|
||||||
|
return accountIds
|
||||||
|
.map((accountId) => resolveLarkAccount({ cfg, accountId }))
|
||||||
|
.filter((account) => account.enabled && account.appId && account.appSecret);
|
||||||
|
}
|
||||||
343
extensions/lark/src/channel.ts
Normal file
343
extensions/lark/src/channel.ts
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
import {
|
||||||
|
DEFAULT_ACCOUNT_ID,
|
||||||
|
type ChannelPlugin,
|
||||||
|
} from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
|
import {
|
||||||
|
listLarkAccountIds,
|
||||||
|
resolveLarkAccount,
|
||||||
|
resolveDefaultLarkAccountId,
|
||||||
|
listEnabledLarkAccounts,
|
||||||
|
normalizeAccountId,
|
||||||
|
} from "./accounts.js";
|
||||||
|
import { sendMessageLark, probeLark } from "./send.js";
|
||||||
|
import { monitorLarkProvider } from "./monitor.js";
|
||||||
|
import { getLarkRuntime } from "./runtime.js";
|
||||||
|
import type { ResolvedLarkAccount } from "./types.js";
|
||||||
|
|
||||||
|
// Channel metadata
|
||||||
|
const meta = {
|
||||||
|
id: "lark" as const,
|
||||||
|
name: "Lark",
|
||||||
|
displayName: "Lark (Feishu)",
|
||||||
|
icon: "💬",
|
||||||
|
docsUrl: "https://docs.clawd.bot/channels/lark",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normalize target ID for messaging
|
||||||
|
function normalizeLarkMessagingTarget(target: string): string {
|
||||||
|
return target
|
||||||
|
.trim()
|
||||||
|
.replace(/^lark:/i, "")
|
||||||
|
.replace(/^feishu:/i, "")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if target looks like a Lark ID
|
||||||
|
function looksLikeLarkTargetId(target: string): boolean {
|
||||||
|
const normalized = normalizeLarkMessagingTarget(target);
|
||||||
|
// Lark chat IDs typically start with 'oc_' for group chats
|
||||||
|
// or are user open_ids
|
||||||
|
return /^oc_[\w-]+$/.test(normalized) || /^ou_[\w-]+$/.test(normalized) || normalized.length > 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const larkPlugin: ChannelPlugin<ResolvedLarkAccount> = {
|
||||||
|
id: "lark",
|
||||||
|
meta: {
|
||||||
|
...meta,
|
||||||
|
quickstartAllowFrom: true,
|
||||||
|
},
|
||||||
|
capabilities: {
|
||||||
|
chatTypes: ["direct", "group"],
|
||||||
|
reactions: false,
|
||||||
|
threads: true,
|
||||||
|
media: true,
|
||||||
|
nativeCommands: false,
|
||||||
|
},
|
||||||
|
reload: { configPrefixes: ["channels.lark"] },
|
||||||
|
config: {
|
||||||
|
listAccountIds: (cfg) => listLarkAccountIds(cfg),
|
||||||
|
resolveAccount: (cfg, accountId) => resolveLarkAccount({ cfg, accountId }),
|
||||||
|
defaultAccountId: (cfg) => resolveDefaultLarkAccountId(cfg),
|
||||||
|
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
||||||
|
const resolvedAccountId = normalizeAccountId(accountId);
|
||||||
|
const updated = { ...cfg };
|
||||||
|
|
||||||
|
if (resolvedAccountId === DEFAULT_ACCOUNT_ID) {
|
||||||
|
updated.channels = {
|
||||||
|
...updated.channels,
|
||||||
|
lark: {
|
||||||
|
...updated.channels?.lark,
|
||||||
|
enabled,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
updated.channels = {
|
||||||
|
...updated.channels,
|
||||||
|
lark: {
|
||||||
|
...updated.channels?.lark,
|
||||||
|
accounts: {
|
||||||
|
...updated.channels?.lark?.accounts,
|
||||||
|
[resolvedAccountId]: {
|
||||||
|
...updated.channels?.lark?.accounts?.[resolvedAccountId],
|
||||||
|
enabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
},
|
||||||
|
deleteAccount: ({ cfg, accountId }) => {
|
||||||
|
const resolvedAccountId = normalizeAccountId(accountId);
|
||||||
|
const updated = { ...cfg };
|
||||||
|
|
||||||
|
if (resolvedAccountId === DEFAULT_ACCOUNT_ID) {
|
||||||
|
if (updated.channels?.lark) {
|
||||||
|
const { appId, appSecret, encryptKey, verificationToken, ...rest } = updated.channels.lark;
|
||||||
|
updated.channels = {
|
||||||
|
...updated.channels,
|
||||||
|
lark: rest,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const accounts = { ...updated.channels?.lark?.accounts };
|
||||||
|
delete accounts[resolvedAccountId];
|
||||||
|
updated.channels = {
|
||||||
|
...updated.channels,
|
||||||
|
lark: {
|
||||||
|
...updated.channels?.lark,
|
||||||
|
accounts: Object.keys(accounts).length > 0 ? accounts : undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
},
|
||||||
|
isConfigured: (account) => Boolean(account.appId && account.appSecret),
|
||||||
|
describeAccount: (account) => ({
|
||||||
|
accountId: account.accountId,
|
||||||
|
name: account.name,
|
||||||
|
enabled: account.enabled,
|
||||||
|
configured: Boolean(account.appId && account.appSecret),
|
||||||
|
appIdSource: account.appIdSource,
|
||||||
|
appSecretSource: account.appSecretSource,
|
||||||
|
}),
|
||||||
|
resolveAllowFrom: ({ cfg, accountId }) =>
|
||||||
|
(resolveLarkAccount({ cfg, accountId }).config.allowFrom ?? []).map((entry) => String(entry)),
|
||||||
|
formatAllowFrom: ({ allowFrom }) =>
|
||||||
|
allowFrom
|
||||||
|
.map((entry) => String(entry).trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((entry) => entry.replace(/^(lark|feishu):/i, ""))
|
||||||
|
.map((entry) => entry.toLowerCase()),
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
||||||
|
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
||||||
|
const useAccountPath = Boolean(cfg.channels?.lark?.accounts?.[resolvedAccountId]);
|
||||||
|
const basePath = useAccountPath
|
||||||
|
? `channels.lark.accounts.${resolvedAccountId}.`
|
||||||
|
: "channels.lark.";
|
||||||
|
return {
|
||||||
|
policy: account.config.dmPolicy ?? "pairing",
|
||||||
|
allowFrom: account.config.allowFrom ?? [],
|
||||||
|
policyPath: `${basePath}dmPolicy`,
|
||||||
|
allowFromPath: basePath,
|
||||||
|
approveHint: `clawdbot config set channels.lark.allowFrom <user_open_id>`,
|
||||||
|
normalizeEntry: (raw) => raw.replace(/^(lark|feishu):/i, ""),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
collectWarnings: ({ account, cfg }) => {
|
||||||
|
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
||||||
|
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
||||||
|
if (groupPolicy !== "open") return [];
|
||||||
|
return [
|
||||||
|
`- Lark groups: groupPolicy="open" with no group allowlist; any group can add the bot.`,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
groups: {
|
||||||
|
resolveRequireMention: ({ cfg, accountId, groupId }) => {
|
||||||
|
const account = resolveLarkAccount({ cfg, accountId });
|
||||||
|
const groups = account.config.groups;
|
||||||
|
const groupConfig = groups?.[groupId] ?? groups?.["*"];
|
||||||
|
return groupConfig?.requireMention ?? true;
|
||||||
|
},
|
||||||
|
resolveToolPolicy: ({ cfg, accountId, groupId }) => {
|
||||||
|
const account = resolveLarkAccount({ cfg, accountId });
|
||||||
|
const groups = account.config.groups;
|
||||||
|
const groupConfig = groups?.[groupId] ?? groups?.["*"];
|
||||||
|
return groupConfig?.toolPolicy ?? "default";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
threading: {
|
||||||
|
resolveReplyToMode: ({ cfg }) => cfg.channels?.lark?.replyToMode ?? "first",
|
||||||
|
},
|
||||||
|
messaging: {
|
||||||
|
normalizeTarget: normalizeLarkMessagingTarget,
|
||||||
|
targetResolver: {
|
||||||
|
looksLikeId: looksLikeLarkTargetId,
|
||||||
|
hint: "<chatId|openId>",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
directory: {
|
||||||
|
self: async () => null,
|
||||||
|
listPeers: async () => [],
|
||||||
|
listGroups: async () => [],
|
||||||
|
},
|
||||||
|
setup: {
|
||||||
|
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||||
|
applyAccountName: ({ cfg, accountId, name }) => {
|
||||||
|
const resolvedAccountId = normalizeAccountId(accountId);
|
||||||
|
if (resolvedAccountId === DEFAULT_ACCOUNT_ID) {
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
channels: {
|
||||||
|
...cfg.channels,
|
||||||
|
lark: {
|
||||||
|
...cfg.channels?.lark,
|
||||||
|
accounts: {
|
||||||
|
...cfg.channels?.lark?.accounts,
|
||||||
|
[resolvedAccountId]: {
|
||||||
|
...cfg.channels?.lark?.accounts?.[resolvedAccountId],
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
validateInput: ({ accountId, input }) => {
|
||||||
|
if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
|
||||||
|
return "Lark env credentials can only be used for the default account.";
|
||||||
|
}
|
||||||
|
if (!input.useEnv && (!input.appId || !input.appSecret)) {
|
||||||
|
return "Lark requires --app-id and --app-secret (or --use-env).";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||||
|
const resolvedAccountId = normalizeAccountId(accountId);
|
||||||
|
if (resolvedAccountId === DEFAULT_ACCOUNT_ID) {
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
channels: {
|
||||||
|
...cfg.channels,
|
||||||
|
lark: {
|
||||||
|
...cfg.channels?.lark,
|
||||||
|
enabled: true,
|
||||||
|
...(input.useEnv
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
...(input.appId ? { appId: input.appId } : {}),
|
||||||
|
...(input.appSecret ? { appSecret: input.appSecret } : {}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
channels: {
|
||||||
|
...cfg.channels,
|
||||||
|
lark: {
|
||||||
|
...cfg.channels?.lark,
|
||||||
|
enabled: true,
|
||||||
|
accounts: {
|
||||||
|
...cfg.channels?.lark?.accounts,
|
||||||
|
[resolvedAccountId]: {
|
||||||
|
...cfg.channels?.lark?.accounts?.[resolvedAccountId],
|
||||||
|
enabled: true,
|
||||||
|
...(input.appId ? { appId: input.appId } : {}),
|
||||||
|
...(input.appSecret ? { appSecret: input.appSecret } : {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outbound: {
|
||||||
|
deliveryMode: "direct",
|
||||||
|
chunker: (text, limit) => getLarkRuntime().channel.text.chunkMarkdownText(text, limit),
|
||||||
|
chunkerMode: "markdown",
|
||||||
|
textChunkLimit: 4000,
|
||||||
|
sendText: async ({ to, text, accountId, replyToId }) => {
|
||||||
|
const result = await sendMessageLark(to, text, {
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
replyToId: replyToId ?? undefined,
|
||||||
|
});
|
||||||
|
return { channel: "lark", ...result };
|
||||||
|
},
|
||||||
|
sendMedia: async ({ to, text, mediaUrl, accountId, replyToId }) => {
|
||||||
|
const result = await sendMessageLark(to, text, {
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
mediaUrl: mediaUrl ?? undefined,
|
||||||
|
replyToId: replyToId ?? undefined,
|
||||||
|
});
|
||||||
|
return { channel: "lark", ...result };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
defaultRuntime: {
|
||||||
|
accountId: DEFAULT_ACCOUNT_ID,
|
||||||
|
running: false,
|
||||||
|
lastStartAt: null,
|
||||||
|
lastStopAt: null,
|
||||||
|
lastError: null,
|
||||||
|
},
|
||||||
|
buildChannelSummary: ({ snapshot }) => ({
|
||||||
|
configured: snapshot.configured ?? false,
|
||||||
|
appIdSource: snapshot.appIdSource ?? "none",
|
||||||
|
appSecretSource: snapshot.appSecretSource ?? "none",
|
||||||
|
running: snapshot.running ?? false,
|
||||||
|
lastStartAt: snapshot.lastStartAt ?? null,
|
||||||
|
lastStopAt: snapshot.lastStopAt ?? null,
|
||||||
|
lastError: snapshot.lastError ?? null,
|
||||||
|
probe: snapshot.probe,
|
||||||
|
lastProbeAt: snapshot.lastProbeAt ?? null,
|
||||||
|
}),
|
||||||
|
probeAccount: async ({ account, timeoutMs }) => {
|
||||||
|
if (!account.appId || !account.appSecret) {
|
||||||
|
return { ok: false, error: "missing credentials" };
|
||||||
|
}
|
||||||
|
return await probeLark(account.appId, account.appSecret, timeoutMs);
|
||||||
|
},
|
||||||
|
buildAccountSnapshot: ({ account, runtime, probe }) => {
|
||||||
|
const configured = Boolean(account.appId && account.appSecret);
|
||||||
|
return {
|
||||||
|
accountId: account.accountId,
|
||||||
|
name: account.name,
|
||||||
|
enabled: account.enabled,
|
||||||
|
configured,
|
||||||
|
appIdSource: account.appIdSource,
|
||||||
|
appSecretSource: account.appSecretSource,
|
||||||
|
running: runtime?.running ?? false,
|
||||||
|
lastStartAt: runtime?.lastStartAt ?? null,
|
||||||
|
lastStopAt: runtime?.lastStopAt ?? null,
|
||||||
|
lastError: runtime?.lastError ?? null,
|
||||||
|
probe,
|
||||||
|
lastInboundAt: runtime?.lastInboundAt ?? null,
|
||||||
|
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gateway: {
|
||||||
|
startAccount: async (ctx) => {
|
||||||
|
const account = ctx.account;
|
||||||
|
ctx.log?.info(`[${account.accountId}] starting Lark provider`);
|
||||||
|
return monitorLarkProvider({
|
||||||
|
accountId: account.accountId,
|
||||||
|
config: ctx.cfg,
|
||||||
|
runtime: ctx.runtime,
|
||||||
|
abortSignal: ctx.abortSignal,
|
||||||
|
webhookPort: account.config.webhookPort,
|
||||||
|
webhookPath: account.config.webhookPath,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
170
extensions/lark/src/client.ts
Normal file
170
extensions/lark/src/client.ts
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
import * as lark from "@larksuiteoapi/node-sdk";
|
||||||
|
import type { ResolvedLarkAccount } from "./types.js";
|
||||||
|
|
||||||
|
// Cache for Lark clients
|
||||||
|
const clientCache = new Map<string, lark.Client>();
|
||||||
|
|
||||||
|
// Create or get cached Lark client
|
||||||
|
export function getLarkClient(account: ResolvedLarkAccount): lark.Client {
|
||||||
|
const cacheKey = `${account.appId}:${account.appSecret}`;
|
||||||
|
|
||||||
|
let client = clientCache.get(cacheKey);
|
||||||
|
if (!client) {
|
||||||
|
client = new lark.Client({
|
||||||
|
appId: account.appId,
|
||||||
|
appSecret: account.appSecret,
|
||||||
|
disableTokenCache: false,
|
||||||
|
});
|
||||||
|
clientCache.set(cacheKey, client);
|
||||||
|
}
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear client cache (useful for testing or credential rotation)
|
||||||
|
export function clearLarkClientCache(): void {
|
||||||
|
clientCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send text message to a chat
|
||||||
|
export async function sendLarkMessage(params: {
|
||||||
|
client: lark.Client;
|
||||||
|
chatId: string;
|
||||||
|
content: string;
|
||||||
|
msgType?: "text" | "post" | "interactive";
|
||||||
|
}): Promise<{ messageId: string; chatId: string }> {
|
||||||
|
const { client, chatId, content, msgType = "text" } = params;
|
||||||
|
|
||||||
|
// Prepare content based on message type
|
||||||
|
let formattedContent: string;
|
||||||
|
if (msgType === "text") {
|
||||||
|
formattedContent = JSON.stringify({ text: content });
|
||||||
|
} else {
|
||||||
|
formattedContent = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await client.im.message.create({
|
||||||
|
params: {
|
||||||
|
receive_id_type: "chat_id",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
receive_id: chatId,
|
||||||
|
msg_type: msgType,
|
||||||
|
content: formattedContent,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.code !== 0) {
|
||||||
|
throw new Error(`Lark API error: ${response.msg} (code: ${response.code})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
messageId: response.data?.message_id ?? "unknown",
|
||||||
|
chatId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reply to a message in a thread
|
||||||
|
export async function replyLarkMessage(params: {
|
||||||
|
client: lark.Client;
|
||||||
|
messageId: string;
|
||||||
|
content: string;
|
||||||
|
msgType?: "text" | "post" | "interactive";
|
||||||
|
}): Promise<{ messageId: string }> {
|
||||||
|
const { client, messageId, content, msgType = "text" } = params;
|
||||||
|
|
||||||
|
let formattedContent: string;
|
||||||
|
if (msgType === "text") {
|
||||||
|
formattedContent = JSON.stringify({ text: content });
|
||||||
|
} else {
|
||||||
|
formattedContent = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await client.im.message.reply({
|
||||||
|
path: {
|
||||||
|
message_id: messageId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
msg_type: msgType,
|
||||||
|
content: formattedContent,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.code !== 0) {
|
||||||
|
throw new Error(`Lark API error: ${response.msg} (code: ${response.code})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
messageId: response.data?.message_id ?? "unknown",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send image message
|
||||||
|
export async function sendLarkImage(params: {
|
||||||
|
client: lark.Client;
|
||||||
|
chatId: string;
|
||||||
|
imageKey: string;
|
||||||
|
}): Promise<{ messageId: string; chatId: string }> {
|
||||||
|
const { client, chatId, imageKey } = params;
|
||||||
|
|
||||||
|
const content = JSON.stringify({ image_key: imageKey });
|
||||||
|
|
||||||
|
const response = await client.im.message.create({
|
||||||
|
params: {
|
||||||
|
receive_id_type: "chat_id",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
receive_id: chatId,
|
||||||
|
msg_type: "image",
|
||||||
|
content,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.code !== 0) {
|
||||||
|
throw new Error(`Lark API error: ${response.msg} (code: ${response.code})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
messageId: response.data?.message_id ?? "unknown",
|
||||||
|
chatId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload image to Lark
|
||||||
|
export async function uploadLarkImage(params: {
|
||||||
|
client: lark.Client;
|
||||||
|
image: Buffer;
|
||||||
|
imageType?: "message" | "avatar";
|
||||||
|
}): Promise<string> {
|
||||||
|
const { client, image, imageType = "message" } = params;
|
||||||
|
|
||||||
|
const response = await client.im.image.create({
|
||||||
|
data: {
|
||||||
|
image_type: imageType,
|
||||||
|
image: Buffer.from(image),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.code !== 0) {
|
||||||
|
throw new Error(`Lark image upload error: ${response.msg} (code: ${response.code})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data?.image_key ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get bot info
|
||||||
|
export async function getLarkBotInfo(client: lark.Client): Promise<{
|
||||||
|
appName: string;
|
||||||
|
openId: string;
|
||||||
|
}> {
|
||||||
|
const response = await client.bot.botInfo.get({});
|
||||||
|
|
||||||
|
if (response.code !== 0) {
|
||||||
|
throw new Error(`Lark API error: ${response.msg} (code: ${response.code})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
appName: response.data?.bot?.bot_name ?? "unknown",
|
||||||
|
openId: response.data?.bot?.open_id ?? "",
|
||||||
|
};
|
||||||
|
}
|
||||||
678
extensions/lark/src/monitor.ts
Normal file
678
extensions/lark/src/monitor.ts
Normal file
@ -0,0 +1,678 @@
|
|||||||
|
import * as http from "node:http";
|
||||||
|
import * as url from "node:url";
|
||||||
|
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||||
|
import type { RuntimeEnv } from "clawdbot/plugin-sdk";
|
||||||
|
import type { ResolvedLarkAccount } from "./types.js";
|
||||||
|
import { resolveLarkAccount } from "./accounts.js";
|
||||||
|
import { getLarkClient } from "./client.js";
|
||||||
|
import { getLarkRuntime } from "./runtime.js";
|
||||||
|
import {
|
||||||
|
hasUserAuthorized,
|
||||||
|
generateAuthUrl,
|
||||||
|
exchangeCodeForToken,
|
||||||
|
getValidUserToken,
|
||||||
|
type UserToken,
|
||||||
|
} from "./oauth.js";
|
||||||
|
|
||||||
|
export type MonitorLarkOpts = {
|
||||||
|
accountId?: string;
|
||||||
|
config?: ClawdbotConfig;
|
||||||
|
runtime?: RuntimeEnv;
|
||||||
|
abortSignal?: AbortSignal;
|
||||||
|
webhookPort?: number;
|
||||||
|
webhookPath?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pending message type - stores messages waiting for user authorization
|
||||||
|
type PendingMessage = {
|
||||||
|
senderId: string;
|
||||||
|
chatId: string;
|
||||||
|
messageText: string;
|
||||||
|
messageId: string;
|
||||||
|
chatType: "p2p" | "group";
|
||||||
|
senderName?: string;
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cache for pending messages (keyed by senderId)
|
||||||
|
const pendingMessagesCache = new Map<string, PendingMessage>();
|
||||||
|
|
||||||
|
// Store a pending message for later processing after auth
|
||||||
|
function storePendingMessage(msg: PendingMessage) {
|
||||||
|
pendingMessagesCache.set(msg.senderId, msg);
|
||||||
|
// Clean up old pending messages (older than 10 minutes)
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [key, value] of pendingMessagesCache.entries()) {
|
||||||
|
if (now - value.timestamp > 10 * 60 * 1000) {
|
||||||
|
pendingMessagesCache.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and remove pending message for a user
|
||||||
|
function getPendingMessage(senderId: string): PendingMessage | undefined {
|
||||||
|
const msg = pendingMessagesCache.get(senderId);
|
||||||
|
if (msg) {
|
||||||
|
pendingMessagesCache.delete(senderId);
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract text content from Lark message
|
||||||
|
function extractMessageText(content: string, messageType: string): string {
|
||||||
|
try {
|
||||||
|
if (messageType === "text") {
|
||||||
|
const parsed = JSON.parse(content);
|
||||||
|
return parsed.text ?? "";
|
||||||
|
}
|
||||||
|
// Handle other message types as needed
|
||||||
|
return content;
|
||||||
|
} catch {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send authorization link to user
|
||||||
|
async function sendAuthLink(params: {
|
||||||
|
account: ResolvedLarkAccount;
|
||||||
|
chatId: string;
|
||||||
|
senderId: string;
|
||||||
|
webhookPort: number;
|
||||||
|
log: (...args: any[]) => void;
|
||||||
|
}) {
|
||||||
|
const { account, chatId, senderId, webhookPort, log } = params;
|
||||||
|
|
||||||
|
// Build redirect URI
|
||||||
|
const redirectUri = account.config.oauthRedirectUri ||
|
||||||
|
`http://localhost:${webhookPort}/oauth/callback`;
|
||||||
|
|
||||||
|
// Default scopes for document access
|
||||||
|
const scope = account.config.oauthScope ||
|
||||||
|
"docx:document:readonly wiki:wiki:readonly drive:drive:readonly";
|
||||||
|
|
||||||
|
// Generate auth URL with state containing senderId for tracking
|
||||||
|
const authUrl = generateAuthUrl({
|
||||||
|
appId: account.appId,
|
||||||
|
redirectUri,
|
||||||
|
state: senderId,
|
||||||
|
scope,
|
||||||
|
});
|
||||||
|
|
||||||
|
log(`[lark:${account.accountId}] Sending auth link to ${senderId}`);
|
||||||
|
|
||||||
|
// Send message with authorization link
|
||||||
|
const client = getLarkClient(account);
|
||||||
|
await client.im.message.create({
|
||||||
|
params: {
|
||||||
|
receive_id_type: "chat_id",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
receive_id: chatId,
|
||||||
|
msg_type: "interactive",
|
||||||
|
content: JSON.stringify({
|
||||||
|
config: {
|
||||||
|
wide_screen_mode: true,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
title: {
|
||||||
|
tag: "plain_text",
|
||||||
|
content: "🔐 需要授权",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "div",
|
||||||
|
text: {
|
||||||
|
tag: "lark_md",
|
||||||
|
content: "要使用文档相关功能,需要你先授权机器人访问你的文档。\n\n点击下方按钮完成授权(只需一次):",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "action",
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
tag: "button",
|
||||||
|
text: {
|
||||||
|
tag: "plain_text",
|
||||||
|
content: "点击授权",
|
||||||
|
},
|
||||||
|
type: "primary",
|
||||||
|
url: authUrl,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle incoming Lark event
|
||||||
|
async function handleLarkEvent(params: {
|
||||||
|
data: any;
|
||||||
|
account: ResolvedLarkAccount;
|
||||||
|
config: ClawdbotConfig;
|
||||||
|
runtime?: RuntimeEnv;
|
||||||
|
webhookPort?: number;
|
||||||
|
skipMentionCheck?: boolean; // Skip mention check for pending message reprocessing
|
||||||
|
}) {
|
||||||
|
const { data, account, config, runtime, webhookPort = 9000, skipMentionCheck = false } = params;
|
||||||
|
const log = runtime?.log ?? console.log;
|
||||||
|
const error = runtime?.error ?? console.error;
|
||||||
|
|
||||||
|
// Get event type from header or schema
|
||||||
|
const eventType = data.header?.event_type ?? data.event?.type;
|
||||||
|
|
||||||
|
if (eventType !== "im.message.receive_v1") {
|
||||||
|
log(`[lark:${account.accountId}] Ignoring event type: ${eventType}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = data.event;
|
||||||
|
if (!event) {
|
||||||
|
log(`[lark:${account.accountId}] No event data`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = event.message;
|
||||||
|
const sender = event.sender;
|
||||||
|
|
||||||
|
if (!message || !sender) {
|
||||||
|
log(`[lark:${account.accountId}] Missing message or sender`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageText = extractMessageText(message.content, message.message_type);
|
||||||
|
const chatId = message.chat_id;
|
||||||
|
const chatType = message.chat_type;
|
||||||
|
const senderId = sender.sender_id?.open_id ?? sender.sender_id?.user_id ?? "unknown";
|
||||||
|
const senderUnionId = sender.sender_id?.union_id;
|
||||||
|
const messageId = message.message_id;
|
||||||
|
const isGroup = chatType !== "p2p";
|
||||||
|
|
||||||
|
// Try to get sender's name from Lark API (requires contact:user.base:readonly permission)
|
||||||
|
let senderName: string | undefined;
|
||||||
|
try {
|
||||||
|
const client = getLarkClient(account);
|
||||||
|
const userInfo = await client.contact.v3.user.get({
|
||||||
|
path: { user_id: senderId },
|
||||||
|
params: { user_id_type: "open_id" },
|
||||||
|
});
|
||||||
|
senderName = userInfo.data?.user?.name;
|
||||||
|
} catch {
|
||||||
|
// Silently ignore - app may not have user info permission
|
||||||
|
// Sender name is optional, we'll just use open_id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build sender info string for logging and context
|
||||||
|
const senderInfo = senderName
|
||||||
|
? `${senderName} (${senderId})`
|
||||||
|
: senderId;
|
||||||
|
|
||||||
|
// Get bot's open_id from cache
|
||||||
|
const botOpenId = botOpenIdCache.get(account.accountId);
|
||||||
|
|
||||||
|
// Check if bot was mentioned in the message
|
||||||
|
// Only respond if THIS bot is @mentioned, not just any @mention
|
||||||
|
const mentions = message.mentions ?? [];
|
||||||
|
const isBotMentioned = botOpenId
|
||||||
|
? mentions.some((m: any) => m.id?.open_id === botOpenId)
|
||||||
|
: false; // If we don't have bot info cached, don't respond to avoid responding to other bots' mentions
|
||||||
|
|
||||||
|
// Only log mention details if there are mentions
|
||||||
|
if (mentions.length > 0) {
|
||||||
|
log(`[lark:${account.accountId}] Mentions: ${mentions.map((m: any) => m.name).join(", ")}, isBotMentioned: ${isBotMentioned}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For group chats, only respond when bot is @mentioned (unless skipMentionCheck is true)
|
||||||
|
if (isGroup && !isBotMentioned && !skipMentionCheck) {
|
||||||
|
log(`[lark:${account.accountId}] Ignoring group message (bot not mentioned)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build session key for this conversation
|
||||||
|
const sessionKey = `lark:${chatId}`;
|
||||||
|
|
||||||
|
// Log inbound message
|
||||||
|
log(`[lark:${account.accountId}] Received message from ${senderInfo} in ${chatType}: ${messageText.slice(0, 50)}...`);
|
||||||
|
|
||||||
|
// Record channel activity
|
||||||
|
try {
|
||||||
|
getLarkRuntime().channel.activity.record({
|
||||||
|
channel: "lark",
|
||||||
|
accountId: account.accountId,
|
||||||
|
direction: "inbound",
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore activity recording errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve agent route for session management
|
||||||
|
const route = getLarkRuntime().channel.routing.resolveAgentRoute({
|
||||||
|
cfg: config,
|
||||||
|
channel: "lark",
|
||||||
|
accountId: account.accountId,
|
||||||
|
});
|
||||||
|
const agentId = route.agentId;
|
||||||
|
|
||||||
|
// Use a single shared session for all Lark chats (groups and DMs)
|
||||||
|
const finalSessionKey = route.mainSessionKey;
|
||||||
|
|
||||||
|
// Check if user has authorized (for document access features)
|
||||||
|
const userAuthorized = hasUserAuthorized(senderId);
|
||||||
|
let userToken: UserToken | null = null;
|
||||||
|
|
||||||
|
if (userAuthorized) {
|
||||||
|
userToken = await getValidUserToken({ account, openId: senderId });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if message requires document access (simple keyword check)
|
||||||
|
const needsDocAccess = /文档|授权|document|doc|wiki|知识库|云文档|飞书文档/i.test(messageText);
|
||||||
|
|
||||||
|
// If needs doc access but not authorized, send auth link and store pending message
|
||||||
|
if (needsDocAccess && !userToken && account.config.requireUserAuth !== false) {
|
||||||
|
log(`[lark:${account.accountId}] User ${senderId} needs authorization for document access`);
|
||||||
|
try {
|
||||||
|
// Store the message for processing after authorization
|
||||||
|
const pendingMsg = {
|
||||||
|
senderId,
|
||||||
|
chatId,
|
||||||
|
messageText,
|
||||||
|
messageId,
|
||||||
|
chatType: chatType as "p2p" | "group",
|
||||||
|
senderName,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
storePendingMessage(pendingMsg);
|
||||||
|
log(`[lark:${account.accountId}] Stored pending message for senderId: ${senderId}, messageId: ${messageId}`);
|
||||||
|
|
||||||
|
await sendAuthLink({
|
||||||
|
account,
|
||||||
|
chatId,
|
||||||
|
senderId,
|
||||||
|
webhookPort,
|
||||||
|
log,
|
||||||
|
});
|
||||||
|
return; // Don't process the message further until user authorizes
|
||||||
|
} catch (authErr) {
|
||||||
|
error(`[lark:${account.accountId}] Failed to send auth link: ${authErr}`);
|
||||||
|
// Continue processing without user token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the inbound context for clawdbot
|
||||||
|
// Include sender info (name and open_id) so the agent knows who sent the message
|
||||||
|
const bodyWithSource = isGroup
|
||||||
|
? `[From: ${senderInfo} in lark group: ${chatId}]\n${messageText}`
|
||||||
|
: `[From: ${senderInfo}]\n${messageText}`;
|
||||||
|
|
||||||
|
const inboundCtx = {
|
||||||
|
Body: bodyWithSource,
|
||||||
|
RawBody: messageText,
|
||||||
|
From: `lark:${senderId}`,
|
||||||
|
FromName: senderName,
|
||||||
|
FromOpenId: senderId,
|
||||||
|
To: `lark:${chatId}`,
|
||||||
|
SessionKey: finalSessionKey,
|
||||||
|
AccountId: account.accountId,
|
||||||
|
ChatType: isGroup ? "group" : "direct",
|
||||||
|
Surface: "lark",
|
||||||
|
Provider: "lark",
|
||||||
|
MessageSid: messageId,
|
||||||
|
IsMentioned: isBotMentioned,
|
||||||
|
UserAuthorized: userAuthorized,
|
||||||
|
UserAccessToken: userToken?.accessToken,
|
||||||
|
OriginatingChannel: "lark" as const,
|
||||||
|
OriginatingTo: `lark:${chatId}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try to dispatch through clawdbot's reply system
|
||||||
|
try {
|
||||||
|
const finalizedCtx = getLarkRuntime().channel.reply.finalizeInboundContext(inboundCtx);
|
||||||
|
|
||||||
|
// Record session for chat history
|
||||||
|
try {
|
||||||
|
const storePath = getLarkRuntime().channel.session.resolveStorePath(config.session?.store, { agentId });
|
||||||
|
await getLarkRuntime().channel.session.recordInboundSession({
|
||||||
|
storePath,
|
||||||
|
sessionKey: finalizedCtx.SessionKey ?? finalSessionKey,
|
||||||
|
ctx: finalizedCtx,
|
||||||
|
createIfMissing: true,
|
||||||
|
// Always update last route so replies go to the most recent chat
|
||||||
|
updateLastRoute: {
|
||||||
|
sessionKey: route.mainSessionKey,
|
||||||
|
channel: "lark",
|
||||||
|
to: chatId,
|
||||||
|
accountId: account.accountId,
|
||||||
|
},
|
||||||
|
onRecordError: (err) => {
|
||||||
|
log(`[lark:${account.accountId}] Session record error: ${err}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (sessionErr) {
|
||||||
|
log(`[lark:${account.accountId}] Failed to record session: ${sessionErr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a simple dispatcher that sends replies back to Lark
|
||||||
|
const client = getLarkClient(account);
|
||||||
|
|
||||||
|
const sendReply = async (text: string) => {
|
||||||
|
await client.im.message.create({
|
||||||
|
params: {
|
||||||
|
receive_id_type: "chat_id",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
receive_id: chatId,
|
||||||
|
msg_type: "text",
|
||||||
|
content: JSON.stringify({ text }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
log(`[lark:${account.accountId}] Sent reply to ${chatId}`);
|
||||||
|
|
||||||
|
// Record outbound activity
|
||||||
|
getLarkRuntime().channel.activity.record({
|
||||||
|
channel: "lark",
|
||||||
|
accountId: account.accountId,
|
||||||
|
direction: "outbound",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use dispatchReplyWithBufferedBlockDispatcher for AI reply
|
||||||
|
const result = await getLarkRuntime().channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
||||||
|
ctx: finalizedCtx,
|
||||||
|
cfg: config,
|
||||||
|
dispatcherOptions: {
|
||||||
|
deliver: async (payload: any, info: any) => {
|
||||||
|
if (payload.text) {
|
||||||
|
await sendReply(payload.text);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (err: any, info: any) => {
|
||||||
|
error(`[lark:${account.accountId}] Reply error: ${err}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.queuedFinal) {
|
||||||
|
log(`[lark:${account.accountId}] No response generated`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error(`[lark:${account.accountId}] Failed to dispatch reply: ${err}`);
|
||||||
|
|
||||||
|
// Fallback: send simple echo reply
|
||||||
|
try {
|
||||||
|
const client = getLarkClient(account);
|
||||||
|
await client.im.message.create({
|
||||||
|
params: {
|
||||||
|
receive_id_type: "chat_id",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
receive_id: chatId,
|
||||||
|
msg_type: "text",
|
||||||
|
content: JSON.stringify({ text: `收到: ${messageText.slice(0, 100)}` }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (fallbackErr) {
|
||||||
|
error(`[lark:${account.accountId}] Fallback reply also failed: ${fallbackErr}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache for bot open_id per account
|
||||||
|
const botOpenIdCache = new Map<string, string>();
|
||||||
|
|
||||||
|
// Start webhook server to receive Lark events
|
||||||
|
export async function monitorLarkProvider(opts: MonitorLarkOpts = {}) {
|
||||||
|
const cfg = opts.config ?? getLarkRuntime().config.loadConfig();
|
||||||
|
const account = resolveLarkAccount({
|
||||||
|
cfg,
|
||||||
|
accountId: opts.accountId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!account.appId || !account.appSecret) {
|
||||||
|
throw new Error(
|
||||||
|
`Lark credentials missing for account "${account.accountId}" (set channels.lark.appId/appSecret or LARK_APP_ID/LARK_APP_SECRET).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const port = opts.webhookPort ?? account.config.webhookPort ?? 9000;
|
||||||
|
const webhookPath = opts.webhookPath ?? account.config.webhookPath ?? "/webhook";
|
||||||
|
|
||||||
|
const log = opts.runtime?.log ?? console.log;
|
||||||
|
const error = opts.runtime?.error ?? console.error;
|
||||||
|
|
||||||
|
// Get bot's open_id from config or environment
|
||||||
|
const botOpenId = account.config.botOpenId?.trim() || process.env.LARK_BOT_OPEN_ID?.trim();
|
||||||
|
if (botOpenId) {
|
||||||
|
botOpenIdCache.set(account.accountId, botOpenId);
|
||||||
|
log(`[lark:${account.accountId}] Bot open_id from config: ${botOpenId}`);
|
||||||
|
} else {
|
||||||
|
log(`[lark:${account.accountId}] Warning: No botOpenId configured. Bot will not respond to @mentions in groups.`);
|
||||||
|
log(`[lark:${account.accountId}] Set channels.lark.botOpenId in config or LARK_BOT_OPEN_ID env var.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to check if URL matches webhook path
|
||||||
|
const matchesWebhookPath = (url: string | undefined): boolean => {
|
||||||
|
if (!url) return false;
|
||||||
|
const urlPath = url.split("?")[0]; // Remove query string
|
||||||
|
return urlPath === webhookPath || urlPath === webhookPath + "/";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create HTTP server
|
||||||
|
const server = http.createServer(async (req, res) => {
|
||||||
|
// Health check endpoint
|
||||||
|
if (req.method === "GET" && req.url === "/health") {
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ status: "ok", channel: "lark", accountId: account.accountId }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle webhook POST requests
|
||||||
|
if (req.method === "POST" && matchesWebhookPath(req.url)) {
|
||||||
|
let body = "";
|
||||||
|
req.on("data", (chunk) => {
|
||||||
|
body += chunk.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on("end", async () => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(body);
|
||||||
|
log(`[lark:${account.accountId}] Received webhook: ${JSON.stringify(data).slice(0, 200)}...`);
|
||||||
|
|
||||||
|
// Handle URL verification challenge from Lark
|
||||||
|
if (data.type === "url_verification") {
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ challenge: data.challenge }));
|
||||||
|
log(`[lark:${account.accountId}] URL verification completed`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle event callback (v2 schema)
|
||||||
|
if (data.schema === "2.0" || data.header) {
|
||||||
|
// Process event asynchronously
|
||||||
|
handleLarkEvent({
|
||||||
|
data,
|
||||||
|
account,
|
||||||
|
config: cfg,
|
||||||
|
runtime: opts.runtime as RuntimeEnv,
|
||||||
|
webhookPort: port,
|
||||||
|
}).catch((err) => {
|
||||||
|
error(`[lark:${account.accountId}] Event handler error: ${err}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Respond immediately to Lark
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ success: true }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle event callback (v1 schema)
|
||||||
|
if (data.event) {
|
||||||
|
handleLarkEvent({
|
||||||
|
data,
|
||||||
|
account,
|
||||||
|
config: cfg,
|
||||||
|
runtime: opts.runtime as RuntimeEnv,
|
||||||
|
webhookPort: port,
|
||||||
|
}).catch((err) => {
|
||||||
|
error(`[lark:${account.accountId}] Event handler error: ${err}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ success: true }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown request type
|
||||||
|
log(`[lark:${account.accountId}] Unknown webhook data: ${JSON.stringify(data).slice(0, 100)}`);
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ success: true }));
|
||||||
|
} catch (err) {
|
||||||
|
error(`[lark:${account.accountId}] Webhook error: ${err}`);
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.writeHead(500, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ error: "Internal server error" }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuth callback endpoint
|
||||||
|
if (req.method === "GET" && req.url?.startsWith("/oauth/callback")) {
|
||||||
|
try {
|
||||||
|
const parsedUrl = url.parse(req.url, true);
|
||||||
|
const code = parsedUrl.query.code as string;
|
||||||
|
const state = parsedUrl.query.state as string;
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
||||||
|
res.end("<h1>授权失败</h1><p>缺少授权码</p>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`[lark:${account.accountId}] OAuth callback received, code: ${code.slice(0, 10)}...`);
|
||||||
|
|
||||||
|
// Get the redirect_uri that was used for this OAuth flow
|
||||||
|
const redirectUri = account.config.oauthRedirectUri ||
|
||||||
|
`http://localhost:${port}/oauth/callback`;
|
||||||
|
|
||||||
|
// Exchange code for token
|
||||||
|
const token = await exchangeCodeForToken({ account, code, redirectUri });
|
||||||
|
|
||||||
|
log(`[lark:${account.accountId}] User authorized: ${token.openId}`);
|
||||||
|
|
||||||
|
// Check for pending message from this user
|
||||||
|
log(`[lark:${account.accountId}] Looking for pending message with key: ${token.openId}`);
|
||||||
|
log(`[lark:${account.accountId}] Current pending messages: ${JSON.stringify([...pendingMessagesCache.keys()])}`);
|
||||||
|
|
||||||
|
const pendingMsg = getPendingMessage(token.openId);
|
||||||
|
|
||||||
|
if (pendingMsg) {
|
||||||
|
log(`[lark:${account.accountId}] Found pending message: ${JSON.stringify(pendingMsg)}`);
|
||||||
|
log(`[lark:${account.accountId}] Processing pending message for ${token.openId}`);
|
||||||
|
|
||||||
|
// Process the pending message with the new user token
|
||||||
|
// Build a fake event data structure to reprocess the message
|
||||||
|
const fakeEventData = {
|
||||||
|
schema: "2.0",
|
||||||
|
header: { event_type: "im.message.receive_v1" },
|
||||||
|
event: {
|
||||||
|
sender: {
|
||||||
|
sender_id: { open_id: pendingMsg.senderId },
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
message_id: pendingMsg.messageId,
|
||||||
|
chat_id: pendingMsg.chatId,
|
||||||
|
chat_type: pendingMsg.chatType,
|
||||||
|
message_type: "text",
|
||||||
|
content: JSON.stringify({ text: pendingMsg.messageText }),
|
||||||
|
mentions: [], // User already authorized, no need for mention check bypass
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process asynchronously so we can respond to the OAuth callback immediately
|
||||||
|
handleLarkEvent({
|
||||||
|
data: fakeEventData,
|
||||||
|
account,
|
||||||
|
config: cfg,
|
||||||
|
runtime: opts.runtime as RuntimeEnv,
|
||||||
|
webhookPort: port,
|
||||||
|
skipMentionCheck: true, // Skip mention check for pending messages
|
||||||
|
}).catch((err) => {
|
||||||
|
error(`[lark:${account.accountId}] Failed to process pending message: ${err}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
||||||
|
res.end(`
|
||||||
|
<html>
|
||||||
|
<head><title>授权成功</title></head>
|
||||||
|
<body style="font-family: sans-serif; text-align: center; padding: 50px;">
|
||||||
|
<h1>✅ 授权成功!</h1>
|
||||||
|
<p>你的 Lark 账号已成功授权给机器人。</p>
|
||||||
|
<p>正在处理你之前的消息,请回到 Lark 查看回复。</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
log(`[lark:${account.accountId}] No pending message found for ${token.openId}`);
|
||||||
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
||||||
|
res.end(`
|
||||||
|
<html>
|
||||||
|
<head><title>授权成功</title></head>
|
||||||
|
<body style="font-family: sans-serif; text-align: center; padding: 50px;">
|
||||||
|
<h1>✅ 授权成功!</h1>
|
||||||
|
<p>你的 Lark 账号已成功授权给机器人。</p>
|
||||||
|
<p>现在可以关闭此页面,回到 Lark 继续使用。</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error(`[lark:${account.accountId}] OAuth callback error: ${err}`);
|
||||||
|
res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
|
||||||
|
res.end(`
|
||||||
|
<html>
|
||||||
|
<head><title>授权失败</title></head>
|
||||||
|
<body style="font-family: sans-serif; text-align: center; padding: 50px;">
|
||||||
|
<h1>❌ 授权失败</h1>
|
||||||
|
<p>错误: ${err}</p>
|
||||||
|
<p>请重试或联系管理员。</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 404 for other requests
|
||||||
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||||
|
res.end("Not found");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle abort signal
|
||||||
|
if (opts.abortSignal) {
|
||||||
|
opts.abortSignal.addEventListener(
|
||||||
|
"abort",
|
||||||
|
() => {
|
||||||
|
log(`[lark:${account.accountId}] Stopping webhook server`);
|
||||||
|
server.close();
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
server.on("error", (err) => {
|
||||||
|
error(`[lark:${account.accountId}] Server error: ${err}`);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(port, () => {
|
||||||
|
log(`[lark:${account.accountId}] Webhook server listening on port ${port} at ${webhookPath}`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
312
extensions/lark/src/oauth.ts
Normal file
312
extensions/lark/src/oauth.ts
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import type { ResolvedLarkAccount } from "./types.js";
|
||||||
|
|
||||||
|
// Token storage directory
|
||||||
|
const TOKEN_STORAGE_DIR = process.env.LARK_TOKEN_DIR || path.join(process.env.HOME || "/tmp", ".clawdbot", "lark-tokens");
|
||||||
|
|
||||||
|
// Ensure token directory exists
|
||||||
|
function ensureTokenDir() {
|
||||||
|
if (!fs.existsSync(TOKEN_STORAGE_DIR)) {
|
||||||
|
fs.mkdirSync(TOKEN_STORAGE_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token data structure
|
||||||
|
export type UserToken = {
|
||||||
|
openId: string;
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
expiresAt: number; // Unix timestamp in ms
|
||||||
|
refreshExpiresAt: number; // Unix timestamp in ms
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get token file path for a user
|
||||||
|
function getTokenPath(openId: string): string {
|
||||||
|
ensureTokenDir();
|
||||||
|
// Sanitize openId for filename
|
||||||
|
const safeId = openId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
||||||
|
return path.join(TOKEN_STORAGE_DIR, `${safeId}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load user token from storage
|
||||||
|
export function loadUserToken(openId: string): UserToken | null {
|
||||||
|
const tokenPath = getTokenPath(openId);
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(tokenPath)) {
|
||||||
|
const data = fs.readFileSync(tokenPath, "utf-8");
|
||||||
|
return JSON.parse(data) as UserToken;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[lark-oauth] Failed to load token for ${openId}:`, err);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save user token to storage
|
||||||
|
export function saveUserToken(token: UserToken): void {
|
||||||
|
const tokenPath = getTokenPath(token.openId);
|
||||||
|
try {
|
||||||
|
ensureTokenDir();
|
||||||
|
fs.writeFileSync(tokenPath, JSON.stringify(token, null, 2));
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[lark-oauth] Failed to save token for ${token.openId}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete user token
|
||||||
|
export function deleteUserToken(openId: string): void {
|
||||||
|
const tokenPath = getTokenPath(openId);
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(tokenPath)) {
|
||||||
|
fs.unlinkSync(tokenPath);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[lark-oauth] Failed to delete token for ${openId}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token is expired (with 5 minute buffer)
|
||||||
|
export function isTokenExpired(token: UserToken): boolean {
|
||||||
|
return Date.now() > token.expiresAt - 5 * 60 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if refresh token is expired
|
||||||
|
export function isRefreshTokenExpired(token: UserToken): boolean {
|
||||||
|
return Date.now() > token.refreshExpiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API base URL - use Lark (International) or Feishu (China)
|
||||||
|
// Set LARK_API_BASE=https://open.feishu.cn for China version
|
||||||
|
const API_BASE = process.env.LARK_API_BASE || "https://open.larksuite.com";
|
||||||
|
|
||||||
|
// Generate OAuth authorization URL
|
||||||
|
export function generateAuthUrl(params: {
|
||||||
|
appId: string;
|
||||||
|
redirectUri: string;
|
||||||
|
state?: string;
|
||||||
|
scope?: string;
|
||||||
|
}): string {
|
||||||
|
const { appId, redirectUri, state, scope } = params;
|
||||||
|
const baseUrl = `${API_BASE}/open-apis/authen/v1/authorize`;
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
app_id: appId,
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
response_type: "code",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
queryParams.set("state", state);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scope) {
|
||||||
|
queryParams.set("scope", scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${baseUrl}?${queryParams.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get app_access_token for API calls
|
||||||
|
async function getAppAccessToken(appId: string, appSecret: string): Promise<string> {
|
||||||
|
const response = await fetch(`${API_BASE}/open-apis/auth/v3/app_access_token/internal`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
app_id: appId,
|
||||||
|
app_secret: appSecret,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.code !== 0 || !data.app_access_token) {
|
||||||
|
throw new Error(`Failed to get app_access_token: ${data.msg} (code: ${data.code})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.app_access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exchange authorization code for tokens
|
||||||
|
export async function exchangeCodeForToken(params: {
|
||||||
|
account: ResolvedLarkAccount;
|
||||||
|
code: string;
|
||||||
|
redirectUri?: string;
|
||||||
|
}): Promise<UserToken> {
|
||||||
|
const { account, code, redirectUri } = params;
|
||||||
|
|
||||||
|
// Step 1: Get app_access_token
|
||||||
|
const appAccessToken = await getAppAccessToken(account.appId, account.appSecret);
|
||||||
|
console.log("[lark-oauth] Got app_access_token");
|
||||||
|
|
||||||
|
// Step 2: Exchange code for user_access_token using app_access_token
|
||||||
|
const tokenResponse = await fetch(`${API_BASE}/open-apis/authen/v1/access_token`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${appAccessToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokenData = await tokenResponse.json();
|
||||||
|
console.log("[lark-oauth] Token response:", JSON.stringify(tokenData, null, 2));
|
||||||
|
|
||||||
|
if (tokenData.code !== 0 || !tokenData.data) {
|
||||||
|
throw new Error(`Failed to exchange code: ${tokenData.msg} (code: ${tokenData.code})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = tokenData.data;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Get user info using the user access token
|
||||||
|
const userInfoResponse = await fetch(`${API_BASE}/open-apis/authen/v1/user_info`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${data.access_token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const userInfoData = await userInfoResponse.json();
|
||||||
|
|
||||||
|
if (userInfoData.code !== 0 || !userInfoData.data) {
|
||||||
|
throw new Error(`Failed to get user info: ${userInfoData.msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const openId = userInfoData.data.open_id;
|
||||||
|
if (!openId) {
|
||||||
|
throw new Error("No open_id in user info response");
|
||||||
|
}
|
||||||
|
|
||||||
|
const token: UserToken = {
|
||||||
|
openId,
|
||||||
|
accessToken: data.access_token!,
|
||||||
|
refreshToken: data.refresh_token!,
|
||||||
|
expiresAt: now + (data.expires_in ?? 7200) * 1000,
|
||||||
|
refreshExpiresAt: now + (data.refresh_expires_in ?? 30 * 24 * 3600) * 1000,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
saveUserToken(token);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh user access token
|
||||||
|
export async function refreshUserToken(params: {
|
||||||
|
account: ResolvedLarkAccount;
|
||||||
|
token: UserToken;
|
||||||
|
}): Promise<UserToken> {
|
||||||
|
const { account, token } = params;
|
||||||
|
|
||||||
|
if (isRefreshTokenExpired(token)) {
|
||||||
|
deleteUserToken(token.openId);
|
||||||
|
throw new Error("Refresh token expired, user needs to re-authorize");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get app_access_token first
|
||||||
|
const appAccessToken = await getAppAccessToken(account.appId, account.appSecret);
|
||||||
|
|
||||||
|
// Use app_access_token to refresh user token
|
||||||
|
const response = await fetch(`${API_BASE}/open-apis/authen/v1/refresh_access_token`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${appAccessToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
refresh_token: token.refreshToken,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseData = await response.json();
|
||||||
|
|
||||||
|
if (responseData.code !== 0 || !responseData.data) {
|
||||||
|
throw new Error(`Failed to refresh token: ${responseData.msg} (code: ${responseData.code})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = responseData.data;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
const newToken: UserToken = {
|
||||||
|
...token,
|
||||||
|
accessToken: data.access_token!,
|
||||||
|
refreshToken: data.refresh_token!,
|
||||||
|
expiresAt: now + (data.expires_in ?? 7200) * 1000,
|
||||||
|
refreshExpiresAt: now + (data.refresh_expires_in ?? 30 * 24 * 3600) * 1000,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
saveUserToken(newToken);
|
||||||
|
return newToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get valid user access token (refresh if needed)
|
||||||
|
export async function getValidUserToken(params: {
|
||||||
|
account: ResolvedLarkAccount;
|
||||||
|
openId: string;
|
||||||
|
}): Promise<UserToken | null> {
|
||||||
|
const { account, openId } = params;
|
||||||
|
|
||||||
|
const token = loadUserToken(openId);
|
||||||
|
if (!token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRefreshTokenExpired(token)) {
|
||||||
|
deleteUserToken(openId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTokenExpired(token)) {
|
||||||
|
try {
|
||||||
|
return await refreshUserToken({ account, token });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[lark-oauth] Failed to refresh token for ${openId}:`, err);
|
||||||
|
deleteUserToken(openId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has authorized
|
||||||
|
export function hasUserAuthorized(openId: string): boolean {
|
||||||
|
const token = loadUserToken(openId);
|
||||||
|
if (!token) return false;
|
||||||
|
if (isRefreshTokenExpired(token)) {
|
||||||
|
deleteUserToken(openId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all authorized users
|
||||||
|
export function listAuthorizedUsers(): string[] {
|
||||||
|
ensureTokenDir();
|
||||||
|
try {
|
||||||
|
const files = fs.readdirSync(TOKEN_STORAGE_DIR);
|
||||||
|
return files
|
||||||
|
.filter(f => f.endsWith(".json"))
|
||||||
|
.map(f => {
|
||||||
|
try {
|
||||||
|
const data = fs.readFileSync(path.join(TOKEN_STORAGE_DIR, f), "utf-8");
|
||||||
|
const token = JSON.parse(data) as UserToken;
|
||||||
|
return token.openId;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((id): id is string => id !== null);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
14
extensions/lark/src/runtime.ts
Normal file
14
extensions/lark/src/runtime.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { PluginRuntime } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
|
let runtime: PluginRuntime | null = null;
|
||||||
|
|
||||||
|
export function setLarkRuntime(next: PluginRuntime) {
|
||||||
|
runtime = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLarkRuntime(): PluginRuntime {
|
||||||
|
if (!runtime) {
|
||||||
|
throw new Error("Lark runtime not initialized");
|
||||||
|
}
|
||||||
|
return runtime;
|
||||||
|
}
|
||||||
159
extensions/lark/src/send.ts
Normal file
159
extensions/lark/src/send.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||||
|
import { resolveLarkAccount } from "./accounts.js";
|
||||||
|
import { getLarkClient, sendLarkMessage, uploadLarkImage, sendLarkImage } from "./client.js";
|
||||||
|
import { getLarkRuntime } from "./runtime.js";
|
||||||
|
|
||||||
|
export type LarkSendOpts = {
|
||||||
|
accountId?: string;
|
||||||
|
mediaUrl?: string;
|
||||||
|
replyToId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LarkSendResult = {
|
||||||
|
messageId: string;
|
||||||
|
chatId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normalize chat ID - handle various input formats
|
||||||
|
function normalizeChatId(to: string): string {
|
||||||
|
const trimmed = to.trim();
|
||||||
|
if (!trimmed) throw new Error("Recipient is required for Lark sends");
|
||||||
|
|
||||||
|
// Strip common prefixes
|
||||||
|
let normalized = trimmed
|
||||||
|
.replace(/^lark:/i, "")
|
||||||
|
.replace(/^feishu:/i, "")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
if (!normalized) throw new Error("Recipient is required for Lark sends");
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send message to Lark
|
||||||
|
export async function sendMessageLark(
|
||||||
|
to: string,
|
||||||
|
text: string,
|
||||||
|
opts: LarkSendOpts = {},
|
||||||
|
): Promise<LarkSendResult> {
|
||||||
|
const cfg = getLarkRuntime().config.loadConfig();
|
||||||
|
const account = resolveLarkAccount({
|
||||||
|
cfg,
|
||||||
|
accountId: opts.accountId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!account.appId || !account.appSecret) {
|
||||||
|
throw new Error(
|
||||||
|
`Lark credentials missing for account "${account.accountId}" (set channels.lark.appId/appSecret or LARK_APP_ID/LARK_APP_SECRET).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatId = normalizeChatId(to);
|
||||||
|
const client = getLarkClient(account);
|
||||||
|
|
||||||
|
// Handle media if provided
|
||||||
|
if (opts.mediaUrl) {
|
||||||
|
try {
|
||||||
|
const media = await getLarkRuntime().media.loadWebMedia(opts.mediaUrl);
|
||||||
|
const mime = media.contentType?.toLowerCase() ?? "";
|
||||||
|
|
||||||
|
// Upload and send image if it's an image type
|
||||||
|
if (mime.startsWith("image/")) {
|
||||||
|
const imageKey = await uploadLarkImage({
|
||||||
|
client,
|
||||||
|
image: media.buffer,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If there's text, send image first then text
|
||||||
|
const imageResult = await sendLarkImage({
|
||||||
|
client,
|
||||||
|
chatId,
|
||||||
|
imageKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (text?.trim()) {
|
||||||
|
const textResult = await sendLarkMessage({
|
||||||
|
client,
|
||||||
|
chatId,
|
||||||
|
content: text,
|
||||||
|
});
|
||||||
|
return textResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageResult;
|
||||||
|
}
|
||||||
|
// For non-image media, include URL in text
|
||||||
|
const textWithMedia = `${text}\n\n📎 ${opts.mediaUrl}`;
|
||||||
|
return await sendLarkMessage({
|
||||||
|
client,
|
||||||
|
chatId,
|
||||||
|
content: textWithMedia,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback to text with media link on error
|
||||||
|
console.warn(`Lark media upload failed, falling back to link: ${err}`);
|
||||||
|
const textWithMedia = `${text}\n\n📎 ${opts.mediaUrl}`;
|
||||||
|
return await sendLarkMessage({
|
||||||
|
client,
|
||||||
|
chatId,
|
||||||
|
content: textWithMedia,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send text message
|
||||||
|
if (!text?.trim()) {
|
||||||
|
throw new Error("Message must be non-empty for Lark sends");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await sendLarkMessage({
|
||||||
|
client,
|
||||||
|
chatId,
|
||||||
|
content: text,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Probe Lark connection
|
||||||
|
export async function probeLark(
|
||||||
|
appId: string,
|
||||||
|
appSecret: string,
|
||||||
|
timeoutMs = 5000,
|
||||||
|
): Promise<{ ok: boolean; bot?: { appName: string; openId: string }; error?: string }> {
|
||||||
|
try {
|
||||||
|
const client = getLarkClient({
|
||||||
|
accountId: "probe",
|
||||||
|
name: "probe",
|
||||||
|
enabled: true,
|
||||||
|
appId,
|
||||||
|
appSecret,
|
||||||
|
appIdSource: "config",
|
||||||
|
appSecretSource: "config",
|
||||||
|
config: { appId, appSecret },
|
||||||
|
});
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await client.bot.botInfo.get({});
|
||||||
|
|
||||||
|
if (response.code !== 0) {
|
||||||
|
return { ok: false, error: response.msg };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
bot: {
|
||||||
|
appName: response.data?.bot?.bot_name ?? "unknown",
|
||||||
|
openId: response.data?.bot?.open_id ?? "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: err instanceof Error ? err.message : String(err),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
129
extensions/lark/src/types.ts
Normal file
129
extensions/lark/src/types.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
|
// Lark configuration stored in clawdbot config file
|
||||||
|
export type LarkChannelConfig = {
|
||||||
|
enabled?: boolean;
|
||||||
|
appId?: string;
|
||||||
|
appSecret?: string;
|
||||||
|
encryptKey?: string;
|
||||||
|
verificationToken?: string;
|
||||||
|
// Bot's open_id for mention filtering (get from Lark developer console or event logs)
|
||||||
|
botOpenId?: string;
|
||||||
|
// OAuth configuration for user authorization
|
||||||
|
oauthRedirectUri?: string; // e.g., "http://localhost:9000/oauth/callback"
|
||||||
|
oauthScope?: string; // e.g., "docx:document:readonly wiki:wiki:readonly"
|
||||||
|
requireUserAuth?: boolean; // If true, require user authorization for certain features
|
||||||
|
// Webhook configuration
|
||||||
|
webhookPort?: number;
|
||||||
|
webhookPath?: string;
|
||||||
|
// DM policy
|
||||||
|
dmPolicy?: "open" | "pairing" | "allowlist";
|
||||||
|
allowFrom?: string[];
|
||||||
|
// Group policy
|
||||||
|
groupPolicy?: "open" | "allowlist";
|
||||||
|
groups?: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
enabled?: boolean;
|
||||||
|
requireMention?: boolean;
|
||||||
|
toolPolicy?: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
// Named accounts for multi-bot setups
|
||||||
|
accounts?: Record<string, LarkAccountConfig>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LarkAccountConfig = {
|
||||||
|
enabled?: boolean;
|
||||||
|
name?: string;
|
||||||
|
appId?: string;
|
||||||
|
appSecret?: string;
|
||||||
|
encryptKey?: string;
|
||||||
|
verificationToken?: string;
|
||||||
|
botOpenId?: string;
|
||||||
|
// OAuth configuration
|
||||||
|
oauthRedirectUri?: string;
|
||||||
|
oauthScope?: string;
|
||||||
|
requireUserAuth?: boolean;
|
||||||
|
webhookPort?: number;
|
||||||
|
webhookPath?: string;
|
||||||
|
dmPolicy?: "open" | "pairing" | "allowlist";
|
||||||
|
allowFrom?: string[];
|
||||||
|
groupPolicy?: "open" | "allowlist";
|
||||||
|
groups?: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
enabled?: boolean;
|
||||||
|
requireMention?: boolean;
|
||||||
|
toolPolicy?: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResolvedLarkAccount = {
|
||||||
|
accountId: string;
|
||||||
|
name?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
appId: string;
|
||||||
|
appSecret: string;
|
||||||
|
encryptKey?: string;
|
||||||
|
verificationToken?: string;
|
||||||
|
appIdSource: "config" | "env" | "none";
|
||||||
|
appSecretSource: "config" | "env" | "none";
|
||||||
|
config: LarkAccountConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lark event types
|
||||||
|
export type LarkMessageEvent = {
|
||||||
|
schema: string;
|
||||||
|
header: {
|
||||||
|
event_id: string;
|
||||||
|
event_type: string;
|
||||||
|
create_time: string;
|
||||||
|
token: string;
|
||||||
|
app_id: string;
|
||||||
|
tenant_key: string;
|
||||||
|
};
|
||||||
|
event: {
|
||||||
|
sender: {
|
||||||
|
sender_id: {
|
||||||
|
union_id: string;
|
||||||
|
user_id: string;
|
||||||
|
open_id: string;
|
||||||
|
};
|
||||||
|
sender_type: string;
|
||||||
|
tenant_key: string;
|
||||||
|
};
|
||||||
|
message: {
|
||||||
|
message_id: string;
|
||||||
|
root_id?: string;
|
||||||
|
parent_id?: string;
|
||||||
|
create_time: string;
|
||||||
|
update_time?: string;
|
||||||
|
chat_id: string;
|
||||||
|
chat_type: "p2p" | "group";
|
||||||
|
message_type: string;
|
||||||
|
content: string;
|
||||||
|
mentions?: Array<{
|
||||||
|
key: string;
|
||||||
|
id: {
|
||||||
|
union_id: string;
|
||||||
|
user_id: string;
|
||||||
|
open_id: string;
|
||||||
|
};
|
||||||
|
name: string;
|
||||||
|
tenant_key: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extend ClawdbotConfig to include lark channel
|
||||||
|
declare module "clawdbot/plugin-sdk" {
|
||||||
|
interface ClawdbotConfig {
|
||||||
|
channels?: {
|
||||||
|
lark?: LarkChannelConfig;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user