6f1bc27101
1. 人设外键重构:persona_template 成为 system_prompt 唯一权威来源, agent/系统节点通过 persona_id FK 引用,含数据迁移脚本 2. Chat UI:去掉底部AI提示、加号改为弹出菜单、新建对话乐观跳转 3. 意识节点:无可用worker时禁止编造agent_id,只能自行完成或拒绝 4. 日志页面:双tab布局(系统日志 + 工作流日志列表选择) 5. 其他:SSE流式聊天、对话删除/重命名、standalone模式修复 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
193 lines
6.5 KiB
Python
193 lines
6.5 KiB
Python
# Copyright 2026 zhaoxi826
|
||
#
|
||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||
# you may not use this file except in compliance with the License.
|
||
# You may obtain a copy of the License at
|
||
#
|
||
# http://www.apache.org/licenses/LICENSE-2.0
|
||
#
|
||
# Unless required by applicable law or agreed to in writing, software
|
||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
# See the License for the specific language governing permissions and
|
||
# limitations under the License.
|
||
|
||
import os
|
||
from typing import Dict
|
||
|
||
from fastapi import FastAPI, WebSocket, Request
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from fastapi.responses import FileResponse, JSONResponse
|
||
from fastapi.staticfiles import StaticFiles
|
||
|
||
from kilostar.utils.standalone_proxy import _STANDALONE
|
||
from kilostar.utils.settings import get_settings
|
||
|
||
if not _STANDALONE:
|
||
from ray import serve
|
||
|
||
from .agent import agent_router
|
||
from .auth import auth_router
|
||
from .system import system_router, system_api_router
|
||
from .platform.frontend import client_router
|
||
from .platform.onebot import onebot_router
|
||
from .provider import provider_router
|
||
from .resource import resource_router
|
||
from .workflow import workflow_router
|
||
from .chat import chat_router
|
||
from kilostar.utils.error import (
|
||
KiloStarError,
|
||
BusinessError,
|
||
InfraError,
|
||
)
|
||
from kilostar.utils.logger import get_logger
|
||
from kilostar.utils.request_context import (
|
||
bind_request_id,
|
||
new_request_id,
|
||
reset_request_id,
|
||
)
|
||
from kilostar.utils.i18n import t
|
||
|
||
_api_logger = get_logger("api")
|
||
|
||
|
||
def _get_locale(request: Request) -> str | None:
|
||
return request.headers.get("accept-language") or None
|
||
|
||
app = FastAPI()
|
||
|
||
_settings = get_settings()
|
||
_cors_origins_env = _settings.kilostar_cors_origins
|
||
_is_dev = _settings.security.kilostar_env.lower() in ("dev", "development")
|
||
if not _cors_origins_env and _is_dev:
|
||
_cors_origins_env = "*"
|
||
elif not _cors_origins_env:
|
||
_cors_origins_env = "http://localhost:8000"
|
||
|
||
_cors_origins = [o.strip() for o in _cors_origins_env.split(",") if o.strip()]
|
||
_allow_credentials = "*" not in _cors_origins
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=_cors_origins,
|
||
allow_credentials=_allow_credentials,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
|
||
@app.middleware("http")
|
||
async def request_id_middleware(request: Request, call_next):
|
||
"""请求级 ``request_id`` 注入。
|
||
|
||
入口策略:``X-Request-Id`` 头存在则继承(便于网关/前端串联调用链),
|
||
否则生成新的 UUID。退出时把它写到响应头,方便客户端日志对账。
|
||
contextvars 让同一请求生命周期内所有协程的日志都自动带上这个 ID。
|
||
"""
|
||
incoming = request.headers.get("X-Request-Id", "").strip()
|
||
request_id = incoming or new_request_id()
|
||
token = bind_request_id(request_id)
|
||
try:
|
||
response = await call_next(request)
|
||
finally:
|
||
reset_request_id(token)
|
||
response.headers["X-Request-Id"] = request_id
|
||
return response
|
||
|
||
app.include_router(system_router) # 健康探针
|
||
app.include_router(system_api_router) # 系统信息(/api/v1/system)
|
||
app.include_router(client_router) # 客户端路径
|
||
app.include_router(onebot_router) # OneBot v11 路径
|
||
app.include_router(auth_router) # 用户路径
|
||
app.include_router(provider_router) # 供应商路径
|
||
app.include_router(resource_router) # 资源路径
|
||
app.include_router(agent_router) # agent路径
|
||
app.include_router(workflow_router) # workflow路径
|
||
app.include_router(chat_router) # chat路径
|
||
|
||
|
||
@app.exception_handler(BusinessError)
|
||
async def business_error_handler(request: Request, exc: BusinessError):
|
||
"""业务可预期错误:按 ``http_status`` 返回 4xx,附 ``code`` + 异常消息。"""
|
||
return JSONResponse(
|
||
status_code=exc.http_status,
|
||
content={"code": exc.code, "message": str(exc) or exc.code},
|
||
)
|
||
|
||
|
||
@app.exception_handler(InfraError)
|
||
async def infra_error_handler(request: Request, exc: InfraError):
|
||
"""系统失败错误:落日志后返回脱敏的 5xx。"""
|
||
_api_logger.exception(
|
||
f"InfraError on {request.method} {request.url.path}: {exc}"
|
||
)
|
||
loc = _get_locale(request)
|
||
return JSONResponse(
|
||
status_code=exc.http_status,
|
||
content={"code": exc.code, "message": t("internal_error", accept_language=loc)},
|
||
)
|
||
|
||
|
||
@app.exception_handler(Exception)
|
||
async def unhandled_exception_handler(request: Request, exc: Exception):
|
||
"""全局兜底:未预期的异常落日志后返回脱敏的 500,避免泄露 traceback。"""
|
||
_api_logger.exception(
|
||
f"Unhandled exception on {request.method} {request.url.path}: {exc}"
|
||
)
|
||
loc = _get_locale(request)
|
||
return JSONResponse(
|
||
status_code=500,
|
||
content={"code": "internal_error", "message": t("internal_error", accept_language=loc)},
|
||
)
|
||
|
||
|
||
base_dir = os.path.dirname(
|
||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
)
|
||
frontend_dir = os.path.join(base_dir, "frontend", "dist")
|
||
|
||
if os.path.exists(frontend_dir):
|
||
app.mount(
|
||
"/assets",
|
||
StaticFiles(directory=os.path.join(frontend_dir, "assets")),
|
||
name="assets",
|
||
)
|
||
|
||
@app.get("/favicon.svg", include_in_schema=False)
|
||
async def serve_favicon():
|
||
return FileResponse(os.path.join(frontend_dir, "favicon.svg"))
|
||
|
||
@app.get("/icons.svg", include_in_schema=False)
|
||
async def serve_icons():
|
||
return FileResponse(os.path.join(frontend_dir, "icons.svg"))
|
||
|
||
@app.get("/{full_path:path}", include_in_schema=False)
|
||
async def serve_frontend(full_path: str):
|
||
# 【重要安全修复】避免拦截不存在的 API 路由。如果是调用了不存在的 /api/ 接口,直接返回 404,不返回前端页面
|
||
if full_path.startswith("api/"):
|
||
return JSONResponse(
|
||
status_code=404, content={"detail": "API endpoint not found"}
|
||
)
|
||
|
||
index_path = os.path.join(frontend_dir, "index.html")
|
||
if os.path.exists(index_path):
|
||
return FileResponse(index_path)
|
||
return JSONResponse(
|
||
status_code=404, content={"detail": t("frontend_not_found")}
|
||
)
|
||
else:
|
||
import logging
|
||
|
||
logging.getLogger("kilostar").warning(
|
||
f"Frontend dist folder not found at {frontend_dir}. Skipping frontend mount."
|
||
)
|
||
|
||
|
||
if not _STANDALONE:
|
||
@serve.deployment
|
||
@serve.ingress(app)
|
||
class KiloStarGateway:
|
||
gateway: Dict[str, WebSocket]
|
||
|
||
def __init__(self):
|
||
self.gateway = {}
|