跳转至

章节后台插图

章节正文和 illustration_task 在同一数据库事务中保存。任务状态为 queued 时,独立 worker 每 5 秒扫描并派发至 tender_section_illustration 队列;Redis 不可用时数据库中的任务仍然保留,下次扫描继续派发。API 和正文生成 worker 不执行图片搜索、选图或下载。

服务边界

  • ImageRetrievalService(image_retrieval_service.py):输入配图需求,调用搜图 provider,完成 按关键词及返回顺序取图和安全下载转存(不调用 LLM 筛选);输出按 query_id 对应的可用图片或失败原因。不决定正文插入位置,不访问章节数据库。
  • SectionIllustrationService(section_illustration_service.py):读取正文版本快照,接收已经取得的图片;根据 image_position_hint 定位,仅由后端解析小节边界,匹配不到唯一有效位置则记录 placement_unmatched 并跳过,不调用 LLM 或另选位置;校验版本和任务 ID 后原子保存正文、来源记录与任务结果。不调用搜图 provider 或下载图片。
  • SectionIllustrationWorker:依次调用“读取快照 → 聚合搜图 → 定位插入”,并处理超时、持久化状态和未使用上传对象的清理。两个阶段在同一章节任务内衔接,仍保持三个章节并发和搜图共享 1 QPS。

默认一个插图 worker 并发处理 3 个章节。搜索仍按同一百度凭据跨进程共享 1 QPS,选图、下载、转存可交错运行。每次搜索的排队、请求和一次 429 重试合计最多 60 秒;每条需求先逐个搜索全部 querys(每词默认最多召回 2 张,由 IMAGE_SEARCH_RESULTS_PER_QUERY 配置),代码按标题匹配为每词选出最多 1 张后按每个 query_id 的随机目标选 1~3 张;下载失败从剩余候选补选,每需求每次执行最多下载 3 次,OSS 上传失败停止当前需求。每个章节插图任务总时限默认 900 秒。

每个 query_id 是一个最多 3 张的处理批次(IMAGE_PROCESSING_BATCH_SIZE);批内逐张执行下载→完整解码校验→WebP 压缩→上传,完成后处理下一张,再进入下一批。不同章节仍可并发。结果列表只保存 URL 和元数据,不保存原图字节。压缩最长边 2000、质量 85,不放大图片,限制输入 1600 万像素和 8MB;压缩在线程执行。DOCX 导出将 WebP 临时转 PNG 后插入。此处的批次不是额外的 ARQ 子任务,最终正文仍按章节原子提交。

任务从 queued 进入 running,最终成为 completed、no_matches、error、already_inserted 或 no_requirements。completed 可以包含部分失败,详情在 items。已插图片和完成状态原子提交;版本或任务 ID 变化时旧任务不能覆盖新正文。运行进程中断后,超过任务时限加 60 秒会重新派发一次,第二次中断则记为错误,等待人工重试。

启动与发布

先执行新迁移 f6d27d4a7d52,再启动新版 API、正文生成 worker 和插图 worker,同时发布新版前端。此改动不自动执行数据库迁移或部署。

开发环境:

make migrate-dev
make worker-section-illustration

make worker、make worker-test 已包含该 worker。本地单独连接测试环境启动使用 make worker-section-illustration-test。Docker 服务名是 worker-section-illustration。生产部署保持原行为;测试环境改为以下独立部署。

测试环境独立部署到 RAG 服务器

在本地 test 分支执行 make deploy-illustration-test。该命令拉取已有 Tender :test 镜像,只更新插图 worker,不执行迁移、Prompt 同步或重启 API / RAG。镜像对应的数据库迁移需先通过 Tender 发布完成。

  • 默认主机:tender-rag;目录:/opt/tender/section-illustration-test。
  • Compose 文件:docker-compose.illustration-test.yaml;项目:tender-illustration-test;容器:tender-worker-section-illustration-test。
  • 配置继续由 /root/docker/common/.env.infisical.test 对应的 Tender Infisical test 环境注入,包括 BAIDU_SEARCH_API_KEY、搜图参数、OSS 配置、数据库和队列设置,无需复制到 RAG 的 .env。
  • 连接已有 tender_internal 网络和 tender-test-redis,启动前核对数据库为 tender_test。不使用 RAG 自己的 Redis 或数据库。此默认配置要求与 Tender 测试环境同机。
  • 首次切换会核对并停止同机旧测试插图容器,给正在运行的任务最多 930 秒退出时间;新容器启动或健康检查失败时恢复旧容器,健康检查通过后删除旧容器。
  • Tender 测试全量部署将原插图服务缩容为 0;deploy-api-generation-test 只发布 API 与正文生成 worker。之后插图代码更新需单独执行 make deploy-illustration-test,全量 Tender 发布不会更新这个独立项目。

测试环境当前与 RAG 共用服务器,拆分的是部署项目和发布生命周期,并不会减少这台主机的总资源消耗。

可配置项:

  • IMAGE_SEARCH_RESULTS_PER_QUERY=2(每个关键词候选上限,范围 1~2;21 个关键词最多召回 42 张候选,文字匹配后最多留下 21 张;不合格的跳过,只有选中图片会下载上传;修改后重启插图 worker)
  • ARQ_SECTION_ILLUSTRATION_QUEUE_NAME=tender_section_illustration
  • ARQ_SECTION_ILLUSTRATION_MAX_JOBS=3(每个 worker 进程的章节并发数)
  • ARQ_SECTION_ILLUSTRATION_TIMEOUT_SECONDS=900

迁移不主动触发历史章节的付费搜图;之后生成的章节会自动记录任务,历史章节可通过入队接口显式发起。

数量分配采用惰性 Fisher–Yates 无放回抽样,按同一章节、需求数 N 建立 3^N 个完整方案的逻辑池。Python 标准库 random.randrange 选择剩余位置,稀疏交换表避免枚举全部组合;新历史字段 illustration_allocation_history 保存剩余数量、交换表与已用方案。正文重生成和清空插图任务不清除此字段。不同 N 分开记录,切回原 N 会继续原方案池。失败重试沿用 image_counts,不消耗新方案。所有方案耗尽时明确报错,不静默重置。历史从启用该机制后开始记录,不追溯以前未记录的分配。

保证的是整个分配向量不重复,不再强制相邻节点张数不同或每个节点都与上次不同。每个节点仍为 1~3 张,实际插入数可能受候选和下载失败影响。此机制不负责图片内容去重。历史与正文在同一事务、同一标书锁下提交;事务回滚不会消耗方案。

新增迁移 5225d842e6c1(前驱 f6d27d4a7d52)必须在运行新版 API 和 worker 前执行。未自动迁移或部署。

前端接口

  • GET /v1/tenders/{tender_id}/illustration-tasks/events:SSE 首帧直接读取数据库任务快照(无任务时也发送 snapshot,内容为 []),不等待 Redis;订阅成功后再次读取,补齐建连期间变化。之后通过 Redis 通知推送状态变化,15 秒心跳;Redis 订阅失败或中途断开时,本次连接改为每 5 秒读取数据库。
  • GET /v1/tenders/{tender_id}/illustration-tasks:只查询属主的章节任务和结果,不触发搜图。
  • POST /v1/tenders/{tender_id}/sections/{section_id}/illustrations,请求体 {"expected_version": 2}:入队或重试,立即返回持久化状态;相同章节已有活动任务时返回原任务。

正文页面使用 SSE 订阅,不再每 2 秒查询任务。worker 派发入队、开始/结束、中断恢复及手动入队提交后发送 Redis 通知。Redis 正常时服务端每 60 秒校准数据库,补偿丢失通知及其他状态变更;降级后每 5 秒校准,快照不变则不发送事件。每次重新连接都会发送首帧,即使快照与上次连接相同。离开页面关闭连接。手动重试仍跳过已插图片。页面刷新或关闭不取消后台任务;重新进入后从数据库恢复状态。当前章节插图期间禁止编辑保存,完成后先读取最新含图正文再解除保护。

当前前端只消费 snapshot,尚未处理服务端 error 事件及正常 EOF 重连;数据库快照读取失败会发送 error 后关闭连接,前端仍需补齐该异常处理。正文生成提交尚未直接发送插图通知;worker 未派发的终态任务及清空任务的变化可能等到定期校准才推送。历史章节没有 illustration_task 时不会出现在快照中,接口不会自动补建任务或触发搜图。

待运行验收

  • 三个章节同时生成:允许三个插图任务运行,百度请求仍合计不超过 1 QPS。
  • 正文提交时 Redis 暂不可用:恢复后任务能被派发。
  • 页面刷新、关闭再打开:任务继续并恢复状态。
  • 重复入队和重复领取:同一任务只允许一个执行者。
  • 插图途中重新生成、调整目录或删除章节:旧任务不能覆盖新正文或其他章节。
  • 部分成功后重试:保留已插图片,仅处理未完成需求。
  • 杀死 worker 后重启:中断任务按时限恢复,重复中断有上限。

本轮只做源码、Python 语法、迁移链和 shell 语法检查;未运行测试、真实外部请求或浏览器验收。

关键词图片缓存

关键词去首尾空格后精确匹配,image_keyword_cache 保存首个成功转存的图片及标题,不设置自动过期。命中后跳过搜索、下载、压缩与上传。图片沿用图库的 images/{gallery_id}/{随机文件名}.webp 路径,章节任务不清理缓存。每份标书自动建立“标书标题-自动配图”图库,ID 保存在标书 metadata 的 illustration_gallery_id;图片写入 image_files 并添加 tender_image_references。缓存命中复用 OSS 文件,仅补齐当前图库文件记录和标书引用;保留搜索标题、大小,跳过 AI 图片分析。图库删除沿用软删除,不删除共享 OSS 文件。失败结果不缓存;首次并发未命中可能重复搜索/上传,数据库唯一键保留首个成功结果,其他上传删除。旧章节图片不会自动回填缓存。

启用前执行迁移 569ab33717aa,并重启 API 与插图 worker。

标题匹配使用中文双字片段/英文词覆盖率,最低覆盖率 30%,以覆盖率和标题精确率加权排序,同分保留上游顺序。无标题或明显无关结果不插入,不调用 LLM。该规则仅判断文字相关性,不判断图片实际内容。缓存键包含 title-match-v1 版本,旧缓存和已保存图库图片保留,但新请求不再命中未经筛选的旧缓存。