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

This commit is contained in:
github-actions[bot] 2026-08-02 16:44:42 +00:00
parent 4edd468ace
commit 2e4cb24c1d
17 changed files with 386 additions and 120 deletions

View file

@ -167,7 +167,11 @@ header {
font-weight: 600;
cursor: pointer;
text-decoration: none;
transition: border-color 0.15s, background 0.15s, color 0.15s, transform 0.1s;
transition:
border-color 0.15s,
background 0.15s,
color 0.15s,
transform 0.1s;
}
.btn:hover {
@ -279,7 +283,9 @@ main {
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px 22px;
transition: border-color 0.15s, transform 0.15s;
transition:
border-color 0.15s,
transform 0.15s;
animation: rise 0.35s cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
@ -407,7 +413,9 @@ main {
border: 1px solid var(--border-strong);
border-radius: 999px;
cursor: pointer;
transition: background 0.18s, border-color 0.18s;
transition:
background 0.18s,
border-color 0.18s;
}
.switch .track::after {
@ -620,7 +628,9 @@ main {
color: var(--text);
font-family: var(--ui);
font-size: 13.5px;
transition: border-color 0.15s, box-shadow 0.15s;
transition:
border-color 0.15s,
box-shadow 0.15s;
}
.field input[type="text"]:focus {
@ -733,7 +743,9 @@ main {
.toast-enter-active,
.toast-leave-active {
transition: opacity 0.22s, transform 0.22s;
transition:
opacity 0.22s,
transform 0.22s;
}
.toast-enter-from,
@ -782,7 +794,9 @@ main {
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s, color 0.15s;
transition:
background 0.15s,
color 0.15s;
}
.tab:hover {

View file

@ -13,7 +13,12 @@
<div class="row2">
<div class="field">
<label>{{ t("groupEditor.name") }}</label>
<input v-model="form.name" type="text" :placeholder="t('groupEditor.namePlaceholder')" required />
<input
v-model="form.name"
type="text"
:placeholder="t('groupEditor.namePlaceholder')"
required
/>
</div>
<div class="field">
<label>{{ t("groupEditor.id") }}</label>
@ -28,19 +33,35 @@
</div>
</div>
<div class="field">
<label>{{ t("groupEditor.admins") }} <span class="lbl-note">{{ t("groupEditor.adminsNote") }}</span></label>
<input v-model="form.adminIds" type="text" :placeholder="t('groupEditor.adminsPlaceholder')" />
<label
>{{ t("groupEditor.admins") }}
<span class="lbl-note">{{ t("groupEditor.adminsNote") }}</span></label
>
<input
v-model="form.adminIds"
type="text"
:placeholder="t('groupEditor.adminsPlaceholder')"
/>
<div class="hint">{{ t("groupEditor.adminsHint") }}</div>
</div>
<div class="field">
<label>{{ t("groupEditor.owners") }} <span class="lbl-note">{{ t("groupEditor.ownersNote") }}</span></label>
<input v-model="form.owners" type="text" :placeholder="t('groupEditor.ownersPlaceholder')" />
<label
>{{ t("groupEditor.owners") }}
<span class="lbl-note">{{ t("groupEditor.ownersNote") }}</span></label
>
<input
v-model="form.owners"
type="text"
:placeholder="t('groupEditor.ownersPlaceholder')"
/>
<div class="hint">{{ t("groupEditor.ownersHint") }}</div>
</div>
<div class="err">{{ formError }}</div>
</form>
<div class="editor-foot">
<button class="btn btn-ghost" type="button" @click="close">{{ t("groupEditor.cancel") }}</button>
<button class="btn btn-ghost" type="button" @click="close">
{{ t("groupEditor.cancel") }}
</button>
<button class="btn btn-accent" type="button" :disabled="saving" @click="save">
{{ t("groupEditor.save") }}
</button>

View file

@ -12,20 +12,38 @@
<span v-if="route.fallback" class="badge fallback">{{ t("route.fallback") }}</span>
</div>
<div class="card-actions">
<button class="icon-btn" :title="t('routeEditor.editTitle')" @click="$emit('edit', route)"></button>
<button class="icon-btn danger" :title="t('routeEditor.close')" @click="$emit('delete', route)"></button>
<button class="icon-btn" :title="t('routeEditor.editTitle')" @click="$emit('edit', route)">
</button>
<button
class="icon-btn danger"
:title="t('routeEditor.close')"
@click="$emit('delete', route)"
>
</button>
</div>
</div>
<div class="chips">
<span v-for="(f, i) in route.filters" :key="i" class="chip" :class="{ exclude: f.exclude }">
<span class="f-type">{{ f.exclude ? t("routeEditor.not") + " " : "" }}{{ t("filter." + f.type) }}</span>
<span class="f-type"
>{{ f.exclude ? t("routeEditor.not") + " " : "" }}{{ t("filter." + f.type) }}</span
>
<span class="f-val">{{ fmtMatch(f.match) }}</span>
</span>
<span v-if="!route.filters.length" class="chip"><span class="f-type">{{ t("route.noFilters") }}</span></span>
<span v-if="!route.filters.length" class="chip"
><span class="f-type">{{ t("route.noFilters") }}</span></span
>
</div>
<div class="target">
<span><b>{{ t("route.channel") }}</b><code>{{ route.target.channelId }}</code></span>
<span v-if="route.target.threadId"><b>{{ t("route.thread") }}</b><code>{{ route.target.threadId }}</code></span>
<span
><b>{{ t("route.channel") }}</b
><code>{{ route.target.channelId }}</code></span
>
<span v-if="route.target.threadId"
><b>{{ t("route.thread") }}</b
><code>{{ route.target.threadId }}</code></span
>
</div>
</article>
</template>

View file

@ -12,7 +12,12 @@
<form class="editor-body" @submit.prevent="save">
<div class="field">
<label>{{ t("routeEditor.name") }}</label>
<input v-model="form.name" type="text" :placeholder="t('routeEditor.namePlaceholder')" required />
<input
v-model="form.name"
type="text"
:placeholder="t('routeEditor.namePlaceholder')"
required
/>
</div>
<div class="row2">
<div class="field">
@ -22,7 +27,11 @@
</div>
<div class="field">
<label>{{ t("routeEditor.language") }}</label>
<input v-model="form.lang" type="text" :placeholder="t('routeEditor.langPlaceholder')" />
<input
v-model="form.lang"
type="text"
:placeholder="t('routeEditor.langPlaceholder')"
/>
<div class="hint">{{ t("routeEditor.langHint") }}</div>
</div>
</div>
@ -32,38 +41,70 @@
</div>
<div class="field inline">
<input v-model="form.fallback" type="checkbox" />
<span>{{ t("routeEditor.fallback") }} <span class="lbl-note">{{ t("routeEditor.fallbackHint") }}</span></span>
<span
>{{ t("routeEditor.fallback") }}
<span class="lbl-note">{{ t("routeEditor.fallbackHint") }}</span></span
>
</div>
<div class="field">
<label>{{ t("routeEditor.filters") }} <span class="lbl-note">{{ t("routeEditor.filtersNote") }}</span></label>
<label
>{{ t("routeEditor.filters") }}
<span class="lbl-note">{{ t("routeEditor.filtersNote") }}</span></label
>
<div v-for="(f, i) in form.filters" :key="i" class="filter-row">
<select v-model="f.type">
<option v-for="ft in FILTER_TYPES" :key="ft" :value="ft">{{ t("filter." + ft) }}</option>
<option v-for="ft in FILTER_TYPES" :key="ft" :value="ft">
{{ t("filter." + ft) }}
</option>
</select>
<input v-model="f.matchText" type="text" :placeholder="t('routeEditor.matchPlaceholder')" />
<input
v-model="f.matchText"
type="text"
:placeholder="t('routeEditor.matchPlaceholder')"
/>
<label class="inline">
<input v-model="f.exclude" type="checkbox" /><span>{{ t("routeEditor.not") }}</span>
</label>
<button type="button" class="icon-btn danger" @click="form.filters.splice(i, 1)"></button>
<button type="button" class="icon-btn danger" @click="form.filters.splice(i, 1)">
</button>
</div>
<button type="button" class="btn btn-ghost add-filter" @click="addFilter">{{ t("routeEditor.addFilter") }}</button>
<button type="button" class="btn btn-ghost add-filter" @click="addFilter">
{{ t("routeEditor.addFilter") }}
</button>
<div class="err">{{ filterError }}</div>
</div>
<div class="row2">
<div class="field">
<label>{{ t("routeEditor.channel") }}</label>
<input v-model="form.channelId" type="text" :placeholder="t('routeEditor.channelPlaceholder')" required />
<input
v-model="form.channelId"
type="text"
:placeholder="t('routeEditor.channelPlaceholder')"
required
/>
</div>
<div class="field">
<label>{{ t("routeEditor.thread") }} <span class="lbl-note">{{ t("routeEditor.threadNote") }}</span></label>
<input v-model="form.threadId" type="text" :placeholder="t('routeEditor.threadPlaceholder')" />
<label
>{{ t("routeEditor.thread") }}
<span class="lbl-note">{{ t("routeEditor.threadNote") }}</span></label
>
<input
v-model="form.threadId"
type="text"
:placeholder="t('routeEditor.threadPlaceholder')"
/>
</div>
</div>
<div class="err">{{ formError }}</div>
</form>
<div class="editor-foot">
<button class="btn btn-ghost" type="button" @click="close">{{ t("routeEditor.cancel") }}</button>
<button class="btn btn-accent" type="button" :disabled="saving" @click="save">{{ t("routeEditor.save") }}</button>
<button class="btn btn-ghost" type="button" @click="close">
{{ t("routeEditor.cancel") }}
</button>
<button class="btn btn-accent" type="button" :disabled="saving" @click="save">
{{ t("routeEditor.save") }}
</button>
</div>
</aside>
</Transition>
@ -131,7 +172,8 @@ watch(
form.fallback = r?.fallback ?? false;
form.channelId = r?.target.channelId ?? "";
form.threadId = r?.target.threadId ?? "";
form.filters = (r && r.filters.length
form.filters = (
r && r.filters.length
? r.filters
: form.fallback
? []

View file

@ -2,7 +2,9 @@
<div>
<div class="log-toolbar">
<span class="kpi-label">{{ t("logs.lastSends", { n: logs.length }) }}</span>
<button class="btn btn-ghost btn-sm" :disabled="loading" @click="refresh">{{ t("logs.refresh") }}</button>
<button class="btn btn-ghost btn-sm" :disabled="loading" @click="refresh">
{{ t("logs.refresh") }}
</button>
</div>
<p v-if="error" class="err">{{ error }}</p>
@ -17,8 +19,14 @@
<span class="log-time">{{ fmtTime(l.ts) }}</span>
</div>
<div class="log-meta">
<span v-if="l.repo"><b>{{ t("logs.repo") }}</b><code>{{ l.repo }}</code></span>
<span><b>{{ t("logs.target") }}</b><code>{{ l.target }}</code></span>
<span v-if="l.repo"
><b>{{ t("logs.repo") }}</b
><code>{{ l.repo }}</code></span
>
<span
><b>{{ t("logs.target") }}</b
><code>{{ l.target }}</code></span
>
</div>
<div v-if="!l.ok && l.error" class="log-error">{{ l.error }}</div>
</article>

View file

@ -192,7 +192,8 @@ const zh: Dict = {
"groupEditor.owners": "来源限定",
"groupEditor.ownersNote": "GitHub 组织 / 用户登录名,逗号分隔)",
"groupEditor.ownersPlaceholder": "my-org, some-user",
"groupEditor.ownersHint": "只有来自这些组织/用户的 webhook 事件才会进入本分组的路由。留空表示不限制。",
"groupEditor.ownersHint":
"只有来自这些组织/用户的 webhook 事件才会进入本分组的路由。留空表示不限制。",
"groupEditor.cancel": "取消",
"groupEditor.save": "保存分组",
"groupEditor.errIdFormat": "ID 只能是 a-z / 0-9 / 短横线",

View file

@ -112,7 +112,11 @@
<span class="route-id">{{ g.id }}</span>
</div>
<div v-if="isSuper" class="card-actions" @click.stop>
<button class="icon-btn" :title="t('groupEditor.editTitle')" @click="openEditGroup(g)">
<button
class="icon-btn"
:title="t('groupEditor.editTitle')"
@click="openEditGroup(g)"
>
</button>
<button class="icon-btn danger" @click="onDeleteGroup(g)"></button>

View file

@ -3,9 +3,7 @@ import { sendMessage } from "../discord-rest";
import { dispatchEvent, isGatewayEnabled } from "../discord";
import type { Env, Route } from "../types";
function mockFetch(
handler: (url: string, init?: RequestInit) => Response,
): void {
function mockFetch(handler: (url: string, init?: RequestInit) => Response): void {
globalThis.fetch = (input: RequestInfo | URL, init?: RequestInit): Promise<Response> =>
Promise.resolve(handler(String(input), init));
}

View file

@ -21,7 +21,11 @@ function createMockKV(): KVNamespace {
delete: async (key: string) => {
store.delete(key);
},
list: async () => ({ keys: [...store.keys()].map((k) => ({ name: k })), list_complete: true, cacheStatus: null }),
list: async () => ({
keys: [...store.keys()].map((k) => ({ name: k })),
list_complete: true,
cacheStatus: null,
}),
} as unknown as KVNamespace;
}

View file

@ -21,7 +21,11 @@ function createMockKV(): KVNamespace {
delete: async (key: string) => {
store.delete(key);
},
list: async () => ({ keys: [...store.keys()].map((k) => ({ name: k })), list_complete: true, cacheStatus: null }),
list: async () => ({
keys: [...store.keys()].map((k) => ({ name: k })),
list_complete: true,
cacheStatus: null,
}),
} as unknown as KVNamespace;
}

View file

@ -144,7 +144,16 @@ export function createActionRoutes(): Hono<{ Bindings: Env }> {
if (!userId) return c.json({ error: "Invalid or expired token" }, 401);
const body = await readJson(c);
const reactions = ["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes"] as const;
const reactions = [
"+1",
"-1",
"laugh",
"confused",
"heart",
"hooray",
"rocket",
"eyes",
] as const;
if (
!body ||
!isNonEmptyString(body.owner) ||
@ -164,14 +173,7 @@ export function createActionRoutes(): Hono<{ Bindings: Env }> {
repo: body.repo,
issue_number: body.issueNumber,
content: body.reaction as
| "+1"
| "-1"
| "laugh"
| "confused"
| "heart"
| "hooray"
| "rocket"
| "eyes",
"+1" | "-1" | "laugh" | "confused" | "heart" | "hooray" | "rocket" | "eyes",
});
} catch (err) {
log.error({ err }, "Failed to create reaction");

View file

@ -7,13 +7,7 @@ import {
clearAdminCookie,
type AdminSession,
} from "./admin-session";
import {
loadGroups,
saveGroups,
resolveScope,
hasAnyAccess,
type AccessScope,
} from "./groups";
import { loadGroups, saveGroups, resolveScope, hasAnyAccess, type AccessScope } from "./groups";
import { getSendLog } from "./send-log";
import { log } from "./log";
@ -40,9 +34,7 @@ function deepEqual(a: unknown, b: unknown): boolean {
const ak = Object.keys(ao);
const bk = Object.keys(bo);
if (ak.length !== bk.length) return false;
return ak.every(
(k) => Object.prototype.hasOwnProperty.call(bo, k) && deepEqual(ao[k], bo[k]),
);
return ak.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && deepEqual(ao[k], bo[k]));
}
return false;
}
@ -314,7 +306,10 @@ export function createAdminRoutes(): Hono<{ Bindings: Env }> {
const groupId = c.req.param("groupId");
const access = groupAccess(s, groupId);
if (!access.ok) {
return c.json({ error: access.status === 404 ? "Group not found" : "Forbidden" }, access.status);
return c.json(
{ error: access.status === 404 ? "Group not found" : "Forbidden" },
access.status,
);
}
const all = await loadRoutes(c.env.KV);
return c.json({ group: access.group, routes: all.filter((r) => r.groupId === groupId) });
@ -326,7 +321,10 @@ export function createAdminRoutes(): Hono<{ Bindings: Env }> {
const groupId = c.req.param("groupId");
const access = groupAccess(s, groupId);
if (!access.ok) {
return c.json({ error: access.status === 404 ? "Group not found" : "Forbidden" }, access.status);
return c.json(
{ error: access.status === 404 ? "Group not found" : "Forbidden" },
access.status,
);
}
let body: unknown;

View file

@ -50,7 +50,11 @@ const APP_COMMANDS = [
type: COMMAND_TYPE.CHAT_INPUT,
description: "GitHub 集成",
options: [
{ type: OPTION_TYPE.SUB_COMMAND, name: "login", description: "绑定你的 GitHub 账号以用本人身份评论" },
{
type: OPTION_TYPE.SUB_COMMAND,
name: "login",
description: "绑定你的 GitHub 账号以用本人身份评论",
},
{ type: OPTION_TYPE.SUB_COMMAND, name: "logout", description: "解绑你的 GitHub 账号" },
{
type: OPTION_TYPE.SUB_COMMAND_GROUP,
@ -62,7 +66,12 @@ const APP_COMMANDS = [
name: "add",
description: "在 issue/PR 下新增评论",
options: [
{ type: OPTION_TYPE.STRING, name: "link", description: "issue/PR 链接", required: true },
{
type: OPTION_TYPE.STRING,
name: "link",
description: "issue/PR 链接",
required: true,
},
],
},
{
@ -70,7 +79,12 @@ const APP_COMMANDS = [
name: "edit",
description: "编辑一条评论",
options: [
{ type: OPTION_TYPE.STRING, name: "link", description: "评论链接(含 #issuecomment-", required: true },
{
type: OPTION_TYPE.STRING,
name: "link",
description: "评论链接(含 #issuecomment-",
required: true,
},
],
},
{
@ -78,7 +92,12 @@ const APP_COMMANDS = [
name: "del",
description: "删除一条评论",
options: [
{ type: OPTION_TYPE.STRING, name: "link", description: "评论链接(含 #issuecomment-", required: true },
{
type: OPTION_TYPE.STRING,
name: "link",
description: "评论链接(含 #issuecomment-",
required: true,
},
],
},
],
@ -186,7 +205,10 @@ export class DiscordGateway {
});
ws.addEventListener("error", (event) => {
log.error({ err: String((event as ErrorEvent).message ?? event) }, "Gateway WebSocket error");
log.error(
{ err: String((event as ErrorEvent).message ?? event) },
"Gateway WebSocket error",
);
});
} catch (err) {
this.connecting = false;
@ -196,10 +218,7 @@ export class DiscordGateway {
}
private scheduleReconnect(): void {
const delay = Math.min(
BASE_RECONNECT_DELAY * 2 ** this.reconnectAttempt,
MAX_RECONNECT_DELAY,
);
const delay = Math.min(BASE_RECONNECT_DELAY * 2 ** this.reconnectAttempt, MAX_RECONNECT_DELAY);
this.reconnectAttempt++;
this.state.storage.setAlarm(Date.now() + delay);
}
@ -277,7 +296,10 @@ export class DiscordGateway {
private identify(): void {
if (!this.socket || this.socket.readyState !== WebSocket.OPEN || !this.token) {
log.warn({ hasSocket: !!this.socket, readyState: this.socket?.readyState ?? null }, "Cannot identify");
log.warn(
{ hasSocket: !!this.socket, readyState: this.socket?.readyState ?? null },
"Cannot identify",
);
return;
}
log.info("Sending IDENTIFY");
@ -374,7 +396,14 @@ export class DiscordGateway {
custom_id?: string;
message?: { id?: string };
};
return this.handleButton(id, token, userId, interaction.channel_id, data.message?.id, data.custom_id);
return this.handleButton(
id,
token,
userId,
interaction.channel_id,
data.message?.id,
data.custom_id,
);
}
if (interaction.type === INTERACTION_TYPE.COMMAND) {
@ -382,14 +411,29 @@ export class DiscordGateway {
name?: string;
type?: number;
target_id?: string;
options?: Array<{ name: string; options?: Array<{ name: string; value?: string; options?: Array<{ name: string; value?: string }> }> }>;
resolved?: { messages?: Record<string, { embeds?: Array<{ url?: string }>; content?: string }> };
options?: Array<{
name: string;
options?: Array<{
name: string;
value?: string;
options?: Array<{ name: string; value?: string }>;
}>;
}>;
resolved?: {
messages?: Record<string, { embeds?: Array<{ url?: string }>; content?: string }>;
};
};
// Right-click (message context-menu) commands.
if (data.type === COMMAND_TYPE.MESSAGE) {
const op =
data.name === MSG_CMD_ADD ? "add" : data.name === MSG_CMD_EDIT ? "edit" : data.name === MSG_CMD_DEL ? "del" : null;
data.name === MSG_CMD_ADD
? "add"
: data.name === MSG_CMD_EDIT
? "edit"
: data.name === MSG_CMD_DEL
? "del"
: null;
if (!op) return;
const target = data.target_id ? data.resolved?.messages?.[data.target_id] : undefined;
const source = target?.embeds?.[0]?.url ?? target?.content ?? "";
@ -403,7 +447,14 @@ export class DiscordGateway {
if (top?.name === "logout") return this.cmdLogout(id, token, userId);
if (top?.name === "comment") {
const sub = top.options?.[0];
const op = sub?.name === "add" ? "add" : sub?.name === "edit" ? "edit" : sub?.name === "del" ? "del" : null;
const op =
sub?.name === "add"
? "add"
: sub?.name === "edit"
? "edit"
: sub?.name === "del"
? "del"
: null;
if (!op) return;
const link = sub?.options?.find((o) => o.name === "link")?.value ?? "";
return this.commentOp(id, token, userId, op, link);
@ -518,7 +569,8 @@ export class DiscordGateway {
private async cmdLogin(id: string, token: string, userId: string | null): Promise<void> {
if (!userId) return this.respond(id, token, "无法识别你的 Discord 账号。");
const clientId = this.env.GITHUB_CLIENT_ID;
if (!clientId) return this.respond(id, token, "服务器未配置 GitHub OAuthGITHUB_CLIENT_ID。");
if (!clientId)
return this.respond(id, token, "服务器未配置 GitHub OAuthGITHUB_CLIENT_ID。");
const state = crypto.randomUUID().replace(/-/g, "");
await this.env.KV.put(
@ -543,7 +595,8 @@ export class DiscordGateway {
/** Map a GitHub op error code to a user-facing (Chinese) message. */
private 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}`;
@ -568,14 +621,28 @@ export class DiscordGateway {
if (op === "add") {
const m = source.match(GITHUB_ISSUE_RE);
if (!m) return this.respond(id, token, "找不到 issue / PR 链接(右键 issue/PR 通知,或用 link 传入链接)。");
return this.openCommentModal(id, token, `${MODAL_ADD}${m[1]}|${m[2]}|${m[3]}`, `评论 ${m[1]}/${m[2]}#${m[3]}`);
if (!m)
return this.respond(
id,
token,
"找不到 issue / PR 链接(右键 issue/PR 通知,或用 link 传入链接)。",
);
return this.openCommentModal(
id,
token,
`${MODAL_ADD}${m[1]}|${m[2]}|${m[3]}`,
`评论 ${m[1]}/${m[2]}#${m[3]}`,
);
}
// edit / del both need a specific comment id.
const m = source.match(GITHUB_COMMENT_RE);
if (!m) {
return this.respond(id, token, "找不到评论链接(需含 `#issuecomment-...`,请右键某条评论通知,或粘贴评论链接)。");
return this.respond(
id,
token,
"找不到评论链接(需含 `#issuecomment-...`,请右键某条评论通知,或粘贴评论链接)。",
);
}
const [, owner, repo, commentId] = m;
@ -591,7 +658,13 @@ export class DiscordGateway {
// edit: fetch current body to prefill the modal.
let prefill = "";
try {
const { body } = await getCommentAsUser(this.env.KV, githubUserId, owner!, repo!, Number(commentId));
const { body } = await getCommentAsUser(
this.env.KV,
githubUserId,
owner!,
repo!,
Number(commentId),
);
prefill = body;
} catch (err) {
return this.respond(id, token, this.errText(err));
@ -662,7 +735,8 @@ export class DiscordGateway {
// ghc|add|owner|repo|issueNumber
if (customId.startsWith(MODAL_ADD)) {
const [owner, repo, number] = customId.slice(MODAL_ADD.length).split("|");
if (!owner || !repo || !number) return this.respond(id, token, "内部错误:无法解析目标 issue。");
if (!owner || !repo || !number)
return this.respond(id, token, "内部错误:无法解析目标 issue。");
try {
const { htmlUrl, login } = await commentAsUser(
this.env.KV,
@ -681,7 +755,8 @@ export class DiscordGateway {
// ghc|edit|owner|repo|commentId
if (customId.startsWith(MODAL_EDIT)) {
const [owner, repo, commentId] = customId.slice(MODAL_EDIT.length).split("|");
if (!owner || !repo || !commentId) return this.respond(id, token, "内部错误:无法解析目标评论。");
if (!owner || !repo || !commentId)
return this.respond(id, token, "内部错误:无法解析目标评论。");
try {
const { htmlUrl } = await editCommentAsUser(
this.env.KV,

View file

@ -67,7 +67,11 @@ const WORKFLOW_CONCLUSION_EMOJI: Record<string, string> = {
type T = (key: string, params?: Record<string, string | number>) => string;
export function formatEvent(route: Route, event: WebhookEvent, tr?: Translations): FormattedMessage {
export function formatEvent(
route: Route,
event: WebhookEvent,
tr?: Translations,
): FormattedMessage {
const { event: eventType, payload } = event;
const repo = (payload.repository as { full_name?: string })?.full_name;
const sender = (payload.sender as { login?: string })?.login;
@ -163,7 +167,9 @@ function formatPush(
descriptionParts.push(t("events.push.branch_created"));
}
descriptionParts.push(t("events.push.commits_pushed", { count, s: count !== 1 ? "s" : "", ref: ref ?? "" }));
descriptionParts.push(
t("events.push.commits_pushed", { count, s: count !== 1 ? "s" : "", ref: ref ?? "" }),
);
if (compareUrl) {
descriptionParts.push(t("events.push.view_comparison", { url: compareUrl }));
@ -219,7 +225,11 @@ function formatPush(
embeds: [
{
author,
title: t("events.push.title", { count, s: count !== 1 ? "s" : "", repo: repo ?? t("common.repository") }),
title: t("events.push.title", {
count,
s: count !== 1 ? "s" : "",
repo: repo ?? t("common.repository"),
}),
url: compareUrl,
color: GITHUB_COLORS.push,
description: descriptionParts.join("\n"),
@ -331,7 +341,11 @@ function formatPullRequest(
embeds: [
{
author,
title: t("events.pr.title", { repo: repo ?? t("common.repository"), number: pr.number ?? "?", title: pr.title ?? t("common.untitled") }),
title: t("events.pr.title", {
repo: repo ?? t("common.repository"),
number: pr.number ?? "?",
title: pr.title ?? t("common.untitled"),
}),
url: pr.html_url,
color: GITHUB_COLORS[colorKey],
description: descriptionParts.join("\n"),
@ -412,7 +426,11 @@ function formatIssues(
embeds: [
{
author,
title: t("events.issues.title", { repo: repo ?? t("common.repository"), number: issue.number ?? "?", title: issue.title ?? t("common.untitled") }),
title: t("events.issues.title", {
repo: repo ?? t("common.repository"),
number: issue.number ?? "?",
title: issue.title ?? t("common.untitled"),
}),
url: issue.html_url,
color: GITHUB_COLORS[colorKey],
description: descriptionParts.join("\n"),
@ -449,7 +467,11 @@ function formatIssueComment(
embeds: [
{
author,
title: t("events.issue_comment.comment_on", { repo: repo ?? t("common.repository"), number: issue.number ?? "?", title: issue.title ?? t("common.untitled") }),
title: t("events.issue_comment.comment_on", {
repo: repo ?? t("common.repository"),
number: issue.number ?? "?",
title: issue.title ?? t("common.untitled"),
}),
url: comment.html_url ?? issue.html_url,
color: GITHUB_COLORS.issue_comment,
description: `${t("events.issue_comment.action_comment", { action: al })}\n\n> ${commentBody}${truncated ? "..." : ""}`,
@ -563,7 +585,13 @@ function formatRelease(
const emoji = action === "deleted" ? "🗑️" : isPrerelease ? "⚠️" : "🚀";
const descriptionParts: string[] = [];
descriptionParts.push(t("events.release.action_release", { emoji, action: al, tag: release.tag_name ?? t("common.unknown") }));
descriptionParts.push(
t("events.release.action_release", {
emoji,
action: al,
tag: release.tag_name ?? t("common.unknown"),
}),
);
if (release.body) {
const truncated = release.body.slice(0, 300);
@ -574,7 +602,10 @@ function formatRelease(
embeds: [
{
author,
title: t("events.release.title", { name: release.name ?? release.tag_name ?? "Release", repo: repo ?? t("common.repository") }),
title: t("events.release.title", {
name: release.name ?? release.tag_name ?? "Release",
repo: repo ?? t("common.repository"),
}),
url: release.html_url,
color: GITHUB_COLORS[colorKey],
description: descriptionParts.join("\n"),
@ -622,7 +653,12 @@ function formatCreate(
embeds: [
{
author,
title: t("events.create.title", { emoji, type: refType, ref, repo: repo ?? t("common.repository") }),
title: t("events.create.title", {
emoji,
type: refType,
ref,
repo: repo ?? t("common.repository"),
}),
color: GITHUB_COLORS.create,
fields,
footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) },
@ -647,7 +683,12 @@ function formatDelete(
embeds: [
{
author,
title: t("events.delete.title", { emoji, type: refType, ref, repo: repo ?? t("common.repository") }),
title: t("events.delete.title", {
emoji,
type: refType,
ref,
repo: repo ?? t("common.repository"),
}),
color: GITHUB_COLORS.delete,
footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) },
timestamp: new Date().toISOString(),
@ -693,7 +734,10 @@ function formatFork(
embeds: [
{
author,
title: t("events.fork.title", { repo: repo ?? t("common.repository"), forkee: forkee?.full_name ?? t("common.unknown") }),
title: t("events.fork.title", {
repo: repo ?? t("common.repository"),
forkee: forkee?.full_name ?? t("common.unknown"),
}),
url: forkee?.html_url ?? repoUrl,
color: GITHUB_COLORS.fork,
footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) },
@ -797,7 +841,11 @@ function formatPullRequestReview(
embeds: [
{
author,
title: t("events.pr_review.title", { repo: repo ?? t("common.repository"), number: pr.number ?? "?", title: pr.title ?? t("common.untitled") }),
title: t("events.pr_review.title", {
repo: repo ?? t("common.repository"),
number: pr.number ?? "?",
title: pr.title ?? t("common.untitled"),
}),
url: review.html_url,
color: GITHUB_COLORS[colorKey],
description: descriptionParts.join("\n"),
@ -833,7 +881,10 @@ function formatPullRequestReviewComment(
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
if (comment.path) {
const loc = comment.position != null ? t("events.pr_review_comment.line", { position: comment.position }) : "";
const loc =
comment.position != null
? t("events.pr_review_comment.line", { position: comment.position })
: "";
fields.push({
name: t("fields.file"),
value: `\`${comment.path}\`${loc}`,
@ -845,7 +896,10 @@ function formatPullRequestReviewComment(
embeds: [
{
author,
title: t("events.pr_review_comment.title", { repo: repo ?? t("common.repository"), number: pr.number ?? "?" }),
title: t("events.pr_review_comment.title", {
repo: repo ?? t("common.repository"),
number: pr.number ?? "?",
}),
url: comment.html_url,
color: GITHUB_COLORS.pull_request_review_commented,
description: `${t("events.pr_review_comment.action_inline", { action: al })}\n\n> ${commentBody}${truncated ? "..." : ""}`,
@ -889,7 +943,10 @@ function formatCommitComment(
embeds: [
{
author,
title: t("events.commit_comment.title", { sha: shortSha, repo: repo ?? t("common.repository") }),
title: t("events.commit_comment.title", {
sha: shortSha,
repo: repo ?? t("common.repository"),
}),
url: comment.html_url,
color: GITHUB_COLORS.commit_comment,
description: `${t("events.commit_comment.action_comment", { action: al })}\n\n> ${commentBody}${truncated ? "..." : ""}`,
@ -1062,7 +1119,11 @@ function formatLabel(
embeds: [
{
author,
title: t("events.label.title", { emoji, action: al, name: label.name ?? t("common.unknown") }),
title: t("events.label.title", {
emoji,
action: al,
name: label.name ?? t("common.unknown"),
}),
color: GITHUB_COLORS.label,
fields: fields.length > 0 ? fields : undefined,
footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) },
@ -1133,7 +1194,11 @@ function formatMilestone(
embeds: [
{
author,
title: t("events.milestone.title", { emoji: stateEmoji, action: al, title: milestone.title ?? t("common.unknown") }),
title: t("events.milestone.title", {
emoji: stateEmoji,
action: al,
title: milestone.title ?? t("common.unknown"),
}),
url: milestone.html_url,
color:
milestone.state === "closed"
@ -1179,13 +1244,20 @@ function formatDiscussion(
embeds: [
{
author,
title: t("events.discussion.title", { emoji: stateEmoji, number: discussion.number ?? "?", title: discussion.title ?? t("common.untitled") }),
title: t("events.discussion.title", {
emoji: stateEmoji,
number: discussion.number ?? "?",
title: discussion.title ?? t("common.untitled"),
}),
url: discussion.html_url,
color:
action === "answered"
? GITHUB_COLORS.discussion_answered
: GITHUB_COLORS.discussion_created,
description: t("events.discussion.action_discussion", { action: al, category: category ? ` in **${category}**` : "" }),
description: t("events.discussion.action_discussion", {
action: al,
category: category ? ` in **${category}**` : "",
}),
footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) },
timestamp: new Date().toISOString(),
},
@ -1217,7 +1289,10 @@ function formatDiscussionComment(
embeds: [
{
author,
title: t("events.discussion_comment.comment_on", { number: discussion.number ?? "?", title: discussion.title ?? t("common.untitled") }),
title: t("events.discussion_comment.comment_on", {
number: discussion.number ?? "?",
title: discussion.title ?? t("common.untitled"),
}),
url: comment.html_url,
color: GITHUB_COLORS.discussion_comment,
description: `${t("events.discussion_comment.action_comment", { action: al })}\n\n> ${commentBody}${truncated ? "..." : ""}`,
@ -1243,7 +1318,8 @@ function formatRepository(
if (action === "renamed") {
const changes = payload.changes as { name?: { from?: string } } | undefined;
const newName = (payload.repository as { full_name?: string })?.full_name ?? t("common.unknown");
const newName =
(payload.repository as { full_name?: string })?.full_name ?? t("common.unknown");
fields.push({
name: t("fields.renamed"),
value: changes?.name?.from ? `${changes.name.from}${newName}` : newName,
@ -1492,7 +1568,10 @@ function formatGeneric(
embeds: [
{
author,
title: t("events.generic.title", { event: eventType, action: payload.action ? `: ${payload.action}` : "" }),
title: t("events.generic.title", {
event: eventType,
action: payload.action ? `: ${payload.action}` : "",
}),
color: GITHUB_COLORS.default,
footer: { text: t("common.footer", { repo: repo ?? t("common.github") }) },
timestamp: new Date().toISOString(),

View file

@ -53,6 +53,9 @@ export function t(
translations?: Translations,
): string {
const dict = translations ?? en;
const raw = getByPath(dict as Record<string, unknown>, key) ?? getByPath(en as Record<string, unknown>, key) ?? key;
const raw =
getByPath(dict as Record<string, unknown>, key) ??
getByPath(en as Record<string, unknown>, key) ??
key;
return params ? interpolate(raw, params) : raw;
}

View file

@ -35,9 +35,7 @@ export async function recordSend(kv: KVNamespace, record: SendRecord): Promise<v
export async function getSendLog(kv: KVNamespace, limit = 50): Promise<SendRecord[]> {
try {
const list = await kv.list({ prefix: KEY_PREFIX, limit: MAX_READ });
const records = await Promise.all(
list.keys.map((k) => kv.get<SendRecord>(k.name, "json")),
);
const records = await Promise.all(list.keys.map((k) => kv.get<SendRecord>(k.name, "json")));
return records
.filter((r): r is SendRecord => r != null)
.sort((a, b) => b.ts - a.ts)

View file

@ -87,9 +87,6 @@ export async function getDiscordLink(
return await kv.get(`discord-link:${discordUserId}`, "text");
}
export async function removeDiscordLink(
kv: KVNamespace,
discordUserId: string,
): Promise<void> {
export async function removeDiscordLink(kv: KVNamespace, discordUserId: string): Promise<void> {
await kv.delete(`discord-link:${discordUserId}`);
}