opencode-chat-channel 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +298 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +242 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# opencode-chat-channel
|
|
2
|
+
|
|
3
|
+
An [opencode](https://opencode.ai) plugin that connects your Feishu (Lark) bot to opencode via **WebSocket long connection** — no public IP required.
|
|
4
|
+
|
|
5
|
+
[中文说明](#中文说明) · [English](#english)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## English
|
|
10
|
+
|
|
11
|
+
### How It Works
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Feishu User sends message
|
|
15
|
+
↓ WebSocket long connection
|
|
16
|
+
Feishu Open Platform
|
|
17
|
+
↓ @larksuiteoapi/node-sdk WSClient
|
|
18
|
+
chat-channel plugin (this repo)
|
|
19
|
+
↓ client.session.prompt()
|
|
20
|
+
opencode AI (Sisyphus)
|
|
21
|
+
↓ text reply
|
|
22
|
+
Feishu User receives reply
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- Each Feishu user (`open_id`) gets their own persistent opencode session
|
|
26
|
+
- Sessions expire after 2 hours of inactivity (auto-cleanup)
|
|
27
|
+
- Replies longer than 4000 characters are automatically split into multiple messages
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
### Quick Install (Recommended)
|
|
32
|
+
|
|
33
|
+
Run this single command in your terminal:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
curl -fsSL https://raw.githubusercontent.com/coneycode/opencode-chat-channel/main/install.sh | bash
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The installer will:
|
|
40
|
+
1. Download the plugin to `~/.config/opencode/plugins/chat-channel.ts`
|
|
41
|
+
2. Add `@larksuiteoapi/node-sdk` to `~/.config/opencode/package.json`
|
|
42
|
+
3. Run `bun install` to install dependencies
|
|
43
|
+
4. Prompt you for your Feishu **App ID** (saved to `.env`) and **App Secret** (saved to macOS Keychain)
|
|
44
|
+
|
|
45
|
+
After installation, start opencode and you should see:
|
|
46
|
+
```
|
|
47
|
+
[chat-channel] 飞书机器人已启动(长连接模式),appId=cli_xxx***
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
> **Requirements**: `bun` and `curl` must be installed. macOS is required for Keychain-based secret storage.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### Manual Installation
|
|
55
|
+
|
|
56
|
+
1. **Copy the plugin file** to your opencode plugins directory:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
cp src/index.ts ~/.config/opencode/plugins/chat-channel.ts
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
2. **Add the dependency** to `~/.config/opencode/package.json`:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"@larksuiteoapi/node-sdk": "^1.37.0"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Then run `bun install` in `~/.config/opencode/`.
|
|
73
|
+
|
|
74
|
+
### Configuration
|
|
75
|
+
|
|
76
|
+
#### Step 1: Create a Feishu Self-Built App
|
|
77
|
+
|
|
78
|
+
1. Visit [Feishu Open Platform](https://open.feishu.cn/app) and create a **self-built app**
|
|
79
|
+
2. Note your **App ID** and **App Secret**
|
|
80
|
+
3. Under "Add App Capabilities", enable **Bot**
|
|
81
|
+
4. Under "Permissions", add:
|
|
82
|
+
- `im:message` (read/send messages)
|
|
83
|
+
- `im:message.group_at_msg` (receive group @ messages, optional)
|
|
84
|
+
5. Under "Event Subscriptions":
|
|
85
|
+
- Select **"Use long connection to receive events"** (no webhook URL needed)
|
|
86
|
+
- Add event: `Receive Message v2.0` (`im.message.receive_v1`)
|
|
87
|
+
6. Publish the app version
|
|
88
|
+
|
|
89
|
+
#### Step 2: Store Credentials
|
|
90
|
+
|
|
91
|
+
**App ID** (non-sensitive, store in `.env`):
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# ~/.config/opencode/.env
|
|
95
|
+
FEISHU_APP_ID=cli_xxxxxxxxxxxxxxxx
|
|
96
|
+
|
|
97
|
+
# Optional: custom opencode API URL (default: http://localhost:4321)
|
|
98
|
+
# OPENCODE_BASE_URL=http://localhost:4321
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**App Secret** (sensitive, store in macOS Keychain):
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
security add-generic-password -a chat-channel -s opencode-chat-channel -w <your-app-secret> -U
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Verify:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
security find-generic-password -a chat-channel -s opencode-chat-channel -w
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### Step 3: Start opencode
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
opencode
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
You should see in the logs:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
[chat-channel] 飞书机器人已启动(长连接模式),appId=cli_xxx***
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### File Structure
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
~/.config/opencode/
|
|
129
|
+
├── plugins/
|
|
130
|
+
│ └── chat-channel.ts # plugin file (copied from src/index.ts)
|
|
131
|
+
├── package.json # dependencies (add @larksuiteoapi/node-sdk)
|
|
132
|
+
└── .env # FEISHU_APP_ID (App Secret in Keychain)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### FAQ
|
|
136
|
+
|
|
137
|
+
**Q: Plugin starts but bot receives no messages**
|
|
138
|
+
- Ensure event subscription is set to "long connection", not "HTTP callback URL"
|
|
139
|
+
- Ensure the app version is published and online
|
|
140
|
+
- Verify App ID / Secret are correct
|
|
141
|
+
|
|
142
|
+
**Q: Replies are slow**
|
|
143
|
+
- AI processing takes time; complex tasks may need 10–30 seconds
|
|
144
|
+
- Simple conversations typically 3–8 seconds
|
|
145
|
+
|
|
146
|
+
**Q: How to reset conversation history**
|
|
147
|
+
- Restart opencode (sessions follow process lifecycle)
|
|
148
|
+
- Or wait 2 hours for session TTL to expire
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 中文说明
|
|
153
|
+
|
|
154
|
+
### 工作原理
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
飞书用户发消息
|
|
158
|
+
↓ WebSocket 长连接
|
|
159
|
+
飞书开放平台
|
|
160
|
+
↓ @larksuiteoapi/node-sdk WSClient
|
|
161
|
+
chat-channel 插件(本仓库)
|
|
162
|
+
↓ client.session.prompt()
|
|
163
|
+
opencode AI (Sisyphus)
|
|
164
|
+
↓ 回复文本
|
|
165
|
+
飞书用户
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- 每个飞书用户(`open_id`)独享一个 opencode session,**对话历史持久保留**
|
|
169
|
+
- session 闲置 2 小时后自动回收,下次对话开新 session
|
|
170
|
+
- AI 回复超过 4000 字时自动分段发送
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
### 一键安装(推荐)
|
|
175
|
+
|
|
176
|
+
在终端执行以下命令:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
curl -fsSL https://raw.githubusercontent.com/coneycode/opencode-chat-channel/main/install.sh | bash
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
安装程序会自动完成:
|
|
183
|
+
1. 下载插件文件到 `~/.config/opencode/plugins/chat-channel.ts`
|
|
184
|
+
2. 将 `@larksuiteoapi/node-sdk` 添加到 `~/.config/opencode/package.json`
|
|
185
|
+
3. 执行 `bun install` 安装依赖
|
|
186
|
+
4. 交互式询问你的 **App ID**(保存到 `.env`)和 **App Secret**(保存到 macOS 钥匙串)
|
|
187
|
+
|
|
188
|
+
安装完成后,启动 opencode 即可看到:
|
|
189
|
+
```
|
|
190
|
+
[chat-channel] 飞书机器人已启动(长连接模式),appId=cli_xxx***
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
> **前置条件**:需要已安装 `bun` 和 `curl`;App Secret 安全存储依赖 macOS 钥匙串。
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### 手动安装
|
|
198
|
+
### 安装
|
|
199
|
+
|
|
200
|
+
1. **复制插件文件** 到 opencode 插件目录:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
cp src/index.ts ~/.config/opencode/plugins/chat-channel.ts
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
2. **添加依赖** 到 `~/.config/opencode/package.json`:
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"dependencies": {
|
|
211
|
+
"@larksuiteoapi/node-sdk": "^1.37.0"
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
然后在 `~/.config/opencode/` 目录运行 `bun install`。
|
|
217
|
+
|
|
218
|
+
### 配置步骤
|
|
219
|
+
|
|
220
|
+
#### 第一步:创建飞书自建应用
|
|
221
|
+
|
|
222
|
+
1. 访问 [飞书开放平台](https://open.feishu.cn/app),新建**企业自建应用**
|
|
223
|
+
2. 记录 **App ID** 和 **App Secret**
|
|
224
|
+
3. 在「添加应用能力」中开启**机器人**
|
|
225
|
+
4. 在「权限管理」中添加以下权限:
|
|
226
|
+
- `im:message`(读取/发送消息)
|
|
227
|
+
- `im:message.group_at_msg`(接收群组 @ 消息,按需)
|
|
228
|
+
5. 在「事件订阅」中:
|
|
229
|
+
- 选择**使用长连接接收事件**(无需填写请求地址)
|
|
230
|
+
- 添加事件:`接收消息 v2.0`(即 `im.message.receive_v1`)
|
|
231
|
+
6. 发布应用版本
|
|
232
|
+
|
|
233
|
+
#### 第二步:写入凭证
|
|
234
|
+
|
|
235
|
+
**App ID**(非敏感,明文存 `.env`):
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
# ~/.config/opencode/.env
|
|
239
|
+
FEISHU_APP_ID=cli_xxxxxxxxxxxxxxxx
|
|
240
|
+
|
|
241
|
+
# 可选:自定义 opencode API 地址(默认: http://localhost:4321)
|
|
242
|
+
# OPENCODE_BASE_URL=http://localhost:4321
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**App Secret**(敏感,存 macOS Keychain,不落盘明文):
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
security add-generic-password -a chat-channel -s opencode-chat-channel -w <你的AppSecret> -U
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
验证写入是否成功:
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
security find-generic-password -a chat-channel -s opencode-chat-channel -w
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
#### 第三步:启动 opencode
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
opencode
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
启动后日志中会看到:
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
[chat-channel] 飞书机器人已启动(长连接模式),appId=cli_xxx***
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### 文件位置
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
~/.config/opencode/
|
|
273
|
+
├── plugins/
|
|
274
|
+
│ └── chat-channel.ts # 插件主文件(从 src/index.ts 复制)
|
|
275
|
+
├── package.json # 依赖(含 @larksuiteoapi/node-sdk)
|
|
276
|
+
└── .env # 环境变量(FEISHU_APP_ID;App Secret 存 Keychain)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### 常见问题
|
|
280
|
+
|
|
281
|
+
**Q: 插件启动后收不到消息**
|
|
282
|
+
- 确认飞书应用事件订阅选择的是「长连接」而非「HTTP 请求地址」
|
|
283
|
+
- 确认应用已发布且版本已上线
|
|
284
|
+
- 确认 App ID/Secret 正确
|
|
285
|
+
|
|
286
|
+
**Q: 回复很慢**
|
|
287
|
+
- AI 处理需要时间,复杂问题可能需要 10-30 秒
|
|
288
|
+
- 简单对话通常 3-8 秒
|
|
289
|
+
|
|
290
|
+
**Q: 想重置对话历史**
|
|
291
|
+
- 关闭 opencode 重启即可(session 随进程生命周期)
|
|
292
|
+
- 或等待 2 小时 session 超时自动重置
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## License
|
|
297
|
+
|
|
298
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode-chat-channel — opencode 飞书机器人插件
|
|
3
|
+
*
|
|
4
|
+
* 通过飞书长连接(WebSocket)接收消息,驱动 opencode AI 回复。
|
|
5
|
+
*
|
|
6
|
+
* 凭证存储方案(macOS Keychain):
|
|
7
|
+
* FEISHU_APP_ID 存放在 ~/.config/opencode/.env(非敏感,可见)
|
|
8
|
+
* FEISHU_APP_SECRET 存放在系统钥匙串,service=opencode-chat-channel,account=chat-channel
|
|
9
|
+
* 写入命令:
|
|
10
|
+
* security add-generic-password -a chat-channel -s opencode-chat-channel -w <secret> -U
|
|
11
|
+
*
|
|
12
|
+
* 每个飞书用户(open_id)独享一个 opencode session,对话历史保留。
|
|
13
|
+
*/
|
|
14
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
15
|
+
export declare const ChatChannelPlugin: Plugin;
|
|
16
|
+
export default ChatChannelPlugin;
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAgDlD,eAAO,MAAM,iBAAiB,EAAE,MAsR/B,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
3
|
+
import { execFileSync } from "child_process";
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
function loadDotEnv(envPath) {
|
|
7
|
+
try {
|
|
8
|
+
const content = readFileSync(envPath, "utf8");
|
|
9
|
+
for (const line of content.split(`
|
|
10
|
+
`)) {
|
|
11
|
+
const trimmed = line.trim();
|
|
12
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
13
|
+
continue;
|
|
14
|
+
const eqIdx = trimmed.indexOf("=");
|
|
15
|
+
if (eqIdx < 1)
|
|
16
|
+
continue;
|
|
17
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
18
|
+
const value = trimmed.slice(eqIdx + 1).trim();
|
|
19
|
+
if (!process.env[key]) {
|
|
20
|
+
process.env[key] = value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
var SESSION_TTL_MS = 2 * 60 * 60 * 1000;
|
|
26
|
+
var OPENCODE_BASE_URL = process.env["OPENCODE_BASE_URL"] ?? "http://localhost:4321";
|
|
27
|
+
var ChatChannelPlugin = async ({ client }) => {
|
|
28
|
+
const configDir = join(process.env["HOME"] ?? "/Users/" + (process.env["USER"] ?? "unknown"), ".config", "opencode");
|
|
29
|
+
loadDotEnv(join(configDir, ".env"));
|
|
30
|
+
const appId = process.env["FEISHU_APP_ID"];
|
|
31
|
+
let appSecret;
|
|
32
|
+
try {
|
|
33
|
+
const stdout = execFileSync("security", [
|
|
34
|
+
"find-generic-password",
|
|
35
|
+
"-a",
|
|
36
|
+
"chat-channel",
|
|
37
|
+
"-s",
|
|
38
|
+
"opencode-chat-channel",
|
|
39
|
+
"-w"
|
|
40
|
+
], { encoding: "utf8" });
|
|
41
|
+
appSecret = stdout.trim() || undefined;
|
|
42
|
+
} catch {
|
|
43
|
+
appSecret = undefined;
|
|
44
|
+
}
|
|
45
|
+
if (!appId || !appSecret) {
|
|
46
|
+
await client.app.log({
|
|
47
|
+
body: {
|
|
48
|
+
service: "chat-channel",
|
|
49
|
+
level: "warn",
|
|
50
|
+
message: !appId ? "FEISHU_APP_ID 未设置(请在 .env 中添加),飞书机器人插件已跳过" : "FEISHU_APP_SECRET 未找到(请写入 Keychain),飞书机器人插件已跳过"
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
const larkClient = new Lark.Client({
|
|
56
|
+
appId,
|
|
57
|
+
appSecret,
|
|
58
|
+
appType: Lark.AppType.SelfBuild,
|
|
59
|
+
domain: Lark.Domain.Feishu
|
|
60
|
+
});
|
|
61
|
+
const userSessions = new Map;
|
|
62
|
+
const processedMessages = new Set;
|
|
63
|
+
const MAX_PROCESSED = 500;
|
|
64
|
+
async function getOrCreateSession(openId) {
|
|
65
|
+
const existing = userSessions.get(openId);
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
if (existing && now - existing.lastActivity < SESSION_TTL_MS) {
|
|
68
|
+
existing.lastActivity = now;
|
|
69
|
+
return existing.sessionId;
|
|
70
|
+
}
|
|
71
|
+
const res = await client.session.create({
|
|
72
|
+
body: { title: `飞书对话 · ${openId}` }
|
|
73
|
+
});
|
|
74
|
+
const sessionId = res.data.id;
|
|
75
|
+
userSessions.set(openId, { sessionId, lastActivity: now });
|
|
76
|
+
await client.app.log({
|
|
77
|
+
body: {
|
|
78
|
+
service: "chat-channel",
|
|
79
|
+
level: "info",
|
|
80
|
+
message: `创建新 session: ${sessionId}`,
|
|
81
|
+
extra: { openId }
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return sessionId;
|
|
85
|
+
}
|
|
86
|
+
function cleanupExpiredSessions() {
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
for (const [openId, meta] of userSessions.entries()) {
|
|
89
|
+
if (now - meta.lastActivity > SESSION_TTL_MS) {
|
|
90
|
+
userSessions.delete(openId);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
setInterval(cleanupExpiredSessions, 30 * 60 * 1000);
|
|
95
|
+
async function replyToFeishu(chatId, text) {
|
|
96
|
+
const MAX_LEN = 4000;
|
|
97
|
+
const chunks = [];
|
|
98
|
+
for (let i = 0;i < text.length; i += MAX_LEN) {
|
|
99
|
+
chunks.push(text.slice(i, i + MAX_LEN));
|
|
100
|
+
}
|
|
101
|
+
for (const chunk of chunks) {
|
|
102
|
+
await larkClient.im.message.create({
|
|
103
|
+
params: { receive_id_type: "chat_id" },
|
|
104
|
+
data: {
|
|
105
|
+
receive_id: chatId,
|
|
106
|
+
content: JSON.stringify({ text: chunk }),
|
|
107
|
+
msg_type: "text"
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function sendThinking(chatId) {
|
|
113
|
+
await larkClient.im.message.create({
|
|
114
|
+
params: { receive_id_type: "chat_id" },
|
|
115
|
+
data: {
|
|
116
|
+
receive_id: chatId,
|
|
117
|
+
content: JSON.stringify({ text: "⏳ 正在思考..." }),
|
|
118
|
+
msg_type: "text"
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function extractResponseText(parts) {
|
|
123
|
+
if (!Array.isArray(parts))
|
|
124
|
+
return "";
|
|
125
|
+
return parts.filter((p) => p?.type === "text").map((p) => p?.text ?? "").join("").trim();
|
|
126
|
+
}
|
|
127
|
+
async function handleMessage(data) {
|
|
128
|
+
const { message, sender } = data;
|
|
129
|
+
if (message.message_type !== "text") {
|
|
130
|
+
await larkClient.im.message.create({
|
|
131
|
+
params: { receive_id_type: "chat_id" },
|
|
132
|
+
data: {
|
|
133
|
+
receive_id: message.chat_id,
|
|
134
|
+
content: JSON.stringify({ text: "暂时只支持文本消息 \uD83D\uDE0A" }),
|
|
135
|
+
msg_type: "text"
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const msgId = message.message_id;
|
|
141
|
+
if (processedMessages.has(msgId)) {
|
|
142
|
+
await client.app.log({
|
|
143
|
+
body: {
|
|
144
|
+
service: "chat-channel",
|
|
145
|
+
level: "info",
|
|
146
|
+
message: `重复消息已忽略: ${msgId}`
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (processedMessages.size >= MAX_PROCESSED)
|
|
152
|
+
processedMessages.clear();
|
|
153
|
+
processedMessages.add(msgId);
|
|
154
|
+
let userText;
|
|
155
|
+
try {
|
|
156
|
+
userText = JSON.parse(message.content).text?.trim();
|
|
157
|
+
} catch {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (!userText)
|
|
161
|
+
return;
|
|
162
|
+
const openId = sender.sender_id?.open_id ?? message.chat_id;
|
|
163
|
+
const chatId = message.chat_id;
|
|
164
|
+
await client.app.log({
|
|
165
|
+
body: {
|
|
166
|
+
service: "chat-channel",
|
|
167
|
+
level: "info",
|
|
168
|
+
message: `收到消息: "${userText.slice(0, 80)}${userText.length > 80 ? "..." : ""}"`,
|
|
169
|
+
extra: { openId, chatId }
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
await sendThinking(chatId);
|
|
173
|
+
let responseText = null;
|
|
174
|
+
try {
|
|
175
|
+
const sessionId = await getOrCreateSession(openId);
|
|
176
|
+
const result = await client.session.prompt({
|
|
177
|
+
path: { id: sessionId },
|
|
178
|
+
body: {
|
|
179
|
+
parts: [{ type: "text", text: userText }],
|
|
180
|
+
model: {
|
|
181
|
+
providerID: "Mify-Anthropic",
|
|
182
|
+
modelID: "ppio/pa/claude-sonnet-4-6"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
responseText = extractResponseText(result.data?.parts ?? []);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
const errorMsg = err?.data?.message ?? err?.message ?? String(err);
|
|
189
|
+
await client.app.log({
|
|
190
|
+
body: {
|
|
191
|
+
service: "chat-channel",
|
|
192
|
+
level: "error",
|
|
193
|
+
message: `处理消息失败: ${errorMsg}`,
|
|
194
|
+
extra: { openId }
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
await replyToFeishu(chatId, `⚠️ 出错了:${errorMsg}`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (responseText) {
|
|
201
|
+
await replyToFeishu(chatId, responseText);
|
|
202
|
+
} else {
|
|
203
|
+
await replyToFeishu(chatId, "(AI 没有返回文字回复)");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const wsClient = new Lark.WSClient({
|
|
207
|
+
appId,
|
|
208
|
+
appSecret,
|
|
209
|
+
loggerLevel: Lark.LoggerLevel.warn
|
|
210
|
+
});
|
|
211
|
+
wsClient.start({
|
|
212
|
+
eventDispatcher: new Lark.EventDispatcher({}).register({
|
|
213
|
+
"im.message.receive_v1": handleMessage
|
|
214
|
+
})
|
|
215
|
+
});
|
|
216
|
+
await client.app.log({
|
|
217
|
+
body: {
|
|
218
|
+
service: "chat-channel",
|
|
219
|
+
level: "info",
|
|
220
|
+
message: `飞书机器人已启动(长连接模式),appId=${appId.slice(0, 8)}***`
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
return {
|
|
224
|
+
event: async ({ event }) => {
|
|
225
|
+
if (event.type === "session.error") {
|
|
226
|
+
await client.app.log({
|
|
227
|
+
body: {
|
|
228
|
+
service: "chat-channel",
|
|
229
|
+
level: "warn",
|
|
230
|
+
message: "opencode session 出现错误",
|
|
231
|
+
extra: event.properties
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
var src_default = ChatChannelPlugin;
|
|
239
|
+
export {
|
|
240
|
+
src_default as default,
|
|
241
|
+
ChatChannelPlugin
|
|
242
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-chat-channel",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "opencode plugin — Feishu (Lark) bot channel via WebSocket long connection",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "bun run build:clean && bun run build:js && bun run build:types",
|
|
21
|
+
"build:clean": "rm -rf dist",
|
|
22
|
+
"build:js": "bun build ./src/index.ts --outdir ./dist --target node --format esm --packages external",
|
|
23
|
+
"build:types": "bunx tsc -p tsconfig.build.json",
|
|
24
|
+
"prepublishOnly": "bun run build",
|
|
25
|
+
"prepack": "bun run build"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"opencode",
|
|
29
|
+
"opencode-plugin",
|
|
30
|
+
"feishu",
|
|
31
|
+
"lark",
|
|
32
|
+
"bot",
|
|
33
|
+
"websocket"
|
|
34
|
+
],
|
|
35
|
+
"author": "",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@larksuiteoapi/node-sdk": "^1.37.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@opencode-ai/plugin": "*"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@opencode-ai/plugin": "*",
|
|
45
|
+
"typescript": "^5.0.0"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"bun": ">=1.2.0"
|
|
49
|
+
}
|
|
50
|
+
}
|