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

@ -8,6 +8,7 @@ import {
clearAdminCookie,
} from "../web/session";
import { groupAcceptsProvider } from "../web/groups";
import { validateGroups } from "../web/admin-routes";
import { loadRoutes, saveRoutes, loadConfig } from "../config";
import type { Env, Route, Group } from "../types";
@ -132,6 +133,72 @@ describe("groupAcceptsProvider", () => {
});
});
describe("validateGroups logTarget", () => {
const baseGroup = {
id: "g",
name: "G",
members: [{ login: "boss", role: "owner" }],
};
it("accepts and normalizes a discord log target", () => {
const res = validateGroups([
{
...baseGroup,
logTarget: { platform: "discord", channelId: "111", threadId: "222" },
},
]);
expect(res.ok).toBe(true);
if (res.ok) {
expect(res.groups[0]!.logTarget).toEqual({
platform: "discord",
channelId: "111",
threadId: "222",
chatId: undefined,
topicId: undefined,
});
}
});
it("accepts a telegram log target", () => {
const res = validateGroups([
{
...baseGroup,
logTarget: { platform: "telegram", chatId: "-100123", topicId: "999" },
},
]);
expect(res.ok).toBe(true);
if (res.ok) {
expect(res.groups[0]!.logTarget).toEqual({
platform: "telegram",
channelId: undefined,
threadId: undefined,
chatId: "-100123",
topicId: "999",
});
}
});
it("rejects a log target without a channel id", () => {
const res = validateGroups([
{ ...baseGroup, logTarget: { platform: "discord", channelId: "" } },
]);
expect(res.ok).toBe(false);
if (!res.ok) expect(res.error).toContain("logTarget.channelId");
});
it("rejects a log target with an unknown platform", () => {
const res = validateGroups([{ ...baseGroup, logTarget: { platform: "slack" } }]);
expect(res.ok).toBe(false);
if (!res.ok) expect(res.error).toContain("logTarget.platform");
});
it("drops a null log target", () => {
const res = validateGroups([{ ...baseGroup, logTarget: null }]);
expect(res.ok).toBe(true);
if (res.ok) expect(res.groups[0]!.logTarget).toBeUndefined();
});
});
describe("config routes persistence", () => {
it("saves and loads routes from KV", async () => {
const kv = createMockKV();