mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-08 01:43:07 +00:00
feat: new codex remote mode
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
import { logger } from '@/ui/logger';
|
||||
|
||||
type ConvertedEvent = {
|
||||
type: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return null;
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function asString(value: unknown): string | null {
|
||||
return typeof value === 'string' && value.length > 0 ? value : null;
|
||||
}
|
||||
|
||||
function asBoolean(value: unknown): boolean | null {
|
||||
return typeof value === 'boolean' ? value : null;
|
||||
}
|
||||
|
||||
function asNumber(value: unknown): number | null {
|
||||
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function extractItemId(params: Record<string, unknown>): string | null {
|
||||
const direct = asString(params.itemId ?? params.item_id ?? params.id);
|
||||
if (direct) return direct;
|
||||
|
||||
const item = asRecord(params.item);
|
||||
if (item) {
|
||||
return asString(item.id ?? item.itemId ?? item.item_id);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractItem(params: Record<string, unknown>): Record<string, unknown> | null {
|
||||
const item = asRecord(params.item);
|
||||
return item ?? params;
|
||||
}
|
||||
|
||||
function normalizeItemType(value: unknown): string | null {
|
||||
const raw = asString(value);
|
||||
if (!raw) return null;
|
||||
return raw.toLowerCase().replace(/[\s_-]/g, '');
|
||||
}
|
||||
|
||||
function extractCommand(value: unknown): string | null {
|
||||
if (typeof value === 'string') return value;
|
||||
if (Array.isArray(value)) {
|
||||
const parts = value.filter((part): part is string => typeof part === 'string');
|
||||
return parts.length > 0 ? parts.join(' ') : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractChanges(value: unknown): Record<string, unknown> | null {
|
||||
const record = asRecord(value);
|
||||
if (record) return record;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const changes: Record<string, unknown> = {};
|
||||
for (const entry of value) {
|
||||
const entryRecord = asRecord(entry);
|
||||
if (!entryRecord) continue;
|
||||
const path = asString(entryRecord.path ?? entryRecord.file ?? entryRecord.filePath ?? entryRecord.file_path);
|
||||
if (path) {
|
||||
changes[path] = entryRecord;
|
||||
}
|
||||
}
|
||||
return Object.keys(changes).length > 0 ? changes : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export class AppServerEventConverter {
|
||||
private readonly agentMessageBuffers = new Map<string, string>();
|
||||
private readonly reasoningBuffers = new Map<string, string>();
|
||||
private readonly commandOutputBuffers = new Map<string, string>();
|
||||
private readonly commandMeta = new Map<string, Record<string, unknown>>();
|
||||
private readonly fileChangeMeta = new Map<string, Record<string, unknown>>();
|
||||
|
||||
handleNotification(method: string, params: unknown): ConvertedEvent[] {
|
||||
const events: ConvertedEvent[] = [];
|
||||
const paramsRecord = asRecord(params) ?? {};
|
||||
|
||||
if (method === 'thread/started' || method === 'thread/resumed') {
|
||||
const thread = asRecord(paramsRecord.thread) ?? paramsRecord;
|
||||
const threadId = asString(thread.threadId ?? thread.thread_id ?? thread.id);
|
||||
if (threadId) {
|
||||
events.push({ type: 'thread_started', thread_id: threadId });
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
if (method === 'turn/started') {
|
||||
const turn = asRecord(paramsRecord.turn) ?? paramsRecord;
|
||||
const turnId = asString(turn.turnId ?? turn.turn_id ?? turn.id);
|
||||
events.push({ type: 'task_started', ...(turnId ? { turn_id: turnId } : {}) });
|
||||
return events;
|
||||
}
|
||||
|
||||
if (method === 'turn/completed') {
|
||||
const turn = asRecord(paramsRecord.turn) ?? paramsRecord;
|
||||
const statusRaw = asString(paramsRecord.status ?? turn.status);
|
||||
const status = statusRaw?.toLowerCase();
|
||||
const turnId = asString(turn.turnId ?? turn.turn_id ?? turn.id);
|
||||
const errorMessage = asString(paramsRecord.error ?? paramsRecord.message ?? paramsRecord.reason);
|
||||
|
||||
if (status === 'interrupted' || status === 'cancelled' || status === 'canceled') {
|
||||
events.push({ type: 'turn_aborted', ...(turnId ? { turn_id: turnId } : {}) });
|
||||
return events;
|
||||
}
|
||||
|
||||
if (status === 'failed' || status === 'error') {
|
||||
events.push({ type: 'task_failed', ...(turnId ? { turn_id: turnId } : {}), ...(errorMessage ? { error: errorMessage } : {}) });
|
||||
return events;
|
||||
}
|
||||
|
||||
events.push({ type: 'task_complete', ...(turnId ? { turn_id: turnId } : {}) });
|
||||
return events;
|
||||
}
|
||||
|
||||
if (method === 'turn/diff/updated') {
|
||||
const diff = asString(paramsRecord.diff ?? paramsRecord.unified_diff ?? paramsRecord.unifiedDiff);
|
||||
if (diff) {
|
||||
events.push({ type: 'turn_diff', unified_diff: diff });
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
if (method === 'thread/tokenUsage/updated') {
|
||||
const info = asRecord(paramsRecord.tokenUsage ?? paramsRecord.token_usage ?? paramsRecord) ?? {};
|
||||
events.push({ type: 'token_count', info });
|
||||
return events;
|
||||
}
|
||||
|
||||
if (method === 'error') {
|
||||
const willRetry = asBoolean(paramsRecord.will_retry ?? paramsRecord.willRetry) ?? false;
|
||||
if (willRetry) return events;
|
||||
const message = asString(paramsRecord.message) ?? asString(asRecord(paramsRecord.error)?.message);
|
||||
if (message) {
|
||||
events.push({ type: 'task_failed', error: message });
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
if (method === 'item/agentMessage/delta') {
|
||||
const itemId = extractItemId(paramsRecord);
|
||||
const delta = asString(paramsRecord.delta ?? paramsRecord.text ?? paramsRecord.message);
|
||||
if (itemId && delta) {
|
||||
const prev = this.agentMessageBuffers.get(itemId) ?? '';
|
||||
this.agentMessageBuffers.set(itemId, prev + delta);
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
if (method === 'item/reasoning/textDelta') {
|
||||
const itemId = extractItemId(paramsRecord) ?? 'reasoning';
|
||||
const delta = asString(paramsRecord.delta ?? paramsRecord.text ?? paramsRecord.message);
|
||||
if (delta) {
|
||||
const prev = this.reasoningBuffers.get(itemId) ?? '';
|
||||
this.reasoningBuffers.set(itemId, prev + delta);
|
||||
events.push({ type: 'agent_reasoning_delta', delta });
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
if (method === 'item/reasoning/summaryPartAdded') {
|
||||
events.push({ type: 'agent_reasoning_section_break' });
|
||||
return events;
|
||||
}
|
||||
|
||||
if (method === 'item/commandExecution/outputDelta') {
|
||||
const itemId = extractItemId(paramsRecord);
|
||||
const delta = asString(paramsRecord.delta ?? paramsRecord.text ?? paramsRecord.output ?? paramsRecord.stdout);
|
||||
if (itemId && delta) {
|
||||
const prev = this.commandOutputBuffers.get(itemId) ?? '';
|
||||
this.commandOutputBuffers.set(itemId, prev + delta);
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
if (method === 'item/started' || method === 'item/completed') {
|
||||
const item = extractItem(paramsRecord);
|
||||
if (!item) return events;
|
||||
|
||||
const itemType = normalizeItemType(item.type ?? item.itemType ?? item.kind);
|
||||
const itemId = extractItemId(paramsRecord) ?? asString(item.id ?? item.itemId ?? item.item_id);
|
||||
|
||||
if (!itemType || !itemId) {
|
||||
return events;
|
||||
}
|
||||
|
||||
if (itemType === 'agentmessage') {
|
||||
if (method === 'item/completed') {
|
||||
const text = asString(item.text ?? item.message ?? item.content) ?? this.agentMessageBuffers.get(itemId);
|
||||
if (text) {
|
||||
events.push({ type: 'agent_message', message: text });
|
||||
}
|
||||
this.agentMessageBuffers.delete(itemId);
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
if (itemType === 'reasoning') {
|
||||
if (method === 'item/completed') {
|
||||
const text = asString(item.text ?? item.message ?? item.content) ?? this.reasoningBuffers.get(itemId);
|
||||
if (text) {
|
||||
events.push({ type: 'agent_reasoning', text });
|
||||
}
|
||||
this.reasoningBuffers.delete(itemId);
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
if (itemType === 'commandexecution') {
|
||||
if (method === 'item/started') {
|
||||
const command = extractCommand(item.command ?? item.cmd ?? item.args);
|
||||
const cwd = asString(item.cwd ?? item.workingDirectory ?? item.working_directory);
|
||||
const autoApproved = asBoolean(item.autoApproved ?? item.auto_approved);
|
||||
const meta: Record<string, unknown> = {};
|
||||
if (command) meta.command = command;
|
||||
if (cwd) meta.cwd = cwd;
|
||||
if (autoApproved !== null) meta.auto_approved = autoApproved;
|
||||
this.commandMeta.set(itemId, meta);
|
||||
|
||||
events.push({
|
||||
type: 'exec_command_begin',
|
||||
call_id: itemId,
|
||||
...meta
|
||||
});
|
||||
}
|
||||
|
||||
if (method === 'item/completed') {
|
||||
const meta = this.commandMeta.get(itemId) ?? {};
|
||||
const output = asString(item.output ?? item.result ?? item.stdout) ?? this.commandOutputBuffers.get(itemId);
|
||||
const stderr = asString(item.stderr);
|
||||
const error = asString(item.error);
|
||||
const exitCode = asNumber(item.exitCode ?? item.exit_code ?? item.exitcode);
|
||||
const status = asString(item.status);
|
||||
|
||||
events.push({
|
||||
type: 'exec_command_end',
|
||||
call_id: itemId,
|
||||
...meta,
|
||||
...(output ? { output } : {}),
|
||||
...(stderr ? { stderr } : {}),
|
||||
...(error ? { error } : {}),
|
||||
...(exitCode !== null ? { exit_code: exitCode } : {}),
|
||||
...(status ? { status } : {})
|
||||
});
|
||||
|
||||
this.commandMeta.delete(itemId);
|
||||
this.commandOutputBuffers.delete(itemId);
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
if (itemType === 'filechange') {
|
||||
if (method === 'item/started') {
|
||||
const changes = extractChanges(item.changes ?? item.change ?? item.diff);
|
||||
const autoApproved = asBoolean(item.autoApproved ?? item.auto_approved);
|
||||
const meta: Record<string, unknown> = {};
|
||||
if (changes) meta.changes = changes;
|
||||
if (autoApproved !== null) meta.auto_approved = autoApproved;
|
||||
this.fileChangeMeta.set(itemId, meta);
|
||||
|
||||
events.push({
|
||||
type: 'patch_apply_begin',
|
||||
call_id: itemId,
|
||||
...meta
|
||||
});
|
||||
}
|
||||
|
||||
if (method === 'item/completed') {
|
||||
const meta = this.fileChangeMeta.get(itemId) ?? {};
|
||||
const stdout = asString(item.stdout ?? item.output);
|
||||
const stderr = asString(item.stderr);
|
||||
const success = asBoolean(item.success ?? item.ok ?? item.applied ?? item.status === 'completed');
|
||||
|
||||
events.push({
|
||||
type: 'patch_apply_end',
|
||||
call_id: itemId,
|
||||
...meta,
|
||||
...(stdout ? { stdout } : {}),
|
||||
...(stderr ? { stderr } : {}),
|
||||
success: success ?? false
|
||||
});
|
||||
|
||||
this.fileChangeMeta.delete(itemId);
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('[AppServerEventConverter] Unhandled notification', { method, params });
|
||||
return events;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.agentMessageBuffers.clear();
|
||||
this.reasoningBuffers.clear();
|
||||
this.commandOutputBuffers.clear();
|
||||
this.commandMeta.clear();
|
||||
this.fileChangeMeta.clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user