openclaw-channel-github 0.1.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 +578 -0
- package/config.example.json +33 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/src/auth/auth.d.ts +22 -0
- package/dist/src/auth/auth.d.ts.map +1 -0
- package/dist/src/auth/auth.js +112 -0
- package/dist/src/auth/auth.js.map +1 -0
- package/dist/src/config/config.d.ts +60 -0
- package/dist/src/config/config.d.ts.map +1 -0
- package/dist/src/config/config.js +184 -0
- package/dist/src/config/config.js.map +1 -0
- package/dist/src/events/events.d.ts +234 -0
- package/dist/src/events/events.d.ts.map +1 -0
- package/dist/src/events/events.js +53 -0
- package/dist/src/events/events.js.map +1 -0
- package/dist/src/main.d.ts +2 -0
- package/dist/src/main.d.ts.map +1 -0
- package/dist/src/main.js +115 -0
- package/dist/src/main.js.map +1 -0
- package/dist/src/normalizer/normalizer.d.ts +94 -0
- package/dist/src/normalizer/normalizer.d.ts.map +1 -0
- package/dist/src/normalizer/normalizer.js +486 -0
- package/dist/src/normalizer/normalizer.js.map +1 -0
- package/dist/src/outbound/outbound.d.ts +24 -0
- package/dist/src/outbound/outbound.d.ts.map +1 -0
- package/dist/src/outbound/outbound.js +125 -0
- package/dist/src/outbound/outbound.js.map +1 -0
- package/dist/src/routing/routing.d.ts +22 -0
- package/dist/src/routing/routing.d.ts.map +1 -0
- package/dist/src/routing/routing.js +97 -0
- package/dist/src/routing/routing.js.map +1 -0
- package/dist/src/server/server.d.ts +20 -0
- package/dist/src/server/server.d.ts.map +1 -0
- package/dist/src/server/server.js +283 -0
- package/dist/src/server/server.js.map +1 -0
- package/dist/src/state/state.d.ts +17 -0
- package/dist/src/state/state.d.ts.map +1 -0
- package/dist/src/state/state.js +49 -0
- package/dist/src/state/state.js.map +1 -0
- package/docs/api.md +450 -0
- package/docs/usage.md +393 -0
- package/openclaw.plugin.json +104 -0
- package/package.json +81 -0
package/README.md
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
# OpenClaw GitHub Channel
|
|
2
|
+
|
|
3
|
+
OpenClaw GitHub Channel 是一个基于 Node.js 的 OpenClaw Channel 插件,用来把 GitHub Issues、Pull Requests、Reviews、Discussions 和部分 CI 事件接入 OpenClaw。
|
|
4
|
+
|
|
5
|
+
它负责两件事:
|
|
6
|
+
|
|
7
|
+
- 接收入站 GitHub Webhook,并归一化为 OpenClaw 可消费的事件格式
|
|
8
|
+
- 将 OpenClaw 的回复写回 GitHub,例如评论、Review 或 Reaction
|
|
9
|
+
|
|
10
|
+
当前实现采用 TypeScript 开发,构建产物位于 `dist/`,插件清单位于 `openclaw.plugin.json`。
|
|
11
|
+
|
|
12
|
+
## 功能概览
|
|
13
|
+
|
|
14
|
+
- 支持 GitHub App Webhook 验签
|
|
15
|
+
- 支持 Issue、PR、Review、Discussion、Check Run、Workflow Run 等事件解析
|
|
16
|
+
- 支持将 GitHub 事件归一化为统一的 `NormalizedEvent`
|
|
17
|
+
- 支持基于 mention、命令、标签、自动触发的路由策略
|
|
18
|
+
- 支持简单的 delivery 去重与 bot loop 防护
|
|
19
|
+
- 支持回写 GitHub 评论、PR Review、Reaction
|
|
20
|
+
- 适合作为 OpenClaw 的“异步协作型 Channel”接入仓库工作流
|
|
21
|
+
|
|
22
|
+
## 仓库结构
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
.
|
|
26
|
+
├── openclaw.plugin.json
|
|
27
|
+
├── package.json
|
|
28
|
+
├── index.ts
|
|
29
|
+
├── src/
|
|
30
|
+
│ ├── main.ts
|
|
31
|
+
│ ├── auth/
|
|
32
|
+
│ ├── config/
|
|
33
|
+
│ ├── events/
|
|
34
|
+
│ ├── normalizer/
|
|
35
|
+
│ ├── outbound/
|
|
36
|
+
│ ├── routing/
|
|
37
|
+
│ ├── server/
|
|
38
|
+
│ └── state/
|
|
39
|
+
├── e2e/
|
|
40
|
+
└── docs/
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
各模块职责如下:
|
|
44
|
+
|
|
45
|
+
- `src/auth`:GitHub App JWT、Webhook HMAC 验签、请求头提取
|
|
46
|
+
- `src/config`:配置加载、解析、校验、仓库和账号匹配
|
|
47
|
+
- `src/events`:GitHub Webhook 类型定义与事件解析
|
|
48
|
+
- `src/normalizer`:把 GitHub 事件转换为统一事件结构
|
|
49
|
+
- `src/routing`:session key、触发策略、bot 检测、outbound marker 检测
|
|
50
|
+
- `src/outbound`:GitHub API 回写
|
|
51
|
+
- `src/state`:去重状态存储
|
|
52
|
+
- `src/server`:HTTP Webhook 入口与处理管线
|
|
53
|
+
- `src/main.ts`:服务启动入口
|
|
54
|
+
- `index.ts`:插件公共导出入口
|
|
55
|
+
|
|
56
|
+
## 运行要求
|
|
57
|
+
|
|
58
|
+
- OpenClaw 主程序建议使用官方文档推荐版本安装
|
|
59
|
+
- 本项目代码运行需要 Node.js 20 或更高版本
|
|
60
|
+
- npm 10 或更高版本
|
|
61
|
+
- 一个已安装到目标仓库或组织的 GitHub App
|
|
62
|
+
- 一个可被 GitHub 回调访问到的 HTTP 地址
|
|
63
|
+
|
|
64
|
+
## 安装 OpenClaw
|
|
65
|
+
|
|
66
|
+
如果你的机器上还没有 OpenClaw,先按官方 CLI 方式安装:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
curl -fsSL https://openclaw.ai/install.sh | bash
|
|
70
|
+
openclaw onboard --install-daemon
|
|
71
|
+
openclaw gateway status
|
|
72
|
+
openclaw dashboard
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
以上命令格式与当前 OpenClaw 文档一致。
|
|
76
|
+
|
|
77
|
+
## 安装插件
|
|
78
|
+
|
|
79
|
+
OpenClaw 的插件分发通常有两种方式:
|
|
80
|
+
|
|
81
|
+
1. npm 包分发
|
|
82
|
+
2. 本地扩展路径加载
|
|
83
|
+
|
|
84
|
+
本项目现在已经补齐了 OpenClaw 识别所需的元数据:
|
|
85
|
+
|
|
86
|
+
- `openclaw.plugin.json`
|
|
87
|
+
- `package.json` 中的 `openclaw.extensions`
|
|
88
|
+
- `package.json` 中的 `openclaw.channel` 和 `openclaw.install`
|
|
89
|
+
|
|
90
|
+
这意味着它可以按 OpenClaw 当前文档中的插件机制被发现和展示。
|
|
91
|
+
|
|
92
|
+
### 方式一:本地路径安装
|
|
93
|
+
|
|
94
|
+
如果你在本地开发或尚未发布 npm 包,推荐直接用 OpenClaw CLI 从本地目录安装。
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
git clone https://github.com/Iceber/openclaw-channel-github.git
|
|
98
|
+
cd openclaw-channel-github
|
|
99
|
+
npm install
|
|
100
|
+
npm run build
|
|
101
|
+
openclaw plugins install -l .
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
说明:
|
|
105
|
+
|
|
106
|
+
- `-l` 表示 link 安装,适合开发期本地调试
|
|
107
|
+
- 如果你想复制安装而不是链接安装,可使用 `openclaw plugins install .`
|
|
108
|
+
- 安装后可用 `openclaw plugins list` 检查插件是否被发现
|
|
109
|
+
|
|
110
|
+
常用检查命令:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
openclaw plugins list
|
|
114
|
+
openclaw plugins info github
|
|
115
|
+
openclaw plugins doctor
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 方式二:npm 包安装
|
|
119
|
+
|
|
120
|
+
如果你准备将该插件发布到 npm,发布后可按 OpenClaw 官方支持的 npm spec 方式安装。
|
|
121
|
+
|
|
122
|
+
典型流程:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npm install
|
|
126
|
+
npm run build
|
|
127
|
+
npm publish
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
发布后安装命令格式如下:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
openclaw plugins install openclaw-channel-github
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
如果后续改为 scoped 包名,则命令会变成:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
openclaw plugins install @your-scope/openclaw-channel-github
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
OpenClaw 当前对 npm 插件安装的命令格式是固定的 `openclaw plugins install <npm-spec>`,不接受 git/url/file spec 作为 npm 安装来源。
|
|
143
|
+
|
|
144
|
+
建议确保发布包中包含以下内容:
|
|
145
|
+
|
|
146
|
+
- `openclaw.plugin.json`
|
|
147
|
+
- `dist/`
|
|
148
|
+
- `package.json`
|
|
149
|
+
|
|
150
|
+
### 安装后启用与重启
|
|
151
|
+
|
|
152
|
+
插件安装后,建议执行:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
openclaw plugins list
|
|
156
|
+
openclaw gateway status
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
如果你的 Gateway 已经在运行,按官方插件文档建议重启 Gateway 以加载新插件。
|
|
160
|
+
|
|
161
|
+
## GitHub App 配置
|
|
162
|
+
|
|
163
|
+
推荐使用 GitHub App 模式接入,而不是直接使用个人访问令牌。
|
|
164
|
+
|
|
165
|
+
### 建议权限
|
|
166
|
+
|
|
167
|
+
当前插件清单中声明的权限如下:
|
|
168
|
+
|
|
169
|
+
- `issues: write`
|
|
170
|
+
- `pull_requests: write`
|
|
171
|
+
- `metadata: read`
|
|
172
|
+
- `discussions: write`
|
|
173
|
+
- `contents: read`
|
|
174
|
+
|
|
175
|
+
### 建议订阅的 Webhook 事件
|
|
176
|
+
|
|
177
|
+
- `issues`
|
|
178
|
+
- `issue_comment`
|
|
179
|
+
- `pull_request`
|
|
180
|
+
- `pull_request_review`
|
|
181
|
+
- `pull_request_review_comment`
|
|
182
|
+
- `discussion`
|
|
183
|
+
- `discussion_comment`
|
|
184
|
+
- `check_run`
|
|
185
|
+
- `workflow_run`
|
|
186
|
+
|
|
187
|
+
### 获取必要信息
|
|
188
|
+
|
|
189
|
+
你需要从 GitHub App 配置页拿到以下信息:
|
|
190
|
+
|
|
191
|
+
- App ID
|
|
192
|
+
- Installation ID
|
|
193
|
+
- Webhook Secret
|
|
194
|
+
- Private Key 文件路径
|
|
195
|
+
|
|
196
|
+
## 配置文件
|
|
197
|
+
|
|
198
|
+
服务启动时默认读取根目录下的 `config.json`。仓库中现在提供了可直接复制的示例文件 `config.example.json`。你也可以在启动命令中传入自定义路径。
|
|
199
|
+
|
|
200
|
+
### 最小配置示例
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"server": {
|
|
205
|
+
"addr": ":8080"
|
|
206
|
+
},
|
|
207
|
+
"channel": {
|
|
208
|
+
"enabled": true,
|
|
209
|
+
"mode": "app",
|
|
210
|
+
"appId": 123456,
|
|
211
|
+
"installationId": 78901234,
|
|
212
|
+
"privateKeyPath": "./github-app.private-key.pem",
|
|
213
|
+
"webhookSecret": "replace-with-your-webhook-secret",
|
|
214
|
+
"repositories": [
|
|
215
|
+
"your-org/your-repo"
|
|
216
|
+
],
|
|
217
|
+
"ignoreBots": true,
|
|
218
|
+
"trigger": {
|
|
219
|
+
"requireMention": true,
|
|
220
|
+
"botUsername": "openclaw-bot",
|
|
221
|
+
"commands": [
|
|
222
|
+
"/openclaw"
|
|
223
|
+
],
|
|
224
|
+
"labels": []
|
|
225
|
+
},
|
|
226
|
+
"outbound": {
|
|
227
|
+
"mode": "comment",
|
|
228
|
+
"outboundMarker": "<!-- openclaw-outbound -->"
|
|
229
|
+
},
|
|
230
|
+
"autoTrigger": {
|
|
231
|
+
"onPROpened": false,
|
|
232
|
+
"onIssueOpened": false
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
如果你只是想先跑起来,直接复制示例文件即可:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
cp config.example.json config.json
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### 多账号配置示例
|
|
245
|
+
|
|
246
|
+
当你需要一个实例管理多个 GitHub App 安装或多组仓库时,可使用 `accounts`。
|
|
247
|
+
|
|
248
|
+
```json
|
|
249
|
+
{
|
|
250
|
+
"server": {
|
|
251
|
+
"addr": ":8080"
|
|
252
|
+
},
|
|
253
|
+
"channel": {
|
|
254
|
+
"enabled": true,
|
|
255
|
+
"ignoreBots": true,
|
|
256
|
+
"trigger": {
|
|
257
|
+
"requireMention": true,
|
|
258
|
+
"botUsername": "openclaw-bot",
|
|
259
|
+
"commands": ["/openclaw"],
|
|
260
|
+
"labels": ["ai-review"]
|
|
261
|
+
},
|
|
262
|
+
"outbound": {
|
|
263
|
+
"mode": "comment",
|
|
264
|
+
"outboundMarker": "<!-- openclaw-outbound -->"
|
|
265
|
+
},
|
|
266
|
+
"autoTrigger": {
|
|
267
|
+
"onPROpened": true,
|
|
268
|
+
"onIssueOpened": false
|
|
269
|
+
},
|
|
270
|
+
"accounts": {
|
|
271
|
+
"team-a": {
|
|
272
|
+
"mode": "app",
|
|
273
|
+
"appId": 111111,
|
|
274
|
+
"installationId": 222222,
|
|
275
|
+
"privateKeyPath": "./keys/team-a.pem",
|
|
276
|
+
"webhookSecret": "team-a-secret",
|
|
277
|
+
"repositories": [
|
|
278
|
+
"org-a/repo-1",
|
|
279
|
+
"org-a/repo-2"
|
|
280
|
+
]
|
|
281
|
+
},
|
|
282
|
+
"team-b": {
|
|
283
|
+
"mode": "app",
|
|
284
|
+
"appId": 333333,
|
|
285
|
+
"installationId": 444444,
|
|
286
|
+
"privateKeyPath": "./keys/team-b.pem",
|
|
287
|
+
"webhookSecret": "team-b-secret",
|
|
288
|
+
"repositories": [
|
|
289
|
+
"org-b/repo-1"
|
|
290
|
+
]
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### 配置项说明
|
|
298
|
+
|
|
299
|
+
#### `server`
|
|
300
|
+
|
|
301
|
+
- `addr`:监听地址,默认 `:8080`
|
|
302
|
+
|
|
303
|
+
#### `channel`
|
|
304
|
+
|
|
305
|
+
- `enabled`:是否启用 GitHub Channel
|
|
306
|
+
- `mode`:认证模式,当前主要使用 `app`
|
|
307
|
+
- `appId`:GitHub App ID
|
|
308
|
+
- `installationId`:GitHub App Installation ID
|
|
309
|
+
- `privateKeyPath`:GitHub App 私钥 PEM 路径
|
|
310
|
+
- `webhookSecret`:Webhook 验签密钥
|
|
311
|
+
- `repositories`:允许处理的仓库列表,格式必须为 `owner/repo`
|
|
312
|
+
- `ignoreBots`:是否忽略 Bot 发送者
|
|
313
|
+
|
|
314
|
+
#### `channel.trigger`
|
|
315
|
+
|
|
316
|
+
- `requireMention`:是否必须 `@bot` 才触发
|
|
317
|
+
- `botUsername`:Bot 用户名,用于 mention 检测
|
|
318
|
+
- `commands`:命令前缀列表,例如 `/openclaw`
|
|
319
|
+
- `labels`:标签触发列表,例如 `ai-review`
|
|
320
|
+
|
|
321
|
+
#### `channel.outbound`
|
|
322
|
+
|
|
323
|
+
- `mode`:回写模式,支持 `comment`、`review`、`auto`
|
|
324
|
+
- `outboundMarker`:写回隐藏标记,用于避免 bot loop
|
|
325
|
+
|
|
326
|
+
#### `channel.autoTrigger`
|
|
327
|
+
|
|
328
|
+
- `onPROpened`:PR 打开时自动触发
|
|
329
|
+
- `onIssueOpened`:Issue 打开时自动触发
|
|
330
|
+
|
|
331
|
+
#### `channel.accounts`
|
|
332
|
+
|
|
333
|
+
- 用于多安装、多仓库分组配置
|
|
334
|
+
- 如果配置了 `accounts`,会优先按仓库匹配具体账号
|
|
335
|
+
|
|
336
|
+
## 启动方式
|
|
337
|
+
|
|
338
|
+
### 开发模式
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
npm install
|
|
342
|
+
npm run dev -- ./config.json
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
说明:当前 `dev` 命令本质上执行的是 `ts-node src/main.ts`,启动参数会作为位置参数传入,推荐直接把配置路径作为最后一个参数。
|
|
346
|
+
|
|
347
|
+
### 生产模式
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
npm install
|
|
351
|
+
npm run build
|
|
352
|
+
npm start -- ./config.json
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### 仅构建
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
npm run build
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
构建完成后主要产物包括:
|
|
362
|
+
|
|
363
|
+
- `dist/main.js`
|
|
364
|
+
- `dist/index.js`
|
|
365
|
+
|
|
366
|
+
## Webhook 配置
|
|
367
|
+
|
|
368
|
+
在 GitHub App 中,将 Webhook URL 指向你的服务地址:
|
|
369
|
+
|
|
370
|
+
```text
|
|
371
|
+
https://your-domain.example.com/webhook
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
健康检查地址:
|
|
375
|
+
|
|
376
|
+
```text
|
|
377
|
+
https://your-domain.example.com/health
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
如果你本地调试,可通过隧道工具暴露本地端口,例如:
|
|
381
|
+
|
|
382
|
+
- ngrok
|
|
383
|
+
- cloudflared tunnel
|
|
384
|
+
- 反向代理到公网地址
|
|
385
|
+
|
|
386
|
+
## 事件处理模型
|
|
387
|
+
|
|
388
|
+
### 支持的事件
|
|
389
|
+
|
|
390
|
+
- `issues`
|
|
391
|
+
- `issue_comment`
|
|
392
|
+
- `pull_request`
|
|
393
|
+
- `pull_request_review`
|
|
394
|
+
- `pull_request_review_comment`
|
|
395
|
+
- `discussion`
|
|
396
|
+
- `discussion_comment`
|
|
397
|
+
- `check_run`
|
|
398
|
+
- `workflow_run`
|
|
399
|
+
|
|
400
|
+
### 触发方式
|
|
401
|
+
|
|
402
|
+
按优先顺序大致如下:
|
|
403
|
+
|
|
404
|
+
1. mention 触发
|
|
405
|
+
2. 命令前缀触发
|
|
406
|
+
3. 标签触发
|
|
407
|
+
4. 自动触发
|
|
408
|
+
|
|
409
|
+
### 会话键
|
|
410
|
+
|
|
411
|
+
会话键用于把同一个 Issue / PR 的后续消息归并到同一个 OpenClaw 会话中。
|
|
412
|
+
|
|
413
|
+
格式如下:
|
|
414
|
+
|
|
415
|
+
```text
|
|
416
|
+
github:<owner>/<repo>:issue:<number>
|
|
417
|
+
github:<owner>/<repo>:pull_request:<number>
|
|
418
|
+
github:<owner>/<repo>:discussion:<number>
|
|
419
|
+
github:<owner>/<repo>:pull_request:<number>:review-thread:<threadId>
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## Bot Loop 防护
|
|
423
|
+
|
|
424
|
+
当前实现提供三层基础保护:
|
|
425
|
+
|
|
426
|
+
1. 忽略 GitHub 标记为 Bot 的发送者
|
|
427
|
+
2. 忽略用户名等于 `botUsername` 的发送者
|
|
428
|
+
3. 检查回写文本中的 `outboundMarker`
|
|
429
|
+
|
|
430
|
+
建议保留默认的 `outboundMarker`,不要删除。
|
|
431
|
+
|
|
432
|
+
## OpenClaw 集成建议
|
|
433
|
+
|
|
434
|
+
这个项目当前已经具备作为 OpenClaw 插件的基础结构:
|
|
435
|
+
|
|
436
|
+
- 根目录存在 `openclaw.plugin.json`
|
|
437
|
+
- `package.json` 声明了 `openclaw.extensions: ["./index.ts"]`
|
|
438
|
+
- 构建后可由 `dist/index.js` 提供运行时代码
|
|
439
|
+
|
|
440
|
+
接入时建议遵循以下原则:
|
|
441
|
+
|
|
442
|
+
- 本地开发优先使用 `openclaw plugins install -l .`
|
|
443
|
+
- 发布后使用 `openclaw plugins install <npm-spec>`
|
|
444
|
+
- 在安装前先执行 `npm install` 和 `npm run build`
|
|
445
|
+
- 运行服务实例时提供正确的 `config.json`
|
|
446
|
+
|
|
447
|
+
如果你需要显式加载本地插件目录,也可以使用 OpenClaw 插件系统的标准方式,通过 `openclaw plugins install` 或 `plugins.load.paths` 管理,而不是依赖临时脚本扫描。
|
|
448
|
+
|
|
449
|
+
## 开发
|
|
450
|
+
|
|
451
|
+
### 安装依赖
|
|
452
|
+
|
|
453
|
+
```bash
|
|
454
|
+
npm install
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### 运行测试
|
|
458
|
+
|
|
459
|
+
```bash
|
|
460
|
+
npm test
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### 运行端到端测试
|
|
464
|
+
|
|
465
|
+
```bash
|
|
466
|
+
npm run test:e2e
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### 持续观察测试
|
|
470
|
+
|
|
471
|
+
```bash
|
|
472
|
+
npm run test:watch
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### 清理构建产物
|
|
476
|
+
|
|
477
|
+
```bash
|
|
478
|
+
npm run clean
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### 当前验证状态
|
|
482
|
+
|
|
483
|
+
本仓库当前已验证:
|
|
484
|
+
|
|
485
|
+
- 单元测试通过
|
|
486
|
+
- E2E 测试通过
|
|
487
|
+
- TypeScript 类型检查通过
|
|
488
|
+
- 构建成功
|
|
489
|
+
|
|
490
|
+
## 部署建议
|
|
491
|
+
|
|
492
|
+
### 单实例部署
|
|
493
|
+
|
|
494
|
+
适合少量仓库场景:
|
|
495
|
+
|
|
496
|
+
- 一个 Node.js 进程
|
|
497
|
+
- 一个公开可访问的 Webhook 地址
|
|
498
|
+
- 一个或少量 GitHub App Installation
|
|
499
|
+
|
|
500
|
+
### 反向代理部署
|
|
501
|
+
|
|
502
|
+
推荐在生产环境中放在反向代理后面,例如:
|
|
503
|
+
|
|
504
|
+
- Nginx
|
|
505
|
+
- Caddy
|
|
506
|
+
- Traefik
|
|
507
|
+
|
|
508
|
+
建议启用:
|
|
509
|
+
|
|
510
|
+
- HTTPS
|
|
511
|
+
- 访问日志
|
|
512
|
+
- 超时控制
|
|
513
|
+
- 基础的请求大小限制
|
|
514
|
+
|
|
515
|
+
### 状态存储说明
|
|
516
|
+
|
|
517
|
+
当前 delivery 去重状态保存在内存中,仅适合单实例或轻量部署场景。
|
|
518
|
+
|
|
519
|
+
如果后续需要生产级高可用部署,建议把 `src/state` 替换为共享存储,例如:
|
|
520
|
+
|
|
521
|
+
- Redis
|
|
522
|
+
- 数据库表
|
|
523
|
+
- 其他带 TTL 的分布式 KV
|
|
524
|
+
|
|
525
|
+
## 已知限制
|
|
526
|
+
|
|
527
|
+
- 当前默认消息处理器只是示例实现,不会自动转发到真实 OpenClaw Gateway
|
|
528
|
+
- `mode: token` 只保留了配置入口,主要实现路径仍以 GitHub App 为主
|
|
529
|
+
- delivery 去重基于内存,不适合多实例横向扩展
|
|
530
|
+
- 默认回写路径当前主要是普通 comment,复杂 Review 策略需要按你的 OpenClaw 集成方式继续扩展
|
|
531
|
+
|
|
532
|
+
## 常见问题
|
|
533
|
+
|
|
534
|
+
### 1. 服务启动后没有收到 GitHub 事件
|
|
535
|
+
|
|
536
|
+
检查以下内容:
|
|
537
|
+
|
|
538
|
+
- Webhook URL 是否可从公网访问
|
|
539
|
+
- GitHub App 是否订阅了正确事件
|
|
540
|
+
- `webhookSecret` 是否与 GitHub App 中配置一致
|
|
541
|
+
- 请求是否实际到达 `/webhook`
|
|
542
|
+
|
|
543
|
+
### 2. Webhook 返回 401
|
|
544
|
+
|
|
545
|
+
通常表示验签失败,重点检查:
|
|
546
|
+
|
|
547
|
+
- `config.json` 中的 `channel.webhookSecret`
|
|
548
|
+
- GitHub App 中的 Webhook Secret
|
|
549
|
+
- 请求是否经过了会修改 body 的代理层
|
|
550
|
+
|
|
551
|
+
### 3. 事件到达但没有触发处理
|
|
552
|
+
|
|
553
|
+
通常是触发条件未命中,检查:
|
|
554
|
+
|
|
555
|
+
- 是否满足 mention
|
|
556
|
+
- 是否满足命令前缀
|
|
557
|
+
- 是否满足标签触发
|
|
558
|
+
- 是否开启了 auto trigger
|
|
559
|
+
|
|
560
|
+
### 4. 服务能处理但无法回写 GitHub
|
|
561
|
+
|
|
562
|
+
检查以下内容:
|
|
563
|
+
|
|
564
|
+
- Installation Token 是否有效
|
|
565
|
+
- GitHub App 权限是否足够
|
|
566
|
+
- 目标仓库是否在 `repositories` allowlist 中
|
|
567
|
+
|
|
568
|
+
## 后续扩展方向
|
|
569
|
+
|
|
570
|
+
- 接入真实 OpenClaw Gateway 调用链路
|
|
571
|
+
- 把内存去重状态迁移到 Redis
|
|
572
|
+
- 丰富 PR Review 回写策略
|
|
573
|
+
- 增加更细粒度的 review thread 路由
|
|
574
|
+
- 增加更严格的速率限制和审计日志
|
|
575
|
+
|
|
576
|
+
## License
|
|
577
|
+
|
|
578
|
+
Apache-2.0
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"server": {
|
|
3
|
+
"addr": ":8080"
|
|
4
|
+
},
|
|
5
|
+
"channel": {
|
|
6
|
+
"enabled": true,
|
|
7
|
+
"mode": "app",
|
|
8
|
+
"appId": 123456,
|
|
9
|
+
"installationId": 78901234,
|
|
10
|
+
"privateKeyPath": "./github-app.private-key.pem",
|
|
11
|
+
"webhookSecret": "replace-with-your-webhook-secret",
|
|
12
|
+
"repositories": [
|
|
13
|
+
"your-org/your-repo"
|
|
14
|
+
],
|
|
15
|
+
"ignoreBots": true,
|
|
16
|
+
"trigger": {
|
|
17
|
+
"requireMention": true,
|
|
18
|
+
"botUsername": "openclaw-bot",
|
|
19
|
+
"commands": [
|
|
20
|
+
"/openclaw"
|
|
21
|
+
],
|
|
22
|
+
"labels": []
|
|
23
|
+
},
|
|
24
|
+
"outbound": {
|
|
25
|
+
"mode": "comment",
|
|
26
|
+
"outboundMarker": "<!-- openclaw-outbound -->"
|
|
27
|
+
},
|
|
28
|
+
"autoTrigger": {
|
|
29
|
+
"onPROpened": false,
|
|
30
|
+
"onIssueOpened": false
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { loadFromFile, parse, validate, isRepoAllowed } from './src/config/config';
|
|
2
|
+
export type { Config, ChannelConfig, TriggerConfig, AutoTriggerConfig, OutboundConfig } from './src/config/config';
|
|
3
|
+
export { GitHubAuth, verifyWebhookSignature } from './src/auth/auth';
|
|
4
|
+
export { EventType, Action, parseEvent } from './src/events/events';
|
|
5
|
+
export type { IssueEvent, IssueCommentEvent, PullRequestEvent, PullRequestReviewEvent, PullRequestReviewCommentEvent, DiscussionEvent, DiscussionCommentEvent, CheckRunEvent, WorkflowRunEvent, } from './src/events/events';
|
|
6
|
+
export { Provider, ThreadType, MessageType, TriggerKind, normalizeIssueOpened, normalizeIssueComment, normalizePullRequestOpened, normalizePullRequestReview, normalizePullRequestReviewComment, normalizeDiscussionCreated, normalizeDiscussionComment, normalizeCheckRun, normalizeWorkflowRun, } from './src/normalizer/normalizer';
|
|
7
|
+
export type { NormalizedEvent, Thread, Message, Sender, Trigger, Context } from './src/normalizer/normalizer';
|
|
8
|
+
export { OutboundSender } from './src/outbound/outbound';
|
|
9
|
+
export { sessionKey, evaluateTrigger, evaluateAutoTrigger, isBotSender, hasOutboundMarker } from './src/routing/routing';
|
|
10
|
+
export type { TriggerResult } from './src/routing/routing';
|
|
11
|
+
export { Store } from './src/state/state';
|
|
12
|
+
export { WebhookHandler, createServer } from './src/server/server';
|
|
13
|
+
export type { MessageHandler } from './src/server/server';
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACnF,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAEnH,OAAO,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAErE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACpE,YAAY,EACV,UAAU,EACV,iBAAiB,EACjB,gBAAgB,EAChB,sBAAsB,EACtB,6BAA6B,EAC7B,eAAe,EACf,sBAAsB,EACtB,aAAa,EACb,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,QAAQ,EACR,UAAU,EACV,WAAW,EACX,WAAW,EACX,oBAAoB,EACpB,qBAAqB,EACrB,0BAA0B,EAC1B,0BAA0B,EAC1B,iCAAiC,EACjC,0BAA0B,EAC1B,0BAA0B,EAC1B,iBAAiB,EACjB,oBAAoB,GACrB,MAAM,6BAA6B,CAAC;AACrC,YAAY,EAAE,eAAe,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AAE9G,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAEzD,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,mBAAmB,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AACzH,YAAY,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnE,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Plugin entry point — re-exports all public modules for OpenClaw plugin loading.
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.createServer = exports.WebhookHandler = exports.Store = exports.hasOutboundMarker = exports.isBotSender = exports.evaluateAutoTrigger = exports.evaluateTrigger = exports.sessionKey = exports.OutboundSender = exports.normalizeWorkflowRun = exports.normalizeCheckRun = exports.normalizeDiscussionComment = exports.normalizeDiscussionCreated = exports.normalizePullRequestReviewComment = exports.normalizePullRequestReview = exports.normalizePullRequestOpened = exports.normalizeIssueComment = exports.normalizeIssueOpened = exports.TriggerKind = exports.MessageType = exports.ThreadType = exports.Provider = exports.parseEvent = exports.Action = exports.EventType = exports.verifyWebhookSignature = exports.GitHubAuth = exports.isRepoAllowed = exports.validate = exports.parse = exports.loadFromFile = void 0;
|
|
5
|
+
var config_1 = require("./src/config/config");
|
|
6
|
+
Object.defineProperty(exports, "loadFromFile", { enumerable: true, get: function () { return config_1.loadFromFile; } });
|
|
7
|
+
Object.defineProperty(exports, "parse", { enumerable: true, get: function () { return config_1.parse; } });
|
|
8
|
+
Object.defineProperty(exports, "validate", { enumerable: true, get: function () { return config_1.validate; } });
|
|
9
|
+
Object.defineProperty(exports, "isRepoAllowed", { enumerable: true, get: function () { return config_1.isRepoAllowed; } });
|
|
10
|
+
var auth_1 = require("./src/auth/auth");
|
|
11
|
+
Object.defineProperty(exports, "GitHubAuth", { enumerable: true, get: function () { return auth_1.GitHubAuth; } });
|
|
12
|
+
Object.defineProperty(exports, "verifyWebhookSignature", { enumerable: true, get: function () { return auth_1.verifyWebhookSignature; } });
|
|
13
|
+
var events_1 = require("./src/events/events");
|
|
14
|
+
Object.defineProperty(exports, "EventType", { enumerable: true, get: function () { return events_1.EventType; } });
|
|
15
|
+
Object.defineProperty(exports, "Action", { enumerable: true, get: function () { return events_1.Action; } });
|
|
16
|
+
Object.defineProperty(exports, "parseEvent", { enumerable: true, get: function () { return events_1.parseEvent; } });
|
|
17
|
+
var normalizer_1 = require("./src/normalizer/normalizer");
|
|
18
|
+
Object.defineProperty(exports, "Provider", { enumerable: true, get: function () { return normalizer_1.Provider; } });
|
|
19
|
+
Object.defineProperty(exports, "ThreadType", { enumerable: true, get: function () { return normalizer_1.ThreadType; } });
|
|
20
|
+
Object.defineProperty(exports, "MessageType", { enumerable: true, get: function () { return normalizer_1.MessageType; } });
|
|
21
|
+
Object.defineProperty(exports, "TriggerKind", { enumerable: true, get: function () { return normalizer_1.TriggerKind; } });
|
|
22
|
+
Object.defineProperty(exports, "normalizeIssueOpened", { enumerable: true, get: function () { return normalizer_1.normalizeIssueOpened; } });
|
|
23
|
+
Object.defineProperty(exports, "normalizeIssueComment", { enumerable: true, get: function () { return normalizer_1.normalizeIssueComment; } });
|
|
24
|
+
Object.defineProperty(exports, "normalizePullRequestOpened", { enumerable: true, get: function () { return normalizer_1.normalizePullRequestOpened; } });
|
|
25
|
+
Object.defineProperty(exports, "normalizePullRequestReview", { enumerable: true, get: function () { return normalizer_1.normalizePullRequestReview; } });
|
|
26
|
+
Object.defineProperty(exports, "normalizePullRequestReviewComment", { enumerable: true, get: function () { return normalizer_1.normalizePullRequestReviewComment; } });
|
|
27
|
+
Object.defineProperty(exports, "normalizeDiscussionCreated", { enumerable: true, get: function () { return normalizer_1.normalizeDiscussionCreated; } });
|
|
28
|
+
Object.defineProperty(exports, "normalizeDiscussionComment", { enumerable: true, get: function () { return normalizer_1.normalizeDiscussionComment; } });
|
|
29
|
+
Object.defineProperty(exports, "normalizeCheckRun", { enumerable: true, get: function () { return normalizer_1.normalizeCheckRun; } });
|
|
30
|
+
Object.defineProperty(exports, "normalizeWorkflowRun", { enumerable: true, get: function () { return normalizer_1.normalizeWorkflowRun; } });
|
|
31
|
+
var outbound_1 = require("./src/outbound/outbound");
|
|
32
|
+
Object.defineProperty(exports, "OutboundSender", { enumerable: true, get: function () { return outbound_1.OutboundSender; } });
|
|
33
|
+
var routing_1 = require("./src/routing/routing");
|
|
34
|
+
Object.defineProperty(exports, "sessionKey", { enumerable: true, get: function () { return routing_1.sessionKey; } });
|
|
35
|
+
Object.defineProperty(exports, "evaluateTrigger", { enumerable: true, get: function () { return routing_1.evaluateTrigger; } });
|
|
36
|
+
Object.defineProperty(exports, "evaluateAutoTrigger", { enumerable: true, get: function () { return routing_1.evaluateAutoTrigger; } });
|
|
37
|
+
Object.defineProperty(exports, "isBotSender", { enumerable: true, get: function () { return routing_1.isBotSender; } });
|
|
38
|
+
Object.defineProperty(exports, "hasOutboundMarker", { enumerable: true, get: function () { return routing_1.hasOutboundMarker; } });
|
|
39
|
+
var state_1 = require("./src/state/state");
|
|
40
|
+
Object.defineProperty(exports, "Store", { enumerable: true, get: function () { return state_1.Store; } });
|
|
41
|
+
var server_1 = require("./src/server/server");
|
|
42
|
+
Object.defineProperty(exports, "WebhookHandler", { enumerable: true, get: function () { return server_1.WebhookHandler; } });
|
|
43
|
+
Object.defineProperty(exports, "createServer", { enumerable: true, get: function () { return server_1.createServer; } });
|
|
44
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":";AAAA,kFAAkF;;;AAElF,8CAAmF;AAA1E,sGAAA,YAAY,OAAA;AAAE,+FAAA,KAAK,OAAA;AAAE,kGAAA,QAAQ,OAAA;AAAE,uGAAA,aAAa,OAAA;AAGrD,wCAAqE;AAA5D,kGAAA,UAAU,OAAA;AAAE,8GAAA,sBAAsB,OAAA;AAE3C,8CAAoE;AAA3D,mGAAA,SAAS,OAAA;AAAE,gGAAA,MAAM,OAAA;AAAE,oGAAA,UAAU,OAAA;AAatC,0DAcqC;AAbnC,sGAAA,QAAQ,OAAA;AACR,wGAAA,UAAU,OAAA;AACV,yGAAA,WAAW,OAAA;AACX,yGAAA,WAAW,OAAA;AACX,kHAAA,oBAAoB,OAAA;AACpB,mHAAA,qBAAqB,OAAA;AACrB,wHAAA,0BAA0B,OAAA;AAC1B,wHAAA,0BAA0B,OAAA;AAC1B,+HAAA,iCAAiC,OAAA;AACjC,wHAAA,0BAA0B,OAAA;AAC1B,wHAAA,0BAA0B,OAAA;AAC1B,+GAAA,iBAAiB,OAAA;AACjB,kHAAA,oBAAoB,OAAA;AAItB,oDAAyD;AAAhD,0GAAA,cAAc,OAAA;AAEvB,iDAAyH;AAAhH,qGAAA,UAAU,OAAA;AAAE,0GAAA,eAAe,OAAA;AAAE,8GAAA,mBAAmB,OAAA;AAAE,sGAAA,WAAW,OAAA;AAAE,4GAAA,iBAAiB,OAAA;AAGzF,2CAA0C;AAAjC,8FAAA,KAAK,OAAA;AAEd,8CAAmE;AAA1D,wGAAA,cAAc,OAAA;AAAE,sGAAA,YAAY,OAAA"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare class GitHubAuth {
|
|
2
|
+
private appId;
|
|
3
|
+
private privateKey;
|
|
4
|
+
private installationToken;
|
|
5
|
+
private tokenExpiry;
|
|
6
|
+
constructor(appId: number, privateKeyPEM: string);
|
|
7
|
+
/** Create a short-lived JWT for GitHub App authentication (valid for 10 minutes). */
|
|
8
|
+
generateJWT(): string;
|
|
9
|
+
/** Cache an installation token with its expiry time. */
|
|
10
|
+
setInstallationToken(token: string, expiry: Date): void;
|
|
11
|
+
/** Return the cached installation token if still valid (with 1-min buffer). */
|
|
12
|
+
getInstallationToken(): string;
|
|
13
|
+
}
|
|
14
|
+
/** Verify X-Hub-Signature-256 header against the payload using HMAC-SHA256. */
|
|
15
|
+
export declare function verifyWebhookSignature(payload: Buffer | string, secret: string, signature: string): boolean;
|
|
16
|
+
/** Extract X-Hub-Signature-256 header from request headers. */
|
|
17
|
+
export declare function extractSignature(headers: Record<string, string | string[] | undefined>): string;
|
|
18
|
+
/** Extract X-GitHub-Delivery header from request headers. */
|
|
19
|
+
export declare function extractDeliveryID(headers: Record<string, string | string[] | undefined>): string;
|
|
20
|
+
/** Extract X-GitHub-Event header from request headers. */
|
|
21
|
+
export declare function extractEventType(headers: Record<string, string | string[] | undefined>): string;
|
|
22
|
+
//# sourceMappingURL=auth.d.ts.map
|