Files
makelore/electron/services/cloud-agent-operations.ts

350 lines
31 KiB
TypeScript

import type { CloudAgentOperations } from '../../shared/cloud-agents';
type OperationSpec = {
method: string; path: string; body?: readonly string[]; output: readonly string[];
query?: Record<string, string>; fixedQuery?: Record<string, string>; cloud?: 'ws'; project?: (value: unknown) => unknown;
};
const prompt = ['request_id', 'thread_id', 'query', 'expected_revision', 'attachment_file_ids'];
const request = ['request_id', 'thread_id', 'run_id', 'status', 'version'];
const schedule = ['name', 'prompt', 'cron_expression', 'timezone', 'enabled', 'result_notification'];
const job = ['id', 'agent_slug', ...schedule, 'next_run_at', 'runs'];
const entry = ['slug', 'name', 'purpose', 'published_version', 'is_creator', 'payer'];
const budget = ['agent_slug', 'unit', 'timezone', 'request_limit_points', 'daily_limit_points', 'daily_committed_points', 'resets_at'];
const draft = ['slug', 'name', 'purpose', 'system_prompt', 'draft_revision', 'updated_at', 'configuration', 'published_version', 'enabled', 'archived'];
const mcpInput = ['name', 'description', 'transport', 'url', 'headers'];
const mcpOutput = ['slug', 'name', 'description', 'transport', 'url', 'enabled', 'has_credentials'];
const child = ['name', 'purpose', 'system_prompt'];
const channelView = ['id', 'address', 'display_name', 'target_agent_address', 'route_revision', 'provider_generation', 'binding_id', 'enabled', 'status', 'worker_online',
'agent_slug', 'published_version', 'desired_state', 'sync_state', 'health', 'last_confirmed_at', 'revision', 'blockers',
'adoption_state', 'managed_agent_slug', 'target_agent_slug', 'target_agent_name', 'target_agent_published_version'];
const channelSession = ['session_id', 'caller_id', 'agent_slug', 'agent_name', 'binding_id', 'provider_generation', 'channel_account_id', 'core_conversation_id', 'access_mode', 'grant_state', 'session_state', 'sequence', 'content_uid', 'thread_id', 'created_at', 'closed_at'];
const channelCaller = ['caller_id', 'binding_id', 'provider_generation', 'channel_account_id', 'agent_slug', 'core_conversation_id', 'access_mode', 'grant_state', 'paired_ws_account_id', 'session_id', 'content_uid', 'thread_id', 'session_state', 'created_at', 'revoked_at'];
const channelOperation = ['operation_id', 'status', 'steps', 'result', 'error_code'];
type ObjectRecord = Record<string, unknown>;
function object(value: unknown): ObjectRecord {
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as ObjectRecord : {};
}
function copy(value: unknown, keys: readonly string[]): ObjectRecord {
const source = object(value);
return Object.fromEntries(keys.filter(key => source[key] !== undefined).map(key => [key, source[key]]));
}
function list(value: unknown, project: (item: unknown) => unknown, max = 200): unknown[] {
return Array.isArray(value) ? value.slice(0, max).map(project) : [];
}
function safeHttps(value: unknown): string | undefined {
if (typeof value !== 'string' || value.length > 2048) return undefined;
try {
const url = new URL(value);
if (url.protocol !== 'https:' || url.username || url.password || url.hash) return undefined;
return url.toString();
} catch { return undefined; }
}
function projectChannelView(value: unknown): ObjectRecord {
const result = copy(value, channelView);
if (Array.isArray(result.blockers)) result.blockers = result.blockers.filter((item): item is string => typeof item === 'string').slice(0, 20);
return result;
}
function projectBindings(value: unknown): unknown {
const source = object(value);
return {
items: list(source.items, projectChannelView),
...(Array.isArray(source.available_accounts) ? { available_accounts: list(source.available_accounts, projectChannelView) } : {}),
};
}
function projectMutation(value: unknown): unknown {
const source = object(value);
return {
...(source.channel === undefined ? {} : { channel: projectChannelView(source.channel) }),
...(source.target_agent === undefined ? {} : { target_agent: source.target_agent === null ? null : typeof source.target_agent === 'string' ? source.target_agent : projectChannelView(source.target_agent) }),
...(source.worker && typeof source.worker === 'object' ? { worker: copy(source.worker, ['account_id', 'address']) } : {}),
...(source.wechat && typeof source.wechat === 'object' ? { wechat: copy(source.wechat, ['status', 'message']) } : {}),
...(source.disconnected === undefined ? {} : { disconnected: source.disconnected === true }),
};
}
function projectWechat(value: unknown): unknown {
const source = object(value);
const qrcode_url = safeHttps(source.qrcode_url);
return { ...copy(source, ['session_key', 'status', 'message']), ...(qrcode_url ? { qrcode_url } : {}) };
}
function projectSession(value: unknown): unknown { return copy(value, channelSession); }
function projectRun(value: unknown): unknown {
const source = object(value);
return { ...copy(source, ['agent_run_id', 'request_id', 'thread_id', 'agent_slug', 'status', 'output', 'version']),
...(source.error && typeof source.error === 'object' ? { error: copy(source.error, ['type', 'message']) } : {}),
...(source.interrupt && typeof source.interrupt === 'object' ? { interrupt: copy(source.interrupt, ['status', 'message', 'run_id', 'questions', 'approval']) } : {}),
};
}
function projectConversation(value: unknown): unknown {
const source = object(value);
return { ...projectSession(source), messages: list(source.messages, item => copy(item, ['id', 'role', 'content', 'request_id', 'run_id', 'created_at'])),
run: source.run == null ? null : projectRun(source.run),
queued_requests: list(source.queued_requests, item => copy(item, ['request_id', 'thread_id', 'run_id', 'status', 'version'])),
next_offset: source.next_offset ?? null };
}
function projectFiles(value: unknown): unknown {
const source = object(value);
return { session_id: source.session_id, thread_id: source.thread_id ?? null,
files: list(source.files, item => copy(item, ['name', 'path', 'directory_path', 'is_dir', 'size'])) };
}
function projectCallers(value: unknown): unknown { return { items: list(object(value).items, item => copy(item, channelCaller)) }; }
function projectDeliverySlot(value: unknown): ObjectRecord {
return copy(value, ['state', 'message_id', 'status', 'artifact_status', 'artifact_count', 'artifact_failure_count']);
}
function projectActivityDelivery(value: unknown): ObjectRecord {
const source = object(value);
const result = copy(source, ['state', 'delivery_id', 'logical_message_id', 'request_id', 'run_id', 'inbound_message_id']);
for (const key of ['ack', 'progress', 'result'] as const) if (source[key] !== undefined) result[key] = projectDeliverySlot(source[key]);
const core = object(source.core);
if (Object.keys(core).length) {
const projectedCore = copy(core, ['state', 'core_status', 'adapter_status', 'provider_status', 'delivery_status', 'part_count', 'sent_part_count', 'failed_part_count']);
if (Array.isArray(core.parts)) {
projectedCore.parts = list(core.parts, part => {
const item = object(part);
return copy(item, ['part_id', 'type', 'core_status', 'adapter_status', 'provider_status', 'error_category']);
});
}
result.core = projectedCore;
}
const outputMessageId = object(source.result).message_id;
if (typeof outputMessageId === 'string' && outputMessageId) result.logical_message_id = outputMessageId;
if (Array.isArray(core.parts)) {
result.parts = list(core.parts, part => {
const item = object(part);
return { part_id: item.part_id, kind: item.type, status: item.adapter_status ?? item.core_status ?? item.provider_status };
});
}
return result;
}
function projectActivity(value: unknown): unknown {
return {
items: list(object(value).items, item => {
const source = object(item);
const result = copy(source, ['run_id', 'request_id', 'agent_slug', 'caller_id', 'access_mode', 'binding_id', 'provider_generation', 'channel_account_id', 'status', 'source', 'channel', 'error_type']);
if (source.token_usage && typeof source.token_usage === 'object') result.token_usage = copy(source.token_usage, ['input_tokens', 'output_tokens', 'total_tokens', 'prompt_tokens', 'completion_tokens', 'cached_tokens']);
if (source.timing && typeof source.timing === 'object') result.timing = copy(source.timing, ['created_at', 'started_at', 'prepared_at', 'first_output_at', 'finished_at', 'first_model_request_at']);
if (source.delivery && typeof source.delivery === 'object') {
const delivery = object(source.delivery);
result.delivery = projectActivityDelivery(delivery);
}
if (source.input_preparation && typeof source.input_preparation === 'object') result.input_preparation = copy(source.input_preparation, ['intake_id', 'delivery_id', 'logical_message_id', 'state', 'prepared_file_count', 'native_request_id', 'native_run_id', 'error_type']);
return result;
}),
next_offset: object(value).next_offset ?? null,
};
}
function projectDelivery(value: unknown): unknown {
const source = object(value);
return { ...copy(source, ['logical_message_id', 'message_id', 'status']), parts: list(source.parts, item => copy(item, ['part_id', 'part_index', 'type', 'status', 'attempts', 'sent_at'])) };
}
function projectControl(value: unknown): unknown {
const source = object(value);
return { ...copy(source, ['operation_id', 'status', 'session_id', 'previous_session_id', 'run_id', 'request_id', 'intake_ids', 'action']),
...(source.session && typeof source.session === 'object' ? { session: projectSession(source.session) } : {}) };
}
function projectOperation(value: unknown): unknown {
const source = object(value);
const operationResult = source.result && typeof source.result === 'object'
? (object(source.result).channel !== undefined ? projectMutation(source.result) : projectChannelView(source.result))
: undefined;
return { ...copy(source, ['operation_id', 'status', 'error_code']),
...(Array.isArray(source.steps) ? { steps: list(source.steps, item => copy(item, ['name', 'step', 'operation_id', 'status', 'error_code'])) } : {}),
...(operationResult !== undefined ? { result: operationResult } : {}) };
}
function projectScheduleNotification(value: unknown): unknown {
const source = object(value);
return copy(source, ['enabled', 'state', 'error_message', 'channel_account_id', 'caller_id']);
}
function projectSchedule(value: unknown): unknown {
const source = object(value);
const result = copy(source, ['id', 'agent_slug', 'name', 'prompt', 'cron_expression', 'timezone', 'enabled', 'next_run_at']);
if (source.result_notification !== undefined) result.result_notification = source.result_notification === null ? null : projectScheduleNotification(source.result_notification);
if (Array.isArray(source.runs)) {
result.runs = list(source.runs, item => {
const run = copy(item, ['status', 'thread_id', 'error_message', 'conversation_available', 'completed_at', 'run_id']);
if (object(item).result_notification !== undefined) run.result_notification = object(item).result_notification === null ? null : projectScheduleNotification(object(item).result_notification);
return run;
}, 20);
}
return result;
}
function projectSchedules(value: unknown): unknown { return { jobs: list(object(value).jobs, projectSchedule) }; }
function validateChannelInput(operation: string, args: ObjectRecord): void {
const channelOperations = new Set([
'createChannelAccount', 'routeChannelAccount', 'enableChannelAccount', 'pauseChannelAccount',
'disconnectChannelAccount', 'channelAccountWechatBindStart', 'channelAccountWechatBindStatus',
'channelAccountWechatBindVerification', 'channelAccountWechatBindAccount', 'channelAccountWechatUnbind',
'channelAccountOperation',
'createChannelBinding', 'switchChannelBindingAgent', 'enableChannelBinding', 'pauseChannelBinding',
'disconnectChannelBinding', 'wechatBindStart',
'wechatBindStatus', 'wechatBindVerification', 'wechatBindAccount', 'wechatUnbind', 'channelSelfCallers',
'channelActivity', 'channelConversations', 'channelConversation',
'channelFiles', 'channelConversationControl', 'channelDelivery', 'retryChannelDeliveryPart', 'channelOperation',
]);
if (channelOperations.has(operation)) {
for (const key of ['operation_id', 'channel_account_id', 'caller_id', 'session_id', 'session_key', 'logical_message_id', 'part_id', 'run_id']) {
const value = args[key];
const valid = key === 'session_key'
? typeof value === 'string' && /^[\x21-\x7e]{1,512}$/.test(value)
: typeof value === 'string' && /^[a-zA-Z0-9._:#-]{1,256}$/.test(value);
if (value !== undefined && !valid) throw new Error('invalid_id');
}
if (args.operation_id !== undefined && (typeof args.operation_id !== 'string' || args.operation_id.length > 128)) throw new Error('invalid_id');
}
if (operation === 'createChannelBinding' && (typeof args.display_name !== 'string' || !args.display_name.trim() || args.display_name.length > 100)) throw new Error('invalid_input');
if (operation === 'createChannelAccount' && (typeof args.display_name !== 'string' || !args.display_name.trim() || args.display_name.length > 100)) throw new Error('invalid_input');
if (operation === 'routeChannelAccount' && args.target_agent_slug !== null
&& (typeof args.target_agent_slug !== 'string' || !/^ml-[a-f0-9]{32}$/.test(args.target_agent_slug))) throw new Error('invalid_id');
if (operation === 'routeChannelAccount' && args.target_agent_slug === null && args.enabled !== false) throw new Error('invalid_input');
if (operation === 'switchChannelBindingAgent' && (typeof args.target_agent_slug !== 'string' || !/^ml-[a-f0-9]{32}$/.test(args.target_agent_slug))) throw new Error('invalid_id');
if (operation === 'wechatBindStart' && typeof args.force !== 'boolean') throw new Error('invalid_input');
if (operation === 'channelAccountWechatBindStart' && typeof args.force !== 'boolean') throw new Error('invalid_input');
if (operation === 'wechatBindVerification' && (typeof args.verify_code !== 'string' || !/^[0-9A-Za-z-]{1,128}$/.test(args.verify_code))) throw new Error('invalid_input');
if (operation === 'channelAccountWechatBindVerification' && (typeof args.verify_code !== 'string' || !/^[0-9A-Za-z-]{1,128}$/.test(args.verify_code))) throw new Error('invalid_input');
if (operation === 'channelConversationControl' && !['resume', 'stop', 'new-session'].includes(String(args.action))) throw new Error('invalid_input');
for (const key of ['expected_revision', 'expected_policy_revision']) {
if (args[key] !== undefined && (!Number.isSafeInteger(args[key]) || Number(args[key]) < 0)) throw new Error('invalid_input');
}
if (args.result_notification !== undefined && args.result_notification !== null) {
const target = object(args.result_notification);
if (target.enabled !== true || typeof target.channel_account_id !== 'string' || !/^[a-zA-Z0-9._:#-]{1,256}$/.test(target.channel_account_id)
|| typeof target.caller_id !== 'string' || !/^[a-zA-Z0-9._:#-]{1,256}$/.test(target.caller_id)) throw new Error('invalid_input');
}
}
/** Only these product operations can cross the Main boundary; no arbitrary upstream URL or credentials. */
const operations: Record<keyof CloudAgentOperations, OperationSpec> = {
applicationCalls: { method: 'GET', path: '/applications/:application_id/calls', query: { offset: 'offset' }, output: ['items', 'next_offset'] },
resources: { method: 'GET', path: '/resources', output: ['mcps', 'skills', 'subagents'] },
createMcp: { method: 'POST', path: '/resources/mcps', body: [...mcpInput, 'operation_id'], output: mcpOutput },
updateMcp: { method: 'PUT', path: '/resources/mcps/:key', body: mcpInput, output: mcpOutput },
enableMcp: { method: 'PUT', path: '/resources/mcps/:key/enabled', body: ['enabled'], output: mcpOutput },
deleteMcp: { method: 'DELETE', path: '/resources/mcps/:key', output: ['deleted'] },
createChild: { method: 'POST', path: '/resources/subagents', body: [...child, 'operation_id'], output: ['slug', ...child] },
updateChild: { method: 'PUT', path: '/resources/subagents/:key', body: child, output: ['slug', ...child] },
confirmSkill: { method: 'POST', path: '/resources/skills/drafts/:draft_id/confirm', output: ['items'] },
deleteSkill: { method: 'DELETE', path: '/resources/skills/:key', output: ['deleted'] },
deleteKnowledge: { method: 'DELETE', path: '/agents/:slug/knowledge/:kb_id', output: ['deleted'] },
deleteKnowledgeFile: { method: 'DELETE', path: '/agents/:slug/knowledge/:kb_id/files/:file_id', output: ['deleted'] },
importKnowledgeAttachment: { method: 'POST', path: '/agents/:slug/knowledge/:kb_id/import-attachment', body: ['operation_id', 'thread_id', 'attachment_id'], output: ['file_id', 'name', 'size', 'status', 'error', 'chunk_count', 'available', 'replaces_file_id', 'processing_task'] },
archiveAgent: { method: 'PUT', path: '/agents/:slug/archive', body: ['archived'], output: draft },
archiveThread: { method: 'PUT', path: '/threads/:thread_id/archive', body: ['archived'], output: ['thread_id', 'archived'] },
version: { method: 'GET', path: '/agents/:slug/versions/:version', output: ['version', 'created_at', 'name', 'purpose', 'system_prompt', 'configuration'] },
restoreVersion: { method: 'POST', path: '/agents/:slug/versions/:version/restore', body: ['expected_revision'], output: draft },
scheduleContext: { method: 'GET', path: '/agents/:slug/schedule-context', output: ['version', 'enabled', 'configuration', 'result_destination', 'payer'] },
knowledge: { method: 'GET', path: '/agents/:slug/knowledge', output: ['databases', 'models'] },
createKnowledge: { method: 'POST', path: '/agents/:slug/knowledge', body: ['operation_id', 'name', 'embedding_model'], output: ['kb_id', 'name', 'description', 'embedding_model'] },
knowledgeFiles: { method: 'GET', path: '/agents/:slug/knowledge/:kb_id/files', query: { offset: 'offset' }, output: ['files', 'next_offset'] },
processKnowledge: { method: 'POST', path: '/agents/:slug/knowledge/:kb_id/files/:file_id/process', body: ['operation_id'], output: ['task_id'] },
catalog: { method: 'GET', path: '/catalog', output: ['models', 'resources', 'pricing'] },
received: { method: 'GET', path: '/received', output: ['agents'] },
entry: { method: 'GET', path: '/entry/:slug', output: entry },
publish: { method: 'POST', path: '/agents/:slug/publish', body: ['operation_id', 'expected_revision'], output: ['version', 'draft_revision'] },
access: { method: 'GET', path: '/agents/:slug/access', output: ['published_version', 'enabled', 'share_url', 'versions', 'grants', 'applications'] },
setEnabled: { method: 'PATCH', path: '/agents/:slug/enabled', body: ['enabled'], output: ['enabled'] },
users: { method: 'GET', cloud: 'ws', path: '/users', query: { query: 'query' }, output: ['users'] },
share: { method: 'PUT', path: '/agents/:slug/shares/:account_id', body: ['enabled'], output: ['account_id', 'enabled'] },
createApplication: { method: 'POST', path: '/agents/:slug/applications', body: ['operation_id', 'name'], output: ['id', 'name', 'enabled'] },
setApplicationEnabled: { method: 'PATCH', path: '/applications/:application_id', body: ['enabled'], output: ['enabled'] },
keys: { method: 'GET', path: '/applications/:application_id/keys', output: ['keys'] },
createKey: { method: 'POST', path: '/applications/:application_id/keys', body: ['operation_id'], output: ['id', 'prefix', 'secret'] },
revokeKey: { method: 'DELETE', path: '/applications/:application_id/keys/:key_id', output: ['revoked'] },
costs: { method: 'GET', cloud: 'ws', path: '/costs', query: { slug: 'agent_slug', offset: 'offset', started_at: 'started_at', ended_at: 'ended_at', source: 'source', application_id: 'application_id' }, output: ['unit', 'items', 'next_offset', 'summary'] },
budget: { method: 'GET', cloud: 'ws', path: '/agents/:slug/budget', output: budget },
saveBudget: { method: 'PUT', cloud: 'ws', path: '/agents/:slug/budget', body: ['request_limit_points', 'daily_limit_points'], output: budget },
threads: { method: 'GET', path: '/threads', query: { slug: 'slug', offset: 'offset', archived: 'archived' }, output: ['threads', 'next_offset'] },
createThread: { method: 'POST', path: '/agents/:slug/threads', body: ['thread_id', 'preview', 'expected_revision'], output: ['thread_id', 'client_thread_id'] },
history: { method: 'GET', path: '/threads/:thread_id', query: { offset: 'offset' }, output: ['thread_id', 'messages', 'run', 'queued_requests', 'schedule_proposals', 'next_offset'] },
viewed: { method: 'POST', path: '/threads/:thread_id/viewed', body: ['run_id'], output: ['viewed'] },
submit: { method: 'POST', path: '/agents/:slug/requests', body: prompt, output: request },
preview: { method: 'POST', path: '/agents/:slug/preview', body: prompt, output: request },
request: { method: 'GET', path: '/requests/:request_id', output: request },
cancelRequest: { method: 'POST', path: '/requests/:request_id/cancel', output: ['status'] },
run: { method: 'GET', path: '/runs/:run_id', output: ['agent_run_id', 'request_id', 'thread_id', 'agent_slug', 'status', 'output', 'version', 'error', 'interrupt'] },
cancelRun: { method: 'POST', path: '/runs/:run_id/cancel', output: ['status'] },
resume: { method: 'POST', path: '/runs/:run_id/resume', body: ['operation_id', 'decision'], output: ['run_id', 'status'] },
schedules: { method: 'GET', path: '/agents/:slug/schedules', output: ['jobs'], project: projectSchedules },
createSchedule: { method: 'POST', path: '/agents/:slug/schedules', body: ['operation_id', ...schedule], output: job, project: projectSchedule },
updateSchedule: { method: 'PUT', path: '/agents/:slug/schedules/:job_id', body: schedule, output: job, project: projectSchedule },
deleteSchedule: { method: 'DELETE', path: '/agents/:slug/schedules/:job_id', output: ['deleted'] },
runSchedule: { method: 'POST', path: '/agents/:slug/schedules/:job_id/run-now', body: ['operation_id'], output: ['thread_id', 'status'] },
attachments: { method: 'GET', path: '/threads/:thread_id/attachments', output: ['attachments'] },
parseAttachment: { method: 'POST', path: '/attachments/tmp/parse', body: ['object_name', 'parse_method'], output: ['parsed_object_name'] },
confirmAttachment: { method: 'POST', path: '/threads/:thread_id/attachments/confirm', body: ['attachments'], output: ['attachments'] },
deleteAttachment: { method: 'DELETE', path: '/threads/:thread_id/attachments/:file_id', output: ['message'] },
files: { method: 'GET', path: '/threads/:thread_id/files', query: { path: 'path' }, output: ['files'] },
channelConnections: { method: 'GET', path: '/channel-connections', output: ['items'], project: value => ({ items: list(object(value).items, projectChannelView) }) },
channelAccounts: { method: 'GET', path: '/channel-accounts', output: ['items'], project: value => ({ items: list(object(value).items, projectChannelView) }) },
createChannelAccount: { method: 'POST', path: '/channel-accounts', body: ['operation_id', 'display_name', 'account_key'], output: ['channel'], project: projectMutation },
routeChannelAccount: { method: 'POST', path: '/channel-accounts/:channel_account_id/route', body: ['operation_id', 'target_agent_slug', 'expected_revision', 'enabled'], output: ['channel', 'target_agent'], project: projectMutation },
enableChannelAccount: { method: 'POST', path: '/channel-accounts/:channel_account_id/enable', body: ['operation_id', 'expected_revision'], output: ['channel'], project: projectMutation },
pauseChannelAccount: { method: 'POST', path: '/channel-accounts/:channel_account_id/pause', body: ['operation_id', 'expected_revision'], output: ['channel'], project: projectMutation },
disconnectChannelAccount: { method: 'POST', path: '/channel-accounts/:channel_account_id/disconnect', body: ['operation_id', 'expected_revision'], output: ['channel', 'wechat', 'disconnected'], project: projectMutation },
channelAccountWechatBindStart: { method: 'POST', path: '/channel-accounts/:channel_account_id/wechat-bind/start', body: ['operation_id', 'force'], output: ['session_key', 'status', 'message', 'qrcode_url'], project: projectWechat },
channelAccountWechatBindStatus: { method: 'GET', path: '/channel-accounts/:channel_account_id/wechat-bind/status', query: { session_key: 'session_key' }, output: ['session_key', 'status', 'message', 'qrcode_url'], project: projectWechat },
channelAccountWechatBindVerification: { method: 'POST', path: '/channel-accounts/:channel_account_id/wechat-bind/verification', body: ['operation_id', 'session_key', 'verify_code'], output: ['session_key', 'status', 'message', 'qrcode_url'], project: projectWechat },
channelAccountWechatBindAccount: { method: 'GET', path: '/channel-accounts/:channel_account_id/wechat-bind/account', output: ['status', 'message'] },
channelAccountWechatUnbind: { method: 'POST', path: '/channel-accounts/:channel_account_id/wechat-bind/unbind', body: ['operation_id'], output: ['status', 'message'] },
channelAccountOperation: { method: 'GET', path: '/channel-operations/:operation_id', output: channelOperation, project: projectOperation },
channelBindings: { method: 'GET', path: '/agents/:slug/channel-bindings', output: ['items', 'available_accounts'], project: projectBindings },
createChannelBinding: { method: 'POST', path: '/agents/:slug/channel-bindings', body: ['operation_id', 'display_name', 'channel_account_id'], output: ['channel', 'worker'], project: projectMutation },
switchChannelBindingAgent: { method: 'POST', path: '/agents/:slug/channel-bindings/:channel_account_id/route', body: ['operation_id', 'target_agent_slug', 'expected_revision', 'enabled'], output: ['channel', 'target_agent'], project: projectMutation },
enableChannelBinding: { method: 'POST', path: '/agents/:slug/channel-bindings/:channel_account_id/enable', body: ['operation_id', 'expected_revision'], output: ['channel'], project: projectMutation },
pauseChannelBinding: { method: 'POST', path: '/agents/:slug/channel-bindings/:channel_account_id/pause', body: ['operation_id', 'expected_revision'], output: ['channel'], project: projectMutation },
disconnectChannelBinding: { method: 'POST', path: '/agents/:slug/channel-bindings/:channel_account_id/disconnect', body: ['operation_id', 'expected_revision'], output: ['channel', 'wechat', 'disconnected'], project: projectMutation },
wechatBindStart: { method: 'POST', path: '/agents/:slug/channel-bindings/:channel_account_id/wechat-bind/start', body: ['operation_id', 'force'], output: ['session_key', 'status', 'message', 'qrcode_url'], project: projectWechat },
wechatBindStatus: { method: 'GET', path: '/agents/:slug/channel-bindings/:channel_account_id/wechat-bind/status', query: { session_key: 'session_key' }, output: ['session_key', 'status', 'message', 'qrcode_url'], project: projectWechat },
wechatBindVerification: { method: 'POST', path: '/agents/:slug/channel-bindings/:channel_account_id/wechat-bind/verification', body: ['operation_id', 'session_key', 'verify_code'], output: ['session_key', 'status', 'message', 'qrcode_url'], project: projectWechat },
wechatBindAccount: { method: 'GET', path: '/agents/:slug/channel-bindings/:channel_account_id/wechat-bind/account', output: ['status', 'message'] },
wechatUnbind: { method: 'POST', path: '/agents/:slug/channel-bindings/:channel_account_id/wechat-bind/unbind', body: ['operation_id'], output: ['status', 'message'] },
channelSelfCallers: { method: 'GET', path: '/agents/:slug/channel-callers', fixedQuery: { access_mode: 'self_only' }, output: ['items'], project: projectCallers },
channelActivity: { method: 'GET', path: '/agents/:slug/channel-bindings/:channel_account_id/activity', query: { offset: 'offset', limit: 'limit' }, output: ['items', 'next_offset'], project: projectActivity },
channelConversations: { method: 'GET', path: '/channel-conversations', query: { slug: 'agent_slug', offset: 'offset' }, output: ['items', 'next_offset'], project: value => ({ items: list(object(value).items, projectSession), next_offset: object(value).next_offset ?? null }) },
channelConversation: { method: 'GET', path: '/channel-conversations/:session_id', query: { offset: 'offset' }, output: ['session_id', 'caller_id', 'agent_slug', 'messages', 'run', 'queued_requests', 'next_offset'], project: projectConversation },
channelFiles: { method: 'GET', path: '/channel-conversations/:session_id/files', query: { path: 'path' }, output: ['session_id', 'thread_id', 'files'], project: projectFiles },
channelConversationControl: { method: 'POST', path: '/channel-conversations/:session_id/controls', body: ['operation_id', 'action', 'run_id', 'decision'], output: ['operation_id', 'status', 'session_id', 'action'], project: projectControl },
channelDelivery: { method: 'GET', path: '/agents/:slug/channel-bindings/:channel_account_id/deliveries/:logical_message_id', output: ['logical_message_id', 'message_id', 'status', 'parts'], project: projectDelivery },
retryChannelDeliveryPart: { method: 'POST', path: '/agents/:slug/channel-bindings/:channel_account_id/deliveries/:logical_message_id/parts/:part_id/retry', output: ['logical_message_id', 'message_id', 'status', 'parts'], project: projectDelivery },
channelOperation: { method: 'GET', path: '/channel-operations/:operation_id', query: { slug: 'agent_slug' }, output: channelOperation, project: projectOperation },
};
export function operationPlan(value: unknown) {
if (!value || typeof value !== 'object') throw new Error('invalid_operation');
const { operation, input } = value as { operation?: unknown; input?: unknown };
if (typeof operation !== 'string' || !Object.hasOwn(operations, operation)
|| !input || typeof input !== 'object' || Array.isArray(input)) throw new Error('invalid_operation');
const spec = operations[operation as keyof CloudAgentOperations];
const args = input as Record<string, unknown>;
validateChannelInput(operation, args);
const routeKeys = [...spec.path.matchAll(/:([a-z_]+)/g)].map(match => match[1]);
const path = spec.path.replace(/:([a-z_]+)/g, (_, key: string) => {
const id = args[key];
if (typeof id !== 'string' || !/^[a-zA-Z0-9._:#-]{1,256}$/.test(id)
|| (key === 'slug' && !/^ml-[a-f0-9]{32}$/.test(id))) throw new Error('invalid_id');
return encodeURIComponent(id);
});
const query = new URLSearchParams();
for (const [name, value] of Object.entries(spec.fixedQuery ?? {})) query.set(name, value);
for (const [key, name] of Object.entries(spec.query ?? {})) {
const v = args[key];
if (v === undefined) continue;
if (key === 'archived' ? typeof v !== 'boolean' : key === 'offset' || key === 'limit' ? !Number.isSafeInteger(v) || Number(v) < 0 || (key === 'limit' && Number(v) > 100) : typeof v !== 'string' || v.length > (key === 'path' ? 2048 : key === 'session_key' ? 512 : 200)
|| (key === 'slug' || key === 'agent_slug') && !/^ml-[a-f0-9]{32}$/.test(String(v))) {
throw new Error('invalid_query');
}
query.set(name, String(v));
}
return {
operation,
input: Object.fromEntries([...new Set([...routeKeys, ...(spec.body ?? []), ...Object.keys(spec.query ?? {})])]
.filter(key => args[key] !== undefined).map(key => [key, args[key]])),
...spec, path: (spec.cloud === 'ws' ? '/api/cloud-agents' : '/api/makelore') + path + (query.size ? '?' + query : ''),
body: spec.body ? Object.fromEntries(spec.body.filter(key => args[key] !== undefined).map(key => [key, args[key]])) : undefined,
project: (result: unknown) => {
if (spec.project) return spec.project(result);
if (!result || typeof result !== 'object' || Array.isArray(result)) throw new Error('invalid_response');
const record = result as Record<string, unknown>;
return Object.fromEntries(spec.output.filter(key => record[key] !== undefined).map(key => [key, record[key]]));
},
};
}