-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathguoba.support.js
More file actions
259 lines (244 loc) · 9.91 KB
/
Copy pathguoba.support.js
File metadata and controls
259 lines (244 loc) · 9.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
/*
* 锅巴(Guoba-Plugin)WebUI 配置支持
* 参照 Guoba-Template-Plugin 实现:命名导出 supportGuoba() 自动接入,
* getConfigData / setConfigData 读写 config.json(components/config.js)。
* 保存后经 __webhookPluginInstance.reloadService() 立即应用挂载方式变更;
* 接口规则因短 TTL 缓存约 2 秒内生效。
*/
import { readConfig, writeConfig } from './components/config.js'
const METHOD_OPTIONS = [
{ label: 'POST(默认)', value: 'post' },
{ label: 'GET', value: 'get' },
{ label: 'PUT', value: 'put' },
{ label: 'DELETE', value: 'delete' },
{ label: 'PATCH', value: 'patch' },
{ label: '任意方式(ANY)', value: 'any' }
]
const MODE_OPTIONS = [
{ label: '自动:优先云崽 HTTP 服务,不可用时独立端口(推荐)', value: 'auto' },
{ label: '复用云崽 HTTP 服务(默认50831)', value: 'express' },
{ label: '独立 HTTP 服务(自定义端口)', value: 'standalone' }
]
/** 请求头校验对象 ↔ 文本(每行一个「头名: 值」) */
function headersToText (map) {
return Object.entries(map || {}).map(([k, v]) => `${k}: ${v}`).join('\n')
}
function textToHeaders (text) {
const map = {}
for (const line of String(text || '').split('\n')) {
const idx = line.indexOf(':')
if (idx < 0) continue
const name = line.slice(0, idx).trim()
const value = line.slice(idx + 1).trim()
if (name) map[name] = value
}
return map
}
/**
* 发送目标 ↔ 文本(每行一个):[转发][私聊|群聊]<号码>,纯数字默认私聊。
* 刻意用文本行而非嵌套 GSubForm:锅巴 SubForm 组件嵌套时会重复触发外层 onSubmit,
* 导致「新增目标时生成多条相同配置」的 bug;文本行绕开该缺陷,config.json 结构不变。
*/
function targetsToText (list) {
return (Array.isArray(list) ? list : [])
.map(t => `${t.forward ? '转发' : ''}${t.type === 'group' ? '群聊' : '私聊'} ${String(t.id ?? '').trim()}`)
.filter(line => /\d/.test(line))
.join('\n')
}
function textToTargets (text) {
const targets = []
for (const line of String(text || '').split('\n')) {
const s = line.trim()
if (!s) continue
let m = s.match(/^(转发|forward|f|zf)?\s*(群聊|群|group|g)\s*[::]?\s*(\d+)$/i)
if (m) { targets.push({ type: 'group', id: m[3], forward: !!m[1] }); continue }
m = s.match(/^(转发|forward|f|zf)?\s*(私聊|私|private|p)\s*[::]?\s*(\d+)$/i)
if (m) { targets.push({ type: 'private', id: m[3], forward: !!m[1] }); continue }
m = s.match(/^(转发|forward|f|zf)?\s*(\d+)$/i)
if (m) targets.push({ type: 'private', id: m[2], forward: !!m[1] })
}
return targets
}
export function supportGuoba () {
return {
// 插件信息(显示在锅巴 Web 端菜单)
pluginInfo: {
name: 'webhook-plugin',
title: 'Webhook 插件',
description: '自定义 HTTP 接口(路径/请求方式/消息模板/发送目标),收到请求后向指定 QQ 发送消息',
author: '@githubmof',
authorLink: 'https://github.com/githubmof',
link: 'https://github.com/githubmof/webhook-plugin',
isV3: true,
isV2: false,
// 配置项 >= 3 个时自动显示在左侧菜单
showInMenu: 'auto',
icon: 'mdi:webhook',
iconColor: '#2f855a'
},
// 配置项信息(WebUI 表单)
configInfo: {
schemas: [
{ label: '服务配置', component: 'SOFT_GROUP_BEGIN' },
{
field: 'enabled',
label: '启用服务',
bottomHelpMessage: '关闭后所有接口返回 503,云崽自身功能不受影响',
component: 'Switch'
},
{
field: 'mode',
label: '挂载方式',
bottomHelpMessage: 'auto:优先复用云崽 HTTP 服务(与云崽登录页同端口);不可用时自动回退独立端口',
component: 'Select',
componentProps: { options: MODE_OPTIONS }
},
{
field: 'host',
label: '独立服务监听地址',
bottomHelpMessage: '仅「独立 HTTP 服务」生效',
component: 'Input',
componentProps: { placeholder: '0.0.0.0' }
},
{
field: 'port',
label: '独立服务端口',
bottomHelpMessage: '仅「独立 HTTP 服务」生效',
component: 'InputNumber',
componentProps: { min: 1, max: 65535, placeholder: '5788' }
},
{
field: 'token',
label: '全局校验 Token',
bottomHelpMessage: '非空时每个请求必须带请求头 X-Webhook-Token: <Token>(GET 带 ?token=<Token>)',
component: 'Input',
componentProps: { placeholder: '留空不校验' }
},
{ label: '转发规则', component: 'SOFT_GROUP_BEGIN' },
{
field: 'rules',
label: '接口规则列表',
bottomHelpMessage: '一条规则对应一个自定义接口。保存后约 2 秒内生效,无需重启',
component: 'GSubForm',
componentProps: {
multiple: true,
maxRows: 50,
schemas: [
{
field: 'enabled',
label: '启用',
component: 'Switch'
},
{
field: 'path',
label: '接口路径',
required: true,
bottomHelpMessage: '须以 / 开头。完整访问地址 = 云崽 HTTP 地址 + 路径',
component: 'Input',
componentProps: { placeholder: '/webhook/test' }
},
{
field: 'method',
label: '请求方式',
component: 'Select',
componentProps: { options: METHOD_OPTIONS }
},
{
field: 'secretHeaders',
label: '请求头校验',
bottomHelpMessage: '每行一个「头名: 值」',
component: 'InputTextArea',
componentProps: { rows: 3, placeholder: 'X-Api-Key: 123456' }
},
{
field: 'template',
label: '消息模板',
bottomHelpMessage: '支持 {body.字段} {query.参数} {header.头名} {method} {path} {ip} {time} {raw} {json},留空默认输出完整请求体',
component: 'InputTextArea',
componentProps: {
rows: 6,
placeholder: '收到新推送\n仓库:{body.repository.full_name}\n提交者:{body.sender.login}'
}
},
{
field: 'targets',
label: '发送目标',
bottomHelpMessage: '每行一个目标:[转发][私聊|群聊]<号码>',
component: 'InputTextArea',
componentProps: {
rows: 4,
placeholder: '10001\n群聊 20002\n转发私聊 10003'
}
},
{
field: 'responseBody',
label: '自定义响应体',
bottomHelpMessage: '返回给调用方的内容,支持占位符;留空返回 {"code":0,"message":"ok"}',
component: 'InputTextArea',
componentProps: { rows: 2, placeholder: '{"code":0,"received":{"name":"{body.name}"}}' }
},
{
field: 'responseStatus',
label: '响应状态码',
component: 'InputNumber',
componentProps: { min: 100, max: 599, placeholder: '200' }
}
]
}
}
],
// 读取配置(回填表单)
getConfigData () {
const cfg = readConfig()
return {
enabled: cfg.enabled !== false,
mode: ['auto', 'express', 'standalone'].includes(cfg.mode) ? cfg.mode : 'auto',
host: cfg.host || '0.0.0.0',
port: Number(cfg.port) || 5788,
token: cfg.token || '',
rules: (Array.isArray(cfg.rules) ? cfg.rules : []).map(r => ({
enabled: r.enabled !== false,
path: r.path || '',
method: String(r.method || 'post').toLowerCase(),
secretHeaders: headersToText(r.secretHeaders),
template: r.template || '',
targets: targetsToText(r.targets),
responseBody: r.responseBody || '',
responseStatus: r.responseStatus || 200
}))
}
},
// 保存配置(写回 config.json)
setConfigData (data, { Result }) {
const rules = (Array.isArray(data.rules) ? data.rules : []).map(r => ({
enabled: r.enabled !== false,
path: String(r.path || '').trim(),
method: String(r.method || 'post').toLowerCase(),
secretHeaders: textToHeaders(r.secretHeaders),
template: String(r.template || ''),
targets: textToTargets(r.targets),
responseBody: String(r.responseBody || ''),
responseStatus: Number(r.responseStatus) || 200
})).filter(r => r.path)
const ret = writeConfig({
enabled: data.enabled !== false,
mode: ['auto', 'express', 'standalone'].includes(data.mode) ? data.mode : 'auto',
host: String(data.host || '0.0.0.0').trim(),
port: Number(data.port) || 5788,
token: String(data.token || '').trim(),
// 允许同 path+method 的重复规则:收到请求后按配置顺序依次执行
rules
})
if (!ret.ok) {
logger?.error?.('[webhook-plugin] 锅巴保存配置失败:', ret.error)
return Result.error(`保存失败:${ret.error}`)
}
// 立即应用挂载方式 / 端口等变更
try {
globalThis.__webhookPluginInstance?.reloadService?.()
} catch (e) { /* 忽略 */ }
return Result.ok({}, '保存成功~(接口增改约 2 秒内生效,重复接口会按顺序依次执行)')
}
}
}
}