mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-18 20:19:09 +08:00
feat: 完成本地 API 中心与 Reader Skill 流程
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
import http from 'http'
|
||||
import { apiServer } from '../http-server'
|
||||
import {
|
||||
LOCAL_API_ENDPOINTS,
|
||||
type LocalApiEndpointId,
|
||||
type LocalApiTestRequest,
|
||||
type LocalApiTestResponse
|
||||
} from '../../shared/local-api-test'
|
||||
|
||||
const REQUEST_TIMEOUT_MS = 10_000
|
||||
const MAX_BODY_SIZE = 512 * 1024
|
||||
|
||||
function isEndpointId(value: unknown): value is LocalApiEndpointId {
|
||||
return typeof value === 'string' && value in LOCAL_API_ENDPOINTS
|
||||
}
|
||||
|
||||
function requestHost(host: string): string {
|
||||
if (host === '0.0.0.0') return '127.0.0.1'
|
||||
if (host === '::') return '::1'
|
||||
return host
|
||||
}
|
||||
|
||||
function invalidResponse(message: string): LocalApiTestResponse {
|
||||
return {
|
||||
ok: false,
|
||||
method: 'GET',
|
||||
path: '',
|
||||
url: '',
|
||||
durationMs: 0,
|
||||
responseSize: 0,
|
||||
errorCode: 'INVALID_REQUEST',
|
||||
error: message
|
||||
}
|
||||
}
|
||||
|
||||
function parseBody(bodyText: string, contentType?: string): { json?: unknown; bodyText?: string } {
|
||||
if (contentType?.includes('application/json')) {
|
||||
try {
|
||||
return { json: JSON.parse(bodyText) }
|
||||
} catch {
|
||||
// Keep malformed responses readable to the tester.
|
||||
}
|
||||
}
|
||||
return { bodyText }
|
||||
}
|
||||
|
||||
export async function testLocalApiRequest(payload: unknown): Promise<LocalApiTestResponse> {
|
||||
if (!payload || typeof payload !== 'object') return invalidResponse('请求格式无效')
|
||||
const { endpointId, query = {}, body = '' } = payload as Partial<LocalApiTestRequest>
|
||||
if (!isEndpointId(endpointId)) return invalidResponse('不允许访问该 API 端点')
|
||||
if (!query || typeof query !== 'object' || Array.isArray(query))
|
||||
return invalidResponse('查询参数格式无效')
|
||||
if (typeof body !== 'string' || Buffer.byteLength(body) > MAX_BODY_SIZE)
|
||||
return invalidResponse('请求体格式无效')
|
||||
|
||||
const endpoint = LOCAL_API_ENDPOINTS[endpointId]
|
||||
const entries = Object.entries(query)
|
||||
if (
|
||||
entries.some(
|
||||
([key, value]) => !endpoint.queryKeys.includes(key as never) || typeof value !== 'string'
|
||||
)
|
||||
) {
|
||||
return invalidResponse('查询参数不属于当前端点')
|
||||
}
|
||||
|
||||
const service = apiServer.getState()
|
||||
const targetHost = requestHost(service.host)
|
||||
const targetPort = service.port
|
||||
const hostPart = targetHost.includes(':') ? `[${targetHost}]` : targetHost
|
||||
const url = new URL(endpoint.path, `http://${hostPart}:${targetPort}`)
|
||||
entries.forEach(([key, value]) => {
|
||||
if (value.trim()) url.searchParams.set(key, value.trim())
|
||||
})
|
||||
|
||||
if (!service.running) {
|
||||
return {
|
||||
ok: false,
|
||||
method: endpoint.method,
|
||||
path: endpoint.path,
|
||||
url: url.toString(),
|
||||
durationMs: 0,
|
||||
responseSize: 0,
|
||||
errorCode: 'API_NOT_RUNNING',
|
||||
error: '请先启动本地 API 服务'
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const startedAt = Date.now()
|
||||
let settled = false
|
||||
const finish = (result: LocalApiTestResponse): void => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
resolve(result)
|
||||
}
|
||||
const request = http.request(
|
||||
url,
|
||||
{
|
||||
method: endpoint.method,
|
||||
headers: endpoint.method === 'POST' ? { 'Content-Type': 'application/json' } : undefined
|
||||
},
|
||||
(response) => {
|
||||
const chunks: Buffer[] = []
|
||||
response.on('data', (chunk: Buffer) => chunks.push(chunk))
|
||||
response.on('end', () => {
|
||||
const bodyText = Buffer.concat(chunks).toString('utf-8')
|
||||
const contentType = Array.isArray(response.headers['content-type'])
|
||||
? response.headers['content-type'][0]
|
||||
: response.headers['content-type']
|
||||
finish({
|
||||
ok: true,
|
||||
method: endpoint.method,
|
||||
path: endpoint.path,
|
||||
url: url.toString(),
|
||||
status: response.statusCode || 0,
|
||||
statusText: response.statusMessage || '',
|
||||
durationMs: Date.now() - startedAt,
|
||||
responseSize: Buffer.byteLength(bodyText),
|
||||
contentType,
|
||||
...parseBody(bodyText, contentType)
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
request.setTimeout(REQUEST_TIMEOUT_MS, () => {
|
||||
request.destroy(new Error('请求超时'))
|
||||
finish({
|
||||
ok: false,
|
||||
method: endpoint.method,
|
||||
path: endpoint.path,
|
||||
url: url.toString(),
|
||||
durationMs: Date.now() - startedAt,
|
||||
responseSize: 0,
|
||||
errorCode: 'TIMEOUT',
|
||||
error: '请求超时(10 秒)'
|
||||
})
|
||||
})
|
||||
request.on('error', (error: NodeJS.ErrnoException) => {
|
||||
finish({
|
||||
ok: false,
|
||||
method: endpoint.method,
|
||||
path: endpoint.path,
|
||||
url: url.toString(),
|
||||
durationMs: Date.now() - startedAt,
|
||||
responseSize: 0,
|
||||
errorCode: error.code === 'ECONNREFUSED' ? 'CONNECTION_REFUSED' : 'UNKNOWN',
|
||||
error: error.message
|
||||
})
|
||||
})
|
||||
if (endpoint.method === 'POST') request.write(body)
|
||||
request.end()
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user