-
Notifications
You must be signed in to change notification settings - Fork 8
feat: major codebase refactor and CI/CD improvement #416
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 |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| --- | ||
| description: Code style, formatting, naming conventions, imports, and type rules | ||
| globs: ["src/**/*.ts", "src/**/*.tsx"] | ||
| alwaysApply: true | ||
| --- | ||
|
|
||
| # Code Style Rules | ||
|
|
||
| ## Formatting (Prettier) | ||
|
|
||
| - Single quotes for strings | ||
| - No trailing commas | ||
| - 4-space indentation (`tabWidth: 4`) | ||
| - 200 character print width | ||
| - LF line endings | ||
| - Always use semicolons | ||
|
|
||
| ## Imports | ||
|
|
||
| ```typescript | ||
| // Node built-ins — use node: prefix | ||
| import { join } from 'node:path'; | ||
|
|
||
| // External packages | ||
| import axios from 'axios'; | ||
| import { Hono } from 'hono'; | ||
|
|
||
| // Internal modules — use @/ alias for src/ | ||
| import { namespace } from '@/types'; | ||
| import { SUCCESS_CODE } from '@/constant/code'; | ||
|
|
||
| // Relative imports — within same provider directory | ||
| import { namespace } from './namespace'; | ||
| ``` | ||
|
|
||
| - Import groups must be separated by blank lines: builtins → externals → internals | ||
| - Alphabetize imports within each group | ||
| - Use `import type` for type-only imports: `import type { Namespace } from '@/types'` | ||
|
|
||
| ## Naming Conventions | ||
|
|
||
| | Item | Convention | Example | | ||
| |---|---|---| | ||
| | Variables, functions, parameters | `camelCase` | `getLocalhostAddress`, `cacheKey` | | ||
| | Types, interfaces, classes | `PascalCase` | `HomeData`, `RouteItem`, `CMSResponse` | | ||
| | Constants | `UPPER_SNAKE_CASE` | `SUCCESS_CODE`, `BANNED_KEYWORDS` | | ||
| | Handler functions in routes | Always `handler` | `const handler = async (ctx) => { ... }` | | ||
| | Route files | Standard names | `namespace.ts`, `home.ts`, `category.ts`, `detail.ts`, `play.ts`, `search.ts` | | ||
|
|
||
| ## Type System | ||
|
|
||
| - Define shared types in `src/types/` | ||
| - Use `RouteItem<T>` generic for typed route definitions: | ||
| ```typescript | ||
| type HomeRoute = RouteItem<{ code: number; data: HomeData[] }>; | ||
| type SearchRoute = RouteItem<{ code: number; data: SearchData[] }>; | ||
| ``` | ||
| - Use `Namespace` type for provider metadata | ||
| - `noImplicitAny` is `false` — explicit types not enforced everywhere, but prefer them for public APIs | ||
| - Avoid using `any` — ESLint rule `@typescript-eslint/no-explicit-any` is set to `"error"` | ||
|
|
||
| ## Exports | ||
|
|
||
| - **Named exports only** — no default exports (except custom error classes) | ||
| - Each route file exports a `route` object conforming to the typed `Route` interface | ||
| - Each namespace file exports a `namespace` object conforming to `Namespace` | ||
| - Handler functions in utils use `export const handler` | ||
|
|
||
| ## General Rules | ||
|
|
||
| - No comments unless explicitly requested | ||
| - Use `const` over `let`; avoid `var` | ||
| - Prefer `async/await` over `.then()` chains | ||
| - The project uses ESM — use `import`/`export`, not `require()` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| --- | ||
| description: Error handling patterns, status codes, and logging rules | ||
| globs: ["src/**/*.ts", "src/**/*.tsx"] | ||
| alwaysApply: true | ||
| --- | ||
|
|
||
| # Error Handling Rules | ||
|
|
||
| ## Status Codes | ||
|
|
||
| Always use the constants from `@/constant/code`: | ||
|
|
||
| | Constant | Value | Meaning | | ||
| |---|---|---| | ||
| | `SUCCESS_CODE` | `0` | Successful response | | ||
| | `ERROR_CODE` | `-1` | Business logic error (upstream returned non-success) | | ||
| | `SYSTEM_ERROR_CODE` | `-2` | Exception/catch error | | ||
|
|
||
| ## Handler Error Pattern | ||
|
|
||
| Every handler must follow this structure. Never throw errors to the framework: | ||
|
|
||
| ```typescript | ||
| import { SUCCESS_CODE, ERROR_CODE, SYSTEM_ERROR_CODE } from '@/constant/code'; | ||
| import { SEARCH_MESSAGE } from '@/constant/message'; | ||
| import logger from '@/utils/logger'; | ||
|
|
||
| const handler = async (ctx) => { | ||
| try { | ||
| logger.info(`${SEARCH_MESSAGE.INFO} - ${namespace.name}`); | ||
|
|
||
| const res = await someRequest(/* ... */); | ||
|
|
||
| if (res.code === 1) { | ||
| // Success path | ||
| return { | ||
| code: SUCCESS_CODE, | ||
| message: SEARCH_MESSAGE.SUCCESS, | ||
| data: transformData(res) | ||
| }; | ||
| } | ||
|
|
||
| // Upstream returned non-success | ||
| logger.error(`${SEARCH_MESSAGE.ERROR} - ${namespace.name} - ${JSON.stringify(res)}`); | ||
| return { | ||
| code: ERROR_CODE, | ||
| message: SEARCH_MESSAGE.ERROR, | ||
| data: [] | ||
| }; | ||
| } catch (error) { | ||
| // Exception path — prevent caching failed responses | ||
| ctx.res.headers.set('Cache-Control', 'no-cache'); | ||
| logger.error(`${SEARCH_MESSAGE.ERROR} - ${namespace.name} - ${error}`); | ||
| return { | ||
| code: SYSTEM_ERROR_CODE, | ||
| message: SEARCH_MESSAGE.ERROR, | ||
| data: [] | ||
| }; | ||
| } | ||
| }; | ||
| ``` | ||
|
|
||
| ## Key Rules | ||
|
|
||
| - **Never throw** — always return a structured `{ code, message, data }` object | ||
| - **Set no-cache on errors** — `ctx.res.headers.set('Cache-Control', 'no-cache')` in catch blocks to prevent caching failed responses | ||
| - **Log every error** — use `logger.error()` including the namespace name for traceability | ||
| - **Empty data on failure** — always return `data: []` on error | ||
| - **Use message constants** — import from `@/constant/message` (e.g., `HOME_MESSAGE`, `SEARCH_MESSAGE`, `DETAIL_MESSAGE`) | ||
| - **Log info on entry** — `logger.info()` at the start of each handler with the action and namespace name | ||
|
|
||
| ## CMS vs Custom Error Handling | ||
|
|
||
| - **CMS handlers** (`utils/cms/*/index.ts`): Check `res.code === 1` for success (CMS convention) | ||
| - **Custom handlers**: Implement the same try/catch/return pattern inline in the route file | ||
|
|
||
| ## Logging | ||
|
|
||
| Use the logger from `@/utils/logger` (Winston-based). Available methods: | ||
| - `logger.info()` — normal operations | ||
| - `logger.error()` — all error cases | ||
| - Logger is configured in `src/config/index.ts` with file output and timestamps | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| --- | ||
| description: General project overview, architecture, and development commands for VodHub | ||
| globs: ["**/*.ts", "**/*.tsx"] | ||
| alwaysApply: true | ||
| --- | ||
|
|
||
| # VodHub General Rules | ||
|
|
||
| ## Project Overview | ||
|
|
||
| VodHub is a video aggregation API service built with **Hono** on Node.js. It normalizes multiple video source providers into a unified REST API supporting categories, search, details, and playback. TypeScript throughout, pnpm as package manager. | ||
|
|
||
| ## Tech Stack | ||
|
|
||
| - **Runtime**: Node.js >= 24, ESM (`"type": "module"` in package.json) | ||
| - **Framework**: Hono (web framework) with `@hono/node-server` | ||
| - **HTTP Client**: Axios | ||
| - **Caching**: cache-manager with in-memory LRU + optional Redis via Keyv | ||
| - **Logging**: Winston | ||
| - **Utilities**: lodash, dayjs, CryptoJS, query-string | ||
|
|
||
| ## Project Structure | ||
|
|
||
| ``` | ||
| src/ | ||
| index.ts # Server entry: boots @hono/node-server | ||
| app.tsx # Hono app: global middleware + route mounting | ||
| api/ # OpenAPI metadata routes | ||
| config/ # App config (port, cache TTL, banned keywords) | ||
| constant/ # Status codes, messages, user-agents, word lists | ||
| middleware/ # Cache middleware, JSON response middleware | ||
| routes/ # Provider route directories (auto-discovered) | ||
| registry.ts # Dynamic route loader using directory-import | ||
| types/ # Core types (Namespace, Route, HomeData, etc.) | ||
| utils/ # Shared CMS handlers, cache, logger, filters | ||
| ``` | ||
|
|
||
| ## Development Commands | ||
|
|
||
| ```bash | ||
| pnpm install # Install dependencies | ||
| pnpm dev # Start dev server with hot reload (tsx watch) | ||
| pnpm start # Start server without watch | ||
| ``` | ||
|
|
||
| No test framework is configured. Linting and type checking: | ||
|
|
||
| ```bash | ||
| npx eslint src/ --ext .ts,.tsx # Lint | ||
| npx eslint src/ --ext .ts,.tsx --fix # Lint with auto-fix | ||
| npx prettier --cache --write "src/**/*.{ts,tsx}" # Format | ||
| npx tsc --noEmit # Type check | ||
| ``` | ||
|
|
||
| Commits use **commitizen** with conventional commits. Run `pnpm commit` for the interactive prompt. | ||
|
|
||
| ## Route URL Pattern | ||
|
|
||
| All routes follow: `GET/POST /api/vodhub/<provider>/<action>` | ||
|
|
||
| Actions per provider: `home`, `homeVod`, `category`, `detail`, `play`, `search` | ||
|
|
||
| ## Global Middleware Order | ||
|
|
||
| 1. `cors()` - CORS handling | ||
| 2. `trimTrailingSlash()` - Trailing slash normalization | ||
| 3. `compress()` - Response compression | ||
| 4. `jsonReturn` - JSON response serialization | ||
| 5. `cache` - Redis/memory caching with deduplication |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| --- | ||
| description: Rules for creating and modifying provider routes in VodHub | ||
| globs: ["src/routes/**/*.ts"] | ||
| alwaysApply: false | ||
| --- | ||
|
|
||
| # Provider Route Rules | ||
|
|
||
| ## Creating a New CMS Provider (Recommended) | ||
|
|
||
| For standard CMS sources, only `namespace.ts` and `index.ts` are needed: | ||
|
|
||
| 1. Create `src/routes/<provider-name>/` directory | ||
| 2. Add `namespace.ts` with provider metadata | ||
| 3. Add `index.ts` that uses the factory: | ||
| ```typescript | ||
| import { namespace } from './namespace'; | ||
| import { createCMSRoutes } from '@/utils/cms/factory'; | ||
| export const routes = createCMSRoutes(namespace); | ||
| ``` | ||
| 4. Routes auto-register via `directory-import` in `registry.ts` | ||
|
|
||
| ## namespace.ts Pattern | ||
|
|
||
| ```typescript | ||
| import type { Namespace } from '@/types'; | ||
|
|
||
| export const namespace: Namespace = { | ||
| name: 'Provider Name', | ||
| url: 'https://example.com', | ||
| description: 'Provider description' | ||
| }; | ||
| ``` | ||
|
|
||
| ## Creating a Custom Provider (e.g., 360kan) | ||
|
|
||
| For non-CMS sources, create individual route files. Each file exports a single `route` object: | ||
|
|
||
| ``` | ||
| src/routes/<provider>/ | ||
| namespace.ts | ||
| home.ts | ||
| homeVod.ts | ||
| category.ts | ||
| detail.ts | ||
| play.ts | ||
| search.ts | ||
| ``` | ||
|
|
||
| Route file pattern: | ||
|
|
||
| ```typescript | ||
| import type { Context } from 'hono'; | ||
| import { namespace } from './namespace'; | ||
| import { SUCCESS_CODE, SYSTEM_ERROR_CODE } from '@/constant/code'; | ||
| import { HOME_MESSAGE } from '@/constant/message'; | ||
| import type { HomeRoute, HomeData } from '@/types'; | ||
| import { filterHomeData } from '@/utils/filters'; | ||
| import logger from '@/utils/logger'; | ||
|
|
||
| const handler = async (ctx: Context) => { | ||
| try { | ||
| logger.info(`${HOME_MESSAGE.INFO} - ${namespace.name}`); | ||
| // ... custom logic to fetch and transform data | ||
| return { | ||
| code: SUCCESS_CODE, | ||
| message: HOME_MESSAGE.SUCCESS, | ||
| data: filterHomeData(newList) | ||
| }; | ||
| } catch (error) { | ||
| ctx.res.headers.set('Cache-Control', 'no-cache'); | ||
| logger.error(`${HOME_MESSAGE.ERROR} - ${namespace.name} - ${error instanceof Error ? error.message : String(error)}`); | ||
| return { | ||
| code: SYSTEM_ERROR_CODE, | ||
| message: HOME_MESSAGE.ERROR, | ||
| data: [] | ||
| }; | ||
| } | ||
| }; | ||
|
|
||
| export const route: HomeRoute = { | ||
| path: '/home', | ||
| name: 'home', | ||
| example: '/360kan/home', | ||
| description: '首页分类列表', | ||
| handler | ||
| }; | ||
| ``` | ||
|
|
||
| ## Route Object Shape | ||
|
|
||
| Every route must conform to its typed `RouteItem<T>`: | ||
| - `path`: string or string[] — URL path segment(s) | ||
| - `name`: string — route identifier (matches filename) | ||
| - `example`: string — example URL path | ||
| - `description`: string — what the route does | ||
| - `handler`: (ctx: Context) => Promise<T> | T | ||
| - `method?`: 'GET' | 'POST' — defaults to GET | ||
|
|
||
| ## Registry Auto-Discovery | ||
|
|
||
| The registry (`src/routes/registry.ts`) auto-discovers routes via `directory-import`: | ||
| - `namespace` export → provider metadata | ||
| - `routes` export → array of route objects (from factory) | ||
| - `route` export → single route object (custom provider) |
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,18 +6,18 @@ | |||||||||||||||||||||
|
|
||||||||||||||||||||||
| version: 2 | ||||||||||||||||||||||
| updates: | ||||||||||||||||||||||
| - package-ecosystem: "devcontainers" | ||||||||||||||||||||||
| directory: "/" | ||||||||||||||||||||||
| schedule: | ||||||||||||||||||||||
| interval: weekly | ||||||||||||||||||||||
| - package-ecosystem: 'devcontainers' | ||||||||||||||||||||||
| directory: '/' | ||||||||||||||||||||||
| schedule: | ||||||||||||||||||||||
| interval: weekly | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| - package-ecosystem: "npm" | ||||||||||||||||||||||
| directory: "/" | ||||||||||||||||||||||
| schedule: | ||||||||||||||||||||||
| interval: weekly | ||||||||||||||||||||||
| day: friday | ||||||||||||||||||||||
| time: "12:00" | ||||||||||||||||||||||
| timezone: Asia/Shanghai | ||||||||||||||||||||||
| open-pull-requests-limit: 100 | ||||||||||||||||||||||
| labels: | ||||||||||||||||||||||
| - "dependencies" | ||||||||||||||||||||||
| - package-ecosystem: 'npm' | ||||||||||||||||||||||
| directory: '/' | ||||||||||||||||||||||
| schedule: | ||||||||||||||||||||||
| interval: monthly | ||||||||||||||||||||||
| day: friday | ||||||||||||||||||||||
| time: '12:00' | ||||||||||||||||||||||
| timezone: Asia/Singapore | ||||||||||||||||||||||
| open-pull-requests-limit: 10 | ||||||||||||||||||||||
|
Comment on lines
+17
to
+21
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
对应用依赖建议至少保持 建议修复- interval: monthly
+ interval: weekly📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| labels: | ||||||||||||||||||||||
| - 'dependencies' | ||||||||||||||||||||||
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.
文档示例与实际实现不一致
示例代码中使用
${error}直接插值,但实际代码实现(如category/index.ts、detail/index.ts等)使用的是更安全的模式:${error instanceof Error ? error.message : String(error)}。建议更新文档以反映实际的最佳实践。
📝 建议修复
} catch (error) { // Exception path — prevent caching failed responses ctx.res.headers.set('Cache-Control', 'no-cache'); - logger.error(`${SEARCH_MESSAGE.ERROR} - ${namespace.name} - ${error}`); + logger.error(`${SEARCH_MESSAGE.ERROR} - ${namespace.name} - ${error instanceof Error ? error.message : String(error)}`); return { code: SYSTEM_ERROR_CODE, message: SEARCH_MESSAGE.ERROR, data: [] }; }🤖 Prompt for AI Agents