koishi-plugin-qq-github-bridge 0.0.0-stage → 1.0.1
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/LICENSE +21 -0
- package/README.md +167 -2
- package/client/env.d.ts +5 -0
- package/client/index.ts +6 -0
- package/client/page.vue +164 -0
- package/client/tsconfig.json +15 -0
- package/dist/index.js +1 -0
- package/dist/style.css +1 -0
- package/docs/ARCHITECTURE.md +100 -0
- package/docs/RELEASING.md +68 -0
- package/docs/RESEARCH.md +37 -0
- package/docs/REVIEW.md +49 -0
- package/docs/TESTING.md +92 -0
- package/lib/config.d.ts +35 -0
- package/lib/config.js +99 -0
- package/lib/config.js.map +1 -0
- package/lib/console.d.ts +7 -0
- package/lib/console.js +73 -0
- package/lib/console.js.map +1 -0
- package/lib/database/models.d.ts +37 -0
- package/lib/database/models.js +21 -0
- package/lib/database/models.js.map +1 -0
- package/lib/handlers/commands.d.ts +28 -0
- package/lib/handlers/commands.js +184 -0
- package/lib/handlers/commands.js.map +1 -0
- package/lib/handlers/message.d.ts +4 -0
- package/lib/handlers/message.js +58 -0
- package/lib/handlers/message.js.map +1 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +44 -0
- package/lib/index.js.map +1 -0
- package/lib/rpc.d.ts +8 -0
- package/lib/rpc.js +3 -0
- package/lib/rpc.js.map +1 -0
- package/lib/services/github.d.ts +22 -0
- package/lib/services/github.js +201 -0
- package/lib/services/github.js.map +1 -0
- package/lib/services/permission.d.ts +12 -0
- package/lib/services/permission.js +56 -0
- package/lib/services/permission.js.map +1 -0
- package/lib/services/repository.d.ts +8 -0
- package/lib/services/repository.js +22 -0
- package/lib/services/repository.js.map +1 -0
- package/lib/services/storage.d.ts +13 -0
- package/lib/services/storage.js +75 -0
- package/lib/services/storage.js.map +1 -0
- package/lib/types.d.ts +93 -0
- package/lib/types.js +3 -0
- package/lib/types.js.map +1 -0
- package/lib/utils/errors.d.ts +8 -0
- package/lib/utils/errors.js +28 -0
- package/lib/utils/errors.js.map +1 -0
- package/lib/utils/lock.d.ts +11 -0
- package/lib/utils/lock.js +38 -0
- package/lib/utils/lock.js.map +1 -0
- package/lib/utils/parser.d.ts +9 -0
- package/lib/utils/parser.js +87 -0
- package/lib/utils/parser.js.map +1 -0
- package/package.json +100 -4
- package/src/config.ts +111 -0
- package/src/console.ts +65 -0
- package/src/database/models.ts +45 -0
- package/src/handlers/commands.ts +177 -0
- package/src/handlers/message.ts +60 -0
- package/src/index.ts +37 -0
- package/src/rpc.ts +10 -0
- package/src/services/github.ts +189 -0
- package/src/services/permission.ts +47 -0
- package/src/services/repository.ts +22 -0
- package/src/services/storage.ts +67 -0
- package/src/types.ts +75 -0
- package/src/utils/errors.ts +18 -0
- package/src/utils/lock.ts +30 -0
- package/src/utils/parser.ts +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 QQ GitHub Bridge contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,168 @@
|
|
|
1
|
-
#
|
|
1
|
+
# koishi-plugin-qq-github-bridge
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/ClozyA/koishi-plugin-qq-github-bridge/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/koishi-plugin-qq-github-bridge)
|
|
5
|
+
|
|
6
|
+
通过 QQ 群评论、关闭、重开 GitHub Issue / Pull Request。使用部署者统一配置的 PAT、群与仓库绑定、独立 QQ 管理员体系和 Koishi 数据库,带官方控制台扩展。
|
|
7
|
+
|
|
8
|
+
## 功能
|
|
9
|
+
|
|
10
|
+
- `re1003 正文` 发布 Issue / PR 普通会话评论,支持 Markdown、多行和中英文,返回评论链接。
|
|
11
|
+
- `close1003` / `open1003` 修改 Issue 或 PR 状态;识别已合并 PR 和无需变更的状态。
|
|
12
|
+
- QQ 群绑定唯一仓库,多个群可共享仓库。目标只来自后台绑定;消息里的链接不能切换仓库。
|
|
13
|
+
- 超级管理员由 Schema 配置;普通管理员可递归授权。只有超级管理员可在 QQ 撤销管理员。
|
|
14
|
+
- 后台管理页面实时读取、添加和删除数据库中的管理员,显示添加者、时间、审计及 Token 检查。
|
|
15
|
+
- 基于事件标识的数据库去重、跨重启冷却、有限只读重试和写入结果不确定处理。
|
|
16
|
+
- 通过 RE2 执行自定义正则,避免回溯型正则阻塞事件循环。
|
|
17
|
+
|
|
18
|
+
不实现 PR 合并、Review Comment、OAuth、Webhook、跨平台机器人或删除 GitHub 内容。
|
|
19
|
+
|
|
20
|
+
## 运行条件
|
|
21
|
+
|
|
22
|
+
| 组件 | 要求 / 本次验证版本 |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| Node.js | >= 22;2026-10-26 本地复核使用 24.15.0 |
|
|
25
|
+
| Koishi | ^4.18.11 |
|
|
26
|
+
| 数据库 | Koishi 持久数据库;实际验证 `database-sqlite` 4.7.0 |
|
|
27
|
+
| HTTP | Koishi `http` 服务;实际验证 0.6.3 |
|
|
28
|
+
| QQ | OneBot v11 / NapCat 群消息,Koishi 平台名必须是 `onebot` |
|
|
29
|
+
| 后台页面 | `console` ^5.30.11 + `auth` ^4.1.7,使用已登录的部署管理员账号 |
|
|
30
|
+
|
|
31
|
+
本插件不安装或接管 QQ 适配器。事件解码集成测试覆盖官方 `@koishijs/plugin-adapter-onebot` 6.0.2 和社区版 `koishi-plugin-adapter-onebot` 6.9.4。官方 6.0.2 已被上游标记为停止官方支持。测试使用人工构造事件,**没有登录真实 QQ,也没有连接真实 NapCat**。实际连接后仍需按 [真实环境验收步骤](docs/TESTING.md) 检查字段及消息投递。
|
|
32
|
+
|
|
33
|
+
## 安装
|
|
34
|
+
|
|
35
|
+
在 **Koishi 应用目录** 安装 npm 包:
|
|
36
|
+
|
|
37
|
+
```powershell
|
|
38
|
+
npm install koishi-plugin-qq-github-bridge
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
也可以从 [GitHub Releases](https://github.com/ClozyA/koishi-plugin-qq-github-bridge/releases) 下载 `.tgz`,在应用目录安装:
|
|
42
|
+
|
|
43
|
+
```powershell
|
|
44
|
+
npm install "C:\你的下载目录\koishi-plugin-qq-github-bridge-1.0.0.tgz"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
重载 Koishi 后,在插件配置中添加 `qq-github-bridge`。npm 依赖更新后如果控制台尚未刷新可用插件列表,请重启该实例。插件市场收录后也可以搜索 `qq-github-bridge` 安装;npm 发布成功不等同于已确认市场收录。
|
|
48
|
+
|
|
49
|
+
首次启用在 Koishi 后台完成:
|
|
50
|
+
|
|
51
|
+
1. 启用 `http`、持久数据库,以及已连接的 OneBot 适配器。SQLite 只需本地数据库文件,不需要另开数据库服务。
|
|
52
|
+
2. 在本插件「GitHub 设置」填写统一 Token;默认 API 为 `https://api.github.com`。
|
|
53
|
+
3. 在「独立权限设置」填写至少一个超级管理员 QQ。填真实数字 QQ,不能填 Koishi 用户 ID、昵称或 QQ UID。
|
|
54
|
+
4. 在「仓库绑定」添加群号、Owner、仓库名并启用。例:`345678901` → `example/project`。
|
|
55
|
+
5. 启用官方 `console` 与 `auth`,以部署管理员登录,打开左侧 **QQ GitHub Bridge**。
|
|
56
|
+
6. 使用「检查连接」验证 Token 身份与选定仓库的可读性。写权限仍需按下文配置。
|
|
57
|
+
|
|
58
|
+
普通使用者不需要 GitHub 账号或 Token。所有 GitHub 操作显示为 Token 所属账号执行;评论固定身份头和操作审计记录真实 QQ 操作者。
|
|
59
|
+
|
|
60
|
+
## GitHub Token
|
|
61
|
+
|
|
62
|
+
推荐 Fine-grained PAT:GitHub Settings → Developer settings → Personal access tokens → Fine-grained tokens。
|
|
63
|
+
|
|
64
|
+
1. 选择仓库所属的 Resource owner。
|
|
65
|
+
2. 仅选择要绑定的仓库。
|
|
66
|
+
3. Repository permissions 设置 **Issues: Read and write**、**Pull requests: Read and write**;Metadata 采用 GitHub 要求的只读权限。
|
|
67
|
+
4. 设置合理有效期;组织要求审批时,等待审批后再使用。Token 所属账号本身也必须有目标操作权限。
|
|
68
|
+
|
|
69
|
+
Fine-grained PAT 限定单个资源所有者,不能用一个这样的 Token 同时覆盖多个组织。多个仓库属于同一组织时可统一选择;确实需要跨组织且组织策略允许时,可以使用拥有适当权限的 classic PAT(公开仓库通常使用 `public_repo`,私有仓库需 `repo`)。第一版仍然只有一个 Token,不能自动突破 GitHub 的授权边界。[GitHub 官方 Token 说明](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)
|
|
70
|
+
|
|
71
|
+
PR 的普通评论使用 Issues comments 接口,状态变更使用对应的 Issues / Pull requests 接口。[评论接口](https://docs.github.com/en/rest/issues/comments#create-an-issue-comment)、[Issue 状态接口](https://docs.github.com/en/rest/issues/issues#update-an-issue)、[PR 状态接口](https://docs.github.com/en/rest/pulls/pulls#update-a-pull-request)
|
|
72
|
+
|
|
73
|
+
不要把 Token 发到 QQ。插件配置使用 secret 输入框;管理页面、审计和对外错误不会返回 Token。部署者仍需保护 Koishi 配置文件、备份和控制台登录账户。
|
|
74
|
+
|
|
75
|
+
## QQ 指令与角色
|
|
76
|
+
|
|
77
|
+
以下指令必须在已启用绑定的 QQ 群内发送,且必须是纯文本消息。
|
|
78
|
+
|
|
79
|
+
| 指令 | 可使用角色 |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `re1003 这个问题已经修复。` | 所有绑定群成员 |
|
|
82
|
+
| `close1003` | 管理员、超级管理员 |
|
|
83
|
+
| `open1003` | 管理员、超级管理员 |
|
|
84
|
+
| `ghadmin add 123456789` | 管理员、超级管理员 |
|
|
85
|
+
| `ghadmin remove 123456789` | 仅超级管理员 |
|
|
86
|
+
| `ghadmin list` / `ghadmin list 2` | 管理员、超级管理员 |
|
|
87
|
+
| `ghadmin confirm 确认码` | 原操作人,在原群确认 |
|
|
88
|
+
|
|
89
|
+
默认关闭、重开、授权和撤权要求二次确认。机器人给出随机确认码,有效期 60 秒;同一人在同一群发起新操作会替换旧确认。确认时再次检查权限,撤权后旧确认失效。取消可等待过期;插件重载会清空确认。
|
|
90
|
+
|
|
91
|
+
QQ 权限从真实 OneBot 用户 ID 与本插件数据库判断,**不会读取 QQ 用户的 Koishi `authority`**。群主、QQ群管理员、机器人其他插件的管理员均不会自动获得本插件权限。权限跨所有绑定群生效。撤销一名管理员不会级联撤销其曾经授权的人。
|
|
92
|
+
|
|
93
|
+
后台维护使用官方控制台登录机制,限制到等级 5 的部署管理员;这是保护部署配置的网页访问门槛,不会授予其关联 QQ 任何额外权限。后台每次操作重新检查当前账户等级、Token 会话及过期时间。`auth` 停用后管理接口移除。后台变更记录 `console:账户ID`;如果账户绑定了 OneBot QQ,审计同时记该 QQ,未绑定时不编造 QQ。
|
|
94
|
+
|
|
95
|
+
## 匹配、正文和模板
|
|
96
|
+
|
|
97
|
+
默认规则:
|
|
98
|
+
|
|
99
|
+
```regex
|
|
100
|
+
^re(?<number>\d+)\s+(?<body>[\s\S]+)$
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
可以修改 `replyPrefix`;填写 `replyPattern` 后,回复前缀仅在清空自定义正则后生效。管理员指令和状态指令优先于自定义评论匹配。
|
|
104
|
+
|
|
105
|
+
自定义规则必须以 `^` 开始、`$` 结束,且声明 `(?<number>...)` 与 `(?<body>...)`;使用 RE2、固定 `u` 模式,不写 `/.../flags`。不支持回溯引用和前后查找。最多 1024 字符,构造失败时拒绝启用。编号必须为可精确表示的正整数;全文匹配再次校验,防止分支和末尾换行绕过边界。
|
|
106
|
+
|
|
107
|
+
例如:
|
|
108
|
+
|
|
109
|
+
```regex
|
|
110
|
+
^回复 #(?<number>\d+):(?<body>[\s\S]+)$
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`回复 #1003:正文` 就会发布评论。捕获到的 `body` 保留原文,包括 Markdown、换行和末尾空格;默认规则的 `\s+` 是指令分隔符,想保留正文起始空白时可采用以上冒号规则。
|
|
114
|
+
|
|
115
|
+
评论模板仅支持一次 `{body}`。昵称、QQ、群号由插件生成固定身份头,昵称会转义并移除控制字符,不与正文混排。正文不做 Markdown 转义、不改写提及;它以 GitHub 机器人账号发布,所以正文里的 `@用户名` 会遵循 GitHub 的通知行为。模板后附内部去重标记,便于人工核查超时结果。
|
|
116
|
+
|
|
117
|
+
第一版不转存图片、语音、文件,不自动剥离 @ 或引用消息。含非文本元素、引用、匿名消息、私聊、QQ 频道、未绑定群、机器人自身及同实例其他 OneBot 机器人消息均忽略。没有可靠消息 ID 或时间戳的事件也忽略。
|
|
118
|
+
|
|
119
|
+
## 配置与运维
|
|
120
|
+
|
|
121
|
+
所有日常配置通过插件 Schema 表单修改,保存后按 Koishi 生命周期重新加载,无需重新安装。数据库管理员立即生效,无需重载。
|
|
122
|
+
|
|
123
|
+
| 设置 | 默认值 | 含义 |
|
|
124
|
+
| --- | --- | --- |
|
|
125
|
+
| `apiBaseUrl` | `https://api.github.com` | 企业版可填 `https://主机/api/v3`;仅本机测试允许 HTTP |
|
|
126
|
+
| `apiVersion` | `2026-03-10` | 企业版需改为其支持的版本 |
|
|
127
|
+
| `requestTimeout` | 15000 ms | 单个 API 请求超时 |
|
|
128
|
+
| `readRetries` | 2 | 网络 / 5xx 的额外只读重试次数 |
|
|
129
|
+
| `userCooldown` | 5000 ms | 每用户跨群共享,保存在数据库 |
|
|
130
|
+
| `maxCommentLength` | 6000 | UTF-16 字符数;渲染后另受 65000 字节上限约束 |
|
|
131
|
+
| `allowMultiline` | `true` | 允许多行正文 |
|
|
132
|
+
| `confirmHighRisk` | `true` | QQ 高风险操作确认;后台也有明确确认步骤 |
|
|
133
|
+
| `confirmationTimeout` | 60000 ms | QQ 确认码有效期 |
|
|
134
|
+
| `enableAdminCommands` | `true` | 关闭后禁止 QQ 新增 / 撤权 / 列表;状态管理仍按管理员权限判断 |
|
|
135
|
+
| `auditEnabled` | `true` | 操作审计;不记录昵称、评论正文或 Token |
|
|
136
|
+
| `auditRetentionDays` | 30 | 每小时清理,关闭审计后也清理过期历史 |
|
|
137
|
+
| `dedupWindow` | 86400000 ms | 24 小时去重;过期历史事件不执行 |
|
|
138
|
+
|
|
139
|
+
错误处理、数据备份、生命周期与已知边界见 [架构与可靠性](docs/ARCHITECTURE.md)。
|
|
140
|
+
|
|
141
|
+
## 开发、验证与发布
|
|
142
|
+
|
|
143
|
+
```powershell
|
|
144
|
+
npm ci
|
|
145
|
+
npm run check
|
|
146
|
+
npm run check:package
|
|
147
|
+
npm pack
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`check` 分别执行后端与前端 TypeScript 检查、构建和自动化测试。前端使用 Koishi 官方 `@koishijs/client` 构建器;由于该版本直接导出 TS/Vue 源码且未包含声明的 `global.d.ts`,检查脚本先从已安装 SDK 生成声明,再对本插件客户端严格检查,不手写 SDK 类型桩。
|
|
151
|
+
|
|
152
|
+
`npm run preview` 启动仅监听 `127.0.0.1` 的开发预览,使用内存数据库、模拟 GitHub 和固定的本地测试账户。此脚本不是生产启动器,不连接真实 QQ 或 GitHub。源码预览使用官方 devMode;生产环境应安装 `.tgz` 或 npm 包,使控制台能从 `node_modules` 加载编译资源。
|
|
153
|
+
|
|
154
|
+
`.tgz` 包含 `lib/`、`dist/`、`src/`、`client/`、文档与许可证,兼容已安装插件的开发模式与源码调试;完整源码交付另外包含测试、锁文件和构建脚本。Koishi 是 peer dependency,唯一直接运行依赖为 `re2-wasm`。
|
|
155
|
+
|
|
156
|
+
CI 在 Linux(Node 22 / 24)和 Windows(Node 24)执行静态检查、构建、126 项自动化测试和实际安装包加载检查。
|
|
157
|
+
|
|
158
|
+
维护者发布新版本时,在 PowerShell 7(`pwsh`)运行:
|
|
159
|
+
|
|
160
|
+
```powershell
|
|
161
|
+
npm version patch
|
|
162
|
+
git push origin main
|
|
163
|
+
git push origin --tags
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
推送 `v*` 标签后,Release workflow 校验版本及 main 分支归属,重新检查并生成 `.tgz` 和 SHA256,创建 GitHub Release,再通过 npm OIDC 可信发布上传同一安装包并生成来源证明。首次 npm 发布及可信发布关系需要维护者初始化;具体配置、开关和失败重试见 [发布说明](docs/RELEASING.md)。
|
|
167
|
+
|
|
168
|
+
[需求复核结论](docs/REVIEW.md)明确区分源码符合程度、自动化验证及尚缺的真实环境验收。[历史测试报告](docs/TESTING.md)保留原始交付的验证范围;官方资料与技术选择见 [调研记录](docs/RESEARCH.md)。
|
package/client/env.d.ts
ADDED
package/client/index.ts
ADDED
package/client/page.vue
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<k-layout>
|
|
3
|
+
<div class="qgh-page">
|
|
4
|
+
<header class="qgh-heading">
|
|
5
|
+
<div><h1>QQ GitHub Bridge</h1><p>管理跨群共享的管理员权限,查看操作记录。</p></div>
|
|
6
|
+
<button :disabled="busy" @click="refresh">刷新数据</button>
|
|
7
|
+
</header>
|
|
8
|
+
<p v-if="notice" class="qgh-notice" role="status">{{ notice }}</p>
|
|
9
|
+
<p v-if="error" class="qgh-error" role="alert">{{ error }}</p>
|
|
10
|
+
<p v-if="!dashboard && !error">正在读取后台数据……</p>
|
|
11
|
+
<template v-if="dashboard">
|
|
12
|
+
<section class="qgh-card">
|
|
13
|
+
<h2>超级管理员</h2>
|
|
14
|
+
<p>{{ dashboard.superAdmins.join('、') }}</p>
|
|
15
|
+
<p class="qgh-muted">超级管理员、仓库绑定、Token 和安全选项在插件配置页修改。此处的普通管理员与 QQ 指令使用同一份数据库。</p>
|
|
16
|
+
</section>
|
|
17
|
+
<section class="qgh-card">
|
|
18
|
+
<h2>普通管理员</h2>
|
|
19
|
+
<form class="qgh-actions" @submit.prevent="requestManage('add', qq)">
|
|
20
|
+
<label for="qgh-qq">QQ 号</label>
|
|
21
|
+
<input id="qgh-qq" v-model="qq" inputmode="numeric" pattern="[1-9][0-9]{4,19}" maxlength="20" placeholder="例如 123456789" required>
|
|
22
|
+
<button :disabled="busy">新增管理员</button>
|
|
23
|
+
</form>
|
|
24
|
+
<div v-if="pending" class="qgh-confirm" role="group" aria-label="确认管理员变更">
|
|
25
|
+
<p>确定{{ pending.action === 'add' ? '添加' : '撤销' }}管理员 <strong>{{ pending.qq }}</strong>?权限在所有绑定群中生效。</p>
|
|
26
|
+
<div class="qgh-actions">
|
|
27
|
+
<button :disabled="busy" @click="confirmManage">{{ pending.action === 'add' ? '确认新增' : '确认撤销' }}</button>
|
|
28
|
+
<button :disabled="busy" @click="pending = undefined">取消</button>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="qgh-scroll"><table>
|
|
32
|
+
<thead><tr><th>QQ 号</th><th>添加者</th><th>添加时间</th><th>操作</th></tr></thead>
|
|
33
|
+
<tbody>
|
|
34
|
+
<tr v-for="admin in dashboard.admins" :key="admin.qq">
|
|
35
|
+
<td>{{ admin.qq }}</td><td>{{ admin.addedBy }}</td><td>{{ time(admin.createdAt) }}</td>
|
|
36
|
+
<td><button :disabled="busy || dashboard.superAdmins.includes(admin.qq)" @click="requestManage('remove', admin.qq)">撤销</button></td>
|
|
37
|
+
</tr>
|
|
38
|
+
<tr v-if="!dashboard.admins.length"><td colspan="4">本页暂无普通管理员。</td></tr>
|
|
39
|
+
</tbody>
|
|
40
|
+
</table></div>
|
|
41
|
+
<div class="qgh-actions qgh-pagination">
|
|
42
|
+
<button :disabled="busy || page <= 1" @click="changePage(-1)">上一页</button><span>第 {{ page }} 页</span>
|
|
43
|
+
<button :disabled="busy || !dashboard.hasMoreAdmins" @click="changePage(1)">下一页</button>
|
|
44
|
+
</div>
|
|
45
|
+
</section>
|
|
46
|
+
<section class="qgh-card">
|
|
47
|
+
<h2>仓库绑定与 Token 检查</h2>
|
|
48
|
+
<div class="qgh-scroll"><table>
|
|
49
|
+
<thead><tr><th>QQ群</th><th>GitHub 仓库</th><th>状态</th></tr></thead>
|
|
50
|
+
<tbody><tr v-for="binding in dashboard.bindings" :key="binding.groupId">
|
|
51
|
+
<td>{{ binding.groupId }}</td><td>{{ binding.repository }}</td><td>{{ binding.enabled ? '已启用' : '已禁用' }}</td>
|
|
52
|
+
</tr><tr v-if="!dashboard.bindings.length"><td colspan="3">请先在插件配置页添加群绑定。</td></tr></tbody>
|
|
53
|
+
</table></div>
|
|
54
|
+
<div class="qgh-actions qgh-pagination">
|
|
55
|
+
<label for="qgh-repo">检查范围</label>
|
|
56
|
+
<select id="qgh-repo" v-model="checkGroup">
|
|
57
|
+
<option value="">仅验证 Token 身份</option>
|
|
58
|
+
<option v-for="binding in dashboard.bindings.filter(b => b.enabled)" :key="binding.groupId" :value="binding.groupId">{{ binding.repository }}({{ binding.groupId }})</option>
|
|
59
|
+
</select>
|
|
60
|
+
<button :disabled="busy" @click="validate">检查连接</button>
|
|
61
|
+
</div>
|
|
62
|
+
<div v-if="tokenStatus" role="status">
|
|
63
|
+
<p>执行 GitHub 操作的账号:{{ tokenStatus.login }}</p><p>{{ tokenStatus.message }}</p>
|
|
64
|
+
<p v-for="repo in tokenStatus.repositories" :key="repo.repository">{{ repo.repository }}:{{ repo.accessible ? '✓' : '✗' }} {{ repo.message }}</p>
|
|
65
|
+
</div>
|
|
66
|
+
</section>
|
|
67
|
+
<section class="qgh-card">
|
|
68
|
+
<h2>最近操作</h2>
|
|
69
|
+
<p class="qgh-muted">{{ dashboard.auditEnabled ? `审计已启用,保留 ${dashboard.auditRetentionDays} 天。` : '审计已关闭,以下为保留期内的历史记录。' }}显示最近 100 条,不包含 Token、昵称和评论正文。pending 表示结果尚未落库,unknown 表示 GitHub 写入结果不确定,请先核对再重试。</p>
|
|
70
|
+
<div class="qgh-scroll"><table>
|
|
71
|
+
<thead><tr><th>时间</th><th>操作者</th><th>操作 / 目标</th><th>群 / 仓库</th><th>结果</th></tr></thead>
|
|
72
|
+
<tbody><tr v-for="audit in dashboard.audits" :key="audit.id">
|
|
73
|
+
<td>{{ time(audit.createdAt) }}</td>
|
|
74
|
+
<td>{{ audit.source }}:{{ audit.actorId }}<span v-if="audit.source === 'console' && audit.actorQQ">(QQ {{ audit.actorQQ }})</span></td>
|
|
75
|
+
<td>{{ audit.action }} {{ audit.target }}</td><td>{{ audit.groupId || '后台' }}<br>{{ audit.repository }}</td>
|
|
76
|
+
<td>{{ audit.result }}<br><small>{{ audit.detail }}</small></td>
|
|
77
|
+
</tr><tr v-if="!dashboard.audits.length"><td colspan="5">暂无审计记录。</td></tr></tbody>
|
|
78
|
+
</table></div>
|
|
79
|
+
</section>
|
|
80
|
+
</template>
|
|
81
|
+
</div>
|
|
82
|
+
</k-layout>
|
|
83
|
+
</template>
|
|
84
|
+
|
|
85
|
+
<script setup lang="ts">
|
|
86
|
+
import { onMounted, ref } from 'vue'
|
|
87
|
+
import { send } from '@koishijs/client'
|
|
88
|
+
import type {} from '../src/rpc'
|
|
89
|
+
import type { Dashboard, TokenStatus } from '../src/types'
|
|
90
|
+
|
|
91
|
+
const dashboard = ref<Dashboard>()
|
|
92
|
+
const tokenStatus = ref<TokenStatus>()
|
|
93
|
+
const page = ref(1)
|
|
94
|
+
const qq = ref('')
|
|
95
|
+
const checkGroup = ref('')
|
|
96
|
+
const busy = ref(false)
|
|
97
|
+
const notice = ref('')
|
|
98
|
+
const error = ref('')
|
|
99
|
+
const pending = ref<{ action: 'add' | 'remove'; qq: string }>()
|
|
100
|
+
const time = (value: string) => new Date(value).toLocaleString()
|
|
101
|
+
|
|
102
|
+
async function run(task: () => Promise<void>) {
|
|
103
|
+
if (busy.value) return
|
|
104
|
+
busy.value = true
|
|
105
|
+
error.value = ''
|
|
106
|
+
notice.value = ''
|
|
107
|
+
try { await task() } catch (reason) {
|
|
108
|
+
error.value = typeof reason === 'string' ? reason : reason instanceof Error ? reason.message : '请求失败,请检查控制台连接并刷新。'
|
|
109
|
+
} finally { busy.value = false }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function load() {
|
|
113
|
+
const data = await send('qq-github-bridge/dashboard', page.value)
|
|
114
|
+
if (!data) throw new Error('控制台尚未连接,请连接后刷新。')
|
|
115
|
+
dashboard.value = data
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function refresh() { return run(load) }
|
|
119
|
+
function changePage(delta: number) { page.value += delta; return refresh() }
|
|
120
|
+
function requestManage(action: 'add' | 'remove', target: string) {
|
|
121
|
+
if (!/^[1-9][0-9]{4,19}$/.test(target)) { error.value = '请输入有效的数字 QQ 号。'; return }
|
|
122
|
+
pending.value = { action, qq: target }
|
|
123
|
+
}
|
|
124
|
+
function confirmManage() {
|
|
125
|
+
if (!pending.value || busy.value) return
|
|
126
|
+
const { action, qq: target } = pending.value
|
|
127
|
+
pending.value = undefined
|
|
128
|
+
return run(async () => {
|
|
129
|
+
const result = await send('qq-github-bridge/admin', action, target)
|
|
130
|
+
if (!result) throw new Error('控制台连接中断,结果未知,请刷新列表核对。')
|
|
131
|
+
notice.value = result
|
|
132
|
+
qq.value = ''
|
|
133
|
+
await load()
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
function validate() {
|
|
137
|
+
return run(async () => {
|
|
138
|
+
tokenStatus.value = await send('qq-github-bridge/validate', checkGroup.value || undefined)
|
|
139
|
+
if (!tokenStatus.value) throw new Error('控制台尚未连接。')
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
onMounted(refresh)
|
|
143
|
+
</script>
|
|
144
|
+
|
|
145
|
+
<style scoped>
|
|
146
|
+
.qgh-page { max-width: 1200px; margin: 0 auto; padding: 24px; color: var(--fg1); }
|
|
147
|
+
.qgh-heading { display: flex; align-items: center; justify-content: space-between; gap: 20px; margin-bottom: 24px; }
|
|
148
|
+
h1 { margin: 0; font-size: 25px; } h2 { font-size: 18px; margin: 0 0 16px; }
|
|
149
|
+
p { line-height: 1.65; } .qgh-muted, .qgh-heading p { opacity: .72; }
|
|
150
|
+
.qgh-card { border: 1px solid var(--k-color-divider, #8885); border-radius: 10px; padding: 22px; margin-bottom: 20px; background: var(--bg1); }
|
|
151
|
+
.qgh-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; margin-bottom: 18px; }
|
|
152
|
+
.qgh-pagination { margin-top: 18px; margin-bottom: 0; }
|
|
153
|
+
input, select, button { font: inherit; padding: 8px 12px; border: 1px solid #8887; border-radius: 6px; color: inherit; background: var(--bg2, transparent); }
|
|
154
|
+
button { cursor: pointer; } button:disabled { opacity: .45; cursor: not-allowed; }
|
|
155
|
+
button:hover:enabled { border-color: var(--k-color-primary, #777); }
|
|
156
|
+
input:focus-visible, select:focus-visible, button:focus-visible { outline: 2px solid var(--k-color-primary, #777); outline-offset: 2px; }
|
|
157
|
+
.qgh-scroll { overflow-x: auto; } table { border-collapse: collapse; width: 100%; text-align: left; }
|
|
158
|
+
th, td { padding: 11px 12px; border-bottom: 1px solid #8884; vertical-align: top; overflow-wrap: anywhere; }
|
|
159
|
+
th { white-space: nowrap; } .qgh-error, .qgh-notice { padding: 12px 16px; border-radius: 6px; white-space: pre-wrap; }
|
|
160
|
+
.qgh-error { background: #df3c3c18; border: 1px solid #df3c3c66; }
|
|
161
|
+
.qgh-notice { background: #3a957318; border: 1px solid #3a957366; }
|
|
162
|
+
.qgh-confirm { border: 1px solid #b38c36; background: #b38c3614; border-radius: 6px; padding: 0 16px; margin-bottom: 16px; }
|
|
163
|
+
@media (max-width: 650px) { .qgh-page { padding: 12px; } .qgh-card { padding: 16px; } .qgh-heading { align-items: flex-start; } select { max-width: 100%; } }
|
|
164
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"paths": {
|
|
7
|
+
"@koishijs/client": ["../.generated/sdk/@koishijs/client/client/index.d.ts"],
|
|
8
|
+
"@koishijs/components": ["../.generated/sdk/@koishijs/components/client/index.d.ts"],
|
|
9
|
+
"schemastery-vue": ["../.generated/sdk/schemastery-vue/src/index.d.ts"]
|
|
10
|
+
},
|
|
11
|
+
"types": ["node"],
|
|
12
|
+
"lib": ["ES2022", "DOM"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["./**/*.ts", "./**/*.vue", "../src/rpc.ts"]
|
|
15
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{defineComponent as T,ref as g,onMounted as A,resolveComponent as H,openBlock as o,createBlock as S,withCtx as V,createElementVNode as t,createElementBlock as s,toDisplayString as n,createCommentVNode as i,Fragment as q,withModifiers as D,withDirectives as $,vModelText as N,createTextVNode as b,renderList as y,vModelSelect as L}from"vue";import{send as C}from"@koishijs/client";const P={class:"qgh-page"},U={class:"qgh-heading"},j=["disabled"],F={key:0,class:"qgh-notice",role:"status"},O={key:1,class:"qgh-error",role:"alert"},R={key:2},z={class:"qgh-card"},J={class:"qgh-card"},K=["disabled"],W={key:0,class:"qgh-confirm",role:"group","aria-label":"确认管理员变更"},X={class:"qgh-actions"},Y=["disabled"],Z=["disabled"],tt={class:"qgh-scroll"},et=["disabled","onClick"],lt={key:0},nt={class:"qgh-actions qgh-pagination"},ot=["disabled"],st=["disabled"],ut={class:"qgh-card"},rt={class:"qgh-scroll"},at={key:0},it={class:"qgh-actions qgh-pagination"},dt=["value"],vt=["disabled"],gt={key:0,role:"status"},pt={class:"qgh-card"},ct={class:"qgh-muted"},ht={class:"qgh-scroll"},qt={key:0},bt={key:0},ft=T({__name:"page",setup(f){const u=g(),d=g(),p=g(1),c=g(""),_=g(""),a=g(false),k=g(""),h=g(""),v=g(),x=r=>new Date(r).toLocaleString();async function Q(r){if(!a.value){a.value=true,h.value="",k.value="";try{await r()}catch(e){h.value=typeof e=="string"?e:e instanceof Error?e.message:"请求失败,请检查控制台连接并刷新。"}finally{a.value=false}}}async function I(){const r=await C("qq-github-bridge/dashboard",p.value);if(!r)throw new Error("控制台尚未连接,请连接后刷新。");u.value=r}function w(){return Q(I)}function E(r){return p.value+=r,w()}function M(r,e){if(!/^[1-9][0-9]{4,19}$/.test(e)){h.value="请输入有效的数字 QQ 号。";return}v.value={action:r,qq:e}}function B(){if(!v.value||a.value)return;const{action:r,qq:e}=v.value;return v.value=void 0,Q(async()=>{const m=await C("qq-github-bridge/admin",r,e);if(!m)throw new Error("控制台连接中断,结果未知,请刷新列表核对。");k.value=m,c.value="",await I()})}function G(){return Q(async()=>{if(d.value=await C("qq-github-bridge/validate",_.value||void 0),!d.value)throw new Error("控制台尚未连接。")})}return A(w),(r,e)=>{const m=H("k-layout");return o(),S(m,null,{default:V(()=>[t("div",P,[t("header",U,[e[6]||(e[6]=t("div",null,[t("h1",null,"QQ GitHub Bridge"),t("p",null,"管理跨群共享的管理员权限,查看操作记录。")],-1)),t("button",{disabled:a.value,onClick:w},"刷新数据",8,j)]),k.value?(o(),s("p",F,n(k.value),1)):i("v-if",true),h.value?(o(),s("p",O,n(h.value),1)):i("v-if",true),!u.value&&!h.value?(o(),s("p",R,"正在读取后台数据……")):i("v-if",true),u.value?(o(),s(q,{key:3},[t("section",z,[e[7]||(e[7]=t("h2",null,"超级管理员",-1)),t("p",null,n(u.value.superAdmins.join("、")),1),e[8]||(e[8]=t("p",{class:"qgh-muted"},"超级管理员、仓库绑定、Token 和安全选项在插件配置页修改。此处的普通管理员与 QQ 指令使用同一份数据库。",-1))]),t("section",J,[e[13]||(e[13]=t("h2",null,"普通管理员",-1)),t("form",{class:"qgh-actions",onSubmit:e[1]||(e[1]=D(l=>M("add",c.value),["prevent"]))},[e[9]||(e[9]=t("label",{for:"qgh-qq"},"QQ 号",-1)),$(t("input",{id:"qgh-qq","onUpdate:modelValue":e[0]||(e[0]=l=>c.value=l),inputmode:"numeric",pattern:"[1-9][0-9]{4,19}",maxlength:"20",placeholder:"例如 123456789",required:""},null,512),[[N,c.value]]),t("button",{disabled:a.value},"新增管理员",8,K)],32),v.value?(o(),s("div",W,[t("p",null,[b("确定"+n(v.value.action==="add"?"添加":"撤销")+"管理员 ",1),t("strong",null,n(v.value.qq),1),e[10]||(e[10]=b("?权限在所有绑定群中生效。",-1))]),t("div",X,[t("button",{disabled:a.value,onClick:B},n(v.value.action==="add"?"确认新增":"确认撤销"),9,Y),t("button",{disabled:a.value,onClick:e[2]||(e[2]=l=>v.value=void 0)},"取消",8,Z)])])):i("v-if",true),t("div",tt,[t("table",null,[e[12]||(e[12]=t("thead",null,[t("tr",null,[t("th",null,"QQ 号"),t("th",null,"添加者"),t("th",null,"添加时间"),t("th",null,"操作")])],-1)),t("tbody",null,[(o(true),s(q,null,y(u.value.admins,l=>(o(),s("tr",{key:l.qq},[t("td",null,n(l.qq),1),t("td",null,n(l.addedBy),1),t("td",null,n(x(l.createdAt)),1),t("td",null,[t("button",{disabled:a.value||u.value.superAdmins.includes(l.qq),onClick:mt=>M("remove",l.qq)},"撤销",8,et)])]))),128)),u.value.admins.length?i("v-if",true):(o(),s("tr",lt,[...e[11]||(e[11]=[t("td",{colspan:"4"},"本页暂无普通管理员。",-1)])]))])])]),t("div",nt,[t("button",{disabled:a.value||p.value<=1,onClick:e[3]||(e[3]=l=>E(-1))},"上一页",8,ot),t("span",null,"第 "+n(p.value)+" 页",1),t("button",{disabled:a.value||!u.value.hasMoreAdmins,onClick:e[4]||(e[4]=l=>E(1))},"下一页",8,st)])]),t("section",ut,[e[18]||(e[18]=t("h2",null,"仓库绑定与 Token 检查",-1)),t("div",rt,[t("table",null,[e[15]||(e[15]=t("thead",null,[t("tr",null,[t("th",null,"QQ群"),t("th",null,"GitHub 仓库"),t("th",null,"状态")])],-1)),t("tbody",null,[(o(true),s(q,null,y(u.value.bindings,l=>(o(),s("tr",{key:l.groupId},[t("td",null,n(l.groupId),1),t("td",null,n(l.repository),1),t("td",null,n(l.enabled?"已启用":"已禁用"),1)]))),128)),u.value.bindings.length?i("v-if",true):(o(),s("tr",at,[...e[14]||(e[14]=[t("td",{colspan:"3"},"请先在插件配置页添加群绑定。",-1)])]))])])]),t("div",it,[e[17]||(e[17]=t("label",{for:"qgh-repo"},"检查范围",-1)),$(t("select",{id:"qgh-repo","onUpdate:modelValue":e[5]||(e[5]=l=>_.value=l)},[e[16]||(e[16]=t("option",{value:""},"仅验证 Token 身份",-1)),(o(true),s(q,null,y(u.value.bindings.filter(l=>l.enabled),l=>(o(),s("option",{key:l.groupId,value:l.groupId},n(l.repository)+"("+n(l.groupId)+")",9,dt))),128))],512),[[L,_.value]]),t("button",{disabled:a.value,onClick:G},"检查连接",8,vt)]),d.value?(o(),s("div",gt,[t("p",null,"执行 GitHub 操作的账号:"+n(d.value.login),1),t("p",null,n(d.value.message),1),(o(true),s(q,null,y(d.value.repositories,l=>(o(),s("p",{key:l.repository},n(l.repository)+":"+n(l.accessible?"✓":"✗")+" "+n(l.message),1))),128))])):i("v-if",true)]),t("section",pt,[e[23]||(e[23]=t("h2",null,"最近操作",-1)),t("p",ct,n(u.value.auditEnabled?`审计已启用,保留 ${u.value.auditRetentionDays} 天。`:"审计已关闭,以下为保留期内的历史记录。")+"显示最近 100 条,不包含 Token、昵称和评论正文。pending 表示结果尚未落库,unknown 表示 GitHub 写入结果不确定,请先核对再重试。",1),t("div",ht,[t("table",null,[e[22]||(e[22]=t("thead",null,[t("tr",null,[t("th",null,"时间"),t("th",null,"操作者"),t("th",null,"操作 / 目标"),t("th",null,"群 / 仓库"),t("th",null,"结果")])],-1)),t("tbody",null,[(o(true),s(q,null,y(u.value.audits,l=>(o(),s("tr",{key:l.id},[t("td",null,n(x(l.createdAt)),1),t("td",null,[b(n(l.source)+":"+n(l.actorId),1),l.source==="console"&&l.actorQQ?(o(),s("span",qt,"(QQ "+n(l.actorQQ)+")",1)):i("v-if",true)]),t("td",null,n(l.action)+" "+n(l.target),1),t("td",null,[b(n(l.groupId||"后台"),1),e[19]||(e[19]=t("br",null,null,-1)),b(n(l.repository),1)]),t("td",null,[b(n(l.result),1),e[20]||(e[20]=t("br",null,null,-1)),t("small",null,n(l.detail),1)])]))),128)),u.value.audits.length?i("v-if",true):(o(),s("tr",bt,[...e[21]||(e[21]=[t("td",{colspan:"5"},"暂无审计记录。",-1)])]))])])])])],64)):i("v-if",true)])]),_:1})}}}),yt=(f,u)=>{const d=f.__vccOpts||f;for(const[p,c]of u)d[p]=c;return d},kt=yt(ft,[["__scopeId","data-v-e8131405"]]),wt=f=>{f.page({name:"QQ GitHub Bridge",path:"/qq-github-bridge",authority:5,component:kt})};export{wt as default};
|
package/dist/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.qgh-page[data-v-e8131405]{max-width:1200px;margin:0 auto;padding:24px;color:var(--fg1)}.qgh-heading[data-v-e8131405]{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-bottom:24px}h1[data-v-e8131405]{margin:0;font-size:25px}h2[data-v-e8131405]{font-size:18px;margin:0 0 16px}p[data-v-e8131405]{line-height:1.65}.qgh-muted[data-v-e8131405],.qgh-heading p[data-v-e8131405]{opacity:.72}.qgh-card[data-v-e8131405]{border:1px solid var(--k-color-divider, #8885);border-radius:10px;padding:22px;margin-bottom:20px;background:var(--bg1)}.qgh-actions[data-v-e8131405]{display:flex;align-items:center;flex-wrap:wrap;gap:12px;margin-bottom:18px}.qgh-pagination[data-v-e8131405]{margin-top:18px;margin-bottom:0}input[data-v-e8131405],select[data-v-e8131405],button[data-v-e8131405]{font:inherit;padding:8px 12px;border:1px solid #8887;border-radius:6px;color:inherit;background:var(--bg2, transparent)}button[data-v-e8131405]{cursor:pointer}button[data-v-e8131405]:disabled{opacity:.45;cursor:not-allowed}button[data-v-e8131405]:hover:enabled{border-color:var(--k-color-primary, #777)}input[data-v-e8131405]:focus-visible,select[data-v-e8131405]:focus-visible,button[data-v-e8131405]:focus-visible{outline:2px solid var(--k-color-primary, #777);outline-offset:2px}.qgh-scroll[data-v-e8131405]{overflow-x:auto}table[data-v-e8131405]{border-collapse:collapse;width:100%;text-align:left}th[data-v-e8131405],td[data-v-e8131405]{padding:11px 12px;border-bottom:1px solid #8884;vertical-align:top;overflow-wrap:anywhere}th[data-v-e8131405]{white-space:nowrap}.qgh-error[data-v-e8131405],.qgh-notice[data-v-e8131405]{padding:12px 16px;border-radius:6px;white-space:pre-wrap}.qgh-error[data-v-e8131405]{background:#df3c3c18;border:1px solid #df3c3c66}.qgh-notice[data-v-e8131405]{background:#3a957318;border:1px solid #3a957366}.qgh-confirm[data-v-e8131405]{border:1px solid #b38c36;background:#b38c3614;border-radius:6px;padding:0 16px;margin-bottom:16px}@media (max-width: 650px){.qgh-page[data-v-e8131405]{padding:12px}.qgh-card[data-v-e8131405]{padding:16px}.qgh-heading[data-v-e8131405]{align-items:flex-start}select[data-v-e8131405]{max-width:100%}}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# 架构与可靠性
|
|
2
|
+
|
|
3
|
+
## 消息路径
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
Koishi 前置中间件
|
|
7
|
+
→ onebot 平台 / 群聊 / 原始 OneBot 字段交叉核验
|
|
8
|
+
→ 中央群绑定解析
|
|
9
|
+
→ 管理员、状态、确认、评论的优先级匹配
|
|
10
|
+
→ 有界串行队列
|
|
11
|
+
→ 持久化事件认领、冷却
|
|
12
|
+
→ 审计 pending、权限检查、必要时请求确认
|
|
13
|
+
→ GitHub 只读预检 → 一次写入
|
|
14
|
+
→ 更新审计 → QQ 纯文本结果
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
普通聊天和不支持的来源交给下游中间件。已识别命令与重复事件被消费。回复用 `h.text()`,不会把 URL、错误或数据库内容解释成 QQ 消息元素。
|
|
18
|
+
|
|
19
|
+
OneBot 传输端是身份信任边界。交叉核验原始 `user_id`、`sender.user_id`、Session 用户 ID、群号和机器人 ID;用户正文不能参与权限判断。OneBot 服务本身应由部署者正确保护,插件无法防御已被控制的适配器。
|
|
20
|
+
|
|
21
|
+
## 模块
|
|
22
|
+
|
|
23
|
+
| 模块 | 职责 |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `config.ts` | 中文 Schema、跨字段校验、敏感输入 |
|
|
26
|
+
| `handlers/message.ts` | OneBot 来源、身份、事件时间、文本提取 |
|
|
27
|
+
| `utils/parser.ts` | RE2 全文匹配、指令优先级、编号 / 正文校验 |
|
|
28
|
+
| `services/repository.ts` | 唯一群号→仓库入口 |
|
|
29
|
+
| `services/permission.ts` | 配置超级管理员与数据库普通管理员 |
|
|
30
|
+
| `services/storage.ts` | 事件认领、冷却、审计、保留期 |
|
|
31
|
+
| `services/github.ts` | Koishi HTTP、端点选择、错误映射、重试、Token 验证 |
|
|
32
|
+
| `handlers/commands.ts` | 队列、确认、操作与审计协调 |
|
|
33
|
+
| `console.ts` / `rpc.ts` / `client/` | 官方控制台 RPC、鉴权与 Vue 页面 |
|
|
34
|
+
|
|
35
|
+
## 数据模型 v1
|
|
36
|
+
|
|
37
|
+
所有表由 `ctx.model.extend()` 注册,使用 Koishi 数据库 API,不直接执行 SQL。
|
|
38
|
+
|
|
39
|
+
| 表 | 主键 | 数据 |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| `qgh_admin` | QQ 字符串 | 添加者、添加时间 |
|
|
42
|
+
| `qgh_event` | SHA-256(platform + 群号 + 消息 ID) | 过期时间 |
|
|
43
|
+
| `qgh_cooldown` | QQ 字符串 | 下次允许操作时间 |
|
|
44
|
+
| `qgh_audit` | UUID | 来源、操作者账号与 QQ、群、仓库、动作、目标、时间、结果、内部原因码 |
|
|
45
|
+
|
|
46
|
+
群绑定、超级管理员、Token 在 Koishi 插件配置中;普通管理员只有数据库一份真源。`addedBy` 使用真实 QQ 或 `console:ID`,不使用昵称。修改配置触发重新加载,数据库行不清空。
|
|
47
|
+
|
|
48
|
+
未来增加字段应使用默认值;重命名使用 Minato 的 `legacy` 迁移机制。不可直接改表名造成已有管理员丢失。停用与卸载不删除权限数据;迁移前备份 Koishi 配置和数据库。
|
|
49
|
+
|
|
50
|
+
## 并发与去重
|
|
51
|
+
|
|
52
|
+
单实例最多容纳 64 个正在执行 / 等待的命令,串行协调授权变更、权限检查和状态读写。相同仓库由不同群触发也不会在本实例内同时更新。管理员数据主键阻止重复授权。GitHub 写入之间至少间隔 1 秒。
|
|
53
|
+
|
|
54
|
+
事件执行前先持久化主键认领;重复投递和热重载使用同一条记录。数据库认领失败时不调用 GitHub。发生超时、服务器错误或进程中断后,事件不自动重试。去重默认 24 小时,记录在窗口之外额外保留 60 秒;过旧事件与超过允许时钟偏差的未来事件会被忽略。缺少消息 ID 或时间戳时也不执行。
|
|
55
|
+
|
|
56
|
+
这是“同一事件至多尝试一次”的策略,**不是跨 GitHub 和数据库的分布式 exactly-once 事务**。认领后进程崩溃可能导致未发布;已提交但响应丢失可能导致结果未知。再次发送一个新 QQ 消息有新的事件 ID,可能产生新评论,因此看到不确定提示时先查 GitHub。评论末尾的内部标记可辅助核对。
|
|
57
|
+
|
|
58
|
+
不支持多个活跃 Koishi 进程共享本插件数据并同时处理相同群。数据库唯一键能挡住相同事件,但不同事件的权限 / 状态队列不跨进程。人工或其他 GitHub 工具也可能在预检后改状态;最终以 API 的响应为准。
|
|
59
|
+
|
|
60
|
+
## GitHub 错误与凭据
|
|
61
|
+
|
|
62
|
+
- 401、403、404、410、422、429、5xx 使用固定中文原因,不返回未经筛选的 API 错误对象、响应体或请求头。
|
|
63
|
+
- 网络错误 / 5xx 仅 GET 有限指数退避。403/429 限流根据 `Retry-After` / `X-RateLimit-Reset` 暂停本实例请求,至少暂停一分钟。
|
|
64
|
+
- POST/PATCH 不自动重试。写入超时、连接中断、5xx 或响应数据无法证明成功时显示“结果不确定”。
|
|
65
|
+
- 评论先 GET 确认目标存在,再 POST `issues/{number}/comments`。PR 普通评论不使用 Review API。
|
|
66
|
+
- PR 状态额外读取 `pulls/{number}` 检查 `merged`,再 PATCH 对应端点。接口没有 merge 方法。
|
|
67
|
+
- 所有 URL 路径由绑定 Owner / Repo 与数字编号生成。不会跟随重定向去更名或转移后的仓库,部署者需显式更新绑定。
|
|
68
|
+
- API 地址只允许 HTTPS,localhost / 127.0.0.1 / ::1 允许 HTTP 测试;拒绝 URL 中的用户名、密码、查询参数和片段。
|
|
69
|
+
- 自定义 API 根地址属于部署者的凭据接收方,必须是可信 GitHub Enterprise 或可信代理。对不可信代理的正确性与保密性无法由插件保证。
|
|
70
|
+
- 固定身份头与正文分开,昵称转义;正文包含当前部署 Token 时拒绝发布。不会为了脱敏而无声改写正文。
|
|
71
|
+
|
|
72
|
+
## 审计一致性
|
|
73
|
+
|
|
74
|
+
重要操作先写 `pending`,成功后写 `success`,无变更为 `noop`,失败为 `failed`,GitHub 结果未知为 `unknown`。确认请求本身是一条无副作用的 `noop`,执行确认后另有实际操作行。
|
|
75
|
+
|
|
76
|
+
审计起始写入失败会阻止操作。若 GitHub / 权限数据已经变更,而审计结果更新失败,返回“操作已完成,但审计结果更新失败”,保留 pending 供核查,不虚报整个操作失败。突然退出也可能留下 pending。开启审计时应定期查看 pending/unknown;本插件不自动补写 GitHub 操作。
|
|
77
|
+
|
|
78
|
+
审计不保存评论正文或昵称。每小时按保留天数删除旧审计,同时清理过期去重与冷却记录;启动时也清理。关闭审计不会关闭去重或权限持久化。
|
|
79
|
+
|
|
80
|
+
## 生命周期和后台安全
|
|
81
|
+
|
|
82
|
+
插件必需依赖 `database` 和 `http`。停用时移除中间件、取消定时器、关闭队列、丢弃待处理任务、清空确认并中止 HTTP。写入中止仍可能已被 GitHub 接收,因此保留事件认领与不确定结果。
|
|
83
|
+
|
|
84
|
+
后台通过 `ctx.inject(['console', 'auth'])` 绑定依赖;没有 auth 时不注册页面或管理 RPC。每个 RPC 有官方 authority=5 门槛,并额外读取当前用户和登录会话,防止旧 socket 的缓存权限在账户降权 / 撤销后继续生效。
|
|
85
|
+
|
|
86
|
+
console 5.30.11 的 `addListener()` 不自动登记清理副作用,本插件在依赖失效或卸载时显式删除本次注册的处理器,且检查处理器身份以免删掉新实例。前后端的权限检查是互补的,隐藏页面不是安全边界。
|
|
87
|
+
|
|
88
|
+
## 运维排错
|
|
89
|
+
|
|
90
|
+
| 现象 | 检查 |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| 插件等待依赖 | `http` 和数据库是否启用;数据库路径是否可写 |
|
|
93
|
+
| QQ 完全无响应 | OneBot 平台名、群绑定启用状态、纯文本、原始数字 QQ / 群号 / message_id / time;不要用控制台 sandbox 代替 OneBot |
|
|
94
|
+
| 后台入口不出现 | console 和 auth 是否同时启用,部署管理员是否已登录 |
|
|
95
|
+
| 控制台资源 403 | 安装 `.tgz` 到应用 node_modules;源码目录开发使用官方 devMode,不要从任意磁盘路径直接作为生产资源加载 |
|
|
96
|
+
| Token 检查可读但写入 403 | 检查两个仓库写权限、Token 所属账号权限、组织审批与 classic PAT 策略 |
|
|
97
|
+
| PR 422 | 查看 GitHub 当前状态、是否已合并、源分支是否仍存在;插件不会伪装成功 |
|
|
98
|
+
| pending / unknown | 先核对 GitHub 评论 / 状态,再决定是否发一条新的 QQ 指令 |
|
|
99
|
+
|
|
100
|
+
数据库驱动自身和其他 Koishi 插件可能有独立日志策略;本插件不会把 GitHub 请求对象交给日志。建议以官方 console 的登录、部署网络边界和受控配置备份共同保护统一 Token。
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# 发布与 CI
|
|
2
|
+
|
|
3
|
+
仓库:https://github.com/ClozyA/koishi-plugin-qq-github-bridge
|
|
4
|
+
|
|
5
|
+
npm:https://www.npmjs.com/package/koishi-plugin-qq-github-bridge
|
|
6
|
+
|
|
7
|
+
所有命令使用 PowerShell 7(`pwsh`)。首次初始化需要可发布此包的 npm 账号和可维护此仓库的 GitHub 账号。
|
|
8
|
+
|
|
9
|
+
## CI
|
|
10
|
+
|
|
11
|
+
`ci.yml` 在 main 推送、Pull Request 及手动触发时运行:
|
|
12
|
+
|
|
13
|
+
- Ubuntu / Node 22、Ubuntu / Node 24、Windows / Node 24。
|
|
14
|
+
- `npm ci`、后端与 Vue 类型检查、构建、全量自动化测试(当前 126 项)。
|
|
15
|
+
- `npm run check:package` 检查真实 npm 文件列表,将 tarball 解压到临时目录,加载发布入口,并用 Koishi / HTTP / SQLite 启动和读写管理员表。宿主依赖来自已安装的开发环境;这项检查不代表真实 QQ 或外部 GitHub 验证。
|
|
16
|
+
|
|
17
|
+
GitHub Actions 使用固定 commit,检查作业仅有仓库读取权限。开发依赖的上游审计告警不被当作零风险;详情见 REVIEW 和 TESTING。
|
|
18
|
+
|
|
19
|
+
## 首次 npm 初始化
|
|
20
|
+
|
|
21
|
+
npm 官方要求包已存在才能配置可信发布。先通过本机已登录 npm 完成首版,再配置 OIDC:
|
|
22
|
+
|
|
23
|
+
```powershell
|
|
24
|
+
npm login --auth-type=web --registry=https://registry.npmjs.org
|
|
25
|
+
npm run check
|
|
26
|
+
npm run check:package
|
|
27
|
+
npm pack
|
|
28
|
+
npm publish ./koishi-plugin-qq-github-bridge-1.0.0.tgz --access public --ignore-scripts
|
|
29
|
+
npm trust github koishi-plugin-qq-github-bridge --repo ClozyA/koishi-plugin-qq-github-bridge --file release.yml --allow-publish --yes
|
|
30
|
+
npm trust list koishi-plugin-qq-github-bridge
|
|
31
|
+
gh variable set NPM_PUBLISH_ENABLED --repo ClozyA/koishi-plugin-qq-github-bridge --body true
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`npm trust` 需要 npm >=11.15,且账户启用 2FA;安全密钥等本人验证需维护者完成。也可以在 npm 包 Settings → Trusted Publisher 配置同一仓库和 `release.yml`,允许 `npm publish`。发布工作流不保存长期 `NPM_TOKEN`。
|
|
35
|
+
|
|
36
|
+
首版本机发布不带 GitHub Actions provenance。后续 CI 发布使用短期 OIDC 身份并生成来源证明。
|
|
37
|
+
|
|
38
|
+
## 后续发布
|
|
39
|
+
|
|
40
|
+
确保 main 已提交且 CI 正常:
|
|
41
|
+
|
|
42
|
+
```powershell
|
|
43
|
+
npm version patch
|
|
44
|
+
git push origin main
|
|
45
|
+
git push origin --tags
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
需要 minor / major 升级时替换 `patch`。`npm version` 更新 manifest 和 lockfile,创建版本提交与同名标签。不要手工只改标签。
|
|
49
|
+
|
|
50
|
+
`release.yml` 会:
|
|
51
|
+
|
|
52
|
+
1. 检查标签恰为 `v` + package.json 版本、对应提交属于 main。
|
|
53
|
+
2. 完整检查、构建、验证 npm 包,再产出唯一 tarball 与 SHA256SUMS。
|
|
54
|
+
3. 创建 GitHub Release 并附安装包和校验值。
|
|
55
|
+
4. 当仓库变量 `NPM_PUBLISH_ENABLED=true` 时,通过 npm OIDC 发布该 tarball。稳定版用 `latest`;带预发布后缀的版本用 `next`,GitHub Release 也标记为预发布。
|
|
56
|
+
5. 发布后读取 npm registry,比对 tarball 的 SHA512 integrity。
|
|
57
|
+
|
|
58
|
+
NPM_PUBLISH_ENABLED 只控制 npm 作业;GitHub Release 不依赖 npm 凭据。未设置该变量时,npm 作业会跳过,不能将此描述为 npm 已发布。
|
|
59
|
+
|
|
60
|
+
## 失败与重试
|
|
61
|
+
|
|
62
|
+
在 Actions → Release → Run workflow 输入已有标签(例如 `v1.0.1`)可重新执行。版本已存在且内容完全相同才跳过 npm 发布;内容不同会失败并要求升级版本,不能覆盖现有 npm 版本。
|
|
63
|
+
|
|
64
|
+
GitHub Release 资产可重复上传;不要移动已经公开发布的标签。npm 首版应使用从同一标签的 GitHub Release 下载的 tarball,避免首版重试时因本机构建差异产生 integrity 不一致。
|
|
65
|
+
|
|
66
|
+
401 / EAUTH 先检查 npm 账户;CI 的 EOIDC / ENEEDAUTH 检查 npm Trusted Publisher 的账号、仓库、文件名、允许的动作以及是否使用 GitHub 托管 runner。首次可信发布关系需要在 npm 规定的时限内使用,过期时需维护者重新配置。
|
|
67
|
+
|
|
68
|
+
官方参考:[npm 可信发布](https://docs.npmjs.com/trusted-publishers/)、[npm trust](https://docs.npmjs.com/cli/v12/commands/npm-trust/)、[Koishi 发布规范](https://koishi.chat/zh-CN/guide/develop/publish.html)。
|