{"owner":"midwayjs","repo":"midway","hasSkills":true,"totalSkillsCount":132,"totalTokensCount":220317,"categories":["anthropic-skill","command-prompt","root-instruction","plugin-manifest"],"hasMcp":false,"mcpConfig":null,"found":[".codex/skills/ui-ux-pro-max/SKILL.md",".cursor/commands/openspec-apply.md",".cursor/commands/openspec-archive.md",".cursor/commands/openspec-proposal.md","AGENTS.md","openspec/AGENTS.md","packages/api-bridge/README.md","packages/axios/README.md","packages/bootstrap/README.md","packages/bull-board/README.md","packages/bull/README.md","packages/bullmq/README.md","packages/busboy/README.md","packages/cache-manager/README.md","packages/captcha/README.md","packages/casbin-redis-adapter/README.md","packages/casbin-typeorm-adapter/README.md","packages/casbin/README.md","packages/code-dye/README.md","packages/consul/README.md","packages/core/README.md","packages/cos/README.md","packages/cron/README.md","packages/cross-domain/README.md","packages/crud/README.md","packages/etcd/README.md","packages/event-emitter/README.md","packages/express-session/README.md","packages/faas/README.md","packages/grpc/README.md","packages/http-proxy/README.md","packages/info/README.md","packages/jwt/README.md","packages/kafka/README.md","packages/leoric/README.md","packages/mcp/README.md","packages/mikro/README.md","packages/mikro7/README.md","packages/mock/README.md","packages/mongoose/README.md","packages/mqtt/README.md","packages/nextjs/README.md","packages/oss/README.md","packages/passport/README.md","packages/piscina/README.md","packages/prometheus/README.md","packages/rabbitmq/README.md","packages/react/README.md","packages/redis/README.md","packages/security/README.md","packages/sequelize/README.md","packages/session/README.md","packages/skill-midway/README.md","packages/skill-midway/skills/midway/SKILL.md","packages/socketio/README.md","packages/static-file/README.md","packages/swagger/README.md","packages/tablestore/README.md","packages/tags/README.md","packages/tenant/README.md","packages/typeorm/README.md","packages/upload/README.md","packages/validate/README.md","packages/validation-joi/README.md","packages/validation-zod/README.md","packages/validation-zod4/README.md","packages/validation/README.md","packages/view-ejs/README.md","packages/view-nunjucks/README.md","packages/view/README.md","packages/vue/README.md","packages/web-bridge/README.md","packages/web-express/README.md","packages/web-koa/README.md","packages/web/README.md","packages/ws/README.md","site/docs/extensions/alinode.md","site/docs/extensions/apollo.md","site/docs/extensions/axios.md","site/docs/extensions/bull.md","site/docs/extensions/bullmq.md","site/docs/extensions/busboy.md","site/docs/extensions/cache.md","site/docs/extensions/caching.md","site/docs/extensions/captcha.md","site/docs/extensions/casbin.md","site/docs/extensions/cfork.md","site/docs/extensions/code_dye.md","site/docs/extensions/commander.md","site/docs/extensions/consul.md","site/docs/extensions/cos.md","site/docs/extensions/cron.md","site/docs/extensions/cross_domain.md","site/docs/extensions/crud.md","site/docs/extensions/egg.md","site/docs/extensions/etcd.md","site/docs/extensions/events.md","site/docs/extensions/express.md","site/docs/extensions/grpc.md","site/docs/extensions/http-proxy.md","site/docs/extensions/i18n.md","site/docs/extensions/info.md","site/docs/extensions/jwt.md","site/docs/extensions/kafka.md","site/docs/extensions/koa.md","site/docs/extensions/mcp.md","site/docs/extensions/mikro.md","site/docs/extensions/mongodb.md","site/docs/extensions/mqtt.md","site/docs/extensions/one-shot.md","site/docs/extensions/orm.md","site/docs/extensions/oss.md","site/docs/extensions/passport.md","site/docs/extensions/piscina.md","site/docs/extensions/pm2.md","site/docs/extensions/process_agent.md","site/docs/extensions/prometheus.md","site/docs/extensions/rabbitmq.md","site/docs/extensions/redis.md","site/docs/extensions/render.md","site/docs/extensions/security.md","site/docs/extensions/sequelize.md","site/docs/extensions/socketio.md","site/docs/extensions/static_file.md","site/docs/extensions/swagger.md","site/docs/extensions/tablestore.md","site/docs/extensions/tags.md","site/docs/extensions/tenant.md","site/docs/extensions/upload.md","site/docs/extensions/validate.md","site/docs/extensions/validation.md","site/docs/extensions/ws.md"],"skills":{".codex/skills/ui-ux-pro-max/SKILL.md":"---\nname: ui-ux-pro-max\ndescription: \"UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 8 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient.\"\n---\n\n# UI/UX Pro Max - Design Intelligence\n\nSearchable database of UI styles, color palettes, font pairings, chart types, product recommendations, UX guidelines, and stack-specific best practices.\n\n## Prerequisites\n\nCheck if Python is installed:\n\n```bash\npython3 --version || python --version\n```\n\nIf Python is not installed, install it based on user's OS:\n\n**macOS:**\n```bash\nbrew install python3\n```\n\n**Ubuntu/Debian:**\n```bash\nsudo apt update && sudo apt install python3\n```\n\n**Windows:**\n```powershell\nwinget install Python.Python.3.12\n```\n\n---\n\n## How to Use This Skill\n\nWhen user requests UI/UX work (design, build, create, implement, review, fix, improve), follow this workflow:\n\n### Step 1: Analyze User Requirements\n\nExtract key information from user request:\n- **Product type**: SaaS, e-commerce, portfolio, dashboard, landing page, etc.\n- **Style keywords**: minimal, playful, professional, elegant, dark mode, etc.\n- **Industry**: healthcare, fintech, gaming, education, etc.\n- **Stack**: React, Vue, Next.js, or default to `html-tailwind`\n\n### Step 2: Search Relevant Domains\n\nUse `search.py` multiple times to gather comprehensive information. Search until you have enough context.\n\n```bash\npython3 .shared/ui-ux-pro-max/scripts/search.py \"<keyword>\" --domain <domain> [-n <max_results>]\n```\n\n**Recommended search order:**\n\n1. **Product** - Get style recommendations for product type\n2. **Style** - Get detailed style guide (colors, effects, frameworks)\n3. **Typography** - Get font pairings with Google Fonts imports\n4. **Color** - Get color palette (Primary, Secondary, CTA, Background, Text, Border)\n5. **Landing** - Get page structure (if landing page)\n6. **Chart** - Get chart recommendations (if dashboard/analytics)\n7. **UX** - Get best practices and anti-patterns\n8. **Stack** - Get stack-specific guidelines (default: html-tailwind)\n\n### Step 3: Stack Guidelines (Default: html-tailwind)\n\nIf user doesn't specify a stack, **default to `html-tailwind`**.\n\n```bash\npython3 .shared/ui-ux-pro-max/scripts/search.py \"<keyword>\" --stack html-tailwind\n```\n\nAvailable stacks: `html-tailwind`, `react`, `nextjs`, `vue`, `svelte`, `swiftui`, `react-native`, `flutter`\n\n---\n\n## Search Reference\n\n### Available Domains\n\n| Domain | Use For | Example Keywords |\n|--------|---------|------------------|\n| `product` | Product type recommendations | SaaS, e-commerce, portfolio, healthcare, beauty, service |\n| `style` | UI styles, colors, effects | glassmorphism, minimalism, dark mode, brutalism |\n| `typography` | Font pairings, Google Fonts | elegant, playful, professional, modern |\n| `color` | Color palettes by product type | saas, ecommerce, healthcare, beauty, fintech, service |\n| `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof |\n| `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie |\n| `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading |\n| `prompt` | AI prompts, CSS keywords | (style name) |\n\n### Available Stacks\n\n| Stack | Focus |\n|-------|-------|\n| `html-tailwind` | Tailwind utilities, responsive, a11y (DEFAULT) |\n| `react` | State, hooks, performance, patterns |\n| `nextjs` | SSR, routing, images, API routes |\n| `vue` | Composition API, Pinia, Vue Router |\n| `svelte` | Runes, stores, SvelteKit |\n| `swiftui` | Views, State, Navigation, Animation |\n| `react-native` | Components, Navigation, Lists |\n| `flutter` | Widgets, State, Layout, Theming |\n\n---\n\n## Example Workflow\n\n**User request:** \"Làm landing page cho dịch vụ chăm sóc da chuyên nghiệp\"\n\n**AI should:**\n\n```bash\n# 1. Search product type\npython3 .shared/ui-ux-pro-max/scripts/search.py \"beauty spa wellness service\" --domain product\n\n# 2. Search style (based on industry: beauty, elegant)\npython3 .shared/ui-ux-pro-max/scripts/search.py \"elegant minimal soft\" --domain style\n\n# 3. Search typography\npython3 .shared/ui-ux-pro-max/scripts/search.py \"elegant luxury\" --domain typography\n\n# 4. Search color palette\npython3 .shared/ui-ux-pro-max/scripts/search.py \"beauty spa wellness\" --domain color\n\n# 5. Search landing page structure\npython3 .shared/ui-ux-pro-max/scripts/search.py \"hero-centric social-proof\" --domain landing\n\n# 6. Search UX guidelines\npython3 .shared/ui-ux-pro-max/scripts/search.py \"animation\" --domain ux\npython3 .shared/ui-ux-pro-max/scripts/search.py \"accessibility\" --domain ux\n\n# 7. Search stack guidelines (default: html-tailwind)\npython3 .shared/ui-ux-pro-max/scripts/search.py \"layout responsive\" --stack html-tailwind\n```\n\n**Then:** Synthesize all search results and implement the design.\n\n---\n\n## Tips for Better Results\n\n1. **Be specific with keywords** - \"healthcare SaaS dashboard\" > \"app\"\n2. **Search multiple times** - Different keywords reveal different insights\n3. **Combine domains** - Style + Typography + Color = Complete design system\n4. **Always check UX** - Search \"animation\", \"z-index\", \"accessibility\" for common issues\n5. **Use stack flag** - Get implementation-specific best practices\n6. **Iterate** - If first search doesn't match, try different keywords\n7. **Split Into Multiple Files** - For better maintainability:\n   - Separate components into individual files (e.g., `Header.tsx`, `Footer.tsx`)\n   - Extract reusable styles into dedicated files\n   - Keep each file focused and under 200-300 lines\n\n---\n\n## Common Rules for Professional UI\n\nThese are frequently overlooked issues that make UI look unprofessional:\n\n### Icons & Visual Elements\n\n| Rule | Do | Don't |\n|------|----|----- |\n| **No emoji icons** | Use SVG icons (Heroicons, Lucide, Simple Icons) | Use emojis like 🎨 🚀 ⚙️ as UI icons |\n| **Stable hover states** | Use color/opacity transitions on hover | Use scale transforms that shift layout |\n| **Correct brand logos** | Research official SVG from Simple Icons | Guess or use incorrect logo paths |\n| **Consistent icon sizing** | Use fixed viewBox (24x24) with w-6 h-6 | Mix different icon sizes randomly |\n\n### Interaction & Cursor\n\n| Rule | Do | Don't |\n|------|----|----- |\n| **Cursor pointer** | Add `cursor-pointer` to all clickable/hoverable cards | Leave default cursor on interactive elements |\n| **Hover feedback** | Provide visual feedback (color, shadow, border) | No indication element is interactive |\n| **Smooth transitions** | Use `transition-colors duration-200` | Instant state changes or too slow (>500ms) |\n\n### Light/Dark Mode Contrast\n\n| Rule | Do | Don't |\n|------|----|----- |\n| **Glass card light mode** | Use `bg-white/80` or higher opacity | Use `bg-white/10` (too transparent) |\n| **Text contrast light** | Use `#0F172A` (slate-900) for text | Use `#94A3B8` (slate-400) for body text |\n| **Muted text light** | Use `#475569` (slate-600) minimum | Use gray-400 or lighter |\n| **Border visibility** | Use `border-gray-200` in light mode | Use `border-white/10` (invisible) |\n\n### Layout & Spacing\n\n| Rule | Do | Don't |\n|------|----|----- |\n| **Floating navbar** | Add `top-4 left-4 right-4` spacing | Stick navbar to `top-0 left-0 right-0` |\n| **Content padding** | Account for fixed navbar height | Let content hide behind fixed elements |\n| **Consistent max-width** | Use same `max-w-6xl` or `max-w-7xl` | Mix different container widths |\n\n---\n\n## Pre-Delivery Checklist\n\nBefore delivering UI code, verify these items:\n\n### Visual Quality\n- [ ] No emojis used as icons (use SVG instead)\n- [ ] All icons from consistent icon set (Heroicons/Lucide)\n",".cursor/commands/openspec-apply.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=.cursor%2Fcommands%2Fopenspec-apply.md -->",".cursor/commands/openspec-archive.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=.cursor%2Fcommands%2Fopenspec-archive.md -->",".cursor/commands/openspec-proposal.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=.cursor%2Fcommands%2Fopenspec-proposal.md -->","AGENTS.md":"<!-- OPENSPEC:START -->\n# OpenSpec Instructions\n\nThese instructions are for AI assistants working in this project.\n\nAlways open `@/openspec/AGENTS.md` when the request:\n- Mentions planning or proposals (words like proposal, spec, change, plan)\n- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work\n- Sounds ambiguous and you need the authoritative spec before coding\n\nUse `@/openspec/AGENTS.md` to learn:\n- How to create and apply change proposals\n- Spec format and conventions\n- Project structure and guidelines\n\nKeep this managed block so 'openspec update' can refresh the instructions.\n\n<!-- OPENSPEC:END -->\n\n# Repository Guide for Agents\n\n## Project overview\n- Midway v4 monorepo managed by lerna + pnpm workspace.\n- TypeScript 5, built with `tsc`, tested with `jest`.\n- IoC container implementation lives in `packages/core` (MidwayContainer, DecoratorManager, MetadataManager).\n\n## Code rules\n- Each component is an independent npm package and can communicate via DI.\n- New/changed components must include tests. Run `npm run test` in the package.\n- Use `mwts` for linting (`npm run lint:fix` when needed).\n- After code changes, rerun the relevant lint command and keep necessary lint-fix output in the patch instead of reverting it casually.\n- Add meaningful comments for functions/classes/interfaces/enums.\n\n## Docs rules\n- Current docs live under `site/docs` (Docusaurus).\n- Match existing doc style/tone/structure for consistency.\n- Show dependency installation in both bash (npm) and JSON `package.json` forms.\n- Doc changes do not require a build check.\n- For component docs, write for beginners first: start from what the component solves, when to use it, and the simplest working path, then go deeper into advanced usage, configuration, and extension points. Prefer tutorial-style progression over API-only lists.\n\n## Practical tips\n- Prefer `pnpm -C <package> test` to scope tests.\n- Keep changes aligned with existing patterns in the targeted package.\n- Pull request titles must use standard commit format, such as `feat: xxx` or `fix: xxx`.\n","openspec/AGENTS.md":"# OpenSpec Instructions\n\nInstructions for AI coding assistants using OpenSpec for spec-driven development.\n\n## TL;DR Quick Checklist\n\n- Search existing work: `openspec spec list --long`, `openspec list` (use `rg` only for full-text search)\n- Decide scope: new capability vs modify existing capability\n- Pick a unique `change-id`: kebab-case, verb-led (`add-`, `update-`, `remove-`, `refactor-`)\n- Scaffold: `proposal.md`, `tasks.md`, `design.md` (only if needed), and delta specs per affected capability\n- Write deltas: use `## ADDED|MODIFIED|REMOVED|RENAMED Requirements`; include at least one `#### Scenario:` per requirement\n- Validate: `openspec validate [change-id] --strict --no-interactive` and fix issues\n- Request approval: Do not start implementation until proposal is approved\n\n## Three-Stage Workflow\n\n### Stage 1: Creating Changes\nCreate proposal when you need to:\n- Add features or functionality\n- Make breaking changes (API, schema)\n- Change architecture or patterns  \n- Optimize performance (changes behavior)\n- Update security patterns\n\nTriggers (examples):\n- \"Help me create a change proposal\"\n- \"Help me plan a change\"\n- \"Help me create a proposal\"\n- \"I want to create a spec proposal\"\n- \"I want to create a spec\"\n\nLoose matching guidance:\n- Contains one of: `proposal`, `change`, `spec`\n- With one of: `create`, `plan`, `make`, `start`, `help`\n\nSkip proposal for:\n- Bug fixes (restore intended behavior)\n- Typos, formatting, comments\n- Dependency updates (non-breaking)\n- Configuration changes\n- Tests for existing behavior\n\n**Workflow**\n1. Review `openspec/project.md`, `openspec list`, and `openspec list --specs` to understand current context.\n2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, optional `design.md`, and spec deltas under `openspec/changes/<id>/`.\n3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement.\n4. Run `openspec validate <id> --strict --no-interactive` and resolve any issues before sharing the proposal.\n\n### Stage 2: Implementing Changes\nTrack these steps as TODOs and complete them one by one.\n1. **Read proposal.md** - Understand what's being built\n2. **Read design.md** (if exists) - Review technical decisions\n3. **Read tasks.md** - Get implementation checklist\n4. **Implement tasks sequentially** - Complete in order\n5. **Confirm completion** - Ensure every item in `tasks.md` is finished before updating statuses\n6. **Update checklist** - After all work is done, set every task to `- [x]` so the list reflects reality\n7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved\n\n### Stage 3: Archiving Changes\nAfter deployment, create separate PR to:\n- Move `changes/[name]/` → `changes/archive/YYYY-MM-DD-[name]/`\n- Update `specs/` if capabilities changed\n- Use `openspec archive <change-id> --skip-specs --yes` for tooling-only changes (always pass the change ID explicitly)\n- Run `openspec validate --strict --no-interactive` to confirm the archived change passes checks\n\n## Before Any Task\n\n**Context Checklist:**\n- [ ] Read relevant specs in `specs/[capability]/spec.md`\n- [ ] Check pending changes in `changes/` for conflicts\n- [ ] Read `openspec/project.md` for conventions\n- [ ] Run `openspec list` to see active changes\n- [ ] Run `openspec list --specs` to see existing capabilities\n\n**Before Creating Specs:**\n- Always check if capability already exists\n- Prefer modifying existing specs over creating duplicates\n- Use `openspec show [spec]` to review current state\n- If request is ambiguous, ask 1–2 clarifying questions before scaffolding\n\n### Search Guidance\n- Enumerate specs: `openspec spec list --long` (or `--json` for scripts)\n- Enumerate changes: `openspec list` (or `openspec change list --json` - deprecated but available)\n- Show details:\n  - Spec: `openspec show <spec-id> --type spec` (use `--json` for filters)\n  - Change: `openspec show <change-id> --json --deltas-only`\n- Full-text search (use ripgrep): `rg -n \"Requirement:|Scenario:\" openspec/specs`\n\n## Quick Start\n\n### CLI Commands\n\n```bash\n# Essential commands\nopenspec list                  # List active changes\nopenspec list --specs          # List specifications\nopenspec show [item]           # Display change or spec\nopenspec validate [item]       # Validate changes or specs\nopenspec archive <change-id> [--yes|-y]   # Archive after deployment (add --yes for non-interactive runs)\n\n# Project management\nopenspec init [path]           # Initialize OpenSpec\nopenspec update [path]         # Update instruction files\n\n# Interactive mode\nopenspec show                  # Prompts for selection\nopenspec validate              # Bulk validation mode\n\n# Debugging\nopenspec show [change] --json --deltas-only\nopenspec validate [change] --strict --no-interactive\n```\n\n### Command Flags\n\n- `--json` - Machine-readable output\n- `--type change|spec` - Disambiguate items\n- `--strict` - Comprehensive validation\n- `--no-interactive` - Disable prompts\n- `--skip-specs` - Archive without spec updates\n- `--yes`/`-y` - Skip confirmation prompts (non-interactive archive)\n\n## Directory Structure\n\n```\nopenspec/\n├── project.md              # Project conventions\n├── specs/                  # Current truth - what IS built\n│   └── [capability]/       # Single focused capability\n│       ├── spec.md         # Requirements and scenarios\n│       └── design.md       # Technical patterns\n├── changes/                # Proposals - what SHOULD change\n│   ├── [change-name]/\n│   │   ├── proposal.md     # Why, what, impact\n│   │   ├── tasks.md        # Implementation checklist\n│   │   ├── design.md       # Technical decisions (optional; see criteria)\n│   │   └── specs/          # Delta changes\n│   │       └── [capability]/\n│   │           └── spec.md # ADDED/MODIFIED/REMOVED\n│   └── archive/            # Completed changes\n```\n\n## Creating Change Proposals\n\n### Decision Tree\n\n```\nNew request?\n├─ Bug fix restoring spec behavior? → Fix directly\n├─ Typo/format/comment? → Fix directly  \n├─ New feature/capability? → Create proposal\n├─ Breaking change? → Create proposal\n├─ Architecture change? → Create proposal\n└─ Unclear? → Create proposal (safer)\n```\n\n### Proposal Structure\n\n1. **Create directory:** `changes/[change-id]/` (kebab-case, verb-led, unique)\n\n2. **Write proposal.md:**\n```markdown\n# Change: [Brief description of change]\n\n## Why\n[1-2 sentences on problem/opportunity]\n\n## What Changes\n- [Bullet list of changes]\n- [Mark breaking changes with **BREAKING**]\n\n## Impact\n- Affected specs: [list capabilities]\n- Affected code: [key files/systems]\n```\n\n3. **Create spec deltas:** `specs/[capability]/spec.md`\n```markdown\n## ADDED Requirements\n### Requirement: New Feature\nThe system SHALL provide...\n\n#### Scenario: Success case\n- **WHEN** user performs action\n- **THEN** expected result\n\n## MODIFIED Requirements\n### Requirement: Existing Feature\n[Complete modified requirement]\n\n## REMOVED Requirements\n### Requirement: Old Feature\n**Reason**: [Why removing]\n**Migration**: [How to handle]\n```\nIf multiple capabilities are affected, create multiple delta files under `changes/[change-id]/specs/<capability>/spec.md`—one per capability.\n\n4. **Create tasks.md:**\n```markdown\n## 1. Implementation\n- [ ] 1.1 Create database schema\n- [ ] 1.2 Implement API endpoint\n- [ ] 1.3 Add frontend component\n- [ ] 1.4 Write tests\n```\n\n5. **Create design.md when needed:**\nCreate `design.md` if any of the following apply; otherwise omit it:\n- Cross-cutting change (multiple services/modules) or a new architectural pattern\n- New external dependency or significant data model changes\n- Security, performance, or migration complexity\n- Ambiguity that benefits from technical decisions before coding\n\nMinimal `design.md` skeleton:\n```markdown\n## Context\n[Background, constraints, stakeholders]\n\n## Goals / Non-Goals\n- Goals: [...]\n- Non-Goals: [...]\n\n## Decisions\n- Decision: [What and why]\n- Alternatives considered: [Options + rationale]\n\n## Risks / Trade-offs\n- [Risk] → Mitigation\n\n## Migration Plan\n[Steps, rollback]\n\n## Open Questions\n- [...]\n```\n\n## Spec File Format\n\n### Critical: Scenario Formatting\n\n**CORRECT** (use #### headers):\n```markdown\n#### Scenario: User login success\n- **WHEN** valid credentials provided\n- **THEN** return JWT token\n```\n\n**WRONG** (don't use bullets or bold):\n```markdown\n- **Scenario: User login**  ❌\n**Scenario**: User login     ❌\n### Scenario: User login      ❌\n```\n\nEvery requirement MUST have at least one scenario.\n\n### Requirement Wording\n- Use SHALL/MUST for normative requirements (avoid should/may unless intentionally non-normative)\n\n### Delta Operations\n\n- `## ADDED Requirements` - New capabilities\n- `## MODIFIED Requirements` - Changed behavior\n- `## REMOVED Requirements` - Deprecated features\n- `## RENAMED Requirements` - Name changes\n\nHeaders matched with `trim(header)` - whitespace ignored.\n\n#### When to use ADDED vs MODIFIED\n- ADDED: Introduces a new capability or sub-capability that can stand alone as a requirement. Prefer ADDED when the change is orthogonal (e.g., adding \"Slash Command Configuration\") rather than altering the semantics of an existing requirement.\n- MODIFIED: Changes the behavior, scope, or acceptance criteria of an existing requirement. Always paste the full, updated requirement content (header + all scenarios). The archiver will replace the entire requirement with what you provide here; partial deltas will drop previous details.\n- RENAMED: Use when only the name changes. If you also change behavior, use RENAMED (name) plus MODIFIED (content) referencing the new name.\n\nCommon pitfall: Using MODIFIED to add a new concern without including the previous text. This causes loss of detail at archive time. If you aren’t explicitly changing the existing requirement, add a new requirement under ADDED instead.\n\nAuthoring a MODIFIED requirement correctly:\n1) Locate the existing requirement in `openspec/specs/<capability>/spec.md`.\n2) Copy the entire requirement block (from `### Requirement: ...` through its scenarios).\n3) Paste it under `## MODIFIED Requirements` and edit to reflect the new behavior.\n4) Ensure the header text matches exactly (whitespace-insensitive) and keep at least one `#### Scenario:`.\n\nExample for RENAMED:\n```markdown\n## RENAMED Requirements\n- FROM: `### Requirement: Login`\n- TO: `### Requirement: User Authentication`\n```\n\n## Troubleshooting\n\n### Common Errors\n\n**\"Change must have at least one delta\"**\n- Check `changes/[name]/specs/` exists with .md files\n- Verify files have operation prefixes (## ADDED Requirements)\n\n**\"Requirement must have at least one scenario\"**\n- Check scenarios use `#### Scenario:` format (4 hashtags)\n- Don't use bullet points or bold for scenario headers\n\n**Silent scenario parsing failures**\n- Exact format required: `#### Scenario: Name`\n- Debug with: `openspec show [change] --json --deltas-only`\n\n### Validation Tips\n\n```bash\n# Always use strict mode for comprehensive checks\nopenspec validate [change] --strict --no-interactive\n\n# Debug delta parsing\nopenspec show [change] --json | jq '.deltas'\n\n# Check specific requirement\nopenspec show [spec] --json -r 1\n```\n\n## Happy Path Script\n\n```bash\n# 1) Explore current state\nopenspec spec list --long\nopenspec list\n# Optional full-text search:\n# rg -n \"Requirement:|Scenario:\" openspec/specs\n# rg -n \"^#|Requirement:\" openspec/changes\n\n# 2) Choose change id and scaffold\nCHANGE=add-two-factor-auth\nmkdir -p openspec/changes/$CHANGE/{specs/auth}\nprintf \"## Why\\n...\\n\\n## What Changes\\n- ...\\n\\n## Impact\\n- ...\\n\" > openspec/changes/$CHANGE/proposal.md\nprintf \"## 1. Implementation\\n- [ ] 1.1 ...\\n\" > openspec/changes/$CHANGE/tasks.md\n\n# 3) Add deltas (example)\ncat > openspec/changes/$CHANGE/specs/auth/spec.md << 'EOF'\n## ADDED Requirements\n### Requirement: Two-Factor Authentication\nUsers MUST provide a second factor during login.\n\n#### Scenario: OTP required\n- **WHEN** valid credentials are provided\n- **THEN** an OTP challenge is required\nEOF\n\n# 4) Validate\nopenspec validate $CHANGE --strict --no-interactive\n```\n\n## Multi-Capability Example\n\n```\nopenspec/changes/add-2fa-notify/\n├── proposal.md\n├── tasks.md\n└── specs/\n    ├── auth/\n    │   └── spec.md   # ADDED: Two-Factor Authentication\n    └── notifications/\n        └── spec.md   # ADDED: OTP email notification\n```\n\nauth/spec.md\n```markdown\n## ADDED Requirements\n### Requirement: Two-Factor Authentication\n...\n```\n\nnotifications/spec.md\n```markdown\n## ADDED Requirements\n### Requirement: OTP Email Notification\n...\n```\n\n## Best Practices\n\n### Simplicity First\n- Default to <100 lines of new code\n- Single-file implementations until proven insufficient\n- Avoid frameworks without clear justification\n- Choose boring, proven patterns\n\n### Complexity Triggers\nOnly add complexity with:\n- Performance data showing current solution too slow\n- Concrete scale requirements (>1000 users, >100MB data)\n- Multiple proven use cases requiring abstraction\n\n### Clear References\n- Use `file.ts:42` format for code locations\n- Reference specs as `specs/auth/spec.md`\n- Link related changes and PRs\n\n### Capability Naming\n- Use verb-noun: `user-auth`, `payment-capture`\n- Single purpose per capability\n- 10-minute understandability rule\n- Split if description needs \"AND\"\n\n### Change ID Naming\n- Use kebab-case, short and descriptive: `add-two-factor-auth`\n- Prefer verb-led prefixes: `add-`, `update-`, `remove-`, `refactor-`\n- Ensure uniqueness; if taken, append `-2`, `-3`, etc.\n\n## Tool Selection Guide\n\n| Task | Tool | Why |\n|------|------|-----|\n| Find files by pattern | Glob | Fast pattern matching |\n| Search code content | Grep | Optimized regex search |\n| Read specific files | Read | Direct file access |\n| Explore unknown scope | Task | Multi-step investigation |\n\n## Error Recovery\n\n### Change Conflicts\n1. Run `openspec list` to see active changes\n2. Check for overlapping specs\n3. Coordinate with change owners\n4. Consider combining proposals\n\n### Validation Failures\n1. Run with `--strict` flag\n2. Check JSON output for details\n3. Verify spec file format\n4. Ensure scenarios properly formatted\n\n### Missing Context\n1. Read project.md first\n2. Check related specs\n3. Review recent archives\n4. Ask for clarification\n\n## Quick Reference\n\n### Stage Indicators\n- `changes/` - Proposed, not yet built\n- `specs/` - Built and deployed\n- `archive/` - Completed changes\n\n### File Purposes\n- `proposal.md` - Why and what\n- `tasks.md` - Implementation steps\n- `design.md` - Technical decisions\n- `spec.md` - Requirements and behavior\n\n### CLI Essentials\n```bash\nopenspec list              # What's in progress?\nopenspec show [item]       # View details\nopenspec validate --strict --no-interactive  # Is it correct?\nopenspec archive <change-id> [--yes|-y]  # Mark complete (add --yes for automation)\n```\n\nRemember: Specs are truth. Changes are proposals. Keep them in sync.\n","packages/api-bridge/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fapi-bridge%2FREADME.md -->","packages/axios/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Faxios%2FREADME.md -->","packages/bootstrap/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fbootstrap%2FREADME.md -->","packages/bull-board/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fbull-board%2FREADME.md -->","packages/bull/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fbull%2FREADME.md -->","packages/bullmq/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fbullmq%2FREADME.md -->","packages/busboy/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fbusboy%2FREADME.md -->","packages/cache-manager/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcache-manager%2FREADME.md -->","packages/captcha/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcaptcha%2FREADME.md -->","packages/casbin-redis-adapter/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcasbin-redis-adapter%2FREADME.md -->","packages/casbin-typeorm-adapter/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcasbin-typeorm-adapter%2FREADME.md -->","packages/casbin/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcasbin%2FREADME.md -->","packages/code-dye/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcode-dye%2FREADME.md -->","packages/consul/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fconsul%2FREADME.md -->","packages/core/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcore%2FREADME.md -->","packages/cos/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcos%2FREADME.md -->","packages/cron/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcron%2FREADME.md -->","packages/cross-domain/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcross-domain%2FREADME.md -->","packages/crud/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcrud%2FREADME.md -->","packages/etcd/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fetcd%2FREADME.md -->","packages/event-emitter/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fevent-emitter%2FREADME.md -->","packages/express-session/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fexpress-session%2FREADME.md -->","packages/faas/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Ffaas%2FREADME.md -->","packages/grpc/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fgrpc%2FREADME.md -->","packages/http-proxy/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fhttp-proxy%2FREADME.md -->","packages/info/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Finfo%2FREADME.md -->","packages/jwt/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fjwt%2FREADME.md -->","packages/kafka/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fkafka%2FREADME.md -->","packages/leoric/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fleoric%2FREADME.md -->","packages/mcp/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fmcp%2FREADME.md -->","packages/mikro/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fmikro%2FREADME.md -->","packages/mikro7/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fmikro7%2FREADME.md -->","packages/mock/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fmock%2FREADME.md -->","packages/mongoose/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fmongoose%2FREADME.md -->","packages/mqtt/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fmqtt%2FREADME.md -->","packages/nextjs/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fnextjs%2FREADME.md -->","packages/oss/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Foss%2FREADME.md -->","packages/passport/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fpassport%2FREADME.md -->","packages/piscina/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fpiscina%2FREADME.md -->","packages/prometheus/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fprometheus%2FREADME.md -->","packages/rabbitmq/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Frabbitmq%2FREADME.md -->","packages/react/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Freact%2FREADME.md -->","packages/redis/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fredis%2FREADME.md -->","packages/security/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fsecurity%2FREADME.md -->","packages/sequelize/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fsequelize%2FREADME.md -->","packages/session/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fsession%2FREADME.md -->","packages/skill-midway/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fskill-midway%2FREADME.md -->","packages/skill-midway/skills/midway/SKILL.md":"---\nname: midway\ndescription: Use when the task involves Midway framework docs, package selection, API lookup, version compatibility, MCP/HTTP/framework components, or migration guidance. Resolve the requested Midway version first, then query the local Midway skill bundle with `midway-skill` commands.\n---\n\n# Midway\n\nUse the local `@midwayjs/skill-midway` bundle before relying on memory.\n\n## Workflow\n\n1. Resolve the user's requested version first.\n   Run `npm exec midway-skill -- resolve-version <version>` or `pnpm exec midway-skill resolve-version <version>`.\n   If no version is given, use `current`.\n\n2. Query the right source for the question.\n   Use `npm exec midway-skill -- lookup-docs --query \"<topic>\"` or `pnpm exec midway-skill lookup-docs --query \"<topic>\"`.\n   Use `npm exec midway-skill -- lookup-api --symbol \"<symbol>\"` or `pnpm exec midway-skill lookup-api --symbol \"<symbol>\"` for current-major API lookups.\n   Use `npm exec midway-skill -- lookup-packages --query \"<package or feature>\"` or `pnpm exec midway-skill lookup-packages --query \"<package or feature>\"`.\n   Use `npm exec midway-skill -- lookup-changelog --package \"<package>\"` or `pnpm exec midway-skill lookup-changelog --package \"<package>\"` for release history.\n\n3. Cite what the bundle resolved.\n   Include the requested version, resolved version, and whether it was an exact match or major-version fallback.\n\n## Rules\n\n- Prefer bundle results over recollection.\n- Historical majors only guarantee `docs` and `changelog`.\n- If `resolve-version` reports `api: false`, do not claim exact historical API details.\n- When results are empty, say that the bundle did not contain a match instead of inventing one.\n- Prefer package names with the `@midwayjs/` scope in answers when known.\n\n## Output\n\n- State the resolved Midway version.\n- Summarize the matched docs/API/package/changelog result.\n- Keep links or source paths from bundle records when relevant.\n","packages/socketio/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fsocketio%2FREADME.md -->","packages/static-file/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fstatic-file%2FREADME.md -->","packages/swagger/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fswagger%2FREADME.md -->","packages/tablestore/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Ftablestore%2FREADME.md -->","packages/tags/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Ftags%2FREADME.md -->","packages/tenant/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Ftenant%2FREADME.md -->","packages/typeorm/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Ftypeorm%2FREADME.md -->","packages/upload/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fupload%2FREADME.md -->","packages/validate/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fvalidate%2FREADME.md -->","packages/validation-joi/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fvalidation-joi%2FREADME.md -->","packages/validation-zod/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fvalidation-zod%2FREADME.md -->","packages/validation-zod4/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fvalidation-zod4%2FREADME.md -->","packages/validation/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fvalidation%2FREADME.md -->","packages/view-ejs/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fview-ejs%2FREADME.md -->","packages/view-nunjucks/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fview-nunjucks%2FREADME.md -->","packages/view/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fview%2FREADME.md -->","packages/vue/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fvue%2FREADME.md -->","packages/web-bridge/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fweb-bridge%2FREADME.md -->","packages/web-express/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fweb-express%2FREADME.md -->","packages/web-koa/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fweb-koa%2FREADME.md -->","packages/web/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fweb%2FREADME.md -->","packages/ws/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fws%2FREADME.md -->","site/docs/extensions/alinode.md":"# Alinode\n\n## 准备工作\n\n需要接入的应用是要部署在独立的服务获取云环境，可以接入互联网服务。\n\n## 创建服务\n\n**第一步**\n\n登录阿里云，点击开通 [阿里云的 Node.js 性能平台](https://www.aliyun.com/product/nodejs) 的服务。\n\n**第二步**\n\n创建新应用，获取 APP ID 和 App Secret。\n<img src=\"https://cdn.nlark.com/yuque/0/2021/png/187105/1617267785895-dd0fb702-91c7-4b25-9c64-8a9358f2d254.png#align=left&display=inline&height=351&margin=%5Bobject%20Object%5D&name=image.png&originHeight=702&originWidth=1548&size=106487&status=done&style=none&width=774\" width=\"774\" />\n\n## 安装监控依赖\n\n**第一步**\n\n安装 Node.js 性能平台所需的组件\n\n```bash\n# 安装版本管理工具 tnvm，安装过程出错参考：https://github.com/aliyun-node/tnvm\n$ wget -O- https://raw.githubusercontent.com/aliyun-node/tnvm/master/install.sh | bash\n$ source ~/.bashrc\n\n# tnvm ls-remote alinode \t\t\t\t\t\t\t\t# 查看需要的版本\n$ tnvm install alinode-v6.5.0 \t\t\t\t\t# 安装需要的版本\n$ tnvm use alinode-v6.5.0 \t\t\t\t\t\t\t# 使用需要的版本\n\n$ npm install @alicloud/agenthub -g \t\t# 安装 agenthub\n```\n\n这里有三个部分\n\n- 1、安装 tnvm（alinode 源）\n- 2、使用 tnvm 安装 alinode（替代默认的 node）\n- 3、安装 alinode 需要的数据采集器\n\n安装完成后，可以检查一下，需要确保 `which node` 和 `which agenthub` 的路径中包括 `.tnvm` 即可。\n\n```bash\n$ which node\n/root/.tnvm/versions/alinode/v3.11.4/bin/node\n\n$ which agenthub\n/root/.tnvm/versions/alinode/v3.11.4/bin/agenthub\n```\n\n将 `创建新应用` 中获得的 `App ID` 和 `App Secret` 按如下所示保存为 `yourconfig.json`。比如放在项目根目录。\n\n```typescript\n{\n  \"appid\": \"****\",\n  \"secret\": \"****\",\n}\n```\n\n启动插件：\n\n```typescript\nagenthub start yourconfig.json\n```\n\n## 启动 node 服务\n\n在安装了服务器中，启动 Node 服务时，需要加入 ENABLE_NODE_LOG=YES 环境变量。\n\n比如：\n\n```bash\n$ NODE_ENV=production ENABLE_NODE_LOG=YES node bootstrap.js\n```\n\n## Docker 容器的方法\n\n关于 docker 容器的方法可以查看 [文档](https://help.aliyun.com/document_detail/66027.html?spm=a2c4g.11186623.6.580.261ba70feI6mWt)。\n\n## 其他\n\n更多内容可以查看阿里云 Node.js 性能平台的 [文档](https://help.aliyun.com/document_detail/60338.html?spm=a2c4g.11186623.6.548.599312e6IkGO9v)。\n","site/docs/extensions/apollo.md":"# Apollo GraphQL\n\nGraphQL 适合需要让前端按需查询字段、聚合多个后端数据源，或者希望用统一查询语言暴露 API 的应用。Midway 通过 `@midwayjs/apollo` 提供基于 Apollo Server 的 GraphQL HTTP 端点，并让 Resolver 直接使用 Midway 的依赖注入和请求上下文。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n## 安装依赖\n\nApollo 组件已经内置 Apollo Server、GraphQL、WebSocket 订阅等运行时依赖，不需要额外安装 `@apollo/server` 或 `graphql`。\n\n```bash\n$ npm i @midwayjs/apollo@4 --save\n```\n\n如果使用 Koa 或 Express，还需要安装对应 Web 框架组件：\n\n```bash\n$ npm i @midwayjs/koa@4 --save\n# 或者\n$ npm i @midwayjs/express@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/apollo\": \"^4.0.0\",\n    \"@midwayjs/koa\": \"^4.0.0\"\n  }\n}\n```\n\n## 开启组件\n\n在 `src/configuration.ts` 中导入 Apollo 组件和一个 Web 框架组件。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as apollo from '@midwayjs/apollo';\n\n@Configuration({\n  imports: [\n    koa,\n    apollo,\n  ],\n})\nexport class MainConfiguration {}\n```\n\n组件默认会把 GraphQL 端点挂载到 `/graphql`，默认支持 `GET` 和 `POST` 请求。\n\n## 最小示例\n\n在 `src/config/config.default.ts` 中配置 GraphQL Schema 和 Resolver。\n\n```typescript\nexport const apollo = {\n  path: '/graphql',\n  typeDefs: `\n    type Query {\n      hello: String!\n    }\n  `,\n  resolvers: {\n    Query: {\n      hello: () => 'hello midway',\n    },\n  },\n};\n```\n\n启动应用后，请求 `/graphql`。\n\n```bash\n$ curl -X POST http://127.0.0.1:7001/graphql \\\n  -H 'content-type: application/json' \\\n  -d '{\"query\":\"{ hello }\"}'\n```\n\n返回：\n\n```json\n{\n  \"data\": {\n    \"hello\": \"hello midway\"\n  }\n}\n```\n\n开发环境下，也可以直接在浏览器访问 `/graphql` 打开 GraphiQL 调试页面。\n\n## 使用 Schema 文件\n\n当 Schema 较多时，推荐把 SDL 放到 `.graphql` 文件中，再通过 `typePaths` 加载。\n\n```graphql\n# src/graphql/schema.graphql\ntype Query {\n  user(id: ID!): User\n}\n\ntype Mutation {\n  updateUserName(id: ID!, name: String!): User\n}\n\ntype User {\n  id: ID!\n  name: String!\n}\n```\n\n配置文件中使用 `typePaths`。\n\n```typescript\nexport const apollo = {\n  path: '/graphql',\n  typePaths: ['./graphql/**/*.graphql'],\n};\n```\n\n`typePaths` 中的相对路径会从应用的 `baseDir` 开始解析，既支持精确文件路径，也支持 glob 表达式。你也可以同时配置 `typeDefs` 和 `typePaths`，组件会在启动时合并。\n\n## Resolver Map\n\n你可以直接在配置里写 Apollo Resolver Map。\n\n```typescript\nimport { UserService } from '../service/user';\n\nexport const apollo = {\n  typeDefs: `\n    type Query {\n      userName(id: ID!): String\n    }\n\n    type Mutation {\n      updateUserName(id: ID!, name: String!): String\n    }\n  `,\n  resolvers: {\n    Query: {\n      userName: async (_parent, args, context) => {\n        const userService = await context.requestContext.getAsync(UserService);\n        const user = await userService.findById(args.id);\n        return user?.name;\n      },\n    },\n    Mutation: {\n      updateUserName: async (_parent, args, context) => {\n        const userService = await context.requestContext.getAsync(UserService);\n        const user = await userService.updateName(args.id, args.name);\n        return user.name;\n      },\n    },\n  },\n};\n```\n\nResolver 的第三个参数就是当前 Midway 请求上下文，不需要再从 `context.ctx` 解包。Koa 场景下，`context` 是 Midway 增强后的 Koa ctx；Express 场景下，`context` 是 Midway 增强后的 Express request/context。\n\nMidway 会在上下文上挂载 `requestContext`、`logger`、`getLogger()`、`setAttr()`、`getAttr()` 和 `getApp()`。\n\n## Resolver 类\n\n也可以使用 Resolver 类来获得更自然的依赖注入体验。相关装饰器可以直接从 `@midwayjs/apollo` 中引入。\n\n```typescript\nimport { Inject } from '@midwayjs/core';\nimport { Args, Context, Mutation, Query, Resolver } from '@midwayjs/apollo';\nimport { UserService } from '../service/user';\n\n@Resolver()\nexport class UserResolver {\n  @Inject()\n  userService: UserService;\n\n  @Query('userName')\n  async userName(@Args('id') id: string, @Context() context) {\n    context.logger.info('query user %s', id);\n    const user = await this.userService.findById(id);\n    return user?.name;\n  }\n\n  @Mutation('updateUserName')\n  async updateUserName(@Args('id') id: string, @Args('name') name: string) {\n    const user = await this.userService.updateName(id, name);\n    return user.name;\n  }\n}\n```\n\nResolver 类会由 Midway 容器托管，支持 `@Inject()` 和请求级依赖。组件会自动发现使用 `@Resolver()` 标记的类，也可以通过 `resolverClasses` 显式传入。\n\n```typescript\nimport { UserResolver } from '../resolver/user';\n\nexport const apollo = {\n  typePaths: ['./graphql/**/*.graphql'],\n  resolverClasses: [UserResolver],\n};\n```\n\n可用的 Resolver 装饰器包括：\n\n| 装饰器            | 说明                     |\n| ----------------- | ------------------------ |\n| `@Resolver()`     | 标记一个 Resolver 类     |\n| `@Query()`        | 标记 Query 字段          |\n| `@Mutation()`     | 标记 Mutation 字段       |\n| `@Subscription()` | 标记 Subscription 字段   |\n\n可用的参数装饰器包括：\n\n| 装饰器       | 说明                     |\n| ------------ | ------------------------ |\n| `@Parent()`  | 获取父级 Resolver 返回值 |\n| `@Args()`    | 获取 GraphQL 参数        |\n| `@Context()` | 获取 Midway 请求上下文   |\n| `@Info()`    | 获取 GraphQL resolve info |\n\n不传参数时会注入完整对象，传入字段名时只注入对应字段，例如 `@Args('id')`。\n\n## 扩展 Context\n\n可以通过 `contextFactory` 给每次 GraphQL 请求补充字段。\n\n```typescript\nexport const apollo = {\n  typeDefs,\n  resolvers,\n  contextFactory: async context => {\n    return {\n      currentUserId: context.headers['x-user-id'],\n    };\n  },\n};\n```\n\n随后在 Resolver 中读取：\n\n```typescript\nconst userId = context.currentUserId;\n```\n\n内置字段如 `requestContext`、`logger`、`getLogger`、`setAttr`、`getAttr` 和 `getApp` 会被保留，不建议覆盖。\n\n## GraphiQL\n\n开发环境默认开启 GraphiQL。浏览器访问 `/graphql` 时会显示 GraphiQL 页面，生产环境默认关闭。\n\n```typescript\nexport const apollo = {\n  graphiql: true,\n};\n```\n\n可以显式关闭：\n\n```typescript\nexport const apollo = {\n  graphiql: false,\n};\n```\n\n也可以定制页面标题和请求地址：\n\n```typescript\nexport const apollo = {\n  graphiql: {\n    title: 'My GraphQL Console',\n    endpoint: '/graphql',\n  },\n};\n```\n\n## 订阅\n\n组件支持 `graphql-ws` 协议的 GraphQL Subscription。默认关闭，需要在配置中开启。\n\n```typescript\nexport const apollo = {\n  path: '/graphql',\n  subscriptions: true,\n  typeDefs: `\n    type Query {\n      hello: String!\n    }\n\n    type Subscription {\n      counter: Int!\n    }\n  `,\n};\n```\n\nResolver 方法需要返回 `AsyncIterable`。\n\n```typescript\nimport { Resolver, Subscription } from '@midwayjs/apollo';\n\n@Resolver()\nexport class CounterResolver {\n  @Subscription('counter')\n  async *counter() {\n    yield {\n      counter: 1,\n    };\n  }\n}\n```\n\n开启后，WebSocket 地址默认和 HTTP GraphQL 地址一致，例如 `ws://127.0.0.1:7001/graphql`。也可以单独指定订阅路径。\n\n```typescript\nexport const apollo = {\n  path: '/graphql',\n  subscriptions: {\n    path: '/graphql',\n    connectionInitWaitTimeout: 3000,\n  },\n};\n```\n\n客户端需要使用 `graphql-ws` 协议连接。\n\n```typescript\nimport { createClient } from 'graphql-ws';\n\nconst client = createClient({\n  url: 'ws://127.0.0.1:7001/graphql',\n});\n\nclient.subscribe(\n  {\n    query: 'subscription { counter }',\n  },\n  {\n    next: data => {\n      console.log(data);\n    },\n    error: err => {\n      console.error(err);\n    },\n    complete: () => {},\n  }\n);\n```\n\n## HTTP 行为\n\n默认只处理 `GET` 和 `POST` 请求。其他方法请求 GraphQL 路径会返回 `405 Method Not Allowed`。\n\n```typescript\nexport const apollo = {\n  methods: ['POST'],\n};\n```\n\n如果修改了 GraphQL 路径，HTTP 和订阅默认都会使用新的路径。\n\n```typescript\nexport const apollo = {\n  path: '/api/graphql',\n};\n```\n\n## Apollo Server 配置\n\nApollo Server 专属配置放在 `apollo.apollo` 下，避免和通用 GraphQL 字段混在一起。\n\n```typescript\nexport const apollo = {\n  typeDefs,\n  resolvers,\n  apollo: {\n    introspection: true,\n    plugins: [],\n  },\n};\n```\n\n常见配置如下：\n\n| 配置项           | 说明                                      |\n| ---------------- | ----------------------------------------- |\n| `path`           | GraphQL HTTP 路径，默认为 `/graphql`      |\n| `methods`        | 允许的 HTTP 方法，默认为 `['GET', 'POST']` |\n| `typeDefs`       | 内联 GraphQL SDL                          |\n| `typePaths`      | `.graphql` 文件路径或 glob 表达式         |\n| `resolvers`      | Apollo Resolver Map                       |\n| `resolverClasses` | Midway Resolver 类列表                   |\n| `contextFactory` | 扩展每次请求的 GraphQL context            |\n| `graphiql`       | GraphiQL 开关或配置                       |\n| `subscriptions`  | GraphQL WebSocket 订阅开关或配置          |\n| `apollo`         | 透传给 Apollo Server 的配置               |\n\n## 生产建议\n\n生产环境建议显式关闭 GraphiQL，并根据安全要求决定是否开启 introspection。\n\n```typescript\nexport const apollo = {\n  graphiql: false,\n  apollo: {\n    introspection: false,\n  },\n};\n```\n\n如果需要文件上传，推荐先通过 GraphQL 获取上传凭证，再由客户端直接上传到 OSS、S3 等对象存储。GraphQL multipart upload 不作为组件默认能力开启。\n","site/docs/extensions/axios.md":"# HTTP 请求\n\n\n\n## 简单的 HTTP 请求\n\nMidway 内置了一个简单的 HTTP 请求客户端，无需引入三方包即可使用。\n\n默认 Get 请求，返回数据为 Buffer。\n\n内置的 Http 客户端只提供最简单的能力，仅满足大部分的前端接口数据获取，如需复杂的功能，比如文件上传等，请使用其他的客户端，如 fetch，axios，got 等。\n\n\n\n### 简单方法形式\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nconst result = await makeHttpRequest('http://127.1:7001/');\n\n// Buffer.isBuffer(result.data)  => true\n```\n\nGet 请求，带上 Query，返回类型为 JSON。\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nconst result = await makeHttpRequest('http://127.1:7001/', {\n  data: {\n    a: 1,\n    b: 2\n  },\n  dataType: 'json',\t// 返回的数据格式\n});\n\n// typeof result.data => 'object'\n// result.data.url => /?a=1&b=2\n```\n\n可以指定类型\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nconst result = await makeHttpRequest('http://127.1:7001/', {\n  method: 'GET',\n  dataType: 'json',\n});\n```\n\n返回 text 格式。\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nconst result = await makeHttpRequest('http://127.1:7001/', {\n  method: 'GET',\n  dataType: 'text',\n});\n```\n\nPOST 请求并返回 JSON。\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nconst result = await makeHttpRequest('http://127.1:7001/', {\n  method: 'POST',\n  data: {\n    a: 1,\n    b: 2\n  },\n  dataType: 'json',\n  contentType:'json', \t// 发送的 post 为 json\n});\n\n// result.data ...\n```\n\n:::caution\n注意，请不要在请求中直接返回 result 对象，result 对象是标准的 httpResponse，在大部分场景下无法被直接序列化，会抛出对象循环的错误。\n:::\n\n设置请求超时时间。\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nlet err;\n// 超时会报错，注意 catch\ntry {\n  const result = await makeHttpRequest('http://127.1:7001/', {\n    method: 'GET',\n    dataType: 'text',\n    timeout: 500,\n  });\n} catch (e) {\n  err = e;\n}\n```\n\n\n\n### 实例形式\n\n```typescript\nimport { HttpClient } from '@midwayjs/core';\n\nconst httpclient = new HttpClient();\nconst result = await httpclient.request('http://127.1:7001/');\n\n// Buffer.isBuffer(result.data)  => true\n```\n\n和方法形式参数相同。\n\n```typescript\nimport { HttpClient } from '@midwayjs/core';\n\nconst httpclient = new HttpClient();\nconst result = await httpclient.request('http://127.1:7001/', {\n  method: 'POST',\n  data: {\n    a: 1,\n    b: 2\n  },\n  dataType: 'json',\n  contentType:'json', \t// 发送的 post 为 json\n});\n\n// result.data ...\n```\n\n示例形式，可以复用创建出的对象，并且每次请求，都可以带上一些固定的参数，比如 header。\n\n```typescript\nimport { HttpClient } from '@midwayjs/core';\n\nconst httpclient = new HttpClient({\n  headers: {\n    'x-timeout': '5'\n  },\n  method: 'POST',\n  timeout: 2000\n});\n\n// 每次都会带上 headers\nconst result = await httpclient.request('http://127.1:7001/');\n\n```\n\n\n\n\n\n## Axios 支持\n\nMidway 包裹了 [axios](https://github.com/axios/axios) 包，使得在代码中可以简单的使用 axios 接口。\n\n和 axios 的一些关系如下：\n\n- 接口完全一致\n- 适配依赖注入写法，完整的类型定义\n- 方便实例管理和配置统一\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n\n### 安装依赖\n\n```bash\n$ npm i @midwayjs/axios@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/axios\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n### 引入组件\n\n\n首先，引入 组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as axios from '@midwayjs/axios';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    axios\t\t// 导入 axios 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n然后在业务代码中即可注入使用。\n\n\n\n### 使用默认 Axios 实例\n\n\n接口和 [axios](https://github.com/axios/axios) 一致。\n\n\n```typescript\naxios.request(config)\naxios.get(url[, config])\naxios.delete(url[, config])\naxios.head(url[, config])\naxios.options(url[, config])\naxios.post(url[, data[, config]])\naxios.put(url[, data[, config]])\naxios.patch(url[, data[, config]])\naxios.postForm(url[, data[, config]])\naxios.putForm(url[, data[, config]])\naxios.patchForm(url[, data[, config]])\n```\n\n\n使用示例：\n```typescript\nimport { HttpService } from '@midwayjs/axios';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  httpService: HttpService;\n\n  async invoke() {\n  \tconst url = 'https://midwayjs.org/resource/101010100.json';\n    const result = await this.httpService.get(url);\n    // TODO result\n  }\n}\n```\n\n\n\n### 配置默认 Axios 实例\n\n\nHttpService 实例等价于 `axios.create` ，所以可以有一些配置参数，这些参数了 axios 本身的参数相同，我们可以在 `src/config.default.ts` 中配置它。\n\n\n比如：\n```typescript\nexport default {\n  // ...\n  axios: {\n    default: {\n      // 所有实例复用的配置\n    },\n    clients: {\n      // 默认实例的配置\n      default: {\n        baseURL: 'https://api.example.com',\n        // `headers` are custom headers to be sent\n        headers: {\n          'X-Requested-With': 'XMLHttpRequest'\n        },\n        timeout: 1000, // default is `0` (no timeout)\n\n        // `withCredentials` indicates whether or not cross-site Access-Control requests\n        // should be made using credentials\n        withCredentials: false, // default\n      },\n    }\n  }\n}\n```\n更多的参数可以参考 [axios global config](https://github.com/axios/axios#config-defaults)。\n\n\n\n### 创建不同实例\n\n和其他的服务多实例相同，配置不同的 key 即可。\n\n```typescript\nexport default {\n  // ...\n  axios: {\n    default: {\n      // 所有实例复用的配置\n    },\n    clients: {\n      default: {\n        // 默认实例\n      },\n      customAxios: {\n        // 自定义实例\n      }\n    }\n  }\n}\n```\n\n使用方式如下：\n\n```typescript\nimport { HttpServiceFactory, HttpService } from '@midwayjs/axios';\nimport { InjectClient } from '@midwayjs/core';\n\n@Provide()\nexport class UserService {\n\n  @InjectClient(HttpServiceFactory, 'customAxios')\n  customAxios: HttpService;\n\n  async invoke() {\n  \tconst url = 'https://midwayjs.org/resource/101010100.json';\n    const result = await this.customAxios.get(url);\n    // TODO result\n  }\n}\n```\n\n\n\n### 配置全局拦截器\n\n如果使用的是默认的 Axios 实例，可以如下配置。\n\n```javascript\nimport { Configuration, IMidwayContainer } from '@midwayjs/core';\nimport * as axios from '@midwayjs/axios';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    axios\t\t// 导入 axios 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n\n  async onReady(container: IMidwayContainer) {\n  \tconst httpService = await container.getAsync(axios.HttpService);\n    httpService.interceptors.request.use(\n      config => {\n        // Do something before request is sent\n        return config;\n      },\n      error => {\n        // Do something with request error\n        return Promise.reject(error);\n      }\n    );\n  }\n}\n```\n\n如果要给其他实例配置，可以参考下面的代码。\n\n```typescript\nimport { Configuration, IMidwayContainer } from '@midwayjs/core';\nimport * as axios from '@midwayjs/axios';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    axios\t\t// 导入 axios 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n\n  async onReady(container: IMidwayContainer) {\n  \tconst httpServiceFactory = await container.getAsync(axios.HttpServiceFactory);\n    const customAxios = httpServiceFactory.get('customAxios');\n    customAxios.interceptors.request.use(\n      config => {\n        //...\n      },\n      error => {\n        //...\n      }\n    );\n  }\n}\n```\n\n### 扩展 AxiosRequestConfig 类型\n\n当前 Axios 库中 [issue](https://github.com/axios/axios/issues?q=is%3Aissue%20state%3Aopen%20AxiosRequestConfig) 有不少扩展 `AxiosRequestConfig` 类型的需求，但是 axios 本身并不支持扩展这个类型，其中的泛型仅限于设置 data 属性的类型。\n\nMidway 中提供了 `AxiosRequestConfig` 的扩展，可以在组件层面增加新的属性。\n\n```typescript\n// interface.ts\nimport '@midwayjs/axios';\n\ndeclare module '@midwayjs/axios/dist/interface' {\n  interface AxiosRequestConfig {\n    retryDelay?: number;\n    retry?: number;\n  }\n}\n```\n\n\n### 直接使用 Axios\n\n`@midayjs/axios`导出了原始的`axios`实例，在非应用环境中可以直接使用。\n\n```typescript\nimport { Axios } from '@midwayjs/axios';\nimport { ReadStream, createWriteStream } from 'fs';\nimport { finished } from 'stream/promises';\n\nasync function download(url: string, filename: string) {\n  const writer = await createWriteStream(filename);\n  const res = Axios.get<ReadStream>(url, {\n    responseType: 'stream',\n  });\n  res.data.pipe(writer);\n  await finished(writer);\n  return res;\n}\n```\n","site/docs/extensions/bull.md":"# 任务队列\n\n队列是一种强大的设计模式，可帮助您应对常见的应用程序扩展和性能挑战。队列可以帮助您解决的一些问题。\n\n示例如下：\n\n-  平滑处理峰值。可以在任意时间启动资源密集型任务，然后将这些任务添加到队列中，而不是同步执行。让任务进程以受控方式从队列中提取任务。也可以轻松添加新的队列消费者以扩展后端任务处理。\n- 分解可能会阻塞 Node.js 事件循环的单一任务。比如用户请求需要像音频转码这样的 CPU 密集型工作，就可以将此任务委托给其他进程，从而释放面向用户的进程以保持响应。\n- 提供跨各种服务的可靠通信渠道。例如，您可以在一个进程或服务中排队任务（作业），并在另一个进程或服务中使用它们。在任何流程或服务的作业生命周期中完成、错误或其他状态更改时，您都可以收到通知（通过监听状态事件）。当队列生产者或消费者失败时，它们的状态被保留，并且当节点重新启动时任务处理可以自动重新启动。\n\nMidway 提供了 @midwayjs/bull 包作为 [Bull](https://github.com/OptimalBits/bull) 之上的抽象/包装器，[Bull](https://github.com/OptimalBits/bull) 是一种流行的、受良好支持的、高性能的基于 Node.js 的队列系统实现。该软件包可以轻松地将 Bull Queues 以友好的方式集成到您的应用程序中。\n\nBull 使用 Redis 来保存作业数据，在使用 Redis 时，Queue 架构是完全分布式，和平台无关。例如，您可以在一个（或多个）节点（进程）中运行一些 Queue 生产者、消费者，而在其他节点上的运行其他生产者和消费者。\n\n本章介绍 @midwayjs/bull 包。我们还建议阅读 [Bull 文档](https://github.com/OptimalBits/bull/blob/master/REFERENCE.md) 以了解更多背景和具体实施细节。\n\n:::tip\n\n- 1、从 v3.6.0 开始，原有任务调度 `@midwayjs/task` 模块废弃，如果查询历史文档，请参考 [这里](../legacy/task)。\n- 2、bull 是一个分布式任务管理系统，必须依赖 redis\n\n:::\n\n\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n\n\n## 安装组件\n\n```bash\n$ npm i @midwayjs/bull@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/bull\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 使用组件\n\n将 bull 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as bull from '@midwayjs/bull';\n\n@Configuration({\n  imports: [\n    // ...\n    bull\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n\n\n## 一些概念\n\nBull 将整个队列分为三个部分\n\n- 1、Queue 队列，管理任务\n- 2、Job，每个任务对象，可以对任务进行启停控制\n- 3、Processor，任务处理，实际的逻辑执行部分\n\n\n\n## 基础配置\n\nbull 是一个分布式任务管理器，强依赖于 redis，在 `config.default.ts` 文件中配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  bull: {\n    // 默认的队列配置\n    defaultQueueOptions: {\n      redis: `redis://127.0.0.1:32768`,\n    }\n  },\n}\n```\n\n有账号密码情况：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  bull: {\n    defaultQueueOptions: {\n      redis: {\n        port: 6379,\n        host: '127.0.0.1',\n        password: 'foobared',\n      },\n    }\n  },\n}\n```\n\n所有的队列都会复用该配置。\n\n\n\n## 编写任务处理器\n\n使用 `@Processor` 装饰器装饰一个类，用于快速定义一个任务处理器（这里我们不使用 Job，避免后续的歧义）。\n\n`@Processor` 装饰器需要传递一个 Queue （队列）的名字，在框架启动时，如果没有名为 `test` 的队列，则会自动创建。\n\n比如，我们在 `src/queue/test.queue.ts` 文件中编写如下代码。\n\n```typescript\n// src/queue/test.queue.ts\nimport { Processor, IProcessor } from '@midwayjs/bull';\n\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  async execute() {\n    // ...\n  }\n}\n```\n\n在启动时，框架会自动查找并初始化上述处理器代码，同时自动创建一个名为 `test` 的 Queue。\n\n\n\n\n\n## 执行任务\n\n当定义完 Processor 之后，由于并未指定 Processor 如何执行，我们还需要手动执行它。\n\n通过获取对应的队列，我们可以很方便的来执行任务。\n\n\n\n### 手动执行任务\n\n比如，我们可以在项目启动后执行。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bull from '@midwayjs/bull';\n\n@Configuration({\n  imports: [\n    // ...\n    bull\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullFramework: bull.Framework;\n\n  //...\n\n  async onServerReady() {\n    // 获取 Processor 相关的队列\n    const testQueue = this.bullFramework.getQueue('test');\n    // 立即添加这个任务\n    await testQueue?.addJobToQueue();\n  }\n}\n```\n\n\n\n### 增加执行参数\n\n我们也可以在执行时，附加一些默认参数。\n\n```typescript\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  async execute(params) {\n    // params.aaa => 1\n  }\n}\n\n\n// invoke\nconst testQueue = this.bullFramework.getQueue('test');\n// 立即添加这个任务\nawait testQueue?.addJobToQueue({\n  aaa: 1,\n  bbb: 2,\n});\n```\n\n\n\n### 任务状态和管理\n\n执行 `addJobToQueue` 后，我们可以获取到一个 `Job` 对象。\n\n```typescript\n// invoke\nconst testQueue = this.bullFramework.getQueue('test');\nconst job = await testQueue?.addJobToQueue();\n```\n\n通过这个 Job 对象，我们可以做进度管理。\n\n```typescript\n// 更新进度\nawait job.progress(60);\n// 获取进度\nconst progress = await job.process();\n// => 60\n```\n\n获取任务状态。\n\n```typescript\nconst state = await job.getState();\n// state => 'delayed' 延迟状态\n// state => 'completed' 完成状态\n```\n\n更多的 Job API，请查看 [文档](https://github.com/OptimalBits/bull/blob/develop/REFERENCE.md)。\n\n\n\n### 延迟执行\n\n执行任务时，也有一些额外的选项。\n\n比如，延迟 1s 执行。\n\n```typescript\nconst testQueue = this.bullFramework.getQueue('test');\n// 立即添加这个任务\nawait testQueue?.addJobToQueue({}, { delay: 1000 });\n```\n\n\n\n### 中间件和错误处理\n\nBull 组件包含可以独立启动的 Framework，有着自己的 App 对象和 Context 结构。\n\n我们可以对 bull 的 App 配置独立的中间件和错误过滤器。\n\n```typescript\n@Configuration({\n  imports: [\n    // ...\n    bull\n  ]\n})\nexport class MainConfiguration {\n\n  @App('bull')\n  bullApp: bull.Application;\n\n  //...\n\n  async onReady() {\n    this.bullApp.useMiddleare( /*中间件*/);\n    this.bullApp.useFilter( /*过滤器*/);\n  }\n}\n```\n\n\n\n### 上下文\n\n任务处理器执行是在请求作用域中，其有着特殊的 Context 对象结构。\n\n```typescript\nexport interface Context extends IMidwayContext {\n  jobId: JobId;\n  job: Job,\n  from: new (...args) => IProcessor;\n}\n```\n\n我们可以直接从 ctx 中访问当前的 Job 对象。\n\n```typescript\n// src/queue/test.queue.ts\nimport { Processor, IProcessor, Context } from '@midwayjs/bull';\n\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n\n  @Inject()\n  ctx: Context;\n\n  async execute() {\n    // ctx.jobId => xxxx\n  }\n}\n```\n\n\n\n### 更多任务选项\n\n除了上面的 delay 之外，还有更多的执行选项。\n\n| 选项             | 类型                  | 描述                                                         |\n| ---------------- | --------------------- | ------------------------------------------------------------ |\n| priority         | number                | 可选的优先级值。范围从 1（最高优先级）到 MAX_INT（最低优先级）。请注意，使用优先级对性能有轻微影响，因此请谨慎使用。 |\n| delay            | number                | 等待可以处理此作业的时间量（毫秒）。请注意，为了获得准确的延迟，服务器和客户端都应该同步它们的时钟。 |\n| attempts         | number                | 在任务完成之前尝试尝试的总次数。                             |\n| repeat           | RepeatOpts            | 根据 cron 规范的重复任务配置，更多可以查看 [RepeatOpts](https://github.com/OptimalBits/bull/blob/master/REFERENCE.md#queueadd)，以及下面的重复任务介绍。 |\n| backoff          | number \\| BackoffOpts | 任务失败时自动重试的回退设置。请参阅 [BackoffOpts](https://github.com/OptimalBits/bull/blob/master/REFERENCE.md#queueadd)。 |\n| lifo             | boolean               | 如果为 true，则将任务添加到队列的右端而不是左端（默认为 false）。 |\n| timeout          | number                | 任务因超时错误而失败的毫秒数。                               |\n| jobId            | number \\| string      | 覆盖任务 id - 默认情况下，任务 id 是唯一整数，但您可以使用此设置覆盖它。如果您使用此选项，则由您来确保 jobId 是唯一的。如果您尝试添加一个 id 已经存在的任务，它将不会被添加。 |\n| removeOnComplete | boolean \\| number     | 如果为 true，则在成功完成后删除任务。如果设置数字，则为指定要保留的任务数量。默认行为是任务信息保留在已完成列表中。 |\n| removeOnFail     | boolean \\| number     | 如果为 true，则在所有尝试后都失败时删除任务。如果设置数字，指定要保留的任务数量。默认行为是将任务信息保留在失败列表中。 |\n| stackTraceLimit  | number                | 限制将在堆栈跟踪中记录的堆栈跟踪行的数量。                   |\n\n\n\n## 重复执行的任务\n\n除了手动执行的方式，我们也可以通过 `@Processor` 装饰器的参数，快速配置任务的重复执行。\n\n```typescript\nimport { Processor, IProcessor } from '@midwayjs/bull';\nimport { FORMAT } from '@midwayjs/core';\n\n@Processor('test', {\n  repeat: {\n    cron: FORMAT.CRONTAB.EVERY_PER_5_SECOND\n  }\n})\nexport class TestProcessor implements IProcessor {\n  @Inject()\n  logger;\n\n  async execute() {\n    // ...\n  }\n}\n```\n\n\n\n## 常用 Cron 表达式\n\n关于 Cron 表达式，格式如下。\n\n```typescript\n*    *    *    *    *    *\n┬    ┬    ┬    ┬    ┬    ┬\n│    │    │    │    │    |\n│    │    │    │    │    └ day of week (0 - 7) (0 or 7 is Sun)\n│    │    │    │    └───── month (1 - 12)\n│    │    │    └────────── day of month (1 - 31)\n│    │    └─────────────── hour (0 - 23)\n│    └──────────────────── minute (0 - 59)\n└───────────────────────── second (0 - 59, optional)\n```\n\n\n\n常见表达式：\n\n- 每隔5秒执行一次：`*/5 * * * * *`\n- 每隔1分钟执行一次：`0 */1 * * * *`\n- 每小时的20分执行一次：`0 20 * * * *`\n- 每天 0 点执行一次：`0 0 0 * * *`\n- 每天的两点35分执行一次：`0 35 2 * * *`\n\n可以使用 [在线工具](https://cron.qqe2.com/) 执行确认下一次执行的时间。\n\nMidway 在框架侧提供了一些常用的表达式，放在 `@midwayjs/core` 中供大家使用。\n\n```typescript\nimport { FORMAT } from '@midwayjs/core';\n\n// 每分钟执行的 cron 表达式\nFORMAT.CRONTAB.EVERY_MINUTE\n```\n\n\n\n内置的还有一些其他的表达式。\n\n| 表达式                         | 对应时间        |\n| ------------------------------ | --------------- |\n| CRONTAB.EVERY_SECOND           | 每秒钟          |\n| CRONTAB.EVERY_MINUTE           | 每分钟          |\n| CRONTAB.EVERY_HOUR             | 每小时整点      |\n| CRONTAB.EVERY_DAY              | 每天 0 点       |\n| CRONTAB.EVERY_DAY_ZERO_FIFTEEN | 每天 0 点 15 分 |\n| CRONTAB.EVERY_DAY_ONE_FIFTEEN  | 每天 1 点 15 分 |\n| CRONTAB.EVERY_PER_5_SECOND     | 每隔 5 秒       |\n| CRONTAB.EVERY_PER_10_SECOND    | 每隔 10 秒      |\n| CRONTAB.EVERY_PER_30_SECOND    | 每隔 30 秒      |\n| CRONTAB.EVERY_PER_5_MINUTE     | 每隔 5 分钟     |\n| CRONTAB.EVERY_PER_10_MINUTE    | 每隔 10 分钟    |\n| CRONTAB.EVERY_PER_30_MINUTE    | 每隔 30 分钟    |\n\n\n\n## 高级配置\n\n\n\n### 清理之前的任务\n\n在默认情况下，框架会自动清理前一次未调度的 **重复执行任务**，保持每一次的重复执行的任务队列为最新。如果在某些环境不需要清理，可以单独关闭。\n\n比如你不需要清理重复：\n\n```typescript\n// src/config/config.prod.ts\nexport default {\n  // ...\n  bull: {\n    clearRepeatJobWhenStart: false,\n  },\n}\n```\n\n:::tip\n\n如果不清理，如果前一次队列为 10s 执行，现在修改为 20s 执行，则两个定时都会存储在 Redis 中，导致代码重复执行。\n\n在日常的开发中，如果不清理，很容易出现代码重复执行这个问题。但是在集群部署的场景，多台服务器轮流重启的情况下，可能会导致定时任务被意外清理，请评估开关的时机。\n\n:::\n\n\n\n也可以在启动时手动清理所有任务。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, App, Inject } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport { join } from 'path';\nimport * as bull from '@midwayjs/bull';\n\n@Configuration({\n  imports: [koa, bull],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: koa.Application;\n\n  @Inject()\n  bullFramework: bull.Framework;\n\n  async onReady() {\n    // 在这个阶段，装饰器队列还未创建，使用 API 提前手动创建队列，装饰器会复用同名队列\n    const queue = this.bullFramework.createQueue('user');\n    // 通过队列手动执行清理\n    await queue.obliterate({ force: true });\n  }\n}\n```\n\n\n\n\n\n### 清理任务历史记录\n\n当开启 Redis 后，默认情况下，bull 会记录所有的成功和失败的任务 key，这可能会导致 redis 的 key 暴涨，我们可以配置成功或者失败后清理的选项。\n\n默认情况下\n\n- 成功时保留的任务记录为 3 条\n- 失败保留的任务记录为 10 条\n\n也可以通过参数进行配置。\n\n比如在装饰器配置。\n\n```typescript\nimport { FORMAT } from '@midwayjs/core';\nimport { IProcessor, Processor } from '@midwayjs/bull';\n\n@Processor('user', {\n  repeat: {\n    cron: FORMAT.CRONTAB.EVERY_MINUTE,\n  },\n  removeOnComplete: 3,\t// 成功后移除任务记录，最多保留最近 3 条记录\n  removeOnFail: 10,\t    // 失败后移除任务记录\n})\nexport class UserService implements IProcessor {\n  execute(data: any) {\n    // ...\n  }\n}\n\n```\n\n也可以在全局 config 中配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  bull: {\n    defaultQueueOptions: {\n      // 默认的任务配置\n      defaultJobOptions: {\n        // 保留 10 条记录\n        removeOnComplete: 10,\n      },\n    },\n  },\n}\n```\n\n\n\n\n\n### Redis 集群\n\n可以使用 bull 提供的 `createClient` 方式来接入自定义的 redis 实例，这样你可以接入 Redis 集群。\n\n比如：\n\n```typescript\n// src/config/config.default\nimport Redis from 'ioredis';\n\nconst clusterOptions = {\n  enableReadyCheck: false,  // 一定要是false\n  retryDelayOnClusterDown: 300,\n  retryDelayOnFailover: 1000,\n  retryDelayOnTryAgain: 3000,\n  slotsRefreshTimeout: 10000,\n  maxRetriesPerRequest: null  // 一定要是null\n}\n\nconst redisClientInstance = new Redis.Cluster([\n  {\n    port: 7000,\n    host: '127.0.0.1'\n  },\n  {\n    port: 7002,\n    host: '127.0.0.1'\n  },\n], clusterOptions);\n\nexport default {\n  bull: {\n    defaultQueueOptions: {\n      createClient: (type, opts) => {\n        return redisClientInstance;\n      },\n      // 这些任务存储的 key，都是相同开头，以便区分用户原有 redis 里面的配置\n      prefix: '{midway-bull}',\n    },\n  }\n}\n```\n\n\n\n## 队列管理\n\n队列是廉价的，每个 Job 都会绑定一个队列，在一些情况下，我们也可以手动对队列进行管理操作。\n\n\n\n### 手动创建队列\n\n除了使用 `@Processor` 简单定义队列，我们还可以使用 API 进行创建。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bull from '@midwayjs/bull';\n\n@Configuration({\n  imports: [\n    // ...\n    bull\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullFramework: bull.Framework;\n\n  async onReady() {\n    const testQueue = this.bullFramework.createQueue('test', {\n      redis: {\n        port: 6379,\n        host: '127.0.0.1',\n        password: 'foobared',\n      },\n      prefix: '{midway-bull}',\n    });\n\n    // ...\n  }\n}\n```\n\n通过 `createQueue` 手动创建队列后，队列依旧会自动保存。如果在启动时 `@Processor` 使用了该队列名，则会自动使用已经创建好的队列。\n\n比如：\n\n```typescript\n// 会自动使用上面手动创建的同名队列\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  async execute(params) {\n  }\n}\n```\n\n\n\n### 获取队列\n\n我们可以简单的根据队列名获取队列。\n\n```typescript\n const testQueue = bullFramework.getQueue('test');\n```\n\n也可以通过装饰器来获取。\n\n```typescript\nimport { InjectQueue, BullQueue } from '@midwayjs/bull';\nimport { Provide } from '@midwayjs/core';\n\n@Provide()\nexport class UserService {\n  @InjectQueue('test')\n  testQueue: BullQueue;\n\n  async invoke() {\n    await this.testQueue.pause();\n    // ...\n  }\n}\n```\n\n\n\n### 队列常用操作\n\n暂停队列。\n\n```typescript\nawait testQueue.pause();\n```\n\n继续队列。\n\n```typescript\nawait testQueue.resume();\n```\n\n队列事件。\n\n```typescript\n// Local events pass the job instance...\ntestQueue.on('progress', function (job, progress) {\n  console.log(`Job ${job.id} is ${progress * 100}% ready!`);\n});\n\ntestQueue.on('completed', function (job, result) {\n  console.log(`Job ${job.id} completed! Result: ${result}`);\n  job.remove();\n});\n```\n\n完整队列 API 请参考 [这里](https://github.com/OptimalBits/bull/blob/develop/REFERENCE.md)。\n\n\n\n## 组件日志\n\n组件有着自己的日志，默认会将 `ctx.logger` 记录在 `midway-bull.log` 中。\n\n我们可以单独配置这个 logger 对象。\n\n```typescript\nexport default {\n  midwayLogger: {\n    clients: {\n      // ...\n      bullLogger: {\n        fileLogName: 'midway-bull.log',\n      },\n    },\n  },\n}\n```\n\n这个日志的输出格式，我们也可以单独配置。\n\n```typescript\nexport default {\n  bull: {\n    // ...\n    contextLoggerFormat: info => {\n      const { jobId, from } = info.ctx;\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} [${jobId} ${from.name}] ${info.message}`;\n    },\n  }\n}\n```\n\n\n\n## 关于 Redis 版本\n\n请尽可能选择最新的版本（ >=5 ），目前在低版本 redis 上有发现定时任务创建失败的问题。\n\n\n\n## Bull UI\n\n在分布式场景中，我们可以资利用 Bull UI 来简化管理。\n\n和 bull 组件类似，需要独立安装和启用。\n\n```bash\n$ npm i @midwayjs/bull-board@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/bull-board\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n将 bull-board 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as bull from '@midwayjs/bull';\nimport * as bullBoard from '@midwayjs/bull-board';\n\n@Configuration({\n  imports: [\n    // ...\n    bull,\n    bullBoard,\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n默认的访问路径为：`http://127.1:7001/ui`。\n\n效果如下：\n\n![](https://img.alicdn.com/imgextra/i2/O1CN01j4wEFb1UacPxA06gs_!!6000000002534-2-tps-1932-1136.png)\n\n可以通过配置进行基础路径的修改。\n\n```typescript\n// src/config/config.prod.ts\nexport default {\n  // ...\n  bullBoard: {\n    basePath: '/ui',\n  },\n}\n```\n\n此外，组件提供了 `BullBoardManager` ，可以添加动态创建的队列。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bull from '@midwayjs/bull';\nimport * as bullBoard from '@midwayjs/bull-board';\n\n@Configuration({\n  imports: [\n    // ...\n    bull,\n    bullBoard\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullFramework: bull.Framework;\n  \n  @Inject()\n  bullBoardManager: bullBoard.BullBoardManager;\n\n  async onServerReady() {\n    const testQueue = this.bullFramework.createQueue('test', {\n      // ...\n    });\n\n    this.bullBoardManager.addQueue(new bullBoard.BullAdapter(testQueue);\n  }\n}\n```\n\n\n\n\n\n## 常见问题\n\n### 1、EVALSHA 错误\n\n![image.png](https://img.alicdn.com/imgextra/i4/O1CN01KfjCKT1yypmNPDkIL_!!6000000006648-2-tps-3540-102.png)\n\n这个问题基本明确，问题会出现在 redis 的集群版本上。\n\n原因是 redis 会对 key 做 hash 来确定存储的 slot，集群下这一步 @midwayjs/bull 的 key 命中了不同的 slot。\n\n解决方案： task 里的 prefix 配置用 {} 包括，强制 redis 只计算 {} 里的hash，例如 `prefix: '{midway-task}'`。\n\n### 2、EVAL inside MULTI is not allowed 错误\n\n表现为 `queue.createBulk()`、`job.moveToFailed()` 等任务队列 API 调用无效，并出现下面的错误。\n\n```\nReplyError: EXECABORT Transaction discarded because of previous errors.\n    at parseError (<project_dir>/node_modules/redis-parser/lib/parser.js:179:12)\n    at parseType (<project_dir>/node_modules/redis-parser/lib/parser.js:302:14) {\n  command: { name: 'exec', args: [] },\n  previousErrors: [\n    ReplyError: ERR 'EVAL' inside MULTI is not allowed\n        at parseError (<project_dir>/node_modules/redis-parser/lib/parser.js:179:12)\n        at parseType (<project_dir>/node_modules/redis-parser/lib/parser.js:302:14) {\n      command: [Object]\n    }\n  ]\n}\n```\n\n:::tip\n\n常出现于使用阿里云 Redis 服务。\n\n:::\n\n由于这些 API 依赖的 Redis Lua 脚本中使用了 EVAL 或者 EVALSHA，阿里云 Redis 使用代理模式连接时，会对 Lua 脚本调用做额外限制，包括 [不允许在 MULTI 事务中执行 EVAL 命令](https://help.aliyun.com/zh/redis/support/usage-of-lua-scripts?#section-8f7-qgv-dlv)，文档中还提到可以通过参数配置 script_check_enable 关闭这一校验，但是验证无效。\n\n解决方案：\n\n* 1、在阿里云控制台操作开启直连地址，将服务切换到直连模式\n* 2、客户端切换成集群模式，参考上述「Redis 集群」章节，切换配置方式\n","site/docs/extensions/bullmq.md":"# 任务队列\n\n队列是一种强大的设计模式，可帮助您应对常见的应用程序扩展和性能挑战。队列可以帮助您解决的一些问题：\n\n- 平滑处理峰值。可以在任意时间启动资源密集型任务，然后将这些任务添加到队列中，而不是同步执行。让任务进程以受控方式从队列中提取任务。也可以轻松添加新的队列消费者以扩展后端任务处理。\n- 分解可能会阻塞 Node.js 事件循环的单一任务。比如用户请求需要像音频转码这样的 CPU 密集型工作，就可以将此任务委托给其他进程，从而释放面向用户的进程以保持响应。\n- 提供跨各种服务的可靠通信渠道。例如，您可以在一个进程或服务中排队任务（作业），并在另一个进程或服务中使用它们。在任何流程或服务的作业生命周期中完成、错误或其他状态更改时，您都可以收到通知（通过监听状态事件）。当队列生产者或消费者失败时，它们的状态被保留，并且当节点重新启动时任务处理可以自动重新启动。\n\nMidway 提供了 `@midwayjs/bullmq` 包作为 [BullMQ](https://github.com/taskforcesh/bullmq) 之上的抽象/包装器。BullMQ 是 Bull 的下一代实现，提供了更好的性能和更多的功能。该软件包可以轻松地将 BullMQ 以友好的方式集成到您的应用程序中。\n\nBullMQ 使用 Redis 来保存作业数据，在使用 Redis 时，Queue 架构是完全分布式，和平台无关。例如，您可以在一个（或多个）节点（进程）中运行一些 Queue 生产者、消费者，而在其他节点上的运行其他生产者和消费者。\n\n:::tip\nBullMQ 是一个分布式任务管理系统，必须依赖 redis\n:::\n\n:::caution\n由于 BullMQ 是 Bull 的升级版，从 v3.20 开始，将由 BullMQ 替代 Bull 组件，如需使用 Bull 组件，请参考 [Bull 文档](./bull)\n:::\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n\n## 安装组件\n\n```bash\n$ npm i @midwayjs/bullmq@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/bullmq\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n## 使用组件\n\n将 bullmq 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as bullmq from '@midwayjs/bullmq';\n\n@Configuration({\n  imports: [\n    // ...\n    bullmq\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n## 一些概念\n\nBullMQ 将整个队列分为以下几个部分：\n\n- Queue：队列，管理任务\n- Job：每个任务对象，可以对任务进行启停控制\n- Worker：任务处理器，实际的逻辑执行部分\n- QueueEvents：队列事件，用于监听任务状态变化\n- FlowProducer：任务流生产者，用于创建任务依赖关系\n\n## 基础配置\n\nbullmq 是一个分布式任务管理器，强依赖于 redis，在 `config.default.ts` 文件中配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  bullmq: {\n    defaultConnection: {\n      host: '127.0.0.1',\n      port: 6379,\n    },\n    // 可选，队列前缀\n    defaultPrefix: '{midway-bullmq}',\n  },\n}\n```\n\n有账号密码情况：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  bullmq: {\n    defaultConnection: {\n      port: 6379,\n      host: '127.0.0.1',\n      password: 'foobared',\n    }\n  },\n}\n```\n\n所有的队列、任务处理器、队列事件、任务流都会复用该配置。\n\n## 编写任务处理器\n\n使用 `@Processor` 装饰器装饰一个类，用于快速定义一个任务处理器。\n\n`@Processor` 装饰器需要传递一个 Queue（队列）的名字，在框架启动时，如果没有名为 `test` 的队列，则会自动创建。\n\n比如，我们在 `src/processor/test.processor.ts` 文件中编写如下代码。\n\n```typescript\nimport { Processor, IProcessor } from '@midwayjs/bullmq';\n\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  async execute(data: any) {\n    // 处理任务逻辑\n    console.log('processing job:', data);\n  }\n}\n```\n\n## 执行任务\n\n当定义完 Processor 之后，由于并未指定 Processor 如何执行，我们还需要手动执行它。\n\n### 手动执行任务\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bullmq from '@midwayjs/bullmq';\n\n@Configuration({\n  imports: [\n    bullmq\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullmqFramework: bullmq.Framework;\n\n  async onServerReady() {\n    // 获取 Processor 相关的队列\n    const testQueue = this.bullmqFramework.getQueue('test');\n    // 立即添加这个任务\n    await testQueue?.addJobToQueue();\n  }\n}\n```\n\n### 增加执行参数\n\n我们也可以在执行时，附加一些参数。\n\n```typescript\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  async execute(params) {\n    // params.name => 'harry'\n  }\n}\n\n// invoke\nconst testQueue = this.bullmqFramework.getQueue('test');\nawait testQueue?.addJobToQueue({\n  aaa: 1,\n  bbb: 2,\n});\n```\n\n### 任务状态和管理\n\n执行 `addJobToQueue` 后，我们可以获取到一个 `Job` 对象。\n\n```typescript\nconst testQueue = this.bullmqFramework.getQueue('test');\nconst job = await testQueue?.addJobToQueue();\n\n// 更新进度\nawait job.updateProgress(60);\n// 获取进度\nconst progress = await job.progress;\n// => 60\n\n// 获取任务状态\nconst state = await job.getState();\n// state => 'delayed' 延迟状态\n// state => 'completed' 完成状态\n// state => 'failed' 失败状态\n```\n\n### 延迟执行\n\n执行任务时，也有一些额外的选项。\n\n比如，延迟 1s 执行。\n\n```typescript\nconst testQueue = this.bullmqFramework.getQueue('test');\nawait testQueue?.addJobToQueue({}, { delay: 1000 });\n```\n\n### 任务重试\n\nBullMQ 支持任务失败重试机制。\n\n```typescript\nconst testQueue = this.bullmqFramework.getQueue('test');\nawait testQueue?.addJobToQueue({}, {\n  attempts: 3,  // 最多重试 3 次\n  backoff: {    // 重试策略\n    type: 'exponential',  // 指数退避\n    delay: 1000          // 初始延迟 1 秒\n  }\n});\n```\n\n### 任务优先级\n\n可以为任务设置优先级，优先级高的任务会优先执行。\n\n```typescript\nconst testQueue = this.bullmqFramework.getQueue('test');\n// priority 值越大优先级越高\nawait testQueue?.addJobToQueue({ priority: 1 }, { priority: 3 }); // 高优先级\nawait testQueue?.addJobToQueue({ priority: 2 }, { priority: 2 }); // 中优先级\nawait testQueue?.addJobToQueue({ priority: 3 }, { priority: 1 }); // 低优先级\n```\n\n### 中间件和错误处理\n\nBullMQ 组件包含可以独立启动的 Framework，有着自己的 App 对象和 Context 结构。\n\n我们可以对 bullmq 的 App 配置独立的中间件和错误过滤器。\n\n```typescript\n@Configuration({\n  imports: [\n    bullmq\n  ]\n})\nexport class MainConfiguration {\n\n  @App('bullmq')\n  bullmqApp: bullmq.Application;\n\n  async onReady() {\n    this.bullmqApp.useMiddleware(/*中间件*/);\n    this.bullmqApp.useFilter(/*过滤器*/);\n  }\n}\n```\n\n### 上下文\n\n任务处理器执行是在请求作用域中，其有着特殊的 Context 对象结构。\n\n```typescript\nexport interface Context extends IMidwayContext {\n  jobId: string;\n  job: Job;\n  token?: string;\n  from: new (...args) => IProcessor;\n}\n```\n\n我们可以直接从 ctx 中访问当前的 Job 对象。\n\n```typescript\nimport { Processor, IProcessor, Context } from '@midwayjs/bullmq';\n\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  @Inject()\n  ctx: Context;\n\n  async execute(data: any) {\n    // ctx.jobId => 当前任务ID\n    // ctx.job => 当前任务对象\n  }\n}\n```\n\n## 重复执行的任务\n\n除了手动执行的方式，我们也可以通过 `@Processor` 装饰器的参数，快速配置任务的重复执行。\n\n```typescript\nimport { Processor, IProcessor } from '@midwayjs/bullmq';\nimport { FORMAT } from '@midwayjs/core';\n\n@Processor('test', {\n  repeat: {\n    pattern: FORMAT.CRONTAB.EVERY_PER_5_SECOND\n  }\n})\nexport class TestProcessor implements IProcessor {\n  async execute() {\n    // 每 5 秒执行一次\n  }\n}\n```\n\n## 高级功能\n\n### 任务流（Flow Producer）\n\nBullMQ 支持创建任务依赖关系，形成任务流。\n\n```typescript\nconst flowProducer = bullmqFramework.createFlowProducer({}, 'test-flow');\n\n// 创建任务流\nawait flowProducer.add({\n  name: 'flow-test',\n  queueName: 'flow-queue-1',\n  data: { value: 1 },\n  children: [\n    {\n      name: 'child-job',\n      queueName: 'flow-queue-2',\n      data: { value: 2 }\n    }\n  ]\n});\n```\n\n### 队列事件\n\nBullMQ 提供了丰富的事件系统，可以监听任务的各种状态变化。\n\n```typescript\nconst eventQueue = bullmqFramework.createQueue('event-queue');\nconst queueEvents = eventQueue.createQueueEvents();\n\n// 监听任务完成事件\nqueueEvents.on('completed', ({ jobId }) => {\n  console.log(`Job ${jobId} completed!`);\n});\n\n// 监听任务失败事件\nqueueEvents.on('failed', ({ jobId, failedReason }) => {\n  console.log(`Job ${jobId} failed: ${failedReason}`);\n});\n```\n\n### 清理任务历史记录\n\n当开启 Redis 后，默认情况下，bullmq 会记录所有的成功和失败的任务 key，这可能会导致 redis 的 key 暴涨，我们可以配置成功或者失败后清理的选项。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  bullmq: {\n    defaultQueueOptions: {\n      defaultJobOptions: {\n        removeOnComplete: 3,  // 成功后只保留最近 3 条记录\n        removeOnFail: 10,     // 失败后只保留最近 10 条记录\n      }\n    }\n  }\n}\n```\n\n### Redis 集群\n\nbullmq 可以指定 connection 实例，你可以将自己创建的 Redis 实例配置到 `defaultConnection` 中，这样就可以接入 Redis 集群。\n\n```typescript\n// src/config/config.default.ts\nimport Redis from 'ioredis';\n\nconst clusterOptions = {\n  enableReadyCheck: false,\n  retryDelayOnClusterDown: 300,\n  retryDelayOnFailover: 1000,\n  retryDelayOnTryAgain: 3000,\n  slotsRefreshTimeout: 10000,\n  maxRetriesPerRequest: null\n}\n\nconst redisClientInstance = new Redis.Cluster([\n  {\n    port: 7000,\n    host: '127.0.0.1'\n  },\n  {\n    port: 7002,\n    host: '127.0.0.1'\n  },\n], clusterOptions);\n\nexport default {\n  bullmq: {\n    defaultConnection: redisClientInstance,\n    defaultPrefix: '{midway-bullmq}',\n  }\n}\n```\n\n### 获取任务处理器\n\n通过内置的方法，我们可以获取到原始的任务处理器（Worker）。\n\n可以启动任意多个 Worker 监听同一个队列，在这种情况下 BullMQ 会把队列中的任务轮流（round-robin）分配给这些 Worker。\n\n当队列中有任务被添加时，BullMQ 会在这两个 Worker 之间自动分配任务。\n\n我们可以通过方法 `getWorker` 获取到首个监听的 Worker 对象，也可以通过方法 `getWorkers` 获取到该队列监听的所有 Worker 对象。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bullmq from '@midwayjs/bullmq';\n\n@Configuration({\n  imports: [\n    // ...\n    bullmq,\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullmqFramework: bullmq.Framework;\n\n  async onServerReady() {\n    const testWorker = this.bullmqFramework.getWorker('test');\n    const allWorkers = this.bullmqFramework.getWorkers('test');\n  }\n}\n\n```\n\n\n## 组件日志\n\n组件有着自己的日志，默认会将 `ctx.logger` 记录在 `midway-bullmq.log` 中。\n\n我们可以单独配置这个 logger 对象。\n\n```typescript\nexport default {\n  midwayLogger: {\n    clients: {\n      bullMQLogger: {\n        fileLogName: 'midway-bullmq.log',\n      },\n    },\n  },\n}\n```\n\n这个日志的输出格式，我们也可以单独配置。\n\n```typescript\nexport default {\n  bullmq: {\n    contextLoggerFormat: info => {\n      const { jobId, from } = info.ctx;\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} [${jobId} ${from.name}] ${info.message}`;\n    },\n  }\n}\n```\n\n## BullMQ 原始对象\n\n组件导出了 BullMQ 的原始对象，可以进行更多的操作。\n\n```typescript\nimport { BullMQ } from '@midwayjs/bullmq';\n```\n\n你可以通过 `BullMQ` 对象，获取到 `Queue`、`Worker`、`FlowProducer` 等对象定义。\n\n## Bull UI\n\n在分布式场景中，我们可以资利用 Bull UI 来简化管理。\n\n和 bull 组件类似，需要独立安装和启用。\n\n```bash\n$ npm i @midwayjs/bull-board@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/bull-board\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n将 bull-board 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as bullmq from '@midwayjs/bullmq';\nimport * as bullBoard from '@midwayjs/bull-board';\n\n@Configuration({\n  imports: [\n    // ...\n    bullmq,\n    bullBoard,\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n默认的访问路径为：`http://127.1:7001/ui`。\n\n效果如下：\n\n![](https://img.alicdn.com/imgextra/i2/O1CN01j4wEFb1UacPxA06gs_!!6000000002534-2-tps-1932-1136.png)\n\n可以通过配置进行基础路径的修改。\n\n```typescript\n// src/config/config.prod.ts\nexport default {\n  // ...\n  bullBoard: {\n    basePath: '/ui',\n  },\n}\n```\n\n此外，组件提供了 `BullBoardManager` ，可以添加动态创建的队列。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bullmq from '@midwayjs/bullmq';\nimport * as bullBoard from '@midwayjs/bull-board';\n\n@Configuration({\n  imports: [\n    // ...\n    bullmq,\n    bullBoard\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullmqFramework: bullmq.Framework;\n  \n  @Inject()\n  bullBoardManager: bullBoard.BullBoardManager;\n\n  async onServerReady() {\n    const testQueue = this.bullmqFramework.createQueue('test', {\n      // ...\n    });\n\n    this.bullBoardManager.addQueue(new bullBoard.BullMQAdapter(testQueue) as any);\n  }\n}\n```\n\n由于最新 bull-board 要求最低 Node.js 版本为 v20，所以 Midway v3 无法将 bull-board 升级；在 v5.23.0 版本下的 `bull-board` 存在类型定义问题，采用 `as any`  的方式绕过。\n","site/docs/extensions/busboy.md":"import Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n# 文件上传\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的通用上传组件，支持 `file` (服务器临时文件) 和 `stream` （流）两种模式。\n\n相关信息：\n\n| web 支持情况      |      |\n| ----------------- | ---- |\n| @midwayjs/koa     | ✅    |\n| @midwayjs/faas    | 💬    |\n| @midwayjs/web     | ✅    |\n| @midwayjs/express | ✅    |\n\n:::caution\n\n💬 部分函数计算平台不支持流式请求响应，请参考对应平台能力。\n\n:::\n\n:::tip\n\n本模块自 3.17.0 起替换 upload 组件。\n\n和 upload 组件的差异为：\n\n* 1、配置的 key 从 `upload` 调整为 `busboy`\n* 2、中间件不再默认加载，手动可配置到全局或者路由\n* 3、入参定义类型调整为 `UploadStreamFileInfo`\n* 4、`fileSize` 的配置有调整\n\n:::\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/busboy@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/busboy\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n## 启用组件\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration } from '@midwayjs/core';\nimport * as busboy from '@midwayjs/busboy';\n\n@Configuration({\n  imports: [\n    // ...other components\n    busboy\n  ],\n  // ...\n})\nexport class MainConfiguration {}\n```\n\n\n\n## 配置中间件\n\n组件中提供了 `UploadMiddleware` 这个中间件，可以将其配置到全局或者特定路由，推荐配置到特定路由，提升性能。\n\n\n\n**路由中间件**\n\n```typescript\nimport { Controller, Post } from '@midwayjs/core';\nimport { UploadMiddleware } from '@midwayjs/busboy';\n\n@Controller('/')\nexport class HomeController {\n\n  @Post('/upload', { middleware: [UploadMiddleware] })\n  async upload(/*...*/) {\n    // ...\n  }\n}\n```\n\n**全局中间件**\n\n<Tabs>\n<TabItem value=\"koa\" label=\"@midwayjs/koa\">\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration } from '@midwayjs/core';\nimport * as busboy from '@midwayjs/busboy';\nimport { Application } from '@midwayjs/koa';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  @App('koa')\n  app: Application;\n  \n  async onReady() {\n    this.app.useMiddleware(busboy.UploadMiddleware);\n  }\n}\n```\n</TabItem>\n<TabItem value=\"egg\" label=\"@midwayjs/web\">\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration } from '@midwayjs/core';\nimport * as busboy from '@midwayjs/busboy';\nimport { Application } from '@midwayjs/web';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  @App('egg')\n  app: Application;\n  \n  async onReady() {\n    this.app.useMiddleware(busboy.UploadMiddleware);\n  }\n}\n```\n</TabItem>\n<TabItem value=\"express\" label=\"@midwayjs/express\">\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration } from '@midwayjs/core';\nimport * as busboy from '@midwayjs/busboy';\nimport { Application } from '@midwayjs/express';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  @App('express')\n  app: Application;\n  \n  async onReady() {\n    this.app.useMiddleware(busboy.UploadMiddleware);\n  }\n}\n```\n</TabItem>\n<TabItem value=\"faas\" label=\"@midwayjs/faas\">\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration } from '@midwayjs/core';\nimport * as busboy from '@midwayjs/busboy';\nimport { Application } from '@midwayjs/faas';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  @App('faas')\n  app: Application;\n  \n  async onReady() {\n    this.app.useMiddleware(busboy.UploadMiddleware);\n  }\n}\n```\n</TabItem>\n</Tabs>\n\n\n\n## 配置\n\n组件使用 `busboy` 作为配置的 key。\n\n\n\n### 上传模式\n\n上传分为三种模式，文件模式，流式模式以及新增的异步迭代器模式。\n\n代码中使用 `@Files()` 装饰器获取上传的文件， `@Fields` 装饰器获取其他上传表单字段。\n\n<Tabs>\n<TabItem value=\"file\" label=\"文件模式\">\n\n`file` 为默认值，配置 mode 为 `file` 字符串。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  busboy: {\n    mode: 'file',\n  },\n}\n```\n\n在代码中获取上传的文件，支持同时上传多个文件。\n\n```typescript\nimport { Controller, Post, Files, Fields } from '@midwayjs/core';\nimport { UploadFileInfo } from '@midwayjs/busboy';\n\n@Controller('/')\nexport class HomeController {\n\n  @Post('/upload', /*...*/)\n  async upload(@Files() files: Array<UploadFileInfo>, @Fields() fields: Record<string, string>) {\n    /*\n    files = [\n      {\n        filename: 'test.pdf',        // 文件原名\n        data: '/var/tmp/xxx.pdf',    // 服务器临时文件地址\n        mimeType: 'application/pdf', // mime\n        fieldName: 'file'            // field name\n      },\n    ]\n    */\n  }\n}\n```\n\n使用 file 模式时， 获取的 `data` 为上传的文件在服务器的 `临时文件地址`，后续可以再通过 `fs.createReadStream` 等方式来处理此文件内容，支持同时上传多个文件，多个文件会以数组的形式存放。\n\n每个数组内的对象包含以下几个字段\n\n```typescript\nexport interface UploadFileInfo {\n  /**\n   * 上传的文件名\n   */\n  filename: string;\n  /**\n   * 上传文件 mime 类型\n   */\n  mimeType: string;\n  /**\n   * 上传服务端保存的路径\n   */\n  data: string;\n  /**\n   * 上传的表单字段名\n   */\n  fieldName: string;\n}\n```\n\n\n\n</TabItem>\n\n<TabItem value=\"asyncIterator\" label=\"异步迭代器模式\">\n\n从 `v3.18.0` 提供，替代原有的 `stream` 模式，该模式支持多个文件流式上传。\n\n配置 mode 为 `asyncIterator` 字符串。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  busboy: {\n    mode: 'asyncIterator',\n  },\n}\n```\n\n在代码中获取上传的文件。\n\n```typescript\nimport { Controller, Post, Files, Fields } from '@midwayjs/core';\nimport { UploadStreamFileInfo, UploadStreamFieldInfo } from '@midwayjs/busboy';\n\n@Controller('/')\nexport class HomeController {\n\n  @Post('/upload', /*...*/)\n  async upload(\n  \t@Files() fileIterator: AsyncGenerator<UploadStreamFileInfo>,\n    @Fields() fieldIterator: AsyncGenerator<UploadStreamFieldInfo>\n  ) {\n    // ...\n  }\n}\n```\n\n在该模式下，`@Files` 和 `@File` 装饰器会提供同一个 `AsyncGenerator` ，而 `@Fields` 会也同样会提供一个 `AsyncGenerator`。\n\n通过循环 `AsyncGenerator` ，可以针对每个上传文件的 `ReadStream` 做处理。\n\n```typescript\nimport { Controller, Post, Files, Fields } from '@midwayjs/core';\nimport { UploadStreamFileInfo, UploadStreamFieldInfo } from '@midwayjs/busboy';\nimport { tmpdir } from 'os';\nimport { createWriteStream } from 'fs';\n\n@Controller('/')\nexport class HomeController {\n\n  @Post('/upload', /*...*/)\n  async upload(\n  \t@Files() fileIterator: AsyncGenerator<UploadStreamFileInfo>,\n    @Fields() fieldIterator: AsyncGenerator<UploadStreamFieldInfo>\n  ) {\n    for await (const file of fileIterator) {\n      const { filename, data } = file;\n      const p = join(tmpdir, filename);\n      const stream = createWriteStream(p);\n      data.pipe(stream);\n    }\n\n    for await (const { name, value } of fieldIterator) {\n      // ...\n    }\n\n    // ...\n  }\n}\n```\n\n注意，如果一次上传中任意一个文件抛出了错误，本次上传流会直接关闭，所有未传输完成的文件都会异常。\n\n异步迭代器中的上传对象包含以下几个字段。\n\n```typescript\nexport interface UploadStreamFieldInfo {\n  /**\n   * 上传的文件名\n   */\n  filename: string;\n  /**\n   * 上传文件 mime 类型\n   */\n  mimeType: string;\n  /**\n   * 上传文件的文件流\n   */\n  data: Readable;\n  /**\n   * 上传的表单字段名\n   */\n  fieldName: string;\n}\n```\n\n异步迭代器中的 `@Fields` 的对象略有不同，返回的数据会包含 `name` 和 `value` 字段。\n\n```typescript\nexport interface UploadStreamFieldInfo {\n  /**\n   * 表单名\n   */\n  name: string;\n  /**\n   * 表单值\n   */\n  value: any;\n}\n```\n\n\n\n</TabItem>\n\n<TabItem value=\"stream\" label=\"流模式\">\n\n:::caution\n\n不再推荐使用。\n\n:::\n\n配置 mode 为 `stream` 字符串。\n\n\n使用 stream 模式时，通过 `@Files` 中获取的 `data` 为 `ReadStream`，后续可以再通过 `pipe` 等方式继续将数据流转至其他 `WriteStream` 或 `TransformStream`。\n\n\n使用 stream 模式时，仅同时上传一个文件，即 `@Files` 数组中只有一个文件数据对象。\n\n另外，stream 模式 `不会` 在服务器上产生临时文件，所以获取到上传的内容后无需手动清理临时文件缓存。\n\n:::tip\n\nfaas 场景实现方式视平台而定，如果平台不支持流式请求/响应但是业务开启了 `mode: 'stream'`，将采用先读取到内存，再模拟流式传输来降级处理。\n\n:::\n\n在代码中获取上传的文件，流式模式下仅支持单个文件。\n\n```typescript\nimport { Controller, Post, Files, Fields } from '@midwayjs/core';\nimport { UploadStreamFileInfo } from '@midwayjs/busboy';\n\n@Controller('/')\nexport class HomeController {\n\n  @Post('/upload', /*...*/)\n  async upload(@Files() files: Array<UploadStreamFileInfo>, @Fields() fields: Record<string, string) {\n    /*\n    files = [\n      {\n        filename: 'test.pdf',        // 文件原名\n        data: ReadStream,            // 文件流\n        mimeType: 'application/pdf', // mime\n        fieldName: 'file'            // field name\n      },\n    ]\n\n    */\n  }\n}\n```\n\n流式模式中的对象包含以下几个字段。\n\n```typescript\nexport interface UploadStreamFieldInfo {\n  /**\n   * 上传的文件名\n   */\n  filename: string;\n  /**\n   * 上传文件 mime 类型\n   */\n  mimeType: string;\n  /**\n   * 上传文件的文件流\n   */\n  data: Readable;\n  /**\n   * 上传的表单字段名\n   */\n  fieldName: string;\n}\n```\n\n</TabItem>\n\n</Tabs>\n\n\n\n### 上传文件后缀检查\n\n通过 `whitelist` 属性，配置允许上传的文件后缀名，配置 `null` 则不校验后缀名。\n\n:::caution\n\n如果配置为 `null`，则不对上传文件后缀名进行校验，如果采取文件上传模式 (mode=file)，则会有可能被攻击者所利用，上传 `.php`、`.asp` 等后缀的 WebShell 实现攻击行为。\n\n当然，由于组件会对上传后的临时文件采取 `重新随机生成` 文件名写入，只要开发者 `不将` 上传后的临时文件地址返回给用户，那么即使用户上传了一些不被预期的文件，那也无需过多担心会被利用。\n\n:::\n\n\n如果上传的文件后缀不匹配，会响应 `400` error，默认值如下：\n\n```ts\n'.jpg',\n'.jpeg',\n'.png',\n'.gif',\n'.bmp',\n'.wbmp',\n'.webp',\n'.tif',\n'.psd',\n'.svg',\n'.js',\n'.jsx',\n'.json',\n'.css',\n'.less',\n'.html',\n'.htm',\n'.xml',\n'.pdf',\n'.zip',\n'.gz',\n'.tgz',\n'.gzip',\n'.mp3',\n'.mp4',\n'.avi',\n```\n\n可以通过组件中导出的 `uploadWhiteList` 获取到默认的后缀名白名单。\n\n另外，midway 上传组件，为了避免部分 `恶意用户`，通过某些技术手段来`伪造`一些可以被截断的扩展名，所以会对获取到的扩展名的二进制数据进行过滤，仅支持 `0x2e`（即英文点 `.`）、`0x30-0x39`（即数字 `0-9`）、`0x61-0x7a`（即小写字母 `a-z`） 范围内的字符作为扩展名，其他字符将会被自动忽略。\n\n你可以传递一个函数，可以根据不同的条件动态返回白名单。\n\n```typescript\n// src/config/config.default.ts\nimport { uploadWhiteList } from '@midwayjs/busboy';\nimport { tmpdir } from 'os';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  busboy: {\n    whitelist: (ctx) => {\n      if (ctx.path === '/') {\n        return [\n          '.jpg',\n          '.jpeg',\n        ];\n      } else {\n        return [\n          '.jpg',\n        ]\n      };\n    },\n    // ...\n  },\n}\n```\n\n\n\n\n\n### 上传文件 MIME 类型检查\n\n部分`恶意用户`，会尝试将 `.php` 等 WebShell 修改扩展名为 `.jpg`，来绕过基于扩展名的白名单过滤规则，在某些服务器环境内，这个 jpg 文件依然会被作为 PHP 脚本来执行，造成安全风险。\n\n组件提供了 `mimeTypeWhiteList` 配置参数 **【请注意，此参数无默认值设置，即默认不校验】**，您可以通过此配置设置允许的文件 MIME 格式，规则为由数组 `[扩展名, mime, [...moreMime]]` 组成的 `二级数组`，例如：\n\n```typescript\n// src/config/config.default.ts\nimport { uploadWhiteList } from '@midwayjs/busboy';\nexport default {\n  // ...\n  busboy: {\n    // ...\n    // 扩展名白名单\n    whitelist: uploadWhiteList,\n    // 仅允许下面这些文件类型可以上传\n    mimeTypeWhiteList: {\n      '.jpg': 'image/jpeg',\n      // 也可以设置多个 MIME type，比如下面的允许 .jpeg 后缀的文件是 jpg 或者是 png 两种类型\n      '.jpeg': ['image/jpeg', 'image/png'],\n      // 其他类型\n      '.gif': 'image/gif',\n      '.bmp': 'image/bmp',\n      '.wbmp': 'image/vnd.wap.wbmp',\n      '.webp': 'image/webp',\n    }\n  },\n}\n```\n\n您也可以使用组件提供的 `DefaultUploadFileMimeType` 变量，作为默认的 MIME 校验规则，它提供了常用的 `.jpg`、`.png`、`.psd` 等文件扩展名的 MIME 数据：\n\n```typescript\n// src/config/config.default.ts\nimport { uploadWhiteList, DefaultUploadFileMimeType } from '@midwayjs/busboy';\nexport default {\n  // ...\n  busboy: {\n    // ...\n    // 扩展名白名单\n    whitelist: uploadWhiteList,\n    // 仅允许下面这些文件类型可以上传\n    mimeTypeWhiteList: DefaultUploadFileMimeType,\n  },\n}\n```\n\n文件格式与对应的 MIME 映射，您可以通过 `https://mimetype.io/` 这个网站来查询，对于文件的 MIME 识别，我们使用的是 [file-type@16](https://www.npmjs.com/package/file-type) 这个 npm 包，请注意它支持的文件类型。\n\n:::info\n\nMIME 类型校验规则仅适用于使用 文件上传模式 `mode=file`，同时设置此校验规则之后，由于需要读取文件内容进行匹配，所以会稍微影响上传性能。\n\n但是，我们依然建议您在条件允许的情况下，设置 `mimeTypeWhiteList` 参数，这将提升您的应用程序安全性。\n\n:::\n\n你可以传递一个函数，可以根据不同的条件动态返回 MIME 规则。\n\n```typescript\n// src/config/config.default.ts\nimport { tmpdir } from 'os';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  busboy: {\n    mimeTypeWhiteList: (ctx) => {\n      if (ctx.path === '/') {\n        return {\n          '.jpg': 'image/jpeg',\n        };\n      } else {\n        return {\n          '.jpeg': ['image/jpeg', 'image/png'],\n        }\n      };\n    }\n  },\n}\n\n```\n\n\n\n### Busboy 上传限制\n\n默认情况下没有限制，可以通过配置修改，数字类型，单位为 byte。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  busboy: {\n    // ...\n    limits: {\n      fileSize: 1024\n    }\n  },\n}\n```\n\n除此之外，还可以设置一些其他的 [限制](https://github.com/mscdex/busboy/tree/master?tab=readme-ov-file#exports)。\n\n\n\n\n### 临时文件与清理\n\n\n如果你使用了 `file` 模式来获取上传的文件，那么上传的文件会存放在您于 `config` 文件中设置的 `upload` 组件配置中的 `tmpdir` 选项指向的文件夹内。\n\n你可以通过在配置中使用 `cleanTimeout` 来控制自动的临时文件清理时间，默认值为 `5 * 60 * 1000`，即上传的文件于 `5 分钟` 后自动清理，设置为 `0` 则视为不开启自动清理功能。\n\n```typescript\n// src/config/config.default.ts\nimport { uploadWhiteList } from '@midwayjs/busboy';\nimport { tmpdir } from 'os';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  busboy: {\n    mode: 'file',\n    tmpdir: join(tmpdir(), 'midway-busboy-files'),\n    cleanTimeout: 5 * 60 * 1000,\n  },\n}\n\n```\n\n你也可以在代码中通过调用 `await ctx.cleanupRequestFiles()` 来主动清理当前请求上传的临时文件。\n\n\n\n### 设置不同路由的配置\n\n通过中间件的不同实例，可以对不同的路由做不同的配置，这种场景下会和全局配置合并，仅能覆盖一小部分配置。\n\n```typescript\nimport { Controller, Post, Files, Fields } from '@midwayjs/core';\nimport { UploadFileInfo, UploadMiddleware } from '@midwayjs/busboy';\n\n@Controller('/')\nexport class HomeController {\n  @Post('/upload1', { middleware: [ createMiddleware(UploadMiddleware, {mode: 'file'}) ]})\n  async upload1(@Files() files Array<UploadFileInfo>) {\n    // ...\n  }\n  \n  @Post('/upload2', { middleware: [ createMiddleware(UploadMiddleware, {mode: 'stream'}) ]})\n  async upload2(@Files() files Array<UploadFileInfo>) {\n    // ...\n  }\n}\n```\n\n当前可以传递的配置包括 `mode` 以及 `busboy` 自带的 [配置](https://github.com/mscdex/busboy/tree/master?tab=readme-ov-file#exports)。\n\n\n\n## 内置错误\n\n以下的错误在不同上传模式下均会自动触发。\n\n* `MultipartInvalidFilenameError` 无效文件名\n* `MultipartInvalidFileTypeError` 无效文件类型\n* `MultipartFileSizeLimitError` 文件大小超出限制\n* `MultipartFileLimitError` 文件数量超出限制\n* `MultipartPartsLimitError` 上传 parts 数量超出限制\n* `MultipartFieldsLimitError` fields 数量超出限制\n* `MultipartError` 其余的 busbuy 错误\n\n\n\n## 安全提示\n\n1. 请注意是否开启 `扩展名白名单` （whiteList），如果扩展名白名单被设置为 `null`，则会有可能被攻击者所利用上传 `.php`、`.asp` 等WebShell。\n2. 请注意是否设置 `match` 或 `ignore` 规则，否则普通的 `POST/PUT` 等接口会有可能被攻击者利用，造成服务器负荷加重和空间大量占用问题。\n3. 请注意是否设置 `文件类型规则` （fileTypeWhiteList）,否则可能会被攻击者伪造文件类型进行上传。\n\n\n\n## 前端文件上传示例\n\n### 1. html form 的形式\n\n```html\n<form action=\"/api/upload\" method=\"post\" enctype=\"multipart/form-data\">\n  Name: <input type=\"text\" name=\"name\" /><br />\n  File: <input type=\"file\" name=\"testFile\" /><br />\n  <input type=\"submit\" value=\"Submit\" />\n</form>\n```\n\n### 2. fetch FormData 方式\n\n```js\nconst fileInput = document.querySelector('#your-file-input') ;\nconst formData = new FormData();\nformData.append('file', fileInput.files[0]);\n\nfetch('/api/upload', {\n  method: 'POST',\n  body: formData,\n});\n```\n\n\n\n## Postman 测试示例\n\n![](https://img.alicdn.com/imgextra/i4/O1CN01iv9ESW1uIShNiRjBF_!!6000000006014-2-tps-2086-1746.png)\n\n","site/docs/extensions/cache.md":"# 缓存\n\nMidway Cache 是为了方便开发者进行缓存操作的组件，它有利于改善项目的性能。它为我们提供了一个数据中心以便进行高效的数据访问。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n## 安装\n\n首先安装相关的组件模块。\n\n```bash\n$ npm i @midwayjs/cache@4 cache-manager --save\n$ npm i @types/cache-manager --save-dev\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/cache\": \"^4.0.0\",\n    \"cache-manager\": \"^3.4.1\",\n    // ...\n  },\n  \"devDependencies\": {\n    \"@types/cache-manager\": \"^3.4.0\",\n    // ...\n  }\n}\n```\n\n\n\n## 使用 Cache\n\nMidway 为不同的 cache 存储提供了统一的 API。默认内置了一个基于内存数据存储的数据中心。如果想要使用别的数据中心，开发者也可以切换到例如 mongodb、fs 等模式。\n\n\n首先，引入 Cache 组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as cache from '@midwayjs/cache';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    cache\t\t// 导入 cache 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n然后在业务代码中即可注入使用。\n\n```typescript\nimport { Inject, Provide } from '@midwayjs/core';\nimport { IUserOptions } from '../interface';\nimport { CacheManager } from '@midwayjs/cache';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  cacheManager: CacheManager;     \t\t\t// 依赖注入 CacheManager\n}\n```\n\n通过提供的 API 来设置，获取缓存数据。\n\n\n```typescript\nimport { Inject, Provide } from '@midwayjs/core';\nimport { IUserOptions } from '../interface';\nimport { CacheManager } from '@midwayjs/cache';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  cacheManager: CacheManager;\n\n  async getUser(options: IUserOptions) {\n    // 设置缓存内容\n    await this.cacheManager.set(`name`, 'stone-jin');\n\n    // 获取缓存内容\n    let result = await this.cacheManager.get(`name`);\n\n    return result;\n  }\n\n  async getUser2(){\n    //获取缓存内容\n    let result = await this.cacheManager.get(`name`);\n    return result;\n  }\n\n  async reset(){\n    await this.cacheManager.reset(); // 清空对应 store 的内容\n  }\n}\n```\n\n\n\n### 设置缓存\n\n\n我们通过 `await this.cache.set(key, value)` 方法进行设置，此处默认过期时间是10s。\n\n\n你也可以手动设置 TTL（过期时间），如下：\n```typescript\nawait this.cacheManager.set(key, value, {ttl: 1000});\t// ttl的单位为秒\n```\n如果你想要 Cache 不过期，则将 TTL 设置为 null 即可。\n```typescript\nawait this.cacheManager.set(key, value, {ttl: null});\n```\n同时你也可以通过全局的 `config.default.ts` 中进行设置。\n```typescript\nexport default {\n  // ...\n  cache: {\n    store: 'memory',\n    options: {\n      max: 100,\n      ttl: 10,\t\t// 修改默认的ttl配置\n    },\n  }\n}\n```\n\n\n### 获取缓存\n\n```typescript\nconst value = await this.cacheManager.get(key);\n```\n如果获取不到，则为 undefined。\n\n\n\n### 移除缓存\n\n\n移除缓存，可以通过 del 方法。\n```typescript\nawait this.cacheManager.del(key);\n```\n\n\n\n### 清空整体store数据（此处是整体清除，需要重点⚠️）\n\n\n比如用户设置了某个 redis 为 store，调用的话，包括非 cache 模块设置的也会清除。\n```typescript\nawait this.cacheManager.reset(); // 这块需要注意\n```\n\n\n\n## 全局配置\n\n\n当我们引用了这个 cache 组件后，我们能对其进行全局的配置。配置方法跟别的组件类似。\n\n\n默认的配置：\n```typescript\nexport default {\n  // ...\n  cache: {\n  \tstore: 'memory',\n    options: {\n      max: 100,\n      ttl: 10,\n    },\n  }\n}\n```\n例如用户可以修改默认的 TTL，也就是过期时间。\n\n\n\n## 其他Cache\n\n\n用户也可以修改  store 方式，在 `config.default.ts` 中进行组件的配置：\n```typescript\nimport * as redisStore from 'cache-manager-ioredis';\n\nexport default {\n  // ...\n  cache: {\n  \tstore: redisStore,\n    options: {\n      host: 'localhost', // default value\n      port: 6379, // default value\n      password: '',\n      db: 0,\n      keyPrefix: 'cache:',\n      ttl: 100\n    },\n  }\n}\n```\n或者修改为 mongodb 的 cache。\n\n\n:::danger\n**再次注意⚠️：使用 redis 作为 cache 的时候，代码里面慎用 reset 方法，因为会把整个 redis 给 flushdb，简称数据清空。**\n:::\n\n\n\n## 相关文档\n\n\n由于 Midway Cache 是基于 cache-manager 封装，所以相关资料用户也可以查询：[cache-manger](https://www.npmjs.com/package/cache-manager)。\n\n\n\n## 常见问题\n\n\n\n### 1、set 和 get 无法得到相同值？\n\n用户使用了 cache 模块，默认是内存式的，例如在本地用 dev 模式，由于是单进程的，那 set 和 ge t最终能达到相同的值。但是用户部署到服务器上面后，由于会有多 worker，相当于第一次请求，落在进程1上，然后第二次落在进程2上，这样获得到空了。\n\n\n解决办法：参考 其他 Cache 的章节，配置 store 为分布式，例如 redis 的 store 等方式。\n","site/docs/extensions/caching.md":"# 缓存\n\n缓存是一个伟大而简单的技术，有助于提高你的应用程序的性能。本组件提供了缓存相关的能力，你可以将数据缓存到不同的数据源，也可以针对不同场景建立多级缓存，提高数据访问速度。\n\n:::tip\n\nMidway 提供基于 [cache-manager v5](https://github.com/node-cache-manager/node-cache-manager) 模块重新封装了缓存组件，原有的缓存模块基于 v3 开发不再迭代，如需查看老文档，请访问 [这里](/docs/extensions/cache)。\n\n:::\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 安装\n\n首先安装相关的组件模块。\n\n```bash\n$ npm i @midwayjs/cache-manager@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/cache-manager\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 启用组件\n\n\n首先，引入组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as cacheManager from '@midwayjs/cache-manager';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    cacheManager,\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n\n\n## 使用缓存\n\n\n\n### 1、配置缓存\n\n在使用前，你需要配置缓存所在的位置，比如内置的内存缓存，或者是引入 Redis 缓存，每个缓存对应了一个缓存的 Store。\n\n下面的示例代码，配置了一个名为 `default` 的内存缓存。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: 'memory',\n      },\n    },\n  }\n}\n```\n\n最常用的场景下，缓存会包含两个参数，配置 `max` 修改缓存的数量，配置 `ttl` 修改缓存的过期时间，单位毫秒。\n\n```typescript\n // src/config/config.default.ts\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: 'memory',\n        options: {\n          max: 100,\n          ttl: 10,\n        },\n      },\n    },\n  }\n}\n```\n\n:::tip\n\n* `ttl` 的单位是毫秒\n* `max` 代表缓存 key 的最大个数\n* 不同的 Store 淘汰 key 的算法不同，内存缓存使用的淘汰算法是 LRU\n\n:::\n\n### 2、使用缓存\n\n可以通过服务工厂的装饰器获取到实例，可以通过简单的 `get` 和 `set` 方法获取和保存缓存。\n\n```typescript\nimport { InjectClient, Provide } from '@midwayjs/core';\nimport { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n\n  @InjectClient(CachingFactory, 'default')\n  cache: MidwayCache;\n\n  async invoke(name: string, value: string) {\n    // 设置缓存\n    await this.cache.set(name, value);\n    // 获取缓存\n    const data = await this.cache.get(name);\n    // ...\n  }\n}\n\n```\n\n动态设置 `ttl` 过期时间。\n\n```typescript\nawait this.cache.set('key', 'value', 1000);\n```\n\n若要禁用缓存过期，可以将 `ttl` 配置属性设置为 0。\n\n```typescript\nawait this.cache.set('key', 'value', 0);\n```\n\n删除单个缓存。\n\n```typescript\nawait this.cache.del('key');\n```\n\n清理整个缓存，可以使用 `reset` 方法。\n\n```typescript\nawait this.cacheManager.reset();\n```\n\n:::danger\n\n注意，清理整个缓存非常危险，如果使用了 Redis 作为缓存 store，将清空整个 Redis 数据。\n\n:::\n\n除了装饰器之外，也可以通过 API 获取缓存实例。\n\n```typescript\nimport { InjectClient, Provide } from '@midwayjs/core';\nimport { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  cachingFactory: CachingFactory;\n\n  async invoke() {\n    const caching = await this.cachingFactory.get('default');\n    // ...\n  }\n}\n```\n\n\n\n### 3、配置多个缓存\n\n和其他组件一样，组件支持配置多个缓存实例。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: 'memory',\n      },\n      otherCaching: {\n        store: 'memory',\n      }\n    },\n  }\n}\n```\n\n可以注入不同的缓存实例。\n\n```typescript\nimport { InjectClient, Provide } from '@midwayjs/core';\nimport { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n\n  @InjectClient(CachingFactory, 'default')\n  cache: MidwayCache;\n  \n  @InjectClient(CachingFactory, 'otherCaching')\n  customCaching: MidwayCache;\n\n}\n\n```\n\n\n\n### 4、配置不同 Store\n\n组件基于 [cache-manager](https://github.com/node-cache-manager/node-cache-manager) 可以配置不同的缓存 Store，比如最常见的可以配置 Redis Store。\n\n假如项目已经配置了一个 `Redis`，通过组件内置的 `createRedisStore` 方法，可以快速创建一个 Redis Store。\n\n\n```typescript\nimport { createRedisStore } from '@midwayjs/cache-manager';\n\n// src/config/config.default.ts\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: createRedisStore('default'),\n        options: {\n          ttl: 10,\n        }\n      },\n    },\n  },\n  redis: {\n    clients: {\n      default: {\n        port: 6379,\n        host: '127.0.0.1',\n      }\n    }\n  }\n}\n```\n\n`createRedisStore` 方法可以传递一个已经配置的 redis 实例名，可以和 redis 组件复用实例。\n\n\n\n### 5、配置三方 Store\n\n除了 Redis 之外，用户也可以自行选择 Cache-Manager 的 Store，列表可以参考 [这里](https://github.com/node-cache-manager/node-cache-manager?tab=readme-ov-file#store-engines)。\n\n下面是一个配置  [node-cache-manager-ioredis-yet](https://github.com/node-cache-manager/node-cache-manager-ioredis-yet) 的例子。\n\n```typescript\n// src/config/config.default.ts\nimport { redisStore } from 'cache-manager-ioredis-yet';\n\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: redisStore,\n        options: {\n          port: 6379,\n          host: 'localhost',\n          ttl: 10,\n        },\n      },\n    },\n  }\n}\n```\n\n\n\n### 6、多级缓存\n\n[cache-manager](https://github.com/node-cache-manager/node-cache-manager) 支持将多个缓存 Store 聚合到一起，实现多级缓存。\n\n下面的示例将内存缓存和 Redis 缓存组合到一起，分别设置 60 秒和 600 秒的默认过期时间。\n\n```typescript\n// src/config/config.default.ts\nimport { createRedisStore } from '@midwayjs/cache-manager';\nexport default {\n  cacheManager: {\n    clients: {\n      memoryCaching: {\n        store: 'memory',\n        options: {\n          ttl: 60_000, // 60 秒，单位为毫秒\n        },\n      },\n      redisCaching: {\n        store: createRedisStore('default'),\n        options: {\n          ttl: 600_000, // 600 秒，单位为毫秒\n        },\n      },\n      multiCaching: {\n        store: ['memoryCaching', 'redisCaching'],\n      },\n    },\n  },\n  redis: {\n    clients: {\n      default: {\n        port: 6379,\n        host: '127.0.0.1',\n      },\n    },\n  },\n};\n\n```\n\n这样 `multiCaching` 这个缓存实例就包含了两级缓存，缓存的优先级从上到下，在查找时，会先查找 `memoryCaching` ，如果内存缓存不存在 key，则继续查找 `redisCaching`。\n\n多级缓存自身的 `options.ttl` 不参与 `set`、`mset` 的写入，请在各个子缓存的 `options.ttl` 中配置默认过期时间。内置内存 Store 和 `createRedisStore` 使用的 TTL 单位都是毫秒。\n\n\n\n### 7、使用多级缓存\n\n和普通缓存类似，多级缓存除了 `set`、`get`、`del`方法外，还增加了 `mset` 、`mget`、`mdel` 方法。\n\n```typescript\nimport { InjectClient, Provide } from '@midwayjs/core';\nimport { CachingFactory, MidwayMultiCache } from '@midwayjs/cache-manager';\n\nconst userId2 = 456;\nconst key2 = 'user_' + userId;\nconst ttl = 5;\n\n@Provide()\nexport class UserService {\n\n  @InjectClient(CachingFactory, 'multiCaching')\n  multiCache: MidwayMultiCache;\n\n  async invoke() {\n    // 设置到所有级别的缓存\n    await this.multiCache.set('foo2', 'bar2', ttl);\n\n    // 从最高优先级的缓存 Store 中获取 key\n    console.log(await this.multiCache.get('foo2'));\n    // >> \"bar2\"\n\n    // 调用每一个 Store 的 del 方法进行删除\n    await this.multiCache.del('foo2');\n\n    // 在所有缓存中设置多个 key，可以多个键值对\n    await this.multiCache.mset(\n      [\n        ['foo', 'bar'],\n        ['foo2', 'bar2'],\n      ],\n      ttl\n    );\n\n    // mget() 从最高优先级的缓存中获取值\n    // 如果第一个缓存 Store 中不包含所有的 key，\n    // 继续在下一个缓存 Store 中查找没有找到的 key。\n    // 这是递归地完成的，直到:\n    // - 所有的 key 都已经查找到值\n    // - 所有的缓存 Store 都被查找过\n    console.log(await this.multiCache.mget('key', 'key2'));\n    // >> ['bar', 'bar2']\n\n    // 调用每一个 Store 的 mdel 方法进行删除\n    await this.multiCache.mdel('foo', 'foo2');\n  }\n}\n\n```\n\n对于上面的内存和 Redis 缓存，调用 `set` 或 `mset` 时省略 TTL 参数，各层会使用自己的默认过期时间；显式传入 TTL 时，本次写入的所有缓存层都会使用该值。\n\n```typescript\n// 使用各层默认 TTL：内存 60 秒，Redis 600 秒\nawait this.multiCache.set('key', 'value');\n\n// 覆盖各层默认 TTL：本次写入的两层缓存都使用 30 秒\nawait this.multiCache.set('key', 'value', 30_000);\n```\n\n### 8、自动刷新\n\n不管是普通缓存还是多级缓存，都支持后台刷新功能，只需要配置 `refreshThreshold` 的时间，单位为毫秒。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: 'memory',\n        options: {\n          refreshThreshold: 3 * 1000,\n        },\n      },\n    },\n  }\n}\n```\n\n如果设置了 `refreshthreshold`，每次从缓存获取值之后，会检查 `ttl` 的值，如果剩余的 `ttl` 小于 `refreshthreshold` ，则系统将异步更新缓存，同时系统会返回旧值，直到 `ttl` 过期。\n\n:::tip\n\n* 在多级缓存的情况下，根据优先级顺序找到第一个包含 key 的 Store。\n\n* 如果阈值较低且执行的函数比较慢，key 可能会过期，有可能会遇到并发更新的情况。\n\n* 后台刷新机制目前只支持单个 key。\n\n* 如果没有为 key 设置 `ttl`，则不会触发刷新机制。对于 redis，`ttl`  默认设置为-1。\n\n:::\n\n\n\n## 自动缓存\n\n### 使用装饰器缓存方法\n\n可以通过 `@Caching` 装饰器缓存方法的结果，比如缓存 http 响应或者服务调用的结果。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Caching } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n  @Caching('default')\n  async getUser(name: string) {\n    return name;\n  }\n}\n\n```\n\n当第一次调用 `getUser` 方法时，会正常执行逻辑，返回结果，装饰器会将结果缓存起来，第二次执行时，如果缓存未失效，则会从缓存中直接返回。\n\n### 指定缓存的 ttl\n\n也可以单独设置 `ttl` 。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Caching } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n  @Caching('default', 100)\n  async getUser(name: string) {\n    return name;\n  }\n}\n```\n\n### 手动指定缓存 key\n\n如果对自动生成的 key 不满意，可以手动指定缓存的 key。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Caching } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n  @Caching('default', 'customKey', 100)\n  async getUser(name: string) {\n    return name;\n  }\n}\n```\n\n### 带逻辑的缓存\n\n如果你希望根据一些特定逻辑进行缓存，比如特定参数，或者特定的 Header，可以传递一个工具函数进行逻辑判断。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Caching } from '@midwayjs/cache-manager';\n\nfunction cacheBy({methodArgs, ctx, target}) {\n  if (methodArgs[0] === 'harry' || methodArgs[0] === 'mike') {\n    return 'cache1';\n  }\n}\n\n@Provide()\nexport class UserService {\n  @Caching('default', cacheBy, 100)\n  async getUser(name: string) {\n    return 'hello ' + name;\n  }\n}\n```\n\n上面的示例中，`cacheBy` 方法自定义了缓存的逻辑，当方法入参值为 `harry` 或者 `mike` 时，将返回缓存的 `key` ，而其他参数时则跳过缓存。\n\n这个时候执行的结果为：\n\n```typescript\nawait userService.getUser('harry'));\t// hello harry\nawait userService.getUser('mike'));\t// hello harry\nawait userService.getUser('lucy'));\t// hello lucy\n```\n\n`@Caching` 装饰器可以在第二个参数中传递一个方法，这个方法的入参 options 为：\n\n* `methodArgs`  当前调用方法的实际参数\n* `ctx` 如果是请求作用域，则是当前调用的上下文对象，如果是单例，则该对象为空对象\n* `target` 当前调用的实例\n\n方法的返回值为字符串或者布尔值，当返回字符串时，表示以该 key 将方法的结果缓存，当返回 `undefined` 或者 `null` 时，表示跳过缓存。\n\n通过这些参数判断，我们可以实现非常灵活的自定义缓存逻辑。\n\n\n\n## 常见问题\n\n\n\n### 1、多进程下内存缓存 set 和 get 无法得到相同值\n\n这是正常现象，每个进程的数据是独立的，仅保存在当前进程中。如需跨进程缓存，请使用 Redis 这类分布式缓存系统。\n","site/docs/extensions/captcha.md":"# 验证码\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的通用验证码组件，支持 `图片验证码`、`计算表达式` 等类型验证码。\n\n您也可以通过此组件，来实现 `短信验证码`、`邮件验证码` 等验证能力，但是注意，本组件本身不含发送短信、邮件功能。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/captcha@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/captcha\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n## 启用组件\n\n在 `src/configuration.ts` 中引入组件。\n\n```typescript\nimport * as captcha from '@midwayjs/captcha';\n\n@Configuration({\n  imports: [\n    // ...other components\n    captcha\n  ],\n})\nexport class MainConfiguration {}\n```\n\n## 调用服务\n\n```typescript\nimport { Controller, Inject } from '@midwayjs/core';\nimport { CaptchaService } from '@midwayjs/captcha';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  ctx;\n\n  @Inject()\n  captchaService: CaptchaService;\n\n  // 示例：获取图像验证码\n  @Get('/get-image-captcha')\n  async getImageCaptcha() {\n    const { id, imageBase64 } = await this.captchaService.image({ width: 120, height: 40 });\n    return {\n      id,          // 验证码 id\n      imageBase64, // 验证码 SVG 图片的 base64 数据，可以直接放入前端的 img 标签内\n    }\n  }\n\n  // 示例：获取计算表达式验证码\n  @Get('/get-formula-captcha')\n  async getFormulaCaptcha() {\n    const { id, imageBase64 } = await this.captchaService.formula({ noise: 1 });\n    return {\n      id,          // 验证码 id\n      imageBase64, // 验证码 SVG 图片的 base64 数据，可以直接放入前端的 img 标签内\n    }\n  }\n\n  // 验证验证码是否正确\n  @Post('/check-captcha')\n  async getCaptcha() {\n    const { id, answer } = this.ctx.request.body;\n    const passed: boolean = await this.captchaService.check(id, answer);\n    if (passed) {\n      return 'passed';\n    }\n    return 'error';\n  }\n\n  // 示例：短信验证码\n  @Post('/sms-code')\n  async sendSMSCode() {\n    // 验证验证码是否正确\n    const { id, text: code } = await this.captchaService.text({ size: 4 });\n    await sendSMS(18888888888, code);\n    return { id }\n  }\n\n  // 示例：邮件验证码\n  @Post('/email-code')\n  async sendEmailCode() {\n    // 验证验证码是否正确\n    const { id, text: code } = await this.captchaService.text({ type: 'number'});\n    await sendEmail('admin@example.com', code);\n    return { id }\n  }\n\n   // 示例：将任意文本内容塞入验证码中\n  @Get('/test-text')\n  async testText() {\n    // 存入内容，获取验证码id\n    const id: string = await this.captchaService.set('123abc');\n    // 根据验证码id，校验内容是否正确\n    const passed: boolean = await this.captchaService.check(id, '123abc');\n    return {\n      passed: passed === true,\n    }\n  }\n}\n```\n\n## 可用配置\n\n```typescript\ninterface CaptchaOptions {\n  default?: { // 默认配置\n    // 验证码字符长度，默认 4 个字符\n    size?: number;\n    // 干扰线条的数量，默认 1 条\n    noise?: number;\n    // 宽度，默认为 120 像素\n    width?: number;\n    // 宽度，默认为 40 像素\n    height?: number;\n    // 图形验证码配置，图形中包含一些字符\n  },\n  image?: {\n    // 验证码字符长度，默认 4 个字符\n    size?: number;\n    // 图像验证码中的字符类型，默认为 'mixed'\n    // - 'mixed' 表示 0-9、A-Z 和 a-z\n    // - 'letter' 表示 A-Z 和 a-z\n    // - 'number' 表示 0-9\n    type?: 'mixed',\n    // 干扰线条的数量，默认 1 条\n    noise?: number;\n    // 宽度，默认为 120 像素\n    width?: number;\n    // 宽度，默认为 40 像素\n    height?: number;\n  },\n  // 计算公式验证码配置，例如返回的图像内容为 1+2，需要用户填入 3\n  formula?: {\n     // 干扰线条的数量，默认 1 条\n    noise?: number;\n    // 宽度，默认为 120 像素\n    width?: number;\n    // 宽度，默认为 40 像素\n    height?: number;\n  },\n  // 纯文本验证码配置，基于纯文本验证码可以实现短信验证码、邮件验证码\n  text?: {\n    // 验证码字符长度，默认 4 个字符\n    size?: number;\n    // 文本验证码中的字符类型，默认为 'mixed'\n    // - 'mixed' 表示 0-9、A-Z 和 a-z\n    // - 'letter' 表示 A-Z 和 a-z\n    // - 'number' 表示 0-9\n    type?: 'mixed',\n  },\n  // 验证码过期时间，默认为 1h\n  expirationTime?: 3600,\n  // 验证码存储的 key 前缀\n  idPrefix: 'midway:vc',\n}\n\nexport const captcha: CaptchaOptions = {\n  default: { // 默认配置\n    size: 4,\n    noise: 1,\n    width: 120,\n    height: 40,\n  },\n  image: {      // 最终会合并 default 配置\n    type: 'mixed',\n  },\n  formula: {},  // 最终会合并 default 配置\n  text: {},     // 最终会合并 default 配置\n  expirationTime: 3600,\n  idPrefix: 'midway:vc',\n}\n```\n\n更多配置请参考 [svg-captcha](https://github.com/produck/svg-captcha)。\n\n### 配置示例一\n\n获取一个 包含 `5个纯英文字母` 的图像验证码，图像宽度 `200` 像素，高度 `50` 像素，并且包含 `3` 条干扰线。\n\n因为图像验证码的配置 `image`， 会与 `default` 配置进行合并，所以可以只修改 `default` 配置：\n\n```typescript\nexport const captcha: CaptchaOptions = {\n  default: {\n    size: 5,\n    noise: 3,\n    width: 200,\n    height: 50\n  },\n  image: {\n    type: 'letter'\n  }\n}\n```\n当然，也可以 `不` 修改 `default` 配置，将宽度、高度等在 `image` 配置项中进行配置，取得 `同样`的效果：\n```typescript\nexport const captcha: CaptchaOptions = {\n  image: {\n    size: 5,\n    noise: 3,\n    width: 200,\n    height: 50\n    type: 'letter'\n  }\n}\n```\n\n### 配置示例二\n\n获取一个图像宽度 `100` 像素，高度 `60` 像素，并且包含 `2` 条干扰线的计算表达式验证码。\n\n因为计算表达式验证码的配置 `formula` ，会与 `default` 配置合并，所以可以只修改 `default` 配置：\n\n```typescript\nexport const captcha: CaptchaOptions = {\n  default: {\n    noise: 2,\n    width: 100,\n    height: 60\n  },\n}\n```\n当然，也可以 `不` 修改 `default` 配置，将宽度、高度等在 `formula` 配置项中进行配置，取得 `同样`的效果：\n```typescript\nexport const captcha: CaptchaOptions = {\n  formula: {\n    noise: 2,\n    width: 100,\n    height: 60\n  }\n}\n```\n\n\n## 组件依赖\n\n验证码的内容存储基于 `@midwayjs/cache-manager` 组件，默认创建了一个名为 `captcha` 的缓存实例，将数据存储在 `memory` 中。\n\n```typescript\nexport default {\n  cacheManager: {\n    clients: {\n      captcha: {\n        store: 'memory',\n      },\n    },\n  },\n};\n```\n\n如果要替换为 `redis` 或其他服务，请参照 `@midwayjs/cache-manager` 的 [文档](/docs/extensions/caching)，对 cache 进行配置。\n\n\n\n\n## 效果\n\n**图片验证码**\n\n![图片验证码](https://gw.alicdn.com/imgextra/i4/O1CN014cEzLH23vEniOgoyp_!!6000000007317-2-tps-120-40.png)\n\n**计算表达式**\n\n ![计算表达式](https://gw.alicdn.com/imgextra/i4/O1CN01u3Mj0q24lRx1md9pX_!!6000000007431-2-tps-120-40.png)\n\n\n## 注意\n\n* 为了防止机器学习破解，使用的 `svg-captcha` 包为 [安全修复后](https://juejin.cn/post/6872656117839691789) 的版本。\n","site/docs/extensions/casbin.md":"# 角色鉴权\n\nCasbin 是一个强大的、高效的开源访问控制框架，其权限管理机制支持多种访问控制模型。\n\n官网文档：https://casbin.org/\n\n\n\n## Casbin 是什么\n\nCasbin 可以：\n\n1. 支持自定义请求的格式，默认的请求格式为`{subject, object, action}`。\n2. 具有访问控制模型model和策略policy两个核心概念。\n3. 支持RBAC中的多层角色继承，不止主体可以有角色，资源也可以具有角色。\n4. 支持内置的超级用户 例如：`root` 或 `administrator`。超级用户可以执行任何操作而无需显式的权限声明。\n5. 支持多种内置的操作符，如 `keyMatch`，方便对路径式的资源进行管理，如 `/foo/bar` 可以映射到 `/foo*`\n\nCasbin 不能：\n\n1. 身份认证 authentication（即验证用户的用户名和密码），Casbin 只负责访问控制。应该有其他专门的组件负责身份认证，然后由 Casbin 进行访问控制，二者是相互配合的关系。\n2. 管理用户列表或角色列表。 Casbin 认为由项目自身来管理用户、角色列表更为合适， 用户通常有他们的密码，但是 Casbin 的设计思想并不是把它作为一个存储密码的容器。 而是存储RBAC方案中用户和角色之间的映射关系。\n\n:::tip\n\n注意：\n\n- 1、在 Midway v3.6.0 之后可用\n- 2、Midway 只是封装了 Casbin 的 API 并提供简单的支持，策略规则编写请查看 [官方文档](https://casbin.org/)\n- 3、Casbin 不提供登录，只提供现有用户的鉴权，需要搭配 passport 等获取用户信息的组件来使用\n\n:::\n\n\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/casbin@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/casbin\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 启用组件\n\n\n首先，引入组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as casbin from '@midwayjs/casbin';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    casbin,\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n\n\n## 准备模型和策略\n\n使用 Casbin 前需要定义模型和策略，这两个文件的内容贯穿本文，建议先去官网了解相关内容。\n\n我们以一个基础的模型为例，比如：\n\n```\n[request_definition]\nr = sub, obj, act\n\n[policy_definition]\np = sub, obj, act\n\n[role_definition]\ng = _, _\ng2 = _, _\n\n[policy_effect]\ne = some(where (p.eft == allow))\n\n[matchers]\nm = g(r.sub, p.sub) && g2(r.obj, p.obj) && r.act == p.act || r.sub == \"root\"\n```\n\n将其保存在项目根目录的 `basic_model.conf` 文件中。\n\n以及包含下面内容的策略文件。\n\n```\np, superuser, user, read:any\np, manager, user_roles, read:any\np, guest, user, read:own\n\ng, alice, superuser\ng, bob, guest\ng, tom, manager\n\ng2, users_list, user\ng2, user_roles, user\ng2, user_permissions, user\ng2, roles_list, role\ng2, role_permissions, role\n```\n\n将其保存在项目根目录的 `basic_policy.csv` 文件中。\n\n\n\n## 配置模型和策略\n\n这里我们的策略将以文件形式进行演示。\n\n配置如下：\n\n```typescript\nimport { MidwayAppInfo } from '@midwayjs/core';\nimport { join } from 'path';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // ...\n    casbin: {\n      modelPath: join(appInfo.appDir, 'basic_model.conf'),\n      policyAdapter: join(appInfo.appDir, 'basic_policy.csv'),\n    }\n  };\n}\n\n```\n\n\n\n## 装饰器鉴权\n\n有多种形式来使用 Casbin，这里以装饰器作为示例。\n\n### 定义资源\n\n首先定义资源，比如放在 `src/resource.ts` 文件中，对应策略文件中对应的资源。\n\n```typescript\nexport enum Resource {\n  USERS_LIST = 'users_list',\n  USER_ROLES = 'user_roles',\n  USER_PERMISSIONS = 'user_permissions',\n  ROLES_LIST = 'roles_list',\n  ROLE_PERMISSIONS = 'role_permission',\n}\n```\n\n\n\n### 配置获取用户的方式\n\n在使用装饰器鉴权时，我们需要配置一个获取用户的方式，比如在 passport 组件之后，我们会从 `ctx.user` 上获取用户名。\n\n```typescript\nimport { MidwayAppInfo } from '@midwayjs/core';\nimport { join } from 'path';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // ...\n    casbin: {\n      modelPath: join(appInfo.appDir, 'basic_model.conf'),\n      policyAdapter: join(appInfo.appDir, 'basic_policy.csv'),\n      usernameFromContext: (ctx) => {\n        return ctx.user;\n      }\n    }\n  };\n}\n\n```\n\n\n\n### 增加守卫\n\n装饰器鉴权依赖守卫，我们可以在全局或者某些路由上开启，全局守卫使用请参考守卫章节。\n\n比如，我们只在下面的 `findAllUsers` 方法上开启鉴权，`AuthGuard` 是 `@midwayjs/casbin` 提供的守卫，可以直接使用。\n\n```typescript\nimport { Controller, Get, UseGuard } from '@midwayjs/core';\nimport { AuthGuard } from '@midwayjs/casbin';\nimport { Resource } from './resouce';\n\n@Controller('/')\nexport class HomeController {\n\n  @UseGuard(AuthGuard)\n  @Get('/users')\n  async findAllUsers() {\n    // ...\n  }\n}\n```\n\n\n\n### 定义权限\n\n使用 `UsePermission` 装饰器定义路由需要的权限。\n\n```typescript\nimport { Controller, Get, UseGuard } from '@midwayjs/core';\nimport { AuthActionVerb, AuthGuard, AuthPossession, UsePermission } from '@midwayjs/casbin';\nimport { Resource } from './resouce';\n\n@Controller('/')\nexport class HomeController {\n\n  @UseGuard(AuthGuard)\n  @UsePermission({\n    action: AuthActionVerb.READ,\n    resource: Resource.USER_ROLES,\n    possession: AuthPossession.ANY\n  })\n  @Get('/users')\n  async findAllUsers() {\n    // ...\n  }\n}\n```\n\n没有权限读取 `USER_ROLES` 的用户不能调用 findAllUsers 方法，在请求时会返回 403 状态码。\n\n比如，上面的 `bob` 用户访问则会返回 403， 而 `tom` 用户访问则正常返回。\n\n\n\n`UsePermission` 需要提供一个对象参数，包括 `action`、`resource`、`possession` 和一个可选的 `isOwn` 的对象。\n\n- `action` 是一个 `AuthActionVerb` 枚举，包含读，写等操作\n- `resource` 资源字符串\n- `possession` 是一个 `AuthPossession` 枚举\n- `isOwn` 是一个接受`Context`（守卫 `canActivate`的参数）作为唯一参数并返回布尔值的函数。 `AuthZGuard` 使用它来确定用户是否是资源的所有者。 如果未定义，将使用返回 `false` 的默认函数。\n\n可以同时定义多个权限，但只有当所有权限都满足时，才能访问该路由。\n\n比如：\n\n```typescript\n@UsePermissions({\n  action: AuthActionVerb.READ,\n  resource: 'USER_ADDRESS',\n  possession: AuthPossession.ANY\n}, {\n  action; AuthActionVerb.READ,\n  resource: 'USER_ROLES,\n  possession: AuthPossession.ANY\n})\n```\n\n只有当用户被授予读取 `USER_ADDRESS` 和 `USER_ROLES` 这两个权限时，才能访问该路由。\n\n\n\n## API 鉴权\n\nCasbin 本身提供了一些通用的 API 和权限相关的功能。\n\n我们可以通过直接注入 `CasbinEnforcerService` 服务来使用。\n\n比如，我们可以在守卫或者中间件中编码。\n\n```typescript\nimport { CasbinEnforcerService } from '@midwayjs/casbin';\nimport { Guard, IGuard } from '@midwayjs/core';\n\n@Guard()\nexport class UserGuard extends IGuard {\n\n  @Inject()\n  casbinEnforcerService: CasbinEnforcerService;\n\n  async canActivate(ctx, clz, methodName) {\n    // 用户登录了，并且是特定的方法，则检查权限\n    if (ctx.user && methodName === 'findAllUsers') {\n      return await this.casbinEnforcerService.enforce(ctx.user, 'USER_ROLES', 'read');\n    }\n    // 未登录用户不允许访问\n    return false;\n  }\n}\n```\n\n在启用守卫后，效果和上面的装饰器相同。\n\n此外，`CasbinEnforcerService` 还有更多的 API，比如重新加载策略。\n\n```typescript\nawait this.casbinEnforcerService.loadPolicy();\n```\n\n\n\n## 分布式策略存储\n\n在多台机器部署的场景下，需要将策略存储到外部。\n\n当前已经实现的适配器有：\n\n- Redis\n- Typeorm\n\n\n\n### Redis Adapter\n\n需要依赖 `@midwayjs/casbin-redis-adapter` 包和 redis 组件。\n\n```bash\n$ npm i @midwayjs/casbin-redis-adapter @midwayjs/redis --save\n```\n\n启用 redis 组件。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as redis from '@midwayjs/redis';\nimport * as casbin from '@midwayjs/casbin';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    redis,\n    casbin,\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n配置 redis 连接和 casbin 适配器。\n\n```typescript\nimport { MidwayAppInfo } from '@midwayjs/core';\nimport { join } from 'path';\nimport { createAdapter } from '@midwayjs/casbin-redis-adapter';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // ...\n    redis: {\n      clients: {\n        // 为 casbin 定义了一个连接\n        'node-casbin-official': {\n          host: '127.0.0.1',\n          port: 6379,\n          password: '',\n          db: '0',\n        }\n      }\n    },\n    casbin: {\n      policyAdapter: createAdapter({\n        // 配置了上面的连接名\n        clientName: 'node-casbin-official'\n      }),\n      // ...\n    },\n  };\n}\n\n```\n\n\n\n### TypeORM Adapter\n\n需要依赖 `@midwayjs/casbin-typeorm-adapter` 包和 typeorm 组件。\n\n```\n$ npm i @midwayjs/casbin-typeorm-adapter @midwayjs/typeorm typeorm@^1.0.0 better-sqlite3 --save\n```\n\n启用 typeorm 组件。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as typeorm from '@midwayjs/typeorm';\nimport * as casbin from '@midwayjs/casbin';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    typeorm,\n    casbin,\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n配置适配器，下面以 better-sqlite3 存储为例，mysql 的配置可以查看 typeorm 组件。\n\n```typescript\nimport { MidwayAppInfo } from '@midwayjs/core';\nimport { join } from 'path';\nimport { CasbinRule, createAdapter } from '@midwayjs/casbin-typeorm-adapter';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // ...\n    typeorm: {\n      dataSource: {\n        // 为 casbin 定义了一个连接\n        'node-casbin-official': {\n          type: 'better-sqlite3',\n          synchronize: true,\n          database: join(appInfo.appDir, 'casbin.sqlite'),\n          // 注意这里显式引入了 Entity\n          entities: [CasbinRule],\n        }\n      }\n    },\n    casbin: {\n      policyAdapter: createAdapter({\n        // 配置了上面的连接名\n        dataSourceName: 'node-casbin-official'\n      }),\n      // ...\n    }\n  };\n}\n```\n\n\n\n## 监视器\n\n使用分布式消息系统，例如 [etcd](https://github.com/coreos/etcd) 来保持多个Casbin执行器实例之间的一致性。 因此，我们的用户可以同时使用多个Casbin 执行器来处理大量的权限检查请求。\n\nMidway 当前只提供一种 Redis 更新策略，如有其他需求，可以给我们提交 issue。\n\n### Redis Watcher\n\n需要依赖 `@midwayjs/casbin-redis-adapter` 包和 redis 组件。\n\n```bash\n$ npm i @midwayjs/casbin-redis-adapter @midwayjs/redis --save\n```\n\n启用 redis 组件。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as redis from '@midwayjs/redis';\nimport * as casbin from '@midwayjs/casbin';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    redis,\n    casbin,\n  ],\n  // ...\n})\nexport class MainConfiguration {\n}\n```\n\n使用示例：\n\n```typescript\nimport { MidwayAppInfo } from '@midwayjs/core';\nimport { join } from 'path';\nimport { createAdapter, createWatcher } from '@midwayjs/casbin-redis-adapter';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // ...\n    redis: {\n      clients: {\n        'node-casbin-official': {\n          host: '127.0.0.1',\n          port: 6379,\n          db: '0',\n        },\n        'node-casbin-sub': {\n          host: '127.0.0.1',\n          port: 6379,\n          db: '0',\n        }\n      }\n    },\n    casbin: {\n      // ...\n      policyAdapter: createAdapter({\n        clientName: 'node-casbin-official'\n      }),\n      policyWatcher: createWatcher({\n        pubClientName: 'node-casbin-official',\n        subClientName: 'node-casbin-sub',\n      })\n    },\n  };\n}\n```\n\n注意，pub/sub 连接需要不同的客户端，上面代码定义了两个客户端。\n\npub 客户端可以和普通 Redis 客户端连接复用，而 sub 需要一个独立的客户端。\n","site/docs/extensions/cfork.md":"# cfork\n\n很多同学没有听过 cfork，cfork 库是 egg-scripts 中用于启动主进程的库，是 egg 使用的基础库之一，他的功能是启动进程，并维持多个进程的保活。\n\n\n文档在此：[https://github.com/node-modules/cfork](https://github.com/node-modules/cfork)\n\n\n由于 bootstrap.js 的特性，有时候不是很适合 pm2 来部署（比如集团内部，全局不安装，需要 API 启动）。\n\n\n我们可以新增一个 `server.js` 用来做主进程的入口，将 `bootstrap.js` 作为每个子进程的启动入口。\n\n\n```javascript\n// server.js\n\n'use strict';\n\nconst cfork = require('cfork');\nconst util = require('util');\nconst path = require('path');\nconst os = require('os');\n\n// 获取 cpu 核数\nconst cpuNumbers = os.cpus().length;\n\ncfork({\n  exec: path.join(__dirname, './bootstrap.js'),\n  count: cpuNumbers,\n})\n  .on('fork', (worker) => {\n    console.warn('[%s] [worker:%d] new worker start', Date(), worker.process.pid);\n  })\n  .on('disconnect', (worker) => {\n    console.warn(\n      '[%s] [master:%s] worker:%s disconnect, exitedAfterDisconnect: %s, state: %s.',\n      Date(),\n      process.pid,\n      worker.process.pid,\n      worker.exitedAfterDisconnect,\n      worker.state\n    );\n  })\n  .on('exit', (worker, code, signal) => {\n    const exitCode = worker.process.exitCode;\n    const err = new Error(\n      util.format(\n        'worker %s died (code: %s, signal: %s, exitedAfterDisconnect: %s, state: %s)',\n        worker.process.pid,\n        exitCode,\n        signal,\n        worker.exitedAfterDisconnect,\n        worker.state\n      )\n    );\n    err.name = 'WorkerDiedError';\n    console.error('[%s] [master:%s] worker exit: %s', Date(), process.pid, err.stack);\n  });\n```\n\n最后启动 `node server.js` 即可。\n","site/docs/extensions/code_dye.md":"# 代码染色\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的代码染色组件。\n\n用于在 HTTP 场景展示调用链路耗时与各个方法的出入参，帮你更快地定位代码问题。\n\n比如：\n\n+ 代码执行缓慢\n  - 不知道是哪一个方法执行的慢：通过代码染色后，可以查看每一个 `方法的执行时长`。\n+ 代码执行错误\n  - 可能是方法没有调到：通过代码染色后，可以查看每一个 `方法的调用链`。\n  - 可能是方法调用参数出错：通过代码染色后，查看每一个`方法的入参和返回值`\n\n使用效果：\n\n![](https://gw.alicdn.com/imgextra/i1/O1CN017Zd6y628M2PvqJO7I_!!6000000007917-2-tps-2392-844.png)\n\n\n\n\n相关信息：\n\n| web 支持情况      |      |\n| ----------------- | ---- |\n| @midwayjs/koa     | ✅    |\n| @midwayjs/faas    | ✅    |\n| @midwayjs/web     | ✅    |\n| @midwayjs/express | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/code-dye@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/code-dye\": \"^4.0.0\"\n    // ...\n  },\n}\n```\n\n\n\n## 启用组件\n\n将 code-dye 组件配置到代码中。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as codeDye from '@midwayjs/code-dye';\n\n@Configuration({\n  imports: [\n    // ...\n    {\n      component: codeDye,\n      enabledEnvironment: ['local'],\t// 只在本地启用\n    }\n  ],\n})\nexport class MainConfiguration {}\n```\n\n\n\n\n\n:::tip\n\n- 可以在 `本地` 或 `研发` 环境开启本组件，便于开发时定位问题，但是 `不建议` 在线上启用，会对线上访问性能产生影响。\n\n:::\n\n\n\n## 配置染色\n\n可以通过`matchQueryKey` 配置，控制当 `query` 参数包含`matchQueryKey` 配置对应的值的时候，进入染色链路，例如，配置为：\n\n```typescript\n// src/config/config.local.ts\nexport default {\n  codeDye: {\n    matchQueryKey: 'codeDyeABC',\n  }\n}\n```\n当请求接口 `http://127.0.0.1:7001/test?codeDyeABC=html` 时，就会判断 `query` 中是否存在 `codeDyeABC` 参数来决定是否染色，并根据参数对应的值，来响应不同的染色结果。\n\n也可以通过`matchHeaderKey` 配置，控制当 `headers` 参数包含 `matchHeaderKey` 配置对应的值的时候，进入染色链路，例如，配置为：\n\n```typescript\n// src/config/config.local.ts\nexport default {\n  codeDye: {\n    matchHeaderKey: 'codeDyeHeader',\n  }\n}\n```\n当请求接口 `http://127.0.0.1:7001/test` 时，就会判断请求的 `headers` 中是否存在 `codeDyeHeader` 参数来决定是否染色，并根据参数对应的值，来响应不同的染色结果。\n\n\n\n## 染色报告\n\n开启了代码染色后，链路染色的结果，可以通过开启染色的不同参数值来配置，目前支持以下三种：\n\n+ `html`：`对` 当前请求的结果进行处理，将染色信息添加到结果中，响应为 `html`，可以在浏览器上查看，效果可以查看此文档上面的图片效果展示。\n+ `json`：`对` 当前请求的结果进行处理，将染色信息添加到结果中，响应为 `json` 结构化信息。\n+ `log`：`不对` 当前请求的结果进行处理，染色的信息将会输出到日志中，不影响请求。\n\n例如，配置为：\n\n```typescript\n// src/config/config.local.ts\nexport default {\n  codeDye: {\n    matchQueryKey: 'codeDyeXXX',\n  }\n}\n```\n\n当请求接口 `http://127.0.0.1:7001/test?codeDyeXXX=html` 时，就会判断 `query` 中 `codeDyeXXX` 参数的值为 `html`，就将染色结果输出在当前请求的响应中，并且内容为 `html` 格式。\n","site/docs/extensions/commander.md":"# 命令行\n\n`@midwayjs/commander` 是一个基于 Midway IoC 容器的命令行组件，底层使用 [commander.js](https://github.com/tj/commander.js#readme) 做参数解析与 help 输出。你可以用 Midway 熟悉的依赖注入方式组织命令、选项解析与业务逻辑，并将命令拆分为多个 Class。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ❌    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ❌   |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n## 安装依赖\n\n在现有项目中安装 commander 组件依赖。\n\n```bash\n$ npm i @midwayjs/commander@4  --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/commander\": \"^4.0.0\"\n  }\n}\n```\n\n## 开启组件\n\n在入口配置中引入组件。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as commander from '@midwayjs/commander';\n\n@Configuration({\n  imports: [commander],\n})\nexport class MainConfiguration {}\n```\n\n\n## 编写命令\n\n目录结构示例：\n\n```bash\n.\n├── src\n│   ├── commands\n│   │   ├── hello.command.ts\n│   │   └── status.command.ts\n│   ├── configuration.ts\n│   └── ...\n├── bootstrap.js\n└── package.json\n```\n\n一个命令对应一个 Class，使用 `@Command()` 修饰，并实现 `CommandRunner` 接口的 `run()` 方法。\n\n\n```typescript\n// src/commands/hello.command.ts\nimport { Inject, ILogger } from '@midwayjs/core';\nimport { Command, CommandRunner, Option } from '@midwayjs/commander';\n\n@Command({\n  name: 'hello',\n  description: 'hello command',\n  arguments: '<name>',\n  aliases: ['hi'],\n})\nexport class HelloCommand implements CommandRunner {\n  @Inject()\n  logger: ILogger;\n\n  @Option({\n    flags: '-f, --foo [foo]',\n    description: 'foo option',\n    defaultValue: 'bar',\n  })\n  parseFoo(val: string) {\n    return `${val}_parsed`;\n  }\n\n  async run(passedParams: string[], options?: Record<string, any>) {\n    const [name] = passedParams;\n    this.logger?.info?.(`hello ${name}`, options);\n  }\n}\n```\n\n### `@Command()` 参数\n\n - `name`：命令名称（命令行调用与 `--help` 展示时使用）\n- `arguments`：命令位置参数声明（例如 `<name>`、`<a> [b]`）\n- `description`：命令描述，会展示在 `--help` 中\n- `argsDescription`：位置参数描述对象，会展示在 `--help` 中\n- `aliases`：命令别名数组\n\n### `@Option()` 参数\n\n- `flags`：选项声明（例如 `-f, --foo [foo]`、`-n, --num <num>`）\n- `description`：选项描述，会展示在 `--help` 中\n- `defaultValue`：默认值（不传该选项时生效）\n- `required`：是否必填（等价于 commander 的 `requiredOption`）\n\n`@Option()` 修饰的方法会作为 commander 的自定义 parser，用于把字符串参数转换为业务需要的类型（例如 number/boolean/自定义格式）。\n\n## 运行命令\n\n该组件作为 Midway Framework 运行，在应用启动时会解析 `process.argv` 并执行匹配的命令。\n\n如果你使用 `bootstrap.js` 作为入口，可以这样启动：\n\n```javascript\n// bootstrap.js\nconst { Bootstrap } = require('@midwayjs/bootstrap');\nBootstrap.run();\n```\n\n然后运行：\n\n```bash\n$ node bootstrap.js hello world --foo baz\n$ node bootstrap.js hi world\n```\n\n## 上下文\n\n每次命令执行都会创建请求上下文（request context），可以在命令类中注入 `Context` 获取运行时信息：\n\n```typescript\nimport { Command, CommandRunner, Context } from '@midwayjs/commander';\nimport { Inject } from '@midwayjs/core';\n\n@Command({ name: 'info', arguments: '<name>' })\nexport class InfoCommand implements CommandRunner {\n  @Inject()\n  ctx: Context;\n\n  async run() {\n    this.ctx.commandName; // 命令名\n    this.ctx.args; // 位置参数数组\n    this.ctx.options; // commander 解析后的选项\n    this.ctx.command; // commander 的 Command 实例\n  }\n}\n```\n\n## 交互式提问（Enquirer）\n\n组件内置了基于 [enquirer](https://github.com/enquirer/enquirer) 的交互式提问能力，适用于运行过程中补齐参数。你可以用 `@QuestionSet()` 组织问题集合，并在命令里通过 `EnquirerService` 触发。\n\n如果希望既支持命令行传参又支持交互补齐，请把对应选项设为可选（否则 commander 会在缺参时直接报错）。\n\n```typescript\nimport {\n  Command,\n  CommandRunner,\n  QuestionSet,\n  Question,\n  ValidateFor,\n  DefaultFor,\n  WhenFor,\n  EnquirerService,\n} from '@midwayjs/commander';\nimport { Inject } from '@midwayjs/core';\n\n@QuestionSet()\nclass ProfileQuestionSet {\n  @Question({ type: 'input', name: 'age', message: 'Your age?' })\n  parseAge(value: string) {\n    return Number.parseInt(value, 10);\n  }\n\n  @Question({ type: 'input', name: 'nickname', message: 'Nickname?' })\n  parseNickname(value: string) {\n    return value;\n  }\n\n  @ValidateFor({ name: 'age' })\n  validateAge(value: string) {\n    return value ? true : 'age required';\n  }\n\n  @DefaultFor({ name: 'nickname' })\n  defaultNickname() {\n    return 'neo';\n  }\n\n  @WhenFor({ name: 'nickname' })\n  whenNickname(answers: Record<string, unknown>) {\n    return Boolean(answers.useNickname);\n  }\n}\n\n@Command({ name: 'ask' })\nexport class AskCommand implements CommandRunner {\n  @Inject()\n  enquirerService: EnquirerService;\n\n  async run(_passedParams: string[], options?: Record<string, any>) {\n    const answers = await this.enquirerService.prompt(ProfileQuestionSet, {\n      useNickname: options?.useNickname,\n    });\n    // use answers.age / answers.nickname\n  }\n}\n```\n\n说明：\n- `@Question()` 修饰的方法会作为 enquirer 的 `result`（用于转换用户输入）。\n- `@DefaultFor()` 会映射到 enquirer 的 `initial`。\n- `@WhenFor()` 支持根据已收集的答案决定是否提问。\n- 可用的 `@*For()` 装饰器：`ValidateFor`、`ChoicesFor`、`MessageFor`、`DefaultFor`、`WhenFor`。\n- `prompt()` 同时支持 `QuestionSet` 名称字符串或类引用，推荐使用类引用。\n\n## 错误处理\n\n默认情况下，CLI 入口会捕获异常并输出日志后退出进程。你可以在配置里提供 `errorHandler` 来接管错误处理：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  commander: {\n    errorHandler: (err: Error) => {\n      console.error(err);\n      process.exit(1);\n    },\n  },\n};\n```\n\n如果你在命令里使用了 `@Catch()` 错误过滤器（Midway Filter），会先走过滤器逻辑，再进入这里的兜底处理。\n\n## 返回值与输出\n\n默认情况下，命令执行完毕后，如果 `run()` 有返回值，框架会将返回值输出到标准输出（stdout），方便用作脚本管道或在测试中断言输出内容。\n\n支持的返回值类型：\n\n- `string` / `Buffer`：直接写入 stdout\n- 普通对象：使用 `JSON.stringify` 后写入 stdout\n- `Readable`：会 pipe 到 stdout\n- `AsyncIterable`：会按迭代顺序逐段写入 stdout\n\n下面是一个返回文本/JSON 的例子（使用 core 的 `ServerResponse` 语义来组织输出格式）：\n\n```typescript\nimport { Command, CommandRunner, CliServerResponse } from '@midwayjs/commander';\n\n@Command({ name: 'status' })\nexport class StatusCommand implements CommandRunner {\n  async run() {\n    return new CliServerResponse({} as any).success().json({ ok: true });\n  }\n}\n```\n\n如果希望按 chunk 逐步输出，可以返回 `CliServerResponse().stream()`：\n\n```typescript\nimport { Command, CommandRunner, CliServerResponse } from '@midwayjs/commander';\n\n@Command({ name: 'stream' })\nexport class StreamCommand implements CommandRunner {\n  async run() {\n    const response = new CliServerResponse({} as any);\n    const stream = response.stream();\n\n    setImmediate(() => {\n      stream.send('a');\n      stream.send({ b: 2 });\n      stream.end();\n    });\n\n    return stream;\n  }\n}\n```\n\n## 日志\n\n组件默认会注册一个名为 `commanderLogger` 的 logger，默认写入 `midway-commander.log`。\n\n你可以在命令类里通过 `@Logger('commanderLogger')` 注入使用，例如：\n\n```typescript\nimport { Logger, ILogger } from '@midwayjs/core';\nimport { Command, CommandRunner } from '@midwayjs/commander';\n\n@Command({ name: 'hello', arguments: '<name>' })\nexport class HelloCommand implements CommandRunner {\n  @Logger('commanderLogger')\n  logger: ILogger;\n\n  async run(passedParams: string[]) {\n    this.logger.info('hello %s', passedParams[0]);\n  }\n}\n```\n\n如果希望自定义日志文件名或级别，可以在应用配置中覆盖 `midwayLogger.clients.commanderLogger`：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  midwayLogger: {\n    clients: {\n      commanderLogger: {\n        fileLogName: 'my-commander.log',\n        level: 'info',\n      },\n    },\n  },\n};\n```\n\n## 单元测试\n\n命令行参数在测试环境中很容易被 Jest/Node 参数污染，推荐通过 framework 的 `runCommand()` 来执行命令，而不是直接 mock `process.argv`。\n\n```typescript\nimport { createLightApp, close } from '@midwayjs/mock';\nimport { Framework } from '@midwayjs/commander';\nimport * as commander from '@midwayjs/commander';\n\ndescribe('commander', () => {\n  it('should run command', async () => {\n    const app = await createLightApp({\n      imports: [commander],\n      preloadModules: [HelloCommand],\n    });\n\n    const framework = app.getFramework() as Framework;\n    await framework.runCommand('hello', 'world', '--foo', 'bar');\n\n    await close(app);\n  });\n});\n```\n","site/docs/extensions/consul.md":"# Consul\n\nconsul 用于微服务下的服务治理，主要特点有：服务发现、服务配置、健康检查、键值存储、安全服务通信、多数据中心等。\n\n本文基于最新组件实现，介绍 Consul 客户端的配置与使用，以及统一抽象的服务发现能力。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n从 v4 开始 Consul 组件基于 Service Factory 进行了重构，API 有着非常大的变化，并提供了服务发现等抽象能力。\n\n感谢 [boostbob](https://github.com/boostbob) 提供的组件。\n\n\n效果如下图：\n![image.png](https://img.alicdn.com/imgextra/i3/O1CN01e5cFZx1I0draeZynr_!!6000000000831-2-tps-1500-471.png)\n\n![image.png](https://img.alicdn.com/imgextra/i2/O1CN01iLYF8r1HQ0B3b47Fh_!!6000000000751-2-tps-1500-895.png)\n\n\n## 安装组件\n\n首先安装 Consul 组件：\n\n```bash\n$ npm i @midwayjs/consul@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/consul\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 引入组件\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as consul from '@midwayjs/consul';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [consul],\n  importConfigs: [join(__dirname, 'config')]\n})\nexport class MainConfiguration {}\n```\n\n\n\n## 配置客户端\n\n支持单客户端与多客户端，遵循 ServiceFactory 配置结构。\n\n**单客户端**\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  consul: {\n    client: {\n      host: '127.0.0.1',\n      port: '8500'\n    }\n  }\n}\n```\n\n**多客户端**\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  consul: {\n    clients: {\n      a: { host: '127.0.0.1', port: '8500' },\n      b: { host: '127.0.0.1', port: '8501' }\n    },\n    defaultClientName: 'a'\n  }\n}\n```\n\n**使用默认实例**\n\n`ConsulService` 作为默认实例代理，直接暴露原生 Consul 客户端方法。\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { ConsulService } from '@midwayjs/consul';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  consul: ConsulService;\n\n  async register() {\n     await this.consul.kv.set('name', 'juhai');\n  }\n}\n```\n\n\n\n**获取指定实例**\n\n通过 `ConsulServiceFactory` 获取指定命名的客户端实例。\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { ConsulServiceFactory } from '@midwayjs/consul';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  consulFactory: ConsulServiceFactory;\n\n  async invoke() {\n    const consulA = this.consulFactory.get('a');\n    const consulB = this.consulFactory.get('b');\n    await consulA.agent.service.register({ name: 'svc-a', id: 'svc-a-1' });\n    await consulB.agent.service.register({ name: 'svc-b', id: 'svc-b-1' });\n  }\n}\n```\n\n\n\n\n\n## 服务发现\n\n从 v4 开始提供基于统一抽象的服务发现能力，Consul 将成为服务发现的客户端来使用。\n\n服务发现包括 **注册服务** 和 **获取服务** 两部分。\n\n\n\n### 配置服务发现\n\n你需要先配置一个客户端，如果配置了多个，可以使用 `serviceDiscoveryClient` 来指定。\n\n通过 `serviceDiscovery` 来配置服务发现。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  consul: {\n    clients: {\n      default: { host: '127.0.0.1', port: '8500' },\n    },\n    serviceDiscovery: {\n      // ...\n    }\n  }\n}\n```\n\n\n\n### 注册服务发现\n\n当服务启动后，你可以将当前的服务注册到 Consul 注册中心。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport { ConsulServiceDiscovery } from '@midwayjs/consul';\nimport { LoadBalancerType } from '@midwayjs/core';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  @Inject()\n  consulDiscovery: ConsulServiceDiscovery;\n\n  async onServerReady() {\n    // 创建一个服务发现客户端\n    const client = this.consulDiscovery.createClient();\n\n    // 注册当前服务\n    await client.register({\n      id: client.defaultMeta.id,\n      name: 'order',\n      tags: ['test'],\n      address: client.defaultMeta.host,\n      port: 7001,\n      meta: {\n        version: '1.0.0'\n      },\n      check: {\n        name: 'TTL Health Check',\n        timeout: '30s',\n        ttl: '10s'\n      }\n    });\n  }\n}\n```\n\n`register` 方法需要传递一些基础的信息。\n\n最重要的有 id，name，以及 address。\n\n```typescript\ninterface RegisterOptions extends CommonOptions {\n  name: string;\n  id?: string;\n  tags?: string[];\n  address?: string;\n  taggedaddresses?: Record<string, any>;\n  meta?: Record<string, string>;\n  namespace?: string;\n  port?: number;\n  kind?: string;\n  proxy?: any;\n  connect?: RegisterConnect;\n  check?: CheckOptions;\n  checks?: CheckOptions[];\n}\n```\n\n`client.defaultMeta` 提供了一些默认生成的数据，包括 id，serviceName, host。\n\n\n\n### 获取可用服务\n\n你可以在任意需要的地方，动态获取到一个服务来调用。\n\n````typescript\n@Provide()\nexport class OrderService {\n  \n  @Inject()\n  consulDiscovery: ConsulServiceDiscovery;\n  \n  async getSerivce() {\n  \tconst instance = await this.consulDiscovery.getInstance({ service: 'order'});\n    // instance.name\n  }\n}\n````\n\n返回的 instance 类型为 `ConsulHealthItem`，结构如下。\n\n```typescript\nexport interface ConsulHealthItem {\n  Node: {\n    ID: string;\n    Node: string;\n    Address: string;\n    Datacenter: string;\n    TaggedAddresses: Record<string, any>;\n    Meta: Record<string, any>;\n    CreateIndex: number;\n    ModifyIndex: number;\n  };\n  Service: {\n    ID: string;\n    Service: string;\n    Tags: string[];\n    Address: string;\n    TaggedAddresses: Record<string, any>;\n    Meta: Record<string, any>;\n    Port: number;\n    Weights: Record<string, any>;\n    EnableTagOverride: boolean;\n    Proxy: Record<string, any>;\n    Connect: Record<string, any>;\n    PeerName: string;\n    CreateIndex: number;\n    ModifyIndex: number;\n  };\n  Checks: Array<{\n    Node: string;\n    CheckID: string;\n    Name: string;\n    Status: 'passing' | 'warning' | 'critical';\n    Notes: string;\n    Output: string;\n    ServiceID: string;\n    ServiceName: string;\n    ServiceTags: string[];\n    Type: string;\n    Interval: string;\n    Timeout: string;\n    ExposedPort: number;\n    Definition: Record<string, any>;\n    CreateIndex: number;\n    ModifyIndex: number;\n  }>;\n}\n\n```\n\n\n\n## 配置中心\n\n\n同时 consul 也能作为一个服务配置的地方，如下代码：\n\n```typescript\nimport { Controller, Get, Inject } from '@midwayjs/core';\nimport { ConsulService } from '@midwayjs/consul';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  consul: ConsulService;\n\n  @Get('/')\n  async home() {\n    await this.consul.kv.set('name', 'juhai');\n    const res = await this.consul.kv.get('name');\n    return res;\n  }\n}\n\n```\n\n我们调用 `kv.set` 方法，我们可以设置对应的配置，通过 `kv.get`  方法可以拿到对应的配置。\n\n\n注意：在代码中，有同学出现，在每次请求中去 get 对应的配置，这时你的 QPS 多少对 Consul server 的压力。\n\n\n所以在QPS比较大的情况，可以如下处理：\n\n```typescript\nimport { Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';\nimport { ConsulService } from '@midwayjs/consul';\n\n@Provide()\n@Scope(ScopeEnum.Singleton)\nexport class ConfigService {\n\n  @Inject()\n  consul: ConsulService;\n\n  config: any;\n\n  @Init()\n  async init() {\n    setInterval(async () => {\n      this.config = await this.consul.kv.get('name');\n    }, 5000);\n    this.config = await this.consul.kv.get('name');\n  }\n\n  async getConfig(){\n    return this.config;\n  }\n}\n\n```\n\n上面的代码，相当于定时去获取对应的配置，当每个请求进来的时候，获取 Scope 为 ScopeEnum.Singleton 服务的 `getConfig` 方法，这样每 5s 一次获取请求，就减少了对服务的压力。\n\nConsul 界面上如下图：\n\n![image.png](https://img.alicdn.com/imgextra/i4/O1CN01V3P6uK1rIVs19JiWn_!!6000000005608-2-tps-1500-374.png)\n\n![image.png](https://img.alicdn.com/imgextra/i2/O1CN014O2GyH1sMvIhmlbs4_!!6000000005753-2-tps-1500-667.png)\n\n\n一共提供如下几种方法：\n\n- [get](https://www.npmjs.com/package/consul#kv-get)，获取对应key的value\n- [keys](https://www.npmjs.com/package/consul#kv-keys)，查询某个prefix的key的列表\n- [set](https://www.npmjs.com/package/consul#kv-set)，设置对应的key的值\n- [del](https://www.npmjs.com/package/consul#kv-del)，删除对应的key\n\n\n","site/docs/extensions/cos.md":"# 腾讯云对象存储（COS）\n\n本文介绍了如何使用 midway 接入腾讯云 COS。\n\n相关信息：\n\n| 描述              |     |\n| ----------------- | --- |\n| 可用于标准项目    | ✅   |\n| 可用于 Serverless | ✅   |\n| 可用于一体化      | ✅   |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/cos@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/cos\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 引入组件\n\n\n首先，引入 组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as cos from '@midwayjs/cos';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    cos\t\t// 导入 cos 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n\n## 配置\n\n比如：\n\n\n**单客户端配置**\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  cos: {\n    client: {\n      SecretId: '***********',\n      SecretKey: '***********',\n    },\n  },\n}\n```\n\n\n**多个客户端配置，需要配置多个**\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  cos: {\n    clients: {\n      instance1: {\n        SecretId: '***********',\n        SecretKey: '***********',\n      },\n      instance2: {\n        SecretId: '***********',\n        SecretKey: '***********',\n      },\n    },\n  },\n}\n```\n更多参数可以查看 [cos-nodejs-sdk-v5](https://github.com/tencentyun/cos-nodejs-sdk-v5) 文档。\n\n\n## 使用 COS 服务\n\n\n我们可以在任意的代码中注入使用。\n```typescript\nimport { Provide, Controller, Inject, Get } from '@midwayjs/core';\nimport { COSService } from '@midwayjs/cos';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  cosService: COSService;\n\n  async invoke() {\n    await this.cosService.sliceUploadFile({\n      Bucket: 'test-1250000000',\n      Region: 'ap-guangzhou',\n      Key: '1.zip',\n      FilePath: './1.zip'\n    },\n  }\n}\n```\n\n\n可以使用 `COSServiceFactory` 获取不同的实例。\n```typescript\nimport { COSServiceFactory } from '@midwayjs/cos';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  cosServiceFactory: COSServiceFactory;\n\n  async save() {\n    const cos1 = await this.cosServiceFactory.get('instance1');\n    const cos2 = await this.cosServiceFactory.get('instance3');\n\n    //...\n\n  }\n}\n```\n\n","site/docs/extensions/cron.md":"# 本地任务\n\n和 bull 组件不同，cron 组件提供的是本地任务能力，即在每台机器的每个进程都会执行。如需不同机器或者不同进程之间只执行一次任务，请使用 [bull 组件](./bull) 。\n\n\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n\n\n## 安装组件\n\n```bash\n$ npm i @midwayjs/cron@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/cron\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 使用组件\n\n将组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as cron from '@midwayjs/cron';\n\n@Configuration({\n  imports: [\n    // ...\n    cron\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n\n\n## 编写任务处理类\n\n使用 `@Job` 装饰器装饰一个类，用于快速定义一个任务处理器。\n\n比如，在 `src/job` 目录中创建一个 `sync.job.ts`，用于某些数据同步任务，代码如下：\n\n```typescript\n// src/job/sync.job.ts\nimport { Job, IJob } from '@midwayjs/cron';\nimport { FORMAT } from '@midwayjs/core';\n\n@Job({\n  cronTime: FORMAT.CRONTAB.EVERY_PER_30_MINUTE,\n  start: true,\n})\nexport class DataSyncCheckerJob implements IJob {\n  async onTick() {\n    // ...\n  }\n}\n```\n\n`@Job` 装饰器用于修饰一个任务类，在初始化时，框架会自动将其转变为一个任务。\n\n任务类需要实现 `IJob` 接口，实现 `onTick` 方法，每当任务触发时，会自动调用 `onTick` 方法。\n\n此外，还有一个可选的 `onComplete` 方法，用于在 `onTick` 完成后执行。\n\n```typescript\n@Job({\n  cronTime: FORMAT.CRONTAB.EVERY_PER_30_MINUTE,\n  start: true,\n})\nexport class DataSyncCheckerJob implements IJob {\n  async onTick() {\n    // ...\n  }\n  \n  async onComplete() {\n    // 记录一些数据等等，用处不是很大\n  }\n}\n```\n\n\n\n`@Job` 装饰器的常用参数如下：\n\n| 参数      | 类型    | 描述                   |\n| --------- | ------- | ---------------------- |\n| cronTime  | string  | crontab 表达式         |\n| start     | boolean | 是否自动启动任务       |\n| runOnInit | boolean | 是否在初始化就执行一次 |\n\n更多参数，请参考 [Cron](https://github.com/kelektiv/node-cron)。\n\n\n\n## 任务管理\n\n除了定时执行任务，我们还通过框架提供的 API，对任务进行手动管理。\n\n比如，下面的代码仅仅定义了一个任务，但是不会启动执行。\n\n```typescript\n@Job('syncJob', {\n  cronTime: '*/2 * * * * *',\t// 每隔 2s 执行\n})\nexport class DataSyncCheckerJob implements IJob {\n  async onTick() {\n    // ...\n  }\n}\n```\n\n我们定义了一个名为 `syncJob` 的任务，并且给它了一个默认的调度时间。\n\n\n\n### 获取任务对象\n\n我们可以通过两种方式获取任务对象。\n\n通过`@InjectJob` 用来注入某个任务，参数为类本身或者任务名。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as cron from '@midwayjs/cron';\nimport { InjectJob, CronJob } from '@midwayjs/cron';\nimport { DataSyncCheckerJob } from './job/sync.job';\n\n@Configuration({\n  imports: [\n    cron\n  ],\n})\nexport class ContainerConfiguration {\n  @InjectJob(DataSyncCheckerJob)\n  syncJob: CronJob;\n  \n  @InjectJob('syncJob')\n  syncJob2: CronJob;\n\n  async onServerReady() {\n    // this.syncJob  === this.syncJob2\n  }\n}\n\n```\n\n通过 Framework API 获取。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as cron from '@midwayjs/cron';\nimport { InjectJob, CronJob } from '@midwayjs/cron';\nimport { DataSyncCheckerJob } from './job/sync.job';\n\n@Configuration({\n  imports: [\n    cron\n  ],\n})\nexport class ContainerConfiguration {\n  @Inject()\n  cronFramework: cron.Framework;\n\n  async onServerReady() {\n    const syncJob = this.cronFramework.getJob(DataSyncCheckerJob);\n    const syncJob2 = this.cronFramework.getJob('syncJob');\n    \n    // syncJob  === syncJob2\n  }\n}\n\n```\n\n:::caution\n\n注意，任务对象都必须在 `onServerReady` 生命周期或者启动之后才能获取。\n\n:::\n\n\n\n### 启停任务\n\n我们可以在初始化或者某些程序执行完成之后，将这个任务启动。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as cron from '@midwayjs/cron';\nimport { InjectJob, CronJob } from '@midwayjs/cron';\nimport { DataSyncCheckerJob } from './job/sync.job';\n\n@Configuration({\n  imports: [\n    cron\n  ],\n})\nexport class ContainerConfiguration {\n  @InjectJob(DataSyncCheckerJob)\n  syncJob: CronJob;\n\n  async onServerReady() {\n    this.syncJob.start();\n    \n    // ...\n    this.syncJob.stop();\n  }\n}\n\n```\n\n\n\n## 上下文\n\n任务执行是在请求作用域中，其有着特殊的 Context 对象结构。\n\n```typescript\nexport interface Context extends IMidwayContext {\n  job: CronJob;\n}\n```\n\n这里的  `CronJob` 类型来自于 [Cron](https://github.com/kelektiv/node-cron) 包。\n\n\n\n## 组件日志\n\n组件有着自己的日志，默认会将 `ctx.logger` 记录在 `midway-cron.log` 中。\n\n我们可以单独配置这个 logger 对象。\n\n```typescript\nexport default {\n  midwayLogger: {\n    // ...\n    clients: {\n      // ...\n      cronLogger: {\n        fileLogName: 'midway-cron.log',\n      },\n    }\n  }\n}\n```\n\n\n\n## 全局配置\n\n可以针对 Job 进行一些全局配置，会和每个 Job 的配置进行合并。\n\n```typescript\nexport default {\n  cron: {\n    defaultCronJobOptions: {\n      // ...\n    }\n  }\n}\n```\n\n这里的 `defaultCronJobOptions` 配置项请参考 [CronJobParameters](https://github.com/kelektiv/node-cron/blob/main/lib/job.js#L51)\n","site/docs/extensions/cross_domain.md":"# 跨域\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的通用跨域组件，支持 `cors` 、`jsonp` 多种模式。\n\n相关信息：\n\n| web 支持情况      |      |\n| ----------------- | ---- |\n| @midwayjs/koa     | ✅    |\n| @midwayjs/faas    | ✅    |\n| @midwayjs/web     | ✅    |\n| @midwayjs/express | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/cross-domain --save\n```\n\n## 引入组件\n\n在 `src/configuration.ts` 中引入组件。\n\n```typescript\nimport * as crossDomain from '@midwayjs/cross-domain';\n@Configuration({\n  imports: [\n    // ...other components\n    crossDomain\n  ],\n})\nexport class MainConfiguration {}\n```\n\n\n\n## 什么是跨域\n\n假设有两个网站：\n\n- **A.com**：这是你的网站，你想要从这里访问一些资源。\n- **B.com**：这是另一个网站，它拥有你想要访问的资源。\n\n### 场景设定\n\n1. **A.com** 是你的主网站，你在这里运行一些 JavaScript 代码。\n2. **B.com** 拥有一些 API 接口，你想要通过 A.com 的 JavaScript 代码来调用这些 API 接口。\n\n由于同源策略，浏览器默认不允许 A.com 的 JavaScript 代码直接访问 B.com 的资源。这是因为浏览器出于安全考虑，防止恶意网站读取其他网站的敏感数据。你想要在 A.com 上运行的 JavaScript 代码中发起一个请求到 B.com 的 API 接口，这个请求会被视为跨域请求。\n\n简单来说，从 A.com 访问 B.com 的接口，就是跨域。\n\n此外，跨域还有一些条件：\n\n* 1、同源策略是浏览器安全机制的一部分，所以一般只有浏览器访问才有跨域问题\n* 2、当浏览器发起跨域请求时，它会根据当前源自动添加一个 `Origin` 头部到请求中，服务端也会根据 `origin` 头判断来源，进而做处理\n\n所以，当你发现跨域设置不生效时，请检查：\n\n* 1、是否真的是跨域请求\n* 2、是否真的从浏览器发起\n* 3、是否请求带有 `origin` 头\n\n\n\n## 什么是 CORS\n\n在前端代码调用后端服务中经常会碰到下面的错误，这就最为常见的跨域 [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) 错误。\n\n```\nAccess to fetch at 'http://127.0.0.1:7002/' from origin 'http://127.0.0.1:7001' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.\n```\n\n出于安全性，浏览器限制脚本内发起的跨源 HTTP 请求。例如，`XMLHttpRequest` 和 [Fetch API](https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API) 遵循[同源策略](https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy)。这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源，除非响应报文包含了正确 CORS 响应头。\n\nCORS 机制允许 Web 应用服务器进行跨源访问控制，从而使跨源数据传输得以安全进行。现代浏览器支持在 API 容器中（例如 [`XMLHttpRequest`](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest) 或 [Fetch](https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API)）使用 CORS，以降低跨源 HTTP 请求所带来的风险。\n\n\n\n## 常见的 CORS 配置\n\n下面列举几种跨域的解决方案，我们以 `fetch` 方法为例。\n\n### 未使用 `credentials` \n\n客户端。\n\n```javascript\nfetch(url);\n```\n\n服务端配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  cors: {\n    origin: '*',\n  },\n}\n```\n\n### 使用 `credentials`\n\n客户端。\n\n```javascript\nfetch(url, {\n  credentials: \"include\",\n});\n```\n\n服务端配置\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  cors: {\n    credentials: true,\n  },\n}\n```\n\n### 限制 `origin` 来源\n\n假如我们的网页地址为 `http://127.0.0.1:7001` 而接口为 `http://127.0.0.1:7002`。\n\n客户端。\n\n```javascript\nfetch('http://127.0.0.1:7002/', {\n  credentials: 'include'\n})\n```\n\n服务端配置，注意，由于启用了 `credentials`，这个时候 `origin` 字段不能为 `*`。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  cors: {\n    origin: 'http://127.0.0.1:7001',\n    credentials: true,\n  },\n}\n```\n\n\n\n## 更多 CORS 配置\n\n完整可用配置如下：\n\n```typescript\nexport const cors = {\n  // 允许跨域的方法，【默认值】为 GET,HEAD,PUT,POST,DELETE,PATCH\n  allowMethods: string |string[];\n  // 设置 Access-Control-Allow-Origin 的值，【默认值】会获取请求头上的 origin\n  // 也可以配置为一个回调方法，传入的参数为 request，需要返回 origin 值\n  // 例如：http://test.midwayjs.org\n  // 如果设置了 credentials，则 origin 不能设置为 *\n  origin: string|Function;\n  // 设置 Access-Control-Allow-Headers 的值，【默认值】会获取请求头上的 Access-Control-Request-Headers\n  allowHeaders: string |string[];\n  // 设置 Access-Control-Expose-Headers 的值\n  exposeHeaders: string |string[];\n  // 设置 Access-Control-Allow-Credentials，【默认值】false\n   // 也可以配置为一个回调方法，传入的参数为 request，返回值为 true 或 false\n  credentials: boolean|Function;\n  // 是否在执行报错的时候，把跨域的 header 信息写入到 error 对的 headers 属性中，【默认值】false\n  keepHeadersOnError: boolean;\n  // 设置 Access-Control-Max-Age\n  maxAge: number;\n}\n```\n\n\n## JSONP 配置\n\n可以在 `src/config/config.default` 中进行 JSONP 配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  jsonp: {\n    callback: 'jsonp',\n    limit: 512,\n  },\n}\n```\n","site/docs/extensions/crud.md":"# CRUD\n\n本文档介绍如何在 Midway 中使用 `@midwayjs/crud`。\n\n`@midwayjs/crud` 不是一个“只能自动生成接口”的脚手架，它本质上提供的是一套**面向资源的标准 CRUD 能力**：\n\n- 统一的增删改查 service 抽象\n- 统一的分页 / 排序 / 过滤 / 搜索协议\n- 可选的 REST 路由快捷生成\n- 和现有 validation / swagger / web 路由体系的接入\n\n如果你经常在不同模块里重复写下面这些代码：\n\n- `findAndCount`\n- `save`\n- `update`\n- `delete`\n- 手动解析 `page`、`limit`、`sort`\n- 为每个资源写一套几乎一样的 Controller\n\n那这个组件就是用来把这些重复劳动收起来的。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n## 这个组件能做什么\n\n先用一句话概括：\n\n**先提供一个可复用的 CRUD service，再按需把它暴露成 HTTP 接口。**\n\n这意味着它支持两种使用方式：\n\n1. 只把它当数据访问层能力来用  \n   适合你的业务逻辑比较复杂，不想让组件替你生成接口。\n\n2. 把它当接口快捷层来用  \n   适合资源型接口很多，希望快速拿到统一的 REST API。\n\n所以它解决的是“资源型接口的重复代码”问题，但**不会替代你的业务 service**。\n\n复杂业务依然应该写在你自己的 service 里，例如：\n\n- 下单时要检查库存、优惠券、支付状态\n- 创建用户时要同步多个系统\n- 删除资源前要做权限和状态机校验\n\n这些逻辑应该继续在你的业务 service 中完成，CRUD 组件只是提供一个稳定的资源操作基座。\n\n## 核心概念\n\n在开始之前，先理解这三个层次。\n\n### 1. `CrudService<T>`\n\n这是最核心的抽象，定义了统一的资源操作接口。\n\n```ts\nlist(query)\nfindOne(id)\ncreate(data)\nupdate(id, data)\ndelete(id)\n```\n\n你可以把它理解成“一个标准化的资源仓储服务接口”。\n\n### 2. 数据库适配层\n\nMidway CRUD 目前提供了 4 个官方适配基类：\n\n- `TypeOrmCrudService<T>`\n- `MikroCrudService<T>`\n- `SequelizeCrudService<T>`\n- `MongooseCrudService<T>`\n\n它们都实现了相同的 CRUD 核心接口，但底层分别接不同的数据访问组件。\n\n### 3. HTTP 暴露层\n\n这是可选的。\n\n如果你希望快速生成路由，可以使用：\n\n- 类式：`@Crud()`\n- 函数式：`defineCrudRoutes()`\n\n这两种方式都只是把同一个 `CrudService` 暴露成 HTTP 接口，不会生成第二套独立逻辑。\n\n## 什么时候适合用\n\n适合：\n\n- 大量资源型接口，结构相似\n- 列表查询都需要统一分页、排序、过滤\n- 想减少重复的 Repository / Model 调用代码\n- 想让不同模块的 API 风格保持一致\n\n不适合：\n\n- 主要是复杂工作流，不是标准资源接口\n- 一个接口需要跨多个聚合、多个事务、多个外部系统\n- 你希望所有查询语义都完全自由，不接受统一约束\n\n如果一个模块的核心不是“资源管理”，而是“复杂业务流程”，那更适合直接写普通 Controller + Service，而不是强行套 CRUD。\n\n## 安装依赖\n\n先安装 CRUD 组件本身：\n\n```bash\n$ npm i @midwayjs/crud@4 --save\n```\n\n然后根据你使用的数据库组件安装对应依赖。\n\n```bash\n# TypeORM\n$ npm i @midwayjs/typeorm@4 typeorm@^1.0.0 --save\n\n# MikroORM\n$ npm i @midwayjs/mikro@4 @mikro-orm/core@^6 --save\n\n# Sequelize\n$ npm i @midwayjs/sequelize@4 sequelize sequelize-typescript --save\n\n# Mongoose\n$ npm i @midwayjs/mongoose@4 mongoose --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/crud\": \"^4.0.0\",\n    \"@midwayjs/typeorm\": \"^4.0.0\",\n    \"@midwayjs/mikro\": \"^4.0.0\",\n    \"@midwayjs/sequelize\": \"^4.0.0\",\n    \"@midwayjs/mongoose\": \"^4.0.0\",\n    \"typeorm\": \"^1.0.0\",\n    \"@mikro-orm/core\": \"^6.4.5\",\n    \"sequelize\": \"^6.37.5\",\n    \"sequelize-typescript\": \"^2.1.6\",\n    \"mongoose\": \"^8.9.5\"\n  }\n}\n```\n\n## 启用组件\n\n在 `src/configuration.ts` 中引入组件。\n\n下面以 `koa + typeorm + crud` 为例：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as typeorm from '@midwayjs/typeorm';\nimport * as crud from '@midwayjs/crud';\n\n@Configuration({\n  imports: [\n    koa,\n    typeorm,\n    crud,\n  ],\n})\nexport class MainConfiguration {}\n```\n\n如果你使用其他数据库组件，只需要把对应组件加到 `imports` 中即可。\n\n## 入门：先从 service-only 开始\n\n对于新手来说，最容易理解的方式不是先生成路由，而是先把它当一个“可复用的数据服务”。\n\n这也是推荐的理解顺序。\n\n### 为什么推荐先学 service-only\n\n因为这样你能先理解：\n\n- 组件真正提供的核心是什么\n- 业务代码应该写在哪里\n- 路由层只是一个可选外壳\n\n如果一上来就只看 `@Crud()`，很容易误以为这只是一个自动生成接口的装饰器。\n\n### 最小 TypeORM 示例\n\n先定义一个资源 service：\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { TypeOrmCrudService } from '@midwayjs/crud/typeorm';\nimport { Repository } from 'typeorm';\n\nimport { UserEntity } from '../entity/user';\n\n@Provide()\nexport class UserCrudService extends TypeOrmCrudService<UserEntity> {\n  @InjectEntityModel(UserEntity)\n  repo: Repository<UserEntity>;\n}\n```\n\n然后在业务 service 里直接组合它：\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  userCrudService: UserCrudService;\n\n  async listUsers() {\n    return this.userCrudService.list({\n      page: 1,\n      limit: 20,\n      sort: [],\n      filters: [],\n    });\n  }\n\n  async createUser(input: CreateUserDTO) {\n    return this.userCrudService.create(input);\n  }\n}\n```\n\n这时候：\n\n- 你已经获得统一的 CRUD 能力\n- 但**不会自动生成任何 HTTP 路由**\n- 业务仍然由你自己的 `UserService` 负责组织\n\n这也是最适合复杂业务场景的用法。\n\n## 其他数据库适配\n\n如果你的项目不是 TypeORM，也可以使用其他官方适配。\n\n### MikroORM\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectRepository } from '@midwayjs/mikro';\nimport { MikroCrudService } from '@midwayjs/crud/mikro';\n\n@Provide()\nexport class UserCrudService extends MikroCrudService<UserEntity> {\n  @InjectRepository(UserEntity)\n  repo;\n}\n```\n\n### Sequelize\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectRepository } from '@midwayjs/sequelize';\nimport { SequelizeCrudService } from '@midwayjs/crud/sequelize';\nimport { Repository } from 'sequelize-typescript';\n\n@Provide()\nexport class UserCrudService extends SequelizeCrudService<UserModel> {\n  @InjectRepository(UserModel)\n  repo: Repository<UserModel>;\n}\n```\n\n### Mongoose\n\n```typescript\nimport { Inject, Provide } from '@midwayjs/core';\nimport { MongooseDataSourceManager } from '@midwayjs/mongoose';\nimport { MongooseCrudService } from '@midwayjs/crud/mongoose';\n\n@Provide()\nexport class UserCrudService extends MongooseCrudService<UserDocument> {\n  @Inject()\n  mongooseDataSourceManager: MongooseDataSourceManager;\n\n  async onReady() {\n    this.repo = this.mongooseDataSourceManager\n      .getDataSource('default')\n      .model('User');\n  }\n}\n```\n\n这几个适配基类的对外用法保持一致，区别主要在于底层仓储注入方式和 ORM 行为。\n\n## 快速生成类式 REST 接口\n\n当你已经有一个 `CrudService` 之后，如果希望快速生成标准资源型接口，可以使用 `@Crud()`。\n\n### 最小类式示例\n\n```typescript\nimport { Controller, Inject } from '@midwayjs/core';\nimport { Crud } from '@midwayjs/crud';\n\nimport { UserEntity } from '../entity/user';\nimport { UserCrudService } from '../service/user.crud';\n\n@Controller('/users')\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n})\nexport class UserController {\n  @Inject()\n  crudService: UserCrudService;\n}\n```\n\n默认会生成：\n\n- `GET /users`\n- `GET /users/:id`\n- `POST /users`\n- `PATCH /users/:id`\n- `DELETE /users/:id`\n\n### 为什么还要 `@Inject() crudService`\n\n因为 `@Crud()` 只是声明“这个 Controller 是一个 CRUD 资源”，真正执行业务的是你绑定的 `crudService`。\n\n也就是说：\n\n- `@Crud()` 负责生成默认路由\n- `crudService` 负责真正执行 CRUD 逻辑\n\n这是这个组件最重要的设计原则之一。\n\n### 业务逻辑写在哪里\n\n不要把复杂业务逻辑塞到 `@Crud()` 里。\n\n推荐做法是：\n\n- 资源级默认行为：写在 `UserCrudService`\n- 复杂业务编排：写在你自己的 `UserService` / Domain Service\n- 非标准动作：写成普通路由方法\n\n例如：\n\n```typescript\n@Controller('/users')\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n})\nexport class UserController {\n  @Inject()\n  crudService: UserCrudService;\n\n  async create() {\n    // 自定义事务、调用多个 service、做额外校验\n  }\n\n  async resetPassword() {\n    // 非标准资源动作\n  }\n}\n```\n\n同名方法会优先使用你手写的实现，因此你可以只覆写部分默认行为。\n\n### 裁剪默认路由\n\n如果你不想暴露所有默认路由，可以通过 `routes.only` 或 `routes.exclude` 控制。\n\n```typescript\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n  routes: {\n    only: ['list', 'detail', 'create'],\n  },\n})\n```\n\n这对于“只能查、不能删”或“只开放后台管理的一部分动作”的场景很有用。\n\n## 函数式路由模式\n\n如果项目使用函数式 API，而不是类式 Controller，可以从 `@midwayjs/crud/functional` 导入 `defineCrudRoutes()`。\n\n### 最小示例\n\n```typescript\nimport { defineApi } from '@midwayjs/core/functional';\nimport { defineCrudRoutes } from '@midwayjs/crud/functional';\n\nimport { UserEntity } from '../entity/user';\nimport { UserCrudService } from '../service/user.crud';\n\nconst crudRoutes = defineCrudRoutes<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n});\n\nexport default defineApi('/users', api => ({\n  ...crudRoutes(api),\n}));\n```\n\n### 和自定义动作一起使用\n\n函数式模式最常见的用法，是把默认 CRUD 路由和自定义动作合并在同一个 `defineApi()` 里。\n\n```typescript\nexport default defineApi('/users', api => ({\n  ...crudRoutes(api),\n  resetPassword: api\n    .post('/:id/reset-password')\n    .handle(async ({ input, ctx }) => {\n      return { ok: true };\n    }),\n}));\n```\n\n这样可以让“标准资源动作”和“业务动作”共存在同一个资源路由下。\n\n## 查询协议\n\n列表接口使用统一的查询协议，这是这个组件最重要的能力之一。\n\n### 支持的 query 参数\n\n- `page=<number>`\n- `limit=<number>`\n- `sort=<field>:<ASC|DESC>`，可重复传入\n- `filter=<field>||<operator>||<value>`，可重复传入\n- `search=<keyword>`\n- `join=<relation>`，可重复传入\n- `fields=<field1,field2,...>`\n\n例如：\n\n```text\nGET /users?page=1&limit=20&sort=createdAt:DESC&filter=status||eq||active&search=harry&join=profile\n```\n\n### 支持的过滤操作符\n\n首阶段支持：\n\n- `eq`\n- `ne`\n- `gt`\n- `gte`\n- `lt`\n- `lte`\n- `in`\n- `like`\n\n### 查询协议的约束\n\n这些约束是有意设计出来的，用来保证资源接口的一致性：\n\n- `sort` 字段必须在 `sortable` 白名单里\n- `filter` 字段必须在 `filterable` 白名单里\n- `search` 只会作用于 `searchable` 白名单\n- `join` 必须在 `join` 白名单里\n- `join` 首阶段只支持一层关系名，不支持 `profile.company`\n\n也就是说，这个组件不会让客户端随意拼接任意字段查询，而是让你在服务端声明资源允许暴露的查询能力。\n\n### 在 `@Crud()` 中声明查询能力\n\n```typescript\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n  query: {\n    defaultLimit: 20,\n    maxLimit: 100,\n    sortable: ['id', 'createdAt'],\n    filterable: ['status'],\n    searchable: ['name', 'email'],\n    join: ['profile'],\n  },\n})\n```\n\n这样配置后，查询行为就会按这个白名单执行。\n\n## 返回结构\n\n默认列表接口会返回统一的分页结构，而不是直接返回数组。\n\n```typescript\ntype CrudPageResult<T> = {\n  data: T[];\n  meta: {\n    page: number;\n    limit: number;\n    total: number;\n    pageCount: number;\n    hasNext: boolean;\n    hasPrev: boolean;\n  };\n};\n```\n\n这能让前端和不同资源接口之间保持一致的分页消费方式。\n\n其他默认返回规则：\n\n- `detail` 返回单个资源对象\n- `create` 返回创建后的资源对象\n- `update` 返回更新后的资源对象\n- `delete` 默认返回 `204 No Content`\n\n## DTO、Validation 与 Swagger\n\nCRUD 路由会尽量复用现有 Midway 组件，而不是另起一套协议。\n\n### DTO 绑定\n\n你可以在 `@Crud()` 中声明：\n\n- `dto.create`\n- `dto.update`\n- `dto.replace`\n- `dto.query`\n\n例如：\n\n```typescript\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n  dto: {\n    create: CreateUserDTO,\n    update: UpdateUserDTO,\n    query: UserQueryDTO,\n  },\n})\n```\n\n### Validation\n\n如果项目里启用了 `@midwayjs/validation` 或兼容的 validation service：\n\n- `dto.create` 会用于创建请求体校验\n- `dto.update` 会用于更新请求体校验\n- `dto.replace` 会用于替换请求体校验\n- `dto.query` 会用于列表查询参数校验\n\n如果没有安装 validation 组件，CRUD 本身不会强行报错，而是只跳过这一步自动校验。\n\n### Swagger\n\n自动生成的 CRUD 路由会复用现有 Web 元数据链，所以 Swagger 组件可以识别到这些路由。\n\n也就是说：\n\n- 默认路由会出现在 Swagger 文档里\n- 基础的 path / query / body / response 元数据会被自动补齐\n\n这意味着你不需要为每一个简单资源接口手动补一遍同样的 swagger 装饰器。\n\n## 软删除\n\n默认删除行为是**硬删除**。\n\n如果你希望某个资源使用软删除，需要显式开启：\n\n```typescript\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n  delete: {\n    mode: 'soft',\n  },\n})\n```\n\n开启后：\n\n- `DELETE /:id` 会走软删除\n- `list` / `detail` 默认排除已软删的数据\n- 如果底层实体或仓储不支持软删除，会直接报错\n\n### 为什么不是默认软删除\n\n因为软删除不是所有资源都适合：\n\n- 会影响唯一键约束\n- 会影响查询逻辑\n- 会影响索引设计\n- 会影响后台数据管理\n\n所以这里采用的是“显式开启”的策略，而不是默认偷偷帮你切换成软删。\n\n## 一个更完整的类式示例\n\n下面给一个稍完整一点的例子，把前面的内容串起来。\n\n```typescript\nimport { Controller, Inject, Provide } from '@midwayjs/core';\nimport { Crud } from '@midwayjs/crud';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { TypeOrmCrudService } from '@midwayjs/crud/typeorm';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class UserCrudService extends TypeOrmCrudService<UserEntity> {\n  @InjectEntityModel(UserEntity)\n  repo: Repository<UserEntity>;\n}\n\n@Controller('/users')\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n  dto: {\n    create: CreateUserDTO,\n    update: UpdateUserDTO,\n    query: UserQueryDTO,\n  },\n  query: {\n    defaultLimit: 20,\n    maxLimit: 100,\n    sortable: ['id', 'createdAt'],\n    filterable: ['status'],\n    searchable: ['name', 'email'],\n    join: ['profile'],\n  },\n  delete: {\n    mode: 'soft',\n  },\n})\nexport class UserController {\n  @Inject()\n  crudService: UserCrudService;\n}\n```\n\n这段代码的效果是：\n\n- 生成一个 `/users` 资源接口\n- 自动获得标准 CRUD 路由\n- 自动使用统一查询协议\n- 自动按 DTO 做校验（如果启用了 validation）\n- 自动被 swagger 扫描\n- 删除逻辑使用软删除\n\n## 二级导出\n\n组件提供这些稳定入口：\n\n- `@midwayjs/crud`\n- `@midwayjs/crud/typeorm`\n- `@midwayjs/crud/mikro`\n- `@midwayjs/crud/sequelize`\n- `@midwayjs/crud/mongoose`\n- `@midwayjs/crud/functional`\n\n建议这样理解：\n\n- 主入口：核心类型 + `@Crud()`\n- 数据库二级入口：各自的 CRUD service 适配\n- `functional`：函数式路由适配\n\n## 最后的建议\n\n如果你是第一次接触这个组件，建议按这个顺序使用：\n\n1. 先学 `service-only`，把它当一个标准 CRUD service\n2. 再用 `@Crud()` 快速展开简单资源接口\n3. 最后再用函数式模式或高级查询配置\n\n这样更容易理解它的边界，也更不容易把复杂业务错误地塞进自动 CRUD 层。\n","site/docs/extensions/egg.md":"# EggJS\n\nMidway 可以使用 EggJS 作为上层 Web 框架，EggJS 提供了非常多常用的插件和 API，帮助用户快速构建企业级 Web 应用。本章节内容，主要介绍 EggJS 在 Midway 中如何使用自身的能力。\n\n| 描述              |      |\n| ----------------- | ---- |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/web@4 egg --save\n$ npm i @midwayjs/egg-ts-helper --save-dev\n```\n\n针对 EggJS 场景，这些包列举如下。\n\n```json\n  \"dependencies\": {\n    \"@midwayjs/web\": \"^4.0.0\",\n    \"@midwayjs/core\": \"^4.0.0\",\n    \"egg\": \"^2.0.0\",\n    \"egg-scripts\": \"^2.10.0\"\n  },\n  \"devDependencies\": {\n    \"@midwayjs/egg-ts-helper\": \"^1.0.1\",\n  },\n```\n\n| @midwayjs/web           | **必须**，Midway EggJS 适配层    |\n|-------------------------|----------------------------|\n| @midwayjs/core          | **必须**，Midway 核心包          |\n| egg                     | **必须**，EggJS 依赖包，提供定义等其他能力 |\n| egg-scripts             | **可选**，EggJS 启动脚本          |\n| @midwayjs/egg-ts-helper | **可选**，EggJS 定义生成工具        |\n\n也可以直接使用脚手架创建示例。\n\n```bash\n# npm v6\n$ npm init midway --type=egg-v3 my_project\n\n# npm v7\n$ npm init midway -- --type=egg-v3 my_project\n```\n\n\n\n## 开启组件\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as web from '@midwayjs/web';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [web],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: web.Application;\n\n  async onReady() {\n\t\t// ...\n  }\n}\n\n```\n\n\n\n## 和默认 EggJS 的不同之处\n\n\n- 1、从 v3 开始，midway 提供了更多的组件，大部分 egg 内置插件默认禁用\n- 2、baseDir 默认调整为 `src` 目录，服务器上为 `dist` 目录\n- 3、禁用 egg-logger，全部替换为 @midwayjs/logger，不可切换\n\n\n\n整个架构如下:\n![](https://cdn.nlark.com/yuque/0/2021/png/501408/1614842824740-fc0c1432-3ace-4f77-b51f-15212984b168.png)\n\n\n## 目录结构\n\n\n除了 Midway 提供的目录结构外，EggJS 还有一些特殊的目录结构（不可变），整个结构如下。\n```\n➜  my_midway_app tree\n.\n├── src\n|   ├── app.ts \t\t\t\t\t\t\t\t\t\t \t\t\t\t## EggJS 扩展 Worker 生命周期文件（可选）\n|   ├── agent.ts\t\t\t\t\t\t\t\t   \t\t\t\t## EggJS 扩展 Agent 生命周期文件（可选）\n|   ├── app\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t## EggJS 固定的根目录（可选）\n|   │   ├── public\t\t \t\t\t\t\t\t\t\t  \t## EggJS 静态托管插件的默认目录（可配）\n|   │   |   └── reset.css\n|   │   ├── view (可选)\t\t\t\t\t\t\t\t\t\t ## EggJS 模板渲染的默认目录（可配）\n|   │   |   └── home.tpl\n|   │   └── extend (可选)\t\t\t\t\t\t\t\t\t ## EggJS 扩展目录（可配）\n|   │       ├── helper.ts (可选)\n|   │       ├── request.ts (可选)\n|   │       ├── response.ts (可选)\n|   │       ├── context.ts (可选)\n|   │       ├── application.ts (可选)\n|   │       └── agent.ts (可选)\n|   │\n|   ├── config\n|   |   ├── plugin.ts\n|   |   ├── config.default.ts\n|   |   ├── config.prod.ts\n|   |   ├── config.test.ts (可选)\n|   |   ├── config.local.ts (可选)\n|   |   └── config.unittest.ts (可选)\n│   ├── controller\t\t\t\t\t\t\t\t \t\t\t\t## Midway 控制器目录（推荐）\n│   ├── service\t\t\t\t\t\t\t\t     \t\t\t\t## Midway 服务目录（推荐）\n│   └── schedule\t\t\t\t\t\t\t\t\t \t\t\t\t## Midway 定时器目录（推荐）\n│\n├── typings                        \t\t\t\t## EggJS 定义生成目录\n├── test\n├── package.json\n└── tsconfig.json\n```\n以上是 EggJS 的目录结构全貌，其中包含了很多 EggJS 特有的目录，有一些在 Midway 体系中已经有相应的能力替代，可以直接替换。整个结构，基本上等价于将 EggJS 的目录结构移动到了 `src` 目录下。\n\n\n由于 EggJS 是基于约定的框架，整个工程的目录结构是固定的，这里列举一些常用的约定目录。\n\n| `src/app/public/**`  | 用于放置静态资源，可选，具体参见内置插件 [egg-static](https://github.com/eggjs/egg-static)。 |\n| --- | --- |\n| `src/config/config.{env}.ts` | 用于编写配置文件，具体参见[配置](https://eggjs.org/zh-cn/basics/config.html)。 |\n| `src/config/plugin.js` | 用于配置需要加载的插件，具体参见[插件](https://eggjs.org/zh-cn/basics/plugin.html)。 |\n| `test/**` | 具体参见[单元测试](https://eggjs.org/zh-cn/core/unittest.html)。 |\n| `src/app.js` 和 `src/agent.js` | 用于自定义启动时的初始化工作，可选，具体参见[启动自定义](https://eggjs.org/zh-cn/basics/app-start.html)。关于`agent.js`的作用参见[Agent机制](https://eggjs.org/zh-cn/core/cluster-and-ipc.html#agent-%E6%9C%BA%E5%88%B6)。 |\n\n\n\n## 配置定义\n\n\nMidway 在脚手架中提供了标准的 EggJS 的 TS 配置写法，MidwayConfig 中包括了 egg 中配置的定义和属性提示，结构如下。\n```typescript\n// src/config/config.default.ts\nimport { MidwayConfig, MidwayAppInfo } from '@midwayjs/core';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // use for cookie sign key, should change to your own and keep security\n    keys: appInfo.name + '_xxxx',\n    egg: {\n      port: 7001,\n    },\n    // security: {\n    //   csrf: false,\n    // },\n  } as MidwayConfig;\n};\n\n```\n通过这样返回方法的形式，在运行期会被自动执行，合并进完整的配置对象。\n\n\n这个函数的参数为 `MidwayAppConfig` 类型，值为以下内容。\n\n| **appInfo** | **说明** |\n| --- | --- |\n| pkg | package.json |\n| name | 应用名，同 pkg.name |\n| baseDir | 应用代码的 src （本地开发）或者 dist （上线后）目录 |\n| appDir | 应用代码的目录 |\n| HOME | 用户目录，如 admin 账户为 /home/admin |\n| root | 应用根目录，只有在 local 和 unittest 环境下为 baseDir，其他都为 HOME。 |\n\n\n\n:::info\n注意，这里的 `baseDir` 和 `appDir` 和 EggJS 应用有所区别。\n:::\n\n\n\n\n## 使用 Egg 插件\n\n插件是 EggJS 的特色之一，`@midwayjs/web` 也支持 EggJS 的插件体系，但是在有 Midway 组件的情况下，尽可能优先使用 Midway 组件。\n\n\n插件一般通过 npm 模块的方式进行复用。\n```bash\n$ npm i egg-mysql --save\n```\n然后需要在应用或框架的 `src/config/plugin.js` 中声明开启。\n\n\n如果有 `export default` ，请写在其中。\n```typescript\nimport { EggPlugin } from 'egg';\nexport default {\n  static: false, // default is true\n  mysql: {\n    enable: true,\n    package: 'egg-mysql'\n  }\n} as EggPlugin;\n\n```\n如果没有 `export default` ，可以直接导出。\n```typescript\n// src/config/plugin.ts\n// 使用 mysql 插件\nexport const mysql = {\n  enable: true,\n  package: 'egg-mysql',\n};\n```\n\n\n在开启插件之后，我们就可以在业务代码中使用插件提供的功能了。一般来说，插件会将对象挂载到 EggJS 的 `app` 和 `ctx` 之上，然后直接使用。\n\n\n```typescript\napp.mysql.query(sql, values);\t\t\t// egg 提供的方法\n```\n在 Midway 中可以通过 `@App` 获取 `app` 对象，以及在请求作用域中通过 `@Inject() ctx` 获取 `ctx` 对象，所以我们可以通过注入来获取插件对象。\n\n\n```typescript\nimport { Provide, Inject, Get } from '@midwayjs/core';\nimport { Application, Context } from '@midwayjs/web';\n\n@Provide()\nexport class HomeController {\n\n  @App()\n  app: Application;\n\n  @Inject()\n  ctx: Context;\n\n  @Get('/')\n  async home() {\n  \tthis.app.mysql.query(sql, values);    // 调用 app 上的方法（如果有的话）\n    this.ctx.mysql.query(sql, values);\t\t// 调用挂载在 ctx 上的方法（如果有的话）\n  }\n}\n```\n此外，还可以通过 `@Plugin` 装饰器来直接注入 `app` 挂载的插件，默认情况下，如果不传参数，将以属性名作为 key。\n\n\n```typescript\nimport { Provide, Get, Plugin } from '@midwayjs/core';\n\n@Provide()\nexport class HomeController {\n\n  @Plugin()\n  mysql: any;\n\n  @Get('/')\n  async home() {\n  \tthis.mysql.query(sql, values);\n  }\n}\n```\n:::info\n`@Plugin() mysql`  等价于 `app.mysql` 。 `@Plugin` 的作用就是从 app 对象上拿对应属性名的插件，所以 `@Plugin() xxx` 就等于 `app['xxx']` 。\n:::\n\n\n## Web 中间件\n\n\n中间件样例如下：\n\n\n```typescript\nimport { Middleware, IMiddleware } from '@midwayjs/core';\nimport { Context, NextFunction } from '@midwayjs/web';\n\n@Middleware()\nexport class ReportMiddleware implements IMiddleware<Context, NextFunction> {\n\n  resolve() {\n    return async (ctx: Context, next: NextFunction) => {\n      const startTime = Date.now();\n      await next();\n      console.log(Date.now() - startTime);\n    };\n  }\n\n}\n```\n\n:::caution\n注意\n\n1、如果要继续使用 EggJS 传统的函数式写法，必须将文件放在 `src/app/middleware` 下\n\n2、egg 自带的内置中间件已经集成\n\n:::\n\n应用中间件。\n\n```typescript\n// src/configuration.ts\nimport { App, Configuration } from '@midwayjs/core';\nimport * as egg from '@midwayjs/web';\nimport { ReportMiddleware } from './middleware/user.middleware';\n\n@Configuration({\n  imports: [egg]\n  // ...\n})\nexport class MainConfiguration {\n\n  @App()\n  app: egg.Application;\n\n  async onReady() {\n    this.app.useMiddleware(ReportMiddleware);\n  }\n}\n\n```\n\n更多用法请参考 [Web 中间件](../middleware)\n\n\n\n## 中间件顺序\n\n由于 egg 也有自己的中间件逻辑，在新版本中，我们将中间件加载顺序做了一定的处理，执行顺序如下：\n\n- 1、egg 框架中的中间件\n- 2、egg 插件通过 config.coreMiddleware 添加的顺序\n- 3、业务代码配置在 config.middleware 中配置的顺序\n- 4、app.useMiddleware 添加的顺序\n\n因为 midway 的中间件会后置加载，所以我们可以在 onReady 中进行自定义排序。\n\n\n\n## BodyParser\n\negg 自带  `bodyParser` 功能，默认会解析 `Post` 请求，自动识别 `json` 和 `form` 类型。\n\n如需 text 或者 xml，可以自行配置。\n\n默认的大小限制为 `1mb`，可以单独对每项配置大小。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  bodyParser: {\n    formLimit: '1mb',\n    jsonLimit: '1mb',\n    textLimit: '1mb',\n    xmlLimit: '1mb',\n  },\n}\n```\n\n注意，使用 Postman 做 Post 请求时的类型选择：\n\n![postman](https://img.alicdn.com/imgextra/i4/O1CN01QCdTsN1S347SuzZU5_!!6000000002190-2-tps-1017-690.png)\n\n\n\n\n## 定时任务\nv3 开始请参考 [bull 组件](./bull) 。\n\n如需兼容之前的 [egg 定时任务](https://eggjs.org/zh-cn/basics/schedule.html) ，请照下列方法。\n\n首先安装 `midway-schedule` 依赖。\n\n```bash\n$ npm i midway-schedule --save\n```\n\n添加到插件中即可。\n\n```typescript\n// src/config/plugin.ts\nexport default {\n  schedule: true,\n  schedulePlus: {\n    enable: true,\n    package: 'midway-schedule',\n  },\n};\n```\n\n使用请参考上一版本文档。\n\n\n\n## 日志\n\nv3 开始无法使用 egg-logger，请参考 [日志](../logger) 章节。\n\n\n\n## 异常处理\n\nEggJS 框架通过 [onerror](https://github.com/eggjs/egg-onerror) 插件提供了统一的错误处理机制，会作为 Midway 的兜底错误逻辑，和 [错误过滤器](../error_filter) 不冲突。\n\n对一个请求的所有处理方法（Middleware、Controller、Service）中抛出的任何异常都会被它捕获，并自动根据请求想要获取的类型返回不同类型的错误（基于 [Content Negotiation](https://tools.ietf.org/html/rfc7231#section-5.3.2)）。\n\n\n\n| 请求需求的格式 | 环境 | errorPageUrl 是否配置 | 返回内容 |\n| --- | --- | --- | --- |\n| HTML & TEXT | local & unittest | - | onerror 自带的错误页面，展示详细的错误信息 |\n| HTML & TEXT | 其他 | 是 | 重定向到 errorPageUrl |\n| HTML & TEXT | 其他 | 否 | onerror 自带的没有错误信息的简单错误页（不推荐） |\n| JSON & JSONP | local & unittest | - | JSON 对象或对应的 JSONP 格式响应，带详细的错误信息 |\n| JSON & JSONP | 其他 | - | JSON 对象或对应的 JSONP 格式响应，不带详细的错误信息 |\n\n\n\n\nonerror 插件的配置中支持 errorPageUrl 属性，当配置了 errorPageUrl 时，一旦用户请求线上应用的 HTML 页面异常，就会重定向到这个地址。\n\n\n在 `src/config/config.default.ts` 中\n```typescript\n// src/config/config.default.ts\nmodule.exports = {\n  onerror: {\n    // 线上页面发生异常时，重定向到这个页面上\n    errorPageUrl: '/50x.html',\n  },\n};\n```\n\n\n\n## 扩展 Application/Context/Request/Response\n\n\n### 增加扩展逻辑\n\n\n虽然 MidwayJS 并不希望直接将属性挂载到 koa 的 Context，App 上（会造成管理和定义的不确定性），但是 EggJS 的这项功能依旧可用。\n\n\n文件位置如下。\n```\n➜  my_midway_app tree\n.\n├── src\n│   ├── app\n│   │   └── extend\n│   │       ├── application.ts\n│   │       ├── context.ts\n│   │       ├── request.ts\n│   │       └── response.ts\n│   ├── config\n│   └── interface.ts\n├── test\n├── package.json\n└── tsconfig.json\n```\n内容和原来的 EggJS 相同。\n```typescript\n// src/app/extend/context.ts\nexport default {\n  get hello() {\n    return 'hello world';\n  },\n};\n```\n### 增加扩展定义\n\nContext 请使用 Midway 的方式来扩展，请查看 [扩展上下文定义](/docs/context_definition)。\n\n\n其余的部分，沿用 egg 的方式，请在 `src/interface.ts` 中扩展。\n```typescript\n// src/interface.ts\ndeclare module 'egg' {\n  interface Request {\n    // ...\n  }\n  interface Response {\n    // ...\n  }\n  interface Application {\n    // ...\n  }\n}\n```\n:::info\n业务自定义扩展的定义请 **不要放在根目录** `typings` 下，避免被 ts-helper 工具覆盖掉。\n:::\n\n\n\n## 使用 egg-scripts 部署\n\n由于 EggJS 提供了默认的多进程部署工具 `egg-scripts` ，Midway 也继续支持这种方式，如果上层是 EggJS，推荐这种部署方式。\n\n首先在依赖中，确保安装 `egg-scripts` 包。\n\n```bash\n$ npm i egg-scripts --save\n```\n\n\n\n添加 `npm scripts` 到 `package.json`：\n\n在上面的代码构建之后，使用我们的 `start` 和 `stop` 命令即可完成启动和停止。\n\n```json\n\"scripts\": {\n    \"start\": \"egg-scripts start --daemon --title=********* --framework=@midwayjs/web\",\n    \"stop\": \"egg-scripts stop --title=*********\",\n}\n```\n\n\n\n:::info\n\n`*********` 的地方是你的项目名。\n:::\n\n> 注意：`egg-scripts` 对 Windows 系统的支持有限，参见 [#22](https://github.com/eggjs/egg-scripts/pull/22)。\n\n####\n\n**启动参数**\n\n```bash\n$ egg-scripts start --port=7001 --daemon --title=egg-server-showcase\n```\n\nCopy\n\n如上示例，支持以下参数：\n\n- `--port=7001` 端口号，默认会读取环境变量 process.env.PORT，如未传递将使用框架内置端口 7001。\n- `--daemon` 是否允许在后台模式，无需 nohup。若使用 Docker 建议直接前台运行。\n- `--env=prod` 框架运行环境，默认会读取环境变量 process.env.EGG_SERVER_ENV， 如未传递将使用框架内置环境 prod。\n- `--workers=2` 框架 worker 线程数，默认会创建和 CPU 核数相当的 app worker 数，可以充分的利用 CPU 资源。\n- `--title=egg-server-showcase` 用于方便 ps 进程时 grep 用，默认为 `egg-server-\\${appname}`。\n- `--framework=yadan` 如果应用使用了[自定义框架](https://eggjs.org/zh-cn/advanced/framework.html)，可以配置 package.json 的 egg.framework 或指定该参数。\n- `--ignore-stderr` 忽略启动期的报错。\n- `--https.key` 指定 HTTPS 所需密钥文件的完整路径。\n- `--https.cert` 指定 HTTPS 所需证书文件的完整路径。\n- 所有 [egg-cluster](https://github.com/eggjs/egg-cluster) 的 Options 都支持透传，如 --port 等。\n\n更多参数可查看 [egg-scripts](https://github.com/eggjs/egg-scripts) 和 [egg-cluster](https://github.com/eggjs/egg-cluster) 文档。\n\n:::info\n\n使用 egg-scripts 部署的日志会存放在 **用户目录** 下**，**比如 `/home/xxxx/logs` 。\n\n:::\n\n\n\n## 启动环境\n\n原有 egg 使用 `EGG_SERVER_ENV` 中作为环境标志，在 Midway 中请使用 `MIDWAY_SERVER_ENV`。\n\n\n\n## State 类型定义\n\n在 egg 底层的 koa 的 Context 中有一个特殊的 State 属性，通过和 Context 类似的方式可以扩展 State 定义。\n\n```typescript\n// src/interface.ts\n\ndeclare module '@midwayjs/web/dist/interface' {\n  interface Context {\n    abc: string;\n  }\n\n  interface State{\n    bbb: string;\n    ccc: number;\n  }\n}\n```\n\n\n\n## 配置\n\n### 默认配置\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  egg: {\n    port: 7001,\n  },\n}\n```\n\n`@midwayjs/web`  所有参数如下：\n\n| 配置项         | 类型             | 描述                         |\n| -------------- | ---------------- | ---------------------------- |\n| port           | number           | 必填，启动的端口             |\n| key            | string           | Buffer                       |\n| cert           | string           | Buffer                       |\n| ca             | string           | Buffer                       |\n| hostname       | string           | 监听的 hostname，默认 127.1  |\n| http2          | boolean          | 可选，http2 支持，默认 false |\n| queryParseMode | simple\\|extended | 默认为 extended              |\n| queryParseOptions | `qs.IParseOptions` | 解析选项，当使用'simple'模式解析时可用 |\n\n以上的属性，对本地和使用 `bootstrap.js` 部署的应用生效。\n\n\n\n### 修改端口\n\n:::tip\n\n注意，这个方式只会对本地研发，以及使用 bootstrap.js 文件部署的项目生效。\n\n:::\n\n默认情况下，我们在 `config.default` 提供了 `7001` 的默认端口参数，修改它就可以修改 egg http 服务的默认端口。\n\n比如我们修改为 `6001`：\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  egg: {\n    port: 6001,\n  },\n}\n```\n\n默认情况下，单测环境由于需要 supertest 来启动端口，我们的 port 配置为 `null`。\n\n```typescript\n// src/config/config.unittest\nexport default {\n  // ...\n  egg: {\n    port: null,\n  },\n}\n```\n\n此外，也可以通过 `midway-bin dev --ts --port=6001` 的方式来临时修改端口，此方法会覆盖配置中的端口。\n\n\n\n### 全局前缀\n\n此功能请参考 [全局前缀](../controller#全局路由前缀)。\n\n\n\n### Https 配置\n\n在大多数的情况，请尽可能使用外部代理的方式来完成 Https 的实现，比如 Nginx。\n\n在一些特殊场景下，你可以通过配置 SSL 证书（TLS 证书）的方式，来直接开启 Https。\n\n首先，你需要提前准备好证书文件，比如 `ssl.key` 和 `ssl.pem`，key 为服务端私钥，pem 为对应的证书。\n\n然后配置即可。\n\n```typescript\n// src/config/config.default\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  egg: {\n    key: join(__dirname, '../ssl/ssl.key'),\n    cert: join(__dirname, '../ssl/ssl.pem'),\n  },\n}\n```\n\n\n\n### favicon 设置\n\n默认情况下，浏览器会发起一个 `favicon.ico` 的请求。\n\n```typescript\n// src/config/config.default\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  siteFile: {\n    '/favicon.ico': readFileSync(join(__dirname, 'favicon.png')),\n  },\n}\n```\n\n\n\n如果开启了 `@midwayjs/static-file` 组件，那么会优先使用组件的静态文件托管。\n\n### 修改上下文日志\n\n可以单独修改 egg 框架的上下文日志。\n\n```typescript\nexport default {\n  egg: {\n    contextLoggerFormat: info => {\n      const ctx = info.ctx;\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} [${ctx.userId} - ${Date.now() - ctx.startTime}ms ${ctx.method}] ${info.message}`;\n    }\n    // ...\n  },\n};\n```\n\n\n\n### Query 数组解析\n\n默认情况下，`ctx.query` 会解析为忽略数组的情况，而 `ctx.queries` 会严格的将所有的字段都变成数组。\n\n如果调整 `queryParseMode` ，则可以使 `ctx.query` 变为两者之间的结构（querystring 的结果）。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  egg: {\n    // ...\n    queryParseMode: 'simple',\n    queryParseOptions: {\n      arrayLimit: 100,\n    },\n  },\n}\n```\n\n\n\n\n## 常见问题\n### 1、生成 ts 定义\n\n\nMidway 提供了 `@midwayjs/egg-ts-hepler` 工具包，用于快速生成 EggJS 开发时所依赖的定义。\n```bash\n$ npm install @midwayjs/egg-ts-helper --save-dev\n```\n在 `package.json` 中加入对应的 `ets` 命令即可，一般来说，我们会在 dev 命令前加入，以保证代码的正确性。\n```json\n  \"scripts\": {\n    \"dev\": \"cross-env ets && cross-env NODE_ENV=local midway-bin dev --ts\",\n  },\n```\n:::info\n在第一次编写代码前，需要执行一次此命令才能有 ts 定义生成。\n:::\n\n\nEggJS 生成的定义在 `typings` 目录中。\n```\n➜  my_midway_app tree\n.\n├── src                            ## midway 项目源码\n├── typings                        ## EggJS 定义生成目录\n├── test\n├── package.json\n└── tsconfig.json\n```\n\n\n### 2、EggJS 中 Configuration 的特殊情况\n\n\n在 EggJS 下， `configuration.ts`  中的生命周期**只会在 worker 下加载执行**。如果在 Agent 有类似的需求，请直接使用 EggJS 自身的 `agent.ts` 处理。\n\n\n\n### 3、异步初始化配置无法覆盖插件配置\n\n`onConfigLoad` 生命周期会在 egg 插件（若有）初始化之后执行，所以不能用于覆盖 egg 插件所使用的配置。\n\n\n\n### 4、默认的 csrf 错误\n\n\n在 post 请求，特别是第一次时用户会发现一个 csrf 报错。原因是 egg 在框架中默认内置了安全插件 [egg-security](https://github.com/eggjs/egg-security)， 默认开启了  csrf 校验。\n\n\n我们可以在配置中关闭它，但是更好的是去[**了解它**](https://eggjs.org/zh-cn/core/security.html#%E5%AE%89%E5%85%A8%E5%A8%81%E8%83%81-csrf-%E7%9A%84%E9%98%B2%E8%8C%83)之后再做选择。\n\n```typescript\nexport const security = {\n  csrf: false,\n};\n```\n\n\n\n\n### 5、不存在定义的问题\n\n一些 egg 插件未提供 ts 定义，导致使用会出现未声明方法的情况，比如 egg-mysql。\n![image.png](https://img.alicdn.com/imgextra/i1/O1CN01mv68zG1zN6nALff8n_!!6000000006701-2-tps-1478-876.png)\n可以使用 any 绕过。\n\n```typescript\nawait (this.app as any).mysql.query(sql);\n```\n\n或者可以自行增加扩展定义。\n\n### 6、获取 Http Server\n\nEggjs 内部封装了原始的 HttpServer，需要通过事件获取。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, App } from '@midwayjs/core';\nimport { Application } from '@midwayjs/web';\n\n@Configuration(/***/)\nexport class MainConfiguration {\n  \n  @App('egg')\n  app: Application;\n  \n  // ...\n  async onServerReady() {\n    this.app.once('server', (server) => {\n      // ...\n    })\n  }\n}\n```\n\n","site/docs/extensions/etcd.md":"# ETCD\n\netcd 是云原生架构中重要的基础组件，由 CNCF 孵化托管。etcd 在微服务和 Kubernetes 集群中可以作为服务注册于发现，也可以作为 key-value 存储的中间件。\n\nMidway 提供基于 [etcd3](https://github.com/microsoft/etcd3) 模块封装的组件，提供 etcd 的客户端调用能力。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/etcd@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/etcd\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n\n## 引入组件\n\n\n首先，引入 组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as etcd from '@midwayjs/etcd';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    etcd,\n  ],\n  // ...\n})\nexport class MainConfiguration {\n}\n```\n\n\n\n## 配置默认客户端\n\n大部分情况下，我们可以只使用默认客户端来完成功能。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  etcd: {\n    client: {\n      host: [\n        '127.0.0.1:2379'\n      ]\n    },\n  },\n}\n```\n\n\n\n## 使用默认客户端\n\n配置完成后，我们就可以在代码中使用了。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { ETCDService } from '@midwayjs/etcd';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  etcdService: ETCDService;\n\n  async invoke() {\n\n    await this.etcdService.put('foo').value('bar');\n\n    const fooValue = await this.etcdService.get('foo').string();\n    console.log('foo was:', fooValue);\n\n    const allFValues = await this.etcdService.getAll().prefix('f').keys();\n    console.log('all our keys starting with \"f\":', allFValues);\n\n    await this.etcdService.delete().all();\n  }\n}\n```\n\n更多 API 请参考 ts 定义或者 [官网文档](https://microsoft.github.io/etcd3/classes/etcd3.html)。\n\n\n\n## 多实例配置\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  etcd: {\n    clients: {\n      instance1: {\n        {\n          host: [\n            '127.0.0.1:2379'\n          ]\n        },\n      },\n  \t\tinstance2: {\n        {\n          host: [\n            '127.0.0.1:2379'\n          ]\n        },\n      }\n    }\n  },\n}\n```\n\n\n\n## 多实例获取\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { ETCDServiceFactory } from '@midwayjs/etcd';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  etcdServiceFactory: ETCDServiceFactory;\n\n  async invoke() {\n    const instance1 = this.etcdServiceFactory.get('instance1');\n    // ...\n\n    const instance2 = this.etcdServiceFactory.get('instance2');\n    // ...\n  }\n}\n```\n\n\n\n## 动态创建实例\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { ETCDServiceFactory } from '@midwayjs/etcd';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  etcdServiceFactory: ETCDServiceFactory;\n\n  async invoke() {\n    const instance3 = await this.etcdServiceFactory.createInstance({\n      host: [\n        '127.0.0.1:2379'\n      ]\n    }, 'instance3');\n    // ...\n  }\n}\n```\n\n\n\n## 服务发现\n\n从 v4 开始提供基于统一抽象的服务发现能力，虽然 ETCD 并不太适合作为服务发现的注册中心来使用，但是我们仍实现了 ETCD 作为服务发现的客户端能力。\n\n服务发现包括 **注册服务** 和 **获取服务** 两部分。\n\n\n### 配置服务发现\n\n你需要先配置一个客户端；若存在多个客户端，可通过 `serviceDiscoveryClient` 指定用于服务发现的客户端。\n\n通过 `serviceDiscovery` 配置服务发现选项：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  etcd: {\n    clients: {\n      default: { hosts: ['127.0.0.1:2379'] }\n    },\n    serviceDiscovery: {\n      namespace: 'services',\n      ttl: 30,\n      // 可选：loadBalancer: LoadBalancerType.ROUND_ROBIN\n    }\n  }\n}\n```\n\n说明：`namespace` 为键空间前缀，实例键结构为 `namespace/serviceName/instanceId`；`ttl` 为实例过期时间（秒）。\n\n\n### 注册服务发现\n\n服务启动后，将当前服务注册到 ETCD：\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport { EtcdServiceDiscovery } from '@midwayjs/etcd';\n\n@Configuration({})\nexport class MainConfiguration {\n  @Inject()\n  etcdDiscovery: EtcdServiceDiscovery;\n\n  async onServerReady() {\n    // 创建服务发现客户端（可传入覆盖项）\n    const client = this.etcdDiscovery.createClient();\n\n    // 注册当前服务并上线（写入键并绑定租约）\n    await client.register({\n      id: client.defaultMeta.id,\n      serviceName: 'order',\n      ttl: 30,\n      meta: {\n        version: '1.0.0',\n        host: client.defaultMeta.host,\n        port: '7001'\n      }\n    });\n  }\n}\n```\n\n说明：注册时会默认上线，无需显式调用 `online`。ETCD 通过 Lease 维持 TTL，定时续约保证存活；`offline` 删除键并撤销租约；`deregister` 注销并清理状态。重复调用 `register/online/offline/deregister` 具有幂等性。\n\n\n### 获取可用服务\n\n在任意位置获取可用实例或按负载均衡选择一个实例：\n\n````typescript\n@Provide()\nexport class OrderService {\n  @Inject()\n  etcdDiscovery: EtcdServiceDiscovery;\n\n  async getService() {\n    const instances = await this.etcdDiscovery.getInstances('order');\n    const one = await this.etcdDiscovery.getInstance('order');\n    return { instances, one };\n  }\n}\n````\n\n返回的实例为注册时写入的对象（`EtcdInstanceMetadata`），通常包含 `serviceName/id/ttl/meta/tags/status` 等字段。\n","site/docs/extensions/events.md":"# 事件\n\n事件组件是基于 [eventemitter2](https://github.com/EventEmitter2/EventEmitter2) 实现的，提供了强大的事件处理能力。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/event-emitter@4 --save\n```\n\n或者在 `package.json` 中增加依赖后，重新安装\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/event-emitter\": \"^4.0.0\"\n  }\n}\n```\n\n## 使用组件\n\n首先在 `configuration.ts` 中引入组件：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as eventEmitter from '@midwayjs/event-emitter';\n\n@Configuration({\n  imports: [\n    // ...\n    eventEmitter\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n## 基础使用\n\n### 定义事件监听器\n\n:::caution\n注意：`@OnEvent()` 装饰器只能用在单例（Singleton）类上。如果在非单例类上使用，事件监听器将无法正常工作。\n:::\n\n使用 `@OnEvent()` 装饰器来定义事件监听器：\n\n```typescript\nimport { Provide, Singleton } from '@midwayjs/core';\nimport { OnEvent } from '@midwayjs/event-emitter';\n\n@Provide()\n@Singleton()    // 必须使用 @Singleton 装饰器\nexport class UserService {\n  @OnEvent('user.created')\n  async handleUserCreated(user: any) {\n    console.log('新用户创建:', user);\n  }\n}\n```\n\n### 发送事件\n\n通过注入 `EventEmitterService` 来发送事件：\n\n```typescript\nimport { Inject } from '@midwayjs/core';\nimport { EventEmitterService } from '@midwayjs/event-emitter';\n\n@Provide()\nexport class UserController {\n  @Inject()\n  eventEmitterService: EventEmitterService;\n\n  async createUser() {\n    const user = { id: 1, name: 'harry' };\n    // ... 创建用户逻辑\n    \n    // 发送同步事件\n    this.eventEmitterService.emit('user.created', user);\n    \n    // 或者发送异步事件\n    await this.eventEmitterService.emitAsync('user.created', user);\n  }\n}\n```\n\n## 高级特性\n\n### 事件监听器选项\n\n`@OnEvent()` 装饰器支持多个配置选项，每个选项都有其特定的用途：\n\n#### prependListener\n\n用于控制监听器的执行顺序。设置为 `true` 时，会将监听器添加到监听器队列的开头，这样可以确保该监听器最先执行。\n\n```typescript\n@Provide()\n@Singleton()\nexport class UserService {\n  // 这个监听器会最先执行\n  @OnEvent('user.created', { prependListener: true })\n  async handleUserCreatedFirst(user: any) {\n    console.log('第一个处理用户创建事件');\n  }\n\n  // 这个监听器会在队列末尾执行\n  @OnEvent('user.created')\n  async handleUserCreated(user: any) {\n    console.log('后续处理用户创建事件');\n  }\n}\n```\n\n#### suppressErrors\n\n控制事件处理器中的错误处理行为：\n- 默认值为 `true`，表示错误会被捕获并忽略\n- 设置为 `false` 时，错误会被抛出，可以在外层捕获\n\n```typescript\n@Provide()\n@Singleton()\nexport class UserService {\n  // 错误会被抛出\n  @OnEvent('user.created', { suppressErrors: false })\n  async handleWithError(user: any) {\n    throw new Error('处理失败');  // 这个错误会被抛出\n  }\n\n  // 错误会被忽略\n  @OnEvent('user.created', { suppressErrors: true })\n  async handleSuppressError(user: any) {\n    throw new Error('处理失败');  // 这个错误会被忽略\n  }\n}\n\n// 在调用处可以捕获错误\n@Provide()\nexport class UserController {\n  @Inject()\n  eventEmitterService: EventEmitterService;\n\n  async createUser() {\n    try {\n      await this.eventEmitterService.emitAsync('user.created', { id: 1 });\n    } catch (err) {\n      // 可以捕获到 suppressErrors: false 的处理器抛出的错误\n      console.error('事件处理失败:', err);\n    }\n  }\n}\n```\n\n#### async\n\n控制事件处理器的执行方式：\n- 设置为 `true` 时，处理器会在单独的事件循环中异步执行\n- 默认为 `false`，会按顺序同步执行\n\n```typescript\n@Provide()\n@Singleton()\nexport class UserService {\n  // 异步执行，不会阻塞其他处理器\n  @OnEvent('user.created', { async: true })\n  async handleAsync(user: any) {\n    await someTimeConsumingTask();\n    console.log('异步处理完成');\n  }\n\n  // 同步执行，会按顺序等待\n  @OnEvent('user.created')\n  async handleSync(user: any) {\n    console.log('同步处理');\n  }\n}\n```\n\n### 通配符事件\n\n通配符事件允许你使用模式匹配来监听一组相关的事件。需要在配置中启用 `wildcard: true`。\n\n#### 配置启用\n\n```typescript\n// config/config.default.ts\nexport default {\n  eventEmitter: {\n    wildcard: true,  // 启用通配符功能\n  }\n}\n```\n\n#### 使用示例\n\n```typescript\n@Provide()\n@Singleton()\nexport class UserService {\n  // 监听所有用户相关事件\n  @OnEvent('user.*')\n  async handleUserEvents(...args: any[]) {\n    // 注意：通配符模式下，事件处理器直接接收发送事件时传入的参数\n    console.log('事件参数:', args);\n  }\n\n  // 监听多层级的事件\n  @OnEvent('user.*.detail')\n  async handleUserDetailEvents(...args: any[]) {\n    // 可以匹配 user.profile.detail, user.settings.detail 等\n    console.log('事件参数:', args);\n  }\n}\n\n// 发送事件示例\n@Provide()\nexport class UserController {\n  @Inject()\n  eventEmitterService: EventEmitterService;\n\n  async updateUser() {\n    // 这些事件都会被上面的 user.* 处理器捕获\n    await this.eventEmitterService.emitAsync('user.created', { id: 1 });\n    await this.eventEmitterService.emitAsync('user.updated', { id: 1, name: 'new name' });\n    await this.eventEmitterService.emitAsync('user.profile.detail', { id: 1, profile: {} });\n  }\n}\n```\n\n:::info\n注意：在使用通配符事件时，事件处理器的参数直接是事件发送时传入的参数。无法在处理器中获取具体触发的事件名称，所以建议在发送事件时将必要的信息包含在事件参数中。\n:::\n\n### 命名空间事件\n\n命名空间事件提供了一种组织和分组事件的方式。通过 `delimiter` 配置分隔符。\n\n#### 配置启用\n\n```typescript\n// config/config.default.ts\nexport default {\n  eventEmitter: {\n    delimiter: '.' // 设置命名空间分隔符\n  }\n}\n```\n\n#### 使用示例\n\n```typescript\n@Provide()\n@Singleton()\nexport class OrderService {\n  // 监听订单命名空间下的创建事件\n  @OnEvent('order.created')\n  async handleOrderCreated(order: any) {\n    console.log('订单创建:', order);\n  }\n\n  // 如果需要使用通配符匹配，需要额外启用 wildcard: true\n  @OnEvent('order.payment.*')\n  async handleOrderPaymentEvents(...args: any[]) {\n    // 可以匹配 order.payment.success, order.payment.failed 等\n    console.log('事件参数:', args);\n  }\n}\n\n// 发送命名空间事件\n@Provide()\nexport class PaymentService {\n  @Inject()\n  eventEmitterService: EventEmitterService;\n\n  async processPayment() {\n    // 发送支付成功事件\n    await this.eventEmitterService.emitAsync('order.payment.success', {\n      orderId: '123',\n      amount: 100\n    });\n  }\n}\n```\n\n:::info\n注意：命名空间的分隔符默认是 `.`，你可以通过 `delimiter` 配置修改。如果需要使用通配符（如 `*`）进行事件匹配，则需要额外启用 `wildcard: true` 配置。\n:::\n\n## 配置\n\n在 `config.default.ts` 中可以配置 Event Emitter 的行为：\n\n```typescript\nexport default {\n  // ...\n  eventEmitter: {\n    // 是否开启通配符事件\n    wildcard: true,\n    // 命名空间分隔符\n    delimiter: '.',\n    // 是否启用新监听器事件\n    newListener: false,\n    // 是否启用移除监听器事件\n    removeListener: false,\n    // 设置最大监听器数量\n    maxListeners: 10,\n    // 是否启用详细的内存泄漏检测\n    verboseMemoryLeak: false,\n  }\n}\n```\n\n## 常见问题\n\n### 1. 事件处理顺序\n\n- 默认情况下，事件处理器按照注册顺序执行\n- 使用 `prependListener: true` 可以将处理器添加到队列开头\n- 异步事件处理器（`emitAsync`）会按顺序等待每个处理器完成\n\n### 2. 错误处理\n\n- 默认情况下，事件处理器中的错误会被抑制（`suppressErrors: true`）\n- 设置 `suppressErrors: false` 可以让错误抛出\n- 使用 `emitAsync` 时，可以通过 try/catch 捕获错误\n\n### 3. 性能考虑\n\n- 合理使用通配符事件，过多的通配符匹配可能影响性能\n- 设置适当的 `maxListeners` 值，避免内存泄漏\n- 及时移除不需要的事件监听器","site/docs/extensions/express.md":"# Express\n\n本章节内容，主要介绍在 Midway 中如何使用 Express 作为上层框架，并使用自身的能力。\n\n| 描述           |      |\n| -------------- | ---- |\n| 包含独立主框架 | ✅    |\n| 包含独立日志   | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/express@4 --save\n$ npm i @types/body-parser @types/express @types/express-session --save-dev\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/express\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    \"@types/body-parser\": \"^1.19.2\",\n    \"@types/express\": \"^4.17.13\",\n    \"@types/express-session\": \"^1.17.4\",\n    // ...\n  }\n}\n```\n\n也可以直接使用脚手架创建示例。\n\n```bash\n# npm v6\n$ npm init midway --type=express-v3 my_project\n\n# npm v7\n$ npm init midway -- --type=express-v3 my_project\n```\n\n\n针对 Express，Midway 提供了 `@midwayjs/express` 包进行了适配，在其中提供了 Midway 特有的依赖注入、切面等能力。\n\n:::info\n我们使用的 Express 版本为 `v4` 。\n:::\n\n\n## 目录结构\n```\n.\n├── src\n│   ├── controller\t\t\t\t\t\t\t\t \t\t\t\t# controller接口的地方\n│   ├── service\t\t\t\t\t\t\t\t\t \t\t\t\t\t# service逻辑处理的地方\n|   └── configuration.ts\t\t\t\t\t\t\t\t\t# 入口及生命周期配置、组件管理\n├── test\n├── package.json\n└── tsconfig.json\n```\n\n\n\n## 开启组件\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [express],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: express.Application;\n\n  async onReady() {}\n}\n```\n\n\n\n\n## 控制器（Controller）\n\n\n整个请求控制器的写法和 Midway 适配其他框架的类似。为了和其他场景的框架写法一致，在请求的时候，Midway 将 Express 的 `req` 映射为 `ctx` 对象。\n```typescript\nimport { Inject, Controller, Get, Provide, Query } from '@midwayjs/core';\nimport { Context, NextFunction } from '@midwayjs/express';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  ctx: Context;\n\n  @Get('/')\n  async home(@Query() id) {\n    console.log(id);\t\t\t\t\t\t// req.query.id === id\n    return 'hello world';\t\t\t\t// 简单返回，等价于 res.send('hello world');\n  }\n}\n```\n你也可以额外注入 `req` 和 `res` 。\n```typescript\nimport { Inject, Controller, Get, Provide, Query } from '@midwayjs/core';\nimport { Context, Response, NextFunction } from '@midwayjs/express';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  ctx: Context;   // 即为 req\n\n  @Inject()\n  req: Context;\n\n  @Inject()\n  res: Response;\n\n  @Get('/')\n  async home(@Query() id) {\n    // this.req.query.id === id\n  }\n}\n```\n\n\n\n## Web 中间件\n\n\nExpress 的中间件写法比较特殊，它的参数不同。\n\n\n```typescript\nimport { Middleware } from '@midwayjs/core';\nimport { Context, Response, NextFunction } from '@midwayjs/express';\n\n@Middleware()\nexport class ReportMiddleware implements IMiddleware<Context, Response, NextFunction> {\n\n  resolve() {\n    return async (\n      req: Context,\n      res: Response,\n      next: NextFunction\n    ) => {\n      console.log('Request...');\n      next();\n    };\n  }\n\n}\n```\n\n注意，这里我们导出了一个 `ReportMiddleware` 类，为了方便对接异步流程，`resolve` 返回可以是 async 函数。\n\nExpress 中的 next 方法，用于调用到下一个中间件，指的注意的是，Express 中间件并非洋葱模型，是单向调用。\n\n\n\n\n### 路由中间件\n\n\n我们可以把上面编写的中间件应用到单个 Controller 上，也可以将中间件应用到单个路由上。\n\n\n```typescript\nimport { Controller, Get, Provide } from '@midwayjs/core';\n\n@Controller('/', { middleware: [ ReportMiddleware ]})\t\t\t// controller 级别的中间件\nexport class HomeController {\n\n  @Get('/', { middleware: [ ReportMiddleware ]})\t\t\t\t// 路由级别的中间件\n  async home() {\n    return 'hello world'\n  }\n}\n```\n\n\n### 全局中间件\n\n\n直接使用 Midway 提供的 `app.generateMiddleware` 方法，在入口处加载全局中间件。\n```typescript\n// src/configuration.ts\nimport { Configuration, ILifeCycle } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport { ReportMiddleware } from './middleware/report.middleware.ts'\n\n@Configuration({\n  imports: [express],\n})\nexport class MainConfiguration implements ILifeCycle {\n\n  @App()\n  app: express.Application;\n\n  async onReady() {\n    this.app.useMiddleware(ReportMiddleware);\n  }\n}\n```\n\n\n除了加载 Class 形式的中间件外，也支持加载传统的 Express 中间件。\n```typescript\n// src/configuration.ts\nimport { Configuration, ILifeCycle, App } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [express],\n})\nexport class MainConfiguration implements ILifeCycle {\n\n  @App()\n  app: express.Application;\n\n  async onReady() {\n    this.app.useMiddleware((req, res, next) => {\n    \t// xxx\n    });\n  }\n}\n```\n你可以通过注入 `app` 对象，来调用到所有 Express 上的方法。\n\n\n\n## 返回统一处理\n\n由于 Express 中间件是单向调用，无法在返回时执行，为此我们额外设计了一个 `@Match` 装饰的过滤器，用于处理返回值的行为。\n\n比如，我们可以定义针对全局返回的过滤器。\n\n```typescript\n// src/filter/globalMatch.filter.ts\nimport { Match } from '@midwayjs/core';\nimport { Context, Response } from '@midwayjs/express';\n\n@Match()\nexport class GlobalMatchFilter {\n  match(value, req, res) {\n    // ...\n    return {\n      status: 200,\n      data: {\n        value\n      },\n    };\n  }\n}\n```\n\n也可以匹配特定的路由做返回。\n\n```typescript\n// src/filter/api.filter.ts\nimport { Match } from '@midwayjs/core';\nimport { Context, Response } from '@midwayjs/express';\n\n@Match((ctx: Context, res: Response) => {\n  return ctx.path === '/api';\n})\nexport class APIMatchFilter {\n  match(value, req: Context, res: Response) {\n    // ...\n    return {\n      data: {\n        message:\n        data: value,\n      },\n    };\n  }\n}\n```\n\n需要应用到 app 中。\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport { join } from 'path';\nimport { APIMatchFilter } from './filter/api.filter';\nimport { GlobalMatchFilter } from 'filter/globalMatch.filter';\n\n@Configuration({\n  imports: [express],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: express.Application;\n\n  async onReady() {\n    // ...\n    this.app.useFilter([APIMatchFilter, GlobalMatchFilter]);\n  }\n}\n```\n\n注意，这类过滤器是按照添加的顺序来匹配执行。\n\n\n\n## 错误处理\n\n和普通的项目相同，使用错误过滤器，但是参数略有不同。\n\n```typescript\nimport { Catch } from '@midwayjs/core';\nimport { Context, Response } from '@midwayjs/express';\n\n@Catch()\nexport class GlobalError {\n  catch(err: Error, req: Context, res: Response) {\n    if (err) {\n      return {\n        status: err.status ?? 500,\n        message: err.message,\n      }\n    }\n  }\n}\n```\n\n需要应用到 app 中。\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport { join } from 'path';\nimport { GlobalError } from './filter/global.filter';\n\n@Configuration({\n  imports: [express],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: express.Application;\n\n  async onReady() {\n    this.app.useMiddleware((req, res, next) => {\n      next();\n    });\n\n    this.app.useFilter([GlobalError]);\n  }\n}\n```\n\n注意，`@Match` 和 `@Catch` 都是过滤器，在内部会自动判断做区分执行。。\n\n\n\n## Cookie\n\n`@midwayjs/express` 自带  `cookie parser` 功能，使用的是 `cookie-parser` 模块。\n\n针对 Cookie，统一使用 `keys` 作为秘钥。\n\n```typescript\n// src/config/config.default\nexport default {\n  keys:  ['key1', 'key2'],\n}\n```\n\n获取 Cookie。\n\n```typescript\nconst cookieValue = req.cookies['cookie-key'];\n```\n\n设置 Cookie。\n\n```typescript\nres.cookie(\n  'cookie-key',\n  'cookie-value',\n  cookieOptions\n);\n```\n\n\n\n##  Session\n\n`@midwayjs/express` 内置了 Session 组件，给我们提供了 `ctx.session` 来访问或者修改当前用户 Session 。\n\n默认情况下为 `cookie-session` ，默认配置如下。\n\n```typescript\n// src/config/config.default\nexport default {\n  session:  {\n    name: 'MW_SESS',\n    resave: true,\n    saveUninitialized: true,\n    cookie: {\n      maxAge: 24 * 3600 * 1000, // ms\n      httpOnly: true,\n      // sameSite: null,\n    },\n  }\n}\n```\n\n我们可以通过简单的 API 来设置 session。\n\n```typescript\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  req;\n\n  @Get('/')\n  async get() {\n    // set all\n    this.req.session = req.query;\n\n    // set value\n    this.req.session.key = 'abc';\n\n    // get\n    const key = this.req.session.key;\n\n    // remove\n    this.req.session = null;\n\n    // set max age\n    this.req.session.maxAge = Number(req.query.maxAge);\n\n    // ...\n  }\n}\n\n```\n\n\n\n## BodyParser\n\n`@midwayjs/express` 自带  `bodyParser` 功能，默认会解析 `Post` 请求，自动识别 `json` 、`text`和 `urlencoded` 类型。\n\n默认的大小限制为 `1mb`，可以单独对每项配置大小。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  bodyParser: {\n    json: {\n      enable: true,\n      limit: '1mb',\n      strict: true,\n    },\n    raw: {\n      enable: false,\n      limit: '1mb',\n    },\n    text: {\n      enable: true,\n      limit: '1mb',\n    },\n    urlencoded: {\n      enable: true,\n      extended: false,\n      limit: '1mb',\n      parameterLimit: 1000,\n    },\n  },\n}\n```\n\n\n\n## 配置\n\n### 默认配置\n\n`@midwayjs/express`  的配置样例如下：\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  express: {\n    port: 7001,\n  },\n}\n```\n\n所有属性描述如下：\n\n| 属性         | 类型                                         | 描述                                                    |\n| ------------ |--------------------------------------------| ------------------------------------------------------- |\n| port         | number                                     | 可选，启动的端口                                        |\n| globalPrefix | string                                     | 可选，全局的 http 前缀                                  |\n| keys         | string[]                                   | 可选，Cookies 签名，如果上层未写 keys，也可以在这里设置 |\n| hostname     | string                                     | 可选，监听的 hostname，默认 127.1                       |\n| key          | string \\| Buffer \\| Array\\<Buffer\\|Object> | 可选，Https key，服务端私钥                             |\n| cert         | string \\| Buffer \\| Array\\<Buffer\\|Object> | 可选，Https cert，服务端证书                            |\n| ca           | string \\| Buffer \\| Array\\<Buffer\\|Object> | 可选，Https ca                                          |\n| http2        | boolean                                    | 可选，http2 支持，默认 false                            |\n\n\n\n### 修改端口\n\n默认情况下，我们在 `config.default` 提供了 `7001` 的默认端口参数，修改它就可以修改 Express http 服务的默认端口。\n\n比如我们修改为 `6001`：\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  express: {\n    port: 6001,\n  },\n}\n```\n\n默认情况下，单测环境由于需要 supertest 来启动端口，我们的 port 配置为 `null`。\n\n```typescript\n// src/config/config.unittest\nexport default {\n  // ...\n  express: {\n    port: null,\n  },\n}\n```\n\n此外，也可以通过 `midway-bin dev --ts --port=6001` 的方式来临时修改端口，此方法会覆盖配置中的端口。\n\n\n\n### 全局前缀\n\n此功能请参考 [全局前缀](../controller#全局路由前缀)。\n\n\n\n### Https 配置\n\n在大多数的情况，请尽可能使用外部代理的方式来完成 Https 的实现，比如 Nginx。\n\n在一些特殊场景下，你可以通过配置 SSL 证书（TLS 证书）的方式，来直接开启 Https。\n\n首先，你需要提前准备好证书文件，比如 `ssl.key` 和 `ssl.pem`，key 为服务端私钥，pem 为对应的证书。\n\n然后配置即可。\n\n```typescript\n// src/config/config.default\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  express: {\n    key: join(__dirname, '../ssl/ssl.key'),\n    cert: join(__dirname, '../ssl/ssl.pem'),\n  },\n}\n```\n\n\n\n### 修改上下文日志\n\n可以单独修改 express 框架的上下文日志。\n\n```typescript\nexport default {\n  express: {\n    contextLoggerFormat: info => {\n      // 等价 req\n      const req = info.ctx;\n      const userId = req?.['session']?.['userId'] || '-';\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} [${userId} - ${Date.now() - req.startTime}ms ${req.method}] ${info.message}`;\n    }\n    // ...\n  },\n};\n```\n\n","site/docs/extensions/grpc.md":"# gRPC\n\ngRPC 是一个高性能、通用的开源 RPC 框架，其由 Google 主要面向移动应用开发并基于 HTTP/2 协议标准而设计，基于 ProtoBuf(Protocol Buffers) 序列化协议开发，且支持众多开发语言。\n\n\n本篇内容演示了如何在 Midway 体系下，提供 gRPC 服务，以及调用 gRPC 服务的方法。\n\n\nMidway 当前采用了最新的 gRPC 官方推荐的 [@grpc/grpc-js](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js) 进行开发，并提供了一些工具包，用于快速发布服务和调用服务。\n\n我们使用的模块为 `@midwayjs/grpc` ，既可以独立发布服务，又可以接入其它框架调用 gRPC 服务。\n\n相关信息：\n\n**提供服务**\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n\n**调用服务**\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n\n**其他**\n\n| 描述                 |      |\n| -------------------- | ---- |\n| 可作为主框架独立使用 | ✅    |\n| 可独立添加中间件     | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/grpc@4 --save\n$ npm i @midwayjs/grpc-helper --save-dev\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/grpc\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    \"@midwayjs/grpc-helper\": \"^1.0.0\",\n    // ...\n  }\n}\n```\n\n\n\n## 开启组件\n\n:::tip\n\n不管是提供服务还是调用服务，都需要开启组件。\n\n:::\n\n`@midwayjs/grpc` 可以作为独立主框架使用。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as grpc from '@midwayjs/grpc';\n\n@Configuration({\n  imports: [grpc],\n  // ...\n})\nexport class MainConfiguration {\n  async onReady() {\n\t\t// ...\n  }\n}\n\n```\n\n也可以附加在其他的主框架下，比如 `@midwayjs/koa` 。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as grpc from '@midwayjs/grpc';\n\n@Configuration({\n  imports: [koa, grpc],\n  // ...\n})\nexport class MainConfiguration {\n  async onReady() {\n\t\t// ...\n  }\n}\n\n```\n\n\n\n## 目录结构\n\n大致的目录结构如下，`src/provider` 是提供 gRPC 服务的目录。\n\n```\n.\n├── package.json\n├── proto                         ## proto 定义文件\n│   └── helloworld.proto\n├── src\n│   ├── configuration.ts          ## 入口配置文件\n│   ├── interface.ts\n│   └── provider                  ## gRPC 提供服务的文件\n│       └── greeter.ts\n├── test\n├── bootstrap.js                  ## 服务启动入口\n└── tsconfig.json\n```\n\n\n\n## 定义服务接口\n\n\n在微服务中，定义一个服务需要特定的接口定义语言（IDL）来完成，在 gRPC中 默认使用 Protocol Buffers  作为序列化协议。\n\n\n序列化协议独立于语言和平台，提供了多种语言的实现，Java，C++，Go 等等,每一种实现都包含了相应语言的编译器和库文件。所以 gRPC 是一个提供和调用都可以跨语言的服务框架。\n\n一个gRPC服务的大体架构可以用官网上的一幅图表示。\n\n![](https://img.alicdn.com/imgextra/i3/O1CN01kpIyg51k8i5DtcGpZ_!!6000000004639-2-tps-621-445.png)\n\n\nProtocol Buffers  协议的文件，默认的后缀为 `.proto` 。.proto后缀的IDL文件,并通过其编译器生成特定语言的数据结构、服务端接口和客户端Stub代码。\n\n\n:::info\n由于 proto 文件可以跨语言使用，为了方便共享，我们一般将 proto 文件放在 src 目录外侧，方便其他工具复制分发。\n:::\n\n\n下面是一个基础的 `proto/helloworld.proto`  文件。\n```protobuf\nsyntax = \"proto3\";\n\npackage helloworld;\n\nservice Greeter {\n  // Sends a greeting\n  rpc SayHello (HelloRequest) returns (HelloReply) {}\n}\n\nmessage HelloRequest {\n  string name = 1;\n}\n\nmessage HelloReply {\n  string message = 1;\n}\n\n```\n\n\nproto3 表示的是第三版的 protobuf 协议，是 gRPC 目前推荐的版本，“语法简单，功能更全”。\n\n\n我们可以用 `service` 格式，定义服务体，其中可以包含方法。同时，我们可以更加细致的通过 `message` 描述服务具体的请求参数和响应参数。\n\n\n我们可以从 [Google 的官网文档](https://developers.google.com/protocol-buffers/docs/overview#simple) 中查看更多细节。\n\n\n:::info\n大家会看到，这和 Java 中的 Class 非常相像，每个结构就相当于 Java 中的一个类。\n:::\n\n\n### 编写 proto 文件\n\n\n现在我们再来看之前的服务，是不是就很好理解了。\n```protobuf\nsyntax = \"proto3\";\n\npackage helloworld;\n\n// 服务的定义\nservice Greeter {\n  // Sends a greeting\n  rpc SayHello (HelloRequest) returns (HelloReply) {}\n}\n\n// 服务的请求参数\nmessage HelloRequest {\n  string name = 1;\n}\n\n// 服务的响应参数\nmessage HelloReply {\n  string message = 1;\n}\n\n```\n\n\n我们定义了一个名为 `Greeter` 的服务，包含一个 `HelloRequest` 结构的请求体，以及返回 `HelloReply` 结构的响应体。\n\n\n接下去，我们将对这个服务给大家做演示。\n\n\n### 生成代码定义\n\n\n传统的 gRPC 框架，需要用户手动编写 proto 文件，以及生成 js 服务，最后再根据 js 生成的服务再编写实现，在 Midway 体系下，我们提供了一个 grpc-helper 工具包来加速这个过程。\n\n\n如果没有安装，可以先安装。\n```bash\n$ npm i @midwayjs/grpc-helper --save-dev\n```\n\n\ngrpc-helper 工具的作用，是将用户提供的 proto 文件，生成对应可读的 ts interface 文件。\n\n\n我们可以添加一个脚本，方便这个过程。\n```json\n{\n\t\"scripts\": {\n     \"generate\": \"tsproto --path proto --output src/domain\"\n  }\n}\n```\n\n\n然后执行 `npm run generate` 。\n\n\n上述命令执行后，会在代码的 `src/domain` 目录中生成 proto 文件对应的服务接口定义。\n\n\n:::info\n不管是提供 gRPC 服务还是调用 gRPC 服务，都要先生成定义。\n:::\n\n\n生成的代码如下，包含有一个命名空间（namespace），以及命名空间下的两个 TypeScript Interface， `Greeter` 用于编写服务端实现， `GreeterClient` 用于编写客户端实现。\n```typescript\n/**\n* This file is auto-generated by grpc-helper\n*/\n\nimport * as grpc from '@midwayjs/grpc';\n\n// 生成的命名空间\nexport namespace helloworld {\n\n  // 服务端使用的定义\n  export interface Greeter {\n    // Sends a greeting\n    sayHello(data: HelloRequest): Promise<HelloReply>;\n  }\n\n  // 客户端使用的定义\n  export interface GreeterClient {\n    // Sends a greeting\n    sayHello(options?: grpc.IClientOptions): grpc.IClientUnaryService<HelloRequest, HelloReply>;\n  }\n\n  // 请求体结构\n  export interface HelloRequest {\n    name?: string;\n  }\n\n  // 响应体结构\n  export interface HelloReply {\n    message?: string;\n  }\n}\n\n```\n\n\n:::info\n每当 proto 文件被修改时，就需要重新生成对应的服务定义，然后将对应的方法实现。\n:::\n\n\n\n## 提供 gRPC 服务（Provider）\n\n\n### 编写服务提供方（Provider）\n\n\n在 `src/provider` 目录中，我们创建 `greeter.ts` ，内容如下\n```typescript\nimport {\n  MSProviderType,\n  Provider,\n  GrpcMethod,\n} from '@midwayjs/core';\nimport { helloworld } from '../domain/helloworld';\n\n/**\n * 实现 helloworld.Greeter 接口的服务\n */\n@Provider(MSProviderType.GRPC, { package: 'helloworld' })\nexport class Greeter implements helloworld.Greeter {\n\n  @GrpcMethod()\n  async sayHello(request: helloworld.HelloRequest) {\n    return { message: 'Hello ' + request.name };\n  }\n}\n\n```\n:::info\n注意，@Provider 装饰器和 @Provide 装饰器不同，前者用于提供服务，后者用于依赖注入容器扫描标识的类。\n:::\n\n\n我们使用 `@Provider` 暴露出一个 RPC 服务， `@Provider` 的第一个参数为 RPC 服务类型，这个参数是个枚举，这里选择 GRPC 类型。\n\n\n`@Provider` 的第二个参数为 RPC 服务的元数据，这里指代的是 gRPC 服务的元数据。这里需要写入 gRPC 的 package 字段，即 proto 文件中的 package 字段（这里的字段用于和 proto 文件加载后的字段做对应）。\n\n\n对于普通的 gRPC 服务接口（UnaryCall），我们只需要使用 `@GrpcMethod()` 装饰器修饰即可。修饰的方法即为服务定义本身，入参为 proto 中定义好的入参，return 值即为定义好的响应体。\n\n:::info\n注意，生成的 Interface 是为了更好的编写服务代码，规范结构，请务必按照定义编写。\n:::\n\n\n### 配置服务\n\n\n配置内容如下。\n```typescript\n// src/config/config.default\nimport { MidwayAppInfo, MidwayConfig } from '@midwayjs/core';\n\nexport default (appInfo: MidwayAppInfo): MidwayConfig => {\n  return {\n    // ...\n    grpcServer: {\n      services: [\n        {\n          protoPath: join(appInfo.appDir, 'proto/hero.proto'),\n          package: 'hero',\n        },\n        {\n          protoPath: join(appInfo.appDir, 'proto/helloworld.proto'),\n          package: 'helloworld',\n        }\n      ],\n    }\n  };\n}\n```\nservices 字段是数组，意味着 Midway 项目可以同时发布多个 gRPC 服务。每个 service 的结构为：\n\n| 属性      | 类型   | 描述                       |\n| --- | --- | --- |\n| protoPath | string | 必选，proto 文件的绝对路径 |\n| package | string | 必选，服务对应的 package |\n\n除了 Service 配置之外，还有一些其他的配置。\n\n| 属性          | 类型              | 描述                                                         |\n| ------------- | ----------------- | ------------------------------------------------------------ |\n| url           | string            | 可选，gRPC 服务地址，默认 6565 端口，比如 'localhost:6565'   |\n| loaderOptions | Object            | 可选，proto file loader 的 options                           |\n| credentials   | ServerCredentials | 可选，grpc Server binding 时的 credentials 参数选项          |\n| serverOptions | ChannelOptions    | 可选，grpc Server 的 [自定义 options](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js#supported-channel-options) |\n\n\n\n### 提供安全证书\n\n可以通过 `credentials` 参数传递安全证书。\n\n```typescript\n// src/config/config.default\nimport { MidwayAppInfo, MidwayConfig } from '@midwayjs/core';\nimport { ServerCredentials } from '@midwayjs/grpc';\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nconst cert = readFileSync(join(__dirname, './cert/server.crt'));\nconst pem = readFileSync(join(__dirname, './cert/server.pem'));\nconst key = readFileSync(join(__dirname, './cert/server.key'));\n\nexport default (appInfo: MidwayAppInfo): MidwayConfig => {\n  return {\n    // ...\n    grpcServer: {\n      // ...\n      credentials: ServerCredentials.createSsl(cert, [{ private_key: key, cert_chain: pem }]);\n    }\n  };\n}\n```\n\n\n\n### 编写单元测试\n\n`@midwayjs/grpc` 库提供了一个 `createGRPCConsumer` 方法，用于实时调用客户端，一般我们用这个方法做测试。\n\n:::caution\n这个方法每次调用会实时连接，不建议将该方法用在生产环境。\n:::\n\n\n在测试中写法如下。\n```typescript\nimport { createApp, close } from '@midwayjs/mock';\nimport { Framework, createGRPCConsumer } from '@midwayjs/grpc';\nimport { join } from 'path';\nimport { helloworld } from '../src/domain/helloworld';\n\ndescribe('test/index.test.ts', () => {\n\n  it('should create multiple grpc service in one server', async () => {\n    const baseDir = join(__dirname, '../');\n\n    // 创建服务\n    const app = await createApp<Framework>();\n\n    // 调用服务\n    const service = await createGRPCConsumer<helloworld.GreeterClient>({\n      package: 'helloworld',\n      protoPath: join(baseDir, 'proto', 'helloworld.proto'),\n      url: 'localhost:6565'\n    });\n\n    const result = await service.sayHello().sendMessage({\n      name: 'harry'\n    });\n\n    expect(result.message).toEqual('Hello harry');\n    await close(app);\n  });\n\n});\n\n```\n\n\n\n## 调用 gRPC 服务（Consumer）\n\n\n我们编写一个 gRPC 服务来调用上面的暴露的服务。\n\n:::info\n事实上，你可以在 Web 的 Controller，或者 Service 等其他地方来调用，这里只是做一个示例。\n:::\n\n\n### 调用配置\n\n\n你需要在 `src/config/config.default.ts` 中增加你需要调用的目标服务以及它的 proto 文件信息。\n\n\n比如，这里我们填写了上面暴露的服务本身，以及该服务的 proto，包名等信息（函数形式）。\n```typescript\n// src/config/config.default\nimport { MidwayAppInfo, MidwayConfig } from '@midwayjs/core';\n\nexport default (appInfo: MidwayAppInfo): MidwayConfig => {\n  return {\n    // ...\n    grpc: {\n      services: [\n        {\n          url: 'localhost:6565',\n          protoPath: join(appInfo.appDir, 'proto/helloworld.proto'),\n          package: 'helloworld',\n        },\n      ],\n    },\n  };\n}\n```\n\n\n### 代码调用\n\n\n配置完后，我们就可以在代码里调用了。\n\n\n`@midwayjs/grpc` 提供了 `clients` ，可以方便的获取到已配置的服务。我们只需要在需要注入的地方，注入这个对象即可。\n\n\n比如：\n```typescript\nimport {\n  Provide,\n  Inject,\n} from '@midwayjs/core';\nimport { helloworld, hero } from '../interface';\nimport { Clients } from '@midwayjs/grpc';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  grpcClients: Clients;\n\n}\n```\n\n\n我们通过 `clients` 获取到对方服务的客户端实例，然后调用即可。\n\n\n```typescript\nimport {\n  Provide,\n  Inject,\n} from '@midwayjs/core';\nimport { helloworld, hero } from '../interface';\nimport { Clients } from '@midwayjs/grpc';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  grpcClients: Clients;\n\n  async invoke() {\n    // 获取服务\n  \tconst greeterService = this.grpcClients.getService<helloworld.GreeterClient>(\n      'helloworld.Greeter'\n    );\n\n    // 调用服务\n    const result = await greeterService.sayHello()\n    \t.sendMessage({\n        name: 'harry'\n      });\n\n    // 返回结果\n    return result;\n  }\n\n}\n```\n\n\n我们也可以利用 `@Init` 装饰器，将需要调用的服务缓存到属性上。这样可以在其他方法调用时复用。\n\n\n示例如下。\n```typescript\nimport {\n  GrpcMethod,\n  MSProviderType,\n  Provider,\n  Inject,\n  Init,\n} from '@midwayjs/core';\nimport { helloworld, hero } from '../interface';\nimport { Clients } from '@midwayjs/grpc';\n\n@Provider(MSProviderType.GRPC, { package: 'hero' })\nexport class HeroService implements hero.HeroService {\n  // 注入客户端\n  @Inject()\n  grpcClients: Clients;\n\n  greeterService: helloworld.GreeterClient;\n\n  @Init()\n  async init() {\n    // 赋值一个服务实例\n    this.greeterService = this.grpcClients.getService<helloworld.GreeterClient>(\n      'helloworld.Greeter'\n    );\n  }\n\n  @GrpcMethod()\n  async findOne(data) {\n    // 调用服务\n    const result = await greeterService.sayHello()\n    \t.sendMessage({\n        name: 'harry'\n      });\n\n    // 返回结果\n    return result;\n  }\n}\n\n```\n\n\n## 流式服务\n\n\ngRPC 的流式服务用于减少连接，让服务端或者客户端不需要等待即可执行任务，从而提高执行效率。\n\n\ngRPC 的流式服务分为三种，以服务端角度来说，为\n\n\n- 服务端接收流（客户端推）\n- 服务端响应流（服务端推）\n- 双向流\n\n\n\n下面我们将一一介绍。\n\n\n### 流式 proto 文件\n\n\n流式的 proto 文件写法不同，需要在希望使用流式的地方将参数标记为 `stream` 。\n```protobuf\n\nsyntax = \"proto3\";\n\npackage math;\n\nmessage AddArgs {\n  int32 id = 1;\n  int32 num = 2;\n}\n\nmessage Num {\n  int32 id = 1;\n  int32 num = 2;\n}\n\nservice Math {\n  rpc Add (AddArgs) returns (Num) {\n  }\n\n\t// 双向流\n  rpc AddMore (stream AddArgs) returns (stream Num) {\n  }\n\n  // 服务端往客户端推\n  rpc SumMany (AddArgs) returns (stream Num) {\n  }\n\n  // 客户端往服务端推\n  rpc AddMany (stream AddArgs) returns (Num) {\n  }\n}\n\n```\n该服务生成的接口定义为：\n\n\n```typescript\nimport {\n  IClientDuplexStreamService,\n  IClientReadableStreamService,\n  IClientUnaryService,\n  IClientWritableStreamService,\n  IClientOptions,\n} from '@midwayjs/grpc';\n\nexport namespace math {\n  export interface AddArgs {\n    id?: number;\n    num?: number;\n  }\n  export interface Num {\n    id?: number;\n    num?: number;\n  }\n\n  /**\n   * server interface\n   */\n  export interface Math {\n    add(data: AddArgs): Promise<Num>;\n    addMore(data: AddArgs): Promise<void>;\n    // 服务端推，客户端读\n    sumMany(data: AddArgs): Promise<void>\n    // 客户端端推，服务端读\n    addMany(num: AddArgs): Promise<void>;\n  }\n\n  /**\n   * client interface\n   */\n  export interface MathClient {\n    add(options?: IClientOptions): IClientUnaryService<AddArgs, Num>;\n    addMore(options?: IClientOptions): IClientDuplexStreamService<AddArgs, Num>;\n    // 服务端推，客户端读\n    sumMany(options?: IClientOptions): IClientReadableStreamService<AddArgs, Num>;\n    // 客户端端推，服务端读\n    addMany(options?: IClientOptions): IClientWritableStreamService<AddArgs, Num>;\n  }\n}\n\n```\n\n\n### 服务端推送\n\n\n客户端调用一次，服务端可以多次返回。通过 `@GrpcMethod()` 的参数来标识流式类型。\n\n\n可用的类型为:\n\n\n- `GrpcStreamTypeEnum.WRITEABLE`  服务端输出流（单工）\n- `GrpcStreamTypeEnum.READABLE`  客户端输出流（单工），服务端接受多次\n- `GrpcStreamTypeEnum.DUPLEX`  双工流\n\n\n\n服务端示例如下：\n```typescript\nimport { GrpcMethod, GrpcStreamTypeEnum, Inject, MSProviderType, Provider } from '@midwayjs/core';\nimport { Context, Metadata } from '@midwayjs/grpc';\nimport { math } from '../interface';\n\n/**\n */\n@Provider(MSProviderType.GRPC, { package: 'math' })\nexport class Math implements math.Math {\n\n  @Inject()\n  ctx: Context;\n\n  @GrpcMethod({type: GrpcStreamTypeEnum.WRITEABLE })\n  async sumMany(args: math.AddArgs) {\n    this.ctx.write({\n      num: 1 + args.num\n    });\n    this.ctx.write({\n      num: 2 + args.num\n    });\n    this.ctx.write({\n      num: 3 + args.num\n    });\n\n    this.ctx.end();\n  }\n\n  // ...\n}\n\n```\n服务端使用 `ctx.write` 方法来返回数据，由于是服务端流，可以返回多次。\n\n\n返回结束后，请使用 `ctx.end()` 方法关闭流。\n\n\n客户端，调用一次，接受多次数据。\n\n\n比如下面的累加逻辑。\n\n\nPromise 写法，会等待服务端数据都返回再做处理。\n```typescript\n// 服务端推送\nlet total = 0;\nlet result = await service.sumMany().sendMessage({\n  num: 1,\n});\n\nresult.forEach(data => {\n  total += data.num;\n});\n\n// total = 9;\n```\n\n\n事件写法，实时处理。\n```typescript\n// 服务端推送\nlet call = service.sumMany().getCall();\n\ncall.on('data', data => {\n\t// do something\n});\n\ncall.sendMessage({\n  num: 1,\n});\n\n```\n\n\n### 客户端推送\n\n\n客户端调用多次，服务端接收多次数据，返回一个结果。通过 `@GrpcMethod({type: GrpcStreamTypeEnum.READABLE})` 的参数来标识流式类型。\n\n\n服务端示例如下：\n```typescript\nimport { GrpcMethod, GrpcStreamTypeEnum, Inject, MSProviderType, Provider } from '@midwayjs/core';\nimport { Context, Metadata } from '@midwayjs/grpc';\nimport { math } from '../interface';\n\n/**\n */\n@Provider(MSProviderType.GRPC, { package: 'math' })\nexport class Math implements math.Math {\n\n  sumDataList: number[] = [];\n\n  @Inject()\n  ctx: Context;\n\n  @GrpcMethod({type: GrpcStreamTypeEnum.READABLE, onEnd: 'sumEnd' })\n  async addMany(data: math.Num) {\n    this.sumDataList.push(data);\n  }\n\n  async sumEnd(): Promise<math.Num> {\n    const total = this.sumDataList.reduce((pre, cur) => {\n      return {\n        num: pre.num + cur.num,\n      }\n    });\n    return total;\n  }\n\n  // ...\n}\n\n```\n\n\n客户端每次调用，都会触发一次 `addMany` 方法。\n\n\n在客户端发送 `end` 事件之后，会调用 `@GrpcMethod` 装饰器上的 `onEnd` 参数指定的方法，该方法的返回值即为最后客户端拿到的值。\n\n\n客户端示例如下：\n```typescript\n// 客户端推送\nconst data = await service.addMany()\n.sendMessage({num: 1})\n.sendMessage({num: 2})\n.sendMessage({num: 3})\n.end();\n\n// data.num = 6\n```\n\n\n### 双向流\n\n\n客户端可以调用多次，服务端也可以接收多次数据，返回多个结果，类似于传统的 TCP 通信。通过 `@GrpcMethod({type: GrpcStreamTypeEnum.DUPLEX})` 的参数来标识双工流式类型。\n\n\n服务端示例如下：\n```typescript\nimport { GrpcMethod, GrpcStreamTypeEnum, Inject, MSProviderType, Provider } from '@midwayjs/core';\nimport { Context, Metadata } from '@midwayjs/grpc';\nimport { math } from '../interface';\n\n/**\n */\n@Provider(MSProviderType.GRPC, { package: 'math' })\nexport class Math implements math.Math {\n\n  @Inject()\n  ctx: Context;\n\n  @GrpcMethod({type: GrpcStreamTypeEnum.DUPLEX, onEnd: 'duplexEnd' })\n  async addMore(message: math.AddArgs)  {\n    this.ctx.write({\n      id: message.id,\n      num: message.num + 10,\n    });\n  }\n\n  async duplexEnd() {\n    console.log('got client end message');\n  }\n  // ...\n}\n\n```\n服务端可以随时使用 `ctx.write` 返回数据，也可以使用 `ctx.end` 来关闭流。\n\n\n客户端示例：\n\n\n对于双工通信的客户端，由于无法保证调用、返回的顺序，我们需要使用监听的模式来消费结果。\n```typescript\nconst clientStream = service.addMore().getCall();\n\nlet total = 0;\nlet idx = 0;\n\nduplexCall.on('data', (data: math.Num) => {\n  total += data.num;\n  idx++;\n  if (idx === 2) {\n    duplexCall.end();\n    // total => 29\n  }\n});\n\nduplexCall.write({\n  num: 3,\n});\n\nduplexCall.write({\n  num: 6,\n});\n```\n\n\n如果希望保证调用顺序，我们也提供了保证顺序的双向流调用方法，但是需要在 proto 中定义一个固定的 id，来确保顺序。\n\n\n比如我们的 Math.proto，对每个入参和出参，都增加了一个固定的 id，所以可以固定顺序。\n```typescript\n\nsyntax = \"proto3\";\n\npackage math;\n\nmessage AddArgs {\n  int32 id = 1;\t\t\t\t\t\t\t//  这里的 id 名字是固定的\n  int32 num = 2;\n}\n\nmessage Num {\n  int32 id = 1;\t\t\t\t\t\t\t//  这里的 id 名字是固定的\n  int32 num = 2;\n}\n\nservice Math {\n  rpc Add (AddArgs) returns (Num) {\n  }\n\n  rpc AddMore (stream AddArgs) returns (stream Num) {\n  }\n\n  // 服务端往客户端推\n  rpc SumMany (AddArgs) returns (stream Num) {\n  }\n\n  // 客户端往服务端推\n  rpc AddMany (stream AddArgs) returns (Num) {\n  }\n}\n\n```\n固定顺序的客户端调用方式如下：\n```typescript\n// 保证顺序的双向流\nconst t = service.addMore();\n\nconst result4 = await new Promise<number>((resolve, reject) => {\n\n  let total = 0;\n\n  // 第一次调用和返回\n  t.sendMessage({\n    num: 2\n  })\n    .then(res => {\n      expect(res.num).toEqual(12);\n      total += res.num;\n  \t})\n    .catch(err => console.error(err));\n\n  // 第二次调用和返回\n  t.sendMessage({\n    num: 5\n  }).then(res => {\n      expect(res.num).toEqual(15);\n      total += res.num;\n      resolve(total);\n    })\n    .catch(err => console.error(err));\n\n  t.end();\n});\n\n// result4 => 27\n```\n默认的 id 为 `id` ，如果服务端定义不同，需要修改，可以在客户端调用时传递。\n```typescript\n// 保证顺序的双向流\nconst t = service.addMore({\n  messageKey: 'uid'\n});\n```\n\n\n## 元数据（Metadata）\n\n\ngRPC 的元数据等价于 HTTP 的上下文。\n\n\n服务端通过 `ctx.sendMetadata` 方法返回元数据，也可以通过 `ctx.metadata` 获取客户端传递的元数据。\n```typescript\nimport {\n  MSProviderType,\n  Provider,\n  GrpcMethod,\n} from '@midwayjs/core';\nimport { helloworld } from '../domain/helloworld';\nimport { Context, Metadata } from '@midwayjs/grpc';\n\n/**\n * 实现 helloworld.Greeter 接口的服务\n */\n@Provider(MSProviderType.GRPC, { package: 'helloworld' })\nexport class Greeter implements helloworld.Greeter {\n\n  @Inject()\n  ctx: Context;\n\n  @GrpcMethod()\n  async sayHello(request: helloworld.HelloRequest) {\n\n    // 客户端传递的元数据\n    console.log(this.ctx.metadata);\n\n    // 创建元数据\n    const meta = new Metadata();\n    this.ctx.metadata.add('xxx', 'bbb');\n    this.ctx.sendMetadata(meta);\n\n    return { message: 'Hello ' + request.name };\n  }\n}\n```\n\n\n客户端通过方法的 options 参数传递元数据。\n```typescript\nimport { Metadata } from '@midwayjs/grpc';\n\nconst meta = new Metadata();\nmeta.add('key', 'value');\n\nconst result = await service.sayHello({\n  metadata: meta,\n}).sendMessage({\n  name: 'harry'\n});\n```\n\n\n获取元数据相对麻烦一些。\n\n\n普通一元调用（UnaryCall）获取元数据需要使用 `sendMessageWithCallback` 方法。\n```typescript\nconst call = service.sayHello().sendMessageWithCallback({\n  name: 'zhangting'\n}, (err) => {\n  if (err) {\n    reject(err);\n  }\n});\ncall.on('metadata', (meta) => {\n  // output meta\n});\n```\n其他流式服务，可以通过 `getCall()` 方法获取原始客户端流对象，从而直接订阅。\n```typescript\n// 获取服务，注意，这里没有 await\nconst call = service.addMany().getCall();\ncall.on('metadata', (meta) => {\n  // output meta\n});\n```\n\n\n## 超时处理\n\n\n我们可以在调用服务时传递参数，单位毫秒。\n```typescript\nconst result = await service.sayHello({\n  timeout: 5000\n}).sendMessage({\n  name: 'harry'\n});\n```\n","site/docs/extensions/http-proxy.md":"# HTTP 代理\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的 HTTP 请求代理组件，支持 GET、POST 等多种请求方法。\n\n相关信息：\n\n| web 支持情况      |     |\n| ----------------- | --- |\n| @midwayjs/koa     | ✅  |\n| @midwayjs/faas    | 💬 |\n| @midwayjs/web     | ✅  |\n| @midwayjs/express | ✅  |\n\n:::caution\n\n💬 部分函数计算平台不支持流式请求响应，请参考对应平台能力。\n\n:::\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/http-proxy@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/http-proxy\": \"^4.0.0\"\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n## 启用组件\n\n在 `src/configuration.ts` 中引入组件\n\n```typescript\n// ...\nimport * as proxy from '@midwayjs/http-proxy';\n\n@Configuration({\n  imports: [\n    // ...other components\n    proxy,\n  ],\n})\nexport class MainConfiguration {}\n```\n\n## 配置\n\n代理配置定义如下：\n\n```typescript\n// 代理配置类型\nexport interface HttpProxyConfig {\n  // 匹配要代理的 URL 正则表达式\n  match: RegExp;\n  // 替换匹配到的链接的 host，将请求代理到此地址\n  host?: string;\n  // 通过正则的表达式捕获组处理代理地址\n  target?: string;\n  // 转发请求超时时间，默认为0不设置超时时间\n  proxyTimeout?: number;\n  // 忽略代理请求转发的 header 中的字段\n  ignoreHeaders?: {\n    [key: string]: boolean;\n  };\n}\n```\n\n代理支持单个代理和多个代理。\n\n单个代理配置\n\n```typescript\n// src/config/config.default.ts\n\nexport default {\n  httpProxy: {\n    match: /\\/tfs\\//,\n    host: 'https://gw.alicdn.com',\n  },\n};\n```\n\n多个代理配置\n\n```typescript\n// src/config/config.default.ts\n\n// 代理配置类型\nexport default {\n  httpProxy: {\n    default: {\n      // 一些每个策略复用的值，会和底下的策略进行合并\n    },\n    strategy: {\n      gw: {\n        // https://gw.alicdn.com/tfs/TB1.1EzoBBh1e4jSZFhXXcC9VXa-48-48.png\n        match: /\\/tfs\\//,\n        host: 'https://gw.alicdn.com',\n      },\n      g: {\n        // https://g.alicdn.com/mtb/lib-mtop/2.6.1/mtop.js\n        match: /\\/bdimg\\/(.*)$/,\n        target: 'https://sm.bdimg.com/$1',\n      },\n      httpBin: {\n        // https://httpbin.org/\n        match: /\\/httpbin\\/(.*)$/,\n        target: 'https://httpbin.org/$1',\n      },\n    },\n  },\n};\n```\n\n## 示例：使用 host 配置代理\n\n```typescript\nexport default {\n  httpProxy: {\n    match: /\\/tfs\\//,\n    host: 'https://gw.alicdn.com',\n  },\n};\n```\n\n当请求您的站点路径为： `https://yourdomain.com/tfs/test.png` 时，`match` 字段配置的正则表达式成功匹配，那么就将原始请求路径中的 `host` 部分 `https://yourdomain.com` 替换为配置的 `https://gw.alicdn.com`，从而发起代理请求到 `https://gw.alicdn.com/tfs/test.png`，并把响应结果返回给请求您站点的用户。\n\n## 示例：使用 target 配置代理\n\n```typescript\nexport default {\n  httpProxy: {\n    match: /\\/httpbin\\/(.*)$/,\n    target: 'https://httpbin.org/$1',\n  },\n};\n```\n\n当请求您的站点路径为： `https://yourdomain.com/httpbin/get?name=midway` 时，`match` 字段配置的正则表达式成功匹配，同时正则的捕获组中有结果 `['get?name=midway']` ，那么就将原始请求路径中的 `$1` 部分替换为捕获组中的第 1 个数据（index: 0）的 `get?name=midway`，从而发起代理请求到 `https://httpbin.org/get?name=midway`，并把响应结果返回给请求您站点的用户。\n","site/docs/extensions/i18n.md":"# 多语言\n\nMidway 提供了多语言组件，让业务可以快速指定不同的语言，展示不同的文案，也可以在 HTTP 场景配合请求参数，请求头等方式来使用。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n## 安装组件\n\n```bash\n$ npm i @midwayjs/i18n@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/i18n\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 使用组件\n\n将 i18n 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as i18n from '@midwayjs/i18n';\n\n@Configuration({\n  imports: [\n    // ...\n    i18n\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n\n\n## 使用\n\n组件提供了 `MidwayI18nService`  服务，用于翻译多语言文本。\n\n使用 `translate` 方法，传入不同的文本关键字和参数，返回不同语言的文本内容。\n\n```typescript\n@Controller('/')\nexport class UserController {\n\n  @Inject()\n  i18nService: MidwayI18nService;\n\n  @Get('/')\n  async index(@Query('username') username: string) {\n    return this.i18nService.translate('HELLO_MESSAGE', {\n      args: {\n        username\n      },\n    });\n  }\n}\n```\n\n\n\n## 配置多语言文案\n\n你可以在配置文件中直接配置，但是大多数情况下，文案会很多，有时候甚至可能文案在远端服务上，这个时候直接配置就不太现实。\n\n一般来说，我们会将文案单独放到某个文案配置目录中，比如 `src/locales` 。\n\n以 `src/locale` 这个目录为例，我们举个例子，结构如下：\n\n```text\n.\n├── src\n│   ├── locales\n|   │   ├── en_US.json\n|   │   └── zh_CN.json\n│   └── controller\n│       └── home.controller.ts\n├── package.json\n└── tsconfig.json\n```\n\n这里我们建了两个多语言的文件，`en_US.json` 和 `zh_CN.json`，分别代表英文和中文。\n\n文件内容分别如下：\n\n```json\n// src/locales/en_US.json\n{\n  \"hello\": \"Hello {username}\",\n  \"email\": \"email id\",\n  \"login\": \"login account\",\n  \"createdAt\": \"register date\"\n}\n```\n\n```json\n// src/locales/zh_CN.json\n{\n  \"hello\": \"你好 {username}\",\n  \"email\": \"邮箱\",\n  \"login\": \"帐号\",\n  \"createdAt\": \"注册时间\"\n}\n```\n\n每行一个字符串对，是一个标准的 JSON 格式内容，也可以使用 js/ts 文件，花括号中是可替换的参数占位。\n\n同时，需要在配置中加入这两个 JSON，其中 `default` 是语言的默认分组。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    // 把你的翻译文本放到这里\n    localeTable: {\n      en_US: {\n        default: require('../locale/en_US'),\n      },\n      zh_CN: {\n        default: require('../locale/zh_CN'),\n      }\n    },\n  }\n}\n```\n\n这样就可以使用了，使用输出如下。\n\n```typescript\nthis.i18nService.translate('hello', {\n  args: {\n    username: 'harry',\n  },\n  locale: 'en_US',\n});\n\n// output: Hello harry.\n\nthis.i18nService.translate('hello', {\n  args: {\n    username: 'harry',\n  },\n  locale: 'zh_CN',\n});\n\n// output: 你好 harry.\n\n```\n\n\n\n## 多语言文案分组\n\n在如下配置中，用户配置的多语言文案在 `default` 分组中。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    // 把你的翻译文本放到这里\n    localeTable: {\n      en_US: {\n        default: require('../locale/en_US'),\n      },\n      zh_CN: {\n        default: require('../locale/zh_CN'),\n      }\n    },\n  }\n}\n```\n\n这样做的好处是，在其他组件或者业务代码中，我们也可以使用不同的分组名，来添加其他的多语言文案。\n\n比如：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    // 把你的翻译文本放到这里\n    localeTable: {\n      en_US: {\n        default: require('../locale/en_US'),\n        user: require('../locale/user_en_US'),\n      },\n      zh_CN: {\n        default: require('../locale/zh_CN'),\n        user: require('../locale/user_zh_CN'),\n      }\n    },\n  }\n}\n```\n\n在代码中，如果调用非默认分组，需要指定分组参数。\n\n```typescript\nthis.i18nService.translate('user.hello', {\n  args: {\n    username: 'harry',\n  },\n  group: 'user',\t\t// 指定其他分组\n  locale: 'en_US',\n});\n\n```\n\n\n\n## 多语言文案格式\n\n多语言文本中可以添加参数，参数可以有 `对象` 和 `数组` 两种形式。\n\n对象形式如下，使用花括号作为占位符。\n\n```text\nHello {username}\n```\n\n使用时，通过配置传递，按对象 key 覆盖变量。\n\n```typescript\nasync index(@Query('username') username: string) {\n  return this.i18nService.translate('hello', {\n    args: {\n      username\n    },\n  });\n}\n```\n\n数组形式如下，使用数字作为占位符。\n\n```text\nHello {0}\n```\n\n使用时，通过配置传递，格式是数组形式，按数组顺序覆盖数字变量。\n\n```typescript\nasync index(@Query('username') username: string) {\n  return this.i18nService.translate('hello', {\n    args: [username]\n  });\n}\n```\n\n\n\n## 动态添加多语言文案\n\n有时候，多语言文案可能放在远端，比如数据库等，我们可以通过 `addLocale` 方法进行动态添加。\n\n比如，在配置加载后，代码使用前。\n\n```typescript\n// configuration.ts\n\n// ...\n@Configuration({\n  imports: [\n    koa,\n    i18n\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  i18nService: MidwayI18nService;\n\n  async onReady() {\n    this.i18nService.addLocale('zh_TW', {\n      hello: '你好，{username} 美麗的世界'\n    });\n  }\n\n\n  // ...\n}\n```\n\n在代码中就可以使用。\n\n```typescript\nasync index(@Query('username') username: string) {\n  return this.i18nService.translate('hello', {\n    args: [username],\n    locale: 'zh_TW'\n  });\n}\n```\n\n\n\n## 通过参数指定当前语言\n\n一般情况下，默认语言为 `en_US`，用户的浏览器访问一般会自带 `Accept-Language` 头，所以会正确识别语言。比如用中文浏览器访问，就能正常显示中文。\n\n除此之外，在 HTTP 场景下可以通过 URL Query，Cookie，Header 来指定语言。\n\n优先级从上到下：\n\n- query: /?locale=en-US\n- cookie: locale=zh-TW\n- header: Accept-Language: zh-CN,zh;q=0.5\n\n当传递了这些参数之后，多语言数据会自动保存到当前用户的 Cookie 中，下次请求会直接用该设定好的语言。\n\n\n\n## 手动设置语言\n\n可以通过调用 `saveRequestLocale` 设置当前语言。\n\n```typescript\nasync index() {\n  // ...\n  this.i18nService.saveRequestLocale('zh_CN');\n}\n```\n\n如果开启了 `writeCookie` 配置，设置后会保存到当前用户的 Cookie 中，下次请求会使用该设置。\n\n\n\n## 语言选择优先级\n\n这些多种设置语言的方式，有着不同的优先级，如下优先级从高到低：\n\n- 1、`i18nService.translate` 方法显式指定的语言\n- 2、通过其他装饰器设置的语言，比如 `@Validate` 装饰器的参数（本质是调用了`i18nService.translate` 方法）\n- 3、通过 `saveRequestLocale` API 直接设置的当前语言\n- 4、通过浏览器 Query，Cookie，Header 设置的语言（本质是调用了 `saveRequestLocale`）\n- 5、i18n 组件配置中的默认语言\n\n\n\n## 关于语言大小写\n\n在代码内部，我们会将所有的多语言，fallback 规则，写入的文本串，返回的 locale 结果，使用下面的规则替代\n\n- 1、使用中划线代替下划线\n- 2、使用小写代替大写\n\n即所有的 `en_US` 都会变成 `en-us`，`zh_CN` 会变成 `zh-cn`。\n\n这样做会安全的适配 URL 和 Cookie。\n\n\n\n## View 中使用\n\n在 Web 类型的框架中，我们默认添加了 locals 变量支持，可以在模板引擎中使用。\n\n假设我们使用的模板引擎是 [Nunjucks](./render)，可以直接引用到 `i18n` 方法。\n\n多语言文案如下：\n\n```json\n{\n  \"hello\": \"Hello {username}\",\n}\n```\n\n模板如下：\n\n```html\n<span>{{ i18n('hello', user) }}</span>\n```\n\n示例如下：\n\n```typescript\n// ...\n\n@Controller('/')\nexport class UserController {\n\n  @Inject()\n  ctx: Context;\n\n  @Get('/')\n  async index() {\n    await this.ctx.render('index', {\n      // 注意这里是整个对象传递给模板\n      user: {\n        username: 'harry',\n      }\n    });\n  }\n}\n```\n\ni18n 方法定义如下：\n\n```typescript\nfunction i18n(templateName: string, args: Record<string, any>) {\n  // ...\n}\n```\n\n方法名可以通过配置修改。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    localsField: 'i18n',\n  }\n}\n```\n\n\n\n\n\n## 配置\n\n### 默认配置\n\n大部分情况下，你只需要在配置 `localeTable` 添加你自己的多语言翻译即可。\n\n下面是完整的配置，你可以在配置定义中找到。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    // 默认语言  \"en_US\"\n    defaultLocale: 'en_US',\n\n    // 把你的翻译文本放到这里\n    localeTable: {\n      en_US: {\n        // group name\n        default: {\n          // hello: 'hello'\n        }\n      },\n      zh_CN: {\n        // group name\n        default: {\n          // hello: '你好'\n        }\n      },\n    },\n\n    // 语言映射，可以用 * 号通配\n    fallbacks: {\n      //   'en_*': 'en_US',\n      //   pt: 'pt-BR',\n    },\n    // 是否将请求参数写入 cookie\n    writeCookie: true,\n    resolver: {\n      // url query 参数，默认是 \"locale\"\n      queryField: 'locale',\n      cookieField: {\n        // Cookie 里的 key，默认是 \"locale\"\n        fieldName: 'locale',\n        // Cookie 域名，默认为空，代表当前域名有效\n        cookieDomain: '',\n        // Cookie 默认的过期时间，默认一年\n        cookieMaxAge: FORMAT.MS.ONE_YEAR,\n      },\n    },\n    localsField: 'i18n',\n    // 缺失翻译键的处理函数，默认返回原始键名\n    missingKeyHandler: (message, options) => message,\n  }\n}\n```\n\n\n\n### 回写 Cookie\n\n默认情况下，多语言组件会将当前用户的语言回写到 Cookie 中，避免下次请求再进行查找以提高性能，我们可以通过配置关闭这个行为。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    writeCookie: false,\n  }\n}\n```\n\n\n\n### 请求解析配置\n\nHTTP 场景下，我们提供了通过参数指定当前语言的能力。\n\n默认情况下，组件通过下面的字段来查找。\n\n- query 的 `locale` 字段\n- cookie 的 `locale` 字段\n- header 的 `Accept-Language` 部分\n\n我们可以通过配置修改查询的字段。\n\n比如，修改 Query 的字段。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    resolver: {\n      queryField: 'abc'\n    },\n  }\n}\n```\n\n我们就可以通过 `/?abc=en-US` 来请求修改语言。\n\n如果不希望通过请求来设置语言，可以将整个 `resolver` 解析关闭，对 Cookie 的回写也将同时停止。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    resolver: false,\n  }\n}\n```\n\n\n### 缺失翻译键处理\n\n当翻译键在所有语言文件中都找不到时，i18n 组件会调用 `missingKeyHandler` 函数来处理这种情况。\n\n默认情况下，当找不到翻译时，会返回原始的键名：\n\n```typescript\n// 默认的 missingKeyHandler\nmissingKeyHandler: (message, options) => message\n```\n\n例如：\n```typescript\n// 假设 \"unknown_key\" 在任何语言文件中都不存在\nthis.i18nService.translate('unknown_key');\n// 返回: \"unknown_key\"\n```\n\n你可以自定义缺失键的处理方式，比如添加特殊标记、记录日志等：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  i18n: {\n    missingKeyHandler: (message, options) => {\n      // 记录缺失的翻译键\n      console.warn(`Missing translation: ${message}`, options);\n\n      // 根据不同的组返回不同的格式\n      const group = options?.group || 'default';\n      const locale = options?.locale || 'default';\n\n      return `[${group}:${locale}:${message}]`;\n    }\n  }\n}\n```\n\n\n## 常用语言\n\n| 语言             | 语言包名 |\n| :--------------- | :------- |\n| 阿拉伯           | ar_EG    |\n| 亞美尼亞         | hy_AM    |\n| 保加利亚语       | bg_BG    |\n| 加泰罗尼亚语     | ca_ES    |\n| 捷克语           | cs_CZ    |\n| 丹麦语           | da_DK    |\n| 德语             | de_DE    |\n| 希腊语           | el_GR    |\n| 英语             | en_GB    |\n| 英语（美式）     | en_US    |\n| 西班牙语         | es_ES    |\n| 爱沙尼亚语       | et_EE    |\n| 波斯语           | fa_IR    |\n| 芬兰语           | fi_FI    |\n| 法语（比利时）   | fr_BE    |\n| 法语             | fr_FR    |\n| 希伯来语         | he_IL    |\n| 印地语           | hi_IN    |\n| 克罗地亚语       | hr_HR    |\n| 匈牙利           | hu_HU    |\n| 冰岛语           | is_IS    |\n| 印度尼西亚语     | id_ID    |\n| 意大利语         | it_IT    |\n| 日语             | ja_JP    |\n| 格鲁吉亚语       | ka_GE    |\n| 卡纳达语         | kn_IN    |\n| 韩语/朝鲜语      | ko_KR    |\n| 库尔德语         | ku_IQ    |\n| 拉脱维亚语       | lv_LV    |\n| 马来语           | ms_MY    |\n| 蒙古语           | mn_MN    |\n| 挪威             | nb_NO    |\n| 尼泊尔语         | ne_NP    |\n| 荷兰语（比利时） | nl_BE    |\n| 荷兰语           | nl_NL    |\n| 波兰语           | pl_PL    |\n| 葡萄牙语(巴西)   | pt_BR    |\n| 葡萄牙语         | pt_PT    |\n| 斯洛伐克语       | sk_SK    |\n| 塞尔维亚         | sr_RS    |\n| 斯洛文尼亚       | sl_SI    |\n| 瑞典语           | sv_SE    |\n| 泰米尔语         | ta_IN    |\n| 泰语             | th_TH    |\n| 土耳其语         | tr_TR    |\n| 罗马尼亚语       | ro_RO    |\n| 俄罗斯语         | ru_RU    |\n| 乌克兰语         | uk_UA    |\n| 越南语           | vi_VN    |\n| 简体中文         | zh_CN    |\n| 繁体中文         | zh_TW    |\n\n\n\n## 常见问题\n\n### 1、测试配置全局语言不生效\n\n一般场景下，你 **无需** 配置全局语言，因为浏览器访问会自动带上语言信息，比如中文浏览器自动返回中文，英文浏览器自动返回英文。\n\n假如你明确希望测试全局设置的效果，请务必按下面操作：\n\n* 1、如果使用的是浏览器，请清空页面 cookie 再访问，因为 cookie 中会记录上一次用户的语言信息\n* 2、如果使用的是 Postman 等工具，请不要带上 cookie 以及语言相关的 Header，Query 等字段\n\n### 2、测试返回的语言非预期\n\n请使用浏览器进行测试，不要使用 Postman。\n\n由于 Postman 请求不会带有浏览器语言相关的 Header，所以服务端无法自动判断语言。\n\n如果你一定要使用 Postman，请参考浏览器请求，加上 `Accept-Language` Header。\n","site/docs/extensions/info.md":"# 信息查看\n\nMidway 提供了 info 组件，用于展示应用的基本信息，方便排查问题。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/info@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/info\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 使用组件\n\n将 info 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as info from '@midwayjs/info';\n\n@Configuration({\n  imports: [\n    // ...\n    info\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n在有些情况下，为了不想让应用信息透出，我们指定在特殊环境下生效。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as info from '@midwayjs/info';\n\n@Configuration({\n  imports: [\n    koa,\n    {\n      component: info,\n      enabledEnvironment: ['local'],\t// 只在本地启用\n    }\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n\n\n## 查看信息\n\n在默认情况下，info 组件会为 Http 场景自动添加一个中间件，我们可以通过 `/_info` 来访问。\n\n默认情况下，会展示系统，进程，以及配置等关键信息。\n\n效果如下：\n\n![info](https://img.alicdn.com/imgextra/i3/O1CN01TCkSvr28x8T7gtnCl_!!6000000007998-2-tps-797-1106.png)\n\n\n\n## 修改访问路由\n\n为了安全，我们可以调整访问的路由。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  info: {\n    infoPath: '/_my_info',\n  }\n}\n```\n\n\n\n## 隐藏信息\n\n默认情况下，info 组件会隐藏秘钥等信息。我们可以配置增减隐藏的关键字，这个配置会对 **环境变量** 以及 **多环境配置** 生效。\n\n关键字可以使用通配符，比如增加一些关键字。\n\n```typescript\n// src/config/config.default.ts\nimport { DefaultHiddenKey } from '@midwayjs/info';\n\nexport default {\n  // ...\n  info: {\n    hiddenKey: DefaultHiddenKey.concat(['*abc', '*def', '*bbb*']),\n  }\n}\n```\n\n\n\n## 调用 API\n\ninfo 组件默认提供了 `InfoService` 用于在非 Http 或是自定义的场景来使用。\n\n比如：\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InfoService } from '@midwayjs/info';\n\n@Provide()\nexport class userService {\n\n  @Inject()\n  inforService: InfoService\n\n  async getInfo() {\n    // 应用信息，应用名等\n    this.inforService.projectInfo();\n    // 系统信息\n    this.inforService.systemInfo();\n    // 堆内存，cpu 等\n    this.inforService.resourceOccupationInfo();\n    // midway 框架的信息\n    this.inforService.softwareInfo();\n    // 当前使用的环境配置\n    this.inforService.midwayConfig();\n    // 依赖注入容器中的服务\n    this.inforService.midwayService();\n    // 系统时间，时区，启动时常\n    this.inforService.timeInfo();\n    // 环境变量\n    this.inforService.envInfo();\n    // 依赖信息\n    this.inforService.dependenciesInfo();\n    // 网络信息\n    this.inforService.networkInfo();\n  }\n}\n```\n\n","site/docs/extensions/jwt.md":"# JWT\n\n`JSON Web Token` (JWT)是一个开放标准(RFC 7519)，它定义了一种紧凑的、自包含的方式，用于作为`JSON`对象在各方之间安全地传输信息。该信息可以被验证和信任，因为它是数字签名的。\n\nMidway 提供了 jwt 组件，简单提供了一些 jwt 相关的 API，可以基于它做独立的鉴权和校验。\n\n相关信息：\n\n| 描述              |     |\n| ----------------- | --- |\n| 可用于标准项目    | ✅  |\n| 可用于 Serverless | ✅  |\n| 可用于一体化      | ✅  |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/jwt@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/jwt\": \"^4.0.0\"\n    // ...\n  },\n}\n```\n\n## 使用组件\n\n将 jwt 组件配置到代码中。\n\n```typescript\nimport { Configuration, IMidwayContainer } from '@midwayjs/core';\nimport { IMidwayContainer } from '@midwayjs/core';\nimport * as jwt from '@midwayjs/jwt';\n\n@Configuration({\n  imports: [\n    // ...\n    jwt,\n  ],\n})\nexport class MainConfiguration {\n  // ...\n}\n```\n\n## 基础配置\n\n然后在配置中设置，默认未加密。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  jwt: {\n    secret: 'xxxxxxxxxxxxxx', // fs.readFileSync('xxxxx.key')\n    sign: {\n      // signOptions\n      expiresIn: '2d', // https://github.com/vercel/ms\n    },\n    verify: {\n      // verifyOptions\n    },\n    decode: {\n      // decodeOptions\n    }\n  },\n};\n```\n\n更多配置请查看 ts 定义。\n\n## 常用 API\n\nMidway 将 jwt 常用 API 提供为同步和异步两种形式。\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { JwtService } from '@midwayjs/jwt';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  jwtService: JwtService;\n\n  async invoke() {\n    // 同步 API\n    this.jwtService.signSync(payload, secretOrPrivateKey, options);\n    this.jwtService.verifySync(token, secretOrPublicKey, options);\n    this.jwtService.decodeSync(token, options);\n\n    // 异步 API\n    await this.jwtService.sign(payload, secretOrPrivateKey, options);\n    await this.jwtService.verify(token, secretOrPublicKey, options);\n    await this.jwtService.decode(token, options);\n  }\n}\n```\n\n这些 API 都来自于 [node-jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) 基础库，如果不了解请阅读原版文档。\n\n## 中间件示例\n\n一般，jwt 还会配合中间件来完成鉴权，下面是一个自定义 jwt 鉴权的中间件示例。\n\n```typescript\n// src/middleware/jwt.middleware\n\nimport { Inject, Middleware, httpError } from '@midwayjs/core';\nimport { Context, NextFunction } from '@midwayjs/koa';\nimport { JwtService } from '@midwayjs/jwt';\n\n@Middleware()\nexport class JwtMiddleware {\n  @Inject()\n  jwtService: JwtService;\n\n  public static getName(): string {\n    return 'jwt';\n  }\n\n  resolve() {\n    return async (ctx: Context, next: NextFunction) => {\n      // 判断下有没有校验信息\n      if (!ctx.headers['authorization']) {\n        throw new httpError.UnauthorizedError();\n      }\n      // 从 header 上获取校验信息\n      const parts = ctx.get('authorization').trim().split(' ');\n\n      if (parts.length !== 2) {\n        throw new httpError.UnauthorizedError();\n      }\n\n      const [scheme, token] = parts;\n\n      if (/^Bearer$/i.test(scheme)) {\n        try {\n          //jwt.verify方法验证token是否有效\n          await this.jwtService.verify(token, {\n            complete: true,\n          });\n        } catch (error) {\n          //token过期 生成新的token\n          const newToken = getToken(user);\n          //将新token放入Authorization中返回给前端\n          ctx.set('Authorization', newToken);\n        }\n        await next();\n      }\n    };\n  }\n\n  // 配置忽略鉴权的路由地址\n  public match(ctx: Context): boolean {\n    const ignore = ctx.path.indexOf('/api/admin/login') !== -1;\n    return !ignore;\n  }\n}\n```\n\n然后在入口启用中间件即可。\n\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration, App, IMidwayContainer, IMidwayApplication} from '@midwayjs/core';\nimport * as jwt from '@midwayjs/jwt';\n\n@Configuration({\n  imports: [\n    // ...\n    jwt,\n  ],\n})\nexport class MainConfiguration {\n\n  @App()\n  app: IMidwayApplication;\n\n  async onReady(applicationContext: IMidwayContainer): Promise<void> {\n    // 添加中间件\n    this.app.useMiddleware([\n      // ...\n      JwtMiddleware,\n    ]);\n  }\n}\n```\n\n\n\n## 原始 JWT 对象\n\n可以通过导出的 `Jwt` 对象引用到原始实例上的对象和方法。\n\n```typescript\nimport { Jwt } from '@midwayjs/jwt';\n\n// Jwt.TokenExpiredError\n```\n","site/docs/extensions/kafka.md":"# Kafka\n\n在复杂系统的架构中，事件流是很重要的一环，包括从事件源中(数据库、传感器、移动设备等)以事件流的方式去实时捕获数据，持久化事件流方便检索，并实时和回顾操作处理响应事件流。\n\n应用于支付和金融交易、实施跟踪和监控汽车等行业信息流动、捕获分析物联网数据等等。\n\n\n在 Midway中，我们提供了订阅 Kafka 的能力，专门来满足用户的这类需求。\n\n相关信息：\n\n**订阅服务**\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ✅     |\n| 包含独立日志      | ✅     |\n\n\n\n## 基础概念\n\n\n分布式流处理平台\n* 发布订阅（流）信息\n* 容错（故障转移）存储信息（流），存储事件流\n* 在消息流发生的时候进行处理，处理事件流\n\n理解 Producer（生产者）\n\n* 发布消息到一个主题或多个 topic (主题)。\n\n理解 Consumer（主题消费者）\n* 订阅一个或者多个 topic,并处理产生的信息。\n\n理解 Stream API\n* 充当一个流处理器，从 1 个或多个 topic 消费输入流，并生产一个输出流到1个或多个输出 topic，有效地将输入流转换到输出流。\n\n理解 Broker\n* 已发布的消息保存在一组服务器中，称之为 Kafka 集群。集群中的每一个服务器都是一个代理（Broker）。 消费者可以订阅一个或多个主题（topic），并从Broker拉数据，从而消费这些已发布的消息。\n\n\n![image.png](https://kafka.apache.org/images/streams-and-tables-p1_p4.png)\n\n:::tip\n从 v3.19 开始，Kafka 组件做了一次重构，Kafka 组件的配置、使用方法和之前都有较大差异，原有使用方式兼容，但是文档不再保留。\n:::\n\n\n## 安装依赖\n\n\n安装 `@midwayjs/kafka` 模块。\n\n```bash\n$ npm i @midwayjs/kafka --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/kafka\": \"^4.0.0\",\n    // ...\n  }\n}\n```\n\n## 开启组件\n\n`@midwayjs/kafka` 可以作为独立主框架使用。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as kafka from '@midwayjs/kafka';\n\n@Configuration({\n  imports: [\n    kafka\n  ],\n  // ...\n})\nexport class MainConfiguration {\n  async onReady() {\n        // ...\n  }\n}\n```\n\n也可以附加在其他的主框架下，比如 `@midwayjs/koa` 。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as kafka from '@midwayjs/kafka';\n\n@Configuration({\n  imports: [\n    koa,\n    kafka\n  ],\n  // ...\n})\nexport class MainConfiguration {\n  async onReady() {\n        // ...\n  }\n}\n```\n\n由于 Kafka 分为 **消费者（Consumer）** 和 **生产者（Producer）** 两部分，两个可以独立使用，我们将分别介绍。\n\n## 消费者（Consumer）\n\n### 目录结构\n\n\n我们一般把消费者放在 consumer 目录。比如 `src/consumer/user.consumer.ts`  。\n```\n➜  my_midway_app tree\n.\n├── src\n│   ├── consumer\n│   │   └── user.consumer.ts\n│   ├── interface.ts\n│   └── service\n│       └── user.service.ts\n├── test\n├── package.json\n└── tsconfig.json\n```\n\n\n### 基础配置\n\n通过 `consumer` 字段和 `@KafkaConsumer` 装饰器，我们可以配置多个消费者。\n\n比如，下面的 `sub1` 和 `sub2` 就是两个不同的消费者。\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    consumer: {\n      sub1: {\n        // ...\n      },\n      sub2: {\n        // ...\n      },\n    }\n  }\n}\n```\n\n最简单的消费者配置需要几个字段，Kafka 的连接配置、消费者配置以及订阅配置。\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    consumer: {\n      sub1: {\n        connectionOptions: {\n          // ...\n        },\n        consumerOptions: {\n          // ...\n        },\n        subscribeOptions: {\n          // ...\n        },\n      },\n    }\n  }\n}\n```\n\n比如：\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    consumer: {\n      sub1: {\n        connectionOptions: {\n          clientId: 'my-app',\n          brokers: ['localhost:9092'],\n        },\n        consumerOptions: {\n          groupId: 'groupId-test-1',\n        },\n        subscribeOptions: {\n          topics: ['topic-test-1'],\n        }\n      },\n    }\n  }\n}\n```\n\n完整可配置参数包括：\n\n- `connectionOptions`：Kafka 的连接配置，即 `new Kafka(consumerOptions)` 的参数\n- `consumerOptions`：Kafka 的消费者配置，即 `kafka.consumer(consumerOptions)` 的参数\n- `subscribeOptions`：Kafka 的订阅配置，即 `consumer.subscribe(subscribeOptions)` 的参数\n- `consumerRunConfig`：消费者运行配置，即 `consumer.run(consumerRunConfig)` 的参数\n\n这些参数的详细说明，可以参考 [KafkaJS Consumer](https://kafka.js.org/docs/consuming) 文档。\n\n### 复用 Kafka 实例\n\n如果如果需要复用 Kafka 实例，可以通过 `kafkaInstanceRef` 字段来指定。\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    consumer: {\n      sub1: {\n        connectionOptions: {\n          clientId: 'my-app',\n          brokers: ['localhost:9092'],\n        },\n        consumerOptions: {\n          groupId: 'groupId-test-1',\n        },\n        subscribeOptions: {\n          topics: ['topic-test-1'],\n        }\n      },\n      sub2: {\n        kafkaInstanceRef: 'sub1',\n        consumerOptions: {\n          groupId: 'groupId-test-2',\n        },\n        subscribeOptions: {\n          topics: ['topic-test-2'],\n        }\n      }\n    }\n  }\n}\n```\n\n注意，上述的 `sub1` 和 `sub2` 是两个不同的消费者，但是它们共享同一个 Kafka 实例，且 `sub2` 的 `groupId` 需要和 `sub1` 不同。\n\n用 Kafka SDK 写法类似如下：\n\n```typescript\nconst kafka = new Kafka({\n  clientId: 'my-app',\n  brokers: ['localhost:9092'],\n});\n\nconst consumer1 = kafka.consumer({ groupId: 'groupId-test-1' });\nconst consumer2 = kafka.consumer({ groupId: 'groupId-test-2' });\n```\n\n### 消费者实现\n\n我们可以在目录中提供一个标准的消费者实现，比如 `src/consumer/sub1.consumer.ts`。\n\n```typescript\n// src/consumer/sub1.consumer.ts\nimport { KafkaConsumer, IKafkaConsumer, EachMessagePayload } from '@midwayjs/kafka';\n\n@KafkaConsumer('sub1')\nclass Sub1Consumer implements IKafkaConsumer {\n  async eachMessage(payload: EachMessagePayload) {\n    // ...\n  }\n}\n```\n\n`sub1` 是消费者名称，使用的是配置中的 `sub1` 消费者。\n\n也可以实现 `eachBatch` 方法，处理批量消息。\n\n```typescript\n// src/consumer/sub1.consumer.ts\nimport { KafkaConsumer, IKafkaConsumer, EachBatchPayload } from '@midwayjs/kafka';\n\n@KafkaConsumer('sub1')\nclass Sub1Consumer implements IKafkaConsumer {\n  async eachBatch(payload: EachBatchPayload) {\n    // ...\n  }\n}\n```\n\n\n### 消息上下文\n\n\n和其他消息订阅机制一样，消息本身通过 `Context` 字段来传递。\n\n```typescript\n// src/consumer/sub1.consumer.ts\nimport { KafkaConsumer, IKafkaConsumer, EachMessagePayload, Context } from '@midwayjs/kafka';\nimport { Inject } from '@midwayjs/core';\n\n@KafkaConsumer('sub1')\nclass Sub1Consumer implements IKafkaConsumer {\n\n  @Inject()\n  ctx: Context;\n\n  async eachMessage(payload: EachMessagePayload) {\n    // ...\n  }\n}\n```\n\n`Context` 字段包括几个属性：\n\n| 属性        | 类型                           | 描述             |\n| ----------- | ------------------------------ | ---------------- |\n| ctx.payload   | EachMessagePayload, EachBatchPayload | 消息内容         |\n| ctx.consumer  | Consumer             | 消费者实例         |\n\n\n你可以通过 `ctx.consumer` 来调用 Kafka 的 API，比如 `ctx.consumer.commitOffsets` 来手动提交偏移量或者 `ctx.consumer.pause` 来暂停消费。\n\n\n## 生产者（Producer）\n\n### 基础配置\n\n服务生产者也需要创建实例，配置本身使用了 [服务工厂](/docs/service_factory) 的设计模式。\n\n配置如下：\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    producer: {\n      clients: {\n        pub1: {\n          // ...\n        },\n        pub2: {\n          // ...\n        }\n      }\n    }\n  }\n}\n```\n\n每个 Producer 实例的配置，同样包括 `connectionOptions` 和 `producerOptions`。\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    producer: {\n      clients: {\n        pub1: {\n          connectionOptions: {\n            clientId: 'my-app',\n            brokers: ['localhost:9092'],\n          },\n          producerOptions: {\n            // ...\n          }\n        }\n      }\n    }\n  }\n}\n```\n\n具体参数可以参考 [KafkaJS Producer](https://kafka.js.org/docs/producing) 文档。\n\n此外，由于 Kafka Consumer 和 Producer 都可以从同一个 Kafka 实例创建，所以它们可以复用同一个 Kafka 实例。\n\nProducer 后于 Consumer 创建，也同样可以使用 `kafkaInstanceRef` 字段来复用 Kafka 实例。\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    consumer: {\n      sub1: {\n        connectionOptions: {\n          clientId: 'my-app',\n          brokers: ['localhost:9092'],\n        },\n      }\n    },\n    producer: {\n      clients: {\n        pub1: {\n          kafkaInstanceRef: 'sub1',\n        }\n      }\n    }\n  }\n}\n```\n\n### 使用 Producer\n\nProducer 不存在默认实例，由于使用了服务工厂的设计模式，所以可以通过 `@InjectClient()` 来注入。\n\n\n```typescript\n// src/service/user.service.ts\nimport { Provide, InjectClient } from '@midwayjs/core';\nimport { KafkaProducerFactory, Producer } from '@midwayjs/kafka';\n\n@Provide()\nexport class UserService {\n  \n  @InjectClient(KafkaProducerFactory, 'pub1')\n  producer: Producer;\n  \n  async invoke() {\n    await this.producer.send({\n      topic: 'topic-test-1',\n      messages: [{ key: 'message-key1', value: 'hello consumer 11 !' }],\n    });\n  }\n}\n```\n\n## Admin\n\nKafka 的 Admin 功能，可以用来创建、删除、查看主题，查看配置和 ACL 等。\n\n### 基础配置\n\n和 Producer 类似，Admin 也使用了服务工厂的设计模式。\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    admin: {\n      clients: {\n        admin1: {\n          // ...\n        }\n      }\n    }\n  }\n}\n```\n\n同样的，Admin 也可以复用 Kafka 实例。\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    consumer: {\n      sub1: {\n        connectionOptions: {\n          clientId: 'my-app',\n          brokers: ['localhost:9092'],\n        },\n      }\n    },\n    admin: {\n      clients: {\n        admin1: {\n          kafkaInstanceRef: 'sub1',\n        }\n      }\n    }\n  }\n}\n```\n\n### 使用 Admin\n\nAdmin 不存在默认实例，由于使用了服务工厂的设计模式，所以可以通过 `@InjectClient()` 来注入。\n\n```typescript\n// src/service/admin.service.ts\nimport { Provide, InjectClient } from '@midwayjs/core';\nimport { KafkaAdminFactory, Admin } from '@midwayjs/kafka';\n\n@Provide()\nexport class AdminService {\n  \n  @InjectClient(KafkaAdminFactory, 'admin1')\n  admin: Admin;\n}\n```\n\n更多的 Admin 使用方法，可以参考 [KafkaJS Admin](https://kafka.js.org/docs/admin) 文档。\n\n\n## 组件日志\n\nKafka 组件默认使用 `kafkaLogger` 日志，默认会将 `ctx.logger` 记录在 `midway-kafka.log`。\n\n你可以通过配置修改。\n\n```typescript\n// src/config/config.default\nexport default {\n  midwayLogger: {\n    clients: {\n      kafkaLogger: {\n        fileLogName: 'midway-kafka.log',\n      },\n    },\n  },\n}\n```\n\n这个日志的输出格式，我们也可以单独配置。\n\n```typescript\nexport default {\n  kafka: {\n    // ...\n    contextLoggerFormat: info => {\n      const { jobId, from } = info.ctx;\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} ${info.message}`;\n    },\n  }\n}\n```\n\n\n## 获取 KafkaJS 模块\n\nKafkaJS 模块，可以通过 `@midwayjs/kafka` 的 `KafkaJS` 字段来获取。\n\n```typescript\nimport { KafkaJS } from '@midwayjs/kafka';\n\nconst { ConfigResourceTypes } = KafkaJS;\n// ...\n```\n\n## 关于分区的警告\n\n如果你使用的是 KafkaJS 的 v2.0.0 版本，你可能会看到如下的警告：\n\n```\n2024-11-04 23:47:28.228 WARN 31729 KafkaJS v2.0.0 switched default partitioner. To retain the same partitioning behavior as in previous versions, create the producer with the option \"createPartitioner: Partitioners.LegacyPartitioner\". See the migration guide at https://kafka.js.org/docs/migration-guide-v2.0.0#producer-new-default-partitioner for details. Silence this warning by setting the environment variable \"KAFKAJS_NO_PARTITIONER_WARNING=1\" { timestamp: '2024-11-04T15:47:28.228Z', logger: 'kafkajs' }\n```\n\n这个警告是由于 KafkaJS 的 v2.0.0 版本默认使用了新的分区器，如果接受新的分区器行为，但想要关闭这个警告消息，可以通过设置环境变量 `KAFKAJS_NO_PARTITIONER_WARNING=1` 来消除这个警告。\n\n或者显示声明分区器。\n\n```typescript\n// src/config/config.default\nimport { KafkaJS } from '@midwayjs/kafka';\nconst { Partitioners } = KafkaJS;\n\nexport default {\n  kafka: {\n    producer: {\n      clients: {\n        pub1: {\n          // ...\n          producerOptions: {\n            createPartitioner: Partitioners.DefaultPartitioner,\n            // ...\n            createPartitioner: Partitioners.LegacyPartitioner,\n          },\n        },\n      },\n    },\n  }\n}\n```\n\n建议你查看 KafkaJS v2.0.0 的 [迁移指南](https://kafka.js.org/docs/migration-guide-v2.0.0#producer-new-default-partitioner) 了解更多细节。\n\n\n\n## 参考文档\n\n- [KafkaJS](https://kafka.js.org/docs/introduction)\n- [apache kafka官网](https://kafka.apache.org/intro)\n","site/docs/extensions/koa.md":"# Koa\n\nKoa 是一个非常轻量易用的 Web 框架。本章节内容，主要介绍在 Midway 中如何使用 Koa 作为上层框架，并使用自身的能力。\n\nMidway 默认的示例都是基于该包。\n\n`@midwayjs/koa` 包默认使用 `koa@2` 以及集成了 `@koa/router` 作为路由基础能力，并默认内置了 `session` 和 `body-parser` 功能。\n\n| 描述           |      |\n| -------------- | ---- |\n| 包含独立主框架 | ✅    |\n| 包含独立日志   | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/koa@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/koa\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n也可以直接使用脚手架创建示例。\n\n```bash\n# npm v6\n$ npm init midway --type=koa-v4 my_project\n\n# npm v7\n$ npm init midway -- --type=koa-v4 my_project\n```\n\n\n\n## 开启组件\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [koa],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: koa.Application;\n\n  async onReady() {\n\t\t// ...\n  }\n}\n\n```\n\n\n\n## BodyParser\n\n`@midwayjs/koa` 自带  `bodyParser` 功能，默认会解析 `Post` 请求，自动识别 `json` 和 `form` 类型。\n\n如需 text 或者 xml，可以自行配置。\n\n默认的大小限制为 `1mb`，可以单独对每项配置大小。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  bodyParser: {\n    enableTypes: ['json', 'form', 'text', 'xml'],\n    formLimit: '1mb',\n    jsonLimit: '1mb',\n    textLimit: '1mb',\n    xmlLimit: '1mb',\n  },\n}\n```\n\n注意，使用 Postman 做 Post 请求时的类型选择：\n\n![postman](https://img.alicdn.com/imgextra/i4/O1CN01QCdTsN1S347SuzZU5_!!6000000002190-2-tps-1017-690.png)\n\n\n关闭 bodyParser 中间件。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  bodyParser: {\n    enable: false,\n    // ...\n  },\n}\n```\n\n\n## Cookie 和 Session\n\n`@midwayjs/koa` 默认封装了 `cookies` 解析和 `Session` 的支持，可以查看 [Cookies 和 Session](../cookie_session)。\n\n\n\n## 扩展 Context\n\n在一些场景下，需要对 Context 做扩展。\n\n如果希望挂在一些临时的请求相关的对象数据，可以使用 `ctx.setAttr(key, value)` API 来实现，比如组件里自用的数据。\n\n如果实在有扩展 Context 的诉求，可以使用 koa 自带的 API。\n\n比如，我们在 `configuration.ts` 中做扩展提供了一个 `render()` 方法。\n\n```typescript\nimport { App, Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\n\n@Configuration({\n\t// ...\n})\nexport class MainConfiguration {\n  @App()\n  app: koa.Application;\n\n  async onReady(container) {\n    Object.defineProperties(app.context, {\n      render: {\n        value: async function (...args) {\n          // ...\n        },\n      },\n    });\n  }\n}\n```\n\n但是这样做无法直接让 Context 包含 Typescript 定义，需要额外增加定义，请参考 [扩展上下文定义](../context_definition)。\n\n\n\n## 获取 Http Server\n\n在一些特殊情况下，你需要获取到原始的 Http Server，我们可以在服务器启动后获取。\n\n```typescript\nimport { App, Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\n\n@Configuration({\n\t// ...\n})\nexport class MainConfiguration {\n  @Inject()\n  framework: koa.Framework;\n\n  async onServerReady(container) {\n    const server = this.framework.getServer();\n    // ...\n  }\n}\n```\n\n\n\n## State 类型定义\n\n在 koa 的 Context 中有一个特殊的 State 属性，通过和 Context 类似的方式可以扩展 State 定义。\n\n```typescript\n// src/interface.ts\n\ndeclare module '@midwayjs/koa/dist/interface' {\n  interface Context {\n    abc: string;\n  }\n\n  interface State {\n    bbb: string;\n    ccc: number;\n  }\n}\n```\n\n\n\n## 获取端口\n\n通过 framework 或者 app 上的 `getPort()` 方法可以获取端口。\n\n```typescript\nimport { App, Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\n\n@Configuration({\n  imports: [\n    koa,\n    // ...\n  ]\n\t// ...\n})\nexport class MainConfiguration {\n  @Inject()\n  framework: koa.Framework;\n\n  async onServerReady(container, mainApp: koa.Application) {\n    const port = this.framework.getPort();\n    const port = this.mainApp.getPort();\n    // ...\n  }\n}\n```\n\n\n\n## 配置\n\n\n\n### 默认配置\n\n`@midwayjs/koa` 的配置样例如下：\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  koa: {\n    port: 7001,\n  },\n}\n```\n\n所有属性描述如下：\n\n| 属性         | 类型                                         | 描述                                                    |\n| ------------ |--------------------------------------------| ------------------------------------------------------- |\n| port         | number                                     | 可选，启动的端口                                        |\n| globalPrefix | string                                     | 可选，全局的 http 前缀                                  |\n| keys         | string[]                                   | 可选，Cookies 签名，如果上层未写 keys，也可以在这里设置 |\n| hostname     | string                                     | 可选，监听的 hostname，默认 127.1                       |\n| key          | string \\| Buffer \\| Array\\Buffer\\|Object>  | 可选，Https key，服务端私钥                             |\n| cert         | string \\| Buffer \\| Array\\<Buffer\\|Object> | 可选，Https cert，服务端证书                            |\n| ca           | string \\| Buffer \\| Array\\<Buffer\\|Object> | 可选，Https ca                                          |\n| http2        | boolean                                    | 可选，http2 支持，默认 false                            |\n| proxy        | boolean                                    | 可选，是否开启代理，如果为 true 则对于 request 请求中的 ip 优先从 Header 字段中 X-Forwarded-For 获取，默认 false           |\n| subdomainOffset        | number                                     | 可选，子域名的偏移量，默认 2                            |\n| proxyIpHeader        | string                                     | 可选，获取代理 ip 的字段名，默认为 X-Forwarded-For |\n| maxIpsCount        | number                                     | 可选，获取的 ips 最大数量，默认为 0（全部返回）|\n| serverTimeout | number                                     | 可选，服务端超时配置，默认为 2 \\* 60 \\* 1000（2 分钟），单位毫秒 |\n| serverOptions | Record\\<string, any>                       | 可选，http Server [选项](https://nodejs.org/docs/latest/api/http.html#httpcreateserveroptions-requestlistener) |\n\n\n\n### 修改端口\n\n默认情况下，我们在 `config.default` 提供了 `7001` 的默认端口参数，修改它就可以修改 koa http 服务的默认端口。\n\n比如我们修改为 `6001`：\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  koa: {\n    port: 6001,\n  },\n}\n```\n\n默认情况下，单测环境由于需要 supertest 来启动端口，我们的 port 配置为 `null`。\n\n```typescript\n// src/config/config.unittest\nexport default {\n  // ...\n  koa: {\n    port: null,\n  },\n}\n```\n\n此外，也可以通过 `midway-bin dev --ts --port=6001` 的方式来临时修改端口，此方法会覆盖配置中的端口。\n\n\n\n### 全局前缀\n\n此功能请参考 [全局前缀](../controller#全局路由前缀)。\n\n\n\n### 反向代理配置\n\n如果使用了 Nginx 等反向代理，请开启 `proxy` 配置。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  koa: {\n    proxy: true,\n  },\n}\n```\n\n默认使用 `X-Forwarded-For` Header，如果代理配置不同，请自行配置不同的 Header。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  koa: {\n    proxy: true,\n    proxyIpHeader: 'X-Forwarded-Host'\n  },\n}\n```\n\n\n\n\n\n### Https 配置\n\n在大多数的情况，请尽可能使用外部代理的方式来完成 Https 的实现，比如 Nginx。\n\n在一些特殊场景下，你可以通过配置 SSL 证书（TLS 证书）的方式，来直接开启 Https。\n\n首先，你需要提前准备好证书文件，比如 `ssl.key` 和 `ssl.pem`，key 为服务端私钥，pem 为对应的证书。\n\n然后配置即可。\n\n```typescript\n// src/config/config.default\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  koa: {\n    key: join(__dirname, '../ssl/ssl.key'),\n    cert: join(__dirname, '../ssl/ssl.pem'),\n  },\n}\n```\n\n\n\n### favicon 设置\n\n默认情况下，浏览器会发起一个 `favicon.ico` 的请求。\n\n框架提供了一个默认中间件，用来处理该请求，你可以指定一个 `favicon` 的 Buffer。\n\n```typescript\n// src/config/config.default\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  siteFile: {\n    favicon: readFileSync(join(__dirname, '../static/fav.ico')),\n  },\n}\n```\n\n如果开启了 `@midwayjs/static-file`  组件，那么会优先使用组件的静态文件托管。\n\n关闭中间件。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  siteFile: {\n    enable: false,\n    // ...\n  },\n}\n```\n\n### 修改上下文日志\n\n可以单独修改 koa 框架的上下文日志。\n\n```typescript\nexport default {\n  koa: {\n    contextLoggerFormat: info => {\n      const ctx = info.ctx;\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} [${ctx.userId} - ${Date.now() - ctx.startTime}ms ${ctx.method}] ${info.message}`;\n    }\n    // ...\n  },\n};\n```\n\n\n### Query 数组解析\n\n默认情况下，koa 使用 `querystring` 解析 query 参数，当碰到数组时，会将数组的数据拆开。\n\n比如：\n\n```\nGET /query?a[0]=1&a[1]=2\n```\n\n拿到的结果是：\n\n```json\n{\n    \"a[0]\": 1,\n    \"a[1]\": 2,\n}\n```\n\n框架提供了一些参数来处理这种情况。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  koa: {\n    queryParseMode: 'extended',\n    // ...\n  },\n}\n```\n\n`queryParseMode` 参数可以选择 `extended`、 `strict`、`first` 三种值。\n\n 当 `queryParseMode` 有值时，会使用 `qs` 模块处理 query，效果同 `koa-qs` 模块。\n\n当请求参数为 `/query?a=1&b=2&a=3&c[0]=1&c[1]=2'` 时。\n\n默认效果（使用 `querystring`）\n\n```JSON\n{\n  \"a\": [\"1\", \"3\" ],\n  \"b\": \"2\",\n  \"c[0]\": \"1\",\n  \"c[1]\": \"2\"\n}\n```\n\n `extended` 效果\n\n```JSON\n{\n  \"a\": [\"1\", \"3\" ],\n  \"b\": \"2\",\n  \"c\": [\"1\", \"2\"]\n}\n```\n\n `strict` 效果\n\n```JSON\n{\n  \"a\": [\"1\", \"3\" ],\n  \"b\": [\"2\"],\n  \"c\": [\"1\", \"2\"]\n}\n```\n\n `first` 效果\n\n```JSON\n{\n  \"a\": \"1\",\n  \"b\": \"2\",\n  \"c\": \"1\"\n}\n```\n\n\n### 超时配置\n\nRequestTiemout 和 ServerTimeout 是两种不同的超时情况。\n\n- `serverTimeout`：用于设置服务器接收到请求后，等待客户端发送数据的超时时间。如果在该时间内客户端没有发送任何数据，则服务器将关闭连接。此超时适用于整个请求-响应周期，包括请求头、请求主体以及响应。\n- `requestTimeout`：用于设置服务器等待客户端发送完整请求的超时时间。这个超时是针对请求头和请求主体的，服务器将在该时间内等待客户端发送完整的请求。如果在超时时间内没有收到完整的请求，则服务器将中止该请求。\n\n默认情况下，`serverTimeout` 为 0，不会触发超时。\n\n如有需求，可以通过配置修改，单位毫秒。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  koa: {\n    serverTimeout: 100_000\n  },\n}\n```\n\n如果程序出现 `ERR_HTTP_REQUEST_TIMEOUT` 这个错误，说明是触发了 `requestTimeout`，默认为 `300_000` （五分钟），单位毫秒，可以通过以下配置修改。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  koa: {\n    serverOptions: {\n      requestTimeout: 600_000\n    }\n  },\n}\n```\n\n","site/docs/extensions/mcp.md":"# MCP (Model Context Protocol)\n\nMCP (Model Context Protocol) 是由 Anthropic 开发的开放标准协议，用于将 AI 模型与外部数据源和工具安全连接。它允许 AI 应用程序与各种服务进行语义集成，为 AI 模型提供了获取实时信息、执行操作和访问资源的标准化方式。\n\n![MCP Architecture](https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/mcp-simple-diagram.png?w=1650&fit=max&auto=format&n=4ZXF1PrDkEaJvXpn&q=85&s=37d50a5429a8f662c1beb564b4df8e65)\n\nMidway 提供了对 MCP 的完整支持和封装，能够简单快速地创建 MCP 服务器。本章节演示了如何在 Midway 体系下，提供 MCP 服务的方法。\n\nMidway 当前采用了最新的 [MCP SDK](https://github.com/modelcontextprotocol/typescript-sdk) 进行开发，并提供了装饰器和框架集成，用于快速发布 MCP 服务。\n\n相关信息：\n\n**提供服务**\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n**传输类型支持**\n\n| 传输类型      | 描述                                    | 支持状态 |\n| ------------- | --------------------------------------- | -------- |\n| stdio         | 标准输入输出传输，适用于 CLI 应用       | ✅        |\n| stream-http   | HTTP 流传输，推荐的现代传输方式         | ✅        |\n| sse           | 服务器发送事件传输（已弃用，向下兼容）  | ✅        |\n\n## 安装依赖\n\n在现有项目中安装 MCP 的依赖。\n\n```bash\n$ npm i @midwayjs/mcp@4 --save\n$ npm i @modelcontextprotocol/sdk zod --save\n```\n\n\n如果使用 `sse` 或 `stream-http` 传输类型，还需要安装 HTTP 框架组件（选择其中一个）：\n\n```bash\n# Express 框架\n$ npm i @midwayjs/express@4 --save\n\n# 或者 Koa 框架\n$ npm i @midwayjs/koa@4 --save\n\n# 或者 Egg.js 框架\n$ npm i @midwayjs/web@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/mcp\": \"^4.0.0\",\n    \"@modelcontextprotocol/sdk\": \"^1.19.0\",\n    \"zod\": \"^3.24.1\",\n    \"@midwayjs/express\": \"^4.0.0\"\n  }\n}\n```\n\n:::tip\n如果使用 `sse` 或 `stream-http` 传输类型，您需要选择并安装一个 HTTP 框架：\n- `@midwayjs/express` - Express 框架\n- `@midwayjs/koa` - Koa 框架  \n- `@midwayjs/web` - Egg.js 框架\n\n如果只使用 `stdio` 传输类型，则不需要安装 HTTP 框架组件。\n:::\n\n## 开启组件\n\n在 `src/configuration.ts` 中导入 MCP 组件：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as mcp from '@midwayjs/mcp';\n// 如果使用 sse/stream-http 传输类型，需要导入 HTTP 框架\nimport * as express from '@midwayjs/express';\n\n@Configuration({\n  imports: [\n    express, // HTTP 框架（sse/stream-http 传输类型需要）\n    mcp      // 导入 MCP 组件\n  ],\n  // ...\n})\nexport class MainConfiguration {}\n```\n\n:::info\n**传输类型依赖说明**：\n- `stdio` 传输：只需要导入 `@midwayjs/mcp`\n- `sse` 或 `stream-http` 传输：需要同时导入 HTTP 框架组件（如 `@midwayjs/express`）\n:::\n\n## 配置\n\n在 `config.default.ts` 中配置 MCP 服务器信息：\n\n```typescript\nexport default {\n  // 如果使用 sse/stream-http 传输类型，需要配置 HTTP 框架\n  express: {\n    port: 3000,\n    keys: ['mcp-secret-key']\n  },\n  // 或者如果使用 Koa\n  // koa: {\n  //   port: 3000,\n  //   keys: ['mcp-secret-key']\n  // },\n  \n  mcp: {\n    serverInfo: {\n      name: 'my-mcp-server',\n      version: '1.0.0',\n    },\n    // 传输类型：stdio | stream-http | sse(已弃用)\n    transportType: 'stream-http',\n    // 可选：自定义端点路径\n    endpoints: {\n      streamHttp: '/mcp',     // StreamHTTP 端点\n      sse: '/sse',            // SSE 端点（向下兼容）\n      messages: '/messages'   // 消息端点\n    }\n  }\n}\n```\n\n:::warning\n- 当使用 `sse` 或 `stream-http` 传输类型时，**必须配置 HTTP 框架的端口和密钥**：\n- 如果只使用 `stdio` 传输类型，则不需要 HTTP 框架配置\n:::\n\n### 传输类型说明\n\nMCP 支持多种传输类型：\n\n- **stdio**: 适用于命令行工具和脚本，通过标准输入输出通信\n- **stream-http**: 推荐的 HTTP 传输方式，支持会话管理和并发请求\n- **sse**: 传统的服务器发送事件传输（已弃用，但保持向下兼容）\n\n:::tip\n推荐使用 `stream-http` 传输类型，它提供了更好的性能和会话管理能力，同时兼容传统的 SSE 客户端。\n:::\n\n### 默认端点配置\n\n当使用 `sse` 或 `stream-http` 传输类型时，Midway MCP 框架会自动配置以下默认端点：\n\n| 端点类型 | 默认路径 | 用途 | 支持的传输类型 |\n|---------|---------|------|----------------|\n| StreamHTTP | `/mcp` | 主要的 MCP 通信端点，推荐使用 | stream-http |\n| SSE | `/sse` | 传统 SSE 客户端兼容端点 | sse, stream-http (向下兼容) |\n| Messages | `/messages` | 消息处理端点 | stream-http, sse |\n\n**自定义端点配置**\n\n您可以通过 `endpoints` 配置项自定义这些端点路径：\n\n```typescript\nexport default {\n  mcp: {\n    serverInfo: {\n      name: 'my-mcp-server',\n      version: '1.0.0',\n    },\n    transportType: 'stream-http',\n    endpoints: {\n      streamHttp: '/api/mcp',        // 自定义 StreamHTTP 端点\n      sse: '/api/events',            // 自定义 SSE 端点\n      messages: '/api/messages'      // 自定义消息端点\n    }\n  }\n}\n```\n\n**端点访问示例**\n\n假设服务器运行在 `http://localhost:3000`，默认端点访问地址为：\n\n- StreamHTTP 客户端：`http://localhost:3000/mcp`\n- SSE 客户端：`http://localhost:3000/sse`\n- 消息端点：`http://localhost:3000/messages`\n\n:::info\n**向下兼容说明**：当服务器配置为 `stream-http` 传输类型时，传统的 SSE 客户端仍然可以通过 `/sse` 端点正常连接和通信。\n:::\n\n### 日志配置\n\nMCP 组件默认志输出，可以通过 `mcpLogger` 进一步配置日志详情。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  midwayLogger: {\n    mcpLogger: {\n      // ...\n    }\n  },\n}\n```\n\n\n\n## 使用方式\n\n### 工具（Tools）\n\n使用 `@Tool` 装饰器创建 MCP 工具：\n\n```typescript\nimport { Tool, IMcpTool, ToolConfig } from '@midwayjs/mcp';\nimport { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport { z } from 'zod';\n\nconst weatherConfig: ToolConfig<{ city: z.ZodString }> = {\n  description: 'Get weather information for a city',\n  inputSchema: {\n    city: z.string().describe('The city name')\n  }\n};\n\n@Tool('get_weather', weatherConfig)\nexport class WeatherTool implements IMcpTool {\n  async execute(args: { city: string }): Promise<CallToolResult> {\n    // 模拟获取天气数据\n    const weather = await this.getWeatherData(args.city);\n    \n    return {\n      content: [\n        {\n          type: 'text',\n          text: `Weather in ${args.city}: ${weather.temperature}°C, ${weather.condition}`\n        }\n      ]\n    };\n  }\n\n  private async getWeatherData(city: string) {\n    // 实际的天气 API 调用\n    return {\n      temperature: 22,\n      condition: 'Sunny'\n    };\n  }\n}\n```\n\n### 提示（Prompts）\n\n使用 `@Prompt` 装饰器创建 MCP 提示：\n\n```typescript\nimport { Prompt, IMcpPrompt, PromptConfig } from '@midwayjs/mcp';\nimport { GetPromptResult } from '@modelcontextprotocol/sdk/types.js';\nimport { z } from 'zod';\n\nconst promptConfig: PromptConfig<{ \n  topic: z.ZodString; \n  style: z.ZodOptional<z.ZodString>; \n}> = {\n  description: 'Generate content on a specific topic',\n  argsSchema: {\n    topic: z.string().describe('The topic to write about'),\n    style: z.string().optional().describe('Writing style (formal, casual, etc.)')\n  }\n};\n\n@Prompt('content_generator', promptConfig)\nexport class ContentPrompt implements IMcpPrompt {\n  async generate(args: { topic: string; style?: string }): Promise<GetPromptResult> {\n    const style = args.style || 'professional';\n    \n    return {\n      messages: [\n        {\n          role: 'user',\n          content: {\n            type: 'text',\n            text: `Write a ${style} article about: ${args.topic}`\n          }\n        }\n      ]\n    };\n  }\n}\n```\n\n### 资源（Resources）\n\n使用 `@Resource` 装饰器创建 MCP 资源：\n\n```typescript\nimport { Resource, IMcpResource, ResourceConfig } from '@midwayjs/mcp';\nimport { ReadResourceResult } from '@modelcontextprotocol/sdk/types.js';\n\nconst resourceConfig: ResourceConfig = {\n  description: 'Access user database information',\n  uri: 'database://users/{id}',\n  mimeType: 'application/json'\n};\n\n@Resource('user_data', resourceConfig)\nexport class UserResource implements IMcpResource {\n  async handle(uri: URL): Promise<ReadResourceResult> {\n    const userId = this.extractUserIdFromUri(uri);\n    const userData = await this.getUserData(userId);\n    \n    return {\n      contents: [\n        {\n          uri: uri.toString(),\n          mimeType: 'application/json',\n          text: JSON.stringify(userData)\n        }\n      ]\n    };\n  }\n\n  private extractUserIdFromUri(uri: URL): string {\n    const path = uri.pathname;\n    return path.split('/').pop() || '';\n  }\n\n  private async getUserData(userId: string) {\n    // 实际的数据库查询\n    return {\n      id: userId,\n      name: 'John Doe',\n      email: 'john@example.com'\n    };\n  }\n}\n```\n\n\n\n## 安全认证\n\n### 内置的 JWT 助手\n\nMidway MCP 框架提供了内置的 JWT 认证助手，可以自动处理 JWT token 验证并将认证信息传递给 MCP 工具、提示和资源。\n\n在配置中启用 JWT 认证助手：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // JWT 配置\n  jwt: {\n    secret: 'your-jwt-secret-key',\n    sign: {\n      expiresIn: '1h'\n    }\n  },\n\n  mcp: {\n    serverInfo: {\n      name: 'my-mcp-server',\n      version: '1.0.0',\n    },\n    transportType: 'stream-http',\n    // 启用内置 JWT 认证助手\n    enableJwtAuthHelper: true,\n  }\n}\n```\n\n认证助手是一个中间件，能将 jwt 的 payload 信息转化为 MCP SDK 中定义的 AuthInfo。\n\n可以通过 `ctx.authInfo` 使用认证信息。\n\n```typescript\nimport { Tool, IMcpTool, ToolConfig, Context } from '@midwayjs/mcp';\nimport { Inject } from '@midwayjs/core';\nimport { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\n@Tool('secure_operation', { description: 'A tool that requires authentication' })\nexport class SecureTool implements IMcpTool {\n  @Inject()\n  ctx: Context;\n\n  async execute(args: any): Promise<CallToolResult> {\n    const authInfo = this.ctx.authInfo;\n    // ...\n  }\n}\n```\n\nJWT payload 应包含以下标准字段：\n\n```json\n{\n  \"aud\": \"client-id\",           \t\t\t\t\t\t// 客户端 ID，映射到 authInfo.clientId\n  \"scope\": \"mcp:read mcp:write\",  \t\t\t\t\t// 权限范围，映射到 authInfo.scopes\n  \"exp\": 1234567890,           \t\t\t\t\t\t\t// 过期时间，映射到 authInfo.expiresAt\n  \"resource\": \"https://api.example.com\", \t\t// 资源 URL，映射到 authInfo.resource\n  \"sub\": \"user-123\",           \t\t\t\t\t\t\t// 用户 ID\n  \"iss\": \"https://auth.example.com\", \t\t\t\t// 签发者\n  \"extra\": {\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// 其他自定义字段会放入 authInfo.extra\n    \"username\": \"testuser\",\n    \"role\": \"admin\"\n  }      \t\t\t\t\t\t\t\t\t\t\n}\n```\n\n\n\n\n\n### 非标数据转换\n\n如果传入的 jwt 结构为非标，需要在配置中自行提供转换函数。\n\n```typescript\n// src/config/config.default.ts\nimport { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';\n\nexport default {\n  // JWT 配置\n  jwt: {\n    secret: 'your-jwt-secret-key',\n    sign: {\n      expiresIn: '1h'\n    }\n  },\n\n  mcp: {\n    serverInfo: {\n      name: 'my-mcp-server',\n      version: '1.0.0',\n    },\n    transportType: 'stream-http',\n    // 启用内置 JWT 认证助手\n    enableJwtAuthHelper: true,\n    // 自定义函数\n    jwtAuthCustomPayloadTransformer: (payload: any, token: string): AuthInfo => {\n      return {\n        // ...使用标准JWT字段映射AuthInfo\n        token: token,\n        clientId: payload.aud,\n        // ...\n      };\n    }\n  }\n}\n```\n\n\n\n\n\n### 更多认证\n\n在其他的认证场景下，也可以使用 Midway 组件或者编写中间件完整。\n\n比如你可以使用 `passport` 组件完成 OAuth 认证。\n\n在认证完成后，按照 MCP SDK 规范，需将认证信息放到 `req.auth` 中。\n\n如：\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport * as mcp from '@midwayjs/mcp';\nimport { MidwayMCPFramework } from '@midwayjs/mcp';\nimport { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';\n\n@Configuration({\n  imports: [\n    express,\n    // ...\n    mcp\n  ],\n  importConfigs: ['./config']\n})\nexport class MainConfiguration {\n  @Inject()\n  mcpFramework: MidwayMCPFramework;\n\n  async onReady(container: IMidwayContainer, mainApp: IMidwayApplication) {\n    const authMiddleware = async (req, res, next) => {\n      // ...\n      const userInfo = {\n        user: 'zhangsan',\n        role: 'admin'\n      }\n      \n      // 必须要设置这个字段\n      req.auth = {\n        token: req.headers.authorization,\n        clientId: 'test-client-id',\n        scopes: ['mcp:read', 'mcp:write'],\n        resource: new URL('https://mcp.example.com/resources'),\n        extra: userInfo,\n      } as AuthInfo;\n      \n      // koa 就放到 ctx.req.auth 上\n\n      await next();\n    }\n    \n    // 注意这里的 mainApp 是 Express\n    mainApp.getMiddleware().insertBefore(authMiddleware, 'mcpMiddleware');\n  }\n}\n```\n\n\n\n\n\n## 完整示例\n\n这里是一个完整的 MCP 服务器示例：\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport * as mcp from '@midwayjs/mcp';\n\n@Configuration({\n  imports: [\n    express,\n    mcp\n  ],\n  importConfigs: ['./config']\n})\nexport class MainConfiguration {}\n\n// src/config/config.default.ts\nexport default {\n  express: {\n    port: 3000,\n  },\n  mcp: {\n    serverInfo: {\n      name: 'example-mcp-server',\n      version: '1.0.0',\n    },\n    transportType: 'stream-http',\n  }\n}\n\n// src/tools/calculator.ts\nimport { Tool, IMcpTool, ToolConfig } from '@midwayjs/mcp';\nimport { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport { z } from 'zod';\n\nconst calcConfig: ToolConfig<{ \n  operation: z.ZodEnum<['add', 'subtract', 'multiply', 'divide']>;\n  a: z.ZodNumber;\n  b: z.ZodNumber;\n}> = {\n  description: 'Perform basic mathematical operations',\n  inputSchema: {\n    operation: z.enum(['add', 'subtract', 'multiply', 'divide']),\n    a: z.number(),\n    b: z.number()\n  }\n};\n\n@Tool('calculator', calcConfig)\nexport class CalculatorTool implements IMcpTool {\n  async execute(args: { \n    operation: 'add' | 'subtract' | 'multiply' | 'divide';\n    a: number;\n    b: number;\n  }): Promise<CallToolResult> {\n    let result: number;\n    \n    switch (args.operation) {\n      case 'add':\n        result = args.a + args.b;\n        break;\n      case 'subtract':\n        result = args.a - args.b;\n        break;\n      case 'multiply':\n        result = args.a * args.b;\n        break;\n      case 'divide':\n        if (args.b === 0) {\n          throw new Error('Division by zero');\n        }\n        result = args.a / args.b;\n        break;\n    }\n    \n    return {\n      content: [\n        {\n          type: 'text',\n          text: `${args.a} ${args.operation} ${args.b} = ${result}`\n        }\n      ]\n    };\n  }\n}\n```\n\n## 动态调用 API\n\n您可以在运行时动态调用 MCP 服务器的原始 API 来注册工具、提示和资源：\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport * as mcp from '@midwayjs/mcp';\nimport { MidwayMCPFramework } from '@midwayjs/mcp';\nimport { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\n@Configuration({\n  imports: [\n    express,\n    mcp\n  ],\n  importConfigs: ['./config']\n})\nexport class MainConfiguration {\n  @Inject()\n  mcpFramework: MidwayMCPFramework;\n\n  async onReady() {\n    const server = this.mcpFramework.getServer();\n    \n    // 动态注册工具\n    server.registerTool({\n      name: 'dynamic_tool',\n      description: '动态注册的工具',\n      inputSchema: {\n        type: 'object',\n        properties: {\n          message: { type: 'string', description: '要处理的消息' }\n        },\n        required: ['message']\n      }\n    }, async (args): Promise<CallToolResult> => {\n      return {\n        content: [{\n          type: 'text',\n          text: `动态处理: ${args.message}`\n        }]\n      };\n    });\n    \n    // 动态注册提示\n    server.registerPrompt({\n      name: 'dynamic_prompt',\n      description: '动态生成的提示',\n      argsSchema: {\n        type: 'object',\n        properties: {\n          topic: { type: 'string', description: '主题' }\n        },\n        required: ['topic']\n      }\n    }, async (args) => {\n      return {\n        messages: [{\n          role: 'user',\n          content: {\n            type: 'text',\n            text: `请写一篇关于 ${args.topic} 的文章`\n          }\n        }]\n      };\n    });\n    \n    // 动态注册资源\n    server.registerResource({\n      uri: 'dynamic://config',\n      name: '动态配置',\n      description: '动态生成的配置资源',\n      mimeType: 'application/json'\n    }, async (uri) => {\n      return {\n        contents: [{\n          uri: uri.toString(),\n          mimeType: 'application/json',\n          text: JSON.stringify({\n            timestamp: new Date().toISOString(),\n            config: 'dynamic data'\n          })\n        }]\n      };\n    });\n  }\n}\n```\n\n:::tip\n动态注册的 API 适用于需要在运行时根据业务逻辑动态添加 MCP 功能的场景。与装饰器方式相比，动态注册提供了更大的灵活性，但需要手动管理注册逻辑。\n:::\n\n## 客户端连接\n\n### StreamHTTP 客户端\n\n```typescript\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';\n\n// 创建客户端实例\nconst client = new Client({\n  name: 'my-mcp-client',\n  version: '1.0.0'\n});\n\n// 创建 StreamHTTP 传输\nconst transport = new StreamableHTTPClientTransport(\n  new URL('http://localhost:3000/mcp'),\n  {\n    requestInit: {\n      headers: {\n        'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'\n      }\n    }\n  }\n);\n\n// 连接到服务器\nawait client.connect(transport);\n\n// 列出可用工具\nconst { tools } = await client.listTools();\nconsole.log('Available tools:', tools);\n\n// 调用工具\nconst result = await client.callTool({\n  name: 'calculator',\n  arguments: {\n    operation: 'add',\n    a: 5,\n    b: 3\n  }\n});\nconsole.log('Tool result:', result);\n\n// 使用完毕后关闭连接\nawait client.close();\n```\n\n\n\n### Stdio 客户端 (进程通信)\n\n```typescript\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';\n\n// 创建客户端实例\nconst client = new Client({\n  name: 'my-mcp-client',\n  version: '1.0.0'\n});\n\n// 创建 Stdio 传输，启动 MCP 服务器进程\nconst transport = new StdioClientTransport({\n  command: 'node',\n  args: ['bootstrap.js'],\n  env: {\n    NODE_ENV: 'production',\n    // 继承当前进程的环境变量\n    ...process.env\n  },\n  // 可选：设置工作目录\n  cwd: '/path/to/mcp/server',\n  // 可选：处理 stderr 输出\n  stderr: 'inherit' // 'pipe' | 'inherit' | 'ignore'\n});\n\n// 连接到服务器（会自动启动子进程）\nawait client.connect(transport);\n\n// 可选：监听 stderr 输出（当 stderr: 'pipe' 时）\nif (transport.stderr) {\n  transport.stderr.on('data', (data) => {\n    console.error('Server stderr:', data.toString());\n  });\n}\n\n// 列出可用工具\nconst { tools } = await client.listTools();\nconsole.log('Available tools:', tools);\n\n// 调用工具\nconst result = await client.callTool({\n  name: 'calculator',\n  arguments: {\n    operation: 'add',\n    a: 5,\n    b: 3\n  }\n});\nconsole.log('Tool result:', result);\n\n// 使用完毕后关闭连接（会终止子进程）\nawait client.close();\n```\n\n:::tip\n**传输类型选择建议**：\n- **StreamHTTP**: 适用于网络服务、微服务架构、Web 应用集成\n- **SSE**: 传统方式，主要用于向下兼容\n- **Stdio**: 适用于命令行工具、本地脚本、桌面应用集成（如 Claude Desktop、Cursor、Trae 等 AI 编辑器）\n:::\n\n### 与编辑器集成\n\n在 Cursor/Trae/Claude Desktop 等编辑器的配置文件中添加您的 MCP 服务器：\n\nstdio 示例如下：\n\n```json\n{\n  \"mcpServers\": {\n    \"my-midway-mcp-server\": {\n      \"command\": \"node\",\n      \"args\": [\"bootstrap.js\"],\n      \"env\": {\n        \"NODE_ENV\": \"production\"\n      }\n    }\n  }\n}\n```\n\nstream-http 示例如下：\n\n```json\n{\n  \"mcpServers\": {\n    \"my-midway-mcp-server\": {\n      \"url\": \"http://localhost:3000/mcp\"\n    }\n  }\n}\n```\n\n## 最佳实践\n\n### 1. 类型安全\n\n使用 TypeScript 泛型确保类型安全：\n\n```typescript\nimport { Tool, ToolConfig } from '@midwayjs/mcp';\nimport { z } from 'zod';\n\n// 定义清晰的输入类型\nconst userSchema = {\n  userId: z.string().min(1),\n  includeProfile: z.boolean().optional()\n} as const;\n\nconst userToolConfig: ToolConfig<typeof userSchema> = {\n  description: 'Get user information',\n  inputSchema: userSchema\n};\n\n@Tool('get_user', userToolConfig)\nexport class UserTool implements IMcpTool {\n  // TypeScript 会自动推断 args 的类型\n  async execute(args: { userId: string; includeProfile?: boolean }) {\n    // 实现逻辑\n  }\n}\n```\n\n### 2. 错误处理\n\n正确处理错误并返回有意义的错误信息：\n\n```typescript\n@Tool('risky_operation', config)\nexport class RiskyTool implements IMcpTool {\n  async execute(args: any): Promise<CallToolResult> {\n    try {\n      const result = await this.performRiskyOperation(args);\n      return {\n        content: [{ type: 'text', text: result }]\n      };\n    } catch (error) {\n      return {\n        content: [{ \n          type: 'text', \n          text: `Error: ${error.message}` \n        }],\n        isError: true\n      };\n    }\n  }\n}\n```\n\n### 3. 资源管理\n\n合理管理资源连接和清理：\n\n```typescript\n@Resource('database_query', config)\nexport class DatabaseResource implements IMcpResource {\n  @Inject()\n  databaseService: DatabaseService;\n\n  async handle(uri: URL): Promise<ReadResourceResult> {\n    const connection = await this.databaseService.getConnection();\n    \n    try {\n      const result = await connection.query(this.parseQuery(uri));\n      return {\n        contents: [{\n          uri: uri.toString(),\n          mimeType: 'application/json',\n          text: JSON.stringify(result)\n        }]\n      };\n    } finally {\n      await connection.close();\n    }\n  }\n}\n```\n\n## 常见问题\n\n### Q: MCP 与传统 API 有什么区别？\n\nA: MCP 是专门为 AI 模型设计的协议，提供了语义化的接口描述、类型安全的参数验证和上下文感知的交互。与传统 REST API 相比，MCP 更适合 AI 模型理解和使用。\n\n### Q: 如何在生产环境中部署 MCP 服务？\n\nA: 推荐使用 `stream-http` 传输类型，配合反向代理（如 Nginx）进行负载均衡。确保启用适当的安全措施和监控。\n\n### Q: 可以在一个应用中同时提供多种传输类型吗？\n\nA: 当前版本每个应用实例只支持一种传输类型。如需支持多种传输类型，建议部署多个应用实例。\n\n### Q: 如何调试 MCP 服务？\n\nA: 使用日志记录、MCP 客户端工具进行测试，或者使用官方提供的 [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector)\n\n\n\n## 相关链接\n\n- [MCP 官方文档](https://modelcontextprotocol.io/)\n- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)\n- [Claude Desktop MCP 配置](https://docs.anthropic.com/claude/docs/mcp)","site/docs/extensions/mikro.md":"import Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n# MikroORM\n\n本章节介绍用户如何在 midway 中使用 MikroORM。 MikroORM 是基于数据映射器、工作单元和身份映射模式的 Node.js 的 TypeScript ORM。\n\nMikroORM 的官网文档在 [这里](https://mikro-orm.io/docs)。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 关于升级\n\n* 从 `v3.14.0` 版本的组件开始，支持 mikro v5/v6 版本，由于 mikro v5 到 v6 有较大的变化，如从 mikro 老版本升级请提前阅读 [Upgrading from v5 to v6](https://mikro-orm.io/docs/upgrading-v5-to-v6)\n* 组件示例已更新为 v6 版本\n* MikroORM v7 已切换为 native ESM，并调整了装饰器包路径。v7 项目请使用 `@midwayjs/mikro7`，不要和 `@midwayjs/mikro` 混用。\n\n\n\n## 安装组件\n\n\n安装 mikro 组件，提供接入 mikro-orm 的能力。`@midwayjs/mikro` 对应 MikroORM v6。\n\n\n```bash\n$ npm i @midwayjs/mikro@4 @mikro-orm/core@^6 @mikro-orm/sqlite@^6 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/mikro\": \"^4.0.0\",\n    \"@mikro-orm/core\": \"^6.0.2\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n同时，还需要引入对应数据库的适配包。\n\n比如：\n\n```typescript\n{\n  \"dependencies\": {\n    // sqlite\n    \"@mikro-orm/sqlite\": \"^6.0.2\",\n\n    // mysql\n    \"@mikro-orm/mysql\": \"^6.0.2\",\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n更多驱动程序请查看 [官方文档](https://mikro-orm.io/docs/usage-with-sql/)。\n\n如果使用 MikroORM v7，请安装独立的 v7 组件。\n\n```bash\n$ npm i @midwayjs/mikro7@4 @mikro-orm/core@^7 @mikro-orm/decorators@^7 @mikro-orm/sqlite@^7 --save\n```\n\n对应 `package.json` 依赖如下。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/mikro7\": \"^4.0.0\",\n    \"@mikro-orm/core\": \"^7.0.0\",\n    \"@mikro-orm/decorators\": \"^7.0.0\",\n    \"@mikro-orm/sqlite\": \"^7.0.0\"\n  }\n}\n```\n\nv7 使用 legacy TypeScript 装饰器时，Entity 装饰器需要从 `@mikro-orm/decorators/legacy` 导入，并在配置中显式设置 `metadataProvider: ReflectMetadataProvider`。同时，项目的 TypeScript 配置需要使用 `moduleResolution: \"nodenext\"`、`\"node20\"` 或 `\"bundler\"`。\n\n\n\n## 引入组件\n\n\n在 `src/configuration.ts` 引入 mikro 组件，示例如下。\n\n```typescript\n// configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as mikro from '@midwayjs/mikro';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    mikro\t\t\t\t// 加载 mikro 组件\n  ],\n  importConfigs: [\n  \tjoin(__dirname, './config')\n  ]\n})\nexport class MainConfiguration {\n\n}\n```\n\n\n\n## 基础使用\n\n和其他 orm 框架类似，都是分为几个步骤：\n\n- 1、定义 Entity\n- 2、配置数据源\n- 3、获取 EntityModel 进行调用\n\n下面的更多 Entity 代码请查看 [示例](https://github.com/midwayjs/midway/tree/main/packages/mikro/test/fixtures/base-fn-origin)。\n\n\n\n### 目录结构\n\n一个基础的参考目录结构如下。\n\n```\nMyProject\n├── src\n│   ├── config\n│   │   └── config.default.ts\n│   ├── entity\n│   │   ├── book.entity.ts\n│   │   ├── index.ts\n│   │   └── base.ts\n│   ├── configuration.ts\n│   └── service\n├── .gitignore\n├── package.json\n├── README.md\n└── tsconfig.json\n```\n\n\n\n### 定义 Entity\n\n定义基础的 Entity。\n\n```typescript\n// src/entity/BaseEntity.ts\nimport { PrimaryKey, Property } from '@mikro-orm/core';\n\nexport abstract class BaseEntity {\n\n  @PrimaryKey()\n  id!: number;\n\n  @Property()\n  createdAt: Date = new Date();\n\n  @Property({ onUpdate: () => new Date() })\n  updatedAt: Date = new Date();\n\n}\n```\n\n定义实际的 Entity，包含一对多，多对多等关系。\n\n```typescript\n// src/entity/book.entity.ts\nimport { Cascade, Collection, Entity, ManyToMany, ManyToOne, Property } from '@mikro-orm/core';\nimport { Author, BookTag, Publisher } from './index';\nimport { BaseEntity } from './base';\n\n@Entity()\nexport class Book extends BaseEntity {\n\n  @Property()\n  title: string;\n\n  @ManyToOne(() => Author)\n  author: Author;\n\n  @ManyToOne(() => Publisher, { cascade: [Cascade.PERSIST, Cascade.REMOVE], nullable: true })\n  publisher?: Publisher;\n\n  @ManyToMany(() => BookTag)\n  tags = new Collection<BookTag>(this);\n\n  @Property({ nullable: true })\n  metaObject?: object;\n\n  @Property({ nullable: true })\n  metaArray?: any[];\n\n  @Property({ nullable: true })\n  metaArrayOfStrings?: string[];\n\n  constructor(title: string, author: Author) {\n    super();\n    this.title = title;\n    this.author = author;\n  }\n\n}\n```\n\n\n\n### 配置数据源\n\nmikro v5 和 v6 略有不同。\n\n<Tabs>\n<TabItem value=\"v6\" label=\"mikro v6\">\n\n```typescript\n// src/config/config.default\nimport { Author, BaseEntity, Book, BookTag, Publisher } from '../entity';\nimport { join } from 'path';\nimport { SqliteDriver } from '@mikro-orm/sqlite';\n\nexport default (appInfo) => {\n  return {\n    mikro: {\n      dataSource: {\n        default: {\n          dbName: join(__dirname, '../../test.sqlite'),\n          driver: SqliteDriver,\t\t// 这里使用了 sqlite 做示例\n          allowGlobalContext: true,\n          // 实体形式\n          entities: [Author, Book, BookTag, Publisher, BaseEntity],\n          // 支持如下的扫描形式，为了兼容我们可以同时进行.js和.ts匹配️\n          entities: [\n            'entity',                        // 指定目录\n            '**/entity/*.entity.{j,t}s',     // 通配加后缀匹配\n          ],\n        }\n      }\n    }\n  }\n}\n```\n\n</TabItem>\n\n<TabItem value=\"v5\" label=\"mikro v5\">\n\n```typescript\n// src/config/config.default\nimport { Author, BaseEntity, Book, BookTag, Publisher } from '../entity';\nimport { join } from 'path';\n\nexport default (appInfo) => {\n  return {\n    mikro: {\n      dataSource: {\n        default: {\n          dbName: join(__dirname, '../../test.sqlite'),\n          type: 'sqlite',\t\t\t// 这里使用了 sqlite 做示例\n          allowGlobalContext: true,\n          // 实体形式\n          entities: [Author, Book, BookTag, Publisher, BaseEntity],\n          // 支持如下的扫描形式，为了兼容我们可以同时进行.js和.ts匹配️\n          entities: [\n            'entity',                        // 指定目录\n            '**/entity/*.entity.{j,t}s',     // 通配加后缀匹配\n          ],\n        }\n      }\n    }\n  }\n}\n```\n\n</TabItem>\n</Tabs>\n\n:::tip\n\nmikro 的 `entities` 字段配置已经经过框架处理，该字段配置请不要参考原始文档。\n\n:::\n\n\n\n### 增删查改\n\n在业务代码中，可以使用 `InjectRepository` 注入 `Repository` 对象执行简单的查询操作。其它的增删改操作可以通过配合`EntityManger ` 的 `persist` 和 `flush` 接口来实现，使用 `InjectEntityManager` 可以直接注入 `EntityManager` 对象，也可以通过`repository.getEntityManager()`获取。\n\n:::caution\n\n* 1、从 5.7 版本开始，MikroORM 将原来 `Repository` 上 `persist` 和 `flush` 等接口标为*弃用*，并计划在 v6 版本中 [彻底移除](https://github.com/mikro-orm/mikro-orm/discussions/3989)，建议直接调用`EntityManager`上的相关接口\n* 2、v6 已经彻底 [弃用](https://mikro-orm.io/docs/upgrading-v5-to-v6#removed-methods-from-entityrepository) 上述接口\n\n:::\n\n```typescript\n// src/service/book.service.ts\nimport { Book } from './entity/book.entity';\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityManager, InjectRepository } from '@midwayjs/mikro';\nimport { QueryOrder } from '@mikro-orm/core';\nimport { EntityManager, EntityRepository } from '@mikro-orm/mysql'; // 需要使用数据库驱动对应的类来执行操作\n\n@Provide()\nexport class BookService {\n\n  @InjectRepository(Book)\n  bookRepository: EntityRepository<Book>;\n\n  @InjectEntityManager()\n  em: EntityManager;\n\n  async queryByRepo() {\n    // 使用Repository查询\n    const books = await this.bookRepository.findAll({\n      populate: ['author'],\n      orderBy: { title: QueryOrder.DESC },\n      limit: 20,\n    });\n    return books;\n  }\n\n  async createBook() {\n    const book = new Book({ title: 'b1', author: { name: 'a1', email: 'e1' } });\n    // 标记保存Book\n    this.em.persist(book);\n    // 执行所有变更\n    await this.em.flush();\n    return book;\n  }\n}\n```\n\n## 高级功能\n\n### 获取数据源\n\n数据源即创建出的数据源对象，我们可以通过注入内置的数据源管理器来获取。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport { MikroDataSourceManager } from '@midwayjs/mikro';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n\n  async onReady(container: IMidwayContainer) {\n    const dataSourceManager = await container.getAsync(MikroDataSourceManager);\n    const orm = dataSourceManager.getDataSource('default');\n    const connection = orm.em.getConnection();\n    // ...\n  }\n}\n```\n\n从 v3.8.0 开始，也可以通过装饰器注入。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport { InjectDataSource } from '@midwayjs/mikro';\nimport { MikroORM, IDatabaseDriver, Connection } from '@mikro-orm/core';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n\n  // 注入默认数据源\n  @InjectDataSource()\n  defaultDataSource: MikroORM<IDatabaseDriver<Connection>>;\n\n  // 注入自定义数据源\n  @InjectDataSource('default1')\n  customDataSource: MikroORM<IDatabaseDriver<Connection>>;\n\n  async onReady(container: IMidwayContainer) {\n    // ...\n  }\n}\n```\n\n\n\n### 日志\n\n可以通过配置将 midway 的 logger 添加到 mikro 中，用于记录 sql 等信息。\n\n```typescript\n// src/config/config.default.ts\nexporg default {\n\tmidwayLogger: {\n    clients: {\n      mikroLogger: {\n        // ...\n      }\n    }\n  },\n  mikro: {\n    dataSource: {\n      default: {\n        entities: [Author, Book, BookTag, Publisher, BaseEntity],\n        // ...\n        logger: 'mikroLogger',\n      }\n    },\n  }\n}\n```\n\n默认情况下  mikro 自带颜色，也会将其写入文件，可以通过配置关闭。\n\n```typescript\n// src/config/config.default.ts\nexporg default {\n\tmidwayLogger: {\n    clients: {\n      mikroLogger: {\n        transports: {\n          console: {\n            autoColors: false,\n          }，\n          file: {\n            fileLogName: 'mikro.log'，\n          },\n        },\n      }\n    }\n  },\n  mikro: {\n    dataSource: {\n      default: {\n        entities: [Author, Book, BookTag, Publisher, BaseEntity],\n        // ...\n        logger: 'mikroLogger',\n        colors: false,\n      }\n    },\n  }\n}\n```\n\n\n\n## 常见问题\n\n\n\n### 1、Node 版本\n\nMikro-orm 对 Node 版本有一些限制，必须为 `>=14.0.0` ，所以 `@midwayjs/mikro` 组件的使用规则也如此。\n\n\n\n### 2、Identity Map\n\nMikro-orm 内部查询有一个 [Identity Map](https://mikro-orm.io/docs/identity-map) 的概念，Midway 已经在所有的内置 Framework 的中间件内置加入了该功能，如果在非请求链路调用场景下使用，比如 `src/configuration` 中，可以开启 `allowGlobalContext` 选项。\n\n\n\n### 3、多库的支持\n\n和其他数据库一样，Midway 支持多数据源的配置。\n\n```typescript\n// src/config/config.default\nimport { Author, BaseEntity, Book, BookTag, Publisher } from '../entity';\nimport { SqlHighlighter } from '@mikro-orm/sql-highlighter';\nimport { join } from 'path';\n\nexport default (appInfo) => {\n  return {\n    mikro: {\n      dataSource: {\n        custom1: {\n          // ...\n        },\n        custom2: {\n          // ...\n        }\n      }\n    }\n  }\n}\n```\n\n注意在使用时，需要传递来自哪个数据源。\n\n```typescript\n// ...\n\n@Provide()\nexport class BookController {\n\n  @InjectRepository(Book, 'custom1')\n  bookRepository: EntityRepository<Book>;\n\n  async findBookAndQuery() {\n    // ...\n  }\n}\n```\n","site/docs/extensions/mongodb.md":"# MongoDB\n\n在这一章节中，我们选择 [Typegoose](https://github.com/typegoose/typegoose) 作为基础的 MongoDB ORM 库。就如同他描述的那样 \" Define Mongoose models using TypeScript classes\"，和 TypeScript 结合的很不错。\n\n简单的来说，Typegoose 使用 TypeScript 编写 Mongoose 模型的 “包装器”，它的大部分能力还是由 [mongoose](https://www.npmjs.com/package/mongoose) 库来提供的。\n\n也可以直接选择 [mongoose](https://www.npmjs.com/package/mongoose) 库来使用，我们会分别描述。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n:::tip\n\n- 1、当前模块从 v3.4.0 开始已经重构，历史写法兼容，如果查询历史文档，请参考 [这里](../legacy/mongodb)。\n- 2、如果代码中有读取配置，注意 `mongoose.clients` 可能会读不到，请使用 `mongoose.dataSource`。\n\n:::\n\n\n\n## 和老写法的区别\n\n如果想使用新版本的用法，请参考下面的流程，将老代码进行修改，新老代码请勿混用。\n\n升级方法：\n\n- 1、无需再使用 `EntityModel` 装饰器\n- 3、在 `src/config.default` 的 `mongoose` 部分配置调整，参考下面的数据源配置部分\n  - 3.1 修改为数据源的形式 `mongoose.dataSource`\n  - 3.2 将实体模型在数据源的 `entities` 字段中声明\n\n\n\n\n## Mongoose 版本依赖\n\n\nmongoose 和你服务器使用的 MongoDB Server 的版本也有着一定的关系，如下，请务必注意。\n\n\n- MongoDB Server 2.4.x: mongoose ^3.8 or 4.x\n- MongoDB Server 2.6.x: mongoose ^3.8.8 or 4.x\n- MongoDB Server 3.0.x: mongoose ^3.8.22, 4.x, or 5.x\n- MongoDB Server 3.2.x: mongoose ^4.3.0 or 5.x\n- MongoDB Server 3.4.x: mongoose ^4.7.3 or 5.x\n- MongoDB Server 3.6.x: mongoose 5.x\n- MongoDB Server 4.0.x: mongoose ^5.2.0\n- MongoDB Server 4.2.x: mongoose ^5.7.0\n- MongoDB Server 4.4.x: mongoose ^5.10.0\n- MongoDB Server 5.x: mongoose ^6.0.0\n\n\n**mongoose 相关的依赖比较复杂，且对应不同的版本，现阶段，我们使用的主要是 mongoose v5 和 v6。**\n\n\n:::info\n从 mongoose@v5.11.0 开始，mongoose 官方支持了定义，所以不再需要安装 @types/mongoose 依赖包。\n:::\n\n安装包依赖版本如下：\n\n**支持 MongoDB Server 6.x**\n\n```json\n  \"dependencies\": {\n    \"mongoose\": \"^7.0.0\",\n    \"@typegoose/typegoose\": \"^10.0.0\",\t\t// 使用 typegoose 需要安装此依赖\n  },\n```\n\n**支持 MongoDB Server 5.x**\n\n```json\n  \"dependencies\": {\n    \"mongoose\": \"^6.0.7\",\n    \"@typegoose/typegoose\": \"^9.0.0\",\t\t// 使用 typegoose 需要安装此依赖\n  },\n```\n\n\n**支持 MongoDB Server 4.4.x**\n\n\n以下版本不需要安装额外定义包。\n```json\n  \"dependencies\": {\n    \"mongoose\": \"^5.13.3\",\n    \"@typegoose/typegoose\": \"^8.0.0\",   // 使用 typegoose 需要安装此依赖\n  },\n```\n\n\n以下版本需要安装额外定义包（不推荐）。\n```json\n \"dependencies\": {\n    \"mongodb\": \"3.6.3\",\t\t\t\t\t\t\t\t\t// mongoose 内部写死了该版本\n    \"mongoose\": \"~5.10.18\",\n    \"@typegoose/typegoose\": \"^7.0.0\",   // 使用 typegoose 需要安装此依赖\n },\n \"devDependencies\": {\n    \"@types/mongodb\": \"3.6.3\",\t\t\t\t\t// 只能使用此版本\n    \"@types/mongoose\": \"~5.10.3\",\n }\n```\n\n\n其余的 MongoDB 安装模块类似，未测。\n\n\n\n## 使用 Typegoose\n\n\n### 1、安装组件\n\n\n安装 Typegoose 组件，提供访问 MongoDB 的能力。\n\n\n**请务必注意，请查看第一小节提前编写/安装 mongoose 等相关依赖包。**\n```bash\n$ npm i @midwayjs/typegoose@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    // 组件\n    \"@midwayjs/typegoose\": \"^4.0.0\",\n    // 上一节中的 mongoose 依赖\n  },\n  \"devDependencies\": {\n    // 上一节中的 mongoose 依赖\n    // ...\n  }\n}\n```\n\n\n\n安装后需要手动在 `src/configuration.ts` 配置，代码如下。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as typegoose from '@midwayjs/typegoose';\n\n@Configuration({\n  imports: [\n    typegoose  \t\t\t\t\t\t\t\t\t\t\t// 加载 typegoose 组件\n  ],\n  importConfigs: [\n  \tjoin(__dirname, './config')\n  ]\n})\nexport class MainConfiguration {\n\n}\n```\n\n:::info\n在该组件中，midway 只是做了简单的配置规则化，并将其注入到初始化流程中。\n:::\n\n### 2、简单的目录结构\n\n\n我们以一个简单的项目举例，其他结构请自行参考。\n\n\n```text\nMyProject\n├── src              \t\t\t\t\t\t\t// TS 根目录\n│   ├── config\n│   │   └── config.default.ts \t\t// 应用配置文件\n│   ├── entity       \t\t\t\t\t\t\t// 实体（数据库 Model) 目录\n│   │   └── user.ts  \t\t\t\t\t  \t// 实体文件\n│   ├── configuration.ts     \t\t\t// Midway 配置文件\n│   └── service      \t\t\t\t\t\t\t// 其他的服务目录\n├── .gitignore\n├── package.json\n├── README.md\n└── tsconfig.json\n```\n\n\n在这里，我们的数据库实体主要放在 `entity` 目录（非强制），这只是一个简单的约定。\n\n\n\n### 3、创建实体文件\n\n比如在 `src/entity/user.ts` 中。\n\n\n```typescript\nimport { prop } from '@typegoose/typegoose';\n\nexport class User {\n  @prop()\n  public name?: string;\n\n  @prop({ type: () => [String] })\n  public jobs?: string[];\n}\n```\n\n等价于使用 mongoose 的下列代码\n\n```typescript\nconst userSchema = new mongoose.Schema({\n  name: String,\n  jobs: [{ type: String }]\n});\n\nconst User = mongoose.model('User', userSchema);\n```\n\n:::info\n所以说，typegoose 只是简化了 model 的创建过程。\n:::\n\n\n\n\n### 4、配置连接信息\n\n\n在 `src/config/config.default.ts` 中加入连接的配置。\n\n```typescript\nimport { User } from '../entity/user';\n\nexport default {\n  // ...\n  mongoose: {\n    dataSource: {\n      default: {\n        uri: 'mongodb://localhost:27017/test',\n        options: {\n          useNewUrlParser: true,\n          useUnifiedTopology: true,\n          user: '***********',\n          pass: '***********'\n        },\n        // 关联实体\n        entities: [ User ]\n      }\n    }\n  },\n}\n```\n\n如需以目录扫描形式关联，请参考 [数据源管理](../data_source)。\n\n\n\n### 5、引用实体，调用数据库\n\n\n示例代码如下：\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typegoose';\nimport { ReturnModelType } from '@typegoose/typegoose';\nimport { User } from '../entity/user';\n\n@Provide()\nexport class TestService {\n\n  @InjectEntityModel(User)\n  userModel: ReturnModelType<typeof User>;\n\n  async getTest(){\n    // create data\n    const { _id: id } = await this.userModel.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User); // an \"as\" assertion, to have types for all properties\n\n    // find data\n    const user = await this.userModel.findById(id).exec();\n    console.log(user)\n  }\n}\n```\n\n\n### 6、多库的情况\n\n首先定义多个实体。\n\n```typescript\nclass User {\n\n  @prop()\n  public name?: string;\n\n  @prop({ type: () => [String] })\n  public jobs?: string[];\n}\n\nclass User2 {\n\n  @prop()\n  public name?: string;\n\n  @prop({ type: () => [String] })\n  public jobs?: string[];\n}\n```\n\n\n将实体配置到多个数据源。\n\n\n在 `src/config/config.default.ts` 中加入数据源的配置。\n```typescript\nimport { User, User2 } from '../entity/user';\n\nexport default {\n  // ...\n  mongoose: {\n    dataSource: {\n      default: {\n        uri: 'mongodb://localhost:27017/test',\n        options: {\n          useNewUrlParser: true,\n          useUnifiedTopology: true,\n          user: '***********',\n          pass: '***********'\n        },\n        entities: [ User ]\n      },\n      db1: {\n        uri: 'mongodb://localhost:27017/test1',\n        options: {\n          useNewUrlParser: true,\n          useUnifiedTopology: true,\n          user: '***********',\n          pass: '***********'\n        },\n        entities: [ User2 ]\n      }\n    }\n  },\n}\n```\n\n\n定义实例时使用固定的连接，在扫描 dataSource 配置 Model 会自动关联 mongoose连接(`getModelForClass(Model, { existingConnection: conn })`)。\n\n```typescript\n@Provide()\nexport class TestService{\n\n  @InjectEntityModel(User)\n  userModel: ReturnModelType<typeof User>;\n\n  @InjectEntityModel(User2)\n  user2Model: ReturnModelType<typeof User2>;\n\n  async getTest(){\n    const { _id: id } = await this.userModel.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User); // an \"as\" assertion, to have types for all properties\n    const user = await this.userModel.findById(id).exec();\n    console.log(user)\n\n    const { _id: id2 } = await this.user2Model.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User2); // an \"as\" assertion, to have types for all properties\n    const user2 = await this.user2Model.findById(id2).exec();\n    console.log(user2)\n  }\n}\n\n```\n\n\n\n### 7、关于 schemaOptions\n\nTypegoose 预留了一个 `setGlobalOptions`  方法用来设置 [schemaOptions](https://typegoose.github.io/typegoose/docs/api/decorators/model-options#schemaoptions) 和一些其他全局性的 [配置](https://typegoose.github.io/typegoose/docs/api/decorators/model-options#options-1)。\n\n我们可以在项目配置加载时设置它。\n\n```typescript\n// srcconfiguration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as typegoose from '@midwayjs/typegoose';\nimport * as Typegoose from '@typegoose/typegoose';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  async onConfigLoad() {\n\n    Typegoose.setGlobalOptions({\n      schemaOptions: {\n        // ...\n      },\n      options: { allowMixed: Severity.ERROR }\n    });\n    // ...\n  }\n}\n```\n\n\n\n\n\n## 直接使用 mongoose\n\nmongoose 组件是 typegoose 的基础组件，有时候我们可以直接使用它。\n\n\n### 1、安装组件\n\n\n**请务必注意，请查看第一小节提前编写/安装 mongoose 等相关依赖包。**\n\n```bash\n$ npm i @midwayjs/mongoose@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    // 组件\n    \"@midwayjs/mongoose\": \"^4.0.0\",\n    // 上一节中的 mongoose 依赖\n  },\n  \"devDependencies\": {\n    // 上一节中的 mongoose 依赖\n    // ...\n  }\n}\n```\n\n\n\n### 2、开启组件\n\n\n安装后需要手动在 `src/configuration.ts` 配置，代码如下。\n```typescript\n// configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as mongoose from '@midwayjs/mongoose';\n\n@Configuration({\n  imports: [\n    mongoose  \t\t\t\t\t\t\t\t\t\t\t// 加载 mongoose 组件\n  ],\n  importConfigs: [\n  \tjoin(__dirname, './config')\n  ]\n})\nexport class MainConfiguration {\n\n}\n```\n\n\n\n\n### 2、配置\n\n和 typegoose 相同，或者说 typegoose 使用的就是 mongoose 的配置。\n\n不管是单库还是多库，数据源配置都是类似的。\n\n\n单库：\n```typescript\nexport default {\n  // ...\n  mongoose: {\n    dataSource: {\n      default: {\n        uri: 'mongodb://localhost:27017/test',\n        options: {\n          useNewUrlParser: true,\n          useUnifiedTopology: true,\n          user: '***********',\n          pass: '**********'\n        }\n      }\n    }\n  },\n}\n```\n多库：\n```typescript\nexport default {\n  // ...\n  mongoose: {\n    dataSource: {\n      default: {\n        uri: 'mongodb://localhost:27017/test',\n        options: {\n          useNewUrlParser: true,\n          useUnifiedTopology: true,\n          user: '***********',\n          pass: '***********'\n        }\n      },\n      db1: {\n        uri: 'mongodb://localhost:27017/test1',\n        options: {\n          useNewUrlParser: true,\n          useUnifiedTopology: true,\n          user: '***********',\n          pass: '***********'\n        }\n      }\n    }\n  },\n}\n```\n\n\n\n### 3、使用\n\n\n当我们希望获取到原始的连接对象时，可以直接使用封装好的 `MongooseConnectionService` 对象。\n```typescript\nimport { Provide, Inject, Init } from '@midwayjs/core';\nimport { MongooseDataSourceManager } from '@midwayjs/mongoose';\nimport { Schema, Document } from 'mongoose';\n\ninterface User extends Document {\n  name: string;\n  email: string;\n  avatar: string;\n}\n\n@Provide()\nexport class TestService {\n\n  @Inject()\n  dataSourceManager: MongooseDataSourceManager;\n\n  @Init()\n  async init() {\n    // get default connection\n    this.conn = this.dataSourceManager.getDataSource('default');\n  }\n\n  async invoke(){\n    const schema = new Schema<User>({\n      name: { type: String, required: true },\n      email: { type: String, required: true },\n      avatar: String\n    });\n    const UserModel = this.conn.model<User>('User', schema);\n    const doc = new UserModel({\n      name: 'Bill',\n      email: 'bill@initech.com',\n      avatar: 'https://i.imgur.com/dM7Thhn.png'\n    });\n    await doc.save();\n  }\n}\n\n```\n\n\n\n\n\n\n## 常见问题\n\n\n### 1、E002: You are using a NodeJS Version below 12.22.0\n\n\n在新版本 @typegoose/typegoose (v8, v9) 中增加了 Node 版本的校验，如果你的 Node.js 版本低于 v12.22.0，就会出现这个提示。\n\n\n普通情况下，请升级 Node.js 到这个版本以上即可解决。\n\n\n在特殊场景下，比如 Serverless 无法修改 Node.js 版本且版本低于 v12.22 的情况下，由于 v12 版本子版本其实都可以，可以通过临时修改 process.version 绕过。\n\n\n```typescript\n// src/configuration.ts\n\nObject.defineProperty(process, 'version', {\n  value: 'v12.22.0',\n  writable: true,\n});\n\n// other code\n\nexport class MainConfiguration {}\n```\n\n\n\n","site/docs/extensions/mqtt.md":"# MQTT\n\nMQTT是用于物联网 (IoT) 的OASIS标准消息传递协议。它被设计为非常轻量级的发布/订阅消息传输，非常适合以较小的代码占用空间和最小的网络带宽连接远程设备。MQTT目前广泛应用于汽车、制造、电信、石油和天然气等行业。\n\n相关信息：\n\n| 描述              |              |\n| ----------------- | ------------ |\n| 可用于标准项目    | ✅            |\n| 可用于 Serverless | 可以发布消息 |\n| 可用于一体化      | ✅            |\n| 包含独立主框架    | ✅            |\n| 包含独立日志      | ✅            |\n\n\n\n## 版本要求\n\n由于 [mqtt](https://github.com/mqttjs/MQTT.js) 库本身的要求，所需要的版本为 **Node.js >= 16**\n\n\n\n## 前置依赖\n\n由于 MQTT 需要 Broker 作为中转传输，你需要自行部署 MQTT Broker 服务，本文档不提供 MQTT 服务本身的部署指导。\n\n\n\n## 安装组件\n\n\n安装 mqtt 组件。\n\n\n```bash\n$ npm i @midwayjs/mqtt@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/mqtt\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n## 启用组件\n\n在 `src/configuration.ts` 中引入组件\n\n```typescript\n// ...\nimport * as mqtt from '@midwayjs/mqtt';\n\n@Configuration({\n  imports: [\n    // ...other components\n    mqtt,\n  ],\n})\nexport class MainConfiguration {}\n```\n\n\n\n由于 MQTT 分为 **订阅者（subscriber）** 和 **发布者（publisher）** 两部分，两个可以独立使用，我们将分别介绍。\n\n\n\n## 订阅服务\n\n### 基础配置\n\n通过 `sub` 字段和 `@MqttSubscriber` 装饰器，我们可以配置多个订阅者。\n\n比如，下面的 `sub1` 和 `sub2` 就是两个不同的订阅者。\n\n```typescript\n// src/config/config.default\n\nexport default {\n  mqtt: {\n    sub: {\n      sub1: {\n        // ...\n      },\n      sub2: {\n        // ...\n      }\n    }\n  }\n}\n```\n\n最简单的订阅者配置需要几个字段，订阅的地址和订阅的 Topic。\n\n```typescript\n// src/config/config.default\n\nexport default {\n  mqtt: {\n    sub: {\n      sub1: {\n        connectOptions: {\n          host: 'test.mosquitto.org',\n          port: 1883,\n        },\n        subscribeOptions: {\n          topicObject: 'test',\n        },\n      },\n      sub2: {\n        // ...\n      }\n    }\n  }\n}\n```\n\n `sub1` 订阅者配置了 `connectOptions` 和 `subscribeOptions` ，分别代表连接配置和订阅配置。\n\n### 订阅实现\n\n我们可以在目录中提供一个标准的订阅器实现，比如 `src/consumer/sub1.subscriber.ts`。\n\n```typescript\n// src/consumer/sub1.subscriber.ts\n\nimport { ILogger, Inject } from '@midwayjs/core';\nimport { Context, IMqttSubscriber, MqttSubscriber } from '@midwayjs/mqtt';\n\n@MqttSubscriber('sub1')\nexport class Sub1Subscriber implements IMqttSubscriber {\n\n  @Inject()\n  ctx: Context;\n\n  async subscribe() {\n    // ...\n  }\n}\n```\n\n`@MqttSubscriber` 装饰器声明了一个订阅类实现，它的参数为订阅者的名字，比如我们配置文件中的 `sub1`。\n\n`IMqttSubscriber` 接口约定了一个 `subscribe` 方法，每当接收到新的消息时，这个方法就会被执行。\n\n和其他消息订阅机制一样，消息本身通过 `Context` 字段来传递。\n\n```typescript\n// ...\nexport class Sub1Subscriber implements IMqttSubscriber {\n  @Inject()\n  ctx: Context;\n\n  async subscribe() {\n    const payload = this.ctx.message.toString();\n    // ...\n  }\n}\n```\n\n`Context` 字段包括几个 mqtt 属性。\n\n| 属性        | 类型                           | 描述             |\n| ----------- | ------------------------------ | ---------------- |\n| ctx.topic   | string                         | 订阅 Topic       |\n| ctx.message | Buffer                         | 消息内容         |\n| ctx.packet  | IPublishPacket（来自 mqtt 库） | publish 的包信息 |\n\n\n\n## 消息发布\n\n### 基础配置\n\n消息发布也需要创建实例，配置本身使用了 [服务工厂](/docs/service_factory) 的设计模式。\n\n比如多实例配置如下：\n\n```typescript\n// src/config/config.default\n\nexport default {\n  mqtt: {\n    pub: {\n      clients: {\n        default: {\n          host: 'test.mosquitto.org',\n          port: 1883,\n        },\n        pub2: {\n          // ...\n        }\n      }\n    }\n  }\n}\n```\n\n上面的配置创建了名为 `default` 和 `pub2` 的两个实例。\n\n\n\n### 使用发布者\n\n如果实例名为 `default` ，则可以使用默认的消息发布类。\n\n比如：\n\n```typescript\n// src/service/user.service.ts\nimport { Provide, Inject } from '@midwayjs/core';\nimport { DefaultMqttProducer } from '@midwayjs/mqtt';\n\n@Provide()\nexport class UserService {\n  \n  @Inject()\n  producer: DefaultMqttProducer;\n  \n  async invoke() {\n    // 同步发布消息\n    this.producer.publish('test', 'hello world');\n    \n    // 异步发布\n    await this.producer.publishAsync('test', 'hello world');\n    \n    // 增加配置\n    await this.producer.publishAsync('test', 'hello world', {\n      qos: 2\n    });\n  }\n}\n```\n\n也可以使用内置的工厂类 `MqttProducerFactory` 注入不同的实例。\n\n```typescript\n// src/service/user.service.ts\nimport { Provide, Inject } from '@midwayjs/core';\nimport { MqttProducerFactory, DefaultMqttProducer } from '@midwayjs/mqtt';\n\n@Provide()\nexport class UserService {\n  \n  @InjectClient(MqttProducerFactory, 'pub2')\n  producer: DefaultMqttProducer;\n  \n  async invoke() {\n    // ...\n  }\n}\n```\n\n\n\n## 组件日志\n\n组件有着自己的日志，默认会将 `ctx.logger` 记录在 `midway-mqtt.log` 中。\n\n我们可以单独配置这个 logger 对象。\n\n```typescript\nexport default {\n  midwayLogger: {\n    // ...\n    mqttLogger: {\n      fileLogName: 'midway-mqtt.log',\n    },\n  }\n}\n```\n\n这个日志的输出格式，我们也可以单独配置。\n\n```typescript\nexport default {\n  mqtt: {\n    // ...\n    contextLoggerFormat: info => {\n      const { jobId, from } = info.ctx;\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} ${info.message}`;\n    },\n  }\n}\n```\n\n","site/docs/extensions/one-shot.md":"# 单次执行\n\n`@midwayjs/one-shot` 是一个只提供 Framework 的一次性脚本执行组件，适合用 IoC 容器组织依赖，并在应用内触发一次性的任务逻辑。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ❌    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ❌   |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n## 安装依赖\n\n在现有项目中安装 one-shot 组件依赖。\n\n```bash\n$ npm i @midwayjs/one-shot@4  --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/one-shot\": \"^4.0.0\"\n  }\n}\n```\n\n## 开启组件\n\n在入口配置中引入组件。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as oneShot from '@midwayjs/one-shot';\n\n@Configuration({\n  imports: [oneShot],\n})\nexport class MainConfiguration {}\n```\n\n## 在生命周期执行\n\n该组件不要求编写额外 Runner，推荐在 `onServerReady` 生命周期里直接执行一次性脚本逻辑。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as oneShot from '@midwayjs/one-shot';\nimport { ScriptService } from './service/script';\n\n@Configuration({\n  imports: [oneShot],\n})\nexport class MainConfiguration {\n  @Inject()\n  scriptService: ScriptService;\n\n  async onServerReady() {\n    await this.scriptService.runOnce();\n  }\n}\n```\n\n## 需要请求作用域时\n\n如果脚本逻辑需要依赖请求作用域（request scope），可以通过 framework 的 `runScript` 执行一个固定格式的 service 类，框架会为本次执行创建上下文并走中间件/过滤器链。\n\n```typescript\n// src/script/syncUser.ts\nimport { Provide } from '@midwayjs/core';\nimport { OneShotRunner, Context } from '@midwayjs/one-shot';\n\n@Provide()\nexport class SyncUserScript implements OneShotRunner<{ id: number }, void> {\n  async run(payload?: { id: number }, ctx?: Context) {\n    // use payload / ctx\n    void payload;\n    void ctx;\n  }\n}\n```\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as oneShot from '@midwayjs/one-shot';\nimport { Framework } from '@midwayjs/one-shot';\nimport { SyncUserScript } from './script/syncUser';\n\n@Configuration({\n  imports: [oneShot],\n})\nexport class MainConfiguration {\n  @Inject()\n  framework: Framework;\n\n  async onServerReady() {\n    await this.framework.runScript(SyncUserScript, { id: 42 });\n  }\n}\n```\n\n## 日志\n\n组件默认会注册一个名为 `oneShotLogger` 的 logger，默认写入 `midway-one-shot.log`。\n\n你可以在脚本服务里通过 `@Logger('oneShotLogger')` 注入使用，例如：\n\n```typescript\nimport { Logger, ILogger } from '@midwayjs/core';\nexport class ScriptService {\n  @Logger('oneShotLogger')\n  logger: ILogger;\n\n  async runOnce() {\n    this.logger.info('run one-shot task');\n  }\n}\n```\n\n如果希望自定义日志文件名或级别，可以在应用配置中覆盖 `midwayLogger.clients.oneShotLogger`：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  midwayLogger: {\n    clients: {\n      oneShotLogger: {\n        fileLogName: 'my-one-shot.log',\n        level: 'info',\n      },\n    },\n  },\n};\n```\n","site/docs/extensions/orm.md":"# TypeORM\n\n[TypeORM](https://github.com/typeorm/typeorm) 是 `node.js` 现有社区最成熟的对象关系映射器（`ORM` ）。本文介绍如何在 Midway 中使用 TypeORM 。\n\n:::tip\n\n本模块是从 v3.4.0 开始为新版本，模块名有变化，历史写法部分兼容，如果查询历史文档，请参考 [这里](../legacy/orm)。\n\n:::\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 和老写法的区别\n\n旧模块为 `@midwayjs/orm` ，新模块为 `@midwayjs/typeorm`，区别如下：\n\n- 1、包名不同\n- 2、在 `src/config.default` 的部分配置调整\n  - 2.1 配置文件中的 key 不同 （orm => typeorm）\n  - 2.2修改为数据源的形式 `typeorm.dataSource`\n  - 2.3 实体模型类或者实体模型类的路径,需要在数据源的 `entities` 字段中声明\n  - 2.4 Subscriber 需要在数据源的 `subscribers` 字段中声明\n- 3、不再使用 `EntityModel` 装饰器，直接使用 typeorm 提供的能力\n\n\n\n## 安装组件\n\n\n安装 typeorm 组件，提供数据库 ORM 能力。\n\n\n```bash\n$ npm i @midwayjs/typeorm@4 typeorm@^1.0.0 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/typeorm\": \"^4.0.0\",\n    \"typeorm\": \"^1.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n## 引入组件\n\n\n在 `src/configuration.ts` 引入 orm 组件，示例如下。\n\n```typescript\n// configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as orm from '@midwayjs/typeorm';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    orm  \t\t\t\t\t\t\t\t\t\t\t\t\t\t// 加载 typeorm 组件\n  ],\n  importConfigs: [\n  \tjoin(__dirname, './config')\n  ]\n})\nexport class MainConfiguration {\n\n}\n```\n\n\n## 安装数据库 Driver\n\n\n常用数据库驱动如下，选择你对应连接的数据库类型安装：\n```bash\n# for MySQL or MariaDB，也可以使用 mysql2 替代\nnpm install mysql --save\nnpm install mysql2 --save\n\n# for PostgreSQL or CockroachDB\nnpm install pg --save\n\n# for SQLite\nnpm install better-sqlite3 --save\n\n# for Microsoft SQL Server\nnpm install mssql --save\n\n# for sql.js\nnpm install sql.js --save\n\n# for Oracle\nnpm install oracledb --save\n\n# for MongoDB(experimental)\nnpm install mongodb --save\n```\n\n:::info\n\n-  Oracle driver 比较特殊，需要查看 [文档](https://github.com/oracle/node-oracledb)\n- 不建议使用 typeorm 链接 mongodb，请使用 mongoose 组件\n\n:::\n\n\n\n\n## 简单的目录结构\n\n\n我们以一个简单的项目举例，其他结构请自行参考。\n\n\n```\nMyProject\n├── src              \t\t\t\t\t\t\t    // TS 根目录\n│   ├── config\n│   │   └── config.default.ts \t\t    // 应用配置文件\n│   ├── entity       \t\t\t\t\t\t\t    // 实体（数据库 Model) 目录\n│   │   └── photo.entity.ts  \t\t\t\t\t// 实体文件\n│   │   └── photoMetadata.entity.ts\n│   ├── configuration.ts     \t\t\t    // Midway 配置文件\n│   └── service      \t\t\t\t\t\t\t    // 其他的服务目录\n├── .gitignore\n├── package.json\n├── README.md\n└── tsconfig.json\n```\n\n\n\n\n在这里，我们的数据库实体主要放在 `entity` 目录（非强制），这只是一个简单的约定。\n\n\n\n## 入门\n\n下面，我们将以 mysql 举例。\n\n\n\n\n### 1、创建 Model\n\n\n我们通过模型和数据库关联，在应用中的模型就是数据库表，在 TypeORM 中，模型是和实体绑定的，每一个实体（Entity) 文件，即是 Model，也是实体（Entity）。\n\n\n在示例中，需要一个实体，我们这里拿 `photo` 举例。新建 entity 目录，在其中添加实体文件 `photo.entity.ts` ，一个简单的实体如下。\n```typescript\n// entity/photo.entity.ts\nexport class Photo {\n  id: number;\n  name: string;\n  description: string;\n  filename: string;\n  views: number;\n  isPublished: boolean;\n}\n```\n要注意，这里的实体文件的每一个属性，其实是和数据库表一一对应的，基于现有的数据库表，我们往上添加内容。\n\n\n### 2、定义实体模型\n\n\n我们使用 `Entity` 来定义一个实体模型类。\n```typescript\n// entity/photo.entity.ts\nimport { Entity } from 'typeorm';\n\n@Entity('photo')\nexport class Photo {\n  id: number;\n  name: string;\n  description: string;\n  filename: string;\n  views: number;\n  isPublished: boolean;\n}\n```\n\n如果表名和当前的实体名不同，可以在参数中指定。\n```typescript\n// entity/photo.entity.ts\nimport { Entity } from 'typeorm';\n\n@Entity('photo_table_name')\nexport class Photo {\n  id: number;\n  name: string;\n  description: string;\n  filename: string;\n  views: number;\n  isPublished: boolean;\n}\n```\n\n\n这些实体列也可以使用 [typeorm_generator](/docs/tool/typeorm_generator) 工具生成。\n\n\n### 3、添加数据库列\n\n\n通过 typeorm 提供的 `@Column` 装饰器来修饰属性，每一个属性对应一个列。\n\n\n```typescript\n// entity/photo.entity.ts\nimport { Entity, Column } from 'typeorm';\n\n@Entity()\nexport class Photo {\n\n  @Column()\n  id: number;\n\n  @Column()\n  name: string;\n\n  @Column()\n  description: string;\n\n  @Column()\n  filename: string;\n\n  @Column()\n  views: number;\n\n  @Column()\n  isPublished: boolean;\n\n}\n```\n\n现在 `id` ， `name` ， `description` ，`filename` ， `views` ， `isPublished` 列将添加到 `photo`  表中。数据库中的列类型是根据您使用的属性类型推断出来的，例如 number 将转换为整数，将字符串转换为 varchar，将布尔值转换为 bool，等等。但是您可以通过在 `@Column`装饰器中显式指定列类型来使用数据库支持的任何列类型。\n\n\n我们生成了带有列的数据库表，但是还剩下一件事。每个数据库表必须具有带主键的列。\n\n\n数据库列包括更多的列选项（ColumnOptions），比如修改列名，指定列类型，列长度等，更多的选项请参考 [官方文档](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/entities.md#%E5%88%97%E9%80%89%E9%A1%B9)。\n\n\n\n\n### 4、创建主键列\n\n\n每个实体必须至少具有一个主键列。要使列成为主键，您需要使用 `@PrimaryColumn`  装饰器。\n\n\n```typescript\n// entity/photo.entity.ts\nimport { Entity, Column, PrimaryColumn } from 'typeorm';\n\n@Entity()\nexport class Photo {\n\n  @PrimaryColumn()\n  id: number;\n\n  @Column()\n  name: string;\n\n  @Column()\n  description: string;\n\n  @Column()\n  filename: string;\n\n  @Column()\n  views: number;\n\n  @Column()\n  isPublished: boolean;\n\n}\n```\n### 5、创建自增主键列\n\n\n现在，如果要设置自增的 id 列，需要将 `@PrimaryColumn` 装饰器更改为 `@PrimaryGeneratedColumn`  装饰器：\n```typescript\n// entity/photo.entity.ts\nimport { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class Photo {\n\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column()\n  name: string;\n\n  @Column()\n  description: string;\n\n  @Column()\n  filename: string;\n\n  @Column()\n  views: number;\n\n  @Column()\n  isPublished: boolean;\n\n}\n```\n\n\n### 6、列数据类型\n\n\n接下来，让我们调整数据类型。默认情况下，字符串映射到类似 `varchar（255）` 的类型（取决于数据库类型）。 Number 映射为类似整数的类型（取决于数据库类型）。但是我们不希望所有列都限制为 varchars 或整数，这个时候可以做一些修改。\n\n\n```typescript\n// entity/photo.entity.ts\nimport { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class Photo {\n\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column({\n  \tlength: 100\n  })\n  name: string;\n\n  @Column('text')\n  description: string;\n\n  @Column()\n  filename: string;\n\n  @Column(\"double\")\n  views: number;\n\n  @Column()\n  isPublished: boolean;\n}\n```\n\n\n示例，不同列名\n```typescript\n@Column({\n  length: 100,\n  name: 'custom_name'\n})\nname: string;\n```\n\n\n此外还有有几种特殊的列类型可以使用：\n\n\n- `@CreateDateColumn` 是一个特殊列，自动为实体插入日期。\n- `@UpdateDateColumn` 是一个特殊列，在每次调用实体管理器或存储库的save时，自动更新实体日期。\n- `@VersionColumn` 是一个特殊列，在每次调用实体管理器或存储库的save时自动增长实体版本（增量编号）。\n- `@DeleteDateColumn` 是一个特殊列，会在调用 soft-delete（软删除）时自动设置实体的删除时间。\n\n比如：\n\n```typescript\n  @CreateDateColumn({\n    type: 'timestamp',\n  })\n  createdDate: Date;\n```\n\n列类型是特定于数据库的。您可以设置数据库支持的任何列类型。有关支持的列类型的更多信息，请参见[此处](https://github.com/typeorm/typeorm/blob/master/docs/entities.md#column-types)。\n\n:::tip\n\n`CreateDateColumn` 和 `UpdateDateColumn` 是依靠第一次同步表结构时，创建列上的默认数据完成的插入日期功能，如果是自己创建的表，需要自行在列上加入默认数据。\n\n:::\n\n\n\n\n### 7、配置连接信息和实体模型\n\n\n请参考 [配置](/docs/env_config) 章节，增加配置文件。\n\n\n然后在 `config.default.ts`  中配置数据库连接信息。\n```typescript\n// src/config/config.default.ts\nimport { Photo } from '../entity/photo.entity';\n\nexport default {\n  // ...\n  typeorm: {\n    dataSource: {\n      default: {\n        /**\n         * 单数据库实例\n         */\n        type: 'mysql',\n        host: '*******',\n        port: 3306,\n        username: '*******',\n        password: '*******',\n        database: undefined,\n        synchronize: false,\t\t    // 如果第一次使用，不存在表，有同步的需求可以写 true，注意会丢数据\n        logging: false,\n\n        // 配置实体模型\n        entities: [Photo],\n\n        // 支持如下的扫描形式，为了兼容我们可以同时进行.js和.ts匹配\n        entities: [\n          'entity',                 // 特定目录\n          '**/*.entity.{j,t}s',     // 通配加后缀匹配\n        ]\n      }\n    }\n  },\n}\n```\n:::tip\n\n- 1. 如果使用的数据库已经有表结构同步的功能，比如云数据库，最好不要开启。如果一定要使用，synchronize 配置最好仅在开发阶段，或者第一次使用，避免造成一致性问题。\n- 2. `entities` 字段配置已经经过框架处理，该字段配置请不要参考原始文档。\n:::\n\n\n `type` 字段你可以使用其他的数据库类型，包括`mysql`, `mariadb`, `postgres`, `cockroachdb`, `better-sqlite3`, `mssql`, `oracle`, `cordova`, `nativescript`, `react-native`, `expo`, or `mongodb`\n\n\n 比如 better-sqlite3，需要以下信息。\n\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  typeorm: {\n    dataSource: {\n      default: {\n        type: 'better-sqlite3',\n        database: path.join(__dirname, '../../test.sqlite'),\n        synchronize: true,\n        logging: true,\n        // ...\n      }\n    }\n  },\n}\n```\n\n\n:::info\n注意：synchronize 字段用于同步表结构。使用 `synchronize: true` 进行生产模式同步是不安全的，在上线后，请把这个字段设置为 false。\n:::\n\n\n### 8、使用 Model 插入数据库数据\n\n\n在常见的 Midway 文件中，使用 `@InjectEntityModel` 装饰器注入我们配置好的 Model。我们所需要做的只是：\n\n\n- 1、创建实体对象\n- 2、执行 `save()`\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from '../entity/photo.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  // save\n  async savePhoto() {\n    // create a entity object\n    let photo = new Photo();\n    photo.name = 'Me and Bears';\n    photo.description = 'I am near polar bears';\n    photo.filename = 'photo-with-bears.jpg';\n    photo.views = 1;\n    photo.isPublished = true;\n\n    // save entity\n    const photoResult = await this.photoModel.save(photo);\n\n    // save success\n    console.log('photo id = ', photoResult.id);\n  }\n}\n```\n\n\n### 9、查询数据\n\n更多的查询参数，请查询 [find文档](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/find-options.md)。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from '../entity/photo.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  // find\n  async findPhotos() {\n\n    // find All\n    let allPhotos = await this.photoModel.find({});\n    console.log(\"All photos from the db: \", allPhotos);\n\n    // find first\n    let firstPhoto = await this.photoModel.findOne({\n      where: {\n        id: 1\n      }\n    });\n    console.log(\"First photo from the db: \", firstPhoto);\n\n    // find one by name\n    let meAndBearsPhoto = await this.photoModel.findOne({\n      where: { name: \"Me and Bears\" }\n    });\n    console.log(\"Me and Bears photo from the db: \", meAndBearsPhoto);\n\n    // find by views\n    let allViewedPhotos = await this.photoModel.find({\n      where: { views: 1 }\n    });\n    console.log(\"All viewed photos: \", allViewedPhotos);\n\n    let allPublishedPhotos = await this.photoModel.find({\n      where: { isPublished: true }\n    });\n    console.log(\"All published photos: \", allPublishedPhotos);\n\n  \t// find and get count\n    let [allPhotos, photosCount] = await this.photoModel.findAndCount({});\n    console.log(\"All photos: \", allPhotos);\n    console.log(\"Photos count: \", photosCount);\n\n  }\n}\n\n```\n\n\n### 10、更新数据库\n\n\n现在，让我们从数据库中加载一个 Photo，对其进行更新并保存。\n\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from '../entity/photo.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  async updatePhoto() {\n\n    let photoToUpdate = await this.photoModel.findOne({\n      where: {\n        id: 1,\n      },\n    });\n    photoToUpdate.name = \"Me, my friends and polar bears\";\n\n    await this.photoModel.save(photoToUpdate);\n  }\n}\n```\n\n### 11、删除数据\n\n`remove` 用于删除给定的实体或实体数组。`delete` 用于按给定的 ID 或者条件删除。\n\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from '../entity/photo.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  async updatePhoto() {\n    /*...*/\n    const photo = await this.photoModel.findOne({\n      where: {\n        id: 1,\n      },\n    });\n\n    // 删除单个\n    await this.photoModel.remove(photo)\n    // 删除多个\n    await this.photoModel.remove([photo1, photo2, photo3]);\n\n    // 按 id 删除\n    await this.photoModel.delete(1);\n    await this.photoModel.delete([1, 2, 3]);\n    await this.photoModel.delete({ name: \"Timber\" });\n  }\n}\n```\n现在，ID = 1的 Photo 将从数据库中删除。\n\n\n此外还有软删除的方法。\n```typescript\nawait this.photoModel.softDelete(1);\n// 使用 restore 方法恢复;\nawait this.photoModel.restore(1);\n```\n\n\n### 12、创建一对一关联\n\n\n让我们与另一个类创建一对一的关系。让我们在 `entity/photoMetadata.entity.ts`  中创建一个新类。这个类包含 photo 的其他元信息。\n\n\n```typescript\nimport { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn } from 'typeorm';\nimport { Photo } from './photo.entity';\n\n@Entity()\nexport class PhotoMetadata {\n\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column(\"int\")\n  height: number;\n\n  @Column(\"int\")\n  width: number;\n\n  @Column()\n  orientation: string;\n\n  @Column()\n  compressed: boolean;\n\n  @Column()\n  comment: string;\n\n  @OneToOne(type => Photo)\n  @JoinColumn()\n  photo: Photo;\n\n}\n```\n\n\n在这里，我们使用一个名为 `@OneToOne`  的新装饰器。它允许我们在两个实体之间创建一对一的关系。`type => Photo`是一个函数，它返回我们要与其建立关系的实体的类。\n\n\n由于语言的特殊性，我们被迫使用一个返回类的函数，而不是直接使用该类。我们也可以将其写为 `() => Photo`  ，但是我们使用 `type => Photo`作为惯例来提高代码的可读性。类型变量本身不包含任何内容。\n\n\n我们还添加了一个 `@JoinColumn`装饰器，它指示关系的这一侧将拥有该关系。关系可以是单向或双向的。关系只有一方可以拥有。关系的所有者端需要使用@JoinColumn装饰器。 如果您运行该应用程序，则会看到一个新生成的表，该表将包含一列，其中包含用于 Photo 关系的外键。\n\n\n```\n+-------------+--------------+----------------------------+\n|                     photo_metadata                      |\n+-------------+--------------+----------------------------+\n| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |\n| height      | int(11)      |                            |\n| width       | int(11)      |                            |\n| comment     | varchar(255) |                            |\n| compressed  | boolean      |                            |\n| orientation | varchar(255) |                            |\n| photoId     | int(11)      | FOREIGN KEY                |\n+-------------+--------------+----------------------------+\n```\n\n\n接下去我们要在代码中关联他们。\n\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from './entity/photo.entity';\nimport { PhotoMetadata } from './entity/photoMetadata.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  @InjectEntityModel(PhotoMetadata)\n  photoMetadataModel: Repository<PhotoMetadata>;\n\n  async updatePhoto() {\n\n  // create a photo\n    let photo = new Photo();\n    photo.name = \"Me and Bears\";\n    photo.description = \"I am near polar bears\";\n    photo.filename = \"photo-with-bears.jpg\";\n    photo.isPublished = true;\n\n    // create a photo metadata\n    let metadata = new PhotoMetadata();\n    metadata.height = 640;\n    metadata.width = 480;\n    metadata.compressed = true;\n    metadata.comment = \"cybershoot\";\n    metadata.orientation = \"portrait\";\n    metadata.photo = photo; // this way we connect them\n\n\n    // first we should save a photo\n    await this.photoModel.save(photo);\n\n    // photo is saved. Now we need to save a photo metadata\n    await this.photoMetadataModel.save(metadata);\n\n    // done\n    console.log(\"Metadata is saved, and relation between metadata and photo is created in the database too\");\n  }\n}\n```\n\n\n### 13、反向关系映射\n\n\n关系映射可以是单向或双向的。当在 PhotoMetadata  和 Photo之间的关系是单向的。关系的所有者是PhotoMetadata，而 Photo对 PhotoMetadata 是一无所知的。这使得从 Photo 端访问 PhotoMetadata 变得很复杂。若要解决此问题，我们添加一个反向的关系映射，使  PhotoMetadata  和 Photo之间变成双向关联。让我们修改我们的实体。\n\n\n```typescript\nimport { Entity } from 'typeorm';\nimport { Column, PrimaryGeneratedColumn, OneToOne, JoinColumn } from 'typeorm';\nimport { Photo } from './photo';\n\n@Entity()\nexport class PhotoMetadata {\n\n  /* ... other columns */\n\n  @OneToOne(type => Photo, photo => photo.metadata)\n  @JoinColumn()\n  photo: Photo;\n}\n```\n```typescript\nimport { Entity } from 'typeorm';\nimport { Entity, Column, PrimaryGeneratedColumn, OneToOne } from 'typeorm';\nimport { PhotoMetadata } from './photoMetadata.entity';\n\n@Entity()\nexport class Photo {\n\n  /* ... other columns */\n\n  @OneToOne(type => PhotoMetadata, photoMetadata => photoMetadata.photo)\n  metadata: PhotoMetadata;\n}\n```\n`photo => photo.metadata` 是一个返回反向映射关系的函数。在这里，我们显式声明 Photo 类的 metadata 属性用于关联 PhotoMetadata。除了传递返回 photo 属性的函数外，您还可以直接将字符串传递给 `@OneToOne`  装饰器，例如 `“metadata”` 。但是我们使用了这种函数回调的方法来让我们的代码写法更简单。\n\n\n请注意，只会在关系映射的一侧使用 `@JoinColumn`  装饰器。无论您放置此装饰器的哪一侧，都是关系的所有者。关系的拥有方在数据库中包含带有外键的列。\n\n\n### 14、加载对象及其依赖关系\n\n\n现在，让我们尝试在单个查询中一起加载出  Photo 和 PhotoMetadata。有两种方法可以执行此操作，使用  `find *`   方法或使用 `QueryBuilder` 功能。让我们首先使用 `find *` 方法。 `find *`  方法允许您使用 `FindOneOptions`  / `FindManyOptions`  接口指定对象。\n\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from './entity/photo.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  // find\n  async findPhoto() {\n\t\t/*...*/\n    let photos = await this.photoModel.find({ relations: [ 'metadata' ] }); // typeorm@0.2.x\n  }\n}\n\n```\n在这里，photos  的值是一个数组，包含了整个数据库的查询结果，并且每个 photo 对象都包含其关联的 metadata 属性。在[此文档](https://github.com/typeorm/typeorm/blob/master/docs/find-options.md)中了解有关 `Find Options` 的更多信息。\n\n\n使用 `Find Options` 很简单，但如果需要更复杂的查询，则应改用 `QueryBuilder` 。 `QueryBuilder`   允许以优雅的方式使用更复杂的查询。\n\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from './entity/photo.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  // find\n  async findPhoto() {\n\t\t/*...*/\n    let photos = await this.photoModel\n      .createQueryBuilder('photo')\n      .innerJoinAndSelect('photo.metadata', 'metadata')\n      .getMany();\n  }\n}\n```\n`QueryBuilder`允许创建和执行几乎任何复杂的 SQL 查询。使用 `QueryBuilder`  时，请像创建  SQL 查询一样思考。在此示例中，“photo”  和 “metadata” 是应用于所选 photos  的别名。您可以使用别名来访问所选数据的列和属性。\n\n\n### 15、使用级联操作自动保存关联对象\n\n\n在我们希望在每次保存另一个对象时都自动保存关联的对象，这个时候可以在关系中设置级联。让我们稍微更改照片的 `@OneToOne`  装饰器。\n\n\n```typescript\nexport class Photo {\n  /// ... other columns\n\n  @OneToOne(type => PhotoMetadata, metadata => metadata.photo, {\n    cascade: true,\n  })\n  metadata: PhotoMetadata;\n}\n```\n使用 `cascade` 允许我们现在不再单独保存 Photo 和 PhotoMetadata，由于级联选项，元数据对象将被自动保存。\n\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from './entity/photo.entity';\nimport { PhotoMetadata } from './entity/photoMetadata.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  async updatePhoto() {\n\n   // create photo object\n    let photo = new Photo();\n    photo.name = \"Me and Bears\";\n    photo.description = \"I am near polar bears\";\n    photo.filename = \"photo-with-bears.jpg\";\n    photo.isPublished = true;\n\n    // create photo metadata object\n    let metadata = new PhotoMetadata();\n    metadata.height = 640;\n    metadata.width = 480;\n    metadata.compressed = true;\n    metadata.comment = \"cybershoot\";\n    metadata.orientation = \"portrait\";\n\n    photo.metadata = metadata;  // this way we connect them\n\n    // save a photo also save the metadata\n    await this.photoModel.save(photo);\n\n    // done\n    console.log(\"Photo is saved, photo metadata is saved too\");\n  }\n}\n```\n\n\n注意，我们现在设置 Photo 的元数据，而不需要像之前那样设置元数据的 Photo 属性。这仅当您从 Photo 这边将 Photo 连接到 PhotoMetadata 时，级联功能才有效。如果在 PhotoMetadata 侧设置，则不会自动保存。\n\n\n### 16、创建多对一/一对多关联\n\n\n让我们创建一个多对一/一对多关系。假设一张照片有一个作者，每个作者可以有很多照片。首先，让我们创建一个 Author 类：\n```typescript\nimport { Entity } from 'typeorm';\nimport { Column, PrimaryGeneratedColumn, OneToMany, JoinColumn } from 'typeorm';\nimport { Photo } from './entity/photo.entity';\n\n@Entity()\nexport class Author {\n\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column()\n  name: string;\n\n  @OneToMany(type => Photo, photo => photo.author) // note: we will create author property in the Photo class below\n  photos: Photo[];\n}\n```\n`Author`  包含了一个反向关系。 `OneToMany`  和 `ManyToOne` 需要成对出现。\n\n\n现在，将关系的所有者添加到 Photo 实体中：\n```typescript\nimport { Entity } from 'typeorm';\nimport { Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';\nimport { PhotoMetadata } from './photoMetadata.entity';\nimport { Author } from './author.entity';\n\n@Entity()\nexport class Photo {\n\n  /* ... other columns */\n\n  @ManyToOne(type => Author, author => author.photos)\n  author: Author;\n}\n```\n\n\n在多对一/一对多关系中，所有者方始终是多对一。这意味着使用 `@ManyToOne`  的类将存储相关对象的 ID。\n\n\n运行应用程序后，ORM 将创建 `author` 表：\n\n\n```\n+-------------+--------------+----------------------------+\n|                          author                         |\n+-------------+--------------+----------------------------+\n| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |\n| name        | varchar(255) |                            |\n+-------------+--------------+----------------------------+\n```\n它还将修改 `photo` 表，添加新的 `author`  列并为其创建外键：\n```\n+-------------+--------------+----------------------------+\n|                         photo                           |\n+-------------+--------------+----------------------------+\n| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |\n| name        | varchar(255) |                            |\n| description | varchar(255) |                            |\n| filename    | varchar(255) |                            |\n| isPublished | boolean      |                            |\n| authorId    | int(11)      | FOREIGN KEY                |\n+-------------+--------------+----------------------------+\n```\n\n\n### 17、创建多对多关联\n\n\n让我们创建一个多对一/多对多关系。假设一张照片可以在许多相册中，并且每个相册可以包含许多照片。让我们创建一个 `Album` 类。\n\n\n```typescript\nimport { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';\n\n@Entity()\nexport class Album {\n\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column()\n  name: string;\n\n  @ManyToMany(type => Photo, photo => photo.albums)\n  @JoinTable()\n  photos: Photo[];\n}\n```\n\n\n`@JoinTable` 用来指明这是关系的所有者。\n\n\n现在，将反向关联添加到 `Photo` 。\n\n\n```typescript\nexport class Photo {\n  /// ... other columns\n\n  @ManyToMany(type => Album, album => album.photos)\n  albums: Album[];\n}\n```\n运行应用程序后，ORM将创建一个 album_photos_photo_albums 联结表：\n\n\n```\n+-------------+--------------+----------------------------+\n|                album_photos_photo_albums                |\n+-------------+--------------+----------------------------+\n| album_id    | int(11)      | PRIMARY KEY FOREIGN KEY    |\n| photo_id    | int(11)      | PRIMARY KEY FOREIGN KEY    |\n+-------------+--------------+----------------------------+\n```\n\n\n现在，让我们将相册和照片插入数据库：\n\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from './entity/photo.entity';\nimport { Album } from './entity/album.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  @InjectEntityModel(Album)\n  albumModel: Repository<Album>\n\n  async updatePhoto() {\n\n    // create a few albums\n    let album1 = new Album();\n    album1.name = \"Bears\";\n    await this.albumModel.save(album1);\n\n    let album2 = new Album();\n    album2.name = \"Me\";\n    await this.albumModel.save(album2);\n\n    // create a few photos\n    let photo = new Photo();\n    photo.name = \"Me and Bears\";\n    photo.description = \"I am near polar bears\";\n    photo.filename = \"photo-with-bears.jpg\";\n    photo.albums = [album1, album2];\n    await this.photoModel.save(photo);\n\n\n    // now our photo is saved and albums are attached to it\n    // now lets load them:\n    const loadedPhoto = await this.photoModel.findOne(1, { relations: [\"albums\"] });  // typeorm@0.2.x\n  }\n}\n```\n`loadedPhoto` 的值为：\n```json\n{\n  id: 1,\n  name: \"Me and Bears\",\n  description: \"I am near polar bears\",\n  filename: \"photo-with-bears.jpg\",\n  albums: [{\n    id: 1,\n    name: \"Bears\"\n  }, {\n    id: 2,\n    name: \"Me\"\n  }]\n}\n```\n\n### 18、使用 QueryBuilder\n\n\n您可以使用QueryBuilder来构建几乎任何复杂的SQL查询。例如，您可以这样做：\n\n\n```typescript\nlet photos = await this.photoModel\n    .createQueryBuilder(\"photo\") // first argument is an alias. Alias is what you are selecting - photos. You must specify it.\n    .innerJoinAndSelect(\"photo.metadata\", \"metadata\")\n    .leftJoinAndSelect(\"photo.albums\", \"album\")\n    .where(\"photo.isPublished = true\")\n    .andWhere(\"(photo.name = :photoName OR photo.name = :bearName)\")\n    .orderBy(\"photo.id\", \"DESC\")\n    .skip(5)\n    .take(10)\n    .setParameters({ photoName: \"My\", bearName: \"Mishka\" })\n    .getMany();\n```\n该查询选择所有带有 “My” 或 “Mishka” 名称的已发布照片。它将从位置 5 开始返回结果（分页偏移），并且将仅选择 10 个结果（分页限制）。选择结果将按 ID 降序排列。该照片的相册将 left-Joined，元数据将自动关联。\n\n\n您将在应用程序中大量使用查询生成器。在 [此处](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/select-query-builder.md) 了解有关QueryBuilder的更多信息。\n\n\n### 19、Event Subscriber\n\n\ntypeorm 提供了一个事件订阅机制，方便在做一些数据库操作时的日志输出，为此 midway 提供了一个 `EventSubscriberModel` 装饰器，用来标注事件订阅类，代码如下。\n\n\n```typescript\nimport { EventSubscriberModel } from '@midwayjs/typeorm';\nimport { EntitySubscriberInterface, InsertEvent, UpdateEvent, RemoveEvent } from 'typeorm';\n\n@EventSubscriberModel()\nexport class EverythingSubscriber implements EntitySubscriberInterface {\n\n  /**\n   * Called before entity insertion.\n   */\n  beforeInsert(event: InsertEvent<any>) {\n    console.log(`BEFORE ENTITY INSERTED: `, event.entity);\n  }\n\n  /**\n   * Called before entity insertion.\n   */\n  beforeUpdate(event: UpdateEvent<any>) {\n    console.log(`BEFORE ENTITY UPDATED: `, event.entity);\n  }\n\n  /**\n   * Called before entity insertion.\n   */\n  beforeRemove(event: RemoveEvent<any>) {\n    console.log(`BEFORE ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity);\n  }\n\n  /**\n   * Called after entity insertion.\n   */\n  afterInsert(event: InsertEvent<any>) {\n    console.log(`AFTER ENTITY INSERTED: `, event.entity);\n  }\n\n  /**\n\t * Called after entity insertion.\n\t */\n  afterUpdate(event: UpdateEvent<any>) {\n    console.log(`AFTER ENTITY UPDATED: `, event.entity);\n  }\n\n  /**\n   * Called after entity insertion.\n   */\n  afterRemove(event: RemoveEvent<any>) {\n    console.log(`AFTER ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity);\n  }\n\n  /**\n   * Called after entity is loaded.\n   */\n  afterLoad(entity: any) {\n    console.log(`AFTER ENTITY LOADED: `, entity);\n  }\n\n}\n```\n\n这个订阅类提供了一些常用的接口，用来在数据库操作时执行一些事情。\n\n同时，我们需要把订阅类加到配置中。\n\n```typescript\n// src/config/config.default.ts\nimport { EverythingSubscriber } from '../event/subscriber';\n\nexport default {\n  // ...\n  typeorm: {\n    dataSource: {\n      default: {\n        // ...\n        entities: [Photo],\n        // 传入订阅类\n        subscribers: [EverythingSubscriber]\n      }\n    }\n  },\n}\n```\n\n\n\n## Repository API\n\n更多 API 请查看 [官网文档](https://github.com/typeorm/typeorm/blob/master/docs/repository-api.md)。\n\n\n\n\n## 高级功能\n### 多数据库支持\n\n\n有时候，我们一个应用中会有多个数据库连接（Connection）的情况，这个时候会有多个配置。我们使用 DataSource 标准的 **对象的形式 **来定义配置。\n\n\n比如下面定义了 `default` 和 `test` 两个数据库连接（Connection）。\n\n\n```typescript\nimport { join } from 'path';\n\nexport default {\n  typeorm: {\n    dataSource: {\n      default: {\n        type: 'better-sqlite3',\n        database: join(__dirname, '../../default.sqlite'),\n        // ...\n      },\n      test: {\n        type: 'mysql',\n        host: '127.0.0.1',\n        port: 3306,\n        // ...\n      }\n    }\n  }\n}\n```\n\n\n在使用时，需要指定模型归属于哪个连接（Connection）。\n```typescript\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { User } from './entity/user.entity';\n\nexport class XXX {\n\n  @InjectEntityModel(User, 'test')\n  testUserModel: Repository<User>;\n\n  //...\n}\n```\n\n\n\n### 列值转换\n\n我们可以在实体定义中处理列值转换。\n\n利用列装饰器的 `transformer` 参数，可以进行出入参的处理，比如对时间格式化。\n\n```typescript\nimport { Entity, Column, CreateDateColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';\nimport * as dayjs from 'dayjs';\n\nconst dateTransformer = {\n  from: (value: Date | number) => {\n    return dayjs(typeof value === 'number' ? value: value.getTime()).format('YYYY-MM-DD HH:mm:ss');\n  },\n  to: () => new Date(),\n};\n\n@Entity()\nexport class Photo {\n  // ...\n\n  @CreateDateColumn({\n    type: 'timestamp',\n    transformer: dateTransformer,\n  })\n  createdAt: Date;\n}\n\n```\n\n\n\n### 指定默认数据源\n\n在包含多个数据源时，可以指定默认的数据源。\n\n```typescript\nexport default {\n  // ...\n  typeorm: {\n    dataSource: {\n      default1: {\n        // ...\n      },\n      default2: {\n        // ...\n      },\n    },\n    // 多个数据源时可以用这个指定默认的数据源\n    defaultDataSourceName: 'default1',\n  },\n};\n```\n\n\n\n### 获取数据源\n\n数据源即创建出的 DataSource 对象，我们可以通过注入内置的数据源管理器来获取。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport { TypeORMDataSourceManager } from '@midwayjs/typeorm';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n\n  async onReady(container: IMidwayContainer) {\n    const dataSourceManager = await container.getAsync(TypeORMDataSourceManager);\n  \tconst conn = dataSourceManager.getDataSource('default');\n    console.log(dataSourceManager.isConnected(conn));\n  }\n}\n```\n\n从 v3.8.0 开始，也可以通过装饰器注入。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport { InjectDataSource } from '@midwayjs/typeorm';\nimport { DataSource } from 'typeorm';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n\n  // 注入默认数据源\n  @InjectDataSource()\n  defaultDataSource: DataSource;\n\n  // 注入自定义数据源\n  @InjectDataSource('default1')\n  customDataSource: DataSource;\n\n  async onReady(container: IMidwayContainer) {\n    // ...\n  }\n}\n```\n\n\n\n### 日志\n\n数据源在未配置日志对象时，组件会自动创建一个 `typeormLogger`，用于保存执行的 SQL 信息，方便排查问题和 SQL 审核。\n\n默认配置为：\n\n```typescript\nexport default {\n  midwayLogger: {\n    clients: {\n      typeormLogger: {\n        fileLogName: 'midway-typeorm.log',\n        enableError: false,\n        level: 'info',\n      },\n    },\n  }\n}\n```\n\n我们可以使用普通日志的配置方式进行调整，如果不希望生成日志，可以配置关闭。\n\n```typescript\nexport default {\n  // ...\n  typeorm: {\n    default: {\n      // 所有数据源关闭\n      logging: false,\n    },\n    dataSource: {\n      default: {\n        // 单个数据源关闭\n        logging: false,\n      },\n    },\n  },\n};\n```\n\n\n\n### 事务\n\ntypeorm 的事务需要先获取到数据源，然后开启事务。\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { TypeORMDataSourceManager } from '@midwayjs/typeorm';\nimport { UserDTO } from '../entity/user';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  dataSourceManager: TypeORMDataSourceManager;\n\n  async updateUser(user: UserDTO) {\n\n    // get dataSource\n    const dataSource = this.dataSourceManager.getDataSource('default');\n\n    // start transaction\n    await dataSource.transaction(async (transactionalEntityManager) => {\n      // run code\n      await transactionalEntityManager.save(UserDTO, user);\n    });\n  }\n\n}\n```\n\n更多的细节，可以参考 [文档](https://github.com/typeorm/typeorm/blob/master/docs/transactions.md)。\n\n\n\n### CLI\n\nTypeORM 默认提供了一个 CLI，用来创建 entity，migration 等，更多文档请查看 [这里](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/using-cli.md)。\n\n由于 TypeORM 的默认配置和 Midway 不同，我们提供了一个简单的修改版本，用于适配 Midway 的数据源配置。\n\n检查安装情况：\n\n```bash\n$ npx mwtypeorm -h\n```\n\n常用的命令有\n\n **创建空 Entity**\n\n将会创建一个 `src/entity/User.ts` 文件。\n\n```bash\n$ npx mwtypeorm entity:create src/entity/User\n```\n\n**创建 Migration**\n\n将会根据现有数据源生成一个 `src/migration/******-photo.entity.ts` 文件。\n\n比如配置如下：\n\n```typescript\nexport default {\n  typeorm: {\n    dataSource: {\n      'default': {\n        // ...\n        entities: [\n          '**/entity/*.entity{.ts,.js}'\n        ],\n        migrations: [\n          '**/migration/*.ts'\n        ],\n      },\n  },\n}\n```\n\n可以执行下面的命令，将修改后的 Entity 生成迁移文件。\n\n```bash\n$ npx mwtypeorm migration:generate -d ./src/config/config.default.ts src/migration/photo\n```\n\n:::caution\n\n注意：上面的 entities 配置由于需要再 CLI 和 Midway 间复用，采用了两者都支持的扫描写法。\n\n:::\n\n\n\n### 关于表结构同步\n\n\n- 如果你已有表结构，想自动创建 Entity，使用 [生成器](https://www.npmjs.com/package/typeorm-model-generator)\n- 如果已经有 Entity 代码，想创建表结构请使用配置中的  `synchronize:  true` ，注意可能会丢失数据\n- 如果已经上线，但是又修改了表结构，可以使用 CLI 中的 `migration:generate`\n\n\n\n## 常见问题\n\n\n### Handshake inactivity timeout\n\n\n一般是网络原因，如果本地出现，可以 ping 但是telnet不通，可以尝试执行如下命令：\n```bash\n$ sudo sysctl -w net.inet.tcp.sack=0\n```\n\n\n\n### 关于 mysql 时间列的时区展示\n\n一般情况下，数据库中保存的是 UTC 时间，如果你希望返回当前时区的时间，可以使用下面的方式\n\n**1、检查 mysql 数据库所在的环境**\n\n比如下面默认的时区其实就是系统 UTC 时间，可以调整为 `+08:00`。\n\n```text\nmysql> show global variables like '%time_zone%';\n+------------------+--------+\n| Variable_name    | Value  |\n+------------------+--------+\n| system_time_zone | UTC    |\n| time_zone        | SYSTEM |\n+------------------+--------+\n2 rows in set (0.05 sec)\n\n```\n\n**2、检查服务代码部署的环境**\n\n尽量和数据库所在的环境一致，如果不一致，请在配置中设置 `timezone` （设置为和 mysql 一致）。\n\n```typescript\nexport default {\n  typeorm: {\n    dataSource: {\n      default: {\n        type: 'mysql',\n        // ...\n        timezone: '+08:00',\n      },\n    },\n  },\n}\n```\n\n\n\n### 时间列返回字符串\n\n配置 dateStrings 可以使 mysql 返回时间按 DATETIME 格式返回，只对 mysql 生效。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  typeorm: {\n    dataSource: {\n      default: {\n        //...\n        dateStrings: true,\n      }\n    }\n  },\n}\n```\n\n如果使用了 `@CreateDateColumn` 和 `@UpdateDateColumn` ，可以调整实体返回类型。\n\n```typescript\n@UpdateDateColumn({\n  name: \"gmt_modified\",\n  type: 'timestamp'\n})\ngmtModified: string;\n\n@CreateDateColumn({\n  name: \"gmt_create\",\n  type: 'timestamp',\n})\ngmtCreate: string;\n```\n\n\n\n效果如下：\n\n**配置前：**\n\n```typescript\ngmtModified: 2021-12-13T03:49:43.000Z,\ngmtCreate: 2021-12-13T03:49:43.000Z\n```\n**配置后：**\n\n```typescript\ngmtModified: '2021-12-13 11:49:43',\ngmtCreate: '2021-12-13 11:49:43'\n```\n\n\n\n### 同时安装 mysql 和 mysql2\n\n在 node_modules 中同时有 mysql  和 mysql2 时，typeorm 会自动加载 mysql，而不是 mysql2。\n\n这个时候如需使用 mysql2，请指定  driver。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  typeorm: {\n    dataSource: {\n      default: {\n        //...\n        type: 'mysql',\n        driver: require('mysql2'),\n      }\n    }\n  },\n}\n```\n\n\n\n\n###  Cannot read properties of undefined (reading 'getRepository')\n\n一般是配置不正确，可以考虑两方面的配置：\n\n- 1、检查 `config.default.ts` 中的 `entities` 配置是否正确\n- 2、检查 `configuration.ts` 文件，确认是否引入 orm\n","site/docs/extensions/oss.md":"# 阿里云对象存储（OSS）\n\n阿里云对象存储服务（Object Storage Service，简称 OSS），是阿里云提供的海量、安全、低成本、高可靠的云存储服务。其数据设计持久性不低于 99.999999999%，服务设计可用性不低于 99.99%。具有与平台无关的 RESTful API 接口，您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。\n\n`@midwayjs/oss` 组件就是在 midway 体系下用于对接 OSS 服务的 sdk。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 前置条件\n\n\n使用 OSS 组件，你需要提前申请一个 OSS Bucket。Bucket 是 OSS 的存储库的概念，你的文件都将存储在这个库里。\n\n\n- OSS 对象存储官网：[https://www.aliyun.com/product/oss](https://www.aliyun.com/product/oss)\n- 什么是对象存储：[https://www.alibabacloud.com/help/zh/doc-detail/31817.htm](https://www.alibabacloud.com/help/zh/doc-detail/31817.htm)\n\n\n## 安装依赖\n\n`@midwayjs/oss` 是主要的功能包。\n\n```bash\n$ npm i @midwayjs/oss@4 --save\n```\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/oss\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n\n## 引入组件\n\n\n首先，引入 组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as oss from '@midwayjs/oss';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    oss\t\t// 导入 oss 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n\n## 配置 OSS\n\n\nOSS 组件需要配置后才能使用。需要填写 OSS 的 bucket、accessKeyId、accessKeySecret 等必要信息。\n\n\n支持普通 oss 客户端和 oss 集群客户端，基于 [ali-oss](https://github.com/ali-sdk/ali-oss/) 这个包。\n\n\n比如：\n\n**普通的 oss bucket 配置**\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  oss: {\n    // normal oss bucket\n    client: {\n      accessKeyId: 'your access key',\n      accessKeySecret: 'your access secret',\n      bucket: 'your bucket name',\n      endpoint: 'oss-cn-hongkong.aliyuncs.com',\n      timeout: '60s',\n    },\n  },\n}\n```\n\n\n**集群（cluster） 模式的 oss bucket 配置，需要配置多个**\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  oss: {\n    // need to config all bucket information under cluster\n    client: {\n      clusters: [{\n        endpoint: 'host1',\n        accessKeyId: 'id1',\n        accessKeySecret: 'secret1',\n      }, {\n        endpoint: 'host2',\n        accessKeyId: 'id2',\n        accessKeySecret: 'secret2',\n      }],\n      schedule: 'masterSlave', //default is `roundRobin`\n      timeout: '60s',\n    },\n  },\n}\n```\n\n**STS 模式**\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  oss: {\n    // if config.sts == true, oss will create STS client\n    client: {\n      sts: true,\n      accessKeyId: 'your access key',\n      accessKeySecret: 'your access secret',\n    },\n  },\n}\n```\n\n## 使用组件\n\n\n可以直接获取 `OSSService`，然后调用接口，比如，保存文件。\n```typescript\nimport { OSSService } from '@midwayjs/oss';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  ossService: OSSService;\n\n  async saveFile() {\n\n    const localFile = join(__dirname, 'test.log');\n    const result = await this.ossService.put('/test/test.log', localFile);\n\n    // => result.url\n  }\n}\n```\n\n\n如果配置的是 STS 模式，客户端可以使用 `OSSSTSService` 。\n```typescript\nimport { OSSSTSService } from '@midwayjs/oss';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  stsService: OSSSTSService;\n\n  async saveFile() {\n\n    const roleArn = '******';\t\t// 这里是阿里云角色的 arn\n    const result = await this.stsService.assumeRole(roleArn);\n\n    // result.credentials.AccessKeyId\n    // result.credentials.AccessKeySecret;\n    // result.credentials.SecurityToken;\n  }\n}\n```\n\n更多的 OSS 客户端 API，请查看 [OSS 文档](https://github.com/ali-sdk/ali-oss)。\n\n\n## 使用多个 OSS Bucket\n\n\n有些应用需要访问多个 oss bucket，那么就需要配置 `oss.clients`。\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  oss: {\n    clients: {\n      bucket1: {\n        bucket: 'bucket1',\n        // ...\n      },\n      bucket2: {\n        bucket: 'bucket2',\n        // ...\n      },\n    },\n    // client, clients，createInstance 方法共享的配置\n    default: {\n      endpoint: '',\n      accessKeyId: '',\n      accessKeySecret: '',\n    },\n  },\n  // other custom config\n  bucket3: {\n    bucket: 'bucket3',\n    // ...\n  },\n}\n```\n\n可以使用 `OSSServiceFactory` 获取不同的实例。\n\n```typescript\nimport { OSSServiceFactory } from '@midwayjs/oss';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  ossServiceFactory: OSSServiceFactory;\n\n  @Config('bucket3')\n  bucket3Config;\n\n  async saveFile() {\n\n    // 默认获取的类型是 OSSService\n    const bucket1 = this.ossServiceFactory.get('bucket1');\n    const bucket2 = this.ossServiceFactory.get('bucket2');\n\n    // 如果是 STS，需要设置泛型联系\n    // const bucket1 = this.ossServiceFactory.get<OSSSTSService>('bucket1');\n\n    // 会合并 config.bucket3 和 config.oss.default\n    const bucket3 = await this.ossServiceFactory.createInstance(this.bucket3Config, 'bucket3');\n    // 传了名字之后也可以从 factory 中获取\n    bucket3 = this.ossServiceFactory.get('bucket3');\n\n  }\n}\n```\n","site/docs/extensions/passport.md":"# 身份验证\n\n身份验证是大多数 Web 应用程序的重要组成部分。因此 Midway 封装了目前 Nodejs 中最流行的 Passport 库。\n\n相关信息：\n\n| web 支持情况      |     |\n| ----------------- | --- |\n| @midwayjs/koa     | ✅  |\n| @midwayjs/faas    | ✅  |\n| @midwayjs/web     | ✅  |\n| @midwayjs/express | ✅  |\n\n从 v3.4.0 开始 Midway 自行维护 passport，将不再需要引入社区包和类型包。\n\n## 一些概念\n\npassport 是社区使用较多的身份验证库，通过称为策略的可扩展插件进行身份验证请求。\n\n它本身包含几个部分：\n\n- 1、验证的策略，比如 jwt 验证，github 验证，oauth 验证等，passport 最为丰富的也是这块\n- 2、执行策略之后，中间件的逻辑处理和配置，比如成功或者失败后的跳转，报错等\n\n## 安装依赖\n\n安装 `npm i @midwayjs/passport` 和相关策略依赖。\n\n```bash\n## 必选\n$ npm i @midwayjs/passport@4 --save\n\n## 可选\n## 下面安装本地策略\n$ npm i passport-local --save\n$ npm i @types/passport-local --save-dev\n## 下面安装 Github 策略\n$ npm i passport-github --save\n## 下面安装 Jwt 策略\n$ npm i passport-jwt --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/passport\": \"^4.0.0\",\n    // 本地策略\n    \"passport-local\": \"^1.0.0\"\n    // Jwt 策略\n    \"passport-jwt\": \"^4.0.0\",\n    // Github 策略\n    \"passport-github\": \"^1.1.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // 本地策略\n    \"@types/passport-local\": \"^1.0.34\",\n    // Jwt 策略\n    \"@types/passport-jwt\": \"^3.0.6\",\n    // Github 策略\n    \"@types/passport-github\": \"^1.1.7\",\n    // ...\n  }\n}\n```\n\n## 启用组件\n\n首先启用组件。\n\n```typescript\n// src/configuration.ts\n\nimport { join } from 'path';\nimport { ILifeCycle } from '@midwayjs/core';\nimport { Configuration } from '@midwayjs/core';\nimport * as passport from '@midwayjs/passport';\n\n@Configuration({\n  imports: [\n    // ...\n    passport,\n  ],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration implements ILifeCycle {}\n```\n\n## 策略示例\n\n这里我们以使用本地认证策略，和 Jwt 策略作为演示。\n\n### 示例：本地策略\n\n我们以 `passport-local` 来介绍 Passport 策略在 Midway 中如何使用， `passport-local` 的官方文档示例如下，通过 `passport.use` 加载一个策略，策略的验证逻辑是一个 `verify` 方法，包含 callback 参数，其余的策略的参数都在构造器中。\n\n```typescript\npassport.use(\n  // 初始化一个策略\n  new LocalStrategy({\n      usernameField: 'username',\n      passwordField: 'password',\n      passReqToCallback: true,\n      session: false\n    },\n    function verify(username, password, done) {\n      User.findOne({ username: username }, function (err, user) {\n        if (err) { return done(err); }\n        if (!user) { return done(null, false); }\n        if (!user.verifyPassword(password)) { return done(null, false); }\n        return done(null, user);\n      });\n    }\n\t)\n);\n```\n\nMidway 对此进行了改造，通过 `@CustomStrategy` 和 `PassportStrategy` 类继承一个 Passport 现有策略。\n\n异步的 `validate`  方法代替原有的 `verify` 方法，`validate` 方法返回验证后的用户结果，方法的参数和原有对应的策略一致。\n\n在 Midway 中编写的效果如下：\n\n```typescript\n// src/strategy/local.strategy.ts\n\nimport { CustomStrategy, PassportStrategy } from '@midwayjs/passport';\nimport { Strategy, IStrategyOptions } from 'passport-local';\nimport { Repository } from 'typeorm';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { UserEntity } from './user';\nimport * as bcrypt from 'bcrypt';\n\n@CustomStrategy()\nexport class LocalStrategy extends PassportStrategy(Strategy) {\n  @InjectEntityModel(UserEntity)\n  userModel: Repository<UserEntity>;\n\n  // 策略的验证\n  async validate(username, password) {\n    const user = await this.userModel.findOneBy({ username });\n    if (!user) {\n      throw new Error('用户不存在 ' + username);\n    }\n    if (!await bcrypt.compare(password, user.password)) {\n      throw new Error('密码错误 ' + username);\n    }\n\n    return user;\n  }\n\n  // 当前策略的构造器参数\n  getStrategyOptions(): IStrategyOptions {\n    return {\n      usernameField: 'username',\n      passwordField: 'password',\n      passReqToCallback: true,\n      session: false\n    };\n  }\n}\n```\n\n:::tip\n\n注意：validate 方法是社区策略 verify 的 Promise 化替代方法，你无需在最后传递 callback 参数。\n\n:::\n\n在 `passport-local` 的官方文档中，实现完策略后，需要作为中间件加载到业务中，比如：\n\n```typescript\napp.post('/login/password', passport.authenticate('local', {\n  successRedirect: '/',\n  failureRedirect: '/login'\n}));\n```\n\n:::tip\n\n这里的 `local` 是 `passport-local` 内部的名字。\n\n:::\n\n在 Midway 中，也需要将上述实现的 `LocalStrategy` 通过中间件加载。\n\n自定义一个中间件继承 `PassportMiddleware` 扩展出的基础中间件，示例如下。\n\n```typescript\n// src/middleware/local.middleware.ts\n\nimport { Middleware } from '@midwayjs/core';\nimport { PassportMiddleware, AuthenticateOptions } from '@midwayjs/passport';\nimport { LocalStrategy } from '../strategy/local.strategy';\n\n@Middleware()\nexport class LocalPassportMiddleware extends PassportMiddleware(LocalStrategy) {\n  // 设置 AuthenticateOptions\n  getAuthenticateOptions(): Promise<AuthenticateOptions> | AuthenticateOptions {\n    return {\n      failureRedirect: '/login',\n    };\n  }\n}\n```\n将中间件加载到全局或者路由。\n\n```typescript\n// src/controller.ts\nimport { Post, Inject, Controller } from '@midwayjs/core';\nimport { LocalPassportMiddleware } from '../middleware/local.middleware';\n\n@Controller('/')\nexport class LocalController {\n  @Post('/passport/local', { middleware: [LocalPassportMiddleware] })\n  async localPassport() {\n    console.log('local user: ', this.ctx.state.user);\n    return this.ctx.state.user;\n  }\n}\n```\n\n使用 curl 模拟一次请求。\n\n```bash\ncurl -X POST http://localhost:7001/passport/local -d '{\"username\": \"demo\", \"password\": \"1234\"}' -H \"Content-Type: application/json\"\n\n结果 {\"username\": \"demo\", \"password\": \"1234\"}\n```\n\n:::caution\n\n注意：如果将中间件放到全局，记得忽略需要登录的路由，否则请求会死循环。\n\n:::\n\n\n\n### 示例：Jwt 策略\n\n首先需要 **额外安装** 依赖和策略：\n\n```bash\n$ npm i @midwayjs/jwt passport-jwt --save\n```\n\n额外启用 jwt 组件。\n\n```typescript\n// configuration.ts\n\nimport { join } from 'path';\nimport * as jwt from '@midwayjs/jwt';\nimport { Configuration, ILifeCycle } from '@midwayjs/core';\nimport * as passport from '@midwayjs/passport';\n\n@Configuration({\n  imports: [\n    // ...\n    jwt,\n    passport,\n  ],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration implements ILifeCycle {}\n```\n\n然后在配置中设置，默认未加密，请不要把敏感信息存放在 payload 中。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  jwt: {\n    secret: 'xxxxxxxxxxxxxx', // fs.readFileSync('xxxxx.key')\n    expiresIn: '2d', // https://github.com/vercel/ms\n  },\n};\n```\n\n```typescript\n// src/strategy/jwt.strategy.ts\n\nimport { CustomStrategy, PassportStrategy } from '@midwayjs/passport';\nimport { Strategy, ExtractJwt } from 'passport-jwt';\nimport { Config } from '@midwayjs/core';\n\n@CustomStrategy()\nexport class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {\n  @Config('jwt')\n  jwtConfig;\n\n  async validate(payload) {\n    return payload;\n  }\n\n  getStrategyOptions(): any {\n    return {\n      secretOrKey: this.jwtConfig.secret,\n      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),\n    };\n  }\n}\n```\n\n:::tip\n\n注意：validate 方法是社区策略 verify 的 Promise 化替代方法，你无需在最后传递 callback 参数。\n\n:::\n\n```typescript\n// src/middleware/jwt.middleware.ts\n\nimport { Middleware } from '@midwayjs/core';\nimport { PassportMiddleware, AuthenticateOptions } from '@midwayjs/passport';\nimport { JwtStrategy } from '../strategy/jwt.strategy';\n\n@Middleware()\nexport class JwtPassportMiddleware extends PassportMiddleware(JwtStrategy) {\n  getAuthenticateOptions(): Promise<AuthenticateOptions> | AuthenticateOptions {\n    return {};\n  }\n}\n```\n\n```typescript\nimport { Post, Inject, Controller } from '@midwayjs/core';\nimport { Context } from '@midwayjs/koa';\nimport { JwtService } from '@midwayjs/jwt';\nimport { JwtPassportMiddleware } from '../middleware/jwt.middleware';\n\n@Controller('/')\nexport class JwtController {\n  @Inject()\n  jwt: JwtService;\n\n  @Inject()\n  ctx: Context;\n\n  @Post('/passport/jwt', { middleware: [JwtPassportMiddleware] })\n  async jwtPassport() {\n    console.log('jwt user: ', this.ctx.state.user);\n    return this.ctx.state.user;\n  }\n\n  @Post('/jwt')\n  async genJwt() {\n    return {\n      t: await this.jwt.sign({ msg: 'Hello Midway' }),\n    };\n  }\n}\n```\n\n使用 curl 模拟请求\n\n```bash\ncurl -X POST http://127.0.0.1:7001/jwt\n\n结果 {\"t\": \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"}\n\ncurl http://127.0.0.1:7001/passport/jwt -H \"Authorization: Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n\n结果 {\"msg\": \"Hello Midway\",\"iat\": 1635468727,\"exp\": 1635468827}\n\n```\n\n## 自定义其他策略\n\n`@midwayjs/passport` 支持自定义[其他策略](http://www.passportjs.org/packages/)，这里以 Github OAuth 为例。\n首先 `npm i passport-github`，之后编写如下代码：\n\n```typescript\n// github-strategy.ts\n\nimport { CustomStrategy, PassportStrategy } from '@midwayjs/passport';\nimport { Strategy, StrategyOptions } from 'passport-github';\n\nconst GITHUB_CLIENT_ID = 'xxxxxx',\n  GITHUB_CLIENT_SECRET = 'xxxxxxxx';\n\n@CustomStrategy()\nexport class GithubStrategy extends PassportStrategy(Strategy, 'github') {\n  async validate(...payload) {\n    return payload;\n  }\n\n  getStrategyOptions(): StrategyOptions {\n    return {\n      clientID: GITHUB_CLIENT_ID,\n      clientSecret: GITHUB_CLIENT_SECRET,\n      callbackURL: 'https://127.0.0.1:7001/auth/github/cb',\n    };\n  }\n}\n```\n\n```typescript\n// src/middleware/github.middleware.ts\n\nimport { AuthenticateOptions, PassportMiddleware } from '@midwayjs/passport';\nimport { Middleware } from '@midwayjs/core';\nimport { GithubStrategy } from './githubStrategy';\n\n@Middleware()\nexport class GithubPassportMiddleware extends PassportMiddleware(GithubStrategy) {\n  getAuthenticateOptions(): AuthenticateOptions | Promise<AuthenticateOptions> {\n    return {};\n  }\n}\n```\n\n```typescript\n// src/controller/auth.controller.ts\n\nimport { Controller, Get, Inject } from '@midwayjs/core';\nimport { GithubPassportMiddleware } from '../../middleware/github';\n\n@Controller('/oauth')\nexport class AuthController {\n  @Inject()\n  ctx;\n\n  @Get('/github', { middleware: [GithubPassportMiddleware] })\n  async githubOAuth() {}\n\n  @Get('/github/cb', { middleware: [GithubPassportMiddleware] })\n  async githubOAuthCallback() {\n    return this.ctx.state.user;\n  }\n}\n```\n\n## 策略选项\n\n| 选项                | 类型    | 描述                                              |\n| ------------------- | ------- | ------------------------------------------------- |\n| failureRedirect     | string  | 失败跳转的 url                                    |\n| session             | boolean | 默认 true，开启后，会自动将用户信息设置到 session |\n| sessionUserProperty | string  | 设置到 session 上的 key，默认 user                |\n| userProperty        | string  | 设置到 ctx.state 或者 req 上的 key，默认 user     |\n| successRedirect     | string  | 用户认证成功后跳转的地址                          |\n\n## 常见问题\n\n### 1、Failed to serialize user into session\n\n由于 passport 默认会尝试将 user 数据写入 session，如果无需将用户保存到 session，可以将 session 支持关闭。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  passport: {\n    session: false,\n  },\n};\n```\n\n如果明确需要保存数据到 Session，则需要重写 `PassportStrategy`的 User 的序列化方法，请不要保存特别大的数据。\n\n比如自己实现的本地策略。\n\n```typescript\n// src/strategy/local.strategy.ts\n\nimport { CustomStrategy, PassportStrategy } from '@midwayjs/passport';\nimport { Repository } from 'typeorm';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { UserEntity } from './user';\nimport * as bcrypt from 'bcrypt';\n\n@CustomStrategy()\nexport class LocalStrategy extends PassportStrategy(Strategy) {\n  // ...\n  serializeUser(user, done) {\n    // 可以只保存用户名\n    done(null, user.username);\n  }\n\n  deserializeUser(id, done) {\n    // 这里不是异步方法，你可以从其他地方根据用户名，反查用户数据。\n    const user = getUserFromDataBase(id);\n\n    done(null, user);\n  }\n}\n```\n","site/docs/extensions/piscina.md":"# 线程池\n\n线程池组件 `@midwayjs/piscina` 基于 [Piscina](https://github.com/piscinajs/piscina) 提供在 Worker 线程池中执行任务的能力，适合 CPU 密集型计算，不会阻塞主线程。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n## 安装组件\n\n```bash\n$ npm i @midwayjs/piscina@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/piscina\": \"^4.0.0\"\n  }\n}\n```\n\n## 引入组件\n\n将组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as piscina from '@midwayjs/piscina';\n\n@Configuration({\n  imports: [\n    piscina\n  ],\n  // ...\n})\nexport class MainConfiguration {}\n```\n\n## 基础用法\n\n框架基于 [文档](/docs/service_factory) 提供了 `PiscinaService` 和 `PiscinaServiceFactory`。\n\n你可以创建单个或者多个线程池对象来管理线程。\n\n以下是 [Piscina](https://github.com/piscinajs/piscina) 的基础用法。\n\n注入 `PiscinaService` 并调用 `run` 方法执行任务：\n\n```typescript\nimport { Inject, Provide } from '@midwayjs/core';\nimport * as piscina from '@midwayjs/piscina';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  piscinaService: piscina.PiscinaService;\n\n  async heavyTask() {\n    // 调用 compute 函数\n    const result1 = await this.piscinaService.run({\n      handler: 'compute',\n      payload: { value: 10 },\n    });\n    console.log(result1); // 20\n\n    // 调用 heavyComputation 函数\n    const result2 = await this.piscinaService.run({\n      handler: 'heavyComputation',\n      payload: { data: [1, 2, 3, 4, 5] },\n    });\n    console.log(result2); // 15\n  }\n}\n```\n\n## 使用 Midway 容器\n\n如果需要在 Worker 中使用 Midway 的依赖注入功能，我们需要在线程中单独在启动一个 Midway 环境。\n\n\n### 1. 创建线程中的环境\n\n你可以在主项目中单独创建一个目录用来保存线程代码，**必须** 使用 `defineConfiguration` 方法来创建入口。\n\n目录结构如下：\n```\n➜  base-app git:(feat/support_background_task) ✗ tree\n.\n├── package.json\n└── src\n    ├── configuration.ts      ## 主项目入口\n    └── worker                ## worker 目录\n        ├── index.ts\n        └── task.ts\n```\n\n我们可以在 Worker 目录中创建一个新 Midway 入口配置：\n\n```typescript\n// src/worker/index.ts\nimport { defineConfiguration } from '@midwayjs/core/functional';\nimport { CommonJSFileDetector } from '@midwayjs/core';\nimport * as piscina from '@midwayjs/piscina';\n\nexport default defineConfiguration({\n  namespace: 'worker',\n  detector: new CommonJSFileDetector(),\n  imports: [piscina], // 导入 Piscina\n});\n```\n\n### 2. 编写任务类\n\n和 Midway 其他组件类似，使用 `@PiscinaTask` 装饰器定义任务：\n\n```typescript\n// src/worker/task.ts\nimport { PiscinaTask, IPiscinaTask } from '@midwayjs/piscina';\n\n@PiscinaTask('calculate')\nexport class CalculateTask implements IPiscinaTask {\n  async execute(payload: { a: number; b: number; operation: string }) {\n    if (payload.operation === 'add') {\n      return payload.a + payload.b;\n    } else if (payload.operation === 'multiply') {\n      return payload.a * payload.b;\n    }\n    throw new Error('Unknown operation');\n  }\n}\n\n@PiscinaTask('square')\nexport class SquareTask implements IPiscinaTask {\n  async execute(payload: { value: number }) {\n    return payload.value * payload.value;\n  }\n}\n```\n\n`@PiscinaTask` 装饰器的参数为一个字符串，代表堆外暴露的 handler 名称。\n\n\n### 3. 配置主应用\n\n在主应用配置中指定 Worker 目录：\n\n```typescript\n// src/config/config.default.ts\nimport { join } from 'path';\n\nexport default {\n  piscina: {\n    client: {\n      // 指定 worker 入口文件\n      workerFile: join(__dirname, '../worker/index'),\n    },\n  },\n};\n```\n\n主应用需要忽略 Worker 目录，避免冲突：\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport { CommonJSFileDetector } from '@midwayjs/core';\nimport * as piscina from '@midwayjs/piscina';\n\n@Configuration({\n  imports: [piscina],\n  detector: new CommonJSFileDetector({\n    ignore: ['**/worker/**'], // 忽略 worker 目录\n  }),\n})\nexport class MainConfiguration {}\n```\n\n### 4. 执行容器任务\n\n使用 `runInContainer` 方法执行 Worker 容器中的任务：\n\n```typescript\n@Provide()\nexport class UserService {\n  @Inject()\n  piscinaService: piscina.PiscinaService;\n\n  async heavyTask() {\n    // 执行 calculate 任务 - 乘法\n    const result1 = await this.piscinaService.runInContainer('calculate', {\n      a: 5,\n      b: 6,\n      operation: 'multiply',\n    });\n    console.log(result1); // 30\n\n    // 执行 calculate 任务 - 加法\n    const result2 = await this.piscinaService.runInContainer('calculate', {\n      a: 10,\n      b: 20,\n      operation: 'add',\n    });\n    console.log(result2); // 30\n\n    // 执行 square 任务\n    const result3 = await this.piscinaService.runInContainer('square', {\n      value: 7,\n    });\n    console.log(result3); // 49\n  }\n}\n```\n\n## 取消任务\n\n使用 `AbortController` 可以取消正在运行的任务：\n\n```typescript\n@Provide()\nexport class UserService {\n  @Inject()\n  piscinaService: piscina.PiscinaService;\n\n  async cancelableTask() {\n    const abortController = new AbortController();\n\n    // 3 秒后取消任务\n    setTimeout(() => {\n      abortController.abort();\n    }, 3000);\n\n    try {\n      const result = await this.piscinaService.run(\n        {\n          handler: 'longRunning',\n          payload: { duration: 10000 }, // 10 秒的任务\n        },\n        {\n          signal: abortController.signal, // 传递 AbortSignal\n        }\n      );\n    } catch (error) {\n      console.error('任务被取消:', error);\n    }\n  }\n}\n```\n\n## 多个 Worker Pool\n\n可以配置多个 Worker Pool，每个 Pool 执行不同的任务：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  piscina: {\n    clients: {\n      // 计算任务池\n      compute: {\n        workerFile: join(__dirname, '../worker/compute.worker'),\n        maxThreads: 4,\n      },\n      // 图像处理任务池\n      image: {\n        workerFile: join(__dirname, '../worker/image.worker'),\n        maxThreads: 2,\n      },\n    },\n  },\n};\n```\n\n使用不同的 Pool：\n\n```typescript\n@Provide()\nexport class UserService {\n  @Inject()\n  piscinaServiceFactory: piscina.PiscinaServiceFactory;\n\n  async useDifferentPools() {\n    // 使用计算池\n    const computePool = this.piscinaServiceFactory.get('compute');\n    const result1 = await computePool.run({\n      handler: 'compute',\n      payload: { value: 10 },\n    });\n\n    // 使用图像处理池\n    const imagePool = this.piscinaServiceFactory.get('image');\n    const result2 = await imagePool.run({\n      handler: 'process',\n      payload: { imagePath: '/path/to/image.jpg' },\n    });\n  }\n}\n```\n\n## 配置选项\n\n### 常用配置\n\n[Piscina](https://github.com/piscinajs/piscina) 有非常丰富的线程配置。\n\n```typescript\nexport default {\n  piscina: {\n    client: {\n      workerFile: join(__dirname, '../worker/index'),\n      minThreads: 1,           // 最小线程数\n      maxThreads: 4,           // 最大线程数\n      idleTimeout: 60000,      // 空闲超时（毫秒）\n      maxQueue: 'auto',        // 最大队列长度\n      concurrentTasksPerWorker: 1, // 每个 Worker 的并发任务数\n    },\n  },\n};\n```\n\n### 多 Pool 配置\n\n```typescript\nexport default {\n  piscina: {\n    clients: {\n      default: {\n        workerFile: join(__dirname, '../worker/default.worker'),\n      },\n      heavy: {\n        workerFile: join(__dirname, '../worker/heavy.worker'),\n        maxThreads: 8,\n        idleTimeout: 30000,\n      },\n    },\n  },\n};\n```\n\n## Worker 文件路径说明\n\n- 支持 `.ts` 和 `.js` 文件，框架会自动查找\n- 建议使用不带扩展名的路径，框架按 `.js -> .ts -> .mjs -> .cjs` 顺序查找\n- 生产环境编译后会自动找到对应的 `.js` 文件\n\n```typescript\n// 推荐：不带扩展名\nworkerFile: join(__dirname, '../worker/compute.worker')\n\n// 也可以：显式指定扩展名\nworkerFile: join(__dirname, '../worker/compute.worker.js')\nworkerFile: join(__dirname, '../worker/compute.worker.ts')\n```\n\n## API 参考\n\n### PiscinaService\n\n#### run(task, options?)\n\n执行普通 Worker 任务。\n\n```typescript\nawait piscinaService.run(\n  {\n    handler: 'functionName',  // Worker 文件中导出的函数名\n    payload: { /* 数据 */ },  // 传递给函数的参数\n  },\n  {\n    signal: abortController.signal, // 可选：AbortSignal\n    transferList: [],                // 可选：可转移对象列表\n  }\n);\n```\n\n#### runInContainer(handler, payload?, options?)\n\n执行 Worker 容器中的 `@PiscinaTask` 任务。\n\n```typescript\nawait piscinaService.runInContainer(\n  'taskName',              // @PiscinaTask 装饰器的参数\n  { /* 数据 */ },          // 传递给 execute 方法的参数\n  {\n    signal: abortController.signal, // 可选：AbortSignal\n  }\n);\n```\n\n## 最佳实践\n\n### 适用场景\n\n- CPU 密集型计算（数据处理、加密解密、图像处理）\n- 耗时较长的同步操作\n- 需要避免阻塞主线程的场景\n- 需要并行处理大量任务的场景\n\n### 选择合适的模式\n\n**普通 Worker 模式**：\n- 适合简单的纯函数计算\n- 不需要依赖注入\n- 性能开销更小\n\n**Midway 容器模式**：\n- 需要使用依赖注入\n- 需要在 Worker 中使用其他服务\n- 适合复杂的业务逻辑\n\n### 注意事项\n\n1. **数据传递**：传递给 Worker 的数据会被序列化，不支持函数、类实例等不可序列化对象\n2. **线程数配置**：根据 CPU 核心数合理配置 `maxThreads`，避免过多线程导致上下文切换开销\n3. **内存管理**：Worker 线程有独立的内存空间，注意避免内存泄漏\n4. **错误处理**：Worker 中的错误会被捕获并传递回主线程，需要适当处理\n5. **路径问题**：Worker 文件路径建议使用绝对路径（如 `join(__dirname, '../worker/xxx')`）\n\n## 常见问题\n\n### 如何传递大量数据？\n\n对于大型数据（如 ArrayBuffer、Buffer），使用 `transferList` 避免数据复制：\n\n```typescript\nconst buffer = new ArrayBuffer(1024 * 1024);\nawait piscinaService.run(\n  { handler: 'process', payload: buffer },\n  { transferList: [buffer] }\n);\n```\n","site/docs/extensions/pm2.md":"# pm2\n\n[PM2](https://github.com/Unitech/pm2) 是带有内置负载平衡器的 Node.js 应用程序的生产过程管理器。可以利用它来简化很多 Node 应用管理的繁琐任务，如性能监控、自动重启、负载均衡等。\n\n## 安装\n\n我们一般会把 pm2 安装到全局。\n\n```bash\n$ npm install pm2 -g    # 命令行安装 pm2\n```\n\n## 常用命令\n\n```bash\n$ pm2 start   \t# 启动一个服务\n$ pm2 list    \t# 列出当前的服务\n$ pm2 stop\t\t\t# 停止某个服务\n$ pm2 restart\t\t# 重启某个服务\n$ pm2 delete\t\t# 删除某个服务\n$ pm2 logs\t\t\t# 查看服务的输出日志\n```\n\n比如， `pm2 list`，就会以表格显示。\n\n![](https://cdn.nlark.com/yuque/0/2021/png/501408/1616560437389-b193a0d0-b463-49f1-a347-8dec20e7504d.png)\n\npm2 的服务都有一个数组 id，你可以用 id 快速操作它。\n\n比如：\n\n```bash\n$ pm2 stop 1\t\t\t# 停止编号为 1 的服务\n$ pm2 delete 1\t\t# 删除编号为 1 的服务\n```\n\n使用 `--name` 参数添加一个应用名。\n\n```bash\n$ pm2 start ./bootstrap.js --name test_app\n```\n\n然后你可以用这个应用名来操作启停。\n\n```bash\n$ pm2 stop test_app\n$ pm2 restart test_app\n```\n\n## 启动应用\n\nMidway 应用一般使用 `npm run start` 做线上部署。其对应的命令为 `NODE_ENV=production node bootstrap.js`。\n\n:::info\n部署前需要执行编译 npm run build\n:::\n\n对应的 pm2 命令为\n\n```bash\n$ NODE_ENV=production pm2 start ./bootstrap.js --name midway_app -i 4\n```\n\n- --name 用于指定应用名\n- -i 用于指定启动的实例数（进程），会使用 cluster 模式启动\n\n效果如下：\n\n![](https://cdn.nlark.com/yuque/0/2021/png/501408/1616562075255-088155ee-7c4f-4eae-b5c5-db826f78b519.png)\n\n## Docker 容器启动\n\n在 Docker 容器中，后台启动的代码都会被退出，达不到预期效果。pm2 使用另一个命令来支持容器启动。\n\n请将命令修改为 pm2-runtime start 。\n\n```bash\n$ NODE_ENV=production pm2-runtime start ./bootstrap.js --name midway_app -i 4\n```\n\n具体的 pm2 行为请参考 [pm2 容器部署说明](https://www.npmjs.com/package/pm2#container-support)。\n","site/docs/extensions/process_agent.md":"# 进程 Agent\n\nmidway 封装了 `@midwayjs/process-agent` 用来解决 node 场景中，多进程部分场景数据进程间数据不一致，或者无法指定 master 进程执行某个方法。\n\n\n举例：\n\n- 如果使用 pm2、cluster、多进程进行部署方式，使用内存的 cache，那这个 cache 在自己的进程内。\n- prometheus，获取 `/metrics` 的时候，需要把所有进程的数据收集上来，而不是某个进程的\n- 健康检查，如果有4个进程，如果有一个进程不正常了，健康检查应该检查失败。\n\n\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 安装方法\n\n使用方法：\n\n```bash\n$ npm install @midwayjs/process-agent@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/process-agent\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n## 引入组件\n\n`configuration.ts` 使用方法：\n\n```typescript\nimport * as processAgent from '@midwayjs/process-agent';\n\n@Configuration({\n  imports: [\n    // ...\n    processAgent\n  ],\n})\nexport class MainConfiguration {\n}\n\n```\n## 使用方法\n\n业务代码 UserService：\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { TestService } from './test';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  testService: TestService;\n\n  async getUser() {\n    let result = await this.testService.setData(1);\n    return result;\n  }\n}\n\n```\n然后调用 testService 的时候，希望只在主进程执行：\n\n```typescript\nimport { Provide, Scope, ScopeEnum } from '@midwayjs/core';\nimport { RunInPrimary } from '@midwayjs/process-agent';\n\n@Provide()\n@Scope(ScopeEnum.Singleton)\nexport class TestService {\n\n  data: any = 0;\n\n  @RunInPrimary()\n  async setData(b){\n    this.data = b;\n    return this.data;\n  }\n\n  @RunInPrimary()\n  async getData(){\n    return this.data;\n  }\n}\n\n```\n注意，执行返回的数据只限于可序列化的数据，比如普通 JSON，不支持包含方法等无法序列化的数据。\n\n\n## 效果描述\n假设采用pm2 或者 egg-script 等多进程方式启动，假设这是个请求\n\n首先：\n\n- 1、设置 setData\n- 2、然后获取 getData\n\n\n如果没有 RunInPrimary 这个装饰器，那请求可能落在进程2，或者进程3，那可能没有获取更新的data。\n\n所以 RunInPrimary 能确保这个函数执行能落到主进程去。\n\n\n## 功能征集\n如果有其他类似相关功能，觉得可以放在这个包里面的，欢迎在评论区，或者 [issue](https://github.com/midwayjs/midway/issues) 里面帮忙提一下，我们会跟大家一起讨论和实现。\n\n","site/docs/extensions/prometheus.md":"# Prometheus\n\nPrometheus（普罗米修斯）是一个最初在 SoundCloud 上构建的监控系统。 自 2012 年成为社区开源项目，拥有非常活跃的开发人员和用户社区。为强调开源及独立维护，Prometheus 于 2016 年加入云原生云计算基金会（CNCF），成为继 Kubernetes 之后的第二个托管项目。\n\nGrafana 是一个开源的度量分析与可视化套件。纯 Javascript 开发的前端工具，通过访问库（如 InfluxDB），展示自定义报表、显示图表等。Grafana 支持许多不同的数据源。每个数据源都有一个特定的查询编辑器,该编辑器定制的特性和功能是公开的特定数据来源。而 Prometheus 正好是其支持的数据源之一。\n\n本篇介绍了 Midway 如何接入 Grafana + Prometheus。\n\n接入效果如下：\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1617259935548-a2df4339-3229-4391-bd3d-4ba8e6979d4d.png)\n\n## 安装依赖\n\n首先安装 Midway 提供的指标监控组件：\n\n```bash\n$ npm install @midwayjs/prometheus@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/prometheus\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n## 引入组件\n\n在 `configuration.ts` 中，引入这个组件：\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as prometheus from '@midwayjs/prometheus'; // 导入模块\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    prometheus\n  ],\n  importConfigs: [join(__dirname, 'config')],\n})\nexport class MainConfiguration {}\n```\n\n启动我们的应用，此时访问的时候多了一个 `${host}:${port}/metrics` 。\n\n:::info\nPrometheus 基于 HTTP 获取监控数据，请包含 web/koa/express 其中任一组件。\n:::\n\n访问接口，返回如下，里面的内容是当前的指标。\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1617260048533-4f725824-9471-40c9-be8b-6dcbf27d9cca.png)\n\n## 其他配置\n\n指标组件也提供了相关的配置，方便开发者进行配置。\n\n可以在 `config.default.ts` 中，修改 prometheus 的配置。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  prometheus: {\n    labels: {\n      APP_NAME: 'demo_project',\n    },\n  },\n}\n```\n\n更多的配置，我们可以查看定义进行配置。\n\n通过配置，我们例如可以归类哪些 node 是同一个应用，因为我们部署的时候，node 程序是分布式的。例如上面我们加了 APP_NAME，用来区分不同的应用，这样在监控指标中，我们可以区分不同的应用。\n\n## 数据采集\n\n我们前面在 Midway 中引入的组件主要是在 Node 中加了指标模块。接下来我们需要让 Prometheus 来采集我们的指标数据。\n\n如果开发者所在部门已经有 Prometheus+grafana 了，则只需将应用的指标地址上报给 PE 或者通过接口上报即可。此处我们假设大家没有 Prometheus+grafana，然后按照下面描述进行操作。\n\n## 搭建 Prometheus\n\n此处我们通过 docker-compose 来搭建 Prometheus， docker-compose.yml 文件如下：\n\n```yaml\nversion: '2.2'\nservices:\n  tapi:\n    logging:\n      driver: 'json-file'\n      options:\n        max-size: '50m'\n    image: prom/prometheus\n    restart: always\n    volumes:\n      - ./prometheus_data:/prometheus_data:rw\n      - ./prometheus.yml:/etc/prometheus/prometheus.yml\n      - ./targets.json:/etc/prometheus/targets.json\n    command:\n      - '--storage.tsdb.path=/prometheus_data'\n      - '--config.file=/etc/prometheus/prometheus.yml'\n      - '--storage.tsdb.retention=10d'\n      - '--web.enable-lifecycle'\n    ports:\n      - '9090:9090'\n```\n\n`prometheus.yml` 文件如下：\n\n```yaml\nglobal:\n  scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.\n  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.\nscrape_configs:\n  - job_name: 'node'\n    file_sd_configs:\n      - refresh_interval: 1m\n        files:\n          - '/etc/prometheus/targets.json'\n  - job_name: 'prometheus'\n    static_configs:\n      - targets: ['localhost:9090']\n```\n\n然后采集的 `targets.json` 如下：下面文件里面 `${ip}` 替换为 Node.js 应用所在服务器的 ip 地址。\n\n```json\n[\n  {\n    \"targets\": [\"${ip}:7001\"],\n    \"labels\": {\n      \"env\": \"prod\",\n      \"job\": \"api\"\n    }\n  }\n]\n```\n\n\n\n然后我们启动 `docker-compose.yml` 文件，\n\n```bash\n$ docker-compose up\n```\n\n至此，Prometheus 已经会去拉取我们 Node 应用程序的指标数据了。\n\n如果想要更新 target 怎么做：\n修改了这个 targets.json 文件后，通过 prometheus 的 reload 方法进行热加载。\n方法如下：\n\n```bash\n$ curl -X POST http://${prometheus的ip}:9090/-/reload\n```\n\n然后我们可以查看 prometheus 的页面也可以确认是否生效，界面地址：\n\n```text\nhttp://${prometheus的ip}:9090/classic/targets\n```\n\n接下来就是如何展示这些采集到的数据了。\n\n\n\n## 数据展示\n\n我们可以借助 Grafana 来展示我们的数据。\n\n此处我们简单通过 Docker 来搭建一下 Grafana：\n\n```bash\n$ docker run -d --name=grafana -p 3000:3000 grafana/grafana\n```\n也可以将 grafana 和 prometheus 放在一起使用 docker-compose 统一管理。\n\n将 grafana 添加到 `docker-compose.yml`, 示例如下：\n\n```yaml\nversion: '2.2'\nservices:\n  tapi:\n    logging:\n      driver: 'json-file'\n      options:\n        max-size: '50m'\n    image: prom/prometheus\n    restart: always\n    volumes:\n      - ./prometheus_data:/prometheus_data:rw  # prometheus Data mapping directory\n      - ./prometheus.yml:/etc/prometheus/prometheus.yml # prometheus Configuration mapping file\n      - ./targets.json:/etc/prometheus/targets.json\n    command:\n      - '--storage.tsdb.path=/prometheus_data'\n      - '--config.file=/etc/prometheus/prometheus.yml'\n      - '--storage.tsdb.retention=10d'\n      - '--web.enable-lifecycle'\n    ports:\n      - '9090:9090'\n  // highlight-start\n  grafana:\n    image: grafana/grafana\n    container_name: \"grafana0\"\n    ports:\n      - \"3000:3000\"\n    restart: always\n    volumes:\n      - \"./grafana_data:/var/lib/grafana\" # grafana data mapping directory\n      - \"./grafana_log:/var/log/grafana\"  # grafana log mapping directory\n    // highlight-end\n```\n重启 `docker-compose.yml` 文件\n\n```bash\ndocker-compose restart\n```\n![](https://cdn.nlark.com/yuque/0/2022/png/525744/1667300763153-5ee476a7-00ff-4899-92ba-5985995b4862.png)\n\n完成以上任意一种, 然后我们访问 127.0.0.1:3000，默认账号密码：admin:admin。\n\n访问后效果如下：\n\n\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1617260561047-c2643a69-6258-491b-937d-9bfc4558252f.png)\n\n然后我们让 Grafana 接入我们的 Prometheus 数据源：\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1617260581029-1e2e06a8-3054-4ad8-96b5-d50ab9bb1612.png)\n\n然后我们点击 Grafana 添加图表：\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1620725466020-28793a78-c03b-48fa-bf16-0c9c8ecc1a94.png)\n\n这边 ID 选择 14403，然后点击 load，然后点击下一步，然后点击 import 后，就能看到我们刚刚接入的效果了。\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1620725497338-a32a8982-d51f-4e74-b511-dc10a7c66d80.png)\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1620725514630-4f654f10-ef3a-41f7-b403-02832d3ef7d8.png)\n\n这样开发者可以运维自己的 Node 程序了，例如，是否最近引入了一个 NPM 包导致了什么内存泄漏的情况，是否最近有应用重启的情况了。\n\n当然还能支持其他的自定义操作。\n\n\n## Socket-io 场景\n\n使用方法：\n\n```bash\n$ npm install @midwayjs/prometheus-socket-io@4 --save\n```\n\n使用方法：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport { join } from 'path';\nimport * as prometheus from '@midwayjs/prometheus';\nimport * as prometheusSocketIo from '@midwayjs/prometheus-socket-io';\n\n@Configuration({\n  imports: [prometheus, prometheusSocketIo],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {}\n```\n\n然后在/metrics 这边就能看到 socket-io 的数据了。\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1631090438583-d925c13c-371a-4037-9f53-edaa34580aab.png)\n\n一共新增 8 个指标。\n后续会提供 Grafana 的模版 ID 给大家使用。\n\n\n## 功能介绍\n\n- [x] 根据 appName 进行分类\n- [x] 查看不同 path 的 qps 情况\n- [x] 查看不同 status 的分布情况\n- [x] 查询不同 path 的 rt 情况\n- [x] 进程的 CPU 使用情况\n- [x] 进程的内存使用情况\n- [x] 堆栈情况\n- [x] Event Loop\n- [ ] 等\n","site/docs/extensions/rabbitmq.md":"# RabbitMQ\n\n在复杂系统的架构中，会有负责处理消息队列的微服务，如下图：服务A负责产生消息给消息队列，而服务B则负责消费消息队列中的任务。\n\n![image.png](https://img.alicdn.com/imgextra/i3/O1CN01SYMbCz1moVSVLl7S2_!!6000000005001-2-tps-646-251.png)\n\n在Midway中，我们提供了订阅rabbitMQ的能力，专门来满足用户的这类需求。\n\n相关信息：\n\n**订阅服务**\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ❌    |\n\n\n\n## 基础概念\n\n\nRabbitMQ 的概念较为复杂，其基于高级消息队列协议即 Advanced Message Queuing Protocol（AMQP），如果第一次接触请阅读一下相关的参考文档。\n\n\nAMQP 有一些概念，Queue、Exchange 和 Binding 构成了 AMQP 协议的核心，包括：\n\n- Producer：消息生产者，即投递消息的程序。\n- Broker：消息队列服务器实体。\n   - Exchange：消息交换机，它指定消息按什么规则，路由到哪个队列。\n   - Binding：绑定，它的作用就是把 Exchange 和 Queue 按照路由规则绑定起来。\n   - Queue：消息队列载体，每个消息都会被投入到一个或多个队列。\n- Consumer：消息消费者，即接受消息的程序。\n\n\n\n简单的理解，消息通过 Publisher 发布到 Exchange（交换机），Consumer 通过订阅 Queue 来接受消息，Exchange 和 Queue 通过路由做连接。\n\n\n![image.png](https://img.alicdn.com/imgextra/i3/O1CN01fLrucw1FVNbCx4NqG_!!6000000000492-2-tps-700-328.png)\n\n\n\n## 消费者（Consumer）使用方法\n\n\n### 安装依赖\n\n\nMidway 提供了订阅 rabbitMQ 的能力，并能够独立部署和使用。安装 `@midwayjs/rabbitmq` 模块及其定义。\n```bash\n$ npm i @midwayjs/rabbitmq@4 --save\n$ npm i amqplib --save\n$ npm i @types/amqplib --save-dev\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/rabbitmq\": \"^4.0.0\",\n    \"amqplib\": \"^0.10.1\",\n    // ...\n  },\n  \"devDependencies\": {\n    \"@types/amqplib\": \"^0.8.2\",\n    // ...\n  }\n}\n```\n\n## 开启组件\n\n`@midwayjs/rabbmitmq` 可以作为独立主框架使用。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as rabbitmq from '@midwayjs/rabbitmq';\n\n@Configuration({\n  imports: [\n    rabbitmq\n  ],\n  // ...\n})\nexport class MainConfiguration {\n  async onReady() {\n        // ...\n  }\n}\n```\n\n也可以附加在其他的主框架下，比如 `@midwayjs/koa` 。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as rabbitmq from '@midwayjs/rabbitmq';\n\n@Configuration({\n  imports: [\n    koa,\n    rabbitmq\n  ],\n  // ...\n})\nexport class MainConfiguration {\n  async onReady() {\n        // ...\n  }\n}\n```\n\n### 目录结构\n\n\n我们一般把能力分为生产者和消费者，而订阅正是消费者的能力。\n\n\n我们一般把消费者放在 consumer 目录。比如 `src/consumer/userConsumer.ts`  。\n```\n➜  my_midway_app tree\n.\n├── src\n│   ├── consumer\n│   │   └── user.consumer.ts\n│   ├── interface.ts\n│   └── service\n│       └── user.service.ts\n├── test\n├── package.json\n└── tsconfig.json\n```\n代码示例如下。\n\n```typescript\nimport { Consumer, MSListenerType, RabbitMQListener, Inject } from '@midwayjs/core';\nimport { Context } from '@midwayjs/rabbitmq';\nimport { ConsumeMessage } from 'amqplib';\n\n@Consumer(MSListenerType.RABBITMQ)\nexport class UserConsumer {\n\n  @Inject()\n  ctx: Context;\n\n  @RabbitMQListener('tasks')\n  async gotData(msg: ConsumeMessage) {\n    this.ctx.channel.ack(msg);\n  }\n\n}\n\n```\n`@Consumer` 装饰器，提供消费者标识，并且它的参数，指定了某种消费框架的类型，比如，我们这里指定了 `MSListenerType.RABBITMQ` 这个类型，指的就是 rabbitMQ 类型。\n\n\n标识了 `@Consumer` 的类，对方法使用 `@RabbitMQListener` 装饰器后，可以绑定一个 RabbitMQ 的队列（Queue)。\n\n\n方法的参数为接收到的消息，类型为 `ConsumeMessage` 。如果返回值需要确认，则需要对服务端进行 `ack` 操作，明确接收到的数据。\n\n\n如果需要订阅多个队列，可以使用多个方法，也可以使用多个文件。\n\n\n### RabbitMQ 消息上下文\n\n\n订阅 `RabbitMQ` 数据的上下文，和 Web 同样的，其中包含一个 `requestContext` ，和每次接收消息的数据绑定。\n\n\n从 ctx 上可以取到 `channel` ，整个 ctx 的定义为：\n```typescript\nexport type Context = {\n  channel: amqp.Channel;\n  requestContext: IMidwayContainer;\n};\n```\n\n\n可以从框架获取定义\n```typescript\nimport { Context } from '@midwayjs/rabbitmq';\n```\n\n\n### 配置消费者\n\n我们需要在配置中指定 rabbitmq 的地址。\n\n```typescript\n// src/config/config.default\nimport { MidwayConfig } from '@midwayjs/core';\n\nexport default {\n  // ...\n  rabbitmq: {\n    url: 'amqp://localhost'\n  }\n} as MidwayConfig;\n```\n\n更多配置：\n\n| 属性 | 描述 |\n| --- | --- |\n| url | rabbitMQ 的连接信息 |\n| socketOptions | amqplib.connect 的第二个参数 |\n| reconnectTime | 队列断连后的重试时间，默认 10 秒 |\n\n\n\n### Fanout Exchange\n\n\nFanout 是一种特定的交换机，如果满足匹配（binding），就往 Exchange 所绑定的 Queue 发送消息。Fanout Exchange 会忽略 RoutingKey 的设置，直接将 Message 广播到所有绑定的 Queue 中。\n\n即所有订阅该交换机的 Queue 都会收到消息。\n\n比如，下面我们添加了两个 Queue，订阅了相同的交换机。\n```typescript\nimport { Consumer, MSListenerType, RabbitMQListener, Inject, App } from '@midwayjs/core';\nimport { Context, Application } from '@midwayjs/rabbitmq';\nimport { ConsumeMessage } from 'amqplib';\n\n@Consumer(MSListenerType.RABBITMQ)\nexport class UserConsumer {\n\n  @App()\n  app: Application;\n\n  @Inject()\n  ctx: Context;\n\n  @Inject()\n  logger;\n\n  @RabbitMQListener('abc', {\n    exchange: 'logs',\n    exchangeOptions: {\n      type: 'fanout',\n      durable: false,\n    },\n    exclusive: true,\n    consumeOptions: {\n      noAck: true,\n    }\n  })\n  async gotData(msg: ConsumeMessage) {\n    this.logger.info('test output1 =>', msg.content.toString('utf8'));\n    // TODO\n  }\n\n  @RabbitMQListener('bcd', {\n    exchange: 'logs',\n    exchangeOptions: {\n      type: 'fanout',\n      durable: false,\n    },\n    exclusive: true,\n    consumeOptions: {\n      noAck: true,\n    }\n  })\n  async gotData2(msg: ConsumeMessage) {\n    this.logger.info('test output2 =>', msg.content.toString('utf8'));\n    // TODO\n  }\n\n}\n\n```\n\n\n订阅的 abc 和 bcd 队列，绑定了相同的交换机 logs，最终的结果是，两个方法都会被调用。\n\n\n### Direct Exchange\n\n\nDirect Exchange 是 RabbitMQ 默认的 Exchange，完全根据 RoutingKey 来路由消息。设置 Exchange 和 Queue 的 Binding 时需指定 RoutingKey（一般为 Queue Name），发消息时也指定一样的 RoutingKey，消息就会被路由到对应的Queue。\n\n\n下面的示例代码，我们不填写 Queue Name，只添加一个 routingKey，交换机类型为 direct。\n```typescript\nimport { Consumer, MSListenerType, RabbitMQListener, Inject, App } from '@midwayjs/core';\nimport { Context, Application } from '../../../../../src';\nimport { ConsumeMessage } from 'amqplib';\n\n@Consumer(MSListenerType.RABBITMQ)\nexport class UserConsumer {\n\n  @App()\n  app: Application;\n\n  @Inject()\n  ctx: Context;\n\n  @Inject()\n  logger;\n\n  @RabbitMQListener('', {\n    exchange: 'direct_logs',\n    exchangeOptions: {\n      type: 'direct',\n      durable: false,\n    },\n    routingKey: 'direct_key',\n    exclusive: true,\n    consumeOptions: {\n      noAck: true,\n    }\n  })\n  async gotData(msg: ConsumeMessage) {\n    // TODO\n  }\n}\n\n```\n\n\ndirect 类型的消息，会根据 routerKey 做定向过滤，所以只有特定订阅能收到消息。\n\n\n\n\n### 装饰器参数\n\n\n`@RabbitMQListener` 装饰器的第一个参数为 queueName，代表需要监听的队列。\n\n\n第二个参数是一个对象，包含队列，交换机等参数，详细定义如下：\n```typescript\nexport interface RabbitMQListenerOptions {\n  exchange?: string;\n  /**\n   * queue options\n   */\n  exclusive?: boolean;\n  durable?: boolean;\n  autoDelete?: boolean;\n  messageTtl?: number;\n  expires?: number;\n  deadLetterExchange?: string;\n  deadLetterRoutingKey?: string;\n  maxLength?: number;\n  maxPriority?: number;\n  pattern?: string;\n  /**\n   * prefetch\n   */\n  prefetch?: number;\n  /**\n   * router\n   */\n  routingKey?: string;\n  /**\n   * exchange options\n   */\n  exchangeOptions?: {\n    type?: 'direct' | 'topic' | 'headers' | 'fanout' | 'match' | string;\n    durable?: boolean;\n    internal?: boolean;\n    autoDelete?: boolean;\n    alternateExchange?: string;\n    arguments?: any;\n  };\n  /**\n   * consumeOptions\n   */\n  consumeOptions?: {\n    consumerTag?: string;\n    noLocal?: boolean;\n    noAck?: boolean;\n    exclusive?: boolean;\n    priority?: number;\n    arguments?: any;\n  };\n}\n```\n\n\n\n\n### 本地测试\n\n\nMidway 提供了一个简单的测试方法用于测试订阅某个数据。 `@midwayjs/mock` 工具提供了一个 `createRabbitMQProducer` 的方法，用于创建一个生产者，通过它，你可以创建一个队列（Queue），以及向这个队列发消息。\n\n\n然后，我们启动一个 app，就可以自动监听到这个队列中的数据，并执行后续逻辑。\n\n```typescript\nimport { createRabbitMQProducer, close, creatApp } from '@midwayjs/mock';\n\ndescribe('/test/index.test.ts', () => {\n  it('should test create message and get from app', async () => {\n    // create a queue and channel\n    const channel = await createRabbitMQProducer('tasks', {\n      isConfirmChannel: true,\n      mock: false,\n      url: 'amqp://localhost',\n    });\n\n    // send data to queue\n    channel.sendToQueue('tasks', Buffer.from('something to do'))\n\n    // create app and got data\n    const app = await creatApp();\n\n    // wait a moment\n\n    await close(app);\n  });\n});\n\n```\n\n\n**示例一**\n\n\n创建一个 fanout exchange。\n```typescript\nconst manager = await createRabbitMQProducer('tasks-fanout', {\n  isConfirmChannel: false,\n  mock: false,\n  url: 'amqp://localhost',\n});\n\n// Name of the exchange\nconst ex = 'logs';\n// Write a message\nconst msg = \"Hello World!\";\n\n// 声明交换机\nmanager.assertExchange(ex, 'fanout', { durable: false }) // 'fanout' will broadcast all messages to all the queues it knows\n\n// 启动服务\nconst app = await creatApp('base-app-fanout', {\n  url: 'amqp://localhost',\n  reconnectTime: 2000\n});\n\n// 发送到交换机，由于不持久化，需要等订阅服务起来之后再发\nmanager.sendToExchange(ex, '', Buffer.from(msg))\n\n// 等一段时间\nawait sleep(5000);\n\n// 校验结果\n\n// 关闭 producer\nawait manager.close();\n\n// 关闭 app\nawait close(app);\n```\n\n\n**示例二**\n\n\n创建一个 direct exchange。\n```typescript\n/**\n  * direct 类型的消息，根据 routerKey 做定向过滤\n  */\nconst manager = await createRabbitMQProducer('tasks-direct', {\n  isConfirmChannel: false,\n  mock: false,\n  url: 'amqp://localhost',\n});\n\n// Name of the exchange\nconst ex = 'direct_logs';\n// Write a message\nconst msg = \"Hello World!\";\n\n// 声明交换机\nmanager.assertExchange(ex, 'direct', { durable: false }) // 'fanout' will broadcast all messages to all the queues it knows\n\nconst app = await creatApp('base-app-direct', {\n  url:  'amqp://localhost',\n  reconnectTime: 2000\n});\n\n// 这里指定 routerKey，发送到交换机\nmanager.sendToExchange(ex, 'direct_key', Buffer.from(msg))\n\n// 校验结果\n\nawait manager.close();\nawait close(app);\n```\n\n\n## 生产者（Producer）使用方法\n\n\n生产者（Producer）也就是第一节中的消息产生者，简单的来说就是会创建一个客户端，将消息发送到 RabbitMQ 服务。\n\n\n注意：当前 Midway 并没有使用组件来支持消息发送，这里展示的示例只是使用纯 SDK 在 Midway 中的写法。\n\n\n### 安装依赖\n\n\n```bash\n$ npm i amqplib amqp-connection-manager --save\n$ npm i @types/amqplib --save-dev\n```\n\n\n### 调用服务发送消息\n\n\n比如，我们在 service 文件下，新增一个 `rabbitmq.ts` 文件。\n```typescript\nimport { Provide, Scope, ScopeEnum, Init, Autoload, Destroy } from '@midwayjs/core';\nimport * as amqp from 'amqp-connection-manager'\n\n@Autoload()\n@Provide()\n@Scope(ScopeEnum.Singleton) // Singleton 单例，全局唯一（进程级别）\nexport class RabbitmqService {\n\n  private connection: amqp.AmqpConnectionManager;\n\n  private channelWrapper;\n\n  @Init()\n  async connect() {\n    // 创建连接，你可以把配置放在 Config 中，然后注入进来\n    this.connection = await amqp.connect('amqp://localhost');\n\n    // 创建 channel\n    this.channelWrapper = this.connection.createChannel({\n      json: true,\n      setup: function(channel) {\n        return Promise.all([\n          // 绑定队列\n        \tchannel.assertQueue(\"tasks\", { durable: true }),\n        ]);\n      }\n    });\n  }\n\n  // 发送消息\n  public async sendToQueue(queueName: string, data: any) {\n  \treturn this.channelWrapper.sendToQueue(queueName, data);\n  }\n\n  @Destroy()\n  async close() {\n  \tawait this.channelWrapper.close();\n    await this.connection.close();\n  }\n}\n\n```\n大概就是创建了一个用来封装消息通信的 service，同时他是全局唯一的 Singleton 单例。由于增加了 `@AutoLoad` 装饰器，可以自执行初始化。\n\n\n这样基础的调用服务就抽象好了，我们只需要在用到的地方，调用 `sendToQueue` 方法即可。\n\n\n比如：\n\n\n```typescript\n@Provide()\nexport class UserService {\n\n  @Inject()\n  rabbitmqService: RabbitmqService;\n\n\tasync invoke() {\n    // TODO\n\n    // 发送消息\n  \tawait this.rabbitmqService.sendToQueue('tasks', {hello: 'world'});\n  }\n}\n```\n\n\n## 参考文档\n\n\n- [理解 RabbitMQ Exchange](https://zhuanlan.zhihu.com/p/37198933)\n- [RabbitMQ for Node.js in 30 steps](https://github.com/Gurenax/node-rabbitmq)\n","site/docs/extensions/redis.md":"# Redis\n\n这里介绍如何快速在 Midway 中使用 Redis。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 安装依赖\n\n`@midwayjs/redis` 是主要的功能包。\n\n```bash\n$ npm i @midwayjs/redis@4 --save\n```\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/redis\": \"^4.0.0\",\n    // ...\n  }\n}\n```\n\n\n## 引入组件\n\n\n首先，引入组件，在 `src/configuration.ts` 中导入：\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as redis from '@midwayjs/redis';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    redis\t\t// 导入 redis 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ],\n})\nexport class MainConfiguration {\n}\n```\n\n\n## 配置 Redis\n\n\n**单客户端配置**\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  redis: {\n    client: {\n      port: 6379, // Redis port\n      host: \"127.0.0.1\", // Redis host\n      password: \"auth\",\n      db: 0,\n    },\n  },\n}\n```\n**Sentinel 配置**\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  redis: {\n    client: {\n      sentinels: [{          // Sentinel instances\n        port: 26379,         // Sentinel port\n        host: '127.0.0.1',   // Sentinel host\n      }],\n      name: 'mymaster',      // Master name\n      password: 'auth',\n      db: 0\n    },\n  },\n}\n```\n\n\n**Cluster 模式配置，需要配置多个**\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  redis: {\n    // Cluster Redis\n    client: {\n      cluster: true,\n      nodes: [{\n        host: 'host',\n        port: 'port',\n      },{\n        host: 'host',\n        port: 'port',\n      }],\n      redisOptions: {\n        family: '',\n        password: 'xxxx',\n        db: 'xxx'\n      }\n    }\n  },\n}\n```\n\n**多个客户端配置，需要配置多个**\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  redis: {\n\t\t// Multi Redis\n    clients: {\n      instance1: {\n        host: 'host',\n        port: 'port',\n        password: 'password',\n        db: 'db',\n      },\n      instance2: {\n        host: 'host',\n        port: 'port',\n        password: 'password',\n        db: 'db',\n      },\n    },\n  },\n}\n```\n更多参数可以查看 [ioredis 文档](https://github.com/luin/ioredis/blob/master/API.md#new_Redis_new)。\n\n\n## 使用 Redis 服务\n\n\n我们可以在任意的代码中注入使用。\n```typescript\nimport { Provide, Controller, Inject, Get } from '@midwayjs/core';\nimport { RedisService } from '@midwayjs/redis';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  redisService: RedisService;\n\n  async invoke() {\n\n    // 简单设置\n    await this.redisService.set('foo', 'bar');\n\n    // 设置过期时间，单位秒\n    await this.redisService.set('foo', 'bar', 'EX', 10);\n\n    // 获取数据\n    const result = await this.redisService.get('foo');\n\n   // result => bar\n  }\n}\n```\n\n\n可以使用 `RedisServiceFactory` 获取不同的实例。\n```typescript\nimport { RedisServiceFactory } from '@midwayjs/redis';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  redisServiceFactory: RedisServiceFactory;\n\n  async save() {\n    const redis1 = this.redisServiceFactory.get('instance1');\n    const redis2 = this.redisServiceFactory.get('instance3');\n\n    //...\n\n  }\n}\n```\n\n也可以通过装饰器获取。\n\n```typescript\nimport { RedisServiceFactory, RedisService } from '@midwayjs/redis';\nimport { InjectClient } from '@midwayjs/core';\n\n@Provide()\nexport class UserService {\n\n  @InjectClient(RedisServiceFactory, 'instance1')\n  redis1: RedisService;\n\n  @InjectClient(RedisServiceFactory, 'instance3')\n  redis2: RedisService;\n\n  async save() {\n    //...\n  }\n}\n```\n\n\n## 服务发现\n\n提供基于统一抽象的服务发现能力，虽然 Redis 一般不作为服务发现的注册中心，但是其拥有 pub/sub 能力可以变相提供功能，我们仍实现了 Redis 将作为服务发现的客户端能力。\n\n服务发现包括 **注册服务** 和 **获取服务** 两部分。\n\n\n### 配置服务发现\n\n你需要先配置一个客户端；若存在多个客户端，可通过 `serviceDiscoveryClient` 指定用于服务发现的客户端。\n\n通过 `serviceDiscovery` 配置服务发现选项：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  redis: {\n    clients: {\n      default: {\n        host: '127.0.0.1',\n        port: 6379\n      }\n    },\n    serviceDiscovery: {\n      prefix: 'services:',\n      ttl: 30,\n      scanCount: 100\n    }\n  }\n}\n```\n\n### 注册服务发现\n\n服务启动后，将当前服务注册到 Redis：\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport { RedisServiceDiscovery } from '@midwayjs/redis';\n\n@Configuration({})\nexport class MainConfiguration {\n  @Inject()\n  redisDiscovery: RedisServiceDiscovery;\n\n  async onServerReady() {\n    const client = this.redisDiscovery.createClient();\n\n    await client.register({\n      serviceName: 'order',\n      id: client.defaultMeta.id,\n      host: client.defaultMeta.host,\n      port: 7001,\n      ttl: 30,\n      meta: { version: '1.0.0' }\n    });\n    // 注册默认上线，无需显式 online\n  }\n}\n```\n\n说明：上线时写入实例键并设置 TTL，客户端会定时刷新 TTL 保持存活；下线删除实例键并清理刷新；注销发布变更消息以便 Listener 全量刷新。\n\n\n### 获取可用服务\n\n在任意位置获取可用实例或按负载均衡选择一个实例：\n\n````typescript\n@Provide()\nexport class OrderService {\n  @Inject()\n  redisDiscovery: RedisServiceDiscovery;\n\n  async getService() {\n    const instances = await this.redisDiscovery.getInstances('order');\n    const one = await this.redisDiscovery.getInstance('order');\n    return { instances, one };\n  }\n}\n````\n\n返回的实例为注册时写入的对象，通常包含 `serviceName/id/host/port/ttl/meta/tags/status` 等字段。\n\n","site/docs/extensions/render.md":"# 模板渲染\n\n本组件用于在 midway 体系使用服务端渲染 ejs，nunjucks 模板。\n\n相关信息：\n\n| web 支持情况      |      |\n| ----------------- | ---- |\n| @midwayjs/koa     | ✅    |\n| @midwayjs/faas    | ✅    |\n| @midwayjs/web     | ✅    |\n| @midwayjs/express | ❌    |\n\n\n\n\n## 使用 ejs\n\n\n### 安装依赖\n\n\n选择对应的模板安装依赖。\n```bash\n$ npm i @midwayjs/view-ejs@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/view-ejs\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n### 引入组件\n\n\n首先，引入组件，在 `configuration.ts` 中导入：\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as view from '@midwayjs/view-ejs';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    view\t\t// 导入 ejs 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n### 配置\n\n配置后缀，映射到指定的引擎。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  view: {\n    mapping: {\n      '.ejs': 'ejs',\n    },\n  },\n  // ejs config\n  ejs: {}\n}\n```\n### 使用\n\n\n注意，默认的 view 目录为 `${appDir}/view` ，在其中创建一个 `hello.ejs` 文件。\n\n\n目录结构如下：\n```\n➜  my_midway_app tree\n.\n├── src\n│   └── controller                 ## Controller 目录\n│       └── home.ts\n├── view                           ## 模板目录\n│   └── hello.ejs\n├── test\n├── package.json\n└── tsconfig.json\n```\n\n\n我们在模板里写一些 ejs 格式的内容，比如：\n```typescript\n// view/hello.ejs\nhello <%= data %>\n```\n\n\n在 Controller 中渲染。\n```typescript\nimport { Inject, Provide } from '@midwayjs/core';\nimport { Context } from '@midwayjs/koa';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  ctx: Context;\n\n  @Get('/')\n  async render(){\n    await this.ctx.render('hello.ejs', {\n      data: 'world',\n    });\n  }\n}\n```\n\n### 配置后缀\n\n默认后缀为 `.html` ，为了改成习惯的 `.ejs` 后缀，我们可以加一个 `defaultExtension` 配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  view: {\n    defaultExtension: '.ejs',\n    mapping: {\n      '.ejs': 'ejs',\n    },\n  },\n  // ejs config\n  ejs: {}\n}\n```\n\n这样我们在渲染时不需要增加后缀。\n\n```typescript\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  ctx: Context;\n\n  @Get('/')\n  async render(){\n    await this.ctx.render('hello', {\n      data: 'world',\n    });\n  }\n}\n```\n\n\n### 默认渲染引擎\n\n我们可以通过 `defaultViewEngine` 来设置默认的渲染引擎。\n\n其作用是，当遇到的模板后缀，比如 `.html` 未在配置的 `mapping` 字段中找到时，使用该 `defaultViewEngine` 字段指定的引擎来渲染。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  view: {\n    defaultViewEngine: 'ejs',\n    mapping: {\n      '.ejs': 'ejs',\n    },\n  },\n  // ejs config\n  ejs: {}\n}\n```\n\n这样，如果模板是 `.html` 后缀，由于 `mapping` 中未指定，依旧会使用 `ejs` 来渲染。\n\n### 配置多个模板目录\n\n如果我们需要将代码封装为组件提供，就需要支持不同的模板目录。\n\n默认的模板目录在 `${appDir}/view`。我们可以在 `rootDir` 字段增加其他的目录。\n\n```typescript\n// src/config/config.default.ts\n\n// 修改默认 view 组件的 default 目录\nexport default {\n  // ...\n  view: {\n    rootDir: {\n      default: path.join(__dirname, './view'),\n    }\n  },\n}\n\n// 其他组件需要增加目录的配置\nexport default {\n  // ...\n  // view 组件的配置\n  view: {\n    rootDir: {\n      anotherRoot: path.join(__dirname, './view'),\n    }\n  },\n}\n```\n\n通过对象合并的机制，使得所有的 `rootDir` 都能合并到一起，组件内部会获取 values 做匹配。\n\n\n\n## 使用 Nunjucks\n\n\n和 ejs 类似，引入对应组件即可。\n\n\n1、选择对应的模板安装依赖。\n```bash\n$ npm i @midwayjs/view-nunjucks@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/view-nunjucks\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n2、引入组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as view from '@midwayjs/view-nunjucks';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    view\t\t// 导入 nunjucks 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n\n3、增加 nunjucks 的配置，比如默认使用 nunjucks。\n```typescript\nexport default {\n  // ...\n  view: {\n    defaultViewEngine: 'nunjucks',\n    mapping: {\n      '.nj': 'nunjucks',\n    },\n  },\n}\n```\n\n\n4、在 view 目录增加模板\n```typescript\n// view/test.nj\nhi, {{ user }}\n```\n\n\n在 Controller 中渲染。\n```typescript\nimport { Inject, Provide } from '@midwayjs/core';\nimport { Context } from '@midwayjs/koa';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  ctx: Context;\n\n  @Get('/')\n  async render(){\n    await ctx.render('test.nj', { user: 'midway' });\n  }\n}\n```\n访问后会输出 `hi, midway` 。\n\n\n如果有自定义 filter 的需求，可以在入口处增加，比如下面增加了一个名为 `hello` 的 filter。\n```typescript\nimport { App, Configuration, Inject } from '@midwayjs/core';\nimport * as view from '@midwayjs/view-nunjucks';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [view],\n  importConfigs: [join(__dirname, 'config')]\n})\nexport class MainConfiguration {\n\n  @App()\n  app;\n\n  @Inject()\n  env: view.NunjucksEnvironment;\n\n  async onReady(){\n    this.env.addFilter('hello', (str) => {\n      return 'hi, ' + str;\n    });\n  }\n}\n\n```\n在模板里可以使用\n```typescript\n{{ name | hello }}\n```\n然后渲染\n```typescript\n// controller\n// ...\nawait ctx.render('test.nj', { name: 'midway' });\n```\n也会输出 `hi, midway` 。\n\n\n\n## 自定义模板引擎\n\n默认我们只提供了 ejs 和 nunjucks 的模板引擎，你也可以编写自己的模板引擎代码。\n\n### 实现模板引擎\n\n首先需要创建一个请求作用域的模板引擎类，它将在每个请求执行时初始化。你需需要实现其中的 `render` 和 `renderString` 方法。如果你的模板引擎不支持某个方法，可以抛出异常。\n\n```typescript\n// lib/view.ts\nimport { Provide, Config } from '@midwayjs/core';\nimport { IViewEngine } from '@midwayjs/view';\n\n@Provide()\nexport class MyView implements IViewEngine {\n\n  @Config('xxxx')\n  viewConfig;\n\n  async render(name: string, locals?: Record<string, any>, options?: RenderOptions) {\n    return myengine.render(name, locals, options);\n  }\n\n  async renderString(tpl: string,\n    locals?: Record<string, any>,\n    options?: RenderOptions) {\n\n    throw new Error('not implement');\n  }\n};\n```\n\n这两个方法接受类似的三个参数，`renderString` 第一个参数需要传入待解析的模板内容本身，而 `render` 方法会解析模板文件。\n\n`render(name, locals, viewOptions)`\n\n- name: 从 `root`（默认是 `/view` ) 相对的 path\n- locals: 模板需要的数据\n- viewOptions: 每次渲染的模板参数，可覆盖的配置，可以在配置文件中重写，其中包含几个参数：\n  - root: 模板的绝对路径\n  - name: 调用 render 方法的原始 name 值\n  - locals: 调用 render 方法的原始 locals 值\n\n`renderString(tpl, locals, viewOptions)`\n\n- tpl: 模板名\n- locals: 和 `render` 一样\n- viewOptions: 和 `render` 一样\n\n### 注册模板引擎\n\n在实现自定义的模板引擎后，我们需要在启动入口注册它。\n\n通过引入 `ViewManager` ，我们可以使用 `use` 方法注册自定义模板引擎。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject, Provide } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as view from '@midwayjs/view';\nimport { MyView } from './lib/my';\n\n@Configuration({\n  imports: [koa, view],\n  importConfigs: [join(__dirname, 'config')]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  viewManager: view.ViewManager;\n\n  async onReady(){\n    this.viewManager.use('ejs', MyView);\n  }\n}\n\n```\n\n\n\n## 注意事项\n\n\n如需在 egg(@midwayjs/web) 场景下使用，请在 `plugin.ts` 中关闭 view 和其相关插件。\n\n\n```typescript\nimport { EggPlugin } from 'egg';\nexport default {\n  // ...\n  view: false,\n} as EggPlugin;\n\n```\n\n\n否则会出现下面类似的错误。\n```\nTypeError: Cannot set property view of #<EggApplication> which has only a getter\n```\n\n","site/docs/extensions/security.md":"# 安全\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的通用安全组件，支持 `csrf` 、`xss` 等多种安全策略。\n\n相关信息：\n\n| web 支持情况      |      |\n| ----------------- | ---- |\n| @midwayjs/koa     | ✅    |\n| @midwayjs/faas    | ✅    |\n| @midwayjs/web     | ✅    |\n| @midwayjs/express | ✅    |\n\n\n\n## 安装使用\n\n1、安装依赖\n\n```bash\n$ npm i @midwayjs/security --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/security\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n2、在 configuration 中引入组件\n\n```typescript\nimport * as security from '@midwayjs/security';\n@Configuration({\n  imports: [\n    // ...other components\n    security\n  ],\n})\nexport class MainConfiguration {}\n```\n\n---\n\n## 防范常见的安全威胁\n\n\n### 一、CSRF\n\nCSRF（Cross-site request forgery 跨站请求伪造），是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。\n\n\n#### 1. 令牌同步模式\n通过响应页面时将 token 渲染到页面上，在开启 `csrf` 配置后，通过 `ctx.csrf` 可以获取到 `csrf token`，可以再返回页面 html 时同步输出\n\n```ts\n@Controller('/')\nexport class HomeController {\n  @Inject()\n  ctx;\n\n  @Get('/home')\n  async home() {\n    return `<form method=\"POST\" action=\"/upload?_csrf=${ this.ctx.csrf }\" >\n      title: <input name=\"title\" />\n      <button type=\"submit\">upload</button>\n    </form>`;\n  }\n}\n```\n\n传递 CSRF token 的字段（上述示例中的 `_csrf`）可以在配置中改变，请查看下述 `配置 -> csrf`。\n\n\n\n#### 2. Cookies 模式\n\n在 CSRF 默认配置下，token 会被设置在 Cookie 中，可以再前端页面中通过 JS 从 Cookies 中获取，然后再 ajax/fetch 等请求中添加到 `header`、`query` 或 `body` 中。\n\n```js\nconst csrftoken = Cookies.get('csrfToken');\nfetch('/api/post', {\n  method: 'POST',\n  headers: {\n    'x-csrf-token': csrftoken\n  },\n  ...\n});\n```\n\n默认配置下，框架会将 `CSRF token` 存在 `Cookie` 中，以方便前端 JS 发起请求时获取到。但是所有的子域名都可以设置 Cookie，因此当我们的应用处于无法保证所有的子域名都受控的情况下，存放在 `Cookie` 中可能有被 `CSRF` 攻击的风险。框架提供了一个配置项 `useSession`，可以将 token 存放到 Session 中。\n\n\n当 `CSRF token` 存储在 `Cookie` 中时，一旦在同一个浏览器上发生用户切换，新登陆的用户将会依旧使用旧的 token（之前用户使用的），这会带来一定的安全风险，因此在每次用户登陆的时候都必须调用 `ctx.rotateCsrfSecret()` 刷新 `CSRF token`，例如：\n\n\n```js\n@Controller('/')\nexport class HomeController {\n  @Inject()\n  ctx;\n\n  @Inject()\n  userService;\n\n  @Get('/login')\n  async login(@Body('username') username: string, @Body('password') password: string) {\n    const user = await userService.login({ username, password });\n    this.ctx.session = { user };\n    this.ctx.rotateCsrfSecret();\n    return { success: true };\n  }\n}\n```\n\n### 二、XSS\n\n`XSS`（cross-site scripting 跨站脚本攻击）攻击是最常见的 Web 攻击，是代码注入的一种。它允许恶意用户将代码注入到网页上，其他用户在观看网页时就会受到影响。\n\n`XSS` 攻击通常指的是通过利用网页开发时留下的漏洞，通过巧妙的方法注入恶意指令代码到网页，使用户加载并执行攻击者恶意制造的网页程序。攻击成功后，攻击者可能得到更高的权限（如执行一些操作）、私密网页内容、会话和cookie等各种内容。\n\n\n#### 1. 反射型的 XSS 攻击\n\n主要是由于服务端接收到客户端的不安全输入，在客户端触发代码执行从而发起 `Web` 攻击。\n\n例如：在某搜索网站搜索时，搜索结果会显示搜索的关键词。搜索关键词填入 `<script>alert('xss')</script>`, 点击搜索后，若页面程序没有对关键词进行处理，这段代码就会直接在页面上执行，弹出 alert。\n\n框架提供了 `ctx.security.escape()` 方法对字符串进行 XSS 过滤。\n\n```ts\n@Controller('/')\nexport class HomeController {\n  @Inject()\n  ctx;\n\n  @Get('/home')\n  async home() {\n    const str = `<script>alert('xss')</script>`;\n    const escapedStr = this.ctx.security.escape(str);\n    // &lt;script&gt;alert(&quot;xss&quot;) &lt;/script&gt;\n    return escapedStr;\n  }\n}\n```\n\n另外当网站输出的内容是作为 js 脚本的。这个时候需要使用 `ctx.security.js()` 来进行过滤。\n\n还有一种情况，有时候我们需要在 `js` 中输出 `json` ，若未做转义，易被利用为 `XSS` 漏洞。框架提供了 `ctx.security.json(变量)` 来提供 json encode，防止 XSS 攻击。\n\n\n```ts\n@Controller('/')\nexport class HomeController {\n  @Inject()\n  ctx;\n\n  @Get('/home')\n  async home() {\n    return `<script>windows.config = ${this.ctx.security.json( ...variable )};</script>`;\n  }\n}\n```\n\n#### 2. 存储型的 XSS 攻击\n\n通过提交带有恶意脚本的内容存储在服务器上，当其他人看到这些内容时发起 Web 攻击，比如一些网站的评论框中，用户恶意将一些代码作为评论内容，若没有过滤，其他用户看到这个评论时恶意代码就会执行。\n\n\n框架提供了 `ctx.security.html()` 来进行过滤。\n\n\n#### 3. 其他 XSS 的防范方式\n\n浏览器自身具有一定针对各种攻击的防范能力，他们一般是通过开启 Web 安全头生效的。框架内置了一些常见的 Web 安全头的支持。\n\n**CSP**\n\n`Content Security Policy`，简称 `CSP`，主要是用来定义页面可以加载哪些资源，减少 `XSS` 的发生。\n\n默认关闭（可通过 `csp: {enable: true}` 配置开启），开启后可以有效的防止 `XSS` 攻击的发生。要配置 `CSP` , 需要对 `CSP` 的 `policy` 策略有了解，具体细节可以参考 [阿里聚安全 - CSP是什么？](https://www.zhihu.com/question/21979782/answer/122682029)\n\n\n**X-Download-Options:noopen**\n\n默认开启（可通过 `noopen: {enable: false}` 配置关闭），禁用 IE 下下载框 Open 按钮，防止 IE 下下载文件默认被打开 XSS。\n\n**X-Content-Type-Options:nosniff**\n禁用 IE8 自动嗅探 mime 功能，默认关闭（可通过 `nosniff: {enable: true}` 配置开启），例如 text/plain 却当成 text/html 渲染，特别当本站点 serve 的内容未必可信的时候。\n\n**X-XSS-Protection**\nIE 提供的一些 XSS 检测与防范，默认开启（可通过 `xssProtection: {enable: false}` 配置关闭）\n\nclose 默认值 false，即设置为 1; mode=block\n\n---\n\n\n## 配置\n\n默认配置如下：\n\n```ts\n// src/config/config.default\nexport default {\n  // ...\n\n  // 默认配置\n  security: {\n    csrf: {\n      enable: true,\n      type: 'ctoken',\n      useSession: false,\n      cookieName: 'csrfToken',\n      sessionName: 'csrfToken',\n      headerName: 'x-csrf-token',\n      bodyName: '_csrf',\n      queryName: '_csrf',\n      refererWhiteList: [],\n    },\n    xframe: {\n      enable: true,\n      value: 'SAMEORIGIN',\n    },\n    csp: {\n      enable: false,\n    },\n    hsts: {\n      enable: false,\n      maxAge: 365 * 24 * 3600,\n      includeSubdomains: false,\n    },\n    noopen: {\n      enable: false,\n    },\n    nosniff: {\n      enable: false,\n    },\n    xssProtection: {\n      enable: true,\n      value: '1; mode=block',\n    },\n  },\n}\n\n```\n\n### csrf\n\n| 配置项 | 类型                                   | 作用描述 | 默认值 |\n| --- |--------------------------------------| --- | --- |\n| enable | boolean                              | 是否开启 | true |\n| type | 'all' / 'any' / 'ctoken' / 'referer' | csrf 校验类型，all/any 等于 ctoken + referer | 'ctoken' 从query/header/body 中获取 csrf token；；'referer' 则可以通过 refererWhiteList 配置白名单 |\n| useSession | boolean                              | csrf token 是否存放在 session 中 | false，默认存放在 cookies 中 |\n| cookieName | string                               | token 在 cookie 中存放的 字段 | 'csrfToken' |\n| sessionName | string                               | token 在 session 中存放的 字段 | 'csrfToken' |\n| headerName | string                               | token 在 header 中存放的 字段 | 'x-csrf-token' |\n| bodyName | string                               | token 在 body 中存放的 字段 | '_csrf' |\n| queryName | string                               | token 在 query 中存放的 字段 | '_csrf' |\n| refererWhiteList | Array\\<string\\>                      | 允许的来源白名单 | [] |\n\n#### 配置 refererWhiteList 不生效？\n+ 原因一：refererWhiteList 中需要配置 referer 的 host 部分，例如 referer 为 `https://midway-demo.com:1234/docs`，则 refererWhiteList 中需要配置 `midway-demo.com:1234`。\n+ 原因二：refererWhiteList 仅在 csrf 配置中 type 为 `referer` 的情况下生效，默认 type 为 `ctoken`，需要修改为 `referer`。\n+ 原因三：发送的http请求中的 referer 字段不是一个标准的 url 地址（例如没有加上请求协议等），参考 [MDN](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Referer) 文档\n\n### xframe\n\n\nxframe 用来配置 `X-Frame-Options` 响应头，用来给浏览器指示允许一个页面可否在 `frame`, `iframe`, `embed` 或者 `object` 中展现的标记。站点可以通过确保网站没有被嵌入到别人的站点里面，从而避免 `clickjacking` 攻击。\n\n`X-Frame-Options` 有三个可能的值：\n\n+ X-Frame-Options: deny：页面不允许在 frame 中展示\n+ X-Frame-Options: sameorigin：该页面可以在相同域名页面的 frame 中展示\n+ X-Frame-Options: allow-from https://example.com/：该页面可以在指定来源的 frame 中展示\n\n\n\n| 配置项 | 类型 | 作用描述 | 默认值 |\n| --- | --- | --- | --- |\n| enable | boolean | 是否开启 | true |\n| value | string | X-Frame-Options 值 | 'SAMEORIGIN' |\n\n\n\n### hsts\n\n`HTTP Strict Transport Security`（通常简称为 `HSTS` ）是一个安全功能，它告诉浏览器只能通过 `HTTPS` 访问当前资源，而不是 `HTTP`。\n\n| 配置项 | 类型 | 作用描述 | 默认值 |\n| --- | --- | --- | --- |\n| enable | boolean | 是否开启 | false |\n| maxAge | number | 在浏览器收到这个请求后的多少 `秒` 时间内凡是访问这个域名下的请求都使用HTTPS请求 | `365 * 24 * 3600` 即一年 |\n| includeSubdomains | boolean | 此规则是否适用于该网站的所有子域名 | false |\n\n\n### csp\n\nHTTP 响应头 `Content-Security-Policy` 允许站点管理者控制指定的页面加载哪些资源。这将帮助防止跨站脚本攻击（XSS）。\n\n\n| 配置项 | 类型                                                       | 作用描述 | 默认值 |\n| --- |----------------------------------------------------------| --- | --- |\n| enable | boolean                                                  | 是否开启 | false |\n| policy | Object\\<key: string, value: string / string[] / boolean> | 策略列表 | {} |\n| reportOnly | boolean                                                  | 是否开启 | false |\n| supportIE | boolean                                                  | 是否支持IE浏览器 | false |\n\n详细的 `policy` 配置可以参考: [Content Security Policy (CSP) 是什么？阿里聚安全](https://www.zhihu.com/question/21979782/answer/122682029)\n\n\n### noopen\n\n用于指定 `IE 8` 以上版本的用户不打开文件而直接保存文件。在下载对话框中不显式“打开”选项。\n\n| 配置项 | 类型 | 作用描述 | 默认值 |\n| --- | --- | --- | --- |\n| enable | boolean | 是否开启 | false |\n\n\n\n\n### nosniff\n\n开启后，如果从 `script` 或 `stylesheet` 读入的文件的 `MIME` 类型与指定 `MIME` 类型不匹配，不允许读取该文件。用于防止 `XSS` 等跨站脚本攻击。\n\n| 配置项 | 类型 | 作用描述 | 默认值 |\n| --- | --- | --- | --- |\n| enable | boolean | 是否开启 | false |\n\n\n\n\n### xssProtection\n\n用于启用浏览器的XSS过滤功能，以防止 `XSS` 跨站脚本攻击。\n\n`X-XSS-Protection` 响应头是 `IE`，`Chrome` 和 `Safari` 的一个特性，当检测到跨站脚本攻击 (XSS (en-US))时，浏览器将停止加载页面。若网站设置了良好的 `Content-Security-Policy` 来禁用内联 JavaScript ('unsafe-inline')，现代浏览器不太需要这些保护， 但其仍然可以为尚不支持 `CSP` 的旧版浏览器的用户提供保护。\n\n`X-XSS-Protection` 可以配置下述四个值\n\n+ `0`: 禁止XSS过滤。\n+ `1`：启用XSS过滤（通常浏览器是默认的）。 如果检测到跨站脚本攻击，浏览器将清除页面（删除不安全的部分）。\n+ `1;mode=block`：启用XSS过滤。 如果检测到攻击，浏览器将不会清除页面，而是阻止页面加载。\n+ `1; report=<reporting-URI>`： Chromium only，启用XSS过滤。 如果检测到跨站脚本攻击，浏览器将清除页面并使用CSP report-uri (en-US)指令的功能发送违规报告。\n\n| 配置项 | 类型 | 作用描述 | 默认值 |\n| --- | --- | --- | --- |\n| enable | boolean | 是否开启 | false |\n| value | string | X-XSS-Protection 配置 | `1; mode=block` |\n\n","site/docs/extensions/sequelize.md":"# Sequelize\n\n本文档介绍如何在 Midway 中使用 Sequelize。\n\n:::tip\n\n当前模块从 v3.4.0 开始已经重构，历史写法兼容，如果查询历史文档，请参考 [这里](../legacy/sequelize)。\n\n:::\n\n相关信息：\n\n| 描述              |     |\n| ----------------- | --- |\n| 可用于标准项目    | ✅  |\n| 可用于 Serverless | ✅  |\n| 可用于一体化      | ✅  |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 和老写法的区别\n\n如果想使用新版本的用法，请参考下面的流程，将老代码进行修改，新老代码不能混用。\n\n升级方法：\n\n- 1、请在业务依赖中显式添加 `sequelize` 和 `sequelize-typescript`\n- 2、不再使用 `BaseTable` 装饰器，而直接使用 `sequelize-typescript` 包导出的 `Table` 装饰器\n- 3、在 `src/config.default` 的 `sequelize` 部分配置调整，参考下面的数据源配置部分\n  - 3.1 修改为数据源的形式 `sequelize.dataSource`\n  - 3.2 将实体模型在数据源的 `entities` 字段中声明\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/sequelize@4 sequelize sequelize-typescript --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/sequelize\": \"^4.0.0\",\n    \"sequelize\": \"^6.21.3\",\n    \"sequelize-typescript\": \"^2.1.0\"\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n## 安装数据库 Driver\n\n常用数据库驱动如下，选择你对应连接的数据库类型安装：\n\n```bash\n# for MySQL or MariaDB，也可以使用 mysql2 替代\nnpm install mysql --save\nnpm install mysql2 --save\n\n# for PostgreSQL or CockroachDB\nnpm install pg --save\n\n# for SQLite\nnpm install sqlite3 --save\n\n# for Microsoft SQL Server\nnpm install mssql --save\n\n# for sql.js\nnpm install sql.js --save\n\n# for Oracle\nnpm install oracledb --save\n\n# for MongoDB(experimental)\nnpm install mongodb --save\n```\n\n下面的文档，我们将以 `mysql2` 作为示例。\n\n\n### Directory structure\n\n一个基础的参考目录结构如下。\n\n\n```\nMyProject\n├── src\n│   ├── config\n│   │   └── config.default.ts\n│   ├── entity\n│   │   └── person.entity.ts\n│   ├── configuration.ts\n│   └── service\n├── .gitignore\n├── package.json\n├── README.md\n└── tsconfig.json\n```\n\n\n## 启用组件\n\n在 `src/configuration.ts` 文件中启用组件。\n\n```typescript\nimport { Configuration, ILifeCycle } from '@midwayjs/core';\nimport { join } from 'path';\nimport * as sequelize from '@midwayjs/sequelize';\n\n@Configuration({\n  imports: [\n    // ...\n    sequelize,\n  ],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration implements ILifeCycle {\n  // ...\n}\n```\n\n## 模型定义\n\n### 1、创建 Model（Entity）\n\n我们通过模型和数据库关联，在应用中的模型就是数据库表，在 Sequelize 中，模型是和实体绑定的，每一个实体（Entity) 文件，即是 Model，也是实体（Entity）。\n\n在示例中，需要一个实体，我们这里拿 `person` 举例。新建 entity 目录，在其中添加实体文件 `person.entity.ts` ，一个简单的实体如下。\n\n```typescript\n// src/entity/person.entity.ts\nimport { Table, Model, Column, HasMany } from 'sequelize-typescript';\n\n@Table\nexport class Hobby extends Model {\n  @Column\n  name: string;\n}\n\n@Table\nexport class Person extends Model {\n  @Column\n  name: string;\n\n  @Column\n  birthday: Date;\n\n  @HasMany(() => Hobby)\n  hobbies: Hobby[];\n}\n```\n\n要注意，这里的实体文件的每一个属性，其实是和数据库表一一对应的，基于现有的数据库表，我们往上添加内容。\n\n`@Table` 装饰器可以在不传递任何参数的情况下使用，更多参数请查看 [定义选项](https://sequelize.org/v5/manual/models-definition.html#configuration) 。\n\n```typescript\n@Table({\n  timestamps: true,\n  ...\n})\nexport class Person extends Model {}\n```\n\n\n\n### 2、主键\n\n主键 (id) 将从基类 Model 继承。 一般来说主键是 Integer 类型并且是自增的。\n\n主键设置有两种方法，设置 `@Column({primaryKey: true})` 或者 `@PrimaryKey`。\n\n比如：\n\n```typescript\nimport { Table, Model, PrimaryKey } from 'sequelize-typescript';\n\n@Table\nexport class Person extends Model {\n  @PrimaryKey\n  name: string;\n}\n```\n\n### 3、时间列\n\n主要指代的是 `@CreatedAt`, `@UpdatedAt`, `@DeletedAt` 单个装饰器标注的列。\n\n比如：\n\n```typescript\nimport { Table, Model, CreatedAt, UpdatedAt, DeletedAt } from 'sequelize-typescript';\n\n@Table\nexport class Person extends Model {\n  @CreatedAt\n  creationDate: Date;\n\n  @UpdatedAt\n  updatedOn: Date;\n\n  @DeletedAt\n  deletionDate: Date;\n}\n```\n\n| 装饰器       | 描述                                                                    |\n| ------------ | ----------------------------------------------------------------------- |\n| `@CreatedAt` | 会设置 `timestamps=true` 和 `createdAt='creationDate'`                  |\n| `@UpdatedAt` | 会设置 `timestamps=true` 和 `updatedAt='updatedOn'`                     |\n| `@DeletedAt` | 会设置 `timestamps=true`, `paranoid=true` 和 `deletedAt='deletionDate'` |\n\n### 4、普通列\n\n@Column 装饰器用于标注普通列，可以在不传递任何参数的情况下使用。 但是因此需要能够自动推断 js 类型（详见[类型推断](https://github.com/sequelize/sequelize-typescript#type-inference)）。\n\n```typescript\nimport { Table, Model, Column } from 'sequelize-typescript';\n\n@Table\nexport class Person extends Model {\n  @Column\n  name: string;\n}\n```\n\n或者指定列类型。\n\n```typescript\nimport { Table, Column, DataType } from 'sequelize-typescript';\n\n@Table\nexport class Person extends Model {\n  @Column(DataType.TEXT)\n  name: string;\n}\n```\n\n更多类型描述，请参考 [这里](https://sequelize.org/v5/manual/models-definition.html#configuration)。\n\n比如：\n\n```typescript\nimport { Table, Model, Column, DataType } from 'sequelize-typescript'\n\n@Table\nexport class Person extends Model {\n  @Column({\n    type: DataType.FLOAT,\n    comment: 'Some value',\n    ...\n  })\n  value: number;\n}\n```\n\n| 装饰器                               | 描述                                                                                              |\n| ------------------------------------ | ------------------------------------------------------------------------------------------------- |\n| `@Column`                            | 使用推导的 [dataType](https://sequelize.org/v5/manual/models-definition.html#data-types) 作为类型 |\n| `@Column(dataType: DataType)`        | 显式设置 [dataType](https://sequelize.org/v5/manual/models-definition.html#data-types)            |\n| `@Column(options: AttributeOptions)` | 设置 [attribute options](https://sequelize.org/v5/manual/models-definition.html#configuration)    |\n\n## 数据源配置\n\n新版本我们启用了 [数据源机制](../data_source)，在 `src/config.default.ts` 中配置：\n\n```typescript\n// src/config/config.default.ts\n\nimport { Person } from '../entity/person.entity';\n\nexport default {\n  // ...\n  sequelize: {\n    dataSource: {\n      // 第一个数据源，数据源的名字可以完全自定义\n      default: {\n        database: 'test4',\n        username: 'root',\n        password: '123456',\n        host: '127.0.0.1',\n        port: 3306,\n        encrypt: false,\n        dialect: 'mysql',\n        define: { charset: 'utf8' },\n        timezone: '+08:00',\n        // 本地的时候，可以通过 sync: true 直接 createTable\n        sync: false,\n        \n        // 实体形式\n        entities: [Person],\n\n        // 支持如下的扫描形式，为了兼容我们可以同时进行.js和.ts匹配️\n        entities: [\n          'entity',                        // 指定目录\n          '**/entity/*.entity.{j,t}s',     // 通配加后缀匹配\n        ],\n      },\n\n      // 第二个数据源\n      default2: {\n        // ...\n      },\n    },\n  },\n};\n```\n\n\n\n## 模型关联\n\n可以通过 `HasMany` 、`@HasOne` 、`@BelongsTo`、`@BelongsToMany` 和 `@ForeignKey` 装饰器在模型中直接描述关系。\n\n:::tip\n\n你不需要在数据库中创建外键也可以使用这个功能。\n\n:::\n\n### 一对多\n\n```typescript\nimport { Table, Model, Column, ForeignKey, BelongsTo, HasMany } from 'sequelize-typescript';\n\n@Table\nexport class Player extends Model {\n  @Column\n  name: string;\n\n  @Column\n  num: number;\n\n  @ForeignKey(() => Team)\n  @Column\n  teamId: number;\n\n  @BelongsTo(() => Team)\n  team: Team;\n}\n\n@Table\nexport class Team extends Model {\n  @Column\n  name: string;\n\n  @HasMany(() => Player)\n  players: Player[];\n}\n```\n\n`sequelize-typescript` 会在内部进行关联，会自动查询出相关的依赖。\n\n比如通过 `find` 查询。\n\n```typescript\nconst team = await Team.findOne({ include: [Player] });\n\nteam.players.forEach((player) => {\n  console.log(`Player ${player.name}`);\n});\n```\n\n### 多对多\n\n```typescript\nimport { Table, Model, Column, ForeignKey, BelongsToMany } from 'sequelize-typescript';\n\n@Table\nexport class Book extends Model {\n  @BelongsToMany(() => Author, () => BookAuthor)\n  authors: Author[];\n}\n\n@Table\nexport class Author extends Model {\n  @BelongsToMany(() => Book, () => BookAuthor)\n  books: Book[];\n}\n\n@Table\nexport class BookAuthor extends Model {\n  @ForeignKey(() => Book)\n  @Column\n  bookId: number;\n\n  @ForeignKey(() => Author)\n  @Column\n  authorId: number;\n}\n```\n\n上面的类型，在某些场景下是不安全的，比如上面的 `BookAuthor`，`Author` 的 `books` 的类型，可能会丢失某些属性，需要手动设置。\n\n```typescript\n@BelongsToMany(() => Book, () => BookAuthor)\nbooks: Array<Book & {BookAuthor: BookAuthor}>;\n```\n\n### 一对一\n\n对于一对一，使用 `@HasOne(...)`（关系的外键存在于另一个模型上）和 `@BelongsTo(...)`（关系的外键存在于此模型上）。\n\n比如：\n\n```typescript\nimport { Table, Column, Model, BelongsTo, ForeignKey } from 'sequelize-typescript';\nimport { User } from './user.entity';\n\n@Table\nexport class Photo extends Model {\n  @ForeignKey(() => User)\n  @Column({\n    comment: '用户Id',\n  })\n  userId: number;\n\n  @BelongsTo(() => User)\n  user: User;\n\n  @Column({\n    comment: '名字',\n  })\n  name: string;\n}\n\n@Table\nexport class User extends Model {\n  @Column\n  name: string;\n}\n```\n\n\n\n### 模型循环依赖\n\n如果你使用了 `@BelongsTo` 装饰器，很容易触发一个模型循环依赖的错误，比如：\n\n```\nReferenceError: Cannot access 'Photo' before initialization\n```\n\n你可以将类型使用 `ReturnType` 包裹起来。\n\n```typescript\nimport { Table, Column, Model, BelongsTo, ForeignKey } from 'sequelize-typescript';\nimport { User } from './user.entity';\n\n@Table\nexport class Photo extends Model {\n  // ...\n  @BelongsTo(() => User)\n  user: ReturnType<() => User>;\n}\n```\n\n\n\n\n\n## 静态操作方法\n\n如果是单个数据源，可以使用下面的静态方法。\n\n### 保存\n\n在需要调用的地方，使用实体模型来操作。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Person } from '../entity/person.entity';\n\n@Provide()\nexport class PersonService {\n  async createPerson() {\n    const person = new Person({ name: 'bob', age: 99 });\n    await person.save();\n  }\n}\n```\n\n### 查找和更新\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Person } from '../entity/person.entity';\n\n@Provide()\nexport class PersonService {\n  async updatePerson() {\n    const person = await Person.findOne();\n    // 更新\n    person.age = 100;\n    await person.save();\n\n    await Person.update(\n      {\n        name: 'bobby',\n      },\n      {\n        where: { id: 1 },\n      }\n    );\n  }\n}\n```\n\n## Repository 模式\n\nRepository 模式可以将查找、创建等静态操作从模型定义中分离出来。它还支持与多个 sequelize 实例（多数据源）一起使用。\n\n### 启动 Repository 模式\n\n和数据源配置相同，只是多了一个属性。\n\n```typescript\n// src/config/config.default.ts\n\nimport { Person } from '../entity/person.entity';\n\nexport default {\n  // ...\n  sequelize: {\n    dataSource: {\n      default: {\n        // ...\n        entities: [Person],\n\n        // 多了这一个\n        repositoryMode: true,\n      },\n    },\n    sync: false,\n  },\n};\n```\n\n如果是多个数据源，务必在每个数据源都开启该属性，开启后，原有的静态操作方法不再可用。\n\n你需要使用 `Repository` 的操作方式。\n\n### 使用 Repository 模式\n\n基本 API 和静态操作相同，Midway 对其进行了一些简单包裹，使用 `InjectRepository` 装饰器可以在服务中注入 `Repository`。\n\n```typescript\nimport { Controller, Get } from '@midwayjs/core';\nimport { InjectRepository } from '@midwayjs/sequelize';\nimport { Photo } from '../entity/photo.entity';\nimport { User } from '../entity/user.entity';\nimport { Op } from 'sequelize';\nimport { Repository } from 'sequelize-typescript';\n\n@Controller('/')\nexport class HomeController {\n  @InjectRepository(User)\n  userRepository: Repository<User>;\n\n  @InjectRepository(Photo)\n  photoRepository: Repository<Photo>;\n\n  @Get('/')\n  async home() {\n    // 查询\n    let result = await this.photoRepository.findAll();\n    console.log(result);\n\n    // 新增\n    await this.photoRepository.create({\n      name: '123',\n    });\n\n    // 删除\n    await this.photoRepository.destroy({\n      where: {\n        name: '123',\n      },\n    });\n\n    // 联合查询\n    // SELECT * FROM photo WHERE name = \"23\" OR name = \"34\";\n    let result = await this.photoRepository.findAll({\n      where: {\n        [Op.or]: [{ name: '23' }, { name: '34' }],\n      },\n    });\n    // => result\n\n    // 连表查询\n    let result = await this.userRepository.findAll({ include: [Photo] });\n    // => result\n  }\n}\n```\n\n关于 OP 的更多用法：[https://sequelize.org/v5/manual/querying.html](https://sequelize.org/v5/manual/querying.html)\n\n### 多库的支持\n\n在 Repository 模式下，我们可以在 `InjectRepository` 参数中指定特定的数据源。\n\n```typescript\nimport { Controller } from '@midwayjs/core';\nimport { InjectRepository } from '@midwayjs/sequelize';\nimport { Photo } from '../entity/photo.entity';\nimport { User } from '../entity/user.entity';\nimport { Repository } from 'sequelize-typescript';\n\n@Controller('/')\nexport class HomeController {\n  // 指定某个数据源\n  @InjectRepository(User, 'default')\n  userRepository: Repository<User>;\n  // ...\n}\n```\n\n\n## 高级功能\n\n### 数据源同步配置\n\nsequelize 在同步数据源时可以添加 sync 的参数。\n\n```typescript\nexport default {\n  // ...\n  sequelize: {\n    dataSource: {\n      default: {\n        sync: true,\n        syncOptions: {\n          force: false,\n          alter: true,\n        },\n      },\n    },\n    // 多个数据源时可以用这个指定默认的数据源\n    defaultDataSourceName: 'default',\n  },\n};\n```\n\n### 指定默认数据源\n\n在包含多个数据源时，可以指定默认的数据源。\n\n```typescript\nexport default {\n  // ...\n  sequelize: {\n    dataSource: {\n      default1: {\n        // ...\n      },\n      default2: {\n        // ...\n      },\n    },\n    // 多个数据源时可以用这个指定默认的数据源\n    defaultDataSourceName: 'default1',\n  },\n};\n```\n\n\n\n### 获取数据源\n\n数据源即创建出的 sequelize 对象，我们可以通过注入内置的数据源管理器来获取。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport { SequelizeDataSourceManager } from '@midwayjs/sequelize';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n\n  async onReady(container: IMidwayContainer) {\n    const dataSourceManager = await container.getAsync(SequelizeDataSourceManager);\n    const conn = dataSourceManager.getDataSource('default');\n    await conn.authenticate();\n  }\n}\n```\n\n从 v3.8.0 开始，也可以通过装饰器注入。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport { InjectDataSource } from '@midwayjs/sequelize';\nimport { Sequelize } from 'sequelize-typescript';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n\n  // 注入默认数据源\n  @InjectDataSource()\n  defaultDataSource: Sequelize;\n\n  // 注入自定义数据源\n  @InjectDataSource('default1')\n  customDataSource: Sequelize;\n\n  async onReady(container: IMidwayContainer) {\n    // ...\n  }\n}\n```\n\n\n\n## 常见问题\n\n### 1、Dialect needs to be explicitly supplied as of v4.0.0\n\n原因为配置中数据源没有指定 `dialect` 字段，确认数据源的结构，格式以及配置合并的结果。\n\n\n\n### 2、生成实体列\n\n请参考社区提供的模块，如 [sequelize-typescript-generator](https://github.com/spinlud/sequelize-typescript-generator)\n\n\n\n### 3、Raw Query\n\n如果遇到比较复杂的，可以使用 [raw query 方法](https://sequelize.org/v5/manual/raw-queries.html)\n\n\n\n### 4、TS2612 错误\n\n如果你的模型列报了 TS2612 错误，比如：\n\n```\nsrc/entity/AesTenantConfigInfo.ts:29:6 - error TS2612: Property 'id' will overwrite the base property in 'Model<AesTenantConfigInfoAttributes, AesTenantConfigInfoAttributes>'. If this is intentional, add an initializer. Otherwise, add a 'declare' modifier or remove the redundant declaration.\n\n29      id?: number;\n        ~~\n```\n\n可以将其赋一个空值。\n\n```typescript\nimport { Table, Column } from 'sequelize-typescript';\n\n@Table\nexport class User extends Model {\n  @Column({\n    primaryKey: true,\n    autoIncrement: true,\n    type: DataType.BIGINT,\n  })\n  id?: number = undefined;\n}\n```\n\n\n\n## 其他\n\n- 上面的文档，翻译自 sequelize-typescript，更多 API ，请参考 [英文文档](<(https://github.com/sequelize/sequelize-typescrip)>)\n- 一些 [案例](https://github.com/ddzyan/midway-practice)\n","site/docs/extensions/socketio.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Fsocketio.md -->","site/docs/extensions/static_file.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Fstatic_file.md -->","site/docs/extensions/swagger.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Fswagger.md -->","site/docs/extensions/tablestore.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Ftablestore.md -->","site/docs/extensions/tags.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Ftags.md -->","site/docs/extensions/tenant.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Ftenant.md -->","site/docs/extensions/upload.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Fupload.md -->","site/docs/extensions/validate.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Fvalidate.md -->","site/docs/extensions/validation.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Fvalidation.md -->","site/docs/extensions/ws.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Fws.md -->"},"files":{".codex/skills/ui-ux-pro-max/SKILL.md":"---\nname: ui-ux-pro-max\ndescription: \"UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 8 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient.\"\n---\n\n# UI/UX Pro Max - Design Intelligence\n\nSearchable database of UI styles, color palettes, font pairings, chart types, product recommendations, UX guidelines, and stack-specific best practices.\n\n## Prerequisites\n\nCheck if Python is installed:\n\n```bash\npython3 --version || python --version\n```\n\nIf Python is not installed, install it based on user's OS:\n\n**macOS:**\n```bash\nbrew install python3\n```\n\n**Ubuntu/Debian:**\n```bash\nsudo apt update && sudo apt install python3\n```\n\n**Windows:**\n```powershell\nwinget install Python.Python.3.12\n```\n\n---\n\n## How to Use This Skill\n\nWhen user requests UI/UX work (design, build, create, implement, review, fix, improve), follow this workflow:\n\n### Step 1: Analyze User Requirements\n\nExtract key information from user request:\n- **Product type**: SaaS, e-commerce, portfolio, dashboard, landing page, etc.\n- **Style keywords**: minimal, playful, professional, elegant, dark mode, etc.\n- **Industry**: healthcare, fintech, gaming, education, etc.\n- **Stack**: React, Vue, Next.js, or default to `html-tailwind`\n\n### Step 2: Search Relevant Domains\n\nUse `search.py` multiple times to gather comprehensive information. Search until you have enough context.\n\n```bash\npython3 .shared/ui-ux-pro-max/scripts/search.py \"<keyword>\" --domain <domain> [-n <max_results>]\n```\n\n**Recommended search order:**\n\n1. **Product** - Get style recommendations for product type\n2. **Style** - Get detailed style guide (colors, effects, frameworks)\n3. **Typography** - Get font pairings with Google Fonts imports\n4. **Color** - Get color palette (Primary, Secondary, CTA, Background, Text, Border)\n5. **Landing** - Get page structure (if landing page)\n6. **Chart** - Get chart recommendations (if dashboard/analytics)\n7. **UX** - Get best practices and anti-patterns\n8. **Stack** - Get stack-specific guidelines (default: html-tailwind)\n\n### Step 3: Stack Guidelines (Default: html-tailwind)\n\nIf user doesn't specify a stack, **default to `html-tailwind`**.\n\n```bash\npython3 .shared/ui-ux-pro-max/scripts/search.py \"<keyword>\" --stack html-tailwind\n```\n\nAvailable stacks: `html-tailwind`, `react`, `nextjs`, `vue`, `svelte`, `swiftui`, `react-native`, `flutter`\n\n---\n\n## Search Reference\n\n### Available Domains\n\n| Domain | Use For | Example Keywords |\n|--------|---------|------------------|\n| `product` | Product type recommendations | SaaS, e-commerce, portfolio, healthcare, beauty, service |\n| `style` | UI styles, colors, effects | glassmorphism, minimalism, dark mode, brutalism |\n| `typography` | Font pairings, Google Fonts | elegant, playful, professional, modern |\n| `color` | Color palettes by product type | saas, ecommerce, healthcare, beauty, fintech, service |\n| `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof |\n| `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie |\n| `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading |\n| `prompt` | AI prompts, CSS keywords | (style name) |\n\n### Available Stacks\n\n| Stack | Focus |\n|-------|-------|\n| `html-tailwind` | Tailwind utilities, responsive, a11y (DEFAULT) |\n| `react` | State, hooks, performance, patterns |\n| `nextjs` | SSR, routing, images, API routes |\n| `vue` | Composition API, Pinia, Vue Router |\n| `svelte` | Runes, stores, SvelteKit |\n| `swiftui` | Views, State, Navigation, Animation |\n| `react-native` | Components, Navigation, Lists |\n| `flutter` | Widgets, State, Layout, Theming |\n\n---\n\n## Example Workflow\n\n**User request:** \"Làm landing page cho dịch vụ chăm sóc da chuyên nghiệp\"\n\n**AI should:**\n\n```bash\n# 1. Search product type\npython3 .shared/ui-ux-pro-max/scripts/search.py \"beauty spa wellness service\" --domain product\n\n# 2. Search style (based on industry: beauty, elegant)\npython3 .shared/ui-ux-pro-max/scripts/search.py \"elegant minimal soft\" --domain style\n\n# 3. Search typography\npython3 .shared/ui-ux-pro-max/scripts/search.py \"elegant luxury\" --domain typography\n\n# 4. Search color palette\npython3 .shared/ui-ux-pro-max/scripts/search.py \"beauty spa wellness\" --domain color\n\n# 5. Search landing page structure\npython3 .shared/ui-ux-pro-max/scripts/search.py \"hero-centric social-proof\" --domain landing\n\n# 6. Search UX guidelines\npython3 .shared/ui-ux-pro-max/scripts/search.py \"animation\" --domain ux\npython3 .shared/ui-ux-pro-max/scripts/search.py \"accessibility\" --domain ux\n\n# 7. Search stack guidelines (default: html-tailwind)\npython3 .shared/ui-ux-pro-max/scripts/search.py \"layout responsive\" --stack html-tailwind\n```\n\n**Then:** Synthesize all search results and implement the design.\n\n---\n\n## Tips for Better Results\n\n1. **Be specific with keywords** - \"healthcare SaaS dashboard\" > \"app\"\n2. **Search multiple times** - Different keywords reveal different insights\n3. **Combine domains** - Style + Typography + Color = Complete design system\n4. **Always check UX** - Search \"animation\", \"z-index\", \"accessibility\" for common issues\n5. **Use stack flag** - Get implementation-specific best practices\n6. **Iterate** - If first search doesn't match, try different keywords\n7. **Split Into Multiple Files** - For better maintainability:\n   - Separate components into individual files (e.g., `Header.tsx`, `Footer.tsx`)\n   - Extract reusable styles into dedicated files\n   - Keep each file focused and under 200-300 lines\n\n---\n\n## Common Rules for Professional UI\n\nThese are frequently overlooked issues that make UI look unprofessional:\n\n### Icons & Visual Elements\n\n| Rule | Do | Don't |\n|------|----|----- |\n| **No emoji icons** | Use SVG icons (Heroicons, Lucide, Simple Icons) | Use emojis like 🎨 🚀 ⚙️ as UI icons |\n| **Stable hover states** | Use color/opacity transitions on hover | Use scale transforms that shift layout |\n| **Correct brand logos** | Research official SVG from Simple Icons | Guess or use incorrect logo paths |\n| **Consistent icon sizing** | Use fixed viewBox (24x24) with w-6 h-6 | Mix different icon sizes randomly |\n\n### Interaction & Cursor\n\n| Rule | Do | Don't |\n|------|----|----- |\n| **Cursor pointer** | Add `cursor-pointer` to all clickable/hoverable cards | Leave default cursor on interactive elements |\n| **Hover feedback** | Provide visual feedback (color, shadow, border) | No indication element is interactive |\n| **Smooth transitions** | Use `transition-colors duration-200` | Instant state changes or too slow (>500ms) |\n\n### Light/Dark Mode Contrast\n\n| Rule | Do | Don't |\n|------|----|----- |\n| **Glass card light mode** | Use `bg-white/80` or higher opacity | Use `bg-white/10` (too transparent) |\n| **Text contrast light** | Use `#0F172A` (slate-900) for text | Use `#94A3B8` (slate-400) for body text |\n| **Muted text light** | Use `#475569` (slate-600) minimum | Use gray-400 or lighter |\n| **Border visibility** | Use `border-gray-200` in light mode | Use `border-white/10` (invisible) |\n\n### Layout & Spacing\n\n| Rule | Do | Don't |\n|------|----|----- |\n| **Floating navbar** | Add `top-4 left-4 right-4` spacing | Stick navbar to `top-0 left-0 right-0` |\n| **Content padding** | Account for fixed navbar height | Let content hide behind fixed elements |\n| **Consistent max-width** | Use same `max-w-6xl` or `max-w-7xl` | Mix different container widths |\n\n---\n\n## Pre-Delivery Checklist\n\nBefore delivering UI code, verify these items:\n\n### Visual Quality\n- [ ] No emojis used as icons (use SVG instead)\n- [ ] All icons from consistent icon set (Heroicons/Lucide)\n",".cursor/commands/openspec-apply.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=.cursor%2Fcommands%2Fopenspec-apply.md -->",".cursor/commands/openspec-archive.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=.cursor%2Fcommands%2Fopenspec-archive.md -->",".cursor/commands/openspec-proposal.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=.cursor%2Fcommands%2Fopenspec-proposal.md -->","AGENTS.md":"<!-- OPENSPEC:START -->\n# OpenSpec Instructions\n\nThese instructions are for AI assistants working in this project.\n\nAlways open `@/openspec/AGENTS.md` when the request:\n- Mentions planning or proposals (words like proposal, spec, change, plan)\n- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work\n- Sounds ambiguous and you need the authoritative spec before coding\n\nUse `@/openspec/AGENTS.md` to learn:\n- How to create and apply change proposals\n- Spec format and conventions\n- Project structure and guidelines\n\nKeep this managed block so 'openspec update' can refresh the instructions.\n\n<!-- OPENSPEC:END -->\n\n# Repository Guide for Agents\n\n## Project overview\n- Midway v4 monorepo managed by lerna + pnpm workspace.\n- TypeScript 5, built with `tsc`, tested with `jest`.\n- IoC container implementation lives in `packages/core` (MidwayContainer, DecoratorManager, MetadataManager).\n\n## Code rules\n- Each component is an independent npm package and can communicate via DI.\n- New/changed components must include tests. Run `npm run test` in the package.\n- Use `mwts` for linting (`npm run lint:fix` when needed).\n- After code changes, rerun the relevant lint command and keep necessary lint-fix output in the patch instead of reverting it casually.\n- Add meaningful comments for functions/classes/interfaces/enums.\n\n## Docs rules\n- Current docs live under `site/docs` (Docusaurus).\n- Match existing doc style/tone/structure for consistency.\n- Show dependency installation in both bash (npm) and JSON `package.json` forms.\n- Doc changes do not require a build check.\n- For component docs, write for beginners first: start from what the component solves, when to use it, and the simplest working path, then go deeper into advanced usage, configuration, and extension points. Prefer tutorial-style progression over API-only lists.\n\n## Practical tips\n- Prefer `pnpm -C <package> test` to scope tests.\n- Keep changes aligned with existing patterns in the targeted package.\n- Pull request titles must use standard commit format, such as `feat: xxx` or `fix: xxx`.\n","openspec/AGENTS.md":"# OpenSpec Instructions\n\nInstructions for AI coding assistants using OpenSpec for spec-driven development.\n\n## TL;DR Quick Checklist\n\n- Search existing work: `openspec spec list --long`, `openspec list` (use `rg` only for full-text search)\n- Decide scope: new capability vs modify existing capability\n- Pick a unique `change-id`: kebab-case, verb-led (`add-`, `update-`, `remove-`, `refactor-`)\n- Scaffold: `proposal.md`, `tasks.md`, `design.md` (only if needed), and delta specs per affected capability\n- Write deltas: use `## ADDED|MODIFIED|REMOVED|RENAMED Requirements`; include at least one `#### Scenario:` per requirement\n- Validate: `openspec validate [change-id] --strict --no-interactive` and fix issues\n- Request approval: Do not start implementation until proposal is approved\n\n## Three-Stage Workflow\n\n### Stage 1: Creating Changes\nCreate proposal when you need to:\n- Add features or functionality\n- Make breaking changes (API, schema)\n- Change architecture or patterns  \n- Optimize performance (changes behavior)\n- Update security patterns\n\nTriggers (examples):\n- \"Help me create a change proposal\"\n- \"Help me plan a change\"\n- \"Help me create a proposal\"\n- \"I want to create a spec proposal\"\n- \"I want to create a spec\"\n\nLoose matching guidance:\n- Contains one of: `proposal`, `change`, `spec`\n- With one of: `create`, `plan`, `make`, `start`, `help`\n\nSkip proposal for:\n- Bug fixes (restore intended behavior)\n- Typos, formatting, comments\n- Dependency updates (non-breaking)\n- Configuration changes\n- Tests for existing behavior\n\n**Workflow**\n1. Review `openspec/project.md`, `openspec list`, and `openspec list --specs` to understand current context.\n2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, optional `design.md`, and spec deltas under `openspec/changes/<id>/`.\n3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement.\n4. Run `openspec validate <id> --strict --no-interactive` and resolve any issues before sharing the proposal.\n\n### Stage 2: Implementing Changes\nTrack these steps as TODOs and complete them one by one.\n1. **Read proposal.md** - Understand what's being built\n2. **Read design.md** (if exists) - Review technical decisions\n3. **Read tasks.md** - Get implementation checklist\n4. **Implement tasks sequentially** - Complete in order\n5. **Confirm completion** - Ensure every item in `tasks.md` is finished before updating statuses\n6. **Update checklist** - After all work is done, set every task to `- [x]` so the list reflects reality\n7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved\n\n### Stage 3: Archiving Changes\nAfter deployment, create separate PR to:\n- Move `changes/[name]/` → `changes/archive/YYYY-MM-DD-[name]/`\n- Update `specs/` if capabilities changed\n- Use `openspec archive <change-id> --skip-specs --yes` for tooling-only changes (always pass the change ID explicitly)\n- Run `openspec validate --strict --no-interactive` to confirm the archived change passes checks\n\n## Before Any Task\n\n**Context Checklist:**\n- [ ] Read relevant specs in `specs/[capability]/spec.md`\n- [ ] Check pending changes in `changes/` for conflicts\n- [ ] Read `openspec/project.md` for conventions\n- [ ] Run `openspec list` to see active changes\n- [ ] Run `openspec list --specs` to see existing capabilities\n\n**Before Creating Specs:**\n- Always check if capability already exists\n- Prefer modifying existing specs over creating duplicates\n- Use `openspec show [spec]` to review current state\n- If request is ambiguous, ask 1–2 clarifying questions before scaffolding\n\n### Search Guidance\n- Enumerate specs: `openspec spec list --long` (or `--json` for scripts)\n- Enumerate changes: `openspec list` (or `openspec change list --json` - deprecated but available)\n- Show details:\n  - Spec: `openspec show <spec-id> --type spec` (use `--json` for filters)\n  - Change: `openspec show <change-id> --json --deltas-only`\n- Full-text search (use ripgrep): `rg -n \"Requirement:|Scenario:\" openspec/specs`\n\n## Quick Start\n\n### CLI Commands\n\n```bash\n# Essential commands\nopenspec list                  # List active changes\nopenspec list --specs          # List specifications\nopenspec show [item]           # Display change or spec\nopenspec validate [item]       # Validate changes or specs\nopenspec archive <change-id> [--yes|-y]   # Archive after deployment (add --yes for non-interactive runs)\n\n# Project management\nopenspec init [path]           # Initialize OpenSpec\nopenspec update [path]         # Update instruction files\n\n# Interactive mode\nopenspec show                  # Prompts for selection\nopenspec validate              # Bulk validation mode\n\n# Debugging\nopenspec show [change] --json --deltas-only\nopenspec validate [change] --strict --no-interactive\n```\n\n### Command Flags\n\n- `--json` - Machine-readable output\n- `--type change|spec` - Disambiguate items\n- `--strict` - Comprehensive validation\n- `--no-interactive` - Disable prompts\n- `--skip-specs` - Archive without spec updates\n- `--yes`/`-y` - Skip confirmation prompts (non-interactive archive)\n\n## Directory Structure\n\n```\nopenspec/\n├── project.md              # Project conventions\n├── specs/                  # Current truth - what IS built\n│   └── [capability]/       # Single focused capability\n│       ├── spec.md         # Requirements and scenarios\n│       └── design.md       # Technical patterns\n├── changes/                # Proposals - what SHOULD change\n│   ├── [change-name]/\n│   │   ├── proposal.md     # Why, what, impact\n│   │   ├── tasks.md        # Implementation checklist\n│   │   ├── design.md       # Technical decisions (optional; see criteria)\n│   │   └── specs/          # Delta changes\n│   │       └── [capability]/\n│   │           └── spec.md # ADDED/MODIFIED/REMOVED\n│   └── archive/            # Completed changes\n```\n\n## Creating Change Proposals\n\n### Decision Tree\n\n```\nNew request?\n├─ Bug fix restoring spec behavior? → Fix directly\n├─ Typo/format/comment? → Fix directly  \n├─ New feature/capability? → Create proposal\n├─ Breaking change? → Create proposal\n├─ Architecture change? → Create proposal\n└─ Unclear? → Create proposal (safer)\n```\n\n### Proposal Structure\n\n1. **Create directory:** `changes/[change-id]/` (kebab-case, verb-led, unique)\n\n2. **Write proposal.md:**\n```markdown\n# Change: [Brief description of change]\n\n## Why\n[1-2 sentences on problem/opportunity]\n\n## What Changes\n- [Bullet list of changes]\n- [Mark breaking changes with **BREAKING**]\n\n## Impact\n- Affected specs: [list capabilities]\n- Affected code: [key files/systems]\n```\n\n3. **Create spec deltas:** `specs/[capability]/spec.md`\n```markdown\n## ADDED Requirements\n### Requirement: New Feature\nThe system SHALL provide...\n\n#### Scenario: Success case\n- **WHEN** user performs action\n- **THEN** expected result\n\n## MODIFIED Requirements\n### Requirement: Existing Feature\n[Complete modified requirement]\n\n## REMOVED Requirements\n### Requirement: Old Feature\n**Reason**: [Why removing]\n**Migration**: [How to handle]\n```\nIf multiple capabilities are affected, create multiple delta files under `changes/[change-id]/specs/<capability>/spec.md`—one per capability.\n\n4. **Create tasks.md:**\n```markdown\n## 1. Implementation\n- [ ] 1.1 Create database schema\n- [ ] 1.2 Implement API endpoint\n- [ ] 1.3 Add frontend component\n- [ ] 1.4 Write tests\n```\n\n5. **Create design.md when needed:**\nCreate `design.md` if any of the following apply; otherwise omit it:\n- Cross-cutting change (multiple services/modules) or a new architectural pattern\n- New external dependency or significant data model changes\n- Security, performance, or migration complexity\n- Ambiguity that benefits from technical decisions before coding\n\nMinimal `design.md` skeleton:\n```markdown\n## Context\n[Background, constraints, stakeholders]\n\n## Goals / Non-Goals\n- Goals: [...]\n- Non-Goals: [...]\n\n## Decisions\n- Decision: [What and why]\n- Alternatives considered: [Options + rationale]\n\n## Risks / Trade-offs\n- [Risk] → Mitigation\n\n## Migration Plan\n[Steps, rollback]\n\n## Open Questions\n- [...]\n```\n\n## Spec File Format\n\n### Critical: Scenario Formatting\n\n**CORRECT** (use #### headers):\n```markdown\n#### Scenario: User login success\n- **WHEN** valid credentials provided\n- **THEN** return JWT token\n```\n\n**WRONG** (don't use bullets or bold):\n```markdown\n- **Scenario: User login**  ❌\n**Scenario**: User login     ❌\n### Scenario: User login      ❌\n```\n\nEvery requirement MUST have at least one scenario.\n\n### Requirement Wording\n- Use SHALL/MUST for normative requirements (avoid should/may unless intentionally non-normative)\n\n### Delta Operations\n\n- `## ADDED Requirements` - New capabilities\n- `## MODIFIED Requirements` - Changed behavior\n- `## REMOVED Requirements` - Deprecated features\n- `## RENAMED Requirements` - Name changes\n\nHeaders matched with `trim(header)` - whitespace ignored.\n\n#### When to use ADDED vs MODIFIED\n- ADDED: Introduces a new capability or sub-capability that can stand alone as a requirement. Prefer ADDED when the change is orthogonal (e.g., adding \"Slash Command Configuration\") rather than altering the semantics of an existing requirement.\n- MODIFIED: Changes the behavior, scope, or acceptance criteria of an existing requirement. Always paste the full, updated requirement content (header + all scenarios). The archiver will replace the entire requirement with what you provide here; partial deltas will drop previous details.\n- RENAMED: Use when only the name changes. If you also change behavior, use RENAMED (name) plus MODIFIED (content) referencing the new name.\n\nCommon pitfall: Using MODIFIED to add a new concern without including the previous text. This causes loss of detail at archive time. If you aren’t explicitly changing the existing requirement, add a new requirement under ADDED instead.\n\nAuthoring a MODIFIED requirement correctly:\n1) Locate the existing requirement in `openspec/specs/<capability>/spec.md`.\n2) Copy the entire requirement block (from `### Requirement: ...` through its scenarios).\n3) Paste it under `## MODIFIED Requirements` and edit to reflect the new behavior.\n4) Ensure the header text matches exactly (whitespace-insensitive) and keep at least one `#### Scenario:`.\n\nExample for RENAMED:\n```markdown\n## RENAMED Requirements\n- FROM: `### Requirement: Login`\n- TO: `### Requirement: User Authentication`\n```\n\n## Troubleshooting\n\n### Common Errors\n\n**\"Change must have at least one delta\"**\n- Check `changes/[name]/specs/` exists with .md files\n- Verify files have operation prefixes (## ADDED Requirements)\n\n**\"Requirement must have at least one scenario\"**\n- Check scenarios use `#### Scenario:` format (4 hashtags)\n- Don't use bullet points or bold for scenario headers\n\n**Silent scenario parsing failures**\n- Exact format required: `#### Scenario: Name`\n- Debug with: `openspec show [change] --json --deltas-only`\n\n### Validation Tips\n\n```bash\n# Always use strict mode for comprehensive checks\nopenspec validate [change] --strict --no-interactive\n\n# Debug delta parsing\nopenspec show [change] --json | jq '.deltas'\n\n# Check specific requirement\nopenspec show [spec] --json -r 1\n```\n\n## Happy Path Script\n\n```bash\n# 1) Explore current state\nopenspec spec list --long\nopenspec list\n# Optional full-text search:\n# rg -n \"Requirement:|Scenario:\" openspec/specs\n# rg -n \"^#|Requirement:\" openspec/changes\n\n# 2) Choose change id and scaffold\nCHANGE=add-two-factor-auth\nmkdir -p openspec/changes/$CHANGE/{specs/auth}\nprintf \"## Why\\n...\\n\\n## What Changes\\n- ...\\n\\n## Impact\\n- ...\\n\" > openspec/changes/$CHANGE/proposal.md\nprintf \"## 1. Implementation\\n- [ ] 1.1 ...\\n\" > openspec/changes/$CHANGE/tasks.md\n\n# 3) Add deltas (example)\ncat > openspec/changes/$CHANGE/specs/auth/spec.md << 'EOF'\n## ADDED Requirements\n### Requirement: Two-Factor Authentication\nUsers MUST provide a second factor during login.\n\n#### Scenario: OTP required\n- **WHEN** valid credentials are provided\n- **THEN** an OTP challenge is required\nEOF\n\n# 4) Validate\nopenspec validate $CHANGE --strict --no-interactive\n```\n\n## Multi-Capability Example\n\n```\nopenspec/changes/add-2fa-notify/\n├── proposal.md\n├── tasks.md\n└── specs/\n    ├── auth/\n    │   └── spec.md   # ADDED: Two-Factor Authentication\n    └── notifications/\n        └── spec.md   # ADDED: OTP email notification\n```\n\nauth/spec.md\n```markdown\n## ADDED Requirements\n### Requirement: Two-Factor Authentication\n...\n```\n\nnotifications/spec.md\n```markdown\n## ADDED Requirements\n### Requirement: OTP Email Notification\n...\n```\n\n## Best Practices\n\n### Simplicity First\n- Default to <100 lines of new code\n- Single-file implementations until proven insufficient\n- Avoid frameworks without clear justification\n- Choose boring, proven patterns\n\n### Complexity Triggers\nOnly add complexity with:\n- Performance data showing current solution too slow\n- Concrete scale requirements (>1000 users, >100MB data)\n- Multiple proven use cases requiring abstraction\n\n### Clear References\n- Use `file.ts:42` format for code locations\n- Reference specs as `specs/auth/spec.md`\n- Link related changes and PRs\n\n### Capability Naming\n- Use verb-noun: `user-auth`, `payment-capture`\n- Single purpose per capability\n- 10-minute understandability rule\n- Split if description needs \"AND\"\n\n### Change ID Naming\n- Use kebab-case, short and descriptive: `add-two-factor-auth`\n- Prefer verb-led prefixes: `add-`, `update-`, `remove-`, `refactor-`\n- Ensure uniqueness; if taken, append `-2`, `-3`, etc.\n\n## Tool Selection Guide\n\n| Task | Tool | Why |\n|------|------|-----|\n| Find files by pattern | Glob | Fast pattern matching |\n| Search code content | Grep | Optimized regex search |\n| Read specific files | Read | Direct file access |\n| Explore unknown scope | Task | Multi-step investigation |\n\n## Error Recovery\n\n### Change Conflicts\n1. Run `openspec list` to see active changes\n2. Check for overlapping specs\n3. Coordinate with change owners\n4. Consider combining proposals\n\n### Validation Failures\n1. Run with `--strict` flag\n2. Check JSON output for details\n3. Verify spec file format\n4. Ensure scenarios properly formatted\n\n### Missing Context\n1. Read project.md first\n2. Check related specs\n3. Review recent archives\n4. Ask for clarification\n\n## Quick Reference\n\n### Stage Indicators\n- `changes/` - Proposed, not yet built\n- `specs/` - Built and deployed\n- `archive/` - Completed changes\n\n### File Purposes\n- `proposal.md` - Why and what\n- `tasks.md` - Implementation steps\n- `design.md` - Technical decisions\n- `spec.md` - Requirements and behavior\n\n### CLI Essentials\n```bash\nopenspec list              # What's in progress?\nopenspec show [item]       # View details\nopenspec validate --strict --no-interactive  # Is it correct?\nopenspec archive <change-id> [--yes|-y]  # Mark complete (add --yes for automation)\n```\n\nRemember: Specs are truth. Changes are proposals. Keep them in sync.\n","packages/api-bridge/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fapi-bridge%2FREADME.md -->","packages/axios/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Faxios%2FREADME.md -->","packages/bootstrap/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fbootstrap%2FREADME.md -->","packages/bull-board/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fbull-board%2FREADME.md -->","packages/bull/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fbull%2FREADME.md -->","packages/bullmq/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fbullmq%2FREADME.md -->","packages/busboy/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fbusboy%2FREADME.md -->","packages/cache-manager/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcache-manager%2FREADME.md -->","packages/captcha/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcaptcha%2FREADME.md -->","packages/casbin-redis-adapter/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcasbin-redis-adapter%2FREADME.md -->","packages/casbin-typeorm-adapter/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcasbin-typeorm-adapter%2FREADME.md -->","packages/casbin/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcasbin%2FREADME.md -->","packages/code-dye/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcode-dye%2FREADME.md -->","packages/consul/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fconsul%2FREADME.md -->","packages/core/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcore%2FREADME.md -->","packages/cos/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcos%2FREADME.md -->","packages/cron/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcron%2FREADME.md -->","packages/cross-domain/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcross-domain%2FREADME.md -->","packages/crud/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcrud%2FREADME.md -->","packages/etcd/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fetcd%2FREADME.md -->","packages/event-emitter/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fevent-emitter%2FREADME.md -->","packages/express-session/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fexpress-session%2FREADME.md -->","packages/faas/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Ffaas%2FREADME.md -->","packages/grpc/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fgrpc%2FREADME.md -->","packages/http-proxy/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fhttp-proxy%2FREADME.md -->","packages/info/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Finfo%2FREADME.md -->","packages/jwt/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fjwt%2FREADME.md -->","packages/kafka/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fkafka%2FREADME.md -->","packages/leoric/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fleoric%2FREADME.md -->","packages/mcp/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fmcp%2FREADME.md -->","packages/mikro/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fmikro%2FREADME.md -->","packages/mikro7/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fmikro7%2FREADME.md -->","packages/mock/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fmock%2FREADME.md -->","packages/mongoose/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fmongoose%2FREADME.md -->","packages/mqtt/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fmqtt%2FREADME.md -->","packages/nextjs/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fnextjs%2FREADME.md -->","packages/oss/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Foss%2FREADME.md -->","packages/passport/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fpassport%2FREADME.md -->","packages/piscina/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fpiscina%2FREADME.md -->","packages/prometheus/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fprometheus%2FREADME.md -->","packages/rabbitmq/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Frabbitmq%2FREADME.md -->","packages/react/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Freact%2FREADME.md -->","packages/redis/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fredis%2FREADME.md -->","packages/security/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fsecurity%2FREADME.md -->","packages/sequelize/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fsequelize%2FREADME.md -->","packages/session/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fsession%2FREADME.md -->","packages/skill-midway/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fskill-midway%2FREADME.md -->","packages/skill-midway/skills/midway/SKILL.md":"---\nname: midway\ndescription: Use when the task involves Midway framework docs, package selection, API lookup, version compatibility, MCP/HTTP/framework components, or migration guidance. Resolve the requested Midway version first, then query the local Midway skill bundle with `midway-skill` commands.\n---\n\n# Midway\n\nUse the local `@midwayjs/skill-midway` bundle before relying on memory.\n\n## Workflow\n\n1. Resolve the user's requested version first.\n   Run `npm exec midway-skill -- resolve-version <version>` or `pnpm exec midway-skill resolve-version <version>`.\n   If no version is given, use `current`.\n\n2. Query the right source for the question.\n   Use `npm exec midway-skill -- lookup-docs --query \"<topic>\"` or `pnpm exec midway-skill lookup-docs --query \"<topic>\"`.\n   Use `npm exec midway-skill -- lookup-api --symbol \"<symbol>\"` or `pnpm exec midway-skill lookup-api --symbol \"<symbol>\"` for current-major API lookups.\n   Use `npm exec midway-skill -- lookup-packages --query \"<package or feature>\"` or `pnpm exec midway-skill lookup-packages --query \"<package or feature>\"`.\n   Use `npm exec midway-skill -- lookup-changelog --package \"<package>\"` or `pnpm exec midway-skill lookup-changelog --package \"<package>\"` for release history.\n\n3. Cite what the bundle resolved.\n   Include the requested version, resolved version, and whether it was an exact match or major-version fallback.\n\n## Rules\n\n- Prefer bundle results over recollection.\n- Historical majors only guarantee `docs` and `changelog`.\n- If `resolve-version` reports `api: false`, do not claim exact historical API details.\n- When results are empty, say that the bundle did not contain a match instead of inventing one.\n- Prefer package names with the `@midwayjs/` scope in answers when known.\n\n## Output\n\n- State the resolved Midway version.\n- Summarize the matched docs/API/package/changelog result.\n- Keep links or source paths from bundle records when relevant.\n","packages/socketio/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fsocketio%2FREADME.md -->","packages/static-file/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fstatic-file%2FREADME.md -->","packages/swagger/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fswagger%2FREADME.md -->","packages/tablestore/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Ftablestore%2FREADME.md -->","packages/tags/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Ftags%2FREADME.md -->","packages/tenant/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Ftenant%2FREADME.md -->","packages/typeorm/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Ftypeorm%2FREADME.md -->","packages/upload/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fupload%2FREADME.md -->","packages/validate/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fvalidate%2FREADME.md -->","packages/validation-joi/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fvalidation-joi%2FREADME.md -->","packages/validation-zod/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fvalidation-zod%2FREADME.md -->","packages/validation-zod4/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fvalidation-zod4%2FREADME.md -->","packages/validation/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fvalidation%2FREADME.md -->","packages/view-ejs/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fview-ejs%2FREADME.md -->","packages/view-nunjucks/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fview-nunjucks%2FREADME.md -->","packages/view/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fview%2FREADME.md -->","packages/vue/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fvue%2FREADME.md -->","packages/web-bridge/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fweb-bridge%2FREADME.md -->","packages/web-express/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fweb-express%2FREADME.md -->","packages/web-koa/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fweb-koa%2FREADME.md -->","packages/web/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fweb%2FREADME.md -->","packages/ws/README.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fws%2FREADME.md -->","site/docs/extensions/alinode.md":"# Alinode\n\n## 准备工作\n\n需要接入的应用是要部署在独立的服务获取云环境，可以接入互联网服务。\n\n## 创建服务\n\n**第一步**\n\n登录阿里云，点击开通 [阿里云的 Node.js 性能平台](https://www.aliyun.com/product/nodejs) 的服务。\n\n**第二步**\n\n创建新应用，获取 APP ID 和 App Secret。\n<img src=\"https://cdn.nlark.com/yuque/0/2021/png/187105/1617267785895-dd0fb702-91c7-4b25-9c64-8a9358f2d254.png#align=left&display=inline&height=351&margin=%5Bobject%20Object%5D&name=image.png&originHeight=702&originWidth=1548&size=106487&status=done&style=none&width=774\" width=\"774\" />\n\n## 安装监控依赖\n\n**第一步**\n\n安装 Node.js 性能平台所需的组件\n\n```bash\n# 安装版本管理工具 tnvm，安装过程出错参考：https://github.com/aliyun-node/tnvm\n$ wget -O- https://raw.githubusercontent.com/aliyun-node/tnvm/master/install.sh | bash\n$ source ~/.bashrc\n\n# tnvm ls-remote alinode \t\t\t\t\t\t\t\t# 查看需要的版本\n$ tnvm install alinode-v6.5.0 \t\t\t\t\t# 安装需要的版本\n$ tnvm use alinode-v6.5.0 \t\t\t\t\t\t\t# 使用需要的版本\n\n$ npm install @alicloud/agenthub -g \t\t# 安装 agenthub\n```\n\n这里有三个部分\n\n- 1、安装 tnvm（alinode 源）\n- 2、使用 tnvm 安装 alinode（替代默认的 node）\n- 3、安装 alinode 需要的数据采集器\n\n安装完成后，可以检查一下，需要确保 `which node` 和 `which agenthub` 的路径中包括 `.tnvm` 即可。\n\n```bash\n$ which node\n/root/.tnvm/versions/alinode/v3.11.4/bin/node\n\n$ which agenthub\n/root/.tnvm/versions/alinode/v3.11.4/bin/agenthub\n```\n\n将 `创建新应用` 中获得的 `App ID` 和 `App Secret` 按如下所示保存为 `yourconfig.json`。比如放在项目根目录。\n\n```typescript\n{\n  \"appid\": \"****\",\n  \"secret\": \"****\",\n}\n```\n\n启动插件：\n\n```typescript\nagenthub start yourconfig.json\n```\n\n## 启动 node 服务\n\n在安装了服务器中，启动 Node 服务时，需要加入 ENABLE_NODE_LOG=YES 环境变量。\n\n比如：\n\n```bash\n$ NODE_ENV=production ENABLE_NODE_LOG=YES node bootstrap.js\n```\n\n## Docker 容器的方法\n\n关于 docker 容器的方法可以查看 [文档](https://help.aliyun.com/document_detail/66027.html?spm=a2c4g.11186623.6.580.261ba70feI6mWt)。\n\n## 其他\n\n更多内容可以查看阿里云 Node.js 性能平台的 [文档](https://help.aliyun.com/document_detail/60338.html?spm=a2c4g.11186623.6.548.599312e6IkGO9v)。\n","site/docs/extensions/apollo.md":"# Apollo GraphQL\n\nGraphQL 适合需要让前端按需查询字段、聚合多个后端数据源，或者希望用统一查询语言暴露 API 的应用。Midway 通过 `@midwayjs/apollo` 提供基于 Apollo Server 的 GraphQL HTTP 端点，并让 Resolver 直接使用 Midway 的依赖注入和请求上下文。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n## 安装依赖\n\nApollo 组件已经内置 Apollo Server、GraphQL、WebSocket 订阅等运行时依赖，不需要额外安装 `@apollo/server` 或 `graphql`。\n\n```bash\n$ npm i @midwayjs/apollo@4 --save\n```\n\n如果使用 Koa 或 Express，还需要安装对应 Web 框架组件：\n\n```bash\n$ npm i @midwayjs/koa@4 --save\n# 或者\n$ npm i @midwayjs/express@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/apollo\": \"^4.0.0\",\n    \"@midwayjs/koa\": \"^4.0.0\"\n  }\n}\n```\n\n## 开启组件\n\n在 `src/configuration.ts` 中导入 Apollo 组件和一个 Web 框架组件。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as apollo from '@midwayjs/apollo';\n\n@Configuration({\n  imports: [\n    koa,\n    apollo,\n  ],\n})\nexport class MainConfiguration {}\n```\n\n组件默认会把 GraphQL 端点挂载到 `/graphql`，默认支持 `GET` 和 `POST` 请求。\n\n## 最小示例\n\n在 `src/config/config.default.ts` 中配置 GraphQL Schema 和 Resolver。\n\n```typescript\nexport const apollo = {\n  path: '/graphql',\n  typeDefs: `\n    type Query {\n      hello: String!\n    }\n  `,\n  resolvers: {\n    Query: {\n      hello: () => 'hello midway',\n    },\n  },\n};\n```\n\n启动应用后，请求 `/graphql`。\n\n```bash\n$ curl -X POST http://127.0.0.1:7001/graphql \\\n  -H 'content-type: application/json' \\\n  -d '{\"query\":\"{ hello }\"}'\n```\n\n返回：\n\n```json\n{\n  \"data\": {\n    \"hello\": \"hello midway\"\n  }\n}\n```\n\n开发环境下，也可以直接在浏览器访问 `/graphql` 打开 GraphiQL 调试页面。\n\n## 使用 Schema 文件\n\n当 Schema 较多时，推荐把 SDL 放到 `.graphql` 文件中，再通过 `typePaths` 加载。\n\n```graphql\n# src/graphql/schema.graphql\ntype Query {\n  user(id: ID!): User\n}\n\ntype Mutation {\n  updateUserName(id: ID!, name: String!): User\n}\n\ntype User {\n  id: ID!\n  name: String!\n}\n```\n\n配置文件中使用 `typePaths`。\n\n```typescript\nexport const apollo = {\n  path: '/graphql',\n  typePaths: ['./graphql/**/*.graphql'],\n};\n```\n\n`typePaths` 中的相对路径会从应用的 `baseDir` 开始解析，既支持精确文件路径，也支持 glob 表达式。你也可以同时配置 `typeDefs` 和 `typePaths`，组件会在启动时合并。\n\n## Resolver Map\n\n你可以直接在配置里写 Apollo Resolver Map。\n\n```typescript\nimport { UserService } from '../service/user';\n\nexport const apollo = {\n  typeDefs: `\n    type Query {\n      userName(id: ID!): String\n    }\n\n    type Mutation {\n      updateUserName(id: ID!, name: String!): String\n    }\n  `,\n  resolvers: {\n    Query: {\n      userName: async (_parent, args, context) => {\n        const userService = await context.requestContext.getAsync(UserService);\n        const user = await userService.findById(args.id);\n        return user?.name;\n      },\n    },\n    Mutation: {\n      updateUserName: async (_parent, args, context) => {\n        const userService = await context.requestContext.getAsync(UserService);\n        const user = await userService.updateName(args.id, args.name);\n        return user.name;\n      },\n    },\n  },\n};\n```\n\nResolver 的第三个参数就是当前 Midway 请求上下文，不需要再从 `context.ctx` 解包。Koa 场景下，`context` 是 Midway 增强后的 Koa ctx；Express 场景下，`context` 是 Midway 增强后的 Express request/context。\n\nMidway 会在上下文上挂载 `requestContext`、`logger`、`getLogger()`、`setAttr()`、`getAttr()` 和 `getApp()`。\n\n## Resolver 类\n\n也可以使用 Resolver 类来获得更自然的依赖注入体验。相关装饰器可以直接从 `@midwayjs/apollo` 中引入。\n\n```typescript\nimport { Inject } from '@midwayjs/core';\nimport { Args, Context, Mutation, Query, Resolver } from '@midwayjs/apollo';\nimport { UserService } from '../service/user';\n\n@Resolver()\nexport class UserResolver {\n  @Inject()\n  userService: UserService;\n\n  @Query('userName')\n  async userName(@Args('id') id: string, @Context() context) {\n    context.logger.info('query user %s', id);\n    const user = await this.userService.findById(id);\n    return user?.name;\n  }\n\n  @Mutation('updateUserName')\n  async updateUserName(@Args('id') id: string, @Args('name') name: string) {\n    const user = await this.userService.updateName(id, name);\n    return user.name;\n  }\n}\n```\n\nResolver 类会由 Midway 容器托管，支持 `@Inject()` 和请求级依赖。组件会自动发现使用 `@Resolver()` 标记的类，也可以通过 `resolverClasses` 显式传入。\n\n```typescript\nimport { UserResolver } from '../resolver/user';\n\nexport const apollo = {\n  typePaths: ['./graphql/**/*.graphql'],\n  resolverClasses: [UserResolver],\n};\n```\n\n可用的 Resolver 装饰器包括：\n\n| 装饰器            | 说明                     |\n| ----------------- | ------------------------ |\n| `@Resolver()`     | 标记一个 Resolver 类     |\n| `@Query()`        | 标记 Query 字段          |\n| `@Mutation()`     | 标记 Mutation 字段       |\n| `@Subscription()` | 标记 Subscription 字段   |\n\n可用的参数装饰器包括：\n\n| 装饰器       | 说明                     |\n| ------------ | ------------------------ |\n| `@Parent()`  | 获取父级 Resolver 返回值 |\n| `@Args()`    | 获取 GraphQL 参数        |\n| `@Context()` | 获取 Midway 请求上下文   |\n| `@Info()`    | 获取 GraphQL resolve info |\n\n不传参数时会注入完整对象，传入字段名时只注入对应字段，例如 `@Args('id')`。\n\n## 扩展 Context\n\n可以通过 `contextFactory` 给每次 GraphQL 请求补充字段。\n\n```typescript\nexport const apollo = {\n  typeDefs,\n  resolvers,\n  contextFactory: async context => {\n    return {\n      currentUserId: context.headers['x-user-id'],\n    };\n  },\n};\n```\n\n随后在 Resolver 中读取：\n\n```typescript\nconst userId = context.currentUserId;\n```\n\n内置字段如 `requestContext`、`logger`、`getLogger`、`setAttr`、`getAttr` 和 `getApp` 会被保留，不建议覆盖。\n\n## GraphiQL\n\n开发环境默认开启 GraphiQL。浏览器访问 `/graphql` 时会显示 GraphiQL 页面，生产环境默认关闭。\n\n```typescript\nexport const apollo = {\n  graphiql: true,\n};\n```\n\n可以显式关闭：\n\n```typescript\nexport const apollo = {\n  graphiql: false,\n};\n```\n\n也可以定制页面标题和请求地址：\n\n```typescript\nexport const apollo = {\n  graphiql: {\n    title: 'My GraphQL Console',\n    endpoint: '/graphql',\n  },\n};\n```\n\n## 订阅\n\n组件支持 `graphql-ws` 协议的 GraphQL Subscription。默认关闭，需要在配置中开启。\n\n```typescript\nexport const apollo = {\n  path: '/graphql',\n  subscriptions: true,\n  typeDefs: `\n    type Query {\n      hello: String!\n    }\n\n    type Subscription {\n      counter: Int!\n    }\n  `,\n};\n```\n\nResolver 方法需要返回 `AsyncIterable`。\n\n```typescript\nimport { Resolver, Subscription } from '@midwayjs/apollo';\n\n@Resolver()\nexport class CounterResolver {\n  @Subscription('counter')\n  async *counter() {\n    yield {\n      counter: 1,\n    };\n  }\n}\n```\n\n开启后，WebSocket 地址默认和 HTTP GraphQL 地址一致，例如 `ws://127.0.0.1:7001/graphql`。也可以单独指定订阅路径。\n\n```typescript\nexport const apollo = {\n  path: '/graphql',\n  subscriptions: {\n    path: '/graphql',\n    connectionInitWaitTimeout: 3000,\n  },\n};\n```\n\n客户端需要使用 `graphql-ws` 协议连接。\n\n```typescript\nimport { createClient } from 'graphql-ws';\n\nconst client = createClient({\n  url: 'ws://127.0.0.1:7001/graphql',\n});\n\nclient.subscribe(\n  {\n    query: 'subscription { counter }',\n  },\n  {\n    next: data => {\n      console.log(data);\n    },\n    error: err => {\n      console.error(err);\n    },\n    complete: () => {},\n  }\n);\n```\n\n## HTTP 行为\n\n默认只处理 `GET` 和 `POST` 请求。其他方法请求 GraphQL 路径会返回 `405 Method Not Allowed`。\n\n```typescript\nexport const apollo = {\n  methods: ['POST'],\n};\n```\n\n如果修改了 GraphQL 路径，HTTP 和订阅默认都会使用新的路径。\n\n```typescript\nexport const apollo = {\n  path: '/api/graphql',\n};\n```\n\n## Apollo Server 配置\n\nApollo Server 专属配置放在 `apollo.apollo` 下，避免和通用 GraphQL 字段混在一起。\n\n```typescript\nexport const apollo = {\n  typeDefs,\n  resolvers,\n  apollo: {\n    introspection: true,\n    plugins: [],\n  },\n};\n```\n\n常见配置如下：\n\n| 配置项           | 说明                                      |\n| ---------------- | ----------------------------------------- |\n| `path`           | GraphQL HTTP 路径，默认为 `/graphql`      |\n| `methods`        | 允许的 HTTP 方法，默认为 `['GET', 'POST']` |\n| `typeDefs`       | 内联 GraphQL SDL                          |\n| `typePaths`      | `.graphql` 文件路径或 glob 表达式         |\n| `resolvers`      | Apollo Resolver Map                       |\n| `resolverClasses` | Midway Resolver 类列表                   |\n| `contextFactory` | 扩展每次请求的 GraphQL context            |\n| `graphiql`       | GraphiQL 开关或配置                       |\n| `subscriptions`  | GraphQL WebSocket 订阅开关或配置          |\n| `apollo`         | 透传给 Apollo Server 的配置               |\n\n## 生产建议\n\n生产环境建议显式关闭 GraphiQL，并根据安全要求决定是否开启 introspection。\n\n```typescript\nexport const apollo = {\n  graphiql: false,\n  apollo: {\n    introspection: false,\n  },\n};\n```\n\n如果需要文件上传，推荐先通过 GraphQL 获取上传凭证，再由客户端直接上传到 OSS、S3 等对象存储。GraphQL multipart upload 不作为组件默认能力开启。\n","site/docs/extensions/axios.md":"# HTTP 请求\n\n\n\n## 简单的 HTTP 请求\n\nMidway 内置了一个简单的 HTTP 请求客户端，无需引入三方包即可使用。\n\n默认 Get 请求，返回数据为 Buffer。\n\n内置的 Http 客户端只提供最简单的能力，仅满足大部分的前端接口数据获取，如需复杂的功能，比如文件上传等，请使用其他的客户端，如 fetch，axios，got 等。\n\n\n\n### 简单方法形式\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nconst result = await makeHttpRequest('http://127.1:7001/');\n\n// Buffer.isBuffer(result.data)  => true\n```\n\nGet 请求，带上 Query，返回类型为 JSON。\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nconst result = await makeHttpRequest('http://127.1:7001/', {\n  data: {\n    a: 1,\n    b: 2\n  },\n  dataType: 'json',\t// 返回的数据格式\n});\n\n// typeof result.data => 'object'\n// result.data.url => /?a=1&b=2\n```\n\n可以指定类型\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nconst result = await makeHttpRequest('http://127.1:7001/', {\n  method: 'GET',\n  dataType: 'json',\n});\n```\n\n返回 text 格式。\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nconst result = await makeHttpRequest('http://127.1:7001/', {\n  method: 'GET',\n  dataType: 'text',\n});\n```\n\nPOST 请求并返回 JSON。\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nconst result = await makeHttpRequest('http://127.1:7001/', {\n  method: 'POST',\n  data: {\n    a: 1,\n    b: 2\n  },\n  dataType: 'json',\n  contentType:'json', \t// 发送的 post 为 json\n});\n\n// result.data ...\n```\n\n:::caution\n注意，请不要在请求中直接返回 result 对象，result 对象是标准的 httpResponse，在大部分场景下无法被直接序列化，会抛出对象循环的错误。\n:::\n\n设置请求超时时间。\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nlet err;\n// 超时会报错，注意 catch\ntry {\n  const result = await makeHttpRequest('http://127.1:7001/', {\n    method: 'GET',\n    dataType: 'text',\n    timeout: 500,\n  });\n} catch (e) {\n  err = e;\n}\n```\n\n\n\n### 实例形式\n\n```typescript\nimport { HttpClient } from '@midwayjs/core';\n\nconst httpclient = new HttpClient();\nconst result = await httpclient.request('http://127.1:7001/');\n\n// Buffer.isBuffer(result.data)  => true\n```\n\n和方法形式参数相同。\n\n```typescript\nimport { HttpClient } from '@midwayjs/core';\n\nconst httpclient = new HttpClient();\nconst result = await httpclient.request('http://127.1:7001/', {\n  method: 'POST',\n  data: {\n    a: 1,\n    b: 2\n  },\n  dataType: 'json',\n  contentType:'json', \t// 发送的 post 为 json\n});\n\n// result.data ...\n```\n\n示例形式，可以复用创建出的对象，并且每次请求，都可以带上一些固定的参数，比如 header。\n\n```typescript\nimport { HttpClient } from '@midwayjs/core';\n\nconst httpclient = new HttpClient({\n  headers: {\n    'x-timeout': '5'\n  },\n  method: 'POST',\n  timeout: 2000\n});\n\n// 每次都会带上 headers\nconst result = await httpclient.request('http://127.1:7001/');\n\n```\n\n\n\n\n\n## Axios 支持\n\nMidway 包裹了 [axios](https://github.com/axios/axios) 包，使得在代码中可以简单的使用 axios 接口。\n\n和 axios 的一些关系如下：\n\n- 接口完全一致\n- 适配依赖注入写法，完整的类型定义\n- 方便实例管理和配置统一\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n\n### 安装依赖\n\n```bash\n$ npm i @midwayjs/axios@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/axios\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n### 引入组件\n\n\n首先，引入 组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as axios from '@midwayjs/axios';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    axios\t\t// 导入 axios 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n然后在业务代码中即可注入使用。\n\n\n\n### 使用默认 Axios 实例\n\n\n接口和 [axios](https://github.com/axios/axios) 一致。\n\n\n```typescript\naxios.request(config)\naxios.get(url[, config])\naxios.delete(url[, config])\naxios.head(url[, config])\naxios.options(url[, config])\naxios.post(url[, data[, config]])\naxios.put(url[, data[, config]])\naxios.patch(url[, data[, config]])\naxios.postForm(url[, data[, config]])\naxios.putForm(url[, data[, config]])\naxios.patchForm(url[, data[, config]])\n```\n\n\n使用示例：\n```typescript\nimport { HttpService } from '@midwayjs/axios';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  httpService: HttpService;\n\n  async invoke() {\n  \tconst url = 'https://midwayjs.org/resource/101010100.json';\n    const result = await this.httpService.get(url);\n    // TODO result\n  }\n}\n```\n\n\n\n### 配置默认 Axios 实例\n\n\nHttpService 实例等价于 `axios.create` ，所以可以有一些配置参数，这些参数了 axios 本身的参数相同，我们可以在 `src/config.default.ts` 中配置它。\n\n\n比如：\n```typescript\nexport default {\n  // ...\n  axios: {\n    default: {\n      // 所有实例复用的配置\n    },\n    clients: {\n      // 默认实例的配置\n      default: {\n        baseURL: 'https://api.example.com',\n        // `headers` are custom headers to be sent\n        headers: {\n          'X-Requested-With': 'XMLHttpRequest'\n        },\n        timeout: 1000, // default is `0` (no timeout)\n\n        // `withCredentials` indicates whether or not cross-site Access-Control requests\n        // should be made using credentials\n        withCredentials: false, // default\n      },\n    }\n  }\n}\n```\n更多的参数可以参考 [axios global config](https://github.com/axios/axios#config-defaults)。\n\n\n\n### 创建不同实例\n\n和其他的服务多实例相同，配置不同的 key 即可。\n\n```typescript\nexport default {\n  // ...\n  axios: {\n    default: {\n      // 所有实例复用的配置\n    },\n    clients: {\n      default: {\n        // 默认实例\n      },\n      customAxios: {\n        // 自定义实例\n      }\n    }\n  }\n}\n```\n\n使用方式如下：\n\n```typescript\nimport { HttpServiceFactory, HttpService } from '@midwayjs/axios';\nimport { InjectClient } from '@midwayjs/core';\n\n@Provide()\nexport class UserService {\n\n  @InjectClient(HttpServiceFactory, 'customAxios')\n  customAxios: HttpService;\n\n  async invoke() {\n  \tconst url = 'https://midwayjs.org/resource/101010100.json';\n    const result = await this.customAxios.get(url);\n    // TODO result\n  }\n}\n```\n\n\n\n### 配置全局拦截器\n\n如果使用的是默认的 Axios 实例，可以如下配置。\n\n```javascript\nimport { Configuration, IMidwayContainer } from '@midwayjs/core';\nimport * as axios from '@midwayjs/axios';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    axios\t\t// 导入 axios 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n\n  async onReady(container: IMidwayContainer) {\n  \tconst httpService = await container.getAsync(axios.HttpService);\n    httpService.interceptors.request.use(\n      config => {\n        // Do something before request is sent\n        return config;\n      },\n      error => {\n        // Do something with request error\n        return Promise.reject(error);\n      }\n    );\n  }\n}\n```\n\n如果要给其他实例配置，可以参考下面的代码。\n\n```typescript\nimport { Configuration, IMidwayContainer } from '@midwayjs/core';\nimport * as axios from '@midwayjs/axios';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    axios\t\t// 导入 axios 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n\n  async onReady(container: IMidwayContainer) {\n  \tconst httpServiceFactory = await container.getAsync(axios.HttpServiceFactory);\n    const customAxios = httpServiceFactory.get('customAxios');\n    customAxios.interceptors.request.use(\n      config => {\n        //...\n      },\n      error => {\n        //...\n      }\n    );\n  }\n}\n```\n\n### 扩展 AxiosRequestConfig 类型\n\n当前 Axios 库中 [issue](https://github.com/axios/axios/issues?q=is%3Aissue%20state%3Aopen%20AxiosRequestConfig) 有不少扩展 `AxiosRequestConfig` 类型的需求，但是 axios 本身并不支持扩展这个类型，其中的泛型仅限于设置 data 属性的类型。\n\nMidway 中提供了 `AxiosRequestConfig` 的扩展，可以在组件层面增加新的属性。\n\n```typescript\n// interface.ts\nimport '@midwayjs/axios';\n\ndeclare module '@midwayjs/axios/dist/interface' {\n  interface AxiosRequestConfig {\n    retryDelay?: number;\n    retry?: number;\n  }\n}\n```\n\n\n### 直接使用 Axios\n\n`@midayjs/axios`导出了原始的`axios`实例，在非应用环境中可以直接使用。\n\n```typescript\nimport { Axios } from '@midwayjs/axios';\nimport { ReadStream, createWriteStream } from 'fs';\nimport { finished } from 'stream/promises';\n\nasync function download(url: string, filename: string) {\n  const writer = await createWriteStream(filename);\n  const res = Axios.get<ReadStream>(url, {\n    responseType: 'stream',\n  });\n  res.data.pipe(writer);\n  await finished(writer);\n  return res;\n}\n```\n","site/docs/extensions/bull.md":"# 任务队列\n\n队列是一种强大的设计模式，可帮助您应对常见的应用程序扩展和性能挑战。队列可以帮助您解决的一些问题。\n\n示例如下：\n\n-  平滑处理峰值。可以在任意时间启动资源密集型任务，然后将这些任务添加到队列中，而不是同步执行。让任务进程以受控方式从队列中提取任务。也可以轻松添加新的队列消费者以扩展后端任务处理。\n- 分解可能会阻塞 Node.js 事件循环的单一任务。比如用户请求需要像音频转码这样的 CPU 密集型工作，就可以将此任务委托给其他进程，从而释放面向用户的进程以保持响应。\n- 提供跨各种服务的可靠通信渠道。例如，您可以在一个进程或服务中排队任务（作业），并在另一个进程或服务中使用它们。在任何流程或服务的作业生命周期中完成、错误或其他状态更改时，您都可以收到通知（通过监听状态事件）。当队列生产者或消费者失败时，它们的状态被保留，并且当节点重新启动时任务处理可以自动重新启动。\n\nMidway 提供了 @midwayjs/bull 包作为 [Bull](https://github.com/OptimalBits/bull) 之上的抽象/包装器，[Bull](https://github.com/OptimalBits/bull) 是一种流行的、受良好支持的、高性能的基于 Node.js 的队列系统实现。该软件包可以轻松地将 Bull Queues 以友好的方式集成到您的应用程序中。\n\nBull 使用 Redis 来保存作业数据，在使用 Redis 时，Queue 架构是完全分布式，和平台无关。例如，您可以在一个（或多个）节点（进程）中运行一些 Queue 生产者、消费者，而在其他节点上的运行其他生产者和消费者。\n\n本章介绍 @midwayjs/bull 包。我们还建议阅读 [Bull 文档](https://github.com/OptimalBits/bull/blob/master/REFERENCE.md) 以了解更多背景和具体实施细节。\n\n:::tip\n\n- 1、从 v3.6.0 开始，原有任务调度 `@midwayjs/task` 模块废弃，如果查询历史文档，请参考 [这里](../legacy/task)。\n- 2、bull 是一个分布式任务管理系统，必须依赖 redis\n\n:::\n\n\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n\n\n## 安装组件\n\n```bash\n$ npm i @midwayjs/bull@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/bull\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 使用组件\n\n将 bull 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as bull from '@midwayjs/bull';\n\n@Configuration({\n  imports: [\n    // ...\n    bull\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n\n\n## 一些概念\n\nBull 将整个队列分为三个部分\n\n- 1、Queue 队列，管理任务\n- 2、Job，每个任务对象，可以对任务进行启停控制\n- 3、Processor，任务处理，实际的逻辑执行部分\n\n\n\n## 基础配置\n\nbull 是一个分布式任务管理器，强依赖于 redis，在 `config.default.ts` 文件中配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  bull: {\n    // 默认的队列配置\n    defaultQueueOptions: {\n      redis: `redis://127.0.0.1:32768`,\n    }\n  },\n}\n```\n\n有账号密码情况：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  bull: {\n    defaultQueueOptions: {\n      redis: {\n        port: 6379,\n        host: '127.0.0.1',\n        password: 'foobared',\n      },\n    }\n  },\n}\n```\n\n所有的队列都会复用该配置。\n\n\n\n## 编写任务处理器\n\n使用 `@Processor` 装饰器装饰一个类，用于快速定义一个任务处理器（这里我们不使用 Job，避免后续的歧义）。\n\n`@Processor` 装饰器需要传递一个 Queue （队列）的名字，在框架启动时，如果没有名为 `test` 的队列，则会自动创建。\n\n比如，我们在 `src/queue/test.queue.ts` 文件中编写如下代码。\n\n```typescript\n// src/queue/test.queue.ts\nimport { Processor, IProcessor } from '@midwayjs/bull';\n\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  async execute() {\n    // ...\n  }\n}\n```\n\n在启动时，框架会自动查找并初始化上述处理器代码，同时自动创建一个名为 `test` 的 Queue。\n\n\n\n\n\n## 执行任务\n\n当定义完 Processor 之后，由于并未指定 Processor 如何执行，我们还需要手动执行它。\n\n通过获取对应的队列，我们可以很方便的来执行任务。\n\n\n\n### 手动执行任务\n\n比如，我们可以在项目启动后执行。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bull from '@midwayjs/bull';\n\n@Configuration({\n  imports: [\n    // ...\n    bull\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullFramework: bull.Framework;\n\n  //...\n\n  async onServerReady() {\n    // 获取 Processor 相关的队列\n    const testQueue = this.bullFramework.getQueue('test');\n    // 立即添加这个任务\n    await testQueue?.addJobToQueue();\n  }\n}\n```\n\n\n\n### 增加执行参数\n\n我们也可以在执行时，附加一些默认参数。\n\n```typescript\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  async execute(params) {\n    // params.aaa => 1\n  }\n}\n\n\n// invoke\nconst testQueue = this.bullFramework.getQueue('test');\n// 立即添加这个任务\nawait testQueue?.addJobToQueue({\n  aaa: 1,\n  bbb: 2,\n});\n```\n\n\n\n### 任务状态和管理\n\n执行 `addJobToQueue` 后，我们可以获取到一个 `Job` 对象。\n\n```typescript\n// invoke\nconst testQueue = this.bullFramework.getQueue('test');\nconst job = await testQueue?.addJobToQueue();\n```\n\n通过这个 Job 对象，我们可以做进度管理。\n\n```typescript\n// 更新进度\nawait job.progress(60);\n// 获取进度\nconst progress = await job.process();\n// => 60\n```\n\n获取任务状态。\n\n```typescript\nconst state = await job.getState();\n// state => 'delayed' 延迟状态\n// state => 'completed' 完成状态\n```\n\n更多的 Job API，请查看 [文档](https://github.com/OptimalBits/bull/blob/develop/REFERENCE.md)。\n\n\n\n### 延迟执行\n\n执行任务时，也有一些额外的选项。\n\n比如，延迟 1s 执行。\n\n```typescript\nconst testQueue = this.bullFramework.getQueue('test');\n// 立即添加这个任务\nawait testQueue?.addJobToQueue({}, { delay: 1000 });\n```\n\n\n\n### 中间件和错误处理\n\nBull 组件包含可以独立启动的 Framework，有着自己的 App 对象和 Context 结构。\n\n我们可以对 bull 的 App 配置独立的中间件和错误过滤器。\n\n```typescript\n@Configuration({\n  imports: [\n    // ...\n    bull\n  ]\n})\nexport class MainConfiguration {\n\n  @App('bull')\n  bullApp: bull.Application;\n\n  //...\n\n  async onReady() {\n    this.bullApp.useMiddleare( /*中间件*/);\n    this.bullApp.useFilter( /*过滤器*/);\n  }\n}\n```\n\n\n\n### 上下文\n\n任务处理器执行是在请求作用域中，其有着特殊的 Context 对象结构。\n\n```typescript\nexport interface Context extends IMidwayContext {\n  jobId: JobId;\n  job: Job,\n  from: new (...args) => IProcessor;\n}\n```\n\n我们可以直接从 ctx 中访问当前的 Job 对象。\n\n```typescript\n// src/queue/test.queue.ts\nimport { Processor, IProcessor, Context } from '@midwayjs/bull';\n\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n\n  @Inject()\n  ctx: Context;\n\n  async execute() {\n    // ctx.jobId => xxxx\n  }\n}\n```\n\n\n\n### 更多任务选项\n\n除了上面的 delay 之外，还有更多的执行选项。\n\n| 选项             | 类型                  | 描述                                                         |\n| ---------------- | --------------------- | ------------------------------------------------------------ |\n| priority         | number                | 可选的优先级值。范围从 1（最高优先级）到 MAX_INT（最低优先级）。请注意，使用优先级对性能有轻微影响，因此请谨慎使用。 |\n| delay            | number                | 等待可以处理此作业的时间量（毫秒）。请注意，为了获得准确的延迟，服务器和客户端都应该同步它们的时钟。 |\n| attempts         | number                | 在任务完成之前尝试尝试的总次数。                             |\n| repeat           | RepeatOpts            | 根据 cron 规范的重复任务配置，更多可以查看 [RepeatOpts](https://github.com/OptimalBits/bull/blob/master/REFERENCE.md#queueadd)，以及下面的重复任务介绍。 |\n| backoff          | number \\| BackoffOpts | 任务失败时自动重试的回退设置。请参阅 [BackoffOpts](https://github.com/OptimalBits/bull/blob/master/REFERENCE.md#queueadd)。 |\n| lifo             | boolean               | 如果为 true，则将任务添加到队列的右端而不是左端（默认为 false）。 |\n| timeout          | number                | 任务因超时错误而失败的毫秒数。                               |\n| jobId            | number \\| string      | 覆盖任务 id - 默认情况下，任务 id 是唯一整数，但您可以使用此设置覆盖它。如果您使用此选项，则由您来确保 jobId 是唯一的。如果您尝试添加一个 id 已经存在的任务，它将不会被添加。 |\n| removeOnComplete | boolean \\| number     | 如果为 true，则在成功完成后删除任务。如果设置数字，则为指定要保留的任务数量。默认行为是任务信息保留在已完成列表中。 |\n| removeOnFail     | boolean \\| number     | 如果为 true，则在所有尝试后都失败时删除任务。如果设置数字，指定要保留的任务数量。默认行为是将任务信息保留在失败列表中。 |\n| stackTraceLimit  | number                | 限制将在堆栈跟踪中记录的堆栈跟踪行的数量。                   |\n\n\n\n## 重复执行的任务\n\n除了手动执行的方式，我们也可以通过 `@Processor` 装饰器的参数，快速配置任务的重复执行。\n\n```typescript\nimport { Processor, IProcessor } from '@midwayjs/bull';\nimport { FORMAT } from '@midwayjs/core';\n\n@Processor('test', {\n  repeat: {\n    cron: FORMAT.CRONTAB.EVERY_PER_5_SECOND\n  }\n})\nexport class TestProcessor implements IProcessor {\n  @Inject()\n  logger;\n\n  async execute() {\n    // ...\n  }\n}\n```\n\n\n\n## 常用 Cron 表达式\n\n关于 Cron 表达式，格式如下。\n\n```typescript\n*    *    *    *    *    *\n┬    ┬    ┬    ┬    ┬    ┬\n│    │    │    │    │    |\n│    │    │    │    │    └ day of week (0 - 7) (0 or 7 is Sun)\n│    │    │    │    └───── month (1 - 12)\n│    │    │    └────────── day of month (1 - 31)\n│    │    └─────────────── hour (0 - 23)\n│    └──────────────────── minute (0 - 59)\n└───────────────────────── second (0 - 59, optional)\n```\n\n\n\n常见表达式：\n\n- 每隔5秒执行一次：`*/5 * * * * *`\n- 每隔1分钟执行一次：`0 */1 * * * *`\n- 每小时的20分执行一次：`0 20 * * * *`\n- 每天 0 点执行一次：`0 0 0 * * *`\n- 每天的两点35分执行一次：`0 35 2 * * *`\n\n可以使用 [在线工具](https://cron.qqe2.com/) 执行确认下一次执行的时间。\n\nMidway 在框架侧提供了一些常用的表达式，放在 `@midwayjs/core` 中供大家使用。\n\n```typescript\nimport { FORMAT } from '@midwayjs/core';\n\n// 每分钟执行的 cron 表达式\nFORMAT.CRONTAB.EVERY_MINUTE\n```\n\n\n\n内置的还有一些其他的表达式。\n\n| 表达式                         | 对应时间        |\n| ------------------------------ | --------------- |\n| CRONTAB.EVERY_SECOND           | 每秒钟          |\n| CRONTAB.EVERY_MINUTE           | 每分钟          |\n| CRONTAB.EVERY_HOUR             | 每小时整点      |\n| CRONTAB.EVERY_DAY              | 每天 0 点       |\n| CRONTAB.EVERY_DAY_ZERO_FIFTEEN | 每天 0 点 15 分 |\n| CRONTAB.EVERY_DAY_ONE_FIFTEEN  | 每天 1 点 15 分 |\n| CRONTAB.EVERY_PER_5_SECOND     | 每隔 5 秒       |\n| CRONTAB.EVERY_PER_10_SECOND    | 每隔 10 秒      |\n| CRONTAB.EVERY_PER_30_SECOND    | 每隔 30 秒      |\n| CRONTAB.EVERY_PER_5_MINUTE     | 每隔 5 分钟     |\n| CRONTAB.EVERY_PER_10_MINUTE    | 每隔 10 分钟    |\n| CRONTAB.EVERY_PER_30_MINUTE    | 每隔 30 分钟    |\n\n\n\n## 高级配置\n\n\n\n### 清理之前的任务\n\n在默认情况下，框架会自动清理前一次未调度的 **重复执行任务**，保持每一次的重复执行的任务队列为最新。如果在某些环境不需要清理，可以单独关闭。\n\n比如你不需要清理重复：\n\n```typescript\n// src/config/config.prod.ts\nexport default {\n  // ...\n  bull: {\n    clearRepeatJobWhenStart: false,\n  },\n}\n```\n\n:::tip\n\n如果不清理，如果前一次队列为 10s 执行，现在修改为 20s 执行，则两个定时都会存储在 Redis 中，导致代码重复执行。\n\n在日常的开发中，如果不清理，很容易出现代码重复执行这个问题。但是在集群部署的场景，多台服务器轮流重启的情况下，可能会导致定时任务被意外清理，请评估开关的时机。\n\n:::\n\n\n\n也可以在启动时手动清理所有任务。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, App, Inject } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport { join } from 'path';\nimport * as bull from '@midwayjs/bull';\n\n@Configuration({\n  imports: [koa, bull],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: koa.Application;\n\n  @Inject()\n  bullFramework: bull.Framework;\n\n  async onReady() {\n    // 在这个阶段，装饰器队列还未创建，使用 API 提前手动创建队列，装饰器会复用同名队列\n    const queue = this.bullFramework.createQueue('user');\n    // 通过队列手动执行清理\n    await queue.obliterate({ force: true });\n  }\n}\n```\n\n\n\n\n\n### 清理任务历史记录\n\n当开启 Redis 后，默认情况下，bull 会记录所有的成功和失败的任务 key，这可能会导致 redis 的 key 暴涨，我们可以配置成功或者失败后清理的选项。\n\n默认情况下\n\n- 成功时保留的任务记录为 3 条\n- 失败保留的任务记录为 10 条\n\n也可以通过参数进行配置。\n\n比如在装饰器配置。\n\n```typescript\nimport { FORMAT } from '@midwayjs/core';\nimport { IProcessor, Processor } from '@midwayjs/bull';\n\n@Processor('user', {\n  repeat: {\n    cron: FORMAT.CRONTAB.EVERY_MINUTE,\n  },\n  removeOnComplete: 3,\t// 成功后移除任务记录，最多保留最近 3 条记录\n  removeOnFail: 10,\t    // 失败后移除任务记录\n})\nexport class UserService implements IProcessor {\n  execute(data: any) {\n    // ...\n  }\n}\n\n```\n\n也可以在全局 config 中配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  bull: {\n    defaultQueueOptions: {\n      // 默认的任务配置\n      defaultJobOptions: {\n        // 保留 10 条记录\n        removeOnComplete: 10,\n      },\n    },\n  },\n}\n```\n\n\n\n\n\n### Redis 集群\n\n可以使用 bull 提供的 `createClient` 方式来接入自定义的 redis 实例，这样你可以接入 Redis 集群。\n\n比如：\n\n```typescript\n// src/config/config.default\nimport Redis from 'ioredis';\n\nconst clusterOptions = {\n  enableReadyCheck: false,  // 一定要是false\n  retryDelayOnClusterDown: 300,\n  retryDelayOnFailover: 1000,\n  retryDelayOnTryAgain: 3000,\n  slotsRefreshTimeout: 10000,\n  maxRetriesPerRequest: null  // 一定要是null\n}\n\nconst redisClientInstance = new Redis.Cluster([\n  {\n    port: 7000,\n    host: '127.0.0.1'\n  },\n  {\n    port: 7002,\n    host: '127.0.0.1'\n  },\n], clusterOptions);\n\nexport default {\n  bull: {\n    defaultQueueOptions: {\n      createClient: (type, opts) => {\n        return redisClientInstance;\n      },\n      // 这些任务存储的 key，都是相同开头，以便区分用户原有 redis 里面的配置\n      prefix: '{midway-bull}',\n    },\n  }\n}\n```\n\n\n\n## 队列管理\n\n队列是廉价的，每个 Job 都会绑定一个队列，在一些情况下，我们也可以手动对队列进行管理操作。\n\n\n\n### 手动创建队列\n\n除了使用 `@Processor` 简单定义队列，我们还可以使用 API 进行创建。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bull from '@midwayjs/bull';\n\n@Configuration({\n  imports: [\n    // ...\n    bull\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullFramework: bull.Framework;\n\n  async onReady() {\n    const testQueue = this.bullFramework.createQueue('test', {\n      redis: {\n        port: 6379,\n        host: '127.0.0.1',\n        password: 'foobared',\n      },\n      prefix: '{midway-bull}',\n    });\n\n    // ...\n  }\n}\n```\n\n通过 `createQueue` 手动创建队列后，队列依旧会自动保存。如果在启动时 `@Processor` 使用了该队列名，则会自动使用已经创建好的队列。\n\n比如：\n\n```typescript\n// 会自动使用上面手动创建的同名队列\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  async execute(params) {\n  }\n}\n```\n\n\n\n### 获取队列\n\n我们可以简单的根据队列名获取队列。\n\n```typescript\n const testQueue = bullFramework.getQueue('test');\n```\n\n也可以通过装饰器来获取。\n\n```typescript\nimport { InjectQueue, BullQueue } from '@midwayjs/bull';\nimport { Provide } from '@midwayjs/core';\n\n@Provide()\nexport class UserService {\n  @InjectQueue('test')\n  testQueue: BullQueue;\n\n  async invoke() {\n    await this.testQueue.pause();\n    // ...\n  }\n}\n```\n\n\n\n### 队列常用操作\n\n暂停队列。\n\n```typescript\nawait testQueue.pause();\n```\n\n继续队列。\n\n```typescript\nawait testQueue.resume();\n```\n\n队列事件。\n\n```typescript\n// Local events pass the job instance...\ntestQueue.on('progress', function (job, progress) {\n  console.log(`Job ${job.id} is ${progress * 100}% ready!`);\n});\n\ntestQueue.on('completed', function (job, result) {\n  console.log(`Job ${job.id} completed! Result: ${result}`);\n  job.remove();\n});\n```\n\n完整队列 API 请参考 [这里](https://github.com/OptimalBits/bull/blob/develop/REFERENCE.md)。\n\n\n\n## 组件日志\n\n组件有着自己的日志，默认会将 `ctx.logger` 记录在 `midway-bull.log` 中。\n\n我们可以单独配置这个 logger 对象。\n\n```typescript\nexport default {\n  midwayLogger: {\n    clients: {\n      // ...\n      bullLogger: {\n        fileLogName: 'midway-bull.log',\n      },\n    },\n  },\n}\n```\n\n这个日志的输出格式，我们也可以单独配置。\n\n```typescript\nexport default {\n  bull: {\n    // ...\n    contextLoggerFormat: info => {\n      const { jobId, from } = info.ctx;\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} [${jobId} ${from.name}] ${info.message}`;\n    },\n  }\n}\n```\n\n\n\n## 关于 Redis 版本\n\n请尽可能选择最新的版本（ >=5 ），目前在低版本 redis 上有发现定时任务创建失败的问题。\n\n\n\n## Bull UI\n\n在分布式场景中，我们可以资利用 Bull UI 来简化管理。\n\n和 bull 组件类似，需要独立安装和启用。\n\n```bash\n$ npm i @midwayjs/bull-board@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/bull-board\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n将 bull-board 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as bull from '@midwayjs/bull';\nimport * as bullBoard from '@midwayjs/bull-board';\n\n@Configuration({\n  imports: [\n    // ...\n    bull,\n    bullBoard,\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n默认的访问路径为：`http://127.1:7001/ui`。\n\n效果如下：\n\n![](https://img.alicdn.com/imgextra/i2/O1CN01j4wEFb1UacPxA06gs_!!6000000002534-2-tps-1932-1136.png)\n\n可以通过配置进行基础路径的修改。\n\n```typescript\n// src/config/config.prod.ts\nexport default {\n  // ...\n  bullBoard: {\n    basePath: '/ui',\n  },\n}\n```\n\n此外，组件提供了 `BullBoardManager` ，可以添加动态创建的队列。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bull from '@midwayjs/bull';\nimport * as bullBoard from '@midwayjs/bull-board';\n\n@Configuration({\n  imports: [\n    // ...\n    bull,\n    bullBoard\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullFramework: bull.Framework;\n  \n  @Inject()\n  bullBoardManager: bullBoard.BullBoardManager;\n\n  async onServerReady() {\n    const testQueue = this.bullFramework.createQueue('test', {\n      // ...\n    });\n\n    this.bullBoardManager.addQueue(new bullBoard.BullAdapter(testQueue);\n  }\n}\n```\n\n\n\n\n\n## 常见问题\n\n### 1、EVALSHA 错误\n\n![image.png](https://img.alicdn.com/imgextra/i4/O1CN01KfjCKT1yypmNPDkIL_!!6000000006648-2-tps-3540-102.png)\n\n这个问题基本明确，问题会出现在 redis 的集群版本上。\n\n原因是 redis 会对 key 做 hash 来确定存储的 slot，集群下这一步 @midwayjs/bull 的 key 命中了不同的 slot。\n\n解决方案： task 里的 prefix 配置用 {} 包括，强制 redis 只计算 {} 里的hash，例如 `prefix: '{midway-task}'`。\n\n### 2、EVAL inside MULTI is not allowed 错误\n\n表现为 `queue.createBulk()`、`job.moveToFailed()` 等任务队列 API 调用无效，并出现下面的错误。\n\n```\nReplyError: EXECABORT Transaction discarded because of previous errors.\n    at parseError (<project_dir>/node_modules/redis-parser/lib/parser.js:179:12)\n    at parseType (<project_dir>/node_modules/redis-parser/lib/parser.js:302:14) {\n  command: { name: 'exec', args: [] },\n  previousErrors: [\n    ReplyError: ERR 'EVAL' inside MULTI is not allowed\n        at parseError (<project_dir>/node_modules/redis-parser/lib/parser.js:179:12)\n        at parseType (<project_dir>/node_modules/redis-parser/lib/parser.js:302:14) {\n      command: [Object]\n    }\n  ]\n}\n```\n\n:::tip\n\n常出现于使用阿里云 Redis 服务。\n\n:::\n\n由于这些 API 依赖的 Redis Lua 脚本中使用了 EVAL 或者 EVALSHA，阿里云 Redis 使用代理模式连接时，会对 Lua 脚本调用做额外限制，包括 [不允许在 MULTI 事务中执行 EVAL 命令](https://help.aliyun.com/zh/redis/support/usage-of-lua-scripts?#section-8f7-qgv-dlv)，文档中还提到可以通过参数配置 script_check_enable 关闭这一校验，但是验证无效。\n\n解决方案：\n\n* 1、在阿里云控制台操作开启直连地址，将服务切换到直连模式\n* 2、客户端切换成集群模式，参考上述「Redis 集群」章节，切换配置方式\n","site/docs/extensions/bullmq.md":"# 任务队列\n\n队列是一种强大的设计模式，可帮助您应对常见的应用程序扩展和性能挑战。队列可以帮助您解决的一些问题：\n\n- 平滑处理峰值。可以在任意时间启动资源密集型任务，然后将这些任务添加到队列中，而不是同步执行。让任务进程以受控方式从队列中提取任务。也可以轻松添加新的队列消费者以扩展后端任务处理。\n- 分解可能会阻塞 Node.js 事件循环的单一任务。比如用户请求需要像音频转码这样的 CPU 密集型工作，就可以将此任务委托给其他进程，从而释放面向用户的进程以保持响应。\n- 提供跨各种服务的可靠通信渠道。例如，您可以在一个进程或服务中排队任务（作业），并在另一个进程或服务中使用它们。在任何流程或服务的作业生命周期中完成、错误或其他状态更改时，您都可以收到通知（通过监听状态事件）。当队列生产者或消费者失败时，它们的状态被保留，并且当节点重新启动时任务处理可以自动重新启动。\n\nMidway 提供了 `@midwayjs/bullmq` 包作为 [BullMQ](https://github.com/taskforcesh/bullmq) 之上的抽象/包装器。BullMQ 是 Bull 的下一代实现，提供了更好的性能和更多的功能。该软件包可以轻松地将 BullMQ 以友好的方式集成到您的应用程序中。\n\nBullMQ 使用 Redis 来保存作业数据，在使用 Redis 时，Queue 架构是完全分布式，和平台无关。例如，您可以在一个（或多个）节点（进程）中运行一些 Queue 生产者、消费者，而在其他节点上的运行其他生产者和消费者。\n\n:::tip\nBullMQ 是一个分布式任务管理系统，必须依赖 redis\n:::\n\n:::caution\n由于 BullMQ 是 Bull 的升级版，从 v3.20 开始，将由 BullMQ 替代 Bull 组件，如需使用 Bull 组件，请参考 [Bull 文档](./bull)\n:::\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n\n## 安装组件\n\n```bash\n$ npm i @midwayjs/bullmq@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/bullmq\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n## 使用组件\n\n将 bullmq 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as bullmq from '@midwayjs/bullmq';\n\n@Configuration({\n  imports: [\n    // ...\n    bullmq\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n## 一些概念\n\nBullMQ 将整个队列分为以下几个部分：\n\n- Queue：队列，管理任务\n- Job：每个任务对象，可以对任务进行启停控制\n- Worker：任务处理器，实际的逻辑执行部分\n- QueueEvents：队列事件，用于监听任务状态变化\n- FlowProducer：任务流生产者，用于创建任务依赖关系\n\n## 基础配置\n\nbullmq 是一个分布式任务管理器，强依赖于 redis，在 `config.default.ts` 文件中配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  bullmq: {\n    defaultConnection: {\n      host: '127.0.0.1',\n      port: 6379,\n    },\n    // 可选，队列前缀\n    defaultPrefix: '{midway-bullmq}',\n  },\n}\n```\n\n有账号密码情况：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  bullmq: {\n    defaultConnection: {\n      port: 6379,\n      host: '127.0.0.1',\n      password: 'foobared',\n    }\n  },\n}\n```\n\n所有的队列、任务处理器、队列事件、任务流都会复用该配置。\n\n## 编写任务处理器\n\n使用 `@Processor` 装饰器装饰一个类，用于快速定义一个任务处理器。\n\n`@Processor` 装饰器需要传递一个 Queue（队列）的名字，在框架启动时，如果没有名为 `test` 的队列，则会自动创建。\n\n比如，我们在 `src/processor/test.processor.ts` 文件中编写如下代码。\n\n```typescript\nimport { Processor, IProcessor } from '@midwayjs/bullmq';\n\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  async execute(data: any) {\n    // 处理任务逻辑\n    console.log('processing job:', data);\n  }\n}\n```\n\n## 执行任务\n\n当定义完 Processor 之后，由于并未指定 Processor 如何执行，我们还需要手动执行它。\n\n### 手动执行任务\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bullmq from '@midwayjs/bullmq';\n\n@Configuration({\n  imports: [\n    bullmq\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullmqFramework: bullmq.Framework;\n\n  async onServerReady() {\n    // 获取 Processor 相关的队列\n    const testQueue = this.bullmqFramework.getQueue('test');\n    // 立即添加这个任务\n    await testQueue?.addJobToQueue();\n  }\n}\n```\n\n### 增加执行参数\n\n我们也可以在执行时，附加一些参数。\n\n```typescript\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  async execute(params) {\n    // params.name => 'harry'\n  }\n}\n\n// invoke\nconst testQueue = this.bullmqFramework.getQueue('test');\nawait testQueue?.addJobToQueue({\n  aaa: 1,\n  bbb: 2,\n});\n```\n\n### 任务状态和管理\n\n执行 `addJobToQueue` 后，我们可以获取到一个 `Job` 对象。\n\n```typescript\nconst testQueue = this.bullmqFramework.getQueue('test');\nconst job = await testQueue?.addJobToQueue();\n\n// 更新进度\nawait job.updateProgress(60);\n// 获取进度\nconst progress = await job.progress;\n// => 60\n\n// 获取任务状态\nconst state = await job.getState();\n// state => 'delayed' 延迟状态\n// state => 'completed' 完成状态\n// state => 'failed' 失败状态\n```\n\n### 延迟执行\n\n执行任务时，也有一些额外的选项。\n\n比如，延迟 1s 执行。\n\n```typescript\nconst testQueue = this.bullmqFramework.getQueue('test');\nawait testQueue?.addJobToQueue({}, { delay: 1000 });\n```\n\n### 任务重试\n\nBullMQ 支持任务失败重试机制。\n\n```typescript\nconst testQueue = this.bullmqFramework.getQueue('test');\nawait testQueue?.addJobToQueue({}, {\n  attempts: 3,  // 最多重试 3 次\n  backoff: {    // 重试策略\n    type: 'exponential',  // 指数退避\n    delay: 1000          // 初始延迟 1 秒\n  }\n});\n```\n\n### 任务优先级\n\n可以为任务设置优先级，优先级高的任务会优先执行。\n\n```typescript\nconst testQueue = this.bullmqFramework.getQueue('test');\n// priority 值越大优先级越高\nawait testQueue?.addJobToQueue({ priority: 1 }, { priority: 3 }); // 高优先级\nawait testQueue?.addJobToQueue({ priority: 2 }, { priority: 2 }); // 中优先级\nawait testQueue?.addJobToQueue({ priority: 3 }, { priority: 1 }); // 低优先级\n```\n\n### 中间件和错误处理\n\nBullMQ 组件包含可以独立启动的 Framework，有着自己的 App 对象和 Context 结构。\n\n我们可以对 bullmq 的 App 配置独立的中间件和错误过滤器。\n\n```typescript\n@Configuration({\n  imports: [\n    bullmq\n  ]\n})\nexport class MainConfiguration {\n\n  @App('bullmq')\n  bullmqApp: bullmq.Application;\n\n  async onReady() {\n    this.bullmqApp.useMiddleware(/*中间件*/);\n    this.bullmqApp.useFilter(/*过滤器*/);\n  }\n}\n```\n\n### 上下文\n\n任务处理器执行是在请求作用域中，其有着特殊的 Context 对象结构。\n\n```typescript\nexport interface Context extends IMidwayContext {\n  jobId: string;\n  job: Job;\n  token?: string;\n  from: new (...args) => IProcessor;\n}\n```\n\n我们可以直接从 ctx 中访问当前的 Job 对象。\n\n```typescript\nimport { Processor, IProcessor, Context } from '@midwayjs/bullmq';\n\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  @Inject()\n  ctx: Context;\n\n  async execute(data: any) {\n    // ctx.jobId => 当前任务ID\n    // ctx.job => 当前任务对象\n  }\n}\n```\n\n## 重复执行的任务\n\n除了手动执行的方式，我们也可以通过 `@Processor` 装饰器的参数，快速配置任务的重复执行。\n\n```typescript\nimport { Processor, IProcessor } from '@midwayjs/bullmq';\nimport { FORMAT } from '@midwayjs/core';\n\n@Processor('test', {\n  repeat: {\n    pattern: FORMAT.CRONTAB.EVERY_PER_5_SECOND\n  }\n})\nexport class TestProcessor implements IProcessor {\n  async execute() {\n    // 每 5 秒执行一次\n  }\n}\n```\n\n## 高级功能\n\n### 任务流（Flow Producer）\n\nBullMQ 支持创建任务依赖关系，形成任务流。\n\n```typescript\nconst flowProducer = bullmqFramework.createFlowProducer({}, 'test-flow');\n\n// 创建任务流\nawait flowProducer.add({\n  name: 'flow-test',\n  queueName: 'flow-queue-1',\n  data: { value: 1 },\n  children: [\n    {\n      name: 'child-job',\n      queueName: 'flow-queue-2',\n      data: { value: 2 }\n    }\n  ]\n});\n```\n\n### 队列事件\n\nBullMQ 提供了丰富的事件系统，可以监听任务的各种状态变化。\n\n```typescript\nconst eventQueue = bullmqFramework.createQueue('event-queue');\nconst queueEvents = eventQueue.createQueueEvents();\n\n// 监听任务完成事件\nqueueEvents.on('completed', ({ jobId }) => {\n  console.log(`Job ${jobId} completed!`);\n});\n\n// 监听任务失败事件\nqueueEvents.on('failed', ({ jobId, failedReason }) => {\n  console.log(`Job ${jobId} failed: ${failedReason}`);\n});\n```\n\n### 清理任务历史记录\n\n当开启 Redis 后，默认情况下，bullmq 会记录所有的成功和失败的任务 key，这可能会导致 redis 的 key 暴涨，我们可以配置成功或者失败后清理的选项。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  bullmq: {\n    defaultQueueOptions: {\n      defaultJobOptions: {\n        removeOnComplete: 3,  // 成功后只保留最近 3 条记录\n        removeOnFail: 10,     // 失败后只保留最近 10 条记录\n      }\n    }\n  }\n}\n```\n\n### Redis 集群\n\nbullmq 可以指定 connection 实例，你可以将自己创建的 Redis 实例配置到 `defaultConnection` 中，这样就可以接入 Redis 集群。\n\n```typescript\n// src/config/config.default.ts\nimport Redis from 'ioredis';\n\nconst clusterOptions = {\n  enableReadyCheck: false,\n  retryDelayOnClusterDown: 300,\n  retryDelayOnFailover: 1000,\n  retryDelayOnTryAgain: 3000,\n  slotsRefreshTimeout: 10000,\n  maxRetriesPerRequest: null\n}\n\nconst redisClientInstance = new Redis.Cluster([\n  {\n    port: 7000,\n    host: '127.0.0.1'\n  },\n  {\n    port: 7002,\n    host: '127.0.0.1'\n  },\n], clusterOptions);\n\nexport default {\n  bullmq: {\n    defaultConnection: redisClientInstance,\n    defaultPrefix: '{midway-bullmq}',\n  }\n}\n```\n\n### 获取任务处理器\n\n通过内置的方法，我们可以获取到原始的任务处理器（Worker）。\n\n可以启动任意多个 Worker 监听同一个队列，在这种情况下 BullMQ 会把队列中的任务轮流（round-robin）分配给这些 Worker。\n\n当队列中有任务被添加时，BullMQ 会在这两个 Worker 之间自动分配任务。\n\n我们可以通过方法 `getWorker` 获取到首个监听的 Worker 对象，也可以通过方法 `getWorkers` 获取到该队列监听的所有 Worker 对象。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bullmq from '@midwayjs/bullmq';\n\n@Configuration({\n  imports: [\n    // ...\n    bullmq,\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullmqFramework: bullmq.Framework;\n\n  async onServerReady() {\n    const testWorker = this.bullmqFramework.getWorker('test');\n    const allWorkers = this.bullmqFramework.getWorkers('test');\n  }\n}\n\n```\n\n\n## 组件日志\n\n组件有着自己的日志，默认会将 `ctx.logger` 记录在 `midway-bullmq.log` 中。\n\n我们可以单独配置这个 logger 对象。\n\n```typescript\nexport default {\n  midwayLogger: {\n    clients: {\n      bullMQLogger: {\n        fileLogName: 'midway-bullmq.log',\n      },\n    },\n  },\n}\n```\n\n这个日志的输出格式，我们也可以单独配置。\n\n```typescript\nexport default {\n  bullmq: {\n    contextLoggerFormat: info => {\n      const { jobId, from } = info.ctx;\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} [${jobId} ${from.name}] ${info.message}`;\n    },\n  }\n}\n```\n\n## BullMQ 原始对象\n\n组件导出了 BullMQ 的原始对象，可以进行更多的操作。\n\n```typescript\nimport { BullMQ } from '@midwayjs/bullmq';\n```\n\n你可以通过 `BullMQ` 对象，获取到 `Queue`、`Worker`、`FlowProducer` 等对象定义。\n\n## Bull UI\n\n在分布式场景中，我们可以资利用 Bull UI 来简化管理。\n\n和 bull 组件类似，需要独立安装和启用。\n\n```bash\n$ npm i @midwayjs/bull-board@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/bull-board\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n将 bull-board 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as bullmq from '@midwayjs/bullmq';\nimport * as bullBoard from '@midwayjs/bull-board';\n\n@Configuration({\n  imports: [\n    // ...\n    bullmq,\n    bullBoard,\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n默认的访问路径为：`http://127.1:7001/ui`。\n\n效果如下：\n\n![](https://img.alicdn.com/imgextra/i2/O1CN01j4wEFb1UacPxA06gs_!!6000000002534-2-tps-1932-1136.png)\n\n可以通过配置进行基础路径的修改。\n\n```typescript\n// src/config/config.prod.ts\nexport default {\n  // ...\n  bullBoard: {\n    basePath: '/ui',\n  },\n}\n```\n\n此外，组件提供了 `BullBoardManager` ，可以添加动态创建的队列。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bullmq from '@midwayjs/bullmq';\nimport * as bullBoard from '@midwayjs/bull-board';\n\n@Configuration({\n  imports: [\n    // ...\n    bullmq,\n    bullBoard\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullmqFramework: bullmq.Framework;\n  \n  @Inject()\n  bullBoardManager: bullBoard.BullBoardManager;\n\n  async onServerReady() {\n    const testQueue = this.bullmqFramework.createQueue('test', {\n      // ...\n    });\n\n    this.bullBoardManager.addQueue(new bullBoard.BullMQAdapter(testQueue) as any);\n  }\n}\n```\n\n由于最新 bull-board 要求最低 Node.js 版本为 v20，所以 Midway v3 无法将 bull-board 升级；在 v5.23.0 版本下的 `bull-board` 存在类型定义问题，采用 `as any`  的方式绕过。\n","site/docs/extensions/busboy.md":"import Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n# 文件上传\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的通用上传组件，支持 `file` (服务器临时文件) 和 `stream` （流）两种模式。\n\n相关信息：\n\n| web 支持情况      |      |\n| ----------------- | ---- |\n| @midwayjs/koa     | ✅    |\n| @midwayjs/faas    | 💬    |\n| @midwayjs/web     | ✅    |\n| @midwayjs/express | ✅    |\n\n:::caution\n\n💬 部分函数计算平台不支持流式请求响应，请参考对应平台能力。\n\n:::\n\n:::tip\n\n本模块自 3.17.0 起替换 upload 组件。\n\n和 upload 组件的差异为：\n\n* 1、配置的 key 从 `upload` 调整为 `busboy`\n* 2、中间件不再默认加载，手动可配置到全局或者路由\n* 3、入参定义类型调整为 `UploadStreamFileInfo`\n* 4、`fileSize` 的配置有调整\n\n:::\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/busboy@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/busboy\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n## 启用组件\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration } from '@midwayjs/core';\nimport * as busboy from '@midwayjs/busboy';\n\n@Configuration({\n  imports: [\n    // ...other components\n    busboy\n  ],\n  // ...\n})\nexport class MainConfiguration {}\n```\n\n\n\n## 配置中间件\n\n组件中提供了 `UploadMiddleware` 这个中间件，可以将其配置到全局或者特定路由，推荐配置到特定路由，提升性能。\n\n\n\n**路由中间件**\n\n```typescript\nimport { Controller, Post } from '@midwayjs/core';\nimport { UploadMiddleware } from '@midwayjs/busboy';\n\n@Controller('/')\nexport class HomeController {\n\n  @Post('/upload', { middleware: [UploadMiddleware] })\n  async upload(/*...*/) {\n    // ...\n  }\n}\n```\n\n**全局中间件**\n\n<Tabs>\n<TabItem value=\"koa\" label=\"@midwayjs/koa\">\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration } from '@midwayjs/core';\nimport * as busboy from '@midwayjs/busboy';\nimport { Application } from '@midwayjs/koa';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  @App('koa')\n  app: Application;\n  \n  async onReady() {\n    this.app.useMiddleware(busboy.UploadMiddleware);\n  }\n}\n```\n</TabItem>\n<TabItem value=\"egg\" label=\"@midwayjs/web\">\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration } from '@midwayjs/core';\nimport * as busboy from '@midwayjs/busboy';\nimport { Application } from '@midwayjs/web';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  @App('egg')\n  app: Application;\n  \n  async onReady() {\n    this.app.useMiddleware(busboy.UploadMiddleware);\n  }\n}\n```\n</TabItem>\n<TabItem value=\"express\" label=\"@midwayjs/express\">\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration } from '@midwayjs/core';\nimport * as busboy from '@midwayjs/busboy';\nimport { Application } from '@midwayjs/express';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  @App('express')\n  app: Application;\n  \n  async onReady() {\n    this.app.useMiddleware(busboy.UploadMiddleware);\n  }\n}\n```\n</TabItem>\n<TabItem value=\"faas\" label=\"@midwayjs/faas\">\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration } from '@midwayjs/core';\nimport * as busboy from '@midwayjs/busboy';\nimport { Application } from '@midwayjs/faas';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  @App('faas')\n  app: Application;\n  \n  async onReady() {\n    this.app.useMiddleware(busboy.UploadMiddleware);\n  }\n}\n```\n</TabItem>\n</Tabs>\n\n\n\n## 配置\n\n组件使用 `busboy` 作为配置的 key。\n\n\n\n### 上传模式\n\n上传分为三种模式，文件模式，流式模式以及新增的异步迭代器模式。\n\n代码中使用 `@Files()` 装饰器获取上传的文件， `@Fields` 装饰器获取其他上传表单字段。\n\n<Tabs>\n<TabItem value=\"file\" label=\"文件模式\">\n\n`file` 为默认值，配置 mode 为 `file` 字符串。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  busboy: {\n    mode: 'file',\n  },\n}\n```\n\n在代码中获取上传的文件，支持同时上传多个文件。\n\n```typescript\nimport { Controller, Post, Files, Fields } from '@midwayjs/core';\nimport { UploadFileInfo } from '@midwayjs/busboy';\n\n@Controller('/')\nexport class HomeController {\n\n  @Post('/upload', /*...*/)\n  async upload(@Files() files: Array<UploadFileInfo>, @Fields() fields: Record<string, string>) {\n    /*\n    files = [\n      {\n        filename: 'test.pdf',        // 文件原名\n        data: '/var/tmp/xxx.pdf',    // 服务器临时文件地址\n        mimeType: 'application/pdf', // mime\n        fieldName: 'file'            // field name\n      },\n    ]\n    */\n  }\n}\n```\n\n使用 file 模式时， 获取的 `data` 为上传的文件在服务器的 `临时文件地址`，后续可以再通过 `fs.createReadStream` 等方式来处理此文件内容，支持同时上传多个文件，多个文件会以数组的形式存放。\n\n每个数组内的对象包含以下几个字段\n\n```typescript\nexport interface UploadFileInfo {\n  /**\n   * 上传的文件名\n   */\n  filename: string;\n  /**\n   * 上传文件 mime 类型\n   */\n  mimeType: string;\n  /**\n   * 上传服务端保存的路径\n   */\n  data: string;\n  /**\n   * 上传的表单字段名\n   */\n  fieldName: string;\n}\n```\n\n\n\n</TabItem>\n\n<TabItem value=\"asyncIterator\" label=\"异步迭代器模式\">\n\n从 `v3.18.0` 提供，替代原有的 `stream` 模式，该模式支持多个文件流式上传。\n\n配置 mode 为 `asyncIterator` 字符串。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  busboy: {\n    mode: 'asyncIterator',\n  },\n}\n```\n\n在代码中获取上传的文件。\n\n```typescript\nimport { Controller, Post, Files, Fields } from '@midwayjs/core';\nimport { UploadStreamFileInfo, UploadStreamFieldInfo } from '@midwayjs/busboy';\n\n@Controller('/')\nexport class HomeController {\n\n  @Post('/upload', /*...*/)\n  async upload(\n  \t@Files() fileIterator: AsyncGenerator<UploadStreamFileInfo>,\n    @Fields() fieldIterator: AsyncGenerator<UploadStreamFieldInfo>\n  ) {\n    // ...\n  }\n}\n```\n\n在该模式下，`@Files` 和 `@File` 装饰器会提供同一个 `AsyncGenerator` ，而 `@Fields` 会也同样会提供一个 `AsyncGenerator`。\n\n通过循环 `AsyncGenerator` ，可以针对每个上传文件的 `ReadStream` 做处理。\n\n```typescript\nimport { Controller, Post, Files, Fields } from '@midwayjs/core';\nimport { UploadStreamFileInfo, UploadStreamFieldInfo } from '@midwayjs/busboy';\nimport { tmpdir } from 'os';\nimport { createWriteStream } from 'fs';\n\n@Controller('/')\nexport class HomeController {\n\n  @Post('/upload', /*...*/)\n  async upload(\n  \t@Files() fileIterator: AsyncGenerator<UploadStreamFileInfo>,\n    @Fields() fieldIterator: AsyncGenerator<UploadStreamFieldInfo>\n  ) {\n    for await (const file of fileIterator) {\n      const { filename, data } = file;\n      const p = join(tmpdir, filename);\n      const stream = createWriteStream(p);\n      data.pipe(stream);\n    }\n\n    for await (const { name, value } of fieldIterator) {\n      // ...\n    }\n\n    // ...\n  }\n}\n```\n\n注意，如果一次上传中任意一个文件抛出了错误，本次上传流会直接关闭，所有未传输完成的文件都会异常。\n\n异步迭代器中的上传对象包含以下几个字段。\n\n```typescript\nexport interface UploadStreamFieldInfo {\n  /**\n   * 上传的文件名\n   */\n  filename: string;\n  /**\n   * 上传文件 mime 类型\n   */\n  mimeType: string;\n  /**\n   * 上传文件的文件流\n   */\n  data: Readable;\n  /**\n   * 上传的表单字段名\n   */\n  fieldName: string;\n}\n```\n\n异步迭代器中的 `@Fields` 的对象略有不同，返回的数据会包含 `name` 和 `value` 字段。\n\n```typescript\nexport interface UploadStreamFieldInfo {\n  /**\n   * 表单名\n   */\n  name: string;\n  /**\n   * 表单值\n   */\n  value: any;\n}\n```\n\n\n\n</TabItem>\n\n<TabItem value=\"stream\" label=\"流模式\">\n\n:::caution\n\n不再推荐使用。\n\n:::\n\n配置 mode 为 `stream` 字符串。\n\n\n使用 stream 模式时，通过 `@Files` 中获取的 `data` 为 `ReadStream`，后续可以再通过 `pipe` 等方式继续将数据流转至其他 `WriteStream` 或 `TransformStream`。\n\n\n使用 stream 模式时，仅同时上传一个文件，即 `@Files` 数组中只有一个文件数据对象。\n\n另外，stream 模式 `不会` 在服务器上产生临时文件，所以获取到上传的内容后无需手动清理临时文件缓存。\n\n:::tip\n\nfaas 场景实现方式视平台而定，如果平台不支持流式请求/响应但是业务开启了 `mode: 'stream'`，将采用先读取到内存，再模拟流式传输来降级处理。\n\n:::\n\n在代码中获取上传的文件，流式模式下仅支持单个文件。\n\n```typescript\nimport { Controller, Post, Files, Fields } from '@midwayjs/core';\nimport { UploadStreamFileInfo } from '@midwayjs/busboy';\n\n@Controller('/')\nexport class HomeController {\n\n  @Post('/upload', /*...*/)\n  async upload(@Files() files: Array<UploadStreamFileInfo>, @Fields() fields: Record<string, string) {\n    /*\n    files = [\n      {\n        filename: 'test.pdf',        // 文件原名\n        data: ReadStream,            // 文件流\n        mimeType: 'application/pdf', // mime\n        fieldName: 'file'            // field name\n      },\n    ]\n\n    */\n  }\n}\n```\n\n流式模式中的对象包含以下几个字段。\n\n```typescript\nexport interface UploadStreamFieldInfo {\n  /**\n   * 上传的文件名\n   */\n  filename: string;\n  /**\n   * 上传文件 mime 类型\n   */\n  mimeType: string;\n  /**\n   * 上传文件的文件流\n   */\n  data: Readable;\n  /**\n   * 上传的表单字段名\n   */\n  fieldName: string;\n}\n```\n\n</TabItem>\n\n</Tabs>\n\n\n\n### 上传文件后缀检查\n\n通过 `whitelist` 属性，配置允许上传的文件后缀名，配置 `null` 则不校验后缀名。\n\n:::caution\n\n如果配置为 `null`，则不对上传文件后缀名进行校验，如果采取文件上传模式 (mode=file)，则会有可能被攻击者所利用，上传 `.php`、`.asp` 等后缀的 WebShell 实现攻击行为。\n\n当然，由于组件会对上传后的临时文件采取 `重新随机生成` 文件名写入，只要开发者 `不将` 上传后的临时文件地址返回给用户，那么即使用户上传了一些不被预期的文件，那也无需过多担心会被利用。\n\n:::\n\n\n如果上传的文件后缀不匹配，会响应 `400` error，默认值如下：\n\n```ts\n'.jpg',\n'.jpeg',\n'.png',\n'.gif',\n'.bmp',\n'.wbmp',\n'.webp',\n'.tif',\n'.psd',\n'.svg',\n'.js',\n'.jsx',\n'.json',\n'.css',\n'.less',\n'.html',\n'.htm',\n'.xml',\n'.pdf',\n'.zip',\n'.gz',\n'.tgz',\n'.gzip',\n'.mp3',\n'.mp4',\n'.avi',\n```\n\n可以通过组件中导出的 `uploadWhiteList` 获取到默认的后缀名白名单。\n\n另外，midway 上传组件，为了避免部分 `恶意用户`，通过某些技术手段来`伪造`一些可以被截断的扩展名，所以会对获取到的扩展名的二进制数据进行过滤，仅支持 `0x2e`（即英文点 `.`）、`0x30-0x39`（即数字 `0-9`）、`0x61-0x7a`（即小写字母 `a-z`） 范围内的字符作为扩展名，其他字符将会被自动忽略。\n\n你可以传递一个函数，可以根据不同的条件动态返回白名单。\n\n```typescript\n// src/config/config.default.ts\nimport { uploadWhiteList } from '@midwayjs/busboy';\nimport { tmpdir } from 'os';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  busboy: {\n    whitelist: (ctx) => {\n      if (ctx.path === '/') {\n        return [\n          '.jpg',\n          '.jpeg',\n        ];\n      } else {\n        return [\n          '.jpg',\n        ]\n      };\n    },\n    // ...\n  },\n}\n```\n\n\n\n\n\n### 上传文件 MIME 类型检查\n\n部分`恶意用户`，会尝试将 `.php` 等 WebShell 修改扩展名为 `.jpg`，来绕过基于扩展名的白名单过滤规则，在某些服务器环境内，这个 jpg 文件依然会被作为 PHP 脚本来执行，造成安全风险。\n\n组件提供了 `mimeTypeWhiteList` 配置参数 **【请注意，此参数无默认值设置，即默认不校验】**，您可以通过此配置设置允许的文件 MIME 格式，规则为由数组 `[扩展名, mime, [...moreMime]]` 组成的 `二级数组`，例如：\n\n```typescript\n// src/config/config.default.ts\nimport { uploadWhiteList } from '@midwayjs/busboy';\nexport default {\n  // ...\n  busboy: {\n    // ...\n    // 扩展名白名单\n    whitelist: uploadWhiteList,\n    // 仅允许下面这些文件类型可以上传\n    mimeTypeWhiteList: {\n      '.jpg': 'image/jpeg',\n      // 也可以设置多个 MIME type，比如下面的允许 .jpeg 后缀的文件是 jpg 或者是 png 两种类型\n      '.jpeg': ['image/jpeg', 'image/png'],\n      // 其他类型\n      '.gif': 'image/gif',\n      '.bmp': 'image/bmp',\n      '.wbmp': 'image/vnd.wap.wbmp',\n      '.webp': 'image/webp',\n    }\n  },\n}\n```\n\n您也可以使用组件提供的 `DefaultUploadFileMimeType` 变量，作为默认的 MIME 校验规则，它提供了常用的 `.jpg`、`.png`、`.psd` 等文件扩展名的 MIME 数据：\n\n```typescript\n// src/config/config.default.ts\nimport { uploadWhiteList, DefaultUploadFileMimeType } from '@midwayjs/busboy';\nexport default {\n  // ...\n  busboy: {\n    // ...\n    // 扩展名白名单\n    whitelist: uploadWhiteList,\n    // 仅允许下面这些文件类型可以上传\n    mimeTypeWhiteList: DefaultUploadFileMimeType,\n  },\n}\n```\n\n文件格式与对应的 MIME 映射，您可以通过 `https://mimetype.io/` 这个网站来查询，对于文件的 MIME 识别，我们使用的是 [file-type@16](https://www.npmjs.com/package/file-type) 这个 npm 包，请注意它支持的文件类型。\n\n:::info\n\nMIME 类型校验规则仅适用于使用 文件上传模式 `mode=file`，同时设置此校验规则之后，由于需要读取文件内容进行匹配，所以会稍微影响上传性能。\n\n但是，我们依然建议您在条件允许的情况下，设置 `mimeTypeWhiteList` 参数，这将提升您的应用程序安全性。\n\n:::\n\n你可以传递一个函数，可以根据不同的条件动态返回 MIME 规则。\n\n```typescript\n// src/config/config.default.ts\nimport { tmpdir } from 'os';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  busboy: {\n    mimeTypeWhiteList: (ctx) => {\n      if (ctx.path === '/') {\n        return {\n          '.jpg': 'image/jpeg',\n        };\n      } else {\n        return {\n          '.jpeg': ['image/jpeg', 'image/png'],\n        }\n      };\n    }\n  },\n}\n\n```\n\n\n\n### Busboy 上传限制\n\n默认情况下没有限制，可以通过配置修改，数字类型，单位为 byte。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  busboy: {\n    // ...\n    limits: {\n      fileSize: 1024\n    }\n  },\n}\n```\n\n除此之外，还可以设置一些其他的 [限制](https://github.com/mscdex/busboy/tree/master?tab=readme-ov-file#exports)。\n\n\n\n\n### 临时文件与清理\n\n\n如果你使用了 `file` 模式来获取上传的文件，那么上传的文件会存放在您于 `config` 文件中设置的 `upload` 组件配置中的 `tmpdir` 选项指向的文件夹内。\n\n你可以通过在配置中使用 `cleanTimeout` 来控制自动的临时文件清理时间，默认值为 `5 * 60 * 1000`，即上传的文件于 `5 分钟` 后自动清理，设置为 `0` 则视为不开启自动清理功能。\n\n```typescript\n// src/config/config.default.ts\nimport { uploadWhiteList } from '@midwayjs/busboy';\nimport { tmpdir } from 'os';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  busboy: {\n    mode: 'file',\n    tmpdir: join(tmpdir(), 'midway-busboy-files'),\n    cleanTimeout: 5 * 60 * 1000,\n  },\n}\n\n```\n\n你也可以在代码中通过调用 `await ctx.cleanupRequestFiles()` 来主动清理当前请求上传的临时文件。\n\n\n\n### 设置不同路由的配置\n\n通过中间件的不同实例，可以对不同的路由做不同的配置，这种场景下会和全局配置合并，仅能覆盖一小部分配置。\n\n```typescript\nimport { Controller, Post, Files, Fields } from '@midwayjs/core';\nimport { UploadFileInfo, UploadMiddleware } from '@midwayjs/busboy';\n\n@Controller('/')\nexport class HomeController {\n  @Post('/upload1', { middleware: [ createMiddleware(UploadMiddleware, {mode: 'file'}) ]})\n  async upload1(@Files() files Array<UploadFileInfo>) {\n    // ...\n  }\n  \n  @Post('/upload2', { middleware: [ createMiddleware(UploadMiddleware, {mode: 'stream'}) ]})\n  async upload2(@Files() files Array<UploadFileInfo>) {\n    // ...\n  }\n}\n```\n\n当前可以传递的配置包括 `mode` 以及 `busboy` 自带的 [配置](https://github.com/mscdex/busboy/tree/master?tab=readme-ov-file#exports)。\n\n\n\n## 内置错误\n\n以下的错误在不同上传模式下均会自动触发。\n\n* `MultipartInvalidFilenameError` 无效文件名\n* `MultipartInvalidFileTypeError` 无效文件类型\n* `MultipartFileSizeLimitError` 文件大小超出限制\n* `MultipartFileLimitError` 文件数量超出限制\n* `MultipartPartsLimitError` 上传 parts 数量超出限制\n* `MultipartFieldsLimitError` fields 数量超出限制\n* `MultipartError` 其余的 busbuy 错误\n\n\n\n## 安全提示\n\n1. 请注意是否开启 `扩展名白名单` （whiteList），如果扩展名白名单被设置为 `null`，则会有可能被攻击者所利用上传 `.php`、`.asp` 等WebShell。\n2. 请注意是否设置 `match` 或 `ignore` 规则，否则普通的 `POST/PUT` 等接口会有可能被攻击者利用，造成服务器负荷加重和空间大量占用问题。\n3. 请注意是否设置 `文件类型规则` （fileTypeWhiteList）,否则可能会被攻击者伪造文件类型进行上传。\n\n\n\n## 前端文件上传示例\n\n### 1. html form 的形式\n\n```html\n<form action=\"/api/upload\" method=\"post\" enctype=\"multipart/form-data\">\n  Name: <input type=\"text\" name=\"name\" /><br />\n  File: <input type=\"file\" name=\"testFile\" /><br />\n  <input type=\"submit\" value=\"Submit\" />\n</form>\n```\n\n### 2. fetch FormData 方式\n\n```js\nconst fileInput = document.querySelector('#your-file-input') ;\nconst formData = new FormData();\nformData.append('file', fileInput.files[0]);\n\nfetch('/api/upload', {\n  method: 'POST',\n  body: formData,\n});\n```\n\n\n\n## Postman 测试示例\n\n![](https://img.alicdn.com/imgextra/i4/O1CN01iv9ESW1uIShNiRjBF_!!6000000006014-2-tps-2086-1746.png)\n\n","site/docs/extensions/cache.md":"# 缓存\n\nMidway Cache 是为了方便开发者进行缓存操作的组件，它有利于改善项目的性能。它为我们提供了一个数据中心以便进行高效的数据访问。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n## 安装\n\n首先安装相关的组件模块。\n\n```bash\n$ npm i @midwayjs/cache@4 cache-manager --save\n$ npm i @types/cache-manager --save-dev\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/cache\": \"^4.0.0\",\n    \"cache-manager\": \"^3.4.1\",\n    // ...\n  },\n  \"devDependencies\": {\n    \"@types/cache-manager\": \"^3.4.0\",\n    // ...\n  }\n}\n```\n\n\n\n## 使用 Cache\n\nMidway 为不同的 cache 存储提供了统一的 API。默认内置了一个基于内存数据存储的数据中心。如果想要使用别的数据中心，开发者也可以切换到例如 mongodb、fs 等模式。\n\n\n首先，引入 Cache 组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as cache from '@midwayjs/cache';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    cache\t\t// 导入 cache 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n然后在业务代码中即可注入使用。\n\n```typescript\nimport { Inject, Provide } from '@midwayjs/core';\nimport { IUserOptions } from '../interface';\nimport { CacheManager } from '@midwayjs/cache';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  cacheManager: CacheManager;     \t\t\t// 依赖注入 CacheManager\n}\n```\n\n通过提供的 API 来设置，获取缓存数据。\n\n\n```typescript\nimport { Inject, Provide } from '@midwayjs/core';\nimport { IUserOptions } from '../interface';\nimport { CacheManager } from '@midwayjs/cache';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  cacheManager: CacheManager;\n\n  async getUser(options: IUserOptions) {\n    // 设置缓存内容\n    await this.cacheManager.set(`name`, 'stone-jin');\n\n    // 获取缓存内容\n    let result = await this.cacheManager.get(`name`);\n\n    return result;\n  }\n\n  async getUser2(){\n    //获取缓存内容\n    let result = await this.cacheManager.get(`name`);\n    return result;\n  }\n\n  async reset(){\n    await this.cacheManager.reset(); // 清空对应 store 的内容\n  }\n}\n```\n\n\n\n### 设置缓存\n\n\n我们通过 `await this.cache.set(key, value)` 方法进行设置，此处默认过期时间是10s。\n\n\n你也可以手动设置 TTL（过期时间），如下：\n```typescript\nawait this.cacheManager.set(key, value, {ttl: 1000});\t// ttl的单位为秒\n```\n如果你想要 Cache 不过期，则将 TTL 设置为 null 即可。\n```typescript\nawait this.cacheManager.set(key, value, {ttl: null});\n```\n同时你也可以通过全局的 `config.default.ts` 中进行设置。\n```typescript\nexport default {\n  // ...\n  cache: {\n    store: 'memory',\n    options: {\n      max: 100,\n      ttl: 10,\t\t// 修改默认的ttl配置\n    },\n  }\n}\n```\n\n\n### 获取缓存\n\n```typescript\nconst value = await this.cacheManager.get(key);\n```\n如果获取不到，则为 undefined。\n\n\n\n### 移除缓存\n\n\n移除缓存，可以通过 del 方法。\n```typescript\nawait this.cacheManager.del(key);\n```\n\n\n\n### 清空整体store数据（此处是整体清除，需要重点⚠️）\n\n\n比如用户设置了某个 redis 为 store，调用的话，包括非 cache 模块设置的也会清除。\n```typescript\nawait this.cacheManager.reset(); // 这块需要注意\n```\n\n\n\n## 全局配置\n\n\n当我们引用了这个 cache 组件后，我们能对其进行全局的配置。配置方法跟别的组件类似。\n\n\n默认的配置：\n```typescript\nexport default {\n  // ...\n  cache: {\n  \tstore: 'memory',\n    options: {\n      max: 100,\n      ttl: 10,\n    },\n  }\n}\n```\n例如用户可以修改默认的 TTL，也就是过期时间。\n\n\n\n## 其他Cache\n\n\n用户也可以修改  store 方式，在 `config.default.ts` 中进行组件的配置：\n```typescript\nimport * as redisStore from 'cache-manager-ioredis';\n\nexport default {\n  // ...\n  cache: {\n  \tstore: redisStore,\n    options: {\n      host: 'localhost', // default value\n      port: 6379, // default value\n      password: '',\n      db: 0,\n      keyPrefix: 'cache:',\n      ttl: 100\n    },\n  }\n}\n```\n或者修改为 mongodb 的 cache。\n\n\n:::danger\n**再次注意⚠️：使用 redis 作为 cache 的时候，代码里面慎用 reset 方法，因为会把整个 redis 给 flushdb，简称数据清空。**\n:::\n\n\n\n## 相关文档\n\n\n由于 Midway Cache 是基于 cache-manager 封装，所以相关资料用户也可以查询：[cache-manger](https://www.npmjs.com/package/cache-manager)。\n\n\n\n## 常见问题\n\n\n\n### 1、set 和 get 无法得到相同值？\n\n用户使用了 cache 模块，默认是内存式的，例如在本地用 dev 模式，由于是单进程的，那 set 和 ge t最终能达到相同的值。但是用户部署到服务器上面后，由于会有多 worker，相当于第一次请求，落在进程1上，然后第二次落在进程2上，这样获得到空了。\n\n\n解决办法：参考 其他 Cache 的章节，配置 store 为分布式，例如 redis 的 store 等方式。\n","site/docs/extensions/caching.md":"# 缓存\n\n缓存是一个伟大而简单的技术，有助于提高你的应用程序的性能。本组件提供了缓存相关的能力，你可以将数据缓存到不同的数据源，也可以针对不同场景建立多级缓存，提高数据访问速度。\n\n:::tip\n\nMidway 提供基于 [cache-manager v5](https://github.com/node-cache-manager/node-cache-manager) 模块重新封装了缓存组件，原有的缓存模块基于 v3 开发不再迭代，如需查看老文档，请访问 [这里](/docs/extensions/cache)。\n\n:::\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 安装\n\n首先安装相关的组件模块。\n\n```bash\n$ npm i @midwayjs/cache-manager@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/cache-manager\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 启用组件\n\n\n首先，引入组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as cacheManager from '@midwayjs/cache-manager';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    cacheManager,\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n\n\n## 使用缓存\n\n\n\n### 1、配置缓存\n\n在使用前，你需要配置缓存所在的位置，比如内置的内存缓存，或者是引入 Redis 缓存，每个缓存对应了一个缓存的 Store。\n\n下面的示例代码，配置了一个名为 `default` 的内存缓存。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: 'memory',\n      },\n    },\n  }\n}\n```\n\n最常用的场景下，缓存会包含两个参数，配置 `max` 修改缓存的数量，配置 `ttl` 修改缓存的过期时间，单位毫秒。\n\n```typescript\n // src/config/config.default.ts\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: 'memory',\n        options: {\n          max: 100,\n          ttl: 10,\n        },\n      },\n    },\n  }\n}\n```\n\n:::tip\n\n* `ttl` 的单位是毫秒\n* `max` 代表缓存 key 的最大个数\n* 不同的 Store 淘汰 key 的算法不同，内存缓存使用的淘汰算法是 LRU\n\n:::\n\n### 2、使用缓存\n\n可以通过服务工厂的装饰器获取到实例，可以通过简单的 `get` 和 `set` 方法获取和保存缓存。\n\n```typescript\nimport { InjectClient, Provide } from '@midwayjs/core';\nimport { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n\n  @InjectClient(CachingFactory, 'default')\n  cache: MidwayCache;\n\n  async invoke(name: string, value: string) {\n    // 设置缓存\n    await this.cache.set(name, value);\n    // 获取缓存\n    const data = await this.cache.get(name);\n    // ...\n  }\n}\n\n```\n\n动态设置 `ttl` 过期时间。\n\n```typescript\nawait this.cache.set('key', 'value', 1000);\n```\n\n若要禁用缓存过期，可以将 `ttl` 配置属性设置为 0。\n\n```typescript\nawait this.cache.set('key', 'value', 0);\n```\n\n删除单个缓存。\n\n```typescript\nawait this.cache.del('key');\n```\n\n清理整个缓存，可以使用 `reset` 方法。\n\n```typescript\nawait this.cacheManager.reset();\n```\n\n:::danger\n\n注意，清理整个缓存非常危险，如果使用了 Redis 作为缓存 store，将清空整个 Redis 数据。\n\n:::\n\n除了装饰器之外，也可以通过 API 获取缓存实例。\n\n```typescript\nimport { InjectClient, Provide } from '@midwayjs/core';\nimport { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  cachingFactory: CachingFactory;\n\n  async invoke() {\n    const caching = await this.cachingFactory.get('default');\n    // ...\n  }\n}\n```\n\n\n\n### 3、配置多个缓存\n\n和其他组件一样，组件支持配置多个缓存实例。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: 'memory',\n      },\n      otherCaching: {\n        store: 'memory',\n      }\n    },\n  }\n}\n```\n\n可以注入不同的缓存实例。\n\n```typescript\nimport { InjectClient, Provide } from '@midwayjs/core';\nimport { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n\n  @InjectClient(CachingFactory, 'default')\n  cache: MidwayCache;\n  \n  @InjectClient(CachingFactory, 'otherCaching')\n  customCaching: MidwayCache;\n\n}\n\n```\n\n\n\n### 4、配置不同 Store\n\n组件基于 [cache-manager](https://github.com/node-cache-manager/node-cache-manager) 可以配置不同的缓存 Store，比如最常见的可以配置 Redis Store。\n\n假如项目已经配置了一个 `Redis`，通过组件内置的 `createRedisStore` 方法，可以快速创建一个 Redis Store。\n\n\n```typescript\nimport { createRedisStore } from '@midwayjs/cache-manager';\n\n// src/config/config.default.ts\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: createRedisStore('default'),\n        options: {\n          ttl: 10,\n        }\n      },\n    },\n  },\n  redis: {\n    clients: {\n      default: {\n        port: 6379,\n        host: '127.0.0.1',\n      }\n    }\n  }\n}\n```\n\n`createRedisStore` 方法可以传递一个已经配置的 redis 实例名，可以和 redis 组件复用实例。\n\n\n\n### 5、配置三方 Store\n\n除了 Redis 之外，用户也可以自行选择 Cache-Manager 的 Store，列表可以参考 [这里](https://github.com/node-cache-manager/node-cache-manager?tab=readme-ov-file#store-engines)。\n\n下面是一个配置  [node-cache-manager-ioredis-yet](https://github.com/node-cache-manager/node-cache-manager-ioredis-yet) 的例子。\n\n```typescript\n// src/config/config.default.ts\nimport { redisStore } from 'cache-manager-ioredis-yet';\n\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: redisStore,\n        options: {\n          port: 6379,\n          host: 'localhost',\n          ttl: 10,\n        },\n      },\n    },\n  }\n}\n```\n\n\n\n### 6、多级缓存\n\n[cache-manager](https://github.com/node-cache-manager/node-cache-manager) 支持将多个缓存 Store 聚合到一起，实现多级缓存。\n\n下面的示例将内存缓存和 Redis 缓存组合到一起，分别设置 60 秒和 600 秒的默认过期时间。\n\n```typescript\n// src/config/config.default.ts\nimport { createRedisStore } from '@midwayjs/cache-manager';\nexport default {\n  cacheManager: {\n    clients: {\n      memoryCaching: {\n        store: 'memory',\n        options: {\n          ttl: 60_000, // 60 秒，单位为毫秒\n        },\n      },\n      redisCaching: {\n        store: createRedisStore('default'),\n        options: {\n          ttl: 600_000, // 600 秒，单位为毫秒\n        },\n      },\n      multiCaching: {\n        store: ['memoryCaching', 'redisCaching'],\n      },\n    },\n  },\n  redis: {\n    clients: {\n      default: {\n        port: 6379,\n        host: '127.0.0.1',\n      },\n    },\n  },\n};\n\n```\n\n这样 `multiCaching` 这个缓存实例就包含了两级缓存，缓存的优先级从上到下，在查找时，会先查找 `memoryCaching` ，如果内存缓存不存在 key，则继续查找 `redisCaching`。\n\n多级缓存自身的 `options.ttl` 不参与 `set`、`mset` 的写入，请在各个子缓存的 `options.ttl` 中配置默认过期时间。内置内存 Store 和 `createRedisStore` 使用的 TTL 单位都是毫秒。\n\n\n\n### 7、使用多级缓存\n\n和普通缓存类似，多级缓存除了 `set`、`get`、`del`方法外，还增加了 `mset` 、`mget`、`mdel` 方法。\n\n```typescript\nimport { InjectClient, Provide } from '@midwayjs/core';\nimport { CachingFactory, MidwayMultiCache } from '@midwayjs/cache-manager';\n\nconst userId2 = 456;\nconst key2 = 'user_' + userId;\nconst ttl = 5;\n\n@Provide()\nexport class UserService {\n\n  @InjectClient(CachingFactory, 'multiCaching')\n  multiCache: MidwayMultiCache;\n\n  async invoke() {\n    // 设置到所有级别的缓存\n    await this.multiCache.set('foo2', 'bar2', ttl);\n\n    // 从最高优先级的缓存 Store 中获取 key\n    console.log(await this.multiCache.get('foo2'));\n    // >> \"bar2\"\n\n    // 调用每一个 Store 的 del 方法进行删除\n    await this.multiCache.del('foo2');\n\n    // 在所有缓存中设置多个 key，可以多个键值对\n    await this.multiCache.mset(\n      [\n        ['foo', 'bar'],\n        ['foo2', 'bar2'],\n      ],\n      ttl\n    );\n\n    // mget() 从最高优先级的缓存中获取值\n    // 如果第一个缓存 Store 中不包含所有的 key，\n    // 继续在下一个缓存 Store 中查找没有找到的 key。\n    // 这是递归地完成的，直到:\n    // - 所有的 key 都已经查找到值\n    // - 所有的缓存 Store 都被查找过\n    console.log(await this.multiCache.mget('key', 'key2'));\n    // >> ['bar', 'bar2']\n\n    // 调用每一个 Store 的 mdel 方法进行删除\n    await this.multiCache.mdel('foo', 'foo2');\n  }\n}\n\n```\n\n对于上面的内存和 Redis 缓存，调用 `set` 或 `mset` 时省略 TTL 参数，各层会使用自己的默认过期时间；显式传入 TTL 时，本次写入的所有缓存层都会使用该值。\n\n```typescript\n// 使用各层默认 TTL：内存 60 秒，Redis 600 秒\nawait this.multiCache.set('key', 'value');\n\n// 覆盖各层默认 TTL：本次写入的两层缓存都使用 30 秒\nawait this.multiCache.set('key', 'value', 30_000);\n```\n\n### 8、自动刷新\n\n不管是普通缓存还是多级缓存，都支持后台刷新功能，只需要配置 `refreshThreshold` 的时间，单位为毫秒。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: 'memory',\n        options: {\n          refreshThreshold: 3 * 1000,\n        },\n      },\n    },\n  }\n}\n```\n\n如果设置了 `refreshthreshold`，每次从缓存获取值之后，会检查 `ttl` 的值，如果剩余的 `ttl` 小于 `refreshthreshold` ，则系统将异步更新缓存，同时系统会返回旧值，直到 `ttl` 过期。\n\n:::tip\n\n* 在多级缓存的情况下，根据优先级顺序找到第一个包含 key 的 Store。\n\n* 如果阈值较低且执行的函数比较慢，key 可能会过期，有可能会遇到并发更新的情况。\n\n* 后台刷新机制目前只支持单个 key。\n\n* 如果没有为 key 设置 `ttl`，则不会触发刷新机制。对于 redis，`ttl`  默认设置为-1。\n\n:::\n\n\n\n## 自动缓存\n\n### 使用装饰器缓存方法\n\n可以通过 `@Caching` 装饰器缓存方法的结果，比如缓存 http 响应或者服务调用的结果。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Caching } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n  @Caching('default')\n  async getUser(name: string) {\n    return name;\n  }\n}\n\n```\n\n当第一次调用 `getUser` 方法时，会正常执行逻辑，返回结果，装饰器会将结果缓存起来，第二次执行时，如果缓存未失效，则会从缓存中直接返回。\n\n### 指定缓存的 ttl\n\n也可以单独设置 `ttl` 。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Caching } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n  @Caching('default', 100)\n  async getUser(name: string) {\n    return name;\n  }\n}\n```\n\n### 手动指定缓存 key\n\n如果对自动生成的 key 不满意，可以手动指定缓存的 key。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Caching } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n  @Caching('default', 'customKey', 100)\n  async getUser(name: string) {\n    return name;\n  }\n}\n```\n\n### 带逻辑的缓存\n\n如果你希望根据一些特定逻辑进行缓存，比如特定参数，或者特定的 Header，可以传递一个工具函数进行逻辑判断。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Caching } from '@midwayjs/cache-manager';\n\nfunction cacheBy({methodArgs, ctx, target}) {\n  if (methodArgs[0] === 'harry' || methodArgs[0] === 'mike') {\n    return 'cache1';\n  }\n}\n\n@Provide()\nexport class UserService {\n  @Caching('default', cacheBy, 100)\n  async getUser(name: string) {\n    return 'hello ' + name;\n  }\n}\n```\n\n上面的示例中，`cacheBy` 方法自定义了缓存的逻辑，当方法入参值为 `harry` 或者 `mike` 时，将返回缓存的 `key` ，而其他参数时则跳过缓存。\n\n这个时候执行的结果为：\n\n```typescript\nawait userService.getUser('harry'));\t// hello harry\nawait userService.getUser('mike'));\t// hello harry\nawait userService.getUser('lucy'));\t// hello lucy\n```\n\n`@Caching` 装饰器可以在第二个参数中传递一个方法，这个方法的入参 options 为：\n\n* `methodArgs`  当前调用方法的实际参数\n* `ctx` 如果是请求作用域，则是当前调用的上下文对象，如果是单例，则该对象为空对象\n* `target` 当前调用的实例\n\n方法的返回值为字符串或者布尔值，当返回字符串时，表示以该 key 将方法的结果缓存，当返回 `undefined` 或者 `null` 时，表示跳过缓存。\n\n通过这些参数判断，我们可以实现非常灵活的自定义缓存逻辑。\n\n\n\n## 常见问题\n\n\n\n### 1、多进程下内存缓存 set 和 get 无法得到相同值\n\n这是正常现象，每个进程的数据是独立的，仅保存在当前进程中。如需跨进程缓存，请使用 Redis 这类分布式缓存系统。\n","site/docs/extensions/captcha.md":"# 验证码\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的通用验证码组件，支持 `图片验证码`、`计算表达式` 等类型验证码。\n\n您也可以通过此组件，来实现 `短信验证码`、`邮件验证码` 等验证能力，但是注意，本组件本身不含发送短信、邮件功能。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/captcha@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/captcha\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n## 启用组件\n\n在 `src/configuration.ts` 中引入组件。\n\n```typescript\nimport * as captcha from '@midwayjs/captcha';\n\n@Configuration({\n  imports: [\n    // ...other components\n    captcha\n  ],\n})\nexport class MainConfiguration {}\n```\n\n## 调用服务\n\n```typescript\nimport { Controller, Inject } from '@midwayjs/core';\nimport { CaptchaService } from '@midwayjs/captcha';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  ctx;\n\n  @Inject()\n  captchaService: CaptchaService;\n\n  // 示例：获取图像验证码\n  @Get('/get-image-captcha')\n  async getImageCaptcha() {\n    const { id, imageBase64 } = await this.captchaService.image({ width: 120, height: 40 });\n    return {\n      id,          // 验证码 id\n      imageBase64, // 验证码 SVG 图片的 base64 数据，可以直接放入前端的 img 标签内\n    }\n  }\n\n  // 示例：获取计算表达式验证码\n  @Get('/get-formula-captcha')\n  async getFormulaCaptcha() {\n    const { id, imageBase64 } = await this.captchaService.formula({ noise: 1 });\n    return {\n      id,          // 验证码 id\n      imageBase64, // 验证码 SVG 图片的 base64 数据，可以直接放入前端的 img 标签内\n    }\n  }\n\n  // 验证验证码是否正确\n  @Post('/check-captcha')\n  async getCaptcha() {\n    const { id, answer } = this.ctx.request.body;\n    const passed: boolean = await this.captchaService.check(id, answer);\n    if (passed) {\n      return 'passed';\n    }\n    return 'error';\n  }\n\n  // 示例：短信验证码\n  @Post('/sms-code')\n  async sendSMSCode() {\n    // 验证验证码是否正确\n    const { id, text: code } = await this.captchaService.text({ size: 4 });\n    await sendSMS(18888888888, code);\n    return { id }\n  }\n\n  // 示例：邮件验证码\n  @Post('/email-code')\n  async sendEmailCode() {\n    // 验证验证码是否正确\n    const { id, text: code } = await this.captchaService.text({ type: 'number'});\n    await sendEmail('admin@example.com', code);\n    return { id }\n  }\n\n   // 示例：将任意文本内容塞入验证码中\n  @Get('/test-text')\n  async testText() {\n    // 存入内容，获取验证码id\n    const id: string = await this.captchaService.set('123abc');\n    // 根据验证码id，校验内容是否正确\n    const passed: boolean = await this.captchaService.check(id, '123abc');\n    return {\n      passed: passed === true,\n    }\n  }\n}\n```\n\n## 可用配置\n\n```typescript\ninterface CaptchaOptions {\n  default?: { // 默认配置\n    // 验证码字符长度，默认 4 个字符\n    size?: number;\n    // 干扰线条的数量，默认 1 条\n    noise?: number;\n    // 宽度，默认为 120 像素\n    width?: number;\n    // 宽度，默认为 40 像素\n    height?: number;\n    // 图形验证码配置，图形中包含一些字符\n  },\n  image?: {\n    // 验证码字符长度，默认 4 个字符\n    size?: number;\n    // 图像验证码中的字符类型，默认为 'mixed'\n    // - 'mixed' 表示 0-9、A-Z 和 a-z\n    // - 'letter' 表示 A-Z 和 a-z\n    // - 'number' 表示 0-9\n    type?: 'mixed',\n    // 干扰线条的数量，默认 1 条\n    noise?: number;\n    // 宽度，默认为 120 像素\n    width?: number;\n    // 宽度，默认为 40 像素\n    height?: number;\n  },\n  // 计算公式验证码配置，例如返回的图像内容为 1+2，需要用户填入 3\n  formula?: {\n     // 干扰线条的数量，默认 1 条\n    noise?: number;\n    // 宽度，默认为 120 像素\n    width?: number;\n    // 宽度，默认为 40 像素\n    height?: number;\n  },\n  // 纯文本验证码配置，基于纯文本验证码可以实现短信验证码、邮件验证码\n  text?: {\n    // 验证码字符长度，默认 4 个字符\n    size?: number;\n    // 文本验证码中的字符类型，默认为 'mixed'\n    // - 'mixed' 表示 0-9、A-Z 和 a-z\n    // - 'letter' 表示 A-Z 和 a-z\n    // - 'number' 表示 0-9\n    type?: 'mixed',\n  },\n  // 验证码过期时间，默认为 1h\n  expirationTime?: 3600,\n  // 验证码存储的 key 前缀\n  idPrefix: 'midway:vc',\n}\n\nexport const captcha: CaptchaOptions = {\n  default: { // 默认配置\n    size: 4,\n    noise: 1,\n    width: 120,\n    height: 40,\n  },\n  image: {      // 最终会合并 default 配置\n    type: 'mixed',\n  },\n  formula: {},  // 最终会合并 default 配置\n  text: {},     // 最终会合并 default 配置\n  expirationTime: 3600,\n  idPrefix: 'midway:vc',\n}\n```\n\n更多配置请参考 [svg-captcha](https://github.com/produck/svg-captcha)。\n\n### 配置示例一\n\n获取一个 包含 `5个纯英文字母` 的图像验证码，图像宽度 `200` 像素，高度 `50` 像素，并且包含 `3` 条干扰线。\n\n因为图像验证码的配置 `image`， 会与 `default` 配置进行合并，所以可以只修改 `default` 配置：\n\n```typescript\nexport const captcha: CaptchaOptions = {\n  default: {\n    size: 5,\n    noise: 3,\n    width: 200,\n    height: 50\n  },\n  image: {\n    type: 'letter'\n  }\n}\n```\n当然，也可以 `不` 修改 `default` 配置，将宽度、高度等在 `image` 配置项中进行配置，取得 `同样`的效果：\n```typescript\nexport const captcha: CaptchaOptions = {\n  image: {\n    size: 5,\n    noise: 3,\n    width: 200,\n    height: 50\n    type: 'letter'\n  }\n}\n```\n\n### 配置示例二\n\n获取一个图像宽度 `100` 像素，高度 `60` 像素，并且包含 `2` 条干扰线的计算表达式验证码。\n\n因为计算表达式验证码的配置 `formula` ，会与 `default` 配置合并，所以可以只修改 `default` 配置：\n\n```typescript\nexport const captcha: CaptchaOptions = {\n  default: {\n    noise: 2,\n    width: 100,\n    height: 60\n  },\n}\n```\n当然，也可以 `不` 修改 `default` 配置，将宽度、高度等在 `formula` 配置项中进行配置，取得 `同样`的效果：\n```typescript\nexport const captcha: CaptchaOptions = {\n  formula: {\n    noise: 2,\n    width: 100,\n    height: 60\n  }\n}\n```\n\n\n## 组件依赖\n\n验证码的内容存储基于 `@midwayjs/cache-manager` 组件，默认创建了一个名为 `captcha` 的缓存实例，将数据存储在 `memory` 中。\n\n```typescript\nexport default {\n  cacheManager: {\n    clients: {\n      captcha: {\n        store: 'memory',\n      },\n    },\n  },\n};\n```\n\n如果要替换为 `redis` 或其他服务，请参照 `@midwayjs/cache-manager` 的 [文档](/docs/extensions/caching)，对 cache 进行配置。\n\n\n\n\n## 效果\n\n**图片验证码**\n\n![图片验证码](https://gw.alicdn.com/imgextra/i4/O1CN014cEzLH23vEniOgoyp_!!6000000007317-2-tps-120-40.png)\n\n**计算表达式**\n\n ![计算表达式](https://gw.alicdn.com/imgextra/i4/O1CN01u3Mj0q24lRx1md9pX_!!6000000007431-2-tps-120-40.png)\n\n\n## 注意\n\n* 为了防止机器学习破解，使用的 `svg-captcha` 包为 [安全修复后](https://juejin.cn/post/6872656117839691789) 的版本。\n","site/docs/extensions/casbin.md":"# 角色鉴权\n\nCasbin 是一个强大的、高效的开源访问控制框架，其权限管理机制支持多种访问控制模型。\n\n官网文档：https://casbin.org/\n\n\n\n## Casbin 是什么\n\nCasbin 可以：\n\n1. 支持自定义请求的格式，默认的请求格式为`{subject, object, action}`。\n2. 具有访问控制模型model和策略policy两个核心概念。\n3. 支持RBAC中的多层角色继承，不止主体可以有角色，资源也可以具有角色。\n4. 支持内置的超级用户 例如：`root` 或 `administrator`。超级用户可以执行任何操作而无需显式的权限声明。\n5. 支持多种内置的操作符，如 `keyMatch`，方便对路径式的资源进行管理，如 `/foo/bar` 可以映射到 `/foo*`\n\nCasbin 不能：\n\n1. 身份认证 authentication（即验证用户的用户名和密码），Casbin 只负责访问控制。应该有其他专门的组件负责身份认证，然后由 Casbin 进行访问控制，二者是相互配合的关系。\n2. 管理用户列表或角色列表。 Casbin 认为由项目自身来管理用户、角色列表更为合适， 用户通常有他们的密码，但是 Casbin 的设计思想并不是把它作为一个存储密码的容器。 而是存储RBAC方案中用户和角色之间的映射关系。\n\n:::tip\n\n注意：\n\n- 1、在 Midway v3.6.0 之后可用\n- 2、Midway 只是封装了 Casbin 的 API 并提供简单的支持，策略规则编写请查看 [官方文档](https://casbin.org/)\n- 3、Casbin 不提供登录，只提供现有用户的鉴权，需要搭配 passport 等获取用户信息的组件来使用\n\n:::\n\n\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/casbin@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/casbin\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 启用组件\n\n\n首先，引入组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as casbin from '@midwayjs/casbin';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    casbin,\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n\n\n## 准备模型和策略\n\n使用 Casbin 前需要定义模型和策略，这两个文件的内容贯穿本文，建议先去官网了解相关内容。\n\n我们以一个基础的模型为例，比如：\n\n```\n[request_definition]\nr = sub, obj, act\n\n[policy_definition]\np = sub, obj, act\n\n[role_definition]\ng = _, _\ng2 = _, _\n\n[policy_effect]\ne = some(where (p.eft == allow))\n\n[matchers]\nm = g(r.sub, p.sub) && g2(r.obj, p.obj) && r.act == p.act || r.sub == \"root\"\n```\n\n将其保存在项目根目录的 `basic_model.conf` 文件中。\n\n以及包含下面内容的策略文件。\n\n```\np, superuser, user, read:any\np, manager, user_roles, read:any\np, guest, user, read:own\n\ng, alice, superuser\ng, bob, guest\ng, tom, manager\n\ng2, users_list, user\ng2, user_roles, user\ng2, user_permissions, user\ng2, roles_list, role\ng2, role_permissions, role\n```\n\n将其保存在项目根目录的 `basic_policy.csv` 文件中。\n\n\n\n## 配置模型和策略\n\n这里我们的策略将以文件形式进行演示。\n\n配置如下：\n\n```typescript\nimport { MidwayAppInfo } from '@midwayjs/core';\nimport { join } from 'path';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // ...\n    casbin: {\n      modelPath: join(appInfo.appDir, 'basic_model.conf'),\n      policyAdapter: join(appInfo.appDir, 'basic_policy.csv'),\n    }\n  };\n}\n\n```\n\n\n\n## 装饰器鉴权\n\n有多种形式来使用 Casbin，这里以装饰器作为示例。\n\n### 定义资源\n\n首先定义资源，比如放在 `src/resource.ts` 文件中，对应策略文件中对应的资源。\n\n```typescript\nexport enum Resource {\n  USERS_LIST = 'users_list',\n  USER_ROLES = 'user_roles',\n  USER_PERMISSIONS = 'user_permissions',\n  ROLES_LIST = 'roles_list',\n  ROLE_PERMISSIONS = 'role_permission',\n}\n```\n\n\n\n### 配置获取用户的方式\n\n在使用装饰器鉴权时，我们需要配置一个获取用户的方式，比如在 passport 组件之后，我们会从 `ctx.user` 上获取用户名。\n\n```typescript\nimport { MidwayAppInfo } from '@midwayjs/core';\nimport { join } from 'path';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // ...\n    casbin: {\n      modelPath: join(appInfo.appDir, 'basic_model.conf'),\n      policyAdapter: join(appInfo.appDir, 'basic_policy.csv'),\n      usernameFromContext: (ctx) => {\n        return ctx.user;\n      }\n    }\n  };\n}\n\n```\n\n\n\n### 增加守卫\n\n装饰器鉴权依赖守卫，我们可以在全局或者某些路由上开启，全局守卫使用请参考守卫章节。\n\n比如，我们只在下面的 `findAllUsers` 方法上开启鉴权，`AuthGuard` 是 `@midwayjs/casbin` 提供的守卫，可以直接使用。\n\n```typescript\nimport { Controller, Get, UseGuard } from '@midwayjs/core';\nimport { AuthGuard } from '@midwayjs/casbin';\nimport { Resource } from './resouce';\n\n@Controller('/')\nexport class HomeController {\n\n  @UseGuard(AuthGuard)\n  @Get('/users')\n  async findAllUsers() {\n    // ...\n  }\n}\n```\n\n\n\n### 定义权限\n\n使用 `UsePermission` 装饰器定义路由需要的权限。\n\n```typescript\nimport { Controller, Get, UseGuard } from '@midwayjs/core';\nimport { AuthActionVerb, AuthGuard, AuthPossession, UsePermission } from '@midwayjs/casbin';\nimport { Resource } from './resouce';\n\n@Controller('/')\nexport class HomeController {\n\n  @UseGuard(AuthGuard)\n  @UsePermission({\n    action: AuthActionVerb.READ,\n    resource: Resource.USER_ROLES,\n    possession: AuthPossession.ANY\n  })\n  @Get('/users')\n  async findAllUsers() {\n    // ...\n  }\n}\n```\n\n没有权限读取 `USER_ROLES` 的用户不能调用 findAllUsers 方法，在请求时会返回 403 状态码。\n\n比如，上面的 `bob` 用户访问则会返回 403， 而 `tom` 用户访问则正常返回。\n\n\n\n`UsePermission` 需要提供一个对象参数，包括 `action`、`resource`、`possession` 和一个可选的 `isOwn` 的对象。\n\n- `action` 是一个 `AuthActionVerb` 枚举，包含读，写等操作\n- `resource` 资源字符串\n- `possession` 是一个 `AuthPossession` 枚举\n- `isOwn` 是一个接受`Context`（守卫 `canActivate`的参数）作为唯一参数并返回布尔值的函数。 `AuthZGuard` 使用它来确定用户是否是资源的所有者。 如果未定义，将使用返回 `false` 的默认函数。\n\n可以同时定义多个权限，但只有当所有权限都满足时，才能访问该路由。\n\n比如：\n\n```typescript\n@UsePermissions({\n  action: AuthActionVerb.READ,\n  resource: 'USER_ADDRESS',\n  possession: AuthPossession.ANY\n}, {\n  action; AuthActionVerb.READ,\n  resource: 'USER_ROLES,\n  possession: AuthPossession.ANY\n})\n```\n\n只有当用户被授予读取 `USER_ADDRESS` 和 `USER_ROLES` 这两个权限时，才能访问该路由。\n\n\n\n## API 鉴权\n\nCasbin 本身提供了一些通用的 API 和权限相关的功能。\n\n我们可以通过直接注入 `CasbinEnforcerService` 服务来使用。\n\n比如，我们可以在守卫或者中间件中编码。\n\n```typescript\nimport { CasbinEnforcerService } from '@midwayjs/casbin';\nimport { Guard, IGuard } from '@midwayjs/core';\n\n@Guard()\nexport class UserGuard extends IGuard {\n\n  @Inject()\n  casbinEnforcerService: CasbinEnforcerService;\n\n  async canActivate(ctx, clz, methodName) {\n    // 用户登录了，并且是特定的方法，则检查权限\n    if (ctx.user && methodName === 'findAllUsers') {\n      return await this.casbinEnforcerService.enforce(ctx.user, 'USER_ROLES', 'read');\n    }\n    // 未登录用户不允许访问\n    return false;\n  }\n}\n```\n\n在启用守卫后，效果和上面的装饰器相同。\n\n此外，`CasbinEnforcerService` 还有更多的 API，比如重新加载策略。\n\n```typescript\nawait this.casbinEnforcerService.loadPolicy();\n```\n\n\n\n## 分布式策略存储\n\n在多台机器部署的场景下，需要将策略存储到外部。\n\n当前已经实现的适配器有：\n\n- Redis\n- Typeorm\n\n\n\n### Redis Adapter\n\n需要依赖 `@midwayjs/casbin-redis-adapter` 包和 redis 组件。\n\n```bash\n$ npm i @midwayjs/casbin-redis-adapter @midwayjs/redis --save\n```\n\n启用 redis 组件。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as redis from '@midwayjs/redis';\nimport * as casbin from '@midwayjs/casbin';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    redis,\n    casbin,\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n配置 redis 连接和 casbin 适配器。\n\n```typescript\nimport { MidwayAppInfo } from '@midwayjs/core';\nimport { join } from 'path';\nimport { createAdapter } from '@midwayjs/casbin-redis-adapter';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // ...\n    redis: {\n      clients: {\n        // 为 casbin 定义了一个连接\n        'node-casbin-official': {\n          host: '127.0.0.1',\n          port: 6379,\n          password: '',\n          db: '0',\n        }\n      }\n    },\n    casbin: {\n      policyAdapter: createAdapter({\n        // 配置了上面的连接名\n        clientName: 'node-casbin-official'\n      }),\n      // ...\n    },\n  };\n}\n\n```\n\n\n\n### TypeORM Adapter\n\n需要依赖 `@midwayjs/casbin-typeorm-adapter` 包和 typeorm 组件。\n\n```\n$ npm i @midwayjs/casbin-typeorm-adapter @midwayjs/typeorm typeorm@^1.0.0 better-sqlite3 --save\n```\n\n启用 typeorm 组件。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as typeorm from '@midwayjs/typeorm';\nimport * as casbin from '@midwayjs/casbin';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    typeorm,\n    casbin,\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n配置适配器，下面以 better-sqlite3 存储为例，mysql 的配置可以查看 typeorm 组件。\n\n```typescript\nimport { MidwayAppInfo } from '@midwayjs/core';\nimport { join } from 'path';\nimport { CasbinRule, createAdapter } from '@midwayjs/casbin-typeorm-adapter';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // ...\n    typeorm: {\n      dataSource: {\n        // 为 casbin 定义了一个连接\n        'node-casbin-official': {\n          type: 'better-sqlite3',\n          synchronize: true,\n          database: join(appInfo.appDir, 'casbin.sqlite'),\n          // 注意这里显式引入了 Entity\n          entities: [CasbinRule],\n        }\n      }\n    },\n    casbin: {\n      policyAdapter: createAdapter({\n        // 配置了上面的连接名\n        dataSourceName: 'node-casbin-official'\n      }),\n      // ...\n    }\n  };\n}\n```\n\n\n\n## 监视器\n\n使用分布式消息系统，例如 [etcd](https://github.com/coreos/etcd) 来保持多个Casbin执行器实例之间的一致性。 因此，我们的用户可以同时使用多个Casbin 执行器来处理大量的权限检查请求。\n\nMidway 当前只提供一种 Redis 更新策略，如有其他需求，可以给我们提交 issue。\n\n### Redis Watcher\n\n需要依赖 `@midwayjs/casbin-redis-adapter` 包和 redis 组件。\n\n```bash\n$ npm i @midwayjs/casbin-redis-adapter @midwayjs/redis --save\n```\n\n启用 redis 组件。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as redis from '@midwayjs/redis';\nimport * as casbin from '@midwayjs/casbin';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    redis,\n    casbin,\n  ],\n  // ...\n})\nexport class MainConfiguration {\n}\n```\n\n使用示例：\n\n```typescript\nimport { MidwayAppInfo } from '@midwayjs/core';\nimport { join } from 'path';\nimport { createAdapter, createWatcher } from '@midwayjs/casbin-redis-adapter';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // ...\n    redis: {\n      clients: {\n        'node-casbin-official': {\n          host: '127.0.0.1',\n          port: 6379,\n          db: '0',\n        },\n        'node-casbin-sub': {\n          host: '127.0.0.1',\n          port: 6379,\n          db: '0',\n        }\n      }\n    },\n    casbin: {\n      // ...\n      policyAdapter: createAdapter({\n        clientName: 'node-casbin-official'\n      }),\n      policyWatcher: createWatcher({\n        pubClientName: 'node-casbin-official',\n        subClientName: 'node-casbin-sub',\n      })\n    },\n  };\n}\n```\n\n注意，pub/sub 连接需要不同的客户端，上面代码定义了两个客户端。\n\npub 客户端可以和普通 Redis 客户端连接复用，而 sub 需要一个独立的客户端。\n","site/docs/extensions/cfork.md":"# cfork\n\n很多同学没有听过 cfork，cfork 库是 egg-scripts 中用于启动主进程的库，是 egg 使用的基础库之一，他的功能是启动进程，并维持多个进程的保活。\n\n\n文档在此：[https://github.com/node-modules/cfork](https://github.com/node-modules/cfork)\n\n\n由于 bootstrap.js 的特性，有时候不是很适合 pm2 来部署（比如集团内部，全局不安装，需要 API 启动）。\n\n\n我们可以新增一个 `server.js` 用来做主进程的入口，将 `bootstrap.js` 作为每个子进程的启动入口。\n\n\n```javascript\n// server.js\n\n'use strict';\n\nconst cfork = require('cfork');\nconst util = require('util');\nconst path = require('path');\nconst os = require('os');\n\n// 获取 cpu 核数\nconst cpuNumbers = os.cpus().length;\n\ncfork({\n  exec: path.join(__dirname, './bootstrap.js'),\n  count: cpuNumbers,\n})\n  .on('fork', (worker) => {\n    console.warn('[%s] [worker:%d] new worker start', Date(), worker.process.pid);\n  })\n  .on('disconnect', (worker) => {\n    console.warn(\n      '[%s] [master:%s] worker:%s disconnect, exitedAfterDisconnect: %s, state: %s.',\n      Date(),\n      process.pid,\n      worker.process.pid,\n      worker.exitedAfterDisconnect,\n      worker.state\n    );\n  })\n  .on('exit', (worker, code, signal) => {\n    const exitCode = worker.process.exitCode;\n    const err = new Error(\n      util.format(\n        'worker %s died (code: %s, signal: %s, exitedAfterDisconnect: %s, state: %s)',\n        worker.process.pid,\n        exitCode,\n        signal,\n        worker.exitedAfterDisconnect,\n        worker.state\n      )\n    );\n    err.name = 'WorkerDiedError';\n    console.error('[%s] [master:%s] worker exit: %s', Date(), process.pid, err.stack);\n  });\n```\n\n最后启动 `node server.js` 即可。\n","site/docs/extensions/code_dye.md":"# 代码染色\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的代码染色组件。\n\n用于在 HTTP 场景展示调用链路耗时与各个方法的出入参，帮你更快地定位代码问题。\n\n比如：\n\n+ 代码执行缓慢\n  - 不知道是哪一个方法执行的慢：通过代码染色后，可以查看每一个 `方法的执行时长`。\n+ 代码执行错误\n  - 可能是方法没有调到：通过代码染色后，可以查看每一个 `方法的调用链`。\n  - 可能是方法调用参数出错：通过代码染色后，查看每一个`方法的入参和返回值`\n\n使用效果：\n\n![](https://gw.alicdn.com/imgextra/i1/O1CN017Zd6y628M2PvqJO7I_!!6000000007917-2-tps-2392-844.png)\n\n\n\n\n相关信息：\n\n| web 支持情况      |      |\n| ----------------- | ---- |\n| @midwayjs/koa     | ✅    |\n| @midwayjs/faas    | ✅    |\n| @midwayjs/web     | ✅    |\n| @midwayjs/express | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/code-dye@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/code-dye\": \"^4.0.0\"\n    // ...\n  },\n}\n```\n\n\n\n## 启用组件\n\n将 code-dye 组件配置到代码中。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as codeDye from '@midwayjs/code-dye';\n\n@Configuration({\n  imports: [\n    // ...\n    {\n      component: codeDye,\n      enabledEnvironment: ['local'],\t// 只在本地启用\n    }\n  ],\n})\nexport class MainConfiguration {}\n```\n\n\n\n\n\n:::tip\n\n- 可以在 `本地` 或 `研发` 环境开启本组件，便于开发时定位问题，但是 `不建议` 在线上启用，会对线上访问性能产生影响。\n\n:::\n\n\n\n## 配置染色\n\n可以通过`matchQueryKey` 配置，控制当 `query` 参数包含`matchQueryKey` 配置对应的值的时候，进入染色链路，例如，配置为：\n\n```typescript\n// src/config/config.local.ts\nexport default {\n  codeDye: {\n    matchQueryKey: 'codeDyeABC',\n  }\n}\n```\n当请求接口 `http://127.0.0.1:7001/test?codeDyeABC=html` 时，就会判断 `query` 中是否存在 `codeDyeABC` 参数来决定是否染色，并根据参数对应的值，来响应不同的染色结果。\n\n也可以通过`matchHeaderKey` 配置，控制当 `headers` 参数包含 `matchHeaderKey` 配置对应的值的时候，进入染色链路，例如，配置为：\n\n```typescript\n// src/config/config.local.ts\nexport default {\n  codeDye: {\n    matchHeaderKey: 'codeDyeHeader',\n  }\n}\n```\n当请求接口 `http://127.0.0.1:7001/test` 时，就会判断请求的 `headers` 中是否存在 `codeDyeHeader` 参数来决定是否染色，并根据参数对应的值，来响应不同的染色结果。\n\n\n\n## 染色报告\n\n开启了代码染色后，链路染色的结果，可以通过开启染色的不同参数值来配置，目前支持以下三种：\n\n+ `html`：`对` 当前请求的结果进行处理，将染色信息添加到结果中，响应为 `html`，可以在浏览器上查看，效果可以查看此文档上面的图片效果展示。\n+ `json`：`对` 当前请求的结果进行处理，将染色信息添加到结果中，响应为 `json` 结构化信息。\n+ `log`：`不对` 当前请求的结果进行处理，染色的信息将会输出到日志中，不影响请求。\n\n例如，配置为：\n\n```typescript\n// src/config/config.local.ts\nexport default {\n  codeDye: {\n    matchQueryKey: 'codeDyeXXX',\n  }\n}\n```\n\n当请求接口 `http://127.0.0.1:7001/test?codeDyeXXX=html` 时，就会判断 `query` 中 `codeDyeXXX` 参数的值为 `html`，就将染色结果输出在当前请求的响应中，并且内容为 `html` 格式。\n","site/docs/extensions/commander.md":"# 命令行\n\n`@midwayjs/commander` 是一个基于 Midway IoC 容器的命令行组件，底层使用 [commander.js](https://github.com/tj/commander.js#readme) 做参数解析与 help 输出。你可以用 Midway 熟悉的依赖注入方式组织命令、选项解析与业务逻辑，并将命令拆分为多个 Class。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ❌    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ❌   |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n## 安装依赖\n\n在现有项目中安装 commander 组件依赖。\n\n```bash\n$ npm i @midwayjs/commander@4  --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/commander\": \"^4.0.0\"\n  }\n}\n```\n\n## 开启组件\n\n在入口配置中引入组件。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as commander from '@midwayjs/commander';\n\n@Configuration({\n  imports: [commander],\n})\nexport class MainConfiguration {}\n```\n\n\n## 编写命令\n\n目录结构示例：\n\n```bash\n.\n├── src\n│   ├── commands\n│   │   ├── hello.command.ts\n│   │   └── status.command.ts\n│   ├── configuration.ts\n│   └── ...\n├── bootstrap.js\n└── package.json\n```\n\n一个命令对应一个 Class，使用 `@Command()` 修饰，并实现 `CommandRunner` 接口的 `run()` 方法。\n\n\n```typescript\n// src/commands/hello.command.ts\nimport { Inject, ILogger } from '@midwayjs/core';\nimport { Command, CommandRunner, Option } from '@midwayjs/commander';\n\n@Command({\n  name: 'hello',\n  description: 'hello command',\n  arguments: '<name>',\n  aliases: ['hi'],\n})\nexport class HelloCommand implements CommandRunner {\n  @Inject()\n  logger: ILogger;\n\n  @Option({\n    flags: '-f, --foo [foo]',\n    description: 'foo option',\n    defaultValue: 'bar',\n  })\n  parseFoo(val: string) {\n    return `${val}_parsed`;\n  }\n\n  async run(passedParams: string[], options?: Record<string, any>) {\n    const [name] = passedParams;\n    this.logger?.info?.(`hello ${name}`, options);\n  }\n}\n```\n\n### `@Command()` 参数\n\n - `name`：命令名称（命令行调用与 `--help` 展示时使用）\n- `arguments`：命令位置参数声明（例如 `<name>`、`<a> [b]`）\n- `description`：命令描述，会展示在 `--help` 中\n- `argsDescription`：位置参数描述对象，会展示在 `--help` 中\n- `aliases`：命令别名数组\n\n### `@Option()` 参数\n\n- `flags`：选项声明（例如 `-f, --foo [foo]`、`-n, --num <num>`）\n- `description`：选项描述，会展示在 `--help` 中\n- `defaultValue`：默认值（不传该选项时生效）\n- `required`：是否必填（等价于 commander 的 `requiredOption`）\n\n`@Option()` 修饰的方法会作为 commander 的自定义 parser，用于把字符串参数转换为业务需要的类型（例如 number/boolean/自定义格式）。\n\n## 运行命令\n\n该组件作为 Midway Framework 运行，在应用启动时会解析 `process.argv` 并执行匹配的命令。\n\n如果你使用 `bootstrap.js` 作为入口，可以这样启动：\n\n```javascript\n// bootstrap.js\nconst { Bootstrap } = require('@midwayjs/bootstrap');\nBootstrap.run();\n```\n\n然后运行：\n\n```bash\n$ node bootstrap.js hello world --foo baz\n$ node bootstrap.js hi world\n```\n\n## 上下文\n\n每次命令执行都会创建请求上下文（request context），可以在命令类中注入 `Context` 获取运行时信息：\n\n```typescript\nimport { Command, CommandRunner, Context } from '@midwayjs/commander';\nimport { Inject } from '@midwayjs/core';\n\n@Command({ name: 'info', arguments: '<name>' })\nexport class InfoCommand implements CommandRunner {\n  @Inject()\n  ctx: Context;\n\n  async run() {\n    this.ctx.commandName; // 命令名\n    this.ctx.args; // 位置参数数组\n    this.ctx.options; // commander 解析后的选项\n    this.ctx.command; // commander 的 Command 实例\n  }\n}\n```\n\n## 交互式提问（Enquirer）\n\n组件内置了基于 [enquirer](https://github.com/enquirer/enquirer) 的交互式提问能力，适用于运行过程中补齐参数。你可以用 `@QuestionSet()` 组织问题集合，并在命令里通过 `EnquirerService` 触发。\n\n如果希望既支持命令行传参又支持交互补齐，请把对应选项设为可选（否则 commander 会在缺参时直接报错）。\n\n```typescript\nimport {\n  Command,\n  CommandRunner,\n  QuestionSet,\n  Question,\n  ValidateFor,\n  DefaultFor,\n  WhenFor,\n  EnquirerService,\n} from '@midwayjs/commander';\nimport { Inject } from '@midwayjs/core';\n\n@QuestionSet()\nclass ProfileQuestionSet {\n  @Question({ type: 'input', name: 'age', message: 'Your age?' })\n  parseAge(value: string) {\n    return Number.parseInt(value, 10);\n  }\n\n  @Question({ type: 'input', name: 'nickname', message: 'Nickname?' })\n  parseNickname(value: string) {\n    return value;\n  }\n\n  @ValidateFor({ name: 'age' })\n  validateAge(value: string) {\n    return value ? true : 'age required';\n  }\n\n  @DefaultFor({ name: 'nickname' })\n  defaultNickname() {\n    return 'neo';\n  }\n\n  @WhenFor({ name: 'nickname' })\n  whenNickname(answers: Record<string, unknown>) {\n    return Boolean(answers.useNickname);\n  }\n}\n\n@Command({ name: 'ask' })\nexport class AskCommand implements CommandRunner {\n  @Inject()\n  enquirerService: EnquirerService;\n\n  async run(_passedParams: string[], options?: Record<string, any>) {\n    const answers = await this.enquirerService.prompt(ProfileQuestionSet, {\n      useNickname: options?.useNickname,\n    });\n    // use answers.age / answers.nickname\n  }\n}\n```\n\n说明：\n- `@Question()` 修饰的方法会作为 enquirer 的 `result`（用于转换用户输入）。\n- `@DefaultFor()` 会映射到 enquirer 的 `initial`。\n- `@WhenFor()` 支持根据已收集的答案决定是否提问。\n- 可用的 `@*For()` 装饰器：`ValidateFor`、`ChoicesFor`、`MessageFor`、`DefaultFor`、`WhenFor`。\n- `prompt()` 同时支持 `QuestionSet` 名称字符串或类引用，推荐使用类引用。\n\n## 错误处理\n\n默认情况下，CLI 入口会捕获异常并输出日志后退出进程。你可以在配置里提供 `errorHandler` 来接管错误处理：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  commander: {\n    errorHandler: (err: Error) => {\n      console.error(err);\n      process.exit(1);\n    },\n  },\n};\n```\n\n如果你在命令里使用了 `@Catch()` 错误过滤器（Midway Filter），会先走过滤器逻辑，再进入这里的兜底处理。\n\n## 返回值与输出\n\n默认情况下，命令执行完毕后，如果 `run()` 有返回值，框架会将返回值输出到标准输出（stdout），方便用作脚本管道或在测试中断言输出内容。\n\n支持的返回值类型：\n\n- `string` / `Buffer`：直接写入 stdout\n- 普通对象：使用 `JSON.stringify` 后写入 stdout\n- `Readable`：会 pipe 到 stdout\n- `AsyncIterable`：会按迭代顺序逐段写入 stdout\n\n下面是一个返回文本/JSON 的例子（使用 core 的 `ServerResponse` 语义来组织输出格式）：\n\n```typescript\nimport { Command, CommandRunner, CliServerResponse } from '@midwayjs/commander';\n\n@Command({ name: 'status' })\nexport class StatusCommand implements CommandRunner {\n  async run() {\n    return new CliServerResponse({} as any).success().json({ ok: true });\n  }\n}\n```\n\n如果希望按 chunk 逐步输出，可以返回 `CliServerResponse().stream()`：\n\n```typescript\nimport { Command, CommandRunner, CliServerResponse } from '@midwayjs/commander';\n\n@Command({ name: 'stream' })\nexport class StreamCommand implements CommandRunner {\n  async run() {\n    const response = new CliServerResponse({} as any);\n    const stream = response.stream();\n\n    setImmediate(() => {\n      stream.send('a');\n      stream.send({ b: 2 });\n      stream.end();\n    });\n\n    return stream;\n  }\n}\n```\n\n## 日志\n\n组件默认会注册一个名为 `commanderLogger` 的 logger，默认写入 `midway-commander.log`。\n\n你可以在命令类里通过 `@Logger('commanderLogger')` 注入使用，例如：\n\n```typescript\nimport { Logger, ILogger } from '@midwayjs/core';\nimport { Command, CommandRunner } from '@midwayjs/commander';\n\n@Command({ name: 'hello', arguments: '<name>' })\nexport class HelloCommand implements CommandRunner {\n  @Logger('commanderLogger')\n  logger: ILogger;\n\n  async run(passedParams: string[]) {\n    this.logger.info('hello %s', passedParams[0]);\n  }\n}\n```\n\n如果希望自定义日志文件名或级别，可以在应用配置中覆盖 `midwayLogger.clients.commanderLogger`：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  midwayLogger: {\n    clients: {\n      commanderLogger: {\n        fileLogName: 'my-commander.log',\n        level: 'info',\n      },\n    },\n  },\n};\n```\n\n## 单元测试\n\n命令行参数在测试环境中很容易被 Jest/Node 参数污染，推荐通过 framework 的 `runCommand()` 来执行命令，而不是直接 mock `process.argv`。\n\n```typescript\nimport { createLightApp, close } from '@midwayjs/mock';\nimport { Framework } from '@midwayjs/commander';\nimport * as commander from '@midwayjs/commander';\n\ndescribe('commander', () => {\n  it('should run command', async () => {\n    const app = await createLightApp({\n      imports: [commander],\n      preloadModules: [HelloCommand],\n    });\n\n    const framework = app.getFramework() as Framework;\n    await framework.runCommand('hello', 'world', '--foo', 'bar');\n\n    await close(app);\n  });\n});\n```\n","site/docs/extensions/consul.md":"# Consul\n\nconsul 用于微服务下的服务治理，主要特点有：服务发现、服务配置、健康检查、键值存储、安全服务通信、多数据中心等。\n\n本文基于最新组件实现，介绍 Consul 客户端的配置与使用，以及统一抽象的服务发现能力。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n从 v4 开始 Consul 组件基于 Service Factory 进行了重构，API 有着非常大的变化，并提供了服务发现等抽象能力。\n\n感谢 [boostbob](https://github.com/boostbob) 提供的组件。\n\n\n效果如下图：\n![image.png](https://img.alicdn.com/imgextra/i3/O1CN01e5cFZx1I0draeZynr_!!6000000000831-2-tps-1500-471.png)\n\n![image.png](https://img.alicdn.com/imgextra/i2/O1CN01iLYF8r1HQ0B3b47Fh_!!6000000000751-2-tps-1500-895.png)\n\n\n## 安装组件\n\n首先安装 Consul 组件：\n\n```bash\n$ npm i @midwayjs/consul@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/consul\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 引入组件\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as consul from '@midwayjs/consul';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [consul],\n  importConfigs: [join(__dirname, 'config')]\n})\nexport class MainConfiguration {}\n```\n\n\n\n## 配置客户端\n\n支持单客户端与多客户端，遵循 ServiceFactory 配置结构。\n\n**单客户端**\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  consul: {\n    client: {\n      host: '127.0.0.1',\n      port: '8500'\n    }\n  }\n}\n```\n\n**多客户端**\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  consul: {\n    clients: {\n      a: { host: '127.0.0.1', port: '8500' },\n      b: { host: '127.0.0.1', port: '8501' }\n    },\n    defaultClientName: 'a'\n  }\n}\n```\n\n**使用默认实例**\n\n`ConsulService` 作为默认实例代理，直接暴露原生 Consul 客户端方法。\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { ConsulService } from '@midwayjs/consul';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  consul: ConsulService;\n\n  async register() {\n     await this.consul.kv.set('name', 'juhai');\n  }\n}\n```\n\n\n\n**获取指定实例**\n\n通过 `ConsulServiceFactory` 获取指定命名的客户端实例。\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { ConsulServiceFactory } from '@midwayjs/consul';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  consulFactory: ConsulServiceFactory;\n\n  async invoke() {\n    const consulA = this.consulFactory.get('a');\n    const consulB = this.consulFactory.get('b');\n    await consulA.agent.service.register({ name: 'svc-a', id: 'svc-a-1' });\n    await consulB.agent.service.register({ name: 'svc-b', id: 'svc-b-1' });\n  }\n}\n```\n\n\n\n\n\n## 服务发现\n\n从 v4 开始提供基于统一抽象的服务发现能力，Consul 将成为服务发现的客户端来使用。\n\n服务发现包括 **注册服务** 和 **获取服务** 两部分。\n\n\n\n### 配置服务发现\n\n你需要先配置一个客户端，如果配置了多个，可以使用 `serviceDiscoveryClient` 来指定。\n\n通过 `serviceDiscovery` 来配置服务发现。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  consul: {\n    clients: {\n      default: { host: '127.0.0.1', port: '8500' },\n    },\n    serviceDiscovery: {\n      // ...\n    }\n  }\n}\n```\n\n\n\n### 注册服务发现\n\n当服务启动后，你可以将当前的服务注册到 Consul 注册中心。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport { ConsulServiceDiscovery } from '@midwayjs/consul';\nimport { LoadBalancerType } from '@midwayjs/core';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  @Inject()\n  consulDiscovery: ConsulServiceDiscovery;\n\n  async onServerReady() {\n    // 创建一个服务发现客户端\n    const client = this.consulDiscovery.createClient();\n\n    // 注册当前服务\n    await client.register({\n      id: client.defaultMeta.id,\n      name: 'order',\n      tags: ['test'],\n      address: client.defaultMeta.host,\n      port: 7001,\n      meta: {\n        version: '1.0.0'\n      },\n      check: {\n        name: 'TTL Health Check',\n        timeout: '30s',\n        ttl: '10s'\n      }\n    });\n  }\n}\n```\n\n`register` 方法需要传递一些基础的信息。\n\n最重要的有 id，name，以及 address。\n\n```typescript\ninterface RegisterOptions extends CommonOptions {\n  name: string;\n  id?: string;\n  tags?: string[];\n  address?: string;\n  taggedaddresses?: Record<string, any>;\n  meta?: Record<string, string>;\n  namespace?: string;\n  port?: number;\n  kind?: string;\n  proxy?: any;\n  connect?: RegisterConnect;\n  check?: CheckOptions;\n  checks?: CheckOptions[];\n}\n```\n\n`client.defaultMeta` 提供了一些默认生成的数据，包括 id，serviceName, host。\n\n\n\n### 获取可用服务\n\n你可以在任意需要的地方，动态获取到一个服务来调用。\n\n````typescript\n@Provide()\nexport class OrderService {\n  \n  @Inject()\n  consulDiscovery: ConsulServiceDiscovery;\n  \n  async getSerivce() {\n  \tconst instance = await this.consulDiscovery.getInstance({ service: 'order'});\n    // instance.name\n  }\n}\n````\n\n返回的 instance 类型为 `ConsulHealthItem`，结构如下。\n\n```typescript\nexport interface ConsulHealthItem {\n  Node: {\n    ID: string;\n    Node: string;\n    Address: string;\n    Datacenter: string;\n    TaggedAddresses: Record<string, any>;\n    Meta: Record<string, any>;\n    CreateIndex: number;\n    ModifyIndex: number;\n  };\n  Service: {\n    ID: string;\n    Service: string;\n    Tags: string[];\n    Address: string;\n    TaggedAddresses: Record<string, any>;\n    Meta: Record<string, any>;\n    Port: number;\n    Weights: Record<string, any>;\n    EnableTagOverride: boolean;\n    Proxy: Record<string, any>;\n    Connect: Record<string, any>;\n    PeerName: string;\n    CreateIndex: number;\n    ModifyIndex: number;\n  };\n  Checks: Array<{\n    Node: string;\n    CheckID: string;\n    Name: string;\n    Status: 'passing' | 'warning' | 'critical';\n    Notes: string;\n    Output: string;\n    ServiceID: string;\n    ServiceName: string;\n    ServiceTags: string[];\n    Type: string;\n    Interval: string;\n    Timeout: string;\n    ExposedPort: number;\n    Definition: Record<string, any>;\n    CreateIndex: number;\n    ModifyIndex: number;\n  }>;\n}\n\n```\n\n\n\n## 配置中心\n\n\n同时 consul 也能作为一个服务配置的地方，如下代码：\n\n```typescript\nimport { Controller, Get, Inject } from '@midwayjs/core';\nimport { ConsulService } from '@midwayjs/consul';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  consul: ConsulService;\n\n  @Get('/')\n  async home() {\n    await this.consul.kv.set('name', 'juhai');\n    const res = await this.consul.kv.get('name');\n    return res;\n  }\n}\n\n```\n\n我们调用 `kv.set` 方法，我们可以设置对应的配置，通过 `kv.get`  方法可以拿到对应的配置。\n\n\n注意：在代码中，有同学出现，在每次请求中去 get 对应的配置，这时你的 QPS 多少对 Consul server 的压力。\n\n\n所以在QPS比较大的情况，可以如下处理：\n\n```typescript\nimport { Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';\nimport { ConsulService } from '@midwayjs/consul';\n\n@Provide()\n@Scope(ScopeEnum.Singleton)\nexport class ConfigService {\n\n  @Inject()\n  consul: ConsulService;\n\n  config: any;\n\n  @Init()\n  async init() {\n    setInterval(async () => {\n      this.config = await this.consul.kv.get('name');\n    }, 5000);\n    this.config = await this.consul.kv.get('name');\n  }\n\n  async getConfig(){\n    return this.config;\n  }\n}\n\n```\n\n上面的代码，相当于定时去获取对应的配置，当每个请求进来的时候，获取 Scope 为 ScopeEnum.Singleton 服务的 `getConfig` 方法，这样每 5s 一次获取请求，就减少了对服务的压力。\n\nConsul 界面上如下图：\n\n![image.png](https://img.alicdn.com/imgextra/i4/O1CN01V3P6uK1rIVs19JiWn_!!6000000005608-2-tps-1500-374.png)\n\n![image.png](https://img.alicdn.com/imgextra/i2/O1CN014O2GyH1sMvIhmlbs4_!!6000000005753-2-tps-1500-667.png)\n\n\n一共提供如下几种方法：\n\n- [get](https://www.npmjs.com/package/consul#kv-get)，获取对应key的value\n- [keys](https://www.npmjs.com/package/consul#kv-keys)，查询某个prefix的key的列表\n- [set](https://www.npmjs.com/package/consul#kv-set)，设置对应的key的值\n- [del](https://www.npmjs.com/package/consul#kv-del)，删除对应的key\n\n\n","site/docs/extensions/cos.md":"# 腾讯云对象存储（COS）\n\n本文介绍了如何使用 midway 接入腾讯云 COS。\n\n相关信息：\n\n| 描述              |     |\n| ----------------- | --- |\n| 可用于标准项目    | ✅   |\n| 可用于 Serverless | ✅   |\n| 可用于一体化      | ✅   |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/cos@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/cos\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 引入组件\n\n\n首先，引入 组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as cos from '@midwayjs/cos';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    cos\t\t// 导入 cos 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n\n## 配置\n\n比如：\n\n\n**单客户端配置**\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  cos: {\n    client: {\n      SecretId: '***********',\n      SecretKey: '***********',\n    },\n  },\n}\n```\n\n\n**多个客户端配置，需要配置多个**\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  cos: {\n    clients: {\n      instance1: {\n        SecretId: '***********',\n        SecretKey: '***********',\n      },\n      instance2: {\n        SecretId: '***********',\n        SecretKey: '***********',\n      },\n    },\n  },\n}\n```\n更多参数可以查看 [cos-nodejs-sdk-v5](https://github.com/tencentyun/cos-nodejs-sdk-v5) 文档。\n\n\n## 使用 COS 服务\n\n\n我们可以在任意的代码中注入使用。\n```typescript\nimport { Provide, Controller, Inject, Get } from '@midwayjs/core';\nimport { COSService } from '@midwayjs/cos';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  cosService: COSService;\n\n  async invoke() {\n    await this.cosService.sliceUploadFile({\n      Bucket: 'test-1250000000',\n      Region: 'ap-guangzhou',\n      Key: '1.zip',\n      FilePath: './1.zip'\n    },\n  }\n}\n```\n\n\n可以使用 `COSServiceFactory` 获取不同的实例。\n```typescript\nimport { COSServiceFactory } from '@midwayjs/cos';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  cosServiceFactory: COSServiceFactory;\n\n  async save() {\n    const cos1 = await this.cosServiceFactory.get('instance1');\n    const cos2 = await this.cosServiceFactory.get('instance3');\n\n    //...\n\n  }\n}\n```\n\n","site/docs/extensions/cron.md":"# 本地任务\n\n和 bull 组件不同，cron 组件提供的是本地任务能力，即在每台机器的每个进程都会执行。如需不同机器或者不同进程之间只执行一次任务，请使用 [bull 组件](./bull) 。\n\n\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n\n\n## 安装组件\n\n```bash\n$ npm i @midwayjs/cron@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/cron\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 使用组件\n\n将组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as cron from '@midwayjs/cron';\n\n@Configuration({\n  imports: [\n    // ...\n    cron\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n\n\n## 编写任务处理类\n\n使用 `@Job` 装饰器装饰一个类，用于快速定义一个任务处理器。\n\n比如，在 `src/job` 目录中创建一个 `sync.job.ts`，用于某些数据同步任务，代码如下：\n\n```typescript\n// src/job/sync.job.ts\nimport { Job, IJob } from '@midwayjs/cron';\nimport { FORMAT } from '@midwayjs/core';\n\n@Job({\n  cronTime: FORMAT.CRONTAB.EVERY_PER_30_MINUTE,\n  start: true,\n})\nexport class DataSyncCheckerJob implements IJob {\n  async onTick() {\n    // ...\n  }\n}\n```\n\n`@Job` 装饰器用于修饰一个任务类，在初始化时，框架会自动将其转变为一个任务。\n\n任务类需要实现 `IJob` 接口，实现 `onTick` 方法，每当任务触发时，会自动调用 `onTick` 方法。\n\n此外，还有一个可选的 `onComplete` 方法，用于在 `onTick` 完成后执行。\n\n```typescript\n@Job({\n  cronTime: FORMAT.CRONTAB.EVERY_PER_30_MINUTE,\n  start: true,\n})\nexport class DataSyncCheckerJob implements IJob {\n  async onTick() {\n    // ...\n  }\n  \n  async onComplete() {\n    // 记录一些数据等等，用处不是很大\n  }\n}\n```\n\n\n\n`@Job` 装饰器的常用参数如下：\n\n| 参数      | 类型    | 描述                   |\n| --------- | ------- | ---------------------- |\n| cronTime  | string  | crontab 表达式         |\n| start     | boolean | 是否自动启动任务       |\n| runOnInit | boolean | 是否在初始化就执行一次 |\n\n更多参数，请参考 [Cron](https://github.com/kelektiv/node-cron)。\n\n\n\n## 任务管理\n\n除了定时执行任务，我们还通过框架提供的 API，对任务进行手动管理。\n\n比如，下面的代码仅仅定义了一个任务，但是不会启动执行。\n\n```typescript\n@Job('syncJob', {\n  cronTime: '*/2 * * * * *',\t// 每隔 2s 执行\n})\nexport class DataSyncCheckerJob implements IJob {\n  async onTick() {\n    // ...\n  }\n}\n```\n\n我们定义了一个名为 `syncJob` 的任务，并且给它了一个默认的调度时间。\n\n\n\n### 获取任务对象\n\n我们可以通过两种方式获取任务对象。\n\n通过`@InjectJob` 用来注入某个任务，参数为类本身或者任务名。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as cron from '@midwayjs/cron';\nimport { InjectJob, CronJob } from '@midwayjs/cron';\nimport { DataSyncCheckerJob } from './job/sync.job';\n\n@Configuration({\n  imports: [\n    cron\n  ],\n})\nexport class ContainerConfiguration {\n  @InjectJob(DataSyncCheckerJob)\n  syncJob: CronJob;\n  \n  @InjectJob('syncJob')\n  syncJob2: CronJob;\n\n  async onServerReady() {\n    // this.syncJob  === this.syncJob2\n  }\n}\n\n```\n\n通过 Framework API 获取。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as cron from '@midwayjs/cron';\nimport { InjectJob, CronJob } from '@midwayjs/cron';\nimport { DataSyncCheckerJob } from './job/sync.job';\n\n@Configuration({\n  imports: [\n    cron\n  ],\n})\nexport class ContainerConfiguration {\n  @Inject()\n  cronFramework: cron.Framework;\n\n  async onServerReady() {\n    const syncJob = this.cronFramework.getJob(DataSyncCheckerJob);\n    const syncJob2 = this.cronFramework.getJob('syncJob');\n    \n    // syncJob  === syncJob2\n  }\n}\n\n```\n\n:::caution\n\n注意，任务对象都必须在 `onServerReady` 生命周期或者启动之后才能获取。\n\n:::\n\n\n\n### 启停任务\n\n我们可以在初始化或者某些程序执行完成之后，将这个任务启动。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as cron from '@midwayjs/cron';\nimport { InjectJob, CronJob } from '@midwayjs/cron';\nimport { DataSyncCheckerJob } from './job/sync.job';\n\n@Configuration({\n  imports: [\n    cron\n  ],\n})\nexport class ContainerConfiguration {\n  @InjectJob(DataSyncCheckerJob)\n  syncJob: CronJob;\n\n  async onServerReady() {\n    this.syncJob.start();\n    \n    // ...\n    this.syncJob.stop();\n  }\n}\n\n```\n\n\n\n## 上下文\n\n任务执行是在请求作用域中，其有着特殊的 Context 对象结构。\n\n```typescript\nexport interface Context extends IMidwayContext {\n  job: CronJob;\n}\n```\n\n这里的  `CronJob` 类型来自于 [Cron](https://github.com/kelektiv/node-cron) 包。\n\n\n\n## 组件日志\n\n组件有着自己的日志，默认会将 `ctx.logger` 记录在 `midway-cron.log` 中。\n\n我们可以单独配置这个 logger 对象。\n\n```typescript\nexport default {\n  midwayLogger: {\n    // ...\n    clients: {\n      // ...\n      cronLogger: {\n        fileLogName: 'midway-cron.log',\n      },\n    }\n  }\n}\n```\n\n\n\n## 全局配置\n\n可以针对 Job 进行一些全局配置，会和每个 Job 的配置进行合并。\n\n```typescript\nexport default {\n  cron: {\n    defaultCronJobOptions: {\n      // ...\n    }\n  }\n}\n```\n\n这里的 `defaultCronJobOptions` 配置项请参考 [CronJobParameters](https://github.com/kelektiv/node-cron/blob/main/lib/job.js#L51)\n","site/docs/extensions/cross_domain.md":"# 跨域\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的通用跨域组件，支持 `cors` 、`jsonp` 多种模式。\n\n相关信息：\n\n| web 支持情况      |      |\n| ----------------- | ---- |\n| @midwayjs/koa     | ✅    |\n| @midwayjs/faas    | ✅    |\n| @midwayjs/web     | ✅    |\n| @midwayjs/express | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/cross-domain --save\n```\n\n## 引入组件\n\n在 `src/configuration.ts` 中引入组件。\n\n```typescript\nimport * as crossDomain from '@midwayjs/cross-domain';\n@Configuration({\n  imports: [\n    // ...other components\n    crossDomain\n  ],\n})\nexport class MainConfiguration {}\n```\n\n\n\n## 什么是跨域\n\n假设有两个网站：\n\n- **A.com**：这是你的网站，你想要从这里访问一些资源。\n- **B.com**：这是另一个网站，它拥有你想要访问的资源。\n\n### 场景设定\n\n1. **A.com** 是你的主网站，你在这里运行一些 JavaScript 代码。\n2. **B.com** 拥有一些 API 接口，你想要通过 A.com 的 JavaScript 代码来调用这些 API 接口。\n\n由于同源策略，浏览器默认不允许 A.com 的 JavaScript 代码直接访问 B.com 的资源。这是因为浏览器出于安全考虑，防止恶意网站读取其他网站的敏感数据。你想要在 A.com 上运行的 JavaScript 代码中发起一个请求到 B.com 的 API 接口，这个请求会被视为跨域请求。\n\n简单来说，从 A.com 访问 B.com 的接口，就是跨域。\n\n此外，跨域还有一些条件：\n\n* 1、同源策略是浏览器安全机制的一部分，所以一般只有浏览器访问才有跨域问题\n* 2、当浏览器发起跨域请求时，它会根据当前源自动添加一个 `Origin` 头部到请求中，服务端也会根据 `origin` 头判断来源，进而做处理\n\n所以，当你发现跨域设置不生效时，请检查：\n\n* 1、是否真的是跨域请求\n* 2、是否真的从浏览器发起\n* 3、是否请求带有 `origin` 头\n\n\n\n## 什么是 CORS\n\n在前端代码调用后端服务中经常会碰到下面的错误，这就最为常见的跨域 [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) 错误。\n\n```\nAccess to fetch at 'http://127.0.0.1:7002/' from origin 'http://127.0.0.1:7001' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.\n```\n\n出于安全性，浏览器限制脚本内发起的跨源 HTTP 请求。例如，`XMLHttpRequest` 和 [Fetch API](https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API) 遵循[同源策略](https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy)。这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源，除非响应报文包含了正确 CORS 响应头。\n\nCORS 机制允许 Web 应用服务器进行跨源访问控制，从而使跨源数据传输得以安全进行。现代浏览器支持在 API 容器中（例如 [`XMLHttpRequest`](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest) 或 [Fetch](https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API)）使用 CORS，以降低跨源 HTTP 请求所带来的风险。\n\n\n\n## 常见的 CORS 配置\n\n下面列举几种跨域的解决方案，我们以 `fetch` 方法为例。\n\n### 未使用 `credentials` \n\n客户端。\n\n```javascript\nfetch(url);\n```\n\n服务端配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  cors: {\n    origin: '*',\n  },\n}\n```\n\n### 使用 `credentials`\n\n客户端。\n\n```javascript\nfetch(url, {\n  credentials: \"include\",\n});\n```\n\n服务端配置\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  cors: {\n    credentials: true,\n  },\n}\n```\n\n### 限制 `origin` 来源\n\n假如我们的网页地址为 `http://127.0.0.1:7001` 而接口为 `http://127.0.0.1:7002`。\n\n客户端。\n\n```javascript\nfetch('http://127.0.0.1:7002/', {\n  credentials: 'include'\n})\n```\n\n服务端配置，注意，由于启用了 `credentials`，这个时候 `origin` 字段不能为 `*`。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  cors: {\n    origin: 'http://127.0.0.1:7001',\n    credentials: true,\n  },\n}\n```\n\n\n\n## 更多 CORS 配置\n\n完整可用配置如下：\n\n```typescript\nexport const cors = {\n  // 允许跨域的方法，【默认值】为 GET,HEAD,PUT,POST,DELETE,PATCH\n  allowMethods: string |string[];\n  // 设置 Access-Control-Allow-Origin 的值，【默认值】会获取请求头上的 origin\n  // 也可以配置为一个回调方法，传入的参数为 request，需要返回 origin 值\n  // 例如：http://test.midwayjs.org\n  // 如果设置了 credentials，则 origin 不能设置为 *\n  origin: string|Function;\n  // 设置 Access-Control-Allow-Headers 的值，【默认值】会获取请求头上的 Access-Control-Request-Headers\n  allowHeaders: string |string[];\n  // 设置 Access-Control-Expose-Headers 的值\n  exposeHeaders: string |string[];\n  // 设置 Access-Control-Allow-Credentials，【默认值】false\n   // 也可以配置为一个回调方法，传入的参数为 request，返回值为 true 或 false\n  credentials: boolean|Function;\n  // 是否在执行报错的时候，把跨域的 header 信息写入到 error 对的 headers 属性中，【默认值】false\n  keepHeadersOnError: boolean;\n  // 设置 Access-Control-Max-Age\n  maxAge: number;\n}\n```\n\n\n## JSONP 配置\n\n可以在 `src/config/config.default` 中进行 JSONP 配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  jsonp: {\n    callback: 'jsonp',\n    limit: 512,\n  },\n}\n```\n","site/docs/extensions/crud.md":"# CRUD\n\n本文档介绍如何在 Midway 中使用 `@midwayjs/crud`。\n\n`@midwayjs/crud` 不是一个“只能自动生成接口”的脚手架，它本质上提供的是一套**面向资源的标准 CRUD 能力**：\n\n- 统一的增删改查 service 抽象\n- 统一的分页 / 排序 / 过滤 / 搜索协议\n- 可选的 REST 路由快捷生成\n- 和现有 validation / swagger / web 路由体系的接入\n\n如果你经常在不同模块里重复写下面这些代码：\n\n- `findAndCount`\n- `save`\n- `update`\n- `delete`\n- 手动解析 `page`、`limit`、`sort`\n- 为每个资源写一套几乎一样的 Controller\n\n那这个组件就是用来把这些重复劳动收起来的。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n## 这个组件能做什么\n\n先用一句话概括：\n\n**先提供一个可复用的 CRUD service，再按需把它暴露成 HTTP 接口。**\n\n这意味着它支持两种使用方式：\n\n1. 只把它当数据访问层能力来用  \n   适合你的业务逻辑比较复杂，不想让组件替你生成接口。\n\n2. 把它当接口快捷层来用  \n   适合资源型接口很多，希望快速拿到统一的 REST API。\n\n所以它解决的是“资源型接口的重复代码”问题，但**不会替代你的业务 service**。\n\n复杂业务依然应该写在你自己的 service 里，例如：\n\n- 下单时要检查库存、优惠券、支付状态\n- 创建用户时要同步多个系统\n- 删除资源前要做权限和状态机校验\n\n这些逻辑应该继续在你的业务 service 中完成，CRUD 组件只是提供一个稳定的资源操作基座。\n\n## 核心概念\n\n在开始之前，先理解这三个层次。\n\n### 1. `CrudService<T>`\n\n这是最核心的抽象，定义了统一的资源操作接口。\n\n```ts\nlist(query)\nfindOne(id)\ncreate(data)\nupdate(id, data)\ndelete(id)\n```\n\n你可以把它理解成“一个标准化的资源仓储服务接口”。\n\n### 2. 数据库适配层\n\nMidway CRUD 目前提供了 4 个官方适配基类：\n\n- `TypeOrmCrudService<T>`\n- `MikroCrudService<T>`\n- `SequelizeCrudService<T>`\n- `MongooseCrudService<T>`\n\n它们都实现了相同的 CRUD 核心接口，但底层分别接不同的数据访问组件。\n\n### 3. HTTP 暴露层\n\n这是可选的。\n\n如果你希望快速生成路由，可以使用：\n\n- 类式：`@Crud()`\n- 函数式：`defineCrudRoutes()`\n\n这两种方式都只是把同一个 `CrudService` 暴露成 HTTP 接口，不会生成第二套独立逻辑。\n\n## 什么时候适合用\n\n适合：\n\n- 大量资源型接口，结构相似\n- 列表查询都需要统一分页、排序、过滤\n- 想减少重复的 Repository / Model 调用代码\n- 想让不同模块的 API 风格保持一致\n\n不适合：\n\n- 主要是复杂工作流，不是标准资源接口\n- 一个接口需要跨多个聚合、多个事务、多个外部系统\n- 你希望所有查询语义都完全自由，不接受统一约束\n\n如果一个模块的核心不是“资源管理”，而是“复杂业务流程”，那更适合直接写普通 Controller + Service，而不是强行套 CRUD。\n\n## 安装依赖\n\n先安装 CRUD 组件本身：\n\n```bash\n$ npm i @midwayjs/crud@4 --save\n```\n\n然后根据你使用的数据库组件安装对应依赖。\n\n```bash\n# TypeORM\n$ npm i @midwayjs/typeorm@4 typeorm@^1.0.0 --save\n\n# MikroORM\n$ npm i @midwayjs/mikro@4 @mikro-orm/core@^6 --save\n\n# Sequelize\n$ npm i @midwayjs/sequelize@4 sequelize sequelize-typescript --save\n\n# Mongoose\n$ npm i @midwayjs/mongoose@4 mongoose --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/crud\": \"^4.0.0\",\n    \"@midwayjs/typeorm\": \"^4.0.0\",\n    \"@midwayjs/mikro\": \"^4.0.0\",\n    \"@midwayjs/sequelize\": \"^4.0.0\",\n    \"@midwayjs/mongoose\": \"^4.0.0\",\n    \"typeorm\": \"^1.0.0\",\n    \"@mikro-orm/core\": \"^6.4.5\",\n    \"sequelize\": \"^6.37.5\",\n    \"sequelize-typescript\": \"^2.1.6\",\n    \"mongoose\": \"^8.9.5\"\n  }\n}\n```\n\n## 启用组件\n\n在 `src/configuration.ts` 中引入组件。\n\n下面以 `koa + typeorm + crud` 为例：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as typeorm from '@midwayjs/typeorm';\nimport * as crud from '@midwayjs/crud';\n\n@Configuration({\n  imports: [\n    koa,\n    typeorm,\n    crud,\n  ],\n})\nexport class MainConfiguration {}\n```\n\n如果你使用其他数据库组件，只需要把对应组件加到 `imports` 中即可。\n\n## 入门：先从 service-only 开始\n\n对于新手来说，最容易理解的方式不是先生成路由，而是先把它当一个“可复用的数据服务”。\n\n这也是推荐的理解顺序。\n\n### 为什么推荐先学 service-only\n\n因为这样你能先理解：\n\n- 组件真正提供的核心是什么\n- 业务代码应该写在哪里\n- 路由层只是一个可选外壳\n\n如果一上来就只看 `@Crud()`，很容易误以为这只是一个自动生成接口的装饰器。\n\n### 最小 TypeORM 示例\n\n先定义一个资源 service：\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { TypeOrmCrudService } from '@midwayjs/crud/typeorm';\nimport { Repository } from 'typeorm';\n\nimport { UserEntity } from '../entity/user';\n\n@Provide()\nexport class UserCrudService extends TypeOrmCrudService<UserEntity> {\n  @InjectEntityModel(UserEntity)\n  repo: Repository<UserEntity>;\n}\n```\n\n然后在业务 service 里直接组合它：\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  userCrudService: UserCrudService;\n\n  async listUsers() {\n    return this.userCrudService.list({\n      page: 1,\n      limit: 20,\n      sort: [],\n      filters: [],\n    });\n  }\n\n  async createUser(input: CreateUserDTO) {\n    return this.userCrudService.create(input);\n  }\n}\n```\n\n这时候：\n\n- 你已经获得统一的 CRUD 能力\n- 但**不会自动生成任何 HTTP 路由**\n- 业务仍然由你自己的 `UserService` 负责组织\n\n这也是最适合复杂业务场景的用法。\n\n## 其他数据库适配\n\n如果你的项目不是 TypeORM，也可以使用其他官方适配。\n\n### MikroORM\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectRepository } from '@midwayjs/mikro';\nimport { MikroCrudService } from '@midwayjs/crud/mikro';\n\n@Provide()\nexport class UserCrudService extends MikroCrudService<UserEntity> {\n  @InjectRepository(UserEntity)\n  repo;\n}\n```\n\n### Sequelize\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectRepository } from '@midwayjs/sequelize';\nimport { SequelizeCrudService } from '@midwayjs/crud/sequelize';\nimport { Repository } from 'sequelize-typescript';\n\n@Provide()\nexport class UserCrudService extends SequelizeCrudService<UserModel> {\n  @InjectRepository(UserModel)\n  repo: Repository<UserModel>;\n}\n```\n\n### Mongoose\n\n```typescript\nimport { Inject, Provide } from '@midwayjs/core';\nimport { MongooseDataSourceManager } from '@midwayjs/mongoose';\nimport { MongooseCrudService } from '@midwayjs/crud/mongoose';\n\n@Provide()\nexport class UserCrudService extends MongooseCrudService<UserDocument> {\n  @Inject()\n  mongooseDataSourceManager: MongooseDataSourceManager;\n\n  async onReady() {\n    this.repo = this.mongooseDataSourceManager\n      .getDataSource('default')\n      .model('User');\n  }\n}\n```\n\n这几个适配基类的对外用法保持一致，区别主要在于底层仓储注入方式和 ORM 行为。\n\n## 快速生成类式 REST 接口\n\n当你已经有一个 `CrudService` 之后，如果希望快速生成标准资源型接口，可以使用 `@Crud()`。\n\n### 最小类式示例\n\n```typescript\nimport { Controller, Inject } from '@midwayjs/core';\nimport { Crud } from '@midwayjs/crud';\n\nimport { UserEntity } from '../entity/user';\nimport { UserCrudService } from '../service/user.crud';\n\n@Controller('/users')\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n})\nexport class UserController {\n  @Inject()\n  crudService: UserCrudService;\n}\n```\n\n默认会生成：\n\n- `GET /users`\n- `GET /users/:id`\n- `POST /users`\n- `PATCH /users/:id`\n- `DELETE /users/:id`\n\n### 为什么还要 `@Inject() crudService`\n\n因为 `@Crud()` 只是声明“这个 Controller 是一个 CRUD 资源”，真正执行业务的是你绑定的 `crudService`。\n\n也就是说：\n\n- `@Crud()` 负责生成默认路由\n- `crudService` 负责真正执行 CRUD 逻辑\n\n这是这个组件最重要的设计原则之一。\n\n### 业务逻辑写在哪里\n\n不要把复杂业务逻辑塞到 `@Crud()` 里。\n\n推荐做法是：\n\n- 资源级默认行为：写在 `UserCrudService`\n- 复杂业务编排：写在你自己的 `UserService` / Domain Service\n- 非标准动作：写成普通路由方法\n\n例如：\n\n```typescript\n@Controller('/users')\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n})\nexport class UserController {\n  @Inject()\n  crudService: UserCrudService;\n\n  async create() {\n    // 自定义事务、调用多个 service、做额外校验\n  }\n\n  async resetPassword() {\n    // 非标准资源动作\n  }\n}\n```\n\n同名方法会优先使用你手写的实现，因此你可以只覆写部分默认行为。\n\n### 裁剪默认路由\n\n如果你不想暴露所有默认路由，可以通过 `routes.only` 或 `routes.exclude` 控制。\n\n```typescript\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n  routes: {\n    only: ['list', 'detail', 'create'],\n  },\n})\n```\n\n这对于“只能查、不能删”或“只开放后台管理的一部分动作”的场景很有用。\n\n## 函数式路由模式\n\n如果项目使用函数式 API，而不是类式 Controller，可以从 `@midwayjs/crud/functional` 导入 `defineCrudRoutes()`。\n\n### 最小示例\n\n```typescript\nimport { defineApi } from '@midwayjs/core/functional';\nimport { defineCrudRoutes } from '@midwayjs/crud/functional';\n\nimport { UserEntity } from '../entity/user';\nimport { UserCrudService } from '../service/user.crud';\n\nconst crudRoutes = defineCrudRoutes<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n});\n\nexport default defineApi('/users', api => ({\n  ...crudRoutes(api),\n}));\n```\n\n### 和自定义动作一起使用\n\n函数式模式最常见的用法，是把默认 CRUD 路由和自定义动作合并在同一个 `defineApi()` 里。\n\n```typescript\nexport default defineApi('/users', api => ({\n  ...crudRoutes(api),\n  resetPassword: api\n    .post('/:id/reset-password')\n    .handle(async ({ input, ctx }) => {\n      return { ok: true };\n    }),\n}));\n```\n\n这样可以让“标准资源动作”和“业务动作”共存在同一个资源路由下。\n\n## 查询协议\n\n列表接口使用统一的查询协议，这是这个组件最重要的能力之一。\n\n### 支持的 query 参数\n\n- `page=<number>`\n- `limit=<number>`\n- `sort=<field>:<ASC|DESC>`，可重复传入\n- `filter=<field>||<operator>||<value>`，可重复传入\n- `search=<keyword>`\n- `join=<relation>`，可重复传入\n- `fields=<field1,field2,...>`\n\n例如：\n\n```text\nGET /users?page=1&limit=20&sort=createdAt:DESC&filter=status||eq||active&search=harry&join=profile\n```\n\n### 支持的过滤操作符\n\n首阶段支持：\n\n- `eq`\n- `ne`\n- `gt`\n- `gte`\n- `lt`\n- `lte`\n- `in`\n- `like`\n\n### 查询协议的约束\n\n这些约束是有意设计出来的，用来保证资源接口的一致性：\n\n- `sort` 字段必须在 `sortable` 白名单里\n- `filter` 字段必须在 `filterable` 白名单里\n- `search` 只会作用于 `searchable` 白名单\n- `join` 必须在 `join` 白名单里\n- `join` 首阶段只支持一层关系名，不支持 `profile.company`\n\n也就是说，这个组件不会让客户端随意拼接任意字段查询，而是让你在服务端声明资源允许暴露的查询能力。\n\n### 在 `@Crud()` 中声明查询能力\n\n```typescript\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n  query: {\n    defaultLimit: 20,\n    maxLimit: 100,\n    sortable: ['id', 'createdAt'],\n    filterable: ['status'],\n    searchable: ['name', 'email'],\n    join: ['profile'],\n  },\n})\n```\n\n这样配置后，查询行为就会按这个白名单执行。\n\n## 返回结构\n\n默认列表接口会返回统一的分页结构，而不是直接返回数组。\n\n```typescript\ntype CrudPageResult<T> = {\n  data: T[];\n  meta: {\n    page: number;\n    limit: number;\n    total: number;\n    pageCount: number;\n    hasNext: boolean;\n    hasPrev: boolean;\n  };\n};\n```\n\n这能让前端和不同资源接口之间保持一致的分页消费方式。\n\n其他默认返回规则：\n\n- `detail` 返回单个资源对象\n- `create` 返回创建后的资源对象\n- `update` 返回更新后的资源对象\n- `delete` 默认返回 `204 No Content`\n\n## DTO、Validation 与 Swagger\n\nCRUD 路由会尽量复用现有 Midway 组件，而不是另起一套协议。\n\n### DTO 绑定\n\n你可以在 `@Crud()` 中声明：\n\n- `dto.create`\n- `dto.update`\n- `dto.replace`\n- `dto.query`\n\n例如：\n\n```typescript\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n  dto: {\n    create: CreateUserDTO,\n    update: UpdateUserDTO,\n    query: UserQueryDTO,\n  },\n})\n```\n\n### Validation\n\n如果项目里启用了 `@midwayjs/validation` 或兼容的 validation service：\n\n- `dto.create` 会用于创建请求体校验\n- `dto.update` 会用于更新请求体校验\n- `dto.replace` 会用于替换请求体校验\n- `dto.query` 会用于列表查询参数校验\n\n如果没有安装 validation 组件，CRUD 本身不会强行报错，而是只跳过这一步自动校验。\n\n### Swagger\n\n自动生成的 CRUD 路由会复用现有 Web 元数据链，所以 Swagger 组件可以识别到这些路由。\n\n也就是说：\n\n- 默认路由会出现在 Swagger 文档里\n- 基础的 path / query / body / response 元数据会被自动补齐\n\n这意味着你不需要为每一个简单资源接口手动补一遍同样的 swagger 装饰器。\n\n## 软删除\n\n默认删除行为是**硬删除**。\n\n如果你希望某个资源使用软删除，需要显式开启：\n\n```typescript\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n  delete: {\n    mode: 'soft',\n  },\n})\n```\n\n开启后：\n\n- `DELETE /:id` 会走软删除\n- `list` / `detail` 默认排除已软删的数据\n- 如果底层实体或仓储不支持软删除，会直接报错\n\n### 为什么不是默认软删除\n\n因为软删除不是所有资源都适合：\n\n- 会影响唯一键约束\n- 会影响查询逻辑\n- 会影响索引设计\n- 会影响后台数据管理\n\n所以这里采用的是“显式开启”的策略，而不是默认偷偷帮你切换成软删。\n\n## 一个更完整的类式示例\n\n下面给一个稍完整一点的例子，把前面的内容串起来。\n\n```typescript\nimport { Controller, Inject, Provide } from '@midwayjs/core';\nimport { Crud } from '@midwayjs/crud';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { TypeOrmCrudService } from '@midwayjs/crud/typeorm';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class UserCrudService extends TypeOrmCrudService<UserEntity> {\n  @InjectEntityModel(UserEntity)\n  repo: Repository<UserEntity>;\n}\n\n@Controller('/users')\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n  dto: {\n    create: CreateUserDTO,\n    update: UpdateUserDTO,\n    query: UserQueryDTO,\n  },\n  query: {\n    defaultLimit: 20,\n    maxLimit: 100,\n    sortable: ['id', 'createdAt'],\n    filterable: ['status'],\n    searchable: ['name', 'email'],\n    join: ['profile'],\n  },\n  delete: {\n    mode: 'soft',\n  },\n})\nexport class UserController {\n  @Inject()\n  crudService: UserCrudService;\n}\n```\n\n这段代码的效果是：\n\n- 生成一个 `/users` 资源接口\n- 自动获得标准 CRUD 路由\n- 自动使用统一查询协议\n- 自动按 DTO 做校验（如果启用了 validation）\n- 自动被 swagger 扫描\n- 删除逻辑使用软删除\n\n## 二级导出\n\n组件提供这些稳定入口：\n\n- `@midwayjs/crud`\n- `@midwayjs/crud/typeorm`\n- `@midwayjs/crud/mikro`\n- `@midwayjs/crud/sequelize`\n- `@midwayjs/crud/mongoose`\n- `@midwayjs/crud/functional`\n\n建议这样理解：\n\n- 主入口：核心类型 + `@Crud()`\n- 数据库二级入口：各自的 CRUD service 适配\n- `functional`：函数式路由适配\n\n## 最后的建议\n\n如果你是第一次接触这个组件，建议按这个顺序使用：\n\n1. 先学 `service-only`，把它当一个标准 CRUD service\n2. 再用 `@Crud()` 快速展开简单资源接口\n3. 最后再用函数式模式或高级查询配置\n\n这样更容易理解它的边界，也更不容易把复杂业务错误地塞进自动 CRUD 层。\n","site/docs/extensions/egg.md":"# EggJS\n\nMidway 可以使用 EggJS 作为上层 Web 框架，EggJS 提供了非常多常用的插件和 API，帮助用户快速构建企业级 Web 应用。本章节内容，主要介绍 EggJS 在 Midway 中如何使用自身的能力。\n\n| 描述              |      |\n| ----------------- | ---- |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/web@4 egg --save\n$ npm i @midwayjs/egg-ts-helper --save-dev\n```\n\n针对 EggJS 场景，这些包列举如下。\n\n```json\n  \"dependencies\": {\n    \"@midwayjs/web\": \"^4.0.0\",\n    \"@midwayjs/core\": \"^4.0.0\",\n    \"egg\": \"^2.0.0\",\n    \"egg-scripts\": \"^2.10.0\"\n  },\n  \"devDependencies\": {\n    \"@midwayjs/egg-ts-helper\": \"^1.0.1\",\n  },\n```\n\n| @midwayjs/web           | **必须**，Midway EggJS 适配层    |\n|-------------------------|----------------------------|\n| @midwayjs/core          | **必须**，Midway 核心包          |\n| egg                     | **必须**，EggJS 依赖包，提供定义等其他能力 |\n| egg-scripts             | **可选**，EggJS 启动脚本          |\n| @midwayjs/egg-ts-helper | **可选**，EggJS 定义生成工具        |\n\n也可以直接使用脚手架创建示例。\n\n```bash\n# npm v6\n$ npm init midway --type=egg-v3 my_project\n\n# npm v7\n$ npm init midway -- --type=egg-v3 my_project\n```\n\n\n\n## 开启组件\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as web from '@midwayjs/web';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [web],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: web.Application;\n\n  async onReady() {\n\t\t// ...\n  }\n}\n\n```\n\n\n\n## 和默认 EggJS 的不同之处\n\n\n- 1、从 v3 开始，midway 提供了更多的组件，大部分 egg 内置插件默认禁用\n- 2、baseDir 默认调整为 `src` 目录，服务器上为 `dist` 目录\n- 3、禁用 egg-logger，全部替换为 @midwayjs/logger，不可切换\n\n\n\n整个架构如下:\n![](https://cdn.nlark.com/yuque/0/2021/png/501408/1614842824740-fc0c1432-3ace-4f77-b51f-15212984b168.png)\n\n\n## 目录结构\n\n\n除了 Midway 提供的目录结构外，EggJS 还有一些特殊的目录结构（不可变），整个结构如下。\n```\n➜  my_midway_app tree\n.\n├── src\n|   ├── app.ts \t\t\t\t\t\t\t\t\t\t \t\t\t\t## EggJS 扩展 Worker 生命周期文件（可选）\n|   ├── agent.ts\t\t\t\t\t\t\t\t   \t\t\t\t## EggJS 扩展 Agent 生命周期文件（可选）\n|   ├── app\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t## EggJS 固定的根目录（可选）\n|   │   ├── public\t\t \t\t\t\t\t\t\t\t  \t## EggJS 静态托管插件的默认目录（可配）\n|   │   |   └── reset.css\n|   │   ├── view (可选)\t\t\t\t\t\t\t\t\t\t ## EggJS 模板渲染的默认目录（可配）\n|   │   |   └── home.tpl\n|   │   └── extend (可选)\t\t\t\t\t\t\t\t\t ## EggJS 扩展目录（可配）\n|   │       ├── helper.ts (可选)\n|   │       ├── request.ts (可选)\n|   │       ├── response.ts (可选)\n|   │       ├── context.ts (可选)\n|   │       ├── application.ts (可选)\n|   │       └── agent.ts (可选)\n|   │\n|   ├── config\n|   |   ├── plugin.ts\n|   |   ├── config.default.ts\n|   |   ├── config.prod.ts\n|   |   ├── config.test.ts (可选)\n|   |   ├── config.local.ts (可选)\n|   |   └── config.unittest.ts (可选)\n│   ├── controller\t\t\t\t\t\t\t\t \t\t\t\t## Midway 控制器目录（推荐）\n│   ├── service\t\t\t\t\t\t\t\t     \t\t\t\t## Midway 服务目录（推荐）\n│   └── schedule\t\t\t\t\t\t\t\t\t \t\t\t\t## Midway 定时器目录（推荐）\n│\n├── typings                        \t\t\t\t## EggJS 定义生成目录\n├── test\n├── package.json\n└── tsconfig.json\n```\n以上是 EggJS 的目录结构全貌，其中包含了很多 EggJS 特有的目录，有一些在 Midway 体系中已经有相应的能力替代，可以直接替换。整个结构，基本上等价于将 EggJS 的目录结构移动到了 `src` 目录下。\n\n\n由于 EggJS 是基于约定的框架，整个工程的目录结构是固定的，这里列举一些常用的约定目录。\n\n| `src/app/public/**`  | 用于放置静态资源，可选，具体参见内置插件 [egg-static](https://github.com/eggjs/egg-static)。 |\n| --- | --- |\n| `src/config/config.{env}.ts` | 用于编写配置文件，具体参见[配置](https://eggjs.org/zh-cn/basics/config.html)。 |\n| `src/config/plugin.js` | 用于配置需要加载的插件，具体参见[插件](https://eggjs.org/zh-cn/basics/plugin.html)。 |\n| `test/**` | 具体参见[单元测试](https://eggjs.org/zh-cn/core/unittest.html)。 |\n| `src/app.js` 和 `src/agent.js` | 用于自定义启动时的初始化工作，可选，具体参见[启动自定义](https://eggjs.org/zh-cn/basics/app-start.html)。关于`agent.js`的作用参见[Agent机制](https://eggjs.org/zh-cn/core/cluster-and-ipc.html#agent-%E6%9C%BA%E5%88%B6)。 |\n\n\n\n## 配置定义\n\n\nMidway 在脚手架中提供了标准的 EggJS 的 TS 配置写法，MidwayConfig 中包括了 egg 中配置的定义和属性提示，结构如下。\n```typescript\n// src/config/config.default.ts\nimport { MidwayConfig, MidwayAppInfo } from '@midwayjs/core';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // use for cookie sign key, should change to your own and keep security\n    keys: appInfo.name + '_xxxx',\n    egg: {\n      port: 7001,\n    },\n    // security: {\n    //   csrf: false,\n    // },\n  } as MidwayConfig;\n};\n\n```\n通过这样返回方法的形式，在运行期会被自动执行，合并进完整的配置对象。\n\n\n这个函数的参数为 `MidwayAppConfig` 类型，值为以下内容。\n\n| **appInfo** | **说明** |\n| --- | --- |\n| pkg | package.json |\n| name | 应用名，同 pkg.name |\n| baseDir | 应用代码的 src （本地开发）或者 dist （上线后）目录 |\n| appDir | 应用代码的目录 |\n| HOME | 用户目录，如 admin 账户为 /home/admin |\n| root | 应用根目录，只有在 local 和 unittest 环境下为 baseDir，其他都为 HOME。 |\n\n\n\n:::info\n注意，这里的 `baseDir` 和 `appDir` 和 EggJS 应用有所区别。\n:::\n\n\n\n\n## 使用 Egg 插件\n\n插件是 EggJS 的特色之一，`@midwayjs/web` 也支持 EggJS 的插件体系，但是在有 Midway 组件的情况下，尽可能优先使用 Midway 组件。\n\n\n插件一般通过 npm 模块的方式进行复用。\n```bash\n$ npm i egg-mysql --save\n```\n然后需要在应用或框架的 `src/config/plugin.js` 中声明开启。\n\n\n如果有 `export default` ，请写在其中。\n```typescript\nimport { EggPlugin } from 'egg';\nexport default {\n  static: false, // default is true\n  mysql: {\n    enable: true,\n    package: 'egg-mysql'\n  }\n} as EggPlugin;\n\n```\n如果没有 `export default` ，可以直接导出。\n```typescript\n// src/config/plugin.ts\n// 使用 mysql 插件\nexport const mysql = {\n  enable: true,\n  package: 'egg-mysql',\n};\n```\n\n\n在开启插件之后，我们就可以在业务代码中使用插件提供的功能了。一般来说，插件会将对象挂载到 EggJS 的 `app` 和 `ctx` 之上，然后直接使用。\n\n\n```typescript\napp.mysql.query(sql, values);\t\t\t// egg 提供的方法\n```\n在 Midway 中可以通过 `@App` 获取 `app` 对象，以及在请求作用域中通过 `@Inject() ctx` 获取 `ctx` 对象，所以我们可以通过注入来获取插件对象。\n\n\n```typescript\nimport { Provide, Inject, Get } from '@midwayjs/core';\nimport { Application, Context } from '@midwayjs/web';\n\n@Provide()\nexport class HomeController {\n\n  @App()\n  app: Application;\n\n  @Inject()\n  ctx: Context;\n\n  @Get('/')\n  async home() {\n  \tthis.app.mysql.query(sql, values);    // 调用 app 上的方法（如果有的话）\n    this.ctx.mysql.query(sql, values);\t\t// 调用挂载在 ctx 上的方法（如果有的话）\n  }\n}\n```\n此外，还可以通过 `@Plugin` 装饰器来直接注入 `app` 挂载的插件，默认情况下，如果不传参数，将以属性名作为 key。\n\n\n```typescript\nimport { Provide, Get, Plugin } from '@midwayjs/core';\n\n@Provide()\nexport class HomeController {\n\n  @Plugin()\n  mysql: any;\n\n  @Get('/')\n  async home() {\n  \tthis.mysql.query(sql, values);\n  }\n}\n```\n:::info\n`@Plugin() mysql`  等价于 `app.mysql` 。 `@Plugin` 的作用就是从 app 对象上拿对应属性名的插件，所以 `@Plugin() xxx` 就等于 `app['xxx']` 。\n:::\n\n\n## Web 中间件\n\n\n中间件样例如下：\n\n\n```typescript\nimport { Middleware, IMiddleware } from '@midwayjs/core';\nimport { Context, NextFunction } from '@midwayjs/web';\n\n@Middleware()\nexport class ReportMiddleware implements IMiddleware<Context, NextFunction> {\n\n  resolve() {\n    return async (ctx: Context, next: NextFunction) => {\n      const startTime = Date.now();\n      await next();\n      console.log(Date.now() - startTime);\n    };\n  }\n\n}\n```\n\n:::caution\n注意\n\n1、如果要继续使用 EggJS 传统的函数式写法，必须将文件放在 `src/app/middleware` 下\n\n2、egg 自带的内置中间件已经集成\n\n:::\n\n应用中间件。\n\n```typescript\n// src/configuration.ts\nimport { App, Configuration } from '@midwayjs/core';\nimport * as egg from '@midwayjs/web';\nimport { ReportMiddleware } from './middleware/user.middleware';\n\n@Configuration({\n  imports: [egg]\n  // ...\n})\nexport class MainConfiguration {\n\n  @App()\n  app: egg.Application;\n\n  async onReady() {\n    this.app.useMiddleware(ReportMiddleware);\n  }\n}\n\n```\n\n更多用法请参考 [Web 中间件](../middleware)\n\n\n\n## 中间件顺序\n\n由于 egg 也有自己的中间件逻辑，在新版本中，我们将中间件加载顺序做了一定的处理，执行顺序如下：\n\n- 1、egg 框架中的中间件\n- 2、egg 插件通过 config.coreMiddleware 添加的顺序\n- 3、业务代码配置在 config.middleware 中配置的顺序\n- 4、app.useMiddleware 添加的顺序\n\n因为 midway 的中间件会后置加载，所以我们可以在 onReady 中进行自定义排序。\n\n\n\n## BodyParser\n\negg 自带  `bodyParser` 功能，默认会解析 `Post` 请求，自动识别 `json` 和 `form` 类型。\n\n如需 text 或者 xml，可以自行配置。\n\n默认的大小限制为 `1mb`，可以单独对每项配置大小。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  bodyParser: {\n    formLimit: '1mb',\n    jsonLimit: '1mb',\n    textLimit: '1mb',\n    xmlLimit: '1mb',\n  },\n}\n```\n\n注意，使用 Postman 做 Post 请求时的类型选择：\n\n![postman](https://img.alicdn.com/imgextra/i4/O1CN01QCdTsN1S347SuzZU5_!!6000000002190-2-tps-1017-690.png)\n\n\n\n\n## 定时任务\nv3 开始请参考 [bull 组件](./bull) 。\n\n如需兼容之前的 [egg 定时任务](https://eggjs.org/zh-cn/basics/schedule.html) ，请照下列方法。\n\n首先安装 `midway-schedule` 依赖。\n\n```bash\n$ npm i midway-schedule --save\n```\n\n添加到插件中即可。\n\n```typescript\n// src/config/plugin.ts\nexport default {\n  schedule: true,\n  schedulePlus: {\n    enable: true,\n    package: 'midway-schedule',\n  },\n};\n```\n\n使用请参考上一版本文档。\n\n\n\n## 日志\n\nv3 开始无法使用 egg-logger，请参考 [日志](../logger) 章节。\n\n\n\n## 异常处理\n\nEggJS 框架通过 [onerror](https://github.com/eggjs/egg-onerror) 插件提供了统一的错误处理机制，会作为 Midway 的兜底错误逻辑，和 [错误过滤器](../error_filter) 不冲突。\n\n对一个请求的所有处理方法（Middleware、Controller、Service）中抛出的任何异常都会被它捕获，并自动根据请求想要获取的类型返回不同类型的错误（基于 [Content Negotiation](https://tools.ietf.org/html/rfc7231#section-5.3.2)）。\n\n\n\n| 请求需求的格式 | 环境 | errorPageUrl 是否配置 | 返回内容 |\n| --- | --- | --- | --- |\n| HTML & TEXT | local & unittest | - | onerror 自带的错误页面，展示详细的错误信息 |\n| HTML & TEXT | 其他 | 是 | 重定向到 errorPageUrl |\n| HTML & TEXT | 其他 | 否 | onerror 自带的没有错误信息的简单错误页（不推荐） |\n| JSON & JSONP | local & unittest | - | JSON 对象或对应的 JSONP 格式响应，带详细的错误信息 |\n| JSON & JSONP | 其他 | - | JSON 对象或对应的 JSONP 格式响应，不带详细的错误信息 |\n\n\n\n\nonerror 插件的配置中支持 errorPageUrl 属性，当配置了 errorPageUrl 时，一旦用户请求线上应用的 HTML 页面异常，就会重定向到这个地址。\n\n\n在 `src/config/config.default.ts` 中\n```typescript\n// src/config/config.default.ts\nmodule.exports = {\n  onerror: {\n    // 线上页面发生异常时，重定向到这个页面上\n    errorPageUrl: '/50x.html',\n  },\n};\n```\n\n\n\n## 扩展 Application/Context/Request/Response\n\n\n### 增加扩展逻辑\n\n\n虽然 MidwayJS 并不希望直接将属性挂载到 koa 的 Context，App 上（会造成管理和定义的不确定性），但是 EggJS 的这项功能依旧可用。\n\n\n文件位置如下。\n```\n➜  my_midway_app tree\n.\n├── src\n│   ├── app\n│   │   └── extend\n│   │       ├── application.ts\n│   │       ├── context.ts\n│   │       ├── request.ts\n│   │       └── response.ts\n│   ├── config\n│   └── interface.ts\n├── test\n├── package.json\n└── tsconfig.json\n```\n内容和原来的 EggJS 相同。\n```typescript\n// src/app/extend/context.ts\nexport default {\n  get hello() {\n    return 'hello world';\n  },\n};\n```\n### 增加扩展定义\n\nContext 请使用 Midway 的方式来扩展，请查看 [扩展上下文定义](/docs/context_definition)。\n\n\n其余的部分，沿用 egg 的方式，请在 `src/interface.ts` 中扩展。\n```typescript\n// src/interface.ts\ndeclare module 'egg' {\n  interface Request {\n    // ...\n  }\n  interface Response {\n    // ...\n  }\n  interface Application {\n    // ...\n  }\n}\n```\n:::info\n业务自定义扩展的定义请 **不要放在根目录** `typings` 下，避免被 ts-helper 工具覆盖掉。\n:::\n\n\n\n## 使用 egg-scripts 部署\n\n由于 EggJS 提供了默认的多进程部署工具 `egg-scripts` ，Midway 也继续支持这种方式，如果上层是 EggJS，推荐这种部署方式。\n\n首先在依赖中，确保安装 `egg-scripts` 包。\n\n```bash\n$ npm i egg-scripts --save\n```\n\n\n\n添加 `npm scripts` 到 `package.json`：\n\n在上面的代码构建之后，使用我们的 `start` 和 `stop` 命令即可完成启动和停止。\n\n```json\n\"scripts\": {\n    \"start\": \"egg-scripts start --daemon --title=********* --framework=@midwayjs/web\",\n    \"stop\": \"egg-scripts stop --title=*********\",\n}\n```\n\n\n\n:::info\n\n`*********` 的地方是你的项目名。\n:::\n\n> 注意：`egg-scripts` 对 Windows 系统的支持有限，参见 [#22](https://github.com/eggjs/egg-scripts/pull/22)。\n\n####\n\n**启动参数**\n\n```bash\n$ egg-scripts start --port=7001 --daemon --title=egg-server-showcase\n```\n\nCopy\n\n如上示例，支持以下参数：\n\n- `--port=7001` 端口号，默认会读取环境变量 process.env.PORT，如未传递将使用框架内置端口 7001。\n- `--daemon` 是否允许在后台模式，无需 nohup。若使用 Docker 建议直接前台运行。\n- `--env=prod` 框架运行环境，默认会读取环境变量 process.env.EGG_SERVER_ENV， 如未传递将使用框架内置环境 prod。\n- `--workers=2` 框架 worker 线程数，默认会创建和 CPU 核数相当的 app worker 数，可以充分的利用 CPU 资源。\n- `--title=egg-server-showcase` 用于方便 ps 进程时 grep 用，默认为 `egg-server-\\${appname}`。\n- `--framework=yadan` 如果应用使用了[自定义框架](https://eggjs.org/zh-cn/advanced/framework.html)，可以配置 package.json 的 egg.framework 或指定该参数。\n- `--ignore-stderr` 忽略启动期的报错。\n- `--https.key` 指定 HTTPS 所需密钥文件的完整路径。\n- `--https.cert` 指定 HTTPS 所需证书文件的完整路径。\n- 所有 [egg-cluster](https://github.com/eggjs/egg-cluster) 的 Options 都支持透传，如 --port 等。\n\n更多参数可查看 [egg-scripts](https://github.com/eggjs/egg-scripts) 和 [egg-cluster](https://github.com/eggjs/egg-cluster) 文档。\n\n:::info\n\n使用 egg-scripts 部署的日志会存放在 **用户目录** 下**，**比如 `/home/xxxx/logs` 。\n\n:::\n\n\n\n## 启动环境\n\n原有 egg 使用 `EGG_SERVER_ENV` 中作为环境标志，在 Midway 中请使用 `MIDWAY_SERVER_ENV`。\n\n\n\n## State 类型定义\n\n在 egg 底层的 koa 的 Context 中有一个特殊的 State 属性，通过和 Context 类似的方式可以扩展 State 定义。\n\n```typescript\n// src/interface.ts\n\ndeclare module '@midwayjs/web/dist/interface' {\n  interface Context {\n    abc: string;\n  }\n\n  interface State{\n    bbb: string;\n    ccc: number;\n  }\n}\n```\n\n\n\n## 配置\n\n### 默认配置\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  egg: {\n    port: 7001,\n  },\n}\n```\n\n`@midwayjs/web`  所有参数如下：\n\n| 配置项         | 类型             | 描述                         |\n| -------------- | ---------------- | ---------------------------- |\n| port           | number           | 必填，启动的端口             |\n| key            | string           | Buffer                       |\n| cert           | string           | Buffer                       |\n| ca             | string           | Buffer                       |\n| hostname       | string           | 监听的 hostname，默认 127.1  |\n| http2          | boolean          | 可选，http2 支持，默认 false |\n| queryParseMode | simple\\|extended | 默认为 extended              |\n| queryParseOptions | `qs.IParseOptions` | 解析选项，当使用'simple'模式解析时可用 |\n\n以上的属性，对本地和使用 `bootstrap.js` 部署的应用生效。\n\n\n\n### 修改端口\n\n:::tip\n\n注意，这个方式只会对本地研发，以及使用 bootstrap.js 文件部署的项目生效。\n\n:::\n\n默认情况下，我们在 `config.default` 提供了 `7001` 的默认端口参数，修改它就可以修改 egg http 服务的默认端口。\n\n比如我们修改为 `6001`：\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  egg: {\n    port: 6001,\n  },\n}\n```\n\n默认情况下，单测环境由于需要 supertest 来启动端口，我们的 port 配置为 `null`。\n\n```typescript\n// src/config/config.unittest\nexport default {\n  // ...\n  egg: {\n    port: null,\n  },\n}\n```\n\n此外，也可以通过 `midway-bin dev --ts --port=6001` 的方式来临时修改端口，此方法会覆盖配置中的端口。\n\n\n\n### 全局前缀\n\n此功能请参考 [全局前缀](../controller#全局路由前缀)。\n\n\n\n### Https 配置\n\n在大多数的情况，请尽可能使用外部代理的方式来完成 Https 的实现，比如 Nginx。\n\n在一些特殊场景下，你可以通过配置 SSL 证书（TLS 证书）的方式，来直接开启 Https。\n\n首先，你需要提前准备好证书文件，比如 `ssl.key` 和 `ssl.pem`，key 为服务端私钥，pem 为对应的证书。\n\n然后配置即可。\n\n```typescript\n// src/config/config.default\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  egg: {\n    key: join(__dirname, '../ssl/ssl.key'),\n    cert: join(__dirname, '../ssl/ssl.pem'),\n  },\n}\n```\n\n\n\n### favicon 设置\n\n默认情况下，浏览器会发起一个 `favicon.ico` 的请求。\n\n```typescript\n// src/config/config.default\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  siteFile: {\n    '/favicon.ico': readFileSync(join(__dirname, 'favicon.png')),\n  },\n}\n```\n\n\n\n如果开启了 `@midwayjs/static-file` 组件，那么会优先使用组件的静态文件托管。\n\n### 修改上下文日志\n\n可以单独修改 egg 框架的上下文日志。\n\n```typescript\nexport default {\n  egg: {\n    contextLoggerFormat: info => {\n      const ctx = info.ctx;\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} [${ctx.userId} - ${Date.now() - ctx.startTime}ms ${ctx.method}] ${info.message}`;\n    }\n    // ...\n  },\n};\n```\n\n\n\n### Query 数组解析\n\n默认情况下，`ctx.query` 会解析为忽略数组的情况，而 `ctx.queries` 会严格的将所有的字段都变成数组。\n\n如果调整 `queryParseMode` ，则可以使 `ctx.query` 变为两者之间的结构（querystring 的结果）。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  egg: {\n    // ...\n    queryParseMode: 'simple',\n    queryParseOptions: {\n      arrayLimit: 100,\n    },\n  },\n}\n```\n\n\n\n\n## 常见问题\n### 1、生成 ts 定义\n\n\nMidway 提供了 `@midwayjs/egg-ts-hepler` 工具包，用于快速生成 EggJS 开发时所依赖的定义。\n```bash\n$ npm install @midwayjs/egg-ts-helper --save-dev\n```\n在 `package.json` 中加入对应的 `ets` 命令即可，一般来说，我们会在 dev 命令前加入，以保证代码的正确性。\n```json\n  \"scripts\": {\n    \"dev\": \"cross-env ets && cross-env NODE_ENV=local midway-bin dev --ts\",\n  },\n```\n:::info\n在第一次编写代码前，需要执行一次此命令才能有 ts 定义生成。\n:::\n\n\nEggJS 生成的定义在 `typings` 目录中。\n```\n➜  my_midway_app tree\n.\n├── src                            ## midway 项目源码\n├── typings                        ## EggJS 定义生成目录\n├── test\n├── package.json\n└── tsconfig.json\n```\n\n\n### 2、EggJS 中 Configuration 的特殊情况\n\n\n在 EggJS 下， `configuration.ts`  中的生命周期**只会在 worker 下加载执行**。如果在 Agent 有类似的需求，请直接使用 EggJS 自身的 `agent.ts` 处理。\n\n\n\n### 3、异步初始化配置无法覆盖插件配置\n\n`onConfigLoad` 生命周期会在 egg 插件（若有）初始化之后执行，所以不能用于覆盖 egg 插件所使用的配置。\n\n\n\n### 4、默认的 csrf 错误\n\n\n在 post 请求，特别是第一次时用户会发现一个 csrf 报错。原因是 egg 在框架中默认内置了安全插件 [egg-security](https://github.com/eggjs/egg-security)， 默认开启了  csrf 校验。\n\n\n我们可以在配置中关闭它，但是更好的是去[**了解它**](https://eggjs.org/zh-cn/core/security.html#%E5%AE%89%E5%85%A8%E5%A8%81%E8%83%81-csrf-%E7%9A%84%E9%98%B2%E8%8C%83)之后再做选择。\n\n```typescript\nexport const security = {\n  csrf: false,\n};\n```\n\n\n\n\n### 5、不存在定义的问题\n\n一些 egg 插件未提供 ts 定义，导致使用会出现未声明方法的情况，比如 egg-mysql。\n![image.png](https://img.alicdn.com/imgextra/i1/O1CN01mv68zG1zN6nALff8n_!!6000000006701-2-tps-1478-876.png)\n可以使用 any 绕过。\n\n```typescript\nawait (this.app as any).mysql.query(sql);\n```\n\n或者可以自行增加扩展定义。\n\n### 6、获取 Http Server\n\nEggjs 内部封装了原始的 HttpServer，需要通过事件获取。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, App } from '@midwayjs/core';\nimport { Application } from '@midwayjs/web';\n\n@Configuration(/***/)\nexport class MainConfiguration {\n  \n  @App('egg')\n  app: Application;\n  \n  // ...\n  async onServerReady() {\n    this.app.once('server', (server) => {\n      // ...\n    })\n  }\n}\n```\n\n","site/docs/extensions/etcd.md":"# ETCD\n\netcd 是云原生架构中重要的基础组件，由 CNCF 孵化托管。etcd 在微服务和 Kubernetes 集群中可以作为服务注册于发现，也可以作为 key-value 存储的中间件。\n\nMidway 提供基于 [etcd3](https://github.com/microsoft/etcd3) 模块封装的组件，提供 etcd 的客户端调用能力。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/etcd@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/etcd\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n\n## 引入组件\n\n\n首先，引入 组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as etcd from '@midwayjs/etcd';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    etcd,\n  ],\n  // ...\n})\nexport class MainConfiguration {\n}\n```\n\n\n\n## 配置默认客户端\n\n大部分情况下，我们可以只使用默认客户端来完成功能。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  etcd: {\n    client: {\n      host: [\n        '127.0.0.1:2379'\n      ]\n    },\n  },\n}\n```\n\n\n\n## 使用默认客户端\n\n配置完成后，我们就可以在代码中使用了。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { ETCDService } from '@midwayjs/etcd';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  etcdService: ETCDService;\n\n  async invoke() {\n\n    await this.etcdService.put('foo').value('bar');\n\n    const fooValue = await this.etcdService.get('foo').string();\n    console.log('foo was:', fooValue);\n\n    const allFValues = await this.etcdService.getAll().prefix('f').keys();\n    console.log('all our keys starting with \"f\":', allFValues);\n\n    await this.etcdService.delete().all();\n  }\n}\n```\n\n更多 API 请参考 ts 定义或者 [官网文档](https://microsoft.github.io/etcd3/classes/etcd3.html)。\n\n\n\n## 多实例配置\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  etcd: {\n    clients: {\n      instance1: {\n        {\n          host: [\n            '127.0.0.1:2379'\n          ]\n        },\n      },\n  \t\tinstance2: {\n        {\n          host: [\n            '127.0.0.1:2379'\n          ]\n        },\n      }\n    }\n  },\n}\n```\n\n\n\n## 多实例获取\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { ETCDServiceFactory } from '@midwayjs/etcd';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  etcdServiceFactory: ETCDServiceFactory;\n\n  async invoke() {\n    const instance1 = this.etcdServiceFactory.get('instance1');\n    // ...\n\n    const instance2 = this.etcdServiceFactory.get('instance2');\n    // ...\n  }\n}\n```\n\n\n\n## 动态创建实例\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { ETCDServiceFactory } from '@midwayjs/etcd';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  etcdServiceFactory: ETCDServiceFactory;\n\n  async invoke() {\n    const instance3 = await this.etcdServiceFactory.createInstance({\n      host: [\n        '127.0.0.1:2379'\n      ]\n    }, 'instance3');\n    // ...\n  }\n}\n```\n\n\n\n## 服务发现\n\n从 v4 开始提供基于统一抽象的服务发现能力，虽然 ETCD 并不太适合作为服务发现的注册中心来使用，但是我们仍实现了 ETCD 作为服务发现的客户端能力。\n\n服务发现包括 **注册服务** 和 **获取服务** 两部分。\n\n\n### 配置服务发现\n\n你需要先配置一个客户端；若存在多个客户端，可通过 `serviceDiscoveryClient` 指定用于服务发现的客户端。\n\n通过 `serviceDiscovery` 配置服务发现选项：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  etcd: {\n    clients: {\n      default: { hosts: ['127.0.0.1:2379'] }\n    },\n    serviceDiscovery: {\n      namespace: 'services',\n      ttl: 30,\n      // 可选：loadBalancer: LoadBalancerType.ROUND_ROBIN\n    }\n  }\n}\n```\n\n说明：`namespace` 为键空间前缀，实例键结构为 `namespace/serviceName/instanceId`；`ttl` 为实例过期时间（秒）。\n\n\n### 注册服务发现\n\n服务启动后，将当前服务注册到 ETCD：\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport { EtcdServiceDiscovery } from '@midwayjs/etcd';\n\n@Configuration({})\nexport class MainConfiguration {\n  @Inject()\n  etcdDiscovery: EtcdServiceDiscovery;\n\n  async onServerReady() {\n    // 创建服务发现客户端（可传入覆盖项）\n    const client = this.etcdDiscovery.createClient();\n\n    // 注册当前服务并上线（写入键并绑定租约）\n    await client.register({\n      id: client.defaultMeta.id,\n      serviceName: 'order',\n      ttl: 30,\n      meta: {\n        version: '1.0.0',\n        host: client.defaultMeta.host,\n        port: '7001'\n      }\n    });\n  }\n}\n```\n\n说明：注册时会默认上线，无需显式调用 `online`。ETCD 通过 Lease 维持 TTL，定时续约保证存活；`offline` 删除键并撤销租约；`deregister` 注销并清理状态。重复调用 `register/online/offline/deregister` 具有幂等性。\n\n\n### 获取可用服务\n\n在任意位置获取可用实例或按负载均衡选择一个实例：\n\n````typescript\n@Provide()\nexport class OrderService {\n  @Inject()\n  etcdDiscovery: EtcdServiceDiscovery;\n\n  async getService() {\n    const instances = await this.etcdDiscovery.getInstances('order');\n    const one = await this.etcdDiscovery.getInstance('order');\n    return { instances, one };\n  }\n}\n````\n\n返回的实例为注册时写入的对象（`EtcdInstanceMetadata`），通常包含 `serviceName/id/ttl/meta/tags/status` 等字段。\n","site/docs/extensions/events.md":"# 事件\n\n事件组件是基于 [eventemitter2](https://github.com/EventEmitter2/EventEmitter2) 实现的，提供了强大的事件处理能力。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/event-emitter@4 --save\n```\n\n或者在 `package.json` 中增加依赖后，重新安装\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/event-emitter\": \"^4.0.0\"\n  }\n}\n```\n\n## 使用组件\n\n首先在 `configuration.ts` 中引入组件：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as eventEmitter from '@midwayjs/event-emitter';\n\n@Configuration({\n  imports: [\n    // ...\n    eventEmitter\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n## 基础使用\n\n### 定义事件监听器\n\n:::caution\n注意：`@OnEvent()` 装饰器只能用在单例（Singleton）类上。如果在非单例类上使用，事件监听器将无法正常工作。\n:::\n\n使用 `@OnEvent()` 装饰器来定义事件监听器：\n\n```typescript\nimport { Provide, Singleton } from '@midwayjs/core';\nimport { OnEvent } from '@midwayjs/event-emitter';\n\n@Provide()\n@Singleton()    // 必须使用 @Singleton 装饰器\nexport class UserService {\n  @OnEvent('user.created')\n  async handleUserCreated(user: any) {\n    console.log('新用户创建:', user);\n  }\n}\n```\n\n### 发送事件\n\n通过注入 `EventEmitterService` 来发送事件：\n\n```typescript\nimport { Inject } from '@midwayjs/core';\nimport { EventEmitterService } from '@midwayjs/event-emitter';\n\n@Provide()\nexport class UserController {\n  @Inject()\n  eventEmitterService: EventEmitterService;\n\n  async createUser() {\n    const user = { id: 1, name: 'harry' };\n    // ... 创建用户逻辑\n    \n    // 发送同步事件\n    this.eventEmitterService.emit('user.created', user);\n    \n    // 或者发送异步事件\n    await this.eventEmitterService.emitAsync('user.created', user);\n  }\n}\n```\n\n## 高级特性\n\n### 事件监听器选项\n\n`@OnEvent()` 装饰器支持多个配置选项，每个选项都有其特定的用途：\n\n#### prependListener\n\n用于控制监听器的执行顺序。设置为 `true` 时，会将监听器添加到监听器队列的开头，这样可以确保该监听器最先执行。\n\n```typescript\n@Provide()\n@Singleton()\nexport class UserService {\n  // 这个监听器会最先执行\n  @OnEvent('user.created', { prependListener: true })\n  async handleUserCreatedFirst(user: any) {\n    console.log('第一个处理用户创建事件');\n  }\n\n  // 这个监听器会在队列末尾执行\n  @OnEvent('user.created')\n  async handleUserCreated(user: any) {\n    console.log('后续处理用户创建事件');\n  }\n}\n```\n\n#### suppressErrors\n\n控制事件处理器中的错误处理行为：\n- 默认值为 `true`，表示错误会被捕获并忽略\n- 设置为 `false` 时，错误会被抛出，可以在外层捕获\n\n```typescript\n@Provide()\n@Singleton()\nexport class UserService {\n  // 错误会被抛出\n  @OnEvent('user.created', { suppressErrors: false })\n  async handleWithError(user: any) {\n    throw new Error('处理失败');  // 这个错误会被抛出\n  }\n\n  // 错误会被忽略\n  @OnEvent('user.created', { suppressErrors: true })\n  async handleSuppressError(user: any) {\n    throw new Error('处理失败');  // 这个错误会被忽略\n  }\n}\n\n// 在调用处可以捕获错误\n@Provide()\nexport class UserController {\n  @Inject()\n  eventEmitterService: EventEmitterService;\n\n  async createUser() {\n    try {\n      await this.eventEmitterService.emitAsync('user.created', { id: 1 });\n    } catch (err) {\n      // 可以捕获到 suppressErrors: false 的处理器抛出的错误\n      console.error('事件处理失败:', err);\n    }\n  }\n}\n```\n\n#### async\n\n控制事件处理器的执行方式：\n- 设置为 `true` 时，处理器会在单独的事件循环中异步执行\n- 默认为 `false`，会按顺序同步执行\n\n```typescript\n@Provide()\n@Singleton()\nexport class UserService {\n  // 异步执行，不会阻塞其他处理器\n  @OnEvent('user.created', { async: true })\n  async handleAsync(user: any) {\n    await someTimeConsumingTask();\n    console.log('异步处理完成');\n  }\n\n  // 同步执行，会按顺序等待\n  @OnEvent('user.created')\n  async handleSync(user: any) {\n    console.log('同步处理');\n  }\n}\n```\n\n### 通配符事件\n\n通配符事件允许你使用模式匹配来监听一组相关的事件。需要在配置中启用 `wildcard: true`。\n\n#### 配置启用\n\n```typescript\n// config/config.default.ts\nexport default {\n  eventEmitter: {\n    wildcard: true,  // 启用通配符功能\n  }\n}\n```\n\n#### 使用示例\n\n```typescript\n@Provide()\n@Singleton()\nexport class UserService {\n  // 监听所有用户相关事件\n  @OnEvent('user.*')\n  async handleUserEvents(...args: any[]) {\n    // 注意：通配符模式下，事件处理器直接接收发送事件时传入的参数\n    console.log('事件参数:', args);\n  }\n\n  // 监听多层级的事件\n  @OnEvent('user.*.detail')\n  async handleUserDetailEvents(...args: any[]) {\n    // 可以匹配 user.profile.detail, user.settings.detail 等\n    console.log('事件参数:', args);\n  }\n}\n\n// 发送事件示例\n@Provide()\nexport class UserController {\n  @Inject()\n  eventEmitterService: EventEmitterService;\n\n  async updateUser() {\n    // 这些事件都会被上面的 user.* 处理器捕获\n    await this.eventEmitterService.emitAsync('user.created', { id: 1 });\n    await this.eventEmitterService.emitAsync('user.updated', { id: 1, name: 'new name' });\n    await this.eventEmitterService.emitAsync('user.profile.detail', { id: 1, profile: {} });\n  }\n}\n```\n\n:::info\n注意：在使用通配符事件时，事件处理器的参数直接是事件发送时传入的参数。无法在处理器中获取具体触发的事件名称，所以建议在发送事件时将必要的信息包含在事件参数中。\n:::\n\n### 命名空间事件\n\n命名空间事件提供了一种组织和分组事件的方式。通过 `delimiter` 配置分隔符。\n\n#### 配置启用\n\n```typescript\n// config/config.default.ts\nexport default {\n  eventEmitter: {\n    delimiter: '.' // 设置命名空间分隔符\n  }\n}\n```\n\n#### 使用示例\n\n```typescript\n@Provide()\n@Singleton()\nexport class OrderService {\n  // 监听订单命名空间下的创建事件\n  @OnEvent('order.created')\n  async handleOrderCreated(order: any) {\n    console.log('订单创建:', order);\n  }\n\n  // 如果需要使用通配符匹配，需要额外启用 wildcard: true\n  @OnEvent('order.payment.*')\n  async handleOrderPaymentEvents(...args: any[]) {\n    // 可以匹配 order.payment.success, order.payment.failed 等\n    console.log('事件参数:', args);\n  }\n}\n\n// 发送命名空间事件\n@Provide()\nexport class PaymentService {\n  @Inject()\n  eventEmitterService: EventEmitterService;\n\n  async processPayment() {\n    // 发送支付成功事件\n    await this.eventEmitterService.emitAsync('order.payment.success', {\n      orderId: '123',\n      amount: 100\n    });\n  }\n}\n```\n\n:::info\n注意：命名空间的分隔符默认是 `.`，你可以通过 `delimiter` 配置修改。如果需要使用通配符（如 `*`）进行事件匹配，则需要额外启用 `wildcard: true` 配置。\n:::\n\n## 配置\n\n在 `config.default.ts` 中可以配置 Event Emitter 的行为：\n\n```typescript\nexport default {\n  // ...\n  eventEmitter: {\n    // 是否开启通配符事件\n    wildcard: true,\n    // 命名空间分隔符\n    delimiter: '.',\n    // 是否启用新监听器事件\n    newListener: false,\n    // 是否启用移除监听器事件\n    removeListener: false,\n    // 设置最大监听器数量\n    maxListeners: 10,\n    // 是否启用详细的内存泄漏检测\n    verboseMemoryLeak: false,\n  }\n}\n```\n\n## 常见问题\n\n### 1. 事件处理顺序\n\n- 默认情况下，事件处理器按照注册顺序执行\n- 使用 `prependListener: true` 可以将处理器添加到队列开头\n- 异步事件处理器（`emitAsync`）会按顺序等待每个处理器完成\n\n### 2. 错误处理\n\n- 默认情况下，事件处理器中的错误会被抑制（`suppressErrors: true`）\n- 设置 `suppressErrors: false` 可以让错误抛出\n- 使用 `emitAsync` 时，可以通过 try/catch 捕获错误\n\n### 3. 性能考虑\n\n- 合理使用通配符事件，过多的通配符匹配可能影响性能\n- 设置适当的 `maxListeners` 值，避免内存泄漏\n- 及时移除不需要的事件监听器","site/docs/extensions/express.md":"# Express\n\n本章节内容，主要介绍在 Midway 中如何使用 Express 作为上层框架，并使用自身的能力。\n\n| 描述           |      |\n| -------------- | ---- |\n| 包含独立主框架 | ✅    |\n| 包含独立日志   | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/express@4 --save\n$ npm i @types/body-parser @types/express @types/express-session --save-dev\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/express\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    \"@types/body-parser\": \"^1.19.2\",\n    \"@types/express\": \"^4.17.13\",\n    \"@types/express-session\": \"^1.17.4\",\n    // ...\n  }\n}\n```\n\n也可以直接使用脚手架创建示例。\n\n```bash\n# npm v6\n$ npm init midway --type=express-v3 my_project\n\n# npm v7\n$ npm init midway -- --type=express-v3 my_project\n```\n\n\n针对 Express，Midway 提供了 `@midwayjs/express` 包进行了适配，在其中提供了 Midway 特有的依赖注入、切面等能力。\n\n:::info\n我们使用的 Express 版本为 `v4` 。\n:::\n\n\n## 目录结构\n```\n.\n├── src\n│   ├── controller\t\t\t\t\t\t\t\t \t\t\t\t# controller接口的地方\n│   ├── service\t\t\t\t\t\t\t\t\t \t\t\t\t\t# service逻辑处理的地方\n|   └── configuration.ts\t\t\t\t\t\t\t\t\t# 入口及生命周期配置、组件管理\n├── test\n├── package.json\n└── tsconfig.json\n```\n\n\n\n## 开启组件\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [express],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: express.Application;\n\n  async onReady() {}\n}\n```\n\n\n\n\n## 控制器（Controller）\n\n\n整个请求控制器的写法和 Midway 适配其他框架的类似。为了和其他场景的框架写法一致，在请求的时候，Midway 将 Express 的 `req` 映射为 `ctx` 对象。\n```typescript\nimport { Inject, Controller, Get, Provide, Query } from '@midwayjs/core';\nimport { Context, NextFunction } from '@midwayjs/express';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  ctx: Context;\n\n  @Get('/')\n  async home(@Query() id) {\n    console.log(id);\t\t\t\t\t\t// req.query.id === id\n    return 'hello world';\t\t\t\t// 简单返回，等价于 res.send('hello world');\n  }\n}\n```\n你也可以额外注入 `req` 和 `res` 。\n```typescript\nimport { Inject, Controller, Get, Provide, Query } from '@midwayjs/core';\nimport { Context, Response, NextFunction } from '@midwayjs/express';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  ctx: Context;   // 即为 req\n\n  @Inject()\n  req: Context;\n\n  @Inject()\n  res: Response;\n\n  @Get('/')\n  async home(@Query() id) {\n    // this.req.query.id === id\n  }\n}\n```\n\n\n\n## Web 中间件\n\n\nExpress 的中间件写法比较特殊，它的参数不同。\n\n\n```typescript\nimport { Middleware } from '@midwayjs/core';\nimport { Context, Response, NextFunction } from '@midwayjs/express';\n\n@Middleware()\nexport class ReportMiddleware implements IMiddleware<Context, Response, NextFunction> {\n\n  resolve() {\n    return async (\n      req: Context,\n      res: Response,\n      next: NextFunction\n    ) => {\n      console.log('Request...');\n      next();\n    };\n  }\n\n}\n```\n\n注意，这里我们导出了一个 `ReportMiddleware` 类，为了方便对接异步流程，`resolve` 返回可以是 async 函数。\n\nExpress 中的 next 方法，用于调用到下一个中间件，指的注意的是，Express 中间件并非洋葱模型，是单向调用。\n\n\n\n\n### 路由中间件\n\n\n我们可以把上面编写的中间件应用到单个 Controller 上，也可以将中间件应用到单个路由上。\n\n\n```typescript\nimport { Controller, Get, Provide } from '@midwayjs/core';\n\n@Controller('/', { middleware: [ ReportMiddleware ]})\t\t\t// controller 级别的中间件\nexport class HomeController {\n\n  @Get('/', { middleware: [ ReportMiddleware ]})\t\t\t\t// 路由级别的中间件\n  async home() {\n    return 'hello world'\n  }\n}\n```\n\n\n### 全局中间件\n\n\n直接使用 Midway 提供的 `app.generateMiddleware` 方法，在入口处加载全局中间件。\n```typescript\n// src/configuration.ts\nimport { Configuration, ILifeCycle } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport { ReportMiddleware } from './middleware/report.middleware.ts'\n\n@Configuration({\n  imports: [express],\n})\nexport class MainConfiguration implements ILifeCycle {\n\n  @App()\n  app: express.Application;\n\n  async onReady() {\n    this.app.useMiddleware(ReportMiddleware);\n  }\n}\n```\n\n\n除了加载 Class 形式的中间件外，也支持加载传统的 Express 中间件。\n```typescript\n// src/configuration.ts\nimport { Configuration, ILifeCycle, App } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [express],\n})\nexport class MainConfiguration implements ILifeCycle {\n\n  @App()\n  app: express.Application;\n\n  async onReady() {\n    this.app.useMiddleware((req, res, next) => {\n    \t// xxx\n    });\n  }\n}\n```\n你可以通过注入 `app` 对象，来调用到所有 Express 上的方法。\n\n\n\n## 返回统一处理\n\n由于 Express 中间件是单向调用，无法在返回时执行，为此我们额外设计了一个 `@Match` 装饰的过滤器，用于处理返回值的行为。\n\n比如，我们可以定义针对全局返回的过滤器。\n\n```typescript\n// src/filter/globalMatch.filter.ts\nimport { Match } from '@midwayjs/core';\nimport { Context, Response } from '@midwayjs/express';\n\n@Match()\nexport class GlobalMatchFilter {\n  match(value, req, res) {\n    // ...\n    return {\n      status: 200,\n      data: {\n        value\n      },\n    };\n  }\n}\n```\n\n也可以匹配特定的路由做返回。\n\n```typescript\n// src/filter/api.filter.ts\nimport { Match } from '@midwayjs/core';\nimport { Context, Response } from '@midwayjs/express';\n\n@Match((ctx: Context, res: Response) => {\n  return ctx.path === '/api';\n})\nexport class APIMatchFilter {\n  match(value, req: Context, res: Response) {\n    // ...\n    return {\n      data: {\n        message:\n        data: value,\n      },\n    };\n  }\n}\n```\n\n需要应用到 app 中。\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport { join } from 'path';\nimport { APIMatchFilter } from './filter/api.filter';\nimport { GlobalMatchFilter } from 'filter/globalMatch.filter';\n\n@Configuration({\n  imports: [express],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: express.Application;\n\n  async onReady() {\n    // ...\n    this.app.useFilter([APIMatchFilter, GlobalMatchFilter]);\n  }\n}\n```\n\n注意，这类过滤器是按照添加的顺序来匹配执行。\n\n\n\n## 错误处理\n\n和普通的项目相同，使用错误过滤器，但是参数略有不同。\n\n```typescript\nimport { Catch } from '@midwayjs/core';\nimport { Context, Response } from '@midwayjs/express';\n\n@Catch()\nexport class GlobalError {\n  catch(err: Error, req: Context, res: Response) {\n    if (err) {\n      return {\n        status: err.status ?? 500,\n        message: err.message,\n      }\n    }\n  }\n}\n```\n\n需要应用到 app 中。\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport { join } from 'path';\nimport { GlobalError } from './filter/global.filter';\n\n@Configuration({\n  imports: [express],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: express.Application;\n\n  async onReady() {\n    this.app.useMiddleware((req, res, next) => {\n      next();\n    });\n\n    this.app.useFilter([GlobalError]);\n  }\n}\n```\n\n注意，`@Match` 和 `@Catch` 都是过滤器，在内部会自动判断做区分执行。。\n\n\n\n## Cookie\n\n`@midwayjs/express` 自带  `cookie parser` 功能，使用的是 `cookie-parser` 模块。\n\n针对 Cookie，统一使用 `keys` 作为秘钥。\n\n```typescript\n// src/config/config.default\nexport default {\n  keys:  ['key1', 'key2'],\n}\n```\n\n获取 Cookie。\n\n```typescript\nconst cookieValue = req.cookies['cookie-key'];\n```\n\n设置 Cookie。\n\n```typescript\nres.cookie(\n  'cookie-key',\n  'cookie-value',\n  cookieOptions\n);\n```\n\n\n\n##  Session\n\n`@midwayjs/express` 内置了 Session 组件，给我们提供了 `ctx.session` 来访问或者修改当前用户 Session 。\n\n默认情况下为 `cookie-session` ，默认配置如下。\n\n```typescript\n// src/config/config.default\nexport default {\n  session:  {\n    name: 'MW_SESS',\n    resave: true,\n    saveUninitialized: true,\n    cookie: {\n      maxAge: 24 * 3600 * 1000, // ms\n      httpOnly: true,\n      // sameSite: null,\n    },\n  }\n}\n```\n\n我们可以通过简单的 API 来设置 session。\n\n```typescript\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  req;\n\n  @Get('/')\n  async get() {\n    // set all\n    this.req.session = req.query;\n\n    // set value\n    this.req.session.key = 'abc';\n\n    // get\n    const key = this.req.session.key;\n\n    // remove\n    this.req.session = null;\n\n    // set max age\n    this.req.session.maxAge = Number(req.query.maxAge);\n\n    // ...\n  }\n}\n\n```\n\n\n\n## BodyParser\n\n`@midwayjs/express` 自带  `bodyParser` 功能，默认会解析 `Post` 请求，自动识别 `json` 、`text`和 `urlencoded` 类型。\n\n默认的大小限制为 `1mb`，可以单独对每项配置大小。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  bodyParser: {\n    json: {\n      enable: true,\n      limit: '1mb',\n      strict: true,\n    },\n    raw: {\n      enable: false,\n      limit: '1mb',\n    },\n    text: {\n      enable: true,\n      limit: '1mb',\n    },\n    urlencoded: {\n      enable: true,\n      extended: false,\n      limit: '1mb',\n      parameterLimit: 1000,\n    },\n  },\n}\n```\n\n\n\n## 配置\n\n### 默认配置\n\n`@midwayjs/express`  的配置样例如下：\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  express: {\n    port: 7001,\n  },\n}\n```\n\n所有属性描述如下：\n\n| 属性         | 类型                                         | 描述                                                    |\n| ------------ |--------------------------------------------| ------------------------------------------------------- |\n| port         | number                                     | 可选，启动的端口                                        |\n| globalPrefix | string                                     | 可选，全局的 http 前缀                                  |\n| keys         | string[]                                   | 可选，Cookies 签名，如果上层未写 keys，也可以在这里设置 |\n| hostname     | string                                     | 可选，监听的 hostname，默认 127.1                       |\n| key          | string \\| Buffer \\| Array\\<Buffer\\|Object> | 可选，Https key，服务端私钥                             |\n| cert         | string \\| Buffer \\| Array\\<Buffer\\|Object> | 可选，Https cert，服务端证书                            |\n| ca           | string \\| Buffer \\| Array\\<Buffer\\|Object> | 可选，Https ca                                          |\n| http2        | boolean                                    | 可选，http2 支持，默认 false                            |\n\n\n\n### 修改端口\n\n默认情况下，我们在 `config.default` 提供了 `7001` 的默认端口参数，修改它就可以修改 Express http 服务的默认端口。\n\n比如我们修改为 `6001`：\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  express: {\n    port: 6001,\n  },\n}\n```\n\n默认情况下，单测环境由于需要 supertest 来启动端口，我们的 port 配置为 `null`。\n\n```typescript\n// src/config/config.unittest\nexport default {\n  // ...\n  express: {\n    port: null,\n  },\n}\n```\n\n此外，也可以通过 `midway-bin dev --ts --port=6001` 的方式来临时修改端口，此方法会覆盖配置中的端口。\n\n\n\n### 全局前缀\n\n此功能请参考 [全局前缀](../controller#全局路由前缀)。\n\n\n\n### Https 配置\n\n在大多数的情况，请尽可能使用外部代理的方式来完成 Https 的实现，比如 Nginx。\n\n在一些特殊场景下，你可以通过配置 SSL 证书（TLS 证书）的方式，来直接开启 Https。\n\n首先，你需要提前准备好证书文件，比如 `ssl.key` 和 `ssl.pem`，key 为服务端私钥，pem 为对应的证书。\n\n然后配置即可。\n\n```typescript\n// src/config/config.default\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  express: {\n    key: join(__dirname, '../ssl/ssl.key'),\n    cert: join(__dirname, '../ssl/ssl.pem'),\n  },\n}\n```\n\n\n\n### 修改上下文日志\n\n可以单独修改 express 框架的上下文日志。\n\n```typescript\nexport default {\n  express: {\n    contextLoggerFormat: info => {\n      // 等价 req\n      const req = info.ctx;\n      const userId = req?.['session']?.['userId'] || '-';\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} [${userId} - ${Date.now() - req.startTime}ms ${req.method}] ${info.message}`;\n    }\n    // ...\n  },\n};\n```\n\n","site/docs/extensions/grpc.md":"# gRPC\n\ngRPC 是一个高性能、通用的开源 RPC 框架，其由 Google 主要面向移动应用开发并基于 HTTP/2 协议标准而设计，基于 ProtoBuf(Protocol Buffers) 序列化协议开发，且支持众多开发语言。\n\n\n本篇内容演示了如何在 Midway 体系下，提供 gRPC 服务，以及调用 gRPC 服务的方法。\n\n\nMidway 当前采用了最新的 gRPC 官方推荐的 [@grpc/grpc-js](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js) 进行开发，并提供了一些工具包，用于快速发布服务和调用服务。\n\n我们使用的模块为 `@midwayjs/grpc` ，既可以独立发布服务，又可以接入其它框架调用 gRPC 服务。\n\n相关信息：\n\n**提供服务**\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n\n**调用服务**\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n\n**其他**\n\n| 描述                 |      |\n| -------------------- | ---- |\n| 可作为主框架独立使用 | ✅    |\n| 可独立添加中间件     | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/grpc@4 --save\n$ npm i @midwayjs/grpc-helper --save-dev\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/grpc\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    \"@midwayjs/grpc-helper\": \"^1.0.0\",\n    // ...\n  }\n}\n```\n\n\n\n## 开启组件\n\n:::tip\n\n不管是提供服务还是调用服务，都需要开启组件。\n\n:::\n\n`@midwayjs/grpc` 可以作为独立主框架使用。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as grpc from '@midwayjs/grpc';\n\n@Configuration({\n  imports: [grpc],\n  // ...\n})\nexport class MainConfiguration {\n  async onReady() {\n\t\t// ...\n  }\n}\n\n```\n\n也可以附加在其他的主框架下，比如 `@midwayjs/koa` 。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as grpc from '@midwayjs/grpc';\n\n@Configuration({\n  imports: [koa, grpc],\n  // ...\n})\nexport class MainConfiguration {\n  async onReady() {\n\t\t// ...\n  }\n}\n\n```\n\n\n\n## 目录结构\n\n大致的目录结构如下，`src/provider` 是提供 gRPC 服务的目录。\n\n```\n.\n├── package.json\n├── proto                         ## proto 定义文件\n│   └── helloworld.proto\n├── src\n│   ├── configuration.ts          ## 入口配置文件\n│   ├── interface.ts\n│   └── provider                  ## gRPC 提供服务的文件\n│       └── greeter.ts\n├── test\n├── bootstrap.js                  ## 服务启动入口\n└── tsconfig.json\n```\n\n\n\n## 定义服务接口\n\n\n在微服务中，定义一个服务需要特定的接口定义语言（IDL）来完成，在 gRPC中 默认使用 Protocol Buffers  作为序列化协议。\n\n\n序列化协议独立于语言和平台，提供了多种语言的实现，Java，C++，Go 等等,每一种实现都包含了相应语言的编译器和库文件。所以 gRPC 是一个提供和调用都可以跨语言的服务框架。\n\n一个gRPC服务的大体架构可以用官网上的一幅图表示。\n\n![](https://img.alicdn.com/imgextra/i3/O1CN01kpIyg51k8i5DtcGpZ_!!6000000004639-2-tps-621-445.png)\n\n\nProtocol Buffers  协议的文件，默认的后缀为 `.proto` 。.proto后缀的IDL文件,并通过其编译器生成特定语言的数据结构、服务端接口和客户端Stub代码。\n\n\n:::info\n由于 proto 文件可以跨语言使用，为了方便共享，我们一般将 proto 文件放在 src 目录外侧，方便其他工具复制分发。\n:::\n\n\n下面是一个基础的 `proto/helloworld.proto`  文件。\n```protobuf\nsyntax = \"proto3\";\n\npackage helloworld;\n\nservice Greeter {\n  // Sends a greeting\n  rpc SayHello (HelloRequest) returns (HelloReply) {}\n}\n\nmessage HelloRequest {\n  string name = 1;\n}\n\nmessage HelloReply {\n  string message = 1;\n}\n\n```\n\n\nproto3 表示的是第三版的 protobuf 协议，是 gRPC 目前推荐的版本，“语法简单，功能更全”。\n\n\n我们可以用 `service` 格式，定义服务体，其中可以包含方法。同时，我们可以更加细致的通过 `message` 描述服务具体的请求参数和响应参数。\n\n\n我们可以从 [Google 的官网文档](https://developers.google.com/protocol-buffers/docs/overview#simple) 中查看更多细节。\n\n\n:::info\n大家会看到，这和 Java 中的 Class 非常相像，每个结构就相当于 Java 中的一个类。\n:::\n\n\n### 编写 proto 文件\n\n\n现在我们再来看之前的服务，是不是就很好理解了。\n```protobuf\nsyntax = \"proto3\";\n\npackage helloworld;\n\n// 服务的定义\nservice Greeter {\n  // Sends a greeting\n  rpc SayHello (HelloRequest) returns (HelloReply) {}\n}\n\n// 服务的请求参数\nmessage HelloRequest {\n  string name = 1;\n}\n\n// 服务的响应参数\nmessage HelloReply {\n  string message = 1;\n}\n\n```\n\n\n我们定义了一个名为 `Greeter` 的服务，包含一个 `HelloRequest` 结构的请求体，以及返回 `HelloReply` 结构的响应体。\n\n\n接下去，我们将对这个服务给大家做演示。\n\n\n### 生成代码定义\n\n\n传统的 gRPC 框架，需要用户手动编写 proto 文件，以及生成 js 服务，最后再根据 js 生成的服务再编写实现，在 Midway 体系下，我们提供了一个 grpc-helper 工具包来加速这个过程。\n\n\n如果没有安装，可以先安装。\n```bash\n$ npm i @midwayjs/grpc-helper --save-dev\n```\n\n\ngrpc-helper 工具的作用，是将用户提供的 proto 文件，生成对应可读的 ts interface 文件。\n\n\n我们可以添加一个脚本，方便这个过程。\n```json\n{\n\t\"scripts\": {\n     \"generate\": \"tsproto --path proto --output src/domain\"\n  }\n}\n```\n\n\n然后执行 `npm run generate` 。\n\n\n上述命令执行后，会在代码的 `src/domain` 目录中生成 proto 文件对应的服务接口定义。\n\n\n:::info\n不管是提供 gRPC 服务还是调用 gRPC 服务，都要先生成定义。\n:::\n\n\n生成的代码如下，包含有一个命名空间（namespace），以及命名空间下的两个 TypeScript Interface， `Greeter` 用于编写服务端实现， `GreeterClient` 用于编写客户端实现。\n```typescript\n/**\n* This file is auto-generated by grpc-helper\n*/\n\nimport * as grpc from '@midwayjs/grpc';\n\n// 生成的命名空间\nexport namespace helloworld {\n\n  // 服务端使用的定义\n  export interface Greeter {\n    // Sends a greeting\n    sayHello(data: HelloRequest): Promise<HelloReply>;\n  }\n\n  // 客户端使用的定义\n  export interface GreeterClient {\n    // Sends a greeting\n    sayHello(options?: grpc.IClientOptions): grpc.IClientUnaryService<HelloRequest, HelloReply>;\n  }\n\n  // 请求体结构\n  export interface HelloRequest {\n    name?: string;\n  }\n\n  // 响应体结构\n  export interface HelloReply {\n    message?: string;\n  }\n}\n\n```\n\n\n:::info\n每当 proto 文件被修改时，就需要重新生成对应的服务定义，然后将对应的方法实现。\n:::\n\n\n\n## 提供 gRPC 服务（Provider）\n\n\n### 编写服务提供方（Provider）\n\n\n在 `src/provider` 目录中，我们创建 `greeter.ts` ，内容如下\n```typescript\nimport {\n  MSProviderType,\n  Provider,\n  GrpcMethod,\n} from '@midwayjs/core';\nimport { helloworld } from '../domain/helloworld';\n\n/**\n * 实现 helloworld.Greeter 接口的服务\n */\n@Provider(MSProviderType.GRPC, { package: 'helloworld' })\nexport class Greeter implements helloworld.Greeter {\n\n  @GrpcMethod()\n  async sayHello(request: helloworld.HelloRequest) {\n    return { message: 'Hello ' + request.name };\n  }\n}\n\n```\n:::info\n注意，@Provider 装饰器和 @Provide 装饰器不同，前者用于提供服务，后者用于依赖注入容器扫描标识的类。\n:::\n\n\n我们使用 `@Provider` 暴露出一个 RPC 服务， `@Provider` 的第一个参数为 RPC 服务类型，这个参数是个枚举，这里选择 GRPC 类型。\n\n\n`@Provider` 的第二个参数为 RPC 服务的元数据，这里指代的是 gRPC 服务的元数据。这里需要写入 gRPC 的 package 字段，即 proto 文件中的 package 字段（这里的字段用于和 proto 文件加载后的字段做对应）。\n\n\n对于普通的 gRPC 服务接口（UnaryCall），我们只需要使用 `@GrpcMethod()` 装饰器修饰即可。修饰的方法即为服务定义本身，入参为 proto 中定义好的入参，return 值即为定义好的响应体。\n\n:::info\n注意，生成的 Interface 是为了更好的编写服务代码，规范结构，请务必按照定义编写。\n:::\n\n\n### 配置服务\n\n\n配置内容如下。\n```typescript\n// src/config/config.default\nimport { MidwayAppInfo, MidwayConfig } from '@midwayjs/core';\n\nexport default (appInfo: MidwayAppInfo): MidwayConfig => {\n  return {\n    // ...\n    grpcServer: {\n      services: [\n        {\n          protoPath: join(appInfo.appDir, 'proto/hero.proto'),\n          package: 'hero',\n        },\n        {\n          protoPath: join(appInfo.appDir, 'proto/helloworld.proto'),\n          package: 'helloworld',\n        }\n      ],\n    }\n  };\n}\n```\nservices 字段是数组，意味着 Midway 项目可以同时发布多个 gRPC 服务。每个 service 的结构为：\n\n| 属性      | 类型   | 描述                       |\n| --- | --- | --- |\n| protoPath | string | 必选，proto 文件的绝对路径 |\n| package | string | 必选，服务对应的 package |\n\n除了 Service 配置之外，还有一些其他的配置。\n\n| 属性          | 类型              | 描述                                                         |\n| ------------- | ----------------- | ------------------------------------------------------------ |\n| url           | string            | 可选，gRPC 服务地址，默认 6565 端口，比如 'localhost:6565'   |\n| loaderOptions | Object            | 可选，proto file loader 的 options                           |\n| credentials   | ServerCredentials | 可选，grpc Server binding 时的 credentials 参数选项          |\n| serverOptions | ChannelOptions    | 可选，grpc Server 的 [自定义 options](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js#supported-channel-options) |\n\n\n\n### 提供安全证书\n\n可以通过 `credentials` 参数传递安全证书。\n\n```typescript\n// src/config/config.default\nimport { MidwayAppInfo, MidwayConfig } from '@midwayjs/core';\nimport { ServerCredentials } from '@midwayjs/grpc';\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nconst cert = readFileSync(join(__dirname, './cert/server.crt'));\nconst pem = readFileSync(join(__dirname, './cert/server.pem'));\nconst key = readFileSync(join(__dirname, './cert/server.key'));\n\nexport default (appInfo: MidwayAppInfo): MidwayConfig => {\n  return {\n    // ...\n    grpcServer: {\n      // ...\n      credentials: ServerCredentials.createSsl(cert, [{ private_key: key, cert_chain: pem }]);\n    }\n  };\n}\n```\n\n\n\n### 编写单元测试\n\n`@midwayjs/grpc` 库提供了一个 `createGRPCConsumer` 方法，用于实时调用客户端，一般我们用这个方法做测试。\n\n:::caution\n这个方法每次调用会实时连接，不建议将该方法用在生产环境。\n:::\n\n\n在测试中写法如下。\n```typescript\nimport { createApp, close } from '@midwayjs/mock';\nimport { Framework, createGRPCConsumer } from '@midwayjs/grpc';\nimport { join } from 'path';\nimport { helloworld } from '../src/domain/helloworld';\n\ndescribe('test/index.test.ts', () => {\n\n  it('should create multiple grpc service in one server', async () => {\n    const baseDir = join(__dirname, '../');\n\n    // 创建服务\n    const app = await createApp<Framework>();\n\n    // 调用服务\n    const service = await createGRPCConsumer<helloworld.GreeterClient>({\n      package: 'helloworld',\n      protoPath: join(baseDir, 'proto', 'helloworld.proto'),\n      url: 'localhost:6565'\n    });\n\n    const result = await service.sayHello().sendMessage({\n      name: 'harry'\n    });\n\n    expect(result.message).toEqual('Hello harry');\n    await close(app);\n  });\n\n});\n\n```\n\n\n\n## 调用 gRPC 服务（Consumer）\n\n\n我们编写一个 gRPC 服务来调用上面的暴露的服务。\n\n:::info\n事实上，你可以在 Web 的 Controller，或者 Service 等其他地方来调用，这里只是做一个示例。\n:::\n\n\n### 调用配置\n\n\n你需要在 `src/config/config.default.ts` 中增加你需要调用的目标服务以及它的 proto 文件信息。\n\n\n比如，这里我们填写了上面暴露的服务本身，以及该服务的 proto，包名等信息（函数形式）。\n```typescript\n// src/config/config.default\nimport { MidwayAppInfo, MidwayConfig } from '@midwayjs/core';\n\nexport default (appInfo: MidwayAppInfo): MidwayConfig => {\n  return {\n    // ...\n    grpc: {\n      services: [\n        {\n          url: 'localhost:6565',\n          protoPath: join(appInfo.appDir, 'proto/helloworld.proto'),\n          package: 'helloworld',\n        },\n      ],\n    },\n  };\n}\n```\n\n\n### 代码调用\n\n\n配置完后，我们就可以在代码里调用了。\n\n\n`@midwayjs/grpc` 提供了 `clients` ，可以方便的获取到已配置的服务。我们只需要在需要注入的地方，注入这个对象即可。\n\n\n比如：\n```typescript\nimport {\n  Provide,\n  Inject,\n} from '@midwayjs/core';\nimport { helloworld, hero } from '../interface';\nimport { Clients } from '@midwayjs/grpc';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  grpcClients: Clients;\n\n}\n```\n\n\n我们通过 `clients` 获取到对方服务的客户端实例，然后调用即可。\n\n\n```typescript\nimport {\n  Provide,\n  Inject,\n} from '@midwayjs/core';\nimport { helloworld, hero } from '../interface';\nimport { Clients } from '@midwayjs/grpc';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  grpcClients: Clients;\n\n  async invoke() {\n    // 获取服务\n  \tconst greeterService = this.grpcClients.getService<helloworld.GreeterClient>(\n      'helloworld.Greeter'\n    );\n\n    // 调用服务\n    const result = await greeterService.sayHello()\n    \t.sendMessage({\n        name: 'harry'\n      });\n\n    // 返回结果\n    return result;\n  }\n\n}\n```\n\n\n我们也可以利用 `@Init` 装饰器，将需要调用的服务缓存到属性上。这样可以在其他方法调用时复用。\n\n\n示例如下。\n```typescript\nimport {\n  GrpcMethod,\n  MSProviderType,\n  Provider,\n  Inject,\n  Init,\n} from '@midwayjs/core';\nimport { helloworld, hero } from '../interface';\nimport { Clients } from '@midwayjs/grpc';\n\n@Provider(MSProviderType.GRPC, { package: 'hero' })\nexport class HeroService implements hero.HeroService {\n  // 注入客户端\n  @Inject()\n  grpcClients: Clients;\n\n  greeterService: helloworld.GreeterClient;\n\n  @Init()\n  async init() {\n    // 赋值一个服务实例\n    this.greeterService = this.grpcClients.getService<helloworld.GreeterClient>(\n      'helloworld.Greeter'\n    );\n  }\n\n  @GrpcMethod()\n  async findOne(data) {\n    // 调用服务\n    const result = await greeterService.sayHello()\n    \t.sendMessage({\n        name: 'harry'\n      });\n\n    // 返回结果\n    return result;\n  }\n}\n\n```\n\n\n## 流式服务\n\n\ngRPC 的流式服务用于减少连接，让服务端或者客户端不需要等待即可执行任务，从而提高执行效率。\n\n\ngRPC 的流式服务分为三种，以服务端角度来说，为\n\n\n- 服务端接收流（客户端推）\n- 服务端响应流（服务端推）\n- 双向流\n\n\n\n下面我们将一一介绍。\n\n\n### 流式 proto 文件\n\n\n流式的 proto 文件写法不同，需要在希望使用流式的地方将参数标记为 `stream` 。\n```protobuf\n\nsyntax = \"proto3\";\n\npackage math;\n\nmessage AddArgs {\n  int32 id = 1;\n  int32 num = 2;\n}\n\nmessage Num {\n  int32 id = 1;\n  int32 num = 2;\n}\n\nservice Math {\n  rpc Add (AddArgs) returns (Num) {\n  }\n\n\t// 双向流\n  rpc AddMore (stream AddArgs) returns (stream Num) {\n  }\n\n  // 服务端往客户端推\n  rpc SumMany (AddArgs) returns (stream Num) {\n  }\n\n  // 客户端往服务端推\n  rpc AddMany (stream AddArgs) returns (Num) {\n  }\n}\n\n```\n该服务生成的接口定义为：\n\n\n```typescript\nimport {\n  IClientDuplexStreamService,\n  IClientReadableStreamService,\n  IClientUnaryService,\n  IClientWritableStreamService,\n  IClientOptions,\n} from '@midwayjs/grpc';\n\nexport namespace math {\n  export interface AddArgs {\n    id?: number;\n    num?: number;\n  }\n  export interface Num {\n    id?: number;\n    num?: number;\n  }\n\n  /**\n   * server interface\n   */\n  export interface Math {\n    add(data: AddArgs): Promise<Num>;\n    addMore(data: AddArgs): Promise<void>;\n    // 服务端推，客户端读\n    sumMany(data: AddArgs): Promise<void>\n    // 客户端端推，服务端读\n    addMany(num: AddArgs): Promise<void>;\n  }\n\n  /**\n   * client interface\n   */\n  export interface MathClient {\n    add(options?: IClientOptions): IClientUnaryService<AddArgs, Num>;\n    addMore(options?: IClientOptions): IClientDuplexStreamService<AddArgs, Num>;\n    // 服务端推，客户端读\n    sumMany(options?: IClientOptions): IClientReadableStreamService<AddArgs, Num>;\n    // 客户端端推，服务端读\n    addMany(options?: IClientOptions): IClientWritableStreamService<AddArgs, Num>;\n  }\n}\n\n```\n\n\n### 服务端推送\n\n\n客户端调用一次，服务端可以多次返回。通过 `@GrpcMethod()` 的参数来标识流式类型。\n\n\n可用的类型为:\n\n\n- `GrpcStreamTypeEnum.WRITEABLE`  服务端输出流（单工）\n- `GrpcStreamTypeEnum.READABLE`  客户端输出流（单工），服务端接受多次\n- `GrpcStreamTypeEnum.DUPLEX`  双工流\n\n\n\n服务端示例如下：\n```typescript\nimport { GrpcMethod, GrpcStreamTypeEnum, Inject, MSProviderType, Provider } from '@midwayjs/core';\nimport { Context, Metadata } from '@midwayjs/grpc';\nimport { math } from '../interface';\n\n/**\n */\n@Provider(MSProviderType.GRPC, { package: 'math' })\nexport class Math implements math.Math {\n\n  @Inject()\n  ctx: Context;\n\n  @GrpcMethod({type: GrpcStreamTypeEnum.WRITEABLE })\n  async sumMany(args: math.AddArgs) {\n    this.ctx.write({\n      num: 1 + args.num\n    });\n    this.ctx.write({\n      num: 2 + args.num\n    });\n    this.ctx.write({\n      num: 3 + args.num\n    });\n\n    this.ctx.end();\n  }\n\n  // ...\n}\n\n```\n服务端使用 `ctx.write` 方法来返回数据，由于是服务端流，可以返回多次。\n\n\n返回结束后，请使用 `ctx.end()` 方法关闭流。\n\n\n客户端，调用一次，接受多次数据。\n\n\n比如下面的累加逻辑。\n\n\nPromise 写法，会等待服务端数据都返回再做处理。\n```typescript\n// 服务端推送\nlet total = 0;\nlet result = await service.sumMany().sendMessage({\n  num: 1,\n});\n\nresult.forEach(data => {\n  total += data.num;\n});\n\n// total = 9;\n```\n\n\n事件写法，实时处理。\n```typescript\n// 服务端推送\nlet call = service.sumMany().getCall();\n\ncall.on('data', data => {\n\t// do something\n});\n\ncall.sendMessage({\n  num: 1,\n});\n\n```\n\n\n### 客户端推送\n\n\n客户端调用多次，服务端接收多次数据，返回一个结果。通过 `@GrpcMethod({type: GrpcStreamTypeEnum.READABLE})` 的参数来标识流式类型。\n\n\n服务端示例如下：\n```typescript\nimport { GrpcMethod, GrpcStreamTypeEnum, Inject, MSProviderType, Provider } from '@midwayjs/core';\nimport { Context, Metadata } from '@midwayjs/grpc';\nimport { math } from '../interface';\n\n/**\n */\n@Provider(MSProviderType.GRPC, { package: 'math' })\nexport class Math implements math.Math {\n\n  sumDataList: number[] = [];\n\n  @Inject()\n  ctx: Context;\n\n  @GrpcMethod({type: GrpcStreamTypeEnum.READABLE, onEnd: 'sumEnd' })\n  async addMany(data: math.Num) {\n    this.sumDataList.push(data);\n  }\n\n  async sumEnd(): Promise<math.Num> {\n    const total = this.sumDataList.reduce((pre, cur) => {\n      return {\n        num: pre.num + cur.num,\n      }\n    });\n    return total;\n  }\n\n  // ...\n}\n\n```\n\n\n客户端每次调用，都会触发一次 `addMany` 方法。\n\n\n在客户端发送 `end` 事件之后，会调用 `@GrpcMethod` 装饰器上的 `onEnd` 参数指定的方法，该方法的返回值即为最后客户端拿到的值。\n\n\n客户端示例如下：\n```typescript\n// 客户端推送\nconst data = await service.addMany()\n.sendMessage({num: 1})\n.sendMessage({num: 2})\n.sendMessage({num: 3})\n.end();\n\n// data.num = 6\n```\n\n\n### 双向流\n\n\n客户端可以调用多次，服务端也可以接收多次数据，返回多个结果，类似于传统的 TCP 通信。通过 `@GrpcMethod({type: GrpcStreamTypeEnum.DUPLEX})` 的参数来标识双工流式类型。\n\n\n服务端示例如下：\n```typescript\nimport { GrpcMethod, GrpcStreamTypeEnum, Inject, MSProviderType, Provider } from '@midwayjs/core';\nimport { Context, Metadata } from '@midwayjs/grpc';\nimport { math } from '../interface';\n\n/**\n */\n@Provider(MSProviderType.GRPC, { package: 'math' })\nexport class Math implements math.Math {\n\n  @Inject()\n  ctx: Context;\n\n  @GrpcMethod({type: GrpcStreamTypeEnum.DUPLEX, onEnd: 'duplexEnd' })\n  async addMore(message: math.AddArgs)  {\n    this.ctx.write({\n      id: message.id,\n      num: message.num + 10,\n    });\n  }\n\n  async duplexEnd() {\n    console.log('got client end message');\n  }\n  // ...\n}\n\n```\n服务端可以随时使用 `ctx.write` 返回数据，也可以使用 `ctx.end` 来关闭流。\n\n\n客户端示例：\n\n\n对于双工通信的客户端，由于无法保证调用、返回的顺序，我们需要使用监听的模式来消费结果。\n```typescript\nconst clientStream = service.addMore().getCall();\n\nlet total = 0;\nlet idx = 0;\n\nduplexCall.on('data', (data: math.Num) => {\n  total += data.num;\n  idx++;\n  if (idx === 2) {\n    duplexCall.end();\n    // total => 29\n  }\n});\n\nduplexCall.write({\n  num: 3,\n});\n\nduplexCall.write({\n  num: 6,\n});\n```\n\n\n如果希望保证调用顺序，我们也提供了保证顺序的双向流调用方法，但是需要在 proto 中定义一个固定的 id，来确保顺序。\n\n\n比如我们的 Math.proto，对每个入参和出参，都增加了一个固定的 id，所以可以固定顺序。\n```typescript\n\nsyntax = \"proto3\";\n\npackage math;\n\nmessage AddArgs {\n  int32 id = 1;\t\t\t\t\t\t\t//  这里的 id 名字是固定的\n  int32 num = 2;\n}\n\nmessage Num {\n  int32 id = 1;\t\t\t\t\t\t\t//  这里的 id 名字是固定的\n  int32 num = 2;\n}\n\nservice Math {\n  rpc Add (AddArgs) returns (Num) {\n  }\n\n  rpc AddMore (stream AddArgs) returns (stream Num) {\n  }\n\n  // 服务端往客户端推\n  rpc SumMany (AddArgs) returns (stream Num) {\n  }\n\n  // 客户端往服务端推\n  rpc AddMany (stream AddArgs) returns (Num) {\n  }\n}\n\n```\n固定顺序的客户端调用方式如下：\n```typescript\n// 保证顺序的双向流\nconst t = service.addMore();\n\nconst result4 = await new Promise<number>((resolve, reject) => {\n\n  let total = 0;\n\n  // 第一次调用和返回\n  t.sendMessage({\n    num: 2\n  })\n    .then(res => {\n      expect(res.num).toEqual(12);\n      total += res.num;\n  \t})\n    .catch(err => console.error(err));\n\n  // 第二次调用和返回\n  t.sendMessage({\n    num: 5\n  }).then(res => {\n      expect(res.num).toEqual(15);\n      total += res.num;\n      resolve(total);\n    })\n    .catch(err => console.error(err));\n\n  t.end();\n});\n\n// result4 => 27\n```\n默认的 id 为 `id` ，如果服务端定义不同，需要修改，可以在客户端调用时传递。\n```typescript\n// 保证顺序的双向流\nconst t = service.addMore({\n  messageKey: 'uid'\n});\n```\n\n\n## 元数据（Metadata）\n\n\ngRPC 的元数据等价于 HTTP 的上下文。\n\n\n服务端通过 `ctx.sendMetadata` 方法返回元数据，也可以通过 `ctx.metadata` 获取客户端传递的元数据。\n```typescript\nimport {\n  MSProviderType,\n  Provider,\n  GrpcMethod,\n} from '@midwayjs/core';\nimport { helloworld } from '../domain/helloworld';\nimport { Context, Metadata } from '@midwayjs/grpc';\n\n/**\n * 实现 helloworld.Greeter 接口的服务\n */\n@Provider(MSProviderType.GRPC, { package: 'helloworld' })\nexport class Greeter implements helloworld.Greeter {\n\n  @Inject()\n  ctx: Context;\n\n  @GrpcMethod()\n  async sayHello(request: helloworld.HelloRequest) {\n\n    // 客户端传递的元数据\n    console.log(this.ctx.metadata);\n\n    // 创建元数据\n    const meta = new Metadata();\n    this.ctx.metadata.add('xxx', 'bbb');\n    this.ctx.sendMetadata(meta);\n\n    return { message: 'Hello ' + request.name };\n  }\n}\n```\n\n\n客户端通过方法的 options 参数传递元数据。\n```typescript\nimport { Metadata } from '@midwayjs/grpc';\n\nconst meta = new Metadata();\nmeta.add('key', 'value');\n\nconst result = await service.sayHello({\n  metadata: meta,\n}).sendMessage({\n  name: 'harry'\n});\n```\n\n\n获取元数据相对麻烦一些。\n\n\n普通一元调用（UnaryCall）获取元数据需要使用 `sendMessageWithCallback` 方法。\n```typescript\nconst call = service.sayHello().sendMessageWithCallback({\n  name: 'zhangting'\n}, (err) => {\n  if (err) {\n    reject(err);\n  }\n});\ncall.on('metadata', (meta) => {\n  // output meta\n});\n```\n其他流式服务，可以通过 `getCall()` 方法获取原始客户端流对象，从而直接订阅。\n```typescript\n// 获取服务，注意，这里没有 await\nconst call = service.addMany().getCall();\ncall.on('metadata', (meta) => {\n  // output meta\n});\n```\n\n\n## 超时处理\n\n\n我们可以在调用服务时传递参数，单位毫秒。\n```typescript\nconst result = await service.sayHello({\n  timeout: 5000\n}).sendMessage({\n  name: 'harry'\n});\n```\n","site/docs/extensions/http-proxy.md":"# HTTP 代理\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的 HTTP 请求代理组件，支持 GET、POST 等多种请求方法。\n\n相关信息：\n\n| web 支持情况      |     |\n| ----------------- | --- |\n| @midwayjs/koa     | ✅  |\n| @midwayjs/faas    | 💬 |\n| @midwayjs/web     | ✅  |\n| @midwayjs/express | ✅  |\n\n:::caution\n\n💬 部分函数计算平台不支持流式请求响应，请参考对应平台能力。\n\n:::\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/http-proxy@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/http-proxy\": \"^4.0.0\"\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n## 启用组件\n\n在 `src/configuration.ts` 中引入组件\n\n```typescript\n// ...\nimport * as proxy from '@midwayjs/http-proxy';\n\n@Configuration({\n  imports: [\n    // ...other components\n    proxy,\n  ],\n})\nexport class MainConfiguration {}\n```\n\n## 配置\n\n代理配置定义如下：\n\n```typescript\n// 代理配置类型\nexport interface HttpProxyConfig {\n  // 匹配要代理的 URL 正则表达式\n  match: RegExp;\n  // 替换匹配到的链接的 host，将请求代理到此地址\n  host?: string;\n  // 通过正则的表达式捕获组处理代理地址\n  target?: string;\n  // 转发请求超时时间，默认为0不设置超时时间\n  proxyTimeout?: number;\n  // 忽略代理请求转发的 header 中的字段\n  ignoreHeaders?: {\n    [key: string]: boolean;\n  };\n}\n```\n\n代理支持单个代理和多个代理。\n\n单个代理配置\n\n```typescript\n// src/config/config.default.ts\n\nexport default {\n  httpProxy: {\n    match: /\\/tfs\\//,\n    host: 'https://gw.alicdn.com',\n  },\n};\n```\n\n多个代理配置\n\n```typescript\n// src/config/config.default.ts\n\n// 代理配置类型\nexport default {\n  httpProxy: {\n    default: {\n      // 一些每个策略复用的值，会和底下的策略进行合并\n    },\n    strategy: {\n      gw: {\n        // https://gw.alicdn.com/tfs/TB1.1EzoBBh1e4jSZFhXXcC9VXa-48-48.png\n        match: /\\/tfs\\//,\n        host: 'https://gw.alicdn.com',\n      },\n      g: {\n        // https://g.alicdn.com/mtb/lib-mtop/2.6.1/mtop.js\n        match: /\\/bdimg\\/(.*)$/,\n        target: 'https://sm.bdimg.com/$1',\n      },\n      httpBin: {\n        // https://httpbin.org/\n        match: /\\/httpbin\\/(.*)$/,\n        target: 'https://httpbin.org/$1',\n      },\n    },\n  },\n};\n```\n\n## 示例：使用 host 配置代理\n\n```typescript\nexport default {\n  httpProxy: {\n    match: /\\/tfs\\//,\n    host: 'https://gw.alicdn.com',\n  },\n};\n```\n\n当请求您的站点路径为： `https://yourdomain.com/tfs/test.png` 时，`match` 字段配置的正则表达式成功匹配，那么就将原始请求路径中的 `host` 部分 `https://yourdomain.com` 替换为配置的 `https://gw.alicdn.com`，从而发起代理请求到 `https://gw.alicdn.com/tfs/test.png`，并把响应结果返回给请求您站点的用户。\n\n## 示例：使用 target 配置代理\n\n```typescript\nexport default {\n  httpProxy: {\n    match: /\\/httpbin\\/(.*)$/,\n    target: 'https://httpbin.org/$1',\n  },\n};\n```\n\n当请求您的站点路径为： `https://yourdomain.com/httpbin/get?name=midway` 时，`match` 字段配置的正则表达式成功匹配，同时正则的捕获组中有结果 `['get?name=midway']` ，那么就将原始请求路径中的 `$1` 部分替换为捕获组中的第 1 个数据（index: 0）的 `get?name=midway`，从而发起代理请求到 `https://httpbin.org/get?name=midway`，并把响应结果返回给请求您站点的用户。\n","site/docs/extensions/i18n.md":"# 多语言\n\nMidway 提供了多语言组件，让业务可以快速指定不同的语言，展示不同的文案，也可以在 HTTP 场景配合请求参数，请求头等方式来使用。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n## 安装组件\n\n```bash\n$ npm i @midwayjs/i18n@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/i18n\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 使用组件\n\n将 i18n 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as i18n from '@midwayjs/i18n';\n\n@Configuration({\n  imports: [\n    // ...\n    i18n\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n\n\n## 使用\n\n组件提供了 `MidwayI18nService`  服务，用于翻译多语言文本。\n\n使用 `translate` 方法，传入不同的文本关键字和参数，返回不同语言的文本内容。\n\n```typescript\n@Controller('/')\nexport class UserController {\n\n  @Inject()\n  i18nService: MidwayI18nService;\n\n  @Get('/')\n  async index(@Query('username') username: string) {\n    return this.i18nService.translate('HELLO_MESSAGE', {\n      args: {\n        username\n      },\n    });\n  }\n}\n```\n\n\n\n## 配置多语言文案\n\n你可以在配置文件中直接配置，但是大多数情况下，文案会很多，有时候甚至可能文案在远端服务上，这个时候直接配置就不太现实。\n\n一般来说，我们会将文案单独放到某个文案配置目录中，比如 `src/locales` 。\n\n以 `src/locale` 这个目录为例，我们举个例子，结构如下：\n\n```text\n.\n├── src\n│   ├── locales\n|   │   ├── en_US.json\n|   │   └── zh_CN.json\n│   └── controller\n│       └── home.controller.ts\n├── package.json\n└── tsconfig.json\n```\n\n这里我们建了两个多语言的文件，`en_US.json` 和 `zh_CN.json`，分别代表英文和中文。\n\n文件内容分别如下：\n\n```json\n// src/locales/en_US.json\n{\n  \"hello\": \"Hello {username}\",\n  \"email\": \"email id\",\n  \"login\": \"login account\",\n  \"createdAt\": \"register date\"\n}\n```\n\n```json\n// src/locales/zh_CN.json\n{\n  \"hello\": \"你好 {username}\",\n  \"email\": \"邮箱\",\n  \"login\": \"帐号\",\n  \"createdAt\": \"注册时间\"\n}\n```\n\n每行一个字符串对，是一个标准的 JSON 格式内容，也可以使用 js/ts 文件，花括号中是可替换的参数占位。\n\n同时，需要在配置中加入这两个 JSON，其中 `default` 是语言的默认分组。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    // 把你的翻译文本放到这里\n    localeTable: {\n      en_US: {\n        default: require('../locale/en_US'),\n      },\n      zh_CN: {\n        default: require('../locale/zh_CN'),\n      }\n    },\n  }\n}\n```\n\n这样就可以使用了，使用输出如下。\n\n```typescript\nthis.i18nService.translate('hello', {\n  args: {\n    username: 'harry',\n  },\n  locale: 'en_US',\n});\n\n// output: Hello harry.\n\nthis.i18nService.translate('hello', {\n  args: {\n    username: 'harry',\n  },\n  locale: 'zh_CN',\n});\n\n// output: 你好 harry.\n\n```\n\n\n\n## 多语言文案分组\n\n在如下配置中，用户配置的多语言文案在 `default` 分组中。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    // 把你的翻译文本放到这里\n    localeTable: {\n      en_US: {\n        default: require('../locale/en_US'),\n      },\n      zh_CN: {\n        default: require('../locale/zh_CN'),\n      }\n    },\n  }\n}\n```\n\n这样做的好处是，在其他组件或者业务代码中，我们也可以使用不同的分组名，来添加其他的多语言文案。\n\n比如：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    // 把你的翻译文本放到这里\n    localeTable: {\n      en_US: {\n        default: require('../locale/en_US'),\n        user: require('../locale/user_en_US'),\n      },\n      zh_CN: {\n        default: require('../locale/zh_CN'),\n        user: require('../locale/user_zh_CN'),\n      }\n    },\n  }\n}\n```\n\n在代码中，如果调用非默认分组，需要指定分组参数。\n\n```typescript\nthis.i18nService.translate('user.hello', {\n  args: {\n    username: 'harry',\n  },\n  group: 'user',\t\t// 指定其他分组\n  locale: 'en_US',\n});\n\n```\n\n\n\n## 多语言文案格式\n\n多语言文本中可以添加参数，参数可以有 `对象` 和 `数组` 两种形式。\n\n对象形式如下，使用花括号作为占位符。\n\n```text\nHello {username}\n```\n\n使用时，通过配置传递，按对象 key 覆盖变量。\n\n```typescript\nasync index(@Query('username') username: string) {\n  return this.i18nService.translate('hello', {\n    args: {\n      username\n    },\n  });\n}\n```\n\n数组形式如下，使用数字作为占位符。\n\n```text\nHello {0}\n```\n\n使用时，通过配置传递，格式是数组形式，按数组顺序覆盖数字变量。\n\n```typescript\nasync index(@Query('username') username: string) {\n  return this.i18nService.translate('hello', {\n    args: [username]\n  });\n}\n```\n\n\n\n## 动态添加多语言文案\n\n有时候，多语言文案可能放在远端，比如数据库等，我们可以通过 `addLocale` 方法进行动态添加。\n\n比如，在配置加载后，代码使用前。\n\n```typescript\n// configuration.ts\n\n// ...\n@Configuration({\n  imports: [\n    koa,\n    i18n\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  i18nService: MidwayI18nService;\n\n  async onReady() {\n    this.i18nService.addLocale('zh_TW', {\n      hello: '你好，{username} 美麗的世界'\n    });\n  }\n\n\n  // ...\n}\n```\n\n在代码中就可以使用。\n\n```typescript\nasync index(@Query('username') username: string) {\n  return this.i18nService.translate('hello', {\n    args: [username],\n    locale: 'zh_TW'\n  });\n}\n```\n\n\n\n## 通过参数指定当前语言\n\n一般情况下，默认语言为 `en_US`，用户的浏览器访问一般会自带 `Accept-Language` 头，所以会正确识别语言。比如用中文浏览器访问，就能正常显示中文。\n\n除此之外，在 HTTP 场景下可以通过 URL Query，Cookie，Header 来指定语言。\n\n优先级从上到下：\n\n- query: /?locale=en-US\n- cookie: locale=zh-TW\n- header: Accept-Language: zh-CN,zh;q=0.5\n\n当传递了这些参数之后，多语言数据会自动保存到当前用户的 Cookie 中，下次请求会直接用该设定好的语言。\n\n\n\n## 手动设置语言\n\n可以通过调用 `saveRequestLocale` 设置当前语言。\n\n```typescript\nasync index() {\n  // ...\n  this.i18nService.saveRequestLocale('zh_CN');\n}\n```\n\n如果开启了 `writeCookie` 配置，设置后会保存到当前用户的 Cookie 中，下次请求会使用该设置。\n\n\n\n## 语言选择优先级\n\n这些多种设置语言的方式，有着不同的优先级，如下优先级从高到低：\n\n- 1、`i18nService.translate` 方法显式指定的语言\n- 2、通过其他装饰器设置的语言，比如 `@Validate` 装饰器的参数（本质是调用了`i18nService.translate` 方法）\n- 3、通过 `saveRequestLocale` API 直接设置的当前语言\n- 4、通过浏览器 Query，Cookie，Header 设置的语言（本质是调用了 `saveRequestLocale`）\n- 5、i18n 组件配置中的默认语言\n\n\n\n## 关于语言大小写\n\n在代码内部，我们会将所有的多语言，fallback 规则，写入的文本串，返回的 locale 结果，使用下面的规则替代\n\n- 1、使用中划线代替下划线\n- 2、使用小写代替大写\n\n即所有的 `en_US` 都会变成 `en-us`，`zh_CN` 会变成 `zh-cn`。\n\n这样做会安全的适配 URL 和 Cookie。\n\n\n\n## View 中使用\n\n在 Web 类型的框架中，我们默认添加了 locals 变量支持，可以在模板引擎中使用。\n\n假设我们使用的模板引擎是 [Nunjucks](./render)，可以直接引用到 `i18n` 方法。\n\n多语言文案如下：\n\n```json\n{\n  \"hello\": \"Hello {username}\",\n}\n```\n\n模板如下：\n\n```html\n<span>{{ i18n('hello', user) }}</span>\n```\n\n示例如下：\n\n```typescript\n// ...\n\n@Controller('/')\nexport class UserController {\n\n  @Inject()\n  ctx: Context;\n\n  @Get('/')\n  async index() {\n    await this.ctx.render('index', {\n      // 注意这里是整个对象传递给模板\n      user: {\n        username: 'harry',\n      }\n    });\n  }\n}\n```\n\ni18n 方法定义如下：\n\n```typescript\nfunction i18n(templateName: string, args: Record<string, any>) {\n  // ...\n}\n```\n\n方法名可以通过配置修改。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    localsField: 'i18n',\n  }\n}\n```\n\n\n\n\n\n## 配置\n\n### 默认配置\n\n大部分情况下，你只需要在配置 `localeTable` 添加你自己的多语言翻译即可。\n\n下面是完整的配置，你可以在配置定义中找到。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    // 默认语言  \"en_US\"\n    defaultLocale: 'en_US',\n\n    // 把你的翻译文本放到这里\n    localeTable: {\n      en_US: {\n        // group name\n        default: {\n          // hello: 'hello'\n        }\n      },\n      zh_CN: {\n        // group name\n        default: {\n          // hello: '你好'\n        }\n      },\n    },\n\n    // 语言映射，可以用 * 号通配\n    fallbacks: {\n      //   'en_*': 'en_US',\n      //   pt: 'pt-BR',\n    },\n    // 是否将请求参数写入 cookie\n    writeCookie: true,\n    resolver: {\n      // url query 参数，默认是 \"locale\"\n      queryField: 'locale',\n      cookieField: {\n        // Cookie 里的 key，默认是 \"locale\"\n        fieldName: 'locale',\n        // Cookie 域名，默认为空，代表当前域名有效\n        cookieDomain: '',\n        // Cookie 默认的过期时间，默认一年\n        cookieMaxAge: FORMAT.MS.ONE_YEAR,\n      },\n    },\n    localsField: 'i18n',\n    // 缺失翻译键的处理函数，默认返回原始键名\n    missingKeyHandler: (message, options) => message,\n  }\n}\n```\n\n\n\n### 回写 Cookie\n\n默认情况下，多语言组件会将当前用户的语言回写到 Cookie 中，避免下次请求再进行查找以提高性能，我们可以通过配置关闭这个行为。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    writeCookie: false,\n  }\n}\n```\n\n\n\n### 请求解析配置\n\nHTTP 场景下，我们提供了通过参数指定当前语言的能力。\n\n默认情况下，组件通过下面的字段来查找。\n\n- query 的 `locale` 字段\n- cookie 的 `locale` 字段\n- header 的 `Accept-Language` 部分\n\n我们可以通过配置修改查询的字段。\n\n比如，修改 Query 的字段。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    resolver: {\n      queryField: 'abc'\n    },\n  }\n}\n```\n\n我们就可以通过 `/?abc=en-US` 来请求修改语言。\n\n如果不希望通过请求来设置语言，可以将整个 `resolver` 解析关闭，对 Cookie 的回写也将同时停止。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    resolver: false,\n  }\n}\n```\n\n\n### 缺失翻译键处理\n\n当翻译键在所有语言文件中都找不到时，i18n 组件会调用 `missingKeyHandler` 函数来处理这种情况。\n\n默认情况下，当找不到翻译时，会返回原始的键名：\n\n```typescript\n// 默认的 missingKeyHandler\nmissingKeyHandler: (message, options) => message\n```\n\n例如：\n```typescript\n// 假设 \"unknown_key\" 在任何语言文件中都不存在\nthis.i18nService.translate('unknown_key');\n// 返回: \"unknown_key\"\n```\n\n你可以自定义缺失键的处理方式，比如添加特殊标记、记录日志等：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  i18n: {\n    missingKeyHandler: (message, options) => {\n      // 记录缺失的翻译键\n      console.warn(`Missing translation: ${message}`, options);\n\n      // 根据不同的组返回不同的格式\n      const group = options?.group || 'default';\n      const locale = options?.locale || 'default';\n\n      return `[${group}:${locale}:${message}]`;\n    }\n  }\n}\n```\n\n\n## 常用语言\n\n| 语言             | 语言包名 |\n| :--------------- | :------- |\n| 阿拉伯           | ar_EG    |\n| 亞美尼亞         | hy_AM    |\n| 保加利亚语       | bg_BG    |\n| 加泰罗尼亚语     | ca_ES    |\n| 捷克语           | cs_CZ    |\n| 丹麦语           | da_DK    |\n| 德语             | de_DE    |\n| 希腊语           | el_GR    |\n| 英语             | en_GB    |\n| 英语（美式）     | en_US    |\n| 西班牙语         | es_ES    |\n| 爱沙尼亚语       | et_EE    |\n| 波斯语           | fa_IR    |\n| 芬兰语           | fi_FI    |\n| 法语（比利时）   | fr_BE    |\n| 法语             | fr_FR    |\n| 希伯来语         | he_IL    |\n| 印地语           | hi_IN    |\n| 克罗地亚语       | hr_HR    |\n| 匈牙利           | hu_HU    |\n| 冰岛语           | is_IS    |\n| 印度尼西亚语     | id_ID    |\n| 意大利语         | it_IT    |\n| 日语             | ja_JP    |\n| 格鲁吉亚语       | ka_GE    |\n| 卡纳达语         | kn_IN    |\n| 韩语/朝鲜语      | ko_KR    |\n| 库尔德语         | ku_IQ    |\n| 拉脱维亚语       | lv_LV    |\n| 马来语           | ms_MY    |\n| 蒙古语           | mn_MN    |\n| 挪威             | nb_NO    |\n| 尼泊尔语         | ne_NP    |\n| 荷兰语（比利时） | nl_BE    |\n| 荷兰语           | nl_NL    |\n| 波兰语           | pl_PL    |\n| 葡萄牙语(巴西)   | pt_BR    |\n| 葡萄牙语         | pt_PT    |\n| 斯洛伐克语       | sk_SK    |\n| 塞尔维亚         | sr_RS    |\n| 斯洛文尼亚       | sl_SI    |\n| 瑞典语           | sv_SE    |\n| 泰米尔语         | ta_IN    |\n| 泰语             | th_TH    |\n| 土耳其语         | tr_TR    |\n| 罗马尼亚语       | ro_RO    |\n| 俄罗斯语         | ru_RU    |\n| 乌克兰语         | uk_UA    |\n| 越南语           | vi_VN    |\n| 简体中文         | zh_CN    |\n| 繁体中文         | zh_TW    |\n\n\n\n## 常见问题\n\n### 1、测试配置全局语言不生效\n\n一般场景下，你 **无需** 配置全局语言，因为浏览器访问会自动带上语言信息，比如中文浏览器自动返回中文，英文浏览器自动返回英文。\n\n假如你明确希望测试全局设置的效果，请务必按下面操作：\n\n* 1、如果使用的是浏览器，请清空页面 cookie 再访问，因为 cookie 中会记录上一次用户的语言信息\n* 2、如果使用的是 Postman 等工具，请不要带上 cookie 以及语言相关的 Header，Query 等字段\n\n### 2、测试返回的语言非预期\n\n请使用浏览器进行测试，不要使用 Postman。\n\n由于 Postman 请求不会带有浏览器语言相关的 Header，所以服务端无法自动判断语言。\n\n如果你一定要使用 Postman，请参考浏览器请求，加上 `Accept-Language` Header。\n","site/docs/extensions/info.md":"# 信息查看\n\nMidway 提供了 info 组件，用于展示应用的基本信息，方便排查问题。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/info@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/info\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 使用组件\n\n将 info 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as info from '@midwayjs/info';\n\n@Configuration({\n  imports: [\n    // ...\n    info\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n在有些情况下，为了不想让应用信息透出，我们指定在特殊环境下生效。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as info from '@midwayjs/info';\n\n@Configuration({\n  imports: [\n    koa,\n    {\n      component: info,\n      enabledEnvironment: ['local'],\t// 只在本地启用\n    }\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n\n\n## 查看信息\n\n在默认情况下，info 组件会为 Http 场景自动添加一个中间件，我们可以通过 `/_info` 来访问。\n\n默认情况下，会展示系统，进程，以及配置等关键信息。\n\n效果如下：\n\n![info](https://img.alicdn.com/imgextra/i3/O1CN01TCkSvr28x8T7gtnCl_!!6000000007998-2-tps-797-1106.png)\n\n\n\n## 修改访问路由\n\n为了安全，我们可以调整访问的路由。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  info: {\n    infoPath: '/_my_info',\n  }\n}\n```\n\n\n\n## 隐藏信息\n\n默认情况下，info 组件会隐藏秘钥等信息。我们可以配置增减隐藏的关键字，这个配置会对 **环境变量** 以及 **多环境配置** 生效。\n\n关键字可以使用通配符，比如增加一些关键字。\n\n```typescript\n// src/config/config.default.ts\nimport { DefaultHiddenKey } from '@midwayjs/info';\n\nexport default {\n  // ...\n  info: {\n    hiddenKey: DefaultHiddenKey.concat(['*abc', '*def', '*bbb*']),\n  }\n}\n```\n\n\n\n## 调用 API\n\ninfo 组件默认提供了 `InfoService` 用于在非 Http 或是自定义的场景来使用。\n\n比如：\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InfoService } from '@midwayjs/info';\n\n@Provide()\nexport class userService {\n\n  @Inject()\n  inforService: InfoService\n\n  async getInfo() {\n    // 应用信息，应用名等\n    this.inforService.projectInfo();\n    // 系统信息\n    this.inforService.systemInfo();\n    // 堆内存，cpu 等\n    this.inforService.resourceOccupationInfo();\n    // midway 框架的信息\n    this.inforService.softwareInfo();\n    // 当前使用的环境配置\n    this.inforService.midwayConfig();\n    // 依赖注入容器中的服务\n    this.inforService.midwayService();\n    // 系统时间，时区，启动时常\n    this.inforService.timeInfo();\n    // 环境变量\n    this.inforService.envInfo();\n    // 依赖信息\n    this.inforService.dependenciesInfo();\n    // 网络信息\n    this.inforService.networkInfo();\n  }\n}\n```\n\n","site/docs/extensions/jwt.md":"# JWT\n\n`JSON Web Token` (JWT)是一个开放标准(RFC 7519)，它定义了一种紧凑的、自包含的方式，用于作为`JSON`对象在各方之间安全地传输信息。该信息可以被验证和信任，因为它是数字签名的。\n\nMidway 提供了 jwt 组件，简单提供了一些 jwt 相关的 API，可以基于它做独立的鉴权和校验。\n\n相关信息：\n\n| 描述              |     |\n| ----------------- | --- |\n| 可用于标准项目    | ✅  |\n| 可用于 Serverless | ✅  |\n| 可用于一体化      | ✅  |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/jwt@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/jwt\": \"^4.0.0\"\n    // ...\n  },\n}\n```\n\n## 使用组件\n\n将 jwt 组件配置到代码中。\n\n```typescript\nimport { Configuration, IMidwayContainer } from '@midwayjs/core';\nimport { IMidwayContainer } from '@midwayjs/core';\nimport * as jwt from '@midwayjs/jwt';\n\n@Configuration({\n  imports: [\n    // ...\n    jwt,\n  ],\n})\nexport class MainConfiguration {\n  // ...\n}\n```\n\n## 基础配置\n\n然后在配置中设置，默认未加密。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  jwt: {\n    secret: 'xxxxxxxxxxxxxx', // fs.readFileSync('xxxxx.key')\n    sign: {\n      // signOptions\n      expiresIn: '2d', // https://github.com/vercel/ms\n    },\n    verify: {\n      // verifyOptions\n    },\n    decode: {\n      // decodeOptions\n    }\n  },\n};\n```\n\n更多配置请查看 ts 定义。\n\n## 常用 API\n\nMidway 将 jwt 常用 API 提供为同步和异步两种形式。\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { JwtService } from '@midwayjs/jwt';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  jwtService: JwtService;\n\n  async invoke() {\n    // 同步 API\n    this.jwtService.signSync(payload, secretOrPrivateKey, options);\n    this.jwtService.verifySync(token, secretOrPublicKey, options);\n    this.jwtService.decodeSync(token, options);\n\n    // 异步 API\n    await this.jwtService.sign(payload, secretOrPrivateKey, options);\n    await this.jwtService.verify(token, secretOrPublicKey, options);\n    await this.jwtService.decode(token, options);\n  }\n}\n```\n\n这些 API 都来自于 [node-jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) 基础库，如果不了解请阅读原版文档。\n\n## 中间件示例\n\n一般，jwt 还会配合中间件来完成鉴权，下面是一个自定义 jwt 鉴权的中间件示例。\n\n```typescript\n// src/middleware/jwt.middleware\n\nimport { Inject, Middleware, httpError } from '@midwayjs/core';\nimport { Context, NextFunction } from '@midwayjs/koa';\nimport { JwtService } from '@midwayjs/jwt';\n\n@Middleware()\nexport class JwtMiddleware {\n  @Inject()\n  jwtService: JwtService;\n\n  public static getName(): string {\n    return 'jwt';\n  }\n\n  resolve() {\n    return async (ctx: Context, next: NextFunction) => {\n      // 判断下有没有校验信息\n      if (!ctx.headers['authorization']) {\n        throw new httpError.UnauthorizedError();\n      }\n      // 从 header 上获取校验信息\n      const parts = ctx.get('authorization').trim().split(' ');\n\n      if (parts.length !== 2) {\n        throw new httpError.UnauthorizedError();\n      }\n\n      const [scheme, token] = parts;\n\n      if (/^Bearer$/i.test(scheme)) {\n        try {\n          //jwt.verify方法验证token是否有效\n          await this.jwtService.verify(token, {\n            complete: true,\n          });\n        } catch (error) {\n          //token过期 生成新的token\n          const newToken = getToken(user);\n          //将新token放入Authorization中返回给前端\n          ctx.set('Authorization', newToken);\n        }\n        await next();\n      }\n    };\n  }\n\n  // 配置忽略鉴权的路由地址\n  public match(ctx: Context): boolean {\n    const ignore = ctx.path.indexOf('/api/admin/login') !== -1;\n    return !ignore;\n  }\n}\n```\n\n然后在入口启用中间件即可。\n\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration, App, IMidwayContainer, IMidwayApplication} from '@midwayjs/core';\nimport * as jwt from '@midwayjs/jwt';\n\n@Configuration({\n  imports: [\n    // ...\n    jwt,\n  ],\n})\nexport class MainConfiguration {\n\n  @App()\n  app: IMidwayApplication;\n\n  async onReady(applicationContext: IMidwayContainer): Promise<void> {\n    // 添加中间件\n    this.app.useMiddleware([\n      // ...\n      JwtMiddleware,\n    ]);\n  }\n}\n```\n\n\n\n## 原始 JWT 对象\n\n可以通过导出的 `Jwt` 对象引用到原始实例上的对象和方法。\n\n```typescript\nimport { Jwt } from '@midwayjs/jwt';\n\n// Jwt.TokenExpiredError\n```\n","site/docs/extensions/kafka.md":"# Kafka\n\n在复杂系统的架构中，事件流是很重要的一环，包括从事件源中(数据库、传感器、移动设备等)以事件流的方式去实时捕获数据，持久化事件流方便检索，并实时和回顾操作处理响应事件流。\n\n应用于支付和金融交易、实施跟踪和监控汽车等行业信息流动、捕获分析物联网数据等等。\n\n\n在 Midway中，我们提供了订阅 Kafka 的能力，专门来满足用户的这类需求。\n\n相关信息：\n\n**订阅服务**\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ✅     |\n| 包含独立日志      | ✅     |\n\n\n\n## 基础概念\n\n\n分布式流处理平台\n* 发布订阅（流）信息\n* 容错（故障转移）存储信息（流），存储事件流\n* 在消息流发生的时候进行处理，处理事件流\n\n理解 Producer（生产者）\n\n* 发布消息到一个主题或多个 topic (主题)。\n\n理解 Consumer（主题消费者）\n* 订阅一个或者多个 topic,并处理产生的信息。\n\n理解 Stream API\n* 充当一个流处理器，从 1 个或多个 topic 消费输入流，并生产一个输出流到1个或多个输出 topic，有效地将输入流转换到输出流。\n\n理解 Broker\n* 已发布的消息保存在一组服务器中，称之为 Kafka 集群。集群中的每一个服务器都是一个代理（Broker）。 消费者可以订阅一个或多个主题（topic），并从Broker拉数据，从而消费这些已发布的消息。\n\n\n![image.png](https://kafka.apache.org/images/streams-and-tables-p1_p4.png)\n\n:::tip\n从 v3.19 开始，Kafka 组件做了一次重构，Kafka 组件的配置、使用方法和之前都有较大差异，原有使用方式兼容，但是文档不再保留。\n:::\n\n\n## 安装依赖\n\n\n安装 `@midwayjs/kafka` 模块。\n\n```bash\n$ npm i @midwayjs/kafka --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/kafka\": \"^4.0.0\",\n    // ...\n  }\n}\n```\n\n## 开启组件\n\n`@midwayjs/kafka` 可以作为独立主框架使用。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as kafka from '@midwayjs/kafka';\n\n@Configuration({\n  imports: [\n    kafka\n  ],\n  // ...\n})\nexport class MainConfiguration {\n  async onReady() {\n        // ...\n  }\n}\n```\n\n也可以附加在其他的主框架下，比如 `@midwayjs/koa` 。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as kafka from '@midwayjs/kafka';\n\n@Configuration({\n  imports: [\n    koa,\n    kafka\n  ],\n  // ...\n})\nexport class MainConfiguration {\n  async onReady() {\n        // ...\n  }\n}\n```\n\n由于 Kafka 分为 **消费者（Consumer）** 和 **生产者（Producer）** 两部分，两个可以独立使用，我们将分别介绍。\n\n## 消费者（Consumer）\n\n### 目录结构\n\n\n我们一般把消费者放在 consumer 目录。比如 `src/consumer/user.consumer.ts`  。\n```\n➜  my_midway_app tree\n.\n├── src\n│   ├── consumer\n│   │   └── user.consumer.ts\n│   ├── interface.ts\n│   └── service\n│       └── user.service.ts\n├── test\n├── package.json\n└── tsconfig.json\n```\n\n\n### 基础配置\n\n通过 `consumer` 字段和 `@KafkaConsumer` 装饰器，我们可以配置多个消费者。\n\n比如，下面的 `sub1` 和 `sub2` 就是两个不同的消费者。\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    consumer: {\n      sub1: {\n        // ...\n      },\n      sub2: {\n        // ...\n      },\n    }\n  }\n}\n```\n\n最简单的消费者配置需要几个字段，Kafka 的连接配置、消费者配置以及订阅配置。\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    consumer: {\n      sub1: {\n        connectionOptions: {\n          // ...\n        },\n        consumerOptions: {\n          // ...\n        },\n        subscribeOptions: {\n          // ...\n        },\n      },\n    }\n  }\n}\n```\n\n比如：\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    consumer: {\n      sub1: {\n        connectionOptions: {\n          clientId: 'my-app',\n          brokers: ['localhost:9092'],\n        },\n        consumerOptions: {\n          groupId: 'groupId-test-1',\n        },\n        subscribeOptions: {\n          topics: ['topic-test-1'],\n        }\n      },\n    }\n  }\n}\n```\n\n完整可配置参数包括：\n\n- `connectionOptions`：Kafka 的连接配置，即 `new Kafka(consumerOptions)` 的参数\n- `consumerOptions`：Kafka 的消费者配置，即 `kafka.consumer(consumerOptions)` 的参数\n- `subscribeOptions`：Kafka 的订阅配置，即 `consumer.subscribe(subscribeOptions)` 的参数\n- `consumerRunConfig`：消费者运行配置，即 `consumer.run(consumerRunConfig)` 的参数\n\n这些参数的详细说明，可以参考 [KafkaJS Consumer](https://kafka.js.org/docs/consuming) 文档。\n\n### 复用 Kafka 实例\n\n如果如果需要复用 Kafka 实例，可以通过 `kafkaInstanceRef` 字段来指定。\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    consumer: {\n      sub1: {\n        connectionOptions: {\n          clientId: 'my-app',\n          brokers: ['localhost:9092'],\n        },\n        consumerOptions: {\n          groupId: 'groupId-test-1',\n        },\n        subscribeOptions: {\n          topics: ['topic-test-1'],\n        }\n      },\n      sub2: {\n        kafkaInstanceRef: 'sub1',\n        consumerOptions: {\n          groupId: 'groupId-test-2',\n        },\n        subscribeOptions: {\n          topics: ['topic-test-2'],\n        }\n      }\n    }\n  }\n}\n```\n\n注意，上述的 `sub1` 和 `sub2` 是两个不同的消费者，但是它们共享同一个 Kafka 实例，且 `sub2` 的 `groupId` 需要和 `sub1` 不同。\n\n用 Kafka SDK 写法类似如下：\n\n```typescript\nconst kafka = new Kafka({\n  clientId: 'my-app',\n  brokers: ['localhost:9092'],\n});\n\nconst consumer1 = kafka.consumer({ groupId: 'groupId-test-1' });\nconst consumer2 = kafka.consumer({ groupId: 'groupId-test-2' });\n```\n\n### 消费者实现\n\n我们可以在目录中提供一个标准的消费者实现，比如 `src/consumer/sub1.consumer.ts`。\n\n```typescript\n// src/consumer/sub1.consumer.ts\nimport { KafkaConsumer, IKafkaConsumer, EachMessagePayload } from '@midwayjs/kafka';\n\n@KafkaConsumer('sub1')\nclass Sub1Consumer implements IKafkaConsumer {\n  async eachMessage(payload: EachMessagePayload) {\n    // ...\n  }\n}\n```\n\n`sub1` 是消费者名称，使用的是配置中的 `sub1` 消费者。\n\n也可以实现 `eachBatch` 方法，处理批量消息。\n\n```typescript\n// src/consumer/sub1.consumer.ts\nimport { KafkaConsumer, IKafkaConsumer, EachBatchPayload } from '@midwayjs/kafka';\n\n@KafkaConsumer('sub1')\nclass Sub1Consumer implements IKafkaConsumer {\n  async eachBatch(payload: EachBatchPayload) {\n    // ...\n  }\n}\n```\n\n\n### 消息上下文\n\n\n和其他消息订阅机制一样，消息本身通过 `Context` 字段来传递。\n\n```typescript\n// src/consumer/sub1.consumer.ts\nimport { KafkaConsumer, IKafkaConsumer, EachMessagePayload, Context } from '@midwayjs/kafka';\nimport { Inject } from '@midwayjs/core';\n\n@KafkaConsumer('sub1')\nclass Sub1Consumer implements IKafkaConsumer {\n\n  @Inject()\n  ctx: Context;\n\n  async eachMessage(payload: EachMessagePayload) {\n    // ...\n  }\n}\n```\n\n`Context` 字段包括几个属性：\n\n| 属性        | 类型                           | 描述             |\n| ----------- | ------------------------------ | ---------------- |\n| ctx.payload   | EachMessagePayload, EachBatchPayload | 消息内容         |\n| ctx.consumer  | Consumer             | 消费者实例         |\n\n\n你可以通过 `ctx.consumer` 来调用 Kafka 的 API，比如 `ctx.consumer.commitOffsets` 来手动提交偏移量或者 `ctx.consumer.pause` 来暂停消费。\n\n\n## 生产者（Producer）\n\n### 基础配置\n\n服务生产者也需要创建实例，配置本身使用了 [服务工厂](/docs/service_factory) 的设计模式。\n\n配置如下：\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    producer: {\n      clients: {\n        pub1: {\n          // ...\n        },\n        pub2: {\n          // ...\n        }\n      }\n    }\n  }\n}\n```\n\n每个 Producer 实例的配置，同样包括 `connectionOptions` 和 `producerOptions`。\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    producer: {\n      clients: {\n        pub1: {\n          connectionOptions: {\n            clientId: 'my-app',\n            brokers: ['localhost:9092'],\n          },\n          producerOptions: {\n            // ...\n          }\n        }\n      }\n    }\n  }\n}\n```\n\n具体参数可以参考 [KafkaJS Producer](https://kafka.js.org/docs/producing) 文档。\n\n此外，由于 Kafka Consumer 和 Producer 都可以从同一个 Kafka 实例创建，所以它们可以复用同一个 Kafka 实例。\n\nProducer 后于 Consumer 创建，也同样可以使用 `kafkaInstanceRef` 字段来复用 Kafka 实例。\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    consumer: {\n      sub1: {\n        connectionOptions: {\n          clientId: 'my-app',\n          brokers: ['localhost:9092'],\n        },\n      }\n    },\n    producer: {\n      clients: {\n        pub1: {\n          kafkaInstanceRef: 'sub1',\n        }\n      }\n    }\n  }\n}\n```\n\n### 使用 Producer\n\nProducer 不存在默认实例，由于使用了服务工厂的设计模式，所以可以通过 `@InjectClient()` 来注入。\n\n\n```typescript\n// src/service/user.service.ts\nimport { Provide, InjectClient } from '@midwayjs/core';\nimport { KafkaProducerFactory, Producer } from '@midwayjs/kafka';\n\n@Provide()\nexport class UserService {\n  \n  @InjectClient(KafkaProducerFactory, 'pub1')\n  producer: Producer;\n  \n  async invoke() {\n    await this.producer.send({\n      topic: 'topic-test-1',\n      messages: [{ key: 'message-key1', value: 'hello consumer 11 !' }],\n    });\n  }\n}\n```\n\n## Admin\n\nKafka 的 Admin 功能，可以用来创建、删除、查看主题，查看配置和 ACL 等。\n\n### 基础配置\n\n和 Producer 类似，Admin 也使用了服务工厂的设计模式。\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    admin: {\n      clients: {\n        admin1: {\n          // ...\n        }\n      }\n    }\n  }\n}\n```\n\n同样的，Admin 也可以复用 Kafka 实例。\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    consumer: {\n      sub1: {\n        connectionOptions: {\n          clientId: 'my-app',\n          brokers: ['localhost:9092'],\n        },\n      }\n    },\n    admin: {\n      clients: {\n        admin1: {\n          kafkaInstanceRef: 'sub1',\n        }\n      }\n    }\n  }\n}\n```\n\n### 使用 Admin\n\nAdmin 不存在默认实例，由于使用了服务工厂的设计模式，所以可以通过 `@InjectClient()` 来注入。\n\n```typescript\n// src/service/admin.service.ts\nimport { Provide, InjectClient } from '@midwayjs/core';\nimport { KafkaAdminFactory, Admin } from '@midwayjs/kafka';\n\n@Provide()\nexport class AdminService {\n  \n  @InjectClient(KafkaAdminFactory, 'admin1')\n  admin: Admin;\n}\n```\n\n更多的 Admin 使用方法，可以参考 [KafkaJS Admin](https://kafka.js.org/docs/admin) 文档。\n\n\n## 组件日志\n\nKafka 组件默认使用 `kafkaLogger` 日志，默认会将 `ctx.logger` 记录在 `midway-kafka.log`。\n\n你可以通过配置修改。\n\n```typescript\n// src/config/config.default\nexport default {\n  midwayLogger: {\n    clients: {\n      kafkaLogger: {\n        fileLogName: 'midway-kafka.log',\n      },\n    },\n  },\n}\n```\n\n这个日志的输出格式，我们也可以单独配置。\n\n```typescript\nexport default {\n  kafka: {\n    // ...\n    contextLoggerFormat: info => {\n      const { jobId, from } = info.ctx;\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} ${info.message}`;\n    },\n  }\n}\n```\n\n\n## 获取 KafkaJS 模块\n\nKafkaJS 模块，可以通过 `@midwayjs/kafka` 的 `KafkaJS` 字段来获取。\n\n```typescript\nimport { KafkaJS } from '@midwayjs/kafka';\n\nconst { ConfigResourceTypes } = KafkaJS;\n// ...\n```\n\n## 关于分区的警告\n\n如果你使用的是 KafkaJS 的 v2.0.0 版本，你可能会看到如下的警告：\n\n```\n2024-11-04 23:47:28.228 WARN 31729 KafkaJS v2.0.0 switched default partitioner. To retain the same partitioning behavior as in previous versions, create the producer with the option \"createPartitioner: Partitioners.LegacyPartitioner\". See the migration guide at https://kafka.js.org/docs/migration-guide-v2.0.0#producer-new-default-partitioner for details. Silence this warning by setting the environment variable \"KAFKAJS_NO_PARTITIONER_WARNING=1\" { timestamp: '2024-11-04T15:47:28.228Z', logger: 'kafkajs' }\n```\n\n这个警告是由于 KafkaJS 的 v2.0.0 版本默认使用了新的分区器，如果接受新的分区器行为，但想要关闭这个警告消息，可以通过设置环境变量 `KAFKAJS_NO_PARTITIONER_WARNING=1` 来消除这个警告。\n\n或者显示声明分区器。\n\n```typescript\n// src/config/config.default\nimport { KafkaJS } from '@midwayjs/kafka';\nconst { Partitioners } = KafkaJS;\n\nexport default {\n  kafka: {\n    producer: {\n      clients: {\n        pub1: {\n          // ...\n          producerOptions: {\n            createPartitioner: Partitioners.DefaultPartitioner,\n            // ...\n            createPartitioner: Partitioners.LegacyPartitioner,\n          },\n        },\n      },\n    },\n  }\n}\n```\n\n建议你查看 KafkaJS v2.0.0 的 [迁移指南](https://kafka.js.org/docs/migration-guide-v2.0.0#producer-new-default-partitioner) 了解更多细节。\n\n\n\n## 参考文档\n\n- [KafkaJS](https://kafka.js.org/docs/introduction)\n- [apache kafka官网](https://kafka.apache.org/intro)\n","site/docs/extensions/koa.md":"# Koa\n\nKoa 是一个非常轻量易用的 Web 框架。本章节内容，主要介绍在 Midway 中如何使用 Koa 作为上层框架，并使用自身的能力。\n\nMidway 默认的示例都是基于该包。\n\n`@midwayjs/koa` 包默认使用 `koa@2` 以及集成了 `@koa/router` 作为路由基础能力，并默认内置了 `session` 和 `body-parser` 功能。\n\n| 描述           |      |\n| -------------- | ---- |\n| 包含独立主框架 | ✅    |\n| 包含独立日志   | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/koa@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/koa\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n也可以直接使用脚手架创建示例。\n\n```bash\n# npm v6\n$ npm init midway --type=koa-v4 my_project\n\n# npm v7\n$ npm init midway -- --type=koa-v4 my_project\n```\n\n\n\n## 开启组件\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [koa],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: koa.Application;\n\n  async onReady() {\n\t\t// ...\n  }\n}\n\n```\n\n\n\n## BodyParser\n\n`@midwayjs/koa` 自带  `bodyParser` 功能，默认会解析 `Post` 请求，自动识别 `json` 和 `form` 类型。\n\n如需 text 或者 xml，可以自行配置。\n\n默认的大小限制为 `1mb`，可以单独对每项配置大小。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  bodyParser: {\n    enableTypes: ['json', 'form', 'text', 'xml'],\n    formLimit: '1mb',\n    jsonLimit: '1mb',\n    textLimit: '1mb',\n    xmlLimit: '1mb',\n  },\n}\n```\n\n注意，使用 Postman 做 Post 请求时的类型选择：\n\n![postman](https://img.alicdn.com/imgextra/i4/O1CN01QCdTsN1S347SuzZU5_!!6000000002190-2-tps-1017-690.png)\n\n\n关闭 bodyParser 中间件。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  bodyParser: {\n    enable: false,\n    // ...\n  },\n}\n```\n\n\n## Cookie 和 Session\n\n`@midwayjs/koa` 默认封装了 `cookies` 解析和 `Session` 的支持，可以查看 [Cookies 和 Session](../cookie_session)。\n\n\n\n## 扩展 Context\n\n在一些场景下，需要对 Context 做扩展。\n\n如果希望挂在一些临时的请求相关的对象数据，可以使用 `ctx.setAttr(key, value)` API 来实现，比如组件里自用的数据。\n\n如果实在有扩展 Context 的诉求，可以使用 koa 自带的 API。\n\n比如，我们在 `configuration.ts` 中做扩展提供了一个 `render()` 方法。\n\n```typescript\nimport { App, Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\n\n@Configuration({\n\t// ...\n})\nexport class MainConfiguration {\n  @App()\n  app: koa.Application;\n\n  async onReady(container) {\n    Object.defineProperties(app.context, {\n      render: {\n        value: async function (...args) {\n          // ...\n        },\n      },\n    });\n  }\n}\n```\n\n但是这样做无法直接让 Context 包含 Typescript 定义，需要额外增加定义，请参考 [扩展上下文定义](../context_definition)。\n\n\n\n## 获取 Http Server\n\n在一些特殊情况下，你需要获取到原始的 Http Server，我们可以在服务器启动后获取。\n\n```typescript\nimport { App, Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\n\n@Configuration({\n\t// ...\n})\nexport class MainConfiguration {\n  @Inject()\n  framework: koa.Framework;\n\n  async onServerReady(container) {\n    const server = this.framework.getServer();\n    // ...\n  }\n}\n```\n\n\n\n## State 类型定义\n\n在 koa 的 Context 中有一个特殊的 State 属性，通过和 Context 类似的方式可以扩展 State 定义。\n\n```typescript\n// src/interface.ts\n\ndeclare module '@midwayjs/koa/dist/interface' {\n  interface Context {\n    abc: string;\n  }\n\n  interface State {\n    bbb: string;\n    ccc: number;\n  }\n}\n```\n\n\n\n## 获取端口\n\n通过 framework 或者 app 上的 `getPort()` 方法可以获取端口。\n\n```typescript\nimport { App, Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\n\n@Configuration({\n  imports: [\n    koa,\n    // ...\n  ]\n\t// ...\n})\nexport class MainConfiguration {\n  @Inject()\n  framework: koa.Framework;\n\n  async onServerReady(container, mainApp: koa.Application) {\n    const port = this.framework.getPort();\n    const port = this.mainApp.getPort();\n    // ...\n  }\n}\n```\n\n\n\n## 配置\n\n\n\n### 默认配置\n\n`@midwayjs/koa` 的配置样例如下：\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  koa: {\n    port: 7001,\n  },\n}\n```\n\n所有属性描述如下：\n\n| 属性         | 类型                                         | 描述                                                    |\n| ------------ |--------------------------------------------| ------------------------------------------------------- |\n| port         | number                                     | 可选，启动的端口                                        |\n| globalPrefix | string                                     | 可选，全局的 http 前缀                                  |\n| keys         | string[]                                   | 可选，Cookies 签名，如果上层未写 keys，也可以在这里设置 |\n| hostname     | string                                     | 可选，监听的 hostname，默认 127.1                       |\n| key          | string \\| Buffer \\| Array\\Buffer\\|Object>  | 可选，Https key，服务端私钥                             |\n| cert         | string \\| Buffer \\| Array\\<Buffer\\|Object> | 可选，Https cert，服务端证书                            |\n| ca           | string \\| Buffer \\| Array\\<Buffer\\|Object> | 可选，Https ca                                          |\n| http2        | boolean                                    | 可选，http2 支持，默认 false                            |\n| proxy        | boolean                                    | 可选，是否开启代理，如果为 true 则对于 request 请求中的 ip 优先从 Header 字段中 X-Forwarded-For 获取，默认 false           |\n| subdomainOffset        | number                                     | 可选，子域名的偏移量，默认 2                            |\n| proxyIpHeader        | string                                     | 可选，获取代理 ip 的字段名，默认为 X-Forwarded-For |\n| maxIpsCount        | number                                     | 可选，获取的 ips 最大数量，默认为 0（全部返回）|\n| serverTimeout | number                                     | 可选，服务端超时配置，默认为 2 \\* 60 \\* 1000（2 分钟），单位毫秒 |\n| serverOptions | Record\\<string, any>                       | 可选，http Server [选项](https://nodejs.org/docs/latest/api/http.html#httpcreateserveroptions-requestlistener) |\n\n\n\n### 修改端口\n\n默认情况下，我们在 `config.default` 提供了 `7001` 的默认端口参数，修改它就可以修改 koa http 服务的默认端口。\n\n比如我们修改为 `6001`：\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  koa: {\n    port: 6001,\n  },\n}\n```\n\n默认情况下，单测环境由于需要 supertest 来启动端口，我们的 port 配置为 `null`。\n\n```typescript\n// src/config/config.unittest\nexport default {\n  // ...\n  koa: {\n    port: null,\n  },\n}\n```\n\n此外，也可以通过 `midway-bin dev --ts --port=6001` 的方式来临时修改端口，此方法会覆盖配置中的端口。\n\n\n\n### 全局前缀\n\n此功能请参考 [全局前缀](../controller#全局路由前缀)。\n\n\n\n### 反向代理配置\n\n如果使用了 Nginx 等反向代理，请开启 `proxy` 配置。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  koa: {\n    proxy: true,\n  },\n}\n```\n\n默认使用 `X-Forwarded-For` Header，如果代理配置不同，请自行配置不同的 Header。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  koa: {\n    proxy: true,\n    proxyIpHeader: 'X-Forwarded-Host'\n  },\n}\n```\n\n\n\n\n\n### Https 配置\n\n在大多数的情况，请尽可能使用外部代理的方式来完成 Https 的实现，比如 Nginx。\n\n在一些特殊场景下，你可以通过配置 SSL 证书（TLS 证书）的方式，来直接开启 Https。\n\n首先，你需要提前准备好证书文件，比如 `ssl.key` 和 `ssl.pem`，key 为服务端私钥，pem 为对应的证书。\n\n然后配置即可。\n\n```typescript\n// src/config/config.default\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  koa: {\n    key: join(__dirname, '../ssl/ssl.key'),\n    cert: join(__dirname, '../ssl/ssl.pem'),\n  },\n}\n```\n\n\n\n### favicon 设置\n\n默认情况下，浏览器会发起一个 `favicon.ico` 的请求。\n\n框架提供了一个默认中间件，用来处理该请求，你可以指定一个 `favicon` 的 Buffer。\n\n```typescript\n// src/config/config.default\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  siteFile: {\n    favicon: readFileSync(join(__dirname, '../static/fav.ico')),\n  },\n}\n```\n\n如果开启了 `@midwayjs/static-file`  组件，那么会优先使用组件的静态文件托管。\n\n关闭中间件。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  siteFile: {\n    enable: false,\n    // ...\n  },\n}\n```\n\n### 修改上下文日志\n\n可以单独修改 koa 框架的上下文日志。\n\n```typescript\nexport default {\n  koa: {\n    contextLoggerFormat: info => {\n      const ctx = info.ctx;\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} [${ctx.userId} - ${Date.now() - ctx.startTime}ms ${ctx.method}] ${info.message}`;\n    }\n    // ...\n  },\n};\n```\n\n\n### Query 数组解析\n\n默认情况下，koa 使用 `querystring` 解析 query 参数，当碰到数组时，会将数组的数据拆开。\n\n比如：\n\n```\nGET /query?a[0]=1&a[1]=2\n```\n\n拿到的结果是：\n\n```json\n{\n    \"a[0]\": 1,\n    \"a[1]\": 2,\n}\n```\n\n框架提供了一些参数来处理这种情况。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  koa: {\n    queryParseMode: 'extended',\n    // ...\n  },\n}\n```\n\n`queryParseMode` 参数可以选择 `extended`、 `strict`、`first` 三种值。\n\n 当 `queryParseMode` 有值时，会使用 `qs` 模块处理 query，效果同 `koa-qs` 模块。\n\n当请求参数为 `/query?a=1&b=2&a=3&c[0]=1&c[1]=2'` 时。\n\n默认效果（使用 `querystring`）\n\n```JSON\n{\n  \"a\": [\"1\", \"3\" ],\n  \"b\": \"2\",\n  \"c[0]\": \"1\",\n  \"c[1]\": \"2\"\n}\n```\n\n `extended` 效果\n\n```JSON\n{\n  \"a\": [\"1\", \"3\" ],\n  \"b\": \"2\",\n  \"c\": [\"1\", \"2\"]\n}\n```\n\n `strict` 效果\n\n```JSON\n{\n  \"a\": [\"1\", \"3\" ],\n  \"b\": [\"2\"],\n  \"c\": [\"1\", \"2\"]\n}\n```\n\n `first` 效果\n\n```JSON\n{\n  \"a\": \"1\",\n  \"b\": \"2\",\n  \"c\": \"1\"\n}\n```\n\n\n### 超时配置\n\nRequestTiemout 和 ServerTimeout 是两种不同的超时情况。\n\n- `serverTimeout`：用于设置服务器接收到请求后，等待客户端发送数据的超时时间。如果在该时间内客户端没有发送任何数据，则服务器将关闭连接。此超时适用于整个请求-响应周期，包括请求头、请求主体以及响应。\n- `requestTimeout`：用于设置服务器等待客户端发送完整请求的超时时间。这个超时是针对请求头和请求主体的，服务器将在该时间内等待客户端发送完整的请求。如果在超时时间内没有收到完整的请求，则服务器将中止该请求。\n\n默认情况下，`serverTimeout` 为 0，不会触发超时。\n\n如有需求，可以通过配置修改，单位毫秒。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  koa: {\n    serverTimeout: 100_000\n  },\n}\n```\n\n如果程序出现 `ERR_HTTP_REQUEST_TIMEOUT` 这个错误，说明是触发了 `requestTimeout`，默认为 `300_000` （五分钟），单位毫秒，可以通过以下配置修改。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  koa: {\n    serverOptions: {\n      requestTimeout: 600_000\n    }\n  },\n}\n```\n\n","site/docs/extensions/mcp.md":"# MCP (Model Context Protocol)\n\nMCP (Model Context Protocol) 是由 Anthropic 开发的开放标准协议，用于将 AI 模型与外部数据源和工具安全连接。它允许 AI 应用程序与各种服务进行语义集成，为 AI 模型提供了获取实时信息、执行操作和访问资源的标准化方式。\n\n![MCP Architecture](https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/mcp-simple-diagram.png?w=1650&fit=max&auto=format&n=4ZXF1PrDkEaJvXpn&q=85&s=37d50a5429a8f662c1beb564b4df8e65)\n\nMidway 提供了对 MCP 的完整支持和封装，能够简单快速地创建 MCP 服务器。本章节演示了如何在 Midway 体系下，提供 MCP 服务的方法。\n\nMidway 当前采用了最新的 [MCP SDK](https://github.com/modelcontextprotocol/typescript-sdk) 进行开发，并提供了装饰器和框架集成，用于快速发布 MCP 服务。\n\n相关信息：\n\n**提供服务**\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n**传输类型支持**\n\n| 传输类型      | 描述                                    | 支持状态 |\n| ------------- | --------------------------------------- | -------- |\n| stdio         | 标准输入输出传输，适用于 CLI 应用       | ✅        |\n| stream-http   | HTTP 流传输，推荐的现代传输方式         | ✅        |\n| sse           | 服务器发送事件传输（已弃用，向下兼容）  | ✅        |\n\n## 安装依赖\n\n在现有项目中安装 MCP 的依赖。\n\n```bash\n$ npm i @midwayjs/mcp@4 --save\n$ npm i @modelcontextprotocol/sdk zod --save\n```\n\n\n如果使用 `sse` 或 `stream-http` 传输类型，还需要安装 HTTP 框架组件（选择其中一个）：\n\n```bash\n# Express 框架\n$ npm i @midwayjs/express@4 --save\n\n# 或者 Koa 框架\n$ npm i @midwayjs/koa@4 --save\n\n# 或者 Egg.js 框架\n$ npm i @midwayjs/web@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/mcp\": \"^4.0.0\",\n    \"@modelcontextprotocol/sdk\": \"^1.19.0\",\n    \"zod\": \"^3.24.1\",\n    \"@midwayjs/express\": \"^4.0.0\"\n  }\n}\n```\n\n:::tip\n如果使用 `sse` 或 `stream-http` 传输类型，您需要选择并安装一个 HTTP 框架：\n- `@midwayjs/express` - Express 框架\n- `@midwayjs/koa` - Koa 框架  \n- `@midwayjs/web` - Egg.js 框架\n\n如果只使用 `stdio` 传输类型，则不需要安装 HTTP 框架组件。\n:::\n\n## 开启组件\n\n在 `src/configuration.ts` 中导入 MCP 组件：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as mcp from '@midwayjs/mcp';\n// 如果使用 sse/stream-http 传输类型，需要导入 HTTP 框架\nimport * as express from '@midwayjs/express';\n\n@Configuration({\n  imports: [\n    express, // HTTP 框架（sse/stream-http 传输类型需要）\n    mcp      // 导入 MCP 组件\n  ],\n  // ...\n})\nexport class MainConfiguration {}\n```\n\n:::info\n**传输类型依赖说明**：\n- `stdio` 传输：只需要导入 `@midwayjs/mcp`\n- `sse` 或 `stream-http` 传输：需要同时导入 HTTP 框架组件（如 `@midwayjs/express`）\n:::\n\n## 配置\n\n在 `config.default.ts` 中配置 MCP 服务器信息：\n\n```typescript\nexport default {\n  // 如果使用 sse/stream-http 传输类型，需要配置 HTTP 框架\n  express: {\n    port: 3000,\n    keys: ['mcp-secret-key']\n  },\n  // 或者如果使用 Koa\n  // koa: {\n  //   port: 3000,\n  //   keys: ['mcp-secret-key']\n  // },\n  \n  mcp: {\n    serverInfo: {\n      name: 'my-mcp-server',\n      version: '1.0.0',\n    },\n    // 传输类型：stdio | stream-http | sse(已弃用)\n    transportType: 'stream-http',\n    // 可选：自定义端点路径\n    endpoints: {\n      streamHttp: '/mcp',     // StreamHTTP 端点\n      sse: '/sse',            // SSE 端点（向下兼容）\n      messages: '/messages'   // 消息端点\n    }\n  }\n}\n```\n\n:::warning\n- 当使用 `sse` 或 `stream-http` 传输类型时，**必须配置 HTTP 框架的端口和密钥**：\n- 如果只使用 `stdio` 传输类型，则不需要 HTTP 框架配置\n:::\n\n### 传输类型说明\n\nMCP 支持多种传输类型：\n\n- **stdio**: 适用于命令行工具和脚本，通过标准输入输出通信\n- **stream-http**: 推荐的 HTTP 传输方式，支持会话管理和并发请求\n- **sse**: 传统的服务器发送事件传输（已弃用，但保持向下兼容）\n\n:::tip\n推荐使用 `stream-http` 传输类型，它提供了更好的性能和会话管理能力，同时兼容传统的 SSE 客户端。\n:::\n\n### 默认端点配置\n\n当使用 `sse` 或 `stream-http` 传输类型时，Midway MCP 框架会自动配置以下默认端点：\n\n| 端点类型 | 默认路径 | 用途 | 支持的传输类型 |\n|---------|---------|------|----------------|\n| StreamHTTP | `/mcp` | 主要的 MCP 通信端点，推荐使用 | stream-http |\n| SSE | `/sse` | 传统 SSE 客户端兼容端点 | sse, stream-http (向下兼容) |\n| Messages | `/messages` | 消息处理端点 | stream-http, sse |\n\n**自定义端点配置**\n\n您可以通过 `endpoints` 配置项自定义这些端点路径：\n\n```typescript\nexport default {\n  mcp: {\n    serverInfo: {\n      name: 'my-mcp-server',\n      version: '1.0.0',\n    },\n    transportType: 'stream-http',\n    endpoints: {\n      streamHttp: '/api/mcp',        // 自定义 StreamHTTP 端点\n      sse: '/api/events',            // 自定义 SSE 端点\n      messages: '/api/messages'      // 自定义消息端点\n    }\n  }\n}\n```\n\n**端点访问示例**\n\n假设服务器运行在 `http://localhost:3000`，默认端点访问地址为：\n\n- StreamHTTP 客户端：`http://localhost:3000/mcp`\n- SSE 客户端：`http://localhost:3000/sse`\n- 消息端点：`http://localhost:3000/messages`\n\n:::info\n**向下兼容说明**：当服务器配置为 `stream-http` 传输类型时，传统的 SSE 客户端仍然可以通过 `/sse` 端点正常连接和通信。\n:::\n\n### 日志配置\n\nMCP 组件默认志输出，可以通过 `mcpLogger` 进一步配置日志详情。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  midwayLogger: {\n    mcpLogger: {\n      // ...\n    }\n  },\n}\n```\n\n\n\n## 使用方式\n\n### 工具（Tools）\n\n使用 `@Tool` 装饰器创建 MCP 工具：\n\n```typescript\nimport { Tool, IMcpTool, ToolConfig } from '@midwayjs/mcp';\nimport { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport { z } from 'zod';\n\nconst weatherConfig: ToolConfig<{ city: z.ZodString }> = {\n  description: 'Get weather information for a city',\n  inputSchema: {\n    city: z.string().describe('The city name')\n  }\n};\n\n@Tool('get_weather', weatherConfig)\nexport class WeatherTool implements IMcpTool {\n  async execute(args: { city: string }): Promise<CallToolResult> {\n    // 模拟获取天气数据\n    const weather = await this.getWeatherData(args.city);\n    \n    return {\n      content: [\n        {\n          type: 'text',\n          text: `Weather in ${args.city}: ${weather.temperature}°C, ${weather.condition}`\n        }\n      ]\n    };\n  }\n\n  private async getWeatherData(city: string) {\n    // 实际的天气 API 调用\n    return {\n      temperature: 22,\n      condition: 'Sunny'\n    };\n  }\n}\n```\n\n### 提示（Prompts）\n\n使用 `@Prompt` 装饰器创建 MCP 提示：\n\n```typescript\nimport { Prompt, IMcpPrompt, PromptConfig } from '@midwayjs/mcp';\nimport { GetPromptResult } from '@modelcontextprotocol/sdk/types.js';\nimport { z } from 'zod';\n\nconst promptConfig: PromptConfig<{ \n  topic: z.ZodString; \n  style: z.ZodOptional<z.ZodString>; \n}> = {\n  description: 'Generate content on a specific topic',\n  argsSchema: {\n    topic: z.string().describe('The topic to write about'),\n    style: z.string().optional().describe('Writing style (formal, casual, etc.)')\n  }\n};\n\n@Prompt('content_generator', promptConfig)\nexport class ContentPrompt implements IMcpPrompt {\n  async generate(args: { topic: string; style?: string }): Promise<GetPromptResult> {\n    const style = args.style || 'professional';\n    \n    return {\n      messages: [\n        {\n          role: 'user',\n          content: {\n            type: 'text',\n            text: `Write a ${style} article about: ${args.topic}`\n          }\n        }\n      ]\n    };\n  }\n}\n```\n\n### 资源（Resources）\n\n使用 `@Resource` 装饰器创建 MCP 资源：\n\n```typescript\nimport { Resource, IMcpResource, ResourceConfig } from '@midwayjs/mcp';\nimport { ReadResourceResult } from '@modelcontextprotocol/sdk/types.js';\n\nconst resourceConfig: ResourceConfig = {\n  description: 'Access user database information',\n  uri: 'database://users/{id}',\n  mimeType: 'application/json'\n};\n\n@Resource('user_data', resourceConfig)\nexport class UserResource implements IMcpResource {\n  async handle(uri: URL): Promise<ReadResourceResult> {\n    const userId = this.extractUserIdFromUri(uri);\n    const userData = await this.getUserData(userId);\n    \n    return {\n      contents: [\n        {\n          uri: uri.toString(),\n          mimeType: 'application/json',\n          text: JSON.stringify(userData)\n        }\n      ]\n    };\n  }\n\n  private extractUserIdFromUri(uri: URL): string {\n    const path = uri.pathname;\n    return path.split('/').pop() || '';\n  }\n\n  private async getUserData(userId: string) {\n    // 实际的数据库查询\n    return {\n      id: userId,\n      name: 'John Doe',\n      email: 'john@example.com'\n    };\n  }\n}\n```\n\n\n\n## 安全认证\n\n### 内置的 JWT 助手\n\nMidway MCP 框架提供了内置的 JWT 认证助手，可以自动处理 JWT token 验证并将认证信息传递给 MCP 工具、提示和资源。\n\n在配置中启用 JWT 认证助手：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // JWT 配置\n  jwt: {\n    secret: 'your-jwt-secret-key',\n    sign: {\n      expiresIn: '1h'\n    }\n  },\n\n  mcp: {\n    serverInfo: {\n      name: 'my-mcp-server',\n      version: '1.0.0',\n    },\n    transportType: 'stream-http',\n    // 启用内置 JWT 认证助手\n    enableJwtAuthHelper: true,\n  }\n}\n```\n\n认证助手是一个中间件，能将 jwt 的 payload 信息转化为 MCP SDK 中定义的 AuthInfo。\n\n可以通过 `ctx.authInfo` 使用认证信息。\n\n```typescript\nimport { Tool, IMcpTool, ToolConfig, Context } from '@midwayjs/mcp';\nimport { Inject } from '@midwayjs/core';\nimport { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\n@Tool('secure_operation', { description: 'A tool that requires authentication' })\nexport class SecureTool implements IMcpTool {\n  @Inject()\n  ctx: Context;\n\n  async execute(args: any): Promise<CallToolResult> {\n    const authInfo = this.ctx.authInfo;\n    // ...\n  }\n}\n```\n\nJWT payload 应包含以下标准字段：\n\n```json\n{\n  \"aud\": \"client-id\",           \t\t\t\t\t\t// 客户端 ID，映射到 authInfo.clientId\n  \"scope\": \"mcp:read mcp:write\",  \t\t\t\t\t// 权限范围，映射到 authInfo.scopes\n  \"exp\": 1234567890,           \t\t\t\t\t\t\t// 过期时间，映射到 authInfo.expiresAt\n  \"resource\": \"https://api.example.com\", \t\t// 资源 URL，映射到 authInfo.resource\n  \"sub\": \"user-123\",           \t\t\t\t\t\t\t// 用户 ID\n  \"iss\": \"https://auth.example.com\", \t\t\t\t// 签发者\n  \"extra\": {\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// 其他自定义字段会放入 authInfo.extra\n    \"username\": \"testuser\",\n    \"role\": \"admin\"\n  }      \t\t\t\t\t\t\t\t\t\t\n}\n```\n\n\n\n\n\n### 非标数据转换\n\n如果传入的 jwt 结构为非标，需要在配置中自行提供转换函数。\n\n```typescript\n// src/config/config.default.ts\nimport { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';\n\nexport default {\n  // JWT 配置\n  jwt: {\n    secret: 'your-jwt-secret-key',\n    sign: {\n      expiresIn: '1h'\n    }\n  },\n\n  mcp: {\n    serverInfo: {\n      name: 'my-mcp-server',\n      version: '1.0.0',\n    },\n    transportType: 'stream-http',\n    // 启用内置 JWT 认证助手\n    enableJwtAuthHelper: true,\n    // 自定义函数\n    jwtAuthCustomPayloadTransformer: (payload: any, token: string): AuthInfo => {\n      return {\n        // ...使用标准JWT字段映射AuthInfo\n        token: token,\n        clientId: payload.aud,\n        // ...\n      };\n    }\n  }\n}\n```\n\n\n\n\n\n### 更多认证\n\n在其他的认证场景下，也可以使用 Midway 组件或者编写中间件完整。\n\n比如你可以使用 `passport` 组件完成 OAuth 认证。\n\n在认证完成后，按照 MCP SDK 规范，需将认证信息放到 `req.auth` 中。\n\n如：\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport * as mcp from '@midwayjs/mcp';\nimport { MidwayMCPFramework } from '@midwayjs/mcp';\nimport { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';\n\n@Configuration({\n  imports: [\n    express,\n    // ...\n    mcp\n  ],\n  importConfigs: ['./config']\n})\nexport class MainConfiguration {\n  @Inject()\n  mcpFramework: MidwayMCPFramework;\n\n  async onReady(container: IMidwayContainer, mainApp: IMidwayApplication) {\n    const authMiddleware = async (req, res, next) => {\n      // ...\n      const userInfo = {\n        user: 'zhangsan',\n        role: 'admin'\n      }\n      \n      // 必须要设置这个字段\n      req.auth = {\n        token: req.headers.authorization,\n        clientId: 'test-client-id',\n        scopes: ['mcp:read', 'mcp:write'],\n        resource: new URL('https://mcp.example.com/resources'),\n        extra: userInfo,\n      } as AuthInfo;\n      \n      // koa 就放到 ctx.req.auth 上\n\n      await next();\n    }\n    \n    // 注意这里的 mainApp 是 Express\n    mainApp.getMiddleware().insertBefore(authMiddleware, 'mcpMiddleware');\n  }\n}\n```\n\n\n\n\n\n## 完整示例\n\n这里是一个完整的 MCP 服务器示例：\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport * as mcp from '@midwayjs/mcp';\n\n@Configuration({\n  imports: [\n    express,\n    mcp\n  ],\n  importConfigs: ['./config']\n})\nexport class MainConfiguration {}\n\n// src/config/config.default.ts\nexport default {\n  express: {\n    port: 3000,\n  },\n  mcp: {\n    serverInfo: {\n      name: 'example-mcp-server',\n      version: '1.0.0',\n    },\n    transportType: 'stream-http',\n  }\n}\n\n// src/tools/calculator.ts\nimport { Tool, IMcpTool, ToolConfig } from '@midwayjs/mcp';\nimport { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport { z } from 'zod';\n\nconst calcConfig: ToolConfig<{ \n  operation: z.ZodEnum<['add', 'subtract', 'multiply', 'divide']>;\n  a: z.ZodNumber;\n  b: z.ZodNumber;\n}> = {\n  description: 'Perform basic mathematical operations',\n  inputSchema: {\n    operation: z.enum(['add', 'subtract', 'multiply', 'divide']),\n    a: z.number(),\n    b: z.number()\n  }\n};\n\n@Tool('calculator', calcConfig)\nexport class CalculatorTool implements IMcpTool {\n  async execute(args: { \n    operation: 'add' | 'subtract' | 'multiply' | 'divide';\n    a: number;\n    b: number;\n  }): Promise<CallToolResult> {\n    let result: number;\n    \n    switch (args.operation) {\n      case 'add':\n        result = args.a + args.b;\n        break;\n      case 'subtract':\n        result = args.a - args.b;\n        break;\n      case 'multiply':\n        result = args.a * args.b;\n        break;\n      case 'divide':\n        if (args.b === 0) {\n          throw new Error('Division by zero');\n        }\n        result = args.a / args.b;\n        break;\n    }\n    \n    return {\n      content: [\n        {\n          type: 'text',\n          text: `${args.a} ${args.operation} ${args.b} = ${result}`\n        }\n      ]\n    };\n  }\n}\n```\n\n## 动态调用 API\n\n您可以在运行时动态调用 MCP 服务器的原始 API 来注册工具、提示和资源：\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport * as mcp from '@midwayjs/mcp';\nimport { MidwayMCPFramework } from '@midwayjs/mcp';\nimport { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\n@Configuration({\n  imports: [\n    express,\n    mcp\n  ],\n  importConfigs: ['./config']\n})\nexport class MainConfiguration {\n  @Inject()\n  mcpFramework: MidwayMCPFramework;\n\n  async onReady() {\n    const server = this.mcpFramework.getServer();\n    \n    // 动态注册工具\n    server.registerTool({\n      name: 'dynamic_tool',\n      description: '动态注册的工具',\n      inputSchema: {\n        type: 'object',\n        properties: {\n          message: { type: 'string', description: '要处理的消息' }\n        },\n        required: ['message']\n      }\n    }, async (args): Promise<CallToolResult> => {\n      return {\n        content: [{\n          type: 'text',\n          text: `动态处理: ${args.message}`\n        }]\n      };\n    });\n    \n    // 动态注册提示\n    server.registerPrompt({\n      name: 'dynamic_prompt',\n      description: '动态生成的提示',\n      argsSchema: {\n        type: 'object',\n        properties: {\n          topic: { type: 'string', description: '主题' }\n        },\n        required: ['topic']\n      }\n    }, async (args) => {\n      return {\n        messages: [{\n          role: 'user',\n          content: {\n            type: 'text',\n            text: `请写一篇关于 ${args.topic} 的文章`\n          }\n        }]\n      };\n    });\n    \n    // 动态注册资源\n    server.registerResource({\n      uri: 'dynamic://config',\n      name: '动态配置',\n      description: '动态生成的配置资源',\n      mimeType: 'application/json'\n    }, async (uri) => {\n      return {\n        contents: [{\n          uri: uri.toString(),\n          mimeType: 'application/json',\n          text: JSON.stringify({\n            timestamp: new Date().toISOString(),\n            config: 'dynamic data'\n          })\n        }]\n      };\n    });\n  }\n}\n```\n\n:::tip\n动态注册的 API 适用于需要在运行时根据业务逻辑动态添加 MCP 功能的场景。与装饰器方式相比，动态注册提供了更大的灵活性，但需要手动管理注册逻辑。\n:::\n\n## 客户端连接\n\n### StreamHTTP 客户端\n\n```typescript\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';\n\n// 创建客户端实例\nconst client = new Client({\n  name: 'my-mcp-client',\n  version: '1.0.0'\n});\n\n// 创建 StreamHTTP 传输\nconst transport = new StreamableHTTPClientTransport(\n  new URL('http://localhost:3000/mcp'),\n  {\n    requestInit: {\n      headers: {\n        'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'\n      }\n    }\n  }\n);\n\n// 连接到服务器\nawait client.connect(transport);\n\n// 列出可用工具\nconst { tools } = await client.listTools();\nconsole.log('Available tools:', tools);\n\n// 调用工具\nconst result = await client.callTool({\n  name: 'calculator',\n  arguments: {\n    operation: 'add',\n    a: 5,\n    b: 3\n  }\n});\nconsole.log('Tool result:', result);\n\n// 使用完毕后关闭连接\nawait client.close();\n```\n\n\n\n### Stdio 客户端 (进程通信)\n\n```typescript\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';\n\n// 创建客户端实例\nconst client = new Client({\n  name: 'my-mcp-client',\n  version: '1.0.0'\n});\n\n// 创建 Stdio 传输，启动 MCP 服务器进程\nconst transport = new StdioClientTransport({\n  command: 'node',\n  args: ['bootstrap.js'],\n  env: {\n    NODE_ENV: 'production',\n    // 继承当前进程的环境变量\n    ...process.env\n  },\n  // 可选：设置工作目录\n  cwd: '/path/to/mcp/server',\n  // 可选：处理 stderr 输出\n  stderr: 'inherit' // 'pipe' | 'inherit' | 'ignore'\n});\n\n// 连接到服务器（会自动启动子进程）\nawait client.connect(transport);\n\n// 可选：监听 stderr 输出（当 stderr: 'pipe' 时）\nif (transport.stderr) {\n  transport.stderr.on('data', (data) => {\n    console.error('Server stderr:', data.toString());\n  });\n}\n\n// 列出可用工具\nconst { tools } = await client.listTools();\nconsole.log('Available tools:', tools);\n\n// 调用工具\nconst result = await client.callTool({\n  name: 'calculator',\n  arguments: {\n    operation: 'add',\n    a: 5,\n    b: 3\n  }\n});\nconsole.log('Tool result:', result);\n\n// 使用完毕后关闭连接（会终止子进程）\nawait client.close();\n```\n\n:::tip\n**传输类型选择建议**：\n- **StreamHTTP**: 适用于网络服务、微服务架构、Web 应用集成\n- **SSE**: 传统方式，主要用于向下兼容\n- **Stdio**: 适用于命令行工具、本地脚本、桌面应用集成（如 Claude Desktop、Cursor、Trae 等 AI 编辑器）\n:::\n\n### 与编辑器集成\n\n在 Cursor/Trae/Claude Desktop 等编辑器的配置文件中添加您的 MCP 服务器：\n\nstdio 示例如下：\n\n```json\n{\n  \"mcpServers\": {\n    \"my-midway-mcp-server\": {\n      \"command\": \"node\",\n      \"args\": [\"bootstrap.js\"],\n      \"env\": {\n        \"NODE_ENV\": \"production\"\n      }\n    }\n  }\n}\n```\n\nstream-http 示例如下：\n\n```json\n{\n  \"mcpServers\": {\n    \"my-midway-mcp-server\": {\n      \"url\": \"http://localhost:3000/mcp\"\n    }\n  }\n}\n```\n\n## 最佳实践\n\n### 1. 类型安全\n\n使用 TypeScript 泛型确保类型安全：\n\n```typescript\nimport { Tool, ToolConfig } from '@midwayjs/mcp';\nimport { z } from 'zod';\n\n// 定义清晰的输入类型\nconst userSchema = {\n  userId: z.string().min(1),\n  includeProfile: z.boolean().optional()\n} as const;\n\nconst userToolConfig: ToolConfig<typeof userSchema> = {\n  description: 'Get user information',\n  inputSchema: userSchema\n};\n\n@Tool('get_user', userToolConfig)\nexport class UserTool implements IMcpTool {\n  // TypeScript 会自动推断 args 的类型\n  async execute(args: { userId: string; includeProfile?: boolean }) {\n    // 实现逻辑\n  }\n}\n```\n\n### 2. 错误处理\n\n正确处理错误并返回有意义的错误信息：\n\n```typescript\n@Tool('risky_operation', config)\nexport class RiskyTool implements IMcpTool {\n  async execute(args: any): Promise<CallToolResult> {\n    try {\n      const result = await this.performRiskyOperation(args);\n      return {\n        content: [{ type: 'text', text: result }]\n      };\n    } catch (error) {\n      return {\n        content: [{ \n          type: 'text', \n          text: `Error: ${error.message}` \n        }],\n        isError: true\n      };\n    }\n  }\n}\n```\n\n### 3. 资源管理\n\n合理管理资源连接和清理：\n\n```typescript\n@Resource('database_query', config)\nexport class DatabaseResource implements IMcpResource {\n  @Inject()\n  databaseService: DatabaseService;\n\n  async handle(uri: URL): Promise<ReadResourceResult> {\n    const connection = await this.databaseService.getConnection();\n    \n    try {\n      const result = await connection.query(this.parseQuery(uri));\n      return {\n        contents: [{\n          uri: uri.toString(),\n          mimeType: 'application/json',\n          text: JSON.stringify(result)\n        }]\n      };\n    } finally {\n      await connection.close();\n    }\n  }\n}\n```\n\n## 常见问题\n\n### Q: MCP 与传统 API 有什么区别？\n\nA: MCP 是专门为 AI 模型设计的协议，提供了语义化的接口描述、类型安全的参数验证和上下文感知的交互。与传统 REST API 相比，MCP 更适合 AI 模型理解和使用。\n\n### Q: 如何在生产环境中部署 MCP 服务？\n\nA: 推荐使用 `stream-http` 传输类型，配合反向代理（如 Nginx）进行负载均衡。确保启用适当的安全措施和监控。\n\n### Q: 可以在一个应用中同时提供多种传输类型吗？\n\nA: 当前版本每个应用实例只支持一种传输类型。如需支持多种传输类型，建议部署多个应用实例。\n\n### Q: 如何调试 MCP 服务？\n\nA: 使用日志记录、MCP 客户端工具进行测试，或者使用官方提供的 [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector)\n\n\n\n## 相关链接\n\n- [MCP 官方文档](https://modelcontextprotocol.io/)\n- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)\n- [Claude Desktop MCP 配置](https://docs.anthropic.com/claude/docs/mcp)","site/docs/extensions/mikro.md":"import Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n# MikroORM\n\n本章节介绍用户如何在 midway 中使用 MikroORM。 MikroORM 是基于数据映射器、工作单元和身份映射模式的 Node.js 的 TypeScript ORM。\n\nMikroORM 的官网文档在 [这里](https://mikro-orm.io/docs)。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 关于升级\n\n* 从 `v3.14.0` 版本的组件开始，支持 mikro v5/v6 版本，由于 mikro v5 到 v6 有较大的变化，如从 mikro 老版本升级请提前阅读 [Upgrading from v5 to v6](https://mikro-orm.io/docs/upgrading-v5-to-v6)\n* 组件示例已更新为 v6 版本\n* MikroORM v7 已切换为 native ESM，并调整了装饰器包路径。v7 项目请使用 `@midwayjs/mikro7`，不要和 `@midwayjs/mikro` 混用。\n\n\n\n## 安装组件\n\n\n安装 mikro 组件，提供接入 mikro-orm 的能力。`@midwayjs/mikro` 对应 MikroORM v6。\n\n\n```bash\n$ npm i @midwayjs/mikro@4 @mikro-orm/core@^6 @mikro-orm/sqlite@^6 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/mikro\": \"^4.0.0\",\n    \"@mikro-orm/core\": \"^6.0.2\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n同时，还需要引入对应数据库的适配包。\n\n比如：\n\n```typescript\n{\n  \"dependencies\": {\n    // sqlite\n    \"@mikro-orm/sqlite\": \"^6.0.2\",\n\n    // mysql\n    \"@mikro-orm/mysql\": \"^6.0.2\",\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n更多驱动程序请查看 [官方文档](https://mikro-orm.io/docs/usage-with-sql/)。\n\n如果使用 MikroORM v7，请安装独立的 v7 组件。\n\n```bash\n$ npm i @midwayjs/mikro7@4 @mikro-orm/core@^7 @mikro-orm/decorators@^7 @mikro-orm/sqlite@^7 --save\n```\n\n对应 `package.json` 依赖如下。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/mikro7\": \"^4.0.0\",\n    \"@mikro-orm/core\": \"^7.0.0\",\n    \"@mikro-orm/decorators\": \"^7.0.0\",\n    \"@mikro-orm/sqlite\": \"^7.0.0\"\n  }\n}\n```\n\nv7 使用 legacy TypeScript 装饰器时，Entity 装饰器需要从 `@mikro-orm/decorators/legacy` 导入，并在配置中显式设置 `metadataProvider: ReflectMetadataProvider`。同时，项目的 TypeScript 配置需要使用 `moduleResolution: \"nodenext\"`、`\"node20\"` 或 `\"bundler\"`。\n\n\n\n## 引入组件\n\n\n在 `src/configuration.ts` 引入 mikro 组件，示例如下。\n\n```typescript\n// configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as mikro from '@midwayjs/mikro';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    mikro\t\t\t\t// 加载 mikro 组件\n  ],\n  importConfigs: [\n  \tjoin(__dirname, './config')\n  ]\n})\nexport class MainConfiguration {\n\n}\n```\n\n\n\n## 基础使用\n\n和其他 orm 框架类似，都是分为几个步骤：\n\n- 1、定义 Entity\n- 2、配置数据源\n- 3、获取 EntityModel 进行调用\n\n下面的更多 Entity 代码请查看 [示例](https://github.com/midwayjs/midway/tree/main/packages/mikro/test/fixtures/base-fn-origin)。\n\n\n\n### 目录结构\n\n一个基础的参考目录结构如下。\n\n```\nMyProject\n├── src\n│   ├── config\n│   │   └── config.default.ts\n│   ├── entity\n│   │   ├── book.entity.ts\n│   │   ├── index.ts\n│   │   └── base.ts\n│   ├── configuration.ts\n│   └── service\n├── .gitignore\n├── package.json\n├── README.md\n└── tsconfig.json\n```\n\n\n\n### 定义 Entity\n\n定义基础的 Entity。\n\n```typescript\n// src/entity/BaseEntity.ts\nimport { PrimaryKey, Property } from '@mikro-orm/core';\n\nexport abstract class BaseEntity {\n\n  @PrimaryKey()\n  id!: number;\n\n  @Property()\n  createdAt: Date = new Date();\n\n  @Property({ onUpdate: () => new Date() })\n  updatedAt: Date = new Date();\n\n}\n```\n\n定义实际的 Entity，包含一对多，多对多等关系。\n\n```typescript\n// src/entity/book.entity.ts\nimport { Cascade, Collection, Entity, ManyToMany, ManyToOne, Property } from '@mikro-orm/core';\nimport { Author, BookTag, Publisher } from './index';\nimport { BaseEntity } from './base';\n\n@Entity()\nexport class Book extends BaseEntity {\n\n  @Property()\n  title: string;\n\n  @ManyToOne(() => Author)\n  author: Author;\n\n  @ManyToOne(() => Publisher, { cascade: [Cascade.PERSIST, Cascade.REMOVE], nullable: true })\n  publisher?: Publisher;\n\n  @ManyToMany(() => BookTag)\n  tags = new Collection<BookTag>(this);\n\n  @Property({ nullable: true })\n  metaObject?: object;\n\n  @Property({ nullable: true })\n  metaArray?: any[];\n\n  @Property({ nullable: true })\n  metaArrayOfStrings?: string[];\n\n  constructor(title: string, author: Author) {\n    super();\n    this.title = title;\n    this.author = author;\n  }\n\n}\n```\n\n\n\n### 配置数据源\n\nmikro v5 和 v6 略有不同。\n\n<Tabs>\n<TabItem value=\"v6\" label=\"mikro v6\">\n\n```typescript\n// src/config/config.default\nimport { Author, BaseEntity, Book, BookTag, Publisher } from '../entity';\nimport { join } from 'path';\nimport { SqliteDriver } from '@mikro-orm/sqlite';\n\nexport default (appInfo) => {\n  return {\n    mikro: {\n      dataSource: {\n        default: {\n          dbName: join(__dirname, '../../test.sqlite'),\n          driver: SqliteDriver,\t\t// 这里使用了 sqlite 做示例\n          allowGlobalContext: true,\n          // 实体形式\n          entities: [Author, Book, BookTag, Publisher, BaseEntity],\n          // 支持如下的扫描形式，为了兼容我们可以同时进行.js和.ts匹配️\n          entities: [\n            'entity',                        // 指定目录\n            '**/entity/*.entity.{j,t}s',     // 通配加后缀匹配\n          ],\n        }\n      }\n    }\n  }\n}\n```\n\n</TabItem>\n\n<TabItem value=\"v5\" label=\"mikro v5\">\n\n```typescript\n// src/config/config.default\nimport { Author, BaseEntity, Book, BookTag, Publisher } from '../entity';\nimport { join } from 'path';\n\nexport default (appInfo) => {\n  return {\n    mikro: {\n      dataSource: {\n        default: {\n          dbName: join(__dirname, '../../test.sqlite'),\n          type: 'sqlite',\t\t\t// 这里使用了 sqlite 做示例\n          allowGlobalContext: true,\n          // 实体形式\n          entities: [Author, Book, BookTag, Publisher, BaseEntity],\n          // 支持如下的扫描形式，为了兼容我们可以同时进行.js和.ts匹配️\n          entities: [\n            'entity',                        // 指定目录\n            '**/entity/*.entity.{j,t}s',     // 通配加后缀匹配\n          ],\n        }\n      }\n    }\n  }\n}\n```\n\n</TabItem>\n</Tabs>\n\n:::tip\n\nmikro 的 `entities` 字段配置已经经过框架处理，该字段配置请不要参考原始文档。\n\n:::\n\n\n\n### 增删查改\n\n在业务代码中，可以使用 `InjectRepository` 注入 `Repository` 对象执行简单的查询操作。其它的增删改操作可以通过配合`EntityManger ` 的 `persist` 和 `flush` 接口来实现，使用 `InjectEntityManager` 可以直接注入 `EntityManager` 对象，也可以通过`repository.getEntityManager()`获取。\n\n:::caution\n\n* 1、从 5.7 版本开始，MikroORM 将原来 `Repository` 上 `persist` 和 `flush` 等接口标为*弃用*，并计划在 v6 版本中 [彻底移除](https://github.com/mikro-orm/mikro-orm/discussions/3989)，建议直接调用`EntityManager`上的相关接口\n* 2、v6 已经彻底 [弃用](https://mikro-orm.io/docs/upgrading-v5-to-v6#removed-methods-from-entityrepository) 上述接口\n\n:::\n\n```typescript\n// src/service/book.service.ts\nimport { Book } from './entity/book.entity';\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityManager, InjectRepository } from '@midwayjs/mikro';\nimport { QueryOrder } from '@mikro-orm/core';\nimport { EntityManager, EntityRepository } from '@mikro-orm/mysql'; // 需要使用数据库驱动对应的类来执行操作\n\n@Provide()\nexport class BookService {\n\n  @InjectRepository(Book)\n  bookRepository: EntityRepository<Book>;\n\n  @InjectEntityManager()\n  em: EntityManager;\n\n  async queryByRepo() {\n    // 使用Repository查询\n    const books = await this.bookRepository.findAll({\n      populate: ['author'],\n      orderBy: { title: QueryOrder.DESC },\n      limit: 20,\n    });\n    return books;\n  }\n\n  async createBook() {\n    const book = new Book({ title: 'b1', author: { name: 'a1', email: 'e1' } });\n    // 标记保存Book\n    this.em.persist(book);\n    // 执行所有变更\n    await this.em.flush();\n    return book;\n  }\n}\n```\n\n## 高级功能\n\n### 获取数据源\n\n数据源即创建出的数据源对象，我们可以通过注入内置的数据源管理器来获取。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport { MikroDataSourceManager } from '@midwayjs/mikro';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n\n  async onReady(container: IMidwayContainer) {\n    const dataSourceManager = await container.getAsync(MikroDataSourceManager);\n    const orm = dataSourceManager.getDataSource('default');\n    const connection = orm.em.getConnection();\n    // ...\n  }\n}\n```\n\n从 v3.8.0 开始，也可以通过装饰器注入。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport { InjectDataSource } from '@midwayjs/mikro';\nimport { MikroORM, IDatabaseDriver, Connection } from '@mikro-orm/core';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n\n  // 注入默认数据源\n  @InjectDataSource()\n  defaultDataSource: MikroORM<IDatabaseDriver<Connection>>;\n\n  // 注入自定义数据源\n  @InjectDataSource('default1')\n  customDataSource: MikroORM<IDatabaseDriver<Connection>>;\n\n  async onReady(container: IMidwayContainer) {\n    // ...\n  }\n}\n```\n\n\n\n### 日志\n\n可以通过配置将 midway 的 logger 添加到 mikro 中，用于记录 sql 等信息。\n\n```typescript\n// src/config/config.default.ts\nexporg default {\n\tmidwayLogger: {\n    clients: {\n      mikroLogger: {\n        // ...\n      }\n    }\n  },\n  mikro: {\n    dataSource: {\n      default: {\n        entities: [Author, Book, BookTag, Publisher, BaseEntity],\n        // ...\n        logger: 'mikroLogger',\n      }\n    },\n  }\n}\n```\n\n默认情况下  mikro 自带颜色，也会将其写入文件，可以通过配置关闭。\n\n```typescript\n// src/config/config.default.ts\nexporg default {\n\tmidwayLogger: {\n    clients: {\n      mikroLogger: {\n        transports: {\n          console: {\n            autoColors: false,\n          }，\n          file: {\n            fileLogName: 'mikro.log'，\n          },\n        },\n      }\n    }\n  },\n  mikro: {\n    dataSource: {\n      default: {\n        entities: [Author, Book, BookTag, Publisher, BaseEntity],\n        // ...\n        logger: 'mikroLogger',\n        colors: false,\n      }\n    },\n  }\n}\n```\n\n\n\n## 常见问题\n\n\n\n### 1、Node 版本\n\nMikro-orm 对 Node 版本有一些限制，必须为 `>=14.0.0` ，所以 `@midwayjs/mikro` 组件的使用规则也如此。\n\n\n\n### 2、Identity Map\n\nMikro-orm 内部查询有一个 [Identity Map](https://mikro-orm.io/docs/identity-map) 的概念，Midway 已经在所有的内置 Framework 的中间件内置加入了该功能，如果在非请求链路调用场景下使用，比如 `src/configuration` 中，可以开启 `allowGlobalContext` 选项。\n\n\n\n### 3、多库的支持\n\n和其他数据库一样，Midway 支持多数据源的配置。\n\n```typescript\n// src/config/config.default\nimport { Author, BaseEntity, Book, BookTag, Publisher } from '../entity';\nimport { SqlHighlighter } from '@mikro-orm/sql-highlighter';\nimport { join } from 'path';\n\nexport default (appInfo) => {\n  return {\n    mikro: {\n      dataSource: {\n        custom1: {\n          // ...\n        },\n        custom2: {\n          // ...\n        }\n      }\n    }\n  }\n}\n```\n\n注意在使用时，需要传递来自哪个数据源。\n\n```typescript\n// ...\n\n@Provide()\nexport class BookController {\n\n  @InjectRepository(Book, 'custom1')\n  bookRepository: EntityRepository<Book>;\n\n  async findBookAndQuery() {\n    // ...\n  }\n}\n```\n","site/docs/extensions/mongodb.md":"# MongoDB\n\n在这一章节中，我们选择 [Typegoose](https://github.com/typegoose/typegoose) 作为基础的 MongoDB ORM 库。就如同他描述的那样 \" Define Mongoose models using TypeScript classes\"，和 TypeScript 结合的很不错。\n\n简单的来说，Typegoose 使用 TypeScript 编写 Mongoose 模型的 “包装器”，它的大部分能力还是由 [mongoose](https://www.npmjs.com/package/mongoose) 库来提供的。\n\n也可以直接选择 [mongoose](https://www.npmjs.com/package/mongoose) 库来使用，我们会分别描述。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n:::tip\n\n- 1、当前模块从 v3.4.0 开始已经重构，历史写法兼容，如果查询历史文档，请参考 [这里](../legacy/mongodb)。\n- 2、如果代码中有读取配置，注意 `mongoose.clients` 可能会读不到，请使用 `mongoose.dataSource`。\n\n:::\n\n\n\n## 和老写法的区别\n\n如果想使用新版本的用法，请参考下面的流程，将老代码进行修改，新老代码请勿混用。\n\n升级方法：\n\n- 1、无需再使用 `EntityModel` 装饰器\n- 3、在 `src/config.default` 的 `mongoose` 部分配置调整，参考下面的数据源配置部分\n  - 3.1 修改为数据源的形式 `mongoose.dataSource`\n  - 3.2 将实体模型在数据源的 `entities` 字段中声明\n\n\n\n\n## Mongoose 版本依赖\n\n\nmongoose 和你服务器使用的 MongoDB Server 的版本也有着一定的关系，如下，请务必注意。\n\n\n- MongoDB Server 2.4.x: mongoose ^3.8 or 4.x\n- MongoDB Server 2.6.x: mongoose ^3.8.8 or 4.x\n- MongoDB Server 3.0.x: mongoose ^3.8.22, 4.x, or 5.x\n- MongoDB Server 3.2.x: mongoose ^4.3.0 or 5.x\n- MongoDB Server 3.4.x: mongoose ^4.7.3 or 5.x\n- MongoDB Server 3.6.x: mongoose 5.x\n- MongoDB Server 4.0.x: mongoose ^5.2.0\n- MongoDB Server 4.2.x: mongoose ^5.7.0\n- MongoDB Server 4.4.x: mongoose ^5.10.0\n- MongoDB Server 5.x: mongoose ^6.0.0\n\n\n**mongoose 相关的依赖比较复杂，且对应不同的版本，现阶段，我们使用的主要是 mongoose v5 和 v6。**\n\n\n:::info\n从 mongoose@v5.11.0 开始，mongoose 官方支持了定义，所以不再需要安装 @types/mongoose 依赖包。\n:::\n\n安装包依赖版本如下：\n\n**支持 MongoDB Server 6.x**\n\n```json\n  \"dependencies\": {\n    \"mongoose\": \"^7.0.0\",\n    \"@typegoose/typegoose\": \"^10.0.0\",\t\t// 使用 typegoose 需要安装此依赖\n  },\n```\n\n**支持 MongoDB Server 5.x**\n\n```json\n  \"dependencies\": {\n    \"mongoose\": \"^6.0.7\",\n    \"@typegoose/typegoose\": \"^9.0.0\",\t\t// 使用 typegoose 需要安装此依赖\n  },\n```\n\n\n**支持 MongoDB Server 4.4.x**\n\n\n以下版本不需要安装额外定义包。\n```json\n  \"dependencies\": {\n    \"mongoose\": \"^5.13.3\",\n    \"@typegoose/typegoose\": \"^8.0.0\",   // 使用 typegoose 需要安装此依赖\n  },\n```\n\n\n以下版本需要安装额外定义包（不推荐）。\n```json\n \"dependencies\": {\n    \"mongodb\": \"3.6.3\",\t\t\t\t\t\t\t\t\t// mongoose 内部写死了该版本\n    \"mongoose\": \"~5.10.18\",\n    \"@typegoose/typegoose\": \"^7.0.0\",   // 使用 typegoose 需要安装此依赖\n },\n \"devDependencies\": {\n    \"@types/mongodb\": \"3.6.3\",\t\t\t\t\t// 只能使用此版本\n    \"@types/mongoose\": \"~5.10.3\",\n }\n```\n\n\n其余的 MongoDB 安装模块类似，未测。\n\n\n\n## 使用 Typegoose\n\n\n### 1、安装组件\n\n\n安装 Typegoose 组件，提供访问 MongoDB 的能力。\n\n\n**请务必注意，请查看第一小节提前编写/安装 mongoose 等相关依赖包。**\n```bash\n$ npm i @midwayjs/typegoose@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    // 组件\n    \"@midwayjs/typegoose\": \"^4.0.0\",\n    // 上一节中的 mongoose 依赖\n  },\n  \"devDependencies\": {\n    // 上一节中的 mongoose 依赖\n    // ...\n  }\n}\n```\n\n\n\n安装后需要手动在 `src/configuration.ts` 配置，代码如下。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as typegoose from '@midwayjs/typegoose';\n\n@Configuration({\n  imports: [\n    typegoose  \t\t\t\t\t\t\t\t\t\t\t// 加载 typegoose 组件\n  ],\n  importConfigs: [\n  \tjoin(__dirname, './config')\n  ]\n})\nexport class MainConfiguration {\n\n}\n```\n\n:::info\n在该组件中，midway 只是做了简单的配置规则化，并将其注入到初始化流程中。\n:::\n\n### 2、简单的目录结构\n\n\n我们以一个简单的项目举例，其他结构请自行参考。\n\n\n```text\nMyProject\n├── src              \t\t\t\t\t\t\t// TS 根目录\n│   ├── config\n│   │   └── config.default.ts \t\t// 应用配置文件\n│   ├── entity       \t\t\t\t\t\t\t// 实体（数据库 Model) 目录\n│   │   └── user.ts  \t\t\t\t\t  \t// 实体文件\n│   ├── configuration.ts     \t\t\t// Midway 配置文件\n│   └── service      \t\t\t\t\t\t\t// 其他的服务目录\n├── .gitignore\n├── package.json\n├── README.md\n└── tsconfig.json\n```\n\n\n在这里，我们的数据库实体主要放在 `entity` 目录（非强制），这只是一个简单的约定。\n\n\n\n### 3、创建实体文件\n\n比如在 `src/entity/user.ts` 中。\n\n\n```typescript\nimport { prop } from '@typegoose/typegoose';\n\nexport class User {\n  @prop()\n  public name?: string;\n\n  @prop({ type: () => [String] })\n  public jobs?: string[];\n}\n```\n\n等价于使用 mongoose 的下列代码\n\n```typescript\nconst userSchema = new mongoose.Schema({\n  name: String,\n  jobs: [{ type: String }]\n});\n\nconst User = mongoose.model('User', userSchema);\n```\n\n:::info\n所以说，typegoose 只是简化了 model 的创建过程。\n:::\n\n\n\n\n### 4、配置连接信息\n\n\n在 `src/config/config.default.ts` 中加入连接的配置。\n\n```typescript\nimport { User } from '../entity/user';\n\nexport default {\n  // ...\n  mongoose: {\n    dataSource: {\n      default: {\n        uri: 'mongodb://localhost:27017/test',\n        options: {\n          useNewUrlParser: true,\n          useUnifiedTopology: true,\n          user: '***********',\n          pass: '***********'\n        },\n        // 关联实体\n        entities: [ User ]\n      }\n    }\n  },\n}\n```\n\n如需以目录扫描形式关联，请参考 [数据源管理](../data_source)。\n\n\n\n### 5、引用实体，调用数据库\n\n\n示例代码如下：\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typegoose';\nimport { ReturnModelType } from '@typegoose/typegoose';\nimport { User } from '../entity/user';\n\n@Provide()\nexport class TestService {\n\n  @InjectEntityModel(User)\n  userModel: ReturnModelType<typeof User>;\n\n  async getTest(){\n    // create data\n    const { _id: id } = await this.userModel.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User); // an \"as\" assertion, to have types for all properties\n\n    // find data\n    const user = await this.userModel.findById(id).exec();\n    console.log(user)\n  }\n}\n```\n\n\n### 6、多库的情况\n\n首先定义多个实体。\n\n```typescript\nclass User {\n\n  @prop()\n  public name?: string;\n\n  @prop({ type: () => [String] })\n  public jobs?: string[];\n}\n\nclass User2 {\n\n  @prop()\n  public name?: string;\n\n  @prop({ type: () => [String] })\n  public jobs?: string[];\n}\n```\n\n\n将实体配置到多个数据源。\n\n\n在 `src/config/config.default.ts` 中加入数据源的配置。\n```typescript\nimport { User, User2 } from '../entity/user';\n\nexport default {\n  // ...\n  mongoose: {\n    dataSource: {\n      default: {\n        uri: 'mongodb://localhost:27017/test',\n        options: {\n          useNewUrlParser: true,\n          useUnifiedTopology: true,\n          user: '***********',\n          pass: '***********'\n        },\n        entities: [ User ]\n      },\n      db1: {\n        uri: 'mongodb://localhost:27017/test1',\n        options: {\n          useNewUrlParser: true,\n          useUnifiedTopology: true,\n          user: '***********',\n          pass: '***********'\n        },\n        entities: [ User2 ]\n      }\n    }\n  },\n}\n```\n\n\n定义实例时使用固定的连接，在扫描 dataSource 配置 Model 会自动关联 mongoose连接(`getModelForClass(Model, { existingConnection: conn })`)。\n\n```typescript\n@Provide()\nexport class TestService{\n\n  @InjectEntityModel(User)\n  userModel: ReturnModelType<typeof User>;\n\n  @InjectEntityModel(User2)\n  user2Model: ReturnModelType<typeof User2>;\n\n  async getTest(){\n    const { _id: id } = await this.userModel.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User); // an \"as\" assertion, to have types for all properties\n    const user = await this.userModel.findById(id).exec();\n    console.log(user)\n\n    const { _id: id2 } = await this.user2Model.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User2); // an \"as\" assertion, to have types for all properties\n    const user2 = await this.user2Model.findById(id2).exec();\n    console.log(user2)\n  }\n}\n\n```\n\n\n\n### 7、关于 schemaOptions\n\nTypegoose 预留了一个 `setGlobalOptions`  方法用来设置 [schemaOptions](https://typegoose.github.io/typegoose/docs/api/decorators/model-options#schemaoptions) 和一些其他全局性的 [配置](https://typegoose.github.io/typegoose/docs/api/decorators/model-options#options-1)。\n\n我们可以在项目配置加载时设置它。\n\n```typescript\n// srcconfiguration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as typegoose from '@midwayjs/typegoose';\nimport * as Typegoose from '@typegoose/typegoose';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  async onConfigLoad() {\n\n    Typegoose.setGlobalOptions({\n      schemaOptions: {\n        // ...\n      },\n      options: { allowMixed: Severity.ERROR }\n    });\n    // ...\n  }\n}\n```\n\n\n\n\n\n## 直接使用 mongoose\n\nmongoose 组件是 typegoose 的基础组件，有时候我们可以直接使用它。\n\n\n### 1、安装组件\n\n\n**请务必注意，请查看第一小节提前编写/安装 mongoose 等相关依赖包。**\n\n```bash\n$ npm i @midwayjs/mongoose@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    // 组件\n    \"@midwayjs/mongoose\": \"^4.0.0\",\n    // 上一节中的 mongoose 依赖\n  },\n  \"devDependencies\": {\n    // 上一节中的 mongoose 依赖\n    // ...\n  }\n}\n```\n\n\n\n### 2、开启组件\n\n\n安装后需要手动在 `src/configuration.ts` 配置，代码如下。\n```typescript\n// configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as mongoose from '@midwayjs/mongoose';\n\n@Configuration({\n  imports: [\n    mongoose  \t\t\t\t\t\t\t\t\t\t\t// 加载 mongoose 组件\n  ],\n  importConfigs: [\n  \tjoin(__dirname, './config')\n  ]\n})\nexport class MainConfiguration {\n\n}\n```\n\n\n\n\n### 2、配置\n\n和 typegoose 相同，或者说 typegoose 使用的就是 mongoose 的配置。\n\n不管是单库还是多库，数据源配置都是类似的。\n\n\n单库：\n```typescript\nexport default {\n  // ...\n  mongoose: {\n    dataSource: {\n      default: {\n        uri: 'mongodb://localhost:27017/test',\n        options: {\n          useNewUrlParser: true,\n          useUnifiedTopology: true,\n          user: '***********',\n          pass: '**********'\n        }\n      }\n    }\n  },\n}\n```\n多库：\n```typescript\nexport default {\n  // ...\n  mongoose: {\n    dataSource: {\n      default: {\n        uri: 'mongodb://localhost:27017/test',\n        options: {\n          useNewUrlParser: true,\n          useUnifiedTopology: true,\n          user: '***********',\n          pass: '***********'\n        }\n      },\n      db1: {\n        uri: 'mongodb://localhost:27017/test1',\n        options: {\n          useNewUrlParser: true,\n          useUnifiedTopology: true,\n          user: '***********',\n          pass: '***********'\n        }\n      }\n    }\n  },\n}\n```\n\n\n\n### 3、使用\n\n\n当我们希望获取到原始的连接对象时，可以直接使用封装好的 `MongooseConnectionService` 对象。\n```typescript\nimport { Provide, Inject, Init } from '@midwayjs/core';\nimport { MongooseDataSourceManager } from '@midwayjs/mongoose';\nimport { Schema, Document } from 'mongoose';\n\ninterface User extends Document {\n  name: string;\n  email: string;\n  avatar: string;\n}\n\n@Provide()\nexport class TestService {\n\n  @Inject()\n  dataSourceManager: MongooseDataSourceManager;\n\n  @Init()\n  async init() {\n    // get default connection\n    this.conn = this.dataSourceManager.getDataSource('default');\n  }\n\n  async invoke(){\n    const schema = new Schema<User>({\n      name: { type: String, required: true },\n      email: { type: String, required: true },\n      avatar: String\n    });\n    const UserModel = this.conn.model<User>('User', schema);\n    const doc = new UserModel({\n      name: 'Bill',\n      email: 'bill@initech.com',\n      avatar: 'https://i.imgur.com/dM7Thhn.png'\n    });\n    await doc.save();\n  }\n}\n\n```\n\n\n\n\n\n\n## 常见问题\n\n\n### 1、E002: You are using a NodeJS Version below 12.22.0\n\n\n在新版本 @typegoose/typegoose (v8, v9) 中增加了 Node 版本的校验，如果你的 Node.js 版本低于 v12.22.0，就会出现这个提示。\n\n\n普通情况下，请升级 Node.js 到这个版本以上即可解决。\n\n\n在特殊场景下，比如 Serverless 无法修改 Node.js 版本且版本低于 v12.22 的情况下，由于 v12 版本子版本其实都可以，可以通过临时修改 process.version 绕过。\n\n\n```typescript\n// src/configuration.ts\n\nObject.defineProperty(process, 'version', {\n  value: 'v12.22.0',\n  writable: true,\n});\n\n// other code\n\nexport class MainConfiguration {}\n```\n\n\n\n","site/docs/extensions/mqtt.md":"# MQTT\n\nMQTT是用于物联网 (IoT) 的OASIS标准消息传递协议。它被设计为非常轻量级的发布/订阅消息传输，非常适合以较小的代码占用空间和最小的网络带宽连接远程设备。MQTT目前广泛应用于汽车、制造、电信、石油和天然气等行业。\n\n相关信息：\n\n| 描述              |              |\n| ----------------- | ------------ |\n| 可用于标准项目    | ✅            |\n| 可用于 Serverless | 可以发布消息 |\n| 可用于一体化      | ✅            |\n| 包含独立主框架    | ✅            |\n| 包含独立日志      | ✅            |\n\n\n\n## 版本要求\n\n由于 [mqtt](https://github.com/mqttjs/MQTT.js) 库本身的要求，所需要的版本为 **Node.js >= 16**\n\n\n\n## 前置依赖\n\n由于 MQTT 需要 Broker 作为中转传输，你需要自行部署 MQTT Broker 服务，本文档不提供 MQTT 服务本身的部署指导。\n\n\n\n## 安装组件\n\n\n安装 mqtt 组件。\n\n\n```bash\n$ npm i @midwayjs/mqtt@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/mqtt\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n## 启用组件\n\n在 `src/configuration.ts` 中引入组件\n\n```typescript\n// ...\nimport * as mqtt from '@midwayjs/mqtt';\n\n@Configuration({\n  imports: [\n    // ...other components\n    mqtt,\n  ],\n})\nexport class MainConfiguration {}\n```\n\n\n\n由于 MQTT 分为 **订阅者（subscriber）** 和 **发布者（publisher）** 两部分，两个可以独立使用，我们将分别介绍。\n\n\n\n## 订阅服务\n\n### 基础配置\n\n通过 `sub` 字段和 `@MqttSubscriber` 装饰器，我们可以配置多个订阅者。\n\n比如，下面的 `sub1` 和 `sub2` 就是两个不同的订阅者。\n\n```typescript\n// src/config/config.default\n\nexport default {\n  mqtt: {\n    sub: {\n      sub1: {\n        // ...\n      },\n      sub2: {\n        // ...\n      }\n    }\n  }\n}\n```\n\n最简单的订阅者配置需要几个字段，订阅的地址和订阅的 Topic。\n\n```typescript\n// src/config/config.default\n\nexport default {\n  mqtt: {\n    sub: {\n      sub1: {\n        connectOptions: {\n          host: 'test.mosquitto.org',\n          port: 1883,\n        },\n        subscribeOptions: {\n          topicObject: 'test',\n        },\n      },\n      sub2: {\n        // ...\n      }\n    }\n  }\n}\n```\n\n `sub1` 订阅者配置了 `connectOptions` 和 `subscribeOptions` ，分别代表连接配置和订阅配置。\n\n### 订阅实现\n\n我们可以在目录中提供一个标准的订阅器实现，比如 `src/consumer/sub1.subscriber.ts`。\n\n```typescript\n// src/consumer/sub1.subscriber.ts\n\nimport { ILogger, Inject } from '@midwayjs/core';\nimport { Context, IMqttSubscriber, MqttSubscriber } from '@midwayjs/mqtt';\n\n@MqttSubscriber('sub1')\nexport class Sub1Subscriber implements IMqttSubscriber {\n\n  @Inject()\n  ctx: Context;\n\n  async subscribe() {\n    // ...\n  }\n}\n```\n\n`@MqttSubscriber` 装饰器声明了一个订阅类实现，它的参数为订阅者的名字，比如我们配置文件中的 `sub1`。\n\n`IMqttSubscriber` 接口约定了一个 `subscribe` 方法，每当接收到新的消息时，这个方法就会被执行。\n\n和其他消息订阅机制一样，消息本身通过 `Context` 字段来传递。\n\n```typescript\n// ...\nexport class Sub1Subscriber implements IMqttSubscriber {\n  @Inject()\n  ctx: Context;\n\n  async subscribe() {\n    const payload = this.ctx.message.toString();\n    // ...\n  }\n}\n```\n\n`Context` 字段包括几个 mqtt 属性。\n\n| 属性        | 类型                           | 描述             |\n| ----------- | ------------------------------ | ---------------- |\n| ctx.topic   | string                         | 订阅 Topic       |\n| ctx.message | Buffer                         | 消息内容         |\n| ctx.packet  | IPublishPacket（来自 mqtt 库） | publish 的包信息 |\n\n\n\n## 消息发布\n\n### 基础配置\n\n消息发布也需要创建实例，配置本身使用了 [服务工厂](/docs/service_factory) 的设计模式。\n\n比如多实例配置如下：\n\n```typescript\n// src/config/config.default\n\nexport default {\n  mqtt: {\n    pub: {\n      clients: {\n        default: {\n          host: 'test.mosquitto.org',\n          port: 1883,\n        },\n        pub2: {\n          // ...\n        }\n      }\n    }\n  }\n}\n```\n\n上面的配置创建了名为 `default` 和 `pub2` 的两个实例。\n\n\n\n### 使用发布者\n\n如果实例名为 `default` ，则可以使用默认的消息发布类。\n\n比如：\n\n```typescript\n// src/service/user.service.ts\nimport { Provide, Inject } from '@midwayjs/core';\nimport { DefaultMqttProducer } from '@midwayjs/mqtt';\n\n@Provide()\nexport class UserService {\n  \n  @Inject()\n  producer: DefaultMqttProducer;\n  \n  async invoke() {\n    // 同步发布消息\n    this.producer.publish('test', 'hello world');\n    \n    // 异步发布\n    await this.producer.publishAsync('test', 'hello world');\n    \n    // 增加配置\n    await this.producer.publishAsync('test', 'hello world', {\n      qos: 2\n    });\n  }\n}\n```\n\n也可以使用内置的工厂类 `MqttProducerFactory` 注入不同的实例。\n\n```typescript\n// src/service/user.service.ts\nimport { Provide, Inject } from '@midwayjs/core';\nimport { MqttProducerFactory, DefaultMqttProducer } from '@midwayjs/mqtt';\n\n@Provide()\nexport class UserService {\n  \n  @InjectClient(MqttProducerFactory, 'pub2')\n  producer: DefaultMqttProducer;\n  \n  async invoke() {\n    // ...\n  }\n}\n```\n\n\n\n## 组件日志\n\n组件有着自己的日志，默认会将 `ctx.logger` 记录在 `midway-mqtt.log` 中。\n\n我们可以单独配置这个 logger 对象。\n\n```typescript\nexport default {\n  midwayLogger: {\n    // ...\n    mqttLogger: {\n      fileLogName: 'midway-mqtt.log',\n    },\n  }\n}\n```\n\n这个日志的输出格式，我们也可以单独配置。\n\n```typescript\nexport default {\n  mqtt: {\n    // ...\n    contextLoggerFormat: info => {\n      const { jobId, from } = info.ctx;\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} ${info.message}`;\n    },\n  }\n}\n```\n\n","site/docs/extensions/one-shot.md":"# 单次执行\n\n`@midwayjs/one-shot` 是一个只提供 Framework 的一次性脚本执行组件，适合用 IoC 容器组织依赖，并在应用内触发一次性的任务逻辑。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ❌    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ❌   |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n## 安装依赖\n\n在现有项目中安装 one-shot 组件依赖。\n\n```bash\n$ npm i @midwayjs/one-shot@4  --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/one-shot\": \"^4.0.0\"\n  }\n}\n```\n\n## 开启组件\n\n在入口配置中引入组件。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as oneShot from '@midwayjs/one-shot';\n\n@Configuration({\n  imports: [oneShot],\n})\nexport class MainConfiguration {}\n```\n\n## 在生命周期执行\n\n该组件不要求编写额外 Runner，推荐在 `onServerReady` 生命周期里直接执行一次性脚本逻辑。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as oneShot from '@midwayjs/one-shot';\nimport { ScriptService } from './service/script';\n\n@Configuration({\n  imports: [oneShot],\n})\nexport class MainConfiguration {\n  @Inject()\n  scriptService: ScriptService;\n\n  async onServerReady() {\n    await this.scriptService.runOnce();\n  }\n}\n```\n\n## 需要请求作用域时\n\n如果脚本逻辑需要依赖请求作用域（request scope），可以通过 framework 的 `runScript` 执行一个固定格式的 service 类，框架会为本次执行创建上下文并走中间件/过滤器链。\n\n```typescript\n// src/script/syncUser.ts\nimport { Provide } from '@midwayjs/core';\nimport { OneShotRunner, Context } from '@midwayjs/one-shot';\n\n@Provide()\nexport class SyncUserScript implements OneShotRunner<{ id: number }, void> {\n  async run(payload?: { id: number }, ctx?: Context) {\n    // use payload / ctx\n    void payload;\n    void ctx;\n  }\n}\n```\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as oneShot from '@midwayjs/one-shot';\nimport { Framework } from '@midwayjs/one-shot';\nimport { SyncUserScript } from './script/syncUser';\n\n@Configuration({\n  imports: [oneShot],\n})\nexport class MainConfiguration {\n  @Inject()\n  framework: Framework;\n\n  async onServerReady() {\n    await this.framework.runScript(SyncUserScript, { id: 42 });\n  }\n}\n```\n\n## 日志\n\n组件默认会注册一个名为 `oneShotLogger` 的 logger，默认写入 `midway-one-shot.log`。\n\n你可以在脚本服务里通过 `@Logger('oneShotLogger')` 注入使用，例如：\n\n```typescript\nimport { Logger, ILogger } from '@midwayjs/core';\nexport class ScriptService {\n  @Logger('oneShotLogger')\n  logger: ILogger;\n\n  async runOnce() {\n    this.logger.info('run one-shot task');\n  }\n}\n```\n\n如果希望自定义日志文件名或级别，可以在应用配置中覆盖 `midwayLogger.clients.oneShotLogger`：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  midwayLogger: {\n    clients: {\n      oneShotLogger: {\n        fileLogName: 'my-one-shot.log',\n        level: 'info',\n      },\n    },\n  },\n};\n```\n","site/docs/extensions/orm.md":"# TypeORM\n\n[TypeORM](https://github.com/typeorm/typeorm) 是 `node.js` 现有社区最成熟的对象关系映射器（`ORM` ）。本文介绍如何在 Midway 中使用 TypeORM 。\n\n:::tip\n\n本模块是从 v3.4.0 开始为新版本，模块名有变化，历史写法部分兼容，如果查询历史文档，请参考 [这里](../legacy/orm)。\n\n:::\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 和老写法的区别\n\n旧模块为 `@midwayjs/orm` ，新模块为 `@midwayjs/typeorm`，区别如下：\n\n- 1、包名不同\n- 2、在 `src/config.default` 的部分配置调整\n  - 2.1 配置文件中的 key 不同 （orm => typeorm）\n  - 2.2修改为数据源的形式 `typeorm.dataSource`\n  - 2.3 实体模型类或者实体模型类的路径,需要在数据源的 `entities` 字段中声明\n  - 2.4 Subscriber 需要在数据源的 `subscribers` 字段中声明\n- 3、不再使用 `EntityModel` 装饰器，直接使用 typeorm 提供的能力\n\n\n\n## 安装组件\n\n\n安装 typeorm 组件，提供数据库 ORM 能力。\n\n\n```bash\n$ npm i @midwayjs/typeorm@4 typeorm@^1.0.0 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/typeorm\": \"^4.0.0\",\n    \"typeorm\": \"^1.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n## 引入组件\n\n\n在 `src/configuration.ts` 引入 orm 组件，示例如下。\n\n```typescript\n// configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as orm from '@midwayjs/typeorm';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    orm  \t\t\t\t\t\t\t\t\t\t\t\t\t\t// 加载 typeorm 组件\n  ],\n  importConfigs: [\n  \tjoin(__dirname, './config')\n  ]\n})\nexport class MainConfiguration {\n\n}\n```\n\n\n## 安装数据库 Driver\n\n\n常用数据库驱动如下，选择你对应连接的数据库类型安装：\n```bash\n# for MySQL or MariaDB，也可以使用 mysql2 替代\nnpm install mysql --save\nnpm install mysql2 --save\n\n# for PostgreSQL or CockroachDB\nnpm install pg --save\n\n# for SQLite\nnpm install better-sqlite3 --save\n\n# for Microsoft SQL Server\nnpm install mssql --save\n\n# for sql.js\nnpm install sql.js --save\n\n# for Oracle\nnpm install oracledb --save\n\n# for MongoDB(experimental)\nnpm install mongodb --save\n```\n\n:::info\n\n-  Oracle driver 比较特殊，需要查看 [文档](https://github.com/oracle/node-oracledb)\n- 不建议使用 typeorm 链接 mongodb，请使用 mongoose 组件\n\n:::\n\n\n\n\n## 简单的目录结构\n\n\n我们以一个简单的项目举例，其他结构请自行参考。\n\n\n```\nMyProject\n├── src              \t\t\t\t\t\t\t    // TS 根目录\n│   ├── config\n│   │   └── config.default.ts \t\t    // 应用配置文件\n│   ├── entity       \t\t\t\t\t\t\t    // 实体（数据库 Model) 目录\n│   │   └── photo.entity.ts  \t\t\t\t\t// 实体文件\n│   │   └── photoMetadata.entity.ts\n│   ├── configuration.ts     \t\t\t    // Midway 配置文件\n│   └── service      \t\t\t\t\t\t\t    // 其他的服务目录\n├── .gitignore\n├── package.json\n├── README.md\n└── tsconfig.json\n```\n\n\n\n\n在这里，我们的数据库实体主要放在 `entity` 目录（非强制），这只是一个简单的约定。\n\n\n\n## 入门\n\n下面，我们将以 mysql 举例。\n\n\n\n\n### 1、创建 Model\n\n\n我们通过模型和数据库关联，在应用中的模型就是数据库表，在 TypeORM 中，模型是和实体绑定的，每一个实体（Entity) 文件，即是 Model，也是实体（Entity）。\n\n\n在示例中，需要一个实体，我们这里拿 `photo` 举例。新建 entity 目录，在其中添加实体文件 `photo.entity.ts` ，一个简单的实体如下。\n```typescript\n// entity/photo.entity.ts\nexport class Photo {\n  id: number;\n  name: string;\n  description: string;\n  filename: string;\n  views: number;\n  isPublished: boolean;\n}\n```\n要注意，这里的实体文件的每一个属性，其实是和数据库表一一对应的，基于现有的数据库表，我们往上添加内容。\n\n\n### 2、定义实体模型\n\n\n我们使用 `Entity` 来定义一个实体模型类。\n```typescript\n// entity/photo.entity.ts\nimport { Entity } from 'typeorm';\n\n@Entity('photo')\nexport class Photo {\n  id: number;\n  name: string;\n  description: string;\n  filename: string;\n  views: number;\n  isPublished: boolean;\n}\n```\n\n如果表名和当前的实体名不同，可以在参数中指定。\n```typescript\n// entity/photo.entity.ts\nimport { Entity } from 'typeorm';\n\n@Entity('photo_table_name')\nexport class Photo {\n  id: number;\n  name: string;\n  description: string;\n  filename: string;\n  views: number;\n  isPublished: boolean;\n}\n```\n\n\n这些实体列也可以使用 [typeorm_generator](/docs/tool/typeorm_generator) 工具生成。\n\n\n### 3、添加数据库列\n\n\n通过 typeorm 提供的 `@Column` 装饰器来修饰属性，每一个属性对应一个列。\n\n\n```typescript\n// entity/photo.entity.ts\nimport { Entity, Column } from 'typeorm';\n\n@Entity()\nexport class Photo {\n\n  @Column()\n  id: number;\n\n  @Column()\n  name: string;\n\n  @Column()\n  description: string;\n\n  @Column()\n  filename: string;\n\n  @Column()\n  views: number;\n\n  @Column()\n  isPublished: boolean;\n\n}\n```\n\n现在 `id` ， `name` ， `description` ，`filename` ， `views` ， `isPublished` 列将添加到 `photo`  表中。数据库中的列类型是根据您使用的属性类型推断出来的，例如 number 将转换为整数，将字符串转换为 varchar，将布尔值转换为 bool，等等。但是您可以通过在 `@Column`装饰器中显式指定列类型来使用数据库支持的任何列类型。\n\n\n我们生成了带有列的数据库表，但是还剩下一件事。每个数据库表必须具有带主键的列。\n\n\n数据库列包括更多的列选项（ColumnOptions），比如修改列名，指定列类型，列长度等，更多的选项请参考 [官方文档](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/entities.md#%E5%88%97%E9%80%89%E9%A1%B9)。\n\n\n\n\n### 4、创建主键列\n\n\n每个实体必须至少具有一个主键列。要使列成为主键，您需要使用 `@PrimaryColumn`  装饰器。\n\n\n```typescript\n// entity/photo.entity.ts\nimport { Entity, Column, PrimaryColumn } from 'typeorm';\n\n@Entity()\nexport class Photo {\n\n  @PrimaryColumn()\n  id: number;\n\n  @Column()\n  name: string;\n\n  @Column()\n  description: string;\n\n  @Column()\n  filename: string;\n\n  @Column()\n  views: number;\n\n  @Column()\n  isPublished: boolean;\n\n}\n```\n### 5、创建自增主键列\n\n\n现在，如果要设置自增的 id 列，需要将 `@PrimaryColumn` 装饰器更改为 `@PrimaryGeneratedColumn`  装饰器：\n```typescript\n// entity/photo.entity.ts\nimport { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class Photo {\n\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column()\n  name: string;\n\n  @Column()\n  description: string;\n\n  @Column()\n  filename: string;\n\n  @Column()\n  views: number;\n\n  @Column()\n  isPublished: boolean;\n\n}\n```\n\n\n### 6、列数据类型\n\n\n接下来，让我们调整数据类型。默认情况下，字符串映射到类似 `varchar（255）` 的类型（取决于数据库类型）。 Number 映射为类似整数的类型（取决于数据库类型）。但是我们不希望所有列都限制为 varchars 或整数，这个时候可以做一些修改。\n\n\n```typescript\n// entity/photo.entity.ts\nimport { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class Photo {\n\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column({\n  \tlength: 100\n  })\n  name: string;\n\n  @Column('text')\n  description: string;\n\n  @Column()\n  filename: string;\n\n  @Column(\"double\")\n  views: number;\n\n  @Column()\n  isPublished: boolean;\n}\n```\n\n\n示例，不同列名\n```typescript\n@Column({\n  length: 100,\n  name: 'custom_name'\n})\nname: string;\n```\n\n\n此外还有有几种特殊的列类型可以使用：\n\n\n- `@CreateDateColumn` 是一个特殊列，自动为实体插入日期。\n- `@UpdateDateColumn` 是一个特殊列，在每次调用实体管理器或存储库的save时，自动更新实体日期。\n- `@VersionColumn` 是一个特殊列，在每次调用实体管理器或存储库的save时自动增长实体版本（增量编号）。\n- `@DeleteDateColumn` 是一个特殊列，会在调用 soft-delete（软删除）时自动设置实体的删除时间。\n\n比如：\n\n```typescript\n  @CreateDateColumn({\n    type: 'timestamp',\n  })\n  createdDate: Date;\n```\n\n列类型是特定于数据库的。您可以设置数据库支持的任何列类型。有关支持的列类型的更多信息，请参见[此处](https://github.com/typeorm/typeorm/blob/master/docs/entities.md#column-types)。\n\n:::tip\n\n`CreateDateColumn` 和 `UpdateDateColumn` 是依靠第一次同步表结构时，创建列上的默认数据完成的插入日期功能，如果是自己创建的表，需要自行在列上加入默认数据。\n\n:::\n\n\n\n\n### 7、配置连接信息和实体模型\n\n\n请参考 [配置](/docs/env_config) 章节，增加配置文件。\n\n\n然后在 `config.default.ts`  中配置数据库连接信息。\n```typescript\n// src/config/config.default.ts\nimport { Photo } from '../entity/photo.entity';\n\nexport default {\n  // ...\n  typeorm: {\n    dataSource: {\n      default: {\n        /**\n         * 单数据库实例\n         */\n        type: 'mysql',\n        host: '*******',\n        port: 3306,\n        username: '*******',\n        password: '*******',\n        database: undefined,\n        synchronize: false,\t\t    // 如果第一次使用，不存在表，有同步的需求可以写 true，注意会丢数据\n        logging: false,\n\n        // 配置实体模型\n        entities: [Photo],\n\n        // 支持如下的扫描形式，为了兼容我们可以同时进行.js和.ts匹配\n        entities: [\n          'entity',                 // 特定目录\n          '**/*.entity.{j,t}s',     // 通配加后缀匹配\n        ]\n      }\n    }\n  },\n}\n```\n:::tip\n\n- 1. 如果使用的数据库已经有表结构同步的功能，比如云数据库，最好不要开启。如果一定要使用，synchronize 配置最好仅在开发阶段，或者第一次使用，避免造成一致性问题。\n- 2. `entities` 字段配置已经经过框架处理，该字段配置请不要参考原始文档。\n:::\n\n\n `type` 字段你可以使用其他的数据库类型，包括`mysql`, `mariadb`, `postgres`, `cockroachdb`, `better-sqlite3`, `mssql`, `oracle`, `cordova`, `nativescript`, `react-native`, `expo`, or `mongodb`\n\n\n 比如 better-sqlite3，需要以下信息。\n\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  typeorm: {\n    dataSource: {\n      default: {\n        type: 'better-sqlite3',\n        database: path.join(__dirname, '../../test.sqlite'),\n        synchronize: true,\n        logging: true,\n        // ...\n      }\n    }\n  },\n}\n```\n\n\n:::info\n注意：synchronize 字段用于同步表结构。使用 `synchronize: true` 进行生产模式同步是不安全的，在上线后，请把这个字段设置为 false。\n:::\n\n\n### 8、使用 Model 插入数据库数据\n\n\n在常见的 Midway 文件中，使用 `@InjectEntityModel` 装饰器注入我们配置好的 Model。我们所需要做的只是：\n\n\n- 1、创建实体对象\n- 2、执行 `save()`\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from '../entity/photo.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  // save\n  async savePhoto() {\n    // create a entity object\n    let photo = new Photo();\n    photo.name = 'Me and Bears';\n    photo.description = 'I am near polar bears';\n    photo.filename = 'photo-with-bears.jpg';\n    photo.views = 1;\n    photo.isPublished = true;\n\n    // save entity\n    const photoResult = await this.photoModel.save(photo);\n\n    // save success\n    console.log('photo id = ', photoResult.id);\n  }\n}\n```\n\n\n### 9、查询数据\n\n更多的查询参数，请查询 [find文档](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/find-options.md)。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from '../entity/photo.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  // find\n  async findPhotos() {\n\n    // find All\n    let allPhotos = await this.photoModel.find({});\n    console.log(\"All photos from the db: \", allPhotos);\n\n    // find first\n    let firstPhoto = await this.photoModel.findOne({\n      where: {\n        id: 1\n      }\n    });\n    console.log(\"First photo from the db: \", firstPhoto);\n\n    // find one by name\n    let meAndBearsPhoto = await this.photoModel.findOne({\n      where: { name: \"Me and Bears\" }\n    });\n    console.log(\"Me and Bears photo from the db: \", meAndBearsPhoto);\n\n    // find by views\n    let allViewedPhotos = await this.photoModel.find({\n      where: { views: 1 }\n    });\n    console.log(\"All viewed photos: \", allViewedPhotos);\n\n    let allPublishedPhotos = await this.photoModel.find({\n      where: { isPublished: true }\n    });\n    console.log(\"All published photos: \", allPublishedPhotos);\n\n  \t// find and get count\n    let [allPhotos, photosCount] = await this.photoModel.findAndCount({});\n    console.log(\"All photos: \", allPhotos);\n    console.log(\"Photos count: \", photosCount);\n\n  }\n}\n\n```\n\n\n### 10、更新数据库\n\n\n现在，让我们从数据库中加载一个 Photo，对其进行更新并保存。\n\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from '../entity/photo.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  async updatePhoto() {\n\n    let photoToUpdate = await this.photoModel.findOne({\n      where: {\n        id: 1,\n      },\n    });\n    photoToUpdate.name = \"Me, my friends and polar bears\";\n\n    await this.photoModel.save(photoToUpdate);\n  }\n}\n```\n\n### 11、删除数据\n\n`remove` 用于删除给定的实体或实体数组。`delete` 用于按给定的 ID 或者条件删除。\n\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from '../entity/photo.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  async updatePhoto() {\n    /*...*/\n    const photo = await this.photoModel.findOne({\n      where: {\n        id: 1,\n      },\n    });\n\n    // 删除单个\n    await this.photoModel.remove(photo)\n    // 删除多个\n    await this.photoModel.remove([photo1, photo2, photo3]);\n\n    // 按 id 删除\n    await this.photoModel.delete(1);\n    await this.photoModel.delete([1, 2, 3]);\n    await this.photoModel.delete({ name: \"Timber\" });\n  }\n}\n```\n现在，ID = 1的 Photo 将从数据库中删除。\n\n\n此外还有软删除的方法。\n```typescript\nawait this.photoModel.softDelete(1);\n// 使用 restore 方法恢复;\nawait this.photoModel.restore(1);\n```\n\n\n### 12、创建一对一关联\n\n\n让我们与另一个类创建一对一的关系。让我们在 `entity/photoMetadata.entity.ts`  中创建一个新类。这个类包含 photo 的其他元信息。\n\n\n```typescript\nimport { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn } from 'typeorm';\nimport { Photo } from './photo.entity';\n\n@Entity()\nexport class PhotoMetadata {\n\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column(\"int\")\n  height: number;\n\n  @Column(\"int\")\n  width: number;\n\n  @Column()\n  orientation: string;\n\n  @Column()\n  compressed: boolean;\n\n  @Column()\n  comment: string;\n\n  @OneToOne(type => Photo)\n  @JoinColumn()\n  photo: Photo;\n\n}\n```\n\n\n在这里，我们使用一个名为 `@OneToOne`  的新装饰器。它允许我们在两个实体之间创建一对一的关系。`type => Photo`是一个函数，它返回我们要与其建立关系的实体的类。\n\n\n由于语言的特殊性，我们被迫使用一个返回类的函数，而不是直接使用该类。我们也可以将其写为 `() => Photo`  ，但是我们使用 `type => Photo`作为惯例来提高代码的可读性。类型变量本身不包含任何内容。\n\n\n我们还添加了一个 `@JoinColumn`装饰器，它指示关系的这一侧将拥有该关系。关系可以是单向或双向的。关系只有一方可以拥有。关系的所有者端需要使用@JoinColumn装饰器。 如果您运行该应用程序，则会看到一个新生成的表，该表将包含一列，其中包含用于 Photo 关系的外键。\n\n\n```\n+-------------+--------------+----------------------------+\n|                     photo_metadata                      |\n+-------------+--------------+----------------------------+\n| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |\n| height      | int(11)      |                            |\n| width       | int(11)      |                            |\n| comment     | varchar(255) |                            |\n| compressed  | boolean      |                            |\n| orientation | varchar(255) |                            |\n| photoId     | int(11)      | FOREIGN KEY                |\n+-------------+--------------+----------------------------+\n```\n\n\n接下去我们要在代码中关联他们。\n\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from './entity/photo.entity';\nimport { PhotoMetadata } from './entity/photoMetadata.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  @InjectEntityModel(PhotoMetadata)\n  photoMetadataModel: Repository<PhotoMetadata>;\n\n  async updatePhoto() {\n\n  // create a photo\n    let photo = new Photo();\n    photo.name = \"Me and Bears\";\n    photo.description = \"I am near polar bears\";\n    photo.filename = \"photo-with-bears.jpg\";\n    photo.isPublished = true;\n\n    // create a photo metadata\n    let metadata = new PhotoMetadata();\n    metadata.height = 640;\n    metadata.width = 480;\n    metadata.compressed = true;\n    metadata.comment = \"cybershoot\";\n    metadata.orientation = \"portrait\";\n    metadata.photo = photo; // this way we connect them\n\n\n    // first we should save a photo\n    await this.photoModel.save(photo);\n\n    // photo is saved. Now we need to save a photo metadata\n    await this.photoMetadataModel.save(metadata);\n\n    // done\n    console.log(\"Metadata is saved, and relation between metadata and photo is created in the database too\");\n  }\n}\n```\n\n\n### 13、反向关系映射\n\n\n关系映射可以是单向或双向的。当在 PhotoMetadata  和 Photo之间的关系是单向的。关系的所有者是PhotoMetadata，而 Photo对 PhotoMetadata 是一无所知的。这使得从 Photo 端访问 PhotoMetadata 变得很复杂。若要解决此问题，我们添加一个反向的关系映射，使  PhotoMetadata  和 Photo之间变成双向关联。让我们修改我们的实体。\n\n\n```typescript\nimport { Entity } from 'typeorm';\nimport { Column, PrimaryGeneratedColumn, OneToOne, JoinColumn } from 'typeorm';\nimport { Photo } from './photo';\n\n@Entity()\nexport class PhotoMetadata {\n\n  /* ... other columns */\n\n  @OneToOne(type => Photo, photo => photo.metadata)\n  @JoinColumn()\n  photo: Photo;\n}\n```\n```typescript\nimport { Entity } from 'typeorm';\nimport { Entity, Column, PrimaryGeneratedColumn, OneToOne } from 'typeorm';\nimport { PhotoMetadata } from './photoMetadata.entity';\n\n@Entity()\nexport class Photo {\n\n  /* ... other columns */\n\n  @OneToOne(type => PhotoMetadata, photoMetadata => photoMetadata.photo)\n  metadata: PhotoMetadata;\n}\n```\n`photo => photo.metadata` 是一个返回反向映射关系的函数。在这里，我们显式声明 Photo 类的 metadata 属性用于关联 PhotoMetadata。除了传递返回 photo 属性的函数外，您还可以直接将字符串传递给 `@OneToOne`  装饰器，例如 `“metadata”` 。但是我们使用了这种函数回调的方法来让我们的代码写法更简单。\n\n\n请注意，只会在关系映射的一侧使用 `@JoinColumn`  装饰器。无论您放置此装饰器的哪一侧，都是关系的所有者。关系的拥有方在数据库中包含带有外键的列。\n\n\n### 14、加载对象及其依赖关系\n\n\n现在，让我们尝试在单个查询中一起加载出  Photo 和 PhotoMetadata。有两种方法可以执行此操作，使用  `find *`   方法或使用 `QueryBuilder` 功能。让我们首先使用 `find *` 方法。 `find *`  方法允许您使用 `FindOneOptions`  / `FindManyOptions`  接口指定对象。\n\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from './entity/photo.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  // find\n  async findPhoto() {\n\t\t/*...*/\n    let photos = await this.photoModel.find({ relations: [ 'metadata' ] }); // typeorm@0.2.x\n  }\n}\n\n```\n在这里，photos  的值是一个数组，包含了整个数据库的查询结果，并且每个 photo 对象都包含其关联的 metadata 属性。在[此文档](https://github.com/typeorm/typeorm/blob/master/docs/find-options.md)中了解有关 `Find Options` 的更多信息。\n\n\n使用 `Find Options` 很简单，但如果需要更复杂的查询，则应改用 `QueryBuilder` 。 `QueryBuilder`   允许以优雅的方式使用更复杂的查询。\n\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from './entity/photo.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  // find\n  async findPhoto() {\n\t\t/*...*/\n    let photos = await this.photoModel\n      .createQueryBuilder('photo')\n      .innerJoinAndSelect('photo.metadata', 'metadata')\n      .getMany();\n  }\n}\n```\n`QueryBuilder`允许创建和执行几乎任何复杂的 SQL 查询。使用 `QueryBuilder`  时，请像创建  SQL 查询一样思考。在此示例中，“photo”  和 “metadata” 是应用于所选 photos  的别名。您可以使用别名来访问所选数据的列和属性。\n\n\n### 15、使用级联操作自动保存关联对象\n\n\n在我们希望在每次保存另一个对象时都自动保存关联的对象，这个时候可以在关系中设置级联。让我们稍微更改照片的 `@OneToOne`  装饰器。\n\n\n```typescript\nexport class Photo {\n  /// ... other columns\n\n  @OneToOne(type => PhotoMetadata, metadata => metadata.photo, {\n    cascade: true,\n  })\n  metadata: PhotoMetadata;\n}\n```\n使用 `cascade` 允许我们现在不再单独保存 Photo 和 PhotoMetadata，由于级联选项，元数据对象将被自动保存。\n\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from './entity/photo.entity';\nimport { PhotoMetadata } from './entity/photoMetadata.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  async updatePhoto() {\n\n   // create photo object\n    let photo = new Photo();\n    photo.name = \"Me and Bears\";\n    photo.description = \"I am near polar bears\";\n    photo.filename = \"photo-with-bears.jpg\";\n    photo.isPublished = true;\n\n    // create photo metadata object\n    let metadata = new PhotoMetadata();\n    metadata.height = 640;\n    metadata.width = 480;\n    metadata.compressed = true;\n    metadata.comment = \"cybershoot\";\n    metadata.orientation = \"portrait\";\n\n    photo.metadata = metadata;  // this way we connect them\n\n    // save a photo also save the metadata\n    await this.photoModel.save(photo);\n\n    // done\n    console.log(\"Photo is saved, photo metadata is saved too\");\n  }\n}\n```\n\n\n注意，我们现在设置 Photo 的元数据，而不需要像之前那样设置元数据的 Photo 属性。这仅当您从 Photo 这边将 Photo 连接到 PhotoMetadata 时，级联功能才有效。如果在 PhotoMetadata 侧设置，则不会自动保存。\n\n\n### 16、创建多对一/一对多关联\n\n\n让我们创建一个多对一/一对多关系。假设一张照片有一个作者，每个作者可以有很多照片。首先，让我们创建一个 Author 类：\n```typescript\nimport { Entity } from 'typeorm';\nimport { Column, PrimaryGeneratedColumn, OneToMany, JoinColumn } from 'typeorm';\nimport { Photo } from './entity/photo.entity';\n\n@Entity()\nexport class Author {\n\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column()\n  name: string;\n\n  @OneToMany(type => Photo, photo => photo.author) // note: we will create author property in the Photo class below\n  photos: Photo[];\n}\n```\n`Author`  包含了一个反向关系。 `OneToMany`  和 `ManyToOne` 需要成对出现。\n\n\n现在，将关系的所有者添加到 Photo 实体中：\n```typescript\nimport { Entity } from 'typeorm';\nimport { Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';\nimport { PhotoMetadata } from './photoMetadata.entity';\nimport { Author } from './author.entity';\n\n@Entity()\nexport class Photo {\n\n  /* ... other columns */\n\n  @ManyToOne(type => Author, author => author.photos)\n  author: Author;\n}\n```\n\n\n在多对一/一对多关系中，所有者方始终是多对一。这意味着使用 `@ManyToOne`  的类将存储相关对象的 ID。\n\n\n运行应用程序后，ORM 将创建 `author` 表：\n\n\n```\n+-------------+--------------+----------------------------+\n|                          author                         |\n+-------------+--------------+----------------------------+\n| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |\n| name        | varchar(255) |                            |\n+-------------+--------------+----------------------------+\n```\n它还将修改 `photo` 表，添加新的 `author`  列并为其创建外键：\n```\n+-------------+--------------+----------------------------+\n|                         photo                           |\n+-------------+--------------+----------------------------+\n| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |\n| name        | varchar(255) |                            |\n| description | varchar(255) |                            |\n| filename    | varchar(255) |                            |\n| isPublished | boolean      |                            |\n| authorId    | int(11)      | FOREIGN KEY                |\n+-------------+--------------+----------------------------+\n```\n\n\n### 17、创建多对多关联\n\n\n让我们创建一个多对一/多对多关系。假设一张照片可以在许多相册中，并且每个相册可以包含许多照片。让我们创建一个 `Album` 类。\n\n\n```typescript\nimport { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';\n\n@Entity()\nexport class Album {\n\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column()\n  name: string;\n\n  @ManyToMany(type => Photo, photo => photo.albums)\n  @JoinTable()\n  photos: Photo[];\n}\n```\n\n\n`@JoinTable` 用来指明这是关系的所有者。\n\n\n现在，将反向关联添加到 `Photo` 。\n\n\n```typescript\nexport class Photo {\n  /// ... other columns\n\n  @ManyToMany(type => Album, album => album.photos)\n  albums: Album[];\n}\n```\n运行应用程序后，ORM将创建一个 album_photos_photo_albums 联结表：\n\n\n```\n+-------------+--------------+----------------------------+\n|                album_photos_photo_albums                |\n+-------------+--------------+----------------------------+\n| album_id    | int(11)      | PRIMARY KEY FOREIGN KEY    |\n| photo_id    | int(11)      | PRIMARY KEY FOREIGN KEY    |\n+-------------+--------------+----------------------------+\n```\n\n\n现在，让我们将相册和照片插入数据库：\n\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from './entity/photo.entity';\nimport { Album } from './entity/album.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  @InjectEntityModel(Album)\n  albumModel: Repository<Album>\n\n  async updatePhoto() {\n\n    // create a few albums\n    let album1 = new Album();\n    album1.name = \"Bears\";\n    await this.albumModel.save(album1);\n\n    let album2 = new Album();\n    album2.name = \"Me\";\n    await this.albumModel.save(album2);\n\n    // create a few photos\n    let photo = new Photo();\n    photo.name = \"Me and Bears\";\n    photo.description = \"I am near polar bears\";\n    photo.filename = \"photo-with-bears.jpg\";\n    photo.albums = [album1, album2];\n    await this.photoModel.save(photo);\n\n\n    // now our photo is saved and albums are attached to it\n    // now lets load them:\n    const loadedPhoto = await this.photoModel.findOne(1, { relations: [\"albums\"] });  // typeorm@0.2.x\n  }\n}\n```\n`loadedPhoto` 的值为：\n```json\n{\n  id: 1,\n  name: \"Me and Bears\",\n  description: \"I am near polar bears\",\n  filename: \"photo-with-bears.jpg\",\n  albums: [{\n    id: 1,\n    name: \"Bears\"\n  }, {\n    id: 2,\n    name: \"Me\"\n  }]\n}\n```\n\n### 18、使用 QueryBuilder\n\n\n您可以使用QueryBuilder来构建几乎任何复杂的SQL查询。例如，您可以这样做：\n\n\n```typescript\nlet photos = await this.photoModel\n    .createQueryBuilder(\"photo\") // first argument is an alias. Alias is what you are selecting - photos. You must specify it.\n    .innerJoinAndSelect(\"photo.metadata\", \"metadata\")\n    .leftJoinAndSelect(\"photo.albums\", \"album\")\n    .where(\"photo.isPublished = true\")\n    .andWhere(\"(photo.name = :photoName OR photo.name = :bearName)\")\n    .orderBy(\"photo.id\", \"DESC\")\n    .skip(5)\n    .take(10)\n    .setParameters({ photoName: \"My\", bearName: \"Mishka\" })\n    .getMany();\n```\n该查询选择所有带有 “My” 或 “Mishka” 名称的已发布照片。它将从位置 5 开始返回结果（分页偏移），并且将仅选择 10 个结果（分页限制）。选择结果将按 ID 降序排列。该照片的相册将 left-Joined，元数据将自动关联。\n\n\n您将在应用程序中大量使用查询生成器。在 [此处](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/select-query-builder.md) 了解有关QueryBuilder的更多信息。\n\n\n### 19、Event Subscriber\n\n\ntypeorm 提供了一个事件订阅机制，方便在做一些数据库操作时的日志输出，为此 midway 提供了一个 `EventSubscriberModel` 装饰器，用来标注事件订阅类，代码如下。\n\n\n```typescript\nimport { EventSubscriberModel } from '@midwayjs/typeorm';\nimport { EntitySubscriberInterface, InsertEvent, UpdateEvent, RemoveEvent } from 'typeorm';\n\n@EventSubscriberModel()\nexport class EverythingSubscriber implements EntitySubscriberInterface {\n\n  /**\n   * Called before entity insertion.\n   */\n  beforeInsert(event: InsertEvent<any>) {\n    console.log(`BEFORE ENTITY INSERTED: `, event.entity);\n  }\n\n  /**\n   * Called before entity insertion.\n   */\n  beforeUpdate(event: UpdateEvent<any>) {\n    console.log(`BEFORE ENTITY UPDATED: `, event.entity);\n  }\n\n  /**\n   * Called before entity insertion.\n   */\n  beforeRemove(event: RemoveEvent<any>) {\n    console.log(`BEFORE ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity);\n  }\n\n  /**\n   * Called after entity insertion.\n   */\n  afterInsert(event: InsertEvent<any>) {\n    console.log(`AFTER ENTITY INSERTED: `, event.entity);\n  }\n\n  /**\n\t * Called after entity insertion.\n\t */\n  afterUpdate(event: UpdateEvent<any>) {\n    console.log(`AFTER ENTITY UPDATED: `, event.entity);\n  }\n\n  /**\n   * Called after entity insertion.\n   */\n  afterRemove(event: RemoveEvent<any>) {\n    console.log(`AFTER ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity);\n  }\n\n  /**\n   * Called after entity is loaded.\n   */\n  afterLoad(entity: any) {\n    console.log(`AFTER ENTITY LOADED: `, entity);\n  }\n\n}\n```\n\n这个订阅类提供了一些常用的接口，用来在数据库操作时执行一些事情。\n\n同时，我们需要把订阅类加到配置中。\n\n```typescript\n// src/config/config.default.ts\nimport { EverythingSubscriber } from '../event/subscriber';\n\nexport default {\n  // ...\n  typeorm: {\n    dataSource: {\n      default: {\n        // ...\n        entities: [Photo],\n        // 传入订阅类\n        subscribers: [EverythingSubscriber]\n      }\n    }\n  },\n}\n```\n\n\n\n## Repository API\n\n更多 API 请查看 [官网文档](https://github.com/typeorm/typeorm/blob/master/docs/repository-api.md)。\n\n\n\n\n## 高级功能\n### 多数据库支持\n\n\n有时候，我们一个应用中会有多个数据库连接（Connection）的情况，这个时候会有多个配置。我们使用 DataSource 标准的 **对象的形式 **来定义配置。\n\n\n比如下面定义了 `default` 和 `test` 两个数据库连接（Connection）。\n\n\n```typescript\nimport { join } from 'path';\n\nexport default {\n  typeorm: {\n    dataSource: {\n      default: {\n        type: 'better-sqlite3',\n        database: join(__dirname, '../../default.sqlite'),\n        // ...\n      },\n      test: {\n        type: 'mysql',\n        host: '127.0.0.1',\n        port: 3306,\n        // ...\n      }\n    }\n  }\n}\n```\n\n\n在使用时，需要指定模型归属于哪个连接（Connection）。\n```typescript\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { User } from './entity/user.entity';\n\nexport class XXX {\n\n  @InjectEntityModel(User, 'test')\n  testUserModel: Repository<User>;\n\n  //...\n}\n```\n\n\n\n### 列值转换\n\n我们可以在实体定义中处理列值转换。\n\n利用列装饰器的 `transformer` 参数，可以进行出入参的处理，比如对时间格式化。\n\n```typescript\nimport { Entity, Column, CreateDateColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';\nimport * as dayjs from 'dayjs';\n\nconst dateTransformer = {\n  from: (value: Date | number) => {\n    return dayjs(typeof value === 'number' ? value: value.getTime()).format('YYYY-MM-DD HH:mm:ss');\n  },\n  to: () => new Date(),\n};\n\n@Entity()\nexport class Photo {\n  // ...\n\n  @CreateDateColumn({\n    type: 'timestamp',\n    transformer: dateTransformer,\n  })\n  createdAt: Date;\n}\n\n```\n\n\n\n### 指定默认数据源\n\n在包含多个数据源时，可以指定默认的数据源。\n\n```typescript\nexport default {\n  // ...\n  typeorm: {\n    dataSource: {\n      default1: {\n        // ...\n      },\n      default2: {\n        // ...\n      },\n    },\n    // 多个数据源时可以用这个指定默认的数据源\n    defaultDataSourceName: 'default1',\n  },\n};\n```\n\n\n\n### 获取数据源\n\n数据源即创建出的 DataSource 对象，我们可以通过注入内置的数据源管理器来获取。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport { TypeORMDataSourceManager } from '@midwayjs/typeorm';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n\n  async onReady(container: IMidwayContainer) {\n    const dataSourceManager = await container.getAsync(TypeORMDataSourceManager);\n  \tconst conn = dataSourceManager.getDataSource('default');\n    console.log(dataSourceManager.isConnected(conn));\n  }\n}\n```\n\n从 v3.8.0 开始，也可以通过装饰器注入。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport { InjectDataSource } from '@midwayjs/typeorm';\nimport { DataSource } from 'typeorm';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n\n  // 注入默认数据源\n  @InjectDataSource()\n  defaultDataSource: DataSource;\n\n  // 注入自定义数据源\n  @InjectDataSource('default1')\n  customDataSource: DataSource;\n\n  async onReady(container: IMidwayContainer) {\n    // ...\n  }\n}\n```\n\n\n\n### 日志\n\n数据源在未配置日志对象时，组件会自动创建一个 `typeormLogger`，用于保存执行的 SQL 信息，方便排查问题和 SQL 审核。\n\n默认配置为：\n\n```typescript\nexport default {\n  midwayLogger: {\n    clients: {\n      typeormLogger: {\n        fileLogName: 'midway-typeorm.log',\n        enableError: false,\n        level: 'info',\n      },\n    },\n  }\n}\n```\n\n我们可以使用普通日志的配置方式进行调整，如果不希望生成日志，可以配置关闭。\n\n```typescript\nexport default {\n  // ...\n  typeorm: {\n    default: {\n      // 所有数据源关闭\n      logging: false,\n    },\n    dataSource: {\n      default: {\n        // 单个数据源关闭\n        logging: false,\n      },\n    },\n  },\n};\n```\n\n\n\n### 事务\n\ntypeorm 的事务需要先获取到数据源，然后开启事务。\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { TypeORMDataSourceManager } from '@midwayjs/typeorm';\nimport { UserDTO } from '../entity/user';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  dataSourceManager: TypeORMDataSourceManager;\n\n  async updateUser(user: UserDTO) {\n\n    // get dataSource\n    const dataSource = this.dataSourceManager.getDataSource('default');\n\n    // start transaction\n    await dataSource.transaction(async (transactionalEntityManager) => {\n      // run code\n      await transactionalEntityManager.save(UserDTO, user);\n    });\n  }\n\n}\n```\n\n更多的细节，可以参考 [文档](https://github.com/typeorm/typeorm/blob/master/docs/transactions.md)。\n\n\n\n### CLI\n\nTypeORM 默认提供了一个 CLI，用来创建 entity，migration 等，更多文档请查看 [这里](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/using-cli.md)。\n\n由于 TypeORM 的默认配置和 Midway 不同，我们提供了一个简单的修改版本，用于适配 Midway 的数据源配置。\n\n检查安装情况：\n\n```bash\n$ npx mwtypeorm -h\n```\n\n常用的命令有\n\n **创建空 Entity**\n\n将会创建一个 `src/entity/User.ts` 文件。\n\n```bash\n$ npx mwtypeorm entity:create src/entity/User\n```\n\n**创建 Migration**\n\n将会根据现有数据源生成一个 `src/migration/******-photo.entity.ts` 文件。\n\n比如配置如下：\n\n```typescript\nexport default {\n  typeorm: {\n    dataSource: {\n      'default': {\n        // ...\n        entities: [\n          '**/entity/*.entity{.ts,.js}'\n        ],\n        migrations: [\n          '**/migration/*.ts'\n        ],\n      },\n  },\n}\n```\n\n可以执行下面的命令，将修改后的 Entity 生成迁移文件。\n\n```bash\n$ npx mwtypeorm migration:generate -d ./src/config/config.default.ts src/migration/photo\n```\n\n:::caution\n\n注意：上面的 entities 配置由于需要再 CLI 和 Midway 间复用，采用了两者都支持的扫描写法。\n\n:::\n\n\n\n### 关于表结构同步\n\n\n- 如果你已有表结构，想自动创建 Entity，使用 [生成器](https://www.npmjs.com/package/typeorm-model-generator)\n- 如果已经有 Entity 代码，想创建表结构请使用配置中的  `synchronize:  true` ，注意可能会丢失数据\n- 如果已经上线，但是又修改了表结构，可以使用 CLI 中的 `migration:generate`\n\n\n\n## 常见问题\n\n\n### Handshake inactivity timeout\n\n\n一般是网络原因，如果本地出现，可以 ping 但是telnet不通，可以尝试执行如下命令：\n```bash\n$ sudo sysctl -w net.inet.tcp.sack=0\n```\n\n\n\n### 关于 mysql 时间列的时区展示\n\n一般情况下，数据库中保存的是 UTC 时间，如果你希望返回当前时区的时间，可以使用下面的方式\n\n**1、检查 mysql 数据库所在的环境**\n\n比如下面默认的时区其实就是系统 UTC 时间，可以调整为 `+08:00`。\n\n```text\nmysql> show global variables like '%time_zone%';\n+------------------+--------+\n| Variable_name    | Value  |\n+------------------+--------+\n| system_time_zone | UTC    |\n| time_zone        | SYSTEM |\n+------------------+--------+\n2 rows in set (0.05 sec)\n\n```\n\n**2、检查服务代码部署的环境**\n\n尽量和数据库所在的环境一致，如果不一致，请在配置中设置 `timezone` （设置为和 mysql 一致）。\n\n```typescript\nexport default {\n  typeorm: {\n    dataSource: {\n      default: {\n        type: 'mysql',\n        // ...\n        timezone: '+08:00',\n      },\n    },\n  },\n}\n```\n\n\n\n### 时间列返回字符串\n\n配置 dateStrings 可以使 mysql 返回时间按 DATETIME 格式返回，只对 mysql 生效。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  typeorm: {\n    dataSource: {\n      default: {\n        //...\n        dateStrings: true,\n      }\n    }\n  },\n}\n```\n\n如果使用了 `@CreateDateColumn` 和 `@UpdateDateColumn` ，可以调整实体返回类型。\n\n```typescript\n@UpdateDateColumn({\n  name: \"gmt_modified\",\n  type: 'timestamp'\n})\ngmtModified: string;\n\n@CreateDateColumn({\n  name: \"gmt_create\",\n  type: 'timestamp',\n})\ngmtCreate: string;\n```\n\n\n\n效果如下：\n\n**配置前：**\n\n```typescript\ngmtModified: 2021-12-13T03:49:43.000Z,\ngmtCreate: 2021-12-13T03:49:43.000Z\n```\n**配置后：**\n\n```typescript\ngmtModified: '2021-12-13 11:49:43',\ngmtCreate: '2021-12-13 11:49:43'\n```\n\n\n\n### 同时安装 mysql 和 mysql2\n\n在 node_modules 中同时有 mysql  和 mysql2 时，typeorm 会自动加载 mysql，而不是 mysql2。\n\n这个时候如需使用 mysql2，请指定  driver。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  typeorm: {\n    dataSource: {\n      default: {\n        //...\n        type: 'mysql',\n        driver: require('mysql2'),\n      }\n    }\n  },\n}\n```\n\n\n\n\n###  Cannot read properties of undefined (reading 'getRepository')\n\n一般是配置不正确，可以考虑两方面的配置：\n\n- 1、检查 `config.default.ts` 中的 `entities` 配置是否正确\n- 2、检查 `configuration.ts` 文件，确认是否引入 orm\n","site/docs/extensions/oss.md":"# 阿里云对象存储（OSS）\n\n阿里云对象存储服务（Object Storage Service，简称 OSS），是阿里云提供的海量、安全、低成本、高可靠的云存储服务。其数据设计持久性不低于 99.999999999%，服务设计可用性不低于 99.99%。具有与平台无关的 RESTful API 接口，您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。\n\n`@midwayjs/oss` 组件就是在 midway 体系下用于对接 OSS 服务的 sdk。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 前置条件\n\n\n使用 OSS 组件，你需要提前申请一个 OSS Bucket。Bucket 是 OSS 的存储库的概念，你的文件都将存储在这个库里。\n\n\n- OSS 对象存储官网：[https://www.aliyun.com/product/oss](https://www.aliyun.com/product/oss)\n- 什么是对象存储：[https://www.alibabacloud.com/help/zh/doc-detail/31817.htm](https://www.alibabacloud.com/help/zh/doc-detail/31817.htm)\n\n\n## 安装依赖\n\n`@midwayjs/oss` 是主要的功能包。\n\n```bash\n$ npm i @midwayjs/oss@4 --save\n```\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/oss\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n\n## 引入组件\n\n\n首先，引入 组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as oss from '@midwayjs/oss';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    oss\t\t// 导入 oss 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n\n## 配置 OSS\n\n\nOSS 组件需要配置后才能使用。需要填写 OSS 的 bucket、accessKeyId、accessKeySecret 等必要信息。\n\n\n支持普通 oss 客户端和 oss 集群客户端，基于 [ali-oss](https://github.com/ali-sdk/ali-oss/) 这个包。\n\n\n比如：\n\n**普通的 oss bucket 配置**\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  oss: {\n    // normal oss bucket\n    client: {\n      accessKeyId: 'your access key',\n      accessKeySecret: 'your access secret',\n      bucket: 'your bucket name',\n      endpoint: 'oss-cn-hongkong.aliyuncs.com',\n      timeout: '60s',\n    },\n  },\n}\n```\n\n\n**集群（cluster） 模式的 oss bucket 配置，需要配置多个**\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  oss: {\n    // need to config all bucket information under cluster\n    client: {\n      clusters: [{\n        endpoint: 'host1',\n        accessKeyId: 'id1',\n        accessKeySecret: 'secret1',\n      }, {\n        endpoint: 'host2',\n        accessKeyId: 'id2',\n        accessKeySecret: 'secret2',\n      }],\n      schedule: 'masterSlave', //default is `roundRobin`\n      timeout: '60s',\n    },\n  },\n}\n```\n\n**STS 模式**\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  oss: {\n    // if config.sts == true, oss will create STS client\n    client: {\n      sts: true,\n      accessKeyId: 'your access key',\n      accessKeySecret: 'your access secret',\n    },\n  },\n}\n```\n\n## 使用组件\n\n\n可以直接获取 `OSSService`，然后调用接口，比如，保存文件。\n```typescript\nimport { OSSService } from '@midwayjs/oss';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  ossService: OSSService;\n\n  async saveFile() {\n\n    const localFile = join(__dirname, 'test.log');\n    const result = await this.ossService.put('/test/test.log', localFile);\n\n    // => result.url\n  }\n}\n```\n\n\n如果配置的是 STS 模式，客户端可以使用 `OSSSTSService` 。\n```typescript\nimport { OSSSTSService } from '@midwayjs/oss';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  stsService: OSSSTSService;\n\n  async saveFile() {\n\n    const roleArn = '******';\t\t// 这里是阿里云角色的 arn\n    const result = await this.stsService.assumeRole(roleArn);\n\n    // result.credentials.AccessKeyId\n    // result.credentials.AccessKeySecret;\n    // result.credentials.SecurityToken;\n  }\n}\n```\n\n更多的 OSS 客户端 API，请查看 [OSS 文档](https://github.com/ali-sdk/ali-oss)。\n\n\n## 使用多个 OSS Bucket\n\n\n有些应用需要访问多个 oss bucket，那么就需要配置 `oss.clients`。\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  oss: {\n    clients: {\n      bucket1: {\n        bucket: 'bucket1',\n        // ...\n      },\n      bucket2: {\n        bucket: 'bucket2',\n        // ...\n      },\n    },\n    // client, clients，createInstance 方法共享的配置\n    default: {\n      endpoint: '',\n      accessKeyId: '',\n      accessKeySecret: '',\n    },\n  },\n  // other custom config\n  bucket3: {\n    bucket: 'bucket3',\n    // ...\n  },\n}\n```\n\n可以使用 `OSSServiceFactory` 获取不同的实例。\n\n```typescript\nimport { OSSServiceFactory } from '@midwayjs/oss';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  ossServiceFactory: OSSServiceFactory;\n\n  @Config('bucket3')\n  bucket3Config;\n\n  async saveFile() {\n\n    // 默认获取的类型是 OSSService\n    const bucket1 = this.ossServiceFactory.get('bucket1');\n    const bucket2 = this.ossServiceFactory.get('bucket2');\n\n    // 如果是 STS，需要设置泛型联系\n    // const bucket1 = this.ossServiceFactory.get<OSSSTSService>('bucket1');\n\n    // 会合并 config.bucket3 和 config.oss.default\n    const bucket3 = await this.ossServiceFactory.createInstance(this.bucket3Config, 'bucket3');\n    // 传了名字之后也可以从 factory 中获取\n    bucket3 = this.ossServiceFactory.get('bucket3');\n\n  }\n}\n```\n","site/docs/extensions/passport.md":"# 身份验证\n\n身份验证是大多数 Web 应用程序的重要组成部分。因此 Midway 封装了目前 Nodejs 中最流行的 Passport 库。\n\n相关信息：\n\n| web 支持情况      |     |\n| ----------------- | --- |\n| @midwayjs/koa     | ✅  |\n| @midwayjs/faas    | ✅  |\n| @midwayjs/web     | ✅  |\n| @midwayjs/express | ✅  |\n\n从 v3.4.0 开始 Midway 自行维护 passport，将不再需要引入社区包和类型包。\n\n## 一些概念\n\npassport 是社区使用较多的身份验证库，通过称为策略的可扩展插件进行身份验证请求。\n\n它本身包含几个部分：\n\n- 1、验证的策略，比如 jwt 验证，github 验证，oauth 验证等，passport 最为丰富的也是这块\n- 2、执行策略之后，中间件的逻辑处理和配置，比如成功或者失败后的跳转，报错等\n\n## 安装依赖\n\n安装 `npm i @midwayjs/passport` 和相关策略依赖。\n\n```bash\n## 必选\n$ npm i @midwayjs/passport@4 --save\n\n## 可选\n## 下面安装本地策略\n$ npm i passport-local --save\n$ npm i @types/passport-local --save-dev\n## 下面安装 Github 策略\n$ npm i passport-github --save\n## 下面安装 Jwt 策略\n$ npm i passport-jwt --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/passport\": \"^4.0.0\",\n    // 本地策略\n    \"passport-local\": \"^1.0.0\"\n    // Jwt 策略\n    \"passport-jwt\": \"^4.0.0\",\n    // Github 策略\n    \"passport-github\": \"^1.1.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // 本地策略\n    \"@types/passport-local\": \"^1.0.34\",\n    // Jwt 策略\n    \"@types/passport-jwt\": \"^3.0.6\",\n    // Github 策略\n    \"@types/passport-github\": \"^1.1.7\",\n    // ...\n  }\n}\n```\n\n## 启用组件\n\n首先启用组件。\n\n```typescript\n// src/configuration.ts\n\nimport { join } from 'path';\nimport { ILifeCycle } from '@midwayjs/core';\nimport { Configuration } from '@midwayjs/core';\nimport * as passport from '@midwayjs/passport';\n\n@Configuration({\n  imports: [\n    // ...\n    passport,\n  ],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration implements ILifeCycle {}\n```\n\n## 策略示例\n\n这里我们以使用本地认证策略，和 Jwt 策略作为演示。\n\n### 示例：本地策略\n\n我们以 `passport-local` 来介绍 Passport 策略在 Midway 中如何使用， `passport-local` 的官方文档示例如下，通过 `passport.use` 加载一个策略，策略的验证逻辑是一个 `verify` 方法，包含 callback 参数，其余的策略的参数都在构造器中。\n\n```typescript\npassport.use(\n  // 初始化一个策略\n  new LocalStrategy({\n      usernameField: 'username',\n      passwordField: 'password',\n      passReqToCallback: true,\n      session: false\n    },\n    function verify(username, password, done) {\n      User.findOne({ username: username }, function (err, user) {\n        if (err) { return done(err); }\n        if (!user) { return done(null, false); }\n        if (!user.verifyPassword(password)) { return done(null, false); }\n        return done(null, user);\n      });\n    }\n\t)\n);\n```\n\nMidway 对此进行了改造，通过 `@CustomStrategy` 和 `PassportStrategy` 类继承一个 Passport 现有策略。\n\n异步的 `validate`  方法代替原有的 `verify` 方法，`validate` 方法返回验证后的用户结果，方法的参数和原有对应的策略一致。\n\n在 Midway 中编写的效果如下：\n\n```typescript\n// src/strategy/local.strategy.ts\n\nimport { CustomStrategy, PassportStrategy } from '@midwayjs/passport';\nimport { Strategy, IStrategyOptions } from 'passport-local';\nimport { Repository } from 'typeorm';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { UserEntity } from './user';\nimport * as bcrypt from 'bcrypt';\n\n@CustomStrategy()\nexport class LocalStrategy extends PassportStrategy(Strategy) {\n  @InjectEntityModel(UserEntity)\n  userModel: Repository<UserEntity>;\n\n  // 策略的验证\n  async validate(username, password) {\n    const user = await this.userModel.findOneBy({ username });\n    if (!user) {\n      throw new Error('用户不存在 ' + username);\n    }\n    if (!await bcrypt.compare(password, user.password)) {\n      throw new Error('密码错误 ' + username);\n    }\n\n    return user;\n  }\n\n  // 当前策略的构造器参数\n  getStrategyOptions(): IStrategyOptions {\n    return {\n      usernameField: 'username',\n      passwordField: 'password',\n      passReqToCallback: true,\n      session: false\n    };\n  }\n}\n```\n\n:::tip\n\n注意：validate 方法是社区策略 verify 的 Promise 化替代方法，你无需在最后传递 callback 参数。\n\n:::\n\n在 `passport-local` 的官方文档中，实现完策略后，需要作为中间件加载到业务中，比如：\n\n```typescript\napp.post('/login/password', passport.authenticate('local', {\n  successRedirect: '/',\n  failureRedirect: '/login'\n}));\n```\n\n:::tip\n\n这里的 `local` 是 `passport-local` 内部的名字。\n\n:::\n\n在 Midway 中，也需要将上述实现的 `LocalStrategy` 通过中间件加载。\n\n自定义一个中间件继承 `PassportMiddleware` 扩展出的基础中间件，示例如下。\n\n```typescript\n// src/middleware/local.middleware.ts\n\nimport { Middleware } from '@midwayjs/core';\nimport { PassportMiddleware, AuthenticateOptions } from '@midwayjs/passport';\nimport { LocalStrategy } from '../strategy/local.strategy';\n\n@Middleware()\nexport class LocalPassportMiddleware extends PassportMiddleware(LocalStrategy) {\n  // 设置 AuthenticateOptions\n  getAuthenticateOptions(): Promise<AuthenticateOptions> | AuthenticateOptions {\n    return {\n      failureRedirect: '/login',\n    };\n  }\n}\n```\n将中间件加载到全局或者路由。\n\n```typescript\n// src/controller.ts\nimport { Post, Inject, Controller } from '@midwayjs/core';\nimport { LocalPassportMiddleware } from '../middleware/local.middleware';\n\n@Controller('/')\nexport class LocalController {\n  @Post('/passport/local', { middleware: [LocalPassportMiddleware] })\n  async localPassport() {\n    console.log('local user: ', this.ctx.state.user);\n    return this.ctx.state.user;\n  }\n}\n```\n\n使用 curl 模拟一次请求。\n\n```bash\ncurl -X POST http://localhost:7001/passport/local -d '{\"username\": \"demo\", \"password\": \"1234\"}' -H \"Content-Type: application/json\"\n\n结果 {\"username\": \"demo\", \"password\": \"1234\"}\n```\n\n:::caution\n\n注意：如果将中间件放到全局，记得忽略需要登录的路由，否则请求会死循环。\n\n:::\n\n\n\n### 示例：Jwt 策略\n\n首先需要 **额外安装** 依赖和策略：\n\n```bash\n$ npm i @midwayjs/jwt passport-jwt --save\n```\n\n额外启用 jwt 组件。\n\n```typescript\n// configuration.ts\n\nimport { join } from 'path';\nimport * as jwt from '@midwayjs/jwt';\nimport { Configuration, ILifeCycle } from '@midwayjs/core';\nimport * as passport from '@midwayjs/passport';\n\n@Configuration({\n  imports: [\n    // ...\n    jwt,\n    passport,\n  ],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration implements ILifeCycle {}\n```\n\n然后在配置中设置，默认未加密，请不要把敏感信息存放在 payload 中。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  jwt: {\n    secret: 'xxxxxxxxxxxxxx', // fs.readFileSync('xxxxx.key')\n    expiresIn: '2d', // https://github.com/vercel/ms\n  },\n};\n```\n\n```typescript\n// src/strategy/jwt.strategy.ts\n\nimport { CustomStrategy, PassportStrategy } from '@midwayjs/passport';\nimport { Strategy, ExtractJwt } from 'passport-jwt';\nimport { Config } from '@midwayjs/core';\n\n@CustomStrategy()\nexport class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {\n  @Config('jwt')\n  jwtConfig;\n\n  async validate(payload) {\n    return payload;\n  }\n\n  getStrategyOptions(): any {\n    return {\n      secretOrKey: this.jwtConfig.secret,\n      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),\n    };\n  }\n}\n```\n\n:::tip\n\n注意：validate 方法是社区策略 verify 的 Promise 化替代方法，你无需在最后传递 callback 参数。\n\n:::\n\n```typescript\n// src/middleware/jwt.middleware.ts\n\nimport { Middleware } from '@midwayjs/core';\nimport { PassportMiddleware, AuthenticateOptions } from '@midwayjs/passport';\nimport { JwtStrategy } from '../strategy/jwt.strategy';\n\n@Middleware()\nexport class JwtPassportMiddleware extends PassportMiddleware(JwtStrategy) {\n  getAuthenticateOptions(): Promise<AuthenticateOptions> | AuthenticateOptions {\n    return {};\n  }\n}\n```\n\n```typescript\nimport { Post, Inject, Controller } from '@midwayjs/core';\nimport { Context } from '@midwayjs/koa';\nimport { JwtService } from '@midwayjs/jwt';\nimport { JwtPassportMiddleware } from '../middleware/jwt.middleware';\n\n@Controller('/')\nexport class JwtController {\n  @Inject()\n  jwt: JwtService;\n\n  @Inject()\n  ctx: Context;\n\n  @Post('/passport/jwt', { middleware: [JwtPassportMiddleware] })\n  async jwtPassport() {\n    console.log('jwt user: ', this.ctx.state.user);\n    return this.ctx.state.user;\n  }\n\n  @Post('/jwt')\n  async genJwt() {\n    return {\n      t: await this.jwt.sign({ msg: 'Hello Midway' }),\n    };\n  }\n}\n```\n\n使用 curl 模拟请求\n\n```bash\ncurl -X POST http://127.0.0.1:7001/jwt\n\n结果 {\"t\": \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"}\n\ncurl http://127.0.0.1:7001/passport/jwt -H \"Authorization: Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n\n结果 {\"msg\": \"Hello Midway\",\"iat\": 1635468727,\"exp\": 1635468827}\n\n```\n\n## 自定义其他策略\n\n`@midwayjs/passport` 支持自定义[其他策略](http://www.passportjs.org/packages/)，这里以 Github OAuth 为例。\n首先 `npm i passport-github`，之后编写如下代码：\n\n```typescript\n// github-strategy.ts\n\nimport { CustomStrategy, PassportStrategy } from '@midwayjs/passport';\nimport { Strategy, StrategyOptions } from 'passport-github';\n\nconst GITHUB_CLIENT_ID = 'xxxxxx',\n  GITHUB_CLIENT_SECRET = 'xxxxxxxx';\n\n@CustomStrategy()\nexport class GithubStrategy extends PassportStrategy(Strategy, 'github') {\n  async validate(...payload) {\n    return payload;\n  }\n\n  getStrategyOptions(): StrategyOptions {\n    return {\n      clientID: GITHUB_CLIENT_ID,\n      clientSecret: GITHUB_CLIENT_SECRET,\n      callbackURL: 'https://127.0.0.1:7001/auth/github/cb',\n    };\n  }\n}\n```\n\n```typescript\n// src/middleware/github.middleware.ts\n\nimport { AuthenticateOptions, PassportMiddleware } from '@midwayjs/passport';\nimport { Middleware } from '@midwayjs/core';\nimport { GithubStrategy } from './githubStrategy';\n\n@Middleware()\nexport class GithubPassportMiddleware extends PassportMiddleware(GithubStrategy) {\n  getAuthenticateOptions(): AuthenticateOptions | Promise<AuthenticateOptions> {\n    return {};\n  }\n}\n```\n\n```typescript\n// src/controller/auth.controller.ts\n\nimport { Controller, Get, Inject } from '@midwayjs/core';\nimport { GithubPassportMiddleware } from '../../middleware/github';\n\n@Controller('/oauth')\nexport class AuthController {\n  @Inject()\n  ctx;\n\n  @Get('/github', { middleware: [GithubPassportMiddleware] })\n  async githubOAuth() {}\n\n  @Get('/github/cb', { middleware: [GithubPassportMiddleware] })\n  async githubOAuthCallback() {\n    return this.ctx.state.user;\n  }\n}\n```\n\n## 策略选项\n\n| 选项                | 类型    | 描述                                              |\n| ------------------- | ------- | ------------------------------------------------- |\n| failureRedirect     | string  | 失败跳转的 url                                    |\n| session             | boolean | 默认 true，开启后，会自动将用户信息设置到 session |\n| sessionUserProperty | string  | 设置到 session 上的 key，默认 user                |\n| userProperty        | string  | 设置到 ctx.state 或者 req 上的 key，默认 user     |\n| successRedirect     | string  | 用户认证成功后跳转的地址                          |\n\n## 常见问题\n\n### 1、Failed to serialize user into session\n\n由于 passport 默认会尝试将 user 数据写入 session，如果无需将用户保存到 session，可以将 session 支持关闭。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  passport: {\n    session: false,\n  },\n};\n```\n\n如果明确需要保存数据到 Session，则需要重写 `PassportStrategy`的 User 的序列化方法，请不要保存特别大的数据。\n\n比如自己实现的本地策略。\n\n```typescript\n// src/strategy/local.strategy.ts\n\nimport { CustomStrategy, PassportStrategy } from '@midwayjs/passport';\nimport { Repository } from 'typeorm';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { UserEntity } from './user';\nimport * as bcrypt from 'bcrypt';\n\n@CustomStrategy()\nexport class LocalStrategy extends PassportStrategy(Strategy) {\n  // ...\n  serializeUser(user, done) {\n    // 可以只保存用户名\n    done(null, user.username);\n  }\n\n  deserializeUser(id, done) {\n    // 这里不是异步方法，你可以从其他地方根据用户名，反查用户数据。\n    const user = getUserFromDataBase(id);\n\n    done(null, user);\n  }\n}\n```\n","site/docs/extensions/piscina.md":"# 线程池\n\n线程池组件 `@midwayjs/piscina` 基于 [Piscina](https://github.com/piscinajs/piscina) 提供在 Worker 线程池中执行任务的能力，适合 CPU 密集型计算，不会阻塞主线程。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n## 安装组件\n\n```bash\n$ npm i @midwayjs/piscina@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/piscina\": \"^4.0.0\"\n  }\n}\n```\n\n## 引入组件\n\n将组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as piscina from '@midwayjs/piscina';\n\n@Configuration({\n  imports: [\n    piscina\n  ],\n  // ...\n})\nexport class MainConfiguration {}\n```\n\n## 基础用法\n\n框架基于 [文档](/docs/service_factory) 提供了 `PiscinaService` 和 `PiscinaServiceFactory`。\n\n你可以创建单个或者多个线程池对象来管理线程。\n\n以下是 [Piscina](https://github.com/piscinajs/piscina) 的基础用法。\n\n注入 `PiscinaService` 并调用 `run` 方法执行任务：\n\n```typescript\nimport { Inject, Provide } from '@midwayjs/core';\nimport * as piscina from '@midwayjs/piscina';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  piscinaService: piscina.PiscinaService;\n\n  async heavyTask() {\n    // 调用 compute 函数\n    const result1 = await this.piscinaService.run({\n      handler: 'compute',\n      payload: { value: 10 },\n    });\n    console.log(result1); // 20\n\n    // 调用 heavyComputation 函数\n    const result2 = await this.piscinaService.run({\n      handler: 'heavyComputation',\n      payload: { data: [1, 2, 3, 4, 5] },\n    });\n    console.log(result2); // 15\n  }\n}\n```\n\n## 使用 Midway 容器\n\n如果需要在 Worker 中使用 Midway 的依赖注入功能，我们需要在线程中单独在启动一个 Midway 环境。\n\n\n### 1. 创建线程中的环境\n\n你可以在主项目中单独创建一个目录用来保存线程代码，**必须** 使用 `defineConfiguration` 方法来创建入口。\n\n目录结构如下：\n```\n➜  base-app git:(feat/support_background_task) ✗ tree\n.\n├── package.json\n└── src\n    ├── configuration.ts      ## 主项目入口\n    └── worker                ## worker 目录\n        ├── index.ts\n        └── task.ts\n```\n\n我们可以在 Worker 目录中创建一个新 Midway 入口配置：\n\n```typescript\n// src/worker/index.ts\nimport { defineConfiguration } from '@midwayjs/core/functional';\nimport { CommonJSFileDetector } from '@midwayjs/core';\nimport * as piscina from '@midwayjs/piscina';\n\nexport default defineConfiguration({\n  namespace: 'worker',\n  detector: new CommonJSFileDetector(),\n  imports: [piscina], // 导入 Piscina\n});\n```\n\n### 2. 编写任务类\n\n和 Midway 其他组件类似，使用 `@PiscinaTask` 装饰器定义任务：\n\n```typescript\n// src/worker/task.ts\nimport { PiscinaTask, IPiscinaTask } from '@midwayjs/piscina';\n\n@PiscinaTask('calculate')\nexport class CalculateTask implements IPiscinaTask {\n  async execute(payload: { a: number; b: number; operation: string }) {\n    if (payload.operation === 'add') {\n      return payload.a + payload.b;\n    } else if (payload.operation === 'multiply') {\n      return payload.a * payload.b;\n    }\n    throw new Error('Unknown operation');\n  }\n}\n\n@PiscinaTask('square')\nexport class SquareTask implements IPiscinaTask {\n  async execute(payload: { value: number }) {\n    return payload.value * payload.value;\n  }\n}\n```\n\n`@PiscinaTask` 装饰器的参数为一个字符串，代表堆外暴露的 handler 名称。\n\n\n### 3. 配置主应用\n\n在主应用配置中指定 Worker 目录：\n\n```typescript\n// src/config/config.default.ts\nimport { join } from 'path';\n\nexport default {\n  piscina: {\n    client: {\n      // 指定 worker 入口文件\n      workerFile: join(__dirname, '../worker/index'),\n    },\n  },\n};\n```\n\n主应用需要忽略 Worker 目录，避免冲突：\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport { CommonJSFileDetector } from '@midwayjs/core';\nimport * as piscina from '@midwayjs/piscina';\n\n@Configuration({\n  imports: [piscina],\n  detector: new CommonJSFileDetector({\n    ignore: ['**/worker/**'], // 忽略 worker 目录\n  }),\n})\nexport class MainConfiguration {}\n```\n\n### 4. 执行容器任务\n\n使用 `runInContainer` 方法执行 Worker 容器中的任务：\n\n```typescript\n@Provide()\nexport class UserService {\n  @Inject()\n  piscinaService: piscina.PiscinaService;\n\n  async heavyTask() {\n    // 执行 calculate 任务 - 乘法\n    const result1 = await this.piscinaService.runInContainer('calculate', {\n      a: 5,\n      b: 6,\n      operation: 'multiply',\n    });\n    console.log(result1); // 30\n\n    // 执行 calculate 任务 - 加法\n    const result2 = await this.piscinaService.runInContainer('calculate', {\n      a: 10,\n      b: 20,\n      operation: 'add',\n    });\n    console.log(result2); // 30\n\n    // 执行 square 任务\n    const result3 = await this.piscinaService.runInContainer('square', {\n      value: 7,\n    });\n    console.log(result3); // 49\n  }\n}\n```\n\n## 取消任务\n\n使用 `AbortController` 可以取消正在运行的任务：\n\n```typescript\n@Provide()\nexport class UserService {\n  @Inject()\n  piscinaService: piscina.PiscinaService;\n\n  async cancelableTask() {\n    const abortController = new AbortController();\n\n    // 3 秒后取消任务\n    setTimeout(() => {\n      abortController.abort();\n    }, 3000);\n\n    try {\n      const result = await this.piscinaService.run(\n        {\n          handler: 'longRunning',\n          payload: { duration: 10000 }, // 10 秒的任务\n        },\n        {\n          signal: abortController.signal, // 传递 AbortSignal\n        }\n      );\n    } catch (error) {\n      console.error('任务被取消:', error);\n    }\n  }\n}\n```\n\n## 多个 Worker Pool\n\n可以配置多个 Worker Pool，每个 Pool 执行不同的任务：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  piscina: {\n    clients: {\n      // 计算任务池\n      compute: {\n        workerFile: join(__dirname, '../worker/compute.worker'),\n        maxThreads: 4,\n      },\n      // 图像处理任务池\n      image: {\n        workerFile: join(__dirname, '../worker/image.worker'),\n        maxThreads: 2,\n      },\n    },\n  },\n};\n```\n\n使用不同的 Pool：\n\n```typescript\n@Provide()\nexport class UserService {\n  @Inject()\n  piscinaServiceFactory: piscina.PiscinaServiceFactory;\n\n  async useDifferentPools() {\n    // 使用计算池\n    const computePool = this.piscinaServiceFactory.get('compute');\n    const result1 = await computePool.run({\n      handler: 'compute',\n      payload: { value: 10 },\n    });\n\n    // 使用图像处理池\n    const imagePool = this.piscinaServiceFactory.get('image');\n    const result2 = await imagePool.run({\n      handler: 'process',\n      payload: { imagePath: '/path/to/image.jpg' },\n    });\n  }\n}\n```\n\n## 配置选项\n\n### 常用配置\n\n[Piscina](https://github.com/piscinajs/piscina) 有非常丰富的线程配置。\n\n```typescript\nexport default {\n  piscina: {\n    client: {\n      workerFile: join(__dirname, '../worker/index'),\n      minThreads: 1,           // 最小线程数\n      maxThreads: 4,           // 最大线程数\n      idleTimeout: 60000,      // 空闲超时（毫秒）\n      maxQueue: 'auto',        // 最大队列长度\n      concurrentTasksPerWorker: 1, // 每个 Worker 的并发任务数\n    },\n  },\n};\n```\n\n### 多 Pool 配置\n\n```typescript\nexport default {\n  piscina: {\n    clients: {\n      default: {\n        workerFile: join(__dirname, '../worker/default.worker'),\n      },\n      heavy: {\n        workerFile: join(__dirname, '../worker/heavy.worker'),\n        maxThreads: 8,\n        idleTimeout: 30000,\n      },\n    },\n  },\n};\n```\n\n## Worker 文件路径说明\n\n- 支持 `.ts` 和 `.js` 文件，框架会自动查找\n- 建议使用不带扩展名的路径，框架按 `.js -> .ts -> .mjs -> .cjs` 顺序查找\n- 生产环境编译后会自动找到对应的 `.js` 文件\n\n```typescript\n// 推荐：不带扩展名\nworkerFile: join(__dirname, '../worker/compute.worker')\n\n// 也可以：显式指定扩展名\nworkerFile: join(__dirname, '../worker/compute.worker.js')\nworkerFile: join(__dirname, '../worker/compute.worker.ts')\n```\n\n## API 参考\n\n### PiscinaService\n\n#### run(task, options?)\n\n执行普通 Worker 任务。\n\n```typescript\nawait piscinaService.run(\n  {\n    handler: 'functionName',  // Worker 文件中导出的函数名\n    payload: { /* 数据 */ },  // 传递给函数的参数\n  },\n  {\n    signal: abortController.signal, // 可选：AbortSignal\n    transferList: [],                // 可选：可转移对象列表\n  }\n);\n```\n\n#### runInContainer(handler, payload?, options?)\n\n执行 Worker 容器中的 `@PiscinaTask` 任务。\n\n```typescript\nawait piscinaService.runInContainer(\n  'taskName',              // @PiscinaTask 装饰器的参数\n  { /* 数据 */ },          // 传递给 execute 方法的参数\n  {\n    signal: abortController.signal, // 可选：AbortSignal\n  }\n);\n```\n\n## 最佳实践\n\n### 适用场景\n\n- CPU 密集型计算（数据处理、加密解密、图像处理）\n- 耗时较长的同步操作\n- 需要避免阻塞主线程的场景\n- 需要并行处理大量任务的场景\n\n### 选择合适的模式\n\n**普通 Worker 模式**：\n- 适合简单的纯函数计算\n- 不需要依赖注入\n- 性能开销更小\n\n**Midway 容器模式**：\n- 需要使用依赖注入\n- 需要在 Worker 中使用其他服务\n- 适合复杂的业务逻辑\n\n### 注意事项\n\n1. **数据传递**：传递给 Worker 的数据会被序列化，不支持函数、类实例等不可序列化对象\n2. **线程数配置**：根据 CPU 核心数合理配置 `maxThreads`，避免过多线程导致上下文切换开销\n3. **内存管理**：Worker 线程有独立的内存空间，注意避免内存泄漏\n4. **错误处理**：Worker 中的错误会被捕获并传递回主线程，需要适当处理\n5. **路径问题**：Worker 文件路径建议使用绝对路径（如 `join(__dirname, '../worker/xxx')`）\n\n## 常见问题\n\n### 如何传递大量数据？\n\n对于大型数据（如 ArrayBuffer、Buffer），使用 `transferList` 避免数据复制：\n\n```typescript\nconst buffer = new ArrayBuffer(1024 * 1024);\nawait piscinaService.run(\n  { handler: 'process', payload: buffer },\n  { transferList: [buffer] }\n);\n```\n","site/docs/extensions/pm2.md":"# pm2\n\n[PM2](https://github.com/Unitech/pm2) 是带有内置负载平衡器的 Node.js 应用程序的生产过程管理器。可以利用它来简化很多 Node 应用管理的繁琐任务，如性能监控、自动重启、负载均衡等。\n\n## 安装\n\n我们一般会把 pm2 安装到全局。\n\n```bash\n$ npm install pm2 -g    # 命令行安装 pm2\n```\n\n## 常用命令\n\n```bash\n$ pm2 start   \t# 启动一个服务\n$ pm2 list    \t# 列出当前的服务\n$ pm2 stop\t\t\t# 停止某个服务\n$ pm2 restart\t\t# 重启某个服务\n$ pm2 delete\t\t# 删除某个服务\n$ pm2 logs\t\t\t# 查看服务的输出日志\n```\n\n比如， `pm2 list`，就会以表格显示。\n\n![](https://cdn.nlark.com/yuque/0/2021/png/501408/1616560437389-b193a0d0-b463-49f1-a347-8dec20e7504d.png)\n\npm2 的服务都有一个数组 id，你可以用 id 快速操作它。\n\n比如：\n\n```bash\n$ pm2 stop 1\t\t\t# 停止编号为 1 的服务\n$ pm2 delete 1\t\t# 删除编号为 1 的服务\n```\n\n使用 `--name` 参数添加一个应用名。\n\n```bash\n$ pm2 start ./bootstrap.js --name test_app\n```\n\n然后你可以用这个应用名来操作启停。\n\n```bash\n$ pm2 stop test_app\n$ pm2 restart test_app\n```\n\n## 启动应用\n\nMidway 应用一般使用 `npm run start` 做线上部署。其对应的命令为 `NODE_ENV=production node bootstrap.js`。\n\n:::info\n部署前需要执行编译 npm run build\n:::\n\n对应的 pm2 命令为\n\n```bash\n$ NODE_ENV=production pm2 start ./bootstrap.js --name midway_app -i 4\n```\n\n- --name 用于指定应用名\n- -i 用于指定启动的实例数（进程），会使用 cluster 模式启动\n\n效果如下：\n\n![](https://cdn.nlark.com/yuque/0/2021/png/501408/1616562075255-088155ee-7c4f-4eae-b5c5-db826f78b519.png)\n\n## Docker 容器启动\n\n在 Docker 容器中，后台启动的代码都会被退出，达不到预期效果。pm2 使用另一个命令来支持容器启动。\n\n请将命令修改为 pm2-runtime start 。\n\n```bash\n$ NODE_ENV=production pm2-runtime start ./bootstrap.js --name midway_app -i 4\n```\n\n具体的 pm2 行为请参考 [pm2 容器部署说明](https://www.npmjs.com/package/pm2#container-support)。\n","site/docs/extensions/process_agent.md":"# 进程 Agent\n\nmidway 封装了 `@midwayjs/process-agent` 用来解决 node 场景中，多进程部分场景数据进程间数据不一致，或者无法指定 master 进程执行某个方法。\n\n\n举例：\n\n- 如果使用 pm2、cluster、多进程进行部署方式，使用内存的 cache，那这个 cache 在自己的进程内。\n- prometheus，获取 `/metrics` 的时候，需要把所有进程的数据收集上来，而不是某个进程的\n- 健康检查，如果有4个进程，如果有一个进程不正常了，健康检查应该检查失败。\n\n\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 安装方法\n\n使用方法：\n\n```bash\n$ npm install @midwayjs/process-agent@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/process-agent\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n## 引入组件\n\n`configuration.ts` 使用方法：\n\n```typescript\nimport * as processAgent from '@midwayjs/process-agent';\n\n@Configuration({\n  imports: [\n    // ...\n    processAgent\n  ],\n})\nexport class MainConfiguration {\n}\n\n```\n## 使用方法\n\n业务代码 UserService：\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { TestService } from './test';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  testService: TestService;\n\n  async getUser() {\n    let result = await this.testService.setData(1);\n    return result;\n  }\n}\n\n```\n然后调用 testService 的时候，希望只在主进程执行：\n\n```typescript\nimport { Provide, Scope, ScopeEnum } from '@midwayjs/core';\nimport { RunInPrimary } from '@midwayjs/process-agent';\n\n@Provide()\n@Scope(ScopeEnum.Singleton)\nexport class TestService {\n\n  data: any = 0;\n\n  @RunInPrimary()\n  async setData(b){\n    this.data = b;\n    return this.data;\n  }\n\n  @RunInPrimary()\n  async getData(){\n    return this.data;\n  }\n}\n\n```\n注意，执行返回的数据只限于可序列化的数据，比如普通 JSON，不支持包含方法等无法序列化的数据。\n\n\n## 效果描述\n假设采用pm2 或者 egg-script 等多进程方式启动，假设这是个请求\n\n首先：\n\n- 1、设置 setData\n- 2、然后获取 getData\n\n\n如果没有 RunInPrimary 这个装饰器，那请求可能落在进程2，或者进程3，那可能没有获取更新的data。\n\n所以 RunInPrimary 能确保这个函数执行能落到主进程去。\n\n\n## 功能征集\n如果有其他类似相关功能，觉得可以放在这个包里面的，欢迎在评论区，或者 [issue](https://github.com/midwayjs/midway/issues) 里面帮忙提一下，我们会跟大家一起讨论和实现。\n\n","site/docs/extensions/prometheus.md":"# Prometheus\n\nPrometheus（普罗米修斯）是一个最初在 SoundCloud 上构建的监控系统。 自 2012 年成为社区开源项目，拥有非常活跃的开发人员和用户社区。为强调开源及独立维护，Prometheus 于 2016 年加入云原生云计算基金会（CNCF），成为继 Kubernetes 之后的第二个托管项目。\n\nGrafana 是一个开源的度量分析与可视化套件。纯 Javascript 开发的前端工具，通过访问库（如 InfluxDB），展示自定义报表、显示图表等。Grafana 支持许多不同的数据源。每个数据源都有一个特定的查询编辑器,该编辑器定制的特性和功能是公开的特定数据来源。而 Prometheus 正好是其支持的数据源之一。\n\n本篇介绍了 Midway 如何接入 Grafana + Prometheus。\n\n接入效果如下：\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1617259935548-a2df4339-3229-4391-bd3d-4ba8e6979d4d.png)\n\n## 安装依赖\n\n首先安装 Midway 提供的指标监控组件：\n\n```bash\n$ npm install @midwayjs/prometheus@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/prometheus\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n## 引入组件\n\n在 `configuration.ts` 中，引入这个组件：\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as prometheus from '@midwayjs/prometheus'; // 导入模块\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    prometheus\n  ],\n  importConfigs: [join(__dirname, 'config')],\n})\nexport class MainConfiguration {}\n```\n\n启动我们的应用，此时访问的时候多了一个 `${host}:${port}/metrics` 。\n\n:::info\nPrometheus 基于 HTTP 获取监控数据，请包含 web/koa/express 其中任一组件。\n:::\n\n访问接口，返回如下，里面的内容是当前的指标。\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1617260048533-4f725824-9471-40c9-be8b-6dcbf27d9cca.png)\n\n## 其他配置\n\n指标组件也提供了相关的配置，方便开发者进行配置。\n\n可以在 `config.default.ts` 中，修改 prometheus 的配置。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  prometheus: {\n    labels: {\n      APP_NAME: 'demo_project',\n    },\n  },\n}\n```\n\n更多的配置，我们可以查看定义进行配置。\n\n通过配置，我们例如可以归类哪些 node 是同一个应用，因为我们部署的时候，node 程序是分布式的。例如上面我们加了 APP_NAME，用来区分不同的应用，这样在监控指标中，我们可以区分不同的应用。\n\n## 数据采集\n\n我们前面在 Midway 中引入的组件主要是在 Node 中加了指标模块。接下来我们需要让 Prometheus 来采集我们的指标数据。\n\n如果开发者所在部门已经有 Prometheus+grafana 了，则只需将应用的指标地址上报给 PE 或者通过接口上报即可。此处我们假设大家没有 Prometheus+grafana，然后按照下面描述进行操作。\n\n## 搭建 Prometheus\n\n此处我们通过 docker-compose 来搭建 Prometheus， docker-compose.yml 文件如下：\n\n```yaml\nversion: '2.2'\nservices:\n  tapi:\n    logging:\n      driver: 'json-file'\n      options:\n        max-size: '50m'\n    image: prom/prometheus\n    restart: always\n    volumes:\n      - ./prometheus_data:/prometheus_data:rw\n      - ./prometheus.yml:/etc/prometheus/prometheus.yml\n      - ./targets.json:/etc/prometheus/targets.json\n    command:\n      - '--storage.tsdb.path=/prometheus_data'\n      - '--config.file=/etc/prometheus/prometheus.yml'\n      - '--storage.tsdb.retention=10d'\n      - '--web.enable-lifecycle'\n    ports:\n      - '9090:9090'\n```\n\n`prometheus.yml` 文件如下：\n\n```yaml\nglobal:\n  scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.\n  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.\nscrape_configs:\n  - job_name: 'node'\n    file_sd_configs:\n      - refresh_interval: 1m\n        files:\n          - '/etc/prometheus/targets.json'\n  - job_name: 'prometheus'\n    static_configs:\n      - targets: ['localhost:9090']\n```\n\n然后采集的 `targets.json` 如下：下面文件里面 `${ip}` 替换为 Node.js 应用所在服务器的 ip 地址。\n\n```json\n[\n  {\n    \"targets\": [\"${ip}:7001\"],\n    \"labels\": {\n      \"env\": \"prod\",\n      \"job\": \"api\"\n    }\n  }\n]\n```\n\n\n\n然后我们启动 `docker-compose.yml` 文件，\n\n```bash\n$ docker-compose up\n```\n\n至此，Prometheus 已经会去拉取我们 Node 应用程序的指标数据了。\n\n如果想要更新 target 怎么做：\n修改了这个 targets.json 文件后，通过 prometheus 的 reload 方法进行热加载。\n方法如下：\n\n```bash\n$ curl -X POST http://${prometheus的ip}:9090/-/reload\n```\n\n然后我们可以查看 prometheus 的页面也可以确认是否生效，界面地址：\n\n```text\nhttp://${prometheus的ip}:9090/classic/targets\n```\n\n接下来就是如何展示这些采集到的数据了。\n\n\n\n## 数据展示\n\n我们可以借助 Grafana 来展示我们的数据。\n\n此处我们简单通过 Docker 来搭建一下 Grafana：\n\n```bash\n$ docker run -d --name=grafana -p 3000:3000 grafana/grafana\n```\n也可以将 grafana 和 prometheus 放在一起使用 docker-compose 统一管理。\n\n将 grafana 添加到 `docker-compose.yml`, 示例如下：\n\n```yaml\nversion: '2.2'\nservices:\n  tapi:\n    logging:\n      driver: 'json-file'\n      options:\n        max-size: '50m'\n    image: prom/prometheus\n    restart: always\n    volumes:\n      - ./prometheus_data:/prometheus_data:rw  # prometheus Data mapping directory\n      - ./prometheus.yml:/etc/prometheus/prometheus.yml # prometheus Configuration mapping file\n      - ./targets.json:/etc/prometheus/targets.json\n    command:\n      - '--storage.tsdb.path=/prometheus_data'\n      - '--config.file=/etc/prometheus/prometheus.yml'\n      - '--storage.tsdb.retention=10d'\n      - '--web.enable-lifecycle'\n    ports:\n      - '9090:9090'\n  // highlight-start\n  grafana:\n    image: grafana/grafana\n    container_name: \"grafana0\"\n    ports:\n      - \"3000:3000\"\n    restart: always\n    volumes:\n      - \"./grafana_data:/var/lib/grafana\" # grafana data mapping directory\n      - \"./grafana_log:/var/log/grafana\"  # grafana log mapping directory\n    // highlight-end\n```\n重启 `docker-compose.yml` 文件\n\n```bash\ndocker-compose restart\n```\n![](https://cdn.nlark.com/yuque/0/2022/png/525744/1667300763153-5ee476a7-00ff-4899-92ba-5985995b4862.png)\n\n完成以上任意一种, 然后我们访问 127.0.0.1:3000，默认账号密码：admin:admin。\n\n访问后效果如下：\n\n\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1617260561047-c2643a69-6258-491b-937d-9bfc4558252f.png)\n\n然后我们让 Grafana 接入我们的 Prometheus 数据源：\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1617260581029-1e2e06a8-3054-4ad8-96b5-d50ab9bb1612.png)\n\n然后我们点击 Grafana 添加图表：\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1620725466020-28793a78-c03b-48fa-bf16-0c9c8ecc1a94.png)\n\n这边 ID 选择 14403，然后点击 load，然后点击下一步，然后点击 import 后，就能看到我们刚刚接入的效果了。\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1620725497338-a32a8982-d51f-4e74-b511-dc10a7c66d80.png)\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1620725514630-4f654f10-ef3a-41f7-b403-02832d3ef7d8.png)\n\n这样开发者可以运维自己的 Node 程序了，例如，是否最近引入了一个 NPM 包导致了什么内存泄漏的情况，是否最近有应用重启的情况了。\n\n当然还能支持其他的自定义操作。\n\n\n## Socket-io 场景\n\n使用方法：\n\n```bash\n$ npm install @midwayjs/prometheus-socket-io@4 --save\n```\n\n使用方法：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport { join } from 'path';\nimport * as prometheus from '@midwayjs/prometheus';\nimport * as prometheusSocketIo from '@midwayjs/prometheus-socket-io';\n\n@Configuration({\n  imports: [prometheus, prometheusSocketIo],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {}\n```\n\n然后在/metrics 这边就能看到 socket-io 的数据了。\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1631090438583-d925c13c-371a-4037-9f53-edaa34580aab.png)\n\n一共新增 8 个指标。\n后续会提供 Grafana 的模版 ID 给大家使用。\n\n\n## 功能介绍\n\n- [x] 根据 appName 进行分类\n- [x] 查看不同 path 的 qps 情况\n- [x] 查看不同 status 的分布情况\n- [x] 查询不同 path 的 rt 情况\n- [x] 进程的 CPU 使用情况\n- [x] 进程的内存使用情况\n- [x] 堆栈情况\n- [x] Event Loop\n- [ ] 等\n","site/docs/extensions/rabbitmq.md":"# RabbitMQ\n\n在复杂系统的架构中，会有负责处理消息队列的微服务，如下图：服务A负责产生消息给消息队列，而服务B则负责消费消息队列中的任务。\n\n![image.png](https://img.alicdn.com/imgextra/i3/O1CN01SYMbCz1moVSVLl7S2_!!6000000005001-2-tps-646-251.png)\n\n在Midway中，我们提供了订阅rabbitMQ的能力，专门来满足用户的这类需求。\n\n相关信息：\n\n**订阅服务**\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ❌    |\n\n\n\n## 基础概念\n\n\nRabbitMQ 的概念较为复杂，其基于高级消息队列协议即 Advanced Message Queuing Protocol（AMQP），如果第一次接触请阅读一下相关的参考文档。\n\n\nAMQP 有一些概念，Queue、Exchange 和 Binding 构成了 AMQP 协议的核心，包括：\n\n- Producer：消息生产者，即投递消息的程序。\n- Broker：消息队列服务器实体。\n   - Exchange：消息交换机，它指定消息按什么规则，路由到哪个队列。\n   - Binding：绑定，它的作用就是把 Exchange 和 Queue 按照路由规则绑定起来。\n   - Queue：消息队列载体，每个消息都会被投入到一个或多个队列。\n- Consumer：消息消费者，即接受消息的程序。\n\n\n\n简单的理解，消息通过 Publisher 发布到 Exchange（交换机），Consumer 通过订阅 Queue 来接受消息，Exchange 和 Queue 通过路由做连接。\n\n\n![image.png](https://img.alicdn.com/imgextra/i3/O1CN01fLrucw1FVNbCx4NqG_!!6000000000492-2-tps-700-328.png)\n\n\n\n## 消费者（Consumer）使用方法\n\n\n### 安装依赖\n\n\nMidway 提供了订阅 rabbitMQ 的能力，并能够独立部署和使用。安装 `@midwayjs/rabbitmq` 模块及其定义。\n```bash\n$ npm i @midwayjs/rabbitmq@4 --save\n$ npm i amqplib --save\n$ npm i @types/amqplib --save-dev\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/rabbitmq\": \"^4.0.0\",\n    \"amqplib\": \"^0.10.1\",\n    // ...\n  },\n  \"devDependencies\": {\n    \"@types/amqplib\": \"^0.8.2\",\n    // ...\n  }\n}\n```\n\n## 开启组件\n\n`@midwayjs/rabbmitmq` 可以作为独立主框架使用。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as rabbitmq from '@midwayjs/rabbitmq';\n\n@Configuration({\n  imports: [\n    rabbitmq\n  ],\n  // ...\n})\nexport class MainConfiguration {\n  async onReady() {\n        // ...\n  }\n}\n```\n\n也可以附加在其他的主框架下，比如 `@midwayjs/koa` 。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as rabbitmq from '@midwayjs/rabbitmq';\n\n@Configuration({\n  imports: [\n    koa,\n    rabbitmq\n  ],\n  // ...\n})\nexport class MainConfiguration {\n  async onReady() {\n        // ...\n  }\n}\n```\n\n### 目录结构\n\n\n我们一般把能力分为生产者和消费者，而订阅正是消费者的能力。\n\n\n我们一般把消费者放在 consumer 目录。比如 `src/consumer/userConsumer.ts`  。\n```\n➜  my_midway_app tree\n.\n├── src\n│   ├── consumer\n│   │   └── user.consumer.ts\n│   ├── interface.ts\n│   └── service\n│       └── user.service.ts\n├── test\n├── package.json\n└── tsconfig.json\n```\n代码示例如下。\n\n```typescript\nimport { Consumer, MSListenerType, RabbitMQListener, Inject } from '@midwayjs/core';\nimport { Context } from '@midwayjs/rabbitmq';\nimport { ConsumeMessage } from 'amqplib';\n\n@Consumer(MSListenerType.RABBITMQ)\nexport class UserConsumer {\n\n  @Inject()\n  ctx: Context;\n\n  @RabbitMQListener('tasks')\n  async gotData(msg: ConsumeMessage) {\n    this.ctx.channel.ack(msg);\n  }\n\n}\n\n```\n`@Consumer` 装饰器，提供消费者标识，并且它的参数，指定了某种消费框架的类型，比如，我们这里指定了 `MSListenerType.RABBITMQ` 这个类型，指的就是 rabbitMQ 类型。\n\n\n标识了 `@Consumer` 的类，对方法使用 `@RabbitMQListener` 装饰器后，可以绑定一个 RabbitMQ 的队列（Queue)。\n\n\n方法的参数为接收到的消息，类型为 `ConsumeMessage` 。如果返回值需要确认，则需要对服务端进行 `ack` 操作，明确接收到的数据。\n\n\n如果需要订阅多个队列，可以使用多个方法，也可以使用多个文件。\n\n\n### RabbitMQ 消息上下文\n\n\n订阅 `RabbitMQ` 数据的上下文，和 Web 同样的，其中包含一个 `requestContext` ，和每次接收消息的数据绑定。\n\n\n从 ctx 上可以取到 `channel` ，整个 ctx 的定义为：\n```typescript\nexport type Context = {\n  channel: amqp.Channel;\n  requestContext: IMidwayContainer;\n};\n```\n\n\n可以从框架获取定义\n```typescript\nimport { Context } from '@midwayjs/rabbitmq';\n```\n\n\n### 配置消费者\n\n我们需要在配置中指定 rabbitmq 的地址。\n\n```typescript\n// src/config/config.default\nimport { MidwayConfig } from '@midwayjs/core';\n\nexport default {\n  // ...\n  rabbitmq: {\n    url: 'amqp://localhost'\n  }\n} as MidwayConfig;\n```\n\n更多配置：\n\n| 属性 | 描述 |\n| --- | --- |\n| url | rabbitMQ 的连接信息 |\n| socketOptions | amqplib.connect 的第二个参数 |\n| reconnectTime | 队列断连后的重试时间，默认 10 秒 |\n\n\n\n### Fanout Exchange\n\n\nFanout 是一种特定的交换机，如果满足匹配（binding），就往 Exchange 所绑定的 Queue 发送消息。Fanout Exchange 会忽略 RoutingKey 的设置，直接将 Message 广播到所有绑定的 Queue 中。\n\n即所有订阅该交换机的 Queue 都会收到消息。\n\n比如，下面我们添加了两个 Queue，订阅了相同的交换机。\n```typescript\nimport { Consumer, MSListenerType, RabbitMQListener, Inject, App } from '@midwayjs/core';\nimport { Context, Application } from '@midwayjs/rabbitmq';\nimport { ConsumeMessage } from 'amqplib';\n\n@Consumer(MSListenerType.RABBITMQ)\nexport class UserConsumer {\n\n  @App()\n  app: Application;\n\n  @Inject()\n  ctx: Context;\n\n  @Inject()\n  logger;\n\n  @RabbitMQListener('abc', {\n    exchange: 'logs',\n    exchangeOptions: {\n      type: 'fanout',\n      durable: false,\n    },\n    exclusive: true,\n    consumeOptions: {\n      noAck: true,\n    }\n  })\n  async gotData(msg: ConsumeMessage) {\n    this.logger.info('test output1 =>', msg.content.toString('utf8'));\n    // TODO\n  }\n\n  @RabbitMQListener('bcd', {\n    exchange: 'logs',\n    exchangeOptions: {\n      type: 'fanout',\n      durable: false,\n    },\n    exclusive: true,\n    consumeOptions: {\n      noAck: true,\n    }\n  })\n  async gotData2(msg: ConsumeMessage) {\n    this.logger.info('test output2 =>', msg.content.toString('utf8'));\n    // TODO\n  }\n\n}\n\n```\n\n\n订阅的 abc 和 bcd 队列，绑定了相同的交换机 logs，最终的结果是，两个方法都会被调用。\n\n\n### Direct Exchange\n\n\nDirect Exchange 是 RabbitMQ 默认的 Exchange，完全根据 RoutingKey 来路由消息。设置 Exchange 和 Queue 的 Binding 时需指定 RoutingKey（一般为 Queue Name），发消息时也指定一样的 RoutingKey，消息就会被路由到对应的Queue。\n\n\n下面的示例代码，我们不填写 Queue Name，只添加一个 routingKey，交换机类型为 direct。\n```typescript\nimport { Consumer, MSListenerType, RabbitMQListener, Inject, App } from '@midwayjs/core';\nimport { Context, Application } from '../../../../../src';\nimport { ConsumeMessage } from 'amqplib';\n\n@Consumer(MSListenerType.RABBITMQ)\nexport class UserConsumer {\n\n  @App()\n  app: Application;\n\n  @Inject()\n  ctx: Context;\n\n  @Inject()\n  logger;\n\n  @RabbitMQListener('', {\n    exchange: 'direct_logs',\n    exchangeOptions: {\n      type: 'direct',\n      durable: false,\n    },\n    routingKey: 'direct_key',\n    exclusive: true,\n    consumeOptions: {\n      noAck: true,\n    }\n  })\n  async gotData(msg: ConsumeMessage) {\n    // TODO\n  }\n}\n\n```\n\n\ndirect 类型的消息，会根据 routerKey 做定向过滤，所以只有特定订阅能收到消息。\n\n\n\n\n### 装饰器参数\n\n\n`@RabbitMQListener` 装饰器的第一个参数为 queueName，代表需要监听的队列。\n\n\n第二个参数是一个对象，包含队列，交换机等参数，详细定义如下：\n```typescript\nexport interface RabbitMQListenerOptions {\n  exchange?: string;\n  /**\n   * queue options\n   */\n  exclusive?: boolean;\n  durable?: boolean;\n  autoDelete?: boolean;\n  messageTtl?: number;\n  expires?: number;\n  deadLetterExchange?: string;\n  deadLetterRoutingKey?: string;\n  maxLength?: number;\n  maxPriority?: number;\n  pattern?: string;\n  /**\n   * prefetch\n   */\n  prefetch?: number;\n  /**\n   * router\n   */\n  routingKey?: string;\n  /**\n   * exchange options\n   */\n  exchangeOptions?: {\n    type?: 'direct' | 'topic' | 'headers' | 'fanout' | 'match' | string;\n    durable?: boolean;\n    internal?: boolean;\n    autoDelete?: boolean;\n    alternateExchange?: string;\n    arguments?: any;\n  };\n  /**\n   * consumeOptions\n   */\n  consumeOptions?: {\n    consumerTag?: string;\n    noLocal?: boolean;\n    noAck?: boolean;\n    exclusive?: boolean;\n    priority?: number;\n    arguments?: any;\n  };\n}\n```\n\n\n\n\n### 本地测试\n\n\nMidway 提供了一个简单的测试方法用于测试订阅某个数据。 `@midwayjs/mock` 工具提供了一个 `createRabbitMQProducer` 的方法，用于创建一个生产者，通过它，你可以创建一个队列（Queue），以及向这个队列发消息。\n\n\n然后，我们启动一个 app，就可以自动监听到这个队列中的数据，并执行后续逻辑。\n\n```typescript\nimport { createRabbitMQProducer, close, creatApp } from '@midwayjs/mock';\n\ndescribe('/test/index.test.ts', () => {\n  it('should test create message and get from app', async () => {\n    // create a queue and channel\n    const channel = await createRabbitMQProducer('tasks', {\n      isConfirmChannel: true,\n      mock: false,\n      url: 'amqp://localhost',\n    });\n\n    // send data to queue\n    channel.sendToQueue('tasks', Buffer.from('something to do'))\n\n    // create app and got data\n    const app = await creatApp();\n\n    // wait a moment\n\n    await close(app);\n  });\n});\n\n```\n\n\n**示例一**\n\n\n创建一个 fanout exchange。\n```typescript\nconst manager = await createRabbitMQProducer('tasks-fanout', {\n  isConfirmChannel: false,\n  mock: false,\n  url: 'amqp://localhost',\n});\n\n// Name of the exchange\nconst ex = 'logs';\n// Write a message\nconst msg = \"Hello World!\";\n\n// 声明交换机\nmanager.assertExchange(ex, 'fanout', { durable: false }) // 'fanout' will broadcast all messages to all the queues it knows\n\n// 启动服务\nconst app = await creatApp('base-app-fanout', {\n  url: 'amqp://localhost',\n  reconnectTime: 2000\n});\n\n// 发送到交换机，由于不持久化，需要等订阅服务起来之后再发\nmanager.sendToExchange(ex, '', Buffer.from(msg))\n\n// 等一段时间\nawait sleep(5000);\n\n// 校验结果\n\n// 关闭 producer\nawait manager.close();\n\n// 关闭 app\nawait close(app);\n```\n\n\n**示例二**\n\n\n创建一个 direct exchange。\n```typescript\n/**\n  * direct 类型的消息，根据 routerKey 做定向过滤\n  */\nconst manager = await createRabbitMQProducer('tasks-direct', {\n  isConfirmChannel: false,\n  mock: false,\n  url: 'amqp://localhost',\n});\n\n// Name of the exchange\nconst ex = 'direct_logs';\n// Write a message\nconst msg = \"Hello World!\";\n\n// 声明交换机\nmanager.assertExchange(ex, 'direct', { durable: false }) // 'fanout' will broadcast all messages to all the queues it knows\n\nconst app = await creatApp('base-app-direct', {\n  url:  'amqp://localhost',\n  reconnectTime: 2000\n});\n\n// 这里指定 routerKey，发送到交换机\nmanager.sendToExchange(ex, 'direct_key', Buffer.from(msg))\n\n// 校验结果\n\nawait manager.close();\nawait close(app);\n```\n\n\n## 生产者（Producer）使用方法\n\n\n生产者（Producer）也就是第一节中的消息产生者，简单的来说就是会创建一个客户端，将消息发送到 RabbitMQ 服务。\n\n\n注意：当前 Midway 并没有使用组件来支持消息发送，这里展示的示例只是使用纯 SDK 在 Midway 中的写法。\n\n\n### 安装依赖\n\n\n```bash\n$ npm i amqplib amqp-connection-manager --save\n$ npm i @types/amqplib --save-dev\n```\n\n\n### 调用服务发送消息\n\n\n比如，我们在 service 文件下，新增一个 `rabbitmq.ts` 文件。\n```typescript\nimport { Provide, Scope, ScopeEnum, Init, Autoload, Destroy } from '@midwayjs/core';\nimport * as amqp from 'amqp-connection-manager'\n\n@Autoload()\n@Provide()\n@Scope(ScopeEnum.Singleton) // Singleton 单例，全局唯一（进程级别）\nexport class RabbitmqService {\n\n  private connection: amqp.AmqpConnectionManager;\n\n  private channelWrapper;\n\n  @Init()\n  async connect() {\n    // 创建连接，你可以把配置放在 Config 中，然后注入进来\n    this.connection = await amqp.connect('amqp://localhost');\n\n    // 创建 channel\n    this.channelWrapper = this.connection.createChannel({\n      json: true,\n      setup: function(channel) {\n        return Promise.all([\n          // 绑定队列\n        \tchannel.assertQueue(\"tasks\", { durable: true }),\n        ]);\n      }\n    });\n  }\n\n  // 发送消息\n  public async sendToQueue(queueName: string, data: any) {\n  \treturn this.channelWrapper.sendToQueue(queueName, data);\n  }\n\n  @Destroy()\n  async close() {\n  \tawait this.channelWrapper.close();\n    await this.connection.close();\n  }\n}\n\n```\n大概就是创建了一个用来封装消息通信的 service，同时他是全局唯一的 Singleton 单例。由于增加了 `@AutoLoad` 装饰器，可以自执行初始化。\n\n\n这样基础的调用服务就抽象好了，我们只需要在用到的地方，调用 `sendToQueue` 方法即可。\n\n\n比如：\n\n\n```typescript\n@Provide()\nexport class UserService {\n\n  @Inject()\n  rabbitmqService: RabbitmqService;\n\n\tasync invoke() {\n    // TODO\n\n    // 发送消息\n  \tawait this.rabbitmqService.sendToQueue('tasks', {hello: 'world'});\n  }\n}\n```\n\n\n## 参考文档\n\n\n- [理解 RabbitMQ Exchange](https://zhuanlan.zhihu.com/p/37198933)\n- [RabbitMQ for Node.js in 30 steps](https://github.com/Gurenax/node-rabbitmq)\n","site/docs/extensions/redis.md":"# Redis\n\n这里介绍如何快速在 Midway 中使用 Redis。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 安装依赖\n\n`@midwayjs/redis` 是主要的功能包。\n\n```bash\n$ npm i @midwayjs/redis@4 --save\n```\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/redis\": \"^4.0.0\",\n    // ...\n  }\n}\n```\n\n\n## 引入组件\n\n\n首先，引入组件，在 `src/configuration.ts` 中导入：\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as redis from '@midwayjs/redis';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    redis\t\t// 导入 redis 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ],\n})\nexport class MainConfiguration {\n}\n```\n\n\n## 配置 Redis\n\n\n**单客户端配置**\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  redis: {\n    client: {\n      port: 6379, // Redis port\n      host: \"127.0.0.1\", // Redis host\n      password: \"auth\",\n      db: 0,\n    },\n  },\n}\n```\n**Sentinel 配置**\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  redis: {\n    client: {\n      sentinels: [{          // Sentinel instances\n        port: 26379,         // Sentinel port\n        host: '127.0.0.1',   // Sentinel host\n      }],\n      name: 'mymaster',      // Master name\n      password: 'auth',\n      db: 0\n    },\n  },\n}\n```\n\n\n**Cluster 模式配置，需要配置多个**\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  redis: {\n    // Cluster Redis\n    client: {\n      cluster: true,\n      nodes: [{\n        host: 'host',\n        port: 'port',\n      },{\n        host: 'host',\n        port: 'port',\n      }],\n      redisOptions: {\n        family: '',\n        password: 'xxxx',\n        db: 'xxx'\n      }\n    }\n  },\n}\n```\n\n**多个客户端配置，需要配置多个**\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  redis: {\n\t\t// Multi Redis\n    clients: {\n      instance1: {\n        host: 'host',\n        port: 'port',\n        password: 'password',\n        db: 'db',\n      },\n      instance2: {\n        host: 'host',\n        port: 'port',\n        password: 'password',\n        db: 'db',\n      },\n    },\n  },\n}\n```\n更多参数可以查看 [ioredis 文档](https://github.com/luin/ioredis/blob/master/API.md#new_Redis_new)。\n\n\n## 使用 Redis 服务\n\n\n我们可以在任意的代码中注入使用。\n```typescript\nimport { Provide, Controller, Inject, Get } from '@midwayjs/core';\nimport { RedisService } from '@midwayjs/redis';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  redisService: RedisService;\n\n  async invoke() {\n\n    // 简单设置\n    await this.redisService.set('foo', 'bar');\n\n    // 设置过期时间，单位秒\n    await this.redisService.set('foo', 'bar', 'EX', 10);\n\n    // 获取数据\n    const result = await this.redisService.get('foo');\n\n   // result => bar\n  }\n}\n```\n\n\n可以使用 `RedisServiceFactory` 获取不同的实例。\n```typescript\nimport { RedisServiceFactory } from '@midwayjs/redis';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  redisServiceFactory: RedisServiceFactory;\n\n  async save() {\n    const redis1 = this.redisServiceFactory.get('instance1');\n    const redis2 = this.redisServiceFactory.get('instance3');\n\n    //...\n\n  }\n}\n```\n\n也可以通过装饰器获取。\n\n```typescript\nimport { RedisServiceFactory, RedisService } from '@midwayjs/redis';\nimport { InjectClient } from '@midwayjs/core';\n\n@Provide()\nexport class UserService {\n\n  @InjectClient(RedisServiceFactory, 'instance1')\n  redis1: RedisService;\n\n  @InjectClient(RedisServiceFactory, 'instance3')\n  redis2: RedisService;\n\n  async save() {\n    //...\n  }\n}\n```\n\n\n## 服务发现\n\n提供基于统一抽象的服务发现能力，虽然 Redis 一般不作为服务发现的注册中心，但是其拥有 pub/sub 能力可以变相提供功能，我们仍实现了 Redis 将作为服务发现的客户端能力。\n\n服务发现包括 **注册服务** 和 **获取服务** 两部分。\n\n\n### 配置服务发现\n\n你需要先配置一个客户端；若存在多个客户端，可通过 `serviceDiscoveryClient` 指定用于服务发现的客户端。\n\n通过 `serviceDiscovery` 配置服务发现选项：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  redis: {\n    clients: {\n      default: {\n        host: '127.0.0.1',\n        port: 6379\n      }\n    },\n    serviceDiscovery: {\n      prefix: 'services:',\n      ttl: 30,\n      scanCount: 100\n    }\n  }\n}\n```\n\n### 注册服务发现\n\n服务启动后，将当前服务注册到 Redis：\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport { RedisServiceDiscovery } from '@midwayjs/redis';\n\n@Configuration({})\nexport class MainConfiguration {\n  @Inject()\n  redisDiscovery: RedisServiceDiscovery;\n\n  async onServerReady() {\n    const client = this.redisDiscovery.createClient();\n\n    await client.register({\n      serviceName: 'order',\n      id: client.defaultMeta.id,\n      host: client.defaultMeta.host,\n      port: 7001,\n      ttl: 30,\n      meta: { version: '1.0.0' }\n    });\n    // 注册默认上线，无需显式 online\n  }\n}\n```\n\n说明：上线时写入实例键并设置 TTL，客户端会定时刷新 TTL 保持存活；下线删除实例键并清理刷新；注销发布变更消息以便 Listener 全量刷新。\n\n\n### 获取可用服务\n\n在任意位置获取可用实例或按负载均衡选择一个实例：\n\n````typescript\n@Provide()\nexport class OrderService {\n  @Inject()\n  redisDiscovery: RedisServiceDiscovery;\n\n  async getService() {\n    const instances = await this.redisDiscovery.getInstances('order');\n    const one = await this.redisDiscovery.getInstance('order');\n    return { instances, one };\n  }\n}\n````\n\n返回的实例为注册时写入的对象，通常包含 `serviceName/id/host/port/ttl/meta/tags/status` 等字段。\n\n","site/docs/extensions/render.md":"# 模板渲染\n\n本组件用于在 midway 体系使用服务端渲染 ejs，nunjucks 模板。\n\n相关信息：\n\n| web 支持情况      |      |\n| ----------------- | ---- |\n| @midwayjs/koa     | ✅    |\n| @midwayjs/faas    | ✅    |\n| @midwayjs/web     | ✅    |\n| @midwayjs/express | ❌    |\n\n\n\n\n## 使用 ejs\n\n\n### 安装依赖\n\n\n选择对应的模板安装依赖。\n```bash\n$ npm i @midwayjs/view-ejs@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/view-ejs\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n### 引入组件\n\n\n首先，引入组件，在 `configuration.ts` 中导入：\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as view from '@midwayjs/view-ejs';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    view\t\t// 导入 ejs 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n### 配置\n\n配置后缀，映射到指定的引擎。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  view: {\n    mapping: {\n      '.ejs': 'ejs',\n    },\n  },\n  // ejs config\n  ejs: {}\n}\n```\n### 使用\n\n\n注意，默认的 view 目录为 `${appDir}/view` ，在其中创建一个 `hello.ejs` 文件。\n\n\n目录结构如下：\n```\n➜  my_midway_app tree\n.\n├── src\n│   └── controller                 ## Controller 目录\n│       └── home.ts\n├── view                           ## 模板目录\n│   └── hello.ejs\n├── test\n├── package.json\n└── tsconfig.json\n```\n\n\n我们在模板里写一些 ejs 格式的内容，比如：\n```typescript\n// view/hello.ejs\nhello <%= data %>\n```\n\n\n在 Controller 中渲染。\n```typescript\nimport { Inject, Provide } from '@midwayjs/core';\nimport { Context } from '@midwayjs/koa';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  ctx: Context;\n\n  @Get('/')\n  async render(){\n    await this.ctx.render('hello.ejs', {\n      data: 'world',\n    });\n  }\n}\n```\n\n### 配置后缀\n\n默认后缀为 `.html` ，为了改成习惯的 `.ejs` 后缀，我们可以加一个 `defaultExtension` 配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  view: {\n    defaultExtension: '.ejs',\n    mapping: {\n      '.ejs': 'ejs',\n    },\n  },\n  // ejs config\n  ejs: {}\n}\n```\n\n这样我们在渲染时不需要增加后缀。\n\n```typescript\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  ctx: Context;\n\n  @Get('/')\n  async render(){\n    await this.ctx.render('hello', {\n      data: 'world',\n    });\n  }\n}\n```\n\n\n### 默认渲染引擎\n\n我们可以通过 `defaultViewEngine` 来设置默认的渲染引擎。\n\n其作用是，当遇到的模板后缀，比如 `.html` 未在配置的 `mapping` 字段中找到时，使用该 `defaultViewEngine` 字段指定的引擎来渲染。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  view: {\n    defaultViewEngine: 'ejs',\n    mapping: {\n      '.ejs': 'ejs',\n    },\n  },\n  // ejs config\n  ejs: {}\n}\n```\n\n这样，如果模板是 `.html` 后缀，由于 `mapping` 中未指定，依旧会使用 `ejs` 来渲染。\n\n### 配置多个模板目录\n\n如果我们需要将代码封装为组件提供，就需要支持不同的模板目录。\n\n默认的模板目录在 `${appDir}/view`。我们可以在 `rootDir` 字段增加其他的目录。\n\n```typescript\n// src/config/config.default.ts\n\n// 修改默认 view 组件的 default 目录\nexport default {\n  // ...\n  view: {\n    rootDir: {\n      default: path.join(__dirname, './view'),\n    }\n  },\n}\n\n// 其他组件需要增加目录的配置\nexport default {\n  // ...\n  // view 组件的配置\n  view: {\n    rootDir: {\n      anotherRoot: path.join(__dirname, './view'),\n    }\n  },\n}\n```\n\n通过对象合并的机制，使得所有的 `rootDir` 都能合并到一起，组件内部会获取 values 做匹配。\n\n\n\n## 使用 Nunjucks\n\n\n和 ejs 类似，引入对应组件即可。\n\n\n1、选择对应的模板安装依赖。\n```bash\n$ npm i @midwayjs/view-nunjucks@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/view-nunjucks\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n2、引入组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as view from '@midwayjs/view-nunjucks';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    view\t\t// 导入 nunjucks 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n\n3、增加 nunjucks 的配置，比如默认使用 nunjucks。\n```typescript\nexport default {\n  // ...\n  view: {\n    defaultViewEngine: 'nunjucks',\n    mapping: {\n      '.nj': 'nunjucks',\n    },\n  },\n}\n```\n\n\n4、在 view 目录增加模板\n```typescript\n// view/test.nj\nhi, {{ user }}\n```\n\n\n在 Controller 中渲染。\n```typescript\nimport { Inject, Provide } from '@midwayjs/core';\nimport { Context } from '@midwayjs/koa';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  ctx: Context;\n\n  @Get('/')\n  async render(){\n    await ctx.render('test.nj', { user: 'midway' });\n  }\n}\n```\n访问后会输出 `hi, midway` 。\n\n\n如果有自定义 filter 的需求，可以在入口处增加，比如下面增加了一个名为 `hello` 的 filter。\n```typescript\nimport { App, Configuration, Inject } from '@midwayjs/core';\nimport * as view from '@midwayjs/view-nunjucks';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [view],\n  importConfigs: [join(__dirname, 'config')]\n})\nexport class MainConfiguration {\n\n  @App()\n  app;\n\n  @Inject()\n  env: view.NunjucksEnvironment;\n\n  async onReady(){\n    this.env.addFilter('hello', (str) => {\n      return 'hi, ' + str;\n    });\n  }\n}\n\n```\n在模板里可以使用\n```typescript\n{{ name | hello }}\n```\n然后渲染\n```typescript\n// controller\n// ...\nawait ctx.render('test.nj', { name: 'midway' });\n```\n也会输出 `hi, midway` 。\n\n\n\n## 自定义模板引擎\n\n默认我们只提供了 ejs 和 nunjucks 的模板引擎，你也可以编写自己的模板引擎代码。\n\n### 实现模板引擎\n\n首先需要创建一个请求作用域的模板引擎类，它将在每个请求执行时初始化。你需需要实现其中的 `render` 和 `renderString` 方法。如果你的模板引擎不支持某个方法，可以抛出异常。\n\n```typescript\n// lib/view.ts\nimport { Provide, Config } from '@midwayjs/core';\nimport { IViewEngine } from '@midwayjs/view';\n\n@Provide()\nexport class MyView implements IViewEngine {\n\n  @Config('xxxx')\n  viewConfig;\n\n  async render(name: string, locals?: Record<string, any>, options?: RenderOptions) {\n    return myengine.render(name, locals, options);\n  }\n\n  async renderString(tpl: string,\n    locals?: Record<string, any>,\n    options?: RenderOptions) {\n\n    throw new Error('not implement');\n  }\n};\n```\n\n这两个方法接受类似的三个参数，`renderString` 第一个参数需要传入待解析的模板内容本身，而 `render` 方法会解析模板文件。\n\n`render(name, locals, viewOptions)`\n\n- name: 从 `root`（默认是 `/view` ) 相对的 path\n- locals: 模板需要的数据\n- viewOptions: 每次渲染的模板参数，可覆盖的配置，可以在配置文件中重写，其中包含几个参数：\n  - root: 模板的绝对路径\n  - name: 调用 render 方法的原始 name 值\n  - locals: 调用 render 方法的原始 locals 值\n\n`renderString(tpl, locals, viewOptions)`\n\n- tpl: 模板名\n- locals: 和 `render` 一样\n- viewOptions: 和 `render` 一样\n\n### 注册模板引擎\n\n在实现自定义的模板引擎后，我们需要在启动入口注册它。\n\n通过引入 `ViewManager` ，我们可以使用 `use` 方法注册自定义模板引擎。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject, Provide } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as view from '@midwayjs/view';\nimport { MyView } from './lib/my';\n\n@Configuration({\n  imports: [koa, view],\n  importConfigs: [join(__dirname, 'config')]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  viewManager: view.ViewManager;\n\n  async onReady(){\n    this.viewManager.use('ejs', MyView);\n  }\n}\n\n```\n\n\n\n## 注意事项\n\n\n如需在 egg(@midwayjs/web) 场景下使用，请在 `plugin.ts` 中关闭 view 和其相关插件。\n\n\n```typescript\nimport { EggPlugin } from 'egg';\nexport default {\n  // ...\n  view: false,\n} as EggPlugin;\n\n```\n\n\n否则会出现下面类似的错误。\n```\nTypeError: Cannot set property view of #<EggApplication> which has only a getter\n```\n\n","site/docs/extensions/security.md":"# 安全\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的通用安全组件，支持 `csrf` 、`xss` 等多种安全策略。\n\n相关信息：\n\n| web 支持情况      |      |\n| ----------------- | ---- |\n| @midwayjs/koa     | ✅    |\n| @midwayjs/faas    | ✅    |\n| @midwayjs/web     | ✅    |\n| @midwayjs/express | ✅    |\n\n\n\n## 安装使用\n\n1、安装依赖\n\n```bash\n$ npm i @midwayjs/security --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/security\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n2、在 configuration 中引入组件\n\n```typescript\nimport * as security from '@midwayjs/security';\n@Configuration({\n  imports: [\n    // ...other components\n    security\n  ],\n})\nexport class MainConfiguration {}\n```\n\n---\n\n## 防范常见的安全威胁\n\n\n### 一、CSRF\n\nCSRF（Cross-site request forgery 跨站请求伪造），是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。\n\n\n#### 1. 令牌同步模式\n通过响应页面时将 token 渲染到页面上，在开启 `csrf` 配置后，通过 `ctx.csrf` 可以获取到 `csrf token`，可以再返回页面 html 时同步输出\n\n```ts\n@Controller('/')\nexport class HomeController {\n  @Inject()\n  ctx;\n\n  @Get('/home')\n  async home() {\n    return `<form method=\"POST\" action=\"/upload?_csrf=${ this.ctx.csrf }\" >\n      title: <input name=\"title\" />\n      <button type=\"submit\">upload</button>\n    </form>`;\n  }\n}\n```\n\n传递 CSRF token 的字段（上述示例中的 `_csrf`）可以在配置中改变，请查看下述 `配置 -> csrf`。\n\n\n\n#### 2. Cookies 模式\n\n在 CSRF 默认配置下，token 会被设置在 Cookie 中，可以再前端页面中通过 JS 从 Cookies 中获取，然后再 ajax/fetch 等请求中添加到 `header`、`query` 或 `body` 中。\n\n```js\nconst csrftoken = Cookies.get('csrfToken');\nfetch('/api/post', {\n  method: 'POST',\n  headers: {\n    'x-csrf-token': csrftoken\n  },\n  ...\n});\n```\n\n默认配置下，框架会将 `CSRF token` 存在 `Cookie` 中，以方便前端 JS 发起请求时获取到。但是所有的子域名都可以设置 Cookie，因此当我们的应用处于无法保证所有的子域名都受控的情况下，存放在 `Cookie` 中可能有被 `CSRF` 攻击的风险。框架提供了一个配置项 `useSession`，可以将 token 存放到 Session 中。\n\n\n当 `CSRF token` 存储在 `Cookie` 中时，一旦在同一个浏览器上发生用户切换，新登陆的用户将会依旧使用旧的 token（之前用户使用的），这会带来一定的安全风险，因此在每次用户登陆的时候都必须调用 `ctx.rotateCsrfSecret()` 刷新 `CSRF token`，例如：\n\n\n```js\n@Controller('/')\nexport class HomeController {\n  @Inject()\n  ctx;\n\n  @Inject()\n  userService;\n\n  @Get('/login')\n  async login(@Body('username') username: string, @Body('password') password: string) {\n    const user = await userService.login({ username, password });\n    this.ctx.session = { user };\n    this.ctx.rotateCsrfSecret();\n    return { success: true };\n  }\n}\n```\n\n### 二、XSS\n\n`XSS`（cross-site scripting 跨站脚本攻击）攻击是最常见的 Web 攻击，是代码注入的一种。它允许恶意用户将代码注入到网页上，其他用户在观看网页时就会受到影响。\n\n`XSS` 攻击通常指的是通过利用网页开发时留下的漏洞，通过巧妙的方法注入恶意指令代码到网页，使用户加载并执行攻击者恶意制造的网页程序。攻击成功后，攻击者可能得到更高的权限（如执行一些操作）、私密网页内容、会话和cookie等各种内容。\n\n\n#### 1. 反射型的 XSS 攻击\n\n主要是由于服务端接收到客户端的不安全输入，在客户端触发代码执行从而发起 `Web` 攻击。\n\n例如：在某搜索网站搜索时，搜索结果会显示搜索的关键词。搜索关键词填入 `<script>alert('xss')</script>`, 点击搜索后，若页面程序没有对关键词进行处理，这段代码就会直接在页面上执行，弹出 alert。\n\n框架提供了 `ctx.security.escape()` 方法对字符串进行 XSS 过滤。\n\n```ts\n@Controller('/')\nexport class HomeController {\n  @Inject()\n  ctx;\n\n  @Get('/home')\n  async home() {\n    const str = `<script>alert('xss')</script>`;\n    const escapedStr = this.ctx.security.escape(str);\n    // &lt;script&gt;alert(&quot;xss&quot;) &lt;/script&gt;\n    return escapedStr;\n  }\n}\n```\n\n另外当网站输出的内容是作为 js 脚本的。这个时候需要使用 `ctx.security.js()` 来进行过滤。\n\n还有一种情况，有时候我们需要在 `js` 中输出 `json` ，若未做转义，易被利用为 `XSS` 漏洞。框架提供了 `ctx.security.json(变量)` 来提供 json encode，防止 XSS 攻击。\n\n\n```ts\n@Controller('/')\nexport class HomeController {\n  @Inject()\n  ctx;\n\n  @Get('/home')\n  async home() {\n    return `<script>windows.config = ${this.ctx.security.json( ...variable )};</script>`;\n  }\n}\n```\n\n#### 2. 存储型的 XSS 攻击\n\n通过提交带有恶意脚本的内容存储在服务器上，当其他人看到这些内容时发起 Web 攻击，比如一些网站的评论框中，用户恶意将一些代码作为评论内容，若没有过滤，其他用户看到这个评论时恶意代码就会执行。\n\n\n框架提供了 `ctx.security.html()` 来进行过滤。\n\n\n#### 3. 其他 XSS 的防范方式\n\n浏览器自身具有一定针对各种攻击的防范能力，他们一般是通过开启 Web 安全头生效的。框架内置了一些常见的 Web 安全头的支持。\n\n**CSP**\n\n`Content Security Policy`，简称 `CSP`，主要是用来定义页面可以加载哪些资源，减少 `XSS` 的发生。\n\n默认关闭（可通过 `csp: {enable: true}` 配置开启），开启后可以有效的防止 `XSS` 攻击的发生。要配置 `CSP` , 需要对 `CSP` 的 `policy` 策略有了解，具体细节可以参考 [阿里聚安全 - CSP是什么？](https://www.zhihu.com/question/21979782/answer/122682029)\n\n\n**X-Download-Options:noopen**\n\n默认开启（可通过 `noopen: {enable: false}` 配置关闭），禁用 IE 下下载框 Open 按钮，防止 IE 下下载文件默认被打开 XSS。\n\n**X-Content-Type-Options:nosniff**\n禁用 IE8 自动嗅探 mime 功能，默认关闭（可通过 `nosniff: {enable: true}` 配置开启），例如 text/plain 却当成 text/html 渲染，特别当本站点 serve 的内容未必可信的时候。\n\n**X-XSS-Protection**\nIE 提供的一些 XSS 检测与防范，默认开启（可通过 `xssProtection: {enable: false}` 配置关闭）\n\nclose 默认值 false，即设置为 1; mode=block\n\n---\n\n\n## 配置\n\n默认配置如下：\n\n```ts\n// src/config/config.default\nexport default {\n  // ...\n\n  // 默认配置\n  security: {\n    csrf: {\n      enable: true,\n      type: 'ctoken',\n      useSession: false,\n      cookieName: 'csrfToken',\n      sessionName: 'csrfToken',\n      headerName: 'x-csrf-token',\n      bodyName: '_csrf',\n      queryName: '_csrf',\n      refererWhiteList: [],\n    },\n    xframe: {\n      enable: true,\n      value: 'SAMEORIGIN',\n    },\n    csp: {\n      enable: false,\n    },\n    hsts: {\n      enable: false,\n      maxAge: 365 * 24 * 3600,\n      includeSubdomains: false,\n    },\n    noopen: {\n      enable: false,\n    },\n    nosniff: {\n      enable: false,\n    },\n    xssProtection: {\n      enable: true,\n      value: '1; mode=block',\n    },\n  },\n}\n\n```\n\n### csrf\n\n| 配置项 | 类型                                   | 作用描述 | 默认值 |\n| --- |--------------------------------------| --- | --- |\n| enable | boolean                              | 是否开启 | true |\n| type | 'all' / 'any' / 'ctoken' / 'referer' | csrf 校验类型，all/any 等于 ctoken + referer | 'ctoken' 从query/header/body 中获取 csrf token；；'referer' 则可以通过 refererWhiteList 配置白名单 |\n| useSession | boolean                              | csrf token 是否存放在 session 中 | false，默认存放在 cookies 中 |\n| cookieName | string                               | token 在 cookie 中存放的 字段 | 'csrfToken' |\n| sessionName | string                               | token 在 session 中存放的 字段 | 'csrfToken' |\n| headerName | string                               | token 在 header 中存放的 字段 | 'x-csrf-token' |\n| bodyName | string                               | token 在 body 中存放的 字段 | '_csrf' |\n| queryName | string                               | token 在 query 中存放的 字段 | '_csrf' |\n| refererWhiteList | Array\\<string\\>                      | 允许的来源白名单 | [] |\n\n#### 配置 refererWhiteList 不生效？\n+ 原因一：refererWhiteList 中需要配置 referer 的 host 部分，例如 referer 为 `https://midway-demo.com:1234/docs`，则 refererWhiteList 中需要配置 `midway-demo.com:1234`。\n+ 原因二：refererWhiteList 仅在 csrf 配置中 type 为 `referer` 的情况下生效，默认 type 为 `ctoken`，需要修改为 `referer`。\n+ 原因三：发送的http请求中的 referer 字段不是一个标准的 url 地址（例如没有加上请求协议等），参考 [MDN](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Referer) 文档\n\n### xframe\n\n\nxframe 用来配置 `X-Frame-Options` 响应头，用来给浏览器指示允许一个页面可否在 `frame`, `iframe`, `embed` 或者 `object` 中展现的标记。站点可以通过确保网站没有被嵌入到别人的站点里面，从而避免 `clickjacking` 攻击。\n\n`X-Frame-Options` 有三个可能的值：\n\n+ X-Frame-Options: deny：页面不允许在 frame 中展示\n+ X-Frame-Options: sameorigin：该页面可以在相同域名页面的 frame 中展示\n+ X-Frame-Options: allow-from https://example.com/：该页面可以在指定来源的 frame 中展示\n\n\n\n| 配置项 | 类型 | 作用描述 | 默认值 |\n| --- | --- | --- | --- |\n| enable | boolean | 是否开启 | true |\n| value | string | X-Frame-Options 值 | 'SAMEORIGIN' |\n\n\n\n### hsts\n\n`HTTP Strict Transport Security`（通常简称为 `HSTS` ）是一个安全功能，它告诉浏览器只能通过 `HTTPS` 访问当前资源，而不是 `HTTP`。\n\n| 配置项 | 类型 | 作用描述 | 默认值 |\n| --- | --- | --- | --- |\n| enable | boolean | 是否开启 | false |\n| maxAge | number | 在浏览器收到这个请求后的多少 `秒` 时间内凡是访问这个域名下的请求都使用HTTPS请求 | `365 * 24 * 3600` 即一年 |\n| includeSubdomains | boolean | 此规则是否适用于该网站的所有子域名 | false |\n\n\n### csp\n\nHTTP 响应头 `Content-Security-Policy` 允许站点管理者控制指定的页面加载哪些资源。这将帮助防止跨站脚本攻击（XSS）。\n\n\n| 配置项 | 类型                                                       | 作用描述 | 默认值 |\n| --- |----------------------------------------------------------| --- | --- |\n| enable | boolean                                                  | 是否开启 | false |\n| policy | Object\\<key: string, value: string / string[] / boolean> | 策略列表 | {} |\n| reportOnly | boolean                                                  | 是否开启 | false |\n| supportIE | boolean                                                  | 是否支持IE浏览器 | false |\n\n详细的 `policy` 配置可以参考: [Content Security Policy (CSP) 是什么？阿里聚安全](https://www.zhihu.com/question/21979782/answer/122682029)\n\n\n### noopen\n\n用于指定 `IE 8` 以上版本的用户不打开文件而直接保存文件。在下载对话框中不显式“打开”选项。\n\n| 配置项 | 类型 | 作用描述 | 默认值 |\n| --- | --- | --- | --- |\n| enable | boolean | 是否开启 | false |\n\n\n\n\n### nosniff\n\n开启后，如果从 `script` 或 `stylesheet` 读入的文件的 `MIME` 类型与指定 `MIME` 类型不匹配，不允许读取该文件。用于防止 `XSS` 等跨站脚本攻击。\n\n| 配置项 | 类型 | 作用描述 | 默认值 |\n| --- | --- | --- | --- |\n| enable | boolean | 是否开启 | false |\n\n\n\n\n### xssProtection\n\n用于启用浏览器的XSS过滤功能，以防止 `XSS` 跨站脚本攻击。\n\n`X-XSS-Protection` 响应头是 `IE`，`Chrome` 和 `Safari` 的一个特性，当检测到跨站脚本攻击 (XSS (en-US))时，浏览器将停止加载页面。若网站设置了良好的 `Content-Security-Policy` 来禁用内联 JavaScript ('unsafe-inline')，现代浏览器不太需要这些保护， 但其仍然可以为尚不支持 `CSP` 的旧版浏览器的用户提供保护。\n\n`X-XSS-Protection` 可以配置下述四个值\n\n+ `0`: 禁止XSS过滤。\n+ `1`：启用XSS过滤（通常浏览器是默认的）。 如果检测到跨站脚本攻击，浏览器将清除页面（删除不安全的部分）。\n+ `1;mode=block`：启用XSS过滤。 如果检测到攻击，浏览器将不会清除页面，而是阻止页面加载。\n+ `1; report=<reporting-URI>`： Chromium only，启用XSS过滤。 如果检测到跨站脚本攻击，浏览器将清除页面并使用CSP report-uri (en-US)指令的功能发送违规报告。\n\n| 配置项 | 类型 | 作用描述 | 默认值 |\n| --- | --- | --- | --- |\n| enable | boolean | 是否开启 | false |\n| value | string | X-XSS-Protection 配置 | `1; mode=block` |\n\n","site/docs/extensions/sequelize.md":"# Sequelize\n\n本文档介绍如何在 Midway 中使用 Sequelize。\n\n:::tip\n\n当前模块从 v3.4.0 开始已经重构，历史写法兼容，如果查询历史文档，请参考 [这里](../legacy/sequelize)。\n\n:::\n\n相关信息：\n\n| 描述              |     |\n| ----------------- | --- |\n| 可用于标准项目    | ✅  |\n| 可用于 Serverless | ✅  |\n| 可用于一体化      | ✅  |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 和老写法的区别\n\n如果想使用新版本的用法，请参考下面的流程，将老代码进行修改，新老代码不能混用。\n\n升级方法：\n\n- 1、请在业务依赖中显式添加 `sequelize` 和 `sequelize-typescript`\n- 2、不再使用 `BaseTable` 装饰器，而直接使用 `sequelize-typescript` 包导出的 `Table` 装饰器\n- 3、在 `src/config.default` 的 `sequelize` 部分配置调整，参考下面的数据源配置部分\n  - 3.1 修改为数据源的形式 `sequelize.dataSource`\n  - 3.2 将实体模型在数据源的 `entities` 字段中声明\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/sequelize@4 sequelize sequelize-typescript --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/sequelize\": \"^4.0.0\",\n    \"sequelize\": \"^6.21.3\",\n    \"sequelize-typescript\": \"^2.1.0\"\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n## 安装数据库 Driver\n\n常用数据库驱动如下，选择你对应连接的数据库类型安装：\n\n```bash\n# for MySQL or MariaDB，也可以使用 mysql2 替代\nnpm install mysql --save\nnpm install mysql2 --save\n\n# for PostgreSQL or CockroachDB\nnpm install pg --save\n\n# for SQLite\nnpm install sqlite3 --save\n\n# for Microsoft SQL Server\nnpm install mssql --save\n\n# for sql.js\nnpm install sql.js --save\n\n# for Oracle\nnpm install oracledb --save\n\n# for MongoDB(experimental)\nnpm install mongodb --save\n```\n\n下面的文档，我们将以 `mysql2` 作为示例。\n\n\n### Directory structure\n\n一个基础的参考目录结构如下。\n\n\n```\nMyProject\n├── src\n│   ├── config\n│   │   └── config.default.ts\n│   ├── entity\n│   │   └── person.entity.ts\n│   ├── configuration.ts\n│   └── service\n├── .gitignore\n├── package.json\n├── README.md\n└── tsconfig.json\n```\n\n\n## 启用组件\n\n在 `src/configuration.ts` 文件中启用组件。\n\n```typescript\nimport { Configuration, ILifeCycle } from '@midwayjs/core';\nimport { join } from 'path';\nimport * as sequelize from '@midwayjs/sequelize';\n\n@Configuration({\n  imports: [\n    // ...\n    sequelize,\n  ],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration implements ILifeCycle {\n  // ...\n}\n```\n\n## 模型定义\n\n### 1、创建 Model（Entity）\n\n我们通过模型和数据库关联，在应用中的模型就是数据库表，在 Sequelize 中，模型是和实体绑定的，每一个实体（Entity) 文件，即是 Model，也是实体（Entity）。\n\n在示例中，需要一个实体，我们这里拿 `person` 举例。新建 entity 目录，在其中添加实体文件 `person.entity.ts` ，一个简单的实体如下。\n\n```typescript\n// src/entity/person.entity.ts\nimport { Table, Model, Column, HasMany } from 'sequelize-typescript';\n\n@Table\nexport class Hobby extends Model {\n  @Column\n  name: string;\n}\n\n@Table\nexport class Person extends Model {\n  @Column\n  name: string;\n\n  @Column\n  birthday: Date;\n\n  @HasMany(() => Hobby)\n  hobbies: Hobby[];\n}\n```\n\n要注意，这里的实体文件的每一个属性，其实是和数据库表一一对应的，基于现有的数据库表，我们往上添加内容。\n\n`@Table` 装饰器可以在不传递任何参数的情况下使用，更多参数请查看 [定义选项](https://sequelize.org/v5/manual/models-definition.html#configuration) 。\n\n```typescript\n@Table({\n  timestamps: true,\n  ...\n})\nexport class Person extends Model {}\n```\n\n\n\n### 2、主键\n\n主键 (id) 将从基类 Model 继承。 一般来说主键是 Integer 类型并且是自增的。\n\n主键设置有两种方法，设置 `@Column({primaryKey: true})` 或者 `@PrimaryKey`。\n\n比如：\n\n```typescript\nimport { Table, Model, PrimaryKey } from 'sequelize-typescript';\n\n@Table\nexport class Person extends Model {\n  @PrimaryKey\n  name: string;\n}\n```\n\n### 3、时间列\n\n主要指代的是 `@CreatedAt`, `@UpdatedAt`, `@DeletedAt` 单个装饰器标注的列。\n\n比如：\n\n```typescript\nimport { Table, Model, CreatedAt, UpdatedAt, DeletedAt } from 'sequelize-typescript';\n\n@Table\nexport class Person extends Model {\n  @CreatedAt\n  creationDate: Date;\n\n  @UpdatedAt\n  updatedOn: Date;\n\n  @DeletedAt\n  deletionDate: Date;\n}\n```\n\n| 装饰器       | 描述                                                                    |\n| ------------ | ----------------------------------------------------------------------- |\n| `@CreatedAt` | 会设置 `timestamps=true` 和 `createdAt='creationDate'`                  |\n| `@UpdatedAt` | 会设置 `timestamps=true` 和 `updatedAt='updatedOn'`                     |\n| `@DeletedAt` | 会设置 `timestamps=true`, `paranoid=true` 和 `deletedAt='deletionDate'` |\n\n### 4、普通列\n\n@Column 装饰器用于标注普通列，可以在不传递任何参数的情况下使用。 但是因此需要能够自动推断 js 类型（详见[类型推断](https://github.com/sequelize/sequelize-typescript#type-inference)）。\n\n```typescript\nimport { Table, Model, Column } from 'sequelize-typescript';\n\n@Table\nexport class Person extends Model {\n  @Column\n  name: string;\n}\n```\n\n或者指定列类型。\n\n```typescript\nimport { Table, Column, DataType } from 'sequelize-typescript';\n\n@Table\nexport class Person extends Model {\n  @Column(DataType.TEXT)\n  name: string;\n}\n```\n\n更多类型描述，请参考 [这里](https://sequelize.org/v5/manual/models-definition.html#configuration)。\n\n比如：\n\n```typescript\nimport { Table, Model, Column, DataType } from 'sequelize-typescript'\n\n@Table\nexport class Person extends Model {\n  @Column({\n    type: DataType.FLOAT,\n    comment: 'Some value',\n    ...\n  })\n  value: number;\n}\n```\n\n| 装饰器                               | 描述                                                                                              |\n| ------------------------------------ | ------------------------------------------------------------------------------------------------- |\n| `@Column`                            | 使用推导的 [dataType](https://sequelize.org/v5/manual/models-definition.html#data-types) 作为类型 |\n| `@Column(dataType: DataType)`        | 显式设置 [dataType](https://sequelize.org/v5/manual/models-definition.html#data-types)            |\n| `@Column(options: AttributeOptions)` | 设置 [attribute options](https://sequelize.org/v5/manual/models-definition.html#configuration)    |\n\n## 数据源配置\n\n新版本我们启用了 [数据源机制](../data_source)，在 `src/config.default.ts` 中配置：\n\n```typescript\n// src/config/config.default.ts\n\nimport { Person } from '../entity/person.entity';\n\nexport default {\n  // ...\n  sequelize: {\n    dataSource: {\n      // 第一个数据源，数据源的名字可以完全自定义\n      default: {\n        database: 'test4',\n        username: 'root',\n        password: '123456',\n        host: '127.0.0.1',\n        port: 3306,\n        encrypt: false,\n        dialect: 'mysql',\n        define: { charset: 'utf8' },\n        timezone: '+08:00',\n        // 本地的时候，可以通过 sync: true 直接 createTable\n        sync: false,\n        \n        // 实体形式\n        entities: [Person],\n\n        // 支持如下的扫描形式，为了兼容我们可以同时进行.js和.ts匹配️\n        entities: [\n          'entity',                        // 指定目录\n          '**/entity/*.entity.{j,t}s',     // 通配加后缀匹配\n        ],\n      },\n\n      // 第二个数据源\n      default2: {\n        // ...\n      },\n    },\n  },\n};\n```\n\n\n\n## 模型关联\n\n可以通过 `HasMany` 、`@HasOne` 、`@BelongsTo`、`@BelongsToMany` 和 `@ForeignKey` 装饰器在模型中直接描述关系。\n\n:::tip\n\n你不需要在数据库中创建外键也可以使用这个功能。\n\n:::\n\n### 一对多\n\n```typescript\nimport { Table, Model, Column, ForeignKey, BelongsTo, HasMany } from 'sequelize-typescript';\n\n@Table\nexport class Player extends Model {\n  @Column\n  name: string;\n\n  @Column\n  num: number;\n\n  @ForeignKey(() => Team)\n  @Column\n  teamId: number;\n\n  @BelongsTo(() => Team)\n  team: Team;\n}\n\n@Table\nexport class Team extends Model {\n  @Column\n  name: string;\n\n  @HasMany(() => Player)\n  players: Player[];\n}\n```\n\n`sequelize-typescript` 会在内部进行关联，会自动查询出相关的依赖。\n\n比如通过 `find` 查询。\n\n```typescript\nconst team = await Team.findOne({ include: [Player] });\n\nteam.players.forEach((player) => {\n  console.log(`Player ${player.name}`);\n});\n```\n\n### 多对多\n\n```typescript\nimport { Table, Model, Column, ForeignKey, BelongsToMany } from 'sequelize-typescript';\n\n@Table\nexport class Book extends Model {\n  @BelongsToMany(() => Author, () => BookAuthor)\n  authors: Author[];\n}\n\n@Table\nexport class Author extends Model {\n  @BelongsToMany(() => Book, () => BookAuthor)\n  books: Book[];\n}\n\n@Table\nexport class BookAuthor extends Model {\n  @ForeignKey(() => Book)\n  @Column\n  bookId: number;\n\n  @ForeignKey(() => Author)\n  @Column\n  authorId: number;\n}\n```\n\n上面的类型，在某些场景下是不安全的，比如上面的 `BookAuthor`，`Author` 的 `books` 的类型，可能会丢失某些属性，需要手动设置。\n\n```typescript\n@BelongsToMany(() => Book, () => BookAuthor)\nbooks: Array<Book & {BookAuthor: BookAuthor}>;\n```\n\n### 一对一\n\n对于一对一，使用 `@HasOne(...)`（关系的外键存在于另一个模型上）和 `@BelongsTo(...)`（关系的外键存在于此模型上）。\n\n比如：\n\n```typescript\nimport { Table, Column, Model, BelongsTo, ForeignKey } from 'sequelize-typescript';\nimport { User } from './user.entity';\n\n@Table\nexport class Photo extends Model {\n  @ForeignKey(() => User)\n  @Column({\n    comment: '用户Id',\n  })\n  userId: number;\n\n  @BelongsTo(() => User)\n  user: User;\n\n  @Column({\n    comment: '名字',\n  })\n  name: string;\n}\n\n@Table\nexport class User extends Model {\n  @Column\n  name: string;\n}\n```\n\n\n\n### 模型循环依赖\n\n如果你使用了 `@BelongsTo` 装饰器，很容易触发一个模型循环依赖的错误，比如：\n\n```\nReferenceError: Cannot access 'Photo' before initialization\n```\n\n你可以将类型使用 `ReturnType` 包裹起来。\n\n```typescript\nimport { Table, Column, Model, BelongsTo, ForeignKey } from 'sequelize-typescript';\nimport { User } from './user.entity';\n\n@Table\nexport class Photo extends Model {\n  // ...\n  @BelongsTo(() => User)\n  user: ReturnType<() => User>;\n}\n```\n\n\n\n\n\n## 静态操作方法\n\n如果是单个数据源，可以使用下面的静态方法。\n\n### 保存\n\n在需要调用的地方，使用实体模型来操作。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Person } from '../entity/person.entity';\n\n@Provide()\nexport class PersonService {\n  async createPerson() {\n    const person = new Person({ name: 'bob', age: 99 });\n    await person.save();\n  }\n}\n```\n\n### 查找和更新\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Person } from '../entity/person.entity';\n\n@Provide()\nexport class PersonService {\n  async updatePerson() {\n    const person = await Person.findOne();\n    // 更新\n    person.age = 100;\n    await person.save();\n\n    await Person.update(\n      {\n        name: 'bobby',\n      },\n      {\n        where: { id: 1 },\n      }\n    );\n  }\n}\n```\n\n## Repository 模式\n\nRepository 模式可以将查找、创建等静态操作从模型定义中分离出来。它还支持与多个 sequelize 实例（多数据源）一起使用。\n\n### 启动 Repository 模式\n\n和数据源配置相同，只是多了一个属性。\n\n```typescript\n// src/config/config.default.ts\n\nimport { Person } from '../entity/person.entity';\n\nexport default {\n  // ...\n  sequelize: {\n    dataSource: {\n      default: {\n        // ...\n        entities: [Person],\n\n        // 多了这一个\n        repositoryMode: true,\n      },\n    },\n    sync: false,\n  },\n};\n```\n\n如果是多个数据源，务必在每个数据源都开启该属性，开启后，原有的静态操作方法不再可用。\n\n你需要使用 `Repository` 的操作方式。\n\n### 使用 Repository 模式\n\n基本 API 和静态操作相同，Midway 对其进行了一些简单包裹，使用 `InjectRepository` 装饰器可以在服务中注入 `Repository`。\n\n```typescript\nimport { Controller, Get } from '@midwayjs/core';\nimport { InjectRepository } from '@midwayjs/sequelize';\nimport { Photo } from '../entity/photo.entity';\nimport { User } from '../entity/user.entity';\nimport { Op } from 'sequelize';\nimport { Repository } from 'sequelize-typescript';\n\n@Controller('/')\nexport class HomeController {\n  @InjectRepository(User)\n  userRepository: Repository<User>;\n\n  @InjectRepository(Photo)\n  photoRepository: Repository<Photo>;\n\n  @Get('/')\n  async home() {\n    // 查询\n    let result = await this.photoRepository.findAll();\n    console.log(result);\n\n    // 新增\n    await this.photoRepository.create({\n      name: '123',\n    });\n\n    // 删除\n    await this.photoRepository.destroy({\n      where: {\n        name: '123',\n      },\n    });\n\n    // 联合查询\n    // SELECT * FROM photo WHERE name = \"23\" OR name = \"34\";\n    let result = await this.photoRepository.findAll({\n      where: {\n        [Op.or]: [{ name: '23' }, { name: '34' }],\n      },\n    });\n    // => result\n\n    // 连表查询\n    let result = await this.userRepository.findAll({ include: [Photo] });\n    // => result\n  }\n}\n```\n\n关于 OP 的更多用法：[https://sequelize.org/v5/manual/querying.html](https://sequelize.org/v5/manual/querying.html)\n\n### 多库的支持\n\n在 Repository 模式下，我们可以在 `InjectRepository` 参数中指定特定的数据源。\n\n```typescript\nimport { Controller } from '@midwayjs/core';\nimport { InjectRepository } from '@midwayjs/sequelize';\nimport { Photo } from '../entity/photo.entity';\nimport { User } from '../entity/user.entity';\nimport { Repository } from 'sequelize-typescript';\n\n@Controller('/')\nexport class HomeController {\n  // 指定某个数据源\n  @InjectRepository(User, 'default')\n  userRepository: Repository<User>;\n  // ...\n}\n```\n\n\n## 高级功能\n\n### 数据源同步配置\n\nsequelize 在同步数据源时可以添加 sync 的参数。\n\n```typescript\nexport default {\n  // ...\n  sequelize: {\n    dataSource: {\n      default: {\n        sync: true,\n        syncOptions: {\n          force: false,\n          alter: true,\n        },\n      },\n    },\n    // 多个数据源时可以用这个指定默认的数据源\n    defaultDataSourceName: 'default',\n  },\n};\n```\n\n### 指定默认数据源\n\n在包含多个数据源时，可以指定默认的数据源。\n\n```typescript\nexport default {\n  // ...\n  sequelize: {\n    dataSource: {\n      default1: {\n        // ...\n      },\n      default2: {\n        // ...\n      },\n    },\n    // 多个数据源时可以用这个指定默认的数据源\n    defaultDataSourceName: 'default1',\n  },\n};\n```\n\n\n\n### 获取数据源\n\n数据源即创建出的 sequelize 对象，我们可以通过注入内置的数据源管理器来获取。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport { SequelizeDataSourceManager } from '@midwayjs/sequelize';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n\n  async onReady(container: IMidwayContainer) {\n    const dataSourceManager = await container.getAsync(SequelizeDataSourceManager);\n    const conn = dataSourceManager.getDataSource('default');\n    await conn.authenticate();\n  }\n}\n```\n\n从 v3.8.0 开始，也可以通过装饰器注入。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport { InjectDataSource } from '@midwayjs/sequelize';\nimport { Sequelize } from 'sequelize-typescript';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n\n  // 注入默认数据源\n  @InjectDataSource()\n  defaultDataSource: Sequelize;\n\n  // 注入自定义数据源\n  @InjectDataSource('default1')\n  customDataSource: Sequelize;\n\n  async onReady(container: IMidwayContainer) {\n    // ...\n  }\n}\n```\n\n\n\n## 常见问题\n\n### 1、Dialect needs to be explicitly supplied as of v4.0.0\n\n原因为配置中数据源没有指定 `dialect` 字段，确认数据源的结构，格式以及配置合并的结果。\n\n\n\n### 2、生成实体列\n\n请参考社区提供的模块，如 [sequelize-typescript-generator](https://github.com/spinlud/sequelize-typescript-generator)\n\n\n\n### 3、Raw Query\n\n如果遇到比较复杂的，可以使用 [raw query 方法](https://sequelize.org/v5/manual/raw-queries.html)\n\n\n\n### 4、TS2612 错误\n\n如果你的模型列报了 TS2612 错误，比如：\n\n```\nsrc/entity/AesTenantConfigInfo.ts:29:6 - error TS2612: Property 'id' will overwrite the base property in 'Model<AesTenantConfigInfoAttributes, AesTenantConfigInfoAttributes>'. If this is intentional, add an initializer. Otherwise, add a 'declare' modifier or remove the redundant declaration.\n\n29      id?: number;\n        ~~\n```\n\n可以将其赋一个空值。\n\n```typescript\nimport { Table, Column } from 'sequelize-typescript';\n\n@Table\nexport class User extends Model {\n  @Column({\n    primaryKey: true,\n    autoIncrement: true,\n    type: DataType.BIGINT,\n  })\n  id?: number = undefined;\n}\n```\n\n\n\n## 其他\n\n- 上面的文档，翻译自 sequelize-typescript，更多 API ，请参考 [英文文档](<(https://github.com/sequelize/sequelize-typescrip)>)\n- 一些 [案例](https://github.com/ddzyan/midway-practice)\n","site/docs/extensions/socketio.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Fsocketio.md -->","site/docs/extensions/static_file.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Fstatic_file.md -->","site/docs/extensions/swagger.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Fswagger.md -->","site/docs/extensions/tablestore.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Ftablestore.md -->","site/docs/extensions/tags.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Ftags.md -->","site/docs/extensions/tenant.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Ftenant.md -->","site/docs/extensions/upload.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Fupload.md -->","site/docs/extensions/validate.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Fvalidate.md -->","site/docs/extensions/validation.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Fvalidation.md -->","site/docs/extensions/ws.md":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Fws.md -->"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"<!-- OPENSPEC:START -->\n# OpenSpec Instructions\n\nThese instructions are for AI assistants working in this project.\n\nAlways open `@/openspec/AGENTS.md` when the request:\n- Mentions planning or proposals (words like proposal, spec, change, plan)\n- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work\n- Sounds ambiguous and you need the authoritative spec before coding\n\nUse `@/openspec/AGENTS.md` to learn:\n- How to create and apply change proposals\n- Spec format and conventions\n- Project structure and guidelines\n\nKeep this managed block so 'openspec update' can refresh the instructions.\n\n<!-- OPENSPEC:END -->\n\n# Repository Guide for Agents\n\n## Project overview\n- Midway v4 monorepo managed by lerna + pnpm workspace.\n- TypeScript 5, built with `tsc`, tested with `jest`.\n- IoC container implementation lives in `packages/core` (MidwayContainer, DecoratorManager, MetadataManager).\n\n## Code rules\n- Each component is an independent npm package and can communicate via DI.\n- New/changed components must include tests. Run `npm run test` in the package.\n- Use `mwts` for linting (`npm run lint:fix` when needed).\n- After code changes, rerun the relevant lint command and keep necessary lint-fix output in the patch instead of reverting it casually.\n- Add meaningful comments for functions/classes/interfaces/enums.\n\n## Docs rules\n- Current docs live under `site/docs` (Docusaurus).\n- Match existing doc style/tone/structure for consistency.\n- Show dependency installation in both bash (npm) and JSON `package.json` forms.\n- Doc changes do not require a build check.\n- For component docs, write for beginners first: start from what the component solves, when to use it, and the simplest working path, then go deeper into advanced usage, configuration, and extension points. Prefer tutorial-style progression over API-only lists.\n\n## Practical tips\n- Prefer `pnpm -C <package> test` to scope tests.\n- Keep changes aligned with existing patterns in the targeted package.\n- Pull request titles must use standard commit format, such as `feat: xxx` or `fix: xxx`.\n","isInternal":false,"tokens":467,"sizeBytes":2107},{"name":"AGENTS.md","path":"openspec/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/openspec/AGENTS.md","title":"openspec - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# OpenSpec Instructions\n\nInstructions for AI coding assistants using OpenSpec for spec-driven development.\n\n## TL;DR Quick Checklist\n\n- Search existing work: `openspec spec list --long`, `openspec list` (use `rg` only for full-text search)\n- Decide scope: new capability vs modify existing capability\n- Pick a unique `change-id`: kebab-case, verb-led (`add-`, `update-`, `remove-`, `refactor-`)\n- Scaffold: `proposal.md`, `tasks.md`, `design.md` (only if needed), and delta specs per affected capability\n- Write deltas: use `## ADDED|MODIFIED|REMOVED|RENAMED Requirements`; include at least one `#### Scenario:` per requirement\n- Validate: `openspec validate [change-id] --strict --no-interactive` and fix issues\n- Request approval: Do not start implementation until proposal is approved\n\n## Three-Stage Workflow\n\n### Stage 1: Creating Changes\nCreate proposal when you need to:\n- Add features or functionality\n- Make breaking changes (API, schema)\n- Change architecture or patterns  \n- Optimize performance (changes behavior)\n- Update security patterns\n\nTriggers (examples):\n- \"Help me create a change proposal\"\n- \"Help me plan a change\"\n- \"Help me create a proposal\"\n- \"I want to create a spec proposal\"\n- \"I want to create a spec\"\n\nLoose matching guidance:\n- Contains one of: `proposal`, `change`, `spec`\n- With one of: `create`, `plan`, `make`, `start`, `help`\n\nSkip proposal for:\n- Bug fixes (restore intended behavior)\n- Typos, formatting, comments\n- Dependency updates (non-breaking)\n- Configuration changes\n- Tests for existing behavior\n\n**Workflow**\n1. Review `openspec/project.md`, `openspec list`, and `openspec list --specs` to understand current context.\n2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, optional `design.md`, and spec deltas under `openspec/changes/<id>/`.\n3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement.\n4. Run `openspec validate <id> --strict --no-interactive` and resolve any issues before sharing the proposal.\n\n### Stage 2: Implementing Changes\nTrack these steps as TODOs and complete them one by one.\n1. **Read proposal.md** - Understand what's being built\n2. **Read design.md** (if exists) - Review technical decisions\n3. **Read tasks.md** - Get implementation checklist\n4. **Implement tasks sequentially** - Complete in order\n5. **Confirm completion** - Ensure every item in `tasks.md` is finished before updating statuses\n6. **Update checklist** - After all work is done, set every task to `- [x]` so the list reflects reality\n7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved\n\n### Stage 3: Archiving Changes\nAfter deployment, create separate PR to:\n- Move `changes/[name]/` → `changes/archive/YYYY-MM-DD-[name]/`\n- Update `specs/` if capabilities changed\n- Use `openspec archive <change-id> --skip-specs --yes` for tooling-only changes (always pass the change ID explicitly)\n- Run `openspec validate --strict --no-interactive` to confirm the archived change passes checks\n\n## Before Any Task\n\n**Context Checklist:**\n- [ ] Read relevant specs in `specs/[capability]/spec.md`\n- [ ] Check pending changes in `changes/` for conflicts\n- [ ] Read `openspec/project.md` for conventions\n- [ ] Run `openspec list` to see active changes\n- [ ] Run `openspec list --specs` to see existing capabilities\n\n**Before Creating Specs:**\n- Always check if capability already exists\n- Prefer modifying existing specs over creating duplicates\n- Use `openspec show [spec]` to review current state\n- If request is ambiguous, ask 1–2 clarifying questions before scaffolding\n\n### Search Guidance\n- Enumerate specs: `openspec spec list --long` (or `--json` for scripts)\n- Enumerate changes: `openspec list` (or `openspec change list --json` - deprecated but available)\n- Show details:\n  - Spec: `openspec show <spec-id> --type spec` (use `--json` for filters)\n  - Change: `openspec show <change-id> --json --deltas-only`\n- Full-text search (use ripgrep): `rg -n \"Requirement:|Scenario:\" openspec/specs`\n\n## Quick Start\n\n### CLI Commands\n\n```bash\n# Essential commands\nopenspec list                  # List active changes\nopenspec list --specs          # List specifications\nopenspec show [item]           # Display change or spec\nopenspec validate [item]       # Validate changes or specs\nopenspec archive <change-id> [--yes|-y]   # Archive after deployment (add --yes for non-interactive runs)\n\n# Project management\nopenspec init [path]           # Initialize OpenSpec\nopenspec update [path]         # Update instruction files\n\n# Interactive mode\nopenspec show                  # Prompts for selection\nopenspec validate              # Bulk validation mode\n\n# Debugging\nopenspec show [change] --json --deltas-only\nopenspec validate [change] --strict --no-interactive\n```\n\n### Command Flags\n\n- `--json` - Machine-readable output\n- `--type change|spec` - Disambiguate items\n- `--strict` - Comprehensive validation\n- `--no-interactive` - Disable prompts\n- `--skip-specs` - Archive without spec updates\n- `--yes`/`-y` - Skip confirmation prompts (non-interactive archive)\n\n## Directory Structure\n\n```\nopenspec/\n├── project.md              # Project conventions\n├── specs/                  # Current truth - what IS built\n│   └── [capability]/       # Single focused capability\n│       ├── spec.md         # Requirements and scenarios\n│       └── design.md       # Technical patterns\n├── changes/                # Proposals - what SHOULD change\n│   ├── [change-name]/\n│   │   ├── proposal.md     # Why, what, impact\n│   │   ├── tasks.md        # Implementation checklist\n│   │   ├── design.md       # Technical decisions (optional; see criteria)\n│   │   └── specs/          # Delta changes\n│   │       └── [capability]/\n│   │           └── spec.md # ADDED/MODIFIED/REMOVED\n│   └── archive/            # Completed changes\n```\n\n## Creating Change Proposals\n\n### Decision Tree\n\n```\nNew request?\n├─ Bug fix restoring spec behavior? → Fix directly\n├─ Typo/format/comment? → Fix directly  \n├─ New feature/capability? → Create proposal\n├─ Breaking change? → Create proposal\n├─ Architecture change? → Create proposal\n└─ Unclear? → Create proposal (safer)\n```\n\n### Proposal Structure\n\n1. **Create directory:** `changes/[change-id]/` (kebab-case, verb-led, unique)\n\n2. **Write proposal.md:**\n```markdown\n# Change: [Brief description of change]\n\n## Why\n[1-2 sentences on problem/opportunity]\n\n## What Changes\n- [Bullet list of changes]\n- [Mark breaking changes with **BREAKING**]\n\n## Impact\n- Affected specs: [list capabilities]\n- Affected code: [key files/systems]\n```\n\n3. **Create spec deltas:** `specs/[capability]/spec.md`\n```markdown\n## ADDED Requirements\n### Requirement: New Feature\nThe system SHALL provide...\n\n#### Scenario: Success case\n- **WHEN** user performs action\n- **THEN** expected result\n\n## MODIFIED Requirements\n### Requirement: Existing Feature\n[Complete modified requirement]\n\n## REMOVED Requirements\n### Requirement: Old Feature\n**Reason**: [Why removing]\n**Migration**: [How to handle]\n```\nIf multiple capabilities are affected, create multiple delta files under `changes/[change-id]/specs/<capability>/spec.md`—one per capability.\n\n4. **Create tasks.md:**\n```markdown\n## 1. Implementation\n- [ ] 1.1 Create database schema\n- [ ] 1.2 Implement API endpoint\n- [ ] 1.3 Add frontend component\n- [ ] 1.4 Write tests\n```\n\n5. **Create design.md when needed:**\nCreate `design.md` if any of the following apply; otherwise omit it:\n- Cross-cutting change (multiple services/modules) or a new architectural pattern\n- New external dependency or significant data model changes\n- Security, performance, or migration complexity\n- Ambiguity that benefits from technical decisions before coding\n\nMinimal `design.md` skeleton:\n```markdown\n## Context\n[Background, constraints, stakeholders]\n\n## Goals / Non-Goals\n- Goals: [...]\n- Non-Goals: [...]\n\n## Decisions\n- Decision: [What and why]\n- Alternatives considered: [Options + rationale]\n\n## Risks / Trade-offs\n- [Risk] → Mitigation\n\n## Migration Plan\n[Steps, rollback]\n\n## Open Questions\n- [...]\n```\n\n## Spec File Format\n\n### Critical: Scenario Formatting\n\n**CORRECT** (use #### headers):\n```markdown\n#### Scenario: User login success\n- **WHEN** valid credentials provided\n- **THEN** return JWT token\n```\n\n**WRONG** (don't use bullets or bold):\n```markdown\n- **Scenario: User login**  ❌\n**Scenario**: User login     ❌\n### Scenario: User login      ❌\n```\n\nEvery requirement MUST have at least one scenario.\n\n### Requirement Wording\n- Use SHALL/MUST for normative requirements (avoid should/may unless intentionally non-normative)\n\n### Delta Operations\n\n- `## ADDED Requirements` - New capabilities\n- `## MODIFIED Requirements` - Changed behavior\n- `## REMOVED Requirements` - Deprecated features\n- `## RENAMED Requirements` - Name changes\n\nHeaders matched with `trim(header)` - whitespace ignored.\n\n#### When to use ADDED vs MODIFIED\n- ADDED: Introduces a new capability or sub-capability that can stand alone as a requirement. Prefer ADDED when the change is orthogonal (e.g., adding \"Slash Command Configuration\") rather than altering the semantics of an existing requirement.\n- MODIFIED: Changes the behavior, scope, or acceptance criteria of an existing requirement. Always paste the full, updated requirement content (header + all scenarios). The archiver will replace the entire requirement with what you provide here; partial deltas will drop previous details.\n- RENAMED: Use when only the name changes. If you also change behavior, use RENAMED (name) plus MODIFIED (content) referencing the new name.\n\nCommon pitfall: Using MODIFIED to add a new concern without including the previous text. This causes loss of detail at archive time. If you aren’t explicitly changing the existing requirement, add a new requirement under ADDED instead.\n\nAuthoring a MODIFIED requirement correctly:\n1) Locate the existing requirement in `openspec/specs/<capability>/spec.md`.\n2) Copy the entire requirement block (from `### Requirement: ...` through its scenarios).\n3) Paste it under `## MODIFIED Requirements` and edit to reflect the new behavior.\n4) Ensure the header text matches exactly (whitespace-insensitive) and keep at least one `#### Scenario:`.\n\nExample for RENAMED:\n```markdown\n## RENAMED Requirements\n- FROM: `### Requirement: Login`\n- TO: `### Requirement: User Authentication`\n```\n\n## Troubleshooting\n\n### Common Errors\n\n**\"Change must have at least one delta\"**\n- Check `changes/[name]/specs/` exists with .md files\n- Verify files have operation prefixes (## ADDED Requirements)\n\n**\"Requirement must have at least one scenario\"**\n- Check scenarios use `#### Scenario:` format (4 hashtags)\n- Don't use bullet points or bold for scenario headers\n\n**Silent scenario parsing failures**\n- Exact format required: `#### Scenario: Name`\n- Debug with: `openspec show [change] --json --deltas-only`\n\n### Validation Tips\n\n```bash\n# Always use strict mode for comprehensive checks\nopenspec validate [change] --strict --no-interactive\n\n# Debug delta parsing\nopenspec show [change] --json | jq '.deltas'\n\n# Check specific requirement\nopenspec show [spec] --json -r 1\n```\n\n## Happy Path Script\n\n```bash\n# 1) Explore current state\nopenspec spec list --long\nopenspec list\n# Optional full-text search:\n# rg -n \"Requirement:|Scenario:\" openspec/specs\n# rg -n \"^#|Requirement:\" openspec/changes\n\n# 2) Choose change id and scaffold\nCHANGE=add-two-factor-auth\nmkdir -p openspec/changes/$CHANGE/{specs/auth}\nprintf \"## Why\\n...\\n\\n## What Changes\\n- ...\\n\\n## Impact\\n- ...\\n\" > openspec/changes/$CHANGE/proposal.md\nprintf \"## 1. Implementation\\n- [ ] 1.1 ...\\n\" > openspec/changes/$CHANGE/tasks.md\n\n# 3) Add deltas (example)\ncat > openspec/changes/$CHANGE/specs/auth/spec.md << 'EOF'\n## ADDED Requirements\n### Requirement: Two-Factor Authentication\nUsers MUST provide a second factor during login.\n\n#### Scenario: OTP required\n- **WHEN** valid credentials are provided\n- **THEN** an OTP challenge is required\nEOF\n\n# 4) Validate\nopenspec validate $CHANGE --strict --no-interactive\n```\n\n## Multi-Capability Example\n\n```\nopenspec/changes/add-2fa-notify/\n├── proposal.md\n├── tasks.md\n└── specs/\n    ├── auth/\n    │   └── spec.md   # ADDED: Two-Factor Authentication\n    └── notifications/\n        └── spec.md   # ADDED: OTP email notification\n```\n\nauth/spec.md\n```markdown\n## ADDED Requirements\n### Requirement: Two-Factor Authentication\n...\n```\n\nnotifications/spec.md\n```markdown\n## ADDED Requirements\n### Requirement: OTP Email Notification\n...\n```\n\n## Best Practices\n\n### Simplicity First\n- Default to <100 lines of new code\n- Single-file implementations until proven insufficient\n- Avoid frameworks without clear justification\n- Choose boring, proven patterns\n\n### Complexity Triggers\nOnly add complexity with:\n- Performance data showing current solution too slow\n- Concrete scale requirements (>1000 users, >100MB data)\n- Multiple proven use cases requiring abstraction\n\n### Clear References\n- Use `file.ts:42` format for code locations\n- Reference specs as `specs/auth/spec.md`\n- Link related changes and PRs\n\n### Capability Naming\n- Use verb-noun: `user-auth`, `payment-capture`\n- Single purpose per capability\n- 10-minute understandability rule\n- Split if description needs \"AND\"\n\n### Change ID Naming\n- Use kebab-case, short and descriptive: `add-two-factor-auth`\n- Prefer verb-led prefixes: `add-`, `update-`, `remove-`, `refactor-`\n- Ensure uniqueness; if taken, append `-2`, `-3`, etc.\n\n## Tool Selection Guide\n\n| Task | Tool | Why |\n|------|------|-----|\n| Find files by pattern | Glob | Fast pattern matching |\n| Search code content | Grep | Optimized regex search |\n| Read specific files | Read | Direct file access |\n| Explore unknown scope | Task | Multi-step investigation |\n\n## Error Recovery\n\n### Change Conflicts\n1. Run `openspec list` to see active changes\n2. Check for overlapping specs\n3. Coordinate with change owners\n4. Consider combining proposals\n\n### Validation Failures\n1. Run with `--strict` flag\n2. Check JSON output for details\n3. Verify spec file format\n4. Ensure scenarios properly formatted\n\n### Missing Context\n1. Read project.md first\n2. Check related specs\n3. Review recent archives\n4. Ask for clarification\n\n## Quick Reference\n\n### Stage Indicators\n- `changes/` - Proposed, not yet built\n- `specs/` - Built and deployed\n- `archive/` - Completed changes\n\n### File Purposes\n- `proposal.md` - Why and what\n- `tasks.md` - Implementation steps\n- `design.md` - Technical decisions\n- `spec.md` - Requirements and behavior\n\n### CLI Essentials\n```bash\nopenspec list              # What's in progress?\nopenspec show [item]       # View details\nopenspec validate --strict --no-interactive  # Is it correct?\nopenspec archive <change-id> [--yes|-y]  # Mark complete (add --yes for automation)\n```\n\nRemember: Specs are truth. Changes are proposals. Keep them in sync.\n","isInternal":false,"tokens":3627,"sizeBytes":15087},{"name":"SKILL.md","path":".codex/skills/ui-ux-pro-max/SKILL.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/.codex/skills/ui-ux-pro-max/SKILL.md","title":"ui-ux-pro-max","category":"anthropic-skill","format":"markdown","content":"---\nname: ui-ux-pro-max\ndescription: \"UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 8 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient.\"\n---\n\n# UI/UX Pro Max - Design Intelligence\n\nSearchable database of UI styles, color palettes, font pairings, chart types, product recommendations, UX guidelines, and stack-specific best practices.\n\n## Prerequisites\n\nCheck if Python is installed:\n\n```bash\npython3 --version || python --version\n```\n\nIf Python is not installed, install it based on user's OS:\n\n**macOS:**\n```bash\nbrew install python3\n```\n\n**Ubuntu/Debian:**\n```bash\nsudo apt update && sudo apt install python3\n```\n\n**Windows:**\n```powershell\nwinget install Python.Python.3.12\n```\n\n---\n\n## How to Use This Skill\n\nWhen user requests UI/UX work (design, build, create, implement, review, fix, improve), follow this workflow:\n\n### Step 1: Analyze User Requirements\n\nExtract key information from user request:\n- **Product type**: SaaS, e-commerce, portfolio, dashboard, landing page, etc.\n- **Style keywords**: minimal, playful, professional, elegant, dark mode, etc.\n- **Industry**: healthcare, fintech, gaming, education, etc.\n- **Stack**: React, Vue, Next.js, or default to `html-tailwind`\n\n### Step 2: Search Relevant Domains\n\nUse `search.py` multiple times to gather comprehensive information. Search until you have enough context.\n\n```bash\npython3 .shared/ui-ux-pro-max/scripts/search.py \"<keyword>\" --domain <domain> [-n <max_results>]\n```\n\n**Recommended search order:**\n\n1. **Product** - Get style recommendations for product type\n2. **Style** - Get detailed style guide (colors, effects, frameworks)\n3. **Typography** - Get font pairings with Google Fonts imports\n4. **Color** - Get color palette (Primary, Secondary, CTA, Background, Text, Border)\n5. **Landing** - Get page structure (if landing page)\n6. **Chart** - Get chart recommendations (if dashboard/analytics)\n7. **UX** - Get best practices and anti-patterns\n8. **Stack** - Get stack-specific guidelines (default: html-tailwind)\n\n### Step 3: Stack Guidelines (Default: html-tailwind)\n\nIf user doesn't specify a stack, **default to `html-tailwind`**.\n\n```bash\npython3 .shared/ui-ux-pro-max/scripts/search.py \"<keyword>\" --stack html-tailwind\n```\n\nAvailable stacks: `html-tailwind`, `react`, `nextjs`, `vue`, `svelte`, `swiftui`, `react-native`, `flutter`\n\n---\n\n## Search Reference\n\n### Available Domains\n\n| Domain | Use For | Example Keywords |\n|--------|---------|------------------|\n| `product` | Product type recommendations | SaaS, e-commerce, portfolio, healthcare, beauty, service |\n| `style` | UI styles, colors, effects | glassmorphism, minimalism, dark mode, brutalism |\n| `typography` | Font pairings, Google Fonts | elegant, playful, professional, modern |\n| `color` | Color palettes by product type | saas, ecommerce, healthcare, beauty, fintech, service |\n| `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof |\n| `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie |\n| `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading |\n| `prompt` | AI prompts, CSS keywords | (style name) |\n\n### Available Stacks\n\n| Stack | Focus |\n|-------|-------|\n| `html-tailwind` | Tailwind utilities, responsive, a11y (DEFAULT) |\n| `react` | State, hooks, performance, patterns |\n| `nextjs` | SSR, routing, images, API routes |\n| `vue` | Composition API, Pinia, Vue Router |\n| `svelte` | Runes, stores, SvelteKit |\n| `swiftui` | Views, State, Navigation, Animation |\n| `react-native` | Components, Navigation, Lists |\n| `flutter` | Widgets, State, Layout, Theming |\n\n---\n\n## Example Workflow\n\n**User request:** \"Làm landing page cho dịch vụ chăm sóc da chuyên nghiệp\"\n\n**AI should:**\n\n```bash\n# 1. Search product type\npython3 .shared/ui-ux-pro-max/scripts/search.py \"beauty spa wellness service\" --domain product\n\n# 2. Search style (based on industry: beauty, elegant)\npython3 .shared/ui-ux-pro-max/scripts/search.py \"elegant minimal soft\" --domain style\n\n# 3. Search typography\npython3 .shared/ui-ux-pro-max/scripts/search.py \"elegant luxury\" --domain typography\n\n# 4. Search color palette\npython3 .shared/ui-ux-pro-max/scripts/search.py \"beauty spa wellness\" --domain color\n\n# 5. Search landing page structure\npython3 .shared/ui-ux-pro-max/scripts/search.py \"hero-centric social-proof\" --domain landing\n\n# 6. Search UX guidelines\npython3 .shared/ui-ux-pro-max/scripts/search.py \"animation\" --domain ux\npython3 .shared/ui-ux-pro-max/scripts/search.py \"accessibility\" --domain ux\n\n# 7. Search stack guidelines (default: html-tailwind)\npython3 .shared/ui-ux-pro-max/scripts/search.py \"layout responsive\" --stack html-tailwind\n```\n\n**Then:** Synthesize all search results and implement the design.\n\n---\n\n## Tips for Better Results\n\n1. **Be specific with keywords** - \"healthcare SaaS dashboard\" > \"app\"\n2. **Search multiple times** - Different keywords reveal different insights\n3. **Combine domains** - Style + Typography + Color = Complete design system\n4. **Always check UX** - Search \"animation\", \"z-index\", \"accessibility\" for common issues\n5. **Use stack flag** - Get implementation-specific best practices\n6. **Iterate** - If first search doesn't match, try different keywords\n7. **Split Into Multiple Files** - For better maintainability:\n   - Separate components into individual files (e.g., `Header.tsx`, `Footer.tsx`)\n   - Extract reusable styles into dedicated files\n   - Keep each file focused and under 200-300 lines\n\n---\n\n## Common Rules for Professional UI\n\nThese are frequently overlooked issues that make UI look unprofessional:\n\n### Icons & Visual Elements\n\n| Rule | Do | Don't |\n|------|----|----- |\n| **No emoji icons** | Use SVG icons (Heroicons, Lucide, Simple Icons) | Use emojis like 🎨 🚀 ⚙️ as UI icons |\n| **Stable hover states** | Use color/opacity transitions on hover | Use scale transforms that shift layout |\n| **Correct brand logos** | Research official SVG from Simple Icons | Guess or use incorrect logo paths |\n| **Consistent icon sizing** | Use fixed viewBox (24x24) with w-6 h-6 | Mix different icon sizes randomly |\n\n### Interaction & Cursor\n\n| Rule | Do | Don't |\n|------|----|----- |\n| **Cursor pointer** | Add `cursor-pointer` to all clickable/hoverable cards | Leave default cursor on interactive elements |\n| **Hover feedback** | Provide visual feedback (color, shadow, border) | No indication element is interactive |\n| **Smooth transitions** | Use `transition-colors duration-200` | Instant state changes or too slow (>500ms) |\n\n### Light/Dark Mode Contrast\n\n| Rule | Do | Don't |\n|------|----|----- |\n| **Glass card light mode** | Use `bg-white/80` or higher opacity | Use `bg-white/10` (too transparent) |\n| **Text contrast light** | Use `#0F172A` (slate-900) for text | Use `#94A3B8` (slate-400) for body text |\n| **Muted text light** | Use `#475569` (slate-600) minimum | Use gray-400 or lighter |\n| **Border visibility** | Use `border-gray-200` in light mode | Use `border-white/10` (invisible) |\n\n### Layout & Spacing\n\n| Rule | Do | Don't |\n|------|----|----- |\n| **Floating navbar** | Add `top-4 left-4 right-4` spacing | Stick navbar to `top-0 left-0 right-0` |\n| **Content padding** | Account for fixed navbar height | Let content hide behind fixed elements |\n| **Consistent max-width** | Use same `max-w-6xl` or `max-w-7xl` | Mix different container widths |\n\n---\n\n## Pre-Delivery Checklist\n\nBefore delivering UI code, verify these items:\n\n### Visual Quality\n- [ ] No emojis used as icons (use SVG instead)\n- [ ] All icons from consistent icon set (Heroicons/Lucide)\n","frontmatter":{"name":"ui-ux-pro-max","description":"UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 8 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient."},"isInternal":false,"tokens":2127,"sizeBytes":8244},{"name":"SKILL.md","path":"packages/skill-midway/skills/midway/SKILL.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/skill-midway/skills/midway/SKILL.md","title":"midway","category":"anthropic-skill","format":"markdown","content":"---\nname: midway\ndescription: Use when the task involves Midway framework docs, package selection, API lookup, version compatibility, MCP/HTTP/framework components, or migration guidance. Resolve the requested Midway version first, then query the local Midway skill bundle with `midway-skill` commands.\n---\n\n# Midway\n\nUse the local `@midwayjs/skill-midway` bundle before relying on memory.\n\n## Workflow\n\n1. Resolve the user's requested version first.\n   Run `npm exec midway-skill -- resolve-version <version>` or `pnpm exec midway-skill resolve-version <version>`.\n   If no version is given, use `current`.\n\n2. Query the right source for the question.\n   Use `npm exec midway-skill -- lookup-docs --query \"<topic>\"` or `pnpm exec midway-skill lookup-docs --query \"<topic>\"`.\n   Use `npm exec midway-skill -- lookup-api --symbol \"<symbol>\"` or `pnpm exec midway-skill lookup-api --symbol \"<symbol>\"` for current-major API lookups.\n   Use `npm exec midway-skill -- lookup-packages --query \"<package or feature>\"` or `pnpm exec midway-skill lookup-packages --query \"<package or feature>\"`.\n   Use `npm exec midway-skill -- lookup-changelog --package \"<package>\"` or `pnpm exec midway-skill lookup-changelog --package \"<package>\"` for release history.\n\n3. Cite what the bundle resolved.\n   Include the requested version, resolved version, and whether it was an exact match or major-version fallback.\n\n## Rules\n\n- Prefer bundle results over recollection.\n- Historical majors only guarantee `docs` and `changelog`.\n- If `resolve-version` reports `api: false`, do not claim exact historical API details.\n- When results are empty, say that the bundle did not contain a match instead of inventing one.\n- Prefer package names with the `@midwayjs/` scope in answers when known.\n\n## Output\n\n- State the resolved Midway version.\n- Summarize the matched docs/API/package/changelog result.\n- Keep links or source paths from bundle records when relevant.\n","frontmatter":{"name":"midway","description":"Use when the task involves Midway framework docs, package selection, API lookup, version compatibility, MCP/HTTP/framework components, or migration guidance. Resolve the requested Midway version first, then query the local Midway skill bundle with `midway-skill` commands."},"isInternal":false,"tokens":449,"sizeBytes":1940},{"name":"alinode.md","path":"site/docs/extensions/alinode.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/alinode.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# Alinode\n\n## 准备工作\n\n需要接入的应用是要部署在独立的服务获取云环境，可以接入互联网服务。\n\n## 创建服务\n\n**第一步**\n\n登录阿里云，点击开通 [阿里云的 Node.js 性能平台](https://www.aliyun.com/product/nodejs) 的服务。\n\n**第二步**\n\n创建新应用，获取 APP ID 和 App Secret。\n<img src=\"https://cdn.nlark.com/yuque/0/2021/png/187105/1617267785895-dd0fb702-91c7-4b25-9c64-8a9358f2d254.png#align=left&display=inline&height=351&margin=%5Bobject%20Object%5D&name=image.png&originHeight=702&originWidth=1548&size=106487&status=done&style=none&width=774\" width=\"774\" />\n\n## 安装监控依赖\n\n**第一步**\n\n安装 Node.js 性能平台所需的组件\n\n```bash\n# 安装版本管理工具 tnvm，安装过程出错参考：https://github.com/aliyun-node/tnvm\n$ wget -O- https://raw.githubusercontent.com/aliyun-node/tnvm/master/install.sh | bash\n$ source ~/.bashrc\n\n# tnvm ls-remote alinode \t\t\t\t\t\t\t\t# 查看需要的版本\n$ tnvm install alinode-v6.5.0 \t\t\t\t\t# 安装需要的版本\n$ tnvm use alinode-v6.5.0 \t\t\t\t\t\t\t# 使用需要的版本\n\n$ npm install @alicloud/agenthub -g \t\t# 安装 agenthub\n```\n\n这里有三个部分\n\n- 1、安装 tnvm（alinode 源）\n- 2、使用 tnvm 安装 alinode（替代默认的 node）\n- 3、安装 alinode 需要的数据采集器\n\n安装完成后，可以检查一下，需要确保 `which node` 和 `which agenthub` 的路径中包括 `.tnvm` 即可。\n\n```bash\n$ which node\n/root/.tnvm/versions/alinode/v3.11.4/bin/node\n\n$ which agenthub\n/root/.tnvm/versions/alinode/v3.11.4/bin/agenthub\n```\n\n将 `创建新应用` 中获得的 `App ID` 和 `App Secret` 按如下所示保存为 `yourconfig.json`。比如放在项目根目录。\n\n```typescript\n{\n  \"appid\": \"****\",\n  \"secret\": \"****\",\n}\n```\n\n启动插件：\n\n```typescript\nagenthub start yourconfig.json\n```\n\n## 启动 node 服务\n\n在安装了服务器中，启动 Node 服务时，需要加入 ENABLE_NODE_LOG=YES 环境变量。\n\n比如：\n\n```bash\n$ NODE_ENV=production ENABLE_NODE_LOG=YES node bootstrap.js\n```\n\n## Docker 容器的方法\n\n关于 docker 容器的方法可以查看 [文档](https://help.aliyun.com/document_detail/66027.html?spm=a2c4g.11186623.6.580.261ba70feI6mWt)。\n\n## 其他\n\n更多内容可以查看阿里云 Node.js 性能平台的 [文档](https://help.aliyun.com/document_detail/60338.html?spm=a2c4g.11186623.6.548.599312e6IkGO9v)。\n","isInternal":false,"tokens":826,"sizeBytes":2413},{"name":"apollo.md","path":"site/docs/extensions/apollo.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/apollo.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# Apollo GraphQL\n\nGraphQL 适合需要让前端按需查询字段、聚合多个后端数据源，或者希望用统一查询语言暴露 API 的应用。Midway 通过 `@midwayjs/apollo` 提供基于 Apollo Server 的 GraphQL HTTP 端点，并让 Resolver 直接使用 Midway 的依赖注入和请求上下文。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n## 安装依赖\n\nApollo 组件已经内置 Apollo Server、GraphQL、WebSocket 订阅等运行时依赖，不需要额外安装 `@apollo/server` 或 `graphql`。\n\n```bash\n$ npm i @midwayjs/apollo@4 --save\n```\n\n如果使用 Koa 或 Express，还需要安装对应 Web 框架组件：\n\n```bash\n$ npm i @midwayjs/koa@4 --save\n# 或者\n$ npm i @midwayjs/express@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/apollo\": \"^4.0.0\",\n    \"@midwayjs/koa\": \"^4.0.0\"\n  }\n}\n```\n\n## 开启组件\n\n在 `src/configuration.ts` 中导入 Apollo 组件和一个 Web 框架组件。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as apollo from '@midwayjs/apollo';\n\n@Configuration({\n  imports: [\n    koa,\n    apollo,\n  ],\n})\nexport class MainConfiguration {}\n```\n\n组件默认会把 GraphQL 端点挂载到 `/graphql`，默认支持 `GET` 和 `POST` 请求。\n\n## 最小示例\n\n在 `src/config/config.default.ts` 中配置 GraphQL Schema 和 Resolver。\n\n```typescript\nexport const apollo = {\n  path: '/graphql',\n  typeDefs: `\n    type Query {\n      hello: String!\n    }\n  `,\n  resolvers: {\n    Query: {\n      hello: () => 'hello midway',\n    },\n  },\n};\n```\n\n启动应用后，请求 `/graphql`。\n\n```bash\n$ curl -X POST http://127.0.0.1:7001/graphql \\\n  -H 'content-type: application/json' \\\n  -d '{\"query\":\"{ hello }\"}'\n```\n\n返回：\n\n```json\n{\n  \"data\": {\n    \"hello\": \"hello midway\"\n  }\n}\n```\n\n开发环境下，也可以直接在浏览器访问 `/graphql` 打开 GraphiQL 调试页面。\n\n## 使用 Schema 文件\n\n当 Schema 较多时，推荐把 SDL 放到 `.graphql` 文件中，再通过 `typePaths` 加载。\n\n```graphql\n# src/graphql/schema.graphql\ntype Query {\n  user(id: ID!): User\n}\n\ntype Mutation {\n  updateUserName(id: ID!, name: String!): User\n}\n\ntype User {\n  id: ID!\n  name: String!\n}\n```\n\n配置文件中使用 `typePaths`。\n\n```typescript\nexport const apollo = {\n  path: '/graphql',\n  typePaths: ['./graphql/**/*.graphql'],\n};\n```\n\n`typePaths` 中的相对路径会从应用的 `baseDir` 开始解析，既支持精确文件路径，也支持 glob 表达式。你也可以同时配置 `typeDefs` 和 `typePaths`，组件会在启动时合并。\n\n## Resolver Map\n\n你可以直接在配置里写 Apollo Resolver Map。\n\n```typescript\nimport { UserService } from '../service/user';\n\nexport const apollo = {\n  typeDefs: `\n    type Query {\n      userName(id: ID!): String\n    }\n\n    type Mutation {\n      updateUserName(id: ID!, name: String!): String\n    }\n  `,\n  resolvers: {\n    Query: {\n      userName: async (_parent, args, context) => {\n        const userService = await context.requestContext.getAsync(UserService);\n        const user = await userService.findById(args.id);\n        return user?.name;\n      },\n    },\n    Mutation: {\n      updateUserName: async (_parent, args, context) => {\n        const userService = await context.requestContext.getAsync(UserService);\n        const user = await userService.updateName(args.id, args.name);\n        return user.name;\n      },\n    },\n  },\n};\n```\n\nResolver 的第三个参数就是当前 Midway 请求上下文，不需要再从 `context.ctx` 解包。Koa 场景下，`context` 是 Midway 增强后的 Koa ctx；Express 场景下，`context` 是 Midway 增强后的 Express request/context。\n\nMidway 会在上下文上挂载 `requestContext`、`logger`、`getLogger()`、`setAttr()`、`getAttr()` 和 `getApp()`。\n\n## Resolver 类\n\n也可以使用 Resolver 类来获得更自然的依赖注入体验。相关装饰器可以直接从 `@midwayjs/apollo` 中引入。\n\n```typescript\nimport { Inject } from '@midwayjs/core';\nimport { Args, Context, Mutation, Query, Resolver } from '@midwayjs/apollo';\nimport { UserService } from '../service/user';\n\n@Resolver()\nexport class UserResolver {\n  @Inject()\n  userService: UserService;\n\n  @Query('userName')\n  async userName(@Args('id') id: string, @Context() context) {\n    context.logger.info('query user %s', id);\n    const user = await this.userService.findById(id);\n    return user?.name;\n  }\n\n  @Mutation('updateUserName')\n  async updateUserName(@Args('id') id: string, @Args('name') name: string) {\n    const user = await this.userService.updateName(id, name);\n    return user.name;\n  }\n}\n```\n\nResolver 类会由 Midway 容器托管，支持 `@Inject()` 和请求级依赖。组件会自动发现使用 `@Resolver()` 标记的类，也可以通过 `resolverClasses` 显式传入。\n\n```typescript\nimport { UserResolver } from '../resolver/user';\n\nexport const apollo = {\n  typePaths: ['./graphql/**/*.graphql'],\n  resolverClasses: [UserResolver],\n};\n```\n\n可用的 Resolver 装饰器包括：\n\n| 装饰器            | 说明                     |\n| ----------------- | ------------------------ |\n| `@Resolver()`     | 标记一个 Resolver 类     |\n| `@Query()`        | 标记 Query 字段          |\n| `@Mutation()`     | 标记 Mutation 字段       |\n| `@Subscription()` | 标记 Subscription 字段   |\n\n可用的参数装饰器包括：\n\n| 装饰器       | 说明                     |\n| ------------ | ------------------------ |\n| `@Parent()`  | 获取父级 Resolver 返回值 |\n| `@Args()`    | 获取 GraphQL 参数        |\n| `@Context()` | 获取 Midway 请求上下文   |\n| `@Info()`    | 获取 GraphQL resolve info |\n\n不传参数时会注入完整对象，传入字段名时只注入对应字段，例如 `@Args('id')`。\n\n## 扩展 Context\n\n可以通过 `contextFactory` 给每次 GraphQL 请求补充字段。\n\n```typescript\nexport const apollo = {\n  typeDefs,\n  resolvers,\n  contextFactory: async context => {\n    return {\n      currentUserId: context.headers['x-user-id'],\n    };\n  },\n};\n```\n\n随后在 Resolver 中读取：\n\n```typescript\nconst userId = context.currentUserId;\n```\n\n内置字段如 `requestContext`、`logger`、`getLogger`、`setAttr`、`getAttr` 和 `getApp` 会被保留，不建议覆盖。\n\n## GraphiQL\n\n开发环境默认开启 GraphiQL。浏览器访问 `/graphql` 时会显示 GraphiQL 页面，生产环境默认关闭。\n\n```typescript\nexport const apollo = {\n  graphiql: true,\n};\n```\n\n可以显式关闭：\n\n```typescript\nexport const apollo = {\n  graphiql: false,\n};\n```\n\n也可以定制页面标题和请求地址：\n\n```typescript\nexport const apollo = {\n  graphiql: {\n    title: 'My GraphQL Console',\n    endpoint: '/graphql',\n  },\n};\n```\n\n## 订阅\n\n组件支持 `graphql-ws` 协议的 GraphQL Subscription。默认关闭，需要在配置中开启。\n\n```typescript\nexport const apollo = {\n  path: '/graphql',\n  subscriptions: true,\n  typeDefs: `\n    type Query {\n      hello: String!\n    }\n\n    type Subscription {\n      counter: Int!\n    }\n  `,\n};\n```\n\nResolver 方法需要返回 `AsyncIterable`。\n\n```typescript\nimport { Resolver, Subscription } from '@midwayjs/apollo';\n\n@Resolver()\nexport class CounterResolver {\n  @Subscription('counter')\n  async *counter() {\n    yield {\n      counter: 1,\n    };\n  }\n}\n```\n\n开启后，WebSocket 地址默认和 HTTP GraphQL 地址一致，例如 `ws://127.0.0.1:7001/graphql`。也可以单独指定订阅路径。\n\n```typescript\nexport const apollo = {\n  path: '/graphql',\n  subscriptions: {\n    path: '/graphql',\n    connectionInitWaitTimeout: 3000,\n  },\n};\n```\n\n客户端需要使用 `graphql-ws` 协议连接。\n\n```typescript\nimport { createClient } from 'graphql-ws';\n\nconst client = createClient({\n  url: 'ws://127.0.0.1:7001/graphql',\n});\n\nclient.subscribe(\n  {\n    query: 'subscription { counter }',\n  },\n  {\n    next: data => {\n      console.log(data);\n    },\n    error: err => {\n      console.error(err);\n    },\n    complete: () => {},\n  }\n);\n```\n\n## HTTP 行为\n\n默认只处理 `GET` 和 `POST` 请求。其他方法请求 GraphQL 路径会返回 `405 Method Not Allowed`。\n\n```typescript\nexport const apollo = {\n  methods: ['POST'],\n};\n```\n\n如果修改了 GraphQL 路径，HTTP 和订阅默认都会使用新的路径。\n\n```typescript\nexport const apollo = {\n  path: '/api/graphql',\n};\n```\n\n## Apollo Server 配置\n\nApollo Server 专属配置放在 `apollo.apollo` 下，避免和通用 GraphQL 字段混在一起。\n\n```typescript\nexport const apollo = {\n  typeDefs,\n  resolvers,\n  apollo: {\n    introspection: true,\n    plugins: [],\n  },\n};\n```\n\n常见配置如下：\n\n| 配置项           | 说明                                      |\n| ---------------- | ----------------------------------------- |\n| `path`           | GraphQL HTTP 路径，默认为 `/graphql`      |\n| `methods`        | 允许的 HTTP 方法，默认为 `['GET', 'POST']` |\n| `typeDefs`       | 内联 GraphQL SDL                          |\n| `typePaths`      | `.graphql` 文件路径或 glob 表达式         |\n| `resolvers`      | Apollo Resolver Map                       |\n| `resolverClasses` | Midway Resolver 类列表                   |\n| `contextFactory` | 扩展每次请求的 GraphQL context            |\n| `graphiql`       | GraphiQL 开关或配置                       |\n| `subscriptions`  | GraphQL WebSocket 订阅开关或配置          |\n| `apollo`         | 透传给 Apollo Server 的配置               |\n\n## 生产建议\n\n生产环境建议显式关闭 GraphiQL，并根据安全要求决定是否开启 introspection。\n\n```typescript\nexport const apollo = {\n  graphiql: false,\n  apollo: {\n    introspection: false,\n  },\n};\n```\n\n如果需要文件上传，推荐先通过 GraphQL 获取上传凭证，再由客户端直接上传到 OSS、S3 等对象存储。GraphQL multipart upload 不作为组件默认能力开启。\n","isInternal":false,"tokens":2834,"sizeBytes":10168},{"name":"axios.md","path":"site/docs/extensions/axios.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/axios.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# HTTP 请求\n\n\n\n## 简单的 HTTP 请求\n\nMidway 内置了一个简单的 HTTP 请求客户端，无需引入三方包即可使用。\n\n默认 Get 请求，返回数据为 Buffer。\n\n内置的 Http 客户端只提供最简单的能力，仅满足大部分的前端接口数据获取，如需复杂的功能，比如文件上传等，请使用其他的客户端，如 fetch，axios，got 等。\n\n\n\n### 简单方法形式\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nconst result = await makeHttpRequest('http://127.1:7001/');\n\n// Buffer.isBuffer(result.data)  => true\n```\n\nGet 请求，带上 Query，返回类型为 JSON。\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nconst result = await makeHttpRequest('http://127.1:7001/', {\n  data: {\n    a: 1,\n    b: 2\n  },\n  dataType: 'json',\t// 返回的数据格式\n});\n\n// typeof result.data => 'object'\n// result.data.url => /?a=1&b=2\n```\n\n可以指定类型\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nconst result = await makeHttpRequest('http://127.1:7001/', {\n  method: 'GET',\n  dataType: 'json',\n});\n```\n\n返回 text 格式。\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nconst result = await makeHttpRequest('http://127.1:7001/', {\n  method: 'GET',\n  dataType: 'text',\n});\n```\n\nPOST 请求并返回 JSON。\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nconst result = await makeHttpRequest('http://127.1:7001/', {\n  method: 'POST',\n  data: {\n    a: 1,\n    b: 2\n  },\n  dataType: 'json',\n  contentType:'json', \t// 发送的 post 为 json\n});\n\n// result.data ...\n```\n\n:::caution\n注意，请不要在请求中直接返回 result 对象，result 对象是标准的 httpResponse，在大部分场景下无法被直接序列化，会抛出对象循环的错误。\n:::\n\n设置请求超时时间。\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nlet err;\n// 超时会报错，注意 catch\ntry {\n  const result = await makeHttpRequest('http://127.1:7001/', {\n    method: 'GET',\n    dataType: 'text',\n    timeout: 500,\n  });\n} catch (e) {\n  err = e;\n}\n```\n\n\n\n### 实例形式\n\n```typescript\nimport { HttpClient } from '@midwayjs/core';\n\nconst httpclient = new HttpClient();\nconst result = await httpclient.request('http://127.1:7001/');\n\n// Buffer.isBuffer(result.data)  => true\n```\n\n和方法形式参数相同。\n\n```typescript\nimport { HttpClient } from '@midwayjs/core';\n\nconst httpclient = new HttpClient();\nconst result = await httpclient.request('http://127.1:7001/', {\n  method: 'POST',\n  data: {\n    a: 1,\n    b: 2\n  },\n  dataType: 'json',\n  contentType:'json', \t// 发送的 post 为 json\n});\n\n// result.data ...\n```\n\n示例形式，可以复用创建出的对象，并且每次请求，都可以带上一些固定的参数，比如 header。\n\n```typescript\nimport { HttpClient } from '@midwayjs/core';\n\nconst httpclient = new HttpClient({\n  headers: {\n    'x-timeout': '5'\n  },\n  method: 'POST',\n  timeout: 2000\n});\n\n// 每次都会带上 headers\nconst result = await httpclient.request('http://127.1:7001/');\n\n```\n\n\n\n\n\n## Axios 支持\n\nMidway 包裹了 [axios](https://github.com/axios/axios) 包，使得在代码中可以简单的使用 axios 接口。\n\n和 axios 的一些关系如下：\n\n- 接口完全一致\n- 适配依赖注入写法，完整的类型定义\n- 方便实例管理和配置统一\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n\n### 安装依赖\n\n```bash\n$ npm i @midwayjs/axios@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/axios\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n### 引入组件\n\n\n首先，引入 组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as axios from '@midwayjs/axios';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    axios\t\t// 导入 axios 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n然后在业务代码中即可注入使用。\n\n\n\n### 使用默认 Axios 实例\n\n\n接口和 [axios](https://github.com/axios/axios) 一致。\n\n\n```typescript\naxios.request(config)\naxios.get(url[, config])\naxios.delete(url[, config])\naxios.head(url[, config])\naxios.options(url[, config])\naxios.post(url[, data[, config]])\naxios.put(url[, data[, config]])\naxios.patch(url[, data[, config]])\naxios.postForm(url[, data[, config]])\naxios.putForm(url[, data[, config]])\naxios.patchForm(url[, data[, config]])\n```\n\n\n使用示例：\n```typescript\nimport { HttpService } from '@midwayjs/axios';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  httpService: HttpService;\n\n  async invoke() {\n  \tconst url = 'https://midwayjs.org/resource/101010100.json';\n    const result = await this.httpService.get(url);\n    // TODO result\n  }\n}\n```\n\n\n\n### 配置默认 Axios 实例\n\n\nHttpService 实例等价于 `axios.create` ，所以可以有一些配置参数，这些参数了 axios 本身的参数相同，我们可以在 `src/config.default.ts` 中配置它。\n\n\n比如：\n```typescript\nexport default {\n  // ...\n  axios: {\n    default: {\n      // 所有实例复用的配置\n    },\n    clients: {\n      // 默认实例的配置\n      default: {\n        baseURL: 'https://api.example.com',\n        // `headers` are custom headers to be sent\n        headers: {\n          'X-Requested-With': 'XMLHttpRequest'\n        },\n        timeout: 1000, // default is `0` (no timeout)\n\n        // `withCredentials` indicates whether or not cross-site Access-Control requests\n        // should be made using credentials\n        withCredentials: false, // default\n      },\n    }\n  }\n}\n```\n更多的参数可以参考 [axios global config](https://github.com/axios/axios#config-defaults)。\n\n\n\n### 创建不同实例\n\n和其他的服务多实例相同，配置不同的 key 即可。\n\n```typescript\nexport default {\n  // ...\n  axios: {\n    default: {\n      // 所有实例复用的配置\n    },\n    clients: {\n      default: {\n        // 默认实例\n      },\n      customAxios: {\n        // 自定义实例\n      }\n    }\n  }\n}\n```\n\n使用方式如下：\n\n```typescript\nimport { HttpServiceFactory, HttpService } from '@midwayjs/axios';\nimport { InjectClient } from '@midwayjs/core';\n\n@Provide()\nexport class UserService {\n\n  @InjectClient(HttpServiceFactory, 'customAxios')\n  customAxios: HttpService;\n\n  async invoke() {\n  \tconst url = 'https://midwayjs.org/resource/101010100.json';\n    const result = await this.customAxios.get(url);\n    // TODO result\n  }\n}\n```\n\n\n\n### 配置全局拦截器\n\n如果使用的是默认的 Axios 实例，可以如下配置。\n\n```javascript\nimport { Configuration, IMidwayContainer } from '@midwayjs/core';\nimport * as axios from '@midwayjs/axios';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    axios\t\t// 导入 axios 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n\n  async onReady(container: IMidwayContainer) {\n  \tconst httpService = await container.getAsync(axios.HttpService);\n    httpService.interceptors.request.use(\n      config => {\n        // Do something before request is sent\n        return config;\n      },\n      error => {\n        // Do something with request error\n        return Promise.reject(error);\n      }\n    );\n  }\n}\n```\n\n如果要给其他实例配置，可以参考下面的代码。\n\n```typescript\nimport { Configuration, IMidwayContainer } from '@midwayjs/core';\nimport * as axios from '@midwayjs/axios';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    axios\t\t// 导入 axios 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n\n  async onReady(container: IMidwayContainer) {\n  \tconst httpServiceFactory = await container.getAsync(axios.HttpServiceFactory);\n    const customAxios = httpServiceFactory.get('customAxios');\n    customAxios.interceptors.request.use(\n      config => {\n        //...\n      },\n      error => {\n        //...\n      }\n    );\n  }\n}\n```\n\n### 扩展 AxiosRequestConfig 类型\n\n当前 Axios 库中 [issue](https://github.com/axios/axios/issues?q=is%3Aissue%20state%3Aopen%20AxiosRequestConfig) 有不少扩展 `AxiosRequestConfig` 类型的需求，但是 axios 本身并不支持扩展这个类型，其中的泛型仅限于设置 data 属性的类型。\n\nMidway 中提供了 `AxiosRequestConfig` 的扩展，可以在组件层面增加新的属性。\n\n```typescript\n// interface.ts\nimport '@midwayjs/axios';\n\ndeclare module '@midwayjs/axios/dist/interface' {\n  interface AxiosRequestConfig {\n    retryDelay?: number;\n    retry?: number;\n  }\n}\n```\n\n\n### 直接使用 Axios\n\n`@midayjs/axios`导出了原始的`axios`实例，在非应用环境中可以直接使用。\n\n```typescript\nimport { Axios } from '@midwayjs/axios';\nimport { ReadStream, createWriteStream } from 'fs';\nimport { finished } from 'stream/promises';\n\nasync function download(url: string, filename: string) {\n  const writer = await createWriteStream(filename);\n  const res = Axios.get<ReadStream>(url, {\n    responseType: 'stream',\n  });\n  res.data.pipe(writer);\n  await finished(writer);\n  return res;\n}\n```\n","isInternal":false,"tokens":2580,"sizeBytes":9413},{"name":"bull.md","path":"site/docs/extensions/bull.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/bull.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# 任务队列\n\n队列是一种强大的设计模式，可帮助您应对常见的应用程序扩展和性能挑战。队列可以帮助您解决的一些问题。\n\n示例如下：\n\n-  平滑处理峰值。可以在任意时间启动资源密集型任务，然后将这些任务添加到队列中，而不是同步执行。让任务进程以受控方式从队列中提取任务。也可以轻松添加新的队列消费者以扩展后端任务处理。\n- 分解可能会阻塞 Node.js 事件循环的单一任务。比如用户请求需要像音频转码这样的 CPU 密集型工作，就可以将此任务委托给其他进程，从而释放面向用户的进程以保持响应。\n- 提供跨各种服务的可靠通信渠道。例如，您可以在一个进程或服务中排队任务（作业），并在另一个进程或服务中使用它们。在任何流程或服务的作业生命周期中完成、错误或其他状态更改时，您都可以收到通知（通过监听状态事件）。当队列生产者或消费者失败时，它们的状态被保留，并且当节点重新启动时任务处理可以自动重新启动。\n\nMidway 提供了 @midwayjs/bull 包作为 [Bull](https://github.com/OptimalBits/bull) 之上的抽象/包装器，[Bull](https://github.com/OptimalBits/bull) 是一种流行的、受良好支持的、高性能的基于 Node.js 的队列系统实现。该软件包可以轻松地将 Bull Queues 以友好的方式集成到您的应用程序中。\n\nBull 使用 Redis 来保存作业数据，在使用 Redis 时，Queue 架构是完全分布式，和平台无关。例如，您可以在一个（或多个）节点（进程）中运行一些 Queue 生产者、消费者，而在其他节点上的运行其他生产者和消费者。\n\n本章介绍 @midwayjs/bull 包。我们还建议阅读 [Bull 文档](https://github.com/OptimalBits/bull/blob/master/REFERENCE.md) 以了解更多背景和具体实施细节。\n\n:::tip\n\n- 1、从 v3.6.0 开始，原有任务调度 `@midwayjs/task` 模块废弃，如果查询历史文档，请参考 [这里](../legacy/task)。\n- 2、bull 是一个分布式任务管理系统，必须依赖 redis\n\n:::\n\n\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n\n\n## 安装组件\n\n```bash\n$ npm i @midwayjs/bull@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/bull\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 使用组件\n\n将 bull 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as bull from '@midwayjs/bull';\n\n@Configuration({\n  imports: [\n    // ...\n    bull\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n\n\n## 一些概念\n\nBull 将整个队列分为三个部分\n\n- 1、Queue 队列，管理任务\n- 2、Job，每个任务对象，可以对任务进行启停控制\n- 3、Processor，任务处理，实际的逻辑执行部分\n\n\n\n## 基础配置\n\nbull 是一个分布式任务管理器，强依赖于 redis，在 `config.default.ts` 文件中配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  bull: {\n    // 默认的队列配置\n    defaultQueueOptions: {\n      redis: `redis://127.0.0.1:32768`,\n    }\n  },\n}\n```\n\n有账号密码情况：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  bull: {\n    defaultQueueOptions: {\n      redis: {\n        port: 6379,\n        host: '127.0.0.1',\n        password: 'foobared',\n      },\n    }\n  },\n}\n```\n\n所有的队列都会复用该配置。\n\n\n\n## 编写任务处理器\n\n使用 `@Processor` 装饰器装饰一个类，用于快速定义一个任务处理器（这里我们不使用 Job，避免后续的歧义）。\n\n`@Processor` 装饰器需要传递一个 Queue （队列）的名字，在框架启动时，如果没有名为 `test` 的队列，则会自动创建。\n\n比如，我们在 `src/queue/test.queue.ts` 文件中编写如下代码。\n\n```typescript\n// src/queue/test.queue.ts\nimport { Processor, IProcessor } from '@midwayjs/bull';\n\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  async execute() {\n    // ...\n  }\n}\n```\n\n在启动时，框架会自动查找并初始化上述处理器代码，同时自动创建一个名为 `test` 的 Queue。\n\n\n\n\n\n## 执行任务\n\n当定义完 Processor 之后，由于并未指定 Processor 如何执行，我们还需要手动执行它。\n\n通过获取对应的队列，我们可以很方便的来执行任务。\n\n\n\n### 手动执行任务\n\n比如，我们可以在项目启动后执行。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bull from '@midwayjs/bull';\n\n@Configuration({\n  imports: [\n    // ...\n    bull\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullFramework: bull.Framework;\n\n  //...\n\n  async onServerReady() {\n    // 获取 Processor 相关的队列\n    const testQueue = this.bullFramework.getQueue('test');\n    // 立即添加这个任务\n    await testQueue?.addJobToQueue();\n  }\n}\n```\n\n\n\n### 增加执行参数\n\n我们也可以在执行时，附加一些默认参数。\n\n```typescript\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  async execute(params) {\n    // params.aaa => 1\n  }\n}\n\n\n// invoke\nconst testQueue = this.bullFramework.getQueue('test');\n// 立即添加这个任务\nawait testQueue?.addJobToQueue({\n  aaa: 1,\n  bbb: 2,\n});\n```\n\n\n\n### 任务状态和管理\n\n执行 `addJobToQueue` 后，我们可以获取到一个 `Job` 对象。\n\n```typescript\n// invoke\nconst testQueue = this.bullFramework.getQueue('test');\nconst job = await testQueue?.addJobToQueue();\n```\n\n通过这个 Job 对象，我们可以做进度管理。\n\n```typescript\n// 更新进度\nawait job.progress(60);\n// 获取进度\nconst progress = await job.process();\n// => 60\n```\n\n获取任务状态。\n\n```typescript\nconst state = await job.getState();\n// state => 'delayed' 延迟状态\n// state => 'completed' 完成状态\n```\n\n更多的 Job API，请查看 [文档](https://github.com/OptimalBits/bull/blob/develop/REFERENCE.md)。\n\n\n\n### 延迟执行\n\n执行任务时，也有一些额外的选项。\n\n比如，延迟 1s 执行。\n\n```typescript\nconst testQueue = this.bullFramework.getQueue('test');\n// 立即添加这个任务\nawait testQueue?.addJobToQueue({}, { delay: 1000 });\n```\n\n\n\n### 中间件和错误处理\n\nBull 组件包含可以独立启动的 Framework，有着自己的 App 对象和 Context 结构。\n\n我们可以对 bull 的 App 配置独立的中间件和错误过滤器。\n\n```typescript\n@Configuration({\n  imports: [\n    // ...\n    bull\n  ]\n})\nexport class MainConfiguration {\n\n  @App('bull')\n  bullApp: bull.Application;\n\n  //...\n\n  async onReady() {\n    this.bullApp.useMiddleare( /*中间件*/);\n    this.bullApp.useFilter( /*过滤器*/);\n  }\n}\n```\n\n\n\n### 上下文\n\n任务处理器执行是在请求作用域中，其有着特殊的 Context 对象结构。\n\n```typescript\nexport interface Context extends IMidwayContext {\n  jobId: JobId;\n  job: Job,\n  from: new (...args) => IProcessor;\n}\n```\n\n我们可以直接从 ctx 中访问当前的 Job 对象。\n\n```typescript\n// src/queue/test.queue.ts\nimport { Processor, IProcessor, Context } from '@midwayjs/bull';\n\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n\n  @Inject()\n  ctx: Context;\n\n  async execute() {\n    // ctx.jobId => xxxx\n  }\n}\n```\n\n\n\n### 更多任务选项\n\n除了上面的 delay 之外，还有更多的执行选项。\n\n| 选项             | 类型                  | 描述                                                         |\n| ---------------- | --------------------- | ------------------------------------------------------------ |\n| priority         | number                | 可选的优先级值。范围从 1（最高优先级）到 MAX_INT（最低优先级）。请注意，使用优先级对性能有轻微影响，因此请谨慎使用。 |\n| delay            | number                | 等待可以处理此作业的时间量（毫秒）。请注意，为了获得准确的延迟，服务器和客户端都应该同步它们的时钟。 |\n| attempts         | number                | 在任务完成之前尝试尝试的总次数。                             |\n| repeat           | RepeatOpts            | 根据 cron 规范的重复任务配置，更多可以查看 [RepeatOpts](https://github.com/OptimalBits/bull/blob/master/REFERENCE.md#queueadd)，以及下面的重复任务介绍。 |\n| backoff          | number \\| BackoffOpts | 任务失败时自动重试的回退设置。请参阅 [BackoffOpts](https://github.com/OptimalBits/bull/blob/master/REFERENCE.md#queueadd)。 |\n| lifo             | boolean               | 如果为 true，则将任务添加到队列的右端而不是左端（默认为 false）。 |\n| timeout          | number                | 任务因超时错误而失败的毫秒数。                               |\n| jobId            | number \\| string      | 覆盖任务 id - 默认情况下，任务 id 是唯一整数，但您可以使用此设置覆盖它。如果您使用此选项，则由您来确保 jobId 是唯一的。如果您尝试添加一个 id 已经存在的任务，它将不会被添加。 |\n| removeOnComplete | boolean \\| number     | 如果为 true，则在成功完成后删除任务。如果设置数字，则为指定要保留的任务数量。默认行为是任务信息保留在已完成列表中。 |\n| removeOnFail     | boolean \\| number     | 如果为 true，则在所有尝试后都失败时删除任务。如果设置数字，指定要保留的任务数量。默认行为是将任务信息保留在失败列表中。 |\n| stackTraceLimit  | number                | 限制将在堆栈跟踪中记录的堆栈跟踪行的数量。                   |\n\n\n\n## 重复执行的任务\n\n除了手动执行的方式，我们也可以通过 `@Processor` 装饰器的参数，快速配置任务的重复执行。\n\n```typescript\nimport { Processor, IProcessor } from '@midwayjs/bull';\nimport { FORMAT } from '@midwayjs/core';\n\n@Processor('test', {\n  repeat: {\n    cron: FORMAT.CRONTAB.EVERY_PER_5_SECOND\n  }\n})\nexport class TestProcessor implements IProcessor {\n  @Inject()\n  logger;\n\n  async execute() {\n    // ...\n  }\n}\n```\n\n\n\n## 常用 Cron 表达式\n\n关于 Cron 表达式，格式如下。\n\n```typescript\n*    *    *    *    *    *\n┬    ┬    ┬    ┬    ┬    ┬\n│    │    │    │    │    |\n│    │    │    │    │    └ day of week (0 - 7) (0 or 7 is Sun)\n│    │    │    │    └───── month (1 - 12)\n│    │    │    └────────── day of month (1 - 31)\n│    │    └─────────────── hour (0 - 23)\n│    └──────────────────── minute (0 - 59)\n└───────────────────────── second (0 - 59, optional)\n```\n\n\n\n常见表达式：\n\n- 每隔5秒执行一次：`*/5 * * * * *`\n- 每隔1分钟执行一次：`0 */1 * * * *`\n- 每小时的20分执行一次：`0 20 * * * *`\n- 每天 0 点执行一次：`0 0 0 * * *`\n- 每天的两点35分执行一次：`0 35 2 * * *`\n\n可以使用 [在线工具](https://cron.qqe2.com/) 执行确认下一次执行的时间。\n\nMidway 在框架侧提供了一些常用的表达式，放在 `@midwayjs/core` 中供大家使用。\n\n```typescript\nimport { FORMAT } from '@midwayjs/core';\n\n// 每分钟执行的 cron 表达式\nFORMAT.CRONTAB.EVERY_MINUTE\n```\n\n\n\n内置的还有一些其他的表达式。\n\n| 表达式                         | 对应时间        |\n| ------------------------------ | --------------- |\n| CRONTAB.EVERY_SECOND           | 每秒钟          |\n| CRONTAB.EVERY_MINUTE           | 每分钟          |\n| CRONTAB.EVERY_HOUR             | 每小时整点      |\n| CRONTAB.EVERY_DAY              | 每天 0 点       |\n| CRONTAB.EVERY_DAY_ZERO_FIFTEEN | 每天 0 点 15 分 |\n| CRONTAB.EVERY_DAY_ONE_FIFTEEN  | 每天 1 点 15 分 |\n| CRONTAB.EVERY_PER_5_SECOND     | 每隔 5 秒       |\n| CRONTAB.EVERY_PER_10_SECOND    | 每隔 10 秒      |\n| CRONTAB.EVERY_PER_30_SECOND    | 每隔 30 秒      |\n| CRONTAB.EVERY_PER_5_MINUTE     | 每隔 5 分钟     |\n| CRONTAB.EVERY_PER_10_MINUTE    | 每隔 10 分钟    |\n| CRONTAB.EVERY_PER_30_MINUTE    | 每隔 30 分钟    |\n\n\n\n## 高级配置\n\n\n\n### 清理之前的任务\n\n在默认情况下，框架会自动清理前一次未调度的 **重复执行任务**，保持每一次的重复执行的任务队列为最新。如果在某些环境不需要清理，可以单独关闭。\n\n比如你不需要清理重复：\n\n```typescript\n// src/config/config.prod.ts\nexport default {\n  // ...\n  bull: {\n    clearRepeatJobWhenStart: false,\n  },\n}\n```\n\n:::tip\n\n如果不清理，如果前一次队列为 10s 执行，现在修改为 20s 执行，则两个定时都会存储在 Redis 中，导致代码重复执行。\n\n在日常的开发中，如果不清理，很容易出现代码重复执行这个问题。但是在集群部署的场景，多台服务器轮流重启的情况下，可能会导致定时任务被意外清理，请评估开关的时机。\n\n:::\n\n\n\n也可以在启动时手动清理所有任务。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, App, Inject } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport { join } from 'path';\nimport * as bull from '@midwayjs/bull';\n\n@Configuration({\n  imports: [koa, bull],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: koa.Application;\n\n  @Inject()\n  bullFramework: bull.Framework;\n\n  async onReady() {\n    // 在这个阶段，装饰器队列还未创建，使用 API 提前手动创建队列，装饰器会复用同名队列\n    const queue = this.bullFramework.createQueue('user');\n    // 通过队列手动执行清理\n    await queue.obliterate({ force: true });\n  }\n}\n```\n\n\n\n\n\n### 清理任务历史记录\n\n当开启 Redis 后，默认情况下，bull 会记录所有的成功和失败的任务 key，这可能会导致 redis 的 key 暴涨，我们可以配置成功或者失败后清理的选项。\n\n默认情况下\n\n- 成功时保留的任务记录为 3 条\n- 失败保留的任务记录为 10 条\n\n也可以通过参数进行配置。\n\n比如在装饰器配置。\n\n```typescript\nimport { FORMAT } from '@midwayjs/core';\nimport { IProcessor, Processor } from '@midwayjs/bull';\n\n@Processor('user', {\n  repeat: {\n    cron: FORMAT.CRONTAB.EVERY_MINUTE,\n  },\n  removeOnComplete: 3,\t// 成功后移除任务记录，最多保留最近 3 条记录\n  removeOnFail: 10,\t    // 失败后移除任务记录\n})\nexport class UserService implements IProcessor {\n  execute(data: any) {\n    // ...\n  }\n}\n\n```\n\n也可以在全局 config 中配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  bull: {\n    defaultQueueOptions: {\n      // 默认的任务配置\n      defaultJobOptions: {\n        // 保留 10 条记录\n        removeOnComplete: 10,\n      },\n    },\n  },\n}\n```\n\n\n\n\n\n### Redis 集群\n\n可以使用 bull 提供的 `createClient` 方式来接入自定义的 redis 实例，这样你可以接入 Redis 集群。\n\n比如：\n\n```typescript\n// src/config/config.default\nimport Redis from 'ioredis';\n\nconst clusterOptions = {\n  enableReadyCheck: false,  // 一定要是false\n  retryDelayOnClusterDown: 300,\n  retryDelayOnFailover: 1000,\n  retryDelayOnTryAgain: 3000,\n  slotsRefreshTimeout: 10000,\n  maxRetriesPerRequest: null  // 一定要是null\n}\n\nconst redisClientInstance = new Redis.Cluster([\n  {\n    port: 7000,\n    host: '127.0.0.1'\n  },\n  {\n    port: 7002,\n    host: '127.0.0.1'\n  },\n], clusterOptions);\n\nexport default {\n  bull: {\n    defaultQueueOptions: {\n      createClient: (type, opts) => {\n        return redisClientInstance;\n      },\n      // 这些任务存储的 key，都是相同开头，以便区分用户原有 redis 里面的配置\n      prefix: '{midway-bull}',\n    },\n  }\n}\n```\n\n\n\n## 队列管理\n\n队列是廉价的，每个 Job 都会绑定一个队列，在一些情况下，我们也可以手动对队列进行管理操作。\n\n\n\n### 手动创建队列\n\n除了使用 `@Processor` 简单定义队列，我们还可以使用 API 进行创建。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bull from '@midwayjs/bull';\n\n@Configuration({\n  imports: [\n    // ...\n    bull\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullFramework: bull.Framework;\n\n  async onReady() {\n    const testQueue = this.bullFramework.createQueue('test', {\n      redis: {\n        port: 6379,\n        host: '127.0.0.1',\n        password: 'foobared',\n      },\n      prefix: '{midway-bull}',\n    });\n\n    // ...\n  }\n}\n```\n\n通过 `createQueue` 手动创建队列后，队列依旧会自动保存。如果在启动时 `@Processor` 使用了该队列名，则会自动使用已经创建好的队列。\n\n比如：\n\n```typescript\n// 会自动使用上面手动创建的同名队列\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  async execute(params) {\n  }\n}\n```\n\n\n\n### 获取队列\n\n我们可以简单的根据队列名获取队列。\n\n```typescript\n const testQueue = bullFramework.getQueue('test');\n```\n\n也可以通过装饰器来获取。\n\n```typescript\nimport { InjectQueue, BullQueue } from '@midwayjs/bull';\nimport { Provide } from '@midwayjs/core';\n\n@Provide()\nexport class UserService {\n  @InjectQueue('test')\n  testQueue: BullQueue;\n\n  async invoke() {\n    await this.testQueue.pause();\n    // ...\n  }\n}\n```\n\n\n\n### 队列常用操作\n\n暂停队列。\n\n```typescript\nawait testQueue.pause();\n```\n\n继续队列。\n\n```typescript\nawait testQueue.resume();\n```\n\n队列事件。\n\n```typescript\n// Local events pass the job instance...\ntestQueue.on('progress', function (job, progress) {\n  console.log(`Job ${job.id} is ${progress * 100}% ready!`);\n});\n\ntestQueue.on('completed', function (job, result) {\n  console.log(`Job ${job.id} completed! Result: ${result}`);\n  job.remove();\n});\n```\n\n完整队列 API 请参考 [这里](https://github.com/OptimalBits/bull/blob/develop/REFERENCE.md)。\n\n\n\n## 组件日志\n\n组件有着自己的日志，默认会将 `ctx.logger` 记录在 `midway-bull.log` 中。\n\n我们可以单独配置这个 logger 对象。\n\n```typescript\nexport default {\n  midwayLogger: {\n    clients: {\n      // ...\n      bullLogger: {\n        fileLogName: 'midway-bull.log',\n      },\n    },\n  },\n}\n```\n\n这个日志的输出格式，我们也可以单独配置。\n\n```typescript\nexport default {\n  bull: {\n    // ...\n    contextLoggerFormat: info => {\n      const { jobId, from } = info.ctx;\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} [${jobId} ${from.name}] ${info.message}`;\n    },\n  }\n}\n```\n\n\n\n## 关于 Redis 版本\n\n请尽可能选择最新的版本（ >=5 ），目前在低版本 redis 上有发现定时任务创建失败的问题。\n\n\n\n## Bull UI\n\n在分布式场景中，我们可以资利用 Bull UI 来简化管理。\n\n和 bull 组件类似，需要独立安装和启用。\n\n```bash\n$ npm i @midwayjs/bull-board@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/bull-board\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n将 bull-board 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as bull from '@midwayjs/bull';\nimport * as bullBoard from '@midwayjs/bull-board';\n\n@Configuration({\n  imports: [\n    // ...\n    bull,\n    bullBoard,\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n默认的访问路径为：`http://127.1:7001/ui`。\n\n效果如下：\n\n![](https://img.alicdn.com/imgextra/i2/O1CN01j4wEFb1UacPxA06gs_!!6000000002534-2-tps-1932-1136.png)\n\n可以通过配置进行基础路径的修改。\n\n```typescript\n// src/config/config.prod.ts\nexport default {\n  // ...\n  bullBoard: {\n    basePath: '/ui',\n  },\n}\n```\n\n此外，组件提供了 `BullBoardManager` ，可以添加动态创建的队列。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bull from '@midwayjs/bull';\nimport * as bullBoard from '@midwayjs/bull-board';\n\n@Configuration({\n  imports: [\n    // ...\n    bull,\n    bullBoard\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullFramework: bull.Framework;\n  \n  @Inject()\n  bullBoardManager: bullBoard.BullBoardManager;\n\n  async onServerReady() {\n    const testQueue = this.bullFramework.createQueue('test', {\n      // ...\n    });\n\n    this.bullBoardManager.addQueue(new bullBoard.BullAdapter(testQueue);\n  }\n}\n```\n\n\n\n\n\n## 常见问题\n\n### 1、EVALSHA 错误\n\n![image.png](https://img.alicdn.com/imgextra/i4/O1CN01KfjCKT1yypmNPDkIL_!!6000000006648-2-tps-3540-102.png)\n\n这个问题基本明确，问题会出现在 redis 的集群版本上。\n\n原因是 redis 会对 key 做 hash 来确定存储的 slot，集群下这一步 @midwayjs/bull 的 key 命中了不同的 slot。\n\n解决方案： task 里的 prefix 配置用 {} 包括，强制 redis 只计算 {} 里的hash，例如 `prefix: '{midway-task}'`。\n\n### 2、EVAL inside MULTI is not allowed 错误\n\n表现为 `queue.createBulk()`、`job.moveToFailed()` 等任务队列 API 调用无效，并出现下面的错误。\n\n```\nReplyError: EXECABORT Transaction discarded because of previous errors.\n    at parseError (<project_dir>/node_modules/redis-parser/lib/parser.js:179:12)\n    at parseType (<project_dir>/node_modules/redis-parser/lib/parser.js:302:14) {\n  command: { name: 'exec', args: [] },\n  previousErrors: [\n    ReplyError: ERR 'EVAL' inside MULTI is not allowed\n        at parseError (<project_dir>/node_modules/redis-parser/lib/parser.js:179:12)\n        at parseType (<project_dir>/node_modules/redis-parser/lib/parser.js:302:14) {\n      command: [Object]\n    }\n  ]\n}\n```\n\n:::tip\n\n常出现于使用阿里云 Redis 服务。\n\n:::\n\n由于这些 API 依赖的 Redis Lua 脚本中使用了 EVAL 或者 EVALSHA，阿里云 Redis 使用代理模式连接时，会对 Lua 脚本调用做额外限制，包括 [不允许在 MULTI 事务中执行 EVAL 命令](https://help.aliyun.com/zh/redis/support/usage-of-lua-scripts?#section-8f7-qgv-dlv)，文档中还提到可以通过参数配置 script_check_enable 关闭这一校验，但是验证无效。\n\n解决方案：\n\n* 1、在阿里云控制台操作开启直连地址，将服务切换到直连模式\n* 2、客户端切换成集群模式，参考上述「Redis 集群」章节，切换配置方式\n","isInternal":false,"tokens":6666,"sizeBytes":22706},{"name":"bullmq.md","path":"site/docs/extensions/bullmq.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/bullmq.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# 任务队列\n\n队列是一种强大的设计模式，可帮助您应对常见的应用程序扩展和性能挑战。队列可以帮助您解决的一些问题：\n\n- 平滑处理峰值。可以在任意时间启动资源密集型任务，然后将这些任务添加到队列中，而不是同步执行。让任务进程以受控方式从队列中提取任务。也可以轻松添加新的队列消费者以扩展后端任务处理。\n- 分解可能会阻塞 Node.js 事件循环的单一任务。比如用户请求需要像音频转码这样的 CPU 密集型工作，就可以将此任务委托给其他进程，从而释放面向用户的进程以保持响应。\n- 提供跨各种服务的可靠通信渠道。例如，您可以在一个进程或服务中排队任务（作业），并在另一个进程或服务中使用它们。在任何流程或服务的作业生命周期中完成、错误或其他状态更改时，您都可以收到通知（通过监听状态事件）。当队列生产者或消费者失败时，它们的状态被保留，并且当节点重新启动时任务处理可以自动重新启动。\n\nMidway 提供了 `@midwayjs/bullmq` 包作为 [BullMQ](https://github.com/taskforcesh/bullmq) 之上的抽象/包装器。BullMQ 是 Bull 的下一代实现，提供了更好的性能和更多的功能。该软件包可以轻松地将 BullMQ 以友好的方式集成到您的应用程序中。\n\nBullMQ 使用 Redis 来保存作业数据，在使用 Redis 时，Queue 架构是完全分布式，和平台无关。例如，您可以在一个（或多个）节点（进程）中运行一些 Queue 生产者、消费者，而在其他节点上的运行其他生产者和消费者。\n\n:::tip\nBullMQ 是一个分布式任务管理系统，必须依赖 redis\n:::\n\n:::caution\n由于 BullMQ 是 Bull 的升级版，从 v3.20 开始，将由 BullMQ 替代 Bull 组件，如需使用 Bull 组件，请参考 [Bull 文档](./bull)\n:::\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n\n## 安装组件\n\n```bash\n$ npm i @midwayjs/bullmq@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/bullmq\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n## 使用组件\n\n将 bullmq 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as bullmq from '@midwayjs/bullmq';\n\n@Configuration({\n  imports: [\n    // ...\n    bullmq\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n## 一些概念\n\nBullMQ 将整个队列分为以下几个部分：\n\n- Queue：队列，管理任务\n- Job：每个任务对象，可以对任务进行启停控制\n- Worker：任务处理器，实际的逻辑执行部分\n- QueueEvents：队列事件，用于监听任务状态变化\n- FlowProducer：任务流生产者，用于创建任务依赖关系\n\n## 基础配置\n\nbullmq 是一个分布式任务管理器，强依赖于 redis，在 `config.default.ts` 文件中配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  bullmq: {\n    defaultConnection: {\n      host: '127.0.0.1',\n      port: 6379,\n    },\n    // 可选，队列前缀\n    defaultPrefix: '{midway-bullmq}',\n  },\n}\n```\n\n有账号密码情况：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  bullmq: {\n    defaultConnection: {\n      port: 6379,\n      host: '127.0.0.1',\n      password: 'foobared',\n    }\n  },\n}\n```\n\n所有的队列、任务处理器、队列事件、任务流都会复用该配置。\n\n## 编写任务处理器\n\n使用 `@Processor` 装饰器装饰一个类，用于快速定义一个任务处理器。\n\n`@Processor` 装饰器需要传递一个 Queue（队列）的名字，在框架启动时，如果没有名为 `test` 的队列，则会自动创建。\n\n比如，我们在 `src/processor/test.processor.ts` 文件中编写如下代码。\n\n```typescript\nimport { Processor, IProcessor } from '@midwayjs/bullmq';\n\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  async execute(data: any) {\n    // 处理任务逻辑\n    console.log('processing job:', data);\n  }\n}\n```\n\n## 执行任务\n\n当定义完 Processor 之后，由于并未指定 Processor 如何执行，我们还需要手动执行它。\n\n### 手动执行任务\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bullmq from '@midwayjs/bullmq';\n\n@Configuration({\n  imports: [\n    bullmq\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullmqFramework: bullmq.Framework;\n\n  async onServerReady() {\n    // 获取 Processor 相关的队列\n    const testQueue = this.bullmqFramework.getQueue('test');\n    // 立即添加这个任务\n    await testQueue?.addJobToQueue();\n  }\n}\n```\n\n### 增加执行参数\n\n我们也可以在执行时，附加一些参数。\n\n```typescript\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  async execute(params) {\n    // params.name => 'harry'\n  }\n}\n\n// invoke\nconst testQueue = this.bullmqFramework.getQueue('test');\nawait testQueue?.addJobToQueue({\n  aaa: 1,\n  bbb: 2,\n});\n```\n\n### 任务状态和管理\n\n执行 `addJobToQueue` 后，我们可以获取到一个 `Job` 对象。\n\n```typescript\nconst testQueue = this.bullmqFramework.getQueue('test');\nconst job = await testQueue?.addJobToQueue();\n\n// 更新进度\nawait job.updateProgress(60);\n// 获取进度\nconst progress = await job.progress;\n// => 60\n\n// 获取任务状态\nconst state = await job.getState();\n// state => 'delayed' 延迟状态\n// state => 'completed' 完成状态\n// state => 'failed' 失败状态\n```\n\n### 延迟执行\n\n执行任务时，也有一些额外的选项。\n\n比如，延迟 1s 执行。\n\n```typescript\nconst testQueue = this.bullmqFramework.getQueue('test');\nawait testQueue?.addJobToQueue({}, { delay: 1000 });\n```\n\n### 任务重试\n\nBullMQ 支持任务失败重试机制。\n\n```typescript\nconst testQueue = this.bullmqFramework.getQueue('test');\nawait testQueue?.addJobToQueue({}, {\n  attempts: 3,  // 最多重试 3 次\n  backoff: {    // 重试策略\n    type: 'exponential',  // 指数退避\n    delay: 1000          // 初始延迟 1 秒\n  }\n});\n```\n\n### 任务优先级\n\n可以为任务设置优先级，优先级高的任务会优先执行。\n\n```typescript\nconst testQueue = this.bullmqFramework.getQueue('test');\n// priority 值越大优先级越高\nawait testQueue?.addJobToQueue({ priority: 1 }, { priority: 3 }); // 高优先级\nawait testQueue?.addJobToQueue({ priority: 2 }, { priority: 2 }); // 中优先级\nawait testQueue?.addJobToQueue({ priority: 3 }, { priority: 1 }); // 低优先级\n```\n\n### 中间件和错误处理\n\nBullMQ 组件包含可以独立启动的 Framework，有着自己的 App 对象和 Context 结构。\n\n我们可以对 bullmq 的 App 配置独立的中间件和错误过滤器。\n\n```typescript\n@Configuration({\n  imports: [\n    bullmq\n  ]\n})\nexport class MainConfiguration {\n\n  @App('bullmq')\n  bullmqApp: bullmq.Application;\n\n  async onReady() {\n    this.bullmqApp.useMiddleware(/*中间件*/);\n    this.bullmqApp.useFilter(/*过滤器*/);\n  }\n}\n```\n\n### 上下文\n\n任务处理器执行是在请求作用域中，其有着特殊的 Context 对象结构。\n\n```typescript\nexport interface Context extends IMidwayContext {\n  jobId: string;\n  job: Job;\n  token?: string;\n  from: new (...args) => IProcessor;\n}\n```\n\n我们可以直接从 ctx 中访问当前的 Job 对象。\n\n```typescript\nimport { Processor, IProcessor, Context } from '@midwayjs/bullmq';\n\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  @Inject()\n  ctx: Context;\n\n  async execute(data: any) {\n    // ctx.jobId => 当前任务ID\n    // ctx.job => 当前任务对象\n  }\n}\n```\n\n## 重复执行的任务\n\n除了手动执行的方式，我们也可以通过 `@Processor` 装饰器的参数，快速配置任务的重复执行。\n\n```typescript\nimport { Processor, IProcessor } from '@midwayjs/bullmq';\nimport { FORMAT } from '@midwayjs/core';\n\n@Processor('test', {\n  repeat: {\n    pattern: FORMAT.CRONTAB.EVERY_PER_5_SECOND\n  }\n})\nexport class TestProcessor implements IProcessor {\n  async execute() {\n    // 每 5 秒执行一次\n  }\n}\n```\n\n## 高级功能\n\n### 任务流（Flow Producer）\n\nBullMQ 支持创建任务依赖关系，形成任务流。\n\n```typescript\nconst flowProducer = bullmqFramework.createFlowProducer({}, 'test-flow');\n\n// 创建任务流\nawait flowProducer.add({\n  name: 'flow-test',\n  queueName: 'flow-queue-1',\n  data: { value: 1 },\n  children: [\n    {\n      name: 'child-job',\n      queueName: 'flow-queue-2',\n      data: { value: 2 }\n    }\n  ]\n});\n```\n\n### 队列事件\n\nBullMQ 提供了丰富的事件系统，可以监听任务的各种状态变化。\n\n```typescript\nconst eventQueue = bullmqFramework.createQueue('event-queue');\nconst queueEvents = eventQueue.createQueueEvents();\n\n// 监听任务完成事件\nqueueEvents.on('completed', ({ jobId }) => {\n  console.log(`Job ${jobId} completed!`);\n});\n\n// 监听任务失败事件\nqueueEvents.on('failed', ({ jobId, failedReason }) => {\n  console.log(`Job ${jobId} failed: ${failedReason}`);\n});\n```\n\n### 清理任务历史记录\n\n当开启 Redis 后，默认情况下，bullmq 会记录所有的成功和失败的任务 key，这可能会导致 redis 的 key 暴涨，我们可以配置成功或者失败后清理的选项。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  bullmq: {\n    defaultQueueOptions: {\n      defaultJobOptions: {\n        removeOnComplete: 3,  // 成功后只保留最近 3 条记录\n        removeOnFail: 10,     // 失败后只保留最近 10 条记录\n      }\n    }\n  }\n}\n```\n\n### Redis 集群\n\nbullmq 可以指定 connection 实例，你可以将自己创建的 Redis 实例配置到 `defaultConnection` 中，这样就可以接入 Redis 集群。\n\n```typescript\n// src/config/config.default.ts\nimport Redis from 'ioredis';\n\nconst clusterOptions = {\n  enableReadyCheck: false,\n  retryDelayOnClusterDown: 300,\n  retryDelayOnFailover: 1000,\n  retryDelayOnTryAgain: 3000,\n  slotsRefreshTimeout: 10000,\n  maxRetriesPerRequest: null\n}\n\nconst redisClientInstance = new Redis.Cluster([\n  {\n    port: 7000,\n    host: '127.0.0.1'\n  },\n  {\n    port: 7002,\n    host: '127.0.0.1'\n  },\n], clusterOptions);\n\nexport default {\n  bullmq: {\n    defaultConnection: redisClientInstance,\n    defaultPrefix: '{midway-bullmq}',\n  }\n}\n```\n\n### 获取任务处理器\n\n通过内置的方法，我们可以获取到原始的任务处理器（Worker）。\n\n可以启动任意多个 Worker 监听同一个队列，在这种情况下 BullMQ 会把队列中的任务轮流（round-robin）分配给这些 Worker。\n\n当队列中有任务被添加时，BullMQ 会在这两个 Worker 之间自动分配任务。\n\n我们可以通过方法 `getWorker` 获取到首个监听的 Worker 对象，也可以通过方法 `getWorkers` 获取到该队列监听的所有 Worker 对象。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bullmq from '@midwayjs/bullmq';\n\n@Configuration({\n  imports: [\n    // ...\n    bullmq,\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullmqFramework: bullmq.Framework;\n\n  async onServerReady() {\n    const testWorker = this.bullmqFramework.getWorker('test');\n    const allWorkers = this.bullmqFramework.getWorkers('test');\n  }\n}\n\n```\n\n\n## 组件日志\n\n组件有着自己的日志，默认会将 `ctx.logger` 记录在 `midway-bullmq.log` 中。\n\n我们可以单独配置这个 logger 对象。\n\n```typescript\nexport default {\n  midwayLogger: {\n    clients: {\n      bullMQLogger: {\n        fileLogName: 'midway-bullmq.log',\n      },\n    },\n  },\n}\n```\n\n这个日志的输出格式，我们也可以单独配置。\n\n```typescript\nexport default {\n  bullmq: {\n    contextLoggerFormat: info => {\n      const { jobId, from } = info.ctx;\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} [${jobId} ${from.name}] ${info.message}`;\n    },\n  }\n}\n```\n\n## BullMQ 原始对象\n\n组件导出了 BullMQ 的原始对象，可以进行更多的操作。\n\n```typescript\nimport { BullMQ } from '@midwayjs/bullmq';\n```\n\n你可以通过 `BullMQ` 对象，获取到 `Queue`、`Worker`、`FlowProducer` 等对象定义。\n\n## Bull UI\n\n在分布式场景中，我们可以资利用 Bull UI 来简化管理。\n\n和 bull 组件类似，需要独立安装和启用。\n\n```bash\n$ npm i @midwayjs/bull-board@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/bull-board\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n将 bull-board 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as bullmq from '@midwayjs/bullmq';\nimport * as bullBoard from '@midwayjs/bull-board';\n\n@Configuration({\n  imports: [\n    // ...\n    bullmq,\n    bullBoard,\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n默认的访问路径为：`http://127.1:7001/ui`。\n\n效果如下：\n\n![](https://img.alicdn.com/imgextra/i2/O1CN01j4wEFb1UacPxA06gs_!!6000000002534-2-tps-1932-1136.png)\n\n可以通过配置进行基础路径的修改。\n\n```typescript\n// src/config/config.prod.ts\nexport default {\n  // ...\n  bullBoard: {\n    basePath: '/ui',\n  },\n}\n```\n\n此外，组件提供了 `BullBoardManager` ，可以添加动态创建的队列。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bullmq from '@midwayjs/bullmq';\nimport * as bullBoard from '@midwayjs/bull-board';\n\n@Configuration({\n  imports: [\n    // ...\n    bullmq,\n    bullBoard\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullmqFramework: bullmq.Framework;\n  \n  @Inject()\n  bullBoardManager: bullBoard.BullBoardManager;\n\n  async onServerReady() {\n    const testQueue = this.bullmqFramework.createQueue('test', {\n      // ...\n    });\n\n    this.bullBoardManager.addQueue(new bullBoard.BullMQAdapter(testQueue) as any);\n  }\n}\n```\n\n由于最新 bull-board 要求最低 Node.js 版本为 v20，所以 Midway v3 无法将 bull-board 升级；在 v5.23.0 版本下的 `bull-board` 存在类型定义问题，采用 `as any`  的方式绕过。\n","isInternal":false,"tokens":4216,"sizeBytes":14296},{"name":"busboy.md","path":"site/docs/extensions/busboy.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/busboy.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"import Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n# 文件上传\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的通用上传组件，支持 `file` (服务器临时文件) 和 `stream` （流）两种模式。\n\n相关信息：\n\n| web 支持情况      |      |\n| ----------------- | ---- |\n| @midwayjs/koa     | ✅    |\n| @midwayjs/faas    | 💬    |\n| @midwayjs/web     | ✅    |\n| @midwayjs/express | ✅    |\n\n:::caution\n\n💬 部分函数计算平台不支持流式请求响应，请参考对应平台能力。\n\n:::\n\n:::tip\n\n本模块自 3.17.0 起替换 upload 组件。\n\n和 upload 组件的差异为：\n\n* 1、配置的 key 从 `upload` 调整为 `busboy`\n* 2、中间件不再默认加载，手动可配置到全局或者路由\n* 3、入参定义类型调整为 `UploadStreamFileInfo`\n* 4、`fileSize` 的配置有调整\n\n:::\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/busboy@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/busboy\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n## 启用组件\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration } from '@midwayjs/core';\nimport * as busboy from '@midwayjs/busboy';\n\n@Configuration({\n  imports: [\n    // ...other components\n    busboy\n  ],\n  // ...\n})\nexport class MainConfiguration {}\n```\n\n\n\n## 配置中间件\n\n组件中提供了 `UploadMiddleware` 这个中间件，可以将其配置到全局或者特定路由，推荐配置到特定路由，提升性能。\n\n\n\n**路由中间件**\n\n```typescript\nimport { Controller, Post } from '@midwayjs/core';\nimport { UploadMiddleware } from '@midwayjs/busboy';\n\n@Controller('/')\nexport class HomeController {\n\n  @Post('/upload', { middleware: [UploadMiddleware] })\n  async upload(/*...*/) {\n    // ...\n  }\n}\n```\n\n**全局中间件**\n\n<Tabs>\n<TabItem value=\"koa\" label=\"@midwayjs/koa\">\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration } from '@midwayjs/core';\nimport * as busboy from '@midwayjs/busboy';\nimport { Application } from '@midwayjs/koa';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  @App('koa')\n  app: Application;\n  \n  async onReady() {\n    this.app.useMiddleware(busboy.UploadMiddleware);\n  }\n}\n```\n</TabItem>\n<TabItem value=\"egg\" label=\"@midwayjs/web\">\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration } from '@midwayjs/core';\nimport * as busboy from '@midwayjs/busboy';\nimport { Application } from '@midwayjs/web';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  @App('egg')\n  app: Application;\n  \n  async onReady() {\n    this.app.useMiddleware(busboy.UploadMiddleware);\n  }\n}\n```\n</TabItem>\n<TabItem value=\"express\" label=\"@midwayjs/express\">\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration } from '@midwayjs/core';\nimport * as busboy from '@midwayjs/busboy';\nimport { Application } from '@midwayjs/express';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  @App('express')\n  app: Application;\n  \n  async onReady() {\n    this.app.useMiddleware(busboy.UploadMiddleware);\n  }\n}\n```\n</TabItem>\n<TabItem value=\"faas\" label=\"@midwayjs/faas\">\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration } from '@midwayjs/core';\nimport * as busboy from '@midwayjs/busboy';\nimport { Application } from '@midwayjs/faas';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  @App('faas')\n  app: Application;\n  \n  async onReady() {\n    this.app.useMiddleware(busboy.UploadMiddleware);\n  }\n}\n```\n</TabItem>\n</Tabs>\n\n\n\n## 配置\n\n组件使用 `busboy` 作为配置的 key。\n\n\n\n### 上传模式\n\n上传分为三种模式，文件模式，流式模式以及新增的异步迭代器模式。\n\n代码中使用 `@Files()` 装饰器获取上传的文件， `@Fields` 装饰器获取其他上传表单字段。\n\n<Tabs>\n<TabItem value=\"file\" label=\"文件模式\">\n\n`file` 为默认值，配置 mode 为 `file` 字符串。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  busboy: {\n    mode: 'file',\n  },\n}\n```\n\n在代码中获取上传的文件，支持同时上传多个文件。\n\n```typescript\nimport { Controller, Post, Files, Fields } from '@midwayjs/core';\nimport { UploadFileInfo } from '@midwayjs/busboy';\n\n@Controller('/')\nexport class HomeController {\n\n  @Post('/upload', /*...*/)\n  async upload(@Files() files: Array<UploadFileInfo>, @Fields() fields: Record<string, string>) {\n    /*\n    files = [\n      {\n        filename: 'test.pdf',        // 文件原名\n        data: '/var/tmp/xxx.pdf',    // 服务器临时文件地址\n        mimeType: 'application/pdf', // mime\n        fieldName: 'file'            // field name\n      },\n    ]\n    */\n  }\n}\n```\n\n使用 file 模式时， 获取的 `data` 为上传的文件在服务器的 `临时文件地址`，后续可以再通过 `fs.createReadStream` 等方式来处理此文件内容，支持同时上传多个文件，多个文件会以数组的形式存放。\n\n每个数组内的对象包含以下几个字段\n\n```typescript\nexport interface UploadFileInfo {\n  /**\n   * 上传的文件名\n   */\n  filename: string;\n  /**\n   * 上传文件 mime 类型\n   */\n  mimeType: string;\n  /**\n   * 上传服务端保存的路径\n   */\n  data: string;\n  /**\n   * 上传的表单字段名\n   */\n  fieldName: string;\n}\n```\n\n\n\n</TabItem>\n\n<TabItem value=\"asyncIterator\" label=\"异步迭代器模式\">\n\n从 `v3.18.0` 提供，替代原有的 `stream` 模式，该模式支持多个文件流式上传。\n\n配置 mode 为 `asyncIterator` 字符串。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  busboy: {\n    mode: 'asyncIterator',\n  },\n}\n```\n\n在代码中获取上传的文件。\n\n```typescript\nimport { Controller, Post, Files, Fields } from '@midwayjs/core';\nimport { UploadStreamFileInfo, UploadStreamFieldInfo } from '@midwayjs/busboy';\n\n@Controller('/')\nexport class HomeController {\n\n  @Post('/upload', /*...*/)\n  async upload(\n  \t@Files() fileIterator: AsyncGenerator<UploadStreamFileInfo>,\n    @Fields() fieldIterator: AsyncGenerator<UploadStreamFieldInfo>\n  ) {\n    // ...\n  }\n}\n```\n\n在该模式下，`@Files` 和 `@File` 装饰器会提供同一个 `AsyncGenerator` ，而 `@Fields` 会也同样会提供一个 `AsyncGenerator`。\n\n通过循环 `AsyncGenerator` ，可以针对每个上传文件的 `ReadStream` 做处理。\n\n```typescript\nimport { Controller, Post, Files, Fields } from '@midwayjs/core';\nimport { UploadStreamFileInfo, UploadStreamFieldInfo } from '@midwayjs/busboy';\nimport { tmpdir } from 'os';\nimport { createWriteStream } from 'fs';\n\n@Controller('/')\nexport class HomeController {\n\n  @Post('/upload', /*...*/)\n  async upload(\n  \t@Files() fileIterator: AsyncGenerator<UploadStreamFileInfo>,\n    @Fields() fieldIterator: AsyncGenerator<UploadStreamFieldInfo>\n  ) {\n    for await (const file of fileIterator) {\n      const { filename, data } = file;\n      const p = join(tmpdir, filename);\n      const stream = createWriteStream(p);\n      data.pipe(stream);\n    }\n\n    for await (const { name, value } of fieldIterator) {\n      // ...\n    }\n\n    // ...\n  }\n}\n```\n\n注意，如果一次上传中任意一个文件抛出了错误，本次上传流会直接关闭，所有未传输完成的文件都会异常。\n\n异步迭代器中的上传对象包含以下几个字段。\n\n```typescript\nexport interface UploadStreamFieldInfo {\n  /**\n   * 上传的文件名\n   */\n  filename: string;\n  /**\n   * 上传文件 mime 类型\n   */\n  mimeType: string;\n  /**\n   * 上传文件的文件流\n   */\n  data: Readable;\n  /**\n   * 上传的表单字段名\n   */\n  fieldName: string;\n}\n```\n\n异步迭代器中的 `@Fields` 的对象略有不同，返回的数据会包含 `name` 和 `value` 字段。\n\n```typescript\nexport interface UploadStreamFieldInfo {\n  /**\n   * 表单名\n   */\n  name: string;\n  /**\n   * 表单值\n   */\n  value: any;\n}\n```\n\n\n\n</TabItem>\n\n<TabItem value=\"stream\" label=\"流模式\">\n\n:::caution\n\n不再推荐使用。\n\n:::\n\n配置 mode 为 `stream` 字符串。\n\n\n使用 stream 模式时，通过 `@Files` 中获取的 `data` 为 `ReadStream`，后续可以再通过 `pipe` 等方式继续将数据流转至其他 `WriteStream` 或 `TransformStream`。\n\n\n使用 stream 模式时，仅同时上传一个文件，即 `@Files` 数组中只有一个文件数据对象。\n\n另外，stream 模式 `不会` 在服务器上产生临时文件，所以获取到上传的内容后无需手动清理临时文件缓存。\n\n:::tip\n\nfaas 场景实现方式视平台而定，如果平台不支持流式请求/响应但是业务开启了 `mode: 'stream'`，将采用先读取到内存，再模拟流式传输来降级处理。\n\n:::\n\n在代码中获取上传的文件，流式模式下仅支持单个文件。\n\n```typescript\nimport { Controller, Post, Files, Fields } from '@midwayjs/core';\nimport { UploadStreamFileInfo } from '@midwayjs/busboy';\n\n@Controller('/')\nexport class HomeController {\n\n  @Post('/upload', /*...*/)\n  async upload(@Files() files: Array<UploadStreamFileInfo>, @Fields() fields: Record<string, string) {\n    /*\n    files = [\n      {\n        filename: 'test.pdf',        // 文件原名\n        data: ReadStream,            // 文件流\n        mimeType: 'application/pdf', // mime\n        fieldName: 'file'            // field name\n      },\n    ]\n\n    */\n  }\n}\n```\n\n流式模式中的对象包含以下几个字段。\n\n```typescript\nexport interface UploadStreamFieldInfo {\n  /**\n   * 上传的文件名\n   */\n  filename: string;\n  /**\n   * 上传文件 mime 类型\n   */\n  mimeType: string;\n  /**\n   * 上传文件的文件流\n   */\n  data: Readable;\n  /**\n   * 上传的表单字段名\n   */\n  fieldName: string;\n}\n```\n\n</TabItem>\n\n</Tabs>\n\n\n\n### 上传文件后缀检查\n\n通过 `whitelist` 属性，配置允许上传的文件后缀名，配置 `null` 则不校验后缀名。\n\n:::caution\n\n如果配置为 `null`，则不对上传文件后缀名进行校验，如果采取文件上传模式 (mode=file)，则会有可能被攻击者所利用，上传 `.php`、`.asp` 等后缀的 WebShell 实现攻击行为。\n\n当然，由于组件会对上传后的临时文件采取 `重新随机生成` 文件名写入，只要开发者 `不将` 上传后的临时文件地址返回给用户，那么即使用户上传了一些不被预期的文件，那也无需过多担心会被利用。\n\n:::\n\n\n如果上传的文件后缀不匹配，会响应 `400` error，默认值如下：\n\n```ts\n'.jpg',\n'.jpeg',\n'.png',\n'.gif',\n'.bmp',\n'.wbmp',\n'.webp',\n'.tif',\n'.psd',\n'.svg',\n'.js',\n'.jsx',\n'.json',\n'.css',\n'.less',\n'.html',\n'.htm',\n'.xml',\n'.pdf',\n'.zip',\n'.gz',\n'.tgz',\n'.gzip',\n'.mp3',\n'.mp4',\n'.avi',\n```\n\n可以通过组件中导出的 `uploadWhiteList` 获取到默认的后缀名白名单。\n\n另外，midway 上传组件，为了避免部分 `恶意用户`，通过某些技术手段来`伪造`一些可以被截断的扩展名，所以会对获取到的扩展名的二进制数据进行过滤，仅支持 `0x2e`（即英文点 `.`）、`0x30-0x39`（即数字 `0-9`）、`0x61-0x7a`（即小写字母 `a-z`） 范围内的字符作为扩展名，其他字符将会被自动忽略。\n\n你可以传递一个函数，可以根据不同的条件动态返回白名单。\n\n```typescript\n// src/config/config.default.ts\nimport { uploadWhiteList } from '@midwayjs/busboy';\nimport { tmpdir } from 'os';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  busboy: {\n    whitelist: (ctx) => {\n      if (ctx.path === '/') {\n        return [\n          '.jpg',\n          '.jpeg',\n        ];\n      } else {\n        return [\n          '.jpg',\n        ]\n      };\n    },\n    // ...\n  },\n}\n```\n\n\n\n\n\n### 上传文件 MIME 类型检查\n\n部分`恶意用户`，会尝试将 `.php` 等 WebShell 修改扩展名为 `.jpg`，来绕过基于扩展名的白名单过滤规则，在某些服务器环境内，这个 jpg 文件依然会被作为 PHP 脚本来执行，造成安全风险。\n\n组件提供了 `mimeTypeWhiteList` 配置参数 **【请注意，此参数无默认值设置，即默认不校验】**，您可以通过此配置设置允许的文件 MIME 格式，规则为由数组 `[扩展名, mime, [...moreMime]]` 组成的 `二级数组`，例如：\n\n```typescript\n// src/config/config.default.ts\nimport { uploadWhiteList } from '@midwayjs/busboy';\nexport default {\n  // ...\n  busboy: {\n    // ...\n    // 扩展名白名单\n    whitelist: uploadWhiteList,\n    // 仅允许下面这些文件类型可以上传\n    mimeTypeWhiteList: {\n      '.jpg': 'image/jpeg',\n      // 也可以设置多个 MIME type，比如下面的允许 .jpeg 后缀的文件是 jpg 或者是 png 两种类型\n      '.jpeg': ['image/jpeg', 'image/png'],\n      // 其他类型\n      '.gif': 'image/gif',\n      '.bmp': 'image/bmp',\n      '.wbmp': 'image/vnd.wap.wbmp',\n      '.webp': 'image/webp',\n    }\n  },\n}\n```\n\n您也可以使用组件提供的 `DefaultUploadFileMimeType` 变量，作为默认的 MIME 校验规则，它提供了常用的 `.jpg`、`.png`、`.psd` 等文件扩展名的 MIME 数据：\n\n```typescript\n// src/config/config.default.ts\nimport { uploadWhiteList, DefaultUploadFileMimeType } from '@midwayjs/busboy';\nexport default {\n  // ...\n  busboy: {\n    // ...\n    // 扩展名白名单\n    whitelist: uploadWhiteList,\n    // 仅允许下面这些文件类型可以上传\n    mimeTypeWhiteList: DefaultUploadFileMimeType,\n  },\n}\n```\n\n文件格式与对应的 MIME 映射，您可以通过 `https://mimetype.io/` 这个网站来查询，对于文件的 MIME 识别，我们使用的是 [file-type@16](https://www.npmjs.com/package/file-type) 这个 npm 包，请注意它支持的文件类型。\n\n:::info\n\nMIME 类型校验规则仅适用于使用 文件上传模式 `mode=file`，同时设置此校验规则之后，由于需要读取文件内容进行匹配，所以会稍微影响上传性能。\n\n但是，我们依然建议您在条件允许的情况下，设置 `mimeTypeWhiteList` 参数，这将提升您的应用程序安全性。\n\n:::\n\n你可以传递一个函数，可以根据不同的条件动态返回 MIME 规则。\n\n```typescript\n// src/config/config.default.ts\nimport { tmpdir } from 'os';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  busboy: {\n    mimeTypeWhiteList: (ctx) => {\n      if (ctx.path === '/') {\n        return {\n          '.jpg': 'image/jpeg',\n        };\n      } else {\n        return {\n          '.jpeg': ['image/jpeg', 'image/png'],\n        }\n      };\n    }\n  },\n}\n\n```\n\n\n\n### Busboy 上传限制\n\n默认情况下没有限制，可以通过配置修改，数字类型，单位为 byte。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  busboy: {\n    // ...\n    limits: {\n      fileSize: 1024\n    }\n  },\n}\n```\n\n除此之外，还可以设置一些其他的 [限制](https://github.com/mscdex/busboy/tree/master?tab=readme-ov-file#exports)。\n\n\n\n\n### 临时文件与清理\n\n\n如果你使用了 `file` 模式来获取上传的文件，那么上传的文件会存放在您于 `config` 文件中设置的 `upload` 组件配置中的 `tmpdir` 选项指向的文件夹内。\n\n你可以通过在配置中使用 `cleanTimeout` 来控制自动的临时文件清理时间，默认值为 `5 * 60 * 1000`，即上传的文件于 `5 分钟` 后自动清理，设置为 `0` 则视为不开启自动清理功能。\n\n```typescript\n// src/config/config.default.ts\nimport { uploadWhiteList } from '@midwayjs/busboy';\nimport { tmpdir } from 'os';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  busboy: {\n    mode: 'file',\n    tmpdir: join(tmpdir(), 'midway-busboy-files'),\n    cleanTimeout: 5 * 60 * 1000,\n  },\n}\n\n```\n\n你也可以在代码中通过调用 `await ctx.cleanupRequestFiles()` 来主动清理当前请求上传的临时文件。\n\n\n\n### 设置不同路由的配置\n\n通过中间件的不同实例，可以对不同的路由做不同的配置，这种场景下会和全局配置合并，仅能覆盖一小部分配置。\n\n```typescript\nimport { Controller, Post, Files, Fields } from '@midwayjs/core';\nimport { UploadFileInfo, UploadMiddleware } from '@midwayjs/busboy';\n\n@Controller('/')\nexport class HomeController {\n  @Post('/upload1', { middleware: [ createMiddleware(UploadMiddleware, {mode: 'file'}) ]})\n  async upload1(@Files() files Array<UploadFileInfo>) {\n    // ...\n  }\n  \n  @Post('/upload2', { middleware: [ createMiddleware(UploadMiddleware, {mode: 'stream'}) ]})\n  async upload2(@Files() files Array<UploadFileInfo>) {\n    // ...\n  }\n}\n```\n\n当前可以传递的配置包括 `mode` 以及 `busboy` 自带的 [配置](https://github.com/mscdex/busboy/tree/master?tab=readme-ov-file#exports)。\n\n\n\n## 内置错误\n\n以下的错误在不同上传模式下均会自动触发。\n\n* `MultipartInvalidFilenameError` 无效文件名\n* `MultipartInvalidFileTypeError` 无效文件类型\n* `MultipartFileSizeLimitError` 文件大小超出限制\n* `MultipartFileLimitError` 文件数量超出限制\n* `MultipartPartsLimitError` 上传 parts 数量超出限制\n* `MultipartFieldsLimitError` fields 数量超出限制\n* `MultipartError` 其余的 busbuy 错误\n\n\n\n## 安全提示\n\n1. 请注意是否开启 `扩展名白名单` （whiteList），如果扩展名白名单被设置为 `null`，则会有可能被攻击者所利用上传 `.php`、`.asp` 等WebShell。\n2. 请注意是否设置 `match` 或 `ignore` 规则，否则普通的 `POST/PUT` 等接口会有可能被攻击者利用，造成服务器负荷加重和空间大量占用问题。\n3. 请注意是否设置 `文件类型规则` （fileTypeWhiteList）,否则可能会被攻击者伪造文件类型进行上传。\n\n\n\n## 前端文件上传示例\n\n### 1. html form 的形式\n\n```html\n<form action=\"/api/upload\" method=\"post\" enctype=\"multipart/form-data\">\n  Name: <input type=\"text\" name=\"name\" /><br />\n  File: <input type=\"file\" name=\"testFile\" /><br />\n  <input type=\"submit\" value=\"Submit\" />\n</form>\n```\n\n### 2. fetch FormData 方式\n\n```js\nconst fileInput = document.querySelector('#your-file-input') ;\nconst formData = new FormData();\nformData.append('file', fileInput.files[0]);\n\nfetch('/api/upload', {\n  method: 'POST',\n  body: formData,\n});\n```\n\n\n\n## Postman 测试示例\n\n![](https://img.alicdn.com/imgextra/i4/O1CN01iv9ESW1uIShNiRjBF_!!6000000006014-2-tps-2086-1746.png)\n\n","isInternal":false,"tokens":5366,"sizeBytes":18355},{"name":"cache.md","path":"site/docs/extensions/cache.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/cache.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# 缓存\n\nMidway Cache 是为了方便开发者进行缓存操作的组件，它有利于改善项目的性能。它为我们提供了一个数据中心以便进行高效的数据访问。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n## 安装\n\n首先安装相关的组件模块。\n\n```bash\n$ npm i @midwayjs/cache@4 cache-manager --save\n$ npm i @types/cache-manager --save-dev\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/cache\": \"^4.0.0\",\n    \"cache-manager\": \"^3.4.1\",\n    // ...\n  },\n  \"devDependencies\": {\n    \"@types/cache-manager\": \"^3.4.0\",\n    // ...\n  }\n}\n```\n\n\n\n## 使用 Cache\n\nMidway 为不同的 cache 存储提供了统一的 API。默认内置了一个基于内存数据存储的数据中心。如果想要使用别的数据中心，开发者也可以切换到例如 mongodb、fs 等模式。\n\n\n首先，引入 Cache 组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as cache from '@midwayjs/cache';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    cache\t\t// 导入 cache 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n然后在业务代码中即可注入使用。\n\n```typescript\nimport { Inject, Provide } from '@midwayjs/core';\nimport { IUserOptions } from '../interface';\nimport { CacheManager } from '@midwayjs/cache';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  cacheManager: CacheManager;     \t\t\t// 依赖注入 CacheManager\n}\n```\n\n通过提供的 API 来设置，获取缓存数据。\n\n\n```typescript\nimport { Inject, Provide } from '@midwayjs/core';\nimport { IUserOptions } from '../interface';\nimport { CacheManager } from '@midwayjs/cache';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  cacheManager: CacheManager;\n\n  async getUser(options: IUserOptions) {\n    // 设置缓存内容\n    await this.cacheManager.set(`name`, 'stone-jin');\n\n    // 获取缓存内容\n    let result = await this.cacheManager.get(`name`);\n\n    return result;\n  }\n\n  async getUser2(){\n    //获取缓存内容\n    let result = await this.cacheManager.get(`name`);\n    return result;\n  }\n\n  async reset(){\n    await this.cacheManager.reset(); // 清空对应 store 的内容\n  }\n}\n```\n\n\n\n### 设置缓存\n\n\n我们通过 `await this.cache.set(key, value)` 方法进行设置，此处默认过期时间是10s。\n\n\n你也可以手动设置 TTL（过期时间），如下：\n```typescript\nawait this.cacheManager.set(key, value, {ttl: 1000});\t// ttl的单位为秒\n```\n如果你想要 Cache 不过期，则将 TTL 设置为 null 即可。\n```typescript\nawait this.cacheManager.set(key, value, {ttl: null});\n```\n同时你也可以通过全局的 `config.default.ts` 中进行设置。\n```typescript\nexport default {\n  // ...\n  cache: {\n    store: 'memory',\n    options: {\n      max: 100,\n      ttl: 10,\t\t// 修改默认的ttl配置\n    },\n  }\n}\n```\n\n\n### 获取缓存\n\n```typescript\nconst value = await this.cacheManager.get(key);\n```\n如果获取不到，则为 undefined。\n\n\n\n### 移除缓存\n\n\n移除缓存，可以通过 del 方法。\n```typescript\nawait this.cacheManager.del(key);\n```\n\n\n\n### 清空整体store数据（此处是整体清除，需要重点⚠️）\n\n\n比如用户设置了某个 redis 为 store，调用的话，包括非 cache 模块设置的也会清除。\n```typescript\nawait this.cacheManager.reset(); // 这块需要注意\n```\n\n\n\n## 全局配置\n\n\n当我们引用了这个 cache 组件后，我们能对其进行全局的配置。配置方法跟别的组件类似。\n\n\n默认的配置：\n```typescript\nexport default {\n  // ...\n  cache: {\n  \tstore: 'memory',\n    options: {\n      max: 100,\n      ttl: 10,\n    },\n  }\n}\n```\n例如用户可以修改默认的 TTL，也就是过期时间。\n\n\n\n## 其他Cache\n\n\n用户也可以修改  store 方式，在 `config.default.ts` 中进行组件的配置：\n```typescript\nimport * as redisStore from 'cache-manager-ioredis';\n\nexport default {\n  // ...\n  cache: {\n  \tstore: redisStore,\n    options: {\n      host: 'localhost', // default value\n      port: 6379, // default value\n      password: '',\n      db: 0,\n      keyPrefix: 'cache:',\n      ttl: 100\n    },\n  }\n}\n```\n或者修改为 mongodb 的 cache。\n\n\n:::danger\n**再次注意⚠️：使用 redis 作为 cache 的时候，代码里面慎用 reset 方法，因为会把整个 redis 给 flushdb，简称数据清空。**\n:::\n\n\n\n## 相关文档\n\n\n由于 Midway Cache 是基于 cache-manager 封装，所以相关资料用户也可以查询：[cache-manger](https://www.npmjs.com/package/cache-manager)。\n\n\n\n## 常见问题\n\n\n\n### 1、set 和 get 无法得到相同值？\n\n用户使用了 cache 模块，默认是内存式的，例如在本地用 dev 模式，由于是单进程的，那 set 和 ge t最终能达到相同的值。但是用户部署到服务器上面后，由于会有多 worker，相当于第一次请求，落在进程1上，然后第二次落在进程2上，这样获得到空了。\n\n\n解决办法：参考 其他 Cache 的章节，配置 store 为分布式，例如 redis 的 store 等方式。\n","isInternal":false,"tokens":1550,"sizeBytes":5402},{"name":"caching.md","path":"site/docs/extensions/caching.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/caching.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# 缓存\n\n缓存是一个伟大而简单的技术，有助于提高你的应用程序的性能。本组件提供了缓存相关的能力，你可以将数据缓存到不同的数据源，也可以针对不同场景建立多级缓存，提高数据访问速度。\n\n:::tip\n\nMidway 提供基于 [cache-manager v5](https://github.com/node-cache-manager/node-cache-manager) 模块重新封装了缓存组件，原有的缓存模块基于 v3 开发不再迭代，如需查看老文档，请访问 [这里](/docs/extensions/cache)。\n\n:::\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 安装\n\n首先安装相关的组件模块。\n\n```bash\n$ npm i @midwayjs/cache-manager@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/cache-manager\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 启用组件\n\n\n首先，引入组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as cacheManager from '@midwayjs/cache-manager';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    cacheManager,\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n\n\n## 使用缓存\n\n\n\n### 1、配置缓存\n\n在使用前，你需要配置缓存所在的位置，比如内置的内存缓存，或者是引入 Redis 缓存，每个缓存对应了一个缓存的 Store。\n\n下面的示例代码，配置了一个名为 `default` 的内存缓存。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: 'memory',\n      },\n    },\n  }\n}\n```\n\n最常用的场景下，缓存会包含两个参数，配置 `max` 修改缓存的数量，配置 `ttl` 修改缓存的过期时间，单位毫秒。\n\n```typescript\n // src/config/config.default.ts\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: 'memory',\n        options: {\n          max: 100,\n          ttl: 10,\n        },\n      },\n    },\n  }\n}\n```\n\n:::tip\n\n* `ttl` 的单位是毫秒\n* `max` 代表缓存 key 的最大个数\n* 不同的 Store 淘汰 key 的算法不同，内存缓存使用的淘汰算法是 LRU\n\n:::\n\n### 2、使用缓存\n\n可以通过服务工厂的装饰器获取到实例，可以通过简单的 `get` 和 `set` 方法获取和保存缓存。\n\n```typescript\nimport { InjectClient, Provide } from '@midwayjs/core';\nimport { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n\n  @InjectClient(CachingFactory, 'default')\n  cache: MidwayCache;\n\n  async invoke(name: string, value: string) {\n    // 设置缓存\n    await this.cache.set(name, value);\n    // 获取缓存\n    const data = await this.cache.get(name);\n    // ...\n  }\n}\n\n```\n\n动态设置 `ttl` 过期时间。\n\n```typescript\nawait this.cache.set('key', 'value', 1000);\n```\n\n若要禁用缓存过期，可以将 `ttl` 配置属性设置为 0。\n\n```typescript\nawait this.cache.set('key', 'value', 0);\n```\n\n删除单个缓存。\n\n```typescript\nawait this.cache.del('key');\n```\n\n清理整个缓存，可以使用 `reset` 方法。\n\n```typescript\nawait this.cacheManager.reset();\n```\n\n:::danger\n\n注意，清理整个缓存非常危险，如果使用了 Redis 作为缓存 store，将清空整个 Redis 数据。\n\n:::\n\n除了装饰器之外，也可以通过 API 获取缓存实例。\n\n```typescript\nimport { InjectClient, Provide } from '@midwayjs/core';\nimport { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  cachingFactory: CachingFactory;\n\n  async invoke() {\n    const caching = await this.cachingFactory.get('default');\n    // ...\n  }\n}\n```\n\n\n\n### 3、配置多个缓存\n\n和其他组件一样，组件支持配置多个缓存实例。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: 'memory',\n      },\n      otherCaching: {\n        store: 'memory',\n      }\n    },\n  }\n}\n```\n\n可以注入不同的缓存实例。\n\n```typescript\nimport { InjectClient, Provide } from '@midwayjs/core';\nimport { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n\n  @InjectClient(CachingFactory, 'default')\n  cache: MidwayCache;\n  \n  @InjectClient(CachingFactory, 'otherCaching')\n  customCaching: MidwayCache;\n\n}\n\n```\n\n\n\n### 4、配置不同 Store\n\n组件基于 [cache-manager](https://github.com/node-cache-manager/node-cache-manager) 可以配置不同的缓存 Store，比如最常见的可以配置 Redis Store。\n\n假如项目已经配置了一个 `Redis`，通过组件内置的 `createRedisStore` 方法，可以快速创建一个 Redis Store。\n\n\n```typescript\nimport { createRedisStore } from '@midwayjs/cache-manager';\n\n// src/config/config.default.ts\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: createRedisStore('default'),\n        options: {\n          ttl: 10,\n        }\n      },\n    },\n  },\n  redis: {\n    clients: {\n      default: {\n        port: 6379,\n        host: '127.0.0.1',\n      }\n    }\n  }\n}\n```\n\n`createRedisStore` 方法可以传递一个已经配置的 redis 实例名，可以和 redis 组件复用实例。\n\n\n\n### 5、配置三方 Store\n\n除了 Redis 之外，用户也可以自行选择 Cache-Manager 的 Store，列表可以参考 [这里](https://github.com/node-cache-manager/node-cache-manager?tab=readme-ov-file#store-engines)。\n\n下面是一个配置  [node-cache-manager-ioredis-yet](https://github.com/node-cache-manager/node-cache-manager-ioredis-yet) 的例子。\n\n```typescript\n// src/config/config.default.ts\nimport { redisStore } from 'cache-manager-ioredis-yet';\n\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: redisStore,\n        options: {\n          port: 6379,\n          host: 'localhost',\n          ttl: 10,\n        },\n      },\n    },\n  }\n}\n```\n\n\n\n### 6、多级缓存\n\n[cache-manager](https://github.com/node-cache-manager/node-cache-manager) 支持将多个缓存 Store 聚合到一起，实现多级缓存。\n\n下面的示例将内存缓存和 Redis 缓存组合到一起，分别设置 60 秒和 600 秒的默认过期时间。\n\n```typescript\n// src/config/config.default.ts\nimport { createRedisStore } from '@midwayjs/cache-manager';\nexport default {\n  cacheManager: {\n    clients: {\n      memoryCaching: {\n        store: 'memory',\n        options: {\n          ttl: 60_000, // 60 秒，单位为毫秒\n        },\n      },\n      redisCaching: {\n        store: createRedisStore('default'),\n        options: {\n          ttl: 600_000, // 600 秒，单位为毫秒\n        },\n      },\n      multiCaching: {\n        store: ['memoryCaching', 'redisCaching'],\n      },\n    },\n  },\n  redis: {\n    clients: {\n      default: {\n        port: 6379,\n        host: '127.0.0.1',\n      },\n    },\n  },\n};\n\n```\n\n这样 `multiCaching` 这个缓存实例就包含了两级缓存，缓存的优先级从上到下，在查找时，会先查找 `memoryCaching` ，如果内存缓存不存在 key，则继续查找 `redisCaching`。\n\n多级缓存自身的 `options.ttl` 不参与 `set`、`mset` 的写入，请在各个子缓存的 `options.ttl` 中配置默认过期时间。内置内存 Store 和 `createRedisStore` 使用的 TTL 单位都是毫秒。\n\n\n\n### 7、使用多级缓存\n\n和普通缓存类似，多级缓存除了 `set`、`get`、`del`方法外，还增加了 `mset` 、`mget`、`mdel` 方法。\n\n```typescript\nimport { InjectClient, Provide } from '@midwayjs/core';\nimport { CachingFactory, MidwayMultiCache } from '@midwayjs/cache-manager';\n\nconst userId2 = 456;\nconst key2 = 'user_' + userId;\nconst ttl = 5;\n\n@Provide()\nexport class UserService {\n\n  @InjectClient(CachingFactory, 'multiCaching')\n  multiCache: MidwayMultiCache;\n\n  async invoke() {\n    // 设置到所有级别的缓存\n    await this.multiCache.set('foo2', 'bar2', ttl);\n\n    // 从最高优先级的缓存 Store 中获取 key\n    console.log(await this.multiCache.get('foo2'));\n    // >> \"bar2\"\n\n    // 调用每一个 Store 的 del 方法进行删除\n    await this.multiCache.del('foo2');\n\n    // 在所有缓存中设置多个 key，可以多个键值对\n    await this.multiCache.mset(\n      [\n        ['foo', 'bar'],\n        ['foo2', 'bar2'],\n      ],\n      ttl\n    );\n\n    // mget() 从最高优先级的缓存中获取值\n    // 如果第一个缓存 Store 中不包含所有的 key，\n    // 继续在下一个缓存 Store 中查找没有找到的 key。\n    // 这是递归地完成的，直到:\n    // - 所有的 key 都已经查找到值\n    // - 所有的缓存 Store 都被查找过\n    console.log(await this.multiCache.mget('key', 'key2'));\n    // >> ['bar', 'bar2']\n\n    // 调用每一个 Store 的 mdel 方法进行删除\n    await this.multiCache.mdel('foo', 'foo2');\n  }\n}\n\n```\n\n对于上面的内存和 Redis 缓存，调用 `set` 或 `mset` 时省略 TTL 参数，各层会使用自己的默认过期时间；显式传入 TTL 时，本次写入的所有缓存层都会使用该值。\n\n```typescript\n// 使用各层默认 TTL：内存 60 秒，Redis 600 秒\nawait this.multiCache.set('key', 'value');\n\n// 覆盖各层默认 TTL：本次写入的两层缓存都使用 30 秒\nawait this.multiCache.set('key', 'value', 30_000);\n```\n\n### 8、自动刷新\n\n不管是普通缓存还是多级缓存，都支持后台刷新功能，只需要配置 `refreshThreshold` 的时间，单位为毫秒。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: 'memory',\n        options: {\n          refreshThreshold: 3 * 1000,\n        },\n      },\n    },\n  }\n}\n```\n\n如果设置了 `refreshthreshold`，每次从缓存获取值之后，会检查 `ttl` 的值，如果剩余的 `ttl` 小于 `refreshthreshold` ，则系统将异步更新缓存，同时系统会返回旧值，直到 `ttl` 过期。\n\n:::tip\n\n* 在多级缓存的情况下，根据优先级顺序找到第一个包含 key 的 Store。\n\n* 如果阈值较低且执行的函数比较慢，key 可能会过期，有可能会遇到并发更新的情况。\n\n* 后台刷新机制目前只支持单个 key。\n\n* 如果没有为 key 设置 `ttl`，则不会触发刷新机制。对于 redis，`ttl`  默认设置为-1。\n\n:::\n\n\n\n## 自动缓存\n\n### 使用装饰器缓存方法\n\n可以通过 `@Caching` 装饰器缓存方法的结果，比如缓存 http 响应或者服务调用的结果。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Caching } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n  @Caching('default')\n  async getUser(name: string) {\n    return name;\n  }\n}\n\n```\n\n当第一次调用 `getUser` 方法时，会正常执行逻辑，返回结果，装饰器会将结果缓存起来，第二次执行时，如果缓存未失效，则会从缓存中直接返回。\n\n### 指定缓存的 ttl\n\n也可以单独设置 `ttl` 。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Caching } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n  @Caching('default', 100)\n  async getUser(name: string) {\n    return name;\n  }\n}\n```\n\n### 手动指定缓存 key\n\n如果对自动生成的 key 不满意，可以手动指定缓存的 key。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Caching } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n  @Caching('default', 'customKey', 100)\n  async getUser(name: string) {\n    return name;\n  }\n}\n```\n\n### 带逻辑的缓存\n\n如果你希望根据一些特定逻辑进行缓存，比如特定参数，或者特定的 Header，可以传递一个工具函数进行逻辑判断。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Caching } from '@midwayjs/cache-manager';\n\nfunction cacheBy({methodArgs, ctx, target}) {\n  if (methodArgs[0] === 'harry' || methodArgs[0] === 'mike') {\n    return 'cache1';\n  }\n}\n\n@Provide()\nexport class UserService {\n  @Caching('default', cacheBy, 100)\n  async getUser(name: string) {\n    return 'hello ' + name;\n  }\n}\n```\n\n上面的示例中，`cacheBy` 方法自定义了缓存的逻辑，当方法入参值为 `harry` 或者 `mike` 时，将返回缓存的 `key` ，而其他参数时则跳过缓存。\n\n这个时候执行的结果为：\n\n```typescript\nawait userService.getUser('harry'));\t// hello harry\nawait userService.getUser('mike'));\t// hello harry\nawait userService.getUser('lucy'));\t// hello lucy\n```\n\n`@Caching` 装饰器可以在第二个参数中传递一个方法，这个方法的入参 options 为：\n\n* `methodArgs`  当前调用方法的实际参数\n* `ctx` 如果是请求作用域，则是当前调用的上下文对象，如果是单例，则该对象为空对象\n* `target` 当前调用的实例\n\n方法的返回值为字符串或者布尔值，当返回字符串时，表示以该 key 将方法的结果缓存，当返回 `undefined` 或者 `null` 时，表示跳过缓存。\n\n通过这些参数判断，我们可以实现非常灵活的自定义缓存逻辑。\n\n\n\n## 常见问题\n\n\n\n### 1、多进程下内存缓存 set 和 get 无法得到相同值\n\n这是正常现象，每个进程的数据是独立的，仅保存在当前进程中。如需跨进程缓存，请使用 Redis 这类分布式缓存系统。\n","isInternal":false,"tokens":4035,"sizeBytes":13605},{"name":"captcha.md","path":"site/docs/extensions/captcha.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/captcha.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# 验证码\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的通用验证码组件，支持 `图片验证码`、`计算表达式` 等类型验证码。\n\n您也可以通过此组件，来实现 `短信验证码`、`邮件验证码` 等验证能力，但是注意，本组件本身不含发送短信、邮件功能。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/captcha@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/captcha\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n## 启用组件\n\n在 `src/configuration.ts` 中引入组件。\n\n```typescript\nimport * as captcha from '@midwayjs/captcha';\n\n@Configuration({\n  imports: [\n    // ...other components\n    captcha\n  ],\n})\nexport class MainConfiguration {}\n```\n\n## 调用服务\n\n```typescript\nimport { Controller, Inject } from '@midwayjs/core';\nimport { CaptchaService } from '@midwayjs/captcha';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  ctx;\n\n  @Inject()\n  captchaService: CaptchaService;\n\n  // 示例：获取图像验证码\n  @Get('/get-image-captcha')\n  async getImageCaptcha() {\n    const { id, imageBase64 } = await this.captchaService.image({ width: 120, height: 40 });\n    return {\n      id,          // 验证码 id\n      imageBase64, // 验证码 SVG 图片的 base64 数据，可以直接放入前端的 img 标签内\n    }\n  }\n\n  // 示例：获取计算表达式验证码\n  @Get('/get-formula-captcha')\n  async getFormulaCaptcha() {\n    const { id, imageBase64 } = await this.captchaService.formula({ noise: 1 });\n    return {\n      id,          // 验证码 id\n      imageBase64, // 验证码 SVG 图片的 base64 数据，可以直接放入前端的 img 标签内\n    }\n  }\n\n  // 验证验证码是否正确\n  @Post('/check-captcha')\n  async getCaptcha() {\n    const { id, answer } = this.ctx.request.body;\n    const passed: boolean = await this.captchaService.check(id, answer);\n    if (passed) {\n      return 'passed';\n    }\n    return 'error';\n  }\n\n  // 示例：短信验证码\n  @Post('/sms-code')\n  async sendSMSCode() {\n    // 验证验证码是否正确\n    const { id, text: code } = await this.captchaService.text({ size: 4 });\n    await sendSMS(18888888888, code);\n    return { id }\n  }\n\n  // 示例：邮件验证码\n  @Post('/email-code')\n  async sendEmailCode() {\n    // 验证验证码是否正确\n    const { id, text: code } = await this.captchaService.text({ type: 'number'});\n    await sendEmail('admin@example.com', code);\n    return { id }\n  }\n\n   // 示例：将任意文本内容塞入验证码中\n  @Get('/test-text')\n  async testText() {\n    // 存入内容，获取验证码id\n    const id: string = await this.captchaService.set('123abc');\n    // 根据验证码id，校验内容是否正确\n    const passed: boolean = await this.captchaService.check(id, '123abc');\n    return {\n      passed: passed === true,\n    }\n  }\n}\n```\n\n## 可用配置\n\n```typescript\ninterface CaptchaOptions {\n  default?: { // 默认配置\n    // 验证码字符长度，默认 4 个字符\n    size?: number;\n    // 干扰线条的数量，默认 1 条\n    noise?: number;\n    // 宽度，默认为 120 像素\n    width?: number;\n    // 宽度，默认为 40 像素\n    height?: number;\n    // 图形验证码配置，图形中包含一些字符\n  },\n  image?: {\n    // 验证码字符长度，默认 4 个字符\n    size?: number;\n    // 图像验证码中的字符类型，默认为 'mixed'\n    // - 'mixed' 表示 0-9、A-Z 和 a-z\n    // - 'letter' 表示 A-Z 和 a-z\n    // - 'number' 表示 0-9\n    type?: 'mixed',\n    // 干扰线条的数量，默认 1 条\n    noise?: number;\n    // 宽度，默认为 120 像素\n    width?: number;\n    // 宽度，默认为 40 像素\n    height?: number;\n  },\n  // 计算公式验证码配置，例如返回的图像内容为 1+2，需要用户填入 3\n  formula?: {\n     // 干扰线条的数量，默认 1 条\n    noise?: number;\n    // 宽度，默认为 120 像素\n    width?: number;\n    // 宽度，默认为 40 像素\n    height?: number;\n  },\n  // 纯文本验证码配置，基于纯文本验证码可以实现短信验证码、邮件验证码\n  text?: {\n    // 验证码字符长度，默认 4 个字符\n    size?: number;\n    // 文本验证码中的字符类型，默认为 'mixed'\n    // - 'mixed' 表示 0-9、A-Z 和 a-z\n    // - 'letter' 表示 A-Z 和 a-z\n    // - 'number' 表示 0-9\n    type?: 'mixed',\n  },\n  // 验证码过期时间，默认为 1h\n  expirationTime?: 3600,\n  // 验证码存储的 key 前缀\n  idPrefix: 'midway:vc',\n}\n\nexport const captcha: CaptchaOptions = {\n  default: { // 默认配置\n    size: 4,\n    noise: 1,\n    width: 120,\n    height: 40,\n  },\n  image: {      // 最终会合并 default 配置\n    type: 'mixed',\n  },\n  formula: {},  // 最终会合并 default 配置\n  text: {},     // 最终会合并 default 配置\n  expirationTime: 3600,\n  idPrefix: 'midway:vc',\n}\n```\n\n更多配置请参考 [svg-captcha](https://github.com/produck/svg-captcha)。\n\n### 配置示例一\n\n获取一个 包含 `5个纯英文字母` 的图像验证码，图像宽度 `200` 像素，高度 `50` 像素，并且包含 `3` 条干扰线。\n\n因为图像验证码的配置 `image`， 会与 `default` 配置进行合并，所以可以只修改 `default` 配置：\n\n```typescript\nexport const captcha: CaptchaOptions = {\n  default: {\n    size: 5,\n    noise: 3,\n    width: 200,\n    height: 50\n  },\n  image: {\n    type: 'letter'\n  }\n}\n```\n当然，也可以 `不` 修改 `default` 配置，将宽度、高度等在 `image` 配置项中进行配置，取得 `同样`的效果：\n```typescript\nexport const captcha: CaptchaOptions = {\n  image: {\n    size: 5,\n    noise: 3,\n    width: 200,\n    height: 50\n    type: 'letter'\n  }\n}\n```\n\n### 配置示例二\n\n获取一个图像宽度 `100` 像素，高度 `60` 像素，并且包含 `2` 条干扰线的计算表达式验证码。\n\n因为计算表达式验证码的配置 `formula` ，会与 `default` 配置合并，所以可以只修改 `default` 配置：\n\n```typescript\nexport const captcha: CaptchaOptions = {\n  default: {\n    noise: 2,\n    width: 100,\n    height: 60\n  },\n}\n```\n当然，也可以 `不` 修改 `default` 配置，将宽度、高度等在 `formula` 配置项中进行配置，取得 `同样`的效果：\n```typescript\nexport const captcha: CaptchaOptions = {\n  formula: {\n    noise: 2,\n    width: 100,\n    height: 60\n  }\n}\n```\n\n\n## 组件依赖\n\n验证码的内容存储基于 `@midwayjs/cache-manager` 组件，默认创建了一个名为 `captcha` 的缓存实例，将数据存储在 `memory` 中。\n\n```typescript\nexport default {\n  cacheManager: {\n    clients: {\n      captcha: {\n        store: 'memory',\n      },\n    },\n  },\n};\n```\n\n如果要替换为 `redis` 或其他服务，请参照 `@midwayjs/cache-manager` 的 [文档](/docs/extensions/caching)，对 cache 进行配置。\n\n\n\n\n## 效果\n\n**图片验证码**\n\n![图片验证码](https://gw.alicdn.com/imgextra/i4/O1CN014cEzLH23vEniOgoyp_!!6000000007317-2-tps-120-40.png)\n\n**计算表达式**\n\n ![计算表达式](https://gw.alicdn.com/imgextra/i4/O1CN01u3Mj0q24lRx1md9pX_!!6000000007431-2-tps-120-40.png)\n\n\n## 注意\n\n* 为了防止机器学习破解，使用的 `svg-captcha` 包为 [安全修复后](https://juejin.cn/post/6872656117839691789) 的版本。\n","isInternal":false,"tokens":2420,"sizeBytes":7630},{"name":"casbin.md","path":"site/docs/extensions/casbin.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/casbin.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# 角色鉴权\n\nCasbin 是一个强大的、高效的开源访问控制框架，其权限管理机制支持多种访问控制模型。\n\n官网文档：https://casbin.org/\n\n\n\n## Casbin 是什么\n\nCasbin 可以：\n\n1. 支持自定义请求的格式，默认的请求格式为`{subject, object, action}`。\n2. 具有访问控制模型model和策略policy两个核心概念。\n3. 支持RBAC中的多层角色继承，不止主体可以有角色，资源也可以具有角色。\n4. 支持内置的超级用户 例如：`root` 或 `administrator`。超级用户可以执行任何操作而无需显式的权限声明。\n5. 支持多种内置的操作符，如 `keyMatch`，方便对路径式的资源进行管理，如 `/foo/bar` 可以映射到 `/foo*`\n\nCasbin 不能：\n\n1. 身份认证 authentication（即验证用户的用户名和密码），Casbin 只负责访问控制。应该有其他专门的组件负责身份认证，然后由 Casbin 进行访问控制，二者是相互配合的关系。\n2. 管理用户列表或角色列表。 Casbin 认为由项目自身来管理用户、角色列表更为合适， 用户通常有他们的密码，但是 Casbin 的设计思想并不是把它作为一个存储密码的容器。 而是存储RBAC方案中用户和角色之间的映射关系。\n\n:::tip\n\n注意：\n\n- 1、在 Midway v3.6.0 之后可用\n- 2、Midway 只是封装了 Casbin 的 API 并提供简单的支持，策略规则编写请查看 [官方文档](https://casbin.org/)\n- 3、Casbin 不提供登录，只提供现有用户的鉴权，需要搭配 passport 等获取用户信息的组件来使用\n\n:::\n\n\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/casbin@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/casbin\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 启用组件\n\n\n首先，引入组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as casbin from '@midwayjs/casbin';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    casbin,\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n\n\n## 准备模型和策略\n\n使用 Casbin 前需要定义模型和策略，这两个文件的内容贯穿本文，建议先去官网了解相关内容。\n\n我们以一个基础的模型为例，比如：\n\n```\n[request_definition]\nr = sub, obj, act\n\n[policy_definition]\np = sub, obj, act\n\n[role_definition]\ng = _, _\ng2 = _, _\n\n[policy_effect]\ne = some(where (p.eft == allow))\n\n[matchers]\nm = g(r.sub, p.sub) && g2(r.obj, p.obj) && r.act == p.act || r.sub == \"root\"\n```\n\n将其保存在项目根目录的 `basic_model.conf` 文件中。\n\n以及包含下面内容的策略文件。\n\n```\np, superuser, user, read:any\np, manager, user_roles, read:any\np, guest, user, read:own\n\ng, alice, superuser\ng, bob, guest\ng, tom, manager\n\ng2, users_list, user\ng2, user_roles, user\ng2, user_permissions, user\ng2, roles_list, role\ng2, role_permissions, role\n```\n\n将其保存在项目根目录的 `basic_policy.csv` 文件中。\n\n\n\n## 配置模型和策略\n\n这里我们的策略将以文件形式进行演示。\n\n配置如下：\n\n```typescript\nimport { MidwayAppInfo } from '@midwayjs/core';\nimport { join } from 'path';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // ...\n    casbin: {\n      modelPath: join(appInfo.appDir, 'basic_model.conf'),\n      policyAdapter: join(appInfo.appDir, 'basic_policy.csv'),\n    }\n  };\n}\n\n```\n\n\n\n## 装饰器鉴权\n\n有多种形式来使用 Casbin，这里以装饰器作为示例。\n\n### 定义资源\n\n首先定义资源，比如放在 `src/resource.ts` 文件中，对应策略文件中对应的资源。\n\n```typescript\nexport enum Resource {\n  USERS_LIST = 'users_list',\n  USER_ROLES = 'user_roles',\n  USER_PERMISSIONS = 'user_permissions',\n  ROLES_LIST = 'roles_list',\n  ROLE_PERMISSIONS = 'role_permission',\n}\n```\n\n\n\n### 配置获取用户的方式\n\n在使用装饰器鉴权时，我们需要配置一个获取用户的方式，比如在 passport 组件之后，我们会从 `ctx.user` 上获取用户名。\n\n```typescript\nimport { MidwayAppInfo } from '@midwayjs/core';\nimport { join } from 'path';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // ...\n    casbin: {\n      modelPath: join(appInfo.appDir, 'basic_model.conf'),\n      policyAdapter: join(appInfo.appDir, 'basic_policy.csv'),\n      usernameFromContext: (ctx) => {\n        return ctx.user;\n      }\n    }\n  };\n}\n\n```\n\n\n\n### 增加守卫\n\n装饰器鉴权依赖守卫，我们可以在全局或者某些路由上开启，全局守卫使用请参考守卫章节。\n\n比如，我们只在下面的 `findAllUsers` 方法上开启鉴权，`AuthGuard` 是 `@midwayjs/casbin` 提供的守卫，可以直接使用。\n\n```typescript\nimport { Controller, Get, UseGuard } from '@midwayjs/core';\nimport { AuthGuard } from '@midwayjs/casbin';\nimport { Resource } from './resouce';\n\n@Controller('/')\nexport class HomeController {\n\n  @UseGuard(AuthGuard)\n  @Get('/users')\n  async findAllUsers() {\n    // ...\n  }\n}\n```\n\n\n\n### 定义权限\n\n使用 `UsePermission` 装饰器定义路由需要的权限。\n\n```typescript\nimport { Controller, Get, UseGuard } from '@midwayjs/core';\nimport { AuthActionVerb, AuthGuard, AuthPossession, UsePermission } from '@midwayjs/casbin';\nimport { Resource } from './resouce';\n\n@Controller('/')\nexport class HomeController {\n\n  @UseGuard(AuthGuard)\n  @UsePermission({\n    action: AuthActionVerb.READ,\n    resource: Resource.USER_ROLES,\n    possession: AuthPossession.ANY\n  })\n  @Get('/users')\n  async findAllUsers() {\n    // ...\n  }\n}\n```\n\n没有权限读取 `USER_ROLES` 的用户不能调用 findAllUsers 方法，在请求时会返回 403 状态码。\n\n比如，上面的 `bob` 用户访问则会返回 403， 而 `tom` 用户访问则正常返回。\n\n\n\n`UsePermission` 需要提供一个对象参数，包括 `action`、`resource`、`possession` 和一个可选的 `isOwn` 的对象。\n\n- `action` 是一个 `AuthActionVerb` 枚举，包含读，写等操作\n- `resource` 资源字符串\n- `possession` 是一个 `AuthPossession` 枚举\n- `isOwn` 是一个接受`Context`（守卫 `canActivate`的参数）作为唯一参数并返回布尔值的函数。 `AuthZGuard` 使用它来确定用户是否是资源的所有者。 如果未定义，将使用返回 `false` 的默认函数。\n\n可以同时定义多个权限，但只有当所有权限都满足时，才能访问该路由。\n\n比如：\n\n```typescript\n@UsePermissions({\n  action: AuthActionVerb.READ,\n  resource: 'USER_ADDRESS',\n  possession: AuthPossession.ANY\n}, {\n  action; AuthActionVerb.READ,\n  resource: 'USER_ROLES,\n  possession: AuthPossession.ANY\n})\n```\n\n只有当用户被授予读取 `USER_ADDRESS` 和 `USER_ROLES` 这两个权限时，才能访问该路由。\n\n\n\n## API 鉴权\n\nCasbin 本身提供了一些通用的 API 和权限相关的功能。\n\n我们可以通过直接注入 `CasbinEnforcerService` 服务来使用。\n\n比如，我们可以在守卫或者中间件中编码。\n\n```typescript\nimport { CasbinEnforcerService } from '@midwayjs/casbin';\nimport { Guard, IGuard } from '@midwayjs/core';\n\n@Guard()\nexport class UserGuard extends IGuard {\n\n  @Inject()\n  casbinEnforcerService: CasbinEnforcerService;\n\n  async canActivate(ctx, clz, methodName) {\n    // 用户登录了，并且是特定的方法，则检查权限\n    if (ctx.user && methodName === 'findAllUsers') {\n      return await this.casbinEnforcerService.enforce(ctx.user, 'USER_ROLES', 'read');\n    }\n    // 未登录用户不允许访问\n    return false;\n  }\n}\n```\n\n在启用守卫后，效果和上面的装饰器相同。\n\n此外，`CasbinEnforcerService` 还有更多的 API，比如重新加载策略。\n\n```typescript\nawait this.casbinEnforcerService.loadPolicy();\n```\n\n\n\n## 分布式策略存储\n\n在多台机器部署的场景下，需要将策略存储到外部。\n\n当前已经实现的适配器有：\n\n- Redis\n- Typeorm\n\n\n\n### Redis Adapter\n\n需要依赖 `@midwayjs/casbin-redis-adapter` 包和 redis 组件。\n\n```bash\n$ npm i @midwayjs/casbin-redis-adapter @midwayjs/redis --save\n```\n\n启用 redis 组件。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as redis from '@midwayjs/redis';\nimport * as casbin from '@midwayjs/casbin';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    redis,\n    casbin,\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n配置 redis 连接和 casbin 适配器。\n\n```typescript\nimport { MidwayAppInfo } from '@midwayjs/core';\nimport { join } from 'path';\nimport { createAdapter } from '@midwayjs/casbin-redis-adapter';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // ...\n    redis: {\n      clients: {\n        // 为 casbin 定义了一个连接\n        'node-casbin-official': {\n          host: '127.0.0.1',\n          port: 6379,\n          password: '',\n          db: '0',\n        }\n      }\n    },\n    casbin: {\n      policyAdapter: createAdapter({\n        // 配置了上面的连接名\n        clientName: 'node-casbin-official'\n      }),\n      // ...\n    },\n  };\n}\n\n```\n\n\n\n### TypeORM Adapter\n\n需要依赖 `@midwayjs/casbin-typeorm-adapter` 包和 typeorm 组件。\n\n```\n$ npm i @midwayjs/casbin-typeorm-adapter @midwayjs/typeorm typeorm@^1.0.0 better-sqlite3 --save\n```\n\n启用 typeorm 组件。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as typeorm from '@midwayjs/typeorm';\nimport * as casbin from '@midwayjs/casbin';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    typeorm,\n    casbin,\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n配置适配器，下面以 better-sqlite3 存储为例，mysql 的配置可以查看 typeorm 组件。\n\n```typescript\nimport { MidwayAppInfo } from '@midwayjs/core';\nimport { join } from 'path';\nimport { CasbinRule, createAdapter } from '@midwayjs/casbin-typeorm-adapter';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // ...\n    typeorm: {\n      dataSource: {\n        // 为 casbin 定义了一个连接\n        'node-casbin-official': {\n          type: 'better-sqlite3',\n          synchronize: true,\n          database: join(appInfo.appDir, 'casbin.sqlite'),\n          // 注意这里显式引入了 Entity\n          entities: [CasbinRule],\n        }\n      }\n    },\n    casbin: {\n      policyAdapter: createAdapter({\n        // 配置了上面的连接名\n        dataSourceName: 'node-casbin-official'\n      }),\n      // ...\n    }\n  };\n}\n```\n\n\n\n## 监视器\n\n使用分布式消息系统，例如 [etcd](https://github.com/coreos/etcd) 来保持多个Casbin执行器实例之间的一致性。 因此，我们的用户可以同时使用多个Casbin 执行器来处理大量的权限检查请求。\n\nMidway 当前只提供一种 Redis 更新策略，如有其他需求，可以给我们提交 issue。\n\n### Redis Watcher\n\n需要依赖 `@midwayjs/casbin-redis-adapter` 包和 redis 组件。\n\n```bash\n$ npm i @midwayjs/casbin-redis-adapter @midwayjs/redis --save\n```\n\n启用 redis 组件。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as redis from '@midwayjs/redis';\nimport * as casbin from '@midwayjs/casbin';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    redis,\n    casbin,\n  ],\n  // ...\n})\nexport class MainConfiguration {\n}\n```\n\n使用示例：\n\n```typescript\nimport { MidwayAppInfo } from '@midwayjs/core';\nimport { join } from 'path';\nimport { createAdapter, createWatcher } from '@midwayjs/casbin-redis-adapter';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // ...\n    redis: {\n      clients: {\n        'node-casbin-official': {\n          host: '127.0.0.1',\n          port: 6379,\n          db: '0',\n        },\n        'node-casbin-sub': {\n          host: '127.0.0.1',\n          port: 6379,\n          db: '0',\n        }\n      }\n    },\n    casbin: {\n      // ...\n      policyAdapter: createAdapter({\n        clientName: 'node-casbin-official'\n      }),\n      policyWatcher: createWatcher({\n        pubClientName: 'node-casbin-official',\n        subClientName: 'node-casbin-sub',\n      })\n    },\n  };\n}\n```\n\n注意，pub/sub 连接需要不同的客户端，上面代码定义了两个客户端。\n\npub 客户端可以和普通 Redis 客户端连接复用，而 sub 需要一个独立的客户端。\n","isInternal":false,"tokens":3833,"sizeBytes":12749},{"name":"cfork.md","path":"site/docs/extensions/cfork.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/cfork.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# cfork\n\n很多同学没有听过 cfork，cfork 库是 egg-scripts 中用于启动主进程的库，是 egg 使用的基础库之一，他的功能是启动进程，并维持多个进程的保活。\n\n\n文档在此：[https://github.com/node-modules/cfork](https://github.com/node-modules/cfork)\n\n\n由于 bootstrap.js 的特性，有时候不是很适合 pm2 来部署（比如集团内部，全局不安装，需要 API 启动）。\n\n\n我们可以新增一个 `server.js` 用来做主进程的入口，将 `bootstrap.js` 作为每个子进程的启动入口。\n\n\n```javascript\n// server.js\n\n'use strict';\n\nconst cfork = require('cfork');\nconst util = require('util');\nconst path = require('path');\nconst os = require('os');\n\n// 获取 cpu 核数\nconst cpuNumbers = os.cpus().length;\n\ncfork({\n  exec: path.join(__dirname, './bootstrap.js'),\n  count: cpuNumbers,\n})\n  .on('fork', (worker) => {\n    console.warn('[%s] [worker:%d] new worker start', Date(), worker.process.pid);\n  })\n  .on('disconnect', (worker) => {\n    console.warn(\n      '[%s] [master:%s] worker:%s disconnect, exitedAfterDisconnect: %s, state: %s.',\n      Date(),\n      process.pid,\n      worker.process.pid,\n      worker.exitedAfterDisconnect,\n      worker.state\n    );\n  })\n  .on('exit', (worker, code, signal) => {\n    const exitCode = worker.process.exitCode;\n    const err = new Error(\n      util.format(\n        'worker %s died (code: %s, signal: %s, exitedAfterDisconnect: %s, state: %s)',\n        worker.process.pid,\n        exitCode,\n        signal,\n        worker.exitedAfterDisconnect,\n        worker.state\n      )\n    );\n    err.name = 'WorkerDiedError';\n    console.error('[%s] [master:%s] worker exit: %s', Date(), process.pid, err.stack);\n  });\n```\n\n最后启动 `node server.js` 即可。\n","isInternal":false,"tokens":506,"sizeBytes":1773},{"name":"code_dye.md","path":"site/docs/extensions/code_dye.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/code_dye.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# 代码染色\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的代码染色组件。\n\n用于在 HTTP 场景展示调用链路耗时与各个方法的出入参，帮你更快地定位代码问题。\n\n比如：\n\n+ 代码执行缓慢\n  - 不知道是哪一个方法执行的慢：通过代码染色后，可以查看每一个 `方法的执行时长`。\n+ 代码执行错误\n  - 可能是方法没有调到：通过代码染色后，可以查看每一个 `方法的调用链`。\n  - 可能是方法调用参数出错：通过代码染色后，查看每一个`方法的入参和返回值`\n\n使用效果：\n\n![](https://gw.alicdn.com/imgextra/i1/O1CN017Zd6y628M2PvqJO7I_!!6000000007917-2-tps-2392-844.png)\n\n\n\n\n相关信息：\n\n| web 支持情况      |      |\n| ----------------- | ---- |\n| @midwayjs/koa     | ✅    |\n| @midwayjs/faas    | ✅    |\n| @midwayjs/web     | ✅    |\n| @midwayjs/express | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/code-dye@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/code-dye\": \"^4.0.0\"\n    // ...\n  },\n}\n```\n\n\n\n## 启用组件\n\n将 code-dye 组件配置到代码中。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as codeDye from '@midwayjs/code-dye';\n\n@Configuration({\n  imports: [\n    // ...\n    {\n      component: codeDye,\n      enabledEnvironment: ['local'],\t// 只在本地启用\n    }\n  ],\n})\nexport class MainConfiguration {}\n```\n\n\n\n\n\n:::tip\n\n- 可以在 `本地` 或 `研发` 环境开启本组件，便于开发时定位问题，但是 `不建议` 在线上启用，会对线上访问性能产生影响。\n\n:::\n\n\n\n## 配置染色\n\n可以通过`matchQueryKey` 配置，控制当 `query` 参数包含`matchQueryKey` 配置对应的值的时候，进入染色链路，例如，配置为：\n\n```typescript\n// src/config/config.local.ts\nexport default {\n  codeDye: {\n    matchQueryKey: 'codeDyeABC',\n  }\n}\n```\n当请求接口 `http://127.0.0.1:7001/test?codeDyeABC=html` 时，就会判断 `query` 中是否存在 `codeDyeABC` 参数来决定是否染色，并根据参数对应的值，来响应不同的染色结果。\n\n也可以通过`matchHeaderKey` 配置，控制当 `headers` 参数包含 `matchHeaderKey` 配置对应的值的时候，进入染色链路，例如，配置为：\n\n```typescript\n// src/config/config.local.ts\nexport default {\n  codeDye: {\n    matchHeaderKey: 'codeDyeHeader',\n  }\n}\n```\n当请求接口 `http://127.0.0.1:7001/test` 时，就会判断请求的 `headers` 中是否存在 `codeDyeHeader` 参数来决定是否染色，并根据参数对应的值，来响应不同的染色结果。\n\n\n\n## 染色报告\n\n开启了代码染色后，链路染色的结果，可以通过开启染色的不同参数值来配置，目前支持以下三种：\n\n+ `html`：`对` 当前请求的结果进行处理，将染色信息添加到结果中，响应为 `html`，可以在浏览器上查看，效果可以查看此文档上面的图片效果展示。\n+ `json`：`对` 当前请求的结果进行处理，将染色信息添加到结果中，响应为 `json` 结构化信息。\n+ `log`：`不对` 当前请求的结果进行处理，染色的信息将会输出到日志中，不影响请求。\n\n例如，配置为：\n\n```typescript\n// src/config/config.local.ts\nexport default {\n  codeDye: {\n    matchQueryKey: 'codeDyeXXX',\n  }\n}\n```\n\n当请求接口 `http://127.0.0.1:7001/test?codeDyeXXX=html` 时，就会判断 `query` 中 `codeDyeXXX` 参数的值为 `html`，就将染色结果输出在当前请求的响应中，并且内容为 `html` 格式。\n","isInternal":false,"tokens":1228,"sizeBytes":3703},{"name":"commander.md","path":"site/docs/extensions/commander.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/commander.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# 命令行\n\n`@midwayjs/commander` 是一个基于 Midway IoC 容器的命令行组件，底层使用 [commander.js](https://github.com/tj/commander.js#readme) 做参数解析与 help 输出。你可以用 Midway 熟悉的依赖注入方式组织命令、选项解析与业务逻辑，并将命令拆分为多个 Class。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ❌    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ❌   |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n## 安装依赖\n\n在现有项目中安装 commander 组件依赖。\n\n```bash\n$ npm i @midwayjs/commander@4  --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/commander\": \"^4.0.0\"\n  }\n}\n```\n\n## 开启组件\n\n在入口配置中引入组件。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as commander from '@midwayjs/commander';\n\n@Configuration({\n  imports: [commander],\n})\nexport class MainConfiguration {}\n```\n\n\n## 编写命令\n\n目录结构示例：\n\n```bash\n.\n├── src\n│   ├── commands\n│   │   ├── hello.command.ts\n│   │   └── status.command.ts\n│   ├── configuration.ts\n│   └── ...\n├── bootstrap.js\n└── package.json\n```\n\n一个命令对应一个 Class，使用 `@Command()` 修饰，并实现 `CommandRunner` 接口的 `run()` 方法。\n\n\n```typescript\n// src/commands/hello.command.ts\nimport { Inject, ILogger } from '@midwayjs/core';\nimport { Command, CommandRunner, Option } from '@midwayjs/commander';\n\n@Command({\n  name: 'hello',\n  description: 'hello command',\n  arguments: '<name>',\n  aliases: ['hi'],\n})\nexport class HelloCommand implements CommandRunner {\n  @Inject()\n  logger: ILogger;\n\n  @Option({\n    flags: '-f, --foo [foo]',\n    description: 'foo option',\n    defaultValue: 'bar',\n  })\n  parseFoo(val: string) {\n    return `${val}_parsed`;\n  }\n\n  async run(passedParams: string[], options?: Record<string, any>) {\n    const [name] = passedParams;\n    this.logger?.info?.(`hello ${name}`, options);\n  }\n}\n```\n\n### `@Command()` 参数\n\n - `name`：命令名称（命令行调用与 `--help` 展示时使用）\n- `arguments`：命令位置参数声明（例如 `<name>`、`<a> [b]`）\n- `description`：命令描述，会展示在 `--help` 中\n- `argsDescription`：位置参数描述对象，会展示在 `--help` 中\n- `aliases`：命令别名数组\n\n### `@Option()` 参数\n\n- `flags`：选项声明（例如 `-f, --foo [foo]`、`-n, --num <num>`）\n- `description`：选项描述，会展示在 `--help` 中\n- `defaultValue`：默认值（不传该选项时生效）\n- `required`：是否必填（等价于 commander 的 `requiredOption`）\n\n`@Option()` 修饰的方法会作为 commander 的自定义 parser，用于把字符串参数转换为业务需要的类型（例如 number/boolean/自定义格式）。\n\n## 运行命令\n\n该组件作为 Midway Framework 运行，在应用启动时会解析 `process.argv` 并执行匹配的命令。\n\n如果你使用 `bootstrap.js` 作为入口，可以这样启动：\n\n```javascript\n// bootstrap.js\nconst { Bootstrap } = require('@midwayjs/bootstrap');\nBootstrap.run();\n```\n\n然后运行：\n\n```bash\n$ node bootstrap.js hello world --foo baz\n$ node bootstrap.js hi world\n```\n\n## 上下文\n\n每次命令执行都会创建请求上下文（request context），可以在命令类中注入 `Context` 获取运行时信息：\n\n```typescript\nimport { Command, CommandRunner, Context } from '@midwayjs/commander';\nimport { Inject } from '@midwayjs/core';\n\n@Command({ name: 'info', arguments: '<name>' })\nexport class InfoCommand implements CommandRunner {\n  @Inject()\n  ctx: Context;\n\n  async run() {\n    this.ctx.commandName; // 命令名\n    this.ctx.args; // 位置参数数组\n    this.ctx.options; // commander 解析后的选项\n    this.ctx.command; // commander 的 Command 实例\n  }\n}\n```\n\n## 交互式提问（Enquirer）\n\n组件内置了基于 [enquirer](https://github.com/enquirer/enquirer) 的交互式提问能力，适用于运行过程中补齐参数。你可以用 `@QuestionSet()` 组织问题集合，并在命令里通过 `EnquirerService` 触发。\n\n如果希望既支持命令行传参又支持交互补齐，请把对应选项设为可选（否则 commander 会在缺参时直接报错）。\n\n```typescript\nimport {\n  Command,\n  CommandRunner,\n  QuestionSet,\n  Question,\n  ValidateFor,\n  DefaultFor,\n  WhenFor,\n  EnquirerService,\n} from '@midwayjs/commander';\nimport { Inject } from '@midwayjs/core';\n\n@QuestionSet()\nclass ProfileQuestionSet {\n  @Question({ type: 'input', name: 'age', message: 'Your age?' })\n  parseAge(value: string) {\n    return Number.parseInt(value, 10);\n  }\n\n  @Question({ type: 'input', name: 'nickname', message: 'Nickname?' })\n  parseNickname(value: string) {\n    return value;\n  }\n\n  @ValidateFor({ name: 'age' })\n  validateAge(value: string) {\n    return value ? true : 'age required';\n  }\n\n  @DefaultFor({ name: 'nickname' })\n  defaultNickname() {\n    return 'neo';\n  }\n\n  @WhenFor({ name: 'nickname' })\n  whenNickname(answers: Record<string, unknown>) {\n    return Boolean(answers.useNickname);\n  }\n}\n\n@Command({ name: 'ask' })\nexport class AskCommand implements CommandRunner {\n  @Inject()\n  enquirerService: EnquirerService;\n\n  async run(_passedParams: string[], options?: Record<string, any>) {\n    const answers = await this.enquirerService.prompt(ProfileQuestionSet, {\n      useNickname: options?.useNickname,\n    });\n    // use answers.age / answers.nickname\n  }\n}\n```\n\n说明：\n- `@Question()` 修饰的方法会作为 enquirer 的 `result`（用于转换用户输入）。\n- `@DefaultFor()` 会映射到 enquirer 的 `initial`。\n- `@WhenFor()` 支持根据已收集的答案决定是否提问。\n- 可用的 `@*For()` 装饰器：`ValidateFor`、`ChoicesFor`、`MessageFor`、`DefaultFor`、`WhenFor`。\n- `prompt()` 同时支持 `QuestionSet` 名称字符串或类引用，推荐使用类引用。\n\n## 错误处理\n\n默认情况下，CLI 入口会捕获异常并输出日志后退出进程。你可以在配置里提供 `errorHandler` 来接管错误处理：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  commander: {\n    errorHandler: (err: Error) => {\n      console.error(err);\n      process.exit(1);\n    },\n  },\n};\n```\n\n如果你在命令里使用了 `@Catch()` 错误过滤器（Midway Filter），会先走过滤器逻辑，再进入这里的兜底处理。\n\n## 返回值与输出\n\n默认情况下，命令执行完毕后，如果 `run()` 有返回值，框架会将返回值输出到标准输出（stdout），方便用作脚本管道或在测试中断言输出内容。\n\n支持的返回值类型：\n\n- `string` / `Buffer`：直接写入 stdout\n- 普通对象：使用 `JSON.stringify` 后写入 stdout\n- `Readable`：会 pipe 到 stdout\n- `AsyncIterable`：会按迭代顺序逐段写入 stdout\n\n下面是一个返回文本/JSON 的例子（使用 core 的 `ServerResponse` 语义来组织输出格式）：\n\n```typescript\nimport { Command, CommandRunner, CliServerResponse } from '@midwayjs/commander';\n\n@Command({ name: 'status' })\nexport class StatusCommand implements CommandRunner {\n  async run() {\n    return new CliServerResponse({} as any).success().json({ ok: true });\n  }\n}\n```\n\n如果希望按 chunk 逐步输出，可以返回 `CliServerResponse().stream()`：\n\n```typescript\nimport { Command, CommandRunner, CliServerResponse } from '@midwayjs/commander';\n\n@Command({ name: 'stream' })\nexport class StreamCommand implements CommandRunner {\n  async run() {\n    const response = new CliServerResponse({} as any);\n    const stream = response.stream();\n\n    setImmediate(() => {\n      stream.send('a');\n      stream.send({ b: 2 });\n      stream.end();\n    });\n\n    return stream;\n  }\n}\n```\n\n## 日志\n\n组件默认会注册一个名为 `commanderLogger` 的 logger，默认写入 `midway-commander.log`。\n\n你可以在命令类里通过 `@Logger('commanderLogger')` 注入使用，例如：\n\n```typescript\nimport { Logger, ILogger } from '@midwayjs/core';\nimport { Command, CommandRunner } from '@midwayjs/commander';\n\n@Command({ name: 'hello', arguments: '<name>' })\nexport class HelloCommand implements CommandRunner {\n  @Logger('commanderLogger')\n  logger: ILogger;\n\n  async run(passedParams: string[]) {\n    this.logger.info('hello %s', passedParams[0]);\n  }\n}\n```\n\n如果希望自定义日志文件名或级别，可以在应用配置中覆盖 `midwayLogger.clients.commanderLogger`：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  midwayLogger: {\n    clients: {\n      commanderLogger: {\n        fileLogName: 'my-commander.log',\n        level: 'info',\n      },\n    },\n  },\n};\n```\n\n## 单元测试\n\n命令行参数在测试环境中很容易被 Jest/Node 参数污染，推荐通过 framework 的 `runCommand()` 来执行命令，而不是直接 mock `process.argv`。\n\n```typescript\nimport { createLightApp, close } from '@midwayjs/mock';\nimport { Framework } from '@midwayjs/commander';\nimport * as commander from '@midwayjs/commander';\n\ndescribe('commander', () => {\n  it('should run command', async () => {\n    const app = await createLightApp({\n      imports: [commander],\n      preloadModules: [HelloCommand],\n    });\n\n    const framework = app.getFramework() as Framework;\n    await framework.runCommand('hello', 'world', '--foo', 'bar');\n\n    await close(app);\n  });\n});\n```\n","isInternal":false,"tokens":2718,"sizeBytes":9537},{"name":"consul.md","path":"site/docs/extensions/consul.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/consul.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# Consul\n\nconsul 用于微服务下的服务治理，主要特点有：服务发现、服务配置、健康检查、键值存储、安全服务通信、多数据中心等。\n\n本文基于最新组件实现，介绍 Consul 客户端的配置与使用，以及统一抽象的服务发现能力。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n从 v4 开始 Consul 组件基于 Service Factory 进行了重构，API 有着非常大的变化，并提供了服务发现等抽象能力。\n\n感谢 [boostbob](https://github.com/boostbob) 提供的组件。\n\n\n效果如下图：\n![image.png](https://img.alicdn.com/imgextra/i3/O1CN01e5cFZx1I0draeZynr_!!6000000000831-2-tps-1500-471.png)\n\n![image.png](https://img.alicdn.com/imgextra/i2/O1CN01iLYF8r1HQ0B3b47Fh_!!6000000000751-2-tps-1500-895.png)\n\n\n## 安装组件\n\n首先安装 Consul 组件：\n\n```bash\n$ npm i @midwayjs/consul@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/consul\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 引入组件\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as consul from '@midwayjs/consul';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [consul],\n  importConfigs: [join(__dirname, 'config')]\n})\nexport class MainConfiguration {}\n```\n\n\n\n## 配置客户端\n\n支持单客户端与多客户端，遵循 ServiceFactory 配置结构。\n\n**单客户端**\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  consul: {\n    client: {\n      host: '127.0.0.1',\n      port: '8500'\n    }\n  }\n}\n```\n\n**多客户端**\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  consul: {\n    clients: {\n      a: { host: '127.0.0.1', port: '8500' },\n      b: { host: '127.0.0.1', port: '8501' }\n    },\n    defaultClientName: 'a'\n  }\n}\n```\n\n**使用默认实例**\n\n`ConsulService` 作为默认实例代理，直接暴露原生 Consul 客户端方法。\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { ConsulService } from '@midwayjs/consul';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  consul: ConsulService;\n\n  async register() {\n     await this.consul.kv.set('name', 'juhai');\n  }\n}\n```\n\n\n\n**获取指定实例**\n\n通过 `ConsulServiceFactory` 获取指定命名的客户端实例。\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { ConsulServiceFactory } from '@midwayjs/consul';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  consulFactory: ConsulServiceFactory;\n\n  async invoke() {\n    const consulA = this.consulFactory.get('a');\n    const consulB = this.consulFactory.get('b');\n    await consulA.agent.service.register({ name: 'svc-a', id: 'svc-a-1' });\n    await consulB.agent.service.register({ name: 'svc-b', id: 'svc-b-1' });\n  }\n}\n```\n\n\n\n\n\n## 服务发现\n\n从 v4 开始提供基于统一抽象的服务发现能力，Consul 将成为服务发现的客户端来使用。\n\n服务发现包括 **注册服务** 和 **获取服务** 两部分。\n\n\n\n### 配置服务发现\n\n你需要先配置一个客户端，如果配置了多个，可以使用 `serviceDiscoveryClient` 来指定。\n\n通过 `serviceDiscovery` 来配置服务发现。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  consul: {\n    clients: {\n      default: { host: '127.0.0.1', port: '8500' },\n    },\n    serviceDiscovery: {\n      // ...\n    }\n  }\n}\n```\n\n\n\n### 注册服务发现\n\n当服务启动后，你可以将当前的服务注册到 Consul 注册中心。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport { ConsulServiceDiscovery } from '@midwayjs/consul';\nimport { LoadBalancerType } from '@midwayjs/core';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  @Inject()\n  consulDiscovery: ConsulServiceDiscovery;\n\n  async onServerReady() {\n    // 创建一个服务发现客户端\n    const client = this.consulDiscovery.createClient();\n\n    // 注册当前服务\n    await client.register({\n      id: client.defaultMeta.id,\n      name: 'order',\n      tags: ['test'],\n      address: client.defaultMeta.host,\n      port: 7001,\n      meta: {\n        version: '1.0.0'\n      },\n      check: {\n        name: 'TTL Health Check',\n        timeout: '30s',\n        ttl: '10s'\n      }\n    });\n  }\n}\n```\n\n`register` 方法需要传递一些基础的信息。\n\n最重要的有 id，name，以及 address。\n\n```typescript\ninterface RegisterOptions extends CommonOptions {\n  name: string;\n  id?: string;\n  tags?: string[];\n  address?: string;\n  taggedaddresses?: Record<string, any>;\n  meta?: Record<string, string>;\n  namespace?: string;\n  port?: number;\n  kind?: string;\n  proxy?: any;\n  connect?: RegisterConnect;\n  check?: CheckOptions;\n  checks?: CheckOptions[];\n}\n```\n\n`client.defaultMeta` 提供了一些默认生成的数据，包括 id，serviceName, host。\n\n\n\n### 获取可用服务\n\n你可以在任意需要的地方，动态获取到一个服务来调用。\n\n````typescript\n@Provide()\nexport class OrderService {\n  \n  @Inject()\n  consulDiscovery: ConsulServiceDiscovery;\n  \n  async getSerivce() {\n  \tconst instance = await this.consulDiscovery.getInstance({ service: 'order'});\n    // instance.name\n  }\n}\n````\n\n返回的 instance 类型为 `ConsulHealthItem`，结构如下。\n\n```typescript\nexport interface ConsulHealthItem {\n  Node: {\n    ID: string;\n    Node: string;\n    Address: string;\n    Datacenter: string;\n    TaggedAddresses: Record<string, any>;\n    Meta: Record<string, any>;\n    CreateIndex: number;\n    ModifyIndex: number;\n  };\n  Service: {\n    ID: string;\n    Service: string;\n    Tags: string[];\n    Address: string;\n    TaggedAddresses: Record<string, any>;\n    Meta: Record<string, any>;\n    Port: number;\n    Weights: Record<string, any>;\n    EnableTagOverride: boolean;\n    Proxy: Record<string, any>;\n    Connect: Record<string, any>;\n    PeerName: string;\n    CreateIndex: number;\n    ModifyIndex: number;\n  };\n  Checks: Array<{\n    Node: string;\n    CheckID: string;\n    Name: string;\n    Status: 'passing' | 'warning' | 'critical';\n    Notes: string;\n    Output: string;\n    ServiceID: string;\n    ServiceName: string;\n    ServiceTags: string[];\n    Type: string;\n    Interval: string;\n    Timeout: string;\n    ExposedPort: number;\n    Definition: Record<string, any>;\n    CreateIndex: number;\n    ModifyIndex: number;\n  }>;\n}\n\n```\n\n\n\n## 配置中心\n\n\n同时 consul 也能作为一个服务配置的地方，如下代码：\n\n```typescript\nimport { Controller, Get, Inject } from '@midwayjs/core';\nimport { ConsulService } from '@midwayjs/consul';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  consul: ConsulService;\n\n  @Get('/')\n  async home() {\n    await this.consul.kv.set('name', 'juhai');\n    const res = await this.consul.kv.get('name');\n    return res;\n  }\n}\n\n```\n\n我们调用 `kv.set` 方法，我们可以设置对应的配置，通过 `kv.get`  方法可以拿到对应的配置。\n\n\n注意：在代码中，有同学出现，在每次请求中去 get 对应的配置，这时你的 QPS 多少对 Consul server 的压力。\n\n\n所以在QPS比较大的情况，可以如下处理：\n\n```typescript\nimport { Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';\nimport { ConsulService } from '@midwayjs/consul';\n\n@Provide()\n@Scope(ScopeEnum.Singleton)\nexport class ConfigService {\n\n  @Inject()\n  consul: ConsulService;\n\n  config: any;\n\n  @Init()\n  async init() {\n    setInterval(async () => {\n      this.config = await this.consul.kv.get('name');\n    }, 5000);\n    this.config = await this.consul.kv.get('name');\n  }\n\n  async getConfig(){\n    return this.config;\n  }\n}\n\n```\n\n上面的代码，相当于定时去获取对应的配置，当每个请求进来的时候，获取 Scope 为 ScopeEnum.Singleton 服务的 `getConfig` 方法，这样每 5s 一次获取请求，就减少了对服务的压力。\n\nConsul 界面上如下图：\n\n![image.png](https://img.alicdn.com/imgextra/i4/O1CN01V3P6uK1rIVs19JiWn_!!6000000005608-2-tps-1500-374.png)\n\n![image.png](https://img.alicdn.com/imgextra/i2/O1CN014O2GyH1sMvIhmlbs4_!!6000000005753-2-tps-1500-667.png)\n\n\n一共提供如下几种方法：\n\n- [get](https://www.npmjs.com/package/consul#kv-get)，获取对应key的value\n- [keys](https://www.npmjs.com/package/consul#kv-keys)，查询某个prefix的key的列表\n- [set](https://www.npmjs.com/package/consul#kv-set)，设置对应的key的值\n- [del](https://www.npmjs.com/package/consul#kv-del)，删除对应的key\n\n\n","isInternal":false,"tokens":2533,"sizeBytes":8690},{"name":"cos.md","path":"site/docs/extensions/cos.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/cos.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# 腾讯云对象存储（COS）\n\n本文介绍了如何使用 midway 接入腾讯云 COS。\n\n相关信息：\n\n| 描述              |     |\n| ----------------- | --- |\n| 可用于标准项目    | ✅   |\n| 可用于 Serverless | ✅   |\n| 可用于一体化      | ✅   |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/cos@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/cos\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 引入组件\n\n\n首先，引入 组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as cos from '@midwayjs/cos';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    cos\t\t// 导入 cos 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n\n## 配置\n\n比如：\n\n\n**单客户端配置**\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  cos: {\n    client: {\n      SecretId: '***********',\n      SecretKey: '***********',\n    },\n  },\n}\n```\n\n\n**多个客户端配置，需要配置多个**\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  cos: {\n    clients: {\n      instance1: {\n        SecretId: '***********',\n        SecretKey: '***********',\n      },\n      instance2: {\n        SecretId: '***********',\n        SecretKey: '***********',\n      },\n    },\n  },\n}\n```\n更多参数可以查看 [cos-nodejs-sdk-v5](https://github.com/tencentyun/cos-nodejs-sdk-v5) 文档。\n\n\n## 使用 COS 服务\n\n\n我们可以在任意的代码中注入使用。\n```typescript\nimport { Provide, Controller, Inject, Get } from '@midwayjs/core';\nimport { COSService } from '@midwayjs/cos';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  cosService: COSService;\n\n  async invoke() {\n    await this.cosService.sliceUploadFile({\n      Bucket: 'test-1250000000',\n      Region: 'ap-guangzhou',\n      Key: '1.zip',\n      FilePath: './1.zip'\n    },\n  }\n}\n```\n\n\n可以使用 `COSServiceFactory` 获取不同的实例。\n```typescript\nimport { COSServiceFactory } from '@midwayjs/cos';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  cosServiceFactory: COSServiceFactory;\n\n  async save() {\n    const cos1 = await this.cosServiceFactory.get('instance1');\n    const cos2 = await this.cosServiceFactory.get('instance3');\n\n    //...\n\n  }\n}\n```\n\n","isInternal":false,"tokens":734,"sizeBytes":2506},{"name":"cron.md","path":"site/docs/extensions/cron.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/cron.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# 本地任务\n\n和 bull 组件不同，cron 组件提供的是本地任务能力，即在每台机器的每个进程都会执行。如需不同机器或者不同进程之间只执行一次任务，请使用 [bull 组件](./bull) 。\n\n\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n\n\n## 安装组件\n\n```bash\n$ npm i @midwayjs/cron@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/cron\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 使用组件\n\n将组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as cron from '@midwayjs/cron';\n\n@Configuration({\n  imports: [\n    // ...\n    cron\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n\n\n## 编写任务处理类\n\n使用 `@Job` 装饰器装饰一个类，用于快速定义一个任务处理器。\n\n比如，在 `src/job` 目录中创建一个 `sync.job.ts`，用于某些数据同步任务，代码如下：\n\n```typescript\n// src/job/sync.job.ts\nimport { Job, IJob } from '@midwayjs/cron';\nimport { FORMAT } from '@midwayjs/core';\n\n@Job({\n  cronTime: FORMAT.CRONTAB.EVERY_PER_30_MINUTE,\n  start: true,\n})\nexport class DataSyncCheckerJob implements IJob {\n  async onTick() {\n    // ...\n  }\n}\n```\n\n`@Job` 装饰器用于修饰一个任务类，在初始化时，框架会自动将其转变为一个任务。\n\n任务类需要实现 `IJob` 接口，实现 `onTick` 方法，每当任务触发时，会自动调用 `onTick` 方法。\n\n此外，还有一个可选的 `onComplete` 方法，用于在 `onTick` 完成后执行。\n\n```typescript\n@Job({\n  cronTime: FORMAT.CRONTAB.EVERY_PER_30_MINUTE,\n  start: true,\n})\nexport class DataSyncCheckerJob implements IJob {\n  async onTick() {\n    // ...\n  }\n  \n  async onComplete() {\n    // 记录一些数据等等，用处不是很大\n  }\n}\n```\n\n\n\n`@Job` 装饰器的常用参数如下：\n\n| 参数      | 类型    | 描述                   |\n| --------- | ------- | ---------------------- |\n| cronTime  | string  | crontab 表达式         |\n| start     | boolean | 是否自动启动任务       |\n| runOnInit | boolean | 是否在初始化就执行一次 |\n\n更多参数，请参考 [Cron](https://github.com/kelektiv/node-cron)。\n\n\n\n## 任务管理\n\n除了定时执行任务，我们还通过框架提供的 API，对任务进行手动管理。\n\n比如，下面的代码仅仅定义了一个任务，但是不会启动执行。\n\n```typescript\n@Job('syncJob', {\n  cronTime: '*/2 * * * * *',\t// 每隔 2s 执行\n})\nexport class DataSyncCheckerJob implements IJob {\n  async onTick() {\n    // ...\n  }\n}\n```\n\n我们定义了一个名为 `syncJob` 的任务，并且给它了一个默认的调度时间。\n\n\n\n### 获取任务对象\n\n我们可以通过两种方式获取任务对象。\n\n通过`@InjectJob` 用来注入某个任务，参数为类本身或者任务名。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as cron from '@midwayjs/cron';\nimport { InjectJob, CronJob } from '@midwayjs/cron';\nimport { DataSyncCheckerJob } from './job/sync.job';\n\n@Configuration({\n  imports: [\n    cron\n  ],\n})\nexport class ContainerConfiguration {\n  @InjectJob(DataSyncCheckerJob)\n  syncJob: CronJob;\n  \n  @InjectJob('syncJob')\n  syncJob2: CronJob;\n\n  async onServerReady() {\n    // this.syncJob  === this.syncJob2\n  }\n}\n\n```\n\n通过 Framework API 获取。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as cron from '@midwayjs/cron';\nimport { InjectJob, CronJob } from '@midwayjs/cron';\nimport { DataSyncCheckerJob } from './job/sync.job';\n\n@Configuration({\n  imports: [\n    cron\n  ],\n})\nexport class ContainerConfiguration {\n  @Inject()\n  cronFramework: cron.Framework;\n\n  async onServerReady() {\n    const syncJob = this.cronFramework.getJob(DataSyncCheckerJob);\n    const syncJob2 = this.cronFramework.getJob('syncJob');\n    \n    // syncJob  === syncJob2\n  }\n}\n\n```\n\n:::caution\n\n注意，任务对象都必须在 `onServerReady` 生命周期或者启动之后才能获取。\n\n:::\n\n\n\n### 启停任务\n\n我们可以在初始化或者某些程序执行完成之后，将这个任务启动。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as cron from '@midwayjs/cron';\nimport { InjectJob, CronJob } from '@midwayjs/cron';\nimport { DataSyncCheckerJob } from './job/sync.job';\n\n@Configuration({\n  imports: [\n    cron\n  ],\n})\nexport class ContainerConfiguration {\n  @InjectJob(DataSyncCheckerJob)\n  syncJob: CronJob;\n\n  async onServerReady() {\n    this.syncJob.start();\n    \n    // ...\n    this.syncJob.stop();\n  }\n}\n\n```\n\n\n\n## 上下文\n\n任务执行是在请求作用域中，其有着特殊的 Context 对象结构。\n\n```typescript\nexport interface Context extends IMidwayContext {\n  job: CronJob;\n}\n```\n\n这里的  `CronJob` 类型来自于 [Cron](https://github.com/kelektiv/node-cron) 包。\n\n\n\n## 组件日志\n\n组件有着自己的日志，默认会将 `ctx.logger` 记录在 `midway-cron.log` 中。\n\n我们可以单独配置这个 logger 对象。\n\n```typescript\nexport default {\n  midwayLogger: {\n    // ...\n    clients: {\n      // ...\n      cronLogger: {\n        fileLogName: 'midway-cron.log',\n      },\n    }\n  }\n}\n```\n\n\n\n## 全局配置\n\n可以针对 Job 进行一些全局配置，会和每个 Job 的配置进行合并。\n\n```typescript\nexport default {\n  cron: {\n    defaultCronJobOptions: {\n      // ...\n    }\n  }\n}\n```\n\n这里的 `defaultCronJobOptions` 配置项请参考 [CronJobParameters](https://github.com/kelektiv/node-cron/blob/main/lib/job.js#L51)\n","isInternal":false,"tokens":1701,"sizeBytes":5869},{"name":"cross_domain.md","path":"site/docs/extensions/cross_domain.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/cross_domain.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# 跨域\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的通用跨域组件，支持 `cors` 、`jsonp` 多种模式。\n\n相关信息：\n\n| web 支持情况      |      |\n| ----------------- | ---- |\n| @midwayjs/koa     | ✅    |\n| @midwayjs/faas    | ✅    |\n| @midwayjs/web     | ✅    |\n| @midwayjs/express | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/cross-domain --save\n```\n\n## 引入组件\n\n在 `src/configuration.ts` 中引入组件。\n\n```typescript\nimport * as crossDomain from '@midwayjs/cross-domain';\n@Configuration({\n  imports: [\n    // ...other components\n    crossDomain\n  ],\n})\nexport class MainConfiguration {}\n```\n\n\n\n## 什么是跨域\n\n假设有两个网站：\n\n- **A.com**：这是你的网站，你想要从这里访问一些资源。\n- **B.com**：这是另一个网站，它拥有你想要访问的资源。\n\n### 场景设定\n\n1. **A.com** 是你的主网站，你在这里运行一些 JavaScript 代码。\n2. **B.com** 拥有一些 API 接口，你想要通过 A.com 的 JavaScript 代码来调用这些 API 接口。\n\n由于同源策略，浏览器默认不允许 A.com 的 JavaScript 代码直接访问 B.com 的资源。这是因为浏览器出于安全考虑，防止恶意网站读取其他网站的敏感数据。你想要在 A.com 上运行的 JavaScript 代码中发起一个请求到 B.com 的 API 接口，这个请求会被视为跨域请求。\n\n简单来说，从 A.com 访问 B.com 的接口，就是跨域。\n\n此外，跨域还有一些条件：\n\n* 1、同源策略是浏览器安全机制的一部分，所以一般只有浏览器访问才有跨域问题\n* 2、当浏览器发起跨域请求时，它会根据当前源自动添加一个 `Origin` 头部到请求中，服务端也会根据 `origin` 头判断来源，进而做处理\n\n所以，当你发现跨域设置不生效时，请检查：\n\n* 1、是否真的是跨域请求\n* 2、是否真的从浏览器发起\n* 3、是否请求带有 `origin` 头\n\n\n\n## 什么是 CORS\n\n在前端代码调用后端服务中经常会碰到下面的错误，这就最为常见的跨域 [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) 错误。\n\n```\nAccess to fetch at 'http://127.0.0.1:7002/' from origin 'http://127.0.0.1:7001' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.\n```\n\n出于安全性，浏览器限制脚本内发起的跨源 HTTP 请求。例如，`XMLHttpRequest` 和 [Fetch API](https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API) 遵循[同源策略](https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy)。这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源，除非响应报文包含了正确 CORS 响应头。\n\nCORS 机制允许 Web 应用服务器进行跨源访问控制，从而使跨源数据传输得以安全进行。现代浏览器支持在 API 容器中（例如 [`XMLHttpRequest`](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest) 或 [Fetch](https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API)）使用 CORS，以降低跨源 HTTP 请求所带来的风险。\n\n\n\n## 常见的 CORS 配置\n\n下面列举几种跨域的解决方案，我们以 `fetch` 方法为例。\n\n### 未使用 `credentials` \n\n客户端。\n\n```javascript\nfetch(url);\n```\n\n服务端配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  cors: {\n    origin: '*',\n  },\n}\n```\n\n### 使用 `credentials`\n\n客户端。\n\n```javascript\nfetch(url, {\n  credentials: \"include\",\n});\n```\n\n服务端配置\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  cors: {\n    credentials: true,\n  },\n}\n```\n\n### 限制 `origin` 来源\n\n假如我们的网页地址为 `http://127.0.0.1:7001` 而接口为 `http://127.0.0.1:7002`。\n\n客户端。\n\n```javascript\nfetch('http://127.0.0.1:7002/', {\n  credentials: 'include'\n})\n```\n\n服务端配置，注意，由于启用了 `credentials`，这个时候 `origin` 字段不能为 `*`。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  cors: {\n    origin: 'http://127.0.0.1:7001',\n    credentials: true,\n  },\n}\n```\n\n\n\n## 更多 CORS 配置\n\n完整可用配置如下：\n\n```typescript\nexport const cors = {\n  // 允许跨域的方法，【默认值】为 GET,HEAD,PUT,POST,DELETE,PATCH\n  allowMethods: string |string[];\n  // 设置 Access-Control-Allow-Origin 的值，【默认值】会获取请求头上的 origin\n  // 也可以配置为一个回调方法，传入的参数为 request，需要返回 origin 值\n  // 例如：http://test.midwayjs.org\n  // 如果设置了 credentials，则 origin 不能设置为 *\n  origin: string|Function;\n  // 设置 Access-Control-Allow-Headers 的值，【默认值】会获取请求头上的 Access-Control-Request-Headers\n  allowHeaders: string |string[];\n  // 设置 Access-Control-Expose-Headers 的值\n  exposeHeaders: string |string[];\n  // 设置 Access-Control-Allow-Credentials，【默认值】false\n   // 也可以配置为一个回调方法，传入的参数为 request，返回值为 true 或 false\n  credentials: boolean|Function;\n  // 是否在执行报错的时候，把跨域的 header 信息写入到 error 对的 headers 属性中，【默认值】false\n  keepHeadersOnError: boolean;\n  // 设置 Access-Control-Max-Age\n  maxAge: number;\n}\n```\n\n\n## JSONP 配置\n\n可以在 `src/config/config.default` 中进行 JSONP 配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  jsonp: {\n    callback: 'jsonp',\n    limit: 512,\n  },\n}\n```\n","isInternal":false,"tokens":1804,"sizeBytes":5773},{"name":"crud.md","path":"site/docs/extensions/crud.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/crud.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# CRUD\n\n本文档介绍如何在 Midway 中使用 `@midwayjs/crud`。\n\n`@midwayjs/crud` 不是一个“只能自动生成接口”的脚手架，它本质上提供的是一套**面向资源的标准 CRUD 能力**：\n\n- 统一的增删改查 service 抽象\n- 统一的分页 / 排序 / 过滤 / 搜索协议\n- 可选的 REST 路由快捷生成\n- 和现有 validation / swagger / web 路由体系的接入\n\n如果你经常在不同模块里重复写下面这些代码：\n\n- `findAndCount`\n- `save`\n- `update`\n- `delete`\n- 手动解析 `page`、`limit`、`sort`\n- 为每个资源写一套几乎一样的 Controller\n\n那这个组件就是用来把这些重复劳动收起来的。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n## 这个组件能做什么\n\n先用一句话概括：\n\n**先提供一个可复用的 CRUD service，再按需把它暴露成 HTTP 接口。**\n\n这意味着它支持两种使用方式：\n\n1. 只把它当数据访问层能力来用  \n   适合你的业务逻辑比较复杂，不想让组件替你生成接口。\n\n2. 把它当接口快捷层来用  \n   适合资源型接口很多，希望快速拿到统一的 REST API。\n\n所以它解决的是“资源型接口的重复代码”问题，但**不会替代你的业务 service**。\n\n复杂业务依然应该写在你自己的 service 里，例如：\n\n- 下单时要检查库存、优惠券、支付状态\n- 创建用户时要同步多个系统\n- 删除资源前要做权限和状态机校验\n\n这些逻辑应该继续在你的业务 service 中完成，CRUD 组件只是提供一个稳定的资源操作基座。\n\n## 核心概念\n\n在开始之前，先理解这三个层次。\n\n### 1. `CrudService<T>`\n\n这是最核心的抽象，定义了统一的资源操作接口。\n\n```ts\nlist(query)\nfindOne(id)\ncreate(data)\nupdate(id, data)\ndelete(id)\n```\n\n你可以把它理解成“一个标准化的资源仓储服务接口”。\n\n### 2. 数据库适配层\n\nMidway CRUD 目前提供了 4 个官方适配基类：\n\n- `TypeOrmCrudService<T>`\n- `MikroCrudService<T>`\n- `SequelizeCrudService<T>`\n- `MongooseCrudService<T>`\n\n它们都实现了相同的 CRUD 核心接口，但底层分别接不同的数据访问组件。\n\n### 3. HTTP 暴露层\n\n这是可选的。\n\n如果你希望快速生成路由，可以使用：\n\n- 类式：`@Crud()`\n- 函数式：`defineCrudRoutes()`\n\n这两种方式都只是把同一个 `CrudService` 暴露成 HTTP 接口，不会生成第二套独立逻辑。\n\n## 什么时候适合用\n\n适合：\n\n- 大量资源型接口，结构相似\n- 列表查询都需要统一分页、排序、过滤\n- 想减少重复的 Repository / Model 调用代码\n- 想让不同模块的 API 风格保持一致\n\n不适合：\n\n- 主要是复杂工作流，不是标准资源接口\n- 一个接口需要跨多个聚合、多个事务、多个外部系统\n- 你希望所有查询语义都完全自由，不接受统一约束\n\n如果一个模块的核心不是“资源管理”，而是“复杂业务流程”，那更适合直接写普通 Controller + Service，而不是强行套 CRUD。\n\n## 安装依赖\n\n先安装 CRUD 组件本身：\n\n```bash\n$ npm i @midwayjs/crud@4 --save\n```\n\n然后根据你使用的数据库组件安装对应依赖。\n\n```bash\n# TypeORM\n$ npm i @midwayjs/typeorm@4 typeorm@^1.0.0 --save\n\n# MikroORM\n$ npm i @midwayjs/mikro@4 @mikro-orm/core@^6 --save\n\n# Sequelize\n$ npm i @midwayjs/sequelize@4 sequelize sequelize-typescript --save\n\n# Mongoose\n$ npm i @midwayjs/mongoose@4 mongoose --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/crud\": \"^4.0.0\",\n    \"@midwayjs/typeorm\": \"^4.0.0\",\n    \"@midwayjs/mikro\": \"^4.0.0\",\n    \"@midwayjs/sequelize\": \"^4.0.0\",\n    \"@midwayjs/mongoose\": \"^4.0.0\",\n    \"typeorm\": \"^1.0.0\",\n    \"@mikro-orm/core\": \"^6.4.5\",\n    \"sequelize\": \"^6.37.5\",\n    \"sequelize-typescript\": \"^2.1.6\",\n    \"mongoose\": \"^8.9.5\"\n  }\n}\n```\n\n## 启用组件\n\n在 `src/configuration.ts` 中引入组件。\n\n下面以 `koa + typeorm + crud` 为例：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as typeorm from '@midwayjs/typeorm';\nimport * as crud from '@midwayjs/crud';\n\n@Configuration({\n  imports: [\n    koa,\n    typeorm,\n    crud,\n  ],\n})\nexport class MainConfiguration {}\n```\n\n如果你使用其他数据库组件，只需要把对应组件加到 `imports` 中即可。\n\n## 入门：先从 service-only 开始\n\n对于新手来说，最容易理解的方式不是先生成路由，而是先把它当一个“可复用的数据服务”。\n\n这也是推荐的理解顺序。\n\n### 为什么推荐先学 service-only\n\n因为这样你能先理解：\n\n- 组件真正提供的核心是什么\n- 业务代码应该写在哪里\n- 路由层只是一个可选外壳\n\n如果一上来就只看 `@Crud()`，很容易误以为这只是一个自动生成接口的装饰器。\n\n### 最小 TypeORM 示例\n\n先定义一个资源 service：\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { TypeOrmCrudService } from '@midwayjs/crud/typeorm';\nimport { Repository } from 'typeorm';\n\nimport { UserEntity } from '../entity/user';\n\n@Provide()\nexport class UserCrudService extends TypeOrmCrudService<UserEntity> {\n  @InjectEntityModel(UserEntity)\n  repo: Repository<UserEntity>;\n}\n```\n\n然后在业务 service 里直接组合它：\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  userCrudService: UserCrudService;\n\n  async listUsers() {\n    return this.userCrudService.list({\n      page: 1,\n      limit: 20,\n      sort: [],\n      filters: [],\n    });\n  }\n\n  async createUser(input: CreateUserDTO) {\n    return this.userCrudService.create(input);\n  }\n}\n```\n\n这时候：\n\n- 你已经获得统一的 CRUD 能力\n- 但**不会自动生成任何 HTTP 路由**\n- 业务仍然由你自己的 `UserService` 负责组织\n\n这也是最适合复杂业务场景的用法。\n\n## 其他数据库适配\n\n如果你的项目不是 TypeORM，也可以使用其他官方适配。\n\n### MikroORM\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectRepository } from '@midwayjs/mikro';\nimport { MikroCrudService } from '@midwayjs/crud/mikro';\n\n@Provide()\nexport class UserCrudService extends MikroCrudService<UserEntity> {\n  @InjectRepository(UserEntity)\n  repo;\n}\n```\n\n### Sequelize\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectRepository } from '@midwayjs/sequelize';\nimport { SequelizeCrudService } from '@midwayjs/crud/sequelize';\nimport { Repository } from 'sequelize-typescript';\n\n@Provide()\nexport class UserCrudService extends SequelizeCrudService<UserModel> {\n  @InjectRepository(UserModel)\n  repo: Repository<UserModel>;\n}\n```\n\n### Mongoose\n\n```typescript\nimport { Inject, Provide } from '@midwayjs/core';\nimport { MongooseDataSourceManager } from '@midwayjs/mongoose';\nimport { MongooseCrudService } from '@midwayjs/crud/mongoose';\n\n@Provide()\nexport class UserCrudService extends MongooseCrudService<UserDocument> {\n  @Inject()\n  mongooseDataSourceManager: MongooseDataSourceManager;\n\n  async onReady() {\n    this.repo = this.mongooseDataSourceManager\n      .getDataSource('default')\n      .model('User');\n  }\n}\n```\n\n这几个适配基类的对外用法保持一致，区别主要在于底层仓储注入方式和 ORM 行为。\n\n## 快速生成类式 REST 接口\n\n当你已经有一个 `CrudService` 之后，如果希望快速生成标准资源型接口，可以使用 `@Crud()`。\n\n### 最小类式示例\n\n```typescript\nimport { Controller, Inject } from '@midwayjs/core';\nimport { Crud } from '@midwayjs/crud';\n\nimport { UserEntity } from '../entity/user';\nimport { UserCrudService } from '../service/user.crud';\n\n@Controller('/users')\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n})\nexport class UserController {\n  @Inject()\n  crudService: UserCrudService;\n}\n```\n\n默认会生成：\n\n- `GET /users`\n- `GET /users/:id`\n- `POST /users`\n- `PATCH /users/:id`\n- `DELETE /users/:id`\n\n### 为什么还要 `@Inject() crudService`\n\n因为 `@Crud()` 只是声明“这个 Controller 是一个 CRUD 资源”，真正执行业务的是你绑定的 `crudService`。\n\n也就是说：\n\n- `@Crud()` 负责生成默认路由\n- `crudService` 负责真正执行 CRUD 逻辑\n\n这是这个组件最重要的设计原则之一。\n\n### 业务逻辑写在哪里\n\n不要把复杂业务逻辑塞到 `@Crud()` 里。\n\n推荐做法是：\n\n- 资源级默认行为：写在 `UserCrudService`\n- 复杂业务编排：写在你自己的 `UserService` / Domain Service\n- 非标准动作：写成普通路由方法\n\n例如：\n\n```typescript\n@Controller('/users')\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n})\nexport class UserController {\n  @Inject()\n  crudService: UserCrudService;\n\n  async create() {\n    // 自定义事务、调用多个 service、做额外校验\n  }\n\n  async resetPassword() {\n    // 非标准资源动作\n  }\n}\n```\n\n同名方法会优先使用你手写的实现，因此你可以只覆写部分默认行为。\n\n### 裁剪默认路由\n\n如果你不想暴露所有默认路由，可以通过 `routes.only` 或 `routes.exclude` 控制。\n\n```typescript\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n  routes: {\n    only: ['list', 'detail', 'create'],\n  },\n})\n```\n\n这对于“只能查、不能删”或“只开放后台管理的一部分动作”的场景很有用。\n\n## 函数式路由模式\n\n如果项目使用函数式 API，而不是类式 Controller，可以从 `@midwayjs/crud/functional` 导入 `defineCrudRoutes()`。\n\n### 最小示例\n\n```typescript\nimport { defineApi } from '@midwayjs/core/functional';\nimport { defineCrudRoutes } from '@midwayjs/crud/functional';\n\nimport { UserEntity } from '../entity/user';\nimport { UserCrudService } from '../service/user.crud';\n\nconst crudRoutes = defineCrudRoutes<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n});\n\nexport default defineApi('/users', api => ({\n  ...crudRoutes(api),\n}));\n```\n\n### 和自定义动作一起使用\n\n函数式模式最常见的用法，是把默认 CRUD 路由和自定义动作合并在同一个 `defineApi()` 里。\n\n```typescript\nexport default defineApi('/users', api => ({\n  ...crudRoutes(api),\n  resetPassword: api\n    .post('/:id/reset-password')\n    .handle(async ({ input, ctx }) => {\n      return { ok: true };\n    }),\n}));\n```\n\n这样可以让“标准资源动作”和“业务动作”共存在同一个资源路由下。\n\n## 查询协议\n\n列表接口使用统一的查询协议，这是这个组件最重要的能力之一。\n\n### 支持的 query 参数\n\n- `page=<number>`\n- `limit=<number>`\n- `sort=<field>:<ASC|DESC>`，可重复传入\n- `filter=<field>||<operator>||<value>`，可重复传入\n- `search=<keyword>`\n- `join=<relation>`，可重复传入\n- `fields=<field1,field2,...>`\n\n例如：\n\n```text\nGET /users?page=1&limit=20&sort=createdAt:DESC&filter=status||eq||active&search=harry&join=profile\n```\n\n### 支持的过滤操作符\n\n首阶段支持：\n\n- `eq`\n- `ne`\n- `gt`\n- `gte`\n- `lt`\n- `lte`\n- `in`\n- `like`\n\n### 查询协议的约束\n\n这些约束是有意设计出来的，用来保证资源接口的一致性：\n\n- `sort` 字段必须在 `sortable` 白名单里\n- `filter` 字段必须在 `filterable` 白名单里\n- `search` 只会作用于 `searchable` 白名单\n- `join` 必须在 `join` 白名单里\n- `join` 首阶段只支持一层关系名，不支持 `profile.company`\n\n也就是说，这个组件不会让客户端随意拼接任意字段查询，而是让你在服务端声明资源允许暴露的查询能力。\n\n### 在 `@Crud()` 中声明查询能力\n\n```typescript\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n  query: {\n    defaultLimit: 20,\n    maxLimit: 100,\n    sortable: ['id', 'createdAt'],\n    filterable: ['status'],\n    searchable: ['name', 'email'],\n    join: ['profile'],\n  },\n})\n```\n\n这样配置后，查询行为就会按这个白名单执行。\n\n## 返回结构\n\n默认列表接口会返回统一的分页结构，而不是直接返回数组。\n\n```typescript\ntype CrudPageResult<T> = {\n  data: T[];\n  meta: {\n    page: number;\n    limit: number;\n    total: number;\n    pageCount: number;\n    hasNext: boolean;\n    hasPrev: boolean;\n  };\n};\n```\n\n这能让前端和不同资源接口之间保持一致的分页消费方式。\n\n其他默认返回规则：\n\n- `detail` 返回单个资源对象\n- `create` 返回创建后的资源对象\n- `update` 返回更新后的资源对象\n- `delete` 默认返回 `204 No Content`\n\n## DTO、Validation 与 Swagger\n\nCRUD 路由会尽量复用现有 Midway 组件，而不是另起一套协议。\n\n### DTO 绑定\n\n你可以在 `@Crud()` 中声明：\n\n- `dto.create`\n- `dto.update`\n- `dto.replace`\n- `dto.query`\n\n例如：\n\n```typescript\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n  dto: {\n    create: CreateUserDTO,\n    update: UpdateUserDTO,\n    query: UserQueryDTO,\n  },\n})\n```\n\n### Validation\n\n如果项目里启用了 `@midwayjs/validation` 或兼容的 validation service：\n\n- `dto.create` 会用于创建请求体校验\n- `dto.update` 会用于更新请求体校验\n- `dto.replace` 会用于替换请求体校验\n- `dto.query` 会用于列表查询参数校验\n\n如果没有安装 validation 组件，CRUD 本身不会强行报错，而是只跳过这一步自动校验。\n\n### Swagger\n\n自动生成的 CRUD 路由会复用现有 Web 元数据链，所以 Swagger 组件可以识别到这些路由。\n\n也就是说：\n\n- 默认路由会出现在 Swagger 文档里\n- 基础的 path / query / body / response 元数据会被自动补齐\n\n这意味着你不需要为每一个简单资源接口手动补一遍同样的 swagger 装饰器。\n\n## 软删除\n\n默认删除行为是**硬删除**。\n\n如果你希望某个资源使用软删除，需要显式开启：\n\n```typescript\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n  delete: {\n    mode: 'soft',\n  },\n})\n```\n\n开启后：\n\n- `DELETE /:id` 会走软删除\n- `list` / `detail` 默认排除已软删的数据\n- 如果底层实体或仓储不支持软删除，会直接报错\n\n### 为什么不是默认软删除\n\n因为软删除不是所有资源都适合：\n\n- 会影响唯一键约束\n- 会影响查询逻辑\n- 会影响索引设计\n- 会影响后台数据管理\n\n所以这里采用的是“显式开启”的策略，而不是默认偷偷帮你切换成软删。\n\n## 一个更完整的类式示例\n\n下面给一个稍完整一点的例子，把前面的内容串起来。\n\n```typescript\nimport { Controller, Inject, Provide } from '@midwayjs/core';\nimport { Crud } from '@midwayjs/crud';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { TypeOrmCrudService } from '@midwayjs/crud/typeorm';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class UserCrudService extends TypeOrmCrudService<UserEntity> {\n  @InjectEntityModel(UserEntity)\n  repo: Repository<UserEntity>;\n}\n\n@Controller('/users')\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n  dto: {\n    create: CreateUserDTO,\n    update: UpdateUserDTO,\n    query: UserQueryDTO,\n  },\n  query: {\n    defaultLimit: 20,\n    maxLimit: 100,\n    sortable: ['id', 'createdAt'],\n    filterable: ['status'],\n    searchable: ['name', 'email'],\n    join: ['profile'],\n  },\n  delete: {\n    mode: 'soft',\n  },\n})\nexport class UserController {\n  @Inject()\n  crudService: UserCrudService;\n}\n```\n\n这段代码的效果是：\n\n- 生成一个 `/users` 资源接口\n- 自动获得标准 CRUD 路由\n- 自动使用统一查询协议\n- 自动按 DTO 做校验（如果启用了 validation）\n- 自动被 swagger 扫描\n- 删除逻辑使用软删除\n\n## 二级导出\n\n组件提供这些稳定入口：\n\n- `@midwayjs/crud`\n- `@midwayjs/crud/typeorm`\n- `@midwayjs/crud/mikro`\n- `@midwayjs/crud/sequelize`\n- `@midwayjs/crud/mongoose`\n- `@midwayjs/crud/functional`\n\n建议这样理解：\n\n- 主入口：核心类型 + `@Crud()`\n- 数据库二级入口：各自的 CRUD service 适配\n- `functional`：函数式路由适配\n\n## 最后的建议\n\n如果你是第一次接触这个组件，建议按这个顺序使用：\n\n1. 先学 `service-only`，把它当一个标准 CRUD service\n2. 再用 `@Crud()` 快速展开简单资源接口\n3. 最后再用函数式模式或高级查询配置\n\n这样更容易理解它的边界，也更不容易把复杂业务错误地塞进自动 CRUD 层。\n","isInternal":false,"tokens":5288,"sizeBytes":16751},{"name":"egg.md","path":"site/docs/extensions/egg.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/egg.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# EggJS\n\nMidway 可以使用 EggJS 作为上层 Web 框架，EggJS 提供了非常多常用的插件和 API，帮助用户快速构建企业级 Web 应用。本章节内容，主要介绍 EggJS 在 Midway 中如何使用自身的能力。\n\n| 描述              |      |\n| ----------------- | ---- |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/web@4 egg --save\n$ npm i @midwayjs/egg-ts-helper --save-dev\n```\n\n针对 EggJS 场景，这些包列举如下。\n\n```json\n  \"dependencies\": {\n    \"@midwayjs/web\": \"^4.0.0\",\n    \"@midwayjs/core\": \"^4.0.0\",\n    \"egg\": \"^2.0.0\",\n    \"egg-scripts\": \"^2.10.0\"\n  },\n  \"devDependencies\": {\n    \"@midwayjs/egg-ts-helper\": \"^1.0.1\",\n  },\n```\n\n| @midwayjs/web           | **必须**，Midway EggJS 适配层    |\n|-------------------------|----------------------------|\n| @midwayjs/core          | **必须**，Midway 核心包          |\n| egg                     | **必须**，EggJS 依赖包，提供定义等其他能力 |\n| egg-scripts             | **可选**，EggJS 启动脚本          |\n| @midwayjs/egg-ts-helper | **可选**，EggJS 定义生成工具        |\n\n也可以直接使用脚手架创建示例。\n\n```bash\n# npm v6\n$ npm init midway --type=egg-v3 my_project\n\n# npm v7\n$ npm init midway -- --type=egg-v3 my_project\n```\n\n\n\n## 开启组件\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as web from '@midwayjs/web';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [web],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: web.Application;\n\n  async onReady() {\n\t\t// ...\n  }\n}\n\n```\n\n\n\n## 和默认 EggJS 的不同之处\n\n\n- 1、从 v3 开始，midway 提供了更多的组件，大部分 egg 内置插件默认禁用\n- 2、baseDir 默认调整为 `src` 目录，服务器上为 `dist` 目录\n- 3、禁用 egg-logger，全部替换为 @midwayjs/logger，不可切换\n\n\n\n整个架构如下:\n![](https://cdn.nlark.com/yuque/0/2021/png/501408/1614842824740-fc0c1432-3ace-4f77-b51f-15212984b168.png)\n\n\n## 目录结构\n\n\n除了 Midway 提供的目录结构外，EggJS 还有一些特殊的目录结构（不可变），整个结构如下。\n```\n➜  my_midway_app tree\n.\n├── src\n|   ├── app.ts \t\t\t\t\t\t\t\t\t\t \t\t\t\t## EggJS 扩展 Worker 生命周期文件（可选）\n|   ├── agent.ts\t\t\t\t\t\t\t\t   \t\t\t\t## EggJS 扩展 Agent 生命周期文件（可选）\n|   ├── app\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t## EggJS 固定的根目录（可选）\n|   │   ├── public\t\t \t\t\t\t\t\t\t\t  \t## EggJS 静态托管插件的默认目录（可配）\n|   │   |   └── reset.css\n|   │   ├── view (可选)\t\t\t\t\t\t\t\t\t\t ## EggJS 模板渲染的默认目录（可配）\n|   │   |   └── home.tpl\n|   │   └── extend (可选)\t\t\t\t\t\t\t\t\t ## EggJS 扩展目录（可配）\n|   │       ├── helper.ts (可选)\n|   │       ├── request.ts (可选)\n|   │       ├── response.ts (可选)\n|   │       ├── context.ts (可选)\n|   │       ├── application.ts (可选)\n|   │       └── agent.ts (可选)\n|   │\n|   ├── config\n|   |   ├── plugin.ts\n|   |   ├── config.default.ts\n|   |   ├── config.prod.ts\n|   |   ├── config.test.ts (可选)\n|   |   ├── config.local.ts (可选)\n|   |   └── config.unittest.ts (可选)\n│   ├── controller\t\t\t\t\t\t\t\t \t\t\t\t## Midway 控制器目录（推荐）\n│   ├── service\t\t\t\t\t\t\t\t     \t\t\t\t## Midway 服务目录（推荐）\n│   └── schedule\t\t\t\t\t\t\t\t\t \t\t\t\t## Midway 定时器目录（推荐）\n│\n├── typings                        \t\t\t\t## EggJS 定义生成目录\n├── test\n├── package.json\n└── tsconfig.json\n```\n以上是 EggJS 的目录结构全貌，其中包含了很多 EggJS 特有的目录，有一些在 Midway 体系中已经有相应的能力替代，可以直接替换。整个结构，基本上等价于将 EggJS 的目录结构移动到了 `src` 目录下。\n\n\n由于 EggJS 是基于约定的框架，整个工程的目录结构是固定的，这里列举一些常用的约定目录。\n\n| `src/app/public/**`  | 用于放置静态资源，可选，具体参见内置插件 [egg-static](https://github.com/eggjs/egg-static)。 |\n| --- | --- |\n| `src/config/config.{env}.ts` | 用于编写配置文件，具体参见[配置](https://eggjs.org/zh-cn/basics/config.html)。 |\n| `src/config/plugin.js` | 用于配置需要加载的插件，具体参见[插件](https://eggjs.org/zh-cn/basics/plugin.html)。 |\n| `test/**` | 具体参见[单元测试](https://eggjs.org/zh-cn/core/unittest.html)。 |\n| `src/app.js` 和 `src/agent.js` | 用于自定义启动时的初始化工作，可选，具体参见[启动自定义](https://eggjs.org/zh-cn/basics/app-start.html)。关于`agent.js`的作用参见[Agent机制](https://eggjs.org/zh-cn/core/cluster-and-ipc.html#agent-%E6%9C%BA%E5%88%B6)。 |\n\n\n\n## 配置定义\n\n\nMidway 在脚手架中提供了标准的 EggJS 的 TS 配置写法，MidwayConfig 中包括了 egg 中配置的定义和属性提示，结构如下。\n```typescript\n// src/config/config.default.ts\nimport { MidwayConfig, MidwayAppInfo } from '@midwayjs/core';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // use for cookie sign key, should change to your own and keep security\n    keys: appInfo.name + '_xxxx',\n    egg: {\n      port: 7001,\n    },\n    // security: {\n    //   csrf: false,\n    // },\n  } as MidwayConfig;\n};\n\n```\n通过这样返回方法的形式，在运行期会被自动执行，合并进完整的配置对象。\n\n\n这个函数的参数为 `MidwayAppConfig` 类型，值为以下内容。\n\n| **appInfo** | **说明** |\n| --- | --- |\n| pkg | package.json |\n| name | 应用名，同 pkg.name |\n| baseDir | 应用代码的 src （本地开发）或者 dist （上线后）目录 |\n| appDir | 应用代码的目录 |\n| HOME | 用户目录，如 admin 账户为 /home/admin |\n| root | 应用根目录，只有在 local 和 unittest 环境下为 baseDir，其他都为 HOME。 |\n\n\n\n:::info\n注意，这里的 `baseDir` 和 `appDir` 和 EggJS 应用有所区别。\n:::\n\n\n\n\n## 使用 Egg 插件\n\n插件是 EggJS 的特色之一，`@midwayjs/web` 也支持 EggJS 的插件体系，但是在有 Midway 组件的情况下，尽可能优先使用 Midway 组件。\n\n\n插件一般通过 npm 模块的方式进行复用。\n```bash\n$ npm i egg-mysql --save\n```\n然后需要在应用或框架的 `src/config/plugin.js` 中声明开启。\n\n\n如果有 `export default` ，请写在其中。\n```typescript\nimport { EggPlugin } from 'egg';\nexport default {\n  static: false, // default is true\n  mysql: {\n    enable: true,\n    package: 'egg-mysql'\n  }\n} as EggPlugin;\n\n```\n如果没有 `export default` ，可以直接导出。\n```typescript\n// src/config/plugin.ts\n// 使用 mysql 插件\nexport const mysql = {\n  enable: true,\n  package: 'egg-mysql',\n};\n```\n\n\n在开启插件之后，我们就可以在业务代码中使用插件提供的功能了。一般来说，插件会将对象挂载到 EggJS 的 `app` 和 `ctx` 之上，然后直接使用。\n\n\n```typescript\napp.mysql.query(sql, values);\t\t\t// egg 提供的方法\n```\n在 Midway 中可以通过 `@App` 获取 `app` 对象，以及在请求作用域中通过 `@Inject() ctx` 获取 `ctx` 对象，所以我们可以通过注入来获取插件对象。\n\n\n```typescript\nimport { Provide, Inject, Get } from '@midwayjs/core';\nimport { Application, Context } from '@midwayjs/web';\n\n@Provide()\nexport class HomeController {\n\n  @App()\n  app: Application;\n\n  @Inject()\n  ctx: Context;\n\n  @Get('/')\n  async home() {\n  \tthis.app.mysql.query(sql, values);    // 调用 app 上的方法（如果有的话）\n    this.ctx.mysql.query(sql, values);\t\t// 调用挂载在 ctx 上的方法（如果有的话）\n  }\n}\n```\n此外，还可以通过 `@Plugin` 装饰器来直接注入 `app` 挂载的插件，默认情况下，如果不传参数，将以属性名作为 key。\n\n\n```typescript\nimport { Provide, Get, Plugin } from '@midwayjs/core';\n\n@Provide()\nexport class HomeController {\n\n  @Plugin()\n  mysql: any;\n\n  @Get('/')\n  async home() {\n  \tthis.mysql.query(sql, values);\n  }\n}\n```\n:::info\n`@Plugin() mysql`  等价于 `app.mysql` 。 `@Plugin` 的作用就是从 app 对象上拿对应属性名的插件，所以 `@Plugin() xxx` 就等于 `app['xxx']` 。\n:::\n\n\n## Web 中间件\n\n\n中间件样例如下：\n\n\n```typescript\nimport { Middleware, IMiddleware } from '@midwayjs/core';\nimport { Context, NextFunction } from '@midwayjs/web';\n\n@Middleware()\nexport class ReportMiddleware implements IMiddleware<Context, NextFunction> {\n\n  resolve() {\n    return async (ctx: Context, next: NextFunction) => {\n      const startTime = Date.now();\n      await next();\n      console.log(Date.now() - startTime);\n    };\n  }\n\n}\n```\n\n:::caution\n注意\n\n1、如果要继续使用 EggJS 传统的函数式写法，必须将文件放在 `src/app/middleware` 下\n\n2、egg 自带的内置中间件已经集成\n\n:::\n\n应用中间件。\n\n```typescript\n// src/configuration.ts\nimport { App, Configuration } from '@midwayjs/core';\nimport * as egg from '@midwayjs/web';\nimport { ReportMiddleware } from './middleware/user.middleware';\n\n@Configuration({\n  imports: [egg]\n  // ...\n})\nexport class MainConfiguration {\n\n  @App()\n  app: egg.Application;\n\n  async onReady() {\n    this.app.useMiddleware(ReportMiddleware);\n  }\n}\n\n```\n\n更多用法请参考 [Web 中间件](../middleware)\n\n\n\n## 中间件顺序\n\n由于 egg 也有自己的中间件逻辑，在新版本中，我们将中间件加载顺序做了一定的处理，执行顺序如下：\n\n- 1、egg 框架中的中间件\n- 2、egg 插件通过 config.coreMiddleware 添加的顺序\n- 3、业务代码配置在 config.middleware 中配置的顺序\n- 4、app.useMiddleware 添加的顺序\n\n因为 midway 的中间件会后置加载，所以我们可以在 onReady 中进行自定义排序。\n\n\n\n## BodyParser\n\negg 自带  `bodyParser` 功能，默认会解析 `Post` 请求，自动识别 `json` 和 `form` 类型。\n\n如需 text 或者 xml，可以自行配置。\n\n默认的大小限制为 `1mb`，可以单独对每项配置大小。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  bodyParser: {\n    formLimit: '1mb',\n    jsonLimit: '1mb',\n    textLimit: '1mb',\n    xmlLimit: '1mb',\n  },\n}\n```\n\n注意，使用 Postman 做 Post 请求时的类型选择：\n\n![postman](https://img.alicdn.com/imgextra/i4/O1CN01QCdTsN1S347SuzZU5_!!6000000002190-2-tps-1017-690.png)\n\n\n\n\n## 定时任务\nv3 开始请参考 [bull 组件](./bull) 。\n\n如需兼容之前的 [egg 定时任务](https://eggjs.org/zh-cn/basics/schedule.html) ，请照下列方法。\n\n首先安装 `midway-schedule` 依赖。\n\n```bash\n$ npm i midway-schedule --save\n```\n\n添加到插件中即可。\n\n```typescript\n// src/config/plugin.ts\nexport default {\n  schedule: true,\n  schedulePlus: {\n    enable: true,\n    package: 'midway-schedule',\n  },\n};\n```\n\n使用请参考上一版本文档。\n\n\n\n## 日志\n\nv3 开始无法使用 egg-logger，请参考 [日志](../logger) 章节。\n\n\n\n## 异常处理\n\nEggJS 框架通过 [onerror](https://github.com/eggjs/egg-onerror) 插件提供了统一的错误处理机制，会作为 Midway 的兜底错误逻辑，和 [错误过滤器](../error_filter) 不冲突。\n\n对一个请求的所有处理方法（Middleware、Controller、Service）中抛出的任何异常都会被它捕获，并自动根据请求想要获取的类型返回不同类型的错误（基于 [Content Negotiation](https://tools.ietf.org/html/rfc7231#section-5.3.2)）。\n\n\n\n| 请求需求的格式 | 环境 | errorPageUrl 是否配置 | 返回内容 |\n| --- | --- | --- | --- |\n| HTML & TEXT | local & unittest | - | onerror 自带的错误页面，展示详细的错误信息 |\n| HTML & TEXT | 其他 | 是 | 重定向到 errorPageUrl |\n| HTML & TEXT | 其他 | 否 | onerror 自带的没有错误信息的简单错误页（不推荐） |\n| JSON & JSONP | local & unittest | - | JSON 对象或对应的 JSONP 格式响应，带详细的错误信息 |\n| JSON & JSONP | 其他 | - | JSON 对象或对应的 JSONP 格式响应，不带详细的错误信息 |\n\n\n\n\nonerror 插件的配置中支持 errorPageUrl 属性，当配置了 errorPageUrl 时，一旦用户请求线上应用的 HTML 页面异常，就会重定向到这个地址。\n\n\n在 `src/config/config.default.ts` 中\n```typescript\n// src/config/config.default.ts\nmodule.exports = {\n  onerror: {\n    // 线上页面发生异常时，重定向到这个页面上\n    errorPageUrl: '/50x.html',\n  },\n};\n```\n\n\n\n## 扩展 Application/Context/Request/Response\n\n\n### 增加扩展逻辑\n\n\n虽然 MidwayJS 并不希望直接将属性挂载到 koa 的 Context，App 上（会造成管理和定义的不确定性），但是 EggJS 的这项功能依旧可用。\n\n\n文件位置如下。\n```\n➜  my_midway_app tree\n.\n├── src\n│   ├── app\n│   │   └── extend\n│   │       ├── application.ts\n│   │       ├── context.ts\n│   │       ├── request.ts\n│   │       └── response.ts\n│   ├── config\n│   └── interface.ts\n├── test\n├── package.json\n└── tsconfig.json\n```\n内容和原来的 EggJS 相同。\n```typescript\n// src/app/extend/context.ts\nexport default {\n  get hello() {\n    return 'hello world';\n  },\n};\n```\n### 增加扩展定义\n\nContext 请使用 Midway 的方式来扩展，请查看 [扩展上下文定义](/docs/context_definition)。\n\n\n其余的部分，沿用 egg 的方式，请在 `src/interface.ts` 中扩展。\n```typescript\n// src/interface.ts\ndeclare module 'egg' {\n  interface Request {\n    // ...\n  }\n  interface Response {\n    // ...\n  }\n  interface Application {\n    // ...\n  }\n}\n```\n:::info\n业务自定义扩展的定义请 **不要放在根目录** `typings` 下，避免被 ts-helper 工具覆盖掉。\n:::\n\n\n\n## 使用 egg-scripts 部署\n\n由于 EggJS 提供了默认的多进程部署工具 `egg-scripts` ，Midway 也继续支持这种方式，如果上层是 EggJS，推荐这种部署方式。\n\n首先在依赖中，确保安装 `egg-scripts` 包。\n\n```bash\n$ npm i egg-scripts --save\n```\n\n\n\n添加 `npm scripts` 到 `package.json`：\n\n在上面的代码构建之后，使用我们的 `start` 和 `stop` 命令即可完成启动和停止。\n\n```json\n\"scripts\": {\n    \"start\": \"egg-scripts start --daemon --title=********* --framework=@midwayjs/web\",\n    \"stop\": \"egg-scripts stop --title=*********\",\n}\n```\n\n\n\n:::info\n\n`*********` 的地方是你的项目名。\n:::\n\n> 注意：`egg-scripts` 对 Windows 系统的支持有限，参见 [#22](https://github.com/eggjs/egg-scripts/pull/22)。\n\n####\n\n**启动参数**\n\n```bash\n$ egg-scripts start --port=7001 --daemon --title=egg-server-showcase\n```\n\nCopy\n\n如上示例，支持以下参数：\n\n- `--port=7001` 端口号，默认会读取环境变量 process.env.PORT，如未传递将使用框架内置端口 7001。\n- `--daemon` 是否允许在后台模式，无需 nohup。若使用 Docker 建议直接前台运行。\n- `--env=prod` 框架运行环境，默认会读取环境变量 process.env.EGG_SERVER_ENV， 如未传递将使用框架内置环境 prod。\n- `--workers=2` 框架 worker 线程数，默认会创建和 CPU 核数相当的 app worker 数，可以充分的利用 CPU 资源。\n- `--title=egg-server-showcase` 用于方便 ps 进程时 grep 用，默认为 `egg-server-\\${appname}`。\n- `--framework=yadan` 如果应用使用了[自定义框架](https://eggjs.org/zh-cn/advanced/framework.html)，可以配置 package.json 的 egg.framework 或指定该参数。\n- `--ignore-stderr` 忽略启动期的报错。\n- `--https.key` 指定 HTTPS 所需密钥文件的完整路径。\n- `--https.cert` 指定 HTTPS 所需证书文件的完整路径。\n- 所有 [egg-cluster](https://github.com/eggjs/egg-cluster) 的 Options 都支持透传，如 --port 等。\n\n更多参数可查看 [egg-scripts](https://github.com/eggjs/egg-scripts) 和 [egg-cluster](https://github.com/eggjs/egg-cluster) 文档。\n\n:::info\n\n使用 egg-scripts 部署的日志会存放在 **用户目录** 下**，**比如 `/home/xxxx/logs` 。\n\n:::\n\n\n\n## 启动环境\n\n原有 egg 使用 `EGG_SERVER_ENV` 中作为环境标志，在 Midway 中请使用 `MIDWAY_SERVER_ENV`。\n\n\n\n## State 类型定义\n\n在 egg 底层的 koa 的 Context 中有一个特殊的 State 属性，通过和 Context 类似的方式可以扩展 State 定义。\n\n```typescript\n// src/interface.ts\n\ndeclare module '@midwayjs/web/dist/interface' {\n  interface Context {\n    abc: string;\n  }\n\n  interface State{\n    bbb: string;\n    ccc: number;\n  }\n}\n```\n\n\n\n## 配置\n\n### 默认配置\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  egg: {\n    port: 7001,\n  },\n}\n```\n\n`@midwayjs/web`  所有参数如下：\n\n| 配置项         | 类型             | 描述                         |\n| -------------- | ---------------- | ---------------------------- |\n| port           | number           | 必填，启动的端口             |\n| key            | string           | Buffer                       |\n| cert           | string           | Buffer                       |\n| ca             | string           | Buffer                       |\n| hostname       | string           | 监听的 hostname，默认 127.1  |\n| http2          | boolean          | 可选，http2 支持，默认 false |\n| queryParseMode | simple\\|extended | 默认为 extended              |\n| queryParseOptions | `qs.IParseOptions` | 解析选项，当使用'simple'模式解析时可用 |\n\n以上的属性，对本地和使用 `bootstrap.js` 部署的应用生效。\n\n\n\n### 修改端口\n\n:::tip\n\n注意，这个方式只会对本地研发，以及使用 bootstrap.js 文件部署的项目生效。\n\n:::\n\n默认情况下，我们在 `config.default` 提供了 `7001` 的默认端口参数，修改它就可以修改 egg http 服务的默认端口。\n\n比如我们修改为 `6001`：\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  egg: {\n    port: 6001,\n  },\n}\n```\n\n默认情况下，单测环境由于需要 supertest 来启动端口，我们的 port 配置为 `null`。\n\n```typescript\n// src/config/config.unittest\nexport default {\n  // ...\n  egg: {\n    port: null,\n  },\n}\n```\n\n此外，也可以通过 `midway-bin dev --ts --port=6001` 的方式来临时修改端口，此方法会覆盖配置中的端口。\n\n\n\n### 全局前缀\n\n此功能请参考 [全局前缀](../controller#全局路由前缀)。\n\n\n\n### Https 配置\n\n在大多数的情况，请尽可能使用外部代理的方式来完成 Https 的实现，比如 Nginx。\n\n在一些特殊场景下，你可以通过配置 SSL 证书（TLS 证书）的方式，来直接开启 Https。\n\n首先，你需要提前准备好证书文件，比如 `ssl.key` 和 `ssl.pem`，key 为服务端私钥，pem 为对应的证书。\n\n然后配置即可。\n\n```typescript\n// src/config/config.default\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  egg: {\n    key: join(__dirname, '../ssl/ssl.key'),\n    cert: join(__dirname, '../ssl/ssl.pem'),\n  },\n}\n```\n\n\n\n### favicon 设置\n\n默认情况下，浏览器会发起一个 `favicon.ico` 的请求。\n\n```typescript\n// src/config/config.default\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  siteFile: {\n    '/favicon.ico': readFileSync(join(__dirname, 'favicon.png')),\n  },\n}\n```\n\n\n\n如果开启了 `@midwayjs/static-file` 组件，那么会优先使用组件的静态文件托管。\n\n### 修改上下文日志\n\n可以单独修改 egg 框架的上下文日志。\n\n```typescript\nexport default {\n  egg: {\n    contextLoggerFormat: info => {\n      const ctx = info.ctx;\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} [${ctx.userId} - ${Date.now() - ctx.startTime}ms ${ctx.method}] ${info.message}`;\n    }\n    // ...\n  },\n};\n```\n\n\n\n### Query 数组解析\n\n默认情况下，`ctx.query` 会解析为忽略数组的情况，而 `ctx.queries` 会严格的将所有的字段都变成数组。\n\n如果调整 `queryParseMode` ，则可以使 `ctx.query` 变为两者之间的结构（querystring 的结果）。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  egg: {\n    // ...\n    queryParseMode: 'simple',\n    queryParseOptions: {\n      arrayLimit: 100,\n    },\n  },\n}\n```\n\n\n\n\n## 常见问题\n### 1、生成 ts 定义\n\n\nMidway 提供了 `@midwayjs/egg-ts-hepler` 工具包，用于快速生成 EggJS 开发时所依赖的定义。\n```bash\n$ npm install @midwayjs/egg-ts-helper --save-dev\n```\n在 `package.json` 中加入对应的 `ets` 命令即可，一般来说，我们会在 dev 命令前加入，以保证代码的正确性。\n```json\n  \"scripts\": {\n    \"dev\": \"cross-env ets && cross-env NODE_ENV=local midway-bin dev --ts\",\n  },\n```\n:::info\n在第一次编写代码前，需要执行一次此命令才能有 ts 定义生成。\n:::\n\n\nEggJS 生成的定义在 `typings` 目录中。\n```\n➜  my_midway_app tree\n.\n├── src                            ## midway 项目源码\n├── typings                        ## EggJS 定义生成目录\n├── test\n├── package.json\n└── tsconfig.json\n```\n\n\n### 2、EggJS 中 Configuration 的特殊情况\n\n\n在 EggJS 下， `configuration.ts`  中的生命周期**只会在 worker 下加载执行**。如果在 Agent 有类似的需求，请直接使用 EggJS 自身的 `agent.ts` 处理。\n\n\n\n### 3、异步初始化配置无法覆盖插件配置\n\n`onConfigLoad` 生命周期会在 egg 插件（若有）初始化之后执行，所以不能用于覆盖 egg 插件所使用的配置。\n\n\n\n### 4、默认的 csrf 错误\n\n\n在 post 请求，特别是第一次时用户会发现一个 csrf 报错。原因是 egg 在框架中默认内置了安全插件 [egg-security](https://github.com/eggjs/egg-security)， 默认开启了  csrf 校验。\n\n\n我们可以在配置中关闭它，但是更好的是去[**了解它**](https://eggjs.org/zh-cn/core/security.html#%E5%AE%89%E5%85%A8%E5%A8%81%E8%83%81-csrf-%E7%9A%84%E9%98%B2%E8%8C%83)之后再做选择。\n\n```typescript\nexport const security = {\n  csrf: false,\n};\n```\n\n\n\n\n### 5、不存在定义的问题\n\n一些 egg 插件未提供 ts 定义，导致使用会出现未声明方法的情况，比如 egg-mysql。\n![image.png](https://img.alicdn.com/imgextra/i1/O1CN01mv68zG1zN6nALff8n_!!6000000006701-2-tps-1478-876.png)\n可以使用 any 绕过。\n\n```typescript\nawait (this.app as any).mysql.query(sql);\n```\n\n或者可以自行增加扩展定义。\n\n### 6、获取 Http Server\n\nEggjs 内部封装了原始的 HttpServer，需要通过事件获取。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, App } from '@midwayjs/core';\nimport { Application } from '@midwayjs/web';\n\n@Configuration(/***/)\nexport class MainConfiguration {\n  \n  @App('egg')\n  app: Application;\n  \n  // ...\n  async onServerReady() {\n    this.app.once('server', (server) => {\n      // ...\n    })\n  }\n}\n```\n\n","isInternal":false,"tokens":7034,"sizeBytes":23058},{"name":"etcd.md","path":"site/docs/extensions/etcd.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/etcd.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# ETCD\n\netcd 是云原生架构中重要的基础组件，由 CNCF 孵化托管。etcd 在微服务和 Kubernetes 集群中可以作为服务注册于发现，也可以作为 key-value 存储的中间件。\n\nMidway 提供基于 [etcd3](https://github.com/microsoft/etcd3) 模块封装的组件，提供 etcd 的客户端调用能力。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/etcd@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/etcd\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n\n## 引入组件\n\n\n首先，引入 组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as etcd from '@midwayjs/etcd';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    etcd,\n  ],\n  // ...\n})\nexport class MainConfiguration {\n}\n```\n\n\n\n## 配置默认客户端\n\n大部分情况下，我们可以只使用默认客户端来完成功能。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  etcd: {\n    client: {\n      host: [\n        '127.0.0.1:2379'\n      ]\n    },\n  },\n}\n```\n\n\n\n## 使用默认客户端\n\n配置完成后，我们就可以在代码中使用了。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { ETCDService } from '@midwayjs/etcd';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  etcdService: ETCDService;\n\n  async invoke() {\n\n    await this.etcdService.put('foo').value('bar');\n\n    const fooValue = await this.etcdService.get('foo').string();\n    console.log('foo was:', fooValue);\n\n    const allFValues = await this.etcdService.getAll().prefix('f').keys();\n    console.log('all our keys starting with \"f\":', allFValues);\n\n    await this.etcdService.delete().all();\n  }\n}\n```\n\n更多 API 请参考 ts 定义或者 [官网文档](https://microsoft.github.io/etcd3/classes/etcd3.html)。\n\n\n\n## 多实例配置\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  etcd: {\n    clients: {\n      instance1: {\n        {\n          host: [\n            '127.0.0.1:2379'\n          ]\n        },\n      },\n  \t\tinstance2: {\n        {\n          host: [\n            '127.0.0.1:2379'\n          ]\n        },\n      }\n    }\n  },\n}\n```\n\n\n\n## 多实例获取\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { ETCDServiceFactory } from '@midwayjs/etcd';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  etcdServiceFactory: ETCDServiceFactory;\n\n  async invoke() {\n    const instance1 = this.etcdServiceFactory.get('instance1');\n    // ...\n\n    const instance2 = this.etcdServiceFactory.get('instance2');\n    // ...\n  }\n}\n```\n\n\n\n## 动态创建实例\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { ETCDServiceFactory } from '@midwayjs/etcd';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  etcdServiceFactory: ETCDServiceFactory;\n\n  async invoke() {\n    const instance3 = await this.etcdServiceFactory.createInstance({\n      host: [\n        '127.0.0.1:2379'\n      ]\n    }, 'instance3');\n    // ...\n  }\n}\n```\n\n\n\n## 服务发现\n\n从 v4 开始提供基于统一抽象的服务发现能力，虽然 ETCD 并不太适合作为服务发现的注册中心来使用，但是我们仍实现了 ETCD 作为服务发现的客户端能力。\n\n服务发现包括 **注册服务** 和 **获取服务** 两部分。\n\n\n### 配置服务发现\n\n你需要先配置一个客户端；若存在多个客户端，可通过 `serviceDiscoveryClient` 指定用于服务发现的客户端。\n\n通过 `serviceDiscovery` 配置服务发现选项：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  etcd: {\n    clients: {\n      default: { hosts: ['127.0.0.1:2379'] }\n    },\n    serviceDiscovery: {\n      namespace: 'services',\n      ttl: 30,\n      // 可选：loadBalancer: LoadBalancerType.ROUND_ROBIN\n    }\n  }\n}\n```\n\n说明：`namespace` 为键空间前缀，实例键结构为 `namespace/serviceName/instanceId`；`ttl` 为实例过期时间（秒）。\n\n\n### 注册服务发现\n\n服务启动后，将当前服务注册到 ETCD：\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport { EtcdServiceDiscovery } from '@midwayjs/etcd';\n\n@Configuration({})\nexport class MainConfiguration {\n  @Inject()\n  etcdDiscovery: EtcdServiceDiscovery;\n\n  async onServerReady() {\n    // 创建服务发现客户端（可传入覆盖项）\n    const client = this.etcdDiscovery.createClient();\n\n    // 注册当前服务并上线（写入键并绑定租约）\n    await client.register({\n      id: client.defaultMeta.id,\n      serviceName: 'order',\n      ttl: 30,\n      meta: {\n        version: '1.0.0',\n        host: client.defaultMeta.host,\n        port: '7001'\n      }\n    });\n  }\n}\n```\n\n说明：注册时会默认上线，无需显式调用 `online`。ETCD 通过 Lease 维持 TTL，定时续约保证存活；`offline` 删除键并撤销租约；`deregister` 注销并清理状态。重复调用 `register/online/offline/deregister` 具有幂等性。\n\n\n### 获取可用服务\n\n在任意位置获取可用实例或按负载均衡选择一个实例：\n\n````typescript\n@Provide()\nexport class OrderService {\n  @Inject()\n  etcdDiscovery: EtcdServiceDiscovery;\n\n  async getService() {\n    const instances = await this.etcdDiscovery.getInstances('order');\n    const one = await this.etcdDiscovery.getInstance('order');\n    return { instances, one };\n  }\n}\n````\n\n返回的实例为注册时写入的对象（`EtcdInstanceMetadata`），通常包含 `serviceName/id/ttl/meta/tags/status` 等字段。\n","isInternal":false,"tokens":1708,"sizeBytes":5900},{"name":"events.md","path":"site/docs/extensions/events.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/events.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# 事件\n\n事件组件是基于 [eventemitter2](https://github.com/EventEmitter2/EventEmitter2) 实现的，提供了强大的事件处理能力。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/event-emitter@4 --save\n```\n\n或者在 `package.json` 中增加依赖后，重新安装\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/event-emitter\": \"^4.0.0\"\n  }\n}\n```\n\n## 使用组件\n\n首先在 `configuration.ts` 中引入组件：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as eventEmitter from '@midwayjs/event-emitter';\n\n@Configuration({\n  imports: [\n    // ...\n    eventEmitter\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n## 基础使用\n\n### 定义事件监听器\n\n:::caution\n注意：`@OnEvent()` 装饰器只能用在单例（Singleton）类上。如果在非单例类上使用，事件监听器将无法正常工作。\n:::\n\n使用 `@OnEvent()` 装饰器来定义事件监听器：\n\n```typescript\nimport { Provide, Singleton } from '@midwayjs/core';\nimport { OnEvent } from '@midwayjs/event-emitter';\n\n@Provide()\n@Singleton()    // 必须使用 @Singleton 装饰器\nexport class UserService {\n  @OnEvent('user.created')\n  async handleUserCreated(user: any) {\n    console.log('新用户创建:', user);\n  }\n}\n```\n\n### 发送事件\n\n通过注入 `EventEmitterService` 来发送事件：\n\n```typescript\nimport { Inject } from '@midwayjs/core';\nimport { EventEmitterService } from '@midwayjs/event-emitter';\n\n@Provide()\nexport class UserController {\n  @Inject()\n  eventEmitterService: EventEmitterService;\n\n  async createUser() {\n    const user = { id: 1, name: 'harry' };\n    // ... 创建用户逻辑\n    \n    // 发送同步事件\n    this.eventEmitterService.emit('user.created', user);\n    \n    // 或者发送异步事件\n    await this.eventEmitterService.emitAsync('user.created', user);\n  }\n}\n```\n\n## 高级特性\n\n### 事件监听器选项\n\n`@OnEvent()` 装饰器支持多个配置选项，每个选项都有其特定的用途：\n\n#### prependListener\n\n用于控制监听器的执行顺序。设置为 `true` 时，会将监听器添加到监听器队列的开头，这样可以确保该监听器最先执行。\n\n```typescript\n@Provide()\n@Singleton()\nexport class UserService {\n  // 这个监听器会最先执行\n  @OnEvent('user.created', { prependListener: true })\n  async handleUserCreatedFirst(user: any) {\n    console.log('第一个处理用户创建事件');\n  }\n\n  // 这个监听器会在队列末尾执行\n  @OnEvent('user.created')\n  async handleUserCreated(user: any) {\n    console.log('后续处理用户创建事件');\n  }\n}\n```\n\n#### suppressErrors\n\n控制事件处理器中的错误处理行为：\n- 默认值为 `true`，表示错误会被捕获并忽略\n- 设置为 `false` 时，错误会被抛出，可以在外层捕获\n\n```typescript\n@Provide()\n@Singleton()\nexport class UserService {\n  // 错误会被抛出\n  @OnEvent('user.created', { suppressErrors: false })\n  async handleWithError(user: any) {\n    throw new Error('处理失败');  // 这个错误会被抛出\n  }\n\n  // 错误会被忽略\n  @OnEvent('user.created', { suppressErrors: true })\n  async handleSuppressError(user: any) {\n    throw new Error('处理失败');  // 这个错误会被忽略\n  }\n}\n\n// 在调用处可以捕获错误\n@Provide()\nexport class UserController {\n  @Inject()\n  eventEmitterService: EventEmitterService;\n\n  async createUser() {\n    try {\n      await this.eventEmitterService.emitAsync('user.created', { id: 1 });\n    } catch (err) {\n      // 可以捕获到 suppressErrors: false 的处理器抛出的错误\n      console.error('事件处理失败:', err);\n    }\n  }\n}\n```\n\n#### async\n\n控制事件处理器的执行方式：\n- 设置为 `true` 时，处理器会在单独的事件循环中异步执行\n- 默认为 `false`，会按顺序同步执行\n\n```typescript\n@Provide()\n@Singleton()\nexport class UserService {\n  // 异步执行，不会阻塞其他处理器\n  @OnEvent('user.created', { async: true })\n  async handleAsync(user: any) {\n    await someTimeConsumingTask();\n    console.log('异步处理完成');\n  }\n\n  // 同步执行，会按顺序等待\n  @OnEvent('user.created')\n  async handleSync(user: any) {\n    console.log('同步处理');\n  }\n}\n```\n\n### 通配符事件\n\n通配符事件允许你使用模式匹配来监听一组相关的事件。需要在配置中启用 `wildcard: true`。\n\n#### 配置启用\n\n```typescript\n// config/config.default.ts\nexport default {\n  eventEmitter: {\n    wildcard: true,  // 启用通配符功能\n  }\n}\n```\n\n#### 使用示例\n\n```typescript\n@Provide()\n@Singleton()\nexport class UserService {\n  // 监听所有用户相关事件\n  @OnEvent('user.*')\n  async handleUserEvents(...args: any[]) {\n    // 注意：通配符模式下，事件处理器直接接收发送事件时传入的参数\n    console.log('事件参数:', args);\n  }\n\n  // 监听多层级的事件\n  @OnEvent('user.*.detail')\n  async handleUserDetailEvents(...args: any[]) {\n    // 可以匹配 user.profile.detail, user.settings.detail 等\n    console.log('事件参数:', args);\n  }\n}\n\n// 发送事件示例\n@Provide()\nexport class UserController {\n  @Inject()\n  eventEmitterService: EventEmitterService;\n\n  async updateUser() {\n    // 这些事件都会被上面的 user.* 处理器捕获\n    await this.eventEmitterService.emitAsync('user.created', { id: 1 });\n    await this.eventEmitterService.emitAsync('user.updated', { id: 1, name: 'new name' });\n    await this.eventEmitterService.emitAsync('user.profile.detail', { id: 1, profile: {} });\n  }\n}\n```\n\n:::info\n注意：在使用通配符事件时，事件处理器的参数直接是事件发送时传入的参数。无法在处理器中获取具体触发的事件名称，所以建议在发送事件时将必要的信息包含在事件参数中。\n:::\n\n### 命名空间事件\n\n命名空间事件提供了一种组织和分组事件的方式。通过 `delimiter` 配置分隔符。\n\n#### 配置启用\n\n```typescript\n// config/config.default.ts\nexport default {\n  eventEmitter: {\n    delimiter: '.' // 设置命名空间分隔符\n  }\n}\n```\n\n#### 使用示例\n\n```typescript\n@Provide()\n@Singleton()\nexport class OrderService {\n  // 监听订单命名空间下的创建事件\n  @OnEvent('order.created')\n  async handleOrderCreated(order: any) {\n    console.log('订单创建:', order);\n  }\n\n  // 如果需要使用通配符匹配，需要额外启用 wildcard: true\n  @OnEvent('order.payment.*')\n  async handleOrderPaymentEvents(...args: any[]) {\n    // 可以匹配 order.payment.success, order.payment.failed 等\n    console.log('事件参数:', args);\n  }\n}\n\n// 发送命名空间事件\n@Provide()\nexport class PaymentService {\n  @Inject()\n  eventEmitterService: EventEmitterService;\n\n  async processPayment() {\n    // 发送支付成功事件\n    await this.eventEmitterService.emitAsync('order.payment.success', {\n      orderId: '123',\n      amount: 100\n    });\n  }\n}\n```\n\n:::info\n注意：命名空间的分隔符默认是 `.`，你可以通过 `delimiter` 配置修改。如果需要使用通配符（如 `*`）进行事件匹配，则需要额外启用 `wildcard: true` 配置。\n:::\n\n## 配置\n\n在 `config.default.ts` 中可以配置 Event Emitter 的行为：\n\n```typescript\nexport default {\n  // ...\n  eventEmitter: {\n    // 是否开启通配符事件\n    wildcard: true,\n    // 命名空间分隔符\n    delimiter: '.',\n    // 是否启用新监听器事件\n    newListener: false,\n    // 是否启用移除监听器事件\n    removeListener: false,\n    // 设置最大监听器数量\n    maxListeners: 10,\n    // 是否启用详细的内存泄漏检测\n    verboseMemoryLeak: false,\n  }\n}\n```\n\n## 常见问题\n\n### 1. 事件处理顺序\n\n- 默认情况下，事件处理器按照注册顺序执行\n- 使用 `prependListener: true` 可以将处理器添加到队列开头\n- 异步事件处理器（`emitAsync`）会按顺序等待每个处理器完成\n\n### 2. 错误处理\n\n- 默认情况下，事件处理器中的错误会被抑制（`suppressErrors: true`）\n- 设置 `suppressErrors: false` 可以让错误抛出\n- 使用 `emitAsync` 时，可以通过 try/catch 捕获错误\n\n### 3. 性能考虑\n\n- 合理使用通配符事件，过多的通配符匹配可能影响性能\n- 设置适当的 `maxListeners` 值，避免内存泄漏\n- 及时移除不需要的事件监听器","isInternal":false,"tokens":2475,"sizeBytes":8569},{"name":"express.md","path":"site/docs/extensions/express.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/express.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# Express\n\n本章节内容，主要介绍在 Midway 中如何使用 Express 作为上层框架，并使用自身的能力。\n\n| 描述           |      |\n| -------------- | ---- |\n| 包含独立主框架 | ✅    |\n| 包含独立日志   | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/express@4 --save\n$ npm i @types/body-parser @types/express @types/express-session --save-dev\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/express\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    \"@types/body-parser\": \"^1.19.2\",\n    \"@types/express\": \"^4.17.13\",\n    \"@types/express-session\": \"^1.17.4\",\n    // ...\n  }\n}\n```\n\n也可以直接使用脚手架创建示例。\n\n```bash\n# npm v6\n$ npm init midway --type=express-v3 my_project\n\n# npm v7\n$ npm init midway -- --type=express-v3 my_project\n```\n\n\n针对 Express，Midway 提供了 `@midwayjs/express` 包进行了适配，在其中提供了 Midway 特有的依赖注入、切面等能力。\n\n:::info\n我们使用的 Express 版本为 `v4` 。\n:::\n\n\n## 目录结构\n```\n.\n├── src\n│   ├── controller\t\t\t\t\t\t\t\t \t\t\t\t# controller接口的地方\n│   ├── service\t\t\t\t\t\t\t\t\t \t\t\t\t\t# service逻辑处理的地方\n|   └── configuration.ts\t\t\t\t\t\t\t\t\t# 入口及生命周期配置、组件管理\n├── test\n├── package.json\n└── tsconfig.json\n```\n\n\n\n## 开启组件\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [express],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: express.Application;\n\n  async onReady() {}\n}\n```\n\n\n\n\n## 控制器（Controller）\n\n\n整个请求控制器的写法和 Midway 适配其他框架的类似。为了和其他场景的框架写法一致，在请求的时候，Midway 将 Express 的 `req` 映射为 `ctx` 对象。\n```typescript\nimport { Inject, Controller, Get, Provide, Query } from '@midwayjs/core';\nimport { Context, NextFunction } from '@midwayjs/express';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  ctx: Context;\n\n  @Get('/')\n  async home(@Query() id) {\n    console.log(id);\t\t\t\t\t\t// req.query.id === id\n    return 'hello world';\t\t\t\t// 简单返回，等价于 res.send('hello world');\n  }\n}\n```\n你也可以额外注入 `req` 和 `res` 。\n```typescript\nimport { Inject, Controller, Get, Provide, Query } from '@midwayjs/core';\nimport { Context, Response, NextFunction } from '@midwayjs/express';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  ctx: Context;   // 即为 req\n\n  @Inject()\n  req: Context;\n\n  @Inject()\n  res: Response;\n\n  @Get('/')\n  async home(@Query() id) {\n    // this.req.query.id === id\n  }\n}\n```\n\n\n\n## Web 中间件\n\n\nExpress 的中间件写法比较特殊，它的参数不同。\n\n\n```typescript\nimport { Middleware } from '@midwayjs/core';\nimport { Context, Response, NextFunction } from '@midwayjs/express';\n\n@Middleware()\nexport class ReportMiddleware implements IMiddleware<Context, Response, NextFunction> {\n\n  resolve() {\n    return async (\n      req: Context,\n      res: Response,\n      next: NextFunction\n    ) => {\n      console.log('Request...');\n      next();\n    };\n  }\n\n}\n```\n\n注意，这里我们导出了一个 `ReportMiddleware` 类，为了方便对接异步流程，`resolve` 返回可以是 async 函数。\n\nExpress 中的 next 方法，用于调用到下一个中间件，指的注意的是，Express 中间件并非洋葱模型，是单向调用。\n\n\n\n\n### 路由中间件\n\n\n我们可以把上面编写的中间件应用到单个 Controller 上，也可以将中间件应用到单个路由上。\n\n\n```typescript\nimport { Controller, Get, Provide } from '@midwayjs/core';\n\n@Controller('/', { middleware: [ ReportMiddleware ]})\t\t\t// controller 级别的中间件\nexport class HomeController {\n\n  @Get('/', { middleware: [ ReportMiddleware ]})\t\t\t\t// 路由级别的中间件\n  async home() {\n    return 'hello world'\n  }\n}\n```\n\n\n### 全局中间件\n\n\n直接使用 Midway 提供的 `app.generateMiddleware` 方法，在入口处加载全局中间件。\n```typescript\n// src/configuration.ts\nimport { Configuration, ILifeCycle } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport { ReportMiddleware } from './middleware/report.middleware.ts'\n\n@Configuration({\n  imports: [express],\n})\nexport class MainConfiguration implements ILifeCycle {\n\n  @App()\n  app: express.Application;\n\n  async onReady() {\n    this.app.useMiddleware(ReportMiddleware);\n  }\n}\n```\n\n\n除了加载 Class 形式的中间件外，也支持加载传统的 Express 中间件。\n```typescript\n// src/configuration.ts\nimport { Configuration, ILifeCycle, App } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [express],\n})\nexport class MainConfiguration implements ILifeCycle {\n\n  @App()\n  app: express.Application;\n\n  async onReady() {\n    this.app.useMiddleware((req, res, next) => {\n    \t// xxx\n    });\n  }\n}\n```\n你可以通过注入 `app` 对象，来调用到所有 Express 上的方法。\n\n\n\n## 返回统一处理\n\n由于 Express 中间件是单向调用，无法在返回时执行，为此我们额外设计了一个 `@Match` 装饰的过滤器，用于处理返回值的行为。\n\n比如，我们可以定义针对全局返回的过滤器。\n\n```typescript\n// src/filter/globalMatch.filter.ts\nimport { Match } from '@midwayjs/core';\nimport { Context, Response } from '@midwayjs/express';\n\n@Match()\nexport class GlobalMatchFilter {\n  match(value, req, res) {\n    // ...\n    return {\n      status: 200,\n      data: {\n        value\n      },\n    };\n  }\n}\n```\n\n也可以匹配特定的路由做返回。\n\n```typescript\n// src/filter/api.filter.ts\nimport { Match } from '@midwayjs/core';\nimport { Context, Response } from '@midwayjs/express';\n\n@Match((ctx: Context, res: Response) => {\n  return ctx.path === '/api';\n})\nexport class APIMatchFilter {\n  match(value, req: Context, res: Response) {\n    // ...\n    return {\n      data: {\n        message:\n        data: value,\n      },\n    };\n  }\n}\n```\n\n需要应用到 app 中。\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport { join } from 'path';\nimport { APIMatchFilter } from './filter/api.filter';\nimport { GlobalMatchFilter } from 'filter/globalMatch.filter';\n\n@Configuration({\n  imports: [express],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: express.Application;\n\n  async onReady() {\n    // ...\n    this.app.useFilter([APIMatchFilter, GlobalMatchFilter]);\n  }\n}\n```\n\n注意，这类过滤器是按照添加的顺序来匹配执行。\n\n\n\n## 错误处理\n\n和普通的项目相同，使用错误过滤器，但是参数略有不同。\n\n```typescript\nimport { Catch } from '@midwayjs/core';\nimport { Context, Response } from '@midwayjs/express';\n\n@Catch()\nexport class GlobalError {\n  catch(err: Error, req: Context, res: Response) {\n    if (err) {\n      return {\n        status: err.status ?? 500,\n        message: err.message,\n      }\n    }\n  }\n}\n```\n\n需要应用到 app 中。\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport { join } from 'path';\nimport { GlobalError } from './filter/global.filter';\n\n@Configuration({\n  imports: [express],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: express.Application;\n\n  async onReady() {\n    this.app.useMiddleware((req, res, next) => {\n      next();\n    });\n\n    this.app.useFilter([GlobalError]);\n  }\n}\n```\n\n注意，`@Match` 和 `@Catch` 都是过滤器，在内部会自动判断做区分执行。。\n\n\n\n## Cookie\n\n`@midwayjs/express` 自带  `cookie parser` 功能，使用的是 `cookie-parser` 模块。\n\n针对 Cookie，统一使用 `keys` 作为秘钥。\n\n```typescript\n// src/config/config.default\nexport default {\n  keys:  ['key1', 'key2'],\n}\n```\n\n获取 Cookie。\n\n```typescript\nconst cookieValue = req.cookies['cookie-key'];\n```\n\n设置 Cookie。\n\n```typescript\nres.cookie(\n  'cookie-key',\n  'cookie-value',\n  cookieOptions\n);\n```\n\n\n\n##  Session\n\n`@midwayjs/express` 内置了 Session 组件，给我们提供了 `ctx.session` 来访问或者修改当前用户 Session 。\n\n默认情况下为 `cookie-session` ，默认配置如下。\n\n```typescript\n// src/config/config.default\nexport default {\n  session:  {\n    name: 'MW_SESS',\n    resave: true,\n    saveUninitialized: true,\n    cookie: {\n      maxAge: 24 * 3600 * 1000, // ms\n      httpOnly: true,\n      // sameSite: null,\n    },\n  }\n}\n```\n\n我们可以通过简单的 API 来设置 session。\n\n```typescript\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  req;\n\n  @Get('/')\n  async get() {\n    // set all\n    this.req.session = req.query;\n\n    // set value\n    this.req.session.key = 'abc';\n\n    // get\n    const key = this.req.session.key;\n\n    // remove\n    this.req.session = null;\n\n    // set max age\n    this.req.session.maxAge = Number(req.query.maxAge);\n\n    // ...\n  }\n}\n\n```\n\n\n\n## BodyParser\n\n`@midwayjs/express` 自带  `bodyParser` 功能，默认会解析 `Post` 请求，自动识别 `json` 、`text`和 `urlencoded` 类型。\n\n默认的大小限制为 `1mb`，可以单独对每项配置大小。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  bodyParser: {\n    json: {\n      enable: true,\n      limit: '1mb',\n      strict: true,\n    },\n    raw: {\n      enable: false,\n      limit: '1mb',\n    },\n    text: {\n      enable: true,\n      limit: '1mb',\n    },\n    urlencoded: {\n      enable: true,\n      extended: false,\n      limit: '1mb',\n      parameterLimit: 1000,\n    },\n  },\n}\n```\n\n\n\n## 配置\n\n### 默认配置\n\n`@midwayjs/express`  的配置样例如下：\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  express: {\n    port: 7001,\n  },\n}\n```\n\n所有属性描述如下：\n\n| 属性         | 类型                                         | 描述                                                    |\n| ------------ |--------------------------------------------| ------------------------------------------------------- |\n| port         | number                                     | 可选，启动的端口                                        |\n| globalPrefix | string                                     | 可选，全局的 http 前缀                                  |\n| keys         | string[]                                   | 可选，Cookies 签名，如果上层未写 keys，也可以在这里设置 |\n| hostname     | string                                     | 可选，监听的 hostname，默认 127.1                       |\n| key          | string \\| Buffer \\| Array\\<Buffer\\|Object> | 可选，Https key，服务端私钥                             |\n| cert         | string \\| Buffer \\| Array\\<Buffer\\|Object> | 可选，Https cert，服务端证书                            |\n| ca           | string \\| Buffer \\| Array\\<Buffer\\|Object> | 可选，Https ca                                          |\n| http2        | boolean                                    | 可选，http2 支持，默认 false                            |\n\n\n\n### 修改端口\n\n默认情况下，我们在 `config.default` 提供了 `7001` 的默认端口参数，修改它就可以修改 Express http 服务的默认端口。\n\n比如我们修改为 `6001`：\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  express: {\n    port: 6001,\n  },\n}\n```\n\n默认情况下，单测环境由于需要 supertest 来启动端口，我们的 port 配置为 `null`。\n\n```typescript\n// src/config/config.unittest\nexport default {\n  // ...\n  express: {\n    port: null,\n  },\n}\n```\n\n此外，也可以通过 `midway-bin dev --ts --port=6001` 的方式来临时修改端口，此方法会覆盖配置中的端口。\n\n\n\n### 全局前缀\n\n此功能请参考 [全局前缀](../controller#全局路由前缀)。\n\n\n\n### Https 配置\n\n在大多数的情况，请尽可能使用外部代理的方式来完成 Https 的实现，比如 Nginx。\n\n在一些特殊场景下，你可以通过配置 SSL 证书（TLS 证书）的方式，来直接开启 Https。\n\n首先，你需要提前准备好证书文件，比如 `ssl.key` 和 `ssl.pem`，key 为服务端私钥，pem 为对应的证书。\n\n然后配置即可。\n\n```typescript\n// src/config/config.default\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  express: {\n    key: join(__dirname, '../ssl/ssl.key'),\n    cert: join(__dirname, '../ssl/ssl.pem'),\n  },\n}\n```\n\n\n\n### 修改上下文日志\n\n可以单独修改 express 框架的上下文日志。\n\n```typescript\nexport default {\n  express: {\n    contextLoggerFormat: info => {\n      // 等价 req\n      const req = info.ctx;\n      const userId = req?.['session']?.['userId'] || '-';\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} [${userId} - ${Date.now() - req.startTime}ms ${req.method}] ${info.message}`;\n    }\n    // ...\n  },\n};\n```\n\n","isInternal":false,"tokens":3564,"sizeBytes":13210},{"name":"grpc.md","path":"site/docs/extensions/grpc.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/grpc.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# gRPC\n\ngRPC 是一个高性能、通用的开源 RPC 框架，其由 Google 主要面向移动应用开发并基于 HTTP/2 协议标准而设计，基于 ProtoBuf(Protocol Buffers) 序列化协议开发，且支持众多开发语言。\n\n\n本篇内容演示了如何在 Midway 体系下，提供 gRPC 服务，以及调用 gRPC 服务的方法。\n\n\nMidway 当前采用了最新的 gRPC 官方推荐的 [@grpc/grpc-js](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js) 进行开发，并提供了一些工具包，用于快速发布服务和调用服务。\n\n我们使用的模块为 `@midwayjs/grpc` ，既可以独立发布服务，又可以接入其它框架调用 gRPC 服务。\n\n相关信息：\n\n**提供服务**\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n\n**调用服务**\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n\n**其他**\n\n| 描述                 |      |\n| -------------------- | ---- |\n| 可作为主框架独立使用 | ✅    |\n| 可独立添加中间件     | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/grpc@4 --save\n$ npm i @midwayjs/grpc-helper --save-dev\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/grpc\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    \"@midwayjs/grpc-helper\": \"^1.0.0\",\n    // ...\n  }\n}\n```\n\n\n\n## 开启组件\n\n:::tip\n\n不管是提供服务还是调用服务，都需要开启组件。\n\n:::\n\n`@midwayjs/grpc` 可以作为独立主框架使用。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as grpc from '@midwayjs/grpc';\n\n@Configuration({\n  imports: [grpc],\n  // ...\n})\nexport class MainConfiguration {\n  async onReady() {\n\t\t// ...\n  }\n}\n\n```\n\n也可以附加在其他的主框架下，比如 `@midwayjs/koa` 。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as grpc from '@midwayjs/grpc';\n\n@Configuration({\n  imports: [koa, grpc],\n  // ...\n})\nexport class MainConfiguration {\n  async onReady() {\n\t\t// ...\n  }\n}\n\n```\n\n\n\n## 目录结构\n\n大致的目录结构如下，`src/provider` 是提供 gRPC 服务的目录。\n\n```\n.\n├── package.json\n├── proto                         ## proto 定义文件\n│   └── helloworld.proto\n├── src\n│   ├── configuration.ts          ## 入口配置文件\n│   ├── interface.ts\n│   └── provider                  ## gRPC 提供服务的文件\n│       └── greeter.ts\n├── test\n├── bootstrap.js                  ## 服务启动入口\n└── tsconfig.json\n```\n\n\n\n## 定义服务接口\n\n\n在微服务中，定义一个服务需要特定的接口定义语言（IDL）来完成，在 gRPC中 默认使用 Protocol Buffers  作为序列化协议。\n\n\n序列化协议独立于语言和平台，提供了多种语言的实现，Java，C++，Go 等等,每一种实现都包含了相应语言的编译器和库文件。所以 gRPC 是一个提供和调用都可以跨语言的服务框架。\n\n一个gRPC服务的大体架构可以用官网上的一幅图表示。\n\n![](https://img.alicdn.com/imgextra/i3/O1CN01kpIyg51k8i5DtcGpZ_!!6000000004639-2-tps-621-445.png)\n\n\nProtocol Buffers  协议的文件，默认的后缀为 `.proto` 。.proto后缀的IDL文件,并通过其编译器生成特定语言的数据结构、服务端接口和客户端Stub代码。\n\n\n:::info\n由于 proto 文件可以跨语言使用，为了方便共享，我们一般将 proto 文件放在 src 目录外侧，方便其他工具复制分发。\n:::\n\n\n下面是一个基础的 `proto/helloworld.proto`  文件。\n```protobuf\nsyntax = \"proto3\";\n\npackage helloworld;\n\nservice Greeter {\n  // Sends a greeting\n  rpc SayHello (HelloRequest) returns (HelloReply) {}\n}\n\nmessage HelloRequest {\n  string name = 1;\n}\n\nmessage HelloReply {\n  string message = 1;\n}\n\n```\n\n\nproto3 表示的是第三版的 protobuf 协议，是 gRPC 目前推荐的版本，“语法简单，功能更全”。\n\n\n我们可以用 `service` 格式，定义服务体，其中可以包含方法。同时，我们可以更加细致的通过 `message` 描述服务具体的请求参数和响应参数。\n\n\n我们可以从 [Google 的官网文档](https://developers.google.com/protocol-buffers/docs/overview#simple) 中查看更多细节。\n\n\n:::info\n大家会看到，这和 Java 中的 Class 非常相像，每个结构就相当于 Java 中的一个类。\n:::\n\n\n### 编写 proto 文件\n\n\n现在我们再来看之前的服务，是不是就很好理解了。\n```protobuf\nsyntax = \"proto3\";\n\npackage helloworld;\n\n// 服务的定义\nservice Greeter {\n  // Sends a greeting\n  rpc SayHello (HelloRequest) returns (HelloReply) {}\n}\n\n// 服务的请求参数\nmessage HelloRequest {\n  string name = 1;\n}\n\n// 服务的响应参数\nmessage HelloReply {\n  string message = 1;\n}\n\n```\n\n\n我们定义了一个名为 `Greeter` 的服务，包含一个 `HelloRequest` 结构的请求体，以及返回 `HelloReply` 结构的响应体。\n\n\n接下去，我们将对这个服务给大家做演示。\n\n\n### 生成代码定义\n\n\n传统的 gRPC 框架，需要用户手动编写 proto 文件，以及生成 js 服务，最后再根据 js 生成的服务再编写实现，在 Midway 体系下，我们提供了一个 grpc-helper 工具包来加速这个过程。\n\n\n如果没有安装，可以先安装。\n```bash\n$ npm i @midwayjs/grpc-helper --save-dev\n```\n\n\ngrpc-helper 工具的作用，是将用户提供的 proto 文件，生成对应可读的 ts interface 文件。\n\n\n我们可以添加一个脚本，方便这个过程。\n```json\n{\n\t\"scripts\": {\n     \"generate\": \"tsproto --path proto --output src/domain\"\n  }\n}\n```\n\n\n然后执行 `npm run generate` 。\n\n\n上述命令执行后，会在代码的 `src/domain` 目录中生成 proto 文件对应的服务接口定义。\n\n\n:::info\n不管是提供 gRPC 服务还是调用 gRPC 服务，都要先生成定义。\n:::\n\n\n生成的代码如下，包含有一个命名空间（namespace），以及命名空间下的两个 TypeScript Interface， `Greeter` 用于编写服务端实现， `GreeterClient` 用于编写客户端实现。\n```typescript\n/**\n* This file is auto-generated by grpc-helper\n*/\n\nimport * as grpc from '@midwayjs/grpc';\n\n// 生成的命名空间\nexport namespace helloworld {\n\n  // 服务端使用的定义\n  export interface Greeter {\n    // Sends a greeting\n    sayHello(data: HelloRequest): Promise<HelloReply>;\n  }\n\n  // 客户端使用的定义\n  export interface GreeterClient {\n    // Sends a greeting\n    sayHello(options?: grpc.IClientOptions): grpc.IClientUnaryService<HelloRequest, HelloReply>;\n  }\n\n  // 请求体结构\n  export interface HelloRequest {\n    name?: string;\n  }\n\n  // 响应体结构\n  export interface HelloReply {\n    message?: string;\n  }\n}\n\n```\n\n\n:::info\n每当 proto 文件被修改时，就需要重新生成对应的服务定义，然后将对应的方法实现。\n:::\n\n\n\n## 提供 gRPC 服务（Provider）\n\n\n### 编写服务提供方（Provider）\n\n\n在 `src/provider` 目录中，我们创建 `greeter.ts` ，内容如下\n```typescript\nimport {\n  MSProviderType,\n  Provider,\n  GrpcMethod,\n} from '@midwayjs/core';\nimport { helloworld } from '../domain/helloworld';\n\n/**\n * 实现 helloworld.Greeter 接口的服务\n */\n@Provider(MSProviderType.GRPC, { package: 'helloworld' })\nexport class Greeter implements helloworld.Greeter {\n\n  @GrpcMethod()\n  async sayHello(request: helloworld.HelloRequest) {\n    return { message: 'Hello ' + request.name };\n  }\n}\n\n```\n:::info\n注意，@Provider 装饰器和 @Provide 装饰器不同，前者用于提供服务，后者用于依赖注入容器扫描标识的类。\n:::\n\n\n我们使用 `@Provider` 暴露出一个 RPC 服务， `@Provider` 的第一个参数为 RPC 服务类型，这个参数是个枚举，这里选择 GRPC 类型。\n\n\n`@Provider` 的第二个参数为 RPC 服务的元数据，这里指代的是 gRPC 服务的元数据。这里需要写入 gRPC 的 package 字段，即 proto 文件中的 package 字段（这里的字段用于和 proto 文件加载后的字段做对应）。\n\n\n对于普通的 gRPC 服务接口（UnaryCall），我们只需要使用 `@GrpcMethod()` 装饰器修饰即可。修饰的方法即为服务定义本身，入参为 proto 中定义好的入参，return 值即为定义好的响应体。\n\n:::info\n注意，生成的 Interface 是为了更好的编写服务代码，规范结构，请务必按照定义编写。\n:::\n\n\n### 配置服务\n\n\n配置内容如下。\n```typescript\n// src/config/config.default\nimport { MidwayAppInfo, MidwayConfig } from '@midwayjs/core';\n\nexport default (appInfo: MidwayAppInfo): MidwayConfig => {\n  return {\n    // ...\n    grpcServer: {\n      services: [\n        {\n          protoPath: join(appInfo.appDir, 'proto/hero.proto'),\n          package: 'hero',\n        },\n        {\n          protoPath: join(appInfo.appDir, 'proto/helloworld.proto'),\n          package: 'helloworld',\n        }\n      ],\n    }\n  };\n}\n```\nservices 字段是数组，意味着 Midway 项目可以同时发布多个 gRPC 服务。每个 service 的结构为：\n\n| 属性      | 类型   | 描述                       |\n| --- | --- | --- |\n| protoPath | string | 必选，proto 文件的绝对路径 |\n| package | string | 必选，服务对应的 package |\n\n除了 Service 配置之外，还有一些其他的配置。\n\n| 属性          | 类型              | 描述                                                         |\n| ------------- | ----------------- | ------------------------------------------------------------ |\n| url           | string            | 可选，gRPC 服务地址，默认 6565 端口，比如 'localhost:6565'   |\n| loaderOptions | Object            | 可选，proto file loader 的 options                           |\n| credentials   | ServerCredentials | 可选，grpc Server binding 时的 credentials 参数选项          |\n| serverOptions | ChannelOptions    | 可选，grpc Server 的 [自定义 options](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js#supported-channel-options) |\n\n\n\n### 提供安全证书\n\n可以通过 `credentials` 参数传递安全证书。\n\n```typescript\n// src/config/config.default\nimport { MidwayAppInfo, MidwayConfig } from '@midwayjs/core';\nimport { ServerCredentials } from '@midwayjs/grpc';\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nconst cert = readFileSync(join(__dirname, './cert/server.crt'));\nconst pem = readFileSync(join(__dirname, './cert/server.pem'));\nconst key = readFileSync(join(__dirname, './cert/server.key'));\n\nexport default (appInfo: MidwayAppInfo): MidwayConfig => {\n  return {\n    // ...\n    grpcServer: {\n      // ...\n      credentials: ServerCredentials.createSsl(cert, [{ private_key: key, cert_chain: pem }]);\n    }\n  };\n}\n```\n\n\n\n### 编写单元测试\n\n`@midwayjs/grpc` 库提供了一个 `createGRPCConsumer` 方法，用于实时调用客户端，一般我们用这个方法做测试。\n\n:::caution\n这个方法每次调用会实时连接，不建议将该方法用在生产环境。\n:::\n\n\n在测试中写法如下。\n```typescript\nimport { createApp, close } from '@midwayjs/mock';\nimport { Framework, createGRPCConsumer } from '@midwayjs/grpc';\nimport { join } from 'path';\nimport { helloworld } from '../src/domain/helloworld';\n\ndescribe('test/index.test.ts', () => {\n\n  it('should create multiple grpc service in one server', async () => {\n    const baseDir = join(__dirname, '../');\n\n    // 创建服务\n    const app = await createApp<Framework>();\n\n    // 调用服务\n    const service = await createGRPCConsumer<helloworld.GreeterClient>({\n      package: 'helloworld',\n      protoPath: join(baseDir, 'proto', 'helloworld.proto'),\n      url: 'localhost:6565'\n    });\n\n    const result = await service.sayHello().sendMessage({\n      name: 'harry'\n    });\n\n    expect(result.message).toEqual('Hello harry');\n    await close(app);\n  });\n\n});\n\n```\n\n\n\n## 调用 gRPC 服务（Consumer）\n\n\n我们编写一个 gRPC 服务来调用上面的暴露的服务。\n\n:::info\n事实上，你可以在 Web 的 Controller，或者 Service 等其他地方来调用，这里只是做一个示例。\n:::\n\n\n### 调用配置\n\n\n你需要在 `src/config/config.default.ts` 中增加你需要调用的目标服务以及它的 proto 文件信息。\n\n\n比如，这里我们填写了上面暴露的服务本身，以及该服务的 proto，包名等信息（函数形式）。\n```typescript\n// src/config/config.default\nimport { MidwayAppInfo, MidwayConfig } from '@midwayjs/core';\n\nexport default (appInfo: MidwayAppInfo): MidwayConfig => {\n  return {\n    // ...\n    grpc: {\n      services: [\n        {\n          url: 'localhost:6565',\n          protoPath: join(appInfo.appDir, 'proto/helloworld.proto'),\n          package: 'helloworld',\n        },\n      ],\n    },\n  };\n}\n```\n\n\n### 代码调用\n\n\n配置完后，我们就可以在代码里调用了。\n\n\n`@midwayjs/grpc` 提供了 `clients` ，可以方便的获取到已配置的服务。我们只需要在需要注入的地方，注入这个对象即可。\n\n\n比如：\n```typescript\nimport {\n  Provide,\n  Inject,\n} from '@midwayjs/core';\nimport { helloworld, hero } from '../interface';\nimport { Clients } from '@midwayjs/grpc';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  grpcClients: Clients;\n\n}\n```\n\n\n我们通过 `clients` 获取到对方服务的客户端实例，然后调用即可。\n\n\n```typescript\nimport {\n  Provide,\n  Inject,\n} from '@midwayjs/core';\nimport { helloworld, hero } from '../interface';\nimport { Clients } from '@midwayjs/grpc';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  grpcClients: Clients;\n\n  async invoke() {\n    // 获取服务\n  \tconst greeterService = this.grpcClients.getService<helloworld.GreeterClient>(\n      'helloworld.Greeter'\n    );\n\n    // 调用服务\n    const result = await greeterService.sayHello()\n    \t.sendMessage({\n        name: 'harry'\n      });\n\n    // 返回结果\n    return result;\n  }\n\n}\n```\n\n\n我们也可以利用 `@Init` 装饰器，将需要调用的服务缓存到属性上。这样可以在其他方法调用时复用。\n\n\n示例如下。\n```typescript\nimport {\n  GrpcMethod,\n  MSProviderType,\n  Provider,\n  Inject,\n  Init,\n} from '@midwayjs/core';\nimport { helloworld, hero } from '../interface';\nimport { Clients } from '@midwayjs/grpc';\n\n@Provider(MSProviderType.GRPC, { package: 'hero' })\nexport class HeroService implements hero.HeroService {\n  // 注入客户端\n  @Inject()\n  grpcClients: Clients;\n\n  greeterService: helloworld.GreeterClient;\n\n  @Init()\n  async init() {\n    // 赋值一个服务实例\n    this.greeterService = this.grpcClients.getService<helloworld.GreeterClient>(\n      'helloworld.Greeter'\n    );\n  }\n\n  @GrpcMethod()\n  async findOne(data) {\n    // 调用服务\n    const result = await greeterService.sayHello()\n    \t.sendMessage({\n        name: 'harry'\n      });\n\n    // 返回结果\n    return result;\n  }\n}\n\n```\n\n\n## 流式服务\n\n\ngRPC 的流式服务用于减少连接，让服务端或者客户端不需要等待即可执行任务，从而提高执行效率。\n\n\ngRPC 的流式服务分为三种，以服务端角度来说，为\n\n\n- 服务端接收流（客户端推）\n- 服务端响应流（服务端推）\n- 双向流\n\n\n\n下面我们将一一介绍。\n\n\n### 流式 proto 文件\n\n\n流式的 proto 文件写法不同，需要在希望使用流式的地方将参数标记为 `stream` 。\n```protobuf\n\nsyntax = \"proto3\";\n\npackage math;\n\nmessage AddArgs {\n  int32 id = 1;\n  int32 num = 2;\n}\n\nmessage Num {\n  int32 id = 1;\n  int32 num = 2;\n}\n\nservice Math {\n  rpc Add (AddArgs) returns (Num) {\n  }\n\n\t// 双向流\n  rpc AddMore (stream AddArgs) returns (stream Num) {\n  }\n\n  // 服务端往客户端推\n  rpc SumMany (AddArgs) returns (stream Num) {\n  }\n\n  // 客户端往服务端推\n  rpc AddMany (stream AddArgs) returns (Num) {\n  }\n}\n\n```\n该服务生成的接口定义为：\n\n\n```typescript\nimport {\n  IClientDuplexStreamService,\n  IClientReadableStreamService,\n  IClientUnaryService,\n  IClientWritableStreamService,\n  IClientOptions,\n} from '@midwayjs/grpc';\n\nexport namespace math {\n  export interface AddArgs {\n    id?: number;\n    num?: number;\n  }\n  export interface Num {\n    id?: number;\n    num?: number;\n  }\n\n  /**\n   * server interface\n   */\n  export interface Math {\n    add(data: AddArgs): Promise<Num>;\n    addMore(data: AddArgs): Promise<void>;\n    // 服务端推，客户端读\n    sumMany(data: AddArgs): Promise<void>\n    // 客户端端推，服务端读\n    addMany(num: AddArgs): Promise<void>;\n  }\n\n  /**\n   * client interface\n   */\n  export interface MathClient {\n    add(options?: IClientOptions): IClientUnaryService<AddArgs, Num>;\n    addMore(options?: IClientOptions): IClientDuplexStreamService<AddArgs, Num>;\n    // 服务端推，客户端读\n    sumMany(options?: IClientOptions): IClientReadableStreamService<AddArgs, Num>;\n    // 客户端端推，服务端读\n    addMany(options?: IClientOptions): IClientWritableStreamService<AddArgs, Num>;\n  }\n}\n\n```\n\n\n### 服务端推送\n\n\n客户端调用一次，服务端可以多次返回。通过 `@GrpcMethod()` 的参数来标识流式类型。\n\n\n可用的类型为:\n\n\n- `GrpcStreamTypeEnum.WRITEABLE`  服务端输出流（单工）\n- `GrpcStreamTypeEnum.READABLE`  客户端输出流（单工），服务端接受多次\n- `GrpcStreamTypeEnum.DUPLEX`  双工流\n\n\n\n服务端示例如下：\n```typescript\nimport { GrpcMethod, GrpcStreamTypeEnum, Inject, MSProviderType, Provider } from '@midwayjs/core';\nimport { Context, Metadata } from '@midwayjs/grpc';\nimport { math } from '../interface';\n\n/**\n */\n@Provider(MSProviderType.GRPC, { package: 'math' })\nexport class Math implements math.Math {\n\n  @Inject()\n  ctx: Context;\n\n  @GrpcMethod({type: GrpcStreamTypeEnum.WRITEABLE })\n  async sumMany(args: math.AddArgs) {\n    this.ctx.write({\n      num: 1 + args.num\n    });\n    this.ctx.write({\n      num: 2 + args.num\n    });\n    this.ctx.write({\n      num: 3 + args.num\n    });\n\n    this.ctx.end();\n  }\n\n  // ...\n}\n\n```\n服务端使用 `ctx.write` 方法来返回数据，由于是服务端流，可以返回多次。\n\n\n返回结束后，请使用 `ctx.end()` 方法关闭流。\n\n\n客户端，调用一次，接受多次数据。\n\n\n比如下面的累加逻辑。\n\n\nPromise 写法，会等待服务端数据都返回再做处理。\n```typescript\n// 服务端推送\nlet total = 0;\nlet result = await service.sumMany().sendMessage({\n  num: 1,\n});\n\nresult.forEach(data => {\n  total += data.num;\n});\n\n// total = 9;\n```\n\n\n事件写法，实时处理。\n```typescript\n// 服务端推送\nlet call = service.sumMany().getCall();\n\ncall.on('data', data => {\n\t// do something\n});\n\ncall.sendMessage({\n  num: 1,\n});\n\n```\n\n\n### 客户端推送\n\n\n客户端调用多次，服务端接收多次数据，返回一个结果。通过 `@GrpcMethod({type: GrpcStreamTypeEnum.READABLE})` 的参数来标识流式类型。\n\n\n服务端示例如下：\n```typescript\nimport { GrpcMethod, GrpcStreamTypeEnum, Inject, MSProviderType, Provider } from '@midwayjs/core';\nimport { Context, Metadata } from '@midwayjs/grpc';\nimport { math } from '../interface';\n\n/**\n */\n@Provider(MSProviderType.GRPC, { package: 'math' })\nexport class Math implements math.Math {\n\n  sumDataList: number[] = [];\n\n  @Inject()\n  ctx: Context;\n\n  @GrpcMethod({type: GrpcStreamTypeEnum.READABLE, onEnd: 'sumEnd' })\n  async addMany(data: math.Num) {\n    this.sumDataList.push(data);\n  }\n\n  async sumEnd(): Promise<math.Num> {\n    const total = this.sumDataList.reduce((pre, cur) => {\n      return {\n        num: pre.num + cur.num,\n      }\n    });\n    return total;\n  }\n\n  // ...\n}\n\n```\n\n\n客户端每次调用，都会触发一次 `addMany` 方法。\n\n\n在客户端发送 `end` 事件之后，会调用 `@GrpcMethod` 装饰器上的 `onEnd` 参数指定的方法，该方法的返回值即为最后客户端拿到的值。\n\n\n客户端示例如下：\n```typescript\n// 客户端推送\nconst data = await service.addMany()\n.sendMessage({num: 1})\n.sendMessage({num: 2})\n.sendMessage({num: 3})\n.end();\n\n// data.num = 6\n```\n\n\n### 双向流\n\n\n客户端可以调用多次，服务端也可以接收多次数据，返回多个结果，类似于传统的 TCP 通信。通过 `@GrpcMethod({type: GrpcStreamTypeEnum.DUPLEX})` 的参数来标识双工流式类型。\n\n\n服务端示例如下：\n```typescript\nimport { GrpcMethod, GrpcStreamTypeEnum, Inject, MSProviderType, Provider } from '@midwayjs/core';\nimport { Context, Metadata } from '@midwayjs/grpc';\nimport { math } from '../interface';\n\n/**\n */\n@Provider(MSProviderType.GRPC, { package: 'math' })\nexport class Math implements math.Math {\n\n  @Inject()\n  ctx: Context;\n\n  @GrpcMethod({type: GrpcStreamTypeEnum.DUPLEX, onEnd: 'duplexEnd' })\n  async addMore(message: math.AddArgs)  {\n    this.ctx.write({\n      id: message.id,\n      num: message.num + 10,\n    });\n  }\n\n  async duplexEnd() {\n    console.log('got client end message');\n  }\n  // ...\n}\n\n```\n服务端可以随时使用 `ctx.write` 返回数据，也可以使用 `ctx.end` 来关闭流。\n\n\n客户端示例：\n\n\n对于双工通信的客户端，由于无法保证调用、返回的顺序，我们需要使用监听的模式来消费结果。\n```typescript\nconst clientStream = service.addMore().getCall();\n\nlet total = 0;\nlet idx = 0;\n\nduplexCall.on('data', (data: math.Num) => {\n  total += data.num;\n  idx++;\n  if (idx === 2) {\n    duplexCall.end();\n    // total => 29\n  }\n});\n\nduplexCall.write({\n  num: 3,\n});\n\nduplexCall.write({\n  num: 6,\n});\n```\n\n\n如果希望保证调用顺序，我们也提供了保证顺序的双向流调用方法，但是需要在 proto 中定义一个固定的 id，来确保顺序。\n\n\n比如我们的 Math.proto，对每个入参和出参，都增加了一个固定的 id，所以可以固定顺序。\n```typescript\n\nsyntax = \"proto3\";\n\npackage math;\n\nmessage AddArgs {\n  int32 id = 1;\t\t\t\t\t\t\t//  这里的 id 名字是固定的\n  int32 num = 2;\n}\n\nmessage Num {\n  int32 id = 1;\t\t\t\t\t\t\t//  这里的 id 名字是固定的\n  int32 num = 2;\n}\n\nservice Math {\n  rpc Add (AddArgs) returns (Num) {\n  }\n\n  rpc AddMore (stream AddArgs) returns (stream Num) {\n  }\n\n  // 服务端往客户端推\n  rpc SumMany (AddArgs) returns (stream Num) {\n  }\n\n  // 客户端往服务端推\n  rpc AddMany (stream AddArgs) returns (Num) {\n  }\n}\n\n```\n固定顺序的客户端调用方式如下：\n```typescript\n// 保证顺序的双向流\nconst t = service.addMore();\n\nconst result4 = await new Promise<number>((resolve, reject) => {\n\n  let total = 0;\n\n  // 第一次调用和返回\n  t.sendMessage({\n    num: 2\n  })\n    .then(res => {\n      expect(res.num).toEqual(12);\n      total += res.num;\n  \t})\n    .catch(err => console.error(err));\n\n  // 第二次调用和返回\n  t.sendMessage({\n    num: 5\n  }).then(res => {\n      expect(res.num).toEqual(15);\n      total += res.num;\n      resolve(total);\n    })\n    .catch(err => console.error(err));\n\n  t.end();\n});\n\n// result4 => 27\n```\n默认的 id 为 `id` ，如果服务端定义不同，需要修改，可以在客户端调用时传递。\n```typescript\n// 保证顺序的双向流\nconst t = service.addMore({\n  messageKey: 'uid'\n});\n```\n\n\n## 元数据（Metadata）\n\n\ngRPC 的元数据等价于 HTTP 的上下文。\n\n\n服务端通过 `ctx.sendMetadata` 方法返回元数据，也可以通过 `ctx.metadata` 获取客户端传递的元数据。\n```typescript\nimport {\n  MSProviderType,\n  Provider,\n  GrpcMethod,\n} from '@midwayjs/core';\nimport { helloworld } from '../domain/helloworld';\nimport { Context, Metadata } from '@midwayjs/grpc';\n\n/**\n * 实现 helloworld.Greeter 接口的服务\n */\n@Provider(MSProviderType.GRPC, { package: 'helloworld' })\nexport class Greeter implements helloworld.Greeter {\n\n  @Inject()\n  ctx: Context;\n\n  @GrpcMethod()\n  async sayHello(request: helloworld.HelloRequest) {\n\n    // 客户端传递的元数据\n    console.log(this.ctx.metadata);\n\n    // 创建元数据\n    const meta = new Metadata();\n    this.ctx.metadata.add('xxx', 'bbb');\n    this.ctx.sendMetadata(meta);\n\n    return { message: 'Hello ' + request.name };\n  }\n}\n```\n\n\n客户端通过方法的 options 参数传递元数据。\n```typescript\nimport { Metadata } from '@midwayjs/grpc';\n\nconst meta = new Metadata();\nmeta.add('key', 'value');\n\nconst result = await service.sayHello({\n  metadata: meta,\n}).sendMessage({\n  name: 'harry'\n});\n```\n\n\n获取元数据相对麻烦一些。\n\n\n普通一元调用（UnaryCall）获取元数据需要使用 `sendMessageWithCallback` 方法。\n```typescript\nconst call = service.sayHello().sendMessageWithCallback({\n  name: 'zhangting'\n}, (err) => {\n  if (err) {\n    reject(err);\n  }\n});\ncall.on('metadata', (meta) => {\n  // output meta\n});\n```\n其他流式服务，可以通过 `getCall()` 方法获取原始客户端流对象，从而直接订阅。\n```typescript\n// 获取服务，注意，这里没有 await\nconst call = service.addMany().getCall();\ncall.on('metadata', (meta) => {\n  // output meta\n});\n```\n\n\n## 超时处理\n\n\n我们可以在调用服务时传递参数，单位毫秒。\n```typescript\nconst result = await service.sayHello({\n  timeout: 5000\n}).sendMessage({\n  name: 'harry'\n});\n```\n","isInternal":false,"tokens":7209,"sizeBytes":25431},{"name":"http-proxy.md","path":"site/docs/extensions/http-proxy.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/http-proxy.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# HTTP 代理\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的 HTTP 请求代理组件，支持 GET、POST 等多种请求方法。\n\n相关信息：\n\n| web 支持情况      |     |\n| ----------------- | --- |\n| @midwayjs/koa     | ✅  |\n| @midwayjs/faas    | 💬 |\n| @midwayjs/web     | ✅  |\n| @midwayjs/express | ✅  |\n\n:::caution\n\n💬 部分函数计算平台不支持流式请求响应，请参考对应平台能力。\n\n:::\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/http-proxy@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/http-proxy\": \"^4.0.0\"\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n## 启用组件\n\n在 `src/configuration.ts` 中引入组件\n\n```typescript\n// ...\nimport * as proxy from '@midwayjs/http-proxy';\n\n@Configuration({\n  imports: [\n    // ...other components\n    proxy,\n  ],\n})\nexport class MainConfiguration {}\n```\n\n## 配置\n\n代理配置定义如下：\n\n```typescript\n// 代理配置类型\nexport interface HttpProxyConfig {\n  // 匹配要代理的 URL 正则表达式\n  match: RegExp;\n  // 替换匹配到的链接的 host，将请求代理到此地址\n  host?: string;\n  // 通过正则的表达式捕获组处理代理地址\n  target?: string;\n  // 转发请求超时时间，默认为0不设置超时时间\n  proxyTimeout?: number;\n  // 忽略代理请求转发的 header 中的字段\n  ignoreHeaders?: {\n    [key: string]: boolean;\n  };\n}\n```\n\n代理支持单个代理和多个代理。\n\n单个代理配置\n\n```typescript\n// src/config/config.default.ts\n\nexport default {\n  httpProxy: {\n    match: /\\/tfs\\//,\n    host: 'https://gw.alicdn.com',\n  },\n};\n```\n\n多个代理配置\n\n```typescript\n// src/config/config.default.ts\n\n// 代理配置类型\nexport default {\n  httpProxy: {\n    default: {\n      // 一些每个策略复用的值，会和底下的策略进行合并\n    },\n    strategy: {\n      gw: {\n        // https://gw.alicdn.com/tfs/TB1.1EzoBBh1e4jSZFhXXcC9VXa-48-48.png\n        match: /\\/tfs\\//,\n        host: 'https://gw.alicdn.com',\n      },\n      g: {\n        // https://g.alicdn.com/mtb/lib-mtop/2.6.1/mtop.js\n        match: /\\/bdimg\\/(.*)$/,\n        target: 'https://sm.bdimg.com/$1',\n      },\n      httpBin: {\n        // https://httpbin.org/\n        match: /\\/httpbin\\/(.*)$/,\n        target: 'https://httpbin.org/$1',\n      },\n    },\n  },\n};\n```\n\n## 示例：使用 host 配置代理\n\n```typescript\nexport default {\n  httpProxy: {\n    match: /\\/tfs\\//,\n    host: 'https://gw.alicdn.com',\n  },\n};\n```\n\n当请求您的站点路径为： `https://yourdomain.com/tfs/test.png` 时，`match` 字段配置的正则表达式成功匹配，那么就将原始请求路径中的 `host` 部分 `https://yourdomain.com` 替换为配置的 `https://gw.alicdn.com`，从而发起代理请求到 `https://gw.alicdn.com/tfs/test.png`，并把响应结果返回给请求您站点的用户。\n\n## 示例：使用 target 配置代理\n\n```typescript\nexport default {\n  httpProxy: {\n    match: /\\/httpbin\\/(.*)$/,\n    target: 'https://httpbin.org/$1',\n  },\n};\n```\n\n当请求您的站点路径为： `https://yourdomain.com/httpbin/get?name=midway` 时，`match` 字段配置的正则表达式成功匹配，同时正则的捕获组中有结果 `['get?name=midway']` ，那么就将原始请求路径中的 `$1` 部分替换为捕获组中的第 1 个数据（index: 0）的 `get?name=midway`，从而发起代理请求到 `https://httpbin.org/get?name=midway`，并把响应结果返回给请求您站点的用户。\n","isInternal":false,"tokens":1176,"sizeBytes":3607},{"name":"i18n.md","path":"site/docs/extensions/i18n.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/i18n.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# 多语言\n\nMidway 提供了多语言组件，让业务可以快速指定不同的语言，展示不同的文案，也可以在 HTTP 场景配合请求参数，请求头等方式来使用。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n## 安装组件\n\n```bash\n$ npm i @midwayjs/i18n@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/i18n\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 使用组件\n\n将 i18n 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as i18n from '@midwayjs/i18n';\n\n@Configuration({\n  imports: [\n    // ...\n    i18n\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n\n\n## 使用\n\n组件提供了 `MidwayI18nService`  服务，用于翻译多语言文本。\n\n使用 `translate` 方法，传入不同的文本关键字和参数，返回不同语言的文本内容。\n\n```typescript\n@Controller('/')\nexport class UserController {\n\n  @Inject()\n  i18nService: MidwayI18nService;\n\n  @Get('/')\n  async index(@Query('username') username: string) {\n    return this.i18nService.translate('HELLO_MESSAGE', {\n      args: {\n        username\n      },\n    });\n  }\n}\n```\n\n\n\n## 配置多语言文案\n\n你可以在配置文件中直接配置，但是大多数情况下，文案会很多，有时候甚至可能文案在远端服务上，这个时候直接配置就不太现实。\n\n一般来说，我们会将文案单独放到某个文案配置目录中，比如 `src/locales` 。\n\n以 `src/locale` 这个目录为例，我们举个例子，结构如下：\n\n```text\n.\n├── src\n│   ├── locales\n|   │   ├── en_US.json\n|   │   └── zh_CN.json\n│   └── controller\n│       └── home.controller.ts\n├── package.json\n└── tsconfig.json\n```\n\n这里我们建了两个多语言的文件，`en_US.json` 和 `zh_CN.json`，分别代表英文和中文。\n\n文件内容分别如下：\n\n```json\n// src/locales/en_US.json\n{\n  \"hello\": \"Hello {username}\",\n  \"email\": \"email id\",\n  \"login\": \"login account\",\n  \"createdAt\": \"register date\"\n}\n```\n\n```json\n// src/locales/zh_CN.json\n{\n  \"hello\": \"你好 {username}\",\n  \"email\": \"邮箱\",\n  \"login\": \"帐号\",\n  \"createdAt\": \"注册时间\"\n}\n```\n\n每行一个字符串对，是一个标准的 JSON 格式内容，也可以使用 js/ts 文件，花括号中是可替换的参数占位。\n\n同时，需要在配置中加入这两个 JSON，其中 `default` 是语言的默认分组。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    // 把你的翻译文本放到这里\n    localeTable: {\n      en_US: {\n        default: require('../locale/en_US'),\n      },\n      zh_CN: {\n        default: require('../locale/zh_CN'),\n      }\n    },\n  }\n}\n```\n\n这样就可以使用了，使用输出如下。\n\n```typescript\nthis.i18nService.translate('hello', {\n  args: {\n    username: 'harry',\n  },\n  locale: 'en_US',\n});\n\n// output: Hello harry.\n\nthis.i18nService.translate('hello', {\n  args: {\n    username: 'harry',\n  },\n  locale: 'zh_CN',\n});\n\n// output: 你好 harry.\n\n```\n\n\n\n## 多语言文案分组\n\n在如下配置中，用户配置的多语言文案在 `default` 分组中。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    // 把你的翻译文本放到这里\n    localeTable: {\n      en_US: {\n        default: require('../locale/en_US'),\n      },\n      zh_CN: {\n        default: require('../locale/zh_CN'),\n      }\n    },\n  }\n}\n```\n\n这样做的好处是，在其他组件或者业务代码中，我们也可以使用不同的分组名，来添加其他的多语言文案。\n\n比如：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    // 把你的翻译文本放到这里\n    localeTable: {\n      en_US: {\n        default: require('../locale/en_US'),\n        user: require('../locale/user_en_US'),\n      },\n      zh_CN: {\n        default: require('../locale/zh_CN'),\n        user: require('../locale/user_zh_CN'),\n      }\n    },\n  }\n}\n```\n\n在代码中，如果调用非默认分组，需要指定分组参数。\n\n```typescript\nthis.i18nService.translate('user.hello', {\n  args: {\n    username: 'harry',\n  },\n  group: 'user',\t\t// 指定其他分组\n  locale: 'en_US',\n});\n\n```\n\n\n\n## 多语言文案格式\n\n多语言文本中可以添加参数，参数可以有 `对象` 和 `数组` 两种形式。\n\n对象形式如下，使用花括号作为占位符。\n\n```text\nHello {username}\n```\n\n使用时，通过配置传递，按对象 key 覆盖变量。\n\n```typescript\nasync index(@Query('username') username: string) {\n  return this.i18nService.translate('hello', {\n    args: {\n      username\n    },\n  });\n}\n```\n\n数组形式如下，使用数字作为占位符。\n\n```text\nHello {0}\n```\n\n使用时，通过配置传递，格式是数组形式，按数组顺序覆盖数字变量。\n\n```typescript\nasync index(@Query('username') username: string) {\n  return this.i18nService.translate('hello', {\n    args: [username]\n  });\n}\n```\n\n\n\n## 动态添加多语言文案\n\n有时候，多语言文案可能放在远端，比如数据库等，我们可以通过 `addLocale` 方法进行动态添加。\n\n比如，在配置加载后，代码使用前。\n\n```typescript\n// configuration.ts\n\n// ...\n@Configuration({\n  imports: [\n    koa,\n    i18n\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  i18nService: MidwayI18nService;\n\n  async onReady() {\n    this.i18nService.addLocale('zh_TW', {\n      hello: '你好，{username} 美麗的世界'\n    });\n  }\n\n\n  // ...\n}\n```\n\n在代码中就可以使用。\n\n```typescript\nasync index(@Query('username') username: string) {\n  return this.i18nService.translate('hello', {\n    args: [username],\n    locale: 'zh_TW'\n  });\n}\n```\n\n\n\n## 通过参数指定当前语言\n\n一般情况下，默认语言为 `en_US`，用户的浏览器访问一般会自带 `Accept-Language` 头，所以会正确识别语言。比如用中文浏览器访问，就能正常显示中文。\n\n除此之外，在 HTTP 场景下可以通过 URL Query，Cookie，Header 来指定语言。\n\n优先级从上到下：\n\n- query: /?locale=en-US\n- cookie: locale=zh-TW\n- header: Accept-Language: zh-CN,zh;q=0.5\n\n当传递了这些参数之后，多语言数据会自动保存到当前用户的 Cookie 中，下次请求会直接用该设定好的语言。\n\n\n\n## 手动设置语言\n\n可以通过调用 `saveRequestLocale` 设置当前语言。\n\n```typescript\nasync index() {\n  // ...\n  this.i18nService.saveRequestLocale('zh_CN');\n}\n```\n\n如果开启了 `writeCookie` 配置，设置后会保存到当前用户的 Cookie 中，下次请求会使用该设置。\n\n\n\n## 语言选择优先级\n\n这些多种设置语言的方式，有着不同的优先级，如下优先级从高到低：\n\n- 1、`i18nService.translate` 方法显式指定的语言\n- 2、通过其他装饰器设置的语言，比如 `@Validate` 装饰器的参数（本质是调用了`i18nService.translate` 方法）\n- 3、通过 `saveRequestLocale` API 直接设置的当前语言\n- 4、通过浏览器 Query，Cookie，Header 设置的语言（本质是调用了 `saveRequestLocale`）\n- 5、i18n 组件配置中的默认语言\n\n\n\n## 关于语言大小写\n\n在代码内部，我们会将所有的多语言，fallback 规则，写入的文本串，返回的 locale 结果，使用下面的规则替代\n\n- 1、使用中划线代替下划线\n- 2、使用小写代替大写\n\n即所有的 `en_US` 都会变成 `en-us`，`zh_CN` 会变成 `zh-cn`。\n\n这样做会安全的适配 URL 和 Cookie。\n\n\n\n## View 中使用\n\n在 Web 类型的框架中，我们默认添加了 locals 变量支持，可以在模板引擎中使用。\n\n假设我们使用的模板引擎是 [Nunjucks](./render)，可以直接引用到 `i18n` 方法。\n\n多语言文案如下：\n\n```json\n{\n  \"hello\": \"Hello {username}\",\n}\n```\n\n模板如下：\n\n```html\n<span>{{ i18n('hello', user) }}</span>\n```\n\n示例如下：\n\n```typescript\n// ...\n\n@Controller('/')\nexport class UserController {\n\n  @Inject()\n  ctx: Context;\n\n  @Get('/')\n  async index() {\n    await this.ctx.render('index', {\n      // 注意这里是整个对象传递给模板\n      user: {\n        username: 'harry',\n      }\n    });\n  }\n}\n```\n\ni18n 方法定义如下：\n\n```typescript\nfunction i18n(templateName: string, args: Record<string, any>) {\n  // ...\n}\n```\n\n方法名可以通过配置修改。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    localsField: 'i18n',\n  }\n}\n```\n\n\n\n\n\n## 配置\n\n### 默认配置\n\n大部分情况下，你只需要在配置 `localeTable` 添加你自己的多语言翻译即可。\n\n下面是完整的配置，你可以在配置定义中找到。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    // 默认语言  \"en_US\"\n    defaultLocale: 'en_US',\n\n    // 把你的翻译文本放到这里\n    localeTable: {\n      en_US: {\n        // group name\n        default: {\n          // hello: 'hello'\n        }\n      },\n      zh_CN: {\n        // group name\n        default: {\n          // hello: '你好'\n        }\n      },\n    },\n\n    // 语言映射，可以用 * 号通配\n    fallbacks: {\n      //   'en_*': 'en_US',\n      //   pt: 'pt-BR',\n    },\n    // 是否将请求参数写入 cookie\n    writeCookie: true,\n    resolver: {\n      // url query 参数，默认是 \"locale\"\n      queryField: 'locale',\n      cookieField: {\n        // Cookie 里的 key，默认是 \"locale\"\n        fieldName: 'locale',\n        // Cookie 域名，默认为空，代表当前域名有效\n        cookieDomain: '',\n        // Cookie 默认的过期时间，默认一年\n        cookieMaxAge: FORMAT.MS.ONE_YEAR,\n      },\n    },\n    localsField: 'i18n',\n    // 缺失翻译键的处理函数，默认返回原始键名\n    missingKeyHandler: (message, options) => message,\n  }\n}\n```\n\n\n\n### 回写 Cookie\n\n默认情况下，多语言组件会将当前用户的语言回写到 Cookie 中，避免下次请求再进行查找以提高性能，我们可以通过配置关闭这个行为。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    writeCookie: false,\n  }\n}\n```\n\n\n\n### 请求解析配置\n\nHTTP 场景下，我们提供了通过参数指定当前语言的能力。\n\n默认情况下，组件通过下面的字段来查找。\n\n- query 的 `locale` 字段\n- cookie 的 `locale` 字段\n- header 的 `Accept-Language` 部分\n\n我们可以通过配置修改查询的字段。\n\n比如，修改 Query 的字段。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    resolver: {\n      queryField: 'abc'\n    },\n  }\n}\n```\n\n我们就可以通过 `/?abc=en-US` 来请求修改语言。\n\n如果不希望通过请求来设置语言，可以将整个 `resolver` 解析关闭，对 Cookie 的回写也将同时停止。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    resolver: false,\n  }\n}\n```\n\n\n### 缺失翻译键处理\n\n当翻译键在所有语言文件中都找不到时，i18n 组件会调用 `missingKeyHandler` 函数来处理这种情况。\n\n默认情况下，当找不到翻译时，会返回原始的键名：\n\n```typescript\n// 默认的 missingKeyHandler\nmissingKeyHandler: (message, options) => message\n```\n\n例如：\n```typescript\n// 假设 \"unknown_key\" 在任何语言文件中都不存在\nthis.i18nService.translate('unknown_key');\n// 返回: \"unknown_key\"\n```\n\n你可以自定义缺失键的处理方式，比如添加特殊标记、记录日志等：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  i18n: {\n    missingKeyHandler: (message, options) => {\n      // 记录缺失的翻译键\n      console.warn(`Missing translation: ${message}`, options);\n\n      // 根据不同的组返回不同的格式\n      const group = options?.group || 'default';\n      const locale = options?.locale || 'default';\n\n      return `[${group}:${locale}:${message}]`;\n    }\n  }\n}\n```\n\n\n## 常用语言\n\n| 语言             | 语言包名 |\n| :--------------- | :------- |\n| 阿拉伯           | ar_EG    |\n| 亞美尼亞         | hy_AM    |\n| 保加利亚语       | bg_BG    |\n| 加泰罗尼亚语     | ca_ES    |\n| 捷克语           | cs_CZ    |\n| 丹麦语           | da_DK    |\n| 德语             | de_DE    |\n| 希腊语           | el_GR    |\n| 英语             | en_GB    |\n| 英语（美式）     | en_US    |\n| 西班牙语         | es_ES    |\n| 爱沙尼亚语       | et_EE    |\n| 波斯语           | fa_IR    |\n| 芬兰语           | fi_FI    |\n| 法语（比利时）   | fr_BE    |\n| 法语             | fr_FR    |\n| 希伯来语         | he_IL    |\n| 印地语           | hi_IN    |\n| 克罗地亚语       | hr_HR    |\n| 匈牙利           | hu_HU    |\n| 冰岛语           | is_IS    |\n| 印度尼西亚语     | id_ID    |\n| 意大利语         | it_IT    |\n| 日语             | ja_JP    |\n| 格鲁吉亚语       | ka_GE    |\n| 卡纳达语         | kn_IN    |\n| 韩语/朝鲜语      | ko_KR    |\n| 库尔德语         | ku_IQ    |\n| 拉脱维亚语       | lv_LV    |\n| 马来语           | ms_MY    |\n| 蒙古语           | mn_MN    |\n| 挪威             | nb_NO    |\n| 尼泊尔语         | ne_NP    |\n| 荷兰语（比利时） | nl_BE    |\n| 荷兰语           | nl_NL    |\n| 波兰语           | pl_PL    |\n| 葡萄牙语(巴西)   | pt_BR    |\n| 葡萄牙语         | pt_PT    |\n| 斯洛伐克语       | sk_SK    |\n| 塞尔维亚         | sr_RS    |\n| 斯洛文尼亚       | sl_SI    |\n| 瑞典语           | sv_SE    |\n| 泰米尔语         | ta_IN    |\n| 泰语             | th_TH    |\n| 土耳其语         | tr_TR    |\n| 罗马尼亚语       | ro_RO    |\n| 俄罗斯语         | ru_RU    |\n| 乌克兰语         | uk_UA    |\n| 越南语           | vi_VN    |\n| 简体中文         | zh_CN    |\n| 繁体中文         | zh_TW    |\n\n\n\n## 常见问题\n\n### 1、测试配置全局语言不生效\n\n一般场景下，你 **无需** 配置全局语言，因为浏览器访问会自动带上语言信息，比如中文浏览器自动返回中文，英文浏览器自动返回英文。\n\n假如你明确希望测试全局设置的效果，请务必按下面操作：\n\n* 1、如果使用的是浏览器，请清空页面 cookie 再访问，因为 cookie 中会记录上一次用户的语言信息\n* 2、如果使用的是 Postman 等工具，请不要带上 cookie 以及语言相关的 Header，Query 等字段\n\n### 2、测试返回的语言非预期\n\n请使用浏览器进行测试，不要使用 Postman。\n\n由于 Postman 请求不会带有浏览器语言相关的 Header，所以服务端无法自动判断语言。\n\n如果你一定要使用 Postman，请参考浏览器请求，加上 `Accept-Language` Header。\n","isInternal":false,"tokens":4632,"sizeBytes":15013},{"name":"info.md","path":"site/docs/extensions/info.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/info.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# 信息查看\n\nMidway 提供了 info 组件，用于展示应用的基本信息，方便排查问题。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/info@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/info\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 使用组件\n\n将 info 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as info from '@midwayjs/info';\n\n@Configuration({\n  imports: [\n    // ...\n    info\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n在有些情况下，为了不想让应用信息透出，我们指定在特殊环境下生效。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as info from '@midwayjs/info';\n\n@Configuration({\n  imports: [\n    koa,\n    {\n      component: info,\n      enabledEnvironment: ['local'],\t// 只在本地启用\n    }\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n\n\n## 查看信息\n\n在默认情况下，info 组件会为 Http 场景自动添加一个中间件，我们可以通过 `/_info` 来访问。\n\n默认情况下，会展示系统，进程，以及配置等关键信息。\n\n效果如下：\n\n![info](https://img.alicdn.com/imgextra/i3/O1CN01TCkSvr28x8T7gtnCl_!!6000000007998-2-tps-797-1106.png)\n\n\n\n## 修改访问路由\n\n为了安全，我们可以调整访问的路由。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  info: {\n    infoPath: '/_my_info',\n  }\n}\n```\n\n\n\n## 隐藏信息\n\n默认情况下，info 组件会隐藏秘钥等信息。我们可以配置增减隐藏的关键字，这个配置会对 **环境变量** 以及 **多环境配置** 生效。\n\n关键字可以使用通配符，比如增加一些关键字。\n\n```typescript\n// src/config/config.default.ts\nimport { DefaultHiddenKey } from '@midwayjs/info';\n\nexport default {\n  // ...\n  info: {\n    hiddenKey: DefaultHiddenKey.concat(['*abc', '*def', '*bbb*']),\n  }\n}\n```\n\n\n\n## 调用 API\n\ninfo 组件默认提供了 `InfoService` 用于在非 Http 或是自定义的场景来使用。\n\n比如：\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InfoService } from '@midwayjs/info';\n\n@Provide()\nexport class userService {\n\n  @Inject()\n  inforService: InfoService\n\n  async getInfo() {\n    // 应用信息，应用名等\n    this.inforService.projectInfo();\n    // 系统信息\n    this.inforService.systemInfo();\n    // 堆内存，cpu 等\n    this.inforService.resourceOccupationInfo();\n    // midway 框架的信息\n    this.inforService.softwareInfo();\n    // 当前使用的环境配置\n    this.inforService.midwayConfig();\n    // 依赖注入容器中的服务\n    this.inforService.midwayService();\n    // 系统时间，时区，启动时常\n    this.inforService.timeInfo();\n    // 环境变量\n    this.inforService.envInfo();\n    // 依赖信息\n    this.inforService.dependenciesInfo();\n    // 网络信息\n    this.inforService.networkInfo();\n  }\n}\n```\n\n","isInternal":false,"tokens":995,"sizeBytes":3295},{"name":"jwt.md","path":"site/docs/extensions/jwt.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/jwt.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# JWT\n\n`JSON Web Token` (JWT)是一个开放标准(RFC 7519)，它定义了一种紧凑的、自包含的方式，用于作为`JSON`对象在各方之间安全地传输信息。该信息可以被验证和信任，因为它是数字签名的。\n\nMidway 提供了 jwt 组件，简单提供了一些 jwt 相关的 API，可以基于它做独立的鉴权和校验。\n\n相关信息：\n\n| 描述              |     |\n| ----------------- | --- |\n| 可用于标准项目    | ✅  |\n| 可用于 Serverless | ✅  |\n| 可用于一体化      | ✅  |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/jwt@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/jwt\": \"^4.0.0\"\n    // ...\n  },\n}\n```\n\n## 使用组件\n\n将 jwt 组件配置到代码中。\n\n```typescript\nimport { Configuration, IMidwayContainer } from '@midwayjs/core';\nimport { IMidwayContainer } from '@midwayjs/core';\nimport * as jwt from '@midwayjs/jwt';\n\n@Configuration({\n  imports: [\n    // ...\n    jwt,\n  ],\n})\nexport class MainConfiguration {\n  // ...\n}\n```\n\n## 基础配置\n\n然后在配置中设置，默认未加密。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  jwt: {\n    secret: 'xxxxxxxxxxxxxx', // fs.readFileSync('xxxxx.key')\n    sign: {\n      // signOptions\n      expiresIn: '2d', // https://github.com/vercel/ms\n    },\n    verify: {\n      // verifyOptions\n    },\n    decode: {\n      // decodeOptions\n    }\n  },\n};\n```\n\n更多配置请查看 ts 定义。\n\n## 常用 API\n\nMidway 将 jwt 常用 API 提供为同步和异步两种形式。\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { JwtService } from '@midwayjs/jwt';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  jwtService: JwtService;\n\n  async invoke() {\n    // 同步 API\n    this.jwtService.signSync(payload, secretOrPrivateKey, options);\n    this.jwtService.verifySync(token, secretOrPublicKey, options);\n    this.jwtService.decodeSync(token, options);\n\n    // 异步 API\n    await this.jwtService.sign(payload, secretOrPrivateKey, options);\n    await this.jwtService.verify(token, secretOrPublicKey, options);\n    await this.jwtService.decode(token, options);\n  }\n}\n```\n\n这些 API 都来自于 [node-jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) 基础库，如果不了解请阅读原版文档。\n\n## 中间件示例\n\n一般，jwt 还会配合中间件来完成鉴权，下面是一个自定义 jwt 鉴权的中间件示例。\n\n```typescript\n// src/middleware/jwt.middleware\n\nimport { Inject, Middleware, httpError } from '@midwayjs/core';\nimport { Context, NextFunction } from '@midwayjs/koa';\nimport { JwtService } from '@midwayjs/jwt';\n\n@Middleware()\nexport class JwtMiddleware {\n  @Inject()\n  jwtService: JwtService;\n\n  public static getName(): string {\n    return 'jwt';\n  }\n\n  resolve() {\n    return async (ctx: Context, next: NextFunction) => {\n      // 判断下有没有校验信息\n      if (!ctx.headers['authorization']) {\n        throw new httpError.UnauthorizedError();\n      }\n      // 从 header 上获取校验信息\n      const parts = ctx.get('authorization').trim().split(' ');\n\n      if (parts.length !== 2) {\n        throw new httpError.UnauthorizedError();\n      }\n\n      const [scheme, token] = parts;\n\n      if (/^Bearer$/i.test(scheme)) {\n        try {\n          //jwt.verify方法验证token是否有效\n          await this.jwtService.verify(token, {\n            complete: true,\n          });\n        } catch (error) {\n          //token过期 生成新的token\n          const newToken = getToken(user);\n          //将新token放入Authorization中返回给前端\n          ctx.set('Authorization', newToken);\n        }\n        await next();\n      }\n    };\n  }\n\n  // 配置忽略鉴权的路由地址\n  public match(ctx: Context): boolean {\n    const ignore = ctx.path.indexOf('/api/admin/login') !== -1;\n    return !ignore;\n  }\n}\n```\n\n然后在入口启用中间件即可。\n\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration, App, IMidwayContainer, IMidwayApplication} from '@midwayjs/core';\nimport * as jwt from '@midwayjs/jwt';\n\n@Configuration({\n  imports: [\n    // ...\n    jwt,\n  ],\n})\nexport class MainConfiguration {\n\n  @App()\n  app: IMidwayApplication;\n\n  async onReady(applicationContext: IMidwayContainer): Promise<void> {\n    // 添加中间件\n    this.app.useMiddleware([\n      // ...\n      JwtMiddleware,\n    ]);\n  }\n}\n```\n\n\n\n## 原始 JWT 对象\n\n可以通过导出的 `Jwt` 对象引用到原始实例上的对象和方法。\n\n```typescript\nimport { Jwt } from '@midwayjs/jwt';\n\n// Jwt.TokenExpiredError\n```\n","isInternal":false,"tokens":1312,"sizeBytes":4718},{"name":"kafka.md","path":"site/docs/extensions/kafka.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/kafka.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# Kafka\n\n在复杂系统的架构中，事件流是很重要的一环，包括从事件源中(数据库、传感器、移动设备等)以事件流的方式去实时捕获数据，持久化事件流方便检索，并实时和回顾操作处理响应事件流。\n\n应用于支付和金融交易、实施跟踪和监控汽车等行业信息流动、捕获分析物联网数据等等。\n\n\n在 Midway中，我们提供了订阅 Kafka 的能力，专门来满足用户的这类需求。\n\n相关信息：\n\n**订阅服务**\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ✅     |\n| 包含独立日志      | ✅     |\n\n\n\n## 基础概念\n\n\n分布式流处理平台\n* 发布订阅（流）信息\n* 容错（故障转移）存储信息（流），存储事件流\n* 在消息流发生的时候进行处理，处理事件流\n\n理解 Producer（生产者）\n\n* 发布消息到一个主题或多个 topic (主题)。\n\n理解 Consumer（主题消费者）\n* 订阅一个或者多个 topic,并处理产生的信息。\n\n理解 Stream API\n* 充当一个流处理器，从 1 个或多个 topic 消费输入流，并生产一个输出流到1个或多个输出 topic，有效地将输入流转换到输出流。\n\n理解 Broker\n* 已发布的消息保存在一组服务器中，称之为 Kafka 集群。集群中的每一个服务器都是一个代理（Broker）。 消费者可以订阅一个或多个主题（topic），并从Broker拉数据，从而消费这些已发布的消息。\n\n\n![image.png](https://kafka.apache.org/images/streams-and-tables-p1_p4.png)\n\n:::tip\n从 v3.19 开始，Kafka 组件做了一次重构，Kafka 组件的配置、使用方法和之前都有较大差异，原有使用方式兼容，但是文档不再保留。\n:::\n\n\n## 安装依赖\n\n\n安装 `@midwayjs/kafka` 模块。\n\n```bash\n$ npm i @midwayjs/kafka --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/kafka\": \"^4.0.0\",\n    // ...\n  }\n}\n```\n\n## 开启组件\n\n`@midwayjs/kafka` 可以作为独立主框架使用。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as kafka from '@midwayjs/kafka';\n\n@Configuration({\n  imports: [\n    kafka\n  ],\n  // ...\n})\nexport class MainConfiguration {\n  async onReady() {\n        // ...\n  }\n}\n```\n\n也可以附加在其他的主框架下，比如 `@midwayjs/koa` 。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as kafka from '@midwayjs/kafka';\n\n@Configuration({\n  imports: [\n    koa,\n    kafka\n  ],\n  // ...\n})\nexport class MainConfiguration {\n  async onReady() {\n        // ...\n  }\n}\n```\n\n由于 Kafka 分为 **消费者（Consumer）** 和 **生产者（Producer）** 两部分，两个可以独立使用，我们将分别介绍。\n\n## 消费者（Consumer）\n\n### 目录结构\n\n\n我们一般把消费者放在 consumer 目录。比如 `src/consumer/user.consumer.ts`  。\n```\n➜  my_midway_app tree\n.\n├── src\n│   ├── consumer\n│   │   └── user.consumer.ts\n│   ├── interface.ts\n│   └── service\n│       └── user.service.ts\n├── test\n├── package.json\n└── tsconfig.json\n```\n\n\n### 基础配置\n\n通过 `consumer` 字段和 `@KafkaConsumer` 装饰器，我们可以配置多个消费者。\n\n比如，下面的 `sub1` 和 `sub2` 就是两个不同的消费者。\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    consumer: {\n      sub1: {\n        // ...\n      },\n      sub2: {\n        // ...\n      },\n    }\n  }\n}\n```\n\n最简单的消费者配置需要几个字段，Kafka 的连接配置、消费者配置以及订阅配置。\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    consumer: {\n      sub1: {\n        connectionOptions: {\n          // ...\n        },\n        consumerOptions: {\n          // ...\n        },\n        subscribeOptions: {\n          // ...\n        },\n      },\n    }\n  }\n}\n```\n\n比如：\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    consumer: {\n      sub1: {\n        connectionOptions: {\n          clientId: 'my-app',\n          brokers: ['localhost:9092'],\n        },\n        consumerOptions: {\n          groupId: 'groupId-test-1',\n        },\n        subscribeOptions: {\n          topics: ['topic-test-1'],\n        }\n      },\n    }\n  }\n}\n```\n\n完整可配置参数包括：\n\n- `connectionOptions`：Kafka 的连接配置，即 `new Kafka(consumerOptions)` 的参数\n- `consumerOptions`：Kafka 的消费者配置，即 `kafka.consumer(consumerOptions)` 的参数\n- `subscribeOptions`：Kafka 的订阅配置，即 `consumer.subscribe(subscribeOptions)` 的参数\n- `consumerRunConfig`：消费者运行配置，即 `consumer.run(consumerRunConfig)` 的参数\n\n这些参数的详细说明，可以参考 [KafkaJS Consumer](https://kafka.js.org/docs/consuming) 文档。\n\n### 复用 Kafka 实例\n\n如果如果需要复用 Kafka 实例，可以通过 `kafkaInstanceRef` 字段来指定。\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    consumer: {\n      sub1: {\n        connectionOptions: {\n          clientId: 'my-app',\n          brokers: ['localhost:9092'],\n        },\n        consumerOptions: {\n          groupId: 'groupId-test-1',\n        },\n        subscribeOptions: {\n          topics: ['topic-test-1'],\n        }\n      },\n      sub2: {\n        kafkaInstanceRef: 'sub1',\n        consumerOptions: {\n          groupId: 'groupId-test-2',\n        },\n        subscribeOptions: {\n          topics: ['topic-test-2'],\n        }\n      }\n    }\n  }\n}\n```\n\n注意，上述的 `sub1` 和 `sub2` 是两个不同的消费者，但是它们共享同一个 Kafka 实例，且 `sub2` 的 `groupId` 需要和 `sub1` 不同。\n\n用 Kafka SDK 写法类似如下：\n\n```typescript\nconst kafka = new Kafka({\n  clientId: 'my-app',\n  brokers: ['localhost:9092'],\n});\n\nconst consumer1 = kafka.consumer({ groupId: 'groupId-test-1' });\nconst consumer2 = kafka.consumer({ groupId: 'groupId-test-2' });\n```\n\n### 消费者实现\n\n我们可以在目录中提供一个标准的消费者实现，比如 `src/consumer/sub1.consumer.ts`。\n\n```typescript\n// src/consumer/sub1.consumer.ts\nimport { KafkaConsumer, IKafkaConsumer, EachMessagePayload } from '@midwayjs/kafka';\n\n@KafkaConsumer('sub1')\nclass Sub1Consumer implements IKafkaConsumer {\n  async eachMessage(payload: EachMessagePayload) {\n    // ...\n  }\n}\n```\n\n`sub1` 是消费者名称，使用的是配置中的 `sub1` 消费者。\n\n也可以实现 `eachBatch` 方法，处理批量消息。\n\n```typescript\n// src/consumer/sub1.consumer.ts\nimport { KafkaConsumer, IKafkaConsumer, EachBatchPayload } from '@midwayjs/kafka';\n\n@KafkaConsumer('sub1')\nclass Sub1Consumer implements IKafkaConsumer {\n  async eachBatch(payload: EachBatchPayload) {\n    // ...\n  }\n}\n```\n\n\n### 消息上下文\n\n\n和其他消息订阅机制一样，消息本身通过 `Context` 字段来传递。\n\n```typescript\n// src/consumer/sub1.consumer.ts\nimport { KafkaConsumer, IKafkaConsumer, EachMessagePayload, Context } from '@midwayjs/kafka';\nimport { Inject } from '@midwayjs/core';\n\n@KafkaConsumer('sub1')\nclass Sub1Consumer implements IKafkaConsumer {\n\n  @Inject()\n  ctx: Context;\n\n  async eachMessage(payload: EachMessagePayload) {\n    // ...\n  }\n}\n```\n\n`Context` 字段包括几个属性：\n\n| 属性        | 类型                           | 描述             |\n| ----------- | ------------------------------ | ---------------- |\n| ctx.payload   | EachMessagePayload, EachBatchPayload | 消息内容         |\n| ctx.consumer  | Consumer             | 消费者实例         |\n\n\n你可以通过 `ctx.consumer` 来调用 Kafka 的 API，比如 `ctx.consumer.commitOffsets` 来手动提交偏移量或者 `ctx.consumer.pause` 来暂停消费。\n\n\n## 生产者（Producer）\n\n### 基础配置\n\n服务生产者也需要创建实例，配置本身使用了 [服务工厂](/docs/service_factory) 的设计模式。\n\n配置如下：\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    producer: {\n      clients: {\n        pub1: {\n          // ...\n        },\n        pub2: {\n          // ...\n        }\n      }\n    }\n  }\n}\n```\n\n每个 Producer 实例的配置，同样包括 `connectionOptions` 和 `producerOptions`。\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    producer: {\n      clients: {\n        pub1: {\n          connectionOptions: {\n            clientId: 'my-app',\n            brokers: ['localhost:9092'],\n          },\n          producerOptions: {\n            // ...\n          }\n        }\n      }\n    }\n  }\n}\n```\n\n具体参数可以参考 [KafkaJS Producer](https://kafka.js.org/docs/producing) 文档。\n\n此外，由于 Kafka Consumer 和 Producer 都可以从同一个 Kafka 实例创建，所以它们可以复用同一个 Kafka 实例。\n\nProducer 后于 Consumer 创建，也同样可以使用 `kafkaInstanceRef` 字段来复用 Kafka 实例。\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    consumer: {\n      sub1: {\n        connectionOptions: {\n          clientId: 'my-app',\n          brokers: ['localhost:9092'],\n        },\n      }\n    },\n    producer: {\n      clients: {\n        pub1: {\n          kafkaInstanceRef: 'sub1',\n        }\n      }\n    }\n  }\n}\n```\n\n### 使用 Producer\n\nProducer 不存在默认实例，由于使用了服务工厂的设计模式，所以可以通过 `@InjectClient()` 来注入。\n\n\n```typescript\n// src/service/user.service.ts\nimport { Provide, InjectClient } from '@midwayjs/core';\nimport { KafkaProducerFactory, Producer } from '@midwayjs/kafka';\n\n@Provide()\nexport class UserService {\n  \n  @InjectClient(KafkaProducerFactory, 'pub1')\n  producer: Producer;\n  \n  async invoke() {\n    await this.producer.send({\n      topic: 'topic-test-1',\n      messages: [{ key: 'message-key1', value: 'hello consumer 11 !' }],\n    });\n  }\n}\n```\n\n## Admin\n\nKafka 的 Admin 功能，可以用来创建、删除、查看主题，查看配置和 ACL 等。\n\n### 基础配置\n\n和 Producer 类似，Admin 也使用了服务工厂的设计模式。\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    admin: {\n      clients: {\n        admin1: {\n          // ...\n        }\n      }\n    }\n  }\n}\n```\n\n同样的，Admin 也可以复用 Kafka 实例。\n\n```typescript\n// src/config/config.default\nexport default {\n  kafka: {\n    consumer: {\n      sub1: {\n        connectionOptions: {\n          clientId: 'my-app',\n          brokers: ['localhost:9092'],\n        },\n      }\n    },\n    admin: {\n      clients: {\n        admin1: {\n          kafkaInstanceRef: 'sub1',\n        }\n      }\n    }\n  }\n}\n```\n\n### 使用 Admin\n\nAdmin 不存在默认实例，由于使用了服务工厂的设计模式，所以可以通过 `@InjectClient()` 来注入。\n\n```typescript\n// src/service/admin.service.ts\nimport { Provide, InjectClient } from '@midwayjs/core';\nimport { KafkaAdminFactory, Admin } from '@midwayjs/kafka';\n\n@Provide()\nexport class AdminService {\n  \n  @InjectClient(KafkaAdminFactory, 'admin1')\n  admin: Admin;\n}\n```\n\n更多的 Admin 使用方法，可以参考 [KafkaJS Admin](https://kafka.js.org/docs/admin) 文档。\n\n\n## 组件日志\n\nKafka 组件默认使用 `kafkaLogger` 日志，默认会将 `ctx.logger` 记录在 `midway-kafka.log`。\n\n你可以通过配置修改。\n\n```typescript\n// src/config/config.default\nexport default {\n  midwayLogger: {\n    clients: {\n      kafkaLogger: {\n        fileLogName: 'midway-kafka.log',\n      },\n    },\n  },\n}\n```\n\n这个日志的输出格式，我们也可以单独配置。\n\n```typescript\nexport default {\n  kafka: {\n    // ...\n    contextLoggerFormat: info => {\n      const { jobId, from } = info.ctx;\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} ${info.message}`;\n    },\n  }\n}\n```\n\n\n## 获取 KafkaJS 模块\n\nKafkaJS 模块，可以通过 `@midwayjs/kafka` 的 `KafkaJS` 字段来获取。\n\n```typescript\nimport { KafkaJS } from '@midwayjs/kafka';\n\nconst { ConfigResourceTypes } = KafkaJS;\n// ...\n```\n\n## 关于分区的警告\n\n如果你使用的是 KafkaJS 的 v2.0.0 版本，你可能会看到如下的警告：\n\n```\n2024-11-04 23:47:28.228 WARN 31729 KafkaJS v2.0.0 switched default partitioner. To retain the same partitioning behavior as in previous versions, create the producer with the option \"createPartitioner: Partitioners.LegacyPartitioner\". See the migration guide at https://kafka.js.org/docs/migration-guide-v2.0.0#producer-new-default-partitioner for details. Silence this warning by setting the environment variable \"KAFKAJS_NO_PARTITIONER_WARNING=1\" { timestamp: '2024-11-04T15:47:28.228Z', logger: 'kafkajs' }\n```\n\n这个警告是由于 KafkaJS 的 v2.0.0 版本默认使用了新的分区器，如果接受新的分区器行为，但想要关闭这个警告消息，可以通过设置环境变量 `KAFKAJS_NO_PARTITIONER_WARNING=1` 来消除这个警告。\n\n或者显示声明分区器。\n\n```typescript\n// src/config/config.default\nimport { KafkaJS } from '@midwayjs/kafka';\nconst { Partitioners } = KafkaJS;\n\nexport default {\n  kafka: {\n    producer: {\n      clients: {\n        pub1: {\n          // ...\n          producerOptions: {\n            createPartitioner: Partitioners.DefaultPartitioner,\n            // ...\n            createPartitioner: Partitioners.LegacyPartitioner,\n          },\n        },\n      },\n    },\n  }\n}\n```\n\n建议你查看 KafkaJS v2.0.0 的 [迁移指南](https://kafka.js.org/docs/migration-guide-v2.0.0#producer-new-default-partitioner) 了解更多细节。\n\n\n\n## 参考文档\n\n- [KafkaJS](https://kafka.js.org/docs/introduction)\n- [apache kafka官网](https://kafka.apache.org/intro)\n","isInternal":false,"tokens":3845,"sizeBytes":13872},{"name":"koa.md","path":"site/docs/extensions/koa.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/koa.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# Koa\n\nKoa 是一个非常轻量易用的 Web 框架。本章节内容，主要介绍在 Midway 中如何使用 Koa 作为上层框架，并使用自身的能力。\n\nMidway 默认的示例都是基于该包。\n\n`@midwayjs/koa` 包默认使用 `koa@2` 以及集成了 `@koa/router` 作为路由基础能力，并默认内置了 `session` 和 `body-parser` 功能。\n\n| 描述           |      |\n| -------------- | ---- |\n| 包含独立主框架 | ✅    |\n| 包含独立日志   | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/koa@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/koa\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n也可以直接使用脚手架创建示例。\n\n```bash\n# npm v6\n$ npm init midway --type=koa-v4 my_project\n\n# npm v7\n$ npm init midway -- --type=koa-v4 my_project\n```\n\n\n\n## 开启组件\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [koa],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: koa.Application;\n\n  async onReady() {\n\t\t// ...\n  }\n}\n\n```\n\n\n\n## BodyParser\n\n`@midwayjs/koa` 自带  `bodyParser` 功能，默认会解析 `Post` 请求，自动识别 `json` 和 `form` 类型。\n\n如需 text 或者 xml，可以自行配置。\n\n默认的大小限制为 `1mb`，可以单独对每项配置大小。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  bodyParser: {\n    enableTypes: ['json', 'form', 'text', 'xml'],\n    formLimit: '1mb',\n    jsonLimit: '1mb',\n    textLimit: '1mb',\n    xmlLimit: '1mb',\n  },\n}\n```\n\n注意，使用 Postman 做 Post 请求时的类型选择：\n\n![postman](https://img.alicdn.com/imgextra/i4/O1CN01QCdTsN1S347SuzZU5_!!6000000002190-2-tps-1017-690.png)\n\n\n关闭 bodyParser 中间件。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  bodyParser: {\n    enable: false,\n    // ...\n  },\n}\n```\n\n\n## Cookie 和 Session\n\n`@midwayjs/koa` 默认封装了 `cookies` 解析和 `Session` 的支持，可以查看 [Cookies 和 Session](../cookie_session)。\n\n\n\n## 扩展 Context\n\n在一些场景下，需要对 Context 做扩展。\n\n如果希望挂在一些临时的请求相关的对象数据，可以使用 `ctx.setAttr(key, value)` API 来实现，比如组件里自用的数据。\n\n如果实在有扩展 Context 的诉求，可以使用 koa 自带的 API。\n\n比如，我们在 `configuration.ts` 中做扩展提供了一个 `render()` 方法。\n\n```typescript\nimport { App, Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\n\n@Configuration({\n\t// ...\n})\nexport class MainConfiguration {\n  @App()\n  app: koa.Application;\n\n  async onReady(container) {\n    Object.defineProperties(app.context, {\n      render: {\n        value: async function (...args) {\n          // ...\n        },\n      },\n    });\n  }\n}\n```\n\n但是这样做无法直接让 Context 包含 Typescript 定义，需要额外增加定义，请参考 [扩展上下文定义](../context_definition)。\n\n\n\n## 获取 Http Server\n\n在一些特殊情况下，你需要获取到原始的 Http Server，我们可以在服务器启动后获取。\n\n```typescript\nimport { App, Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\n\n@Configuration({\n\t// ...\n})\nexport class MainConfiguration {\n  @Inject()\n  framework: koa.Framework;\n\n  async onServerReady(container) {\n    const server = this.framework.getServer();\n    // ...\n  }\n}\n```\n\n\n\n## State 类型定义\n\n在 koa 的 Context 中有一个特殊的 State 属性，通过和 Context 类似的方式可以扩展 State 定义。\n\n```typescript\n// src/interface.ts\n\ndeclare module '@midwayjs/koa/dist/interface' {\n  interface Context {\n    abc: string;\n  }\n\n  interface State {\n    bbb: string;\n    ccc: number;\n  }\n}\n```\n\n\n\n## 获取端口\n\n通过 framework 或者 app 上的 `getPort()` 方法可以获取端口。\n\n```typescript\nimport { App, Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\n\n@Configuration({\n  imports: [\n    koa,\n    // ...\n  ]\n\t// ...\n})\nexport class MainConfiguration {\n  @Inject()\n  framework: koa.Framework;\n\n  async onServerReady(container, mainApp: koa.Application) {\n    const port = this.framework.getPort();\n    const port = this.mainApp.getPort();\n    // ...\n  }\n}\n```\n\n\n\n## 配置\n\n\n\n### 默认配置\n\n`@midwayjs/koa` 的配置样例如下：\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  koa: {\n    port: 7001,\n  },\n}\n```\n\n所有属性描述如下：\n\n| 属性         | 类型                                         | 描述                                                    |\n| ------------ |--------------------------------------------| ------------------------------------------------------- |\n| port         | number                                     | 可选，启动的端口                                        |\n| globalPrefix | string                                     | 可选，全局的 http 前缀                                  |\n| keys         | string[]                                   | 可选，Cookies 签名，如果上层未写 keys，也可以在这里设置 |\n| hostname     | string                                     | 可选，监听的 hostname，默认 127.1                       |\n| key          | string \\| Buffer \\| Array\\Buffer\\|Object>  | 可选，Https key，服务端私钥                             |\n| cert         | string \\| Buffer \\| Array\\<Buffer\\|Object> | 可选，Https cert，服务端证书                            |\n| ca           | string \\| Buffer \\| Array\\<Buffer\\|Object> | 可选，Https ca                                          |\n| http2        | boolean                                    | 可选，http2 支持，默认 false                            |\n| proxy        | boolean                                    | 可选，是否开启代理，如果为 true 则对于 request 请求中的 ip 优先从 Header 字段中 X-Forwarded-For 获取，默认 false           |\n| subdomainOffset        | number                                     | 可选，子域名的偏移量，默认 2                            |\n| proxyIpHeader        | string                                     | 可选，获取代理 ip 的字段名，默认为 X-Forwarded-For |\n| maxIpsCount        | number                                     | 可选，获取的 ips 最大数量，默认为 0（全部返回）|\n| serverTimeout | number                                     | 可选，服务端超时配置，默认为 2 \\* 60 \\* 1000（2 分钟），单位毫秒 |\n| serverOptions | Record\\<string, any>                       | 可选，http Server [选项](https://nodejs.org/docs/latest/api/http.html#httpcreateserveroptions-requestlistener) |\n\n\n\n### 修改端口\n\n默认情况下，我们在 `config.default` 提供了 `7001` 的默认端口参数，修改它就可以修改 koa http 服务的默认端口。\n\n比如我们修改为 `6001`：\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  koa: {\n    port: 6001,\n  },\n}\n```\n\n默认情况下，单测环境由于需要 supertest 来启动端口，我们的 port 配置为 `null`。\n\n```typescript\n// src/config/config.unittest\nexport default {\n  // ...\n  koa: {\n    port: null,\n  },\n}\n```\n\n此外，也可以通过 `midway-bin dev --ts --port=6001` 的方式来临时修改端口，此方法会覆盖配置中的端口。\n\n\n\n### 全局前缀\n\n此功能请参考 [全局前缀](../controller#全局路由前缀)。\n\n\n\n### 反向代理配置\n\n如果使用了 Nginx 等反向代理，请开启 `proxy` 配置。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  koa: {\n    proxy: true,\n  },\n}\n```\n\n默认使用 `X-Forwarded-For` Header，如果代理配置不同，请自行配置不同的 Header。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  koa: {\n    proxy: true,\n    proxyIpHeader: 'X-Forwarded-Host'\n  },\n}\n```\n\n\n\n\n\n### Https 配置\n\n在大多数的情况，请尽可能使用外部代理的方式来完成 Https 的实现，比如 Nginx。\n\n在一些特殊场景下，你可以通过配置 SSL 证书（TLS 证书）的方式，来直接开启 Https。\n\n首先，你需要提前准备好证书文件，比如 `ssl.key` 和 `ssl.pem`，key 为服务端私钥，pem 为对应的证书。\n\n然后配置即可。\n\n```typescript\n// src/config/config.default\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  koa: {\n    key: join(__dirname, '../ssl/ssl.key'),\n    cert: join(__dirname, '../ssl/ssl.pem'),\n  },\n}\n```\n\n\n\n### favicon 设置\n\n默认情况下，浏览器会发起一个 `favicon.ico` 的请求。\n\n框架提供了一个默认中间件，用来处理该请求，你可以指定一个 `favicon` 的 Buffer。\n\n```typescript\n// src/config/config.default\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  siteFile: {\n    favicon: readFileSync(join(__dirname, '../static/fav.ico')),\n  },\n}\n```\n\n如果开启了 `@midwayjs/static-file`  组件，那么会优先使用组件的静态文件托管。\n\n关闭中间件。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  siteFile: {\n    enable: false,\n    // ...\n  },\n}\n```\n\n### 修改上下文日志\n\n可以单独修改 koa 框架的上下文日志。\n\n```typescript\nexport default {\n  koa: {\n    contextLoggerFormat: info => {\n      const ctx = info.ctx;\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} [${ctx.userId} - ${Date.now() - ctx.startTime}ms ${ctx.method}] ${info.message}`;\n    }\n    // ...\n  },\n};\n```\n\n\n### Query 数组解析\n\n默认情况下，koa 使用 `querystring` 解析 query 参数，当碰到数组时，会将数组的数据拆开。\n\n比如：\n\n```\nGET /query?a[0]=1&a[1]=2\n```\n\n拿到的结果是：\n\n```json\n{\n    \"a[0]\": 1,\n    \"a[1]\": 2,\n}\n```\n\n框架提供了一些参数来处理这种情况。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  koa: {\n    queryParseMode: 'extended',\n    // ...\n  },\n}\n```\n\n`queryParseMode` 参数可以选择 `extended`、 `strict`、`first` 三种值。\n\n 当 `queryParseMode` 有值时，会使用 `qs` 模块处理 query，效果同 `koa-qs` 模块。\n\n当请求参数为 `/query?a=1&b=2&a=3&c[0]=1&c[1]=2'` 时。\n\n默认效果（使用 `querystring`）\n\n```JSON\n{\n  \"a\": [\"1\", \"3\" ],\n  \"b\": \"2\",\n  \"c[0]\": \"1\",\n  \"c[1]\": \"2\"\n}\n```\n\n `extended` 效果\n\n```JSON\n{\n  \"a\": [\"1\", \"3\" ],\n  \"b\": \"2\",\n  \"c\": [\"1\", \"2\"]\n}\n```\n\n `strict` 效果\n\n```JSON\n{\n  \"a\": [\"1\", \"3\" ],\n  \"b\": [\"2\"],\n  \"c\": [\"1\", \"2\"]\n}\n```\n\n `first` 效果\n\n```JSON\n{\n  \"a\": \"1\",\n  \"b\": \"2\",\n  \"c\": \"1\"\n}\n```\n\n\n### 超时配置\n\nRequestTiemout 和 ServerTimeout 是两种不同的超时情况。\n\n- `serverTimeout`：用于设置服务器接收到请求后，等待客户端发送数据的超时时间。如果在该时间内客户端没有发送任何数据，则服务器将关闭连接。此超时适用于整个请求-响应周期，包括请求头、请求主体以及响应。\n- `requestTimeout`：用于设置服务器等待客户端发送完整请求的超时时间。这个超时是针对请求头和请求主体的，服务器将在该时间内等待客户端发送完整的请求。如果在超时时间内没有收到完整的请求，则服务器将中止该请求。\n\n默认情况下，`serverTimeout` 为 0，不会触发超时。\n\n如有需求，可以通过配置修改，单位毫秒。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  koa: {\n    serverTimeout: 100_000\n  },\n}\n```\n\n如果程序出现 `ERR_HTTP_REQUEST_TIMEOUT` 这个错误，说明是触发了 `requestTimeout`，默认为 `300_000` （五分钟），单位毫秒，可以通过以下配置修改。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  koa: {\n    serverOptions: {\n      requestTimeout: 600_000\n    }\n  },\n}\n```\n\n","isInternal":false,"tokens":3384,"sizeBytes":12051},{"name":"mcp.md","path":"site/docs/extensions/mcp.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/mcp.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# MCP (Model Context Protocol)\n\nMCP (Model Context Protocol) 是由 Anthropic 开发的开放标准协议，用于将 AI 模型与外部数据源和工具安全连接。它允许 AI 应用程序与各种服务进行语义集成，为 AI 模型提供了获取实时信息、执行操作和访问资源的标准化方式。\n\n![MCP Architecture](https://mintcdn.com/mcp/4ZXF1PrDkEaJvXpn/images/mcp-simple-diagram.png?w=1650&fit=max&auto=format&n=4ZXF1PrDkEaJvXpn&q=85&s=37d50a5429a8f662c1beb564b4df8e65)\n\nMidway 提供了对 MCP 的完整支持和封装，能够简单快速地创建 MCP 服务器。本章节演示了如何在 Midway 体系下，提供 MCP 服务的方法。\n\nMidway 当前采用了最新的 [MCP SDK](https://github.com/modelcontextprotocol/typescript-sdk) 进行开发，并提供了装饰器和框架集成，用于快速发布 MCP 服务。\n\n相关信息：\n\n**提供服务**\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n**传输类型支持**\n\n| 传输类型      | 描述                                    | 支持状态 |\n| ------------- | --------------------------------------- | -------- |\n| stdio         | 标准输入输出传输，适用于 CLI 应用       | ✅        |\n| stream-http   | HTTP 流传输，推荐的现代传输方式         | ✅        |\n| sse           | 服务器发送事件传输（已弃用，向下兼容）  | ✅        |\n\n## 安装依赖\n\n在现有项目中安装 MCP 的依赖。\n\n```bash\n$ npm i @midwayjs/mcp@4 --save\n$ npm i @modelcontextprotocol/sdk zod --save\n```\n\n\n如果使用 `sse` 或 `stream-http` 传输类型，还需要安装 HTTP 框架组件（选择其中一个）：\n\n```bash\n# Express 框架\n$ npm i @midwayjs/express@4 --save\n\n# 或者 Koa 框架\n$ npm i @midwayjs/koa@4 --save\n\n# 或者 Egg.js 框架\n$ npm i @midwayjs/web@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/mcp\": \"^4.0.0\",\n    \"@modelcontextprotocol/sdk\": \"^1.19.0\",\n    \"zod\": \"^3.24.1\",\n    \"@midwayjs/express\": \"^4.0.0\"\n  }\n}\n```\n\n:::tip\n如果使用 `sse` 或 `stream-http` 传输类型，您需要选择并安装一个 HTTP 框架：\n- `@midwayjs/express` - Express 框架\n- `@midwayjs/koa` - Koa 框架  \n- `@midwayjs/web` - Egg.js 框架\n\n如果只使用 `stdio` 传输类型，则不需要安装 HTTP 框架组件。\n:::\n\n## 开启组件\n\n在 `src/configuration.ts` 中导入 MCP 组件：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as mcp from '@midwayjs/mcp';\n// 如果使用 sse/stream-http 传输类型，需要导入 HTTP 框架\nimport * as express from '@midwayjs/express';\n\n@Configuration({\n  imports: [\n    express, // HTTP 框架（sse/stream-http 传输类型需要）\n    mcp      // 导入 MCP 组件\n  ],\n  // ...\n})\nexport class MainConfiguration {}\n```\n\n:::info\n**传输类型依赖说明**：\n- `stdio` 传输：只需要导入 `@midwayjs/mcp`\n- `sse` 或 `stream-http` 传输：需要同时导入 HTTP 框架组件（如 `@midwayjs/express`）\n:::\n\n## 配置\n\n在 `config.default.ts` 中配置 MCP 服务器信息：\n\n```typescript\nexport default {\n  // 如果使用 sse/stream-http 传输类型，需要配置 HTTP 框架\n  express: {\n    port: 3000,\n    keys: ['mcp-secret-key']\n  },\n  // 或者如果使用 Koa\n  // koa: {\n  //   port: 3000,\n  //   keys: ['mcp-secret-key']\n  // },\n  \n  mcp: {\n    serverInfo: {\n      name: 'my-mcp-server',\n      version: '1.0.0',\n    },\n    // 传输类型：stdio | stream-http | sse(已弃用)\n    transportType: 'stream-http',\n    // 可选：自定义端点路径\n    endpoints: {\n      streamHttp: '/mcp',     // StreamHTTP 端点\n      sse: '/sse',            // SSE 端点（向下兼容）\n      messages: '/messages'   // 消息端点\n    }\n  }\n}\n```\n\n:::warning\n- 当使用 `sse` 或 `stream-http` 传输类型时，**必须配置 HTTP 框架的端口和密钥**：\n- 如果只使用 `stdio` 传输类型，则不需要 HTTP 框架配置\n:::\n\n### 传输类型说明\n\nMCP 支持多种传输类型：\n\n- **stdio**: 适用于命令行工具和脚本，通过标准输入输出通信\n- **stream-http**: 推荐的 HTTP 传输方式，支持会话管理和并发请求\n- **sse**: 传统的服务器发送事件传输（已弃用，但保持向下兼容）\n\n:::tip\n推荐使用 `stream-http` 传输类型，它提供了更好的性能和会话管理能力，同时兼容传统的 SSE 客户端。\n:::\n\n### 默认端点配置\n\n当使用 `sse` 或 `stream-http` 传输类型时，Midway MCP 框架会自动配置以下默认端点：\n\n| 端点类型 | 默认路径 | 用途 | 支持的传输类型 |\n|---------|---------|------|----------------|\n| StreamHTTP | `/mcp` | 主要的 MCP 通信端点，推荐使用 | stream-http |\n| SSE | `/sse` | 传统 SSE 客户端兼容端点 | sse, stream-http (向下兼容) |\n| Messages | `/messages` | 消息处理端点 | stream-http, sse |\n\n**自定义端点配置**\n\n您可以通过 `endpoints` 配置项自定义这些端点路径：\n\n```typescript\nexport default {\n  mcp: {\n    serverInfo: {\n      name: 'my-mcp-server',\n      version: '1.0.0',\n    },\n    transportType: 'stream-http',\n    endpoints: {\n      streamHttp: '/api/mcp',        // 自定义 StreamHTTP 端点\n      sse: '/api/events',            // 自定义 SSE 端点\n      messages: '/api/messages'      // 自定义消息端点\n    }\n  }\n}\n```\n\n**端点访问示例**\n\n假设服务器运行在 `http://localhost:3000`，默认端点访问地址为：\n\n- StreamHTTP 客户端：`http://localhost:3000/mcp`\n- SSE 客户端：`http://localhost:3000/sse`\n- 消息端点：`http://localhost:3000/messages`\n\n:::info\n**向下兼容说明**：当服务器配置为 `stream-http` 传输类型时，传统的 SSE 客户端仍然可以通过 `/sse` 端点正常连接和通信。\n:::\n\n### 日志配置\n\nMCP 组件默认志输出，可以通过 `mcpLogger` 进一步配置日志详情。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  midwayLogger: {\n    mcpLogger: {\n      // ...\n    }\n  },\n}\n```\n\n\n\n## 使用方式\n\n### 工具（Tools）\n\n使用 `@Tool` 装饰器创建 MCP 工具：\n\n```typescript\nimport { Tool, IMcpTool, ToolConfig } from '@midwayjs/mcp';\nimport { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport { z } from 'zod';\n\nconst weatherConfig: ToolConfig<{ city: z.ZodString }> = {\n  description: 'Get weather information for a city',\n  inputSchema: {\n    city: z.string().describe('The city name')\n  }\n};\n\n@Tool('get_weather', weatherConfig)\nexport class WeatherTool implements IMcpTool {\n  async execute(args: { city: string }): Promise<CallToolResult> {\n    // 模拟获取天气数据\n    const weather = await this.getWeatherData(args.city);\n    \n    return {\n      content: [\n        {\n          type: 'text',\n          text: `Weather in ${args.city}: ${weather.temperature}°C, ${weather.condition}`\n        }\n      ]\n    };\n  }\n\n  private async getWeatherData(city: string) {\n    // 实际的天气 API 调用\n    return {\n      temperature: 22,\n      condition: 'Sunny'\n    };\n  }\n}\n```\n\n### 提示（Prompts）\n\n使用 `@Prompt` 装饰器创建 MCP 提示：\n\n```typescript\nimport { Prompt, IMcpPrompt, PromptConfig } from '@midwayjs/mcp';\nimport { GetPromptResult } from '@modelcontextprotocol/sdk/types.js';\nimport { z } from 'zod';\n\nconst promptConfig: PromptConfig<{ \n  topic: z.ZodString; \n  style: z.ZodOptional<z.ZodString>; \n}> = {\n  description: 'Generate content on a specific topic',\n  argsSchema: {\n    topic: z.string().describe('The topic to write about'),\n    style: z.string().optional().describe('Writing style (formal, casual, etc.)')\n  }\n};\n\n@Prompt('content_generator', promptConfig)\nexport class ContentPrompt implements IMcpPrompt {\n  async generate(args: { topic: string; style?: string }): Promise<GetPromptResult> {\n    const style = args.style || 'professional';\n    \n    return {\n      messages: [\n        {\n          role: 'user',\n          content: {\n            type: 'text',\n            text: `Write a ${style} article about: ${args.topic}`\n          }\n        }\n      ]\n    };\n  }\n}\n```\n\n### 资源（Resources）\n\n使用 `@Resource` 装饰器创建 MCP 资源：\n\n```typescript\nimport { Resource, IMcpResource, ResourceConfig } from '@midwayjs/mcp';\nimport { ReadResourceResult } from '@modelcontextprotocol/sdk/types.js';\n\nconst resourceConfig: ResourceConfig = {\n  description: 'Access user database information',\n  uri: 'database://users/{id}',\n  mimeType: 'application/json'\n};\n\n@Resource('user_data', resourceConfig)\nexport class UserResource implements IMcpResource {\n  async handle(uri: URL): Promise<ReadResourceResult> {\n    const userId = this.extractUserIdFromUri(uri);\n    const userData = await this.getUserData(userId);\n    \n    return {\n      contents: [\n        {\n          uri: uri.toString(),\n          mimeType: 'application/json',\n          text: JSON.stringify(userData)\n        }\n      ]\n    };\n  }\n\n  private extractUserIdFromUri(uri: URL): string {\n    const path = uri.pathname;\n    return path.split('/').pop() || '';\n  }\n\n  private async getUserData(userId: string) {\n    // 实际的数据库查询\n    return {\n      id: userId,\n      name: 'John Doe',\n      email: 'john@example.com'\n    };\n  }\n}\n```\n\n\n\n## 安全认证\n\n### 内置的 JWT 助手\n\nMidway MCP 框架提供了内置的 JWT 认证助手，可以自动处理 JWT token 验证并将认证信息传递给 MCP 工具、提示和资源。\n\n在配置中启用 JWT 认证助手：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // JWT 配置\n  jwt: {\n    secret: 'your-jwt-secret-key',\n    sign: {\n      expiresIn: '1h'\n    }\n  },\n\n  mcp: {\n    serverInfo: {\n      name: 'my-mcp-server',\n      version: '1.0.0',\n    },\n    transportType: 'stream-http',\n    // 启用内置 JWT 认证助手\n    enableJwtAuthHelper: true,\n  }\n}\n```\n\n认证助手是一个中间件，能将 jwt 的 payload 信息转化为 MCP SDK 中定义的 AuthInfo。\n\n可以通过 `ctx.authInfo` 使用认证信息。\n\n```typescript\nimport { Tool, IMcpTool, ToolConfig, Context } from '@midwayjs/mcp';\nimport { Inject } from '@midwayjs/core';\nimport { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\n@Tool('secure_operation', { description: 'A tool that requires authentication' })\nexport class SecureTool implements IMcpTool {\n  @Inject()\n  ctx: Context;\n\n  async execute(args: any): Promise<CallToolResult> {\n    const authInfo = this.ctx.authInfo;\n    // ...\n  }\n}\n```\n\nJWT payload 应包含以下标准字段：\n\n```json\n{\n  \"aud\": \"client-id\",           \t\t\t\t\t\t// 客户端 ID，映射到 authInfo.clientId\n  \"scope\": \"mcp:read mcp:write\",  \t\t\t\t\t// 权限范围，映射到 authInfo.scopes\n  \"exp\": 1234567890,           \t\t\t\t\t\t\t// 过期时间，映射到 authInfo.expiresAt\n  \"resource\": \"https://api.example.com\", \t\t// 资源 URL，映射到 authInfo.resource\n  \"sub\": \"user-123\",           \t\t\t\t\t\t\t// 用户 ID\n  \"iss\": \"https://auth.example.com\", \t\t\t\t// 签发者\n  \"extra\": {\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// 其他自定义字段会放入 authInfo.extra\n    \"username\": \"testuser\",\n    \"role\": \"admin\"\n  }      \t\t\t\t\t\t\t\t\t\t\n}\n```\n\n\n\n\n\n### 非标数据转换\n\n如果传入的 jwt 结构为非标，需要在配置中自行提供转换函数。\n\n```typescript\n// src/config/config.default.ts\nimport { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';\n\nexport default {\n  // JWT 配置\n  jwt: {\n    secret: 'your-jwt-secret-key',\n    sign: {\n      expiresIn: '1h'\n    }\n  },\n\n  mcp: {\n    serverInfo: {\n      name: 'my-mcp-server',\n      version: '1.0.0',\n    },\n    transportType: 'stream-http',\n    // 启用内置 JWT 认证助手\n    enableJwtAuthHelper: true,\n    // 自定义函数\n    jwtAuthCustomPayloadTransformer: (payload: any, token: string): AuthInfo => {\n      return {\n        // ...使用标准JWT字段映射AuthInfo\n        token: token,\n        clientId: payload.aud,\n        // ...\n      };\n    }\n  }\n}\n```\n\n\n\n\n\n### 更多认证\n\n在其他的认证场景下，也可以使用 Midway 组件或者编写中间件完整。\n\n比如你可以使用 `passport` 组件完成 OAuth 认证。\n\n在认证完成后，按照 MCP SDK 规范，需将认证信息放到 `req.auth` 中。\n\n如：\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport * as mcp from '@midwayjs/mcp';\nimport { MidwayMCPFramework } from '@midwayjs/mcp';\nimport { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';\n\n@Configuration({\n  imports: [\n    express,\n    // ...\n    mcp\n  ],\n  importConfigs: ['./config']\n})\nexport class MainConfiguration {\n  @Inject()\n  mcpFramework: MidwayMCPFramework;\n\n  async onReady(container: IMidwayContainer, mainApp: IMidwayApplication) {\n    const authMiddleware = async (req, res, next) => {\n      // ...\n      const userInfo = {\n        user: 'zhangsan',\n        role: 'admin'\n      }\n      \n      // 必须要设置这个字段\n      req.auth = {\n        token: req.headers.authorization,\n        clientId: 'test-client-id',\n        scopes: ['mcp:read', 'mcp:write'],\n        resource: new URL('https://mcp.example.com/resources'),\n        extra: userInfo,\n      } as AuthInfo;\n      \n      // koa 就放到 ctx.req.auth 上\n\n      await next();\n    }\n    \n    // 注意这里的 mainApp 是 Express\n    mainApp.getMiddleware().insertBefore(authMiddleware, 'mcpMiddleware');\n  }\n}\n```\n\n\n\n\n\n## 完整示例\n\n这里是一个完整的 MCP 服务器示例：\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport * as mcp from '@midwayjs/mcp';\n\n@Configuration({\n  imports: [\n    express,\n    mcp\n  ],\n  importConfigs: ['./config']\n})\nexport class MainConfiguration {}\n\n// src/config/config.default.ts\nexport default {\n  express: {\n    port: 3000,\n  },\n  mcp: {\n    serverInfo: {\n      name: 'example-mcp-server',\n      version: '1.0.0',\n    },\n    transportType: 'stream-http',\n  }\n}\n\n// src/tools/calculator.ts\nimport { Tool, IMcpTool, ToolConfig } from '@midwayjs/mcp';\nimport { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport { z } from 'zod';\n\nconst calcConfig: ToolConfig<{ \n  operation: z.ZodEnum<['add', 'subtract', 'multiply', 'divide']>;\n  a: z.ZodNumber;\n  b: z.ZodNumber;\n}> = {\n  description: 'Perform basic mathematical operations',\n  inputSchema: {\n    operation: z.enum(['add', 'subtract', 'multiply', 'divide']),\n    a: z.number(),\n    b: z.number()\n  }\n};\n\n@Tool('calculator', calcConfig)\nexport class CalculatorTool implements IMcpTool {\n  async execute(args: { \n    operation: 'add' | 'subtract' | 'multiply' | 'divide';\n    a: number;\n    b: number;\n  }): Promise<CallToolResult> {\n    let result: number;\n    \n    switch (args.operation) {\n      case 'add':\n        result = args.a + args.b;\n        break;\n      case 'subtract':\n        result = args.a - args.b;\n        break;\n      case 'multiply':\n        result = args.a * args.b;\n        break;\n      case 'divide':\n        if (args.b === 0) {\n          throw new Error('Division by zero');\n        }\n        result = args.a / args.b;\n        break;\n    }\n    \n    return {\n      content: [\n        {\n          type: 'text',\n          text: `${args.a} ${args.operation} ${args.b} = ${result}`\n        }\n      ]\n    };\n  }\n}\n```\n\n## 动态调用 API\n\n您可以在运行时动态调用 MCP 服务器的原始 API 来注册工具、提示和资源：\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport * as mcp from '@midwayjs/mcp';\nimport { MidwayMCPFramework } from '@midwayjs/mcp';\nimport { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\n@Configuration({\n  imports: [\n    express,\n    mcp\n  ],\n  importConfigs: ['./config']\n})\nexport class MainConfiguration {\n  @Inject()\n  mcpFramework: MidwayMCPFramework;\n\n  async onReady() {\n    const server = this.mcpFramework.getServer();\n    \n    // 动态注册工具\n    server.registerTool({\n      name: 'dynamic_tool',\n      description: '动态注册的工具',\n      inputSchema: {\n        type: 'object',\n        properties: {\n          message: { type: 'string', description: '要处理的消息' }\n        },\n        required: ['message']\n      }\n    }, async (args): Promise<CallToolResult> => {\n      return {\n        content: [{\n          type: 'text',\n          text: `动态处理: ${args.message}`\n        }]\n      };\n    });\n    \n    // 动态注册提示\n    server.registerPrompt({\n      name: 'dynamic_prompt',\n      description: '动态生成的提示',\n      argsSchema: {\n        type: 'object',\n        properties: {\n          topic: { type: 'string', description: '主题' }\n        },\n        required: ['topic']\n      }\n    }, async (args) => {\n      return {\n        messages: [{\n          role: 'user',\n          content: {\n            type: 'text',\n            text: `请写一篇关于 ${args.topic} 的文章`\n          }\n        }]\n      };\n    });\n    \n    // 动态注册资源\n    server.registerResource({\n      uri: 'dynamic://config',\n      name: '动态配置',\n      description: '动态生成的配置资源',\n      mimeType: 'application/json'\n    }, async (uri) => {\n      return {\n        contents: [{\n          uri: uri.toString(),\n          mimeType: 'application/json',\n          text: JSON.stringify({\n            timestamp: new Date().toISOString(),\n            config: 'dynamic data'\n          })\n        }]\n      };\n    });\n  }\n}\n```\n\n:::tip\n动态注册的 API 适用于需要在运行时根据业务逻辑动态添加 MCP 功能的场景。与装饰器方式相比，动态注册提供了更大的灵活性，但需要手动管理注册逻辑。\n:::\n\n## 客户端连接\n\n### StreamHTTP 客户端\n\n```typescript\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';\n\n// 创建客户端实例\nconst client = new Client({\n  name: 'my-mcp-client',\n  version: '1.0.0'\n});\n\n// 创建 StreamHTTP 传输\nconst transport = new StreamableHTTPClientTransport(\n  new URL('http://localhost:3000/mcp'),\n  {\n    requestInit: {\n      headers: {\n        'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'\n      }\n    }\n  }\n);\n\n// 连接到服务器\nawait client.connect(transport);\n\n// 列出可用工具\nconst { tools } = await client.listTools();\nconsole.log('Available tools:', tools);\n\n// 调用工具\nconst result = await client.callTool({\n  name: 'calculator',\n  arguments: {\n    operation: 'add',\n    a: 5,\n    b: 3\n  }\n});\nconsole.log('Tool result:', result);\n\n// 使用完毕后关闭连接\nawait client.close();\n```\n\n\n\n### Stdio 客户端 (进程通信)\n\n```typescript\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';\n\n// 创建客户端实例\nconst client = new Client({\n  name: 'my-mcp-client',\n  version: '1.0.0'\n});\n\n// 创建 Stdio 传输，启动 MCP 服务器进程\nconst transport = new StdioClientTransport({\n  command: 'node',\n  args: ['bootstrap.js'],\n  env: {\n    NODE_ENV: 'production',\n    // 继承当前进程的环境变量\n    ...process.env\n  },\n  // 可选：设置工作目录\n  cwd: '/path/to/mcp/server',\n  // 可选：处理 stderr 输出\n  stderr: 'inherit' // 'pipe' | 'inherit' | 'ignore'\n});\n\n// 连接到服务器（会自动启动子进程）\nawait client.connect(transport);\n\n// 可选：监听 stderr 输出（当 stderr: 'pipe' 时）\nif (transport.stderr) {\n  transport.stderr.on('data', (data) => {\n    console.error('Server stderr:', data.toString());\n  });\n}\n\n// 列出可用工具\nconst { tools } = await client.listTools();\nconsole.log('Available tools:', tools);\n\n// 调用工具\nconst result = await client.callTool({\n  name: 'calculator',\n  arguments: {\n    operation: 'add',\n    a: 5,\n    b: 3\n  }\n});\nconsole.log('Tool result:', result);\n\n// 使用完毕后关闭连接（会终止子进程）\nawait client.close();\n```\n\n:::tip\n**传输类型选择建议**：\n- **StreamHTTP**: 适用于网络服务、微服务架构、Web 应用集成\n- **SSE**: 传统方式，主要用于向下兼容\n- **Stdio**: 适用于命令行工具、本地脚本、桌面应用集成（如 Claude Desktop、Cursor、Trae 等 AI 编辑器）\n:::\n\n### 与编辑器集成\n\n在 Cursor/Trae/Claude Desktop 等编辑器的配置文件中添加您的 MCP 服务器：\n\nstdio 示例如下：\n\n```json\n{\n  \"mcpServers\": {\n    \"my-midway-mcp-server\": {\n      \"command\": \"node\",\n      \"args\": [\"bootstrap.js\"],\n      \"env\": {\n        \"NODE_ENV\": \"production\"\n      }\n    }\n  }\n}\n```\n\nstream-http 示例如下：\n\n```json\n{\n  \"mcpServers\": {\n    \"my-midway-mcp-server\": {\n      \"url\": \"http://localhost:3000/mcp\"\n    }\n  }\n}\n```\n\n## 最佳实践\n\n### 1. 类型安全\n\n使用 TypeScript 泛型确保类型安全：\n\n```typescript\nimport { Tool, ToolConfig } from '@midwayjs/mcp';\nimport { z } from 'zod';\n\n// 定义清晰的输入类型\nconst userSchema = {\n  userId: z.string().min(1),\n  includeProfile: z.boolean().optional()\n} as const;\n\nconst userToolConfig: ToolConfig<typeof userSchema> = {\n  description: 'Get user information',\n  inputSchema: userSchema\n};\n\n@Tool('get_user', userToolConfig)\nexport class UserTool implements IMcpTool {\n  // TypeScript 会自动推断 args 的类型\n  async execute(args: { userId: string; includeProfile?: boolean }) {\n    // 实现逻辑\n  }\n}\n```\n\n### 2. 错误处理\n\n正确处理错误并返回有意义的错误信息：\n\n```typescript\n@Tool('risky_operation', config)\nexport class RiskyTool implements IMcpTool {\n  async execute(args: any): Promise<CallToolResult> {\n    try {\n      const result = await this.performRiskyOperation(args);\n      return {\n        content: [{ type: 'text', text: result }]\n      };\n    } catch (error) {\n      return {\n        content: [{ \n          type: 'text', \n          text: `Error: ${error.message}` \n        }],\n        isError: true\n      };\n    }\n  }\n}\n```\n\n### 3. 资源管理\n\n合理管理资源连接和清理：\n\n```typescript\n@Resource('database_query', config)\nexport class DatabaseResource implements IMcpResource {\n  @Inject()\n  databaseService: DatabaseService;\n\n  async handle(uri: URL): Promise<ReadResourceResult> {\n    const connection = await this.databaseService.getConnection();\n    \n    try {\n      const result = await connection.query(this.parseQuery(uri));\n      return {\n        contents: [{\n          uri: uri.toString(),\n          mimeType: 'application/json',\n          text: JSON.stringify(result)\n        }]\n      };\n    } finally {\n      await connection.close();\n    }\n  }\n}\n```\n\n## 常见问题\n\n### Q: MCP 与传统 API 有什么区别？\n\nA: MCP 是专门为 AI 模型设计的协议，提供了语义化的接口描述、类型安全的参数验证和上下文感知的交互。与传统 REST API 相比，MCP 更适合 AI 模型理解和使用。\n\n### Q: 如何在生产环境中部署 MCP 服务？\n\nA: 推荐使用 `stream-http` 传输类型，配合反向代理（如 Nginx）进行负载均衡。确保启用适当的安全措施和监控。\n\n### Q: 可以在一个应用中同时提供多种传输类型吗？\n\nA: 当前版本每个应用实例只支持一种传输类型。如需支持多种传输类型，建议部署多个应用实例。\n\n### Q: 如何调试 MCP 服务？\n\nA: 使用日志记录、MCP 客户端工具进行测试，或者使用官方提供的 [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector)\n\n\n\n## 相关链接\n\n- [MCP 官方文档](https://modelcontextprotocol.io/)\n- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)\n- [Claude Desktop MCP 配置](https://docs.anthropic.com/claude/docs/mcp)","isInternal":false,"tokens":6862,"sizeBytes":24037},{"name":"mikro.md","path":"site/docs/extensions/mikro.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/mikro.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"import Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n# MikroORM\n\n本章节介绍用户如何在 midway 中使用 MikroORM。 MikroORM 是基于数据映射器、工作单元和身份映射模式的 Node.js 的 TypeScript ORM。\n\nMikroORM 的官网文档在 [这里](https://mikro-orm.io/docs)。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 关于升级\n\n* 从 `v3.14.0` 版本的组件开始，支持 mikro v5/v6 版本，由于 mikro v5 到 v6 有较大的变化，如从 mikro 老版本升级请提前阅读 [Upgrading from v5 to v6](https://mikro-orm.io/docs/upgrading-v5-to-v6)\n* 组件示例已更新为 v6 版本\n* MikroORM v7 已切换为 native ESM，并调整了装饰器包路径。v7 项目请使用 `@midwayjs/mikro7`，不要和 `@midwayjs/mikro` 混用。\n\n\n\n## 安装组件\n\n\n安装 mikro 组件，提供接入 mikro-orm 的能力。`@midwayjs/mikro` 对应 MikroORM v6。\n\n\n```bash\n$ npm i @midwayjs/mikro@4 @mikro-orm/core@^6 @mikro-orm/sqlite@^6 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/mikro\": \"^4.0.0\",\n    \"@mikro-orm/core\": \"^6.0.2\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n同时，还需要引入对应数据库的适配包。\n\n比如：\n\n```typescript\n{\n  \"dependencies\": {\n    // sqlite\n    \"@mikro-orm/sqlite\": \"^6.0.2\",\n\n    // mysql\n    \"@mikro-orm/mysql\": \"^6.0.2\",\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n更多驱动程序请查看 [官方文档](https://mikro-orm.io/docs/usage-with-sql/)。\n\n如果使用 MikroORM v7，请安装独立的 v7 组件。\n\n```bash\n$ npm i @midwayjs/mikro7@4 @mikro-orm/core@^7 @mikro-orm/decorators@^7 @mikro-orm/sqlite@^7 --save\n```\n\n对应 `package.json` 依赖如下。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/mikro7\": \"^4.0.0\",\n    \"@mikro-orm/core\": \"^7.0.0\",\n    \"@mikro-orm/decorators\": \"^7.0.0\",\n    \"@mikro-orm/sqlite\": \"^7.0.0\"\n  }\n}\n```\n\nv7 使用 legacy TypeScript 装饰器时，Entity 装饰器需要从 `@mikro-orm/decorators/legacy` 导入，并在配置中显式设置 `metadataProvider: ReflectMetadataProvider`。同时，项目的 TypeScript 配置需要使用 `moduleResolution: \"nodenext\"`、`\"node20\"` 或 `\"bundler\"`。\n\n\n\n## 引入组件\n\n\n在 `src/configuration.ts` 引入 mikro 组件，示例如下。\n\n```typescript\n// configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as mikro from '@midwayjs/mikro';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    mikro\t\t\t\t// 加载 mikro 组件\n  ],\n  importConfigs: [\n  \tjoin(__dirname, './config')\n  ]\n})\nexport class MainConfiguration {\n\n}\n```\n\n\n\n## 基础使用\n\n和其他 orm 框架类似，都是分为几个步骤：\n\n- 1、定义 Entity\n- 2、配置数据源\n- 3、获取 EntityModel 进行调用\n\n下面的更多 Entity 代码请查看 [示例](https://github.com/midwayjs/midway/tree/main/packages/mikro/test/fixtures/base-fn-origin)。\n\n\n\n### 目录结构\n\n一个基础的参考目录结构如下。\n\n```\nMyProject\n├── src\n│   ├── config\n│   │   └── config.default.ts\n│   ├── entity\n│   │   ├── book.entity.ts\n│   │   ├── index.ts\n│   │   └── base.ts\n│   ├── configuration.ts\n│   └── service\n├── .gitignore\n├── package.json\n├── README.md\n└── tsconfig.json\n```\n\n\n\n### 定义 Entity\n\n定义基础的 Entity。\n\n```typescript\n// src/entity/BaseEntity.ts\nimport { PrimaryKey, Property } from '@mikro-orm/core';\n\nexport abstract class BaseEntity {\n\n  @PrimaryKey()\n  id!: number;\n\n  @Property()\n  createdAt: Date = new Date();\n\n  @Property({ onUpdate: () => new Date() })\n  updatedAt: Date = new Date();\n\n}\n```\n\n定义实际的 Entity，包含一对多，多对多等关系。\n\n```typescript\n// src/entity/book.entity.ts\nimport { Cascade, Collection, Entity, ManyToMany, ManyToOne, Property } from '@mikro-orm/core';\nimport { Author, BookTag, Publisher } from './index';\nimport { BaseEntity } from './base';\n\n@Entity()\nexport class Book extends BaseEntity {\n\n  @Property()\n  title: string;\n\n  @ManyToOne(() => Author)\n  author: Author;\n\n  @ManyToOne(() => Publisher, { cascade: [Cascade.PERSIST, Cascade.REMOVE], nullable: true })\n  publisher?: Publisher;\n\n  @ManyToMany(() => BookTag)\n  tags = new Collection<BookTag>(this);\n\n  @Property({ nullable: true })\n  metaObject?: object;\n\n  @Property({ nullable: true })\n  metaArray?: any[];\n\n  @Property({ nullable: true })\n  metaArrayOfStrings?: string[];\n\n  constructor(title: string, author: Author) {\n    super();\n    this.title = title;\n    this.author = author;\n  }\n\n}\n```\n\n\n\n### 配置数据源\n\nmikro v5 和 v6 略有不同。\n\n<Tabs>\n<TabItem value=\"v6\" label=\"mikro v6\">\n\n```typescript\n// src/config/config.default\nimport { Author, BaseEntity, Book, BookTag, Publisher } from '../entity';\nimport { join } from 'path';\nimport { SqliteDriver } from '@mikro-orm/sqlite';\n\nexport default (appInfo) => {\n  return {\n    mikro: {\n      dataSource: {\n        default: {\n          dbName: join(__dirname, '../../test.sqlite'),\n          driver: SqliteDriver,\t\t// 这里使用了 sqlite 做示例\n          allowGlobalContext: true,\n          // 实体形式\n          entities: [Author, Book, BookTag, Publisher, BaseEntity],\n          // 支持如下的扫描形式，为了兼容我们可以同时进行.js和.ts匹配️\n          entities: [\n            'entity',                        // 指定目录\n            '**/entity/*.entity.{j,t}s',     // 通配加后缀匹配\n          ],\n        }\n      }\n    }\n  }\n}\n```\n\n</TabItem>\n\n<TabItem value=\"v5\" label=\"mikro v5\">\n\n```typescript\n// src/config/config.default\nimport { Author, BaseEntity, Book, BookTag, Publisher } from '../entity';\nimport { join } from 'path';\n\nexport default (appInfo) => {\n  return {\n    mikro: {\n      dataSource: {\n        default: {\n          dbName: join(__dirname, '../../test.sqlite'),\n          type: 'sqlite',\t\t\t// 这里使用了 sqlite 做示例\n          allowGlobalContext: true,\n          // 实体形式\n          entities: [Author, Book, BookTag, Publisher, BaseEntity],\n          // 支持如下的扫描形式，为了兼容我们可以同时进行.js和.ts匹配️\n          entities: [\n            'entity',                        // 指定目录\n            '**/entity/*.entity.{j,t}s',     // 通配加后缀匹配\n          ],\n        }\n      }\n    }\n  }\n}\n```\n\n</TabItem>\n</Tabs>\n\n:::tip\n\nmikro 的 `entities` 字段配置已经经过框架处理，该字段配置请不要参考原始文档。\n\n:::\n\n\n\n### 增删查改\n\n在业务代码中，可以使用 `InjectRepository` 注入 `Repository` 对象执行简单的查询操作。其它的增删改操作可以通过配合`EntityManger ` 的 `persist` 和 `flush` 接口来实现，使用 `InjectEntityManager` 可以直接注入 `EntityManager` 对象，也可以通过`repository.getEntityManager()`获取。\n\n:::caution\n\n* 1、从 5.7 版本开始，MikroORM 将原来 `Repository` 上 `persist` 和 `flush` 等接口标为*弃用*，并计划在 v6 版本中 [彻底移除](https://github.com/mikro-orm/mikro-orm/discussions/3989)，建议直接调用`EntityManager`上的相关接口\n* 2、v6 已经彻底 [弃用](https://mikro-orm.io/docs/upgrading-v5-to-v6#removed-methods-from-entityrepository) 上述接口\n\n:::\n\n```typescript\n// src/service/book.service.ts\nimport { Book } from './entity/book.entity';\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityManager, InjectRepository } from '@midwayjs/mikro';\nimport { QueryOrder } from '@mikro-orm/core';\nimport { EntityManager, EntityRepository } from '@mikro-orm/mysql'; // 需要使用数据库驱动对应的类来执行操作\n\n@Provide()\nexport class BookService {\n\n  @InjectRepository(Book)\n  bookRepository: EntityRepository<Book>;\n\n  @InjectEntityManager()\n  em: EntityManager;\n\n  async queryByRepo() {\n    // 使用Repository查询\n    const books = await this.bookRepository.findAll({\n      populate: ['author'],\n      orderBy: { title: QueryOrder.DESC },\n      limit: 20,\n    });\n    return books;\n  }\n\n  async createBook() {\n    const book = new Book({ title: 'b1', author: { name: 'a1', email: 'e1' } });\n    // 标记保存Book\n    this.em.persist(book);\n    // 执行所有变更\n    await this.em.flush();\n    return book;\n  }\n}\n```\n\n## 高级功能\n\n### 获取数据源\n\n数据源即创建出的数据源对象，我们可以通过注入内置的数据源管理器来获取。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport { MikroDataSourceManager } from '@midwayjs/mikro';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n\n  async onReady(container: IMidwayContainer) {\n    const dataSourceManager = await container.getAsync(MikroDataSourceManager);\n    const orm = dataSourceManager.getDataSource('default');\n    const connection = orm.em.getConnection();\n    // ...\n  }\n}\n```\n\n从 v3.8.0 开始，也可以通过装饰器注入。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport { InjectDataSource } from '@midwayjs/mikro';\nimport { MikroORM, IDatabaseDriver, Connection } from '@mikro-orm/core';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n\n  // 注入默认数据源\n  @InjectDataSource()\n  defaultDataSource: MikroORM<IDatabaseDriver<Connection>>;\n\n  // 注入自定义数据源\n  @InjectDataSource('default1')\n  customDataSource: MikroORM<IDatabaseDriver<Connection>>;\n\n  async onReady(container: IMidwayContainer) {\n    // ...\n  }\n}\n```\n\n\n\n### 日志\n\n可以通过配置将 midway 的 logger 添加到 mikro 中，用于记录 sql 等信息。\n\n```typescript\n// src/config/config.default.ts\nexporg default {\n\tmidwayLogger: {\n    clients: {\n      mikroLogger: {\n        // ...\n      }\n    }\n  },\n  mikro: {\n    dataSource: {\n      default: {\n        entities: [Author, Book, BookTag, Publisher, BaseEntity],\n        // ...\n        logger: 'mikroLogger',\n      }\n    },\n  }\n}\n```\n\n默认情况下  mikro 自带颜色，也会将其写入文件，可以通过配置关闭。\n\n```typescript\n// src/config/config.default.ts\nexporg default {\n\tmidwayLogger: {\n    clients: {\n      mikroLogger: {\n        transports: {\n          console: {\n            autoColors: false,\n          }，\n          file: {\n            fileLogName: 'mikro.log'，\n          },\n        },\n      }\n    }\n  },\n  mikro: {\n    dataSource: {\n      default: {\n        entities: [Author, Book, BookTag, Publisher, BaseEntity],\n        // ...\n        logger: 'mikroLogger',\n        colors: false,\n      }\n    },\n  }\n}\n```\n\n\n\n## 常见问题\n\n\n\n### 1、Node 版本\n\nMikro-orm 对 Node 版本有一些限制，必须为 `>=14.0.0` ，所以 `@midwayjs/mikro` 组件的使用规则也如此。\n\n\n\n### 2、Identity Map\n\nMikro-orm 内部查询有一个 [Identity Map](https://mikro-orm.io/docs/identity-map) 的概念，Midway 已经在所有的内置 Framework 的中间件内置加入了该功能，如果在非请求链路调用场景下使用，比如 `src/configuration` 中，可以开启 `allowGlobalContext` 选项。\n\n\n\n### 3、多库的支持\n\n和其他数据库一样，Midway 支持多数据源的配置。\n\n```typescript\n// src/config/config.default\nimport { Author, BaseEntity, Book, BookTag, Publisher } from '../entity';\nimport { SqlHighlighter } from '@mikro-orm/sql-highlighter';\nimport { join } from 'path';\n\nexport default (appInfo) => {\n  return {\n    mikro: {\n      dataSource: {\n        custom1: {\n          // ...\n        },\n        custom2: {\n          // ...\n        }\n      }\n    }\n  }\n}\n```\n\n注意在使用时，需要传递来自哪个数据源。\n\n```typescript\n// ...\n\n@Provide()\nexport class BookController {\n\n  @InjectRepository(Book, 'custom1')\n  bookRepository: EntityRepository<Book>;\n\n  async findBookAndQuery() {\n    // ...\n  }\n}\n```\n","isInternal":false,"tokens":3511,"sizeBytes":12056},{"name":"mongodb.md","path":"site/docs/extensions/mongodb.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/mongodb.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# MongoDB\n\n在这一章节中，我们选择 [Typegoose](https://github.com/typegoose/typegoose) 作为基础的 MongoDB ORM 库。就如同他描述的那样 \" Define Mongoose models using TypeScript classes\"，和 TypeScript 结合的很不错。\n\n简单的来说，Typegoose 使用 TypeScript 编写 Mongoose 模型的 “包装器”，它的大部分能力还是由 [mongoose](https://www.npmjs.com/package/mongoose) 库来提供的。\n\n也可以直接选择 [mongoose](https://www.npmjs.com/package/mongoose) 库来使用，我们会分别描述。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n:::tip\n\n- 1、当前模块从 v3.4.0 开始已经重构，历史写法兼容，如果查询历史文档，请参考 [这里](../legacy/mongodb)。\n- 2、如果代码中有读取配置，注意 `mongoose.clients` 可能会读不到，请使用 `mongoose.dataSource`。\n\n:::\n\n\n\n## 和老写法的区别\n\n如果想使用新版本的用法，请参考下面的流程，将老代码进行修改，新老代码请勿混用。\n\n升级方法：\n\n- 1、无需再使用 `EntityModel` 装饰器\n- 3、在 `src/config.default` 的 `mongoose` 部分配置调整，参考下面的数据源配置部分\n  - 3.1 修改为数据源的形式 `mongoose.dataSource`\n  - 3.2 将实体模型在数据源的 `entities` 字段中声明\n\n\n\n\n## Mongoose 版本依赖\n\n\nmongoose 和你服务器使用的 MongoDB Server 的版本也有着一定的关系，如下，请务必注意。\n\n\n- MongoDB Server 2.4.x: mongoose ^3.8 or 4.x\n- MongoDB Server 2.6.x: mongoose ^3.8.8 or 4.x\n- MongoDB Server 3.0.x: mongoose ^3.8.22, 4.x, or 5.x\n- MongoDB Server 3.2.x: mongoose ^4.3.0 or 5.x\n- MongoDB Server 3.4.x: mongoose ^4.7.3 or 5.x\n- MongoDB Server 3.6.x: mongoose 5.x\n- MongoDB Server 4.0.x: mongoose ^5.2.0\n- MongoDB Server 4.2.x: mongoose ^5.7.0\n- MongoDB Server 4.4.x: mongoose ^5.10.0\n- MongoDB Server 5.x: mongoose ^6.0.0\n\n\n**mongoose 相关的依赖比较复杂，且对应不同的版本，现阶段，我们使用的主要是 mongoose v5 和 v6。**\n\n\n:::info\n从 mongoose@v5.11.0 开始，mongoose 官方支持了定义，所以不再需要安装 @types/mongoose 依赖包。\n:::\n\n安装包依赖版本如下：\n\n**支持 MongoDB Server 6.x**\n\n```json\n  \"dependencies\": {\n    \"mongoose\": \"^7.0.0\",\n    \"@typegoose/typegoose\": \"^10.0.0\",\t\t// 使用 typegoose 需要安装此依赖\n  },\n```\n\n**支持 MongoDB Server 5.x**\n\n```json\n  \"dependencies\": {\n    \"mongoose\": \"^6.0.7\",\n    \"@typegoose/typegoose\": \"^9.0.0\",\t\t// 使用 typegoose 需要安装此依赖\n  },\n```\n\n\n**支持 MongoDB Server 4.4.x**\n\n\n以下版本不需要安装额外定义包。\n```json\n  \"dependencies\": {\n    \"mongoose\": \"^5.13.3\",\n    \"@typegoose/typegoose\": \"^8.0.0\",   // 使用 typegoose 需要安装此依赖\n  },\n```\n\n\n以下版本需要安装额外定义包（不推荐）。\n```json\n \"dependencies\": {\n    \"mongodb\": \"3.6.3\",\t\t\t\t\t\t\t\t\t// mongoose 内部写死了该版本\n    \"mongoose\": \"~5.10.18\",\n    \"@typegoose/typegoose\": \"^7.0.0\",   // 使用 typegoose 需要安装此依赖\n },\n \"devDependencies\": {\n    \"@types/mongodb\": \"3.6.3\",\t\t\t\t\t// 只能使用此版本\n    \"@types/mongoose\": \"~5.10.3\",\n }\n```\n\n\n其余的 MongoDB 安装模块类似，未测。\n\n\n\n## 使用 Typegoose\n\n\n### 1、安装组件\n\n\n安装 Typegoose 组件，提供访问 MongoDB 的能力。\n\n\n**请务必注意，请查看第一小节提前编写/安装 mongoose 等相关依赖包。**\n```bash\n$ npm i @midwayjs/typegoose@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    // 组件\n    \"@midwayjs/typegoose\": \"^4.0.0\",\n    // 上一节中的 mongoose 依赖\n  },\n  \"devDependencies\": {\n    // 上一节中的 mongoose 依赖\n    // ...\n  }\n}\n```\n\n\n\n安装后需要手动在 `src/configuration.ts` 配置，代码如下。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as typegoose from '@midwayjs/typegoose';\n\n@Configuration({\n  imports: [\n    typegoose  \t\t\t\t\t\t\t\t\t\t\t// 加载 typegoose 组件\n  ],\n  importConfigs: [\n  \tjoin(__dirname, './config')\n  ]\n})\nexport class MainConfiguration {\n\n}\n```\n\n:::info\n在该组件中，midway 只是做了简单的配置规则化，并将其注入到初始化流程中。\n:::\n\n### 2、简单的目录结构\n\n\n我们以一个简单的项目举例，其他结构请自行参考。\n\n\n```text\nMyProject\n├── src              \t\t\t\t\t\t\t// TS 根目录\n│   ├── config\n│   │   └── config.default.ts \t\t// 应用配置文件\n│   ├── entity       \t\t\t\t\t\t\t// 实体（数据库 Model) 目录\n│   │   └── user.ts  \t\t\t\t\t  \t// 实体文件\n│   ├── configuration.ts     \t\t\t// Midway 配置文件\n│   └── service      \t\t\t\t\t\t\t// 其他的服务目录\n├── .gitignore\n├── package.json\n├── README.md\n└── tsconfig.json\n```\n\n\n在这里，我们的数据库实体主要放在 `entity` 目录（非强制），这只是一个简单的约定。\n\n\n\n### 3、创建实体文件\n\n比如在 `src/entity/user.ts` 中。\n\n\n```typescript\nimport { prop } from '@typegoose/typegoose';\n\nexport class User {\n  @prop()\n  public name?: string;\n\n  @prop({ type: () => [String] })\n  public jobs?: string[];\n}\n```\n\n等价于使用 mongoose 的下列代码\n\n```typescript\nconst userSchema = new mongoose.Schema({\n  name: String,\n  jobs: [{ type: String }]\n});\n\nconst User = mongoose.model('User', userSchema);\n```\n\n:::info\n所以说，typegoose 只是简化了 model 的创建过程。\n:::\n\n\n\n\n### 4、配置连接信息\n\n\n在 `src/config/config.default.ts` 中加入连接的配置。\n\n```typescript\nimport { User } from '../entity/user';\n\nexport default {\n  // ...\n  mongoose: {\n    dataSource: {\n      default: {\n        uri: 'mongodb://localhost:27017/test',\n        options: {\n          useNewUrlParser: true,\n          useUnifiedTopology: true,\n          user: '***********',\n          pass: '***********'\n        },\n        // 关联实体\n        entities: [ User ]\n      }\n    }\n  },\n}\n```\n\n如需以目录扫描形式关联，请参考 [数据源管理](../data_source)。\n\n\n\n### 5、引用实体，调用数据库\n\n\n示例代码如下：\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typegoose';\nimport { ReturnModelType } from '@typegoose/typegoose';\nimport { User } from '../entity/user';\n\n@Provide()\nexport class TestService {\n\n  @InjectEntityModel(User)\n  userModel: ReturnModelType<typeof User>;\n\n  async getTest(){\n    // create data\n    const { _id: id } = await this.userModel.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User); // an \"as\" assertion, to have types for all properties\n\n    // find data\n    const user = await this.userModel.findById(id).exec();\n    console.log(user)\n  }\n}\n```\n\n\n### 6、多库的情况\n\n首先定义多个实体。\n\n```typescript\nclass User {\n\n  @prop()\n  public name?: string;\n\n  @prop({ type: () => [String] })\n  public jobs?: string[];\n}\n\nclass User2 {\n\n  @prop()\n  public name?: string;\n\n  @prop({ type: () => [String] })\n  public jobs?: string[];\n}\n```\n\n\n将实体配置到多个数据源。\n\n\n在 `src/config/config.default.ts` 中加入数据源的配置。\n```typescript\nimport { User, User2 } from '../entity/user';\n\nexport default {\n  // ...\n  mongoose: {\n    dataSource: {\n      default: {\n        uri: 'mongodb://localhost:27017/test',\n        options: {\n          useNewUrlParser: true,\n          useUnifiedTopology: true,\n          user: '***********',\n          pass: '***********'\n        },\n        entities: [ User ]\n      },\n      db1: {\n        uri: 'mongodb://localhost:27017/test1',\n        options: {\n          useNewUrlParser: true,\n          useUnifiedTopology: true,\n          user: '***********',\n          pass: '***********'\n        },\n        entities: [ User2 ]\n      }\n    }\n  },\n}\n```\n\n\n定义实例时使用固定的连接，在扫描 dataSource 配置 Model 会自动关联 mongoose连接(`getModelForClass(Model, { existingConnection: conn })`)。\n\n```typescript\n@Provide()\nexport class TestService{\n\n  @InjectEntityModel(User)\n  userModel: ReturnModelType<typeof User>;\n\n  @InjectEntityModel(User2)\n  user2Model: ReturnModelType<typeof User2>;\n\n  async getTest(){\n    const { _id: id } = await this.userModel.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User); // an \"as\" assertion, to have types for all properties\n    const user = await this.userModel.findById(id).exec();\n    console.log(user)\n\n    const { _id: id2 } = await this.user2Model.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User2); // an \"as\" assertion, to have types for all properties\n    const user2 = await this.user2Model.findById(id2).exec();\n    console.log(user2)\n  }\n}\n\n```\n\n\n\n### 7、关于 schemaOptions\n\nTypegoose 预留了一个 `setGlobalOptions`  方法用来设置 [schemaOptions](https://typegoose.github.io/typegoose/docs/api/decorators/model-options#schemaoptions) 和一些其他全局性的 [配置](https://typegoose.github.io/typegoose/docs/api/decorators/model-options#options-1)。\n\n我们可以在项目配置加载时设置它。\n\n```typescript\n// srcconfiguration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as typegoose from '@midwayjs/typegoose';\nimport * as Typegoose from '@typegoose/typegoose';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  async onConfigLoad() {\n\n    Typegoose.setGlobalOptions({\n      schemaOptions: {\n        // ...\n      },\n      options: { allowMixed: Severity.ERROR }\n    });\n    // ...\n  }\n}\n```\n\n\n\n\n\n## 直接使用 mongoose\n\nmongoose 组件是 typegoose 的基础组件，有时候我们可以直接使用它。\n\n\n### 1、安装组件\n\n\n**请务必注意，请查看第一小节提前编写/安装 mongoose 等相关依赖包。**\n\n```bash\n$ npm i @midwayjs/mongoose@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    // 组件\n    \"@midwayjs/mongoose\": \"^4.0.0\",\n    // 上一节中的 mongoose 依赖\n  },\n  \"devDependencies\": {\n    // 上一节中的 mongoose 依赖\n    // ...\n  }\n}\n```\n\n\n\n### 2、开启组件\n\n\n安装后需要手动在 `src/configuration.ts` 配置，代码如下。\n```typescript\n// configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as mongoose from '@midwayjs/mongoose';\n\n@Configuration({\n  imports: [\n    mongoose  \t\t\t\t\t\t\t\t\t\t\t// 加载 mongoose 组件\n  ],\n  importConfigs: [\n  \tjoin(__dirname, './config')\n  ]\n})\nexport class MainConfiguration {\n\n}\n```\n\n\n\n\n### 2、配置\n\n和 typegoose 相同，或者说 typegoose 使用的就是 mongoose 的配置。\n\n不管是单库还是多库，数据源配置都是类似的。\n\n\n单库：\n```typescript\nexport default {\n  // ...\n  mongoose: {\n    dataSource: {\n      default: {\n        uri: 'mongodb://localhost:27017/test',\n        options: {\n          useNewUrlParser: true,\n          useUnifiedTopology: true,\n          user: '***********',\n          pass: '**********'\n        }\n      }\n    }\n  },\n}\n```\n多库：\n```typescript\nexport default {\n  // ...\n  mongoose: {\n    dataSource: {\n      default: {\n        uri: 'mongodb://localhost:27017/test',\n        options: {\n          useNewUrlParser: true,\n          useUnifiedTopology: true,\n          user: '***********',\n          pass: '***********'\n        }\n      },\n      db1: {\n        uri: 'mongodb://localhost:27017/test1',\n        options: {\n          useNewUrlParser: true,\n          useUnifiedTopology: true,\n          user: '***********',\n          pass: '***********'\n        }\n      }\n    }\n  },\n}\n```\n\n\n\n### 3、使用\n\n\n当我们希望获取到原始的连接对象时，可以直接使用封装好的 `MongooseConnectionService` 对象。\n```typescript\nimport { Provide, Inject, Init } from '@midwayjs/core';\nimport { MongooseDataSourceManager } from '@midwayjs/mongoose';\nimport { Schema, Document } from 'mongoose';\n\ninterface User extends Document {\n  name: string;\n  email: string;\n  avatar: string;\n}\n\n@Provide()\nexport class TestService {\n\n  @Inject()\n  dataSourceManager: MongooseDataSourceManager;\n\n  @Init()\n  async init() {\n    // get default connection\n    this.conn = this.dataSourceManager.getDataSource('default');\n  }\n\n  async invoke(){\n    const schema = new Schema<User>({\n      name: { type: String, required: true },\n      email: { type: String, required: true },\n      avatar: String\n    });\n    const UserModel = this.conn.model<User>('User', schema);\n    const doc = new UserModel({\n      name: 'Bill',\n      email: 'bill@initech.com',\n      avatar: 'https://i.imgur.com/dM7Thhn.png'\n    });\n    await doc.save();\n  }\n}\n\n```\n\n\n\n\n\n\n## 常见问题\n\n\n### 1、E002: You are using a NodeJS Version below 12.22.0\n\n\n在新版本 @typegoose/typegoose (v8, v9) 中增加了 Node 版本的校验，如果你的 Node.js 版本低于 v12.22.0，就会出现这个提示。\n\n\n普通情况下，请升级 Node.js 到这个版本以上即可解决。\n\n\n在特殊场景下，比如 Serverless 无法修改 Node.js 版本且版本低于 v12.22 的情况下，由于 v12 版本子版本其实都可以，可以通过临时修改 process.version 绕过。\n\n\n```typescript\n// src/configuration.ts\n\nObject.defineProperty(process, 'version', {\n  value: 'v12.22.0',\n  writable: true,\n});\n\n// other code\n\nexport class MainConfiguration {}\n```\n\n\n\n","isInternal":false,"tokens":3913,"sizeBytes":13526},{"name":"mqtt.md","path":"site/docs/extensions/mqtt.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/mqtt.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# MQTT\n\nMQTT是用于物联网 (IoT) 的OASIS标准消息传递协议。它被设计为非常轻量级的发布/订阅消息传输，非常适合以较小的代码占用空间和最小的网络带宽连接远程设备。MQTT目前广泛应用于汽车、制造、电信、石油和天然气等行业。\n\n相关信息：\n\n| 描述              |              |\n| ----------------- | ------------ |\n| 可用于标准项目    | ✅            |\n| 可用于 Serverless | 可以发布消息 |\n| 可用于一体化      | ✅            |\n| 包含独立主框架    | ✅            |\n| 包含独立日志      | ✅            |\n\n\n\n## 版本要求\n\n由于 [mqtt](https://github.com/mqttjs/MQTT.js) 库本身的要求，所需要的版本为 **Node.js >= 16**\n\n\n\n## 前置依赖\n\n由于 MQTT 需要 Broker 作为中转传输，你需要自行部署 MQTT Broker 服务，本文档不提供 MQTT 服务本身的部署指导。\n\n\n\n## 安装组件\n\n\n安装 mqtt 组件。\n\n\n```bash\n$ npm i @midwayjs/mqtt@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/mqtt\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n## 启用组件\n\n在 `src/configuration.ts` 中引入组件\n\n```typescript\n// ...\nimport * as mqtt from '@midwayjs/mqtt';\n\n@Configuration({\n  imports: [\n    // ...other components\n    mqtt,\n  ],\n})\nexport class MainConfiguration {}\n```\n\n\n\n由于 MQTT 分为 **订阅者（subscriber）** 和 **发布者（publisher）** 两部分，两个可以独立使用，我们将分别介绍。\n\n\n\n## 订阅服务\n\n### 基础配置\n\n通过 `sub` 字段和 `@MqttSubscriber` 装饰器，我们可以配置多个订阅者。\n\n比如，下面的 `sub1` 和 `sub2` 就是两个不同的订阅者。\n\n```typescript\n// src/config/config.default\n\nexport default {\n  mqtt: {\n    sub: {\n      sub1: {\n        // ...\n      },\n      sub2: {\n        // ...\n      }\n    }\n  }\n}\n```\n\n最简单的订阅者配置需要几个字段，订阅的地址和订阅的 Topic。\n\n```typescript\n// src/config/config.default\n\nexport default {\n  mqtt: {\n    sub: {\n      sub1: {\n        connectOptions: {\n          host: 'test.mosquitto.org',\n          port: 1883,\n        },\n        subscribeOptions: {\n          topicObject: 'test',\n        },\n      },\n      sub2: {\n        // ...\n      }\n    }\n  }\n}\n```\n\n `sub1` 订阅者配置了 `connectOptions` 和 `subscribeOptions` ，分别代表连接配置和订阅配置。\n\n### 订阅实现\n\n我们可以在目录中提供一个标准的订阅器实现，比如 `src/consumer/sub1.subscriber.ts`。\n\n```typescript\n// src/consumer/sub1.subscriber.ts\n\nimport { ILogger, Inject } from '@midwayjs/core';\nimport { Context, IMqttSubscriber, MqttSubscriber } from '@midwayjs/mqtt';\n\n@MqttSubscriber('sub1')\nexport class Sub1Subscriber implements IMqttSubscriber {\n\n  @Inject()\n  ctx: Context;\n\n  async subscribe() {\n    // ...\n  }\n}\n```\n\n`@MqttSubscriber` 装饰器声明了一个订阅类实现，它的参数为订阅者的名字，比如我们配置文件中的 `sub1`。\n\n`IMqttSubscriber` 接口约定了一个 `subscribe` 方法，每当接收到新的消息时，这个方法就会被执行。\n\n和其他消息订阅机制一样，消息本身通过 `Context` 字段来传递。\n\n```typescript\n// ...\nexport class Sub1Subscriber implements IMqttSubscriber {\n  @Inject()\n  ctx: Context;\n\n  async subscribe() {\n    const payload = this.ctx.message.toString();\n    // ...\n  }\n}\n```\n\n`Context` 字段包括几个 mqtt 属性。\n\n| 属性        | 类型                           | 描述             |\n| ----------- | ------------------------------ | ---------------- |\n| ctx.topic   | string                         | 订阅 Topic       |\n| ctx.message | Buffer                         | 消息内容         |\n| ctx.packet  | IPublishPacket（来自 mqtt 库） | publish 的包信息 |\n\n\n\n## 消息发布\n\n### 基础配置\n\n消息发布也需要创建实例，配置本身使用了 [服务工厂](/docs/service_factory) 的设计模式。\n\n比如多实例配置如下：\n\n```typescript\n// src/config/config.default\n\nexport default {\n  mqtt: {\n    pub: {\n      clients: {\n        default: {\n          host: 'test.mosquitto.org',\n          port: 1883,\n        },\n        pub2: {\n          // ...\n        }\n      }\n    }\n  }\n}\n```\n\n上面的配置创建了名为 `default` 和 `pub2` 的两个实例。\n\n\n\n### 使用发布者\n\n如果实例名为 `default` ，则可以使用默认的消息发布类。\n\n比如：\n\n```typescript\n// src/service/user.service.ts\nimport { Provide, Inject } from '@midwayjs/core';\nimport { DefaultMqttProducer } from '@midwayjs/mqtt';\n\n@Provide()\nexport class UserService {\n  \n  @Inject()\n  producer: DefaultMqttProducer;\n  \n  async invoke() {\n    // 同步发布消息\n    this.producer.publish('test', 'hello world');\n    \n    // 异步发布\n    await this.producer.publishAsync('test', 'hello world');\n    \n    // 增加配置\n    await this.producer.publishAsync('test', 'hello world', {\n      qos: 2\n    });\n  }\n}\n```\n\n也可以使用内置的工厂类 `MqttProducerFactory` 注入不同的实例。\n\n```typescript\n// src/service/user.service.ts\nimport { Provide, Inject } from '@midwayjs/core';\nimport { MqttProducerFactory, DefaultMqttProducer } from '@midwayjs/mqtt';\n\n@Provide()\nexport class UserService {\n  \n  @InjectClient(MqttProducerFactory, 'pub2')\n  producer: DefaultMqttProducer;\n  \n  async invoke() {\n    // ...\n  }\n}\n```\n\n\n\n## 组件日志\n\n组件有着自己的日志，默认会将 `ctx.logger` 记录在 `midway-mqtt.log` 中。\n\n我们可以单独配置这个 logger 对象。\n\n```typescript\nexport default {\n  midwayLogger: {\n    // ...\n    mqttLogger: {\n      fileLogName: 'midway-mqtt.log',\n    },\n  }\n}\n```\n\n这个日志的输出格式，我们也可以单独配置。\n\n```typescript\nexport default {\n  mqtt: {\n    // ...\n    contextLoggerFormat: info => {\n      const { jobId, from } = info.ctx;\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} ${info.message}`;\n    },\n  }\n}\n```\n\n","isInternal":false,"tokens":1743,"sizeBytes":6059},{"name":"one-shot.md","path":"site/docs/extensions/one-shot.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/one-shot.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# 单次执行\n\n`@midwayjs/one-shot` 是一个只提供 Framework 的一次性脚本执行组件，适合用 IoC 容器组织依赖，并在应用内触发一次性的任务逻辑。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ❌    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ❌   |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n## 安装依赖\n\n在现有项目中安装 one-shot 组件依赖。\n\n```bash\n$ npm i @midwayjs/one-shot@4  --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/one-shot\": \"^4.0.0\"\n  }\n}\n```\n\n## 开启组件\n\n在入口配置中引入组件。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as oneShot from '@midwayjs/one-shot';\n\n@Configuration({\n  imports: [oneShot],\n})\nexport class MainConfiguration {}\n```\n\n## 在生命周期执行\n\n该组件不要求编写额外 Runner，推荐在 `onServerReady` 生命周期里直接执行一次性脚本逻辑。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as oneShot from '@midwayjs/one-shot';\nimport { ScriptService } from './service/script';\n\n@Configuration({\n  imports: [oneShot],\n})\nexport class MainConfiguration {\n  @Inject()\n  scriptService: ScriptService;\n\n  async onServerReady() {\n    await this.scriptService.runOnce();\n  }\n}\n```\n\n## 需要请求作用域时\n\n如果脚本逻辑需要依赖请求作用域（request scope），可以通过 framework 的 `runScript` 执行一个固定格式的 service 类，框架会为本次执行创建上下文并走中间件/过滤器链。\n\n```typescript\n// src/script/syncUser.ts\nimport { Provide } from '@midwayjs/core';\nimport { OneShotRunner, Context } from '@midwayjs/one-shot';\n\n@Provide()\nexport class SyncUserScript implements OneShotRunner<{ id: number }, void> {\n  async run(payload?: { id: number }, ctx?: Context) {\n    // use payload / ctx\n    void payload;\n    void ctx;\n  }\n}\n```\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as oneShot from '@midwayjs/one-shot';\nimport { Framework } from '@midwayjs/one-shot';\nimport { SyncUserScript } from './script/syncUser';\n\n@Configuration({\n  imports: [oneShot],\n})\nexport class MainConfiguration {\n  @Inject()\n  framework: Framework;\n\n  async onServerReady() {\n    await this.framework.runScript(SyncUserScript, { id: 42 });\n  }\n}\n```\n\n## 日志\n\n组件默认会注册一个名为 `oneShotLogger` 的 logger，默认写入 `midway-one-shot.log`。\n\n你可以在脚本服务里通过 `@Logger('oneShotLogger')` 注入使用，例如：\n\n```typescript\nimport { Logger, ILogger } from '@midwayjs/core';\nexport class ScriptService {\n  @Logger('oneShotLogger')\n  logger: ILogger;\n\n  async runOnce() {\n    this.logger.info('run one-shot task');\n  }\n}\n```\n\n如果希望自定义日志文件名或级别，可以在应用配置中覆盖 `midwayLogger.clients.oneShotLogger`：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  midwayLogger: {\n    clients: {\n      oneShotLogger: {\n        fileLogName: 'my-one-shot.log',\n        level: 'info',\n      },\n    },\n  },\n};\n```\n","isInternal":false,"tokens":942,"sizeBytes":3312},{"name":"orm.md","path":"site/docs/extensions/orm.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/orm.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# TypeORM\n\n[TypeORM](https://github.com/typeorm/typeorm) 是 `node.js` 现有社区最成熟的对象关系映射器（`ORM` ）。本文介绍如何在 Midway 中使用 TypeORM 。\n\n:::tip\n\n本模块是从 v3.4.0 开始为新版本，模块名有变化，历史写法部分兼容，如果查询历史文档，请参考 [这里](../legacy/orm)。\n\n:::\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 和老写法的区别\n\n旧模块为 `@midwayjs/orm` ，新模块为 `@midwayjs/typeorm`，区别如下：\n\n- 1、包名不同\n- 2、在 `src/config.default` 的部分配置调整\n  - 2.1 配置文件中的 key 不同 （orm => typeorm）\n  - 2.2修改为数据源的形式 `typeorm.dataSource`\n  - 2.3 实体模型类或者实体模型类的路径,需要在数据源的 `entities` 字段中声明\n  - 2.4 Subscriber 需要在数据源的 `subscribers` 字段中声明\n- 3、不再使用 `EntityModel` 装饰器，直接使用 typeorm 提供的能力\n\n\n\n## 安装组件\n\n\n安装 typeorm 组件，提供数据库 ORM 能力。\n\n\n```bash\n$ npm i @midwayjs/typeorm@4 typeorm@^1.0.0 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/typeorm\": \"^4.0.0\",\n    \"typeorm\": \"^1.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n## 引入组件\n\n\n在 `src/configuration.ts` 引入 orm 组件，示例如下。\n\n```typescript\n// configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as orm from '@midwayjs/typeorm';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    orm  \t\t\t\t\t\t\t\t\t\t\t\t\t\t// 加载 typeorm 组件\n  ],\n  importConfigs: [\n  \tjoin(__dirname, './config')\n  ]\n})\nexport class MainConfiguration {\n\n}\n```\n\n\n## 安装数据库 Driver\n\n\n常用数据库驱动如下，选择你对应连接的数据库类型安装：\n```bash\n# for MySQL or MariaDB，也可以使用 mysql2 替代\nnpm install mysql --save\nnpm install mysql2 --save\n\n# for PostgreSQL or CockroachDB\nnpm install pg --save\n\n# for SQLite\nnpm install better-sqlite3 --save\n\n# for Microsoft SQL Server\nnpm install mssql --save\n\n# for sql.js\nnpm install sql.js --save\n\n# for Oracle\nnpm install oracledb --save\n\n# for MongoDB(experimental)\nnpm install mongodb --save\n```\n\n:::info\n\n-  Oracle driver 比较特殊，需要查看 [文档](https://github.com/oracle/node-oracledb)\n- 不建议使用 typeorm 链接 mongodb，请使用 mongoose 组件\n\n:::\n\n\n\n\n## 简单的目录结构\n\n\n我们以一个简单的项目举例，其他结构请自行参考。\n\n\n```\nMyProject\n├── src              \t\t\t\t\t\t\t    // TS 根目录\n│   ├── config\n│   │   └── config.default.ts \t\t    // 应用配置文件\n│   ├── entity       \t\t\t\t\t\t\t    // 实体（数据库 Model) 目录\n│   │   └── photo.entity.ts  \t\t\t\t\t// 实体文件\n│   │   └── photoMetadata.entity.ts\n│   ├── configuration.ts     \t\t\t    // Midway 配置文件\n│   └── service      \t\t\t\t\t\t\t    // 其他的服务目录\n├── .gitignore\n├── package.json\n├── README.md\n└── tsconfig.json\n```\n\n\n\n\n在这里，我们的数据库实体主要放在 `entity` 目录（非强制），这只是一个简单的约定。\n\n\n\n## 入门\n\n下面，我们将以 mysql 举例。\n\n\n\n\n### 1、创建 Model\n\n\n我们通过模型和数据库关联，在应用中的模型就是数据库表，在 TypeORM 中，模型是和实体绑定的，每一个实体（Entity) 文件，即是 Model，也是实体（Entity）。\n\n\n在示例中，需要一个实体，我们这里拿 `photo` 举例。新建 entity 目录，在其中添加实体文件 `photo.entity.ts` ，一个简单的实体如下。\n```typescript\n// entity/photo.entity.ts\nexport class Photo {\n  id: number;\n  name: string;\n  description: string;\n  filename: string;\n  views: number;\n  isPublished: boolean;\n}\n```\n要注意，这里的实体文件的每一个属性，其实是和数据库表一一对应的，基于现有的数据库表，我们往上添加内容。\n\n\n### 2、定义实体模型\n\n\n我们使用 `Entity` 来定义一个实体模型类。\n```typescript\n// entity/photo.entity.ts\nimport { Entity } from 'typeorm';\n\n@Entity('photo')\nexport class Photo {\n  id: number;\n  name: string;\n  description: string;\n  filename: string;\n  views: number;\n  isPublished: boolean;\n}\n```\n\n如果表名和当前的实体名不同，可以在参数中指定。\n```typescript\n// entity/photo.entity.ts\nimport { Entity } from 'typeorm';\n\n@Entity('photo_table_name')\nexport class Photo {\n  id: number;\n  name: string;\n  description: string;\n  filename: string;\n  views: number;\n  isPublished: boolean;\n}\n```\n\n\n这些实体列也可以使用 [typeorm_generator](/docs/tool/typeorm_generator) 工具生成。\n\n\n### 3、添加数据库列\n\n\n通过 typeorm 提供的 `@Column` 装饰器来修饰属性，每一个属性对应一个列。\n\n\n```typescript\n// entity/photo.entity.ts\nimport { Entity, Column } from 'typeorm';\n\n@Entity()\nexport class Photo {\n\n  @Column()\n  id: number;\n\n  @Column()\n  name: string;\n\n  @Column()\n  description: string;\n\n  @Column()\n  filename: string;\n\n  @Column()\n  views: number;\n\n  @Column()\n  isPublished: boolean;\n\n}\n```\n\n现在 `id` ， `name` ， `description` ，`filename` ， `views` ， `isPublished` 列将添加到 `photo`  表中。数据库中的列类型是根据您使用的属性类型推断出来的，例如 number 将转换为整数，将字符串转换为 varchar，将布尔值转换为 bool，等等。但是您可以通过在 `@Column`装饰器中显式指定列类型来使用数据库支持的任何列类型。\n\n\n我们生成了带有列的数据库表，但是还剩下一件事。每个数据库表必须具有带主键的列。\n\n\n数据库列包括更多的列选项（ColumnOptions），比如修改列名，指定列类型，列长度等，更多的选项请参考 [官方文档](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/entities.md#%E5%88%97%E9%80%89%E9%A1%B9)。\n\n\n\n\n### 4、创建主键列\n\n\n每个实体必须至少具有一个主键列。要使列成为主键，您需要使用 `@PrimaryColumn`  装饰器。\n\n\n```typescript\n// entity/photo.entity.ts\nimport { Entity, Column, PrimaryColumn } from 'typeorm';\n\n@Entity()\nexport class Photo {\n\n  @PrimaryColumn()\n  id: number;\n\n  @Column()\n  name: string;\n\n  @Column()\n  description: string;\n\n  @Column()\n  filename: string;\n\n  @Column()\n  views: number;\n\n  @Column()\n  isPublished: boolean;\n\n}\n```\n### 5、创建自增主键列\n\n\n现在，如果要设置自增的 id 列，需要将 `@PrimaryColumn` 装饰器更改为 `@PrimaryGeneratedColumn`  装饰器：\n```typescript\n// entity/photo.entity.ts\nimport { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class Photo {\n\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column()\n  name: string;\n\n  @Column()\n  description: string;\n\n  @Column()\n  filename: string;\n\n  @Column()\n  views: number;\n\n  @Column()\n  isPublished: boolean;\n\n}\n```\n\n\n### 6、列数据类型\n\n\n接下来，让我们调整数据类型。默认情况下，字符串映射到类似 `varchar（255）` 的类型（取决于数据库类型）。 Number 映射为类似整数的类型（取决于数据库类型）。但是我们不希望所有列都限制为 varchars 或整数，这个时候可以做一些修改。\n\n\n```typescript\n// entity/photo.entity.ts\nimport { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class Photo {\n\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column({\n  \tlength: 100\n  })\n  name: string;\n\n  @Column('text')\n  description: string;\n\n  @Column()\n  filename: string;\n\n  @Column(\"double\")\n  views: number;\n\n  @Column()\n  isPublished: boolean;\n}\n```\n\n\n示例，不同列名\n```typescript\n@Column({\n  length: 100,\n  name: 'custom_name'\n})\nname: string;\n```\n\n\n此外还有有几种特殊的列类型可以使用：\n\n\n- `@CreateDateColumn` 是一个特殊列，自动为实体插入日期。\n- `@UpdateDateColumn` 是一个特殊列，在每次调用实体管理器或存储库的save时，自动更新实体日期。\n- `@VersionColumn` 是一个特殊列，在每次调用实体管理器或存储库的save时自动增长实体版本（增量编号）。\n- `@DeleteDateColumn` 是一个特殊列，会在调用 soft-delete（软删除）时自动设置实体的删除时间。\n\n比如：\n\n```typescript\n  @CreateDateColumn({\n    type: 'timestamp',\n  })\n  createdDate: Date;\n```\n\n列类型是特定于数据库的。您可以设置数据库支持的任何列类型。有关支持的列类型的更多信息，请参见[此处](https://github.com/typeorm/typeorm/blob/master/docs/entities.md#column-types)。\n\n:::tip\n\n`CreateDateColumn` 和 `UpdateDateColumn` 是依靠第一次同步表结构时，创建列上的默认数据完成的插入日期功能，如果是自己创建的表，需要自行在列上加入默认数据。\n\n:::\n\n\n\n\n### 7、配置连接信息和实体模型\n\n\n请参考 [配置](/docs/env_config) 章节，增加配置文件。\n\n\n然后在 `config.default.ts`  中配置数据库连接信息。\n```typescript\n// src/config/config.default.ts\nimport { Photo } from '../entity/photo.entity';\n\nexport default {\n  // ...\n  typeorm: {\n    dataSource: {\n      default: {\n        /**\n         * 单数据库实例\n         */\n        type: 'mysql',\n        host: '*******',\n        port: 3306,\n        username: '*******',\n        password: '*******',\n        database: undefined,\n        synchronize: false,\t\t    // 如果第一次使用，不存在表，有同步的需求可以写 true，注意会丢数据\n        logging: false,\n\n        // 配置实体模型\n        entities: [Photo],\n\n        // 支持如下的扫描形式，为了兼容我们可以同时进行.js和.ts匹配\n        entities: [\n          'entity',                 // 特定目录\n          '**/*.entity.{j,t}s',     // 通配加后缀匹配\n        ]\n      }\n    }\n  },\n}\n```\n:::tip\n\n- 1. 如果使用的数据库已经有表结构同步的功能，比如云数据库，最好不要开启。如果一定要使用，synchronize 配置最好仅在开发阶段，或者第一次使用，避免造成一致性问题。\n- 2. `entities` 字段配置已经经过框架处理，该字段配置请不要参考原始文档。\n:::\n\n\n `type` 字段你可以使用其他的数据库类型，包括`mysql`, `mariadb`, `postgres`, `cockroachdb`, `better-sqlite3`, `mssql`, `oracle`, `cordova`, `nativescript`, `react-native`, `expo`, or `mongodb`\n\n\n 比如 better-sqlite3，需要以下信息。\n\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  typeorm: {\n    dataSource: {\n      default: {\n        type: 'better-sqlite3',\n        database: path.join(__dirname, '../../test.sqlite'),\n        synchronize: true,\n        logging: true,\n        // ...\n      }\n    }\n  },\n}\n```\n\n\n:::info\n注意：synchronize 字段用于同步表结构。使用 `synchronize: true` 进行生产模式同步是不安全的，在上线后，请把这个字段设置为 false。\n:::\n\n\n### 8、使用 Model 插入数据库数据\n\n\n在常见的 Midway 文件中，使用 `@InjectEntityModel` 装饰器注入我们配置好的 Model。我们所需要做的只是：\n\n\n- 1、创建实体对象\n- 2、执行 `save()`\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from '../entity/photo.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  // save\n  async savePhoto() {\n    // create a entity object\n    let photo = new Photo();\n    photo.name = 'Me and Bears';\n    photo.description = 'I am near polar bears';\n    photo.filename = 'photo-with-bears.jpg';\n    photo.views = 1;\n    photo.isPublished = true;\n\n    // save entity\n    const photoResult = await this.photoModel.save(photo);\n\n    // save success\n    console.log('photo id = ', photoResult.id);\n  }\n}\n```\n\n\n### 9、查询数据\n\n更多的查询参数，请查询 [find文档](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/find-options.md)。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from '../entity/photo.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  // find\n  async findPhotos() {\n\n    // find All\n    let allPhotos = await this.photoModel.find({});\n    console.log(\"All photos from the db: \", allPhotos);\n\n    // find first\n    let firstPhoto = await this.photoModel.findOne({\n      where: {\n        id: 1\n      }\n    });\n    console.log(\"First photo from the db: \", firstPhoto);\n\n    // find one by name\n    let meAndBearsPhoto = await this.photoModel.findOne({\n      where: { name: \"Me and Bears\" }\n    });\n    console.log(\"Me and Bears photo from the db: \", meAndBearsPhoto);\n\n    // find by views\n    let allViewedPhotos = await this.photoModel.find({\n      where: { views: 1 }\n    });\n    console.log(\"All viewed photos: \", allViewedPhotos);\n\n    let allPublishedPhotos = await this.photoModel.find({\n      where: { isPublished: true }\n    });\n    console.log(\"All published photos: \", allPublishedPhotos);\n\n  \t// find and get count\n    let [allPhotos, photosCount] = await this.photoModel.findAndCount({});\n    console.log(\"All photos: \", allPhotos);\n    console.log(\"Photos count: \", photosCount);\n\n  }\n}\n\n```\n\n\n### 10、更新数据库\n\n\n现在，让我们从数据库中加载一个 Photo，对其进行更新并保存。\n\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from '../entity/photo.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  async updatePhoto() {\n\n    let photoToUpdate = await this.photoModel.findOne({\n      where: {\n        id: 1,\n      },\n    });\n    photoToUpdate.name = \"Me, my friends and polar bears\";\n\n    await this.photoModel.save(photoToUpdate);\n  }\n}\n```\n\n### 11、删除数据\n\n`remove` 用于删除给定的实体或实体数组。`delete` 用于按给定的 ID 或者条件删除。\n\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from '../entity/photo.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  async updatePhoto() {\n    /*...*/\n    const photo = await this.photoModel.findOne({\n      where: {\n        id: 1,\n      },\n    });\n\n    // 删除单个\n    await this.photoModel.remove(photo)\n    // 删除多个\n    await this.photoModel.remove([photo1, photo2, photo3]);\n\n    // 按 id 删除\n    await this.photoModel.delete(1);\n    await this.photoModel.delete([1, 2, 3]);\n    await this.photoModel.delete({ name: \"Timber\" });\n  }\n}\n```\n现在，ID = 1的 Photo 将从数据库中删除。\n\n\n此外还有软删除的方法。\n```typescript\nawait this.photoModel.softDelete(1);\n// 使用 restore 方法恢复;\nawait this.photoModel.restore(1);\n```\n\n\n### 12、创建一对一关联\n\n\n让我们与另一个类创建一对一的关系。让我们在 `entity/photoMetadata.entity.ts`  中创建一个新类。这个类包含 photo 的其他元信息。\n\n\n```typescript\nimport { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn } from 'typeorm';\nimport { Photo } from './photo.entity';\n\n@Entity()\nexport class PhotoMetadata {\n\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column(\"int\")\n  height: number;\n\n  @Column(\"int\")\n  width: number;\n\n  @Column()\n  orientation: string;\n\n  @Column()\n  compressed: boolean;\n\n  @Column()\n  comment: string;\n\n  @OneToOne(type => Photo)\n  @JoinColumn()\n  photo: Photo;\n\n}\n```\n\n\n在这里，我们使用一个名为 `@OneToOne`  的新装饰器。它允许我们在两个实体之间创建一对一的关系。`type => Photo`是一个函数，它返回我们要与其建立关系的实体的类。\n\n\n由于语言的特殊性，我们被迫使用一个返回类的函数，而不是直接使用该类。我们也可以将其写为 `() => Photo`  ，但是我们使用 `type => Photo`作为惯例来提高代码的可读性。类型变量本身不包含任何内容。\n\n\n我们还添加了一个 `@JoinColumn`装饰器，它指示关系的这一侧将拥有该关系。关系可以是单向或双向的。关系只有一方可以拥有。关系的所有者端需要使用@JoinColumn装饰器。 如果您运行该应用程序，则会看到一个新生成的表，该表将包含一列，其中包含用于 Photo 关系的外键。\n\n\n```\n+-------------+--------------+----------------------------+\n|                     photo_metadata                      |\n+-------------+--------------+----------------------------+\n| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |\n| height      | int(11)      |                            |\n| width       | int(11)      |                            |\n| comment     | varchar(255) |                            |\n| compressed  | boolean      |                            |\n| orientation | varchar(255) |                            |\n| photoId     | int(11)      | FOREIGN KEY                |\n+-------------+--------------+----------------------------+\n```\n\n\n接下去我们要在代码中关联他们。\n\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from './entity/photo.entity';\nimport { PhotoMetadata } from './entity/photoMetadata.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  @InjectEntityModel(PhotoMetadata)\n  photoMetadataModel: Repository<PhotoMetadata>;\n\n  async updatePhoto() {\n\n  // create a photo\n    let photo = new Photo();\n    photo.name = \"Me and Bears\";\n    photo.description = \"I am near polar bears\";\n    photo.filename = \"photo-with-bears.jpg\";\n    photo.isPublished = true;\n\n    // create a photo metadata\n    let metadata = new PhotoMetadata();\n    metadata.height = 640;\n    metadata.width = 480;\n    metadata.compressed = true;\n    metadata.comment = \"cybershoot\";\n    metadata.orientation = \"portrait\";\n    metadata.photo = photo; // this way we connect them\n\n\n    // first we should save a photo\n    await this.photoModel.save(photo);\n\n    // photo is saved. Now we need to save a photo metadata\n    await this.photoMetadataModel.save(metadata);\n\n    // done\n    console.log(\"Metadata is saved, and relation between metadata and photo is created in the database too\");\n  }\n}\n```\n\n\n### 13、反向关系映射\n\n\n关系映射可以是单向或双向的。当在 PhotoMetadata  和 Photo之间的关系是单向的。关系的所有者是PhotoMetadata，而 Photo对 PhotoMetadata 是一无所知的。这使得从 Photo 端访问 PhotoMetadata 变得很复杂。若要解决此问题，我们添加一个反向的关系映射，使  PhotoMetadata  和 Photo之间变成双向关联。让我们修改我们的实体。\n\n\n```typescript\nimport { Entity } from 'typeorm';\nimport { Column, PrimaryGeneratedColumn, OneToOne, JoinColumn } from 'typeorm';\nimport { Photo } from './photo';\n\n@Entity()\nexport class PhotoMetadata {\n\n  /* ... other columns */\n\n  @OneToOne(type => Photo, photo => photo.metadata)\n  @JoinColumn()\n  photo: Photo;\n}\n```\n```typescript\nimport { Entity } from 'typeorm';\nimport { Entity, Column, PrimaryGeneratedColumn, OneToOne } from 'typeorm';\nimport { PhotoMetadata } from './photoMetadata.entity';\n\n@Entity()\nexport class Photo {\n\n  /* ... other columns */\n\n  @OneToOne(type => PhotoMetadata, photoMetadata => photoMetadata.photo)\n  metadata: PhotoMetadata;\n}\n```\n`photo => photo.metadata` 是一个返回反向映射关系的函数。在这里，我们显式声明 Photo 类的 metadata 属性用于关联 PhotoMetadata。除了传递返回 photo 属性的函数外，您还可以直接将字符串传递给 `@OneToOne`  装饰器，例如 `“metadata”` 。但是我们使用了这种函数回调的方法来让我们的代码写法更简单。\n\n\n请注意，只会在关系映射的一侧使用 `@JoinColumn`  装饰器。无论您放置此装饰器的哪一侧，都是关系的所有者。关系的拥有方在数据库中包含带有外键的列。\n\n\n### 14、加载对象及其依赖关系\n\n\n现在，让我们尝试在单个查询中一起加载出  Photo 和 PhotoMetadata。有两种方法可以执行此操作，使用  `find *`   方法或使用 `QueryBuilder` 功能。让我们首先使用 `find *` 方法。 `find *`  方法允许您使用 `FindOneOptions`  / `FindManyOptions`  接口指定对象。\n\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from './entity/photo.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  // find\n  async findPhoto() {\n\t\t/*...*/\n    let photos = await this.photoModel.find({ relations: [ 'metadata' ] }); // typeorm@0.2.x\n  }\n}\n\n```\n在这里，photos  的值是一个数组，包含了整个数据库的查询结果，并且每个 photo 对象都包含其关联的 metadata 属性。在[此文档](https://github.com/typeorm/typeorm/blob/master/docs/find-options.md)中了解有关 `Find Options` 的更多信息。\n\n\n使用 `Find Options` 很简单，但如果需要更复杂的查询，则应改用 `QueryBuilder` 。 `QueryBuilder`   允许以优雅的方式使用更复杂的查询。\n\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from './entity/photo.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  // find\n  async findPhoto() {\n\t\t/*...*/\n    let photos = await this.photoModel\n      .createQueryBuilder('photo')\n      .innerJoinAndSelect('photo.metadata', 'metadata')\n      .getMany();\n  }\n}\n```\n`QueryBuilder`允许创建和执行几乎任何复杂的 SQL 查询。使用 `QueryBuilder`  时，请像创建  SQL 查询一样思考。在此示例中，“photo”  和 “metadata” 是应用于所选 photos  的别名。您可以使用别名来访问所选数据的列和属性。\n\n\n### 15、使用级联操作自动保存关联对象\n\n\n在我们希望在每次保存另一个对象时都自动保存关联的对象，这个时候可以在关系中设置级联。让我们稍微更改照片的 `@OneToOne`  装饰器。\n\n\n```typescript\nexport class Photo {\n  /// ... other columns\n\n  @OneToOne(type => PhotoMetadata, metadata => metadata.photo, {\n    cascade: true,\n  })\n  metadata: PhotoMetadata;\n}\n```\n使用 `cascade` 允许我们现在不再单独保存 Photo 和 PhotoMetadata，由于级联选项，元数据对象将被自动保存。\n\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from './entity/photo.entity';\nimport { PhotoMetadata } from './entity/photoMetadata.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  async updatePhoto() {\n\n   // create photo object\n    let photo = new Photo();\n    photo.name = \"Me and Bears\";\n    photo.description = \"I am near polar bears\";\n    photo.filename = \"photo-with-bears.jpg\";\n    photo.isPublished = true;\n\n    // create photo metadata object\n    let metadata = new PhotoMetadata();\n    metadata.height = 640;\n    metadata.width = 480;\n    metadata.compressed = true;\n    metadata.comment = \"cybershoot\";\n    metadata.orientation = \"portrait\";\n\n    photo.metadata = metadata;  // this way we connect them\n\n    // save a photo also save the metadata\n    await this.photoModel.save(photo);\n\n    // done\n    console.log(\"Photo is saved, photo metadata is saved too\");\n  }\n}\n```\n\n\n注意，我们现在设置 Photo 的元数据，而不需要像之前那样设置元数据的 Photo 属性。这仅当您从 Photo 这边将 Photo 连接到 PhotoMetadata 时，级联功能才有效。如果在 PhotoMetadata 侧设置，则不会自动保存。\n\n\n### 16、创建多对一/一对多关联\n\n\n让我们创建一个多对一/一对多关系。假设一张照片有一个作者，每个作者可以有很多照片。首先，让我们创建一个 Author 类：\n```typescript\nimport { Entity } from 'typeorm';\nimport { Column, PrimaryGeneratedColumn, OneToMany, JoinColumn } from 'typeorm';\nimport { Photo } from './entity/photo.entity';\n\n@Entity()\nexport class Author {\n\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column()\n  name: string;\n\n  @OneToMany(type => Photo, photo => photo.author) // note: we will create author property in the Photo class below\n  photos: Photo[];\n}\n```\n`Author`  包含了一个反向关系。 `OneToMany`  和 `ManyToOne` 需要成对出现。\n\n\n现在，将关系的所有者添加到 Photo 实体中：\n```typescript\nimport { Entity } from 'typeorm';\nimport { Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';\nimport { PhotoMetadata } from './photoMetadata.entity';\nimport { Author } from './author.entity';\n\n@Entity()\nexport class Photo {\n\n  /* ... other columns */\n\n  @ManyToOne(type => Author, author => author.photos)\n  author: Author;\n}\n```\n\n\n在多对一/一对多关系中，所有者方始终是多对一。这意味着使用 `@ManyToOne`  的类将存储相关对象的 ID。\n\n\n运行应用程序后，ORM 将创建 `author` 表：\n\n\n```\n+-------------+--------------+----------------------------+\n|                          author                         |\n+-------------+--------------+----------------------------+\n| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |\n| name        | varchar(255) |                            |\n+-------------+--------------+----------------------------+\n```\n它还将修改 `photo` 表，添加新的 `author`  列并为其创建外键：\n```\n+-------------+--------------+----------------------------+\n|                         photo                           |\n+-------------+--------------+----------------------------+\n| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |\n| name        | varchar(255) |                            |\n| description | varchar(255) |                            |\n| filename    | varchar(255) |                            |\n| isPublished | boolean      |                            |\n| authorId    | int(11)      | FOREIGN KEY                |\n+-------------+--------------+----------------------------+\n```\n\n\n### 17、创建多对多关联\n\n\n让我们创建一个多对一/多对多关系。假设一张照片可以在许多相册中，并且每个相册可以包含许多照片。让我们创建一个 `Album` 类。\n\n\n```typescript\nimport { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';\n\n@Entity()\nexport class Album {\n\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column()\n  name: string;\n\n  @ManyToMany(type => Photo, photo => photo.albums)\n  @JoinTable()\n  photos: Photo[];\n}\n```\n\n\n`@JoinTable` 用来指明这是关系的所有者。\n\n\n现在，将反向关联添加到 `Photo` 。\n\n\n```typescript\nexport class Photo {\n  /// ... other columns\n\n  @ManyToMany(type => Album, album => album.photos)\n  albums: Album[];\n}\n```\n运行应用程序后，ORM将创建一个 album_photos_photo_albums 联结表：\n\n\n```\n+-------------+--------------+----------------------------+\n|                album_photos_photo_albums                |\n+-------------+--------------+----------------------------+\n| album_id    | int(11)      | PRIMARY KEY FOREIGN KEY    |\n| photo_id    | int(11)      | PRIMARY KEY FOREIGN KEY    |\n+-------------+--------------+----------------------------+\n```\n\n\n现在，让我们将相册和照片插入数据库：\n\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { Photo } from './entity/photo.entity';\nimport { Album } from './entity/album.entity';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class PhotoService {\n\n  @InjectEntityModel(Photo)\n  photoModel: Repository<Photo>;\n\n  @InjectEntityModel(Album)\n  albumModel: Repository<Album>\n\n  async updatePhoto() {\n\n    // create a few albums\n    let album1 = new Album();\n    album1.name = \"Bears\";\n    await this.albumModel.save(album1);\n\n    let album2 = new Album();\n    album2.name = \"Me\";\n    await this.albumModel.save(album2);\n\n    // create a few photos\n    let photo = new Photo();\n    photo.name = \"Me and Bears\";\n    photo.description = \"I am near polar bears\";\n    photo.filename = \"photo-with-bears.jpg\";\n    photo.albums = [album1, album2];\n    await this.photoModel.save(photo);\n\n\n    // now our photo is saved and albums are attached to it\n    // now lets load them:\n    const loadedPhoto = await this.photoModel.findOne(1, { relations: [\"albums\"] });  // typeorm@0.2.x\n  }\n}\n```\n`loadedPhoto` 的值为：\n```json\n{\n  id: 1,\n  name: \"Me and Bears\",\n  description: \"I am near polar bears\",\n  filename: \"photo-with-bears.jpg\",\n  albums: [{\n    id: 1,\n    name: \"Bears\"\n  }, {\n    id: 2,\n    name: \"Me\"\n  }]\n}\n```\n\n### 18、使用 QueryBuilder\n\n\n您可以使用QueryBuilder来构建几乎任何复杂的SQL查询。例如，您可以这样做：\n\n\n```typescript\nlet photos = await this.photoModel\n    .createQueryBuilder(\"photo\") // first argument is an alias. Alias is what you are selecting - photos. You must specify it.\n    .innerJoinAndSelect(\"photo.metadata\", \"metadata\")\n    .leftJoinAndSelect(\"photo.albums\", \"album\")\n    .where(\"photo.isPublished = true\")\n    .andWhere(\"(photo.name = :photoName OR photo.name = :bearName)\")\n    .orderBy(\"photo.id\", \"DESC\")\n    .skip(5)\n    .take(10)\n    .setParameters({ photoName: \"My\", bearName: \"Mishka\" })\n    .getMany();\n```\n该查询选择所有带有 “My” 或 “Mishka” 名称的已发布照片。它将从位置 5 开始返回结果（分页偏移），并且将仅选择 10 个结果（分页限制）。选择结果将按 ID 降序排列。该照片的相册将 left-Joined，元数据将自动关联。\n\n\n您将在应用程序中大量使用查询生成器。在 [此处](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/select-query-builder.md) 了解有关QueryBuilder的更多信息。\n\n\n### 19、Event Subscriber\n\n\ntypeorm 提供了一个事件订阅机制，方便在做一些数据库操作时的日志输出，为此 midway 提供了一个 `EventSubscriberModel` 装饰器，用来标注事件订阅类，代码如下。\n\n\n```typescript\nimport { EventSubscriberModel } from '@midwayjs/typeorm';\nimport { EntitySubscriberInterface, InsertEvent, UpdateEvent, RemoveEvent } from 'typeorm';\n\n@EventSubscriberModel()\nexport class EverythingSubscriber implements EntitySubscriberInterface {\n\n  /**\n   * Called before entity insertion.\n   */\n  beforeInsert(event: InsertEvent<any>) {\n    console.log(`BEFORE ENTITY INSERTED: `, event.entity);\n  }\n\n  /**\n   * Called before entity insertion.\n   */\n  beforeUpdate(event: UpdateEvent<any>) {\n    console.log(`BEFORE ENTITY UPDATED: `, event.entity);\n  }\n\n  /**\n   * Called before entity insertion.\n   */\n  beforeRemove(event: RemoveEvent<any>) {\n    console.log(`BEFORE ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity);\n  }\n\n  /**\n   * Called after entity insertion.\n   */\n  afterInsert(event: InsertEvent<any>) {\n    console.log(`AFTER ENTITY INSERTED: `, event.entity);\n  }\n\n  /**\n\t * Called after entity insertion.\n\t */\n  afterUpdate(event: UpdateEvent<any>) {\n    console.log(`AFTER ENTITY UPDATED: `, event.entity);\n  }\n\n  /**\n   * Called after entity insertion.\n   */\n  afterRemove(event: RemoveEvent<any>) {\n    console.log(`AFTER ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity);\n  }\n\n  /**\n   * Called after entity is loaded.\n   */\n  afterLoad(entity: any) {\n    console.log(`AFTER ENTITY LOADED: `, entity);\n  }\n\n}\n```\n\n这个订阅类提供了一些常用的接口，用来在数据库操作时执行一些事情。\n\n同时，我们需要把订阅类加到配置中。\n\n```typescript\n// src/config/config.default.ts\nimport { EverythingSubscriber } from '../event/subscriber';\n\nexport default {\n  // ...\n  typeorm: {\n    dataSource: {\n      default: {\n        // ...\n        entities: [Photo],\n        // 传入订阅类\n        subscribers: [EverythingSubscriber]\n      }\n    }\n  },\n}\n```\n\n\n\n## Repository API\n\n更多 API 请查看 [官网文档](https://github.com/typeorm/typeorm/blob/master/docs/repository-api.md)。\n\n\n\n\n## 高级功能\n### 多数据库支持\n\n\n有时候，我们一个应用中会有多个数据库连接（Connection）的情况，这个时候会有多个配置。我们使用 DataSource 标准的 **对象的形式 **来定义配置。\n\n\n比如下面定义了 `default` 和 `test` 两个数据库连接（Connection）。\n\n\n```typescript\nimport { join } from 'path';\n\nexport default {\n  typeorm: {\n    dataSource: {\n      default: {\n        type: 'better-sqlite3',\n        database: join(__dirname, '../../default.sqlite'),\n        // ...\n      },\n      test: {\n        type: 'mysql',\n        host: '127.0.0.1',\n        port: 3306,\n        // ...\n      }\n    }\n  }\n}\n```\n\n\n在使用时，需要指定模型归属于哪个连接（Connection）。\n```typescript\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { User } from './entity/user.entity';\n\nexport class XXX {\n\n  @InjectEntityModel(User, 'test')\n  testUserModel: Repository<User>;\n\n  //...\n}\n```\n\n\n\n### 列值转换\n\n我们可以在实体定义中处理列值转换。\n\n利用列装饰器的 `transformer` 参数，可以进行出入参的处理，比如对时间格式化。\n\n```typescript\nimport { Entity, Column, CreateDateColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';\nimport * as dayjs from 'dayjs';\n\nconst dateTransformer = {\n  from: (value: Date | number) => {\n    return dayjs(typeof value === 'number' ? value: value.getTime()).format('YYYY-MM-DD HH:mm:ss');\n  },\n  to: () => new Date(),\n};\n\n@Entity()\nexport class Photo {\n  // ...\n\n  @CreateDateColumn({\n    type: 'timestamp',\n    transformer: dateTransformer,\n  })\n  createdAt: Date;\n}\n\n```\n\n\n\n### 指定默认数据源\n\n在包含多个数据源时，可以指定默认的数据源。\n\n```typescript\nexport default {\n  // ...\n  typeorm: {\n    dataSource: {\n      default1: {\n        // ...\n      },\n      default2: {\n        // ...\n      },\n    },\n    // 多个数据源时可以用这个指定默认的数据源\n    defaultDataSourceName: 'default1',\n  },\n};\n```\n\n\n\n### 获取数据源\n\n数据源即创建出的 DataSource 对象，我们可以通过注入内置的数据源管理器来获取。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport { TypeORMDataSourceManager } from '@midwayjs/typeorm';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n\n  async onReady(container: IMidwayContainer) {\n    const dataSourceManager = await container.getAsync(TypeORMDataSourceManager);\n  \tconst conn = dataSourceManager.getDataSource('default');\n    console.log(dataSourceManager.isConnected(conn));\n  }\n}\n```\n\n从 v3.8.0 开始，也可以通过装饰器注入。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport { InjectDataSource } from '@midwayjs/typeorm';\nimport { DataSource } from 'typeorm';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n\n  // 注入默认数据源\n  @InjectDataSource()\n  defaultDataSource: DataSource;\n\n  // 注入自定义数据源\n  @InjectDataSource('default1')\n  customDataSource: DataSource;\n\n  async onReady(container: IMidwayContainer) {\n    // ...\n  }\n}\n```\n\n\n\n### 日志\n\n数据源在未配置日志对象时，组件会自动创建一个 `typeormLogger`，用于保存执行的 SQL 信息，方便排查问题和 SQL 审核。\n\n默认配置为：\n\n```typescript\nexport default {\n  midwayLogger: {\n    clients: {\n      typeormLogger: {\n        fileLogName: 'midway-typeorm.log',\n        enableError: false,\n        level: 'info',\n      },\n    },\n  }\n}\n```\n\n我们可以使用普通日志的配置方式进行调整，如果不希望生成日志，可以配置关闭。\n\n```typescript\nexport default {\n  // ...\n  typeorm: {\n    default: {\n      // 所有数据源关闭\n      logging: false,\n    },\n    dataSource: {\n      default: {\n        // 单个数据源关闭\n        logging: false,\n      },\n    },\n  },\n};\n```\n\n\n\n### 事务\n\ntypeorm 的事务需要先获取到数据源，然后开启事务。\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { TypeORMDataSourceManager } from '@midwayjs/typeorm';\nimport { UserDTO } from '../entity/user';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  dataSourceManager: TypeORMDataSourceManager;\n\n  async updateUser(user: UserDTO) {\n\n    // get dataSource\n    const dataSource = this.dataSourceManager.getDataSource('default');\n\n    // start transaction\n    await dataSource.transaction(async (transactionalEntityManager) => {\n      // run code\n      await transactionalEntityManager.save(UserDTO, user);\n    });\n  }\n\n}\n```\n\n更多的细节，可以参考 [文档](https://github.com/typeorm/typeorm/blob/master/docs/transactions.md)。\n\n\n\n### CLI\n\nTypeORM 默认提供了一个 CLI，用来创建 entity，migration 等，更多文档请查看 [这里](https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/using-cli.md)。\n\n由于 TypeORM 的默认配置和 Midway 不同，我们提供了一个简单的修改版本，用于适配 Midway 的数据源配置。\n\n检查安装情况：\n\n```bash\n$ npx mwtypeorm -h\n```\n\n常用的命令有\n\n **创建空 Entity**\n\n将会创建一个 `src/entity/User.ts` 文件。\n\n```bash\n$ npx mwtypeorm entity:create src/entity/User\n```\n\n**创建 Migration**\n\n将会根据现有数据源生成一个 `src/migration/******-photo.entity.ts` 文件。\n\n比如配置如下：\n\n```typescript\nexport default {\n  typeorm: {\n    dataSource: {\n      'default': {\n        // ...\n        entities: [\n          '**/entity/*.entity{.ts,.js}'\n        ],\n        migrations: [\n          '**/migration/*.ts'\n        ],\n      },\n  },\n}\n```\n\n可以执行下面的命令，将修改后的 Entity 生成迁移文件。\n\n```bash\n$ npx mwtypeorm migration:generate -d ./src/config/config.default.ts src/migration/photo\n```\n\n:::caution\n\n注意：上面的 entities 配置由于需要再 CLI 和 Midway 间复用，采用了两者都支持的扫描写法。\n\n:::\n\n\n\n### 关于表结构同步\n\n\n- 如果你已有表结构，想自动创建 Entity，使用 [生成器](https://www.npmjs.com/package/typeorm-model-generator)\n- 如果已经有 Entity 代码，想创建表结构请使用配置中的  `synchronize:  true` ，注意可能会丢失数据\n- 如果已经上线，但是又修改了表结构，可以使用 CLI 中的 `migration:generate`\n\n\n\n## 常见问题\n\n\n### Handshake inactivity timeout\n\n\n一般是网络原因，如果本地出现，可以 ping 但是telnet不通，可以尝试执行如下命令：\n```bash\n$ sudo sysctl -w net.inet.tcp.sack=0\n```\n\n\n\n### 关于 mysql 时间列的时区展示\n\n一般情况下，数据库中保存的是 UTC 时间，如果你希望返回当前时区的时间，可以使用下面的方式\n\n**1、检查 mysql 数据库所在的环境**\n\n比如下面默认的时区其实就是系统 UTC 时间，可以调整为 `+08:00`。\n\n```text\nmysql> show global variables like '%time_zone%';\n+------------------+--------+\n| Variable_name    | Value  |\n+------------------+--------+\n| system_time_zone | UTC    |\n| time_zone        | SYSTEM |\n+------------------+--------+\n2 rows in set (0.05 sec)\n\n```\n\n**2、检查服务代码部署的环境**\n\n尽量和数据库所在的环境一致，如果不一致，请在配置中设置 `timezone` （设置为和 mysql 一致）。\n\n```typescript\nexport default {\n  typeorm: {\n    dataSource: {\n      default: {\n        type: 'mysql',\n        // ...\n        timezone: '+08:00',\n      },\n    },\n  },\n}\n```\n\n\n\n### 时间列返回字符串\n\n配置 dateStrings 可以使 mysql 返回时间按 DATETIME 格式返回，只对 mysql 生效。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  typeorm: {\n    dataSource: {\n      default: {\n        //...\n        dateStrings: true,\n      }\n    }\n  },\n}\n```\n\n如果使用了 `@CreateDateColumn` 和 `@UpdateDateColumn` ，可以调整实体返回类型。\n\n```typescript\n@UpdateDateColumn({\n  name: \"gmt_modified\",\n  type: 'timestamp'\n})\ngmtModified: string;\n\n@CreateDateColumn({\n  name: \"gmt_create\",\n  type: 'timestamp',\n})\ngmtCreate: string;\n```\n\n\n\n效果如下：\n\n**配置前：**\n\n```typescript\ngmtModified: 2021-12-13T03:49:43.000Z,\ngmtCreate: 2021-12-13T03:49:43.000Z\n```\n**配置后：**\n\n```typescript\ngmtModified: '2021-12-13 11:49:43',\ngmtCreate: '2021-12-13 11:49:43'\n```\n\n\n\n### 同时安装 mysql 和 mysql2\n\n在 node_modules 中同时有 mysql  和 mysql2 时，typeorm 会自动加载 mysql，而不是 mysql2。\n\n这个时候如需使用 mysql2，请指定  driver。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  typeorm: {\n    dataSource: {\n      default: {\n        //...\n        type: 'mysql',\n        driver: require('mysql2'),\n      }\n    }\n  },\n}\n```\n\n\n\n\n###  Cannot read properties of undefined (reading 'getRepository')\n\n一般是配置不正确，可以考虑两方面的配置：\n\n- 1、检查 `config.default.ts` 中的 `entities` 配置是否正确\n- 2、检查 `configuration.ts` 文件，确认是否引入 orm\n","isInternal":false,"tokens":11191,"sizeBytes":42017},{"name":"oss.md","path":"site/docs/extensions/oss.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/oss.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# 阿里云对象存储（OSS）\n\n阿里云对象存储服务（Object Storage Service，简称 OSS），是阿里云提供的海量、安全、低成本、高可靠的云存储服务。其数据设计持久性不低于 99.999999999%，服务设计可用性不低于 99.99%。具有与平台无关的 RESTful API 接口，您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。\n\n`@midwayjs/oss` 组件就是在 midway 体系下用于对接 OSS 服务的 sdk。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 前置条件\n\n\n使用 OSS 组件，你需要提前申请一个 OSS Bucket。Bucket 是 OSS 的存储库的概念，你的文件都将存储在这个库里。\n\n\n- OSS 对象存储官网：[https://www.aliyun.com/product/oss](https://www.aliyun.com/product/oss)\n- 什么是对象存储：[https://www.alibabacloud.com/help/zh/doc-detail/31817.htm](https://www.alibabacloud.com/help/zh/doc-detail/31817.htm)\n\n\n## 安装依赖\n\n`@midwayjs/oss` 是主要的功能包。\n\n```bash\n$ npm i @midwayjs/oss@4 --save\n```\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/oss\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n\n## 引入组件\n\n\n首先，引入 组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as oss from '@midwayjs/oss';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    oss\t\t// 导入 oss 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n\n## 配置 OSS\n\n\nOSS 组件需要配置后才能使用。需要填写 OSS 的 bucket、accessKeyId、accessKeySecret 等必要信息。\n\n\n支持普通 oss 客户端和 oss 集群客户端，基于 [ali-oss](https://github.com/ali-sdk/ali-oss/) 这个包。\n\n\n比如：\n\n**普通的 oss bucket 配置**\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  oss: {\n    // normal oss bucket\n    client: {\n      accessKeyId: 'your access key',\n      accessKeySecret: 'your access secret',\n      bucket: 'your bucket name',\n      endpoint: 'oss-cn-hongkong.aliyuncs.com',\n      timeout: '60s',\n    },\n  },\n}\n```\n\n\n**集群（cluster） 模式的 oss bucket 配置，需要配置多个**\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  oss: {\n    // need to config all bucket information under cluster\n    client: {\n      clusters: [{\n        endpoint: 'host1',\n        accessKeyId: 'id1',\n        accessKeySecret: 'secret1',\n      }, {\n        endpoint: 'host2',\n        accessKeyId: 'id2',\n        accessKeySecret: 'secret2',\n      }],\n      schedule: 'masterSlave', //default is `roundRobin`\n      timeout: '60s',\n    },\n  },\n}\n```\n\n**STS 模式**\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  oss: {\n    // if config.sts == true, oss will create STS client\n    client: {\n      sts: true,\n      accessKeyId: 'your access key',\n      accessKeySecret: 'your access secret',\n    },\n  },\n}\n```\n\n## 使用组件\n\n\n可以直接获取 `OSSService`，然后调用接口，比如，保存文件。\n```typescript\nimport { OSSService } from '@midwayjs/oss';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  ossService: OSSService;\n\n  async saveFile() {\n\n    const localFile = join(__dirname, 'test.log');\n    const result = await this.ossService.put('/test/test.log', localFile);\n\n    // => result.url\n  }\n}\n```\n\n\n如果配置的是 STS 模式，客户端可以使用 `OSSSTSService` 。\n```typescript\nimport { OSSSTSService } from '@midwayjs/oss';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  stsService: OSSSTSService;\n\n  async saveFile() {\n\n    const roleArn = '******';\t\t// 这里是阿里云角色的 arn\n    const result = await this.stsService.assumeRole(roleArn);\n\n    // result.credentials.AccessKeyId\n    // result.credentials.AccessKeySecret;\n    // result.credentials.SecurityToken;\n  }\n}\n```\n\n更多的 OSS 客户端 API，请查看 [OSS 文档](https://github.com/ali-sdk/ali-oss)。\n\n\n## 使用多个 OSS Bucket\n\n\n有些应用需要访问多个 oss bucket，那么就需要配置 `oss.clients`。\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  oss: {\n    clients: {\n      bucket1: {\n        bucket: 'bucket1',\n        // ...\n      },\n      bucket2: {\n        bucket: 'bucket2',\n        // ...\n      },\n    },\n    // client, clients，createInstance 方法共享的配置\n    default: {\n      endpoint: '',\n      accessKeyId: '',\n      accessKeySecret: '',\n    },\n  },\n  // other custom config\n  bucket3: {\n    bucket: 'bucket3',\n    // ...\n  },\n}\n```\n\n可以使用 `OSSServiceFactory` 获取不同的实例。\n\n```typescript\nimport { OSSServiceFactory } from '@midwayjs/oss';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  ossServiceFactory: OSSServiceFactory;\n\n  @Config('bucket3')\n  bucket3Config;\n\n  async saveFile() {\n\n    // 默认获取的类型是 OSSService\n    const bucket1 = this.ossServiceFactory.get('bucket1');\n    const bucket2 = this.ossServiceFactory.get('bucket2');\n\n    // 如果是 STS，需要设置泛型联系\n    // const bucket1 = this.ossServiceFactory.get<OSSSTSService>('bucket1');\n\n    // 会合并 config.bucket3 和 config.oss.default\n    const bucket3 = await this.ossServiceFactory.createInstance(this.bucket3Config, 'bucket3');\n    // 传了名字之后也可以从 factory 中获取\n    bucket3 = this.ossServiceFactory.get('bucket3');\n\n  }\n}\n```\n","isInternal":false,"tokens":1668,"sizeBytes":5794},{"name":"passport.md","path":"site/docs/extensions/passport.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/passport.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# 身份验证\n\n身份验证是大多数 Web 应用程序的重要组成部分。因此 Midway 封装了目前 Nodejs 中最流行的 Passport 库。\n\n相关信息：\n\n| web 支持情况      |     |\n| ----------------- | --- |\n| @midwayjs/koa     | ✅  |\n| @midwayjs/faas    | ✅  |\n| @midwayjs/web     | ✅  |\n| @midwayjs/express | ✅  |\n\n从 v3.4.0 开始 Midway 自行维护 passport，将不再需要引入社区包和类型包。\n\n## 一些概念\n\npassport 是社区使用较多的身份验证库，通过称为策略的可扩展插件进行身份验证请求。\n\n它本身包含几个部分：\n\n- 1、验证的策略，比如 jwt 验证，github 验证，oauth 验证等，passport 最为丰富的也是这块\n- 2、执行策略之后，中间件的逻辑处理和配置，比如成功或者失败后的跳转，报错等\n\n## 安装依赖\n\n安装 `npm i @midwayjs/passport` 和相关策略依赖。\n\n```bash\n## 必选\n$ npm i @midwayjs/passport@4 --save\n\n## 可选\n## 下面安装本地策略\n$ npm i passport-local --save\n$ npm i @types/passport-local --save-dev\n## 下面安装 Github 策略\n$ npm i passport-github --save\n## 下面安装 Jwt 策略\n$ npm i passport-jwt --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/passport\": \"^4.0.0\",\n    // 本地策略\n    \"passport-local\": \"^1.0.0\"\n    // Jwt 策略\n    \"passport-jwt\": \"^4.0.0\",\n    // Github 策略\n    \"passport-github\": \"^1.1.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // 本地策略\n    \"@types/passport-local\": \"^1.0.34\",\n    // Jwt 策略\n    \"@types/passport-jwt\": \"^3.0.6\",\n    // Github 策略\n    \"@types/passport-github\": \"^1.1.7\",\n    // ...\n  }\n}\n```\n\n## 启用组件\n\n首先启用组件。\n\n```typescript\n// src/configuration.ts\n\nimport { join } from 'path';\nimport { ILifeCycle } from '@midwayjs/core';\nimport { Configuration } from '@midwayjs/core';\nimport * as passport from '@midwayjs/passport';\n\n@Configuration({\n  imports: [\n    // ...\n    passport,\n  ],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration implements ILifeCycle {}\n```\n\n## 策略示例\n\n这里我们以使用本地认证策略，和 Jwt 策略作为演示。\n\n### 示例：本地策略\n\n我们以 `passport-local` 来介绍 Passport 策略在 Midway 中如何使用， `passport-local` 的官方文档示例如下，通过 `passport.use` 加载一个策略，策略的验证逻辑是一个 `verify` 方法，包含 callback 参数，其余的策略的参数都在构造器中。\n\n```typescript\npassport.use(\n  // 初始化一个策略\n  new LocalStrategy({\n      usernameField: 'username',\n      passwordField: 'password',\n      passReqToCallback: true,\n      session: false\n    },\n    function verify(username, password, done) {\n      User.findOne({ username: username }, function (err, user) {\n        if (err) { return done(err); }\n        if (!user) { return done(null, false); }\n        if (!user.verifyPassword(password)) { return done(null, false); }\n        return done(null, user);\n      });\n    }\n\t)\n);\n```\n\nMidway 对此进行了改造，通过 `@CustomStrategy` 和 `PassportStrategy` 类继承一个 Passport 现有策略。\n\n异步的 `validate`  方法代替原有的 `verify` 方法，`validate` 方法返回验证后的用户结果，方法的参数和原有对应的策略一致。\n\n在 Midway 中编写的效果如下：\n\n```typescript\n// src/strategy/local.strategy.ts\n\nimport { CustomStrategy, PassportStrategy } from '@midwayjs/passport';\nimport { Strategy, IStrategyOptions } from 'passport-local';\nimport { Repository } from 'typeorm';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { UserEntity } from './user';\nimport * as bcrypt from 'bcrypt';\n\n@CustomStrategy()\nexport class LocalStrategy extends PassportStrategy(Strategy) {\n  @InjectEntityModel(UserEntity)\n  userModel: Repository<UserEntity>;\n\n  // 策略的验证\n  async validate(username, password) {\n    const user = await this.userModel.findOneBy({ username });\n    if (!user) {\n      throw new Error('用户不存在 ' + username);\n    }\n    if (!await bcrypt.compare(password, user.password)) {\n      throw new Error('密码错误 ' + username);\n    }\n\n    return user;\n  }\n\n  // 当前策略的构造器参数\n  getStrategyOptions(): IStrategyOptions {\n    return {\n      usernameField: 'username',\n      passwordField: 'password',\n      passReqToCallback: true,\n      session: false\n    };\n  }\n}\n```\n\n:::tip\n\n注意：validate 方法是社区策略 verify 的 Promise 化替代方法，你无需在最后传递 callback 参数。\n\n:::\n\n在 `passport-local` 的官方文档中，实现完策略后，需要作为中间件加载到业务中，比如：\n\n```typescript\napp.post('/login/password', passport.authenticate('local', {\n  successRedirect: '/',\n  failureRedirect: '/login'\n}));\n```\n\n:::tip\n\n这里的 `local` 是 `passport-local` 内部的名字。\n\n:::\n\n在 Midway 中，也需要将上述实现的 `LocalStrategy` 通过中间件加载。\n\n自定义一个中间件继承 `PassportMiddleware` 扩展出的基础中间件，示例如下。\n\n```typescript\n// src/middleware/local.middleware.ts\n\nimport { Middleware } from '@midwayjs/core';\nimport { PassportMiddleware, AuthenticateOptions } from '@midwayjs/passport';\nimport { LocalStrategy } from '../strategy/local.strategy';\n\n@Middleware()\nexport class LocalPassportMiddleware extends PassportMiddleware(LocalStrategy) {\n  // 设置 AuthenticateOptions\n  getAuthenticateOptions(): Promise<AuthenticateOptions> | AuthenticateOptions {\n    return {\n      failureRedirect: '/login',\n    };\n  }\n}\n```\n将中间件加载到全局或者路由。\n\n```typescript\n// src/controller.ts\nimport { Post, Inject, Controller } from '@midwayjs/core';\nimport { LocalPassportMiddleware } from '../middleware/local.middleware';\n\n@Controller('/')\nexport class LocalController {\n  @Post('/passport/local', { middleware: [LocalPassportMiddleware] })\n  async localPassport() {\n    console.log('local user: ', this.ctx.state.user);\n    return this.ctx.state.user;\n  }\n}\n```\n\n使用 curl 模拟一次请求。\n\n```bash\ncurl -X POST http://localhost:7001/passport/local -d '{\"username\": \"demo\", \"password\": \"1234\"}' -H \"Content-Type: application/json\"\n\n结果 {\"username\": \"demo\", \"password\": \"1234\"}\n```\n\n:::caution\n\n注意：如果将中间件放到全局，记得忽略需要登录的路由，否则请求会死循环。\n\n:::\n\n\n\n### 示例：Jwt 策略\n\n首先需要 **额外安装** 依赖和策略：\n\n```bash\n$ npm i @midwayjs/jwt passport-jwt --save\n```\n\n额外启用 jwt 组件。\n\n```typescript\n// configuration.ts\n\nimport { join } from 'path';\nimport * as jwt from '@midwayjs/jwt';\nimport { Configuration, ILifeCycle } from '@midwayjs/core';\nimport * as passport from '@midwayjs/passport';\n\n@Configuration({\n  imports: [\n    // ...\n    jwt,\n    passport,\n  ],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration implements ILifeCycle {}\n```\n\n然后在配置中设置，默认未加密，请不要把敏感信息存放在 payload 中。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  jwt: {\n    secret: 'xxxxxxxxxxxxxx', // fs.readFileSync('xxxxx.key')\n    expiresIn: '2d', // https://github.com/vercel/ms\n  },\n};\n```\n\n```typescript\n// src/strategy/jwt.strategy.ts\n\nimport { CustomStrategy, PassportStrategy } from '@midwayjs/passport';\nimport { Strategy, ExtractJwt } from 'passport-jwt';\nimport { Config } from '@midwayjs/core';\n\n@CustomStrategy()\nexport class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {\n  @Config('jwt')\n  jwtConfig;\n\n  async validate(payload) {\n    return payload;\n  }\n\n  getStrategyOptions(): any {\n    return {\n      secretOrKey: this.jwtConfig.secret,\n      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),\n    };\n  }\n}\n```\n\n:::tip\n\n注意：validate 方法是社区策略 verify 的 Promise 化替代方法，你无需在最后传递 callback 参数。\n\n:::\n\n```typescript\n// src/middleware/jwt.middleware.ts\n\nimport { Middleware } from '@midwayjs/core';\nimport { PassportMiddleware, AuthenticateOptions } from '@midwayjs/passport';\nimport { JwtStrategy } from '../strategy/jwt.strategy';\n\n@Middleware()\nexport class JwtPassportMiddleware extends PassportMiddleware(JwtStrategy) {\n  getAuthenticateOptions(): Promise<AuthenticateOptions> | AuthenticateOptions {\n    return {};\n  }\n}\n```\n\n```typescript\nimport { Post, Inject, Controller } from '@midwayjs/core';\nimport { Context } from '@midwayjs/koa';\nimport { JwtService } from '@midwayjs/jwt';\nimport { JwtPassportMiddleware } from '../middleware/jwt.middleware';\n\n@Controller('/')\nexport class JwtController {\n  @Inject()\n  jwt: JwtService;\n\n  @Inject()\n  ctx: Context;\n\n  @Post('/passport/jwt', { middleware: [JwtPassportMiddleware] })\n  async jwtPassport() {\n    console.log('jwt user: ', this.ctx.state.user);\n    return this.ctx.state.user;\n  }\n\n  @Post('/jwt')\n  async genJwt() {\n    return {\n      t: await this.jwt.sign({ msg: 'Hello Midway' }),\n    };\n  }\n}\n```\n\n使用 curl 模拟请求\n\n```bash\ncurl -X POST http://127.0.0.1:7001/jwt\n\n结果 {\"t\": \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"}\n\ncurl http://127.0.0.1:7001/passport/jwt -H \"Authorization: Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n\n结果 {\"msg\": \"Hello Midway\",\"iat\": 1635468727,\"exp\": 1635468827}\n\n```\n\n## 自定义其他策略\n\n`@midwayjs/passport` 支持自定义[其他策略](http://www.passportjs.org/packages/)，这里以 Github OAuth 为例。\n首先 `npm i passport-github`，之后编写如下代码：\n\n```typescript\n// github-strategy.ts\n\nimport { CustomStrategy, PassportStrategy } from '@midwayjs/passport';\nimport { Strategy, StrategyOptions } from 'passport-github';\n\nconst GITHUB_CLIENT_ID = 'xxxxxx',\n  GITHUB_CLIENT_SECRET = 'xxxxxxxx';\n\n@CustomStrategy()\nexport class GithubStrategy extends PassportStrategy(Strategy, 'github') {\n  async validate(...payload) {\n    return payload;\n  }\n\n  getStrategyOptions(): StrategyOptions {\n    return {\n      clientID: GITHUB_CLIENT_ID,\n      clientSecret: GITHUB_CLIENT_SECRET,\n      callbackURL: 'https://127.0.0.1:7001/auth/github/cb',\n    };\n  }\n}\n```\n\n```typescript\n// src/middleware/github.middleware.ts\n\nimport { AuthenticateOptions, PassportMiddleware } from '@midwayjs/passport';\nimport { Middleware } from '@midwayjs/core';\nimport { GithubStrategy } from './githubStrategy';\n\n@Middleware()\nexport class GithubPassportMiddleware extends PassportMiddleware(GithubStrategy) {\n  getAuthenticateOptions(): AuthenticateOptions | Promise<AuthenticateOptions> {\n    return {};\n  }\n}\n```\n\n```typescript\n// src/controller/auth.controller.ts\n\nimport { Controller, Get, Inject } from '@midwayjs/core';\nimport { GithubPassportMiddleware } from '../../middleware/github';\n\n@Controller('/oauth')\nexport class AuthController {\n  @Inject()\n  ctx;\n\n  @Get('/github', { middleware: [GithubPassportMiddleware] })\n  async githubOAuth() {}\n\n  @Get('/github/cb', { middleware: [GithubPassportMiddleware] })\n  async githubOAuthCallback() {\n    return this.ctx.state.user;\n  }\n}\n```\n\n## 策略选项\n\n| 选项                | 类型    | 描述                                              |\n| ------------------- | ------- | ------------------------------------------------- |\n| failureRedirect     | string  | 失败跳转的 url                                    |\n| session             | boolean | 默认 true，开启后，会自动将用户信息设置到 session |\n| sessionUserProperty | string  | 设置到 session 上的 key，默认 user                |\n| userProperty        | string  | 设置到 ctx.state 或者 req 上的 key，默认 user     |\n| successRedirect     | string  | 用户认证成功后跳转的地址                          |\n\n## 常见问题\n\n### 1、Failed to serialize user into session\n\n由于 passport 默认会尝试将 user 数据写入 session，如果无需将用户保存到 session，可以将 session 支持关闭。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  passport: {\n    session: false,\n  },\n};\n```\n\n如果明确需要保存数据到 Session，则需要重写 `PassportStrategy`的 User 的序列化方法，请不要保存特别大的数据。\n\n比如自己实现的本地策略。\n\n```typescript\n// src/strategy/local.strategy.ts\n\nimport { CustomStrategy, PassportStrategy } from '@midwayjs/passport';\nimport { Repository } from 'typeorm';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { UserEntity } from './user';\nimport * as bcrypt from 'bcrypt';\n\n@CustomStrategy()\nexport class LocalStrategy extends PassportStrategy(Strategy) {\n  // ...\n  serializeUser(user, done) {\n    // 可以只保存用户名\n    done(null, user.username);\n  }\n\n  deserializeUser(id, done) {\n    // 这里不是异步方法，你可以从其他地方根据用户名，反查用户数据。\n    const user = getUserFromDataBase(id);\n\n    done(null, user);\n  }\n}\n```\n","isInternal":false,"tokens":3444,"sizeBytes":12928},{"name":"piscina.md","path":"site/docs/extensions/piscina.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/piscina.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# 线程池\n\n线程池组件 `@midwayjs/piscina` 基于 [Piscina](https://github.com/piscinajs/piscina) 提供在 Worker 线程池中执行任务的能力，适合 CPU 密集型计算，不会阻塞主线程。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n## 安装组件\n\n```bash\n$ npm i @midwayjs/piscina@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/piscina\": \"^4.0.0\"\n  }\n}\n```\n\n## 引入组件\n\n将组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as piscina from '@midwayjs/piscina';\n\n@Configuration({\n  imports: [\n    piscina\n  ],\n  // ...\n})\nexport class MainConfiguration {}\n```\n\n## 基础用法\n\n框架基于 [文档](/docs/service_factory) 提供了 `PiscinaService` 和 `PiscinaServiceFactory`。\n\n你可以创建单个或者多个线程池对象来管理线程。\n\n以下是 [Piscina](https://github.com/piscinajs/piscina) 的基础用法。\n\n注入 `PiscinaService` 并调用 `run` 方法执行任务：\n\n```typescript\nimport { Inject, Provide } from '@midwayjs/core';\nimport * as piscina from '@midwayjs/piscina';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  piscinaService: piscina.PiscinaService;\n\n  async heavyTask() {\n    // 调用 compute 函数\n    const result1 = await this.piscinaService.run({\n      handler: 'compute',\n      payload: { value: 10 },\n    });\n    console.log(result1); // 20\n\n    // 调用 heavyComputation 函数\n    const result2 = await this.piscinaService.run({\n      handler: 'heavyComputation',\n      payload: { data: [1, 2, 3, 4, 5] },\n    });\n    console.log(result2); // 15\n  }\n}\n```\n\n## 使用 Midway 容器\n\n如果需要在 Worker 中使用 Midway 的依赖注入功能，我们需要在线程中单独在启动一个 Midway 环境。\n\n\n### 1. 创建线程中的环境\n\n你可以在主项目中单独创建一个目录用来保存线程代码，**必须** 使用 `defineConfiguration` 方法来创建入口。\n\n目录结构如下：\n```\n➜  base-app git:(feat/support_background_task) ✗ tree\n.\n├── package.json\n└── src\n    ├── configuration.ts      ## 主项目入口\n    └── worker                ## worker 目录\n        ├── index.ts\n        └── task.ts\n```\n\n我们可以在 Worker 目录中创建一个新 Midway 入口配置：\n\n```typescript\n// src/worker/index.ts\nimport { defineConfiguration } from '@midwayjs/core/functional';\nimport { CommonJSFileDetector } from '@midwayjs/core';\nimport * as piscina from '@midwayjs/piscina';\n\nexport default defineConfiguration({\n  namespace: 'worker',\n  detector: new CommonJSFileDetector(),\n  imports: [piscina], // 导入 Piscina\n});\n```\n\n### 2. 编写任务类\n\n和 Midway 其他组件类似，使用 `@PiscinaTask` 装饰器定义任务：\n\n```typescript\n// src/worker/task.ts\nimport { PiscinaTask, IPiscinaTask } from '@midwayjs/piscina';\n\n@PiscinaTask('calculate')\nexport class CalculateTask implements IPiscinaTask {\n  async execute(payload: { a: number; b: number; operation: string }) {\n    if (payload.operation === 'add') {\n      return payload.a + payload.b;\n    } else if (payload.operation === 'multiply') {\n      return payload.a * payload.b;\n    }\n    throw new Error('Unknown operation');\n  }\n}\n\n@PiscinaTask('square')\nexport class SquareTask implements IPiscinaTask {\n  async execute(payload: { value: number }) {\n    return payload.value * payload.value;\n  }\n}\n```\n\n`@PiscinaTask` 装饰器的参数为一个字符串，代表堆外暴露的 handler 名称。\n\n\n### 3. 配置主应用\n\n在主应用配置中指定 Worker 目录：\n\n```typescript\n// src/config/config.default.ts\nimport { join } from 'path';\n\nexport default {\n  piscina: {\n    client: {\n      // 指定 worker 入口文件\n      workerFile: join(__dirname, '../worker/index'),\n    },\n  },\n};\n```\n\n主应用需要忽略 Worker 目录，避免冲突：\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport { CommonJSFileDetector } from '@midwayjs/core';\nimport * as piscina from '@midwayjs/piscina';\n\n@Configuration({\n  imports: [piscina],\n  detector: new CommonJSFileDetector({\n    ignore: ['**/worker/**'], // 忽略 worker 目录\n  }),\n})\nexport class MainConfiguration {}\n```\n\n### 4. 执行容器任务\n\n使用 `runInContainer` 方法执行 Worker 容器中的任务：\n\n```typescript\n@Provide()\nexport class UserService {\n  @Inject()\n  piscinaService: piscina.PiscinaService;\n\n  async heavyTask() {\n    // 执行 calculate 任务 - 乘法\n    const result1 = await this.piscinaService.runInContainer('calculate', {\n      a: 5,\n      b: 6,\n      operation: 'multiply',\n    });\n    console.log(result1); // 30\n\n    // 执行 calculate 任务 - 加法\n    const result2 = await this.piscinaService.runInContainer('calculate', {\n      a: 10,\n      b: 20,\n      operation: 'add',\n    });\n    console.log(result2); // 30\n\n    // 执行 square 任务\n    const result3 = await this.piscinaService.runInContainer('square', {\n      value: 7,\n    });\n    console.log(result3); // 49\n  }\n}\n```\n\n## 取消任务\n\n使用 `AbortController` 可以取消正在运行的任务：\n\n```typescript\n@Provide()\nexport class UserService {\n  @Inject()\n  piscinaService: piscina.PiscinaService;\n\n  async cancelableTask() {\n    const abortController = new AbortController();\n\n    // 3 秒后取消任务\n    setTimeout(() => {\n      abortController.abort();\n    }, 3000);\n\n    try {\n      const result = await this.piscinaService.run(\n        {\n          handler: 'longRunning',\n          payload: { duration: 10000 }, // 10 秒的任务\n        },\n        {\n          signal: abortController.signal, // 传递 AbortSignal\n        }\n      );\n    } catch (error) {\n      console.error('任务被取消:', error);\n    }\n  }\n}\n```\n\n## 多个 Worker Pool\n\n可以配置多个 Worker Pool，每个 Pool 执行不同的任务：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  piscina: {\n    clients: {\n      // 计算任务池\n      compute: {\n        workerFile: join(__dirname, '../worker/compute.worker'),\n        maxThreads: 4,\n      },\n      // 图像处理任务池\n      image: {\n        workerFile: join(__dirname, '../worker/image.worker'),\n        maxThreads: 2,\n      },\n    },\n  },\n};\n```\n\n使用不同的 Pool：\n\n```typescript\n@Provide()\nexport class UserService {\n  @Inject()\n  piscinaServiceFactory: piscina.PiscinaServiceFactory;\n\n  async useDifferentPools() {\n    // 使用计算池\n    const computePool = this.piscinaServiceFactory.get('compute');\n    const result1 = await computePool.run({\n      handler: 'compute',\n      payload: { value: 10 },\n    });\n\n    // 使用图像处理池\n    const imagePool = this.piscinaServiceFactory.get('image');\n    const result2 = await imagePool.run({\n      handler: 'process',\n      payload: { imagePath: '/path/to/image.jpg' },\n    });\n  }\n}\n```\n\n## 配置选项\n\n### 常用配置\n\n[Piscina](https://github.com/piscinajs/piscina) 有非常丰富的线程配置。\n\n```typescript\nexport default {\n  piscina: {\n    client: {\n      workerFile: join(__dirname, '../worker/index'),\n      minThreads: 1,           // 最小线程数\n      maxThreads: 4,           // 最大线程数\n      idleTimeout: 60000,      // 空闲超时（毫秒）\n      maxQueue: 'auto',        // 最大队列长度\n      concurrentTasksPerWorker: 1, // 每个 Worker 的并发任务数\n    },\n  },\n};\n```\n\n### 多 Pool 配置\n\n```typescript\nexport default {\n  piscina: {\n    clients: {\n      default: {\n        workerFile: join(__dirname, '../worker/default.worker'),\n      },\n      heavy: {\n        workerFile: join(__dirname, '../worker/heavy.worker'),\n        maxThreads: 8,\n        idleTimeout: 30000,\n      },\n    },\n  },\n};\n```\n\n## Worker 文件路径说明\n\n- 支持 `.ts` 和 `.js` 文件，框架会自动查找\n- 建议使用不带扩展名的路径，框架按 `.js -> .ts -> .mjs -> .cjs` 顺序查找\n- 生产环境编译后会自动找到对应的 `.js` 文件\n\n```typescript\n// 推荐：不带扩展名\nworkerFile: join(__dirname, '../worker/compute.worker')\n\n// 也可以：显式指定扩展名\nworkerFile: join(__dirname, '../worker/compute.worker.js')\nworkerFile: join(__dirname, '../worker/compute.worker.ts')\n```\n\n## API 参考\n\n### PiscinaService\n\n#### run(task, options?)\n\n执行普通 Worker 任务。\n\n```typescript\nawait piscinaService.run(\n  {\n    handler: 'functionName',  // Worker 文件中导出的函数名\n    payload: { /* 数据 */ },  // 传递给函数的参数\n  },\n  {\n    signal: abortController.signal, // 可选：AbortSignal\n    transferList: [],                // 可选：可转移对象列表\n  }\n);\n```\n\n#### runInContainer(handler, payload?, options?)\n\n执行 Worker 容器中的 `@PiscinaTask` 任务。\n\n```typescript\nawait piscinaService.runInContainer(\n  'taskName',              // @PiscinaTask 装饰器的参数\n  { /* 数据 */ },          // 传递给 execute 方法的参数\n  {\n    signal: abortController.signal, // 可选：AbortSignal\n  }\n);\n```\n\n## 最佳实践\n\n### 适用场景\n\n- CPU 密集型计算（数据处理、加密解密、图像处理）\n- 耗时较长的同步操作\n- 需要避免阻塞主线程的场景\n- 需要并行处理大量任务的场景\n\n### 选择合适的模式\n\n**普通 Worker 模式**：\n- 适合简单的纯函数计算\n- 不需要依赖注入\n- 性能开销更小\n\n**Midway 容器模式**：\n- 需要使用依赖注入\n- 需要在 Worker 中使用其他服务\n- 适合复杂的业务逻辑\n\n### 注意事项\n\n1. **数据传递**：传递给 Worker 的数据会被序列化，不支持函数、类实例等不可序列化对象\n2. **线程数配置**：根据 CPU 核心数合理配置 `maxThreads`，避免过多线程导致上下文切换开销\n3. **内存管理**：Worker 线程有独立的内存空间，注意避免内存泄漏\n4. **错误处理**：Worker 中的错误会被捕获并传递回主线程，需要适当处理\n5. **路径问题**：Worker 文件路径建议使用绝对路径（如 `join(__dirname, '../worker/xxx')`）\n\n## 常见问题\n\n### 如何传递大量数据？\n\n对于大型数据（如 ArrayBuffer、Buffer），使用 `transferList` 避免数据复制：\n\n```typescript\nconst buffer = new ArrayBuffer(1024 * 1024);\nawait piscinaService.run(\n  { handler: 'process', payload: buffer },\n  { transferList: [buffer] }\n);\n```\n","isInternal":false,"tokens":3104,"sizeBytes":10586},{"name":"pm2.md","path":"site/docs/extensions/pm2.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/pm2.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# pm2\n\n[PM2](https://github.com/Unitech/pm2) 是带有内置负载平衡器的 Node.js 应用程序的生产过程管理器。可以利用它来简化很多 Node 应用管理的繁琐任务，如性能监控、自动重启、负载均衡等。\n\n## 安装\n\n我们一般会把 pm2 安装到全局。\n\n```bash\n$ npm install pm2 -g    # 命令行安装 pm2\n```\n\n## 常用命令\n\n```bash\n$ pm2 start   \t# 启动一个服务\n$ pm2 list    \t# 列出当前的服务\n$ pm2 stop\t\t\t# 停止某个服务\n$ pm2 restart\t\t# 重启某个服务\n$ pm2 delete\t\t# 删除某个服务\n$ pm2 logs\t\t\t# 查看服务的输出日志\n```\n\n比如， `pm2 list`，就会以表格显示。\n\n![](https://cdn.nlark.com/yuque/0/2021/png/501408/1616560437389-b193a0d0-b463-49f1-a347-8dec20e7504d.png)\n\npm2 的服务都有一个数组 id，你可以用 id 快速操作它。\n\n比如：\n\n```bash\n$ pm2 stop 1\t\t\t# 停止编号为 1 的服务\n$ pm2 delete 1\t\t# 删除编号为 1 的服务\n```\n\n使用 `--name` 参数添加一个应用名。\n\n```bash\n$ pm2 start ./bootstrap.js --name test_app\n```\n\n然后你可以用这个应用名来操作启停。\n\n```bash\n$ pm2 stop test_app\n$ pm2 restart test_app\n```\n\n## 启动应用\n\nMidway 应用一般使用 `npm run start` 做线上部署。其对应的命令为 `NODE_ENV=production node bootstrap.js`。\n\n:::info\n部署前需要执行编译 npm run build\n:::\n\n对应的 pm2 命令为\n\n```bash\n$ NODE_ENV=production pm2 start ./bootstrap.js --name midway_app -i 4\n```\n\n- --name 用于指定应用名\n- -i 用于指定启动的实例数（进程），会使用 cluster 模式启动\n\n效果如下：\n\n![](https://cdn.nlark.com/yuque/0/2021/png/501408/1616562075255-088155ee-7c4f-4eae-b5c5-db826f78b519.png)\n\n## Docker 容器启动\n\n在 Docker 容器中，后台启动的代码都会被退出，达不到预期效果。pm2 使用另一个命令来支持容器启动。\n\n请将命令修改为 pm2-runtime start 。\n\n```bash\n$ NODE_ENV=production pm2-runtime start ./bootstrap.js --name midway_app -i 4\n```\n\n具体的 pm2 行为请参考 [pm2 容器部署说明](https://www.npmjs.com/package/pm2#container-support)。\n","isInternal":false,"tokens":759,"sizeBytes":2119},{"name":"process_agent.md","path":"site/docs/extensions/process_agent.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/process_agent.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# 进程 Agent\n\nmidway 封装了 `@midwayjs/process-agent` 用来解决 node 场景中，多进程部分场景数据进程间数据不一致，或者无法指定 master 进程执行某个方法。\n\n\n举例：\n\n- 如果使用 pm2、cluster、多进程进行部署方式，使用内存的 cache，那这个 cache 在自己的进程内。\n- prometheus，获取 `/metrics` 的时候，需要把所有进程的数据收集上来，而不是某个进程的\n- 健康检查，如果有4个进程，如果有一个进程不正常了，健康检查应该检查失败。\n\n\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 安装方法\n\n使用方法：\n\n```bash\n$ npm install @midwayjs/process-agent@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/process-agent\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n## 引入组件\n\n`configuration.ts` 使用方法：\n\n```typescript\nimport * as processAgent from '@midwayjs/process-agent';\n\n@Configuration({\n  imports: [\n    // ...\n    processAgent\n  ],\n})\nexport class MainConfiguration {\n}\n\n```\n## 使用方法\n\n业务代码 UserService：\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { TestService } from './test';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  testService: TestService;\n\n  async getUser() {\n    let result = await this.testService.setData(1);\n    return result;\n  }\n}\n\n```\n然后调用 testService 的时候，希望只在主进程执行：\n\n```typescript\nimport { Provide, Scope, ScopeEnum } from '@midwayjs/core';\nimport { RunInPrimary } from '@midwayjs/process-agent';\n\n@Provide()\n@Scope(ScopeEnum.Singleton)\nexport class TestService {\n\n  data: any = 0;\n\n  @RunInPrimary()\n  async setData(b){\n    this.data = b;\n    return this.data;\n  }\n\n  @RunInPrimary()\n  async getData(){\n    return this.data;\n  }\n}\n\n```\n注意，执行返回的数据只限于可序列化的数据，比如普通 JSON，不支持包含方法等无法序列化的数据。\n\n\n## 效果描述\n假设采用pm2 或者 egg-script 等多进程方式启动，假设这是个请求\n\n首先：\n\n- 1、设置 setData\n- 2、然后获取 getData\n\n\n如果没有 RunInPrimary 这个装饰器，那请求可能落在进程2，或者进程3，那可能没有获取更新的data。\n\n所以 RunInPrimary 能确保这个函数执行能落到主进程去。\n\n\n## 功能征集\n如果有其他类似相关功能，觉得可以放在这个包里面的，欢迎在评论区，或者 [issue](https://github.com/midwayjs/midway/issues) 里面帮忙提一下，我们会跟大家一起讨论和实现。\n\n","isInternal":false,"tokens":859,"sizeBytes":2865},{"name":"prometheus.md","path":"site/docs/extensions/prometheus.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/prometheus.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# Prometheus\n\nPrometheus（普罗米修斯）是一个最初在 SoundCloud 上构建的监控系统。 自 2012 年成为社区开源项目，拥有非常活跃的开发人员和用户社区。为强调开源及独立维护，Prometheus 于 2016 年加入云原生云计算基金会（CNCF），成为继 Kubernetes 之后的第二个托管项目。\n\nGrafana 是一个开源的度量分析与可视化套件。纯 Javascript 开发的前端工具，通过访问库（如 InfluxDB），展示自定义报表、显示图表等。Grafana 支持许多不同的数据源。每个数据源都有一个特定的查询编辑器,该编辑器定制的特性和功能是公开的特定数据来源。而 Prometheus 正好是其支持的数据源之一。\n\n本篇介绍了 Midway 如何接入 Grafana + Prometheus。\n\n接入效果如下：\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1617259935548-a2df4339-3229-4391-bd3d-4ba8e6979d4d.png)\n\n## 安装依赖\n\n首先安装 Midway 提供的指标监控组件：\n\n```bash\n$ npm install @midwayjs/prometheus@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/prometheus\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n## 引入组件\n\n在 `configuration.ts` 中，引入这个组件：\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as prometheus from '@midwayjs/prometheus'; // 导入模块\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    prometheus\n  ],\n  importConfigs: [join(__dirname, 'config')],\n})\nexport class MainConfiguration {}\n```\n\n启动我们的应用，此时访问的时候多了一个 `${host}:${port}/metrics` 。\n\n:::info\nPrometheus 基于 HTTP 获取监控数据，请包含 web/koa/express 其中任一组件。\n:::\n\n访问接口，返回如下，里面的内容是当前的指标。\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1617260048533-4f725824-9471-40c9-be8b-6dcbf27d9cca.png)\n\n## 其他配置\n\n指标组件也提供了相关的配置，方便开发者进行配置。\n\n可以在 `config.default.ts` 中，修改 prometheus 的配置。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  prometheus: {\n    labels: {\n      APP_NAME: 'demo_project',\n    },\n  },\n}\n```\n\n更多的配置，我们可以查看定义进行配置。\n\n通过配置，我们例如可以归类哪些 node 是同一个应用，因为我们部署的时候，node 程序是分布式的。例如上面我们加了 APP_NAME，用来区分不同的应用，这样在监控指标中，我们可以区分不同的应用。\n\n## 数据采集\n\n我们前面在 Midway 中引入的组件主要是在 Node 中加了指标模块。接下来我们需要让 Prometheus 来采集我们的指标数据。\n\n如果开发者所在部门已经有 Prometheus+grafana 了，则只需将应用的指标地址上报给 PE 或者通过接口上报即可。此处我们假设大家没有 Prometheus+grafana，然后按照下面描述进行操作。\n\n## 搭建 Prometheus\n\n此处我们通过 docker-compose 来搭建 Prometheus， docker-compose.yml 文件如下：\n\n```yaml\nversion: '2.2'\nservices:\n  tapi:\n    logging:\n      driver: 'json-file'\n      options:\n        max-size: '50m'\n    image: prom/prometheus\n    restart: always\n    volumes:\n      - ./prometheus_data:/prometheus_data:rw\n      - ./prometheus.yml:/etc/prometheus/prometheus.yml\n      - ./targets.json:/etc/prometheus/targets.json\n    command:\n      - '--storage.tsdb.path=/prometheus_data'\n      - '--config.file=/etc/prometheus/prometheus.yml'\n      - '--storage.tsdb.retention=10d'\n      - '--web.enable-lifecycle'\n    ports:\n      - '9090:9090'\n```\n\n`prometheus.yml` 文件如下：\n\n```yaml\nglobal:\n  scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.\n  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.\nscrape_configs:\n  - job_name: 'node'\n    file_sd_configs:\n      - refresh_interval: 1m\n        files:\n          - '/etc/prometheus/targets.json'\n  - job_name: 'prometheus'\n    static_configs:\n      - targets: ['localhost:9090']\n```\n\n然后采集的 `targets.json` 如下：下面文件里面 `${ip}` 替换为 Node.js 应用所在服务器的 ip 地址。\n\n```json\n[\n  {\n    \"targets\": [\"${ip}:7001\"],\n    \"labels\": {\n      \"env\": \"prod\",\n      \"job\": \"api\"\n    }\n  }\n]\n```\n\n\n\n然后我们启动 `docker-compose.yml` 文件，\n\n```bash\n$ docker-compose up\n```\n\n至此，Prometheus 已经会去拉取我们 Node 应用程序的指标数据了。\n\n如果想要更新 target 怎么做：\n修改了这个 targets.json 文件后，通过 prometheus 的 reload 方法进行热加载。\n方法如下：\n\n```bash\n$ curl -X POST http://${prometheus的ip}:9090/-/reload\n```\n\n然后我们可以查看 prometheus 的页面也可以确认是否生效，界面地址：\n\n```text\nhttp://${prometheus的ip}:9090/classic/targets\n```\n\n接下来就是如何展示这些采集到的数据了。\n\n\n\n## 数据展示\n\n我们可以借助 Grafana 来展示我们的数据。\n\n此处我们简单通过 Docker 来搭建一下 Grafana：\n\n```bash\n$ docker run -d --name=grafana -p 3000:3000 grafana/grafana\n```\n也可以将 grafana 和 prometheus 放在一起使用 docker-compose 统一管理。\n\n将 grafana 添加到 `docker-compose.yml`, 示例如下：\n\n```yaml\nversion: '2.2'\nservices:\n  tapi:\n    logging:\n      driver: 'json-file'\n      options:\n        max-size: '50m'\n    image: prom/prometheus\n    restart: always\n    volumes:\n      - ./prometheus_data:/prometheus_data:rw  # prometheus Data mapping directory\n      - ./prometheus.yml:/etc/prometheus/prometheus.yml # prometheus Configuration mapping file\n      - ./targets.json:/etc/prometheus/targets.json\n    command:\n      - '--storage.tsdb.path=/prometheus_data'\n      - '--config.file=/etc/prometheus/prometheus.yml'\n      - '--storage.tsdb.retention=10d'\n      - '--web.enable-lifecycle'\n    ports:\n      - '9090:9090'\n  // highlight-start\n  grafana:\n    image: grafana/grafana\n    container_name: \"grafana0\"\n    ports:\n      - \"3000:3000\"\n    restart: always\n    volumes:\n      - \"./grafana_data:/var/lib/grafana\" # grafana data mapping directory\n      - \"./grafana_log:/var/log/grafana\"  # grafana log mapping directory\n    // highlight-end\n```\n重启 `docker-compose.yml` 文件\n\n```bash\ndocker-compose restart\n```\n![](https://cdn.nlark.com/yuque/0/2022/png/525744/1667300763153-5ee476a7-00ff-4899-92ba-5985995b4862.png)\n\n完成以上任意一种, 然后我们访问 127.0.0.1:3000，默认账号密码：admin:admin。\n\n访问后效果如下：\n\n\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1617260561047-c2643a69-6258-491b-937d-9bfc4558252f.png)\n\n然后我们让 Grafana 接入我们的 Prometheus 数据源：\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1617260581029-1e2e06a8-3054-4ad8-96b5-d50ab9bb1612.png)\n\n然后我们点击 Grafana 添加图表：\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1620725466020-28793a78-c03b-48fa-bf16-0c9c8ecc1a94.png)\n\n这边 ID 选择 14403，然后点击 load，然后点击下一步，然后点击 import 后，就能看到我们刚刚接入的效果了。\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1620725497338-a32a8982-d51f-4e74-b511-dc10a7c66d80.png)\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1620725514630-4f654f10-ef3a-41f7-b403-02832d3ef7d8.png)\n\n这样开发者可以运维自己的 Node 程序了，例如，是否最近引入了一个 NPM 包导致了什么内存泄漏的情况，是否最近有应用重启的情况了。\n\n当然还能支持其他的自定义操作。\n\n\n## Socket-io 场景\n\n使用方法：\n\n```bash\n$ npm install @midwayjs/prometheus-socket-io@4 --save\n```\n\n使用方法：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport { join } from 'path';\nimport * as prometheus from '@midwayjs/prometheus';\nimport * as prometheusSocketIo from '@midwayjs/prometheus-socket-io';\n\n@Configuration({\n  imports: [prometheus, prometheusSocketIo],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {}\n```\n\n然后在/metrics 这边就能看到 socket-io 的数据了。\n\n![](https://cdn.nlark.com/yuque/0/2021/png/187105/1631090438583-d925c13c-371a-4037-9f53-edaa34580aab.png)\n\n一共新增 8 个指标。\n后续会提供 Grafana 的模版 ID 给大家使用。\n\n\n## 功能介绍\n\n- [x] 根据 appName 进行分类\n- [x] 查看不同 path 的 qps 情况\n- [x] 查看不同 status 的分布情况\n- [x] 查询不同 path 的 rt 情况\n- [x] 进程的 CPU 使用情况\n- [x] 进程的内存使用情况\n- [x] 堆栈情况\n- [x] Event Loop\n- [ ] 等\n","isInternal":false,"tokens":2738,"sizeBytes":8715},{"name":"rabbitmq.md","path":"site/docs/extensions/rabbitmq.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/rabbitmq.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# RabbitMQ\n\n在复杂系统的架构中，会有负责处理消息队列的微服务，如下图：服务A负责产生消息给消息队列，而服务B则负责消费消息队列中的任务。\n\n![image.png](https://img.alicdn.com/imgextra/i3/O1CN01SYMbCz1moVSVLl7S2_!!6000000005001-2-tps-646-251.png)\n\n在Midway中，我们提供了订阅rabbitMQ的能力，专门来满足用户的这类需求。\n\n相关信息：\n\n**订阅服务**\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ❌    |\n\n\n\n## 基础概念\n\n\nRabbitMQ 的概念较为复杂，其基于高级消息队列协议即 Advanced Message Queuing Protocol（AMQP），如果第一次接触请阅读一下相关的参考文档。\n\n\nAMQP 有一些概念，Queue、Exchange 和 Binding 构成了 AMQP 协议的核心，包括：\n\n- Producer：消息生产者，即投递消息的程序。\n- Broker：消息队列服务器实体。\n   - Exchange：消息交换机，它指定消息按什么规则，路由到哪个队列。\n   - Binding：绑定，它的作用就是把 Exchange 和 Queue 按照路由规则绑定起来。\n   - Queue：消息队列载体，每个消息都会被投入到一个或多个队列。\n- Consumer：消息消费者，即接受消息的程序。\n\n\n\n简单的理解，消息通过 Publisher 发布到 Exchange（交换机），Consumer 通过订阅 Queue 来接受消息，Exchange 和 Queue 通过路由做连接。\n\n\n![image.png](https://img.alicdn.com/imgextra/i3/O1CN01fLrucw1FVNbCx4NqG_!!6000000000492-2-tps-700-328.png)\n\n\n\n## 消费者（Consumer）使用方法\n\n\n### 安装依赖\n\n\nMidway 提供了订阅 rabbitMQ 的能力，并能够独立部署和使用。安装 `@midwayjs/rabbitmq` 模块及其定义。\n```bash\n$ npm i @midwayjs/rabbitmq@4 --save\n$ npm i amqplib --save\n$ npm i @types/amqplib --save-dev\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/rabbitmq\": \"^4.0.0\",\n    \"amqplib\": \"^0.10.1\",\n    // ...\n  },\n  \"devDependencies\": {\n    \"@types/amqplib\": \"^0.8.2\",\n    // ...\n  }\n}\n```\n\n## 开启组件\n\n`@midwayjs/rabbmitmq` 可以作为独立主框架使用。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as rabbitmq from '@midwayjs/rabbitmq';\n\n@Configuration({\n  imports: [\n    rabbitmq\n  ],\n  // ...\n})\nexport class MainConfiguration {\n  async onReady() {\n        // ...\n  }\n}\n```\n\n也可以附加在其他的主框架下，比如 `@midwayjs/koa` 。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as rabbitmq from '@midwayjs/rabbitmq';\n\n@Configuration({\n  imports: [\n    koa,\n    rabbitmq\n  ],\n  // ...\n})\nexport class MainConfiguration {\n  async onReady() {\n        // ...\n  }\n}\n```\n\n### 目录结构\n\n\n我们一般把能力分为生产者和消费者，而订阅正是消费者的能力。\n\n\n我们一般把消费者放在 consumer 目录。比如 `src/consumer/userConsumer.ts`  。\n```\n➜  my_midway_app tree\n.\n├── src\n│   ├── consumer\n│   │   └── user.consumer.ts\n│   ├── interface.ts\n│   └── service\n│       └── user.service.ts\n├── test\n├── package.json\n└── tsconfig.json\n```\n代码示例如下。\n\n```typescript\nimport { Consumer, MSListenerType, RabbitMQListener, Inject } from '@midwayjs/core';\nimport { Context } from '@midwayjs/rabbitmq';\nimport { ConsumeMessage } from 'amqplib';\n\n@Consumer(MSListenerType.RABBITMQ)\nexport class UserConsumer {\n\n  @Inject()\n  ctx: Context;\n\n  @RabbitMQListener('tasks')\n  async gotData(msg: ConsumeMessage) {\n    this.ctx.channel.ack(msg);\n  }\n\n}\n\n```\n`@Consumer` 装饰器，提供消费者标识，并且它的参数，指定了某种消费框架的类型，比如，我们这里指定了 `MSListenerType.RABBITMQ` 这个类型，指的就是 rabbitMQ 类型。\n\n\n标识了 `@Consumer` 的类，对方法使用 `@RabbitMQListener` 装饰器后，可以绑定一个 RabbitMQ 的队列（Queue)。\n\n\n方法的参数为接收到的消息，类型为 `ConsumeMessage` 。如果返回值需要确认，则需要对服务端进行 `ack` 操作，明确接收到的数据。\n\n\n如果需要订阅多个队列，可以使用多个方法，也可以使用多个文件。\n\n\n### RabbitMQ 消息上下文\n\n\n订阅 `RabbitMQ` 数据的上下文，和 Web 同样的，其中包含一个 `requestContext` ，和每次接收消息的数据绑定。\n\n\n从 ctx 上可以取到 `channel` ，整个 ctx 的定义为：\n```typescript\nexport type Context = {\n  channel: amqp.Channel;\n  requestContext: IMidwayContainer;\n};\n```\n\n\n可以从框架获取定义\n```typescript\nimport { Context } from '@midwayjs/rabbitmq';\n```\n\n\n### 配置消费者\n\n我们需要在配置中指定 rabbitmq 的地址。\n\n```typescript\n// src/config/config.default\nimport { MidwayConfig } from '@midwayjs/core';\n\nexport default {\n  // ...\n  rabbitmq: {\n    url: 'amqp://localhost'\n  }\n} as MidwayConfig;\n```\n\n更多配置：\n\n| 属性 | 描述 |\n| --- | --- |\n| url | rabbitMQ 的连接信息 |\n| socketOptions | amqplib.connect 的第二个参数 |\n| reconnectTime | 队列断连后的重试时间，默认 10 秒 |\n\n\n\n### Fanout Exchange\n\n\nFanout 是一种特定的交换机，如果满足匹配（binding），就往 Exchange 所绑定的 Queue 发送消息。Fanout Exchange 会忽略 RoutingKey 的设置，直接将 Message 广播到所有绑定的 Queue 中。\n\n即所有订阅该交换机的 Queue 都会收到消息。\n\n比如，下面我们添加了两个 Queue，订阅了相同的交换机。\n```typescript\nimport { Consumer, MSListenerType, RabbitMQListener, Inject, App } from '@midwayjs/core';\nimport { Context, Application } from '@midwayjs/rabbitmq';\nimport { ConsumeMessage } from 'amqplib';\n\n@Consumer(MSListenerType.RABBITMQ)\nexport class UserConsumer {\n\n  @App()\n  app: Application;\n\n  @Inject()\n  ctx: Context;\n\n  @Inject()\n  logger;\n\n  @RabbitMQListener('abc', {\n    exchange: 'logs',\n    exchangeOptions: {\n      type: 'fanout',\n      durable: false,\n    },\n    exclusive: true,\n    consumeOptions: {\n      noAck: true,\n    }\n  })\n  async gotData(msg: ConsumeMessage) {\n    this.logger.info('test output1 =>', msg.content.toString('utf8'));\n    // TODO\n  }\n\n  @RabbitMQListener('bcd', {\n    exchange: 'logs',\n    exchangeOptions: {\n      type: 'fanout',\n      durable: false,\n    },\n    exclusive: true,\n    consumeOptions: {\n      noAck: true,\n    }\n  })\n  async gotData2(msg: ConsumeMessage) {\n    this.logger.info('test output2 =>', msg.content.toString('utf8'));\n    // TODO\n  }\n\n}\n\n```\n\n\n订阅的 abc 和 bcd 队列，绑定了相同的交换机 logs，最终的结果是，两个方法都会被调用。\n\n\n### Direct Exchange\n\n\nDirect Exchange 是 RabbitMQ 默认的 Exchange，完全根据 RoutingKey 来路由消息。设置 Exchange 和 Queue 的 Binding 时需指定 RoutingKey（一般为 Queue Name），发消息时也指定一样的 RoutingKey，消息就会被路由到对应的Queue。\n\n\n下面的示例代码，我们不填写 Queue Name，只添加一个 routingKey，交换机类型为 direct。\n```typescript\nimport { Consumer, MSListenerType, RabbitMQListener, Inject, App } from '@midwayjs/core';\nimport { Context, Application } from '../../../../../src';\nimport { ConsumeMessage } from 'amqplib';\n\n@Consumer(MSListenerType.RABBITMQ)\nexport class UserConsumer {\n\n  @App()\n  app: Application;\n\n  @Inject()\n  ctx: Context;\n\n  @Inject()\n  logger;\n\n  @RabbitMQListener('', {\n    exchange: 'direct_logs',\n    exchangeOptions: {\n      type: 'direct',\n      durable: false,\n    },\n    routingKey: 'direct_key',\n    exclusive: true,\n    consumeOptions: {\n      noAck: true,\n    }\n  })\n  async gotData(msg: ConsumeMessage) {\n    // TODO\n  }\n}\n\n```\n\n\ndirect 类型的消息，会根据 routerKey 做定向过滤，所以只有特定订阅能收到消息。\n\n\n\n\n### 装饰器参数\n\n\n`@RabbitMQListener` 装饰器的第一个参数为 queueName，代表需要监听的队列。\n\n\n第二个参数是一个对象，包含队列，交换机等参数，详细定义如下：\n```typescript\nexport interface RabbitMQListenerOptions {\n  exchange?: string;\n  /**\n   * queue options\n   */\n  exclusive?: boolean;\n  durable?: boolean;\n  autoDelete?: boolean;\n  messageTtl?: number;\n  expires?: number;\n  deadLetterExchange?: string;\n  deadLetterRoutingKey?: string;\n  maxLength?: number;\n  maxPriority?: number;\n  pattern?: string;\n  /**\n   * prefetch\n   */\n  prefetch?: number;\n  /**\n   * router\n   */\n  routingKey?: string;\n  /**\n   * exchange options\n   */\n  exchangeOptions?: {\n    type?: 'direct' | 'topic' | 'headers' | 'fanout' | 'match' | string;\n    durable?: boolean;\n    internal?: boolean;\n    autoDelete?: boolean;\n    alternateExchange?: string;\n    arguments?: any;\n  };\n  /**\n   * consumeOptions\n   */\n  consumeOptions?: {\n    consumerTag?: string;\n    noLocal?: boolean;\n    noAck?: boolean;\n    exclusive?: boolean;\n    priority?: number;\n    arguments?: any;\n  };\n}\n```\n\n\n\n\n### 本地测试\n\n\nMidway 提供了一个简单的测试方法用于测试订阅某个数据。 `@midwayjs/mock` 工具提供了一个 `createRabbitMQProducer` 的方法，用于创建一个生产者，通过它，你可以创建一个队列（Queue），以及向这个队列发消息。\n\n\n然后，我们启动一个 app，就可以自动监听到这个队列中的数据，并执行后续逻辑。\n\n```typescript\nimport { createRabbitMQProducer, close, creatApp } from '@midwayjs/mock';\n\ndescribe('/test/index.test.ts', () => {\n  it('should test create message and get from app', async () => {\n    // create a queue and channel\n    const channel = await createRabbitMQProducer('tasks', {\n      isConfirmChannel: true,\n      mock: false,\n      url: 'amqp://localhost',\n    });\n\n    // send data to queue\n    channel.sendToQueue('tasks', Buffer.from('something to do'))\n\n    // create app and got data\n    const app = await creatApp();\n\n    // wait a moment\n\n    await close(app);\n  });\n});\n\n```\n\n\n**示例一**\n\n\n创建一个 fanout exchange。\n```typescript\nconst manager = await createRabbitMQProducer('tasks-fanout', {\n  isConfirmChannel: false,\n  mock: false,\n  url: 'amqp://localhost',\n});\n\n// Name of the exchange\nconst ex = 'logs';\n// Write a message\nconst msg = \"Hello World!\";\n\n// 声明交换机\nmanager.assertExchange(ex, 'fanout', { durable: false }) // 'fanout' will broadcast all messages to all the queues it knows\n\n// 启动服务\nconst app = await creatApp('base-app-fanout', {\n  url: 'amqp://localhost',\n  reconnectTime: 2000\n});\n\n// 发送到交换机，由于不持久化，需要等订阅服务起来之后再发\nmanager.sendToExchange(ex, '', Buffer.from(msg))\n\n// 等一段时间\nawait sleep(5000);\n\n// 校验结果\n\n// 关闭 producer\nawait manager.close();\n\n// 关闭 app\nawait close(app);\n```\n\n\n**示例二**\n\n\n创建一个 direct exchange。\n```typescript\n/**\n  * direct 类型的消息，根据 routerKey 做定向过滤\n  */\nconst manager = await createRabbitMQProducer('tasks-direct', {\n  isConfirmChannel: false,\n  mock: false,\n  url: 'amqp://localhost',\n});\n\n// Name of the exchange\nconst ex = 'direct_logs';\n// Write a message\nconst msg = \"Hello World!\";\n\n// 声明交换机\nmanager.assertExchange(ex, 'direct', { durable: false }) // 'fanout' will broadcast all messages to all the queues it knows\n\nconst app = await creatApp('base-app-direct', {\n  url:  'amqp://localhost',\n  reconnectTime: 2000\n});\n\n// 这里指定 routerKey，发送到交换机\nmanager.sendToExchange(ex, 'direct_key', Buffer.from(msg))\n\n// 校验结果\n\nawait manager.close();\nawait close(app);\n```\n\n\n## 生产者（Producer）使用方法\n\n\n生产者（Producer）也就是第一节中的消息产生者，简单的来说就是会创建一个客户端，将消息发送到 RabbitMQ 服务。\n\n\n注意：当前 Midway 并没有使用组件来支持消息发送，这里展示的示例只是使用纯 SDK 在 Midway 中的写法。\n\n\n### 安装依赖\n\n\n```bash\n$ npm i amqplib amqp-connection-manager --save\n$ npm i @types/amqplib --save-dev\n```\n\n\n### 调用服务发送消息\n\n\n比如，我们在 service 文件下，新增一个 `rabbitmq.ts` 文件。\n```typescript\nimport { Provide, Scope, ScopeEnum, Init, Autoload, Destroy } from '@midwayjs/core';\nimport * as amqp from 'amqp-connection-manager'\n\n@Autoload()\n@Provide()\n@Scope(ScopeEnum.Singleton) // Singleton 单例，全局唯一（进程级别）\nexport class RabbitmqService {\n\n  private connection: amqp.AmqpConnectionManager;\n\n  private channelWrapper;\n\n  @Init()\n  async connect() {\n    // 创建连接，你可以把配置放在 Config 中，然后注入进来\n    this.connection = await amqp.connect('amqp://localhost');\n\n    // 创建 channel\n    this.channelWrapper = this.connection.createChannel({\n      json: true,\n      setup: function(channel) {\n        return Promise.all([\n          // 绑定队列\n        \tchannel.assertQueue(\"tasks\", { durable: true }),\n        ]);\n      }\n    });\n  }\n\n  // 发送消息\n  public async sendToQueue(queueName: string, data: any) {\n  \treturn this.channelWrapper.sendToQueue(queueName, data);\n  }\n\n  @Destroy()\n  async close() {\n  \tawait this.channelWrapper.close();\n    await this.connection.close();\n  }\n}\n\n```\n大概就是创建了一个用来封装消息通信的 service，同时他是全局唯一的 Singleton 单例。由于增加了 `@AutoLoad` 装饰器，可以自执行初始化。\n\n\n这样基础的调用服务就抽象好了，我们只需要在用到的地方，调用 `sendToQueue` 方法即可。\n\n\n比如：\n\n\n```typescript\n@Provide()\nexport class UserService {\n\n  @Inject()\n  rabbitmqService: RabbitmqService;\n\n\tasync invoke() {\n    // TODO\n\n    // 发送消息\n  \tawait this.rabbitmqService.sendToQueue('tasks', {hello: 'world'});\n  }\n}\n```\n\n\n## 参考文档\n\n\n- [理解 RabbitMQ Exchange](https://zhuanlan.zhihu.com/p/37198933)\n- [RabbitMQ for Node.js in 30 steps](https://github.com/Gurenax/node-rabbitmq)\n","isInternal":false,"tokens":4082,"sizeBytes":14131},{"name":"redis.md","path":"site/docs/extensions/redis.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/redis.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# Redis\n\n这里介绍如何快速在 Midway 中使用 Redis。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 安装依赖\n\n`@midwayjs/redis` 是主要的功能包。\n\n```bash\n$ npm i @midwayjs/redis@4 --save\n```\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/redis\": \"^4.0.0\",\n    // ...\n  }\n}\n```\n\n\n## 引入组件\n\n\n首先，引入组件，在 `src/configuration.ts` 中导入：\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as redis from '@midwayjs/redis';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    redis\t\t// 导入 redis 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ],\n})\nexport class MainConfiguration {\n}\n```\n\n\n## 配置 Redis\n\n\n**单客户端配置**\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  redis: {\n    client: {\n      port: 6379, // Redis port\n      host: \"127.0.0.1\", // Redis host\n      password: \"auth\",\n      db: 0,\n    },\n  },\n}\n```\n**Sentinel 配置**\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  redis: {\n    client: {\n      sentinels: [{          // Sentinel instances\n        port: 26379,         // Sentinel port\n        host: '127.0.0.1',   // Sentinel host\n      }],\n      name: 'mymaster',      // Master name\n      password: 'auth',\n      db: 0\n    },\n  },\n}\n```\n\n\n**Cluster 模式配置，需要配置多个**\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  redis: {\n    // Cluster Redis\n    client: {\n      cluster: true,\n      nodes: [{\n        host: 'host',\n        port: 'port',\n      },{\n        host: 'host',\n        port: 'port',\n      }],\n      redisOptions: {\n        family: '',\n        password: 'xxxx',\n        db: 'xxx'\n      }\n    }\n  },\n}\n```\n\n**多个客户端配置，需要配置多个**\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  redis: {\n\t\t// Multi Redis\n    clients: {\n      instance1: {\n        host: 'host',\n        port: 'port',\n        password: 'password',\n        db: 'db',\n      },\n      instance2: {\n        host: 'host',\n        port: 'port',\n        password: 'password',\n        db: 'db',\n      },\n    },\n  },\n}\n```\n更多参数可以查看 [ioredis 文档](https://github.com/luin/ioredis/blob/master/API.md#new_Redis_new)。\n\n\n## 使用 Redis 服务\n\n\n我们可以在任意的代码中注入使用。\n```typescript\nimport { Provide, Controller, Inject, Get } from '@midwayjs/core';\nimport { RedisService } from '@midwayjs/redis';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  redisService: RedisService;\n\n  async invoke() {\n\n    // 简单设置\n    await this.redisService.set('foo', 'bar');\n\n    // 设置过期时间，单位秒\n    await this.redisService.set('foo', 'bar', 'EX', 10);\n\n    // 获取数据\n    const result = await this.redisService.get('foo');\n\n   // result => bar\n  }\n}\n```\n\n\n可以使用 `RedisServiceFactory` 获取不同的实例。\n```typescript\nimport { RedisServiceFactory } from '@midwayjs/redis';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  redisServiceFactory: RedisServiceFactory;\n\n  async save() {\n    const redis1 = this.redisServiceFactory.get('instance1');\n    const redis2 = this.redisServiceFactory.get('instance3');\n\n    //...\n\n  }\n}\n```\n\n也可以通过装饰器获取。\n\n```typescript\nimport { RedisServiceFactory, RedisService } from '@midwayjs/redis';\nimport { InjectClient } from '@midwayjs/core';\n\n@Provide()\nexport class UserService {\n\n  @InjectClient(RedisServiceFactory, 'instance1')\n  redis1: RedisService;\n\n  @InjectClient(RedisServiceFactory, 'instance3')\n  redis2: RedisService;\n\n  async save() {\n    //...\n  }\n}\n```\n\n\n## 服务发现\n\n提供基于统一抽象的服务发现能力，虽然 Redis 一般不作为服务发现的注册中心，但是其拥有 pub/sub 能力可以变相提供功能，我们仍实现了 Redis 将作为服务发现的客户端能力。\n\n服务发现包括 **注册服务** 和 **获取服务** 两部分。\n\n\n### 配置服务发现\n\n你需要先配置一个客户端；若存在多个客户端，可通过 `serviceDiscoveryClient` 指定用于服务发现的客户端。\n\n通过 `serviceDiscovery` 配置服务发现选项：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  redis: {\n    clients: {\n      default: {\n        host: '127.0.0.1',\n        port: 6379\n      }\n    },\n    serviceDiscovery: {\n      prefix: 'services:',\n      ttl: 30,\n      scanCount: 100\n    }\n  }\n}\n```\n\n### 注册服务发现\n\n服务启动后，将当前服务注册到 Redis：\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport { RedisServiceDiscovery } from '@midwayjs/redis';\n\n@Configuration({})\nexport class MainConfiguration {\n  @Inject()\n  redisDiscovery: RedisServiceDiscovery;\n\n  async onServerReady() {\n    const client = this.redisDiscovery.createClient();\n\n    await client.register({\n      serviceName: 'order',\n      id: client.defaultMeta.id,\n      host: client.defaultMeta.host,\n      port: 7001,\n      ttl: 30,\n      meta: { version: '1.0.0' }\n    });\n    // 注册默认上线，无需显式 online\n  }\n}\n```\n\n说明：上线时写入实例键并设置 TTL，客户端会定时刷新 TTL 保持存活；下线删除实例键并清理刷新；注销发布变更消息以便 Listener 全量刷新。\n\n\n### 获取可用服务\n\n在任意位置获取可用实例或按负载均衡选择一个实例：\n\n````typescript\n@Provide()\nexport class OrderService {\n  @Inject()\n  redisDiscovery: RedisServiceDiscovery;\n\n  async getService() {\n    const instances = await this.redisDiscovery.getInstances('order');\n    const one = await this.redisDiscovery.getInstance('order');\n    return { instances, one };\n  }\n}\n````\n\n返回的实例为注册时写入的对象，通常包含 `serviceName/id/host/port/ttl/meta/tags/status` 等字段。\n\n","isInternal":false,"tokens":1699,"sizeBytes":6141},{"name":"render.md","path":"site/docs/extensions/render.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/render.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# 模板渲染\n\n本组件用于在 midway 体系使用服务端渲染 ejs，nunjucks 模板。\n\n相关信息：\n\n| web 支持情况      |      |\n| ----------------- | ---- |\n| @midwayjs/koa     | ✅    |\n| @midwayjs/faas    | ✅    |\n| @midwayjs/web     | ✅    |\n| @midwayjs/express | ❌    |\n\n\n\n\n## 使用 ejs\n\n\n### 安装依赖\n\n\n选择对应的模板安装依赖。\n```bash\n$ npm i @midwayjs/view-ejs@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/view-ejs\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n### 引入组件\n\n\n首先，引入组件，在 `configuration.ts` 中导入：\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as view from '@midwayjs/view-ejs';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    view\t\t// 导入 ejs 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n### 配置\n\n配置后缀，映射到指定的引擎。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  view: {\n    mapping: {\n      '.ejs': 'ejs',\n    },\n  },\n  // ejs config\n  ejs: {}\n}\n```\n### 使用\n\n\n注意，默认的 view 目录为 `${appDir}/view` ，在其中创建一个 `hello.ejs` 文件。\n\n\n目录结构如下：\n```\n➜  my_midway_app tree\n.\n├── src\n│   └── controller                 ## Controller 目录\n│       └── home.ts\n├── view                           ## 模板目录\n│   └── hello.ejs\n├── test\n├── package.json\n└── tsconfig.json\n```\n\n\n我们在模板里写一些 ejs 格式的内容，比如：\n```typescript\n// view/hello.ejs\nhello <%= data %>\n```\n\n\n在 Controller 中渲染。\n```typescript\nimport { Inject, Provide } from '@midwayjs/core';\nimport { Context } from '@midwayjs/koa';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  ctx: Context;\n\n  @Get('/')\n  async render(){\n    await this.ctx.render('hello.ejs', {\n      data: 'world',\n    });\n  }\n}\n```\n\n### 配置后缀\n\n默认后缀为 `.html` ，为了改成习惯的 `.ejs` 后缀，我们可以加一个 `defaultExtension` 配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  view: {\n    defaultExtension: '.ejs',\n    mapping: {\n      '.ejs': 'ejs',\n    },\n  },\n  // ejs config\n  ejs: {}\n}\n```\n\n这样我们在渲染时不需要增加后缀。\n\n```typescript\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  ctx: Context;\n\n  @Get('/')\n  async render(){\n    await this.ctx.render('hello', {\n      data: 'world',\n    });\n  }\n}\n```\n\n\n### 默认渲染引擎\n\n我们可以通过 `defaultViewEngine` 来设置默认的渲染引擎。\n\n其作用是，当遇到的模板后缀，比如 `.html` 未在配置的 `mapping` 字段中找到时，使用该 `defaultViewEngine` 字段指定的引擎来渲染。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  view: {\n    defaultViewEngine: 'ejs',\n    mapping: {\n      '.ejs': 'ejs',\n    },\n  },\n  // ejs config\n  ejs: {}\n}\n```\n\n这样，如果模板是 `.html` 后缀，由于 `mapping` 中未指定，依旧会使用 `ejs` 来渲染。\n\n### 配置多个模板目录\n\n如果我们需要将代码封装为组件提供，就需要支持不同的模板目录。\n\n默认的模板目录在 `${appDir}/view`。我们可以在 `rootDir` 字段增加其他的目录。\n\n```typescript\n// src/config/config.default.ts\n\n// 修改默认 view 组件的 default 目录\nexport default {\n  // ...\n  view: {\n    rootDir: {\n      default: path.join(__dirname, './view'),\n    }\n  },\n}\n\n// 其他组件需要增加目录的配置\nexport default {\n  // ...\n  // view 组件的配置\n  view: {\n    rootDir: {\n      anotherRoot: path.join(__dirname, './view'),\n    }\n  },\n}\n```\n\n通过对象合并的机制，使得所有的 `rootDir` 都能合并到一起，组件内部会获取 values 做匹配。\n\n\n\n## 使用 Nunjucks\n\n\n和 ejs 类似，引入对应组件即可。\n\n\n1、选择对应的模板安装依赖。\n```bash\n$ npm i @midwayjs/view-nunjucks@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/view-nunjucks\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n2、引入组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as view from '@midwayjs/view-nunjucks';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    view\t\t// 导入 nunjucks 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n\n3、增加 nunjucks 的配置，比如默认使用 nunjucks。\n```typescript\nexport default {\n  // ...\n  view: {\n    defaultViewEngine: 'nunjucks',\n    mapping: {\n      '.nj': 'nunjucks',\n    },\n  },\n}\n```\n\n\n4、在 view 目录增加模板\n```typescript\n// view/test.nj\nhi, {{ user }}\n```\n\n\n在 Controller 中渲染。\n```typescript\nimport { Inject, Provide } from '@midwayjs/core';\nimport { Context } from '@midwayjs/koa';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  ctx: Context;\n\n  @Get('/')\n  async render(){\n    await ctx.render('test.nj', { user: 'midway' });\n  }\n}\n```\n访问后会输出 `hi, midway` 。\n\n\n如果有自定义 filter 的需求，可以在入口处增加，比如下面增加了一个名为 `hello` 的 filter。\n```typescript\nimport { App, Configuration, Inject } from '@midwayjs/core';\nimport * as view from '@midwayjs/view-nunjucks';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [view],\n  importConfigs: [join(__dirname, 'config')]\n})\nexport class MainConfiguration {\n\n  @App()\n  app;\n\n  @Inject()\n  env: view.NunjucksEnvironment;\n\n  async onReady(){\n    this.env.addFilter('hello', (str) => {\n      return 'hi, ' + str;\n    });\n  }\n}\n\n```\n在模板里可以使用\n```typescript\n{{ name | hello }}\n```\n然后渲染\n```typescript\n// controller\n// ...\nawait ctx.render('test.nj', { name: 'midway' });\n```\n也会输出 `hi, midway` 。\n\n\n\n## 自定义模板引擎\n\n默认我们只提供了 ejs 和 nunjucks 的模板引擎，你也可以编写自己的模板引擎代码。\n\n### 实现模板引擎\n\n首先需要创建一个请求作用域的模板引擎类，它将在每个请求执行时初始化。你需需要实现其中的 `render` 和 `renderString` 方法。如果你的模板引擎不支持某个方法，可以抛出异常。\n\n```typescript\n// lib/view.ts\nimport { Provide, Config } from '@midwayjs/core';\nimport { IViewEngine } from '@midwayjs/view';\n\n@Provide()\nexport class MyView implements IViewEngine {\n\n  @Config('xxxx')\n  viewConfig;\n\n  async render(name: string, locals?: Record<string, any>, options?: RenderOptions) {\n    return myengine.render(name, locals, options);\n  }\n\n  async renderString(tpl: string,\n    locals?: Record<string, any>,\n    options?: RenderOptions) {\n\n    throw new Error('not implement');\n  }\n};\n```\n\n这两个方法接受类似的三个参数，`renderString` 第一个参数需要传入待解析的模板内容本身，而 `render` 方法会解析模板文件。\n\n`render(name, locals, viewOptions)`\n\n- name: 从 `root`（默认是 `/view` ) 相对的 path\n- locals: 模板需要的数据\n- viewOptions: 每次渲染的模板参数，可覆盖的配置，可以在配置文件中重写，其中包含几个参数：\n  - root: 模板的绝对路径\n  - name: 调用 render 方法的原始 name 值\n  - locals: 调用 render 方法的原始 locals 值\n\n`renderString(tpl, locals, viewOptions)`\n\n- tpl: 模板名\n- locals: 和 `render` 一样\n- viewOptions: 和 `render` 一样\n\n### 注册模板引擎\n\n在实现自定义的模板引擎后，我们需要在启动入口注册它。\n\n通过引入 `ViewManager` ，我们可以使用 `use` 方法注册自定义模板引擎。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject, Provide } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as view from '@midwayjs/view';\nimport { MyView } from './lib/my';\n\n@Configuration({\n  imports: [koa, view],\n  importConfigs: [join(__dirname, 'config')]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  viewManager: view.ViewManager;\n\n  async onReady(){\n    this.viewManager.use('ejs', MyView);\n  }\n}\n\n```\n\n\n\n## 注意事项\n\n\n如需在 egg(@midwayjs/web) 场景下使用，请在 `plugin.ts` 中关闭 view 和其相关插件。\n\n\n```typescript\nimport { EggPlugin } from 'egg';\nexport default {\n  // ...\n  view: false,\n} as EggPlugin;\n\n```\n\n\n否则会出现下面类似的错误。\n```\nTypeError: Cannot set property view of #<EggApplication> which has only a getter\n```\n\n","isInternal":false,"tokens":2576,"sizeBytes":8691},{"name":"security.md","path":"site/docs/extensions/security.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/security.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# 安全\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的通用安全组件，支持 `csrf` 、`xss` 等多种安全策略。\n\n相关信息：\n\n| web 支持情况      |      |\n| ----------------- | ---- |\n| @midwayjs/koa     | ✅    |\n| @midwayjs/faas    | ✅    |\n| @midwayjs/web     | ✅    |\n| @midwayjs/express | ✅    |\n\n\n\n## 安装使用\n\n1、安装依赖\n\n```bash\n$ npm i @midwayjs/security --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/security\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n2、在 configuration 中引入组件\n\n```typescript\nimport * as security from '@midwayjs/security';\n@Configuration({\n  imports: [\n    // ...other components\n    security\n  ],\n})\nexport class MainConfiguration {}\n```\n\n---\n\n## 防范常见的安全威胁\n\n\n### 一、CSRF\n\nCSRF（Cross-site request forgery 跨站请求伪造），是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。\n\n\n#### 1. 令牌同步模式\n通过响应页面时将 token 渲染到页面上，在开启 `csrf` 配置后，通过 `ctx.csrf` 可以获取到 `csrf token`，可以再返回页面 html 时同步输出\n\n```ts\n@Controller('/')\nexport class HomeController {\n  @Inject()\n  ctx;\n\n  @Get('/home')\n  async home() {\n    return `<form method=\"POST\" action=\"/upload?_csrf=${ this.ctx.csrf }\" >\n      title: <input name=\"title\" />\n      <button type=\"submit\">upload</button>\n    </form>`;\n  }\n}\n```\n\n传递 CSRF token 的字段（上述示例中的 `_csrf`）可以在配置中改变，请查看下述 `配置 -> csrf`。\n\n\n\n#### 2. Cookies 模式\n\n在 CSRF 默认配置下，token 会被设置在 Cookie 中，可以再前端页面中通过 JS 从 Cookies 中获取，然后再 ajax/fetch 等请求中添加到 `header`、`query` 或 `body` 中。\n\n```js\nconst csrftoken = Cookies.get('csrfToken');\nfetch('/api/post', {\n  method: 'POST',\n  headers: {\n    'x-csrf-token': csrftoken\n  },\n  ...\n});\n```\n\n默认配置下，框架会将 `CSRF token` 存在 `Cookie` 中，以方便前端 JS 发起请求时获取到。但是所有的子域名都可以设置 Cookie，因此当我们的应用处于无法保证所有的子域名都受控的情况下，存放在 `Cookie` 中可能有被 `CSRF` 攻击的风险。框架提供了一个配置项 `useSession`，可以将 token 存放到 Session 中。\n\n\n当 `CSRF token` 存储在 `Cookie` 中时，一旦在同一个浏览器上发生用户切换，新登陆的用户将会依旧使用旧的 token（之前用户使用的），这会带来一定的安全风险，因此在每次用户登陆的时候都必须调用 `ctx.rotateCsrfSecret()` 刷新 `CSRF token`，例如：\n\n\n```js\n@Controller('/')\nexport class HomeController {\n  @Inject()\n  ctx;\n\n  @Inject()\n  userService;\n\n  @Get('/login')\n  async login(@Body('username') username: string, @Body('password') password: string) {\n    const user = await userService.login({ username, password });\n    this.ctx.session = { user };\n    this.ctx.rotateCsrfSecret();\n    return { success: true };\n  }\n}\n```\n\n### 二、XSS\n\n`XSS`（cross-site scripting 跨站脚本攻击）攻击是最常见的 Web 攻击，是代码注入的一种。它允许恶意用户将代码注入到网页上，其他用户在观看网页时就会受到影响。\n\n`XSS` 攻击通常指的是通过利用网页开发时留下的漏洞，通过巧妙的方法注入恶意指令代码到网页，使用户加载并执行攻击者恶意制造的网页程序。攻击成功后，攻击者可能得到更高的权限（如执行一些操作）、私密网页内容、会话和cookie等各种内容。\n\n\n#### 1. 反射型的 XSS 攻击\n\n主要是由于服务端接收到客户端的不安全输入，在客户端触发代码执行从而发起 `Web` 攻击。\n\n例如：在某搜索网站搜索时，搜索结果会显示搜索的关键词。搜索关键词填入 `<script>alert('xss')</script>`, 点击搜索后，若页面程序没有对关键词进行处理，这段代码就会直接在页面上执行，弹出 alert。\n\n框架提供了 `ctx.security.escape()` 方法对字符串进行 XSS 过滤。\n\n```ts\n@Controller('/')\nexport class HomeController {\n  @Inject()\n  ctx;\n\n  @Get('/home')\n  async home() {\n    const str = `<script>alert('xss')</script>`;\n    const escapedStr = this.ctx.security.escape(str);\n    // &lt;script&gt;alert(&quot;xss&quot;) &lt;/script&gt;\n    return escapedStr;\n  }\n}\n```\n\n另外当网站输出的内容是作为 js 脚本的。这个时候需要使用 `ctx.security.js()` 来进行过滤。\n\n还有一种情况，有时候我们需要在 `js` 中输出 `json` ，若未做转义，易被利用为 `XSS` 漏洞。框架提供了 `ctx.security.json(变量)` 来提供 json encode，防止 XSS 攻击。\n\n\n```ts\n@Controller('/')\nexport class HomeController {\n  @Inject()\n  ctx;\n\n  @Get('/home')\n  async home() {\n    return `<script>windows.config = ${this.ctx.security.json( ...variable )};</script>`;\n  }\n}\n```\n\n#### 2. 存储型的 XSS 攻击\n\n通过提交带有恶意脚本的内容存储在服务器上，当其他人看到这些内容时发起 Web 攻击，比如一些网站的评论框中，用户恶意将一些代码作为评论内容，若没有过滤，其他用户看到这个评论时恶意代码就会执行。\n\n\n框架提供了 `ctx.security.html()` 来进行过滤。\n\n\n#### 3. 其他 XSS 的防范方式\n\n浏览器自身具有一定针对各种攻击的防范能力，他们一般是通过开启 Web 安全头生效的。框架内置了一些常见的 Web 安全头的支持。\n\n**CSP**\n\n`Content Security Policy`，简称 `CSP`，主要是用来定义页面可以加载哪些资源，减少 `XSS` 的发生。\n\n默认关闭（可通过 `csp: {enable: true}` 配置开启），开启后可以有效的防止 `XSS` 攻击的发生。要配置 `CSP` , 需要对 `CSP` 的 `policy` 策略有了解，具体细节可以参考 [阿里聚安全 - CSP是什么？](https://www.zhihu.com/question/21979782/answer/122682029)\n\n\n**X-Download-Options:noopen**\n\n默认开启（可通过 `noopen: {enable: false}` 配置关闭），禁用 IE 下下载框 Open 按钮，防止 IE 下下载文件默认被打开 XSS。\n\n**X-Content-Type-Options:nosniff**\n禁用 IE8 自动嗅探 mime 功能，默认关闭（可通过 `nosniff: {enable: true}` 配置开启），例如 text/plain 却当成 text/html 渲染，特别当本站点 serve 的内容未必可信的时候。\n\n**X-XSS-Protection**\nIE 提供的一些 XSS 检测与防范，默认开启（可通过 `xssProtection: {enable: false}` 配置关闭）\n\nclose 默认值 false，即设置为 1; mode=block\n\n---\n\n\n## 配置\n\n默认配置如下：\n\n```ts\n// src/config/config.default\nexport default {\n  // ...\n\n  // 默认配置\n  security: {\n    csrf: {\n      enable: true,\n      type: 'ctoken',\n      useSession: false,\n      cookieName: 'csrfToken',\n      sessionName: 'csrfToken',\n      headerName: 'x-csrf-token',\n      bodyName: '_csrf',\n      queryName: '_csrf',\n      refererWhiteList: [],\n    },\n    xframe: {\n      enable: true,\n      value: 'SAMEORIGIN',\n    },\n    csp: {\n      enable: false,\n    },\n    hsts: {\n      enable: false,\n      maxAge: 365 * 24 * 3600,\n      includeSubdomains: false,\n    },\n    noopen: {\n      enable: false,\n    },\n    nosniff: {\n      enable: false,\n    },\n    xssProtection: {\n      enable: true,\n      value: '1; mode=block',\n    },\n  },\n}\n\n```\n\n### csrf\n\n| 配置项 | 类型                                   | 作用描述 | 默认值 |\n| --- |--------------------------------------| --- | --- |\n| enable | boolean                              | 是否开启 | true |\n| type | 'all' / 'any' / 'ctoken' / 'referer' | csrf 校验类型，all/any 等于 ctoken + referer | 'ctoken' 从query/header/body 中获取 csrf token；；'referer' 则可以通过 refererWhiteList 配置白名单 |\n| useSession | boolean                              | csrf token 是否存放在 session 中 | false，默认存放在 cookies 中 |\n| cookieName | string                               | token 在 cookie 中存放的 字段 | 'csrfToken' |\n| sessionName | string                               | token 在 session 中存放的 字段 | 'csrfToken' |\n| headerName | string                               | token 在 header 中存放的 字段 | 'x-csrf-token' |\n| bodyName | string                               | token 在 body 中存放的 字段 | '_csrf' |\n| queryName | string                               | token 在 query 中存放的 字段 | '_csrf' |\n| refererWhiteList | Array\\<string\\>                      | 允许的来源白名单 | [] |\n\n#### 配置 refererWhiteList 不生效？\n+ 原因一：refererWhiteList 中需要配置 referer 的 host 部分，例如 referer 为 `https://midway-demo.com:1234/docs`，则 refererWhiteList 中需要配置 `midway-demo.com:1234`。\n+ 原因二：refererWhiteList 仅在 csrf 配置中 type 为 `referer` 的情况下生效，默认 type 为 `ctoken`，需要修改为 `referer`。\n+ 原因三：发送的http请求中的 referer 字段不是一个标准的 url 地址（例如没有加上请求协议等），参考 [MDN](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Referer) 文档\n\n### xframe\n\n\nxframe 用来配置 `X-Frame-Options` 响应头，用来给浏览器指示允许一个页面可否在 `frame`, `iframe`, `embed` 或者 `object` 中展现的标记。站点可以通过确保网站没有被嵌入到别人的站点里面，从而避免 `clickjacking` 攻击。\n\n`X-Frame-Options` 有三个可能的值：\n\n+ X-Frame-Options: deny：页面不允许在 frame 中展示\n+ X-Frame-Options: sameorigin：该页面可以在相同域名页面的 frame 中展示\n+ X-Frame-Options: allow-from https://example.com/：该页面可以在指定来源的 frame 中展示\n\n\n\n| 配置项 | 类型 | 作用描述 | 默认值 |\n| --- | --- | --- | --- |\n| enable | boolean | 是否开启 | true |\n| value | string | X-Frame-Options 值 | 'SAMEORIGIN' |\n\n\n\n### hsts\n\n`HTTP Strict Transport Security`（通常简称为 `HSTS` ）是一个安全功能，它告诉浏览器只能通过 `HTTPS` 访问当前资源，而不是 `HTTP`。\n\n| 配置项 | 类型 | 作用描述 | 默认值 |\n| --- | --- | --- | --- |\n| enable | boolean | 是否开启 | false |\n| maxAge | number | 在浏览器收到这个请求后的多少 `秒` 时间内凡是访问这个域名下的请求都使用HTTPS请求 | `365 * 24 * 3600` 即一年 |\n| includeSubdomains | boolean | 此规则是否适用于该网站的所有子域名 | false |\n\n\n### csp\n\nHTTP 响应头 `Content-Security-Policy` 允许站点管理者控制指定的页面加载哪些资源。这将帮助防止跨站脚本攻击（XSS）。\n\n\n| 配置项 | 类型                                                       | 作用描述 | 默认值 |\n| --- |----------------------------------------------------------| --- | --- |\n| enable | boolean                                                  | 是否开启 | false |\n| policy | Object\\<key: string, value: string / string[] / boolean> | 策略列表 | {} |\n| reportOnly | boolean                                                  | 是否开启 | false |\n| supportIE | boolean                                                  | 是否支持IE浏览器 | false |\n\n详细的 `policy` 配置可以参考: [Content Security Policy (CSP) 是什么？阿里聚安全](https://www.zhihu.com/question/21979782/answer/122682029)\n\n\n### noopen\n\n用于指定 `IE 8` 以上版本的用户不打开文件而直接保存文件。在下载对话框中不显式“打开”选项。\n\n| 配置项 | 类型 | 作用描述 | 默认值 |\n| --- | --- | --- | --- |\n| enable | boolean | 是否开启 | false |\n\n\n\n\n### nosniff\n\n开启后，如果从 `script` 或 `stylesheet` 读入的文件的 `MIME` 类型与指定 `MIME` 类型不匹配，不允许读取该文件。用于防止 `XSS` 等跨站脚本攻击。\n\n| 配置项 | 类型 | 作用描述 | 默认值 |\n| --- | --- | --- | --- |\n| enable | boolean | 是否开启 | false |\n\n\n\n\n### xssProtection\n\n用于启用浏览器的XSS过滤功能，以防止 `XSS` 跨站脚本攻击。\n\n`X-XSS-Protection` 响应头是 `IE`，`Chrome` 和 `Safari` 的一个特性，当检测到跨站脚本攻击 (XSS (en-US))时，浏览器将停止加载页面。若网站设置了良好的 `Content-Security-Policy` 来禁用内联 JavaScript ('unsafe-inline')，现代浏览器不太需要这些保护， 但其仍然可以为尚不支持 `CSP` 的旧版浏览器的用户提供保护。\n\n`X-XSS-Protection` 可以配置下述四个值\n\n+ `0`: 禁止XSS过滤。\n+ `1`：启用XSS过滤（通常浏览器是默认的）。 如果检测到跨站脚本攻击，浏览器将清除页面（删除不安全的部分）。\n+ `1;mode=block`：启用XSS过滤。 如果检测到攻击，浏览器将不会清除页面，而是阻止页面加载。\n+ `1; report=<reporting-URI>`： Chromium only，启用XSS过滤。 如果检测到跨站脚本攻击，浏览器将清除页面并使用CSP report-uri (en-US)指令的功能发送违规报告。\n\n| 配置项 | 类型 | 作用描述 | 默认值 |\n| --- | --- | --- | --- |\n| enable | boolean | 是否开启 | false |\n| value | string | X-XSS-Protection 配置 | `1; mode=block` |\n\n","isInternal":false,"tokens":4129,"sizeBytes":13353},{"name":"sequelize.md","path":"site/docs/extensions/sequelize.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/sequelize.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"# Sequelize\n\n本文档介绍如何在 Midway 中使用 Sequelize。\n\n:::tip\n\n当前模块从 v3.4.0 开始已经重构，历史写法兼容，如果查询历史文档，请参考 [这里](../legacy/sequelize)。\n\n:::\n\n相关信息：\n\n| 描述              |     |\n| ----------------- | --- |\n| 可用于标准项目    | ✅  |\n| 可用于 Serverless | ✅  |\n| 可用于一体化      | ✅  |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 和老写法的区别\n\n如果想使用新版本的用法，请参考下面的流程，将老代码进行修改，新老代码不能混用。\n\n升级方法：\n\n- 1、请在业务依赖中显式添加 `sequelize` 和 `sequelize-typescript`\n- 2、不再使用 `BaseTable` 装饰器，而直接使用 `sequelize-typescript` 包导出的 `Table` 装饰器\n- 3、在 `src/config.default` 的 `sequelize` 部分配置调整，参考下面的数据源配置部分\n  - 3.1 修改为数据源的形式 `sequelize.dataSource`\n  - 3.2 将实体模型在数据源的 `entities` 字段中声明\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/sequelize@4 sequelize sequelize-typescript --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/sequelize\": \"^4.0.0\",\n    \"sequelize\": \"^6.21.3\",\n    \"sequelize-typescript\": \"^2.1.0\"\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n## 安装数据库 Driver\n\n常用数据库驱动如下，选择你对应连接的数据库类型安装：\n\n```bash\n# for MySQL or MariaDB，也可以使用 mysql2 替代\nnpm install mysql --save\nnpm install mysql2 --save\n\n# for PostgreSQL or CockroachDB\nnpm install pg --save\n\n# for SQLite\nnpm install sqlite3 --save\n\n# for Microsoft SQL Server\nnpm install mssql --save\n\n# for sql.js\nnpm install sql.js --save\n\n# for Oracle\nnpm install oracledb --save\n\n# for MongoDB(experimental)\nnpm install mongodb --save\n```\n\n下面的文档，我们将以 `mysql2` 作为示例。\n\n\n### Directory structure\n\n一个基础的参考目录结构如下。\n\n\n```\nMyProject\n├── src\n│   ├── config\n│   │   └── config.default.ts\n│   ├── entity\n│   │   └── person.entity.ts\n│   ├── configuration.ts\n│   └── service\n├── .gitignore\n├── package.json\n├── README.md\n└── tsconfig.json\n```\n\n\n## 启用组件\n\n在 `src/configuration.ts` 文件中启用组件。\n\n```typescript\nimport { Configuration, ILifeCycle } from '@midwayjs/core';\nimport { join } from 'path';\nimport * as sequelize from '@midwayjs/sequelize';\n\n@Configuration({\n  imports: [\n    // ...\n    sequelize,\n  ],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration implements ILifeCycle {\n  // ...\n}\n```\n\n## 模型定义\n\n### 1、创建 Model（Entity）\n\n我们通过模型和数据库关联，在应用中的模型就是数据库表，在 Sequelize 中，模型是和实体绑定的，每一个实体（Entity) 文件，即是 Model，也是实体（Entity）。\n\n在示例中，需要一个实体，我们这里拿 `person` 举例。新建 entity 目录，在其中添加实体文件 `person.entity.ts` ，一个简单的实体如下。\n\n```typescript\n// src/entity/person.entity.ts\nimport { Table, Model, Column, HasMany } from 'sequelize-typescript';\n\n@Table\nexport class Hobby extends Model {\n  @Column\n  name: string;\n}\n\n@Table\nexport class Person extends Model {\n  @Column\n  name: string;\n\n  @Column\n  birthday: Date;\n\n  @HasMany(() => Hobby)\n  hobbies: Hobby[];\n}\n```\n\n要注意，这里的实体文件的每一个属性，其实是和数据库表一一对应的，基于现有的数据库表，我们往上添加内容。\n\n`@Table` 装饰器可以在不传递任何参数的情况下使用，更多参数请查看 [定义选项](https://sequelize.org/v5/manual/models-definition.html#configuration) 。\n\n```typescript\n@Table({\n  timestamps: true,\n  ...\n})\nexport class Person extends Model {}\n```\n\n\n\n### 2、主键\n\n主键 (id) 将从基类 Model 继承。 一般来说主键是 Integer 类型并且是自增的。\n\n主键设置有两种方法，设置 `@Column({primaryKey: true})` 或者 `@PrimaryKey`。\n\n比如：\n\n```typescript\nimport { Table, Model, PrimaryKey } from 'sequelize-typescript';\n\n@Table\nexport class Person extends Model {\n  @PrimaryKey\n  name: string;\n}\n```\n\n### 3、时间列\n\n主要指代的是 `@CreatedAt`, `@UpdatedAt`, `@DeletedAt` 单个装饰器标注的列。\n\n比如：\n\n```typescript\nimport { Table, Model, CreatedAt, UpdatedAt, DeletedAt } from 'sequelize-typescript';\n\n@Table\nexport class Person extends Model {\n  @CreatedAt\n  creationDate: Date;\n\n  @UpdatedAt\n  updatedOn: Date;\n\n  @DeletedAt\n  deletionDate: Date;\n}\n```\n\n| 装饰器       | 描述                                                                    |\n| ------------ | ----------------------------------------------------------------------- |\n| `@CreatedAt` | 会设置 `timestamps=true` 和 `createdAt='creationDate'`                  |\n| `@UpdatedAt` | 会设置 `timestamps=true` 和 `updatedAt='updatedOn'`                     |\n| `@DeletedAt` | 会设置 `timestamps=true`, `paranoid=true` 和 `deletedAt='deletionDate'` |\n\n### 4、普通列\n\n@Column 装饰器用于标注普通列，可以在不传递任何参数的情况下使用。 但是因此需要能够自动推断 js 类型（详见[类型推断](https://github.com/sequelize/sequelize-typescript#type-inference)）。\n\n```typescript\nimport { Table, Model, Column } from 'sequelize-typescript';\n\n@Table\nexport class Person extends Model {\n  @Column\n  name: string;\n}\n```\n\n或者指定列类型。\n\n```typescript\nimport { Table, Column, DataType } from 'sequelize-typescript';\n\n@Table\nexport class Person extends Model {\n  @Column(DataType.TEXT)\n  name: string;\n}\n```\n\n更多类型描述，请参考 [这里](https://sequelize.org/v5/manual/models-definition.html#configuration)。\n\n比如：\n\n```typescript\nimport { Table, Model, Column, DataType } from 'sequelize-typescript'\n\n@Table\nexport class Person extends Model {\n  @Column({\n    type: DataType.FLOAT,\n    comment: 'Some value',\n    ...\n  })\n  value: number;\n}\n```\n\n| 装饰器                               | 描述                                                                                              |\n| ------------------------------------ | ------------------------------------------------------------------------------------------------- |\n| `@Column`                            | 使用推导的 [dataType](https://sequelize.org/v5/manual/models-definition.html#data-types) 作为类型 |\n| `@Column(dataType: DataType)`        | 显式设置 [dataType](https://sequelize.org/v5/manual/models-definition.html#data-types)            |\n| `@Column(options: AttributeOptions)` | 设置 [attribute options](https://sequelize.org/v5/manual/models-definition.html#configuration)    |\n\n## 数据源配置\n\n新版本我们启用了 [数据源机制](../data_source)，在 `src/config.default.ts` 中配置：\n\n```typescript\n// src/config/config.default.ts\n\nimport { Person } from '../entity/person.entity';\n\nexport default {\n  // ...\n  sequelize: {\n    dataSource: {\n      // 第一个数据源，数据源的名字可以完全自定义\n      default: {\n        database: 'test4',\n        username: 'root',\n        password: '123456',\n        host: '127.0.0.1',\n        port: 3306,\n        encrypt: false,\n        dialect: 'mysql',\n        define: { charset: 'utf8' },\n        timezone: '+08:00',\n        // 本地的时候，可以通过 sync: true 直接 createTable\n        sync: false,\n        \n        // 实体形式\n        entities: [Person],\n\n        // 支持如下的扫描形式，为了兼容我们可以同时进行.js和.ts匹配️\n        entities: [\n          'entity',                        // 指定目录\n          '**/entity/*.entity.{j,t}s',     // 通配加后缀匹配\n        ],\n      },\n\n      // 第二个数据源\n      default2: {\n        // ...\n      },\n    },\n  },\n};\n```\n\n\n\n## 模型关联\n\n可以通过 `HasMany` 、`@HasOne` 、`@BelongsTo`、`@BelongsToMany` 和 `@ForeignKey` 装饰器在模型中直接描述关系。\n\n:::tip\n\n你不需要在数据库中创建外键也可以使用这个功能。\n\n:::\n\n### 一对多\n\n```typescript\nimport { Table, Model, Column, ForeignKey, BelongsTo, HasMany } from 'sequelize-typescript';\n\n@Table\nexport class Player extends Model {\n  @Column\n  name: string;\n\n  @Column\n  num: number;\n\n  @ForeignKey(() => Team)\n  @Column\n  teamId: number;\n\n  @BelongsTo(() => Team)\n  team: Team;\n}\n\n@Table\nexport class Team extends Model {\n  @Column\n  name: string;\n\n  @HasMany(() => Player)\n  players: Player[];\n}\n```\n\n`sequelize-typescript` 会在内部进行关联，会自动查询出相关的依赖。\n\n比如通过 `find` 查询。\n\n```typescript\nconst team = await Team.findOne({ include: [Player] });\n\nteam.players.forEach((player) => {\n  console.log(`Player ${player.name}`);\n});\n```\n\n### 多对多\n\n```typescript\nimport { Table, Model, Column, ForeignKey, BelongsToMany } from 'sequelize-typescript';\n\n@Table\nexport class Book extends Model {\n  @BelongsToMany(() => Author, () => BookAuthor)\n  authors: Author[];\n}\n\n@Table\nexport class Author extends Model {\n  @BelongsToMany(() => Book, () => BookAuthor)\n  books: Book[];\n}\n\n@Table\nexport class BookAuthor extends Model {\n  @ForeignKey(() => Book)\n  @Column\n  bookId: number;\n\n  @ForeignKey(() => Author)\n  @Column\n  authorId: number;\n}\n```\n\n上面的类型，在某些场景下是不安全的，比如上面的 `BookAuthor`，`Author` 的 `books` 的类型，可能会丢失某些属性，需要手动设置。\n\n```typescript\n@BelongsToMany(() => Book, () => BookAuthor)\nbooks: Array<Book & {BookAuthor: BookAuthor}>;\n```\n\n### 一对一\n\n对于一对一，使用 `@HasOne(...)`（关系的外键存在于另一个模型上）和 `@BelongsTo(...)`（关系的外键存在于此模型上）。\n\n比如：\n\n```typescript\nimport { Table, Column, Model, BelongsTo, ForeignKey } from 'sequelize-typescript';\nimport { User } from './user.entity';\n\n@Table\nexport class Photo extends Model {\n  @ForeignKey(() => User)\n  @Column({\n    comment: '用户Id',\n  })\n  userId: number;\n\n  @BelongsTo(() => User)\n  user: User;\n\n  @Column({\n    comment: '名字',\n  })\n  name: string;\n}\n\n@Table\nexport class User extends Model {\n  @Column\n  name: string;\n}\n```\n\n\n\n### 模型循环依赖\n\n如果你使用了 `@BelongsTo` 装饰器，很容易触发一个模型循环依赖的错误，比如：\n\n```\nReferenceError: Cannot access 'Photo' before initialization\n```\n\n你可以将类型使用 `ReturnType` 包裹起来。\n\n```typescript\nimport { Table, Column, Model, BelongsTo, ForeignKey } from 'sequelize-typescript';\nimport { User } from './user.entity';\n\n@Table\nexport class Photo extends Model {\n  // ...\n  @BelongsTo(() => User)\n  user: ReturnType<() => User>;\n}\n```\n\n\n\n\n\n## 静态操作方法\n\n如果是单个数据源，可以使用下面的静态方法。\n\n### 保存\n\n在需要调用的地方，使用实体模型来操作。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Person } from '../entity/person.entity';\n\n@Provide()\nexport class PersonService {\n  async createPerson() {\n    const person = new Person({ name: 'bob', age: 99 });\n    await person.save();\n  }\n}\n```\n\n### 查找和更新\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Person } from '../entity/person.entity';\n\n@Provide()\nexport class PersonService {\n  async updatePerson() {\n    const person = await Person.findOne();\n    // 更新\n    person.age = 100;\n    await person.save();\n\n    await Person.update(\n      {\n        name: 'bobby',\n      },\n      {\n        where: { id: 1 },\n      }\n    );\n  }\n}\n```\n\n## Repository 模式\n\nRepository 模式可以将查找、创建等静态操作从模型定义中分离出来。它还支持与多个 sequelize 实例（多数据源）一起使用。\n\n### 启动 Repository 模式\n\n和数据源配置相同，只是多了一个属性。\n\n```typescript\n// src/config/config.default.ts\n\nimport { Person } from '../entity/person.entity';\n\nexport default {\n  // ...\n  sequelize: {\n    dataSource: {\n      default: {\n        // ...\n        entities: [Person],\n\n        // 多了这一个\n        repositoryMode: true,\n      },\n    },\n    sync: false,\n  },\n};\n```\n\n如果是多个数据源，务必在每个数据源都开启该属性，开启后，原有的静态操作方法不再可用。\n\n你需要使用 `Repository` 的操作方式。\n\n### 使用 Repository 模式\n\n基本 API 和静态操作相同，Midway 对其进行了一些简单包裹，使用 `InjectRepository` 装饰器可以在服务中注入 `Repository`。\n\n```typescript\nimport { Controller, Get } from '@midwayjs/core';\nimport { InjectRepository } from '@midwayjs/sequelize';\nimport { Photo } from '../entity/photo.entity';\nimport { User } from '../entity/user.entity';\nimport { Op } from 'sequelize';\nimport { Repository } from 'sequelize-typescript';\n\n@Controller('/')\nexport class HomeController {\n  @InjectRepository(User)\n  userRepository: Repository<User>;\n\n  @InjectRepository(Photo)\n  photoRepository: Repository<Photo>;\n\n  @Get('/')\n  async home() {\n    // 查询\n    let result = await this.photoRepository.findAll();\n    console.log(result);\n\n    // 新增\n    await this.photoRepository.create({\n      name: '123',\n    });\n\n    // 删除\n    await this.photoRepository.destroy({\n      where: {\n        name: '123',\n      },\n    });\n\n    // 联合查询\n    // SELECT * FROM photo WHERE name = \"23\" OR name = \"34\";\n    let result = await this.photoRepository.findAll({\n      where: {\n        [Op.or]: [{ name: '23' }, { name: '34' }],\n      },\n    });\n    // => result\n\n    // 连表查询\n    let result = await this.userRepository.findAll({ include: [Photo] });\n    // => result\n  }\n}\n```\n\n关于 OP 的更多用法：[https://sequelize.org/v5/manual/querying.html](https://sequelize.org/v5/manual/querying.html)\n\n### 多库的支持\n\n在 Repository 模式下，我们可以在 `InjectRepository` 参数中指定特定的数据源。\n\n```typescript\nimport { Controller } from '@midwayjs/core';\nimport { InjectRepository } from '@midwayjs/sequelize';\nimport { Photo } from '../entity/photo.entity';\nimport { User } from '../entity/user.entity';\nimport { Repository } from 'sequelize-typescript';\n\n@Controller('/')\nexport class HomeController {\n  // 指定某个数据源\n  @InjectRepository(User, 'default')\n  userRepository: Repository<User>;\n  // ...\n}\n```\n\n\n## 高级功能\n\n### 数据源同步配置\n\nsequelize 在同步数据源时可以添加 sync 的参数。\n\n```typescript\nexport default {\n  // ...\n  sequelize: {\n    dataSource: {\n      default: {\n        sync: true,\n        syncOptions: {\n          force: false,\n          alter: true,\n        },\n      },\n    },\n    // 多个数据源时可以用这个指定默认的数据源\n    defaultDataSourceName: 'default',\n  },\n};\n```\n\n### 指定默认数据源\n\n在包含多个数据源时，可以指定默认的数据源。\n\n```typescript\nexport default {\n  // ...\n  sequelize: {\n    dataSource: {\n      default1: {\n        // ...\n      },\n      default2: {\n        // ...\n      },\n    },\n    // 多个数据源时可以用这个指定默认的数据源\n    defaultDataSourceName: 'default1',\n  },\n};\n```\n\n\n\n### 获取数据源\n\n数据源即创建出的 sequelize 对象，我们可以通过注入内置的数据源管理器来获取。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport { SequelizeDataSourceManager } from '@midwayjs/sequelize';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n\n  async onReady(container: IMidwayContainer) {\n    const dataSourceManager = await container.getAsync(SequelizeDataSourceManager);\n    const conn = dataSourceManager.getDataSource('default');\n    await conn.authenticate();\n  }\n}\n```\n\n从 v3.8.0 开始，也可以通过装饰器注入。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport { InjectDataSource } from '@midwayjs/sequelize';\nimport { Sequelize } from 'sequelize-typescript';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n\n  // 注入默认数据源\n  @InjectDataSource()\n  defaultDataSource: Sequelize;\n\n  // 注入自定义数据源\n  @InjectDataSource('default1')\n  customDataSource: Sequelize;\n\n  async onReady(container: IMidwayContainer) {\n    // ...\n  }\n}\n```\n\n\n\n## 常见问题\n\n### 1、Dialect needs to be explicitly supplied as of v4.0.0\n\n原因为配置中数据源没有指定 `dialect` 字段，确认数据源的结构，格式以及配置合并的结果。\n\n\n\n### 2、生成实体列\n\n请参考社区提供的模块，如 [sequelize-typescript-generator](https://github.com/spinlud/sequelize-typescript-generator)\n\n\n\n### 3、Raw Query\n\n如果遇到比较复杂的，可以使用 [raw query 方法](https://sequelize.org/v5/manual/raw-queries.html)\n\n\n\n### 4、TS2612 错误\n\n如果你的模型列报了 TS2612 错误，比如：\n\n```\nsrc/entity/AesTenantConfigInfo.ts:29:6 - error TS2612: Property 'id' will overwrite the base property in 'Model<AesTenantConfigInfoAttributes, AesTenantConfigInfoAttributes>'. If this is intentional, add an initializer. Otherwise, add a 'declare' modifier or remove the redundant declaration.\n\n29      id?: number;\n        ~~\n```\n\n可以将其赋一个空值。\n\n```typescript\nimport { Table, Column } from 'sequelize-typescript';\n\n@Table\nexport class User extends Model {\n  @Column({\n    primaryKey: true,\n    autoIncrement: true,\n    type: DataType.BIGINT,\n  })\n  id?: number = undefined;\n}\n```\n\n\n\n## 其他\n\n- 上面的文档，翻译自 sequelize-typescript，更多 API ，请参考 [英文文档](<(https://github.com/sequelize/sequelize-typescrip)>)\n- 一些 [案例](https://github.com/ddzyan/midway-practice)\n","isInternal":false,"tokens":4678,"sizeBytes":17788},{"name":"socketio.md","path":"site/docs/extensions/socketio.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/socketio.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Fsocketio.md -->","isInternal":false,"tokens":7696,"sizeBytes":26347},{"name":"static_file.md","path":"site/docs/extensions/static_file.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/static_file.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Fstatic_file.md -->","isInternal":false,"tokens":1431,"sizeBytes":5299},{"name":"swagger.md","path":"site/docs/extensions/swagger.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/swagger.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Fswagger.md -->","isInternal":false,"tokens":10297,"sizeBytes":37352},{"name":"tablestore.md","path":"site/docs/extensions/tablestore.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/tablestore.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Ftablestore.md -->","isInternal":false,"tokens":1097,"sizeBytes":4135},{"name":"tags.md","path":"site/docs/extensions/tags.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/tags.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Ftags.md -->","isInternal":false,"tokens":2974,"sizeBytes":10331},{"name":"tenant.md","path":"site/docs/extensions/tenant.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/tenant.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Ftenant.md -->","isInternal":false,"tokens":926,"sizeBytes":3135},{"name":"upload.md","path":"site/docs/extensions/upload.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/upload.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Fupload.md -->","isInternal":false,"tokens":4102,"sizeBytes":13398},{"name":"validate.md","path":"site/docs/extensions/validate.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/validate.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Fvalidate.md -->","isInternal":false,"tokens":6705,"sizeBytes":23219},{"name":"validation.md","path":"site/docs/extensions/validation.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/validation.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Fvalidation.md -->","isInternal":false,"tokens":8110,"sizeBytes":27873},{"name":"ws.md","path":"site/docs/extensions/ws.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/site/docs/extensions/ws.md","title":"Extensions Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=site%2Fdocs%2Fextensions%2Fws.md -->","isInternal":false,"tokens":3611,"sizeBytes":12758},{"name":"openspec-apply.md","path":".cursor/commands/openspec-apply.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/.cursor/commands/openspec-apply.md","title":"/openspec-apply","category":"command-prompt","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=.cursor%2Fcommands%2Fopenspec-apply.md -->","frontmatter":{"name":"/openspec-apply","id":"openspec-apply","category":"OpenSpec","description":"Implement an approved OpenSpec change and keep tasks in sync."},"isInternal":false,"tokens":295,"sizeBytes":1283},{"name":"openspec-archive.md","path":".cursor/commands/openspec-archive.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/.cursor/commands/openspec-archive.md","title":"/openspec-archive","category":"command-prompt","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=.cursor%2Fcommands%2Fopenspec-archive.md -->","frontmatter":{"name":"/openspec-archive","id":"openspec-archive","category":"OpenSpec","description":"Archive a deployed OpenSpec change and update specs."},"isInternal":false,"tokens":447,"sizeBytes":2018},{"name":"openspec-proposal.md","path":".cursor/commands/openspec-proposal.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/.cursor/commands/openspec-proposal.md","title":"/openspec-proposal","category":"command-prompt","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=.cursor%2Fcommands%2Fopenspec-proposal.md -->","frontmatter":{"name":"/openspec-proposal","id":"openspec-proposal","category":"OpenSpec","description":"Scaffold a new OpenSpec change and validate strictly."},"isInternal":false,"tokens":557,"sizeBytes":2532},{"name":"README.md","path":"packages/api-bridge/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/api-bridge/README.md","title":"api-bridge Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fapi-bridge%2FREADME.md -->","isInternal":false,"tokens":886,"sizeBytes":3435},{"name":"README.md","path":"packages/axios/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/axios/README.md","title":"axios Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Faxios%2FREADME.md -->","isInternal":false,"tokens":123,"sizeBytes":433},{"name":"README.md","path":"packages/bootstrap/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/bootstrap/README.md","title":"bootstrap Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fbootstrap%2FREADME.md -->","isInternal":false,"tokens":124,"sizeBytes":443},{"name":"README.md","path":"packages/bull-board/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/bull-board/README.md","title":"bull-board Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fbull-board%2FREADME.md -->","isInternal":false,"tokens":55,"sizeBytes":189},{"name":"README.md","path":"packages/bull/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/bull/README.md","title":"bull Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fbull%2FREADME.md -->","isInternal":false,"tokens":54,"sizeBytes":183},{"name":"README.md","path":"packages/bullmq/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/bullmq/README.md","title":"bullmq Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fbullmq%2FREADME.md -->","isInternal":false,"tokens":55,"sizeBytes":185},{"name":"README.md","path":"packages/busboy/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/busboy/README.md","title":"busboy Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fbusboy%2FREADME.md -->","isInternal":false,"tokens":128,"sizeBytes":443},{"name":"README.md","path":"packages/cache-manager/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/cache-manager/README.md","title":"cache-manager Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcache-manager%2FREADME.md -->","isInternal":false,"tokens":123,"sizeBytes":431},{"name":"README.md","path":"packages/captcha/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/captcha/README.md","title":"captcha Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcaptcha%2FREADME.md -->","isInternal":false,"tokens":55,"sizeBytes":210},{"name":"README.md","path":"packages/casbin-redis-adapter/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/casbin-redis-adapter/README.md","title":"casbin-redis-adapter Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcasbin-redis-adapter%2FREADME.md -->","isInternal":false,"tokens":126,"sizeBytes":437},{"name":"README.md","path":"packages/casbin-typeorm-adapter/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/casbin-typeorm-adapter/README.md","title":"casbin-typeorm-adapter Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcasbin-typeorm-adapter%2FREADME.md -->","isInternal":false,"tokens":127,"sizeBytes":439},{"name":"README.md","path":"packages/casbin/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/casbin/README.md","title":"casbin Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcasbin%2FREADME.md -->","isInternal":false,"tokens":125,"sizeBytes":433},{"name":"README.md","path":"packages/code-dye/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/code-dye/README.md","title":"code-dye Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcode-dye%2FREADME.md -->","isInternal":false,"tokens":51,"sizeBytes":183},{"name":"README.md","path":"packages/consul/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/consul/README.md","title":"consul Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fconsul%2FREADME.md -->","isInternal":false,"tokens":123,"sizeBytes":424},{"name":"README.md","path":"packages/core/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/core/README.md","title":"core Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcore%2FREADME.md -->","isInternal":false,"tokens":122,"sizeBytes":422},{"name":"README.md","path":"packages/cos/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/cos/README.md","title":"cos Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcos%2FREADME.md -->","isInternal":false,"tokens":124,"sizeBytes":430},{"name":"README.md","path":"packages/cron/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/cron/README.md","title":"cron Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcron%2FREADME.md -->","isInternal":false,"tokens":124,"sizeBytes":431},{"name":"README.md","path":"packages/cross-domain/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/cross-domain/README.md","title":"cross-domain Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcross-domain%2FREADME.md -->","isInternal":false,"tokens":266,"sizeBytes":846},{"name":"README.md","path":"packages/crud/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/crud/README.md","title":"crud Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fcrud%2FREADME.md -->","isInternal":false,"tokens":51,"sizeBytes":190},{"name":"README.md","path":"packages/etcd/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/etcd/README.md","title":"etcd Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fetcd%2FREADME.md -->","isInternal":false,"tokens":125,"sizeBytes":431},{"name":"README.md","path":"packages/event-emitter/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/event-emitter/README.md","title":"event-emitter Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fevent-emitter%2FREADME.md -->","isInternal":false,"tokens":125,"sizeBytes":431},{"name":"README.md","path":"packages/express-session/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/express-session/README.md","title":"express-session Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fexpress-session%2FREADME.md -->","isInternal":false,"tokens":580,"sizeBytes":2332},{"name":"README.md","path":"packages/faas/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/faas/README.md","title":"faas Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Ffaas%2FREADME.md -->","isInternal":false,"tokens":605,"sizeBytes":2105},{"name":"README.md","path":"packages/grpc/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/grpc/README.md","title":"grpc Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fgrpc%2FREADME.md -->","isInternal":false,"tokens":59,"sizeBytes":211},{"name":"README.md","path":"packages/http-proxy/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/http-proxy/README.md","title":"http-proxy Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fhttp-proxy%2FREADME.md -->","isInternal":false,"tokens":218,"sizeBytes":629},{"name":"README.md","path":"packages/info/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/info/README.md","title":"info Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Finfo%2FREADME.md -->","isInternal":false,"tokens":170,"sizeBytes":593},{"name":"README.md","path":"packages/jwt/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/jwt/README.md","title":"jwt Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fjwt%2FREADME.md -->","isInternal":false,"tokens":262,"sizeBytes":977},{"name":"README.md","path":"packages/kafka/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/kafka/README.md","title":"kafka Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fkafka%2FREADME.md -->","isInternal":false,"tokens":139,"sizeBytes":485},{"name":"README.md","path":"packages/leoric/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/leoric/README.md","title":"leoric Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fleoric%2FREADME.md -->","isInternal":false,"tokens":293,"sizeBytes":1160},{"name":"README.md","path":"packages/mcp/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/mcp/README.md","title":"mcp Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fmcp%2FREADME.md -->","isInternal":false,"tokens":125,"sizeBytes":433},{"name":"README.md","path":"packages/mikro/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/mikro/README.md","title":"mikro Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fmikro%2FREADME.md -->","isInternal":false,"tokens":52,"sizeBytes":191},{"name":"README.md","path":"packages/mikro7/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/mikro7/README.md","title":"mikro7 Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fmikro7%2FREADME.md -->","isInternal":false,"tokens":55,"sizeBytes":197},{"name":"README.md","path":"packages/mock/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/mock/README.md","title":"mock Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fmock%2FREADME.md -->","isInternal":false,"tokens":125,"sizeBytes":422},{"name":"README.md","path":"packages/mongoose/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/mongoose/README.md","title":"mongoose Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fmongoose%2FREADME.md -->","isInternal":false,"tokens":128,"sizeBytes":449},{"name":"README.md","path":"packages/mqtt/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/mqtt/README.md","title":"mqtt Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fmqtt%2FREADME.md -->","isInternal":false,"tokens":124,"sizeBytes":434},{"name":"README.md","path":"packages/nextjs/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/nextjs/README.md","title":"nextjs Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fnextjs%2FREADME.md -->","isInternal":false,"tokens":737,"sizeBytes":2787},{"name":"README.md","path":"packages/oss/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/oss/README.md","title":"oss Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Foss%2FREADME.md -->","isInternal":false,"tokens":124,"sizeBytes":430},{"name":"README.md","path":"packages/passport/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/passport/README.md","title":"passport Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fpassport%2FREADME.md -->","isInternal":false,"tokens":1800,"sizeBytes":6783},{"name":"README.md","path":"packages/piscina/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/piscina/README.md","title":"piscina Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fpiscina%2FREADME.md -->","isInternal":false,"tokens":18,"sizeBytes":86},{"name":"README.md","path":"packages/prometheus/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/prometheus/README.md","title":"prometheus Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fprometheus%2FREADME.md -->","isInternal":false,"tokens":642,"sizeBytes":2325},{"name":"README.md","path":"packages/rabbitmq/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/rabbitmq/README.md","title":"rabbitmq Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Frabbitmq%2FREADME.md -->","isInternal":false,"tokens":144,"sizeBytes":504},{"name":"README.md","path":"packages/react/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/react/README.md","title":"react Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Freact%2FREADME.md -->","isInternal":false,"tokens":1257,"sizeBytes":4775},{"name":"README.md","path":"packages/redis/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/redis/README.md","title":"redis Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fredis%2FREADME.md -->","isInternal":false,"tokens":124,"sizeBytes":432},{"name":"README.md","path":"packages/security/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/security/README.md","title":"security Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fsecurity%2FREADME.md -->","isInternal":false,"tokens":1909,"sizeBytes":5739},{"name":"README.md","path":"packages/sequelize/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/sequelize/README.md","title":"sequelize Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fsequelize%2FREADME.md -->","isInternal":false,"tokens":51,"sizeBytes":195},{"name":"README.md","path":"packages/session/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/session/README.md","title":"session Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fsession%2FREADME.md -->","isInternal":false,"tokens":475,"sizeBytes":1841},{"name":"README.md","path":"packages/skill-midway/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/skill-midway/README.md","title":"skill-midway Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fskill-midway%2FREADME.md -->","isInternal":false,"tokens":942,"sizeBytes":3745},{"name":"README.md","path":"packages/socketio/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/socketio/README.md","title":"socketio Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fsocketio%2FREADME.md -->","isInternal":false,"tokens":125,"sizeBytes":439},{"name":"README.md","path":"packages/static-file/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/static-file/README.md","title":"static-file Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fstatic-file%2FREADME.md -->","isInternal":false,"tokens":501,"sizeBytes":1979},{"name":"README.md","path":"packages/swagger/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/swagger/README.md","title":"swagger Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fswagger%2FREADME.md -->","isInternal":false,"tokens":67,"sizeBytes":257},{"name":"README.md","path":"packages/tablestore/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/tablestore/README.md","title":"tablestore Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Ftablestore%2FREADME.md -->","isInternal":false,"tokens":125,"sizeBytes":437},{"name":"README.md","path":"packages/tags/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/tags/README.md","title":"tags Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Ftags%2FREADME.md -->","isInternal":false,"tokens":50,"sizeBytes":180},{"name":"README.md","path":"packages/tenant/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/tenant/README.md","title":"tenant Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Ftenant%2FREADME.md -->","isInternal":false,"tokens":124,"sizeBytes":433},{"name":"README.md","path":"packages/typeorm/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/typeorm/README.md","title":"typeorm Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Ftypeorm%2FREADME.md -->","isInternal":false,"tokens":398,"sizeBytes":1582},{"name":"README.md","path":"packages/upload/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/upload/README.md","title":"upload Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fupload%2FREADME.md -->","isInternal":false,"tokens":602,"sizeBytes":2066},{"name":"README.md","path":"packages/validate/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/validate/README.md","title":"validate Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fvalidate%2FREADME.md -->","isInternal":false,"tokens":127,"sizeBytes":447},{"name":"README.md","path":"packages/validation-joi/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/validation-joi/README.md","title":"validation-joi Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fvalidation-joi%2FREADME.md -->","isInternal":false,"tokens":130,"sizeBytes":455},{"name":"README.md","path":"packages/validation-zod/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/validation-zod/README.md","title":"validation-zod Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fvalidation-zod%2FREADME.md -->","isInternal":false,"tokens":130,"sizeBytes":445},{"name":"README.md","path":"packages/validation-zod4/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/validation-zod4/README.md","title":"validation-zod4 Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fvalidation-zod4%2FREADME.md -->","isInternal":false,"tokens":134,"sizeBytes":455},{"name":"README.md","path":"packages/validation/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/validation/README.md","title":"validation Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fvalidation%2FREADME.md -->","isInternal":false,"tokens":127,"sizeBytes":447},{"name":"README.md","path":"packages/view-ejs/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/view-ejs/README.md","title":"view-ejs Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fview-ejs%2FREADME.md -->","isInternal":false,"tokens":130,"sizeBytes":440},{"name":"README.md","path":"packages/view-nunjucks/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/view-nunjucks/README.md","title":"view-nunjucks Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fview-nunjucks%2FREADME.md -->","isInternal":false,"tokens":133,"sizeBytes":455},{"name":"README.md","path":"packages/view/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/view/README.md","title":"view Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fview%2FREADME.md -->","isInternal":false,"tokens":1215,"sizeBytes":4982},{"name":"README.md","path":"packages/vue/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/vue/README.md","title":"vue Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fvue%2FREADME.md -->","isInternal":false,"tokens":214,"sizeBytes":712},{"name":"README.md","path":"packages/web-bridge/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/web-bridge/README.md","title":"web-bridge Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fweb-bridge%2FREADME.md -->","isInternal":false,"tokens":241,"sizeBytes":828},{"name":"README.md","path":"packages/web-express/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/web-express/README.md","title":"web-express Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fweb-express%2FREADME.md -->","isInternal":false,"tokens":127,"sizeBytes":441},{"name":"README.md","path":"packages/web-koa/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/web-koa/README.md","title":"web-koa Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fweb-koa%2FREADME.md -->","isInternal":false,"tokens":128,"sizeBytes":429},{"name":"README.md","path":"packages/web/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/web/README.md","title":"web Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fweb%2FREADME.md -->","isInternal":false,"tokens":127,"sizeBytes":428},{"name":"README.md","path":"packages/ws/README.md","rawUrl":"https://raw.githubusercontent.com/midwayjs/midway/HEAD/packages/ws/README.md","title":"ws Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/midwayjs/midway?path=packages%2Fws%2FREADME.md -->","isInternal":false,"tokens":124,"sizeBytes":439}],"systemPromptSnippet":"<yakaai_skills repo=\"midwayjs/midway\">\n<!-- File: AGENTS.md (Tokens: ~467 | Category: root-instruction) -->\n<!-- OPENSPEC:START -->\n# OpenSpec Instructions\n\nThese instructions are for AI assistants working in this project.\n\nAlways open `@/openspec/AGENTS.md` when the request:\n- Mentions planning or proposals (words like proposal, spec, change, plan)\n- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work\n- Sounds ambiguous and you need the authoritative spec before coding\n\nUse `@/openspec/AGENTS.md` to learn:\n- How to create and apply change proposals\n- Spec format and conventions\n- Project structure and guidelines\n\nKeep this managed block so 'openspec update' can refresh the instructions.\n\n<!-- OPENSPEC:END -->\n\n# Repository Guide for Agents\n\n## Project overview\n- Midway v4 monorepo managed by lerna + pnpm workspace.\n- TypeScript 5, built with `tsc`, tested with `jest`.\n- IoC container implementation lives in `packages/core` (MidwayContainer, DecoratorManager, MetadataManager).\n\n## Code rules\n- Each component is an independent npm package and can communicate via DI.\n- New/changed components must include tests. Run `npm run test` in the package.\n- Use `mwts` for linting (`npm run lint:fix` when needed).\n- After code changes, rerun the relevant lint command and keep necessary lint-fix output in the patch instead of reverting it casually.\n- Add meaningful comments for functions/classes/interfaces/enums.\n\n## Docs rules\n- Current docs live under `site/docs` (Docusaurus).\n- Match existing doc style/tone/structure for consistency.\n- Show dependency installation in both bash (npm) and JSON `package.json` forms.\n- Doc changes do not require a build check.\n- For component docs, write for beginners first: start from what the component solves, when to use it, and the simplest working path, then go deeper into advanced usage, configuration, and extension points. Prefer tutorial-style progression over API-only lists.\n\n## Practical tips\n- Prefer `pnpm -C <package> test` to scope tests.\n- Keep changes aligned with existing patterns in the targeted package.\n- Pull request titles must use standard commit format, such as `feat: xxx` or `fix: xxx`.\n\n<!-- File: openspec/AGENTS.md (Tokens: ~3627 | Category: root-instruction) -->\n# OpenSpec Instructions\n\nInstructions for AI coding assistants using OpenSpec for spec-driven development.\n\n## TL;DR Quick Checklist\n\n- Search existing work: `openspec spec list --long`, `openspec list` (use `rg` only for full-text search)\n- Decide scope: new capability vs modify existing capability\n- Pick a unique `change-id`: kebab-case, verb-led (`add-`, `update-`, `remove-`, `refactor-`)\n- Scaffold: `proposal.md`, `tasks.md`, `design.md` (only if needed), and delta specs per affected capability\n- Write deltas: use `## ADDED|MODIFIED|REMOVED|RENAMED Requirements`; include at least one `#### Scenario:` per requirement\n- Validate: `openspec validate [change-id] --strict --no-interactive` and fix issues\n- Request approval: Do not start implementation until proposal is approved\n\n## Three-Stage Workflow\n\n### Stage 1: Creating Changes\nCreate proposal when you need to:\n- Add features or functionality\n- Make breaking changes (API, schema)\n- Change architecture or patterns  \n- Optimize performance (changes behavior)\n- Update security patterns\n\nTriggers (examples):\n- \"Help me create a change proposal\"\n- \"Help me plan a change\"\n- \"Help me create a proposal\"\n- \"I want to create a spec proposal\"\n- \"I want to create a spec\"\n\nLoose matching guidance:\n- Contains one of: `proposal`, `change`, `spec`\n- With one of: `create`, `plan`, `make`, `start`, `help`\n\nSkip proposal for:\n- Bug fixes (restore intended behavior)\n- Typos, formatting, comments\n- Dependency updates (non-breaking)\n- Configuration changes\n- Tests for existing behavior\n\n**Workflow**\n1. Review `openspec/project.md`, `openspec list`, and `openspec list --specs` to understand current context.\n2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, optional `design.md`, and spec deltas under `openspec/changes/<id>/`.\n3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement.\n4. Run `openspec validate <id> --strict --no-interactive` and resolve any issues before sharing the proposal.\n\n### Stage 2: Implementing Changes\nTrack these steps as TODOs and complete them one by one.\n1. **Read proposal.md** - Understand what's being built\n2. **Read design.md** (if exists) - Review technical decisions\n3. **Read tasks.md** - Get implementation checklist\n4. **Implement tasks sequentially** - Complete in order\n5. **Confirm completion** - Ensure every item in `tasks.md` is finished before updating statuses\n6. **Update checklist** - After all work is done, set every task to `- [x]` so the list reflects reality\n7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved\n\n### Stage 3: Archiving Changes\nAfter deployment, create separate PR to:\n- Move `changes/[name]/` → `changes/archive/YYYY-MM-DD-[name]/`\n- Update `specs/` if capabilities changed\n- Use `openspec archive <change-id> --skip-specs --yes` for tooling-only changes (always pass the change ID explicitly)\n- Run `openspec validate --strict --no-interactive` to confirm the archived change passes checks\n\n## Before Any Task\n\n**Context Checklist:**\n- [ ] Read relevant specs in `specs/[capability]/spec.md`\n- [ ] Check pending changes in `changes/` for conflicts\n- [ ] Read `openspec/project.md` for conventions\n- [ ] Run `openspec list` to see active changes\n- [ ] Run `openspec list --specs` to see existing capabilities\n\n**Before Creating Specs:**\n- Always check if capability already exists\n- Prefer modifying existing specs over creating duplicates\n- Use `openspec show [spec]` to review current state\n- If request is ambiguous, ask 1–2 clarifying questions before scaffolding\n\n### Search Guidance\n- Enumerate specs: `openspec spec list --long` (or `--json` for scripts)\n- Enumerate changes: `openspec list` (or `openspec change list --json` - deprecated but available)\n- Show details:\n  - Spec: `openspec show <spec-id> --type spec` (use `--json` for filters)\n  - Change: `openspec show <change-id> --json --deltas-only`\n- Full-text search (use ripgrep): `rg -n \"Requirement:|Scenario:\" openspec/specs`\n\n## Quick Start\n\n### CLI Commands\n\n```bash\n# Essential commands\nopenspec list                  # List active changes\nopenspec list --specs          # List specifications\nopenspec show [item]           # Display change or spec\nopenspec validate [item]       # Validate changes or specs\nopenspec archive <change-id> [--yes|-y]   # Archive after deployment (add --yes for non-interactive runs)\n\n# Project management\nopenspec init [path]           # Initialize OpenSpec\nopenspec update [path]         # Update instruction files\n\n# Interactive mode\nopenspec show                  # Prompts for selection\nopenspec validate              # Bulk validation mode\n\n# Debugging\nopenspec show [change] --json --deltas-only\nopenspec validate [change] --strict --no-interactive\n```\n\n### Command Flags\n\n- `--json` - Machine-readable output\n- `--type change|spec` - Disambiguate items\n- `--strict` - Comprehensive validation\n- `--no-interactive` - Disable prompts\n- `--skip-specs` - Archive without spec updates\n- `--yes`/`-y` - Skip confirmation prompts (non-interactive archive)\n\n## Directory Structure\n\n```\nopenspec/\n├── project.md              # Project conventions\n├── specs/                  # Current truth - what IS built\n│   └── [capability]/       # Single focused capability\n│       ├── spec.md         # Requirements and scenarios\n│       └── design.md       # Technical patterns\n├── changes/                # Proposals - what SHOULD change\n│   ├── [change-name]/\n│   │   ├── proposal.md     # Why, what, impact\n│   │   ├── tasks.md        # Implementation checklist\n│   │   ├── design.md       # Technical decisions (optional; see criteria)\n│   │   └── specs/          # Delta changes\n│   │       └── [capability]/\n│   │           └── spec.md # ADDED/MODIFIED/REMOVED\n│   └── archive/            # Completed changes\n```\n\n## Creating Change Proposals\n\n### Decision Tree\n\n```\nNew request?\n├─ Bug fix restoring spec behavior? → Fix directly\n├─ Typo/format/comment? → Fix directly  \n├─ New feature/capability? → Create proposal\n├─ Breaking change? → Create proposal\n├─ Architecture change? → Create proposal\n└─ Unclear? → Create proposal (safer)\n```\n\n### Proposal Structure\n\n1. **Create directory:** `changes/[change-id]/` (kebab-case, verb-led, unique)\n\n2. **Write proposal.md:**\n```markdown\n# Change: [Brief description of change]\n\n## Why\n[1-2 sentences on problem/opportunity]\n\n## What Changes\n- [Bullet list of changes]\n- [Mark breaking changes with **BREAKING**]\n\n## Impact\n- Affected specs: [list capabilities]\n- Affected code: [key files/systems]\n```\n\n3. **Create spec deltas:** `specs/[capability]/spec.md`\n```markdown\n## ADDED Requirements\n### Requirement: New Feature\nThe system SHALL provide...\n\n#### Scenario: Success case\n- **WHEN** user performs action\n- **THEN** expected result\n\n## MODIFIED Requirements\n### Requirement: Existing Feature\n[Complete modified requirement]\n\n## REMOVED Requirements\n### Requirement: Old Feature\n**Reason**: [Why removing]\n**Migration**: [How to handle]\n```\nIf multiple capabilities are affected, create multiple delta files under `changes/[change-id]/specs/<capability>/spec.md`—one per capability.\n\n4. **Create tasks.md:**\n```markdown\n## 1. Implementation\n- [ ] 1.1 Create database schema\n- [ ] 1.2 Implement API endpoint\n- [ ] 1.3 Add frontend component\n- [ ] 1.4 Write tests\n```\n\n5. **Create design.md when needed:**\nCreate `design.md` if any of the following apply; otherwise omit it:\n- Cross-cutting change (multiple services/modules) or a new architectural pattern\n- New external dependency or significant data model changes\n- Security, performance, or migration complexity\n- Ambiguity that benefits from technical decisions before coding\n\nMinimal `design.md` skeleton:\n```markdown\n## Context\n[Background, constraints, stakeholders]\n\n## Goals / Non-Goals\n- Goals: [...]\n- Non-Goals: [...]\n\n## Decisions\n- Decision: [What and why]\n- Alternatives considered: [Options + rationale]\n\n## Risks / Trade-offs\n- [Risk] → Mitigation\n\n## Migration Plan\n[Steps, rollback]\n\n## Open Questions\n- [...]\n```\n\n## Spec File Format\n\n### Critical: Scenario Formatting\n\n**CORRECT** (use #### headers):\n```markdown\n#### Scenario: User login success\n- **WHEN** valid credentials provided\n- **THEN** return JWT token\n```\n\n**WRONG** (don't use bullets or bold):\n```markdown\n- **Scenario: User login**  ❌\n**Scenario**: User login     ❌\n### Scenario: User login      ❌\n```\n\nEvery requirement MUST have at least one scenario.\n\n### Requirement Wording\n- Use SHALL/MUST for normative requirements (avoid should/may unless intentionally non-normative)\n\n### Delta Operations\n\n- `## ADDED Requirements` - New capabilities\n- `## MODIFIED Requirements` - Changed behavior\n- `## REMOVED Requirements` - Deprecated features\n- `## RENAMED Requirements` - Name changes\n\nHeaders matched with `trim(header)` - whitespace ignored.\n\n#### When to use ADDED vs MODIFIED\n- ADDED: Introduces a new capability or sub-capability that can stand alone as a requirement. Prefer ADDED when the change is orthogonal (e.g., adding \"Slash Command Configuration\") rather than altering the semantics of an existing requirement.\n- MODIFIED: Changes the behavior, scope, or acceptance criteria of an existing requirement. Always paste the full, updated requirement content (header + all scenarios). The archiver will replace the entire requirement with what you provide here; partial deltas will drop previous details.\n- RENAMED: Use when only the name changes. If you also change behavior, use RENAMED (name) plus MODIFIED (content) referencing the new name.\n\nCommon pitfall: Using MODIFIED to add a new concern without including the previous text. This causes loss of detail at archive time. If you aren’t explicitly changing the existing requirement, add a new requirement under ADDED instead.\n\nAuthoring a MODIFIED requirement correctly:\n1) Locate the existing requirement in `openspec/specs/<capability>/spec.md`.\n2) Copy the entire requirement block (from `### Requirement: ...` through its scenarios).\n3) Paste it under `## MODIFIED Requirements` and edit to reflect the new behavior.\n4) Ensure the header text matches exactly (whitespace-insensitive) and keep at least one `#### Scenario:`.\n\nExample for RENAMED:\n```markdown\n## RENAMED Requirements\n- FROM: `### Requirement: Login`\n- TO: `### Requirement: User Authentication`\n```\n\n## Troubleshooting\n\n### Common Errors\n\n**\"Change must have at least one delta\"**\n- Check `changes/[name]/specs/` exists with .md files\n- Verify files have operation prefixes (## ADDED Requirements)\n\n**\"Requirement must have at least one scenario\"**\n- Check scenarios use `#### Scenario:` format (4 hashtags)\n- Don't use bullet points or bold for scenario headers\n\n**Silent scenario parsing failures**\n- Exact format required: `#### Scenario: Name`\n- Debug with: `openspec show [change] --json --deltas-only`\n\n### Validation Tips\n\n```bash\n# Always use strict mode for comprehensive checks\nopenspec validate [change] --strict --no-interactive\n\n# Debug delta parsing\nopenspec show [change] --json | jq '.deltas'\n\n# Check specific requirement\nopenspec show [spec] --json -r 1\n```\n\n## Happy Path Script\n\n```bash\n# 1) Explore current state\nopenspec spec list --long\nopenspec list\n# Optional full-text search:\n# rg -n \"Requirement:|Scenario:\" openspec/specs\n# rg -n \"^#|Requirement:\" openspec/changes\n\n# 2) Choose change id and scaffold\nCHANGE=add-two-factor-auth\nmkdir -p openspec/changes/$CHANGE/{specs/auth}\nprintf \"## Why\\n...\\n\\n## What Changes\\n- ...\\n\\n## Impact\\n- ...\\n\" > openspec/changes/$CHANGE/proposal.md\nprintf \"## 1. Implementation\\n- [ ] 1.1 ...\\n\" > openspec/changes/$CHANGE/tasks.md\n\n# 3) Add deltas (example)\ncat > openspec/changes/$CHANGE/specs/auth/spec.md << 'EOF'\n## ADDED Requirements\n### Requirement: Two-Factor Authentication\nUsers MUST provide a second factor during login.\n\n#### Scenario: OTP required\n- **WHEN** valid credentials are provided\n- **THEN** an OTP challenge is required\nEOF\n\n# 4) Validate\nopenspec validate $CHANGE --strict --no-interactive\n```\n\n## Multi-Capability Example\n\n```\nopenspec/changes/add-2fa-notify/\n├── proposal.md\n├── tasks.md\n└── specs/\n    ├── auth/\n    │   └── spec.md   # ADDED: Two-Factor Authentication\n    └── notifications/\n        └── spec.md   # ADDED: OTP email notification\n```\n\nauth/spec.md\n```markdown\n## ADDED Requirements\n### Requirement: Two-Factor Authentication\n...\n```\n\nnotifications/spec.md\n```markdown\n## ADDED Requirements\n### Requirement: OTP Email Notification\n...\n```\n\n## Best Practices\n\n### Simplicity First\n- Default to <100 lines of new code\n- Single-file implementations until proven insufficient\n- Avoid frameworks without clear justification\n- Choose boring, proven patterns\n\n### Complexity Triggers\nOnly add complexity with:\n- Performance data showing current solution too slow\n- Concrete scale requirements (>1000 users, >100MB data)\n- Multiple proven use cases requiring abstraction\n\n### Clear References\n- Use `file.ts:42` format for code locations\n- Reference specs as `specs/auth/spec.md`\n- Link related changes and PRs\n\n### Capability Naming\n- Use verb-noun: `user-auth`, `payment-capture`\n- Single purpose per capability\n- 10-minute understandability rule\n- Split if description needs \"AND\"\n\n### Change ID Naming\n- Use kebab-case, short and descriptive: `add-two-factor-auth`\n- Prefer verb-led prefixes: `add-`, `update-`, `remove-`, `refactor-`\n- Ensure uniqueness; if taken, append `-2`, `-3`, etc.\n\n## Tool Selection Guide\n\n| Task | Tool | Why |\n|------|------|-----|\n| Find files by pattern | Glob | Fast pattern matching |\n| Search code content | Grep | Optimized regex search |\n| Read specific files | Read | Direct file access |\n| Explore unknown scope | Task | Multi-step investigation |\n\n## Error Recovery\n\n### Change Conflicts\n1. Run `openspec list` to see active changes\n2. Check for overlapping specs\n3. Coordinate with change owners\n4. Consider combining proposals\n\n### Validation Failures\n1. Run with `--strict` flag\n2. Check JSON output for details\n3. Verify spec file format\n4. Ensure scenarios properly formatted\n\n### Missing Context\n1. Read project.md first\n2. Check related specs\n3. Review recent archives\n4. Ask for clarification\n\n## Quick Reference\n\n### Stage Indicators\n- `changes/` - Proposed, not yet built\n- `specs/` - Built and deployed\n- `archive/` - Completed changes\n\n### File Purposes\n- `proposal.md` - Why and what\n- `tasks.md` - Implementation steps\n- `design.md` - Technical decisions\n- `spec.md` - Requirements and behavior\n\n### CLI Essentials\n```bash\nopenspec list              # What's in progress?\nopenspec show [item]       # View details\nopenspec validate --strict --no-interactive  # Is it correct?\nopenspec archive <change-id> [--yes|-y]  # Mark complete (add --yes for automation)\n```\n\nRemember: Specs are truth. Changes are proposals. Keep them in sync.\n\n<!-- File: .codex/skills/ui-ux-pro-max/SKILL.md (Tokens: ~2127 | Category: anthropic-skill) -->\n---\nname: ui-ux-pro-max\ndescription: \"UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 8 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient.\"\n---\n\n# UI/UX Pro Max - Design Intelligence\n\nSearchable database of UI styles, color palettes, font pairings, chart types, product recommendations, UX guidelines, and stack-specific best practices.\n\n## Prerequisites\n\nCheck if Python is installed:\n\n```bash\npython3 --version || python --version\n```\n\nIf Python is not installed, install it based on user's OS:\n\n**macOS:**\n```bash\nbrew install python3\n```\n\n**Ubuntu/Debian:**\n```bash\nsudo apt update && sudo apt install python3\n```\n\n**Windows:**\n```powershell\nwinget install Python.Python.3.12\n```\n\n---\n\n## How to Use This Skill\n\nWhen user requests UI/UX work (design, build, create, implement, review, fix, improve), follow this workflow:\n\n### Step 1: Analyze User Requirements\n\nExtract key information from user request:\n- **Product type**: SaaS, e-commerce, portfolio, dashboard, landing page, etc.\n- **Style keywords**: minimal, playful, professional, elegant, dark mode, etc.\n- **Industry**: healthcare, fintech, gaming, education, etc.\n- **Stack**: React, Vue, Next.js, or default to `html-tailwind`\n\n### Step 2: Search Relevant Domains\n\nUse `search.py` multiple times to gather comprehensive information. Search until you have enough context.\n\n```bash\npython3 .shared/ui-ux-pro-max/scripts/search.py \"<keyword>\" --domain <domain> [-n <max_results>]\n```\n\n**Recommended search order:**\n\n1. **Product** - Get style recommendations for product type\n2. **Style** - Get detailed style guide (colors, effects, frameworks)\n3. **Typography** - Get font pairings with Google Fonts imports\n4. **Color** - Get color palette (Primary, Secondary, CTA, Background, Text, Border)\n5. **Landing** - Get page structure (if landing page)\n6. **Chart** - Get chart recommendations (if dashboard/analytics)\n7. **UX** - Get best practices and anti-patterns\n8. **Stack** - Get stack-specific guidelines (default: html-tailwind)\n\n### Step 3: Stack Guidelines (Default: html-tailwind)\n\nIf user doesn't specify a stack, **default to `html-tailwind`**.\n\n```bash\npython3 .shared/ui-ux-pro-max/scripts/search.py \"<keyword>\" --stack html-tailwind\n```\n\nAvailable stacks: `html-tailwind`, `react`, `nextjs`, `vue`, `svelte`, `swiftui`, `react-native`, `flutter`\n\n---\n\n## Search Reference\n\n### Available Domains\n\n| Domain | Use For | Example Keywords |\n|--------|---------|------------------|\n| `product` | Product type recommendations | SaaS, e-commerce, portfolio, healthcare, beauty, service |\n| `style` | UI styles, colors, effects | glassmorphism, minimalism, dark mode, brutalism |\n| `typography` | Font pairings, Google Fonts | elegant, playful, professional, modern |\n| `color` | Color palettes by product type | saas, ecommerce, healthcare, beauty, fintech, service |\n| `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof |\n| `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie |\n| `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading |\n| `prompt` | AI prompts, CSS keywords | (style name) |\n\n### Available Stacks\n\n| Stack | Focus |\n|-------|-------|\n| `html-tailwind` | Tailwind utilities, responsive, a11y (DEFAULT) |\n| `react` | State, hooks, performance, patterns |\n| `nextjs` | SSR, routing, images, API routes |\n| `vue` | Composition API, Pinia, Vue Router |\n| `svelte` | Runes, stores, SvelteKit |\n| `swiftui` | Views, State, Navigation, Animation |\n| `react-native` | Components, Navigation, Lists |\n| `flutter` | Widgets, State, Layout, Theming |\n\n---\n\n## Example Workflow\n\n**User request:** \"Làm landing page cho dịch vụ chăm sóc da chuyên nghiệp\"\n\n**AI should:**\n\n```bash\n# 1. Search product type\npython3 .shared/ui-ux-pro-max/scripts/search.py \"beauty spa wellness service\" --domain product\n\n# 2. Search style (based on industry: beauty, elegant)\npython3 .shared/ui-ux-pro-max/scripts/search.py \"elegant minimal soft\" --domain style\n\n# 3. Search typography\npython3 .shared/ui-ux-pro-max/scripts/search.py \"elegant luxury\" --domain typography\n\n# 4. Search color palette\npython3 .shared/ui-ux-pro-max/scripts/search.py \"beauty spa wellness\" --domain color\n\n# 5. Search landing page structure\npython3 .shared/ui-ux-pro-max/scripts/search.py \"hero-centric social-proof\" --domain landing\n\n# 6. Search UX guidelines\npython3 .shared/ui-ux-pro-max/scripts/search.py \"animation\" --domain ux\npython3 .shared/ui-ux-pro-max/scripts/search.py \"accessibility\" --domain ux\n\n# 7. Search stack guidelines (default: html-tailwind)\npython3 .shared/ui-ux-pro-max/scripts/search.py \"layout responsive\" --stack html-tailwind\n```\n\n**Then:** Synthesize all search results and implement the design.\n\n---\n\n## Tips for Better Results\n\n1. **Be specific with keywords** - \"healthcare SaaS dashboard\" > \"app\"\n2. **Search multiple times** - Different keywords reveal different insights\n3. **Combine domains** - Style + Typography + Color = Complete design system\n4. **Always check UX** - Search \"animation\", \"z-index\", \"accessibility\" for common issues\n5. **Use stack flag** - Get implementation-specific best practices\n6. **Iterate** - If first search doesn't match, try different keywords\n7. **Split Into Multiple Files** - For better maintainability:\n   - Separate components into individual files (e.g., `Header.tsx`, `Footer.tsx`)\n   - Extract reusable styles into dedicated files\n   - Keep each file focused and under 200-300 lines\n\n---\n\n## Common Rules for Professional UI\n\nThese are frequently overlooked issues that make UI look unprofessional:\n\n### Icons & Visual Elements\n\n| Rule | Do | Don't |\n|------|----|----- |\n| **No emoji icons** | Use SVG icons (Heroicons, Lucide, Simple Icons) | Use emojis like 🎨 🚀 ⚙️ as UI icons |\n| **Stable hover states** | Use color/opacity transitions on hover | Use scale transforms that shift layout |\n| **Correct brand logos** | Research official SVG from Simple Icons | Guess or use incorrect logo paths |\n| **Consistent icon sizing** | Use fixed viewBox (24x24) with w-6 h-6 | Mix different icon sizes randomly |\n\n### Interaction & Cursor\n\n| Rule | Do | Don't |\n|------|----|----- |\n| **Cursor pointer** | Add `cursor-pointer` to all clickable/hoverable cards | Leave default cursor on interactive elements |\n| **Hover feedback** | Provide visual feedback (color, shadow, border) | No indication element is interactive |\n| **Smooth transitions** | Use `transition-colors duration-200` | Instant state changes or too slow (>500ms) |\n\n### Light/Dark Mode Contrast\n\n| Rule | Do | Don't |\n|------|----|----- |\n| **Glass card light mode** | Use `bg-white/80` or higher opacity | Use `bg-white/10` (too transparent) |\n| **Text contrast light** | Use `#0F172A` (slate-900) for text | Use `#94A3B8` (slate-400) for body text |\n| **Muted text light** | Use `#475569` (slate-600) minimum | Use gray-400 or lighter |\n| **Border visibility** | Use `border-gray-200` in light mode | Use `border-white/10` (invisible) |\n\n### Layout & Spacing\n\n| Rule | Do | Don't |\n|------|----|----- |\n| **Floating navbar** | Add `top-4 left-4 right-4` spacing | Stick navbar to `top-0 left-0 right-0` |\n| **Content padding** | Account for fixed navbar height | Let content hide behind fixed elements |\n| **Consistent max-width** | Use same `max-w-6xl` or `max-w-7xl` | Mix different container widths |\n\n---\n\n## Pre-Delivery Checklist\n\nBefore delivering UI code, verify these items:\n\n### Visual Quality\n- [ ] No emojis used as icons (use SVG instead)\n- [ ] All icons from consistent icon set (Heroicons/Lucide)\n\n<!-- File: packages/skill-midway/skills/midway/SKILL.md (Tokens: ~449 | Category: anthropic-skill) -->\n---\nname: midway\ndescription: Use when the task involves Midway framework docs, package selection, API lookup, version compatibility, MCP/HTTP/framework components, or migration guidance. Resolve the requested Midway version first, then query the local Midway skill bundle with `midway-skill` commands.\n---\n\n# Midway\n\nUse the local `@midwayjs/skill-midway` bundle before relying on memory.\n\n## Workflow\n\n1. Resolve the user's requested version first.\n   Run `npm exec midway-skill -- resolve-version <version>` or `pnpm exec midway-skill resolve-version <version>`.\n   If no version is given, use `current`.\n\n2. Query the right source for the question.\n   Use `npm exec midway-skill -- lookup-docs --query \"<topic>\"` or `pnpm exec midway-skill lookup-docs --query \"<topic>\"`.\n   Use `npm exec midway-skill -- lookup-api --symbol \"<symbol>\"` or `pnpm exec midway-skill lookup-api --symbol \"<symbol>\"` for current-major API lookups.\n   Use `npm exec midway-skill -- lookup-packages --query \"<package or feature>\"` or `pnpm exec midway-skill lookup-packages --query \"<package or feature>\"`.\n   Use `npm exec midway-skill -- lookup-changelog --package \"<package>\"` or `pnpm exec midway-skill lookup-changelog --package \"<package>\"` for release history.\n\n3. Cite what the bundle resolved.\n   Include the requested version, resolved version, and whether it was an exact match or major-version fallback.\n\n## Rules\n\n- Prefer bundle results over recollection.\n- Historical majors only guarantee `docs` and `changelog`.\n- If `resolve-version` reports `api: false`, do not claim exact historical API details.\n- When results are empty, say that the bundle did not contain a match instead of inventing one.\n- Prefer package names with the `@midwayjs/` scope in answers when known.\n\n## Output\n\n- State the resolved Midway version.\n- Summarize the matched docs/API/package/changelog result.\n- Keep links or source paths from bundle records when relevant.\n\n<!-- File: site/docs/extensions/alinode.md (Tokens: ~826 | Category: anthropic-skill) -->\n# Alinode\n\n## 准备工作\n\n需要接入的应用是要部署在独立的服务获取云环境，可以接入互联网服务。\n\n## 创建服务\n\n**第一步**\n\n登录阿里云，点击开通 [阿里云的 Node.js 性能平台](https://www.aliyun.com/product/nodejs) 的服务。\n\n**第二步**\n\n创建新应用，获取 APP ID 和 App Secret。\n<img src=\"https://cdn.nlark.com/yuque/0/2021/png/187105/1617267785895-dd0fb702-91c7-4b25-9c64-8a9358f2d254.png#align=left&display=inline&height=351&margin=%5Bobject%20Object%5D&name=image.png&originHeight=702&originWidth=1548&size=106487&status=done&style=none&width=774\" width=\"774\" />\n\n## 安装监控依赖\n\n**第一步**\n\n安装 Node.js 性能平台所需的组件\n\n```bash\n# 安装版本管理工具 tnvm，安装过程出错参考：https://github.com/aliyun-node/tnvm\n$ wget -O- https://raw.githubusercontent.com/aliyun-node/tnvm/master/install.sh | bash\n$ source ~/.bashrc\n\n# tnvm ls-remote alinode \t\t\t\t\t\t\t\t# 查看需要的版本\n$ tnvm install alinode-v6.5.0 \t\t\t\t\t# 安装需要的版本\n$ tnvm use alinode-v6.5.0 \t\t\t\t\t\t\t# 使用需要的版本\n\n$ npm install @alicloud/agenthub -g \t\t# 安装 agenthub\n```\n\n这里有三个部分\n\n- 1、安装 tnvm（alinode 源）\n- 2、使用 tnvm 安装 alinode（替代默认的 node）\n- 3、安装 alinode 需要的数据采集器\n\n安装完成后，可以检查一下，需要确保 `which node` 和 `which agenthub` 的路径中包括 `.tnvm` 即可。\n\n```bash\n$ which node\n/root/.tnvm/versions/alinode/v3.11.4/bin/node\n\n$ which agenthub\n/root/.tnvm/versions/alinode/v3.11.4/bin/agenthub\n```\n\n将 `创建新应用` 中获得的 `App ID` 和 `App Secret` 按如下所示保存为 `yourconfig.json`。比如放在项目根目录。\n\n```typescript\n{\n  \"appid\": \"****\",\n  \"secret\": \"****\",\n}\n```\n\n启动插件：\n\n```typescript\nagenthub start yourconfig.json\n```\n\n## 启动 node 服务\n\n在安装了服务器中，启动 Node 服务时，需要加入 ENABLE_NODE_LOG=YES 环境变量。\n\n比如：\n\n```bash\n$ NODE_ENV=production ENABLE_NODE_LOG=YES node bootstrap.js\n```\n\n## Docker 容器的方法\n\n关于 docker 容器的方法可以查看 [文档](https://help.aliyun.com/document_detail/66027.html?spm=a2c4g.11186623.6.580.261ba70feI6mWt)。\n\n## 其他\n\n更多内容可以查看阿里云 Node.js 性能平台的 [文档](https://help.aliyun.com/document_detail/60338.html?spm=a2c4g.11186623.6.548.599312e6IkGO9v)。\n\n<!-- File: site/docs/extensions/apollo.md (Tokens: ~2834 | Category: anthropic-skill) -->\n# Apollo GraphQL\n\nGraphQL 适合需要让前端按需查询字段、聚合多个后端数据源，或者希望用统一查询语言暴露 API 的应用。Midway 通过 `@midwayjs/apollo` 提供基于 Apollo Server 的 GraphQL HTTP 端点，并让 Resolver 直接使用 Midway 的依赖注入和请求上下文。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n## 安装依赖\n\nApollo 组件已经内置 Apollo Server、GraphQL、WebSocket 订阅等运行时依赖，不需要额外安装 `@apollo/server` 或 `graphql`。\n\n```bash\n$ npm i @midwayjs/apollo@4 --save\n```\n\n如果使用 Koa 或 Express，还需要安装对应 Web 框架组件：\n\n```bash\n$ npm i @midwayjs/koa@4 --save\n# 或者\n$ npm i @midwayjs/express@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/apollo\": \"^4.0.0\",\n    \"@midwayjs/koa\": \"^4.0.0\"\n  }\n}\n```\n\n## 开启组件\n\n在 `src/configuration.ts` 中导入 Apollo 组件和一个 Web 框架组件。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as apollo from '@midwayjs/apollo';\n\n@Configuration({\n  imports: [\n    koa,\n    apollo,\n  ],\n})\nexport class MainConfiguration {}\n```\n\n组件默认会把 GraphQL 端点挂载到 `/graphql`，默认支持 `GET` 和 `POST` 请求。\n\n## 最小示例\n\n在 `src/config/config.default.ts` 中配置 GraphQL Schema 和 Resolver。\n\n```typescript\nexport const apollo = {\n  path: '/graphql',\n  typeDefs: `\n    type Query {\n      hello: String!\n    }\n  `,\n  resolvers: {\n    Query: {\n      hello: () => 'hello midway',\n    },\n  },\n};\n```\n\n启动应用后，请求 `/graphql`。\n\n```bash\n$ curl -X POST http://127.0.0.1:7001/graphql \\\n  -H 'content-type: application/json' \\\n  -d '{\"query\":\"{ hello }\"}'\n```\n\n返回：\n\n```json\n{\n  \"data\": {\n    \"hello\": \"hello midway\"\n  }\n}\n```\n\n开发环境下，也可以直接在浏览器访问 `/graphql` 打开 GraphiQL 调试页面。\n\n## 使用 Schema 文件\n\n当 Schema 较多时，推荐把 SDL 放到 `.graphql` 文件中，再通过 `typePaths` 加载。\n\n```graphql\n# src/graphql/schema.graphql\ntype Query {\n  user(id: ID!): User\n}\n\ntype Mutation {\n  updateUserName(id: ID!, name: String!): User\n}\n\ntype User {\n  id: ID!\n  name: String!\n}\n```\n\n配置文件中使用 `typePaths`。\n\n```typescript\nexport const apollo = {\n  path: '/graphql',\n  typePaths: ['./graphql/**/*.graphql'],\n};\n```\n\n`typePaths` 中的相对路径会从应用的 `baseDir` 开始解析，既支持精确文件路径，也支持 glob 表达式。你也可以同时配置 `typeDefs` 和 `typePaths`，组件会在启动时合并。\n\n## Resolver Map\n\n你可以直接在配置里写 Apollo Resolver Map。\n\n```typescript\nimport { UserService } from '../service/user';\n\nexport const apollo = {\n  typeDefs: `\n    type Query {\n      userName(id: ID!): String\n    }\n\n    type Mutation {\n      updateUserName(id: ID!, name: String!): String\n    }\n  `,\n  resolvers: {\n    Query: {\n      userName: async (_parent, args, context) => {\n        const userService = await context.requestContext.getAsync(UserService);\n        const user = await userService.findById(args.id);\n        return user?.name;\n      },\n    },\n    Mutation: {\n      updateUserName: async (_parent, args, context) => {\n        const userService = await context.requestContext.getAsync(UserService);\n        const user = await userService.updateName(args.id, args.name);\n        return user.name;\n      },\n    },\n  },\n};\n```\n\nResolver 的第三个参数就是当前 Midway 请求上下文，不需要再从 `context.ctx` 解包。Koa 场景下，`context` 是 Midway 增强后的 Koa ctx；Express 场景下，`context` 是 Midway 增强后的 Express request/context。\n\nMidway 会在上下文上挂载 `requestContext`、`logger`、`getLogger()`、`setAttr()`、`getAttr()` 和 `getApp()`。\n\n## Resolver 类\n\n也可以使用 Resolver 类来获得更自然的依赖注入体验。相关装饰器可以直接从 `@midwayjs/apollo` 中引入。\n\n```typescript\nimport { Inject } from '@midwayjs/core';\nimport { Args, Context, Mutation, Query, Resolver } from '@midwayjs/apollo';\nimport { UserService } from '../service/user';\n\n@Resolver()\nexport class UserResolver {\n  @Inject()\n  userService: UserService;\n\n  @Query('userName')\n  async userName(@Args('id') id: string, @Context() context) {\n    context.logger.info('query user %s', id);\n    const user = await this.userService.findById(id);\n    return user?.name;\n  }\n\n  @Mutation('updateUserName')\n  async updateUserName(@Args('id') id: string, @Args('name') name: string) {\n    const user = await this.userService.updateName(id, name);\n    return user.name;\n  }\n}\n```\n\nResolver 类会由 Midway 容器托管，支持 `@Inject()` 和请求级依赖。组件会自动发现使用 `@Resolver()` 标记的类，也可以通过 `resolverClasses` 显式传入。\n\n```typescript\nimport { UserResolver } from '../resolver/user';\n\nexport const apollo = {\n  typePaths: ['./graphql/**/*.graphql'],\n  resolverClasses: [UserResolver],\n};\n```\n\n可用的 Resolver 装饰器包括：\n\n| 装饰器            | 说明                     |\n| ----------------- | ------------------------ |\n| `@Resolver()`     | 标记一个 Resolver 类     |\n| `@Query()`        | 标记 Query 字段          |\n| `@Mutation()`     | 标记 Mutation 字段       |\n| `@Subscription()` | 标记 Subscription 字段   |\n\n可用的参数装饰器包括：\n\n| 装饰器       | 说明                     |\n| ------------ | ------------------------ |\n| `@Parent()`  | 获取父级 Resolver 返回值 |\n| `@Args()`    | 获取 GraphQL 参数        |\n| `@Context()` | 获取 Midway 请求上下文   |\n| `@Info()`    | 获取 GraphQL resolve info |\n\n不传参数时会注入完整对象，传入字段名时只注入对应字段，例如 `@Args('id')`。\n\n## 扩展 Context\n\n可以通过 `contextFactory` 给每次 GraphQL 请求补充字段。\n\n```typescript\nexport const apollo = {\n  typeDefs,\n  resolvers,\n  contextFactory: async context => {\n    return {\n      currentUserId: context.headers['x-user-id'],\n    };\n  },\n};\n```\n\n随后在 Resolver 中读取：\n\n```typescript\nconst userId = context.currentUserId;\n```\n\n内置字段如 `requestContext`、`logger`、`getLogger`、`setAttr`、`getAttr` 和 `getApp` 会被保留，不建议覆盖。\n\n## GraphiQL\n\n开发环境默认开启 GraphiQL。浏览器访问 `/graphql` 时会显示 GraphiQL 页面，生产环境默认关闭。\n\n```typescript\nexport const apollo = {\n  graphiql: true,\n};\n```\n\n可以显式关闭：\n\n```typescript\nexport const apollo = {\n  graphiql: false,\n};\n```\n\n也可以定制页面标题和请求地址：\n\n```typescript\nexport const apollo = {\n  graphiql: {\n    title: 'My GraphQL Console',\n    endpoint: '/graphql',\n  },\n};\n```\n\n## 订阅\n\n组件支持 `graphql-ws` 协议的 GraphQL Subscription。默认关闭，需要在配置中开启。\n\n```typescript\nexport const apollo = {\n  path: '/graphql',\n  subscriptions: true,\n  typeDefs: `\n    type Query {\n      hello: String!\n    }\n\n    type Subscription {\n      counter: Int!\n    }\n  `,\n};\n```\n\nResolver 方法需要返回 `AsyncIterable`。\n\n```typescript\nimport { Resolver, Subscription } from '@midwayjs/apollo';\n\n@Resolver()\nexport class CounterResolver {\n  @Subscription('counter')\n  async *counter() {\n    yield {\n      counter: 1,\n    };\n  }\n}\n```\n\n开启后，WebSocket 地址默认和 HTTP GraphQL 地址一致，例如 `ws://127.0.0.1:7001/graphql`。也可以单独指定订阅路径。\n\n```typescript\nexport const apollo = {\n  path: '/graphql',\n  subscriptions: {\n    path: '/graphql',\n    connectionInitWaitTimeout: 3000,\n  },\n};\n```\n\n客户端需要使用 `graphql-ws` 协议连接。\n\n```typescript\nimport { createClient } from 'graphql-ws';\n\nconst client = createClient({\n  url: 'ws://127.0.0.1:7001/graphql',\n});\n\nclient.subscribe(\n  {\n    query: 'subscription { counter }',\n  },\n  {\n    next: data => {\n      console.log(data);\n    },\n    error: err => {\n      console.error(err);\n    },\n    complete: () => {},\n  }\n);\n```\n\n## HTTP 行为\n\n默认只处理 `GET` 和 `POST` 请求。其他方法请求 GraphQL 路径会返回 `405 Method Not Allowed`。\n\n```typescript\nexport const apollo = {\n  methods: ['POST'],\n};\n```\n\n如果修改了 GraphQL 路径，HTTP 和订阅默认都会使用新的路径。\n\n```typescript\nexport const apollo = {\n  path: '/api/graphql',\n};\n```\n\n## Apollo Server 配置\n\nApollo Server 专属配置放在 `apollo.apollo` 下，避免和通用 GraphQL 字段混在一起。\n\n```typescript\nexport const apollo = {\n  typeDefs,\n  resolvers,\n  apollo: {\n    introspection: true,\n    plugins: [],\n  },\n};\n```\n\n常见配置如下：\n\n| 配置项           | 说明                                      |\n| ---------------- | ----------------------------------------- |\n| `path`           | GraphQL HTTP 路径，默认为 `/graphql`      |\n| `methods`        | 允许的 HTTP 方法，默认为 `['GET', 'POST']` |\n| `typeDefs`       | 内联 GraphQL SDL                          |\n| `typePaths`      | `.graphql` 文件路径或 glob 表达式         |\n| `resolvers`      | Apollo Resolver Map                       |\n| `resolverClasses` | Midway Resolver 类列表                   |\n| `contextFactory` | 扩展每次请求的 GraphQL context            |\n| `graphiql`       | GraphiQL 开关或配置                       |\n| `subscriptions`  | GraphQL WebSocket 订阅开关或配置          |\n| `apollo`         | 透传给 Apollo Server 的配置               |\n\n## 生产建议\n\n生产环境建议显式关闭 GraphiQL，并根据安全要求决定是否开启 introspection。\n\n```typescript\nexport const apollo = {\n  graphiql: false,\n  apollo: {\n    introspection: false,\n  },\n};\n```\n\n如果需要文件上传，推荐先通过 GraphQL 获取上传凭证，再由客户端直接上传到 OSS、S3 等对象存储。GraphQL multipart upload 不作为组件默认能力开启。\n\n<!-- File: site/docs/extensions/axios.md (Tokens: ~2580 | Category: anthropic-skill) -->\n# HTTP 请求\n\n\n\n## 简单的 HTTP 请求\n\nMidway 内置了一个简单的 HTTP 请求客户端，无需引入三方包即可使用。\n\n默认 Get 请求，返回数据为 Buffer。\n\n内置的 Http 客户端只提供最简单的能力，仅满足大部分的前端接口数据获取，如需复杂的功能，比如文件上传等，请使用其他的客户端，如 fetch，axios，got 等。\n\n\n\n### 简单方法形式\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nconst result = await makeHttpRequest('http://127.1:7001/');\n\n// Buffer.isBuffer(result.data)  => true\n```\n\nGet 请求，带上 Query，返回类型为 JSON。\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nconst result = await makeHttpRequest('http://127.1:7001/', {\n  data: {\n    a: 1,\n    b: 2\n  },\n  dataType: 'json',\t// 返回的数据格式\n});\n\n// typeof result.data => 'object'\n// result.data.url => /?a=1&b=2\n```\n\n可以指定类型\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nconst result = await makeHttpRequest('http://127.1:7001/', {\n  method: 'GET',\n  dataType: 'json',\n});\n```\n\n返回 text 格式。\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nconst result = await makeHttpRequest('http://127.1:7001/', {\n  method: 'GET',\n  dataType: 'text',\n});\n```\n\nPOST 请求并返回 JSON。\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nconst result = await makeHttpRequest('http://127.1:7001/', {\n  method: 'POST',\n  data: {\n    a: 1,\n    b: 2\n  },\n  dataType: 'json',\n  contentType:'json', \t// 发送的 post 为 json\n});\n\n// result.data ...\n```\n\n:::caution\n注意，请不要在请求中直接返回 result 对象，result 对象是标准的 httpResponse，在大部分场景下无法被直接序列化，会抛出对象循环的错误。\n:::\n\n设置请求超时时间。\n\n```typescript\nimport { makeHttpRequest } from '@midwayjs/core';\n\nlet err;\n// 超时会报错，注意 catch\ntry {\n  const result = await makeHttpRequest('http://127.1:7001/', {\n    method: 'GET',\n    dataType: 'text',\n    timeout: 500,\n  });\n} catch (e) {\n  err = e;\n}\n```\n\n\n\n### 实例形式\n\n```typescript\nimport { HttpClient } from '@midwayjs/core';\n\nconst httpclient = new HttpClient();\nconst result = await httpclient.request('http://127.1:7001/');\n\n// Buffer.isBuffer(result.data)  => true\n```\n\n和方法形式参数相同。\n\n```typescript\nimport { HttpClient } from '@midwayjs/core';\n\nconst httpclient = new HttpClient();\nconst result = await httpclient.request('http://127.1:7001/', {\n  method: 'POST',\n  data: {\n    a: 1,\n    b: 2\n  },\n  dataType: 'json',\n  contentType:'json', \t// 发送的 post 为 json\n});\n\n// result.data ...\n```\n\n示例形式，可以复用创建出的对象，并且每次请求，都可以带上一些固定的参数，比如 header。\n\n```typescript\nimport { HttpClient } from '@midwayjs/core';\n\nconst httpclient = new HttpClient({\n  headers: {\n    'x-timeout': '5'\n  },\n  method: 'POST',\n  timeout: 2000\n});\n\n// 每次都会带上 headers\nconst result = await httpclient.request('http://127.1:7001/');\n\n```\n\n\n\n\n\n## Axios 支持\n\nMidway 包裹了 [axios](https://github.com/axios/axios) 包，使得在代码中可以简单的使用 axios 接口。\n\n和 axios 的一些关系如下：\n\n- 接口完全一致\n- 适配依赖注入写法，完整的类型定义\n- 方便实例管理和配置统一\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n\n### 安装依赖\n\n```bash\n$ npm i @midwayjs/axios@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/axios\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n### 引入组件\n\n\n首先，引入 组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as axios from '@midwayjs/axios';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    axios\t\t// 导入 axios 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n然后在业务代码中即可注入使用。\n\n\n\n### 使用默认 Axios 实例\n\n\n接口和 [axios](https://github.com/axios/axios) 一致。\n\n\n```typescript\naxios.request(config)\naxios.get(url[, config])\naxios.delete(url[, config])\naxios.head(url[, config])\naxios.options(url[, config])\naxios.post(url[, data[, config]])\naxios.put(url[, data[, config]])\naxios.patch(url[, data[, config]])\naxios.postForm(url[, data[, config]])\naxios.putForm(url[, data[, config]])\naxios.patchForm(url[, data[, config]])\n```\n\n\n使用示例：\n```typescript\nimport { HttpService } from '@midwayjs/axios';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  httpService: HttpService;\n\n  async invoke() {\n  \tconst url = 'https://midwayjs.org/resource/101010100.json';\n    const result = await this.httpService.get(url);\n    // TODO result\n  }\n}\n```\n\n\n\n### 配置默认 Axios 实例\n\n\nHttpService 实例等价于 `axios.create` ，所以可以有一些配置参数，这些参数了 axios 本身的参数相同，我们可以在 `src/config.default.ts` 中配置它。\n\n\n比如：\n```typescript\nexport default {\n  // ...\n  axios: {\n    default: {\n      // 所有实例复用的配置\n    },\n    clients: {\n      // 默认实例的配置\n      default: {\n        baseURL: 'https://api.example.com',\n        // `headers` are custom headers to be sent\n        headers: {\n          'X-Requested-With': 'XMLHttpRequest'\n        },\n        timeout: 1000, // default is `0` (no timeout)\n\n        // `withCredentials` indicates whether or not cross-site Access-Control requests\n        // should be made using credentials\n        withCredentials: false, // default\n      },\n    }\n  }\n}\n```\n更多的参数可以参考 [axios global config](https://github.com/axios/axios#config-defaults)。\n\n\n\n### 创建不同实例\n\n和其他的服务多实例相同，配置不同的 key 即可。\n\n```typescript\nexport default {\n  // ...\n  axios: {\n    default: {\n      // 所有实例复用的配置\n    },\n    clients: {\n      default: {\n        // 默认实例\n      },\n      customAxios: {\n        // 自定义实例\n      }\n    }\n  }\n}\n```\n\n使用方式如下：\n\n```typescript\nimport { HttpServiceFactory, HttpService } from '@midwayjs/axios';\nimport { InjectClient } from '@midwayjs/core';\n\n@Provide()\nexport class UserService {\n\n  @InjectClient(HttpServiceFactory, 'customAxios')\n  customAxios: HttpService;\n\n  async invoke() {\n  \tconst url = 'https://midwayjs.org/resource/101010100.json';\n    const result = await this.customAxios.get(url);\n    // TODO result\n  }\n}\n```\n\n\n\n### 配置全局拦截器\n\n如果使用的是默认的 Axios 实例，可以如下配置。\n\n```javascript\nimport { Configuration, IMidwayContainer } from '@midwayjs/core';\nimport * as axios from '@midwayjs/axios';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    axios\t\t// 导入 axios 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n\n  async onReady(container: IMidwayContainer) {\n  \tconst httpService = await container.getAsync(axios.HttpService);\n    httpService.interceptors.request.use(\n      config => {\n        // Do something before request is sent\n        return config;\n      },\n      error => {\n        // Do something with request error\n        return Promise.reject(error);\n      }\n    );\n  }\n}\n```\n\n如果要给其他实例配置，可以参考下面的代码。\n\n```typescript\nimport { Configuration, IMidwayContainer } from '@midwayjs/core';\nimport * as axios from '@midwayjs/axios';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    axios\t\t// 导入 axios 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n\n  async onReady(container: IMidwayContainer) {\n  \tconst httpServiceFactory = await container.getAsync(axios.HttpServiceFactory);\n    const customAxios = httpServiceFactory.get('customAxios');\n    customAxios.interceptors.request.use(\n      config => {\n        //...\n      },\n      error => {\n        //...\n      }\n    );\n  }\n}\n```\n\n### 扩展 AxiosRequestConfig 类型\n\n当前 Axios 库中 [issue](https://github.com/axios/axios/issues?q=is%3Aissue%20state%3Aopen%20AxiosRequestConfig) 有不少扩展 `AxiosRequestConfig` 类型的需求，但是 axios 本身并不支持扩展这个类型，其中的泛型仅限于设置 data 属性的类型。\n\nMidway 中提供了 `AxiosRequestConfig` 的扩展，可以在组件层面增加新的属性。\n\n```typescript\n// interface.ts\nimport '@midwayjs/axios';\n\ndeclare module '@midwayjs/axios/dist/interface' {\n  interface AxiosRequestConfig {\n    retryDelay?: number;\n    retry?: number;\n  }\n}\n```\n\n\n### 直接使用 Axios\n\n`@midayjs/axios`导出了原始的`axios`实例，在非应用环境中可以直接使用。\n\n```typescript\nimport { Axios } from '@midwayjs/axios';\nimport { ReadStream, createWriteStream } from 'fs';\nimport { finished } from 'stream/promises';\n\nasync function download(url: string, filename: string) {\n  const writer = await createWriteStream(filename);\n  const res = Axios.get<ReadStream>(url, {\n    responseType: 'stream',\n  });\n  res.data.pipe(writer);\n  await finished(writer);\n  return res;\n}\n```\n\n<!-- File: site/docs/extensions/bull.md (Tokens: ~6666 | Category: anthropic-skill) -->\n# 任务队列\n\n队列是一种强大的设计模式，可帮助您应对常见的应用程序扩展和性能挑战。队列可以帮助您解决的一些问题。\n\n示例如下：\n\n-  平滑处理峰值。可以在任意时间启动资源密集型任务，然后将这些任务添加到队列中，而不是同步执行。让任务进程以受控方式从队列中提取任务。也可以轻松添加新的队列消费者以扩展后端任务处理。\n- 分解可能会阻塞 Node.js 事件循环的单一任务。比如用户请求需要像音频转码这样的 CPU 密集型工作，就可以将此任务委托给其他进程，从而释放面向用户的进程以保持响应。\n- 提供跨各种服务的可靠通信渠道。例如，您可以在一个进程或服务中排队任务（作业），并在另一个进程或服务中使用它们。在任何流程或服务的作业生命周期中完成、错误或其他状态更改时，您都可以收到通知（通过监听状态事件）。当队列生产者或消费者失败时，它们的状态被保留，并且当节点重新启动时任务处理可以自动重新启动。\n\nMidway 提供了 @midwayjs/bull 包作为 [Bull](https://github.com/OptimalBits/bull) 之上的抽象/包装器，[Bull](https://github.com/OptimalBits/bull) 是一种流行的、受良好支持的、高性能的基于 Node.js 的队列系统实现。该软件包可以轻松地将 Bull Queues 以友好的方式集成到您的应用程序中。\n\nBull 使用 Redis 来保存作业数据，在使用 Redis 时，Queue 架构是完全分布式，和平台无关。例如，您可以在一个（或多个）节点（进程）中运行一些 Queue 生产者、消费者，而在其他节点上的运行其他生产者和消费者。\n\n本章介绍 @midwayjs/bull 包。我们还建议阅读 [Bull 文档](https://github.com/OptimalBits/bull/blob/master/REFERENCE.md) 以了解更多背景和具体实施细节。\n\n:::tip\n\n- 1、从 v3.6.0 开始，原有任务调度 `@midwayjs/task` 模块废弃，如果查询历史文档，请参考 [这里](../legacy/task)。\n- 2、bull 是一个分布式任务管理系统，必须依赖 redis\n\n:::\n\n\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n\n\n## 安装组件\n\n```bash\n$ npm i @midwayjs/bull@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/bull\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 使用组件\n\n将 bull 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as bull from '@midwayjs/bull';\n\n@Configuration({\n  imports: [\n    // ...\n    bull\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n\n\n## 一些概念\n\nBull 将整个队列分为三个部分\n\n- 1、Queue 队列，管理任务\n- 2、Job，每个任务对象，可以对任务进行启停控制\n- 3、Processor，任务处理，实际的逻辑执行部分\n\n\n\n## 基础配置\n\nbull 是一个分布式任务管理器，强依赖于 redis，在 `config.default.ts` 文件中配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  bull: {\n    // 默认的队列配置\n    defaultQueueOptions: {\n      redis: `redis://127.0.0.1:32768`,\n    }\n  },\n}\n```\n\n有账号密码情况：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  bull: {\n    defaultQueueOptions: {\n      redis: {\n        port: 6379,\n        host: '127.0.0.1',\n        password: 'foobared',\n      },\n    }\n  },\n}\n```\n\n所有的队列都会复用该配置。\n\n\n\n## 编写任务处理器\n\n使用 `@Processor` 装饰器装饰一个类，用于快速定义一个任务处理器（这里我们不使用 Job，避免后续的歧义）。\n\n`@Processor` 装饰器需要传递一个 Queue （队列）的名字，在框架启动时，如果没有名为 `test` 的队列，则会自动创建。\n\n比如，我们在 `src/queue/test.queue.ts` 文件中编写如下代码。\n\n```typescript\n// src/queue/test.queue.ts\nimport { Processor, IProcessor } from '@midwayjs/bull';\n\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  async execute() {\n    // ...\n  }\n}\n```\n\n在启动时，框架会自动查找并初始化上述处理器代码，同时自动创建一个名为 `test` 的 Queue。\n\n\n\n\n\n## 执行任务\n\n当定义完 Processor 之后，由于并未指定 Processor 如何执行，我们还需要手动执行它。\n\n通过获取对应的队列，我们可以很方便的来执行任务。\n\n\n\n### 手动执行任务\n\n比如，我们可以在项目启动后执行。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bull from '@midwayjs/bull';\n\n@Configuration({\n  imports: [\n    // ...\n    bull\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullFramework: bull.Framework;\n\n  //...\n\n  async onServerReady() {\n    // 获取 Processor 相关的队列\n    const testQueue = this.bullFramework.getQueue('test');\n    // 立即添加这个任务\n    await testQueue?.addJobToQueue();\n  }\n}\n```\n\n\n\n### 增加执行参数\n\n我们也可以在执行时，附加一些默认参数。\n\n```typescript\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  async execute(params) {\n    // params.aaa => 1\n  }\n}\n\n\n// invoke\nconst testQueue = this.bullFramework.getQueue('test');\n// 立即添加这个任务\nawait testQueue?.addJobToQueue({\n  aaa: 1,\n  bbb: 2,\n});\n```\n\n\n\n### 任务状态和管理\n\n执行 `addJobToQueue` 后，我们可以获取到一个 `Job` 对象。\n\n```typescript\n// invoke\nconst testQueue = this.bullFramework.getQueue('test');\nconst job = await testQueue?.addJobToQueue();\n```\n\n通过这个 Job 对象，我们可以做进度管理。\n\n```typescript\n// 更新进度\nawait job.progress(60);\n// 获取进度\nconst progress = await job.process();\n// => 60\n```\n\n获取任务状态。\n\n```typescript\nconst state = await job.getState();\n// state => 'delayed' 延迟状态\n// state => 'completed' 完成状态\n```\n\n更多的 Job API，请查看 [文档](https://github.com/OptimalBits/bull/blob/develop/REFERENCE.md)。\n\n\n\n### 延迟执行\n\n执行任务时，也有一些额外的选项。\n\n比如，延迟 1s 执行。\n\n```typescript\nconst testQueue = this.bullFramework.getQueue('test');\n// 立即添加这个任务\nawait testQueue?.addJobToQueue({}, { delay: 1000 });\n```\n\n\n\n### 中间件和错误处理\n\nBull 组件包含可以独立启动的 Framework，有着自己的 App 对象和 Context 结构。\n\n我们可以对 bull 的 App 配置独立的中间件和错误过滤器。\n\n```typescript\n@Configuration({\n  imports: [\n    // ...\n    bull\n  ]\n})\nexport class MainConfiguration {\n\n  @App('bull')\n  bullApp: bull.Application;\n\n  //...\n\n  async onReady() {\n    this.bullApp.useMiddleare( /*中间件*/);\n    this.bullApp.useFilter( /*过滤器*/);\n  }\n}\n```\n\n\n\n### 上下文\n\n任务处理器执行是在请求作用域中，其有着特殊的 Context 对象结构。\n\n```typescript\nexport interface Context extends IMidwayContext {\n  jobId: JobId;\n  job: Job,\n  from: new (...args) => IProcessor;\n}\n```\n\n我们可以直接从 ctx 中访问当前的 Job 对象。\n\n```typescript\n// src/queue/test.queue.ts\nimport { Processor, IProcessor, Context } from '@midwayjs/bull';\n\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n\n  @Inject()\n  ctx: Context;\n\n  async execute() {\n    // ctx.jobId => xxxx\n  }\n}\n```\n\n\n\n### 更多任务选项\n\n除了上面的 delay 之外，还有更多的执行选项。\n\n| 选项             | 类型                  | 描述                                                         |\n| ---------------- | --------------------- | ------------------------------------------------------------ |\n| priority         | number                | 可选的优先级值。范围从 1（最高优先级）到 MAX_INT（最低优先级）。请注意，使用优先级对性能有轻微影响，因此请谨慎使用。 |\n| delay            | number                | 等待可以处理此作业的时间量（毫秒）。请注意，为了获得准确的延迟，服务器和客户端都应该同步它们的时钟。 |\n| attempts         | number                | 在任务完成之前尝试尝试的总次数。                             |\n| repeat           | RepeatOpts            | 根据 cron 规范的重复任务配置，更多可以查看 [RepeatOpts](https://github.com/OptimalBits/bull/blob/master/REFERENCE.md#queueadd)，以及下面的重复任务介绍。 |\n| backoff          | number \\| BackoffOpts | 任务失败时自动重试的回退设置。请参阅 [BackoffOpts](https://github.com/OptimalBits/bull/blob/master/REFERENCE.md#queueadd)。 |\n| lifo             | boolean               | 如果为 true，则将任务添加到队列的右端而不是左端（默认为 false）。 |\n| timeout          | number                | 任务因超时错误而失败的毫秒数。                               |\n| jobId            | number \\| string      | 覆盖任务 id - 默认情况下，任务 id 是唯一整数，但您可以使用此设置覆盖它。如果您使用此选项，则由您来确保 jobId 是唯一的。如果您尝试添加一个 id 已经存在的任务，它将不会被添加。 |\n| removeOnComplete | boolean \\| number     | 如果为 true，则在成功完成后删除任务。如果设置数字，则为指定要保留的任务数量。默认行为是任务信息保留在已完成列表中。 |\n| removeOnFail     | boolean \\| number     | 如果为 true，则在所有尝试后都失败时删除任务。如果设置数字，指定要保留的任务数量。默认行为是将任务信息保留在失败列表中。 |\n| stackTraceLimit  | number                | 限制将在堆栈跟踪中记录的堆栈跟踪行的数量。                   |\n\n\n\n## 重复执行的任务\n\n除了手动执行的方式，我们也可以通过 `@Processor` 装饰器的参数，快速配置任务的重复执行。\n\n```typescript\nimport { Processor, IProcessor } from '@midwayjs/bull';\nimport { FORMAT } from '@midwayjs/core';\n\n@Processor('test', {\n  repeat: {\n    cron: FORMAT.CRONTAB.EVERY_PER_5_SECOND\n  }\n})\nexport class TestProcessor implements IProcessor {\n  @Inject()\n  logger;\n\n  async execute() {\n    // ...\n  }\n}\n```\n\n\n\n## 常用 Cron 表达式\n\n关于 Cron 表达式，格式如下。\n\n```typescript\n*    *    *    *    *    *\n┬    ┬    ┬    ┬    ┬    ┬\n│    │    │    │    │    |\n│    │    │    │    │    └ day of week (0 - 7) (0 or 7 is Sun)\n│    │    │    │    └───── month (1 - 12)\n│    │    │    └────────── day of month (1 - 31)\n│    │    └─────────────── hour (0 - 23)\n│    └──────────────────── minute (0 - 59)\n└───────────────────────── second (0 - 59, optional)\n```\n\n\n\n常见表达式：\n\n- 每隔5秒执行一次：`*/5 * * * * *`\n- 每隔1分钟执行一次：`0 */1 * * * *`\n- 每小时的20分执行一次：`0 20 * * * *`\n- 每天 0 点执行一次：`0 0 0 * * *`\n- 每天的两点35分执行一次：`0 35 2 * * *`\n\n可以使用 [在线工具](https://cron.qqe2.com/) 执行确认下一次执行的时间。\n\nMidway 在框架侧提供了一些常用的表达式，放在 `@midwayjs/core` 中供大家使用。\n\n```typescript\nimport { FORMAT } from '@midwayjs/core';\n\n// 每分钟执行的 cron 表达式\nFORMAT.CRONTAB.EVERY_MINUTE\n```\n\n\n\n内置的还有一些其他的表达式。\n\n| 表达式                         | 对应时间        |\n| ------------------------------ | --------------- |\n| CRONTAB.EVERY_SECOND           | 每秒钟          |\n| CRONTAB.EVERY_MINUTE           | 每分钟          |\n| CRONTAB.EVERY_HOUR             | 每小时整点      |\n| CRONTAB.EVERY_DAY              | 每天 0 点       |\n| CRONTAB.EVERY_DAY_ZERO_FIFTEEN | 每天 0 点 15 分 |\n| CRONTAB.EVERY_DAY_ONE_FIFTEEN  | 每天 1 点 15 分 |\n| CRONTAB.EVERY_PER_5_SECOND     | 每隔 5 秒       |\n| CRONTAB.EVERY_PER_10_SECOND    | 每隔 10 秒      |\n| CRONTAB.EVERY_PER_30_SECOND    | 每隔 30 秒      |\n| CRONTAB.EVERY_PER_5_MINUTE     | 每隔 5 分钟     |\n| CRONTAB.EVERY_PER_10_MINUTE    | 每隔 10 分钟    |\n| CRONTAB.EVERY_PER_30_MINUTE    | 每隔 30 分钟    |\n\n\n\n## 高级配置\n\n\n\n### 清理之前的任务\n\n在默认情况下，框架会自动清理前一次未调度的 **重复执行任务**，保持每一次的重复执行的任务队列为最新。如果在某些环境不需要清理，可以单独关闭。\n\n比如你不需要清理重复：\n\n```typescript\n// src/config/config.prod.ts\nexport default {\n  // ...\n  bull: {\n    clearRepeatJobWhenStart: false,\n  },\n}\n```\n\n:::tip\n\n如果不清理，如果前一次队列为 10s 执行，现在修改为 20s 执行，则两个定时都会存储在 Redis 中，导致代码重复执行。\n\n在日常的开发中，如果不清理，很容易出现代码重复执行这个问题。但是在集群部署的场景，多台服务器轮流重启的情况下，可能会导致定时任务被意外清理，请评估开关的时机。\n\n:::\n\n\n\n也可以在启动时手动清理所有任务。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, App, Inject } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport { join } from 'path';\nimport * as bull from '@midwayjs/bull';\n\n@Configuration({\n  imports: [koa, bull],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: koa.Application;\n\n  @Inject()\n  bullFramework: bull.Framework;\n\n  async onReady() {\n    // 在这个阶段，装饰器队列还未创建，使用 API 提前手动创建队列，装饰器会复用同名队列\n    const queue = this.bullFramework.createQueue('user');\n    // 通过队列手动执行清理\n    await queue.obliterate({ force: true });\n  }\n}\n```\n\n\n\n\n\n### 清理任务历史记录\n\n当开启 Redis 后，默认情况下，bull 会记录所有的成功和失败的任务 key，这可能会导致 redis 的 key 暴涨，我们可以配置成功或者失败后清理的选项。\n\n默认情况下\n\n- 成功时保留的任务记录为 3 条\n- 失败保留的任务记录为 10 条\n\n也可以通过参数进行配置。\n\n比如在装饰器配置。\n\n```typescript\nimport { FORMAT } from '@midwayjs/core';\nimport { IProcessor, Processor } from '@midwayjs/bull';\n\n@Processor('user', {\n  repeat: {\n    cron: FORMAT.CRONTAB.EVERY_MINUTE,\n  },\n  removeOnComplete: 3,\t// 成功后移除任务记录，最多保留最近 3 条记录\n  removeOnFail: 10,\t    // 失败后移除任务记录\n})\nexport class UserService implements IProcessor {\n  execute(data: any) {\n    // ...\n  }\n}\n\n```\n\n也可以在全局 config 中配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  bull: {\n    defaultQueueOptions: {\n      // 默认的任务配置\n      defaultJobOptions: {\n        // 保留 10 条记录\n        removeOnComplete: 10,\n      },\n    },\n  },\n}\n```\n\n\n\n\n\n### Redis 集群\n\n可以使用 bull 提供的 `createClient` 方式来接入自定义的 redis 实例，这样你可以接入 Redis 集群。\n\n比如：\n\n```typescript\n// src/config/config.default\nimport Redis from 'ioredis';\n\nconst clusterOptions = {\n  enableReadyCheck: false,  // 一定要是false\n  retryDelayOnClusterDown: 300,\n  retryDelayOnFailover: 1000,\n  retryDelayOnTryAgain: 3000,\n  slotsRefreshTimeout: 10000,\n  maxRetriesPerRequest: null  // 一定要是null\n}\n\nconst redisClientInstance = new Redis.Cluster([\n  {\n    port: 7000,\n    host: '127.0.0.1'\n  },\n  {\n    port: 7002,\n    host: '127.0.0.1'\n  },\n], clusterOptions);\n\nexport default {\n  bull: {\n    defaultQueueOptions: {\n      createClient: (type, opts) => {\n        return redisClientInstance;\n      },\n      // 这些任务存储的 key，都是相同开头，以便区分用户原有 redis 里面的配置\n      prefix: '{midway-bull}',\n    },\n  }\n}\n```\n\n\n\n## 队列管理\n\n队列是廉价的，每个 Job 都会绑定一个队列，在一些情况下，我们也可以手动对队列进行管理操作。\n\n\n\n### 手动创建队列\n\n除了使用 `@Processor` 简单定义队列，我们还可以使用 API 进行创建。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bull from '@midwayjs/bull';\n\n@Configuration({\n  imports: [\n    // ...\n    bull\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullFramework: bull.Framework;\n\n  async onReady() {\n    const testQueue = this.bullFramework.createQueue('test', {\n      redis: {\n        port: 6379,\n        host: '127.0.0.1',\n        password: 'foobared',\n      },\n      prefix: '{midway-bull}',\n    });\n\n    // ...\n  }\n}\n```\n\n通过 `createQueue` 手动创建队列后，队列依旧会自动保存。如果在启动时 `@Processor` 使用了该队列名，则会自动使用已经创建好的队列。\n\n比如：\n\n```typescript\n// 会自动使用上面手动创建的同名队列\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  async execute(params) {\n  }\n}\n```\n\n\n\n### 获取队列\n\n我们可以简单的根据队列名获取队列。\n\n```typescript\n const testQueue = bullFramework.getQueue('test');\n```\n\n也可以通过装饰器来获取。\n\n```typescript\nimport { InjectQueue, BullQueue } from '@midwayjs/bull';\nimport { Provide } from '@midwayjs/core';\n\n@Provide()\nexport class UserService {\n  @InjectQueue('test')\n  testQueue: BullQueue;\n\n  async invoke() {\n    await this.testQueue.pause();\n    // ...\n  }\n}\n```\n\n\n\n### 队列常用操作\n\n暂停队列。\n\n```typescript\nawait testQueue.pause();\n```\n\n继续队列。\n\n```typescript\nawait testQueue.resume();\n```\n\n队列事件。\n\n```typescript\n// Local events pass the job instance...\ntestQueue.on('progress', function (job, progress) {\n  console.log(`Job ${job.id} is ${progress * 100}% ready!`);\n});\n\ntestQueue.on('completed', function (job, result) {\n  console.log(`Job ${job.id} completed! Result: ${result}`);\n  job.remove();\n});\n```\n\n完整队列 API 请参考 [这里](https://github.com/OptimalBits/bull/blob/develop/REFERENCE.md)。\n\n\n\n## 组件日志\n\n组件有着自己的日志，默认会将 `ctx.logger` 记录在 `midway-bull.log` 中。\n\n我们可以单独配置这个 logger 对象。\n\n```typescript\nexport default {\n  midwayLogger: {\n    clients: {\n      // ...\n      bullLogger: {\n        fileLogName: 'midway-bull.log',\n      },\n    },\n  },\n}\n```\n\n这个日志的输出格式，我们也可以单独配置。\n\n```typescript\nexport default {\n  bull: {\n    // ...\n    contextLoggerFormat: info => {\n      const { jobId, from } = info.ctx;\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} [${jobId} ${from.name}] ${info.message}`;\n    },\n  }\n}\n```\n\n\n\n## 关于 Redis 版本\n\n请尽可能选择最新的版本（ >=5 ），目前在低版本 redis 上有发现定时任务创建失败的问题。\n\n\n\n## Bull UI\n\n在分布式场景中，我们可以资利用 Bull UI 来简化管理。\n\n和 bull 组件类似，需要独立安装和启用。\n\n```bash\n$ npm i @midwayjs/bull-board@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/bull-board\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n将 bull-board 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as bull from '@midwayjs/bull';\nimport * as bullBoard from '@midwayjs/bull-board';\n\n@Configuration({\n  imports: [\n    // ...\n    bull,\n    bullBoard,\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n默认的访问路径为：`http://127.1:7001/ui`。\n\n效果如下：\n\n![](https://img.alicdn.com/imgextra/i2/O1CN01j4wEFb1UacPxA06gs_!!6000000002534-2-tps-1932-1136.png)\n\n可以通过配置进行基础路径的修改。\n\n```typescript\n// src/config/config.prod.ts\nexport default {\n  // ...\n  bullBoard: {\n    basePath: '/ui',\n  },\n}\n```\n\n此外，组件提供了 `BullBoardManager` ，可以添加动态创建的队列。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bull from '@midwayjs/bull';\nimport * as bullBoard from '@midwayjs/bull-board';\n\n@Configuration({\n  imports: [\n    // ...\n    bull,\n    bullBoard\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullFramework: bull.Framework;\n  \n  @Inject()\n  bullBoardManager: bullBoard.BullBoardManager;\n\n  async onServerReady() {\n    const testQueue = this.bullFramework.createQueue('test', {\n      // ...\n    });\n\n    this.bullBoardManager.addQueue(new bullBoard.BullAdapter(testQueue);\n  }\n}\n```\n\n\n\n\n\n## 常见问题\n\n### 1、EVALSHA 错误\n\n![image.png](https://img.alicdn.com/imgextra/i4/O1CN01KfjCKT1yypmNPDkIL_!!6000000006648-2-tps-3540-102.png)\n\n这个问题基本明确，问题会出现在 redis 的集群版本上。\n\n原因是 redis 会对 key 做 hash 来确定存储的 slot，集群下这一步 @midwayjs/bull 的 key 命中了不同的 slot。\n\n解决方案： task 里的 prefix 配置用 {} 包括，强制 redis 只计算 {} 里的hash，例如 `prefix: '{midway-task}'`。\n\n### 2、EVAL inside MULTI is not allowed 错误\n\n表现为 `queue.createBulk()`、`job.moveToFailed()` 等任务队列 API 调用无效，并出现下面的错误。\n\n```\nReplyError: EXECABORT Transaction discarded because of previous errors.\n    at parseError (<project_dir>/node_modules/redis-parser/lib/parser.js:179:12)\n    at parseType (<project_dir>/node_modules/redis-parser/lib/parser.js:302:14) {\n  command: { name: 'exec', args: [] },\n  previousErrors: [\n    ReplyError: ERR 'EVAL' inside MULTI is not allowed\n        at parseError (<project_dir>/node_modules/redis-parser/lib/parser.js:179:12)\n        at parseType (<project_dir>/node_modules/redis-parser/lib/parser.js:302:14) {\n      command: [Object]\n    }\n  ]\n}\n```\n\n:::tip\n\n常出现于使用阿里云 Redis 服务。\n\n:::\n\n由于这些 API 依赖的 Redis Lua 脚本中使用了 EVAL 或者 EVALSHA，阿里云 Redis 使用代理模式连接时，会对 Lua 脚本调用做额外限制，包括 [不允许在 MULTI 事务中执行 EVAL 命令](https://help.aliyun.com/zh/redis/support/usage-of-lua-scripts?#section-8f7-qgv-dlv)，文档中还提到可以通过参数配置 script_check_enable 关闭这一校验，但是验证无效。\n\n解决方案：\n\n* 1、在阿里云控制台操作开启直连地址，将服务切换到直连模式\n* 2、客户端切换成集群模式，参考上述「Redis 集群」章节，切换配置方式\n\n<!-- File: site/docs/extensions/bullmq.md (Tokens: ~4216 | Category: anthropic-skill) -->\n# 任务队列\n\n队列是一种强大的设计模式，可帮助您应对常见的应用程序扩展和性能挑战。队列可以帮助您解决的一些问题：\n\n- 平滑处理峰值。可以在任意时间启动资源密集型任务，然后将这些任务添加到队列中，而不是同步执行。让任务进程以受控方式从队列中提取任务。也可以轻松添加新的队列消费者以扩展后端任务处理。\n- 分解可能会阻塞 Node.js 事件循环的单一任务。比如用户请求需要像音频转码这样的 CPU 密集型工作，就可以将此任务委托给其他进程，从而释放面向用户的进程以保持响应。\n- 提供跨各种服务的可靠通信渠道。例如，您可以在一个进程或服务中排队任务（作业），并在另一个进程或服务中使用它们。在任何流程或服务的作业生命周期中完成、错误或其他状态更改时，您都可以收到通知（通过监听状态事件）。当队列生产者或消费者失败时，它们的状态被保留，并且当节点重新启动时任务处理可以自动重新启动。\n\nMidway 提供了 `@midwayjs/bullmq` 包作为 [BullMQ](https://github.com/taskforcesh/bullmq) 之上的抽象/包装器。BullMQ 是 Bull 的下一代实现，提供了更好的性能和更多的功能。该软件包可以轻松地将 BullMQ 以友好的方式集成到您的应用程序中。\n\nBullMQ 使用 Redis 来保存作业数据，在使用 Redis 时，Queue 架构是完全分布式，和平台无关。例如，您可以在一个（或多个）节点（进程）中运行一些 Queue 生产者、消费者，而在其他节点上的运行其他生产者和消费者。\n\n:::tip\nBullMQ 是一个分布式任务管理系统，必须依赖 redis\n:::\n\n:::caution\n由于 BullMQ 是 Bull 的升级版，从 v3.20 开始，将由 BullMQ 替代 Bull 组件，如需使用 Bull 组件，请参考 [Bull 文档](./bull)\n:::\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n\n## 安装组件\n\n```bash\n$ npm i @midwayjs/bullmq@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/bullmq\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n## 使用组件\n\n将 bullmq 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as bullmq from '@midwayjs/bullmq';\n\n@Configuration({\n  imports: [\n    // ...\n    bullmq\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n## 一些概念\n\nBullMQ 将整个队列分为以下几个部分：\n\n- Queue：队列，管理任务\n- Job：每个任务对象，可以对任务进行启停控制\n- Worker：任务处理器，实际的逻辑执行部分\n- QueueEvents：队列事件，用于监听任务状态变化\n- FlowProducer：任务流生产者，用于创建任务依赖关系\n\n## 基础配置\n\nbullmq 是一个分布式任务管理器，强依赖于 redis，在 `config.default.ts` 文件中配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  bullmq: {\n    defaultConnection: {\n      host: '127.0.0.1',\n      port: 6379,\n    },\n    // 可选，队列前缀\n    defaultPrefix: '{midway-bullmq}',\n  },\n}\n```\n\n有账号密码情况：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  bullmq: {\n    defaultConnection: {\n      port: 6379,\n      host: '127.0.0.1',\n      password: 'foobared',\n    }\n  },\n}\n```\n\n所有的队列、任务处理器、队列事件、任务流都会复用该配置。\n\n## 编写任务处理器\n\n使用 `@Processor` 装饰器装饰一个类，用于快速定义一个任务处理器。\n\n`@Processor` 装饰器需要传递一个 Queue（队列）的名字，在框架启动时，如果没有名为 `test` 的队列，则会自动创建。\n\n比如，我们在 `src/processor/test.processor.ts` 文件中编写如下代码。\n\n```typescript\nimport { Processor, IProcessor } from '@midwayjs/bullmq';\n\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  async execute(data: any) {\n    // 处理任务逻辑\n    console.log('processing job:', data);\n  }\n}\n```\n\n## 执行任务\n\n当定义完 Processor 之后，由于并未指定 Processor 如何执行，我们还需要手动执行它。\n\n### 手动执行任务\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bullmq from '@midwayjs/bullmq';\n\n@Configuration({\n  imports: [\n    bullmq\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullmqFramework: bullmq.Framework;\n\n  async onServerReady() {\n    // 获取 Processor 相关的队列\n    const testQueue = this.bullmqFramework.getQueue('test');\n    // 立即添加这个任务\n    await testQueue?.addJobToQueue();\n  }\n}\n```\n\n### 增加执行参数\n\n我们也可以在执行时，附加一些参数。\n\n```typescript\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  async execute(params) {\n    // params.name => 'harry'\n  }\n}\n\n// invoke\nconst testQueue = this.bullmqFramework.getQueue('test');\nawait testQueue?.addJobToQueue({\n  aaa: 1,\n  bbb: 2,\n});\n```\n\n### 任务状态和管理\n\n执行 `addJobToQueue` 后，我们可以获取到一个 `Job` 对象。\n\n```typescript\nconst testQueue = this.bullmqFramework.getQueue('test');\nconst job = await testQueue?.addJobToQueue();\n\n// 更新进度\nawait job.updateProgress(60);\n// 获取进度\nconst progress = await job.progress;\n// => 60\n\n// 获取任务状态\nconst state = await job.getState();\n// state => 'delayed' 延迟状态\n// state => 'completed' 完成状态\n// state => 'failed' 失败状态\n```\n\n### 延迟执行\n\n执行任务时，也有一些额外的选项。\n\n比如，延迟 1s 执行。\n\n```typescript\nconst testQueue = this.bullmqFramework.getQueue('test');\nawait testQueue?.addJobToQueue({}, { delay: 1000 });\n```\n\n### 任务重试\n\nBullMQ 支持任务失败重试机制。\n\n```typescript\nconst testQueue = this.bullmqFramework.getQueue('test');\nawait testQueue?.addJobToQueue({}, {\n  attempts: 3,  // 最多重试 3 次\n  backoff: {    // 重试策略\n    type: 'exponential',  // 指数退避\n    delay: 1000          // 初始延迟 1 秒\n  }\n});\n```\n\n### 任务优先级\n\n可以为任务设置优先级，优先级高的任务会优先执行。\n\n```typescript\nconst testQueue = this.bullmqFramework.getQueue('test');\n// priority 值越大优先级越高\nawait testQueue?.addJobToQueue({ priority: 1 }, { priority: 3 }); // 高优先级\nawait testQueue?.addJobToQueue({ priority: 2 }, { priority: 2 }); // 中优先级\nawait testQueue?.addJobToQueue({ priority: 3 }, { priority: 1 }); // 低优先级\n```\n\n### 中间件和错误处理\n\nBullMQ 组件包含可以独立启动的 Framework，有着自己的 App 对象和 Context 结构。\n\n我们可以对 bullmq 的 App 配置独立的中间件和错误过滤器。\n\n```typescript\n@Configuration({\n  imports: [\n    bullmq\n  ]\n})\nexport class MainConfiguration {\n\n  @App('bullmq')\n  bullmqApp: bullmq.Application;\n\n  async onReady() {\n    this.bullmqApp.useMiddleware(/*中间件*/);\n    this.bullmqApp.useFilter(/*过滤器*/);\n  }\n}\n```\n\n### 上下文\n\n任务处理器执行是在请求作用域中，其有着特殊的 Context 对象结构。\n\n```typescript\nexport interface Context extends IMidwayContext {\n  jobId: string;\n  job: Job;\n  token?: string;\n  from: new (...args) => IProcessor;\n}\n```\n\n我们可以直接从 ctx 中访问当前的 Job 对象。\n\n```typescript\nimport { Processor, IProcessor, Context } from '@midwayjs/bullmq';\n\n@Processor('test')\nexport class TestProcessor implements IProcessor {\n  @Inject()\n  ctx: Context;\n\n  async execute(data: any) {\n    // ctx.jobId => 当前任务ID\n    // ctx.job => 当前任务对象\n  }\n}\n```\n\n## 重复执行的任务\n\n除了手动执行的方式，我们也可以通过 `@Processor` 装饰器的参数，快速配置任务的重复执行。\n\n```typescript\nimport { Processor, IProcessor } from '@midwayjs/bullmq';\nimport { FORMAT } from '@midwayjs/core';\n\n@Processor('test', {\n  repeat: {\n    pattern: FORMAT.CRONTAB.EVERY_PER_5_SECOND\n  }\n})\nexport class TestProcessor implements IProcessor {\n  async execute() {\n    // 每 5 秒执行一次\n  }\n}\n```\n\n## 高级功能\n\n### 任务流（Flow Producer）\n\nBullMQ 支持创建任务依赖关系，形成任务流。\n\n```typescript\nconst flowProducer = bullmqFramework.createFlowProducer({}, 'test-flow');\n\n// 创建任务流\nawait flowProducer.add({\n  name: 'flow-test',\n  queueName: 'flow-queue-1',\n  data: { value: 1 },\n  children: [\n    {\n      name: 'child-job',\n      queueName: 'flow-queue-2',\n      data: { value: 2 }\n    }\n  ]\n});\n```\n\n### 队列事件\n\nBullMQ 提供了丰富的事件系统，可以监听任务的各种状态变化。\n\n```typescript\nconst eventQueue = bullmqFramework.createQueue('event-queue');\nconst queueEvents = eventQueue.createQueueEvents();\n\n// 监听任务完成事件\nqueueEvents.on('completed', ({ jobId }) => {\n  console.log(`Job ${jobId} completed!`);\n});\n\n// 监听任务失败事件\nqueueEvents.on('failed', ({ jobId, failedReason }) => {\n  console.log(`Job ${jobId} failed: ${failedReason}`);\n});\n```\n\n### 清理任务历史记录\n\n当开启 Redis 后，默认情况下，bullmq 会记录所有的成功和失败的任务 key，这可能会导致 redis 的 key 暴涨，我们可以配置成功或者失败后清理的选项。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  bullmq: {\n    defaultQueueOptions: {\n      defaultJobOptions: {\n        removeOnComplete: 3,  // 成功后只保留最近 3 条记录\n        removeOnFail: 10,     // 失败后只保留最近 10 条记录\n      }\n    }\n  }\n}\n```\n\n### Redis 集群\n\nbullmq 可以指定 connection 实例，你可以将自己创建的 Redis 实例配置到 `defaultConnection` 中，这样就可以接入 Redis 集群。\n\n```typescript\n// src/config/config.default.ts\nimport Redis from 'ioredis';\n\nconst clusterOptions = {\n  enableReadyCheck: false,\n  retryDelayOnClusterDown: 300,\n  retryDelayOnFailover: 1000,\n  retryDelayOnTryAgain: 3000,\n  slotsRefreshTimeout: 10000,\n  maxRetriesPerRequest: null\n}\n\nconst redisClientInstance = new Redis.Cluster([\n  {\n    port: 7000,\n    host: '127.0.0.1'\n  },\n  {\n    port: 7002,\n    host: '127.0.0.1'\n  },\n], clusterOptions);\n\nexport default {\n  bullmq: {\n    defaultConnection: redisClientInstance,\n    defaultPrefix: '{midway-bullmq}',\n  }\n}\n```\n\n### 获取任务处理器\n\n通过内置的方法，我们可以获取到原始的任务处理器（Worker）。\n\n可以启动任意多个 Worker 监听同一个队列，在这种情况下 BullMQ 会把队列中的任务轮流（round-robin）分配给这些 Worker。\n\n当队列中有任务被添加时，BullMQ 会在这两个 Worker 之间自动分配任务。\n\n我们可以通过方法 `getWorker` 获取到首个监听的 Worker 对象，也可以通过方法 `getWorkers` 获取到该队列监听的所有 Worker 对象。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bullmq from '@midwayjs/bullmq';\n\n@Configuration({\n  imports: [\n    // ...\n    bullmq,\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullmqFramework: bullmq.Framework;\n\n  async onServerReady() {\n    const testWorker = this.bullmqFramework.getWorker('test');\n    const allWorkers = this.bullmqFramework.getWorkers('test');\n  }\n}\n\n```\n\n\n## 组件日志\n\n组件有着自己的日志，默认会将 `ctx.logger` 记录在 `midway-bullmq.log` 中。\n\n我们可以单独配置这个 logger 对象。\n\n```typescript\nexport default {\n  midwayLogger: {\n    clients: {\n      bullMQLogger: {\n        fileLogName: 'midway-bullmq.log',\n      },\n    },\n  },\n}\n```\n\n这个日志的输出格式，我们也可以单独配置。\n\n```typescript\nexport default {\n  bullmq: {\n    contextLoggerFormat: info => {\n      const { jobId, from } = info.ctx;\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} [${jobId} ${from.name}] ${info.message}`;\n    },\n  }\n}\n```\n\n## BullMQ 原始对象\n\n组件导出了 BullMQ 的原始对象，可以进行更多的操作。\n\n```typescript\nimport { BullMQ } from '@midwayjs/bullmq';\n```\n\n你可以通过 `BullMQ` 对象，获取到 `Queue`、`Worker`、`FlowProducer` 等对象定义。\n\n## Bull UI\n\n在分布式场景中，我们可以资利用 Bull UI 来简化管理。\n\n和 bull 组件类似，需要独立安装和启用。\n\n```bash\n$ npm i @midwayjs/bull-board@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/bull-board\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n将 bull-board 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as bullmq from '@midwayjs/bullmq';\nimport * as bullBoard from '@midwayjs/bull-board';\n\n@Configuration({\n  imports: [\n    // ...\n    bullmq,\n    bullBoard,\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n默认的访问路径为：`http://127.1:7001/ui`。\n\n效果如下：\n\n![](https://img.alicdn.com/imgextra/i2/O1CN01j4wEFb1UacPxA06gs_!!6000000002534-2-tps-1932-1136.png)\n\n可以通过配置进行基础路径的修改。\n\n```typescript\n// src/config/config.prod.ts\nexport default {\n  // ...\n  bullBoard: {\n    basePath: '/ui',\n  },\n}\n```\n\n此外，组件提供了 `BullBoardManager` ，可以添加动态创建的队列。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as bullmq from '@midwayjs/bullmq';\nimport * as bullBoard from '@midwayjs/bull-board';\n\n@Configuration({\n  imports: [\n    // ...\n    bullmq,\n    bullBoard\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  bullmqFramework: bullmq.Framework;\n  \n  @Inject()\n  bullBoardManager: bullBoard.BullBoardManager;\n\n  async onServerReady() {\n    const testQueue = this.bullmqFramework.createQueue('test', {\n      // ...\n    });\n\n    this.bullBoardManager.addQueue(new bullBoard.BullMQAdapter(testQueue) as any);\n  }\n}\n```\n\n由于最新 bull-board 要求最低 Node.js 版本为 v20，所以 Midway v3 无法将 bull-board 升级；在 v5.23.0 版本下的 `bull-board` 存在类型定义问题，采用 `as any`  的方式绕过。\n\n<!-- File: site/docs/extensions/busboy.md (Tokens: ~5366 | Category: anthropic-skill) -->\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n# 文件上传\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的通用上传组件，支持 `file` (服务器临时文件) 和 `stream` （流）两种模式。\n\n相关信息：\n\n| web 支持情况      |      |\n| ----------------- | ---- |\n| @midwayjs/koa     | ✅    |\n| @midwayjs/faas    | 💬    |\n| @midwayjs/web     | ✅    |\n| @midwayjs/express | ✅    |\n\n:::caution\n\n💬 部分函数计算平台不支持流式请求响应，请参考对应平台能力。\n\n:::\n\n:::tip\n\n本模块自 3.17.0 起替换 upload 组件。\n\n和 upload 组件的差异为：\n\n* 1、配置的 key 从 `upload` 调整为 `busboy`\n* 2、中间件不再默认加载，手动可配置到全局或者路由\n* 3、入参定义类型调整为 `UploadStreamFileInfo`\n* 4、`fileSize` 的配置有调整\n\n:::\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/busboy@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/busboy\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n\n\n## 启用组件\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration } from '@midwayjs/core';\nimport * as busboy from '@midwayjs/busboy';\n\n@Configuration({\n  imports: [\n    // ...other components\n    busboy\n  ],\n  // ...\n})\nexport class MainConfiguration {}\n```\n\n\n\n## 配置中间件\n\n组件中提供了 `UploadMiddleware` 这个中间件，可以将其配置到全局或者特定路由，推荐配置到特定路由，提升性能。\n\n\n\n**路由中间件**\n\n```typescript\nimport { Controller, Post } from '@midwayjs/core';\nimport { UploadMiddleware } from '@midwayjs/busboy';\n\n@Controller('/')\nexport class HomeController {\n\n  @Post('/upload', { middleware: [UploadMiddleware] })\n  async upload(/*...*/) {\n    // ...\n  }\n}\n```\n\n**全局中间件**\n\n<Tabs>\n<TabItem value=\"koa\" label=\"@midwayjs/koa\">\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration } from '@midwayjs/core';\nimport * as busboy from '@midwayjs/busboy';\nimport { Application } from '@midwayjs/koa';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  @App('koa')\n  app: Application;\n  \n  async onReady() {\n    this.app.useMiddleware(busboy.UploadMiddleware);\n  }\n}\n```\n</TabItem>\n<TabItem value=\"egg\" label=\"@midwayjs/web\">\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration } from '@midwayjs/core';\nimport * as busboy from '@midwayjs/busboy';\nimport { Application } from '@midwayjs/web';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  @App('egg')\n  app: Application;\n  \n  async onReady() {\n    this.app.useMiddleware(busboy.UploadMiddleware);\n  }\n}\n```\n</TabItem>\n<TabItem value=\"express\" label=\"@midwayjs/express\">\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration } from '@midwayjs/core';\nimport * as busboy from '@midwayjs/busboy';\nimport { Application } from '@midwayjs/express';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  @App('express')\n  app: Application;\n  \n  async onReady() {\n    this.app.useMiddleware(busboy.UploadMiddleware);\n  }\n}\n```\n</TabItem>\n<TabItem value=\"faas\" label=\"@midwayjs/faas\">\n\n```typescript\n// src/configuration.ts\n\nimport { Configuration } from '@midwayjs/core';\nimport * as busboy from '@midwayjs/busboy';\nimport { Application } from '@midwayjs/faas';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  @App('faas')\n  app: Application;\n  \n  async onReady() {\n    this.app.useMiddleware(busboy.UploadMiddleware);\n  }\n}\n```\n</TabItem>\n</Tabs>\n\n\n\n## 配置\n\n组件使用 `busboy` 作为配置的 key。\n\n\n\n### 上传模式\n\n上传分为三种模式，文件模式，流式模式以及新增的异步迭代器模式。\n\n代码中使用 `@Files()` 装饰器获取上传的文件， `@Fields` 装饰器获取其他上传表单字段。\n\n<Tabs>\n<TabItem value=\"file\" label=\"文件模式\">\n\n`file` 为默认值，配置 mode 为 `file` 字符串。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  busboy: {\n    mode: 'file',\n  },\n}\n```\n\n在代码中获取上传的文件，支持同时上传多个文件。\n\n```typescript\nimport { Controller, Post, Files, Fields } from '@midwayjs/core';\nimport { UploadFileInfo } from '@midwayjs/busboy';\n\n@Controller('/')\nexport class HomeController {\n\n  @Post('/upload', /*...*/)\n  async upload(@Files() files: Array<UploadFileInfo>, @Fields() fields: Record<string, string>) {\n    /*\n    files = [\n      {\n        filename: 'test.pdf',        // 文件原名\n        data: '/var/tmp/xxx.pdf',    // 服务器临时文件地址\n        mimeType: 'application/pdf', // mime\n        fieldName: 'file'            // field name\n      },\n    ]\n    */\n  }\n}\n```\n\n使用 file 模式时， 获取的 `data` 为上传的文件在服务器的 `临时文件地址`，后续可以再通过 `fs.createReadStream` 等方式来处理此文件内容，支持同时上传多个文件，多个文件会以数组的形式存放。\n\n每个数组内的对象包含以下几个字段\n\n```typescript\nexport interface UploadFileInfo {\n  /**\n   * 上传的文件名\n   */\n  filename: string;\n  /**\n   * 上传文件 mime 类型\n   */\n  mimeType: string;\n  /**\n   * 上传服务端保存的路径\n   */\n  data: string;\n  /**\n   * 上传的表单字段名\n   */\n  fieldName: string;\n}\n```\n\n\n\n</TabItem>\n\n<TabItem value=\"asyncIterator\" label=\"异步迭代器模式\">\n\n从 `v3.18.0` 提供，替代原有的 `stream` 模式，该模式支持多个文件流式上传。\n\n配置 mode 为 `asyncIterator` 字符串。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  busboy: {\n    mode: 'asyncIterator',\n  },\n}\n```\n\n在代码中获取上传的文件。\n\n```typescript\nimport { Controller, Post, Files, Fields } from '@midwayjs/core';\nimport { UploadStreamFileInfo, UploadStreamFieldInfo } from '@midwayjs/busboy';\n\n@Controller('/')\nexport class HomeController {\n\n  @Post('/upload', /*...*/)\n  async upload(\n  \t@Files() fileIterator: AsyncGenerator<UploadStreamFileInfo>,\n    @Fields() fieldIterator: AsyncGenerator<UploadStreamFieldInfo>\n  ) {\n    // ...\n  }\n}\n```\n\n在该模式下，`@Files` 和 `@File` 装饰器会提供同一个 `AsyncGenerator` ，而 `@Fields` 会也同样会提供一个 `AsyncGenerator`。\n\n通过循环 `AsyncGenerator` ，可以针对每个上传文件的 `ReadStream` 做处理。\n\n```typescript\nimport { Controller, Post, Files, Fields } from '@midwayjs/core';\nimport { UploadStreamFileInfo, UploadStreamFieldInfo } from '@midwayjs/busboy';\nimport { tmpdir } from 'os';\nimport { createWriteStream } from 'fs';\n\n@Controller('/')\nexport class HomeController {\n\n  @Post('/upload', /*...*/)\n  async upload(\n  \t@Files() fileIterator: AsyncGenerator<UploadStreamFileInfo>,\n    @Fields() fieldIterator: AsyncGenerator<UploadStreamFieldInfo>\n  ) {\n    for await (const file of fileIterator) {\n      const { filename, data } = file;\n      const p = join(tmpdir, filename);\n      const stream = createWriteStream(p);\n      data.pipe(stream);\n    }\n\n    for await (const { name, value } of fieldIterator) {\n      // ...\n    }\n\n    // ...\n  }\n}\n```\n\n注意，如果一次上传中任意一个文件抛出了错误，本次上传流会直接关闭，所有未传输完成的文件都会异常。\n\n异步迭代器中的上传对象包含以下几个字段。\n\n```typescript\nexport interface UploadStreamFieldInfo {\n  /**\n   * 上传的文件名\n   */\n  filename: string;\n  /**\n   * 上传文件 mime 类型\n   */\n  mimeType: string;\n  /**\n   * 上传文件的文件流\n   */\n  data: Readable;\n  /**\n   * 上传的表单字段名\n   */\n  fieldName: string;\n}\n```\n\n异步迭代器中的 `@Fields` 的对象略有不同，返回的数据会包含 `name` 和 `value` 字段。\n\n```typescript\nexport interface UploadStreamFieldInfo {\n  /**\n   * 表单名\n   */\n  name: string;\n  /**\n   * 表单值\n   */\n  value: any;\n}\n```\n\n\n\n</TabItem>\n\n<TabItem value=\"stream\" label=\"流模式\">\n\n:::caution\n\n不再推荐使用。\n\n:::\n\n配置 mode 为 `stream` 字符串。\n\n\n使用 stream 模式时，通过 `@Files` 中获取的 `data` 为 `ReadStream`，后续可以再通过 `pipe` 等方式继续将数据流转至其他 `WriteStream` 或 `TransformStream`。\n\n\n使用 stream 模式时，仅同时上传一个文件，即 `@Files` 数组中只有一个文件数据对象。\n\n另外，stream 模式 `不会` 在服务器上产生临时文件，所以获取到上传的内容后无需手动清理临时文件缓存。\n\n:::tip\n\nfaas 场景实现方式视平台而定，如果平台不支持流式请求/响应但是业务开启了 `mode: 'stream'`，将采用先读取到内存，再模拟流式传输来降级处理。\n\n:::\n\n在代码中获取上传的文件，流式模式下仅支持单个文件。\n\n```typescript\nimport { Controller, Post, Files, Fields } from '@midwayjs/core';\nimport { UploadStreamFileInfo } from '@midwayjs/busboy';\n\n@Controller('/')\nexport class HomeController {\n\n  @Post('/upload', /*...*/)\n  async upload(@Files() files: Array<UploadStreamFileInfo>, @Fields() fields: Record<string, string) {\n    /*\n    files = [\n      {\n        filename: 'test.pdf',        // 文件原名\n        data: ReadStream,            // 文件流\n        mimeType: 'application/pdf', // mime\n        fieldName: 'file'            // field name\n      },\n    ]\n\n    */\n  }\n}\n```\n\n流式模式中的对象包含以下几个字段。\n\n```typescript\nexport interface UploadStreamFieldInfo {\n  /**\n   * 上传的文件名\n   */\n  filename: string;\n  /**\n   * 上传文件 mime 类型\n   */\n  mimeType: string;\n  /**\n   * 上传文件的文件流\n   */\n  data: Readable;\n  /**\n   * 上传的表单字段名\n   */\n  fieldName: string;\n}\n```\n\n</TabItem>\n\n</Tabs>\n\n\n\n### 上传文件后缀检查\n\n通过 `whitelist` 属性，配置允许上传的文件后缀名，配置 `null` 则不校验后缀名。\n\n:::caution\n\n如果配置为 `null`，则不对上传文件后缀名进行校验，如果采取文件上传模式 (mode=file)，则会有可能被攻击者所利用，上传 `.php`、`.asp` 等后缀的 WebShell 实现攻击行为。\n\n当然，由于组件会对上传后的临时文件采取 `重新随机生成` 文件名写入，只要开发者 `不将` 上传后的临时文件地址返回给用户，那么即使用户上传了一些不被预期的文件，那也无需过多担心会被利用。\n\n:::\n\n\n如果上传的文件后缀不匹配，会响应 `400` error，默认值如下：\n\n```ts\n'.jpg',\n'.jpeg',\n'.png',\n'.gif',\n'.bmp',\n'.wbmp',\n'.webp',\n'.tif',\n'.psd',\n'.svg',\n'.js',\n'.jsx',\n'.json',\n'.css',\n'.less',\n'.html',\n'.htm',\n'.xml',\n'.pdf',\n'.zip',\n'.gz',\n'.tgz',\n'.gzip',\n'.mp3',\n'.mp4',\n'.avi',\n```\n\n可以通过组件中导出的 `uploadWhiteList` 获取到默认的后缀名白名单。\n\n另外，midway 上传组件，为了避免部分 `恶意用户`，通过某些技术手段来`伪造`一些可以被截断的扩展名，所以会对获取到的扩展名的二进制数据进行过滤，仅支持 `0x2e`（即英文点 `.`）、`0x30-0x39`（即数字 `0-9`）、`0x61-0x7a`（即小写字母 `a-z`） 范围内的字符作为扩展名，其他字符将会被自动忽略。\n\n你可以传递一个函数，可以根据不同的条件动态返回白名单。\n\n```typescript\n// src/config/config.default.ts\nimport { uploadWhiteList } from '@midwayjs/busboy';\nimport { tmpdir } from 'os';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  busboy: {\n    whitelist: (ctx) => {\n      if (ctx.path === '/') {\n        return [\n          '.jpg',\n          '.jpeg',\n        ];\n      } else {\n        return [\n          '.jpg',\n        ]\n      };\n    },\n    // ...\n  },\n}\n```\n\n\n\n\n\n### 上传文件 MIME 类型检查\n\n部分`恶意用户`，会尝试将 `.php` 等 WebShell 修改扩展名为 `.jpg`，来绕过基于扩展名的白名单过滤规则，在某些服务器环境内，这个 jpg 文件依然会被作为 PHP 脚本来执行，造成安全风险。\n\n组件提供了 `mimeTypeWhiteList` 配置参数 **【请注意，此参数无默认值设置，即默认不校验】**，您可以通过此配置设置允许的文件 MIME 格式，规则为由数组 `[扩展名, mime, [...moreMime]]` 组成的 `二级数组`，例如：\n\n```typescript\n// src/config/config.default.ts\nimport { uploadWhiteList } from '@midwayjs/busboy';\nexport default {\n  // ...\n  busboy: {\n    // ...\n    // 扩展名白名单\n    whitelist: uploadWhiteList,\n    // 仅允许下面这些文件类型可以上传\n    mimeTypeWhiteList: {\n      '.jpg': 'image/jpeg',\n      // 也可以设置多个 MIME type，比如下面的允许 .jpeg 后缀的文件是 jpg 或者是 png 两种类型\n      '.jpeg': ['image/jpeg', 'image/png'],\n      // 其他类型\n      '.gif': 'image/gif',\n      '.bmp': 'image/bmp',\n      '.wbmp': 'image/vnd.wap.wbmp',\n      '.webp': 'image/webp',\n    }\n  },\n}\n```\n\n您也可以使用组件提供的 `DefaultUploadFileMimeType` 变量，作为默认的 MIME 校验规则，它提供了常用的 `.jpg`、`.png`、`.psd` 等文件扩展名的 MIME 数据：\n\n```typescript\n// src/config/config.default.ts\nimport { uploadWhiteList, DefaultUploadFileMimeType } from '@midwayjs/busboy';\nexport default {\n  // ...\n  busboy: {\n    // ...\n    // 扩展名白名单\n    whitelist: uploadWhiteList,\n    // 仅允许下面这些文件类型可以上传\n    mimeTypeWhiteList: DefaultUploadFileMimeType,\n  },\n}\n```\n\n文件格式与对应的 MIME 映射，您可以通过 `https://mimetype.io/` 这个网站来查询，对于文件的 MIME 识别，我们使用的是 [file-type@16](https://www.npmjs.com/package/file-type) 这个 npm 包，请注意它支持的文件类型。\n\n:::info\n\nMIME 类型校验规则仅适用于使用 文件上传模式 `mode=file`，同时设置此校验规则之后，由于需要读取文件内容进行匹配，所以会稍微影响上传性能。\n\n但是，我们依然建议您在条件允许的情况下，设置 `mimeTypeWhiteList` 参数，这将提升您的应用程序安全性。\n\n:::\n\n你可以传递一个函数，可以根据不同的条件动态返回 MIME 规则。\n\n```typescript\n// src/config/config.default.ts\nimport { tmpdir } from 'os';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  busboy: {\n    mimeTypeWhiteList: (ctx) => {\n      if (ctx.path === '/') {\n        return {\n          '.jpg': 'image/jpeg',\n        };\n      } else {\n        return {\n          '.jpeg': ['image/jpeg', 'image/png'],\n        }\n      };\n    }\n  },\n}\n\n```\n\n\n\n### Busboy 上传限制\n\n默认情况下没有限制，可以通过配置修改，数字类型，单位为 byte。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  busboy: {\n    // ...\n    limits: {\n      fileSize: 1024\n    }\n  },\n}\n```\n\n除此之外，还可以设置一些其他的 [限制](https://github.com/mscdex/busboy/tree/master?tab=readme-ov-file#exports)。\n\n\n\n\n### 临时文件与清理\n\n\n如果你使用了 `file` 模式来获取上传的文件，那么上传的文件会存放在您于 `config` 文件中设置的 `upload` 组件配置中的 `tmpdir` 选项指向的文件夹内。\n\n你可以通过在配置中使用 `cleanTimeout` 来控制自动的临时文件清理时间，默认值为 `5 * 60 * 1000`，即上传的文件于 `5 分钟` 后自动清理，设置为 `0` 则视为不开启自动清理功能。\n\n```typescript\n// src/config/config.default.ts\nimport { uploadWhiteList } from '@midwayjs/busboy';\nimport { tmpdir } from 'os';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  busboy: {\n    mode: 'file',\n    tmpdir: join(tmpdir(), 'midway-busboy-files'),\n    cleanTimeout: 5 * 60 * 1000,\n  },\n}\n\n```\n\n你也可以在代码中通过调用 `await ctx.cleanupRequestFiles()` 来主动清理当前请求上传的临时文件。\n\n\n\n### 设置不同路由的配置\n\n通过中间件的不同实例，可以对不同的路由做不同的配置，这种场景下会和全局配置合并，仅能覆盖一小部分配置。\n\n```typescript\nimport { Controller, Post, Files, Fields } from '@midwayjs/core';\nimport { UploadFileInfo, UploadMiddleware } from '@midwayjs/busboy';\n\n@Controller('/')\nexport class HomeController {\n  @Post('/upload1', { middleware: [ createMiddleware(UploadMiddleware, {mode: 'file'}) ]})\n  async upload1(@Files() files Array<UploadFileInfo>) {\n    // ...\n  }\n  \n  @Post('/upload2', { middleware: [ createMiddleware(UploadMiddleware, {mode: 'stream'}) ]})\n  async upload2(@Files() files Array<UploadFileInfo>) {\n    // ...\n  }\n}\n```\n\n当前可以传递的配置包括 `mode` 以及 `busboy` 自带的 [配置](https://github.com/mscdex/busboy/tree/master?tab=readme-ov-file#exports)。\n\n\n\n## 内置错误\n\n以下的错误在不同上传模式下均会自动触发。\n\n* `MultipartInvalidFilenameError` 无效文件名\n* `MultipartInvalidFileTypeError` 无效文件类型\n* `MultipartFileSizeLimitError` 文件大小超出限制\n* `MultipartFileLimitError` 文件数量超出限制\n* `MultipartPartsLimitError` 上传 parts 数量超出限制\n* `MultipartFieldsLimitError` fields 数量超出限制\n* `MultipartError` 其余的 busbuy 错误\n\n\n\n## 安全提示\n\n1. 请注意是否开启 `扩展名白名单` （whiteList），如果扩展名白名单被设置为 `null`，则会有可能被攻击者所利用上传 `.php`、`.asp` 等WebShell。\n2. 请注意是否设置 `match` 或 `ignore` 规则，否则普通的 `POST/PUT` 等接口会有可能被攻击者利用，造成服务器负荷加重和空间大量占用问题。\n3. 请注意是否设置 `文件类型规则` （fileTypeWhiteList）,否则可能会被攻击者伪造文件类型进行上传。\n\n\n\n## 前端文件上传示例\n\n### 1. html form 的形式\n\n```html\n<form action=\"/api/upload\" method=\"post\" enctype=\"multipart/form-data\">\n  Name: <input type=\"text\" name=\"name\" /><br />\n  File: <input type=\"file\" name=\"testFile\" /><br />\n  <input type=\"submit\" value=\"Submit\" />\n</form>\n```\n\n### 2. fetch FormData 方式\n\n```js\nconst fileInput = document.querySelector('#your-file-input') ;\nconst formData = new FormData();\nformData.append('file', fileInput.files[0]);\n\nfetch('/api/upload', {\n  method: 'POST',\n  body: formData,\n});\n```\n\n\n\n## Postman 测试示例\n\n![](https://img.alicdn.com/imgextra/i4/O1CN01iv9ESW1uIShNiRjBF_!!6000000006014-2-tps-2086-1746.png)\n\n<!-- File: site/docs/extensions/cache.md (Tokens: ~1550 | Category: anthropic-skill) -->\n# 缓存\n\nMidway Cache 是为了方便开发者进行缓存操作的组件，它有利于改善项目的性能。它为我们提供了一个数据中心以便进行高效的数据访问。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n## 安装\n\n首先安装相关的组件模块。\n\n```bash\n$ npm i @midwayjs/cache@4 cache-manager --save\n$ npm i @types/cache-manager --save-dev\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/cache\": \"^4.0.0\",\n    \"cache-manager\": \"^3.4.1\",\n    // ...\n  },\n  \"devDependencies\": {\n    \"@types/cache-manager\": \"^3.4.0\",\n    // ...\n  }\n}\n```\n\n\n\n## 使用 Cache\n\nMidway 为不同的 cache 存储提供了统一的 API。默认内置了一个基于内存数据存储的数据中心。如果想要使用别的数据中心，开发者也可以切换到例如 mongodb、fs 等模式。\n\n\n首先，引入 Cache 组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as cache from '@midwayjs/cache';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    cache\t\t// 导入 cache 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n然后在业务代码中即可注入使用。\n\n```typescript\nimport { Inject, Provide } from '@midwayjs/core';\nimport { IUserOptions } from '../interface';\nimport { CacheManager } from '@midwayjs/cache';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  cacheManager: CacheManager;     \t\t\t// 依赖注入 CacheManager\n}\n```\n\n通过提供的 API 来设置，获取缓存数据。\n\n\n```typescript\nimport { Inject, Provide } from '@midwayjs/core';\nimport { IUserOptions } from '../interface';\nimport { CacheManager } from '@midwayjs/cache';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  cacheManager: CacheManager;\n\n  async getUser(options: IUserOptions) {\n    // 设置缓存内容\n    await this.cacheManager.set(`name`, 'stone-jin');\n\n    // 获取缓存内容\n    let result = await this.cacheManager.get(`name`);\n\n    return result;\n  }\n\n  async getUser2(){\n    //获取缓存内容\n    let result = await this.cacheManager.get(`name`);\n    return result;\n  }\n\n  async reset(){\n    await this.cacheManager.reset(); // 清空对应 store 的内容\n  }\n}\n```\n\n\n\n### 设置缓存\n\n\n我们通过 `await this.cache.set(key, value)` 方法进行设置，此处默认过期时间是10s。\n\n\n你也可以手动设置 TTL（过期时间），如下：\n```typescript\nawait this.cacheManager.set(key, value, {ttl: 1000});\t// ttl的单位为秒\n```\n如果你想要 Cache 不过期，则将 TTL 设置为 null 即可。\n```typescript\nawait this.cacheManager.set(key, value, {ttl: null});\n```\n同时你也可以通过全局的 `config.default.ts` 中进行设置。\n```typescript\nexport default {\n  // ...\n  cache: {\n    store: 'memory',\n    options: {\n      max: 100,\n      ttl: 10,\t\t// 修改默认的ttl配置\n    },\n  }\n}\n```\n\n\n### 获取缓存\n\n```typescript\nconst value = await this.cacheManager.get(key);\n```\n如果获取不到，则为 undefined。\n\n\n\n### 移除缓存\n\n\n移除缓存，可以通过 del 方法。\n```typescript\nawait this.cacheManager.del(key);\n```\n\n\n\n### 清空整体store数据（此处是整体清除，需要重点⚠️）\n\n\n比如用户设置了某个 redis 为 store，调用的话，包括非 cache 模块设置的也会清除。\n```typescript\nawait this.cacheManager.reset(); // 这块需要注意\n```\n\n\n\n## 全局配置\n\n\n当我们引用了这个 cache 组件后，我们能对其进行全局的配置。配置方法跟别的组件类似。\n\n\n默认的配置：\n```typescript\nexport default {\n  // ...\n  cache: {\n  \tstore: 'memory',\n    options: {\n      max: 100,\n      ttl: 10,\n    },\n  }\n}\n```\n例如用户可以修改默认的 TTL，也就是过期时间。\n\n\n\n## 其他Cache\n\n\n用户也可以修改  store 方式，在 `config.default.ts` 中进行组件的配置：\n```typescript\nimport * as redisStore from 'cache-manager-ioredis';\n\nexport default {\n  // ...\n  cache: {\n  \tstore: redisStore,\n    options: {\n      host: 'localhost', // default value\n      port: 6379, // default value\n      password: '',\n      db: 0,\n      keyPrefix: 'cache:',\n      ttl: 100\n    },\n  }\n}\n```\n或者修改为 mongodb 的 cache。\n\n\n:::danger\n**再次注意⚠️：使用 redis 作为 cache 的时候，代码里面慎用 reset 方法，因为会把整个 redis 给 flushdb，简称数据清空。**\n:::\n\n\n\n## 相关文档\n\n\n由于 Midway Cache 是基于 cache-manager 封装，所以相关资料用户也可以查询：[cache-manger](https://www.npmjs.com/package/cache-manager)。\n\n\n\n## 常见问题\n\n\n\n### 1、set 和 get 无法得到相同值？\n\n用户使用了 cache 模块，默认是内存式的，例如在本地用 dev 模式，由于是单进程的，那 set 和 ge t最终能达到相同的值。但是用户部署到服务器上面后，由于会有多 worker，相当于第一次请求，落在进程1上，然后第二次落在进程2上，这样获得到空了。\n\n\n解决办法：参考 其他 Cache 的章节，配置 store 为分布式，例如 redis 的 store 等方式。\n\n<!-- File: site/docs/extensions/caching.md (Tokens: ~4035 | Category: anthropic-skill) -->\n# 缓存\n\n缓存是一个伟大而简单的技术，有助于提高你的应用程序的性能。本组件提供了缓存相关的能力，你可以将数据缓存到不同的数据源，也可以针对不同场景建立多级缓存，提高数据访问速度。\n\n:::tip\n\nMidway 提供基于 [cache-manager v5](https://github.com/node-cache-manager/node-cache-manager) 模块重新封装了缓存组件，原有的缓存模块基于 v3 开发不再迭代，如需查看老文档，请访问 [这里](/docs/extensions/cache)。\n\n:::\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 安装\n\n首先安装相关的组件模块。\n\n```bash\n$ npm i @midwayjs/cache-manager@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/cache-manager\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 启用组件\n\n\n首先，引入组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as cacheManager from '@midwayjs/cache-manager';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    cacheManager,\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n\n\n## 使用缓存\n\n\n\n### 1、配置缓存\n\n在使用前，你需要配置缓存所在的位置，比如内置的内存缓存，或者是引入 Redis 缓存，每个缓存对应了一个缓存的 Store。\n\n下面的示例代码，配置了一个名为 `default` 的内存缓存。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: 'memory',\n      },\n    },\n  }\n}\n```\n\n最常用的场景下，缓存会包含两个参数，配置 `max` 修改缓存的数量，配置 `ttl` 修改缓存的过期时间，单位毫秒。\n\n```typescript\n // src/config/config.default.ts\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: 'memory',\n        options: {\n          max: 100,\n          ttl: 10,\n        },\n      },\n    },\n  }\n}\n```\n\n:::tip\n\n* `ttl` 的单位是毫秒\n* `max` 代表缓存 key 的最大个数\n* 不同的 Store 淘汰 key 的算法不同，内存缓存使用的淘汰算法是 LRU\n\n:::\n\n### 2、使用缓存\n\n可以通过服务工厂的装饰器获取到实例，可以通过简单的 `get` 和 `set` 方法获取和保存缓存。\n\n```typescript\nimport { InjectClient, Provide } from '@midwayjs/core';\nimport { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n\n  @InjectClient(CachingFactory, 'default')\n  cache: MidwayCache;\n\n  async invoke(name: string, value: string) {\n    // 设置缓存\n    await this.cache.set(name, value);\n    // 获取缓存\n    const data = await this.cache.get(name);\n    // ...\n  }\n}\n\n```\n\n动态设置 `ttl` 过期时间。\n\n```typescript\nawait this.cache.set('key', 'value', 1000);\n```\n\n若要禁用缓存过期，可以将 `ttl` 配置属性设置为 0。\n\n```typescript\nawait this.cache.set('key', 'value', 0);\n```\n\n删除单个缓存。\n\n```typescript\nawait this.cache.del('key');\n```\n\n清理整个缓存，可以使用 `reset` 方法。\n\n```typescript\nawait this.cacheManager.reset();\n```\n\n:::danger\n\n注意，清理整个缓存非常危险，如果使用了 Redis 作为缓存 store，将清空整个 Redis 数据。\n\n:::\n\n除了装饰器之外，也可以通过 API 获取缓存实例。\n\n```typescript\nimport { InjectClient, Provide } from '@midwayjs/core';\nimport { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  cachingFactory: CachingFactory;\n\n  async invoke() {\n    const caching = await this.cachingFactory.get('default');\n    // ...\n  }\n}\n```\n\n\n\n### 3、配置多个缓存\n\n和其他组件一样，组件支持配置多个缓存实例。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: 'memory',\n      },\n      otherCaching: {\n        store: 'memory',\n      }\n    },\n  }\n}\n```\n\n可以注入不同的缓存实例。\n\n```typescript\nimport { InjectClient, Provide } from '@midwayjs/core';\nimport { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n\n  @InjectClient(CachingFactory, 'default')\n  cache: MidwayCache;\n  \n  @InjectClient(CachingFactory, 'otherCaching')\n  customCaching: MidwayCache;\n\n}\n\n```\n\n\n\n### 4、配置不同 Store\n\n组件基于 [cache-manager](https://github.com/node-cache-manager/node-cache-manager) 可以配置不同的缓存 Store，比如最常见的可以配置 Redis Store。\n\n假如项目已经配置了一个 `Redis`，通过组件内置的 `createRedisStore` 方法，可以快速创建一个 Redis Store。\n\n\n```typescript\nimport { createRedisStore } from '@midwayjs/cache-manager';\n\n// src/config/config.default.ts\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: createRedisStore('default'),\n        options: {\n          ttl: 10,\n        }\n      },\n    },\n  },\n  redis: {\n    clients: {\n      default: {\n        port: 6379,\n        host: '127.0.0.1',\n      }\n    }\n  }\n}\n```\n\n`createRedisStore` 方法可以传递一个已经配置的 redis 实例名，可以和 redis 组件复用实例。\n\n\n\n### 5、配置三方 Store\n\n除了 Redis 之外，用户也可以自行选择 Cache-Manager 的 Store，列表可以参考 [这里](https://github.com/node-cache-manager/node-cache-manager?tab=readme-ov-file#store-engines)。\n\n下面是一个配置  [node-cache-manager-ioredis-yet](https://github.com/node-cache-manager/node-cache-manager-ioredis-yet) 的例子。\n\n```typescript\n// src/config/config.default.ts\nimport { redisStore } from 'cache-manager-ioredis-yet';\n\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: redisStore,\n        options: {\n          port: 6379,\n          host: 'localhost',\n          ttl: 10,\n        },\n      },\n    },\n  }\n}\n```\n\n\n\n### 6、多级缓存\n\n[cache-manager](https://github.com/node-cache-manager/node-cache-manager) 支持将多个缓存 Store 聚合到一起，实现多级缓存。\n\n下面的示例将内存缓存和 Redis 缓存组合到一起，分别设置 60 秒和 600 秒的默认过期时间。\n\n```typescript\n// src/config/config.default.ts\nimport { createRedisStore } from '@midwayjs/cache-manager';\nexport default {\n  cacheManager: {\n    clients: {\n      memoryCaching: {\n        store: 'memory',\n        options: {\n          ttl: 60_000, // 60 秒，单位为毫秒\n        },\n      },\n      redisCaching: {\n        store: createRedisStore('default'),\n        options: {\n          ttl: 600_000, // 600 秒，单位为毫秒\n        },\n      },\n      multiCaching: {\n        store: ['memoryCaching', 'redisCaching'],\n      },\n    },\n  },\n  redis: {\n    clients: {\n      default: {\n        port: 6379,\n        host: '127.0.0.1',\n      },\n    },\n  },\n};\n\n```\n\n这样 `multiCaching` 这个缓存实例就包含了两级缓存，缓存的优先级从上到下，在查找时，会先查找 `memoryCaching` ，如果内存缓存不存在 key，则继续查找 `redisCaching`。\n\n多级缓存自身的 `options.ttl` 不参与 `set`、`mset` 的写入，请在各个子缓存的 `options.ttl` 中配置默认过期时间。内置内存 Store 和 `createRedisStore` 使用的 TTL 单位都是毫秒。\n\n\n\n### 7、使用多级缓存\n\n和普通缓存类似，多级缓存除了 `set`、`get`、`del`方法外，还增加了 `mset` 、`mget`、`mdel` 方法。\n\n```typescript\nimport { InjectClient, Provide } from '@midwayjs/core';\nimport { CachingFactory, MidwayMultiCache } from '@midwayjs/cache-manager';\n\nconst userId2 = 456;\nconst key2 = 'user_' + userId;\nconst ttl = 5;\n\n@Provide()\nexport class UserService {\n\n  @InjectClient(CachingFactory, 'multiCaching')\n  multiCache: MidwayMultiCache;\n\n  async invoke() {\n    // 设置到所有级别的缓存\n    await this.multiCache.set('foo2', 'bar2', ttl);\n\n    // 从最高优先级的缓存 Store 中获取 key\n    console.log(await this.multiCache.get('foo2'));\n    // >> \"bar2\"\n\n    // 调用每一个 Store 的 del 方法进行删除\n    await this.multiCache.del('foo2');\n\n    // 在所有缓存中设置多个 key，可以多个键值对\n    await this.multiCache.mset(\n      [\n        ['foo', 'bar'],\n        ['foo2', 'bar2'],\n      ],\n      ttl\n    );\n\n    // mget() 从最高优先级的缓存中获取值\n    // 如果第一个缓存 Store 中不包含所有的 key，\n    // 继续在下一个缓存 Store 中查找没有找到的 key。\n    // 这是递归地完成的，直到:\n    // - 所有的 key 都已经查找到值\n    // - 所有的缓存 Store 都被查找过\n    console.log(await this.multiCache.mget('key', 'key2'));\n    // >> ['bar', 'bar2']\n\n    // 调用每一个 Store 的 mdel 方法进行删除\n    await this.multiCache.mdel('foo', 'foo2');\n  }\n}\n\n```\n\n对于上面的内存和 Redis 缓存，调用 `set` 或 `mset` 时省略 TTL 参数，各层会使用自己的默认过期时间；显式传入 TTL 时，本次写入的所有缓存层都会使用该值。\n\n```typescript\n// 使用各层默认 TTL：内存 60 秒，Redis 600 秒\nawait this.multiCache.set('key', 'value');\n\n// 覆盖各层默认 TTL：本次写入的两层缓存都使用 30 秒\nawait this.multiCache.set('key', 'value', 30_000);\n```\n\n### 8、自动刷新\n\n不管是普通缓存还是多级缓存，都支持后台刷新功能，只需要配置 `refreshThreshold` 的时间，单位为毫秒。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  cacheManager: {\n    clients: {\n      default: {\n        store: 'memory',\n        options: {\n          refreshThreshold: 3 * 1000,\n        },\n      },\n    },\n  }\n}\n```\n\n如果设置了 `refreshthreshold`，每次从缓存获取值之后，会检查 `ttl` 的值，如果剩余的 `ttl` 小于 `refreshthreshold` ，则系统将异步更新缓存，同时系统会返回旧值，直到 `ttl` 过期。\n\n:::tip\n\n* 在多级缓存的情况下，根据优先级顺序找到第一个包含 key 的 Store。\n\n* 如果阈值较低且执行的函数比较慢，key 可能会过期，有可能会遇到并发更新的情况。\n\n* 后台刷新机制目前只支持单个 key。\n\n* 如果没有为 key 设置 `ttl`，则不会触发刷新机制。对于 redis，`ttl`  默认设置为-1。\n\n:::\n\n\n\n## 自动缓存\n\n### 使用装饰器缓存方法\n\n可以通过 `@Caching` 装饰器缓存方法的结果，比如缓存 http 响应或者服务调用的结果。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Caching } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n  @Caching('default')\n  async getUser(name: string) {\n    return name;\n  }\n}\n\n```\n\n当第一次调用 `getUser` 方法时，会正常执行逻辑，返回结果，装饰器会将结果缓存起来，第二次执行时，如果缓存未失效，则会从缓存中直接返回。\n\n### 指定缓存的 ttl\n\n也可以单独设置 `ttl` 。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Caching } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n  @Caching('default', 100)\n  async getUser(name: string) {\n    return name;\n  }\n}\n```\n\n### 手动指定缓存 key\n\n如果对自动生成的 key 不满意，可以手动指定缓存的 key。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Caching } from '@midwayjs/cache-manager';\n\n@Provide()\nexport class UserService {\n  @Caching('default', 'customKey', 100)\n  async getUser(name: string) {\n    return name;\n  }\n}\n```\n\n### 带逻辑的缓存\n\n如果你希望根据一些特定逻辑进行缓存，比如特定参数，或者特定的 Header，可以传递一个工具函数进行逻辑判断。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { Caching } from '@midwayjs/cache-manager';\n\nfunction cacheBy({methodArgs, ctx, target}) {\n  if (methodArgs[0] === 'harry' || methodArgs[0] === 'mike') {\n    return 'cache1';\n  }\n}\n\n@Provide()\nexport class UserService {\n  @Caching('default', cacheBy, 100)\n  async getUser(name: string) {\n    return 'hello ' + name;\n  }\n}\n```\n\n上面的示例中，`cacheBy` 方法自定义了缓存的逻辑，当方法入参值为 `harry` 或者 `mike` 时，将返回缓存的 `key` ，而其他参数时则跳过缓存。\n\n这个时候执行的结果为：\n\n```typescript\nawait userService.getUser('harry'));\t// hello harry\nawait userService.getUser('mike'));\t// hello harry\nawait userService.getUser('lucy'));\t// hello lucy\n```\n\n`@Caching` 装饰器可以在第二个参数中传递一个方法，这个方法的入参 options 为：\n\n* `methodArgs`  当前调用方法的实际参数\n* `ctx` 如果是请求作用域，则是当前调用的上下文对象，如果是单例，则该对象为空对象\n* `target` 当前调用的实例\n\n方法的返回值为字符串或者布尔值，当返回字符串时，表示以该 key 将方法的结果缓存，当返回 `undefined` 或者 `null` 时，表示跳过缓存。\n\n通过这些参数判断，我们可以实现非常灵活的自定义缓存逻辑。\n\n\n\n## 常见问题\n\n\n\n### 1、多进程下内存缓存 set 和 get 无法得到相同值\n\n这是正常现象，每个进程的数据是独立的，仅保存在当前进程中。如需跨进程缓存，请使用 Redis 这类分布式缓存系统。\n\n<!-- File: site/docs/extensions/captcha.md (Tokens: ~2420 | Category: anthropic-skill) -->\n# 验证码\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的通用验证码组件，支持 `图片验证码`、`计算表达式` 等类型验证码。\n\n您也可以通过此组件，来实现 `短信验证码`、`邮件验证码` 等验证能力，但是注意，本组件本身不含发送短信、邮件功能。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/captcha@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/captcha\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n## 启用组件\n\n在 `src/configuration.ts` 中引入组件。\n\n```typescript\nimport * as captcha from '@midwayjs/captcha';\n\n@Configuration({\n  imports: [\n    // ...other components\n    captcha\n  ],\n})\nexport class MainConfiguration {}\n```\n\n## 调用服务\n\n```typescript\nimport { Controller, Inject } from '@midwayjs/core';\nimport { CaptchaService } from '@midwayjs/captcha';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  ctx;\n\n  @Inject()\n  captchaService: CaptchaService;\n\n  // 示例：获取图像验证码\n  @Get('/get-image-captcha')\n  async getImageCaptcha() {\n    const { id, imageBase64 } = await this.captchaService.image({ width: 120, height: 40 });\n    return {\n      id,          // 验证码 id\n      imageBase64, // 验证码 SVG 图片的 base64 数据，可以直接放入前端的 img 标签内\n    }\n  }\n\n  // 示例：获取计算表达式验证码\n  @Get('/get-formula-captcha')\n  async getFormulaCaptcha() {\n    const { id, imageBase64 } = await this.captchaService.formula({ noise: 1 });\n    return {\n      id,          // 验证码 id\n      imageBase64, // 验证码 SVG 图片的 base64 数据，可以直接放入前端的 img 标签内\n    }\n  }\n\n  // 验证验证码是否正确\n  @Post('/check-captcha')\n  async getCaptcha() {\n    const { id, answer } = this.ctx.request.body;\n    const passed: boolean = await this.captchaService.check(id, answer);\n    if (passed) {\n      return 'passed';\n    }\n    return 'error';\n  }\n\n  // 示例：短信验证码\n  @Post('/sms-code')\n  async sendSMSCode() {\n    // 验证验证码是否正确\n    const { id, text: code } = await this.captchaService.text({ size: 4 });\n    await sendSMS(18888888888, code);\n    return { id }\n  }\n\n  // 示例：邮件验证码\n  @Post('/email-code')\n  async sendEmailCode() {\n    // 验证验证码是否正确\n    const { id, text: code } = await this.captchaService.text({ type: 'number'});\n    await sendEmail('admin@example.com', code);\n    return { id }\n  }\n\n   // 示例：将任意文本内容塞入验证码中\n  @Get('/test-text')\n  async testText() {\n    // 存入内容，获取验证码id\n    const id: string = await this.captchaService.set('123abc');\n    // 根据验证码id，校验内容是否正确\n    const passed: boolean = await this.captchaService.check(id, '123abc');\n    return {\n      passed: passed === true,\n    }\n  }\n}\n```\n\n## 可用配置\n\n```typescript\ninterface CaptchaOptions {\n  default?: { // 默认配置\n    // 验证码字符长度，默认 4 个字符\n    size?: number;\n    // 干扰线条的数量，默认 1 条\n    noise?: number;\n    // 宽度，默认为 120 像素\n    width?: number;\n    // 宽度，默认为 40 像素\n    height?: number;\n    // 图形验证码配置，图形中包含一些字符\n  },\n  image?: {\n    // 验证码字符长度，默认 4 个字符\n    size?: number;\n    // 图像验证码中的字符类型，默认为 'mixed'\n    // - 'mixed' 表示 0-9、A-Z 和 a-z\n    // - 'letter' 表示 A-Z 和 a-z\n    // - 'number' 表示 0-9\n    type?: 'mixed',\n    // 干扰线条的数量，默认 1 条\n    noise?: number;\n    // 宽度，默认为 120 像素\n    width?: number;\n    // 宽度，默认为 40 像素\n    height?: number;\n  },\n  // 计算公式验证码配置，例如返回的图像内容为 1+2，需要用户填入 3\n  formula?: {\n     // 干扰线条的数量，默认 1 条\n    noise?: number;\n    // 宽度，默认为 120 像素\n    width?: number;\n    // 宽度，默认为 40 像素\n    height?: number;\n  },\n  // 纯文本验证码配置，基于纯文本验证码可以实现短信验证码、邮件验证码\n  text?: {\n    // 验证码字符长度，默认 4 个字符\n    size?: number;\n    // 文本验证码中的字符类型，默认为 'mixed'\n    // - 'mixed' 表示 0-9、A-Z 和 a-z\n    // - 'letter' 表示 A-Z 和 a-z\n    // - 'number' 表示 0-9\n    type?: 'mixed',\n  },\n  // 验证码过期时间，默认为 1h\n  expirationTime?: 3600,\n  // 验证码存储的 key 前缀\n  idPrefix: 'midway:vc',\n}\n\nexport const captcha: CaptchaOptions = {\n  default: { // 默认配置\n    size: 4,\n    noise: 1,\n    width: 120,\n    height: 40,\n  },\n  image: {      // 最终会合并 default 配置\n    type: 'mixed',\n  },\n  formula: {},  // 最终会合并 default 配置\n  text: {},     // 最终会合并 default 配置\n  expirationTime: 3600,\n  idPrefix: 'midway:vc',\n}\n```\n\n更多配置请参考 [svg-captcha](https://github.com/produck/svg-captcha)。\n\n### 配置示例一\n\n获取一个 包含 `5个纯英文字母` 的图像验证码，图像宽度 `200` 像素，高度 `50` 像素，并且包含 `3` 条干扰线。\n\n因为图像验证码的配置 `image`， 会与 `default` 配置进行合并，所以可以只修改 `default` 配置：\n\n```typescript\nexport const captcha: CaptchaOptions = {\n  default: {\n    size: 5,\n    noise: 3,\n    width: 200,\n    height: 50\n  },\n  image: {\n    type: 'letter'\n  }\n}\n```\n当然，也可以 `不` 修改 `default` 配置，将宽度、高度等在 `image` 配置项中进行配置，取得 `同样`的效果：\n```typescript\nexport const captcha: CaptchaOptions = {\n  image: {\n    size: 5,\n    noise: 3,\n    width: 200,\n    height: 50\n    type: 'letter'\n  }\n}\n```\n\n### 配置示例二\n\n获取一个图像宽度 `100` 像素，高度 `60` 像素，并且包含 `2` 条干扰线的计算表达式验证码。\n\n因为计算表达式验证码的配置 `formula` ，会与 `default` 配置合并，所以可以只修改 `default` 配置：\n\n```typescript\nexport const captcha: CaptchaOptions = {\n  default: {\n    noise: 2,\n    width: 100,\n    height: 60\n  },\n}\n```\n当然，也可以 `不` 修改 `default` 配置，将宽度、高度等在 `formula` 配置项中进行配置，取得 `同样`的效果：\n```typescript\nexport const captcha: CaptchaOptions = {\n  formula: {\n    noise: 2,\n    width: 100,\n    height: 60\n  }\n}\n```\n\n\n## 组件依赖\n\n验证码的内容存储基于 `@midwayjs/cache-manager` 组件，默认创建了一个名为 `captcha` 的缓存实例，将数据存储在 `memory` 中。\n\n```typescript\nexport default {\n  cacheManager: {\n    clients: {\n      captcha: {\n        store: 'memory',\n      },\n    },\n  },\n};\n```\n\n如果要替换为 `redis` 或其他服务，请参照 `@midwayjs/cache-manager` 的 [文档](/docs/extensions/caching)，对 cache 进行配置。\n\n\n\n\n## 效果\n\n**图片验证码**\n\n![图片验证码](https://gw.alicdn.com/imgextra/i4/O1CN014cEzLH23vEniOgoyp_!!6000000007317-2-tps-120-40.png)\n\n**计算表达式**\n\n ![计算表达式](https://gw.alicdn.com/imgextra/i4/O1CN01u3Mj0q24lRx1md9pX_!!6000000007431-2-tps-120-40.png)\n\n\n## 注意\n\n* 为了防止机器学习破解，使用的 `svg-captcha` 包为 [安全修复后](https://juejin.cn/post/6872656117839691789) 的版本。\n\n<!-- File: site/docs/extensions/casbin.md (Tokens: ~3833 | Category: anthropic-skill) -->\n# 角色鉴权\n\nCasbin 是一个强大的、高效的开源访问控制框架，其权限管理机制支持多种访问控制模型。\n\n官网文档：https://casbin.org/\n\n\n\n## Casbin 是什么\n\nCasbin 可以：\n\n1. 支持自定义请求的格式，默认的请求格式为`{subject, object, action}`。\n2. 具有访问控制模型model和策略policy两个核心概念。\n3. 支持RBAC中的多层角色继承，不止主体可以有角色，资源也可以具有角色。\n4. 支持内置的超级用户 例如：`root` 或 `administrator`。超级用户可以执行任何操作而无需显式的权限声明。\n5. 支持多种内置的操作符，如 `keyMatch`，方便对路径式的资源进行管理，如 `/foo/bar` 可以映射到 `/foo*`\n\nCasbin 不能：\n\n1. 身份认证 authentication（即验证用户的用户名和密码），Casbin 只负责访问控制。应该有其他专门的组件负责身份认证，然后由 Casbin 进行访问控制，二者是相互配合的关系。\n2. 管理用户列表或角色列表。 Casbin 认为由项目自身来管理用户、角色列表更为合适， 用户通常有他们的密码，但是 Casbin 的设计思想并不是把它作为一个存储密码的容器。 而是存储RBAC方案中用户和角色之间的映射关系。\n\n:::tip\n\n注意：\n\n- 1、在 Midway v3.6.0 之后可用\n- 2、Midway 只是封装了 Casbin 的 API 并提供简单的支持，策略规则编写请查看 [官方文档](https://casbin.org/)\n- 3、Casbin 不提供登录，只提供现有用户的鉴权，需要搭配 passport 等获取用户信息的组件来使用\n\n:::\n\n\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/casbin@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/casbin\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 启用组件\n\n\n首先，引入组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as casbin from '@midwayjs/casbin';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    casbin,\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n\n\n## 准备模型和策略\n\n使用 Casbin 前需要定义模型和策略，这两个文件的内容贯穿本文，建议先去官网了解相关内容。\n\n我们以一个基础的模型为例，比如：\n\n```\n[request_definition]\nr = sub, obj, act\n\n[policy_definition]\np = sub, obj, act\n\n[role_definition]\ng = _, _\ng2 = _, _\n\n[policy_effect]\ne = some(where (p.eft == allow))\n\n[matchers]\nm = g(r.sub, p.sub) && g2(r.obj, p.obj) && r.act == p.act || r.sub == \"root\"\n```\n\n将其保存在项目根目录的 `basic_model.conf` 文件中。\n\n以及包含下面内容的策略文件。\n\n```\np, superuser, user, read:any\np, manager, user_roles, read:any\np, guest, user, read:own\n\ng, alice, superuser\ng, bob, guest\ng, tom, manager\n\ng2, users_list, user\ng2, user_roles, user\ng2, user_permissions, user\ng2, roles_list, role\ng2, role_permissions, role\n```\n\n将其保存在项目根目录的 `basic_policy.csv` 文件中。\n\n\n\n## 配置模型和策略\n\n这里我们的策略将以文件形式进行演示。\n\n配置如下：\n\n```typescript\nimport { MidwayAppInfo } from '@midwayjs/core';\nimport { join } from 'path';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // ...\n    casbin: {\n      modelPath: join(appInfo.appDir, 'basic_model.conf'),\n      policyAdapter: join(appInfo.appDir, 'basic_policy.csv'),\n    }\n  };\n}\n\n```\n\n\n\n## 装饰器鉴权\n\n有多种形式来使用 Casbin，这里以装饰器作为示例。\n\n### 定义资源\n\n首先定义资源，比如放在 `src/resource.ts` 文件中，对应策略文件中对应的资源。\n\n```typescript\nexport enum Resource {\n  USERS_LIST = 'users_list',\n  USER_ROLES = 'user_roles',\n  USER_PERMISSIONS = 'user_permissions',\n  ROLES_LIST = 'roles_list',\n  ROLE_PERMISSIONS = 'role_permission',\n}\n```\n\n\n\n### 配置获取用户的方式\n\n在使用装饰器鉴权时，我们需要配置一个获取用户的方式，比如在 passport 组件之后，我们会从 `ctx.user` 上获取用户名。\n\n```typescript\nimport { MidwayAppInfo } from '@midwayjs/core';\nimport { join } from 'path';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // ...\n    casbin: {\n      modelPath: join(appInfo.appDir, 'basic_model.conf'),\n      policyAdapter: join(appInfo.appDir, 'basic_policy.csv'),\n      usernameFromContext: (ctx) => {\n        return ctx.user;\n      }\n    }\n  };\n}\n\n```\n\n\n\n### 增加守卫\n\n装饰器鉴权依赖守卫，我们可以在全局或者某些路由上开启，全局守卫使用请参考守卫章节。\n\n比如，我们只在下面的 `findAllUsers` 方法上开启鉴权，`AuthGuard` 是 `@midwayjs/casbin` 提供的守卫，可以直接使用。\n\n```typescript\nimport { Controller, Get, UseGuard } from '@midwayjs/core';\nimport { AuthGuard } from '@midwayjs/casbin';\nimport { Resource } from './resouce';\n\n@Controller('/')\nexport class HomeController {\n\n  @UseGuard(AuthGuard)\n  @Get('/users')\n  async findAllUsers() {\n    // ...\n  }\n}\n```\n\n\n\n### 定义权限\n\n使用 `UsePermission` 装饰器定义路由需要的权限。\n\n```typescript\nimport { Controller, Get, UseGuard } from '@midwayjs/core';\nimport { AuthActionVerb, AuthGuard, AuthPossession, UsePermission } from '@midwayjs/casbin';\nimport { Resource } from './resouce';\n\n@Controller('/')\nexport class HomeController {\n\n  @UseGuard(AuthGuard)\n  @UsePermission({\n    action: AuthActionVerb.READ,\n    resource: Resource.USER_ROLES,\n    possession: AuthPossession.ANY\n  })\n  @Get('/users')\n  async findAllUsers() {\n    // ...\n  }\n}\n```\n\n没有权限读取 `USER_ROLES` 的用户不能调用 findAllUsers 方法，在请求时会返回 403 状态码。\n\n比如，上面的 `bob` 用户访问则会返回 403， 而 `tom` 用户访问则正常返回。\n\n\n\n`UsePermission` 需要提供一个对象参数，包括 `action`、`resource`、`possession` 和一个可选的 `isOwn` 的对象。\n\n- `action` 是一个 `AuthActionVerb` 枚举，包含读，写等操作\n- `resource` 资源字符串\n- `possession` 是一个 `AuthPossession` 枚举\n- `isOwn` 是一个接受`Context`（守卫 `canActivate`的参数）作为唯一参数并返回布尔值的函数。 `AuthZGuard` 使用它来确定用户是否是资源的所有者。 如果未定义，将使用返回 `false` 的默认函数。\n\n可以同时定义多个权限，但只有当所有权限都满足时，才能访问该路由。\n\n比如：\n\n```typescript\n@UsePermissions({\n  action: AuthActionVerb.READ,\n  resource: 'USER_ADDRESS',\n  possession: AuthPossession.ANY\n}, {\n  action; AuthActionVerb.READ,\n  resource: 'USER_ROLES,\n  possession: AuthPossession.ANY\n})\n```\n\n只有当用户被授予读取 `USER_ADDRESS` 和 `USER_ROLES` 这两个权限时，才能访问该路由。\n\n\n\n## API 鉴权\n\nCasbin 本身提供了一些通用的 API 和权限相关的功能。\n\n我们可以通过直接注入 `CasbinEnforcerService` 服务来使用。\n\n比如，我们可以在守卫或者中间件中编码。\n\n```typescript\nimport { CasbinEnforcerService } from '@midwayjs/casbin';\nimport { Guard, IGuard } from '@midwayjs/core';\n\n@Guard()\nexport class UserGuard extends IGuard {\n\n  @Inject()\n  casbinEnforcerService: CasbinEnforcerService;\n\n  async canActivate(ctx, clz, methodName) {\n    // 用户登录了，并且是特定的方法，则检查权限\n    if (ctx.user && methodName === 'findAllUsers') {\n      return await this.casbinEnforcerService.enforce(ctx.user, 'USER_ROLES', 'read');\n    }\n    // 未登录用户不允许访问\n    return false;\n  }\n}\n```\n\n在启用守卫后，效果和上面的装饰器相同。\n\n此外，`CasbinEnforcerService` 还有更多的 API，比如重新加载策略。\n\n```typescript\nawait this.casbinEnforcerService.loadPolicy();\n```\n\n\n\n## 分布式策略存储\n\n在多台机器部署的场景下，需要将策略存储到外部。\n\n当前已经实现的适配器有：\n\n- Redis\n- Typeorm\n\n\n\n### Redis Adapter\n\n需要依赖 `@midwayjs/casbin-redis-adapter` 包和 redis 组件。\n\n```bash\n$ npm i @midwayjs/casbin-redis-adapter @midwayjs/redis --save\n```\n\n启用 redis 组件。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as redis from '@midwayjs/redis';\nimport * as casbin from '@midwayjs/casbin';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    redis,\n    casbin,\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n配置 redis 连接和 casbin 适配器。\n\n```typescript\nimport { MidwayAppInfo } from '@midwayjs/core';\nimport { join } from 'path';\nimport { createAdapter } from '@midwayjs/casbin-redis-adapter';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // ...\n    redis: {\n      clients: {\n        // 为 casbin 定义了一个连接\n        'node-casbin-official': {\n          host: '127.0.0.1',\n          port: 6379,\n          password: '',\n          db: '0',\n        }\n      }\n    },\n    casbin: {\n      policyAdapter: createAdapter({\n        // 配置了上面的连接名\n        clientName: 'node-casbin-official'\n      }),\n      // ...\n    },\n  };\n}\n\n```\n\n\n\n### TypeORM Adapter\n\n需要依赖 `@midwayjs/casbin-typeorm-adapter` 包和 typeorm 组件。\n\n```\n$ npm i @midwayjs/casbin-typeorm-adapter @midwayjs/typeorm typeorm@^1.0.0 better-sqlite3 --save\n```\n\n启用 typeorm 组件。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as typeorm from '@midwayjs/typeorm';\nimport * as casbin from '@midwayjs/casbin';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    typeorm,\n    casbin,\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n配置适配器，下面以 better-sqlite3 存储为例，mysql 的配置可以查看 typeorm 组件。\n\n```typescript\nimport { MidwayAppInfo } from '@midwayjs/core';\nimport { join } from 'path';\nimport { CasbinRule, createAdapter } from '@midwayjs/casbin-typeorm-adapter';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // ...\n    typeorm: {\n      dataSource: {\n        // 为 casbin 定义了一个连接\n        'node-casbin-official': {\n          type: 'better-sqlite3',\n          synchronize: true,\n          database: join(appInfo.appDir, 'casbin.sqlite'),\n          // 注意这里显式引入了 Entity\n          entities: [CasbinRule],\n        }\n      }\n    },\n    casbin: {\n      policyAdapter: createAdapter({\n        // 配置了上面的连接名\n        dataSourceName: 'node-casbin-official'\n      }),\n      // ...\n    }\n  };\n}\n```\n\n\n\n## 监视器\n\n使用分布式消息系统，例如 [etcd](https://github.com/coreos/etcd) 来保持多个Casbin执行器实例之间的一致性。 因此，我们的用户可以同时使用多个Casbin 执行器来处理大量的权限检查请求。\n\nMidway 当前只提供一种 Redis 更新策略，如有其他需求，可以给我们提交 issue。\n\n### Redis Watcher\n\n需要依赖 `@midwayjs/casbin-redis-adapter` 包和 redis 组件。\n\n```bash\n$ npm i @midwayjs/casbin-redis-adapter @midwayjs/redis --save\n```\n\n启用 redis 组件。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as redis from '@midwayjs/redis';\nimport * as casbin from '@midwayjs/casbin';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [\n    // ...\n    redis,\n    casbin,\n  ],\n  // ...\n})\nexport class MainConfiguration {\n}\n```\n\n使用示例：\n\n```typescript\nimport { MidwayAppInfo } from '@midwayjs/core';\nimport { join } from 'path';\nimport { createAdapter, createWatcher } from '@midwayjs/casbin-redis-adapter';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // ...\n    redis: {\n      clients: {\n        'node-casbin-official': {\n          host: '127.0.0.1',\n          port: 6379,\n          db: '0',\n        },\n        'node-casbin-sub': {\n          host: '127.0.0.1',\n          port: 6379,\n          db: '0',\n        }\n      }\n    },\n    casbin: {\n      // ...\n      policyAdapter: createAdapter({\n        clientName: 'node-casbin-official'\n      }),\n      policyWatcher: createWatcher({\n        pubClientName: 'node-casbin-official',\n        subClientName: 'node-casbin-sub',\n      })\n    },\n  };\n}\n```\n\n注意，pub/sub 连接需要不同的客户端，上面代码定义了两个客户端。\n\npub 客户端可以和普通 Redis 客户端连接复用，而 sub 需要一个独立的客户端。\n\n<!-- File: site/docs/extensions/cfork.md (Tokens: ~506 | Category: anthropic-skill) -->\n# cfork\n\n很多同学没有听过 cfork，cfork 库是 egg-scripts 中用于启动主进程的库，是 egg 使用的基础库之一，他的功能是启动进程，并维持多个进程的保活。\n\n\n文档在此：[https://github.com/node-modules/cfork](https://github.com/node-modules/cfork)\n\n\n由于 bootstrap.js 的特性，有时候不是很适合 pm2 来部署（比如集团内部，全局不安装，需要 API 启动）。\n\n\n我们可以新增一个 `server.js` 用来做主进程的入口，将 `bootstrap.js` 作为每个子进程的启动入口。\n\n\n```javascript\n// server.js\n\n'use strict';\n\nconst cfork = require('cfork');\nconst util = require('util');\nconst path = require('path');\nconst os = require('os');\n\n// 获取 cpu 核数\nconst cpuNumbers = os.cpus().length;\n\ncfork({\n  exec: path.join(__dirname, './bootstrap.js'),\n  count: cpuNumbers,\n})\n  .on('fork', (worker) => {\n    console.warn('[%s] [worker:%d] new worker start', Date(), worker.process.pid);\n  })\n  .on('disconnect', (worker) => {\n    console.warn(\n      '[%s] [master:%s] worker:%s disconnect, exitedAfterDisconnect: %s, state: %s.',\n      Date(),\n      process.pid,\n      worker.process.pid,\n      worker.exitedAfterDisconnect,\n      worker.state\n    );\n  })\n  .on('exit', (worker, code, signal) => {\n    const exitCode = worker.process.exitCode;\n    const err = new Error(\n      util.format(\n        'worker %s died (code: %s, signal: %s, exitedAfterDisconnect: %s, state: %s)',\n        worker.process.pid,\n        exitCode,\n        signal,\n        worker.exitedAfterDisconnect,\n        worker.state\n      )\n    );\n    err.name = 'WorkerDiedError';\n    console.error('[%s] [master:%s] worker exit: %s', Date(), process.pid, err.stack);\n  });\n```\n\n最后启动 `node server.js` 即可。\n\n<!-- File: site/docs/extensions/code_dye.md (Tokens: ~1228 | Category: anthropic-skill) -->\n# 代码染色\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的代码染色组件。\n\n用于在 HTTP 场景展示调用链路耗时与各个方法的出入参，帮你更快地定位代码问题。\n\n比如：\n\n+ 代码执行缓慢\n  - 不知道是哪一个方法执行的慢：通过代码染色后，可以查看每一个 `方法的执行时长`。\n+ 代码执行错误\n  - 可能是方法没有调到：通过代码染色后，可以查看每一个 `方法的调用链`。\n  - 可能是方法调用参数出错：通过代码染色后，查看每一个`方法的入参和返回值`\n\n使用效果：\n\n![](https://gw.alicdn.com/imgextra/i1/O1CN017Zd6y628M2PvqJO7I_!!6000000007917-2-tps-2392-844.png)\n\n\n\n\n相关信息：\n\n| web 支持情况      |      |\n| ----------------- | ---- |\n| @midwayjs/koa     | ✅    |\n| @midwayjs/faas    | ✅    |\n| @midwayjs/web     | ✅    |\n| @midwayjs/express | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/code-dye@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/code-dye\": \"^4.0.0\"\n    // ...\n  },\n}\n```\n\n\n\n## 启用组件\n\n将 code-dye 组件配置到代码中。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as codeDye from '@midwayjs/code-dye';\n\n@Configuration({\n  imports: [\n    // ...\n    {\n      component: codeDye,\n      enabledEnvironment: ['local'],\t// 只在本地启用\n    }\n  ],\n})\nexport class MainConfiguration {}\n```\n\n\n\n\n\n:::tip\n\n- 可以在 `本地` 或 `研发` 环境开启本组件，便于开发时定位问题，但是 `不建议` 在线上启用，会对线上访问性能产生影响。\n\n:::\n\n\n\n## 配置染色\n\n可以通过`matchQueryKey` 配置，控制当 `query` 参数包含`matchQueryKey` 配置对应的值的时候，进入染色链路，例如，配置为：\n\n```typescript\n// src/config/config.local.ts\nexport default {\n  codeDye: {\n    matchQueryKey: 'codeDyeABC',\n  }\n}\n```\n当请求接口 `http://127.0.0.1:7001/test?codeDyeABC=html` 时，就会判断 `query` 中是否存在 `codeDyeABC` 参数来决定是否染色，并根据参数对应的值，来响应不同的染色结果。\n\n也可以通过`matchHeaderKey` 配置，控制当 `headers` 参数包含 `matchHeaderKey` 配置对应的值的时候，进入染色链路，例如，配置为：\n\n```typescript\n// src/config/config.local.ts\nexport default {\n  codeDye: {\n    matchHeaderKey: 'codeDyeHeader',\n  }\n}\n```\n当请求接口 `http://127.0.0.1:7001/test` 时，就会判断请求的 `headers` 中是否存在 `codeDyeHeader` 参数来决定是否染色，并根据参数对应的值，来响应不同的染色结果。\n\n\n\n## 染色报告\n\n开启了代码染色后，链路染色的结果，可以通过开启染色的不同参数值来配置，目前支持以下三种：\n\n+ `html`：`对` 当前请求的结果进行处理，将染色信息添加到结果中，响应为 `html`，可以在浏览器上查看，效果可以查看此文档上面的图片效果展示。\n+ `json`：`对` 当前请求的结果进行处理，将染色信息添加到结果中，响应为 `json` 结构化信息。\n+ `log`：`不对` 当前请求的结果进行处理，染色的信息将会输出到日志中，不影响请求。\n\n例如，配置为：\n\n```typescript\n// src/config/config.local.ts\nexport default {\n  codeDye: {\n    matchQueryKey: 'codeDyeXXX',\n  }\n}\n```\n\n当请求接口 `http://127.0.0.1:7001/test?codeDyeXXX=html` 时，就会判断 `query` 中 `codeDyeXXX` 参数的值为 `html`，就将染色结果输出在当前请求的响应中，并且内容为 `html` 格式。\n\n<!-- File: site/docs/extensions/commander.md (Tokens: ~2718 | Category: anthropic-skill) -->\n# 命令行\n\n`@midwayjs/commander` 是一个基于 Midway IoC 容器的命令行组件，底层使用 [commander.js](https://github.com/tj/commander.js#readme) 做参数解析与 help 输出。你可以用 Midway 熟悉的依赖注入方式组织命令、选项解析与业务逻辑，并将命令拆分为多个 Class。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ❌    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ❌   |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n## 安装依赖\n\n在现有项目中安装 commander 组件依赖。\n\n```bash\n$ npm i @midwayjs/commander@4  --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/commander\": \"^4.0.0\"\n  }\n}\n```\n\n## 开启组件\n\n在入口配置中引入组件。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as commander from '@midwayjs/commander';\n\n@Configuration({\n  imports: [commander],\n})\nexport class MainConfiguration {}\n```\n\n\n## 编写命令\n\n目录结构示例：\n\n```bash\n.\n├── src\n│   ├── commands\n│   │   ├── hello.command.ts\n│   │   └── status.command.ts\n│   ├── configuration.ts\n│   └── ...\n├── bootstrap.js\n└── package.json\n```\n\n一个命令对应一个 Class，使用 `@Command()` 修饰，并实现 `CommandRunner` 接口的 `run()` 方法。\n\n\n```typescript\n// src/commands/hello.command.ts\nimport { Inject, ILogger } from '@midwayjs/core';\nimport { Command, CommandRunner, Option } from '@midwayjs/commander';\n\n@Command({\n  name: 'hello',\n  description: 'hello command',\n  arguments: '<name>',\n  aliases: ['hi'],\n})\nexport class HelloCommand implements CommandRunner {\n  @Inject()\n  logger: ILogger;\n\n  @Option({\n    flags: '-f, --foo [foo]',\n    description: 'foo option',\n    defaultValue: 'bar',\n  })\n  parseFoo(val: string) {\n    return `${val}_parsed`;\n  }\n\n  async run(passedParams: string[], options?: Record<string, any>) {\n    const [name] = passedParams;\n    this.logger?.info?.(`hello ${name}`, options);\n  }\n}\n```\n\n### `@Command()` 参数\n\n - `name`：命令名称（命令行调用与 `--help` 展示时使用）\n- `arguments`：命令位置参数声明（例如 `<name>`、`<a> [b]`）\n- `description`：命令描述，会展示在 `--help` 中\n- `argsDescription`：位置参数描述对象，会展示在 `--help` 中\n- `aliases`：命令别名数组\n\n### `@Option()` 参数\n\n- `flags`：选项声明（例如 `-f, --foo [foo]`、`-n, --num <num>`）\n- `description`：选项描述，会展示在 `--help` 中\n- `defaultValue`：默认值（不传该选项时生效）\n- `required`：是否必填（等价于 commander 的 `requiredOption`）\n\n`@Option()` 修饰的方法会作为 commander 的自定义 parser，用于把字符串参数转换为业务需要的类型（例如 number/boolean/自定义格式）。\n\n## 运行命令\n\n该组件作为 Midway Framework 运行，在应用启动时会解析 `process.argv` 并执行匹配的命令。\n\n如果你使用 `bootstrap.js` 作为入口，可以这样启动：\n\n```javascript\n// bootstrap.js\nconst { Bootstrap } = require('@midwayjs/bootstrap');\nBootstrap.run();\n```\n\n然后运行：\n\n```bash\n$ node bootstrap.js hello world --foo baz\n$ node bootstrap.js hi world\n```\n\n## 上下文\n\n每次命令执行都会创建请求上下文（request context），可以在命令类中注入 `Context` 获取运行时信息：\n\n```typescript\nimport { Command, CommandRunner, Context } from '@midwayjs/commander';\nimport { Inject } from '@midwayjs/core';\n\n@Command({ name: 'info', arguments: '<name>' })\nexport class InfoCommand implements CommandRunner {\n  @Inject()\n  ctx: Context;\n\n  async run() {\n    this.ctx.commandName; // 命令名\n    this.ctx.args; // 位置参数数组\n    this.ctx.options; // commander 解析后的选项\n    this.ctx.command; // commander 的 Command 实例\n  }\n}\n```\n\n## 交互式提问（Enquirer）\n\n组件内置了基于 [enquirer](https://github.com/enquirer/enquirer) 的交互式提问能力，适用于运行过程中补齐参数。你可以用 `@QuestionSet()` 组织问题集合，并在命令里通过 `EnquirerService` 触发。\n\n如果希望既支持命令行传参又支持交互补齐，请把对应选项设为可选（否则 commander 会在缺参时直接报错）。\n\n```typescript\nimport {\n  Command,\n  CommandRunner,\n  QuestionSet,\n  Question,\n  ValidateFor,\n  DefaultFor,\n  WhenFor,\n  EnquirerService,\n} from '@midwayjs/commander';\nimport { Inject } from '@midwayjs/core';\n\n@QuestionSet()\nclass ProfileQuestionSet {\n  @Question({ type: 'input', name: 'age', message: 'Your age?' })\n  parseAge(value: string) {\n    return Number.parseInt(value, 10);\n  }\n\n  @Question({ type: 'input', name: 'nickname', message: 'Nickname?' })\n  parseNickname(value: string) {\n    return value;\n  }\n\n  @ValidateFor({ name: 'age' })\n  validateAge(value: string) {\n    return value ? true : 'age required';\n  }\n\n  @DefaultFor({ name: 'nickname' })\n  defaultNickname() {\n    return 'neo';\n  }\n\n  @WhenFor({ name: 'nickname' })\n  whenNickname(answers: Record<string, unknown>) {\n    return Boolean(answers.useNickname);\n  }\n}\n\n@Command({ name: 'ask' })\nexport class AskCommand implements CommandRunner {\n  @Inject()\n  enquirerService: EnquirerService;\n\n  async run(_passedParams: string[], options?: Record<string, any>) {\n    const answers = await this.enquirerService.prompt(ProfileQuestionSet, {\n      useNickname: options?.useNickname,\n    });\n    // use answers.age / answers.nickname\n  }\n}\n```\n\n说明：\n- `@Question()` 修饰的方法会作为 enquirer 的 `result`（用于转换用户输入）。\n- `@DefaultFor()` 会映射到 enquirer 的 `initial`。\n- `@WhenFor()` 支持根据已收集的答案决定是否提问。\n- 可用的 `@*For()` 装饰器：`ValidateFor`、`ChoicesFor`、`MessageFor`、`DefaultFor`、`WhenFor`。\n- `prompt()` 同时支持 `QuestionSet` 名称字符串或类引用，推荐使用类引用。\n\n## 错误处理\n\n默认情况下，CLI 入口会捕获异常并输出日志后退出进程。你可以在配置里提供 `errorHandler` 来接管错误处理：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  commander: {\n    errorHandler: (err: Error) => {\n      console.error(err);\n      process.exit(1);\n    },\n  },\n};\n```\n\n如果你在命令里使用了 `@Catch()` 错误过滤器（Midway Filter），会先走过滤器逻辑，再进入这里的兜底处理。\n\n## 返回值与输出\n\n默认情况下，命令执行完毕后，如果 `run()` 有返回值，框架会将返回值输出到标准输出（stdout），方便用作脚本管道或在测试中断言输出内容。\n\n支持的返回值类型：\n\n- `string` / `Buffer`：直接写入 stdout\n- 普通对象：使用 `JSON.stringify` 后写入 stdout\n- `Readable`：会 pipe 到 stdout\n- `AsyncIterable`：会按迭代顺序逐段写入 stdout\n\n下面是一个返回文本/JSON 的例子（使用 core 的 `ServerResponse` 语义来组织输出格式）：\n\n```typescript\nimport { Command, CommandRunner, CliServerResponse } from '@midwayjs/commander';\n\n@Command({ name: 'status' })\nexport class StatusCommand implements CommandRunner {\n  async run() {\n    return new CliServerResponse({} as any).success().json({ ok: true });\n  }\n}\n```\n\n如果希望按 chunk 逐步输出，可以返回 `CliServerResponse().stream()`：\n\n```typescript\nimport { Command, CommandRunner, CliServerResponse } from '@midwayjs/commander';\n\n@Command({ name: 'stream' })\nexport class StreamCommand implements CommandRunner {\n  async run() {\n    const response = new CliServerResponse({} as any);\n    const stream = response.stream();\n\n    setImmediate(() => {\n      stream.send('a');\n      stream.send({ b: 2 });\n      stream.end();\n    });\n\n    return stream;\n  }\n}\n```\n\n## 日志\n\n组件默认会注册一个名为 `commanderLogger` 的 logger，默认写入 `midway-commander.log`。\n\n你可以在命令类里通过 `@Logger('commanderLogger')` 注入使用，例如：\n\n```typescript\nimport { Logger, ILogger } from '@midwayjs/core';\nimport { Command, CommandRunner } from '@midwayjs/commander';\n\n@Command({ name: 'hello', arguments: '<name>' })\nexport class HelloCommand implements CommandRunner {\n  @Logger('commanderLogger')\n  logger: ILogger;\n\n  async run(passedParams: string[]) {\n    this.logger.info('hello %s', passedParams[0]);\n  }\n}\n```\n\n如果希望自定义日志文件名或级别，可以在应用配置中覆盖 `midwayLogger.clients.commanderLogger`：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  midwayLogger: {\n    clients: {\n      commanderLogger: {\n        fileLogName: 'my-commander.log',\n        level: 'info',\n      },\n    },\n  },\n};\n```\n\n## 单元测试\n\n命令行参数在测试环境中很容易被 Jest/Node 参数污染，推荐通过 framework 的 `runCommand()` 来执行命令，而不是直接 mock `process.argv`。\n\n```typescript\nimport { createLightApp, close } from '@midwayjs/mock';\nimport { Framework } from '@midwayjs/commander';\nimport * as commander from '@midwayjs/commander';\n\ndescribe('commander', () => {\n  it('should run command', async () => {\n    const app = await createLightApp({\n      imports: [commander],\n      preloadModules: [HelloCommand],\n    });\n\n    const framework = app.getFramework() as Framework;\n    await framework.runCommand('hello', 'world', '--foo', 'bar');\n\n    await close(app);\n  });\n});\n```\n\n<!-- File: site/docs/extensions/consul.md (Tokens: ~2533 | Category: anthropic-skill) -->\n# Consul\n\nconsul 用于微服务下的服务治理，主要特点有：服务发现、服务配置、健康检查、键值存储、安全服务通信、多数据中心等。\n\n本文基于最新组件实现，介绍 Consul 客户端的配置与使用，以及统一抽象的服务发现能力。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n从 v4 开始 Consul 组件基于 Service Factory 进行了重构，API 有着非常大的变化，并提供了服务发现等抽象能力。\n\n感谢 [boostbob](https://github.com/boostbob) 提供的组件。\n\n\n效果如下图：\n![image.png](https://img.alicdn.com/imgextra/i3/O1CN01e5cFZx1I0draeZynr_!!6000000000831-2-tps-1500-471.png)\n\n![image.png](https://img.alicdn.com/imgextra/i2/O1CN01iLYF8r1HQ0B3b47Fh_!!6000000000751-2-tps-1500-895.png)\n\n\n## 安装组件\n\n首先安装 Consul 组件：\n\n```bash\n$ npm i @midwayjs/consul@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/consul\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 引入组件\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as consul from '@midwayjs/consul';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [consul],\n  importConfigs: [join(__dirname, 'config')]\n})\nexport class MainConfiguration {}\n```\n\n\n\n## 配置客户端\n\n支持单客户端与多客户端，遵循 ServiceFactory 配置结构。\n\n**单客户端**\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  consul: {\n    client: {\n      host: '127.0.0.1',\n      port: '8500'\n    }\n  }\n}\n```\n\n**多客户端**\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  consul: {\n    clients: {\n      a: { host: '127.0.0.1', port: '8500' },\n      b: { host: '127.0.0.1', port: '8501' }\n    },\n    defaultClientName: 'a'\n  }\n}\n```\n\n**使用默认实例**\n\n`ConsulService` 作为默认实例代理，直接暴露原生 Consul 客户端方法。\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { ConsulService } from '@midwayjs/consul';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  consul: ConsulService;\n\n  async register() {\n     await this.consul.kv.set('name', 'juhai');\n  }\n}\n```\n\n\n\n**获取指定实例**\n\n通过 `ConsulServiceFactory` 获取指定命名的客户端实例。\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\nimport { ConsulServiceFactory } from '@midwayjs/consul';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  consulFactory: ConsulServiceFactory;\n\n  async invoke() {\n    const consulA = this.consulFactory.get('a');\n    const consulB = this.consulFactory.get('b');\n    await consulA.agent.service.register({ name: 'svc-a', id: 'svc-a-1' });\n    await consulB.agent.service.register({ name: 'svc-b', id: 'svc-b-1' });\n  }\n}\n```\n\n\n\n\n\n## 服务发现\n\n从 v4 开始提供基于统一抽象的服务发现能力，Consul 将成为服务发现的客户端来使用。\n\n服务发现包括 **注册服务** 和 **获取服务** 两部分。\n\n\n\n### 配置服务发现\n\n你需要先配置一个客户端，如果配置了多个，可以使用 `serviceDiscoveryClient` 来指定。\n\n通过 `serviceDiscovery` 来配置服务发现。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  consul: {\n    clients: {\n      default: { host: '127.0.0.1', port: '8500' },\n    },\n    serviceDiscovery: {\n      // ...\n    }\n  }\n}\n```\n\n\n\n### 注册服务发现\n\n当服务启动后，你可以将当前的服务注册到 Consul 注册中心。\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport { ConsulServiceDiscovery } from '@midwayjs/consul';\nimport { LoadBalancerType } from '@midwayjs/core';\n\n@Configuration({\n  // ...\n})\nexport class MainConfiguration {\n  @Inject()\n  consulDiscovery: ConsulServiceDiscovery;\n\n  async onServerReady() {\n    // 创建一个服务发现客户端\n    const client = this.consulDiscovery.createClient();\n\n    // 注册当前服务\n    await client.register({\n      id: client.defaultMeta.id,\n      name: 'order',\n      tags: ['test'],\n      address: client.defaultMeta.host,\n      port: 7001,\n      meta: {\n        version: '1.0.0'\n      },\n      check: {\n        name: 'TTL Health Check',\n        timeout: '30s',\n        ttl: '10s'\n      }\n    });\n  }\n}\n```\n\n`register` 方法需要传递一些基础的信息。\n\n最重要的有 id，name，以及 address。\n\n```typescript\ninterface RegisterOptions extends CommonOptions {\n  name: string;\n  id?: string;\n  tags?: string[];\n  address?: string;\n  taggedaddresses?: Record<string, any>;\n  meta?: Record<string, string>;\n  namespace?: string;\n  port?: number;\n  kind?: string;\n  proxy?: any;\n  connect?: RegisterConnect;\n  check?: CheckOptions;\n  checks?: CheckOptions[];\n}\n```\n\n`client.defaultMeta` 提供了一些默认生成的数据，包括 id，serviceName, host。\n\n\n\n### 获取可用服务\n\n你可以在任意需要的地方，动态获取到一个服务来调用。\n\n````typescript\n@Provide()\nexport class OrderService {\n  \n  @Inject()\n  consulDiscovery: ConsulServiceDiscovery;\n  \n  async getSerivce() {\n  \tconst instance = await this.consulDiscovery.getInstance({ service: 'order'});\n    // instance.name\n  }\n}\n````\n\n返回的 instance 类型为 `ConsulHealthItem`，结构如下。\n\n```typescript\nexport interface ConsulHealthItem {\n  Node: {\n    ID: string;\n    Node: string;\n    Address: string;\n    Datacenter: string;\n    TaggedAddresses: Record<string, any>;\n    Meta: Record<string, any>;\n    CreateIndex: number;\n    ModifyIndex: number;\n  };\n  Service: {\n    ID: string;\n    Service: string;\n    Tags: string[];\n    Address: string;\n    TaggedAddresses: Record<string, any>;\n    Meta: Record<string, any>;\n    Port: number;\n    Weights: Record<string, any>;\n    EnableTagOverride: boolean;\n    Proxy: Record<string, any>;\n    Connect: Record<string, any>;\n    PeerName: string;\n    CreateIndex: number;\n    ModifyIndex: number;\n  };\n  Checks: Array<{\n    Node: string;\n    CheckID: string;\n    Name: string;\n    Status: 'passing' | 'warning' | 'critical';\n    Notes: string;\n    Output: string;\n    ServiceID: string;\n    ServiceName: string;\n    ServiceTags: string[];\n    Type: string;\n    Interval: string;\n    Timeout: string;\n    ExposedPort: number;\n    Definition: Record<string, any>;\n    CreateIndex: number;\n    ModifyIndex: number;\n  }>;\n}\n\n```\n\n\n\n## 配置中心\n\n\n同时 consul 也能作为一个服务配置的地方，如下代码：\n\n```typescript\nimport { Controller, Get, Inject } from '@midwayjs/core';\nimport { ConsulService } from '@midwayjs/consul';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  consul: ConsulService;\n\n  @Get('/')\n  async home() {\n    await this.consul.kv.set('name', 'juhai');\n    const res = await this.consul.kv.get('name');\n    return res;\n  }\n}\n\n```\n\n我们调用 `kv.set` 方法，我们可以设置对应的配置，通过 `kv.get`  方法可以拿到对应的配置。\n\n\n注意：在代码中，有同学出现，在每次请求中去 get 对应的配置，这时你的 QPS 多少对 Consul server 的压力。\n\n\n所以在QPS比较大的情况，可以如下处理：\n\n```typescript\nimport { Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';\nimport { ConsulService } from '@midwayjs/consul';\n\n@Provide()\n@Scope(ScopeEnum.Singleton)\nexport class ConfigService {\n\n  @Inject()\n  consul: ConsulService;\n\n  config: any;\n\n  @Init()\n  async init() {\n    setInterval(async () => {\n      this.config = await this.consul.kv.get('name');\n    }, 5000);\n    this.config = await this.consul.kv.get('name');\n  }\n\n  async getConfig(){\n    return this.config;\n  }\n}\n\n```\n\n上面的代码，相当于定时去获取对应的配置，当每个请求进来的时候，获取 Scope 为 ScopeEnum.Singleton 服务的 `getConfig` 方法，这样每 5s 一次获取请求，就减少了对服务的压力。\n\nConsul 界面上如下图：\n\n![image.png](https://img.alicdn.com/imgextra/i4/O1CN01V3P6uK1rIVs19JiWn_!!6000000005608-2-tps-1500-374.png)\n\n![image.png](https://img.alicdn.com/imgextra/i2/O1CN014O2GyH1sMvIhmlbs4_!!6000000005753-2-tps-1500-667.png)\n\n\n一共提供如下几种方法：\n\n- [get](https://www.npmjs.com/package/consul#kv-get)，获取对应key的value\n- [keys](https://www.npmjs.com/package/consul#kv-keys)，查询某个prefix的key的列表\n- [set](https://www.npmjs.com/package/consul#kv-set)，设置对应的key的值\n- [del](https://www.npmjs.com/package/consul#kv-del)，删除对应的key\n\n<!-- File: site/docs/extensions/cos.md (Tokens: ~734 | Category: anthropic-skill) -->\n# 腾讯云对象存储（COS）\n\n本文介绍了如何使用 midway 接入腾讯云 COS。\n\n相关信息：\n\n| 描述              |     |\n| ----------------- | --- |\n| 可用于标准项目    | ✅   |\n| 可用于 Serverless | ✅   |\n| 可用于一体化      | ✅   |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/cos@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/cos\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 引入组件\n\n\n首先，引入 组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as cos from '@midwayjs/cos';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    cos\t\t// 导入 cos 组件\n  ],\n  importConfigs: [\n    join(__dirname, 'config')\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n\n## 配置\n\n比如：\n\n\n**单客户端配置**\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  cos: {\n    client: {\n      SecretId: '***********',\n      SecretKey: '***********',\n    },\n  },\n}\n```\n\n\n**多个客户端配置，需要配置多个**\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  cos: {\n    clients: {\n      instance1: {\n        SecretId: '***********',\n        SecretKey: '***********',\n      },\n      instance2: {\n        SecretId: '***********',\n        SecretKey: '***********',\n      },\n    },\n  },\n}\n```\n更多参数可以查看 [cos-nodejs-sdk-v5](https://github.com/tencentyun/cos-nodejs-sdk-v5) 文档。\n\n\n## 使用 COS 服务\n\n\n我们可以在任意的代码中注入使用。\n```typescript\nimport { Provide, Controller, Inject, Get } from '@midwayjs/core';\nimport { COSService } from '@midwayjs/cos';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  cosService: COSService;\n\n  async invoke() {\n    await this.cosService.sliceUploadFile({\n      Bucket: 'test-1250000000',\n      Region: 'ap-guangzhou',\n      Key: '1.zip',\n      FilePath: './1.zip'\n    },\n  }\n}\n```\n\n\n可以使用 `COSServiceFactory` 获取不同的实例。\n```typescript\nimport { COSServiceFactory } from '@midwayjs/cos';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  cosServiceFactory: COSServiceFactory;\n\n  async save() {\n    const cos1 = await this.cosServiceFactory.get('instance1');\n    const cos2 = await this.cosServiceFactory.get('instance3');\n\n    //...\n\n  }\n}\n```\n\n<!-- File: site/docs/extensions/cron.md (Tokens: ~1701 | Category: anthropic-skill) -->\n# 本地任务\n\n和 bull 组件不同，cron 组件提供的是本地任务能力，即在每台机器的每个进程都会执行。如需不同机器或者不同进程之间只执行一次任务，请使用 [bull 组件](./bull) 。\n\n\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n\n\n## 安装组件\n\n```bash\n$ npm i @midwayjs/cron@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/cron\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 使用组件\n\n将组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as cron from '@midwayjs/cron';\n\n@Configuration({\n  imports: [\n    // ...\n    cron\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n\n\n## 编写任务处理类\n\n使用 `@Job` 装饰器装饰一个类，用于快速定义一个任务处理器。\n\n比如，在 `src/job` 目录中创建一个 `sync.job.ts`，用于某些数据同步任务，代码如下：\n\n```typescript\n// src/job/sync.job.ts\nimport { Job, IJob } from '@midwayjs/cron';\nimport { FORMAT } from '@midwayjs/core';\n\n@Job({\n  cronTime: FORMAT.CRONTAB.EVERY_PER_30_MINUTE,\n  start: true,\n})\nexport class DataSyncCheckerJob implements IJob {\n  async onTick() {\n    // ...\n  }\n}\n```\n\n`@Job` 装饰器用于修饰一个任务类，在初始化时，框架会自动将其转变为一个任务。\n\n任务类需要实现 `IJob` 接口，实现 `onTick` 方法，每当任务触发时，会自动调用 `onTick` 方法。\n\n此外，还有一个可选的 `onComplete` 方法，用于在 `onTick` 完成后执行。\n\n```typescript\n@Job({\n  cronTime: FORMAT.CRONTAB.EVERY_PER_30_MINUTE,\n  start: true,\n})\nexport class DataSyncCheckerJob implements IJob {\n  async onTick() {\n    // ...\n  }\n  \n  async onComplete() {\n    // 记录一些数据等等，用处不是很大\n  }\n}\n```\n\n\n\n`@Job` 装饰器的常用参数如下：\n\n| 参数      | 类型    | 描述                   |\n| --------- | ------- | ---------------------- |\n| cronTime  | string  | crontab 表达式         |\n| start     | boolean | 是否自动启动任务       |\n| runOnInit | boolean | 是否在初始化就执行一次 |\n\n更多参数，请参考 [Cron](https://github.com/kelektiv/node-cron)。\n\n\n\n## 任务管理\n\n除了定时执行任务，我们还通过框架提供的 API，对任务进行手动管理。\n\n比如，下面的代码仅仅定义了一个任务，但是不会启动执行。\n\n```typescript\n@Job('syncJob', {\n  cronTime: '*/2 * * * * *',\t// 每隔 2s 执行\n})\nexport class DataSyncCheckerJob implements IJob {\n  async onTick() {\n    // ...\n  }\n}\n```\n\n我们定义了一个名为 `syncJob` 的任务，并且给它了一个默认的调度时间。\n\n\n\n### 获取任务对象\n\n我们可以通过两种方式获取任务对象。\n\n通过`@InjectJob` 用来注入某个任务，参数为类本身或者任务名。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as cron from '@midwayjs/cron';\nimport { InjectJob, CronJob } from '@midwayjs/cron';\nimport { DataSyncCheckerJob } from './job/sync.job';\n\n@Configuration({\n  imports: [\n    cron\n  ],\n})\nexport class ContainerConfiguration {\n  @InjectJob(DataSyncCheckerJob)\n  syncJob: CronJob;\n  \n  @InjectJob('syncJob')\n  syncJob2: CronJob;\n\n  async onServerReady() {\n    // this.syncJob  === this.syncJob2\n  }\n}\n\n```\n\n通过 Framework API 获取。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as cron from '@midwayjs/cron';\nimport { InjectJob, CronJob } from '@midwayjs/cron';\nimport { DataSyncCheckerJob } from './job/sync.job';\n\n@Configuration({\n  imports: [\n    cron\n  ],\n})\nexport class ContainerConfiguration {\n  @Inject()\n  cronFramework: cron.Framework;\n\n  async onServerReady() {\n    const syncJob = this.cronFramework.getJob(DataSyncCheckerJob);\n    const syncJob2 = this.cronFramework.getJob('syncJob');\n    \n    // syncJob  === syncJob2\n  }\n}\n\n```\n\n:::caution\n\n注意，任务对象都必须在 `onServerReady` 生命周期或者启动之后才能获取。\n\n:::\n\n\n\n### 启停任务\n\n我们可以在初始化或者某些程序执行完成之后，将这个任务启动。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, Inject } from '@midwayjs/core';\nimport * as cron from '@midwayjs/cron';\nimport { InjectJob, CronJob } from '@midwayjs/cron';\nimport { DataSyncCheckerJob } from './job/sync.job';\n\n@Configuration({\n  imports: [\n    cron\n  ],\n})\nexport class ContainerConfiguration {\n  @InjectJob(DataSyncCheckerJob)\n  syncJob: CronJob;\n\n  async onServerReady() {\n    this.syncJob.start();\n    \n    // ...\n    this.syncJob.stop();\n  }\n}\n\n```\n\n\n\n## 上下文\n\n任务执行是在请求作用域中，其有着特殊的 Context 对象结构。\n\n```typescript\nexport interface Context extends IMidwayContext {\n  job: CronJob;\n}\n```\n\n这里的  `CronJob` 类型来自于 [Cron](https://github.com/kelektiv/node-cron) 包。\n\n\n\n## 组件日志\n\n组件有着自己的日志，默认会将 `ctx.logger` 记录在 `midway-cron.log` 中。\n\n我们可以单独配置这个 logger 对象。\n\n```typescript\nexport default {\n  midwayLogger: {\n    // ...\n    clients: {\n      // ...\n      cronLogger: {\n        fileLogName: 'midway-cron.log',\n      },\n    }\n  }\n}\n```\n\n\n\n## 全局配置\n\n可以针对 Job 进行一些全局配置，会和每个 Job 的配置进行合并。\n\n```typescript\nexport default {\n  cron: {\n    defaultCronJobOptions: {\n      // ...\n    }\n  }\n}\n```\n\n这里的 `defaultCronJobOptions` 配置项请参考 [CronJobParameters](https://github.com/kelektiv/node-cron/blob/main/lib/job.js#L51)\n\n<!-- File: site/docs/extensions/cross_domain.md (Tokens: ~1804 | Category: anthropic-skill) -->\n# 跨域\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的通用跨域组件，支持 `cors` 、`jsonp` 多种模式。\n\n相关信息：\n\n| web 支持情况      |      |\n| ----------------- | ---- |\n| @midwayjs/koa     | ✅    |\n| @midwayjs/faas    | ✅    |\n| @midwayjs/web     | ✅    |\n| @midwayjs/express | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/cross-domain --save\n```\n\n## 引入组件\n\n在 `src/configuration.ts` 中引入组件。\n\n```typescript\nimport * as crossDomain from '@midwayjs/cross-domain';\n@Configuration({\n  imports: [\n    // ...other components\n    crossDomain\n  ],\n})\nexport class MainConfiguration {}\n```\n\n\n\n## 什么是跨域\n\n假设有两个网站：\n\n- **A.com**：这是你的网站，你想要从这里访问一些资源。\n- **B.com**：这是另一个网站，它拥有你想要访问的资源。\n\n### 场景设定\n\n1. **A.com** 是你的主网站，你在这里运行一些 JavaScript 代码。\n2. **B.com** 拥有一些 API 接口，你想要通过 A.com 的 JavaScript 代码来调用这些 API 接口。\n\n由于同源策略，浏览器默认不允许 A.com 的 JavaScript 代码直接访问 B.com 的资源。这是因为浏览器出于安全考虑，防止恶意网站读取其他网站的敏感数据。你想要在 A.com 上运行的 JavaScript 代码中发起一个请求到 B.com 的 API 接口，这个请求会被视为跨域请求。\n\n简单来说，从 A.com 访问 B.com 的接口，就是跨域。\n\n此外，跨域还有一些条件：\n\n* 1、同源策略是浏览器安全机制的一部分，所以一般只有浏览器访问才有跨域问题\n* 2、当浏览器发起跨域请求时，它会根据当前源自动添加一个 `Origin` 头部到请求中，服务端也会根据 `origin` 头判断来源，进而做处理\n\n所以，当你发现跨域设置不生效时，请检查：\n\n* 1、是否真的是跨域请求\n* 2、是否真的从浏览器发起\n* 3、是否请求带有 `origin` 头\n\n\n\n## 什么是 CORS\n\n在前端代码调用后端服务中经常会碰到下面的错误，这就最为常见的跨域 [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) 错误。\n\n```\nAccess to fetch at 'http://127.0.0.1:7002/' from origin 'http://127.0.0.1:7001' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.\n```\n\n出于安全性，浏览器限制脚本内发起的跨源 HTTP 请求。例如，`XMLHttpRequest` 和 [Fetch API](https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API) 遵循[同源策略](https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy)。这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源，除非响应报文包含了正确 CORS 响应头。\n\nCORS 机制允许 Web 应用服务器进行跨源访问控制，从而使跨源数据传输得以安全进行。现代浏览器支持在 API 容器中（例如 [`XMLHttpRequest`](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest) 或 [Fetch](https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API)）使用 CORS，以降低跨源 HTTP 请求所带来的风险。\n\n\n\n## 常见的 CORS 配置\n\n下面列举几种跨域的解决方案，我们以 `fetch` 方法为例。\n\n### 未使用 `credentials` \n\n客户端。\n\n```javascript\nfetch(url);\n```\n\n服务端配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  cors: {\n    origin: '*',\n  },\n}\n```\n\n### 使用 `credentials`\n\n客户端。\n\n```javascript\nfetch(url, {\n  credentials: \"include\",\n});\n```\n\n服务端配置\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  cors: {\n    credentials: true,\n  },\n}\n```\n\n### 限制 `origin` 来源\n\n假如我们的网页地址为 `http://127.0.0.1:7001` 而接口为 `http://127.0.0.1:7002`。\n\n客户端。\n\n```javascript\nfetch('http://127.0.0.1:7002/', {\n  credentials: 'include'\n})\n```\n\n服务端配置，注意，由于启用了 `credentials`，这个时候 `origin` 字段不能为 `*`。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  cors: {\n    origin: 'http://127.0.0.1:7001',\n    credentials: true,\n  },\n}\n```\n\n\n\n## 更多 CORS 配置\n\n完整可用配置如下：\n\n```typescript\nexport const cors = {\n  // 允许跨域的方法，【默认值】为 GET,HEAD,PUT,POST,DELETE,PATCH\n  allowMethods: string |string[];\n  // 设置 Access-Control-Allow-Origin 的值，【默认值】会获取请求头上的 origin\n  // 也可以配置为一个回调方法，传入的参数为 request，需要返回 origin 值\n  // 例如：http://test.midwayjs.org\n  // 如果设置了 credentials，则 origin 不能设置为 *\n  origin: string|Function;\n  // 设置 Access-Control-Allow-Headers 的值，【默认值】会获取请求头上的 Access-Control-Request-Headers\n  allowHeaders: string |string[];\n  // 设置 Access-Control-Expose-Headers 的值\n  exposeHeaders: string |string[];\n  // 设置 Access-Control-Allow-Credentials，【默认值】false\n   // 也可以配置为一个回调方法，传入的参数为 request，返回值为 true 或 false\n  credentials: boolean|Function;\n  // 是否在执行报错的时候，把跨域的 header 信息写入到 error 对的 headers 属性中，【默认值】false\n  keepHeadersOnError: boolean;\n  // 设置 Access-Control-Max-Age\n  maxAge: number;\n}\n```\n\n\n## JSONP 配置\n\n可以在 `src/config/config.default` 中进行 JSONP 配置。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  jsonp: {\n    callback: 'jsonp',\n    limit: 512,\n  },\n}\n```\n\n<!-- File: site/docs/extensions/crud.md (Tokens: ~5288 | Category: anthropic-skill) -->\n# CRUD\n\n本文档介绍如何在 Midway 中使用 `@midwayjs/crud`。\n\n`@midwayjs/crud` 不是一个“只能自动生成接口”的脚手架，它本质上提供的是一套**面向资源的标准 CRUD 能力**：\n\n- 统一的增删改查 service 抽象\n- 统一的分页 / 排序 / 过滤 / 搜索协议\n- 可选的 REST 路由快捷生成\n- 和现有 validation / swagger / web 路由体系的接入\n\n如果你经常在不同模块里重复写下面这些代码：\n\n- `findAndCount`\n- `save`\n- `update`\n- `delete`\n- 手动解析 `page`、`limit`、`sort`\n- 为每个资源写一套几乎一样的 Controller\n\n那这个组件就是用来把这些重复劳动收起来的。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n## 这个组件能做什么\n\n先用一句话概括：\n\n**先提供一个可复用的 CRUD service，再按需把它暴露成 HTTP 接口。**\n\n这意味着它支持两种使用方式：\n\n1. 只把它当数据访问层能力来用  \n   适合你的业务逻辑比较复杂，不想让组件替你生成接口。\n\n2. 把它当接口快捷层来用  \n   适合资源型接口很多，希望快速拿到统一的 REST API。\n\n所以它解决的是“资源型接口的重复代码”问题，但**不会替代你的业务 service**。\n\n复杂业务依然应该写在你自己的 service 里，例如：\n\n- 下单时要检查库存、优惠券、支付状态\n- 创建用户时要同步多个系统\n- 删除资源前要做权限和状态机校验\n\n这些逻辑应该继续在你的业务 service 中完成，CRUD 组件只是提供一个稳定的资源操作基座。\n\n## 核心概念\n\n在开始之前，先理解这三个层次。\n\n### 1. `CrudService<T>`\n\n这是最核心的抽象，定义了统一的资源操作接口。\n\n```ts\nlist(query)\nfindOne(id)\ncreate(data)\nupdate(id, data)\ndelete(id)\n```\n\n你可以把它理解成“一个标准化的资源仓储服务接口”。\n\n### 2. 数据库适配层\n\nMidway CRUD 目前提供了 4 个官方适配基类：\n\n- `TypeOrmCrudService<T>`\n- `MikroCrudService<T>`\n- `SequelizeCrudService<T>`\n- `MongooseCrudService<T>`\n\n它们都实现了相同的 CRUD 核心接口，但底层分别接不同的数据访问组件。\n\n### 3. HTTP 暴露层\n\n这是可选的。\n\n如果你希望快速生成路由，可以使用：\n\n- 类式：`@Crud()`\n- 函数式：`defineCrudRoutes()`\n\n这两种方式都只是把同一个 `CrudService` 暴露成 HTTP 接口，不会生成第二套独立逻辑。\n\n## 什么时候适合用\n\n适合：\n\n- 大量资源型接口，结构相似\n- 列表查询都需要统一分页、排序、过滤\n- 想减少重复的 Repository / Model 调用代码\n- 想让不同模块的 API 风格保持一致\n\n不适合：\n\n- 主要是复杂工作流，不是标准资源接口\n- 一个接口需要跨多个聚合、多个事务、多个外部系统\n- 你希望所有查询语义都完全自由，不接受统一约束\n\n如果一个模块的核心不是“资源管理”，而是“复杂业务流程”，那更适合直接写普通 Controller + Service，而不是强行套 CRUD。\n\n## 安装依赖\n\n先安装 CRUD 组件本身：\n\n```bash\n$ npm i @midwayjs/crud@4 --save\n```\n\n然后根据你使用的数据库组件安装对应依赖。\n\n```bash\n# TypeORM\n$ npm i @midwayjs/typeorm@4 typeorm@^1.0.0 --save\n\n# MikroORM\n$ npm i @midwayjs/mikro@4 @mikro-orm/core@^6 --save\n\n# Sequelize\n$ npm i @midwayjs/sequelize@4 sequelize sequelize-typescript --save\n\n# Mongoose\n$ npm i @midwayjs/mongoose@4 mongoose --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/crud\": \"^4.0.0\",\n    \"@midwayjs/typeorm\": \"^4.0.0\",\n    \"@midwayjs/mikro\": \"^4.0.0\",\n    \"@midwayjs/sequelize\": \"^4.0.0\",\n    \"@midwayjs/mongoose\": \"^4.0.0\",\n    \"typeorm\": \"^1.0.0\",\n    \"@mikro-orm/core\": \"^6.4.5\",\n    \"sequelize\": \"^6.37.5\",\n    \"sequelize-typescript\": \"^2.1.6\",\n    \"mongoose\": \"^8.9.5\"\n  }\n}\n```\n\n## 启用组件\n\n在 `src/configuration.ts` 中引入组件。\n\n下面以 `koa + typeorm + crud` 为例：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as typeorm from '@midwayjs/typeorm';\nimport * as crud from '@midwayjs/crud';\n\n@Configuration({\n  imports: [\n    koa,\n    typeorm,\n    crud,\n  ],\n})\nexport class MainConfiguration {}\n```\n\n如果你使用其他数据库组件，只需要把对应组件加到 `imports` 中即可。\n\n## 入门：先从 service-only 开始\n\n对于新手来说，最容易理解的方式不是先生成路由，而是先把它当一个“可复用的数据服务”。\n\n这也是推荐的理解顺序。\n\n### 为什么推荐先学 service-only\n\n因为这样你能先理解：\n\n- 组件真正提供的核心是什么\n- 业务代码应该写在哪里\n- 路由层只是一个可选外壳\n\n如果一上来就只看 `@Crud()`，很容易误以为这只是一个自动生成接口的装饰器。\n\n### 最小 TypeORM 示例\n\n先定义一个资源 service：\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { TypeOrmCrudService } from '@midwayjs/crud/typeorm';\nimport { Repository } from 'typeorm';\n\nimport { UserEntity } from '../entity/user';\n\n@Provide()\nexport class UserCrudService extends TypeOrmCrudService<UserEntity> {\n  @InjectEntityModel(UserEntity)\n  repo: Repository<UserEntity>;\n}\n```\n\n然后在业务 service 里直接组合它：\n\n```typescript\nimport { Provide, Inject } from '@midwayjs/core';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  userCrudService: UserCrudService;\n\n  async listUsers() {\n    return this.userCrudService.list({\n      page: 1,\n      limit: 20,\n      sort: [],\n      filters: [],\n    });\n  }\n\n  async createUser(input: CreateUserDTO) {\n    return this.userCrudService.create(input);\n  }\n}\n```\n\n这时候：\n\n- 你已经获得统一的 CRUD 能力\n- 但**不会自动生成任何 HTTP 路由**\n- 业务仍然由你自己的 `UserService` 负责组织\n\n这也是最适合复杂业务场景的用法。\n\n## 其他数据库适配\n\n如果你的项目不是 TypeORM，也可以使用其他官方适配。\n\n### MikroORM\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectRepository } from '@midwayjs/mikro';\nimport { MikroCrudService } from '@midwayjs/crud/mikro';\n\n@Provide()\nexport class UserCrudService extends MikroCrudService<UserEntity> {\n  @InjectRepository(UserEntity)\n  repo;\n}\n```\n\n### Sequelize\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InjectRepository } from '@midwayjs/sequelize';\nimport { SequelizeCrudService } from '@midwayjs/crud/sequelize';\nimport { Repository } from 'sequelize-typescript';\n\n@Provide()\nexport class UserCrudService extends SequelizeCrudService<UserModel> {\n  @InjectRepository(UserModel)\n  repo: Repository<UserModel>;\n}\n```\n\n### Mongoose\n\n```typescript\nimport { Inject, Provide } from '@midwayjs/core';\nimport { MongooseDataSourceManager } from '@midwayjs/mongoose';\nimport { MongooseCrudService } from '@midwayjs/crud/mongoose';\n\n@Provide()\nexport class UserCrudService extends MongooseCrudService<UserDocument> {\n  @Inject()\n  mongooseDataSourceManager: MongooseDataSourceManager;\n\n  async onReady() {\n    this.repo = this.mongooseDataSourceManager\n      .getDataSource('default')\n      .model('User');\n  }\n}\n```\n\n这几个适配基类的对外用法保持一致，区别主要在于底层仓储注入方式和 ORM 行为。\n\n## 快速生成类式 REST 接口\n\n当你已经有一个 `CrudService` 之后，如果希望快速生成标准资源型接口，可以使用 `@Crud()`。\n\n### 最小类式示例\n\n```typescript\nimport { Controller, Inject } from '@midwayjs/core';\nimport { Crud } from '@midwayjs/crud';\n\nimport { UserEntity } from '../entity/user';\nimport { UserCrudService } from '../service/user.crud';\n\n@Controller('/users')\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n})\nexport class UserController {\n  @Inject()\n  crudService: UserCrudService;\n}\n```\n\n默认会生成：\n\n- `GET /users`\n- `GET /users/:id`\n- `POST /users`\n- `PATCH /users/:id`\n- `DELETE /users/:id`\n\n### 为什么还要 `@Inject() crudService`\n\n因为 `@Crud()` 只是声明“这个 Controller 是一个 CRUD 资源”，真正执行业务的是你绑定的 `crudService`。\n\n也就是说：\n\n- `@Crud()` 负责生成默认路由\n- `crudService` 负责真正执行 CRUD 逻辑\n\n这是这个组件最重要的设计原则之一。\n\n### 业务逻辑写在哪里\n\n不要把复杂业务逻辑塞到 `@Crud()` 里。\n\n推荐做法是：\n\n- 资源级默认行为：写在 `UserCrudService`\n- 复杂业务编排：写在你自己的 `UserService` / Domain Service\n- 非标准动作：写成普通路由方法\n\n例如：\n\n```typescript\n@Controller('/users')\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n})\nexport class UserController {\n  @Inject()\n  crudService: UserCrudService;\n\n  async create() {\n    // 自定义事务、调用多个 service、做额外校验\n  }\n\n  async resetPassword() {\n    // 非标准资源动作\n  }\n}\n```\n\n同名方法会优先使用你手写的实现，因此你可以只覆写部分默认行为。\n\n### 裁剪默认路由\n\n如果你不想暴露所有默认路由，可以通过 `routes.only` 或 `routes.exclude` 控制。\n\n```typescript\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n  routes: {\n    only: ['list', 'detail', 'create'],\n  },\n})\n```\n\n这对于“只能查、不能删”或“只开放后台管理的一部分动作”的场景很有用。\n\n## 函数式路由模式\n\n如果项目使用函数式 API，而不是类式 Controller，可以从 `@midwayjs/crud/functional` 导入 `defineCrudRoutes()`。\n\n### 最小示例\n\n```typescript\nimport { defineApi } from '@midwayjs/core/functional';\nimport { defineCrudRoutes } from '@midwayjs/crud/functional';\n\nimport { UserEntity } from '../entity/user';\nimport { UserCrudService } from '../service/user.crud';\n\nconst crudRoutes = defineCrudRoutes<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n});\n\nexport default defineApi('/users', api => ({\n  ...crudRoutes(api),\n}));\n```\n\n### 和自定义动作一起使用\n\n函数式模式最常见的用法，是把默认 CRUD 路由和自定义动作合并在同一个 `defineApi()` 里。\n\n```typescript\nexport default defineApi('/users', api => ({\n  ...crudRoutes(api),\n  resetPassword: api\n    .post('/:id/reset-password')\n    .handle(async ({ input, ctx }) => {\n      return { ok: true };\n    }),\n}));\n```\n\n这样可以让“标准资源动作”和“业务动作”共存在同一个资源路由下。\n\n## 查询协议\n\n列表接口使用统一的查询协议，这是这个组件最重要的能力之一。\n\n### 支持的 query 参数\n\n- `page=<number>`\n- `limit=<number>`\n- `sort=<field>:<ASC|DESC>`，可重复传入\n- `filter=<field>||<operator>||<value>`，可重复传入\n- `search=<keyword>`\n- `join=<relation>`，可重复传入\n- `fields=<field1,field2,...>`\n\n例如：\n\n```text\nGET /users?page=1&limit=20&sort=createdAt:DESC&filter=status||eq||active&search=harry&join=profile\n```\n\n### 支持的过滤操作符\n\n首阶段支持：\n\n- `eq`\n- `ne`\n- `gt`\n- `gte`\n- `lt`\n- `lte`\n- `in`\n- `like`\n\n### 查询协议的约束\n\n这些约束是有意设计出来的，用来保证资源接口的一致性：\n\n- `sort` 字段必须在 `sortable` 白名单里\n- `filter` 字段必须在 `filterable` 白名单里\n- `search` 只会作用于 `searchable` 白名单\n- `join` 必须在 `join` 白名单里\n- `join` 首阶段只支持一层关系名，不支持 `profile.company`\n\n也就是说，这个组件不会让客户端随意拼接任意字段查询，而是让你在服务端声明资源允许暴露的查询能力。\n\n### 在 `@Crud()` 中声明查询能力\n\n```typescript\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n  query: {\n    defaultLimit: 20,\n    maxLimit: 100,\n    sortable: ['id', 'createdAt'],\n    filterable: ['status'],\n    searchable: ['name', 'email'],\n    join: ['profile'],\n  },\n})\n```\n\n这样配置后，查询行为就会按这个白名单执行。\n\n## 返回结构\n\n默认列表接口会返回统一的分页结构，而不是直接返回数组。\n\n```typescript\ntype CrudPageResult<T> = {\n  data: T[];\n  meta: {\n    page: number;\n    limit: number;\n    total: number;\n    pageCount: number;\n    hasNext: boolean;\n    hasPrev: boolean;\n  };\n};\n```\n\n这能让前端和不同资源接口之间保持一致的分页消费方式。\n\n其他默认返回规则：\n\n- `detail` 返回单个资源对象\n- `create` 返回创建后的资源对象\n- `update` 返回更新后的资源对象\n- `delete` 默认返回 `204 No Content`\n\n## DTO、Validation 与 Swagger\n\nCRUD 路由会尽量复用现有 Midway 组件，而不是另起一套协议。\n\n### DTO 绑定\n\n你可以在 `@Crud()` 中声明：\n\n- `dto.create`\n- `dto.update`\n- `dto.replace`\n- `dto.query`\n\n例如：\n\n```typescript\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n  dto: {\n    create: CreateUserDTO,\n    update: UpdateUserDTO,\n    query: UserQueryDTO,\n  },\n})\n```\n\n### Validation\n\n如果项目里启用了 `@midwayjs/validation` 或兼容的 validation service：\n\n- `dto.create` 会用于创建请求体校验\n- `dto.update` 会用于更新请求体校验\n- `dto.replace` 会用于替换请求体校验\n- `dto.query` 会用于列表查询参数校验\n\n如果没有安装 validation 组件，CRUD 本身不会强行报错，而是只跳过这一步自动校验。\n\n### Swagger\n\n自动生成的 CRUD 路由会复用现有 Web 元数据链，所以 Swagger 组件可以识别到这些路由。\n\n也就是说：\n\n- 默认路由会出现在 Swagger 文档里\n- 基础的 path / query / body / response 元数据会被自动补齐\n\n这意味着你不需要为每一个简单资源接口手动补一遍同样的 swagger 装饰器。\n\n## 软删除\n\n默认删除行为是**硬删除**。\n\n如果你希望某个资源使用软删除，需要显式开启：\n\n```typescript\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n  delete: {\n    mode: 'soft',\n  },\n})\n```\n\n开启后：\n\n- `DELETE /:id` 会走软删除\n- `list` / `detail` 默认排除已软删的数据\n- 如果底层实体或仓储不支持软删除，会直接报错\n\n### 为什么不是默认软删除\n\n因为软删除不是所有资源都适合：\n\n- 会影响唯一键约束\n- 会影响查询逻辑\n- 会影响索引设计\n- 会影响后台数据管理\n\n所以这里采用的是“显式开启”的策略，而不是默认偷偷帮你切换成软删。\n\n## 一个更完整的类式示例\n\n下面给一个稍完整一点的例子，把前面的内容串起来。\n\n```typescript\nimport { Controller, Inject, Provide } from '@midwayjs/core';\nimport { Crud } from '@midwayjs/crud';\nimport { InjectEntityModel } from '@midwayjs/typeorm';\nimport { TypeOrmCrudService } from '@midwayjs/crud/typeorm';\nimport { Repository } from 'typeorm';\n\n@Provide()\nexport class UserCrudService extends TypeOrmCrudService<UserEntity> {\n  @InjectEntityModel(UserEntity)\n  repo: Repository<UserEntity>;\n}\n\n@Controller('/users')\n@Crud<UserEntity>({\n  model: UserEntity,\n  service: UserCrudService,\n  dto: {\n    create: CreateUserDTO,\n    update: UpdateUserDTO,\n    query: UserQueryDTO,\n  },\n  query: {\n    defaultLimit: 20,\n    maxLimit: 100,\n    sortable: ['id', 'createdAt'],\n    filterable: ['status'],\n    searchable: ['name', 'email'],\n    join: ['profile'],\n  },\n  delete: {\n    mode: 'soft',\n  },\n})\nexport class UserController {\n  @Inject()\n  crudService: UserCrudService;\n}\n```\n\n这段代码的效果是：\n\n- 生成一个 `/users` 资源接口\n- 自动获得标准 CRUD 路由\n- 自动使用统一查询协议\n- 自动按 DTO 做校验（如果启用了 validation）\n- 自动被 swagger 扫描\n- 删除逻辑使用软删除\n\n## 二级导出\n\n组件提供这些稳定入口：\n\n- `@midwayjs/crud`\n- `@midwayjs/crud/typeorm`\n- `@midwayjs/crud/mikro`\n- `@midwayjs/crud/sequelize`\n- `@midwayjs/crud/mongoose`\n- `@midwayjs/crud/functional`\n\n建议这样理解：\n\n- 主入口：核心类型 + `@Crud()`\n- 数据库二级入口：各自的 CRUD service 适配\n- `functional`：函数式路由适配\n\n## 最后的建议\n\n如果你是第一次接触这个组件，建议按这个顺序使用：\n\n1. 先学 `service-only`，把它当一个标准 CRUD service\n2. 再用 `@Crud()` 快速展开简单资源接口\n3. 最后再用函数式模式或高级查询配置\n\n这样更容易理解它的边界，也更不容易把复杂业务错误地塞进自动 CRUD 层。\n\n<!-- File: site/docs/extensions/egg.md (Tokens: ~7034 | Category: anthropic-skill) -->\n# EggJS\n\nMidway 可以使用 EggJS 作为上层 Web 框架，EggJS 提供了非常多常用的插件和 API，帮助用户快速构建企业级 Web 应用。本章节内容，主要介绍 EggJS 在 Midway 中如何使用自身的能力。\n\n| 描述              |      |\n| ----------------- | ---- |\n| 包含独立主框架    | ✅    |\n| 包含独立日志      | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/web@4 egg --save\n$ npm i @midwayjs/egg-ts-helper --save-dev\n```\n\n针对 EggJS 场景，这些包列举如下。\n\n```json\n  \"dependencies\": {\n    \"@midwayjs/web\": \"^4.0.0\",\n    \"@midwayjs/core\": \"^4.0.0\",\n    \"egg\": \"^2.0.0\",\n    \"egg-scripts\": \"^2.10.0\"\n  },\n  \"devDependencies\": {\n    \"@midwayjs/egg-ts-helper\": \"^1.0.1\",\n  },\n```\n\n| @midwayjs/web           | **必须**，Midway EggJS 适配层    |\n|-------------------------|----------------------------|\n| @midwayjs/core          | **必须**，Midway 核心包          |\n| egg                     | **必须**，EggJS 依赖包，提供定义等其他能力 |\n| egg-scripts             | **可选**，EggJS 启动脚本          |\n| @midwayjs/egg-ts-helper | **可选**，EggJS 定义生成工具        |\n\n也可以直接使用脚手架创建示例。\n\n```bash\n# npm v6\n$ npm init midway --type=egg-v3 my_project\n\n# npm v7\n$ npm init midway -- --type=egg-v3 my_project\n```\n\n\n\n## 开启组件\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as web from '@midwayjs/web';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [web],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: web.Application;\n\n  async onReady() {\n\t\t// ...\n  }\n}\n\n```\n\n\n\n## 和默认 EggJS 的不同之处\n\n\n- 1、从 v3 开始，midway 提供了更多的组件，大部分 egg 内置插件默认禁用\n- 2、baseDir 默认调整为 `src` 目录，服务器上为 `dist` 目录\n- 3、禁用 egg-logger，全部替换为 @midwayjs/logger，不可切换\n\n\n\n整个架构如下:\n![](https://cdn.nlark.com/yuque/0/2021/png/501408/1614842824740-fc0c1432-3ace-4f77-b51f-15212984b168.png)\n\n\n## 目录结构\n\n\n除了 Midway 提供的目录结构外，EggJS 还有一些特殊的目录结构（不可变），整个结构如下。\n```\n➜  my_midway_app tree\n.\n├── src\n|   ├── app.ts \t\t\t\t\t\t\t\t\t\t \t\t\t\t## EggJS 扩展 Worker 生命周期文件（可选）\n|   ├── agent.ts\t\t\t\t\t\t\t\t   \t\t\t\t## EggJS 扩展 Agent 生命周期文件（可选）\n|   ├── app\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t## EggJS 固定的根目录（可选）\n|   │   ├── public\t\t \t\t\t\t\t\t\t\t  \t## EggJS 静态托管插件的默认目录（可配）\n|   │   |   └── reset.css\n|   │   ├── view (可选)\t\t\t\t\t\t\t\t\t\t ## EggJS 模板渲染的默认目录（可配）\n|   │   |   └── home.tpl\n|   │   └── extend (可选)\t\t\t\t\t\t\t\t\t ## EggJS 扩展目录（可配）\n|   │       ├── helper.ts (可选)\n|   │       ├── request.ts (可选)\n|   │       ├── response.ts (可选)\n|   │       ├── context.ts (可选)\n|   │       ├── application.ts (可选)\n|   │       └── agent.ts (可选)\n|   │\n|   ├── config\n|   |   ├── plugin.ts\n|   |   ├── config.default.ts\n|   |   ├── config.prod.ts\n|   |   ├── config.test.ts (可选)\n|   |   ├── config.local.ts (可选)\n|   |   └── config.unittest.ts (可选)\n│   ├── controller\t\t\t\t\t\t\t\t \t\t\t\t## Midway 控制器目录（推荐）\n│   ├── service\t\t\t\t\t\t\t\t     \t\t\t\t## Midway 服务目录（推荐）\n│   └── schedule\t\t\t\t\t\t\t\t\t \t\t\t\t## Midway 定时器目录（推荐）\n│\n├── typings                        \t\t\t\t## EggJS 定义生成目录\n├── test\n├── package.json\n└── tsconfig.json\n```\n以上是 EggJS 的目录结构全貌，其中包含了很多 EggJS 特有的目录，有一些在 Midway 体系中已经有相应的能力替代，可以直接替换。整个结构，基本上等价于将 EggJS 的目录结构移动到了 `src` 目录下。\n\n\n由于 EggJS 是基于约定的框架，整个工程的目录结构是固定的，这里列举一些常用的约定目录。\n\n| `src/app/public/**`  | 用于放置静态资源，可选，具体参见内置插件 [egg-static](https://github.com/eggjs/egg-static)。 |\n| --- | --- |\n| `src/config/config.{env}.ts` | 用于编写配置文件，具体参见[配置](https://eggjs.org/zh-cn/basics/config.html)。 |\n| `src/config/plugin.js` | 用于配置需要加载的插件，具体参见[插件](https://eggjs.org/zh-cn/basics/plugin.html)。 |\n| `test/**` | 具体参见[单元测试](https://eggjs.org/zh-cn/core/unittest.html)。 |\n| `src/app.js` 和 `src/agent.js` | 用于自定义启动时的初始化工作，可选，具体参见[启动自定义](https://eggjs.org/zh-cn/basics/app-start.html)。关于`agent.js`的作用参见[Agent机制](https://eggjs.org/zh-cn/core/cluster-and-ipc.html#agent-%E6%9C%BA%E5%88%B6)。 |\n\n\n\n## 配置定义\n\n\nMidway 在脚手架中提供了标准的 EggJS 的 TS 配置写法，MidwayConfig 中包括了 egg 中配置的定义和属性提示，结构如下。\n```typescript\n// src/config/config.default.ts\nimport { MidwayConfig, MidwayAppInfo } from '@midwayjs/core';\n\nexport default (appInfo: MidwayAppInfo) => {\n  return {\n    // use for cookie sign key, should change to your own and keep security\n    keys: appInfo.name + '_xxxx',\n    egg: {\n      port: 7001,\n    },\n    // security: {\n    //   csrf: false,\n    // },\n  } as MidwayConfig;\n};\n\n```\n通过这样返回方法的形式，在运行期会被自动执行，合并进完整的配置对象。\n\n\n这个函数的参数为 `MidwayAppConfig` 类型，值为以下内容。\n\n| **appInfo** | **说明** |\n| --- | --- |\n| pkg | package.json |\n| name | 应用名，同 pkg.name |\n| baseDir | 应用代码的 src （本地开发）或者 dist （上线后）目录 |\n| appDir | 应用代码的目录 |\n| HOME | 用户目录，如 admin 账户为 /home/admin |\n| root | 应用根目录，只有在 local 和 unittest 环境下为 baseDir，其他都为 HOME。 |\n\n\n\n:::info\n注意，这里的 `baseDir` 和 `appDir` 和 EggJS 应用有所区别。\n:::\n\n\n\n\n## 使用 Egg 插件\n\n插件是 EggJS 的特色之一，`@midwayjs/web` 也支持 EggJS 的插件体系，但是在有 Midway 组件的情况下，尽可能优先使用 Midway 组件。\n\n\n插件一般通过 npm 模块的方式进行复用。\n```bash\n$ npm i egg-mysql --save\n```\n然后需要在应用或框架的 `src/config/plugin.js` 中声明开启。\n\n\n如果有 `export default` ，请写在其中。\n```typescript\nimport { EggPlugin } from 'egg';\nexport default {\n  static: false, // default is true\n  mysql: {\n    enable: true,\n    package: 'egg-mysql'\n  }\n} as EggPlugin;\n\n```\n如果没有 `export default` ，可以直接导出。\n```typescript\n// src/config/plugin.ts\n// 使用 mysql 插件\nexport const mysql = {\n  enable: true,\n  package: 'egg-mysql',\n};\n```\n\n\n在开启插件之后，我们就可以在业务代码中使用插件提供的功能了。一般来说，插件会将对象挂载到 EggJS 的 `app` 和 `ctx` 之上，然后直接使用。\n\n\n```typescript\napp.mysql.query(sql, values);\t\t\t// egg 提供的方法\n```\n在 Midway 中可以通过 `@App` 获取 `app` 对象，以及在请求作用域中通过 `@Inject() ctx` 获取 `ctx` 对象，所以我们可以通过注入来获取插件对象。\n\n\n```typescript\nimport { Provide, Inject, Get } from '@midwayjs/core';\nimport { Application, Context } from '@midwayjs/web';\n\n@Provide()\nexport class HomeController {\n\n  @App()\n  app: Application;\n\n  @Inject()\n  ctx: Context;\n\n  @Get('/')\n  async home() {\n  \tthis.app.mysql.query(sql, values);    // 调用 app 上的方法（如果有的话）\n    this.ctx.mysql.query(sql, values);\t\t// 调用挂载在 ctx 上的方法（如果有的话）\n  }\n}\n```\n此外，还可以通过 `@Plugin` 装饰器来直接注入 `app` 挂载的插件，默认情况下，如果不传参数，将以属性名作为 key。\n\n\n```typescript\nimport { Provide, Get, Plugin } from '@midwayjs/core';\n\n@Provide()\nexport class HomeController {\n\n  @Plugin()\n  mysql: any;\n\n  @Get('/')\n  async home() {\n  \tthis.mysql.query(sql, values);\n  }\n}\n```\n:::info\n`@Plugin() mysql`  等价于 `app.mysql` 。 `@Plugin` 的作用就是从 app 对象上拿对应属性名的插件，所以 `@Plugin() xxx` 就等于 `app['xxx']` 。\n:::\n\n\n## Web 中间件\n\n\n中间件样例如下：\n\n\n```typescript\nimport { Middleware, IMiddleware } from '@midwayjs/core';\nimport { Context, NextFunction } from '@midwayjs/web';\n\n@Middleware()\nexport class ReportMiddleware implements IMiddleware<Context, NextFunction> {\n\n  resolve() {\n    return async (ctx: Context, next: NextFunction) => {\n      const startTime = Date.now();\n      await next();\n      console.log(Date.now() - startTime);\n    };\n  }\n\n}\n```\n\n:::caution\n注意\n\n1、如果要继续使用 EggJS 传统的函数式写法，必须将文件放在 `src/app/middleware` 下\n\n2、egg 自带的内置中间件已经集成\n\n:::\n\n应用中间件。\n\n```typescript\n// src/configuration.ts\nimport { App, Configuration } from '@midwayjs/core';\nimport * as egg from '@midwayjs/web';\nimport { ReportMiddleware } from './middleware/user.middleware';\n\n@Configuration({\n  imports: [egg]\n  // ...\n})\nexport class MainConfiguration {\n\n  @App()\n  app: egg.Application;\n\n  async onReady() {\n    this.app.useMiddleware(ReportMiddleware);\n  }\n}\n\n```\n\n更多用法请参考 [Web 中间件](../middleware)\n\n\n\n## 中间件顺序\n\n由于 egg 也有自己的中间件逻辑，在新版本中，我们将中间件加载顺序做了一定的处理，执行顺序如下：\n\n- 1、egg 框架中的中间件\n- 2、egg 插件通过 config.coreMiddleware 添加的顺序\n- 3、业务代码配置在 config.middleware 中配置的顺序\n- 4、app.useMiddleware 添加的顺序\n\n因为 midway 的中间件会后置加载，所以我们可以在 onReady 中进行自定义排序。\n\n\n\n## BodyParser\n\negg 自带  `bodyParser` 功能，默认会解析 `Post` 请求，自动识别 `json` 和 `form` 类型。\n\n如需 text 或者 xml，可以自行配置。\n\n默认的大小限制为 `1mb`，可以单独对每项配置大小。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  bodyParser: {\n    formLimit: '1mb',\n    jsonLimit: '1mb',\n    textLimit: '1mb',\n    xmlLimit: '1mb',\n  },\n}\n```\n\n注意，使用 Postman 做 Post 请求时的类型选择：\n\n![postman](https://img.alicdn.com/imgextra/i4/O1CN01QCdTsN1S347SuzZU5_!!6000000002190-2-tps-1017-690.png)\n\n\n\n\n## 定时任务\nv3 开始请参考 [bull 组件](./bull) 。\n\n如需兼容之前的 [egg 定时任务](https://eggjs.org/zh-cn/basics/schedule.html) ，请照下列方法。\n\n首先安装 `midway-schedule` 依赖。\n\n```bash\n$ npm i midway-schedule --save\n```\n\n添加到插件中即可。\n\n```typescript\n// src/config/plugin.ts\nexport default {\n  schedule: true,\n  schedulePlus: {\n    enable: true,\n    package: 'midway-schedule',\n  },\n};\n```\n\n使用请参考上一版本文档。\n\n\n\n## 日志\n\nv3 开始无法使用 egg-logger，请参考 [日志](../logger) 章节。\n\n\n\n## 异常处理\n\nEggJS 框架通过 [onerror](https://github.com/eggjs/egg-onerror) 插件提供了统一的错误处理机制，会作为 Midway 的兜底错误逻辑，和 [错误过滤器](../error_filter) 不冲突。\n\n对一个请求的所有处理方法（Middleware、Controller、Service）中抛出的任何异常都会被它捕获，并自动根据请求想要获取的类型返回不同类型的错误（基于 [Content Negotiation](https://tools.ietf.org/html/rfc7231#section-5.3.2)）。\n\n\n\n| 请求需求的格式 | 环境 | errorPageUrl 是否配置 | 返回内容 |\n| --- | --- | --- | --- |\n| HTML & TEXT | local & unittest | - | onerror 自带的错误页面，展示详细的错误信息 |\n| HTML & TEXT | 其他 | 是 | 重定向到 errorPageUrl |\n| HTML & TEXT | 其他 | 否 | onerror 自带的没有错误信息的简单错误页（不推荐） |\n| JSON & JSONP | local & unittest | - | JSON 对象或对应的 JSONP 格式响应，带详细的错误信息 |\n| JSON & JSONP | 其他 | - | JSON 对象或对应的 JSONP 格式响应，不带详细的错误信息 |\n\n\n\n\nonerror 插件的配置中支持 errorPageUrl 属性，当配置了 errorPageUrl 时，一旦用户请求线上应用的 HTML 页面异常，就会重定向到这个地址。\n\n\n在 `src/config/config.default.ts` 中\n```typescript\n// src/config/config.default.ts\nmodule.exports = {\n  onerror: {\n    // 线上页面发生异常时，重定向到这个页面上\n    errorPageUrl: '/50x.html',\n  },\n};\n```\n\n\n\n## 扩展 Application/Context/Request/Response\n\n\n### 增加扩展逻辑\n\n\n虽然 MidwayJS 并不希望直接将属性挂载到 koa 的 Context，App 上（会造成管理和定义的不确定性），但是 EggJS 的这项功能依旧可用。\n\n\n文件位置如下。\n```\n➜  my_midway_app tree\n.\n├── src\n│   ├── app\n│   │   └── extend\n│   │       ├── application.ts\n│   │       ├── context.ts\n│   │       ├── request.ts\n│   │       └── response.ts\n│   ├── config\n│   └── interface.ts\n├── test\n├── package.json\n└── tsconfig.json\n```\n内容和原来的 EggJS 相同。\n```typescript\n// src/app/extend/context.ts\nexport default {\n  get hello() {\n    return 'hello world';\n  },\n};\n```\n### 增加扩展定义\n\nContext 请使用 Midway 的方式来扩展，请查看 [扩展上下文定义](/docs/context_definition)。\n\n\n其余的部分，沿用 egg 的方式，请在 `src/interface.ts` 中扩展。\n```typescript\n// src/interface.ts\ndeclare module 'egg' {\n  interface Request {\n    // ...\n  }\n  interface Response {\n    // ...\n  }\n  interface Application {\n    // ...\n  }\n}\n```\n:::info\n业务自定义扩展的定义请 **不要放在根目录** `typings` 下，避免被 ts-helper 工具覆盖掉。\n:::\n\n\n\n## 使用 egg-scripts 部署\n\n由于 EggJS 提供了默认的多进程部署工具 `egg-scripts` ，Midway 也继续支持这种方式，如果上层是 EggJS，推荐这种部署方式。\n\n首先在依赖中，确保安装 `egg-scripts` 包。\n\n```bash\n$ npm i egg-scripts --save\n```\n\n\n\n添加 `npm scripts` 到 `package.json`：\n\n在上面的代码构建之后，使用我们的 `start` 和 `stop` 命令即可完成启动和停止。\n\n```json\n\"scripts\": {\n    \"start\": \"egg-scripts start --daemon --title=********* --framework=@midwayjs/web\",\n    \"stop\": \"egg-scripts stop --title=*********\",\n}\n```\n\n\n\n:::info\n\n`*********` 的地方是你的项目名。\n:::\n\n> 注意：`egg-scripts` 对 Windows 系统的支持有限，参见 [#22](https://github.com/eggjs/egg-scripts/pull/22)。\n\n####\n\n**启动参数**\n\n```bash\n$ egg-scripts start --port=7001 --daemon --title=egg-server-showcase\n```\n\nCopy\n\n如上示例，支持以下参数：\n\n- `--port=7001` 端口号，默认会读取环境变量 process.env.PORT，如未传递将使用框架内置端口 7001。\n- `--daemon` 是否允许在后台模式，无需 nohup。若使用 Docker 建议直接前台运行。\n- `--env=prod` 框架运行环境，默认会读取环境变量 process.env.EGG_SERVER_ENV， 如未传递将使用框架内置环境 prod。\n- `--workers=2` 框架 worker 线程数，默认会创建和 CPU 核数相当的 app worker 数，可以充分的利用 CPU 资源。\n- `--title=egg-server-showcase` 用于方便 ps 进程时 grep 用，默认为 `egg-server-\\${appname}`。\n- `--framework=yadan` 如果应用使用了[自定义框架](https://eggjs.org/zh-cn/advanced/framework.html)，可以配置 package.json 的 egg.framework 或指定该参数。\n- `--ignore-stderr` 忽略启动期的报错。\n- `--https.key` 指定 HTTPS 所需密钥文件的完整路径。\n- `--https.cert` 指定 HTTPS 所需证书文件的完整路径。\n- 所有 [egg-cluster](https://github.com/eggjs/egg-cluster) 的 Options 都支持透传，如 --port 等。\n\n更多参数可查看 [egg-scripts](https://github.com/eggjs/egg-scripts) 和 [egg-cluster](https://github.com/eggjs/egg-cluster) 文档。\n\n:::info\n\n使用 egg-scripts 部署的日志会存放在 **用户目录** 下**，**比如 `/home/xxxx/logs` 。\n\n:::\n\n\n\n## 启动环境\n\n原有 egg 使用 `EGG_SERVER_ENV` 中作为环境标志，在 Midway 中请使用 `MIDWAY_SERVER_ENV`。\n\n\n\n## State 类型定义\n\n在 egg 底层的 koa 的 Context 中有一个特殊的 State 属性，通过和 Context 类似的方式可以扩展 State 定义。\n\n```typescript\n// src/interface.ts\n\ndeclare module '@midwayjs/web/dist/interface' {\n  interface Context {\n    abc: string;\n  }\n\n  interface State{\n    bbb: string;\n    ccc: number;\n  }\n}\n```\n\n\n\n## 配置\n\n### 默认配置\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  egg: {\n    port: 7001,\n  },\n}\n```\n\n`@midwayjs/web`  所有参数如下：\n\n| 配置项         | 类型             | 描述                         |\n| -------------- | ---------------- | ---------------------------- |\n| port           | number           | 必填，启动的端口             |\n| key            | string           | Buffer                       |\n| cert           | string           | Buffer                       |\n| ca             | string           | Buffer                       |\n| hostname       | string           | 监听的 hostname，默认 127.1  |\n| http2          | boolean          | 可选，http2 支持，默认 false |\n| queryParseMode | simple\\|extended | 默认为 extended              |\n| queryParseOptions | `qs.IParseOptions` | 解析选项，当使用'simple'模式解析时可用 |\n\n以上的属性，对本地和使用 `bootstrap.js` 部署的应用生效。\n\n\n\n### 修改端口\n\n:::tip\n\n注意，这个方式只会对本地研发，以及使用 bootstrap.js 文件部署的项目生效。\n\n:::\n\n默认情况下，我们在 `config.default` 提供了 `7001` 的默认端口参数，修改它就可以修改 egg http 服务的默认端口。\n\n比如我们修改为 `6001`：\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  egg: {\n    port: 6001,\n  },\n}\n```\n\n默认情况下，单测环境由于需要 supertest 来启动端口，我们的 port 配置为 `null`。\n\n```typescript\n// src/config/config.unittest\nexport default {\n  // ...\n  egg: {\n    port: null,\n  },\n}\n```\n\n此外，也可以通过 `midway-bin dev --ts --port=6001` 的方式来临时修改端口，此方法会覆盖配置中的端口。\n\n\n\n### 全局前缀\n\n此功能请参考 [全局前缀](../controller#全局路由前缀)。\n\n\n\n### Https 配置\n\n在大多数的情况，请尽可能使用外部代理的方式来完成 Https 的实现，比如 Nginx。\n\n在一些特殊场景下，你可以通过配置 SSL 证书（TLS 证书）的方式，来直接开启 Https。\n\n首先，你需要提前准备好证书文件，比如 `ssl.key` 和 `ssl.pem`，key 为服务端私钥，pem 为对应的证书。\n\n然后配置即可。\n\n```typescript\n// src/config/config.default\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  egg: {\n    key: join(__dirname, '../ssl/ssl.key'),\n    cert: join(__dirname, '../ssl/ssl.pem'),\n  },\n}\n```\n\n\n\n### favicon 设置\n\n默认情况下，浏览器会发起一个 `favicon.ico` 的请求。\n\n```typescript\n// src/config/config.default\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  siteFile: {\n    '/favicon.ico': readFileSync(join(__dirname, 'favicon.png')),\n  },\n}\n```\n\n\n\n如果开启了 `@midwayjs/static-file` 组件，那么会优先使用组件的静态文件托管。\n\n### 修改上下文日志\n\n可以单独修改 egg 框架的上下文日志。\n\n```typescript\nexport default {\n  egg: {\n    contextLoggerFormat: info => {\n      const ctx = info.ctx;\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} [${ctx.userId} - ${Date.now() - ctx.startTime}ms ${ctx.method}] ${info.message}`;\n    }\n    // ...\n  },\n};\n```\n\n\n\n### Query 数组解析\n\n默认情况下，`ctx.query` 会解析为忽略数组的情况，而 `ctx.queries` 会严格的将所有的字段都变成数组。\n\n如果调整 `queryParseMode` ，则可以使 `ctx.query` 变为两者之间的结构（querystring 的结果）。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  egg: {\n    // ...\n    queryParseMode: 'simple',\n    queryParseOptions: {\n      arrayLimit: 100,\n    },\n  },\n}\n```\n\n\n\n\n## 常见问题\n### 1、生成 ts 定义\n\n\nMidway 提供了 `@midwayjs/egg-ts-hepler` 工具包，用于快速生成 EggJS 开发时所依赖的定义。\n```bash\n$ npm install @midwayjs/egg-ts-helper --save-dev\n```\n在 `package.json` 中加入对应的 `ets` 命令即可，一般来说，我们会在 dev 命令前加入，以保证代码的正确性。\n```json\n  \"scripts\": {\n    \"dev\": \"cross-env ets && cross-env NODE_ENV=local midway-bin dev --ts\",\n  },\n```\n:::info\n在第一次编写代码前，需要执行一次此命令才能有 ts 定义生成。\n:::\n\n\nEggJS 生成的定义在 `typings` 目录中。\n```\n➜  my_midway_app tree\n.\n├── src                            ## midway 项目源码\n├── typings                        ## EggJS 定义生成目录\n├── test\n├── package.json\n└── tsconfig.json\n```\n\n\n### 2、EggJS 中 Configuration 的特殊情况\n\n\n在 EggJS 下， `configuration.ts`  中的生命周期**只会在 worker 下加载执行**。如果在 Agent 有类似的需求，请直接使用 EggJS 自身的 `agent.ts` 处理。\n\n\n\n### 3、异步初始化配置无法覆盖插件配置\n\n`onConfigLoad` 生命周期会在 egg 插件（若有）初始化之后执行，所以不能用于覆盖 egg 插件所使用的配置。\n\n\n\n### 4、默认的 csrf 错误\n\n\n在 post 请求，特别是第一次时用户会发现一个 csrf 报错。原因是 egg 在框架中默认内置了安全插件 [egg-security](https://github.com/eggjs/egg-security)， 默认开启了  csrf 校验。\n\n\n我们可以在配置中关闭它，但是更好的是去[**了解它**](https://eggjs.org/zh-cn/core/security.html#%E5%AE%89%E5%85%A8%E5%A8%81%E8%83%81-csrf-%E7%9A%84%E9%98%B2%E8%8C%83)之后再做选择。\n\n```typescript\nexport const security = {\n  csrf: false,\n};\n```\n\n\n\n\n### 5、不存在定义的问题\n\n一些 egg 插件未提供 ts 定义，导致使用会出现未声明方法的情况，比如 egg-mysql。\n![image.png](https://img.alicdn.com/imgextra/i1/O1CN01mv68zG1zN6nALff8n_!!6000000006701-2-tps-1478-876.png)\n可以使用 any 绕过。\n\n```typescript\nawait (this.app as any).mysql.query(sql);\n```\n\n或者可以自行增加扩展定义。\n\n### 6、获取 Http Server\n\nEggjs 内部封装了原始的 HttpServer，需要通过事件获取。\n\n```typescript\n// src/configuration.ts\nimport { Configuration, App } from '@midwayjs/core';\nimport { Application } from '@midwayjs/web';\n\n@Configuration(/***/)\nexport class MainConfiguration {\n  \n  @App('egg')\n  app: Application;\n  \n  // ...\n  async onServerReady() {\n    this.app.once('server', (server) => {\n      // ...\n    })\n  }\n}\n```\n\n<!-- File: site/docs/extensions/etcd.md (Tokens: ~1708 | Category: anthropic-skill) -->\n# ETCD\n\netcd 是云原生架构中重要的基础组件，由 CNCF 孵化托管。etcd 在微服务和 Kubernetes 集群中可以作为服务注册于发现，也可以作为 key-value 存储的中间件。\n\nMidway 提供基于 [etcd3](https://github.com/microsoft/etcd3) 模块封装的组件，提供 etcd 的客户端调用能力。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/etcd@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/etcd\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n\n## 引入组件\n\n\n首先，引入 组件，在 `configuration.ts` 中导入：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as etcd from '@midwayjs/etcd';\nimport { join } from 'path'\n\n@Configuration({\n  imports: [\n    // ...\n    etcd,\n  ],\n  // ...\n})\nexport class MainConfiguration {\n}\n```\n\n\n\n## 配置默认客户端\n\n大部分情况下，我们可以只使用默认客户端来完成功能。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  etcd: {\n    client: {\n      host: [\n        '127.0.0.1:2379'\n      ]\n    },\n  },\n}\n```\n\n\n\n## 使用默认客户端\n\n配置完成后，我们就可以在代码中使用了。\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { ETCDService } from '@midwayjs/etcd';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  etcdService: ETCDService;\n\n  async invoke() {\n\n    await this.etcdService.put('foo').value('bar');\n\n    const fooValue = await this.etcdService.get('foo').string();\n    console.log('foo was:', fooValue);\n\n    const allFValues = await this.etcdService.getAll().prefix('f').keys();\n    console.log('all our keys starting with \"f\":', allFValues);\n\n    await this.etcdService.delete().all();\n  }\n}\n```\n\n更多 API 请参考 ts 定义或者 [官网文档](https://microsoft.github.io/etcd3/classes/etcd3.html)。\n\n\n\n## 多实例配置\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  etcd: {\n    clients: {\n      instance1: {\n        {\n          host: [\n            '127.0.0.1:2379'\n          ]\n        },\n      },\n  \t\tinstance2: {\n        {\n          host: [\n            '127.0.0.1:2379'\n          ]\n        },\n      }\n    }\n  },\n}\n```\n\n\n\n## 多实例获取\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { ETCDServiceFactory } from '@midwayjs/etcd';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  etcdServiceFactory: ETCDServiceFactory;\n\n  async invoke() {\n    const instance1 = this.etcdServiceFactory.get('instance1');\n    // ...\n\n    const instance2 = this.etcdServiceFactory.get('instance2');\n    // ...\n  }\n}\n```\n\n\n\n## 动态创建实例\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { ETCDServiceFactory } from '@midwayjs/etcd';\nimport { join } from 'path';\n\n@Provide()\nexport class UserService {\n\n  @Inject()\n  etcdServiceFactory: ETCDServiceFactory;\n\n  async invoke() {\n    const instance3 = await this.etcdServiceFactory.createInstance({\n      host: [\n        '127.0.0.1:2379'\n      ]\n    }, 'instance3');\n    // ...\n  }\n}\n```\n\n\n\n## 服务发现\n\n从 v4 开始提供基于统一抽象的服务发现能力，虽然 ETCD 并不太适合作为服务发现的注册中心来使用，但是我们仍实现了 ETCD 作为服务发现的客户端能力。\n\n服务发现包括 **注册服务** 和 **获取服务** 两部分。\n\n\n### 配置服务发现\n\n你需要先配置一个客户端；若存在多个客户端，可通过 `serviceDiscoveryClient` 指定用于服务发现的客户端。\n\n通过 `serviceDiscovery` 配置服务发现选项：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  etcd: {\n    clients: {\n      default: { hosts: ['127.0.0.1:2379'] }\n    },\n    serviceDiscovery: {\n      namespace: 'services',\n      ttl: 30,\n      // 可选：loadBalancer: LoadBalancerType.ROUND_ROBIN\n    }\n  }\n}\n```\n\n说明：`namespace` 为键空间前缀，实例键结构为 `namespace/serviceName/instanceId`；`ttl` 为实例过期时间（秒）。\n\n\n### 注册服务发现\n\n服务启动后，将当前服务注册到 ETCD：\n\n```typescript\nimport { Configuration, Inject } from '@midwayjs/core';\nimport { EtcdServiceDiscovery } from '@midwayjs/etcd';\n\n@Configuration({})\nexport class MainConfiguration {\n  @Inject()\n  etcdDiscovery: EtcdServiceDiscovery;\n\n  async onServerReady() {\n    // 创建服务发现客户端（可传入覆盖项）\n    const client = this.etcdDiscovery.createClient();\n\n    // 注册当前服务并上线（写入键并绑定租约）\n    await client.register({\n      id: client.defaultMeta.id,\n      serviceName: 'order',\n      ttl: 30,\n      meta: {\n        version: '1.0.0',\n        host: client.defaultMeta.host,\n        port: '7001'\n      }\n    });\n  }\n}\n```\n\n说明：注册时会默认上线，无需显式调用 `online`。ETCD 通过 Lease 维持 TTL，定时续约保证存活；`offline` 删除键并撤销租约；`deregister` 注销并清理状态。重复调用 `register/online/offline/deregister` 具有幂等性。\n\n\n### 获取可用服务\n\n在任意位置获取可用实例或按负载均衡选择一个实例：\n\n````typescript\n@Provide()\nexport class OrderService {\n  @Inject()\n  etcdDiscovery: EtcdServiceDiscovery;\n\n  async getService() {\n    const instances = await this.etcdDiscovery.getInstances('order');\n    const one = await this.etcdDiscovery.getInstance('order');\n    return { instances, one };\n  }\n}\n````\n\n返回的实例为注册时写入的对象（`EtcdInstanceMetadata`），通常包含 `serviceName/id/ttl/meta/tags/status` 等字段。\n\n<!-- File: site/docs/extensions/events.md (Tokens: ~2475 | Category: anthropic-skill) -->\n# 事件\n\n事件组件是基于 [eventemitter2](https://github.com/EventEmitter2/EventEmitter2) 实现的，提供了强大的事件处理能力。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/event-emitter@4 --save\n```\n\n或者在 `package.json` 中增加依赖后，重新安装\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/event-emitter\": \"^4.0.0\"\n  }\n}\n```\n\n## 使用组件\n\n首先在 `configuration.ts` 中引入组件：\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as eventEmitter from '@midwayjs/event-emitter';\n\n@Configuration({\n  imports: [\n    // ...\n    eventEmitter\n  ]\n})\nexport class MainConfiguration {\n}\n```\n\n## 基础使用\n\n### 定义事件监听器\n\n:::caution\n注意：`@OnEvent()` 装饰器只能用在单例（Singleton）类上。如果在非单例类上使用，事件监听器将无法正常工作。\n:::\n\n使用 `@OnEvent()` 装饰器来定义事件监听器：\n\n```typescript\nimport { Provide, Singleton } from '@midwayjs/core';\nimport { OnEvent } from '@midwayjs/event-emitter';\n\n@Provide()\n@Singleton()    // 必须使用 @Singleton 装饰器\nexport class UserService {\n  @OnEvent('user.created')\n  async handleUserCreated(user: any) {\n    console.log('新用户创建:', user);\n  }\n}\n```\n\n### 发送事件\n\n通过注入 `EventEmitterService` 来发送事件：\n\n```typescript\nimport { Inject } from '@midwayjs/core';\nimport { EventEmitterService } from '@midwayjs/event-emitter';\n\n@Provide()\nexport class UserController {\n  @Inject()\n  eventEmitterService: EventEmitterService;\n\n  async createUser() {\n    const user = { id: 1, name: 'harry' };\n    // ... 创建用户逻辑\n    \n    // 发送同步事件\n    this.eventEmitterService.emit('user.created', user);\n    \n    // 或者发送异步事件\n    await this.eventEmitterService.emitAsync('user.created', user);\n  }\n}\n```\n\n## 高级特性\n\n### 事件监听器选项\n\n`@OnEvent()` 装饰器支持多个配置选项，每个选项都有其特定的用途：\n\n#### prependListener\n\n用于控制监听器的执行顺序。设置为 `true` 时，会将监听器添加到监听器队列的开头，这样可以确保该监听器最先执行。\n\n```typescript\n@Provide()\n@Singleton()\nexport class UserService {\n  // 这个监听器会最先执行\n  @OnEvent('user.created', { prependListener: true })\n  async handleUserCreatedFirst(user: any) {\n    console.log('第一个处理用户创建事件');\n  }\n\n  // 这个监听器会在队列末尾执行\n  @OnEvent('user.created')\n  async handleUserCreated(user: any) {\n    console.log('后续处理用户创建事件');\n  }\n}\n```\n\n#### suppressErrors\n\n控制事件处理器中的错误处理行为：\n- 默认值为 `true`，表示错误会被捕获并忽略\n- 设置为 `false` 时，错误会被抛出，可以在外层捕获\n\n```typescript\n@Provide()\n@Singleton()\nexport class UserService {\n  // 错误会被抛出\n  @OnEvent('user.created', { suppressErrors: false })\n  async handleWithError(user: any) {\n    throw new Error('处理失败');  // 这个错误会被抛出\n  }\n\n  // 错误会被忽略\n  @OnEvent('user.created', { suppressErrors: true })\n  async handleSuppressError(user: any) {\n    throw new Error('处理失败');  // 这个错误会被忽略\n  }\n}\n\n// 在调用处可以捕获错误\n@Provide()\nexport class UserController {\n  @Inject()\n  eventEmitterService: EventEmitterService;\n\n  async createUser() {\n    try {\n      await this.eventEmitterService.emitAsync('user.created', { id: 1 });\n    } catch (err) {\n      // 可以捕获到 suppressErrors: false 的处理器抛出的错误\n      console.error('事件处理失败:', err);\n    }\n  }\n}\n```\n\n#### async\n\n控制事件处理器的执行方式：\n- 设置为 `true` 时，处理器会在单独的事件循环中异步执行\n- 默认为 `false`，会按顺序同步执行\n\n```typescript\n@Provide()\n@Singleton()\nexport class UserService {\n  // 异步执行，不会阻塞其他处理器\n  @OnEvent('user.created', { async: true })\n  async handleAsync(user: any) {\n    await someTimeConsumingTask();\n    console.log('异步处理完成');\n  }\n\n  // 同步执行，会按顺序等待\n  @OnEvent('user.created')\n  async handleSync(user: any) {\n    console.log('同步处理');\n  }\n}\n```\n\n### 通配符事件\n\n通配符事件允许你使用模式匹配来监听一组相关的事件。需要在配置中启用 `wildcard: true`。\n\n#### 配置启用\n\n```typescript\n// config/config.default.ts\nexport default {\n  eventEmitter: {\n    wildcard: true,  // 启用通配符功能\n  }\n}\n```\n\n#### 使用示例\n\n```typescript\n@Provide()\n@Singleton()\nexport class UserService {\n  // 监听所有用户相关事件\n  @OnEvent('user.*')\n  async handleUserEvents(...args: any[]) {\n    // 注意：通配符模式下，事件处理器直接接收发送事件时传入的参数\n    console.log('事件参数:', args);\n  }\n\n  // 监听多层级的事件\n  @OnEvent('user.*.detail')\n  async handleUserDetailEvents(...args: any[]) {\n    // 可以匹配 user.profile.detail, user.settings.detail 等\n    console.log('事件参数:', args);\n  }\n}\n\n// 发送事件示例\n@Provide()\nexport class UserController {\n  @Inject()\n  eventEmitterService: EventEmitterService;\n\n  async updateUser() {\n    // 这些事件都会被上面的 user.* 处理器捕获\n    await this.eventEmitterService.emitAsync('user.created', { id: 1 });\n    await this.eventEmitterService.emitAsync('user.updated', { id: 1, name: 'new name' });\n    await this.eventEmitterService.emitAsync('user.profile.detail', { id: 1, profile: {} });\n  }\n}\n```\n\n:::info\n注意：在使用通配符事件时，事件处理器的参数直接是事件发送时传入的参数。无法在处理器中获取具体触发的事件名称，所以建议在发送事件时将必要的信息包含在事件参数中。\n:::\n\n### 命名空间事件\n\n命名空间事件提供了一种组织和分组事件的方式。通过 `delimiter` 配置分隔符。\n\n#### 配置启用\n\n```typescript\n// config/config.default.ts\nexport default {\n  eventEmitter: {\n    delimiter: '.' // 设置命名空间分隔符\n  }\n}\n```\n\n#### 使用示例\n\n```typescript\n@Provide()\n@Singleton()\nexport class OrderService {\n  // 监听订单命名空间下的创建事件\n  @OnEvent('order.created')\n  async handleOrderCreated(order: any) {\n    console.log('订单创建:', order);\n  }\n\n  // 如果需要使用通配符匹配，需要额外启用 wildcard: true\n  @OnEvent('order.payment.*')\n  async handleOrderPaymentEvents(...args: any[]) {\n    // 可以匹配 order.payment.success, order.payment.failed 等\n    console.log('事件参数:', args);\n  }\n}\n\n// 发送命名空间事件\n@Provide()\nexport class PaymentService {\n  @Inject()\n  eventEmitterService: EventEmitterService;\n\n  async processPayment() {\n    // 发送支付成功事件\n    await this.eventEmitterService.emitAsync('order.payment.success', {\n      orderId: '123',\n      amount: 100\n    });\n  }\n}\n```\n\n:::info\n注意：命名空间的分隔符默认是 `.`，你可以通过 `delimiter` 配置修改。如果需要使用通配符（如 `*`）进行事件匹配，则需要额外启用 `wildcard: true` 配置。\n:::\n\n## 配置\n\n在 `config.default.ts` 中可以配置 Event Emitter 的行为：\n\n```typescript\nexport default {\n  // ...\n  eventEmitter: {\n    // 是否开启通配符事件\n    wildcard: true,\n    // 命名空间分隔符\n    delimiter: '.',\n    // 是否启用新监听器事件\n    newListener: false,\n    // 是否启用移除监听器事件\n    removeListener: false,\n    // 设置最大监听器数量\n    maxListeners: 10,\n    // 是否启用详细的内存泄漏检测\n    verboseMemoryLeak: false,\n  }\n}\n```\n\n## 常见问题\n\n### 1. 事件处理顺序\n\n- 默认情况下，事件处理器按照注册顺序执行\n- 使用 `prependListener: true` 可以将处理器添加到队列开头\n- 异步事件处理器（`emitAsync`）会按顺序等待每个处理器完成\n\n### 2. 错误处理\n\n- 默认情况下，事件处理器中的错误会被抑制（`suppressErrors: true`）\n- 设置 `suppressErrors: false` 可以让错误抛出\n- 使用 `emitAsync` 时，可以通过 try/catch 捕获错误\n\n### 3. 性能考虑\n\n- 合理使用通配符事件，过多的通配符匹配可能影响性能\n- 设置适当的 `maxListeners` 值，避免内存泄漏\n- 及时移除不需要的事件监听器\n\n<!-- File: site/docs/extensions/express.md (Tokens: ~3564 | Category: anthropic-skill) -->\n# Express\n\n本章节内容，主要介绍在 Midway 中如何使用 Express 作为上层框架，并使用自身的能力。\n\n| 描述           |      |\n| -------------- | ---- |\n| 包含独立主框架 | ✅    |\n| 包含独立日志   | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/express@4 --save\n$ npm i @types/body-parser @types/express @types/express-session --save-dev\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/express\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    \"@types/body-parser\": \"^1.19.2\",\n    \"@types/express\": \"^4.17.13\",\n    \"@types/express-session\": \"^1.17.4\",\n    // ...\n  }\n}\n```\n\n也可以直接使用脚手架创建示例。\n\n```bash\n# npm v6\n$ npm init midway --type=express-v3 my_project\n\n# npm v7\n$ npm init midway -- --type=express-v3 my_project\n```\n\n\n针对 Express，Midway 提供了 `@midwayjs/express` 包进行了适配，在其中提供了 Midway 特有的依赖注入、切面等能力。\n\n:::info\n我们使用的 Express 版本为 `v4` 。\n:::\n\n\n## 目录结构\n```\n.\n├── src\n│   ├── controller\t\t\t\t\t\t\t\t \t\t\t\t# controller接口的地方\n│   ├── service\t\t\t\t\t\t\t\t\t \t\t\t\t\t# service逻辑处理的地方\n|   └── configuration.ts\t\t\t\t\t\t\t\t\t# 入口及生命周期配置、组件管理\n├── test\n├── package.json\n└── tsconfig.json\n```\n\n\n\n## 开启组件\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [express],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: express.Application;\n\n  async onReady() {}\n}\n```\n\n\n\n\n## 控制器（Controller）\n\n\n整个请求控制器的写法和 Midway 适配其他框架的类似。为了和其他场景的框架写法一致，在请求的时候，Midway 将 Express 的 `req` 映射为 `ctx` 对象。\n```typescript\nimport { Inject, Controller, Get, Provide, Query } from '@midwayjs/core';\nimport { Context, NextFunction } from '@midwayjs/express';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  ctx: Context;\n\n  @Get('/')\n  async home(@Query() id) {\n    console.log(id);\t\t\t\t\t\t// req.query.id === id\n    return 'hello world';\t\t\t\t// 简单返回，等价于 res.send('hello world');\n  }\n}\n```\n你也可以额外注入 `req` 和 `res` 。\n```typescript\nimport { Inject, Controller, Get, Provide, Query } from '@midwayjs/core';\nimport { Context, Response, NextFunction } from '@midwayjs/express';\n\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  ctx: Context;   // 即为 req\n\n  @Inject()\n  req: Context;\n\n  @Inject()\n  res: Response;\n\n  @Get('/')\n  async home(@Query() id) {\n    // this.req.query.id === id\n  }\n}\n```\n\n\n\n## Web 中间件\n\n\nExpress 的中间件写法比较特殊，它的参数不同。\n\n\n```typescript\nimport { Middleware } from '@midwayjs/core';\nimport { Context, Response, NextFunction } from '@midwayjs/express';\n\n@Middleware()\nexport class ReportMiddleware implements IMiddleware<Context, Response, NextFunction> {\n\n  resolve() {\n    return async (\n      req: Context,\n      res: Response,\n      next: NextFunction\n    ) => {\n      console.log('Request...');\n      next();\n    };\n  }\n\n}\n```\n\n注意，这里我们导出了一个 `ReportMiddleware` 类，为了方便对接异步流程，`resolve` 返回可以是 async 函数。\n\nExpress 中的 next 方法，用于调用到下一个中间件，指的注意的是，Express 中间件并非洋葱模型，是单向调用。\n\n\n\n\n### 路由中间件\n\n\n我们可以把上面编写的中间件应用到单个 Controller 上，也可以将中间件应用到单个路由上。\n\n\n```typescript\nimport { Controller, Get, Provide } from '@midwayjs/core';\n\n@Controller('/', { middleware: [ ReportMiddleware ]})\t\t\t// controller 级别的中间件\nexport class HomeController {\n\n  @Get('/', { middleware: [ ReportMiddleware ]})\t\t\t\t// 路由级别的中间件\n  async home() {\n    return 'hello world'\n  }\n}\n```\n\n\n### 全局中间件\n\n\n直接使用 Midway 提供的 `app.generateMiddleware` 方法，在入口处加载全局中间件。\n```typescript\n// src/configuration.ts\nimport { Configuration, ILifeCycle } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport { ReportMiddleware } from './middleware/report.middleware.ts'\n\n@Configuration({\n  imports: [express],\n})\nexport class MainConfiguration implements ILifeCycle {\n\n  @App()\n  app: express.Application;\n\n  async onReady() {\n    this.app.useMiddleware(ReportMiddleware);\n  }\n}\n```\n\n\n除了加载 Class 形式的中间件外，也支持加载传统的 Express 中间件。\n```typescript\n// src/configuration.ts\nimport { Configuration, ILifeCycle, App } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport { join } from 'path';\n\n@Configuration({\n  imports: [express],\n})\nexport class MainConfiguration implements ILifeCycle {\n\n  @App()\n  app: express.Application;\n\n  async onReady() {\n    this.app.useMiddleware((req, res, next) => {\n    \t// xxx\n    });\n  }\n}\n```\n你可以通过注入 `app` 对象，来调用到所有 Express 上的方法。\n\n\n\n## 返回统一处理\n\n由于 Express 中间件是单向调用，无法在返回时执行，为此我们额外设计了一个 `@Match` 装饰的过滤器，用于处理返回值的行为。\n\n比如，我们可以定义针对全局返回的过滤器。\n\n```typescript\n// src/filter/globalMatch.filter.ts\nimport { Match } from '@midwayjs/core';\nimport { Context, Response } from '@midwayjs/express';\n\n@Match()\nexport class GlobalMatchFilter {\n  match(value, req, res) {\n    // ...\n    return {\n      status: 200,\n      data: {\n        value\n      },\n    };\n  }\n}\n```\n\n也可以匹配特定的路由做返回。\n\n```typescript\n// src/filter/api.filter.ts\nimport { Match } from '@midwayjs/core';\nimport { Context, Response } from '@midwayjs/express';\n\n@Match((ctx: Context, res: Response) => {\n  return ctx.path === '/api';\n})\nexport class APIMatchFilter {\n  match(value, req: Context, res: Response) {\n    // ...\n    return {\n      data: {\n        message:\n        data: value,\n      },\n    };\n  }\n}\n```\n\n需要应用到 app 中。\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport { join } from 'path';\nimport { APIMatchFilter } from './filter/api.filter';\nimport { GlobalMatchFilter } from 'filter/globalMatch.filter';\n\n@Configuration({\n  imports: [express],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: express.Application;\n\n  async onReady() {\n    // ...\n    this.app.useFilter([APIMatchFilter, GlobalMatchFilter]);\n  }\n}\n```\n\n注意，这类过滤器是按照添加的顺序来匹配执行。\n\n\n\n## 错误处理\n\n和普通的项目相同，使用错误过滤器，但是参数略有不同。\n\n```typescript\nimport { Catch } from '@midwayjs/core';\nimport { Context, Response } from '@midwayjs/express';\n\n@Catch()\nexport class GlobalError {\n  catch(err: Error, req: Context, res: Response) {\n    if (err) {\n      return {\n        status: err.status ?? 500,\n        message: err.message,\n      }\n    }\n  }\n}\n```\n\n需要应用到 app 中。\n\n```typescript\nimport { Configuration, App } from '@midwayjs/core';\nimport * as express from '@midwayjs/express';\nimport { join } from 'path';\nimport { GlobalError } from './filter/global.filter';\n\n@Configuration({\n  imports: [express],\n  importConfigs: [join(__dirname, './config')],\n})\nexport class MainConfiguration {\n  @App()\n  app: express.Application;\n\n  async onReady() {\n    this.app.useMiddleware((req, res, next) => {\n      next();\n    });\n\n    this.app.useFilter([GlobalError]);\n  }\n}\n```\n\n注意，`@Match` 和 `@Catch` 都是过滤器，在内部会自动判断做区分执行。。\n\n\n\n## Cookie\n\n`@midwayjs/express` 自带  `cookie parser` 功能，使用的是 `cookie-parser` 模块。\n\n针对 Cookie，统一使用 `keys` 作为秘钥。\n\n```typescript\n// src/config/config.default\nexport default {\n  keys:  ['key1', 'key2'],\n}\n```\n\n获取 Cookie。\n\n```typescript\nconst cookieValue = req.cookies['cookie-key'];\n```\n\n设置 Cookie。\n\n```typescript\nres.cookie(\n  'cookie-key',\n  'cookie-value',\n  cookieOptions\n);\n```\n\n\n\n##  Session\n\n`@midwayjs/express` 内置了 Session 组件，给我们提供了 `ctx.session` 来访问或者修改当前用户 Session 。\n\n默认情况下为 `cookie-session` ，默认配置如下。\n\n```typescript\n// src/config/config.default\nexport default {\n  session:  {\n    name: 'MW_SESS',\n    resave: true,\n    saveUninitialized: true,\n    cookie: {\n      maxAge: 24 * 3600 * 1000, // ms\n      httpOnly: true,\n      // sameSite: null,\n    },\n  }\n}\n```\n\n我们可以通过简单的 API 来设置 session。\n\n```typescript\n@Controller('/')\nexport class HomeController {\n\n  @Inject()\n  req;\n\n  @Get('/')\n  async get() {\n    // set all\n    this.req.session = req.query;\n\n    // set value\n    this.req.session.key = 'abc';\n\n    // get\n    const key = this.req.session.key;\n\n    // remove\n    this.req.session = null;\n\n    // set max age\n    this.req.session.maxAge = Number(req.query.maxAge);\n\n    // ...\n  }\n}\n\n```\n\n\n\n## BodyParser\n\n`@midwayjs/express` 自带  `bodyParser` 功能，默认会解析 `Post` 请求，自动识别 `json` 、`text`和 `urlencoded` 类型。\n\n默认的大小限制为 `1mb`，可以单独对每项配置大小。\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  bodyParser: {\n    json: {\n      enable: true,\n      limit: '1mb',\n      strict: true,\n    },\n    raw: {\n      enable: false,\n      limit: '1mb',\n    },\n    text: {\n      enable: true,\n      limit: '1mb',\n    },\n    urlencoded: {\n      enable: true,\n      extended: false,\n      limit: '1mb',\n      parameterLimit: 1000,\n    },\n  },\n}\n```\n\n\n\n## 配置\n\n### 默认配置\n\n`@midwayjs/express`  的配置样例如下：\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  express: {\n    port: 7001,\n  },\n}\n```\n\n所有属性描述如下：\n\n| 属性         | 类型                                         | 描述                                                    |\n| ------------ |--------------------------------------------| ------------------------------------------------------- |\n| port         | number                                     | 可选，启动的端口                                        |\n| globalPrefix | string                                     | 可选，全局的 http 前缀                                  |\n| keys         | string[]                                   | 可选，Cookies 签名，如果上层未写 keys，也可以在这里设置 |\n| hostname     | string                                     | 可选，监听的 hostname，默认 127.1                       |\n| key          | string \\| Buffer \\| Array\\<Buffer\\|Object> | 可选，Https key，服务端私钥                             |\n| cert         | string \\| Buffer \\| Array\\<Buffer\\|Object> | 可选，Https cert，服务端证书                            |\n| ca           | string \\| Buffer \\| Array\\<Buffer\\|Object> | 可选，Https ca                                          |\n| http2        | boolean                                    | 可选，http2 支持，默认 false                            |\n\n\n\n### 修改端口\n\n默认情况下，我们在 `config.default` 提供了 `7001` 的默认端口参数，修改它就可以修改 Express http 服务的默认端口。\n\n比如我们修改为 `6001`：\n\n```typescript\n// src/config/config.default\nexport default {\n  // ...\n  express: {\n    port: 6001,\n  },\n}\n```\n\n默认情况下，单测环境由于需要 supertest 来启动端口，我们的 port 配置为 `null`。\n\n```typescript\n// src/config/config.unittest\nexport default {\n  // ...\n  express: {\n    port: null,\n  },\n}\n```\n\n此外，也可以通过 `midway-bin dev --ts --port=6001` 的方式来临时修改端口，此方法会覆盖配置中的端口。\n\n\n\n### 全局前缀\n\n此功能请参考 [全局前缀](../controller#全局路由前缀)。\n\n\n\n### Https 配置\n\n在大多数的情况，请尽可能使用外部代理的方式来完成 Https 的实现，比如 Nginx。\n\n在一些特殊场景下，你可以通过配置 SSL 证书（TLS 证书）的方式，来直接开启 Https。\n\n首先，你需要提前准备好证书文件，比如 `ssl.key` 和 `ssl.pem`，key 为服务端私钥，pem 为对应的证书。\n\n然后配置即可。\n\n```typescript\n// src/config/config.default\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport default {\n  // ...\n  express: {\n    key: join(__dirname, '../ssl/ssl.key'),\n    cert: join(__dirname, '../ssl/ssl.pem'),\n  },\n}\n```\n\n\n\n### 修改上下文日志\n\n可以单独修改 express 框架的上下文日志。\n\n```typescript\nexport default {\n  express: {\n    contextLoggerFormat: info => {\n      // 等价 req\n      const req = info.ctx;\n      const userId = req?.['session']?.['userId'] || '-';\n      return `${info.timestamp} ${info.LEVEL} ${info.pid} [${userId} - ${Date.now() - req.startTime}ms ${req.method}] ${info.message}`;\n    }\n    // ...\n  },\n};\n```\n\n<!-- File: site/docs/extensions/grpc.md (Tokens: ~7209 | Category: anthropic-skill) -->\n# gRPC\n\ngRPC 是一个高性能、通用的开源 RPC 框架，其由 Google 主要面向移动应用开发并基于 HTTP/2 协议标准而设计，基于 ProtoBuf(Protocol Buffers) 序列化协议开发，且支持众多开发语言。\n\n\n本篇内容演示了如何在 Midway 体系下，提供 gRPC 服务，以及调用 gRPC 服务的方法。\n\n\nMidway 当前采用了最新的 gRPC 官方推荐的 [@grpc/grpc-js](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js) 进行开发，并提供了一些工具包，用于快速发布服务和调用服务。\n\n我们使用的模块为 `@midwayjs/grpc` ，既可以独立发布服务，又可以接入其它框架调用 gRPC 服务。\n\n相关信息：\n\n**提供服务**\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ❌    |\n| 可用于一体化      | ✅    |\n\n**调用服务**\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n\n**其他**\n\n| 描述                 |      |\n| -------------------- | ---- |\n| 可作为主框架独立使用 | ✅    |\n| 可独立添加中间件     | ✅    |\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/grpc@4 --save\n$ npm i @midwayjs/grpc-helper --save-dev\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/grpc\": \"^4.0.0\",\n    // ...\n  },\n  \"devDependencies\": {\n    \"@midwayjs/grpc-helper\": \"^1.0.0\",\n    // ...\n  }\n}\n```\n\n\n\n## 开启组件\n\n:::tip\n\n不管是提供服务还是调用服务，都需要开启组件。\n\n:::\n\n`@midwayjs/grpc` 可以作为独立主框架使用。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as grpc from '@midwayjs/grpc';\n\n@Configuration({\n  imports: [grpc],\n  // ...\n})\nexport class MainConfiguration {\n  async onReady() {\n\t\t// ...\n  }\n}\n\n```\n\n也可以附加在其他的主框架下，比如 `@midwayjs/koa` 。\n\n```typescript\n// src/configuration.ts\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as grpc from '@midwayjs/grpc';\n\n@Configuration({\n  imports: [koa, grpc],\n  // ...\n})\nexport class MainConfiguration {\n  async onReady() {\n\t\t// ...\n  }\n}\n\n```\n\n\n\n## 目录结构\n\n大致的目录结构如下，`src/provider` 是提供 gRPC 服务的目录。\n\n```\n.\n├── package.json\n├── proto                         ## proto 定义文件\n│   └── helloworld.proto\n├── src\n│   ├── configuration.ts          ## 入口配置文件\n│   ├── interface.ts\n│   └── provider                  ## gRPC 提供服务的文件\n│       └── greeter.ts\n├── test\n├── bootstrap.js                  ## 服务启动入口\n└── tsconfig.json\n```\n\n\n\n## 定义服务接口\n\n\n在微服务中，定义一个服务需要特定的接口定义语言（IDL）来完成，在 gRPC中 默认使用 Protocol Buffers  作为序列化协议。\n\n\n序列化协议独立于语言和平台，提供了多种语言的实现，Java，C++，Go 等等,每一种实现都包含了相应语言的编译器和库文件。所以 gRPC 是一个提供和调用都可以跨语言的服务框架。\n\n一个gRPC服务的大体架构可以用官网上的一幅图表示。\n\n![](https://img.alicdn.com/imgextra/i3/O1CN01kpIyg51k8i5DtcGpZ_!!6000000004639-2-tps-621-445.png)\n\n\nProtocol Buffers  协议的文件，默认的后缀为 `.proto` 。.proto后缀的IDL文件,并通过其编译器生成特定语言的数据结构、服务端接口和客户端Stub代码。\n\n\n:::info\n由于 proto 文件可以跨语言使用，为了方便共享，我们一般将 proto 文件放在 src 目录外侧，方便其他工具复制分发。\n:::\n\n\n下面是一个基础的 `proto/helloworld.proto`  文件。\n```protobuf\nsyntax = \"proto3\";\n\npackage helloworld;\n\nservice Greeter {\n  // Sends a greeting\n  rpc SayHello (HelloRequest) returns (HelloReply) {}\n}\n\nmessage HelloRequest {\n  string name = 1;\n}\n\nmessage HelloReply {\n  string message = 1;\n}\n\n```\n\n\nproto3 表示的是第三版的 protobuf 协议，是 gRPC 目前推荐的版本，“语法简单，功能更全”。\n\n\n我们可以用 `service` 格式，定义服务体，其中可以包含方法。同时，我们可以更加细致的通过 `message` 描述服务具体的请求参数和响应参数。\n\n\n我们可以从 [Google 的官网文档](https://developers.google.com/protocol-buffers/docs/overview#simple) 中查看更多细节。\n\n\n:::info\n大家会看到，这和 Java 中的 Class 非常相像，每个结构就相当于 Java 中的一个类。\n:::\n\n\n### 编写 proto 文件\n\n\n现在我们再来看之前的服务，是不是就很好理解了。\n```protobuf\nsyntax = \"proto3\";\n\npackage helloworld;\n\n// 服务的定义\nservice Greeter {\n  // Sends a greeting\n  rpc SayHello (HelloRequest) returns (HelloReply) {}\n}\n\n// 服务的请求参数\nmessage HelloRequest {\n  string name = 1;\n}\n\n// 服务的响应参数\nmessage HelloReply {\n  string message = 1;\n}\n\n```\n\n\n我们定义了一个名为 `Greeter` 的服务，包含一个 `HelloRequest` 结构的请求体，以及返回 `HelloReply` 结构的响应体。\n\n\n接下去，我们将对这个服务给大家做演示。\n\n\n### 生成代码定义\n\n\n传统的 gRPC 框架，需要用户手动编写 proto 文件，以及生成 js 服务，最后再根据 js 生成的服务再编写实现，在 Midway 体系下，我们提供了一个 grpc-helper 工具包来加速这个过程。\n\n\n如果没有安装，可以先安装。\n```bash\n$ npm i @midwayjs/grpc-helper --save-dev\n```\n\n\ngrpc-helper 工具的作用，是将用户提供的 proto 文件，生成对应可读的 ts interface 文件。\n\n\n我们可以添加一个脚本，方便这个过程。\n```json\n{\n\t\"scripts\": {\n     \"generate\": \"tsproto --path proto --output src/domain\"\n  }\n}\n```\n\n\n然后执行 `npm run generate` 。\n\n\n上述命令执行后，会在代码的 `src/domain` 目录中生成 proto 文件对应的服务接口定义。\n\n\n:::info\n不管是提供 gRPC 服务还是调用 gRPC 服务，都要先生成定义。\n:::\n\n\n生成的代码如下，包含有一个命名空间（namespace），以及命名空间下的两个 TypeScript Interface， `Greeter` 用于编写服务端实现， `GreeterClient` 用于编写客户端实现。\n```typescript\n/**\n* This file is auto-generated by grpc-helper\n*/\n\nimport * as grpc from '@midwayjs/grpc';\n\n// 生成的命名空间\nexport namespace helloworld {\n\n  // 服务端使用的定义\n  export interface Greeter {\n    // Sends a greeting\n    sayHello(data: HelloRequest): Promise<HelloReply>;\n  }\n\n  // 客户端使用的定义\n  export interface GreeterClient {\n    // Sends a greeting\n    sayHello(options?: grpc.IClientOptions): grpc.IClientUnaryService<HelloRequest, HelloReply>;\n  }\n\n  // 请求体结构\n  export interface HelloRequest {\n    name?: string;\n  }\n\n  // 响应体结构\n  export interface HelloReply {\n    message?: string;\n  }\n}\n\n```\n\n\n:::info\n每当 proto 文件被修改时，就需要重新生成对应的服务定义，然后将对应的方法实现。\n:::\n\n\n\n## 提供 gRPC 服务（Provider）\n\n\n### 编写服务提供方（Provider）\n\n\n在 `src/provider` 目录中，我们创建 `greeter.ts` ，内容如下\n```typescript\nimport {\n  MSProviderType,\n  Provider,\n  GrpcMethod,\n} from '@midwayjs/core';\nimport { helloworld } from '../domain/helloworld';\n\n/**\n * 实现 helloworld.Greeter 接口的服务\n */\n@Provider(MSProviderType.GRPC, { package: 'helloworld' })\nexport class Greeter implements helloworld.Greeter {\n\n  @GrpcMethod()\n  async sayHello(request: helloworld.HelloRequest) {\n    return { message: 'Hello ' + request.name };\n  }\n}\n\n```\n:::info\n注意，@Provider 装饰器和 @Provide 装饰器不同，前者用于提供服务，后者用于依赖注入容器扫描标识的类。\n:::\n\n\n我们使用 `@Provider` 暴露出一个 RPC 服务， `@Provider` 的第一个参数为 RPC 服务类型，这个参数是个枚举，这里选择 GRPC 类型。\n\n\n`@Provider` 的第二个参数为 RPC 服务的元数据，这里指代的是 gRPC 服务的元数据。这里需要写入 gRPC 的 package 字段，即 proto 文件中的 package 字段（这里的字段用于和 proto 文件加载后的字段做对应）。\n\n\n对于普通的 gRPC 服务接口（UnaryCall），我们只需要使用 `@GrpcMethod()` 装饰器修饰即可。修饰的方法即为服务定义本身，入参为 proto 中定义好的入参，return 值即为定义好的响应体。\n\n:::info\n注意，生成的 Interface 是为了更好的编写服务代码，规范结构，请务必按照定义编写。\n:::\n\n\n### 配置服务\n\n\n配置内容如下。\n```typescript\n// src/config/config.default\nimport { MidwayAppInfo, MidwayConfig } from '@midwayjs/core';\n\nexport default (appInfo: MidwayAppInfo): MidwayConfig => {\n  return {\n    // ...\n    grpcServer: {\n      services: [\n        {\n          protoPath: join(appInfo.appDir, 'proto/hero.proto'),\n          package: 'hero',\n        },\n        {\n          protoPath: join(appInfo.appDir, 'proto/helloworld.proto'),\n          package: 'helloworld',\n        }\n      ],\n    }\n  };\n}\n```\nservices 字段是数组，意味着 Midway 项目可以同时发布多个 gRPC 服务。每个 service 的结构为：\n\n| 属性      | 类型   | 描述                       |\n| --- | --- | --- |\n| protoPath | string | 必选，proto 文件的绝对路径 |\n| package | string | 必选，服务对应的 package |\n\n除了 Service 配置之外，还有一些其他的配置。\n\n| 属性          | 类型              | 描述                                                         |\n| ------------- | ----------------- | ------------------------------------------------------------ |\n| url           | string            | 可选，gRPC 服务地址，默认 6565 端口，比如 'localhost:6565'   |\n| loaderOptions | Object            | 可选，proto file loader 的 options                           |\n| credentials   | ServerCredentials | 可选，grpc Server binding 时的 credentials 参数选项          |\n| serverOptions | ChannelOptions    | 可选，grpc Server 的 [自定义 options](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js#supported-channel-options) |\n\n\n\n### 提供安全证书\n\n可以通过 `credentials` 参数传递安全证书。\n\n```typescript\n// src/config/config.default\nimport { MidwayAppInfo, MidwayConfig } from '@midwayjs/core';\nimport { ServerCredentials } from '@midwayjs/grpc';\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nconst cert = readFileSync(join(__dirname, './cert/server.crt'));\nconst pem = readFileSync(join(__dirname, './cert/server.pem'));\nconst key = readFileSync(join(__dirname, './cert/server.key'));\n\nexport default (appInfo: MidwayAppInfo): MidwayConfig => {\n  return {\n    // ...\n    grpcServer: {\n      // ...\n      credentials: ServerCredentials.createSsl(cert, [{ private_key: key, cert_chain: pem }]);\n    }\n  };\n}\n```\n\n\n\n### 编写单元测试\n\n`@midwayjs/grpc` 库提供了一个 `createGRPCConsumer` 方法，用于实时调用客户端，一般我们用这个方法做测试。\n\n:::caution\n这个方法每次调用会实时连接，不建议将该方法用在生产环境。\n:::\n\n\n在测试中写法如下。\n```typescript\nimport { createApp, close } from '@midwayjs/mock';\nimport { Framework, createGRPCConsumer } from '@midwayjs/grpc';\nimport { join } from 'path';\nimport { helloworld } from '../src/domain/helloworld';\n\ndescribe('test/index.test.ts', () => {\n\n  it('should create multiple grpc service in one server', async () => {\n    const baseDir = join(__dirname, '../');\n\n    // 创建服务\n    const app = await createApp<Framework>();\n\n    // 调用服务\n    const service = await createGRPCConsumer<helloworld.GreeterClient>({\n      package: 'helloworld',\n      protoPath: join(baseDir, 'proto', 'helloworld.proto'),\n      url: 'localhost:6565'\n    });\n\n    const result = await service.sayHello().sendMessage({\n      name: 'harry'\n    });\n\n    expect(result.message).toEqual('Hello harry');\n    await close(app);\n  });\n\n});\n\n```\n\n\n\n## 调用 gRPC 服务（Consumer）\n\n\n我们编写一个 gRPC 服务来调用上面的暴露的服务。\n\n:::info\n事实上，你可以在 Web 的 Controller，或者 Service 等其他地方来调用，这里只是做一个示例。\n:::\n\n\n### 调用配置\n\n\n你需要在 `src/config/config.default.ts` 中增加你需要调用的目标服务以及它的 proto 文件信息。\n\n\n比如，这里我们填写了上面暴露的服务本身，以及该服务的 proto，包名等信息（函数形式）。\n```typescript\n// src/config/config.default\nimport { MidwayAppInfo, MidwayConfig } from '@midwayjs/core';\n\nexport default (appInfo: MidwayAppInfo): MidwayConfig => {\n  return {\n    // ...\n    grpc: {\n      services: [\n        {\n          url: 'localhost:6565',\n          protoPath: join(appInfo.appDir, 'proto/helloworld.proto'),\n          package: 'helloworld',\n        },\n      ],\n    },\n  };\n}\n```\n\n\n### 代码调用\n\n\n配置完后，我们就可以在代码里调用了。\n\n\n`@midwayjs/grpc` 提供了 `clients` ，可以方便的获取到已配置的服务。我们只需要在需要注入的地方，注入这个对象即可。\n\n\n比如：\n```typescript\nimport {\n  Provide,\n  Inject,\n} from '@midwayjs/core';\nimport { helloworld, hero } from '../interface';\nimport { Clients } from '@midwayjs/grpc';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  grpcClients: Clients;\n\n}\n```\n\n\n我们通过 `clients` 获取到对方服务的客户端实例，然后调用即可。\n\n\n```typescript\nimport {\n  Provide,\n  Inject,\n} from '@midwayjs/core';\nimport { helloworld, hero } from '../interface';\nimport { Clients } from '@midwayjs/grpc';\n\n@Provide()\nexport class UserService {\n  @Inject()\n  grpcClients: Clients;\n\n  async invoke() {\n    // 获取服务\n  \tconst greeterService = this.grpcClients.getService<helloworld.GreeterClient>(\n      'helloworld.Greeter'\n    );\n\n    // 调用服务\n    const result = await greeterService.sayHello()\n    \t.sendMessage({\n        name: 'harry'\n      });\n\n    // 返回结果\n    return result;\n  }\n\n}\n```\n\n\n我们也可以利用 `@Init` 装饰器，将需要调用的服务缓存到属性上。这样可以在其他方法调用时复用。\n\n\n示例如下。\n```typescript\nimport {\n  GrpcMethod,\n  MSProviderType,\n  Provider,\n  Inject,\n  Init,\n} from '@midwayjs/core';\nimport { helloworld, hero } from '../interface';\nimport { Clients } from '@midwayjs/grpc';\n\n@Provider(MSProviderType.GRPC, { package: 'hero' })\nexport class HeroService implements hero.HeroService {\n  // 注入客户端\n  @Inject()\n  grpcClients: Clients;\n\n  greeterService: helloworld.GreeterClient;\n\n  @Init()\n  async init() {\n    // 赋值一个服务实例\n    this.greeterService = this.grpcClients.getService<helloworld.GreeterClient>(\n      'helloworld.Greeter'\n    );\n  }\n\n  @GrpcMethod()\n  async findOne(data) {\n    // 调用服务\n    const result = await greeterService.sayHello()\n    \t.sendMessage({\n        name: 'harry'\n      });\n\n    // 返回结果\n    return result;\n  }\n}\n\n```\n\n\n## 流式服务\n\n\ngRPC 的流式服务用于减少连接，让服务端或者客户端不需要等待即可执行任务，从而提高执行效率。\n\n\ngRPC 的流式服务分为三种，以服务端角度来说，为\n\n\n- 服务端接收流（客户端推）\n- 服务端响应流（服务端推）\n- 双向流\n\n\n\n下面我们将一一介绍。\n\n\n### 流式 proto 文件\n\n\n流式的 proto 文件写法不同，需要在希望使用流式的地方将参数标记为 `stream` 。\n```protobuf\n\nsyntax = \"proto3\";\n\npackage math;\n\nmessage AddArgs {\n  int32 id = 1;\n  int32 num = 2;\n}\n\nmessage Num {\n  int32 id = 1;\n  int32 num = 2;\n}\n\nservice Math {\n  rpc Add (AddArgs) returns (Num) {\n  }\n\n\t// 双向流\n  rpc AddMore (stream AddArgs) returns (stream Num) {\n  }\n\n  // 服务端往客户端推\n  rpc SumMany (AddArgs) returns (stream Num) {\n  }\n\n  // 客户端往服务端推\n  rpc AddMany (stream AddArgs) returns (Num) {\n  }\n}\n\n```\n该服务生成的接口定义为：\n\n\n```typescript\nimport {\n  IClientDuplexStreamService,\n  IClientReadableStreamService,\n  IClientUnaryService,\n  IClientWritableStreamService,\n  IClientOptions,\n} from '@midwayjs/grpc';\n\nexport namespace math {\n  export interface AddArgs {\n    id?: number;\n    num?: number;\n  }\n  export interface Num {\n    id?: number;\n    num?: number;\n  }\n\n  /**\n   * server interface\n   */\n  export interface Math {\n    add(data: AddArgs): Promise<Num>;\n    addMore(data: AddArgs): Promise<void>;\n    // 服务端推，客户端读\n    sumMany(data: AddArgs): Promise<void>\n    // 客户端端推，服务端读\n    addMany(num: AddArgs): Promise<void>;\n  }\n\n  /**\n   * client interface\n   */\n  export interface MathClient {\n    add(options?: IClientOptions): IClientUnaryService<AddArgs, Num>;\n    addMore(options?: IClientOptions): IClientDuplexStreamService<AddArgs, Num>;\n    // 服务端推，客户端读\n    sumMany(options?: IClientOptions): IClientReadableStreamService<AddArgs, Num>;\n    // 客户端端推，服务端读\n    addMany(options?: IClientOptions): IClientWritableStreamService<AddArgs, Num>;\n  }\n}\n\n```\n\n\n### 服务端推送\n\n\n客户端调用一次，服务端可以多次返回。通过 `@GrpcMethod()` 的参数来标识流式类型。\n\n\n可用的类型为:\n\n\n- `GrpcStreamTypeEnum.WRITEABLE`  服务端输出流（单工）\n- `GrpcStreamTypeEnum.READABLE`  客户端输出流（单工），服务端接受多次\n- `GrpcStreamTypeEnum.DUPLEX`  双工流\n\n\n\n服务端示例如下：\n```typescript\nimport { GrpcMethod, GrpcStreamTypeEnum, Inject, MSProviderType, Provider } from '@midwayjs/core';\nimport { Context, Metadata } from '@midwayjs/grpc';\nimport { math } from '../interface';\n\n/**\n */\n@Provider(MSProviderType.GRPC, { package: 'math' })\nexport class Math implements math.Math {\n\n  @Inject()\n  ctx: Context;\n\n  @GrpcMethod({type: GrpcStreamTypeEnum.WRITEABLE })\n  async sumMany(args: math.AddArgs) {\n    this.ctx.write({\n      num: 1 + args.num\n    });\n    this.ctx.write({\n      num: 2 + args.num\n    });\n    this.ctx.write({\n      num: 3 + args.num\n    });\n\n    this.ctx.end();\n  }\n\n  // ...\n}\n\n```\n服务端使用 `ctx.write` 方法来返回数据，由于是服务端流，可以返回多次。\n\n\n返回结束后，请使用 `ctx.end()` 方法关闭流。\n\n\n客户端，调用一次，接受多次数据。\n\n\n比如下面的累加逻辑。\n\n\nPromise 写法，会等待服务端数据都返回再做处理。\n```typescript\n// 服务端推送\nlet total = 0;\nlet result = await service.sumMany().sendMessage({\n  num: 1,\n});\n\nresult.forEach(data => {\n  total += data.num;\n});\n\n// total = 9;\n```\n\n\n事件写法，实时处理。\n```typescript\n// 服务端推送\nlet call = service.sumMany().getCall();\n\ncall.on('data', data => {\n\t// do something\n});\n\ncall.sendMessage({\n  num: 1,\n});\n\n```\n\n\n### 客户端推送\n\n\n客户端调用多次，服务端接收多次数据，返回一个结果。通过 `@GrpcMethod({type: GrpcStreamTypeEnum.READABLE})` 的参数来标识流式类型。\n\n\n服务端示例如下：\n```typescript\nimport { GrpcMethod, GrpcStreamTypeEnum, Inject, MSProviderType, Provider } from '@midwayjs/core';\nimport { Context, Metadata } from '@midwayjs/grpc';\nimport { math } from '../interface';\n\n/**\n */\n@Provider(MSProviderType.GRPC, { package: 'math' })\nexport class Math implements math.Math {\n\n  sumDataList: number[] = [];\n\n  @Inject()\n  ctx: Context;\n\n  @GrpcMethod({type: GrpcStreamTypeEnum.READABLE, onEnd: 'sumEnd' })\n  async addMany(data: math.Num) {\n    this.sumDataList.push(data);\n  }\n\n  async sumEnd(): Promise<math.Num> {\n    const total = this.sumDataList.reduce((pre, cur) => {\n      return {\n        num: pre.num + cur.num,\n      }\n    });\n    return total;\n  }\n\n  // ...\n}\n\n```\n\n\n客户端每次调用，都会触发一次 `addMany` 方法。\n\n\n在客户端发送 `end` 事件之后，会调用 `@GrpcMethod` 装饰器上的 `onEnd` 参数指定的方法，该方法的返回值即为最后客户端拿到的值。\n\n\n客户端示例如下：\n```typescript\n// 客户端推送\nconst data = await service.addMany()\n.sendMessage({num: 1})\n.sendMessage({num: 2})\n.sendMessage({num: 3})\n.end();\n\n// data.num = 6\n```\n\n\n### 双向流\n\n\n客户端可以调用多次，服务端也可以接收多次数据，返回多个结果，类似于传统的 TCP 通信。通过 `@GrpcMethod({type: GrpcStreamTypeEnum.DUPLEX})` 的参数来标识双工流式类型。\n\n\n服务端示例如下：\n```typescript\nimport { GrpcMethod, GrpcStreamTypeEnum, Inject, MSProviderType, Provider } from '@midwayjs/core';\nimport { Context, Metadata } from '@midwayjs/grpc';\nimport { math } from '../interface';\n\n/**\n */\n@Provider(MSProviderType.GRPC, { package: 'math' })\nexport class Math implements math.Math {\n\n  @Inject()\n  ctx: Context;\n\n  @GrpcMethod({type: GrpcStreamTypeEnum.DUPLEX, onEnd: 'duplexEnd' })\n  async addMore(message: math.AddArgs)  {\n    this.ctx.write({\n      id: message.id,\n      num: message.num + 10,\n    });\n  }\n\n  async duplexEnd() {\n    console.log('got client end message');\n  }\n  // ...\n}\n\n```\n服务端可以随时使用 `ctx.write` 返回数据，也可以使用 `ctx.end` 来关闭流。\n\n\n客户端示例：\n\n\n对于双工通信的客户端，由于无法保证调用、返回的顺序，我们需要使用监听的模式来消费结果。\n```typescript\nconst clientStream = service.addMore().getCall();\n\nlet total = 0;\nlet idx = 0;\n\nduplexCall.on('data', (data: math.Num) => {\n  total += data.num;\n  idx++;\n  if (idx === 2) {\n    duplexCall.end();\n    // total => 29\n  }\n});\n\nduplexCall.write({\n  num: 3,\n});\n\nduplexCall.write({\n  num: 6,\n});\n```\n\n\n如果希望保证调用顺序，我们也提供了保证顺序的双向流调用方法，但是需要在 proto 中定义一个固定的 id，来确保顺序。\n\n\n比如我们的 Math.proto，对每个入参和出参，都增加了一个固定的 id，所以可以固定顺序。\n```typescript\n\nsyntax = \"proto3\";\n\npackage math;\n\nmessage AddArgs {\n  int32 id = 1;\t\t\t\t\t\t\t//  这里的 id 名字是固定的\n  int32 num = 2;\n}\n\nmessage Num {\n  int32 id = 1;\t\t\t\t\t\t\t//  这里的 id 名字是固定的\n  int32 num = 2;\n}\n\nservice Math {\n  rpc Add (AddArgs) returns (Num) {\n  }\n\n  rpc AddMore (stream AddArgs) returns (stream Num) {\n  }\n\n  // 服务端往客户端推\n  rpc SumMany (AddArgs) returns (stream Num) {\n  }\n\n  // 客户端往服务端推\n  rpc AddMany (stream AddArgs) returns (Num) {\n  }\n}\n\n```\n固定顺序的客户端调用方式如下：\n```typescript\n// 保证顺序的双向流\nconst t = service.addMore();\n\nconst result4 = await new Promise<number>((resolve, reject) => {\n\n  let total = 0;\n\n  // 第一次调用和返回\n  t.sendMessage({\n    num: 2\n  })\n    .then(res => {\n      expect(res.num).toEqual(12);\n      total += res.num;\n  \t})\n    .catch(err => console.error(err));\n\n  // 第二次调用和返回\n  t.sendMessage({\n    num: 5\n  }).then(res => {\n      expect(res.num).toEqual(15);\n      total += res.num;\n      resolve(total);\n    })\n    .catch(err => console.error(err));\n\n  t.end();\n});\n\n// result4 => 27\n```\n默认的 id 为 `id` ，如果服务端定义不同，需要修改，可以在客户端调用时传递。\n```typescript\n// 保证顺序的双向流\nconst t = service.addMore({\n  messageKey: 'uid'\n});\n```\n\n\n## 元数据（Metadata）\n\n\ngRPC 的元数据等价于 HTTP 的上下文。\n\n\n服务端通过 `ctx.sendMetadata` 方法返回元数据，也可以通过 `ctx.metadata` 获取客户端传递的元数据。\n```typescript\nimport {\n  MSProviderType,\n  Provider,\n  GrpcMethod,\n} from '@midwayjs/core';\nimport { helloworld } from '../domain/helloworld';\nimport { Context, Metadata } from '@midwayjs/grpc';\n\n/**\n * 实现 helloworld.Greeter 接口的服务\n */\n@Provider(MSProviderType.GRPC, { package: 'helloworld' })\nexport class Greeter implements helloworld.Greeter {\n\n  @Inject()\n  ctx: Context;\n\n  @GrpcMethod()\n  async sayHello(request: helloworld.HelloRequest) {\n\n    // 客户端传递的元数据\n    console.log(this.ctx.metadata);\n\n    // 创建元数据\n    const meta = new Metadata();\n    this.ctx.metadata.add('xxx', 'bbb');\n    this.ctx.sendMetadata(meta);\n\n    return { message: 'Hello ' + request.name };\n  }\n}\n```\n\n\n客户端通过方法的 options 参数传递元数据。\n```typescript\nimport { Metadata } from '@midwayjs/grpc';\n\nconst meta = new Metadata();\nmeta.add('key', 'value');\n\nconst result = await service.sayHello({\n  metadata: meta,\n}).sendMessage({\n  name: 'harry'\n});\n```\n\n\n获取元数据相对麻烦一些。\n\n\n普通一元调用（UnaryCall）获取元数据需要使用 `sendMessageWithCallback` 方法。\n```typescript\nconst call = service.sayHello().sendMessageWithCallback({\n  name: 'zhangting'\n}, (err) => {\n  if (err) {\n    reject(err);\n  }\n});\ncall.on('metadata', (meta) => {\n  // output meta\n});\n```\n其他流式服务，可以通过 `getCall()` 方法获取原始客户端流对象，从而直接订阅。\n```typescript\n// 获取服务，注意，这里没有 await\nconst call = service.addMany().getCall();\ncall.on('metadata', (meta) => {\n  // output meta\n});\n```\n\n\n## 超时处理\n\n\n我们可以在调用服务时传递参数，单位毫秒。\n```typescript\nconst result = await service.sayHello({\n  timeout: 5000\n}).sendMessage({\n  name: 'harry'\n});\n```\n\n<!-- File: site/docs/extensions/http-proxy.md (Tokens: ~1176 | Category: anthropic-skill) -->\n# HTTP 代理\n\n适用于 `@midwayjs/faas` 、`@midwayjs/web` 、`@midwayjs/koa` 和 `@midwayjs/express` 多种框架的 HTTP 请求代理组件，支持 GET、POST 等多种请求方法。\n\n相关信息：\n\n| web 支持情况      |     |\n| ----------------- | --- |\n| @midwayjs/koa     | ✅  |\n| @midwayjs/faas    | 💬 |\n| @midwayjs/web     | ✅  |\n| @midwayjs/express | ✅  |\n\n:::caution\n\n💬 部分函数计算平台不支持流式请求响应，请参考对应平台能力。\n\n:::\n\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/http-proxy@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/http-proxy\": \"^4.0.0\"\n    // ...\n  },\n  \"devDependencies\": {\n    // ...\n  }\n}\n```\n\n## 启用组件\n\n在 `src/configuration.ts` 中引入组件\n\n```typescript\n// ...\nimport * as proxy from '@midwayjs/http-proxy';\n\n@Configuration({\n  imports: [\n    // ...other components\n    proxy,\n  ],\n})\nexport class MainConfiguration {}\n```\n\n## 配置\n\n代理配置定义如下：\n\n```typescript\n// 代理配置类型\nexport interface HttpProxyConfig {\n  // 匹配要代理的 URL 正则表达式\n  match: RegExp;\n  // 替换匹配到的链接的 host，将请求代理到此地址\n  host?: string;\n  // 通过正则的表达式捕获组处理代理地址\n  target?: string;\n  // 转发请求超时时间，默认为0不设置超时时间\n  proxyTimeout?: number;\n  // 忽略代理请求转发的 header 中的字段\n  ignoreHeaders?: {\n    [key: string]: boolean;\n  };\n}\n```\n\n代理支持单个代理和多个代理。\n\n单个代理配置\n\n```typescript\n// src/config/config.default.ts\n\nexport default {\n  httpProxy: {\n    match: /\\/tfs\\//,\n    host: 'https://gw.alicdn.com',\n  },\n};\n```\n\n多个代理配置\n\n```typescript\n// src/config/config.default.ts\n\n// 代理配置类型\nexport default {\n  httpProxy: {\n    default: {\n      // 一些每个策略复用的值，会和底下的策略进行合并\n    },\n    strategy: {\n      gw: {\n        // https://gw.alicdn.com/tfs/TB1.1EzoBBh1e4jSZFhXXcC9VXa-48-48.png\n        match: /\\/tfs\\//,\n        host: 'https://gw.alicdn.com',\n      },\n      g: {\n        // https://g.alicdn.com/mtb/lib-mtop/2.6.1/mtop.js\n        match: /\\/bdimg\\/(.*)$/,\n        target: 'https://sm.bdimg.com/$1',\n      },\n      httpBin: {\n        // https://httpbin.org/\n        match: /\\/httpbin\\/(.*)$/,\n        target: 'https://httpbin.org/$1',\n      },\n    },\n  },\n};\n```\n\n## 示例：使用 host 配置代理\n\n```typescript\nexport default {\n  httpProxy: {\n    match: /\\/tfs\\//,\n    host: 'https://gw.alicdn.com',\n  },\n};\n```\n\n当请求您的站点路径为： `https://yourdomain.com/tfs/test.png` 时，`match` 字段配置的正则表达式成功匹配，那么就将原始请求路径中的 `host` 部分 `https://yourdomain.com` 替换为配置的 `https://gw.alicdn.com`，从而发起代理请求到 `https://gw.alicdn.com/tfs/test.png`，并把响应结果返回给请求您站点的用户。\n\n## 示例：使用 target 配置代理\n\n```typescript\nexport default {\n  httpProxy: {\n    match: /\\/httpbin\\/(.*)$/,\n    target: 'https://httpbin.org/$1',\n  },\n};\n```\n\n当请求您的站点路径为： `https://yourdomain.com/httpbin/get?name=midway` 时，`match` 字段配置的正则表达式成功匹配，同时正则的捕获组中有结果 `['get?name=midway']` ，那么就将原始请求路径中的 `$1` 部分替换为捕获组中的第 1 个数据（index: 0）的 `get?name=midway`，从而发起代理请求到 `https://httpbin.org/get?name=midway`，并把响应结果返回给请求您站点的用户。\n\n<!-- File: site/docs/extensions/i18n.md (Tokens: ~4632 | Category: anthropic-skill) -->\n# 多语言\n\nMidway 提供了多语言组件，让业务可以快速指定不同的语言，展示不同的文案，也可以在 HTTP 场景配合请求参数，请求头等方式来使用。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n## 安装组件\n\n```bash\n$ npm i @midwayjs/i18n@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/i18n\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 使用组件\n\n将 i18n 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as i18n from '@midwayjs/i18n';\n\n@Configuration({\n  imports: [\n    // ...\n    i18n\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n\n\n## 使用\n\n组件提供了 `MidwayI18nService`  服务，用于翻译多语言文本。\n\n使用 `translate` 方法，传入不同的文本关键字和参数，返回不同语言的文本内容。\n\n```typescript\n@Controller('/')\nexport class UserController {\n\n  @Inject()\n  i18nService: MidwayI18nService;\n\n  @Get('/')\n  async index(@Query('username') username: string) {\n    return this.i18nService.translate('HELLO_MESSAGE', {\n      args: {\n        username\n      },\n    });\n  }\n}\n```\n\n\n\n## 配置多语言文案\n\n你可以在配置文件中直接配置，但是大多数情况下，文案会很多，有时候甚至可能文案在远端服务上，这个时候直接配置就不太现实。\n\n一般来说，我们会将文案单独放到某个文案配置目录中，比如 `src/locales` 。\n\n以 `src/locale` 这个目录为例，我们举个例子，结构如下：\n\n```text\n.\n├── src\n│   ├── locales\n|   │   ├── en_US.json\n|   │   └── zh_CN.json\n│   └── controller\n│       └── home.controller.ts\n├── package.json\n└── tsconfig.json\n```\n\n这里我们建了两个多语言的文件，`en_US.json` 和 `zh_CN.json`，分别代表英文和中文。\n\n文件内容分别如下：\n\n```json\n// src/locales/en_US.json\n{\n  \"hello\": \"Hello {username}\",\n  \"email\": \"email id\",\n  \"login\": \"login account\",\n  \"createdAt\": \"register date\"\n}\n```\n\n```json\n// src/locales/zh_CN.json\n{\n  \"hello\": \"你好 {username}\",\n  \"email\": \"邮箱\",\n  \"login\": \"帐号\",\n  \"createdAt\": \"注册时间\"\n}\n```\n\n每行一个字符串对，是一个标准的 JSON 格式内容，也可以使用 js/ts 文件，花括号中是可替换的参数占位。\n\n同时，需要在配置中加入这两个 JSON，其中 `default` 是语言的默认分组。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    // 把你的翻译文本放到这里\n    localeTable: {\n      en_US: {\n        default: require('../locale/en_US'),\n      },\n      zh_CN: {\n        default: require('../locale/zh_CN'),\n      }\n    },\n  }\n}\n```\n\n这样就可以使用了，使用输出如下。\n\n```typescript\nthis.i18nService.translate('hello', {\n  args: {\n    username: 'harry',\n  },\n  locale: 'en_US',\n});\n\n// output: Hello harry.\n\nthis.i18nService.translate('hello', {\n  args: {\n    username: 'harry',\n  },\n  locale: 'zh_CN',\n});\n\n// output: 你好 harry.\n\n```\n\n\n\n## 多语言文案分组\n\n在如下配置中，用户配置的多语言文案在 `default` 分组中。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    // 把你的翻译文本放到这里\n    localeTable: {\n      en_US: {\n        default: require('../locale/en_US'),\n      },\n      zh_CN: {\n        default: require('../locale/zh_CN'),\n      }\n    },\n  }\n}\n```\n\n这样做的好处是，在其他组件或者业务代码中，我们也可以使用不同的分组名，来添加其他的多语言文案。\n\n比如：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    // 把你的翻译文本放到这里\n    localeTable: {\n      en_US: {\n        default: require('../locale/en_US'),\n        user: require('../locale/user_en_US'),\n      },\n      zh_CN: {\n        default: require('../locale/zh_CN'),\n        user: require('../locale/user_zh_CN'),\n      }\n    },\n  }\n}\n```\n\n在代码中，如果调用非默认分组，需要指定分组参数。\n\n```typescript\nthis.i18nService.translate('user.hello', {\n  args: {\n    username: 'harry',\n  },\n  group: 'user',\t\t// 指定其他分组\n  locale: 'en_US',\n});\n\n```\n\n\n\n## 多语言文案格式\n\n多语言文本中可以添加参数，参数可以有 `对象` 和 `数组` 两种形式。\n\n对象形式如下，使用花括号作为占位符。\n\n```text\nHello {username}\n```\n\n使用时，通过配置传递，按对象 key 覆盖变量。\n\n```typescript\nasync index(@Query('username') username: string) {\n  return this.i18nService.translate('hello', {\n    args: {\n      username\n    },\n  });\n}\n```\n\n数组形式如下，使用数字作为占位符。\n\n```text\nHello {0}\n```\n\n使用时，通过配置传递，格式是数组形式，按数组顺序覆盖数字变量。\n\n```typescript\nasync index(@Query('username') username: string) {\n  return this.i18nService.translate('hello', {\n    args: [username]\n  });\n}\n```\n\n\n\n## 动态添加多语言文案\n\n有时候，多语言文案可能放在远端，比如数据库等，我们可以通过 `addLocale` 方法进行动态添加。\n\n比如，在配置加载后，代码使用前。\n\n```typescript\n// configuration.ts\n\n// ...\n@Configuration({\n  imports: [\n    koa,\n    i18n\n  ]\n})\nexport class MainConfiguration {\n\n  @Inject()\n  i18nService: MidwayI18nService;\n\n  async onReady() {\n    this.i18nService.addLocale('zh_TW', {\n      hello: '你好，{username} 美麗的世界'\n    });\n  }\n\n\n  // ...\n}\n```\n\n在代码中就可以使用。\n\n```typescript\nasync index(@Query('username') username: string) {\n  return this.i18nService.translate('hello', {\n    args: [username],\n    locale: 'zh_TW'\n  });\n}\n```\n\n\n\n## 通过参数指定当前语言\n\n一般情况下，默认语言为 `en_US`，用户的浏览器访问一般会自带 `Accept-Language` 头，所以会正确识别语言。比如用中文浏览器访问，就能正常显示中文。\n\n除此之外，在 HTTP 场景下可以通过 URL Query，Cookie，Header 来指定语言。\n\n优先级从上到下：\n\n- query: /?locale=en-US\n- cookie: locale=zh-TW\n- header: Accept-Language: zh-CN,zh;q=0.5\n\n当传递了这些参数之后，多语言数据会自动保存到当前用户的 Cookie 中，下次请求会直接用该设定好的语言。\n\n\n\n## 手动设置语言\n\n可以通过调用 `saveRequestLocale` 设置当前语言。\n\n```typescript\nasync index() {\n  // ...\n  this.i18nService.saveRequestLocale('zh_CN');\n}\n```\n\n如果开启了 `writeCookie` 配置，设置后会保存到当前用户的 Cookie 中，下次请求会使用该设置。\n\n\n\n## 语言选择优先级\n\n这些多种设置语言的方式，有着不同的优先级，如下优先级从高到低：\n\n- 1、`i18nService.translate` 方法显式指定的语言\n- 2、通过其他装饰器设置的语言，比如 `@Validate` 装饰器的参数（本质是调用了`i18nService.translate` 方法）\n- 3、通过 `saveRequestLocale` API 直接设置的当前语言\n- 4、通过浏览器 Query，Cookie，Header 设置的语言（本质是调用了 `saveRequestLocale`）\n- 5、i18n 组件配置中的默认语言\n\n\n\n## 关于语言大小写\n\n在代码内部，我们会将所有的多语言，fallback 规则，写入的文本串，返回的 locale 结果，使用下面的规则替代\n\n- 1、使用中划线代替下划线\n- 2、使用小写代替大写\n\n即所有的 `en_US` 都会变成 `en-us`，`zh_CN` 会变成 `zh-cn`。\n\n这样做会安全的适配 URL 和 Cookie。\n\n\n\n## View 中使用\n\n在 Web 类型的框架中，我们默认添加了 locals 变量支持，可以在模板引擎中使用。\n\n假设我们使用的模板引擎是 [Nunjucks](./render)，可以直接引用到 `i18n` 方法。\n\n多语言文案如下：\n\n```json\n{\n  \"hello\": \"Hello {username}\",\n}\n```\n\n模板如下：\n\n```html\n<span>{{ i18n('hello', user) }}</span>\n```\n\n示例如下：\n\n```typescript\n// ...\n\n@Controller('/')\nexport class UserController {\n\n  @Inject()\n  ctx: Context;\n\n  @Get('/')\n  async index() {\n    await this.ctx.render('index', {\n      // 注意这里是整个对象传递给模板\n      user: {\n        username: 'harry',\n      }\n    });\n  }\n}\n```\n\ni18n 方法定义如下：\n\n```typescript\nfunction i18n(templateName: string, args: Record<string, any>) {\n  // ...\n}\n```\n\n方法名可以通过配置修改。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    localsField: 'i18n',\n  }\n}\n```\n\n\n\n\n\n## 配置\n\n### 默认配置\n\n大部分情况下，你只需要在配置 `localeTable` 添加你自己的多语言翻译即可。\n\n下面是完整的配置，你可以在配置定义中找到。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    // 默认语言  \"en_US\"\n    defaultLocale: 'en_US',\n\n    // 把你的翻译文本放到这里\n    localeTable: {\n      en_US: {\n        // group name\n        default: {\n          // hello: 'hello'\n        }\n      },\n      zh_CN: {\n        // group name\n        default: {\n          // hello: '你好'\n        }\n      },\n    },\n\n    // 语言映射，可以用 * 号通配\n    fallbacks: {\n      //   'en_*': 'en_US',\n      //   pt: 'pt-BR',\n    },\n    // 是否将请求参数写入 cookie\n    writeCookie: true,\n    resolver: {\n      // url query 参数，默认是 \"locale\"\n      queryField: 'locale',\n      cookieField: {\n        // Cookie 里的 key，默认是 \"locale\"\n        fieldName: 'locale',\n        // Cookie 域名，默认为空，代表当前域名有效\n        cookieDomain: '',\n        // Cookie 默认的过期时间，默认一年\n        cookieMaxAge: FORMAT.MS.ONE_YEAR,\n      },\n    },\n    localsField: 'i18n',\n    // 缺失翻译键的处理函数，默认返回原始键名\n    missingKeyHandler: (message, options) => message,\n  }\n}\n```\n\n\n\n### 回写 Cookie\n\n默认情况下，多语言组件会将当前用户的语言回写到 Cookie 中，避免下次请求再进行查找以提高性能，我们可以通过配置关闭这个行为。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    writeCookie: false,\n  }\n}\n```\n\n\n\n### 请求解析配置\n\nHTTP 场景下，我们提供了通过参数指定当前语言的能力。\n\n默认情况下，组件通过下面的字段来查找。\n\n- query 的 `locale` 字段\n- cookie 的 `locale` 字段\n- header 的 `Accept-Language` 部分\n\n我们可以通过配置修改查询的字段。\n\n比如，修改 Query 的字段。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    resolver: {\n      queryField: 'abc'\n    },\n  }\n}\n```\n\n我们就可以通过 `/?abc=en-US` 来请求修改语言。\n\n如果不希望通过请求来设置语言，可以将整个 `resolver` 解析关闭，对 Cookie 的回写也将同时停止。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  i18n: {\n    resolver: false,\n  }\n}\n```\n\n\n### 缺失翻译键处理\n\n当翻译键在所有语言文件中都找不到时，i18n 组件会调用 `missingKeyHandler` 函数来处理这种情况。\n\n默认情况下，当找不到翻译时，会返回原始的键名：\n\n```typescript\n// 默认的 missingKeyHandler\nmissingKeyHandler: (message, options) => message\n```\n\n例如：\n```typescript\n// 假设 \"unknown_key\" 在任何语言文件中都不存在\nthis.i18nService.translate('unknown_key');\n// 返回: \"unknown_key\"\n```\n\n你可以自定义缺失键的处理方式，比如添加特殊标记、记录日志等：\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  i18n: {\n    missingKeyHandler: (message, options) => {\n      // 记录缺失的翻译键\n      console.warn(`Missing translation: ${message}`, options);\n\n      // 根据不同的组返回不同的格式\n      const group = options?.group || 'default';\n      const locale = options?.locale || 'default';\n\n      return `[${group}:${locale}:${message}]`;\n    }\n  }\n}\n```\n\n\n## 常用语言\n\n| 语言             | 语言包名 |\n| :--------------- | :------- |\n| 阿拉伯           | ar_EG    |\n| 亞美尼亞         | hy_AM    |\n| 保加利亚语       | bg_BG    |\n| 加泰罗尼亚语     | ca_ES    |\n| 捷克语           | cs_CZ    |\n| 丹麦语           | da_DK    |\n| 德语             | de_DE    |\n| 希腊语           | el_GR    |\n| 英语             | en_GB    |\n| 英语（美式）     | en_US    |\n| 西班牙语         | es_ES    |\n| 爱沙尼亚语       | et_EE    |\n| 波斯语           | fa_IR    |\n| 芬兰语           | fi_FI    |\n| 法语（比利时）   | fr_BE    |\n| 法语             | fr_FR    |\n| 希伯来语         | he_IL    |\n| 印地语           | hi_IN    |\n| 克罗地亚语       | hr_HR    |\n| 匈牙利           | hu_HU    |\n| 冰岛语           | is_IS    |\n| 印度尼西亚语     | id_ID    |\n| 意大利语         | it_IT    |\n| 日语             | ja_JP    |\n| 格鲁吉亚语       | ka_GE    |\n| 卡纳达语         | kn_IN    |\n| 韩语/朝鲜语      | ko_KR    |\n| 库尔德语         | ku_IQ    |\n| 拉脱维亚语       | lv_LV    |\n| 马来语           | ms_MY    |\n| 蒙古语           | mn_MN    |\n| 挪威             | nb_NO    |\n| 尼泊尔语         | ne_NP    |\n| 荷兰语（比利时） | nl_BE    |\n| 荷兰语           | nl_NL    |\n| 波兰语           | pl_PL    |\n| 葡萄牙语(巴西)   | pt_BR    |\n| 葡萄牙语         | pt_PT    |\n| 斯洛伐克语       | sk_SK    |\n| 塞尔维亚         | sr_RS    |\n| 斯洛文尼亚       | sl_SI    |\n| 瑞典语           | sv_SE    |\n| 泰米尔语         | ta_IN    |\n| 泰语             | th_TH    |\n| 土耳其语         | tr_TR    |\n| 罗马尼亚语       | ro_RO    |\n| 俄罗斯语         | ru_RU    |\n| 乌克兰语         | uk_UA    |\n| 越南语           | vi_VN    |\n| 简体中文         | zh_CN    |\n| 繁体中文         | zh_TW    |\n\n\n\n## 常见问题\n\n### 1、测试配置全局语言不生效\n\n一般场景下，你 **无需** 配置全局语言，因为浏览器访问会自动带上语言信息，比如中文浏览器自动返回中文，英文浏览器自动返回英文。\n\n假如你明确希望测试全局设置的效果，请务必按下面操作：\n\n* 1、如果使用的是浏览器，请清空页面 cookie 再访问，因为 cookie 中会记录上一次用户的语言信息\n* 2、如果使用的是 Postman 等工具，请不要带上 cookie 以及语言相关的 Header，Query 等字段\n\n### 2、测试返回的语言非预期\n\n请使用浏览器进行测试，不要使用 Postman。\n\n由于 Postman 请求不会带有浏览器语言相关的 Header，所以服务端无法自动判断语言。\n\n如果你一定要使用 Postman，请参考浏览器请求，加上 `Accept-Language` Header。\n\n<!-- File: site/docs/extensions/info.md (Tokens: ~995 | Category: anthropic-skill) -->\n# 信息查看\n\nMidway 提供了 info 组件，用于展示应用的基本信息，方便排查问题。\n\n相关信息：\n\n| 描述              |      |\n| ----------------- | ---- |\n| 可用于标准项目    | ✅    |\n| 可用于 Serverless | ✅    |\n| 可用于一体化      | ✅    |\n| 包含独立主框架    | ❌    |\n| 包含独立日志      | ❌    |\n\n\n## 安装依赖\n\n```bash\n$ npm i @midwayjs/info@4 --save\n```\n\n或者在 `package.json` 中增加如下依赖后，重新安装。\n\n```json\n{\n  \"dependencies\": {\n    \"@midwayjs/info\": \"^4.0.0\",\n    // ...\n  },\n}\n```\n\n\n\n## 使用组件\n\n将 info 组件配置到代码中。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as info from '@midwayjs/info';\n\n@Configuration({\n  imports: [\n    // ...\n    info\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n在有些情况下，为了不想让应用信息透出，我们指定在特殊环境下生效。\n\n```typescript\nimport { Configuration } from '@midwayjs/core';\nimport * as koa from '@midwayjs/koa';\nimport * as info from '@midwayjs/info';\n\n@Configuration({\n  imports: [\n    koa,\n    {\n      component: info,\n      enabledEnvironment: ['local'],\t// 只在本地启用\n    }\n  ]\n})\nexport class MainConfiguration {\n  //...\n}\n```\n\n\n\n## 查看信息\n\n在默认情况下，info 组件会为 Http 场景自动添加一个中间件，我们可以通过 `/_info` 来访问。\n\n默认情况下，会展示系统，进程，以及配置等关键信息。\n\n效果如下：\n\n![info](https://img.alicdn.com/imgextra/i3/O1CN01TCkSvr28x8T7gtnCl_!!6000000007998-2-tps-797-1106.png)\n\n\n\n## 修改访问路由\n\n为了安全，我们可以调整访问的路由。\n\n```typescript\n// src/config/config.default.ts\nexport default {\n  // ...\n  info: {\n    infoPath: '/_my_info',\n  }\n}\n```\n\n\n\n## 隐藏信息\n\n默认情况下，info 组件会隐藏秘钥等信息。我们可以配置增减隐藏的关键字，这个配置会对 **环境变量** 以及 **多环境配置** 生效。\n\n关键字可以使用通配符，比如增加一些关键字。\n\n```typescript\n// src/config/config.default.ts\nimport { DefaultHiddenKey } from '@midwayjs/info';\n\nexport default {\n  // ...\n  info: {\n    hiddenKey: DefaultHiddenKey.concat(['*abc', '*def', '*bbb*']),\n  }\n}\n```\n\n\n\n## 调用 API\n\ninfo 组件默认提供了 `InfoService` 用于在非 Http 或是自定义的场景来使用。\n\n比如：\n\n```typescript\nimport { Provide } from '@midwayjs/core';\nimport { InfoService } from '@midwayjs/info';\n\n@Provide()\nexport class userService {\n\n  @Inject()\n  inforService: InfoService\n\n  async getInfo() {\n    // 应用信息，应用名等\n    this.inforService.projectInfo();\n    // 系统信息\n    this.inforService.systemInfo();\n    // 堆内存，cpu 等\n    this.inforService.resourceOccupationInfo();\n    // midway 框架的信息\n    this.inforService.softwareInfo();\n    // 当前使用的环境配置\n    this.inforService.midwayConfig();\n    // 依赖注入容器中的服务\n    this.inforService.midwayService();\n    // 系统时间，时区，启动时常\n    this.inforService.timeInfo();\n    // 环境变量\n    this.inforService.envInfo();\n    // 依赖信息\n    this.inforService.dependenciesInfo();\n    // 网络信息\n    this.inforService.networkInfo();\n  }\n}\n```\n\n<!-- ... +102 more modular skills available via /api/skills/midwayjs/midway?path={path} -->\n</yakaai_skills>"}