- 移除管理员凭证存储逻辑,改用 Cookie 管理会话 - 清理 admin-auth 中的存储函数,使用 Cookie 进行会话管理 - 更新 apiClient 请求逻辑,支持管理员 Cookie - 修改 auth store,调整登录和验证逻辑 - 添加 Cookie 处理函数,确保安全性和兼容性 - 更新测试脚本,验证 Cookie 功能
229 lines
8.9 KiB
TypeScript
229 lines
8.9 KiB
TypeScript
/**
|
|
* Mock upstream server that simulates OpenAI / Azure OpenAI / OpenAI Responses APIs.
|
|
* Usage: npx tsx tests/mock-upstream.ts
|
|
* Listens on port 9999.
|
|
*/
|
|
|
|
const PORT = 9999
|
|
const INSPECTABLE_REQUEST_KEYS = ["chat-completions", "messages", "models", "responses"] as const
|
|
|
|
type InspectableRequestKey = typeof INSPECTABLE_REQUEST_KEYS[number]
|
|
|
|
type CapturedRequest = {
|
|
body: unknown
|
|
headers: Record<string, string>
|
|
method: string
|
|
path: string
|
|
}
|
|
|
|
const capturedRequests = new Map<InspectableRequestKey, CapturedRequest>()
|
|
|
|
const toHeaderRecord = (headers: Headers): Record<string, string> => {
|
|
return Object.fromEntries(Array.from(headers.entries()).map(([key, value]) => [key.toLowerCase(), value]))
|
|
}
|
|
|
|
const getCaptureKey = (path: string): InspectableRequestKey | null => {
|
|
if (path.endsWith("/chat/completions")) {
|
|
return "chat-completions"
|
|
}
|
|
|
|
if (path.endsWith("/messages")) {
|
|
return "messages"
|
|
}
|
|
|
|
if (path.endsWith("/responses")) {
|
|
return "responses"
|
|
}
|
|
|
|
if (path.endsWith("/models")) {
|
|
return "models"
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
const rememberRequest = (req: Request, path: string, body: unknown) => {
|
|
const captureKey = getCaptureKey(path)
|
|
if (!captureKey) {
|
|
return
|
|
}
|
|
|
|
capturedRequests.set(captureKey, {
|
|
body,
|
|
headers: toHeaderRecord(req.headers),
|
|
method: req.method,
|
|
path,
|
|
})
|
|
}
|
|
|
|
// --- OpenAI Chat Completions (non-stream) ---
|
|
const openaiChatResponse = (model: string) => ({
|
|
id: "chatcmpl-mock-001",
|
|
object: "chat.completion",
|
|
created: 1700000000,
|
|
model,
|
|
choices: [{
|
|
index: 0,
|
|
message: { role: "assistant", content: "Mock response from " + model },
|
|
finish_reason: "stop",
|
|
}],
|
|
usage: { prompt_tokens: 10, completion_tokens: 8, total_tokens: 18 },
|
|
})
|
|
|
|
// --- OpenAI Chat Completions (stream) ---
|
|
const openaiChatStreamChunks = (model: string) => [
|
|
`data: ${JSON.stringify({ id: "chatcmpl-mock-001", object: "chat.completion.chunk", model, choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }], usage: null })}\n\n`,
|
|
`data: ${JSON.stringify({ id: "chatcmpl-mock-001", object: "chat.completion.chunk", model, choices: [{ index: 0, delta: { content: "Mock " }, finish_reason: null }], usage: null })}\n\n`,
|
|
`data: ${JSON.stringify({ id: "chatcmpl-mock-001", object: "chat.completion.chunk", model, choices: [{ index: 0, delta: { content: "stream " }, finish_reason: null }], usage: null })}\n\n`,
|
|
`data: ${JSON.stringify({ id: "chatcmpl-mock-001", object: "chat.completion.chunk", model, choices: [{ index: 0, delta: { content: "response" }, finish_reason: null }], usage: null })}\n\n`,
|
|
`data: ${JSON.stringify({ id: "chatcmpl-mock-001", object: "chat.completion.chunk", model, choices: [{ index: 0, delta: {}, finish_reason: "stop" }], usage: { prompt_tokens: 10, completion_tokens: 3, total_tokens: 13 } })}\n\n`,
|
|
`data: [DONE]\n\n`,
|
|
]
|
|
|
|
// --- OpenAI Responses (non-stream) ---
|
|
const openaiResponsesResponse = (model: string) => ({
|
|
id: "resp-mock-001",
|
|
object: "response",
|
|
created_at: 1700000000,
|
|
status: "completed",
|
|
model,
|
|
output: [{
|
|
id: "msg-mock-001",
|
|
type: "message",
|
|
status: "completed",
|
|
content: [{ type: "output_text", text: "Mock responses output" }],
|
|
role: "assistant",
|
|
}],
|
|
usage: { input_tokens: 8, output_tokens: 5, total_tokens: 13 },
|
|
})
|
|
|
|
// --- OpenAI Responses (stream) ---
|
|
const openaiResponsesStreamChunks = (model: string) => {
|
|
const respId = "resp-mock-001"
|
|
const msgId = "msg-mock-001"
|
|
return [
|
|
`event: response.created\ndata: ${JSON.stringify({ type: "response.created", response: { id: respId, object: "response", model, status: "in_progress", output: [], usage: null } })}\n\n`,
|
|
`event: response.output_item.added\ndata: ${JSON.stringify({ type: "response.output_item.added", item: { id: msgId, type: "message", status: "in_progress", content: [], role: "assistant" }, output_index: 0 })}\n\n`,
|
|
`event: response.output_text.delta\ndata: ${JSON.stringify({ type: "response.output_text.delta", delta: "Mock ", item_id: msgId, output_index: 0, content_index: 0 })}\n\n`,
|
|
`event: response.output_text.delta\ndata: ${JSON.stringify({ type: "response.output_text.delta", delta: "stream", item_id: msgId, output_index: 0, content_index: 0 })}\n\n`,
|
|
`event: response.completed\ndata: ${JSON.stringify({ type: "response.completed", response: { id: respId, object: "response", model, status: "completed", output: [{ id: msgId, type: "message", status: "completed", content: [{ type: "output_text", text: "Mock stream" }], role: "assistant" }], usage: { input_tokens: 8, output_tokens: 4, total_tokens: 12 } } })}\n\n`,
|
|
]
|
|
}
|
|
|
|
// --- Claude Messages (non-stream) ---
|
|
const claudeMessagesResponse = (model: string) => ({
|
|
id: "msg-mock-claude-001",
|
|
type: "message",
|
|
role: "assistant",
|
|
model,
|
|
content: [{ type: "text", text: "Mock Claude response" }],
|
|
stop_reason: "end_turn",
|
|
stop_sequence: null,
|
|
usage: { input_tokens: 12, output_tokens: 6 },
|
|
})
|
|
|
|
// --- Claude Messages (stream) ---
|
|
const claudeMessagesStreamChunks = (model: string) => [
|
|
`event: message_start\ndata: ${JSON.stringify({ type: "message_start", message: { id: "msg-mock-claude-001", type: "message", role: "assistant", model, content: [], stop_reason: null, usage: { input_tokens: 12, output_tokens: 0 } } })}\n\n`,
|
|
`event: content_block_start\ndata: ${JSON.stringify({ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } })}\n\n`,
|
|
`event: content_block_delta\ndata: ${JSON.stringify({ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Mock Claude stream" } })}\n\n`,
|
|
`event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: 0 })}\n\n`,
|
|
`event: message_delta\ndata: ${JSON.stringify({ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 4 } })}\n\n`,
|
|
`event: message_stop\ndata: ${JSON.stringify({ type: "message_stop" })}\n\n`,
|
|
]
|
|
|
|
const server = Bun.serve({
|
|
port: PORT,
|
|
async fetch(req) {
|
|
const url = new URL(req.url)
|
|
const path = url.pathname
|
|
const inspectKey = url.searchParams.get("key")
|
|
|
|
if (path === "/__reset" && req.method === "POST") {
|
|
capturedRequests.clear()
|
|
return Response.json({ ok: true })
|
|
}
|
|
|
|
if (path === "/__inspect" && req.method === "GET") {
|
|
if (!inspectKey || !INSPECTABLE_REQUEST_KEYS.includes(inspectKey as InspectableRequestKey)) {
|
|
return Response.json({
|
|
keys: INSPECTABLE_REQUEST_KEYS,
|
|
}, { status: 400 })
|
|
}
|
|
|
|
return Response.json(capturedRequests.get(inspectKey as InspectableRequestKey) || null)
|
|
}
|
|
|
|
const body = req.method === "POST" ? await req.json().catch(() => ({})) : {}
|
|
const model = (body as any).model || "mock-model"
|
|
const isStream = (body as any).stream === true
|
|
|
|
console.log(`[MOCK] ${req.method} ${path} model=${model} stream=${isStream}`)
|
|
rememberRequest(req, path, body)
|
|
|
|
// OpenAI Chat Completions
|
|
if (path.endsWith("/chat/completions")) {
|
|
if (isStream) {
|
|
const chunks = openaiChatStreamChunks(model)
|
|
const stream = new ReadableStream({
|
|
async start(controller) {
|
|
for (const chunk of chunks) {
|
|
controller.enqueue(new TextEncoder().encode(chunk))
|
|
await new Promise(r => setTimeout(r, 50))
|
|
}
|
|
controller.close()
|
|
}
|
|
})
|
|
return new Response(stream, {
|
|
headers: { "content-type": "text/event-stream", "cache-control": "no-cache" },
|
|
})
|
|
}
|
|
return Response.json(openaiChatResponse(model))
|
|
}
|
|
|
|
// OpenAI Responses API
|
|
if (path.endsWith("/responses")) {
|
|
if (isStream) {
|
|
const chunks = openaiResponsesStreamChunks(model)
|
|
const stream = new ReadableStream({
|
|
async start(controller) {
|
|
for (const chunk of chunks) {
|
|
controller.enqueue(new TextEncoder().encode(chunk))
|
|
await new Promise(r => setTimeout(r, 50))
|
|
}
|
|
controller.close()
|
|
}
|
|
})
|
|
return new Response(stream, {
|
|
headers: { "content-type": "text/event-stream", "cache-control": "no-cache" },
|
|
})
|
|
}
|
|
return Response.json(openaiResponsesResponse(model))
|
|
}
|
|
|
|
// Claude Messages API
|
|
if (path.endsWith("/messages")) {
|
|
if (isStream) {
|
|
const chunks = claudeMessagesStreamChunks(model)
|
|
const stream = new ReadableStream({
|
|
async start(controller) {
|
|
for (const chunk of chunks) {
|
|
controller.enqueue(new TextEncoder().encode(chunk))
|
|
await new Promise(r => setTimeout(r, 50))
|
|
}
|
|
controller.close()
|
|
}
|
|
})
|
|
return new Response(stream, {
|
|
headers: { "content-type": "text/event-stream", "cache-control": "no-cache" },
|
|
})
|
|
}
|
|
return Response.json(claudeMessagesResponse(model))
|
|
}
|
|
|
|
return new Response("Mock: unknown route " + path, { status: 404 })
|
|
},
|
|
})
|
|
|
|
console.log(`[MOCK] Upstream mock server running on http://localhost:${PORT}`)
|