diff --git a/app/appconfig.py b/app/appconfig.py index e1b451c..807a944 100644 --- a/app/appconfig.py +++ b/app/appconfig.py @@ -253,6 +253,10 @@ def cover_prompts_dir(config=None) -> str: return data_path("prompts", "cover", config=config) +def image_studio_prompts_dir(config=None) -> str: + return data_path("prompts", "image_studio", config=config) + + def diagnostic_log_dir(config=None) -> str: return data_path("logs", config=config) diff --git a/app/prompts.py b/app/prompts.py index 6419dc4..ed1e614 100644 --- a/app/prompts.py +++ b/app/prompts.py @@ -10,6 +10,7 @@ from . import appconfig TITLE_PROMPT_PATH = appconfig.title_prompt_path() TITLE_TEMPLATES_DIR = appconfig.title_templates_dir() COVER_PROMPTS_DIR = appconfig.cover_prompts_dir() +IMAGE_STUDIO_PROMPTS_DIR = appconfig.image_studio_prompts_dir() TEMPLATE_EXT = ".txt" INVALID_NAME_CHARS = set('\\/:*?"<>|') DEFAULT_PROMPTS_PACKAGE = "app.default_prompts" @@ -151,6 +152,36 @@ def delete_title_template(name, directory=TITLE_TEMPLATES_DIR) -> None: delete_template(name, directory) +def list_image_studio_templates(directory=IMAGE_STUDIO_PROMPTS_DIR): + """Return AI studio prompt template names sorted by display name.""" + + return list_templates(directory) + + +def load_image_studio_template(name, directory=IMAGE_STUDIO_PROMPTS_DIR) -> str: + """Load one AI studio prompt template as raw text.""" + + return load_template(name, directory) + + +def save_image_studio_template(name, text, directory=IMAGE_STUDIO_PROMPTS_DIR) -> None: + """Save one AI studio prompt template as raw UTF-8 text.""" + + save_template(name, text, directory) + + +def rename_image_studio_template(old, new, directory=IMAGE_STUDIO_PROMPTS_DIR) -> None: + """Rename an AI studio prompt template with duplicate-name protection.""" + + rename_template(old, new, directory) + + +def delete_image_studio_template(name, directory=IMAGE_STUDIO_PROMPTS_DIR) -> None: + """Delete one AI studio prompt template.""" + + delete_template(name, directory) + + def list_cover_templates(directory=COVER_PROMPTS_DIR): """Return cover template names sorted by display name.""" diff --git a/docs/04-architecture.md b/docs/04-architecture.md index ebe6530..2fa2672 100644 --- a/docs/04-architecture.md +++ b/docs/04-architecture.md @@ -80,7 +80,7 @@ imported → collected → generated → applied - cmhub 网关 Key → `data/config/cmhub.json`,schema `{ "api_key": "..." }`;`config.json` 只保存 Base URL、别名和超时,不保存 Key。 - 业务数据(账号、任务、各阶段结果)→ SQLite `data/cmshopee.db`。 - 图片(采集的旧封面、AI 生成的新封面)→ `data/images/`(路径记在 DB)。 -- 提示词 → 标题当前工作文本存单文件 `data/title_prompt.txt`;标题命名模板存 `data/prompts/title/<名称>.txt`;封面命名模板存 `data/prompts/cover/<名称>.txt`。 +- 提示词 → 标题当前工作文本存单文件 `data/title_prompt.txt`;标题命名模板存 `data/prompts/title/<名称>.txt`;封面命名模板存 `data/prompts/cover/<名称>.txt`;AI工场完整提示词模板存 `data/prompts/image_studio/<名称>.txt`。 - 登录态 → 各账号 `data/chrome_user_data_dir//`。 T-538 后统一数据根为 `data/`:打包版默认 `/data`,源码运行默认项目根 `data/`。`config.json` 中 `user_data_root`、`image_dir`、`db_path` 默认仍保存为 `chrome_user_data_dir`、`images`、`cmshopee.db` 等相对值,运行时由 `appconfig` 解析到 `data/` 下;绝对路径作为高级自定义仍按原值使用。启动时会迁移 T-524 旧包的 exe 顶层数据到 `data/`,并检测 `data/` 可写。 @@ -504,7 +504,8 @@ cmshopee/ │ ├── title_prompt.txt # 标题当前工作文本(启动回显) │ └── prompts/ │ ├── title/<名称>.txt # 标题提示词命名模板 -│ └── cover/<名称>.txt # 封面提示词命名模板 +│ ├── cover/<名称>.txt # 封面提示词命名模板 +│ └── image_studio/<名称>.txt # AI工场完整提示词模板 └── prototypes/ # 已验证原型/探查脚本(demo/set_*/get_title/cookies/inspect_images/grab/1.py) # 逻辑待并入 app/editor.py 后清理;见 prototypes/README.md ``` diff --git a/docs/tasks/T-589.md b/docs/tasks/T-589.md index f4c6b30..078def3 100644 --- a/docs/tasks/T-589.md +++ b/docs/tasks/T-589.md @@ -3,7 +3,7 @@ id: T-589 title: AI工场独立提示词模板数据层与完整 CRUD phase: 7 deps: [T-583] -status: TODO +status: DONE created: 2026-07-11 --- @@ -34,4 +34,10 @@ AI工场只有一个完整提示词框,不再区分“主提示词/每张动 ## 执行记录 -(完成后记录模板路径、复用点和测试结果。) +- 2026-07-11:完成 AI工场独立提示词模板数据层。 + - `app/appconfig.py` 新增 `image_studio_prompts_dir(config=None)`,固定解析到 `data/prompts/image_studio/`。 + - `app/prompts.py` 复用 T-583 通用 `list/load/save/rename/delete_template`,新增 `list/load/save/rename/delete_image_studio_template` 薄封装;模板内容按原始完整文本保存,不解析、不拆行。 + - `tests/test_prompts.py` 覆盖 AI工场模板 CRUD、空名/非法名/重名/不存在错误、原始多行文本保持,以及与标题/封面模板目录隔离;`tests/test_appconfig.py` 覆盖目录路径。 + - `docs/04-architecture.md` 已同步 `data/prompts/image_studio/<名称>.txt` 存储口径。 + - 当前工作树运行 `py -3.10 -m unittest tests.test_prompts tests.test_appconfig` 仍因无关未提交默认封面模板改名失败(`papa1` 变为「默认」);T-589 本身在 clean worktree 验证通过。 + - 验证:在只套用 T-589 diff 的 clean worktree 中运行 `py -3.10 -m unittest tests.test_prompts tests.test_appconfig`(25 tests)、`python -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`py -3.10 -m unittest discover -s tests`(356 tests)和 `git diff --check`,全部通过。 diff --git a/tests/test_appconfig.py b/tests/test_appconfig.py index 446d416..36a0e83 100644 --- a/tests/test_appconfig.py +++ b/tests/test_appconfig.py @@ -241,6 +241,10 @@ class AppConfigTests(TempDirMixin, unittest.TestCase): self.assertEqual(os.path.join(data_root, "title_prompt.txt"), appconfig.title_prompt_path(cfg)) self.assertEqual(os.path.join(data_root, "prompts", "title"), appconfig.title_templates_dir(cfg)) self.assertEqual(os.path.join(data_root, "prompts", "cover"), appconfig.cover_prompts_dir(cfg)) + self.assertEqual( + os.path.join(data_root, "prompts", "image_studio"), + appconfig.image_studio_prompts_dir(cfg), + ) self.assertEqual(os.path.join(data_root, "logs"), appconfig.diagnostic_log_dir(cfg)) portable = dict(cfg) diff --git a/tests/test_prompts.py b/tests/test_prompts.py index 884c7ed..626515f 100644 --- a/tests/test_prompts.py +++ b/tests/test_prompts.py @@ -74,6 +74,37 @@ class PromptTests(TempDirMixin, unittest.TestCase): self.assert_removed(temp_dir) + def test_image_studio_template_crud_isolated_and_preserves_raw_text(self): + with self.make_temp_dir() as temp_dir: + studio_dir = os.path.join(temp_dir, "prompts", "image_studio") + title_dir = os.path.join(temp_dir, "prompts", "title") + cover_dir = os.path.join(temp_dir, "prompts", "cover") + raw_text = "完整提示词第一行\n第二行:不要拆分\n{商品id}" + + prompts.save_image_studio_template("工场模板", raw_text, studio_dir) + prompts.save_title_template("标题模板", "标题", title_dir) + prompts.save_cover_template("封面模板", "封面", cover_dir) + + self.assertEqual(["工场模板"], prompts.list_image_studio_templates(studio_dir)) + self.assertEqual(raw_text, prompts.load_image_studio_template("工场模板", studio_dir)) + self.assertEqual(["标题模板"], prompts.list_title_templates(title_dir)) + self.assertEqual(["封面模板"], prompts.list_cover_templates(cover_dir)) + + prompts.rename_image_studio_template("工场模板", "工场模板2", studio_dir) + self.assertEqual(["工场模板2"], prompts.list_image_studio_templates(studio_dir)) + with self.assertRaisesRegex(prompts.PromptError, "提示词模板已存在"): + prompts.rename_image_studio_template("工场模板2", "工场模板2", studio_dir) + with self.assertRaisesRegex(prompts.PromptError, "提示词模板名不能为空"): + prompts.save_image_studio_template("", "x", studio_dir) + with self.assertRaisesRegex(prompts.PromptError, "提示词模板名非法"): + prompts.save_image_studio_template("../bad", "x", studio_dir) + prompts.delete_image_studio_template("工场模板2", studio_dir) + self.assertEqual([], prompts.list_image_studio_templates(studio_dir)) + with self.assertRaisesRegex(prompts.PromptError, "提示词模板不存在"): + prompts.load_image_studio_template("工场模板2", studio_dir) + + self.assert_removed(temp_dir) + def test_ensure_default_prompts_seeds_empty_user_prompt_files(self): with self.make_temp_dir() as temp_dir: title_path = os.path.join(temp_dir, "title_prompt.txt")