feat(ai-studio): add prompt template storage
This commit is contained in:
@@ -253,6 +253,10 @@ def cover_prompts_dir(config=None) -> str:
|
|||||||
return data_path("prompts", "cover", config=config)
|
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:
|
def diagnostic_log_dir(config=None) -> str:
|
||||||
return data_path("logs", config=config)
|
return data_path("logs", config=config)
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from . import appconfig
|
|||||||
TITLE_PROMPT_PATH = appconfig.title_prompt_path()
|
TITLE_PROMPT_PATH = appconfig.title_prompt_path()
|
||||||
TITLE_TEMPLATES_DIR = appconfig.title_templates_dir()
|
TITLE_TEMPLATES_DIR = appconfig.title_templates_dir()
|
||||||
COVER_PROMPTS_DIR = appconfig.cover_prompts_dir()
|
COVER_PROMPTS_DIR = appconfig.cover_prompts_dir()
|
||||||
|
IMAGE_STUDIO_PROMPTS_DIR = appconfig.image_studio_prompts_dir()
|
||||||
TEMPLATE_EXT = ".txt"
|
TEMPLATE_EXT = ".txt"
|
||||||
INVALID_NAME_CHARS = set('\\/:*?"<>|')
|
INVALID_NAME_CHARS = set('\\/:*?"<>|')
|
||||||
DEFAULT_PROMPTS_PACKAGE = "app.default_prompts"
|
DEFAULT_PROMPTS_PACKAGE = "app.default_prompts"
|
||||||
@@ -151,6 +152,36 @@ def delete_title_template(name, directory=TITLE_TEMPLATES_DIR) -> None:
|
|||||||
delete_template(name, directory)
|
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):
|
def list_cover_templates(directory=COVER_PROMPTS_DIR):
|
||||||
"""Return cover template names sorted by display name."""
|
"""Return cover template names sorted by display name."""
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ imported → collected → generated → applied
|
|||||||
- cmhub 网关 Key → `data/config/cmhub.json`,schema `{ "api_key": "..." }`;`config.json` 只保存 Base URL、别名和超时,不保存 Key。
|
- cmhub 网关 Key → `data/config/cmhub.json`,schema `{ "api_key": "..." }`;`config.json` 只保存 Base URL、别名和超时,不保存 Key。
|
||||||
- 业务数据(账号、任务、各阶段结果)→ SQLite `data/cmshopee.db`。
|
- 业务数据(账号、任务、各阶段结果)→ SQLite `data/cmshopee.db`。
|
||||||
- 图片(采集的旧封面、AI 生成的新封面)→ `data/images/`(路径记在 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/<slug>/`。
|
- 登录态 → 各账号 `data/chrome_user_data_dir/<slug>/`。
|
||||||
|
|
||||||
T-538 后统一数据根为 `data/`:打包版默认 `<exe目录>/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/` 可写。
|
T-538 后统一数据根为 `data/`:打包版默认 `<exe目录>/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 # 标题当前工作文本(启动回显)
|
│ ├── title_prompt.txt # 标题当前工作文本(启动回显)
|
||||||
│ └── prompts/
|
│ └── prompts/
|
||||||
│ ├── title/<名称>.txt # 标题提示词命名模板
|
│ ├── title/<名称>.txt # 标题提示词命名模板
|
||||||
│ └── cover/<名称>.txt # 封面提示词命名模板
|
│ ├── cover/<名称>.txt # 封面提示词命名模板
|
||||||
|
│ └── image_studio/<名称>.txt # AI工场完整提示词模板
|
||||||
└── prototypes/ # 已验证原型/探查脚本(demo/set_*/get_title/cookies/inspect_images/grab/1.py)
|
└── prototypes/ # 已验证原型/探查脚本(demo/set_*/get_title/cookies/inspect_images/grab/1.py)
|
||||||
# 逻辑待并入 app/editor.py 后清理;见 prototypes/README.md
|
# 逻辑待并入 app/editor.py 后清理;见 prototypes/README.md
|
||||||
```
|
```
|
||||||
|
|||||||
+8
-2
@@ -3,7 +3,7 @@ id: T-589
|
|||||||
title: AI工场独立提示词模板数据层与完整 CRUD
|
title: AI工场独立提示词模板数据层与完整 CRUD
|
||||||
phase: 7
|
phase: 7
|
||||||
deps: [T-583]
|
deps: [T-583]
|
||||||
status: TODO
|
status: DONE
|
||||||
created: 2026-07-11
|
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`,全部通过。
|
||||||
|
|||||||
@@ -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, "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", "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", "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))
|
self.assertEqual(os.path.join(data_root, "logs"), appconfig.diagnostic_log_dir(cfg))
|
||||||
|
|
||||||
portable = dict(cfg)
|
portable = dict(cfg)
|
||||||
|
|||||||
@@ -74,6 +74,37 @@ class PromptTests(TempDirMixin, unittest.TestCase):
|
|||||||
|
|
||||||
self.assert_removed(temp_dir)
|
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):
|
def test_ensure_default_prompts_seeds_empty_user_prompt_files(self):
|
||||||
with self.make_temp_dir() as temp_dir:
|
with self.make_temp_dir() as temp_dir:
|
||||||
title_path = os.path.join(temp_dir, "title_prompt.txt")
|
title_path = os.path.join(temp_dir, "title_prompt.txt")
|
||||||
|
|||||||
Reference in New Issue
Block a user