From 3f6f7f17b52133ab4178896686a3c983dab08cf5 Mon Sep 17 00:00:00 2001 From: RhenCloud Date: Fri, 14 Aug 2026 08:49:48 +0800 Subject: [PATCH] feat(groups): host-based forge sources with optional display name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - forgeSources entries are now { host, type, name? }: the repository URL's hostname is matched case-insensitively against host (github.com for GitHub, distinct hosts for multiple Gitea instances); the footer label is the optional name, falling back to the host - GroupEditor renders one row per source: host input + type select + optional display name (grid layout); hostname validation mirrors the server - fix: apiFetch sends Content-Type: application/json — h3's readBody only parses JSON bodies with that header, so every PUT/POST from the refactored console arrived as a raw string and failed with 'groups must be an array' - hardening: readJsonBody (admin + actions) JSON-parses string bodies so curl and older clients without the content-type header still work - regression test: groups PUT without content-type + forgeSources round-trip - docs: groups.md/message-format.md (en/zh), AGENTS.md, config.example.yaml --- AGENTS.md | 4 +- app/assets/css/main.css | 26 +++++++ app/components/GroupEditor.vue | 83 ++++++++++++++------ app/composables/useApi.ts | 10 ++- app/composables/useI18n.ts | 26 ++++--- app/types.ts | 8 +- config.example.yaml | 11 ++- docs/guide/groups.md | 18 +++-- docs/guide/message-format.md | 2 +- docs/zh/guide/groups.md | 18 +++-- docs/zh/guide/message-format.md | 2 +- server/lib/core/dispatch.ts | 4 +- server/lib/formatters/helpers.ts | 67 ++++++++++------ server/lib/types.ts | 34 ++++++-- server/lib/web/actions.ts | 10 ++- server/lib/web/admin.ts | 67 +++++++++++++++- tests/admin-api.test.ts | 47 ++++++++++- tests/discord.test.ts | 11 ++- tests/formatter.test.ts | 129 +++++++++++++++++++++++++++---- 19 files changed, 469 insertions(+), 108 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f133059..9a8a103 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ Core pipeline: GitHub Webhook → Worker (verify + filter + format) → Discord - Invites: single-use 7-day links (`invite:{token}`) for joining a group as admin/viewer; `ALLOW_SELF_SIGNUP=1` gives access-less users a personal group on first login (self-service SaaS entry) - Audit log: every admin operation (logins, group/route/member/invite changes) recorded in D1 `audit_logs`; pruned by the scheduled trigger after `AUDIT_RETENTION_DAYS` (default 90) - Group webhook log channel: optional `Group.logTarget` (Discord channel/thread or Telegram chat/topic) receives one summary message per webhook the group's routes dispatched (event, repo, delivery id, per route×target ✅/❌ outcome, green/red color); best-effort, not recorded in `send_logs` -- Per-group forge branding: optional `Group.forgeLabel` (default off) renders the source forge in the message footer — GitHub brand + favicon, Gitea instance hostname + favicon (from the repo URL origin), or plain `Custom` for custom webhooks; Discord shows the footer icon + name, Telegram a linked name +- Per-group forge branding: optional `Group.forgeSources` (a list of `{ host, type: "github" | "gitea", name? }` the group defines itself) labels each message's footer with the first entry whose type matches the event's provider and whose host matches the repository URL's hostname (GitHub matches `github.com`, so two Gitea instances can be `git1.example.com`/`git2.example.com`); the label is the entry's optional `name` (fallback: host); links/favicons are derived from the repo URL; Discord shows the footer icon + name, Telegram a linked name - Local dev: wrangler + Miniflare ## Architecture @@ -125,7 +125,7 @@ tests/ # bun test unit tests (webhook, formatter, discord, tel - Record every admin operation (login/logout, group/route/member/invite changes) to D1 `audit_logs`; the scheduled trigger prunes entries past `AUDIT_RETENTION_DAYS` - Mention Discord roles on route trigger: route-level `discordRoleIds` are rendered as `<@&id>` into the Discord message `content` (Telegram targets ignore the field) - Format 28 GitHub/Gitea event types plus `custom` webhooks as platform-neutral messages (Discord embeds + Telegram HTML) -- Show the forge source (GitHub / Gitea instance / custom) in the message footer when the group enables `Group.forgeLabel` +- Show the forge source (named per `Group.forgeSources` host entries) in the message footer when the group defines a host matching the event's repository - Route messages to Discord channels/threads and Telegram chats/topics via REST - Edit already-sent messages in place for `workflow_run` / `check_run` progress (stable `updateKey`, KV `msg:*` tracking) - Record every dispatch attempt to D1 `send_logs` (route id, event, target, ok/error, duration, error code) diff --git a/app/assets/css/main.css b/app/assets/css/main.css index 897a8fe..f806797 100644 --- a/app/assets/css/main.css +++ b/app/assets/css/main.css @@ -478,6 +478,32 @@ @apply mt-1 w-full justify-center; } + .forge-source-row { + @apply mb-2 grid items-center gap-2 rounded-sm border border-border bg-surface-2 p-2.5; + grid-template-areas: + "host type del" + "name name name"; + grid-template-columns: 1fr auto auto; + } + + .forge-source-row .f-host { + grid-area: host; + } + + .forge-source-row .select.forge-type { + grid-area: type; + @apply w-[110px]; + } + + .forge-source-row .f-name { + grid-area: name; + } + + .forge-source-row > .icon-btn { + grid-area: del; + justify-self: end; + } + /* ---- Toasts ---- */ .toasts { @apply pointer-events-none fixed bottom-7 left-1/2 z-[60] flex -translate-x-1/2 flex-col items-center gap-2; diff --git a/app/components/GroupEditor.vue b/app/components/GroupEditor.vue index 4025073..c59bcf0 100644 --- a/app/components/GroupEditor.vue +++ b/app/components/GroupEditor.vue @@ -46,27 +46,47 @@ />
{{ t("groupEditor.langHint") }}
-
- - -
-
- - -
{{ t("groupEditor.forgeLabelHint") }}
+
+ + +
+
+ +
+ + + +
+ +
{{ t("groupEditor.forgeSourcesHint") }}
+