Files
zclaw_openfang/desktop/src/lib/kernel-agent.ts
iven 1e65b56a0f
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
fix(identity): 3 项根因级修复 — Agent ID 映射 + user_profile 读取 + 用户画像 fallback
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>
2026-04-16 17:07:38 +08:00

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;
}