fix(ai-outfit): 标题解析改为「按逗号分割」,避开 Markdown 表格坑 (§19.23)

实测模型常返回 Markdown 表格(編號/標題/字元數估算 三列),按行解析会把
表头/分隔线/带 | 数字的行当成标题,写进 Excel A 全是「| 編號 | 標題 | … |」。
改用「提示词约定标题逗号分隔 + 解析按逗号拆分」,简单可靠。

- ai_text_service._clean_titles:从按行拆改为按逗号拆(半角 , / 全角 ,,
  换行作兜底分隔),每段走 _clean_title_line 去空白/序号/引号、丢空
- config_service.DEFAULT_TITLE_PROMPT:改逗号分隔风格,明确禁 Markdown 表格/
  换行/序号/編號/字元數/引号/表情
- extract_text_from_response(取首条)、generate_texts/generate_titles/
  _TitleWorker 均不变

测试:逗号分隔(全角/半角)→ 多条;逗号+换行混用都拆开;逐段去序号/引号;
Markdown 表头单行不再炸成多条垃圾。全套 py37 通过(test_config_service 的
packaging 模板失败属并行 §19.13,与本改动无关)。离屏冒烟:逗号响应解析 +
按序回填 Excel A。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 16:24:34 +08:00
co-authored by Claude Opus 4.8
parent e7fab724fc
commit 61d8e20d86
5 changed files with 64 additions and 19 deletions
+5 -5
View File
@@ -428,8 +428,8 @@ Excel 行 → `OutfitTask` 列表的转换由 `excel_service` 完成;核心只
> **一次请求、纯提示词(不传图)、生成多条、按序回填**(用户在提示词里自写数量)。
- **行来源**:复用 `excel_service.read_all_rows(excel)`(状态无关)——拿到全部有效行(仅为「填到哪些行 + 行号」),**覆盖式**写 A,不看 E 列状态、不引入新列。
- **一次请求、纯提示词**:把用户标题提示词**原样**(不带图、不替换 `{title}`)发一次给文本模型;提示词里由用户自写数量(如「生成 6 条…每行一条」)。
- **解析多条**:从响应里把文本**按行**拆成多条,每条清洗(去行首序号/符号 `1. / - / ①`、去首尾引号、单行化、丢空行)→ 得到标题列表。
- **一次请求、纯提示词**:把用户标题提示词**原样**(不带图、不替换 `{title}`)发一次给文本模型;提示词里由用户自写数量并**约定标题之间用逗号隔开**(如「生成 6 条…标题之间用逗号分隔,不要表格/换行/序号」)。
- **按逗号拆分多条**:把响应文本按**逗号**(半角 `,` / 全角 `,`,并兼容换行作兜底分隔)拆成多条,每条清洗(去首尾空白、去行首序号/符号 `1. / - / ①`、去首尾引号、丢空)→ 得到标题列表。**不再按行/不解析 Markdown 表格**——靠提示词约定逗号分隔(§19.23)。
- **按序回填**:第 i 条 → 第 i 行 A(`write_title_result(excel, row_index, title)`),逐条写、刷新中栏「处理明细」表该行「标题」列。
- **条数对不上时兜底**:`N = len(标题)`,`R = len(行)`。`N > R` → 多出的 `N-R` 条**丢弃** + 日志提示;`N < R` → 后 `R-N` 行**留空** + 日志提示。不报错中止。
- **生成完重载 Excel**:刷新界面与内存任务,紧接「开始生成」跑图即用新标题(图片生成当场 `load_outfit_tasks` 读 A 列)。
@@ -443,7 +443,7 @@ Excel 行 → `OutfitTask` 列表的转换由 `excel_service` 完成;核心只
- `images`/`images_edits`:纯图片接口,不能返回文字 → 抛 `AiTextServiceError`。
- 一次 POST → `extract_titles_from_response` 返回**多条**清洗后标题。
- `generate_text(prompt, image_path=None) -> str`:保留(返回第一条,= `generate_texts` 的 `[0]`),供单条场景与既有单测;与 `generate_texts` 共用同一段 POST。
- `extract_titles_from_response(data) -> List[str]`:取 `choices[0].message.content` / gemini `candidates[0].content.parts[].text` 的原始文本,**按行拆分 + 逐行清洗**(去序号/符号/引号、单行化、丢空),返回标题列表。`extract_text_from_response` = 取其首条(兼容保留)。
- `extract_titles_from_response(data) -> List[str]`:取 `choices[0].message.content` / gemini `candidates[0].content.parts[].text` 的原始文本,**按逗号(`,`/`,`,兼容换行)拆分 + 逐段清洗**(去首尾空白/序号/符号/引号、丢空),返回标题列表(§19.23)。`extract_text_from_response` = 取其首条(兼容保留)。
### 17.3 模型与提示词
@@ -455,7 +455,7 @@ Excel 行 → `OutfitTask` 列表的转换由 `excel_service` 完成;核心只
- **命中的模型是纯图片接口(images/images_edits)时,开跑前就拦截(§19.21)**:`_find_title_model` 检出 `api_type ∈ {images, images_edits}` → 返回错误「『{名字}』是图片模型({api_type}),不能生成文字标题,请改选 chat/gemini 文本模型」→ `_resolve_title_model_config` 弹窗 + 中止,不逐行失败。
- 边界:`api_type=chat` 但实际返回图片的模型(如 Nano Banana)类型上无法预判,仍在运行时由 `AiTextClient`/「未找到文字标题」逐行暴露——这是固有限制。
- **`ai_models.json` 需有一条文本/视觉模型**(管理员维护,含真实 key,不入库):`api_type` 设 `chat`、`model` 填中转的真实 id(如 `gpt-5.5`)、`name` 与 `title_model` 一致(默认 `GPT-5.5 文本`)。`docs/ai_models.sample.json` 已含 chat 示例可参照。
- **标题提示词**:单份,存 `~/.cmbot/config/title_prompt.txt`(`load_title_prompt`/`save_title_prompt`,仿旧式单份,标题侧不做多套模板);默认文案为**批量风格**——让模型生成**多条**电商女装标题、**每行一条**、不带序号/引号/表情,数量由用户在提示词里写(默认示例可写「生成 10 条」)。
- **标题提示词**:单份,存 `~/.cmbot/config/title_prompt.txt`(`load_title_prompt`/`save_title_prompt`,仿旧式单份,标题侧不做多套模板);默认文案为**批量风格 + 逗号分隔**——让模型生成**多条**电商女装标题、**标题之间用逗号分隔**、**不要表格/换行/序号/引号/表情**,数量由用户在提示词里写(默认示例可写「生成 10 条」)。与 §17.1 的逗号拆分配套。
### 17.4 界面与运行
@@ -472,7 +472,7 @@ Excel 行 → `OutfitTask` 列表的转换由 `excel_service` 完成;核心只
- 条数对不上:N>行数多的丢弃 + 日志;N<行数后面行留空 + 日志(不报错中止)。
- `title_model` 找不到对应模型 / 命中条目是纯图片接口(images/images_edits)时,**开跑前**弹窗报错并中止(§19.21)。
- 生成完重载 Excel,紧接「开始生成」跑图用的是新标题。
- `extract_titles_from_response`(多行→多条、清洗)、`generate_titles`(一次请求纯文本、客户端异常)、`write_title_result`(只改 A)、`_find_title_model`(命中/缺失/图片模型报错)单测通过,Python 3.7。
- `extract_titles_from_response`(**按逗号拆分**→多条、清洗、兼容换行、Markdown 表格不再当多条)、`generate_titles`(一次请求纯文本、客户端异常)、`write_title_result`(只改 A)、`_find_title_model`(命中/缺失/图片模型报错)单测通过,Python 3.7。
### 17.6 决策:标题模型「配置定名」而非下拉