-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add crud api #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,13 @@ | ||
| // https://nitro.unjs.io/config | ||
| export default defineNitroConfig({ | ||
| srcDir: 'server', | ||
| compatibilityDate: '2025-07-24', | ||
| compatibilityDate: '2025-07-25', | ||
| storage: { | ||
| redis: { | ||
| driver: 'memory', | ||
| }, | ||
| }, | ||
| experimental: { | ||
| wasm: true, | ||
| }, | ||
| }) |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,141 @@ | ||||||||||
| import type { ChatCompletionResponse, ChatRequestBody } from '@llamakit/core' | ||||||||||
| import type { ClientData } from '../../../../types/api' | ||||||||||
| import { createChatTransformer } from '@llamakit/core' | ||||||||||
|
|
||||||||||
| export default defineEventHandler(async (event): Promise<ChatCompletionResponse> => { | ||||||||||
| const clientId = getRouterParam(event, 'client') | ||||||||||
| const body = await readBody<ChatRequestBody>(event) | ||||||||||
|
|
||||||||||
| if (!clientId) { | ||||||||||
| throw createError({ | ||||||||||
| statusCode: 400, | ||||||||||
| statusMessage: 'Client ID is required', | ||||||||||
| }) | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const storage = useStorage('redis') | ||||||||||
|
|
||||||||||
| try { | ||||||||||
| // 获取client配置 | ||||||||||
| const clientData = await storage.getItem<ClientData>(`client:${clientId}`) | ||||||||||
| if (!clientData) { | ||||||||||
| throw createError({ | ||||||||||
| statusCode: 404, | ||||||||||
| statusMessage: 'Client not found', | ||||||||||
| }) | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // 创建ChatTransformer | ||||||||||
| const transformer = createChatTransformer({ | ||||||||||
| apiKey: clientData.apiKey, | ||||||||||
| baseUrl: clientData.baseUrl, | ||||||||||
| }) | ||||||||||
|
|
||||||||||
| const startTimestamp = new Date() | ||||||||||
| const [result, resolve] = transformer(body, { timestamp: startTimestamp }) | ||||||||||
|
|
||||||||||
| // 生成唯一的日志ID | ||||||||||
| const logId = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}` | ||||||||||
|
|
||||||||||
| // 将初始结果写入本地KV Storage | ||||||||||
| const logs = await storage.getItem<any[]>(`client:${clientId}:logs`) || [] | ||||||||||
| const logEntry = { | ||||||||||
| id: logId, | ||||||||||
| ...result, | ||||||||||
| requestInfo: { | ||||||||||
| url: `${clientData.baseUrl}/v1/chat/completions`, | ||||||||||
| method: 'POST', | ||||||||||
| headers: { | ||||||||||
| 'Authorization': `Bearer ${clientData.apiKey}`, | ||||||||||
|
Leetfs marked this conversation as resolved.
|
||||||||||
| 'Content-Type': 'application/json', | ||||||||||
| }, | ||||||||||
| body: JSON.stringify(body), | ||||||||||
| timestamp: startTimestamp.toISOString(), | ||||||||||
| }, | ||||||||||
| } | ||||||||||
|
|
||||||||||
| logs.unshift(logEntry) // 最新的放在前面 | ||||||||||
| await storage.setItem(`client:${clientId}:logs`, logs) | ||||||||||
|
|
||||||||||
| try { | ||||||||||
| // 转发用户请求到AI服务器 | ||||||||||
| const response = await $fetch<ChatCompletionResponse>(`${clientData.baseUrl}/v1/chat/completions`, { | ||||||||||
| method: 'POST', | ||||||||||
| headers: { | ||||||||||
| 'Authorization': `Bearer ${clientData.apiKey}`, | ||||||||||
| 'Content-Type': 'application/json', | ||||||||||
| }, | ||||||||||
| body, | ||||||||||
| }) | ||||||||||
|
|
||||||||||
| // 处理返回结果(异步写入) | ||||||||||
| const endTimestamp = new Date() | ||||||||||
| const finalResult = resolve(response, { timestamp: endTimestamp }) | ||||||||||
|
|
||||||||||
| // 更新日志条目 | ||||||||||
| const updatedLogs = await storage.getItem<any[]>(`client:${clientId}:logs`) || [] | ||||||||||
| const logIndex = updatedLogs.findIndex(log => log.id === logId) | ||||||||||
| if (logIndex !== -1) { | ||||||||||
| updatedLogs[logIndex] = { | ||||||||||
| ...updatedLogs[logIndex], | ||||||||||
| ...finalResult, | ||||||||||
| responseInfo: { | ||||||||||
| status: 200, | ||||||||||
| headers: {}, | ||||||||||
| body: JSON.stringify(response), | ||||||||||
| timestamp: endTimestamp.toISOString(), | ||||||||||
| duration: endTimestamp.getTime() - startTimestamp.getTime(), | ||||||||||
| }, | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // 异步更新存储,不阻塞响应 | ||||||||||
| storage.setItem(`client:${clientId}:logs`, updatedLogs).catch(console.error) | ||||||||||
|
||||||||||
| storage.setItem(`client:${clientId}:logs`, updatedLogs).catch(console.error) | |
| storage.setItem(`client:${clientId}:logs`, updatedLogs).catch((error) => { | |
| console.error(`Failed to update logs for client: ${clientId}. Operation: setItem. Key: client:${clientId}:logs. Error:`, error); | |
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import type { RemoveClientResponse } from '../../types/api' | ||
|
|
||
| export default defineEventHandler(async (event): Promise<RemoveClientResponse> => { | ||
| const clientId = getRouterParam(event, 'id') | ||
|
|
||
| if (!clientId) { | ||
| throw createError({ | ||
| statusCode: 400, | ||
| statusMessage: 'Client ID is required', | ||
| }) | ||
| } | ||
|
|
||
| const storage = useStorage('redis') | ||
|
|
||
| try { | ||
| // 检查client是否存在 | ||
| const clientData = await storage.getItem(`client:${clientId}`) | ||
| if (!clientData) { | ||
| throw createError({ | ||
| statusCode: 404, | ||
| statusMessage: 'Client not found', | ||
| }) | ||
| } | ||
|
|
||
| // 事务性删除:先删除相关数据,再删除客户端 | ||
| // 清理相关的日志数据 | ||
| await storage.removeItem(`client:${clientId}:logs`) | ||
|
|
||
| // 从client列表中移除 | ||
| const clientList = await storage.getItem<string[]>('client:list') || [] | ||
| const updatedList = clientList.filter(id => id !== clientId) | ||
| await storage.setItem('client:list', updatedList) | ||
|
|
||
| // 最后删除client数据 | ||
| await storage.removeItem(`client:${clientId}`) | ||
|
|
||
| return { | ||
| success: true, | ||
| message: 'Client removed successfully', | ||
| } | ||
| } | ||
| catch (error: any) { | ||
| // 如果是已知错误,直接抛出 | ||
| if (error.statusCode) { | ||
| throw error | ||
| } | ||
|
|
||
| // 处理未知错误 | ||
| throw createError({ | ||
| statusCode: 500, | ||
| statusMessage: 'Failed to remove client', | ||
| }) | ||
| } | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import type { ClientData, UpdateClientBody, UpdateClientResponse } from '../../types/api' | ||
|
|
||
| export default defineEventHandler(async (event): Promise<UpdateClientResponse> => { | ||
| const clientId = getRouterParam(event, 'id') | ||
| const body = await readBody<UpdateClientBody>(event) | ||
|
|
||
| if (!clientId) { | ||
| throw createError({ | ||
| statusCode: 400, | ||
| statusMessage: 'Client ID is required', | ||
| }) | ||
| } | ||
|
|
||
| const storage = useStorage('redis') | ||
|
|
||
| try { | ||
| // 获取现有client数据 | ||
| const existingClient = await storage.getItem<ClientData>(`client:${clientId}`) | ||
| if (!existingClient) { | ||
| throw createError({ | ||
| statusCode: 404, | ||
| statusMessage: 'Client not found', | ||
| }) | ||
| } | ||
|
|
||
| // 更新client数据 | ||
| const updatedClient: ClientData = { | ||
| ...existingClient, | ||
| ...(body.name && { name: body.name }), | ||
| ...(body.baseUrl && { baseUrl: body.baseUrl }), | ||
| ...(body.apiKey && { apiKey: body.apiKey }), | ||
| updatedAt: new Date().toISOString(), | ||
| } | ||
|
|
||
| await storage.setItem(`client:${clientId}`, updatedClient) | ||
|
|
||
| return { | ||
| success: true, | ||
| message: 'Client updated successfully', | ||
| client: updatedClient, | ||
| } | ||
| } | ||
| catch (error: any) { | ||
| // 如果是已知错误,直接抛出 | ||
| if (error.statusCode) { | ||
| throw error | ||
| } | ||
|
|
||
| // 处理未知错误 | ||
| throw createError({ | ||
| statusCode: 500, | ||
| statusMessage: 'Failed to update client', | ||
| }) | ||
| } | ||
| }) |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,62 @@ | ||||||||||||
| import type { ClientData, CreateClientBody, CreateClientResponse } from '../../types/api' | ||||||||||||
| import { randomUUID } from 'node:crypto' | ||||||||||||
|
|
||||||||||||
| export default defineEventHandler(async (event): Promise<CreateClientResponse> => { | ||||||||||||
| const body = await readBody<CreateClientBody>(event) | ||||||||||||
|
|
||||||||||||
| // 验证请求体 | ||||||||||||
| if (!body.apiKey || !body.baseUrl || !body.name) { | ||||||||||||
| throw createError({ | ||||||||||||
| statusCode: 400, | ||||||||||||
| statusMessage: 'Missing required fields: apiKey, baseUrl, or name', | ||||||||||||
| }) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // 验证URL格式 | ||||||||||||
| try { | ||||||||||||
| // eslint-disable-next-line no-new | ||||||||||||
| new URL(body.baseUrl) | ||||||||||||
|
Comment on lines
+17
to
+18
|
||||||||||||
| // eslint-disable-next-line no-new | |
| new URL(body.baseUrl) | |
| if (!URL.canParse(body.baseUrl)) { | |
| throw new Error('Invalid URL') | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import type { ClientData, GetClientsQuery, GetClientsResponse } from '../../types/api' | ||
|
|
||
| export default defineEventHandler(async (event): Promise<GetClientsResponse> => { | ||
| const query = getQuery<GetClientsQuery>(event) | ||
|
|
||
| const limit = Math.min(Number(query.limit) || 10, 100) // 最大100个 | ||
| const offset = Number(query.offset) || 0 | ||
|
|
||
| const storage = useStorage('redis') | ||
|
|
||
| try { | ||
| // 获取所有client ID列表 | ||
| const clientList = await storage.getItem<string[]>('client:list') || [] | ||
| const total = clientList.length | ||
|
|
||
| // 分页获取client数据 | ||
| const paginatedIds = clientList.slice(offset, offset + limit) | ||
| const clients: ClientData[] = [] | ||
|
|
||
| for (const clientId of paginatedIds) { | ||
| const clientData = await storage.getItem<ClientData>(`client:${clientId}`) | ||
| if (clientData) { | ||
| clients.push(clientData) | ||
| } | ||
| } | ||
|
|
||
| return { | ||
| clients, | ||
| total, | ||
| limit, | ||
| offset, | ||
| } | ||
| } | ||
| catch { | ||
| throw createError({ | ||
| statusCode: 500, | ||
| statusMessage: 'Failed to fetch clients', | ||
| }) | ||
| } | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The log ID generation using
Date.now()andMath.random()could potentially create collisions in high-concurrency scenarios. Consider using a more robust unique ID generator likerandomUUID()from Node.js crypto, which is already imported in other files.