diff --git a/docs/10-lan-update.md b/docs/10-lan-update.md index c03598e..36f5154 100644 --- a/docs/10-lan-update.md +++ b/docs/10-lan-update.md @@ -73,6 +73,7 @@ config\ app_config.json # 用户偏好(输出格式、最近文件夹、最近模板、更新源等) templates.json # 用户自定义模板 + ai_models.json # AI 穿搭模型配置(管理员填写 key,首次可由出厂模板播种) logs\ output\ # 默认导出目录的回退位置(安装根不可写时) ``` @@ -106,7 +107,7 @@ 1. 环境变量 `CMBOT_DATA_DIR` 非空时使用它(覆盖口,供测试或特殊部署)。 2. 打包态(`sys.frozen`)→ `~/.cmbot`(即 `%USERPROFILE%\.cmbot`)。**不依赖启动器注入环境变量**:即使用户绕过 `Launcher.exe` 直接双击 `app\CMBot.exe`,数据也落在 `~/.cmbot`。 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`)。 数据目录分离与 `~/.cmbot` 约定后续需同步 `docs/05-project-architecture.md` 与 `docs/09` 第 5、6 节的目录说明。 diff --git a/packaging/default_config/ai_models.json b/packaging/default_config/ai_models.json new file mode 100644 index 0000000..3f168ef --- /dev/null +++ b/packaging/default_config/ai_models.json @@ -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": {} + } + ] +} diff --git a/packaging/default_config/app_config.json b/packaging/default_config/app_config.json index a317b06..5f8fc36 100644 --- a/packaging/default_config/app_config.json +++ b/packaging/default_config/app_config.json @@ -6,6 +6,7 @@ "last_print_dir": "", "last_template": "", "last_batch_mode": "full_combo", + "outfit_model": "Nano Banana 2", "update_source": "http://cm.xiapi.com", "update_user": "admin", "update_pass": "chengma" diff --git a/src/launcher.py b/src/launcher.py index ddd6405..2dbfcf1 100644 --- a/src/launcher.py +++ b/src/launcher.py @@ -3,7 +3,7 @@ Compiled to Launcher.exe (PyInstaller onefile) and run instead of launching 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 staging\\app.new into app\\, keeping app.old\\ for rollback); 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 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") 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.""" src = app_dir / "config" if not src.is_dir(): diff --git a/tests/test_config_service.py b/tests/test_config_service.py index 33a9ff5..a8d6afb 100644 --- a/tests/test_config_service.py +++ b/tests/test_config_service.py @@ -54,6 +54,15 @@ class TestOutfitConfigHelpers(unittest.TestCase): "".encode("utf-8") + json.dumps([{"name": "z"}]).encode("utf-8")) 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 ---------------------------------------------- def test_prompt_default_when_missing(self): diff --git a/tests/test_launcher.py b/tests/test_launcher.py index acdd85d..b0ae750 100644 --- a/tests/test_launcher.py +++ b/tests/test_launcher.py @@ -25,6 +25,8 @@ class _Base(unittest.TestCase): (self.app / "config" / "app_config.json").write_text( json.dumps({"update_source": "http://x"}), 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._prev = os.environ.get("CMBOT_DATA_DIR") os.environ["CMBOT_DATA_DIR"] = str(self.data) @@ -48,13 +50,18 @@ class TestSeed(_Base): launcher.seed_defaults(self.app, self.data) self.assertTrue((self.data / "config" / "app_config.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): (self.data / "config").mkdir(parents=True) (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) kept = json.loads((self.data / "config" / "app_config.json").read_text(encoding="utf-8")) 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):