feat: add tool status management and localization for skill installation

- Updated chat message types to include tool statuses.
- Enhanced localization files for English, Thai, and Chinese to support new tool status messages.
- Modified HomePage and SkillsPage components to handle tool statuses in chat messages.
- Implemented tool status merging and updating logic in the chat store.
- Added handling for tool status events in the gateway event processing.
- Created tests for chat message rendering with tool statuses and skill installation shortcuts.
- Improved gateway event dispatching for tool lifecycle events.
This commit is contained in:
duanshuwen
2026-04-23 20:27:54 +08:00
parent 979fb0a0f6
commit df600272d6
29 changed files with 2041 additions and 384 deletions

View File

@@ -1,4 +1,3 @@
import { randomUUID } from 'node:crypto';
import { EventEmitter } from 'node:events';
import { createServer } from 'node:net';
import { join } from 'node:path';
@@ -63,6 +62,7 @@ import {
import { GatewayStateController, type GatewayRuntimeStatus } from './state';
import { connectGatewaySocket, waitForGatewayReady } from './ws-client';
import { dispatchGatewayRpcMethod } from './rpc-dispatch';
import { createRandomId } from './random-id';
type RuntimeChangeBroadcast = {
topics: RuntimeRefreshTopic[];
@@ -95,6 +95,11 @@ type GatewayNotificationEvent =
params?: unknown;
};
type GatewayToolStatusNotification = {
status: 'running' | 'completed' | 'error';
payload?: unknown;
};
export interface GatewayManagerEvents {
status: (status: GatewayRuntimeStatus) => void;
message: (message: unknown) => void;
@@ -104,6 +109,7 @@ export interface GatewayManagerEvents {
'channel:status': (data: { channelId: string; status: string }) => void;
'chat:message': (data: { message: unknown }) => void;
'gateway:ready': (payload: unknown) => void;
'tool:status': (payload: GatewayToolStatusNotification) => void;
}
function isRecord(value: unknown): value is Record<string, unknown> {
@@ -243,6 +249,78 @@ function extractTextFromGatewayPayload(payload: Record<string, unknown>): string
return '';
}
function getRecordStringField(record: Record<string, unknown>, ...keys: string[]): string | undefined {
for (const key of keys) {
const value = record[key];
if (typeof value === 'string' && value.trim()) {
return value;
}
}
return undefined;
}
function getRecordNumberField(record: Record<string, unknown>, ...keys: string[]): number | undefined {
for (const key of keys) {
const value = record[key];
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
}
return undefined;
}
function normalizeGatewayToolStatus(value: unknown): 'running' | 'completed' | 'error' {
if (typeof value !== 'string') {
return 'completed';
}
switch (value.trim().toLowerCase()) {
case 'running':
case 'completed':
case 'error':
return value.trim().toLowerCase() as 'running' | 'completed' | 'error';
case 'failed':
return 'error';
default:
return 'completed';
}
}
function normalizeToolStatusEvent(value: GatewayToolStatusNotification): GatewayEvent | null {
const payload = isRecord(value.payload) ? value.payload : {};
const sessionKey = getRecordStringField(payload, 'sessionKey', 'session_key');
const runId = getRecordStringField(payload, 'runId', 'run_id');
const toolName = getRecordStringField(payload, 'toolName', 'tool_name', 'name');
if (!sessionKey || !runId || !toolName) {
return null;
}
const toolCallId = getRecordStringField(payload, 'toolCallId', 'tool_call_id', 'id');
const summary = getRecordStringField(payload, 'summary', 'message');
const durationMs = getRecordNumberField(payload, 'durationMs', 'duration_ms');
const errorMessage = getRecordStringField(payload, 'error');
const status = errorMessage
? 'error'
: normalizeGatewayToolStatus(payload.status ?? value.status);
return {
type: 'tool:status',
sessionKey: normalizeAgentSessionKey(sessionKey),
runId,
toolCallId,
toolName,
status,
updatedAt: normalizeTimestamp(payload.timestamp) ?? Date.now(),
summary: errorMessage ?? summary,
durationMs,
input: payload.input ?? payload.arguments ?? payload.args,
result: payload.result,
};
}
function buildGatewayRpcError(error: unknown, fallback: string): Error {
if (typeof error === 'string' && error.trim()) {
return new Error(error);
@@ -292,7 +370,7 @@ export class GatewayManager extends EventEmitter {
private readonly processOwner = new OpenClawProcessOwner();
private readonly pendingRequests = new Map<string, PendingGatewayRequest>();
private readonly deltaSnapshots = new Map<string, string>();
private gatewayToken = randomUUID();
private gatewayToken = createRandomId();
private socket: WebSocket | null = null;
private child: GatewayProcessHandle | null = null;
private port: number | null = null;
@@ -343,6 +421,7 @@ export class GatewayManager extends EventEmitter {
override on(eventName: 'channel:status', listener: GatewayManagerEvents['channel:status']): this;
override on(eventName: 'chat:message', listener: GatewayManagerEvents['chat:message']): this;
override on(eventName: 'gateway:ready', listener: GatewayManagerEvents['gateway:ready']): this;
override on(eventName: 'tool:status', listener: GatewayManagerEvents['tool:status']): this;
override on(eventName: string | symbol, listener: (...args: any[]) => void): this;
override on(eventName: string | symbol, listener: (...args: any[]) => void): this {
return super.on(eventName, listener);
@@ -356,6 +435,7 @@ export class GatewayManager extends EventEmitter {
override once(eventName: 'channel:status', listener: GatewayManagerEvents['channel:status']): this;
override once(eventName: 'chat:message', listener: GatewayManagerEvents['chat:message']): this;
override once(eventName: 'gateway:ready', listener: GatewayManagerEvents['gateway:ready']): this;
override once(eventName: 'tool:status', listener: GatewayManagerEvents['tool:status']): this;
override once(eventName: string | symbol, listener: (...args: any[]) => void): this;
override once(eventName: string | symbol, listener: (...args: any[]) => void): this {
return super.once(eventName, listener);
@@ -369,6 +449,7 @@ export class GatewayManager extends EventEmitter {
override off(eventName: 'channel:status', listener: GatewayManagerEvents['channel:status']): this;
override off(eventName: 'chat:message', listener: GatewayManagerEvents['chat:message']): this;
override off(eventName: 'gateway:ready', listener: GatewayManagerEvents['gateway:ready']): this;
override off(eventName: 'tool:status', listener: GatewayManagerEvents['tool:status']): this;
override off(eventName: string | symbol, listener: (...args: any[]) => void): this;
override off(eventName: string | symbol, listener: (...args: any[]) => void): this {
return super.off(eventName, listener);
@@ -414,6 +495,13 @@ export class GatewayManager extends EventEmitter {
}
});
this.on('tool:status', (payload) => {
const event = normalizeToolStatusEvent(payload);
if (event) {
this.broadcast(event);
}
});
this.on('error', (error) => {
logManager.debug('GatewayManager emitted error event', error);
});