opencode-chat-channel 1.0.0 → 1.2.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 CHANGED
@@ -1,6 +1,9 @@
1
1
  # opencode-chat-channel
2
2
 
3
- An [opencode](https://opencode.ai) plugin that connects your Feishu (Lark) bot to opencode via **WebSocket long connection** — no public IP required.
3
+ An [opencode](https://opencode.ai) plugin that connects instant messaging bots to opencode AI via a unified multi-channel architecture.
4
+
5
+ **Currently supported**: Feishu (Lark) via WebSocket long connection — no public IP required.
6
+ **Skeleton ready**: WeCom (企业微信) — see `src/channels/wecom/index.ts`.
4
7
 
5
8
  [中文说明](#中文说明) · [English](#english)
6
9
 
@@ -11,67 +14,65 @@ An [opencode](https://opencode.ai) plugin that connects your Feishu (Lark) bot t
11
14
  ### How It Works
12
15
 
13
16
  ```
14
- Feishu User sends message
15
- ↓ WebSocket long connection
16
- Feishu Open Platform
17
- ↓ @larksuiteoapi/node-sdk WSClient
17
+ User sends message (Feishu / WeCom / ...)
18
+ Channel adapter (WebSocket / HTTP callback / ...)
18
19
  chat-channel plugin (this repo)
19
20
  ↓ client.session.prompt()
20
21
  opencode AI (Sisyphus)
21
22
  ↓ text reply
22
- Feishu User receives reply
23
+ User receives reply
23
24
  ```
24
25
 
25
- - Each Feishu user (`open_id`) gets their own persistent opencode session
26
+ - Each user (`open_id` / user ID) gets their own persistent opencode session
26
27
  - Sessions expire after 2 hours of inactivity (auto-cleanup)
27
- - Replies longer than 4000 characters are automatically split into multiple messages
28
+ - Replies longer than 4000 characters are automatically split
28
29
 
29
30
  ---
30
31
 
31
- ### Quick Install (Recommended)
32
+ ### Installation
32
33
 
33
- Run this single command in your terminal:
34
+ Since this is an **opencode plugin**, opencode handles installation automatically via npm. No scripts needed.
34
35
 
35
- ```bash
36
- curl -fsSL https://raw.githubusercontent.com/coneycode/opencode-chat-channel/main/install.sh | bash
37
- ```
36
+ #### Step 1: Add to `opencode.json`
38
37
 
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)
38
+ Edit `~/.config/opencode/opencode.json` and add the plugin:
44
39
 
45
- After installation, start opencode and you should see:
46
- ```
47
- [chat-channel] 飞书机器人已启动(长连接模式),appId=cli_xxx***
40
+ ```json
41
+ {
42
+ "plugin": [
43
+ "opencode-chat-channel@latest"
44
+ ]
45
+ }
48
46
  ```
49
47
 
50
- > **Requirements**: `bun` and `curl` must be installed. macOS is required for Keychain-based secret storage.
51
-
52
- ---
48
+ opencode will pull and install the package automatically on next startup.
53
49
 
54
- ### Manual Installation
50
+ #### Step 2: Select channels to enable
55
51
 
56
- 1. **Copy the plugin file** to your opencode plugins directory:
52
+ Add `CHAT_CHANNELS` to `~/.config/opencode/.env` to control which channels are active:
57
53
 
58
54
  ```bash
59
- cp src/index.ts ~/.config/opencode/plugins/chat-channel.ts
60
- ```
55
+ # ~/.config/opencode/.env
61
56
 
62
- 2. **Add the dependency** to `~/.config/opencode/package.json`:
57
+ # Enable Feishu only (recommended to start)
58
+ CHAT_CHANNELS=feishu
63
59
 
64
- ```json
65
- {
66
- "dependencies": {
67
- "@larksuiteoapi/node-sdk": "^1.37.0"
68
- }
69
- }
60
+ # Enable multiple channels at once
61
+ # CHAT_CHANNELS=feishu,wecom
62
+
63
+ # Leave unset or empty = auto mode: any channel with valid credentials starts automatically
64
+ # CHAT_CHANNELS=
70
65
  ```
71
66
 
72
- Then run `bun install` in `~/.config/opencode/`.
67
+ Then configure credentials for each enabled channel (see sections below).
68
+
69
+ #### Step 3: Configure channel credentials
70
+
71
+ See the [Feishu Configuration](#feishu-configuration) section below.
73
72
 
74
- ### Configuration
73
+ ---
74
+
75
+ ### Feishu Configuration
75
76
 
76
77
  #### Step 1: Create a Feishu Self-Built App
77
78
 
@@ -98,13 +99,21 @@ FEISHU_APP_ID=cli_xxxxxxxxxxxxxxxx
98
99
  # OPENCODE_BASE_URL=http://localhost:4321
99
100
  ```
100
101
 
101
- **App Secret** (sensitive, store in macOS Keychain):
102
+ **App Secret** choose the method for your platform:
102
103
 
103
- ```bash
104
- security add-generic-password -a chat-channel -s opencode-chat-channel -w <your-app-secret> -U
105
- ```
104
+ | Platform | Method | Command |
105
+ |----------|--------|---------|
106
+ | macOS | Keychain (recommended) | `security add-generic-password -a chat-channel -s opencode-chat-channel -w <secret> -U` |
107
+ | Windows / Linux | `.env` file | Add `FEISHU_APP_SECRET=<secret>` to `~/.config/opencode/.env` |
108
+ | All platforms | Environment variable | Set `FEISHU_APP_SECRET=<secret>` before launching opencode |
109
+
110
+ > **Priority**: environment variable → macOS Keychain → `.env` file value (already loaded as env var).
111
+ > The plugin tries each in order and uses the first one found.
106
112
 
107
- Verify:
113
+ > ⚠️ If you store the secret in `.env`, ensure the file has restricted permissions:
114
+ > `chmod 600 ~/.config/opencode/.env`
115
+
116
+ **macOS Keychain** (verify):
108
117
 
109
118
  ```bash
110
119
  security find-generic-password -a chat-channel -s opencode-chat-channel -w
@@ -119,19 +128,47 @@ opencode
119
128
  You should see in the logs:
120
129
 
121
130
  ```
122
- [chat-channel] 飞书机器人已启动(长连接模式),appId=cli_xxx***
131
+ [feishu] 飞书机器人已启动(长连接模式),appId=cli_xxx***
132
+ chat-channel 已启动,活跃渠道: feishu
123
133
  ```
124
134
 
125
- ### File Structure
135
+ ---
136
+
137
+ ### Adding a New Channel
138
+
139
+ The plugin uses a `ChatChannel` interface. To add a new channel:
126
140
 
141
+ 1. Create `src/channels/<name>/index.ts` implementing `ChatChannel` and exporting a `ChannelFactory`
142
+ 2. Register the factory in `src/index.ts` → `CHANNEL_FACTORIES` array
143
+
144
+ ```typescript
145
+ // src/channels/myapp/index.ts
146
+ import type { ChatChannel, ChannelFactory } from "../../types.js";
147
+
148
+ class MyAppChannel implements ChatChannel {
149
+ readonly name = "myapp";
150
+ async start(onMessage) { /* connect, call onMessage on each msg */ }
151
+ async send(target, text) { /* send reply */ }
152
+ }
153
+
154
+ export const myappChannelFactory: ChannelFactory = async (client) => {
155
+ // read credentials, return null if not configured
156
+ return new MyAppChannel(...);
157
+ };
127
158
  ```
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)
159
+
160
+ ```typescript
161
+ // src/index.ts add to CHANNEL_FACTORIES
162
+ import { myappChannelFactory } from "./channels/myapp/index.js";
163
+
164
+ const CHANNEL_FACTORIES: ChannelFactory[] = [
165
+ feishuChannelFactory,
166
+ myappChannelFactory, // ← add here
167
+ ];
133
168
  ```
134
169
 
170
+ ---
171
+
135
172
  ### FAQ
136
173
 
137
174
  **Q: Plugin starts but bot receives no messages**
@@ -154,68 +191,65 @@ You should see in the logs:
154
191
  ### 工作原理
155
192
 
156
193
  ```
157
- 飞书用户发消息
158
- ↓ WebSocket 长连接
159
- 飞书开放平台
160
- ↓ @larksuiteoapi/node-sdk WSClient
194
+ 用户发消息(飞书 / 企业微信 / ...)
195
+ 渠道适配器(WebSocket 长连接 / HTTP 回调 / ...)
161
196
  chat-channel 插件(本仓库)
162
197
  ↓ client.session.prompt()
163
198
  opencode AI (Sisyphus)
164
199
  ↓ 回复文本
165
- 飞书用户
200
+ 用户收到回复
166
201
  ```
167
202
 
168
- - 每个飞书用户(`open_id`)独享一个 opencode session,**对话历史持久保留**
203
+ - 每个用户独享一个 opencode session,**对话历史持久保留**
169
204
  - session 闲置 2 小时后自动回收,下次对话开新 session
170
205
  - AI 回复超过 4000 字时自动分段发送
171
206
 
172
207
  ---
173
208
 
174
- ### 一键安装(推荐)
209
+ ### 安装
175
210
 
176
- 在终端执行以下命令:
211
+ 本项目是 **opencode 插件**,opencode 通过 npm 自动管理安装,无需额外脚本。
177
212
 
178
- ```bash
179
- curl -fsSL https://raw.githubusercontent.com/coneycode/opencode-chat-channel/main/install.sh | bash
180
- ```
213
+ #### 第一步:添加到 `opencode.json`
181
214
 
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 钥匙串)
215
+ 编辑 `~/.config/opencode/opencode.json`,在 `plugin` 数组中添加:
187
216
 
188
- 安装完成后,启动 opencode 即可看到:
189
- ```
190
- [chat-channel] 飞书机器人已启动(长连接模式),appId=cli_xxx***
217
+ ```json
218
+ {
219
+ "plugin": [
220
+ "opencode-chat-channel@latest"
221
+ ]
222
+ }
191
223
  ```
192
224
 
193
- > **前置条件**:需要已安装 `bun` 和 `curl`;App Secret 安全存储依赖 macOS 钥匙串。
225
+ 下次启动 opencode 时会自动拉取并安装。
194
226
 
195
- ---
227
+ #### 第二步:选择要启用的渠道
196
228
 
197
- ### 手动安装
198
- ### 安装
199
-
200
- 1. **复制插件文件** 到 opencode 插件目录:
229
+ `~/.config/opencode/.env` 中配置 `CHAT_CHANNELS`,指定要开启哪些渠道:
201
230
 
202
231
  ```bash
203
- cp src/index.ts ~/.config/opencode/plugins/chat-channel.ts
204
- ```
232
+ # ~/.config/opencode/.env
205
233
 
206
- 2. **添加依赖** 到 `~/.config/opencode/package.json`:
234
+ # 只开启飞书(初始推荐)
235
+ CHAT_CHANNELS=feishu
207
236
 
208
- ```json
209
- {
210
- "dependencies": {
211
- "@larksuiteoapi/node-sdk": "^1.37.0"
212
- }
213
- }
237
+ # 同时开启多个渠道
238
+ # CHAT_CHANNELS=feishu,wecom
239
+
240
+ # 留空或不配置 = 自动模式:凭证存在的渠道自动启用
241
+ # CHAT_CHANNELS=
214
242
  ```
215
243
 
216
- 然后在 `~/.config/opencode/` 目录运行 `bun install`。
244
+ 然后配置对应渠道的凭证(见下方各渠道配置说明)。
245
+
246
+ #### 第三步:配置渠道凭证
217
247
 
218
- ### 配置步骤
248
+ 参见下方各渠道的配置说明。
249
+
250
+ ---
251
+
252
+ ### 飞书配置
219
253
 
220
254
  #### 第一步:创建飞书自建应用
221
255
 
@@ -238,17 +272,25 @@ cp src/index.ts ~/.config/opencode/plugins/chat-channel.ts
238
272
  # ~/.config/opencode/.env
239
273
  FEISHU_APP_ID=cli_xxxxxxxxxxxxxxxx
240
274
 
241
- # 可选:自定义 opencode API 地址(默认: http://localhost:4321)
275
+ # 可选:自定义 opencode API 地址(默认:http://localhost:4321)
242
276
  # OPENCODE_BASE_URL=http://localhost:4321
243
277
  ```
244
278
 
245
- **App Secret**(敏感,存 macOS Keychain,不落盘明文):
279
+ **App Secret**—按使用的平台选择存储方式:
246
280
 
247
- ```bash
248
- security add-generic-password -a chat-channel -s opencode-chat-channel -w <你的AppSecret> -U
249
- ```
281
+ | 平台 | 方式 | 命令 |
282
+ |------|------|------|
283
+ | macOS | 钒匙串(推荐,不落盘明文) | `security add-generic-password -a chat-channel -s opencode-chat-channel -w <secret> -U` |
284
+ | Windows / Linux | 写入 `.env` 文件 | 在 `~/.config/opencode/.env` 中添加 `FEISHU_APP_SECRET=<secret>` |
285
+ | 所有平台 | 环境变量 | 启动 opencode 前设置 `FEISHU_APP_SECRET=<secret>` |
250
286
 
251
- 验证写入是否成功:
287
+ > **读取优先级**:环境变量 → macOS 钒匙串 → `.env` 文件(已在插件启动时自动读入环境变量)。
288
+ > 插件依次尝试,找到第一个有效值即停止。
289
+
290
+ > ⚠️ 如果将 Secret 写入 `.env`,建议限制文件权限:
291
+ > `chmod 600 ~/.config/opencode/.env`
292
+
293
+ **macOS 钒匙串**验证:
252
294
 
253
295
  ```bash
254
296
  security find-generic-password -a chat-channel -s opencode-chat-channel -w
@@ -263,19 +305,39 @@ opencode
263
305
  启动后日志中会看到:
264
306
 
265
307
  ```
266
- [chat-channel] 飞书机器人已启动(长连接模式),appId=cli_xxx***
308
+ [feishu] 飞书机器人已启动(长连接模式),appId=cli_xxx***
309
+ chat-channel 已启动,活跃渠道: feishu
267
310
  ```
268
311
 
269
- ### 文件位置
312
+ ---
313
+
314
+ ### 接入新渠道
315
+
316
+ 插件基于 `ChatChannel` 接口设计,新增渠道步骤:
317
+
318
+ 1. 新建 `src/channels/<渠道名>/index.ts`,实现 `ChatChannel` 接口并导出 `ChannelFactory`
319
+ 2. 在 `src/index.ts` 的 `CHANNEL_FACTORIES` 数组中注册工厂函数
320
+
321
+ 参见 `src/channels/wecom/index.ts` — 企业微信骨架,含详细的实现 TODO 注释。
322
+
323
+ ---
324
+
325
+ ### 项目结构
270
326
 
271
327
  ```
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)
328
+ src/
329
+ ├── index.ts # 插件入口,注册所有渠道
330
+ ├── types.ts # ChatChannel 接口 & 公共类型
331
+ ├── session-manager.ts # opencode session 管理 & 工具函数
332
+ └── channels/
333
+ ├── feishu/
334
+ │ └── index.ts # 飞书渠道实现(已完成)
335
+ └── wecom/
336
+ └── index.ts # 企业微信渠道骨架(待实现)
277
337
  ```
278
338
 
339
+ ---
340
+
279
341
  ### 常见问题
280
342
 
281
343
  **Q: 插件启动后收不到消息**
@@ -0,0 +1,21 @@
1
+ /**
2
+ * opencode-chat-channel — 飞书渠道适配器
3
+ *
4
+ * 通过飞书长连接(WebSocket)接收消息,实现 ChatChannel 接口。
5
+ *
6
+ * 凭证读取优先级(FEISHU_APP_SECRET):
7
+ * 1. 环境变量 FEISHU_APP_SECRET(所有平台,最高优先)
8
+ * 2. macOS Keychain(仅 macOS,自动尝试)
9
+ * service=opencode-chat-channel,account=chat-channel
10
+ * 写入:security add-generic-password -a chat-channel -s opencode-chat-channel -w <secret> -U
11
+ * 3. ~/.config/opencode/.env 中的 FEISHU_APP_SECRET(Windows/Linux 推荐)
12
+ *
13
+ * FEISHU_APP_ID 始终从 .env 文件读取(非敏感)。
14
+ */
15
+ import type { ChannelFactory } from "../../types.js";
16
+ /**
17
+ * 飞书渠道工厂函数。
18
+ * 凭证不完整时记录警告日志并返回 null(插件跳过该渠道)。
19
+ */
20
+ export declare const feishuChannelFactory: ChannelFactory;
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/channels/feishu/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,KAAK,EAAe,cAAc,EAAiC,MAAM,gBAAgB,CAAC;AAyKjG;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,cAuBlC,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * opencode-chat-channel — 企业微信渠道适配器(骨架)
3
+ *
4
+ * 企业微信接入方式:
5
+ * - 自建应用通过"企业微信回调"(HTTP Webhook)接收消息
6
+ * - 或通过"接收消息" API(需要公网地址或内网穿透)
7
+ *
8
+ * 凭证配置(待实现时填充):
9
+ * WECOM_CORP_ID 企业 ID,存放在 ~/.config/opencode/.env
10
+ * WECOM_AGENT_ID 应用 AgentId,存放在 ~/.config/opencode/.env
11
+ * WECOM_SECRET 应用 Secret,存 macOS Keychain:
12
+ * security add-generic-password -a chat-channel-wecom -s opencode-chat-channel -w <secret> -U
13
+ * WECOM_TOKEN 企业微信回调 Token,存放在 ~/.config/opencode/.env
14
+ * WECOM_ENCODING_AES_KEY 企业微信消息加解密 Key,存 macOS Keychain:
15
+ * security add-generic-password -a chat-channel-wecom-aes -s opencode-chat-channel -w <key> -U
16
+ *
17
+ * TODO: 完整实现
18
+ * 1. 启动 HTTP 服务器接收企业微信回调(验证 Token、解密消息体)
19
+ * 2. 调用企业微信 API 发送文本消息
20
+ * 3. 支持消息加解密(EnterpriseWeChatCrypto)
21
+ *
22
+ * 参考文档:
23
+ * https://developer.work.weixin.qq.com/document/path/90238
24
+ * https://developer.work.weixin.qq.com/document/path/90236
25
+ */
26
+ import type { ChannelFactory } from "../../types.js";
27
+ /**
28
+ * 企业微信渠道工厂函数。
29
+ *
30
+ * 读取 WECOM_CORP_ID / WECOM_AGENT_ID 环境变量,
31
+ * 以及 Keychain 中的 WECOM_SECRET。
32
+ * 任意凭证缺失时返回 null,插件跳过该渠道。
33
+ *
34
+ * @todo 待企业微信实现完成后启用此工厂函数
35
+ */
36
+ export declare const wecomChannelFactory: ChannelFactory;
37
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/channels/wecom/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAe,cAAc,EAAiC,MAAM,gBAAgB,CAAC;AA4FjG;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,EAAE,cAuBjC,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,17 +1,21 @@
1
1
  /**
2
- * opencode-chat-channel — opencode 飞书机器人插件
2
+ * opencode-chat-channel — opencode 多渠道机器人插件
3
3
  *
4
- * 通过飞书长连接(WebSocket)接收消息,驱动 opencode AI 回复。
4
+ * 通过 .env 文件中的 CHAT_CHANNELS 配置项选择启用哪些渠道:
5
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
6
+ * CHAT_CHANNELS=feishu # 只启用飞书
7
+ * CHAT_CHANNELS=feishu,wecom # 同时启用飞书和企业微信
8
+ * CHAT_CHANNELS= # 留空:自动探测(凭证存在即启用)
9
+ * # 不配置此项:同留空,自动探测
11
10
  *
12
- * 每个飞书用户(open_id)独享一个 opencode session,对话历史保留。
11
+ * 其他配置项:
12
+ * OPENCODE_BASE_URL opencode API 地址(默认 http://localhost:4321)
13
+ *
14
+ * 各渠道的凭证配置详见 README。
13
15
  */
14
16
  import type { Plugin } from "@opencode-ai/plugin";
15
17
  export declare const ChatChannelPlugin: Plugin;
16
18
  export default ChatChannelPlugin;
19
+ export type { ChatChannel, ChannelFactory, ChannelName, IncomingMessage, PluginClient } from "./types.js";
20
+ export { SessionManager, extractResponseText, loadDotEnv } from "./session-manager.js";
17
21
  //# sourceMappingURL=index.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAgIlD,eAAO,MAAM,iBAAiB,EAAE,MAyF/B,CAAC;AAEF,eAAe,iBAAiB,CAAC;AAGjC,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1G,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -1,8 +1,59 @@
1
1
  // src/index.ts
2
- import * as Lark from "@larksuiteoapi/node-sdk";
3
- import { execFileSync } from "child_process";
4
- import { readFileSync } from "fs";
5
2
  import { join } from "path";
3
+
4
+ // src/session-manager.ts
5
+ import { readFileSync } from "fs";
6
+ var SESSION_TTL_MS = 2 * 60 * 60 * 1000;
7
+
8
+ class SessionManager {
9
+ client;
10
+ channel;
11
+ titleFn;
12
+ constructor(client, channel, titleFn = (id) => `${channel} · ${id}`) {
13
+ this.client = client;
14
+ this.channel = channel;
15
+ this.titleFn = titleFn;
16
+ }
17
+ sessions = new Map;
18
+ async getOrCreate(userId) {
19
+ const existing = this.sessions.get(userId);
20
+ const now = Date.now();
21
+ if (existing && now - existing.lastActivity < SESSION_TTL_MS) {
22
+ existing.lastActivity = now;
23
+ return existing.sessionId;
24
+ }
25
+ const res = await this.client.session.create({
26
+ body: { title: this.titleFn(userId) }
27
+ });
28
+ const sessionId = res.data.id;
29
+ this.sessions.set(userId, { sessionId, lastActivity: now });
30
+ await this.client.app.log({
31
+ body: {
32
+ service: "chat-channel",
33
+ level: "info",
34
+ message: `[${this.channel}] 创建新 session: ${sessionId}`,
35
+ extra: { userId }
36
+ }
37
+ });
38
+ return sessionId;
39
+ }
40
+ cleanup() {
41
+ const now = Date.now();
42
+ for (const [userId, meta] of this.sessions.entries()) {
43
+ if (now - meta.lastActivity > SESSION_TTL_MS) {
44
+ this.sessions.delete(userId);
45
+ }
46
+ }
47
+ }
48
+ startAutoCleanup(intervalMs = 30 * 60 * 1000) {
49
+ return setInterval(() => this.cleanup(), intervalMs);
50
+ }
51
+ }
52
+ function extractResponseText(parts) {
53
+ if (!Array.isArray(parts))
54
+ return "";
55
+ return parts.filter((p) => p?.type === "text").map((p) => p?.text ?? "").join("").trim();
56
+ }
6
57
  function loadDotEnv(envPath) {
7
58
  try {
8
59
  const content = readFileSync(envPath, "utf8");
@@ -22,95 +73,84 @@ function loadDotEnv(envPath) {
22
73
  }
23
74
  } catch {}
24
75
  }
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;
76
+
77
+ // src/channels/feishu/index.ts
78
+ import * as Lark from "@larksuiteoapi/node-sdk";
79
+ import { execFileSync } from "child_process";
80
+ var MAX_MSG_LEN = 4000;
81
+ var MAX_PROCESSED = 500;
82
+ function readAppSecret() {
83
+ if (process.env["FEISHU_APP_SECRET"]) {
84
+ return process.env["FEISHU_APP_SECRET"];
44
85
  }
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
- }
86
+ if (process.platform === "darwin") {
87
+ try {
88
+ const stdout = execFileSync("security", ["find-generic-password", "-a", "chat-channel", "-s", "opencode-chat-channel", "-w"], { encoding: "utf8" });
89
+ return stdout.trim() || undefined;
90
+ } catch {}
91
+ }
92
+ return;
93
+ }
94
+
95
+ class FeishuChannel {
96
+ appId;
97
+ appSecret;
98
+ client;
99
+ name = "feishu";
100
+ larkClient;
101
+ processedMessages = new Set;
102
+ constructor(appId, appSecret, client) {
103
+ this.appId = appId;
104
+ this.appSecret = appSecret;
105
+ this.client = client;
106
+ this.larkClient = new Lark.Client({
107
+ appId,
108
+ appSecret,
109
+ appType: Lark.AppType.SelfBuild,
110
+ domain: Lark.Domain.Feishu
52
111
  });
53
- return {};
54
112
  }
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}` }
113
+ async start(onMessage) {
114
+ const wsClient = new Lark.WSClient({
115
+ appId: this.appId,
116
+ appSecret: this.appSecret,
117
+ loggerLevel: Lark.LoggerLevel.warn
73
118
  });
74
- const sessionId = res.data.id;
75
- userSessions.set(openId, { sessionId, lastActivity: now });
76
- await client.app.log({
119
+ wsClient.start({
120
+ eventDispatcher: new Lark.EventDispatcher({}).register({
121
+ "im.message.receive_v1": async (data) => {
122
+ const parsed = this.parseEvent(data);
123
+ if (parsed)
124
+ await onMessage(parsed);
125
+ }
126
+ })
127
+ });
128
+ await this.client.app.log({
77
129
  body: {
78
130
  service: "chat-channel",
79
131
  level: "info",
80
- message: `创建新 session: ${sessionId}`,
81
- extra: { openId }
132
+ message: `[feishu] 飞书机器人已启动(长连接模式),appId=${this.appId.slice(0, 8)}***`
82
133
  }
83
134
  });
84
- return sessionId;
85
135
  }
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;
136
+ async send(replyTarget, text) {
97
137
  const chunks = [];
98
- for (let i = 0;i < text.length; i += MAX_LEN) {
99
- chunks.push(text.slice(i, i + MAX_LEN));
138
+ for (let i = 0;i < text.length; i += MAX_MSG_LEN) {
139
+ chunks.push(text.slice(i, i + MAX_MSG_LEN));
100
140
  }
101
141
  for (const chunk of chunks) {
102
- await larkClient.im.message.create({
142
+ await this.larkClient.im.message.create({
103
143
  params: { receive_id_type: "chat_id" },
104
144
  data: {
105
- receive_id: chatId,
145
+ receive_id: replyTarget,
106
146
  content: JSON.stringify({ text: chunk }),
107
147
  msg_type: "text"
108
148
  }
109
149
  });
110
150
  }
111
151
  }
112
- async function sendThinking(chatId) {
113
- await larkClient.im.message.create({
152
+ async sendThinking(chatId) {
153
+ await this.larkClient.im.message.create({
114
154
  params: { receive_id_type: "chat_id" },
115
155
  data: {
116
156
  receive_id: chatId,
@@ -119,15 +159,12 @@ var ChatChannelPlugin = async ({ client }) => {
119
159
  }
120
160
  });
121
161
  }
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;
162
+ parseEvent(data) {
163
+ const { message, sender } = data ?? {};
164
+ if (!message || !sender)
165
+ return null;
129
166
  if (message.message_type !== "text") {
130
- await larkClient.im.message.create({
167
+ this.larkClient.im.message.create({
131
168
  params: { receive_id_type: "chat_id" },
132
169
  data: {
133
170
  receive_id: message.chat_id,
@@ -135,48 +172,142 @@ var ChatChannelPlugin = async ({ client }) => {
135
172
  msg_type: "text"
136
173
  }
137
174
  });
138
- return;
175
+ return null;
139
176
  }
140
177
  const msgId = message.message_id;
141
- if (processedMessages.has(msgId)) {
178
+ if (this.processedMessages.has(msgId))
179
+ return null;
180
+ if (this.processedMessages.size >= MAX_PROCESSED)
181
+ this.processedMessages.clear();
182
+ this.processedMessages.add(msgId);
183
+ let text;
184
+ try {
185
+ text = JSON.parse(message.content).text?.trim();
186
+ } catch {
187
+ return null;
188
+ }
189
+ if (!text)
190
+ return null;
191
+ return {
192
+ messageId: msgId,
193
+ userId: sender.sender_id?.open_id ?? message.chat_id,
194
+ replyTarget: message.chat_id,
195
+ text
196
+ };
197
+ }
198
+ }
199
+ var feishuChannelFactory = async (client) => {
200
+ const appId = process.env["FEISHU_APP_ID"];
201
+ const appSecret = readAppSecret();
202
+ if (!appId || !appSecret) {
203
+ const isMac = process.platform === "darwin";
204
+ const secretHint = isMac ? `macOS: security add-generic-password -a chat-channel -s opencode-chat-channel -w <secret> -U
205
+ ` + " 或在 .env 中添加 FEISHU_APP_SECRET=<secret>" : ".env 中添加 FEISHU_APP_SECRET=<secret>";
206
+ await client.app.log({
207
+ body: {
208
+ service: "chat-channel",
209
+ level: "warn",
210
+ message: !appId ? "[feishu] FEISHU_APP_ID 未设置,请在 .env 中添加 FEISHU_APP_ID=cli_xxx,飞书渠道已跳过" : `[feishu] FEISHU_APP_SECRET 未找到,飞书渠道已跳过。请配置:${secretHint}`
211
+ }
212
+ });
213
+ return null;
214
+ }
215
+ return new FeishuChannel(appId, appSecret, client);
216
+ };
217
+
218
+ // src/channels/wecom/index.ts
219
+ class WeComChannel {
220
+ corpId;
221
+ agentId;
222
+ secret;
223
+ client;
224
+ name = "wecom";
225
+ constructor(corpId, agentId, secret, client) {
226
+ this.corpId = corpId;
227
+ this.agentId = agentId;
228
+ this.secret = secret;
229
+ this.client = client;
230
+ }
231
+ async start(onMessage) {
232
+ await this.client.app.log({
233
+ body: {
234
+ service: "chat-channel",
235
+ level: "warn",
236
+ message: "[wecom] 企业微信渠道尚未实现,已跳过。如需接入,请参考 src/channels/wecom/index.ts 中的 TODO。"
237
+ }
238
+ });
239
+ }
240
+ async send(replyTarget, text) {
241
+ throw new Error("[wecom] WeComChannel.send() 尚未实现");
242
+ }
243
+ }
244
+ var wecomChannelFactory = async (client) => {
245
+ const corpId = process.env["WECOM_CORP_ID"];
246
+ const agentId = process.env["WECOM_AGENT_ID"];
247
+ const secret = undefined;
248
+ if (!corpId || !agentId || !secret) {
249
+ if (corpId || agentId) {
142
250
  await client.app.log({
143
251
  body: {
144
252
  service: "chat-channel",
145
- level: "info",
146
- message: `重复消息已忽略: ${msgId}`
253
+ level: "warn",
254
+ message: "[wecom] 企业微信凭证不完整(需要 WECOM_CORP_ID、WECOM_AGENT_ID、WECOM_SECRET),渠道已跳过"
147
255
  }
148
256
  });
149
- return;
150
257
  }
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;
258
+ return null;
259
+ }
260
+ return new WeComChannel(corpId, agentId, secret, client);
261
+ };
262
+
263
+ // src/index.ts
264
+ var CHANNEL_REGISTRY = {
265
+ feishu: feishuChannelFactory,
266
+ wecom: wecomChannelFactory
267
+ };
268
+ function resolveEnabledChannels(client) {
269
+ const raw = process.env["CHAT_CHANNELS"]?.trim();
270
+ if (!raw)
271
+ return null;
272
+ const requested = raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
273
+ const known = Object.keys(CHANNEL_REGISTRY);
274
+ const enabled = [];
275
+ for (const name of requested) {
276
+ if (known.includes(name)) {
277
+ enabled.push(name);
278
+ } else {
279
+ client.app.log({
280
+ body: {
281
+ service: "chat-channel",
282
+ level: "warn",
283
+ message: `[config] 未知渠道名 "${name}",已忽略。可用渠道:${known.join(", ")}`
284
+ }
285
+ });
159
286
  }
160
- if (!userText)
161
- return;
162
- const openId = sender.sender_id?.open_id ?? message.chat_id;
163
- const chatId = message.chat_id;
287
+ }
288
+ return enabled;
289
+ }
290
+ function createMessageHandler(channel, sessionManager, client) {
291
+ return async (msg) => {
292
+ const { userId, replyTarget, text } = msg;
164
293
  await client.app.log({
165
294
  body: {
166
295
  service: "chat-channel",
167
296
  level: "info",
168
- message: `收到消息: "${userText.slice(0, 80)}${userText.length > 80 ? "..." : ""}"`,
169
- extra: { openId, chatId }
297
+ message: `[${channel.name}] 收到消息: "${text.slice(0, 80)}${text.length > 80 ? "..." : ""}"`,
298
+ extra: { userId, replyTarget }
170
299
  }
171
300
  });
172
- await sendThinking(chatId);
301
+ if ("sendThinking" in channel && typeof channel.sendThinking === "function") {
302
+ await channel.sendThinking(replyTarget);
303
+ }
173
304
  let responseText = null;
174
305
  try {
175
- const sessionId = await getOrCreateSession(openId);
306
+ const sessionId = await sessionManager.getOrCreate(userId);
176
307
  const result = await client.session.prompt({
177
308
  path: { id: sessionId },
178
309
  body: {
179
- parts: [{ type: "text", text: userText }],
310
+ parts: [{ type: "text", text }],
180
311
  model: {
181
312
  providerID: "Mify-Anthropic",
182
313
  modelID: "ppio/pa/claude-sonnet-4-6"
@@ -190,34 +321,60 @@ var ChatChannelPlugin = async ({ client }) => {
190
321
  body: {
191
322
  service: "chat-channel",
192
323
  level: "error",
193
- message: `处理消息失败: ${errorMsg}`,
194
- extra: { openId }
324
+ message: `[${channel.name}] 处理消息失败: ${errorMsg}`,
325
+ extra: { userId }
195
326
  }
196
327
  });
197
- await replyToFeishu(chatId, `⚠️ 出错了:${errorMsg}`);
328
+ await channel.send(replyTarget, `⚠️ 出错了:${errorMsg}`);
198
329
  return;
199
330
  }
200
- if (responseText) {
201
- await replyToFeishu(chatId, responseText);
202
- } else {
203
- await replyToFeishu(chatId, "(AI 没有返回文字回复)");
204
- }
331
+ await channel.send(replyTarget, responseText || "(AI 没有返回文字回复)");
332
+ };
333
+ }
334
+ var ChatChannelPlugin = async ({ client }) => {
335
+ const configDir = join(process.env["HOME"] ?? `/Users/${process.env["USER"] ?? "unknown"}`, ".config", "opencode");
336
+ loadDotEnv(join(configDir, ".env"));
337
+ const enabledNames = resolveEnabledChannels(client);
338
+ const factories = enabledNames ? enabledNames.map((name) => [name, CHANNEL_REGISTRY[name]]) : Object.entries(CHANNEL_REGISTRY);
339
+ if (enabledNames) {
340
+ await client.app.log({
341
+ body: {
342
+ service: "chat-channel",
343
+ level: "info",
344
+ message: `[config] CHAT_CHANNELS="${process.env["CHAT_CHANNELS"]}",将启用: ${enabledNames.join(", ") || "(无)"}`
345
+ }
346
+ });
347
+ }
348
+ const channels = [];
349
+ for (const [, factory] of factories) {
350
+ const channel = await factory(client);
351
+ if (channel)
352
+ channels.push(channel);
353
+ }
354
+ if (channels.length === 0) {
355
+ const hint = enabledNames ? `检查 CHAT_CHANNELS="${process.env["CHAT_CHANNELS"]}" 指定的渠道是否已配置凭证` : "请在 .env 中配置至少一个渠道的凭证,或设置 CHAT_CHANNELS=<渠道名> 明确指定";
356
+ await client.app.log({
357
+ body: {
358
+ service: "chat-channel",
359
+ level: "warn",
360
+ message: `所有渠道均未就绪,插件空启动。${hint}`
361
+ }
362
+ });
363
+ return {};
364
+ }
365
+ const cleanupTimers = [];
366
+ for (const channel of channels) {
367
+ const sessionManager = new SessionManager(client, channel.name, (userId) => `${channel.name} 对话 · ${userId}`);
368
+ const timer = sessionManager.startAutoCleanup();
369
+ cleanupTimers.push(timer);
370
+ const handleMessage = createMessageHandler(channel, sessionManager, client);
371
+ await channel.start(handleMessage);
205
372
  }
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
373
  await client.app.log({
217
374
  body: {
218
375
  service: "chat-channel",
219
376
  level: "info",
220
- message: `飞书机器人已启动(长连接模式),appId=${appId.slice(0, 8)}***`
377
+ message: `chat-channel 已启动,活跃渠道: ${channels.map((c) => c.name).join(", ")}`
221
378
  }
222
379
  });
223
380
  return {
@@ -237,6 +394,9 @@ var ChatChannelPlugin = async ({ client }) => {
237
394
  };
238
395
  var src_default = ChatChannelPlugin;
239
396
  export {
397
+ loadDotEnv,
398
+ extractResponseText,
240
399
  src_default as default,
400
+ SessionManager,
241
401
  ChatChannelPlugin
242
402
  };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * opencode-chat-channel — SessionManager
3
+ *
4
+ * 管理"渠道用户 ID → opencode session"的映射。
5
+ * 与具体渠道无关,所有 channel 实现共享同一套逻辑。
6
+ */
7
+ import type { PluginClient } from "./types.js";
8
+ export declare class SessionManager {
9
+ private readonly client;
10
+ private readonly channel;
11
+ private readonly titleFn;
12
+ /**
13
+ * @param client opencode plugin 客户端(用于创建 session 和日志)
14
+ * @param channel 渠道名称(用于日志标识)
15
+ * @param titleFn 生成 session 标题的函数(可选,默认使用 userId)
16
+ */
17
+ constructor(client: PluginClient, channel: string, titleFn?: (userId: string) => string);
18
+ private readonly sessions;
19
+ /** 获取已有的有效 session,或为该用户创建新 session。 */
20
+ getOrCreate(userId: string): Promise<string>;
21
+ /** 清理所有已过期的 session 记录。 */
22
+ cleanup(): void;
23
+ /** 启动定时清理(默认每 30 分钟)。返回 timer,供外部 clearInterval。 */
24
+ startAutoCleanup(intervalMs?: number): ReturnType<typeof setInterval>;
25
+ }
26
+ /**
27
+ * 从 AI 响应 parts 中提取纯文本。
28
+ * 与渠道无关,各 channel 实现均可复用。
29
+ */
30
+ export declare function extractResponseText(parts: unknown[]): string;
31
+ /**
32
+ * 从 ~/.config/opencode/.env 读取环境变量并注入 process.env。
33
+ * opencode 不会自动加载该文件,需插件自行调用。
34
+ */
35
+ export declare function loadDotEnv(envPath: string): void;
36
+ //# sourceMappingURL=session-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAgB/C,qBAAa,cAAc;IAOvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAR1B;;;;OAIG;gBAEgB,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MACxB;IAGxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;IAE3D,wCAAwC;IAClC,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA6BlD,2BAA2B;IAC3B,OAAO,IAAI,IAAI;IASf,oDAAoD;IACpD,gBAAgB,CAAC,UAAU,SAAiB,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC;CAG9E;AAID;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,CAO5D;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAiBhD"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * opencode-chat-channel — 多渠道抽象层
3
+ *
4
+ * 每个渠道(飞书、企业微信等)实现 ChatChannel 接口即可接入。
5
+ */
6
+ import type { Plugin } from "@opencode-ai/plugin";
7
+ /** opencode Plugin 客户端类型(从 Plugin 回调参数中提取) */
8
+ export type PluginClient = Parameters<Plugin>[0]["client"];
9
+ /** 从渠道收到的标准化消息 */
10
+ export interface IncomingMessage {
11
+ /** 消息唯一 ID(用于去重) */
12
+ messageId: string;
13
+ /** 用户唯一标识(用于 session 绑定,不同渠道格式不同) */
14
+ userId: string;
15
+ /** 回复目标 ID(群 ID / 用户 ID,由渠道决定具体含义) */
16
+ replyTarget: string;
17
+ /** 消息文本内容 */
18
+ text: string;
19
+ }
20
+ /**
21
+ * ChatChannel — 渠道适配器接口
22
+ *
23
+ * 每个渠道实现此接口:
24
+ * - name: 渠道标识符(用于日志、配置 key)
25
+ * - start(): 启动监听,收到消息时调用 onMessage 回调
26
+ * - send(): 向指定 target 发送文本回复
27
+ * - stop(): 优雅关闭(可选)
28
+ */
29
+ export interface ChatChannel {
30
+ /** 渠道唯一名称,如 "feishu"、"wecom" */
31
+ readonly name: string;
32
+ /**
33
+ * 启动渠道监听。
34
+ * 每当收到用户消息时,调用 onMessage(msg)。
35
+ */
36
+ start(onMessage: (msg: IncomingMessage) => Promise<void>): Promise<void>;
37
+ /**
38
+ * 向指定目标发送文本消息。
39
+ * replyTarget 为 IncomingMessage.replyTarget。
40
+ */
41
+ send(replyTarget: string, text: string): Promise<void>;
42
+ /** 优雅停止渠道(可选)。 */
43
+ stop?(): Promise<void>;
44
+ }
45
+ /**
46
+ * ChannelFactory — 渠道工厂函数签名
47
+ *
48
+ * 工厂函数负责读取配置、验证凭证、构造并返回 ChatChannel 实例。
49
+ * 若凭证未配置,应返回 null(插件将跳过该渠道并记录日志)。
50
+ */
51
+ export type ChannelFactory = (client: PluginClient) => Promise<ChatChannel | null>;
52
+ /**
53
+ * 已注册的渠道名称。
54
+ * 新增渠道时在此处添加对应字面量,便于编译期检查。
55
+ */
56
+ export type ChannelName = "feishu" | "wecom";
57
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAElD,8CAA8C;AAC9C,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;AAI3D,kBAAkB;AAClB,MAAM,WAAW,eAAe;IAC9B,oBAAoB;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAID;;;;;;;;GAQG;AACH,MAAM,WAAW,WAAW;IAC1B,gCAAgC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,KAAK,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzE;;;OAGG;IACH,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvD,kBAAkB;IAClB,IAAI,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAID;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,CAC3B,MAAM,EAAE,YAAY,KACjB,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;AAIjC;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-chat-channel",
3
- "version": "1.0.0",
4
- "description": "opencode plugin — Feishu (Lark) bot channel via WebSocket long connection",
3
+ "version": "1.2.0",
4
+ "description": "opencode plugin — multi-channel bot (Feishu/Lark, WeCom) with extensible ChatChannel interface",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -29,8 +29,11 @@
29
29
  "opencode-plugin",
30
30
  "feishu",
31
31
  "lark",
32
+ "wecom",
33
+ "wechat",
32
34
  "bot",
33
- "websocket"
35
+ "websocket",
36
+ "multi-channel"
34
37
  ],
35
38
  "author": "",
36
39
  "license": "MIT",