feat: provides a queued based API service to allow async processing and authentication management
This commit is contained in:
parent
a276027bcf
commit
ff881653ef
155
apps/demo-nextjs-app-router/api_scripts/tuzi_api_queue.md
Normal file
155
apps/demo-nextjs-app-router/api_scripts/tuzi_api_queue.md
Normal file
@ -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
|
||||||
12
apps/demo-nextjs-app-router/app/api/tu_zi/queue/route.tsx
Normal file
12
apps/demo-nextjs-app-router/app/api/tu_zi/queue/route.tsx
Normal file
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
51
apps/demo-nextjs-app-router/app/api/tu_zi/route.tsx
Normal file
51
apps/demo-nextjs-app-router/app/api/tu_zi/route.tsx
Normal file
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,9 +5,9 @@ import { v4 as uuidv4 } from "uuid"; // Make sure to install this package
|
|||||||
// Configure the queue with a concurrency limit
|
// Configure the queue with a concurrency limit
|
||||||
const scriptQueue = new PQueue({ concurrency: 2 });
|
const scriptQueue = new PQueue({ concurrency: 2 });
|
||||||
|
|
||||||
// Cleanup interval (24 hours in ms)
|
// Cleanup interval (1 hours in ms)
|
||||||
const CLEANUP_INTERVAL = 24 * 60 * 60 * 1000;
|
const CLEANUP_INTERVAL = 1 * 60 * 60 * 1000;
|
||||||
const RESULT_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours in ms
|
const RESULT_EXPIRY = 3 * 60 * 60 * 1000;
|
||||||
|
|
||||||
export type ScriptParams = {
|
export type ScriptParams = {
|
||||||
scriptPath: string;
|
scriptPath: string;
|
||||||
|
|||||||
191
apps/demo-nextjs-app-router/services/tuziApiQueueService.ts
Normal file
191
apps/demo-nextjs-app-router/services/tuziApiQueueService.ts
Normal file
@ -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<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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<string, ApiQueueState>();
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user