mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
chore: auto-fix lint & formatting [skip ci]
This commit is contained in:
parent
c75567cf15
commit
1375db9a20
15 changed files with 160 additions and 88 deletions
|
|
@ -202,7 +202,9 @@
|
||||||
:placeholder="t('routeEditor.threadPlaceholder')"
|
:placeholder="t('routeEditor.threadPlaceholder')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="form.logPlatform === 'telegram' || form.logPlatform === 'feishu'">
|
<template
|
||||||
|
v-else-if="form.logPlatform === 'telegram' || form.logPlatform === 'feishu'"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
v-model="form.logChatId"
|
v-model="form.logChatId"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,13 @@
|
||||||
: 'bg-accent'
|
: 'bg-accent'
|
||||||
"
|
"
|
||||||
></span>
|
></span>
|
||||||
{{ tg.platform === "telegram" ? "Telegram" : tg.platform === "feishu" ? t("route.feishu") : "Discord" }}
|
{{
|
||||||
|
tg.platform === "telegram"
|
||||||
|
? "Telegram"
|
||||||
|
: tg.platform === "feishu"
|
||||||
|
? t("route.feishu")
|
||||||
|
: "Discord"
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="route.fallback" class="route-badge route-badge-fallback">{{
|
<span v-if="route.fallback" class="route-badge route-badge-fallback">{{
|
||||||
t("route.fallback")
|
t("route.fallback")
|
||||||
|
|
@ -59,12 +65,16 @@
|
||||||
<div v-for="(tg, i) in route.targets" :key="i" class="route-target">
|
<div v-for="(tg, i) in route.targets" :key="i" class="route-target">
|
||||||
<div class="route-target-row">
|
<div class="route-target-row">
|
||||||
<span class="route-target-label">
|
<span class="route-target-label">
|
||||||
<template v-if="tg.platform === 'telegram' || tg.platform === 'feishu'">{{ t("route.chat") }}</template>
|
<template v-if="tg.platform === 'telegram' || tg.platform === 'feishu'">{{
|
||||||
|
t("route.chat")
|
||||||
|
}}</template>
|
||||||
<template v-else-if="tg.threadId">{{ t("route.thread") }}</template>
|
<template v-else-if="tg.threadId">{{ t("route.thread") }}</template>
|
||||||
<template v-else>{{ t("route.channel") }}</template>
|
<template v-else>{{ t("route.channel") }}</template>
|
||||||
</span>
|
</span>
|
||||||
<code class="route-target-id">
|
<code class="route-target-id">
|
||||||
<template v-if="tg.platform === 'telegram' || tg.platform === 'feishu'">{{ tg.chatId }}</template>
|
<template v-if="tg.platform === 'telegram' || tg.platform === 'feishu'">{{
|
||||||
|
tg.chatId
|
||||||
|
}}</template>
|
||||||
<template v-else-if="tg.threadId">{{ tg.threadId }}</template>
|
<template v-else-if="tg.threadId">{{ tg.threadId }}</template>
|
||||||
<template v-else>{{ tg.channelId }}</template>
|
<template v-else>{{ tg.channelId }}</template>
|
||||||
</code>
|
</code>
|
||||||
|
|
|
||||||
|
|
@ -287,7 +287,12 @@ watch(
|
||||||
? r.targets.map((tg) => ({
|
? r.targets.map((tg) => ({
|
||||||
...blankTarget(),
|
...blankTarget(),
|
||||||
...tg,
|
...tg,
|
||||||
platform: tg.platform === "telegram" ? "telegram" : tg.platform === "feishu" ? "feishu" : "discord",
|
platform:
|
||||||
|
tg.platform === "telegram"
|
||||||
|
? "telegram"
|
||||||
|
: tg.platform === "feishu"
|
||||||
|
? "feishu"
|
||||||
|
: "discord",
|
||||||
}))
|
}))
|
||||||
: [blankTarget()];
|
: [blankTarget()];
|
||||||
if (r?.ast) {
|
if (r?.ast) {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ WebHooker requires several secrets to function. For local development, store the
|
||||||
### Required Secrets
|
### Required Secrets
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
| ----------------------- | ------------------------------------------------------------------------ |
|
| ----------------------- | -------------------------------------------------------------------------- |
|
||||||
| `GITHUB_WEBHOOK_SECRET` | Webhook secret from your GitHub App settings |
|
| `GITHUB_WEBHOOK_SECRET` | Webhook secret from your GitHub App settings |
|
||||||
| `GITEA_WEBHOOK_SECRET` | Webhook secret from your Gitea instance (only to receive Gitea webhooks) |
|
| `GITEA_WEBHOOK_SECRET` | Webhook secret from your Gitea instance (only to receive Gitea webhooks) |
|
||||||
| `GITHUB_CLIENT_ID` | OAuth client ID from App settings |
|
| `GITHUB_CLIENT_ID` | OAuth client ID from App settings |
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,7 @@ To enable inbound:
|
||||||
### Required permissions
|
### Required permissions
|
||||||
|
|
||||||
| Permission | Purpose |
|
| Permission | Purpose |
|
||||||
| ---------- | ------- |
|
| ------------------------ | ---------------------------------------------------------- |
|
||||||
| `im:message` | Read and send direct messages and group chat messages. |
|
| `im:message` | Read and send direct messages and group chat messages. |
|
||||||
| `im:message:send_as_bot` | Send messages as an app bot (alternative to `im:message`). |
|
| `im:message:send_as_bot` | Send messages as an app bot (alternative to `im:message`). |
|
||||||
| `im:message:send` | Send messages V2 (historical version, alternative). |
|
| `im:message:send` | Send messages V2 (historical version, alternative). |
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,7 @@ WebHooker 使用应用级凭证(`FEISHU_APP_ID` / `FEISHU_APP_SECRET`)请求
|
||||||
### 所需权限
|
### 所需权限
|
||||||
|
|
||||||
| 权限 | 用途 |
|
| 权限 | 用途 |
|
||||||
| ---- | ---- |
|
| ------------------------ | ------------------------------------------------- |
|
||||||
| `im:message` | 读取和发送单聊与群聊消息。 |
|
| `im:message` | 读取和发送单聊与群聊消息。 |
|
||||||
| `im:message:send_as_bot` | 以应用机器人身份发送消息(`im:message` 的替代)。 |
|
| `im:message:send_as_bot` | 以应用机器人身份发送消息(`im:message` 的替代)。 |
|
||||||
| `im:message:send` | 旧版发送消息权限(`im:message` 的替代)。 |
|
| `im:message:send` | 旧版发送消息权限(`im:message` 的替代)。 |
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,11 @@ export class FeishuDriver implements PlatformDriver {
|
||||||
}
|
}
|
||||||
const tokenRes = await getTenantAccessToken(env);
|
const tokenRes = await getTenantAccessToken(env);
|
||||||
if (!tokenRes.ok || !tokenRes.token) {
|
if (!tokenRes.ok || !tokenRes.token) {
|
||||||
return { ok: false, error: tokenRes.error ?? "No token", errorCode: tokenRes.errorCode ?? "NO_TOKEN" };
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: tokenRes.error ?? "No token",
|
||||||
|
errorCode: tokenRes.errorCode ?? "NO_TOKEN",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return sendMessage(tokenRes.token, chatId, renderNeutralMessage(message));
|
return sendMessage(tokenRes.token, chatId, renderNeutralMessage(message));
|
||||||
}
|
}
|
||||||
|
|
@ -30,7 +34,11 @@ export class FeishuDriver implements PlatformDriver {
|
||||||
}
|
}
|
||||||
const tokenRes = await getTenantAccessToken(env);
|
const tokenRes = await getTenantAccessToken(env);
|
||||||
if (!tokenRes.ok || !tokenRes.token) {
|
if (!tokenRes.ok || !tokenRes.token) {
|
||||||
return { ok: false, error: tokenRes.error ?? "No token", errorCode: tokenRes.errorCode ?? "NO_TOKEN" };
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: tokenRes.error ?? "No token",
|
||||||
|
errorCode: tokenRes.errorCode ?? "NO_TOKEN",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return updateMessage(tokenRes.token, messageId, renderNeutralMessage(message));
|
return updateMessage(tokenRes.token, messageId, renderNeutralMessage(message));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,7 @@ const MAX_MARKDOWN = 4000;
|
||||||
const MAX_NOTE = 500;
|
const MAX_NOTE = 500;
|
||||||
|
|
||||||
type FeishuTemplate =
|
type FeishuTemplate =
|
||||||
| "blue"
|
"blue" | "green" | "red" | "yellow" | "orange" | "purple" | "indigo" | "wathet" | "lime" | "grey";
|
||||||
| "green"
|
|
||||||
| "red"
|
|
||||||
| "yellow"
|
|
||||||
| "orange"
|
|
||||||
| "purple"
|
|
||||||
| "indigo"
|
|
||||||
| "wathet"
|
|
||||||
| "lime"
|
|
||||||
| "grey";
|
|
||||||
|
|
||||||
function colorToTemplate(color?: number): FeishuTemplate {
|
function colorToTemplate(color?: number): FeishuTemplate {
|
||||||
if (color == null) return "blue";
|
if (color == null) return "blue";
|
||||||
|
|
@ -89,7 +80,9 @@ export function renderNeutralMessage(message: NeutralMessage): Record<string, un
|
||||||
|
|
||||||
const meta: string[] = [];
|
const meta: string[] = [];
|
||||||
if (message.forge?.name) {
|
if (message.forge?.name) {
|
||||||
meta.push(message.forge.url ? `[${message.forge.name}](${message.forge.url})` : message.forge.name);
|
meta.push(
|
||||||
|
message.forge.url ? `[${message.forge.name}](${message.forge.url})` : message.forge.name,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (message.footer) meta.push(message.footer);
|
if (message.footer) meta.push(message.footer);
|
||||||
const ts = formatTimestamp(message.timestamp);
|
const ts = formatTimestamp(message.timestamp);
|
||||||
|
|
@ -128,7 +121,8 @@ export function renderNeutralMessage(message: NeutralMessage): Record<string, un
|
||||||
actions: message.actions.map((action) => ({
|
actions: message.actions.map((action) => ({
|
||||||
tag: "button",
|
tag: "button",
|
||||||
text: { tag: "plain_text", content: action.label },
|
text: { tag: "plain_text", content: action.label },
|
||||||
type: action.style === "danger" ? "danger" : action.style === "primary" ? "primary" : "default",
|
type:
|
||||||
|
action.style === "danger" ? "danger" : action.style === "primary" ? "primary" : "default",
|
||||||
action_id: action.id,
|
action_id: action.id,
|
||||||
value: { v: action.id },
|
value: { v: action.id },
|
||||||
})),
|
})),
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,10 @@ async function feishuRequest(
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
lastError = data?.msg ?? data?.error?.message ?? `HTTP ${res.status}`;
|
lastError = data?.msg ?? data?.error?.message ?? `HTTP ${res.status}`;
|
||||||
log.error({ status: res.status, err: lastError, label, attempts: attempt + 1 }, "Feishu API error");
|
log.error(
|
||||||
|
{ status: res.status, err: lastError, label, attempts: attempt + 1 },
|
||||||
|
"Feishu API error",
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: lastError,
|
error: lastError,
|
||||||
|
|
@ -60,7 +63,10 @@ async function feishuRequest(
|
||||||
|
|
||||||
if (data && typeof data.code === "number" && data.code !== 0) {
|
if (data && typeof data.code === "number" && data.code !== 0) {
|
||||||
lastError = data.msg ?? `Feishu code ${data.code}`;
|
lastError = data.msg ?? `Feishu code ${data.code}`;
|
||||||
log.error({ code: data.code, err: lastError, label, attempts: attempt + 1 }, "Feishu business error");
|
log.error(
|
||||||
|
{ code: data.code, err: lastError, label, attempts: attempt + 1 },
|
||||||
|
"Feishu business error",
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: lastError,
|
error: lastError,
|
||||||
|
|
@ -113,7 +119,11 @@ export async function getTenantAccessToken(env: Env): Promise<TokenResult> {
|
||||||
const appId = env.FEISHU_APP_ID?.trim();
|
const appId = env.FEISHU_APP_ID?.trim();
|
||||||
const appSecret = env.FEISHU_APP_SECRET?.trim();
|
const appSecret = env.FEISHU_APP_SECRET?.trim();
|
||||||
if (!appId || !appSecret) {
|
if (!appId || !appSecret) {
|
||||||
return { ok: false, error: "FEISHU_APP_ID/FEISHU_APP_SECRET not configured", errorCode: "NO_TOKEN" };
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: "FEISHU_APP_ID/FEISHU_APP_SECRET not configured",
|
||||||
|
errorCode: "NO_TOKEN",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const cached = await env.KV.get(TOKEN_KEY);
|
const cached = await env.KV.get(TOKEN_KEY);
|
||||||
|
|
@ -126,9 +136,12 @@ export async function getTenantAccessToken(env: Env): Promise<TokenResult> {
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
|
body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
|
||||||
});
|
});
|
||||||
const data = (await res.json().catch(() => null)) as
|
const data = (await res.json().catch(() => null)) as {
|
||||||
| { code?: number; msg?: string; tenant_access_token?: string; expire?: number }
|
code?: number;
|
||||||
| null;
|
msg?: string;
|
||||||
|
tenant_access_token?: string;
|
||||||
|
expire?: number;
|
||||||
|
} | null;
|
||||||
if (!res.ok || !data || data.code !== 0 || !data.tenant_access_token) {
|
if (!res.ok || !data || data.code !== 0 || !data.tenant_access_token) {
|
||||||
const err = data?.msg ?? `HTTP ${res.status}`;
|
const err = data?.msg ?? `HTTP ${res.status}`;
|
||||||
return { ok: false, error: err, errorCode: "FEISHU_TOKEN", status: res.status };
|
return { ok: false, error: err, errorCode: "FEISHU_TOKEN", status: res.status };
|
||||||
|
|
@ -169,11 +182,7 @@ export async function updateMessage(
|
||||||
return feishuRequest(url, "PATCH", token, body, messageId);
|
return feishuRequest(url, "PATCH", token, body, messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendText(
|
export async function sendText(token: string, chatId: string, text: string): Promise<SendResult> {
|
||||||
token: string,
|
|
||||||
chatId: string,
|
|
||||||
text: string,
|
|
||||||
): Promise<SendResult> {
|
|
||||||
const url = `${FEISHU_API}/open-apis/im/v1/messages?receive_id_type=chat_id`;
|
const url = `${FEISHU_API}/open-apis/im/v1/messages?receive_id_type=chat_id`;
|
||||||
const body: Record<string, unknown> = {
|
const body: Record<string, unknown> = {
|
||||||
receive_id: chatId,
|
receive_id: chatId,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import type { Env } from "../../types";
|
import type { Env } from "../../types";
|
||||||
import { log } from "../../lib/log";
|
import { log } from "../../lib/log";
|
||||||
import { getOAuthURL, commentAsUser, mergePullRequestAsUser, closePullRequestAsUser } from "../../github/oauth";
|
import {
|
||||||
|
getOAuthURL,
|
||||||
|
commentAsUser,
|
||||||
|
mergePullRequestAsUser,
|
||||||
|
closePullRequestAsUser,
|
||||||
|
} from "../../github/oauth";
|
||||||
import { getFeishuLink, removeFeishuLink } from "../../github/store";
|
import { getFeishuLink, removeFeishuLink } from "../../github/store";
|
||||||
import { getTenantAccessToken, sendText, updateCard } from "./rest";
|
import { getTenantAccessToken, sendText, updateCard } from "./rest";
|
||||||
|
|
||||||
|
|
@ -77,7 +82,8 @@ function describeError(err: unknown): string {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
const msg = err.message;
|
const msg = err.message;
|
||||||
if (msg.includes("GITHUB_TOKEN_EXPIRED")) return "GitHub 绑定已过期,请重新用 /gh login 绑定。";
|
if (msg.includes("GITHUB_TOKEN_EXPIRED")) return "GitHub 绑定已过期,请重新用 /gh login 绑定。";
|
||||||
if (msg.includes("GITHUB_FORBIDDEN")) return "没有权限操作该仓库(GitHub 返回 403),请确认你的账号权限。";
|
if (msg.includes("GITHUB_FORBIDDEN"))
|
||||||
|
return "没有权限操作该仓库(GitHub 返回 403),请确认你的账号权限。";
|
||||||
if (msg.includes("GITHUB_NOT_FOUND")) return "未找到对应的 GitHub 资源(404)。";
|
if (msg.includes("GITHUB_NOT_FOUND")) return "未找到对应的 GitHub 资源(404)。";
|
||||||
return msg || "操作失败";
|
return msg || "操作失败";
|
||||||
}
|
}
|
||||||
|
|
@ -111,11 +117,15 @@ async function handleLogin(env: Env, openId: string, chatId: string): Promise<vo
|
||||||
};
|
};
|
||||||
await env.KV.put(`state:${state}`, JSON.stringify(pending), { expirationTtl: 600 });
|
await env.KV.put(`state:${state}`, JSON.stringify(pending), { expirationTtl: 600 });
|
||||||
const url = getOAuthURL(env.GITHUB_CLIENT_ID ?? "", state);
|
const url = getOAuthURL(env.GITHUB_CLIENT_ID ?? "", state);
|
||||||
await replyToken(env, chatId, [
|
await replyToken(
|
||||||
|
env,
|
||||||
|
chatId,
|
||||||
|
[
|
||||||
"点击下方链接绑定 GitHub 账号:",
|
"点击下方链接绑定 GitHub 账号:",
|
||||||
url,
|
url,
|
||||||
"绑定后可在飞书里用 /gh comment 评论、/gh merge 合并、/gh close 关闭 PR。",
|
"绑定后可在飞书里用 /gh comment 评论、/gh merge 合并、/gh close 关闭 PR。",
|
||||||
].join("\n"));
|
].join("\n"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLogout(env: Env, openId: string, chatId: string): Promise<void> {
|
async function handleLogout(env: Env, openId: string, chatId: string): Promise<void> {
|
||||||
|
|
@ -128,7 +138,12 @@ async function handleLogout(env: Env, openId: string, chatId: string): Promise<v
|
||||||
await replyToken(env, chatId, "已解绑 GitHub 账号。");
|
await replyToken(env, chatId, "已解绑 GitHub 账号。");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleComment(env: Env, openId: string, chatId: string, text: string): Promise<void> {
|
async function handleComment(
|
||||||
|
env: Env,
|
||||||
|
openId: string,
|
||||||
|
chatId: string,
|
||||||
|
text: string,
|
||||||
|
): Promise<void> {
|
||||||
const githubUserId = await getFeishuLink(env.DB, openId);
|
const githubUserId = await getFeishuLink(env.DB, openId);
|
||||||
if (!githubUserId) {
|
if (!githubUserId) {
|
||||||
await replyToken(env, chatId, "请先 /gh login 绑定 GitHub 账号。");
|
await replyToken(env, chatId, "请先 /gh login 绑定 GitHub 账号。");
|
||||||
|
|
@ -136,16 +151,31 @@ async function handleComment(env: Env, openId: string, chatId: string, text: str
|
||||||
}
|
}
|
||||||
const target = extractTarget(text);
|
const target = extractTarget(text);
|
||||||
if (!target) {
|
if (!target) {
|
||||||
await replyToken(env, chatId, "请在消息里带上 PR/Issue 链接,例如:/gh comment https://github.com/o/r/pull/7 看起来不错");
|
await replyToken(
|
||||||
|
env,
|
||||||
|
chatId,
|
||||||
|
"请在消息里带上 PR/Issue 链接,例如:/gh comment https://github.com/o/r/pull/7 看起来不错",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ghIdx = text.indexOf("/gh");
|
const ghIdx = text.indexOf("/gh");
|
||||||
const rest = text.slice(ghIdx + 3).trim();
|
const rest = text.slice(ghIdx + 3).trim();
|
||||||
const parts = rest.split(/\s+/);
|
const parts = rest.split(/\s+/);
|
||||||
const linkIdx = parts.findIndex((p) => p.includes("github.com"));
|
const linkIdx = parts.findIndex((p) => p.includes("github.com"));
|
||||||
const body = parts.slice(linkIdx + 1).join(" ").trim() || "(来自飞书)";
|
const body =
|
||||||
|
parts
|
||||||
|
.slice(linkIdx + 1)
|
||||||
|
.join(" ")
|
||||||
|
.trim() || "(来自飞书)";
|
||||||
try {
|
try {
|
||||||
const res = await commentAsUser(env.KV, githubUserId, target.owner, target.repo, target.number, body);
|
const res = await commentAsUser(
|
||||||
|
env.KV,
|
||||||
|
githubUserId,
|
||||||
|
target.owner,
|
||||||
|
target.repo,
|
||||||
|
target.number,
|
||||||
|
body,
|
||||||
|
);
|
||||||
await replyToken(env, chatId, `✅ 已评论:[查看](${res.htmlUrl})(@${res.login})`);
|
await replyToken(env, chatId, `✅ 已评论:[查看](${res.htmlUrl})(@${res.login})`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await replyToken(env, chatId, `❌ ${describeError(err)}`);
|
await replyToken(env, chatId, `❌ ${describeError(err)}`);
|
||||||
|
|
@ -166,9 +196,13 @@ async function handleMergeOrClose(
|
||||||
}
|
}
|
||||||
const target = extractTarget(body);
|
const target = extractTarget(body);
|
||||||
if (!target) {
|
if (!target) {
|
||||||
await replyToken(env, chatId, action === "merge"
|
await replyToken(
|
||||||
|
env,
|
||||||
|
chatId,
|
||||||
|
action === "merge"
|
||||||
? "请在消息里带上 PR 链接,例如:/gh merge https://github.com/o/r/pull/7"
|
? "请在消息里带上 PR 链接,例如:/gh merge https://github.com/o/r/pull/7"
|
||||||
: "请在消息里带上 PR 链接,例如:/gh close https://github.com/o/r/pull/7");
|
: "请在消息里带上 PR 链接,例如:/gh close https://github.com/o/r/pull/7",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|
@ -177,7 +211,11 @@ async function handleMergeOrClose(
|
||||||
} else {
|
} else {
|
||||||
await closePullRequestAsUser(env.KV, githubUserId, target.owner, target.repo, target.number);
|
await closePullRequestAsUser(env.KV, githubUserId, target.owner, target.repo, target.number);
|
||||||
}
|
}
|
||||||
await replyToken(env, chatId, `✅ 已${action === "merge" ? "合并" : "关闭"} ${target.owner}/${target.repo}#${target.number}`);
|
await replyToken(
|
||||||
|
env,
|
||||||
|
chatId,
|
||||||
|
`✅ 已${action === "merge" ? "合并" : "关闭"} ${target.owner}/${target.repo}#${target.number}`,
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await replyToken(env, chatId, `❌ ${describeError(err)}`);
|
await replyToken(env, chatId, `❌ ${describeError(err)}`);
|
||||||
}
|
}
|
||||||
|
|
@ -218,7 +256,11 @@ async function handleMessage(env: Env, event: FeishuEvent): Promise<void> {
|
||||||
await handleMergeOrClose(env, openId, chatId, "close", text);
|
await handleMergeOrClose(env, openId, chatId, "close", text);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
await replyToken(env, chatId, "未知指令。可用:/gh login | logout | comment <链接> <内容> | merge <链接> | close <链接>");
|
await replyToken(
|
||||||
|
env,
|
||||||
|
chatId,
|
||||||
|
"未知指令。可用:/gh login | logout | comment <链接> <内容> | merge <链接> | close <链接>",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -238,8 +280,7 @@ async function handleCardAction(env: Env, payload: Record<string, unknown>): Pro
|
||||||
const event = (payload.event ?? {}) as FeishuEvent;
|
const event = (payload.event ?? {}) as FeishuEvent;
|
||||||
const action = event.action ?? {};
|
const action = event.action ?? {};
|
||||||
const actionId = action.action_id ?? action.value?.v ?? "";
|
const actionId = action.action_id ?? action.value?.v ?? "";
|
||||||
const openId =
|
const openId = event.operator?.operator_id?.open_id ?? event.sender?.sender_id?.open_id ?? "";
|
||||||
event.operator?.operator_id?.open_id ?? event.sender?.sender_id?.open_id ?? "";
|
|
||||||
const cardToken = event.token ?? "";
|
const cardToken = event.token ?? "";
|
||||||
const openMessageId = event.open_message_id ?? "";
|
const openMessageId = event.open_message_id ?? "";
|
||||||
|
|
||||||
|
|
@ -282,7 +323,11 @@ async function handleCardAction(env: Env, payload: Record<string, unknown>): Pro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function replyCard(env: Env, cardToken: string, card: Record<string, unknown>): Promise<void> {
|
async function replyCard(
|
||||||
|
env: Env,
|
||||||
|
cardToken: string,
|
||||||
|
card: Record<string, unknown>,
|
||||||
|
): Promise<void> {
|
||||||
const tokenRes = await getTenantAccessToken(env);
|
const tokenRes = await getTenantAccessToken(env);
|
||||||
if (!tokenRes.ok || !tokenRes.token) return;
|
if (!tokenRes.ok || !tokenRes.token) return;
|
||||||
await updateCard(tokenRes.token, cardToken, card);
|
await updateCard(tokenRes.token, cardToken, card);
|
||||||
|
|
@ -317,8 +362,7 @@ export async function handleFeishuWebhookRequest(request: Request, env: Env): Pr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventType =
|
const eventType = (header?.event_type as string) ?? eventPart?.type ?? type;
|
||||||
(header?.event_type as string) ?? eventPart?.type ?? type;
|
|
||||||
try {
|
try {
|
||||||
if (eventType === "im.message.receive_v1" || eventType === "message") {
|
if (eventType === "im.message.receive_v1" || eventType === "message") {
|
||||||
await handleMessage(env, (payload.event ?? payload) as FeishuEvent);
|
await handleMessage(env, (payload.event ?? payload) as FeishuEvent);
|
||||||
|
|
|
||||||
|
|
@ -126,17 +126,12 @@ export async function saveFeishuLink(
|
||||||
githubUserId: string,
|
githubUserId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await db
|
await db
|
||||||
.prepare(
|
.prepare("INSERT OR REPLACE INTO feishu_links (feishu_user_id, github_user_id) VALUES (?, ?)")
|
||||||
"INSERT OR REPLACE INTO feishu_links (feishu_user_id, github_user_id) VALUES (?, ?)",
|
|
||||||
)
|
|
||||||
.bind(feishuUserId, githubUserId)
|
.bind(feishuUserId, githubUserId)
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFeishuLink(
|
export async function getFeishuLink(db: D1Database, feishuUserId: string): Promise<string | null> {
|
||||||
db: D1Database,
|
|
||||||
feishuUserId: string,
|
|
||||||
): Promise<string | null> {
|
|
||||||
const { results } = await db
|
const { results } = await db
|
||||||
.prepare("SELECT github_user_id FROM feishu_links WHERE feishu_user_id = ?")
|
.prepare("SELECT github_user_id FROM feishu_links WHERE feishu_user_id = ?")
|
||||||
.bind(feishuUserId)
|
.bind(feishuUserId)
|
||||||
|
|
@ -145,8 +140,5 @@ export async function getFeishuLink(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeFeishuLink(db: D1Database, feishuUserId: string): Promise<void> {
|
export async function removeFeishuLink(db: D1Database, feishuUserId: string): Promise<void> {
|
||||||
await db
|
await db.prepare("DELETE FROM feishu_links WHERE feishu_user_id = ?").bind(feishuUserId).run();
|
||||||
.prepare("DELETE FROM feishu_links WHERE feishu_user_id = ?")
|
|
||||||
.bind(feishuUserId)
|
|
||||||
.run();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -256,7 +256,8 @@ function validateTarget(
|
||||||
platform,
|
platform,
|
||||||
channelId: platform === "discord" ? (target.channelId as string) : undefined,
|
channelId: platform === "discord" ? (target.channelId as string) : undefined,
|
||||||
threadId: platform === "discord" ? ((target.threadId as string) ?? undefined) : undefined,
|
threadId: platform === "discord" ? ((target.threadId as string) ?? undefined) : undefined,
|
||||||
chatId: platform === "telegram" || platform === "feishu" ? (target.chatId as string) : undefined,
|
chatId:
|
||||||
|
platform === "telegram" || platform === "feishu" ? (target.chatId as string) : undefined,
|
||||||
topicId: platform === "telegram" ? ((target.topicId as string) ?? undefined) : undefined,
|
topicId: platform === "telegram" ? ((target.topicId as string) ?? undefined) : undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { describe, it, expect } from "bun:test";
|
import { describe, it, expect } from "bun:test";
|
||||||
import type { Env } from "../server/lib/types";
|
import type { Env } from "../server/lib/types";
|
||||||
import { verifyFeishuSignature, handleFeishuWebhookRequest } from "../server/lib/drivers/feishu/updates";
|
import {
|
||||||
|
verifyFeishuSignature,
|
||||||
|
handleFeishuWebhookRequest,
|
||||||
|
} from "../server/lib/drivers/feishu/updates";
|
||||||
|
|
||||||
function sign(secret: string, timestamp: string, nonce: string, body: string): Promise<string> {
|
function sign(secret: string, timestamp: string, nonce: string, body: string): Promise<string> {
|
||||||
return (async () => {
|
return (async () => {
|
||||||
|
|
@ -11,7 +14,11 @@ function sign(secret: string, timestamp: string, nonce: string, body: string): P
|
||||||
false,
|
false,
|
||||||
["sign"],
|
["sign"],
|
||||||
);
|
);
|
||||||
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${timestamp}\n${nonce}\n${body}`));
|
const sig = await crypto.subtle.sign(
|
||||||
|
"HMAC",
|
||||||
|
key,
|
||||||
|
new TextEncoder().encode(`${timestamp}\n${nonce}\n${body}`),
|
||||||
|
);
|
||||||
let binary = "";
|
let binary = "";
|
||||||
const bytes = new Uint8Array(sig);
|
const bytes = new Uint8Array(sig);
|
||||||
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i] ?? 0);
|
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i] ?? 0);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue