feat: support old title placeholder in title prompts
This commit is contained in:
@@ -127,11 +127,7 @@ def gen_title(
|
|||||||
payload = _chat_payload(
|
payload = _chat_payload(
|
||||||
model,
|
model,
|
||||||
[
|
[
|
||||||
{"role": "system", "content": str(title_prompt or "").strip()},
|
{"role": "user", "content": _compose_title_prompt(title_prompt, old_title)},
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "旧标题:\n%s\n\n请只返回新标题,不要解释。" % str(old_title or ""),
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
attempts = _attempt_count(ai_cfg, retry)
|
attempts = _attempt_count(ai_cfg, retry)
|
||||||
@@ -840,7 +836,7 @@ def _gen_title_cmhub(
|
|||||||
resolution = _normalize_cmhub_resolution(ai_cfg.get("resolution", "1k"))
|
resolution = _normalize_cmhub_resolution(ai_cfg.get("resolution", "1k"))
|
||||||
_notify_step(on_step, "title_build_request")
|
_notify_step(on_step, "title_build_request")
|
||||||
payload = {
|
payload = {
|
||||||
"prompt": _cmhub_title_prompt(title_prompt, old_title),
|
"prompt": _compose_title_prompt(title_prompt, old_title),
|
||||||
"model": runtime["alias"],
|
"model": runtime["alias"],
|
||||||
"resolution": resolution,
|
"resolution": resolution,
|
||||||
}
|
}
|
||||||
@@ -1045,11 +1041,16 @@ def _cmhub_runtime(config, operation, cmhub_config_path):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _cmhub_title_prompt(title_prompt, old_title):
|
def _compose_title_prompt(title_prompt, old_title):
|
||||||
return "%s\n\n旧标题:\n%s\n\n请只返回新标题,不要解释。" % (
|
prompt = str(title_prompt or "").strip()
|
||||||
str(title_prompt or "").strip(),
|
old_title_text = str(old_title or "")
|
||||||
str(old_title or ""),
|
if "{旧标题}" in prompt:
|
||||||
)
|
body = prompt.replace("{旧标题}", old_title_text).strip()
|
||||||
|
else:
|
||||||
|
body = "%s\n\n旧标题:\n%s" % (prompt, old_title_text)
|
||||||
|
if body:
|
||||||
|
return "%s\n\n请只返回新标题,不要解释。" % body
|
||||||
|
return "请只返回新标题,不要解释。"
|
||||||
|
|
||||||
|
|
||||||
def _normalize_cmhub_resolution(resolution):
|
def _normalize_cmhub_resolution(resolution):
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ class GenerateTab(QWidget):
|
|||||||
prompts.load_title_prompt(self.title_prompt_path)
|
prompts.load_title_prompt(self.title_prompt_path)
|
||||||
)
|
)
|
||||||
self.save_title_button = QPushButton("保存标题提示词")
|
self.save_title_button = QPushButton("保存标题提示词")
|
||||||
|
self.insert_old_title_button = QPushButton("插入旧标题")
|
||||||
|
self.insert_old_title_button.setObjectName("insertOldTitleButton")
|
||||||
self.cover_prompt_edit = QPlainTextEdit()
|
self.cover_prompt_edit = QPlainTextEdit()
|
||||||
self.cover_prompt_edit.setObjectName("coverPromptEdit")
|
self.cover_prompt_edit.setObjectName("coverPromptEdit")
|
||||||
self.cover_prompt_edit.setPlaceholderText("封面提示词")
|
self.cover_prompt_edit.setPlaceholderText("封面提示词")
|
||||||
@@ -115,7 +117,11 @@ class GenerateTab(QWidget):
|
|||||||
left_layout.setContentsMargins(0, 0, 12, 0)
|
left_layout.setContentsMargins(0, 0, 12, 0)
|
||||||
left_layout.addWidget(QLabel("标题提示词"))
|
left_layout.addWidget(QLabel("标题提示词"))
|
||||||
left_layout.addWidget(self.title_prompt_edit, 1)
|
left_layout.addWidget(self.title_prompt_edit, 1)
|
||||||
left_layout.addWidget(self.save_title_button)
|
title_action_layout = QHBoxLayout()
|
||||||
|
title_action_layout.addWidget(self.save_title_button)
|
||||||
|
title_action_layout.addWidget(self.insert_old_title_button)
|
||||||
|
title_action_layout.addStretch(1)
|
||||||
|
left_layout.addLayout(title_action_layout)
|
||||||
left_layout.addWidget(QLabel("封面提示词"))
|
left_layout.addWidget(QLabel("封面提示词"))
|
||||||
left_layout.addWidget(self.cover_template_combo)
|
left_layout.addWidget(self.cover_template_combo)
|
||||||
cover_template_layout = QHBoxLayout()
|
cover_template_layout = QHBoxLayout()
|
||||||
@@ -230,6 +236,7 @@ class GenerateTab(QWidget):
|
|||||||
self.status_filter.currentIndexChanged.connect(self.refresh_tasks)
|
self.status_filter.currentIndexChanged.connect(self.refresh_tasks)
|
||||||
self.refresh_button.clicked.connect(self.refresh_tasks)
|
self.refresh_button.clicked.connect(self.refresh_tasks)
|
||||||
self.save_title_button.clicked.connect(self.save_title_prompt)
|
self.save_title_button.clicked.connect(self.save_title_prompt)
|
||||||
|
self.insert_old_title_button.clicked.connect(self.insert_old_title_placeholder)
|
||||||
self.cover_template_combo.currentIndexChanged.connect(self.load_selected_cover_template)
|
self.cover_template_combo.currentIndexChanged.connect(self.load_selected_cover_template)
|
||||||
self.new_cover_template_button.clicked.connect(self.new_cover_template)
|
self.new_cover_template_button.clicked.connect(self.new_cover_template)
|
||||||
self.save_cover_template_button.clicked.connect(self.save_cover_template)
|
self.save_cover_template_button.clicked.connect(self.save_cover_template)
|
||||||
@@ -454,6 +461,9 @@ class GenerateTab(QWidget):
|
|||||||
def insert_title_placeholder(self, checked=False):
|
def insert_title_placeholder(self, checked=False):
|
||||||
self.cover_prompt_edit.insertPlainText("{新标题}")
|
self.cover_prompt_edit.insertPlainText("{新标题}")
|
||||||
|
|
||||||
|
def insert_old_title_placeholder(self, checked=False):
|
||||||
|
self.title_prompt_edit.insertPlainText("{旧标题}")
|
||||||
|
|
||||||
def preview_cover_prompt(self, checked=False):
|
def preview_cover_prompt(self, checked=False):
|
||||||
task = self._selected_task()
|
task = self._selected_task()
|
||||||
if task is None:
|
if task is None:
|
||||||
|
|||||||
@@ -416,9 +416,9 @@ data/images/<batch_id>/<slug>/<task_id>_<item_id>_new.<ext> # AI 生成的新
|
|||||||
|
|
||||||
提示词管理:
|
提示词管理:
|
||||||
|
|
||||||
- **标题提示词**:单个文本,「保存」写入 `data/title_prompt.txt`;软件启动时加载该文件回显到输入框(缺失则空)。
|
- **标题提示词**:单个文本,「保存」写入 `data/title_prompt.txt`;软件启动时加载该文件回显到输入框(缺失则空)。T-549 后标题提示词支持 `{旧标题}` 占位符:若提示词含 `{旧标题}`,生成前替换为该任务旧标题且不再自动追加旧标题块;若不含,则保持旧行为自动追加“旧标题:...”块。两种情况都会保留“请只返回新标题,不要解释。”输出约束。
|
||||||
- **封面提示词**:多模板。下拉选模板(读 `data/prompts/cover/*.txt`),图标工具栏 新建/保存/另存为/重命名/删除;重名校验、删除二次确认、删空给默认。
|
- **封面提示词**:多模板。下拉选模板(读 `data/prompts/cover/*.txt`),图标工具栏 新建/保存/另存为/重命名/删除;重名校验、删除二次确认、删空给默认。
|
||||||
- **变量**:封面提示词支持占位符 `{旧标题}`、`{新标题}`、`{商品id}`、`{店铺}`,生成前用该任务真实值替换(`render_prompt`)。「插入标题」= 在光标处插入 `{新标题}`;「预览」= 用某条任务的值替换变量后展示,确认实际发送给 AI 的内容。
|
- **变量**:标题提示词本阶段只支持 `{旧标题}`,左侧按钮「插入旧标题」在标题提示词光标处插入 `{旧标题}`。封面提示词支持占位符 `{旧标题}`、`{新标题}`、`{商品id}`、`{店铺}`,生成前用该任务真实值替换(`render_prompt`)。「插入标题」= 在封面提示词光标处插入 `{新标题}`;「预览」= 用某条任务的值替换封面变量后展示,确认实际发送给 AI 的内容。
|
||||||
|
|
||||||
### 6.3 应用更新(③ Tab)
|
### 6.3 应用更新(③ Tab)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -139,7 +139,7 @@
|
|||||||
| T-536 | GUI 按钮圆角全局统一 | T-512, T-513, T-523 | 现象:只有③「开始更新」及少数上色按钮(删除批次/删除账号/未匹配(n))有 `border-radius: 4px`,其余按钮走原生渲染显直角——不一致。根因:圆角是 T-512/T-513 给按钮上色时顺带写进 QSS 的副产品,不是全局形状决策;一旦给 `QPushButton` 设 stylesheet 就放弃原生渲染,才补了 radius/border。方案(**方式 A:全局统一圆角**):① 在 `app/gui/widgets.py` 抽一个**共享按钮基础样式常量/helper**(统一 `border-radius`,如 4px,与卡片 6px 圆角语言协调),并**接管按钮的全部视觉状态**——normal/hover/pressed/disabled/focus 的背景、边框都定义好,避免全局设 QSS 后按钮变扁平方块、丢 hover 反馈;② 在主窗口/app 级用全局 `QPushButton` QSS 应用该基础样式,让**所有按钮共享同一圆角**;③ warning(`startUpdateButton`)与 danger(`_danger_outline_button_style`:删除批次/删除账号/未匹配)按钮改为**只叠加颜色**,复用共享基础样式的圆角/内边距/状态,不再各自重写 radius/border——杜绝“上色=顺带圆角”的隐性耦合;④ 以 Windows 为主目标做一次视觉自测(hover/按下/禁用不劣于原生)。只改 GUI 样式层(`widgets.py` + 主窗口全局 QSS + 各上色按钮引用),不改任何按钮的启用/禁用逻辑、行为、业务流程、DB、Excel、Shopee/CDP。GUI 单测至少断言上色按钮仍带各自语义色且不再各自硬写 radius(改为引用共享样式);圆角外观本身以人工视觉验收为准 | DONE |
|
| T-536 | GUI 按钮圆角全局统一 | T-512, T-513, T-523 | 现象:只有③「开始更新」及少数上色按钮(删除批次/删除账号/未匹配(n))有 `border-radius: 4px`,其余按钮走原生渲染显直角——不一致。根因:圆角是 T-512/T-513 给按钮上色时顺带写进 QSS 的副产品,不是全局形状决策;一旦给 `QPushButton` 设 stylesheet 就放弃原生渲染,才补了 radius/border。方案(**方式 A:全局统一圆角**):① 在 `app/gui/widgets.py` 抽一个**共享按钮基础样式常量/helper**(统一 `border-radius`,如 4px,与卡片 6px 圆角语言协调),并**接管按钮的全部视觉状态**——normal/hover/pressed/disabled/focus 的背景、边框都定义好,避免全局设 QSS 后按钮变扁平方块、丢 hover 反馈;② 在主窗口/app 级用全局 `QPushButton` QSS 应用该基础样式,让**所有按钮共享同一圆角**;③ warning(`startUpdateButton`)与 danger(`_danger_outline_button_style`:删除批次/删除账号/未匹配)按钮改为**只叠加颜色**,复用共享基础样式的圆角/内边距/状态,不再各自重写 radius/border——杜绝“上色=顺带圆角”的隐性耦合;④ 以 Windows 为主目标做一次视觉自测(hover/按下/禁用不劣于原生)。只改 GUI 样式层(`widgets.py` + 主窗口全局 QSS + 各上色按钮引用),不改任何按钮的启用/禁用逻辑、行为、业务流程、DB、Excel、Shopee/CDP。GUI 单测至少断言上色按钮仍带各自语义色且不再各自硬写 radius(改为引用共享样式);圆角外观本身以人工视觉验收为准 | DONE |
|
||||||
| T-546 | cmhub 客户端共享 Session + 连接池 + 代理处理(补 T-545 未覆盖的下载慢根因) | T-545, T-526 | 现象:实测生图拿到 `image_url` 后本地下载单条约 191 秒、而同一 URL 用 curl 约 10 秒;日志伴随 `connect_timeout: 连接 cmhub 超时`。T-545 已做「生图并发上限 5 + 独立下载线程池」解决“下载堵住生图槽位”的流水线问题,但**未根治单条下载在并发争抢下变慢**。根因(代码核实):① cmhub 所有 HTTP(`_cmhub_call_once` 的生成/models/balance、`_download_cmhub_image` 的下载)都是**裸 `requests.get`/`requests.request`、无共享 `Session`**,每个任务全新 TCP+TLS,叠加同步生成长连接(单条 60~260s)与 `connect_timeout` 触发的重试,形成连接风暴/连接饥饿;② `requests` 默认 `trust_env=True` 读 `HTTP(S)_PROXY`/`ALL_PROXY`,若系统带慢代理会拖累,而干净窗口的 curl 直连快。方案:① cmhub 所有 HTTP 统一走一个**模块级共享 `requests.Session`**,挂 `HTTPAdapter(pool_connections/pool_maxsize)`,池大小 ≥(实际生图并发 + 下载并发)以免连接不足排队、复用连接减少握手与 connect_timeout;生成与下载共享该 Session(`requests.Session` 跨线程发请求安全,但连接池要够大);② 代理处理——诊断日志脱敏记录 cmhub 请求是否经代理;对 cmhub 请求提供明确策略(可配 `trust_env=False` 或显式 `proxies`),避免误走慢代理,默认可先保持读环境但提供关闭开关;③ 复测口径——Session+连接池到位后单条下载耗时应回落到与 curl 同量级;若在 T-545 的并发上限 5 下仍显著慢,评估把 cmhub 生图默认并发再降(1~2);④ 确认 connect_timeout 重试退避不加剧连接风暴。诊断建议:可先临时把图片并发设 1 复测以区分“并发争抢”与“代理”。边界:只改 `app/ai.py` 的 cmhub HTTP 客户端层(+ 可选 config 代理项)+ 诊断日志 + 相关文档;不改 cmhub 协议、T-545 的并发/下载池语义、生成编排、DB、Excel、CDP/Shopee。测试:`tests/test_ai.py` 覆盖共享 Session 被复用(mock 同一 session 多次调用)、代理配置被尊重、连接池大小设定;现有 cmhub 用例保持绿。落地:`app/ai.py` 加模块级 `_cmhub_session()`(`HTTPAdapter` 池 `CMHUB_HTTP_POOL_SIZE=32`)+ `_apply_cmhub_proxy()`;生成/下载/models/balance 统一走该 Session;`ai.cmhub.use_system_proxy` 默认 `false`(绕过系统代理,公网网关直连),`_cmhub_runtime` 读取并应用、`fetch_cmhub_models/balance` 加同名参数;`test_ai.py` 补 3 项新测试并把原 18 处 `requests` mock 改到共享 Session;243 单测全绿。**待 Windows 实网复测确认下载提速**(troubleshooting 已加「cmhub 图片下载很慢」排障节与代理定位命令) | DONE |
|
| T-546 | cmhub 客户端共享 Session + 连接池 + 代理处理(补 T-545 未覆盖的下载慢根因) | T-545, T-526 | 现象:实测生图拿到 `image_url` 后本地下载单条约 191 秒、而同一 URL 用 curl 约 10 秒;日志伴随 `connect_timeout: 连接 cmhub 超时`。T-545 已做「生图并发上限 5 + 独立下载线程池」解决“下载堵住生图槽位”的流水线问题,但**未根治单条下载在并发争抢下变慢**。根因(代码核实):① cmhub 所有 HTTP(`_cmhub_call_once` 的生成/models/balance、`_download_cmhub_image` 的下载)都是**裸 `requests.get`/`requests.request`、无共享 `Session`**,每个任务全新 TCP+TLS,叠加同步生成长连接(单条 60~260s)与 `connect_timeout` 触发的重试,形成连接风暴/连接饥饿;② `requests` 默认 `trust_env=True` 读 `HTTP(S)_PROXY`/`ALL_PROXY`,若系统带慢代理会拖累,而干净窗口的 curl 直连快。方案:① cmhub 所有 HTTP 统一走一个**模块级共享 `requests.Session`**,挂 `HTTPAdapter(pool_connections/pool_maxsize)`,池大小 ≥(实际生图并发 + 下载并发)以免连接不足排队、复用连接减少握手与 connect_timeout;生成与下载共享该 Session(`requests.Session` 跨线程发请求安全,但连接池要够大);② 代理处理——诊断日志脱敏记录 cmhub 请求是否经代理;对 cmhub 请求提供明确策略(可配 `trust_env=False` 或显式 `proxies`),避免误走慢代理,默认可先保持读环境但提供关闭开关;③ 复测口径——Session+连接池到位后单条下载耗时应回落到与 curl 同量级;若在 T-545 的并发上限 5 下仍显著慢,评估把 cmhub 生图默认并发再降(1~2);④ 确认 connect_timeout 重试退避不加剧连接风暴。诊断建议:可先临时把图片并发设 1 复测以区分“并发争抢”与“代理”。边界:只改 `app/ai.py` 的 cmhub HTTP 客户端层(+ 可选 config 代理项)+ 诊断日志 + 相关文档;不改 cmhub 协议、T-545 的并发/下载池语义、生成编排、DB、Excel、CDP/Shopee。测试:`tests/test_ai.py` 覆盖共享 Session 被复用(mock 同一 session 多次调用)、代理配置被尊重、连接池大小设定;现有 cmhub 用例保持绿。落地:`app/ai.py` 加模块级 `_cmhub_session()`(`HTTPAdapter` 池 `CMHUB_HTTP_POOL_SIZE=32`)+ `_apply_cmhub_proxy()`;生成/下载/models/balance 统一走该 Session;`ai.cmhub.use_system_proxy` 默认 `false`(绕过系统代理,公网网关直连),`_cmhub_runtime` 读取并应用、`fetch_cmhub_models/balance` 加同名参数;`test_ai.py` 补 3 项新测试并把原 18 处 `requests` mock 改到共享 Session;243 单测全绿。**待 Windows 实网复测确认下载提速**(troubleshooting 已加「cmhub 图片下载很慢」排障节与代理定位命令) | DONE |
|
||||||
| T-548 | cmhub 图片下载改用系统 curl(Win10+ 自带) | T-546, T-538 | 背景:图片下载慢的另一手段——用 curl(用户的快基准)下载。curl 在 **Windows 10+ 自带**(`C:\Windows\System32\curl.exe`),**无需捆绑**(不用项目里 `wget/` 那个 2022 老 wget,其 OpenSSL 太旧且捆绑二进制易被杀软误报);应用底线本就是 Win10+(PySide6/Qt6 不支持 Win7)。**前置说明**:若 T-546 的代理绕过已让 requests 下载回落到 curl 同量级,本任务可作可选/降级;此处按用户要求落地。方案:① **只改图片下载这一步**(`app/ai.py` `_download_cmhub_image`),cmhub 生成/models/balance 仍走 requests 共享 Session。② 后端选择:新增 config `ai.cmhub.download_with_curl`(`auto`/`true`/`false`,默认 `auto`=Windows 且检测到 curl 用 curl,否则回退 requests);`shutil.which("curl")` 或固定 `System32\curl.exe` 检测;**非 Windows / 无 curl / curl 执行失败 → 自动回退现有 requests 下载**(保证 Linux/Mac/CI/无 curl 环境和现有测试不受影响)。③ **安全防护(硬要求)**:a) SSRF 不可绕过——调 curl 前仍执行 `_assert_public_http_url()` 拦内网/回环 IP,用 `subprocess.run([...], shell=False)` 参数列表、不拼 shell;b) token 不泄漏——`image_url` 带 `?token=...`,直接进命令行会出现在进程列表(tasklist)和日志,改用 curl `-K <临时配置文件>`(把 `url = "..."` 写进临时文件传入、用完删)避免 URL 进程列表暴露,日志一律脱敏、不打印完整 URL/命令;c) 绕过代理——按 `use_system_proxy`:false 时加 `--noproxy "*"`(这才是 curl 快的关键,与 T-546 一致),true 时不加。④ 超时/大小:`--connect-timeout <connect>` + `--max-time <read≈650>`;下载到临时文件、读回字节后**仍复用现有 `_save_jpeg` 重编码**;保留 20MB 上限(`--max-filesize` + 落盘后大小复核)。⑤ 重试:沿用现有 `_download_cmhub_image_with_retry`(3 次),只替换内层下载实现;curl 非零退出=失败、走现有重试语义。⑥ 打包:curl 系统自带、不捆绑、不改 spec;`docs/packaging.md` 注明依赖 Win10+ 自带 curl。测试:`tests/test_ai.py` mock `subprocess.run`——断言 curl 参数含 `--noproxy`/超时/`-K` 临时配置(URL 不在 argv)、SSRF 校验先于 curl、curl 失败回退 requests、非 Windows/无 curl 自动回退;现有下载用例保持绿。边界:只改 `_download_cmhub_image` 下载后端 + config 项 + 诊断脱敏 + 打包文档;不改生成/API 调用、`_save_jpeg`、并发/重试语义、DB、Excel、CDP/Shopee | DONE |
|
| T-548 | cmhub 图片下载改用系统 curl(Win10+ 自带) | T-546, T-538 | 背景:图片下载慢的另一手段——用 curl(用户的快基准)下载。curl 在 **Windows 10+ 自带**(`C:\Windows\System32\curl.exe`),**无需捆绑**(不用项目里 `wget/` 那个 2022 老 wget,其 OpenSSL 太旧且捆绑二进制易被杀软误报);应用底线本就是 Win10+(PySide6/Qt6 不支持 Win7)。**前置说明**:若 T-546 的代理绕过已让 requests 下载回落到 curl 同量级,本任务可作可选/降级;此处按用户要求落地。方案:① **只改图片下载这一步**(`app/ai.py` `_download_cmhub_image`),cmhub 生成/models/balance 仍走 requests 共享 Session。② 后端选择:新增 config `ai.cmhub.download_with_curl`(`auto`/`true`/`false`,默认 `auto`=Windows 且检测到 curl 用 curl,否则回退 requests);`shutil.which("curl")` 或固定 `System32\curl.exe` 检测;**非 Windows / 无 curl / curl 执行失败 → 自动回退现有 requests 下载**(保证 Linux/Mac/CI/无 curl 环境和现有测试不受影响)。③ **安全防护(硬要求)**:a) SSRF 不可绕过——调 curl 前仍执行 `_assert_public_http_url()` 拦内网/回环 IP,用 `subprocess.run([...], shell=False)` 参数列表、不拼 shell;b) token 不泄漏——`image_url` 带 `?token=...`,直接进命令行会出现在进程列表(tasklist)和日志,改用 curl `-K <临时配置文件>`(把 `url = "..."` 写进临时文件传入、用完删)避免 URL 进程列表暴露,日志一律脱敏、不打印完整 URL/命令;c) 绕过代理——按 `use_system_proxy`:false 时加 `--noproxy "*"`(这才是 curl 快的关键,与 T-546 一致),true 时不加。④ 超时/大小:`--connect-timeout <connect>` + `--max-time <read≈650>`;下载到临时文件、读回字节后**仍复用现有 `_save_jpeg` 重编码**;保留 20MB 上限(`--max-filesize` + 落盘后大小复核)。⑤ 重试:沿用现有 `_download_cmhub_image_with_retry`(3 次),只替换内层下载实现;curl 非零退出=失败、走现有重试语义。⑥ 打包:curl 系统自带、不捆绑、不改 spec;`docs/packaging.md` 注明依赖 Win10+ 自带 curl。测试:`tests/test_ai.py` mock `subprocess.run`——断言 curl 参数含 `--noproxy`/超时/`-K` 临时配置(URL 不在 argv)、SSRF 校验先于 curl、curl 失败回退 requests、非 Windows/无 curl 自动回退;现有下载用例保持绿。边界:只改 `_download_cmhub_image` 下载后端 + config 项 + 诊断脱敏 + 打包文档;不改生成/API 调用、`_save_jpeg`、并发/重试语义、DB、Excel、CDP/Shopee | DONE |
|
||||||
| T-549 | ② 标题提示词支持 `{旧标题}` 占位符 | T-302p, T-303 | 需求:② 标题提示词像封面提示词那样支持插入变量,本任务**只做 `{旧标题}`**(不做 `{商品id}/{店铺}/{新标题}`)。现状:标题提示词被原样传给 `gen_title`,由代码在末尾**无条件自动追加**「旧标题:<旧标题>\n\n请只返回新标题,不要解释」;`app/prompts.py` 的 `render_prompt` 已支持 `{旧标题}`(封面在用),标题这边没接。方案:① UI(`app/gui/tabs/generate.py`)——标题提示词区新增「插入旧标题」按钮,在 `title_prompt_edit` 光标处插入 `{旧标题}`(照搬封面「插入标题」的 `insert_*_placeholder` 做法);不加 `{新标题}` 等其它变量按钮。② 组装规则(`app/ai.py`,cmhub 与 direct 两条路径共用一个 helper,如 `_compose_title_prompt(title_prompt, old_title)`):**若标题提示词含 `{旧标题}`** → 用该商品 `old_title` 替换占位符、**不再自动追加**「旧标题:<旧标题>」块(避免旧标题出现两次);**若不含** → **保持现状自动追加**(向后兼容,老 `title_prompt.txt` 行为不变);两种情况都**保留**末尾「请只返回新标题,不要解释」输出格式约束。③ 只处理 `{旧标题}`,其它 `{...}` 原样保留不动。测试:`tests/test_ai.py` 覆盖——含 `{旧标题}` 时替换且旧标题只出现一次、不自动追加;不含时保持原有自动追加;cmhub 与 direct 两路径都验证;`tests/test_gui.py` 断言「插入旧标题」按钮在光标处插入 `{旧标题}`。边界:只改标题提示词组装 + ② UI + 单测;不改封面提示词、`render_prompt`、cmhub HTTP、DB、Excel、CDP/Shopee | TODO |
|
| T-549 | ② 标题提示词支持 `{旧标题}` 占位符 | T-302p, T-303 | 需求:② 标题提示词像封面提示词那样支持插入变量,本任务**只做 `{旧标题}`**(不做 `{商品id}/{店铺}/{新标题}`)。现状:标题提示词被原样传给 `gen_title`,由代码在末尾**无条件自动追加**「旧标题:<旧标题>\n\n请只返回新标题,不要解释」;`app/prompts.py` 的 `render_prompt` 已支持 `{旧标题}`(封面在用),标题这边没接。方案:① UI(`app/gui/tabs/generate.py`)——标题提示词区新增「插入旧标题」按钮,在 `title_prompt_edit` 光标处插入 `{旧标题}`(照搬封面「插入标题」的 `insert_*_placeholder` 做法);不加 `{新标题}` 等其它变量按钮。② 组装规则(`app/ai.py`,cmhub 与 direct 两条路径共用一个 helper,如 `_compose_title_prompt(title_prompt, old_title)`):**若标题提示词含 `{旧标题}`** → 用该商品 `old_title` 替换占位符、**不再自动追加**「旧标题:<旧标题>」块(避免旧标题出现两次);**若不含** → **保持现状自动追加**(向后兼容,老 `title_prompt.txt` 行为不变);两种情况都**保留**末尾「请只返回新标题,不要解释」输出格式约束。③ 只处理 `{旧标题}`,其它 `{...}` 原样保留不动。测试:`tests/test_ai.py` 覆盖——含 `{旧标题}` 时替换且旧标题只出现一次、不自动追加;不含时保持原有自动追加;cmhub 与 direct 两路径都验证;`tests/test_gui.py` 断言「插入旧标题」按钮在光标处插入 `{旧标题}`。边界:只改标题提示词组装 + ② UI + 单测;不改封面提示词、`render_prompt`、cmhub HTTP、DB、Excel、CDP/Shopee | DONE |
|
||||||
|
|
||||||
## Phase 8 · 工程基础设施后续(`docs/engineering-review.md`)
|
## Phase 8 · 工程基础设施后续(`docs/engineering-review.md`)
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -309,6 +309,7 @@ fetch_cmhub_models(base_url, api_key, connect_timeout=10, read_timeout=30) -> li
|
|||||||
|
|
||||||
- `backend=direct`:内部兼容/手工回滚路径;标题用 `default_text_model`、封面用 `default_image_model`(`appconfig.get_model` 取定义,含 url/key/api_type)。
|
- `backend=direct`:内部兼容/手工回滚路径;标题用 `default_text_model`、封面用 `default_image_model`(`appconfig.get_model` 取定义,含 url/key/api_type)。
|
||||||
- `backend=cmhub`:普通产品默认路径;标题调用 `POST /api/v1/generate/title`,封面调用 `POST /api/v1/generate/image`,模型字段使用 `ai.cmhub.title_alias/image_alias`,Key 来自 `data/config/cmhub.json`。
|
- `backend=cmhub`:普通产品默认路径;标题调用 `POST /api/v1/generate/title`,封面调用 `POST /api/v1/generate/image`,模型字段使用 `ai.cmhub.title_alias/image_alias`,Key 来自 `data/config/cmhub.json`。
|
||||||
|
- 标题提示词组装:`gen_title()` 的 direct 与 cmhub 路径共用标题 prompt 规则。若标题提示词包含 `{旧标题}`,生成前替换为该任务旧标题,不再自动追加旧标题块;若不包含 `{旧标题}`,保持旧行为自动追加“旧标题:...”块。两种情况都会追加“请只返回新标题,不要解释。”输出约束;其它 `{...}` 原样保留。
|
||||||
- `fetch_cmhub_models()` 调 `GET /api/v1/models` 返回别名清单,供⑤设置页动态下拉使用;Base URL 会先规整为网关根,HTTP 404 映射为 `not_found` 并提示检查 Base URL 或实例是否部署 `/api/v1/models`。
|
- `fetch_cmhub_models()` 调 `GET /api/v1/models` 返回别名清单,供⑤设置页动态下拉使用;Base URL 会先规整为网关根,HTTP 404 映射为 `not_found` 并提示检查 Base URL 或实例是否部署 `/api/v1/models`。
|
||||||
- `api_type=chat/auto` 走 OpenAI-compatible chat JSON;`api_type=images_edits` 走 multipart form。
|
- `api_type=chat/auto` 走 OpenAI-compatible chat JSON;`api_type=images_edits` 走 multipart form。
|
||||||
- direct 连接超时参考模型 `connect_timeout_seconds`;**返回超时 = 模型 `timeout_seconds` 或 `appconfig.response_timeout()`(随分辨率:512/1k/2k/4k → 180/240/360/600)**。cmhub 使用 `requests timeout=(connect, read)`,connect 来自 `ai.cmhub.connect_timeout`;cmhub 生文读取等待固定 600s,cmhub 生图请求和图片下载读取等待固定 650s,且生图读超时不自动重发。
|
- direct 连接超时参考模型 `connect_timeout_seconds`;**返回超时 = 模型 `timeout_seconds` 或 `appconfig.response_timeout()`(随分辨率:512/1k/2k/4k → 180/240/360/600)**。cmhub 使用 `requests timeout=(connect, read)`,connect 来自 `ai.cmhub.connect_timeout`;cmhub 生文读取等待固定 600s,cmhub 生图请求和图片下载读取等待固定 650s,且生图读超时不自动重发。
|
||||||
@@ -341,7 +342,7 @@ render_prompt(template_text, task) -> str
|
|||||||
|
|
||||||
要点:
|
要点:
|
||||||
|
|
||||||
- 「插入标题」在封面提示词光标处插入 `{新标题}`;「预览」对选中任务调用 `render_prompt` 后展示。
|
- 「插入旧标题」在标题提示词光标处插入 `{旧标题}`;「插入标题」在封面提示词光标处插入 `{新标题}`;「预览」对选中任务调用 `render_prompt` 后展示封面提示词。
|
||||||
- 生成封面时 `gen_cover` 的 prompt = `render_prompt(当前封面模板, task)`。
|
- 生成封面时 `gen_cover` 的 prompt = `render_prompt(当前封面模板, task)`。
|
||||||
- 模板与 `data/title_prompt.txt` 均为可手改的纯文本文件。
|
- 模板与 `data/title_prompt.txt` 均为可手改的纯文本文件。
|
||||||
- `list_cover_templates()` 不会在启动时创建文件;只有保存/新建/另存为才写 `data/prompts/cover/*.txt`。
|
- `list_cover_templates()` 不会在启动时创建文件;只有保存/新建/另存为才写 `data/prompts/cover/*.txt`。
|
||||||
@@ -419,7 +420,7 @@ T-523 后 GUI 已从旧 `app/gui.py` 拆为 `app/gui/` 包:`__init__.py` 负
|
|||||||
② AI生成当前要点(T-302/T-302p/T-303/T-303b + 诊断日志补丁):
|
② AI生成当前要点(T-302/T-302p/T-303/T-303b + 诊断日志补丁):
|
||||||
|
|
||||||
- 左右 `QSplitter`:左侧约 1/4 为标题提示词、封面提示词两个多行输入;右侧为筛选栏 + 任务列表。
|
- 左右 `QSplitter`:左侧约 1/4 为标题提示词、封面提示词两个多行输入;右侧为筛选栏 + 任务列表。
|
||||||
- 标题提示词启动时从 `data/title_prompt.txt` 回显;点击「保存标题提示词」写回该文件。
|
- 标题提示词启动时从 `data/title_prompt.txt` 回显;点击「保存标题提示词」写回该文件;点击「插入旧标题」在光标处插入 `{旧标题}`。标题生成时若提示词含 `{旧标题}` 则替换且不重复追加旧标题,否则保持旧行为自动追加旧标题块。
|
||||||
- 封面提示词模板下拉读取 `data/prompts/cover/*.txt`;支持新建、保存、另存为、重命名、删除。删除由 GUI 二次确认,删空后下拉显示内存态“默认”,不会自动建文件。
|
- 封面提示词模板下拉读取 `data/prompts/cover/*.txt`;支持新建、保存、另存为、重命名、删除。删除由 GUI 二次确认,删空后下拉显示内存态“默认”,不会自动建文件。
|
||||||
- 「插入标题」在封面提示词光标处插入 `{新标题}`;「预览」使用当前选中任务(无选择则用第一条)调用 `prompts.render_prompt()` 并弹窗展示。
|
- 「插入标题」在封面提示词光标处插入 `{新标题}`;「预览」使用当前选中任务(无选择则用第一条)调用 `prompts.render_prompt()` 并弹窗展示。
|
||||||
- 筛选栏包含:批次、店铺、商品ID、状态、刷新。批次来自 `db.list_batches()`;店铺来自当前任务别名并优先显示匹配账号名;商品ID输入框按包含匹配 `item_id`,清空表示全部;状态支持全部/待生成/已生成/失败/略过/已更新。
|
- 筛选栏包含:批次、店铺、商品ID、状态、刷新。批次来自 `db.list_batches()`;店铺来自当前任务别名并优先显示匹配账号名;商品ID输入框按包含匹配 `item_id`,清空表示全部;状态支持全部/待生成/已生成/失败/略过/已更新。
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
```jsonc
|
```jsonc
|
||||||
{ "prompt": <title_prompt + 旧标题合成为单串>, "model": <title_alias>, "resolution": <可选> }
|
{ "prompt": <title_prompt + 旧标题合成为单串>, "model": <title_alias>, "resolution": <可选> }
|
||||||
```
|
```
|
||||||
现在 `gen_title` 用 system+user 两条 message,cmhub 只收单个 `prompt`,需把「提示词 + 旧标题 + 只返回新标题」折叠为一个 `prompt` 字符串。
|
T-549 后 direct 与 cmhub 共用标题 prompt 组装:若标题提示词包含 `{旧标题}`,先替换该占位符且不重复追加旧标题块;若不包含,则保持旧行为自动追加「旧标题:...」。两种情况都把「提示词 + 旧标题/占位符结果 + 只返回新标题」折叠为一个最终 prompt 字符串。
|
||||||
- **响应**:解析 `titles`,取 `titles[0]`(当前一条任务要一个新标题);空列表/空串按现有语义抛 `AIError("AI 返回为空标题")`。
|
- **响应**:解析 `titles`,取 `titles[0]`(当前一条任务要一个新标题);空列表/空串按现有语义抛 `AIError("AI 返回为空标题")`。
|
||||||
- 不复用现有 `_call_with_retry` 的一刀切重试逻辑;cmhub 分支新增专用 HTTP helper,优先使用项目已依赖的 `requests`,传 `timeout=(connect_timeout, read_timeout)` 以区分连接超时和读超时。`model_used`/`points_cost`/`points_balance`/`call_id` 不改变返回值,通过 `on_step`/事件回调上报给 `generate_batch` 与 GUI worker;T-526 只保证 metadata 事件完整传出,不直接要求写 GUI `run_logs`,T-528 再由 GUI worker 脱敏写 `run_logs` 和展示余额;不入 Excel。
|
- 不复用现有 `_call_with_retry` 的一刀切重试逻辑;cmhub 分支新增专用 HTTP helper,优先使用项目已依赖的 `requests`,传 `timeout=(connect_timeout, read_timeout)` 以区分连接超时和读超时。`model_used`/`points_cost`/`points_balance`/`call_id` 不改变返回值,通过 `on_step`/事件回调上报给 `generate_batch` 与 GUI worker;T-526 只保证 metadata 事件完整传出,不直接要求写 GUI `run_logs`,T-528 再由 GUI worker 脱敏写 `run_logs` 和展示余额;不入 Excel。
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+2
-2
@@ -93,9 +93,9 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
- 左侧(提示词管理,上下两块):
|
- 左侧(提示词管理,上下两块):
|
||||||
- **标题提示词**:多行输入 + 「保存」(写 `data/title_prompt.txt`);启动时自动加载回显。
|
- **标题提示词**:多行输入 + 「保存标题提示词」(写 `data/title_prompt.txt`)+「插入旧标题」(插 `{旧标题}`);启动时自动加载回显。若标题提示词包含 `{旧标题}`,生成前替换为当前任务旧标题且不重复追加旧标题块;不包含时保持旧行为自动追加旧标题。
|
||||||
- **封面提示词**:模板下拉(读 `data/prompts/cover/*.txt`)+ 图标工具栏(新建/保存/另存为/重命名/删除)+ 多行输入 + 「插入标题」(插 `{新标题}`)/「预览」(变量替换后查看)。
|
- **封面提示词**:模板下拉(读 `data/prompts/cover/*.txt`)+ 图标工具栏(新建/保存/另存为/重命名/删除)+ 多行输入 + 「插入标题」(插 `{新标题}`)/「预览」(变量替换后查看)。
|
||||||
- 变量:`{旧标题}`/`{新标题}`/`{商品id}`/`{店铺}`,生成前按任务替换。
|
- 变量:标题提示词本阶段只支持 `{旧标题}`;封面提示词支持 `{旧标题}`/`{新标题}`/`{商品id}`/`{店铺}`,生成前按任务替换。
|
||||||
- 右上:按导入批次 / 店铺 / 商品ID / 状态筛选任务;商品ID输入框按包含匹配 `item_id`,清空表示全部。
|
- 右上:按导入批次 / 店铺 / 商品ID / 状态筛选任务;商品ID输入框按包含匹配 `item_id`,清空表示全部。
|
||||||
- 右下:任务列表(店铺名、商品id、旧标题、新标题、状态)+ AI生成运行日志;已生成、未提交线上、非运行中的任务可双击「新标题」列本地微调,写回 `tasks.new_title`,清空 `last_error` 并回到可更新;双击其他列弹窗展示旧封面 | 新封面(纯查看)。
|
- 右下:任务列表(店铺名、商品id、旧标题、新标题、状态)+ AI生成运行日志;已生成、未提交线上、非运行中的任务可双击「新标题」列本地微调,写回 `tasks.new_title`,清空 `last_error` 并回到可更新;双击其他列弹窗展示旧封面 | 新封面(纯查看)。
|
||||||
- 底部**单个「开始生成」+「停止」**,并增加「生成封面图片(成本较高)」复选框:默认不勾选,只生成标题;勾选后才在标题完成后按 `image_concurrency` 生成封面。cmhub 模式会把用户设置的图片并发内部限制到最大 5,并用同样最大 5 的独立下载线程池拉取 `image_url`,不新增用户可见下载并发配置;运行日志显示用户设置并发和实际并发。该开关状态持久化到 `config.json` 的 `ai.generate_cover`,但入口放在②,便于用户在每轮生成前做成本判断。
|
- 底部**单个「开始生成」+「停止」**,并增加「生成封面图片(成本较高)」复选框:默认不勾选,只生成标题;勾选后才在标题完成后按 `image_concurrency` 生成封面。cmhub 模式会把用户设置的图片并发内部限制到最大 5,并用同样最大 5 的独立下载线程池拉取 `image_url`,不新增用户可见下载并发配置;运行日志显示用户设置并发和实际并发。该开关状态持久化到 `config.json` 的 `ai.generate_cover`,但入口放在②,便于用户在每轮生成前做成本判断。
|
||||||
|
|||||||
@@ -1500,3 +1500,11 @@
|
|||||||
- 测试:`tests/test_ai.py` 覆盖 curl 参数、URL 不进 argv、SSRF 校验先于 subprocess、curl 失败回退 requests、无 curl 自动回退 requests;现有 cmhub 测试 helper 显式关闭 curl,避免单测误连真实网络。`tests/test_appconfig.py` 覆盖 `download_with_curl` 默认值和归一。
|
- 测试:`tests/test_ai.py` 覆盖 curl 参数、URL 不进 argv、SSRF 校验先于 subprocess、curl 失败回退 requests、无 curl 自动回退 requests;现有 cmhub 测试 helper 显式关闭 curl,避免单测误连真实网络。`tests/test_appconfig.py` 覆盖 `download_with_curl` 默认值和归一。
|
||||||
- 文档:同步 `docs/06-tasks.md`、`docs/04-architecture.md`、`docs/api.md`、`docs/cmhub-integration-design.md`、`docs/packaging.md` 和 `docs/current-state.md`。
|
- 文档:同步 `docs/06-tasks.md`、`docs/04-architecture.md`、`docs/api.md`、`docs/cmhub-integration-design.md`、`docs/packaging.md` 和 `docs/current-state.md`。
|
||||||
- 验证:`py -3.10 -m unittest discover -s tests -p "test_ai.py"` 通过(38 tests);`py -3.10 -m unittest discover -s tests -p "test_appconfig.py"` 通过(16 tests);`python -m ruff check app tests main.py` 通过;`py -3.10 -m compileall app main.py` 通过;`git diff --check` 通过;`py -3.10 -m unittest discover -s tests` 通过(250 tests)。
|
- 验证:`py -3.10 -m unittest discover -s tests -p "test_ai.py"` 通过(38 tests);`py -3.10 -m unittest discover -s tests -p "test_appconfig.py"` 通过(16 tests);`python -m ruff check app tests main.py` 通过;`py -3.10 -m compileall app main.py` 通过;`git diff --check` 通过;`py -3.10 -m unittest discover -s tests` 通过(250 tests)。
|
||||||
|
|
||||||
|
## 【2026-07-08】T-549 · 标题提示词支持 `{旧标题}` 占位符
|
||||||
|
|
||||||
|
- 需求:②AI生成的标题提示词支持在光标处插入 `{旧标题}`,并在生成标题时按当前任务旧标题替换;不支持 `{商品id}`、`{店铺}`、`{新标题}` 等其它标题变量。
|
||||||
|
- 代码:`app/gui/tabs/generate.py` 在标题提示词区新增「插入旧标题」按钮,点击后把 `{旧标题}` 插入 `title_prompt_edit` 当前光标位置。`app/ai.py` 新增统一标题 prompt 组装 helper,direct 与 cmhub 共享:提示词含 `{旧标题}` 时替换当前旧标题且不重复追加旧标题块;不含时保持旧行为自动追加「旧标题:...」;两种情况都保留「请只返回新标题,不要解释。」输出约束,其它 `{...}` 原样保留。
|
||||||
|
- 测试:`tests/test_ai.py` 覆盖 direct/cmhub 两路径的 `{旧标题}` 替换、旧标题不重复追加、旧行为兼容和其它占位符保留;`tests/test_gui.py` 覆盖「插入旧标题」按钮和光标插入行为。
|
||||||
|
- 文档:`docs/06-tasks.md` 将 T-549 标为 DONE;同步 `docs/04-architecture.md`、`docs/api.md`、`docs/routes.md`、`docs/cmhub-integration-design.md` 和 `docs/current-state.md`。
|
||||||
|
- 验证:`python -m ruff check app tests main.py` 通过;`py -3.10 -m compileall app main.py` 通过;`git diff --check` 通过;`py -3.10 -m unittest discover -s tests` 通过(252 tests)。
|
||||||
|
|||||||
+61
-1
@@ -181,7 +181,40 @@ class AITests(TempDirMixin, unittest.TestCase):
|
|||||||
body = json.loads(calls[-1][0].data.decode("utf-8"))
|
body = json.loads(calls[-1][0].data.decode("utf-8"))
|
||||||
self.assertEqual("text-model", body["model"])
|
self.assertEqual("text-model", body["model"])
|
||||||
self.assertEqual(0, body["temperature"])
|
self.assertEqual(0, body["temperature"])
|
||||||
self.assertNotIn("sk-text-secret", body["messages"][1]["content"])
|
self.assertEqual(1, len(body["messages"]))
|
||||||
|
self.assertIn("优化标题", body["messages"][0]["content"])
|
||||||
|
self.assertIn("旧标题:\n旧标题", body["messages"][0]["content"])
|
||||||
|
self.assertIn("请只返回新标题,不要解释。", body["messages"][0]["content"])
|
||||||
|
self.assertNotIn("sk-text-secret", body["messages"][0]["content"])
|
||||||
|
|
||||||
|
self.assert_removed(temp_dir)
|
||||||
|
|
||||||
|
def test_gen_title_renders_old_title_placeholder_without_duplicate_append(self):
|
||||||
|
with self.make_temp_dir() as temp_dir:
|
||||||
|
models_path = os.path.join(temp_dir, "ai_models.json")
|
||||||
|
self._write_models(models_path)
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_urlopen(request, timeout=None):
|
||||||
|
calls.append(request)
|
||||||
|
return _Response({"choices": [{"message": {"content": "新标题"}}]})
|
||||||
|
|
||||||
|
with mock.patch("app.ai.urllib.request.urlopen", side_effect=fake_urlopen):
|
||||||
|
title = ai.gen_title(
|
||||||
|
"请基于{旧标题}重写标题,保留 {商品id}",
|
||||||
|
"原始标题A",
|
||||||
|
config=self._config(),
|
||||||
|
models_path=models_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual("新标题", title)
|
||||||
|
body = json.loads(calls[-1].data.decode("utf-8"))
|
||||||
|
content = body["messages"][0]["content"]
|
||||||
|
self.assertIn("请基于原始标题A重写标题", content)
|
||||||
|
self.assertEqual(1, content.count("原始标题A"))
|
||||||
|
self.assertNotIn("旧标题:", content)
|
||||||
|
self.assertIn("{商品id}", content)
|
||||||
|
self.assertIn("请只返回新标题,不要解释。", content)
|
||||||
|
|
||||||
self.assert_removed(temp_dir)
|
self.assert_removed(temp_dir)
|
||||||
|
|
||||||
@@ -339,6 +372,33 @@ class AITests(TempDirMixin, unittest.TestCase):
|
|||||||
|
|
||||||
self.assert_removed(temp_dir)
|
self.assert_removed(temp_dir)
|
||||||
|
|
||||||
|
def test_cmhub_gen_title_renders_old_title_placeholder_once(self):
|
||||||
|
with self.make_temp_dir() as temp_dir:
|
||||||
|
cfg, key_path = self._cmhub_config(temp_dir)
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_request(method, url, **kwargs):
|
||||||
|
calls.append((method, url, kwargs))
|
||||||
|
return _RequestsResponse({"titles": ["新标题"]})
|
||||||
|
|
||||||
|
with mock.patch.object(ai._cmhub_session(), "request", side_effect=fake_request):
|
||||||
|
title = ai.gen_title(
|
||||||
|
"请参考 {旧标题} 输出更短标题,店铺变量 {店铺}",
|
||||||
|
"原始标题B",
|
||||||
|
config=cfg,
|
||||||
|
cmhub_config_path=key_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual("新标题", title)
|
||||||
|
prompt = calls[0][2]["json"]["prompt"]
|
||||||
|
self.assertIn("请参考 原始标题B 输出更短标题", prompt)
|
||||||
|
self.assertEqual(1, prompt.count("原始标题B"))
|
||||||
|
self.assertNotIn("旧标题:", prompt)
|
||||||
|
self.assertIn("{店铺}", prompt)
|
||||||
|
self.assertIn("请只返回新标题,不要解释。", prompt)
|
||||||
|
|
||||||
|
self.assert_removed(temp_dir)
|
||||||
|
|
||||||
def test_cmhub_missing_config_raises_clear_error(self):
|
def test_cmhub_missing_config_raises_clear_error(self):
|
||||||
with self.make_temp_dir() as temp_dir:
|
with self.make_temp_dir() as temp_dir:
|
||||||
cfg = self._config()
|
cfg = self._config()
|
||||||
|
|||||||
@@ -1501,6 +1501,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
self.assertEqual("标题提示词", tab.title_prompt_edit.placeholderText())
|
self.assertEqual("标题提示词", tab.title_prompt_edit.placeholderText())
|
||||||
self.assertEqual("封面提示词", tab.cover_prompt_edit.placeholderText())
|
self.assertEqual("封面提示词", tab.cover_prompt_edit.placeholderText())
|
||||||
self.assertEqual("保存标题提示词", tab.save_title_button.text())
|
self.assertEqual("保存标题提示词", tab.save_title_button.text())
|
||||||
|
self.assertEqual("插入旧标题", tab.insert_old_title_button.text())
|
||||||
self.assertEqual("开始生成", tab.generate_button.text())
|
self.assertEqual("开始生成", tab.generate_button.text())
|
||||||
self.assertEqual("停止", tab.stop_generate_button.text())
|
self.assertEqual("停止", tab.stop_generate_button.text())
|
||||||
self.assertEqual("重置生成结果", tab.reset_generate_button.text())
|
self.assertEqual("重置生成结果", tab.reset_generate_button.text())
|
||||||
@@ -1679,6 +1680,12 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
tab.title_prompt_edit.setPlainText("新标题提示词")
|
tab.title_prompt_edit.setPlainText("新标题提示词")
|
||||||
tab.save_title_prompt()
|
tab.save_title_prompt()
|
||||||
self.assertEqual("新标题提示词", prompts.load_title_prompt(title_prompt_path))
|
self.assertEqual("新标题提示词", prompts.load_title_prompt(title_prompt_path))
|
||||||
|
tab.title_prompt_edit.setPlainText("AB")
|
||||||
|
cursor = tab.title_prompt_edit.textCursor()
|
||||||
|
cursor.setPosition(1)
|
||||||
|
tab.title_prompt_edit.setTextCursor(cursor)
|
||||||
|
tab.insert_old_title_placeholder()
|
||||||
|
self.assertEqual("A{旧标题}B", tab.title_prompt_edit.toPlainText())
|
||||||
|
|
||||||
tab.cover_prompt_edit.setPlainText("另存模板 {新标题}")
|
tab.cover_prompt_edit.setPlainText("另存模板 {新标题}")
|
||||||
with mock.patch("app.gui.QInputDialog.getText", return_value=("另存", True)):
|
with mock.patch("app.gui.QInputDialog.getText", return_value=("另存", True)):
|
||||||
|
|||||||
Reference in New Issue
Block a user