"添加前端模板和运行代码模块"
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
import ky, { type KyResponse, type AfterResponseHook, type NormalizedOptions } from 'ky';
|
||||
import { createParser, type EventSourceParser } from 'eventsource-parser';
|
||||
|
||||
export interface SSEOptions {
|
||||
onData: (data: string) => void;
|
||||
onEvent?: (event: any) => void;
|
||||
onCompleted?: (error?: Error) => void;
|
||||
onAborted?: () => void;
|
||||
onReconnectInterval?: (interval: number) => void;
|
||||
}
|
||||
|
||||
export const createSSEHook = (options: SSEOptions): AfterResponseHook => {
|
||||
const hook: AfterResponseHook = async (request: Request, _options: NormalizedOptions, response: KyResponse) => {
|
||||
if (!response.ok || !response.body) {
|
||||
return;
|
||||
}
|
||||
|
||||
let completed = false;
|
||||
const innerOnCompleted = (error?: Error): void => {
|
||||
if (completed) {
|
||||
return;
|
||||
}
|
||||
|
||||
completed = true;
|
||||
options.onCompleted?.(error);
|
||||
};
|
||||
|
||||
const isAborted = false;
|
||||
|
||||
const reader: ReadableStreamDefaultReader<Uint8Array> = response.body.getReader();
|
||||
|
||||
const decoder = new TextDecoder('utf8');
|
||||
|
||||
const parser: EventSourceParser = createParser({
|
||||
onEvent: (event) => {
|
||||
if (event.data) {
|
||||
options.onEvent?.(event);
|
||||
const dataArray: string[] = event.data.split('\n');
|
||||
for (const data of dataArray) {
|
||||
options.onData(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const read = (): void => {
|
||||
if (isAborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
reader.read().then((result: ReadableStreamReadResult<Uint8Array>) => {
|
||||
if (result.done) {
|
||||
innerOnCompleted();
|
||||
return;
|
||||
}
|
||||
|
||||
parser.feed(decoder.decode(result.value, { stream: true }));
|
||||
|
||||
read();
|
||||
}).catch(error => {
|
||||
if (request.signal.aborted) {
|
||||
options.onAborted?.();
|
||||
return;
|
||||
}
|
||||
|
||||
innerOnCompleted(error as Error);
|
||||
});
|
||||
};
|
||||
|
||||
read();
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
return hook;
|
||||
};
|
||||
|
||||
type ContentPart =
|
||||
| { type: 'text'; text: string }
|
||||
| { type: 'image_url'; image_url: { url: string } };
|
||||
|
||||
export interface ChatMessage {
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string | ContentPart[];
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface ChatStreamOptions {
|
||||
endpoint: string;
|
||||
messages: ChatMessage[];
|
||||
apiId: string;
|
||||
onUpdate: (content: string) => void;
|
||||
onComplete: () => void;
|
||||
onError: (error: Error) => void;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export const sendChatStream = async (options: ChatStreamOptions): Promise<void> => {
|
||||
const { messages, onUpdate, onComplete, onError, signal } = options;
|
||||
|
||||
let currentContent = '';
|
||||
|
||||
const sseHook = createSSEHook({
|
||||
onData: (data: string) => {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
if (parsed.choices?.[0]?.delta?.content) {
|
||||
currentContent += parsed.choices[0].delta.content;
|
||||
onUpdate(currentContent);
|
||||
}
|
||||
} catch {
|
||||
console.warn('Failed to parse SSE data:', data);
|
||||
}
|
||||
},
|
||||
onCompleted: (error?: Error) => {
|
||||
if (error) {
|
||||
onError(error);
|
||||
} else {
|
||||
onComplete();
|
||||
}
|
||||
},
|
||||
onAborted: () => {
|
||||
console.log('Stream aborted');
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await ky.post(options.endpoint, {
|
||||
json: {
|
||||
messages: messages.map(msg => ({
|
||||
role: msg.role,
|
||||
content: msg.content
|
||||
})),
|
||||
enable_thinking: false
|
||||
},
|
||||
headers: {
|
||||
'X-App-Id': options.apiId,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
signal,
|
||||
hooks: {
|
||||
afterResponse: [sseHook]
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (!signal?.aborted) {
|
||||
onError(error as Error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const APP_ID = import.meta.env.VITE_APP_ID;
|
||||
|
||||
const apiClient = axios.create({
|
||||
timeout: 60000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-App-Id': APP_ID
|
||||
}
|
||||
});
|
||||
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response.data,
|
||||
(error) => {
|
||||
console.error('API 请求错误:', error);
|
||||
if (error.response?.data?.status === 999) {
|
||||
throw new Error(error.response.data.msg);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export interface ImageGenerationParams {
|
||||
prompt: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
image_num?: number;
|
||||
}
|
||||
|
||||
export interface ImageGenerationResponse {
|
||||
status: number;
|
||||
msg: string;
|
||||
data: {
|
||||
task_id: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ImageQueryResponse {
|
||||
status: number;
|
||||
msg: string;
|
||||
data: {
|
||||
task_status: string;
|
||||
task_progress: number;
|
||||
sub_task_result_list?: Array<{
|
||||
sub_task_status: string;
|
||||
final_image_list?: Array<{
|
||||
img_url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export const generateImage = async (params: ImageGenerationParams): Promise<ImageGenerationResponse> => {
|
||||
return apiClient.post(
|
||||
'https://api-integrations.appmiaoda.com/app-7a7nlc9zki69/api-2bk93oeO9Nyz/v1/bce/wenxinworkshop/ai_custom_image_creation/v2/text2image',
|
||||
{
|
||||
prompt: params.prompt,
|
||||
width: params.width || 768,
|
||||
height: params.height || 1360,
|
||||
image_num: params.image_num || 1
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const queryImageResult = async (taskId: number): Promise<ImageQueryResponse> => {
|
||||
return apiClient.post(
|
||||
'https://api-integrations.appmiaoda.com/app-7a7nlc9zki69/api-2jBYdN3A9Nyz/v1/bce/wenxinworkshop/ai_custom_image_creation/v2/query_task',
|
||||
{
|
||||
task_id: taskId
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const pollImageResult = async (
|
||||
taskId: number,
|
||||
maxAttempts = 30,
|
||||
interval = 2000
|
||||
): Promise<string> => {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
const result = await queryImageResult(taskId);
|
||||
|
||||
if (result.data.task_status === 'SUCCESS') {
|
||||
const imageUrl = result.data.sub_task_result_list?.[0]?.final_image_list?.[0]?.img_url;
|
||||
if (imageUrl) {
|
||||
return imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.data.task_status === 'FAILED') {
|
||||
throw new Error('图片生成失败');
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, interval));
|
||||
}
|
||||
|
||||
throw new Error('图片生成超时');
|
||||
};
|
||||
Reference in New Issue
Block a user