From b8a4850e80b71cfb8157689eeda293b8318572a5 Mon Sep 17 00:00:00 2001 From: bridge Date: Tue, 30 Dec 2025 21:23:30 +0800 Subject: [PATCH] add llm config panel --- README.md | 3 + src/server/main.py | 97 +++ src/utils/llm/client.py | 14 +- web/src/api/game.ts | 22 + web/src/components/SystemMenu.vue | 11 +- .../game/panels/system/LLMConfigPanel.vue | 689 ++++++++++++++++++ 6 files changed, 830 insertions(+), 6 deletions(-) create mode 100644 web/src/components/game/panels/system/LLMConfigPanel.vue diff --git a/README.md b/README.md index 3378c39..10ee455 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,9 @@ ## 贡献者 - Aku, 世界观\玩法设计与讨论 +## 参考 +- 参考了ailifeengine部分ui + ## 许可证 本项目采用 [LICENSE](LICENSE) 文件中指定的许可证。 diff --git a/src/server/main.py b/src/server/main.py index 7e0b2af..0db5cf5 100644 --- a/src/server/main.py +++ b/src/server/main.py @@ -36,6 +36,9 @@ from src.sim.save.save_game import save_game, list_saves from src.sim.load.load_game import load_game from src.utils import protagonist as prot_utils import random +from omegaconf import OmegaConf +from src.utils.llm.client import test_connectivity +from src.utils.llm.config import LLMConfig, LLMMode # 全局游戏实例 game_instance = { @@ -985,6 +988,100 @@ def delete_avatar(req: DeleteAvatarRequest): raise HTTPException(status_code=500, detail=str(e)) +# --- LLM Config API --- + +class LLMConfigDTO(BaseModel): + base_url: str + api_key: str + model_name: str + fast_model_name: str + mode: str + +class TestConnectionRequest(BaseModel): + base_url: str + api_key: str + model_name: str + +@app.get("/api/config/llm") +def get_llm_config(): + """获取当前 LLM 配置""" + return { + "base_url": getattr(CONFIG.llm, "base_url", ""), + "api_key": getattr(CONFIG.llm, "key", ""), + "model_name": getattr(CONFIG.llm, "model_name", ""), + "fast_model_name": getattr(CONFIG.llm, "fast_model_name", ""), + "mode": getattr(CONFIG.llm, "mode", "default") + } + +@app.post("/api/config/llm/test") +def test_llm_connection(req: TestConnectionRequest): + """测试 LLM 连接""" + try: + # 构造临时配置 + config = LLMConfig( + base_url=req.base_url, + api_key=req.api_key, + model_name=req.model_name + ) + + success = test_connectivity(config=config) + + if success: + return {"status": "ok", "message": "连接成功"} + else: + raise HTTPException(status_code=400, detail="连接失败") + except Exception as e: + raise HTTPException(status_code=500, detail=f"测试出错: {str(e)}") + +@app.post("/api/config/llm/save") +def save_llm_config(req: LLMConfigDTO): + """保存 LLM 配置""" + try: + # 1. Update In-Memory Config (Partial update) + # OmegaConf object attributes can be set directly if they exist + if not OmegaConf.is_config(CONFIG): + # 理论上 CONFIG 是 DictConfig + pass + + # 直接更新 CONFIG.llm 的属性 + CONFIG.llm.base_url = req.base_url + CONFIG.llm.key = req.api_key + CONFIG.llm.model_name = req.model_name + CONFIG.llm.fast_model_name = req.fast_model_name + CONFIG.llm.mode = req.mode + + # 2. Persist to local_config.yml + # 使用 src/utils/config.py 中类似的路径逻辑 + # 注意:这里我们假设是在项目根目录下运行,或者静态文件路径是相对固定的 + # 为了稳健,我们复用 CONFIG 加载时的路径逻辑(但这里是写入) + + local_config_path = "static/local_config.yml" + + # Load existing or create new + if os.path.exists(local_config_path): + conf = OmegaConf.load(local_config_path) + else: + conf = OmegaConf.create({}) + + # Ensure llm section exists + if "llm" not in conf: + conf.llm = {} + + conf.llm.base_url = req.base_url + conf.llm.key = req.api_key + conf.llm.model_name = req.model_name + conf.llm.fast_model_name = req.fast_model_name + conf.llm.mode = req.mode + + OmegaConf.save(conf, local_config_path) + + return {"status": "ok", "message": "配置已保存"} + except Exception as e: + import traceback + traceback.print_exc() + raise HTTPException(status_code=500, detail=f"保存失败: {str(e)}") + + # --- 存档系统 API --- class SaveGameRequest(BaseModel): diff --git a/src/utils/llm/client.py b/src/utils/llm/client.py index e6f9e73..d818a73 100644 --- a/src/utils/llm/client.py +++ b/src/utils/llm/client.py @@ -173,18 +173,21 @@ async def call_llm_with_task_name( return await call_llm_with_template(template_path, infos, mode, max_retries) -def test_connectivity(mode: LLMMode = LLMMode.NORMAL) -> bool: +def test_connectivity(mode: LLMMode = LLMMode.NORMAL, config: Optional[LLMConfig] = None) -> bool: """ 测试 LLM 服务连通性 (同步版本) Args: - mode: 测试使用的模式 (NORMAL/FAST) + mode: 测试使用的模式 (NORMAL/FAST),如果传入 config 则忽略此参数 + config: 直接使用该配置进行测试 Returns: bool: 连接成功返回 True,失败返回 False """ try: - config = LLMConfig.from_mode(mode) + if config is None: + config = LLMConfig.from_mode(mode) + if HAS_LITELLM: # 使用 litellm 同步接口 litellm.completion( @@ -197,5 +200,6 @@ def test_connectivity(mode: LLMMode = LLMMode.NORMAL) -> bool: # 直接调用 requests 实现 _call_with_requests(config, "test") return True - except Exception: - return False \ No newline at end of file + except Exception as e: + print(f"Connectivity test failed: {e}") + return False diff --git a/web/src/api/game.ts b/web/src/api/game.ts index 48c5b98..3f3d206 100644 --- a/web/src/api/game.ts +++ b/web/src/api/game.ts @@ -59,6 +59,14 @@ export interface PhenomenonDTO { effect_desc: string; } +export interface LLMConfigDTO { + base_url: string; + api_key: string; + model_name: string; + fast_model_name: string; + mode: string; +} + export const gameApi = { // --- World State --- @@ -149,5 +157,19 @@ export const gameApi = { deleteAvatar(avatarId: string) { return httpClient.post<{ status: string; message: string }>('/api/action/delete_avatar', { avatar_id: avatarId }); + }, + + // --- LLM Config --- + + fetchLLMConfig() { + return httpClient.get('/api/config/llm'); + }, + + testLLMConnection(config: LLMConfigDTO) { + return httpClient.post<{ status: string; message: string }>('/api/config/llm/test', config); + }, + + saveLLMConfig(config: LLMConfigDTO) { + return httpClient.post<{ status: string; message: string }>('/api/config/llm/save', config); } }; diff --git a/web/src/components/SystemMenu.vue b/web/src/components/SystemMenu.vue index 80cd7a9..8bb04a4 100644 --- a/web/src/components/SystemMenu.vue +++ b/web/src/components/SystemMenu.vue @@ -3,6 +3,7 @@ import { ref, watch } from 'vue' import SaveLoadPanel from './game/panels/system/SaveLoadPanel.vue' import CreateAvatarPanel from './game/panels/system/CreateAvatarPanel.vue' import DeleteAvatarPanel from './game/panels/system/DeleteAvatarPanel.vue' +import LLMConfigPanel from './game/panels/system/LLMConfigPanel.vue' const props = defineProps<{ visible: boolean @@ -12,7 +13,7 @@ const emit = defineEmits<{ (e: 'close'): void }>() -const activeTab = ref<'save' | 'load' | 'create' | 'delete'>('load') +const activeTab = ref<'save' | 'load' | 'create' | 'delete' | 'llm'>('load') function switchTab(tab: typeof activeTab.value) { activeTab.value = tab @@ -60,6 +61,12 @@ watch(() => props.visible, (val) => { > 删除角色 + diff --git a/web/src/components/game/panels/system/LLMConfigPanel.vue b/web/src/components/game/panels/system/LLMConfigPanel.vue new file mode 100644 index 0000000..16a69b7 --- /dev/null +++ b/web/src/components/game/panels/system/LLMConfigPanel.vue @@ -0,0 +1,689 @@ + + + + +