feat: store packaged user data under data directory
This commit is contained in:
+35
-32
@@ -6,7 +6,7 @@
|
||||
|
||||
- 形态:本地函数 + 子进程(Chrome)+ CDP(`127.0.0.1:<port>`)+ SQLite + openpyxl + AI 服务调用。
|
||||
- 编码:UTF-8;传 Chrome / `setFileInputFiles` 的路径为 **Windows 绝对路径**。
|
||||
- 凭证:登录态在 user-data-dir;密码、AI Key 本地明文存于 config/DB;`config.json`、`config/ai_models.json`、`config/cmhub.json`、`cmshopee.db`、`chrome_user_data_dir/`、`images/` 必须 gitignore;UI 打码显示,不出现在日志/导出。
|
||||
- 凭证:登录态在 user-data-dir;密码、AI Key 本地明文存于 config/DB;T-538 后默认落在 `data/config.json`、`data/config/ai_models.json`、`data/config/cmhub.json`、`data/cmshopee.db`、`data/chrome_user_data_dir/`、`data/images/`,`data/` 必须 gitignore;UI 打码显示,不出现在日志/导出。
|
||||
- 失败处理:抛带中文说明的异常或返回状态字段;GUI 负责提示,不静默吞错。
|
||||
|
||||
## appconfig 模块(`app/appconfig.py`,已建)
|
||||
@@ -16,13 +16,16 @@
|
||||
```python
|
||||
class ConfigError(RuntimeError): ...
|
||||
default_config() -> dict
|
||||
load_config(path="config.json") -> dict # 不存在则写默认
|
||||
save_config(config, path="config.json") -> dict
|
||||
update_config(updates, path="config.json") -> dict
|
||||
load_config(path="data/config.json") -> dict # 不存在则写默认;返回运行时 data_dir/config_path/ai_models_path/cmhub_config_path
|
||||
save_config(config, path="data/config.json") -> dict
|
||||
update_config(updates, path="data/config.json") -> dict
|
||||
chrome_path(config=None) -> str
|
||||
user_data_root(config=None) -> str
|
||||
image_dir(config=None) -> str
|
||||
db_path(config=None) -> str
|
||||
data_dir(config=None) -> str
|
||||
resolve_data_path(path, config=None) -> str
|
||||
prepare_data_dir(...) -> str # 启动时迁移旧布局并检测 data_dir 可写
|
||||
default_debug_port(config=None) -> int
|
||||
debug_port_range(config=None) -> tuple # (start, end)
|
||||
cdp_ready_timeout(config=None) -> int
|
||||
@@ -36,7 +39,7 @@ cmhub_request_url(base_url, endpoint) -> str # 先规整 base_url,再拼 /ap
|
||||
response_timeout(config=None) -> int # = resolution_timeouts[resolution](返回超时,随分辨率)
|
||||
```
|
||||
|
||||
`default_config()` / `load_config()` 包含 `shopee_update` 安全配置段:历史/调试兼容测试商品 ID、是否允许真实提交、是否允许更新封面、每批最大更新条数、成功后是否关闭本轮新开编辑页、内部兼容 `dry_run`、多账号并行、最大并行账号数。普通正式更新不再用测试商品 ID 阻断当前筛选结果。`config.json` 不保存 AI Key;写入 `api_key` / `*_key` / `token` / `*_token` / `password` / `*_password` 等敏感字段时抛 `ConfigError`。普通产品默认 cmhub,AI Key 存 `config/cmhub.json`;`config/ai_models.json` 仅为 direct 内部兼容路径。
|
||||
`default_config()` / `load_config()` 包含 `shopee_update` 安全配置段:历史/调试兼容测试商品 ID、是否允许真实提交、是否允许更新封面、每批最大更新条数、成功后是否关闭本轮新开编辑页、内部兼容 `dry_run`、多账号并行、最大并行账号数。普通正式更新不再用测试商品 ID 阻断当前筛选结果。`config.json` 不保存 AI Key;写入 `api_key` / `*_key` / `token` / `*_token` / `password` / `*_password` 等敏感字段时抛 `ConfigError`。普通产品默认 cmhub,AI Key 存 `data/config/cmhub.json`;`data/config/ai_models.json` 仅为 direct 内部兼容路径。T-538 后,配置中默认仍保存 `chrome_user_data_dir`、`images`、`cmshopee.db` 等相对值,运行时解析到 `data/` 下,保持免安装目录可移动。
|
||||
|
||||
敏感信息展示/日志辅助:
|
||||
|
||||
@@ -47,22 +50,22 @@ redact_secrets(text, secret_values=None) -> str # 用已知明文值替换自
|
||||
```
|
||||
|
||||
|
||||
cmhub Key 文件(`config/cmhub.json`,含本地明文密钥,T-526 已建;UI 由 T-527 接入):
|
||||
cmhub Key 文件(`data/config/cmhub.json`,含本地明文密钥,T-526 已建;UI 由 T-527 接入):
|
||||
|
||||
```python
|
||||
default_cmhub_config() -> dict
|
||||
load_cmhub_config(path="config/cmhub.json") -> dict # 缺文件返回空 key;默认 cmhub 但生成时会提示补配置
|
||||
save_cmhub_config(config, path="config/cmhub.json") -> dict
|
||||
get_cmhub_api_key(path="config/cmhub.json", masked=False) -> str
|
||||
load_cmhub_config(path="data/config/cmhub.json") -> dict # 缺文件返回空 key;默认 cmhub 但生成时会提示补配置
|
||||
save_cmhub_config(config, path="data/config/cmhub.json") -> dict
|
||||
get_cmhub_api_key(path="data/config/cmhub.json", masked=False) -> str
|
||||
```
|
||||
|
||||
`config.json` 只保存 `ai.backend`、`ai.cmhub.base_url/title_alias/image_alias/connect_timeout` 等非密钥配置;T-529 后普通设置页固定保存 `ai.backend=cmhub`,不暴露后端切换;`config/cmhub.json` 必须 gitignore,展示时打码,不写日志/导出。
|
||||
AI 模型清单(`config/ai_models.json`,含本地明文密钥,已建;UI 由 ⑤ 设置复用):
|
||||
`data/config.json` 只保存 `ai.backend`、`ai.cmhub.base_url/title_alias/image_alias/connect_timeout` 等非密钥配置;T-529 后普通设置页固定保存 `ai.backend=cmhub`,不暴露后端切换;`data/config/cmhub.json` 必须 gitignore,展示时打码,不写日志/导出。
|
||||
AI 模型清单(`data/config/ai_models.json`,含本地明文密钥,已建;UI 由 ⑤ 设置复用):
|
||||
|
||||
```python
|
||||
default_ai_models_config() -> dict
|
||||
load_ai_models_config(path="config/ai_models.json") -> dict # 不存在则写默认,至少 text/image 各一个
|
||||
save_ai_models_config(config, path="config/ai_models.json") -> dict
|
||||
load_ai_models_config(path="data/config/ai_models.json") -> dict # 不存在则写默认,至少 text/image 各一个
|
||||
save_ai_models_config(config, path="data/config/ai_models.json") -> dict
|
||||
list_ai_models(category=None) -> list[dict] # category=text/image 过滤;默认 api_key 打码,含 api_key_set
|
||||
add_ai_model(model) -> None # name 唯一校验
|
||||
update_ai_model(name, **fields) -> None
|
||||
@@ -286,10 +289,10 @@ apply_task(account, task, close_success_tab=False) -> dict
|
||||
```python
|
||||
class AIError(RuntimeError): ...
|
||||
|
||||
gen_title(title_prompt, old_title, retry=None, config=None, models_path="config/ai_models.json", on_step=None, on_event=None, cmhub_config_path="config/cmhub.json") -> str
|
||||
gen_title(title_prompt, old_title, retry=None, config=None, models_path="data/config/ai_models.json", on_step=None, on_event=None, cmhub_config_path="data/config/cmhub.json") -> str
|
||||
# 文本生成:按 backend 分流;direct 走 chat JSON,cmhub 走 /generate/title;提示词 + 旧标题 → 新标题
|
||||
|
||||
gen_cover(cover_prompt, old_cover_path, out_path, resolution=None, jpg_quality=None, retry=None, config=None, models_path="config/ai_models.json", on_step=None, on_event=None, cmhub_config_path="config/cmhub.json") -> str
|
||||
gen_cover(cover_prompt, old_cover_path, out_path, resolution=None, jpg_quality=None, retry=None, config=None, models_path="data/config/ai_models.json", on_step=None, on_event=None, cmhub_config_path="data/config/cmhub.json") -> str
|
||||
# 图像生成(image-to-image):按 backend 分流;direct 走 chat/images_edits,cmhub 走 /generate/image;
|
||||
# 支持返回 url / data URL / b64_json,按 resolution resize 并以 jpg_quality 保存 JPEG,返回路径;新生成默认写入 `image_dir/<batch_id>/<slug>/<task_id>_<item_id>_new.jpg`,历史 DB 已存路径继续按原路径读取
|
||||
|
||||
@@ -305,7 +308,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=cmhub`:普通产品默认路径;标题调用 `POST /api/v1/generate/title`,封面调用 `POST /api/v1/generate/image`,模型字段使用 `ai.cmhub.title_alias/image_alias`,Key 来自 `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`。
|
||||
- `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。
|
||||
- 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`,read 随分辨率且封顶 600s。
|
||||
@@ -320,15 +323,15 @@ fetch_cmhub_models(base_url, api_key, connect_timeout=10, read_timeout=30) -> li
|
||||
class PromptError(RuntimeError): ...
|
||||
|
||||
# 标题提示词:单文件
|
||||
load_title_prompt(path="title_prompt.txt") -> str # 启动回显;缺失返回 ""
|
||||
save_title_prompt(text, path="title_prompt.txt") -> None # 「保存」按钮
|
||||
load_title_prompt(path="data/title_prompt.txt") -> str # 启动回显;缺失返回 ""
|
||||
save_title_prompt(text, path="data/title_prompt.txt") -> None # 「保存」按钮
|
||||
|
||||
# 封面提示词:多模板(prompts/cover/<名称>.txt)
|
||||
list_cover_templates(directory="prompts/cover") -> list[str] # 模板名列表(下拉用)
|
||||
load_cover_template(name, directory="prompts/cover") -> str
|
||||
save_cover_template(name, text, directory="prompts/cover") -> None
|
||||
rename_cover_template(old, new, directory="prompts/cover") -> None
|
||||
delete_cover_template(name, directory="prompts/cover") -> None
|
||||
# 封面提示词:多模板(data/prompts/cover/<名称>.txt)
|
||||
list_cover_templates(directory="data/prompts/cover") -> list[str] # 模板名列表(下拉用)
|
||||
load_cover_template(name, directory="data/prompts/cover") -> str
|
||||
save_cover_template(name, text, directory="data/prompts/cover") -> None
|
||||
rename_cover_template(old, new, directory="data/prompts/cover") -> None
|
||||
delete_cover_template(name, directory="data/prompts/cover") -> None
|
||||
|
||||
# 变量替换
|
||||
render_prompt(template_text, task) -> str
|
||||
@@ -339,8 +342,8 @@ render_prompt(template_text, task) -> str
|
||||
|
||||
- 「插入标题」在封面提示词光标处插入 `{新标题}`;「预览」对选中任务调用 `render_prompt` 后展示。
|
||||
- 生成封面时 `gen_cover` 的 prompt = `render_prompt(当前封面模板, task)`。
|
||||
- 模板与 `title_prompt.txt` 均为可手改的纯文本文件。
|
||||
- `list_cover_templates()` 不会在启动时创建文件;只有保存/新建/另存为才写 `prompts/cover/*.txt`。
|
||||
- 模板与 `data/title_prompt.txt` 均为可手改的纯文本文件。
|
||||
- `list_cover_templates()` 不会在启动时创建文件;只有保存/新建/另存为才写 `data/prompts/cover/*.txt`。
|
||||
- 模板名不可为空,不允许路径分隔符、`..` 或 Windows 非法文件名字符;重命名时目标重名会报错。
|
||||
|
||||
## gui 模块(`app/gui/` 包,已建,PySide6)
|
||||
@@ -381,13 +384,13 @@ T-523 后 GUI 已从旧 `app/gui.py` 拆为 `app/gui/` 包:`__init__.py` 负
|
||||
|
||||
⑤ 设置当前要点(T-501):
|
||||
|
||||
- `SettingsTab` 使用居中内容区 + 适度左右留白布局,当前留白已从 T-506 初始实现缩短到约 40%;实现上使用最大内容宽度和自适应 margin,避免固定像素导致小屏挤压。各设置组默认响应式 3 列表单:短字段占 1 格,长字段(URL/API Key/路径)跨 2 格或 3 格,窄窗口降为 2 列/1 列。点击「保存设置」成功后,调用 `QMessageBox.information` 弹出“设置已保存”轻量提示框,同时保留状态栏提示。T-531 已完成:`save_app_settings()` 返回 bool,成功写 `config.json` + `config/cmhub.json` 后清 dirty,失败保留 dirty 并让调用方阻止离开。
|
||||
- `SettingsTab` 的 cmhub 网关配置:Base URL 保存/刷新前规整为网关根;API Key 单独读写 `config/cmhub.json`;别名下拉来自 `fetch_cmhub_models()`,按 `operation_type` 分生文/生图并过滤未计价别名;「测试连接/查余额」调用 cmhub models + balance helper;保存设置固定写 `ai.backend=cmhub`。`refresh_cmhub_models()` / `test_cmhub_connection()` 使用输入框实时值但不得自动保存,成功文案提醒用户保存。`is_dirty()` / `discard_unsaved_changes()` / `_suspend_dirty`(或等价机制)用于 T-531:用户编辑置脏,程序化回填不置脏,放弃时重新加载 `config.json` + `config/cmhub.json` 并回填控件。
|
||||
- `SettingsTab` 使用居中内容区 + 适度左右留白布局,当前留白已从 T-506 初始实现缩短到约 40%;实现上使用最大内容宽度和自适应 margin,避免固定像素导致小屏挤压。各设置组默认响应式 3 列表单:短字段占 1 格,长字段(URL/API Key/路径)跨 2 格或 3 格,窄窗口降为 2 列/1 列。点击「保存设置」成功后,调用 `QMessageBox.information` 弹出“设置已保存”轻量提示框,同时保留状态栏提示。T-531 已完成:`save_app_settings()` 返回 bool,成功写 `data/config.json` + `data/config/cmhub.json` 后清 dirty,失败保留 dirty 并让调用方阻止离开。
|
||||
- `SettingsTab` 的 cmhub 网关配置:Base URL 保存/刷新前规整为网关根;API Key 单独读写 `data/config/cmhub.json`;别名下拉来自 `fetch_cmhub_models()`,按 `operation_type` 分生文/生图并过滤未计价别名;「测试连接/查余额」调用 cmhub models + balance helper;保存设置固定写 `ai.backend=cmhub`。`refresh_cmhub_models()` / `test_cmhub_connection()` 使用输入框实时值但不得自动保存,成功文案提醒用户保存。`is_dirty()` / `discard_unsaved_changes()` / `_suspend_dirty`(或等价机制)用于 T-531:用户编辑置脏,程序化回填不置脏,放弃时重新加载 `data/config.json` + `data/config/cmhub.json` 并回填控件。
|
||||
- T-532 要求 `SettingsTab._on_cmhub_finished()` 从 worker payload 的 `balance` / user/account 字段提取 cmhub 账号身份,成功文案优先显示 `cmhub 账号「<账号名>」连接成功:...`;当前 `/balance` 结构兼容 `{ "user": "cmhub_user", "points_balance": 88, "account": { "username": "cmhub_user", "display_name": "主账号" } }`,显示名优先 `account.display_name`,再兜底 `account.username` / `user` / 顶层常见字段;账号字段缺失时保持 `cmhub 连接成功:...`。显示名必须脱敏处理邮箱,且不得把 API Key、token 或完整敏感响应写入 GUI、run log 或诊断日志。
|
||||
- `MainWindow` 已负责⑤设置页离开守卫:切 Tab 与 `closeEvent` 发现 `SettingsTab.is_dirty()` 时弹保存/放弃/取消;保存成功后继续,保存失败或取消时回到⑤。由于 `QTabWidget.currentChanged` 是切换后信号,需维护上一个 index,并用 `_reverting_tab_change` 或等价 guard 防止 `setCurrentIndex()` 递归。
|
||||
- 模型详情字段按 3 个组件一组排列:启用、类别、api_type、连接超时等短字段一格;服务商名、模型 ID 视宽度占一格或两格;网址、密钥跨整行或跨 2/3 列。
|
||||
- 模型数据读写复用 `appconfig.list_ai_models(..., reveal_api_key=True)`、`add_ai_model()`、`update_ai_model()`、`delete_ai_model()`;保存时保留现有 `extra_body` 与 `timeout_seconds`。
|
||||
- 密钥字段使用 `QLineEdit.Password` 打码显示;首次保存/变更非空 Key 前弹窗提示“本地明文保存”;明文只写入已 gitignore 的 `config/ai_models.json`,不得进入日志/导出。
|
||||
- 密钥字段使用 `QLineEdit.Password` 打码显示;首次保存/变更非空 Key 前弹窗提示“本地明文保存”;明文只写入已 gitignore 的 `data/config/ai_models.json`,不得进入日志/导出。
|
||||
- 删除按钮在当前类别只剩 1 个模型时禁用;后端仍以“至少启用一个 text 和 image 模型”为硬约束。
|
||||
- 「测试连接」创建 `AIModelTestWorker` 后台调用 `appconfig.test_ai_model()`,GUI 主线程不直接发网络请求。
|
||||
- 角色与生成参数读写 `config.json`,并按 3 个组件一组排列:标题大模型(仅 text)、图片大模型(仅 image)、标题/图片并发、失败重试、分辨率、jpg 质量。
|
||||
@@ -415,8 +418,8 @@ T-523 后 GUI 已从旧 `app/gui.py` 拆为 `app/gui/` 包:`__init__.py` 负
|
||||
② AI生成当前要点(T-302/T-302p/T-303/T-303b + 诊断日志补丁):
|
||||
|
||||
- 左右 `QSplitter`:左侧约 1/4 为标题提示词、封面提示词两个多行输入;右侧为筛选栏 + 任务列表。
|
||||
- 标题提示词启动时从 `title_prompt.txt` 回显;点击「保存标题提示词」写回该文件。
|
||||
- 封面提示词模板下拉读取 `prompts/cover/*.txt`;支持新建、保存、另存为、重命名、删除。删除由 GUI 二次确认,删空后下拉显示内存态“默认”,不会自动建文件。
|
||||
- 标题提示词启动时从 `data/title_prompt.txt` 回显;点击「保存标题提示词」写回该文件。
|
||||
- 封面提示词模板下拉读取 `data/prompts/cover/*.txt`;支持新建、保存、另存为、重命名、删除。删除由 GUI 二次确认,删空后下拉显示内存态“默认”,不会自动建文件。
|
||||
- 「插入标题」在封面提示词光标处插入 `{新标题}`;「预览」使用当前选中任务(无选择则用第一条)调用 `prompts.render_prompt()` 并弹窗展示。
|
||||
- 筛选栏包含:批次、店铺、商品ID、状态、刷新。批次来自 `db.list_batches()`;店铺来自当前任务别名并优先显示匹配账号名;商品ID输入框按包含匹配 `item_id`,清空表示全部;状态支持全部/待生成/已生成/失败/略过/已更新。
|
||||
- 任务列表使用 `QTableView + GenerateTaskTableModel`,列为:店铺、商品ID、旧标题、新标题、状态。`stage=collected` 显示“待生成”,`stage=generated` 显示“已生成”,`status=failed/skipped/running` 优先显示对应状态;已生成、未提交线上、非运行中的「新标题」列可双击编辑,调用 `db.update_generated_title()` 写回本地并清空 `last_error`。
|
||||
@@ -440,7 +443,7 @@ T-523 后 GUI 已从旧 `app/gui.py` 拆为 `app/gui/` 包:`__init__.py` 负
|
||||
- 检查本轮更新:不做账号登录预检,不调用 `editor.apply_task()`,不写任务状态,不回写 Excel;只把每条“将更新/将略过”写入运行日志并弹汇总。
|
||||
- 真实更新前先做账号就绪预检:无账号、当前筛选结果匹配账号 Chrome 未启动、CDP 端口不可访问、未登录,或本轮涉及账号调试端口冲突时,返回 `blocked=True`,GUI 弹窗汇总并跳转/引导去④账号管理;预检不通过时不调用 `editor.apply_task()`、不写失败状态、不自动启动 Chrome。
|
||||
- 预检通过后默认串行;若 `parallel_accounts=true` 且 `max_parallel_accounts>1`,按账号分组并行执行,不同账号可同时跑,同一账号内仍串行。每条执行 `db.mark_running(..., "apply")` → `editor.apply_task(account, task, close_success_tab=设置值)` → `db.set_applied()`;成功推进 `stage=applied/status=success/committed=1`,失败保持原 stage、`status=failed/committed=0/last_error`,单条失败继续下一条。
|
||||
- 真实更新打开商品页失败时,`ApplyWorker` 应把 `open_product()` 捕获到的 Shopee toast 文案上浮到③可见运行日志、`run_log_events` 和任务失败原因;若失败发生在 `open_product()` 内部,本轮自动新建 tab 要关闭,复用用户已有 tab 不关闭;本地 `logs/` 可保存失败现场 HTML/toast JSON 片段供开发排查,但必须脱敏。
|
||||
- 真实更新打开商品页失败时,`ApplyWorker` 应把 `open_product()` 捕获到的 Shopee toast 文案上浮到③可见运行日志、`run_log_events` 和任务失败原因;若失败发生在 `open_product()` 内部,本轮自动新建 tab 要关闭,复用用户已有 tab 不关闭;本地 `data/logs/` 可保存失败现场 HTML/toast JSON 片段供开发排查,但必须脱敏。
|
||||
|
||||
- 检查和真实更新都会创建 `run_logs`,并把逐条事件写入 `run_log_events`;点击「检查本轮更新」或「开始更新」时先清空 `ApplyTab` 可见日志文本并写入本轮开始摘要,后续只追加本轮日志;③ 页面不自动把上一轮历史日志混入当前运行界面。
|
||||
- 若 `close_success_tab=true`,`editor.apply_task()` 只关闭本轮自动新开且成功提交的商品页;确认后跳回商品列表页时,关闭前等待 2 秒;进入编辑页后的失败和复用的用户已有 tab 不关闭。`open_product()` 内部打开失败的新建 tab 由 `open_product()` 自行关闭。
|
||||
|
||||
Reference in New Issue
Block a user