feat: v0.1.1 迭代——人设外键重构、Chat UI优化、意识节点防幻觉、日志双视图

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>
This commit is contained in:
2026-06-05 06:18:47 +00:00
parent e3b8686d45
commit 6f1bc27101
39 changed files with 2904 additions and 524 deletions
+3 -2
View File
@@ -28,7 +28,7 @@ if not _STANDALONE:
from .agent import agent_router
from .auth import auth_router
from .system import system_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
@@ -93,7 +93,8 @@ async def request_id_middleware(request: Request, call_next):
response.headers["X-Request-Id"] = request_id
return response
app.include_router(system_router) # 健康探针 + 系统信息
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) # 用户路径
+19 -48
View File
@@ -36,7 +36,7 @@ class AgentRegister(BaseModel):
model_id: str
individual_name: str
tools: Optional[List[str]] = None
custom_system_prompt: Optional[str] = None
persona_id: Optional[str] = None
display_name: Optional[str] = None
@@ -79,13 +79,19 @@ async def load_agent(
agent_register.provider_title,
agent_register.model_id,
agent_register.tools,
agent_register.custom_system_prompt,
agent_register.persona_id,
agent_register.display_name,
)
scope = agent_register.individual_name
toolsets = await get_all_toolsets_for_scope(scope)
custom_prompt = agent_register.custom_system_prompt
# Resolve persona system_prompt from DB
persona_prompt = None
if agent_register.persona_id:
tpl = await postgres_database.get_template.remote(agent_register.persona_id)
if tpl:
persona_prompt = tpl.system_prompt
match scope:
case "regulatory_node":
@@ -97,7 +103,7 @@ async def load_agent(
agent_register.tools,
toolsets,
accept_lang,
custom_prompt,
persona_prompt,
)
case "consciousness_node":
node = ray_actor_hook("consciousness_node").consciousness_node
@@ -108,7 +114,7 @@ async def load_agent(
agent_register.tools,
toolsets,
accept_lang,
custom_prompt,
persona_prompt,
)
case _:
pass
@@ -130,7 +136,7 @@ class WorkerIndividualCreate(BaseModel):
description: str
provider_title: str
model_id: str
system_prompt: str
persona_id: str
output_template: dict
bound_skill: Dict[str, List[str]]
workspace: List[str]
@@ -153,7 +159,7 @@ class WorkerIndividualUpdate(BaseModel):
description: Optional[str] = None
provider_title: Optional[str] = None
model_id: Optional[str] = None
system_prompt: Optional[str] = None
persona_id: Optional[str] = None
output_template: Optional[dict] = None
bound_skill: Optional[Dict[str, List[str]]] = None
workspace: Optional[List[str]] = None
@@ -280,34 +286,21 @@ async def delete_worker_individual(
class PersonaTemplateCreate(BaseModel):
name: str
description: str = ""
system_prompt: str = ""
agent_type: AgentType = "ordinary"
provider_title: Optional[str] = None
model_id: Optional[str] = None
tools: Optional[List[str]] = None
tags: Optional[List[str]] = None
class PersonaTemplateUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
system_prompt: Optional[str] = None
agent_type: Optional[AgentType] = None
provider_title: Optional[str] = None
model_id: Optional[str] = None
tools: Optional[List[str]] = None
tags: Optional[List[str]] = None
@agent_router.get("/template")
async def list_templates(
include_builtin: bool = True,
token_data: TokenData = Depends(Accessor.get_current_user),
):
postgres_database = ray_actor_hook("postgres_database").postgres_database
templates = await postgres_database.list_templates.remote(
owner_id=token_data.user_id, include_builtin=include_builtin
owner_id=token_data.user_id
)
return {"templates": templates}
@@ -319,7 +312,9 @@ async def create_template(
):
postgres_database = ray_actor_hook("postgres_database").postgres_database
tpl = await postgres_database.add_template.remote(
**data.model_dump(), owner_id=token_data.user_id, is_builtin=False
name=data.name,
system_prompt=data.system_prompt,
owner_id=token_data.user_id,
)
return {"message": "success", "template_id": tpl.template_id}
@@ -334,7 +329,7 @@ async def update_template(
tpl = await postgres_database.get_template.remote(template_id)
if not tpl:
raise HTTPException(status_code=404, detail="Template not found")
if not tpl.is_builtin and tpl.owner_id != token_data.user_id:
if tpl.owner_id != token_data.user_id:
raise HTTPException(status_code=403, detail="Forbidden")
updated = await postgres_database.update_template.remote(
template_id, **data.model_dump(exclude_unset=True)
@@ -351,31 +346,7 @@ async def delete_template(
tpl = await postgres_database.get_template.remote(template_id)
if not tpl:
raise HTTPException(status_code=404, detail="Template not found")
if tpl.is_builtin or tpl.owner_id != token_data.user_id:
if tpl.owner_id != token_data.user_id:
raise HTTPException(status_code=403, detail="Forbidden")
await postgres_database.delete_template.remote(template_id)
return {"message": "success"}
@agent_router.post("/worker/from-template/{template_id}")
async def create_worker_from_template(
template_id: str,
token_data: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
):
"""从人设模板快速创建一个 Worker Agent,字段直接从模板复制。"""
postgres_database = ray_actor_hook("postgres_database").postgres_database
tpl = await postgres_database.get_template.remote(template_id)
if not tpl:
raise HTTPException(status_code=404, detail="Template not found")
worker = await postgres_database.add_worker_individual.remote(
agent_name=tpl.name,
agent_type=tpl.agent_type,
description=tpl.description,
system_prompt=tpl.system_prompt,
provider_title=tpl.provider_title or "",
model_id=tpl.model_id or "",
tools=tpl.tools or [],
owner_id=token_data.user_id,
template_origin_id=template_id,
)
return {"message": "success", "agent_id": worker.agent_id}
+10 -3
View File
@@ -38,9 +38,16 @@ async def create_user(user_register: UserRegister, request: Request):
"""注册新用户:异步线程池里做 argon2 哈希,再交由 PostgresDatabase Actor 落库。"""
register_limiter.check(request)
postgres_database = ray_actor_hook("postgres_database").postgres_database
hashed_password = await run_in_threadpool(
Accessor.hash_password, user_register.password
)
try:
hashed_password = await run_in_threadpool(
Accessor.hash_password, user_register.password
)
except ValueError as e:
from fastapi.responses import JSONResponse
return JSONResponse(
status_code=400,
content={"code": "password_invalid", "message": str(e)},
)
user = await postgres_database.add_user.remote(
user_register.user_name, hashed_password
)
+119 -1
View File
@@ -12,7 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from fastapi import APIRouter, Depends
import json
import asyncio
import httpx
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from kilostar.utils.ray_hook import ray_actor_hook
from kilostar.utils.access import Accessor, TokenData
@@ -136,3 +140,117 @@ async def send_chat_message(
)
return {"reply": response_msg}
@chat_router.delete("/{chat_id}")
async def delete_chat_session(
chat_id: str,
token_data: TokenData = Depends(Accessor.get_current_user),
):
postgres_database = ray_actor_hook("postgres_database").postgres_database
session = await postgres_database.get_chat_session.remote(chat_id=chat_id)
if not session:
raise HTTPException(status_code=404, detail="Chat session not found")
if session.user_id != token_data.user_id:
raise HTTPException(status_code=403, detail="Forbidden")
await postgres_database.delete_chat_session.remote(chat_id=chat_id)
return {"message": "success"}
@chat_router.post("/{chat_id}/stream")
async def stream_chat_message(
chat_id: str,
request_body: SendMessageRequest,
request: Request,
token_data: TokenData = Depends(Accessor.get_current_user),
):
"""SSE 流式聊天端点:逐 token 推送 AI 回复。"""
postgres_database = ray_actor_hook("postgres_database").postgres_database
# 存用户消息
await postgres_database.add_chat_message.remote(
chat_id=chat_id, message=request_body.message, message_owner="user"
)
# 获取 regulatory_node 的 provider 配置
node_config = await postgres_database.get_system_node_config.remote("regulatory_node")
if not node_config:
raise HTTPException(status_code=500, detail="Regulatory node not configured")
# 获取 provider 详情
from kilostar.core.global_state_machine.gsm_snapshot import fetch_snapshot
global_state_machine = ray_actor_hook("global_state_machine").global_state_machine
snapshot = await fetch_snapshot(gsm_actor=global_state_machine)
provider = snapshot.providers.get(node_config.provider_title)
if not provider:
raise HTTPException(status_code=500, detail="Provider not available")
# 加载历史消息作为上下文
history_msgs = await postgres_database.list_chat_messages.remote(chat_id=chat_id)
messages = []
system_prompt = "你是 KiloStar 助手,友善、简洁地回答用户的问题。"
if node_config.persona_id:
tpl = await postgres_database.get_template.remote(node_config.persona_id)
if tpl and tpl.system_prompt:
system_prompt += "\n" + tpl.system_prompt
messages.append({"role": "system", "content": system_prompt})
for msg in history_msgs:
role = "user" if msg.message_owner == "user" else "assistant"
messages.append({"role": role, "content": msg.message})
async def event_generator():
full_response = ""
try:
async with httpx.AsyncClient(timeout=120.0) as client:
url = provider.provider_url.rstrip("/") + "/chat/completions"
payload = {
"model": node_config.model_id,
"messages": messages,
"stream": True,
}
async with client.stream(
"POST",
url,
json=payload,
headers={
"Authorization": f"Bearer {provider.provider_apikey}",
"Content-Type": "application/json",
},
) as resp:
async for line in resp.aiter_lines():
if await request.is_disconnected():
break
if not line.startswith("data: "):
continue
data_str = line[6:]
if data_str.strip() == "[DONE]":
break
try:
chunk = json.loads(data_str)
delta = chunk.get("choices", [{}])[0].get("delta", {})
token = delta.get("content", "")
if token:
full_response += token
yield f"data: {json.dumps({'token': token})}\n\n"
except (json.JSONDecodeError, IndexError, KeyError):
continue
except Exception as e:
from kilostar.utils.logger import get_logger
get_logger("chat_stream").exception(f"Stream error: {e}")
if not full_response:
full_response = "抱歉,生成回复时出错。"
yield f"data: {json.dumps({'token': full_response})}\n\n"
# 流结束,存入数据库
if full_response:
await postgres_database.add_chat_message.remote(
chat_id=chat_id,
message=full_response,
message_owner="regulatory_node",
)
yield f"data: {json.dumps({'done': True, 'full_message': full_response})}\n\n"
return StreamingResponse(event_generator(), media_type="text/event-stream")
+7 -8
View File
@@ -35,6 +35,7 @@ from kilostar.utils.config_loader import (
)
system_router = APIRouter(tags=["system"])
system_api_router = APIRouter(prefix="/api/v1/system", tags=["system"])
@system_router.get("/health/live", include_in_schema=True)
@@ -69,15 +70,15 @@ async def readiness():
)
@system_router.get("/config/workflow")
@system_api_router.get("/config/workflow")
async def get_workflow_config_endpoint(
_: TokenData = Depends(Accessor.get_current_user),
):
config = get_workflow_config()
return {"config": config.model_dump()}
return config.model_dump()
@system_router.put("/config/workflow")
@system_api_router.put("/config/workflow")
async def update_workflow_config_endpoint(
update: WorkflowConfig,
_: TokenData = Depends(RoleChecker(allowed_roles=UserAuthority.USER)),
@@ -86,7 +87,7 @@ async def update_workflow_config_endpoint(
return {"status": "ok", "config": update.model_dump()}
@system_router.get("/logs")
@system_api_router.get("/logs")
async def query_system_logs(
trace_id: str | None = None,
event_type: str | None = None,
@@ -95,9 +96,7 @@ async def query_system_logs(
offset: int = 0,
_: TokenData = Depends(Accessor.get_current_user),
):
from kilostar.utils.ray_hook import ray_actor_hook
pg = await ray_actor_hook.get_actor("postgres_database")
pg = ray_actor_hook("postgres_database").postgres_database
logs = await pg.query_event_logs.remote(
trace_id=trace_id,
event_type=event_type,
@@ -108,7 +107,7 @@ async def query_system_logs(
return {"logs": logs, "count": len(logs)}
@system_router.get("/api/v1/system/node-labels")
@system_api_router.get("/node-labels")
async def get_node_labels(
_: TokenData = Depends(Accessor.get_current_user),
):