mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(agents): pixelart icons, per-role colors, runtime identity merge, locale templates
This commit is contained in:
parent
2a8f6774e7
commit
be1fc86836
@ -918,11 +918,26 @@ public class AgentGraphBuilder {
|
||||
|
||||
private String buildEnhancedPrompt(AgentEntity entity, boolean builtinSearchEnabled,
|
||||
Set<String> boundTools, Integer maxInputTokens) {
|
||||
// 通过 MemoryManager 从所有 MemoryProvider 组装系统提示词(快照冻结)
|
||||
// The agent's own systemPrompt encodes its identity (role / goal /
|
||||
// backstory). The memory block from workspace files (AGENTS.md, SOUL.md,
|
||||
// PROFILE.md, MEMORY.md, ...) augments that identity with durable
|
||||
// context. Both are independently optional, but when both exist they
|
||||
// must be joined — earlier this branch picked memory and silently
|
||||
// dropped the identity prompt, so editor-side identity changes never
|
||||
// reached runtime if the agent had any workspace files.
|
||||
String identityPrompt = entity.getSystemPrompt() != null ? entity.getSystemPrompt().trim() : "";
|
||||
String memoryPrompt = memoryManager.buildSystemPromptBlock(entity.getId());
|
||||
String basePrompt = (memoryPrompt != null && !memoryPrompt.isBlank())
|
||||
? memoryPrompt
|
||||
: (entity.getSystemPrompt() != null ? entity.getSystemPrompt() : "");
|
||||
StringBuilder basePromptBuilder = new StringBuilder();
|
||||
if (!identityPrompt.isEmpty()) {
|
||||
basePromptBuilder.append(identityPrompt);
|
||||
}
|
||||
if (memoryPrompt != null && !memoryPrompt.isBlank()) {
|
||||
if (basePromptBuilder.length() > 0) {
|
||||
basePromptBuilder.append("\n\n");
|
||||
}
|
||||
basePromptBuilder.append(memoryPrompt);
|
||||
}
|
||||
String basePrompt = basePromptBuilder.toString();
|
||||
|
||||
// 使用 skill runtime 构建技能增强(per-agent 绑定过滤)
|
||||
Set<Long> boundSkillIds = agentBindingService.getBoundSkillIds(entity.getId());
|
||||
|
||||
@ -42,10 +42,15 @@ public class TemplateController {
|
||||
public R<AgentEntity> apply(
|
||||
@PathVariable String id,
|
||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId,
|
||||
// Accept-Language is forwarded by the frontend (zh-CN, zh, en, en-US, ...)
|
||||
// so the new agent's display name matches the user's locale —
|
||||
// a Chinese user hiring "客服助理" should not get an English
|
||||
// "Customer Support" agent in their list.
|
||||
@RequestHeader(value = "Accept-Language", required = false) String acceptLanguage,
|
||||
Authentication auth) {
|
||||
long wsId = workspaceId != null ? workspaceId : 1L;
|
||||
Long userId = resolveUserId(auth);
|
||||
return R.ok(templateService.applyTemplate(id, wsId, userId));
|
||||
return R.ok(templateService.applyTemplate(id, wsId, userId, acceptLanguage));
|
||||
}
|
||||
|
||||
private Long resolveUserId(Authentication auth) {
|
||||
|
||||
@ -63,24 +63,45 @@ public class TemplateService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用模板创建 Agent 及其工作区文件
|
||||
*
|
||||
* @param templateId 模板 ID
|
||||
* @param workspaceId 目标工作区 ID(来自 X-Workspace-Id header)
|
||||
* @param creatorUserId 当前用户 ID(用于 RFC-077 创建者归属)
|
||||
* @return 创建的 AgentEntity
|
||||
* Backwards-compatible overload that defaults to the template's English
|
||||
* display strings (existing callers without locale context).
|
||||
*/
|
||||
@Transactional
|
||||
public AgentEntity applyTemplate(String templateId, Long workspaceId, Long creatorUserId) {
|
||||
return applyTemplate(templateId, workspaceId, creatorUserId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a template, picking name/description in the caller's preferred
|
||||
* language so the resulting agent reads natively in their locale. Falls
|
||||
* back to the template's primary (English) fields when the localized
|
||||
* variant is missing or no locale was supplied.
|
||||
*
|
||||
* @param templateId template ID
|
||||
* @param workspaceId target workspace ID (from X-Workspace-Id header)
|
||||
* @param creatorUserId current user ID (creator attribution)
|
||||
* @param acceptLanguage raw Accept-Language header; null/blank → English
|
||||
*/
|
||||
@Transactional
|
||||
public AgentEntity applyTemplate(String templateId, Long workspaceId, Long creatorUserId, String acceptLanguage) {
|
||||
TemplateDTO template = listTemplates().stream()
|
||||
.filter(t -> t.getId().equals(templateId))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new MateClawException("err.agent.template_not_found", "模板不存在: " + templateId));
|
||||
|
||||
// 1. 创建 Agent — RFC-077: 显式注入 workspaceId/creatorUserId,避免 DB 默认值兜底成 1(issue #26 Bug A)
|
||||
boolean preferZh = isChineseLocale(acceptLanguage);
|
||||
String displayName = preferZh && template.getNameZh() != null && !template.getNameZh().isBlank()
|
||||
? template.getNameZh()
|
||||
: template.getName();
|
||||
String displayDesc = preferZh && template.getDescriptionZh() != null && !template.getDescriptionZh().isBlank()
|
||||
? template.getDescriptionZh()
|
||||
: template.getDescription();
|
||||
|
||||
// 1. Create the Agent. workspaceId/creatorUserId are passed in
|
||||
// explicitly so the DB default does not silently fall back to 1.
|
||||
AgentEntity agent = new AgentEntity();
|
||||
agent.setName(template.getName());
|
||||
agent.setDescription(template.getDescription());
|
||||
agent.setName(displayName);
|
||||
agent.setDescription(displayDesc);
|
||||
agent.setAgentType(template.getAgentType());
|
||||
agent.setIcon(template.getIcon());
|
||||
agent.setTags(template.getTags());
|
||||
@ -116,4 +137,15 @@ public class TemplateService {
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the raw Accept-Language header best-matches a Chinese locale.
|
||||
* Implementation is intentionally simple — we only need to disambiguate
|
||||
* "Chinese vs not" for picking nameZh / descriptionZh.
|
||||
*/
|
||||
private boolean isChineseLocale(String acceptLanguage) {
|
||||
if (acceptLanguage == null || acceptLanguage.isBlank()) return false;
|
||||
String first = acceptLanguage.split(",")[0].trim().toLowerCase();
|
||||
return first.startsWith("zh");
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,21 +10,21 @@ MERGE INTO mate_agent (id, name, description, agent_type, system_prompt, model_n
|
||||
KEY (id)
|
||||
VALUES (1000000001, 'MateClaw Assistant', 'Default AI assistant with ReAct mode and tool calling', 'react',
|
||||
'You are MateClaw, an intelligent AI assistant. You can help users answer questions, analyze data, and execute tasks. Please respond professionally and in a friendly manner.',
|
||||
NULL, 100, TRUE, '🤖', 'default,assistant', NOW(), NOW(), 0);
|
||||
NULL, 100, TRUE, 'pi:robot-face-happy', 'default,assistant', NOW(), NOW(), 0);
|
||||
|
||||
-- Default Agent: Task Planner (Plan-Execute mode)
|
||||
MERGE INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted)
|
||||
KEY (id)
|
||||
VALUES (1000000002, 'Task Planner', 'Task planning assistant for complex multi-step tasks', 'plan_execute',
|
||||
'You are a professional task planning and execution assistant. You excel at breaking complex goals into executable steps and completing them systematically.',
|
||||
NULL, 100, TRUE, '📋', 'planning,task', NOW(), NOW(), 0);
|
||||
NULL, 100, TRUE, 'pi:clipboard-note', 'planning,task', NOW(), NOW(), 0);
|
||||
|
||||
-- StateGraph ReAct Agent (StateGraph architecture)
|
||||
MERGE INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted)
|
||||
KEY (id)
|
||||
VALUES (1000000003, 'StateGraph ReAct', 'StateGraph-based ReAct Agent with explicit reasoning loops and tool calling', 'react',
|
||||
'You are an intelligent assistant based on the StateGraph architecture. You can use tools to help users solve problems. Please respond professionally and in a friendly manner.',
|
||||
NULL, 100, TRUE, '🔄', 'react,stategraph,tools', NOW(), NOW(), 0);
|
||||
NULL, 100, TRUE, 'pi:cpu', 'react,stategraph,tools', NOW(), NOW(), 0);
|
||||
|
||||
-- ==================== Local Model Providers (displayed first) ====================
|
||||
|
||||
|
||||
@ -9,21 +9,21 @@ ON DUPLICATE KEY UPDATE username=VALUES(username), password=VALUES(password), ni
|
||||
INSERT INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted)
|
||||
VALUES (1000000001, 'MateClaw Assistant', 'Default AI assistant with ReAct mode and tool calling', 'react',
|
||||
'You are MateClaw, an intelligent AI assistant. You can help users answer questions, analyze data, and execute tasks. Please respond professionally and in a friendly manner.',
|
||||
NULL, 100, TRUE, '🤖', 'default,assistant', NOW(), NOW(), 0)
|
||||
NULL, 100, TRUE, 'pi:robot-face-happy', 'default,assistant', NOW(), NOW(), 0)
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), agent_type=VALUES(agent_type), system_prompt=VALUES(system_prompt), model_name=VALUES(model_name), max_iterations=VALUES(max_iterations), enabled=VALUES(enabled), icon=VALUES(icon), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
|
||||
|
||||
-- Default Agent: Task Planner (Plan-Execute mode)
|
||||
INSERT INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted)
|
||||
VALUES (1000000002, 'Task Planner', 'Task planning assistant for complex multi-step tasks', 'plan_execute',
|
||||
'You are a professional task planning and execution assistant. You excel at breaking complex goals into executable steps and completing them systematically.',
|
||||
NULL, 100, TRUE, '📋', 'planning,task', NOW(), NOW(), 0)
|
||||
NULL, 100, TRUE, 'pi:clipboard-note', 'planning,task', NOW(), NOW(), 0)
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), agent_type=VALUES(agent_type), system_prompt=VALUES(system_prompt), model_name=VALUES(model_name), max_iterations=VALUES(max_iterations), enabled=VALUES(enabled), icon=VALUES(icon), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
|
||||
|
||||
-- StateGraph ReAct Agent (StateGraph architecture)
|
||||
INSERT INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted)
|
||||
VALUES (1000000003, 'StateGraph ReAct', 'StateGraph-based ReAct Agent with explicit reasoning loops and tool calling', 'react',
|
||||
'You are an intelligent assistant based on the StateGraph architecture. You can use tools to help users solve problems. Please respond professionally and in a friendly manner.',
|
||||
NULL, 100, TRUE, '🔄', 'react,stategraph,tools', NOW(), NOW(), 0)
|
||||
NULL, 100, TRUE, 'pi:cpu', 'react,stategraph,tools', NOW(), NOW(), 0)
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), agent_type=VALUES(agent_type), system_prompt=VALUES(system_prompt), model_name=VALUES(model_name), max_iterations=VALUES(max_iterations), enabled=VALUES(enabled), icon=VALUES(icon), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
|
||||
|
||||
-- ==================== Local Model Providers (displayed first) ====================
|
||||
|
||||
@ -9,21 +9,21 @@ ON DUPLICATE KEY UPDATE username=VALUES(username), password=VALUES(password), ni
|
||||
INSERT INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted)
|
||||
VALUES (1000000001, 'MateClaw Assistant', '默认 AI 助手,基于 ReAct 模式,支持工具调用', 'react',
|
||||
'你是 MateClaw,一个智能 AI 助手。你可以帮助用户回答问题、分析数据、执行任务。请用中文回复,保持专业、友好的态度。',
|
||||
NULL, 100, TRUE, '🤖', 'default,assistant', NOW(), NOW(), 0)
|
||||
NULL, 100, TRUE, 'pi:robot-face-happy', 'default,assistant', NOW(), NOW(), 0)
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), agent_type=VALUES(agent_type), system_prompt=VALUES(system_prompt), model_name=VALUES(model_name), max_iterations=VALUES(max_iterations), enabled=VALUES(enabled), icon=VALUES(icon), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
|
||||
|
||||
-- 默认 Agent:任务规划助手(Plan-Execute 模式)
|
||||
INSERT INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted)
|
||||
VALUES (1000000002, 'Task Planner', '任务规划助手,适合复杂多步骤任务', 'plan_execute',
|
||||
'你是一个专业的任务规划和执行助手。你擅长将复杂目标分解为可执行的步骤,并逐步完成。请用中文回复。',
|
||||
NULL, 100, TRUE, '📋', 'planning,task', NOW(), NOW(), 0)
|
||||
NULL, 100, TRUE, 'pi:clipboard-note', 'planning,task', NOW(), NOW(), 0)
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), agent_type=VALUES(agent_type), system_prompt=VALUES(system_prompt), model_name=VALUES(model_name), max_iterations=VALUES(max_iterations), enabled=VALUES(enabled), icon=VALUES(icon), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
|
||||
|
||||
-- StateGraph ReAct Agent(支持 StateGraph 架构)
|
||||
INSERT INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted)
|
||||
VALUES (1000000003, 'StateGraph ReAct', '基于 StateGraph 的 ReAct Agent,支持显式推理循环和工具调用', 'react',
|
||||
'你是基于 StateGraph 架构的智能助手。你可以使用工具来帮助用户解决问题。请用中文回复,保持专业、友好的态度。',
|
||||
NULL, 100, TRUE, '🔄', 'react,stategraph,tools', NOW(), NOW(), 0)
|
||||
NULL, 100, TRUE, 'pi:cpu', 'react,stategraph,tools', NOW(), NOW(), 0)
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), agent_type=VALUES(agent_type), system_prompt=VALUES(system_prompt), model_name=VALUES(model_name), max_iterations=VALUES(max_iterations), enabled=VALUES(enabled), icon=VALUES(icon), tags=VALUES(tags), update_time=VALUES(update_time), deleted=VALUES(deleted);
|
||||
|
||||
-- ==================== 本地模型 Provider(优先展示) ====================
|
||||
|
||||
@ -10,21 +10,21 @@ MERGE INTO mate_agent (id, name, description, agent_type, system_prompt, model_n
|
||||
KEY (id)
|
||||
VALUES (1000000001, 'MateClaw Assistant', '默认 AI 助手,基于 ReAct 模式,支持工具调用', 'react',
|
||||
'你是 MateClaw,一个智能 AI 助手。你可以帮助用户回答问题、分析数据、执行任务。请用中文回复,保持专业、友好的态度。',
|
||||
NULL, 100, TRUE, '🤖', 'default,assistant', NOW(), NOW(), 0);
|
||||
NULL, 100, TRUE, 'pi:robot-face-happy', 'default,assistant', NOW(), NOW(), 0);
|
||||
|
||||
-- 默认 Agent:任务规划助手(Plan-Execute 模式)
|
||||
MERGE INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted)
|
||||
KEY (id)
|
||||
VALUES (1000000002, 'Task Planner', '任务规划助手,适合复杂多步骤任务', 'plan_execute',
|
||||
'你是一个专业的任务规划和执行助手。你擅长将复杂目标分解为可执行的步骤,并逐步完成。请用中文回复。',
|
||||
NULL, 100, TRUE, '📋', 'planning,task', NOW(), NOW(), 0);
|
||||
NULL, 100, TRUE, 'pi:clipboard-note', 'planning,task', NOW(), NOW(), 0);
|
||||
|
||||
-- StateGraph ReAct Agent(支持 StateGraph 架构)
|
||||
MERGE INTO mate_agent (id, name, description, agent_type, system_prompt, model_name, max_iterations, enabled, icon, tags, create_time, update_time, deleted)
|
||||
KEY (id)
|
||||
VALUES (1000000003, 'StateGraph ReAct', '基于 StateGraph 的 ReAct Agent,支持显式推理循环和工具调用', 'react',
|
||||
'你是基于 StateGraph 架构的智能助手。你可以使用工具来帮助用户解决问题。请用中文回复,保持专业、友好的态度。',
|
||||
NULL, 100, TRUE, '🔄', 'react,stategraph,tools', NOW(), NOW(), 0);
|
||||
NULL, 100, TRUE, 'pi:cpu', 'react,stategraph,tools', NOW(), NOW(), 0);
|
||||
|
||||
-- ==================== 本地模型 Provider(优先展示) ====================
|
||||
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
-- Convert the three default seed agents from emoji to pixelarticons icons.
|
||||
-- The card UI now treats `pi:<name>` as an inline pixel-art SVG, so the
|
||||
-- defaults need to match. Guarded with the original emoji so a user who
|
||||
-- already customised the icon keeps their choice; non-seed (user-created)
|
||||
-- agents are intentionally untouched.
|
||||
UPDATE mate_agent SET icon = 'pi:robot-face-happy' WHERE id = 1000000001 AND icon = '🤖';
|
||||
UPDATE mate_agent SET icon = 'pi:clipboard-note' WHERE id = 1000000002 AND icon = '📋';
|
||||
UPDATE mate_agent SET icon = 'pi:cpu' WHERE id = 1000000003 AND icon = '🔄';
|
||||
@ -0,0 +1,8 @@
|
||||
-- Convert the three default seed agents from emoji to pixelarticons icons.
|
||||
-- The card UI now treats `pi:<name>` as an inline pixel-art SVG, so the
|
||||
-- defaults need to match. Guarded with the original emoji so a user who
|
||||
-- already customised the icon keeps their choice; non-seed (user-created)
|
||||
-- agents are intentionally untouched.
|
||||
UPDATE mate_agent SET icon = 'pi:robot-face-happy' WHERE id = 1000000001 AND icon = '🤖';
|
||||
UPDATE mate_agent SET icon = 'pi:clipboard-note' WHERE id = 1000000002 AND icon = '📋';
|
||||
UPDATE mate_agent SET icon = 'pi:cpu' WHERE id = 1000000003 AND icon = '🔄';
|
||||
@ -4,7 +4,7 @@
|
||||
"nameZh": "代码审查员",
|
||||
"description": "Expert code reviewer. Reads code, identifies issues, suggests improvements.",
|
||||
"descriptionZh": "代码审查专家。阅读代码、发现问题、提出改进建议。",
|
||||
"icon": "🔍",
|
||||
"icon": "pi:bug",
|
||||
"agentType": "react",
|
||||
"tags": "code,review,developer",
|
||||
"maxIterations": 10,
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"nameZh": "客服助理",
|
||||
"description": "Empathy first, solution second — answers customers in 5 minutes without sounding like a script.",
|
||||
"descriptionZh": "先共情,再解决。5 分钟内给客户一个不像机器的答复。",
|
||||
"icon": "💬",
|
||||
"icon": "pi:headphone",
|
||||
"agentType": "react",
|
||||
"tags": "support,customer,operations",
|
||||
"maxIterations": 10,
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"nameZh": "数据分析师",
|
||||
"description": "Turns business data into actionable insights — asks the right question first, writes SQL second.",
|
||||
"descriptionZh": "把业务数据变成可执行的洞察。先问对问题,再写 SQL。",
|
||||
"icon": "📈",
|
||||
"icon": "pi:chart-bar-big",
|
||||
"agentType": "react",
|
||||
"tags": "data,analysis,sql",
|
||||
"maxIterations": 12,
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"nameZh": "通用助手",
|
||||
"description": "A versatile AI assistant for everyday tasks — search, write, analyze, and more.",
|
||||
"descriptionZh": "通用 AI 助手,适合日常任务——搜索、写作、分析等。",
|
||||
"icon": "🤖",
|
||||
"icon": "pi:robot-face-happy",
|
||||
"agentType": "react",
|
||||
"tags": "general,assistant,default",
|
||||
"maxIterations": 10,
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"nameZh": "产品助理",
|
||||
"description": "Turns fuzzy requests into clear PRDs — always asks 'who is the user and what do they want' first.",
|
||||
"descriptionZh": "把模糊需求理清楚。永远先问\"用户是谁、他想干嘛\"。",
|
||||
"icon": "📝",
|
||||
"icon": "pi:notes",
|
||||
"agentType": "react",
|
||||
"tags": "product,prd,requirements",
|
||||
"maxIterations": 12,
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"nameZh": "研究分析师",
|
||||
"description": "Breaks down complex research tasks into steps. Uses web search and Wiki for deep analysis.",
|
||||
"descriptionZh": "将复杂研究任务分解为步骤。使用网络搜索和 Wiki 进行深度分析。",
|
||||
"icon": "📊",
|
||||
"icon": "pi:book-open",
|
||||
"agentType": "plan_execute",
|
||||
"tags": "research,analysis,planning",
|
||||
"maxIterations": 20,
|
||||
|
||||
@ -7,7 +7,7 @@ export const http = axios.create({
|
||||
timeout: 30000,
|
||||
})
|
||||
|
||||
// 请求拦截器:注入 Token + Workspace ID
|
||||
// 请求拦截器:注入 Token + Workspace ID + Accept-Language
|
||||
http.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
@ -17,6 +17,15 @@ http.interceptors.request.use((config) => {
|
||||
if (workspaceId) {
|
||||
config.headers['X-Workspace-Id'] = workspaceId
|
||||
}
|
||||
// Forward the user's UI locale so locale-sensitive endpoints (e.g.
|
||||
// template apply) can pick the right display strings. Native browsers
|
||||
// already send Accept-Language, but the user's chosen UI language may
|
||||
// differ from the OS default — explicitly setting it keeps the two
|
||||
// in sync.
|
||||
const locale = localStorage.getItem('mateclaw_locale')
|
||||
if (locale) {
|
||||
config.headers['Accept-Language'] = locale
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
|
||||
@ -76,7 +76,10 @@ const sizeClass = computed(() => {
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
color: var(--mc-text-primary);
|
||||
/* Inherit color so parents (e.g. an agent card avatar) can tint the
|
||||
* pixelart SVG via `currentColor`. Without parental color, the icon
|
||||
* still picks up text-primary through normal CSS inheritance. */
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Pixelart SVGs ship with viewBox=0 0 24 24 + fill=currentColor — they
|
||||
|
||||
@ -152,8 +152,8 @@ export default {
|
||||
ttsStop: 'Stop Reading',
|
||||
conversations: 'Conversations',
|
||||
newChat: 'New Chat',
|
||||
loadingAgents: 'Loading agents...',
|
||||
selectAgent: 'Please select an agent',
|
||||
loadingAgents: 'Loading employees...',
|
||||
selectAgent: 'Please select an employee',
|
||||
noConversations: 'No conversations yet',
|
||||
startNewChat: 'Start a new chat above',
|
||||
messages: '{count} messages',
|
||||
@ -165,7 +165,7 @@ export default {
|
||||
noActiveModel: 'No active model. Go to Model Management to select an available model before chatting.',
|
||||
providerNotReady: 'The active model belongs to {name}, but this provider is not fully configured. Please complete the API Key or Base URL in Model Management.',
|
||||
noAvailableModel: 'No available model.',
|
||||
loadAgentsFailed: 'Failed to load agents',
|
||||
loadAgentsFailed: 'Failed to load employees',
|
||||
loadModelFailed: 'Failed to load model state',
|
||||
loadConversationsFailed: 'Failed to load conversations',
|
||||
loadMessagesFailed: 'Failed to load messages',
|
||||
@ -295,9 +295,9 @@ export default {
|
||||
activity: 'Activity',
|
||||
acpEndpoints: 'ACP Endpoints',
|
||||
settingsGroup: 'Settings',
|
||||
agents: 'Agents',
|
||||
agents: 'Digital Employees',
|
||||
backstage: 'Backstage',
|
||||
backstageTooltip: 'See what your agents are doing right now',
|
||||
backstageTooltip: 'See what your employees are doing right now',
|
||||
security: 'Security',
|
||||
tokenUsage: 'Token Usage',
|
||||
cronJobs: 'Cron Jobs',
|
||||
@ -873,20 +873,21 @@ export default {
|
||||
loadFileFailed: 'Failed to load file content',
|
||||
},
|
||||
agents: {
|
||||
title: 'Agent Management',
|
||||
desc: 'Create, edit, and manage your AI agents',
|
||||
newAgent: 'New Agent',
|
||||
kicker: 'Employee Studio',
|
||||
title: 'Digital Employees',
|
||||
desc: 'Hire, train, and manage your digital employees',
|
||||
newAgent: 'Hire Employee',
|
||||
live: {
|
||||
atWork: '{n} at work — see backstage',
|
||||
needsAttention: '{n} need attention — see backstage',
|
||||
},
|
||||
templates: {
|
||||
title: 'Choose a Template',
|
||||
desc: 'Start with a pre-configured agent, or create from scratch.',
|
||||
title: 'Choose a Role',
|
||||
desc: 'Hire from a pre-written job description, or start from scratch.',
|
||||
skip: 'Start from Scratch',
|
||||
applied: 'Agent created from template',
|
||||
applied: 'Employee onboarded',
|
||||
},
|
||||
search: 'Search agents...',
|
||||
search: 'Search employees...',
|
||||
tabs: {
|
||||
all: 'All',
|
||||
react: 'ReAct',
|
||||
@ -913,11 +914,11 @@ export default {
|
||||
enabled: 'Enabled',
|
||||
disabled: 'Disabled',
|
||||
},
|
||||
emptyTitle: 'No Agents',
|
||||
emptyDesc: 'Create your first agent to get started',
|
||||
emptyTitle: 'No Employees Yet',
|
||||
emptyDesc: 'Hire your first employee to get started',
|
||||
modal: {
|
||||
newTitle: 'New Agent',
|
||||
editTitle: 'Edit Agent',
|
||||
newTitle: 'New Employee',
|
||||
editTitle: 'Edit Employee',
|
||||
},
|
||||
fields: {
|
||||
name: 'Name',
|
||||
@ -938,7 +939,7 @@ export default {
|
||||
defaultThinkingLevel: 'Default Thinking Level',
|
||||
modelName: 'Model',
|
||||
modelGlobalDefault: 'Use global default',
|
||||
modelHint: 'Override the global default model for this Agent. Leave blank to follow Settings → Models.',
|
||||
modelHint: 'Override the global default model for this employee. Leave blank to follow Settings → Models.',
|
||||
tags: 'Tags',
|
||||
enabled: 'Enabled',
|
||||
},
|
||||
@ -962,7 +963,7 @@ export default {
|
||||
update: 'Save',
|
||||
},
|
||||
placeholders: {
|
||||
name: 'Agent name',
|
||||
name: 'Employee name',
|
||||
icon: 'Emoji or URL',
|
||||
description: 'Brief description',
|
||||
systemPrompt: 'You are a helpful AI assistant...',
|
||||
@ -975,13 +976,13 @@ export default {
|
||||
messages: {
|
||||
noDescription: 'No description',
|
||||
noTagline: 'No role and goal yet',
|
||||
deleteConfirm: 'Are you sure you want to delete this agent? This cannot be undone.',
|
||||
loadFailed: 'Failed to load agents',
|
||||
saveFailed: 'Failed to save agent',
|
||||
saveSuccess: 'Agent saved',
|
||||
deleteFailed: 'Failed to delete agent',
|
||||
deleteSuccess: 'Agent deleted',
|
||||
toggleFailed: 'Failed to toggle agent status',
|
||||
deleteConfirm: 'Let this employee go? This cannot be undone.',
|
||||
loadFailed: 'Failed to load employees',
|
||||
saveFailed: 'Failed to save employee',
|
||||
saveSuccess: 'Employee saved',
|
||||
deleteFailed: 'Failed to delete employee',
|
||||
deleteSuccess: 'Employee let go',
|
||||
toggleFailed: 'Failed to toggle status',
|
||||
toggleSuccess: 'Status updated',
|
||||
},
|
||||
binding: {
|
||||
|
||||
@ -152,8 +152,8 @@ export default {
|
||||
ttsStop: '停止朗读',
|
||||
conversations: '会话列表',
|
||||
newChat: '新对话',
|
||||
loadingAgents: '加载 Agent 中...',
|
||||
selectAgent: '请选择 Agent',
|
||||
loadingAgents: '加载员工中...',
|
||||
selectAgent: '请选择数字员工',
|
||||
noConversations: '暂无会话',
|
||||
startNewChat: '开始新对话吧',
|
||||
messages: '{count} 条消息',
|
||||
@ -165,7 +165,7 @@ export default {
|
||||
noActiveModel: '当前没有激活模型。先到模型管理中选择一个可用模型,再开始对话。',
|
||||
providerNotReady: '当前激活模型属于 {name},但这个提供商还没配置完成。请到模型管理中补全 API Key 或 Base URL。',
|
||||
noAvailableModel: '当前没有可用模型。',
|
||||
loadAgentsFailed: '加载 Agent 列表失败',
|
||||
loadAgentsFailed: '加载员工列表失败',
|
||||
loadModelFailed: '加载模型状态失败',
|
||||
loadConversationsFailed: '加载会话列表失败',
|
||||
loadMessagesFailed: '加载消息记录失败',
|
||||
@ -295,9 +295,9 @@ export default {
|
||||
activity: '活动记录',
|
||||
acpEndpoints: 'ACP 端点',
|
||||
settingsGroup: '设置',
|
||||
agents: '智能体',
|
||||
agents: '数字员工',
|
||||
backstage: '后台',
|
||||
backstageTooltip: '看看你的智能体此刻在做什么',
|
||||
backstageTooltip: '看看你的数字员工此刻在做什么',
|
||||
security: '安全',
|
||||
tokenUsage: 'Token 统计',
|
||||
cronJobs: '定时任务',
|
||||
@ -771,20 +771,21 @@ export default {
|
||||
loadFileFailed: '加载文件内容失败',
|
||||
},
|
||||
agents: {
|
||||
title: '智能体管理',
|
||||
desc: '创建、编辑和管理你的 AI 智能体',
|
||||
newAgent: '新建智能体',
|
||||
kicker: '员工工作室',
|
||||
title: '数字员工',
|
||||
desc: '招募、培训和管理你的数字员工',
|
||||
newAgent: '新员工',
|
||||
live: {
|
||||
atWork: '{n} 个在干活 · 看现场',
|
||||
needsAttention: '{n} 个需要看看 · 去现场',
|
||||
},
|
||||
templates: {
|
||||
title: '选择模板',
|
||||
desc: '选择预配置的 Agent 模板快速开始,或从空白创建。',
|
||||
title: '选择岗位',
|
||||
desc: '从一份岗位说明书快速招聘,或从空白开始。',
|
||||
skip: '从空白开始',
|
||||
applied: '已从模板创建 Agent',
|
||||
applied: '员工已上岗',
|
||||
},
|
||||
search: '搜索智能体...',
|
||||
search: '搜索员工...',
|
||||
tabs: {
|
||||
all: '全部',
|
||||
react: 'ReAct',
|
||||
@ -811,11 +812,11 @@ export default {
|
||||
enabled: '已启用',
|
||||
disabled: '已停用',
|
||||
},
|
||||
emptyTitle: '暂无智能体',
|
||||
emptyDesc: '创建你的第一个智能体开始使用',
|
||||
emptyTitle: '还没有员工',
|
||||
emptyDesc: '招聘第一位员工开始',
|
||||
modal: {
|
||||
newTitle: '新建智能体',
|
||||
editTitle: '编辑智能体',
|
||||
newTitle: '新员工',
|
||||
editTitle: '编辑员工',
|
||||
},
|
||||
fields: {
|
||||
name: '名称',
|
||||
@ -836,7 +837,7 @@ export default {
|
||||
defaultThinkingLevel: '默认思考深度',
|
||||
modelName: '模型',
|
||||
modelGlobalDefault: '使用全局默认模型',
|
||||
modelHint: '为此 Agent 单独指定模型,留空则跟随「设置 → 模型」中的全局默认。',
|
||||
modelHint: '为该员工单独指定模型,留空则跟随「设置 → 模型」中的全局默认。',
|
||||
tags: '标签',
|
||||
enabled: '启用',
|
||||
},
|
||||
@ -860,7 +861,7 @@ export default {
|
||||
update: '保存',
|
||||
},
|
||||
placeholders: {
|
||||
name: '智能体名称',
|
||||
name: '员工名字',
|
||||
icon: 'Emoji 或 URL',
|
||||
description: '简短描述',
|
||||
systemPrompt: '你是一个有帮助的 AI 助手...',
|
||||
@ -873,12 +874,12 @@ export default {
|
||||
messages: {
|
||||
noDescription: '暂无描述',
|
||||
noTagline: '还没填岗位与目标',
|
||||
deleteConfirm: '确认删除该智能体吗?删除后不可恢复。',
|
||||
loadFailed: '加载智能体列表失败',
|
||||
saveFailed: '保存智能体失败',
|
||||
saveSuccess: '智能体已保存',
|
||||
deleteFailed: '删除智能体失败',
|
||||
deleteSuccess: '智能体已删除',
|
||||
deleteConfirm: '确认让该员工离职吗?删除后不可恢复。',
|
||||
loadFailed: '加载员工列表失败',
|
||||
saveFailed: '保存员工失败',
|
||||
saveSuccess: '员工已保存',
|
||||
deleteFailed: '删除员工失败',
|
||||
deleteSuccess: '员工已离职',
|
||||
toggleFailed: '切换状态失败',
|
||||
toggleSuccess: '状态已更新',
|
||||
},
|
||||
|
||||
66
mateclaw-ui/src/utils/agentIconColor.ts
Normal file
66
mateclaw-ui/src/utils/agentIconColor.ts
Normal file
@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Per-role accent color for digital-employee icons.
|
||||
*
|
||||
* The pixelarticons SVGs render with `fill="currentColor"`, so wrapping
|
||||
* the icon in an element with a `color: ...` style tints the glyph
|
||||
* without touching the SVG itself. We only use this in agent contexts
|
||||
* (cards, picker, chat header) so skills / tools keep their default
|
||||
* neutral color.
|
||||
*
|
||||
* The palette is tuned to MateClaw's warm/earthy brand — every entry sits
|
||||
* around 45-55% lightness with mid saturation, so colors stay distinct
|
||||
* but live in the same room as the rust-orange primary, instead of
|
||||
* competing with it.
|
||||
*/
|
||||
|
||||
const BRAND_FALLBACK = 'var(--mc-primary)'
|
||||
|
||||
/** icon name (without `pi:` prefix) → CSS color */
|
||||
const ICON_COLOR_MAP: Record<string, string> = {
|
||||
// Engineering / inspection — rosewood, sits next to the brand rust
|
||||
'bug': 'hsl(8, 62%, 50%)',
|
||||
'search': 'hsl(8, 62%, 50%)',
|
||||
|
||||
// Research / writing — sage green, calm, "library" feel
|
||||
'book-open': 'hsl(155, 32%, 42%)',
|
||||
'notes': 'hsl(285, 30%, 50%)',
|
||||
'article': 'hsl(155, 32%, 42%)',
|
||||
|
||||
// Data / analytics — dusk blue
|
||||
'chart-bar-big': 'hsl(212, 45%, 48%)',
|
||||
'chart': 'hsl(212, 45%, 48%)',
|
||||
'analytics': 'hsl(212, 45%, 48%)',
|
||||
|
||||
// Customer / support — terracotta, warm and approachable
|
||||
'headphone': 'hsl(20, 68%, 50%)',
|
||||
'message': 'hsl(20, 68%, 50%)',
|
||||
'message-text': 'hsl(20, 68%, 50%)',
|
||||
|
||||
// General / friendly assistants — warm amber
|
||||
'robot-face-happy': 'hsl(38, 72%, 50%)',
|
||||
'robot-face': 'hsl(38, 72%, 50%)',
|
||||
'robot': 'hsl(38, 72%, 50%)',
|
||||
|
||||
// System / infrastructure — slate teal
|
||||
'cpu': 'hsl(195, 28%, 42%)',
|
||||
'cloud': 'hsl(195, 28%, 42%)',
|
||||
|
||||
// Planning / task — indigo
|
||||
'clipboard-note': 'hsl(232, 38%, 52%)',
|
||||
'clipboard': 'hsl(232, 38%, 52%)',
|
||||
'list-box': 'hsl(232, 38%, 52%)',
|
||||
'checkbox-on': 'hsl(232, 38%, 52%)',
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the accent color for a stored icon string. Returns the brand
|
||||
* primary as a CSS variable for emoji / URL / unknown icons so callers
|
||||
* can apply the color unconditionally.
|
||||
*/
|
||||
export function agentIconColor(iconValue: string | null | undefined): string {
|
||||
if (!iconValue) return BRAND_FALLBACK
|
||||
const v = iconValue.trim()
|
||||
if (!v.startsWith('pi:')) return BRAND_FALLBACK
|
||||
const name = v.slice(3)
|
||||
return ICON_COLOR_MAP[name] || BRAND_FALLBACK
|
||||
}
|
||||
@ -54,9 +54,17 @@ export function isStructuredPrompt(systemPrompt: string | null | undefined): boo
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a systemPrompt into role/goal/backstory/extra. If no section markers
|
||||
* are present, the entire prompt becomes `extra` so legacy agents keep their
|
||||
* prompt verbatim.
|
||||
* Parse a systemPrompt into role/goal/backstory/extra. The parse contract is
|
||||
* lossless: every byte of the original prompt ends up in one of the four
|
||||
* fields, so a parse → serialize round-trip never silently drops content.
|
||||
*
|
||||
* - If no recognized section markers exist, the whole prompt becomes `extra`.
|
||||
* - If recognized markers exist:
|
||||
* - Lines before the first heading (preamble) prepend to `extra`.
|
||||
* - Unknown `## Heading` blocks (e.g. `## Notes`, `## Examples`) keep
|
||||
* their heading line and content and append to `extra` verbatim.
|
||||
* - Multiple headings of the same kind concatenate (last writer wins on
|
||||
* intent, but content is preserved).
|
||||
*/
|
||||
export function parsePrompt(systemPrompt: string | null | undefined): AgentPromptProfile {
|
||||
const profile = emptyProfile()
|
||||
@ -68,29 +76,57 @@ export function parsePrompt(systemPrompt: string | null | undefined): AgentPromp
|
||||
}
|
||||
|
||||
const lines = systemPrompt.split(/\r?\n/)
|
||||
let current: SectionKey | null = null
|
||||
const buffers: Record<SectionKey, string[]> = {
|
||||
role: [],
|
||||
goal: [],
|
||||
backstory: [],
|
||||
extra: [],
|
||||
}
|
||||
const preamble: string[] = []
|
||||
// Unknown sections are captured as { heading, body } pairs so we can
|
||||
// re-emit them verbatim (including their `## Heading` line) into `extra`.
|
||||
const unknownSections: { heading: string; body: string[] }[] = []
|
||||
|
||||
type Bucket = { kind: 'preamble' } | { kind: 'known'; key: SectionKey } | { kind: 'unknown'; index: number }
|
||||
let bucket: Bucket = { kind: 'preamble' }
|
||||
|
||||
for (const line of lines) {
|
||||
const m = line.match(SECTION_HEADING_REGEX)
|
||||
if (m) {
|
||||
const key = HEADING_TO_KEY[m[1].trim().toLowerCase()]
|
||||
const headingText = m[1].trim()
|
||||
const key = HEADING_TO_KEY[headingText.toLowerCase()]
|
||||
if (key) {
|
||||
current = key
|
||||
bucket = { kind: 'known', key }
|
||||
continue
|
||||
}
|
||||
// Unknown heading — start a new captured block, keep the original
|
||||
// heading text so we can round-trip it back.
|
||||
const idx = unknownSections.length
|
||||
unknownSections.push({ heading: line, body: [] })
|
||||
bucket = { kind: 'unknown', index: idx }
|
||||
continue
|
||||
}
|
||||
if (current) buffers[current].push(line)
|
||||
if (bucket.kind === 'preamble') preamble.push(line)
|
||||
else if (bucket.kind === 'known') buffers[bucket.key].push(line)
|
||||
else unknownSections[bucket.index].body.push(line)
|
||||
}
|
||||
|
||||
for (const key of SECTION_KEYS) {
|
||||
profile[key] = buffers[key].join('\n').trim()
|
||||
// Compose `extra` lossless: preamble first, then the recognized "extra"
|
||||
// section's content, then any unknown sections (keeping their headings).
|
||||
const extraParts: string[] = []
|
||||
const trimmedPreamble = preamble.join('\n').trim()
|
||||
if (trimmedPreamble) extraParts.push(trimmedPreamble)
|
||||
const trimmedExtra = buffers.extra.join('\n').trim()
|
||||
if (trimmedExtra) extraParts.push(trimmedExtra)
|
||||
for (const sec of unknownSections) {
|
||||
const body = sec.body.join('\n').trimEnd()
|
||||
extraParts.push(body ? `${sec.heading}\n${body}` : sec.heading)
|
||||
}
|
||||
|
||||
profile.role = buffers.role.join('\n').trim()
|
||||
profile.goal = buffers.goal.join('\n').trim()
|
||||
profile.backstory = buffers.backstory.join('\n').trim()
|
||||
profile.extra = extraParts.join('\n\n').trim()
|
||||
return profile
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<div class="mc-page-inner agents-page">
|
||||
<div class="mc-page-header">
|
||||
<div>
|
||||
<div class="mc-page-kicker">Agent Studio</div>
|
||||
<div class="mc-page-kicker">{{ t('agents.kicker') }}</div>
|
||||
<h1 class="mc-page-title">{{ t('agents.title') }}</h1>
|
||||
<p class="mc-page-desc">{{ t('agents.desc') }}</p>
|
||||
</div>
|
||||
@ -67,7 +67,11 @@
|
||||
the card readable at a glance.
|
||||
-->
|
||||
<div class="agent-card__top">
|
||||
<span class="agent-card__avatar" :class="{ 'agent-card__avatar--off': !agent.enabled }">
|
||||
<span
|
||||
class="agent-card__avatar"
|
||||
:class="{ 'agent-card__avatar--off': !agent.enabled }"
|
||||
:style="{ color: agentIconColor(agent.icon) }"
|
||||
>
|
||||
<SkillIcon :value="agent.icon" :size="40" :fallback="'🧑💼'" />
|
||||
</span>
|
||||
<div class="agent-card__identity">
|
||||
@ -144,7 +148,9 @@
|
||||
:class="{ applying: applyingTemplate }"
|
||||
@click="!applyingTemplate && applyTemplate(tpl.id)"
|
||||
>
|
||||
<div class="template-icon">{{ tpl.icon }}</div>
|
||||
<div class="template-icon" :style="{ color: agentIconColor(tpl.icon) }">
|
||||
<SkillIcon :value="tpl.icon" :size="28" :fallback="'🧑💼'" />
|
||||
</div>
|
||||
<div class="template-info">
|
||||
<h4 class="template-name">{{ $i18n.locale === 'zh-CN' && tpl.nameZh ? tpl.nameZh : tpl.name }}</h4>
|
||||
<p class="template-detail">{{ $i18n.locale === 'zh-CN' && tpl.descriptionZh ? tpl.descriptionZh : tpl.description }}</p>
|
||||
@ -432,6 +438,7 @@ import {
|
||||
TAGLINE_CJK_BUDGET,
|
||||
type AgentPromptProfile,
|
||||
} from '@/utils/agentPromptProfile'
|
||||
import { agentIconColor } from '@/utils/agentIconColor'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
|
||||
<div class="agent-selector">
|
||||
<button class="agent-select-trigger" @click="agentDropdownOpen = !agentDropdownOpen" :title="`${$t('chat.selectAgent')} (⌘K)`">
|
||||
<span class="agent-select-trigger__icon"><SkillIcon :value="currentAgent?.icon" :size="24" :fallback="'🤖'" /></span>
|
||||
<span class="agent-select-trigger__icon" :style="{ color: agentIconColor(currentAgent?.icon) }"><SkillIcon :value="currentAgent?.icon" :size="24" :fallback="'🤖'" /></span>
|
||||
<span v-if="!convPanelCollapsed || isMobile" class="agent-select-trigger__name">{{ currentAgent?.name || $t('chat.selectAgent') }}</span>
|
||||
<svg v-if="!convPanelCollapsed || isMobile" class="agent-select-trigger__arrow" :class="{ open: agentDropdownOpen }" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
@ -42,10 +42,10 @@
|
||||
:class="{ active: String(agent.id) === String(selectedAgentId) }"
|
||||
@click="selectAgent(agent)"
|
||||
>
|
||||
<span class="agent-dropdown-item__icon"><SkillIcon :value="agent.icon" :size="18" :fallback="'🤖'" /></span>
|
||||
<span class="agent-dropdown-item__icon" :style="{ color: agentIconColor(agent.icon) }"><SkillIcon :value="agent.icon" :size="18" :fallback="'🤖'" /></span>
|
||||
<div class="agent-dropdown-item__info">
|
||||
<span class="agent-dropdown-item__name">{{ agent.name }}</span>
|
||||
<span class="agent-dropdown-item__desc">{{ agent.description || agent.agentType }}</span>
|
||||
<span class="agent-dropdown-item__desc">{{ agentTagline(agent) }}</span>
|
||||
</div>
|
||||
<span v-if="String(agent.id) === String(selectedAgentId)" class="agent-dropdown-item__check">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
@ -153,10 +153,21 @@
|
||||
</button>
|
||||
<div class="chat-stage-copy" v-if="currentAgent">
|
||||
<div class="chat-stage-kicker">{{ $t('nav.chat') }}</div>
|
||||
<div class="agent-badge" :title="currentAgent.name">
|
||||
<span class="agent-badge-icon"><SkillIcon :value="currentAgent.icon" :size="22" :fallback="'🤖'" /></span>
|
||||
<span class="agent-badge-name">{{ currentAgent.name }}</span>
|
||||
<span class="agent-badge-type">{{ currentAgent.agentType === 'react' ? 'ReAct' : 'Plan-Execute' }}</span>
|
||||
<!--
|
||||
Header reads as "who is this employee" — name + tagline.
|
||||
The runtime mode (ReAct / Plan-Execute) is technical jargon
|
||||
to end users and lives in the badge tooltip instead, so the
|
||||
header doesn't get polluted.
|
||||
-->
|
||||
<div
|
||||
class="agent-badge"
|
||||
:title="`${currentAgent.name}${currentAgentRuntimeMode ? ' · ' + currentAgentRuntimeMode : ''}`"
|
||||
>
|
||||
<span class="agent-badge-icon" :style="{ color: agentIconColor(currentAgent.icon) }"><SkillIcon :value="currentAgent.icon" :size="22" :fallback="'🤖'" /></span>
|
||||
<div class="agent-badge-text">
|
||||
<span class="agent-badge-name">{{ currentAgent.name }}</span>
|
||||
<span v-if="currentAgentTagline" class="agent-badge-tagline">{{ currentAgentTagline }}</span>
|
||||
</div>
|
||||
<span class="status-dot" :class="connectionStatusClass" :title="connectionStatusLabel"></span>
|
||||
</div>
|
||||
</div>
|
||||
@ -314,6 +325,8 @@ import type { Conversation, Agent, ModelConfig, ProviderInfo, ActiveModelsInfo,
|
||||
// 导入组件化组件
|
||||
import MessageList from '@/components/chat/MessageList.vue'
|
||||
import SkillIcon from '@/components/common/SkillIcon.vue'
|
||||
import { parsePrompt, deriveTagline } from '@/utils/agentPromptProfile'
|
||||
import { agentIconColor } from '@/utils/agentIconColor'
|
||||
import ChatInput from '@/components/chat/ChatInput.vue'
|
||||
import StreamLoadingBar from '@/components/chat/StreamLoadingBar.vue'
|
||||
import TalkMode from '@/components/chat/TalkMode.vue'
|
||||
@ -663,6 +676,25 @@ const connectionStatusLabel = computed(() => {
|
||||
// ============ 计算属性 ============
|
||||
const currentAgent = computed(() => agents.value.find(a => String(a.id) === String(selectedAgentId.value)))
|
||||
|
||||
/** Tagline derived from the agent's role/goal — same source of truth as the
|
||||
* Agents page card, so the chat header reads the employee identically.
|
||||
* Falls back to the agent's description when no triad is set. */
|
||||
function agentTagline(agent: Agent): string {
|
||||
const profile = parsePrompt(agent.systemPrompt)
|
||||
return deriveTagline(profile, agent.description) || (agent.description || '')
|
||||
}
|
||||
|
||||
const currentAgentTagline = computed(() =>
|
||||
currentAgent.value ? agentTagline(currentAgent.value) : '',
|
||||
)
|
||||
/** Human label for the agent's runtime mode — surfaces in the badge tooltip
|
||||
* only, never in the visible header. */
|
||||
const currentAgentRuntimeMode = computed(() => {
|
||||
const a = currentAgent.value
|
||||
if (!a) return ''
|
||||
return a.agentType === 'react' ? t('agents.types.react') : t('agents.types.planExecute')
|
||||
})
|
||||
|
||||
// 按日期分组的会话列表
|
||||
// Per-conversation last-viewed timestamp store (localStorage-backed, MVP).
|
||||
// Keyed by conversationId. Updated when user opens a conversation; read by
|
||||
@ -2334,6 +2366,13 @@ function handleCodeCopy(e: MouseEvent) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.agent-badge-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agent-badge-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
@ -2341,14 +2380,18 @@ function handleCodeCopy(e: MouseEvent) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.agent-badge-type {
|
||||
/* The tagline answers "what does this employee do" — runtime mode lives in
|
||||
the badge tooltip, not on screen, so we don't compete for attention. */
|
||||
.agent-badge-tagline {
|
||||
font-size: 11px;
|
||||
color: var(--mc-primary-light);
|
||||
background: var(--mc-bg-elevated);
|
||||
padding: 1px 6px;
|
||||
border-radius: 10px;
|
||||
color: var(--mc-text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
@ -2561,8 +2604,7 @@ function handleCodeCopy(e: MouseEvent) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.agent-badge-name,
|
||||
.agent-badge-type {
|
||||
.agent-badge-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user