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
Phase 1.0 — Butler Mode UI: - Hide "自动化" and "技能市场" entries from sidebar navigation - Remove AutomationPanel and SkillMarket view rendering from App.tsx - Simplify MainViewType to only 'chat' - Main interface is now: chat + conversation list + detail panel only Phase 1.1 — Mode Differentiation: - Add subagent_enabled field to ChatModeConfig (Rust), StreamChatRequest (Tauri), gateway-client, kernel-client, saas-relay-client, and streamStore - TaskTool is now only registered when subagent_enabled=true (Ultra mode) - System prompt includes sub-agent delegation instructions only in Ultra mode - Frontend transmits subagent_enabled from ChatMode config through the full stack This connects the 4-tier mode selector (Flash/Thinking/Pro/Ultra) to actual backend behavioral differences — Ultra mode now truly enables sub-agent delegation.
553 lines
19 KiB
TypeScript
553 lines
19 KiB
TypeScript
/**
|
|
* ZCLAW Kernel Client (Tauri Internal)
|
|
*
|
|
* Client for communicating with the internal ZCLAW Kernel via Tauri commands.
|
|
* This replaces the external ZCLAW Gateway WebSocket connection.
|
|
*
|
|
* Phase 5 of Intelligence Layer Migration.
|
|
*
|
|
* Domain methods are installed from mixin modules following the same pattern
|
|
* used by gateway-api.ts for GatewayClient:
|
|
* - kernel-agent.ts → installAgentMethods()
|
|
* - kernel-chat.ts → installChatMethods()
|
|
* - kernel-hands.ts → installHandMethods()
|
|
* - kernel-skills.ts → installSkillMethods()
|
|
* - kernel-triggers.ts → installTriggerMethods()
|
|
* - kernel-a2a.ts → installA2aMethods()
|
|
*/
|
|
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
import { createLogger } from './logger';
|
|
|
|
// Re-export all types from the shared types module
|
|
export type { UnlistenFn } from '@tauri-apps/api/event';
|
|
export type {
|
|
ConnectionState,
|
|
KernelStatus,
|
|
AgentInfo,
|
|
CreateAgentRequest,
|
|
CreateAgentResponse,
|
|
ChatResponse,
|
|
EventCallback,
|
|
StreamCallbacks,
|
|
StreamEventDelta,
|
|
StreamEventToolStart,
|
|
StreamEventToolEnd,
|
|
StreamEventIterationStart,
|
|
StreamEventComplete,
|
|
StreamEventError,
|
|
StreamEventHandStart,
|
|
StreamEventHandEnd,
|
|
StreamChatEvent,
|
|
StreamChunkPayload,
|
|
KernelConfig,
|
|
} from './kernel-types';
|
|
|
|
import type {
|
|
ConnectionState,
|
|
KernelStatus,
|
|
KernelConfig,
|
|
EventCallback,
|
|
} from './kernel-types';
|
|
|
|
const log = createLogger('KernelClient');
|
|
|
|
// === Tauri Runtime Detection ===
|
|
|
|
/**
|
|
* Check if running in Tauri environment
|
|
* NOTE: This checks synchronously. For more reliable detection,
|
|
* use probeTauriAvailability() which actually tries to call a Tauri command.
|
|
*/
|
|
export function isTauriRuntime(): boolean {
|
|
const result = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window;
|
|
log.debug('isTauriRuntime() check:', result, 'window exists:', typeof window !== 'undefined', '__TAURI_INTERNALS__ exists:', typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Probe if Tauri is actually available by trying to invoke a command.
|
|
* This is more reliable than checking __TAURI_INTERNALS__ which may not be set
|
|
* immediately when the page loads.
|
|
*/
|
|
let _tauriAvailable: boolean | null = null;
|
|
|
|
export async function probeTauriAvailability(): Promise<boolean> {
|
|
if (_tauriAvailable !== null) {
|
|
return _tauriAvailable;
|
|
}
|
|
|
|
// First check if window.__TAURI_INTERNALS__ exists
|
|
if (typeof window === 'undefined' || !('__TAURI_INTERNALS__' in window)) {
|
|
log.debug('probeTauriAvailability: __TAURI_INTERNALS__ not found');
|
|
_tauriAvailable = false;
|
|
return false;
|
|
}
|
|
|
|
// Try to actually invoke a simple command to verify Tauri is working
|
|
try {
|
|
// Use kernel_status as a lightweight health check
|
|
await invoke('kernel_status');
|
|
log.debug('probeTauriAvailability: kernel_status succeeded');
|
|
_tauriAvailable = true;
|
|
return true;
|
|
} catch (e) {
|
|
// Try without plugin prefix - some Tauri versions don't use it
|
|
log.debug('probeTauriAvailability: kernel_status invoke failed', { error: e });
|
|
try {
|
|
// Just checking if invoke function exists is enough
|
|
log.debug('probeTauriAvailability: Tauri invoke available');
|
|
_tauriAvailable = true;
|
|
return true;
|
|
} catch (e) {
|
|
log.debug('probeTauriAvailability: secondary invoke check failed', { error: e });
|
|
_tauriAvailable = false;
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// === KernelClient Core Class ===
|
|
|
|
/**
|
|
* ZCLAW Kernel Client
|
|
*
|
|
* Provides a GatewayClient-compatible interface that uses Tauri commands
|
|
* to communicate with the internal ZCLAW Kernel instead of external WebSocket.
|
|
*
|
|
* Domain-specific methods (agents, chat, hands, skills, triggers, a2a)
|
|
* are installed at the bottom of this file via mixin functions.
|
|
*/
|
|
export class KernelClient {
|
|
private state: ConnectionState = 'disconnected';
|
|
private eventListeners = new Map<string, Set<EventCallback>>();
|
|
private kernelStatus: KernelStatus | null = null;
|
|
/** @internal stored as _defaultAgentId so mixin methods can access via getDefaultAgentId() */
|
|
defaultAgentId: string = '';
|
|
private config: KernelConfig = {};
|
|
|
|
// State change callbacks
|
|
onStateChange?: (state: ConnectionState) => void;
|
|
onLog?: (level: string, message: string) => void;
|
|
|
|
constructor(opts?: {
|
|
url?: string;
|
|
token?: string;
|
|
autoReconnect?: boolean;
|
|
reconnectInterval?: number;
|
|
requestTimeout?: number;
|
|
kernelConfig?: KernelConfig;
|
|
}) {
|
|
// Store kernel config if provided
|
|
if (opts?.kernelConfig) {
|
|
this.config = opts.kernelConfig;
|
|
}
|
|
}
|
|
|
|
updateOptions(opts?: {
|
|
url?: string;
|
|
token?: string;
|
|
autoReconnect?: boolean;
|
|
reconnectInterval?: number;
|
|
requestTimeout?: number;
|
|
kernelConfig?: KernelConfig;
|
|
}): void {
|
|
if (opts?.kernelConfig) {
|
|
this.config = opts.kernelConfig;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set kernel configuration (must be called before connect)
|
|
*/
|
|
setConfig(config: KernelConfig): void {
|
|
this.config = config;
|
|
}
|
|
|
|
getState(): ConnectionState {
|
|
return this.state;
|
|
}
|
|
|
|
/**
|
|
* Initialize and connect to the internal Kernel
|
|
*/
|
|
async connect(): Promise<void> {
|
|
// Always try to (re)initialize - backend will handle config changes
|
|
// by rebooting the kernel if needed
|
|
this.setState('connecting');
|
|
|
|
try {
|
|
// Validate that we have required config
|
|
if (!this.config.provider || !this.config.model || !this.config.apiKey) {
|
|
throw new Error('请先在"模型与 API"设置页面配置模型');
|
|
}
|
|
|
|
// Initialize the kernel via Tauri command with config
|
|
const configRequest = {
|
|
provider: this.config.provider,
|
|
model: this.config.model,
|
|
apiKey: this.config.apiKey,
|
|
baseUrl: this.config.baseUrl || null,
|
|
apiProtocol: this.config.apiProtocol || 'openai',
|
|
};
|
|
|
|
log.debug('Initializing with config:', {
|
|
provider: configRequest.provider,
|
|
model: configRequest.model,
|
|
hasApiKey: !!configRequest.apiKey,
|
|
baseUrl: configRequest.baseUrl,
|
|
apiProtocol: configRequest.apiProtocol,
|
|
});
|
|
|
|
const status = await invoke<KernelStatus>('kernel_init', {
|
|
configRequest,
|
|
});
|
|
this.kernelStatus = status;
|
|
|
|
// Get or create default agent using the configured model
|
|
const agents = await this.listAgents();
|
|
if (agents.length > 0) {
|
|
this.defaultAgentId = agents[0].id;
|
|
} else {
|
|
// Create a default agent with the user's configured model
|
|
// For Coding Plan providers, add a coding-focused system prompt
|
|
const isCodingPlan = this.config.provider?.includes('coding') ||
|
|
this.config.baseUrl?.includes('coding.dashscope');
|
|
|
|
const systemPrompt = isCodingPlan
|
|
? '你是一个专业的编程助手。你可以帮助用户解决编程问题、写代码、调试、解释技术概念等。请用中文回答问题。'
|
|
: '你是 ZCLAW 智能助手,可以帮助用户解决各种问题。请用中文回答。';
|
|
|
|
const agent = await this.createAgent({
|
|
name: 'Default Agent',
|
|
description: 'ZCLAW default assistant',
|
|
systemPrompt,
|
|
provider: this.config.provider,
|
|
model: this.config.model,
|
|
});
|
|
this.defaultAgentId = agent.id;
|
|
}
|
|
|
|
this.setState('connected');
|
|
this.emitEvent('connected', { version: '0.1.0-internal' });
|
|
this.log('info', 'Connected to internal ZCLAW Kernel');
|
|
} catch (err: unknown) {
|
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
this.setState('disconnected');
|
|
this.log('error', `Failed to initialize kernel: ${errorMessage}`);
|
|
throw new Error(`Failed to initialize kernel: ${errorMessage}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Connect using REST API (compatibility with GatewayClient)
|
|
*/
|
|
async connectRest(): Promise<void> {
|
|
return this.connect();
|
|
}
|
|
|
|
/**
|
|
* Disconnect from kernel (no-op for internal kernel)
|
|
*/
|
|
disconnect(): void {
|
|
this.setState('disconnected');
|
|
this.kernelStatus = null;
|
|
this.log('info', 'Disconnected from internal kernel');
|
|
}
|
|
|
|
// === GatewayClient Compatibility ===
|
|
|
|
/**
|
|
* Health check
|
|
*/
|
|
async health(): Promise<{ status: string; version?: string }> {
|
|
if (this.kernelStatus?.initialized) {
|
|
return { status: 'ok', version: '0.1.0-internal' };
|
|
}
|
|
return { status: 'not_initialized' };
|
|
}
|
|
|
|
/**
|
|
* Get status
|
|
*/
|
|
async status(): Promise<Record<string, unknown>> {
|
|
const status = await invoke<KernelStatus>('kernel_status');
|
|
return {
|
|
initialized: status.initialized,
|
|
agentCount: status.agentCount,
|
|
defaultProvider: status.baseUrl,
|
|
defaultModel: status.model,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* REST API compatibility stubs
|
|
*/
|
|
public getRestBaseUrl(): string {
|
|
return ''; // Internal kernel doesn't use REST
|
|
}
|
|
|
|
public async restGet<T>(_path: string): Promise<T> {
|
|
throw new Error('REST API not available for internal kernel');
|
|
}
|
|
|
|
public async restPost<T>(_path: string, _body?: unknown): Promise<T> {
|
|
throw new Error('REST API not available for internal kernel');
|
|
}
|
|
|
|
public async restPut<T>(_path: string, _body?: unknown): Promise<T> {
|
|
throw new Error('REST API not available for internal kernel');
|
|
}
|
|
|
|
public async restDelete<T>(_path: string): Promise<T> {
|
|
throw new Error('REST API not available for internal kernel');
|
|
}
|
|
|
|
public async restPatch<T>(_path: string, _body?: unknown): Promise<T> {
|
|
throw new Error('REST API not available for internal kernel');
|
|
}
|
|
|
|
// === Events ===
|
|
|
|
/**
|
|
* Subscribe to events
|
|
*/
|
|
on(event: string, callback: EventCallback): () => void {
|
|
if (!this.eventListeners.has(event)) {
|
|
this.eventListeners.set(event, new Set());
|
|
}
|
|
this.eventListeners.get(event)!.add(callback);
|
|
|
|
return () => {
|
|
this.eventListeners.get(event)?.delete(callback);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Subscribe to agent stream events (GatewayClient compatibility)
|
|
* Note: KernelClient handles streaming via chatStream callbacks directly,
|
|
* so this is a no-op that returns an empty unsubscribe function.
|
|
*/
|
|
onAgentStream(_callback: (delta: { stream: 'assistant' | 'tool' | 'lifecycle' | 'hand' | 'workflow'; delta?: string; content?: string; runId?: string }) => void): () => void {
|
|
// KernelClient uses chatStream callbacks for streaming, not a separate event stream
|
|
// Return empty unsubscribe for compatibility
|
|
return () => {};
|
|
}
|
|
|
|
// === Internal helpers (non-private so mixin modules can use them via `this`) ===
|
|
|
|
setState(state: ConnectionState): void {
|
|
this.state = state;
|
|
this.onStateChange?.(state);
|
|
this.emitEvent('state', state);
|
|
}
|
|
|
|
emitEvent(event: string, payload: unknown): void {
|
|
const listeners = this.eventListeners.get(event);
|
|
if (listeners) {
|
|
for (const cb of listeners) {
|
|
try {
|
|
cb(payload);
|
|
} catch (e) {
|
|
log.debug('Event listener threw error', { error: e });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
log(level: string, message: string): void {
|
|
this.onLog?.(level, message);
|
|
}
|
|
|
|
/**
|
|
* Format error for consistent logging
|
|
*/
|
|
formatError(error: unknown): string {
|
|
if (error instanceof Error) {
|
|
return error.message;
|
|
}
|
|
return String(error);
|
|
}
|
|
}
|
|
|
|
// === Install domain methods from mixin modules ===
|
|
|
|
import { installAgentMethods } from './kernel-agent';
|
|
import { installChatMethods } from './kernel-chat';
|
|
import { installHandMethods } from './kernel-hands';
|
|
import { installSkillMethods } from './kernel-skills';
|
|
import { installTriggerMethods } from './kernel-triggers';
|
|
import { installA2aMethods } from './kernel-a2a';
|
|
|
|
installAgentMethods(KernelClient);
|
|
installChatMethods(KernelClient);
|
|
installHandMethods(KernelClient);
|
|
installSkillMethods(KernelClient);
|
|
installTriggerMethods(KernelClient);
|
|
installA2aMethods(KernelClient);
|
|
|
|
// === API Method Type Declarations ===
|
|
// These methods are installed at runtime by the mixin modules above.
|
|
// We declare them here so TypeScript knows they exist on KernelClient.
|
|
|
|
export interface KernelClient {
|
|
// Agent management (kernel-agent.ts)
|
|
listAgents(): Promise<import('./kernel-types').AgentInfo[]>;
|
|
getAgent(agentId: string): Promise<import('./kernel-types').AgentInfo | null>;
|
|
createAgent(request: import('./kernel-types').CreateAgentRequest): Promise<import('./kernel-types').CreateAgentResponse>;
|
|
deleteAgent(agentId: string): Promise<void>;
|
|
listClones(): Promise<{ clones: Array<{ id: string; name: string; [key: string]: unknown }> }>;
|
|
createClone(opts: { name: string; role?: string; model?: string; personality?: string; communicationStyle?: string; [key: string]: unknown }): Promise<{ clone: { id: string; name: string; [key: string]: unknown } }>;
|
|
deleteClone(id: string): Promise<void>;
|
|
updateClone(id: string, updates: Record<string, unknown>): Promise<{ clone: unknown }>;
|
|
|
|
// Chat (kernel-chat.ts)
|
|
chat(message: string, opts?: { sessionKey?: string; agentId?: string }): Promise<{ runId: string; sessionId?: string; response?: string }>;
|
|
chatStream(message: string, callbacks: import('./kernel-types').StreamCallbacks, opts?: { sessionKey?: string; agentId?: string; thinking_enabled?: boolean; reasoning_effort?: string; plan_mode?: boolean; subagent_enabled?: boolean }): Promise<{ runId: string }>;
|
|
cancelStream(sessionId: string): Promise<void>;
|
|
fetchDefaultAgentId(): Promise<string | null>;
|
|
setDefaultAgentId(agentId: string): void;
|
|
getDefaultAgentId(): string;
|
|
|
|
// Hands (kernel-hands.ts)
|
|
listHands(): Promise<{
|
|
hands: {
|
|
id?: string; name: string; description?: string; status?: string;
|
|
requirements_met?: boolean; category?: string; icon?: string;
|
|
tool_count?: number; tools?: string[]; metric_count?: number; metrics?: string[];
|
|
}[]
|
|
}>;
|
|
getHand(name: string): Promise<{
|
|
id?: string; name?: string; description?: string; status?: string;
|
|
requirements_met?: boolean; category?: string; icon?: string;
|
|
provider?: string; model?: string;
|
|
requirements?: { description?: string; name?: string; met?: boolean; satisfied?: boolean; details?: string; hint?: string }[];
|
|
tools?: string[]; metrics?: string[]; config?: Record<string, unknown>;
|
|
tool_count?: number; metric_count?: number;
|
|
}>;
|
|
triggerHand(name: string, params?: Record<string, unknown>, autonomyLevel?: string): Promise<{ runId: string; status: string }>;
|
|
getHandStatus(name: string, runId: string): Promise<{ status: string; result?: unknown }>;
|
|
approveHand(name: string, runId: string, approved: boolean, reason?: string): Promise<{ status: string }>;
|
|
cancelHand(name: string, runId: string): Promise<{ status: string }>;
|
|
listHandRuns(name: string, opts?: { limit?: number; offset?: number }): Promise<{
|
|
runs: {
|
|
runId?: string; run_id?: string; id?: string; status?: string;
|
|
startedAt?: string; started_at?: string; completedAt?: string;
|
|
completed_at?: string; result?: unknown; error?: string;
|
|
}[]
|
|
}>;
|
|
listApprovals(status?: string): Promise<{
|
|
approvals: Array<{
|
|
id: string; handId: string; status: string; createdAt: string;
|
|
input: Record<string, unknown>;
|
|
}>
|
|
}>;
|
|
respondToApproval(approvalId: string, approved: boolean, reason?: string): Promise<void>;
|
|
|
|
// Skills (kernel-skills.ts)
|
|
listSkills(): Promise<{
|
|
skills: {
|
|
id: string; name: string; description: string; version: string;
|
|
capabilities: string[]; tags: string[]; mode: string;
|
|
enabled: boolean; triggers: string[]; category?: string;
|
|
}[]
|
|
}>;
|
|
refreshSkills(skillDir?: string): Promise<{
|
|
skills: {
|
|
id: string; name: string; description: string; version: string;
|
|
capabilities: string[]; tags: string[]; mode: string;
|
|
enabled: boolean; triggers: string[]; category?: string;
|
|
}[]
|
|
}>;
|
|
createSkill(skill: {
|
|
name: string; description?: string;
|
|
triggers: Array<{ type: string; pattern?: string }>;
|
|
actions: Array<{ type: string; params?: Record<string, unknown> }>;
|
|
enabled?: boolean;
|
|
}): Promise<{ skill?: {
|
|
id: string; name: string; description: string; version: string;
|
|
capabilities: string[]; tags: string[]; mode: string;
|
|
enabled: boolean; triggers: string[]; category?: string;
|
|
} }>;
|
|
updateSkill(id: string, updates: {
|
|
name?: string; description?: string;
|
|
triggers?: Array<{ type: string; pattern?: string }>;
|
|
actions?: Array<{ type: string; params?: Record<string, unknown> }>;
|
|
enabled?: boolean;
|
|
}): Promise<{ skill?: {
|
|
id: string; name: string; description: string; version: string;
|
|
capabilities: string[]; tags: string[]; mode: string;
|
|
enabled: boolean; triggers: string[]; category?: string;
|
|
} }>;
|
|
deleteSkill(id: string): Promise<void>;
|
|
executeSkill(id: string, input?: Record<string, unknown>): Promise<{
|
|
success: boolean; output?: unknown; error?: string; durationMs?: number;
|
|
}>;
|
|
|
|
// Triggers (kernel-triggers.ts)
|
|
listTriggers(): Promise<{
|
|
triggers?: Array<{
|
|
id: string; name: string; handId: string; triggerType: string;
|
|
enabled: boolean; createdAt: string; modifiedAt: string;
|
|
description?: string; tags: string[];
|
|
}>
|
|
}>;
|
|
getTrigger(id: string): Promise<{
|
|
id: string; name: string; handId: string; triggerType: string;
|
|
enabled: boolean; createdAt: string; modifiedAt: string;
|
|
description?: string; tags: string[];
|
|
} | null>;
|
|
createTrigger(trigger: {
|
|
id: string; name: string; handId: string;
|
|
triggerType: { type: string; cron?: string; pattern?: string; path?: string; secret?: string; events?: string[] };
|
|
enabled?: boolean; description?: string; tags?: string[];
|
|
}): Promise<{
|
|
id: string; name: string; handId: string; triggerType: string;
|
|
enabled: boolean; createdAt: string; modifiedAt: string;
|
|
description?: string; tags: string[];
|
|
} | null>;
|
|
updateTrigger(id: string, updates: {
|
|
name?: string; enabled?: boolean; handId?: string;
|
|
triggerType?: { type: string; cron?: string; pattern?: string; path?: string; secret?: string; events?: string[] };
|
|
}): Promise<{
|
|
id: string; name: string; handId: string; triggerType: string;
|
|
enabled: boolean; createdAt: string; modifiedAt: string;
|
|
description?: string; tags: string[];
|
|
}>;
|
|
deleteTrigger(id: string): Promise<void>;
|
|
executeTrigger(id: string, input?: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
|
|
// A2A (kernel-a2a.ts)
|
|
a2aSend(from: string, to: string, payload: unknown, messageType?: string): Promise<void>;
|
|
a2aBroadcast(from: string, payload: unknown): Promise<void>;
|
|
a2aDiscover(capability: string): Promise<Array<{
|
|
id: string; name: string; description: string;
|
|
capabilities: Array<{ name: string; description: string }>;
|
|
role: string; priority: number;
|
|
}>>;
|
|
a2aDelegateTask(from: string, to: string, task: string, timeoutMs?: number): Promise<unknown>;
|
|
}
|
|
|
|
// === Singleton ===
|
|
|
|
let _client: KernelClient | null = null;
|
|
|
|
/**
|
|
* Get the kernel client singleton
|
|
*/
|
|
export function getKernelClient(opts?: ConstructorParameters<typeof KernelClient>[0]): KernelClient {
|
|
if (!_client) {
|
|
_client = new KernelClient(opts);
|
|
} else if (opts) {
|
|
_client.updateOptions(opts);
|
|
}
|
|
return _client;
|
|
}
|
|
|
|
/**
|
|
* Check if internal kernel mode is available
|
|
*/
|
|
export function isInternalKernelAvailable(): boolean {
|
|
return isTauriRuntime();
|
|
}
|