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>
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@muse/shared",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"vitest": "^4.0.18",
|
||||
"@vitest/coverage-v8": "^4.0.18"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export type {
|
||||
Platform,
|
||||
ContentItem,
|
||||
PlatformConfig,
|
||||
PlatformAdapter,
|
||||
} from "./types/content";
|
||||
|
||||
export { MVP_PLATFORMS, getPlatformConfig } from "./platforms";
|
||||
@@ -0,0 +1,60 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { MVP_PLATFORMS, getPlatformConfig } from "./platforms";
|
||||
|
||||
describe("MVP_PLATFORMS", () => {
|
||||
it("contains exactly 8 platforms", () => {
|
||||
expect(MVP_PLATFORMS).toHaveLength(8);
|
||||
});
|
||||
|
||||
it("includes all 8 platforms", () => {
|
||||
const ids = MVP_PLATFORMS.map((p) => p.id);
|
||||
expect(ids).toContain("douyin");
|
||||
expect(ids).toContain("tiktok");
|
||||
expect(ids).toContain("xiaohongshu");
|
||||
expect(ids).toContain("youtube");
|
||||
expect(ids).toContain("instagram");
|
||||
expect(ids).toContain("twitter");
|
||||
expect(ids).toContain("bilibili");
|
||||
expect(ids).toContain("weibo");
|
||||
});
|
||||
|
||||
it("all platforms are enabled by default", () => {
|
||||
MVP_PLATFORMS.forEach((p) => {
|
||||
expect(p.enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("each platform has required fields", () => {
|
||||
MVP_PLATFORMS.forEach((p) => {
|
||||
expect(p.id).toBeTruthy();
|
||||
expect(p.name).toBeTruthy();
|
||||
expect(p.icon).toBeTruthy();
|
||||
expect(p.color).toBeTruthy();
|
||||
expect(p.endpoints.trending).toBeTruthy();
|
||||
expect(p.endpoints.detail).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPlatformConfig", () => {
|
||||
it("returns config for known platform", () => {
|
||||
const config = getPlatformConfig("douyin");
|
||||
expect(config).toBeDefined();
|
||||
expect(config!.id).toBe("douyin");
|
||||
expect(config!.name).toBe("抖音");
|
||||
});
|
||||
|
||||
it("returns undefined for unknown platform", () => {
|
||||
expect(getPlatformConfig("unknown")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns correct config for each platform", () => {
|
||||
expect(getPlatformConfig("tiktok")?.name).toBe("TikTok");
|
||||
expect(getPlatformConfig("xiaohongshu")?.name).toBe("小红书");
|
||||
expect(getPlatformConfig("youtube")?.name).toBe("YouTube");
|
||||
expect(getPlatformConfig("instagram")?.name).toBe("Instagram");
|
||||
expect(getPlatformConfig("twitter")?.name).toBe("Twitter/X");
|
||||
expect(getPlatformConfig("bilibili")?.name).toBe("哔哩哔哩");
|
||||
expect(getPlatformConfig("weibo")?.name).toBe("微博");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { PlatformConfig } from "./types/content";
|
||||
|
||||
export const MVP_PLATFORMS: PlatformConfig[] = [
|
||||
{
|
||||
id: "douyin",
|
||||
name: "抖音",
|
||||
icon: "📱",
|
||||
color: "#000000",
|
||||
enabled: true,
|
||||
endpoints: {
|
||||
trending: "/api/v1/douyin/web/fetch_hot_search_result",
|
||||
detail: "/api/v1/douyin/web/fetch_one_video",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "tiktok",
|
||||
name: "TikTok",
|
||||
icon: "🎵",
|
||||
color: "#00F2EA",
|
||||
enabled: true,
|
||||
endpoints: {
|
||||
trending: "/api/v1/tiktok/web/fetch_trending_post",
|
||||
detail: "/api/v1/tiktok/web/fetch_post_detail",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "xiaohongshu",
|
||||
name: "小红书",
|
||||
icon: "📕",
|
||||
color: "#FF2442",
|
||||
enabled: true,
|
||||
endpoints: {
|
||||
trending: "/api/v1/xiaohongshu/app/v2/fetch_feed",
|
||||
detail: "/api/v1/xiaohongshu/app/v2/fetch_note_detail",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "youtube",
|
||||
name: "YouTube",
|
||||
icon: "▶️",
|
||||
color: "#FF0000",
|
||||
enabled: true,
|
||||
endpoints: {
|
||||
trending: "/api/v1/youtube/web/get_trending_videos",
|
||||
detail: "/api/v1/youtube/web/get_video_info",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "instagram",
|
||||
name: "Instagram",
|
||||
icon: "📷",
|
||||
color: "#E4405F",
|
||||
enabled: true,
|
||||
endpoints: {
|
||||
trending: "/api/v1/instagram/v1/fetch_explore_sections",
|
||||
detail: "/api/v1/instagram/v2/fetch_post_info",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "twitter",
|
||||
name: "Twitter/X",
|
||||
icon: "𝕏",
|
||||
color: "#000000",
|
||||
enabled: true,
|
||||
endpoints: {
|
||||
trending: "/api/v1/twitter/web/fetch_trending",
|
||||
detail: "/api/v1/twitter/web/fetch_tweet_detail",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "bilibili",
|
||||
name: "哔哩哔哩",
|
||||
icon: "📺",
|
||||
color: "#00A1D6",
|
||||
enabled: true,
|
||||
endpoints: {
|
||||
trending: "/api/v1/bilibili/web/fetch_com_popular",
|
||||
detail: "/api/v1/bilibili/web/fetch_video_detail",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "weibo",
|
||||
name: "微博",
|
||||
icon: "🔥",
|
||||
color: "#FF8200",
|
||||
enabled: true,
|
||||
endpoints: {
|
||||
trending: "/api/v1/weibo/app/fetch_hot_search",
|
||||
detail: "/api/v1/weibo/app/fetch_status_detail",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function getPlatformConfig(platformId: string): PlatformConfig | undefined {
|
||||
return MVP_PLATFORMS.find((p) => p.id === platformId);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
export type Platform =
|
||||
| "douyin"
|
||||
| "tiktok"
|
||||
| "xiaohongshu"
|
||||
| "youtube"
|
||||
| "instagram"
|
||||
| "twitter"
|
||||
| "bilibili"
|
||||
| "weibo";
|
||||
|
||||
export interface ContentItem {
|
||||
id: string;
|
||||
title: string;
|
||||
cover_url?: string;
|
||||
video_url?: string;
|
||||
author_name: string;
|
||||
author_avatar?: string;
|
||||
play_count?: number;
|
||||
like_count?: number;
|
||||
collect_count?: number;
|
||||
comment_count?: number;
|
||||
share_count?: number;
|
||||
publish_time: string;
|
||||
platform: Platform;
|
||||
original_url: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface PlatformConfig {
|
||||
id: Platform;
|
||||
name: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
enabled: boolean;
|
||||
endpoints: {
|
||||
trending: string;
|
||||
detail: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PlatformAdapter {
|
||||
fetchTrending(count: number): Promise<ContentItem[]>;
|
||||
fetchDetail(id: string): Promise<ContentItem>;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
include: ["src/**/*.test.ts"],
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["text", "text-summary", "lcov"],
|
||||
include: ["src/**"],
|
||||
exclude: ["src/**/*.test.*"],
|
||||
thresholds: {
|
||||
branches: 80,
|
||||
functions: 80,
|
||||
lines: 80,
|
||||
statements: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user