Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Issue 2: IdentityFile 枚举补全 UserProfile 变体 - get_file()/propose_change()/approve_proposal() 补全 match arm - identity_get_file/identity_propose_change Tauri 命令支持 user_profile Issue 1: Agent ID 映射机制 - 新增 resolveKernelAgentId() 工具函数 (带缓存) - ButlerPanel 使用 kernel UUID 替代 SaaS relay "1" 查询 VikingStorage Issue 3: 用户画像 fallback 注入 - build_system_prompt 改为 async,identity user_profile 为默认值时 从 VikingStorage preferences 路径查询最近 5 条记忆作为 fallback - intelligence_hooks 调用处同步加 .await Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
312 lines
11 KiB
TypeScript
312 lines
11 KiB
TypeScript
/**
|
|
* kernel-agent.ts - Agent & Clone management methods for KernelClient
|
|
*
|
|
* Installed onto KernelClient.prototype via installAgentMethods().
|
|
*/
|
|
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
import type { KernelClient } from './kernel-client';
|
|
import type { AgentInfo, CreateAgentRequest, CreateAgentResponse } from './kernel-types';
|
|
|
|
export function installAgentMethods(ClientClass: { prototype: KernelClient }): void {
|
|
const proto = ClientClass.prototype as unknown as Record<string, unknown>;
|
|
|
|
// ─── Agent Management ───
|
|
|
|
/**
|
|
* List all agents
|
|
*/
|
|
proto.listAgents = async function (this: KernelClient): Promise<AgentInfo[]> {
|
|
return invoke<AgentInfo[]>('agent_list');
|
|
};
|
|
|
|
/**
|
|
* Get agent by ID
|
|
*/
|
|
proto.getAgent = async function (this: KernelClient, agentId: string): Promise<AgentInfo | null> {
|
|
return invoke<AgentInfo | null>('agent_get', { agentId });
|
|
};
|
|
|
|
/**
|
|
* Create a new agent
|
|
*/
|
|
proto.createAgent = async function (this: KernelClient, request: CreateAgentRequest): Promise<CreateAgentResponse> {
|
|
return invoke<CreateAgentResponse>('agent_create', {
|
|
request: {
|
|
name: request.name,
|
|
description: request.description,
|
|
systemPrompt: request.systemPrompt,
|
|
soul: request.soul,
|
|
provider: request.provider || 'anthropic',
|
|
model: request.model || 'claude-sonnet-4-20250514',
|
|
maxTokens: request.maxTokens || 4096,
|
|
temperature: request.temperature || 0.7,
|
|
},
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Delete an agent
|
|
*/
|
|
proto.deleteAgent = async function (this: KernelClient, agentId: string): Promise<void> {
|
|
return invoke('agent_delete', { agentId });
|
|
};
|
|
|
|
// ─── Clone/Agent Adaptation (GatewayClient interface compatibility) ───
|
|
|
|
/**
|
|
* List clones — maps to listAgents() with field adaptation
|
|
* Maps all available AgentInfo fields to Clone interface properties
|
|
*/
|
|
proto.listClones = async function (this: KernelClient): Promise<{ clones: any[] }> {
|
|
const agents = await this.listAgents();
|
|
const clones = agents.map((agent) => {
|
|
// Parse personality/emoji/nickname from SOUL.md content
|
|
const soulLines = (agent.soul || '').split('\n');
|
|
let emoji: string | undefined;
|
|
let personality: string | undefined;
|
|
let nickname: string | undefined;
|
|
for (const line of soulLines) {
|
|
if (!emoji || !nickname) {
|
|
// Parse header line: "> 🦞 Nickname" or "> 🦞"
|
|
const headerMatch = line.match(/^>\s*(\p{Emoji_Presentation}|\p{Extended_Pictographic})?\s*(.+)$/u);
|
|
if (headerMatch) {
|
|
if (headerMatch[1] && !emoji) emoji = headerMatch[1];
|
|
if (headerMatch[2]?.trim() && !nickname) nickname = headerMatch[2].trim();
|
|
}
|
|
// Also check emoji without nickname
|
|
if (!emoji) {
|
|
const emojiOnly = line.match(/^>\s*(\p{Emoji_Presentation}|\p{Extended_Pictographic})\s*$/u);
|
|
if (emojiOnly) emoji = emojiOnly[1];
|
|
}
|
|
}
|
|
if (!personality) {
|
|
const match = line.match(/##\s*(?:性格|核心特质|沟通风格)/);
|
|
if (match) personality = line.trim();
|
|
}
|
|
}
|
|
|
|
// Parse userName/userRole from userProfile
|
|
let userName: string | undefined;
|
|
let userRole: string | undefined;
|
|
if (agent.userProfile && typeof agent.userProfile === 'object') {
|
|
const profile = agent.userProfile as Record<string, unknown>;
|
|
userName = profile.userName as string | undefined || profile.name as string | undefined;
|
|
userRole = profile.userRole as string | undefined || profile.role as string | undefined;
|
|
}
|
|
|
|
return {
|
|
id: agent.id,
|
|
name: agent.name,
|
|
role: agent.description,
|
|
nickname,
|
|
model: agent.model,
|
|
soul: agent.soul,
|
|
systemPrompt: agent.systemPrompt,
|
|
temperature: agent.temperature,
|
|
maxTokens: agent.maxTokens,
|
|
emoji,
|
|
personality,
|
|
userName,
|
|
userRole,
|
|
createdAt: agent.createdAt || new Date().toISOString(),
|
|
updatedAt: agent.updatedAt,
|
|
};
|
|
});
|
|
return { clones };
|
|
};
|
|
|
|
/**
|
|
* Create clone — maps to createAgent()
|
|
*/
|
|
proto.createClone = async function (this: KernelClient, opts: {
|
|
name: string;
|
|
role?: string;
|
|
model?: string;
|
|
personality?: string;
|
|
communicationStyle?: string;
|
|
scenarios?: string[];
|
|
emoji?: string;
|
|
notes?: string;
|
|
[key: string]: unknown;
|
|
}): Promise<{ clone: any }> {
|
|
// Build soul content from personality data
|
|
const soulParts: string[] = [];
|
|
if (opts.personality) soulParts.push(`## 性格\n${opts.personality}`);
|
|
if (opts.communicationStyle) soulParts.push(`## 沟通风格\n${opts.communicationStyle}`);
|
|
if (opts.scenarios?.length) soulParts.push(`## 使用场景\n${opts.scenarios.join(', ')}`);
|
|
if (opts.notes) soulParts.push(`## 备注\n${opts.notes}`);
|
|
const soul = soulParts.length > 0 ? soulParts.join('\n\n') : undefined;
|
|
|
|
const response = await this.createAgent({
|
|
name: opts.name,
|
|
description: opts.role,
|
|
model: opts.model,
|
|
soul,
|
|
});
|
|
const clone = {
|
|
id: response.id,
|
|
name: response.name,
|
|
role: opts.role,
|
|
model: opts.model,
|
|
personality: opts.personality,
|
|
communicationStyle: opts.communicationStyle,
|
|
emoji: opts.emoji,
|
|
scenarios: opts.scenarios,
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
return { clone };
|
|
};
|
|
|
|
/**
|
|
* Delete clone — maps to deleteAgent()
|
|
*/
|
|
proto.deleteClone = async function (this: KernelClient, id: string): Promise<void> {
|
|
return this.deleteAgent(id);
|
|
};
|
|
|
|
/**
|
|
* Update clone — maps to kernel agent_update + identity system for nickname/userName
|
|
*/
|
|
proto.updateClone = async function (this: KernelClient, id: string, updates: Record<string, unknown>): Promise<{ clone: unknown }> {
|
|
await invoke('agent_update', {
|
|
agentId: id,
|
|
updates: {
|
|
name: updates.name as string | undefined,
|
|
description: updates.description as string | undefined,
|
|
systemPrompt: updates.systemPrompt as string | undefined,
|
|
model: updates.model as string | undefined,
|
|
provider: updates.provider as string | undefined,
|
|
maxTokens: updates.maxTokens as number | undefined,
|
|
temperature: updates.temperature as number | undefined,
|
|
},
|
|
});
|
|
|
|
// Sync nickname/emoji to SOUL.md via identity system
|
|
const nickname = updates.nickname as string | undefined;
|
|
const emoji = updates.emoji as string | undefined;
|
|
if (nickname || emoji) {
|
|
try {
|
|
const currentSoul = await invoke<string | null>('identity_get_file', { agentId: id, file: 'soul' });
|
|
const soul = currentSoul || '';
|
|
// Inject or update nickname line in SOUL.md header
|
|
const lines = soul.split('\n');
|
|
const headerIdx = lines.findIndex((l: string) => l.startsWith('> '));
|
|
if (headerIdx >= 0) {
|
|
// Update existing header line
|
|
let header = lines[headerIdx];
|
|
if (emoji && !header.match(/\p{Emoji_Presentation}|\p{Extended_Pictographic}/u)) {
|
|
header = `> ${emoji} ${header.slice(2)}`;
|
|
}
|
|
lines[headerIdx] = header;
|
|
} else if (emoji || nickname) {
|
|
// Add header line after title
|
|
const label = nickname || '';
|
|
const icon = emoji || '';
|
|
const titleIdx = lines.findIndex((l: string) => l.startsWith('# '));
|
|
if (titleIdx >= 0) {
|
|
lines.splice(titleIdx + 1, 0, `> ${icon} ${label}`.trim());
|
|
}
|
|
}
|
|
await invoke('identity_update_file', { agentId: id, file: 'soul', content: lines.join('\n') });
|
|
} catch {
|
|
// Identity system update is non-critical
|
|
}
|
|
}
|
|
|
|
// Sync userName/userRole to USER.md via identity system
|
|
const userName = updates.userName as string | undefined;
|
|
const userRole = updates.userRole as string | undefined;
|
|
if (userName || userRole) {
|
|
try {
|
|
const currentProfile = await invoke<string | null>('identity_get_file', { agentId: id, file: 'user_profile' });
|
|
const profile = currentProfile || '# 用户档案\n';
|
|
const profileLines = profile.split('\n');
|
|
|
|
// Update or add userName
|
|
if (userName) {
|
|
const nameIdx = profileLines.findIndex((l: string) => l.includes('姓名') || l.includes('userName'));
|
|
if (nameIdx >= 0) {
|
|
profileLines[nameIdx] = `- 姓名:${userName}`;
|
|
} else {
|
|
const sectionIdx = profileLines.findIndex((l: string) => l.startsWith('## 基本信息'));
|
|
if (sectionIdx >= 0) {
|
|
profileLines.splice(sectionIdx + 1, 0, '', `- 姓名:${userName}`);
|
|
} else {
|
|
profileLines.push('', '## 基本信息', '', `- 姓名:${userName}`);
|
|
}
|
|
}
|
|
}
|
|
// Update or add userRole
|
|
if (userRole) {
|
|
const roleIdx = profileLines.findIndex((l: string) => l.includes('角色') || l.includes('userRole'));
|
|
if (roleIdx >= 0) {
|
|
profileLines[roleIdx] = `- 角色:${userRole}`;
|
|
} else {
|
|
profileLines.push(`- 角色:${userRole}`);
|
|
}
|
|
}
|
|
|
|
await invoke('identity_update_file', { agentId: id, file: 'user_profile', content: profileLines.join('\n') });
|
|
} catch {
|
|
// Identity system update is non-critical
|
|
}
|
|
}
|
|
|
|
// Return updated clone representation
|
|
const clone = {
|
|
id,
|
|
name: updates.name,
|
|
role: updates.description || updates.role,
|
|
nickname: updates.nickname,
|
|
model: updates.model,
|
|
emoji: updates.emoji,
|
|
personality: updates.personality,
|
|
communicationStyle: updates.communicationStyle,
|
|
systemPrompt: updates.systemPrompt,
|
|
userName: updates.userName,
|
|
userRole: updates.userRole,
|
|
};
|
|
return { clone };
|
|
};
|
|
}
|
|
|
|
// === Agent ID Resolution ===
|
|
|
|
/**
|
|
* Cached kernel default agent UUID.
|
|
* The conversationStore's DEFAULT_AGENT has id="1", but VikingStorage
|
|
* stores data under kernel UUIDs. This cache bridges the gap.
|
|
*/
|
|
let _cachedDefaultKernelAgentId: string | null = null;
|
|
|
|
/**
|
|
* Resolve an agent ID to the kernel's actual agent UUID.
|
|
* - If already a UUID (8-4-4 hex pattern), return as-is.
|
|
* - If "1" or undefined, query agent_list and cache the first kernel agent's UUID.
|
|
* - Falls back to the original ID if kernel has no agents.
|
|
*/
|
|
export async function resolveKernelAgentId(agentId: string | undefined): Promise<string> {
|
|
if (agentId && /^[0-9a-f]{8}-[0-9a-f]{4}-/.test(agentId)) {
|
|
return agentId;
|
|
}
|
|
if (_cachedDefaultKernelAgentId) {
|
|
return _cachedDefaultKernelAgentId;
|
|
}
|
|
try {
|
|
const agents = await invoke<{ id: string }[]>('agent_list');
|
|
if (agents.length > 0) {
|
|
_cachedDefaultKernelAgentId = agents[0].id;
|
|
return _cachedDefaultKernelAgentId;
|
|
}
|
|
} catch {
|
|
// Kernel may not be available
|
|
}
|
|
return agentId || '1';
|
|
}
|
|
|
|
/** Invalidate cache when kernel reconnects (new instance may have different UUIDs) */
|
|
export function invalidateKernelAgentIdCache(): void {
|
|
_cachedDefaultKernelAgentId = null;
|
|
}
|