add llm config panel
This commit is contained in:
@@ -206,6 +206,9 @@
|
|||||||
## 贡献者
|
## 贡献者
|
||||||
- Aku, 世界观\玩法设计与讨论
|
- Aku, 世界观\玩法设计与讨论
|
||||||
|
|
||||||
|
## 参考
|
||||||
|
- 参考了ailifeengine部分ui
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
本项目采用 [LICENSE](LICENSE) 文件中指定的许可证。
|
本项目采用 [LICENSE](LICENSE) 文件中指定的许可证。
|
||||||
|
|||||||
@@ -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.sim.load.load_game import load_game
|
||||||
from src.utils import protagonist as prot_utils
|
from src.utils import protagonist as prot_utils
|
||||||
import random
|
import random
|
||||||
|
from omegaconf import OmegaConf
|
||||||
|
from src.utils.llm.client import test_connectivity
|
||||||
|
from src.utils.llm.config import LLMConfig, LLMMode
|
||||||
|
|
||||||
# 全局游戏实例
|
# 全局游戏实例
|
||||||
game_instance = {
|
game_instance = {
|
||||||
@@ -985,6 +988,100 @@ def delete_avatar(req: DeleteAvatarRequest):
|
|||||||
raise HTTPException(status_code=500, detail=str(e))
|
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 ---
|
# --- 存档系统 API ---
|
||||||
|
|
||||||
class SaveGameRequest(BaseModel):
|
class SaveGameRequest(BaseModel):
|
||||||
|
|||||||
@@ -173,18 +173,21 @@ async def call_llm_with_task_name(
|
|||||||
return await call_llm_with_template(template_path, infos, mode, max_retries)
|
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 服务连通性 (同步版本)
|
测试 LLM 服务连通性 (同步版本)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
mode: 测试使用的模式 (NORMAL/FAST)
|
mode: 测试使用的模式 (NORMAL/FAST),如果传入 config 则忽略此参数
|
||||||
|
config: 直接使用该配置进行测试
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: 连接成功返回 True,失败返回 False
|
bool: 连接成功返回 True,失败返回 False
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
config = LLMConfig.from_mode(mode)
|
if config is None:
|
||||||
|
config = LLMConfig.from_mode(mode)
|
||||||
|
|
||||||
if HAS_LITELLM:
|
if HAS_LITELLM:
|
||||||
# 使用 litellm 同步接口
|
# 使用 litellm 同步接口
|
||||||
litellm.completion(
|
litellm.completion(
|
||||||
@@ -197,5 +200,6 @@ def test_connectivity(mode: LLMMode = LLMMode.NORMAL) -> bool:
|
|||||||
# 直接调用 requests 实现
|
# 直接调用 requests 实现
|
||||||
_call_with_requests(config, "test")
|
_call_with_requests(config, "test")
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception as e:
|
||||||
return False
|
print(f"Connectivity test failed: {e}")
|
||||||
|
return False
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ export interface PhenomenonDTO {
|
|||||||
effect_desc: string;
|
effect_desc: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LLMConfigDTO {
|
||||||
|
base_url: string;
|
||||||
|
api_key: string;
|
||||||
|
model_name: string;
|
||||||
|
fast_model_name: string;
|
||||||
|
mode: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const gameApi = {
|
export const gameApi = {
|
||||||
// --- World State ---
|
// --- World State ---
|
||||||
|
|
||||||
@@ -149,5 +157,19 @@ export const gameApi = {
|
|||||||
|
|
||||||
deleteAvatar(avatarId: string) {
|
deleteAvatar(avatarId: string) {
|
||||||
return httpClient.post<{ status: string; message: string }>('/api/action/delete_avatar', { avatar_id: avatarId });
|
return httpClient.post<{ status: string; message: string }>('/api/action/delete_avatar', { avatar_id: avatarId });
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- LLM Config ---
|
||||||
|
|
||||||
|
fetchLLMConfig() {
|
||||||
|
return httpClient.get<LLMConfigDTO>('/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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ref, watch } from 'vue'
|
|||||||
import SaveLoadPanel from './game/panels/system/SaveLoadPanel.vue'
|
import SaveLoadPanel from './game/panels/system/SaveLoadPanel.vue'
|
||||||
import CreateAvatarPanel from './game/panels/system/CreateAvatarPanel.vue'
|
import CreateAvatarPanel from './game/panels/system/CreateAvatarPanel.vue'
|
||||||
import DeleteAvatarPanel from './game/panels/system/DeleteAvatarPanel.vue'
|
import DeleteAvatarPanel from './game/panels/system/DeleteAvatarPanel.vue'
|
||||||
|
import LLMConfigPanel from './game/panels/system/LLMConfigPanel.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
visible: boolean
|
visible: boolean
|
||||||
@@ -12,7 +13,7 @@ const emit = defineEmits<{
|
|||||||
(e: 'close'): void
|
(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) {
|
function switchTab(tab: typeof activeTab.value) {
|
||||||
activeTab.value = tab
|
activeTab.value = tab
|
||||||
@@ -60,6 +61,12 @@ watch(() => props.visible, (val) => {
|
|||||||
>
|
>
|
||||||
删除角色
|
删除角色
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
:class="{ active: activeTab === 'llm' }"
|
||||||
|
@click="switchTab('llm')"
|
||||||
|
>
|
||||||
|
LLM设置
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="menu-content">
|
<div class="menu-content">
|
||||||
@@ -77,6 +84,8 @@ watch(() => props.visible, (val) => {
|
|||||||
currently it stays to allow creating more or just refreshes internal list -->
|
currently it stays to allow creating more or just refreshes internal list -->
|
||||||
|
|
||||||
<DeleteAvatarPanel v-else-if="activeTab === 'delete'" />
|
<DeleteAvatarPanel v-else-if="activeTab === 'delete'" />
|
||||||
|
|
||||||
|
<LLMConfigPanel v-else-if="activeTab === 'llm'" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
689
web/src/components/game/panels/system/LLMConfigPanel.vue
Normal file
689
web/src/components/game/panels/system/LLMConfigPanel.vue
Normal file
@@ -0,0 +1,689 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { gameApi, type LLMConfigDTO } from '../../../../api/game'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const loading = ref(false)
|
||||||
|
const testing = ref(false)
|
||||||
|
const showHelpModal = ref(false)
|
||||||
|
|
||||||
|
const config = ref<LLMConfigDTO>({
|
||||||
|
base_url: '',
|
||||||
|
api_key: '',
|
||||||
|
model_name: '',
|
||||||
|
fast_model_name: '',
|
||||||
|
mode: 'default'
|
||||||
|
})
|
||||||
|
|
||||||
|
const modeOptions = [
|
||||||
|
{ label: '均衡 (Default)', value: 'default', desc: '自动选择模型' },
|
||||||
|
{ label: '智能 (Normal)', value: 'normal', desc: '全用智能模型' },
|
||||||
|
{ label: '快速 (Fast)', value: 'fast', desc: '全用快速模型' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const presets = [
|
||||||
|
{
|
||||||
|
name: '通义千问',
|
||||||
|
base_url: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||||
|
model_name: 'qwen-plus',
|
||||||
|
fast_model_name: 'qwen-turbo'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'DeepSeek',
|
||||||
|
base_url: 'https://api.deepseek.com',
|
||||||
|
model_name: 'deepseek-chat',
|
||||||
|
fast_model_name: 'deepseek-chat'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '硅基流动',
|
||||||
|
base_url: 'https://api.siliconflow.cn/v1',
|
||||||
|
model_name: 'Qwen/Qwen2.5-72B-Instruct',
|
||||||
|
fast_model_name: 'Qwen/Qwen2.5-7B-Instruct'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'OpenRouter',
|
||||||
|
base_url: 'https://openrouter.ai/api/v1',
|
||||||
|
model_name: 'anthropic/claude-3.5-sonnet',
|
||||||
|
fast_model_name: 'google/gemini-flash-1.5'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
async function fetchConfig() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await gameApi.fetchLLMConfig()
|
||||||
|
// 确保 API Key 在前端展示为空,增加安全性提示
|
||||||
|
config.value = { ...res, api_key: '' }
|
||||||
|
} catch (e) {
|
||||||
|
message.error('获取配置失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPreset(preset: typeof presets[0]) {
|
||||||
|
config.value.base_url = preset.base_url
|
||||||
|
config.value.model_name = preset.model_name
|
||||||
|
config.value.fast_model_name = preset.fast_model_name
|
||||||
|
message.info(`已应用 ${preset.name} 预设 (请填写 API Key)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTestAndSave() {
|
||||||
|
if (!config.value.api_key) {
|
||||||
|
message.warning('请填写 API Key')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!config.value.base_url) {
|
||||||
|
message.warning('请填写 Base URL')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
testing.value = true
|
||||||
|
try {
|
||||||
|
// 1. 测试连接
|
||||||
|
await gameApi.testLLMConnection(config.value)
|
||||||
|
message.success('连接测试成功')
|
||||||
|
|
||||||
|
// 2. 保存配置
|
||||||
|
await gameApi.saveLLMConfig(config.value)
|
||||||
|
message.success('配置已保存')
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error('测试或保存失败: ' + (e.response?.data?.detail || e.message))
|
||||||
|
} finally {
|
||||||
|
testing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchConfig()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="llm-panel">
|
||||||
|
<div v-if="loading" class="loading">加载中...</div>
|
||||||
|
<div v-else class="config-form">
|
||||||
|
|
||||||
|
<!-- 预设按钮 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">快速填充</div>
|
||||||
|
<div class="preset-buttons">
|
||||||
|
<button
|
||||||
|
v-for="preset in presets"
|
||||||
|
:key="preset.name"
|
||||||
|
class="preset-btn"
|
||||||
|
@click="applyPreset(preset)"
|
||||||
|
>
|
||||||
|
{{ preset.name }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 核心配置 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">API 配置</div>
|
||||||
|
|
||||||
|
<div class="form-item">
|
||||||
|
<div class="label-row">
|
||||||
|
<label>API Key</label>
|
||||||
|
<button class="help-btn" @click="showHelpModal = true">什么是 API / 如何获取?</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model="config.api_key"
|
||||||
|
type="password"
|
||||||
|
placeholder="在此填入你自己的 API Key (通常以 sk- 开头)"
|
||||||
|
class="input-field"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-item">
|
||||||
|
<label>Base URL</label>
|
||||||
|
<input
|
||||||
|
v-model="config.base_url"
|
||||||
|
type="text"
|
||||||
|
placeholder="https://api.example.com/v1"
|
||||||
|
class="input-field"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 模型配置 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">模型选择</div>
|
||||||
|
|
||||||
|
<div class="form-item">
|
||||||
|
<label>智能模型 (Normal)</label>
|
||||||
|
<div class="desc">用于处理复杂逻辑、剧情生成等任务</div>
|
||||||
|
<input
|
||||||
|
v-model="config.model_name"
|
||||||
|
type="text"
|
||||||
|
placeholder="例如: gpt-4, claude-3-opus, qwen-plus"
|
||||||
|
class="input-field"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-item">
|
||||||
|
<label>快速模型 (Fast)</label>
|
||||||
|
<div class="desc">用于简单判定、频繁交互等任务</div>
|
||||||
|
<input
|
||||||
|
v-model="config.fast_model_name"
|
||||||
|
type="text"
|
||||||
|
placeholder="例如: gpt-3.5-turbo, qwen-turbo"
|
||||||
|
class="input-field"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 模式选择 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">运行模式</div>
|
||||||
|
<div class="mode-options horizontal">
|
||||||
|
<label
|
||||||
|
v-for="opt in modeOptions"
|
||||||
|
:key="opt.value"
|
||||||
|
class="mode-radio"
|
||||||
|
:class="{ active: config.mode === opt.value }"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
v-model="config.mode"
|
||||||
|
:value="opt.value"
|
||||||
|
class="hidden-radio"
|
||||||
|
/>
|
||||||
|
<div class="radio-content">
|
||||||
|
<div class="radio-label">{{ opt.label }}</div>
|
||||||
|
<div class="radio-desc">{{ opt.desc }}</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部操作 -->
|
||||||
|
<div class="action-bar">
|
||||||
|
<button
|
||||||
|
class="save-btn"
|
||||||
|
:disabled="testing"
|
||||||
|
@click="handleTestAndSave"
|
||||||
|
>
|
||||||
|
{{ testing ? '测试连接中...' : '测试连通性并保存' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 帮助弹窗 -->
|
||||||
|
<div v-if="showHelpModal" class="modal-overlay" @click.self="showHelpModal = false">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>什么是 API? 新手配置指南</h3>
|
||||||
|
<button class="close-btn" @click="showHelpModal = false">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="help-section">
|
||||||
|
<h4>🌐 1. 什么是 API?</h4>
|
||||||
|
<p>
|
||||||
|
API (应用程序接口) 就像是一条“电话线”。本游戏本身不具备思考能力,它通过这条线连接到远端的 <strong>AI 大脑</strong> (如 Qwen 或 DeepSeek 的服务器)。当游戏进行每月结算并决定 NPC 动作时,会将相关信息通过 API 发给 AI,AI 思考后再把结果传回来。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="help-section">
|
||||||
|
<h4>⚡ 2. 推荐的模型 (2025版)</h4>
|
||||||
|
<div class="model-cards">
|
||||||
|
<div class="card">
|
||||||
|
<h5>Qwen-Plus / Fast</h5>
|
||||||
|
<p>国内大厂 (阿里),稳定且免费额度大,适合入门。</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h5>DeepSeek V3</h5>
|
||||||
|
<p>性价比极高,中文叙事逻辑更符合国人习惯。</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h5>Gemini 3 Pro / Fast</h5>
|
||||||
|
<p>Google 出品,综合性能顶尖。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="help-section">
|
||||||
|
<h4>📝 3. 如何填入配置?</h4>
|
||||||
|
<p>获得 API 后,你需要填入以下三大核心参数才能使用,通常你可以在api提供方的文档中找到这些参数怎么填:</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<p><strong>API Base URL (接口地址):</strong> AI 的访问大门,通常由厂商提供 (如 <code>https://api.deepseek.com</code>)。</p>
|
||||||
|
<p><strong>API Key (密钥):</strong> 你的身份凭证,就像账号密码。</p>
|
||||||
|
<p><strong>Model Name (模型名称):</strong> 告诉服务器你想用哪颗大脑,如 <code>deepseek-chat</code> 或 <code>gemini-3-flash-preview</code>。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="help-section">
|
||||||
|
<h4>🔗 4. 从哪里获取 Key?</h4>
|
||||||
|
<ul class="link-list">
|
||||||
|
<li><a href="https://bailian.console.aliyun.com/" target="_blank">阿里云百炼 (Qwen) <span class="recommend-tag">最推荐</span></a></li>
|
||||||
|
<li><a href="https://platform.deepseek.com/" target="_blank">DeepSeek 开放平台 (国内推荐,便宜)</a></li>
|
||||||
|
<li><a href="https://openrouter.ai/" target="_blank">OpenRouter (全机型聚合,推荐)</a></li>
|
||||||
|
<li><a href="https://cloud.siliconflow.cn/" target="_blank">硅基流动 (国内聚合)</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="help-section">
|
||||||
|
<h4>🛡️ 5. 安全说明</h4>
|
||||||
|
<p>
|
||||||
|
您的 API Key 仅保存在您的本地电脑配置文件中 (`static/local_config.yml`),由本地运行的游戏后端直接与模型厂商通信。本游戏 (Cultivation World Simulator) 是完全开源的程序,绝不会将您的 Key 上传至任何第三方服务器。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="confirm-btn" @click="showHelpModal = false">我明白了</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.llm-panel {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
color: #888;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ddd;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border-left: 3px solid #4a9eff;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-btn {
|
||||||
|
background: #333;
|
||||||
|
border: 1px solid #444;
|
||||||
|
color: #ccc;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-btn:hover {
|
||||||
|
background: #444;
|
||||||
|
border-color: #666;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #bbb;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item .desc {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
width: 100%;
|
||||||
|
background: #222;
|
||||||
|
border: 1px solid #444;
|
||||||
|
color: #ddd;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #4a9eff;
|
||||||
|
background: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-btn {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #444;
|
||||||
|
color: #888;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-btn:hover {
|
||||||
|
border-color: #666;
|
||||||
|
color: #bbb;
|
||||||
|
background: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-options.horizontal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-options.horizontal .mode-radio {
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 12px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-radio {
|
||||||
|
display: flex;
|
||||||
|
background: #222;
|
||||||
|
border: 1px solid #333;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-radio:hover {
|
||||||
|
background: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-radio.active {
|
||||||
|
background: #1a2a3a;
|
||||||
|
border-color: #4a9eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-radio {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-label {
|
||||||
|
color: #ddd;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-desc {
|
||||||
|
color: #777;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Styles */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
z-index: 2000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: #0f1115;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 700px;
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 90vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 20px 50px rgba(0,0,0,0.7);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 20px 24px;
|
||||||
|
border-bottom: 1px solid #222;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: linear-gradient(to bottom, #1a1c22, #0f1115);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3::before {
|
||||||
|
content: "?";
|
||||||
|
display: inline-flex;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 1px solid #00e0b0;
|
||||||
|
color: #00e0b0;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 14px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #666;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 24px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-section h4 {
|
||||||
|
color: #6da; /* 类似截图中的青绿色 */
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-section p {
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-cards {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
flex: 1;
|
||||||
|
background: #16181d;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h5 {
|
||||||
|
color: #8a9eff;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card p {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #777;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
background: #111;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block p {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block strong {
|
||||||
|
color: #00e0b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block code {
|
||||||
|
background: #333;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #ff79c6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
background: #16181d;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-list li {
|
||||||
|
border-bottom: 1px solid #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-list li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-list a {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
color: #ddd;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-list a:hover {
|
||||||
|
background: #1f2229;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-list a::after {
|
||||||
|
content: "↗";
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-top: 1px solid #222;
|
||||||
|
background: #0f1115;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn {
|
||||||
|
width: 100%;
|
||||||
|
background: #0099cc; /* 类似截图中的蓝色按钮 */
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn:hover {
|
||||||
|
background: #0088bb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
background: #2a8a4a;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn:hover:not(:disabled) {
|
||||||
|
background: #3aa85a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn:disabled {
|
||||||
|
background: #33443a;
|
||||||
|
color: #888;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
background: #ff4d4f;
|
||||||
|
color: white;
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-left: 6px;
|
||||||
|
font-weight: normal;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user