import { EMPTY_CLOUD_CONFIGURATION, type CloudAgentConfiguration, type CloudAgentDraft, type CloudAgentPage, type CreateCloudAgent, type SaveCloudAgentDraft, type CloudUpload } from '../../shared/cloud-agents'; import { operationPlan } from './cloud-agent-operations'; import { CloudAgentJournal } from './cloud-agent-journal'; import type { CloudRecent } from '../../shared/cloud-agents'; import { WORKS_SQUARE_CONFIG } from '../api/works-config'; import { proxyAwareFetch, runWithDeadline } from '../utils/proxy-fetch'; import { getValidWorksSquareAccessToken, getWorksSquareAccountBinding, isCurrentWorksSquareAccountBinding, subscribeWorksSquareSession, type WorksSquareAccountBinding, } from './works-square-session'; const MESSAGES: Record = { session_expired: '登录已过期,请重新登录', account_changed: '账号已切换,请重新打开智能体', account_locked: '账号已锁定,请稍后重试', module_unavailable: 'AI 智能体模块暂不可用', agent_not_found: '智能体不存在', draft_revision_conflict: '草稿已在其他设备更新,你的输入已保留', operation_conflict: '创建操作的内容发生冲突,请刷新列表确认', cloud_service_unavailable: '智能体服务暂时不可用,请重试', invalid_input: '请检查名称、用途和配置内容', model_required: '请先选择模型并保存', model_unavailable: '所选模型当前不可用,请刷新模型目录', resource_unavailable: '所选资源已不可用,请刷新配置', publish_required: '请先发布智能体', access_revoked: '该智能体的使用授权已撤销', application_disabled: '应用已停用', key_revoked: '此凭据已撤销,请新建凭据', insufficient_balance: '创建者的词元点数不足', request_conflict: '该请求已用于其他输入,请开始新的请求', thread_archived: '请先恢复已归档的对话再提交任务', thread_active: '请先处理待回答或审批的任务,并停止运行及排队任务,再归档对话', knowledge_processing: '知识库有文档正在处理,请完成后再维护', agent_archived: '请先从归档恢复智能体,再启用', attachment_too_large: '附件最大支持 5 MB', download_failed: '文件保存失败,请重试', channel_not_found: '微信渠道不存在或已解绑', channel_operation_conflict: '微信渠道正在处理其他操作,请稍后查看状态', channel_request_invalid: '微信渠道请求无效,请刷新后重试', channel_rate_limited: '微信渠道操作过于频繁,请稍后重试', channel_conversation_not_found: '微信会话不存在或已关闭', channel_session_control_failed: '微信会话操作失败,请刷新后重试', channel_delivery_not_found: '渠道交付记录不存在', channel_delivery_retry_failed: '文件补发失败,请刷新交付记录后重试', pairing_expired: '配对码已过期,请重新生成', verification_required: '请先完成微信验证', artifact_unavailable: '文件已不可用,请刷新文件列表', operation_unavailable: '操作状态暂时无法确认,请稍后查询', channel_scope_changed: '微信渠道版本已变化,请刷新后重试', binding_revoked: '微信渠道已暂停、切换或撤销', binding_paused: '微信渠道已暂停,请先启用后重试', caller_not_found: '渠道调用者不存在', delivery_not_found: '交付记录不存在', delivery_part_not_found: '交付文件分片不存在', artifact_snapshot_missing: '文件快照尚未准备好,请稍后重试', channel_control_failed: '微信会话控制失败,请刷新后重试', run_active: '当前会话仍在执行,请稍后重试', run_not_active: '当前会话没有可操作的运行', channel_session_unavailable: '新的微信会话暂时无法建立', pairing_invalid: '配对请求无效,请刷新后重试', invitation_invalid: '配对码无效或已撤销', invitation_consumed: '配对码已经使用', invites_disabled: '当前渠道仅允许创建者本人配对', agentbus_unavailable: '渠道服务暂时不可用,请稍后重试', }; export class CloudAgentsError extends Error { constructor(readonly status: number, readonly code: string) { super(MESSAGES[code] ?? MESSAGES.cloud_service_unavailable); } } function record(value: unknown): Record { return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; } function textField(value: unknown, max: number, nonempty = true): string { if (typeof value !== 'string' || value.length > max || (nonempty && !value.trim())) { throw new CloudAgentsError(422, 'invalid_input'); } return value; } function revision(value: unknown): number { if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) { throw new CloudAgentsError(422, 'invalid_input'); } return value; } function slug(value: string): string { if (!/^ml-[a-f0-9]{32}$/.test(value)) throw new CloudAgentsError(422, 'invalid_input'); return value; } function projectDraft(value: unknown): CloudAgentDraft { const item = record(value); try { return { slug: slug(textField(item.slug, 80)), name: textField(item.name, 100), purpose: textField(item.purpose, 2000), system_prompt: textField(item.system_prompt, 32000, false), draft_revision: revision(item.draft_revision), updated_at: textField(item.updated_at, 40), configuration: configuration(item.configuration), published_version: item.published_version == null ? null : revision(item.published_version), enabled: item.enabled !== false, archived: item.archived === true, }; } catch { throw new CloudAgentsError(502, 'cloud_service_unavailable'); } } function configuration(value: unknown): CloudAgentConfiguration { const input = record(value); const result = { ...EMPTY_CLOUD_CONFIGURATION, ...Object.fromEntries( Object.keys(EMPTY_CLOUD_CONFIGURATION).filter(key => input[key] !== undefined).map(key => [key, input[key]]), ) }; textField(result.model, 200, false); for (const key of ['tools', 'knowledges', 'mcps', 'skills', 'preload_skills', 'subagents'] as const) { if (!Array.isArray(result[key]) || result[key].length > 100 || result[key].some(v => typeof v !== 'string' || v.length > 200)) { throw new CloudAgentsError(422, 'invalid_input'); } } if (!['default', 'always_trust'].includes(result.tool_approval_mode) || !Number.isInteger(result.max_execution_steps) || result.max_execution_steps < 1 || result.max_execution_steps > 300 || !Number.isInteger(result.max_output_tokens) || result.max_output_tokens < 1 || result.max_output_tokens > 32768 || !Number.isInteger(result.max_run_seconds) || result.max_run_seconds < 10 || result.max_run_seconds > 3600) { throw new CloudAgentsError(422, 'invalid_input'); } return result; } type Session = { binding: WorksSquareAccountBinding; accessToken: string; apiBaseUrl: string; expiresAt: number; }; /** Main owns cloud credentials and transport; Renderer receives product data only. */ export class CloudAgentsModule { private cached: Session | null = null; private readonly unsubscribe: () => void; private readonly streams = new Map(); private activityTimer: ReturnType; private activityPolling = false; private activitySeen: Map | null = null; constructor(private readonly fetchImpl: (input: string, init?: RequestInit) => Promise = proxyAwareFetch, private readonly journal = new CloudAgentJournal()) { this.unsubscribe = subscribeWorksSquareSession(() => { if (this.cached && !isCurrentWorksSquareAccountBinding(this.cached.binding)) { this.cached = null; this.activitySeen = null; } for (const [controller, binding] of this.streams) { if (!isCurrentWorksSquareAccountBinding(binding)) controller.abort(); } }); this.activityTimer = setInterval(() => { void this.pollActivity(); }, 30000); this.activityTimer.unref?.(); } dispose(): void { this.cached = null; clearInterval(this.activityTimer); for (const controller of this.streams.keys()) controller.abort(); this.unsubscribe(); } async list(cursor: string | null = null, archived = false): Promise { if (cursor !== null && !/^\d{1,16}$/.test(cursor)) throw new CloudAgentsError(422, 'invalid_input'); const params = new URLSearchParams(); if (cursor) params.set('cursor', cursor); if (archived) params.set('archived', 'true'); const body = record(await this.request('/api/makelore/agents' + (params.size ? '?' + params : ''), 'GET')); if (!Array.isArray(body.agents) || body.agents.length > 100 || (body.next_cursor !== null && (typeof body.next_cursor !== 'string' || !/^\d{1,16}$/.test(body.next_cursor)))) { throw new CloudAgentsError(502, 'cloud_service_unavailable'); } return { agents: body.agents.map(projectDraft), next_cursor: body.next_cursor as string | null }; } async get(agentSlug: string): Promise { return projectDraft(await this.request(`/api/makelore/agents/${slug(agentSlug)}`, 'GET')); } async create(value: unknown): Promise { const input = record(value); const operationId = textField(input.operation_id, 36); if (!/^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(operationId)) { throw new CloudAgentsError(422, 'invalid_input'); } const body: CreateCloudAgent = { operation_id: operationId, name: textField(input.name, 100).trim(), purpose: textField(input.purpose, 2000).trim(), }; return this.withRecovery('createAgent', { ...body }, () => this.request('/api/makelore/agents', 'POST', body).then(projectDraft)); } private async withRecovery(operation: string, input: Record, execute: () => Promise): Promise { const binding = getWorksSquareAccountBinding(); if (!binding) throw new CloudAgentsError(401, 'session_expired'); const operationKey = operation === 'retryChannelDeliveryPart' ? [input.channel_account_id, input.logical_message_id, input.part_id] .map(value => encodeURIComponent(typeof value === 'string' ? value : '')).join(':') : input.request_id ?? input.operation_id; const id = [operation, input.slug ?? input.application_id ?? input.run_id ?? '', operationKey].join(':'); await this.journal.put(binding.accountKey, { id, operation, input, created_at: new Date().toISOString() }, () => this.requireCurrent(binding)); this.requireCurrent(binding); const result = await execute(); this.requireCurrent(binding); await this.journal.remove(binding.accountKey, id, () => this.requireCurrent(binding)); return result; } async recovery() { const binding = getWorksSquareAccountBinding(); if (!binding) throw new CloudAgentsError(401, 'session_expired'); const state = await this.journal.read(binding.accountKey); this.requireCurrent(binding); return state; } async rememberRecent(value: unknown) { const binding = getWorksSquareAccountBinding(); if (!binding) throw new CloudAgentsError(401, 'session_expired'); const input = record(value); const recent: CloudRecent | null = value === null ? null : { slug: slug(textField(input.slug, 64)), mode: input.mode === 'preview' ? 'preview' : 'published', ...(input.thread_id === undefined ? {} : { thread_id: textField(input.thread_id, 128) }), ...(input.draft_revision === undefined ? {} : { draft_revision: revision(input.draft_revision) }), }; await this.journal.remember(binding.accountKey, recent, () => this.requireCurrent(binding)); return { saved: true }; } async resolvePending(value: unknown) { const input = record(value); const id = textField(input.id, 300); const binding = getWorksSquareAccountBinding(); if (!binding) throw new CloudAgentsError(401, 'session_expired'); const state = await this.recovery(); const pending = state.pending.find(item => item.id === id); if (!pending) throw new CloudAgentsError(404, 'invalid_input'); if (input.discard === true) { await this.journal.remove(binding.accountKey, id, () => this.requireCurrent(binding)); return { discarded: true }; } if (pending.operation === 'wechatBindVerification') { // The verification code is intentionally never persisted. First recover the // durable provider receipt: the code may already have been consumed even if // the response was lost. Only an absent receipt requires new input. const pendingInput = record(pending.input); const pendingSlug = typeof pendingInput.slug === 'string' ? pendingInput.slug : undefined; const operationId = typeof pendingInput.operation_id === 'string' ? pendingInput.operation_id : undefined; if (pendingSlug && operationId) { try { const receipt = await this.execute({ operation: 'channelOperation', input: { slug: pendingSlug, operation_id: operationId } }); const status = typeof record(receipt).status === 'string' ? String(record(receipt).status).toLowerCase() : ''; const terminal = ['completed', 'complete', 'succeeded', 'success', 'failed', 'rejected', 'cancelled', 'canceled', 'expired', 'error'].includes(status); if (terminal) await this.journal.remove(binding.accountKey, id, () => this.requireCurrent(binding)); return { operation: pending.operation, input: pending.input, result: receipt, ...(terminal ? {} : { pending: true }) }; } catch (error) { if (!(error instanceof CloudAgentsError) || error.status !== 404) throw error; } } // No receipt exists (or this is an old record without enough metadata), so // the Renderer must collect a fresh verification code. return { operation: pending.operation, input: pending.input, requires_input: true }; } const result = pending.operation === 'createAgent' ? await this.create(pending.input) : await this.execute({ operation: pending.operation, input: pending.input }); return { operation: pending.operation, input: pending.input, result }; } async save(agentSlug: string, value: unknown): Promise { const input = record(value); const body: SaveCloudAgentDraft = { expected_revision: revision(input.expected_revision), name: textField(input.name, 100).trim(), purpose: textField(input.purpose, 2000).trim(), system_prompt: textField(input.system_prompt, 32000, false), ...(input.configuration === undefined ? {} : { configuration: configuration(input.configuration) }), }; return projectDraft(await this.request(`/api/makelore/agents/${slug(agentSlug)}/draft`, 'PATCH', body)); } private async pollActivity(): Promise { if (!this.cached || this.activityPolling) return; this.activityPolling = true; try { const binding = this.cached.binding; const result = record(await this.request('/api/makelore/threads', 'GET')); const rows = Array.isArray(result.threads) ? result.threads.map(record) : []; const next = new Map(rows.filter(row => typeof row.run_id === 'string').map(row => [String(row.run_id), String(row.status)])); if (this.activitySeen) { const changed = rows.filter(row => row.unread === true && typeof row.run_id === 'string' && ['completed', 'failed', 'interrupted'].includes(String(row.status)) && this.activitySeen?.get(row.run_id) !== row.status); if (changed.length) { const { Notification, BrowserWindow } = await import('electron'); this.requireCurrent(binding); if (Notification.isSupported()) { const notice = new Notification({ title: '智能体有新的任务结果', body: changed.some(row => row.status === 'interrupted') ? '有任务等待你的确认,打开活动查看。' : '打开活动查看完成结果或失败原因。' }); notice.on('click', () => { if (!isCurrentWorksSquareAccountBinding(binding)) return; const window = BrowserWindow.getAllWindows().find(w => !w.isDestroyed()); window?.show(); window?.focus(); window?.webContents.send('navigate', '/cloud-agents?view=activity'); }); notice.show(); } } } this.activitySeen = next; } catch(error) { if (error instanceof CloudAgentsError && [401, 403, 423].includes(error.status)) this.cached = null; // The durable activity list remains the source of truth when notifications or transport are unavailable. } finally { this.activityPolling = false; } } async execute(value: unknown): Promise { let plan; try { plan = operationPlan(value); } catch { throw new CloudAgentsError(422, 'invalid_input'); } const invoke = async () => plan.project(await this.request(plan.path, plan.method, plan.body, plan.cloud === 'ws')); const recoverable = ['submit', 'preview', 'resume', 'publish', 'createApplication', 'createKey', 'createKnowledge', 'processKnowledge', 'createSchedule', 'runSchedule', 'importKnowledgeAttachment', 'createChild', 'createChannelBinding', 'switchChannelBindingAgent', 'enableChannelBinding', 'pauseChannelBinding', 'disconnectChannelBinding', 'channelPolicy', 'createChannelPairing', 'wechatBindStart', 'wechatBindVerification', 'wechatUnbind', 'revokeChannelCaller', 'channelConversationControl', 'retryChannelDeliveryPart']; let result: unknown; if (recoverable.includes(plan.operation)) { const journalInput = plan.operation === 'wechatBindVerification' ? Object.fromEntries(Object.entries(plan.input).filter(([key]) => key !== 'verify_code')) : plan.input; result = await this.withRecovery(plan.operation, journalInput, invoke); } else result = await invoke(); try { const projected = plan.project(result); if (record(value).operation === 'access') { // The address is public; both exchanged tokens stay in Main. const binding = getWorksSquareAccountBinding(); if (!binding) throw new CloudAgentsError(401, 'session_expired'); const session = await runWithDeadline(signal => this.session(binding, signal), 30000); this.requireCurrent(binding); return { ...projected, api_url: session.apiBaseUrl + '/api/makelore/api/requests' }; } return projected; } catch (error) { if (error instanceof CloudAgentsError) throw error; throw new CloudAgentsError(502, 'cloud_service_unavailable'); } } async upload(): Promise { const body = await this.pickUpload(); if (!body) return null; const result = record(await this.request('/api/makelore/attachments/tmp', 'POST', body)); return { object_name: textField(result.object_name, 2048), file_name: textField(result.file_name, 255), file_type: typeof result.file_type === 'string' ? result.file_type : 'application/octet-stream', parse_supported: result.parse_supported === true, parse_methods: Array.isArray(result.parse_methods) ? result.parse_methods.filter((v): v is string => typeof v === 'string') : [], }; } async uploadKnowledge(value: unknown): Promise { const input = record(value); const slug = textField(input.slug, 128), kbId = textField(input.kb_id, 128); const operationId = textField(input.operation_id, 64); const replacement = input.replaces_file_id === undefined ? '' : textField(input.replaces_file_id, 64); if (replacement && !/^[a-zA-Z0-9_-]+$/.test(replacement)) throw new CloudAgentsError(422, 'invalid_input'); if (!/^ml-[a-f0-9]{32}$/.test(slug) || !/^kb_[a-z0-9]+$/.test(kbId) || !/^[a-f0-9-]{36}$/.test(operationId)) throw new CloudAgentsError(422, 'invalid_input'); const body = await this.pickUpload(); if (!body) return null; const result = record(await this.request(`/api/makelore/agents/${slug}/knowledge/${kbId}/files?operation_id=${operationId}` + (replacement ? '&replaces_file_id=' + replacement : ''), 'POST', body)); return Object.fromEntries(['file_id', 'name', 'size', 'status', 'error', 'chunk_count'].map(key => [key, result[key]])); } async uploadSkill(): Promise { const body = await this.pickUpload(); if (!body) return null; const result = record(await this.request('/api/makelore/resources/skills/upload', 'POST', body)); return { draft_id: result.draft_id, items: result.items }; } private async pickUpload(): Promise { const binding = getWorksSquareAccountBinding(); if (!binding) throw new CloudAgentsError(401, 'session_expired'); const { dialog } = await import('electron'); const { readFile, stat } = await import('node:fs/promises'); const { basename } = await import('node:path'); const picked = await dialog.showOpenDialog({ title: '添加智能体附件(最大 5 MB)', properties: ['openFile'] }); this.requireCurrent(binding); if (picked.canceled || !picked.filePaths[0]) return null; const path = picked.filePaths[0]; if ((await stat(path)).size > 5 * 1024 * 1024) throw new CloudAgentsError(422, 'attachment_too_large'); const bytes = await readFile(path); this.requireCurrent(binding); if (bytes.length > 5 * 1024 * 1024) throw new CloudAgentsError(422, 'attachment_too_large'); const body = new FormData(); body.append('file', new Blob([new Uint8Array(bytes)]), basename(path)); return body; } async download(value: unknown): Promise<{ saved: boolean }> { const input = record(value); const threadId = textField(input.thread_id, 128); const path = textField(input.path, 2048); if (!/^[a-zA-Z0-9_-]+$/.test(threadId)) throw new CloudAgentsError(422, 'invalid_input'); const binding = getWorksSquareAccountBinding(); if (!binding) throw new CloudAgentsError(401, 'session_expired'); const { dialog } = await import('electron'); const fs = await import('node:fs/promises'); const { basename, dirname, join } = await import('node:path'); const picked = await dialog.showSaveDialog({ title: '保存智能体文件', defaultPath: basename(path), properties: ['createDirectory', 'showOverwriteConfirmation'] }); this.requireCurrent(binding); if (picked.canceled || !picked.filePath) return { saved: false }; const temporary = join(dirname(picked.filePath), '.makelore-download-' + crypto.randomUUID()); try { await runWithDeadline(async signal => { const session = await this.session(binding, signal); this.requireCurrent(binding); const url = session.apiBaseUrl + '/api/makelore/threads/' + encodeURIComponent(threadId) + '/artifacts/' + path.replace(/^\/+/, '').split('/').map(encodeURIComponent).join('/') + '?download=true'; const response = await this.fetchImpl(url, { redirect: 'error', signal, headers: { Authorization: 'Bearer ' + session.accessToken } }); if (!response.ok || !response.body) throw new CloudAgentsError(response.status || 502, 'download_failed'); const output = await fs.open(temporary, 'wx'); const reader = response.body.getReader(); let size = 0; try { while (true) { const { value: chunk, done } = await reader.read(); this.requireCurrent(binding); if (done) break; size += chunk.byteLength; if (size > 1024 * 1024 * 1024) throw new CloudAgentsError(413, 'download_failed'); let offset = 0; while (offset < chunk.byteLength) offset += (await output.write(chunk, offset, chunk.byteLength - offset)).bytesWritten; } } finally { await reader.cancel().catch(() => undefined); await output.close(); } }, 300000); this.requireCurrent(binding); await fs.rename(temporary, picked.filePath); return { saved: true }; } catch(error) { if (error instanceof CloudAgentsError) throw error; throw new CloudAgentsError(502, 'download_failed'); } finally { await fs.unlink(temporary).catch(() => undefined); } } /** Save a personal-channel artifact through the session-scoped bridge. */ async downloadChannelArtifact(value: unknown): Promise<{ saved: boolean }> { const input = record(value); const sessionId = textField(input.session_id, 256); const path = textField(input.path, 2048); if (!/^[a-zA-Z0-9._:#-]{1,256}$/.test(sessionId) || path.includes('\\') || path.includes('\0') || /:\/\//.test(path) || path.replace(/^\/+/, '').split('/').some(part => !part || part === '..' || part === '.')) { throw new CloudAgentsError(422, 'invalid_input'); } const binding = getWorksSquareAccountBinding(); if (!binding) throw new CloudAgentsError(401, 'session_expired'); const { dialog } = await import('electron'); const fs = await import('node:fs/promises'); const { basename, dirname, join } = await import('node:path'); const picked = await dialog.showSaveDialog({ title: '保存微信渠道文件', defaultPath: basename(path), properties: ['createDirectory', 'showOverwriteConfirmation'] }); this.requireCurrent(binding); if (picked.canceled || !picked.filePath) return { saved: false }; const temporary = join(dirname(picked.filePath), '.makelore-channel-download-' + crypto.randomUUID()); try { await runWithDeadline(async signal => { const session = await this.session(binding, signal); this.requireCurrent(binding); const encodedPath = path.replace(/^\/+/, '').split('/').map(encodeURIComponent).join('/'); const url = session.apiBaseUrl + '/api/makelore/channel-conversations/' + encodeURIComponent(sessionId) + '/artifacts/' + encodedPath + '?download=true'; const response = await this.fetchImpl(url, { redirect: 'error', signal, headers: { Authorization: 'Bearer ' + session.accessToken } }); if (!response.ok || !response.body) throw new CloudAgentsError(response.status || 502, 'download_failed'); const output = await fs.open(temporary, 'wx'); const reader = response.body.getReader(); let size = 0; try { while (true) { const { value: chunk, done } = await reader.read(); this.requireCurrent(binding); if (done) break; size += chunk.byteLength; if (size > 1024 * 1024 * 1024) throw new CloudAgentsError(413, 'download_failed'); let offset = 0; while (offset < chunk.byteLength) offset += (await output.write(chunk, offset, chunk.byteLength - offset)).bytesWritten; } } finally { await reader.cancel().catch(() => undefined); await output.close(); } }, 300000); this.requireCurrent(binding); await fs.rename(temporary, picked.filePath); return { saved: true }; } catch(error) { if (error instanceof CloudAgentsError) throw error; throw new CloudAgentsError(502, 'download_failed'); } finally { await fs.unlink(temporary).catch(() => undefined); } } async *events(runId: string, after: string, signal: AbortSignal): AsyncGenerator { if (!/^[a-zA-Z0-9_-]{1,64}$/.test(runId) || !/^\d+-\d+$/.test(after)) { throw new CloudAgentsError(422, 'invalid_input'); } const binding = getWorksSquareAccountBinding(); if (!binding) throw new CloudAgentsError(401, 'session_expired'); const controller = new AbortController(); const abort = () => controller.abort(); signal.addEventListener('abort', abort, { once: true }); if (signal.aborted) abort(); this.streams.set(controller, binding); let cancelReader: (() => Promise) | undefined; try { const session = await runWithDeadline(s => this.session(binding, s), 30000, controller.signal); this.requireCurrent(binding); const response = await this.fetchImpl(session.apiBaseUrl + '/api/makelore/runs/' + runId + '/events?after_seq=' + after, { method: 'GET', redirect: 'error', signal: controller.signal, headers: { Authorization: 'Bearer ' + session.accessToken, Accept: 'text/event-stream' }, }); this.requireCurrent(binding); if (!response.ok || !response.body) throw new CloudAgentsError(response.status || 502, 'cloud_service_unavailable'); const reader = response.body.getReader(); cancelReader = () => reader.cancel(); while (true) { const chunk = await reader.read(); this.requireCurrent(binding); if (chunk.done) break; yield chunk.value; } } finally { await cancelReader?.().catch(() => undefined); controller.abort(); signal.removeEventListener('abort', abort); this.streams.delete(controller); } } private async request(path: string, method: string, body?: unknown, worksSquare = false): Promise { const binding = getWorksSquareAccountBinding(); if (!binding) throw new CloudAgentsError(401, 'session_expired'); try { return await runWithDeadline(async (signal) => { const session = worksSquare ? { apiBaseUrl: WORKS_SQUARE_CONFIG.apiBaseUrl, accessToken: await getValidWorksSquareAccessToken(), } : await this.session(binding, signal); this.requireCurrent(binding); if (!session.accessToken) throw new CloudAgentsError(401, 'session_expired'); const result = await this.fetchJson(session.apiBaseUrl + path, { method, signal, redirect: 'error', headers: { Authorization: `Bearer ${session.accessToken}`, ...(body instanceof FormData ? {} : { 'Content-Type': 'application/json' }) }, ...(body === undefined ? {} : { body: body instanceof FormData ? body : JSON.stringify(body) }), }); this.requireCurrent(binding); return result; }, 30000); } catch (error) { this.requireCurrent(binding); if (error instanceof CloudAgentsError) { if (error.status === 401) this.cached = null; throw error; } throw new CloudAgentsError(502, 'cloud_service_unavailable'); } } private requireCurrent(binding: WorksSquareAccountBinding): void { if (!isCurrentWorksSquareAccountBinding(binding)) throw new CloudAgentsError(409, 'account_changed'); } private async session(binding: WorksSquareAccountBinding, signal: AbortSignal): Promise { if (this.cached && this.cached.expiresAt > Date.now() + 5000 && this.cached.binding.accountKey === binding.accountKey && this.cached.binding.epoch === binding.epoch) { return this.cached; } const token = await getValidWorksSquareAccessToken(); this.requireCurrent(binding); if (!token) throw new CloudAgentsError(401, 'session_expired'); const value = record(await this.fetchJson(`${WORKS_SQUARE_CONFIG.apiBaseUrl}/api/cloud-agents/session`, { method: 'POST', signal, redirect: 'error', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, })); this.requireCurrent(binding); const url = new URL(textField(value.api_base_url, 2048)); if ((url.protocol !== 'https:' && !(url.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname))) || url.username || url.password || url.search || url.hash || value.scope !== 'makelore-agents' || value.token_type !== 'bearer' || typeof value.expires_at !== 'number' || value.expires_at * 1000 <= Date.now() || value.expires_at * 1000 > Date.now() + 300000) { throw new CloudAgentsError(502, 'cloud_service_unavailable'); } this.cached = { binding, accessToken: textField(value.access_token, 8192), apiBaseUrl: url.toString().replace(/\/+$/, ''), expiresAt: value.expires_at * 1000, }; return this.cached; } private async fetchJson(url: string, init: RequestInit): Promise { const response = await this.fetchImpl(url, init); const raw = await response.text(); if (raw.length > 4_000_000) throw new CloudAgentsError(502, 'cloud_service_unavailable'); const body: unknown = JSON.parse(raw); if (!response.ok) { const code = record(record(body).detail).code; throw new CloudAgentsError( response.status, typeof code === 'string' && Object.hasOwn(MESSAGES, code) ? code : response.status === 422 ? 'invalid_input' : response.status === 401 ? 'session_expired' : response.status === 403 ? 'access_revoked' : 'cloud_service_unavailable', ); } return body; } }