chore: auto-fix lint & formatting [skip ci]

This commit is contained in:
github-actions[bot] 2026-08-02 19:03:12 +00:00
parent d540d465d1
commit 7b341ff9f4
9 changed files with 117 additions and 115 deletions

View file

@ -42,18 +42,18 @@ npx wrangler dev # Start local dev server
### Secrets (`.dev.vars` for local, Worker Secrets for production)
| Variable | Description |
| ------------------------- | --------------------------------------------------------------------------------------------- |
| `GITHUB_WEBHOOK_SECRET` | Webhook secret from GitHub |
| `GITHUB_APP_ID` | GitHub App ID |
| `GITHUB_PRIVATE_KEY` | App private key (PEM) |
| `GITHUB_CLIENT_ID` | OAuth client ID |
| `GITHUB_CLIENT_SECRET` | OAuth client secret |
| `DISCORD_TOKEN` | Bot token |
| `DISCORD_PUBLIC_KEY` | Discord application public key (from the Developer Portal) — required for interactions |
| `DISCORD_APPLICATION_ID` | Discord application id (optional; auto-resolved via `GET /oauth2/applications/@me` if omitted) |
| `BASE_URL` | Public URL for OAuth callbacks |
| `ADMIN_USER_IDS` | Comma-separated GitHub user IDs (or logins) allowed to access `/admin` |
| Variable | Description |
| ------------------------ | ---------------------------------------------------------------------------------------------- |
| `GITHUB_WEBHOOK_SECRET` | Webhook secret from GitHub |
| `GITHUB_APP_ID` | GitHub App ID |
| `GITHUB_PRIVATE_KEY` | App private key (PEM) |
| `GITHUB_CLIENT_ID` | OAuth client ID |
| `GITHUB_CLIENT_SECRET` | OAuth client secret |
| `DISCORD_TOKEN` | Bot token |
| `DISCORD_PUBLIC_KEY` | Discord application public key (from the Developer Portal) — required for interactions |
| `DISCORD_APPLICATION_ID` | Discord application id (optional; auto-resolved via `GET /oauth2/applications/@me` if omitted) |
| `BASE_URL` | Public URL for OAuth callbacks |
| `ADMIN_USER_IDS` | Comma-separated GitHub user IDs (or logins) allowed to access `/admin` |
### Routes
@ -211,12 +211,12 @@ The bot registers native **slash** and **message context-menu** commands, synced
**Requirements:**
| Item | How |
| ---------------- | ----------------------------------------------------------------- |
| Public key | `DISCORD_PUBLIC_KEY` set + Interactions Endpoint URL configured |
| Invite scope | Bot invited with `applications.commands` (see invite URL above) |
| OAuth | `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` and `BASE_URL` configured |
| User linked | Each user runs `/gh login` first |
| Item | How |
| ------------ | --------------------------------------------------------------------- |
| Public key | `DISCORD_PUBLIC_KEY` set + Interactions Endpoint URL configured |
| Invite scope | Bot invited with `applications.commands` (see invite URL above) |
| OAuth | `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` and `BASE_URL` configured |
| User linked | Each user runs `/gh login` first |
## Deployment

View file

@ -42,18 +42,18 @@ npx wrangler dev # 启动本地开发服务器
### 密钥(本地用 `.dev.vars`,生产用 Worker Secrets
| 变量 | 说明 |
| ------------------------- | -------------------------------------------------------------------------- |
| `GITHUB_WEBHOOK_SECRET` | GitHub webhook 密钥 |
| `GITHUB_APP_ID` | GitHub App ID |
| `GITHUB_PRIVATE_KEY` | App 私钥PEM |
| `GITHUB_CLIENT_ID` | OAuth Client ID |
| `GITHUB_CLIENT_SECRET` | OAuth Client Secret |
| `DISCORD_TOKEN` | 机器人 token |
| `DISCORD_PUBLIC_KEY` | Discord 应用的公钥(开发者门户获取)—— 交互功能必需 |
| `DISCORD_APPLICATION_ID` | Discord 应用 ID可选省略时通过 `GET /oauth2/applications/@me` 自动获取) |
| `BASE_URL` | 公网地址(用于 OAuth 回调) |
| `ADMIN_USER_IDS` | 允许访问 `/admin` 的 GitHub 用户 ID或登录名逗号分隔 |
| 变量 | 说明 |
| ------------------------ | --------------------------------------------------------------------------- |
| `GITHUB_WEBHOOK_SECRET` | GitHub webhook 密钥 |
| `GITHUB_APP_ID` | GitHub App ID |
| `GITHUB_PRIVATE_KEY` | App 私钥PEM |
| `GITHUB_CLIENT_ID` | OAuth Client ID |
| `GITHUB_CLIENT_SECRET` | OAuth Client Secret |
| `DISCORD_TOKEN` | 机器人 token |
| `DISCORD_PUBLIC_KEY` | Discord 应用的公钥(开发者门户获取)—— 交互功能必需 |
| `DISCORD_APPLICATION_ID` | Discord 应用 ID可选省略时通过 `GET /oauth2/applications/@me` 自动获取) |
| `BASE_URL` | 公网地址(用于 OAuth 回调) |
| `ADMIN_USER_IDS` | 允许访问 `/admin` 的 GitHub 用户 ID或登录名逗号分隔 |
### 路由配置
@ -210,12 +210,12 @@ bot 通过定时任务(每 5 分钟)同步注册原生的**斜杠命令**与
**要求:**
| 项目 | 说明 |
| ------------ | ---------------------------------------------------------------- |
| Public Key | 已配置 `DISCORD_PUBLIC_KEY` 且已设置 Interactions Endpoint URL |
| 邀请 scope | 邀请时带上 `applications.commands`(见上方邀请链接) |
| OAuth | 已配置 `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET``BASE_URL` |
| 用户绑定 | 每个用户先执行 `/gh login` |
| 项目 | 说明 |
| ---------- | ---------------------------------------------------------------- |
| Public Key | 已配置 `DISCORD_PUBLIC_KEY` 且已设置 Interactions Endpoint URL |
| 邀请 scope | 邀请时带上 `applications.commands`(见上方邀请链接) |
| OAuth | 已配置 `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET``BASE_URL` |
| 用户绑定 | 每个用户先执行 `/gh login` |
## 部署

View file

@ -10,27 +10,27 @@ https://your-worker.workers.dev
## Endpoints
| Method | Path | Auth | Description |
| -------- | ------------------------------ | -------------- | ------------------------ |
| `GET` | `/health` | None | Health check |
| `POST` | `/webhook` | HMAC signature | GitHub webhook ingestion |
| Method | Path | Auth | Description |
| -------- | ------------------------------ | ----------------- | ------------------------------------------------------ |
| `GET` | `/health` | None | Health check |
| `POST` | `/webhook` | HMAC signature | GitHub webhook ingestion |
| `POST` | `/discord/interactions` | Ed25519 signature | Discord interactions (slash commands, buttons, modals) |
| `GET` | `/auth/github` | None | Start GitHub OAuth flow |
| `GET` | `/auth/github/callback` | None | OAuth callback |
| `DELETE` | `/auth/token/:userId` | None | Revoke user token |
| `POST` | `/api/comment` | Bearer token | Create issue comment |
| `POST` | `/api/merge` | Bearer token | Merge pull request |
| `POST` | `/api/close` | Bearer token | Close pull request |
| `POST` | `/api/react` | Bearer token | Add reaction to issue |
| `GET` | `/admin` | Admin session | Config console UI |
| `GET` | `/admin/api/routes` | Admin session | List routes |
| `PUT` | `/admin/api/routes` | Admin session | Replace routes |
| `GET` | `/admin/api/groups` | Admin session | List groups (scoped) |
| `PUT` | `/admin/api/groups` | Admin session | Replace groups (super) |
| `GET` | `/admin/api/groups/:id/routes` | Admin session | List a group's routes |
| `PUT` | `/admin/api/groups/:id/routes` | Admin session | Replace a group's routes |
| `GET` | `/admin/api/me` | Admin session | Current session info |
| `GET` | `/admin/api/logs` | Admin session | Send logs (scoped) |
| `GET` | `/auth/github` | None | Start GitHub OAuth flow |
| `GET` | `/auth/github/callback` | None | OAuth callback |
| `DELETE` | `/auth/token/:userId` | None | Revoke user token |
| `POST` | `/api/comment` | Bearer token | Create issue comment |
| `POST` | `/api/merge` | Bearer token | Merge pull request |
| `POST` | `/api/close` | Bearer token | Close pull request |
| `POST` | `/api/react` | Bearer token | Add reaction to issue |
| `GET` | `/admin` | Admin session | Config console UI |
| `GET` | `/admin/api/routes` | Admin session | List routes |
| `PUT` | `/admin/api/routes` | Admin session | Replace routes |
| `GET` | `/admin/api/groups` | Admin session | List groups (scoped) |
| `PUT` | `/admin/api/groups` | Admin session | Replace groups (super) |
| `GET` | `/admin/api/groups/:id/routes` | Admin session | List a group's routes |
| `PUT` | `/admin/api/groups/:id/routes` | Admin session | Replace a group's routes |
| `GET` | `/admin/api/me` | Admin session | Current session info |
| `GET` | `/admin/api/logs` | Admin session | Send logs (scoped) |
## Admin Console

View file

@ -17,12 +17,12 @@ WebHooker requires several secrets to function. For local development, store the
### Optional Secrets
| Variable | Description | Default |
| ------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------- |
| `DISCORD_PUBLIC_KEY` | Discord application public key (Developer Portal) — required for interactions | Unset → interactions return `401` |
| `DISCORD_APPLICATION_ID` | Discord application id; auto-resolved when omitted | Auto-resolved |
| `BASE_URL` | Public URL for OAuth callbacks | `http://localhost:8787` |
| `ADMIN_USER_IDS` | Comma-separated GitHub user IDs (or logins) allowed to access the Web UI | Disabled |
| Variable | Description | Default |
| ------------------------ | ----------------------------------------------------------------------------- | --------------------------------- |
| `DISCORD_PUBLIC_KEY` | Discord application public key (Developer Portal) — required for interactions | Unset → interactions return `401` |
| `DISCORD_APPLICATION_ID` | Discord application id; auto-resolved when omitted | Auto-resolved |
| `BASE_URL` | Public URL for OAuth callbacks | `http://localhost:8787` |
| `ADMIN_USER_IDS` | Comma-separated GitHub user IDs (or logins) allowed to access the Web UI | Disabled |
## Web UI

View file

@ -17,11 +17,11 @@ POST /discord/interactions → verify (Ed25519) → handle /gh slash & context c
### Components
| Component | Role |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Cloudflare Worker** | HTTP ingress, signature verification, delivery dedup, event parsing, route matching, REST send |
| **Interactions Endpoint** | Verifies Ed25519 signatures and handles `/gh` interactions (slash commands, context-menu commands, buttons, modals) |
| **KV** | Token storage (`token:{userId}`), OAuth state (`state:{hex}`), route config (`config:routes`), send logs, delivery dedup |
| Component | Role |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Cloudflare Worker** | HTTP ingress, signature verification, delivery dedup, event parsing, route matching, REST send |
| **Interactions Endpoint** | Verifies Ed25519 signatures and handles `/gh` interactions (slash commands, context-menu commands, buttons, modals) |
| **KV** | Token storage (`token:{userId}`), OAuth state (`state:{hex}`), route config (`config:routes`), send logs, delivery dedup |
### Data Flow

View file

@ -10,27 +10,27 @@ https://your-worker.workers.dev
## 端点
| 方法 | 路径 | 鉴权 | 说明 |
| -------- | ------------------------------ | ------------ | ------------------------ |
| `GET` | `/health` | 无 | 健康检查 |
| `POST` | `/webhook` | HMAC 签名 | GitHub webhook 接入 |
| 方法 | 路径 | 鉴权 | 说明 |
| -------- | ------------------------------ | ------------ | ------------------------------------- |
| `GET` | `/health` | 无 | 健康检查 |
| `POST` | `/webhook` | HMAC 签名 | GitHub webhook 接入 |
| `POST` | `/discord/interactions` | Ed25519 签名 | Discord 交互斜杠命令、按钮、modal |
| `GET` | `/auth/github` | 无 | 启动 GitHub OAuth 流程 |
| `GET` | `/auth/github/callback` | 无 | OAuth 回调 |
| `DELETE` | `/auth/token/:userId` | 无 | 撤销用户 Token |
| `POST` | `/api/comment` | Bearer Token | 创建议题评论 |
| `POST` | `/api/merge` | Bearer Token | 合并拉取请求 |
| `POST` | `/api/close` | Bearer Token | 关闭拉取请求 |
| `POST` | `/api/react` | Bearer Token | 添加议题反应 |
| `GET` | `/admin` | 管理员会话 | 配置控制台页面 |
| `GET` | `/admin/api/routes` | 管理员会话 | 列出路由 |
| `PUT` | `/admin/api/routes` | 管理员会话 | 替换路由 |
| `GET` | `/admin/api/groups` | 管理员会话 | 列出分组(按权限过滤) |
| `PUT` | `/admin/api/groups` | 管理员会话 | 替换分组(仅超级管理员) |
| `GET` | `/admin/api/groups/:id/routes` | 管理员会话 | 列出某分组的路由 |
| `PUT` | `/admin/api/groups/:id/routes` | 管理员会话 | 替换某分组的路由 |
| `GET` | `/admin/api/me` | 管理员会话 | 当前会话信息 |
| `GET` | `/admin/api/logs` | 管理员会话 | 发送日志(按权限过滤) |
| `GET` | `/auth/github` | 无 | 启动 GitHub OAuth 流程 |
| `GET` | `/auth/github/callback` | 无 | OAuth 回调 |
| `DELETE` | `/auth/token/:userId` | 无 | 撤销用户 Token |
| `POST` | `/api/comment` | Bearer Token | 创建议题评论 |
| `POST` | `/api/merge` | Bearer Token | 合并拉取请求 |
| `POST` | `/api/close` | Bearer Token | 关闭拉取请求 |
| `POST` | `/api/react` | Bearer Token | 添加议题反应 |
| `GET` | `/admin` | 管理员会话 | 配置控制台页面 |
| `GET` | `/admin/api/routes` | 管理员会话 | 列出路由 |
| `PUT` | `/admin/api/routes` | 管理员会话 | 替换路由 |
| `GET` | `/admin/api/groups` | 管理员会话 | 列出分组(按权限过滤) |
| `PUT` | `/admin/api/groups` | 管理员会话 | 替换分组(仅超级管理员) |
| `GET` | `/admin/api/groups/:id/routes` | 管理员会话 | 列出某分组的路由 |
| `PUT` | `/admin/api/groups/:id/routes` | 管理员会话 | 替换某分组的路由 |
| `GET` | `/admin/api/me` | 管理员会话 | 当前会话信息 |
| `GET` | `/admin/api/logs` | 管理员会话 | 发送日志(按权限过滤) |
## 管理控制台

View file

@ -184,4 +184,4 @@ WebHooker 内置了位于 `/admin` 的配置控制台,可在浏览器中管理
| `logs:send:{ts}-{hex}` | 发送记录 | 1 小时 |
| `cmd:guild:{id}` | 已注册命令的服务器 id去重标记 | 永久 |
| `cmd:registered:global` | 全局命令已注册标记24h 去重) | 1 天 |
| `config:discord-app-id` | Discord 应用 id 缓存 | 永久 |
| `config:discord-app-id` | Discord 应用 id 缓存 | 永久 |

View file

@ -17,11 +17,11 @@ POST /discord/interactions → 验证 (Ed25519) → 处理 /gh 斜杠与右键
### 组件
| 组件 | 职责 |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------- |
| **Cloudflare Worker** | HTTP 入口、签名验证、投递去重、事件解析、路由匹配、REST 发送 |
| **Interactions Endpoint** | 验证 Ed25519 签名并处理 `/gh` 交互斜杠命令、右键菜单、按钮、modal |
| **KV** | Token 存储 (`token:{userId}`)、OAuth 状态 (`state:{hex}`)、路由配置 (`config:routes`)、发送日志、投递去重 |
| 组件 | 职责 |
| ------------------------- | --------------------------------------------------------------------------------------------------------- |
| **Cloudflare Worker** | HTTP 入口、签名验证、投递去重、事件解析、路由匹配、REST 发送 |
| **Interactions Endpoint** | 验证 Ed25519 签名并处理 `/gh` 交互斜杠命令、右键菜单、按钮、modal |
| **KV** | Token 存储 (`token:{userId}`)、OAuth 状态 (`state:{hex}`)、路由配置 (`config:routes`)、发送日志、投递去重 |
### 数据流

View file

@ -165,7 +165,10 @@ export async function handleInteractionRequest(request: Request, env: Env): Prom
const signature = request.headers.get("X-Signature-Ed25519");
const timestamp = request.headers.get("X-Signature-Timestamp");
if (!signature || !timestamp || !env.DISCORD_PUBLIC_KEY) {
log.warn({ hasSig: !!signature, hasTs: !!timestamp, hasKey: !!env.DISCORD_PUBLIC_KEY }, "Discord interaction missing signature");
log.warn(
{ hasSig: !!signature, hasTs: !!timestamp, hasKey: !!env.DISCORD_PUBLIC_KEY },
"Discord interaction missing signature",
);
return new Response("Invalid signature", { status: 401 });
}
if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > TIMESTAMP_TOLERANCE_SECONDS) {
@ -293,7 +296,12 @@ async function respond(env: Env, id: string, token: string, content: string): Pr
});
}
async function interactionCallback(env: Env, id: string, token: string, body: unknown): Promise<void> {
async function interactionCallback(
env: Env,
id: string,
token: string,
body: unknown,
): Promise<void> {
const res = await fetch(`${DISCORD_API}/interactions/${id}/${token}/callback`, {
method: "POST",
headers: { "Content-Type": "application/json" },
@ -386,8 +394,7 @@ async function handleButton(
async function cmdLogin(env: Env, id: string, token: string, userId: string | null): Promise<void> {
if (!userId) return respond(env, id, token, "无法识别你的 Discord 账号。");
const clientId = env.GITHUB_CLIENT_ID;
if (!clientId)
return respond(env, id, token, "服务器未配置 GitHub OAuthGITHUB_CLIENT_ID。");
if (!clientId) return respond(env, id, token, "服务器未配置 GitHub OAuthGITHUB_CLIENT_ID。");
const state = crypto.randomUUID().replace(/-/g, "");
await env.KV.put(
@ -404,7 +411,12 @@ async function cmdLogin(env: Env, id: string, token: string, userId: string | nu
);
}
async function cmdLogout(env: Env, id: string, token: string, userId: string | null): Promise<void> {
async function cmdLogout(
env: Env,
id: string,
token: string,
userId: string | null,
): Promise<void> {
if (!userId) return respond(env, id, token, "无法识别你的 Discord 账号。");
await removeDiscordLink(env.KV, userId);
await respond(env, id, token, "已解绑你的 GitHub 账号。");
@ -413,8 +425,7 @@ async function cmdLogout(env: Env, id: string, token: string, userId: string | n
/** Map a GitHub op error code to a user-facing (Chinese) message. */
function errText(err: unknown): string {
const t = err instanceof Error ? err.message : String(err);
if (t === "GITHUB_TOKEN_EXPIRED")
return "GitHub 授权已过期或无效,请重新使用 `/gh login` 绑定。";
if (t === "GITHUB_TOKEN_EXPIRED") return "GitHub 授权已过期或无效,请重新使用 `/gh login` 绑定。";
if (t === "GITHUB_FORBIDDEN") return "GitHub 拒绝了此操作:你的账号没有权限修改/删除这条评论。";
if (t === "GITHUB_NOT_FOUND") return "找不到目标(可能评论已被删除或仓库不可访问)。";
return `操作失败:${t}`;
@ -480,13 +491,7 @@ async function commentOp(
// edit: fetch current body to prefill the modal.
let prefill = "";
try {
const { body } = await getCommentAsUser(
env.KV,
githubUserId,
owner!,
repo!,
Number(commentId),
);
const { body } = await getCommentAsUser(env.KV, githubUserId, owner!, repo!, Number(commentId));
prefill = body;
} catch (err) {
return respond(env, id, token, errText(err));
@ -678,14 +683,11 @@ export async function syncGuildCommands(env: Env): Promise<void> {
for (const guild of guilds) {
try {
if (await env.KV.get(`cmd:guild:${guild.id}`)) continue;
const r = await fetch(
`${DISCORD_API}/applications/${appId}/guilds/${guild.id}/commands`,
{
method: "PUT",
headers: { Authorization: `Bot ${token}`, "Content-Type": "application/json" },
body: JSON.stringify(APP_COMMANDS),
},
);
const r = await fetch(`${DISCORD_API}/applications/${appId}/guilds/${guild.id}/commands`, {
method: "PUT",
headers: { Authorization: `Bot ${token}`, "Content-Type": "application/json" },
body: JSON.stringify(APP_COMMANDS),
});
if (r.ok) {
await env.KV.put(`cmd:guild:${guild.id}`, "1");
log.info({ guildId: guild.id }, "Registered guild application commands");