Dev - #6
Merged
Merged
Dev#6
Conversation
v1.3.1 内容批次(VERSION 常量留待推送时 bump 为 1.3.1):
- 插件目录拆分(方案B):内置插件移到 parsers_builtin/ + channels_builtin/,
用户插件独立挂载 ego_parsers/ + ego_channels/;Dockerfile VOLUME + 5 套 compose
加卷;同名内置只读、上传同名拒绝、恢复跳过同名内置并返回 skipped_builtin
- 备份/恢复改为只打包用户插件(顺带修掉原先完全不含 channels 的漏项)
- 韧性链路收口:_do_send_direct 抽出 _send_via_channel(复用 _send + 熔断/限流/
延迟重排);HALF_OPEN 单探测闸门;defer 超限直进死信;ingress 优雅停机排空
;插件 metadata+version+missing 状态
- 新通道:SMTP 邮件(授权码三处提示 + encryption 端口推断)
- 新解析器:Generic JSON / Form / Text 三个通用解析器(启动 auto-sync 登记)
- 升级提示红字告警:version.json 的 upgrade_warning{zh,en},弹层顶部红字渲染,
字段缺失静默不显示(旧 JSON 兼容)
测试:213 -> 298 passed
Co-Authored-by: QwenPaw <[email protected]>
- VERSION 常量(api/pages.py, main.py, build.py) 1.3.0 → 1.3.1 - version.json: version=1.3.1, release_date=2026-09-25, changelog 更新为 v1.3.1 的 8 条 - README.md / README.en.md 标题更新 - changelog v1.3.1 状态: 进行中 → 已发布 Co-Authored-by: QwenPaw <[email protected]>
v1.3.1 改进清单 #1。 - 根因:parser_loader / channel_loader 用 importlib FileLoader spec_from_file_location + exec_module 加载,其按 (size, mtime) 缓存源码。 秒内同长度重写时 reload 会复用旧缓存 → 运行中仍是旧代码。 - 修复:load_parser / load_plugin 改为显式读源 + compile + exec 到 types.ModuleType,每次从磁盘重新读取,在线编辑 / restore 后立即生效。 - api/backup.py 的 restore 写回插件后新增 reload 循环: 恢复的 parser 调 parser_loader.reload_parser、channel 调 channel_loader.reload_plugin;失败收集到 reload_errors 返回。 (此前 api/backup.py 漏 import channel_loader 导致 NameError,已补。) 测试:test_restore_reload_parser / test_restore_reload_channel (同长度秒内重写,restore 后运行中缓存刷新为新版)。 全量 300 passed。 Co-Authored-by: QwenPaw <[email protected]>
v1.3.1 改进清单 #2。 - 根因:上传/PUT 内容直接 f.save()/open('w') 落盘,先写后校验; 语法错误或 DB 冲突时旧插件文件已被覆盖丢失。 - 修复:新增 plugin_paths.atomic_upload / atomic_write_string, 先写同目录临时文件 (.tmp_*.py) + 验证(parser_loader.validate_parser / channel_loader.validate_channel:语法 + 执行 + 必含 parse/Channel), 验证通过才 os.replace() 原子替换到用户目录;失败清理临时文件、 旧插件保持不变。 - 校验委托 lazy import parser_loader / channel_loader(避免循环导入)。 - api/parsers.py 新增同 filename 存在性预检(避免覆盖已有解析器)。 - 流内容可能为 bytes(Flask FileStorage / BytesIO),写入文本临时 文件前 decode utf-8。 - 测试:新增 3 例(坏新插件不建文件、PUT 坏内容不覆盖旧解析器、 PUT 坏内容不覆盖旧通道插件)。 全量 303 passed。 Co-Authored-by: QwenPaw <[email protected]>
v1.3.1 改进清单 #3。 - 根因:通道 429/408 错误未被识别,既可能误触熔断、又会把 可恢复限流当成失败消耗重试。 - HTTP 通道(bark/dingtalk/feishu/telegram/wechat_work_api/ wechat_work_bot)send 中新增 resp.status_code in (429,408) 检测, 返回 'HTTP {code}: {text[:200]}'。 - circuit_breaker:新增 is_throttle()(429/408/too many requests/rate limit/...),classify 对限流返回 False(不计熔断失败)。 - sender_engine:_send_via_channel 遇限流 → blocked=True, reason='throttle',返回 ok=None(defer 信号);process_queue_item 对 reason='throttle' 用 THROTTLE_DEFER_SECONDS=30 延迟重排 (不消耗重试次数)。 - 测试:新增 TestThrottleDefer(429/408 → deferred、reason=throttle、 defer_seconds=30、retry_count 不变、breaker 未打开)。 全量 305 passed。 Co-Authored-by: QwenPaw <[email protected]>
v1.3.1 改进清单 #4。 - 现状:导出 config 原样导出(含 SMTP 密码/授权码/token)。 - 修复:api/backup.py 新增 _SENSITIVE_KEYS + _mask_config(), _export_channel() 对 password/auth_code/token/secret/api_key/webhook/ device_key/access_token/appsecret/client_secret 隐藏为 ***。 仅 Export(JSON 文件)脱敏;Backup(zip)拷 config/ 原始文件, 保留完整凭据以便恢复。 - README(.en) 增加「安全提示」:备份 ZIP 含完整凭据需妥善保管; JSON 导出脱敏,仅展示归档。 - 测试:新增 TestExportMasking(export 全量+单通道脱敏;backup zip 保留完整密码/token,不脱敏)。 全量 308 passed。 Co-Authored-by: QwenPaw <[email protected]>
v1.3.1 改进清单 #5。 后端已到位(SMTPChannel 有 test() 做 连接+登录+NOOP; api/channels.py test 端点经 create_channel 按 type 路由, channel_loader._unpack_test_result 兼容 bool/(ok,err)),故本次: - i18n.py 新增 ch.test_conn=「测试连接」/ch.test_send=「测试发送」 (en: Test connection / Test send)。 - templates/channels_page.html 测试按钮 title 按类型: ch.type=='smtp_email' → 测试连接,否则 → 测试发送。 - 测试补充: * test_smtp_email_channel.py:SMTP test() 成功路径 (连接+登录+NOOP 全过 → (True,''),命令含 AUTH/NOOP)。 * test_channel_endpoints.py:非 SMTP 通道(wechat_work_bot) 走现有 .test() 逻辑,起真 webhook 桩回 {errcode:0} → {ok:true}; + 前端 i18n 文案 key 存在性断言。 全量 311 passed。 Co-Authored-by: QwenPaw <[email protected]>
v1.3.2 改进清单 #5。 - 现状:conflict_reason 上传即拒同名内置,但「手动放同名文件到用户目录」 或迁移残留会静默遮蔽内置(用户版优先),无任何提示。 - plugin_paths 新增 shadows_builtin(kind, fn):用户目录存在该文件 且内置目录也有同名 → True。 - list_plugins 每项加 shadow_builtin 字段(用户遮蔽内置=True,否则 False)。 - api/parsers.py::_decorate 给解析器补 shadow_builtin。 - 前端 channels_page / parsers_page 列表加「遮蔽内置」警告徽章 (bg-warning ⚠),不强制删除。 - i18n 新增 ch.shadow_builtin / parser.shadow_builtin(中英)。 - 测试:test_plugin_dirs.py 新增 2 项(列表标记 + /api/parsers 暴露字段)。 全量 313 passed。 Co-Authored-by: QwenPaw <[email protected]>
#5 补充 + 文档收口: - #5 通道主表 shadow 徽章: * api/channels.py api_channels() 每项加 plugin_shadowed (通道引用的插件被同名用户副本遮蔽 → True) * channels_page.html 主表类型列加 ⚠ ch.shadow_builtin 徽章 * test_plugin_dirs.py 新增 TestChannelApiShadowedPlugin - #6 迁移说明(中英):README 加「从旧版本升级 v1.3.0→v1.3.1+」 三步(导出/升级镜像/恢复) - #7 卷操作语义(中英):up/down/down -v 三行警告,down -v 删卷丢插件 - #8 令牌桶(中):出站限流段补 Token Bucket + burst=分钟额度 全量 314 passed。 Co-Authored-by: QwenPaw <[email protected]>
v1.3.2 改进清单 #8(Burst 语义文档)英文补齐。 README.en.md 出站限流段加 Token Bucket 说明: 桶容量(burst)= 分钟额度;按 额度÷60 /s 补充;发一条取 1 令牌, 桶空时排队而非丢弃(最长等 EGO_RATE_MAX_WAIT 秒,超时则延迟重排)。 与 README.md 中文版对齐。 Co-Authored-by: QwenPaw <[email protected]>
v1.3.2 改进清单 #9。 - 现状:restore 逐文件直接写盘,无原子/全量校验,失败可能半成功。 - 修复:全量落临时目录 → 全量校验 → 校验通过才统一 os.replace; 任一失败 → 原文件不动、无新文件写入。 * 配置 JSON:校验可解析 * 插件:validate_parser/validate_channel 校验可加载 * 失败时 shutil.rmtree 清临时目录,原文件不变 - 测试:test_normal_restore_ok parser 改为合法 parse(); 新增 test_atomic_restore_rollback_on_fail(原文件不损坏、无半成功) + test_atomic_restore_rollback_config_fail(JSON 失败全部不写)。 全量 316 passed。 Co-Authored-by: QwenPaw <[email protected]>
- plugin_paths.read_source_meta 新增 ego_min_version 键(正则解析 PARSER_EGO_MIN_VERSION / CHANNEL_EGO_MIN_VERSION,缺失空串向后兼容) - api/parsers.py _decorate 暴露 ego_min_version 到 /api/parsers 响应 - tests/test_plugin_dirs.py 新增 test_read_source_meta_ego_min_version (有/无 EGO_MIN_VERSION 两 case) - 删除残留 ego.db-shm/ego.db-wal(WAL 文件) EGo v1.3.2 清单 10/10 全完成。
#8 Restore 配置恢复语义:新增 config_manager.import_from_json()
(事务性 JSON→DB 全量替换,失败整体回滚)。/api/restore 不再调 load_all()
——后者在 DB 非空时以 DB 为准反刷 JSON,使"配置恢复"变成 no-op。
恢复后补登内置解析器并重启 source listener。
#9 同名 Parser 并发上传 TOCTOU:新增 plugin_paths.filename_lock(),
把「查重 → 落盘 → 入库」串行化;parsers 的 POST/PUT/DELETE 共用该锁。
#10 Restore 提交阶段 best-effort rollback(替换前备份 .restore.bak,失败反向恢复)。
#11 上传体积限制:Flask MAX_CONTENT_LENGTH(默认 32MB,EGO_MAX_UPLOAD_MB 可覆盖)
+ 413 JSON 处理;备份上传另设 MAX_BACKUP_UPLOAD_SIZE(20MB,早于 file.read())。
额外修真 bug:main.py 的 `web_ui.source_mgr = mgr` 是死代码——API 读的是
`current_app.source_mgr`(app 属性),而 create_app(source_mgr=None) 已把它定成
None,导致 sources CRUD 的监听启停、restore 的监听重启长期全是空操作。
已补 `web_ui.app.source_mgr = mgr`。
#13 Restore 保真:导入时还原 created_at(缺失回落 CURRENT_TIMESTAMP),
否则恢复一次备份所有"创建时间"都会变成恢复时刻。
#14 删除 config_manager._import_table() 死函数。
#12/#15 文档同步:architecture.md(真相源=SQLite、发送路径共用 _send_via_channel()、
Blueprint/通道/测试计数、日期)、README 中英(备份含 channels/*.py、通道表补 SMTP、
恢复语义说明)、doc/sdk/{parser,channel}.{zh,en}.md 新增「插件元数据」章节
(EGO_MIN_VERSION 明确为兼容性声明,非 enforcement)。
测试:325 passed。新增 5 项(restore 覆盖 DB / 并发同名上传 / restore 重启监听 /
超大上传 413 / created_at 保真),并修复 restore 测试污染共享测试库的问题。
#9 缺核心配置文件不再被当成"空配置" - import_from_json 会清空全部 5 张表再按 JSON 重建,所以缺文件的 ZIP 等于把那几张表静默清空。现在 /api/restore 要求 ZIP 必须含全部 5 个核心 配置文件(parsers/sources/channels/templates/bindings),缺一即拒绝。 - 兼容性核实:_CONFIG_FILES 自 278a223 引入以来就是这 5 个 (git log -S'settings.json' 零命中),且 export_all() 每次启动写全 5 个, 所以真 EGo 备份必然齐全 —— 该检查不会误伤任何合法备份。 - 只要求"已知的这 5 个",不限制 ZIP 内多余文件 → 将来新增配置项时旧包仍可恢复。 校验移到"替换文件之前" - 抽出 _stage_and_validate():落临时目录 + 文件名安全 + JSON 可解析 + **结构校验(_validate_config,非空即 fatal)** + 插件可加载。 原先结构校验只在 import_from_json 里 log.warning,形状错要等导入时 KeyError 才炸 —— 而那时插件文件已经替换完了,会留下半成品。 #10 dry-run(WebUI「预览」)不再"读个文件名就返回" - 走与真实恢复**完全相同**的校验(体积 / 完整性 / JSON 结构 / 插件可加载), 只是不落最终位置;返回 errors 与 staged(将要写入的文件)。 - 前端:renderDiff 早已在读 data.errors(此前恒为空,属死代码),现已接上; 校验不通过时不显示"确认恢复"按钮;预览补渲染 channels 插件行。 文档:README 中英 + architecture.md 说明"恢复要求完整备份"与"预览即校验"。 测试:330 passed。新增 5 项(残缺包拒绝 / 残缺包 dry-run 拒绝 / dry-run 校验 不落盘 / dry-run 通过时回报 staged / 结构非法在 staging 即拒绝且不写插件文件), 均已验证"回退修复则失败";并为 test_plugin_dirs 的 restore 测试补上 config 目录 + 数据库隔离(restore 现在真写 CONFIG_DIR 并全量替换 DB)。
按用户要求推翻 65804b5 里"缺核心配置文件即拒绝"的处理: import_from_json(only=...) 支持部分恢复 - 只处理**指定**的 schema_key:清空对应表 → 按 JSON 重建(保留 id/created_at); 不处理的表**原样保留**(不清空)。 - 区分:文件被处理且内容为 `[]` → 清空该表;不在处理范围 / 文件不存在 → 表不动。 - 导入末尾清理指向已消失 source/channel/template 的孤儿绑定 —— 部分恢复后 绑定可能悬空,留着会让路由持续匹配不存在的对象而反复失败。 - 表格驱动(_IMPORT_SPECS),去掉原先手写的 5 段重复代码。 api/restore:完整性由 error 降级为 warning - 缺核心文件不再拒绝,改在 result["warnings"] 提示;新增 config_skipped 回报"未包含、故原样保留"的表。dry-run 同样提示。 修一个实测到的真 bug - import_from_json 原先按**磁盘**判断配置文件是否存在。但 config/ 目录里通常 已有上一轮导出的 parsers.json,于是"ZIP 没带 parsers.json"被误判成"带了", 陈旧内容被导入(test env 实测 config_imported 里出现 parsers、config_skipped=None)。 - 改为由 **ZIP 内容**决定允许覆盖哪些表:present_keys = ZIP 里 config/*.json 对应的 key。 UI 文案(原先没跟上备份内容的变化) - set.backup_desc 补上"通道插件"(备份早就含 channels/*.py,文案一直只写解析器) - set.restore_desc / set.confirm_restore 改为部分恢复语义(原"当前配置将被覆盖"已失准) - 新增 set.restore_warn / set.restore_skipped;renderDiff 渲染 warnings 与 config_skipped 文档:README 中英 + architecture.md 同步为"部分恢复 + 提示"(撤掉上一版的"缺一即拒绝")。 测试:332 passed。新增/改写 6 项(部分恢复保留未含表 / dry-run 提示 / 孤儿绑定清理 / 磁盘陈旧文件不被导入 / 结构非法在 staging 即拒绝 / dry-run 回报 staged), 均验证"回退修复则失败"。
版本号(5 处 + 变更日志头部) - main.py:34 / api/pages.py:13 / build.py:16(含用法示例)→ "1.3.2" - README.md:1 / README.en.md:1 标题 → v1.3.2 - version.json:version=1.3.2、release_date=2026-09-26、changelog 换成 1.3.2 的 6 条 - doc/changelog.md:头部「开发中,尚未发布」→「(2026-09-26)」 version.json 的 upgrade_warning 改写 - 1.3.1 的原文案是「本次升级包含插件目录拆分(破坏性变更)」,对升到 1.3.2 的用户不成立 (那是 1.3.1 的事),直接置空又会丢掉"升级前先备份"的提示 → 换成 1.3.2 的事实性提醒: 「升级前请先备份:请先在「系统设置 → 备份」导出 ZIP,内含全部配置与你上传的插件 (含完整推送凭据,请妥善保管)。升级后如遇异常,可随时用它恢复。」(中英各一条) 文档修正 - README 中英:「恢复语义」段落原先**自相矛盾**(上一句说"未出现在备份里的配置会被移除", 下一句说"保持原样")—— 合并为准确表述;并在配置章节加⚠️ 「升级前请先备份」提示。 - deploy/README.md 与 README.en.md 新增「升级与备份」章节(原先**完全没有**): 升级前先备份 + `docker compose pull && up -d`、五个卷的内容清单、 以及 `down -v` 会连数据库与配置一起删除的警告。 - doc/changelog.md 新增 v1.3.2 条目(恢复语义修正、校验边界、插件 metadata、 修掉的真 bug、文档变更)。 注意(发版链路):version_checker 读的是 **main 分支**的 version.json (GITHUB_VERSION_URL,version_checker.py:16)。当前 origin/main 仍是 1.3.0 且无 upgrade_warning,因此 1.3.1 的升级告警从未送达真实用户。需把本文件合并到 main 才生效。 332 passed。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.