From 690c25f630a6dc46f73fd257c4f3b30c49da6751 Mon Sep 17 00:00:00 2001 From: WCY-dt <834421194@qq.com> Date: Sun, 20 Sep 2026 02:36:34 +0800 Subject: [PATCH 1/4] release: Upgrade version to v1.3.37 --- CHANGELOG.md | 2 ++ README.md | 2 +- README_zh.md | 2 +- build/config.yml | 4 ++-- build/darwin/Info.dev.plist | 4 ++-- build/darwin/Info.plist | 4 ++-- build/darwin/Taskfile.yml | 4 ++-- build/darwin/create-dmg.sh | 2 +- build/ios/Info.dev.plist | 4 ++-- build/ios/Info.plist | 4 ++-- build/ios/build.sh | 4 ++-- build/linux/Taskfile.yml | 2 +- build/linux/create-appimage.sh | 2 +- build/linux/nfpm/nfpm.yaml | 2 +- build/windows/info.json | 4 ++-- build/windows/installer.nsi | 4 ++-- build/windows/nsis/wails_tools.nsh | 2 +- build/windows/wails.exe.manifest | 2 +- docs/SERVER_MODE/swagger.json | 2 +- frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- internal/version/version.go | 2 +- main-core.go | 2 +- skills/mrrss-assistant/references/api.md | 2 +- website/package-lock.json | 4 ++-- website/package.json | 2 +- 26 files changed, 38 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11babb12..460c7fc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to MrRSS will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + ## [1.3.36] - 2026-09-20 ### Added diff --git a/README.md b/README.md index 613adfe5..b7c2b684 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ English | 简体中文

-[![Version](https://img.shields.io/badge/version-1.3.36-blue.svg)](https://github.com/DevXDojo/MrRSS/releases) +[![Version](https://img.shields.io/badge/version-1.3.37-blue.svg)](https://github.com/DevXDojo/MrRSS/releases) [![License](https://img.shields.io/badge/license-GPLv3-green.svg)](LICENSE) [![Go](https://img.shields.io/badge/Go-1.27+-00ADD8?logo=go)](https://go.dev/) [![Wails](https://img.shields.io/badge/Wails-v3%20alpha-red)](https://wails.io/) diff --git a/README_zh.md b/README_zh.md index 2ce870b6..753a63ae 100644 --- a/README_zh.md +++ b/README_zh.md @@ -8,7 +8,7 @@ English | 简体中文

-[![Version](https://img.shields.io/badge/version-1.3.36-blue.svg)](https://github.com/DevXDojo/MrRSS/releases) +[![Version](https://img.shields.io/badge/version-1.3.37-blue.svg)](https://github.com/DevXDojo/MrRSS/releases) [![License](https://img.shields.io/badge/license-GPLv3-green.svg)](LICENSE) [![Go](https://img.shields.io/badge/Go-1.27+-00ADD8?logo=go)](https://go.dev/) [![Wails](https://img.shields.io/badge/Wails-v3%20alpha-red)](https://wails.io/) diff --git a/build/config.yml b/build/config.yml index 1f852d07..5187fc1c 100644 --- a/build/config.yml +++ b/build/config.yml @@ -11,7 +11,7 @@ info: description: "A modern, standalone RSS reader" # The application description copyright: "Copyright © 2026" # Copyright text comments: "Built with Wails" # Comments - version: "1.3.36" # The application version + version: "1.3.37" # The application version # iOS build configuration (uncomment to customise iOS project generation) # Note: Keys under `ios` OVERRIDE values under `info` when set. @@ -21,7 +21,7 @@ info: # # The display name shown under the app icon (CFBundleDisplayName/CFBundleName) # displayName: "MrRSS" # # The app version to embed in Info.plist (CFBundleShortVersionString/CFBundleVersion) -# version: "1.3.36" +# version: "1.3.37" # # The company/organisation name for templates and project settings # company: "Ch3nyang" # # Additional comments to embed in Info.plist metadata diff --git a/build/darwin/Info.dev.plist b/build/darwin/Info.dev.plist index 1ea37049..dee0b6fe 100644 --- a/build/darwin/Info.dev.plist +++ b/build/darwin/Info.dev.plist @@ -10,11 +10,11 @@ CFBundleIdentifier com.mrrss.app CFBundleVersion - 1.3.36 + 1.3.37 CFBundleGetInfoString Built with Wails CFBundleShortVersionString - 1.3.36 + 1.3.37 CFBundleIconFile icons LSMinimumSystemVersion diff --git a/build/darwin/Info.plist b/build/darwin/Info.plist index 88444ac8..cdad3ad7 100644 --- a/build/darwin/Info.plist +++ b/build/darwin/Info.plist @@ -10,11 +10,11 @@ CFBundleIdentifier com.mrrss.app CFBundleVersion - 1.3.36 + 1.3.37 CFBundleGetInfoString Built with Wails CFBundleShortVersionString - 1.3.36 + 1.3.37 CFBundleIconFile icons LSMinimumSystemVersion diff --git a/build/darwin/Taskfile.yml b/build/darwin/Taskfile.yml index 9b4f8338..c82a4fb7 100644 --- a/build/darwin/Taskfile.yml +++ b/build/darwin/Taskfile.yml @@ -72,9 +72,9 @@ tasks: CFBundlePackageType APPL CFBundleShortVersionString - 1.3.36 + 1.3.37 CFBundleVersion - 1.3.36 + 1.3.37 LSMinimumSystemVersion 10.13 NSHighResolutionCapable diff --git a/build/darwin/create-dmg.sh b/build/darwin/create-dmg.sh index 740ee40e..87c6c616 100644 --- a/build/darwin/create-dmg.sh +++ b/build/darwin/create-dmg.sh @@ -12,7 +12,7 @@ set -e APP_NAME="MrRSS" # Get version from frontend/package.json if available, otherwise use default -VERSION=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' frontend/package.json 2>/dev/null | head -1 | sed 's/.*"\([^"]*\)".*/\1/' || echo "1.3.36") +VERSION=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' frontend/package.json 2>/dev/null | head -1 | sed 's/.*"\([^"]*\)".*/\1/' || echo "1.3.37") APP_PUBLISHER="Ch3nyang" APP_URL="https://github.com/DevXDojo/MrRSS" APP_DESCRIPTION="A Modern, Cross-Platform Desktop RSS Reader" diff --git a/build/ios/Info.dev.plist b/build/ios/Info.dev.plist index 00a3061d..1fe79923 100644 --- a/build/ios/Info.dev.plist +++ b/build/ios/Info.dev.plist @@ -13,9 +13,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.36-dev + 1.3.37-dev CFBundleVersion - 1.3.36 + 1.3.37 LSRequiresIPhoneOS MinimumOSVersion diff --git a/build/ios/Info.plist b/build/ios/Info.plist index f586e85e..3f0e32ca 100644 --- a/build/ios/Info.plist +++ b/build/ios/Info.plist @@ -13,9 +13,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.36 + 1.3.37 CFBundleVersion - 1.3.36 + 1.3.37 LSRequiresIPhoneOS MinimumOSVersion diff --git a/build/ios/build.sh b/build/ios/build.sh index 65261008..a26ac527 100644 --- a/build/ios/build.sh +++ b/build/ios/build.sh @@ -4,8 +4,8 @@ set -e # Build configuration APP_NAME="MrRSS" BUNDLE_ID="com.mrrss.app" -VERSION="1.3.36" -BUILD_NUMBER="1.3.36" +VERSION="1.3.37" +BUILD_NUMBER="1.3.37" BUILD_DIR="build/ios" TARGET="simulator" diff --git a/build/linux/Taskfile.yml b/build/linux/Taskfile.yml index af0f05c4..b655750f 100644 --- a/build/linux/Taskfile.yml +++ b/build/linux/Taskfile.yml @@ -124,7 +124,7 @@ tasks: cmds: - cd {{.BIN_DIR}} && tar -czf {{.APP_NAME}}-{{.VERSION}}-linux-{{.ARCH}}.tar.gz {{.APP_NAME}} vars: - VERSION: '1.3.36' + VERSION: '1.3.37' ARCH: '{{.ARCH | default "amd64"}}' generate:dotdesktop: diff --git a/build/linux/create-appimage.sh b/build/linux/create-appimage.sh index 12f49ed6..c6302d23 100755 --- a/build/linux/create-appimage.sh +++ b/build/linux/create-appimage.sh @@ -13,7 +13,7 @@ set -e APP_NAME="MrRSS" # Get version from frontend/package.json if available, otherwise use default -VERSION=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' frontend/package.json 2>/dev/null | head -1 | sed 's/.*"\([^"]*\)".*/\1/' || echo "1.3.36") +VERSION=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' frontend/package.json 2>/dev/null | head -1 | sed 's/.*"\([^"]*\)".*/\1/' || echo "1.3.37") # Get architecture from environment variable or default to amd64 ARCH=${ARCH:-amd64} echo "Target architecture: ${ARCH}" diff --git a/build/linux/nfpm/nfpm.yaml b/build/linux/nfpm/nfpm.yaml index ff461f85..56f26a1e 100644 --- a/build/linux/nfpm/nfpm.yaml +++ b/build/linux/nfpm/nfpm.yaml @@ -6,7 +6,7 @@ name: "MrRSS" arch: ${GOARCH} platform: "linux" -version: "1.3.36" +version: "1.3.37" section: "default" priority: "extra" maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> diff --git a/build/windows/info.json b/build/windows/info.json index d92d96f6..3209b0de 100644 --- a/build/windows/info.json +++ b/build/windows/info.json @@ -1,10 +1,10 @@ { "fixed": { - "file_version": "1.3.36" + "file_version": "1.3.37" }, "info": { "0000": { - "ProductVersion": "1.3.36", + "ProductVersion": "1.3.37", "CompanyName": "Ch3nyang", "FileDescription": "MrRSS", "LegalCopyright": "Copyright © 2026", diff --git a/build/windows/installer.nsi b/build/windows/installer.nsi index 0559ba64..9101f2ca 100644 --- a/build/windows/installer.nsi +++ b/build/windows/installer.nsi @@ -9,8 +9,8 @@ ; All paths in this script are relative to the script directory. !define APP_NAME "MrRSS" -!define APP_VERSION "1.3.36" -!define APP_VERSION_NUMERIC "1.3.36.0" ; NSIS requires X.X.X.X format +!define APP_VERSION "1.3.37" +!define APP_VERSION_NUMERIC "1.3.37.0" ; NSIS requires X.X.X.X format !define APP_PUBLISHER "Ch3nyang" !define APP_URL "https://github.com/DevXDojo/MrRSS" !define APP_DESCRIPTION "A Modern, Cross-Platform Desktop RSS Reader" diff --git a/build/windows/nsis/wails_tools.nsh b/build/windows/nsis/wails_tools.nsh index 8096b669..08afbfdd 100644 --- a/build/windows/nsis/wails_tools.nsh +++ b/build/windows/nsis/wails_tools.nsh @@ -14,7 +14,7 @@ !define INFO_PRODUCTNAME "MrRSS" !endif !ifndef INFO_PRODUCTVERSION - !define INFO_PRODUCTVERSION "1.3.36" + !define INFO_PRODUCTVERSION "1.3.37" !endif !ifndef INFO_COPYRIGHT !define INFO_COPYRIGHT "Copyright © 2026" diff --git a/build/windows/wails.exe.manifest b/build/windows/wails.exe.manifest index 2b79da19..d587e2ba 100644 --- a/build/windows/wails.exe.manifest +++ b/build/windows/wails.exe.manifest @@ -1,6 +1,6 @@ - + diff --git a/docs/SERVER_MODE/swagger.json b/docs/SERVER_MODE/swagger.json index b8d794c6..9193b79d 100644 --- a/docs/SERVER_MODE/swagger.json +++ b/docs/SERVER_MODE/swagger.json @@ -12,7 +12,7 @@ "name": "GPL-3.0", "url": "https://www.gnu.org/licenses/gpl-3.0.en.html" }, - "version": "1.3.36" + "version": "1.3.37" }, "host": "localhost:1234", "basePath": "/api", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b08a6730..934bc175 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mrrss-frontend", - "version": "1.3.36", + "version": "1.3.37", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mrrss-frontend", - "version": "1.3.36", + "version": "1.3.37", "dependencies": { "@eslint/js": "^10.0.1", "@phosphor-icons/vue": "^2.2.1", diff --git a/frontend/package.json b/frontend/package.json index 0ed57cb8..fe8c4ed4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "mrrss-frontend", "private": true, - "version": "1.3.36", + "version": "1.3.37", "type": "module", "scripts": { "dev": "vite", diff --git a/internal/version/version.go b/internal/version/version.go index 801e5a05..e541a4e5 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -1,4 +1,4 @@ package version // Version is the current application version -const Version = "1.3.36" +const Version = "1.3.37" diff --git a/main-core.go b/main-core.go index ec1f7bf1..d1891d70 100644 --- a/main-core.go +++ b/main-core.go @@ -31,7 +31,7 @@ import ( ) // @title MrRSS API -// @version 1.3.36 +// @version 1.3.37 // @description MrRSS is a modern, cross-platform desktop RSS reader with auto-translation, smart feed discovery, and AI-powered summarization. // @contact.name API Support diff --git a/skills/mrrss-assistant/references/api.md b/skills/mrrss-assistant/references/api.md index 98d058fb..e9e525be 100644 --- a/skills/mrrss-assistant/references/api.md +++ b/skills/mrrss-assistant/references/api.md @@ -6,7 +6,7 @@ Generated from `docs/SERVER_MODE/swagger.json`. Regenerate with: python skills/mrrss-assistant/scripts/generate_api_reference.py docs/SERVER_MODE/swagger.json skills/mrrss-assistant/references/api.md ``` -- API version: `1.3.36` +- API version: `1.3.37` - API root: `{base_url}/api` - Endpoint paths below are relative to the API root unless they already start with `/api/`. diff --git a/website/package-lock.json b/website/package-lock.json index 3723af1f..8869bd6a 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -1,12 +1,12 @@ { "name": "mrrss-website", - "version": "1.3.36", + "version": "1.3.37", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mrrss-website", - "version": "1.3.36", + "version": "1.3.37", "dependencies": { "@phosphor-icons/vue": "^2.2.1", "vue": "^3.5.42" diff --git a/website/package.json b/website/package.json index 42ce835a..cd800cfc 100644 --- a/website/package.json +++ b/website/package.json @@ -1,6 +1,6 @@ { "name": "mrrss-website", - "version": "1.3.36", + "version": "1.3.37", "description": "MrRSS - AI-Powered RSS Reader Landing Page", "type": "module", "scripts": { From e37dc1930ab1e51ed087baa35ab3eb0a1193a11f Mon Sep 17 00:00:00 2001 From: WCY-dt <834421194@qq.com> Date: Sun, 20 Sep 2026 03:29:24 +0800 Subject: [PATCH 2/4] feat: add configurable notification channels and scheduled briefings --- CHANGELOG.md | 8 + config/defaults.json | 1 + docs/NOTIFICATIONS.md | 72 +++ docs/SERVER_MODE/swagger.json | 382 ++++++++++++ docs/SETTINGS.md | 2 + frontend/src/assets/brands/README.md | 6 + frontend/src/assets/brands/discord.svg | 1 + frontend/src/assets/brands/feishu.svg | 6 + frontend/src/assets/brands/telegram.svg | 1 + .../src/components/modals/SettingsModal.vue | 83 ++- .../settings/notifications/ChannelEditor.vue | 160 +++++ .../notifications/NotificationsTab.test.ts | 170 ++++++ .../notifications/NotificationsTab.vue | 480 +++++++++++++++ .../settings/notifications/RuleEditor.vue | 300 ++++++++++ .../settings/notifications/notifications.css | 96 +++ .../composables/core/useSettings.generated.ts | 3 + .../notification/useNotifications.ts | 111 ++++ frontend/src/i18n/locales/en.ts | 2 + frontend/src/i18n/locales/notifications.ts | 331 +++++++++++ frontend/src/i18n/locales/zh.ts | 2 + frontend/src/types/notification.test.ts | 17 + frontend/src/types/notification.ts | 103 ++++ frontend/src/types/settings.generated.ts | 1 + frontend/src/types/settings.ts | 1 + internal/config/config.go | 3 + internal/config/defaults.json | 1 + internal/config/settings_keys.go | 2 +- internal/config/settings_schema.json | 7 + internal/database/notifications.go | 189 ++++++ internal/database/notifications_test.go | 71 +++ internal/database/schema.go | 3 + internal/handlers/core/handler.go | 3 + internal/handlers/core/scheduler.go | 1 + .../handlers/notification/notification.go | 166 ++++++ .../notification/notification_test.go | 85 +++ internal/handlers/settings/settings_base.go | 1 + .../handlers/settings/settings_handlers.go | 3 + internal/notification/config.go | 276 +++++++++ internal/notification/message.go | 122 ++++ internal/notification/notification_test.go | 546 ++++++++++++++++++ internal/notification/provider.go | 147 +++++ internal/notification/service.go | 339 +++++++++++ internal/routes/settings_routes.go | 5 + skills/mrrss-assistant/references/api.md | 37 ++ 44 files changed, 4331 insertions(+), 15 deletions(-) create mode 100644 docs/NOTIFICATIONS.md create mode 100644 frontend/src/assets/brands/README.md create mode 100644 frontend/src/assets/brands/discord.svg create mode 100644 frontend/src/assets/brands/feishu.svg create mode 100644 frontend/src/assets/brands/telegram.svg create mode 100644 frontend/src/components/modals/settings/notifications/ChannelEditor.vue create mode 100644 frontend/src/components/modals/settings/notifications/NotificationsTab.test.ts create mode 100644 frontend/src/components/modals/settings/notifications/NotificationsTab.vue create mode 100644 frontend/src/components/modals/settings/notifications/RuleEditor.vue create mode 100644 frontend/src/components/modals/settings/notifications/notifications.css create mode 100644 frontend/src/composables/notification/useNotifications.ts create mode 100644 frontend/src/i18n/locales/notifications.ts create mode 100644 frontend/src/types/notification.test.ts create mode 100644 frontend/src/types/notification.ts create mode 100644 internal/database/notifications.go create mode 100644 internal/database/notifications_test.go create mode 100644 internal/handlers/notification/notification.go create mode 100644 internal/handlers/notification/notification_test.go create mode 100644 internal/notification/config.go create mode 100644 internal/notification/message.go create mode 100644 internal/notification/notification_test.go create mode 100644 internal/notification/provider.go create mode 100644 internal/notification/service.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 460c7fc2..c72907d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add a dedicated Notifications settings tab with locally bundled Telegram, Discord and Feishu/Lark logos, multiple destinations, encrypted credentials, connection tests, condition-based article alerts, scheduled briefings, quiet hours, previews and delivery history. Persist queued messages across restarts and retry transient failures with bounded backoff in desktop and server modes. (#1208) + +### Fixed + +- Keep settings usable on narrow screens with horizontal tab navigation and a separate search row. Protect unsaved notification drafts when closing settings or switching tabs. (#1208) + ## [1.3.36] - 2026-09-20 ### Added diff --git a/config/defaults.json b/config/defaults.json index f3895369..0a02ba03 100644 --- a/config/defaults.json +++ b/config/defaults.json @@ -82,6 +82,7 @@ "network_bandwidth_mbps": "0", "network_latency_ms": "0", "network_speed": "medium", + "notification_config": "", "notion_api_key": "", "notion_enabled": false, "notion_page_id": "", diff --git a/docs/NOTIFICATIONS.md b/docs/NOTIFICATIONS.md new file mode 100644 index 00000000..1245446b --- /dev/null +++ b/docs/NOTIFICATIONS.md @@ -0,0 +1,72 @@ +# Message notifications / 消息推送 + +Open **Settings → Notifications / 设置 → 消息推送**. Notifications are off by default and work in both desktop and server builds while MrRSS is running. + +## Quick start / 快速开始 + +1. Expand a service card and add a destination. Each service supports multiple destinations, such as a personal chat and a team channel. +2. Enter its credentials, send a test if desired, and save. Tests use the current form without requiring a save and contain no article content. +3. Add an article alert or scheduled briefing. Select the receiving destinations, optional conditions and schedule, then preview matches before saving. +4. Enable the master switch. Automatic delivery starts with articles received after activation; it does not send the existing archive. + +展开渠道卡片并添加接收目标,填写凭据后可先测试再保存。然后创建提醒或摘要规则,选择接收目标、匹配条件和计划,通过预览确认效果。最后打开总开关;只处理启用后新收到的文章,不会批量发送历史文章。 + +## Channels / 渠道 + +| Service | Configuration | +| --- | --- | +| Telegram | Create a bot with BotFather. Start a conversation or add it to a group. Supply its bot token and chat ID or channel username. An optional topic ID targets a forum topic. The bot needs posting permission in channels. | +| Discord | Create a webhook under the receiving channel's **Settings → Integrations → Webhooks**. Paste its HTTPS URL. Automatic mentions and link previews are disabled. Forum webhooks requiring a thread are not supported in this release. | +| Feishu / Lark | Add a custom group bot and paste its webhook. If signature verification is enabled, enter the signing secret. Keyword restrictions must permit `MrRSS`; IP restrictions must permit the sending device. Keep its system clock accurate. | + +Telegram 支持聊天、群组、频道和话题;Discord 使用频道 Webhook;飞书及 Lark 使用自定义群机器人,支持签名校验。飞书关键词校验请允许 `MrRSS`,开启 IP 白名单时请允许当前设备的出口 IP。 + +Endpoints are restricted to the official Telegram, Discord, Feishu and Lark services over HTTPS. Redirects are rejected. Connections follow the application's global proxy settings. Custom relay endpoints are not supported. + +Credentials use MrRSS's existing machine-bound encryption. Configuration responses contain placeholders instead of tokens, webhook URLs or signing secrets. Leaving a placeholder unchanged preserves its saved value; replacing it updates the secret, and clearing an optional signing secret removes it. Do not expect copied encrypted settings to work on a different machine. + +## Rules and content / 规则与内容 + +- **Article alerts** check at an interval of 1–1440 minutes. The scheduler wakes approximately every 15 seconds; network work can delay a cycle. +- **Scheduled briefings** run at a selected local time and on optional weekdays. No selected weekdays means every day. Choose the timezone where MrRSS runs, or an explicit timezone. A missed time can run later on the same scheduled day when the app is available. +- Combine up to 10 conditions using **all (AND)** or **any (OR)**. Conditions support title, summary/excerpt, feed name, feed category, author, URL, favorite and read-later state. Text comparisons ignore case; `does not contain` and `does not equal` provide exclusions. Favorite/read-later conditions examine state when a new article is scanned; changing an old article's state does not create a new event. +- Hidden articles are excluded. Unread-only is optional. Matching rules run independently, so overlapping rules can intentionally send the same article more than once. +- Each run includes 1–20 articles, in arrival order. Additional matches remain for the next scheduled run. Selective rules page through new articles, so a matching article is not restricted to the first page of incoming articles. +- Include or omit source links and article summaries. Summaries prefer cached AI summaries and otherwise use feed excerpts. This feature does not make additional AI calls or synthesize a cross-article AI report. It also works without AI configured. +- Previews inspect the latest 500 local articles, show the match count and sample messages, and never send or change reading state. This differs from the future-article baseline used by automatic delivery. + +支持 AND / OR 条件组合与反向匹配,可按标题、摘要、订阅源、分类、作者、链接、收藏或稍后阅读状态筛选。每次最多 20 篇,剩余匹配文章留待下次计划。摘要优先使用缓存的 AI 摘要,其次使用 RSS 简介,不产生额外 AI 调用。预览使用最近 500 篇文章,不会实际发送。 + +## Quiet hours and changes / 静默与配置变更 + +Shared settings save automatically. Destination and rule editors have explicit Save and Cancel actions. Leaving an unsaved editor requires a discard confirmation. Concurrent configuration edits use a revision check to prevent a stale window overwriting newer settings. + +Quiet hours support overnight ranges. Automatic scanning and delivery pause during that range and resume afterward; tests remain available. A daily briefing whose scheduled time falls during quiet hours is deferred while it is still the scheduled day. If quiet hours extend beyond midnight, it waits for the next eligible scheduled time. + +Editing a rule or destination, disabling a destination/rule, or switching off notifications cancels the affected pending messages and resets their baseline. Re-enabling begins with newly received articles. Changing only a destination's display name preserves its baseline. To delay delivery without resetting it, use quiet hours. + +公共选项自动保存,渠道与规则独立保存。静默时段内暂缓自动扫描和发送;跨日后按照下一个符合条件的定时时间执行。修改规则或接收目标、暂停规则/目标或关闭总开关,会取消对应待发消息;重新启用从新的文章开始。只想临时延后发送时,请使用静默时段。 + +## Delivery behavior / 发送行为 + +Article cursors and queued messages commit in the same SQLite transaction. Pending messages survive a restart. Long content is split into bounded messages, and each completed part is recorded separately. Each channel sends at most one queued part per scheduler cycle, preserving order during backoff. + +Temporary network errors, HTTP 429 and server errors are retried up to three total attempts, with backoff and supported provider retry delays. Authentication and application-level rejections fail without repeated sends. As with most webhook APIs, an ambiguous network failure or crash after provider acceptance can result in a duplicate; exactly-once external delivery is not guaranteed. + +Delivery history shows the latest 100 records. Completed records are retained for at most 30 days and capped at 1,000. Message bodies are cleared after success, final failure or cancellation; the history API never returns bodies or credentials. Configuration and queue storage remain local, and only explicitly enabled destinations receive article data. + +发送队列与扫描进度原子保存,重启可恢复。长消息分段处理,每段独立记录。临时错误最多尝试三次,鉴权或平台业务错误直接显示失败。网络结果不确定时仍可能重复送达。记录显示最近 100 条,完成记录最多保留 30 天、1,000 条;成功、最终失败或取消后清除消息正文。 + +## HTTP API + +| Method | Endpoint | Purpose | +| --- | --- | --- | +| GET | `/api/notifications/config` | Configuration with redacted secrets and revision. | +| PUT | `/api/notifications/config` | Validate and atomically replace configuration; include the loaded revision. `409` means reload before editing again. | +| POST | `/api/notifications/test` | Send a fixed test using a `Channel` object; masked fields are restored only for the matching stored channel ID and provider. | +| POST | `/api/notifications/preview` | Preview a `Rule` against recent articles without sending. | +| GET | `/api/notifications/history` | Recent delivery metadata; excludes message bodies and credentials. | + +Payloads are defined in [`notification/config.go`](../internal/notification/config.go) and mirrored by [`notification.ts`](../frontend/src/types/notification.ts). The schema-driven `notification_config` setting is encrypted and owned by this API; generic settings autosaves cannot overwrite it. No new listener or access boundary is introduced. + +Provider references: [Telegram Bot API](https://core.telegram.org/bots/api#sendmessage), [Discord webhooks](https://docs.discord.com/developers/resources/webhook#execute-webhook), [Feishu custom bots](https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot). diff --git a/docs/SERVER_MODE/swagger.json b/docs/SERVER_MODE/swagger.json index 9193b79d..4c6ba158 100644 --- a/docs/SERVER_MODE/swagger.json +++ b/docs/SERVER_MODE/swagger.json @@ -5868,6 +5868,176 @@ } } } + }, + "/notifications/config": { + "get": { + "description": "Returns configuration and revision with secret placeholders. Automatic notifications are opt-in.", + "produces": [ + "application/json" + ], + "tags": [ + "notifications" + ], + "summary": "Read notification settings", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/notification.Config" + } + } + } + }, + "put": { + "description": "Atomically saves validated configuration and new-article baselines. Preserve secret placeholders from GET. Edited or disabled rules cancel affected pending messages. A stale revision returns 409.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "notifications" + ], + "summary": "Save notification settings", + "parameters": [ + { + "description": "Complete configuration with current revision", + "name": "config", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/notification.Config" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/notification.Config" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/notifications/history": { + "get": { + "description": "Latest 100 delivery records. Excludes message bodies and credentials. Completed records are retained for up to 30 days, capped at 1000.", + "produces": [ + "application/json" + ], + "tags": [ + "notifications" + ], + "summary": "List notification deliveries", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/database.NotificationDelivery" + } + } + } + } + } + }, + "/notifications/preview": { + "post": { + "description": "Tests a draft rule against the latest 500 local articles, without sending messages or changing reading state. Production rules process articles received after activation.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "notifications" + ], + "summary": "Preview a notification rule", + "parameters": [ + { + "description": "Rule to preview", + "name": "rule", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/notification.Rule" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/notification.Preview" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/notifications/test": { + "post": { + "description": "Sends a fixed test without article content. Uses draft credentials or matching saved placeholders, even when automatic notifications are disabled.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "notifications" + ], + "summary": "Test a notification destination", + "parameters": [ + { + "description": "Destination to test", + "name": "channel", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/notification.Channel" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } } }, "definitions": { @@ -7092,6 +7262,218 @@ "type": "string" } } + }, + "notification.Channel": { + "type": "object", + "properties": { + "chat_id": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "secret": { + "type": "string" + }, + "thread_id": { + "type": "integer" + }, + "token": { + "type": "string" + }, + "webhook": { + "type": "string" + } + } + }, + "notification.Preview": { + "type": "object", + "properties": { + "matched": { + "type": "integer" + }, + "messages": { + "type": "array", + "items": { + "type": "string" + } + }, + "scanned": { + "type": "integer" + } + } + }, + "notification.Rule": { + "type": "object", + "properties": { + "channel_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/definitions/notification.Condition" + } + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "include_link": { + "type": "boolean" + }, + "include_summary": { + "type": "boolean" + }, + "interval_minutes": { + "type": "integer" + }, + "match": { + "description": "all or any", + "type": "string" + }, + "max_items": { + "type": "integer" + }, + "mode": { + "description": "alert: interval, digest: daily local time", + "type": "string" + }, + "name": { + "type": "string" + }, + "time": { + "type": "string" + }, + "unread_only": { + "type": "boolean" + }, + "weekdays": { + "description": "0=Sunday; empty means every day", + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "response.APIResponse": { + "type": "object", + "properties": { + "data": {}, + "error": { + "$ref": "#/definitions/response.ErrorInfo" + }, + "success": { + "type": "boolean" + } + } + }, + "database.NotificationDelivery": { + "type": "object", + "properties": { + "attempts": { + "type": "integer" + }, + "channel_id": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "error_code": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "label": { + "type": "string" + }, + "next_attempt": { + "type": "integer" + }, + "rule_id": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "notification.Condition": { + "type": "object", + "properties": { + "field": { + "type": "string" + }, + "operator": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "notification.Config": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "$ref": "#/definitions/notification.Channel" + } + }, + "enabled": { + "type": "boolean" + }, + "quiet_enabled": { + "type": "boolean" + }, + "quiet_end": { + "type": "string" + }, + "quiet_start": { + "type": "string" + }, + "revision": { + "type": "integer" + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/notification.Rule" + } + }, + "timezone": { + "type": "string" + } + } + }, + "response.ErrorInfo": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + } } }, "securityDefinitions": { diff --git a/docs/SETTINGS.md b/docs/SETTINGS.md index eb0ac898..259b7037 100644 --- a/docs/SETTINGS.md +++ b/docs/SETTINGS.md @@ -1,5 +1,7 @@ # Settings Management System +For notification channels, alert rules, scheduled briefings and delivery behavior, see the [Notifications guide](NOTIFICATIONS.md). + ## Table of Contents - [Quick Start](#quick-start) diff --git a/frontend/src/assets/brands/README.md b/frontend/src/assets/brands/README.md new file mode 100644 index 00000000..3fb093fa --- /dev/null +++ b/frontend/src/assets/brands/README.md @@ -0,0 +1,6 @@ +Notification channel brand marks are bundled locally so opening settings does not contact third parties. + +- Telegram and Discord: [Simple Icons](https://github.com/simple-icons/simple-icons), CC0; original brand shapes with their brand colors. +- Feishu: [Feishu Open Platform favicon](https://lf-package-cn.feishucdn.com/obj/feishu-static/lark/open/website/favicon-logo.svg), linked by https://open.feishu.cn/. + +Marks identify the respective integrations; their trademarks belong to their owners. diff --git a/frontend/src/assets/brands/discord.svg b/frontend/src/assets/brands/discord.svg new file mode 100644 index 00000000..53bfd316 --- /dev/null +++ b/frontend/src/assets/brands/discord.svg @@ -0,0 +1 @@ +Discord \ No newline at end of file diff --git a/frontend/src/assets/brands/feishu.svg b/frontend/src/assets/brands/feishu.svg new file mode 100644 index 00000000..131d3f03 --- /dev/null +++ b/frontend/src/assets/brands/feishu.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/assets/brands/telegram.svg b/frontend/src/assets/brands/telegram.svg new file mode 100644 index 00000000..e791fb79 --- /dev/null +++ b/frontend/src/assets/brands/telegram.svg @@ -0,0 +1 @@ +Telegram \ No newline at end of file diff --git a/frontend/src/components/modals/SettingsModal.vue b/frontend/src/components/modals/SettingsModal.vue index 41f10fa2..0f29bb74 100644 --- a/frontend/src/components/modals/SettingsModal.vue +++ b/frontend/src/components/modals/SettingsModal.vue @@ -9,6 +9,7 @@ import ContentTab from './settings/content/ContentTab.vue'; import AITab from './settings/ai/AITab.vue'; import NetworkTab from './settings/network/NetworkTab.vue'; import PluginsTab from './settings/plugins/PluginsTab.vue'; +import NotificationsTab from './settings/notifications/NotificationsTab.vue'; import ShortcutsTab from './settings/shortcuts/ShortcutsTab.vue'; import RulesTab from './settings/rules/RulesTab.vue'; import StatisticsTab from './settings/statistics/StatisticsTab.vue'; @@ -16,6 +17,7 @@ import AboutTab from './settings/about/AboutTab.vue'; import DiscoverAllFeedsModal from './discovery/DiscoverAllFeedsModal.vue'; import { PhGear, + PhBellRinging, PhSlidersHorizontal, PhBookOpen, PhRss, @@ -49,7 +51,7 @@ const props = withDefaults(defineProps(), { }); // Modal close handling; nested modals are placed above this layer automatically. -const { zIndex: modalZIndex } = useModalClose(() => emit('close'), LARGE_MODAL_Z_INDEX); +const { zIndex: modalZIndex } = useModalClose(() => void requestClose(), LARGE_MODAL_Z_INDEX); // Use composables const { settings, fetchSettings, applySettings } = useSettings(); @@ -85,6 +87,31 @@ const emit = defineEmits<{ }>(); const activeTab: Ref = ref(props.initialTab); +const notificationEditing = ref(false); +const notificationBusy = ref(false); +let checkingNotificationExit = false; + +async function canLeaveNotifications() { + if (notificationBusy.value) { + window.showToast(t('setting.notifications.waitForSave'), 'info'); + return false; + } + if (!notificationEditing.value) return true; + if (checkingNotificationExit) return false; + checkingNotificationExit = true; + try { + return await window.showConfirm({ + title: t('setting.notifications.unsavedTitle'), + message: t('setting.notifications.unsavedMessage'), + }); + } finally { + checkingNotificationExit = false; + } +} + +async function requestClose() { + if (await canLeaveNotifications()) emit('close'); +} const showDiscoverAllModal = ref(false); const settingsContentRef = ref(null); const settingsSearchRef = ref(null); @@ -140,6 +167,12 @@ const settingsTabs: Array<{ labelKey: 'setting.tab.network', searchNamespaces: ['setting.network'], }, + { + id: 'notifications', + icon: PhBellRinging, + labelKey: 'setting.notifications.title', + searchNamespaces: ['setting.notifications'], + }, { id: 'plugins', icon: PhPuzzlePiece, @@ -226,12 +259,14 @@ const settingsSearchResults = computed(() => { .map(({ rank: _rank, ...result }) => result); }); -function selectSettingsTab(tab: TabName) { +async function selectSettingsTab(tab: TabName) { + if (tab !== activeTab.value && !(await canLeaveNotifications())) return; activeTab.value = tab; clearSettingsSearch(); } async function selectSettingsSearchResult(result: SettingsSearchResult) { + if (result.tab !== activeTab.value && !(await canLeaveNotifications())) return; activeTab.value = result.tab; settingsSearchOpen.value = false; @@ -251,8 +286,9 @@ async function selectSettingsSearchResult(result: SettingsSearchResult) { : candidateList.filter((element) => (element.textContent?.toLocaleLowerCase() || '').includes(query) ); - const match = matches - .sort((left, right) => (left.textContent?.length || 0) - (right.textContent?.length || 0))[0]; + const match = matches.sort( + (left, right) => (left.textContent?.length || 0) - (right.textContent?.length || 0) + )[0]; if (!match) return; match.scrollIntoView({ behavior: 'smooth', block: 'center' }); @@ -281,8 +317,7 @@ function handleSettingsSearchKeydown(event: KeyboardEvent) { } else if (event.key === 'ArrowUp') { event.preventDefault(); settingsSearchOpen.value = true; - highlightedSearchIndex.value = - (highlightedSearchIndex.value - 1 + resultCount) % resultCount; + highlightedSearchIndex.value = (highlightedSearchIndex.value - 1 + resultCount) % resultCount; } else if (event.key === 'Enter') { event.preventDefault(); const result = settingsSearchResults.value[highlightedSearchIndex.value]; @@ -330,7 +365,7 @@ function handleDiscoverAll() { class="bg-bg-primary w-full max-w-5xl h-full sm:h-[800px] sm:max-h-[90vh] flex flex-col rounded-none sm:rounded-2xl shadow-2xl border border-border overflow-hidden animate-fade-in mx-2 sm:mx-4 my-2 sm:my-4" >

-
+
-
-
+
+ + + {{ t('modal.feed.picker.keyboardHint') }} +

{{ t( @@ -560,10 +691,17 @@ onBeforeUnmount(() => {

  • +

    {{ previewField(match, displaySelection.title || '.', byPath).slice(0, 200) }}

    @@ -579,8 +717,42 @@ onBeforeUnmount(() => {

    {{ previewField(match, displaySelection.content, byPath) }}

    +

    + {{ t('modal.feed.picker.missingFields') }} +

+ +
+ + {{ resultPage + 1 }} / {{ pageCount }} + +
{