Files
wehub-resource-sync 3cd11ababe
Check Markdown links / linkChecker (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:38:56 +08:00

376 lines
11 KiB
TypeScript

import { HookEventType, PluginContext } from './types';
interface PostOptions extends RequestInit {
headers?: Record<string, string>;
[key: string]: any;
}
export interface ErrorResponse {
status: number;
statusText: string;
body: string;
headers?: Headers;
}
export class HttpError extends Error {
response: ErrorResponse;
constructor(message: string, response: ErrorResponse) {
super(message);
this.name = 'HttpError';
this.response = response;
}
}
export class TimeoutError extends Error {
url: string;
timeout: number;
method: string;
constructor(message: string, url: string, timeout: number, method: string) {
super(message);
this.name = 'TimeoutError';
this.url = url;
this.timeout = timeout;
this.method = method;
}
}
/**
* Helper function to get the text from the current content part of a request/response context
* @param context - The plugin context containing request/response data
* @param eventType - The type of hook event (beforeRequestHook or afterRequestHook)
* @returns The text from the current content part of the request/response context
*/
export const getText = (
context: PluginContext,
eventType: HookEventType
): string => {
return getCurrentContentPart(context, eventType)
.textArray.filter((text) => text)
.join('\n');
};
/**
* Extracts the current content and its text representation from a request/response context
* @param context - The plugin context containing request/response data
* @param eventType - The type of hook event (beforeRequestHook or afterRequestHook)
* @returns An object containing the raw content and an array of extracted text strings
*/
export const getCurrentContentPart = (
context: PluginContext,
eventType: HookEventType
): {
content: Array<any> | string | Record<string, any> | null;
textArray: Array<string>;
} => {
// Determine if we're handling request or response data
const target = eventType === 'beforeRequestHook' ? 'request' : 'response';
const json = context[target].json;
if (target === 'request') {
return getRequestContentPart(json, context.requestType!);
} else {
return getResponseContentPart(json, context.requestType || '');
}
};
const getRequestContentPart = (json: any, requestType: string) => {
let content: Array<any> | string | Record<string, any> | null = null;
let textArray: Array<string> = [];
if (requestType === 'chatComplete' || requestType === 'messages') {
content = json.messages[json.messages.length - 1].content;
textArray = Array.isArray(content)
? content.map((item: any) => item.text || '')
: [content];
} else if (requestType === 'complete') {
content = json.prompt;
textArray = Array.isArray(content)
? content.map((item: any) => item)
: [content];
} else if (requestType === 'embed') {
content = json.input;
textArray = Array.isArray(content) ? content : [content];
}
return { content, textArray };
};
const getResponseContentPart = (json: any, requestType: string) => {
let content: Array<any> | string | Record<string, any> | null = null;
let textArray: Array<string> = [];
// This can happen for streaming mode.
if (!json) {
return { content: null, textArray: [] };
}
if (requestType === 'chatComplete') {
content = json.choices[0].message.content as string;
textArray = [content];
} else if (requestType === 'complete') {
content = json.choices[0].text as string;
textArray = [content];
} else if (requestType === 'messages') {
content = json.content;
textArray = (content as Array<any>).map((item: any) => item.text || '');
}
return { content, textArray };
};
/**
* Updates the content of a request or response in the plugin context
* @param context - The plugin context containing request/response data
* @param eventType - The type of hook event (beforeRequestHook or afterRequestHook)
* @param newContent - New content to set (can be string, array, or object)
* @param textArray - Optional array of text strings to update text fields
*/
export const setCurrentContentPart = (
context: PluginContext,
eventType: HookEventType,
transformedData: Record<string, any>,
textArray: Array<string | null> | null = null
): void => {
const requestType = context.requestType;
const target = eventType === 'beforeRequestHook' ? 'request' : 'response';
const json = context[target].json;
if (textArray?.length === 0 || !textArray) {
return;
}
if (target === 'request') {
setRequestContentPart(json, requestType!, textArray, transformedData);
} else {
setResponseContentPart(json, requestType!, textArray, transformedData);
}
};
function setRequestContentPart(
json: any,
requestType: string,
textArray: Array<string | null>,
transformedData: Record<string, any>
) {
// Create a safe to use shallow copy of the json
const updatedJson = { ...json };
if (requestType === 'chatComplete' || requestType === 'messages') {
updatedJson.messages = [...json.messages];
const lastMessage = {
...updatedJson.messages[updatedJson.messages.length - 1],
};
const originalContent = lastMessage.content;
if (Array.isArray(originalContent)) {
lastMessage.content = originalContent.map((item: any, index: number) => ({
...item,
text: textArray[index] || item.text,
}));
} else {
lastMessage.content = textArray[0] || originalContent;
}
updatedJson.messages[updatedJson.messages.length - 1] = lastMessage;
} else if (requestType === 'complete') {
updatedJson.prompt = Array.isArray(updatedJson.prompt)
? textArray.map((text, index) => text || updatedJson.prompt[index])
: textArray[0];
}
transformedData.request.json = updatedJson;
}
function setResponseContentPart(
json: any,
requestType: string,
textArray: Array<string | null>,
transformedData: Record<string, any>
) {
// Create a safe to use shallow copy of the json
const updatedJson = { ...json };
if (requestType === 'chatComplete') {
updatedJson.choices = [...json.choices];
const firstChoice = {
...updatedJson.choices[0],
};
firstChoice.message = {
...firstChoice.message,
content: textArray[0] || firstChoice.message.content,
};
updatedJson.choices[0] = firstChoice;
} else if (requestType === 'complete') {
updatedJson.choices = [...json.choices];
updatedJson.choices[json.choices.length - 1].text =
textArray[0] || json.choices[json.choices.length - 1].text;
} else if (requestType === 'messages') {
updatedJson.content = textArray.map(
(text, index) => text || updatedJson.content[index]
);
}
transformedData.response.json = updatedJson;
}
/**
* Sends a POST request to the specified URL with the given data and timeout.
* @param url - The URL to send the POST request to.
* @param data - The data to be sent in the request body.
* @param options - Additional options for the fetch call.
* @param timeout - Timeout in milliseconds (default: 5 seconds).
* @returns A promise that resolves to the JSON response.
* @throws {HttpError} Throws an HttpError with detailed information if the request fails.
* @throws {Error} Throws a generic Error for network issues or timeouts.
*/
export async function post<T = any>(
url: string,
data: any,
options: PostOptions = {},
timeout: number = 5000
): Promise<T> {
const defaultOptions: PostOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
};
const mergedOptions: PostOptions = { ...defaultOptions, ...options };
if (mergedOptions.headers) {
mergedOptions.headers = {
...defaultOptions.headers,
...mergedOptions.headers,
};
}
try {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const response: Response = await fetch(url, {
...mergedOptions,
signal: controller.signal,
});
clearTimeout(id);
if (!response.ok) {
let errorBody: string;
try {
errorBody = await response.text();
} catch (e) {
errorBody = 'Unable to retrieve response body';
}
const errorResponse: ErrorResponse = {
status: response.status,
statusText: response.statusText,
body: errorBody,
headers: response.headers,
};
throw new HttpError(
`HTTP error! status: ${response.status}`,
errorResponse
);
}
return (await response.json()) as T;
} catch (error: any) {
if (error instanceof HttpError) {
throw error;
}
if (error.name === 'AbortError') {
throw new TimeoutError(
`Request timed out after ${timeout}ms`,
url,
timeout,
mergedOptions.method || 'POST'
);
}
// console.error('Error in post request:', error);
throw error;
}
}
/**
* Sends a POST request to the specified URL with the given data and timeout.
* @param url - The URL to send the POST request to.
* @param data - The data to be sent in the request body.
* @param options - Additional options for the fetch call.
* @param timeout - Timeout in milliseconds (default: 5 seconds).
* @returns A promise that resolves to the JSON response.
* @throws {HttpError} Throws an HttpError with detailed information if the request fails.
* @throws {Error} Throws a generic Error for network issues or timeouts.
*/
export async function postWithCloudflareServiceBinding<T = any>(
url: string,
data: any,
serviceBinding: any,
options: PostOptions = {},
timeout: number = 5000
): Promise<T> {
const defaultOptions: PostOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
};
const mergedOptions: PostOptions = { ...defaultOptions, ...options };
if (mergedOptions.headers) {
mergedOptions.headers = {
...defaultOptions.headers,
...mergedOptions.headers,
};
}
try {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const response: Response = await serviceBinding.fetch(url, {
...mergedOptions,
signal: controller.signal,
});
clearTimeout(id);
if (!response.ok) {
let errorBody: string;
try {
errorBody = await response.text();
} catch (e) {
errorBody = 'Unable to retrieve response body';
}
const errorResponse: ErrorResponse = {
status: response.status,
statusText: response.statusText,
body: errorBody,
};
throw new HttpError(
`HTTP error! status: ${response.status}`,
errorResponse
);
}
return (await response.json()) as T;
} catch (error: any) {
if (error instanceof HttpError) {
throw error;
}
if (error.name === 'AbortError') {
throw new TimeoutError(
`Request timed out after ${timeout}ms`,
url,
timeout,
mergedOptions.method || 'POST'
);
}
// console.error('Error in post request:', error);
throw error;
}
}