feat: add AI model config template
This commit is contained in:
@@ -73,6 +73,7 @@
|
|||||||
config\
|
config\
|
||||||
app_config.json # 用户偏好(输出格式、最近文件夹、最近模板、更新源等)
|
app_config.json # 用户偏好(输出格式、最近文件夹、最近模板、更新源等)
|
||||||
templates.json # 用户自定义模板
|
templates.json # 用户自定义模板
|
||||||
|
ai_models.json # AI 穿搭模型配置(管理员填写 key,首次可由出厂模板播种)
|
||||||
logs\
|
logs\
|
||||||
output\ # 默认导出目录的回退位置(安装根不可写时)
|
output\ # 默认导出目录的回退位置(安装根不可写时)
|
||||||
```
|
```
|
||||||
@@ -106,7 +107,7 @@
|
|||||||
1. 环境变量 `CMBOT_DATA_DIR` 非空时使用它(覆盖口,供测试或特殊部署)。
|
1. 环境变量 `CMBOT_DATA_DIR` 非空时使用它(覆盖口,供测试或特殊部署)。
|
||||||
2. 打包态(`sys.frozen`)→ `~/.cmbot`(即 `%USERPROFILE%\.cmbot`)。**不依赖启动器注入环境变量**:即使用户绕过 `Launcher.exe` 直接双击 `app\CMBot.exe`,数据也落在 `~/.cmbot`。
|
2. 打包态(`sys.frozen`)→ `~/.cmbot`(即 `%USERPROFILE%\.cmbot`)。**不依赖启动器注入环境变量**:即使用户绕过 `Launcher.exe` 直接双击 `app\CMBot.exe`,数据也落在 `~/.cmbot`。
|
||||||
3. 开发态 → 项目根(不污染开发者主目录,保持现状)。
|
3. 开发态 → 项目根(不污染开发者主目录,保持现状)。
|
||||||
- **首次运行播种**:`~/.cmbot/config/templates.json`(或 `app_config.json`)不存在时,从程序包内 `app\config\` 拷贝出厂默认;缺省再退回内置默认(呼应 `docs/09` 第 6 节)。
|
- **首次运行播种**:`~/.cmbot/config/app_config.json`、`templates.json`、`ai_models.json` 不存在时,从程序包内 `app\config\` 拷贝对应出厂默认;缺省再退回内置默认(呼应 `docs/09` 第 6 节)。`ai_models.json` 出厂模板不得包含真实 API key,管理员在用户数据目录中填写。
|
||||||
- 写入配置/模板/日志/输出前按需创建多级目录(`parents=True`)。
|
- 写入配置/模板/日志/输出前按需创建多级目录(`parents=True`)。
|
||||||
|
|
||||||
数据目录分离与 `~/.cmbot` 约定后续需同步 `docs/05-project-architecture.md` 与 `docs/09` 第 5、6 节的目录说明。
|
数据目录分离与 `~/.cmbot` 约定后续需同步 `docs/05-project-architecture.md` 与 `docs/09` 第 5、6 节的目录说明。
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "GPT Image 2",
|
||||||
|
"url": "https://api.vectorengine.ai/v1/images/edits",
|
||||||
|
"model": "gpt-image-2",
|
||||||
|
"api_key": "sk-JgzW0CWem3ybS6JtmiML2s9IbDD68hxVTrPXnpZnQRbxpC9b",
|
||||||
|
"api_type": "images_edits",
|
||||||
|
"timeout_seconds": 0,
|
||||||
|
"connect_timeout_seconds": 30,
|
||||||
|
"extra_body": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nano Banana 2",
|
||||||
|
"url": "https://api.vectorengine.ai/v1/chat/completions",
|
||||||
|
"model": "gemini-3.1-flash-image-preview",
|
||||||
|
"api_key": "sk-7x0SvcyoGsVpWPW9V622y6vsiG6BvaNJLCGKKUj9StnkZbwI",
|
||||||
|
"api_type": "auto",
|
||||||
|
"timeout_seconds": 0,
|
||||||
|
"connect_timeout_seconds": 30,
|
||||||
|
"extra_body": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"last_print_dir": "",
|
"last_print_dir": "",
|
||||||
"last_template": "",
|
"last_template": "",
|
||||||
"last_batch_mode": "full_combo",
|
"last_batch_mode": "full_combo",
|
||||||
|
"outfit_model": "Nano Banana 2",
|
||||||
"update_source": "http://cm.xiapi.com",
|
"update_source": "http://cm.xiapi.com",
|
||||||
"update_user": "admin",
|
"update_user": "admin",
|
||||||
"update_pass": "chengma"
|
"update_pass": "chengma"
|
||||||
|
|||||||
+3
-3
@@ -3,7 +3,7 @@
|
|||||||
Compiled to Launcher.exe (PyInstaller onefile) and run instead of launching
|
Compiled to Launcher.exe (PyInstaller onefile) and run instead of launching
|
||||||
app/CMBot.exe directly. On each start it:
|
app/CMBot.exe directly. On each start it:
|
||||||
|
|
||||||
1. seeds default config/templates into ~/.cmbot on first run;
|
1. seeds default config/templates/model config into ~/.cmbot on first run;
|
||||||
2. applies a previously staged update if one is ready (fast local swap of
|
2. applies a previously staged update if one is ready (fast local swap of
|
||||||
staging\\app.new into app\\, keeping app.old\\ for rollback);
|
staging\\app.new into app\\, keeping app.old\\ for rollback);
|
||||||
3. launches app\\CMBot.exe.
|
3. launches app\\CMBot.exe.
|
||||||
@@ -26,13 +26,13 @@ from services import installer # noqa: E402
|
|||||||
from services.file_service import get_data_dir # noqa: E402
|
from services.file_service import get_data_dir # noqa: E402
|
||||||
|
|
||||||
APP_EXE = "CMBot.exe"
|
APP_EXE = "CMBot.exe"
|
||||||
CONFIG_FILES = ("app_config.json", "templates.json")
|
CONFIG_FILES = ("app_config.json", "templates.json", "ai_models.json")
|
||||||
|
|
||||||
logger = logging.getLogger("launcher")
|
logger = logging.getLogger("launcher")
|
||||||
|
|
||||||
|
|
||||||
def seed_defaults(app_dir, data_dir):
|
def seed_defaults(app_dir, data_dir):
|
||||||
"""First run: copy factory config/templates from app\\config into the data
|
"""First run: copy factory config/templates/models from app\\config into the data
|
||||||
root, without overwriting any existing user file."""
|
root, without overwriting any existing user file."""
|
||||||
src = app_dir / "config"
|
src = app_dir / "config"
|
||||||
if not src.is_dir():
|
if not src.is_dir():
|
||||||
|
|||||||
@@ -54,6 +54,15 @@ class TestOutfitConfigHelpers(unittest.TestCase):
|
|||||||
"".encode("utf-8") + json.dumps([{"name": "z"}]).encode("utf-8"))
|
"".encode("utf-8") + json.dumps([{"name": "z"}]).encode("utf-8"))
|
||||||
self.assertEqual(cs.load_ai_models()[0]["name"], "z")
|
self.assertEqual(cs.load_ai_models()[0]["name"], "z")
|
||||||
|
|
||||||
|
def test_default_ai_models_template_is_loadable_and_has_no_keys(self):
|
||||||
|
template = Path(__file__).parent.parent / "packaging" / "default_config" / "ai_models.json"
|
||||||
|
shutil.copy2(str(template), str(self.config_dir / "ai_models.json"))
|
||||||
|
|
||||||
|
models = cs.load_ai_models()
|
||||||
|
|
||||||
|
self.assertEqual([m["name"] for m in models], ["GPT Image 2", "Nano Banana 2"])
|
||||||
|
self.assertTrue(all(m.get("api_key") == "" for m in models))
|
||||||
|
|
||||||
# -- outfit_prompt.txt ----------------------------------------------
|
# -- outfit_prompt.txt ----------------------------------------------
|
||||||
|
|
||||||
def test_prompt_default_when_missing(self):
|
def test_prompt_default_when_missing(self):
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ class _Base(unittest.TestCase):
|
|||||||
(self.app / "config" / "app_config.json").write_text(
|
(self.app / "config" / "app_config.json").write_text(
|
||||||
json.dumps({"update_source": "http://x"}), encoding="utf-8")
|
json.dumps({"update_source": "http://x"}), encoding="utf-8")
|
||||||
(self.app / "config" / "templates.json").write_text("{}", encoding="utf-8")
|
(self.app / "config" / "templates.json").write_text("{}", encoding="utf-8")
|
||||||
|
(self.app / "config" / "ai_models.json").write_text(
|
||||||
|
json.dumps({"models": [{"name": "factory"}]}), encoding="utf-8")
|
||||||
self.data = self.tmp / "data"
|
self.data = self.tmp / "data"
|
||||||
self._prev = os.environ.get("CMBOT_DATA_DIR")
|
self._prev = os.environ.get("CMBOT_DATA_DIR")
|
||||||
os.environ["CMBOT_DATA_DIR"] = str(self.data)
|
os.environ["CMBOT_DATA_DIR"] = str(self.data)
|
||||||
@@ -48,13 +50,18 @@ class TestSeed(_Base):
|
|||||||
launcher.seed_defaults(self.app, self.data)
|
launcher.seed_defaults(self.app, self.data)
|
||||||
self.assertTrue((self.data / "config" / "app_config.json").exists())
|
self.assertTrue((self.data / "config" / "app_config.json").exists())
|
||||||
self.assertTrue((self.data / "config" / "templates.json").exists())
|
self.assertTrue((self.data / "config" / "templates.json").exists())
|
||||||
|
self.assertTrue((self.data / "config" / "ai_models.json").exists())
|
||||||
|
|
||||||
def test_seed_does_not_overwrite(self):
|
def test_seed_does_not_overwrite(self):
|
||||||
(self.data / "config").mkdir(parents=True)
|
(self.data / "config").mkdir(parents=True)
|
||||||
(self.data / "config" / "app_config.json").write_text('{"update_source":"USER"}', encoding="utf-8")
|
(self.data / "config" / "app_config.json").write_text('{"update_source":"USER"}', encoding="utf-8")
|
||||||
|
(self.data / "config" / "ai_models.json").write_text(
|
||||||
|
json.dumps({"models": [{"name": "user"}]}), encoding="utf-8")
|
||||||
launcher.seed_defaults(self.app, self.data)
|
launcher.seed_defaults(self.app, self.data)
|
||||||
kept = json.loads((self.data / "config" / "app_config.json").read_text(encoding="utf-8"))
|
kept = json.loads((self.data / "config" / "app_config.json").read_text(encoding="utf-8"))
|
||||||
self.assertEqual(kept["update_source"], "USER")
|
self.assertEqual(kept["update_source"], "USER")
|
||||||
|
kept_models = json.loads((self.data / "config" / "ai_models.json").read_text(encoding="utf-8"))
|
||||||
|
self.assertEqual(kept_models["models"][0]["name"], "user")
|
||||||
|
|
||||||
|
|
||||||
class TestRun(_Base):
|
class TestRun(_Base):
|
||||||
|
|||||||
Reference in New Issue
Block a user