import type { CloudAgentDraft, CloudAgentPage, CreateCloudAgent, SaveCloudAgentDraft } 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: '请检查名称、用途和配置内容', }; 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), }; } catch { throw new CloudAgentsError(502, 'cloud_service_unavailable'); } } type Session = { binding: WorksSquareAccountBinding; accessToken: string; apiBaseUrl: string; expiresAt: number; }; /** Main owns both clouds' credentials; public methods return only Agent drafts. */ export class CloudAgentsModule { private cached: Session | null = null; private readonly unsubscribe: () => void; constructor(private readonly fetchImpl: (input: string, init?: RequestInit) => Promise = proxyAwareFetch) { this.unsubscribe = subscribeWorksSquareSession(() => { if (this.cached && !isCurrentWorksSquareAccountBinding(this.cached.binding)) this.cached = null; }); } dispose(): void { this.cached = null; this.unsubscribe(); } async list(cursor: string | null = null): Promise { if (cursor !== null && !/^\d{1,16}$/.test(cursor)) throw new CloudAgentsError(422, 'invalid_input'); const body = record(await this.request('/api/makelore/agents' + (cursor ? `?cursor=${cursor}` : ''), '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 projectDraft(await this.request('/api/makelore/agents', 'POST', body)); } 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), }; return projectDraft(await this.request(`/api/makelore/agents/${slug(agentSlug)}/draft`, 'PATCH', body)); } private async request(path: string, method: string, body?: unknown): Promise { const binding = getWorksSquareAccountBinding(); if (!binding) throw new CloudAgentsError(401, 'session_expired'); try { return await runWithDeadline(async (signal) => { const session = await this.session(binding, signal); this.requireCurrent(binding); const result = await this.fetchJson(session.apiBaseUrl + path, { method, signal, redirect: 'error', headers: { Authorization: `Bearer ${session.accessToken}`, 'Content-Type': 'application/json' }, ...(body === undefined ? {} : { 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; if (error.status === 422) throw new CloudAgentsError(502, 'cloud_service_unavailable'); 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-agent-drafts' || 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 : 'cloud_service_unavailable', ); } return body; } }