import type { AiSearchParams, AiSearchResponse, AiSearchResult, ChatParams, ChatResponse, Choice, ChoiceMessage, ImageParams, ImageResult, RerankParams, RerankResponse, Usage, VideoParams, VideoTaskResult, } from './models' export class SkillsClient { private readonly apiKey: string; private readonly baseUrl: string; constructor(options: { apiKey: string; baseUrl: string }) { this.apiKey = options.apiKey; this.baseUrl = options.baseUrl; } private headers(): Record { return { Authorization: `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', }; } private url(path: string): string { return `${this.baseUrl}/skills/v1/${path}`; } private async post(path: string, body: unknown): Promise { const resp = await fetch(this.url(path), { method: 'POST', headers: this.headers(), body: JSON.stringify(body), }); if (!resp.ok) { throw new Error(`HTTP ${resp.status}: ${await resp.text()}`); } return resp.json() as Promise; } // ────────── Chat ────────── private buildChatBody(params: ChatParams): Record { const body: Record = { model: params.model ?? 'gpt-5.1', messages: params.messages, stream: params.stream ?? false, }; if (params.maxTokens != null) body.maxTokens = params.maxTokens; if (params.temperature != null) body.temperature = params.temperature; if (params.topP != null) body.topP = params.topP; if (params.responseFormat != null) body.responseFormat = params.responseFormat; if (params.tools != null) body.tools = params.tools; if (params.toolChoice != null) body.toolChoice = params.toolChoice; return body; } /** 非流式聊天 */ async chat(params: ChatParams): Promise { const resp = await fetch(this.url('chat/completions'), { method: 'POST', headers: this.headers(), body: JSON.stringify(this.buildChatBody({ ...params, stream: false })), }); if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${await resp.text()}`); return parseChatResponse(await resp.json() as Record); } /** 流式聊天 (SSE) */ async *chatStream(params: ChatParams): AsyncGenerator { const resp = await fetch(this.url('chat/completions'), { method: 'POST', headers: this.headers(), body: JSON.stringify(this.buildChatBody({ ...params, stream: true })), }); if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${await resp.text()}`); if (!resp.body) throw new Error('Response body is null'); yield* parseSSEStream(resp.body, parseChatResponse); } // ────────── Image ────────── /** 图片生成 */ async imageGenerate(params: ImageParams): Promise { return this.post('image/generate', { model: params.model ?? 'Nano Banana Pro', ...params, }); } // ────────── Video ────────── /** 创建视频生成任务 */ async videoCreateTask(params: VideoParams): Promise { return this.post('video/tasks', { model: params.model ?? 'Doubao-Seedance-1.5-pro', ...params, }); } /** 查询视频生成任务状态 */ async videoGetTask(params: Pick): Promise { return this.post('video/tasks/query', { model: params.model ?? 'Doubao-Seedance-1.5-pro', taskId: params.taskId, }); } // ────────── AI Search ────────── /** AI 搜索(非流式) */ async aiSearch(params: AiSearchParams): Promise { return this.post('aiSearch', { model: params.model ?? 'aiSearch', ...params, stream: false, }); } /** AI 搜索(流式 SSE) */ async *aiSearchStream(params: AiSearchParams): AsyncGenerator { const resp = await fetch(this.url('aiSearch'), { method: 'POST', headers: this.headers(), body: JSON.stringify({ model: params.model ?? 'aiSearch', ...params, stream: true, }), }); if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${await resp.text()}`); if (!resp.body) throw new Error('Response body is null'); yield* parseSSEStream(resp.body, (data) => (data.data ?? data) as AiSearchResponse); } // ────────── Rerank ────────── /** 重排 */ async rerank(params: RerankParams): Promise { return this.post('rerank', { model: params.model ?? 'qwen3-vl-rerank', ...params, }); } } // ────────── helpers ────────── async function* parseSSEStream( body: ReadableStream, parse: (data: Record) => T, ): AsyncGenerator { const decoder = new TextDecoder(); const reader = body.getReader(); let buffer = ''; try { while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() ?? ''; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || !trimmed.startsWith('data:')) continue; const dataStr = trimmed.slice(5).trim(); if (dataStr === '[DONE]') return; try { yield parse(JSON.parse(dataStr) as Record); } catch { /* skip malformed lines */ } } } } finally { reader.releaseLock(); } } function parseChatResponse(data: Record): ChatResponse { const rawChoices = (data.choices as Record[] | undefined) ?? []; const choices: Choice[] = rawChoices.map((c) => { const msgData = c.message as Record | undefined; const deltaData = c.delta as Record | undefined; const toMsg = (d: Record): ChoiceMessage => ({ role: d.role as string | undefined, content: d.content as string | undefined, reasoningContent: d.reasoningContent as string | undefined, toolCalls: d.toolCalls as unknown[] | undefined, }); return { message: msgData ? toMsg(msgData) : undefined, delta: deltaData ? toMsg(deltaData) : undefined, finishReason: (c.finishReason ?? c.finish_reason) as string | undefined, }; }); const usageData = data.usage as Record | undefined; const usage: Usage | undefined = usageData ? { promptTokens: (usageData.promptTokens ?? usageData.prompt_tokens) as number | undefined, completionTokens: (usageData.completionTokens ?? usageData.completion_tokens) as number | undefined, totalTokens: (usageData.totalTokens ?? usageData.total_tokens) as number | undefined, } : undefined; return { id: data.id as string | undefined, model: data.model as string | undefined, success: data.success as boolean | undefined, errorMessage: (data.errorMessage ?? data.error_message) as string | undefined, choices, usage, }; }