feat(groups): per-group webhook log channel (logTarget)

Add an optional Discord channel/thread or Telegram chat/topic (Group.logTarget) that receives a summary message per webhook the group's routes dispatched: event/action, repo, delivery id and per route×target OK/FAIL lines with green/red embed color. Validated by the admin API and editable in the group editor; docs and example config updated.
This commit is contained in:
RhenCloud 2026-08-13 08:36:46 +08:00
parent b35c2c2f90
commit 0b078d938b
No known key found for this signature in database
GPG key ID: A574A617378C4E0B
17 changed files with 526 additions and 35 deletions

View file

@ -689,6 +689,29 @@ main {
box-shadow: 0 0 0 3px var(--accent-dim);
}
.field select {
width: 100%;
padding: 10px 12px;
background: var(--surface);
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
color: var(--text);
font-family: var(--ui);
font-size: 13.5px;
cursor: pointer;
outline: none;
}
.field select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-dim);
}
.field select + input[type="text"],
.field input[type="text"] + input[type="text"] {
margin-top: 8px;
}
.field .hint {
font-size: 11px;
color: var(--faint);

View file

@ -106,6 +106,42 @@
</div>
<div class="hint">{{ t("groupEditor.providersHint") }}</div>
</div>
<div class="field">
<label
>{{ t("groupEditor.logTarget") }}
<span class="lbl-note">{{ t("groupEditor.logTargetNote") }}</span></label
>
<select v-model="form.logPlatform">
<option value="">{{ t("groupEditor.logDisabled") }}</option>
<option value="discord">Discord</option>
<option value="telegram">Telegram</option>
</select>
<template v-if="form.logPlatform === 'discord'">
<input
v-model="form.logChannelId"
type="text"
:placeholder="t('routeEditor.channelPlaceholder')"
/>
<input
v-model="form.logThreadId"
type="text"
:placeholder="t('routeEditor.threadPlaceholder')"
/>
</template>
<template v-else-if="form.logPlatform === 'telegram'">
<input
v-model="form.logChatId"
type="text"
:placeholder="t('routeEditor.chatPlaceholder')"
/>
<input
v-model="form.logTopicId"
type="text"
:placeholder="t('routeEditor.topicPlaceholder')"
/>
</template>
<div class="hint">{{ t("groupEditor.logTargetHint") }}</div>
</div>
<div class="err">{{ formError }}</div>
</form>
<div class="editor-foot">
@ -149,6 +185,11 @@ const form = reactive({
providers: [] as ("github" | "gitea")[],
emoji: true,
lang: "",
logPlatform: "" as "" | "discord" | "telegram",
logChannelId: "",
logThreadId: "",
logChatId: "",
logTopicId: "",
});
function splitList(text: string): string[] {
@ -170,6 +211,7 @@ watch(
(open) => {
if (!open) return;
const g = props.group;
const lt = g?.logTarget;
form.id = g?.id ?? "";
form.name = g?.name ?? "";
form.owners = (g?.owners ?? []).join(", ");
@ -178,6 +220,11 @@ watch(
);
form.emoji = g?.emoji ?? true;
form.lang = g?.lang ?? "";
form.logPlatform = lt?.platform ?? "";
form.logChannelId = lt?.channelId ?? "";
form.logThreadId = lt?.threadId ?? "";
form.logChatId = lt?.chatId ?? "";
form.logTopicId = lt?.topicId ?? "";
formError.value = "";
},
);
@ -198,6 +245,29 @@ function save(): void {
formError.value = t("groupEditor.errName");
return;
}
let logTarget:
| { platform: "discord"; channelId: string; threadId?: string }
| {
platform: "telegram";
chatId: string;
topicId?: string;
}
| undefined;
if (form.logPlatform === "discord") {
const channelId = form.logChannelId.trim();
if (!channelId) {
formError.value = t("groupEditor.errLogChannel");
return;
}
logTarget = { platform: "discord", channelId, threadId: form.logThreadId.trim() || undefined };
} else if (form.logPlatform === "telegram") {
const chatId = form.logChatId.trim();
if (!chatId) {
formError.value = t("groupEditor.errLogChat");
return;
}
logTarget = { platform: "telegram", chatId, topicId: form.logTopicId.trim() || undefined };
}
const owners = splitList(form.owners);
const members = props.group?.members
? props.group.members.map((m) => ({ ...m }))
@ -213,6 +283,7 @@ function save(): void {
providers: form.providers.length ? form.providers : undefined,
emoji: form.emoji,
lang: form.lang.trim() || undefined,
logTarget,
});
}
</script>

View file

@ -168,6 +168,13 @@ const en: Dict = {
"groupEditor.providersHint":
"Only webhook events from the checked forges (GitHub / Gitea) enter this group's routes.",
"groupEditor.emoji": "Show emojis in messages",
"groupEditor.logTarget": "Webhook log channel",
"groupEditor.logTargetNote": "(optional)",
"groupEditor.logDisabled": "— Disabled —",
"groupEditor.logTargetHint":
"A Discord channel/thread or Telegram chat/topic that receives a summary of every webhook this group's routes dispatch.",
"groupEditor.errLogChannel": "Log channel ID is required when Discord is selected",
"groupEditor.errLogChat": "Log chat ID is required when Telegram is selected",
"groupEditor.cancel": "Cancel",
"groupEditor.save": "Save group",
"groupEditor.errIdFormat": "ID must be a-z / 0-9 / dashes",
@ -384,6 +391,13 @@ const zh: Dict = {
"groupEditor.providersHint":
"只有来自所勾选 forgeGitHub / Gitea的 webhook 事件才会进入本分组的路由。",
"groupEditor.emoji": "消息中显示表情符号",
"groupEditor.logTarget": "Webhook 日志频道",
"groupEditor.logTargetNote": "(可选)",
"groupEditor.logDisabled": "— 未启用 —",
"groupEditor.logTargetHint":
"一个 Discord 频道/子区或 Telegram 群组/话题本分组路由分发dispatch的每个 webhook 都会向其发送摘要。",
"groupEditor.errLogChannel": "选择 Discord 时必须填写日志频道 ID",
"groupEditor.errLogChat": "选择 Telegram 时必须填写日志群组 ID",
"groupEditor.cancel": "取消",
"groupEditor.save": "保存分组",
"groupEditor.errIdFormat": "ID 只能是 a-z / 0-9 / 短横线",

View file

@ -40,6 +40,7 @@ export interface Group {
providers?: ("github" | "gitea" | "gitlab")[];
emoji?: boolean;
lang?: string;
logTarget?: RouteTarget;
}
export interface GroupInvite {