fix(chat): 助手身份显示当前智能体名称/头像,无则回退云超服

问题:聊天区助手名称/头像沿用上游硬编码 nick='QwenPaw'、
avatar='/qwenpaw.png'。前者把当前智能体(如'小园')显示成了'QwenPaw';
后者资源未随迁移携带,dev 下实际返回 SPA index.html(头像渲染破损)。

修复:
- 前端 Chat 页:welcome.nick/avatar 改为解析当前智能体——名称取
  selectedAgent.name,头像取 selectedAgent.avatar;智能体无名称/头像时
  回退云超服品牌 BRAND.name / BRAND.assistantAvatar(/pineagents-icon.svg)。
- 支持智能体头像字段(端到端):后端 AgentProfileConfig.avatar、
  AgentSummary.avatar、agents list/create/update 透传、agent_sync
  IDENTITY_FIELDS 加 avatar(云超服身份同步);前端 AgentSummary/
  AgentProfileConfig/CreateAgentRequest 类型补 avatar。
- branding.ts 新增 assistantAvatar 品牌头像常量(替代缺失的 /qwenpaw.png)。

验证:后端重启后 /agents 返回 avatar 字段;/pineagents-icon.svg 200
image/svg+xml(原 /qwenpaw.png 实为 SPA fallback 破损);前端 tsc 0 错;
Chat/agents/constants 32 文件 322 测试全过。
This commit is contained in:
Pine
2026-09-04 17:11:29 +08:00
parent 29b1c7a3da
commit 932fe54c16
6 changed files with 35 additions and 5 deletions
+4
View File
@@ -17,6 +17,8 @@ export type AgentStartupStatus =
export interface AgentSummary {
id: string;
name: string;
/** 智能体头像(URL 路径/数据 URL);空串表示无,由调用方回退云超服品牌头像。 */
avatar?: string;
description: string;
workspace_dir: string;
enabled: boolean;
@@ -103,6 +105,7 @@ export interface MemoryGraphSnapshot {
export interface AgentProfileConfig {
id: string;
name: string;
avatar?: string;
description?: string;
workspace_dir?: string;
backend?: AgentBackend;
@@ -145,6 +148,7 @@ export interface AgentModelSettingsPatch {
export interface CreateAgentRequest {
id?: string;
name: string;
avatar?: string;
description?: string;
workspace_dir?: string;
language?: string;
+3
View File
@@ -5,6 +5,9 @@
export const BRAND = {
name: "云超服OPC", // 产品名(对外统一品牌;上游:QwenPaw→云超服)
fullName: "云南省超级个体服务平台", // 品牌全称(正式场合使用)
// 云超服品牌头像:智能体无头像时聊天等场景回退到它
// (上游缺省是 /qwenpaw.png,本地未随迁移携带该资源)
assistantAvatar: "/pineagents-icon.svg",
cli: "pineagents", // CLI 命令(上游:qwenpaw/copaw
packageName: "pineagents", // 后端包名(已彻底改名)
envPrefix: "PINEAGENTS_", // 环境变量主前缀
+8 -2
View File
@@ -38,6 +38,7 @@ import { chatApi } from "../../api/modules/chat";
import { agentApi } from "../../api/modules/agent";
import { skillApi } from "../../api/modules/skill";
import { getApiUrl } from "../../api/config";
import { BRAND } from "../../constants/branding";
import { buildAuthHeaders } from "../../api/authHeaders";
import { providerApi } from "../../api/modules/provider";
import type { ProviderInfo, ModelInfo, SkillSpec } from "../../api/types";
@@ -1279,6 +1280,11 @@ export default function ChatPage() {
}>
>([]);
const selectedAgentInfo = agents.find((agent) => agent.id === selectedAgent);
// 聊天助手身份:优先当前智能体的名称/头像;智能体无名称/头像时
// 回退云超服品牌身份(BRAND.name / BRAND.assistantAvatar)。
const chatAssistantNick = selectedAgentInfo?.name?.trim() || BRAND.name;
const chatAssistantAvatar =
selectedAgentInfo?.avatar?.trim() || BRAND.assistantAvatar;
const selectedAgentBackend = selectedAgentInfo?.backend ?? "qwenpaw";
const backendCapabilities = selectedAgentInfo?.backend_capabilities;
const usesQwenPawBackend = requiresQwenPawModel(selectedAgentBackend);
@@ -3152,8 +3158,8 @@ export default function ChatPage() {
},
welcome: {
...i18nConfig.welcome,
nick: extNick ?? "QwenPaw",
avatar: extAvatar ?? "/qwenpaw.png",
nick: extNick ?? chatAssistantNick,
avatar: extAvatar ?? chatAssistantAvatar,
...(extGreeting !== undefined ? { greeting: extGreeting } : {}),
...(extDescription !== undefined
? { description: extDescription }
+3 -1
View File
@@ -17,7 +17,7 @@ from .server_client import forward
logger = logging.getLogger(__name__)
# 本地 agent.json 中需要随服务端身份同步的字段
IDENTITY_FIELDS = ("name", "description", "language", "model_name")
IDENTITY_FIELDS = ("name", "avatar", "description", "language", "model_name")
async def fetch_server_agents(auth_header: str) -> list[dict]:
@@ -70,6 +70,7 @@ async def create_server_agent(
description: str = "",
language: str = "zh",
model_name: str = "",
avatar: str = "",
) -> dict:
"""在服务端创建智能体身份,返回服务端生成的记录。"""
return await forward(
@@ -80,6 +81,7 @@ async def create_server_agent(
"description": description,
"language": language or "zh",
"model_name": model_name,
"avatar": avatar,
},
auth_header=auth_header,
)
+9 -2
View File
@@ -52,6 +52,7 @@ class AgentSummary(BaseModel):
id: str
name: str
avatar: str = ""
description: str
workspace_dir: str
enabled: bool
@@ -95,6 +96,7 @@ class CreateAgentRequest(BaseModel):
id: str | None = None
name: str
avatar: str = ""
description: str = ""
workspace_dir: str | None = None
language: str | None = None
@@ -330,6 +332,8 @@ async def list_agents(request: Request = None) -> AgentListResponse:
(server_info or {}).get("name")
or agent_config.name
),
avatar=(server_info or {}).get("avatar")
or agent_config.avatar,
description=description,
workspace_dir=agent_ref.workspace_dir,
enabled=enabled,
@@ -358,6 +362,7 @@ async def list_agents(request: Request = None) -> AgentListResponse:
AgentSummary(
id=agent_id,
name=agent_id.title(),
avatar="",
description="",
workspace_dir=agent_ref.workspace_dir,
enabled=enabled,
@@ -549,6 +554,7 @@ async def create_agent(
name=request.name,
description=request.description,
language=request.language or config.agents.language or "zh",
avatar=getattr(request, "avatar", "") or "",
)
new_id = server_info["id"]
@@ -589,6 +595,7 @@ async def create_agent(
agent_config = AgentProfileConfig(
id=new_id,
name=server_info.get("name") or request.name,
avatar=server_info.get("avatar") or getattr(request, "avatar", "") or "",
description=server_info.get("description") or request.description,
workspace_dir=str(workspace_dir),
backend=request.backend,
@@ -823,7 +830,7 @@ async def update_agent(
identity_patch = {
k: v
for k, v in update_data.items()
if k in ("name", "description", "language")
if k in ("name", "avatar", "description", "language")
}
if identity_patch:
server_info = await update_server_agent(
@@ -832,7 +839,7 @@ async def update_agent(
identity_patch,
)
if server_info:
for key in ("name", "description", "language"):
for key in ("name", "avatar", "description", "language"):
if server_info.get(key) is not None:
update_data[key] = server_info[key]
+8
View File
@@ -1676,6 +1676,14 @@ class AgentProfileConfig(BaseModel):
id: str = Field(..., description="Unique agent ID")
name: str = Field(..., description="Human-readable agent name")
avatar: str = Field(
default="",
description=(
"Agent avatar. URL path (e.g. '/logo.png') or data URL; empty "
"means the agent has no avatar and callers fall back to the "
"product (云超服) brand identity."
),
)
description: str = Field(default="", description="Agent description")
workspace_dir: str = Field(
default="",