The v4 bump moved the PostCSS plugin to @tailwindcss/postcss, breaking
the production build while the project still uses a v3-style
tailwind.config.ts and @apply components.
- add feishu_links D1 table and link store helpers
- implement X-Lark-Signature verification, url_verification, /gh login|logout|comment|merge|close and card.action.trigger Merge/Close
- render interactive cards with clickable title link, inline links and callback buttons (no whole-card card_link)
- bind Feishu account in OAuth callback
- document event subscription and required scopes
Add a field filter type reading any payload value by JSONPath with array expansion, 12 comparison operators (eq/ne/contains/startsWith/endsWith/regex/gt/gte/lt/lte/in/exists), a visual AST builder (all/any/not) in the route editor, chip-based multi-value input, a stateless POST /admin/api/test-match dry-run, and named filter fragments stored in D1 (d1_fragments, migration 0010) inlined into route ASTs on insert.
- dispatch: attach per-send detail (title/url/description/match/provider)
so the admin log view shows message context, not just a bare ok flag
- idempotency: compare dedup expiry against claimed_at instead of the new
expires_at so non-expired duplicates are rejected and expired keys re-claim
- Move oversized queue payloads from KV to R2 (PAYLOAD binding, webhooks/YYYY/MM/DD/*.json, KV queue:payload:* fallback)
- Persist routes/groups to D1 (d1_routes/d1_groups) with memory -> KV -> D1 three-tier cache, seeded from legacy KV config keys
- Move webhook dedup (dedup_keys), delivery state (delivery_state) and message tracking (message_tracking) to D1 via canUseD1 probe with automatic KV fallback
- Batch send_logs inserts (recordSendBatch) and add group_id/ts index
- Add storage-prune scheduled task for expired dedup/state/tracking rows
- Add TTL to invite:group:{id} index and audit all ephemeral KV keys
- Add D1 indexes for the new tables
- Sync AGENTS.md, README.md/zh and docs/ (en/zh) with the new storage layout
Add LICENSE (MIT), an IdempotencyStore abstraction with a KV implementation and provider-scoped delivery keys, optional replay protection for custom webhooks (X-WebHooker-Timestamp + X-WebHooker-Nonce), and per-request correlation ids in webhook responses and logs.
- GitHub footer icon now uses fluidicon.png instead of favicon.ico —
Discord silently drops .ico embed footer icons (png/jpg/webp/gif only)
- Gitea instances use {origin}/assets/img/favicon.png (verified on
codeberg.org and gitea.com); links are still derived from the repo URL
- tests + docs updated to the new icon URLs
- 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