This document is the working handbook for coding agents and maintainers in this repository.
It is updated for the current codebase status (main keeps web_panel; legacy scripts moved to cli branch).
- This repo is organized by branch:
main: Web panel implementation underweb_panel/(FastAPI + Docker)cli: Legacy CLI scripts (msgForward.py,get_session.py,getCIDTEST.py)
- For ongoing feature work,
main+web_panel/is the source of truth. - Legacy CLI mode is maintained in
clibranch for compatibility/manual runs.
- Root (
main):README.md: primary usage guide (Web panel focused)AGENTS.md: agent/developer handbook.gitignore: repo ignore rules
- Web panel (
main):web_panel/app/main.py: FastAPI entrypoint, routes, page orchestrationweb_panel/app/forwarder_service.py: forwarding pipeline + runnerweb_panel/app/config_store.py: config parsing/persistence, session path conventionsweb_panel/app/checkpoint_store.py: checkpoint DB table and operationsweb_panel/app/auth_security.py: password hashing and login lockoutweb_panel/app/history_store.py: run history DBweb_panel/app/backup_manager.py: backup create/delete/restoreweb_panel/app/templates/: dashboard/setup/forward-settings/plan-backup pagesweb_panel/app/static/style.css: panel stylesweb_panel/config_presets/text_replacement_rules.json: versionedTEXT_REPLACEMENT_REGEXrule set (data/ is gitignored, so this is the only history the rules have)web_panel/scripts/restore_text_rules.py: restore those rules into a running container, refusing to write unless they compile and survive the config encode/decode round-tripweb_panel/tests/: standalone regression scripts (no pytest; run each withpython <file>inside the container) —test_config_roundtrip.py,test_persistence_audit.py,test_forwarder_resilience.py,test_lock_staleness.pyweb_panel/tools/create_session.py: createst2rss.sessionin container data dirweb_panel/docker-compose.yml,web_panel/Dockerfile: container runtimeweb_panel/data/: runtime state (config, db, logs, session, backups)
- Session filename:
session_name.session - Checkpoints:
cache/last_ids/*.txt - Lock file:
cache/forwarder.lock
- Session filename:
data/session/t2rss.session - Legacy session migration on startup:
session_name.session->t2rss.session- also migrates SQLite side files (
-journal,-shm,-wal)
- Checkpoints in SQLite table
channel_last_id(data/panel.db) - Lock file:
data/state/forwarder.lock
Primary persisted artifacts under web_panel/data/:
config.env: panel and forwarder configpanel.db:channel_last_id(checkpoint store)run_history(execution records)login_guard(anti-bruteforce state)
state/forwarder.lock: single-run lockstate/downloads/: temporary media filesstate/rss_feed.xml: last successful RSS XML cachestate/rss_session/: temporary copied Telethon sessions for RSS refreshstate/rss_media/: cached images exposed through tokenized RSS media URLslogs/panel.log: rotating app logsbackups/*.zip: snapshots and rollback artifacts
Forwarding-related keys:
API_ID,API_HASH,PHONE,PASSWORDDESTINATION_CHANNELCHANNEL_IDSCHANNEL_IDENTIFIERSCHANNEL_SOURCES_JSON(authoritative for flow UI source rows)KEYWORD_BLACKLISTUSER_ID_BLACKLISTDEDUPLICATION_ENABLEDDEDUPLICATION_115_ENABLEDDEDUPLICATION_BAIDU_ENABLEDDEDUPLICATION_UC_ENABLEDDEDUPLICATION_CACHE_SIZEMAX_VIDEO_SIZE_MB(default 10; 0 = unlimited)ALLOW_XUNLEI_ENABLEDALLOW_PAN123_ENABLEDALLOW_CAIYUN_ENABLEDALLOW_GUANGYA_ENABLEDALLOW_ALIYUN_ENABLED
Panel/security/scheduler keys:
PANEL_AUTO_RUN_ENABLEDPANEL_AUTO_RUN_INTERVAL_MINUTESPANEL_TOTAL_TIMEOUT_SECONDSPANEL_TEST_MODE_ENABLEDPANEL_SESSION_SECRETPANEL_ADMIN_USERNAMEPANEL_ADMIN_PASSWORD(legacy plain-text fallback)PANEL_ADMIN_PASSWORD_HASH(PBKDF2-SHA256 primary)PANEL_LOGIN_MAX_FAILURESPANEL_LOGIN_WINDOW_SECONDSPANEL_LOGIN_LOCK_SECONDSPANEL_RSS_ENABLEDPANEL_RSS_TOKENPANEL_RSS_ITEM_LIMIT
- Auth and session:
GET/POST /login,GET /logout
- Main pages:
GET /dashboardGET /setupinitialization and session managementGET /forward-settingssource/target/filter/checkpoint settingsGET /plan-backupscheduler + backups
- Operations:
POST /run,POST /run/stopPOST /setup/savePOST /setup/admin-credentials-save(requires current password)POST /forward-settings/resolvePOST /forward-settings/savePOST /forward-settings/checkpoints/upsertPOST /forward-settings/checkpoints/batch-savePOST /forward-settings/checkpoints/deletePOST /session/upload,POST /session/delete- backup create/download/delete/restore endpoints
- APIs:
GET /rss/{token}.xmltokenized RSS feed; returns valid XML from live refresh, cached XML, or an empty fallbackGET /rss-media/{token}/{filename}tokenized cached RSS media fileGET /api/statusGET /api/logs/tailPOST /api/logs/clearGET /health
run_forwarder_once() flow:
- Validate required config and active source CID list
- Enforce lock file (
forwarder.lock), which stores<boot_id>:<pid>; a stale lock (other instance, legacy bare PID, malformed) is cleared and the run proceeds - Open Telethon client with
t2rss.session - If dedup enabled, pre-clean destination recent messages by Quark link
- Pull new messages from each source by DB checkpoint (
min_id=last_id). A source that fails to fetch is logged, recorded infetch_failed_channelsand skipped — it never aborts the remaining sources - Merge and sort by message date
- If dedup enabled:
- Optional pre-resolve via Bot for trigger messages (see section 8)
- Stage 1: dedup repeated links within current batch
- Stage 2: skip links already found in destination history cache
- Forward remaining messages. Per-message gates run in this order:
- keyword / user blacklist
- restricted netdisk providers (see section 8) ->
skipped_restricted_provider MAX_VIDEO_SIZE_MBcheck, read from Telegram metadata before any download ->skipped_large_video- media download, wrapped in a 180s
asyncio.wait_for(MEDIA_DOWNLOAD_TIMEOUT_SECONDS) ->skipped_media_timeout - send with retry, then delete the temp file in
finally
- Checkpoint update behavior:
- Normal success: update to
latest_ids_map(max fetched per source) - Cancel/timeout/error: partial update to
forwarded_ids_map forwarded_ids_mapadvances for every outcome excepterror— a deliberately skipped message counts as progress. Only genuine send failures hold position so they are retried. Test mode advances nothing.- Every write is then clamped by
_clamp_checkpoints_below_failures()tooldest_failed_id - 1per channel, so a later success in the same batch cannot drag the checkpoint past an earlier failure. Held-back channels are reported instats.checkpoint_held_back_channels.
- Normal success: update to
- Remove lock in
finally
Current dedup key target priority: quark > 115 > baidu > uc.
A message yields exactly ONE dedup key — the highest-priority provider present.
Links are read from message text, MessageEntityTextUrl blue hyperlinks, button
URLs, and bot-resolved URLs. Query strings and fragments (?pwd=, ?password=,
?public=1, #访问码:...) are stripped from the key.
| Priority | Provider | Matched | Normalized key | Toggle |
|---|---|---|---|---|
| 1 | Quark | pan.quark.cn/s/<token> |
unchanged | always on |
| 2 | 115 | 115cdn.com/s/<token>, hdhive.com/resource/115/<token> |
kept as separate keys | DEDUPLICATION_115_ENABLED |
| 3 | Baidu | pan.baidu.com/s/<token>, pan.baidu.com/share/init?surl=<token> |
/s/<token>; surl=X folds to /s/1X |
DEDUPLICATION_BAIDU_ENABLED |
| 4 | UC | drive.uc.cn/s/<token>, fast.uc.cn/s/<token> |
collapsed to drive.uc.cn/s/<token> |
DEDUPLICATION_UC_ENABLED |
These are a forwarding filter, independent of dedup. All default to false:
| Provider | Matched | Toggle |
|---|---|---|
| 迅雷 | pan.xunlei.com/s/ |
ALLOW_XUNLEI_ENABLED |
| 123网盘 | 123pan.com/s/, 123<3 digits>.com/s/ (domain rotates: 123684/123865/123912) |
ALLOW_PAN123_ENABLED |
| 移动云盘 | yun.139.com/shareweb/#/w/i/ |
ALLOW_CAIYUN_ENABLED |
| 光鸭云盘 | guangyapan.com/s/ |
ALLOW_GUANGYA_ENABLED |
| 阿里云盘 | alipan.com/s/, aliyundrive.com/s/ |
ALLOW_ALIYUN_ENABLED |
A message is skipped (skipped_restricted_provider) only when every netdisk
link it carries belongs to a disabled restricted provider. Messages that also
contain a quark/115/baidu/uc link — or a link from an enabled restricted
provider — still forward, and link-free messages are unaffected.
has_allowed_netdisk_link() deliberately evaluates with all dedup toggles
forced ON, so narrowing dedup scope can never change forwarding eligibility.
When DEDUPLICATION_ENABLED=true:
- Destination pre-clean dedup runs on last
DEDUPLICATION_CACHE_SIZEdestination messages. - Intra-run dedup and destination-history dedup both apply.
- Dedup link extraction checks message text,
MessageEntityTextUrlblue hyperlinks, and button URLs. - For messages containing trigger phrase
点击获取夸克链接:- System extracts bot jump links from text/entities/buttons (
t.meortg://resolve) - Sends
/start(withstart/startapppayload if present) in a conversation - Extracts URL from bot reply text/buttons (prefers Quark URL)
- Uses that resolved Quark link before dedup filtering (scenario-7 fix)
- Replaces
点击获取夸克链接in outbound text with resolved URL - Caches bot result by
bot + start payloadinside the run
- System extracts bot jump links from text/entities/buttons (
Important behavior notes:
- In test mode, no real forwarding and no checkpoint updates.
- Bot expansion is not pre-run dedup in test mode (no real side-effect interactions are executed).
- Dedup scope still depends on
DEDUPLICATION_CACHE_SIZEfor destination history visibility.
- Source-of-truth for source rows after save is
sources_input+ parsed tokens. - CID table row deletion in UI removes row and synchronizes textarea token list.
- If a source is removed from textarea and saved, it should not reappear.
CHANNEL_SOURCES_JSONstores row-level source/cid/enabled/status/error metadata.- Enabled source channels derive
CHANNEL_IDSfor runtime forwarding.
- Admin credentials are edited through a separate dialog and endpoint:
POST /setup/admin-credentials-save- Requires
PANEL_ADMIN_OLD_PASSWORD - Supports username-only, password-only, or both
- General setup save (
/setup/save) does not modify admin credentials. - Session upload accepts any
.sessionfilename and always saves ast2rss.session.
- Never print or persist plaintext secrets to logs.
PANEL_ADMIN_PASSWORD_HASHis preferred; plaintextPANEL_ADMIN_PASSWORDis legacy fallback only.PANEL_SESSION_SECRETshould be stable in production (avoid temporary process-only secret).- Login lockout controlled by:
PANEL_LOGIN_MAX_FAILURESPANEL_LOGIN_WINDOW_SECONDSPANEL_LOGIN_LOCK_SECONDS
- Restore is blocked when runner is active.
- Restore auto-creates rollback backup (
pre_restore_auto_*) first. - Backup path validation rejects traversal and non-zip invalid names.
- Restore keeps backups directory itself out of destructive overwrite path.
When editing code, preserve these guarantees unless intentionally migrating behavior:
- Single-instance lock behavior. The lock stores
<boot_id>:<pid>; a lock from another process instance (or the legacy bare-PID format) is stale and is cleared automatically. Never reduce the guard to a barelock_file.exists(): inside a container the app is always PID 1, so a lock left by a container that died mid-run wedges the forwarder forever. - One unreachable source must never abort the cycle. The per-channel fetch is
wrapped in try/except (
CancelledErrorstill propagates) and failures are recorded instats.fetch_failed_channels. - Checkpoint consistency and monotonic progression semantics
- Skipped messages advance the checkpoint; only
errorholds position. Reverting this reintroduces the self-perpetuating loop where a permanently-skipped message is re-fetched on every run forever. - Every checkpoint write goes through
_clamp_checkpoints_below_failures(), so no write can advance past a message that failed to send. Writinglatest_ids_map(or any bare high-water mark) directly makes a failed message be skipped forever, which defeats the rule above. - Every network await that can block indefinitely (notably
download_media) stays bounded by a timeout, so one stalled item cannot consume the wholePANEL_TOTAL_TIMEOUT_SECONDSbudget. - Media temp file cleanup in
finally - Non-blocking async path in forwarding loops
- Clear skip-reason metrics and logs
- Timeout/cancel
run_historyrecords carry the run's real stats (via thestats_sinkshared dict), not a bare status marker - Backward-safe startup migrations (legacy session / txt checkpoints)
If changing dedup/filter behavior:
- Explicitly document stage order impacts
- Verify stats counters remain meaningful (
skipped_*,after_*) - Verify checkpoint update semantics on success vs cancel/error
Run these sanity checks after non-trivial edits:
python -m compileall web_panel/app web_panel/tools
python -c "from pathlib import Path; from jinja2 import Environment, FileSystemLoader; env=Environment(loader=FileSystemLoader('web_panel/app/templates')); [env.get_template(p.name) for p in Path('web_panel/app/templates').glob('*.html')]; print('ok')"For container verification:
cd web_panel
docker compose up -d --build
curl http://127.0.0.1:8080/healthFixed in save_raw_config (config_store.py). load_raw_config() decodes an
escaped \n into a real newline, but the old save_raw_config() re-encoded it
only for keys present in the submitted dict. A multiline value that was merely
carried over unchanged got written as several physical lines in config.env,
and everything after the first line was silently dropped on the next read. So
any save — setup/RSS, scheduler, admin password — destroyed all but the
first regex rule, even though those forms never touch the field. Encoding now
happens for every key on the way out; see
web_panel/tests/test_config_roundtrip.py.
Remaining constraints:
-
web_panel/data/is gitignored, so the live rules have no history.web_panel/config_presets/text_replacement_rules.jsonis the source of truth; update it whenever rules change, and restore withweb_panel/scripts/restore_text_rules.py(verifies before writing). -
Never put a literal
\ninside a rule. Rules are persisted joined on\n, so a literal\ndecodes into a real line break and splits one rule into invalid fragments. Use\s*/\s+for whitespace and newlines. -
Rules must be verified after a save/reload round-trip, not just after compiling — a rule can compile fine and still not survive persistence.
-
Promo footers vary segment by segment (
来自/频道/群组/投稿/资源搜索/反馈合作appear in different combinations). Prefer standalone per-label rules over one rule that hard-codes a fixed multi-segment sequence, and require an@handleso ordinary prose mentioning 频道/群组 is not deleted. -
Some promo URLs are only visible after entity materialization (a 1-char hidden hyperlink becomes a bare URL glued to the text). The end-anchored
t.me|telegram.me|link3.ccrule must stay last in the list. -
POST /plan-backup/cleanupdoes not touchconfig.env(it only clears downloads,tmp_*dirs, a stale lock, session sidecars and uploaded restore zips). -
A backup taken while the rules are broken will faithfully capture the broken state; restoring it re-applies the damage. Check the rule count before trusting a restore.
-
clibranch legacy scripts still usesession_name.sessionand text-file checkpoints. -
mainweb panel usest2rss.sessionand SQLite checkpoints. -
Root
README.mdonmaindocuments Web panel mode; legacy usage lives inclibranch.
- Add automated tests for:
- source parsing and save semantics
- dedup stages (including bot-expanded link cases)
- checkpoint update paths (success/cancel/error)
- Add structured metric endpoints for observability.
- Consider isolating bot-conversation logic behind a dedicated adapter for easier mocking.