Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .cursor/rules/code-style.mdc
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()`
82 changes: 82 additions & 0 deletions .cursor/rules/error-handling.mdc
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: []
};
}
};
Comment on lines +50 to +60

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

文档示例与实际实现不一致

示例代码中使用 ${error} 直接插值,但实际代码实现(如 category/index.tsdetail/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
Verify each finding against the current code and only fix it if needed.

In @.cursor/rules/error-handling.mdc around lines 50 - 60, The catch block logs
the error by interpolating `${error}` which is inconsistent with other modules;
change the logger.error call in this catch (and the returned message handling)
to safely extract the error message using a conditional like `error instanceof
Error ? error.message : String(error)` when logging and when forming any
user-facing message (refer to ctx.res.headers.set, logger.error,
SEARCH_MESSAGE.ERROR, and SYSTEM_ERROR_CODE to locate the catch block and return
object); ensure the log includes the descriptive context (`SEARCH_MESSAGE.ERROR
- ${namespace.name}`) combined with the safe error string.

```

## 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
69 changes: 69 additions & 0 deletions .cursor/rules/general.mdc
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
105 changes: 105 additions & 0 deletions .cursor/rules/routes.mdc
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)
27 changes: 0 additions & 27 deletions .eslintrc.cjs

This file was deleted.

28 changes: 14 additions & 14 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

npm 改为月更会显著拉长漏洞暴露窗口。

对应用依赖建议至少保持 weekly,否则高危修复可能延迟数周。

建议修复
-          interval: monthly
+          interval: weekly
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interval: monthly
day: friday
time: '12:00'
timezone: Asia/Singapore
open-pull-requests-limit: 10
interval: weekly
day: friday
time: '12:00'
timezone: Asia/Singapore
open-pull-requests-limit: 10
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/dependabot.yml around lines 17 - 21, The Dependabot config currently
sets the update interval to monthly which lengthens vulnerability exposure;
locate the npm package-ecosystem block (look for package-ecosystem: "npm" and
the interval field) and change interval: monthly to interval: weekly so npm
dependency updates run weekly; keep existing day/time/timezone and
open-pull-requests-limit values unchanged unless you want different cadence.

labels:
- 'dependencies'
Loading
Loading