Skip to content

Commit ce89290

Browse files
committed
v0.2.1 version. Added frontend for API key support.
1 parent 9d8a2bc commit ce89290

8 files changed

Lines changed: 969 additions & 15 deletions

File tree

config/__init__.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
from config.prompts_chara import *
33
import json
44
import os
5+
import logging
6+
7+
# Setup logger for this module
8+
logger = logging.getLogger(__name__)
59

610
# 读取角色配置
711
CHARACTER_JSON_PATH = os.path.join(os.path.dirname(__file__), 'characters.json')
@@ -14,10 +18,10 @@ def get_character_data():
1418
with open(CHARACTER_JSON_PATH, 'r', encoding='utf-8') as f:
1519
character_data = json.load(f)
1620
except FileNotFoundError:
17-
print(f"⚠️ 未找到猫娘配置文件: {CHARACTER_JSON_PATH},请检查文件是否存在。使用默认人设。")
21+
logger.info(f"未找到猫娘配置文件: {CHARACTER_JSON_PATH},请检查文件是否存在。使用默认人设。")
1822
character_data = {"主人": _default_master, "猫娘": _default_lanlan}
1923
except Exception as e:
20-
print(f"💥 读取猫娘配置文件出错: {e},使用默认人设。")
24+
logger.error(f"💥 读取猫娘配置文件出错: {e},使用默认人设。")
2125
character_data = {"主人": _default_master, "猫娘": _default_lanlan}
2226

2327
# MASTER_NAME 必须始终存在,取档案名
@@ -42,18 +46,21 @@ def get_character_data():
4246
TIME_COMPRESSED_TABLE_NAME = "time_indexed_compressed"
4347

4448
try:
45-
with open('core_config.txt', 'r') as f:
49+
with open('./config/core_config.json', 'r', encoding='utf-8') as f:
4650
core_cfg = json.load(f)
4751
if 'coreApiKey' in core_cfg and core_cfg['coreApiKey'] and core_cfg['coreApiKey'] != CORE_API_KEY:
48-
print(f"Warning: coreApiKey in core_config.txt is updated. Overwriting CORE_API_KEY.")
52+
logger.warning("coreApiKey in core_config.json is updated. Overwriting CORE_API_KEY.")
4953
CORE_API_KEY = core_cfg['coreApiKey']
5054

5155
except FileNotFoundError:
5256
pass
5357
except Exception as e:
54-
print(f"💥 Error parsing core_config.txt: {e}")
58+
logger.error(f"Error parsing Core API Key: {e}")
5559

5660
if AUDIO_API_KEY == '':
5761
AUDIO_API_KEY = CORE_API_KEY
5862
if OPENROUTER_API_KEY == '':
5963
OPENROUTER_API_KEY = CORE_API_KEY
64+
65+
if not CORE_API_KEY.startswith('sk'):
66+
logger.error("💥 请检查Core API Key是否正确,通常以'sk-'开头。请在设置页面中重新设置。")

main_server.py

Lines changed: 240 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
# -*- coding: utf-8 -*-
2+
import sys, os
3+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
24
import asyncio
35
import json
46
import traceback
5-
import sys
67
import uuid
78
import logging
89
from datetime import datetime
10+
import webbrowser
911

1012
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, File, UploadFile, Form
1113
from fastapi.staticfiles import StaticFiles
@@ -15,7 +17,6 @@
1517
from utils.preferences import load_user_preferences, update_model_preferences, validate_model_preferences, move_model_to_top
1618
from utils.frontend_utils import find_models, load_characters, save_characters
1719
from multiprocessing import Process, Queue, Event
18-
import os
1920
import atexit
2021
import dashscope
2122
from dashscope.audio.tts_v2 import VoiceEnrollmentService
@@ -77,6 +78,20 @@ def cleanup():
7778
# relative to where the server is running (gemini-live-app/).
7879
app.mount("/static", StaticFiles(directory="static"), name="static")
7980

81+
# 使用 FastAPI 的 app.state 来管理启动配置
82+
def get_start_config():
83+
"""从 app.state 获取启动配置"""
84+
if hasattr(app.state, 'start_config'):
85+
return app.state.start_config
86+
return {
87+
"browser_mode_enabled": False,
88+
"browser_page": "chara_manager",
89+
'server': None
90+
}
91+
92+
def set_start_config(config):
93+
"""设置启动配置到 app.state"""
94+
app.state.start_config = config
8095

8196
# *** CORRECTED ROOT PATH TO SERVE index.html ***
8297
@app.get("/", response_class=HTMLResponse)
@@ -147,6 +162,61 @@ async def set_preferred_model(request: Request):
147162
except Exception as e:
148163
return {"success": False, "error": str(e)}
149164

165+
@app.get("/api/config/core_api")
166+
async def get_core_config():
167+
"""获取核心配置(API Key)"""
168+
try:
169+
# 尝试从core_config.json读取
170+
try:
171+
with open('./config/core_config.json', 'r', encoding='utf-8') as f:
172+
core_cfg = json.load(f)
173+
api_key = core_cfg.get('coreApiKey', '')
174+
except FileNotFoundError:
175+
# 如果文件不存在,返回当前内存中的CORE_API_KEY
176+
api_key = CORE_API_KEY
177+
178+
return {
179+
"api_key": api_key,
180+
"success": True
181+
}
182+
except Exception as e:
183+
return {
184+
"success": False,
185+
"error": str(e)
186+
}
187+
188+
189+
@app.post("/api/config/core_api")
190+
async def update_core_config(request: Request):
191+
"""更新核心配置(API Key)"""
192+
try:
193+
data = await request.json()
194+
if not data:
195+
return {"success": False, "error": "无效的数据"}
196+
197+
if 'coreApiKey' not in data:
198+
return {"success": False, "error": "缺少coreApiKey字段"}
199+
200+
api_key = data['coreApiKey']
201+
if api_key is None:
202+
return {"success": False, "error": "API Key不能为null"}
203+
204+
if not isinstance(api_key, str):
205+
return {"success": False, "error": "API Key必须是字符串类型"}
206+
207+
api_key = api_key.strip()
208+
if not api_key:
209+
return {"success": False, "error": "API Key不能为空"}
210+
211+
# 保存到core_config.json
212+
core_cfg = {"coreApiKey": api_key}
213+
with open('./config/core_config.json', 'w', encoding='utf-8') as f:
214+
json.dump(core_cfg, f, indent=2, ensure_ascii=False)
215+
216+
return {"success": True, "message": "API Key已保存"}
217+
except Exception as e:
218+
return {"success": False, "error": str(e)}
219+
150220

151221
@app.on_event("startup")
152222
async def startup_event():
@@ -161,6 +231,29 @@ async def startup_event():
161231
)
162232
sync_process[k].start()
163233
logger.info(f"同步连接器进程已启动 (PID: {sync_process[k].pid})")
234+
235+
# 如果启用了浏览器模式,在服务器启动完成后打开浏览器
236+
current_config = get_start_config()
237+
print(f"启动配置: {current_config}")
238+
if current_config['browser_mode_enabled']:
239+
import threading
240+
241+
def launch_browser_delayed():
242+
# 等待一小段时间确保服务器完全启动
243+
import time
244+
time.sleep(1)
245+
# 从 app.state 获取配置
246+
config = get_start_config()
247+
url = f"http://127.0.0.1:{MAIN_SERVER_PORT}/{config['browser_page']}"
248+
try:
249+
webbrowser.open(url)
250+
logger.info(f"服务器启动完成,已打开浏览器访问: {url}")
251+
except Exception as e:
252+
logger.error(f"打开浏览器失败: {e}")
253+
254+
# 在独立线程中启动浏览器
255+
t = threading.Thread(target=launch_browser_delayed, daemon=True)
256+
t.start()
164257

165258

166259
@app.on_event("shutdown")
@@ -175,6 +268,19 @@ async def shutdown_event():
175268
if sync_process[k].is_alive():
176269
sync_process[k].terminate() # 如果超时,强制终止
177270
logger.info("同步连接器进程已停止")
271+
272+
# 向memory_server发送关闭信号
273+
try:
274+
import requests
275+
from config import MEMORY_SERVER_PORT
276+
shutdown_url = f"http://localhost:{MEMORY_SERVER_PORT}/shutdown"
277+
response = requests.post(shutdown_url, timeout=2)
278+
if response.status_code == 200:
279+
logger.info("已向memory_server发送关闭信号")
280+
else:
281+
logger.warning(f"向memory_server发送关闭信号失败,状态码: {response.status_code}")
282+
except Exception as e:
283+
logger.warning(f"向memory_server发送关闭信号时出错: {e}")
178284

179285

180286
@app.websocket("/ws/{lanlan_name}")
@@ -250,6 +356,12 @@ async def chara_manager(request: Request):
250356
async def voice_clone_page(request: Request, lanlan_name: str = ""):
251357
return templates.TemplateResponse("templates/voice_clone.html", {"request": request, "lanlan_name": lanlan_name})
252358

359+
@app.get("/api_key", response_class=HTMLResponse)
360+
async def api_key_settings(request: Request):
361+
"""API Key 设置页面"""
362+
return templates.TemplateResponse("templates/api_key_settings.html", {
363+
"request": request
364+
})
253365

254366
@app.get('/api/characters')
255367
async def get_characters():
@@ -324,6 +436,33 @@ async def update_catgirl_voice_id(name: str, request: Request):
324436
save_characters(characters)
325437
return {"success": True}
326438

439+
@app.post('/api/characters/clear_voice_ids')
440+
async def clear_voice_ids():
441+
"""清除所有角色的本地Voice ID记录"""
442+
try:
443+
characters = load_characters()
444+
cleared_count = 0
445+
446+
# 清除所有猫娘的voice_id
447+
if '猫娘' in characters:
448+
for name in characters['猫娘']:
449+
if 'voice_id' in characters['猫娘'][name] and characters['猫娘'][name]['voice_id']:
450+
characters['猫娘'][name]['voice_id'] = ''
451+
cleared_count += 1
452+
453+
save_characters(characters)
454+
455+
return JSONResponse({
456+
'success': True,
457+
'message': f'已清除 {cleared_count} 个角色的Voice ID记录',
458+
'cleared_count': cleared_count
459+
})
460+
except Exception as e:
461+
return JSONResponse({
462+
'success': False,
463+
'error': f'清除Voice ID记录时出错: {str(e)}'
464+
}, status_code=500)
465+
327466
@app.post('/api/tmpfiles_voice_clone')
328467
async def tmpfiles_voice_clone(file: UploadFile = File(...), prefix: str = Form(...)):
329468
import os
@@ -374,6 +513,49 @@ async def delete_catgirl(name: str):
374513
save_characters(characters)
375514
return {"success": True}
376515

516+
@app.post('/api/beacon/shutdown')
517+
async def beacon_shutdown():
518+
"""Beacon API for graceful server shutdown"""
519+
try:
520+
# 从 app.state 获取配置
521+
current_config = get_start_config()
522+
# Only respond to beacon if server was started with --open-browser
523+
if current_config['browser_mode_enabled']:
524+
logger.info("收到beacon信号,准备关闭服务器...")
525+
# Schedule server shutdown
526+
asyncio.create_task(shutdown_server_async())
527+
return {"success": True, "message": "服务器关闭信号已接收"}
528+
except Exception as e:
529+
logger.error(f"Beacon处理错误: {e}")
530+
return {"success": False, "error": str(e)}
531+
532+
async def shutdown_server_async():
533+
"""异步关闭服务器"""
534+
try:
535+
# Give a small delay to allow the beacon response to be sent
536+
await asyncio.sleep(0.5)
537+
logger.info("正在关闭服务器...")
538+
539+
# 向memory_server发送关闭信号
540+
try:
541+
import requests
542+
from config import MEMORY_SERVER_PORT
543+
shutdown_url = f"http://localhost:{MEMORY_SERVER_PORT}/shutdown"
544+
response = requests.post(shutdown_url, timeout=1)
545+
if response.status_code == 200:
546+
logger.info("已向memory_server发送关闭信号")
547+
else:
548+
logger.warning(f"向memory_server发送关闭信号失败,状态码: {response.status_code}")
549+
except Exception as e:
550+
logger.warning(f"向memory_server发送关闭信号时出错: {e}")
551+
552+
# Signal the server to stop
553+
current_config = get_start_config()
554+
if current_config['server'] is not None:
555+
current_config['server'].should_exit = True
556+
except Exception as e:
557+
logger.error(f"关闭服务器时出错: {e}")
558+
377559
@app.post('/api/characters/catgirl/{old_name}/rename')
378560
async def rename_catgirl(old_name: str, request: Request):
379561
data = await request.json()
@@ -408,15 +590,68 @@ async def get_index(request: Request, lanlan_name: str):
408590

409591

410592
# --- Run the Server ---
411-
# (Keep your existing __main__ block)
412593
if __name__ == "__main__":
413594
import uvicorn
595+
import argparse
596+
import os
597+
import signal
598+
599+
parser = argparse.ArgumentParser()
600+
parser.add_argument("--open-browser", action="store_true",
601+
help="启动后是否打开浏览器并监控它")
602+
parser.add_argument("--page", type=str, default="",
603+
choices=["index", "chara_manager", "api_key"],
604+
help="要打开的页面路由(不含域名和端口)")
605+
args = parser.parse_args()
414606

415607
logger.info("--- Starting FastAPI Server ---")
416608
# Use os.path.abspath to show full path clearly
417609
logger.info(f"Serving static files from: {os.path.abspath('static')}")
418610
logger.info(f"Serving index.html from: {os.path.abspath('templates/index.html')}")
419611
logger.info(f"Access UI at: http://127.0.0.1:{MAIN_SERVER_PORT} (or your network IP:{MAIN_SERVER_PORT})")
420612
logger.info("-----------------------------")
421-
# Run from the directory containing server.py (gemini-live-app/)
422-
uvicorn.run("main_server:app", host="0.0.0.0", port=MAIN_SERVER_PORT, reload=False)
613+
614+
# 1) 配置 UVicorn
615+
config = uvicorn.Config(
616+
app=app,
617+
host="0.0.0.0",
618+
port=MAIN_SERVER_PORT,
619+
log_level="info",
620+
loop="asyncio",
621+
reload=False,
622+
)
623+
server = uvicorn.Server(config)
624+
625+
# Set browser mode flag if --open-browser is used
626+
if args.open_browser:
627+
# 使用 FastAPI 的 app.state 来管理配置
628+
start_config = {
629+
"browser_mode_enabled": True,
630+
"browser_page": args.page if args.page!='index' else '',
631+
'server': server
632+
}
633+
set_start_config(start_config)
634+
else:
635+
# 设置默认配置
636+
start_config = {
637+
"browser_mode_enabled": False,
638+
"browser_page": "",
639+
'server': server
640+
}
641+
set_start_config(start_config)
642+
643+
print(f"启动配置: {get_start_config()}")
644+
645+
# 2) 定义服务器关闭回调
646+
def shutdown_server():
647+
logger.info("收到浏览器关闭信号,正在关闭服务器...")
648+
os.kill(os.getpid(), signal.SIGTERM)
649+
650+
# 4) 启动服务器(阻塞,直到 server.should_exit=True)
651+
logger.info("--- Starting FastAPI Server ---")
652+
logger.info(f"Access UI at: http://127.0.0.1:{MAIN_SERVER_PORT}/{args.page}")
653+
654+
try:
655+
server.run()
656+
finally:
657+
logger.info("服务器已关闭")

0 commit comments

Comments
 (0)