wxs 6cc703ada2 feat: monorepo 重构 + 新增 5 个平台适配器
项目从单体结构重构为 pnpm monorepo (shared/backend/frontend),
新增 YouTube、Instagram、Twitter/X、哔哩哔哩、微博 5 个平台适配器,
包含完整的单元测试和 E2E 测试覆盖。

- 完成 T-031~T-044: 5 个适配器实现、注册、配置和测试
- 重构前后端分离: Hono 后端 + Next.js 前端
- 151 个单元测试 + 21 个 Mock E2E + 25 个真实 E2E
- 适配器基于真实 TikHub API 响应结构实现

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:43:25 +08:00

72 lines
1.8 KiB
TypeScript

import { waitForSlot } from "./rate-limiter";
const TIKHUB_BASE_URL = "https://api.tikhub.io";
// Runtime API Key (set via POST /api/settings)
let runtimeApiKey: string | null = null;
export function setRuntimeApiKey(key: string) {
runtimeApiKey = key;
}
export function getApiKey(): string | null {
return runtimeApiKey || process.env.TIKHUB_API_KEY || null;
}
export async function tikhubFetch<T>(
endpoint: string,
params?: Record<string, string>,
method: "GET" | "POST" = "GET",
body?: Record<string, unknown>
): Promise<T> {
const apiKey = getApiKey();
if (!apiKey) {
throw new TikHubError(401, "API Key 未配置,请在设置页面配置 TikHub API Key");
}
await waitForSlot();
const url = new URL(endpoint, TIKHUB_BASE_URL);
if (params) {
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
}
const res = await fetch(url.toString(), {
method,
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
...(method === "POST" ? { body: JSON.stringify(body || {}) } : {}),
});
if (!res.ok) {
if (res.status === 401) {
throw new TikHubError(401, "API Key 无效,请检查配置");
}
if (res.status === 429) {
throw new TikHubError(429, "请求过于频繁,请稍后重试");
}
throw new TikHubError(res.status, `TikHub API 错误: ${res.status}`);
}
const json = await res.json();
// TikHub wraps all responses in { code, data, ... } envelope
// Unwrap to return the inner data directly
if (json?.code !== undefined && json?.data !== undefined) {
return json.data as T;
}
return json as T;
}
export class TikHubError extends Error {
constructor(
public statusCode: number,
message: string
) {
super(message);
this.name = "TikHubError";
}
}