diff --git a/apps/demo-nextjs-app-router/api_scripts/tuzi_api_queue.md b/apps/demo-nextjs-app-router/api_scripts/tuzi_api_queue.md new file mode 100644 index 0000000..d196679 --- /dev/null +++ b/apps/demo-nextjs-app-router/api_scripts/tuzi_api_queue.md @@ -0,0 +1,155 @@ +# Tu-Zi API Proxy Service Documentation + +## Overview + +This API provides a queueing proxy service for requests to Tu-Zi services. It allows asynchronous processing of requests with authentication management, concurrency control, and status tracking. + +## Base URL + +``` +/api/tu_zi +``` + +## Endpoints + +### Submit API Request + +Submits a request to a Tu-Zi API endpoint and returns a token for tracking. + +**Endpoint:** `POST /api/tu_zi` + +**Headers:** + +- `X-Api-Target` (required): The full URL of the Tu-Zi API endpoint to call + +**Request Body:** + +- The raw body content to be forwarded to the target API + +**Response:** + +```json +{ + "token": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +**Example:** + +```bash +curl -X POST "https://animator-gg-api.ca2324.servep2p.com:8443/api/tu_zi" \ + -H "X-Api-Target: https://api.tu-zi.com/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "gpt-4-gizmo-g-2fkFE8rbu", + "messages": [ + { + "role": "user", + "content": "Who are you? What can you do?" + } + ], + "stream": false +}' +``` + +### Get Request Status + +Retrieves the status and result of an API request by token. + +**Endpoint:** `GET /api/tu_zi?token={token}` + +**Query Parameters:** + +- `token` (required): The execution token returned by the POST request + +**Response:** + +```json +{ + "status": "completed", + "result": { + "taskId": "550e8400-e29b-41d4-a716-446655440000", + "success": true, + "response": "{ ... response from Tu-Zi API ... }" + }, + "timestamp": 1743991194237 +} +``` + +**Status Values:** + +- `pending`: Request is waiting in the queue +- `running`: Request is currently being processed +- `completed`: Request has completed successfully +- `failed`: Request failed to complete + +**Example:** + +```bash +curl -X GET "https://animator-gg-api.ca2324.servep2p.com:8443/api/tu_zi?token=550e8400-e29b-41d4-a716-446655440000" +``` + +### Get Queue Statistics + +Retrieves information about the current request queue. + +**Endpoint:** `GET /api/tu_zi/queue` + +**Response:** + +```json +{ + "size": 3, + "pending": 2, + "isPaused": false, + "activeExecutions": 5 +} +``` + +**Response Fields:** + +- `size`: Number of requests in the queue +- `pending`: Number of pending requests +- `isPaused`: Whether the queue is paused +- `activeExecutions`: Total number of active executions (including completed but not yet expired) + +**Example:** + +```bash +curl -X GET "https://animator-gg-api.ca2324.servep2p.com:8443/api/tu_zi/queue" +``` + +## Error Handling + +The API returns appropriate HTTP status codes: + +- `200 OK`: Request processed successfully +- `400 Bad Request`: Missing required parameters +- `500 Internal Server Error`: Server-side error during request execution + +Error responses include a JSON body with an error message: + +```json +{ + "error": "Error message description" +} +``` + +## Authentication + +The service automatically adds appropriate authentication headers to requests based on the target hostname. Currently, the service supports: + +- Tu-Zi domains (`*.tu-zi.com`) + +## Request Lifecycle + +- The service uses a queue to manage concurrent requests (default: 2) +- Each request result is stored for 3 hours after completion +- After this time, the result will be purged, and querying the token will return an "Invalid execution token or result expired" error + +## Client Usage Pattern + +1. Submit a request to the Tu-Zi API through the proxy +2. Receive a token immediately +3. Poll the status endpoint with the token until the status is "completed" or "failed" +4. Process the response or handle the error diff --git a/apps/demo-nextjs-app-router/app/api/tu_zi/queue/route.tsx b/apps/demo-nextjs-app-router/app/api/tu_zi/queue/route.tsx new file mode 100644 index 0000000..859e18d --- /dev/null +++ b/apps/demo-nextjs-app-router/app/api/tu_zi/queue/route.tsx @@ -0,0 +1,12 @@ +import { NextResponse } from "next/server"; +import { getQueueStats } from "../../../../services/tuziApiQueueService"; + +// Endpoint to get queue statistics +export async function GET() { + try { + const stats = getQueueStats(); + return NextResponse.json(stats); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/apps/demo-nextjs-app-router/app/api/tu_zi/route.tsx b/apps/demo-nextjs-app-router/app/api/tu_zi/route.tsx new file mode 100644 index 0000000..c2f111d --- /dev/null +++ b/apps/demo-nextjs-app-router/app/api/tu_zi/route.tsx @@ -0,0 +1,51 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + getApiCallState, + queueApiRequest, +} from "../../../services/tuziApiQueueService"; + +// Modified POST method that returns a token instead of waiting for execution result +export async function POST(request: NextRequest) { + try { + const body = await request.text(); + + const HEADER_X_TARGET = "X-Api-Target"; + const apiTargetUrl = request.headers.get(HEADER_X_TARGET); + if (!apiTargetUrl) { + return NextResponse.json( + { error: `Header ${HEADER_X_TARGET} is required` }, + { status: 400 }, + ); + } + + const token = queueApiRequest({ + apiTarget: apiTargetUrl, + postBody: body, + }); + + return NextResponse.json({ token }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} + +// API to query execution result by token from path parameter +export async function GET(request: NextRequest) { + try { + // Get token from the search params in the URL + const { searchParams } = new URL(request.url); + const token = searchParams.get("token"); + + if (!token) { + return NextResponse.json( + { error: "Token is required as a query parameter" }, + { status: 400 }, + ); + } + + const execution = getApiCallState(token); + return NextResponse.json(execution); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/apps/demo-nextjs-app-router/services/bashExecService.ts b/apps/demo-nextjs-app-router/services/bashExecService.ts index f0c6f01..69f182c 100644 --- a/apps/demo-nextjs-app-router/services/bashExecService.ts +++ b/apps/demo-nextjs-app-router/services/bashExecService.ts @@ -5,9 +5,9 @@ import { v4 as uuidv4 } from "uuid"; // Make sure to install this package // Configure the queue with a concurrency limit const scriptQueue = new PQueue({ concurrency: 2 }); -// Cleanup interval (24 hours in ms) -const CLEANUP_INTERVAL = 24 * 60 * 60 * 1000; -const RESULT_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours in ms +// Cleanup interval (1 hours in ms) +const CLEANUP_INTERVAL = 1 * 60 * 60 * 1000; +const RESULT_EXPIRY = 3 * 60 * 60 * 1000; export type ScriptParams = { scriptPath: string; diff --git a/apps/demo-nextjs-app-router/services/tuziApiQueueService.ts b/apps/demo-nextjs-app-router/services/tuziApiQueueService.ts new file mode 100644 index 0000000..fdfb424 --- /dev/null +++ b/apps/demo-nextjs-app-router/services/tuziApiQueueService.ts @@ -0,0 +1,191 @@ +import PQueue from "p-queue"; +import { v4 as uuidv4 } from "uuid"; // Make sure to install this package + +// Configure the queue with a concurrency limit +const scriptQueue = new PQueue({ concurrency: 2 }); + +// Cleanup interval (1 hours in ms) +const CLEANUP_INTERVAL = 1 * 60 * 60 * 1000; +const RESULT_EXPIRY = 3 * 60 * 60 * 1000; +// API authentication settings +const API_AUTH_SETTINGS: [RegExp, string][] = [ + [/(\.|^)tu-zi\.com$/, `Bearer ${process.env.TUZI_API_KEY}`], +]; + +export type ApiParams = { + apiTarget: string; + postBody: string; + headers?: Map; +}; + +export type ApiResult = { + taskId: string; + success: boolean; + response: string; + error?: string; +}; + +export type ApiQueueState = { + status: "pending" | "running" | "completed" | "failed"; + result?: ApiResult; + timestamp: number; +}; + +// Store execution results by token +const apiExecResults = new Map(); +/** + * Clean up expired execution results + */ +function cleanupExpiredResults() { + const now = Date.now(); + for (const [token, data] of apiExecResults.entries()) { + if (now - data.timestamp > RESULT_EXPIRY) { + apiExecResults.delete(token); + } + } +} + +// Set up periodic cleanup +setInterval(cleanupExpiredResults, CLEANUP_INTERVAL); + +export function queueApiRequest(params: ApiParams): string { + const token = uuidv4(); + + // Initialize result as pending with current timestamp + apiExecResults.set(token, { + status: "pending", + timestamp: Date.now(), + }); + + // Add the API request to the queue + scriptQueue.add(async () => { + try { + // Update status to running + apiExecResults.set(token, { + status: "running", + timestamp: Date.now(), + }); + + // Prepare headers with authentication + const headers = new Headers(); + + // Add authentication based on URL pattern + var allowdHost = false; + const targetURL = new URL(params.apiTarget); + // Iterate over the API_AUTH_SETTINGS array + for (const [pattern, authValue] of API_AUTH_SETTINGS) { + if (pattern.test(targetURL.hostname)) { + headers.set("Authorization", authValue); + allowdHost = true; + break; // Stop checking once a match is found + } + } + + // If no matching pattern found, throw an error + if (!allowdHost) { + throw new Error( + `No matching authentication pattern for ${targetURL.hostname}`, + ); + } + + if (params.headers) { + for (const [key, value] of params.headers.entries()) { + headers.set(key, value); + } + } + + // Set content type if not already set + if (!headers.has("Content-Type")) { + headers.set("Content-Type", "application/json"); + } + + // Execute the fetch request + const response = await fetch(params.apiTarget, { + method: "POST", + headers: headers, + body: params.postBody, + }); + + // Check if response is OK + if (!response.ok) { + throw new Error(`API request failed with status ${response.status}`); + } + + // Get the response text + const responseText = await response.text(); + + // Store successful result + apiExecResults.set(token, { + status: "completed", + result: { + taskId: token, + success: true, + response: responseText, + }, + timestamp: Date.now(), + }); + } catch (error: any) { + // Handle API execution errors + apiExecResults.set(token, { + status: "failed", + result: { + taskId: token, + success: false, + response: "", + error: error.message || "Failed to execute API request", + }, + timestamp: Date.now(), + }); + } + }); + + return token; +} + +/** + * Query the status and result of a script execution + * @param token The token returned by executeScript + * @returns An object containing execution status and result (if available) + */ +export function getApiCallState(token: string): ApiQueueState { + const execution = apiExecResults.get(token); + + if (!execution) { + return { + status: "failed", + result: { + taskId: token, + success: false, + response: "", + error: "Invalid execution token or result expired", + }, + timestamp: Date.now(), + }; + } + + return { + status: execution.status, + result: execution.result, + timestamp: Date.now(), + }; +} + +// Get current queue size and pending tasks +export function getQueueStats() { + return { + size: scriptQueue.size, + pending: scriptQueue.pending, + isPaused: scriptQueue.isPaused, + activeExecutions: apiExecResults.size, + }; +} + +// Pause the queue (no further tasks will be executed until resumed) +export function pauseQueue() { + scriptQueue.pause(); +} + +// Resume the queue +export function resumeQueue() { + scriptQueue.start(); +}