{"owner":"eggjs","repo":"egg","hasSkills":true,"totalSkillsCount":93,"totalTokensCount":147550,"categories":["copilot-instructions","root-instruction","plugin-manifest","anthropic-skill"],"hasMcp":false,"mcpConfig":null,"found":[".github/copilot-instructions.md","AGENTS.md","packages/cluster/README.md","packages/cookies/README.md","packages/core/README.md","packages/core/benchmark/middleware/README.md","packages/egg/README.md","packages/errors/README.md","packages/extend2/README.md","packages/koa-static-cache/README.md","packages/koa/Readme.md","packages/koa/docs/api/context.md","packages/koa/docs/api/index.md","packages/koa/docs/api/request.md","packages/koa/docs/api/response.md","packages/koa/docs/error-handling.md","packages/koa/docs/faq.md","packages/koa/docs/guide.md","packages/koa/docs/koa-vs-express.md","packages/koa/docs/migration.md","packages/koa/docs/troubleshooting.md","packages/loader-fs/README.md","packages/path-matching/README.md","packages/router/README.md","packages/skills/AGENTS.md","packages/skills/PLAN.md","packages/skills/README.md","packages/skills/egg-controller/SKILL.md","packages/skills/egg-controller/references/ajv-validate.md","packages/skills/egg-controller/references/http-controller.md","packages/skills/egg-controller/references/mcp-controller.md","packages/skills/egg-controller/references/middleware.md","packages/skills/egg-controller/references/schedule.md","packages/skills/egg-core/SKILL.md","packages/skills/egg-core/references/aop.md","packages/skills/egg-core/references/background-task.md","packages/skills/egg-core/references/dynamic-inject.md","packages/skills/egg-core/references/eventbus.md","packages/skills/egg-core/references/inject.md","packages/skills/egg-core/references/module.md","packages/skills/egg-core/references/proto.md","packages/skills/egg-unittest/SKILL.md","packages/skills/egg-unittest/references/background-task-test.md","packages/skills/egg-unittest/references/eventbus-test.md","packages/skills/egg-unittest/references/http-test.md","packages/skills/egg-unittest/references/mock.md","packages/skills/egg-unittest/references/service-test.md","packages/skills/egg/SKILL.md","packages/skills/eval/evals-egg-controller.json","packages/skills/eval/evals-egg-core.json","packages/skills/eval/evals-egg-unittest.json","packages/skills/eval/evals-routing.json","packages/skills/package.json","packages/supertest/README.md","packages/tsconfig/README.md","packages/utils/README.md","plugins/cors/README.md","plugins/development/README.md","plugins/jsonp/README.md","plugins/logrotator/README.md","plugins/mock/README.md","plugins/multipart/README.md","plugins/onerror/README.md","plugins/redis/README.md","plugins/schedule/README.md","plugins/security/README.md","plugins/session/README.md","plugins/static/README.md","plugins/status/README.md","plugins/tracer/README.md","plugins/typebox-validate/README.md","plugins/view-nunjucks/README.md","plugins/view/README.md","plugins/watcher/README.md","tegg/AGENTS.md","tools/create-egg/README.md","tools/create-egg/src/templates/egg3-simple-js/README.md","tools/create-egg/src/templates/egg3-simple-ts/README.md","tools/create-egg/src/templates/egg3-tegg/README.md","tools/create-egg/src/templates/simple-ts/README.md","tools/create-egg/src/templates/tegg/README.md","tools/egg-bin/README.md","tools/egg-bundler/README.md","tools/egg-bundler/docs/output-structure.md","tools/scripts/README.md","wiki/packages/core.md","wiki/packages/egg-bundler.md","wiki/packages/loader-fs.md","wiki/packages/onerror.md","wiki/packages/service-worker.md","wiki/packages/tegg-vitest.md","wiki/packages/typings.md","wiki/packages/utils.md"],"skills":{".github/copilot-instructions.md":"# Eggjs Framework - GitHub Copilot Development Instructions\n\n**Always reference these instructions first and fallback to search or additional context gathering only when you encounter unexpected information that does not match the information provided here.**\n\n## Overview\n\nEggjs is a progressive Node.js framework for building enterprise-class server-side applications. Built on top of Koa.js, it provides a plugin system, conventions over configuration, and enterprise-grade features like clustering, logging, and security.\n\nThis is a **utoo monorepo** with multiple packages using utoo workspaces and catalog mode for centralized dependency management.\n\n## Prerequisites and Environment Setup\n\n- **Node.js >= 22.18.0 required** - This is a hard requirement\n- Enable utoo first: `npm install --global utoo@latest`\n- **NEVER CANCEL** any build or test commands - they can take several minutes to complete\n\n## Bootstrap and Build Process\n\n**Run these commands after a fresh clone:**\n\n```bash\n# 1. Enable utoo (required first)\nnpm install --global utoo@latest\n\n# 2. Install all dependencies - takes ~63 seconds. NEVER CANCEL. Set timeout to 120+ seconds.\nut install\n\n# 3. Run lint to check code quality across all packages - takes ~2 seconds\nut run lint\n\n# 4. Build all packages when validating build output - takes ~14 seconds. NEVER CANCEL. Set timeout to 60+ seconds.\nut run build\n```\n\nRun unit tests from a clean source tree, not immediately after `ut run build`.\nThe main CI test job installs dependencies with `ut install` and\nruns tests with `ut run ci`; it does not run `build` before tests.\n\n## Monorepo Structure\n\n### Key Packages (all in `packages/` directory)\n\n- **`packages/egg/`** - Main Eggjs framework (TypeScript, uses tsdown for builds)\n- **`packages/core/`** - Core plugin framework\n- **`packages/utils/`** - Utility functions\n- **`packages/mock/`** - Testing utilities\n- **`packages/cluster/`** - Cluster management\n- **`packages/koa/`** - Koa web framework\n- **`packages/supertest/`** - HTTP testing utilities\n- **`packages/extend2/`** - Object extension utility\n\n### Supporting Directories\n\n- **`examples/`** - Two example apps: `helloworld-commonjs` and `helloworld-typescript` (currently have runtime issues)\n- **`site/`** - Documentation website built with VitePress\n\n## Essential Commands and Timing\n\n### Build Commands\n\n- `ut run build` - **Build all packages (~14 seconds). NEVER CANCEL. Set timeout to 60+ seconds.**\n- `ut run clean-dist` - Clean all dist directories\n\n### Testing Commands\n\n- `ut run test` - **Run all tests (~2 minutes). NEVER CANCEL. Set timeout to 180+ seconds.**\n- `ut run test:cov` - **Run tests with coverage (~2 minutes). NEVER CANCEL. Set timeout to 180+ seconds.**\n- `ut run ci` - **Run tests with coverage (~2.1 minutes). NEVER CANCEL. Set timeout to 180+ seconds.**\n\n### Linting Commands\n\n- `ut run lint` - Run oxlint across all packages (~2 seconds)\n\n### Documentation Commands\n\n- `ut run site:dev` - Start documentation dev server (defaults to VitePress port 5173)\n- `ut run site:build` - **Build documentation site (~24 seconds). NEVER CANCEL. Set timeout to 60+ seconds.**\n\n### Example Applications (Currently Not Working)\n\n- `ut run example:dev:commonjs` - Start CommonJS example (has runtime issues)\n- `ut run example:dev:typescript` - Start TypeScript example (has runtime issues)\n\n## Package-Specific Commands\n\nRun commands for specific packages using `ut --filter=<package>`:\n\n```bash\n# Examples\nut --filter=egg run test\nut --filter=@eggjs/core run build\nut --filter=site run dev\n```\n\n## Development Workflow\n\n### 1. Making Changes\n\n- Work from the source tree first. Build when you need to validate package output, but do not run build immediately before unit tests.\n- Work primarily in `packages/egg/src/` for core framework features\n- Use TypeScript throughout - all packages are TypeScript-based\n- Follow the existing directory conventions in `packages/egg/src/`:\n  - `lib/` - Core classes (Application, Agent, EggApplicationCore)\n  - `app/extend/` - Framework extensions (context, helper, request, response)\n  - `config/` - Default configurations and plugins\n  - `lib/core/` - Core components (httpclient, logger, messenger)\n  - `lib/loader/` - Application loaders\n\n### 2. Validation Steps\n\n**Always perform these validation steps after making changes:**\n\n```bash\n# 1. Run lint to check code quality across all packages\nut run lint\n\n# 2. Run tests from a clean tree (some failures are expected in fresh environment)\nut run test\n\n# 3. Build all packages when build output or packaging behavior is relevant\nut run build\n\n# 4. Test documentation site when docs changed\nut run site:dev\n```\n\n### 3. Testing Strategy\n\n- **All packages use Vitest for testing** - this is the standard test runner\n- Test files follow pattern: `test/**/*.test.ts`\n- Use `import { describe, it } from 'vitest'` for test functions\n- Use Node.js built-in `assert` module for assertions\n- Create test fixtures in `packages/egg/test/fixtures/apps/` for scenario testing\n\n## Key Framework Concepts\n\n### Architecture\n\n- **EggApplicationCore** - Base application class with core functionality\n- **Application** - Main app class for worker processes\n- **Agent** - Agent process class for background tasks\n- **Context** - Extended Koa context with Egg-specific features\n- **BaseContextClass** - Base for controllers, services, subscriptions\n\n### Loading Convention (Automatic Discovery Order)\n\n1. Plugin system\n2. Configurations\n3. Application/Request/Response/Context extensions\n4. Custom loaders\n5. Services\n6. Middlewares\n7. Controllers\n8. Router\n\n### Cluster vs Single Mode\n\n- **Cluster Mode** (default) - Multi-process with master, agent, and worker processes\n- **Single Mode** - Single process for development/testing\n\n## Working with TypeScript\n\n- All packages use strict TypeScript mode\n- Uses tsdown for unbundled ESM builds (preserves file structure)\n- Each package has `tsdown.config.ts` for build configuration\n- **All sub-project tsconfig.json files MUST extend from root:** `\"extends\": \"../../tsconfig.json\"`\n- Root tsconfig.json includes all packages in `references` array\n\n## utoo Workspace & Catalog Dependencies\n\n- Dependencies defined in `.utoo.toml` catalog section\n- Reference catalog entries: `\"package-name\": \"catalog:\"`\n- Internal workspace dependencies: `\"package-name\": \"workspace:*\"`\n- This ensures consistent versions across all packages\n\n## Common Issues and Troubleshooting\n\n### Test Failures\n\n- Some tests may fail in fresh environments - this is normal\n- Focus on fixing only failures related to your changes\n- Examples may have runtime issues - don't use them for validation\n- If tegg tests fail with `duplicate proto` after a local build, remove stale\n  `dist/` directories outside fixtures and re-run tests. Built `dist/*.js`\n  files can be scanned alongside `src/*.ts`, loading the same decorated class\n  twice.\n\n### Build Issues\n\n- Run `ut run build` when validating build output, package exports, or changes\n  that affect generated artifacts.\n- TypeScript compilation errors will show clearly\n- Build warnings are generally acceptable\n\n### ESM/CommonJS Issues\n\n- Framework supports both ESM and CommonJS\n- Main package exports both formats\n- If you see \"ERR_UNKNOWN_FILE_EXTENSION\" errors, ensure packages are built first\n\n## File Locations Reference\n\n### Key Configuration Files\n\n- `package.json` / `.utoo.toml` - Workspace patterns and catalog configuration\n- `package.json` - Root monorepo scripts and devDependencies\n- `packages/egg/package.json` - Main framework package configuration\n- `packages/egg/tsdown.config.ts` - Build configuration\n- `packages/egg/src/config/plugin.ts` - Built-in plugin configurations\n- `packages/egg/src/config/config.default.ts` - Default framework configuration\n\n### Important Source Files\n\n- `packages/egg/src/lib/application.ts` - Main Application class\n- `packages/egg/src/lib/agent.ts` - Agent process manager\n- `packages/egg/src/lib/egg.ts` - Core EggApplicationCore\n- `packages/egg/src/lib/start.ts` - Application startup logic\n- `packages/egg/src/lib/loader/` - Convention-based loaders\n\n### Build Outputs\n\n- `packages/*/dist/` - Built JavaScript and TypeScript definitions\n- `site/dist/` - Built documentation site\n\n## Validation Scenarios\n\nAfter making changes, always verify:\n\n1. **Linting Passes**: `ut run lint` shows no new errors\n2. **Tests Run From Clean Sources**: `ut run test` executes without stale build artifacts interfering\n3. **Documentation Loads**: `ut run site:dev` starts successfully and the printed VitePress URL responds\n4. **Build Success When Relevant**: `ut run build` completes without errors\n\n**Remember**: This is a complex enterprise framework. Validate incrementally, keep unit tests isolated from stale build artifacts, and focus on the core packages (`egg`, `core`, `utils`) for most development work.\n\n## Commit Message Format\n\n**CRITICAL: All commits MUST follow the [Angular Commit Message Format](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines) as specified in CONTRIBUTING.md.**\n\n### Required Format Structure\n\n```\n<type>(<scope>): <subject>\n<BLANK LINE>\n<body>\n<BLANK LINE>\n<footer>\n```\n\n### Mandatory Types\n\n- **feat**: A new feature\n- **fix**: A bug fix\n- **docs**: Documentation-only changes\n- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)\n- **refactor**: A code change that neither fixes a bug nor adds a feature\n- **perf**: A code change that improves performance\n- **test**: Adding missing tests\n- **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation\n- **deps**: Updates about dependencies\n\n### Scope Guidelines\n\n- **Package-specific changes**: Use package names like `core`, `mock`, `cluster`, `utils`, `tsconfig`, `extend2`\n- **Cross-package changes**: Use feature areas like `loader`, `plugin`, `config`, `build`\n- **Component-specific**: Use component names like `application`, `agent`, `context`\n\n### Subject Rules\n\n- Use imperative, present tense: \"change\" not \"changed\" nor \"changes\"\n- Don't capitalize first letter\n- No period (.) at the end\n- Be succinct and descriptive\n\n### Examples\n\n```\nfeat(tsconfig): integrate package into monorepo with vitest\n\nMerge @eggjs/tsconfig repository into packages/tsconfig/ and refactor\nto use vitest testing framework instead of Node.js test runner.\n\n- Update all consuming packages to use workspace:* dependencies\n- Add vitest configuration and convert test assertions\n- Remove external catalog dependency in favor of workspace package\n\nCloses #123\n```\n\n```\nfix(core): resolve loader initialization race condition\n\nThe loader was attempting to initialize plugins before configurations\nwere fully loaded, causing intermittent startup failures.\n\nFixes #456\n```\n\n```\nchore: update dependencies to latest versions\n\nUpdate catalog dependencies and rebuild packages to ensure\ncompatibility with latest versions.\n```\n\n**NEVER commit without following this format - it breaks the project's automated changelog and release process.**\n","AGENTS.md":"# AGENTS.md\n\nThis is the canonical shared instruction file for coding agents working in this repository.\n\nIf another agent-specific file exists, it should import or defer to this file for shared repository guidance.\n\n## Project Map\n\nEgg is maintained as a utoo monorepo.\n\n- `packages/` contains core framework packages and shared internals.\n- `plugins/` contains optional Egg integrations.\n- `tools/` contains developer tooling such as CLI packages.\n- `tegg/` contains the tegg ecosystem.\n- `examples/` contains sample applications.\n- `site/docs/` contains the English and Chinese documentation site.\n- tests usually live beside packages under `test/`, often with fixtures under `test/fixtures/`.\n\n## Core Commands\n\nThe repository runs on [utoo](https://github.com/utooland/utoo) (`ut`); workspace patterns and overrides live in `package.json`, and shared catalogs live in the tracked `.utoo.toml`. CI installs the latest utoo release; the old pnpm configuration is retained only as a migration reference.\n\n- `npm install --global utoo@latest` installs the latest utoo release on a clean machine.\n- `ut install` hydrates the workspace.\n- `ut run build` builds all packages.\n- `ut run test` runs the main test suite.\n- `ut run lint` runs linting.\n- `ut run typecheck` runs TypeScript checking.\n- use filtered commands for focused work, for example `ut run test --workspace @eggjs/bin` or `ut run build --workspace @eggjs/bin`; prefer the package-name form of `--workspace` (the `./tools/...` path form does not match on Windows); a package without its own script (for example `build` in @eggjs/scripts) needs the root script plus the tsdown workspace path filter instead: `ut run build -- --workspace ./tools/scripts`.\n\n### Local CI\n\nRun tests **without building first**. The CI workflow (`ut install → ut run ci`) never runs `build` before tests. If `dist/` directories exist from a prior build, tegg plugin tests will fail with `duplicate proto` errors because globby scans both `src/*.ts` and `dist/*.js`, loading the same decorated class twice.\n\nWhen you see `duplicate proto` failures locally:\n\n```bash\nfind tegg packages plugins tools -name dist -type d \\\n  -not -path '*/node_modules/*' -not -path '*/test/*' -not -path '*/fixtures/*' \\\n  -exec rm -rf {} +\n```\n\nThen re-run tests.\n\n## Coding Conventions\n\n- prefer existing repo patterns over inventing new ones\n- prefer ESM and TypeScript-first changes where applicable\n- keep file names lowercase with hyphens\n- keep public API changes deliberate and documented\n- use `oxfmt` and `oxlint --type-aware` conventions already present in the repo\n- **tegg multi-app isolation**: do NOT introduce new process-global mutable\n  runtime state in `tegg/`; per-app state must be backed by a `TeggScope` slot.\n  Hooks registered through the bag-pinned `app.*LifecycleUtil` getters need no\n  extra wrap; detached/escape-point access (timers, emitter listeners, proxy\n  handlers, module-level lifecycle-util statics) must run inside\n  `TeggScope.run(app._teggScopeBag, ...)`. See the \"Multi-App Isolation\n  (TeggScope)\" section in `tegg/AGENTS.md` for the full rules.\n- **V8 startup snapshot lifecycle**: a snapshot build runs through\n  `configWillLoad` and resumes from `configDidLoad` only after restore. Plugin\n  constructors and `configWillLoad` must therefore keep only serializable\n  configuration and metadata; create cluster clients, sockets, servers, file\n  watchers, timers, native clients, and other runtime resources in\n  `configDidLoad` or a later hook. If one plugin consumes another plugin's\n  runtime instance, declare that plugin dependency so their `configDidLoad`\n  ordering is deterministic. Do not hide an early initialization violation\n  behind a placeholder/deferred proxy or recorded-call replay; fail fast and\n  move the initialization to the correct lifecycle phase. Use\n  `snapshotWillSerialize`/`snapshotDidDeserialize` only for framework-owned\n  resources that must exist before the cutoff and have an explicit symmetric\n  release/restore implementation.\n- **V8 startup snapshot dependencies**: the egg-bundler can build a V8 startup\n  snapshot (`snapshot: true`), where the app boots only to `configWillLoad` at\n  BUILD time. Any module loaded or instantiated during that boot that creates a\n  non-serializable native binding — llhttp `HTTPParser` (http/https/undici),\n  `nghttp2` (http2, and anything built on it), tls `SecureContext`, dns\n  `ChannelWrap`, a `WebAssembly` instance (undici's llhttp; WASM is disabled under\n  `--build-snapshot`), fs watchers, native addons, open sockets — makes the\n  snapshot build FATAL (\"global handle not serialized\"). Such modules must be kept\n  EXTERNAL (not inlined) so the prelude stubs them at build and forwards to the\n  real module via `globalThis.__RUNTIME_REQUIRE` at restore. The framework default\n  list is `DEFAULT_SNAPSHOT_LAZY_MODULES` in `tools/egg-bundler/src/lib/prelude.ts`\n  (network builtins + `inspector` + `undici` + `urllib`); apps extend it via\n  `egg.snapshot.lazyModules` in `package.json`. **When adding a framework\n  dependency that touches the network/native stack during boot, check whether it\n  must be added to that list.** A package that only reaches the network stack\n  _transitively_ is already covered because those builtins are lazy (e.g.\n  `@modelcontextprotocol/sdk` → `@hono/node-server` → `http2`, `@grpc/grpc-js` →\n  `http2`); only a package that DIRECTLY creates native/WASM state at module-eval\n  or boot-time instantiation (like `undici`) needs adding. See the \"Snapshot\n  lazy-external defaults\" section in `wiki/packages/egg-bundler.md` for details.\n\n## TypeScript Global Types\n\n- put package-wide global augmentations in a dedicated `src/global.ts` or `src/global.d.ts`\n- shared cross-package global types belong in `@eggjs/typings`, not in one consumer package\n- import shared global augmentations from the package entry that needs the type surface, for example `import '@eggjs/typings/global'`\n- keep `declare global` files as modules by using an `import type` or `export {}`\n\n## Testing And PR Expectations\n\n- run the most targeted tests that validate the touched area\n- include regression coverage when changing loader, cluster, agent, HTTP, or process behavior\n- use Angular-style commit messages such as `fix(loader): ensure middleware order`\n- keep PR descriptions clear about motivation, scope, and test evidence\n\n## Security And Config\n\n- review `SECURITY.md` before handling vulnerability-related work\n- do not commit secrets, credentials, or local-only URLs\n- keep local Node.js aligned with `engines.node` and use the latest utoo release\n\n## Shared Knowledge Workflow\n\nThis repository also maintains an LLM-owned wiki for durable project knowledge.\n\n### Explicit Wiki Write Trigger\n\nCreate, update, reorganize, or log wiki content only when the user explicitly\nrequests a wiki build or maintenance task. Reading the wiki for context does\nnot authorize writing to it.\n\nOrdinary coding, debugging, testing, reviews, documentation edits, and PR work\nmust not produce wiki changes, even when they reveal durable findings or change\nAPIs, architecture, contributor workflows, or testing expectations. Do not\ninclude incidental wiki updates in commits or PRs. Report relevant findings in\nthe task response or PR description instead.\n\nThe wiki writing rules below apply only within an explicitly requested wiki\ntask. Use this three-layer model:\n\n### Raw Sources\n\nRaw sources are the source of truth.\n\nThey include:\n\n- repository code under `packages/`, `plugins/`, `tools/`, `tegg/`, `examples/`, and `scripts/`\n- user-facing docs under `site/docs/`\n- root markdown files such as `README.md`, `README.zh-CN.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, and `SECURITY.md`\n- explicit external artifacts referenced by the user or task\n- imported materials stored under `raw/`\n\nRules:\n\n- do not treat wiki summaries as authoritative when raw sources disagree\n- do not rewrite raw sources unless the task requires it\n- during an explicitly requested wiki task, capture repeatedly used external sources in the wiki\n\n### Wiki\n\nThe wiki lives under `wiki/` and stores durable synthesized knowledge.\n\n### Schema\n\nShared workflow rules live here in `AGENTS.md`.\n\nAgent-specific files should stay thin and point back to this file instead of duplicating the schema.\n\n## Wiki Layout\n\n- when consulting the wiki, read `wiki/index.md` first\n- `wiki/log.md` is the append-only chronological log\n- `wiki/packages/` holds package, plugin, tool, and subsystem pages\n- `wiki/concepts/` holds architectural and cross-cutting pages\n- `wiki/workflows/` holds repeatable procedures\n- `wiki/decisions/` holds notable tradeoffs and decisions\n- `wiki/sources/` holds summaries of major source documents or external materials\n\n## Wiki Page Rules\n\nUse these page types:\n\n- `package`\n- `concept`\n- `workflow`\n- `decision`\n- `source`\n\nEvery substantive page should begin with frontmatter:\n\n```yaml\n---\ntitle: Short human-readable title\ntype: package|concept|workflow|decision|source\nsummary: One-line summary\nsource_files:\n  - path/or/url\nupdated_at: YYYY-MM-DD\nstatus: seed|active|stale\n---\n```\n\nUse lowercase kebab-case filenames and keep one topic per page.\n\nUse the workspace-local Asia/Shanghai calendar date for wiki log headings and\nfrontmatter `updated_at` values.\n\n## Citation And Freshness Rules\n\n- every nontrivial wiki claim should be traceable to raw sources\n- list major source paths in `source_files`\n- label non-obvious synthesis as `Inference:`\n- record conflicts explicitly instead of flattening them\n- mark stale or unresolved claims when freshness is uncertain\n\n## Index And Log Rules\n\n`wiki/index.md` should:\n\n- list durable pages by category\n- give each page a one-line summary\n- stay concise enough to scan quickly\n\n`wiki/log.md` should record:\n\n- ingestion of substantial new sources\n- durable findings produced during a query\n- material wiki refactors or lint passes\n- code or docs changes that alter previously recorded understanding\n\nDo not log trivial typo-only edits.\n\n## Standard Workflows\n\nIngest and Lint are wiki maintenance workflows and require an explicit user\nrequest. Query is read-only by default; its write-back steps apply only when\nthe user also explicitly requests wiki construction or maintenance.\n\n### Ingest\n\n1. Read `wiki/index.md`.\n2. Find existing pages that should absorb the new information.\n3. Create or update a `source` page if the source is substantial.\n4. Update impacted wiki pages.\n5. Update `wiki/index.md`.\n6. Append to `wiki/log.md` if the wiki changed materially.\n\n### Query\n\n1. Read `wiki/index.md` and relevant wiki pages first.\n2. Use the wiki as the starting point, not the final authority.\n3. Read raw sources for verification, detail, or freshness.\n4. Only within an explicitly requested wiki task, write durable findings back into the wiki.\n5. Only within that wiki task, update the log when the wiki changes materially.\n\n### Lint\n\nCheck the wiki for:\n\n- orphan pages\n- stale claims after code or docs changes\n- duplicated concepts\n- missing source references\n- missing pages for frequently touched areas\n- contradictions between pages\n\nPrefer restructuring pages over making them longer.\n\n## Repo-Specific Wiki Priorities\n\nPrioritize durable wiki coverage for:\n\n- `packages/egg`, `packages/core`, `packages/utils`, and other foundational packages\n- plugins under `plugins/`\n- tools under `tools/`\n- `tegg/`\n- docs structure under `site/docs/`\n- loading, lifecycle, plugin model, testing, release, and docs-maintenance concepts\n\n## Change Trigger\n\nOnly an explicit user request to build or maintain the wiki triggers wiki\noutput. Code or documentation changes and newly discovered findings do not\ntrigger it automatically. Leave existing wiki files unchanged during ordinary\ntasks.\n","packages/cluster/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fcluster%2FREADME.md -->","packages/cookies/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fcookies%2FREADME.md -->","packages/core/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fcore%2FREADME.md -->","packages/core/benchmark/middleware/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fcore%2Fbenchmark%2Fmiddleware%2FREADME.md -->","packages/egg/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fegg%2FREADME.md -->","packages/errors/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Ferrors%2FREADME.md -->","packages/extend2/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fextend2%2FREADME.md -->","packages/koa-static-cache/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fkoa-static-cache%2FREADME.md -->","packages/koa/Readme.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fkoa%2FReadme.md -->","packages/koa/docs/api/context.md":"# Context\n\nA Koa Context encapsulates node's `request` and `response` objects\ninto a single object which provides many helpful methods for writing\nweb applications and APIs.\nThese operations are used so frequently in HTTP server development\nthat they are added at this level instead of a higher level framework,\nwhich would force middleware to re-implement this common functionality.\n\nA `Context` is created _per_ request, and is referenced in middleware\nas the receiver, or the `ctx` identifier, as shown in the following\nsnippet:\n\n```js\napp.use(async (ctx) => {\n  ctx; // is the Context\n  ctx.request; // is a Koa Request\n  ctx.response; // is a Koa Response\n});\n```\n\nMany of the context's accessors and methods simply delegate to their `ctx.request` or `ctx.response`\nequivalents for convenience, and are otherwise identical. For example `ctx.type` and `ctx.length`\ndelegate to the `response` object, and `ctx.path` and `ctx.method` delegate to the `request`.\n\n## API\n\n`Context` specific methods and accessors.\n\n### ctx.req\n\nNode's `request` object.\n\n### ctx.res\n\nNode's `response` object.\n\nBypassing Koa's response handling is **not supported**. Avoid using the following node properties:\n\n- `res.statusCode`\n- `res.writeHead()`\n- `res.write()`\n- `res.end()`\n\n### ctx.request\n\nA Koa `Request` object.\n\n### ctx.response\n\nA Koa `Response` object.\n\n### ctx.state\n\nThe recommended namespace for passing information through middleware and to your frontend views.\n\n```js\nctx.state.user = await User.find(id);\n```\n\n### ctx.app\n\nApplication instance reference.\n\n### ctx.app.emit\n\nKoa applications extend an internal [EventEmitter](https://nodejs.org/dist/latest-v11.x/docs/api/events.html). `ctx.app.emit` emits an event with a type, defined by the first argument. For each event you can hook up \"listeners\", which is a function that is called when the event is emitted. Consult the [error handling docs](https://koajs.com/#error-handling) for more information.\n\n### ctx.cookies.get(name, [options])\n\nGet cookie `name` with `options`:\n\n- `signed` the cookie requested should be signed\n\nKoa uses the [cookies](https://github.com/pillarjs/cookies) module where options are simply passed.\n\n### ctx.cookies.set(name, value, [options])\n\nSet cookie `name` to `value` with `options`:\n\n- `maxAge`: a number representing the milliseconds from `Date.now()` for expiry.\n- `expires`: a `Date` object indicating the cookie's expiration date (expires at the end of session by default).\n- `path`: a string indicating the path of the cookie (`/` by default).\n- `domain`: a string indicating the domain of the cookie (no default).\n- `secure`: a boolean indicating whether the cookie is only to be sent over HTTPS (`false` by default for HTTP, `true` by default for HTTPS). [Read more about this option](https://github.com/pillarjs/cookies#secure-cookies).\n- `httpOnly`: a boolean indicating whether the cookie is only to be sent over HTTP(S), and not made available to client JavaScript (`true` by default).\n- `sameSite`: a boolean or string indicating whether the cookie is a \"same site\" cookie (`false` by default). This can be set to `'strict'`, `'lax'`, `'none'`, or `true` (which maps to `'strict'`).\n- `signed`: a boolean indicating whether the cookie is to be signed (`false` by default). If this is true, another cookie of the same name with the `.sig` suffix appended will also be sent, with a 27-byte url-safe base64 SHA1 value representing the hash of _cookie-name_=_cookie-value_ against the first [Keygrip](https://www.npmjs.com/package/keygrip) key. This signature key is used to detect tampering the next time a cookie is received.\n- `overwrite`: a boolean indicating whether to overwrite previously set cookies of the same name (`false` by default). If this is true, all cookies set during the same request with the same name (regardless of path or domain) are filtered out of the Set-Cookie header when setting this cookie.\n\nKoa uses the [cookies](https://github.com/pillarjs/cookies) module where options are simply passed.\n\n### ctx.throw([status], [msg], [properties])\n\nHelper method to throw an error with a `.status` property\ndefaulting to `500` that will allow Koa to respond appropriately.\nThe following combinations are allowed:\n\n```js\nctx.throw(400);\nctx.throw(400, 'name required');\nctx.throw(400, 'name required', { user: user });\n```\n\nFor example `ctx.throw(400, 'name required')` is equivalent to:\n\n```js\nconst err = new Error('name required');\nerr.status = 400;\nerr.expose = true;\nthrow err;\n```\n\nNote that these are user-level errors and are flagged with\n`err.expose` meaning the messages are appropriate for\nclient responses, which is typically not the case for\nerror messages since you do not want to leak failure\ndetails.\n\nYou may optionally pass a `properties` object which is merged into the error as-is, useful for decorating machine-friendly errors which are reported to the requester upstream.\n\n```js\nctx.throw(401, 'access_denied', { user: user });\n```\n\nKoa uses [http-errors](https://github.com/jshttp/http-errors) to create errors. `status` should only be passed as the first parameter.\n\n### ctx.assert(value, [status], [msg], [properties])\n\nHelper method to throw an error similar to `.throw()`\nwhen `!value`. Similar to node's [assert()](http://nodejs.org/api/assert.html)\nmethod.\n\n```js\nctx.assert(ctx.state.user, 401, 'User not found. Please login!');\n```\n\n### ctx.respond\n\nTo bypass Koa's built-in response handling, you may explicitly set `ctx.respond = false;`. Use this if you want to write to the raw `res` object instead of letting Koa handle the response for you.\n\nNote that using this is **not** supported by Koa. This may break intended functionality of Koa middleware and Koa itself. Using this property is considered a hack and is only a convenience to those wishing to use traditional `fn(req, res)` functions and middleware within Koa.\n\n## Request aliases\n\nThe following accessors and alias [Request](request.md) equivalents:\n\n- `ctx.header`\n- `ctx.headers`\n- `ctx.method`\n- `ctx.method=`\n- `ctx.url`\n- `ctx.url=`\n- `ctx.originalUrl`\n- `ctx.origin`\n- `ctx.href`\n- `ctx.path`\n- `ctx.path=`\n- `ctx.query`\n- `ctx.query=`\n- `ctx.querystring`\n- `ctx.querystring=`\n- `ctx.host`\n- `ctx.hostname`\n- `ctx.fresh`\n- `ctx.stale`\n- `ctx.socket`\n- `ctx.protocol`\n- `ctx.secure`\n- `ctx.ip`\n- `ctx.ips`\n- `ctx.subdomains`\n- `ctx.is()`\n- `ctx.accepts()`\n- `ctx.acceptsEncodings()`\n- `ctx.acceptsCharsets()`\n- `ctx.acceptsLanguages()`\n- `ctx.get()`\n\n## Response aliases\n\nThe following accessors and alias [Response](response.md) equivalents:\n\n- `ctx.body`\n- `ctx.body=`\n- `ctx.status`\n- `ctx.status=`\n- `ctx.message`\n- `ctx.message=`\n- `ctx.length=`\n- `ctx.length`\n- `ctx.type=`\n- `ctx.type`\n- `ctx.headerSent`\n- `ctx.redirect()`\n- `ctx.attachment()`\n- `ctx.set()`\n- `ctx.append()`\n- `ctx.remove()`\n- `ctx.lastModified=`\n- `ctx.etag=`\n","packages/koa/docs/api/index.md":"# Installation\n\nKoa requires **node v7.6.0** or higher for ES2015 and async function support.\n\nYou can quickly install a supported version of node with your favorite version manager:\n\n```bash\n$ nvm install 7\n$ npm i koa\n$ node my-koa-app.js\n```\n\n# Application\n\nA Koa application is an object containing an array of middleware functions\nwhich are composed and executed in a stack-like manner upon request. Koa is similar to many\nother middleware systems that you may have encountered such as Ruby's Rack, Connect, and so on -\nhowever a key design decision was made to provide high level \"sugar\" at the otherwise low-level\nmiddleware layer. This improves interoperability, robustness, and makes writing middleware much\nmore enjoyable.\n\nThis includes methods for common tasks like content-negotiation, cache freshness, proxy support, and redirection\namong others. Despite supplying a reasonably large number of helpful methods Koa maintains a small footprint, as\nno middleware are bundled.\n\nThe obligatory hello world application:\n\n<!-- runkit:endpoint -->\n\n```js\nconst Koa = require('koa');\nconst app = new Koa();\n\napp.use(async (ctx) => {\n  ctx.body = 'Hello World';\n});\n\napp.listen(3000);\n```\n\n## Cascading\n\nKoa middleware cascade in a more traditional way as you may be used to with similar tools -\nthis was previously difficult to make user friendly with node's use of callbacks.\nHowever with async functions we can achieve \"true\" middleware. Contrasting Connect's implementation which\nsimply passes control through series of functions until one returns, Koa invoke \"downstream\", then\ncontrol flows back \"upstream\".\n\nThe following example responds with \"Hello World\", however first the request flows through\nthe `x-response-time` and `logging` middleware to mark when the request started, then yields control through the response middleware. When a middleware invokes `next()`\nthe function suspends and passes control to the next middleware defined. After there are no more\nmiddleware to execute downstream, the stack will unwind and each middleware is resumed to perform\nits upstream behaviour.\n\n<!-- runkit:endpoint -->\n\n```js\nconst Koa = require('koa');\nconst app = new Koa();\n\n// logger\n\napp.use(async (ctx, next) => {\n  await next();\n  const rt = ctx.response.get('X-Response-Time');\n  console.log(`${ctx.method} ${ctx.url} - ${rt}`);\n});\n\n// x-response-time\n\napp.use(async (ctx, next) => {\n  const start = Date.now();\n  await next();\n  const ms = Date.now() - start;\n  ctx.set('X-Response-Time', `${ms}ms`);\n});\n\n// response\n\napp.use(async (ctx) => {\n  ctx.body = 'Hello World';\n});\n\napp.listen(3000);\n```\n\n## Settings\n\nApplication settings are properties on the `app` instance, currently\nthe following are supported:\n\n- `app.env` defaulting to the **NODE_ENV** or \"development\"\n- `app.keys` array of signed cookie keys\n- `app.proxy` when true proxy header fields will be trusted\n- `app.subdomainOffset` offset of `.subdomains` to ignore, default to 2\n- `app.proxyIpHeader` proxy ip header, default to `X-Forwarded-For`\n- `app.maxIpsCount` max ips read from proxy ip header, default to 0 (means infinity)\n\nYou can pass the settings to the constructor:\n\n```js\nconst Koa = require('koa');\nconst app = new Koa({ proxy: true });\n```\n\nor dynamically:\n\n```js\nconst Koa = require('koa');\nconst app = new Koa();\napp.proxy = true;\n```\n\n## app.listen(...)\n\nA Koa application is not a 1-to-1 representation of an HTTP server.\nOne or more Koa applications may be mounted together to form larger\napplications with a single HTTP server.\n\nCreate and return an HTTP server, passing the given arguments to\n`Server#listen()`. These arguments are documented on [nodejs.org](http://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback). The following is a useless Koa application bound to port `3000`:\n\n```js\nconst Koa = require('koa');\nconst app = new Koa();\napp.listen(3000);\n```\n\nThe `app.listen(...)` method is simply sugar for the following:\n\n```js\nconst http = require('http');\nconst Koa = require('koa');\nconst app = new Koa();\nhttp.createServer(app.callback()).listen(3000);\n```\n\nThis means you can spin up the same application as both HTTP and HTTPS\nor on multiple addresses:\n\n```js\nconst http = require('http');\nconst https = require('https');\nconst Koa = require('koa');\nconst app = new Koa();\nhttp.createServer(app.callback()).listen(3000);\nhttps.createServer(app.callback()).listen(3001);\n```\n\n## app.callback()\n\nReturn a callback function suitable for the `http.createServer()`\nmethod to handle a request.\nYou may also use this callback function to mount your Koa app in a\nConnect/Express app.\n\n## app.use(function)\n\nAdd the given middleware function to this application.\n`app.use()` returns `this`, so is chainable.\n\n```js\napp.use(someMiddleware);\napp.use(someOtherMiddleware);\napp.listen(3000);\n```\n\nIs the same as\n\n```js\napp.use(someMiddleware).use(someOtherMiddleware).listen(3000);\n```\n\nSee [Middleware](https://github.com/koajs/koa/wiki#middleware) for\nmore information.\n\n## app.keys=\n\nSet signed cookie keys.\n\nThese are passed to [KeyGrip](https://github.com/crypto-utils/keygrip),\nhowever you may also pass your own `KeyGrip` instance. For\nexample the following are acceptable:\n\n```js\napp.keys = [\n  'OEK5zjaAMPc3L6iK7PyUjCOziUH3rsrMKB9u8H07La1SkfwtuBoDnHaaPCkG5Brg',\n  'MNKeIebviQnCPo38ufHcSfw3FFv8EtnAe1xE02xkN1wkCV1B2z126U44yk2BQVK7',\n];\napp.keys = new KeyGrip(\n  [\n    'OEK5zjaAMPc3L6iK7PyUjCOziUH3rsrMKB9u8H07La1SkfwtuBoDnHaaPCkG5Brg',\n    'MNKeIebviQnCPo38ufHcSfw3FFv8EtnAe1xE02xkN1wkCV1B2z126U44yk2BQVK7',\n  ],\n  'sha256',\n);\n```\n\nFor security reasons, please ensure that the key is long enough and random.\n\nThese keys may be rotated and are used when signing cookies\nwith the `{ signed: true }` option:\n\n```js\nctx.cookies.set('name', 'tobi', { signed: true });\n```\n\n## app.context\n\n`app.context` is the prototype from which `ctx` is created.\nYou may add additional properties to `ctx` by editing `app.context`.\nThis is useful for adding properties or methods to `ctx` to be used across your entire app,\nwhich may be more performant (no middleware) and/or easier (fewer `require()`s)\nat the expense of relying more on `ctx`, which could be considered an anti-pattern.\n\nFor example, to add a reference to your database from `ctx`:\n\n```js\napp.context.db = db();\n\napp.use(async (ctx) => {\n  console.log(ctx.db);\n});\n```\n\nNote:\n\n- Many properties on `ctx` are defined using getters, setters, and `Object.defineProperty()`. You can only edit these properties (not recommended) by using `Object.defineProperty()` on `app.context`. See https://github.com/koajs/koa/issues/652.\n- Mounted apps currently use their parent's `ctx` and settings. Thus, mounted apps are really just groups of middleware.\n\n## Error Handling\n\nBy default outputs all errors to stderr unless `app.silent` is `true`.\nThe default error handler also won't output errors when `err.status` is `404` or `err.expose` is `true`.\nTo perform custom error-handling logic such as centralized logging you can add an \"error\" event listener:\n\n```js\napp.on('error', (err) => {\n  log.error('server error', err);\n});\n```\n\nIf an error is in the req/res cycle and it is _not_ possible to respond to the client, the `Context` instance is also passed:\n\n```js\napp.on('error', (err, ctx) => {\n  log.error('server error', err, ctx);\n});\n```\n\nWhen an error occurs _and_ it is still possible to respond to the client, aka no data has been written to the socket, Koa will respond\nappropriately with a 500 \"Internal Server Error\". In either case\nan app-level \"error\" is emitted for logging purposes.\n","packages/koa/docs/api/request.md":"# Request\n\nA Koa `Request` object is an abstraction on top of node's vanilla request object,\nproviding additional functionality that is useful for every day HTTP server\ndevelopment.\n\n## API\n\n### request.header\n\nRequest header object. This is the same as the [`headers`](https://nodejs.org/api/http.html#http_message_headers) field\non node's [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage).\n\n### request.header=\n\nSet request header object.\n\n### request.headers\n\nRequest header object. Alias as `request.header`.\n\n### request.headers=\n\nSet request header object. Alias as `request.header=`.\n\n### request.method\n\nRequest method.\n\n### request.method=\n\nSet request method, useful for implementing middleware\nsuch as `methodOverride()`.\n\n### request.length\n\nReturn request Content-Length as a number when present, or `undefined`.\n\n### request.url\n\nGet request URL.\n\n### request.url=\n\nSet request URL, useful for url rewrites.\n\n### request.originalUrl\n\nGet request original URL.\n\n### request.origin\n\nGet origin of URL, include `protocol` and `host`.\n\n```js\nctx.request.origin;\n// => http://example.com\n```\n\n### request.href\n\nGet full request URL, include `protocol`, `host` and `url`.\n\n```js\nctx.request.href;\n// => http://example.com/foo/bar?q=1\n```\n\n### request.path\n\nGet request pathname.\n\n### request.path=\n\nSet request pathname and retain query-string when present.\n\n### request.querystring\n\nGet raw query string void of `?`.\n\n### request.querystring=\n\nSet raw query string.\n\n### request.search\n\nGet raw query string with the `?`.\n\n### request.search=\n\nSet raw query string.\n\n### request.host\n\nGet host (hostname:port) when present. Supports `X-Forwarded-Host`\nwhen `app.proxy` is **true**, otherwise `Host` is used.\n\n### request.hostname\n\nGet hostname when present. Supports `X-Forwarded-Host`\nwhen `app.proxy` is **true**, otherwise `Host` is used.\n\nIf host is IPv6, Koa delegates parsing to\n[WHATWG URL API](https://nodejs.org/dist/latest-v8.x/docs/api/url.html#url_the_whatwg_url_api),\n_Note_ This may impact performance.\n\n### request.URL\n\nGet WHATWG parsed URL object.\n\n### request.type\n\nGet request `Content-Type` void of parameters such as \"charset\".\n\n```js\nconst ct = ctx.request.type;\n// => \"image/png\"\n```\n\n### request.charset\n\nGet request charset when present, or `undefined`:\n\n```js\nctx.request.charset;\n// => \"utf-8\"\n```\n\n### request.query\n\nGet parsed query-string, returning an empty object when no\nquery-string is present. Note that this getter does _not_\nsupport nested parsing.\n\nFor example \"color=blue&size=small\":\n\n```js\n{\n  color: 'blue',\n  size: 'small'\n}\n```\n\n### request.query=\n\nSet query-string to the given object. Note that this\nsetter does _not_ support nested objects.\n\n```js\nctx.query = { next: '/login' };\n```\n\n### request.fresh\n\nCheck if a request cache is \"fresh\", aka the contents have not changed. This\nmethod is for cache negotiation between `If-None-Match` / `ETag`, and `If-Modified-Since` and `Last-Modified`. It should be referenced after setting one or more of these response headers.\n\n```js\n// freshness check requires status 20x or 304\nctx.status = 200;\nctx.set('ETag', '123');\n\n// cache is ok\nif (ctx.fresh) {\n  ctx.status = 304;\n  return;\n}\n\n// cache is stale\n// fetch new data\nctx.body = await db.find('something');\n```\n\n### request.stale\n\nInverse of `request.fresh`.\n\n### request.protocol\n\nReturn request protocol, \"https\" or \"http\". Supports `X-Forwarded-Proto`\nwhen `app.proxy` is **true**.\n\n### request.secure\n\nShorthand for `ctx.protocol == \"https\"` to check if a request was\nissued via TLS.\n\n### request.ip\n\nRequest remote address. Supports `X-Forwarded-For` when `app.proxy`\nis **true**.\n\n### request.ips\n\nWhen `X-Forwarded-For` is present and `app.proxy` is enabled an array\nof these ips is returned, ordered from upstream -> downstream. When\ndisabled an empty array is returned.\n\nFor example if the value were \"client, proxy1, proxy2\",\nyou would receive the array `[\"client\", \"proxy1\", \"proxy2\"]`.\n\nMost of the reverse proxy(nginx) set x-forwarded-for via\n`proxy_add_x_forwarded_for`, which poses a certain security risk.\nA malicious attacker can forge a client's ip address by forging\na `X-Forwarded-For`request header. The request sent by the client\nhas an `X-Forwarded-For` request header for 'forged'. After being\nforwarded by the reverse proxy, `request.ips` will be\n['forged', 'client', 'proxy1', 'proxy2'].\n\nKoa offers two options to avoid being bypassed.\n\nIf you can control the reverse proxy, you can avoid bypassing\nby adjusting the configuration, or use the `app.proxyIpHeader`\nprovided by koa to avoid reading `x-forwarded-for` to get ips.\n\n```js\nconst app = new Koa({\n  proxy: true,\n  proxyIpHeader: 'X-Real-IP',\n});\n```\n\nIf you know exactly how many reverse proxies are in front of\nthe server, you can avoid reading the user's forged request\nheader by configuring `app.maxIpsCount`:\n\n```js\nconst app = new Koa({\n  proxy: true,\n  maxIpsCount: 1, // only one proxy in front of the server\n});\n\n// request.header['X-Forwarded-For'] === [ '127.0.0.1', '127.0.0.2' ];\n// ctx.ips === [ '127.0.0.2' ];\n```\n\n### request.subdomains\n\nReturn subdomains as an array.\n\nSubdomains are the dot-separated parts of the host before the main domain of\nthe app. By default, the domain of the app is assumed to be the last two\nparts of the host. This can be changed by setting `app.subdomainOffset`.\n\nFor example, if the domain is \"tobi.ferrets.example.com\":\nIf `app.subdomainOffset` is not set, `ctx.subdomains` is `[\"ferrets\", \"tobi\"]`.\nIf `app.subdomainOffset` is 3, `ctx.subdomains` is `[\"tobi\"]`.\n\n### request.is(types...)\n\nCheck if the incoming request contains the \"Content-Type\"\nheader field, and it contains any of the give mime `type`s.\nIf there is no request body, `null` is returned.\nIf there is no content type, or the match fails `false` is returned.\nOtherwise, it returns the matching content-type.\n\n```js\n// With Content-Type: text/html; charset=utf-8\nctx.is('html'); // => 'html'\nctx.is('text/html'); // => 'text/html'\nctx.is('text/*', 'text/html'); // => 'text/html'\n\n// When Content-Type is application/json\nctx.is('json', 'urlencoded'); // => 'json'\nctx.is('application/json'); // => 'application/json'\nctx.is('html', 'application/*'); // => 'application/json'\n\nctx.is('html'); // => false\n```\n\nFor example if you want to ensure that\nonly images are sent to a given route:\n\n```js\nif (ctx.is('image/*')) {\n  // process\n} else {\n  ctx.throw(415, 'images only!');\n}\n```\n\n### Content Negotiation\n\nKoa's `request` object includes helpful content negotiation utilities powered by [accepts](http://github.com/expressjs/accepts) and [negotiator](https://github.com/federomero/negotiator). These utilities are:\n\n- `request.accepts(types)`\n- `request.acceptsEncodings(types)`\n- `request.acceptsCharsets(charsets)`\n- `request.acceptsLanguages(langs)`\n\nIf no types are supplied, **all** acceptable types are returned.\n\nIf multiple types are supplied, the best match will be returned. If no matches are found, a `false` is returned, and you should send a `406 \"Not Acceptable\"` response to the client.\n\nIn the case of missing accept headers where any type is acceptable, the first type will be returned. Thus, the order of types you supply is important.\n\n### request.accepts(types)\n\nCheck if the given `type(s)` is acceptable, returning the best match when true, otherwise `false`. The `type` value may be one or more mime type string\nsuch as \"application/json\", the extension name\nsuch as \"json\", or an array `[\"json\", \"html\", \"text/plain\"]`.\n\n```js\n// Accept: text/html\nctx.accepts('html');\n// => \"html\"\n\n// Accept: text/*, application/json\nctx.accepts('html');\n// => \"html\"\nctx.accepts('text/html');\n// => \"text/html\"\nctx.accepts('json', 'text');\n// => \"json\"\nctx.accepts('application/json');\n// => \"application/json\"\n\n// Accept: text/*, application/json\nctx.accepts('image/png');\nctx.accepts('png');\n// => false\n\n// Accept: text/*;q=.5, application/json\nctx.accepts(['html', 'json']);\nctx.accepts('html', 'json');\n// => \"json\"\n\n// No Accept header\nctx.accepts('html', 'json');\n// => \"html\"\nctx.accepts('json', 'html');\n// => \"json\"\n```\n\nYou may call `ctx.accepts()` as many times as you like,\nor use a switch:\n\n```js\nswitch (ctx.accepts('json', 'html', 'text')) {\n  case 'json':\n    break;\n  case 'html':\n    break;\n  case 'text':\n    break;\n  default:\n    ctx.throw(406, 'json, html, or text only');\n}\n```\n\n### request.acceptsEncodings(encodings)\n\nCheck if `encodings` are acceptable, returning the best match when true, otherwise `false`. Note that you should include `identity` as one of the encodings!\n\n```js\n// Accept-Encoding: gzip\nctx.acceptsEncodings('gzip', 'deflate', 'identity');\n// => \"gzip\"\n\nctx.acceptsEncodings(['gzip', 'deflate', 'identity']);\n// => \"gzip\"\n```\n\nWhen no arguments are given all accepted encodings\nare returned as an array:\n\n```js\n// Accept-Encoding: gzip, deflate\nctx.acceptsEncodings();\n// => [\"gzip\", \"deflate\", \"identity\"]\n```\n\nNote that the `identity` encoding (which means no encoding) could be unacceptable if the client explicitly sends `identity;q=0`. Although this is an edge case, you should still handle the case where this method returns `false`.\n\n### request.acceptsCharsets(charsets)\n\nCheck if `charsets` are acceptable, returning\nthe best match when true, otherwise `false`.\n\n```js\n// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5\nctx.acceptsCharsets('utf-8', 'utf-7');\n// => \"utf-8\"\n\nctx.acceptsCharsets(['utf-7', 'utf-8']);\n// => \"utf-8\"\n```\n\nWhen no arguments are given all accepted charsets\nare returned as an array:\n\n```js\n// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5\nctx.acceptsCharsets();\n// => [\"utf-8\", \"utf-7\", \"iso-8859-1\"]\n```\n\n### request.acceptsLanguages(langs)\n\nCheck if `langs` are acceptable, returning\nthe best match when true, otherwise `false`.\n\n```js\n// Accept-Language: en;q=0.8, es, pt\nctx.acceptsLanguages('es', 'en');\n// => \"es\"\n\nctx.acceptsLanguages(['en', 'es']);\n// => \"es\"\n```\n\nWhen no arguments are given all accepted languages\nare returned as an array:\n\n```js\n// Accept-Language: en;q=0.8, es, pt\nctx.acceptsLanguages();\n// => [\"es\", \"pt\", \"en\"]\n```\n\n### request.idempotent\n\nCheck if the request is idempotent.\n\n### request.socket\n\nReturn the request socket.\n\n### request.get(field)\n\nReturn request header with case-insensitive `field`.\n","packages/koa/docs/api/response.md":"# Response\n\nA Koa `Response` object is an abstraction on top of node's vanilla response object,\nproviding additional functionality that is useful for every day HTTP server\ndevelopment.\n\n## API\n\n### response.header\n\nResponse header object.\n\n### response.headers\n\nResponse header object. Alias as `response.header`.\n\n### response.socket\n\nResponse socket. Points to net.Socket instance as `request.socket`.\n\n### response.status\n\nGet response status. By default, `response.status` is set to `404` unlike node's `res.statusCode` which defaults to `200`.\n\n### response.status=\n\nSet response status via numeric code:\n\n- 100 \"continue\"\n- 101 \"switching protocols\"\n- 102 \"processing\"\n- 200 \"ok\"\n- 201 \"created\"\n- 202 \"accepted\"\n- 203 \"non-authoritative information\"\n- 204 \"no content\"\n- 205 \"reset content\"\n- 206 \"partial content\"\n- 207 \"multi-status\"\n- 208 \"already reported\"\n- 226 \"im used\"\n- 300 \"multiple choices\"\n- 301 \"moved permanently\"\n- 302 \"found\"\n- 303 \"see other\"\n- 304 \"not modified\"\n- 305 \"use proxy\"\n- 307 \"temporary redirect\"\n- 308 \"permanent redirect\"\n- 400 \"bad request\"\n- 401 \"unauthorized\"\n- 402 \"payment required\"\n- 403 \"forbidden\"\n- 404 \"not found\"\n- 405 \"method not allowed\"\n- 406 \"not acceptable\"\n- 407 \"proxy authentication required\"\n- 408 \"request timeout\"\n- 409 \"conflict\"\n- 410 \"gone\"\n- 411 \"length required\"\n- 412 \"precondition failed\"\n- 413 \"payload too large\"\n- 414 \"uri too long\"\n- 415 \"unsupported media type\"\n- 416 \"range not satisfiable\"\n- 417 \"expectation failed\"\n- 418 \"I'm a teapot\"\n- 422 \"unprocessable entity\"\n- 423 \"locked\"\n- 424 \"failed dependency\"\n- 426 \"upgrade required\"\n- 428 \"precondition required\"\n- 429 \"too many requests\"\n- 431 \"request header fields too large\"\n- 500 \"internal server error\"\n- 501 \"not implemented\"\n- 502 \"bad gateway\"\n- 503 \"service unavailable\"\n- 504 \"gateway timeout\"\n- 505 \"http version not supported\"\n- 506 \"variant also negotiates\"\n- 507 \"insufficient storage\"\n- 508 \"loop detected\"\n- 510 \"not extended\"\n- 511 \"network authentication required\"\n\n**NOTE**: don't worry too much about memorizing these strings,\nif you have a typo an error will be thrown, displaying this list\nso you can make a correction.\n\nSince `response.status` default is set to `404`, to send a response\nwithout a body and with a different status is to be done like this:\n\n```js\nctx.response.status = 200;\n\n// Or whatever other status\nctx.response.status = 204;\n```\n\n### response.message\n\nGet response status message. By default, `response.message` is\nassociated with `response.status`.\n\n### response.message=\n\nSet response status message to the given value.\n\n### response.length=\n\nSet response Content-Length to the given value.\n\n### response.length\n\nReturn response Content-Length as a number when present, or deduce\nfrom `ctx.body` when possible, or `undefined`.\n\n### response.body\n\nGet response body.\n\n### response.body=\n\nSet response body to one of the following:\n\n- `string` written\n- `Buffer` written\n- `Stream` piped\n- `Object` || `Array` json-stringified\n- `null` || `undefined` no content response\n\nIf `response.status` has not been set, Koa will automatically set the status to `200` or `204` depending on `response.body`. Specifically, if `response.body` has not been set or has been set as `null` or `undefined`, Koa will automatically set `response.status` to `204`. If you really want to send no content response with other status, you should override the `204` status as the following way:\n\n```js\n// This must be always set first before status, since null | undefined\n// body automatically sets the status to 204\nctx.body = null;\n// Now we override the 204 status with the desired one\nctx.status = 200;\n```\n\nKoa doesn't guard against everything that could be put as a response body -- a function doesn't serialise meaningfully, returning a boolean may make sense based on your application, and while an error works, it may not work as intended as some properties of an error are not enumerable. We recommend adding middleware in your app that asserts body types per app. A sample middleware might be:\n\n```js\napp.use(async (ctx, next) => {\n  await next();\n\n  ctx.assert.equal('object', typeof ctx.body, 500, 'some dev did something wrong');\n});\n```\n\n#### String\n\nThe Content-Type is defaulted to text/html or text/plain, both with\na default charset of utf-8. The Content-Length field is also set.\n\n#### Buffer\n\nThe Content-Type is defaulted to application/octet-stream, and Content-Length\nis also set.\n\n#### Stream\n\nThe Content-Type is defaulted to application/octet-stream.\n\nWhenever a stream is set as the response body, `.onerror` is automatically added as a listener to the `error` event to catch any errors.\nIn addition, whenever the request is closed (even prematurely), the stream is destroyed.\nIf you do not want these two features, do not set the stream as the body directly.\nFor example, you may not want this when setting the body as an HTTP stream in a proxy as it would destroy the underlying connection.\n\nSee: https://github.com/koajs/koa/pull/612 for more information.\n\nHere's an example of stream error handling without automatically destroying the stream:\n\n```js\nconst PassThrough = require('stream').PassThrough;\n\napp.use(async (ctx) => {\n  ctx.body = someHTTPStream.on('error', (err) => ctx.onerror(err)).pipe(PassThrough());\n});\n```\n\n#### Object\n\nThe Content-Type is defaulted to application/json. This includes plain objects `{ foo: 'bar' }` and arrays `['foo', 'bar']`.\n\n### response.get(field)\n\nGet a response header field value with case-insensitive `field`.\n\n```js\nconst etag = ctx.response.get('ETag');\n```\n\n### response.has(field)\n\nReturns `true` if the header identified by name is currently set in the outgoing headers.\nThe header name matching is case-insensitive.\n\n```js\nconst rateLimited = ctx.response.has('X-RateLimit-Limit');\n```\n\n### response.set(field, value)\n\nSet response header `field` to `value`:\n\n```js\nctx.set('Cache-Control', 'no-cache');\n```\n\n### response.append(field, value)\n\nAppend additional header `field` with value `val`.\n\n```js\nctx.append('Link', '<http://127.0.0.1/>');\n```\n\n### response.set(fields)\n\nSet several response header `fields` with an object:\n\n```js\nctx.set({\n  Etag: '1234',\n  'Last-Modified': date,\n});\n```\n\nThis delegates to [setHeader](https://nodejs.org/dist/latest/docs/api/http.html#http_request_setheader_name_value) which sets or updates headers by specified keys and doesn't reset the entire header.\n\n### response.remove(field)\n\nRemove header `field`.\n\n### response.type\n\nGet response `Content-Type` void of parameters such as \"charset\".\n\n```js\nconst ct = ctx.type;\n// => \"image/png\"\n```\n\n### response.type=\n\nSet response `Content-Type` via mime string or file extension.\n\n```js\nctx.type = 'text/plain; charset=utf-8';\nctx.type = 'image/png';\nctx.type = '.png';\nctx.type = 'png';\n```\n\nNote: when appropriate a `charset` is selected for you, for\nexample `response.type = 'html'` will default to \"utf-8\". If you need to overwrite `charset`,\nuse `ctx.set('Content-Type', 'text/html')` to set response header field to value directly.\n\n### response.is(types...)\n\nVery similar to `ctx.request.is()`.\nCheck whether the response type is one of the supplied types.\nThis is particularly useful for creating middleware that\nmanipulate responses.\n\nFor example, this is a middleware that minifies\nall HTML responses except for streams.\n\n```js\nconst minify = require('html-minifier');\n\napp.use(async (ctx, next) => {\n  await next();\n\n  if (!ctx.response.is('html')) return;\n\n  let body = ctx.body;\n  if (!body || body.pipe) return;\n\n  if (Buffer.isBuffer(body)) body = body.toString();\n  ctx.body = minify(body);\n});\n```\n\n### response.redirect(url, [alt])\n\nPerform a [302] redirect to `url`.\n\nThe string \"back\" is special-cased\nto provide Referrer support, when Referrer\nis not present `alt` or \"/\" is used.\n\n```js\nctx.redirect('back');\nctx.redirect('back', '/index.html');\nctx.redirect('/login');\nctx.redirect('http://google.com');\n```\n\nTo alter the default status of `302`, simply assign the status\nbefore or after this call. To alter the body, assign it after this call:\n\n```js\nctx.status = 301;\nctx.redirect('/cart');\nctx.body = 'Redirecting to shopping cart';\n```\n\n### response.attachment([filename], [options])\n\nSet `Content-Disposition` to \"attachment\" to signal the client\nto prompt for download. Optionally specify the `filename` of the\ndownload and some [options](https://github.com/jshttp/content-disposition#options).\n\n### response.headerSent\n\nCheck if a response header has already been sent. Useful for seeing\nif the client may be notified on error.\n\n### response.lastModified\n\nReturn the `Last-Modified` header as a `Date`, if it exists.\n\n### response.lastModified=\n\nSet the `Last-Modified` header as an appropriate UTC string.\nYou can either set it as a `Date` or date string.\n\n```js\nctx.response.lastModified = new Date();\n```\n\n### response.etag=\n\nSet the ETag of a response including the wrapped `\"`s.\nNote that there is no corresponding `response.etag` getter.\n\n```js\nctx.response.etag = crypto.createHash('md5').update(ctx.body).digest('hex');\n```\n\n### response.vary(field)\n\nVary on `field`.\n\n### response.flushHeaders()\n\nFlush any set headers, and begin the body.\n","packages/koa/docs/error-handling.md":"# Error Handling\n\n## Try-Catch\n\nUsing async functions means that you can try-catch `next`.\nThis example adds a `.status` to all errors:\n\n```js\napp.use(async (ctx, next) => {\n  try {\n    await next();\n  } catch (err) {\n    err.status = err.statusCode || err.status || 500;\n    throw err;\n  }\n});\n```\n\n### Default Error Handler\n\nThe default error handler is essentially a `try-catch` at\nthe very beginning of the middleware chain. To use a\ndifferent error handler, simply put another `try-catch` at\nthe beginning of the middleware chain, and handle the error\nthere. However, the default error handler is good enough for\nmost use cases. It will use a status code of `err.status`,\nor by default 500. If `err.expose` is true, then `err.message`\nwill be the reply. Otherwise, a message generated from the\nerror code will be used (e.g. for the code 500 the message\n\"Internal Server Error\" will be used). All headers will be\ncleared from the request, but any headers in `err.headers`\nwill then be set. You can use a `try-catch`, as specified\nabove, to add a header to this list.\n\nHere is an example of creating your own error handler:\n\n```js\napp.use(async (ctx, next) => {\n  try {\n    await next();\n  } catch (err) {\n    // will only respond with JSON\n    ctx.status = err.statusCode || err.status || 500;\n    ctx.body = {\n      message: err.message,\n    };\n  }\n});\n```\n\n## The Error Event\n\nError event listeners can be specified with `app.on('error')`.\nIf no error listener is specified, a default error listener\nis used. Error listeners receive all errors that make their\nway back through the middleware chain, if an error is caught\nand not thrown again, it will not be passed to the error\nlistener. If no error event listener is specified, then\n`app.onerror` will be used, which simply log the error unless\n`error.expose`is true or `app.silent` is true or `error.status`\nis 404.\n","packages/koa/docs/faq.md":"# Frequently Asked Questions\n\n## Does Koa replace Express?\n\nIt's more like Connect, but a lot of the Express goodies\nwere moved to the middleware level in Koa to help form\na stronger foundation. This makes middleware more enjoyable\nand less error-prone to write, for the entire stack, not\njust the end application code.\n\nTypically many middleware would\nre-implement similar features, or even worse incorrectly implement them,\nwhen features like signed cookie secrets among others are typically application-specific,\nnot middleware specific.\n\n## Does Koa replace Connect?\n\nNo, just a different take on similar functionality\nnow that async functions allow us to write code with fewer\ncallbacks. Connect is equally capable, and some may still prefer it,\nit's up to what you prefer.\n\n## Does Koa include routing?\n\nNo - out of the box Koa has no form of routing, however\nmany routing middleware are available: https://github.com/koajs/koa/wiki\n\n## Why isn't Koa just Express 4.0?\n\nKoa is a pretty large departure from what people know about Express,\nthe design is fundamentally much different, so the migration from\nExpress 3.0 to this Express 4.0 would effectively mean rewriting\nthe entire application, so we thought it would be more appropriate\nto create a new library.\n\n## What custom properties do the Koa objects have?\n\nKoa uses its own custom objects: `ctx`, `ctx.request`, and `ctx.response`.\nThese objects abstract node's `req` and `res` objects with convenience methods and getters/setters.\nGenerally, properties added to these objects must obey the following rules:\n\n- They must be either very commonly used and/or must do something useful\n- If a property exists as a setter, then it will also exist as a getter, but not vice versa\n\nMany of `ctx.request` and `ctx.response`'s properties are delegated to `ctx`.\nIf it's a getter/setter, then both the getter and the setter will strictly\ncorrespond to either `ctx.request` or `ctx.response`.\n\nPlease think about these rules before suggesting additional properties.\n","packages/koa/docs/guide.md":"# Guide\n\nThis guide covers Koa topics that are not directly API related, such as best practices for writing middleware and application structure suggestions. In these examples we use async functions as middleware - you can also use commonFunction or generatorFunction which will be a little different.\n\n## Table of Contents\n\n- [Guide](#guide)\n  - [Table of Contents](#table-of-contents)\n  - [Writing Middleware](#writing-middleware)\n  - [Middleware Best Practices](#middleware-best-practices)\n    - [Middleware options](#middleware-options)\n    - [Named middleware](#named-middleware)\n    - [Combining multiple middleware with koa-compose](#combining-multiple-middleware-with-koa-compose)\n    - [Response Middleware](#response-middleware)\n  - [Async operations](#async-operations)\n  - [Debugging Koa](#debugging-koa)\n\n## Writing Middleware\n\nKoa middleware are simple functions which return a `MiddlewareFunction` with signature (ctx, next). When\nthe middleware is run, it must manually invoke `next()` to run the \"downstream\" middleware.\n\nFor example if you wanted to track how long it takes for a request to propagate through Koa by adding an\n`X-Response-Time` header field the middleware would look like the following:\n\n```js\nasync function responseTime(ctx, next) {\n  const start = Date.now();\n  await next();\n  const ms = Date.now() - start;\n  ctx.set('X-Response-Time', `${ms}ms`);\n}\n\napp.use(responseTime);\n```\n\nIf you're a front-end developer you can think any code before `next();` as the \"capture\" phase,\nwhile any code after is the \"bubble\" phase. This crude gif illustrates how async function allow us\nto properly utilize stack flow to implement request and response flows:\n\n![Koa middleware](/docs/middleware.gif)\n\n1.  Create a date to track response time\n2.  Await control to the next middleware\n3.  Create another date to track duration\n4.  Await control to the next middleware\n5.  Set the response body to \"Hello World\"\n6.  Calculate duration time\n7.  Output log line\n8.  Calculate response time\n9.  Set `X-Response-Time` header field\n10. Hand off to Koa to handle the response\n\nNext we'll look at the best practices for creating Koa middleware.\n\n## Middleware Best Practices\n\nThis section covers middleware authoring best practices, such as middleware\naccepting options, named middleware for debugging, among others.\n\n### Middleware options\n\nWhen creating a public middleware, it's useful to conform to the convention of\nwrapping the middleware in a function that accepts options, allowing users to\nextend functionality. Even if your middleware accepts _no_ options, this is still\na good idea to keep things uniform.\n\nHere our contrived `logger` middleware accepts a `format` string for customization,\nand returns the middleware itself:\n\n```js\nfunction logger(format) {\n  format = format || ':method \":url\"';\n\n  return async function (ctx, next) {\n    const str = format.replace(':method', ctx.method).replace(':url', ctx.url);\n\n    console.log(str);\n\n    await next();\n  };\n}\n\napp.use(logger());\napp.use(logger(':method :url'));\n```\n\n### Named middleware\n\nNaming middleware is optional, however it's useful for debugging purposes to assign a name.\n\n```js\nfunction logger(format) {\n  return async function logger(ctx, next) {};\n}\n```\n\n### Combining multiple middleware with koa-compose\n\nSometimes you want to \"compose\" multiple middleware into a single middleware for easy re-use or exporting. You can use [koa-compose](https://github.com/koajs/compose)\n\n```js\nconst compose = require('koa-compose');\n\nasync function random(ctx, next) {\n  if ('/random' == ctx.path) {\n    ctx.body = Math.floor(Math.random() * 10);\n  } else {\n    await next();\n  }\n}\n\nasync function backwards(ctx, next) {\n  if ('/backwards' == ctx.path) {\n    ctx.body = 'sdrawkcab';\n  } else {\n    await next();\n  }\n}\n\nasync function pi(ctx, next) {\n  if ('/pi' == ctx.path) {\n    ctx.body = String(Math.PI);\n  } else {\n    await next();\n  }\n}\n\nconst all = compose([random, backwards, pi]);\n\napp.use(all);\n```\n\n### Response Middleware\n\nMiddleware that decide to respond to a request and wish to bypass downstream middleware may\nsimply omit `next()`. Typically this will be in routing middleware, but this can be performed by\nany. For example the following will respond with \"two\", however all three are executed, giving the\ndownstream \"three\" middleware a chance to manipulate the response.\n\n```js\napp.use(async function (ctx, next) {\n  console.log('>> one');\n  await next();\n  console.log('<< one');\n});\n\napp.use(async function (ctx, next) {\n  console.log('>> two');\n  ctx.body = 'two';\n  await next();\n  console.log('<< two');\n});\n\napp.use(async function (ctx, next) {\n  console.log('>> three');\n  await next();\n  console.log('<< three');\n});\n```\n\nThe following configuration omits `next()` in the second middleware, and will still respond\nwith \"two\", however the third (and any other downstream middleware) will be ignored:\n\n```js\napp.use(async function (ctx, next) {\n  console.log('>> one');\n  await next();\n  console.log('<< one');\n});\n\napp.use(async function (ctx, next) {\n  console.log('>> two');\n  ctx.body = 'two';\n  console.log('<< two');\n});\n\napp.use(async function (ctx, next) {\n  console.log('>> three');\n  await next();\n  console.log('<< three');\n});\n```\n\nWhen the furthest downstream middleware executes `next();`, it's really yielding to a noop\nfunction, allowing the middleware to compose correctly anywhere in the stack.\n\n## Async operations\n\nAsync function and promise forms Koa's foundation, allowing\nyou to write non-blocking sequential code. For example this middleware reads the filenames from `./docs`,\nand then reads the contents of each markdown file in parallel before assigning the body to the joint result.\n\n```js\nconst fs = require('mz/fs');\n\napp.use(async function (ctx, next) {\n  const paths = await fs.readdir('docs');\n  const files = await Promise.all(paths.map((path) => fs.readFile(`docs/${path}`, 'utf8')));\n\n  ctx.type = 'markdown';\n  ctx.body = files.join('');\n});\n```\n\n## Debugging Koa\n\nKoa along with many of the libraries it's built with support the **DEBUG** environment variable from [debug](https://github.com/visionmedia/debug) which provides simple conditional logging.\n\nFor example\nto see all Koa-specific debugging information just pass `DEBUG=koa*` and upon boot you'll see the list of middleware used, among other things.\n\n```\n$ DEBUG=koa* node --harmony examples/simple\n  koa:application use responseTime +0ms\n  koa:application use logger +4ms\n  koa:application use contentLength +0ms\n  koa:application use notfound +0ms\n  koa:application use response +0ms\n  koa:application listen +0ms\n```\n\nSince JavaScript does not allow defining function names at\nruntime, you can also set a middleware's name as `._name`.\nThis is useful when you don't have control of a middleware's name.\nFor example:\n\n```js\nconst path = require('path');\nconst serve = require('koa-static');\n\nconst publicFiles = serve(path.join(__dirname, 'public'));\npublicFiles._name = 'static /public';\n\napp.use(publicFiles);\n```\n\nNow, instead of just seeing \"serve\" when debugging, you will see:\n\n```\n  koa:application use static /public +0ms\n```\n","packages/koa/docs/koa-vs-express.md":"# Koa vs Express\n\nPhilosophically, Koa aims to \"fix and replace node\", whereas Express \"augments node\".\nKoa uses promises and async functions to rid apps of callback hell and simplify error handling.\nIt exposes its own `ctx.request` and `ctx.response` objects instead of node's `req` and `res` objects.\n\nExpress, on the other hand, augments node's `req` and `res` objects with additional properties and methods\nand includes many other \"framework\" features, such as routing and templating, which Koa does not.\n\nThus, Koa can be viewed as an abstraction of node.js's `http` modules, where as Express is an application framework for node.js.\n\n|           Feature | Koa | Express | Connect |\n| ----------------: | --- | ------- | ------- |\n| Middleware Kernel | ✓   | ✓       | ✓       |\n|           Routing |     | ✓       |         |\n|        Templating |     | ✓       |         |\n|     Sending Files |     | ✓       |         |\n|             JSONP |     | ✓       |         |\n\nThus, if you'd like to be closer to node.js and traditional node.js-style coding, you probably want to stick to Connect/Express or similar frameworks.\nIf you want to get rid of callbacks, use Koa.\n\nAs result of this different philosophy is that traditional node.js \"middleware\", i.e. functions of the form `(req, res, next)`, are incompatible with Koa. Your application will essentially have to be rewritten from the ground, up.\n\n## Does Koa replace Express?\n\nIt's more like Connect, but a lot of the Express goodies\nwere moved to the middleware level in Koa to help form\na stronger foundation. This makes middleware more enjoyable\nand less error-prone to write, for the entire stack, not\njust the end application code.\n\nTypically many middleware would\nre-implement similar features, or even worse incorrectly implement them,\nwhen features like signed cookie secrets among others are typically application-specific,\nnot middleware specific.\n\n## Does Koa replace Connect?\n\nNo, just a different take on similar functionality\nnow that generators allow us to write code with less\ncallbacks. Connect is equally capable, and some may still prefer it,\nit's up to what you prefer.\n\n## Why isn't Koa just Express 4.0?\n\nKoa is a pretty large departure from what people know about Express,\nthe design is fundamentally much different, so the migration from\nExpress 3.0 to this Express 4.0 would effectively mean rewriting\nthe entire application, so we thought it would be more appropriate\nto create a new library.\n\n## How is Koa different than Connect/Express?\n\n### Promises-based control flow\n\nNo callback hell.\n\nBetter error handling through try/catch.\n\nNo need for domains.\n\n### Koa is barebones\n\nUnlike both Connect and Express, Koa does not include any middleware.\n\nUnlike Express, routing is not provided.\n\nUnlike Express, many convenience utilities are not provided. For example, sending files.\n\nKoa is more modular.\n\n### Koa relies less on middleware\n\nFor example, instead of a \"body parsing\" middleware, you would instead use a body parsing function.\n\n### Koa abstracts node's request/response\n\nLess hackery.\n\nBetter user experience.\n\nProper stream handling.\n\n### Koa routing (third party libraries support)\n\nSince Express comes with its own routing, but Koa does not have\nany in-built routing, there are third party libraries available such as\nkoa-router and koa-route.\nSimilarly, just like we have helmet for security in Express, for Koa\nwe have koa-helmet available and the list goes on for Koa available third\nparty libraries.\n","packages/koa/docs/migration.md":"# Migrating from Koa v1.x to v2.x\n\n## New middleware signature\n\nKoa v2 introduces a new signature for middleware.\n\n**Old signature middleware (v1.x) support will be removed in v3**\n\nThe new middleware signature is:\n\n```js\n// uses async arrow functions\napp.use(async (ctx, next) => {\n  try {\n    await next(); // next is now a function\n  } catch (err) {\n    ctx.body = { message: err.message };\n    ctx.status = err.status || 500;\n  }\n});\n\napp.use(async (ctx) => {\n  const user = await User.getById(this.session.userid); // await instead of yield\n  ctx.body = user; // ctx instead of this\n});\n```\n\nYou don't have to use asynchronous functions - you just have to pass a function that returns a promise.\nA regular function that returns a promise works too!\n\nThe signature has changed to pass `Context` via an explicit parameter, `ctx` above, instead of via\n`this`. The context passing change makes Koa more compatible with es6 arrow functions, which capture `this`.\n\n## Using v1.x Middleware in v2.x\n\nKoa v2.x will try to convert legacy signature, generator middleware on `app.use`, using [koa-convert](https://github.com/koajs/convert).\nIt is however recommended that you choose to migrate all v1.x middleware as soon as possible.\n\n```js\n// Koa will convert\napp.use(function* (next) {\n  const start = Date.now();\n  yield next;\n  const ms = Date.now() - start;\n  console.log(`${this.method} ${this.url} - ${ms}ms`);\n});\n```\n\nYou could do it manually as well, in which case Koa will not convert.\n\n```js\nconst convert = require('koa-convert');\n\napp.use(\n  convert(function* (next) {\n    const start = Date.now();\n    yield next;\n    const ms = Date.now() - start;\n    console.log(`${this.method} ${this.url} - ${ms}ms`);\n  }),\n);\n```\n\n## Upgrading middleware\n\nYou will have to convert your generators to async functions with the new middleware signature:\n\n```js\napp.use(async (ctx, next) => {\n  const user = await Users.getById(this.session.user_id);\n  await next();\n  ctx.body = { message: 'some message' };\n});\n```\n\nUpgrading your middleware may require some work. One migration path is to update them one-by-one.\n\n1. Wrap all your current middleware in `koa-convert`\n2. Test\n3. `npm outdated` to see which Koa middleware is outdated\n4. Update one outdated middleware, remove using `koa-convert`\n5. Test\n6. Repeat steps 3-5 until you're done\n\n## Updating your code\n\nYou should start refactoring your code now to ease migrating to Koa v2:\n\n- Return promises everywhere!\n- Do not use `yield*`\n- Do not use `yield {}` or `yield []`.\n  - Convert `yield []` into `yield Promise.all([])`\n  - Convert `yield {}` into `yield Bluebird.props({})`\n\nYou could also refactor your logic outside of Koa middleware functions. Create functions like\n`function* someLogic(ctx) {}` and call it in your middleware as\n`const result = yield someLogic(this)`.\nNot using `this` will help migrations to the new middleware signature, which does not use `this`.\n\n## Application object constructor requires new\n\nIn v1.x, the Application constructor function could be called directly, without `new` to\ninstantiate an instance of an application. For example:\n\n```js\nvar koa = require('koa');\nvar app = (module.exports = koa());\n```\n\nv2.x uses es6 classes which require the `new` keyword to be used.\n\n```js\nvar koa = require('koa');\nvar app = (module.exports = new koa());\n```\n\n## ENV specific logging behavior removed\n\nAn explicit check for the `test` environment was removed from error handling.\n\n## Dependency changes\n\n- [co](https://github.com/tj/co) is no longer bundled with Koa. Require or import it directly.\n- [composition](https://github.com/thenables/composition) is no longer used and deprecated.\n\n## v1.x support\n\nThe v1.x branch is still supported but should not receive feature updates. Except for this migration\nguide, documentation will target the latest version.\n\n## Help out\n\nIf you encounter migration related issues not covered by this migration guide, please consider\nsubmitting a documentation pull request.\n","packages/koa/docs/troubleshooting.md":"# Troubleshooting Koa\n\n- [Whenever I try to access my route, it sends back a 404](#whenever-i-try-to-access-my-route-it-sends-back-a-404)\n- [My response or context changes have no effect](#my-response-or-context-changes-have-no-effect)\n- [My middleware is not called](#my-middleware-is-not-called)\n\nSee also [debugging Koa](guide.md#debugging-koa).\n\nIf you encounter a problem and later learn how to fix it, and think others might also encounter that problem, please\nconsider contributing to this documentation.\n\n## Whenever I try to access my route, it sends back a 404\n\nThis is a common but troublesome problem when working with Koa middleware. First, it is critical to understand when Koa generates a 404. Koa does not care which or how much middleware was run, in many cases a 200 and 404 trigger the same number of middleware. Instead, the default status for any response is 404. The most obvious way this is changed is through `ctx.status`. However, if `ctx.body` is set when the status has not been explicitly defined (through `ctx.status`), the status is set to 200. This explains why simply setting the body results in a 200. Once the middleware is done (when the middleware and any returned promises are complete), Koa sends out the response. After that, nothing can alter the response. If it was a 404 at the time, it will be a 404 at the end, even if `ctx.status` or `ctx.body` are set afterwords.\n\nEven though we now understand the basis of a 404, it might not be as clear why a 404 is generated in a specific case. This can be especially troublesome when it seems that `ctx.status` or `ctx.body` are set.\n\nThe unexpected 404 is a specific symptom of one of these more general problems:\n\n- [My response or context changes have no effect](#my-response-or-context-changes-have-no-effect)\n- [My middleware is not called](#my-middleware-is-not-called)\n\n## My response or context changes have no effect\n\nThis can be caused when the response is sent before the code making the change is\nexecuted. If the change is to the `ctx.body` or `ctx.status` setter, this can cause a 404 and\nis by far the most common cause of these problems.\n\n### Problematic code\n\n```js\nrouter.get('/fetch', function (ctx, next) {\n  models.Book.findById(parseInt(ctx.query.id)).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\nWhen used, this route will always send back a 404, even though `ctx.body` is set.\n\nThe same behavior would occur in this `async` version:\n\n```js\nrouter.get('/fetch', async (ctx, next) => {\n  models.Book.findById(parseInt(ctx.query.id)).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\n### Cause\n\n`ctx.body` is not set until _after_ the response has been sent. The code doesn't tell Koa to wait for the database to return the record. Koa sends the response after the middleware has been run, but not after the callback inside the middleware has been run. In the gap there, `ctx.body` has not yet been set, so Koa responds with a 404.\n\n### Identifying this as the issue\n\nAdding another piece of middleware and some logging can be extremely helpful in identifying this issue.\n\n```js\nrouter.use('/fetch', function (ctx, next) {\n  return next().then(function () {\n    console.log('Middleware done');\n  });\n});\n\nrouter.get('/fetch', function (ctx, next) {\n  models.Book.findById(parseInt(ctx.query.id)).then(function (book) {\n    ctx.body = book;\n    console.log('Body set');\n  });\n});\n```\n\nIf you see this in the logs:\n\n```\nMiddleware done\nBody set\n```\n\nIt means that the body is being set after the middleware is done, and after the response has been sent. If you see only one or none of these logs, proceed to [My middleware is not called](#my-middleware-is-not-called). If they are in the right order, make sure you haven't explicitly set the status to 404, make sure that it actually is a 404, and if that fails feel free to ask for help.\n\n### Solution\n\n```js\nrouter.get('/fetch', function (ctx, next) {\n  return models.Book.findById(parseInt(ctx.query.id)).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\nReturning the promise given by the database interface tells Koa to wait for the promise to finish before responding. At that time, the body will have been set. This results in Koa sending back a 200 with a proper response.\n\nThe fix in the `async` version is to add an `await` statement:\n\n```js\nrouter.get('/fetch', async (ctx, next) => {\n  await models.Book.findById(parseInt(ctx.query.id)).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\n## My middleware is not called\n\nThis can be due to an interrupted chain of middleware calls. This can cause a 404 if the\nmiddleware that is skipped is responsible for the `ctx.body` or `ctx.status` setter.\nThis is less common than [My response or context changes have no effect](#my-response-or-context-changes-have-no-effect),\nbut it can be a much bigger pain to troubleshoot.\n\n### Problematic code\n\n```js\nrouter.use(function (ctx, next) {\n  // Don't Repeat Yourself! Let's parse the ID here for all our middleware\n  if (ctx.query.id) {\n    ctx.state.id = parseInt(ctx.query.id);\n  }\n});\n\nrouter.get('/fetch', function (ctx, next) {\n  return models.Book.findById(ctx.state.id).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\n### Cause\n\nIn the code above, the book is never fetched from the database, and in fact our route was never called. Look closely at our helper middleware. We forgot to `return next()`! This causes the middleware flow to never reach our route, ending our \"helper\" middleware.\n\n### Identifying this as the issue\n\nIdentifying this problem is easier than most, add a log at the beginning of the route. If it doesn't trigger, your route was never reached in the middleware chain.\n\n```js\nrouter.use(function (ctx, next) {\n  // Don't Repeat Yourself! Let's parse the ID here for all our middleware\n  if (ctx.query.id) {\n    ctx.state.id = parseInt(ctx.query.id);\n  }\n});\n\nrouter.get('/fetch', function (ctx, next) {\n  console.log('Route called'); // Never happens\n  return models.Book.findById(ctx.state.id).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\nTo find the middleware causing the problem, try adding logging at various points in the middleware chain.\n\n### Solution\n\nThe solution for this is rather easy, simply add `return next()` to the end of your helper middleware.\n\n```js\nrouter.use(function (ctx, next) {\n  if (ctx.query.id) {\n    ctx.state.id = parseInt(ctx.query.id);\n  }\n  return next();\n});\n\nrouter.get('/fetch', function (ctx, next) {\n  return models.Book.findById(ctx.state.id).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n","packages/loader-fs/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Floader-fs%2FREADME.md -->","packages/path-matching/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fpath-matching%2FREADME.md -->","packages/router/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Frouter%2FREADME.md -->","packages/skills/AGENTS.md":"# Skills 编写与评测规范\n\n遵循[根目录 AGENTS.md](../../AGENTS.md) 中的共享仓库指引。以下补充 `packages/skills/` 的专属约定。\n\n`packages/skills/` 目录包含 AI agent skills — 纯 markdown 文档，指导 AI 助手使用 Egg 框架。以 `@eggjs/skills` npm 包发布，仅含 `.md` 文件。\n\n> **Skill 编写基础知识**：SKILL.md 格式、frontmatter 规范、目录结构、progressive disclosure、写作风格等通用知识请使用 `/skill-creator` skill 获取指导。以下仅记录 Egg 项目特有的约定。\n\n## Egg Skills 架构\n\nSkills 采用分层路由模式：\n\n- **入口 skill** (`egg/`) — 分析用户意图，通过关键词匹配和决策逻辑路由到专业 skill\n- **专业 skills** — 提供特定领域的深度指导：\n  - `egg-core/` — 核心概念：模块、依赖注入、生命周期、AccessLevel、后台任务\n  - `egg-controller/` — 实现指导：HTTPController、MCPController、Schedule、Ajv 校验\n\n## Egg Skill Frontmatter 约定\n\n- **`name`**：入口 skill 使用 `egg`，专业 skill 以 `egg-` 为前缀（如 `egg-controller`、`egg-core`）\n- **`allowed-tools`**：统一使用 `Read`（纯文档指导型，不修改文件）\n- **`description`**：\n  - 中文：以\"本技能用于...\"开头，包含触发关键词\n  - 英文：以\"Use when...\"开头，包含触发关键词\n\n## 专业 Skill 编写规范\n\n**入口 Skill（如 `egg/`）编写要点：**\n\n对应 /skill-creator 的 **Workflow-Based** 模式。\n\n1. 决策框架包含明确步骤：识别意图 → 检查模糊意图 → 协议/用例特定指示\n2. 为每个专业 skill 列出中英文触发关键词\n3. 冲突解决规则：明确意图模糊时的优先级（如\"基础优先\"——核心概念优先于控制器实现）\n4. 示例分析：多个完整示例，格式为 用户查询 → 分析 → 决策 → 响应策略\n5. 快速参考表：用户意图 → 关键词 → 推荐 skill 映射\n6. 交叉引用话术：回答完主问题后，附\"如需了解 X，请参阅 `@eggjs/skills-xxx`\"\n\n**专业 Skill 两种组织模式：**\n\n| 模式                               | /skill-creator 对应 | 适用场景           | SKILL.md 内容           | references/ 用途   |\n| ---------------------------------- | ------------------- | ------------------ | ----------------------- | ------------------ |\n| **概念型**（如 `egg-core/`）       | Reference-Based     | 概念解释、架构理解 | 自包含的深度内容        | 更深入的专题文档   |\n| **索引型**（如 `egg-controller/`） | Workflow-Based      | 多种实现方式的选择 | 精简的决策树 + 快速参考 | 每种实现的详细指南 |\n\n**概念型 Skill 内容结构：**\n\n1. 概述（一段话说明覆盖范围）\n2. 按概念分块，每个概念包含：定义 → 使用场景 → 装饰器/API 模式 → 代码示例\n3. 重要约束（反模式、限制）\n4. 快速决策指南表（场景 → 推荐方式）\n5. 最佳实践\n6. 参考资料链接\n\n**索引型 Skill 内容结构：**\n\n1. 决策树（根据需求选择实现方式）\n2. 每种类型的快速参考（装饰器、参数、特点）\n3. 最佳实践\n4. 参考资料链接\n\n## Reference 文档编写要点\n\nReference 文档（`references/*.md`）**不需要 YAML frontmatter**，只有 `SKILL.md` 才需要。\n\n**核心原则：Skill 不是文档的重新排版，而是填补\"文档到生产代码\"之间的缝隙。**\n\nSkill 的价值 = 文档 + 实践经验 - 重复内容。如果内容和 `site/docs/` 中的文档高度重复，说明 skill 写得不对。\n\n| 内容类型            | 该放文档（site/docs/） | 该放 Skill（packages/skills/） |\n| ------------------- | ---------------------- | ------------------------------ |\n| API 签名、参数说明  | Yes                    | No（引用文档即可）             |\n| 易错点 / 常见错误   | 部分                   | **Yes（重点）**                |\n| 完整端到端模式      | No                     | **Yes**                        |\n| 跨模块集成知识      | 散落各处               | **Yes（聚合）**                |\n| 文件放置 / 命名约定 | 部分                   | **Yes**                        |\n| 场景化决策树        | No                     | **Yes**                        |\n\n**编写 Reference 文档的具体步骤：**\n\n1. **先读对应的文档**（如 `site/docs/zh-CN/basics/mcpcontroller.md`），理解已有内容\n2. **向维护者提问，收集文档未覆盖的知识**，重点关注：\n   - 导入路径、命名约定等易错点（AI 最容易犯的错）\n   - 文件应该放在哪个目录？命名规则是什么？\n   - 和其他模块（Module、Service、DI）的集成关系\n   - 哪些配置参数实际开发中需要关心，哪些用默认值即可\n   - 哪些是内部扩展机制不需要暴露给应用开发者\n3. **以常见错误表开头** — 把 AI 最容易写错的地方放在最醒目的位置（如错误的导入路径、错误的 API 用法）\n4. **文件约定** — 目录结构、命名规则、配置文件位置，这些文档里通常不会详细说明\n5. **场景化决策树** — 从用户意图出发（\"让 AI 查数据\"），而非从 API 出发（\"@MCPTool\"）\n6. **端到端完整示例** — 从配置文件到控制器到 Service 到测试，展示所有相关文件和它们的关系\n7. **精简的装饰器对照表放末尾** — 仅作为速查，不展开 API 详解\n\n## 添加新 Skill\n\n1. 在 `packages/skills/` 下创建目录：`packages/skills/<skill-name>/`\n2. 创建 `SKILL.md`（格式规范参考 `/skill-creator`，frontmatter 遵循上述 Egg 约定）\n3. 创建 `references/` 目录（初始为空时放置 `.gitkeep`）\n4. 按需在 `references/*.md` 中添加详细参考文档\n5. 更新入口 skill（`egg/SKILL.md`）的路由逻辑以包含新 skill\n6. 如果 skill 涉及 controller 类型，同时更新 `egg-controller/SKILL.md` 决策树\n\n## 添加新 Reference 文档\n\n1. 在 skill 的 `references/` 目录中创建 `.md` 文件\n2. 遵循命名规范（kebab-case，描述性命名：`http-controller.md`、`mcp-controller.md`）\n3. 包含完整的代码示例和决策树\n4. 更新父级 `SKILL.md` 引用新文档\n5. 如果 `references/` 中已有文件，移除 `.gitkeep`\n\n## Skill 评测\n\n评测用例存放在 `packages/skills/eval/` 目录下，用于验证 AI 使用 skill 后的回答质量。\n\n**评测文件结构：**\n\n```text\npackages/skills/eval/\n├── evals-egg-core.json        # egg-core skill 评测用例\n├── evals-egg-controller.json  # egg-controller skill 评测用例\n├── evals-routing.json         # 入口路由评测用例\n├── .gitignore                 # 忽略 *-workspace/ 目录\n└── <skill-name>-workspace/    # 评测输出（gitignored），由 /skill-creator 管理\n    └── iteration-N/\n        ├── REPORT.md          # 对比评分报告\n        ├── GRADING.md         # with-skill 通过率报告\n        └── {prefix}-{id}/     # 每个用例一个目录\n            ├── eval_metadata.json\n            ├── with_skill/outputs/\n            └── without_skill/outputs/\n```\n\n**评测用例 JSON 格式：**\n\n```json\n{\n  \"skill_name\": \"egg-controller\",\n  \"description\": \"控制器评测：覆盖 http-controller、mcp-controller、schedule、ajv-validate\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"用户的任务描述\",\n      \"expected_output\": \"期望输出的关键要素描述\",\n      \"files\": [{ \"path\": \"相对路径\", \"content\": \"文件内容（可选，用于提供上下文或有 bug 的代码）\" }]\n    }\n  ]\n}\n```\n\n**评测流程：**\n\n使用 `/skill-creator` skill 运行评测和生成结果展示。评测流程概述：\n\n1. **编写评测用例** — 在对应的 `evals-*.json` 中添加用例\n2. **运行评测** — 通过 `/skill-creator` 为每个用例启动两个并行 subagent（with-skill 和 site-docs），使用下方 prompt 模板\n3. **评分和展示** — `/skill-creator` 负责评分、生成对比报告、启动可视化 viewer 供人工 review\n4. **改进 skill** — 根据评分结果和人工 feedback 改进 skill 内容，开启新的 iteration\n\n**评测对比的两组环境：**\n\n每个评测用例需要在两种环境下分别运行，对比 skill 是否有效。Prompt 中不应包含任何流程指引（如\"先判断使用哪个 skill\"），只提供参考资料和访问约束，让 AI 自然行动。\n\n| 环境           | system prompt                                                                         | 可访问范围（prompt 约束）  |\n| -------------- | ------------------------------------------------------------------------------------- | -------------------------- |\n| **with-skill** | `egg/SKILL.md`（入口 skill）的完整内容                                                | 仅 `packages/skills/` 目录 |\n| **site-docs**  | 角色声明 + `site/docs/` 的完整文件目录列表（通过 `find site/docs -name '*.md'` 生成） | 仅 `site/docs/` 目录       |\n\n**Prompt 模板：**\n\nwith-skill 环境：\n\n```text\n你是 EGG 框架开发专家。你只能通过 Read 工具读取 packages/skills/ 目录下的文件，不能访问 site/docs/ 或项目源码。\n\n{egg/SKILL.md 的完整内容}\n\n---\n{eval prompt}\n```\n\nsite-docs 环境：\n\n```text\n你是 EGG 框架开发专家。你只能通过 Read 工具读取 site/docs/ 目录下的文件，不能访问 packages/skills/ 或项目源码。项目文档目录如下：\n\n{完整的 site/docs/ 文件列表，通过 find site/docs -name '*.md' | sort 生成}\n\n---\n{eval prompt}\n```\n\n两组 prompt 的差异仅在于参考资料不同，不包含额外的流程提示。subagent 均具备 Read 工具权限。访问范围通过 prompt 约束（软限制，非技术硬限制）。\n\n两组使用相同的 eval prompt，对比输出质量差异。如果 with-skill 没有明显优于 site-docs，说明 skill 内容需要改进——要么缺少文档未覆盖的知识，要么存在与文档的不必要重复。\n\n**输出目录：**\n\n评测结果保存到 `packages/skills/eval/<skill-name>-workspace/iteration-N/` 目录下（已在 `.gitignore` 中通过 `*-workspace/` 忽略），具体目录结构由 `/skill-creator` 管理。\n\n**评测用例设计原则：**\n\n每个 reference 文档至少覆盖 5 个以上评测用例，需覆盖以下场景类型：\n\n| 场景类型             | 说明                                    | 示例                                                     |\n| -------------------- | --------------------------------------- | -------------------------------------------------------- |\n| **泛化需求描述**     | 不了解框架术语，用口语化描述需求        | \"帮我加个参数校验\"                                       |\n| **精确需求描述**     | 明确指定技术方案和约束                  | \"用 TypeBox 定义 Schema，email 用 format: email\"         |\n| **使用咨询**         | 询问用法、区别、选型                    | \"Optional 和 Null 有什么区别\"                            |\n| **问题排查**         | 提供有 bug 的代码（附 files），要求诊断 | \"校验跑不起来，帮我看看\"                                 |\n| **新项目代码生成**   | 在全新项目中从零开始生成功能代码        | \"帮我写一个创建订单的接口，需要做参数校验\"               |\n| **存量项目代码生成** | 在包含老 egg 代码的项目中生成或迁移代码 | \"帮我把这个老的 egg controller 改成 HTTPController 写法\" |\n| **不常用 API**       | 需要查外部文档链接才能回答              | \"用 Tuple 定义元组校验\"                                  |\n","packages/skills/PLAN.md":"# Skills 评测方案设计\n\n## 目标\n\n为 `packages/skills/` 设计一套评测体系，覆盖两个层面：\n\n1. **静态校验** — 验证 Skill 文件的结构正确性、引用完整性\n2. **动态评测** — 用 LLM-as-Judge 评估 AI 基于 Skill 生成回答的质量\n\n全部手动触发运行，不集成 CI。\n\n---\n\n## 目录结构\n\n```\npackages/skills/\n├── egg/\n├── controller/\n├── tegg-core/\n├── eval/                          # 新增：评测目录\n│   ├── static/\n│   │   └── validate.test.ts       # 静态校验测试\n│   ├── dynamic/\n│   │   ├── routing.eval.ts        # 入口路由评测\n│   │   └── quality.eval.ts        # 内容质量评测\n│   ├── fixtures/\n│   │   ├── routing-cases.ts       # 路由测试用例\n│   │   └── quality-cases.ts       # 质量测试用例\n│   └── lib/\n│       ├── skill-loader.ts        # Skill 文件加载器\n│       ├── judge.ts               # LLM-as-Judge 核心逻辑\n│       └── types.ts               # 共享类型定义\n├── vitest.config.ts\n├── package.json\n└── tsconfig.json\n```\n\n---\n\n## 第一部分：静态校验\n\n### 校验项\n\n| 校验项               | 说明                                                          |\n| -------------------- | ------------------------------------------------------------- |\n| **Frontmatter 格式** | 每个 SKILL.md 必须包含 `name`、`description`、`allowed-tools` |\n| **引用文件存在性**   | SKILL.md 中提到的 `references/*.md` 文件必须存在              |\n| **交叉引用一致性**   | 入口 skill 提到的子 skill 目录必须存在且包含 SKILL.md         |\n| **Markdown 结构**    | 标题层级合理（以 `# ` 开头，不跳级）                          |\n| **决策表完整性**     | 入口 skill 的路由表中每个 skill 都有对应目录                  |\n\n### 实现方式\n\n使用 vitest + node:assert 编写测试，通过 Node.js fs API 读取文件并解析：\n\n```typescript\n// eval/static/validate.test.ts\nimport { describe, it } from 'vitest';\nimport assert from 'node:assert/strict';\nimport { loadAllSkills } from '../lib/skill-loader.ts';\n\ndescribe('Skill 静态校验', () => {\n  describe('Frontmatter', () => {\n    it('每个 SKILL.md 包含必填字段: name, description, allowed-tools', ...);\n  });\n\n  describe('引用完整性', () => {\n    it('SKILL.md 中引用的 references/ 文件均存在', ...);\n    it('入口 skill 引用的子 skill 目录均存在', ...);\n  });\n\n  describe('Markdown 结构', () => {\n    it('标题层级不跳级', ...);\n  });\n});\n```\n\n---\n\n## 第二部分：动态评测（LLM-as-Judge）\n\n### 评测维度\n\n动态评测分为两个子场景：\n\n#### 2.1 路由评测 — 入口 Skill 是否正确路由\n\n测试 `egg/SKILL.md` 的决策逻辑：给定用户查询，判断 AI 是否路由到正确的子 skill。\n\n**测试用例结构：**\n\n```typescript\n// eval/fixtures/routing-cases.ts\nexport const routingCases: RoutingCase[] = [\n  {\n    query: '如何创建 HTTP controller？',\n    expectedSkill: 'controller',\n    reason: '明确提到 controller，属于协议实现',\n  },\n  {\n    query: '@SingletonProto 和 @ContextProto 有什么区别？',\n    expectedSkill: 'tegg-core',\n    reason: '关于对象生命周期，属于核心概念',\n  },\n  {\n    query: '我需要创建一个可以被 HTTP 控制器使用的服务',\n    expectedSkill: 'tegg-core',\n    reason: '模糊意图，按规则 1（基础优先）应路由到 core',\n  },\n  // ... 更多用例\n];\n```\n\n**测试实现：**\n\n```typescript\n// eval/dynamic/routing.eval.ts\nimport { describe, it } from 'vitest';\nimport assert from 'node:assert/strict';\nimport Anthropic from '@anthropic-ai/sdk';\nimport { loadSkillContent } from '../lib/skill-loader.ts';\nimport { routingCases } from '../fixtures/routing-cases.ts';\n\nconst client = new Anthropic();\nconst AVAILABLE_SKILLS = ['controller', 'tegg-core'];\n\ndescribe('路由评测', () => {\n  // 加载入口 skill 作为 system prompt\n  const entrySkillContent = loadSkillContent('egg');\n\n  for (const { query, expectedSkill, reason } of routingCases) {\n    it(`\"${query}\" → ${expectedSkill}`, async () => {\n      // 1. 将 SKILL.md 作为 system prompt，发送用户查询\n      const response = await client.messages.create({\n        model: 'claude-sonnet-4-20250514',\n        max_tokens: 1024,\n        system: [\n          entrySkillContent,\n          // 约束输出格式，让 AI 只做路由决策\n          `你是 EGG 框架技能路由器。根据上面的决策指南，分析用户查询并选择应该加载的技能。`,\n          `可选技能: ${AVAILABLE_SKILLS.join(', ')}`,\n          `只输出 JSON: {\"skill\": \"<技能名>\", \"reason\": \"<简要理由>\"}`,\n        ].join('\\n\\n'),\n        messages: [{ role: 'user', content: query }],\n      });\n\n      // 2. 解析 AI 回答中的路由选择\n      const text = response.content[0].type === 'text' ? response.content[0].text : '';\n      const parsed = JSON.parse(text);\n\n      // 3. 断言路由正确性\n      assert.equal(\n        parsed.skill,\n        expectedSkill,\n        `路由错误: 期望 \"${expectedSkill}\" 但得到 \"${parsed.skill}\"` +\n          `\\n  用例理由: ${reason}` +\n          `\\n  AI 理由: ${parsed.reason}`,\n      );\n    });\n  }\n});\n```\n\n#### 2.2 内容质量评测 — 子 Skill 回答质量\n\n测试各子 skill 对领域问题的回答质量。\n\n**测试用例结构：**\n\n```typescript\n// eval/fixtures/quality-cases.ts\nexport const qualityCases: QualityCase[] = [\n  {\n    skill: 'controller',\n    query: '如何创建一个 POST 接口接收 JSON body？',\n    criteria: [\n      '使用 @HTTPController 装饰器',\n      '使用 @HTTPMethod 且 method 为 POST',\n      '使用 @HTTPBody() 获取请求体',\n      '包含完整可运行的代码示例',\n    ],\n    references: ['references/http-controller.md'], // 需要加载的参考文档\n  },\n  {\n    skill: 'tegg-core',\n    query: '如何让一个服务可以被其他模块访问？',\n    criteria: ['提到 AccessLevel.PUBLIC', '使用 @SingletonProto 装饰器', '解释跨模块访问机制'],\n    references: [],\n  },\n  // ... 更多用例\n];\n```\n\n**测试实现：**\n\n```typescript\n// eval/dynamic/quality.eval.ts\nimport { describe, it } from 'vitest';\nimport assert from 'node:assert/strict';\nimport Anthropic from '@anthropic-ai/sdk';\nimport { loadSkillContent, loadReference } from '../lib/skill-loader.ts';\nimport { qualityCases } from '../fixtures/quality-cases.ts';\nimport { judge } from '../lib/judge.ts';\n\nconst client = new Anthropic();\n\ndescribe('内容质量评测', () => {\n  for (const testCase of qualityCases) {\n    describe(`[${testCase.skill}] ${testCase.query}`, () => {\n      let aiResponse: string;\n\n      // Step 1: 加载 skill 内容作为 system prompt，向被测 LLM 提问\n      it('生成回答', async () => {\n        const skillContent = loadSkillContent(testCase.skill);\n        const refContents = testCase.references.map((ref) => loadReference(testCase.skill, ref));\n\n        const systemPrompt = [skillContent, ...refContents].join('\\n\\n---\\n\\n');\n\n        const response = await client.messages.create({\n          model: 'claude-sonnet-4-20250514',\n          max_tokens: 2048,\n          system: systemPrompt,\n          messages: [{ role: 'user', content: testCase.query }],\n        });\n\n        aiResponse = response.content[0].type === 'text' ? response.content[0].text : '';\n        assert.ok(aiResponse.length > 0, 'AI 应该返回非空回答');\n      });\n\n      // Step 2: 用 Judge LLM 对回答逐项评分\n      it('通过质量评审', async () => {\n        const result = await judge(client, {\n          query: testCase.query,\n          response: aiResponse,\n          criteria: testCase.criteria,\n        });\n\n        // 输出详细评分到 console 供人工查看\n        console.log(`    得分: ${result.totalScore} (${result.passed}/${result.total})`);\n        for (const item of result.details) {\n          const icon = item.score === 1 ? '✓' : '✗';\n          console.log(`      ${icon} ${item.criterion}: ${item.reason}`);\n        }\n\n        // 断言：所有 criteria 都应满足\n        assert.ok(\n          result.totalScore >= 0.8,\n          `质量不达标: ${result.totalScore} < 0.8\\n` +\n            result.details\n              .filter((d) => d.score === 0)\n              .map((d) => `  ✗ ${d.criterion}: ${d.reason}`)\n              .join('\\n'),\n        );\n      });\n    });\n  }\n});\n```\n\n### LLM-as-Judge 实现\n\n```typescript\n// eval/lib/judge.ts\nimport type Anthropic from '@anthropic-ai/sdk';\nimport type { JudgeInput, JudgeResult, JudgeDetail } from './types.ts';\n\nexport async function judge(client: Anthropic, input: JudgeInput): Promise<JudgeResult> {\n  const criteriaList = input.criteria.map((c, i) => `${i + 1}. ${c}`).join('\\n');\n\n  const response = await client.messages.create({\n    model: 'claude-sonnet-4-20250514',\n    max_tokens: 1024,\n    system: '你是 AI 回答质量评估专家。严格按照 JSON 格式输出评分结果。',\n    messages: [\n      {\n        role: 'user',\n        content: `请根据评分标准，对以下 AI 回答逐项评分。\n\n## 评分标准\n${criteriaList}\n\n## 用户问题\n${input.query}\n\n## AI 回答\n${input.response}\n\n## 输出格式（严格 JSON）\n{\n  \"details\": [\n    { \"criterion\": \"标准内容\", \"score\": 0 或 1, \"reason\": \"简要理由\" }\n  ]\n}`,\n      },\n    ],\n  });\n\n  const text = response.content[0].type === 'text' ? response.content[0].text : '';\n  const parsed = JSON.parse(text);\n  const details: JudgeDetail[] = parsed.details;\n  const passed = details.filter((d) => d.score === 1).length;\n\n  return {\n    details,\n    passed,\n    total: details.length,\n    totalScore: passed / details.length,\n  };\n}\n```\n\n### 评测报告\n\n运行评测后生成 JSON 报告：\n\n```json\n{\n  \"timestamp\": \"2026-02-05T10:00:00Z\",\n  \"routing\": {\n    \"total\": 10,\n    \"correct\": 9,\n    \"accuracy\": 0.9,\n    \"failures\": [\n      {\n        \"query\": \"...\",\n        \"expected\": \"tegg-core\",\n        \"actual\": \"controller\",\n        \"reason\": \"...\"\n      }\n    ]\n  },\n  \"quality\": {\n    \"controller\": {\n      \"cases\": 5,\n      \"avg_score\": 0.85,\n      \"details\": [...]\n    },\n    \"tegg-core\": {\n      \"cases\": 5,\n      \"avg_score\": 0.90,\n      \"details\": [...]\n    }\n  }\n}\n```\n\n---\n\n## 第三部分：技术选型与依赖\n\n| 组件                  | 选型               | 理由                           |\n| --------------------- | ------------------ | ------------------------------ |\n| 测试框架              | vitest             | 遵循 monorepo 标准             |\n| 断言库                | node:assert/strict | Node.js 内置，零依赖           |\n| YAML frontmatter 解析 | gray-matter        | 成熟的 frontmatter 解析库      |\n| LLM 调用              | @anthropic-ai/sdk  | 使用 Claude API 做评测和 Judge |\n| 报告输出              | JSON 文件          | 简单可读，方便后续扩展为可视化 |\n\n### package.json scripts\n\n```json\n{\n  \"scripts\": {\n    \"test\": \"vitest run --config vitest.config.ts eval/static/\",\n    \"eval\": \"vitest run --config vitest.config.ts eval/dynamic/\",\n    \"eval:routing\": \"vitest run --config vitest.config.ts eval/dynamic/routing.eval.ts\",\n    \"eval:quality\": \"vitest run --config vitest.config.ts eval/dynamic/quality.eval.ts\"\n  }\n}\n```\n\n- `test` — 运行静态校验（快速，无 API 调用）\n- `eval` — 运行全部动态评测\n- `eval:routing` — 仅运行路由评测\n- `eval:quality` — 仅运行内容质量评测\n\n动态评测需设置 `ANTHROPIC_API_KEY` 环境变量。\n\n---\n\n## 实施步骤\n\n1. 在 worktree (`egg-skills-eval`) 中添加依赖：vitest、gray-matter、@anthropic-ai/sdk\n2. 添加 `vitest.config.ts` 和更新 `package.json` scripts\n3. 创建 `eval/lib/` 基础工具：skill-loader、types、judge\n4. 实现 `eval/static/validate.test.ts` 静态校验\n5. 编写路由测试用例 `eval/fixtures/routing-cases.ts`\n6. 实现 `eval/dynamic/routing.eval.ts` 路由评测\n7. 编写质量测试用例 `eval/fixtures/quality-cases.ts`\n8. 实现 `eval/dynamic/quality.eval.ts` 内容质量评测\n","packages/skills/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fskills%2FREADME.md -->","packages/skills/egg-controller/SKILL.md":"---\nname: egg-controller\ndescription: Use when creating API endpoints, implementing protocol handlers, exposing interfaces for specific clients, adding parameter validation, or applying middleware to controllers. Covers HTTP, MCP and Schedule controllers, Ajv/TypeBox parameter validation, and Middleware (function-style and AOP) for EGG framework applications.\nallowed-tools: Read\n---\n\n# EGG 控制器\n\n---\n\n## 控制器选择决策树\n\n```\n需要暴露什么接口/客户端协议？\n\n1. HTTP 接口？例如 HTML/JSON/SSR/SSE，可以使用 HTTPController，参考 `references/http-controller.md`\n\n2. 定时任务，可以使用 Schedule，参考 `references/schedule.md`\n\n3. MCP 接口，可以使用 MCPController，参考 `references/mcp-controller.md`\n\n需要做参数校验？\n\n4. 使用 Ajv + TypeBox 做入参校验，参考 `references/ajv-validate.md`\n\n需要给控制器加中间件（日志、鉴权、耗时统计等横切逻辑）？\n\n5. 使用 Middleware，支持函数式写法和 AOP 写法，参考 `references/middleware.md`\n```\n\n---\n\n## 控制器快速参考\n\n### HTTPController\n\n- **装饰器**：`@HTTPController`、`@HTTPMethod`\n- **参数**：`@HTTPParam`、`@HTTPQuery`、`@HTTPBody`、`@HTTPHeaders`、`@Cookies`、`@Request`、`@Context`\n- **详细文档**：`references/http-controller.md`\n\n### MCPController\n\n- **装饰器**：`@MCPController`、`@MCPTool`、`@MCPPrompt`、`@MCPResource`\n- **特点**：集成 LLM、Zod 验证、登录态支持\n- **详细文档**：`references/mcp-controller.md`\n\n### Schedule\n\n- **装饰器**：`@Schedule<T>`\n- **模式**：Worker/All\n- **详细文档**：`references/schedule.md`\n\n### Ajv 参数校验\n\n- **导入**：`import { Ajv, Type, Static } from 'egg/ajv'`\n- **方式**：`@Inject() ajv: Ajv`，调用 `ajv.validate(schema, data)`\n- **特点**：TypeBox 定义一次 Schema，同时获得校验和 TypeScript 类型\n- **详细文档**：`references/ajv-validate.md`\n\n### Middleware 中间件\n\n- **导入**：`import { Middleware } from 'egg'`（AOP 类从 `import { Advice } from 'egg/aop'`）\n- **写法**：函数式（Koa 中间件函数）和 AOP（@Advice 类），通过 `@Middleware()` 应用\n- **特点**：支持类级别和方法级别，函数式和 AOP 不能在同一个 `@Middleware()` 中混用\n- **详细文档**：`references/middleware.md`\n\n---\n\n## 常见问题排查\n\n| 现象                   | 原因                                     | 解决方案                                                           |\n| ---------------------- | ---------------------------------------- | ------------------------------------------------------------------ |\n| MCP 装饰器 import 报错 | 从 `'egg'` 导入                          | MCP 装饰器从 `'@eggjs/tegg'` 导入，zod 从 `'@eggjs/tegg/zod'` 导入 |\n| MCP Schema 报错        | 用了 `z.object()` 包装                   | 直接用普通对象 `{ name: z.string() }`                              |\n| 定时任务不生效         | 放在 `app/schedule` 目录                 | 放在模块目录中，避免和 egg 默认扫描冲突                            |\n| Ajv 校验 import 报错   | 从 `typebox` 或 `ajv` 导入               | 统一从 `'egg/ajv'` 导入 Type、Static、Ajv 等                       |\n| type 推导不完整        | 用 `type` 定义                           | 用 `interface Foo extends Static<typeof Schema> {}` 代替           |\n| Middleware 混用报错    | 函数和 Advice 类放同一个 `@Middleware()` | 分开写多个 `@Middleware()`，每个内部类型一致                       |\n| AOP Middleware 不生效  | Advice 类没加 `@Advice()` 装饰器         | 必须同时有 `@Advice()` 装饰器才能被识别为 AOP                      |\n\n---\n\n## 最佳实践\n\n- **控制器精简**：业务逻辑委托给 Service 层\n- **参数验证**：使用装饰器和类型定义\n- **错误处理**：根据协议转换错误和响应码\n- **RESTful 设计**：遵循 HTTP 方法和资源命名\n- **响应一致性**：统一响应格式\n\n---\n\n## 参考资料\n\n详细的控制器开发文档：\n\n- `references/http-controller.md` - HTTP 接口完整指南\n- `references/mcp-controller.md` - MCP 接口开发\n- `references/schedule.md` - 定时任务\n- `references/ajv-validate.md` - Ajv 参数校验\n- `references/middleware.md` - Middleware 中间件（函数式 + AOP）\n\n核心概念（`egg-core` skill）：模块、依赖注入、对象生命周期\n\n单元测试（`egg-unittest` skill）：HTTP 接口测试、Service 测试、Mock\n","packages/skills/egg-controller/references/ajv-validate.md":"# Ajv 参数校验指南\n\n## 常见错误\n\n| 错误写法                         | 正确写法                          | 说明                                           |\n| -------------------------------- | --------------------------------- | ---------------------------------------------- |\n| `import { Type } from 'typebox'` | `import { Type } from 'egg/ajv'`  | 必须从 `egg/ajv` 导入，内部已封装 typebox      |\n| `import { Ajv } from 'ajv'`      | `import { Ajv } from 'egg/ajv'`   | Ajv 实例通过 `egg/ajv` 导出                    |\n| `new Ajv()` 手动创建实例         | `@Inject() ajv: Ajv` 注入全局单例 | 框架已配置好 formats 和 keywords，不要自行创建 |\n| 在 Service 中做参数校验          | 在 Controller 中做参数校验        | 入参校验应在 Controller 层完成                 |\n\n---\n\n## 核心概念\n\nEgg 通过 `@eggjs/typebox-validate` 插件集成 Ajv（JSON Schema 校验库）和 TypeBox（TypeScript-first 的 JSON Schema 构建器）。**定义一次 Schema，同时获得参数校验和 TypeScript 类型推导。**\n\n### 导入路径\n\n```typescript\n// 所有 Ajv/TypeBox 相关导入统一从 egg/ajv\nimport { Ajv, Type, Static, TransformEnum } from 'egg/ajv';\n```\n\n---\n\n## 使用方式\n\n通过 `@Inject()` 注入全局 Ajv 单例，在 Controller 方法中调用 `ajv.validate()` 进行校验。\n\n### 完整示例\n\n```typescript\n// app/userModule/UserController.ts\nimport { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPBody, Inject } from 'egg';\nimport { Ajv, Type, Static, TransformEnum } from 'egg/ajv';\n\n// 1. 定义 Schema\nconst CreateUserSchema = Type.Object({\n  name: Type.String({\n    transform: [TransformEnum.trim],\n    minLength: 1,\n    maxLength: 50,\n  }),\n  email: Type.String({ format: 'email' }),\n  age: Type.Optional(Type.Integer({ minimum: 0, maximum: 150 })),\n});\n\n// 2. 从 Schema 推导类型\ntype CreateUserParams = Static<typeof CreateUserSchema>;\n\n// 3. 在 Controller 中注入 Ajv 并校验\n@HTTPController()\nexport class UserController {\n  @Inject()\n  private readonly ajv: Ajv;\n\n  @HTTPMethod({\n    method: HTTPMethodEnum.POST,\n    path: '/api/users',\n  })\n  async create(@HTTPBody() body: CreateUserParams) {\n    // 校验失败自动抛出 AjvInvalidParamError\n    this.ajv.validate(CreateUserSchema, body);\n\n    // 校验通过，body 已经有完整类型提示\n    return { name: body.name, email: body.email };\n  }\n}\n```\n\n---\n\n## Schema 定义\n\n### 常用类型\n\n```typescript\nimport { Type } from 'egg/ajv';\n\nType.String(); // string\nType.Number(); // number\nType.Integer(); // 整数\nType.Boolean(); // boolean\nType.Optional(Type.String()); // string | undefined\nType.Array(Type.String()); // string[]\nType.Object({ name: Type.String() }); // { name: string }\nType.Union([Type.String(), Type.Number()]); // string | number\nType.Literal('admin'); // 'admin'\n```\n\n完整的 TypeBox JSON Schema 类型定义参考：https://github.com/sinclairzx81/typebox#json-types\n\n### 内置 format 校验\n\n框架通过 `ajv-formats` 预注册了以下格式：\n\n| format      | 说明       | 示例                                   |\n| ----------- | ---------- | -------------------------------------- |\n| `email`     | 邮箱       | `user@example.com`                     |\n| `uri`       | URI        | `https://example.com`                  |\n| `uuid`      | UUID       | `550e8400-e29b-41d4-a716-446655440000` |\n| `date`      | 日期       | `2024-01-01`                           |\n| `date-time` | 日期时间   | `2024-01-01T00:00:00Z`                 |\n| `time`      | 时间       | `12:00:00`                             |\n| `ipv4`      | IPv4       | `192.168.1.1`                          |\n| `ipv6`      | IPv6       | `::1`                                  |\n| `hostname`  | 主机名     | `example.com`                          |\n| `regex`     | 正则表达式 | `^\\\\d+$`                               |\n\n```typescript\nType.String({ format: 'email' });\nType.String({ format: 'uuid' });\nType.String({ format: 'date-time' });\n```\n\n### transform 预处理\n\n通过 `ajv-keywords` 的 `transform` 关键字，在校验前对字符串做预处理：\n\n```typescript\nimport { TransformEnum } from 'egg/ajv';\n\nType.String({\n  transform: [TransformEnum.trim], // 去除首尾空格\n});\n\nType.String({\n  transform: [TransformEnum.trim, TransformEnum.toLowerCase], // 去空格 + 转小写\n});\n```\n\n---\n\n## 最佳实践\n\n- **Schema 与 Controller 同文件** — Schema 定义放在使用它的 Controller 文件中，保持就近原则\n- **校验在 Controller 层** — 不要在 Service 层做入参校验，Service 信任上层传入的数据\n- **善用 Optional** — 非必填字段用 `Type.Optional()` 包装，避免前端遗漏字段导致校验失败\n- **善用 transform** — 对用户输入做 trim 预处理，减少脏数据\n","packages/skills/egg-controller/references/http-controller.md":"# HTTPController 开发指南\n\n## 快速开始\n\n### 创建基本 HTTP 接口\n\n使用 `@HTTPController` 和 `@HTTPMethod` 装饰器创建 HTTP 接口：\n\n```typescript\nimport { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPParam } from 'egg';\n\n@HTTPController()\nexport class DemoController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/hello/:name' })\n  async hello(@HTTPParam() name: string) {\n    return { message: 'hello ' + name };\n  }\n}\n```\n\n### 设置路径前缀\n\n为控制器设置统一路径前缀：\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class PathController {\n  // 最终路径: GET /api/hello\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: 'hello' })\n  async hello() {}\n\n  // 最终路径: POST /api/create\n  @HTTPMethod({ method: HTTPMethodEnum.POST, path: 'create' })\n  async create() {}\n}\n```\n\n---\n\n## 参数装饰器决策\n\n### 决策树：选择合适的参数装饰器\n\n```\n需要从 HTTP 请求获取什么信息？\n\n├─ URL 路径参数\n│  └─ → @HTTPParam()\n│\n├─ URL 查询参数\n│  ├─ 单个值 → @HTTPQuery()\n│  └─ 多个值（数组） → @HTTPQueries()\n│\n├─ 请求体（POST/PUT）\n│  └─ → @HTTPBody()\n│     ├─ json → 对象\n│     ├─ text → 字符串\n│\n├─ 请求头\n│  └─ → @HTTPHeaders()\n│     └─ 注意：key 自动转小写\n│\n├─ Cookie\n│  └─ → @Cookies()\n│\n├─ 原始 HTTP 请求对象\n│  └─ → @Request()\n│     └─ 注意：不要和 @HTTPBody 一起消费请求体\n│\n└─ Egg Context（框架功能）\n   └─ → @Context()\n```\n\n### 参考对照表（参数装饰器）\n\n| 装饰器           | 获取内容         | 类型                  | 默认值 | 支持选项            |\n| ---------------- | ---------------- | --------------------- | ------ | ------------------- |\n| `@HTTPParam()`   | URL 路径参数     | `string`              | 变量名 | `{ name?: string }` |\n| `@HTTPQuery()`   | 查询参数（单个） | `string`              | 变量名 | `{ name?: string }` |\n| `@HTTPQueries()` | 查询参数（多个） | `string[]`            | 变量名 | `{ name?: string }` |\n| `@HTTPBody()`    | 请求体           | `object \\| string`    | -      | -                   |\n| `@HTTPHeaders()` | 请求头           | `IncomingHttpHeaders` | -      | -                   |\n| `@Cookies()`     | Cookie           | `HTTPCookies`         | -      | -                   |\n| `@Request()`     | HTTP 请求对象    | `HTTPRequest`         | -      | -                   |\n| `@Context()`     | Egg Context      | `EggContext`          | -      | -                   |\n\n### 快速选择指南\n\n**需要从 URL 获取参数（id）** → `@HTTPParam`\n**需要从查询字符串获取参数（category=books）** → `@HTTPQuery`\n**需要从查询字符串获取全部值（tag=tech&tag=dev）** → `@HTTPQueries`\n**需要获取请求体（POST JSON）** → `@HTTPBody`\n**需要获取请求头字段** → `@HTTPHeaders`（注意：使用小写key）\n**需要读取 Cookie** → `@Cookies`\n**需要访问原始请求对象** → `@Request`\n\n---\n\n## HTTP 响应\n\n### JSON 响应（默认）\n\n直接返回对象，框架自动序列化为 JSON：\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class JsonController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/data' })\n  async getData() {\n    return { result: 'hello world' };\n  }\n}\n```\n\n### 自定义响应\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class ResponseController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/custom' })\n  async customResponse(@Context() ctx: EggContext) {\n    ctx.status = 201;\n    ctx.set('X-Custom', 'value');\n    ctx.type = 'json';\n    return { message: 'Created' };\n  }\n}\n```\n\n---\n\n## 服务端渲染（SSR）\n\n```typescript\n@HTTPController({ path: '/' })\nexport class SSRController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })\n  async render(@Context() ctx: EggContext) {\n    ctx.type = 'html';\n    return `\n      <!DOCTYPE html>\n      <html>\n        <head><title>Home</title></head>\n        <body><h1>Hello World</h1></body>\n      </html>\n    `;\n  }\n}\n```\n\n---\n\n## 流式响应（Streaming）\n\n```typescript\nimport { Readable } from 'node:stream';\nimport { setTimeout } from 'node:timers/promises';\n\nasync function* generateHtml() {\n  yield '<html><body>';\n  for (let i = 1; i <= 5; i++) {\n    yield `<p>Chunk ${i}</p>`;\n    await setTimeout(1000);\n  }\n  yield '</body></html>';\n}\n\n@HTTPController({ path: '/api' })\nexport class StreamController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/stream' })\n  async streamHtml(@Context() ctx: EggContext) {\n    ctx.type = 'html';\n    return Readable.from(generateHtml());\n  }\n}\n```\n\n---\n\n## 路由优先级管理\n\n### 默认优先级规则\n\n| Path                            | RegExp Index | Priority | 说明             |\n| ------------------------------- | ------------ | -------- | ---------------- |\n| `/*`                            | `[0]`        | 0        | 通配符，最低     |\n| `/hello/:name`                  | `[1]`        | 1000     | 单参数           |\n| `/hello/world/message/:message` | `[3]`        | 3000     | 三参数           |\n| `/hello/:name/message/:message` | `[1, 3]`     | 4000     | 多参数，索引更大 |\n| `/hello/world`                  | `[]`         | 100000   | 静态路径，最高   |\n\n### 手动设置优先级\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class PriorityController {\n  @HTTPMethod({\n    method: HTTPMethodEnum.GET,\n    path: '/(api|openapi)/version',\n    priority: 100000, // 提升优先级\n  })\n  async high() {}\n\n  @HTTPMethod({\n    method: HTTPMethodEnum.POST,\n    path: '/(api|openapi)/(.+)',\n  })\n  async low() {}\n}\n```\n\n---\n\n## 参数装饰器详解\n\n### @HTTPParam\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：从 URL 路径中提取参数\n\n**语法**：`@HTTPParam(param?: HTTPParamParams)`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api/users' })\nexport class UserController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: ':userId/posts/:postId' })\n  async getPost(@HTTPParam() userId: string, @HTTPParam() postId: string) {\n    return { userId, postId };\n  }\n}\n```\n\n#### 使用要点\n\n- 参数类型必须是 `string`\n- 参数名默认和变量名相同\n- 支持正则表达式捕获：`path: '/files/(.*)'`\n- 使用 `{ name: '0' }` 获取正则第一个匹配\n- 支持多参数路径\n\n#### 示例\n\n```typescript\n// 路径参数\n@HTTPController({ path: '/api/users' })\nexport class UserController {\n  // GET /users/:id\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: ':id' })\n  async getUser(@HTTPParam() id: string) {\n    return { userId: id };\n  }\n\n  // GET /users/:userId/posts/:postId\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: ':userId/posts/:postId' })\n  async getPost(@HTTPParam() userId: string, @HTTPParam() postId: string) {\n    return { userId, postId };\n  }\n\n  // 获取正则匹配\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/files/(.*)' })\n  async getFile(@HTTPParam({ name: '0' }) path: string) {\n    return { path };\n  }\n}\n```\n\n---\n\n### @HTTPQuery\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：从 URL 查询字符串提取参数（`?key=value`）\n\n**语法**：\n\n- `@HTTPQuery(param?: HTTPQueryParams)` - 返回首个匹配的值（`string`）\n- `@HTTPQueries(param?: HTTPQueriesParams)` - 返回全部值的数组（`string[]`）\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api/search' })\nexport class SearchController {\n  // GET /api/search?category=books\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })\n  async searchByCategory(@HTTPQuery() category: string) {\n    return { category };\n  }\n\n  // GET /api/search?tag=tech&tag=dev\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })\n  async searchByTags(@HTTPQueries({ name: 'tag' }) tags: string[]) {\n    return { tags };\n  }\n}\n```\n\n#### 使用要点\n\n- `@HTTPQuery`：返回首个匹配的值\n- `@HTTPQueries`：返回全部值的数组\n- 参数名默认与变量名匹配\n- 使用 `{ name: 'key' }` 指定查询参数名\n\n---\n\n### @HTTPBody\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：从请求体提取数据（POST/PUT 请求的 body）\n\n**语法**：`@HTTPBody()`\n\n**类型**：`object | string | FormData`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api/users' })\nexport class UserController {\n  @HTTPMethod({ method: HTTPMethodEnum.POST, path: '/' })\n  async createUser(@HTTPBody() body: { name: string; email: string }) {\n    return { userId: '123', ...body };\n  }\n}\n```\n\n#### Content-Type 解析\n\n| Content-Type                        | 解析结果        |\n| ----------------------------------- | --------------- |\n| `application/json`                  | 对象 `object`   |\n| `text/plain`                        | 字符串 `string` |\n| `application/x-www-form-urlencoded` | 对象 `object`   |\n\n**注意**：其他类型注入空值，需用 `@Request` 手动处理\n\n---\n\n### @HTTPHeaders\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：获取 HTTP 请求头中的字段\n\n**语法**：`@HTTPHeaders()`\n\n**类型**：`IncomingHttpHeaders`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class HeaderController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/info' })\n  async getInfo(@HTTPHeaders() headers: IncomingHttpHeaders) {\n    const auth = headers['authorization'];\n    const custom = headers['x-custom'];\n    return { auth, custom };\n  }\n}\n```\n\n#### 使用要点\n\n- Headers key 会自动转为小写，取值时使用小写字符\n- 获取单个值：`headers['x-custom']`\n\n---\n\n### @Cookies\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：从 HTTP Cookie 中读取会话数据\n\n**语法**：`@Cookies()`\n\n**类型**：`HTTPCookies`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class SessionController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/session' })\n  async getSession(@Cookies() cookies: HTTPCookies) {\n    const session = cookies.get('sessionId');\n    return { session };\n  }\n}\n```\n\n#### 使用要点\n\n- 使用 `cookies.get(key)` 读取 Cookie\n- 使用 `{ signed: false }` 读取未签名的 Cookie\n\n---\n\n### @Request\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：访问完整的 HTTP 请求对象\n\n**语法**：`@Request()`\n\n**类型**：`HTTPRequest`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class RequestController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/debug' })\n  async getDebug(@Request() request: HTTPRequest) {\n    const url = request.url;\n    const method = request.method;\n    const contentType = request.headers.get('content-type');\n    const rawBody = await request.text();\n    return { url, method, contentType, bodyLength: rawBody.length };\n  }\n}\n```\n\n#### 使用要点\n\n- `request.url`：请求 URL\n- `request.method`：请求方法\n- `request.headers`：Headers 对象\n- `request.text()`：读取请求体为文本\n- `request.arrayBuffer()`：读取请求体为 ArrayBuffer\n\n### @Context\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：访问 Egg 框架的 Context 对象\n\n**语法**：`@Context()`\n\n**类型**：`EggContext`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class DebugController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/debug' })\n  async debug(@Context() ctx: EggContext) {\n    return {\n      app: ctx.app.name,\n      ip: ctx.ip,\n      userAgent: ctx.get('user-agent'),\n    };\n  }\n}\n```\n","packages/skills/egg-controller/references/mcp-controller.md":"# MCPController 开发指南\n\n## 常见错误\n\n生成 MCPController 代码时，**必须**注意以下易错点：\n\n| 错误写法                         | 正确写法                              | 说明                                     |\n| -------------------------------- | ------------------------------------- | ---------------------------------------- |\n| `from 'egg'`                     | `from '@eggjs/tegg'`                  | 所有 MCP 装饰器和类型来自 `@eggjs/tegg`  |\n| `import z from 'zod'`            | `import { z } from '@eggjs/tegg/zod'` | 框架内置 zod，必须使用具名导入           |\n| `z.object({ name: z.string() })` | `{ name: z.string() }`                | Schema 使用普通对象，不要用 `z.object()` |\n| `args: ToolArgs<MySchema>`       | `args: ToolArgs<typeof MySchema>`     | 类型参数必须用 `typeof`                  |\n| `@MCPController` 不加括号        | `@MCPController()`                    | 装饰器必须带括号调用                     |\n\n---\n\n## 文件约定\n\n### 文件位置与命名\n\nMCPController 放在 module 的 `controller/` 目录下，命名规则为 `{Name}MCPController.ts`：\n\n```\napp/module-name/\n├── controller/\n│   ├── PackageMCPController.ts    ← MCP 控制器\n│   └── PackageHTTPController.ts   ← 同模块可共存 HTTP 控制器\n└── service/\n    └── PackageService.ts\n```\n\n### 插件配置\n\n在 `config/plugin.ts` 中启用：\n\n```typescript\nplugin.mcpProxy = true;\n```\n\n### 路径配置\n\n在 `config/config.default.ts` 中配置 MCP 路径（通常不需要修改，以下为默认值）：\n\n```typescript\nimport { randomUUID } from 'node:crypto';\n\nexport default () => {\n  const config = {\n    mcp: {\n      sseInitPath: '/mcp/sse',\n      sseMessagePath: '/mcp/message',\n      streamPath: '/mcp/stream',\n      statelessStreamPath: '/mcp/stateless/stream',\n      sessionIdGenerator: randomUUID,\n    },\n  };\n  return config;\n};\n```\n\n当使用 `@MCPController({ name: 'myServer' })` 声明命名服务时，路径自动变为：\n\n- `/mcp/myServer/sse`\n- `/mcp/myServer/message`\n- `/mcp/myServer/stream`\n- `/mcp/myServer/stateless/stream`\n\n### AccessLevel\n\n`@MCPController` 装饰器内部已默认设置 AccessLevel（PUBLIC），不需要再手动声明。\n\n---\n\n## 场景决策树\n\n```\n用户需要什么？\n\n├─ \"让 AI 能查数据 / 执行操作\"\n│  └─ → @MCPTool + @Inject Service 处理业务\n│\n├─ \"给 AI 一个提示词模板\"\n│  └─ → @MCPPrompt\n│\n├─ \"让 AI 读取某类资源数据\"\n│  ├─ 资源地址固定 → @MCPResource({ uri: '...' })\n│  └─ 资源地址动态 → @MCPResource({ template: [...] })\n│\n├─ \"Tool 执行中要推送进度\"\n│  └─ → @MCPTool + @Extra() 获取 sendNotification（见下方 @Extra 章节）\n│\n└─ \"Tool 中需要读取自定义请求头\"\n   └─ → @MCPTool + @Extra() 获取 requestInfo.headers\n```\n\n---\n\n## 端到端完整示例\n\n以下展示一个完整的 MCP 功能从配置到测试的所有文件：\n\n### 1. 插件配置 — `config/plugin.ts`\n\n```typescript\nplugin.mcpProxy = true;\n```\n\n### 2. 控制器 — `app/npm/controller/PackageMCPController.ts`\n\n```typescript\nimport {\n  MCPController,\n  MCPTool,\n  MCPToolResponse,\n  MCPPrompt,\n  MCPPromptResponse,\n  MCPResource,\n  MCPResourceResponse,\n  ToolArgs,\n  ToolArgsSchema,\n  PromptArgs,\n  PromptArgsSchema,\n  Inject,\n} from '@eggjs/tegg';\nimport { z } from '@eggjs/tegg/zod';\n\nimport { PackageService } from '../service/PackageService.ts';\n\nconst SearchSchema = {\n  name: z.string({ description: 'npm package name' }),\n};\n\nconst SummarySchema = {\n  name: z.string(),\n};\n\n@MCPController()\nexport class PackageMCPController {\n  @Inject()\n  private readonly packageService: PackageService;\n\n  @MCPTool({ description: 'Search npm package info' })\n  async searchPackage(@ToolArgsSchema(SearchSchema) args: ToolArgs<typeof SearchSchema>): Promise<MCPToolResponse> {\n    const pkg = await this.packageService.findByName(args.name);\n    if (!pkg) {\n      return { content: [{ type: 'text', text: `Package ${args.name} not found` }] };\n    }\n    return { content: [{ type: 'text', text: JSON.stringify(pkg) }] };\n  }\n\n  @MCPPrompt({ description: 'Generate package summary' })\n  async summarize(@PromptArgsSchema(SummarySchema) args: PromptArgs<typeof SummarySchema>): Promise<MCPPromptResponse> {\n    return {\n      messages: [\n        {\n          role: 'user',\n          content: {\n            type: 'text',\n            text: `Summarize the npm package: ${args.name}`,\n          },\n        },\n      ],\n    };\n  }\n\n  @MCPResource({\n    template: ['npm://{name}/{?version}', { list: undefined }],\n  })\n  async getPackageReadme(uri: URL): Promise<MCPResourceResponse> {\n    const name = uri.hostname;\n    const readme = await this.packageService.getReadme(name);\n    return { contents: [{ uri: uri.toString(), text: readme }] };\n  }\n}\n```\n\n### 3. Service — `app/npm/service/PackageService.ts`\n\n```typescript\nimport { SingletonProto } from 'egg';\n\n@SingletonProto()\nexport class PackageService {\n  async findByName(name: string) {\n    // 业务逻辑\n  }\n\n  async getReadme(name: string): Promise<string> {\n    // 业务逻辑\n  }\n}\n```\n\n### 4. 单元测试 — `test/npm/controller/PackageMCPController.test.ts`\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from 'egg-mock/bootstrap';\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\n\ndescribe('PackageMCPController', () => {\n  it('should search package via tool', async () => {\n    app.mockCsrf();\n    const client: Client = await app.mcpClient();\n\n    const tools = await client.listTools();\n    assert(tools.tools.some((t) => t.name === 'searchPackage'));\n\n    const res = await client.callTool({\n      name: 'searchPackage',\n      arguments: { name: 'egg' },\n    });\n    assert(res.content[0].type === 'text');\n  });\n\n  it('should get prompt', async () => {\n    app.mockCsrf();\n    const client: Client = await app.mcpClient();\n\n    const res = await client.getPrompt({\n      name: 'summarize',\n      arguments: { name: 'egg' },\n    });\n    assert(res.messages.length > 0);\n  });\n\n  it('should read resource', async () => {\n    app.mockCsrf();\n    const client: Client = await app.mcpClient();\n\n    const res = await client.readResource({\n      uri: 'npm://egg?version=4.0.0',\n    });\n    assert(res.contents.length > 0);\n  });\n});\n```\n\n---\n\n## @Extra() 的使用场景\n\n`@Extra()` 装饰器注入 `ToolExtra` 对象，提供两个能力：\n\n### 发送通知（长任务进度推送）\n\n```typescript\n@MCPTool()\nasync longTask(\n  @ToolArgsSchema(Schema) args: ToolArgs<typeof Schema>,\n  @Extra() extra: ToolExtra,\n): Promise<MCPToolResponse> {\n  const { sendNotification } = extra;\n  for (let i = 0; i < 10; i++) {\n    await sendNotification({\n      method: 'notifications/message',\n      params: { level: 'info', data: `Step ${i + 1}/10` },\n    });\n    // ... 执行步骤\n  }\n  return { content: [{ type: 'text', text: 'Done' }] };\n}\n```\n\n### 读取自定义请求头\n\n```typescript\n@MCPTool()\nasync myTool(\n  @ToolArgsSchema(Schema) args: ToolArgs<typeof Schema>,\n  @Extra() extra: ToolExtra,\n): Promise<MCPToolResponse> {\n  const headers = extra.requestInfo?.headers;\n  // 处理自定义 header\n}\n```\n\n---\n\n## 装饰器参考\n\n| 装饰器                | 用途         | 常用参数                                   | 返回类型              |\n| --------------------- | ------------ | ------------------------------------------ | --------------------- |\n| `@MCPController()`    | 声明控制器   | `{ name?: string }`                        | -                     |\n| `@MCPTool()`          | 声明工具     | `{ name?: string, description?: string }`  | `MCPToolResponse`     |\n| `@MCPPrompt()`        | 声明提示词   | `{ name?: string, description?: string }`  | `MCPPromptResponse`   |\n| `@MCPResource()`      | 声明资源     | `{ uri: string }` 或 `{ template: [...] }` | `MCPResourceResponse` |\n| `@ToolArgsSchema()`   | Tool 参数    | Zod Schema 普通对象                        | -                     |\n| `@PromptArgsSchema()` | Prompt 参数  | Zod Schema 普通对象                        | -                     |\n| `@Extra()`            | 额外上下文   | -                                          | `ToolExtra`           |\n| `@Inject()`           | 注入 Service | -                                          | -                     |\n\n**注意**：`@MCPController` 的 `version`、`timeout` 等参数通常不需要配置。\n","packages/skills/egg-controller/references/middleware.md":"# Middleware 中间件指南\n\n## 常见错误\n\n| 错误写法                                   | 正确写法                           | 说明                                            |\n| ------------------------------------------ | ---------------------------------- | ----------------------------------------------- |\n| `import { Middleware } from '@eggjs/tegg'` | `import { Middleware } from 'egg'` | Middleware 从 `egg` 导入                        |\n| `import { Advice } from 'egg'`             | `import { Advice } from 'egg/aop'` | AOP 装饰器从 `egg/aop` 导入                     |\n| `@Middleware(funcMw, AdviceClass)`         | 分开写两个 `@Middleware`           | 同一个 `@Middleware()` 中不能混用函数式和 AOP   |\n| AOP Advice 中用实例属性存请求级状态        | 使用 `ctx.set()`/`ctx.get()`       | Advice 默认 Singleton，实例属性会被并发请求共享 |\n| 把中间件文件放在 `app/middleware/`         | 放在模块目录下                     | 函数式中间件放在模块中，通过 import 引用        |\n\n---\n\n## 两种中间件模式\n\nEgg 的 `@Middleware` 装饰器支持两种中间件写法，根据传入参数类型自动识别：\n\n- **AOP 写法（推荐）**：使用 `@Advice` 类，支持 `@Inject` 注入 Proto 依赖，拥有丰富的生命周期钩子\n- **函数式写法（旧版兼容）**：标准 Koa 中间件函数，需要从 `ctx` 对象上手动获取依赖\n\n新项目应优先使用 AOP 写法。函数式写法主要用于兼容旧的 egg 中间件或非常简单的场景。\n\n---\n\n## 实现中间件\n\n### AOP 写法（推荐）\n\n使用 `@Advice()` 装饰器定义类，实现 `IAdvice` 的 `around` 方法，写法与 Koa 中间件一致（`next` 调用目标方法）。Advice 本身是 Proto，支持 `@Inject` 注入依赖。`around` 中可以修改入参（`ctx.args`）和返回值：\n\n```typescript\n// app/modules/foo/advice/LogAdvice.ts\nimport { AccessLevel, Inject, Logger } from 'egg';\nimport { Advice, IAdvice, AdviceContext } from 'egg/aop';\n\n// 跨模块使用时需设置 accessLevel: AccessLevel.PUBLIC\n@Advice({ accessLevel: AccessLevel.PUBLIC })\nexport class LogAdvice implements IAdvice {\n  @Inject()\n  logger: Logger;\n\n  async around(ctx: AdviceContext, next: () => Promise<any>): Promise<any> {\n    // 修改入参：ctx.args 对应控制器方法的参数列表\n    // ctx.args[0] = sanitize(ctx.args[0]);\n\n    const start = Date.now();\n    const result = await next();\n    this.logger.info('%s cost %dms', ctx.method, Date.now() - start);\n\n    // 修改返回值：直接返回新的值即可\n    return { success: true, data: result };\n  }\n}\n```\n\n### 函数式写法（旧版兼容）\n\n标准 Koa 中间件函数，签名为 `(ctx: Context, next: Next) => Promise<void>`。旧版 egg 写法，无法使用 `@Inject`，需要从 `ctx` 对象上手动获取依赖：\n\n```typescript\n// app/modules/foo/middleware/count.ts\nimport type { Context, Next } from 'egg';\n\nexport async function countMw(ctx: Context, next: Next): Promise<void> {\n  const start = Date.now();\n  await next();\n  ctx.set('X-Response-Time', `${Date.now() - start}ms`);\n}\n```\n\n---\n\n## 应用中间件\n\n通过 `@Middleware()` 装饰器将中间件应用到控制器，支持类级别和方法级别：\n\n```typescript\n// app/modules/foo/FooController.ts\nimport { HTTPController, HTTPMethod, HTTPMethodEnum, Middleware } from 'egg';\nimport { LogAdvice } from '../common/advice/LogAdvice.ts';\nimport { countMw } from './middleware/count.ts';\n\n@HTTPController({ path: '/api' })\n@Middleware(LogAdvice) // 类级别：所有方法都会执行\nexport class FooController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/profile' })\n  @Middleware(countMw) // 方法级别：仅此方法执行\n  async getProfile() {\n    return { name: 'test' };\n  }\n}\n```\n\n---\n\n## 执行顺序\n\n遵循洋葱模型，类级别先执行，方法级别后执行：\n\n```typescript\n@Middleware(globalMw)\nexport class FooController {\n  // 进：globalMw → methodMw → hello()\n  // 出：hello() → methodMw → globalMw\n  @Middleware(methodMw)\n  async hello() {}\n\n  // 多个 @Middleware 从下往上执行（靠近方法的先注册）\n  // 进：globalMw → mw3 → mw2 → mw1 → multiple()\n  // 出：multiple() → mw1 → mw2 → mw3 → globalMw\n  @Middleware(mw1)\n  @Middleware(mw2)\n  @Middleware(mw3)\n  async multiple() {}\n}\n```\n\n**若混用函数式和 AOP 中间件，所有函数式中间件（无论类级别还是方法级别）会先于所有 AOP 中间件执行。** 即函数式和 AOP 分属两个独立的执行阶段，函数式阶段在前，AOP 阶段在后：\n\n```typescript\n@Middleware(countMw) // 函数式 - 类级别\n@Middleware(LogAdvice) // AOP - 类级别\nexport class FooController {\n  @Middleware(timeMw) // 函数式 - 方法级别\n  @Middleware(AuthAdvice) // AOP - 方法级别\n  async hello() {}\n}\n\n// 实际执行顺序：\n// countMw → timeMw → LogAdvice → AuthAdvice → hello()\n// （先所有函数式，再所有 AOP；各阶段内类级别先于方法级别）\n```\n","packages/skills/egg-controller/references/schedule.md":"# 定时任务开发指南\n\n## 注意事项\n\n- **不要将代码放在 `app/schedule` 目录下**，egg 默认会扫描该路径注册定时任务，会和装饰器方式冲突\n- 定时任务类必须包含一个 `subscribe` 方法，框架调度时会调用该方法\n- import 路径是 `egg/schedule`，不是 `egg`\n\n## Step 1: 创建定时任务\n\n使用 `@Schedule` 装饰器标识一个类为定时任务，支持 interval 和 cron 两种调度模式。\n\n### interval 模式\n\n按固定间隔执行。`interval` 支持毫秒数或 [ms](https://github.com/vercel/ms) 格式字符串（如 `'5s'`、`'1m'`）。\n\n```typescript\n// app/{moduleName}/schedule/Demo.ts\nimport { Inject, Logger } from 'egg';\nimport { IntervalParams, Schedule, ScheduleType } from 'egg/schedule';\n\n@Schedule<IntervalParams>({\n  type: ScheduleType.WORKER,\n  scheduleData: {\n    interval: '5s',\n  },\n})\nexport class DemoScheduler {\n  @Inject()\n  private logger: Logger;\n\n  async subscribe() {\n    this.logger.info('schedule called');\n  }\n}\n```\n\n### cron 模式\n\n按 cron 表达式执行，格式参考 [cron-parser](https://github.com/harrisiirak/cron-parser)：\n\n```text\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```typescript\n// app/{moduleName}/schedule/CronDemo.ts\nimport { Inject, Logger } from 'egg';\nimport { CronParams, Schedule, ScheduleType } from 'egg/schedule';\n\n@Schedule<CronParams>({\n  type: ScheduleType.WORKER,\n  scheduleData: {\n    cron: '0 0 3 * * *', // 每日 3 点执行\n  },\n})\nexport class CronScheduler {\n  @Inject()\n  private logger: Logger;\n\n  async subscribe() {\n    this.logger.info('schedule called');\n  }\n}\n```\n\n## Step 2: 选择工作模式\n\n| 模式                  | 说明                                     | 使用场景                             |\n| --------------------- | ---------------------------------------- | ------------------------------------ |\n| `ScheduleType.WORKER` | 每台机器只有一个 worker 执行（随机选择） | 大多数场景，如数据同步、缓存刷新     |\n| `ScheduleType.ALL`    | 每台机器的所有 worker 都执行             | 需要每个 worker 都更新本地状态的场景 |\n\n## Step 3: 配置运行参数\n\n`@Schedule` 装饰器支持第二个参数，控制定时任务的运行行为：\n\n```typescript\n@Schedule<IntervalParams>(\n  {\n    type: ScheduleType.WORKER,\n    scheduleData: {\n      interval: '1m',\n    },\n  },\n  {\n    immediate: true, // 应用启动后立即执行一次\n    // disable: true,           // 禁用该定时任务\n    env: ['devserver', 'test'], // 仅在指定环境下启动\n  },\n)\nexport class MyScheduler {\n  async subscribe() {\n    // ...\n  }\n}\n```\n\n| 参数        | 类型     | 说明                            |\n| ----------- | -------- | ------------------------------- |\n| `immediate` | boolean  | 应用启动并 ready 后立即执行一次 |\n| `disable`   | boolean  | 设为 true 时不启动该定时任务    |\n| `env`       | string[] | 仅在指定环境下启动              |\n","packages/skills/egg-core/SKILL.md":"---\nname: egg-core\ndescription: 本技能用于处理 EGG 基础核心概念，包括模块架构、@SingletonProto、@ContextProto、@Inject 装饰器、动态注入、BackgroundTaskHelper 后台任务、EventBus 事件总线和 AOP 切面编程。用于理解 EGG 的基础构建块、依赖注入、对象生命周期管理、运行时多实现动态选择、请求返回后的异步任务处理、事件驱动架构和横切关注点。\nallowed-tools: Read\n---\n\n# egg 核心概念\n\n## 代码组织与依赖注入\n\n### Step 1: 代码写在 module 中\n\n#### 什么是模块？\n\n模块是 EGG 中基础的代码组织单元。只有模块内的代码会被框架扫描和加载。模块之间相互独立，但可以通过 `@Inject` 装饰器访问其他模块的对象。\n\n#### 定义 module\n\n在目录中添加包含 `eggModule.name` 字段的 `package.json` 文件来声明该目录为模块：\n\n```json\n{\n  \"name\": \"foo\",\n  \"eggModule\": {\n    \"name\": \"foo\"\n  }\n}\n```\n\n**重要提示**：模块名称不能包含 `-` 或其他特殊字符；使用驼峰命名规则。\n\n#### 正确示例\n\n```\napp/\n└── userModule/           ✅ 驼峰命名\n    ├── package.json\n    │   └── { \"eggModule\": { \"name\": \"userModule\" } }\n    └── service.ts        ✅ 会被框架加载\n```\n\n#### 错误示例\n\n```\napp/\n├── user-module/          ❌ 名称包含 `-`\n│   └── package.json\n│       └── { \"eggModule\": { \"name\": \"user-module\" } }\n│\n└── common/               ❌ 缺少 package.json（不是 module）\n    └── utils.ts          ❌ 不会被框架加载\n```\n\n#### 模块配置\n\n在模块根目录创建 `module.yml` 用于模块特定配置：\n\n```yaml\nfoo: bar\n```\n\n通过 `@Inject()` 注入配置，使用 `moduleConfig`：\n\n```typescript\nimport { SingletonProto, Inject } from 'egg';\n\ninterface ModuleConfig {\n  foo: string;\n}\n\n@SingletonProto()\nexport class ConfigService {\n  @Inject()\n  private readonly moduleConfig: ModuleConfig;\n\n  async hello(): Promise<string> {\n    return `hello ${this.moduleConfig.foo}`;\n  }\n}\n```\n\n#### 模块组织最佳实践\n\n- 新应用：按功能在 `app/` 目录中组织\n- 存量应用：保留老的 egg 代码在 `app/controller`/`app/service`，将新增的 module 代码放在 `app/module/`\n- 可以在 `dependencies` 中导入 npm 包作为额外模块\n\n### 导入路径\n\n所有装饰器和类型统一从 `egg` 导入，不要从 `@eggjs/tegg` 导入：\n\n```typescript\n// ✅ 正确\nimport { SingletonProto, ContextProto, Inject, AccessLevel } from 'egg';\n\n// ❌ 错误 — 不要从 @eggjs/tegg 导入\nimport { SingletonProto } from '@eggjs/tegg';\n```\n\n---\n\n### Step 2: 用 Proto 实现 Service\n\n#### SingletonProto\n\n应用启动时立即创建，整个应用生命周期内只有一个实例，性能更好，应该作为默认选择。\n\n```typescript\nimport { SingletonProto } from 'egg';\n\n@SingletonProto()\nexport class HelloService {\n  async hello(): Promise<string> {\n    return 'hello';\n  }\n}\n```\n\n#### ContextProto\n\n请求到达时按需创建，每个请求一个实例，请求结束自动销毁。仅在需要隔离不同请求的上下文信息时使用。\n\n```typescript\nimport { ContextProto } from 'egg';\n\n@ContextProto()\nexport class RequestContext {\n  userId: string;\n}\n```\n\n**重要提示**：大多数服务应该使用 `SingletonProto` 以获得更好的性能。只有当请求上下文必须在服务之间共享以确保请求之间隔离时，才使用 `ContextProto`。\n\n#### AccessLevel\n\nproto 对象默认 accessLevel 为 `AccessLevel.PRIVATE`，仅在当前 module 内使用。可以设置为 `AccessLevel.PUBLIC`，进行跨模块访问。\n\n```typescript\nimport { AccessLevel, ContextProto, SingletonProto } from 'egg';\n\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class SharedService {}\n\n@ContextProto({ accessLevel: AccessLevel.PUBLIC })\nexport class SharedContextService {}\n```\n\n### Step 3: 通过 Inject 使用 Service\n\n#### 基本用法\n\n使用 `@Inject()` 注入其他 Proto 或 Egg 对象：\n\n```typescript\nimport { Inject, Logger, SingletonProto } from 'egg';\nimport { FooService } from './FooService.ts';\n\n@SingletonProto()\nexport class HelloService {\n  @Inject()\n  fooService: FooService; // 注入另一个 Proto\n\n  @Inject()\n  logger: Logger; // 注入 Egg 对象\n\n  async hello(): Promise<string> {\n    this.logger.info(`[HelloService] ${this.fooService.hello()}`);\n  }\n}\n```\n\n#### 动态注入\n\n当同一个抽象有多种实现，需要在运行时动态选择时，通过 `EggObjectFactory` 按类型获取实现，无需 if/else。详见 `references/dynamic-inject.md`。\n\n#### 重要约束\n\n- **不能有循环依赖**：Proto 或模块之间都不能有循环依赖\n- **不能有同名对象**：一个模块不能有相同名称和初始化类型的 Proto\n- **按需注入**：不要直接注入 `app` 或 `ctx`，按需注入特定对象\n\n### 快速决策指南\n\n| 场景                             | 使用方式                                               |\n| -------------------------------- | ------------------------------------------------------ |\n| 无状态服务                       | `@SingletonProto()`                                    |\n| 跨服务共享的请求级状态           | `@ContextProto()`                                      |\n| 需要跨模块访问                   | `@SingletonProto({ accessLevel: AccessLevel.PUBLIC })` |\n| 注入依赖                         | `@Inject()`                                            |\n| 使用自定义名称注入               | `@Inject({ name: 'customName' })`                      |\n| 同一抽象多种实现，运行时动态选择 | `QualifierImplDecoratorUtil` + `EggObjectFactory`      |\n\n## 异步任务\n\n| 特性       | BackgroundTaskHelper          | EventBus                                  |\n| ---------- | ----------------------------- | ----------------------------------------- |\n| **耦合**   | 高 — 异步逻辑写在触发者代码中 | 低 — handler 独立，新增处理者不修改触发者 |\n| **上下文** | 共享触发者的请求上下文        | handler 运行在独立的新上下文中            |\n| **扩展**   | 需要修改触发者代码            | 新增 `@Event` handler 类即可              |\n\n```\n需要在请求之外执行任务？\n│\n├─ 请求返回后执行，依赖当前请求上下文\n│  └─ → BackgroundTaskHelper（references/background-task.md）\n│\n├─ 请求返回后执行，不依赖当前请求上下文，需要解耦\n│  └─ → EventBus（references/eventbus.md）\n│\n└─ 定时或周期执行\n   └─ → Schedule（参考 egg-controller skill）\n```\n\n## AOP 切面编程\n\nAOP 用于将日志、鉴权、缓存、事务等横切关注点从业务代码中分离。AOP 装饰器从 `egg/aop` 导入（不是 `egg`）。\n\n```\n需要在方法执行前后添加通用逻辑？\n│\n├─ 针对特定方法 → @Pointcut（在目标方法上声明）\n│\n└─ 批量切入多个类/方法 → @Crosscut（在 Advice 类上声明匹配规则）\n```\n\n详细用法（Advice 生命周期、AdviceContext、Pointcut/Crosscut 选型、参数透传、执行顺序）请参阅 `references/aop.md`。\n\n## 常见问题排查\n\n| 现象                   | 原因                                       | 解决方案                                     |\n| ---------------------- | ------------------------------------------ | -------------------------------------------- |\n| 模块没被加载           | `eggModule.name` 包含 `-` 等特殊字符       | 改为驼峰命名                                 |\n| `EggPrototypeNotFound` | 跨模块注入但 accessLevel 为 PRIVATE        | 改为 `AccessLevel.PUBLIC`                    |\n| 注入对象不对           | 类型为 `interface`/`any`，回退到属性名匹配 | 改用 class 类型或 `@Inject({ name: 'xxx' })` |\n| 混用注入方式报错       | 属性注入和构造函数注入不能混用             | 统一使用一种方式                             |\n| 可选依赖启动报错       | 缺少 optional 标记                         | `@Inject({ optional: true })`                |\n\n## 参考资料\n\n- 详细的 module 文档，请参阅：`references/module.md`\n- Inject 装饰器使用，请参阅：`references/inject.md`\n- SingletonProto 和 ContextProto 详情，请参阅：`references/proto.md`\n- 动态注入（Qualifier 动态注入），请参阅：`references/dynamic-inject.md`\n- 请求后异步任务（BackgroundTaskHelper），请参阅：`references/background-task.md`\n- 事件总线（EventBus），请参阅：`references/eventbus.md`\n- AOP 切面编程（Advice、Pointcut、Crosscut），请参阅：`references/aop.md`\n\n单元测试（`egg-unittest` skill）：Service 测试、BackgroundTask 测试、EventBus 测试\n","packages/skills/egg-core/references/aop.md":"# AOP 切面编程指南\n\n## 常见错误\n\n| 错误写法                                   | 正确写法                              | 说明                                                                                   |\n| ------------------------------------------ | ------------------------------------- | -------------------------------------------------------------------------------------- |\n| `import { Advice } from 'egg'`             | `import { Advice } from 'egg/aop'`    | AOP 装饰器从 `egg/aop` 导入，不是 `egg`                                                |\n| `import { Advice } from '@eggjs/tegg/aop'` | `import { Advice } from 'egg/aop'`    | 统一从 `egg/aop` 导入                                                                  |\n| Advice 类没有加 `@Advice()` 装饰器         | 必须同时有 `@Advice()`                | `@Pointcut` 和 `@Crosscut` 都要求目标是 Advice 类                                      |\n| `@Crosscut` 直接切 Egg 内置对象            | 只能切 tegg Proto 对象                | Egg 中的对象（如 app、ctx）无法被 Crosscut                                             |\n| Advice 中用实例属性存状态                  | 使用 `ctx.set()`/`ctx.get()` 共享状态 | Advice 默认是 Singleton，实例属性会被并发请求共享，必须用 AdviceContext 传递调用级状态 |\n\n---\n\n## 核心概念\n\n### Advice（切面逻辑）\n\nAdvice 是 AOP 的核心，定义了在目标方法执行前后要做什么。Advice 本身也是一种 Proto，默认 initType 为 Singleton（全局单例），可以使用 `@Inject` 注入依赖。如需每请求一个实例，显式指定 `@Advice({ initType: ObjectInitType.CONTEXT })`。\n\n```typescript\nimport { Advice, IAdvice, AdviceContext } from 'egg/aop';\nimport { Inject, Logger } from 'egg';\n\n@Advice()\nexport class LogAdvice implements IAdvice {\n  @Inject()\n  private logger: Logger;\n\n  async beforeCall(ctx: AdviceContext): Promise<void> {\n    ctx.set('startTime', Date.now());\n  }\n\n  async afterReturn(ctx: AdviceContext, result: any): Promise<void> {\n    // 方法成功返回后执行\n  }\n\n  async afterThrow(ctx: AdviceContext, error: Error): Promise<void> {\n    // 方法抛出异常后执行\n  }\n\n  async afterFinally(ctx: AdviceContext): Promise<void> {\n    const duration = Date.now() - ctx.get('startTime');\n    this.logger.info('%s cost %dms', String(ctx.method), duration);\n  }\n\n  async around(ctx: AdviceContext, next: () => Promise<any>): Promise<any> {\n    // 类似 koa 中间件，可以包裹方法执行\n    return await next();\n  }\n}\n```\n\n**执行顺序：**\n\n```typescript\nawait beforeCall(ctx);\ntry {\n  const result = await around(ctx, next); // next 执行目标方法\n  await afterReturn(ctx, result);\n  return result;\n} catch (e) {\n  await afterThrow(ctx, e);\n  throw e;\n} finally {\n  await afterFinally(ctx);\n}\n```\n\n**关键点：**\n\n- 所有 hook 方法都是可选的，按需实现\n- 只有 `around` 可以修改返回值\n- `beforeCall` 中可以通过修改 `ctx.args` 改变方法入参\n- 多个 Advice 之间通过 `ctx.set(key, value)` / `ctx.get(key)` 共享状态\n\n### AdviceContext\n\n```typescript\ninterface AdviceContext<T = object, K = any> {\n  that: T; // 被切的对象实例\n  method: PropertyKey; // 被切的方法名\n  args: any[]; // 方法参数（可修改）\n  adviceParams?: K; // 装饰器透传的参数\n  get(key: PropertyKey): any; // 获取共享状态\n  set(key: PropertyKey, value: any): this; // 设置共享状态\n}\n```\n\n---\n\n## Pointcut vs Crosscut\n\n### Pointcut — 精确切入\n\n在特定类的特定方法上声明 Advice，适合精确控制：\n\n```typescript\nimport { SingletonProto } from 'egg';\nimport { Pointcut } from 'egg/aop';\n\n@SingletonProto()\nexport class OrderService {\n  @Pointcut(LogAdvice)\n  async createOrder(data: any) {\n    // 业务逻辑\n  }\n\n  // 可以传参给 Advice\n  @Pointcut(TransactionAdvice, { adviceParams: { propagation: 'REQUIRED' } })\n  async updateOrder(id: string, data: any) {\n    // 业务逻辑\n  }\n}\n```\n\n### Crosscut — 批量切入\n\n在 Advice 类上声明切入规则，适合横切多个类/方法：\n\n```typescript\nimport { Crosscut, Advice, IAdvice, PointcutType } from 'egg/aop';\n\n// 模式 1：指定类和方法\n@Crosscut({\n  type: PointcutType.CLASS,\n  clazz: OrderService,\n  methodName: 'createOrder',\n})\n@Advice()\nexport class AuditAdvice implements IAdvice {\n  async afterReturn(ctx: AdviceContext): Promise<void> {\n    // 审计日志\n  }\n}\n\n// 模式 2：正则匹配\n@Crosscut({\n  type: PointcutType.NAME,\n  className: /.*Service$/i,\n  methodName: /^(create|update|delete)/,\n})\n@Advice()\nexport class OperationLogAdvice implements IAdvice {\n  async around(ctx: AdviceContext, next: () => Promise<any>): Promise<any> {\n    // 记录所有 Service 的写操作\n    return await next();\n  }\n}\n\n// 模式 3：自定义回调\n@Crosscut({\n  type: PointcutType.CUSTOM,\n  callback: (clazz, method) => {\n    return clazz.name.endsWith('Repository') && method !== 'constructor';\n  },\n})\n@Advice()\nexport class DbMetricsAdvice implements IAdvice {}\n```\n\n### 执行顺序\n\n- `@Crosscut` 默认 order: `100`\n- `@Pointcut` 默认 order: `1000`\n- order 越小越先执行\n- 同一方法上多个 Advice 按 order 升序排列\n\n通过 `order` 参数自定义顺序：\n\n```typescript\n// Pointcut：第二个参数中设置 order\n@Pointcut(LogAdvice, { order: 50 })\nasync createOrder(data: any) {}\n\n// Crosscut：第二个参数中设置 order\n@Crosscut(\n  { type: PointcutType.NAME, className: /.*Service$/i, methodName: /.+/ },\n  { order: 200 },\n)\n@Advice()\nexport class MetricsAdvice implements IAdvice {}\n```\n\n---\n\n## 参数透传\n\n同一个 Advice 在不同方法上可能需要不同的行为，通过 `adviceParams` 传参：\n\n```typescript\n@Advice()\nexport class CacheAdvice implements IAdvice {\n  async around(ctx: AdviceContext<any, { ttl: number }>, next: () => Promise<any>): Promise<any> {\n    const ttl = ctx.adviceParams?.ttl ?? 60;\n    // 根据 ttl 实现缓存逻辑\n    return await next();\n  }\n}\n\n@SingletonProto()\nexport class UserService {\n  @Pointcut(CacheAdvice, { adviceParams: { ttl: 3600 } })\n  async getUser(id: string) {\n    /* ... */\n  }\n\n  @Pointcut(CacheAdvice, { adviceParams: { ttl: 60 } })\n  async getUserList() {\n    /* ... */\n  }\n}\n```\n\n---\n\n## 典型场景\n\n| 场景                        | 推荐方式                 | 说明              |\n| --------------------------- | ------------------------ | ----------------- |\n| 特定方法加日志/缓存         | `@Pointcut(Advice)`      | 精确控制          |\n| 所有 Service 的写操作加审计 | `@Crosscut(NAME)` + 正则 | 批量匹配          |\n| 事务包裹                    | `@Pointcut(TxAdvice)`    | around 中管理事务 |\n","packages/skills/egg-core/references/background-task.md":"# BackgroundTask 异步任务指南\n\n## 常见错误\n\n| 错误写法                                                    | 正确写法                                        | 说明                                                      |\n| ----------------------------------------------------------- | ----------------------------------------------- | --------------------------------------------------------- |\n| `setTimeout(() => { ... }, 0)`                              | `backgroundTaskHelper.run(async () => { ... })` | setTimeout 执行时上下文已释放，会导致访问已销毁对象的错误 |\n| `setImmediate(() => { ... })`                               | `backgroundTaskHelper.run(async () => { ... })` | 同上，框架不会等待 setImmediate 的回调                    |\n| `process.nextTick(() => { ... })`                           | `backgroundTaskHelper.run(async () => { ... })` | 同上                                                      |\n| 在 Service 中 `await` 后台任务                              | 直接调用 `run()` 不 await                       | `run()` 是\"发射后不管\"，不返回 Promise                    |\n| `import { BackgroundTaskHelper } from '@eggjs/tegg/helper'` | `import { BackgroundTaskHelper } from 'egg'`    | 统一从 `egg` 导入，不要从 `@eggjs/tegg` 子路径导入        |\n\n---\n\n## 使用方式\n\n### 基本用法\n\n通过 `@Inject()` 注入 `BackgroundTaskHelper`，调用 `run()` 方法。`run()` 接受一个异步函数，任务会立即开始执行但不阻塞当前流程。框架在请求结束（Context preDestroy）时会等待所有后台任务完成（最多等待 timeout），然后再释放上下文。\n\n```typescript\nimport { BackgroundTaskHelper, Inject, SingletonProto, AccessLevel } from 'egg';\n\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class MetricsService {\n  @Inject()\n  private backgroundTaskHelper: BackgroundTaskHelper;\n\n  async reportAfterResponse(data: Record<string, unknown>) {\n    // run() 是非阻塞的，任务立即开始执行但不会被 await\n    this.backgroundTaskHelper.run(async () => {\n      // 框架会保持上下文存活直到任务完成或超时\n      await this.sendMetrics(data);\n    });\n  }\n\n  private async sendMetrics(data: Record<string, unknown>) {\n    // 实际上报逻辑\n  }\n}\n```\n\n---\n\n## 超时配置\n\n框架默认等待 5 秒，超时后放弃等待并释放上下文。\n\n### 按请求调整超时\n\n```typescript\nthis.backgroundTaskHelper.timeout = 10000; // 10 秒\nthis.backgroundTaskHelper.run(async () => {\n  await heavyWork();\n});\n```\n\n注意：timeout 是 context 级别的设置，同一请求中多次设置以最后一次为准。\n\n### 全局配置\n\n```typescript\n// config/config.default.ts\nexport default {\n  backgroundTask: {\n    timeout: 10000, // 全局默认 10 秒\n  },\n};\n```\n\n### 无限等待\n\n```typescript\n// 不推荐在生产环境使用，除非确定任务一定会完成\nthis.backgroundTaskHelper.timeout = Infinity;\n```\n\n---\n\n## 错误处理\n\nBackgroundTaskHelper 内部会捕获所有错误并记录日志，**不会**抛出异常或影响其他后台任务。\n\n```typescript\nthis.backgroundTaskHelper.run(async () => {\n  // 即使这里抛出异常，也不会影响其他后台任务或框架\n  throw new Error('something went wrong');\n  // 错误会被记录为: [BackgroundTaskHelper] background throw error:something went wrong\n});\n\n// 如果需要自定义错误处理\nthis.backgroundTaskHelper.run(async () => {\n  try {\n    await unreliableOperation();\n  } catch (e) {\n    // 自定义错误处理：重试、告警等\n    await this.alertService.notify(e);\n  }\n});\n```\n\n---\n\n## 原理\n\n请求处理完成后，框架会立即释放上下文（防止内存泄漏）。如果用 `setTimeout` 等方式在上下文释放后访问注入的服务，会因为上下文已销毁而出错。\n\nBackgroundTaskHelper 的作用是：\n\n1. `run()` 被调用时，任务立即开始执行（不是延后到响应发送后）\n2. 任务不会被 await，因此不阻塞当前请求的返回\n3. 请求结束时（Context preDestroy），框架等待所有后台任务完成（最多等待 timeout）\n4. 等待结束后才释放上下文\n\n这就是为什么必须用 `backgroundTaskHelper.run()` 而不是 `setTimeout` — 后者绕过了框架的上下文生命周期管理，执行时上下文可能已被释放。\n\n---\n\n## 单元测试\n\nBackgroundTaskHelper 的测试方法参考 `egg-unittest` skill 的 `references/background-task-test.md`。\n","packages/skills/egg-core/references/dynamic-inject.md":"# 动态注入开发指南\n\n## 何时使用\n\n- **用动态注入**：同一抽象有多种实现，运行时按参数选择（如多种支付方式、多种存储后端）\n- **用普通 `@Inject()`**：依赖只有一个实现，编译时就能确定\n\n## Step 1: 定义抽象类和类型枚举\n\n```typescript\n// AbstractHello.ts\nexport abstract class AbstractHello {\n  abstract hello(): string;\n}\n\n// HelloType.ts\nexport enum HelloType {\n  FOO = 'FOO',\n  BAR = 'BAR',\n}\n```\n\n如果类型是无限扩展的，没有固定枚举，可以用 `Record<string, string>` 代替：\n\n```typescript\ntype AnyEnum = Record<string, string>;\n```\n\n## Step 2: 创建自定义装饰器\n\n```typescript\n// decorator/Hello.ts\nimport { ImplDecorator, QualifierImplDecoratorUtil } from 'egg';\nimport { HelloType } from '../HelloType.ts';\nimport { AbstractHello } from '../AbstractHello.ts';\n\nexport const HELLO_ATTRIBUTE = Symbol('HELLO_ATTRIBUTE');\n\nexport const Hello: ImplDecorator<AbstractHello, typeof HelloType> = QualifierImplDecoratorUtil.generatorDecorator(\n  AbstractHello,\n  HELLO_ATTRIBUTE,\n);\n```\n\n**注意事项：**\n\n- **ATTRIBUTE（Symbol）不要重复**，重复会导致实现被覆盖\n- **抽象类不要指定错**，否则可能导致实现被覆盖\n\n## Step 3: 实现抽象类\n\n每个实现加上 `@SingletonProto()`（或 `@ContextProto()`）和自定义装饰器：\n\n```typescript\n// impl/FooHello.ts\nimport { SingletonProto } from 'egg';\nimport { Hello } from '../decorator/Hello.ts';\nimport { HelloType } from '../HelloType.ts';\nimport { AbstractHello } from '../AbstractHello.ts';\n\n@SingletonProto()\n@Hello(HelloType.FOO)\nexport class FooHello extends AbstractHello {\n  hello(): string {\n    return 'hello, foo';\n  }\n}\n```\n\n## Step 4: 动态获取实现\n\n通过 `EggObjectFactory` 在运行时按类型获取对应实现：\n\n```typescript\n// HelloService.ts\nimport { EggObjectFactory, SingletonProto, Inject } from 'egg';\nimport { HelloType } from './HelloType.ts';\nimport { AbstractHello } from './AbstractHello.ts';\n\n@SingletonProto()\nexport class HelloService {\n  @Inject()\n  private eggObjectFactory: EggObjectFactory;\n\n  async hello(type: HelloType): Promise<string> {\n    const helloImpl = await this.eggObjectFactory.getEggObject(AbstractHello, type);\n    return helloImpl.hello();\n  }\n}\n```\n","packages/skills/egg-core/references/eventbus.md":"# EventBus 事件总线指南\n\n## 常见错误\n\n| 错误写法                                         | 正确写法                                    | 说明                                             |\n| ------------------------------------------------ | ------------------------------------------- | ------------------------------------------------ |\n| `import { EventBus } from '@eggjs/tegg'`         | `import { EventBus } from 'egg'`            | 统一从 `egg` 导入                                |\n| `import { Event } from '@eggjs/tegg'`            | `import { Event } from 'egg'`               | 装饰器也从 `egg` 导入                            |\n| 在 handler 的 `handle` 方法中直接访问请求上下文  | 使用 `@EventContext()` 注入 `IEventContext` | handler 运行在独立的上下文中，不是触发者的上下文 |\n| `@Event('hello')` 但没有声明 Events 接口         | 先在 `declare module 'egg'` 中声明事件类型  | 不声明会导致类型检查报错                         |\n| handler 的 `handle` 方法签名与 Events 声明不一致 | 确保参数类型与 Events 中定义一致            | TypeScript 会检查签名是否匹配                    |\n\n---\n\n## 使用步骤\n\n### 1. 声明事件类型\n\n在模块中创建类型声明文件，定义事件名称和参数签名：\n\n```typescript\n// app/myModule/event.ts\nimport 'egg';\n\ndeclare module 'egg' {\n  interface Events {\n    orderCreated: (orderId: string, userId: string) => void;\n    paymentCompleted: (orderId: string, amount: number) => void;\n  }\n}\n```\n\n**注意：** 文件开头必须有 `import 'egg'`，否则 `declare module` 会覆盖而非合并模块声明。\n\n### 2. 触发事件\n\n通过 `@Inject()` 注入 `EventBus` 或 `ContextEventBus`，调用 `emit()` 触发事件：\n\n```typescript\nimport { SingletonProto, Inject, EventBus, AccessLevel } from 'egg';\n\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class OrderService {\n  @Inject()\n  private eventBus: EventBus;\n\n  async createOrder(userId: string, items: Item[]): Promise<string> {\n    const orderId = await this.saveOrder(userId, items);\n    // 触发事件，不阻塞当前流程\n    this.eventBus.emit('orderCreated', orderId, userId);\n    return orderId;\n  }\n}\n```\n\n`emit()` 的参数类型由 Events 接口约束，TypeScript 会自动提示和校验。\n\n### 3. 消费事件\n\n用 `@Event()` 装饰器标记 handler 类。handler 必须实现 `handle` 方法，参数签名与 Events 声明一致：\n\n```typescript\nimport { Event, Inject } from 'egg';\nimport type { EggLogger } from 'egg';\n\n@Event('orderCreated')\nexport class OrderNotificationHandler {\n  @Inject()\n  private logger: EggLogger;\n\n  async handle(orderId: string, userId: string): Promise<void> {\n    this.logger.info('[OrderNotification] order %s created by user %s', orderId, userId);\n    // 发送通知等业务逻辑\n  }\n}\n```\n\n**handler 的关键特性：**\n\n- handler 运行在独立的上下文中，不是触发者的上下文\n- 同一事件可以有多个 handler，它们并行执行\n- handler 文件放在模块目录中，框架自动扫描和注册\n\n### 4. 消费多个事件\n\n一个 handler 可以处理多个事件，通过 `@EventContext()` 获取事件上下文来区分：\n\n```typescript\nimport { Event, EventContext, type IEventContext } from 'egg';\n\n@Event('orderCreated')\n@Event('paymentCompleted')\nexport class AuditLogHandler {\n  async handle(@EventContext() ctx: IEventContext, ...args: unknown[]): Promise<void> {\n    console.log('event:', ctx.eventName, 'args:', args);\n  }\n}\n```\n\n`@EventContext()` 只能修饰 `handle` 方法的第一个参数。不需要区分事件时可以省略。\n\n---\n\n## Cork/Uncork 事件缓冲\n\n当需要在一个操作中触发多个事件，但希望它们在操作全部完成后才被处理时，使用 cork/uncork：\n\n```typescript\nimport { ContextProto, Inject, type ContextEventBus } from 'egg';\n\n@ContextProto()\nexport class BatchService {\n  @Inject()\n  private eventBus: ContextEventBus; // 注意：cork/uncork 需要 ContextEventBus\n\n  async processBatch(items: string[]): Promise<void> {\n    this.eventBus.cork(); // 开始缓冲，事件不会立即派发\n\n    for (const item of items) {\n      this.eventBus.emit('orderCreated', item, 'batch-user');\n    }\n\n    this.eventBus.uncork(); // 释放缓冲，所有事件一次性派发\n  }\n}\n```\n\n**Cork/Uncork 行为：**\n\n- 支持嵌套调用 — 内层 uncork 不会释放事件，只有最外层 uncork 才会\n- `ContextEventBus` 的 cork/uncork 自动管理 corkId，无需手动指定\n- cork 期间 emit 的事件会被暂存，uncork 后按顺序派发\n\n---\n\n## 单元测试\n\nEventBus 的测试方法参考 `egg-unittest` skill 的 `references/eventbus-test.md`。\n","packages/skills/egg-core/references/inject.md":"# Inject 开发指南\n\n## 基本用法\n\n使用 `@Inject()` 注入其他 Proto 对象或 Egg 内置对象：\n\n```typescript\nimport { SingletonProto, Inject } from 'egg';\n\n@SingletonProto()\nexport class OrderService {\n  @Inject()\n  userService: UserService; // 按类型自动匹配\n\n  @Inject({ name: 'customName' })\n  config: any; // 按名称匹配\n\n  @Inject('shorthand')\n  other: any; // 字符串简写，等同于 { name: 'shorthand' }\n}\n```\n\n## 名称解析规则\n\n框架按以下优先级确定注入目标：\n\n1. **显式指定 name** — `@Inject({ name: 'xxx' })` 或 `@Inject('xxx')`\n2. **类型元数据自动关联** — 当属性类型是使用了 `@SingletonProto()` 或 `@ContextProto()` 装饰器的 class 时，框架自动从类型元数据匹配对应的 Proto 对象。此时属性名可以任意取值，不影响注入结果\n3. **属性名兜底** — 当类型为 `interface`、`any`、`unknown` 等无法获取运行时元信息的类型时，使用属性名作为注入名称\n\n```typescript\n@SingletonProto()\nexport class MyService {\n  @Inject()\n  fooService: FooService; // 优先级 2：从 FooService 类型自动关联\n\n  @Inject()\n  whatever: FooService; // 优先级 2：同样生效，属性名不影响匹配\n\n  @Inject({ name: 'bar' })\n  baz: FooService; // 优先级 1：显式 name → \"bar\"\n\n  @Inject()\n  something: any; // 优先级 3：类型无元数据，回退到属性名 → \"something\"\n}\n```\n\n## 可选注入\n\n默认情况下，注入目标不存在会在启动时报错。使用 `optional` 可以跳过不存在的依赖：\n\n```typescript\nimport { SingletonProto, Inject, InjectOptional } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  @Inject({ optional: true })\n  maybeService?: SomeService; // 不存在时为 undefined\n\n  @InjectOptional()\n  anotherOptional?: OtherService; // 简写方式，效果相同\n}\n```\n\n## 构造函数注入\n\n除了属性注入，也支持构造函数参数注入：\n\n```typescript\nimport { SingletonProto, Inject } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  constructor(\n    @Inject() readonly fooService: FooService,\n    @Inject({ optional: true }) readonly barService?: BarService,\n  ) {}\n}\n```\n\n**注意：构造函数注入和属性注入不能混用。** 一个 class 只能选择其中一种方式，否则框架会报错。\n\n## 注入 Egg 内置对象\n\n框架会自动遍历 `Application` 和 `Context` 对象的所有属性，均可通过 `@Inject()` 注入。\n\n#### 注入配置\n\n```typescript\nimport { Inject, SingletonProto, EggAppConfig } from 'egg';\n\n@SingletonProto()\nclass Foo {\n  @Inject()\n  config: EggAppConfig;\n\n  bar(): void {\n    console.log('current env is %s', this.config.env);\n  }\n}\n```\n\n#### 注入 Logger\n\n专为 logger 做了优化，可以直接注入 custom logger：\n\n```typescript\nimport { Inject, SingletonProto, Logger } from 'egg';\n\n@SingletonProto()\nclass FooService {\n  @Inject()\n  logger: Logger; // 注入 ${appname}-web.log\n\n  @Inject()\n  coreLogger: Logger; // 注入 egg-web.log\n\n  @Inject()\n  fooLogger: Logger; // 注入 customLogger 中配置的 fooLogger\n}\n```\n\n#### 注入 Service\n\n> 强烈建议把 Egg Service 的代码通过 Proto 重新封装再注入。对于已有的 Service，可以通过以下方式引入：\n\n```typescript\nimport { Service, Inject, SingletonProto } from 'egg';\n\n@SingletonProto()\nclass FooService {\n  @Inject()\n  service: Service; // 注入整个 ctx.service\n\n  get xxxService() {\n    return this.service.xxxService;\n  }\n}\n```\n\n#### 注入 HttpClient\n\n```typescript\nimport { Inject, SingletonProto, HttpClient } from 'egg';\n\n@SingletonProto()\nclass Foo {\n  @Inject()\n  httpClient: HttpClient;\n\n  async bar(): Promise<void> {\n    await this.httpClient.request('https://example.com');\n  }\n}\n```\n\n## 注入模块配置\n\n在模块根目录创建 `module.yml`，通过 `moduleConfig` 名称注入，框架自动注入当前模块的配置：\n\n```yaml\n# module.yml\napiEndpoint: https://api.example.com\nretryCount: 3\n```\n\n```typescript\nimport { SingletonProto, Inject } from 'egg';\n\ninterface ModuleConfig {\n  apiEndpoint: string;\n  retryCount: number;\n}\n\n@SingletonProto()\nexport class ApiService {\n  @Inject()\n  moduleConfig: ModuleConfig;\n\n  async call(): Promise<void> {\n    // this.moduleConfig.apiEndpoint → \"https://api.example.com\"\n  }\n}\n```\n\n如需注入其他模块的配置，使用 `@ConfigSourceQualifier` 指定模块名：\n\n```typescript\nimport { SingletonProto, Inject, ConfigSourceQualifier } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  @Inject()\n  @ConfigSourceQualifier('otherModule')\n  moduleConfig: OtherModuleConfig; // 注入 otherModule 的配置\n}\n```\n\n## Qualifier 限定符\n\n当同名对象存在多个实现时，使用限定符消歧义：\n\n### @InitTypeQualifier\n\n指定注入 Singleton 还是 Context 实例：\n\n```typescript\nimport { SingletonProto, Inject, InitTypeQualifier, ObjectInitType } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  @Inject()\n  @InitTypeQualifier(ObjectInitType.CONTEXT)\n  barService: BarService; // 强制注入 ContextProto 版本\n}\n```\n\n> 大多数情况下不需要手动指定，框架会根据类型元数据自动推导。\n\n### @EggQualifier\n\n当 `app` 和 `ctx` 上存在同名属性时，框架默认优先匹配 `ctx`。使用 `@EggQualifier` 显式指定来源：\n\n```typescript\nimport { SingletonProto, Inject, EggQualifier, EggType } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  @Inject()\n  @EggQualifier(EggType.APP)\n  someProp: any; // 强制从 app 注入\n\n  @Inject()\n  @EggQualifier(EggType.CONTEXT)\n  someProp2: any; // 强制从 ctx 注入\n}\n```\n\n### @ModuleQualifier\n\n指定从哪个模块注入：\n\n```typescript\nimport { SingletonProto, Inject, ModuleQualifier } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  @Inject()\n  @ModuleQualifier('userModule')\n  userService: UserService; // 明确从 userModule 注入\n}\n```\n\n## 重要约束\n\n- **不能有循环依赖**：Proto 之间或模块之间都不能形成循环引用\n- **不能有同名对象**：同一模块内不能存在相同名称和相同初始化类型的 Proto\n- **按需注入**：不要直接注入 `app` 或 `ctx`，按需注入具体的属性（如 `logger`、`config`）\n","packages/skills/egg-core/references/module.md":"# module 开发指南\n\n## 创建 module\n\n在 `app` 目录中，创建 module 目录，并在该目录中添加 `package.json`。\n\n```json\n{\n  \"name\": \"moduleName\",\n  \"eggModule\": {\n    \"name\": \"moduleName\"\n  }\n}\n```\n\n**重要提示**：\n\n- 模块名称不能包含 `-` 或其他特殊字符；使用驼峰命名规则。\n- module 的 `package.json` 文件中，仅包含 `name` 以及 `eggModule.name` 字段，不应该有其他额外内容。\n\n## module 发现机制\n\n框架支持两种模式：自动扫描（默认）和手动声明。两者互斥，当 `config/module.json` 存在时，自动扫描完全禁用。\n\n### 模式一：自动扫描（默认）\n\n当 `config/module.json` **不存在**时，框架通过以下两种方式发现模块：\n\n**1. 目录扫描**\n\n从项目根目录开始扫描，查找含有 `eggModule.name` 的 `package.json`。\n\n- 默认扫描深度：10 层\n- 自动排除：隐藏目录（`.` 开头）、`node_modules/`、`coverage/`\n\n可通过应用配置调整扫描深度和排除路径：\n\n```typescript\n// config/config.default.ts\nexport default {\n  tegg: {\n    readModuleOptions: {\n      deep: 5, // 默认 10\n      extraFilePattern: ['!**/dist'], // 额外排除 dist 目录\n    },\n  },\n};\n```\n\n`extraFilePattern` 使用 [globby](https://github.com/sindresorhus/globby) 语法，以 `!` 开头表示排除。\n\n```\napp/\n├── fooModule/          ✅ 被扫描到\n│   └── package.json    { \"eggModule\": { \"name\": \"fooModule\" } }\n├── barModule/          ✅ 被扫描到\n│   └── package.json    { \"eggModule\": { \"name\": \"barModule\" } }\n├── .hidden/            ❌ 隐藏目录，自动排除\n│   └── package.json\n└── common/             ❌ 无 package.json，不会加载\n    └── utils.ts\n```\n\n**2. npm 包扫描**\n\n框架会遍历项目 `package.json` 中 `dependencies` 的每个包（不含 `devDependencies`），检查其 `package.json` 是否含有 `eggModule.name`，如果有则自动作为模块加载。\n\n### 模式二：手动声明\n\n创建 `config/module.json` 后，自动扫描**完全禁用**，仅加载文件中声明的模块：\n\n```json\n[\n  { \"path\": \"../app/module-a\" }, // 相对于 config 目录的路径\n  { \"package\": \"@eggjs/common-module\" } // npm 包名\n]\n```\n\n## module 配置\n\n在模块根目录创建 `module.yml` 存放模块专属配置：\n\n```\napp/\n└── userModule/\n    ├── package.json\n    ├── module.yml          # 基础配置\n    ├── module.unittest.yml # 环境特定配置（可选）\n    └── UserService.ts\n```\n\n### 配置文件格式\n\n支持 YAML 和 JSON 两种格式，优先加载 YAML：\n\n```yaml\n# module.yml\nfeatures:\n  dynamic:\n    foo: bar\n```\n\n### 环境配置合并\n\n框架会按 `module.yml` → `module.{env}.yml` 的顺序深度合并：\n\n```yaml\n# module.yml\nfeatures:\n  dynamic:\n    foo: bar\n\n# module.unittest.yml\nfeatures:\n  dynamic:\n    testMode: true\n```\n\nunittest 环境下合并结果：\n\n```json\n{\n  \"features\": {\n    \"dynamic\": {\n      \"foo\": \"bar\",\n      \"testMode\": true\n    }\n  }\n}\n```\n\n### 注入配置\n\n通过 `@Inject()` 注入 `moduleConfig`，框架自动注入当前模块的配置：\n\n```typescript\nimport { SingletonProto, Inject } from 'egg';\n\ninterface ModuleConfig {\n  features: {\n    dynamic: {\n      foo: string;\n    };\n  };\n}\n\n@SingletonProto()\nexport class UserService {\n  @Inject()\n  moduleConfig: ModuleConfig;\n\n  async getFeatureFlag(): Promise<string> {\n    return this.moduleConfig.features.dynamic.foo; // 'bar'\n  }\n}\n```\n\n## module 目录组织\n\n### 新应用\n\n直接在 `app/` 目录下平铺 module。可按功能划分，也可按业务划分：\n\n**按功能划分：**\n\n```\napp/\n├── authModule/\n│   └── package.json\n├── logModule/\n│   └── package.json\n└── storageModule/\n    └── package.json\n```\n\n**按业务划分：**\n\n```\napp/\n├── userModule/\n│   └── package.json\n├── orderModule/\n│   └── package.json\n└── paymentModule/\n    └── package.json\n```\n\n### 存量应用（包含老的 egg 写法）\n\n保留原有 `app/controller`、`app/service` 等目录不动，将新增的 module 统一放在 `app/module/` 下：\n\n```\napp/\n├── controller/           # 老的 egg 代码，保持不变\n├── service/\n├── module/               # 新增 module 统一放这里\n│   ├── userModule/\n│   │   └── package.json\n│   └── orderModule/\n│       └── package.json\n└── router.ts\n```\n","packages/skills/egg-core/references/proto.md":"# Proto 开发指南\n\n## SingletonProto vs ContextProto\n\n|          | SingletonProto     | ContextProto       |\n| -------- | ------------------ | ------------------ |\n| 创建时机 | 应用启动时立即创建 | 请求到达时按需创建 |\n| 实例数量 | 整个应用 1 个      | 每个请求 1 个      |\n| 销毁时机 | 应用关闭           | 请求结束           |\n\n**决策逻辑：默认使用 `@SingletonProto()`**，性能更好。只有当需要在多个服务间共享请求级隔离状态时，才使用 `@ContextProto()`。\n\n```typescript\nimport { SingletonProto, ContextProto } from 'egg';\n\n// ✅ 默认选择：无状态服务用 SingletonProto\n@SingletonProto()\nexport class UserService {\n  async findUser(id: string): Promise<User> {\n    // 无需请求级状态，适合 Singleton\n  }\n}\n\n// 仅在需要请求级隔离时使用 ContextProto\n@ContextProto()\nexport class RequestContext {\n  traceId: string;\n  userId: string;\n  // 每个请求独立的状态\n}\n```\n\n## 装饰器参数\n\n`@SingletonProto()` 和 `@ContextProto()` 接受相同的可选参数：\n\n| 参数          | 类型        | 默认值         | 说明                                 |\n| ------------- | ----------- | -------------- | ------------------------------------ |\n| name          | string      | 类名首字母小写 | 实例名称                             |\n| accessLevel   | AccessLevel | PRIVATE        | 跨模块可见性                         |\n| protoImplType | string      | 'DEFAULT'      | 实现类型标记（高级用法，一般不需要） |\n\n名称推导规则：类名首字母自动转小写，如 `MyService` → `myService`。\n\n```typescript\nimport { SingletonProto, AccessLevel } from 'egg';\n\n// 使用默认参数：name 为 \"helloService\"，accessLevel 为 PRIVATE\n@SingletonProto()\nexport class HelloService {}\n\n// 自定义参数\n@SingletonProto({\n  name: 'customName',\n  accessLevel: AccessLevel.PUBLIC,\n})\nexport class HelloService {}\n```\n\n## 注入规则\n\n**SingletonProto 可以注入 ContextProto**，框架会自动处理生命周期差异。\n\n实现机制：框架不会直接注入 ContextProto 实例，而是通过 lazy getter/Proxy，在每次访问时从当前请求上下文动态解析，确保每个请求拿到各自的实例。\n\n```typescript\nimport { SingletonProto, ContextProto, Inject } from 'egg';\n\n@ContextProto()\nexport class RequestContext {\n  userId: string;\n}\n\n@SingletonProto()\nexport class AppService {\n  @Inject()\n  requestContext: RequestContext; // ✅ 每次访问自动解析当前请求的实例\n}\n```\n\nContextProto 也可以注入 SingletonProto：\n\n```typescript\n@ContextProto()\nexport class RequestHandler {\n  @Inject()\n  appService: AppService; // ✅ 直接注入\n}\n```\n\n## AccessLevel 跨模块访问\n\n- `AccessLevel.PRIVATE`（默认）：仅同模块内可注入\n- `AccessLevel.PUBLIC`：跨模块可注入\n\n模块边界由 `package.json` 中的 `eggModule.name` 决定。\n\n```typescript\n// userModule 中\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class UserService {}\n\n// orderModule 中\n@SingletonProto()\nexport class OrderService {\n  @Inject()\n  userService: UserService; // ✅ UserService 是 PUBLIC，跨模块可注入\n}\n```\n\n```typescript\n// userModule 中\n@SingletonProto() // 默认 PRIVATE\nexport class UserHelper {}\n\n// orderModule 中\n@SingletonProto()\nexport class OrderService {\n  @Inject()\n  userHelper: UserHelper; // ❌ 报错：UserHelper 是 PRIVATE，不能跨模块注入\n}\n```\n\n## 生命周期\n\nProto 对象支持以下生命周期钩子，按执行顺序排列：\n\n| 阶段 | 装饰器                      | 说明                             |\n| ---- | --------------------------- | -------------------------------- |\n| 1    | constructor                 | 构造函数                         |\n| 2    | `@LifecyclePostConstruct()` | 构造完成后                       |\n| 3    | `@LifecyclePreInject()`     | 注入前                           |\n| 4    | —                           | 依赖注入                         |\n| 5    | `@LifecyclePostInject()`    | 注入完成后                       |\n| 6    | `@LifecycleInit()`          | 异步初始化（对象就绪前最后一步） |\n| 7    | —                           | **对象就绪，可被使用**           |\n| 8    | `@LifecyclePreDestroy()`    | 销毁前                           |\n| 9    | `@LifecycleDestroy()`       | 销毁，用于清理资源               |\n\n常用的是 `@LifecycleInit()` 和 `@LifecycleDestroy()`。\n\n**示例 1：SingletonProto 异步初始化和资源清理**\n\n```typescript\nimport { SingletonProto, LifecycleInit, LifecycleDestroy } from 'egg';\n\n@SingletonProto()\nexport class DatabaseService {\n  private connection: Connection;\n\n  @LifecycleInit()\n  async init(): Promise<void> {\n    this.connection = await createConnection();\n  }\n\n  @LifecycleDestroy()\n  async destroy(): Promise<void> {\n    await this.connection.close();\n  }\n}\n```\n\n**示例 2：ContextProto 在注入完成后初始化状态**\n\n```typescript\nimport { ContextProto, Inject, LifecyclePostInject } from 'egg';\n\n@ContextProto()\nexport class RequestTracer {\n  @Inject()\n  traceService: TraceService;\n\n  traceId: string;\n\n  @LifecyclePostInject()\n  async postInject(): Promise<void> {\n    // 依赖注入完成后，利用注入的服务初始化状态\n    this.traceId = await this.traceService.generateTraceId();\n  }\n}\n```\n","packages/skills/egg-unittest/SKILL.md":"---\nname: egg-unittest\ndescription: 本技能用于编写 EGG 应用的单元测试。覆盖 HTTP 接口测试、Service/DI 对象测试、Mock 数据模拟、BackgroundTask 和 EventBus 测试。使用 @eggjs/mock、app.httpRequest()、app.getEggObject()、mm() 等 API。\nallowed-tools: Read\n---\n\n# EGG 单元测试\n\n---\n\n## 原理\n\n`egg-bin test` 使用 Vitest 运行测试，自动完成以下工作：\n\n- 以当前项目目录为 baseDir，创建并启动一个 MockApplication 实例（即 `app`）\n- 注入 `@eggjs/mock/setup_vitest` 管理 app 生命周期（`beforeAll` 启动 app、`afterEach` 恢复 mock、`afterAll` 关闭 app）\n- 注入 Vitest 全局变量（`describe`、`it`、`beforeAll` 等），无需手动 import\n  测试代码中通过 `import { app, mm } from '@eggjs/mock/bootstrap'` 获取已启动的 app 实例和 mock 工具，直接使用即可。\n\n---\n\n## 配置检查\n\n确保项目中有以下配置：\n\n**package.json：**\n\n```json\n{\n  \"scripts\": {\n    \"test\": \"egg-bin test\"\n  },\n  \"devDependencies\": {\n    \"@eggjs/bin\": \"^8\",\n    \"@eggjs/mock\": \"^8\"\n  }\n}\n```\n\n**测试文件约定：**\n\n- 测试目录：`test/`\n- 文件命名：`*.test.ts`\n\n**自定义 setup 文件（可选）：**\n\n如果存在 `test/.setup.ts`，egg-bin 会自动将其加入 vitest setupFiles，在 `@eggjs/mock/setup_vitest` 之前执行（即 app 启动之前）。可用于设置环境变量等全局初始化：\n\n```typescript\n// test/.setup.ts\nbeforeAll(() => {\n  process.env.SOME_CONFIG = 'test-value';\n});\n```\n\n---\n\n## 简单示例\n\n### HTTP 接口测试\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\n\ndescribe('test/controller/home.test.ts', () => {\n  it('should GET /', () => {\n    return app.httpRequest().get('/').expect(200).expect('hello world');\n  });\n});\n```\n\n### Service 测试\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\nimport { UserService } from '../app/modules/user/UserService.ts';\n\ndescribe('test/service/user.test.ts', () => {\n  it('should get user', async () => {\n    const userService = await app.getEggObject(UserService);\n    const user = await userService.getById('1');\n    assert(user);\n    assert.equal(user.name, 'test');\n  });\n});\n```\n\n---\n\n## 测试场景决策树\n\n```\n要测什么？\n\n1. HTTP 接口（GET/POST/PUT/DELETE）？\n   → 参考 references/http-test.md\n\n2. Service / DI 对象的方法？\n   → 参考 references/service-test.md\n\n3. 需要 mock 外部依赖？（HTTP 调用、Service 方法、Session、CSRF）\n   → 参考 references/mock.md\n\n4. BackgroundTaskHelper（后台异步任务）？\n   → 参考 references/background-task-test.md\n\n5. EventBus（事件驱动）？\n   → 参考 references/eventbus-test.md\n```\n\n---\n\n## 快速参考\n\n| API                                                  | 说明                                    |\n| ---------------------------------------------------- | --------------------------------------- |\n| `import { app, mm } from '@eggjs/mock/bootstrap'`    | 标准测试入口                            |\n| `app.httpRequest().get('/path').expect(200)`         | HTTP 接口测试                           |\n| `app.getEggObject(Class)`                            | 获取 SingletonProto / ContextProto 实例 |\n| `app.mockModuleContextScope(async (ctx) => { ... })` | ContextProto 测试作用域                 |\n| `mm(Class.prototype, 'method', fn)`                  | Mock Proto 方法                         |\n| `app.mockCsrf()`                                     | 跳过 CSRF 校验（POST 测试必备）         |\n| `app.mockHttpclient(url, data)`                      | Mock 外部 HTTP 调用                     |\n| `app.getEventWaiter()`                               | 获取 EventBus 事件等待器                |\n\n---\n\n## 常见错误\n\n| 错误写法                           | 正确写法                                      | 说明                        |\n| ---------------------------------- | --------------------------------------------- | --------------------------- |\n| `import { app } from 'egg'`        | `import { app } from '@eggjs/mock/bootstrap'` | 测试使用 mock 包            |\n| `before()` / `after()`             | `beforeAll()` / `afterAll()`                  | Vitest 钩子，不是 Mocha     |\n| POST 测试报 403                    | 加 `app.mockCsrf()`                           | 安全插件默认开启 CSRF       |\n| 手动写 `afterEach(mm.restore)`     | 不需要                                        | egg-bin 自动注入 mock 恢复  |\n| 代码写在 describe 内、hooks 外     | 放入 `beforeAll` / `beforeEach`               | describe 体在加载阶段就执行 |\n| `await app.ready()` 配合 bootstrap | 不需要                                        | bootstrap 自动处理生命周期  |\n\n---\n\n## 参考资料\n\n- `references/http-test.md` — HTTP 接口测试\n- `references/service-test.md` — Service/DI 对象测试\n- `references/mock.md` — Mock 模式\n- `references/background-task-test.md` — BackgroundTaskHelper 测试\n- `references/eventbus-test.md` — EventBus 测试\n","packages/skills/egg-unittest/references/background-task-test.md":"# BackgroundTaskHelper 测试\n\n## 常见错误\n\n| 错误写法                                           | 正确写法                                                                              | 说明                                                 |\n| -------------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------- |\n| 不等待就断言                                       | 用 `mockModuleContextScope`（自动等待）或 `app.backgroundTasksFinished()`（手动等待） | 后台任务异步执行，必须等待完成后再断言               |\n| 在 `mockModuleContextScope` 回调内断言后台任务结果 | 在 `mockModuleContextScope` 返回后断言                                                | 回调内任务尚未完成，返回后才会等待完成               |\n| 用 `TimerUtil.sleep` 等待                          | 用 `app.backgroundTasksFinished()`                                                    | sleep 时间不确定，`backgroundTasksFinished` 精确等待 |\n\n---\n\n## 使用 mockModuleContextScope\n\n`mockModuleContextScope` 退出时会自动等待所有后台任务完成（内部触发 `doPreDestroy`），scope 退出后直接断言即可：\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\nimport { CountService } from '../app/modules/count/CountService.ts';\n\nit('should complete background task', async () => {\n  await app.mockModuleContextScope(async (ctx) => {\n    const countService = await ctx.getEggObject(CountService);\n    // countService 内部通过 backgroundTaskHelper.run() 触发后台任务\n    await countService.doSomething();\n  });\n\n  // scope 退出后，后台任务已完成，直接断言\n  const countService = await app.getEggObject(CountService);\n  assert.equal(countService.count, 1);\n});\n```\n\n## 使用 backgroundTasksFinished\n\n不通过 `mockModuleContextScope` 触发的场景（如 HTTP 接口测试），scope 退出的自动等待机制不适用，需要手动调用 `app.backgroundTasksFinished()` 等待所有后台任务完成后，再做断言：\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\nimport { CountService } from '../app/modules/count/CountService.ts';\n\nit('should complete background task', async () => {\n  await app.httpRequest().get('/api/trigger-task').expect(200);\n\n  // 等待后台任务完成\n  await app.backgroundTasksFinished();\n\n  const countService = await app.getEggObject(CountService);\n  assert.equal(countService.count, 1);\n});\n```\n","packages/skills/egg-unittest/references/eventbus-test.md":"# EventBus 测试\n\n## 常见错误\n\n| 错误写法                         | 正确写法                                           | 说明                                         |\n| -------------------------------- | -------------------------------------------------- | -------------------------------------------- |\n| 先 emit 再 `eventWaiter.await()` | 先 `eventWaiter.await()` 再触发业务逻辑            | await 注册监听器，必须在事件发出前           |\n| 不等待事件处理完成就断言         | 使用 `eventWaiter.await('eventName')` 等待后再断言 | handler 异步执行，不等待则断言时可能尚未完成 |\n\n---\n\n## 基本测试模式\n\n使用 `app.getEventWaiter()` 等待事件被处理完成：\n\n```typescript\nimport assert from 'node:assert';\nimport { app, mm } from '@eggjs/mock/bootstrap';\nimport { HelloService } from '../app/modules/hello/HelloService.ts';\nimport { HelloHandler } from '../app/modules/hello/HelloHandler.ts';\n\ndescribe('EventBus', () => {\n  it('should handle event', async () => {\n    // mock handler 捕获调用参数\n    const mockFn = async (msg: string) => {};\n    mm(HelloHandler.prototype, 'handle', mockFn);\n\n    await app.mockModuleContextScope(async (ctx) => {\n      const helloService = await ctx.getEggObject(HelloService);\n      const eventWaiter = await app.getEventWaiter();\n\n      // 1. 先注册等待（必须在 emit 之前）\n      const eventPromise = eventWaiter.await('helloEgg');\n\n      // 2. 触发业务逻辑（内部会 emit 事件）\n      helloService.hello();\n\n      // 3. 等待 handler 执行完成\n      await eventPromise;\n    });\n\n    // 4. 验证 handler 被调用及参数\n    assert.equal(mockFn.called, 1);\n    assert.deepStrictEqual(mockFn.lastCalledArguments, ['hello']);\n  });\n});\n```\n","packages/skills/egg-unittest/references/http-test.md":"# HTTP 接口测试\n\n## 常见错误\n\n| 错误写法                                                 | 正确写法                        | 说明                                          |\n| -------------------------------------------------------- | ------------------------------- | --------------------------------------------- |\n| POST 测试不加 `app.mockCsrf()`                           | 在 POST 前调用 `app.mockCsrf()` | 安全插件默认开启 CSRF，不 mock 会返回 403     |\n| `app.httpRequest().get('/').expect(200)` 不 return/await | 必须 `return` 或 `await`        | 否则断言不会执行，测试永远通过                |\n| `.expect({ foo: 'bar' })` 用于部分匹配                   | 使用 `result.body` 手动断言     | `.expect(body)` 是全量匹配（deepStrictEqual） |\n\n---\n\n## 基本用法\n\n通过 `app.httpRequest()` 发起 HTTP 请求，返回 SuperTest 对象：\n\n```typescript\nimport { app } from '@eggjs/mock/bootstrap';\n\ndescribe('UserController', () => {\n  it('should GET /api/users', () => {\n    return app.httpRequest().get('/api/users').expect(200).expect({ users: [] });\n  });\n});\n```\n\n---\n\n## POST 请求 + CSRF\n\nPOST/PUT/DELETE 请求需要先调用 `app.mockCsrf()` 跳过 CSRF 校验：\n\n```typescript\nit('should POST /api/users', () => {\n  app.mockCsrf();\n  return app\n    .httpRequest()\n    .post('/api/users')\n    .send({ name: 'test', email: 'test@example.com' })\n    .expect(200)\n    .expect({ id: '1', name: 'test' });\n});\n```\n\n表单提交使用 `.type('form')`：\n\n```typescript\nit('should POST form data', () => {\n  app.mockCsrf();\n  return app.httpRequest().post('/api/login').type('form').send({ username: 'admin', password: '123' }).expect(200);\n});\n```\n\n---\n\n## 请求构造\n\n```typescript\napp\n  .httpRequest()\n  .get('/api/users')\n  .set('Authorization', 'Bearer token123') // 设置 header\n  .set('Accept', 'application/json') // 设置 Accept\n  .query({ page: 1, limit: 10 }) // 查询参数\n  .expect(200);\n```\n\n---\n\n## 响应断言\n\n使用 `.expect()` 链式断言：\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\n\nit('should validate response', () => {\n  return app\n    .httpRequest()\n    .get('/api/users/1')\n    .expect(200) // 只校验状态码\n    .expect({ id: '1', name: 'test' }) // 只校验 body（deepStrictEqual）\n    .expect(200, { id: '1', name: 'test' }) // 状态码 + body 合并\n    .expect('hello world') // body 字符串匹配\n    .expect(/hello/) // body 正则匹配\n    .expect('content-type', /json/) // header 匹配\n    .expect([200, 302]) // 多状态码匹配（任一即可）\n    .expect((res) => {\n      // 自定义断言函数\n      assert(res.body.id);\n    });\n});\n```\n\n使用 `result` 做更灵活的断言：\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\n\nit('should validate response', async () => {\n  const result = await app.httpRequest().get('/api/users/1');\n\n  assert.equal(result.status, 200);\n  assert.equal(result.body.name, 'test');\n  assert(result.body.id);\n  assert.match(result.headers['content-type'], /json/);\n});\n```\n\n完整的请求构造和断言 API 可查看项目 node_modules 中 `@eggjs/supertest` 的类型定义。\n\n---\n\n## 端到端示例\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\n\ndescribe('test/controller/user.test.ts', () => {\n  describe('GET /api/users/:id', () => {\n    it('should return user', () => {\n      return app.httpRequest().get('/api/users/1').expect(200).expect({ id: '1', name: 'test' });\n    });\n\n    it('should return 404 when user not found', () => {\n      return app.httpRequest().get('/api/users/999').expect(404);\n    });\n  });\n\n  describe('POST /api/users', () => {\n    it('should create user', () => {\n      app.mockCsrf();\n      return app.httpRequest().post('/api/users').send({ name: 'new user', email: 'new@example.com' }).expect(201);\n    });\n\n    it('should return 422 with invalid params', () => {\n      app.mockCsrf();\n      return app.httpRequest().post('/api/users').send({ name: '' }).expect(422);\n    });\n  });\n});\n```\n","packages/skills/egg-unittest/references/mock.md":"# Mock 模式\n\n## 常见错误\n\n| 错误写法                       | 正确写法                                   | 说明                           |\n| ------------------------------ | ------------------------------------------ | ------------------------------ |\n| `mm(service, 'method', fn)`    | `mm(ServiceClass.prototype, 'method', fn)` | DI 对象需 mock 原型，不是实例  |\n| 手动写 `afterEach(mm.restore)` | 不需要                                     | egg-bin 自动注入 mock 恢复     |\n| `new Ajv()` mock 单独实例      | mock 原型方法                              | DI 容器管理的对象通过原型 mock |\n\n---\n\n## mm() — Mock Proto 方法\n\n最常用的 mock 方式，mock DI 对象的原型方法：\n\n```typescript\nimport assert from 'node:assert';\nimport { app, mm } from '@eggjs/mock/bootstrap';\nimport { UserService } from '../app/modules/user/UserService.ts';\nimport { OrderService } from '../app/modules/order/OrderService.ts';\n\ndescribe('OrderService', () => {\n  it('should mock user service', async () => {\n    mm(UserService.prototype, 'getById', async () => {\n      return { id: '1', name: 'mocked user' };\n    });\n\n    const orderService = await app.getEggObject(OrderService);\n    const result = await orderService.createForUser('1');\n    assert.equal(result.userName, 'mocked user');\n  });\n});\n```\n\nmock 函数会自动记录调用信息，可以用来断言调用参数：\n\n```typescript\nimport assert from 'node:assert';\nimport { app, mm } from '@eggjs/mock/bootstrap';\nimport { NotifyService } from '../app/modules/notify/NotifyService.ts';\nimport { OrderService } from '../app/modules/order/OrderService.ts';\n\nit('should call notify with correct args', async () => {\n  const mockFn = async (userId: string, message: string) => {};\n  mm(NotifyService.prototype, 'send', mockFn);\n\n  const orderService = await app.getEggObject(OrderService);\n  await orderService.create({ productId: '1' });\n\n  assert.equal(mockFn.called, 1); // 调用次数\n  assert.deepStrictEqual(mockFn.lastCalledArguments, ['user-1', '订单创建成功']); // 最后一次调用参数\n  // mockFn.calledArguments — 所有调用参数的数组\n});\n```\n\n---\n\n## mm.spy() — 不替换实现，只记录调用\n\n```typescript\nit('should spy on method', async () => {\n  mm.spy(NotifyService.prototype, 'send');\n\n  const orderService = await app.getEggObject(OrderService);\n  await orderService.create({ productId: '1' });\n\n  // 原方法正常执行，同时记录了调用信息\n  const sendFn = NotifyService.prototype.send;\n  assert.equal(sendFn.called, 1);\n  assert.equal(sendFn.lastCalledArguments[0], 'user-1');\n});\n```\n\n---\n\n## app.mockHttpclient() — Mock HttpClient 请求\n\nMock 通过 `@Inject() httpclient: HttpClient` 注入的 HttpClient 发送的请求：\n\n```typescript\nit('should mock external API', () => {\n  app.mockHttpclient('https://api.example.com/users', {\n    data: JSON.stringify({ name: 'test' }),\n  });\n\n  return app.httpRequest().get('/api/proxy/users').expect(200).expect({ name: 'test' });\n});\n```\n\n---\n\n## app.mockCsrf() — 跳过 CSRF\n\nPOST/PUT/DELETE 测试时跳过 CSRF 校验：\n\n```typescript\nit('should POST without CSRF error', () => {\n  app.mockCsrf();\n  return app.httpRequest().post('/api/users').send({ name: 'test' }).expect(200);\n});\n```\n\n---\n\n## Mock 恢复\n\negg-bin 自动注入 `@eggjs/mock/setup_vitest`，会在 `afterEach` 钩子中自动调用 `mm.restore()`，无需手动编写。\n","packages/skills/egg-unittest/references/service-test.md":"# Service / DI 对象测试\n\n## 常见错误\n\n| 错误写法                 | 正确写法                                  | 说明                |\n| ------------------------ | ----------------------------------------- | ------------------- |\n| `ctx.service.user.get()` | `ctx.getEggObject(UserService)`           | 旧写法，新项目用 DI |\n| 不 await `getEggObject`  | `const svc = await ctx.getEggObject(Svc)` | 返回 Promise        |\n\n---\n\n## Singleton 测试\n\n`@SingletonProto` 对象直接通过 `app.getEggObject()` 获取：\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\nimport { ConfigService } from '../app/modules/foo/ConfigService.ts';\n\ndescribe('ConfigService', () => {\n  it('should get config', async () => {\n    const configService = await app.getEggObject(ConfigService);\n    const value = configService.get('key');\n    assert.equal(value, 'expected');\n  });\n});\n```\n\n---\n\n## ContextProto 测试\n\n`@ContextProto` 对象可以直接通过 `app.getEggObject()` 获取，也可以在 `app.mockModuleContextScope` 中通过 `ctx.getEggObject()` 获取。后者会创建带 DI 生命周期的 ctx，退出时自动销毁：\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\nimport { UserService } from '../app/modules/user/UserService.ts';\n\ndescribe('UserService', () => {\n  it('should get user in context scope', async () => {\n    await app.mockModuleContextScope(async (ctx) => {\n      const userService = await ctx.getEggObject(UserService);\n      const user = await userService.getById('1');\n      assert(user);\n    });\n  });\n});\n```\n\n---\n\n## Mock 被注入的依赖\n\n当 Service A 依赖 Service B 时，mock B 的原型方法：\n\n```typescript\nimport assert from 'node:assert';\nimport { app, mm } from '@eggjs/mock/bootstrap';\nimport { OrderService } from '../app/modules/order/OrderService.ts';\nimport { PaymentService } from '../app/modules/payment/PaymentService.ts';\n\ndescribe('OrderService', () => {\n  it('should create order with mocked payment', async () => {\n    mm(PaymentService.prototype, 'charge', async () => {\n      return { transactionId: 'mock-tx-001' };\n    });\n\n    const orderService = await app.getEggObject(OrderService);\n    const order = await orderService.create({ productId: '1', amount: 100 });\n    assert.equal(order.transactionId, 'mock-tx-001');\n  });\n});\n```\n","packages/skills/egg/SKILL.md":"---\nname: egg\ndescription: 本技能用于处理 EGG 框架。它提供基于用户意图在核心概念、控制器和单元测试之间做选择的决策指导。作为所有 EGG 相关问题的入口点使用。覆盖模块架构、依赖注入、后台任务、EventBus 事件总线、AOP 切面编程、HTTP/MCP/Schedule 控制器、Ajv 参数校验、单元测试等。\nallowed-tools: Read\n---\n\n# EGG 决策指南\n\n## 概述\n\n本技能帮助根据用户意图和任务类型确定使用哪个专用的 EGG 技能。EGG 文档组织为两个主要领域：\n\n1. **核心概念**（`egg-core` skill）：模块架构、依赖注入、对象生命周期、EventBus 事件总线、AOP 切面编程\n2. **控制器**（`egg-controller` skill）：用于 API 端点的各种协议特定控制器\n3. **单元测试**（`egg-unittest` skill）：HTTP 接口测试、Service/DI 对象测试、Mock 模拟、BackgroundTask 和 EventBus 测试\n\n## 技能选择逻辑\n\n### 使用 `egg-core` skill 当用户询问：\n\n**用户询问关于：**\n\n- 模块架构和组织\n- `@SingletonProto` vs `@ContextProto` 的使用\n- 使用 `@Inject` 的依赖注入\n- 对象生命周期和实例化\n- 模块之间的访问控制（`AccessLevel`）\n- 模块配置（`module.yml`、`package.json`）\n- 使用限定符解决命名冲突\n- 请求返回后执行异步任务（BackgroundTaskHelper）\n- 事件驱动架构（EventBus、@Event）\n- AOP 切面编程（@Advice、@Pointcut、@Crosscut）\n\n**触发关键词：**\n\n- module、workspace、modules\n- singleton、单例、@SingletonProto\n- context、request context、@ContextProto\n- inject、injection、dependency injection、@Inject\n- prototype、lifecycle、实例化\n- background task、异步任务、后台任务、BackgroundTaskHelper\n- eventbus、event bus、事件总线、事件驱动、@Event、emit、发布订阅、解耦\n- aop、切面、aspect、advice、pointcut、crosscut、拦截器、横切关注点\n- access level、private、public、@ModuleQualifier\n- configuration、module config\n\n**示例查询：**\n\n- \"如何在 EGG 中创建模块？\"\n- \"SingletonProto 和 ContextProto 有什么区别？\"\n- \"如何注入服务？\"\n- \"如何访问其他模块的对象？\"\n- \"EGG 中的 AccessLevel 是什么？\"\n- \"如何用 EventBus 解耦异步任务？\"\n- \"EventBus 和 BackgroundTaskHelper 有什么区别？\"\n- \"如何用 AOP 给所有 Service 加日志？\"\n\n### 使用 `egg-controller` skill 当用户询问：\n\n**用户询问关于：**\n\n- 创建 API 端点或接口\n- 实现特定协议处理器（HTTP、MCP 等）\n- 连接到外部系统或客户端\n- 处理传入的请求/响应\n- 控制器级别的装饰器和模式\n- 参数校验（Ajv + TypeBox）\n- 控制器中间件（Middleware，函数式或 AOP 写法）\n\n**触发关键词：**\n\n- controller、控制器\n- HTTP、API、REST、endpoint\n- MCP、LLM、AI、tool\n- schedule、timer、cron、scheduled、定时\n- SSE、streaming、server-sent events\n- validate、校验、参数校验、ajv、typebox、schema\n- middleware、中间件、拦截器、洋葱模型、@Middleware\n\n**示例查询：**\n\n- \"如何创建 HTTP controller？\"\n- \"如何实现 MCP 接口？\"\n- \"怎么实现定时任务？\"\n- \"帮我给接口加上参数校验\"\n- \"如何给控制器加中间件？\"\n- \"怎么写一个鉴权中间件？\"\n\n### 使用 `egg-unittest` skill 当用户询问：\n\n**用户询问关于：**\n\n- 编写单元测试或集成测试\n- 使用 @eggjs/mock 进行测试\n- 测试 HTTP 接口（app.httpRequest）\n- 测试 Service/DI 对象\n- Mock 数据或依赖\n- 测试 BackgroundTaskHelper 或 EventBus\n\n**触发关键词：**\n\n- test、测试、单测、单元测试、unittest、unit test\n- mock、mm、@eggjs/mock、mockCsrf、mockHttpclient、mockSession、mockContext\n- httpRequest、supertest\n- getEggObject、mockModuleContextScope\n- eventWaiter、BackgroundTaskHelper 测试\n- vitest、describe、it、beforeAll\n\n**示例查询：**\n\n- \"如何写单元测试？\"\n- \"如何测试 HTTP 接口？\"\n- \"如何 mock 一个 Service？\"\n- \"POST 请求测试报 CSRF 错误\"\n- \"如何测试 ContextProto 的 Service？\"\n- \"怎么测试后台任务是否执行完成？\"\n- \"怎么测试 EventBus 事件是否被正确处理？\"\n\n---\n\n## 决策框架\n\n### 步骤 1：识别意图类型\n\n询问：**用户是在询问构建块/框架内部 OR 实现特定接口？**\n\n**构建块/框架内部** → 使用 `egg-core` skill\n\n- 理解 EGG 如何工作\n- 组织代码结构\n- 管理对象生命周期\n- 设置模块\n\n**实现特定接口** → 使用 `egg-controller` skill\n\n- 创建 API/端点\n- 处理不同协议\n- 处理请求/响应\n\n### 步骤 2：检查模糊意图\n\n如果用户的意图可能是核心 OR 控制器（例如，\"如何实现一个需要跨模块访问的服务？\"）：\n\n**决策优先级**：核心概念优先\n\n理由：即使服务将在控制器中使用，关于跨模块访问（`AccessLevel`）的基本问题是一个核心概念。一旦理解了核心结构，用户就可以在控制器中应用它。\n\n**行动**：\n\n1. 使用 `egg-core` skill\n2. 解释概念（例如，`AccessLevel.PUBLIC`）\n3. 核心解释后，建议：\"如果你需要在特定控制器中使用它，请使用 `egg-controller` skill\n\n### 步骤 3：协议/用例特定指示器\n\n| 协议/用例               | 主要技能         | 次要技能 |\n| ----------------------- | ---------------- | -------- |\n| HTTP API                | `egg-controller` | -        |\n| MCP                     | `egg-controller` | -        |\n| Scheduled Tasks         | `egg-controller` | -        |\n| Parameter validation    | `egg-controller` | -        |\n| Controller Middleware   | `egg-controller` | -        |\n| Background tasks        | `egg-core`       | -        |\n| Event-driven / EventBus | `egg-core`       | -        |\n| AOP / 切面编程          | `egg-core`       | -        |\n| Cross-module injection  | `egg-core`       | -        |\n| Module structure        | `egg-core`       | -        |\n| Object lifecycle        | `egg-core`       | -        |\n| Unit Testing            | `egg-unittest`   | -        |\n| Mock / Test helpers     | `egg-unittest`   | -        |\n\n## 冲突解决规则\n\n### 规则 1：基础优先\n\n当问题同时涉及核心概念 AND 控制器实现时：\n\n- **示例**：\"如何实现一个 HTTP 控制器可以使用的单例服务？\"\n- **决策**：从 `egg-core` skill 开始解释 SingletonProto 和 AccessLevel\n- **后续**：\"现在你理解了服务定义，使用 `egg-controller` skill 实现注入此服务的 HTTP 控制器。\"\n\n### 规则 2：显式覆盖\n\n如果用户明确提及特定控制器类型：\n\n- **示例**：\"如何使用 HTTPController 配合 ContextProto 服务？\"\n- **决策**：使用 `egg-controller` skill（HTTPController 是显式的）\n- **后续**：解释 HTTPController 实现，如果需要简要提及来自核心概念的 ContextProto\n\n### 规则 3：学习语境\n\n如果用户问\"什么是 X？\"或\"Y 如何工作？\"：\n\n- **核心概念问题** → 使用 `egg-core` skill\n- **控制器类型问题** → 使用 `egg-controller` skill\n- **一般 EGG 问题** → 使用本技能的决策框架\n\n如果用户问\"如何实现 X？\"或\"给我看 Y 的代码？\"：\n\n- **实现特定问题** → 根据决策框架加载特定 skill\n\n## 快速参考表\n\n| 用户意图             | 关键词                                  | 使用技能               |\n| -------------------- | --------------------------------------- | ---------------------- |\n| Module architecture  | module、workspace、organization         | `egg-core` skill       |\n| Object lifecycle     | singleton、context、lifecycle           | `egg-core` skill       |\n| Dependency injection | inject、@Inject、dependency             | `egg-core` skill       |\n| Access control       | private、public、cross-module           | `egg-core` skill       |\n| Background tasks     | background task、异步任务、后台任务     | `egg-core` skill       |\n| Event-driven         | eventbus、事件总线、@Event、emit        | `egg-core` skill       |\n| AOP 切面编程         | aop、切面、advice、pointcut、crosscut   | `egg-core` skill       |\n| HTTP endpoints       | HTTP、API、REST                         | `egg-controller` skill |\n| LLM/AI integration   | MCP、tool、prompt                       | `egg-controller` skill |\n| Scheduling           | schedule、cron、timer                   | `egg-controller` skill |\n| Param validation     | validate、校验、ajv、typebox、schema    | `egg-controller` skill |\n| Middleware 中间件    | middleware、中间件、拦截器、@Middleware | `egg-controller` skill |\n| Unit testing         | test、mock、unittest、单测              | `egg-unittest` skill   |\n| Mock dependencies    | mock、mm、mockCsrf、mockHttpclient      | `egg-unittest` skill   |\n\n---\n\n## 示例\n\n### 示例 1：明确的核心意图\n\n**用户**：\"@SingletonProto 和 @ContextProto 有什么区别？\"\n\n**分析**：问题关于核心装饰器和对象生命周期\n**决策**：使用 `egg-core` skill\n\n**用户**：\"如何在 EGG 中创建模块？\"\n\n**分析**：问题关于模块架构（核心概念）\n**决策**：使用 `egg-core` skill\n\n### 示例 2：明确的控制器意图\n\n**用户**：\"如何创建返回 JSON 的 HTTP controller？\"\n\n**分析**：问题关于实现特定协议（HTTP）\n**决策**：使用 `egg-controller` skill\n\n### 示例 3：模糊意图（核心 > 控制器）\n\n**用户**：\"我需要创建一个可以被 HTTP 控制器使用的服务。如何实现？\"\n\n**分析**：用户需要理解核心概念（跨模块访问）AND 控制器实现\n**决策**：首先使用 `egg-core` skill（基础）\n\n**响应**：\n\n1. 解释 `@SingletonProto` 配合 `AccessLevel.PUBLIC` 使服务可访问\n2. 展示如何注入服务：`@Inject() myService: MyService`\n3. 后续：\"现在你可以在 HTTPController 中注入此服务。实现详情请使用 `egg-controller` skill。\"\n\n### 示例 4：显式控制器加上核心知识\n\n**用户**：\"如何在 HTTPController 中使用 @Inject 访问用户服务？\"\n\n**分析**：用户明确提及 HTTPController（控制器类型）但询问 @Inject（核心概念）\n**决策**：使用 `egg-controller` skill（HTTPController 是明确意图）\n\n**响应**：\n\n1. 展示配合 `@Inject` 的 HTTPController 实现\n2. 简要解释 @Inject 如何工作（核心概念摘要）\n3. 注意：\"包含限定符的详细 @Inject 使用，请使用 `egg-core` skill\"\n\n### 示例 5：测试相关\n\n**用户**：\"帮我写个 UserController 的单元测试\"\n\n**分析**：问题关于编写测试代码\n**决策**：使用 `egg-unittest` skill\n\n**用户**：\"POST 请求测试报 403 错误\"\n\n**分析**：测试中遇到 CSRF 问题\n**决策**：使用 `egg-unittest` skill\n\n## 路由最佳实践\n\n1. **优先考虑显式控制器提及**：如果用户命名特定控制器（HTTP），即使涉及核心概念也使用控制器技能\n\n2. **基础先行**：如果实现前需要理解核心概念，先解释核心概念\n\n3. **简短上下文可以接受**：当路由到一个技能时，提及另一个技能的存在以供后续问题\n\n4. **混合响应可接受**：当意图真正混合时，提供两个技能的简短上下文但专注于主要意图\n\n5. **渐进式披露**：除非明确要求，不要同时加载两个技能。让用户引导探索\n\n---\n\n## 技能交互\n\n本技能（`egg` skill）应该：\n\n- 为框架内部概念路由到 `egg-core` skill\n- 为协议特定实现路由到 `egg-controller` skill\n- 为测试相关问题路由到 `egg-unittest` skill\n- 当意图模糊时提供决策指导\n- 当存在有用的上下文时交叉引用技能\n","packages/skills/eval/evals-egg-controller.json":"{\n  \"skill_name\": \"egg-controller\",\n  \"description\": \"控制器评测：覆盖 http-controller、mcp-controller、schedule、ajv-validate、middleware\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"帮我写一个 POST /api/users 接口，接收 JSON body 创建用户，返回 201\",\n      \"expected_output\": \"@HTTPController + @HTTPMethod POST + @HTTPBody + ctx.status = 201，完整可运行代码\"\n    },\n    {\n      \"id\": 2,\n      \"prompt\": \"帮我实现 GET /api/users/:userId/posts/:postId，获取用户的某篇文章\",\n      \"expected_output\": \"@HTTPParam() userId + @HTTPParam() postId，路径参数定义和获取的完整代码\"\n    },\n    {\n      \"id\": 3,\n      \"prompt\": \"我有个搜索接口 /api/search?tag=tech&tag=dev，需要获取 tag 的全部值\",\n      \"expected_output\": \"使用 @HTTPQueries({ name: 'tag' }) 获取 string[] 数组，不是 @HTTPQuery\"\n    },\n    {\n      \"id\": 4,\n      \"prompt\": \"帮我写个 SSR 页面，GET / 返回完整的 HTML\",\n      \"expected_output\": \"@HTTPController + ctx.type = 'html' + return HTML 字符串\"\n    },\n    {\n      \"id\": 5,\n      \"prompt\": \"帮我实现流式输出，一段一段返回 HTML 内容\",\n      \"expected_output\": \"async generator + Readable.from()，设置 ctx.type，完整代码\"\n    },\n    {\n      \"id\": 6,\n      \"prompt\": \"我有两个路由 /api/:name 和 /api/health，请求 /api/health 匹配到了 :name，帮我看看代码\",\n      \"expected_output\": \"静态路径优先级高于参数路径应该自动匹配，如果冲突可用 priority 参数调整\",\n      \"files\": [\n        {\n          \"path\": \"app/apiModule/ApiController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPParam } from 'egg';\\n\\n@HTTPController({ path: '/api' })\\nexport class ApiController {\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/:name' })\\n  async getByName(@HTTPParam() name: string) {\\n    return { name };\\n  }\\n\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/health' })\\n  async health() {\\n    return { status: 'ok' };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 7,\n      \"prompt\": \"帮我给这个 Controller 加一个认证中间件的逻辑，从 header 里取 Authorization 做校验\",\n      \"expected_output\": \"使用 @HTTPHeaders() 获取 headers，key 用小写 headers['authorization']\",\n      \"files\": [\n        {\n          \"path\": \"app/apiModule/UserController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum } from 'egg';\\n\\n@HTTPController({ path: '/api/users' })\\nexport class UserController {\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })\\n  async list() {\\n    return { users: [] };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 8,\n      \"prompt\": \"帮我写一个完整的 CRUD 模块：userModule，包含 UserService 和 UserController，支持 GET/POST/PUT/DELETE\",\n      \"expected_output\": \"完整文件结构：package.json + UserService(@SingletonProto + PUBLIC) + UserController(@HTTPController)，四个 HTTP 方法的完整代码\"\n    },\n    {\n      \"id\": 9,\n      \"prompt\": \"基于这个 Service，帮我写一个 HTTPController 暴露查询和创建接口\",\n      \"expected_output\": \"@HTTPController + 两个 @HTTPMethod（GET 用 @HTTPParam，POST 用 @HTTPBody）+ @Inject 注入 ProductService\",\n      \"files\": [\n        {\n          \"path\": \"app/productModule/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"productModule\\\",\\n  \\\"eggModule\\\": { \\\"name\\\": \\\"productModule\\\" }\\n}\"\n        },\n        {\n          \"path\": \"app/productModule/ProductService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class ProductService {\\n  async findById(id: string) {\\n    return { id, name: 'Product ' + id, price: 99 };\\n  }\\n\\n  async create(data: { name: string; price: number }) {\\n    return { id: 'new-id', ...data };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 10,\n      \"prompt\": \"帮我实现一个 MCP Tool，让 AI 能搜索我们的内部文档\",\n      \"expected_output\": \"@MCPController() + @MCPTool() + @ToolArgsSchema + Service 注入，import 从 @eggjs/tegg，zod 从 @eggjs/tegg/zod\"\n    },\n    {\n      \"id\": 11,\n      \"prompt\": \"MCP Tool 的参数 Schema 写法有问题，帮我看看\",\n      \"expected_output\": \"指出不应该用 z.object() 包装，直接用普通对象 { name: z.string() }\",\n      \"files\": [\n        {\n          \"path\": \"app/docModule/DocMCPController.ts\",\n          \"content\": \"import { MCPController, MCPTool, MCPToolResponse, ToolArgs, ToolArgsSchema, Inject } from '@eggjs/tegg';\\nimport { z } from '@eggjs/tegg/zod';\\n\\nconst SearchSchema = z.object({\\n  keyword: z.string({ description: 'search keyword' }),\\n  limit: z.number().optional(),\\n});\\n\\n@MCPController()\\nexport class DocMCPController {\\n  @MCPTool({ description: 'Search internal docs' })\\n  async search(\\n    @ToolArgsSchema(SearchSchema) args: ToolArgs<typeof SearchSchema>,\\n  ): Promise<MCPToolResponse> {\\n    return { content: [{ type: 'text', text: 'results' }] };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 12,\n      \"prompt\": \"我的 MCP Tool 要处理一个大数据导出，可能要跑几分钟，怎么给客户端推送进度\",\n      \"expected_output\": \"@Extra() 获取 ToolExtra，用 sendNotification 推送进度，给出完整代码\"\n    },\n    {\n      \"id\": 13,\n      \"prompt\": \"帮我写一个 MCP Resource，让 AI 能读取指定用户的 profile\",\n      \"expected_output\": \"@MCPResource({ template: [...] }) + uri 参数解析，完整代码\"\n    },\n    {\n      \"id\": 14,\n      \"prompt\": \"帮我写个定时任务，每天凌晨 3 点清理过期数据\",\n      \"expected_output\": \"@Schedule<CronParams> + cron: '0 0 3 * * *' + ScheduleType.WORKER，import 从 egg/schedule\"\n    },\n    {\n      \"id\": 15,\n      \"prompt\": \"定时任务放在 app/schedule 目录下不生效，帮我看看\",\n      \"expected_output\": \"指出不能放在 app/schedule 下（和 egg 默认扫描冲突），应放在模块目录中\",\n      \"files\": [\n        {\n          \"path\": \"app/schedule/CleanTask.ts\",\n          \"content\": \"import { Inject, Logger } from 'egg';\\nimport { IntervalParams, Schedule, ScheduleType } from 'egg/schedule';\\n\\n@Schedule<IntervalParams>({\\n  type: ScheduleType.WORKER,\\n  scheduleData: { interval: '1h' },\\n})\\nexport class CleanTask {\\n  @Inject()\\n  private logger: Logger;\\n\\n  async subscribe() {\\n    this.logger.info('cleaning...');\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 16,\n      \"prompt\": \"帮我写一个每 30 秒刷新缓存的定时任务，要求每个 worker 都执行\",\n      \"expected_output\": \"@Schedule<IntervalParams> + interval: '30s' + ScheduleType.ALL\"\n    },\n    {\n      \"id\": 17,\n      \"prompt\": \"基于这个 CacheService，帮我写一个定时任务每 5 分钟刷新一次缓存\",\n      \"expected_output\": \"@Schedule<IntervalParams> + interval: '5m' + @Inject() cacheService，import 从 egg/schedule\",\n      \"files\": [\n        {\n          \"path\": \"app/cacheModule/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"cacheModule\\\",\\n  \\\"eggModule\\\": { \\\"name\\\": \\\"cacheModule\\\" }\\n}\"\n        },\n        {\n          \"path\": \"app/cacheModule/CacheService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class CacheService {\\n  private cache = new Map<string, any>();\\n\\n  async refresh() {\\n    this.cache.clear();\\n    // reload from db\\n  }\\n\\n  get(key: string) {\\n    return this.cache.get(key);\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 18,\n      \"prompt\": \"帮我把这个老的 egg controller 改成 HTTPController 写法\",\n      \"expected_output\": \"用 @HTTPController + @HTTPMethod 重写，参数用装饰器获取，注入 Service 替代 ctx.service\",\n      \"files\": [\n        {\n          \"path\": \"app/controller/user.ts\",\n          \"content\": \"import { Controller } from 'egg';\\n\\nexport default class UserController extends Controller {\\n  async show() {\\n    const { ctx } = this;\\n    const id = ctx.params.id;\\n    const user = await ctx.service.user.find(id);\\n    ctx.body = { success: true, data: user };\\n  }\\n\\n  async create() {\\n    const { ctx } = this;\\n    const body = ctx.request.body;\\n    const user = await ctx.service.user.create(body);\\n    ctx.status = 201;\\n    ctx.body = { success: true, data: user };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 19,\n      \"prompt\": \"帮我给这个 POST /api/users 接口加上参数校验，name 必填且最多50字符，email 必须是邮箱格式\",\n      \"expected_output\": \"从 egg/ajv 导入 Type/Ajv/Static，定义 TypeBox Schema（name: Type.String + maxLength, email: Type.String + format: email），@Inject() ajv: Ajv，ajv.validate()\",\n      \"files\": [\n        {\n          \"path\": \"app/userModule/UserController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPBody } from 'egg';\\n\\n@HTTPController({ path: '/api/users' })\\nexport class UserController {\\n  @HTTPMethod({ method: HTTPMethodEnum.POST, path: '/' })\\n  async create(@HTTPBody() body: { name: string; email: string }) {\\n    return { id: '1', ...body };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 20,\n      \"prompt\": \"参数校验跑不起来，帮我看看代码哪里有问题\",\n      \"expected_output\": \"指出三个错误：1. 导入路径应从 egg/ajv 而非 typebox/ajv 2. 不应 new Ajv() 手动创建 3. 应使用 @Inject() 注入全局单例\",\n      \"files\": [\n        {\n          \"path\": \"app/userModule/UserController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPBody, Inject } from 'egg';\\nimport { Type, Static } from 'typebox';\\nimport Ajv from 'ajv';\\n\\nconst CreateUserSchema = Type.Object({\\n  name: Type.String({ maxLength: 50 }),\\n  email: Type.String({ format: 'email' }),\\n});\\n\\ntype CreateUserParams = Static<typeof CreateUserSchema>;\\n\\nconst ajv = new Ajv();\\n\\n@HTTPController({ path: '/api/users' })\\nexport class UserController {\\n  @HTTPMethod({ method: HTTPMethodEnum.POST, path: '/' })\\n  async create(@HTTPBody() body: CreateUserParams) {\\n    const valid = ajv.validate(CreateUserSchema, body);\\n    if (!valid) throw new Error('Validation failed');\\n    return { id: '1', ...body };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 21,\n      \"prompt\": \"帮我写一个注册接口的参数校验，要校验 email 格式、生日要是日期格式、用户ID要是 UUID 格式\",\n      \"expected_output\": \"使用 Type.String({ format: 'email' })、Type.String({ format: 'date' })、Type.String({ format: 'uuid' })，从 egg/ajv 导入\"\n    },\n    {\n      \"id\": 22,\n      \"prompt\": \"用户输入的名字前后经常有空格，怎么在参数校验时自动去掉空格\",\n      \"expected_output\": \"使用 TransformEnum.trim，从 egg/ajv 导入 TransformEnum，Type.String({ transform: [TransformEnum.trim] })\"\n    },\n    {\n      \"id\": 23,\n      \"prompt\": \"有些字段是选填的，比如用户的昵称和个人简介，校验时不传不应该报错\",\n      \"expected_output\": \"使用 Type.Optional() 包装选填字段，如 Type.Optional(Type.String())\"\n    },\n    {\n      \"id\": 24,\n      \"prompt\": \"帮我写一个校验 Schema，要求 tags 字段是一个元组，第一个元素是字符串类型的分类名，第二个元素是数字类型的权重\",\n      \"expected_output\": \"使用 Type.Tuple([Type.String(), Type.Number()])，AI 需要从 TypeBox 文档链接查找 Tuple 用法\"\n    },\n    {\n      \"id\": 25,\n      \"prompt\": \"基于这个 OrderService，帮我写一个创建订单的接口，需要做参数校验：productId 必填字符串，quantity 必填正整数，remark 选填\",\n      \"expected_output\": \"定义 Schema + @Inject() ajv: Ajv + ajv.validate() + @Inject() orderService，完整可运行代码\",\n      \"files\": [\n        {\n          \"path\": \"app/orderModule/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"orderModule\\\",\\n  \\\"eggModule\\\": { \\\"name\\\": \\\"orderModule\\\" }\\n}\"\n        },\n        {\n          \"path\": \"app/orderModule/OrderService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class OrderService {\\n  async create(data: { productId: string; quantity: number; remark?: string }) {\\n    return { orderId: 'order-' + Date.now(), ...data };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 26,\n      \"prompt\": \"帮我给这个 Controller 加个日志中间件，记录每个请求的耗时\",\n      \"expected_output\": \"使用 AOP 写法：定义 @Advice 类实现 IAdvice 的 around 方法，@Inject Logger，用 @Middleware 应用到 Controller。import Advice 从 egg/aop，Middleware 从 egg\",\n      \"files\": [\n        {\n          \"path\": \"app/modules/user/UserController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum } from 'egg';\\n\\n@HTTPController({ path: '/api/users' })\\nexport class UserController {\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })\\n  async list() {\\n    return { users: [] };\\n  }\\n\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/:id' })\\n  async show() {\\n    return { id: '1', name: 'test' };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 27,\n      \"prompt\": \"帮我写一个鉴权中间件，从 header 取 token 校验，校验失败抛异常，只应用到部分接口\",\n      \"expected_output\": \"AOP @Advice 类 + around 方法中校验 token + @Middleware 应用到方法级别（而非类级别），import Advice/IAdvice/AdviceContext 从 egg/aop\"\n    },\n    {\n      \"id\": 28,\n      \"prompt\": \"中间件代码报错 AOP and MiddlewareFunc can not be mixed，帮我看看哪里写错了\",\n      \"expected_output\": \"指出不能在同一个 @Middleware() 中混用函数和 Advice 类，需要分开写两个 @Middleware\",\n      \"files\": [\n        {\n          \"path\": \"app/modules/api/ApiController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum, Middleware } from 'egg';\\nimport { LogAdvice } from '../common/advice/LogAdvice.ts';\\nimport { countMw } from './middleware/count.ts';\\n\\n@HTTPController({ path: '/api' })\\n@Middleware(LogAdvice, countMw)\\nexport class ApiController {\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/hello' })\\n  async hello() {\\n    return { message: 'hello' };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 29,\n      \"prompt\": \"我想写一个中间件统一包装接口返回值为 { success: true, data: ... } 的格式\",\n      \"expected_output\": \"AOP @Advice 类 + around 方法中 await next() 拿到返回值后包装为 { success: true, data: result }，用 @Middleware 应用\"\n    },\n    {\n      \"id\": 30,\n      \"prompt\": \"AOP 中间件和函数式中间件有什么区别？什么时候用哪种\",\n      \"expected_output\": \"AOP 为推荐写法，支持 @Inject 注入依赖；函数式为旧版兼容写法，需从 ctx 手动获取依赖。新项目优先用 AOP\"\n    },\n    {\n      \"id\": 31,\n      \"prompt\": \"帮我把这个老的 egg 函数式中间件改成 AOP 写法\",\n      \"expected_output\": \"定义 @Advice 类 + 实现 around 方法，用 @Inject 注入 Logger 替代 ctx.logger，import 从 egg/aop\",\n      \"files\": [\n        {\n          \"path\": \"app/modules/api/middleware/log.ts\",\n          \"content\": \"import type { Context, Next } from 'egg';\\n\\nexport async function logMw(ctx: Context, next: Next): Promise<void> {\\n  const start = Date.now();\\n  await next();\\n  ctx.logger.info('request %s %s cost %dms', ctx.method, ctx.url, Date.now() - start);\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 32,\n      \"prompt\": \"我在 Advice 中用了一个实例属性 count 来统计请求数，但是多个请求之间数据串了，帮我看看\",\n      \"expected_output\": \"指出 Advice 默认是 Singleton，实例属性会被并发请求共享。请求级状态应使用 ctx.set()/ctx.get() 传递\",\n      \"files\": [\n        {\n          \"path\": \"app/modules/api/advice/RequestCountAdvice.ts\",\n          \"content\": \"import { Advice, IAdvice, AdviceContext } from 'egg/aop';\\n\\n@Advice()\\nexport class RequestCountAdvice implements IAdvice {\\n  private requestId = '';\\n\\n  async around(ctx: AdviceContext, next: () => Promise<any>): Promise<any> {\\n    this.requestId = Math.random().toString(36).slice(2);\\n    const result = await next();\\n    return { ...result, requestId: this.requestId };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 33,\n      \"prompt\": \"帮我看看这个 Controller 上的中间件执行顺序是什么样的\",\n      \"expected_output\": \"函数式和 AOP 分属两个独立执行阶段：先执行所有函数式中间件（类级别 countMw → 方法级别 timeMw），再执行所有 AOP 中间件（类级别 LogAdvice → 方法级别 AuthAdvice），最后执行 hello()。即 countMw → timeMw → LogAdvice → AuthAdvice → hello()，与装饰器书写顺序无关\",\n      \"files\": [\n        {\n          \"path\": \"app/modules/api/ApiController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum, Middleware } from 'egg';\\nimport { LogAdvice } from '../common/advice/LogAdvice.ts';\\nimport { AuthAdvice } from '../common/advice/AuthAdvice.ts';\\nimport { countMw } from './middleware/count.ts';\\nimport { timeMw } from './middleware/time.ts';\\n\\n@HTTPController({ path: '/api' })\\n@Middleware(countMw)\\n@Middleware(LogAdvice)\\nexport class ApiController {\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/hello' })\\n  @Middleware(timeMw)\\n  @Middleware(AuthAdvice)\\n  async hello() {\\n    return { message: 'hello' };\\n  }\\n}\"\n        }\n      ]\n    }\n  ]\n}\n","packages/skills/eval/evals-egg-core.json":"{\n  \"skill_name\": \"egg-core\",\n  \"description\": \"核心概念评测：覆盖 module、proto、inject、dynamic-inject、background-task、eventbus、aop\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"帮我新建一个 userModule，我是新项目从零开始\",\n      \"expected_output\": \"创建 package.json(eggModule.name)，驼峰命名，放在 app/ 下，给出完整目录结构\"\n    },\n    {\n      \"id\": 2,\n      \"prompt\": \"我们是老的 egg 项目，有大量 app/controller 和 app/service 代码，现在想引入 module 开发新功能，怎么组织目录？\",\n      \"expected_output\": \"保留原有 app/controller、app/service 不动，新增 module 放在 app/module/ 下\"\n    },\n    {\n      \"id\": 3,\n      \"prompt\": \"帮我写一个 module.yml 配置，本地和线上的数据库地址不同，怎么分环境管理\",\n      \"expected_output\": \"创建 module.yml 基础配置 + module.{env}.yml 环境配置，框架自动深度合并\"\n    },\n    {\n      \"id\": 4,\n      \"prompt\": \"我新加了一个模块，启动后发现没被加载，帮我看看什么问题\",\n      \"expected_output\": \"指出 eggModule.name 包含 - 不合法，应使用驼峰命名\",\n      \"files\": [\n        {\n          \"path\": \"app/user-module/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"user-module\\\",\\n  \\\"eggModule\\\": {\\n    \\\"name\\\": \\\"user-module\\\"\\n  }\\n}\"\n        },\n        {\n          \"path\": \"app/user-module/UserService.ts\",\n          \"content\": \"import { SingletonProto } from 'egg';\\n\\n@SingletonProto()\\nexport class UserService {\\n  async getUser() { return 'hello'; }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 5,\n      \"prompt\": \"帮我写一个 UserService，查询用户信息，需要被其他模块调用\",\n      \"expected_output\": \"使用 @SingletonProto({ accessLevel: AccessLevel.PUBLIC })，包含完整代码\"\n    },\n    {\n      \"id\": 6,\n      \"prompt\": \"帮我实现一个请求级别的 TraceContext，存 traceId 和 userId，多个 Service 之间共享\",\n      \"expected_output\": \"使用 @ContextProto()，每个请求独立实例，其他 Service 通过 @Inject 共享同一个实例\"\n    },\n    {\n      \"id\": 7,\n      \"prompt\": \"我的 SingletonProto 需要在启动时连接 Redis，连接成功后才能对外提供服务，怎么写？\",\n      \"expected_output\": \"使用 @LifecycleInit() 做异步初始化，@LifecycleDestroy() 关闭连接，给出完整代码\"\n    },\n    {\n      \"id\": 8,\n      \"prompt\": \"SingletonProto 能注入 ContextProto 吗？我需要在单例服务里获取当前请求的用户信息\",\n      \"expected_output\": \"可以注入，框架通过 lazy getter/Proxy 自动处理，每次访问从当前请求上下文动态解析\"\n    },\n    {\n      \"id\": 9,\n      \"prompt\": \"启动报错 EggPrototypeNotFound，帮我看看代码哪里有问题\",\n      \"expected_output\": \"指出 OrderService 跨模块注入了 UserService 但 UserService 的 accessLevel 是 PRIVATE，应改为 PUBLIC\",\n      \"files\": [\n        {\n          \"path\": \"app/userModule/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"userModule\\\",\\n  \\\"eggModule\\\": { \\\"name\\\": \\\"userModule\\\" }\\n}\"\n        },\n        {\n          \"path\": \"app/userModule/UserService.ts\",\n          \"content\": \"import { SingletonProto } from 'egg';\\n\\n@SingletonProto()\\nexport class UserService {\\n  async findUser(id: string) {\\n    return { id, name: 'test' };\\n  }\\n}\"\n        },\n        {\n          \"path\": \"app/orderModule/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"orderModule\\\",\\n  \\\"eggModule\\\": { \\\"name\\\": \\\"orderModule\\\" }\\n}\"\n        },\n        {\n          \"path\": \"app/orderModule/OrderService.ts\",\n          \"content\": \"import { SingletonProto, Inject } from 'egg';\\nimport { UserService } from '../userModule/UserService.ts';\\n\\n@SingletonProto()\\nexport class OrderService {\\n  @Inject()\\n  userService: UserService;\\n\\n  async createOrder(userId: string) {\\n    const user = await this.userService.findUser(userId);\\n    return { orderId: '1', user };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 10,\n      \"prompt\": \"注入的对象不对，我期望注入 FooService 但拿到的不是，帮我看看\",\n      \"expected_output\": \"指出 IFooService 是 interface 无运行时元信息，回退到属性名 fooHelper 匹配，应改用 class 类型或 @Inject({ name: 'fooService' })\",\n      \"files\": [\n        {\n          \"path\": \"app/bizModule/FooService.ts\",\n          \"content\": \"import { SingletonProto } from 'egg';\\n\\nexport interface IFooService {\\n  doWork(): Promise<string>;\\n}\\n\\n@SingletonProto()\\nexport class FooService implements IFooService {\\n  async doWork() { return 'done'; }\\n}\"\n        },\n        {\n          \"path\": \"app/bizModule/BarService.ts\",\n          \"content\": \"import { SingletonProto, Inject } from 'egg';\\nimport type { IFooService } from './FooService.ts';\\n\\n@SingletonProto()\\nexport class BarService {\\n  @Inject()\\n  fooHelper: IFooService;  // 期望注入 FooService\\n\\n  async run() {\\n    return this.fooHelper.doWork();\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 11,\n      \"prompt\": \"帮我写注入 config 和 logger 的代码，还要注入自定义的 bizLogger\",\n      \"expected_output\": \"@Inject() config: EggAppConfig + @Inject() logger: Logger + @Inject() bizLogger: Logger，给出完整代码\"\n    },\n    {\n      \"id\": 12,\n      \"prompt\": \"帮我把这个老的 egg Service 改成 module 写法\",\n      \"expected_output\": \"用 @SingletonProto 重写，@Inject() 注入依赖替代 this.ctx/this.app，去掉 extends Service\",\n      \"files\": [\n        {\n          \"path\": \"app/service/OrderService.ts\",\n          \"content\": \"import { Service } from 'egg';\\n\\nexport default class OrderService extends Service {\\n  async create(data: any) {\\n    const user = await this.ctx.service.user.find(data.userId);\\n    this.ctx.logger.info('creating order for %s', user.name);\\n    const result = await this.app.httpclient.request('https://api.example.com/orders', {\\n      method: 'POST',\\n      data,\\n    });\\n    return result.data;\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 13,\n      \"prompt\": \"启动报错说注入方式冲突了，帮我看看\",\n      \"expected_output\": \"指出构造函数注入和属性注入混用了，不能混用，需改为统一使用一种方式\",\n      \"files\": [\n        {\n          \"path\": \"app/bizModule/MixedService.ts\",\n          \"content\": \"import { SingletonProto, Inject } from 'egg';\\n\\n@SingletonProto()\\nexport class MixedService {\\n  @Inject()\\n  logger: Logger;\\n\\n  constructor(\\n    @Inject() readonly config: EggAppConfig,\\n  ) {}\\n\\n  async run() {\\n    this.logger.info('config: %j', this.config);\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 14,\n      \"prompt\": \"有个依赖在某些环境不一定存在，注入时不想让应用挂掉\",\n      \"expected_output\": \"使用 @Inject({ optional: true }) 或 @InjectOptional()，不存在时为 undefined\"\n    },\n    {\n      \"id\": 15,\n      \"prompt\": \"两个模块都有 DataService，注入时报冲突了，帮我看看怎么改\",\n      \"expected_output\": \"使用 @ModuleQualifier('moduleName') 指定从哪个模块注入\",\n      \"files\": [\n        {\n          \"path\": \"app/cacheModule/DataService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class DataService {\\n  async get(key: string) { return 'from cache'; }\\n}\"\n        },\n        {\n          \"path\": \"app/dbModule/DataService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class DataService {\\n  async get(key: string) { return 'from db'; }\\n}\"\n        },\n        {\n          \"path\": \"app/bizModule/BizService.ts\",\n          \"content\": \"import { SingletonProto, Inject } from 'egg';\\n\\n@SingletonProto()\\nexport class BizService {\\n  @Inject()\\n  dataService: DataService;  // 报冲突\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 16,\n      \"prompt\": \"帮我实现策略模式，有邮件、短信、钉钉三种通知方式，运行时根据配置选择\",\n      \"expected_output\": \"使用动态注入：抽象类 AbstractNotifier + 枚举 + QualifierImplDecoratorUtil 装饰器 + EggObjectFactory.getEggObject()，给出所有文件完整代码\"\n    },\n    {\n      \"id\": 17,\n      \"prompt\": \"动态注入不生效，getEggObject 报找不到实现，帮我看看代码\",\n      \"expected_output\": \"指出 EmailNotifier 缺少 @SingletonProto() 装饰器，实现类必须同时有 Proto 装饰器和自定义装饰器\",\n      \"files\": [\n        {\n          \"path\": \"app/notifyModule/AbstractNotifier.ts\",\n          \"content\": \"export abstract class AbstractNotifier {\\n  abstract send(msg: string): Promise<void>;\\n}\"\n        },\n        {\n          \"path\": \"app/notifyModule/NotifyType.ts\",\n          \"content\": \"export enum NotifyType {\\n  EMAIL = 'EMAIL',\\n  SMS = 'SMS',\\n}\"\n        },\n        {\n          \"path\": \"app/notifyModule/decorator/Notify.ts\",\n          \"content\": \"import { ImplDecorator, QualifierImplDecoratorUtil } from 'egg';\\nimport { NotifyType } from '../NotifyType.ts';\\nimport { AbstractNotifier } from '../AbstractNotifier.ts';\\n\\nexport const NOTIFY_ATTRIBUTE = Symbol('NOTIFY_ATTRIBUTE');\\n\\nexport const Notify: ImplDecorator<AbstractNotifier, typeof NotifyType> =\\n  QualifierImplDecoratorUtil.generatorDecorator(AbstractNotifier, NOTIFY_ATTRIBUTE);\"\n        },\n        {\n          \"path\": \"app/notifyModule/impl/EmailNotifier.ts\",\n          \"content\": \"import { Notify } from '../decorator/Notify.ts';\\nimport { NotifyType } from '../NotifyType.ts';\\nimport { AbstractNotifier } from '../AbstractNotifier.ts';\\n\\n@Notify(NotifyType.EMAIL)\\nexport class EmailNotifier extends AbstractNotifier {\\n  async send(msg: string) {\\n    console.log('send email:', msg);\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 18,\n      \"prompt\": \"我想读取另一个模块的 moduleConfig，不是当前模块的\",\n      \"expected_output\": \"使用 @ConfigSourceQualifier('otherModule')，给出完整代码\"\n    },\n    {\n      \"id\": 19,\n      \"prompt\": \"请求返回后还要做一些事情，比如记日志、发通知，怎么实现\",\n      \"expected_output\": \"使用 BackgroundTaskHelper，@Inject() 注入，调用 run() 方法，从 egg 导入\"\n    },\n    {\n      \"id\": 20,\n      \"prompt\": \"用 BackgroundTaskHelper 在请求返回后异步上报埋点数据，帮我写个 MetricsService\",\n      \"expected_output\": \"@SingletonProto + @Inject() backgroundTaskHelper: BackgroundTaskHelper + run(async () => { ... })，从 egg 导入\"\n    },\n    {\n      \"id\": 21,\n      \"prompt\": \"用了 setTimeout 做后台任务，偶尔报错，帮我看看什么问题\",\n      \"expected_output\": \"指出 setTimeout 执行时上下文已释放，应改用 BackgroundTaskHelper.run()\",\n      \"files\": [\n        {\n          \"path\": \"app/orderModule/OrderService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel, Inject } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class OrderService {\\n  @Inject()\\n  private logger: any;\\n\\n  async createOrder(data: any) {\\n    const order = { id: Date.now(), ...data };\\n    // 请求返回后异步记录日志\\n    setTimeout(async () => {\\n      this.logger.info('Order created:', order.id);\\n      await this.sendNotification(order);\\n    }, 0);\\n    return order;\\n  }\\n\\n  private async sendNotification(order: any) {\\n    // 发送通知\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 22,\n      \"prompt\": \"后台任务默认超时是多少？我有个任务比较慢，怎么改成更长的时间\",\n      \"expected_output\": \"默认 5 秒，可通过 backgroundTaskHelper.timeout 按请求设置，或在 config.default.ts 中 backgroundTask.timeout 全局配置\"\n    },\n    {\n      \"id\": 23,\n      \"prompt\": \"帮我写一个订单创建接口，创建成功后异步发送通知邮件，不影响接口响应速度\",\n      \"expected_output\": \"OrderService 中 @Inject() backgroundTaskHelper，在 createOrder 方法中调用 run() 异步发送邮件\"\n    },\n    {\n      \"id\": 24,\n      \"prompt\": \"帮我把这个用 setTimeout 的后台逻辑改成正确的写法\",\n      \"expected_output\": \"将 setTimeout 替换为 backgroundTaskHelper.run()，注入 BackgroundTaskHelper\",\n      \"files\": [\n        {\n          \"path\": \"app/cacheModule/CacheService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel, Inject } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class CacheService {\\n  @Inject()\\n  private redisService: any;\\n\\n  async getData(key: string) {\\n    const data = await this.redisService.get(key);\\n    if (!data) {\\n      const fresh = await this.fetchFromDB(key);\\n      // 异步回写缓存，不阻塞响应\\n      setTimeout(async () => {\\n        await this.redisService.set(key, fresh, 3600);\\n      }, 0);\\n      return fresh;\\n    }\\n    return data;\\n  }\\n\\n  private async fetchFromDB(key: string) {\\n    return { key, value: 'from-db' };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 25,\n      \"prompt\": \"后台任务里抛了异常，但接口没报错也没看到日志，怎么回事\",\n      \"expected_output\": \"BackgroundTaskHelper 内部捕获所有异常并通过 logger.error 记录，日志前缀为 [BackgroundTaskHelper]，检查日志配置是否正确\",\n      \"files\": [\n        {\n          \"path\": \"app/reportModule/ReportService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel, Inject, BackgroundTaskHelper } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class ReportService {\\n  @Inject()\\n  private backgroundTaskHelper: BackgroundTaskHelper;\\n\\n  async generateReport(data: any) {\\n    this.backgroundTaskHelper.run(async () => {\\n      // 这里可能会抛异常\\n      const result = await this.callExternalAPI(data);\\n      await this.saveReport(result);\\n    });\\n    return { status: 'processing' };\\n  }\\n\\n  private async callExternalAPI(data: any) {\\n    throw new Error('API timeout');\\n  }\\n\\n  private async saveReport(result: any) {}\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 26,\n      \"prompt\": \"我有个后台任务可能要跑 30 秒，怎么配置超时不被框架提前中断\",\n      \"expected_output\": \"设置 backgroundTaskHelper.timeout = 30000 或全局 config backgroundTask.timeout: 30000\"\n    },\n    {\n      \"id\": 27,\n      \"prompt\": \"订单创建后要发通知、记日志、更新统计，但不想耦合在一起，怎么做\",\n      \"expected_output\": \"使用 EventBus 解耦：声明 Events 接口 + emit 触发事件 + 多个 @Event handler 分别处理通知、日志、统计\"\n    },\n    {\n      \"id\": 28,\n      \"prompt\": \"用 EventBus 实现订单事件，要求类型安全，帮我写 Events 声明和 handler\",\n      \"expected_output\": \"declare module 'egg' { interface Events { ... } } + @Event handler + 从 egg 导入 EventBus/Event，事件声明文件开头有 import 'egg'\"\n    },\n    {\n      \"id\": 29,\n      \"prompt\": \"EventBus 和 BackgroundTaskHelper 什么区别，我该用哪个\",\n      \"expected_output\": \"对比耦合度（强耦合 vs 解耦）、上下文（共享 vs 独立）、扩展性（改触发者 vs 加 handler），给出选型建议\"\n    },\n    {\n      \"id\": 30,\n      \"prompt\": \"EventBus 的 handler 没有被触发，帮我看看\",\n      \"expected_output\": \"指出 handler 文件不在 module 目录中，框架无法扫描到，应移到模块目录下\",\n      \"files\": [\n        {\n          \"path\": \"app/orderModule/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"orderModule\\\",\\n  \\\"eggModule\\\": { \\\"name\\\": \\\"orderModule\\\" }\\n}\"\n        },\n        {\n          \"path\": \"app/orderModule/OrderService.ts\",\n          \"content\": \"import { SingletonProto, Inject, EventBus } from 'egg';\\n\\n@SingletonProto()\\nexport class OrderService {\\n  @Inject()\\n  private eventBus: EventBus;\\n\\n  async createOrder(userId: string) {\\n    this.eventBus.emit('orderCreated', '123', userId);\\n    return { orderId: '123' };\\n  }\\n}\"\n        },\n        {\n          \"path\": \"app/handler/OrderNotificationHandler.ts\",\n          \"content\": \"import { Event } from 'egg';\\n\\n@Event('orderCreated')\\nexport class OrderNotificationHandler {\\n  async handle(orderId: string, userId: string): Promise<void> {\\n    console.log('notify:', orderId, userId);\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 31,\n      \"prompt\": \"帮我用 EventBus 实现用户注册后异步发送欢迎邮件\",\n      \"expected_output\": \"完整代码：Events 类型声明（import 'egg' + declare module）+ 注册 Service 中 emit + @Event handler 发送邮件\"\n    },\n    {\n      \"id\": 32,\n      \"prompt\": \"帮我把这个 BackgroundTaskHelper 的写法改成 EventBus 解耦\",\n      \"expected_output\": \"拆分为 EventBus.emit() + 独立 @Event handler 类，去掉 BackgroundTaskHelper\",\n      \"files\": [\n        {\n          \"path\": \"app/orderModule/OrderService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel, Inject, BackgroundTaskHelper } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class OrderService {\\n  @Inject()\\n  private backgroundTaskHelper: BackgroundTaskHelper;\\n\\n  @Inject()\\n  private notifyService: any;\\n\\n  @Inject()\\n  private statsService: any;\\n\\n  async createOrder(data: any) {\\n    const order = { id: Date.now(), ...data };\\n    this.backgroundTaskHelper.run(async () => {\\n      await this.notifyService.send(order);\\n      await this.statsService.record(order);\\n    });\\n    return order;\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 33,\n      \"prompt\": \"EventBus 的 cork/uncork 怎么用，我想批量操作完成后再统一触发事件\",\n      \"expected_output\": \"注入 ContextEventBus，cork() 开始缓冲 → emit 多个事件 → uncork() 释放，支持嵌套调用\"\n    },\n    {\n      \"id\": 34,\n      \"prompt\": \"所有 Service 的方法调用都要记录耗时日志，不想在每个方法里写\",\n      \"expected_output\": \"使用 AOP @Crosscut + PointcutType.NAME 正则匹配 Service，Advice 中 beforeCall 记录开始时间，afterFinally 计算耗时并打日志\"\n    },\n    {\n      \"id\": 35,\n      \"prompt\": \"用 @Pointcut 给 OrderService.createOrder 加事务包裹\",\n      \"expected_output\": \"@Advice 实现 around hook 管理事务（beginTransaction/commit/rollback）+ @Pointcut(TxAdvice) 装饰目标方法，从 egg/aop 导入\"\n    },\n    {\n      \"id\": 36,\n      \"prompt\": \"Pointcut 和 Crosscut 有什么区别，怎么选\",\n      \"expected_output\": \"Pointcut 在目标方法上声明，精确切入单个方法；Crosscut 在 Advice 类上声明匹配规则，批量切入多个类/方法\"\n    },\n    {\n      \"id\": 37,\n      \"prompt\": \"AOP 的 Advice 不生效，帮我看看\",\n      \"expected_output\": \"指出缺少 @Advice() 装饰器，@Crosscut 和 @Advice() 必须同时使用\",\n      \"files\": [\n        {\n          \"path\": \"app/logModule/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"logModule\\\",\\n  \\\"eggModule\\\": { \\\"name\\\": \\\"logModule\\\" }\\n}\"\n        },\n        {\n          \"path\": \"app/logModule/OperationLogAdvice.ts\",\n          \"content\": \"import { Crosscut, IAdvice, AdviceContext, PointcutType } from 'egg/aop';\\nimport { Inject, Logger } from 'egg';\\n\\n@Crosscut({\\n  type: PointcutType.NAME,\\n  className: /.*Service$/i,\\n  methodName: /^(create|update|delete)/,\\n})\\nexport class OperationLogAdvice implements IAdvice {\\n  @Inject()\\n  private logger: Logger;\\n\\n  async afterFinally(ctx: AdviceContext): Promise<void> {\\n    this.logger.info('called %s', String(ctx.method));\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 38,\n      \"prompt\": \"帮我用 AOP 实现统一的异常捕获和错误日志\",\n      \"expected_output\": \"Advice 实现 afterThrow hook 记录异常信息，使用 @Crosscut 或 @Pointcut 应用到目标方法，从 egg/aop 导入\"\n    },\n    {\n      \"id\": 39,\n      \"prompt\": \"adviceParams 怎么用，同一个 Advice 不同方法需要不同参数\",\n      \"expected_output\": \"@Pointcut 第二个参数传 { adviceParams: {...} }，Advice 中通过 ctx.adviceParams 获取\"\n    },\n    {\n      \"id\": 40,\n      \"prompt\": \"同一个方法上有 Crosscut 和 Pointcut 两个 Advice，执行顺序是什么，能控制吗\",\n      \"expected_output\": \"Crosscut 默认 order 100 先执行，Pointcut 默认 order 1000 后执行，可通过 order 参数调整顺序\"\n    },\n    {\n      \"id\": 41,\n      \"prompt\": \"帮我用 Crosscut 的 CUSTOM 模式，对所有 Repository 类的非 constructor 方法加监控\",\n      \"expected_output\": \"使用 PointcutType.CUSTOM + callback 函数匹配 Repository 类并排除 constructor，从 egg/aop 导入\"\n    }\n  ]\n}\n","packages/skills/eval/evals-egg-unittest.json":"{\n  \"skill_name\": \"egg-unittest\",\n  \"description\": \"单元测试评测：覆盖 http-test、service-test、mock、background-task-test、eventbus-test\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"帮我给这个 UserController 写个单元测试，要测 GET 和 POST 接口\",\n      \"expected_output\": \"import { app } from '@eggjs/mock/bootstrap'，app.httpRequest() 测 GET 和 POST，POST 前调用 app.mockCsrf()，return 或 await httpRequest\",\n      \"files\": [\n        {\n          \"path\": \"app/modules/user/UserController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPParam, HTTPBody, Inject } from 'egg';\\nimport { UserService } from './UserService.ts';\\n\\n@HTTPController({ path: '/api/users' })\\nexport class UserController {\\n  @Inject()\\n  userService: UserService;\\n\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/:id' })\\n  async show(@HTTPParam() id: string) {\\n    return await this.userService.getById(id);\\n  }\\n\\n  @HTTPMethod({ method: HTTPMethodEnum.POST, path: '/' })\\n  async create(@HTTPBody() body: { name: string; email: string }) {\\n    return await this.userService.create(body);\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 2,\n      \"prompt\": \"POST 接口测试报 403 错误，帮我看看\",\n      \"expected_output\": \"指出需要调用 app.mockCsrf() 跳过 CSRF 校验\",\n      \"files\": [\n        {\n          \"path\": \"test/controller/user.test.ts\",\n          \"content\": \"import { app } from '@eggjs/mock/bootstrap';\\n\\ndescribe('UserController', () => {\\n  it('should create user', () => {\\n    return app.httpRequest()\\n      .post('/api/users')\\n      .send({ name: 'test', email: 'test@example.com' })\\n      .expect(200);\\n  });\\n});\"\n        }\n      ]\n    },\n    {\n      \"id\": 3,\n      \"prompt\": \"httpRequest 的 expect 有哪些用法\",\n      \"expected_output\": \"expect(status)、expect(body)、expect(status, body) 合并、expect('header', value)、expect(/regex/)、expect([200, 302]) 多状态码、expect(fn) 自定义断言\"\n    },\n    {\n      \"id\": 4,\n      \"prompt\": \"测试写了但永远通过，不管接口返回啥都不报错\",\n      \"expected_output\": \"httpRequest 必须 return 或 await，否则断言不会执行\",\n      \"files\": [\n        {\n          \"path\": \"test/controller/home.test.ts\",\n          \"content\": \"import { app } from '@eggjs/mock/bootstrap';\\n\\ndescribe('HomeController', () => {\\n  it('should return 200', () => {\\n    app.httpRequest()\\n      .get('/')\\n      .expect(200)\\n      .expect('hello');\\n  });\\n});\"\n        }\n      ]\n    },\n    {\n      \"id\": 5,\n      \"prompt\": \"帮我写一个完整的 CRUD 接口测试\",\n      \"expected_output\": \"GET/POST/PUT/DELETE 完整测试，POST/PUT/DELETE 都要 mockCsrf，import 从 @eggjs/mock/bootstrap\"\n    },\n    {\n      \"id\": 6,\n      \"prompt\": \"基于这个 OrderService 帮我写完整的单元测试\",\n      \"expected_output\": \"app.getEggObject(OrderService) 获取实例 + mm mock PaymentService 依赖 + 断言返回值\",\n      \"files\": [\n        {\n          \"path\": \"app/modules/order/OrderService.ts\",\n          \"content\": \"import { SingletonProto, Inject } from 'egg';\\nimport { PaymentService } from '../payment/PaymentService.ts';\\n\\n@SingletonProto()\\nexport class OrderService {\\n  @Inject()\\n  paymentService: PaymentService;\\n\\n  async create(data: { productId: string; amount: number }) {\\n    const tx = await this.paymentService.charge(data.amount);\\n    return { orderId: 'order-1', transactionId: tx.id };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 7,\n      \"prompt\": \"ContextProto 的 Service 怎么在测试里获取实例\",\n      \"expected_output\": \"app.getEggObject(ContextProtoClass) 可以直接获取，也可以在 app.mockModuleContextScope 回调内用 ctx.getEggObject 获取（后者提供上下文生命周期管理，退出时自动销毁）\"\n    },\n    {\n      \"id\": 8,\n      \"prompt\": \"Singleton 和 ContextProto 的测试方式有什么区别\",\n      \"expected_output\": \"两者都可以用 app.getEggObject 直接获取。ContextProto 还可以在 app.mockModuleContextScope 回调内用 ctx.getEggObject 获取，提供上下文生命周期管理（退出时自动销毁、自动等待后台任务）\"\n    },\n    {\n      \"id\": 9,\n      \"prompt\": \"怎么测试一个依赖其他 Service 的 Service\",\n      \"expected_output\": \"mm(DependencyClass.prototype, 'method', fn) mock 依赖的原型方法，再 getEggObject 获取被测 Service\"\n    },\n    {\n      \"id\": 10,\n      \"prompt\": \"mockModuleContextScope 回调里的断言通过了但实际上没执行，测试直接过了\",\n      \"expected_output\": \"mockModuleContextScope 的回调必须 await，否则 scope 提前退出\",\n      \"files\": [\n        {\n          \"path\": \"test/service/user.test.ts\",\n          \"content\": \"import { app } from '@eggjs/mock/bootstrap';\\nimport assert from 'node:assert';\\nimport { UserService } from '../app/modules/user/UserService.ts';\\n\\ndescribe('UserService', () => {\\n  it('should get user', () => {\\n    app.mockModuleContextScope(async (ctx) => {\\n      const userService = await ctx.getEggObject(UserService);\\n      const user = await userService.getById('1');\\n      assert.equal(user.name, 'test');\\n    });\\n  });\\n});\"\n        }\n      ]\n    },\n    {\n      \"id\": 11,\n      \"prompt\": \"怎么 mock 一个被注入的 Service 的方法\",\n      \"expected_output\": \"mm(ServiceClass.prototype, 'methodName', async () => { ... })，import mm 从 @eggjs/mock/bootstrap，mock 原型而非实例\"\n    },\n    {\n      \"id\": 12,\n      \"prompt\": \"mm mock 了但没生效，帮我看看\",\n      \"expected_output\": \"mock 的是实例而非原型，应该用 mm(ServiceClass.prototype, ...)\",\n      \"files\": [\n        {\n          \"path\": \"test/service/order.test.ts\",\n          \"content\": \"import { app, mm } from '@eggjs/mock/bootstrap';\\nimport assert from 'node:assert';\\nimport { PaymentService } from '../app/modules/payment/PaymentService.ts';\\nimport { OrderService } from '../app/modules/order/OrderService.ts';\\n\\ndescribe('OrderService', () => {\\n  it('should mock payment', async () => {\\n    const paymentService = await app.getEggObject(PaymentService);\\n    mm(paymentService, 'charge', async () => ({ id: 'tx-mock' }));\\n\\n    const orderService = await app.getEggObject(OrderService);\\n    const order = await orderService.create({ productId: '1', amount: 100 });\\n    assert.equal(order.transactionId, 'tx-mock');\\n  });\\n});\"\n        }\n      ]\n    },\n    {\n      \"id\": 13,\n      \"prompt\": \"怎么验证 mock 的方法被调用了几次、传了什么参数\",\n      \"expected_output\": \"mockFn.called 调用次数，mockFn.lastCalledArguments 最后一次参数，mockFn.calledArguments 所有参数\"\n    },\n    {\n      \"id\": 14,\n      \"prompt\": \"怎么 mock 外部 HTTP 请求，我的 Service 里用了 httpclient 调第三方 API\",\n      \"expected_output\": \"app.mockHttpclient(url, { data: ... })，mock 通过 @Inject 注入的 HttpClient 发送的请求\"\n    },\n    {\n      \"id\": 15,\n      \"prompt\": \"mm 和 mm.spy 有什么区别\",\n      \"expected_output\": \"mm 替换方法实现，spy 保留原实现不替换，两者都会记录调用信息（called、lastCalledArguments）\"\n    },\n    {\n      \"id\": 16,\n      \"prompt\": \"怎么 mock 一个方法让它抛出异常\",\n      \"expected_output\": \"mm.error(Class.prototype, 'method', new Error('...'))\"\n    },\n    {\n      \"id\": 17,\n      \"prompt\": \"怎么测试后台任务是否执行完成\",\n      \"expected_output\": \"两种方式：1. mockModuleContextScope 退出时自动等待（内部触发 doPreDestroy） 2. app.backgroundTasksFinished() 手动等待。不应该用 sleep\"\n    },\n    {\n      \"id\": 18,\n      \"prompt\": \"用 httpRequest 触发的后台任务怎么等它完成\",\n      \"expected_output\": \"await app.backgroundTasksFinished()\"\n    },\n    {\n      \"id\": 19,\n      \"prompt\": \"测试里用了 TimerUtil.sleep 等后台任务，CI 上经常超时不稳定\",\n      \"expected_output\": \"用 app.backgroundTasksFinished() 替代 sleep，精确等待任务完成\",\n      \"files\": [\n        {\n          \"path\": \"test/service/task.test.ts\",\n          \"content\": \"import { app } from '@eggjs/mock/bootstrap';\\nimport assert from 'node:assert';\\nimport { TimerUtil } from '@eggjs/tegg-common-util';\\nimport { CountService } from '../app/modules/count/CountService.ts';\\n\\nit('should complete task', async () => {\\n  await app.httpRequest().get('/api/trigger-task').expect(200);\\n  await TimerUtil.sleep(3000);\\n  const countService = await app.getEggObject(CountService);\\n  assert.equal(countService.count, 1);\\n});\"\n        }\n      ]\n    },\n    {\n      \"id\": 20,\n      \"prompt\": \"backgroundTasksFinished 调了但断言还是失败，count 一直是 0\",\n      \"expected_output\": \"检查用于验证的 Service 是否是 @SingletonProto，ContextProto 每次 getEggObject 拿到的是不同实例\"\n    },\n    {\n      \"id\": 21,\n      \"prompt\": \"怎么测试 EventBus 事件是否被正确处理\",\n      \"expected_output\": \"app.getEventWaiter() 获取 waiter，eventWaiter.await('eventName') 必须在 emit 之前注册，用 mm mock handler 验证调用\"\n    },\n    {\n      \"id\": 22,\n      \"prompt\": \"eventWaiter.await 一直挂着不返回，测试超时了\",\n      \"expected_output\": \"eventWaiter.await 必须在 emit 之前调用，先注册等待再触发业务逻辑\",\n      \"files\": [\n        {\n          \"path\": \"test/event/hello.test.ts\",\n          \"content\": \"import { app } from '@eggjs/mock/bootstrap';\\nimport { HelloService } from '../app/modules/hello/HelloService.ts';\\n\\nit('should handle event', async () => {\\n  await app.mockModuleContextScope(async (ctx) => {\\n    const helloService = await ctx.getEggObject(HelloService);\\n    helloService.hello();\\n\\n    const eventWaiter = await app.getEventWaiter();\\n    await eventWaiter.await('helloEgg');\\n  });\\n});\"\n        }\n      ]\n    },\n    {\n      \"id\": 23,\n      \"prompt\": \"怎么验证 EventBus handler 收到的参数是否正确\",\n      \"expected_output\": \"mm mock handler 原型方法 + mockFn.lastCalledArguments 断言参数\"\n    },\n    {\n      \"id\": 24,\n      \"prompt\": \"帮我写一个 EventBus 事件的完整测试\",\n      \"expected_output\": \"完整示例：import bootstrap + mockModuleContextScope + getEventWaiter + await 在 emit 前 + mm mock handler + 断言\"\n    }\n  ]\n}\n","packages/skills/eval/evals-routing.json":"{\n  \"skill_name\": \"egg\",\n  \"description\": \"入口 skill 路由决策评测\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"帮我写一个接口\",\n      \"expected_output\": \"路由到 egg-controller skill\"\n    },\n    {\n      \"id\": 2,\n      \"prompt\": \"帮我写一个 GET /api/users/:id 的接口，返回用户信息\",\n      \"expected_output\": \"路由到 egg-controller skill\"\n    },\n    {\n      \"id\": 3,\n      \"prompt\": \"我想写个服务，不知道用 SingletonProto 还是 ContextProto\",\n      \"expected_output\": \"路由到 egg-core skill\"\n    },\n    {\n      \"id\": 4,\n      \"prompt\": \"帮我实现一个定时同步数据的功能，每 10 分钟跑一次\",\n      \"expected_output\": \"路由到 egg-controller skill\"\n    },\n    {\n      \"id\": 5,\n      \"prompt\": \"我的 @Inject 注入报错了，找不到对象\",\n      \"expected_output\": \"路由到 egg-core skill\"\n    },\n    {\n      \"id\": 6,\n      \"prompt\": \"帮我搭一个新模块，包含增删改查接口和 Service\",\n      \"expected_output\": \"路由到 egg-core skill（基础优先，先建模块再写接口）\"\n    },\n    {\n      \"id\": 7,\n      \"prompt\": \"我要给 AI 暴露几个 MCP Tool，查询订单和退款\",\n      \"expected_output\": \"路由到 egg-controller skill\"\n    },\n    {\n      \"id\": 8,\n      \"prompt\": \"不同环境的模块配置怎么管理\",\n      \"expected_output\": \"路由到 egg-core skill\"\n    },\n    {\n      \"id\": 9,\n      \"prompt\": \"帮我用事件总线解耦订单通知\",\n      \"expected_output\": \"路由到 egg-core skill\"\n    },\n    {\n      \"id\": 10,\n      \"prompt\": \"EventBus 和 BackgroundTaskHelper 怎么选\",\n      \"expected_output\": \"路由到 egg-core skill\"\n    },\n    {\n      \"id\": 11,\n      \"prompt\": \"帮我用 AOP 给所有 Service 加日志\",\n      \"expected_output\": \"路由到 egg-core skill\"\n    }\n  ]\n}\n","packages/skills/package.json":"{\n  \"name\": \"@eggjs/skills\",\n  \"version\": \"4.2.0\",\n  \"description\": \"agent skills for egg\",\n  \"keywords\": [\n    \"egg\",\n    \"skill\"\n  ],\n  \"homepage\": \"https://github.com/eggjs/egg/tree/next/packages/skills\",\n  \"bugs\": {\n    \"url\": \"https://github.com/eggjs/egg/issues\"\n  },\n  \"license\": \"MIT\",\n  \"author\": \"eggjs\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/eggjs/egg.git\",\n    \"directory\": \"packages/skills\"\n  },\n  \"files\": [\n    \"egg-controller/**/*.md\",\n    \"egg-core/**/*.md\",\n    \"egg-unittest/**/*.md\",\n    \"egg/**/*.md\"\n  ],\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"engines\": {\n    \"node\": \">=22.18.0\"\n  }\n}\n","packages/supertest/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fsupertest%2FREADME.md -->","packages/tsconfig/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Ftsconfig%2FREADME.md -->","packages/utils/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Futils%2FREADME.md -->","plugins/cors/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fcors%2FREADME.md -->","plugins/development/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fdevelopment%2FREADME.md -->","plugins/jsonp/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fjsonp%2FREADME.md -->","plugins/logrotator/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Flogrotator%2FREADME.md -->","plugins/mock/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fmock%2FREADME.md -->","plugins/multipart/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fmultipart%2FREADME.md -->","plugins/onerror/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fonerror%2FREADME.md -->","plugins/redis/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fredis%2FREADME.md -->","plugins/schedule/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fschedule%2FREADME.md -->","plugins/security/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fsecurity%2FREADME.md -->","plugins/session/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fsession%2FREADME.md -->","plugins/static/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fstatic%2FREADME.md -->","plugins/status/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fstatus%2FREADME.md -->","plugins/tracer/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Ftracer%2FREADME.md -->","plugins/typebox-validate/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Ftypebox-validate%2FREADME.md -->","plugins/view-nunjucks/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fview-nunjucks%2FREADME.md -->","plugins/view/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fview%2FREADME.md -->","plugins/watcher/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fwatcher%2FREADME.md -->","tegg/AGENTS.md":"# AGENTS.md\n\nFollow the shared repository guidance in [the root AGENTS.md](../AGENTS.md).\nThis file adds guidance for working in `tegg/`.\n\n## Overview\n\nTegg is a modular IoC (Inversion of Control) framework for Egg.js, providing dependency injection, lifecycle management, and plugin architecture. It's designed for building large-scale, maintainable Node.js applications using TypeScript decorators.\n\n**Requirements:**\n\n- Node.js >= 22.18.0\n- ESM only (no CommonJS)\n- egg >= 4.1.0\n\n## Monorepo Structure\n\n**IMPORTANT:** Tegg is part of the main [Egg.js monorepo](https://github.com/eggjs/egg). All build, test, and version management commands should be run from the monorepo root.\n\nThe tegg packages are organized as follows within the main monorepo:\n\n```\ncore/          # 24 core packages - decorators, runtime, metadata, loaders\nplugin/        # 10 plugin packages - Egg.js plugins that integrate core functionality\nstandalone/    # 1 standalone package - standalone runtime without Egg.js\n```\n\n**Dependency Management:**\n\n- Uses utoo workspaces with `catalog:` protocol for shared external dependencies\n- Uses `workspace:*` protocol for internal monorepo dependencies (both tegg and egg packages)\n- All shared dependency versions centralized in the root `.utoo.toml` (not in tegg/)\n- `catalogMode: prefer` set in root `.npmrc` for automatic catalog usage\n- Tegg workspace patterns are defined in root package.json as `tegg/core/*`, `tegg/plugin/*`, `tegg/standalone/*`\n\n### Key Core Packages\n\n- **core-decorator**: Basic decorators (`@Inject`, `@ContextProto`, `@SingletonProto`)\n- **metadata**: Metadata management for prototypes, modules, and dependency graphs\n- **runtime**: Runtime container and object lifecycle management\n- **loader**: Module discovery and loading system\n- **lifecycle**: Lifecycle hooks and management\n- **types**: TypeScript type definitions\n- **common-util**: Shared utilities\n\n### Key Plugin Packages\n\n- **plugin/tegg**: Main Egg.js plugin integrating tegg runtime\n- **plugin/config**: Module configuration support\n- **plugin/controller**: HTTP controller decorator support\n- **plugin/aop**: AOP runtime integration\n- **plugin/eventbus**: Event bus system\n- **plugin/schedule**: Scheduled task support\n- **plugin/dal**: Data access layer\n- **plugin/orm**: Leoric ORM integration\n\n## Development Commands\n\n**Note:** All commands below should be run from the **monorepo root** (`../egg`), not from the tegg directory.\n\n### Build & Clean\n\n```bash\nut run build               # Build all packages including tegg (runs build in all workspaces)\nut run clean-dist          # Clean all build artifacts including tegg (removes dist, tsbuildinfo)\n```\n\n### Testing\n\nAll tegg packages use **Vitest** for testing and are integrated with the main Egg.js monorepo test suite.\n\n```bash\nut run test                # Run vitest tests for all packages (from monorepo root)\nut run test:cov            # Run tests with coverage\nut run ci                  # Full CI: vitest with coverage and bail on first failure\n```\n\nRun tegg tests from clean sources, not immediately after `pnpm run build`. If\nlocal tests fail with `duplicate proto` after a build, remove stale `dist/`\ndirectories outside fixtures as described in the root `AGENTS.md` Local CI\nsection, then re-run tests.\n\n**Note:** Tests are configured in the monorepo root `vitest.config.ts` which includes all tegg packages (`tegg/core/*`, `tegg/plugin/*`, `tegg/standalone/*`).\n\n### Type Checking & Linting\n\n```bash\nut run typecheck           # Clean and type check all workspaces (including tegg)\nut run lint                # Run oxlint with type-aware checking on all packages\nut run fmtcheck            # Check code formatting with oxfmt\n```\n\n**Note:** oxlint automatically runs with `--type-aware` flag for enhanced TypeScript checking.\n\n### Version Management\n\n**Note:** Run these commands from the monorepo root (`../egg`).\n\n```bash\nut run version:patch       # Bump patch version (0.0.X)\nut run version:minor       # Bump minor version (0.X.0)\nut run version:major       # Bump major version (X.0.0)\nut run version:prepatch    # Bump to next prerelease patch version\nut run version:preminor    # Bump to next prerelease minor version\nut run version:premajor    # Bump to next prerelease major version\nut run version:alpha       # Bump prerelease alpha version\nut run version:beta        # Bump prerelease beta version\nut run version:rc          # Bump prerelease rc version\n```\n\n### Working with Individual Packages\n\n**Note:** Run from the monorepo root to work with individual tegg packages.\n\n```bash\n# Install dependencies\nut install              # Install all dependencies from native utoo configuration\n\n# Type check specific packages\nut run typecheck                    # Type check all packages\nut --filter @eggjs/tegg-runtime run typecheck\n\n# Build specific packages\nut --filter @eggjs/metadata run build\nut --filter @eggjs/tegg-runtime run build\n\n# Clean specific package\nnode -e \"require('node:fs').rmSync('tegg/core/runtime/dist', { recursive: true, force: true })\"\n```\n\n**Note:** Individual tegg packages don't have test scripts in their package.json. Tests are run via the monorepo root vitest configuration.\n\n## Architecture Concepts\n\n### Prototype System\n\nTegg uses a prototype-based system where classes are decorated to define how they should be instantiated:\n\n- **ContextProto**: Instance per request context (scoped to HTTP request)\n- **SingletonProto**: Single instance for entire application lifecycle\n- **MultiInstanceProto**: Multiple instances of same class with different qualifiers\n\nEach prototype has:\n\n- **AccessLevel**: `PRIVATE` (module-only) or `PUBLIC` (globally accessible)\n- **InitType**: Defines lifecycle scope (`CONTEXT`, `SINGLETON`)\n- **Name**: Instance identifier (defaults to camelCase class name)\n\n### Dependency Injection\n\nDependencies are resolved through `@Inject()` decorator:\n\n- Property injection: `@Inject() logger: Logger`\n- Constructor injection: `constructor(@Inject() logger: Logger)`\n- Optional injection: `@InjectOptional()` or `@Inject({ optional: true })`\n\n**Injection Rules:**\n\n- ContextProto can inject any prototype\n- SingletonProto cannot inject ContextProto\n- No circular dependencies allowed (between prototypes or modules)\n- Cannot inject `ctx`/`app` directly - inject specific services instead\n\n### Qualifiers\n\nWhen multiple implementations exist, use qualifiers to disambiguate:\n\n- `@InitTypeQualifier(ObjectInitType.CONTEXT)`: Specify init type\n- `@ModuleQualifier('moduleName')`: Specify source module\n- `@EggQualifier(EggType.CONTEXT)`: Specify egg context vs app\n- Custom qualifiers for dynamic injection patterns\n\n### Module System\n\nModules are organizational units discovered by scanning:\n\n- `app/modules/` directory (auto-discovered)\n- `config/module.json` (manual declaration for npm packages)\n\nEach module contains:\n\n- Prototype classes with decorators\n- Optional `module.json` or `package.json` with tegg metadata\n- Module-level dependencies on other modules\n\nThe **GlobalGraph** builds a dependency graph of all modules and validates:\n\n- No circular module dependencies\n- All prototype dependencies are resolvable\n- Access level constraints are respected\n\n### Lifecycle Hooks\n\nObjects can implement `EggObjectLifecycle` interface or use decorators:\n\n- `@LifecyclePostConstruct()`: After constructor\n- `@LifecyclePreInject()`: Before dependency injection\n- `@LifecyclePostInject()`: After dependency injection\n- `@LifecycleInit()`: Custom async initialization\n- `@LifecyclePreDestroy()`: Before object destruction\n- `@LifecycleDestroy()`: Resource cleanup\n\n### Runtime Object Management\n\nThe runtime manages object instances through:\n\n- **EggObjectFactory**: Creates and retrieves object instances\n- **LoadUnitInstance**: Manages module instances and their objects\n- **EggContext**: Request-scoped context holding ContextProto instances\n- **ContextObjectGraph**: Dependency graph for a specific context\n\n## Testing Patterns\n\n### Testing with MockApplication\n\n```typescript\nimport { MockApplication } from '@eggjs/mock';\n\n// Create context scope\nawait app.mockModuleContextScope(async (ctx: Context) => {\n  // Get object by class\n  const service = await ctx.getEggObject(HelloService);\n\n  // Get object by name with qualifiers\n  const logger = await ctx.getEggObjectFromName('logger', {\n    qualifier: 'bizLogger',\n  });\n});\n```\n\n### Async Tasks in Tests\n\nUse `BackgroundTaskHelper` instead of `setTimeout`/`setImmediate`:\n\n```typescript\n@ContextProto()\nclass MyService {\n  @Inject()\n  backgroundTaskHelper: BackgroundTaskHelper;\n\n  async doWork() {\n    this.backgroundTaskHelper.run(async () => {\n      // Async work here\n    });\n  }\n}\n```\n\n## Important Implementation Details\n\n### Metadata Registration\n\nDecorators register metadata on classes that is later used by the loader:\n\n- Prototype metadata: `PrototypeUtil.setXXX()` stores on class\n- Injection metadata: `InjectObjectInfo` stored per property/parameter\n- Qualifier metadata: `QualifierUtil.addProperQualifier()` for disambiguation\n\n### Loading Process\n\n1. **Loader** scans directories and discovers modules\n2. **EggPrototypeFactory** creates `EggPrototype` from decorated classes\n3. **GlobalGraph** validates and builds dependency graph\n4. **LoadUnitFactory** creates `LoadUnit` for each module\n5. **Runtime** instantiates objects on-demand based on graph\n\n### Dynamic Injection\n\nFor selecting implementations at runtime:\n\n```typescript\n// Define abstract class and enum\nabstract class AbstractHello { abstract hello(): string; }\nenum HelloType { FOO = 'FOO', BAR = 'BAR' }\n\n// Create decorator\nconst Hello = QualifierImplDecoratorUtil.generatorDecorator(\n  AbstractHello,\n  'HELLO_ATTRIBUTE'\n);\n\n// Apply to implementations\n@ContextProto()\n@Hello(HelloType.FOO)\nclass FooHello extends AbstractHello { ... }\n\n// Get instance dynamically\nconst impl = await eggObjectFactory.getEggObject(\n  AbstractHello,\n  HelloType.FOO\n);\n```\n\n## Multi-App Isolation (TeggScope) — MUST follow\n\nTegg supports multiple apps booting and serving requests **concurrently in one\nprocess** without cross-talk. This is built on `TeggScope`\n(`@eggjs/tegg-types`), a type-free `AsyncLocalStorage<Map<symbol, unknown>>`.\nEach app owns a per-app \"bag\" (`app._teggScopeBag`); per-app state lives in\nslots inside that bag, and the active bag is established with\n`TeggScope.run(app._teggScopeBag, ...)`. Per-app singletons resolve via the same\nold static call sites (e.g. `EggPrototypeFactory.instance`) — they now read the\ncurrent scope instead of a process global.\n\nWhen you touch tegg core/plugins, follow these rules:\n\n1. **Never add new process-global mutable runtime state.** A `static` field /\n   `Map` / singleton that holds per-app data WILL leak across concurrent apps.\n   If you need such state, back it with a `TeggScope` slot:\n\n   ```ts\n   import { TeggScope } from '@eggjs/tegg-types';\n   const X_SLOT = Symbol('tegg:<pkg>:<name>'); // module-private, never exported\n   export class X {\n     static get instance(): X {\n       return TeggScope.resolve(X_SLOT, () => new X(), 'X.instance');\n     }\n   }\n   ```\n\n   Import `TeggScope` **only** from `@eggjs/tegg-types`; never import another\n   package's slot. A package that imports `TeggScope` must declare\n   `@eggjs/tegg-types` as a direct dependency.\n\n2. **Shared, app-agnostic registries stay global.** Class/type-keyed maps\n   populated at import time with app-agnostic values (e.g.\n   `EggPrototypeCreatorFactory` creator map, `registerEggObjectCreateMethod`,\n   `registerLoadUnitInstanceClass`) must NOT be scoped. Only state that holds\n   per-app instances/data is scoped. (`LoadUnitFactory`'s creator map is\n   two-tier: a global base for import-time creators + a per-app overlay for\n   boot-time, app-capturing creators.)\n\n3. **Lifecycle-hook registration via `app.*LifecycleUtil` is bag-pinned.**\n   Calling `app.{loadUnit,eggPrototype,eggObject,eggContext,loadUnitInstance}LifecycleUtil.registerLifecycle(hook)`\n   (and the matching `deleteLifecycle` in `beforeClose`) does **not** need a\n   `TeggScope.run` wrap — these app getters are pinned to this app's bag (via\n   `xxxLifecycleUtilFromBag`), so they resolve the correct per-app util even with\n   no active scope. Wrapping is still fine when the same block does other\n   scope-dependent work (as the tegg plugin's own boot does). Do **not** register\n   lifecycle hooks in the boot **constructor** — `app._teggScopeBag` does not\n   exist yet; do it in `configWillLoad`/`configDidLoad`/`didLoad`. (Accessing a\n   lifecycle util through a module-level static instead of `app.*LifecycleUtil`\n   still needs an active scope.)\n\n4. **Resolve egg objects per-app.** To get a proto from a class, prefer\n   `EggPrototypeFactory.instance.getPrototypeByClazz(clazz)` (per-app) before\n   falling back to `PrototypeUtil.getClazzProto(clazz)` (a process-global slot\n   on the class, overwritten by concurrent boot). `ctx.getEggObject` /\n   `app.getEggObject` already do this and wrap in the app scope.\n\n5. **Escape points** — code that runs **detached** from the request must\n   re-establish the scope. Capture `const bag = TeggScope.current()` at\n   registration/scheduling and re-enter `TeggScope.run(bag, cb)` inside the\n   callback for: emitter listeners triggered later (`res.on('close')`,\n   `signal.addEventListener('abort')`), fire-and-forget `EventBus.emit` from a\n   detached context, and timers created outside a scope. Timers/promises created\n   **inside** an active scope inherit it automatically — no wrap needed.\n\n6. **Strict-mode fuse.** Under true multi-app (`> 1` live app) any access that\n   escapes to the process-default bag throws in dev / warns in prod. If you see\n   `[tegg] TeggScope escaped to the process-default bag`, you have an unwrapped\n   access — wrap the relevant boot/request/escape path in `TeggScope.run`.\n\n7. **Single app is unchanged.** With one app the default bag is used silently\n   and the fuse never fires, so existing single-app behavior and tests are\n   unaffected. Add multi-app regression coverage (two concurrent apps sharing a\n   module) when you change loader/runtime/lifecycle/eventbus behavior — see\n   `tegg/plugin/tegg/test/MultiApp.test.ts`.\n\n**Performance:** `TeggScope.resolve` adds ~8 ns/access and `TeggScope.run`\n~5 ns/call over a plain static read (Node 22); egg already runs on\nAsyncLocalStorage, so there is no new process-wide async penalty. The cost is\nnegligible relative to real request work. The per-app lifecycle-util facade is an\nexplicit delegating object (not a `Proxy`) — each method is a direct slot-resolve\nplus a method call, with no per-access trap or bound-function allocation.\n\n## Common Patterns\n\n### Creating a New Core Package\n\n1. Add to `tegg/core/` directory within the main monorepo\n2. Include `tsconfig.json` extending `@eggjs/tsconfig`\n3. Add standard scripts to `package.json`:\n   - `\"typecheck\": \"tsc --noEmit\"`\n4. Export public API through `src/index.ts`\n5. Use `workspace:*` for internal dependencies and `catalog:` for external dependencies\n\n### Creating a New Plugin\n\n1. Add to `tegg/plugin/` directory within the main monorepo\n2. Define `eggPlugin` in `package.json` with dependencies\n3. Create `app.ts` for initialization\n4. Add tests using Vitest and `@eggjs/mock`\n5. Tests will be automatically discovered by the root `vitest.config.ts`\n\n### Working with TypeScript\n\n- Use `emitDecoratorMetadata` for type inference in injection\n- `design:type` and `design:paramtypes` are used for automatic dependency resolution\n- All packages target ESM with `.js` extensions in imports\n","tools/create-egg/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fcreate-egg%2FREADME.md -->","tools/create-egg/src/templates/egg3-simple-js/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fcreate-egg%2Fsrc%2Ftemplates%2Fegg3-simple-js%2FREADME.md -->","tools/create-egg/src/templates/egg3-simple-ts/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fcreate-egg%2Fsrc%2Ftemplates%2Fegg3-simple-ts%2FREADME.md -->","tools/create-egg/src/templates/egg3-tegg/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fcreate-egg%2Fsrc%2Ftemplates%2Fegg3-tegg%2FREADME.md -->","tools/create-egg/src/templates/simple-ts/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fcreate-egg%2Fsrc%2Ftemplates%2Fsimple-ts%2FREADME.md -->","tools/create-egg/src/templates/tegg/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fcreate-egg%2Fsrc%2Ftemplates%2Ftegg%2FREADME.md -->","tools/egg-bin/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fegg-bin%2FREADME.md -->","tools/egg-bundler/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fegg-bundler%2FREADME.md -->","tools/egg-bundler/docs/output-structure.md":"# Bundle output structure\n\n`@eggjs/egg-bundler` produces a runnable CJS bundle under the configured\n`outputDir`. Everything except configured or auto-detected externals is inlined\ninto the generated worker file by default.\n\n## Layout\n\n```\n<outputDir>/\n├── worker.js             # self-contained entry produced from worker.entry.ts\n├── app/port/binary.html  # runtime asset copied from <baseDir>/app/port/binary.html\n├── app/port/login.html   # runtime asset copied from <baseDir>/app/port/login.html\n├── package.json          # `{ \"type\": \"commonjs\" }`, so Node parses generated *.js as CJS\n└── bundle-manifest.json  # reference/debug metadata written by Bundler\n```\n\n`pack.singleFile` defaults to `true`. When a programmatic caller explicitly\nsets it to `false`, `@utoo/pack` may also emit opaque module-graph and runtime\nchunks such as `_root-of-the-server___<hash>.js` and\n`_turbopack__runtime.js`. Snapshot builds always force single-file output. The\ncompiler `tsconfig.json` is written to the bundler's generated entry directory,\nnot to the deploy output.\n\nFor `target: 'cluster'`, `worker.js` is replaced by two role-specific entry\nfiles:\n\n```text\n<outputDir>/\n├── app_worker.js         # self-contained application worker entry\n├── agent_worker.js       # self-contained agent worker entry\n├── app/...               # copied runtime assets, when present\n├── package.json          # `{ \"type\": \"commonjs\" }`\n└── bundle-manifest.json\n```\n\nThe source generator shares its rendering logic, but each role is a separate\nentry. With the default single-file setting, `@utoo/pack` inlines the complete\ndependency graph into both outputs; snapshot builds rely on this so each file\ncan independently build and restore its own V8 snapshot blob.\n\n## Running the bundle\n\n```bash\ncd <outputDir>\nnode worker.js\n```\n\nThe worker entry installs `ManifestStore.setBundleStore(...)` and\n`globalThis.__EGG_BUNDLE_MODULE_LOADER__` before calling\n`startEgg({ baseDir: outputDir, framework, mode: 'single' })`, so framework\nspecifier lookup is served by the already imported bundled framework module,\nwithout adding framework path aliases. Runtime lookup keeps\nthe deploy output directory separate from the original app paths: the bundle map\nis keyed by relKey, output-dir absolute paths, precomputed original app absolute\npaths, and manifest `resolveCache` request aliases. Application code and plugins\nmay still use `fs` for resources such as config, views, or assets.\n\nThe supported ordinary cluster workflow is:\n\n```bash\negg-bin bundle --cluster\negg-scripts start --bundle --bundle-dir ./dist-bundle\n```\n\n`egg-scripts` resolves `app_worker.js` and `agent_worker.js` from\n`--bundle-dir`, then supplies them to the cluster launcher. Plain bundles\nsupport process and `worker_threads` start modes. To build and restore\nrole-specific V8 snapshots:\n\n```bash\negg-bin snapshot build --cluster\negg-scripts start --bundle \\\n  --app-snapshot-blob ./dist-bundle/app.snapshot.blob \\\n  --agent-snapshot-blob ./dist-bundle/agent.snapshot.blob\n```\n\nEither role blob may be omitted; that role then starts from its bundle\nJavaScript. Snapshot blobs require process mode.\n\n## Runtime assets\n\nThe bundler copies application runtime assets from `<baseDir>/app` by default\ninto the same relative path under `outputDir`, excluding manifest-known module\nfiles and source-like files such as `.js`, `.ts`, `.mjs`, and `.cjs` outside\nstatic asset directories. For example, `<baseDir>/app/port/binary.html` is\nemitted as `<outputDir>/app/port/binary.html`. Since bundled workers start Egg with\n`baseDir: outputDir`, existing reads such as\n`fs.readFile(path.join(app.config.baseDir, 'app/port/binary.html'))` resolve to\nthe copied file in bundle mode and continue to resolve to the original file in\nnon-bundle mode. Static asset directories such as `app/public`, `app/assets`,\nand `app/static` are copied verbatim so frontend `.js` and `.css` files remain\nservable from the bundled app. Applications may replace those force-copy\ndirectories with `bundle.runtimeAssets.forceCopyDirs` in `module.yml`, for\nexample:\n\n```yaml\nbundle:\n  runtimeAssets:\n    roots:\n      - app\n    forceCopyDirs:\n      - app/public\n      - app/assets\n      - app/static\n      - app/port\n```\n\nApplications may also replace the scanned roots with\n`bundle.runtimeAssets.roots`. Root entries are baseDir-relative directories, and\nthe output keeps the same baseDir-relative path.\n\n## `bundle-manifest.json`\n\nA reference file produced by `Bundler` (not consumed at runtime). Shape:\n\n```json\n{\n  \"version\": 1,\n  \"generatedAt\": \"2026-04-11T00:00:00.000Z\",\n  \"mode\": \"production\",\n  \"target\": \"single\",\n  \"baseDir\": \"/abs/path/to/app\",\n  \"framework\": \"egg\",\n  \"entries\": [{ \"name\": \"worker\", \"source\": \"/abs/path/to/app/.egg-bundle/entries/worker.entry.ts\" }],\n  \"externals\": [\"some-native-addon\", \"...\"],\n  \"chunks\": [\"package.json\", \"worker.js\", \"...\"]\n}\n```\n\nUse it to inspect what went into the bundle or to drive deterministic-bundle\nchecks (T17).\n\n## Externals\n\nPackages classified as external by `ExternalsResolver` are **not** inlined.\nThis includes the user's `externals.force` list plus auto-detected entries from\nroot `peerDependencies`, root `optionalDependencies`, root dependency packages\nwith native addons/native binaries, root dependency packages whose optional peer\ndependencies cannot be resolved, and the names of those missing optional peer\npackages as `extraExternals`, plus native optional platform packages declared by\nroot dependencies.\nThe native addon and missing optional peer checks run only while resolving the\napp's root dependencies/optionalDependencies; `ExternalsResolver` does not\nrecursively scan every transitive dependency. `externals.inline`\nremoves an auto-detected external unless the same name is also present in\n`externals.force`. External packages must be resolvable from the output worker.\nA typical deployment keeps `outputDir` below the application or deployment root\nand installs production dependencies at that root. Do not overwrite the\ngenerated output `package.json`: its `{ \"type\": \"commonjs\" }` marker is required\nwhen the parent application package uses ESM. ESM-only packages, `egg`,\n`@swc/helpers`, and `@eggjs/*`\npackages are bundled by default unless an explicit or auto-detected external\nrule applies, such as `externals.force`, peer/optional dependency metadata,\nnative addon detection, or missing optional peer detection. For wrappers around\nnative optional platform packages that cannot be loaded through `createRequire`,\nthe wrapper stays bundled so the CommonJS standalone output does not emit a\nplain `require(wrapper)`, while the optional native platform packages remain\nexternal.\n\n## Known limitations\n\n- **Cluster bootstrap modules**: bundled cluster workers do not support\n  `options.require`; the supported launcher fails before spawning workers.\n- **Snapshot blobs and worker threads**: plain cluster bundles support\n  `worker_threads`, but custom V8 snapshot blobs require process mode.\n- **Native addons**: always external. If a native module is missing from the\n  deployment target, the bundle will fail to start at runtime with the usual\n  Node module resolution error.\n- **Tegg**: decorated files listed in `manifest.extensions.tegg` are included\n  as side-effect imports in the worker entry; if `tegg` is disabled in\n  `BundlerConfig`, tegg collection is intended to be skipped (not yet wired).\n","tools/scripts/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fscripts%2FREADME.md -->","wiki/packages/core.md":"---\ntitle: Core Package\ntype: package\nsummary: Loader, lifecycle, and application core primitives used by Egg runtime packages.\nsource_files:\n  - packages/core/src/index.ts\n  - packages/core/src/loader/file_loader.ts\n  - packages/core/src/loader/context_loader.ts\n  - packages/core/src/loader/egg_loader.ts\nupdated_at: 2026-05-10\nstatus: active\n---\n\n# Core Package\n\n`@eggjs/core` exports the runtime loader primitives used to assemble an Egg\napplication, including `EggLoader`, `FileLoader`, `ContextLoader`, manifest\nsupport, lifecycle, and base context classes.\n\n## LoaderFS\n\n`@eggjs/core` consumes and re-exports `LoaderFS` and `RealLoaderFS` from\n`@eggjs/loader-fs`. The abstraction remains the minimal filesystem boundary for\nloader-facing file access: `exists`, `stat`, `realpath`, `readJSON`, `glob`, and\n`loadFile`, without trying to polyfill the full Node.js `fs` module.\n\n`EggLoaderOptions`, `FileLoaderOptions`, and `ContextLoaderOptions` can carry a\ncustom `loaderFS`. `EggLoader` passes its loader FS into `loadToApp()` and\n`loadToContext()` so later bundled loaders can replace file discovery and module\nloading without changing the public loader call sites.\n","wiki/packages/egg-bundler.md":"---\ntitle: Egg Bundler\ntype: package\nsummary: Bundles Egg applications for Node startup snapshots and tegg standalone service-worker targets.\nsource_files:\n  - packages/core/src/lifecycle.ts\n  - packages/egg/src/lib/egg.ts\n  - packages/cluster/src/worker_protocol/worker-thread.ts\n  - packages/cluster/src/utils/mode/impl/worker_threads/agent.ts\n  - packages/cluster/src/utils/mode/impl/worker_threads/app.ts\n  - plugins/watcher/src/lib/boot.ts\n  - tools/egg-bundler/src/index.ts\n  - tools/egg-bundler/src/compat/leoric/index.ts\n  - tools/egg-bundler/src/compat/leoric/runtime-require-loader.cjs\n  - tools/egg-bundler/src/lib/Bundler.ts\n  - tools/egg-bundler/src/lib/EntryGenerator.ts\n  - tools/egg-bundler/src/lib/ExternalsResolver.ts\n  - tools/egg-bundler/src/lib/prelude.ts\n  - tools/egg-bundler/src/lib/PackRunner.ts\n  - tools/egg-bundler/src/lib/StandaloneWorkerBundler.ts\n  - tools/egg-bundler/src/lib/importMetaPatch.ts\n  - tools/egg-bin/src/commands/bundle.ts\n  - tools/egg-bin/src/commands/snapshot.ts\n  - tools/scripts/src/commands/start.ts\n  - packages/loader-fs/src/index.ts\n  - packages/loader-fs/src/manifest_loader_fs.ts\n  - tegg/core/types/src/metadata/model/TeggManifest.ts\n  - tegg/core/loader/src/LoaderFactory.ts\n  - tegg/core/loader/src/LoaderUtil.ts\n  - tegg/core/loader/src/TeggManifestLoaderFS.ts\n  - tegg/core/loader/src/impl/ModuleLoader.ts\n  - tegg/plugin/dal/src/lib/DataSource.ts\n  - tegg/plugin/tegg/src/lib/EggModuleLoader.ts\n  - tegg/standalone/standalone/src/EggModuleLoader.ts\n  - tools/egg-bundler/docs/output-structure.md\n  - site/docs/advanced/snapshot.md\n  - site/docs/zh-CN/advanced/snapshot.md\n  - examples/helloworld-service-worker\nupdated_at: 2026-10-06\nstatus: active\n---\n\n# Egg Bundler\n\n`@eggjs/egg-bundler` is a developer tooling package under `tools/egg-bundler/`.\nIt exposes `bundle(config)` and the `Bundler` class for producing a deployable\nCommonJS artifact from an Egg application, plus `StandaloneWorkerBundler` for the\ntegg standalone service worker (Cloudflare Workers) — see the section below.\n\nThe bundler engine (`PackRunner`) is `@utoo/pack`, whose engine is **Turbopack**\n(not mako). `@utoo/pack` only emits **CommonJS** — `output.type` is `standalone`\nor `export` (single-file self-executing IIFE); there is no ESM output. In\nsingle-file (`export`) mode the IIFE ends with `module.exports = factory()` where\n`factory()` returns the entry's ES-module namespace, so a Node `require()` of the\noutput exposes the entry's `export default`/named exports; `library.name`/`export`\nonly affect the `exports[name]`/`globalThis[name]` fallback branches. That CJS-only\nconstraint is why worker output always needs a thin ESM wrapper.\n\n## Public Surfaces\n\n- `tools/egg-bundler/src/index.ts` exports `bundle`, `Bundler`, the helper\n  classes, and the public config/result types.\n- `tools/egg-bin/src/commands/bundle.ts` wires the package into\n  `egg-bin bundle`.\n\n## Bundle Flow\n\n1. `ManifestLoader` loads the app startup manifest, defaulting to\n   `<baseDir>/.egg/manifest.json`.\n2. `ExternalsResolver` classifies packages that should stay external.\n3. `EntryGenerator` writes either one single-process worker entry or separate\n   app-worker and agent-worker entries. Each installs the bundle manifest/module\n   loader before starting its Egg runtime role.\n4. `PackRunner` invokes `@utoo/pack`.\n5. `Bundler` writes `bundle-manifest.json` and returns absolute output paths.\n\n## Current Behavior\n\n- Relative `outputDir` values are resolved from `baseDir`.\n- Default mode is `production`; `development` is also accepted.\n- `PackRunner` defaults to `singleFile: true`, so each normal worker entry is a\n  self-contained CommonJS file. Snapshot builds require and force that setting.\n  A programmatic caller may opt into opaque `@utoo/pack` runtime/module chunks\n  for a non-snapshot build with `singleFile: false`.\n- If `<baseDir>/.egg/manifest.json` is missing, `ManifestLoader` starts the app\n  with `metadataOnly: true` to generate it. This skips the agent and normal boot\n  lifecycle, runs `loadMetadata()` hooks, and the manifest generation child\n  process exits after writing the manifest, so registered `beforeClose` hooks do\n  not run.\n- The default `single` target runs in Egg single-process mode. Its worker entry\n  treats the deploy output directory as the runtime Egg `baseDir`, passes the\n  framework specifier explicitly to `startEgg`, maps that specifier to the\n  already bundled framework module, and precomputes original app absolute\n  aliases so bundled module lookup can serve relKeys, output-dir absolute paths,\n  original app absolute paths, and manifest `resolveCache` request aliases.\n- The `cluster` target emits `app_worker.js` and `agent_worker.js`. Their roles\n  are fixed while generating the entries rather than selected by\n  `EGG_PROCESS_TYPE` or another runtime switch. Both entries use the shared\n  `@eggjs/cluster/worker_protocol` implementation, accept the master's normal\n  JSON argv contract, and select process IPC or `worker_threads.parentPort`\n  from `startMode`. Snapshot builds force each output to remain independently\n  self-contained, so no common runtime chunk is emitted between the two files.\n  Custom V8 snapshot blobs remain process-only because Node does not expose a\n  per-Worker snapshot-blob API; option parsing rejects that combination before\n  creating a worker thread. Sticky-session mode is also process-only because its\n  socket handoff uses process IPC. Worker-thread shutdown is an explicit protocol:\n  the master requests graceful exit, the worker awaits `app.close()` or\n  `agent.close()`, and the master falls back to `Worker.terminate()` immediately\n  if the worker emits an error or after the configured close timeout. Successful\n  shutdown cancels its timeout timer so it does not keep the event loop alive.\n  A nonzero graceful-exit code or worker error is propagated after all app\n  workers and the agent have been given their cleanup attempt; the master exits\n  with code `1`. Timeout termination retains its existing successful master-exit\n  behavior, and process-mode shutdown behavior is unchanged.\n  With worker-thread `reusePort`, an explicit master port of `0` defers to the\n  application's configured shared listen port rather than being rejected before\n  the worker protocol runs.\n- `egg-bin bundle --cluster` is the ordinary cluster-bundle producer. It selects\n  the bundler's `cluster` target and writes `app_worker.js`, `agent_worker.js`,\n  and the bundle manifest without constructing snapshot blobs. Its output can\n  be passed directly to `egg-scripts start --bundle`. `--cluster` is an Egg app\n  mode and is rejected when standalone mode is selected explicitly or inferred\n  from `--entry`.\n- `egg-bin snapshot build --cluster` selects that target and runs two independent\n  V8 snapshot builds: `app_worker.js` produces `app.snapshot.blob`, while\n  `agent_worker.js` produces `agent.snapshot.blob`. Their paths can be set\n  independently with `--app-snapshot-blob` and `--agent-snapshot-blob`; the\n  existing `--blob` flag remains exclusive to single-process builds. The role\n  comes from the entry filename/code, so snapshot construction does not use\n  `EGG_SNAPSHOT_ROLE`. The command rejects identical role blob paths and removes\n  each existing target immediately before building (except in dry-run mode), so\n  a stale blob cannot satisfy the post-build existence check.\n- A `snapshot: true` output remains a normal runnable bundle; generating it does\n  not dedicate the JavaScript file to blob restore. The prelude and generated\n  entry use `v8.startupSnapshot.isBuildingSnapshot()` to detect a real\n  `--build-snapshot` process. A plain invocation keeps Node's web globals intact,\n  installs `__RUNTIME_REQUIRE` before the bundle IIFE, and follows the ordinary\n  bundle startup path. Only snapshot construction installs build-time stubs. On\n  restore, V8 does not re-evaluate the file and instead invokes the serialized\n  deserialize main, which installs the same runtime require hook before resuming.\n- `egg-scripts start --bundle` explicitly selects bundled cluster workers.\n  `--bundle-dir` defaults to `./dist-bundle`, and supplies the fixed defaults\n  `<bundle-dir>/app_worker.js` and `<bundle-dir>/agent_worker.js`; either worker\n  path can be overridden explicitly. Snapshot restore is enabled per role only\n  when `--app-snapshot-blob` or `--agent-snapshot-blob` is provided. Blob\n  locations never determine worker paths, and one role's arguments never\n  determine the other's. Bundle path and role options are ignored unless\n  `--bundle` is present. The existing `--snapshot-blob` remains the single-process\n  launcher and takes precedence when it is supplied together with `--bundle`.\n- Snapshot-backed single-process launches and bundled cluster workers reject\n  non-empty `options.require` before startup. A runtime bootstrap module cannot\n  preserve the source worker's before-framework ordering once the bundle graph\n  has been statically evaluated; silently ignoring it would disable\n  instrumentation or patches without failing the deployment.\n- Explicit `externals.force` entries are external, and `ExternalsResolver`\n  auto-detects root `peerDependencies`, root `optionalDependencies`, root\n  dependency packages with native addons, root dependency packages whose optional\n  peer dependencies cannot be resolved, the missing optional peer package names\n  themselves as `extraExternals`, and native optional platform packages as\n  external.\n- `externals.inline` removes an auto-detected external unless the same package\n  name is also listed in `externals.force`.\n- ESM-only packages, `egg`, `@swc/helpers`, and `@eggjs/*` packages are bundled\n  by default unless `externals.force` or dependency metadata applies. If a\n  wrapper around native optional platform packages cannot be loaded through\n  `createRequire`, the wrapper stays bundled and the platform packages are kept\n  external.\n- `BundlerConfig.tegg` is accepted but intentionally not wired into the current\n  implementation yet.\n\n### Snapshot lifecycle boundary\n\nWith `snapshot: true`, Egg executes `configWillLoad` during snapshot construction\nand stops before `configDidLoad`. After V8 restores the heap, Egg runs the\nregistered `snapshotDidDeserialize` hooks and then resumes the ordinary lifecycle\nfrom `configDidLoad`. Function-style app and agent boot hooks are registered as\n`configDidLoad` hooks, so they already run on the runtime side of this boundary.\n\nPlugin constructors and `configWillLoad` may prepare only serializable\nconfiguration or metadata. Runtime resources such as cluster clients, sockets,\nservers, filesystem watchers, timers, and native clients must be created in\n`configDidLoad` or later. A plugin that consumes another plugin's runtime object\nmust also declare that plugin dependency; this makes the corresponding\n`configDidLoad` order explicit instead of relying on incidental discovery order.\n\n`EggApplicationCore.clusterWrapper()` enforces this contract by throwing during\nsnapshot construction and becoming available after restore. The watcher plugin\nis the reference pattern: its boot constructor stores the app only,\n`configDidLoad` creates and wires the watcher cluster client, and `didLoad` waits\nfor readiness. A generic deferred proxy is intentionally not used because it\nwould silently record arbitrary calls, cover only clients created through\n`clusterWrapper()`, and move failures away from the actual lifecycle violation.\n\nFramework-owned resources that necessarily exist before the cutoff may instead\nimplement a symmetric `snapshotWillSerialize`/`snapshotDidDeserialize` pair, as\nEgg does for its messenger and logger transports. That mechanism is for explicit\nresource ownership, not a substitute for moving plugin runtime initialization to\n`configDidLoad`.\n\n### Snapshot lazy-external defaults\n\nIn `snapshot: true` mode the bundler keeps a set of modules **lazy-external** so a\nV8 startup snapshot stays serializable: each is emitted as an `externalRequire`,\nleft out of the build-time heap (a member-proxy stub from the prelude's\n`__makeLazyExt`), and forwarded to the real module at restore via\n`globalThis.__RUNTIME_REQUIRE`. The injected hook also lazy-stubs any **non-builtin**\nexternal at build (`!__isBuiltin(id)`); the explicit list mainly exists to (a) cover\nbuiltins the `!isBuiltin` rule skips and (b) **force npm packages external** that\nwould otherwise be inlined.\n\n- `DEFAULT_SNAPSHOT_LAZY_MODULES` (in `src/lib/prelude.ts`) covers the Node network\n  stack (`http`/`https`/`http2`/`tls`/`dns`), `inspector`, `cluster`/`node:cluster`,\n  **and egg's HTTP client stack `undici` + `urllib`**. `cluster` is runtime-sensitive:\n  Node chooses its primary or worker implementation when the module is first loaded,\n  while snapshot construction happens outside a cluster worker. Keeping both module\n  specifiers lazy ensures each restored worker loads the worker implementation\n  instead of retaining the builder's primary implementation. Egg builds its\n  `HttpClient` (urllib → undici) during boot, and undici instantiates an llhttp\n  `WebAssembly` (disabled under\n  `--build-snapshot`) + `HTTPParser` that cannot be serialized. As npm packages\n  urllib/undici would be inlined; listing them forces them external (`Bundler` adds\n  the lazy ids to the externals map) so the member-proxy stub is used at build — an\n  app gets a serializable snapshot without listing them in `egg.snapshot.lazyModules`.\n- The member-proxy records the build-time access path (`get`/`apply`/`construct`)\n  and replays it against the real module on restore, so\n  `class HttpClient extends urllib.HttpClient` (and urllib's own\n  `class BaseAgent extends undici.Agent`) keep\n  working: the `extends` is evaluated against the build stub, then `super(...)` /\n  inherited methods resolve to the real base class after deserialization. The\n  first successful replay is memoized, so an instance returned by a recorded call\n  or constructor keeps its identity and subsequent writes are observable.\n- Proxy reads and writes preserve the inherited receiver at restore. Static or\n  prototype accessors on the real base therefore observe the application subclass\n  or instance as `this`, while direct proxy access still uses the real exported\n  object. This is required by symbol-backed model state such as Leoric's\n  `Bone.synchronized` accessor.\n\n#### Leoric compatibility boundary\n\nSnapshot mode intentionally forces the Leoric core into the bundle. Keeping the\nwhole package external was rejected because application/TEGG model modules are\nevaluated while constructing the snapshot: subclasses, prototype chains, and ORM\nmetadata would then be created against a lazy external proxy. Restore can forward\nmember access to the real package, but it cannot replace prototype and metadata\nidentity that is already frozen into the heap. Deferring every model module to\nruntime would avoid that proxy boundary, but would also move the ORM model graph\nout of the snapshot and require Leoric/TEGG-specific lazy-loading behavior.\n\nUnmodified Leoric cannot be safely inlined either. Its driver, realm, and migration\npaths contain expression-based CommonJS `require(...)` calls that `@utoo/pack`\ncannot statically enumerate. A single-file build would otherwise fail on the\ndynamic expression or retain runtime loads for optional drivers and filesystem\nmodules that are not represented by the bundle graph.\n\nThe compatibility layer is therefore a scoped compromise:\n\n- `resolveLeoricSnapshotCompatibility()` removes `leoric` from auto-detected\n  externals and rejects attempts to force it external or add it to snapshot lazy\n  modules.\n- Snapshot builds resolve both imports and requires to Leoric's CommonJS entry.\n  Leoric 2.16 also exports an ESM entry under `dist/`; selecting it would bypass\n  the compatibility rule for `lib/` and expose optional database imports to the\n  bundler.\n- A module rule applies only to the known Leoric source files and rewrites their\n  runtime-selected requires to `globalThis.__RUNTIME_REQUIRE(...)`. Leoric core and\n  model semantics stay bundled, while database clients remain runtime dependencies.\n  The rule also handles TypeScript's compiled dynamic-import callback while\n  preserving its Promise and module namespace conversion.\n- The loader fails the build if an unrecognized expression-based require remains,\n  rather than silently emitting a partial artifact. Migration files used at runtime\n  still need the documented runtime-asset copy configuration.\n\nInference: this is deliberately package- and version-sensitive. A Leoric release\nthat moves these files or changes the require expressions can invalidate the path\ncondition or rewrite patterns, so upgrades require the unit and real-build coverage\nto pass. The shim should be removed when Leoric no longer needs expression-based\nruntime loading, or when `@utoo/pack` provides an equivalent supported runtime\ndynamic-require contract without modifying Turbopack.\n\n#### When does a new dependency need adding?\n\nOnly a package that **directly** creates non-serializable native/WASM state at\nmodule-eval or boot-time instantiation needs a list entry (like `undici`, which\ncompiles llhttp WASM). A package that only reaches the network/native stack\n**transitively** is already covered, because the underlying builtins are lazy:\n\n- `@modelcontextprotocol/sdk` (a default tegg-controller dep, loaded at boot via\n  `tegg/plugin/controller` → `MCPControllerRegister`) → its StreamableHTTP\n  transport `require`s `@hono/node-server`, which does a top-level\n  `require(\"http2\")` + `class extends globalThis.Request`. `http2` is already lazy\n  and `globalThis.Request` is stubbed by the prelude, and no MCP transport/server\n  is instantiated at boot — so the SDK does **not** need a list entry.\n- `@grpc/grpc-js`, `ws` — not present in OSS tegg; gRPC would route through the\n  already-lazy `http2` anyway. No entry needed.\n\n`Inference:` audited 2026-06-28 against `tegg/plugin/controller`, default egg\nplugins, and the suspect packages' module-eval closures. The opt-in\n`mcp-client`/`mcp-proxy`/`langchain` plugins pull the SDK _client_ transports\n(`eventsource`/`cross-spawn`/`pkce-challenge`); if an app enables those and\nsnapshots, re-assess via `egg.snapshot.lazyModules` — that is an app concern, not\na framework default.\n\n## Standalone Worker Bundle (Cloudflare Workers)\n\n`StandaloneWorkerBundler` (`src/lib/StandaloneWorkerBundler.ts`) bundles a tegg\n**standalone service worker** (`@eggjs/service-worker`, see the\n[service worker page](./service-worker.md)) for Cloudflare workerd. This is a\nseparate path from the Egg app `Bundler`/`EntryGenerator` flow above — there is no\n`startEgg`/`app.listen`; the artifact is a fetch handler.\n\n**CLI.** `egg-bin bundle` drives it as a one-shot command (mirroring the egg-app\n`egg-bin bundle`): a standalone bundle is selected by `--entry` (or `--target\nstandalone`); `--framework` names the app package (e.g. `@eggjs/service-worker`),\nwhich must export `loadMetadata` (the standalone counterpart of the egg app's\nframework specifier — no separate app-module/app-export flags). The command runs\n`loadMetadata` + `StandaloneWorkerBundler` internally, so the caller never threads\nthe manifest by hand. The metadata scan runs in an `egg-bin` child process, where\nthe CLI's detected TypeScript compiler and configured `--require`/`--import`\nhooks are active; a TypeScript standalone app therefore does not need to wrap the\nbundle command in its own `NODE_OPTIONS`. `tools/egg-bin/src/commands/bundle.ts`\nbranches on the target, and `tools/egg-bin/scripts/standalone-metadata.mjs`\nperforms the scan.\n\n**Injection-based seam.** The bundler does NOT synthesize the host entry. The user\nauthors a plain, locally-runnable `worker.ts`\n(`new ServiceWorkerApp(dir)` + `export default { fetch }`, or `addEventListener`).\n`run()` then:\n\n1. Filters the caller-supplied tegg manifest (`ServiceWorkerApp.loadMetadata`)\n   by `excludeModules` — a general escape hatch (teggDal no longer needs it: the\n   bundle-mode dynamic-loader fix below makes a scanned-but-unused DAL module load\n   cleanly).\n2. Writes a **build-managed copy of the user's `entry` beside it** (a\n   `.egg-worker-entry.ts` sibling, so the user's relative imports and\n   `import.meta` resolve unchanged), prepending an injected prelude that\n   static-imports every decorated file (so Turbopack bundles them) and installs\n   `globalThis.__EGG_BUNDLE_MODULE_LOADER__` + `globalThis.__EGG_BUNDLE_MANIFEST__`.\n   Evaluation order is safe because import declarations load before module bodies,\n   so the globals are set before the user's `new ServiceWorkerApp(dir)` runs.\n   `StandaloneApp.init` falls back to `__EGG_BUNDLE_MANIFEST__` when no `manifest`\n   option is passed, so the same `worker.ts` runs bundled (global manifest) and\n   unbundled under Node (runtime fs scan).\n3. Runs `PackRunner` (`singleFile: true`, `useDefineForClassFields: true` — the\n   `false` egg-app default would erase uninitialized `#x?: T;` private fields while\n   the code still references `this.#x`), with node builtins external and\n   `resolve.alias` mapping `globby`/`os`/`node:os` to inert stubs (bundle mode never\n   globs; workerd's `nodejs_compat` has no `node:os`).\n4. Applies `patchImportMetaInContent` (`src/lib/importMetaPatch.ts`) to fix\n   Turbopack's broken `import.meta` shim (`__turbopack_context__.F` is otherwise\n   undefined — breaks in Node AND workerd), writes the result as `worker.cjs`, and\n   deletes the injected copy.\n5. Emits the entry per `format`: `module` (default) → an ESM wrapper `index.mjs`\n   (`import worker from './worker.cjs'; export default worker.default;` — a\n   Cloudflare module worker); `service-worker` → no wrapper, the entry is\n   `worker.cjs` itself (a classic, non-module script whose\n   `addEventListener('fetch')` registered on evaluation; an ESM wrapper would move\n   it to module scope where workerd does not dispatch fetch events).\n\n`wrangler.jsonc` sets `nodejs_compat` (tegg needs `AsyncLocalStorage`) and points\n`main` at the wrapper. Verified on Node and workerd (`wrangler dev`): the example's\n`GET /hello/` and `POST /mcp/calc/stream` both return 200. Example:\n`examples/helloworld-service-worker/{worker.ts,wrangler.jsonc}`, built via\n`npm run bundle:cf` (`egg-bin bundle --framework @eggjs/service-worker --entry worker.ts`).\n\n**Bundle-mode dynamic module loading.** Egg and standalone adapt the shared\n`TeggManifest.moduleDescriptors[].decoratedFiles` data through the same\n`createTeggManifestLoaderFS()` helper. The result is a `ManifestLoaderFS`\noverlay: manifest-indexed decorated files are authoritative, while the host's\nnormal loader view may remain as a fallback for unrelated files.\n\nThe host passes that view into the initial `LoaderFactory.loadApp()` scan.\n`ModuleLoader.createModuleLoader()` then installs an explicitly supplied view in\nthe current application's `TeggScope`; later loader creation in the same app\nreuses it. This matters for dynamic multi-instance discovery: DAL's\n`DataSource.getObjects()` still creates a module loader and calls `load()` to\nfind table classes, but in a bundle that scan now reads the scoped manifest view\ninstead of the unavailable runtime filesystem. No class list is added to\n`MultiInstancePrototypeGetObjectsContext`, and generic `LoaderFS` construction\nremains side-effect free.\n\nThe common `ModuleLoader` does not read bundle globals. It first asks its\n`LoaderFS` for an authoritative file list. `ManifestLoaderFS` returns the exact\nmanifest-indexed decorated files, including TypeScript-origin keys and an\nauthoritative empty list; only a source without such a view falls back to\n`LoaderUtil.filePattern()` plus `LoaderFS.glob()`. The standalone host uses\n`globalThis.__EGG_BUNDLE_MANIFEST__` only as an entry source for the shared\nmanifest and constructs the same loader view as Egg.\n\nThis source-level distinction matters when `egg-scripts start` sets\n`EGG_TS_ENABLE=false`: real filesystem discovery still excludes TypeScript,\nwhile `.ts`/`.mts`/`.cts` manifest keys remain discoverable because they resolve\nfrom the in-memory bundle module map rather than files Node must execute. No\nbundle-specific condition is added to `LoaderUtil`.\n\n### Startup benchmark methodology\n\nThe module graph and lifecycle through `configWillLoad` are already captured in\nthe blob, so restore mainly runs runtime initialization from `configDidLoad`\nonward, including `didReady` and connection/listening. The cnpmcore comparison\nexecutes the snapshot-ready JavaScript artifacts normally for the bundle\nbaseline, then restores blobs built from those same files. On Node.js 24.18.1\nand Apple M1 Pro, it runs one warm-up plus ten interleaved measurements per mode.\nSingle process measures direct Node.js spawn through listening; cluster uses the\nmaster's internal orchestration-to-ready timer to exclude launcher and master\nbootstrap. The recorded medians were 947 ms vs 379 ms for single process and\n1356 ms vs 591 ms for cluster, or 2.50x and 2.30x faster restores.\n\nModule identity follows a separate path. Bundle hosts obtain the name from the\nshared `TeggManifest`, normal hosts resolve it while scanning module config, and\nboth store it on `ModuleDescriptor`. `GlobalGraph.moduleConfigList` then carries\nthat name into `LoadUnitFactory`; neither `LoaderFS` nor `LoaderFactory` resolves\n`unitName`. Manifest reference/descriptor names for the same unit path are\nvalidated for consistency.\n\n**Format targets differ.** The `module` format runs on Cloudflare workerd. The\n`service-worker` format targets Web Service Worker / edge runtimes that expose a\nglobal `addEventListener('fetch')` — it is **not** a workerd target: workerd's\n`nodejs_compat` (required for tegg's `AsyncLocalStorage`) only supports the\nmodule-worker format, and `wrangler` rejects a service-worker-format script that\nimports Node builtins (`Unexpected external import of \"assert\"…, no default\nexport`). The minimal `helloworld-service-worker` example intentionally demonstrates\nonly the deployable module-worker path; the classic format remains available through\nthe bundler and CLI for compatible hosts.\n","wiki/packages/loader-fs.md":"---\ntitle: Loader FS Package\ntype: package\nsummary: Shared loader-facing filesystem boundary for Egg loaders and future bundled runtimes.\nsource_files:\n  - packages/loader-fs/src/index.ts\n  - packages/loader-fs/src/manifest_loader_fs.ts\n  - tegg/core/loader/src/LoaderFactory.ts\n  - tegg/core/loader/src/TeggManifestLoaderFS.ts\n  - tegg/core/types/src/scope/TeggScope.ts\n  - tegg/plugin/dal/src/lib/DataSource.ts\n  - packages/loader-fs/package.json\nupdated_at: 2026-08-03\nstatus: active\n---\n\n# Loader FS Package\n\n`@eggjs/loader-fs` owns the small filesystem interface used by loader code. It\nexports `LoaderFS`, `LoaderFSGlobOptions`, `RealLoaderFS`, and\n`ManifestLoaderFS`.\n\n`LoaderFS` intentionally covers only loader-facing operations: `exists`, `stat`,\n`realpath`, `readJSON`, `glob`, and `loadFile`. It is not a Node.js `fs`\npolyfill. A source may additionally implement `getKnownFiles(directory)` when\nit has an authoritative, precomputed view. `undefined` means the source has no\nsuch view and discovery should fall back to `glob`; an empty array is an\nauthoritative empty directory.\n\n`RealLoaderFS` is the default implementation for normal non-bundled runtime. It\ndelegates file checks to Node `fs`, JSON reads to `utility.readJSONSync`, glob\ndiscovery to `globby.sync`, and module loading to the same `@eggjs/utils`\n`importModule()` path used by the core loader.\n\n`ManifestLoaderFS` presents a host-neutral manifest file index through the same\ninterface. Its input contains only `baseDir`, `fileDiscovery`, and\n`resolveCache`; it does not depend on Egg's `ManifestStore` or on tegg types.\nEgg can pass `ManifestStore` structurally, while standalone uses\n`createTeggManifestLoaderFS()` to adapt\n`TeggManifest.moduleDescriptors[].decoratedFiles` into the same file index.\nManifest directories expose their exact indexed files through `getKnownFiles`,\nincluding TypeScript-origin source keys and authoritative empty lists. Unknown\ndirectories delegate to the fallback source.\n\n`@eggjs/core` depends on this package and re-exports its public API so existing\ncore consumers can still import the loader filesystem boundary from core while\ntegg and later bundled runtime packages can depend on the smaller package\ndirectly.\n\n`ModuleLoader` first asks its `LoaderFS` for an authoritative file list. This\nlets a manifest source control discovery without leaking bundle state into the\ngeneric loader. Sources without such a list keep the normal behavior:\n`LoaderUtil.filePattern()` selects supported runtime extensions and\n`LoaderFS.glob()` scans the directory. Consequently `EGG_TS_ENABLE=false`\ncontinues to exclude real TypeScript files, but it does not discard `.ts` keys\nthat a manifest resolves from an in-memory bundle module map.\n\n`LoaderFS` intentionally does not resolve a tegg module name. The host resolves\nidentity onto `ModuleDescriptor`, `GlobalGraph.moduleConfigList` carries it, and\n`LoadUnitFactory` consumes it. LoaderFS only controls which files are visible to\n`ModuleLoader`.\n\nEach host supplies its selected file view when it starts the initial module\nload. `ModuleLoader.createModuleLoader()` is the integration point that installs\nthat view as the current `TeggScope`'s module-loader filesystem; without a\nhost-supplied view it initializes one `RealLoaderFS`. A later explicit view is\nauthoritative, so a manifest-backed view can replace an earlier real-filesystem\ndefault. Generic `LoaderFS` constructors remain side-effect free and\nhost-neutral. Later module loaders created without an explicit filesystem see\nthe same per-app file view as the initial module scan. This lets dynamic\nmulti-instance callbacks such as DAL keep their existing loader-based discovery\nwithout adding host or loader state to their public context. The scoped value is\ndropped with the app's scope bag and remains isolated between concurrent apps.\n","wiki/packages/onerror.md":"---\ntitle: Onerror Plugin\ntype: package\nsummary: Default Egg error-handling plugin and configurable response negotiation layer.\nsource_files:\n  - plugins/onerror/README.md\n  - plugins/onerror/src/app.ts\n  - plugins/onerror/src/lib/onerror.ts\n  - plugins/onerror/src/lib/error_view.ts\n  - plugins/onerror/src/config/config.default.ts\nupdated_at: 2026-05-06\nstatus: active\n---\n\n# Onerror Plugin\n\n`@eggjs/onerror` is the default Egg error-handling plugin. It installs\n`ctx.onerror` and supports configurable handlers for HTML, text, JSON, JS\n(JSONP), redirect, and catch-all error responses.\n\n## Public Configuration\n\n- `errorPageUrl` redirects production HTML requests after unexpected errors.\n- `accepts` customizes content negotiation.\n- `all`, `html`, `text`, `json`, and `js` customize response handling for\n  specific response types.\n- `appErrorFilter` can suppress logging for selected errors.\n\n## Current Behavior\n\nThe plugin owns its Koa-style `onerror()` implementation in\n`plugins/onerror/src/lib/onerror.ts` instead of importing it from\n`koa-onerror`. This keeps the response behavior local to the plugin and avoids\nreading `koa-onerror` package templates when the plugin app boot hook is\nimported, which is important for static bundling.\n\nDefault HTML error-page rendering still flows through `ErrorView` and the\nplugin's local default template. Applications can continue to provide custom\ntemplates and handlers through the documented `config.onerror` options.\n","wiki/packages/service-worker.md":"---\ntitle: Standalone service worker stack\ntype: package\nsummary: Fetch-semantics standalone runtime — HTTP controllers and MCP tools served from a tegg module without an egg application\nsource_files:\n  - tegg/standalone/service-worker-runtime/src\n  - tegg/standalone/service-worker/src\n  - tegg/standalone/service-worker-controller/src\n  - tegg/standalone/standalone/package.json\n  - tegg/standalone/standalone/src/StandaloneApp.ts\n  - tegg/core/controller-runtime/src\n  - tegg/core/controller-decorator/src\n  - tegg/core/types/src/controller-decorator\n  - tegg/plugin/controller/src\n  - examples/helloworld-service-worker\n  - tools/egg-bundler/src/lib/StandaloneWorkerBundler.ts\nupdated_at: 2026-08-06\nstatus: active\n---\n\nThree packages provide the standalone service worker runtime on top of the\nmodule-plugin mechanism (declarative `@InnerObjectProto` /\n`@XxxLifecycleProto` hooks, see the module-plugin pages):\n\n- `@eggjs/service-worker-runtime` — protocol-agnostic: `@Runner()` entry\n  dispatching events to `@EventHandlerProto('<type>')` handlers, event\n  injection into ContextProtos, `BackgroundTaskHelper` re-export\n  (ctx-destroy draining).\n- `@eggjs/service-worker-controller` — the fetch controller transport (the\n  `serviceWorker` eggModule): `FetchEventHandler`, `FetchRouter` + fetch\n  parameter binding (body/param/query/queries/headers/cookies/request via the\n  `@HTTP*` decorators), `HTTP/MCP RegisterProvider`, `ServiceWorkerMcpRouter`,\n  MCP stateless streamable HTTP under `/mcp[/name]/stream`. `@HTTPCookies()`\n  binds a fetch-native `ServiceWorkerCookies` — an `@eggjs/cookies`-compatible\n  `get`/`set` (reads the `Cookie` header, writes `Set-Cookie` onto\n  `ctx.responseHeaders`) — chosen over `@eggjs/cookies`, which has app coupling\n  (`app.emit('cookieLimitExceed')`, unguarded) + heavy egg-flavored deps unwanted\n  at the edge. Signing/encryption are not implemented (inject `@eggjs/cookies` if\n  needed). Users annotate with the `Cookies` type from `@eggjs/tegg`;\n  `ServiceWorkerCookies` is the internal impl.\n- `@eggjs/service-worker` — the host app only: the `ServiceWorkerApp` facade\n  over `StandaloneApp` with `serve()` (node:http bridge) and embedded\n  `handleEvent()`. Its single frameworkDep is its OWN package root, so the\n  framework modules (runtime + controller) are auto-discovered from its package\n  deps via the node_modules eggModule convention — no hand-listed/ordered\n  packages. Its `index` exports only `ServiceWorkerApp`; consumers import\n  controller decorators and middleware contracts from `@eggjs/tegg`, and the\n  fetch context type from `@eggjs/tegg/standalone`. The transport package remains\n  a host adapter rather than a user-facing controller API.\n\nKey mechanics and constraints:\n\n- **App-wide `config` inner object = the entry app module's `module.yml`**:\n  `StandaloneApp` designates the module scanned from `baseDir` (its module dir IS\n  the cwd) as the entry app module and exposes its `module.yml` as the app-wide\n  `config` inner object (`@Inject() config`), the single config surface — there is\n  no programmatic `config` override. Subsystems read their own slice:\n  `config.backgroundTask.timeout` (BackgroundTaskHelper), `config.mcp` (transport\n  selection + DNS-rebinding options `allowedHosts`/`allowedOrigins`/\n  `enableDnsRebindingProtection`). Config VALUES live in the app's `module.yml`\n  (env variants via `module.<env>.yml`); capability OBJECTS (`mcpAuthHandler`,\n  `fetchContextFactory`, `errorResponseMapper`) are `@InjectOptional()` inner\n  objects a host supplies through the generic `innerObjectHandlers` seam, each\n  with a default when absent (allow-all auth, plain context, unmapped error). So\n  `ServiceWorkerApp` has no options of its own — `ServiceWorkerAppOptions` is an\n  alias of `StandaloneAppOptions`. `config` joins `logger`/`moduleConfigs`/\n  `moduleConfig`/`runtimeConfig` as a framework-owned inner object (a host-provided\n  `config` handler is ignored).\n- **Four-package controller layering** (mirrors the egg host): host-agnostic\n  runtime `@eggjs/controller-runtime` (a plain LIBRARY, not an eggModule — base\n  register classes, collect-only `MCPControllerRegister`, `McpRouter`/`Router`\n  abstractions, `MCPServerHelper`, and the controller inner-object prototypes)\n  → egg transport `@eggjs/controller-plugin` (the `teggController` plugin\n  module) → fetch transport `@eggjs/service-worker-controller` (the\n  `serviceWorker` module) → host app `@eggjs/service-worker`. Each HOST package\n  owns the scanned eggModule and re-exports the runtime's inner-object protos\n  into it (`runtimeProtos.ts`); the runtime library itself is never scanned.\n  The service worker depends only on the egg-free runtime, never on the egg\n  plugin.\n- **Controller Advice executes at the bound invocation boundary**: applications\n  apply an `@Advice` class with `@Middleware` on a controller or method. The shared\n  runtime resolves every such class through the active host container and composes\n  only its `around()` hook around the terminal HTTP or MCP invocation; a class with\n  no `around()` hook simply advances to the next middleware. Argument binding\n  happens before the chain. HTTP writes the method result to the host response\n  before `next()` unwinds. MCP method-level Advice wraps the bound SDK callback;\n  controller-level Advice remains outside transport dispatch so the legacy\n  middleware contract can inspect or replace the streaming response after\n  `next()`.\n  `AbstractControllerAdvice.around()` forwards the host context, `next`, and\n  `AdviceContext` to the existing `middleware()` contract. Explicit `@Pointcut`\n  remains an independent AOP path. Controller packages do not depend on the AOP\n  package, and host context is passed directly with no process-global context map.\n- **Host-agnostic MCP register + `McpRouter` boundary**: the shared\n  `MCPControllerRegister` (`@eggjs/controller-runtime`) only COLLECTS tool/resource/prompt\n  records and delegates transport to an injected `McpRouter`\n  (`registerServer(reg)`); it no longer touches `app`. The egg host's node-HTTP\n  transport is `EggMcpRouter` (built imperatively in the controller plugin's\n  `app.ts`, which holds `app`), the service worker's fetch transport is\n  `ServiceWorkerMcpRouter`. Both provide the `mcpRouter` DI object; the two host\n  plugins are never used together, so the shared name does not collide. Because\n  the register needs no `app`, the egg host no longer provides `eggApp` as a\n  module inner object — the Egg `Application` is out of the module DI graph.\n  Like HTTP, per-proto `register()` only collects during load-unit creation;\n  one `MCPControllerRegister.doRegister()` groups the complete records by\n  server name and calls `mcpRouter.registerServer()` once per server. Egg\n  finalizes on the last `CONTROLLER_LOAD_UNIT`; the fetch host finalizes while\n  constructing its routes on the first event. Each host's `registerServer()`\n  mounts directly — there is no second router `doRegister()` phase.\n  Each collected record contains only its controller proto and MCP metadata;\n  the host passes its container factory once when it creates an\n  `MCPServerHelper`, matching the HTTP register's dependency flow. The helper's\n  request callback resolves the controller lazily in the active context.\n- **HTTP registration has separate validation and mutation phases**:\n  `HTTPControllerRegister` creates each `HTTPMethodRegister` once in priority\n  order, calls `checkDuplicate()` on all of them, then calls `register()` on all\n  of them. `checkDuplicate()` checks the real host router and, for host-gated\n  routes, tracks same-host rules in the existing `checkRouters` map. The first\n  phase is not a temporary copy of the complete host router.\n- **MCP SDK >= 1.29 stateless transports are single-shot** (reuse throws), so\n  each MCP request builds a fresh `MCPServerHelper` + web-standard transport\n  from register records collected at boot. The service worker's\n  `ServiceWorkerMcpRouter` uses `WebStandardStreamableHTTPServerTransport`\n  (Request in, Response out) — no node req/res bridging. Auth is an\n  `mcpAuthHandler` extension point (`@InjectOptional`, via `innerObjectHandlers`;\n  absent → allow); DNS-rebinding options are\n  read from `config.mcp` (see the app-wide config bullet).\n- **Config-selected transport provider** (`config.mcp.transport` +\n  `ServiceWorkerMcpRouter.registerTransport`): `config.mcp.transport` (from the\n  app's `module.yml`) selects which transport mounts per app — the built-in\n  `'web'` (web-standard streamable HTTP, the default) or a host-registered\n  alternative by name. A host registers an alternative via\n  `registerTransport(name, provider)` and points its app config at it\n  (`mcp.transport: <name>`); the provider then fully OWNS the server's transport\n  (mutually exclusive with the built-in), so a host can select an alternative\n  transport (e.g. a node-based SSE `/sse`+`/messages`, which the SDK has no\n  web-standard equivalent for, plus streamable) WHOLESALE without forking the\n  router, a facade `mcp` option, an IoC override, or a module swap. The provider\n  receives an `McpServerMountContext` (the `FetchRouter`, live `registration`,\n  `serverName`/`basePath`, and shared\n  `authenticate`/`createServerHelper`/`selectMiddlewares`/`compose` so it reuses\n  the same auth gate, MCP server helper, and middleware pipeline). The registry\n  is `TeggScope`-scoped per app; an unknown name falls back to the built-in, and\n  node:http stays entirely in the host that registers the alternative. Egg's\n  separate `EggMcpRouter` keeps its extension hooks directly on the router\n  instance.\n- **Streaming lifecycle**: a streaming body keeps pulling from ContextProto\n  objects after the runner returns, but the tegg context is destroyed at return.\n  `FetchEventHandler.#guardResponseStream` tees the body — the client consumes\n  one branch, and the request context's `preDestroy`\n  (`EggContextLifecycleUtil.registerObjectLifecycle`) awaits the other branch\n  draining to a sink — so the ContextProto objects the stream reads from stay\n  alive until the source is fully produced, then the context tears down.\n  (No `config.backgroundTask.timeout` cap, so a legitimately long stream — SSE —\n  is never cut short.)\n- **Unified errors**: framework failures reply `{ code, message }` JSON\n  (`NOT_FOUND` / `INTERNAL_SERVER_ERROR`); `ctx.responseHeaders` merge onto\n  the final response.\n- **frameworkDeps scan excludes `test/**`\\*\\*: the framework packages are\n  themselves modules; without the exclusion their test fixtures load as\n  business modules (duplicate controller names) in workspace layouts.\n- **The runner and business modules share the PUBLIC `eggObjectFactory`**:\n  `@eggjs/standalone` directly depends on `@eggjs/dynamic-inject-runtime`, whose\n  module provides the canonical PUBLIC factory. `StandaloneApp` always scans its\n  own package root as an independent built-in root before host `frameworkDeps`,\n  so this does not rely on recursively scanning framework dependency trees.\n  `ServiceWorkerRunner` injects that public factory to dispatch\n  `AbstractEventHandler` by event type; the service-worker runtime does not\n  provide a second PRIVATE factory. `@eggjs/ajv-plugin` remains opt-in and is\n  discovered only when an application or selected framework root brings it into\n  the standalone module references.\n- Per-app mutable state is owned by the app's TeggScope bag, either through its\n  inner objects or explicit scope slots such as the manifest loader view and MCP\n  transport registry. Two concurrent `ServiceWorkerApp`s are isolated\n  (`test/MultiApp.test.ts`).\n- **Cloudflare Workers bundle (injection-based)**: Node runs a `ServiceWorkerApp`\n  directly (runtime fs scan); workerd has no runtime filesystem, so modules are\n  discovered at build time and inlined by `@eggjs/egg-bundler`'s\n  `StandaloneWorkerBundler` (see the [egg-bundler page](./egg-bundler.md)). The\n  seam keeps the host entry as USER code: a plain `worker.ts`\n  (`new ServiceWorkerApp(dir)` + `export default { fetch }`) that also runs\n  unbundled under Node. The bundler injects the scanned imports + manifest into a\n  build-managed copy beside `worker.ts`; `StandaloneApp.init` falls back to\n  `globalThis.__EGG_BUNDLE_MANIFEST__` (mirroring the existing\n  `__EGG_BUNDLE_MODULE_LOADER__` global) when no `manifest` option is passed, so a\n  single `worker.ts` serves both environments with no build-only import. Verified\n  on workerd (`wrangler dev`): `GET /hello/` + `POST /mcp/calc/stream` both 200.\n\nExample: `examples/helloworld-service-worker` (entry `main.ts` lives outside\nthe scanned `app/` module dir on purpose — the scan imports every module\nfile, and importing an entry that boots the app recurses; `worker.ts` is the\nCloudflare Workers entry, built by `npm run bundle:cf` through `egg-bin bundle`).\n","wiki/packages/tegg-vitest.md":"---\ntitle: Tegg Vitest Adapter\ntype: package\nsummary: Vitest 5 runner for Egg request contexts and tegg module scopes.\nsource_files:\n  - .github/workflows/ci.yml\n  - .utoo.toml\n  - tegg/core/vitest/package.json\n  - tegg/core/vitest/src/runner.ts\n  - tegg/core/vitest/src/index.ts\n  - tegg/core/vitest/src/setup.ts\n  - tegg/core/vitest/test/fixture_app.test.ts\n  - tegg/core/vitest/test/runner-multi-app.test.ts\n  - plugins/mock/package.json\n  - tools/egg-bin/src/commands/test.ts\n  - tools/egg-bin/src/commands/cov.ts\n  - tools/egg-bin/test/commands/test.test.ts\n  - tools/egg-bin/test/commands/cov.test.ts\n  - tools/create-egg/src/templates/simple-ts/package.json\n  - tools/create-egg/src/templates/tegg/package.json\nupdated_at: 2026-10-06\nstatus: active\n---\n\n## Compatibility\n\n`@eggjs/tegg-vitest` requires `vitest@^5.0.1`. The runner extends `TestRunner`\nfrom `vitest`. `@eggjs/mock` has the same range for its optional Vitest peer.\nVitest 4 is no longer supported.\n\nLifecycle overrides use Vitest 5 argument types and\nforward all arguments, including the file list passed to `onAfterRunFiles`\nand the retry options passed to `onBeforeTryTask`.\nThe monorepo catalog uses `^5.0.1` for Vitest, its V8 coverage provider, and its\nUI. The TypeScript and tegg application templates also use Vitest `^5.0.1`.\n\nThe `test-tegg-vitest` CI job installs the catalog versions on Node.js 24 and\n26, typechecks the adapter, and runs its tests with isolated workers and with\none shared thread worker (`--pool threads --no-isolate --maxWorkers 1`).\n\n`egg-bin` uses the Vitest 5 `startVitest` API with `config: false` to prevent\nparent config discovery. Coverage exclusions are relative to the application\nroot; absolute user patterns are converted to relative patterns before Vitest\nmatches them. `TEST_REPORTER=json` uses Vitest 5's default output file,\n`.vitest/json/output.json` under the application root.\n\n## Context lifecycle\n\n`egg-bin` automatically selects the adapter's `runner` and `setup` entry points.\n`configureTeggRunner()` lets a test file choose its application and mock cleanup\nbehavior. During collection, the runner resolves that application and waits for\n`app.ready()`.\n\nThe runner holds a module scope for each file and each test. Before each test\nattempt, it creates a request context and enters it synchronously, before any\nawaited cleanup. Otherwise, the caller's async continuation retains the previous\ncontext on a retry. The previous attempt's scope is released before the new\nscope starts. After the test, the runner releases the test scope and restores\nthe file's context.\n\nThe fixture application tests cover injected service identity and scope cleanup\nacross retries. The concurrent-app regression drives the runner hooks for two\napps that share a service class. It checks separate request contexts and service\ninstances, retry argument forwarding, and independent scope cleanup.\nRun the adapter suite from the repository root with\n`vitest run --root tegg/core/vitest --config vitest.config.ts`.\n","wiki/packages/typings.md":"---\ntitle: Typings Package\ntype: package\nsummary: Shared TypeScript type surface for cross-package Egg typings.\nsource_files:\n  - packages/typings/package.json\n  - packages/typings/src/index.ts\n  - packages/typings/src/global.ts\nupdated_at: 2026-04-26\nstatus: seed\n---\n\n# Typings Package\n\n`@eggjs/typings` is the shared package for type contracts that must be consumed\nby more than one Egg package without creating a dependency on a specific runtime\npackage.\n\nCurrent shared types:\n\n- `BundleModuleLoader` in `packages/typings/src/index.ts`\n- `globalThis.__EGG_BUNDLE_MODULE_LOADER__` augmentation in `packages/typings/src/global.ts`\n\nImport `@eggjs/typings/global` from a package entrypoint when that package needs\nthe shared global augmentation in its type surface.\n","wiki/packages/utils.md":"---\ntitle: Utils Package\ntype: package\nsummary: Shared utility package for Egg module loading, config/plugin helpers, and bundled module-loader integration.\nsource_files:\n  - packages/utils/README.md\n  - packages/utils/src/import.ts\n  - packages/utils/test/bundle-import.test.ts\n  - packages/utils/test/module-importer.test.ts\n  - packages/typings/src/index.ts\n  - packages/typings/src/global.ts\nupdated_at: 2026-06-27\nstatus: active\n---\n\n# Utils Package\n\n`@eggjs/utils` is a shared utility package used across Egg packages.\n\n## Public Surfaces\n\n- `getPlugins(options)`, `getLoadUnits(options)`, `getConfig(options)`, and\n  `getFrameworkPath(options)` expose application/framework discovery helpers.\n- `importModule(filepath, options)` and `importResolve(filepath, options)`\n  centralize runtime module loading.\n- `setBundleModuleLoader(loader)` registers the bundled runtime loader hook.\n\n## Bundled Module Loading\n\n`setBundleModuleLoader(loader)` stores the hook on `globalThis` so bundled and\nexternal copies of `@eggjs/utils` share the same loader. The loader receives the\noriginal `importModule()` filepath after POSIX separator normalization, or a\nvirtual specifier. Returning `undefined` falls back to the normal resolution\npath; other return values follow the same default-export unwrapping rules as\n`importModule()`.\n\nWhen a bundle loader is installed and `importModule()` falls through to native\nESM loading, `packages/utils/src/import.ts` uses an opaque native dynamic import\ncreated with `new Function('specifier', 'return import(specifier);')`. This\nprevents bundlers such as Turbopack from rewriting non-static dynamic import\nexpressions and preserves Node's native fallback for external ESM modules.\n\n## Module-loader hook priority\n\n`importModule()` resolves through three hooks before the native `import()` /\n`require()` fallback (see `packages/utils/src/import.ts`):\n\n1. **`__EGG_BUNDLE_MODULE_LOADER__`** (`setBundleModuleLoader`) — synchronous\n   `globalThis` bundle-map lookup, runs before on-disk resolution and receives\n   the POSIX-normalized filepath. Returning `undefined` falls through.\n2. **Snapshot loader** (`setSnapshotModuleLoader`) — a module-local hook (not a\n   `globalThis` global) used by the V8 snapshot entry generator, keyed by the\n   resolved path. Once registered it handles every load that reaches it, so the\n   importer below is bypassed while it is active.\n3. **`__EGG_MODULE_IMPORTER__`** — async (or sync, since awaited) `globalThis`\n   importer that receives the resolved path (the `importResolve()` result, with\n   OS-native separators — _not_ normalized) and replaces native `await import()`.\n\nThe bundle loader and importer globals are typed in `@eggjs/typings`\n(`BundleModuleLoader`, `ModuleImporter`) and augmented onto `globalThis` in\n`@eggjs/typings/global`. `@eggjs/core`'s `ManifestLoaderFS` (`#loadBundledModule`)\nconsults **only** `__EGG_BUNDLE_MODULE_LOADER__`; the importer/native path is\nreached transitively through its `@eggjs/loader-fs` fallback, which calls back\ninto `importModule()`. The tegg loader (`LoaderUtil.loadFile`) consults both\nglobals directly, but passes the _original_ loader filepath separator-normalized\nto POSIX — not the `importResolve()` output — so the two callers differ in both\nwhich path they pass and whether it is normalized.\n\n`__EGG_MODULE_IMPORTER__` has two contract uses:\n\n- **Bundler-based test runners (Vitest):** route loading through the runner's\n  module graph so loader and test file share one module instance (otherwise\n  `ctx.getEggObject(ClassRef)` throws \"can not get proto\").\n- **V8 startup-snapshot restore:** the deserialized main function has no host\n  dynamic-import callback, so native `import()` throws\n  (`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`). The snapshot entry installs a\n  synchronous `require()`-based importer (`createRequire()` over the output\n  dir); `require()` loads ESM on Node >= 22. Covered by\n  `test/module-importer.test.ts` (inline require importer +\n  `fixtures/module-importer-require-esm/run.mjs`, which asserts the no-callback\n  premise via `node:vm`).\n"},"files":{".github/copilot-instructions.md":"# Eggjs Framework - GitHub Copilot Development Instructions\n\n**Always reference these instructions first and fallback to search or additional context gathering only when you encounter unexpected information that does not match the information provided here.**\n\n## Overview\n\nEggjs is a progressive Node.js framework for building enterprise-class server-side applications. Built on top of Koa.js, it provides a plugin system, conventions over configuration, and enterprise-grade features like clustering, logging, and security.\n\nThis is a **utoo monorepo** with multiple packages using utoo workspaces and catalog mode for centralized dependency management.\n\n## Prerequisites and Environment Setup\n\n- **Node.js >= 22.18.0 required** - This is a hard requirement\n- Enable utoo first: `npm install --global utoo@latest`\n- **NEVER CANCEL** any build or test commands - they can take several minutes to complete\n\n## Bootstrap and Build Process\n\n**Run these commands after a fresh clone:**\n\n```bash\n# 1. Enable utoo (required first)\nnpm install --global utoo@latest\n\n# 2. Install all dependencies - takes ~63 seconds. NEVER CANCEL. Set timeout to 120+ seconds.\nut install\n\n# 3. Run lint to check code quality across all packages - takes ~2 seconds\nut run lint\n\n# 4. Build all packages when validating build output - takes ~14 seconds. NEVER CANCEL. Set timeout to 60+ seconds.\nut run build\n```\n\nRun unit tests from a clean source tree, not immediately after `ut run build`.\nThe main CI test job installs dependencies with `ut install` and\nruns tests with `ut run ci`; it does not run `build` before tests.\n\n## Monorepo Structure\n\n### Key Packages (all in `packages/` directory)\n\n- **`packages/egg/`** - Main Eggjs framework (TypeScript, uses tsdown for builds)\n- **`packages/core/`** - Core plugin framework\n- **`packages/utils/`** - Utility functions\n- **`packages/mock/`** - Testing utilities\n- **`packages/cluster/`** - Cluster management\n- **`packages/koa/`** - Koa web framework\n- **`packages/supertest/`** - HTTP testing utilities\n- **`packages/extend2/`** - Object extension utility\n\n### Supporting Directories\n\n- **`examples/`** - Two example apps: `helloworld-commonjs` and `helloworld-typescript` (currently have runtime issues)\n- **`site/`** - Documentation website built with VitePress\n\n## Essential Commands and Timing\n\n### Build Commands\n\n- `ut run build` - **Build all packages (~14 seconds). NEVER CANCEL. Set timeout to 60+ seconds.**\n- `ut run clean-dist` - Clean all dist directories\n\n### Testing Commands\n\n- `ut run test` - **Run all tests (~2 minutes). NEVER CANCEL. Set timeout to 180+ seconds.**\n- `ut run test:cov` - **Run tests with coverage (~2 minutes). NEVER CANCEL. Set timeout to 180+ seconds.**\n- `ut run ci` - **Run tests with coverage (~2.1 minutes). NEVER CANCEL. Set timeout to 180+ seconds.**\n\n### Linting Commands\n\n- `ut run lint` - Run oxlint across all packages (~2 seconds)\n\n### Documentation Commands\n\n- `ut run site:dev` - Start documentation dev server (defaults to VitePress port 5173)\n- `ut run site:build` - **Build documentation site (~24 seconds). NEVER CANCEL. Set timeout to 60+ seconds.**\n\n### Example Applications (Currently Not Working)\n\n- `ut run example:dev:commonjs` - Start CommonJS example (has runtime issues)\n- `ut run example:dev:typescript` - Start TypeScript example (has runtime issues)\n\n## Package-Specific Commands\n\nRun commands for specific packages using `ut --filter=<package>`:\n\n```bash\n# Examples\nut --filter=egg run test\nut --filter=@eggjs/core run build\nut --filter=site run dev\n```\n\n## Development Workflow\n\n### 1. Making Changes\n\n- Work from the source tree first. Build when you need to validate package output, but do not run build immediately before unit tests.\n- Work primarily in `packages/egg/src/` for core framework features\n- Use TypeScript throughout - all packages are TypeScript-based\n- Follow the existing directory conventions in `packages/egg/src/`:\n  - `lib/` - Core classes (Application, Agent, EggApplicationCore)\n  - `app/extend/` - Framework extensions (context, helper, request, response)\n  - `config/` - Default configurations and plugins\n  - `lib/core/` - Core components (httpclient, logger, messenger)\n  - `lib/loader/` - Application loaders\n\n### 2. Validation Steps\n\n**Always perform these validation steps after making changes:**\n\n```bash\n# 1. Run lint to check code quality across all packages\nut run lint\n\n# 2. Run tests from a clean tree (some failures are expected in fresh environment)\nut run test\n\n# 3. Build all packages when build output or packaging behavior is relevant\nut run build\n\n# 4. Test documentation site when docs changed\nut run site:dev\n```\n\n### 3. Testing Strategy\n\n- **All packages use Vitest for testing** - this is the standard test runner\n- Test files follow pattern: `test/**/*.test.ts`\n- Use `import { describe, it } from 'vitest'` for test functions\n- Use Node.js built-in `assert` module for assertions\n- Create test fixtures in `packages/egg/test/fixtures/apps/` for scenario testing\n\n## Key Framework Concepts\n\n### Architecture\n\n- **EggApplicationCore** - Base application class with core functionality\n- **Application** - Main app class for worker processes\n- **Agent** - Agent process class for background tasks\n- **Context** - Extended Koa context with Egg-specific features\n- **BaseContextClass** - Base for controllers, services, subscriptions\n\n### Loading Convention (Automatic Discovery Order)\n\n1. Plugin system\n2. Configurations\n3. Application/Request/Response/Context extensions\n4. Custom loaders\n5. Services\n6. Middlewares\n7. Controllers\n8. Router\n\n### Cluster vs Single Mode\n\n- **Cluster Mode** (default) - Multi-process with master, agent, and worker processes\n- **Single Mode** - Single process for development/testing\n\n## Working with TypeScript\n\n- All packages use strict TypeScript mode\n- Uses tsdown for unbundled ESM builds (preserves file structure)\n- Each package has `tsdown.config.ts` for build configuration\n- **All sub-project tsconfig.json files MUST extend from root:** `\"extends\": \"../../tsconfig.json\"`\n- Root tsconfig.json includes all packages in `references` array\n\n## utoo Workspace & Catalog Dependencies\n\n- Dependencies defined in `.utoo.toml` catalog section\n- Reference catalog entries: `\"package-name\": \"catalog:\"`\n- Internal workspace dependencies: `\"package-name\": \"workspace:*\"`\n- This ensures consistent versions across all packages\n\n## Common Issues and Troubleshooting\n\n### Test Failures\n\n- Some tests may fail in fresh environments - this is normal\n- Focus on fixing only failures related to your changes\n- Examples may have runtime issues - don't use them for validation\n- If tegg tests fail with `duplicate proto` after a local build, remove stale\n  `dist/` directories outside fixtures and re-run tests. Built `dist/*.js`\n  files can be scanned alongside `src/*.ts`, loading the same decorated class\n  twice.\n\n### Build Issues\n\n- Run `ut run build` when validating build output, package exports, or changes\n  that affect generated artifacts.\n- TypeScript compilation errors will show clearly\n- Build warnings are generally acceptable\n\n### ESM/CommonJS Issues\n\n- Framework supports both ESM and CommonJS\n- Main package exports both formats\n- If you see \"ERR_UNKNOWN_FILE_EXTENSION\" errors, ensure packages are built first\n\n## File Locations Reference\n\n### Key Configuration Files\n\n- `package.json` / `.utoo.toml` - Workspace patterns and catalog configuration\n- `package.json` - Root monorepo scripts and devDependencies\n- `packages/egg/package.json` - Main framework package configuration\n- `packages/egg/tsdown.config.ts` - Build configuration\n- `packages/egg/src/config/plugin.ts` - Built-in plugin configurations\n- `packages/egg/src/config/config.default.ts` - Default framework configuration\n\n### Important Source Files\n\n- `packages/egg/src/lib/application.ts` - Main Application class\n- `packages/egg/src/lib/agent.ts` - Agent process manager\n- `packages/egg/src/lib/egg.ts` - Core EggApplicationCore\n- `packages/egg/src/lib/start.ts` - Application startup logic\n- `packages/egg/src/lib/loader/` - Convention-based loaders\n\n### Build Outputs\n\n- `packages/*/dist/` - Built JavaScript and TypeScript definitions\n- `site/dist/` - Built documentation site\n\n## Validation Scenarios\n\nAfter making changes, always verify:\n\n1. **Linting Passes**: `ut run lint` shows no new errors\n2. **Tests Run From Clean Sources**: `ut run test` executes without stale build artifacts interfering\n3. **Documentation Loads**: `ut run site:dev` starts successfully and the printed VitePress URL responds\n4. **Build Success When Relevant**: `ut run build` completes without errors\n\n**Remember**: This is a complex enterprise framework. Validate incrementally, keep unit tests isolated from stale build artifacts, and focus on the core packages (`egg`, `core`, `utils`) for most development work.\n\n## Commit Message Format\n\n**CRITICAL: All commits MUST follow the [Angular Commit Message Format](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines) as specified in CONTRIBUTING.md.**\n\n### Required Format Structure\n\n```\n<type>(<scope>): <subject>\n<BLANK LINE>\n<body>\n<BLANK LINE>\n<footer>\n```\n\n### Mandatory Types\n\n- **feat**: A new feature\n- **fix**: A bug fix\n- **docs**: Documentation-only changes\n- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)\n- **refactor**: A code change that neither fixes a bug nor adds a feature\n- **perf**: A code change that improves performance\n- **test**: Adding missing tests\n- **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation\n- **deps**: Updates about dependencies\n\n### Scope Guidelines\n\n- **Package-specific changes**: Use package names like `core`, `mock`, `cluster`, `utils`, `tsconfig`, `extend2`\n- **Cross-package changes**: Use feature areas like `loader`, `plugin`, `config`, `build`\n- **Component-specific**: Use component names like `application`, `agent`, `context`\n\n### Subject Rules\n\n- Use imperative, present tense: \"change\" not \"changed\" nor \"changes\"\n- Don't capitalize first letter\n- No period (.) at the end\n- Be succinct and descriptive\n\n### Examples\n\n```\nfeat(tsconfig): integrate package into monorepo with vitest\n\nMerge @eggjs/tsconfig repository into packages/tsconfig/ and refactor\nto use vitest testing framework instead of Node.js test runner.\n\n- Update all consuming packages to use workspace:* dependencies\n- Add vitest configuration and convert test assertions\n- Remove external catalog dependency in favor of workspace package\n\nCloses #123\n```\n\n```\nfix(core): resolve loader initialization race condition\n\nThe loader was attempting to initialize plugins before configurations\nwere fully loaded, causing intermittent startup failures.\n\nFixes #456\n```\n\n```\nchore: update dependencies to latest versions\n\nUpdate catalog dependencies and rebuild packages to ensure\ncompatibility with latest versions.\n```\n\n**NEVER commit without following this format - it breaks the project's automated changelog and release process.**\n","AGENTS.md":"# AGENTS.md\n\nThis is the canonical shared instruction file for coding agents working in this repository.\n\nIf another agent-specific file exists, it should import or defer to this file for shared repository guidance.\n\n## Project Map\n\nEgg is maintained as a utoo monorepo.\n\n- `packages/` contains core framework packages and shared internals.\n- `plugins/` contains optional Egg integrations.\n- `tools/` contains developer tooling such as CLI packages.\n- `tegg/` contains the tegg ecosystem.\n- `examples/` contains sample applications.\n- `site/docs/` contains the English and Chinese documentation site.\n- tests usually live beside packages under `test/`, often with fixtures under `test/fixtures/`.\n\n## Core Commands\n\nThe repository runs on [utoo](https://github.com/utooland/utoo) (`ut`); workspace patterns and overrides live in `package.json`, and shared catalogs live in the tracked `.utoo.toml`. CI installs the latest utoo release; the old pnpm configuration is retained only as a migration reference.\n\n- `npm install --global utoo@latest` installs the latest utoo release on a clean machine.\n- `ut install` hydrates the workspace.\n- `ut run build` builds all packages.\n- `ut run test` runs the main test suite.\n- `ut run lint` runs linting.\n- `ut run typecheck` runs TypeScript checking.\n- use filtered commands for focused work, for example `ut run test --workspace @eggjs/bin` or `ut run build --workspace @eggjs/bin`; prefer the package-name form of `--workspace` (the `./tools/...` path form does not match on Windows); a package without its own script (for example `build` in @eggjs/scripts) needs the root script plus the tsdown workspace path filter instead: `ut run build -- --workspace ./tools/scripts`.\n\n### Local CI\n\nRun tests **without building first**. The CI workflow (`ut install → ut run ci`) never runs `build` before tests. If `dist/` directories exist from a prior build, tegg plugin tests will fail with `duplicate proto` errors because globby scans both `src/*.ts` and `dist/*.js`, loading the same decorated class twice.\n\nWhen you see `duplicate proto` failures locally:\n\n```bash\nfind tegg packages plugins tools -name dist -type d \\\n  -not -path '*/node_modules/*' -not -path '*/test/*' -not -path '*/fixtures/*' \\\n  -exec rm -rf {} +\n```\n\nThen re-run tests.\n\n## Coding Conventions\n\n- prefer existing repo patterns over inventing new ones\n- prefer ESM and TypeScript-first changes where applicable\n- keep file names lowercase with hyphens\n- keep public API changes deliberate and documented\n- use `oxfmt` and `oxlint --type-aware` conventions already present in the repo\n- **tegg multi-app isolation**: do NOT introduce new process-global mutable\n  runtime state in `tegg/`; per-app state must be backed by a `TeggScope` slot.\n  Hooks registered through the bag-pinned `app.*LifecycleUtil` getters need no\n  extra wrap; detached/escape-point access (timers, emitter listeners, proxy\n  handlers, module-level lifecycle-util statics) must run inside\n  `TeggScope.run(app._teggScopeBag, ...)`. See the \"Multi-App Isolation\n  (TeggScope)\" section in `tegg/AGENTS.md` for the full rules.\n- **V8 startup snapshot lifecycle**: a snapshot build runs through\n  `configWillLoad` and resumes from `configDidLoad` only after restore. Plugin\n  constructors and `configWillLoad` must therefore keep only serializable\n  configuration and metadata; create cluster clients, sockets, servers, file\n  watchers, timers, native clients, and other runtime resources in\n  `configDidLoad` or a later hook. If one plugin consumes another plugin's\n  runtime instance, declare that plugin dependency so their `configDidLoad`\n  ordering is deterministic. Do not hide an early initialization violation\n  behind a placeholder/deferred proxy or recorded-call replay; fail fast and\n  move the initialization to the correct lifecycle phase. Use\n  `snapshotWillSerialize`/`snapshotDidDeserialize` only for framework-owned\n  resources that must exist before the cutoff and have an explicit symmetric\n  release/restore implementation.\n- **V8 startup snapshot dependencies**: the egg-bundler can build a V8 startup\n  snapshot (`snapshot: true`), where the app boots only to `configWillLoad` at\n  BUILD time. Any module loaded or instantiated during that boot that creates a\n  non-serializable native binding — llhttp `HTTPParser` (http/https/undici),\n  `nghttp2` (http2, and anything built on it), tls `SecureContext`, dns\n  `ChannelWrap`, a `WebAssembly` instance (undici's llhttp; WASM is disabled under\n  `--build-snapshot`), fs watchers, native addons, open sockets — makes the\n  snapshot build FATAL (\"global handle not serialized\"). Such modules must be kept\n  EXTERNAL (not inlined) so the prelude stubs them at build and forwards to the\n  real module via `globalThis.__RUNTIME_REQUIRE` at restore. The framework default\n  list is `DEFAULT_SNAPSHOT_LAZY_MODULES` in `tools/egg-bundler/src/lib/prelude.ts`\n  (network builtins + `inspector` + `undici` + `urllib`); apps extend it via\n  `egg.snapshot.lazyModules` in `package.json`. **When adding a framework\n  dependency that touches the network/native stack during boot, check whether it\n  must be added to that list.** A package that only reaches the network stack\n  _transitively_ is already covered because those builtins are lazy (e.g.\n  `@modelcontextprotocol/sdk` → `@hono/node-server` → `http2`, `@grpc/grpc-js` →\n  `http2`); only a package that DIRECTLY creates native/WASM state at module-eval\n  or boot-time instantiation (like `undici`) needs adding. See the \"Snapshot\n  lazy-external defaults\" section in `wiki/packages/egg-bundler.md` for details.\n\n## TypeScript Global Types\n\n- put package-wide global augmentations in a dedicated `src/global.ts` or `src/global.d.ts`\n- shared cross-package global types belong in `@eggjs/typings`, not in one consumer package\n- import shared global augmentations from the package entry that needs the type surface, for example `import '@eggjs/typings/global'`\n- keep `declare global` files as modules by using an `import type` or `export {}`\n\n## Testing And PR Expectations\n\n- run the most targeted tests that validate the touched area\n- include regression coverage when changing loader, cluster, agent, HTTP, or process behavior\n- use Angular-style commit messages such as `fix(loader): ensure middleware order`\n- keep PR descriptions clear about motivation, scope, and test evidence\n\n## Security And Config\n\n- review `SECURITY.md` before handling vulnerability-related work\n- do not commit secrets, credentials, or local-only URLs\n- keep local Node.js aligned with `engines.node` and use the latest utoo release\n\n## Shared Knowledge Workflow\n\nThis repository also maintains an LLM-owned wiki for durable project knowledge.\n\n### Explicit Wiki Write Trigger\n\nCreate, update, reorganize, or log wiki content only when the user explicitly\nrequests a wiki build or maintenance task. Reading the wiki for context does\nnot authorize writing to it.\n\nOrdinary coding, debugging, testing, reviews, documentation edits, and PR work\nmust not produce wiki changes, even when they reveal durable findings or change\nAPIs, architecture, contributor workflows, or testing expectations. Do not\ninclude incidental wiki updates in commits or PRs. Report relevant findings in\nthe task response or PR description instead.\n\nThe wiki writing rules below apply only within an explicitly requested wiki\ntask. Use this three-layer model:\n\n### Raw Sources\n\nRaw sources are the source of truth.\n\nThey include:\n\n- repository code under `packages/`, `plugins/`, `tools/`, `tegg/`, `examples/`, and `scripts/`\n- user-facing docs under `site/docs/`\n- root markdown files such as `README.md`, `README.zh-CN.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, and `SECURITY.md`\n- explicit external artifacts referenced by the user or task\n- imported materials stored under `raw/`\n\nRules:\n\n- do not treat wiki summaries as authoritative when raw sources disagree\n- do not rewrite raw sources unless the task requires it\n- during an explicitly requested wiki task, capture repeatedly used external sources in the wiki\n\n### Wiki\n\nThe wiki lives under `wiki/` and stores durable synthesized knowledge.\n\n### Schema\n\nShared workflow rules live here in `AGENTS.md`.\n\nAgent-specific files should stay thin and point back to this file instead of duplicating the schema.\n\n## Wiki Layout\n\n- when consulting the wiki, read `wiki/index.md` first\n- `wiki/log.md` is the append-only chronological log\n- `wiki/packages/` holds package, plugin, tool, and subsystem pages\n- `wiki/concepts/` holds architectural and cross-cutting pages\n- `wiki/workflows/` holds repeatable procedures\n- `wiki/decisions/` holds notable tradeoffs and decisions\n- `wiki/sources/` holds summaries of major source documents or external materials\n\n## Wiki Page Rules\n\nUse these page types:\n\n- `package`\n- `concept`\n- `workflow`\n- `decision`\n- `source`\n\nEvery substantive page should begin with frontmatter:\n\n```yaml\n---\ntitle: Short human-readable title\ntype: package|concept|workflow|decision|source\nsummary: One-line summary\nsource_files:\n  - path/or/url\nupdated_at: YYYY-MM-DD\nstatus: seed|active|stale\n---\n```\n\nUse lowercase kebab-case filenames and keep one topic per page.\n\nUse the workspace-local Asia/Shanghai calendar date for wiki log headings and\nfrontmatter `updated_at` values.\n\n## Citation And Freshness Rules\n\n- every nontrivial wiki claim should be traceable to raw sources\n- list major source paths in `source_files`\n- label non-obvious synthesis as `Inference:`\n- record conflicts explicitly instead of flattening them\n- mark stale or unresolved claims when freshness is uncertain\n\n## Index And Log Rules\n\n`wiki/index.md` should:\n\n- list durable pages by category\n- give each page a one-line summary\n- stay concise enough to scan quickly\n\n`wiki/log.md` should record:\n\n- ingestion of substantial new sources\n- durable findings produced during a query\n- material wiki refactors or lint passes\n- code or docs changes that alter previously recorded understanding\n\nDo not log trivial typo-only edits.\n\n## Standard Workflows\n\nIngest and Lint are wiki maintenance workflows and require an explicit user\nrequest. Query is read-only by default; its write-back steps apply only when\nthe user also explicitly requests wiki construction or maintenance.\n\n### Ingest\n\n1. Read `wiki/index.md`.\n2. Find existing pages that should absorb the new information.\n3. Create or update a `source` page if the source is substantial.\n4. Update impacted wiki pages.\n5. Update `wiki/index.md`.\n6. Append to `wiki/log.md` if the wiki changed materially.\n\n### Query\n\n1. Read `wiki/index.md` and relevant wiki pages first.\n2. Use the wiki as the starting point, not the final authority.\n3. Read raw sources for verification, detail, or freshness.\n4. Only within an explicitly requested wiki task, write durable findings back into the wiki.\n5. Only within that wiki task, update the log when the wiki changes materially.\n\n### Lint\n\nCheck the wiki for:\n\n- orphan pages\n- stale claims after code or docs changes\n- duplicated concepts\n- missing source references\n- missing pages for frequently touched areas\n- contradictions between pages\n\nPrefer restructuring pages over making them longer.\n\n## Repo-Specific Wiki Priorities\n\nPrioritize durable wiki coverage for:\n\n- `packages/egg`, `packages/core`, `packages/utils`, and other foundational packages\n- plugins under `plugins/`\n- tools under `tools/`\n- `tegg/`\n- docs structure under `site/docs/`\n- loading, lifecycle, plugin model, testing, release, and docs-maintenance concepts\n\n## Change Trigger\n\nOnly an explicit user request to build or maintain the wiki triggers wiki\noutput. Code or documentation changes and newly discovered findings do not\ntrigger it automatically. Leave existing wiki files unchanged during ordinary\ntasks.\n","packages/cluster/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fcluster%2FREADME.md -->","packages/cookies/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fcookies%2FREADME.md -->","packages/core/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fcore%2FREADME.md -->","packages/core/benchmark/middleware/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fcore%2Fbenchmark%2Fmiddleware%2FREADME.md -->","packages/egg/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fegg%2FREADME.md -->","packages/errors/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Ferrors%2FREADME.md -->","packages/extend2/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fextend2%2FREADME.md -->","packages/koa-static-cache/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fkoa-static-cache%2FREADME.md -->","packages/koa/Readme.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fkoa%2FReadme.md -->","packages/koa/docs/api/context.md":"# Context\n\nA Koa Context encapsulates node's `request` and `response` objects\ninto a single object which provides many helpful methods for writing\nweb applications and APIs.\nThese operations are used so frequently in HTTP server development\nthat they are added at this level instead of a higher level framework,\nwhich would force middleware to re-implement this common functionality.\n\nA `Context` is created _per_ request, and is referenced in middleware\nas the receiver, or the `ctx` identifier, as shown in the following\nsnippet:\n\n```js\napp.use(async (ctx) => {\n  ctx; // is the Context\n  ctx.request; // is a Koa Request\n  ctx.response; // is a Koa Response\n});\n```\n\nMany of the context's accessors and methods simply delegate to their `ctx.request` or `ctx.response`\nequivalents for convenience, and are otherwise identical. For example `ctx.type` and `ctx.length`\ndelegate to the `response` object, and `ctx.path` and `ctx.method` delegate to the `request`.\n\n## API\n\n`Context` specific methods and accessors.\n\n### ctx.req\n\nNode's `request` object.\n\n### ctx.res\n\nNode's `response` object.\n\nBypassing Koa's response handling is **not supported**. Avoid using the following node properties:\n\n- `res.statusCode`\n- `res.writeHead()`\n- `res.write()`\n- `res.end()`\n\n### ctx.request\n\nA Koa `Request` object.\n\n### ctx.response\n\nA Koa `Response` object.\n\n### ctx.state\n\nThe recommended namespace for passing information through middleware and to your frontend views.\n\n```js\nctx.state.user = await User.find(id);\n```\n\n### ctx.app\n\nApplication instance reference.\n\n### ctx.app.emit\n\nKoa applications extend an internal [EventEmitter](https://nodejs.org/dist/latest-v11.x/docs/api/events.html). `ctx.app.emit` emits an event with a type, defined by the first argument. For each event you can hook up \"listeners\", which is a function that is called when the event is emitted. Consult the [error handling docs](https://koajs.com/#error-handling) for more information.\n\n### ctx.cookies.get(name, [options])\n\nGet cookie `name` with `options`:\n\n- `signed` the cookie requested should be signed\n\nKoa uses the [cookies](https://github.com/pillarjs/cookies) module where options are simply passed.\n\n### ctx.cookies.set(name, value, [options])\n\nSet cookie `name` to `value` with `options`:\n\n- `maxAge`: a number representing the milliseconds from `Date.now()` for expiry.\n- `expires`: a `Date` object indicating the cookie's expiration date (expires at the end of session by default).\n- `path`: a string indicating the path of the cookie (`/` by default).\n- `domain`: a string indicating the domain of the cookie (no default).\n- `secure`: a boolean indicating whether the cookie is only to be sent over HTTPS (`false` by default for HTTP, `true` by default for HTTPS). [Read more about this option](https://github.com/pillarjs/cookies#secure-cookies).\n- `httpOnly`: a boolean indicating whether the cookie is only to be sent over HTTP(S), and not made available to client JavaScript (`true` by default).\n- `sameSite`: a boolean or string indicating whether the cookie is a \"same site\" cookie (`false` by default). This can be set to `'strict'`, `'lax'`, `'none'`, or `true` (which maps to `'strict'`).\n- `signed`: a boolean indicating whether the cookie is to be signed (`false` by default). If this is true, another cookie of the same name with the `.sig` suffix appended will also be sent, with a 27-byte url-safe base64 SHA1 value representing the hash of _cookie-name_=_cookie-value_ against the first [Keygrip](https://www.npmjs.com/package/keygrip) key. This signature key is used to detect tampering the next time a cookie is received.\n- `overwrite`: a boolean indicating whether to overwrite previously set cookies of the same name (`false` by default). If this is true, all cookies set during the same request with the same name (regardless of path or domain) are filtered out of the Set-Cookie header when setting this cookie.\n\nKoa uses the [cookies](https://github.com/pillarjs/cookies) module where options are simply passed.\n\n### ctx.throw([status], [msg], [properties])\n\nHelper method to throw an error with a `.status` property\ndefaulting to `500` that will allow Koa to respond appropriately.\nThe following combinations are allowed:\n\n```js\nctx.throw(400);\nctx.throw(400, 'name required');\nctx.throw(400, 'name required', { user: user });\n```\n\nFor example `ctx.throw(400, 'name required')` is equivalent to:\n\n```js\nconst err = new Error('name required');\nerr.status = 400;\nerr.expose = true;\nthrow err;\n```\n\nNote that these are user-level errors and are flagged with\n`err.expose` meaning the messages are appropriate for\nclient responses, which is typically not the case for\nerror messages since you do not want to leak failure\ndetails.\n\nYou may optionally pass a `properties` object which is merged into the error as-is, useful for decorating machine-friendly errors which are reported to the requester upstream.\n\n```js\nctx.throw(401, 'access_denied', { user: user });\n```\n\nKoa uses [http-errors](https://github.com/jshttp/http-errors) to create errors. `status` should only be passed as the first parameter.\n\n### ctx.assert(value, [status], [msg], [properties])\n\nHelper method to throw an error similar to `.throw()`\nwhen `!value`. Similar to node's [assert()](http://nodejs.org/api/assert.html)\nmethod.\n\n```js\nctx.assert(ctx.state.user, 401, 'User not found. Please login!');\n```\n\n### ctx.respond\n\nTo bypass Koa's built-in response handling, you may explicitly set `ctx.respond = false;`. Use this if you want to write to the raw `res` object instead of letting Koa handle the response for you.\n\nNote that using this is **not** supported by Koa. This may break intended functionality of Koa middleware and Koa itself. Using this property is considered a hack and is only a convenience to those wishing to use traditional `fn(req, res)` functions and middleware within Koa.\n\n## Request aliases\n\nThe following accessors and alias [Request](request.md) equivalents:\n\n- `ctx.header`\n- `ctx.headers`\n- `ctx.method`\n- `ctx.method=`\n- `ctx.url`\n- `ctx.url=`\n- `ctx.originalUrl`\n- `ctx.origin`\n- `ctx.href`\n- `ctx.path`\n- `ctx.path=`\n- `ctx.query`\n- `ctx.query=`\n- `ctx.querystring`\n- `ctx.querystring=`\n- `ctx.host`\n- `ctx.hostname`\n- `ctx.fresh`\n- `ctx.stale`\n- `ctx.socket`\n- `ctx.protocol`\n- `ctx.secure`\n- `ctx.ip`\n- `ctx.ips`\n- `ctx.subdomains`\n- `ctx.is()`\n- `ctx.accepts()`\n- `ctx.acceptsEncodings()`\n- `ctx.acceptsCharsets()`\n- `ctx.acceptsLanguages()`\n- `ctx.get()`\n\n## Response aliases\n\nThe following accessors and alias [Response](response.md) equivalents:\n\n- `ctx.body`\n- `ctx.body=`\n- `ctx.status`\n- `ctx.status=`\n- `ctx.message`\n- `ctx.message=`\n- `ctx.length=`\n- `ctx.length`\n- `ctx.type=`\n- `ctx.type`\n- `ctx.headerSent`\n- `ctx.redirect()`\n- `ctx.attachment()`\n- `ctx.set()`\n- `ctx.append()`\n- `ctx.remove()`\n- `ctx.lastModified=`\n- `ctx.etag=`\n","packages/koa/docs/api/index.md":"# Installation\n\nKoa requires **node v7.6.0** or higher for ES2015 and async function support.\n\nYou can quickly install a supported version of node with your favorite version manager:\n\n```bash\n$ nvm install 7\n$ npm i koa\n$ node my-koa-app.js\n```\n\n# Application\n\nA Koa application is an object containing an array of middleware functions\nwhich are composed and executed in a stack-like manner upon request. Koa is similar to many\nother middleware systems that you may have encountered such as Ruby's Rack, Connect, and so on -\nhowever a key design decision was made to provide high level \"sugar\" at the otherwise low-level\nmiddleware layer. This improves interoperability, robustness, and makes writing middleware much\nmore enjoyable.\n\nThis includes methods for common tasks like content-negotiation, cache freshness, proxy support, and redirection\namong others. Despite supplying a reasonably large number of helpful methods Koa maintains a small footprint, as\nno middleware are bundled.\n\nThe obligatory hello world application:\n\n<!-- runkit:endpoint -->\n\n```js\nconst Koa = require('koa');\nconst app = new Koa();\n\napp.use(async (ctx) => {\n  ctx.body = 'Hello World';\n});\n\napp.listen(3000);\n```\n\n## Cascading\n\nKoa middleware cascade in a more traditional way as you may be used to with similar tools -\nthis was previously difficult to make user friendly with node's use of callbacks.\nHowever with async functions we can achieve \"true\" middleware. Contrasting Connect's implementation which\nsimply passes control through series of functions until one returns, Koa invoke \"downstream\", then\ncontrol flows back \"upstream\".\n\nThe following example responds with \"Hello World\", however first the request flows through\nthe `x-response-time` and `logging` middleware to mark when the request started, then yields control through the response middleware. When a middleware invokes `next()`\nthe function suspends and passes control to the next middleware defined. After there are no more\nmiddleware to execute downstream, the stack will unwind and each middleware is resumed to perform\nits upstream behaviour.\n\n<!-- runkit:endpoint -->\n\n```js\nconst Koa = require('koa');\nconst app = new Koa();\n\n// logger\n\napp.use(async (ctx, next) => {\n  await next();\n  const rt = ctx.response.get('X-Response-Time');\n  console.log(`${ctx.method} ${ctx.url} - ${rt}`);\n});\n\n// x-response-time\n\napp.use(async (ctx, next) => {\n  const start = Date.now();\n  await next();\n  const ms = Date.now() - start;\n  ctx.set('X-Response-Time', `${ms}ms`);\n});\n\n// response\n\napp.use(async (ctx) => {\n  ctx.body = 'Hello World';\n});\n\napp.listen(3000);\n```\n\n## Settings\n\nApplication settings are properties on the `app` instance, currently\nthe following are supported:\n\n- `app.env` defaulting to the **NODE_ENV** or \"development\"\n- `app.keys` array of signed cookie keys\n- `app.proxy` when true proxy header fields will be trusted\n- `app.subdomainOffset` offset of `.subdomains` to ignore, default to 2\n- `app.proxyIpHeader` proxy ip header, default to `X-Forwarded-For`\n- `app.maxIpsCount` max ips read from proxy ip header, default to 0 (means infinity)\n\nYou can pass the settings to the constructor:\n\n```js\nconst Koa = require('koa');\nconst app = new Koa({ proxy: true });\n```\n\nor dynamically:\n\n```js\nconst Koa = require('koa');\nconst app = new Koa();\napp.proxy = true;\n```\n\n## app.listen(...)\n\nA Koa application is not a 1-to-1 representation of an HTTP server.\nOne or more Koa applications may be mounted together to form larger\napplications with a single HTTP server.\n\nCreate and return an HTTP server, passing the given arguments to\n`Server#listen()`. These arguments are documented on [nodejs.org](http://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback). The following is a useless Koa application bound to port `3000`:\n\n```js\nconst Koa = require('koa');\nconst app = new Koa();\napp.listen(3000);\n```\n\nThe `app.listen(...)` method is simply sugar for the following:\n\n```js\nconst http = require('http');\nconst Koa = require('koa');\nconst app = new Koa();\nhttp.createServer(app.callback()).listen(3000);\n```\n\nThis means you can spin up the same application as both HTTP and HTTPS\nor on multiple addresses:\n\n```js\nconst http = require('http');\nconst https = require('https');\nconst Koa = require('koa');\nconst app = new Koa();\nhttp.createServer(app.callback()).listen(3000);\nhttps.createServer(app.callback()).listen(3001);\n```\n\n## app.callback()\n\nReturn a callback function suitable for the `http.createServer()`\nmethod to handle a request.\nYou may also use this callback function to mount your Koa app in a\nConnect/Express app.\n\n## app.use(function)\n\nAdd the given middleware function to this application.\n`app.use()` returns `this`, so is chainable.\n\n```js\napp.use(someMiddleware);\napp.use(someOtherMiddleware);\napp.listen(3000);\n```\n\nIs the same as\n\n```js\napp.use(someMiddleware).use(someOtherMiddleware).listen(3000);\n```\n\nSee [Middleware](https://github.com/koajs/koa/wiki#middleware) for\nmore information.\n\n## app.keys=\n\nSet signed cookie keys.\n\nThese are passed to [KeyGrip](https://github.com/crypto-utils/keygrip),\nhowever you may also pass your own `KeyGrip` instance. For\nexample the following are acceptable:\n\n```js\napp.keys = [\n  'OEK5zjaAMPc3L6iK7PyUjCOziUH3rsrMKB9u8H07La1SkfwtuBoDnHaaPCkG5Brg',\n  'MNKeIebviQnCPo38ufHcSfw3FFv8EtnAe1xE02xkN1wkCV1B2z126U44yk2BQVK7',\n];\napp.keys = new KeyGrip(\n  [\n    'OEK5zjaAMPc3L6iK7PyUjCOziUH3rsrMKB9u8H07La1SkfwtuBoDnHaaPCkG5Brg',\n    'MNKeIebviQnCPo38ufHcSfw3FFv8EtnAe1xE02xkN1wkCV1B2z126U44yk2BQVK7',\n  ],\n  'sha256',\n);\n```\n\nFor security reasons, please ensure that the key is long enough and random.\n\nThese keys may be rotated and are used when signing cookies\nwith the `{ signed: true }` option:\n\n```js\nctx.cookies.set('name', 'tobi', { signed: true });\n```\n\n## app.context\n\n`app.context` is the prototype from which `ctx` is created.\nYou may add additional properties to `ctx` by editing `app.context`.\nThis is useful for adding properties or methods to `ctx` to be used across your entire app,\nwhich may be more performant (no middleware) and/or easier (fewer `require()`s)\nat the expense of relying more on `ctx`, which could be considered an anti-pattern.\n\nFor example, to add a reference to your database from `ctx`:\n\n```js\napp.context.db = db();\n\napp.use(async (ctx) => {\n  console.log(ctx.db);\n});\n```\n\nNote:\n\n- Many properties on `ctx` are defined using getters, setters, and `Object.defineProperty()`. You can only edit these properties (not recommended) by using `Object.defineProperty()` on `app.context`. See https://github.com/koajs/koa/issues/652.\n- Mounted apps currently use their parent's `ctx` and settings. Thus, mounted apps are really just groups of middleware.\n\n## Error Handling\n\nBy default outputs all errors to stderr unless `app.silent` is `true`.\nThe default error handler also won't output errors when `err.status` is `404` or `err.expose` is `true`.\nTo perform custom error-handling logic such as centralized logging you can add an \"error\" event listener:\n\n```js\napp.on('error', (err) => {\n  log.error('server error', err);\n});\n```\n\nIf an error is in the req/res cycle and it is _not_ possible to respond to the client, the `Context` instance is also passed:\n\n```js\napp.on('error', (err, ctx) => {\n  log.error('server error', err, ctx);\n});\n```\n\nWhen an error occurs _and_ it is still possible to respond to the client, aka no data has been written to the socket, Koa will respond\nappropriately with a 500 \"Internal Server Error\". In either case\nan app-level \"error\" is emitted for logging purposes.\n","packages/koa/docs/api/request.md":"# Request\n\nA Koa `Request` object is an abstraction on top of node's vanilla request object,\nproviding additional functionality that is useful for every day HTTP server\ndevelopment.\n\n## API\n\n### request.header\n\nRequest header object. This is the same as the [`headers`](https://nodejs.org/api/http.html#http_message_headers) field\non node's [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage).\n\n### request.header=\n\nSet request header object.\n\n### request.headers\n\nRequest header object. Alias as `request.header`.\n\n### request.headers=\n\nSet request header object. Alias as `request.header=`.\n\n### request.method\n\nRequest method.\n\n### request.method=\n\nSet request method, useful for implementing middleware\nsuch as `methodOverride()`.\n\n### request.length\n\nReturn request Content-Length as a number when present, or `undefined`.\n\n### request.url\n\nGet request URL.\n\n### request.url=\n\nSet request URL, useful for url rewrites.\n\n### request.originalUrl\n\nGet request original URL.\n\n### request.origin\n\nGet origin of URL, include `protocol` and `host`.\n\n```js\nctx.request.origin;\n// => http://example.com\n```\n\n### request.href\n\nGet full request URL, include `protocol`, `host` and `url`.\n\n```js\nctx.request.href;\n// => http://example.com/foo/bar?q=1\n```\n\n### request.path\n\nGet request pathname.\n\n### request.path=\n\nSet request pathname and retain query-string when present.\n\n### request.querystring\n\nGet raw query string void of `?`.\n\n### request.querystring=\n\nSet raw query string.\n\n### request.search\n\nGet raw query string with the `?`.\n\n### request.search=\n\nSet raw query string.\n\n### request.host\n\nGet host (hostname:port) when present. Supports `X-Forwarded-Host`\nwhen `app.proxy` is **true**, otherwise `Host` is used.\n\n### request.hostname\n\nGet hostname when present. Supports `X-Forwarded-Host`\nwhen `app.proxy` is **true**, otherwise `Host` is used.\n\nIf host is IPv6, Koa delegates parsing to\n[WHATWG URL API](https://nodejs.org/dist/latest-v8.x/docs/api/url.html#url_the_whatwg_url_api),\n_Note_ This may impact performance.\n\n### request.URL\n\nGet WHATWG parsed URL object.\n\n### request.type\n\nGet request `Content-Type` void of parameters such as \"charset\".\n\n```js\nconst ct = ctx.request.type;\n// => \"image/png\"\n```\n\n### request.charset\n\nGet request charset when present, or `undefined`:\n\n```js\nctx.request.charset;\n// => \"utf-8\"\n```\n\n### request.query\n\nGet parsed query-string, returning an empty object when no\nquery-string is present. Note that this getter does _not_\nsupport nested parsing.\n\nFor example \"color=blue&size=small\":\n\n```js\n{\n  color: 'blue',\n  size: 'small'\n}\n```\n\n### request.query=\n\nSet query-string to the given object. Note that this\nsetter does _not_ support nested objects.\n\n```js\nctx.query = { next: '/login' };\n```\n\n### request.fresh\n\nCheck if a request cache is \"fresh\", aka the contents have not changed. This\nmethod is for cache negotiation between `If-None-Match` / `ETag`, and `If-Modified-Since` and `Last-Modified`. It should be referenced after setting one or more of these response headers.\n\n```js\n// freshness check requires status 20x or 304\nctx.status = 200;\nctx.set('ETag', '123');\n\n// cache is ok\nif (ctx.fresh) {\n  ctx.status = 304;\n  return;\n}\n\n// cache is stale\n// fetch new data\nctx.body = await db.find('something');\n```\n\n### request.stale\n\nInverse of `request.fresh`.\n\n### request.protocol\n\nReturn request protocol, \"https\" or \"http\". Supports `X-Forwarded-Proto`\nwhen `app.proxy` is **true**.\n\n### request.secure\n\nShorthand for `ctx.protocol == \"https\"` to check if a request was\nissued via TLS.\n\n### request.ip\n\nRequest remote address. Supports `X-Forwarded-For` when `app.proxy`\nis **true**.\n\n### request.ips\n\nWhen `X-Forwarded-For` is present and `app.proxy` is enabled an array\nof these ips is returned, ordered from upstream -> downstream. When\ndisabled an empty array is returned.\n\nFor example if the value were \"client, proxy1, proxy2\",\nyou would receive the array `[\"client\", \"proxy1\", \"proxy2\"]`.\n\nMost of the reverse proxy(nginx) set x-forwarded-for via\n`proxy_add_x_forwarded_for`, which poses a certain security risk.\nA malicious attacker can forge a client's ip address by forging\na `X-Forwarded-For`request header. The request sent by the client\nhas an `X-Forwarded-For` request header for 'forged'. After being\nforwarded by the reverse proxy, `request.ips` will be\n['forged', 'client', 'proxy1', 'proxy2'].\n\nKoa offers two options to avoid being bypassed.\n\nIf you can control the reverse proxy, you can avoid bypassing\nby adjusting the configuration, or use the `app.proxyIpHeader`\nprovided by koa to avoid reading `x-forwarded-for` to get ips.\n\n```js\nconst app = new Koa({\n  proxy: true,\n  proxyIpHeader: 'X-Real-IP',\n});\n```\n\nIf you know exactly how many reverse proxies are in front of\nthe server, you can avoid reading the user's forged request\nheader by configuring `app.maxIpsCount`:\n\n```js\nconst app = new Koa({\n  proxy: true,\n  maxIpsCount: 1, // only one proxy in front of the server\n});\n\n// request.header['X-Forwarded-For'] === [ '127.0.0.1', '127.0.0.2' ];\n// ctx.ips === [ '127.0.0.2' ];\n```\n\n### request.subdomains\n\nReturn subdomains as an array.\n\nSubdomains are the dot-separated parts of the host before the main domain of\nthe app. By default, the domain of the app is assumed to be the last two\nparts of the host. This can be changed by setting `app.subdomainOffset`.\n\nFor example, if the domain is \"tobi.ferrets.example.com\":\nIf `app.subdomainOffset` is not set, `ctx.subdomains` is `[\"ferrets\", \"tobi\"]`.\nIf `app.subdomainOffset` is 3, `ctx.subdomains` is `[\"tobi\"]`.\n\n### request.is(types...)\n\nCheck if the incoming request contains the \"Content-Type\"\nheader field, and it contains any of the give mime `type`s.\nIf there is no request body, `null` is returned.\nIf there is no content type, or the match fails `false` is returned.\nOtherwise, it returns the matching content-type.\n\n```js\n// With Content-Type: text/html; charset=utf-8\nctx.is('html'); // => 'html'\nctx.is('text/html'); // => 'text/html'\nctx.is('text/*', 'text/html'); // => 'text/html'\n\n// When Content-Type is application/json\nctx.is('json', 'urlencoded'); // => 'json'\nctx.is('application/json'); // => 'application/json'\nctx.is('html', 'application/*'); // => 'application/json'\n\nctx.is('html'); // => false\n```\n\nFor example if you want to ensure that\nonly images are sent to a given route:\n\n```js\nif (ctx.is('image/*')) {\n  // process\n} else {\n  ctx.throw(415, 'images only!');\n}\n```\n\n### Content Negotiation\n\nKoa's `request` object includes helpful content negotiation utilities powered by [accepts](http://github.com/expressjs/accepts) and [negotiator](https://github.com/federomero/negotiator). These utilities are:\n\n- `request.accepts(types)`\n- `request.acceptsEncodings(types)`\n- `request.acceptsCharsets(charsets)`\n- `request.acceptsLanguages(langs)`\n\nIf no types are supplied, **all** acceptable types are returned.\n\nIf multiple types are supplied, the best match will be returned. If no matches are found, a `false` is returned, and you should send a `406 \"Not Acceptable\"` response to the client.\n\nIn the case of missing accept headers where any type is acceptable, the first type will be returned. Thus, the order of types you supply is important.\n\n### request.accepts(types)\n\nCheck if the given `type(s)` is acceptable, returning the best match when true, otherwise `false`. The `type` value may be one or more mime type string\nsuch as \"application/json\", the extension name\nsuch as \"json\", or an array `[\"json\", \"html\", \"text/plain\"]`.\n\n```js\n// Accept: text/html\nctx.accepts('html');\n// => \"html\"\n\n// Accept: text/*, application/json\nctx.accepts('html');\n// => \"html\"\nctx.accepts('text/html');\n// => \"text/html\"\nctx.accepts('json', 'text');\n// => \"json\"\nctx.accepts('application/json');\n// => \"application/json\"\n\n// Accept: text/*, application/json\nctx.accepts('image/png');\nctx.accepts('png');\n// => false\n\n// Accept: text/*;q=.5, application/json\nctx.accepts(['html', 'json']);\nctx.accepts('html', 'json');\n// => \"json\"\n\n// No Accept header\nctx.accepts('html', 'json');\n// => \"html\"\nctx.accepts('json', 'html');\n// => \"json\"\n```\n\nYou may call `ctx.accepts()` as many times as you like,\nor use a switch:\n\n```js\nswitch (ctx.accepts('json', 'html', 'text')) {\n  case 'json':\n    break;\n  case 'html':\n    break;\n  case 'text':\n    break;\n  default:\n    ctx.throw(406, 'json, html, or text only');\n}\n```\n\n### request.acceptsEncodings(encodings)\n\nCheck if `encodings` are acceptable, returning the best match when true, otherwise `false`. Note that you should include `identity` as one of the encodings!\n\n```js\n// Accept-Encoding: gzip\nctx.acceptsEncodings('gzip', 'deflate', 'identity');\n// => \"gzip\"\n\nctx.acceptsEncodings(['gzip', 'deflate', 'identity']);\n// => \"gzip\"\n```\n\nWhen no arguments are given all accepted encodings\nare returned as an array:\n\n```js\n// Accept-Encoding: gzip, deflate\nctx.acceptsEncodings();\n// => [\"gzip\", \"deflate\", \"identity\"]\n```\n\nNote that the `identity` encoding (which means no encoding) could be unacceptable if the client explicitly sends `identity;q=0`. Although this is an edge case, you should still handle the case where this method returns `false`.\n\n### request.acceptsCharsets(charsets)\n\nCheck if `charsets` are acceptable, returning\nthe best match when true, otherwise `false`.\n\n```js\n// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5\nctx.acceptsCharsets('utf-8', 'utf-7');\n// => \"utf-8\"\n\nctx.acceptsCharsets(['utf-7', 'utf-8']);\n// => \"utf-8\"\n```\n\nWhen no arguments are given all accepted charsets\nare returned as an array:\n\n```js\n// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5\nctx.acceptsCharsets();\n// => [\"utf-8\", \"utf-7\", \"iso-8859-1\"]\n```\n\n### request.acceptsLanguages(langs)\n\nCheck if `langs` are acceptable, returning\nthe best match when true, otherwise `false`.\n\n```js\n// Accept-Language: en;q=0.8, es, pt\nctx.acceptsLanguages('es', 'en');\n// => \"es\"\n\nctx.acceptsLanguages(['en', 'es']);\n// => \"es\"\n```\n\nWhen no arguments are given all accepted languages\nare returned as an array:\n\n```js\n// Accept-Language: en;q=0.8, es, pt\nctx.acceptsLanguages();\n// => [\"es\", \"pt\", \"en\"]\n```\n\n### request.idempotent\n\nCheck if the request is idempotent.\n\n### request.socket\n\nReturn the request socket.\n\n### request.get(field)\n\nReturn request header with case-insensitive `field`.\n","packages/koa/docs/api/response.md":"# Response\n\nA Koa `Response` object is an abstraction on top of node's vanilla response object,\nproviding additional functionality that is useful for every day HTTP server\ndevelopment.\n\n## API\n\n### response.header\n\nResponse header object.\n\n### response.headers\n\nResponse header object. Alias as `response.header`.\n\n### response.socket\n\nResponse socket. Points to net.Socket instance as `request.socket`.\n\n### response.status\n\nGet response status. By default, `response.status` is set to `404` unlike node's `res.statusCode` which defaults to `200`.\n\n### response.status=\n\nSet response status via numeric code:\n\n- 100 \"continue\"\n- 101 \"switching protocols\"\n- 102 \"processing\"\n- 200 \"ok\"\n- 201 \"created\"\n- 202 \"accepted\"\n- 203 \"non-authoritative information\"\n- 204 \"no content\"\n- 205 \"reset content\"\n- 206 \"partial content\"\n- 207 \"multi-status\"\n- 208 \"already reported\"\n- 226 \"im used\"\n- 300 \"multiple choices\"\n- 301 \"moved permanently\"\n- 302 \"found\"\n- 303 \"see other\"\n- 304 \"not modified\"\n- 305 \"use proxy\"\n- 307 \"temporary redirect\"\n- 308 \"permanent redirect\"\n- 400 \"bad request\"\n- 401 \"unauthorized\"\n- 402 \"payment required\"\n- 403 \"forbidden\"\n- 404 \"not found\"\n- 405 \"method not allowed\"\n- 406 \"not acceptable\"\n- 407 \"proxy authentication required\"\n- 408 \"request timeout\"\n- 409 \"conflict\"\n- 410 \"gone\"\n- 411 \"length required\"\n- 412 \"precondition failed\"\n- 413 \"payload too large\"\n- 414 \"uri too long\"\n- 415 \"unsupported media type\"\n- 416 \"range not satisfiable\"\n- 417 \"expectation failed\"\n- 418 \"I'm a teapot\"\n- 422 \"unprocessable entity\"\n- 423 \"locked\"\n- 424 \"failed dependency\"\n- 426 \"upgrade required\"\n- 428 \"precondition required\"\n- 429 \"too many requests\"\n- 431 \"request header fields too large\"\n- 500 \"internal server error\"\n- 501 \"not implemented\"\n- 502 \"bad gateway\"\n- 503 \"service unavailable\"\n- 504 \"gateway timeout\"\n- 505 \"http version not supported\"\n- 506 \"variant also negotiates\"\n- 507 \"insufficient storage\"\n- 508 \"loop detected\"\n- 510 \"not extended\"\n- 511 \"network authentication required\"\n\n**NOTE**: don't worry too much about memorizing these strings,\nif you have a typo an error will be thrown, displaying this list\nso you can make a correction.\n\nSince `response.status` default is set to `404`, to send a response\nwithout a body and with a different status is to be done like this:\n\n```js\nctx.response.status = 200;\n\n// Or whatever other status\nctx.response.status = 204;\n```\n\n### response.message\n\nGet response status message. By default, `response.message` is\nassociated with `response.status`.\n\n### response.message=\n\nSet response status message to the given value.\n\n### response.length=\n\nSet response Content-Length to the given value.\n\n### response.length\n\nReturn response Content-Length as a number when present, or deduce\nfrom `ctx.body` when possible, or `undefined`.\n\n### response.body\n\nGet response body.\n\n### response.body=\n\nSet response body to one of the following:\n\n- `string` written\n- `Buffer` written\n- `Stream` piped\n- `Object` || `Array` json-stringified\n- `null` || `undefined` no content response\n\nIf `response.status` has not been set, Koa will automatically set the status to `200` or `204` depending on `response.body`. Specifically, if `response.body` has not been set or has been set as `null` or `undefined`, Koa will automatically set `response.status` to `204`. If you really want to send no content response with other status, you should override the `204` status as the following way:\n\n```js\n// This must be always set first before status, since null | undefined\n// body automatically sets the status to 204\nctx.body = null;\n// Now we override the 204 status with the desired one\nctx.status = 200;\n```\n\nKoa doesn't guard against everything that could be put as a response body -- a function doesn't serialise meaningfully, returning a boolean may make sense based on your application, and while an error works, it may not work as intended as some properties of an error are not enumerable. We recommend adding middleware in your app that asserts body types per app. A sample middleware might be:\n\n```js\napp.use(async (ctx, next) => {\n  await next();\n\n  ctx.assert.equal('object', typeof ctx.body, 500, 'some dev did something wrong');\n});\n```\n\n#### String\n\nThe Content-Type is defaulted to text/html or text/plain, both with\na default charset of utf-8. The Content-Length field is also set.\n\n#### Buffer\n\nThe Content-Type is defaulted to application/octet-stream, and Content-Length\nis also set.\n\n#### Stream\n\nThe Content-Type is defaulted to application/octet-stream.\n\nWhenever a stream is set as the response body, `.onerror` is automatically added as a listener to the `error` event to catch any errors.\nIn addition, whenever the request is closed (even prematurely), the stream is destroyed.\nIf you do not want these two features, do not set the stream as the body directly.\nFor example, you may not want this when setting the body as an HTTP stream in a proxy as it would destroy the underlying connection.\n\nSee: https://github.com/koajs/koa/pull/612 for more information.\n\nHere's an example of stream error handling without automatically destroying the stream:\n\n```js\nconst PassThrough = require('stream').PassThrough;\n\napp.use(async (ctx) => {\n  ctx.body = someHTTPStream.on('error', (err) => ctx.onerror(err)).pipe(PassThrough());\n});\n```\n\n#### Object\n\nThe Content-Type is defaulted to application/json. This includes plain objects `{ foo: 'bar' }` and arrays `['foo', 'bar']`.\n\n### response.get(field)\n\nGet a response header field value with case-insensitive `field`.\n\n```js\nconst etag = ctx.response.get('ETag');\n```\n\n### response.has(field)\n\nReturns `true` if the header identified by name is currently set in the outgoing headers.\nThe header name matching is case-insensitive.\n\n```js\nconst rateLimited = ctx.response.has('X-RateLimit-Limit');\n```\n\n### response.set(field, value)\n\nSet response header `field` to `value`:\n\n```js\nctx.set('Cache-Control', 'no-cache');\n```\n\n### response.append(field, value)\n\nAppend additional header `field` with value `val`.\n\n```js\nctx.append('Link', '<http://127.0.0.1/>');\n```\n\n### response.set(fields)\n\nSet several response header `fields` with an object:\n\n```js\nctx.set({\n  Etag: '1234',\n  'Last-Modified': date,\n});\n```\n\nThis delegates to [setHeader](https://nodejs.org/dist/latest/docs/api/http.html#http_request_setheader_name_value) which sets or updates headers by specified keys and doesn't reset the entire header.\n\n### response.remove(field)\n\nRemove header `field`.\n\n### response.type\n\nGet response `Content-Type` void of parameters such as \"charset\".\n\n```js\nconst ct = ctx.type;\n// => \"image/png\"\n```\n\n### response.type=\n\nSet response `Content-Type` via mime string or file extension.\n\n```js\nctx.type = 'text/plain; charset=utf-8';\nctx.type = 'image/png';\nctx.type = '.png';\nctx.type = 'png';\n```\n\nNote: when appropriate a `charset` is selected for you, for\nexample `response.type = 'html'` will default to \"utf-8\". If you need to overwrite `charset`,\nuse `ctx.set('Content-Type', 'text/html')` to set response header field to value directly.\n\n### response.is(types...)\n\nVery similar to `ctx.request.is()`.\nCheck whether the response type is one of the supplied types.\nThis is particularly useful for creating middleware that\nmanipulate responses.\n\nFor example, this is a middleware that minifies\nall HTML responses except for streams.\n\n```js\nconst minify = require('html-minifier');\n\napp.use(async (ctx, next) => {\n  await next();\n\n  if (!ctx.response.is('html')) return;\n\n  let body = ctx.body;\n  if (!body || body.pipe) return;\n\n  if (Buffer.isBuffer(body)) body = body.toString();\n  ctx.body = minify(body);\n});\n```\n\n### response.redirect(url, [alt])\n\nPerform a [302] redirect to `url`.\n\nThe string \"back\" is special-cased\nto provide Referrer support, when Referrer\nis not present `alt` or \"/\" is used.\n\n```js\nctx.redirect('back');\nctx.redirect('back', '/index.html');\nctx.redirect('/login');\nctx.redirect('http://google.com');\n```\n\nTo alter the default status of `302`, simply assign the status\nbefore or after this call. To alter the body, assign it after this call:\n\n```js\nctx.status = 301;\nctx.redirect('/cart');\nctx.body = 'Redirecting to shopping cart';\n```\n\n### response.attachment([filename], [options])\n\nSet `Content-Disposition` to \"attachment\" to signal the client\nto prompt for download. Optionally specify the `filename` of the\ndownload and some [options](https://github.com/jshttp/content-disposition#options).\n\n### response.headerSent\n\nCheck if a response header has already been sent. Useful for seeing\nif the client may be notified on error.\n\n### response.lastModified\n\nReturn the `Last-Modified` header as a `Date`, if it exists.\n\n### response.lastModified=\n\nSet the `Last-Modified` header as an appropriate UTC string.\nYou can either set it as a `Date` or date string.\n\n```js\nctx.response.lastModified = new Date();\n```\n\n### response.etag=\n\nSet the ETag of a response including the wrapped `\"`s.\nNote that there is no corresponding `response.etag` getter.\n\n```js\nctx.response.etag = crypto.createHash('md5').update(ctx.body).digest('hex');\n```\n\n### response.vary(field)\n\nVary on `field`.\n\n### response.flushHeaders()\n\nFlush any set headers, and begin the body.\n","packages/koa/docs/error-handling.md":"# Error Handling\n\n## Try-Catch\n\nUsing async functions means that you can try-catch `next`.\nThis example adds a `.status` to all errors:\n\n```js\napp.use(async (ctx, next) => {\n  try {\n    await next();\n  } catch (err) {\n    err.status = err.statusCode || err.status || 500;\n    throw err;\n  }\n});\n```\n\n### Default Error Handler\n\nThe default error handler is essentially a `try-catch` at\nthe very beginning of the middleware chain. To use a\ndifferent error handler, simply put another `try-catch` at\nthe beginning of the middleware chain, and handle the error\nthere. However, the default error handler is good enough for\nmost use cases. It will use a status code of `err.status`,\nor by default 500. If `err.expose` is true, then `err.message`\nwill be the reply. Otherwise, a message generated from the\nerror code will be used (e.g. for the code 500 the message\n\"Internal Server Error\" will be used). All headers will be\ncleared from the request, but any headers in `err.headers`\nwill then be set. You can use a `try-catch`, as specified\nabove, to add a header to this list.\n\nHere is an example of creating your own error handler:\n\n```js\napp.use(async (ctx, next) => {\n  try {\n    await next();\n  } catch (err) {\n    // will only respond with JSON\n    ctx.status = err.statusCode || err.status || 500;\n    ctx.body = {\n      message: err.message,\n    };\n  }\n});\n```\n\n## The Error Event\n\nError event listeners can be specified with `app.on('error')`.\nIf no error listener is specified, a default error listener\nis used. Error listeners receive all errors that make their\nway back through the middleware chain, if an error is caught\nand not thrown again, it will not be passed to the error\nlistener. If no error event listener is specified, then\n`app.onerror` will be used, which simply log the error unless\n`error.expose`is true or `app.silent` is true or `error.status`\nis 404.\n","packages/koa/docs/faq.md":"# Frequently Asked Questions\n\n## Does Koa replace Express?\n\nIt's more like Connect, but a lot of the Express goodies\nwere moved to the middleware level in Koa to help form\na stronger foundation. This makes middleware more enjoyable\nand less error-prone to write, for the entire stack, not\njust the end application code.\n\nTypically many middleware would\nre-implement similar features, or even worse incorrectly implement them,\nwhen features like signed cookie secrets among others are typically application-specific,\nnot middleware specific.\n\n## Does Koa replace Connect?\n\nNo, just a different take on similar functionality\nnow that async functions allow us to write code with fewer\ncallbacks. Connect is equally capable, and some may still prefer it,\nit's up to what you prefer.\n\n## Does Koa include routing?\n\nNo - out of the box Koa has no form of routing, however\nmany routing middleware are available: https://github.com/koajs/koa/wiki\n\n## Why isn't Koa just Express 4.0?\n\nKoa is a pretty large departure from what people know about Express,\nthe design is fundamentally much different, so the migration from\nExpress 3.0 to this Express 4.0 would effectively mean rewriting\nthe entire application, so we thought it would be more appropriate\nto create a new library.\n\n## What custom properties do the Koa objects have?\n\nKoa uses its own custom objects: `ctx`, `ctx.request`, and `ctx.response`.\nThese objects abstract node's `req` and `res` objects with convenience methods and getters/setters.\nGenerally, properties added to these objects must obey the following rules:\n\n- They must be either very commonly used and/or must do something useful\n- If a property exists as a setter, then it will also exist as a getter, but not vice versa\n\nMany of `ctx.request` and `ctx.response`'s properties are delegated to `ctx`.\nIf it's a getter/setter, then both the getter and the setter will strictly\ncorrespond to either `ctx.request` or `ctx.response`.\n\nPlease think about these rules before suggesting additional properties.\n","packages/koa/docs/guide.md":"# Guide\n\nThis guide covers Koa topics that are not directly API related, such as best practices for writing middleware and application structure suggestions. In these examples we use async functions as middleware - you can also use commonFunction or generatorFunction which will be a little different.\n\n## Table of Contents\n\n- [Guide](#guide)\n  - [Table of Contents](#table-of-contents)\n  - [Writing Middleware](#writing-middleware)\n  - [Middleware Best Practices](#middleware-best-practices)\n    - [Middleware options](#middleware-options)\n    - [Named middleware](#named-middleware)\n    - [Combining multiple middleware with koa-compose](#combining-multiple-middleware-with-koa-compose)\n    - [Response Middleware](#response-middleware)\n  - [Async operations](#async-operations)\n  - [Debugging Koa](#debugging-koa)\n\n## Writing Middleware\n\nKoa middleware are simple functions which return a `MiddlewareFunction` with signature (ctx, next). When\nthe middleware is run, it must manually invoke `next()` to run the \"downstream\" middleware.\n\nFor example if you wanted to track how long it takes for a request to propagate through Koa by adding an\n`X-Response-Time` header field the middleware would look like the following:\n\n```js\nasync function responseTime(ctx, next) {\n  const start = Date.now();\n  await next();\n  const ms = Date.now() - start;\n  ctx.set('X-Response-Time', `${ms}ms`);\n}\n\napp.use(responseTime);\n```\n\nIf you're a front-end developer you can think any code before `next();` as the \"capture\" phase,\nwhile any code after is the \"bubble\" phase. This crude gif illustrates how async function allow us\nto properly utilize stack flow to implement request and response flows:\n\n![Koa middleware](/docs/middleware.gif)\n\n1.  Create a date to track response time\n2.  Await control to the next middleware\n3.  Create another date to track duration\n4.  Await control to the next middleware\n5.  Set the response body to \"Hello World\"\n6.  Calculate duration time\n7.  Output log line\n8.  Calculate response time\n9.  Set `X-Response-Time` header field\n10. Hand off to Koa to handle the response\n\nNext we'll look at the best practices for creating Koa middleware.\n\n## Middleware Best Practices\n\nThis section covers middleware authoring best practices, such as middleware\naccepting options, named middleware for debugging, among others.\n\n### Middleware options\n\nWhen creating a public middleware, it's useful to conform to the convention of\nwrapping the middleware in a function that accepts options, allowing users to\nextend functionality. Even if your middleware accepts _no_ options, this is still\na good idea to keep things uniform.\n\nHere our contrived `logger` middleware accepts a `format` string for customization,\nand returns the middleware itself:\n\n```js\nfunction logger(format) {\n  format = format || ':method \":url\"';\n\n  return async function (ctx, next) {\n    const str = format.replace(':method', ctx.method).replace(':url', ctx.url);\n\n    console.log(str);\n\n    await next();\n  };\n}\n\napp.use(logger());\napp.use(logger(':method :url'));\n```\n\n### Named middleware\n\nNaming middleware is optional, however it's useful for debugging purposes to assign a name.\n\n```js\nfunction logger(format) {\n  return async function logger(ctx, next) {};\n}\n```\n\n### Combining multiple middleware with koa-compose\n\nSometimes you want to \"compose\" multiple middleware into a single middleware for easy re-use or exporting. You can use [koa-compose](https://github.com/koajs/compose)\n\n```js\nconst compose = require('koa-compose');\n\nasync function random(ctx, next) {\n  if ('/random' == ctx.path) {\n    ctx.body = Math.floor(Math.random() * 10);\n  } else {\n    await next();\n  }\n}\n\nasync function backwards(ctx, next) {\n  if ('/backwards' == ctx.path) {\n    ctx.body = 'sdrawkcab';\n  } else {\n    await next();\n  }\n}\n\nasync function pi(ctx, next) {\n  if ('/pi' == ctx.path) {\n    ctx.body = String(Math.PI);\n  } else {\n    await next();\n  }\n}\n\nconst all = compose([random, backwards, pi]);\n\napp.use(all);\n```\n\n### Response Middleware\n\nMiddleware that decide to respond to a request and wish to bypass downstream middleware may\nsimply omit `next()`. Typically this will be in routing middleware, but this can be performed by\nany. For example the following will respond with \"two\", however all three are executed, giving the\ndownstream \"three\" middleware a chance to manipulate the response.\n\n```js\napp.use(async function (ctx, next) {\n  console.log('>> one');\n  await next();\n  console.log('<< one');\n});\n\napp.use(async function (ctx, next) {\n  console.log('>> two');\n  ctx.body = 'two';\n  await next();\n  console.log('<< two');\n});\n\napp.use(async function (ctx, next) {\n  console.log('>> three');\n  await next();\n  console.log('<< three');\n});\n```\n\nThe following configuration omits `next()` in the second middleware, and will still respond\nwith \"two\", however the third (and any other downstream middleware) will be ignored:\n\n```js\napp.use(async function (ctx, next) {\n  console.log('>> one');\n  await next();\n  console.log('<< one');\n});\n\napp.use(async function (ctx, next) {\n  console.log('>> two');\n  ctx.body = 'two';\n  console.log('<< two');\n});\n\napp.use(async function (ctx, next) {\n  console.log('>> three');\n  await next();\n  console.log('<< three');\n});\n```\n\nWhen the furthest downstream middleware executes `next();`, it's really yielding to a noop\nfunction, allowing the middleware to compose correctly anywhere in the stack.\n\n## Async operations\n\nAsync function and promise forms Koa's foundation, allowing\nyou to write non-blocking sequential code. For example this middleware reads the filenames from `./docs`,\nand then reads the contents of each markdown file in parallel before assigning the body to the joint result.\n\n```js\nconst fs = require('mz/fs');\n\napp.use(async function (ctx, next) {\n  const paths = await fs.readdir('docs');\n  const files = await Promise.all(paths.map((path) => fs.readFile(`docs/${path}`, 'utf8')));\n\n  ctx.type = 'markdown';\n  ctx.body = files.join('');\n});\n```\n\n## Debugging Koa\n\nKoa along with many of the libraries it's built with support the **DEBUG** environment variable from [debug](https://github.com/visionmedia/debug) which provides simple conditional logging.\n\nFor example\nto see all Koa-specific debugging information just pass `DEBUG=koa*` and upon boot you'll see the list of middleware used, among other things.\n\n```\n$ DEBUG=koa* node --harmony examples/simple\n  koa:application use responseTime +0ms\n  koa:application use logger +4ms\n  koa:application use contentLength +0ms\n  koa:application use notfound +0ms\n  koa:application use response +0ms\n  koa:application listen +0ms\n```\n\nSince JavaScript does not allow defining function names at\nruntime, you can also set a middleware's name as `._name`.\nThis is useful when you don't have control of a middleware's name.\nFor example:\n\n```js\nconst path = require('path');\nconst serve = require('koa-static');\n\nconst publicFiles = serve(path.join(__dirname, 'public'));\npublicFiles._name = 'static /public';\n\napp.use(publicFiles);\n```\n\nNow, instead of just seeing \"serve\" when debugging, you will see:\n\n```\n  koa:application use static /public +0ms\n```\n","packages/koa/docs/koa-vs-express.md":"# Koa vs Express\n\nPhilosophically, Koa aims to \"fix and replace node\", whereas Express \"augments node\".\nKoa uses promises and async functions to rid apps of callback hell and simplify error handling.\nIt exposes its own `ctx.request` and `ctx.response` objects instead of node's `req` and `res` objects.\n\nExpress, on the other hand, augments node's `req` and `res` objects with additional properties and methods\nand includes many other \"framework\" features, such as routing and templating, which Koa does not.\n\nThus, Koa can be viewed as an abstraction of node.js's `http` modules, where as Express is an application framework for node.js.\n\n|           Feature | Koa | Express | Connect |\n| ----------------: | --- | ------- | ------- |\n| Middleware Kernel | ✓   | ✓       | ✓       |\n|           Routing |     | ✓       |         |\n|        Templating |     | ✓       |         |\n|     Sending Files |     | ✓       |         |\n|             JSONP |     | ✓       |         |\n\nThus, if you'd like to be closer to node.js and traditional node.js-style coding, you probably want to stick to Connect/Express or similar frameworks.\nIf you want to get rid of callbacks, use Koa.\n\nAs result of this different philosophy is that traditional node.js \"middleware\", i.e. functions of the form `(req, res, next)`, are incompatible with Koa. Your application will essentially have to be rewritten from the ground, up.\n\n## Does Koa replace Express?\n\nIt's more like Connect, but a lot of the Express goodies\nwere moved to the middleware level in Koa to help form\na stronger foundation. This makes middleware more enjoyable\nand less error-prone to write, for the entire stack, not\njust the end application code.\n\nTypically many middleware would\nre-implement similar features, or even worse incorrectly implement them,\nwhen features like signed cookie secrets among others are typically application-specific,\nnot middleware specific.\n\n## Does Koa replace Connect?\n\nNo, just a different take on similar functionality\nnow that generators allow us to write code with less\ncallbacks. Connect is equally capable, and some may still prefer it,\nit's up to what you prefer.\n\n## Why isn't Koa just Express 4.0?\n\nKoa is a pretty large departure from what people know about Express,\nthe design is fundamentally much different, so the migration from\nExpress 3.0 to this Express 4.0 would effectively mean rewriting\nthe entire application, so we thought it would be more appropriate\nto create a new library.\n\n## How is Koa different than Connect/Express?\n\n### Promises-based control flow\n\nNo callback hell.\n\nBetter error handling through try/catch.\n\nNo need for domains.\n\n### Koa is barebones\n\nUnlike both Connect and Express, Koa does not include any middleware.\n\nUnlike Express, routing is not provided.\n\nUnlike Express, many convenience utilities are not provided. For example, sending files.\n\nKoa is more modular.\n\n### Koa relies less on middleware\n\nFor example, instead of a \"body parsing\" middleware, you would instead use a body parsing function.\n\n### Koa abstracts node's request/response\n\nLess hackery.\n\nBetter user experience.\n\nProper stream handling.\n\n### Koa routing (third party libraries support)\n\nSince Express comes with its own routing, but Koa does not have\nany in-built routing, there are third party libraries available such as\nkoa-router and koa-route.\nSimilarly, just like we have helmet for security in Express, for Koa\nwe have koa-helmet available and the list goes on for Koa available third\nparty libraries.\n","packages/koa/docs/migration.md":"# Migrating from Koa v1.x to v2.x\n\n## New middleware signature\n\nKoa v2 introduces a new signature for middleware.\n\n**Old signature middleware (v1.x) support will be removed in v3**\n\nThe new middleware signature is:\n\n```js\n// uses async arrow functions\napp.use(async (ctx, next) => {\n  try {\n    await next(); // next is now a function\n  } catch (err) {\n    ctx.body = { message: err.message };\n    ctx.status = err.status || 500;\n  }\n});\n\napp.use(async (ctx) => {\n  const user = await User.getById(this.session.userid); // await instead of yield\n  ctx.body = user; // ctx instead of this\n});\n```\n\nYou don't have to use asynchronous functions - you just have to pass a function that returns a promise.\nA regular function that returns a promise works too!\n\nThe signature has changed to pass `Context` via an explicit parameter, `ctx` above, instead of via\n`this`. The context passing change makes Koa more compatible with es6 arrow functions, which capture `this`.\n\n## Using v1.x Middleware in v2.x\n\nKoa v2.x will try to convert legacy signature, generator middleware on `app.use`, using [koa-convert](https://github.com/koajs/convert).\nIt is however recommended that you choose to migrate all v1.x middleware as soon as possible.\n\n```js\n// Koa will convert\napp.use(function* (next) {\n  const start = Date.now();\n  yield next;\n  const ms = Date.now() - start;\n  console.log(`${this.method} ${this.url} - ${ms}ms`);\n});\n```\n\nYou could do it manually as well, in which case Koa will not convert.\n\n```js\nconst convert = require('koa-convert');\n\napp.use(\n  convert(function* (next) {\n    const start = Date.now();\n    yield next;\n    const ms = Date.now() - start;\n    console.log(`${this.method} ${this.url} - ${ms}ms`);\n  }),\n);\n```\n\n## Upgrading middleware\n\nYou will have to convert your generators to async functions with the new middleware signature:\n\n```js\napp.use(async (ctx, next) => {\n  const user = await Users.getById(this.session.user_id);\n  await next();\n  ctx.body = { message: 'some message' };\n});\n```\n\nUpgrading your middleware may require some work. One migration path is to update them one-by-one.\n\n1. Wrap all your current middleware in `koa-convert`\n2. Test\n3. `npm outdated` to see which Koa middleware is outdated\n4. Update one outdated middleware, remove using `koa-convert`\n5. Test\n6. Repeat steps 3-5 until you're done\n\n## Updating your code\n\nYou should start refactoring your code now to ease migrating to Koa v2:\n\n- Return promises everywhere!\n- Do not use `yield*`\n- Do not use `yield {}` or `yield []`.\n  - Convert `yield []` into `yield Promise.all([])`\n  - Convert `yield {}` into `yield Bluebird.props({})`\n\nYou could also refactor your logic outside of Koa middleware functions. Create functions like\n`function* someLogic(ctx) {}` and call it in your middleware as\n`const result = yield someLogic(this)`.\nNot using `this` will help migrations to the new middleware signature, which does not use `this`.\n\n## Application object constructor requires new\n\nIn v1.x, the Application constructor function could be called directly, without `new` to\ninstantiate an instance of an application. For example:\n\n```js\nvar koa = require('koa');\nvar app = (module.exports = koa());\n```\n\nv2.x uses es6 classes which require the `new` keyword to be used.\n\n```js\nvar koa = require('koa');\nvar app = (module.exports = new koa());\n```\n\n## ENV specific logging behavior removed\n\nAn explicit check for the `test` environment was removed from error handling.\n\n## Dependency changes\n\n- [co](https://github.com/tj/co) is no longer bundled with Koa. Require or import it directly.\n- [composition](https://github.com/thenables/composition) is no longer used and deprecated.\n\n## v1.x support\n\nThe v1.x branch is still supported but should not receive feature updates. Except for this migration\nguide, documentation will target the latest version.\n\n## Help out\n\nIf you encounter migration related issues not covered by this migration guide, please consider\nsubmitting a documentation pull request.\n","packages/koa/docs/troubleshooting.md":"# Troubleshooting Koa\n\n- [Whenever I try to access my route, it sends back a 404](#whenever-i-try-to-access-my-route-it-sends-back-a-404)\n- [My response or context changes have no effect](#my-response-or-context-changes-have-no-effect)\n- [My middleware is not called](#my-middleware-is-not-called)\n\nSee also [debugging Koa](guide.md#debugging-koa).\n\nIf you encounter a problem and later learn how to fix it, and think others might also encounter that problem, please\nconsider contributing to this documentation.\n\n## Whenever I try to access my route, it sends back a 404\n\nThis is a common but troublesome problem when working with Koa middleware. First, it is critical to understand when Koa generates a 404. Koa does not care which or how much middleware was run, in many cases a 200 and 404 trigger the same number of middleware. Instead, the default status for any response is 404. The most obvious way this is changed is through `ctx.status`. However, if `ctx.body` is set when the status has not been explicitly defined (through `ctx.status`), the status is set to 200. This explains why simply setting the body results in a 200. Once the middleware is done (when the middleware and any returned promises are complete), Koa sends out the response. After that, nothing can alter the response. If it was a 404 at the time, it will be a 404 at the end, even if `ctx.status` or `ctx.body` are set afterwords.\n\nEven though we now understand the basis of a 404, it might not be as clear why a 404 is generated in a specific case. This can be especially troublesome when it seems that `ctx.status` or `ctx.body` are set.\n\nThe unexpected 404 is a specific symptom of one of these more general problems:\n\n- [My response or context changes have no effect](#my-response-or-context-changes-have-no-effect)\n- [My middleware is not called](#my-middleware-is-not-called)\n\n## My response or context changes have no effect\n\nThis can be caused when the response is sent before the code making the change is\nexecuted. If the change is to the `ctx.body` or `ctx.status` setter, this can cause a 404 and\nis by far the most common cause of these problems.\n\n### Problematic code\n\n```js\nrouter.get('/fetch', function (ctx, next) {\n  models.Book.findById(parseInt(ctx.query.id)).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\nWhen used, this route will always send back a 404, even though `ctx.body` is set.\n\nThe same behavior would occur in this `async` version:\n\n```js\nrouter.get('/fetch', async (ctx, next) => {\n  models.Book.findById(parseInt(ctx.query.id)).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\n### Cause\n\n`ctx.body` is not set until _after_ the response has been sent. The code doesn't tell Koa to wait for the database to return the record. Koa sends the response after the middleware has been run, but not after the callback inside the middleware has been run. In the gap there, `ctx.body` has not yet been set, so Koa responds with a 404.\n\n### Identifying this as the issue\n\nAdding another piece of middleware and some logging can be extremely helpful in identifying this issue.\n\n```js\nrouter.use('/fetch', function (ctx, next) {\n  return next().then(function () {\n    console.log('Middleware done');\n  });\n});\n\nrouter.get('/fetch', function (ctx, next) {\n  models.Book.findById(parseInt(ctx.query.id)).then(function (book) {\n    ctx.body = book;\n    console.log('Body set');\n  });\n});\n```\n\nIf you see this in the logs:\n\n```\nMiddleware done\nBody set\n```\n\nIt means that the body is being set after the middleware is done, and after the response has been sent. If you see only one or none of these logs, proceed to [My middleware is not called](#my-middleware-is-not-called). If they are in the right order, make sure you haven't explicitly set the status to 404, make sure that it actually is a 404, and if that fails feel free to ask for help.\n\n### Solution\n\n```js\nrouter.get('/fetch', function (ctx, next) {\n  return models.Book.findById(parseInt(ctx.query.id)).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\nReturning the promise given by the database interface tells Koa to wait for the promise to finish before responding. At that time, the body will have been set. This results in Koa sending back a 200 with a proper response.\n\nThe fix in the `async` version is to add an `await` statement:\n\n```js\nrouter.get('/fetch', async (ctx, next) => {\n  await models.Book.findById(parseInt(ctx.query.id)).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\n## My middleware is not called\n\nThis can be due to an interrupted chain of middleware calls. This can cause a 404 if the\nmiddleware that is skipped is responsible for the `ctx.body` or `ctx.status` setter.\nThis is less common than [My response or context changes have no effect](#my-response-or-context-changes-have-no-effect),\nbut it can be a much bigger pain to troubleshoot.\n\n### Problematic code\n\n```js\nrouter.use(function (ctx, next) {\n  // Don't Repeat Yourself! Let's parse the ID here for all our middleware\n  if (ctx.query.id) {\n    ctx.state.id = parseInt(ctx.query.id);\n  }\n});\n\nrouter.get('/fetch', function (ctx, next) {\n  return models.Book.findById(ctx.state.id).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\n### Cause\n\nIn the code above, the book is never fetched from the database, and in fact our route was never called. Look closely at our helper middleware. We forgot to `return next()`! This causes the middleware flow to never reach our route, ending our \"helper\" middleware.\n\n### Identifying this as the issue\n\nIdentifying this problem is easier than most, add a log at the beginning of the route. If it doesn't trigger, your route was never reached in the middleware chain.\n\n```js\nrouter.use(function (ctx, next) {\n  // Don't Repeat Yourself! Let's parse the ID here for all our middleware\n  if (ctx.query.id) {\n    ctx.state.id = parseInt(ctx.query.id);\n  }\n});\n\nrouter.get('/fetch', function (ctx, next) {\n  console.log('Route called'); // Never happens\n  return models.Book.findById(ctx.state.id).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\nTo find the middleware causing the problem, try adding logging at various points in the middleware chain.\n\n### Solution\n\nThe solution for this is rather easy, simply add `return next()` to the end of your helper middleware.\n\n```js\nrouter.use(function (ctx, next) {\n  if (ctx.query.id) {\n    ctx.state.id = parseInt(ctx.query.id);\n  }\n  return next();\n});\n\nrouter.get('/fetch', function (ctx, next) {\n  return models.Book.findById(ctx.state.id).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n","packages/loader-fs/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Floader-fs%2FREADME.md -->","packages/path-matching/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fpath-matching%2FREADME.md -->","packages/router/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Frouter%2FREADME.md -->","packages/skills/AGENTS.md":"# Skills 编写与评测规范\n\n遵循[根目录 AGENTS.md](../../AGENTS.md) 中的共享仓库指引。以下补充 `packages/skills/` 的专属约定。\n\n`packages/skills/` 目录包含 AI agent skills — 纯 markdown 文档，指导 AI 助手使用 Egg 框架。以 `@eggjs/skills` npm 包发布，仅含 `.md` 文件。\n\n> **Skill 编写基础知识**：SKILL.md 格式、frontmatter 规范、目录结构、progressive disclosure、写作风格等通用知识请使用 `/skill-creator` skill 获取指导。以下仅记录 Egg 项目特有的约定。\n\n## Egg Skills 架构\n\nSkills 采用分层路由模式：\n\n- **入口 skill** (`egg/`) — 分析用户意图，通过关键词匹配和决策逻辑路由到专业 skill\n- **专业 skills** — 提供特定领域的深度指导：\n  - `egg-core/` — 核心概念：模块、依赖注入、生命周期、AccessLevel、后台任务\n  - `egg-controller/` — 实现指导：HTTPController、MCPController、Schedule、Ajv 校验\n\n## Egg Skill Frontmatter 约定\n\n- **`name`**：入口 skill 使用 `egg`，专业 skill 以 `egg-` 为前缀（如 `egg-controller`、`egg-core`）\n- **`allowed-tools`**：统一使用 `Read`（纯文档指导型，不修改文件）\n- **`description`**：\n  - 中文：以\"本技能用于...\"开头，包含触发关键词\n  - 英文：以\"Use when...\"开头，包含触发关键词\n\n## 专业 Skill 编写规范\n\n**入口 Skill（如 `egg/`）编写要点：**\n\n对应 /skill-creator 的 **Workflow-Based** 模式。\n\n1. 决策框架包含明确步骤：识别意图 → 检查模糊意图 → 协议/用例特定指示\n2. 为每个专业 skill 列出中英文触发关键词\n3. 冲突解决规则：明确意图模糊时的优先级（如\"基础优先\"——核心概念优先于控制器实现）\n4. 示例分析：多个完整示例，格式为 用户查询 → 分析 → 决策 → 响应策略\n5. 快速参考表：用户意图 → 关键词 → 推荐 skill 映射\n6. 交叉引用话术：回答完主问题后，附\"如需了解 X，请参阅 `@eggjs/skills-xxx`\"\n\n**专业 Skill 两种组织模式：**\n\n| 模式                               | /skill-creator 对应 | 适用场景           | SKILL.md 内容           | references/ 用途   |\n| ---------------------------------- | ------------------- | ------------------ | ----------------------- | ------------------ |\n| **概念型**（如 `egg-core/`）       | Reference-Based     | 概念解释、架构理解 | 自包含的深度内容        | 更深入的专题文档   |\n| **索引型**（如 `egg-controller/`） | Workflow-Based      | 多种实现方式的选择 | 精简的决策树 + 快速参考 | 每种实现的详细指南 |\n\n**概念型 Skill 内容结构：**\n\n1. 概述（一段话说明覆盖范围）\n2. 按概念分块，每个概念包含：定义 → 使用场景 → 装饰器/API 模式 → 代码示例\n3. 重要约束（反模式、限制）\n4. 快速决策指南表（场景 → 推荐方式）\n5. 最佳实践\n6. 参考资料链接\n\n**索引型 Skill 内容结构：**\n\n1. 决策树（根据需求选择实现方式）\n2. 每种类型的快速参考（装饰器、参数、特点）\n3. 最佳实践\n4. 参考资料链接\n\n## Reference 文档编写要点\n\nReference 文档（`references/*.md`）**不需要 YAML frontmatter**，只有 `SKILL.md` 才需要。\n\n**核心原则：Skill 不是文档的重新排版，而是填补\"文档到生产代码\"之间的缝隙。**\n\nSkill 的价值 = 文档 + 实践经验 - 重复内容。如果内容和 `site/docs/` 中的文档高度重复，说明 skill 写得不对。\n\n| 内容类型            | 该放文档（site/docs/） | 该放 Skill（packages/skills/） |\n| ------------------- | ---------------------- | ------------------------------ |\n| API 签名、参数说明  | Yes                    | No（引用文档即可）             |\n| 易错点 / 常见错误   | 部分                   | **Yes（重点）**                |\n| 完整端到端模式      | No                     | **Yes**                        |\n| 跨模块集成知识      | 散落各处               | **Yes（聚合）**                |\n| 文件放置 / 命名约定 | 部分                   | **Yes**                        |\n| 场景化决策树        | No                     | **Yes**                        |\n\n**编写 Reference 文档的具体步骤：**\n\n1. **先读对应的文档**（如 `site/docs/zh-CN/basics/mcpcontroller.md`），理解已有内容\n2. **向维护者提问，收集文档未覆盖的知识**，重点关注：\n   - 导入路径、命名约定等易错点（AI 最容易犯的错）\n   - 文件应该放在哪个目录？命名规则是什么？\n   - 和其他模块（Module、Service、DI）的集成关系\n   - 哪些配置参数实际开发中需要关心，哪些用默认值即可\n   - 哪些是内部扩展机制不需要暴露给应用开发者\n3. **以常见错误表开头** — 把 AI 最容易写错的地方放在最醒目的位置（如错误的导入路径、错误的 API 用法）\n4. **文件约定** — 目录结构、命名规则、配置文件位置，这些文档里通常不会详细说明\n5. **场景化决策树** — 从用户意图出发（\"让 AI 查数据\"），而非从 API 出发（\"@MCPTool\"）\n6. **端到端完整示例** — 从配置文件到控制器到 Service 到测试，展示所有相关文件和它们的关系\n7. **精简的装饰器对照表放末尾** — 仅作为速查，不展开 API 详解\n\n## 添加新 Skill\n\n1. 在 `packages/skills/` 下创建目录：`packages/skills/<skill-name>/`\n2. 创建 `SKILL.md`（格式规范参考 `/skill-creator`，frontmatter 遵循上述 Egg 约定）\n3. 创建 `references/` 目录（初始为空时放置 `.gitkeep`）\n4. 按需在 `references/*.md` 中添加详细参考文档\n5. 更新入口 skill（`egg/SKILL.md`）的路由逻辑以包含新 skill\n6. 如果 skill 涉及 controller 类型，同时更新 `egg-controller/SKILL.md` 决策树\n\n## 添加新 Reference 文档\n\n1. 在 skill 的 `references/` 目录中创建 `.md` 文件\n2. 遵循命名规范（kebab-case，描述性命名：`http-controller.md`、`mcp-controller.md`）\n3. 包含完整的代码示例和决策树\n4. 更新父级 `SKILL.md` 引用新文档\n5. 如果 `references/` 中已有文件，移除 `.gitkeep`\n\n## Skill 评测\n\n评测用例存放在 `packages/skills/eval/` 目录下，用于验证 AI 使用 skill 后的回答质量。\n\n**评测文件结构：**\n\n```text\npackages/skills/eval/\n├── evals-egg-core.json        # egg-core skill 评测用例\n├── evals-egg-controller.json  # egg-controller skill 评测用例\n├── evals-routing.json         # 入口路由评测用例\n├── .gitignore                 # 忽略 *-workspace/ 目录\n└── <skill-name>-workspace/    # 评测输出（gitignored），由 /skill-creator 管理\n    └── iteration-N/\n        ├── REPORT.md          # 对比评分报告\n        ├── GRADING.md         # with-skill 通过率报告\n        └── {prefix}-{id}/     # 每个用例一个目录\n            ├── eval_metadata.json\n            ├── with_skill/outputs/\n            └── without_skill/outputs/\n```\n\n**评测用例 JSON 格式：**\n\n```json\n{\n  \"skill_name\": \"egg-controller\",\n  \"description\": \"控制器评测：覆盖 http-controller、mcp-controller、schedule、ajv-validate\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"用户的任务描述\",\n      \"expected_output\": \"期望输出的关键要素描述\",\n      \"files\": [{ \"path\": \"相对路径\", \"content\": \"文件内容（可选，用于提供上下文或有 bug 的代码）\" }]\n    }\n  ]\n}\n```\n\n**评测流程：**\n\n使用 `/skill-creator` skill 运行评测和生成结果展示。评测流程概述：\n\n1. **编写评测用例** — 在对应的 `evals-*.json` 中添加用例\n2. **运行评测** — 通过 `/skill-creator` 为每个用例启动两个并行 subagent（with-skill 和 site-docs），使用下方 prompt 模板\n3. **评分和展示** — `/skill-creator` 负责评分、生成对比报告、启动可视化 viewer 供人工 review\n4. **改进 skill** — 根据评分结果和人工 feedback 改进 skill 内容，开启新的 iteration\n\n**评测对比的两组环境：**\n\n每个评测用例需要在两种环境下分别运行，对比 skill 是否有效。Prompt 中不应包含任何流程指引（如\"先判断使用哪个 skill\"），只提供参考资料和访问约束，让 AI 自然行动。\n\n| 环境           | system prompt                                                                         | 可访问范围（prompt 约束）  |\n| -------------- | ------------------------------------------------------------------------------------- | -------------------------- |\n| **with-skill** | `egg/SKILL.md`（入口 skill）的完整内容                                                | 仅 `packages/skills/` 目录 |\n| **site-docs**  | 角色声明 + `site/docs/` 的完整文件目录列表（通过 `find site/docs -name '*.md'` 生成） | 仅 `site/docs/` 目录       |\n\n**Prompt 模板：**\n\nwith-skill 环境：\n\n```text\n你是 EGG 框架开发专家。你只能通过 Read 工具读取 packages/skills/ 目录下的文件，不能访问 site/docs/ 或项目源码。\n\n{egg/SKILL.md 的完整内容}\n\n---\n{eval prompt}\n```\n\nsite-docs 环境：\n\n```text\n你是 EGG 框架开发专家。你只能通过 Read 工具读取 site/docs/ 目录下的文件，不能访问 packages/skills/ 或项目源码。项目文档目录如下：\n\n{完整的 site/docs/ 文件列表，通过 find site/docs -name '*.md' | sort 生成}\n\n---\n{eval prompt}\n```\n\n两组 prompt 的差异仅在于参考资料不同，不包含额外的流程提示。subagent 均具备 Read 工具权限。访问范围通过 prompt 约束（软限制，非技术硬限制）。\n\n两组使用相同的 eval prompt，对比输出质量差异。如果 with-skill 没有明显优于 site-docs，说明 skill 内容需要改进——要么缺少文档未覆盖的知识，要么存在与文档的不必要重复。\n\n**输出目录：**\n\n评测结果保存到 `packages/skills/eval/<skill-name>-workspace/iteration-N/` 目录下（已在 `.gitignore` 中通过 `*-workspace/` 忽略），具体目录结构由 `/skill-creator` 管理。\n\n**评测用例设计原则：**\n\n每个 reference 文档至少覆盖 5 个以上评测用例，需覆盖以下场景类型：\n\n| 场景类型             | 说明                                    | 示例                                                     |\n| -------------------- | --------------------------------------- | -------------------------------------------------------- |\n| **泛化需求描述**     | 不了解框架术语，用口语化描述需求        | \"帮我加个参数校验\"                                       |\n| **精确需求描述**     | 明确指定技术方案和约束                  | \"用 TypeBox 定义 Schema，email 用 format: email\"         |\n| **使用咨询**         | 询问用法、区别、选型                    | \"Optional 和 Null 有什么区别\"                            |\n| **问题排查**         | 提供有 bug 的代码（附 files），要求诊断 | \"校验跑不起来，帮我看看\"                                 |\n| **新项目代码生成**   | 在全新项目中从零开始生成功能代码        | \"帮我写一个创建订单的接口，需要做参数校验\"               |\n| **存量项目代码生成** | 在包含老 egg 代码的项目中生成或迁移代码 | \"帮我把这个老的 egg controller 改成 HTTPController 写法\" |\n| **不常用 API**       | 需要查外部文档链接才能回答              | \"用 Tuple 定义元组校验\"                                  |\n","packages/skills/PLAN.md":"# Skills 评测方案设计\n\n## 目标\n\n为 `packages/skills/` 设计一套评测体系，覆盖两个层面：\n\n1. **静态校验** — 验证 Skill 文件的结构正确性、引用完整性\n2. **动态评测** — 用 LLM-as-Judge 评估 AI 基于 Skill 生成回答的质量\n\n全部手动触发运行，不集成 CI。\n\n---\n\n## 目录结构\n\n```\npackages/skills/\n├── egg/\n├── controller/\n├── tegg-core/\n├── eval/                          # 新增：评测目录\n│   ├── static/\n│   │   └── validate.test.ts       # 静态校验测试\n│   ├── dynamic/\n│   │   ├── routing.eval.ts        # 入口路由评测\n│   │   └── quality.eval.ts        # 内容质量评测\n│   ├── fixtures/\n│   │   ├── routing-cases.ts       # 路由测试用例\n│   │   └── quality-cases.ts       # 质量测试用例\n│   └── lib/\n│       ├── skill-loader.ts        # Skill 文件加载器\n│       ├── judge.ts               # LLM-as-Judge 核心逻辑\n│       └── types.ts               # 共享类型定义\n├── vitest.config.ts\n├── package.json\n└── tsconfig.json\n```\n\n---\n\n## 第一部分：静态校验\n\n### 校验项\n\n| 校验项               | 说明                                                          |\n| -------------------- | ------------------------------------------------------------- |\n| **Frontmatter 格式** | 每个 SKILL.md 必须包含 `name`、`description`、`allowed-tools` |\n| **引用文件存在性**   | SKILL.md 中提到的 `references/*.md` 文件必须存在              |\n| **交叉引用一致性**   | 入口 skill 提到的子 skill 目录必须存在且包含 SKILL.md         |\n| **Markdown 结构**    | 标题层级合理（以 `# ` 开头，不跳级）                          |\n| **决策表完整性**     | 入口 skill 的路由表中每个 skill 都有对应目录                  |\n\n### 实现方式\n\n使用 vitest + node:assert 编写测试，通过 Node.js fs API 读取文件并解析：\n\n```typescript\n// eval/static/validate.test.ts\nimport { describe, it } from 'vitest';\nimport assert from 'node:assert/strict';\nimport { loadAllSkills } from '../lib/skill-loader.ts';\n\ndescribe('Skill 静态校验', () => {\n  describe('Frontmatter', () => {\n    it('每个 SKILL.md 包含必填字段: name, description, allowed-tools', ...);\n  });\n\n  describe('引用完整性', () => {\n    it('SKILL.md 中引用的 references/ 文件均存在', ...);\n    it('入口 skill 引用的子 skill 目录均存在', ...);\n  });\n\n  describe('Markdown 结构', () => {\n    it('标题层级不跳级', ...);\n  });\n});\n```\n\n---\n\n## 第二部分：动态评测（LLM-as-Judge）\n\n### 评测维度\n\n动态评测分为两个子场景：\n\n#### 2.1 路由评测 — 入口 Skill 是否正确路由\n\n测试 `egg/SKILL.md` 的决策逻辑：给定用户查询，判断 AI 是否路由到正确的子 skill。\n\n**测试用例结构：**\n\n```typescript\n// eval/fixtures/routing-cases.ts\nexport const routingCases: RoutingCase[] = [\n  {\n    query: '如何创建 HTTP controller？',\n    expectedSkill: 'controller',\n    reason: '明确提到 controller，属于协议实现',\n  },\n  {\n    query: '@SingletonProto 和 @ContextProto 有什么区别？',\n    expectedSkill: 'tegg-core',\n    reason: '关于对象生命周期，属于核心概念',\n  },\n  {\n    query: '我需要创建一个可以被 HTTP 控制器使用的服务',\n    expectedSkill: 'tegg-core',\n    reason: '模糊意图，按规则 1（基础优先）应路由到 core',\n  },\n  // ... 更多用例\n];\n```\n\n**测试实现：**\n\n```typescript\n// eval/dynamic/routing.eval.ts\nimport { describe, it } from 'vitest';\nimport assert from 'node:assert/strict';\nimport Anthropic from '@anthropic-ai/sdk';\nimport { loadSkillContent } from '../lib/skill-loader.ts';\nimport { routingCases } from '../fixtures/routing-cases.ts';\n\nconst client = new Anthropic();\nconst AVAILABLE_SKILLS = ['controller', 'tegg-core'];\n\ndescribe('路由评测', () => {\n  // 加载入口 skill 作为 system prompt\n  const entrySkillContent = loadSkillContent('egg');\n\n  for (const { query, expectedSkill, reason } of routingCases) {\n    it(`\"${query}\" → ${expectedSkill}`, async () => {\n      // 1. 将 SKILL.md 作为 system prompt，发送用户查询\n      const response = await client.messages.create({\n        model: 'claude-sonnet-4-20250514',\n        max_tokens: 1024,\n        system: [\n          entrySkillContent,\n          // 约束输出格式，让 AI 只做路由决策\n          `你是 EGG 框架技能路由器。根据上面的决策指南，分析用户查询并选择应该加载的技能。`,\n          `可选技能: ${AVAILABLE_SKILLS.join(', ')}`,\n          `只输出 JSON: {\"skill\": \"<技能名>\", \"reason\": \"<简要理由>\"}`,\n        ].join('\\n\\n'),\n        messages: [{ role: 'user', content: query }],\n      });\n\n      // 2. 解析 AI 回答中的路由选择\n      const text = response.content[0].type === 'text' ? response.content[0].text : '';\n      const parsed = JSON.parse(text);\n\n      // 3. 断言路由正确性\n      assert.equal(\n        parsed.skill,\n        expectedSkill,\n        `路由错误: 期望 \"${expectedSkill}\" 但得到 \"${parsed.skill}\"` +\n          `\\n  用例理由: ${reason}` +\n          `\\n  AI 理由: ${parsed.reason}`,\n      );\n    });\n  }\n});\n```\n\n#### 2.2 内容质量评测 — 子 Skill 回答质量\n\n测试各子 skill 对领域问题的回答质量。\n\n**测试用例结构：**\n\n```typescript\n// eval/fixtures/quality-cases.ts\nexport const qualityCases: QualityCase[] = [\n  {\n    skill: 'controller',\n    query: '如何创建一个 POST 接口接收 JSON body？',\n    criteria: [\n      '使用 @HTTPController 装饰器',\n      '使用 @HTTPMethod 且 method 为 POST',\n      '使用 @HTTPBody() 获取请求体',\n      '包含完整可运行的代码示例',\n    ],\n    references: ['references/http-controller.md'], // 需要加载的参考文档\n  },\n  {\n    skill: 'tegg-core',\n    query: '如何让一个服务可以被其他模块访问？',\n    criteria: ['提到 AccessLevel.PUBLIC', '使用 @SingletonProto 装饰器', '解释跨模块访问机制'],\n    references: [],\n  },\n  // ... 更多用例\n];\n```\n\n**测试实现：**\n\n```typescript\n// eval/dynamic/quality.eval.ts\nimport { describe, it } from 'vitest';\nimport assert from 'node:assert/strict';\nimport Anthropic from '@anthropic-ai/sdk';\nimport { loadSkillContent, loadReference } from '../lib/skill-loader.ts';\nimport { qualityCases } from '../fixtures/quality-cases.ts';\nimport { judge } from '../lib/judge.ts';\n\nconst client = new Anthropic();\n\ndescribe('内容质量评测', () => {\n  for (const testCase of qualityCases) {\n    describe(`[${testCase.skill}] ${testCase.query}`, () => {\n      let aiResponse: string;\n\n      // Step 1: 加载 skill 内容作为 system prompt，向被测 LLM 提问\n      it('生成回答', async () => {\n        const skillContent = loadSkillContent(testCase.skill);\n        const refContents = testCase.references.map((ref) => loadReference(testCase.skill, ref));\n\n        const systemPrompt = [skillContent, ...refContents].join('\\n\\n---\\n\\n');\n\n        const response = await client.messages.create({\n          model: 'claude-sonnet-4-20250514',\n          max_tokens: 2048,\n          system: systemPrompt,\n          messages: [{ role: 'user', content: testCase.query }],\n        });\n\n        aiResponse = response.content[0].type === 'text' ? response.content[0].text : '';\n        assert.ok(aiResponse.length > 0, 'AI 应该返回非空回答');\n      });\n\n      // Step 2: 用 Judge LLM 对回答逐项评分\n      it('通过质量评审', async () => {\n        const result = await judge(client, {\n          query: testCase.query,\n          response: aiResponse,\n          criteria: testCase.criteria,\n        });\n\n        // 输出详细评分到 console 供人工查看\n        console.log(`    得分: ${result.totalScore} (${result.passed}/${result.total})`);\n        for (const item of result.details) {\n          const icon = item.score === 1 ? '✓' : '✗';\n          console.log(`      ${icon} ${item.criterion}: ${item.reason}`);\n        }\n\n        // 断言：所有 criteria 都应满足\n        assert.ok(\n          result.totalScore >= 0.8,\n          `质量不达标: ${result.totalScore} < 0.8\\n` +\n            result.details\n              .filter((d) => d.score === 0)\n              .map((d) => `  ✗ ${d.criterion}: ${d.reason}`)\n              .join('\\n'),\n        );\n      });\n    });\n  }\n});\n```\n\n### LLM-as-Judge 实现\n\n```typescript\n// eval/lib/judge.ts\nimport type Anthropic from '@anthropic-ai/sdk';\nimport type { JudgeInput, JudgeResult, JudgeDetail } from './types.ts';\n\nexport async function judge(client: Anthropic, input: JudgeInput): Promise<JudgeResult> {\n  const criteriaList = input.criteria.map((c, i) => `${i + 1}. ${c}`).join('\\n');\n\n  const response = await client.messages.create({\n    model: 'claude-sonnet-4-20250514',\n    max_tokens: 1024,\n    system: '你是 AI 回答质量评估专家。严格按照 JSON 格式输出评分结果。',\n    messages: [\n      {\n        role: 'user',\n        content: `请根据评分标准，对以下 AI 回答逐项评分。\n\n## 评分标准\n${criteriaList}\n\n## 用户问题\n${input.query}\n\n## AI 回答\n${input.response}\n\n## 输出格式（严格 JSON）\n{\n  \"details\": [\n    { \"criterion\": \"标准内容\", \"score\": 0 或 1, \"reason\": \"简要理由\" }\n  ]\n}`,\n      },\n    ],\n  });\n\n  const text = response.content[0].type === 'text' ? response.content[0].text : '';\n  const parsed = JSON.parse(text);\n  const details: JudgeDetail[] = parsed.details;\n  const passed = details.filter((d) => d.score === 1).length;\n\n  return {\n    details,\n    passed,\n    total: details.length,\n    totalScore: passed / details.length,\n  };\n}\n```\n\n### 评测报告\n\n运行评测后生成 JSON 报告：\n\n```json\n{\n  \"timestamp\": \"2026-02-05T10:00:00Z\",\n  \"routing\": {\n    \"total\": 10,\n    \"correct\": 9,\n    \"accuracy\": 0.9,\n    \"failures\": [\n      {\n        \"query\": \"...\",\n        \"expected\": \"tegg-core\",\n        \"actual\": \"controller\",\n        \"reason\": \"...\"\n      }\n    ]\n  },\n  \"quality\": {\n    \"controller\": {\n      \"cases\": 5,\n      \"avg_score\": 0.85,\n      \"details\": [...]\n    },\n    \"tegg-core\": {\n      \"cases\": 5,\n      \"avg_score\": 0.90,\n      \"details\": [...]\n    }\n  }\n}\n```\n\n---\n\n## 第三部分：技术选型与依赖\n\n| 组件                  | 选型               | 理由                           |\n| --------------------- | ------------------ | ------------------------------ |\n| 测试框架              | vitest             | 遵循 monorepo 标准             |\n| 断言库                | node:assert/strict | Node.js 内置，零依赖           |\n| YAML frontmatter 解析 | gray-matter        | 成熟的 frontmatter 解析库      |\n| LLM 调用              | @anthropic-ai/sdk  | 使用 Claude API 做评测和 Judge |\n| 报告输出              | JSON 文件          | 简单可读，方便后续扩展为可视化 |\n\n### package.json scripts\n\n```json\n{\n  \"scripts\": {\n    \"test\": \"vitest run --config vitest.config.ts eval/static/\",\n    \"eval\": \"vitest run --config vitest.config.ts eval/dynamic/\",\n    \"eval:routing\": \"vitest run --config vitest.config.ts eval/dynamic/routing.eval.ts\",\n    \"eval:quality\": \"vitest run --config vitest.config.ts eval/dynamic/quality.eval.ts\"\n  }\n}\n```\n\n- `test` — 运行静态校验（快速，无 API 调用）\n- `eval` — 运行全部动态评测\n- `eval:routing` — 仅运行路由评测\n- `eval:quality` — 仅运行内容质量评测\n\n动态评测需设置 `ANTHROPIC_API_KEY` 环境变量。\n\n---\n\n## 实施步骤\n\n1. 在 worktree (`egg-skills-eval`) 中添加依赖：vitest、gray-matter、@anthropic-ai/sdk\n2. 添加 `vitest.config.ts` 和更新 `package.json` scripts\n3. 创建 `eval/lib/` 基础工具：skill-loader、types、judge\n4. 实现 `eval/static/validate.test.ts` 静态校验\n5. 编写路由测试用例 `eval/fixtures/routing-cases.ts`\n6. 实现 `eval/dynamic/routing.eval.ts` 路由评测\n7. 编写质量测试用例 `eval/fixtures/quality-cases.ts`\n8. 实现 `eval/dynamic/quality.eval.ts` 内容质量评测\n","packages/skills/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fskills%2FREADME.md -->","packages/skills/egg-controller/SKILL.md":"---\nname: egg-controller\ndescription: Use when creating API endpoints, implementing protocol handlers, exposing interfaces for specific clients, adding parameter validation, or applying middleware to controllers. Covers HTTP, MCP and Schedule controllers, Ajv/TypeBox parameter validation, and Middleware (function-style and AOP) for EGG framework applications.\nallowed-tools: Read\n---\n\n# EGG 控制器\n\n---\n\n## 控制器选择决策树\n\n```\n需要暴露什么接口/客户端协议？\n\n1. HTTP 接口？例如 HTML/JSON/SSR/SSE，可以使用 HTTPController，参考 `references/http-controller.md`\n\n2. 定时任务，可以使用 Schedule，参考 `references/schedule.md`\n\n3. MCP 接口，可以使用 MCPController，参考 `references/mcp-controller.md`\n\n需要做参数校验？\n\n4. 使用 Ajv + TypeBox 做入参校验，参考 `references/ajv-validate.md`\n\n需要给控制器加中间件（日志、鉴权、耗时统计等横切逻辑）？\n\n5. 使用 Middleware，支持函数式写法和 AOP 写法，参考 `references/middleware.md`\n```\n\n---\n\n## 控制器快速参考\n\n### HTTPController\n\n- **装饰器**：`@HTTPController`、`@HTTPMethod`\n- **参数**：`@HTTPParam`、`@HTTPQuery`、`@HTTPBody`、`@HTTPHeaders`、`@Cookies`、`@Request`、`@Context`\n- **详细文档**：`references/http-controller.md`\n\n### MCPController\n\n- **装饰器**：`@MCPController`、`@MCPTool`、`@MCPPrompt`、`@MCPResource`\n- **特点**：集成 LLM、Zod 验证、登录态支持\n- **详细文档**：`references/mcp-controller.md`\n\n### Schedule\n\n- **装饰器**：`@Schedule<T>`\n- **模式**：Worker/All\n- **详细文档**：`references/schedule.md`\n\n### Ajv 参数校验\n\n- **导入**：`import { Ajv, Type, Static } from 'egg/ajv'`\n- **方式**：`@Inject() ajv: Ajv`，调用 `ajv.validate(schema, data)`\n- **特点**：TypeBox 定义一次 Schema，同时获得校验和 TypeScript 类型\n- **详细文档**：`references/ajv-validate.md`\n\n### Middleware 中间件\n\n- **导入**：`import { Middleware } from 'egg'`（AOP 类从 `import { Advice } from 'egg/aop'`）\n- **写法**：函数式（Koa 中间件函数）和 AOP（@Advice 类），通过 `@Middleware()` 应用\n- **特点**：支持类级别和方法级别，函数式和 AOP 不能在同一个 `@Middleware()` 中混用\n- **详细文档**：`references/middleware.md`\n\n---\n\n## 常见问题排查\n\n| 现象                   | 原因                                     | 解决方案                                                           |\n| ---------------------- | ---------------------------------------- | ------------------------------------------------------------------ |\n| MCP 装饰器 import 报错 | 从 `'egg'` 导入                          | MCP 装饰器从 `'@eggjs/tegg'` 导入，zod 从 `'@eggjs/tegg/zod'` 导入 |\n| MCP Schema 报错        | 用了 `z.object()` 包装                   | 直接用普通对象 `{ name: z.string() }`                              |\n| 定时任务不生效         | 放在 `app/schedule` 目录                 | 放在模块目录中，避免和 egg 默认扫描冲突                            |\n| Ajv 校验 import 报错   | 从 `typebox` 或 `ajv` 导入               | 统一从 `'egg/ajv'` 导入 Type、Static、Ajv 等                       |\n| type 推导不完整        | 用 `type` 定义                           | 用 `interface Foo extends Static<typeof Schema> {}` 代替           |\n| Middleware 混用报错    | 函数和 Advice 类放同一个 `@Middleware()` | 分开写多个 `@Middleware()`，每个内部类型一致                       |\n| AOP Middleware 不生效  | Advice 类没加 `@Advice()` 装饰器         | 必须同时有 `@Advice()` 装饰器才能被识别为 AOP                      |\n\n---\n\n## 最佳实践\n\n- **控制器精简**：业务逻辑委托给 Service 层\n- **参数验证**：使用装饰器和类型定义\n- **错误处理**：根据协议转换错误和响应码\n- **RESTful 设计**：遵循 HTTP 方法和资源命名\n- **响应一致性**：统一响应格式\n\n---\n\n## 参考资料\n\n详细的控制器开发文档：\n\n- `references/http-controller.md` - HTTP 接口完整指南\n- `references/mcp-controller.md` - MCP 接口开发\n- `references/schedule.md` - 定时任务\n- `references/ajv-validate.md` - Ajv 参数校验\n- `references/middleware.md` - Middleware 中间件（函数式 + AOP）\n\n核心概念（`egg-core` skill）：模块、依赖注入、对象生命周期\n\n单元测试（`egg-unittest` skill）：HTTP 接口测试、Service 测试、Mock\n","packages/skills/egg-controller/references/ajv-validate.md":"# Ajv 参数校验指南\n\n## 常见错误\n\n| 错误写法                         | 正确写法                          | 说明                                           |\n| -------------------------------- | --------------------------------- | ---------------------------------------------- |\n| `import { Type } from 'typebox'` | `import { Type } from 'egg/ajv'`  | 必须从 `egg/ajv` 导入，内部已封装 typebox      |\n| `import { Ajv } from 'ajv'`      | `import { Ajv } from 'egg/ajv'`   | Ajv 实例通过 `egg/ajv` 导出                    |\n| `new Ajv()` 手动创建实例         | `@Inject() ajv: Ajv` 注入全局单例 | 框架已配置好 formats 和 keywords，不要自行创建 |\n| 在 Service 中做参数校验          | 在 Controller 中做参数校验        | 入参校验应在 Controller 层完成                 |\n\n---\n\n## 核心概念\n\nEgg 通过 `@eggjs/typebox-validate` 插件集成 Ajv（JSON Schema 校验库）和 TypeBox（TypeScript-first 的 JSON Schema 构建器）。**定义一次 Schema，同时获得参数校验和 TypeScript 类型推导。**\n\n### 导入路径\n\n```typescript\n// 所有 Ajv/TypeBox 相关导入统一从 egg/ajv\nimport { Ajv, Type, Static, TransformEnum } from 'egg/ajv';\n```\n\n---\n\n## 使用方式\n\n通过 `@Inject()` 注入全局 Ajv 单例，在 Controller 方法中调用 `ajv.validate()` 进行校验。\n\n### 完整示例\n\n```typescript\n// app/userModule/UserController.ts\nimport { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPBody, Inject } from 'egg';\nimport { Ajv, Type, Static, TransformEnum } from 'egg/ajv';\n\n// 1. 定义 Schema\nconst CreateUserSchema = Type.Object({\n  name: Type.String({\n    transform: [TransformEnum.trim],\n    minLength: 1,\n    maxLength: 50,\n  }),\n  email: Type.String({ format: 'email' }),\n  age: Type.Optional(Type.Integer({ minimum: 0, maximum: 150 })),\n});\n\n// 2. 从 Schema 推导类型\ntype CreateUserParams = Static<typeof CreateUserSchema>;\n\n// 3. 在 Controller 中注入 Ajv 并校验\n@HTTPController()\nexport class UserController {\n  @Inject()\n  private readonly ajv: Ajv;\n\n  @HTTPMethod({\n    method: HTTPMethodEnum.POST,\n    path: '/api/users',\n  })\n  async create(@HTTPBody() body: CreateUserParams) {\n    // 校验失败自动抛出 AjvInvalidParamError\n    this.ajv.validate(CreateUserSchema, body);\n\n    // 校验通过，body 已经有完整类型提示\n    return { name: body.name, email: body.email };\n  }\n}\n```\n\n---\n\n## Schema 定义\n\n### 常用类型\n\n```typescript\nimport { Type } from 'egg/ajv';\n\nType.String(); // string\nType.Number(); // number\nType.Integer(); // 整数\nType.Boolean(); // boolean\nType.Optional(Type.String()); // string | undefined\nType.Array(Type.String()); // string[]\nType.Object({ name: Type.String() }); // { name: string }\nType.Union([Type.String(), Type.Number()]); // string | number\nType.Literal('admin'); // 'admin'\n```\n\n完整的 TypeBox JSON Schema 类型定义参考：https://github.com/sinclairzx81/typebox#json-types\n\n### 内置 format 校验\n\n框架通过 `ajv-formats` 预注册了以下格式：\n\n| format      | 说明       | 示例                                   |\n| ----------- | ---------- | -------------------------------------- |\n| `email`     | 邮箱       | `user@example.com`                     |\n| `uri`       | URI        | `https://example.com`                  |\n| `uuid`      | UUID       | `550e8400-e29b-41d4-a716-446655440000` |\n| `date`      | 日期       | `2024-01-01`                           |\n| `date-time` | 日期时间   | `2024-01-01T00:00:00Z`                 |\n| `time`      | 时间       | `12:00:00`                             |\n| `ipv4`      | IPv4       | `192.168.1.1`                          |\n| `ipv6`      | IPv6       | `::1`                                  |\n| `hostname`  | 主机名     | `example.com`                          |\n| `regex`     | 正则表达式 | `^\\\\d+$`                               |\n\n```typescript\nType.String({ format: 'email' });\nType.String({ format: 'uuid' });\nType.String({ format: 'date-time' });\n```\n\n### transform 预处理\n\n通过 `ajv-keywords` 的 `transform` 关键字，在校验前对字符串做预处理：\n\n```typescript\nimport { TransformEnum } from 'egg/ajv';\n\nType.String({\n  transform: [TransformEnum.trim], // 去除首尾空格\n});\n\nType.String({\n  transform: [TransformEnum.trim, TransformEnum.toLowerCase], // 去空格 + 转小写\n});\n```\n\n---\n\n## 最佳实践\n\n- **Schema 与 Controller 同文件** — Schema 定义放在使用它的 Controller 文件中，保持就近原则\n- **校验在 Controller 层** — 不要在 Service 层做入参校验，Service 信任上层传入的数据\n- **善用 Optional** — 非必填字段用 `Type.Optional()` 包装，避免前端遗漏字段导致校验失败\n- **善用 transform** — 对用户输入做 trim 预处理，减少脏数据\n","packages/skills/egg-controller/references/http-controller.md":"# HTTPController 开发指南\n\n## 快速开始\n\n### 创建基本 HTTP 接口\n\n使用 `@HTTPController` 和 `@HTTPMethod` 装饰器创建 HTTP 接口：\n\n```typescript\nimport { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPParam } from 'egg';\n\n@HTTPController()\nexport class DemoController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/hello/:name' })\n  async hello(@HTTPParam() name: string) {\n    return { message: 'hello ' + name };\n  }\n}\n```\n\n### 设置路径前缀\n\n为控制器设置统一路径前缀：\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class PathController {\n  // 最终路径: GET /api/hello\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: 'hello' })\n  async hello() {}\n\n  // 最终路径: POST /api/create\n  @HTTPMethod({ method: HTTPMethodEnum.POST, path: 'create' })\n  async create() {}\n}\n```\n\n---\n\n## 参数装饰器决策\n\n### 决策树：选择合适的参数装饰器\n\n```\n需要从 HTTP 请求获取什么信息？\n\n├─ URL 路径参数\n│  └─ → @HTTPParam()\n│\n├─ URL 查询参数\n│  ├─ 单个值 → @HTTPQuery()\n│  └─ 多个值（数组） → @HTTPQueries()\n│\n├─ 请求体（POST/PUT）\n│  └─ → @HTTPBody()\n│     ├─ json → 对象\n│     ├─ text → 字符串\n│\n├─ 请求头\n│  └─ → @HTTPHeaders()\n│     └─ 注意：key 自动转小写\n│\n├─ Cookie\n│  └─ → @Cookies()\n│\n├─ 原始 HTTP 请求对象\n│  └─ → @Request()\n│     └─ 注意：不要和 @HTTPBody 一起消费请求体\n│\n└─ Egg Context（框架功能）\n   └─ → @Context()\n```\n\n### 参考对照表（参数装饰器）\n\n| 装饰器           | 获取内容         | 类型                  | 默认值 | 支持选项            |\n| ---------------- | ---------------- | --------------------- | ------ | ------------------- |\n| `@HTTPParam()`   | URL 路径参数     | `string`              | 变量名 | `{ name?: string }` |\n| `@HTTPQuery()`   | 查询参数（单个） | `string`              | 变量名 | `{ name?: string }` |\n| `@HTTPQueries()` | 查询参数（多个） | `string[]`            | 变量名 | `{ name?: string }` |\n| `@HTTPBody()`    | 请求体           | `object \\| string`    | -      | -                   |\n| `@HTTPHeaders()` | 请求头           | `IncomingHttpHeaders` | -      | -                   |\n| `@Cookies()`     | Cookie           | `HTTPCookies`         | -      | -                   |\n| `@Request()`     | HTTP 请求对象    | `HTTPRequest`         | -      | -                   |\n| `@Context()`     | Egg Context      | `EggContext`          | -      | -                   |\n\n### 快速选择指南\n\n**需要从 URL 获取参数（id）** → `@HTTPParam`\n**需要从查询字符串获取参数（category=books）** → `@HTTPQuery`\n**需要从查询字符串获取全部值（tag=tech&tag=dev）** → `@HTTPQueries`\n**需要获取请求体（POST JSON）** → `@HTTPBody`\n**需要获取请求头字段** → `@HTTPHeaders`（注意：使用小写key）\n**需要读取 Cookie** → `@Cookies`\n**需要访问原始请求对象** → `@Request`\n\n---\n\n## HTTP 响应\n\n### JSON 响应（默认）\n\n直接返回对象，框架自动序列化为 JSON：\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class JsonController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/data' })\n  async getData() {\n    return { result: 'hello world' };\n  }\n}\n```\n\n### 自定义响应\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class ResponseController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/custom' })\n  async customResponse(@Context() ctx: EggContext) {\n    ctx.status = 201;\n    ctx.set('X-Custom', 'value');\n    ctx.type = 'json';\n    return { message: 'Created' };\n  }\n}\n```\n\n---\n\n## 服务端渲染（SSR）\n\n```typescript\n@HTTPController({ path: '/' })\nexport class SSRController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })\n  async render(@Context() ctx: EggContext) {\n    ctx.type = 'html';\n    return `\n      <!DOCTYPE html>\n      <html>\n        <head><title>Home</title></head>\n        <body><h1>Hello World</h1></body>\n      </html>\n    `;\n  }\n}\n```\n\n---\n\n## 流式响应（Streaming）\n\n```typescript\nimport { Readable } from 'node:stream';\nimport { setTimeout } from 'node:timers/promises';\n\nasync function* generateHtml() {\n  yield '<html><body>';\n  for (let i = 1; i <= 5; i++) {\n    yield `<p>Chunk ${i}</p>`;\n    await setTimeout(1000);\n  }\n  yield '</body></html>';\n}\n\n@HTTPController({ path: '/api' })\nexport class StreamController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/stream' })\n  async streamHtml(@Context() ctx: EggContext) {\n    ctx.type = 'html';\n    return Readable.from(generateHtml());\n  }\n}\n```\n\n---\n\n## 路由优先级管理\n\n### 默认优先级规则\n\n| Path                            | RegExp Index | Priority | 说明             |\n| ------------------------------- | ------------ | -------- | ---------------- |\n| `/*`                            | `[0]`        | 0        | 通配符，最低     |\n| `/hello/:name`                  | `[1]`        | 1000     | 单参数           |\n| `/hello/world/message/:message` | `[3]`        | 3000     | 三参数           |\n| `/hello/:name/message/:message` | `[1, 3]`     | 4000     | 多参数，索引更大 |\n| `/hello/world`                  | `[]`         | 100000   | 静态路径，最高   |\n\n### 手动设置优先级\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class PriorityController {\n  @HTTPMethod({\n    method: HTTPMethodEnum.GET,\n    path: '/(api|openapi)/version',\n    priority: 100000, // 提升优先级\n  })\n  async high() {}\n\n  @HTTPMethod({\n    method: HTTPMethodEnum.POST,\n    path: '/(api|openapi)/(.+)',\n  })\n  async low() {}\n}\n```\n\n---\n\n## 参数装饰器详解\n\n### @HTTPParam\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：从 URL 路径中提取参数\n\n**语法**：`@HTTPParam(param?: HTTPParamParams)`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api/users' })\nexport class UserController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: ':userId/posts/:postId' })\n  async getPost(@HTTPParam() userId: string, @HTTPParam() postId: string) {\n    return { userId, postId };\n  }\n}\n```\n\n#### 使用要点\n\n- 参数类型必须是 `string`\n- 参数名默认和变量名相同\n- 支持正则表达式捕获：`path: '/files/(.*)'`\n- 使用 `{ name: '0' }` 获取正则第一个匹配\n- 支持多参数路径\n\n#### 示例\n\n```typescript\n// 路径参数\n@HTTPController({ path: '/api/users' })\nexport class UserController {\n  // GET /users/:id\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: ':id' })\n  async getUser(@HTTPParam() id: string) {\n    return { userId: id };\n  }\n\n  // GET /users/:userId/posts/:postId\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: ':userId/posts/:postId' })\n  async getPost(@HTTPParam() userId: string, @HTTPParam() postId: string) {\n    return { userId, postId };\n  }\n\n  // 获取正则匹配\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/files/(.*)' })\n  async getFile(@HTTPParam({ name: '0' }) path: string) {\n    return { path };\n  }\n}\n```\n\n---\n\n### @HTTPQuery\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：从 URL 查询字符串提取参数（`?key=value`）\n\n**语法**：\n\n- `@HTTPQuery(param?: HTTPQueryParams)` - 返回首个匹配的值（`string`）\n- `@HTTPQueries(param?: HTTPQueriesParams)` - 返回全部值的数组（`string[]`）\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api/search' })\nexport class SearchController {\n  // GET /api/search?category=books\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })\n  async searchByCategory(@HTTPQuery() category: string) {\n    return { category };\n  }\n\n  // GET /api/search?tag=tech&tag=dev\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })\n  async searchByTags(@HTTPQueries({ name: 'tag' }) tags: string[]) {\n    return { tags };\n  }\n}\n```\n\n#### 使用要点\n\n- `@HTTPQuery`：返回首个匹配的值\n- `@HTTPQueries`：返回全部值的数组\n- 参数名默认与变量名匹配\n- 使用 `{ name: 'key' }` 指定查询参数名\n\n---\n\n### @HTTPBody\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：从请求体提取数据（POST/PUT 请求的 body）\n\n**语法**：`@HTTPBody()`\n\n**类型**：`object | string | FormData`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api/users' })\nexport class UserController {\n  @HTTPMethod({ method: HTTPMethodEnum.POST, path: '/' })\n  async createUser(@HTTPBody() body: { name: string; email: string }) {\n    return { userId: '123', ...body };\n  }\n}\n```\n\n#### Content-Type 解析\n\n| Content-Type                        | 解析结果        |\n| ----------------------------------- | --------------- |\n| `application/json`                  | 对象 `object`   |\n| `text/plain`                        | 字符串 `string` |\n| `application/x-www-form-urlencoded` | 对象 `object`   |\n\n**注意**：其他类型注入空值，需用 `@Request` 手动处理\n\n---\n\n### @HTTPHeaders\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：获取 HTTP 请求头中的字段\n\n**语法**：`@HTTPHeaders()`\n\n**类型**：`IncomingHttpHeaders`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class HeaderController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/info' })\n  async getInfo(@HTTPHeaders() headers: IncomingHttpHeaders) {\n    const auth = headers['authorization'];\n    const custom = headers['x-custom'];\n    return { auth, custom };\n  }\n}\n```\n\n#### 使用要点\n\n- Headers key 会自动转为小写，取值时使用小写字符\n- 获取单个值：`headers['x-custom']`\n\n---\n\n### @Cookies\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：从 HTTP Cookie 中读取会话数据\n\n**语法**：`@Cookies()`\n\n**类型**：`HTTPCookies`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class SessionController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/session' })\n  async getSession(@Cookies() cookies: HTTPCookies) {\n    const session = cookies.get('sessionId');\n    return { session };\n  }\n}\n```\n\n#### 使用要点\n\n- 使用 `cookies.get(key)` 读取 Cookie\n- 使用 `{ signed: false }` 读取未签名的 Cookie\n\n---\n\n### @Request\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：访问完整的 HTTP 请求对象\n\n**语法**：`@Request()`\n\n**类型**：`HTTPRequest`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class RequestController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/debug' })\n  async getDebug(@Request() request: HTTPRequest) {\n    const url = request.url;\n    const method = request.method;\n    const contentType = request.headers.get('content-type');\n    const rawBody = await request.text();\n    return { url, method, contentType, bodyLength: rawBody.length };\n  }\n}\n```\n\n#### 使用要点\n\n- `request.url`：请求 URL\n- `request.method`：请求方法\n- `request.headers`：Headers 对象\n- `request.text()`：读取请求体为文本\n- `request.arrayBuffer()`：读取请求体为 ArrayBuffer\n\n### @Context\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：访问 Egg 框架的 Context 对象\n\n**语法**：`@Context()`\n\n**类型**：`EggContext`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class DebugController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/debug' })\n  async debug(@Context() ctx: EggContext) {\n    return {\n      app: ctx.app.name,\n      ip: ctx.ip,\n      userAgent: ctx.get('user-agent'),\n    };\n  }\n}\n```\n","packages/skills/egg-controller/references/mcp-controller.md":"# MCPController 开发指南\n\n## 常见错误\n\n生成 MCPController 代码时，**必须**注意以下易错点：\n\n| 错误写法                         | 正确写法                              | 说明                                     |\n| -------------------------------- | ------------------------------------- | ---------------------------------------- |\n| `from 'egg'`                     | `from '@eggjs/tegg'`                  | 所有 MCP 装饰器和类型来自 `@eggjs/tegg`  |\n| `import z from 'zod'`            | `import { z } from '@eggjs/tegg/zod'` | 框架内置 zod，必须使用具名导入           |\n| `z.object({ name: z.string() })` | `{ name: z.string() }`                | Schema 使用普通对象，不要用 `z.object()` |\n| `args: ToolArgs<MySchema>`       | `args: ToolArgs<typeof MySchema>`     | 类型参数必须用 `typeof`                  |\n| `@MCPController` 不加括号        | `@MCPController()`                    | 装饰器必须带括号调用                     |\n\n---\n\n## 文件约定\n\n### 文件位置与命名\n\nMCPController 放在 module 的 `controller/` 目录下，命名规则为 `{Name}MCPController.ts`：\n\n```\napp/module-name/\n├── controller/\n│   ├── PackageMCPController.ts    ← MCP 控制器\n│   └── PackageHTTPController.ts   ← 同模块可共存 HTTP 控制器\n└── service/\n    └── PackageService.ts\n```\n\n### 插件配置\n\n在 `config/plugin.ts` 中启用：\n\n```typescript\nplugin.mcpProxy = true;\n```\n\n### 路径配置\n\n在 `config/config.default.ts` 中配置 MCP 路径（通常不需要修改，以下为默认值）：\n\n```typescript\nimport { randomUUID } from 'node:crypto';\n\nexport default () => {\n  const config = {\n    mcp: {\n      sseInitPath: '/mcp/sse',\n      sseMessagePath: '/mcp/message',\n      streamPath: '/mcp/stream',\n      statelessStreamPath: '/mcp/stateless/stream',\n      sessionIdGenerator: randomUUID,\n    },\n  };\n  return config;\n};\n```\n\n当使用 `@MCPController({ name: 'myServer' })` 声明命名服务时，路径自动变为：\n\n- `/mcp/myServer/sse`\n- `/mcp/myServer/message`\n- `/mcp/myServer/stream`\n- `/mcp/myServer/stateless/stream`\n\n### AccessLevel\n\n`@MCPController` 装饰器内部已默认设置 AccessLevel（PUBLIC），不需要再手动声明。\n\n---\n\n## 场景决策树\n\n```\n用户需要什么？\n\n├─ \"让 AI 能查数据 / 执行操作\"\n│  └─ → @MCPTool + @Inject Service 处理业务\n│\n├─ \"给 AI 一个提示词模板\"\n│  └─ → @MCPPrompt\n│\n├─ \"让 AI 读取某类资源数据\"\n│  ├─ 资源地址固定 → @MCPResource({ uri: '...' })\n│  └─ 资源地址动态 → @MCPResource({ template: [...] })\n│\n├─ \"Tool 执行中要推送进度\"\n│  └─ → @MCPTool + @Extra() 获取 sendNotification（见下方 @Extra 章节）\n│\n└─ \"Tool 中需要读取自定义请求头\"\n   └─ → @MCPTool + @Extra() 获取 requestInfo.headers\n```\n\n---\n\n## 端到端完整示例\n\n以下展示一个完整的 MCP 功能从配置到测试的所有文件：\n\n### 1. 插件配置 — `config/plugin.ts`\n\n```typescript\nplugin.mcpProxy = true;\n```\n\n### 2. 控制器 — `app/npm/controller/PackageMCPController.ts`\n\n```typescript\nimport {\n  MCPController,\n  MCPTool,\n  MCPToolResponse,\n  MCPPrompt,\n  MCPPromptResponse,\n  MCPResource,\n  MCPResourceResponse,\n  ToolArgs,\n  ToolArgsSchema,\n  PromptArgs,\n  PromptArgsSchema,\n  Inject,\n} from '@eggjs/tegg';\nimport { z } from '@eggjs/tegg/zod';\n\nimport { PackageService } from '../service/PackageService.ts';\n\nconst SearchSchema = {\n  name: z.string({ description: 'npm package name' }),\n};\n\nconst SummarySchema = {\n  name: z.string(),\n};\n\n@MCPController()\nexport class PackageMCPController {\n  @Inject()\n  private readonly packageService: PackageService;\n\n  @MCPTool({ description: 'Search npm package info' })\n  async searchPackage(@ToolArgsSchema(SearchSchema) args: ToolArgs<typeof SearchSchema>): Promise<MCPToolResponse> {\n    const pkg = await this.packageService.findByName(args.name);\n    if (!pkg) {\n      return { content: [{ type: 'text', text: `Package ${args.name} not found` }] };\n    }\n    return { content: [{ type: 'text', text: JSON.stringify(pkg) }] };\n  }\n\n  @MCPPrompt({ description: 'Generate package summary' })\n  async summarize(@PromptArgsSchema(SummarySchema) args: PromptArgs<typeof SummarySchema>): Promise<MCPPromptResponse> {\n    return {\n      messages: [\n        {\n          role: 'user',\n          content: {\n            type: 'text',\n            text: `Summarize the npm package: ${args.name}`,\n          },\n        },\n      ],\n    };\n  }\n\n  @MCPResource({\n    template: ['npm://{name}/{?version}', { list: undefined }],\n  })\n  async getPackageReadme(uri: URL): Promise<MCPResourceResponse> {\n    const name = uri.hostname;\n    const readme = await this.packageService.getReadme(name);\n    return { contents: [{ uri: uri.toString(), text: readme }] };\n  }\n}\n```\n\n### 3. Service — `app/npm/service/PackageService.ts`\n\n```typescript\nimport { SingletonProto } from 'egg';\n\n@SingletonProto()\nexport class PackageService {\n  async findByName(name: string) {\n    // 业务逻辑\n  }\n\n  async getReadme(name: string): Promise<string> {\n    // 业务逻辑\n  }\n}\n```\n\n### 4. 单元测试 — `test/npm/controller/PackageMCPController.test.ts`\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from 'egg-mock/bootstrap';\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\n\ndescribe('PackageMCPController', () => {\n  it('should search package via tool', async () => {\n    app.mockCsrf();\n    const client: Client = await app.mcpClient();\n\n    const tools = await client.listTools();\n    assert(tools.tools.some((t) => t.name === 'searchPackage'));\n\n    const res = await client.callTool({\n      name: 'searchPackage',\n      arguments: { name: 'egg' },\n    });\n    assert(res.content[0].type === 'text');\n  });\n\n  it('should get prompt', async () => {\n    app.mockCsrf();\n    const client: Client = await app.mcpClient();\n\n    const res = await client.getPrompt({\n      name: 'summarize',\n      arguments: { name: 'egg' },\n    });\n    assert(res.messages.length > 0);\n  });\n\n  it('should read resource', async () => {\n    app.mockCsrf();\n    const client: Client = await app.mcpClient();\n\n    const res = await client.readResource({\n      uri: 'npm://egg?version=4.0.0',\n    });\n    assert(res.contents.length > 0);\n  });\n});\n```\n\n---\n\n## @Extra() 的使用场景\n\n`@Extra()` 装饰器注入 `ToolExtra` 对象，提供两个能力：\n\n### 发送通知（长任务进度推送）\n\n```typescript\n@MCPTool()\nasync longTask(\n  @ToolArgsSchema(Schema) args: ToolArgs<typeof Schema>,\n  @Extra() extra: ToolExtra,\n): Promise<MCPToolResponse> {\n  const { sendNotification } = extra;\n  for (let i = 0; i < 10; i++) {\n    await sendNotification({\n      method: 'notifications/message',\n      params: { level: 'info', data: `Step ${i + 1}/10` },\n    });\n    // ... 执行步骤\n  }\n  return { content: [{ type: 'text', text: 'Done' }] };\n}\n```\n\n### 读取自定义请求头\n\n```typescript\n@MCPTool()\nasync myTool(\n  @ToolArgsSchema(Schema) args: ToolArgs<typeof Schema>,\n  @Extra() extra: ToolExtra,\n): Promise<MCPToolResponse> {\n  const headers = extra.requestInfo?.headers;\n  // 处理自定义 header\n}\n```\n\n---\n\n## 装饰器参考\n\n| 装饰器                | 用途         | 常用参数                                   | 返回类型              |\n| --------------------- | ------------ | ------------------------------------------ | --------------------- |\n| `@MCPController()`    | 声明控制器   | `{ name?: string }`                        | -                     |\n| `@MCPTool()`          | 声明工具     | `{ name?: string, description?: string }`  | `MCPToolResponse`     |\n| `@MCPPrompt()`        | 声明提示词   | `{ name?: string, description?: string }`  | `MCPPromptResponse`   |\n| `@MCPResource()`      | 声明资源     | `{ uri: string }` 或 `{ template: [...] }` | `MCPResourceResponse` |\n| `@ToolArgsSchema()`   | Tool 参数    | Zod Schema 普通对象                        | -                     |\n| `@PromptArgsSchema()` | Prompt 参数  | Zod Schema 普通对象                        | -                     |\n| `@Extra()`            | 额外上下文   | -                                          | `ToolExtra`           |\n| `@Inject()`           | 注入 Service | -                                          | -                     |\n\n**注意**：`@MCPController` 的 `version`、`timeout` 等参数通常不需要配置。\n","packages/skills/egg-controller/references/middleware.md":"# Middleware 中间件指南\n\n## 常见错误\n\n| 错误写法                                   | 正确写法                           | 说明                                            |\n| ------------------------------------------ | ---------------------------------- | ----------------------------------------------- |\n| `import { Middleware } from '@eggjs/tegg'` | `import { Middleware } from 'egg'` | Middleware 从 `egg` 导入                        |\n| `import { Advice } from 'egg'`             | `import { Advice } from 'egg/aop'` | AOP 装饰器从 `egg/aop` 导入                     |\n| `@Middleware(funcMw, AdviceClass)`         | 分开写两个 `@Middleware`           | 同一个 `@Middleware()` 中不能混用函数式和 AOP   |\n| AOP Advice 中用实例属性存请求级状态        | 使用 `ctx.set()`/`ctx.get()`       | Advice 默认 Singleton，实例属性会被并发请求共享 |\n| 把中间件文件放在 `app/middleware/`         | 放在模块目录下                     | 函数式中间件放在模块中，通过 import 引用        |\n\n---\n\n## 两种中间件模式\n\nEgg 的 `@Middleware` 装饰器支持两种中间件写法，根据传入参数类型自动识别：\n\n- **AOP 写法（推荐）**：使用 `@Advice` 类，支持 `@Inject` 注入 Proto 依赖，拥有丰富的生命周期钩子\n- **函数式写法（旧版兼容）**：标准 Koa 中间件函数，需要从 `ctx` 对象上手动获取依赖\n\n新项目应优先使用 AOP 写法。函数式写法主要用于兼容旧的 egg 中间件或非常简单的场景。\n\n---\n\n## 实现中间件\n\n### AOP 写法（推荐）\n\n使用 `@Advice()` 装饰器定义类，实现 `IAdvice` 的 `around` 方法，写法与 Koa 中间件一致（`next` 调用目标方法）。Advice 本身是 Proto，支持 `@Inject` 注入依赖。`around` 中可以修改入参（`ctx.args`）和返回值：\n\n```typescript\n// app/modules/foo/advice/LogAdvice.ts\nimport { AccessLevel, Inject, Logger } from 'egg';\nimport { Advice, IAdvice, AdviceContext } from 'egg/aop';\n\n// 跨模块使用时需设置 accessLevel: AccessLevel.PUBLIC\n@Advice({ accessLevel: AccessLevel.PUBLIC })\nexport class LogAdvice implements IAdvice {\n  @Inject()\n  logger: Logger;\n\n  async around(ctx: AdviceContext, next: () => Promise<any>): Promise<any> {\n    // 修改入参：ctx.args 对应控制器方法的参数列表\n    // ctx.args[0] = sanitize(ctx.args[0]);\n\n    const start = Date.now();\n    const result = await next();\n    this.logger.info('%s cost %dms', ctx.method, Date.now() - start);\n\n    // 修改返回值：直接返回新的值即可\n    return { success: true, data: result };\n  }\n}\n```\n\n### 函数式写法（旧版兼容）\n\n标准 Koa 中间件函数，签名为 `(ctx: Context, next: Next) => Promise<void>`。旧版 egg 写法，无法使用 `@Inject`，需要从 `ctx` 对象上手动获取依赖：\n\n```typescript\n// app/modules/foo/middleware/count.ts\nimport type { Context, Next } from 'egg';\n\nexport async function countMw(ctx: Context, next: Next): Promise<void> {\n  const start = Date.now();\n  await next();\n  ctx.set('X-Response-Time', `${Date.now() - start}ms`);\n}\n```\n\n---\n\n## 应用中间件\n\n通过 `@Middleware()` 装饰器将中间件应用到控制器，支持类级别和方法级别：\n\n```typescript\n// app/modules/foo/FooController.ts\nimport { HTTPController, HTTPMethod, HTTPMethodEnum, Middleware } from 'egg';\nimport { LogAdvice } from '../common/advice/LogAdvice.ts';\nimport { countMw } from './middleware/count.ts';\n\n@HTTPController({ path: '/api' })\n@Middleware(LogAdvice) // 类级别：所有方法都会执行\nexport class FooController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/profile' })\n  @Middleware(countMw) // 方法级别：仅此方法执行\n  async getProfile() {\n    return { name: 'test' };\n  }\n}\n```\n\n---\n\n## 执行顺序\n\n遵循洋葱模型，类级别先执行，方法级别后执行：\n\n```typescript\n@Middleware(globalMw)\nexport class FooController {\n  // 进：globalMw → methodMw → hello()\n  // 出：hello() → methodMw → globalMw\n  @Middleware(methodMw)\n  async hello() {}\n\n  // 多个 @Middleware 从下往上执行（靠近方法的先注册）\n  // 进：globalMw → mw3 → mw2 → mw1 → multiple()\n  // 出：multiple() → mw1 → mw2 → mw3 → globalMw\n  @Middleware(mw1)\n  @Middleware(mw2)\n  @Middleware(mw3)\n  async multiple() {}\n}\n```\n\n**若混用函数式和 AOP 中间件，所有函数式中间件（无论类级别还是方法级别）会先于所有 AOP 中间件执行。** 即函数式和 AOP 分属两个独立的执行阶段，函数式阶段在前，AOP 阶段在后：\n\n```typescript\n@Middleware(countMw) // 函数式 - 类级别\n@Middleware(LogAdvice) // AOP - 类级别\nexport class FooController {\n  @Middleware(timeMw) // 函数式 - 方法级别\n  @Middleware(AuthAdvice) // AOP - 方法级别\n  async hello() {}\n}\n\n// 实际执行顺序：\n// countMw → timeMw → LogAdvice → AuthAdvice → hello()\n// （先所有函数式，再所有 AOP；各阶段内类级别先于方法级别）\n```\n","packages/skills/egg-controller/references/schedule.md":"# 定时任务开发指南\n\n## 注意事项\n\n- **不要将代码放在 `app/schedule` 目录下**，egg 默认会扫描该路径注册定时任务，会和装饰器方式冲突\n- 定时任务类必须包含一个 `subscribe` 方法，框架调度时会调用该方法\n- import 路径是 `egg/schedule`，不是 `egg`\n\n## Step 1: 创建定时任务\n\n使用 `@Schedule` 装饰器标识一个类为定时任务，支持 interval 和 cron 两种调度模式。\n\n### interval 模式\n\n按固定间隔执行。`interval` 支持毫秒数或 [ms](https://github.com/vercel/ms) 格式字符串（如 `'5s'`、`'1m'`）。\n\n```typescript\n// app/{moduleName}/schedule/Demo.ts\nimport { Inject, Logger } from 'egg';\nimport { IntervalParams, Schedule, ScheduleType } from 'egg/schedule';\n\n@Schedule<IntervalParams>({\n  type: ScheduleType.WORKER,\n  scheduleData: {\n    interval: '5s',\n  },\n})\nexport class DemoScheduler {\n  @Inject()\n  private logger: Logger;\n\n  async subscribe() {\n    this.logger.info('schedule called');\n  }\n}\n```\n\n### cron 模式\n\n按 cron 表达式执行，格式参考 [cron-parser](https://github.com/harrisiirak/cron-parser)：\n\n```text\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```typescript\n// app/{moduleName}/schedule/CronDemo.ts\nimport { Inject, Logger } from 'egg';\nimport { CronParams, Schedule, ScheduleType } from 'egg/schedule';\n\n@Schedule<CronParams>({\n  type: ScheduleType.WORKER,\n  scheduleData: {\n    cron: '0 0 3 * * *', // 每日 3 点执行\n  },\n})\nexport class CronScheduler {\n  @Inject()\n  private logger: Logger;\n\n  async subscribe() {\n    this.logger.info('schedule called');\n  }\n}\n```\n\n## Step 2: 选择工作模式\n\n| 模式                  | 说明                                     | 使用场景                             |\n| --------------------- | ---------------------------------------- | ------------------------------------ |\n| `ScheduleType.WORKER` | 每台机器只有一个 worker 执行（随机选择） | 大多数场景，如数据同步、缓存刷新     |\n| `ScheduleType.ALL`    | 每台机器的所有 worker 都执行             | 需要每个 worker 都更新本地状态的场景 |\n\n## Step 3: 配置运行参数\n\n`@Schedule` 装饰器支持第二个参数，控制定时任务的运行行为：\n\n```typescript\n@Schedule<IntervalParams>(\n  {\n    type: ScheduleType.WORKER,\n    scheduleData: {\n      interval: '1m',\n    },\n  },\n  {\n    immediate: true, // 应用启动后立即执行一次\n    // disable: true,           // 禁用该定时任务\n    env: ['devserver', 'test'], // 仅在指定环境下启动\n  },\n)\nexport class MyScheduler {\n  async subscribe() {\n    // ...\n  }\n}\n```\n\n| 参数        | 类型     | 说明                            |\n| ----------- | -------- | ------------------------------- |\n| `immediate` | boolean  | 应用启动并 ready 后立即执行一次 |\n| `disable`   | boolean  | 设为 true 时不启动该定时任务    |\n| `env`       | string[] | 仅在指定环境下启动              |\n","packages/skills/egg-core/SKILL.md":"---\nname: egg-core\ndescription: 本技能用于处理 EGG 基础核心概念，包括模块架构、@SingletonProto、@ContextProto、@Inject 装饰器、动态注入、BackgroundTaskHelper 后台任务、EventBus 事件总线和 AOP 切面编程。用于理解 EGG 的基础构建块、依赖注入、对象生命周期管理、运行时多实现动态选择、请求返回后的异步任务处理、事件驱动架构和横切关注点。\nallowed-tools: Read\n---\n\n# egg 核心概念\n\n## 代码组织与依赖注入\n\n### Step 1: 代码写在 module 中\n\n#### 什么是模块？\n\n模块是 EGG 中基础的代码组织单元。只有模块内的代码会被框架扫描和加载。模块之间相互独立，但可以通过 `@Inject` 装饰器访问其他模块的对象。\n\n#### 定义 module\n\n在目录中添加包含 `eggModule.name` 字段的 `package.json` 文件来声明该目录为模块：\n\n```json\n{\n  \"name\": \"foo\",\n  \"eggModule\": {\n    \"name\": \"foo\"\n  }\n}\n```\n\n**重要提示**：模块名称不能包含 `-` 或其他特殊字符；使用驼峰命名规则。\n\n#### 正确示例\n\n```\napp/\n└── userModule/           ✅ 驼峰命名\n    ├── package.json\n    │   └── { \"eggModule\": { \"name\": \"userModule\" } }\n    └── service.ts        ✅ 会被框架加载\n```\n\n#### 错误示例\n\n```\napp/\n├── user-module/          ❌ 名称包含 `-`\n│   └── package.json\n│       └── { \"eggModule\": { \"name\": \"user-module\" } }\n│\n└── common/               ❌ 缺少 package.json（不是 module）\n    └── utils.ts          ❌ 不会被框架加载\n```\n\n#### 模块配置\n\n在模块根目录创建 `module.yml` 用于模块特定配置：\n\n```yaml\nfoo: bar\n```\n\n通过 `@Inject()` 注入配置，使用 `moduleConfig`：\n\n```typescript\nimport { SingletonProto, Inject } from 'egg';\n\ninterface ModuleConfig {\n  foo: string;\n}\n\n@SingletonProto()\nexport class ConfigService {\n  @Inject()\n  private readonly moduleConfig: ModuleConfig;\n\n  async hello(): Promise<string> {\n    return `hello ${this.moduleConfig.foo}`;\n  }\n}\n```\n\n#### 模块组织最佳实践\n\n- 新应用：按功能在 `app/` 目录中组织\n- 存量应用：保留老的 egg 代码在 `app/controller`/`app/service`，将新增的 module 代码放在 `app/module/`\n- 可以在 `dependencies` 中导入 npm 包作为额外模块\n\n### 导入路径\n\n所有装饰器和类型统一从 `egg` 导入，不要从 `@eggjs/tegg` 导入：\n\n```typescript\n// ✅ 正确\nimport { SingletonProto, ContextProto, Inject, AccessLevel } from 'egg';\n\n// ❌ 错误 — 不要从 @eggjs/tegg 导入\nimport { SingletonProto } from '@eggjs/tegg';\n```\n\n---\n\n### Step 2: 用 Proto 实现 Service\n\n#### SingletonProto\n\n应用启动时立即创建，整个应用生命周期内只有一个实例，性能更好，应该作为默认选择。\n\n```typescript\nimport { SingletonProto } from 'egg';\n\n@SingletonProto()\nexport class HelloService {\n  async hello(): Promise<string> {\n    return 'hello';\n  }\n}\n```\n\n#### ContextProto\n\n请求到达时按需创建，每个请求一个实例，请求结束自动销毁。仅在需要隔离不同请求的上下文信息时使用。\n\n```typescript\nimport { ContextProto } from 'egg';\n\n@ContextProto()\nexport class RequestContext {\n  userId: string;\n}\n```\n\n**重要提示**：大多数服务应该使用 `SingletonProto` 以获得更好的性能。只有当请求上下文必须在服务之间共享以确保请求之间隔离时，才使用 `ContextProto`。\n\n#### AccessLevel\n\nproto 对象默认 accessLevel 为 `AccessLevel.PRIVATE`，仅在当前 module 内使用。可以设置为 `AccessLevel.PUBLIC`，进行跨模块访问。\n\n```typescript\nimport { AccessLevel, ContextProto, SingletonProto } from 'egg';\n\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class SharedService {}\n\n@ContextProto({ accessLevel: AccessLevel.PUBLIC })\nexport class SharedContextService {}\n```\n\n### Step 3: 通过 Inject 使用 Service\n\n#### 基本用法\n\n使用 `@Inject()` 注入其他 Proto 或 Egg 对象：\n\n```typescript\nimport { Inject, Logger, SingletonProto } from 'egg';\nimport { FooService } from './FooService.ts';\n\n@SingletonProto()\nexport class HelloService {\n  @Inject()\n  fooService: FooService; // 注入另一个 Proto\n\n  @Inject()\n  logger: Logger; // 注入 Egg 对象\n\n  async hello(): Promise<string> {\n    this.logger.info(`[HelloService] ${this.fooService.hello()}`);\n  }\n}\n```\n\n#### 动态注入\n\n当同一个抽象有多种实现，需要在运行时动态选择时，通过 `EggObjectFactory` 按类型获取实现，无需 if/else。详见 `references/dynamic-inject.md`。\n\n#### 重要约束\n\n- **不能有循环依赖**：Proto 或模块之间都不能有循环依赖\n- **不能有同名对象**：一个模块不能有相同名称和初始化类型的 Proto\n- **按需注入**：不要直接注入 `app` 或 `ctx`，按需注入特定对象\n\n### 快速决策指南\n\n| 场景                             | 使用方式                                               |\n| -------------------------------- | ------------------------------------------------------ |\n| 无状态服务                       | `@SingletonProto()`                                    |\n| 跨服务共享的请求级状态           | `@ContextProto()`                                      |\n| 需要跨模块访问                   | `@SingletonProto({ accessLevel: AccessLevel.PUBLIC })` |\n| 注入依赖                         | `@Inject()`                                            |\n| 使用自定义名称注入               | `@Inject({ name: 'customName' })`                      |\n| 同一抽象多种实现，运行时动态选择 | `QualifierImplDecoratorUtil` + `EggObjectFactory`      |\n\n## 异步任务\n\n| 特性       | BackgroundTaskHelper          | EventBus                                  |\n| ---------- | ----------------------------- | ----------------------------------------- |\n| **耦合**   | 高 — 异步逻辑写在触发者代码中 | 低 — handler 独立，新增处理者不修改触发者 |\n| **上下文** | 共享触发者的请求上下文        | handler 运行在独立的新上下文中            |\n| **扩展**   | 需要修改触发者代码            | 新增 `@Event` handler 类即可              |\n\n```\n需要在请求之外执行任务？\n│\n├─ 请求返回后执行，依赖当前请求上下文\n│  └─ → BackgroundTaskHelper（references/background-task.md）\n│\n├─ 请求返回后执行，不依赖当前请求上下文，需要解耦\n│  └─ → EventBus（references/eventbus.md）\n│\n└─ 定时或周期执行\n   └─ → Schedule（参考 egg-controller skill）\n```\n\n## AOP 切面编程\n\nAOP 用于将日志、鉴权、缓存、事务等横切关注点从业务代码中分离。AOP 装饰器从 `egg/aop` 导入（不是 `egg`）。\n\n```\n需要在方法执行前后添加通用逻辑？\n│\n├─ 针对特定方法 → @Pointcut（在目标方法上声明）\n│\n└─ 批量切入多个类/方法 → @Crosscut（在 Advice 类上声明匹配规则）\n```\n\n详细用法（Advice 生命周期、AdviceContext、Pointcut/Crosscut 选型、参数透传、执行顺序）请参阅 `references/aop.md`。\n\n## 常见问题排查\n\n| 现象                   | 原因                                       | 解决方案                                     |\n| ---------------------- | ------------------------------------------ | -------------------------------------------- |\n| 模块没被加载           | `eggModule.name` 包含 `-` 等特殊字符       | 改为驼峰命名                                 |\n| `EggPrototypeNotFound` | 跨模块注入但 accessLevel 为 PRIVATE        | 改为 `AccessLevel.PUBLIC`                    |\n| 注入对象不对           | 类型为 `interface`/`any`，回退到属性名匹配 | 改用 class 类型或 `@Inject({ name: 'xxx' })` |\n| 混用注入方式报错       | 属性注入和构造函数注入不能混用             | 统一使用一种方式                             |\n| 可选依赖启动报错       | 缺少 optional 标记                         | `@Inject({ optional: true })`                |\n\n## 参考资料\n\n- 详细的 module 文档，请参阅：`references/module.md`\n- Inject 装饰器使用，请参阅：`references/inject.md`\n- SingletonProto 和 ContextProto 详情，请参阅：`references/proto.md`\n- 动态注入（Qualifier 动态注入），请参阅：`references/dynamic-inject.md`\n- 请求后异步任务（BackgroundTaskHelper），请参阅：`references/background-task.md`\n- 事件总线（EventBus），请参阅：`references/eventbus.md`\n- AOP 切面编程（Advice、Pointcut、Crosscut），请参阅：`references/aop.md`\n\n单元测试（`egg-unittest` skill）：Service 测试、BackgroundTask 测试、EventBus 测试\n","packages/skills/egg-core/references/aop.md":"# AOP 切面编程指南\n\n## 常见错误\n\n| 错误写法                                   | 正确写法                              | 说明                                                                                   |\n| ------------------------------------------ | ------------------------------------- | -------------------------------------------------------------------------------------- |\n| `import { Advice } from 'egg'`             | `import { Advice } from 'egg/aop'`    | AOP 装饰器从 `egg/aop` 导入，不是 `egg`                                                |\n| `import { Advice } from '@eggjs/tegg/aop'` | `import { Advice } from 'egg/aop'`    | 统一从 `egg/aop` 导入                                                                  |\n| Advice 类没有加 `@Advice()` 装饰器         | 必须同时有 `@Advice()`                | `@Pointcut` 和 `@Crosscut` 都要求目标是 Advice 类                                      |\n| `@Crosscut` 直接切 Egg 内置对象            | 只能切 tegg Proto 对象                | Egg 中的对象（如 app、ctx）无法被 Crosscut                                             |\n| Advice 中用实例属性存状态                  | 使用 `ctx.set()`/`ctx.get()` 共享状态 | Advice 默认是 Singleton，实例属性会被并发请求共享，必须用 AdviceContext 传递调用级状态 |\n\n---\n\n## 核心概念\n\n### Advice（切面逻辑）\n\nAdvice 是 AOP 的核心，定义了在目标方法执行前后要做什么。Advice 本身也是一种 Proto，默认 initType 为 Singleton（全局单例），可以使用 `@Inject` 注入依赖。如需每请求一个实例，显式指定 `@Advice({ initType: ObjectInitType.CONTEXT })`。\n\n```typescript\nimport { Advice, IAdvice, AdviceContext } from 'egg/aop';\nimport { Inject, Logger } from 'egg';\n\n@Advice()\nexport class LogAdvice implements IAdvice {\n  @Inject()\n  private logger: Logger;\n\n  async beforeCall(ctx: AdviceContext): Promise<void> {\n    ctx.set('startTime', Date.now());\n  }\n\n  async afterReturn(ctx: AdviceContext, result: any): Promise<void> {\n    // 方法成功返回后执行\n  }\n\n  async afterThrow(ctx: AdviceContext, error: Error): Promise<void> {\n    // 方法抛出异常后执行\n  }\n\n  async afterFinally(ctx: AdviceContext): Promise<void> {\n    const duration = Date.now() - ctx.get('startTime');\n    this.logger.info('%s cost %dms', String(ctx.method), duration);\n  }\n\n  async around(ctx: AdviceContext, next: () => Promise<any>): Promise<any> {\n    // 类似 koa 中间件，可以包裹方法执行\n    return await next();\n  }\n}\n```\n\n**执行顺序：**\n\n```typescript\nawait beforeCall(ctx);\ntry {\n  const result = await around(ctx, next); // next 执行目标方法\n  await afterReturn(ctx, result);\n  return result;\n} catch (e) {\n  await afterThrow(ctx, e);\n  throw e;\n} finally {\n  await afterFinally(ctx);\n}\n```\n\n**关键点：**\n\n- 所有 hook 方法都是可选的，按需实现\n- 只有 `around` 可以修改返回值\n- `beforeCall` 中可以通过修改 `ctx.args` 改变方法入参\n- 多个 Advice 之间通过 `ctx.set(key, value)` / `ctx.get(key)` 共享状态\n\n### AdviceContext\n\n```typescript\ninterface AdviceContext<T = object, K = any> {\n  that: T; // 被切的对象实例\n  method: PropertyKey; // 被切的方法名\n  args: any[]; // 方法参数（可修改）\n  adviceParams?: K; // 装饰器透传的参数\n  get(key: PropertyKey): any; // 获取共享状态\n  set(key: PropertyKey, value: any): this; // 设置共享状态\n}\n```\n\n---\n\n## Pointcut vs Crosscut\n\n### Pointcut — 精确切入\n\n在特定类的特定方法上声明 Advice，适合精确控制：\n\n```typescript\nimport { SingletonProto } from 'egg';\nimport { Pointcut } from 'egg/aop';\n\n@SingletonProto()\nexport class OrderService {\n  @Pointcut(LogAdvice)\n  async createOrder(data: any) {\n    // 业务逻辑\n  }\n\n  // 可以传参给 Advice\n  @Pointcut(TransactionAdvice, { adviceParams: { propagation: 'REQUIRED' } })\n  async updateOrder(id: string, data: any) {\n    // 业务逻辑\n  }\n}\n```\n\n### Crosscut — 批量切入\n\n在 Advice 类上声明切入规则，适合横切多个类/方法：\n\n```typescript\nimport { Crosscut, Advice, IAdvice, PointcutType } from 'egg/aop';\n\n// 模式 1：指定类和方法\n@Crosscut({\n  type: PointcutType.CLASS,\n  clazz: OrderService,\n  methodName: 'createOrder',\n})\n@Advice()\nexport class AuditAdvice implements IAdvice {\n  async afterReturn(ctx: AdviceContext): Promise<void> {\n    // 审计日志\n  }\n}\n\n// 模式 2：正则匹配\n@Crosscut({\n  type: PointcutType.NAME,\n  className: /.*Service$/i,\n  methodName: /^(create|update|delete)/,\n})\n@Advice()\nexport class OperationLogAdvice implements IAdvice {\n  async around(ctx: AdviceContext, next: () => Promise<any>): Promise<any> {\n    // 记录所有 Service 的写操作\n    return await next();\n  }\n}\n\n// 模式 3：自定义回调\n@Crosscut({\n  type: PointcutType.CUSTOM,\n  callback: (clazz, method) => {\n    return clazz.name.endsWith('Repository') && method !== 'constructor';\n  },\n})\n@Advice()\nexport class DbMetricsAdvice implements IAdvice {}\n```\n\n### 执行顺序\n\n- `@Crosscut` 默认 order: `100`\n- `@Pointcut` 默认 order: `1000`\n- order 越小越先执行\n- 同一方法上多个 Advice 按 order 升序排列\n\n通过 `order` 参数自定义顺序：\n\n```typescript\n// Pointcut：第二个参数中设置 order\n@Pointcut(LogAdvice, { order: 50 })\nasync createOrder(data: any) {}\n\n// Crosscut：第二个参数中设置 order\n@Crosscut(\n  { type: PointcutType.NAME, className: /.*Service$/i, methodName: /.+/ },\n  { order: 200 },\n)\n@Advice()\nexport class MetricsAdvice implements IAdvice {}\n```\n\n---\n\n## 参数透传\n\n同一个 Advice 在不同方法上可能需要不同的行为，通过 `adviceParams` 传参：\n\n```typescript\n@Advice()\nexport class CacheAdvice implements IAdvice {\n  async around(ctx: AdviceContext<any, { ttl: number }>, next: () => Promise<any>): Promise<any> {\n    const ttl = ctx.adviceParams?.ttl ?? 60;\n    // 根据 ttl 实现缓存逻辑\n    return await next();\n  }\n}\n\n@SingletonProto()\nexport class UserService {\n  @Pointcut(CacheAdvice, { adviceParams: { ttl: 3600 } })\n  async getUser(id: string) {\n    /* ... */\n  }\n\n  @Pointcut(CacheAdvice, { adviceParams: { ttl: 60 } })\n  async getUserList() {\n    /* ... */\n  }\n}\n```\n\n---\n\n## 典型场景\n\n| 场景                        | 推荐方式                 | 说明              |\n| --------------------------- | ------------------------ | ----------------- |\n| 特定方法加日志/缓存         | `@Pointcut(Advice)`      | 精确控制          |\n| 所有 Service 的写操作加审计 | `@Crosscut(NAME)` + 正则 | 批量匹配          |\n| 事务包裹                    | `@Pointcut(TxAdvice)`    | around 中管理事务 |\n","packages/skills/egg-core/references/background-task.md":"# BackgroundTask 异步任务指南\n\n## 常见错误\n\n| 错误写法                                                    | 正确写法                                        | 说明                                                      |\n| ----------------------------------------------------------- | ----------------------------------------------- | --------------------------------------------------------- |\n| `setTimeout(() => { ... }, 0)`                              | `backgroundTaskHelper.run(async () => { ... })` | setTimeout 执行时上下文已释放，会导致访问已销毁对象的错误 |\n| `setImmediate(() => { ... })`                               | `backgroundTaskHelper.run(async () => { ... })` | 同上，框架不会等待 setImmediate 的回调                    |\n| `process.nextTick(() => { ... })`                           | `backgroundTaskHelper.run(async () => { ... })` | 同上                                                      |\n| 在 Service 中 `await` 后台任务                              | 直接调用 `run()` 不 await                       | `run()` 是\"发射后不管\"，不返回 Promise                    |\n| `import { BackgroundTaskHelper } from '@eggjs/tegg/helper'` | `import { BackgroundTaskHelper } from 'egg'`    | 统一从 `egg` 导入，不要从 `@eggjs/tegg` 子路径导入        |\n\n---\n\n## 使用方式\n\n### 基本用法\n\n通过 `@Inject()` 注入 `BackgroundTaskHelper`，调用 `run()` 方法。`run()` 接受一个异步函数，任务会立即开始执行但不阻塞当前流程。框架在请求结束（Context preDestroy）时会等待所有后台任务完成（最多等待 timeout），然后再释放上下文。\n\n```typescript\nimport { BackgroundTaskHelper, Inject, SingletonProto, AccessLevel } from 'egg';\n\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class MetricsService {\n  @Inject()\n  private backgroundTaskHelper: BackgroundTaskHelper;\n\n  async reportAfterResponse(data: Record<string, unknown>) {\n    // run() 是非阻塞的，任务立即开始执行但不会被 await\n    this.backgroundTaskHelper.run(async () => {\n      // 框架会保持上下文存活直到任务完成或超时\n      await this.sendMetrics(data);\n    });\n  }\n\n  private async sendMetrics(data: Record<string, unknown>) {\n    // 实际上报逻辑\n  }\n}\n```\n\n---\n\n## 超时配置\n\n框架默认等待 5 秒，超时后放弃等待并释放上下文。\n\n### 按请求调整超时\n\n```typescript\nthis.backgroundTaskHelper.timeout = 10000; // 10 秒\nthis.backgroundTaskHelper.run(async () => {\n  await heavyWork();\n});\n```\n\n注意：timeout 是 context 级别的设置，同一请求中多次设置以最后一次为准。\n\n### 全局配置\n\n```typescript\n// config/config.default.ts\nexport default {\n  backgroundTask: {\n    timeout: 10000, // 全局默认 10 秒\n  },\n};\n```\n\n### 无限等待\n\n```typescript\n// 不推荐在生产环境使用，除非确定任务一定会完成\nthis.backgroundTaskHelper.timeout = Infinity;\n```\n\n---\n\n## 错误处理\n\nBackgroundTaskHelper 内部会捕获所有错误并记录日志，**不会**抛出异常或影响其他后台任务。\n\n```typescript\nthis.backgroundTaskHelper.run(async () => {\n  // 即使这里抛出异常，也不会影响其他后台任务或框架\n  throw new Error('something went wrong');\n  // 错误会被记录为: [BackgroundTaskHelper] background throw error:something went wrong\n});\n\n// 如果需要自定义错误处理\nthis.backgroundTaskHelper.run(async () => {\n  try {\n    await unreliableOperation();\n  } catch (e) {\n    // 自定义错误处理：重试、告警等\n    await this.alertService.notify(e);\n  }\n});\n```\n\n---\n\n## 原理\n\n请求处理完成后，框架会立即释放上下文（防止内存泄漏）。如果用 `setTimeout` 等方式在上下文释放后访问注入的服务，会因为上下文已销毁而出错。\n\nBackgroundTaskHelper 的作用是：\n\n1. `run()` 被调用时，任务立即开始执行（不是延后到响应发送后）\n2. 任务不会被 await，因此不阻塞当前请求的返回\n3. 请求结束时（Context preDestroy），框架等待所有后台任务完成（最多等待 timeout）\n4. 等待结束后才释放上下文\n\n这就是为什么必须用 `backgroundTaskHelper.run()` 而不是 `setTimeout` — 后者绕过了框架的上下文生命周期管理，执行时上下文可能已被释放。\n\n---\n\n## 单元测试\n\nBackgroundTaskHelper 的测试方法参考 `egg-unittest` skill 的 `references/background-task-test.md`。\n","packages/skills/egg-core/references/dynamic-inject.md":"# 动态注入开发指南\n\n## 何时使用\n\n- **用动态注入**：同一抽象有多种实现，运行时按参数选择（如多种支付方式、多种存储后端）\n- **用普通 `@Inject()`**：依赖只有一个实现，编译时就能确定\n\n## Step 1: 定义抽象类和类型枚举\n\n```typescript\n// AbstractHello.ts\nexport abstract class AbstractHello {\n  abstract hello(): string;\n}\n\n// HelloType.ts\nexport enum HelloType {\n  FOO = 'FOO',\n  BAR = 'BAR',\n}\n```\n\n如果类型是无限扩展的，没有固定枚举，可以用 `Record<string, string>` 代替：\n\n```typescript\ntype AnyEnum = Record<string, string>;\n```\n\n## Step 2: 创建自定义装饰器\n\n```typescript\n// decorator/Hello.ts\nimport { ImplDecorator, QualifierImplDecoratorUtil } from 'egg';\nimport { HelloType } from '../HelloType.ts';\nimport { AbstractHello } from '../AbstractHello.ts';\n\nexport const HELLO_ATTRIBUTE = Symbol('HELLO_ATTRIBUTE');\n\nexport const Hello: ImplDecorator<AbstractHello, typeof HelloType> = QualifierImplDecoratorUtil.generatorDecorator(\n  AbstractHello,\n  HELLO_ATTRIBUTE,\n);\n```\n\n**注意事项：**\n\n- **ATTRIBUTE（Symbol）不要重复**，重复会导致实现被覆盖\n- **抽象类不要指定错**，否则可能导致实现被覆盖\n\n## Step 3: 实现抽象类\n\n每个实现加上 `@SingletonProto()`（或 `@ContextProto()`）和自定义装饰器：\n\n```typescript\n// impl/FooHello.ts\nimport { SingletonProto } from 'egg';\nimport { Hello } from '../decorator/Hello.ts';\nimport { HelloType } from '../HelloType.ts';\nimport { AbstractHello } from '../AbstractHello.ts';\n\n@SingletonProto()\n@Hello(HelloType.FOO)\nexport class FooHello extends AbstractHello {\n  hello(): string {\n    return 'hello, foo';\n  }\n}\n```\n\n## Step 4: 动态获取实现\n\n通过 `EggObjectFactory` 在运行时按类型获取对应实现：\n\n```typescript\n// HelloService.ts\nimport { EggObjectFactory, SingletonProto, Inject } from 'egg';\nimport { HelloType } from './HelloType.ts';\nimport { AbstractHello } from './AbstractHello.ts';\n\n@SingletonProto()\nexport class HelloService {\n  @Inject()\n  private eggObjectFactory: EggObjectFactory;\n\n  async hello(type: HelloType): Promise<string> {\n    const helloImpl = await this.eggObjectFactory.getEggObject(AbstractHello, type);\n    return helloImpl.hello();\n  }\n}\n```\n","packages/skills/egg-core/references/eventbus.md":"# EventBus 事件总线指南\n\n## 常见错误\n\n| 错误写法                                         | 正确写法                                    | 说明                                             |\n| ------------------------------------------------ | ------------------------------------------- | ------------------------------------------------ |\n| `import { EventBus } from '@eggjs/tegg'`         | `import { EventBus } from 'egg'`            | 统一从 `egg` 导入                                |\n| `import { Event } from '@eggjs/tegg'`            | `import { Event } from 'egg'`               | 装饰器也从 `egg` 导入                            |\n| 在 handler 的 `handle` 方法中直接访问请求上下文  | 使用 `@EventContext()` 注入 `IEventContext` | handler 运行在独立的上下文中，不是触发者的上下文 |\n| `@Event('hello')` 但没有声明 Events 接口         | 先在 `declare module 'egg'` 中声明事件类型  | 不声明会导致类型检查报错                         |\n| handler 的 `handle` 方法签名与 Events 声明不一致 | 确保参数类型与 Events 中定义一致            | TypeScript 会检查签名是否匹配                    |\n\n---\n\n## 使用步骤\n\n### 1. 声明事件类型\n\n在模块中创建类型声明文件，定义事件名称和参数签名：\n\n```typescript\n// app/myModule/event.ts\nimport 'egg';\n\ndeclare module 'egg' {\n  interface Events {\n    orderCreated: (orderId: string, userId: string) => void;\n    paymentCompleted: (orderId: string, amount: number) => void;\n  }\n}\n```\n\n**注意：** 文件开头必须有 `import 'egg'`，否则 `declare module` 会覆盖而非合并模块声明。\n\n### 2. 触发事件\n\n通过 `@Inject()` 注入 `EventBus` 或 `ContextEventBus`，调用 `emit()` 触发事件：\n\n```typescript\nimport { SingletonProto, Inject, EventBus, AccessLevel } from 'egg';\n\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class OrderService {\n  @Inject()\n  private eventBus: EventBus;\n\n  async createOrder(userId: string, items: Item[]): Promise<string> {\n    const orderId = await this.saveOrder(userId, items);\n    // 触发事件，不阻塞当前流程\n    this.eventBus.emit('orderCreated', orderId, userId);\n    return orderId;\n  }\n}\n```\n\n`emit()` 的参数类型由 Events 接口约束，TypeScript 会自动提示和校验。\n\n### 3. 消费事件\n\n用 `@Event()` 装饰器标记 handler 类。handler 必须实现 `handle` 方法，参数签名与 Events 声明一致：\n\n```typescript\nimport { Event, Inject } from 'egg';\nimport type { EggLogger } from 'egg';\n\n@Event('orderCreated')\nexport class OrderNotificationHandler {\n  @Inject()\n  private logger: EggLogger;\n\n  async handle(orderId: string, userId: string): Promise<void> {\n    this.logger.info('[OrderNotification] order %s created by user %s', orderId, userId);\n    // 发送通知等业务逻辑\n  }\n}\n```\n\n**handler 的关键特性：**\n\n- handler 运行在独立的上下文中，不是触发者的上下文\n- 同一事件可以有多个 handler，它们并行执行\n- handler 文件放在模块目录中，框架自动扫描和注册\n\n### 4. 消费多个事件\n\n一个 handler 可以处理多个事件，通过 `@EventContext()` 获取事件上下文来区分：\n\n```typescript\nimport { Event, EventContext, type IEventContext } from 'egg';\n\n@Event('orderCreated')\n@Event('paymentCompleted')\nexport class AuditLogHandler {\n  async handle(@EventContext() ctx: IEventContext, ...args: unknown[]): Promise<void> {\n    console.log('event:', ctx.eventName, 'args:', args);\n  }\n}\n```\n\n`@EventContext()` 只能修饰 `handle` 方法的第一个参数。不需要区分事件时可以省略。\n\n---\n\n## Cork/Uncork 事件缓冲\n\n当需要在一个操作中触发多个事件，但希望它们在操作全部完成后才被处理时，使用 cork/uncork：\n\n```typescript\nimport { ContextProto, Inject, type ContextEventBus } from 'egg';\n\n@ContextProto()\nexport class BatchService {\n  @Inject()\n  private eventBus: ContextEventBus; // 注意：cork/uncork 需要 ContextEventBus\n\n  async processBatch(items: string[]): Promise<void> {\n    this.eventBus.cork(); // 开始缓冲，事件不会立即派发\n\n    for (const item of items) {\n      this.eventBus.emit('orderCreated', item, 'batch-user');\n    }\n\n    this.eventBus.uncork(); // 释放缓冲，所有事件一次性派发\n  }\n}\n```\n\n**Cork/Uncork 行为：**\n\n- 支持嵌套调用 — 内层 uncork 不会释放事件，只有最外层 uncork 才会\n- `ContextEventBus` 的 cork/uncork 自动管理 corkId，无需手动指定\n- cork 期间 emit 的事件会被暂存，uncork 后按顺序派发\n\n---\n\n## 单元测试\n\nEventBus 的测试方法参考 `egg-unittest` skill 的 `references/eventbus-test.md`。\n","packages/skills/egg-core/references/inject.md":"# Inject 开发指南\n\n## 基本用法\n\n使用 `@Inject()` 注入其他 Proto 对象或 Egg 内置对象：\n\n```typescript\nimport { SingletonProto, Inject } from 'egg';\n\n@SingletonProto()\nexport class OrderService {\n  @Inject()\n  userService: UserService; // 按类型自动匹配\n\n  @Inject({ name: 'customName' })\n  config: any; // 按名称匹配\n\n  @Inject('shorthand')\n  other: any; // 字符串简写，等同于 { name: 'shorthand' }\n}\n```\n\n## 名称解析规则\n\n框架按以下优先级确定注入目标：\n\n1. **显式指定 name** — `@Inject({ name: 'xxx' })` 或 `@Inject('xxx')`\n2. **类型元数据自动关联** — 当属性类型是使用了 `@SingletonProto()` 或 `@ContextProto()` 装饰器的 class 时，框架自动从类型元数据匹配对应的 Proto 对象。此时属性名可以任意取值，不影响注入结果\n3. **属性名兜底** — 当类型为 `interface`、`any`、`unknown` 等无法获取运行时元信息的类型时，使用属性名作为注入名称\n\n```typescript\n@SingletonProto()\nexport class MyService {\n  @Inject()\n  fooService: FooService; // 优先级 2：从 FooService 类型自动关联\n\n  @Inject()\n  whatever: FooService; // 优先级 2：同样生效，属性名不影响匹配\n\n  @Inject({ name: 'bar' })\n  baz: FooService; // 优先级 1：显式 name → \"bar\"\n\n  @Inject()\n  something: any; // 优先级 3：类型无元数据，回退到属性名 → \"something\"\n}\n```\n\n## 可选注入\n\n默认情况下，注入目标不存在会在启动时报错。使用 `optional` 可以跳过不存在的依赖：\n\n```typescript\nimport { SingletonProto, Inject, InjectOptional } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  @Inject({ optional: true })\n  maybeService?: SomeService; // 不存在时为 undefined\n\n  @InjectOptional()\n  anotherOptional?: OtherService; // 简写方式，效果相同\n}\n```\n\n## 构造函数注入\n\n除了属性注入，也支持构造函数参数注入：\n\n```typescript\nimport { SingletonProto, Inject } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  constructor(\n    @Inject() readonly fooService: FooService,\n    @Inject({ optional: true }) readonly barService?: BarService,\n  ) {}\n}\n```\n\n**注意：构造函数注入和属性注入不能混用。** 一个 class 只能选择其中一种方式，否则框架会报错。\n\n## 注入 Egg 内置对象\n\n框架会自动遍历 `Application` 和 `Context` 对象的所有属性，均可通过 `@Inject()` 注入。\n\n#### 注入配置\n\n```typescript\nimport { Inject, SingletonProto, EggAppConfig } from 'egg';\n\n@SingletonProto()\nclass Foo {\n  @Inject()\n  config: EggAppConfig;\n\n  bar(): void {\n    console.log('current env is %s', this.config.env);\n  }\n}\n```\n\n#### 注入 Logger\n\n专为 logger 做了优化，可以直接注入 custom logger：\n\n```typescript\nimport { Inject, SingletonProto, Logger } from 'egg';\n\n@SingletonProto()\nclass FooService {\n  @Inject()\n  logger: Logger; // 注入 ${appname}-web.log\n\n  @Inject()\n  coreLogger: Logger; // 注入 egg-web.log\n\n  @Inject()\n  fooLogger: Logger; // 注入 customLogger 中配置的 fooLogger\n}\n```\n\n#### 注入 Service\n\n> 强烈建议把 Egg Service 的代码通过 Proto 重新封装再注入。对于已有的 Service，可以通过以下方式引入：\n\n```typescript\nimport { Service, Inject, SingletonProto } from 'egg';\n\n@SingletonProto()\nclass FooService {\n  @Inject()\n  service: Service; // 注入整个 ctx.service\n\n  get xxxService() {\n    return this.service.xxxService;\n  }\n}\n```\n\n#### 注入 HttpClient\n\n```typescript\nimport { Inject, SingletonProto, HttpClient } from 'egg';\n\n@SingletonProto()\nclass Foo {\n  @Inject()\n  httpClient: HttpClient;\n\n  async bar(): Promise<void> {\n    await this.httpClient.request('https://example.com');\n  }\n}\n```\n\n## 注入模块配置\n\n在模块根目录创建 `module.yml`，通过 `moduleConfig` 名称注入，框架自动注入当前模块的配置：\n\n```yaml\n# module.yml\napiEndpoint: https://api.example.com\nretryCount: 3\n```\n\n```typescript\nimport { SingletonProto, Inject } from 'egg';\n\ninterface ModuleConfig {\n  apiEndpoint: string;\n  retryCount: number;\n}\n\n@SingletonProto()\nexport class ApiService {\n  @Inject()\n  moduleConfig: ModuleConfig;\n\n  async call(): Promise<void> {\n    // this.moduleConfig.apiEndpoint → \"https://api.example.com\"\n  }\n}\n```\n\n如需注入其他模块的配置，使用 `@ConfigSourceQualifier` 指定模块名：\n\n```typescript\nimport { SingletonProto, Inject, ConfigSourceQualifier } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  @Inject()\n  @ConfigSourceQualifier('otherModule')\n  moduleConfig: OtherModuleConfig; // 注入 otherModule 的配置\n}\n```\n\n## Qualifier 限定符\n\n当同名对象存在多个实现时，使用限定符消歧义：\n\n### @InitTypeQualifier\n\n指定注入 Singleton 还是 Context 实例：\n\n```typescript\nimport { SingletonProto, Inject, InitTypeQualifier, ObjectInitType } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  @Inject()\n  @InitTypeQualifier(ObjectInitType.CONTEXT)\n  barService: BarService; // 强制注入 ContextProto 版本\n}\n```\n\n> 大多数情况下不需要手动指定，框架会根据类型元数据自动推导。\n\n### @EggQualifier\n\n当 `app` 和 `ctx` 上存在同名属性时，框架默认优先匹配 `ctx`。使用 `@EggQualifier` 显式指定来源：\n\n```typescript\nimport { SingletonProto, Inject, EggQualifier, EggType } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  @Inject()\n  @EggQualifier(EggType.APP)\n  someProp: any; // 强制从 app 注入\n\n  @Inject()\n  @EggQualifier(EggType.CONTEXT)\n  someProp2: any; // 强制从 ctx 注入\n}\n```\n\n### @ModuleQualifier\n\n指定从哪个模块注入：\n\n```typescript\nimport { SingletonProto, Inject, ModuleQualifier } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  @Inject()\n  @ModuleQualifier('userModule')\n  userService: UserService; // 明确从 userModule 注入\n}\n```\n\n## 重要约束\n\n- **不能有循环依赖**：Proto 之间或模块之间都不能形成循环引用\n- **不能有同名对象**：同一模块内不能存在相同名称和相同初始化类型的 Proto\n- **按需注入**：不要直接注入 `app` 或 `ctx`，按需注入具体的属性（如 `logger`、`config`）\n","packages/skills/egg-core/references/module.md":"# module 开发指南\n\n## 创建 module\n\n在 `app` 目录中，创建 module 目录，并在该目录中添加 `package.json`。\n\n```json\n{\n  \"name\": \"moduleName\",\n  \"eggModule\": {\n    \"name\": \"moduleName\"\n  }\n}\n```\n\n**重要提示**：\n\n- 模块名称不能包含 `-` 或其他特殊字符；使用驼峰命名规则。\n- module 的 `package.json` 文件中，仅包含 `name` 以及 `eggModule.name` 字段，不应该有其他额外内容。\n\n## module 发现机制\n\n框架支持两种模式：自动扫描（默认）和手动声明。两者互斥，当 `config/module.json` 存在时，自动扫描完全禁用。\n\n### 模式一：自动扫描（默认）\n\n当 `config/module.json` **不存在**时，框架通过以下两种方式发现模块：\n\n**1. 目录扫描**\n\n从项目根目录开始扫描，查找含有 `eggModule.name` 的 `package.json`。\n\n- 默认扫描深度：10 层\n- 自动排除：隐藏目录（`.` 开头）、`node_modules/`、`coverage/`\n\n可通过应用配置调整扫描深度和排除路径：\n\n```typescript\n// config/config.default.ts\nexport default {\n  tegg: {\n    readModuleOptions: {\n      deep: 5, // 默认 10\n      extraFilePattern: ['!**/dist'], // 额外排除 dist 目录\n    },\n  },\n};\n```\n\n`extraFilePattern` 使用 [globby](https://github.com/sindresorhus/globby) 语法，以 `!` 开头表示排除。\n\n```\napp/\n├── fooModule/          ✅ 被扫描到\n│   └── package.json    { \"eggModule\": { \"name\": \"fooModule\" } }\n├── barModule/          ✅ 被扫描到\n│   └── package.json    { \"eggModule\": { \"name\": \"barModule\" } }\n├── .hidden/            ❌ 隐藏目录，自动排除\n│   └── package.json\n└── common/             ❌ 无 package.json，不会加载\n    └── utils.ts\n```\n\n**2. npm 包扫描**\n\n框架会遍历项目 `package.json` 中 `dependencies` 的每个包（不含 `devDependencies`），检查其 `package.json` 是否含有 `eggModule.name`，如果有则自动作为模块加载。\n\n### 模式二：手动声明\n\n创建 `config/module.json` 后，自动扫描**完全禁用**，仅加载文件中声明的模块：\n\n```json\n[\n  { \"path\": \"../app/module-a\" }, // 相对于 config 目录的路径\n  { \"package\": \"@eggjs/common-module\" } // npm 包名\n]\n```\n\n## module 配置\n\n在模块根目录创建 `module.yml` 存放模块专属配置：\n\n```\napp/\n└── userModule/\n    ├── package.json\n    ├── module.yml          # 基础配置\n    ├── module.unittest.yml # 环境特定配置（可选）\n    └── UserService.ts\n```\n\n### 配置文件格式\n\n支持 YAML 和 JSON 两种格式，优先加载 YAML：\n\n```yaml\n# module.yml\nfeatures:\n  dynamic:\n    foo: bar\n```\n\n### 环境配置合并\n\n框架会按 `module.yml` → `module.{env}.yml` 的顺序深度合并：\n\n```yaml\n# module.yml\nfeatures:\n  dynamic:\n    foo: bar\n\n# module.unittest.yml\nfeatures:\n  dynamic:\n    testMode: true\n```\n\nunittest 环境下合并结果：\n\n```json\n{\n  \"features\": {\n    \"dynamic\": {\n      \"foo\": \"bar\",\n      \"testMode\": true\n    }\n  }\n}\n```\n\n### 注入配置\n\n通过 `@Inject()` 注入 `moduleConfig`，框架自动注入当前模块的配置：\n\n```typescript\nimport { SingletonProto, Inject } from 'egg';\n\ninterface ModuleConfig {\n  features: {\n    dynamic: {\n      foo: string;\n    };\n  };\n}\n\n@SingletonProto()\nexport class UserService {\n  @Inject()\n  moduleConfig: ModuleConfig;\n\n  async getFeatureFlag(): Promise<string> {\n    return this.moduleConfig.features.dynamic.foo; // 'bar'\n  }\n}\n```\n\n## module 目录组织\n\n### 新应用\n\n直接在 `app/` 目录下平铺 module。可按功能划分，也可按业务划分：\n\n**按功能划分：**\n\n```\napp/\n├── authModule/\n│   └── package.json\n├── logModule/\n│   └── package.json\n└── storageModule/\n    └── package.json\n```\n\n**按业务划分：**\n\n```\napp/\n├── userModule/\n│   └── package.json\n├── orderModule/\n│   └── package.json\n└── paymentModule/\n    └── package.json\n```\n\n### 存量应用（包含老的 egg 写法）\n\n保留原有 `app/controller`、`app/service` 等目录不动，将新增的 module 统一放在 `app/module/` 下：\n\n```\napp/\n├── controller/           # 老的 egg 代码，保持不变\n├── service/\n├── module/               # 新增 module 统一放这里\n│   ├── userModule/\n│   │   └── package.json\n│   └── orderModule/\n│       └── package.json\n└── router.ts\n```\n","packages/skills/egg-core/references/proto.md":"# Proto 开发指南\n\n## SingletonProto vs ContextProto\n\n|          | SingletonProto     | ContextProto       |\n| -------- | ------------------ | ------------------ |\n| 创建时机 | 应用启动时立即创建 | 请求到达时按需创建 |\n| 实例数量 | 整个应用 1 个      | 每个请求 1 个      |\n| 销毁时机 | 应用关闭           | 请求结束           |\n\n**决策逻辑：默认使用 `@SingletonProto()`**，性能更好。只有当需要在多个服务间共享请求级隔离状态时，才使用 `@ContextProto()`。\n\n```typescript\nimport { SingletonProto, ContextProto } from 'egg';\n\n// ✅ 默认选择：无状态服务用 SingletonProto\n@SingletonProto()\nexport class UserService {\n  async findUser(id: string): Promise<User> {\n    // 无需请求级状态，适合 Singleton\n  }\n}\n\n// 仅在需要请求级隔离时使用 ContextProto\n@ContextProto()\nexport class RequestContext {\n  traceId: string;\n  userId: string;\n  // 每个请求独立的状态\n}\n```\n\n## 装饰器参数\n\n`@SingletonProto()` 和 `@ContextProto()` 接受相同的可选参数：\n\n| 参数          | 类型        | 默认值         | 说明                                 |\n| ------------- | ----------- | -------------- | ------------------------------------ |\n| name          | string      | 类名首字母小写 | 实例名称                             |\n| accessLevel   | AccessLevel | PRIVATE        | 跨模块可见性                         |\n| protoImplType | string      | 'DEFAULT'      | 实现类型标记（高级用法，一般不需要） |\n\n名称推导规则：类名首字母自动转小写，如 `MyService` → `myService`。\n\n```typescript\nimport { SingletonProto, AccessLevel } from 'egg';\n\n// 使用默认参数：name 为 \"helloService\"，accessLevel 为 PRIVATE\n@SingletonProto()\nexport class HelloService {}\n\n// 自定义参数\n@SingletonProto({\n  name: 'customName',\n  accessLevel: AccessLevel.PUBLIC,\n})\nexport class HelloService {}\n```\n\n## 注入规则\n\n**SingletonProto 可以注入 ContextProto**，框架会自动处理生命周期差异。\n\n实现机制：框架不会直接注入 ContextProto 实例，而是通过 lazy getter/Proxy，在每次访问时从当前请求上下文动态解析，确保每个请求拿到各自的实例。\n\n```typescript\nimport { SingletonProto, ContextProto, Inject } from 'egg';\n\n@ContextProto()\nexport class RequestContext {\n  userId: string;\n}\n\n@SingletonProto()\nexport class AppService {\n  @Inject()\n  requestContext: RequestContext; // ✅ 每次访问自动解析当前请求的实例\n}\n```\n\nContextProto 也可以注入 SingletonProto：\n\n```typescript\n@ContextProto()\nexport class RequestHandler {\n  @Inject()\n  appService: AppService; // ✅ 直接注入\n}\n```\n\n## AccessLevel 跨模块访问\n\n- `AccessLevel.PRIVATE`（默认）：仅同模块内可注入\n- `AccessLevel.PUBLIC`：跨模块可注入\n\n模块边界由 `package.json` 中的 `eggModule.name` 决定。\n\n```typescript\n// userModule 中\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class UserService {}\n\n// orderModule 中\n@SingletonProto()\nexport class OrderService {\n  @Inject()\n  userService: UserService; // ✅ UserService 是 PUBLIC，跨模块可注入\n}\n```\n\n```typescript\n// userModule 中\n@SingletonProto() // 默认 PRIVATE\nexport class UserHelper {}\n\n// orderModule 中\n@SingletonProto()\nexport class OrderService {\n  @Inject()\n  userHelper: UserHelper; // ❌ 报错：UserHelper 是 PRIVATE，不能跨模块注入\n}\n```\n\n## 生命周期\n\nProto 对象支持以下生命周期钩子，按执行顺序排列：\n\n| 阶段 | 装饰器                      | 说明                             |\n| ---- | --------------------------- | -------------------------------- |\n| 1    | constructor                 | 构造函数                         |\n| 2    | `@LifecyclePostConstruct()` | 构造完成后                       |\n| 3    | `@LifecyclePreInject()`     | 注入前                           |\n| 4    | —                           | 依赖注入                         |\n| 5    | `@LifecyclePostInject()`    | 注入完成后                       |\n| 6    | `@LifecycleInit()`          | 异步初始化（对象就绪前最后一步） |\n| 7    | —                           | **对象就绪，可被使用**           |\n| 8    | `@LifecyclePreDestroy()`    | 销毁前                           |\n| 9    | `@LifecycleDestroy()`       | 销毁，用于清理资源               |\n\n常用的是 `@LifecycleInit()` 和 `@LifecycleDestroy()`。\n\n**示例 1：SingletonProto 异步初始化和资源清理**\n\n```typescript\nimport { SingletonProto, LifecycleInit, LifecycleDestroy } from 'egg';\n\n@SingletonProto()\nexport class DatabaseService {\n  private connection: Connection;\n\n  @LifecycleInit()\n  async init(): Promise<void> {\n    this.connection = await createConnection();\n  }\n\n  @LifecycleDestroy()\n  async destroy(): Promise<void> {\n    await this.connection.close();\n  }\n}\n```\n\n**示例 2：ContextProto 在注入完成后初始化状态**\n\n```typescript\nimport { ContextProto, Inject, LifecyclePostInject } from 'egg';\n\n@ContextProto()\nexport class RequestTracer {\n  @Inject()\n  traceService: TraceService;\n\n  traceId: string;\n\n  @LifecyclePostInject()\n  async postInject(): Promise<void> {\n    // 依赖注入完成后，利用注入的服务初始化状态\n    this.traceId = await this.traceService.generateTraceId();\n  }\n}\n```\n","packages/skills/egg-unittest/SKILL.md":"---\nname: egg-unittest\ndescription: 本技能用于编写 EGG 应用的单元测试。覆盖 HTTP 接口测试、Service/DI 对象测试、Mock 数据模拟、BackgroundTask 和 EventBus 测试。使用 @eggjs/mock、app.httpRequest()、app.getEggObject()、mm() 等 API。\nallowed-tools: Read\n---\n\n# EGG 单元测试\n\n---\n\n## 原理\n\n`egg-bin test` 使用 Vitest 运行测试，自动完成以下工作：\n\n- 以当前项目目录为 baseDir，创建并启动一个 MockApplication 实例（即 `app`）\n- 注入 `@eggjs/mock/setup_vitest` 管理 app 生命周期（`beforeAll` 启动 app、`afterEach` 恢复 mock、`afterAll` 关闭 app）\n- 注入 Vitest 全局变量（`describe`、`it`、`beforeAll` 等），无需手动 import\n  测试代码中通过 `import { app, mm } from '@eggjs/mock/bootstrap'` 获取已启动的 app 实例和 mock 工具，直接使用即可。\n\n---\n\n## 配置检查\n\n确保项目中有以下配置：\n\n**package.json：**\n\n```json\n{\n  \"scripts\": {\n    \"test\": \"egg-bin test\"\n  },\n  \"devDependencies\": {\n    \"@eggjs/bin\": \"^8\",\n    \"@eggjs/mock\": \"^8\"\n  }\n}\n```\n\n**测试文件约定：**\n\n- 测试目录：`test/`\n- 文件命名：`*.test.ts`\n\n**自定义 setup 文件（可选）：**\n\n如果存在 `test/.setup.ts`，egg-bin 会自动将其加入 vitest setupFiles，在 `@eggjs/mock/setup_vitest` 之前执行（即 app 启动之前）。可用于设置环境变量等全局初始化：\n\n```typescript\n// test/.setup.ts\nbeforeAll(() => {\n  process.env.SOME_CONFIG = 'test-value';\n});\n```\n\n---\n\n## 简单示例\n\n### HTTP 接口测试\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\n\ndescribe('test/controller/home.test.ts', () => {\n  it('should GET /', () => {\n    return app.httpRequest().get('/').expect(200).expect('hello world');\n  });\n});\n```\n\n### Service 测试\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\nimport { UserService } from '../app/modules/user/UserService.ts';\n\ndescribe('test/service/user.test.ts', () => {\n  it('should get user', async () => {\n    const userService = await app.getEggObject(UserService);\n    const user = await userService.getById('1');\n    assert(user);\n    assert.equal(user.name, 'test');\n  });\n});\n```\n\n---\n\n## 测试场景决策树\n\n```\n要测什么？\n\n1. HTTP 接口（GET/POST/PUT/DELETE）？\n   → 参考 references/http-test.md\n\n2. Service / DI 对象的方法？\n   → 参考 references/service-test.md\n\n3. 需要 mock 外部依赖？（HTTP 调用、Service 方法、Session、CSRF）\n   → 参考 references/mock.md\n\n4. BackgroundTaskHelper（后台异步任务）？\n   → 参考 references/background-task-test.md\n\n5. EventBus（事件驱动）？\n   → 参考 references/eventbus-test.md\n```\n\n---\n\n## 快速参考\n\n| API                                                  | 说明                                    |\n| ---------------------------------------------------- | --------------------------------------- |\n| `import { app, mm } from '@eggjs/mock/bootstrap'`    | 标准测试入口                            |\n| `app.httpRequest().get('/path').expect(200)`         | HTTP 接口测试                           |\n| `app.getEggObject(Class)`                            | 获取 SingletonProto / ContextProto 实例 |\n| `app.mockModuleContextScope(async (ctx) => { ... })` | ContextProto 测试作用域                 |\n| `mm(Class.prototype, 'method', fn)`                  | Mock Proto 方法                         |\n| `app.mockCsrf()`                                     | 跳过 CSRF 校验（POST 测试必备）         |\n| `app.mockHttpclient(url, data)`                      | Mock 外部 HTTP 调用                     |\n| `app.getEventWaiter()`                               | 获取 EventBus 事件等待器                |\n\n---\n\n## 常见错误\n\n| 错误写法                           | 正确写法                                      | 说明                        |\n| ---------------------------------- | --------------------------------------------- | --------------------------- |\n| `import { app } from 'egg'`        | `import { app } from '@eggjs/mock/bootstrap'` | 测试使用 mock 包            |\n| `before()` / `after()`             | `beforeAll()` / `afterAll()`                  | Vitest 钩子，不是 Mocha     |\n| POST 测试报 403                    | 加 `app.mockCsrf()`                           | 安全插件默认开启 CSRF       |\n| 手动写 `afterEach(mm.restore)`     | 不需要                                        | egg-bin 自动注入 mock 恢复  |\n| 代码写在 describe 内、hooks 外     | 放入 `beforeAll` / `beforeEach`               | describe 体在加载阶段就执行 |\n| `await app.ready()` 配合 bootstrap | 不需要                                        | bootstrap 自动处理生命周期  |\n\n---\n\n## 参考资料\n\n- `references/http-test.md` — HTTP 接口测试\n- `references/service-test.md` — Service/DI 对象测试\n- `references/mock.md` — Mock 模式\n- `references/background-task-test.md` — BackgroundTaskHelper 测试\n- `references/eventbus-test.md` — EventBus 测试\n","packages/skills/egg-unittest/references/background-task-test.md":"# BackgroundTaskHelper 测试\n\n## 常见错误\n\n| 错误写法                                           | 正确写法                                                                              | 说明                                                 |\n| -------------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------- |\n| 不等待就断言                                       | 用 `mockModuleContextScope`（自动等待）或 `app.backgroundTasksFinished()`（手动等待） | 后台任务异步执行，必须等待完成后再断言               |\n| 在 `mockModuleContextScope` 回调内断言后台任务结果 | 在 `mockModuleContextScope` 返回后断言                                                | 回调内任务尚未完成，返回后才会等待完成               |\n| 用 `TimerUtil.sleep` 等待                          | 用 `app.backgroundTasksFinished()`                                                    | sleep 时间不确定，`backgroundTasksFinished` 精确等待 |\n\n---\n\n## 使用 mockModuleContextScope\n\n`mockModuleContextScope` 退出时会自动等待所有后台任务完成（内部触发 `doPreDestroy`），scope 退出后直接断言即可：\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\nimport { CountService } from '../app/modules/count/CountService.ts';\n\nit('should complete background task', async () => {\n  await app.mockModuleContextScope(async (ctx) => {\n    const countService = await ctx.getEggObject(CountService);\n    // countService 内部通过 backgroundTaskHelper.run() 触发后台任务\n    await countService.doSomething();\n  });\n\n  // scope 退出后，后台任务已完成，直接断言\n  const countService = await app.getEggObject(CountService);\n  assert.equal(countService.count, 1);\n});\n```\n\n## 使用 backgroundTasksFinished\n\n不通过 `mockModuleContextScope` 触发的场景（如 HTTP 接口测试），scope 退出的自动等待机制不适用，需要手动调用 `app.backgroundTasksFinished()` 等待所有后台任务完成后，再做断言：\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\nimport { CountService } from '../app/modules/count/CountService.ts';\n\nit('should complete background task', async () => {\n  await app.httpRequest().get('/api/trigger-task').expect(200);\n\n  // 等待后台任务完成\n  await app.backgroundTasksFinished();\n\n  const countService = await app.getEggObject(CountService);\n  assert.equal(countService.count, 1);\n});\n```\n","packages/skills/egg-unittest/references/eventbus-test.md":"# EventBus 测试\n\n## 常见错误\n\n| 错误写法                         | 正确写法                                           | 说明                                         |\n| -------------------------------- | -------------------------------------------------- | -------------------------------------------- |\n| 先 emit 再 `eventWaiter.await()` | 先 `eventWaiter.await()` 再触发业务逻辑            | await 注册监听器，必须在事件发出前           |\n| 不等待事件处理完成就断言         | 使用 `eventWaiter.await('eventName')` 等待后再断言 | handler 异步执行，不等待则断言时可能尚未完成 |\n\n---\n\n## 基本测试模式\n\n使用 `app.getEventWaiter()` 等待事件被处理完成：\n\n```typescript\nimport assert from 'node:assert';\nimport { app, mm } from '@eggjs/mock/bootstrap';\nimport { HelloService } from '../app/modules/hello/HelloService.ts';\nimport { HelloHandler } from '../app/modules/hello/HelloHandler.ts';\n\ndescribe('EventBus', () => {\n  it('should handle event', async () => {\n    // mock handler 捕获调用参数\n    const mockFn = async (msg: string) => {};\n    mm(HelloHandler.prototype, 'handle', mockFn);\n\n    await app.mockModuleContextScope(async (ctx) => {\n      const helloService = await ctx.getEggObject(HelloService);\n      const eventWaiter = await app.getEventWaiter();\n\n      // 1. 先注册等待（必须在 emit 之前）\n      const eventPromise = eventWaiter.await('helloEgg');\n\n      // 2. 触发业务逻辑（内部会 emit 事件）\n      helloService.hello();\n\n      // 3. 等待 handler 执行完成\n      await eventPromise;\n    });\n\n    // 4. 验证 handler 被调用及参数\n    assert.equal(mockFn.called, 1);\n    assert.deepStrictEqual(mockFn.lastCalledArguments, ['hello']);\n  });\n});\n```\n","packages/skills/egg-unittest/references/http-test.md":"# HTTP 接口测试\n\n## 常见错误\n\n| 错误写法                                                 | 正确写法                        | 说明                                          |\n| -------------------------------------------------------- | ------------------------------- | --------------------------------------------- |\n| POST 测试不加 `app.mockCsrf()`                           | 在 POST 前调用 `app.mockCsrf()` | 安全插件默认开启 CSRF，不 mock 会返回 403     |\n| `app.httpRequest().get('/').expect(200)` 不 return/await | 必须 `return` 或 `await`        | 否则断言不会执行，测试永远通过                |\n| `.expect({ foo: 'bar' })` 用于部分匹配                   | 使用 `result.body` 手动断言     | `.expect(body)` 是全量匹配（deepStrictEqual） |\n\n---\n\n## 基本用法\n\n通过 `app.httpRequest()` 发起 HTTP 请求，返回 SuperTest 对象：\n\n```typescript\nimport { app } from '@eggjs/mock/bootstrap';\n\ndescribe('UserController', () => {\n  it('should GET /api/users', () => {\n    return app.httpRequest().get('/api/users').expect(200).expect({ users: [] });\n  });\n});\n```\n\n---\n\n## POST 请求 + CSRF\n\nPOST/PUT/DELETE 请求需要先调用 `app.mockCsrf()` 跳过 CSRF 校验：\n\n```typescript\nit('should POST /api/users', () => {\n  app.mockCsrf();\n  return app\n    .httpRequest()\n    .post('/api/users')\n    .send({ name: 'test', email: 'test@example.com' })\n    .expect(200)\n    .expect({ id: '1', name: 'test' });\n});\n```\n\n表单提交使用 `.type('form')`：\n\n```typescript\nit('should POST form data', () => {\n  app.mockCsrf();\n  return app.httpRequest().post('/api/login').type('form').send({ username: 'admin', password: '123' }).expect(200);\n});\n```\n\n---\n\n## 请求构造\n\n```typescript\napp\n  .httpRequest()\n  .get('/api/users')\n  .set('Authorization', 'Bearer token123') // 设置 header\n  .set('Accept', 'application/json') // 设置 Accept\n  .query({ page: 1, limit: 10 }) // 查询参数\n  .expect(200);\n```\n\n---\n\n## 响应断言\n\n使用 `.expect()` 链式断言：\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\n\nit('should validate response', () => {\n  return app\n    .httpRequest()\n    .get('/api/users/1')\n    .expect(200) // 只校验状态码\n    .expect({ id: '1', name: 'test' }) // 只校验 body（deepStrictEqual）\n    .expect(200, { id: '1', name: 'test' }) // 状态码 + body 合并\n    .expect('hello world') // body 字符串匹配\n    .expect(/hello/) // body 正则匹配\n    .expect('content-type', /json/) // header 匹配\n    .expect([200, 302]) // 多状态码匹配（任一即可）\n    .expect((res) => {\n      // 自定义断言函数\n      assert(res.body.id);\n    });\n});\n```\n\n使用 `result` 做更灵活的断言：\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\n\nit('should validate response', async () => {\n  const result = await app.httpRequest().get('/api/users/1');\n\n  assert.equal(result.status, 200);\n  assert.equal(result.body.name, 'test');\n  assert(result.body.id);\n  assert.match(result.headers['content-type'], /json/);\n});\n```\n\n完整的请求构造和断言 API 可查看项目 node_modules 中 `@eggjs/supertest` 的类型定义。\n\n---\n\n## 端到端示例\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\n\ndescribe('test/controller/user.test.ts', () => {\n  describe('GET /api/users/:id', () => {\n    it('should return user', () => {\n      return app.httpRequest().get('/api/users/1').expect(200).expect({ id: '1', name: 'test' });\n    });\n\n    it('should return 404 when user not found', () => {\n      return app.httpRequest().get('/api/users/999').expect(404);\n    });\n  });\n\n  describe('POST /api/users', () => {\n    it('should create user', () => {\n      app.mockCsrf();\n      return app.httpRequest().post('/api/users').send({ name: 'new user', email: 'new@example.com' }).expect(201);\n    });\n\n    it('should return 422 with invalid params', () => {\n      app.mockCsrf();\n      return app.httpRequest().post('/api/users').send({ name: '' }).expect(422);\n    });\n  });\n});\n```\n","packages/skills/egg-unittest/references/mock.md":"# Mock 模式\n\n## 常见错误\n\n| 错误写法                       | 正确写法                                   | 说明                           |\n| ------------------------------ | ------------------------------------------ | ------------------------------ |\n| `mm(service, 'method', fn)`    | `mm(ServiceClass.prototype, 'method', fn)` | DI 对象需 mock 原型，不是实例  |\n| 手动写 `afterEach(mm.restore)` | 不需要                                     | egg-bin 自动注入 mock 恢复     |\n| `new Ajv()` mock 单独实例      | mock 原型方法                              | DI 容器管理的对象通过原型 mock |\n\n---\n\n## mm() — Mock Proto 方法\n\n最常用的 mock 方式，mock DI 对象的原型方法：\n\n```typescript\nimport assert from 'node:assert';\nimport { app, mm } from '@eggjs/mock/bootstrap';\nimport { UserService } from '../app/modules/user/UserService.ts';\nimport { OrderService } from '../app/modules/order/OrderService.ts';\n\ndescribe('OrderService', () => {\n  it('should mock user service', async () => {\n    mm(UserService.prototype, 'getById', async () => {\n      return { id: '1', name: 'mocked user' };\n    });\n\n    const orderService = await app.getEggObject(OrderService);\n    const result = await orderService.createForUser('1');\n    assert.equal(result.userName, 'mocked user');\n  });\n});\n```\n\nmock 函数会自动记录调用信息，可以用来断言调用参数：\n\n```typescript\nimport assert from 'node:assert';\nimport { app, mm } from '@eggjs/mock/bootstrap';\nimport { NotifyService } from '../app/modules/notify/NotifyService.ts';\nimport { OrderService } from '../app/modules/order/OrderService.ts';\n\nit('should call notify with correct args', async () => {\n  const mockFn = async (userId: string, message: string) => {};\n  mm(NotifyService.prototype, 'send', mockFn);\n\n  const orderService = await app.getEggObject(OrderService);\n  await orderService.create({ productId: '1' });\n\n  assert.equal(mockFn.called, 1); // 调用次数\n  assert.deepStrictEqual(mockFn.lastCalledArguments, ['user-1', '订单创建成功']); // 最后一次调用参数\n  // mockFn.calledArguments — 所有调用参数的数组\n});\n```\n\n---\n\n## mm.spy() — 不替换实现，只记录调用\n\n```typescript\nit('should spy on method', async () => {\n  mm.spy(NotifyService.prototype, 'send');\n\n  const orderService = await app.getEggObject(OrderService);\n  await orderService.create({ productId: '1' });\n\n  // 原方法正常执行，同时记录了调用信息\n  const sendFn = NotifyService.prototype.send;\n  assert.equal(sendFn.called, 1);\n  assert.equal(sendFn.lastCalledArguments[0], 'user-1');\n});\n```\n\n---\n\n## app.mockHttpclient() — Mock HttpClient 请求\n\nMock 通过 `@Inject() httpclient: HttpClient` 注入的 HttpClient 发送的请求：\n\n```typescript\nit('should mock external API', () => {\n  app.mockHttpclient('https://api.example.com/users', {\n    data: JSON.stringify({ name: 'test' }),\n  });\n\n  return app.httpRequest().get('/api/proxy/users').expect(200).expect({ name: 'test' });\n});\n```\n\n---\n\n## app.mockCsrf() — 跳过 CSRF\n\nPOST/PUT/DELETE 测试时跳过 CSRF 校验：\n\n```typescript\nit('should POST without CSRF error', () => {\n  app.mockCsrf();\n  return app.httpRequest().post('/api/users').send({ name: 'test' }).expect(200);\n});\n```\n\n---\n\n## Mock 恢复\n\negg-bin 自动注入 `@eggjs/mock/setup_vitest`，会在 `afterEach` 钩子中自动调用 `mm.restore()`，无需手动编写。\n","packages/skills/egg-unittest/references/service-test.md":"# Service / DI 对象测试\n\n## 常见错误\n\n| 错误写法                 | 正确写法                                  | 说明                |\n| ------------------------ | ----------------------------------------- | ------------------- |\n| `ctx.service.user.get()` | `ctx.getEggObject(UserService)`           | 旧写法，新项目用 DI |\n| 不 await `getEggObject`  | `const svc = await ctx.getEggObject(Svc)` | 返回 Promise        |\n\n---\n\n## Singleton 测试\n\n`@SingletonProto` 对象直接通过 `app.getEggObject()` 获取：\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\nimport { ConfigService } from '../app/modules/foo/ConfigService.ts';\n\ndescribe('ConfigService', () => {\n  it('should get config', async () => {\n    const configService = await app.getEggObject(ConfigService);\n    const value = configService.get('key');\n    assert.equal(value, 'expected');\n  });\n});\n```\n\n---\n\n## ContextProto 测试\n\n`@ContextProto` 对象可以直接通过 `app.getEggObject()` 获取，也可以在 `app.mockModuleContextScope` 中通过 `ctx.getEggObject()` 获取。后者会创建带 DI 生命周期的 ctx，退出时自动销毁：\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\nimport { UserService } from '../app/modules/user/UserService.ts';\n\ndescribe('UserService', () => {\n  it('should get user in context scope', async () => {\n    await app.mockModuleContextScope(async (ctx) => {\n      const userService = await ctx.getEggObject(UserService);\n      const user = await userService.getById('1');\n      assert(user);\n    });\n  });\n});\n```\n\n---\n\n## Mock 被注入的依赖\n\n当 Service A 依赖 Service B 时，mock B 的原型方法：\n\n```typescript\nimport assert from 'node:assert';\nimport { app, mm } from '@eggjs/mock/bootstrap';\nimport { OrderService } from '../app/modules/order/OrderService.ts';\nimport { PaymentService } from '../app/modules/payment/PaymentService.ts';\n\ndescribe('OrderService', () => {\n  it('should create order with mocked payment', async () => {\n    mm(PaymentService.prototype, 'charge', async () => {\n      return { transactionId: 'mock-tx-001' };\n    });\n\n    const orderService = await app.getEggObject(OrderService);\n    const order = await orderService.create({ productId: '1', amount: 100 });\n    assert.equal(order.transactionId, 'mock-tx-001');\n  });\n});\n```\n","packages/skills/egg/SKILL.md":"---\nname: egg\ndescription: 本技能用于处理 EGG 框架。它提供基于用户意图在核心概念、控制器和单元测试之间做选择的决策指导。作为所有 EGG 相关问题的入口点使用。覆盖模块架构、依赖注入、后台任务、EventBus 事件总线、AOP 切面编程、HTTP/MCP/Schedule 控制器、Ajv 参数校验、单元测试等。\nallowed-tools: Read\n---\n\n# EGG 决策指南\n\n## 概述\n\n本技能帮助根据用户意图和任务类型确定使用哪个专用的 EGG 技能。EGG 文档组织为两个主要领域：\n\n1. **核心概念**（`egg-core` skill）：模块架构、依赖注入、对象生命周期、EventBus 事件总线、AOP 切面编程\n2. **控制器**（`egg-controller` skill）：用于 API 端点的各种协议特定控制器\n3. **单元测试**（`egg-unittest` skill）：HTTP 接口测试、Service/DI 对象测试、Mock 模拟、BackgroundTask 和 EventBus 测试\n\n## 技能选择逻辑\n\n### 使用 `egg-core` skill 当用户询问：\n\n**用户询问关于：**\n\n- 模块架构和组织\n- `@SingletonProto` vs `@ContextProto` 的使用\n- 使用 `@Inject` 的依赖注入\n- 对象生命周期和实例化\n- 模块之间的访问控制（`AccessLevel`）\n- 模块配置（`module.yml`、`package.json`）\n- 使用限定符解决命名冲突\n- 请求返回后执行异步任务（BackgroundTaskHelper）\n- 事件驱动架构（EventBus、@Event）\n- AOP 切面编程（@Advice、@Pointcut、@Crosscut）\n\n**触发关键词：**\n\n- module、workspace、modules\n- singleton、单例、@SingletonProto\n- context、request context、@ContextProto\n- inject、injection、dependency injection、@Inject\n- prototype、lifecycle、实例化\n- background task、异步任务、后台任务、BackgroundTaskHelper\n- eventbus、event bus、事件总线、事件驱动、@Event、emit、发布订阅、解耦\n- aop、切面、aspect、advice、pointcut、crosscut、拦截器、横切关注点\n- access level、private、public、@ModuleQualifier\n- configuration、module config\n\n**示例查询：**\n\n- \"如何在 EGG 中创建模块？\"\n- \"SingletonProto 和 ContextProto 有什么区别？\"\n- \"如何注入服务？\"\n- \"如何访问其他模块的对象？\"\n- \"EGG 中的 AccessLevel 是什么？\"\n- \"如何用 EventBus 解耦异步任务？\"\n- \"EventBus 和 BackgroundTaskHelper 有什么区别？\"\n- \"如何用 AOP 给所有 Service 加日志？\"\n\n### 使用 `egg-controller` skill 当用户询问：\n\n**用户询问关于：**\n\n- 创建 API 端点或接口\n- 实现特定协议处理器（HTTP、MCP 等）\n- 连接到外部系统或客户端\n- 处理传入的请求/响应\n- 控制器级别的装饰器和模式\n- 参数校验（Ajv + TypeBox）\n- 控制器中间件（Middleware，函数式或 AOP 写法）\n\n**触发关键词：**\n\n- controller、控制器\n- HTTP、API、REST、endpoint\n- MCP、LLM、AI、tool\n- schedule、timer、cron、scheduled、定时\n- SSE、streaming、server-sent events\n- validate、校验、参数校验、ajv、typebox、schema\n- middleware、中间件、拦截器、洋葱模型、@Middleware\n\n**示例查询：**\n\n- \"如何创建 HTTP controller？\"\n- \"如何实现 MCP 接口？\"\n- \"怎么实现定时任务？\"\n- \"帮我给接口加上参数校验\"\n- \"如何给控制器加中间件？\"\n- \"怎么写一个鉴权中间件？\"\n\n### 使用 `egg-unittest` skill 当用户询问：\n\n**用户询问关于：**\n\n- 编写单元测试或集成测试\n- 使用 @eggjs/mock 进行测试\n- 测试 HTTP 接口（app.httpRequest）\n- 测试 Service/DI 对象\n- Mock 数据或依赖\n- 测试 BackgroundTaskHelper 或 EventBus\n\n**触发关键词：**\n\n- test、测试、单测、单元测试、unittest、unit test\n- mock、mm、@eggjs/mock、mockCsrf、mockHttpclient、mockSession、mockContext\n- httpRequest、supertest\n- getEggObject、mockModuleContextScope\n- eventWaiter、BackgroundTaskHelper 测试\n- vitest、describe、it、beforeAll\n\n**示例查询：**\n\n- \"如何写单元测试？\"\n- \"如何测试 HTTP 接口？\"\n- \"如何 mock 一个 Service？\"\n- \"POST 请求测试报 CSRF 错误\"\n- \"如何测试 ContextProto 的 Service？\"\n- \"怎么测试后台任务是否执行完成？\"\n- \"怎么测试 EventBus 事件是否被正确处理？\"\n\n---\n\n## 决策框架\n\n### 步骤 1：识别意图类型\n\n询问：**用户是在询问构建块/框架内部 OR 实现特定接口？**\n\n**构建块/框架内部** → 使用 `egg-core` skill\n\n- 理解 EGG 如何工作\n- 组织代码结构\n- 管理对象生命周期\n- 设置模块\n\n**实现特定接口** → 使用 `egg-controller` skill\n\n- 创建 API/端点\n- 处理不同协议\n- 处理请求/响应\n\n### 步骤 2：检查模糊意图\n\n如果用户的意图可能是核心 OR 控制器（例如，\"如何实现一个需要跨模块访问的服务？\"）：\n\n**决策优先级**：核心概念优先\n\n理由：即使服务将在控制器中使用，关于跨模块访问（`AccessLevel`）的基本问题是一个核心概念。一旦理解了核心结构，用户就可以在控制器中应用它。\n\n**行动**：\n\n1. 使用 `egg-core` skill\n2. 解释概念（例如，`AccessLevel.PUBLIC`）\n3. 核心解释后，建议：\"如果你需要在特定控制器中使用它，请使用 `egg-controller` skill\n\n### 步骤 3：协议/用例特定指示器\n\n| 协议/用例               | 主要技能         | 次要技能 |\n| ----------------------- | ---------------- | -------- |\n| HTTP API                | `egg-controller` | -        |\n| MCP                     | `egg-controller` | -        |\n| Scheduled Tasks         | `egg-controller` | -        |\n| Parameter validation    | `egg-controller` | -        |\n| Controller Middleware   | `egg-controller` | -        |\n| Background tasks        | `egg-core`       | -        |\n| Event-driven / EventBus | `egg-core`       | -        |\n| AOP / 切面编程          | `egg-core`       | -        |\n| Cross-module injection  | `egg-core`       | -        |\n| Module structure        | `egg-core`       | -        |\n| Object lifecycle        | `egg-core`       | -        |\n| Unit Testing            | `egg-unittest`   | -        |\n| Mock / Test helpers     | `egg-unittest`   | -        |\n\n## 冲突解决规则\n\n### 规则 1：基础优先\n\n当问题同时涉及核心概念 AND 控制器实现时：\n\n- **示例**：\"如何实现一个 HTTP 控制器可以使用的单例服务？\"\n- **决策**：从 `egg-core` skill 开始解释 SingletonProto 和 AccessLevel\n- **后续**：\"现在你理解了服务定义，使用 `egg-controller` skill 实现注入此服务的 HTTP 控制器。\"\n\n### 规则 2：显式覆盖\n\n如果用户明确提及特定控制器类型：\n\n- **示例**：\"如何使用 HTTPController 配合 ContextProto 服务？\"\n- **决策**：使用 `egg-controller` skill（HTTPController 是显式的）\n- **后续**：解释 HTTPController 实现，如果需要简要提及来自核心概念的 ContextProto\n\n### 规则 3：学习语境\n\n如果用户问\"什么是 X？\"或\"Y 如何工作？\"：\n\n- **核心概念问题** → 使用 `egg-core` skill\n- **控制器类型问题** → 使用 `egg-controller` skill\n- **一般 EGG 问题** → 使用本技能的决策框架\n\n如果用户问\"如何实现 X？\"或\"给我看 Y 的代码？\"：\n\n- **实现特定问题** → 根据决策框架加载特定 skill\n\n## 快速参考表\n\n| 用户意图             | 关键词                                  | 使用技能               |\n| -------------------- | --------------------------------------- | ---------------------- |\n| Module architecture  | module、workspace、organization         | `egg-core` skill       |\n| Object lifecycle     | singleton、context、lifecycle           | `egg-core` skill       |\n| Dependency injection | inject、@Inject、dependency             | `egg-core` skill       |\n| Access control       | private、public、cross-module           | `egg-core` skill       |\n| Background tasks     | background task、异步任务、后台任务     | `egg-core` skill       |\n| Event-driven         | eventbus、事件总线、@Event、emit        | `egg-core` skill       |\n| AOP 切面编程         | aop、切面、advice、pointcut、crosscut   | `egg-core` skill       |\n| HTTP endpoints       | HTTP、API、REST                         | `egg-controller` skill |\n| LLM/AI integration   | MCP、tool、prompt                       | `egg-controller` skill |\n| Scheduling           | schedule、cron、timer                   | `egg-controller` skill |\n| Param validation     | validate、校验、ajv、typebox、schema    | `egg-controller` skill |\n| Middleware 中间件    | middleware、中间件、拦截器、@Middleware | `egg-controller` skill |\n| Unit testing         | test、mock、unittest、单测              | `egg-unittest` skill   |\n| Mock dependencies    | mock、mm、mockCsrf、mockHttpclient      | `egg-unittest` skill   |\n\n---\n\n## 示例\n\n### 示例 1：明确的核心意图\n\n**用户**：\"@SingletonProto 和 @ContextProto 有什么区别？\"\n\n**分析**：问题关于核心装饰器和对象生命周期\n**决策**：使用 `egg-core` skill\n\n**用户**：\"如何在 EGG 中创建模块？\"\n\n**分析**：问题关于模块架构（核心概念）\n**决策**：使用 `egg-core` skill\n\n### 示例 2：明确的控制器意图\n\n**用户**：\"如何创建返回 JSON 的 HTTP controller？\"\n\n**分析**：问题关于实现特定协议（HTTP）\n**决策**：使用 `egg-controller` skill\n\n### 示例 3：模糊意图（核心 > 控制器）\n\n**用户**：\"我需要创建一个可以被 HTTP 控制器使用的服务。如何实现？\"\n\n**分析**：用户需要理解核心概念（跨模块访问）AND 控制器实现\n**决策**：首先使用 `egg-core` skill（基础）\n\n**响应**：\n\n1. 解释 `@SingletonProto` 配合 `AccessLevel.PUBLIC` 使服务可访问\n2. 展示如何注入服务：`@Inject() myService: MyService`\n3. 后续：\"现在你可以在 HTTPController 中注入此服务。实现详情请使用 `egg-controller` skill。\"\n\n### 示例 4：显式控制器加上核心知识\n\n**用户**：\"如何在 HTTPController 中使用 @Inject 访问用户服务？\"\n\n**分析**：用户明确提及 HTTPController（控制器类型）但询问 @Inject（核心概念）\n**决策**：使用 `egg-controller` skill（HTTPController 是明确意图）\n\n**响应**：\n\n1. 展示配合 `@Inject` 的 HTTPController 实现\n2. 简要解释 @Inject 如何工作（核心概念摘要）\n3. 注意：\"包含限定符的详细 @Inject 使用，请使用 `egg-core` skill\"\n\n### 示例 5：测试相关\n\n**用户**：\"帮我写个 UserController 的单元测试\"\n\n**分析**：问题关于编写测试代码\n**决策**：使用 `egg-unittest` skill\n\n**用户**：\"POST 请求测试报 403 错误\"\n\n**分析**：测试中遇到 CSRF 问题\n**决策**：使用 `egg-unittest` skill\n\n## 路由最佳实践\n\n1. **优先考虑显式控制器提及**：如果用户命名特定控制器（HTTP），即使涉及核心概念也使用控制器技能\n\n2. **基础先行**：如果实现前需要理解核心概念，先解释核心概念\n\n3. **简短上下文可以接受**：当路由到一个技能时，提及另一个技能的存在以供后续问题\n\n4. **混合响应可接受**：当意图真正混合时，提供两个技能的简短上下文但专注于主要意图\n\n5. **渐进式披露**：除非明确要求，不要同时加载两个技能。让用户引导探索\n\n---\n\n## 技能交互\n\n本技能（`egg` skill）应该：\n\n- 为框架内部概念路由到 `egg-core` skill\n- 为协议特定实现路由到 `egg-controller` skill\n- 为测试相关问题路由到 `egg-unittest` skill\n- 当意图模糊时提供决策指导\n- 当存在有用的上下文时交叉引用技能\n","packages/skills/eval/evals-egg-controller.json":"{\n  \"skill_name\": \"egg-controller\",\n  \"description\": \"控制器评测：覆盖 http-controller、mcp-controller、schedule、ajv-validate、middleware\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"帮我写一个 POST /api/users 接口，接收 JSON body 创建用户，返回 201\",\n      \"expected_output\": \"@HTTPController + @HTTPMethod POST + @HTTPBody + ctx.status = 201，完整可运行代码\"\n    },\n    {\n      \"id\": 2,\n      \"prompt\": \"帮我实现 GET /api/users/:userId/posts/:postId，获取用户的某篇文章\",\n      \"expected_output\": \"@HTTPParam() userId + @HTTPParam() postId，路径参数定义和获取的完整代码\"\n    },\n    {\n      \"id\": 3,\n      \"prompt\": \"我有个搜索接口 /api/search?tag=tech&tag=dev，需要获取 tag 的全部值\",\n      \"expected_output\": \"使用 @HTTPQueries({ name: 'tag' }) 获取 string[] 数组，不是 @HTTPQuery\"\n    },\n    {\n      \"id\": 4,\n      \"prompt\": \"帮我写个 SSR 页面，GET / 返回完整的 HTML\",\n      \"expected_output\": \"@HTTPController + ctx.type = 'html' + return HTML 字符串\"\n    },\n    {\n      \"id\": 5,\n      \"prompt\": \"帮我实现流式输出，一段一段返回 HTML 内容\",\n      \"expected_output\": \"async generator + Readable.from()，设置 ctx.type，完整代码\"\n    },\n    {\n      \"id\": 6,\n      \"prompt\": \"我有两个路由 /api/:name 和 /api/health，请求 /api/health 匹配到了 :name，帮我看看代码\",\n      \"expected_output\": \"静态路径优先级高于参数路径应该自动匹配，如果冲突可用 priority 参数调整\",\n      \"files\": [\n        {\n          \"path\": \"app/apiModule/ApiController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPParam } from 'egg';\\n\\n@HTTPController({ path: '/api' })\\nexport class ApiController {\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/:name' })\\n  async getByName(@HTTPParam() name: string) {\\n    return { name };\\n  }\\n\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/health' })\\n  async health() {\\n    return { status: 'ok' };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 7,\n      \"prompt\": \"帮我给这个 Controller 加一个认证中间件的逻辑，从 header 里取 Authorization 做校验\",\n      \"expected_output\": \"使用 @HTTPHeaders() 获取 headers，key 用小写 headers['authorization']\",\n      \"files\": [\n        {\n          \"path\": \"app/apiModule/UserController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum } from 'egg';\\n\\n@HTTPController({ path: '/api/users' })\\nexport class UserController {\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })\\n  async list() {\\n    return { users: [] };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 8,\n      \"prompt\": \"帮我写一个完整的 CRUD 模块：userModule，包含 UserService 和 UserController，支持 GET/POST/PUT/DELETE\",\n      \"expected_output\": \"完整文件结构：package.json + UserService(@SingletonProto + PUBLIC) + UserController(@HTTPController)，四个 HTTP 方法的完整代码\"\n    },\n    {\n      \"id\": 9,\n      \"prompt\": \"基于这个 Service，帮我写一个 HTTPController 暴露查询和创建接口\",\n      \"expected_output\": \"@HTTPController + 两个 @HTTPMethod（GET 用 @HTTPParam，POST 用 @HTTPBody）+ @Inject 注入 ProductService\",\n      \"files\": [\n        {\n          \"path\": \"app/productModule/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"productModule\\\",\\n  \\\"eggModule\\\": { \\\"name\\\": \\\"productModule\\\" }\\n}\"\n        },\n        {\n          \"path\": \"app/productModule/ProductService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class ProductService {\\n  async findById(id: string) {\\n    return { id, name: 'Product ' + id, price: 99 };\\n  }\\n\\n  async create(data: { name: string; price: number }) {\\n    return { id: 'new-id', ...data };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 10,\n      \"prompt\": \"帮我实现一个 MCP Tool，让 AI 能搜索我们的内部文档\",\n      \"expected_output\": \"@MCPController() + @MCPTool() + @ToolArgsSchema + Service 注入，import 从 @eggjs/tegg，zod 从 @eggjs/tegg/zod\"\n    },\n    {\n      \"id\": 11,\n      \"prompt\": \"MCP Tool 的参数 Schema 写法有问题，帮我看看\",\n      \"expected_output\": \"指出不应该用 z.object() 包装，直接用普通对象 { name: z.string() }\",\n      \"files\": [\n        {\n          \"path\": \"app/docModule/DocMCPController.ts\",\n          \"content\": \"import { MCPController, MCPTool, MCPToolResponse, ToolArgs, ToolArgsSchema, Inject } from '@eggjs/tegg';\\nimport { z } from '@eggjs/tegg/zod';\\n\\nconst SearchSchema = z.object({\\n  keyword: z.string({ description: 'search keyword' }),\\n  limit: z.number().optional(),\\n});\\n\\n@MCPController()\\nexport class DocMCPController {\\n  @MCPTool({ description: 'Search internal docs' })\\n  async search(\\n    @ToolArgsSchema(SearchSchema) args: ToolArgs<typeof SearchSchema>,\\n  ): Promise<MCPToolResponse> {\\n    return { content: [{ type: 'text', text: 'results' }] };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 12,\n      \"prompt\": \"我的 MCP Tool 要处理一个大数据导出，可能要跑几分钟，怎么给客户端推送进度\",\n      \"expected_output\": \"@Extra() 获取 ToolExtra，用 sendNotification 推送进度，给出完整代码\"\n    },\n    {\n      \"id\": 13,\n      \"prompt\": \"帮我写一个 MCP Resource，让 AI 能读取指定用户的 profile\",\n      \"expected_output\": \"@MCPResource({ template: [...] }) + uri 参数解析，完整代码\"\n    },\n    {\n      \"id\": 14,\n      \"prompt\": \"帮我写个定时任务，每天凌晨 3 点清理过期数据\",\n      \"expected_output\": \"@Schedule<CronParams> + cron: '0 0 3 * * *' + ScheduleType.WORKER，import 从 egg/schedule\"\n    },\n    {\n      \"id\": 15,\n      \"prompt\": \"定时任务放在 app/schedule 目录下不生效，帮我看看\",\n      \"expected_output\": \"指出不能放在 app/schedule 下（和 egg 默认扫描冲突），应放在模块目录中\",\n      \"files\": [\n        {\n          \"path\": \"app/schedule/CleanTask.ts\",\n          \"content\": \"import { Inject, Logger } from 'egg';\\nimport { IntervalParams, Schedule, ScheduleType } from 'egg/schedule';\\n\\n@Schedule<IntervalParams>({\\n  type: ScheduleType.WORKER,\\n  scheduleData: { interval: '1h' },\\n})\\nexport class CleanTask {\\n  @Inject()\\n  private logger: Logger;\\n\\n  async subscribe() {\\n    this.logger.info('cleaning...');\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 16,\n      \"prompt\": \"帮我写一个每 30 秒刷新缓存的定时任务，要求每个 worker 都执行\",\n      \"expected_output\": \"@Schedule<IntervalParams> + interval: '30s' + ScheduleType.ALL\"\n    },\n    {\n      \"id\": 17,\n      \"prompt\": \"基于这个 CacheService，帮我写一个定时任务每 5 分钟刷新一次缓存\",\n      \"expected_output\": \"@Schedule<IntervalParams> + interval: '5m' + @Inject() cacheService，import 从 egg/schedule\",\n      \"files\": [\n        {\n          \"path\": \"app/cacheModule/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"cacheModule\\\",\\n  \\\"eggModule\\\": { \\\"name\\\": \\\"cacheModule\\\" }\\n}\"\n        },\n        {\n          \"path\": \"app/cacheModule/CacheService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class CacheService {\\n  private cache = new Map<string, any>();\\n\\n  async refresh() {\\n    this.cache.clear();\\n    // reload from db\\n  }\\n\\n  get(key: string) {\\n    return this.cache.get(key);\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 18,\n      \"prompt\": \"帮我把这个老的 egg controller 改成 HTTPController 写法\",\n      \"expected_output\": \"用 @HTTPController + @HTTPMethod 重写，参数用装饰器获取，注入 Service 替代 ctx.service\",\n      \"files\": [\n        {\n          \"path\": \"app/controller/user.ts\",\n          \"content\": \"import { Controller } from 'egg';\\n\\nexport default class UserController extends Controller {\\n  async show() {\\n    const { ctx } = this;\\n    const id = ctx.params.id;\\n    const user = await ctx.service.user.find(id);\\n    ctx.body = { success: true, data: user };\\n  }\\n\\n  async create() {\\n    const { ctx } = this;\\n    const body = ctx.request.body;\\n    const user = await ctx.service.user.create(body);\\n    ctx.status = 201;\\n    ctx.body = { success: true, data: user };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 19,\n      \"prompt\": \"帮我给这个 POST /api/users 接口加上参数校验，name 必填且最多50字符，email 必须是邮箱格式\",\n      \"expected_output\": \"从 egg/ajv 导入 Type/Ajv/Static，定义 TypeBox Schema（name: Type.String + maxLength, email: Type.String + format: email），@Inject() ajv: Ajv，ajv.validate()\",\n      \"files\": [\n        {\n          \"path\": \"app/userModule/UserController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPBody } from 'egg';\\n\\n@HTTPController({ path: '/api/users' })\\nexport class UserController {\\n  @HTTPMethod({ method: HTTPMethodEnum.POST, path: '/' })\\n  async create(@HTTPBody() body: { name: string; email: string }) {\\n    return { id: '1', ...body };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 20,\n      \"prompt\": \"参数校验跑不起来，帮我看看代码哪里有问题\",\n      \"expected_output\": \"指出三个错误：1. 导入路径应从 egg/ajv 而非 typebox/ajv 2. 不应 new Ajv() 手动创建 3. 应使用 @Inject() 注入全局单例\",\n      \"files\": [\n        {\n          \"path\": \"app/userModule/UserController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPBody, Inject } from 'egg';\\nimport { Type, Static } from 'typebox';\\nimport Ajv from 'ajv';\\n\\nconst CreateUserSchema = Type.Object({\\n  name: Type.String({ maxLength: 50 }),\\n  email: Type.String({ format: 'email' }),\\n});\\n\\ntype CreateUserParams = Static<typeof CreateUserSchema>;\\n\\nconst ajv = new Ajv();\\n\\n@HTTPController({ path: '/api/users' })\\nexport class UserController {\\n  @HTTPMethod({ method: HTTPMethodEnum.POST, path: '/' })\\n  async create(@HTTPBody() body: CreateUserParams) {\\n    const valid = ajv.validate(CreateUserSchema, body);\\n    if (!valid) throw new Error('Validation failed');\\n    return { id: '1', ...body };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 21,\n      \"prompt\": \"帮我写一个注册接口的参数校验，要校验 email 格式、生日要是日期格式、用户ID要是 UUID 格式\",\n      \"expected_output\": \"使用 Type.String({ format: 'email' })、Type.String({ format: 'date' })、Type.String({ format: 'uuid' })，从 egg/ajv 导入\"\n    },\n    {\n      \"id\": 22,\n      \"prompt\": \"用户输入的名字前后经常有空格，怎么在参数校验时自动去掉空格\",\n      \"expected_output\": \"使用 TransformEnum.trim，从 egg/ajv 导入 TransformEnum，Type.String({ transform: [TransformEnum.trim] })\"\n    },\n    {\n      \"id\": 23,\n      \"prompt\": \"有些字段是选填的，比如用户的昵称和个人简介，校验时不传不应该报错\",\n      \"expected_output\": \"使用 Type.Optional() 包装选填字段，如 Type.Optional(Type.String())\"\n    },\n    {\n      \"id\": 24,\n      \"prompt\": \"帮我写一个校验 Schema，要求 tags 字段是一个元组，第一个元素是字符串类型的分类名，第二个元素是数字类型的权重\",\n      \"expected_output\": \"使用 Type.Tuple([Type.String(), Type.Number()])，AI 需要从 TypeBox 文档链接查找 Tuple 用法\"\n    },\n    {\n      \"id\": 25,\n      \"prompt\": \"基于这个 OrderService，帮我写一个创建订单的接口，需要做参数校验：productId 必填字符串，quantity 必填正整数，remark 选填\",\n      \"expected_output\": \"定义 Schema + @Inject() ajv: Ajv + ajv.validate() + @Inject() orderService，完整可运行代码\",\n      \"files\": [\n        {\n          \"path\": \"app/orderModule/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"orderModule\\\",\\n  \\\"eggModule\\\": { \\\"name\\\": \\\"orderModule\\\" }\\n}\"\n        },\n        {\n          \"path\": \"app/orderModule/OrderService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class OrderService {\\n  async create(data: { productId: string; quantity: number; remark?: string }) {\\n    return { orderId: 'order-' + Date.now(), ...data };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 26,\n      \"prompt\": \"帮我给这个 Controller 加个日志中间件，记录每个请求的耗时\",\n      \"expected_output\": \"使用 AOP 写法：定义 @Advice 类实现 IAdvice 的 around 方法，@Inject Logger，用 @Middleware 应用到 Controller。import Advice 从 egg/aop，Middleware 从 egg\",\n      \"files\": [\n        {\n          \"path\": \"app/modules/user/UserController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum } from 'egg';\\n\\n@HTTPController({ path: '/api/users' })\\nexport class UserController {\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })\\n  async list() {\\n    return { users: [] };\\n  }\\n\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/:id' })\\n  async show() {\\n    return { id: '1', name: 'test' };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 27,\n      \"prompt\": \"帮我写一个鉴权中间件，从 header 取 token 校验，校验失败抛异常，只应用到部分接口\",\n      \"expected_output\": \"AOP @Advice 类 + around 方法中校验 token + @Middleware 应用到方法级别（而非类级别），import Advice/IAdvice/AdviceContext 从 egg/aop\"\n    },\n    {\n      \"id\": 28,\n      \"prompt\": \"中间件代码报错 AOP and MiddlewareFunc can not be mixed，帮我看看哪里写错了\",\n      \"expected_output\": \"指出不能在同一个 @Middleware() 中混用函数和 Advice 类，需要分开写两个 @Middleware\",\n      \"files\": [\n        {\n          \"path\": \"app/modules/api/ApiController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum, Middleware } from 'egg';\\nimport { LogAdvice } from '../common/advice/LogAdvice.ts';\\nimport { countMw } from './middleware/count.ts';\\n\\n@HTTPController({ path: '/api' })\\n@Middleware(LogAdvice, countMw)\\nexport class ApiController {\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/hello' })\\n  async hello() {\\n    return { message: 'hello' };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 29,\n      \"prompt\": \"我想写一个中间件统一包装接口返回值为 { success: true, data: ... } 的格式\",\n      \"expected_output\": \"AOP @Advice 类 + around 方法中 await next() 拿到返回值后包装为 { success: true, data: result }，用 @Middleware 应用\"\n    },\n    {\n      \"id\": 30,\n      \"prompt\": \"AOP 中间件和函数式中间件有什么区别？什么时候用哪种\",\n      \"expected_output\": \"AOP 为推荐写法，支持 @Inject 注入依赖；函数式为旧版兼容写法，需从 ctx 手动获取依赖。新项目优先用 AOP\"\n    },\n    {\n      \"id\": 31,\n      \"prompt\": \"帮我把这个老的 egg 函数式中间件改成 AOP 写法\",\n      \"expected_output\": \"定义 @Advice 类 + 实现 around 方法，用 @Inject 注入 Logger 替代 ctx.logger，import 从 egg/aop\",\n      \"files\": [\n        {\n          \"path\": \"app/modules/api/middleware/log.ts\",\n          \"content\": \"import type { Context, Next } from 'egg';\\n\\nexport async function logMw(ctx: Context, next: Next): Promise<void> {\\n  const start = Date.now();\\n  await next();\\n  ctx.logger.info('request %s %s cost %dms', ctx.method, ctx.url, Date.now() - start);\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 32,\n      \"prompt\": \"我在 Advice 中用了一个实例属性 count 来统计请求数，但是多个请求之间数据串了，帮我看看\",\n      \"expected_output\": \"指出 Advice 默认是 Singleton，实例属性会被并发请求共享。请求级状态应使用 ctx.set()/ctx.get() 传递\",\n      \"files\": [\n        {\n          \"path\": \"app/modules/api/advice/RequestCountAdvice.ts\",\n          \"content\": \"import { Advice, IAdvice, AdviceContext } from 'egg/aop';\\n\\n@Advice()\\nexport class RequestCountAdvice implements IAdvice {\\n  private requestId = '';\\n\\n  async around(ctx: AdviceContext, next: () => Promise<any>): Promise<any> {\\n    this.requestId = Math.random().toString(36).slice(2);\\n    const result = await next();\\n    return { ...result, requestId: this.requestId };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 33,\n      \"prompt\": \"帮我看看这个 Controller 上的中间件执行顺序是什么样的\",\n      \"expected_output\": \"函数式和 AOP 分属两个独立执行阶段：先执行所有函数式中间件（类级别 countMw → 方法级别 timeMw），再执行所有 AOP 中间件（类级别 LogAdvice → 方法级别 AuthAdvice），最后执行 hello()。即 countMw → timeMw → LogAdvice → AuthAdvice → hello()，与装饰器书写顺序无关\",\n      \"files\": [\n        {\n          \"path\": \"app/modules/api/ApiController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum, Middleware } from 'egg';\\nimport { LogAdvice } from '../common/advice/LogAdvice.ts';\\nimport { AuthAdvice } from '../common/advice/AuthAdvice.ts';\\nimport { countMw } from './middleware/count.ts';\\nimport { timeMw } from './middleware/time.ts';\\n\\n@HTTPController({ path: '/api' })\\n@Middleware(countMw)\\n@Middleware(LogAdvice)\\nexport class ApiController {\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/hello' })\\n  @Middleware(timeMw)\\n  @Middleware(AuthAdvice)\\n  async hello() {\\n    return { message: 'hello' };\\n  }\\n}\"\n        }\n      ]\n    }\n  ]\n}\n","packages/skills/eval/evals-egg-core.json":"{\n  \"skill_name\": \"egg-core\",\n  \"description\": \"核心概念评测：覆盖 module、proto、inject、dynamic-inject、background-task、eventbus、aop\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"帮我新建一个 userModule，我是新项目从零开始\",\n      \"expected_output\": \"创建 package.json(eggModule.name)，驼峰命名，放在 app/ 下，给出完整目录结构\"\n    },\n    {\n      \"id\": 2,\n      \"prompt\": \"我们是老的 egg 项目，有大量 app/controller 和 app/service 代码，现在想引入 module 开发新功能，怎么组织目录？\",\n      \"expected_output\": \"保留原有 app/controller、app/service 不动，新增 module 放在 app/module/ 下\"\n    },\n    {\n      \"id\": 3,\n      \"prompt\": \"帮我写一个 module.yml 配置，本地和线上的数据库地址不同，怎么分环境管理\",\n      \"expected_output\": \"创建 module.yml 基础配置 + module.{env}.yml 环境配置，框架自动深度合并\"\n    },\n    {\n      \"id\": 4,\n      \"prompt\": \"我新加了一个模块，启动后发现没被加载，帮我看看什么问题\",\n      \"expected_output\": \"指出 eggModule.name 包含 - 不合法，应使用驼峰命名\",\n      \"files\": [\n        {\n          \"path\": \"app/user-module/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"user-module\\\",\\n  \\\"eggModule\\\": {\\n    \\\"name\\\": \\\"user-module\\\"\\n  }\\n}\"\n        },\n        {\n          \"path\": \"app/user-module/UserService.ts\",\n          \"content\": \"import { SingletonProto } from 'egg';\\n\\n@SingletonProto()\\nexport class UserService {\\n  async getUser() { return 'hello'; }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 5,\n      \"prompt\": \"帮我写一个 UserService，查询用户信息，需要被其他模块调用\",\n      \"expected_output\": \"使用 @SingletonProto({ accessLevel: AccessLevel.PUBLIC })，包含完整代码\"\n    },\n    {\n      \"id\": 6,\n      \"prompt\": \"帮我实现一个请求级别的 TraceContext，存 traceId 和 userId，多个 Service 之间共享\",\n      \"expected_output\": \"使用 @ContextProto()，每个请求独立实例，其他 Service 通过 @Inject 共享同一个实例\"\n    },\n    {\n      \"id\": 7,\n      \"prompt\": \"我的 SingletonProto 需要在启动时连接 Redis，连接成功后才能对外提供服务，怎么写？\",\n      \"expected_output\": \"使用 @LifecycleInit() 做异步初始化，@LifecycleDestroy() 关闭连接，给出完整代码\"\n    },\n    {\n      \"id\": 8,\n      \"prompt\": \"SingletonProto 能注入 ContextProto 吗？我需要在单例服务里获取当前请求的用户信息\",\n      \"expected_output\": \"可以注入，框架通过 lazy getter/Proxy 自动处理，每次访问从当前请求上下文动态解析\"\n    },\n    {\n      \"id\": 9,\n      \"prompt\": \"启动报错 EggPrototypeNotFound，帮我看看代码哪里有问题\",\n      \"expected_output\": \"指出 OrderService 跨模块注入了 UserService 但 UserService 的 accessLevel 是 PRIVATE，应改为 PUBLIC\",\n      \"files\": [\n        {\n          \"path\": \"app/userModule/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"userModule\\\",\\n  \\\"eggModule\\\": { \\\"name\\\": \\\"userModule\\\" }\\n}\"\n        },\n        {\n          \"path\": \"app/userModule/UserService.ts\",\n          \"content\": \"import { SingletonProto } from 'egg';\\n\\n@SingletonProto()\\nexport class UserService {\\n  async findUser(id: string) {\\n    return { id, name: 'test' };\\n  }\\n}\"\n        },\n        {\n          \"path\": \"app/orderModule/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"orderModule\\\",\\n  \\\"eggModule\\\": { \\\"name\\\": \\\"orderModule\\\" }\\n}\"\n        },\n        {\n          \"path\": \"app/orderModule/OrderService.ts\",\n          \"content\": \"import { SingletonProto, Inject } from 'egg';\\nimport { UserService } from '../userModule/UserService.ts';\\n\\n@SingletonProto()\\nexport class OrderService {\\n  @Inject()\\n  userService: UserService;\\n\\n  async createOrder(userId: string) {\\n    const user = await this.userService.findUser(userId);\\n    return { orderId: '1', user };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 10,\n      \"prompt\": \"注入的对象不对，我期望注入 FooService 但拿到的不是，帮我看看\",\n      \"expected_output\": \"指出 IFooService 是 interface 无运行时元信息，回退到属性名 fooHelper 匹配，应改用 class 类型或 @Inject({ name: 'fooService' })\",\n      \"files\": [\n        {\n          \"path\": \"app/bizModule/FooService.ts\",\n          \"content\": \"import { SingletonProto } from 'egg';\\n\\nexport interface IFooService {\\n  doWork(): Promise<string>;\\n}\\n\\n@SingletonProto()\\nexport class FooService implements IFooService {\\n  async doWork() { return 'done'; }\\n}\"\n        },\n        {\n          \"path\": \"app/bizModule/BarService.ts\",\n          \"content\": \"import { SingletonProto, Inject } from 'egg';\\nimport type { IFooService } from './FooService.ts';\\n\\n@SingletonProto()\\nexport class BarService {\\n  @Inject()\\n  fooHelper: IFooService;  // 期望注入 FooService\\n\\n  async run() {\\n    return this.fooHelper.doWork();\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 11,\n      \"prompt\": \"帮我写注入 config 和 logger 的代码，还要注入自定义的 bizLogger\",\n      \"expected_output\": \"@Inject() config: EggAppConfig + @Inject() logger: Logger + @Inject() bizLogger: Logger，给出完整代码\"\n    },\n    {\n      \"id\": 12,\n      \"prompt\": \"帮我把这个老的 egg Service 改成 module 写法\",\n      \"expected_output\": \"用 @SingletonProto 重写，@Inject() 注入依赖替代 this.ctx/this.app，去掉 extends Service\",\n      \"files\": [\n        {\n          \"path\": \"app/service/OrderService.ts\",\n          \"content\": \"import { Service } from 'egg';\\n\\nexport default class OrderService extends Service {\\n  async create(data: any) {\\n    const user = await this.ctx.service.user.find(data.userId);\\n    this.ctx.logger.info('creating order for %s', user.name);\\n    const result = await this.app.httpclient.request('https://api.example.com/orders', {\\n      method: 'POST',\\n      data,\\n    });\\n    return result.data;\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 13,\n      \"prompt\": \"启动报错说注入方式冲突了，帮我看看\",\n      \"expected_output\": \"指出构造函数注入和属性注入混用了，不能混用，需改为统一使用一种方式\",\n      \"files\": [\n        {\n          \"path\": \"app/bizModule/MixedService.ts\",\n          \"content\": \"import { SingletonProto, Inject } from 'egg';\\n\\n@SingletonProto()\\nexport class MixedService {\\n  @Inject()\\n  logger: Logger;\\n\\n  constructor(\\n    @Inject() readonly config: EggAppConfig,\\n  ) {}\\n\\n  async run() {\\n    this.logger.info('config: %j', this.config);\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 14,\n      \"prompt\": \"有个依赖在某些环境不一定存在，注入时不想让应用挂掉\",\n      \"expected_output\": \"使用 @Inject({ optional: true }) 或 @InjectOptional()，不存在时为 undefined\"\n    },\n    {\n      \"id\": 15,\n      \"prompt\": \"两个模块都有 DataService，注入时报冲突了，帮我看看怎么改\",\n      \"expected_output\": \"使用 @ModuleQualifier('moduleName') 指定从哪个模块注入\",\n      \"files\": [\n        {\n          \"path\": \"app/cacheModule/DataService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class DataService {\\n  async get(key: string) { return 'from cache'; }\\n}\"\n        },\n        {\n          \"path\": \"app/dbModule/DataService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class DataService {\\n  async get(key: string) { return 'from db'; }\\n}\"\n        },\n        {\n          \"path\": \"app/bizModule/BizService.ts\",\n          \"content\": \"import { SingletonProto, Inject } from 'egg';\\n\\n@SingletonProto()\\nexport class BizService {\\n  @Inject()\\n  dataService: DataService;  // 报冲突\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 16,\n      \"prompt\": \"帮我实现策略模式，有邮件、短信、钉钉三种通知方式，运行时根据配置选择\",\n      \"expected_output\": \"使用动态注入：抽象类 AbstractNotifier + 枚举 + QualifierImplDecoratorUtil 装饰器 + EggObjectFactory.getEggObject()，给出所有文件完整代码\"\n    },\n    {\n      \"id\": 17,\n      \"prompt\": \"动态注入不生效，getEggObject 报找不到实现，帮我看看代码\",\n      \"expected_output\": \"指出 EmailNotifier 缺少 @SingletonProto() 装饰器，实现类必须同时有 Proto 装饰器和自定义装饰器\",\n      \"files\": [\n        {\n          \"path\": \"app/notifyModule/AbstractNotifier.ts\",\n          \"content\": \"export abstract class AbstractNotifier {\\n  abstract send(msg: string): Promise<void>;\\n}\"\n        },\n        {\n          \"path\": \"app/notifyModule/NotifyType.ts\",\n          \"content\": \"export enum NotifyType {\\n  EMAIL = 'EMAIL',\\n  SMS = 'SMS',\\n}\"\n        },\n        {\n          \"path\": \"app/notifyModule/decorator/Notify.ts\",\n          \"content\": \"import { ImplDecorator, QualifierImplDecoratorUtil } from 'egg';\\nimport { NotifyType } from '../NotifyType.ts';\\nimport { AbstractNotifier } from '../AbstractNotifier.ts';\\n\\nexport const NOTIFY_ATTRIBUTE = Symbol('NOTIFY_ATTRIBUTE');\\n\\nexport const Notify: ImplDecorator<AbstractNotifier, typeof NotifyType> =\\n  QualifierImplDecoratorUtil.generatorDecorator(AbstractNotifier, NOTIFY_ATTRIBUTE);\"\n        },\n        {\n          \"path\": \"app/notifyModule/impl/EmailNotifier.ts\",\n          \"content\": \"import { Notify } from '../decorator/Notify.ts';\\nimport { NotifyType } from '../NotifyType.ts';\\nimport { AbstractNotifier } from '../AbstractNotifier.ts';\\n\\n@Notify(NotifyType.EMAIL)\\nexport class EmailNotifier extends AbstractNotifier {\\n  async send(msg: string) {\\n    console.log('send email:', msg);\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 18,\n      \"prompt\": \"我想读取另一个模块的 moduleConfig，不是当前模块的\",\n      \"expected_output\": \"使用 @ConfigSourceQualifier('otherModule')，给出完整代码\"\n    },\n    {\n      \"id\": 19,\n      \"prompt\": \"请求返回后还要做一些事情，比如记日志、发通知，怎么实现\",\n      \"expected_output\": \"使用 BackgroundTaskHelper，@Inject() 注入，调用 run() 方法，从 egg 导入\"\n    },\n    {\n      \"id\": 20,\n      \"prompt\": \"用 BackgroundTaskHelper 在请求返回后异步上报埋点数据，帮我写个 MetricsService\",\n      \"expected_output\": \"@SingletonProto + @Inject() backgroundTaskHelper: BackgroundTaskHelper + run(async () => { ... })，从 egg 导入\"\n    },\n    {\n      \"id\": 21,\n      \"prompt\": \"用了 setTimeout 做后台任务，偶尔报错，帮我看看什么问题\",\n      \"expected_output\": \"指出 setTimeout 执行时上下文已释放，应改用 BackgroundTaskHelper.run()\",\n      \"files\": [\n        {\n          \"path\": \"app/orderModule/OrderService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel, Inject } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class OrderService {\\n  @Inject()\\n  private logger: any;\\n\\n  async createOrder(data: any) {\\n    const order = { id: Date.now(), ...data };\\n    // 请求返回后异步记录日志\\n    setTimeout(async () => {\\n      this.logger.info('Order created:', order.id);\\n      await this.sendNotification(order);\\n    }, 0);\\n    return order;\\n  }\\n\\n  private async sendNotification(order: any) {\\n    // 发送通知\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 22,\n      \"prompt\": \"后台任务默认超时是多少？我有个任务比较慢，怎么改成更长的时间\",\n      \"expected_output\": \"默认 5 秒，可通过 backgroundTaskHelper.timeout 按请求设置，或在 config.default.ts 中 backgroundTask.timeout 全局配置\"\n    },\n    {\n      \"id\": 23,\n      \"prompt\": \"帮我写一个订单创建接口，创建成功后异步发送通知邮件，不影响接口响应速度\",\n      \"expected_output\": \"OrderService 中 @Inject() backgroundTaskHelper，在 createOrder 方法中调用 run() 异步发送邮件\"\n    },\n    {\n      \"id\": 24,\n      \"prompt\": \"帮我把这个用 setTimeout 的后台逻辑改成正确的写法\",\n      \"expected_output\": \"将 setTimeout 替换为 backgroundTaskHelper.run()，注入 BackgroundTaskHelper\",\n      \"files\": [\n        {\n          \"path\": \"app/cacheModule/CacheService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel, Inject } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class CacheService {\\n  @Inject()\\n  private redisService: any;\\n\\n  async getData(key: string) {\\n    const data = await this.redisService.get(key);\\n    if (!data) {\\n      const fresh = await this.fetchFromDB(key);\\n      // 异步回写缓存，不阻塞响应\\n      setTimeout(async () => {\\n        await this.redisService.set(key, fresh, 3600);\\n      }, 0);\\n      return fresh;\\n    }\\n    return data;\\n  }\\n\\n  private async fetchFromDB(key: string) {\\n    return { key, value: 'from-db' };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 25,\n      \"prompt\": \"后台任务里抛了异常，但接口没报错也没看到日志，怎么回事\",\n      \"expected_output\": \"BackgroundTaskHelper 内部捕获所有异常并通过 logger.error 记录，日志前缀为 [BackgroundTaskHelper]，检查日志配置是否正确\",\n      \"files\": [\n        {\n          \"path\": \"app/reportModule/ReportService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel, Inject, BackgroundTaskHelper } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class ReportService {\\n  @Inject()\\n  private backgroundTaskHelper: BackgroundTaskHelper;\\n\\n  async generateReport(data: any) {\\n    this.backgroundTaskHelper.run(async () => {\\n      // 这里可能会抛异常\\n      const result = await this.callExternalAPI(data);\\n      await this.saveReport(result);\\n    });\\n    return { status: 'processing' };\\n  }\\n\\n  private async callExternalAPI(data: any) {\\n    throw new Error('API timeout');\\n  }\\n\\n  private async saveReport(result: any) {}\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 26,\n      \"prompt\": \"我有个后台任务可能要跑 30 秒，怎么配置超时不被框架提前中断\",\n      \"expected_output\": \"设置 backgroundTaskHelper.timeout = 30000 或全局 config backgroundTask.timeout: 30000\"\n    },\n    {\n      \"id\": 27,\n      \"prompt\": \"订单创建后要发通知、记日志、更新统计，但不想耦合在一起，怎么做\",\n      \"expected_output\": \"使用 EventBus 解耦：声明 Events 接口 + emit 触发事件 + 多个 @Event handler 分别处理通知、日志、统计\"\n    },\n    {\n      \"id\": 28,\n      \"prompt\": \"用 EventBus 实现订单事件，要求类型安全，帮我写 Events 声明和 handler\",\n      \"expected_output\": \"declare module 'egg' { interface Events { ... } } + @Event handler + 从 egg 导入 EventBus/Event，事件声明文件开头有 import 'egg'\"\n    },\n    {\n      \"id\": 29,\n      \"prompt\": \"EventBus 和 BackgroundTaskHelper 什么区别，我该用哪个\",\n      \"expected_output\": \"对比耦合度（强耦合 vs 解耦）、上下文（共享 vs 独立）、扩展性（改触发者 vs 加 handler），给出选型建议\"\n    },\n    {\n      \"id\": 30,\n      \"prompt\": \"EventBus 的 handler 没有被触发，帮我看看\",\n      \"expected_output\": \"指出 handler 文件不在 module 目录中，框架无法扫描到，应移到模块目录下\",\n      \"files\": [\n        {\n          \"path\": \"app/orderModule/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"orderModule\\\",\\n  \\\"eggModule\\\": { \\\"name\\\": \\\"orderModule\\\" }\\n}\"\n        },\n        {\n          \"path\": \"app/orderModule/OrderService.ts\",\n          \"content\": \"import { SingletonProto, Inject, EventBus } from 'egg';\\n\\n@SingletonProto()\\nexport class OrderService {\\n  @Inject()\\n  private eventBus: EventBus;\\n\\n  async createOrder(userId: string) {\\n    this.eventBus.emit('orderCreated', '123', userId);\\n    return { orderId: '123' };\\n  }\\n}\"\n        },\n        {\n          \"path\": \"app/handler/OrderNotificationHandler.ts\",\n          \"content\": \"import { Event } from 'egg';\\n\\n@Event('orderCreated')\\nexport class OrderNotificationHandler {\\n  async handle(orderId: string, userId: string): Promise<void> {\\n    console.log('notify:', orderId, userId);\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 31,\n      \"prompt\": \"帮我用 EventBus 实现用户注册后异步发送欢迎邮件\",\n      \"expected_output\": \"完整代码：Events 类型声明（import 'egg' + declare module）+ 注册 Service 中 emit + @Event handler 发送邮件\"\n    },\n    {\n      \"id\": 32,\n      \"prompt\": \"帮我把这个 BackgroundTaskHelper 的写法改成 EventBus 解耦\",\n      \"expected_output\": \"拆分为 EventBus.emit() + 独立 @Event handler 类，去掉 BackgroundTaskHelper\",\n      \"files\": [\n        {\n          \"path\": \"app/orderModule/OrderService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel, Inject, BackgroundTaskHelper } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class OrderService {\\n  @Inject()\\n  private backgroundTaskHelper: BackgroundTaskHelper;\\n\\n  @Inject()\\n  private notifyService: any;\\n\\n  @Inject()\\n  private statsService: any;\\n\\n  async createOrder(data: any) {\\n    const order = { id: Date.now(), ...data };\\n    this.backgroundTaskHelper.run(async () => {\\n      await this.notifyService.send(order);\\n      await this.statsService.record(order);\\n    });\\n    return order;\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 33,\n      \"prompt\": \"EventBus 的 cork/uncork 怎么用，我想批量操作完成后再统一触发事件\",\n      \"expected_output\": \"注入 ContextEventBus，cork() 开始缓冲 → emit 多个事件 → uncork() 释放，支持嵌套调用\"\n    },\n    {\n      \"id\": 34,\n      \"prompt\": \"所有 Service 的方法调用都要记录耗时日志，不想在每个方法里写\",\n      \"expected_output\": \"使用 AOP @Crosscut + PointcutType.NAME 正则匹配 Service，Advice 中 beforeCall 记录开始时间，afterFinally 计算耗时并打日志\"\n    },\n    {\n      \"id\": 35,\n      \"prompt\": \"用 @Pointcut 给 OrderService.createOrder 加事务包裹\",\n      \"expected_output\": \"@Advice 实现 around hook 管理事务（beginTransaction/commit/rollback）+ @Pointcut(TxAdvice) 装饰目标方法，从 egg/aop 导入\"\n    },\n    {\n      \"id\": 36,\n      \"prompt\": \"Pointcut 和 Crosscut 有什么区别，怎么选\",\n      \"expected_output\": \"Pointcut 在目标方法上声明，精确切入单个方法；Crosscut 在 Advice 类上声明匹配规则，批量切入多个类/方法\"\n    },\n    {\n      \"id\": 37,\n      \"prompt\": \"AOP 的 Advice 不生效，帮我看看\",\n      \"expected_output\": \"指出缺少 @Advice() 装饰器，@Crosscut 和 @Advice() 必须同时使用\",\n      \"files\": [\n        {\n          \"path\": \"app/logModule/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"logModule\\\",\\n  \\\"eggModule\\\": { \\\"name\\\": \\\"logModule\\\" }\\n}\"\n        },\n        {\n          \"path\": \"app/logModule/OperationLogAdvice.ts\",\n          \"content\": \"import { Crosscut, IAdvice, AdviceContext, PointcutType } from 'egg/aop';\\nimport { Inject, Logger } from 'egg';\\n\\n@Crosscut({\\n  type: PointcutType.NAME,\\n  className: /.*Service$/i,\\n  methodName: /^(create|update|delete)/,\\n})\\nexport class OperationLogAdvice implements IAdvice {\\n  @Inject()\\n  private logger: Logger;\\n\\n  async afterFinally(ctx: AdviceContext): Promise<void> {\\n    this.logger.info('called %s', String(ctx.method));\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 38,\n      \"prompt\": \"帮我用 AOP 实现统一的异常捕获和错误日志\",\n      \"expected_output\": \"Advice 实现 afterThrow hook 记录异常信息，使用 @Crosscut 或 @Pointcut 应用到目标方法，从 egg/aop 导入\"\n    },\n    {\n      \"id\": 39,\n      \"prompt\": \"adviceParams 怎么用，同一个 Advice 不同方法需要不同参数\",\n      \"expected_output\": \"@Pointcut 第二个参数传 { adviceParams: {...} }，Advice 中通过 ctx.adviceParams 获取\"\n    },\n    {\n      \"id\": 40,\n      \"prompt\": \"同一个方法上有 Crosscut 和 Pointcut 两个 Advice，执行顺序是什么，能控制吗\",\n      \"expected_output\": \"Crosscut 默认 order 100 先执行，Pointcut 默认 order 1000 后执行，可通过 order 参数调整顺序\"\n    },\n    {\n      \"id\": 41,\n      \"prompt\": \"帮我用 Crosscut 的 CUSTOM 模式，对所有 Repository 类的非 constructor 方法加监控\",\n      \"expected_output\": \"使用 PointcutType.CUSTOM + callback 函数匹配 Repository 类并排除 constructor，从 egg/aop 导入\"\n    }\n  ]\n}\n","packages/skills/eval/evals-egg-unittest.json":"{\n  \"skill_name\": \"egg-unittest\",\n  \"description\": \"单元测试评测：覆盖 http-test、service-test、mock、background-task-test、eventbus-test\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"帮我给这个 UserController 写个单元测试，要测 GET 和 POST 接口\",\n      \"expected_output\": \"import { app } from '@eggjs/mock/bootstrap'，app.httpRequest() 测 GET 和 POST，POST 前调用 app.mockCsrf()，return 或 await httpRequest\",\n      \"files\": [\n        {\n          \"path\": \"app/modules/user/UserController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPParam, HTTPBody, Inject } from 'egg';\\nimport { UserService } from './UserService.ts';\\n\\n@HTTPController({ path: '/api/users' })\\nexport class UserController {\\n  @Inject()\\n  userService: UserService;\\n\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/:id' })\\n  async show(@HTTPParam() id: string) {\\n    return await this.userService.getById(id);\\n  }\\n\\n  @HTTPMethod({ method: HTTPMethodEnum.POST, path: '/' })\\n  async create(@HTTPBody() body: { name: string; email: string }) {\\n    return await this.userService.create(body);\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 2,\n      \"prompt\": \"POST 接口测试报 403 错误，帮我看看\",\n      \"expected_output\": \"指出需要调用 app.mockCsrf() 跳过 CSRF 校验\",\n      \"files\": [\n        {\n          \"path\": \"test/controller/user.test.ts\",\n          \"content\": \"import { app } from '@eggjs/mock/bootstrap';\\n\\ndescribe('UserController', () => {\\n  it('should create user', () => {\\n    return app.httpRequest()\\n      .post('/api/users')\\n      .send({ name: 'test', email: 'test@example.com' })\\n      .expect(200);\\n  });\\n});\"\n        }\n      ]\n    },\n    {\n      \"id\": 3,\n      \"prompt\": \"httpRequest 的 expect 有哪些用法\",\n      \"expected_output\": \"expect(status)、expect(body)、expect(status, body) 合并、expect('header', value)、expect(/regex/)、expect([200, 302]) 多状态码、expect(fn) 自定义断言\"\n    },\n    {\n      \"id\": 4,\n      \"prompt\": \"测试写了但永远通过，不管接口返回啥都不报错\",\n      \"expected_output\": \"httpRequest 必须 return 或 await，否则断言不会执行\",\n      \"files\": [\n        {\n          \"path\": \"test/controller/home.test.ts\",\n          \"content\": \"import { app } from '@eggjs/mock/bootstrap';\\n\\ndescribe('HomeController', () => {\\n  it('should return 200', () => {\\n    app.httpRequest()\\n      .get('/')\\n      .expect(200)\\n      .expect('hello');\\n  });\\n});\"\n        }\n      ]\n    },\n    {\n      \"id\": 5,\n      \"prompt\": \"帮我写一个完整的 CRUD 接口测试\",\n      \"expected_output\": \"GET/POST/PUT/DELETE 完整测试，POST/PUT/DELETE 都要 mockCsrf，import 从 @eggjs/mock/bootstrap\"\n    },\n    {\n      \"id\": 6,\n      \"prompt\": \"基于这个 OrderService 帮我写完整的单元测试\",\n      \"expected_output\": \"app.getEggObject(OrderService) 获取实例 + mm mock PaymentService 依赖 + 断言返回值\",\n      \"files\": [\n        {\n          \"path\": \"app/modules/order/OrderService.ts\",\n          \"content\": \"import { SingletonProto, Inject } from 'egg';\\nimport { PaymentService } from '../payment/PaymentService.ts';\\n\\n@SingletonProto()\\nexport class OrderService {\\n  @Inject()\\n  paymentService: PaymentService;\\n\\n  async create(data: { productId: string; amount: number }) {\\n    const tx = await this.paymentService.charge(data.amount);\\n    return { orderId: 'order-1', transactionId: tx.id };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 7,\n      \"prompt\": \"ContextProto 的 Service 怎么在测试里获取实例\",\n      \"expected_output\": \"app.getEggObject(ContextProtoClass) 可以直接获取，也可以在 app.mockModuleContextScope 回调内用 ctx.getEggObject 获取（后者提供上下文生命周期管理，退出时自动销毁）\"\n    },\n    {\n      \"id\": 8,\n      \"prompt\": \"Singleton 和 ContextProto 的测试方式有什么区别\",\n      \"expected_output\": \"两者都可以用 app.getEggObject 直接获取。ContextProto 还可以在 app.mockModuleContextScope 回调内用 ctx.getEggObject 获取，提供上下文生命周期管理（退出时自动销毁、自动等待后台任务）\"\n    },\n    {\n      \"id\": 9,\n      \"prompt\": \"怎么测试一个依赖其他 Service 的 Service\",\n      \"expected_output\": \"mm(DependencyClass.prototype, 'method', fn) mock 依赖的原型方法，再 getEggObject 获取被测 Service\"\n    },\n    {\n      \"id\": 10,\n      \"prompt\": \"mockModuleContextScope 回调里的断言通过了但实际上没执行，测试直接过了\",\n      \"expected_output\": \"mockModuleContextScope 的回调必须 await，否则 scope 提前退出\",\n      \"files\": [\n        {\n          \"path\": \"test/service/user.test.ts\",\n          \"content\": \"import { app } from '@eggjs/mock/bootstrap';\\nimport assert from 'node:assert';\\nimport { UserService } from '../app/modules/user/UserService.ts';\\n\\ndescribe('UserService', () => {\\n  it('should get user', () => {\\n    app.mockModuleContextScope(async (ctx) => {\\n      const userService = await ctx.getEggObject(UserService);\\n      const user = await userService.getById('1');\\n      assert.equal(user.name, 'test');\\n    });\\n  });\\n});\"\n        }\n      ]\n    },\n    {\n      \"id\": 11,\n      \"prompt\": \"怎么 mock 一个被注入的 Service 的方法\",\n      \"expected_output\": \"mm(ServiceClass.prototype, 'methodName', async () => { ... })，import mm 从 @eggjs/mock/bootstrap，mock 原型而非实例\"\n    },\n    {\n      \"id\": 12,\n      \"prompt\": \"mm mock 了但没生效，帮我看看\",\n      \"expected_output\": \"mock 的是实例而非原型，应该用 mm(ServiceClass.prototype, ...)\",\n      \"files\": [\n        {\n          \"path\": \"test/service/order.test.ts\",\n          \"content\": \"import { app, mm } from '@eggjs/mock/bootstrap';\\nimport assert from 'node:assert';\\nimport { PaymentService } from '../app/modules/payment/PaymentService.ts';\\nimport { OrderService } from '../app/modules/order/OrderService.ts';\\n\\ndescribe('OrderService', () => {\\n  it('should mock payment', async () => {\\n    const paymentService = await app.getEggObject(PaymentService);\\n    mm(paymentService, 'charge', async () => ({ id: 'tx-mock' }));\\n\\n    const orderService = await app.getEggObject(OrderService);\\n    const order = await orderService.create({ productId: '1', amount: 100 });\\n    assert.equal(order.transactionId, 'tx-mock');\\n  });\\n});\"\n        }\n      ]\n    },\n    {\n      \"id\": 13,\n      \"prompt\": \"怎么验证 mock 的方法被调用了几次、传了什么参数\",\n      \"expected_output\": \"mockFn.called 调用次数，mockFn.lastCalledArguments 最后一次参数，mockFn.calledArguments 所有参数\"\n    },\n    {\n      \"id\": 14,\n      \"prompt\": \"怎么 mock 外部 HTTP 请求，我的 Service 里用了 httpclient 调第三方 API\",\n      \"expected_output\": \"app.mockHttpclient(url, { data: ... })，mock 通过 @Inject 注入的 HttpClient 发送的请求\"\n    },\n    {\n      \"id\": 15,\n      \"prompt\": \"mm 和 mm.spy 有什么区别\",\n      \"expected_output\": \"mm 替换方法实现，spy 保留原实现不替换，两者都会记录调用信息（called、lastCalledArguments）\"\n    },\n    {\n      \"id\": 16,\n      \"prompt\": \"怎么 mock 一个方法让它抛出异常\",\n      \"expected_output\": \"mm.error(Class.prototype, 'method', new Error('...'))\"\n    },\n    {\n      \"id\": 17,\n      \"prompt\": \"怎么测试后台任务是否执行完成\",\n      \"expected_output\": \"两种方式：1. mockModuleContextScope 退出时自动等待（内部触发 doPreDestroy） 2. app.backgroundTasksFinished() 手动等待。不应该用 sleep\"\n    },\n    {\n      \"id\": 18,\n      \"prompt\": \"用 httpRequest 触发的后台任务怎么等它完成\",\n      \"expected_output\": \"await app.backgroundTasksFinished()\"\n    },\n    {\n      \"id\": 19,\n      \"prompt\": \"测试里用了 TimerUtil.sleep 等后台任务，CI 上经常超时不稳定\",\n      \"expected_output\": \"用 app.backgroundTasksFinished() 替代 sleep，精确等待任务完成\",\n      \"files\": [\n        {\n          \"path\": \"test/service/task.test.ts\",\n          \"content\": \"import { app } from '@eggjs/mock/bootstrap';\\nimport assert from 'node:assert';\\nimport { TimerUtil } from '@eggjs/tegg-common-util';\\nimport { CountService } from '../app/modules/count/CountService.ts';\\n\\nit('should complete task', async () => {\\n  await app.httpRequest().get('/api/trigger-task').expect(200);\\n  await TimerUtil.sleep(3000);\\n  const countService = await app.getEggObject(CountService);\\n  assert.equal(countService.count, 1);\\n});\"\n        }\n      ]\n    },\n    {\n      \"id\": 20,\n      \"prompt\": \"backgroundTasksFinished 调了但断言还是失败，count 一直是 0\",\n      \"expected_output\": \"检查用于验证的 Service 是否是 @SingletonProto，ContextProto 每次 getEggObject 拿到的是不同实例\"\n    },\n    {\n      \"id\": 21,\n      \"prompt\": \"怎么测试 EventBus 事件是否被正确处理\",\n      \"expected_output\": \"app.getEventWaiter() 获取 waiter，eventWaiter.await('eventName') 必须在 emit 之前注册，用 mm mock handler 验证调用\"\n    },\n    {\n      \"id\": 22,\n      \"prompt\": \"eventWaiter.await 一直挂着不返回，测试超时了\",\n      \"expected_output\": \"eventWaiter.await 必须在 emit 之前调用，先注册等待再触发业务逻辑\",\n      \"files\": [\n        {\n          \"path\": \"test/event/hello.test.ts\",\n          \"content\": \"import { app } from '@eggjs/mock/bootstrap';\\nimport { HelloService } from '../app/modules/hello/HelloService.ts';\\n\\nit('should handle event', async () => {\\n  await app.mockModuleContextScope(async (ctx) => {\\n    const helloService = await ctx.getEggObject(HelloService);\\n    helloService.hello();\\n\\n    const eventWaiter = await app.getEventWaiter();\\n    await eventWaiter.await('helloEgg');\\n  });\\n});\"\n        }\n      ]\n    },\n    {\n      \"id\": 23,\n      \"prompt\": \"怎么验证 EventBus handler 收到的参数是否正确\",\n      \"expected_output\": \"mm mock handler 原型方法 + mockFn.lastCalledArguments 断言参数\"\n    },\n    {\n      \"id\": 24,\n      \"prompt\": \"帮我写一个 EventBus 事件的完整测试\",\n      \"expected_output\": \"完整示例：import bootstrap + mockModuleContextScope + getEventWaiter + await 在 emit 前 + mm mock handler + 断言\"\n    }\n  ]\n}\n","packages/skills/eval/evals-routing.json":"{\n  \"skill_name\": \"egg\",\n  \"description\": \"入口 skill 路由决策评测\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"帮我写一个接口\",\n      \"expected_output\": \"路由到 egg-controller skill\"\n    },\n    {\n      \"id\": 2,\n      \"prompt\": \"帮我写一个 GET /api/users/:id 的接口，返回用户信息\",\n      \"expected_output\": \"路由到 egg-controller skill\"\n    },\n    {\n      \"id\": 3,\n      \"prompt\": \"我想写个服务，不知道用 SingletonProto 还是 ContextProto\",\n      \"expected_output\": \"路由到 egg-core skill\"\n    },\n    {\n      \"id\": 4,\n      \"prompt\": \"帮我实现一个定时同步数据的功能，每 10 分钟跑一次\",\n      \"expected_output\": \"路由到 egg-controller skill\"\n    },\n    {\n      \"id\": 5,\n      \"prompt\": \"我的 @Inject 注入报错了，找不到对象\",\n      \"expected_output\": \"路由到 egg-core skill\"\n    },\n    {\n      \"id\": 6,\n      \"prompt\": \"帮我搭一个新模块，包含增删改查接口和 Service\",\n      \"expected_output\": \"路由到 egg-core skill（基础优先，先建模块再写接口）\"\n    },\n    {\n      \"id\": 7,\n      \"prompt\": \"我要给 AI 暴露几个 MCP Tool，查询订单和退款\",\n      \"expected_output\": \"路由到 egg-controller skill\"\n    },\n    {\n      \"id\": 8,\n      \"prompt\": \"不同环境的模块配置怎么管理\",\n      \"expected_output\": \"路由到 egg-core skill\"\n    },\n    {\n      \"id\": 9,\n      \"prompt\": \"帮我用事件总线解耦订单通知\",\n      \"expected_output\": \"路由到 egg-core skill\"\n    },\n    {\n      \"id\": 10,\n      \"prompt\": \"EventBus 和 BackgroundTaskHelper 怎么选\",\n      \"expected_output\": \"路由到 egg-core skill\"\n    },\n    {\n      \"id\": 11,\n      \"prompt\": \"帮我用 AOP 给所有 Service 加日志\",\n      \"expected_output\": \"路由到 egg-core skill\"\n    }\n  ]\n}\n","packages/skills/package.json":"{\n  \"name\": \"@eggjs/skills\",\n  \"version\": \"4.2.0\",\n  \"description\": \"agent skills for egg\",\n  \"keywords\": [\n    \"egg\",\n    \"skill\"\n  ],\n  \"homepage\": \"https://github.com/eggjs/egg/tree/next/packages/skills\",\n  \"bugs\": {\n    \"url\": \"https://github.com/eggjs/egg/issues\"\n  },\n  \"license\": \"MIT\",\n  \"author\": \"eggjs\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/eggjs/egg.git\",\n    \"directory\": \"packages/skills\"\n  },\n  \"files\": [\n    \"egg-controller/**/*.md\",\n    \"egg-core/**/*.md\",\n    \"egg-unittest/**/*.md\",\n    \"egg/**/*.md\"\n  ],\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"engines\": {\n    \"node\": \">=22.18.0\"\n  }\n}\n","packages/supertest/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fsupertest%2FREADME.md -->","packages/tsconfig/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Ftsconfig%2FREADME.md -->","packages/utils/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Futils%2FREADME.md -->","plugins/cors/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fcors%2FREADME.md -->","plugins/development/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fdevelopment%2FREADME.md -->","plugins/jsonp/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fjsonp%2FREADME.md -->","plugins/logrotator/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Flogrotator%2FREADME.md -->","plugins/mock/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fmock%2FREADME.md -->","plugins/multipart/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fmultipart%2FREADME.md -->","plugins/onerror/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fonerror%2FREADME.md -->","plugins/redis/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fredis%2FREADME.md -->","plugins/schedule/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fschedule%2FREADME.md -->","plugins/security/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fsecurity%2FREADME.md -->","plugins/session/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fsession%2FREADME.md -->","plugins/static/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fstatic%2FREADME.md -->","plugins/status/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fstatus%2FREADME.md -->","plugins/tracer/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Ftracer%2FREADME.md -->","plugins/typebox-validate/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Ftypebox-validate%2FREADME.md -->","plugins/view-nunjucks/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fview-nunjucks%2FREADME.md -->","plugins/view/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fview%2FREADME.md -->","plugins/watcher/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fwatcher%2FREADME.md -->","tegg/AGENTS.md":"# AGENTS.md\n\nFollow the shared repository guidance in [the root AGENTS.md](../AGENTS.md).\nThis file adds guidance for working in `tegg/`.\n\n## Overview\n\nTegg is a modular IoC (Inversion of Control) framework for Egg.js, providing dependency injection, lifecycle management, and plugin architecture. It's designed for building large-scale, maintainable Node.js applications using TypeScript decorators.\n\n**Requirements:**\n\n- Node.js >= 22.18.0\n- ESM only (no CommonJS)\n- egg >= 4.1.0\n\n## Monorepo Structure\n\n**IMPORTANT:** Tegg is part of the main [Egg.js monorepo](https://github.com/eggjs/egg). All build, test, and version management commands should be run from the monorepo root.\n\nThe tegg packages are organized as follows within the main monorepo:\n\n```\ncore/          # 24 core packages - decorators, runtime, metadata, loaders\nplugin/        # 10 plugin packages - Egg.js plugins that integrate core functionality\nstandalone/    # 1 standalone package - standalone runtime without Egg.js\n```\n\n**Dependency Management:**\n\n- Uses utoo workspaces with `catalog:` protocol for shared external dependencies\n- Uses `workspace:*` protocol for internal monorepo dependencies (both tegg and egg packages)\n- All shared dependency versions centralized in the root `.utoo.toml` (not in tegg/)\n- `catalogMode: prefer` set in root `.npmrc` for automatic catalog usage\n- Tegg workspace patterns are defined in root package.json as `tegg/core/*`, `tegg/plugin/*`, `tegg/standalone/*`\n\n### Key Core Packages\n\n- **core-decorator**: Basic decorators (`@Inject`, `@ContextProto`, `@SingletonProto`)\n- **metadata**: Metadata management for prototypes, modules, and dependency graphs\n- **runtime**: Runtime container and object lifecycle management\n- **loader**: Module discovery and loading system\n- **lifecycle**: Lifecycle hooks and management\n- **types**: TypeScript type definitions\n- **common-util**: Shared utilities\n\n### Key Plugin Packages\n\n- **plugin/tegg**: Main Egg.js plugin integrating tegg runtime\n- **plugin/config**: Module configuration support\n- **plugin/controller**: HTTP controller decorator support\n- **plugin/aop**: AOP runtime integration\n- **plugin/eventbus**: Event bus system\n- **plugin/schedule**: Scheduled task support\n- **plugin/dal**: Data access layer\n- **plugin/orm**: Leoric ORM integration\n\n## Development Commands\n\n**Note:** All commands below should be run from the **monorepo root** (`../egg`), not from the tegg directory.\n\n### Build & Clean\n\n```bash\nut run build               # Build all packages including tegg (runs build in all workspaces)\nut run clean-dist          # Clean all build artifacts including tegg (removes dist, tsbuildinfo)\n```\n\n### Testing\n\nAll tegg packages use **Vitest** for testing and are integrated with the main Egg.js monorepo test suite.\n\n```bash\nut run test                # Run vitest tests for all packages (from monorepo root)\nut run test:cov            # Run tests with coverage\nut run ci                  # Full CI: vitest with coverage and bail on first failure\n```\n\nRun tegg tests from clean sources, not immediately after `pnpm run build`. If\nlocal tests fail with `duplicate proto` after a build, remove stale `dist/`\ndirectories outside fixtures as described in the root `AGENTS.md` Local CI\nsection, then re-run tests.\n\n**Note:** Tests are configured in the monorepo root `vitest.config.ts` which includes all tegg packages (`tegg/core/*`, `tegg/plugin/*`, `tegg/standalone/*`).\n\n### Type Checking & Linting\n\n```bash\nut run typecheck           # Clean and type check all workspaces (including tegg)\nut run lint                # Run oxlint with type-aware checking on all packages\nut run fmtcheck            # Check code formatting with oxfmt\n```\n\n**Note:** oxlint automatically runs with `--type-aware` flag for enhanced TypeScript checking.\n\n### Version Management\n\n**Note:** Run these commands from the monorepo root (`../egg`).\n\n```bash\nut run version:patch       # Bump patch version (0.0.X)\nut run version:minor       # Bump minor version (0.X.0)\nut run version:major       # Bump major version (X.0.0)\nut run version:prepatch    # Bump to next prerelease patch version\nut run version:preminor    # Bump to next prerelease minor version\nut run version:premajor    # Bump to next prerelease major version\nut run version:alpha       # Bump prerelease alpha version\nut run version:beta        # Bump prerelease beta version\nut run version:rc          # Bump prerelease rc version\n```\n\n### Working with Individual Packages\n\n**Note:** Run from the monorepo root to work with individual tegg packages.\n\n```bash\n# Install dependencies\nut install              # Install all dependencies from native utoo configuration\n\n# Type check specific packages\nut run typecheck                    # Type check all packages\nut --filter @eggjs/tegg-runtime run typecheck\n\n# Build specific packages\nut --filter @eggjs/metadata run build\nut --filter @eggjs/tegg-runtime run build\n\n# Clean specific package\nnode -e \"require('node:fs').rmSync('tegg/core/runtime/dist', { recursive: true, force: true })\"\n```\n\n**Note:** Individual tegg packages don't have test scripts in their package.json. Tests are run via the monorepo root vitest configuration.\n\n## Architecture Concepts\n\n### Prototype System\n\nTegg uses a prototype-based system where classes are decorated to define how they should be instantiated:\n\n- **ContextProto**: Instance per request context (scoped to HTTP request)\n- **SingletonProto**: Single instance for entire application lifecycle\n- **MultiInstanceProto**: Multiple instances of same class with different qualifiers\n\nEach prototype has:\n\n- **AccessLevel**: `PRIVATE` (module-only) or `PUBLIC` (globally accessible)\n- **InitType**: Defines lifecycle scope (`CONTEXT`, `SINGLETON`)\n- **Name**: Instance identifier (defaults to camelCase class name)\n\n### Dependency Injection\n\nDependencies are resolved through `@Inject()` decorator:\n\n- Property injection: `@Inject() logger: Logger`\n- Constructor injection: `constructor(@Inject() logger: Logger)`\n- Optional injection: `@InjectOptional()` or `@Inject({ optional: true })`\n\n**Injection Rules:**\n\n- ContextProto can inject any prototype\n- SingletonProto cannot inject ContextProto\n- No circular dependencies allowed (between prototypes or modules)\n- Cannot inject `ctx`/`app` directly - inject specific services instead\n\n### Qualifiers\n\nWhen multiple implementations exist, use qualifiers to disambiguate:\n\n- `@InitTypeQualifier(ObjectInitType.CONTEXT)`: Specify init type\n- `@ModuleQualifier('moduleName')`: Specify source module\n- `@EggQualifier(EggType.CONTEXT)`: Specify egg context vs app\n- Custom qualifiers for dynamic injection patterns\n\n### Module System\n\nModules are organizational units discovered by scanning:\n\n- `app/modules/` directory (auto-discovered)\n- `config/module.json` (manual declaration for npm packages)\n\nEach module contains:\n\n- Prototype classes with decorators\n- Optional `module.json` or `package.json` with tegg metadata\n- Module-level dependencies on other modules\n\nThe **GlobalGraph** builds a dependency graph of all modules and validates:\n\n- No circular module dependencies\n- All prototype dependencies are resolvable\n- Access level constraints are respected\n\n### Lifecycle Hooks\n\nObjects can implement `EggObjectLifecycle` interface or use decorators:\n\n- `@LifecyclePostConstruct()`: After constructor\n- `@LifecyclePreInject()`: Before dependency injection\n- `@LifecyclePostInject()`: After dependency injection\n- `@LifecycleInit()`: Custom async initialization\n- `@LifecyclePreDestroy()`: Before object destruction\n- `@LifecycleDestroy()`: Resource cleanup\n\n### Runtime Object Management\n\nThe runtime manages object instances through:\n\n- **EggObjectFactory**: Creates and retrieves object instances\n- **LoadUnitInstance**: Manages module instances and their objects\n- **EggContext**: Request-scoped context holding ContextProto instances\n- **ContextObjectGraph**: Dependency graph for a specific context\n\n## Testing Patterns\n\n### Testing with MockApplication\n\n```typescript\nimport { MockApplication } from '@eggjs/mock';\n\n// Create context scope\nawait app.mockModuleContextScope(async (ctx: Context) => {\n  // Get object by class\n  const service = await ctx.getEggObject(HelloService);\n\n  // Get object by name with qualifiers\n  const logger = await ctx.getEggObjectFromName('logger', {\n    qualifier: 'bizLogger',\n  });\n});\n```\n\n### Async Tasks in Tests\n\nUse `BackgroundTaskHelper` instead of `setTimeout`/`setImmediate`:\n\n```typescript\n@ContextProto()\nclass MyService {\n  @Inject()\n  backgroundTaskHelper: BackgroundTaskHelper;\n\n  async doWork() {\n    this.backgroundTaskHelper.run(async () => {\n      // Async work here\n    });\n  }\n}\n```\n\n## Important Implementation Details\n\n### Metadata Registration\n\nDecorators register metadata on classes that is later used by the loader:\n\n- Prototype metadata: `PrototypeUtil.setXXX()` stores on class\n- Injection metadata: `InjectObjectInfo` stored per property/parameter\n- Qualifier metadata: `QualifierUtil.addProperQualifier()` for disambiguation\n\n### Loading Process\n\n1. **Loader** scans directories and discovers modules\n2. **EggPrototypeFactory** creates `EggPrototype` from decorated classes\n3. **GlobalGraph** validates and builds dependency graph\n4. **LoadUnitFactory** creates `LoadUnit` for each module\n5. **Runtime** instantiates objects on-demand based on graph\n\n### Dynamic Injection\n\nFor selecting implementations at runtime:\n\n```typescript\n// Define abstract class and enum\nabstract class AbstractHello { abstract hello(): string; }\nenum HelloType { FOO = 'FOO', BAR = 'BAR' }\n\n// Create decorator\nconst Hello = QualifierImplDecoratorUtil.generatorDecorator(\n  AbstractHello,\n  'HELLO_ATTRIBUTE'\n);\n\n// Apply to implementations\n@ContextProto()\n@Hello(HelloType.FOO)\nclass FooHello extends AbstractHello { ... }\n\n// Get instance dynamically\nconst impl = await eggObjectFactory.getEggObject(\n  AbstractHello,\n  HelloType.FOO\n);\n```\n\n## Multi-App Isolation (TeggScope) — MUST follow\n\nTegg supports multiple apps booting and serving requests **concurrently in one\nprocess** without cross-talk. This is built on `TeggScope`\n(`@eggjs/tegg-types`), a type-free `AsyncLocalStorage<Map<symbol, unknown>>`.\nEach app owns a per-app \"bag\" (`app._teggScopeBag`); per-app state lives in\nslots inside that bag, and the active bag is established with\n`TeggScope.run(app._teggScopeBag, ...)`. Per-app singletons resolve via the same\nold static call sites (e.g. `EggPrototypeFactory.instance`) — they now read the\ncurrent scope instead of a process global.\n\nWhen you touch tegg core/plugins, follow these rules:\n\n1. **Never add new process-global mutable runtime state.** A `static` field /\n   `Map` / singleton that holds per-app data WILL leak across concurrent apps.\n   If you need such state, back it with a `TeggScope` slot:\n\n   ```ts\n   import { TeggScope } from '@eggjs/tegg-types';\n   const X_SLOT = Symbol('tegg:<pkg>:<name>'); // module-private, never exported\n   export class X {\n     static get instance(): X {\n       return TeggScope.resolve(X_SLOT, () => new X(), 'X.instance');\n     }\n   }\n   ```\n\n   Import `TeggScope` **only** from `@eggjs/tegg-types`; never import another\n   package's slot. A package that imports `TeggScope` must declare\n   `@eggjs/tegg-types` as a direct dependency.\n\n2. **Shared, app-agnostic registries stay global.** Class/type-keyed maps\n   populated at import time with app-agnostic values (e.g.\n   `EggPrototypeCreatorFactory` creator map, `registerEggObjectCreateMethod`,\n   `registerLoadUnitInstanceClass`) must NOT be scoped. Only state that holds\n   per-app instances/data is scoped. (`LoadUnitFactory`'s creator map is\n   two-tier: a global base for import-time creators + a per-app overlay for\n   boot-time, app-capturing creators.)\n\n3. **Lifecycle-hook registration via `app.*LifecycleUtil` is bag-pinned.**\n   Calling `app.{loadUnit,eggPrototype,eggObject,eggContext,loadUnitInstance}LifecycleUtil.registerLifecycle(hook)`\n   (and the matching `deleteLifecycle` in `beforeClose`) does **not** need a\n   `TeggScope.run` wrap — these app getters are pinned to this app's bag (via\n   `xxxLifecycleUtilFromBag`), so they resolve the correct per-app util even with\n   no active scope. Wrapping is still fine when the same block does other\n   scope-dependent work (as the tegg plugin's own boot does). Do **not** register\n   lifecycle hooks in the boot **constructor** — `app._teggScopeBag` does not\n   exist yet; do it in `configWillLoad`/`configDidLoad`/`didLoad`. (Accessing a\n   lifecycle util through a module-level static instead of `app.*LifecycleUtil`\n   still needs an active scope.)\n\n4. **Resolve egg objects per-app.** To get a proto from a class, prefer\n   `EggPrototypeFactory.instance.getPrototypeByClazz(clazz)` (per-app) before\n   falling back to `PrototypeUtil.getClazzProto(clazz)` (a process-global slot\n   on the class, overwritten by concurrent boot). `ctx.getEggObject` /\n   `app.getEggObject` already do this and wrap in the app scope.\n\n5. **Escape points** — code that runs **detached** from the request must\n   re-establish the scope. Capture `const bag = TeggScope.current()` at\n   registration/scheduling and re-enter `TeggScope.run(bag, cb)` inside the\n   callback for: emitter listeners triggered later (`res.on('close')`,\n   `signal.addEventListener('abort')`), fire-and-forget `EventBus.emit` from a\n   detached context, and timers created outside a scope. Timers/promises created\n   **inside** an active scope inherit it automatically — no wrap needed.\n\n6. **Strict-mode fuse.** Under true multi-app (`> 1` live app) any access that\n   escapes to the process-default bag throws in dev / warns in prod. If you see\n   `[tegg] TeggScope escaped to the process-default bag`, you have an unwrapped\n   access — wrap the relevant boot/request/escape path in `TeggScope.run`.\n\n7. **Single app is unchanged.** With one app the default bag is used silently\n   and the fuse never fires, so existing single-app behavior and tests are\n   unaffected. Add multi-app regression coverage (two concurrent apps sharing a\n   module) when you change loader/runtime/lifecycle/eventbus behavior — see\n   `tegg/plugin/tegg/test/MultiApp.test.ts`.\n\n**Performance:** `TeggScope.resolve` adds ~8 ns/access and `TeggScope.run`\n~5 ns/call over a plain static read (Node 22); egg already runs on\nAsyncLocalStorage, so there is no new process-wide async penalty. The cost is\nnegligible relative to real request work. The per-app lifecycle-util facade is an\nexplicit delegating object (not a `Proxy`) — each method is a direct slot-resolve\nplus a method call, with no per-access trap or bound-function allocation.\n\n## Common Patterns\n\n### Creating a New Core Package\n\n1. Add to `tegg/core/` directory within the main monorepo\n2. Include `tsconfig.json` extending `@eggjs/tsconfig`\n3. Add standard scripts to `package.json`:\n   - `\"typecheck\": \"tsc --noEmit\"`\n4. Export public API through `src/index.ts`\n5. Use `workspace:*` for internal dependencies and `catalog:` for external dependencies\n\n### Creating a New Plugin\n\n1. Add to `tegg/plugin/` directory within the main monorepo\n2. Define `eggPlugin` in `package.json` with dependencies\n3. Create `app.ts` for initialization\n4. Add tests using Vitest and `@eggjs/mock`\n5. Tests will be automatically discovered by the root `vitest.config.ts`\n\n### Working with TypeScript\n\n- Use `emitDecoratorMetadata` for type inference in injection\n- `design:type` and `design:paramtypes` are used for automatic dependency resolution\n- All packages target ESM with `.js` extensions in imports\n","tools/create-egg/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fcreate-egg%2FREADME.md -->","tools/create-egg/src/templates/egg3-simple-js/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fcreate-egg%2Fsrc%2Ftemplates%2Fegg3-simple-js%2FREADME.md -->","tools/create-egg/src/templates/egg3-simple-ts/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fcreate-egg%2Fsrc%2Ftemplates%2Fegg3-simple-ts%2FREADME.md -->","tools/create-egg/src/templates/egg3-tegg/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fcreate-egg%2Fsrc%2Ftemplates%2Fegg3-tegg%2FREADME.md -->","tools/create-egg/src/templates/simple-ts/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fcreate-egg%2Fsrc%2Ftemplates%2Fsimple-ts%2FREADME.md -->","tools/create-egg/src/templates/tegg/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fcreate-egg%2Fsrc%2Ftemplates%2Ftegg%2FREADME.md -->","tools/egg-bin/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fegg-bin%2FREADME.md -->","tools/egg-bundler/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fegg-bundler%2FREADME.md -->","tools/egg-bundler/docs/output-structure.md":"# Bundle output structure\n\n`@eggjs/egg-bundler` produces a runnable CJS bundle under the configured\n`outputDir`. Everything except configured or auto-detected externals is inlined\ninto the generated worker file by default.\n\n## Layout\n\n```\n<outputDir>/\n├── worker.js             # self-contained entry produced from worker.entry.ts\n├── app/port/binary.html  # runtime asset copied from <baseDir>/app/port/binary.html\n├── app/port/login.html   # runtime asset copied from <baseDir>/app/port/login.html\n├── package.json          # `{ \"type\": \"commonjs\" }`, so Node parses generated *.js as CJS\n└── bundle-manifest.json  # reference/debug metadata written by Bundler\n```\n\n`pack.singleFile` defaults to `true`. When a programmatic caller explicitly\nsets it to `false`, `@utoo/pack` may also emit opaque module-graph and runtime\nchunks such as `_root-of-the-server___<hash>.js` and\n`_turbopack__runtime.js`. Snapshot builds always force single-file output. The\ncompiler `tsconfig.json` is written to the bundler's generated entry directory,\nnot to the deploy output.\n\nFor `target: 'cluster'`, `worker.js` is replaced by two role-specific entry\nfiles:\n\n```text\n<outputDir>/\n├── app_worker.js         # self-contained application worker entry\n├── agent_worker.js       # self-contained agent worker entry\n├── app/...               # copied runtime assets, when present\n├── package.json          # `{ \"type\": \"commonjs\" }`\n└── bundle-manifest.json\n```\n\nThe source generator shares its rendering logic, but each role is a separate\nentry. With the default single-file setting, `@utoo/pack` inlines the complete\ndependency graph into both outputs; snapshot builds rely on this so each file\ncan independently build and restore its own V8 snapshot blob.\n\n## Running the bundle\n\n```bash\ncd <outputDir>\nnode worker.js\n```\n\nThe worker entry installs `ManifestStore.setBundleStore(...)` and\n`globalThis.__EGG_BUNDLE_MODULE_LOADER__` before calling\n`startEgg({ baseDir: outputDir, framework, mode: 'single' })`, so framework\nspecifier lookup is served by the already imported bundled framework module,\nwithout adding framework path aliases. Runtime lookup keeps\nthe deploy output directory separate from the original app paths: the bundle map\nis keyed by relKey, output-dir absolute paths, precomputed original app absolute\npaths, and manifest `resolveCache` request aliases. Application code and plugins\nmay still use `fs` for resources such as config, views, or assets.\n\nThe supported ordinary cluster workflow is:\n\n```bash\negg-bin bundle --cluster\negg-scripts start --bundle --bundle-dir ./dist-bundle\n```\n\n`egg-scripts` resolves `app_worker.js` and `agent_worker.js` from\n`--bundle-dir`, then supplies them to the cluster launcher. Plain bundles\nsupport process and `worker_threads` start modes. To build and restore\nrole-specific V8 snapshots:\n\n```bash\negg-bin snapshot build --cluster\negg-scripts start --bundle \\\n  --app-snapshot-blob ./dist-bundle/app.snapshot.blob \\\n  --agent-snapshot-blob ./dist-bundle/agent.snapshot.blob\n```\n\nEither role blob may be omitted; that role then starts from its bundle\nJavaScript. Snapshot blobs require process mode.\n\n## Runtime assets\n\nThe bundler copies application runtime assets from `<baseDir>/app` by default\ninto the same relative path under `outputDir`, excluding manifest-known module\nfiles and source-like files such as `.js`, `.ts`, `.mjs`, and `.cjs` outside\nstatic asset directories. For example, `<baseDir>/app/port/binary.html` is\nemitted as `<outputDir>/app/port/binary.html`. Since bundled workers start Egg with\n`baseDir: outputDir`, existing reads such as\n`fs.readFile(path.join(app.config.baseDir, 'app/port/binary.html'))` resolve to\nthe copied file in bundle mode and continue to resolve to the original file in\nnon-bundle mode. Static asset directories such as `app/public`, `app/assets`,\nand `app/static` are copied verbatim so frontend `.js` and `.css` files remain\nservable from the bundled app. Applications may replace those force-copy\ndirectories with `bundle.runtimeAssets.forceCopyDirs` in `module.yml`, for\nexample:\n\n```yaml\nbundle:\n  runtimeAssets:\n    roots:\n      - app\n    forceCopyDirs:\n      - app/public\n      - app/assets\n      - app/static\n      - app/port\n```\n\nApplications may also replace the scanned roots with\n`bundle.runtimeAssets.roots`. Root entries are baseDir-relative directories, and\nthe output keeps the same baseDir-relative path.\n\n## `bundle-manifest.json`\n\nA reference file produced by `Bundler` (not consumed at runtime). Shape:\n\n```json\n{\n  \"version\": 1,\n  \"generatedAt\": \"2026-04-11T00:00:00.000Z\",\n  \"mode\": \"production\",\n  \"target\": \"single\",\n  \"baseDir\": \"/abs/path/to/app\",\n  \"framework\": \"egg\",\n  \"entries\": [{ \"name\": \"worker\", \"source\": \"/abs/path/to/app/.egg-bundle/entries/worker.entry.ts\" }],\n  \"externals\": [\"some-native-addon\", \"...\"],\n  \"chunks\": [\"package.json\", \"worker.js\", \"...\"]\n}\n```\n\nUse it to inspect what went into the bundle or to drive deterministic-bundle\nchecks (T17).\n\n## Externals\n\nPackages classified as external by `ExternalsResolver` are **not** inlined.\nThis includes the user's `externals.force` list plus auto-detected entries from\nroot `peerDependencies`, root `optionalDependencies`, root dependency packages\nwith native addons/native binaries, root dependency packages whose optional peer\ndependencies cannot be resolved, and the names of those missing optional peer\npackages as `extraExternals`, plus native optional platform packages declared by\nroot dependencies.\nThe native addon and missing optional peer checks run only while resolving the\napp's root dependencies/optionalDependencies; `ExternalsResolver` does not\nrecursively scan every transitive dependency. `externals.inline`\nremoves an auto-detected external unless the same name is also present in\n`externals.force`. External packages must be resolvable from the output worker.\nA typical deployment keeps `outputDir` below the application or deployment root\nand installs production dependencies at that root. Do not overwrite the\ngenerated output `package.json`: its `{ \"type\": \"commonjs\" }` marker is required\nwhen the parent application package uses ESM. ESM-only packages, `egg`,\n`@swc/helpers`, and `@eggjs/*`\npackages are bundled by default unless an explicit or auto-detected external\nrule applies, such as `externals.force`, peer/optional dependency metadata,\nnative addon detection, or missing optional peer detection. For wrappers around\nnative optional platform packages that cannot be loaded through `createRequire`,\nthe wrapper stays bundled so the CommonJS standalone output does not emit a\nplain `require(wrapper)`, while the optional native platform packages remain\nexternal.\n\n## Known limitations\n\n- **Cluster bootstrap modules**: bundled cluster workers do not support\n  `options.require`; the supported launcher fails before spawning workers.\n- **Snapshot blobs and worker threads**: plain cluster bundles support\n  `worker_threads`, but custom V8 snapshot blobs require process mode.\n- **Native addons**: always external. If a native module is missing from the\n  deployment target, the bundle will fail to start at runtime with the usual\n  Node module resolution error.\n- **Tegg**: decorated files listed in `manifest.extensions.tegg` are included\n  as side-effect imports in the worker entry; if `tegg` is disabled in\n  `BundlerConfig`, tegg collection is intended to be skipped (not yet wired).\n","tools/scripts/README.md":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fscripts%2FREADME.md -->","wiki/packages/core.md":"---\ntitle: Core Package\ntype: package\nsummary: Loader, lifecycle, and application core primitives used by Egg runtime packages.\nsource_files:\n  - packages/core/src/index.ts\n  - packages/core/src/loader/file_loader.ts\n  - packages/core/src/loader/context_loader.ts\n  - packages/core/src/loader/egg_loader.ts\nupdated_at: 2026-05-10\nstatus: active\n---\n\n# Core Package\n\n`@eggjs/core` exports the runtime loader primitives used to assemble an Egg\napplication, including `EggLoader`, `FileLoader`, `ContextLoader`, manifest\nsupport, lifecycle, and base context classes.\n\n## LoaderFS\n\n`@eggjs/core` consumes and re-exports `LoaderFS` and `RealLoaderFS` from\n`@eggjs/loader-fs`. The abstraction remains the minimal filesystem boundary for\nloader-facing file access: `exists`, `stat`, `realpath`, `readJSON`, `glob`, and\n`loadFile`, without trying to polyfill the full Node.js `fs` module.\n\n`EggLoaderOptions`, `FileLoaderOptions`, and `ContextLoaderOptions` can carry a\ncustom `loaderFS`. `EggLoader` passes its loader FS into `loadToApp()` and\n`loadToContext()` so later bundled loaders can replace file discovery and module\nloading without changing the public loader call sites.\n","wiki/packages/egg-bundler.md":"---\ntitle: Egg Bundler\ntype: package\nsummary: Bundles Egg applications for Node startup snapshots and tegg standalone service-worker targets.\nsource_files:\n  - packages/core/src/lifecycle.ts\n  - packages/egg/src/lib/egg.ts\n  - packages/cluster/src/worker_protocol/worker-thread.ts\n  - packages/cluster/src/utils/mode/impl/worker_threads/agent.ts\n  - packages/cluster/src/utils/mode/impl/worker_threads/app.ts\n  - plugins/watcher/src/lib/boot.ts\n  - tools/egg-bundler/src/index.ts\n  - tools/egg-bundler/src/compat/leoric/index.ts\n  - tools/egg-bundler/src/compat/leoric/runtime-require-loader.cjs\n  - tools/egg-bundler/src/lib/Bundler.ts\n  - tools/egg-bundler/src/lib/EntryGenerator.ts\n  - tools/egg-bundler/src/lib/ExternalsResolver.ts\n  - tools/egg-bundler/src/lib/prelude.ts\n  - tools/egg-bundler/src/lib/PackRunner.ts\n  - tools/egg-bundler/src/lib/StandaloneWorkerBundler.ts\n  - tools/egg-bundler/src/lib/importMetaPatch.ts\n  - tools/egg-bin/src/commands/bundle.ts\n  - tools/egg-bin/src/commands/snapshot.ts\n  - tools/scripts/src/commands/start.ts\n  - packages/loader-fs/src/index.ts\n  - packages/loader-fs/src/manifest_loader_fs.ts\n  - tegg/core/types/src/metadata/model/TeggManifest.ts\n  - tegg/core/loader/src/LoaderFactory.ts\n  - tegg/core/loader/src/LoaderUtil.ts\n  - tegg/core/loader/src/TeggManifestLoaderFS.ts\n  - tegg/core/loader/src/impl/ModuleLoader.ts\n  - tegg/plugin/dal/src/lib/DataSource.ts\n  - tegg/plugin/tegg/src/lib/EggModuleLoader.ts\n  - tegg/standalone/standalone/src/EggModuleLoader.ts\n  - tools/egg-bundler/docs/output-structure.md\n  - site/docs/advanced/snapshot.md\n  - site/docs/zh-CN/advanced/snapshot.md\n  - examples/helloworld-service-worker\nupdated_at: 2026-10-06\nstatus: active\n---\n\n# Egg Bundler\n\n`@eggjs/egg-bundler` is a developer tooling package under `tools/egg-bundler/`.\nIt exposes `bundle(config)` and the `Bundler` class for producing a deployable\nCommonJS artifact from an Egg application, plus `StandaloneWorkerBundler` for the\ntegg standalone service worker (Cloudflare Workers) — see the section below.\n\nThe bundler engine (`PackRunner`) is `@utoo/pack`, whose engine is **Turbopack**\n(not mako). `@utoo/pack` only emits **CommonJS** — `output.type` is `standalone`\nor `export` (single-file self-executing IIFE); there is no ESM output. In\nsingle-file (`export`) mode the IIFE ends with `module.exports = factory()` where\n`factory()` returns the entry's ES-module namespace, so a Node `require()` of the\noutput exposes the entry's `export default`/named exports; `library.name`/`export`\nonly affect the `exports[name]`/`globalThis[name]` fallback branches. That CJS-only\nconstraint is why worker output always needs a thin ESM wrapper.\n\n## Public Surfaces\n\n- `tools/egg-bundler/src/index.ts` exports `bundle`, `Bundler`, the helper\n  classes, and the public config/result types.\n- `tools/egg-bin/src/commands/bundle.ts` wires the package into\n  `egg-bin bundle`.\n\n## Bundle Flow\n\n1. `ManifestLoader` loads the app startup manifest, defaulting to\n   `<baseDir>/.egg/manifest.json`.\n2. `ExternalsResolver` classifies packages that should stay external.\n3. `EntryGenerator` writes either one single-process worker entry or separate\n   app-worker and agent-worker entries. Each installs the bundle manifest/module\n   loader before starting its Egg runtime role.\n4. `PackRunner` invokes `@utoo/pack`.\n5. `Bundler` writes `bundle-manifest.json` and returns absolute output paths.\n\n## Current Behavior\n\n- Relative `outputDir` values are resolved from `baseDir`.\n- Default mode is `production`; `development` is also accepted.\n- `PackRunner` defaults to `singleFile: true`, so each normal worker entry is a\n  self-contained CommonJS file. Snapshot builds require and force that setting.\n  A programmatic caller may opt into opaque `@utoo/pack` runtime/module chunks\n  for a non-snapshot build with `singleFile: false`.\n- If `<baseDir>/.egg/manifest.json` is missing, `ManifestLoader` starts the app\n  with `metadataOnly: true` to generate it. This skips the agent and normal boot\n  lifecycle, runs `loadMetadata()` hooks, and the manifest generation child\n  process exits after writing the manifest, so registered `beforeClose` hooks do\n  not run.\n- The default `single` target runs in Egg single-process mode. Its worker entry\n  treats the deploy output directory as the runtime Egg `baseDir`, passes the\n  framework specifier explicitly to `startEgg`, maps that specifier to the\n  already bundled framework module, and precomputes original app absolute\n  aliases so bundled module lookup can serve relKeys, output-dir absolute paths,\n  original app absolute paths, and manifest `resolveCache` request aliases.\n- The `cluster` target emits `app_worker.js` and `agent_worker.js`. Their roles\n  are fixed while generating the entries rather than selected by\n  `EGG_PROCESS_TYPE` or another runtime switch. Both entries use the shared\n  `@eggjs/cluster/worker_protocol` implementation, accept the master's normal\n  JSON argv contract, and select process IPC or `worker_threads.parentPort`\n  from `startMode`. Snapshot builds force each output to remain independently\n  self-contained, so no common runtime chunk is emitted between the two files.\n  Custom V8 snapshot blobs remain process-only because Node does not expose a\n  per-Worker snapshot-blob API; option parsing rejects that combination before\n  creating a worker thread. Sticky-session mode is also process-only because its\n  socket handoff uses process IPC. Worker-thread shutdown is an explicit protocol:\n  the master requests graceful exit, the worker awaits `app.close()` or\n  `agent.close()`, and the master falls back to `Worker.terminate()` immediately\n  if the worker emits an error or after the configured close timeout. Successful\n  shutdown cancels its timeout timer so it does not keep the event loop alive.\n  A nonzero graceful-exit code or worker error is propagated after all app\n  workers and the agent have been given their cleanup attempt; the master exits\n  with code `1`. Timeout termination retains its existing successful master-exit\n  behavior, and process-mode shutdown behavior is unchanged.\n  With worker-thread `reusePort`, an explicit master port of `0` defers to the\n  application's configured shared listen port rather than being rejected before\n  the worker protocol runs.\n- `egg-bin bundle --cluster` is the ordinary cluster-bundle producer. It selects\n  the bundler's `cluster` target and writes `app_worker.js`, `agent_worker.js`,\n  and the bundle manifest without constructing snapshot blobs. Its output can\n  be passed directly to `egg-scripts start --bundle`. `--cluster` is an Egg app\n  mode and is rejected when standalone mode is selected explicitly or inferred\n  from `--entry`.\n- `egg-bin snapshot build --cluster` selects that target and runs two independent\n  V8 snapshot builds: `app_worker.js` produces `app.snapshot.blob`, while\n  `agent_worker.js` produces `agent.snapshot.blob`. Their paths can be set\n  independently with `--app-snapshot-blob` and `--agent-snapshot-blob`; the\n  existing `--blob` flag remains exclusive to single-process builds. The role\n  comes from the entry filename/code, so snapshot construction does not use\n  `EGG_SNAPSHOT_ROLE`. The command rejects identical role blob paths and removes\n  each existing target immediately before building (except in dry-run mode), so\n  a stale blob cannot satisfy the post-build existence check.\n- A `snapshot: true` output remains a normal runnable bundle; generating it does\n  not dedicate the JavaScript file to blob restore. The prelude and generated\n  entry use `v8.startupSnapshot.isBuildingSnapshot()` to detect a real\n  `--build-snapshot` process. A plain invocation keeps Node's web globals intact,\n  installs `__RUNTIME_REQUIRE` before the bundle IIFE, and follows the ordinary\n  bundle startup path. Only snapshot construction installs build-time stubs. On\n  restore, V8 does not re-evaluate the file and instead invokes the serialized\n  deserialize main, which installs the same runtime require hook before resuming.\n- `egg-scripts start --bundle` explicitly selects bundled cluster workers.\n  `--bundle-dir` defaults to `./dist-bundle`, and supplies the fixed defaults\n  `<bundle-dir>/app_worker.js` and `<bundle-dir>/agent_worker.js`; either worker\n  path can be overridden explicitly. Snapshot restore is enabled per role only\n  when `--app-snapshot-blob` or `--agent-snapshot-blob` is provided. Blob\n  locations never determine worker paths, and one role's arguments never\n  determine the other's. Bundle path and role options are ignored unless\n  `--bundle` is present. The existing `--snapshot-blob` remains the single-process\n  launcher and takes precedence when it is supplied together with `--bundle`.\n- Snapshot-backed single-process launches and bundled cluster workers reject\n  non-empty `options.require` before startup. A runtime bootstrap module cannot\n  preserve the source worker's before-framework ordering once the bundle graph\n  has been statically evaluated; silently ignoring it would disable\n  instrumentation or patches without failing the deployment.\n- Explicit `externals.force` entries are external, and `ExternalsResolver`\n  auto-detects root `peerDependencies`, root `optionalDependencies`, root\n  dependency packages with native addons, root dependency packages whose optional\n  peer dependencies cannot be resolved, the missing optional peer package names\n  themselves as `extraExternals`, and native optional platform packages as\n  external.\n- `externals.inline` removes an auto-detected external unless the same package\n  name is also listed in `externals.force`.\n- ESM-only packages, `egg`, `@swc/helpers`, and `@eggjs/*` packages are bundled\n  by default unless `externals.force` or dependency metadata applies. If a\n  wrapper around native optional platform packages cannot be loaded through\n  `createRequire`, the wrapper stays bundled and the platform packages are kept\n  external.\n- `BundlerConfig.tegg` is accepted but intentionally not wired into the current\n  implementation yet.\n\n### Snapshot lifecycle boundary\n\nWith `snapshot: true`, Egg executes `configWillLoad` during snapshot construction\nand stops before `configDidLoad`. After V8 restores the heap, Egg runs the\nregistered `snapshotDidDeserialize` hooks and then resumes the ordinary lifecycle\nfrom `configDidLoad`. Function-style app and agent boot hooks are registered as\n`configDidLoad` hooks, so they already run on the runtime side of this boundary.\n\nPlugin constructors and `configWillLoad` may prepare only serializable\nconfiguration or metadata. Runtime resources such as cluster clients, sockets,\nservers, filesystem watchers, timers, and native clients must be created in\n`configDidLoad` or later. A plugin that consumes another plugin's runtime object\nmust also declare that plugin dependency; this makes the corresponding\n`configDidLoad` order explicit instead of relying on incidental discovery order.\n\n`EggApplicationCore.clusterWrapper()` enforces this contract by throwing during\nsnapshot construction and becoming available after restore. The watcher plugin\nis the reference pattern: its boot constructor stores the app only,\n`configDidLoad` creates and wires the watcher cluster client, and `didLoad` waits\nfor readiness. A generic deferred proxy is intentionally not used because it\nwould silently record arbitrary calls, cover only clients created through\n`clusterWrapper()`, and move failures away from the actual lifecycle violation.\n\nFramework-owned resources that necessarily exist before the cutoff may instead\nimplement a symmetric `snapshotWillSerialize`/`snapshotDidDeserialize` pair, as\nEgg does for its messenger and logger transports. That mechanism is for explicit\nresource ownership, not a substitute for moving plugin runtime initialization to\n`configDidLoad`.\n\n### Snapshot lazy-external defaults\n\nIn `snapshot: true` mode the bundler keeps a set of modules **lazy-external** so a\nV8 startup snapshot stays serializable: each is emitted as an `externalRequire`,\nleft out of the build-time heap (a member-proxy stub from the prelude's\n`__makeLazyExt`), and forwarded to the real module at restore via\n`globalThis.__RUNTIME_REQUIRE`. The injected hook also lazy-stubs any **non-builtin**\nexternal at build (`!__isBuiltin(id)`); the explicit list mainly exists to (a) cover\nbuiltins the `!isBuiltin` rule skips and (b) **force npm packages external** that\nwould otherwise be inlined.\n\n- `DEFAULT_SNAPSHOT_LAZY_MODULES` (in `src/lib/prelude.ts`) covers the Node network\n  stack (`http`/`https`/`http2`/`tls`/`dns`), `inspector`, `cluster`/`node:cluster`,\n  **and egg's HTTP client stack `undici` + `urllib`**. `cluster` is runtime-sensitive:\n  Node chooses its primary or worker implementation when the module is first loaded,\n  while snapshot construction happens outside a cluster worker. Keeping both module\n  specifiers lazy ensures each restored worker loads the worker implementation\n  instead of retaining the builder's primary implementation. Egg builds its\n  `HttpClient` (urllib → undici) during boot, and undici instantiates an llhttp\n  `WebAssembly` (disabled under\n  `--build-snapshot`) + `HTTPParser` that cannot be serialized. As npm packages\n  urllib/undici would be inlined; listing them forces them external (`Bundler` adds\n  the lazy ids to the externals map) so the member-proxy stub is used at build — an\n  app gets a serializable snapshot without listing them in `egg.snapshot.lazyModules`.\n- The member-proxy records the build-time access path (`get`/`apply`/`construct`)\n  and replays it against the real module on restore, so\n  `class HttpClient extends urllib.HttpClient` (and urllib's own\n  `class BaseAgent extends undici.Agent`) keep\n  working: the `extends` is evaluated against the build stub, then `super(...)` /\n  inherited methods resolve to the real base class after deserialization. The\n  first successful replay is memoized, so an instance returned by a recorded call\n  or constructor keeps its identity and subsequent writes are observable.\n- Proxy reads and writes preserve the inherited receiver at restore. Static or\n  prototype accessors on the real base therefore observe the application subclass\n  or instance as `this`, while direct proxy access still uses the real exported\n  object. This is required by symbol-backed model state such as Leoric's\n  `Bone.synchronized` accessor.\n\n#### Leoric compatibility boundary\n\nSnapshot mode intentionally forces the Leoric core into the bundle. Keeping the\nwhole package external was rejected because application/TEGG model modules are\nevaluated while constructing the snapshot: subclasses, prototype chains, and ORM\nmetadata would then be created against a lazy external proxy. Restore can forward\nmember access to the real package, but it cannot replace prototype and metadata\nidentity that is already frozen into the heap. Deferring every model module to\nruntime would avoid that proxy boundary, but would also move the ORM model graph\nout of the snapshot and require Leoric/TEGG-specific lazy-loading behavior.\n\nUnmodified Leoric cannot be safely inlined either. Its driver, realm, and migration\npaths contain expression-based CommonJS `require(...)` calls that `@utoo/pack`\ncannot statically enumerate. A single-file build would otherwise fail on the\ndynamic expression or retain runtime loads for optional drivers and filesystem\nmodules that are not represented by the bundle graph.\n\nThe compatibility layer is therefore a scoped compromise:\n\n- `resolveLeoricSnapshotCompatibility()` removes `leoric` from auto-detected\n  externals and rejects attempts to force it external or add it to snapshot lazy\n  modules.\n- Snapshot builds resolve both imports and requires to Leoric's CommonJS entry.\n  Leoric 2.16 also exports an ESM entry under `dist/`; selecting it would bypass\n  the compatibility rule for `lib/` and expose optional database imports to the\n  bundler.\n- A module rule applies only to the known Leoric source files and rewrites their\n  runtime-selected requires to `globalThis.__RUNTIME_REQUIRE(...)`. Leoric core and\n  model semantics stay bundled, while database clients remain runtime dependencies.\n  The rule also handles TypeScript's compiled dynamic-import callback while\n  preserving its Promise and module namespace conversion.\n- The loader fails the build if an unrecognized expression-based require remains,\n  rather than silently emitting a partial artifact. Migration files used at runtime\n  still need the documented runtime-asset copy configuration.\n\nInference: this is deliberately package- and version-sensitive. A Leoric release\nthat moves these files or changes the require expressions can invalidate the path\ncondition or rewrite patterns, so upgrades require the unit and real-build coverage\nto pass. The shim should be removed when Leoric no longer needs expression-based\nruntime loading, or when `@utoo/pack` provides an equivalent supported runtime\ndynamic-require contract without modifying Turbopack.\n\n#### When does a new dependency need adding?\n\nOnly a package that **directly** creates non-serializable native/WASM state at\nmodule-eval or boot-time instantiation needs a list entry (like `undici`, which\ncompiles llhttp WASM). A package that only reaches the network/native stack\n**transitively** is already covered, because the underlying builtins are lazy:\n\n- `@modelcontextprotocol/sdk` (a default tegg-controller dep, loaded at boot via\n  `tegg/plugin/controller` → `MCPControllerRegister`) → its StreamableHTTP\n  transport `require`s `@hono/node-server`, which does a top-level\n  `require(\"http2\")` + `class extends globalThis.Request`. `http2` is already lazy\n  and `globalThis.Request` is stubbed by the prelude, and no MCP transport/server\n  is instantiated at boot — so the SDK does **not** need a list entry.\n- `@grpc/grpc-js`, `ws` — not present in OSS tegg; gRPC would route through the\n  already-lazy `http2` anyway. No entry needed.\n\n`Inference:` audited 2026-06-28 against `tegg/plugin/controller`, default egg\nplugins, and the suspect packages' module-eval closures. The opt-in\n`mcp-client`/`mcp-proxy`/`langchain` plugins pull the SDK _client_ transports\n(`eventsource`/`cross-spawn`/`pkce-challenge`); if an app enables those and\nsnapshots, re-assess via `egg.snapshot.lazyModules` — that is an app concern, not\na framework default.\n\n## Standalone Worker Bundle (Cloudflare Workers)\n\n`StandaloneWorkerBundler` (`src/lib/StandaloneWorkerBundler.ts`) bundles a tegg\n**standalone service worker** (`@eggjs/service-worker`, see the\n[service worker page](./service-worker.md)) for Cloudflare workerd. This is a\nseparate path from the Egg app `Bundler`/`EntryGenerator` flow above — there is no\n`startEgg`/`app.listen`; the artifact is a fetch handler.\n\n**CLI.** `egg-bin bundle` drives it as a one-shot command (mirroring the egg-app\n`egg-bin bundle`): a standalone bundle is selected by `--entry` (or `--target\nstandalone`); `--framework` names the app package (e.g. `@eggjs/service-worker`),\nwhich must export `loadMetadata` (the standalone counterpart of the egg app's\nframework specifier — no separate app-module/app-export flags). The command runs\n`loadMetadata` + `StandaloneWorkerBundler` internally, so the caller never threads\nthe manifest by hand. The metadata scan runs in an `egg-bin` child process, where\nthe CLI's detected TypeScript compiler and configured `--require`/`--import`\nhooks are active; a TypeScript standalone app therefore does not need to wrap the\nbundle command in its own `NODE_OPTIONS`. `tools/egg-bin/src/commands/bundle.ts`\nbranches on the target, and `tools/egg-bin/scripts/standalone-metadata.mjs`\nperforms the scan.\n\n**Injection-based seam.** The bundler does NOT synthesize the host entry. The user\nauthors a plain, locally-runnable `worker.ts`\n(`new ServiceWorkerApp(dir)` + `export default { fetch }`, or `addEventListener`).\n`run()` then:\n\n1. Filters the caller-supplied tegg manifest (`ServiceWorkerApp.loadMetadata`)\n   by `excludeModules` — a general escape hatch (teggDal no longer needs it: the\n   bundle-mode dynamic-loader fix below makes a scanned-but-unused DAL module load\n   cleanly).\n2. Writes a **build-managed copy of the user's `entry` beside it** (a\n   `.egg-worker-entry.ts` sibling, so the user's relative imports and\n   `import.meta` resolve unchanged), prepending an injected prelude that\n   static-imports every decorated file (so Turbopack bundles them) and installs\n   `globalThis.__EGG_BUNDLE_MODULE_LOADER__` + `globalThis.__EGG_BUNDLE_MANIFEST__`.\n   Evaluation order is safe because import declarations load before module bodies,\n   so the globals are set before the user's `new ServiceWorkerApp(dir)` runs.\n   `StandaloneApp.init` falls back to `__EGG_BUNDLE_MANIFEST__` when no `manifest`\n   option is passed, so the same `worker.ts` runs bundled (global manifest) and\n   unbundled under Node (runtime fs scan).\n3. Runs `PackRunner` (`singleFile: true`, `useDefineForClassFields: true` — the\n   `false` egg-app default would erase uninitialized `#x?: T;` private fields while\n   the code still references `this.#x`), with node builtins external and\n   `resolve.alias` mapping `globby`/`os`/`node:os` to inert stubs (bundle mode never\n   globs; workerd's `nodejs_compat` has no `node:os`).\n4. Applies `patchImportMetaInContent` (`src/lib/importMetaPatch.ts`) to fix\n   Turbopack's broken `import.meta` shim (`__turbopack_context__.F` is otherwise\n   undefined — breaks in Node AND workerd), writes the result as `worker.cjs`, and\n   deletes the injected copy.\n5. Emits the entry per `format`: `module` (default) → an ESM wrapper `index.mjs`\n   (`import worker from './worker.cjs'; export default worker.default;` — a\n   Cloudflare module worker); `service-worker` → no wrapper, the entry is\n   `worker.cjs` itself (a classic, non-module script whose\n   `addEventListener('fetch')` registered on evaluation; an ESM wrapper would move\n   it to module scope where workerd does not dispatch fetch events).\n\n`wrangler.jsonc` sets `nodejs_compat` (tegg needs `AsyncLocalStorage`) and points\n`main` at the wrapper. Verified on Node and workerd (`wrangler dev`): the example's\n`GET /hello/` and `POST /mcp/calc/stream` both return 200. Example:\n`examples/helloworld-service-worker/{worker.ts,wrangler.jsonc}`, built via\n`npm run bundle:cf` (`egg-bin bundle --framework @eggjs/service-worker --entry worker.ts`).\n\n**Bundle-mode dynamic module loading.** Egg and standalone adapt the shared\n`TeggManifest.moduleDescriptors[].decoratedFiles` data through the same\n`createTeggManifestLoaderFS()` helper. The result is a `ManifestLoaderFS`\noverlay: manifest-indexed decorated files are authoritative, while the host's\nnormal loader view may remain as a fallback for unrelated files.\n\nThe host passes that view into the initial `LoaderFactory.loadApp()` scan.\n`ModuleLoader.createModuleLoader()` then installs an explicitly supplied view in\nthe current application's `TeggScope`; later loader creation in the same app\nreuses it. This matters for dynamic multi-instance discovery: DAL's\n`DataSource.getObjects()` still creates a module loader and calls `load()` to\nfind table classes, but in a bundle that scan now reads the scoped manifest view\ninstead of the unavailable runtime filesystem. No class list is added to\n`MultiInstancePrototypeGetObjectsContext`, and generic `LoaderFS` construction\nremains side-effect free.\n\nThe common `ModuleLoader` does not read bundle globals. It first asks its\n`LoaderFS` for an authoritative file list. `ManifestLoaderFS` returns the exact\nmanifest-indexed decorated files, including TypeScript-origin keys and an\nauthoritative empty list; only a source without such a view falls back to\n`LoaderUtil.filePattern()` plus `LoaderFS.glob()`. The standalone host uses\n`globalThis.__EGG_BUNDLE_MANIFEST__` only as an entry source for the shared\nmanifest and constructs the same loader view as Egg.\n\nThis source-level distinction matters when `egg-scripts start` sets\n`EGG_TS_ENABLE=false`: real filesystem discovery still excludes TypeScript,\nwhile `.ts`/`.mts`/`.cts` manifest keys remain discoverable because they resolve\nfrom the in-memory bundle module map rather than files Node must execute. No\nbundle-specific condition is added to `LoaderUtil`.\n\n### Startup benchmark methodology\n\nThe module graph and lifecycle through `configWillLoad` are already captured in\nthe blob, so restore mainly runs runtime initialization from `configDidLoad`\nonward, including `didReady` and connection/listening. The cnpmcore comparison\nexecutes the snapshot-ready JavaScript artifacts normally for the bundle\nbaseline, then restores blobs built from those same files. On Node.js 24.18.1\nand Apple M1 Pro, it runs one warm-up plus ten interleaved measurements per mode.\nSingle process measures direct Node.js spawn through listening; cluster uses the\nmaster's internal orchestration-to-ready timer to exclude launcher and master\nbootstrap. The recorded medians were 947 ms vs 379 ms for single process and\n1356 ms vs 591 ms for cluster, or 2.50x and 2.30x faster restores.\n\nModule identity follows a separate path. Bundle hosts obtain the name from the\nshared `TeggManifest`, normal hosts resolve it while scanning module config, and\nboth store it on `ModuleDescriptor`. `GlobalGraph.moduleConfigList` then carries\nthat name into `LoadUnitFactory`; neither `LoaderFS` nor `LoaderFactory` resolves\n`unitName`. Manifest reference/descriptor names for the same unit path are\nvalidated for consistency.\n\n**Format targets differ.** The `module` format runs on Cloudflare workerd. The\n`service-worker` format targets Web Service Worker / edge runtimes that expose a\nglobal `addEventListener('fetch')` — it is **not** a workerd target: workerd's\n`nodejs_compat` (required for tegg's `AsyncLocalStorage`) only supports the\nmodule-worker format, and `wrangler` rejects a service-worker-format script that\nimports Node builtins (`Unexpected external import of \"assert\"…, no default\nexport`). The minimal `helloworld-service-worker` example intentionally demonstrates\nonly the deployable module-worker path; the classic format remains available through\nthe bundler and CLI for compatible hosts.\n","wiki/packages/loader-fs.md":"---\ntitle: Loader FS Package\ntype: package\nsummary: Shared loader-facing filesystem boundary for Egg loaders and future bundled runtimes.\nsource_files:\n  - packages/loader-fs/src/index.ts\n  - packages/loader-fs/src/manifest_loader_fs.ts\n  - tegg/core/loader/src/LoaderFactory.ts\n  - tegg/core/loader/src/TeggManifestLoaderFS.ts\n  - tegg/core/types/src/scope/TeggScope.ts\n  - tegg/plugin/dal/src/lib/DataSource.ts\n  - packages/loader-fs/package.json\nupdated_at: 2026-08-03\nstatus: active\n---\n\n# Loader FS Package\n\n`@eggjs/loader-fs` owns the small filesystem interface used by loader code. It\nexports `LoaderFS`, `LoaderFSGlobOptions`, `RealLoaderFS`, and\n`ManifestLoaderFS`.\n\n`LoaderFS` intentionally covers only loader-facing operations: `exists`, `stat`,\n`realpath`, `readJSON`, `glob`, and `loadFile`. It is not a Node.js `fs`\npolyfill. A source may additionally implement `getKnownFiles(directory)` when\nit has an authoritative, precomputed view. `undefined` means the source has no\nsuch view and discovery should fall back to `glob`; an empty array is an\nauthoritative empty directory.\n\n`RealLoaderFS` is the default implementation for normal non-bundled runtime. It\ndelegates file checks to Node `fs`, JSON reads to `utility.readJSONSync`, glob\ndiscovery to `globby.sync`, and module loading to the same `@eggjs/utils`\n`importModule()` path used by the core loader.\n\n`ManifestLoaderFS` presents a host-neutral manifest file index through the same\ninterface. Its input contains only `baseDir`, `fileDiscovery`, and\n`resolveCache`; it does not depend on Egg's `ManifestStore` or on tegg types.\nEgg can pass `ManifestStore` structurally, while standalone uses\n`createTeggManifestLoaderFS()` to adapt\n`TeggManifest.moduleDescriptors[].decoratedFiles` into the same file index.\nManifest directories expose their exact indexed files through `getKnownFiles`,\nincluding TypeScript-origin source keys and authoritative empty lists. Unknown\ndirectories delegate to the fallback source.\n\n`@eggjs/core` depends on this package and re-exports its public API so existing\ncore consumers can still import the loader filesystem boundary from core while\ntegg and later bundled runtime packages can depend on the smaller package\ndirectly.\n\n`ModuleLoader` first asks its `LoaderFS` for an authoritative file list. This\nlets a manifest source control discovery without leaking bundle state into the\ngeneric loader. Sources without such a list keep the normal behavior:\n`LoaderUtil.filePattern()` selects supported runtime extensions and\n`LoaderFS.glob()` scans the directory. Consequently `EGG_TS_ENABLE=false`\ncontinues to exclude real TypeScript files, but it does not discard `.ts` keys\nthat a manifest resolves from an in-memory bundle module map.\n\n`LoaderFS` intentionally does not resolve a tegg module name. The host resolves\nidentity onto `ModuleDescriptor`, `GlobalGraph.moduleConfigList` carries it, and\n`LoadUnitFactory` consumes it. LoaderFS only controls which files are visible to\n`ModuleLoader`.\n\nEach host supplies its selected file view when it starts the initial module\nload. `ModuleLoader.createModuleLoader()` is the integration point that installs\nthat view as the current `TeggScope`'s module-loader filesystem; without a\nhost-supplied view it initializes one `RealLoaderFS`. A later explicit view is\nauthoritative, so a manifest-backed view can replace an earlier real-filesystem\ndefault. Generic `LoaderFS` constructors remain side-effect free and\nhost-neutral. Later module loaders created without an explicit filesystem see\nthe same per-app file view as the initial module scan. This lets dynamic\nmulti-instance callbacks such as DAL keep their existing loader-based discovery\nwithout adding host or loader state to their public context. The scoped value is\ndropped with the app's scope bag and remains isolated between concurrent apps.\n","wiki/packages/onerror.md":"---\ntitle: Onerror Plugin\ntype: package\nsummary: Default Egg error-handling plugin and configurable response negotiation layer.\nsource_files:\n  - plugins/onerror/README.md\n  - plugins/onerror/src/app.ts\n  - plugins/onerror/src/lib/onerror.ts\n  - plugins/onerror/src/lib/error_view.ts\n  - plugins/onerror/src/config/config.default.ts\nupdated_at: 2026-05-06\nstatus: active\n---\n\n# Onerror Plugin\n\n`@eggjs/onerror` is the default Egg error-handling plugin. It installs\n`ctx.onerror` and supports configurable handlers for HTML, text, JSON, JS\n(JSONP), redirect, and catch-all error responses.\n\n## Public Configuration\n\n- `errorPageUrl` redirects production HTML requests after unexpected errors.\n- `accepts` customizes content negotiation.\n- `all`, `html`, `text`, `json`, and `js` customize response handling for\n  specific response types.\n- `appErrorFilter` can suppress logging for selected errors.\n\n## Current Behavior\n\nThe plugin owns its Koa-style `onerror()` implementation in\n`plugins/onerror/src/lib/onerror.ts` instead of importing it from\n`koa-onerror`. This keeps the response behavior local to the plugin and avoids\nreading `koa-onerror` package templates when the plugin app boot hook is\nimported, which is important for static bundling.\n\nDefault HTML error-page rendering still flows through `ErrorView` and the\nplugin's local default template. Applications can continue to provide custom\ntemplates and handlers through the documented `config.onerror` options.\n","wiki/packages/service-worker.md":"---\ntitle: Standalone service worker stack\ntype: package\nsummary: Fetch-semantics standalone runtime — HTTP controllers and MCP tools served from a tegg module without an egg application\nsource_files:\n  - tegg/standalone/service-worker-runtime/src\n  - tegg/standalone/service-worker/src\n  - tegg/standalone/service-worker-controller/src\n  - tegg/standalone/standalone/package.json\n  - tegg/standalone/standalone/src/StandaloneApp.ts\n  - tegg/core/controller-runtime/src\n  - tegg/core/controller-decorator/src\n  - tegg/core/types/src/controller-decorator\n  - tegg/plugin/controller/src\n  - examples/helloworld-service-worker\n  - tools/egg-bundler/src/lib/StandaloneWorkerBundler.ts\nupdated_at: 2026-08-06\nstatus: active\n---\n\nThree packages provide the standalone service worker runtime on top of the\nmodule-plugin mechanism (declarative `@InnerObjectProto` /\n`@XxxLifecycleProto` hooks, see the module-plugin pages):\n\n- `@eggjs/service-worker-runtime` — protocol-agnostic: `@Runner()` entry\n  dispatching events to `@EventHandlerProto('<type>')` handlers, event\n  injection into ContextProtos, `BackgroundTaskHelper` re-export\n  (ctx-destroy draining).\n- `@eggjs/service-worker-controller` — the fetch controller transport (the\n  `serviceWorker` eggModule): `FetchEventHandler`, `FetchRouter` + fetch\n  parameter binding (body/param/query/queries/headers/cookies/request via the\n  `@HTTP*` decorators), `HTTP/MCP RegisterProvider`, `ServiceWorkerMcpRouter`,\n  MCP stateless streamable HTTP under `/mcp[/name]/stream`. `@HTTPCookies()`\n  binds a fetch-native `ServiceWorkerCookies` — an `@eggjs/cookies`-compatible\n  `get`/`set` (reads the `Cookie` header, writes `Set-Cookie` onto\n  `ctx.responseHeaders`) — chosen over `@eggjs/cookies`, which has app coupling\n  (`app.emit('cookieLimitExceed')`, unguarded) + heavy egg-flavored deps unwanted\n  at the edge. Signing/encryption are not implemented (inject `@eggjs/cookies` if\n  needed). Users annotate with the `Cookies` type from `@eggjs/tegg`;\n  `ServiceWorkerCookies` is the internal impl.\n- `@eggjs/service-worker` — the host app only: the `ServiceWorkerApp` facade\n  over `StandaloneApp` with `serve()` (node:http bridge) and embedded\n  `handleEvent()`. Its single frameworkDep is its OWN package root, so the\n  framework modules (runtime + controller) are auto-discovered from its package\n  deps via the node_modules eggModule convention — no hand-listed/ordered\n  packages. Its `index` exports only `ServiceWorkerApp`; consumers import\n  controller decorators and middleware contracts from `@eggjs/tegg`, and the\n  fetch context type from `@eggjs/tegg/standalone`. The transport package remains\n  a host adapter rather than a user-facing controller API.\n\nKey mechanics and constraints:\n\n- **App-wide `config` inner object = the entry app module's `module.yml`**:\n  `StandaloneApp` designates the module scanned from `baseDir` (its module dir IS\n  the cwd) as the entry app module and exposes its `module.yml` as the app-wide\n  `config` inner object (`@Inject() config`), the single config surface — there is\n  no programmatic `config` override. Subsystems read their own slice:\n  `config.backgroundTask.timeout` (BackgroundTaskHelper), `config.mcp` (transport\n  selection + DNS-rebinding options `allowedHosts`/`allowedOrigins`/\n  `enableDnsRebindingProtection`). Config VALUES live in the app's `module.yml`\n  (env variants via `module.<env>.yml`); capability OBJECTS (`mcpAuthHandler`,\n  `fetchContextFactory`, `errorResponseMapper`) are `@InjectOptional()` inner\n  objects a host supplies through the generic `innerObjectHandlers` seam, each\n  with a default when absent (allow-all auth, plain context, unmapped error). So\n  `ServiceWorkerApp` has no options of its own — `ServiceWorkerAppOptions` is an\n  alias of `StandaloneAppOptions`. `config` joins `logger`/`moduleConfigs`/\n  `moduleConfig`/`runtimeConfig` as a framework-owned inner object (a host-provided\n  `config` handler is ignored).\n- **Four-package controller layering** (mirrors the egg host): host-agnostic\n  runtime `@eggjs/controller-runtime` (a plain LIBRARY, not an eggModule — base\n  register classes, collect-only `MCPControllerRegister`, `McpRouter`/`Router`\n  abstractions, `MCPServerHelper`, and the controller inner-object prototypes)\n  → egg transport `@eggjs/controller-plugin` (the `teggController` plugin\n  module) → fetch transport `@eggjs/service-worker-controller` (the\n  `serviceWorker` module) → host app `@eggjs/service-worker`. Each HOST package\n  owns the scanned eggModule and re-exports the runtime's inner-object protos\n  into it (`runtimeProtos.ts`); the runtime library itself is never scanned.\n  The service worker depends only on the egg-free runtime, never on the egg\n  plugin.\n- **Controller Advice executes at the bound invocation boundary**: applications\n  apply an `@Advice` class with `@Middleware` on a controller or method. The shared\n  runtime resolves every such class through the active host container and composes\n  only its `around()` hook around the terminal HTTP or MCP invocation; a class with\n  no `around()` hook simply advances to the next middleware. Argument binding\n  happens before the chain. HTTP writes the method result to the host response\n  before `next()` unwinds. MCP method-level Advice wraps the bound SDK callback;\n  controller-level Advice remains outside transport dispatch so the legacy\n  middleware contract can inspect or replace the streaming response after\n  `next()`.\n  `AbstractControllerAdvice.around()` forwards the host context, `next`, and\n  `AdviceContext` to the existing `middleware()` contract. Explicit `@Pointcut`\n  remains an independent AOP path. Controller packages do not depend on the AOP\n  package, and host context is passed directly with no process-global context map.\n- **Host-agnostic MCP register + `McpRouter` boundary**: the shared\n  `MCPControllerRegister` (`@eggjs/controller-runtime`) only COLLECTS tool/resource/prompt\n  records and delegates transport to an injected `McpRouter`\n  (`registerServer(reg)`); it no longer touches `app`. The egg host's node-HTTP\n  transport is `EggMcpRouter` (built imperatively in the controller plugin's\n  `app.ts`, which holds `app`), the service worker's fetch transport is\n  `ServiceWorkerMcpRouter`. Both provide the `mcpRouter` DI object; the two host\n  plugins are never used together, so the shared name does not collide. Because\n  the register needs no `app`, the egg host no longer provides `eggApp` as a\n  module inner object — the Egg `Application` is out of the module DI graph.\n  Like HTTP, per-proto `register()` only collects during load-unit creation;\n  one `MCPControllerRegister.doRegister()` groups the complete records by\n  server name and calls `mcpRouter.registerServer()` once per server. Egg\n  finalizes on the last `CONTROLLER_LOAD_UNIT`; the fetch host finalizes while\n  constructing its routes on the first event. Each host's `registerServer()`\n  mounts directly — there is no second router `doRegister()` phase.\n  Each collected record contains only its controller proto and MCP metadata;\n  the host passes its container factory once when it creates an\n  `MCPServerHelper`, matching the HTTP register's dependency flow. The helper's\n  request callback resolves the controller lazily in the active context.\n- **HTTP registration has separate validation and mutation phases**:\n  `HTTPControllerRegister` creates each `HTTPMethodRegister` once in priority\n  order, calls `checkDuplicate()` on all of them, then calls `register()` on all\n  of them. `checkDuplicate()` checks the real host router and, for host-gated\n  routes, tracks same-host rules in the existing `checkRouters` map. The first\n  phase is not a temporary copy of the complete host router.\n- **MCP SDK >= 1.29 stateless transports are single-shot** (reuse throws), so\n  each MCP request builds a fresh `MCPServerHelper` + web-standard transport\n  from register records collected at boot. The service worker's\n  `ServiceWorkerMcpRouter` uses `WebStandardStreamableHTTPServerTransport`\n  (Request in, Response out) — no node req/res bridging. Auth is an\n  `mcpAuthHandler` extension point (`@InjectOptional`, via `innerObjectHandlers`;\n  absent → allow); DNS-rebinding options are\n  read from `config.mcp` (see the app-wide config bullet).\n- **Config-selected transport provider** (`config.mcp.transport` +\n  `ServiceWorkerMcpRouter.registerTransport`): `config.mcp.transport` (from the\n  app's `module.yml`) selects which transport mounts per app — the built-in\n  `'web'` (web-standard streamable HTTP, the default) or a host-registered\n  alternative by name. A host registers an alternative via\n  `registerTransport(name, provider)` and points its app config at it\n  (`mcp.transport: <name>`); the provider then fully OWNS the server's transport\n  (mutually exclusive with the built-in), so a host can select an alternative\n  transport (e.g. a node-based SSE `/sse`+`/messages`, which the SDK has no\n  web-standard equivalent for, plus streamable) WHOLESALE without forking the\n  router, a facade `mcp` option, an IoC override, or a module swap. The provider\n  receives an `McpServerMountContext` (the `FetchRouter`, live `registration`,\n  `serverName`/`basePath`, and shared\n  `authenticate`/`createServerHelper`/`selectMiddlewares`/`compose` so it reuses\n  the same auth gate, MCP server helper, and middleware pipeline). The registry\n  is `TeggScope`-scoped per app; an unknown name falls back to the built-in, and\n  node:http stays entirely in the host that registers the alternative. Egg's\n  separate `EggMcpRouter` keeps its extension hooks directly on the router\n  instance.\n- **Streaming lifecycle**: a streaming body keeps pulling from ContextProto\n  objects after the runner returns, but the tegg context is destroyed at return.\n  `FetchEventHandler.#guardResponseStream` tees the body — the client consumes\n  one branch, and the request context's `preDestroy`\n  (`EggContextLifecycleUtil.registerObjectLifecycle`) awaits the other branch\n  draining to a sink — so the ContextProto objects the stream reads from stay\n  alive until the source is fully produced, then the context tears down.\n  (No `config.backgroundTask.timeout` cap, so a legitimately long stream — SSE —\n  is never cut short.)\n- **Unified errors**: framework failures reply `{ code, message }` JSON\n  (`NOT_FOUND` / `INTERNAL_SERVER_ERROR`); `ctx.responseHeaders` merge onto\n  the final response.\n- **frameworkDeps scan excludes `test/**`\\*\\*: the framework packages are\n  themselves modules; without the exclusion their test fixtures load as\n  business modules (duplicate controller names) in workspace layouts.\n- **The runner and business modules share the PUBLIC `eggObjectFactory`**:\n  `@eggjs/standalone` directly depends on `@eggjs/dynamic-inject-runtime`, whose\n  module provides the canonical PUBLIC factory. `StandaloneApp` always scans its\n  own package root as an independent built-in root before host `frameworkDeps`,\n  so this does not rely on recursively scanning framework dependency trees.\n  `ServiceWorkerRunner` injects that public factory to dispatch\n  `AbstractEventHandler` by event type; the service-worker runtime does not\n  provide a second PRIVATE factory. `@eggjs/ajv-plugin` remains opt-in and is\n  discovered only when an application or selected framework root brings it into\n  the standalone module references.\n- Per-app mutable state is owned by the app's TeggScope bag, either through its\n  inner objects or explicit scope slots such as the manifest loader view and MCP\n  transport registry. Two concurrent `ServiceWorkerApp`s are isolated\n  (`test/MultiApp.test.ts`).\n- **Cloudflare Workers bundle (injection-based)**: Node runs a `ServiceWorkerApp`\n  directly (runtime fs scan); workerd has no runtime filesystem, so modules are\n  discovered at build time and inlined by `@eggjs/egg-bundler`'s\n  `StandaloneWorkerBundler` (see the [egg-bundler page](./egg-bundler.md)). The\n  seam keeps the host entry as USER code: a plain `worker.ts`\n  (`new ServiceWorkerApp(dir)` + `export default { fetch }`) that also runs\n  unbundled under Node. The bundler injects the scanned imports + manifest into a\n  build-managed copy beside `worker.ts`; `StandaloneApp.init` falls back to\n  `globalThis.__EGG_BUNDLE_MANIFEST__` (mirroring the existing\n  `__EGG_BUNDLE_MODULE_LOADER__` global) when no `manifest` option is passed, so a\n  single `worker.ts` serves both environments with no build-only import. Verified\n  on workerd (`wrangler dev`): `GET /hello/` + `POST /mcp/calc/stream` both 200.\n\nExample: `examples/helloworld-service-worker` (entry `main.ts` lives outside\nthe scanned `app/` module dir on purpose — the scan imports every module\nfile, and importing an entry that boots the app recurses; `worker.ts` is the\nCloudflare Workers entry, built by `npm run bundle:cf` through `egg-bin bundle`).\n","wiki/packages/tegg-vitest.md":"---\ntitle: Tegg Vitest Adapter\ntype: package\nsummary: Vitest 5 runner for Egg request contexts and tegg module scopes.\nsource_files:\n  - .github/workflows/ci.yml\n  - .utoo.toml\n  - tegg/core/vitest/package.json\n  - tegg/core/vitest/src/runner.ts\n  - tegg/core/vitest/src/index.ts\n  - tegg/core/vitest/src/setup.ts\n  - tegg/core/vitest/test/fixture_app.test.ts\n  - tegg/core/vitest/test/runner-multi-app.test.ts\n  - plugins/mock/package.json\n  - tools/egg-bin/src/commands/test.ts\n  - tools/egg-bin/src/commands/cov.ts\n  - tools/egg-bin/test/commands/test.test.ts\n  - tools/egg-bin/test/commands/cov.test.ts\n  - tools/create-egg/src/templates/simple-ts/package.json\n  - tools/create-egg/src/templates/tegg/package.json\nupdated_at: 2026-10-06\nstatus: active\n---\n\n## Compatibility\n\n`@eggjs/tegg-vitest` requires `vitest@^5.0.1`. The runner extends `TestRunner`\nfrom `vitest`. `@eggjs/mock` has the same range for its optional Vitest peer.\nVitest 4 is no longer supported.\n\nLifecycle overrides use Vitest 5 argument types and\nforward all arguments, including the file list passed to `onAfterRunFiles`\nand the retry options passed to `onBeforeTryTask`.\nThe monorepo catalog uses `^5.0.1` for Vitest, its V8 coverage provider, and its\nUI. The TypeScript and tegg application templates also use Vitest `^5.0.1`.\n\nThe `test-tegg-vitest` CI job installs the catalog versions on Node.js 24 and\n26, typechecks the adapter, and runs its tests with isolated workers and with\none shared thread worker (`--pool threads --no-isolate --maxWorkers 1`).\n\n`egg-bin` uses the Vitest 5 `startVitest` API with `config: false` to prevent\nparent config discovery. Coverage exclusions are relative to the application\nroot; absolute user patterns are converted to relative patterns before Vitest\nmatches them. `TEST_REPORTER=json` uses Vitest 5's default output file,\n`.vitest/json/output.json` under the application root.\n\n## Context lifecycle\n\n`egg-bin` automatically selects the adapter's `runner` and `setup` entry points.\n`configureTeggRunner()` lets a test file choose its application and mock cleanup\nbehavior. During collection, the runner resolves that application and waits for\n`app.ready()`.\n\nThe runner holds a module scope for each file and each test. Before each test\nattempt, it creates a request context and enters it synchronously, before any\nawaited cleanup. Otherwise, the caller's async continuation retains the previous\ncontext on a retry. The previous attempt's scope is released before the new\nscope starts. After the test, the runner releases the test scope and restores\nthe file's context.\n\nThe fixture application tests cover injected service identity and scope cleanup\nacross retries. The concurrent-app regression drives the runner hooks for two\napps that share a service class. It checks separate request contexts and service\ninstances, retry argument forwarding, and independent scope cleanup.\nRun the adapter suite from the repository root with\n`vitest run --root tegg/core/vitest --config vitest.config.ts`.\n","wiki/packages/typings.md":"---\ntitle: Typings Package\ntype: package\nsummary: Shared TypeScript type surface for cross-package Egg typings.\nsource_files:\n  - packages/typings/package.json\n  - packages/typings/src/index.ts\n  - packages/typings/src/global.ts\nupdated_at: 2026-04-26\nstatus: seed\n---\n\n# Typings Package\n\n`@eggjs/typings` is the shared package for type contracts that must be consumed\nby more than one Egg package without creating a dependency on a specific runtime\npackage.\n\nCurrent shared types:\n\n- `BundleModuleLoader` in `packages/typings/src/index.ts`\n- `globalThis.__EGG_BUNDLE_MODULE_LOADER__` augmentation in `packages/typings/src/global.ts`\n\nImport `@eggjs/typings/global` from a package entrypoint when that package needs\nthe shared global augmentation in its type surface.\n","wiki/packages/utils.md":"---\ntitle: Utils Package\ntype: package\nsummary: Shared utility package for Egg module loading, config/plugin helpers, and bundled module-loader integration.\nsource_files:\n  - packages/utils/README.md\n  - packages/utils/src/import.ts\n  - packages/utils/test/bundle-import.test.ts\n  - packages/utils/test/module-importer.test.ts\n  - packages/typings/src/index.ts\n  - packages/typings/src/global.ts\nupdated_at: 2026-06-27\nstatus: active\n---\n\n# Utils Package\n\n`@eggjs/utils` is a shared utility package used across Egg packages.\n\n## Public Surfaces\n\n- `getPlugins(options)`, `getLoadUnits(options)`, `getConfig(options)`, and\n  `getFrameworkPath(options)` expose application/framework discovery helpers.\n- `importModule(filepath, options)` and `importResolve(filepath, options)`\n  centralize runtime module loading.\n- `setBundleModuleLoader(loader)` registers the bundled runtime loader hook.\n\n## Bundled Module Loading\n\n`setBundleModuleLoader(loader)` stores the hook on `globalThis` so bundled and\nexternal copies of `@eggjs/utils` share the same loader. The loader receives the\noriginal `importModule()` filepath after POSIX separator normalization, or a\nvirtual specifier. Returning `undefined` falls back to the normal resolution\npath; other return values follow the same default-export unwrapping rules as\n`importModule()`.\n\nWhen a bundle loader is installed and `importModule()` falls through to native\nESM loading, `packages/utils/src/import.ts` uses an opaque native dynamic import\ncreated with `new Function('specifier', 'return import(specifier);')`. This\nprevents bundlers such as Turbopack from rewriting non-static dynamic import\nexpressions and preserves Node's native fallback for external ESM modules.\n\n## Module-loader hook priority\n\n`importModule()` resolves through three hooks before the native `import()` /\n`require()` fallback (see `packages/utils/src/import.ts`):\n\n1. **`__EGG_BUNDLE_MODULE_LOADER__`** (`setBundleModuleLoader`) — synchronous\n   `globalThis` bundle-map lookup, runs before on-disk resolution and receives\n   the POSIX-normalized filepath. Returning `undefined` falls through.\n2. **Snapshot loader** (`setSnapshotModuleLoader`) — a module-local hook (not a\n   `globalThis` global) used by the V8 snapshot entry generator, keyed by the\n   resolved path. Once registered it handles every load that reaches it, so the\n   importer below is bypassed while it is active.\n3. **`__EGG_MODULE_IMPORTER__`** — async (or sync, since awaited) `globalThis`\n   importer that receives the resolved path (the `importResolve()` result, with\n   OS-native separators — _not_ normalized) and replaces native `await import()`.\n\nThe bundle loader and importer globals are typed in `@eggjs/typings`\n(`BundleModuleLoader`, `ModuleImporter`) and augmented onto `globalThis` in\n`@eggjs/typings/global`. `@eggjs/core`'s `ManifestLoaderFS` (`#loadBundledModule`)\nconsults **only** `__EGG_BUNDLE_MODULE_LOADER__`; the importer/native path is\nreached transitively through its `@eggjs/loader-fs` fallback, which calls back\ninto `importModule()`. The tegg loader (`LoaderUtil.loadFile`) consults both\nglobals directly, but passes the _original_ loader filepath separator-normalized\nto POSIX — not the `importResolve()` output — so the two callers differ in both\nwhich path they pass and whether it is normalized.\n\n`__EGG_MODULE_IMPORTER__` has two contract uses:\n\n- **Bundler-based test runners (Vitest):** route loading through the runner's\n  module graph so loader and test file share one module instance (otherwise\n  `ctx.getEggObject(ClassRef)` throws \"can not get proto\").\n- **V8 startup-snapshot restore:** the deserialized main function has no host\n  dynamic-import callback, so native `import()` throws\n  (`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`). The snapshot entry installs a\n  synchronous `require()`-based importer (`createRequire()` over the output\n  dir); `require()` loads ESM on Node >= 22. Covered by\n  `test/module-importer.test.ts` (inline require importer +\n  `fixtures/module-importer-require-esm/run.mjs`, which asserts the no-callback\n  premise via `node:vm`).\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENTS.md\n\nThis is the canonical shared instruction file for coding agents working in this repository.\n\nIf another agent-specific file exists, it should import or defer to this file for shared repository guidance.\n\n## Project Map\n\nEgg is maintained as a utoo monorepo.\n\n- `packages/` contains core framework packages and shared internals.\n- `plugins/` contains optional Egg integrations.\n- `tools/` contains developer tooling such as CLI packages.\n- `tegg/` contains the tegg ecosystem.\n- `examples/` contains sample applications.\n- `site/docs/` contains the English and Chinese documentation site.\n- tests usually live beside packages under `test/`, often with fixtures under `test/fixtures/`.\n\n## Core Commands\n\nThe repository runs on [utoo](https://github.com/utooland/utoo) (`ut`); workspace patterns and overrides live in `package.json`, and shared catalogs live in the tracked `.utoo.toml`. CI installs the latest utoo release; the old pnpm configuration is retained only as a migration reference.\n\n- `npm install --global utoo@latest` installs the latest utoo release on a clean machine.\n- `ut install` hydrates the workspace.\n- `ut run build` builds all packages.\n- `ut run test` runs the main test suite.\n- `ut run lint` runs linting.\n- `ut run typecheck` runs TypeScript checking.\n- use filtered commands for focused work, for example `ut run test --workspace @eggjs/bin` or `ut run build --workspace @eggjs/bin`; prefer the package-name form of `--workspace` (the `./tools/...` path form does not match on Windows); a package without its own script (for example `build` in @eggjs/scripts) needs the root script plus the tsdown workspace path filter instead: `ut run build -- --workspace ./tools/scripts`.\n\n### Local CI\n\nRun tests **without building first**. The CI workflow (`ut install → ut run ci`) never runs `build` before tests. If `dist/` directories exist from a prior build, tegg plugin tests will fail with `duplicate proto` errors because globby scans both `src/*.ts` and `dist/*.js`, loading the same decorated class twice.\n\nWhen you see `duplicate proto` failures locally:\n\n```bash\nfind tegg packages plugins tools -name dist -type d \\\n  -not -path '*/node_modules/*' -not -path '*/test/*' -not -path '*/fixtures/*' \\\n  -exec rm -rf {} +\n```\n\nThen re-run tests.\n\n## Coding Conventions\n\n- prefer existing repo patterns over inventing new ones\n- prefer ESM and TypeScript-first changes where applicable\n- keep file names lowercase with hyphens\n- keep public API changes deliberate and documented\n- use `oxfmt` and `oxlint --type-aware` conventions already present in the repo\n- **tegg multi-app isolation**: do NOT introduce new process-global mutable\n  runtime state in `tegg/`; per-app state must be backed by a `TeggScope` slot.\n  Hooks registered through the bag-pinned `app.*LifecycleUtil` getters need no\n  extra wrap; detached/escape-point access (timers, emitter listeners, proxy\n  handlers, module-level lifecycle-util statics) must run inside\n  `TeggScope.run(app._teggScopeBag, ...)`. See the \"Multi-App Isolation\n  (TeggScope)\" section in `tegg/AGENTS.md` for the full rules.\n- **V8 startup snapshot lifecycle**: a snapshot build runs through\n  `configWillLoad` and resumes from `configDidLoad` only after restore. Plugin\n  constructors and `configWillLoad` must therefore keep only serializable\n  configuration and metadata; create cluster clients, sockets, servers, file\n  watchers, timers, native clients, and other runtime resources in\n  `configDidLoad` or a later hook. If one plugin consumes another plugin's\n  runtime instance, declare that plugin dependency so their `configDidLoad`\n  ordering is deterministic. Do not hide an early initialization violation\n  behind a placeholder/deferred proxy or recorded-call replay; fail fast and\n  move the initialization to the correct lifecycle phase. Use\n  `snapshotWillSerialize`/`snapshotDidDeserialize` only for framework-owned\n  resources that must exist before the cutoff and have an explicit symmetric\n  release/restore implementation.\n- **V8 startup snapshot dependencies**: the egg-bundler can build a V8 startup\n  snapshot (`snapshot: true`), where the app boots only to `configWillLoad` at\n  BUILD time. Any module loaded or instantiated during that boot that creates a\n  non-serializable native binding — llhttp `HTTPParser` (http/https/undici),\n  `nghttp2` (http2, and anything built on it), tls `SecureContext`, dns\n  `ChannelWrap`, a `WebAssembly` instance (undici's llhttp; WASM is disabled under\n  `--build-snapshot`), fs watchers, native addons, open sockets — makes the\n  snapshot build FATAL (\"global handle not serialized\"). Such modules must be kept\n  EXTERNAL (not inlined) so the prelude stubs them at build and forwards to the\n  real module via `globalThis.__RUNTIME_REQUIRE` at restore. The framework default\n  list is `DEFAULT_SNAPSHOT_LAZY_MODULES` in `tools/egg-bundler/src/lib/prelude.ts`\n  (network builtins + `inspector` + `undici` + `urllib`); apps extend it via\n  `egg.snapshot.lazyModules` in `package.json`. **When adding a framework\n  dependency that touches the network/native stack during boot, check whether it\n  must be added to that list.** A package that only reaches the network stack\n  _transitively_ is already covered because those builtins are lazy (e.g.\n  `@modelcontextprotocol/sdk` → `@hono/node-server` → `http2`, `@grpc/grpc-js` →\n  `http2`); only a package that DIRECTLY creates native/WASM state at module-eval\n  or boot-time instantiation (like `undici`) needs adding. See the \"Snapshot\n  lazy-external defaults\" section in `wiki/packages/egg-bundler.md` for details.\n\n## TypeScript Global Types\n\n- put package-wide global augmentations in a dedicated `src/global.ts` or `src/global.d.ts`\n- shared cross-package global types belong in `@eggjs/typings`, not in one consumer package\n- import shared global augmentations from the package entry that needs the type surface, for example `import '@eggjs/typings/global'`\n- keep `declare global` files as modules by using an `import type` or `export {}`\n\n## Testing And PR Expectations\n\n- run the most targeted tests that validate the touched area\n- include regression coverage when changing loader, cluster, agent, HTTP, or process behavior\n- use Angular-style commit messages such as `fix(loader): ensure middleware order`\n- keep PR descriptions clear about motivation, scope, and test evidence\n\n## Security And Config\n\n- review `SECURITY.md` before handling vulnerability-related work\n- do not commit secrets, credentials, or local-only URLs\n- keep local Node.js aligned with `engines.node` and use the latest utoo release\n\n## Shared Knowledge Workflow\n\nThis repository also maintains an LLM-owned wiki for durable project knowledge.\n\n### Explicit Wiki Write Trigger\n\nCreate, update, reorganize, or log wiki content only when the user explicitly\nrequests a wiki build or maintenance task. Reading the wiki for context does\nnot authorize writing to it.\n\nOrdinary coding, debugging, testing, reviews, documentation edits, and PR work\nmust not produce wiki changes, even when they reveal durable findings or change\nAPIs, architecture, contributor workflows, or testing expectations. Do not\ninclude incidental wiki updates in commits or PRs. Report relevant findings in\nthe task response or PR description instead.\n\nThe wiki writing rules below apply only within an explicitly requested wiki\ntask. Use this three-layer model:\n\n### Raw Sources\n\nRaw sources are the source of truth.\n\nThey include:\n\n- repository code under `packages/`, `plugins/`, `tools/`, `tegg/`, `examples/`, and `scripts/`\n- user-facing docs under `site/docs/`\n- root markdown files such as `README.md`, `README.zh-CN.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, and `SECURITY.md`\n- explicit external artifacts referenced by the user or task\n- imported materials stored under `raw/`\n\nRules:\n\n- do not treat wiki summaries as authoritative when raw sources disagree\n- do not rewrite raw sources unless the task requires it\n- during an explicitly requested wiki task, capture repeatedly used external sources in the wiki\n\n### Wiki\n\nThe wiki lives under `wiki/` and stores durable synthesized knowledge.\n\n### Schema\n\nShared workflow rules live here in `AGENTS.md`.\n\nAgent-specific files should stay thin and point back to this file instead of duplicating the schema.\n\n## Wiki Layout\n\n- when consulting the wiki, read `wiki/index.md` first\n- `wiki/log.md` is the append-only chronological log\n- `wiki/packages/` holds package, plugin, tool, and subsystem pages\n- `wiki/concepts/` holds architectural and cross-cutting pages\n- `wiki/workflows/` holds repeatable procedures\n- `wiki/decisions/` holds notable tradeoffs and decisions\n- `wiki/sources/` holds summaries of major source documents or external materials\n\n## Wiki Page Rules\n\nUse these page types:\n\n- `package`\n- `concept`\n- `workflow`\n- `decision`\n- `source`\n\nEvery substantive page should begin with frontmatter:\n\n```yaml\n---\ntitle: Short human-readable title\ntype: package|concept|workflow|decision|source\nsummary: One-line summary\nsource_files:\n  - path/or/url\nupdated_at: YYYY-MM-DD\nstatus: seed|active|stale\n---\n```\n\nUse lowercase kebab-case filenames and keep one topic per page.\n\nUse the workspace-local Asia/Shanghai calendar date for wiki log headings and\nfrontmatter `updated_at` values.\n\n## Citation And Freshness Rules\n\n- every nontrivial wiki claim should be traceable to raw sources\n- list major source paths in `source_files`\n- label non-obvious synthesis as `Inference:`\n- record conflicts explicitly instead of flattening them\n- mark stale or unresolved claims when freshness is uncertain\n\n## Index And Log Rules\n\n`wiki/index.md` should:\n\n- list durable pages by category\n- give each page a one-line summary\n- stay concise enough to scan quickly\n\n`wiki/log.md` should record:\n\n- ingestion of substantial new sources\n- durable findings produced during a query\n- material wiki refactors or lint passes\n- code or docs changes that alter previously recorded understanding\n\nDo not log trivial typo-only edits.\n\n## Standard Workflows\n\nIngest and Lint are wiki maintenance workflows and require an explicit user\nrequest. Query is read-only by default; its write-back steps apply only when\nthe user also explicitly requests wiki construction or maintenance.\n\n### Ingest\n\n1. Read `wiki/index.md`.\n2. Find existing pages that should absorb the new information.\n3. Create or update a `source` page if the source is substantial.\n4. Update impacted wiki pages.\n5. Update `wiki/index.md`.\n6. Append to `wiki/log.md` if the wiki changed materially.\n\n### Query\n\n1. Read `wiki/index.md` and relevant wiki pages first.\n2. Use the wiki as the starting point, not the final authority.\n3. Read raw sources for verification, detail, or freshness.\n4. Only within an explicitly requested wiki task, write durable findings back into the wiki.\n5. Only within that wiki task, update the log when the wiki changes materially.\n\n### Lint\n\nCheck the wiki for:\n\n- orphan pages\n- stale claims after code or docs changes\n- duplicated concepts\n- missing source references\n- missing pages for frequently touched areas\n- contradictions between pages\n\nPrefer restructuring pages over making them longer.\n\n## Repo-Specific Wiki Priorities\n\nPrioritize durable wiki coverage for:\n\n- `packages/egg`, `packages/core`, `packages/utils`, and other foundational packages\n- plugins under `plugins/`\n- tools under `tools/`\n- `tegg/`\n- docs structure under `site/docs/`\n- loading, lifecycle, plugin model, testing, release, and docs-maintenance concepts\n\n## Change Trigger\n\nOnly an explicit user request to build or maintain the wiki triggers wiki\noutput. Code or documentation changes and newly discovered findings do not\ntrigger it automatically. Leave existing wiki files unchanged during ordinary\ntasks.\n","isInternal":false,"tokens":2693,"sizeBytes":11789},{"name":"AGENTS.md","path":"tegg/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/tegg/AGENTS.md","title":"tegg - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENTS.md\n\nFollow the shared repository guidance in [the root AGENTS.md](../AGENTS.md).\nThis file adds guidance for working in `tegg/`.\n\n## Overview\n\nTegg is a modular IoC (Inversion of Control) framework for Egg.js, providing dependency injection, lifecycle management, and plugin architecture. It's designed for building large-scale, maintainable Node.js applications using TypeScript decorators.\n\n**Requirements:**\n\n- Node.js >= 22.18.0\n- ESM only (no CommonJS)\n- egg >= 4.1.0\n\n## Monorepo Structure\n\n**IMPORTANT:** Tegg is part of the main [Egg.js monorepo](https://github.com/eggjs/egg). All build, test, and version management commands should be run from the monorepo root.\n\nThe tegg packages are organized as follows within the main monorepo:\n\n```\ncore/          # 24 core packages - decorators, runtime, metadata, loaders\nplugin/        # 10 plugin packages - Egg.js plugins that integrate core functionality\nstandalone/    # 1 standalone package - standalone runtime without Egg.js\n```\n\n**Dependency Management:**\n\n- Uses utoo workspaces with `catalog:` protocol for shared external dependencies\n- Uses `workspace:*` protocol for internal monorepo dependencies (both tegg and egg packages)\n- All shared dependency versions centralized in the root `.utoo.toml` (not in tegg/)\n- `catalogMode: prefer` set in root `.npmrc` for automatic catalog usage\n- Tegg workspace patterns are defined in root package.json as `tegg/core/*`, `tegg/plugin/*`, `tegg/standalone/*`\n\n### Key Core Packages\n\n- **core-decorator**: Basic decorators (`@Inject`, `@ContextProto`, `@SingletonProto`)\n- **metadata**: Metadata management for prototypes, modules, and dependency graphs\n- **runtime**: Runtime container and object lifecycle management\n- **loader**: Module discovery and loading system\n- **lifecycle**: Lifecycle hooks and management\n- **types**: TypeScript type definitions\n- **common-util**: Shared utilities\n\n### Key Plugin Packages\n\n- **plugin/tegg**: Main Egg.js plugin integrating tegg runtime\n- **plugin/config**: Module configuration support\n- **plugin/controller**: HTTP controller decorator support\n- **plugin/aop**: AOP runtime integration\n- **plugin/eventbus**: Event bus system\n- **plugin/schedule**: Scheduled task support\n- **plugin/dal**: Data access layer\n- **plugin/orm**: Leoric ORM integration\n\n## Development Commands\n\n**Note:** All commands below should be run from the **monorepo root** (`../egg`), not from the tegg directory.\n\n### Build & Clean\n\n```bash\nut run build               # Build all packages including tegg (runs build in all workspaces)\nut run clean-dist          # Clean all build artifacts including tegg (removes dist, tsbuildinfo)\n```\n\n### Testing\n\nAll tegg packages use **Vitest** for testing and are integrated with the main Egg.js monorepo test suite.\n\n```bash\nut run test                # Run vitest tests for all packages (from monorepo root)\nut run test:cov            # Run tests with coverage\nut run ci                  # Full CI: vitest with coverage and bail on first failure\n```\n\nRun tegg tests from clean sources, not immediately after `pnpm run build`. If\nlocal tests fail with `duplicate proto` after a build, remove stale `dist/`\ndirectories outside fixtures as described in the root `AGENTS.md` Local CI\nsection, then re-run tests.\n\n**Note:** Tests are configured in the monorepo root `vitest.config.ts` which includes all tegg packages (`tegg/core/*`, `tegg/plugin/*`, `tegg/standalone/*`).\n\n### Type Checking & Linting\n\n```bash\nut run typecheck           # Clean and type check all workspaces (including tegg)\nut run lint                # Run oxlint with type-aware checking on all packages\nut run fmtcheck            # Check code formatting with oxfmt\n```\n\n**Note:** oxlint automatically runs with `--type-aware` flag for enhanced TypeScript checking.\n\n### Version Management\n\n**Note:** Run these commands from the monorepo root (`../egg`).\n\n```bash\nut run version:patch       # Bump patch version (0.0.X)\nut run version:minor       # Bump minor version (0.X.0)\nut run version:major       # Bump major version (X.0.0)\nut run version:prepatch    # Bump to next prerelease patch version\nut run version:preminor    # Bump to next prerelease minor version\nut run version:premajor    # Bump to next prerelease major version\nut run version:alpha       # Bump prerelease alpha version\nut run version:beta        # Bump prerelease beta version\nut run version:rc          # Bump prerelease rc version\n```\n\n### Working with Individual Packages\n\n**Note:** Run from the monorepo root to work with individual tegg packages.\n\n```bash\n# Install dependencies\nut install              # Install all dependencies from native utoo configuration\n\n# Type check specific packages\nut run typecheck                    # Type check all packages\nut --filter @eggjs/tegg-runtime run typecheck\n\n# Build specific packages\nut --filter @eggjs/metadata run build\nut --filter @eggjs/tegg-runtime run build\n\n# Clean specific package\nnode -e \"require('node:fs').rmSync('tegg/core/runtime/dist', { recursive: true, force: true })\"\n```\n\n**Note:** Individual tegg packages don't have test scripts in their package.json. Tests are run via the monorepo root vitest configuration.\n\n## Architecture Concepts\n\n### Prototype System\n\nTegg uses a prototype-based system where classes are decorated to define how they should be instantiated:\n\n- **ContextProto**: Instance per request context (scoped to HTTP request)\n- **SingletonProto**: Single instance for entire application lifecycle\n- **MultiInstanceProto**: Multiple instances of same class with different qualifiers\n\nEach prototype has:\n\n- **AccessLevel**: `PRIVATE` (module-only) or `PUBLIC` (globally accessible)\n- **InitType**: Defines lifecycle scope (`CONTEXT`, `SINGLETON`)\n- **Name**: Instance identifier (defaults to camelCase class name)\n\n### Dependency Injection\n\nDependencies are resolved through `@Inject()` decorator:\n\n- Property injection: `@Inject() logger: Logger`\n- Constructor injection: `constructor(@Inject() logger: Logger)`\n- Optional injection: `@InjectOptional()` or `@Inject({ optional: true })`\n\n**Injection Rules:**\n\n- ContextProto can inject any prototype\n- SingletonProto cannot inject ContextProto\n- No circular dependencies allowed (between prototypes or modules)\n- Cannot inject `ctx`/`app` directly - inject specific services instead\n\n### Qualifiers\n\nWhen multiple implementations exist, use qualifiers to disambiguate:\n\n- `@InitTypeQualifier(ObjectInitType.CONTEXT)`: Specify init type\n- `@ModuleQualifier('moduleName')`: Specify source module\n- `@EggQualifier(EggType.CONTEXT)`: Specify egg context vs app\n- Custom qualifiers for dynamic injection patterns\n\n### Module System\n\nModules are organizational units discovered by scanning:\n\n- `app/modules/` directory (auto-discovered)\n- `config/module.json` (manual declaration for npm packages)\n\nEach module contains:\n\n- Prototype classes with decorators\n- Optional `module.json` or `package.json` with tegg metadata\n- Module-level dependencies on other modules\n\nThe **GlobalGraph** builds a dependency graph of all modules and validates:\n\n- No circular module dependencies\n- All prototype dependencies are resolvable\n- Access level constraints are respected\n\n### Lifecycle Hooks\n\nObjects can implement `EggObjectLifecycle` interface or use decorators:\n\n- `@LifecyclePostConstruct()`: After constructor\n- `@LifecyclePreInject()`: Before dependency injection\n- `@LifecyclePostInject()`: After dependency injection\n- `@LifecycleInit()`: Custom async initialization\n- `@LifecyclePreDestroy()`: Before object destruction\n- `@LifecycleDestroy()`: Resource cleanup\n\n### Runtime Object Management\n\nThe runtime manages object instances through:\n\n- **EggObjectFactory**: Creates and retrieves object instances\n- **LoadUnitInstance**: Manages module instances and their objects\n- **EggContext**: Request-scoped context holding ContextProto instances\n- **ContextObjectGraph**: Dependency graph for a specific context\n\n## Testing Patterns\n\n### Testing with MockApplication\n\n```typescript\nimport { MockApplication } from '@eggjs/mock';\n\n// Create context scope\nawait app.mockModuleContextScope(async (ctx: Context) => {\n  // Get object by class\n  const service = await ctx.getEggObject(HelloService);\n\n  // Get object by name with qualifiers\n  const logger = await ctx.getEggObjectFromName('logger', {\n    qualifier: 'bizLogger',\n  });\n});\n```\n\n### Async Tasks in Tests\n\nUse `BackgroundTaskHelper` instead of `setTimeout`/`setImmediate`:\n\n```typescript\n@ContextProto()\nclass MyService {\n  @Inject()\n  backgroundTaskHelper: BackgroundTaskHelper;\n\n  async doWork() {\n    this.backgroundTaskHelper.run(async () => {\n      // Async work here\n    });\n  }\n}\n```\n\n## Important Implementation Details\n\n### Metadata Registration\n\nDecorators register metadata on classes that is later used by the loader:\n\n- Prototype metadata: `PrototypeUtil.setXXX()` stores on class\n- Injection metadata: `InjectObjectInfo` stored per property/parameter\n- Qualifier metadata: `QualifierUtil.addProperQualifier()` for disambiguation\n\n### Loading Process\n\n1. **Loader** scans directories and discovers modules\n2. **EggPrototypeFactory** creates `EggPrototype` from decorated classes\n3. **GlobalGraph** validates and builds dependency graph\n4. **LoadUnitFactory** creates `LoadUnit` for each module\n5. **Runtime** instantiates objects on-demand based on graph\n\n### Dynamic Injection\n\nFor selecting implementations at runtime:\n\n```typescript\n// Define abstract class and enum\nabstract class AbstractHello { abstract hello(): string; }\nenum HelloType { FOO = 'FOO', BAR = 'BAR' }\n\n// Create decorator\nconst Hello = QualifierImplDecoratorUtil.generatorDecorator(\n  AbstractHello,\n  'HELLO_ATTRIBUTE'\n);\n\n// Apply to implementations\n@ContextProto()\n@Hello(HelloType.FOO)\nclass FooHello extends AbstractHello { ... }\n\n// Get instance dynamically\nconst impl = await eggObjectFactory.getEggObject(\n  AbstractHello,\n  HelloType.FOO\n);\n```\n\n## Multi-App Isolation (TeggScope) — MUST follow\n\nTegg supports multiple apps booting and serving requests **concurrently in one\nprocess** without cross-talk. This is built on `TeggScope`\n(`@eggjs/tegg-types`), a type-free `AsyncLocalStorage<Map<symbol, unknown>>`.\nEach app owns a per-app \"bag\" (`app._teggScopeBag`); per-app state lives in\nslots inside that bag, and the active bag is established with\n`TeggScope.run(app._teggScopeBag, ...)`. Per-app singletons resolve via the same\nold static call sites (e.g. `EggPrototypeFactory.instance`) — they now read the\ncurrent scope instead of a process global.\n\nWhen you touch tegg core/plugins, follow these rules:\n\n1. **Never add new process-global mutable runtime state.** A `static` field /\n   `Map` / singleton that holds per-app data WILL leak across concurrent apps.\n   If you need such state, back it with a `TeggScope` slot:\n\n   ```ts\n   import { TeggScope } from '@eggjs/tegg-types';\n   const X_SLOT = Symbol('tegg:<pkg>:<name>'); // module-private, never exported\n   export class X {\n     static get instance(): X {\n       return TeggScope.resolve(X_SLOT, () => new X(), 'X.instance');\n     }\n   }\n   ```\n\n   Import `TeggScope` **only** from `@eggjs/tegg-types`; never import another\n   package's slot. A package that imports `TeggScope` must declare\n   `@eggjs/tegg-types` as a direct dependency.\n\n2. **Shared, app-agnostic registries stay global.** Class/type-keyed maps\n   populated at import time with app-agnostic values (e.g.\n   `EggPrototypeCreatorFactory` creator map, `registerEggObjectCreateMethod`,\n   `registerLoadUnitInstanceClass`) must NOT be scoped. Only state that holds\n   per-app instances/data is scoped. (`LoadUnitFactory`'s creator map is\n   two-tier: a global base for import-time creators + a per-app overlay for\n   boot-time, app-capturing creators.)\n\n3. **Lifecycle-hook registration via `app.*LifecycleUtil` is bag-pinned.**\n   Calling `app.{loadUnit,eggPrototype,eggObject,eggContext,loadUnitInstance}LifecycleUtil.registerLifecycle(hook)`\n   (and the matching `deleteLifecycle` in `beforeClose`) does **not** need a\n   `TeggScope.run` wrap — these app getters are pinned to this app's bag (via\n   `xxxLifecycleUtilFromBag`), so they resolve the correct per-app util even with\n   no active scope. Wrapping is still fine when the same block does other\n   scope-dependent work (as the tegg plugin's own boot does). Do **not** register\n   lifecycle hooks in the boot **constructor** — `app._teggScopeBag` does not\n   exist yet; do it in `configWillLoad`/`configDidLoad`/`didLoad`. (Accessing a\n   lifecycle util through a module-level static instead of `app.*LifecycleUtil`\n   still needs an active scope.)\n\n4. **Resolve egg objects per-app.** To get a proto from a class, prefer\n   `EggPrototypeFactory.instance.getPrototypeByClazz(clazz)` (per-app) before\n   falling back to `PrototypeUtil.getClazzProto(clazz)` (a process-global slot\n   on the class, overwritten by concurrent boot). `ctx.getEggObject` /\n   `app.getEggObject` already do this and wrap in the app scope.\n\n5. **Escape points** — code that runs **detached** from the request must\n   re-establish the scope. Capture `const bag = TeggScope.current()` at\n   registration/scheduling and re-enter `TeggScope.run(bag, cb)` inside the\n   callback for: emitter listeners triggered later (`res.on('close')`,\n   `signal.addEventListener('abort')`), fire-and-forget `EventBus.emit` from a\n   detached context, and timers created outside a scope. Timers/promises created\n   **inside** an active scope inherit it automatically — no wrap needed.\n\n6. **Strict-mode fuse.** Under true multi-app (`> 1` live app) any access that\n   escapes to the process-default bag throws in dev / warns in prod. If you see\n   `[tegg] TeggScope escaped to the process-default bag`, you have an unwrapped\n   access — wrap the relevant boot/request/escape path in `TeggScope.run`.\n\n7. **Single app is unchanged.** With one app the default bag is used silently\n   and the fuse never fires, so existing single-app behavior and tests are\n   unaffected. Add multi-app regression coverage (two concurrent apps sharing a\n   module) when you change loader/runtime/lifecycle/eventbus behavior — see\n   `tegg/plugin/tegg/test/MultiApp.test.ts`.\n\n**Performance:** `TeggScope.resolve` adds ~8 ns/access and `TeggScope.run`\n~5 ns/call over a plain static read (Node 22); egg already runs on\nAsyncLocalStorage, so there is no new process-wide async penalty. The cost is\nnegligible relative to real request work. The per-app lifecycle-util facade is an\nexplicit delegating object (not a `Proxy`) — each method is a direct slot-resolve\nplus a method call, with no per-access trap or bound-function allocation.\n\n## Common Patterns\n\n### Creating a New Core Package\n\n1. Add to `tegg/core/` directory within the main monorepo\n2. Include `tsconfig.json` extending `@eggjs/tsconfig`\n3. Add standard scripts to `package.json`:\n   - `\"typecheck\": \"tsc --noEmit\"`\n4. Export public API through `src/index.ts`\n5. Use `workspace:*` for internal dependencies and `catalog:` for external dependencies\n\n### Creating a New Plugin\n\n1. Add to `tegg/plugin/` directory within the main monorepo\n2. Define `eggPlugin` in `package.json` with dependencies\n3. Create `app.ts` for initialization\n4. Add tests using Vitest and `@eggjs/mock`\n5. Tests will be automatically discovered by the root `vitest.config.ts`\n\n### Working with TypeScript\n\n- Use `emitDecoratorMetadata` for type inference in injection\n- `design:type` and `design:paramtypes` are used for automatic dependency resolution\n- All packages target ESM with `.js` extensions in imports\n","isInternal":false,"tokens":3664,"sizeBytes":15580},{"name":"copilot-instructions.md","path":".github/copilot-instructions.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/.github/copilot-instructions.md","title":"GitHub Copilot Instructions","category":"copilot-instructions","format":"markdown","content":"# Eggjs Framework - GitHub Copilot Development Instructions\n\n**Always reference these instructions first and fallback to search or additional context gathering only when you encounter unexpected information that does not match the information provided here.**\n\n## Overview\n\nEggjs is a progressive Node.js framework for building enterprise-class server-side applications. Built on top of Koa.js, it provides a plugin system, conventions over configuration, and enterprise-grade features like clustering, logging, and security.\n\nThis is a **utoo monorepo** with multiple packages using utoo workspaces and catalog mode for centralized dependency management.\n\n## Prerequisites and Environment Setup\n\n- **Node.js >= 22.18.0 required** - This is a hard requirement\n- Enable utoo first: `npm install --global utoo@latest`\n- **NEVER CANCEL** any build or test commands - they can take several minutes to complete\n\n## Bootstrap and Build Process\n\n**Run these commands after a fresh clone:**\n\n```bash\n# 1. Enable utoo (required first)\nnpm install --global utoo@latest\n\n# 2. Install all dependencies - takes ~63 seconds. NEVER CANCEL. Set timeout to 120+ seconds.\nut install\n\n# 3. Run lint to check code quality across all packages - takes ~2 seconds\nut run lint\n\n# 4. Build all packages when validating build output - takes ~14 seconds. NEVER CANCEL. Set timeout to 60+ seconds.\nut run build\n```\n\nRun unit tests from a clean source tree, not immediately after `ut run build`.\nThe main CI test job installs dependencies with `ut install` and\nruns tests with `ut run ci`; it does not run `build` before tests.\n\n## Monorepo Structure\n\n### Key Packages (all in `packages/` directory)\n\n- **`packages/egg/`** - Main Eggjs framework (TypeScript, uses tsdown for builds)\n- **`packages/core/`** - Core plugin framework\n- **`packages/utils/`** - Utility functions\n- **`packages/mock/`** - Testing utilities\n- **`packages/cluster/`** - Cluster management\n- **`packages/koa/`** - Koa web framework\n- **`packages/supertest/`** - HTTP testing utilities\n- **`packages/extend2/`** - Object extension utility\n\n### Supporting Directories\n\n- **`examples/`** - Two example apps: `helloworld-commonjs` and `helloworld-typescript` (currently have runtime issues)\n- **`site/`** - Documentation website built with VitePress\n\n## Essential Commands and Timing\n\n### Build Commands\n\n- `ut run build` - **Build all packages (~14 seconds). NEVER CANCEL. Set timeout to 60+ seconds.**\n- `ut run clean-dist` - Clean all dist directories\n\n### Testing Commands\n\n- `ut run test` - **Run all tests (~2 minutes). NEVER CANCEL. Set timeout to 180+ seconds.**\n- `ut run test:cov` - **Run tests with coverage (~2 minutes). NEVER CANCEL. Set timeout to 180+ seconds.**\n- `ut run ci` - **Run tests with coverage (~2.1 minutes). NEVER CANCEL. Set timeout to 180+ seconds.**\n\n### Linting Commands\n\n- `ut run lint` - Run oxlint across all packages (~2 seconds)\n\n### Documentation Commands\n\n- `ut run site:dev` - Start documentation dev server (defaults to VitePress port 5173)\n- `ut run site:build` - **Build documentation site (~24 seconds). NEVER CANCEL. Set timeout to 60+ seconds.**\n\n### Example Applications (Currently Not Working)\n\n- `ut run example:dev:commonjs` - Start CommonJS example (has runtime issues)\n- `ut run example:dev:typescript` - Start TypeScript example (has runtime issues)\n\n## Package-Specific Commands\n\nRun commands for specific packages using `ut --filter=<package>`:\n\n```bash\n# Examples\nut --filter=egg run test\nut --filter=@eggjs/core run build\nut --filter=site run dev\n```\n\n## Development Workflow\n\n### 1. Making Changes\n\n- Work from the source tree first. Build when you need to validate package output, but do not run build immediately before unit tests.\n- Work primarily in `packages/egg/src/` for core framework features\n- Use TypeScript throughout - all packages are TypeScript-based\n- Follow the existing directory conventions in `packages/egg/src/`:\n  - `lib/` - Core classes (Application, Agent, EggApplicationCore)\n  - `app/extend/` - Framework extensions (context, helper, request, response)\n  - `config/` - Default configurations and plugins\n  - `lib/core/` - Core components (httpclient, logger, messenger)\n  - `lib/loader/` - Application loaders\n\n### 2. Validation Steps\n\n**Always perform these validation steps after making changes:**\n\n```bash\n# 1. Run lint to check code quality across all packages\nut run lint\n\n# 2. Run tests from a clean tree (some failures are expected in fresh environment)\nut run test\n\n# 3. Build all packages when build output or packaging behavior is relevant\nut run build\n\n# 4. Test documentation site when docs changed\nut run site:dev\n```\n\n### 3. Testing Strategy\n\n- **All packages use Vitest for testing** - this is the standard test runner\n- Test files follow pattern: `test/**/*.test.ts`\n- Use `import { describe, it } from 'vitest'` for test functions\n- Use Node.js built-in `assert` module for assertions\n- Create test fixtures in `packages/egg/test/fixtures/apps/` for scenario testing\n\n## Key Framework Concepts\n\n### Architecture\n\n- **EggApplicationCore** - Base application class with core functionality\n- **Application** - Main app class for worker processes\n- **Agent** - Agent process class for background tasks\n- **Context** - Extended Koa context with Egg-specific features\n- **BaseContextClass** - Base for controllers, services, subscriptions\n\n### Loading Convention (Automatic Discovery Order)\n\n1. Plugin system\n2. Configurations\n3. Application/Request/Response/Context extensions\n4. Custom loaders\n5. Services\n6. Middlewares\n7. Controllers\n8. Router\n\n### Cluster vs Single Mode\n\n- **Cluster Mode** (default) - Multi-process with master, agent, and worker processes\n- **Single Mode** - Single process for development/testing\n\n## Working with TypeScript\n\n- All packages use strict TypeScript mode\n- Uses tsdown for unbundled ESM builds (preserves file structure)\n- Each package has `tsdown.config.ts` for build configuration\n- **All sub-project tsconfig.json files MUST extend from root:** `\"extends\": \"../../tsconfig.json\"`\n- Root tsconfig.json includes all packages in `references` array\n\n## utoo Workspace & Catalog Dependencies\n\n- Dependencies defined in `.utoo.toml` catalog section\n- Reference catalog entries: `\"package-name\": \"catalog:\"`\n- Internal workspace dependencies: `\"package-name\": \"workspace:*\"`\n- This ensures consistent versions across all packages\n\n## Common Issues and Troubleshooting\n\n### Test Failures\n\n- Some tests may fail in fresh environments - this is normal\n- Focus on fixing only failures related to your changes\n- Examples may have runtime issues - don't use them for validation\n- If tegg tests fail with `duplicate proto` after a local build, remove stale\n  `dist/` directories outside fixtures and re-run tests. Built `dist/*.js`\n  files can be scanned alongside `src/*.ts`, loading the same decorated class\n  twice.\n\n### Build Issues\n\n- Run `ut run build` when validating build output, package exports, or changes\n  that affect generated artifacts.\n- TypeScript compilation errors will show clearly\n- Build warnings are generally acceptable\n\n### ESM/CommonJS Issues\n\n- Framework supports both ESM and CommonJS\n- Main package exports both formats\n- If you see \"ERR_UNKNOWN_FILE_EXTENSION\" errors, ensure packages are built first\n\n## File Locations Reference\n\n### Key Configuration Files\n\n- `package.json` / `.utoo.toml` - Workspace patterns and catalog configuration\n- `package.json` - Root monorepo scripts and devDependencies\n- `packages/egg/package.json` - Main framework package configuration\n- `packages/egg/tsdown.config.ts` - Build configuration\n- `packages/egg/src/config/plugin.ts` - Built-in plugin configurations\n- `packages/egg/src/config/config.default.ts` - Default framework configuration\n\n### Important Source Files\n\n- `packages/egg/src/lib/application.ts` - Main Application class\n- `packages/egg/src/lib/agent.ts` - Agent process manager\n- `packages/egg/src/lib/egg.ts` - Core EggApplicationCore\n- `packages/egg/src/lib/start.ts` - Application startup logic\n- `packages/egg/src/lib/loader/` - Convention-based loaders\n\n### Build Outputs\n\n- `packages/*/dist/` - Built JavaScript and TypeScript definitions\n- `site/dist/` - Built documentation site\n\n## Validation Scenarios\n\nAfter making changes, always verify:\n\n1. **Linting Passes**: `ut run lint` shows no new errors\n2. **Tests Run From Clean Sources**: `ut run test` executes without stale build artifacts interfering\n3. **Documentation Loads**: `ut run site:dev` starts successfully and the printed VitePress URL responds\n4. **Build Success When Relevant**: `ut run build` completes without errors\n\n**Remember**: This is a complex enterprise framework. Validate incrementally, keep unit tests isolated from stale build artifacts, and focus on the core packages (`egg`, `core`, `utils`) for most development work.\n\n## Commit Message Format\n\n**CRITICAL: All commits MUST follow the [Angular Commit Message Format](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines) as specified in CONTRIBUTING.md.**\n\n### Required Format Structure\n\n```\n<type>(<scope>): <subject>\n<BLANK LINE>\n<body>\n<BLANK LINE>\n<footer>\n```\n\n### Mandatory Types\n\n- **feat**: A new feature\n- **fix**: A bug fix\n- **docs**: Documentation-only changes\n- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)\n- **refactor**: A code change that neither fixes a bug nor adds a feature\n- **perf**: A code change that improves performance\n- **test**: Adding missing tests\n- **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation\n- **deps**: Updates about dependencies\n\n### Scope Guidelines\n\n- **Package-specific changes**: Use package names like `core`, `mock`, `cluster`, `utils`, `tsconfig`, `extend2`\n- **Cross-package changes**: Use feature areas like `loader`, `plugin`, `config`, `build`\n- **Component-specific**: Use component names like `application`, `agent`, `context`\n\n### Subject Rules\n\n- Use imperative, present tense: \"change\" not \"changed\" nor \"changes\"\n- Don't capitalize first letter\n- No period (.) at the end\n- Be succinct and descriptive\n\n### Examples\n\n```\nfeat(tsconfig): integrate package into monorepo with vitest\n\nMerge @eggjs/tsconfig repository into packages/tsconfig/ and refactor\nto use vitest testing framework instead of Node.js test runner.\n\n- Update all consuming packages to use workspace:* dependencies\n- Add vitest configuration and convert test assertions\n- Remove external catalog dependency in favor of workspace package\n\nCloses #123\n```\n\n```\nfix(core): resolve loader initialization race condition\n\nThe loader was attempting to initialize plugins before configurations\nwere fully loaded, causing intermittent startup failures.\n\nFixes #456\n```\n\n```\nchore: update dependencies to latest versions\n\nUpdate catalog dependencies and rebuild packages to ensure\ncompatibility with latest versions.\n```\n\n**NEVER commit without following this format - it breaks the project's automated changelog and release process.**\n","isInternal":false,"tokens":2563,"sizeBytes":11090},{"name":"context.md","path":"packages/koa/docs/api/context.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/koa/docs/api/context.md","title":"Api Skill","category":"anthropic-skill","format":"markdown","content":"# Context\n\nA Koa Context encapsulates node's `request` and `response` objects\ninto a single object which provides many helpful methods for writing\nweb applications and APIs.\nThese operations are used so frequently in HTTP server development\nthat they are added at this level instead of a higher level framework,\nwhich would force middleware to re-implement this common functionality.\n\nA `Context` is created _per_ request, and is referenced in middleware\nas the receiver, or the `ctx` identifier, as shown in the following\nsnippet:\n\n```js\napp.use(async (ctx) => {\n  ctx; // is the Context\n  ctx.request; // is a Koa Request\n  ctx.response; // is a Koa Response\n});\n```\n\nMany of the context's accessors and methods simply delegate to their `ctx.request` or `ctx.response`\nequivalents for convenience, and are otherwise identical. For example `ctx.type` and `ctx.length`\ndelegate to the `response` object, and `ctx.path` and `ctx.method` delegate to the `request`.\n\n## API\n\n`Context` specific methods and accessors.\n\n### ctx.req\n\nNode's `request` object.\n\n### ctx.res\n\nNode's `response` object.\n\nBypassing Koa's response handling is **not supported**. Avoid using the following node properties:\n\n- `res.statusCode`\n- `res.writeHead()`\n- `res.write()`\n- `res.end()`\n\n### ctx.request\n\nA Koa `Request` object.\n\n### ctx.response\n\nA Koa `Response` object.\n\n### ctx.state\n\nThe recommended namespace for passing information through middleware and to your frontend views.\n\n```js\nctx.state.user = await User.find(id);\n```\n\n### ctx.app\n\nApplication instance reference.\n\n### ctx.app.emit\n\nKoa applications extend an internal [EventEmitter](https://nodejs.org/dist/latest-v11.x/docs/api/events.html). `ctx.app.emit` emits an event with a type, defined by the first argument. For each event you can hook up \"listeners\", which is a function that is called when the event is emitted. Consult the [error handling docs](https://koajs.com/#error-handling) for more information.\n\n### ctx.cookies.get(name, [options])\n\nGet cookie `name` with `options`:\n\n- `signed` the cookie requested should be signed\n\nKoa uses the [cookies](https://github.com/pillarjs/cookies) module where options are simply passed.\n\n### ctx.cookies.set(name, value, [options])\n\nSet cookie `name` to `value` with `options`:\n\n- `maxAge`: a number representing the milliseconds from `Date.now()` for expiry.\n- `expires`: a `Date` object indicating the cookie's expiration date (expires at the end of session by default).\n- `path`: a string indicating the path of the cookie (`/` by default).\n- `domain`: a string indicating the domain of the cookie (no default).\n- `secure`: a boolean indicating whether the cookie is only to be sent over HTTPS (`false` by default for HTTP, `true` by default for HTTPS). [Read more about this option](https://github.com/pillarjs/cookies#secure-cookies).\n- `httpOnly`: a boolean indicating whether the cookie is only to be sent over HTTP(S), and not made available to client JavaScript (`true` by default).\n- `sameSite`: a boolean or string indicating whether the cookie is a \"same site\" cookie (`false` by default). This can be set to `'strict'`, `'lax'`, `'none'`, or `true` (which maps to `'strict'`).\n- `signed`: a boolean indicating whether the cookie is to be signed (`false` by default). If this is true, another cookie of the same name with the `.sig` suffix appended will also be sent, with a 27-byte url-safe base64 SHA1 value representing the hash of _cookie-name_=_cookie-value_ against the first [Keygrip](https://www.npmjs.com/package/keygrip) key. This signature key is used to detect tampering the next time a cookie is received.\n- `overwrite`: a boolean indicating whether to overwrite previously set cookies of the same name (`false` by default). If this is true, all cookies set during the same request with the same name (regardless of path or domain) are filtered out of the Set-Cookie header when setting this cookie.\n\nKoa uses the [cookies](https://github.com/pillarjs/cookies) module where options are simply passed.\n\n### ctx.throw([status], [msg], [properties])\n\nHelper method to throw an error with a `.status` property\ndefaulting to `500` that will allow Koa to respond appropriately.\nThe following combinations are allowed:\n\n```js\nctx.throw(400);\nctx.throw(400, 'name required');\nctx.throw(400, 'name required', { user: user });\n```\n\nFor example `ctx.throw(400, 'name required')` is equivalent to:\n\n```js\nconst err = new Error('name required');\nerr.status = 400;\nerr.expose = true;\nthrow err;\n```\n\nNote that these are user-level errors and are flagged with\n`err.expose` meaning the messages are appropriate for\nclient responses, which is typically not the case for\nerror messages since you do not want to leak failure\ndetails.\n\nYou may optionally pass a `properties` object which is merged into the error as-is, useful for decorating machine-friendly errors which are reported to the requester upstream.\n\n```js\nctx.throw(401, 'access_denied', { user: user });\n```\n\nKoa uses [http-errors](https://github.com/jshttp/http-errors) to create errors. `status` should only be passed as the first parameter.\n\n### ctx.assert(value, [status], [msg], [properties])\n\nHelper method to throw an error similar to `.throw()`\nwhen `!value`. Similar to node's [assert()](http://nodejs.org/api/assert.html)\nmethod.\n\n```js\nctx.assert(ctx.state.user, 401, 'User not found. Please login!');\n```\n\n### ctx.respond\n\nTo bypass Koa's built-in response handling, you may explicitly set `ctx.respond = false;`. Use this if you want to write to the raw `res` object instead of letting Koa handle the response for you.\n\nNote that using this is **not** supported by Koa. This may break intended functionality of Koa middleware and Koa itself. Using this property is considered a hack and is only a convenience to those wishing to use traditional `fn(req, res)` functions and middleware within Koa.\n\n## Request aliases\n\nThe following accessors and alias [Request](request.md) equivalents:\n\n- `ctx.header`\n- `ctx.headers`\n- `ctx.method`\n- `ctx.method=`\n- `ctx.url`\n- `ctx.url=`\n- `ctx.originalUrl`\n- `ctx.origin`\n- `ctx.href`\n- `ctx.path`\n- `ctx.path=`\n- `ctx.query`\n- `ctx.query=`\n- `ctx.querystring`\n- `ctx.querystring=`\n- `ctx.host`\n- `ctx.hostname`\n- `ctx.fresh`\n- `ctx.stale`\n- `ctx.socket`\n- `ctx.protocol`\n- `ctx.secure`\n- `ctx.ip`\n- `ctx.ips`\n- `ctx.subdomains`\n- `ctx.is()`\n- `ctx.accepts()`\n- `ctx.acceptsEncodings()`\n- `ctx.acceptsCharsets()`\n- `ctx.acceptsLanguages()`\n- `ctx.get()`\n\n## Response aliases\n\nThe following accessors and alias [Response](response.md) equivalents:\n\n- `ctx.body`\n- `ctx.body=`\n- `ctx.status`\n- `ctx.status=`\n- `ctx.message`\n- `ctx.message=`\n- `ctx.length=`\n- `ctx.length`\n- `ctx.type=`\n- `ctx.type`\n- `ctx.headerSent`\n- `ctx.redirect()`\n- `ctx.attachment()`\n- `ctx.set()`\n- `ctx.append()`\n- `ctx.remove()`\n- `ctx.lastModified=`\n- `ctx.etag=`\n","isInternal":false,"tokens":1710,"sizeBytes":6855},{"name":"index.md","path":"packages/koa/docs/api/index.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/koa/docs/api/index.md","title":"Api Skill","category":"anthropic-skill","format":"markdown","content":"# Installation\n\nKoa requires **node v7.6.0** or higher for ES2015 and async function support.\n\nYou can quickly install a supported version of node with your favorite version manager:\n\n```bash\n$ nvm install 7\n$ npm i koa\n$ node my-koa-app.js\n```\n\n# Application\n\nA Koa application is an object containing an array of middleware functions\nwhich are composed and executed in a stack-like manner upon request. Koa is similar to many\nother middleware systems that you may have encountered such as Ruby's Rack, Connect, and so on -\nhowever a key design decision was made to provide high level \"sugar\" at the otherwise low-level\nmiddleware layer. This improves interoperability, robustness, and makes writing middleware much\nmore enjoyable.\n\nThis includes methods for common tasks like content-negotiation, cache freshness, proxy support, and redirection\namong others. Despite supplying a reasonably large number of helpful methods Koa maintains a small footprint, as\nno middleware are bundled.\n\nThe obligatory hello world application:\n\n<!-- runkit:endpoint -->\n\n```js\nconst Koa = require('koa');\nconst app = new Koa();\n\napp.use(async (ctx) => {\n  ctx.body = 'Hello World';\n});\n\napp.listen(3000);\n```\n\n## Cascading\n\nKoa middleware cascade in a more traditional way as you may be used to with similar tools -\nthis was previously difficult to make user friendly with node's use of callbacks.\nHowever with async functions we can achieve \"true\" middleware. Contrasting Connect's implementation which\nsimply passes control through series of functions until one returns, Koa invoke \"downstream\", then\ncontrol flows back \"upstream\".\n\nThe following example responds with \"Hello World\", however first the request flows through\nthe `x-response-time` and `logging` middleware to mark when the request started, then yields control through the response middleware. When a middleware invokes `next()`\nthe function suspends and passes control to the next middleware defined. After there are no more\nmiddleware to execute downstream, the stack will unwind and each middleware is resumed to perform\nits upstream behaviour.\n\n<!-- runkit:endpoint -->\n\n```js\nconst Koa = require('koa');\nconst app = new Koa();\n\n// logger\n\napp.use(async (ctx, next) => {\n  await next();\n  const rt = ctx.response.get('X-Response-Time');\n  console.log(`${ctx.method} ${ctx.url} - ${rt}`);\n});\n\n// x-response-time\n\napp.use(async (ctx, next) => {\n  const start = Date.now();\n  await next();\n  const ms = Date.now() - start;\n  ctx.set('X-Response-Time', `${ms}ms`);\n});\n\n// response\n\napp.use(async (ctx) => {\n  ctx.body = 'Hello World';\n});\n\napp.listen(3000);\n```\n\n## Settings\n\nApplication settings are properties on the `app` instance, currently\nthe following are supported:\n\n- `app.env` defaulting to the **NODE_ENV** or \"development\"\n- `app.keys` array of signed cookie keys\n- `app.proxy` when true proxy header fields will be trusted\n- `app.subdomainOffset` offset of `.subdomains` to ignore, default to 2\n- `app.proxyIpHeader` proxy ip header, default to `X-Forwarded-For`\n- `app.maxIpsCount` max ips read from proxy ip header, default to 0 (means infinity)\n\nYou can pass the settings to the constructor:\n\n```js\nconst Koa = require('koa');\nconst app = new Koa({ proxy: true });\n```\n\nor dynamically:\n\n```js\nconst Koa = require('koa');\nconst app = new Koa();\napp.proxy = true;\n```\n\n## app.listen(...)\n\nA Koa application is not a 1-to-1 representation of an HTTP server.\nOne or more Koa applications may be mounted together to form larger\napplications with a single HTTP server.\n\nCreate and return an HTTP server, passing the given arguments to\n`Server#listen()`. These arguments are documented on [nodejs.org](http://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback). The following is a useless Koa application bound to port `3000`:\n\n```js\nconst Koa = require('koa');\nconst app = new Koa();\napp.listen(3000);\n```\n\nThe `app.listen(...)` method is simply sugar for the following:\n\n```js\nconst http = require('http');\nconst Koa = require('koa');\nconst app = new Koa();\nhttp.createServer(app.callback()).listen(3000);\n```\n\nThis means you can spin up the same application as both HTTP and HTTPS\nor on multiple addresses:\n\n```js\nconst http = require('http');\nconst https = require('https');\nconst Koa = require('koa');\nconst app = new Koa();\nhttp.createServer(app.callback()).listen(3000);\nhttps.createServer(app.callback()).listen(3001);\n```\n\n## app.callback()\n\nReturn a callback function suitable for the `http.createServer()`\nmethod to handle a request.\nYou may also use this callback function to mount your Koa app in a\nConnect/Express app.\n\n## app.use(function)\n\nAdd the given middleware function to this application.\n`app.use()` returns `this`, so is chainable.\n\n```js\napp.use(someMiddleware);\napp.use(someOtherMiddleware);\napp.listen(3000);\n```\n\nIs the same as\n\n```js\napp.use(someMiddleware).use(someOtherMiddleware).listen(3000);\n```\n\nSee [Middleware](https://github.com/koajs/koa/wiki#middleware) for\nmore information.\n\n## app.keys=\n\nSet signed cookie keys.\n\nThese are passed to [KeyGrip](https://github.com/crypto-utils/keygrip),\nhowever you may also pass your own `KeyGrip` instance. For\nexample the following are acceptable:\n\n```js\napp.keys = [\n  'OEK5zjaAMPc3L6iK7PyUjCOziUH3rsrMKB9u8H07La1SkfwtuBoDnHaaPCkG5Brg',\n  'MNKeIebviQnCPo38ufHcSfw3FFv8EtnAe1xE02xkN1wkCV1B2z126U44yk2BQVK7',\n];\napp.keys = new KeyGrip(\n  [\n    'OEK5zjaAMPc3L6iK7PyUjCOziUH3rsrMKB9u8H07La1SkfwtuBoDnHaaPCkG5Brg',\n    'MNKeIebviQnCPo38ufHcSfw3FFv8EtnAe1xE02xkN1wkCV1B2z126U44yk2BQVK7',\n  ],\n  'sha256',\n);\n```\n\nFor security reasons, please ensure that the key is long enough and random.\n\nThese keys may be rotated and are used when signing cookies\nwith the `{ signed: true }` option:\n\n```js\nctx.cookies.set('name', 'tobi', { signed: true });\n```\n\n## app.context\n\n`app.context` is the prototype from which `ctx` is created.\nYou may add additional properties to `ctx` by editing `app.context`.\nThis is useful for adding properties or methods to `ctx` to be used across your entire app,\nwhich may be more performant (no middleware) and/or easier (fewer `require()`s)\nat the expense of relying more on `ctx`, which could be considered an anti-pattern.\n\nFor example, to add a reference to your database from `ctx`:\n\n```js\napp.context.db = db();\n\napp.use(async (ctx) => {\n  console.log(ctx.db);\n});\n```\n\nNote:\n\n- Many properties on `ctx` are defined using getters, setters, and `Object.defineProperty()`. You can only edit these properties (not recommended) by using `Object.defineProperty()` on `app.context`. See https://github.com/koajs/koa/issues/652.\n- Mounted apps currently use their parent's `ctx` and settings. Thus, mounted apps are really just groups of middleware.\n\n## Error Handling\n\nBy default outputs all errors to stderr unless `app.silent` is `true`.\nThe default error handler also won't output errors when `err.status` is `404` or `err.expose` is `true`.\nTo perform custom error-handling logic such as centralized logging you can add an \"error\" event listener:\n\n```js\napp.on('error', (err) => {\n  log.error('server error', err);\n});\n```\n\nIf an error is in the req/res cycle and it is _not_ possible to respond to the client, the `Context` instance is also passed:\n\n```js\napp.on('error', (err, ctx) => {\n  log.error('server error', err, ctx);\n});\n```\n\nWhen an error occurs _and_ it is still possible to respond to the client, aka no data has been written to the socket, Koa will respond\nappropriately with a 500 \"Internal Server Error\". In either case\nan app-level \"error\" is emitted for logging purposes.\n","isInternal":false,"tokens":1943,"sizeBytes":7576},{"name":"request.md","path":"packages/koa/docs/api/request.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/koa/docs/api/request.md","title":"Api Skill","category":"anthropic-skill","format":"markdown","content":"# Request\n\nA Koa `Request` object is an abstraction on top of node's vanilla request object,\nproviding additional functionality that is useful for every day HTTP server\ndevelopment.\n\n## API\n\n### request.header\n\nRequest header object. This is the same as the [`headers`](https://nodejs.org/api/http.html#http_message_headers) field\non node's [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage).\n\n### request.header=\n\nSet request header object.\n\n### request.headers\n\nRequest header object. Alias as `request.header`.\n\n### request.headers=\n\nSet request header object. Alias as `request.header=`.\n\n### request.method\n\nRequest method.\n\n### request.method=\n\nSet request method, useful for implementing middleware\nsuch as `methodOverride()`.\n\n### request.length\n\nReturn request Content-Length as a number when present, or `undefined`.\n\n### request.url\n\nGet request URL.\n\n### request.url=\n\nSet request URL, useful for url rewrites.\n\n### request.originalUrl\n\nGet request original URL.\n\n### request.origin\n\nGet origin of URL, include `protocol` and `host`.\n\n```js\nctx.request.origin;\n// => http://example.com\n```\n\n### request.href\n\nGet full request URL, include `protocol`, `host` and `url`.\n\n```js\nctx.request.href;\n// => http://example.com/foo/bar?q=1\n```\n\n### request.path\n\nGet request pathname.\n\n### request.path=\n\nSet request pathname and retain query-string when present.\n\n### request.querystring\n\nGet raw query string void of `?`.\n\n### request.querystring=\n\nSet raw query string.\n\n### request.search\n\nGet raw query string with the `?`.\n\n### request.search=\n\nSet raw query string.\n\n### request.host\n\nGet host (hostname:port) when present. Supports `X-Forwarded-Host`\nwhen `app.proxy` is **true**, otherwise `Host` is used.\n\n### request.hostname\n\nGet hostname when present. Supports `X-Forwarded-Host`\nwhen `app.proxy` is **true**, otherwise `Host` is used.\n\nIf host is IPv6, Koa delegates parsing to\n[WHATWG URL API](https://nodejs.org/dist/latest-v8.x/docs/api/url.html#url_the_whatwg_url_api),\n_Note_ This may impact performance.\n\n### request.URL\n\nGet WHATWG parsed URL object.\n\n### request.type\n\nGet request `Content-Type` void of parameters such as \"charset\".\n\n```js\nconst ct = ctx.request.type;\n// => \"image/png\"\n```\n\n### request.charset\n\nGet request charset when present, or `undefined`:\n\n```js\nctx.request.charset;\n// => \"utf-8\"\n```\n\n### request.query\n\nGet parsed query-string, returning an empty object when no\nquery-string is present. Note that this getter does _not_\nsupport nested parsing.\n\nFor example \"color=blue&size=small\":\n\n```js\n{\n  color: 'blue',\n  size: 'small'\n}\n```\n\n### request.query=\n\nSet query-string to the given object. Note that this\nsetter does _not_ support nested objects.\n\n```js\nctx.query = { next: '/login' };\n```\n\n### request.fresh\n\nCheck if a request cache is \"fresh\", aka the contents have not changed. This\nmethod is for cache negotiation between `If-None-Match` / `ETag`, and `If-Modified-Since` and `Last-Modified`. It should be referenced after setting one or more of these response headers.\n\n```js\n// freshness check requires status 20x or 304\nctx.status = 200;\nctx.set('ETag', '123');\n\n// cache is ok\nif (ctx.fresh) {\n  ctx.status = 304;\n  return;\n}\n\n// cache is stale\n// fetch new data\nctx.body = await db.find('something');\n```\n\n### request.stale\n\nInverse of `request.fresh`.\n\n### request.protocol\n\nReturn request protocol, \"https\" or \"http\". Supports `X-Forwarded-Proto`\nwhen `app.proxy` is **true**.\n\n### request.secure\n\nShorthand for `ctx.protocol == \"https\"` to check if a request was\nissued via TLS.\n\n### request.ip\n\nRequest remote address. Supports `X-Forwarded-For` when `app.proxy`\nis **true**.\n\n### request.ips\n\nWhen `X-Forwarded-For` is present and `app.proxy` is enabled an array\nof these ips is returned, ordered from upstream -> downstream. When\ndisabled an empty array is returned.\n\nFor example if the value were \"client, proxy1, proxy2\",\nyou would receive the array `[\"client\", \"proxy1\", \"proxy2\"]`.\n\nMost of the reverse proxy(nginx) set x-forwarded-for via\n`proxy_add_x_forwarded_for`, which poses a certain security risk.\nA malicious attacker can forge a client's ip address by forging\na `X-Forwarded-For`request header. The request sent by the client\nhas an `X-Forwarded-For` request header for 'forged'. After being\nforwarded by the reverse proxy, `request.ips` will be\n['forged', 'client', 'proxy1', 'proxy2'].\n\nKoa offers two options to avoid being bypassed.\n\nIf you can control the reverse proxy, you can avoid bypassing\nby adjusting the configuration, or use the `app.proxyIpHeader`\nprovided by koa to avoid reading `x-forwarded-for` to get ips.\n\n```js\nconst app = new Koa({\n  proxy: true,\n  proxyIpHeader: 'X-Real-IP',\n});\n```\n\nIf you know exactly how many reverse proxies are in front of\nthe server, you can avoid reading the user's forged request\nheader by configuring `app.maxIpsCount`:\n\n```js\nconst app = new Koa({\n  proxy: true,\n  maxIpsCount: 1, // only one proxy in front of the server\n});\n\n// request.header['X-Forwarded-For'] === [ '127.0.0.1', '127.0.0.2' ];\n// ctx.ips === [ '127.0.0.2' ];\n```\n\n### request.subdomains\n\nReturn subdomains as an array.\n\nSubdomains are the dot-separated parts of the host before the main domain of\nthe app. By default, the domain of the app is assumed to be the last two\nparts of the host. This can be changed by setting `app.subdomainOffset`.\n\nFor example, if the domain is \"tobi.ferrets.example.com\":\nIf `app.subdomainOffset` is not set, `ctx.subdomains` is `[\"ferrets\", \"tobi\"]`.\nIf `app.subdomainOffset` is 3, `ctx.subdomains` is `[\"tobi\"]`.\n\n### request.is(types...)\n\nCheck if the incoming request contains the \"Content-Type\"\nheader field, and it contains any of the give mime `type`s.\nIf there is no request body, `null` is returned.\nIf there is no content type, or the match fails `false` is returned.\nOtherwise, it returns the matching content-type.\n\n```js\n// With Content-Type: text/html; charset=utf-8\nctx.is('html'); // => 'html'\nctx.is('text/html'); // => 'text/html'\nctx.is('text/*', 'text/html'); // => 'text/html'\n\n// When Content-Type is application/json\nctx.is('json', 'urlencoded'); // => 'json'\nctx.is('application/json'); // => 'application/json'\nctx.is('html', 'application/*'); // => 'application/json'\n\nctx.is('html'); // => false\n```\n\nFor example if you want to ensure that\nonly images are sent to a given route:\n\n```js\nif (ctx.is('image/*')) {\n  // process\n} else {\n  ctx.throw(415, 'images only!');\n}\n```\n\n### Content Negotiation\n\nKoa's `request` object includes helpful content negotiation utilities powered by [accepts](http://github.com/expressjs/accepts) and [negotiator](https://github.com/federomero/negotiator). These utilities are:\n\n- `request.accepts(types)`\n- `request.acceptsEncodings(types)`\n- `request.acceptsCharsets(charsets)`\n- `request.acceptsLanguages(langs)`\n\nIf no types are supplied, **all** acceptable types are returned.\n\nIf multiple types are supplied, the best match will be returned. If no matches are found, a `false` is returned, and you should send a `406 \"Not Acceptable\"` response to the client.\n\nIn the case of missing accept headers where any type is acceptable, the first type will be returned. Thus, the order of types you supply is important.\n\n### request.accepts(types)\n\nCheck if the given `type(s)` is acceptable, returning the best match when true, otherwise `false`. The `type` value may be one or more mime type string\nsuch as \"application/json\", the extension name\nsuch as \"json\", or an array `[\"json\", \"html\", \"text/plain\"]`.\n\n```js\n// Accept: text/html\nctx.accepts('html');\n// => \"html\"\n\n// Accept: text/*, application/json\nctx.accepts('html');\n// => \"html\"\nctx.accepts('text/html');\n// => \"text/html\"\nctx.accepts('json', 'text');\n// => \"json\"\nctx.accepts('application/json');\n// => \"application/json\"\n\n// Accept: text/*, application/json\nctx.accepts('image/png');\nctx.accepts('png');\n// => false\n\n// Accept: text/*;q=.5, application/json\nctx.accepts(['html', 'json']);\nctx.accepts('html', 'json');\n// => \"json\"\n\n// No Accept header\nctx.accepts('html', 'json');\n// => \"html\"\nctx.accepts('json', 'html');\n// => \"json\"\n```\n\nYou may call `ctx.accepts()` as many times as you like,\nor use a switch:\n\n```js\nswitch (ctx.accepts('json', 'html', 'text')) {\n  case 'json':\n    break;\n  case 'html':\n    break;\n  case 'text':\n    break;\n  default:\n    ctx.throw(406, 'json, html, or text only');\n}\n```\n\n### request.acceptsEncodings(encodings)\n\nCheck if `encodings` are acceptable, returning the best match when true, otherwise `false`. Note that you should include `identity` as one of the encodings!\n\n```js\n// Accept-Encoding: gzip\nctx.acceptsEncodings('gzip', 'deflate', 'identity');\n// => \"gzip\"\n\nctx.acceptsEncodings(['gzip', 'deflate', 'identity']);\n// => \"gzip\"\n```\n\nWhen no arguments are given all accepted encodings\nare returned as an array:\n\n```js\n// Accept-Encoding: gzip, deflate\nctx.acceptsEncodings();\n// => [\"gzip\", \"deflate\", \"identity\"]\n```\n\nNote that the `identity` encoding (which means no encoding) could be unacceptable if the client explicitly sends `identity;q=0`. Although this is an edge case, you should still handle the case where this method returns `false`.\n\n### request.acceptsCharsets(charsets)\n\nCheck if `charsets` are acceptable, returning\nthe best match when true, otherwise `false`.\n\n```js\n// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5\nctx.acceptsCharsets('utf-8', 'utf-7');\n// => \"utf-8\"\n\nctx.acceptsCharsets(['utf-7', 'utf-8']);\n// => \"utf-8\"\n```\n\nWhen no arguments are given all accepted charsets\nare returned as an array:\n\n```js\n// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5\nctx.acceptsCharsets();\n// => [\"utf-8\", \"utf-7\", \"iso-8859-1\"]\n```\n\n### request.acceptsLanguages(langs)\n\nCheck if `langs` are acceptable, returning\nthe best match when true, otherwise `false`.\n\n```js\n// Accept-Language: en;q=0.8, es, pt\nctx.acceptsLanguages('es', 'en');\n// => \"es\"\n\nctx.acceptsLanguages(['en', 'es']);\n// => \"es\"\n```\n\nWhen no arguments are given all accepted languages\nare returned as an array:\n\n```js\n// Accept-Language: en;q=0.8, es, pt\nctx.acceptsLanguages();\n// => [\"es\", \"pt\", \"en\"]\n```\n\n### request.idempotent\n\nCheck if the request is idempotent.\n\n### request.socket\n\nReturn the request socket.\n\n### request.get(field)\n\nReturn request header with case-insensitive `field`.\n","isInternal":false,"tokens":2716,"sizeBytes":10331},{"name":"response.md","path":"packages/koa/docs/api/response.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/koa/docs/api/response.md","title":"Api Skill","category":"anthropic-skill","format":"markdown","content":"# Response\n\nA Koa `Response` object is an abstraction on top of node's vanilla response object,\nproviding additional functionality that is useful for every day HTTP server\ndevelopment.\n\n## API\n\n### response.header\n\nResponse header object.\n\n### response.headers\n\nResponse header object. Alias as `response.header`.\n\n### response.socket\n\nResponse socket. Points to net.Socket instance as `request.socket`.\n\n### response.status\n\nGet response status. By default, `response.status` is set to `404` unlike node's `res.statusCode` which defaults to `200`.\n\n### response.status=\n\nSet response status via numeric code:\n\n- 100 \"continue\"\n- 101 \"switching protocols\"\n- 102 \"processing\"\n- 200 \"ok\"\n- 201 \"created\"\n- 202 \"accepted\"\n- 203 \"non-authoritative information\"\n- 204 \"no content\"\n- 205 \"reset content\"\n- 206 \"partial content\"\n- 207 \"multi-status\"\n- 208 \"already reported\"\n- 226 \"im used\"\n- 300 \"multiple choices\"\n- 301 \"moved permanently\"\n- 302 \"found\"\n- 303 \"see other\"\n- 304 \"not modified\"\n- 305 \"use proxy\"\n- 307 \"temporary redirect\"\n- 308 \"permanent redirect\"\n- 400 \"bad request\"\n- 401 \"unauthorized\"\n- 402 \"payment required\"\n- 403 \"forbidden\"\n- 404 \"not found\"\n- 405 \"method not allowed\"\n- 406 \"not acceptable\"\n- 407 \"proxy authentication required\"\n- 408 \"request timeout\"\n- 409 \"conflict\"\n- 410 \"gone\"\n- 411 \"length required\"\n- 412 \"precondition failed\"\n- 413 \"payload too large\"\n- 414 \"uri too long\"\n- 415 \"unsupported media type\"\n- 416 \"range not satisfiable\"\n- 417 \"expectation failed\"\n- 418 \"I'm a teapot\"\n- 422 \"unprocessable entity\"\n- 423 \"locked\"\n- 424 \"failed dependency\"\n- 426 \"upgrade required\"\n- 428 \"precondition required\"\n- 429 \"too many requests\"\n- 431 \"request header fields too large\"\n- 500 \"internal server error\"\n- 501 \"not implemented\"\n- 502 \"bad gateway\"\n- 503 \"service unavailable\"\n- 504 \"gateway timeout\"\n- 505 \"http version not supported\"\n- 506 \"variant also negotiates\"\n- 507 \"insufficient storage\"\n- 508 \"loop detected\"\n- 510 \"not extended\"\n- 511 \"network authentication required\"\n\n**NOTE**: don't worry too much about memorizing these strings,\nif you have a typo an error will be thrown, displaying this list\nso you can make a correction.\n\nSince `response.status` default is set to `404`, to send a response\nwithout a body and with a different status is to be done like this:\n\n```js\nctx.response.status = 200;\n\n// Or whatever other status\nctx.response.status = 204;\n```\n\n### response.message\n\nGet response status message. By default, `response.message` is\nassociated with `response.status`.\n\n### response.message=\n\nSet response status message to the given value.\n\n### response.length=\n\nSet response Content-Length to the given value.\n\n### response.length\n\nReturn response Content-Length as a number when present, or deduce\nfrom `ctx.body` when possible, or `undefined`.\n\n### response.body\n\nGet response body.\n\n### response.body=\n\nSet response body to one of the following:\n\n- `string` written\n- `Buffer` written\n- `Stream` piped\n- `Object` || `Array` json-stringified\n- `null` || `undefined` no content response\n\nIf `response.status` has not been set, Koa will automatically set the status to `200` or `204` depending on `response.body`. Specifically, if `response.body` has not been set or has been set as `null` or `undefined`, Koa will automatically set `response.status` to `204`. If you really want to send no content response with other status, you should override the `204` status as the following way:\n\n```js\n// This must be always set first before status, since null | undefined\n// body automatically sets the status to 204\nctx.body = null;\n// Now we override the 204 status with the desired one\nctx.status = 200;\n```\n\nKoa doesn't guard against everything that could be put as a response body -- a function doesn't serialise meaningfully, returning a boolean may make sense based on your application, and while an error works, it may not work as intended as some properties of an error are not enumerable. We recommend adding middleware in your app that asserts body types per app. A sample middleware might be:\n\n```js\napp.use(async (ctx, next) => {\n  await next();\n\n  ctx.assert.equal('object', typeof ctx.body, 500, 'some dev did something wrong');\n});\n```\n\n#### String\n\nThe Content-Type is defaulted to text/html or text/plain, both with\na default charset of utf-8. The Content-Length field is also set.\n\n#### Buffer\n\nThe Content-Type is defaulted to application/octet-stream, and Content-Length\nis also set.\n\n#### Stream\n\nThe Content-Type is defaulted to application/octet-stream.\n\nWhenever a stream is set as the response body, `.onerror` is automatically added as a listener to the `error` event to catch any errors.\nIn addition, whenever the request is closed (even prematurely), the stream is destroyed.\nIf you do not want these two features, do not set the stream as the body directly.\nFor example, you may not want this when setting the body as an HTTP stream in a proxy as it would destroy the underlying connection.\n\nSee: https://github.com/koajs/koa/pull/612 for more information.\n\nHere's an example of stream error handling without automatically destroying the stream:\n\n```js\nconst PassThrough = require('stream').PassThrough;\n\napp.use(async (ctx) => {\n  ctx.body = someHTTPStream.on('error', (err) => ctx.onerror(err)).pipe(PassThrough());\n});\n```\n\n#### Object\n\nThe Content-Type is defaulted to application/json. This includes plain objects `{ foo: 'bar' }` and arrays `['foo', 'bar']`.\n\n### response.get(field)\n\nGet a response header field value with case-insensitive `field`.\n\n```js\nconst etag = ctx.response.get('ETag');\n```\n\n### response.has(field)\n\nReturns `true` if the header identified by name is currently set in the outgoing headers.\nThe header name matching is case-insensitive.\n\n```js\nconst rateLimited = ctx.response.has('X-RateLimit-Limit');\n```\n\n### response.set(field, value)\n\nSet response header `field` to `value`:\n\n```js\nctx.set('Cache-Control', 'no-cache');\n```\n\n### response.append(field, value)\n\nAppend additional header `field` with value `val`.\n\n```js\nctx.append('Link', '<http://127.0.0.1/>');\n```\n\n### response.set(fields)\n\nSet several response header `fields` with an object:\n\n```js\nctx.set({\n  Etag: '1234',\n  'Last-Modified': date,\n});\n```\n\nThis delegates to [setHeader](https://nodejs.org/dist/latest/docs/api/http.html#http_request_setheader_name_value) which sets or updates headers by specified keys and doesn't reset the entire header.\n\n### response.remove(field)\n\nRemove header `field`.\n\n### response.type\n\nGet response `Content-Type` void of parameters such as \"charset\".\n\n```js\nconst ct = ctx.type;\n// => \"image/png\"\n```\n\n### response.type=\n\nSet response `Content-Type` via mime string or file extension.\n\n```js\nctx.type = 'text/plain; charset=utf-8';\nctx.type = 'image/png';\nctx.type = '.png';\nctx.type = 'png';\n```\n\nNote: when appropriate a `charset` is selected for you, for\nexample `response.type = 'html'` will default to \"utf-8\". If you need to overwrite `charset`,\nuse `ctx.set('Content-Type', 'text/html')` to set response header field to value directly.\n\n### response.is(types...)\n\nVery similar to `ctx.request.is()`.\nCheck whether the response type is one of the supplied types.\nThis is particularly useful for creating middleware that\nmanipulate responses.\n\nFor example, this is a middleware that minifies\nall HTML responses except for streams.\n\n```js\nconst minify = require('html-minifier');\n\napp.use(async (ctx, next) => {\n  await next();\n\n  if (!ctx.response.is('html')) return;\n\n  let body = ctx.body;\n  if (!body || body.pipe) return;\n\n  if (Buffer.isBuffer(body)) body = body.toString();\n  ctx.body = minify(body);\n});\n```\n\n### response.redirect(url, [alt])\n\nPerform a [302] redirect to `url`.\n\nThe string \"back\" is special-cased\nto provide Referrer support, when Referrer\nis not present `alt` or \"/\" is used.\n\n```js\nctx.redirect('back');\nctx.redirect('back', '/index.html');\nctx.redirect('/login');\nctx.redirect('http://google.com');\n```\n\nTo alter the default status of `302`, simply assign the status\nbefore or after this call. To alter the body, assign it after this call:\n\n```js\nctx.status = 301;\nctx.redirect('/cart');\nctx.body = 'Redirecting to shopping cart';\n```\n\n### response.attachment([filename], [options])\n\nSet `Content-Disposition` to \"attachment\" to signal the client\nto prompt for download. Optionally specify the `filename` of the\ndownload and some [options](https://github.com/jshttp/content-disposition#options).\n\n### response.headerSent\n\nCheck if a response header has already been sent. Useful for seeing\nif the client may be notified on error.\n\n### response.lastModified\n\nReturn the `Last-Modified` header as a `Date`, if it exists.\n\n### response.lastModified=\n\nSet the `Last-Modified` header as an appropriate UTC string.\nYou can either set it as a `Date` or date string.\n\n```js\nctx.response.lastModified = new Date();\n```\n\n### response.etag=\n\nSet the ETag of a response including the wrapped `\"`s.\nNote that there is no corresponding `response.etag` getter.\n\n```js\nctx.response.etag = crypto.createHash('md5').update(ctx.body).digest('hex');\n```\n\n### response.vary(field)\n\nVary on `field`.\n\n### response.flushHeaders()\n\nFlush any set headers, and begin the body.\n","isInternal":false,"tokens":2243,"sizeBytes":9193},{"name":"error-handling.md","path":"packages/koa/docs/error-handling.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/koa/docs/error-handling.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Error Handling\n\n## Try-Catch\n\nUsing async functions means that you can try-catch `next`.\nThis example adds a `.status` to all errors:\n\n```js\napp.use(async (ctx, next) => {\n  try {\n    await next();\n  } catch (err) {\n    err.status = err.statusCode || err.status || 500;\n    throw err;\n  }\n});\n```\n\n### Default Error Handler\n\nThe default error handler is essentially a `try-catch` at\nthe very beginning of the middleware chain. To use a\ndifferent error handler, simply put another `try-catch` at\nthe beginning of the middleware chain, and handle the error\nthere. However, the default error handler is good enough for\nmost use cases. It will use a status code of `err.status`,\nor by default 500. If `err.expose` is true, then `err.message`\nwill be the reply. Otherwise, a message generated from the\nerror code will be used (e.g. for the code 500 the message\n\"Internal Server Error\" will be used). All headers will be\ncleared from the request, but any headers in `err.headers`\nwill then be set. You can use a `try-catch`, as specified\nabove, to add a header to this list.\n\nHere is an example of creating your own error handler:\n\n```js\napp.use(async (ctx, next) => {\n  try {\n    await next();\n  } catch (err) {\n    // will only respond with JSON\n    ctx.status = err.statusCode || err.status || 500;\n    ctx.body = {\n      message: err.message,\n    };\n  }\n});\n```\n\n## The Error Event\n\nError event listeners can be specified with `app.on('error')`.\nIf no error listener is specified, a default error listener\nis used. Error listeners receive all errors that make their\nway back through the middleware chain, if an error is caught\nand not thrown again, it will not be passed to the error\nlistener. If no error event listener is specified, then\n`app.onerror` will be used, which simply log the error unless\n`error.expose`is true or `app.silent` is true or `error.status`\nis 404.\n","isInternal":false,"tokens":470,"sizeBytes":1874},{"name":"faq.md","path":"packages/koa/docs/faq.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/koa/docs/faq.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Frequently Asked Questions\n\n## Does Koa replace Express?\n\nIt's more like Connect, but a lot of the Express goodies\nwere moved to the middleware level in Koa to help form\na stronger foundation. This makes middleware more enjoyable\nand less error-prone to write, for the entire stack, not\njust the end application code.\n\nTypically many middleware would\nre-implement similar features, or even worse incorrectly implement them,\nwhen features like signed cookie secrets among others are typically application-specific,\nnot middleware specific.\n\n## Does Koa replace Connect?\n\nNo, just a different take on similar functionality\nnow that async functions allow us to write code with fewer\ncallbacks. Connect is equally capable, and some may still prefer it,\nit's up to what you prefer.\n\n## Does Koa include routing?\n\nNo - out of the box Koa has no form of routing, however\nmany routing middleware are available: https://github.com/koajs/koa/wiki\n\n## Why isn't Koa just Express 4.0?\n\nKoa is a pretty large departure from what people know about Express,\nthe design is fundamentally much different, so the migration from\nExpress 3.0 to this Express 4.0 would effectively mean rewriting\nthe entire application, so we thought it would be more appropriate\nto create a new library.\n\n## What custom properties do the Koa objects have?\n\nKoa uses its own custom objects: `ctx`, `ctx.request`, and `ctx.response`.\nThese objects abstract node's `req` and `res` objects with convenience methods and getters/setters.\nGenerally, properties added to these objects must obey the following rules:\n\n- They must be either very commonly used and/or must do something useful\n- If a property exists as a setter, then it will also exist as a getter, but not vice versa\n\nMany of `ctx.request` and `ctx.response`'s properties are delegated to `ctx`.\nIf it's a getter/setter, then both the getter and the setter will strictly\ncorrespond to either `ctx.request` or `ctx.response`.\n\nPlease think about these rules before suggesting additional properties.\n","isInternal":false,"tokens":436,"sizeBytes":2019},{"name":"guide.md","path":"packages/koa/docs/guide.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/koa/docs/guide.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Guide\n\nThis guide covers Koa topics that are not directly API related, such as best practices for writing middleware and application structure suggestions. In these examples we use async functions as middleware - you can also use commonFunction or generatorFunction which will be a little different.\n\n## Table of Contents\n\n- [Guide](#guide)\n  - [Table of Contents](#table-of-contents)\n  - [Writing Middleware](#writing-middleware)\n  - [Middleware Best Practices](#middleware-best-practices)\n    - [Middleware options](#middleware-options)\n    - [Named middleware](#named-middleware)\n    - [Combining multiple middleware with koa-compose](#combining-multiple-middleware-with-koa-compose)\n    - [Response Middleware](#response-middleware)\n  - [Async operations](#async-operations)\n  - [Debugging Koa](#debugging-koa)\n\n## Writing Middleware\n\nKoa middleware are simple functions which return a `MiddlewareFunction` with signature (ctx, next). When\nthe middleware is run, it must manually invoke `next()` to run the \"downstream\" middleware.\n\nFor example if you wanted to track how long it takes for a request to propagate through Koa by adding an\n`X-Response-Time` header field the middleware would look like the following:\n\n```js\nasync function responseTime(ctx, next) {\n  const start = Date.now();\n  await next();\n  const ms = Date.now() - start;\n  ctx.set('X-Response-Time', `${ms}ms`);\n}\n\napp.use(responseTime);\n```\n\nIf you're a front-end developer you can think any code before `next();` as the \"capture\" phase,\nwhile any code after is the \"bubble\" phase. This crude gif illustrates how async function allow us\nto properly utilize stack flow to implement request and response flows:\n\n![Koa middleware](/docs/middleware.gif)\n\n1.  Create a date to track response time\n2.  Await control to the next middleware\n3.  Create another date to track duration\n4.  Await control to the next middleware\n5.  Set the response body to \"Hello World\"\n6.  Calculate duration time\n7.  Output log line\n8.  Calculate response time\n9.  Set `X-Response-Time` header field\n10. Hand off to Koa to handle the response\n\nNext we'll look at the best practices for creating Koa middleware.\n\n## Middleware Best Practices\n\nThis section covers middleware authoring best practices, such as middleware\naccepting options, named middleware for debugging, among others.\n\n### Middleware options\n\nWhen creating a public middleware, it's useful to conform to the convention of\nwrapping the middleware in a function that accepts options, allowing users to\nextend functionality. Even if your middleware accepts _no_ options, this is still\na good idea to keep things uniform.\n\nHere our contrived `logger` middleware accepts a `format` string for customization,\nand returns the middleware itself:\n\n```js\nfunction logger(format) {\n  format = format || ':method \":url\"';\n\n  return async function (ctx, next) {\n    const str = format.replace(':method', ctx.method).replace(':url', ctx.url);\n\n    console.log(str);\n\n    await next();\n  };\n}\n\napp.use(logger());\napp.use(logger(':method :url'));\n```\n\n### Named middleware\n\nNaming middleware is optional, however it's useful for debugging purposes to assign a name.\n\n```js\nfunction logger(format) {\n  return async function logger(ctx, next) {};\n}\n```\n\n### Combining multiple middleware with koa-compose\n\nSometimes you want to \"compose\" multiple middleware into a single middleware for easy re-use or exporting. You can use [koa-compose](https://github.com/koajs/compose)\n\n```js\nconst compose = require('koa-compose');\n\nasync function random(ctx, next) {\n  if ('/random' == ctx.path) {\n    ctx.body = Math.floor(Math.random() * 10);\n  } else {\n    await next();\n  }\n}\n\nasync function backwards(ctx, next) {\n  if ('/backwards' == ctx.path) {\n    ctx.body = 'sdrawkcab';\n  } else {\n    await next();\n  }\n}\n\nasync function pi(ctx, next) {\n  if ('/pi' == ctx.path) {\n    ctx.body = String(Math.PI);\n  } else {\n    await next();\n  }\n}\n\nconst all = compose([random, backwards, pi]);\n\napp.use(all);\n```\n\n### Response Middleware\n\nMiddleware that decide to respond to a request and wish to bypass downstream middleware may\nsimply omit `next()`. Typically this will be in routing middleware, but this can be performed by\nany. For example the following will respond with \"two\", however all three are executed, giving the\ndownstream \"three\" middleware a chance to manipulate the response.\n\n```js\napp.use(async function (ctx, next) {\n  console.log('>> one');\n  await next();\n  console.log('<< one');\n});\n\napp.use(async function (ctx, next) {\n  console.log('>> two');\n  ctx.body = 'two';\n  await next();\n  console.log('<< two');\n});\n\napp.use(async function (ctx, next) {\n  console.log('>> three');\n  await next();\n  console.log('<< three');\n});\n```\n\nThe following configuration omits `next()` in the second middleware, and will still respond\nwith \"two\", however the third (and any other downstream middleware) will be ignored:\n\n```js\napp.use(async function (ctx, next) {\n  console.log('>> one');\n  await next();\n  console.log('<< one');\n});\n\napp.use(async function (ctx, next) {\n  console.log('>> two');\n  ctx.body = 'two';\n  console.log('<< two');\n});\n\napp.use(async function (ctx, next) {\n  console.log('>> three');\n  await next();\n  console.log('<< three');\n});\n```\n\nWhen the furthest downstream middleware executes `next();`, it's really yielding to a noop\nfunction, allowing the middleware to compose correctly anywhere in the stack.\n\n## Async operations\n\nAsync function and promise forms Koa's foundation, allowing\nyou to write non-blocking sequential code. For example this middleware reads the filenames from `./docs`,\nand then reads the contents of each markdown file in parallel before assigning the body to the joint result.\n\n```js\nconst fs = require('mz/fs');\n\napp.use(async function (ctx, next) {\n  const paths = await fs.readdir('docs');\n  const files = await Promise.all(paths.map((path) => fs.readFile(`docs/${path}`, 'utf8')));\n\n  ctx.type = 'markdown';\n  ctx.body = files.join('');\n});\n```\n\n## Debugging Koa\n\nKoa along with many of the libraries it's built with support the **DEBUG** environment variable from [debug](https://github.com/visionmedia/debug) which provides simple conditional logging.\n\nFor example\nto see all Koa-specific debugging information just pass `DEBUG=koa*` and upon boot you'll see the list of middleware used, among other things.\n\n```\n$ DEBUG=koa* node --harmony examples/simple\n  koa:application use responseTime +0ms\n  koa:application use logger +4ms\n  koa:application use contentLength +0ms\n  koa:application use notfound +0ms\n  koa:application use response +0ms\n  koa:application listen +0ms\n```\n\nSince JavaScript does not allow defining function names at\nruntime, you can also set a middleware's name as `._name`.\nThis is useful when you don't have control of a middleware's name.\nFor example:\n\n```js\nconst path = require('path');\nconst serve = require('koa-static');\n\nconst publicFiles = serve(path.join(__dirname, 'public'));\npublicFiles._name = 'static /public';\n\napp.use(publicFiles);\n```\n\nNow, instead of just seeing \"serve\" when debugging, you will see:\n\n```\n  koa:application use static /public +0ms\n```\n","isInternal":false,"tokens":1686,"sizeBytes":7143},{"name":"koa-vs-express.md","path":"packages/koa/docs/koa-vs-express.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/koa/docs/koa-vs-express.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Koa vs Express\n\nPhilosophically, Koa aims to \"fix and replace node\", whereas Express \"augments node\".\nKoa uses promises and async functions to rid apps of callback hell and simplify error handling.\nIt exposes its own `ctx.request` and `ctx.response` objects instead of node's `req` and `res` objects.\n\nExpress, on the other hand, augments node's `req` and `res` objects with additional properties and methods\nand includes many other \"framework\" features, such as routing and templating, which Koa does not.\n\nThus, Koa can be viewed as an abstraction of node.js's `http` modules, where as Express is an application framework for node.js.\n\n|           Feature | Koa | Express | Connect |\n| ----------------: | --- | ------- | ------- |\n| Middleware Kernel | ✓   | ✓       | ✓       |\n|           Routing |     | ✓       |         |\n|        Templating |     | ✓       |         |\n|     Sending Files |     | ✓       |         |\n|             JSONP |     | ✓       |         |\n\nThus, if you'd like to be closer to node.js and traditional node.js-style coding, you probably want to stick to Connect/Express or similar frameworks.\nIf you want to get rid of callbacks, use Koa.\n\nAs result of this different philosophy is that traditional node.js \"middleware\", i.e. functions of the form `(req, res, next)`, are incompatible with Koa. Your application will essentially have to be rewritten from the ground, up.\n\n## Does Koa replace Express?\n\nIt's more like Connect, but a lot of the Express goodies\nwere moved to the middleware level in Koa to help form\na stronger foundation. This makes middleware more enjoyable\nand less error-prone to write, for the entire stack, not\njust the end application code.\n\nTypically many middleware would\nre-implement similar features, or even worse incorrectly implement them,\nwhen features like signed cookie secrets among others are typically application-specific,\nnot middleware specific.\n\n## Does Koa replace Connect?\n\nNo, just a different take on similar functionality\nnow that generators allow us to write code with less\ncallbacks. Connect is equally capable, and some may still prefer it,\nit's up to what you prefer.\n\n## Why isn't Koa just Express 4.0?\n\nKoa is a pretty large departure from what people know about Express,\nthe design is fundamentally much different, so the migration from\nExpress 3.0 to this Express 4.0 would effectively mean rewriting\nthe entire application, so we thought it would be more appropriate\nto create a new library.\n\n## How is Koa different than Connect/Express?\n\n### Promises-based control flow\n\nNo callback hell.\n\nBetter error handling through try/catch.\n\nNo need for domains.\n\n### Koa is barebones\n\nUnlike both Connect and Express, Koa does not include any middleware.\n\nUnlike Express, routing is not provided.\n\nUnlike Express, many convenience utilities are not provided. For example, sending files.\n\nKoa is more modular.\n\n### Koa relies less on middleware\n\nFor example, instead of a \"body parsing\" middleware, you would instead use a body parsing function.\n\n### Koa abstracts node's request/response\n\nLess hackery.\n\nBetter user experience.\n\nProper stream handling.\n\n### Koa routing (third party libraries support)\n\nSince Express comes with its own routing, but Koa does not have\nany in-built routing, there are third party libraries available such as\nkoa-router and koa-route.\nSimilarly, just like we have helmet for security in Express, for Koa\nwe have koa-helmet available and the list goes on for Koa available third\nparty libraries.\n","isInternal":false,"tokens":771,"sizeBytes":3519},{"name":"migration.md","path":"packages/koa/docs/migration.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/koa/docs/migration.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Migrating from Koa v1.x to v2.x\n\n## New middleware signature\n\nKoa v2 introduces a new signature for middleware.\n\n**Old signature middleware (v1.x) support will be removed in v3**\n\nThe new middleware signature is:\n\n```js\n// uses async arrow functions\napp.use(async (ctx, next) => {\n  try {\n    await next(); // next is now a function\n  } catch (err) {\n    ctx.body = { message: err.message };\n    ctx.status = err.status || 500;\n  }\n});\n\napp.use(async (ctx) => {\n  const user = await User.getById(this.session.userid); // await instead of yield\n  ctx.body = user; // ctx instead of this\n});\n```\n\nYou don't have to use asynchronous functions - you just have to pass a function that returns a promise.\nA regular function that returns a promise works too!\n\nThe signature has changed to pass `Context` via an explicit parameter, `ctx` above, instead of via\n`this`. The context passing change makes Koa more compatible with es6 arrow functions, which capture `this`.\n\n## Using v1.x Middleware in v2.x\n\nKoa v2.x will try to convert legacy signature, generator middleware on `app.use`, using [koa-convert](https://github.com/koajs/convert).\nIt is however recommended that you choose to migrate all v1.x middleware as soon as possible.\n\n```js\n// Koa will convert\napp.use(function* (next) {\n  const start = Date.now();\n  yield next;\n  const ms = Date.now() - start;\n  console.log(`${this.method} ${this.url} - ${ms}ms`);\n});\n```\n\nYou could do it manually as well, in which case Koa will not convert.\n\n```js\nconst convert = require('koa-convert');\n\napp.use(\n  convert(function* (next) {\n    const start = Date.now();\n    yield next;\n    const ms = Date.now() - start;\n    console.log(`${this.method} ${this.url} - ${ms}ms`);\n  }),\n);\n```\n\n## Upgrading middleware\n\nYou will have to convert your generators to async functions with the new middleware signature:\n\n```js\napp.use(async (ctx, next) => {\n  const user = await Users.getById(this.session.user_id);\n  await next();\n  ctx.body = { message: 'some message' };\n});\n```\n\nUpgrading your middleware may require some work. One migration path is to update them one-by-one.\n\n1. Wrap all your current middleware in `koa-convert`\n2. Test\n3. `npm outdated` to see which Koa middleware is outdated\n4. Update one outdated middleware, remove using `koa-convert`\n5. Test\n6. Repeat steps 3-5 until you're done\n\n## Updating your code\n\nYou should start refactoring your code now to ease migrating to Koa v2:\n\n- Return promises everywhere!\n- Do not use `yield*`\n- Do not use `yield {}` or `yield []`.\n  - Convert `yield []` into `yield Promise.all([])`\n  - Convert `yield {}` into `yield Bluebird.props({})`\n\nYou could also refactor your logic outside of Koa middleware functions. Create functions like\n`function* someLogic(ctx) {}` and call it in your middleware as\n`const result = yield someLogic(this)`.\nNot using `this` will help migrations to the new middleware signature, which does not use `this`.\n\n## Application object constructor requires new\n\nIn v1.x, the Application constructor function could be called directly, without `new` to\ninstantiate an instance of an application. For example:\n\n```js\nvar koa = require('koa');\nvar app = (module.exports = koa());\n```\n\nv2.x uses es6 classes which require the `new` keyword to be used.\n\n```js\nvar koa = require('koa');\nvar app = (module.exports = new koa());\n```\n\n## ENV specific logging behavior removed\n\nAn explicit check for the `test` environment was removed from error handling.\n\n## Dependency changes\n\n- [co](https://github.com/tj/co) is no longer bundled with Koa. Require or import it directly.\n- [composition](https://github.com/thenables/composition) is no longer used and deprecated.\n\n## v1.x support\n\nThe v1.x branch is still supported but should not receive feature updates. Except for this migration\nguide, documentation will target the latest version.\n\n## Help out\n\nIf you encounter migration related issues not covered by this migration guide, please consider\nsubmitting a documentation pull request.\n","isInternal":false,"tokens":969,"sizeBytes":3996},{"name":"troubleshooting.md","path":"packages/koa/docs/troubleshooting.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/koa/docs/troubleshooting.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Troubleshooting Koa\n\n- [Whenever I try to access my route, it sends back a 404](#whenever-i-try-to-access-my-route-it-sends-back-a-404)\n- [My response or context changes have no effect](#my-response-or-context-changes-have-no-effect)\n- [My middleware is not called](#my-middleware-is-not-called)\n\nSee also [debugging Koa](guide.md#debugging-koa).\n\nIf you encounter a problem and later learn how to fix it, and think others might also encounter that problem, please\nconsider contributing to this documentation.\n\n## Whenever I try to access my route, it sends back a 404\n\nThis is a common but troublesome problem when working with Koa middleware. First, it is critical to understand when Koa generates a 404. Koa does not care which or how much middleware was run, in many cases a 200 and 404 trigger the same number of middleware. Instead, the default status for any response is 404. The most obvious way this is changed is through `ctx.status`. However, if `ctx.body` is set when the status has not been explicitly defined (through `ctx.status`), the status is set to 200. This explains why simply setting the body results in a 200. Once the middleware is done (when the middleware and any returned promises are complete), Koa sends out the response. After that, nothing can alter the response. If it was a 404 at the time, it will be a 404 at the end, even if `ctx.status` or `ctx.body` are set afterwords.\n\nEven though we now understand the basis of a 404, it might not be as clear why a 404 is generated in a specific case. This can be especially troublesome when it seems that `ctx.status` or `ctx.body` are set.\n\nThe unexpected 404 is a specific symptom of one of these more general problems:\n\n- [My response or context changes have no effect](#my-response-or-context-changes-have-no-effect)\n- [My middleware is not called](#my-middleware-is-not-called)\n\n## My response or context changes have no effect\n\nThis can be caused when the response is sent before the code making the change is\nexecuted. If the change is to the `ctx.body` or `ctx.status` setter, this can cause a 404 and\nis by far the most common cause of these problems.\n\n### Problematic code\n\n```js\nrouter.get('/fetch', function (ctx, next) {\n  models.Book.findById(parseInt(ctx.query.id)).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\nWhen used, this route will always send back a 404, even though `ctx.body` is set.\n\nThe same behavior would occur in this `async` version:\n\n```js\nrouter.get('/fetch', async (ctx, next) => {\n  models.Book.findById(parseInt(ctx.query.id)).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\n### Cause\n\n`ctx.body` is not set until _after_ the response has been sent. The code doesn't tell Koa to wait for the database to return the record. Koa sends the response after the middleware has been run, but not after the callback inside the middleware has been run. In the gap there, `ctx.body` has not yet been set, so Koa responds with a 404.\n\n### Identifying this as the issue\n\nAdding another piece of middleware and some logging can be extremely helpful in identifying this issue.\n\n```js\nrouter.use('/fetch', function (ctx, next) {\n  return next().then(function () {\n    console.log('Middleware done');\n  });\n});\n\nrouter.get('/fetch', function (ctx, next) {\n  models.Book.findById(parseInt(ctx.query.id)).then(function (book) {\n    ctx.body = book;\n    console.log('Body set');\n  });\n});\n```\n\nIf you see this in the logs:\n\n```\nMiddleware done\nBody set\n```\n\nIt means that the body is being set after the middleware is done, and after the response has been sent. If you see only one or none of these logs, proceed to [My middleware is not called](#my-middleware-is-not-called). If they are in the right order, make sure you haven't explicitly set the status to 404, make sure that it actually is a 404, and if that fails feel free to ask for help.\n\n### Solution\n\n```js\nrouter.get('/fetch', function (ctx, next) {\n  return models.Book.findById(parseInt(ctx.query.id)).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\nReturning the promise given by the database interface tells Koa to wait for the promise to finish before responding. At that time, the body will have been set. This results in Koa sending back a 200 with a proper response.\n\nThe fix in the `async` version is to add an `await` statement:\n\n```js\nrouter.get('/fetch', async (ctx, next) => {\n  await models.Book.findById(parseInt(ctx.query.id)).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\n## My middleware is not called\n\nThis can be due to an interrupted chain of middleware calls. This can cause a 404 if the\nmiddleware that is skipped is responsible for the `ctx.body` or `ctx.status` setter.\nThis is less common than [My response or context changes have no effect](#my-response-or-context-changes-have-no-effect),\nbut it can be a much bigger pain to troubleshoot.\n\n### Problematic code\n\n```js\nrouter.use(function (ctx, next) {\n  // Don't Repeat Yourself! Let's parse the ID here for all our middleware\n  if (ctx.query.id) {\n    ctx.state.id = parseInt(ctx.query.id);\n  }\n});\n\nrouter.get('/fetch', function (ctx, next) {\n  return models.Book.findById(ctx.state.id).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\n### Cause\n\nIn the code above, the book is never fetched from the database, and in fact our route was never called. Look closely at our helper middleware. We forgot to `return next()`! This causes the middleware flow to never reach our route, ending our \"helper\" middleware.\n\n### Identifying this as the issue\n\nIdentifying this problem is easier than most, add a log at the beginning of the route. If it doesn't trigger, your route was never reached in the middleware chain.\n\n```js\nrouter.use(function (ctx, next) {\n  // Don't Repeat Yourself! Let's parse the ID here for all our middleware\n  if (ctx.query.id) {\n    ctx.state.id = parseInt(ctx.query.id);\n  }\n});\n\nrouter.get('/fetch', function (ctx, next) {\n  console.log('Route called'); // Never happens\n  return models.Book.findById(ctx.state.id).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\nTo find the middleware causing the problem, try adding logging at various points in the middleware chain.\n\n### Solution\n\nThe solution for this is rather easy, simply add `return next()` to the end of your helper middleware.\n\n```js\nrouter.use(function (ctx, next) {\n  if (ctx.query.id) {\n    ctx.state.id = parseInt(ctx.query.id);\n  }\n  return next();\n});\n\nrouter.get('/fetch', function (ctx, next) {\n  return models.Book.findById(ctx.state.id).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n","isInternal":false,"tokens":1580,"sizeBytes":6592},{"name":"AGENTS.md","path":"packages/skills/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/AGENTS.md","title":"Skills Skill","category":"anthropic-skill","format":"markdown","content":"# Skills 编写与评测规范\n\n遵循[根目录 AGENTS.md](../../AGENTS.md) 中的共享仓库指引。以下补充 `packages/skills/` 的专属约定。\n\n`packages/skills/` 目录包含 AI agent skills — 纯 markdown 文档，指导 AI 助手使用 Egg 框架。以 `@eggjs/skills` npm 包发布，仅含 `.md` 文件。\n\n> **Skill 编写基础知识**：SKILL.md 格式、frontmatter 规范、目录结构、progressive disclosure、写作风格等通用知识请使用 `/skill-creator` skill 获取指导。以下仅记录 Egg 项目特有的约定。\n\n## Egg Skills 架构\n\nSkills 采用分层路由模式：\n\n- **入口 skill** (`egg/`) — 分析用户意图，通过关键词匹配和决策逻辑路由到专业 skill\n- **专业 skills** — 提供特定领域的深度指导：\n  - `egg-core/` — 核心概念：模块、依赖注入、生命周期、AccessLevel、后台任务\n  - `egg-controller/` — 实现指导：HTTPController、MCPController、Schedule、Ajv 校验\n\n## Egg Skill Frontmatter 约定\n\n- **`name`**：入口 skill 使用 `egg`，专业 skill 以 `egg-` 为前缀（如 `egg-controller`、`egg-core`）\n- **`allowed-tools`**：统一使用 `Read`（纯文档指导型，不修改文件）\n- **`description`**：\n  - 中文：以\"本技能用于...\"开头，包含触发关键词\n  - 英文：以\"Use when...\"开头，包含触发关键词\n\n## 专业 Skill 编写规范\n\n**入口 Skill（如 `egg/`）编写要点：**\n\n对应 /skill-creator 的 **Workflow-Based** 模式。\n\n1. 决策框架包含明确步骤：识别意图 → 检查模糊意图 → 协议/用例特定指示\n2. 为每个专业 skill 列出中英文触发关键词\n3. 冲突解决规则：明确意图模糊时的优先级（如\"基础优先\"——核心概念优先于控制器实现）\n4. 示例分析：多个完整示例，格式为 用户查询 → 分析 → 决策 → 响应策略\n5. 快速参考表：用户意图 → 关键词 → 推荐 skill 映射\n6. 交叉引用话术：回答完主问题后，附\"如需了解 X，请参阅 `@eggjs/skills-xxx`\"\n\n**专业 Skill 两种组织模式：**\n\n| 模式                               | /skill-creator 对应 | 适用场景           | SKILL.md 内容           | references/ 用途   |\n| ---------------------------------- | ------------------- | ------------------ | ----------------------- | ------------------ |\n| **概念型**（如 `egg-core/`）       | Reference-Based     | 概念解释、架构理解 | 自包含的深度内容        | 更深入的专题文档   |\n| **索引型**（如 `egg-controller/`） | Workflow-Based      | 多种实现方式的选择 | 精简的决策树 + 快速参考 | 每种实现的详细指南 |\n\n**概念型 Skill 内容结构：**\n\n1. 概述（一段话说明覆盖范围）\n2. 按概念分块，每个概念包含：定义 → 使用场景 → 装饰器/API 模式 → 代码示例\n3. 重要约束（反模式、限制）\n4. 快速决策指南表（场景 → 推荐方式）\n5. 最佳实践\n6. 参考资料链接\n\n**索引型 Skill 内容结构：**\n\n1. 决策树（根据需求选择实现方式）\n2. 每种类型的快速参考（装饰器、参数、特点）\n3. 最佳实践\n4. 参考资料链接\n\n## Reference 文档编写要点\n\nReference 文档（`references/*.md`）**不需要 YAML frontmatter**，只有 `SKILL.md` 才需要。\n\n**核心原则：Skill 不是文档的重新排版，而是填补\"文档到生产代码\"之间的缝隙。**\n\nSkill 的价值 = 文档 + 实践经验 - 重复内容。如果内容和 `site/docs/` 中的文档高度重复，说明 skill 写得不对。\n\n| 内容类型            | 该放文档（site/docs/） | 该放 Skill（packages/skills/） |\n| ------------------- | ---------------------- | ------------------------------ |\n| API 签名、参数说明  | Yes                    | No（引用文档即可）             |\n| 易错点 / 常见错误   | 部分                   | **Yes（重点）**                |\n| 完整端到端模式      | No                     | **Yes**                        |\n| 跨模块集成知识      | 散落各处               | **Yes（聚合）**                |\n| 文件放置 / 命名约定 | 部分                   | **Yes**                        |\n| 场景化决策树        | No                     | **Yes**                        |\n\n**编写 Reference 文档的具体步骤：**\n\n1. **先读对应的文档**（如 `site/docs/zh-CN/basics/mcpcontroller.md`），理解已有内容\n2. **向维护者提问，收集文档未覆盖的知识**，重点关注：\n   - 导入路径、命名约定等易错点（AI 最容易犯的错）\n   - 文件应该放在哪个目录？命名规则是什么？\n   - 和其他模块（Module、Service、DI）的集成关系\n   - 哪些配置参数实际开发中需要关心，哪些用默认值即可\n   - 哪些是内部扩展机制不需要暴露给应用开发者\n3. **以常见错误表开头** — 把 AI 最容易写错的地方放在最醒目的位置（如错误的导入路径、错误的 API 用法）\n4. **文件约定** — 目录结构、命名规则、配置文件位置，这些文档里通常不会详细说明\n5. **场景化决策树** — 从用户意图出发（\"让 AI 查数据\"），而非从 API 出发（\"@MCPTool\"）\n6. **端到端完整示例** — 从配置文件到控制器到 Service 到测试，展示所有相关文件和它们的关系\n7. **精简的装饰器对照表放末尾** — 仅作为速查，不展开 API 详解\n\n## 添加新 Skill\n\n1. 在 `packages/skills/` 下创建目录：`packages/skills/<skill-name>/`\n2. 创建 `SKILL.md`（格式规范参考 `/skill-creator`，frontmatter 遵循上述 Egg 约定）\n3. 创建 `references/` 目录（初始为空时放置 `.gitkeep`）\n4. 按需在 `references/*.md` 中添加详细参考文档\n5. 更新入口 skill（`egg/SKILL.md`）的路由逻辑以包含新 skill\n6. 如果 skill 涉及 controller 类型，同时更新 `egg-controller/SKILL.md` 决策树\n\n## 添加新 Reference 文档\n\n1. 在 skill 的 `references/` 目录中创建 `.md` 文件\n2. 遵循命名规范（kebab-case，描述性命名：`http-controller.md`、`mcp-controller.md`）\n3. 包含完整的代码示例和决策树\n4. 更新父级 `SKILL.md` 引用新文档\n5. 如果 `references/` 中已有文件，移除 `.gitkeep`\n\n## Skill 评测\n\n评测用例存放在 `packages/skills/eval/` 目录下，用于验证 AI 使用 skill 后的回答质量。\n\n**评测文件结构：**\n\n```text\npackages/skills/eval/\n├── evals-egg-core.json        # egg-core skill 评测用例\n├── evals-egg-controller.json  # egg-controller skill 评测用例\n├── evals-routing.json         # 入口路由评测用例\n├── .gitignore                 # 忽略 *-workspace/ 目录\n└── <skill-name>-workspace/    # 评测输出（gitignored），由 /skill-creator 管理\n    └── iteration-N/\n        ├── REPORT.md          # 对比评分报告\n        ├── GRADING.md         # with-skill 通过率报告\n        └── {prefix}-{id}/     # 每个用例一个目录\n            ├── eval_metadata.json\n            ├── with_skill/outputs/\n            └── without_skill/outputs/\n```\n\n**评测用例 JSON 格式：**\n\n```json\n{\n  \"skill_name\": \"egg-controller\",\n  \"description\": \"控制器评测：覆盖 http-controller、mcp-controller、schedule、ajv-validate\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"用户的任务描述\",\n      \"expected_output\": \"期望输出的关键要素描述\",\n      \"files\": [{ \"path\": \"相对路径\", \"content\": \"文件内容（可选，用于提供上下文或有 bug 的代码）\" }]\n    }\n  ]\n}\n```\n\n**评测流程：**\n\n使用 `/skill-creator` skill 运行评测和生成结果展示。评测流程概述：\n\n1. **编写评测用例** — 在对应的 `evals-*.json` 中添加用例\n2. **运行评测** — 通过 `/skill-creator` 为每个用例启动两个并行 subagent（with-skill 和 site-docs），使用下方 prompt 模板\n3. **评分和展示** — `/skill-creator` 负责评分、生成对比报告、启动可视化 viewer 供人工 review\n4. **改进 skill** — 根据评分结果和人工 feedback 改进 skill 内容，开启新的 iteration\n\n**评测对比的两组环境：**\n\n每个评测用例需要在两种环境下分别运行，对比 skill 是否有效。Prompt 中不应包含任何流程指引（如\"先判断使用哪个 skill\"），只提供参考资料和访问约束，让 AI 自然行动。\n\n| 环境           | system prompt                                                                         | 可访问范围（prompt 约束）  |\n| -------------- | ------------------------------------------------------------------------------------- | -------------------------- |\n| **with-skill** | `egg/SKILL.md`（入口 skill）的完整内容                                                | 仅 `packages/skills/` 目录 |\n| **site-docs**  | 角色声明 + `site/docs/` 的完整文件目录列表（通过 `find site/docs -name '*.md'` 生成） | 仅 `site/docs/` 目录       |\n\n**Prompt 模板：**\n\nwith-skill 环境：\n\n```text\n你是 EGG 框架开发专家。你只能通过 Read 工具读取 packages/skills/ 目录下的文件，不能访问 site/docs/ 或项目源码。\n\n{egg/SKILL.md 的完整内容}\n\n---\n{eval prompt}\n```\n\nsite-docs 环境：\n\n```text\n你是 EGG 框架开发专家。你只能通过 Read 工具读取 site/docs/ 目录下的文件，不能访问 packages/skills/ 或项目源码。项目文档目录如下：\n\n{完整的 site/docs/ 文件列表，通过 find site/docs -name '*.md' | sort 生成}\n\n---\n{eval prompt}\n```\n\n两组 prompt 的差异仅在于参考资料不同，不包含额外的流程提示。subagent 均具备 Read 工具权限。访问范围通过 prompt 约束（软限制，非技术硬限制）。\n\n两组使用相同的 eval prompt，对比输出质量差异。如果 with-skill 没有明显优于 site-docs，说明 skill 内容需要改进——要么缺少文档未覆盖的知识，要么存在与文档的不必要重复。\n\n**输出目录：**\n\n评测结果保存到 `packages/skills/eval/<skill-name>-workspace/iteration-N/` 目录下（已在 `.gitignore` 中通过 `*-workspace/` 忽略），具体目录结构由 `/skill-creator` 管理。\n\n**评测用例设计原则：**\n\n每个 reference 文档至少覆盖 5 个以上评测用例，需覆盖以下场景类型：\n\n| 场景类型             | 说明                                    | 示例                                                     |\n| -------------------- | --------------------------------------- | -------------------------------------------------------- |\n| **泛化需求描述**     | 不了解框架术语，用口语化描述需求        | \"帮我加个参数校验\"                                       |\n| **精确需求描述**     | 明确指定技术方案和约束                  | \"用 TypeBox 定义 Schema，email 用 format: email\"         |\n| **使用咨询**         | 询问用法、区别、选型                    | \"Optional 和 Null 有什么区别\"                            |\n| **问题排查**         | 提供有 bug 的代码（附 files），要求诊断 | \"校验跑不起来，帮我看看\"                                 |\n| **新项目代码生成**   | 在全新项目中从零开始生成功能代码        | \"帮我写一个创建订单的接口，需要做参数校验\"               |\n| **存量项目代码生成** | 在包含老 egg 代码的项目中生成或迁移代码 | \"帮我把这个老的 egg controller 改成 HTTPController 写法\" |\n| **不常用 API**       | 需要查外部文档链接才能回答              | \"用 Tuple 定义元组校验\"                                  |\n","isInternal":false,"tokens":3711,"sizeBytes":11791},{"name":"ajv-validate.md","path":"packages/skills/egg-controller/references/ajv-validate.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/egg-controller/references/ajv-validate.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Ajv 参数校验指南\n\n## 常见错误\n\n| 错误写法                         | 正确写法                          | 说明                                           |\n| -------------------------------- | --------------------------------- | ---------------------------------------------- |\n| `import { Type } from 'typebox'` | `import { Type } from 'egg/ajv'`  | 必须从 `egg/ajv` 导入，内部已封装 typebox      |\n| `import { Ajv } from 'ajv'`      | `import { Ajv } from 'egg/ajv'`   | Ajv 实例通过 `egg/ajv` 导出                    |\n| `new Ajv()` 手动创建实例         | `@Inject() ajv: Ajv` 注入全局单例 | 框架已配置好 formats 和 keywords，不要自行创建 |\n| 在 Service 中做参数校验          | 在 Controller 中做参数校验        | 入参校验应在 Controller 层完成                 |\n\n---\n\n## 核心概念\n\nEgg 通过 `@eggjs/typebox-validate` 插件集成 Ajv（JSON Schema 校验库）和 TypeBox（TypeScript-first 的 JSON Schema 构建器）。**定义一次 Schema，同时获得参数校验和 TypeScript 类型推导。**\n\n### 导入路径\n\n```typescript\n// 所有 Ajv/TypeBox 相关导入统一从 egg/ajv\nimport { Ajv, Type, Static, TransformEnum } from 'egg/ajv';\n```\n\n---\n\n## 使用方式\n\n通过 `@Inject()` 注入全局 Ajv 单例，在 Controller 方法中调用 `ajv.validate()` 进行校验。\n\n### 完整示例\n\n```typescript\n// app/userModule/UserController.ts\nimport { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPBody, Inject } from 'egg';\nimport { Ajv, Type, Static, TransformEnum } from 'egg/ajv';\n\n// 1. 定义 Schema\nconst CreateUserSchema = Type.Object({\n  name: Type.String({\n    transform: [TransformEnum.trim],\n    minLength: 1,\n    maxLength: 50,\n  }),\n  email: Type.String({ format: 'email' }),\n  age: Type.Optional(Type.Integer({ minimum: 0, maximum: 150 })),\n});\n\n// 2. 从 Schema 推导类型\ntype CreateUserParams = Static<typeof CreateUserSchema>;\n\n// 3. 在 Controller 中注入 Ajv 并校验\n@HTTPController()\nexport class UserController {\n  @Inject()\n  private readonly ajv: Ajv;\n\n  @HTTPMethod({\n    method: HTTPMethodEnum.POST,\n    path: '/api/users',\n  })\n  async create(@HTTPBody() body: CreateUserParams) {\n    // 校验失败自动抛出 AjvInvalidParamError\n    this.ajv.validate(CreateUserSchema, body);\n\n    // 校验通过，body 已经有完整类型提示\n    return { name: body.name, email: body.email };\n  }\n}\n```\n\n---\n\n## Schema 定义\n\n### 常用类型\n\n```typescript\nimport { Type } from 'egg/ajv';\n\nType.String(); // string\nType.Number(); // number\nType.Integer(); // 整数\nType.Boolean(); // boolean\nType.Optional(Type.String()); // string | undefined\nType.Array(Type.String()); // string[]\nType.Object({ name: Type.String() }); // { name: string }\nType.Union([Type.String(), Type.Number()]); // string | number\nType.Literal('admin'); // 'admin'\n```\n\n完整的 TypeBox JSON Schema 类型定义参考：https://github.com/sinclairzx81/typebox#json-types\n\n### 内置 format 校验\n\n框架通过 `ajv-formats` 预注册了以下格式：\n\n| format      | 说明       | 示例                                   |\n| ----------- | ---------- | -------------------------------------- |\n| `email`     | 邮箱       | `user@example.com`                     |\n| `uri`       | URI        | `https://example.com`                  |\n| `uuid`      | UUID       | `550e8400-e29b-41d4-a716-446655440000` |\n| `date`      | 日期       | `2024-01-01`                           |\n| `date-time` | 日期时间   | `2024-01-01T00:00:00Z`                 |\n| `time`      | 时间       | `12:00:00`                             |\n| `ipv4`      | IPv4       | `192.168.1.1`                          |\n| `ipv6`      | IPv6       | `::1`                                  |\n| `hostname`  | 主机名     | `example.com`                          |\n| `regex`     | 正则表达式 | `^\\\\d+$`                               |\n\n```typescript\nType.String({ format: 'email' });\nType.String({ format: 'uuid' });\nType.String({ format: 'date-time' });\n```\n\n### transform 预处理\n\n通过 `ajv-keywords` 的 `transform` 关键字，在校验前对字符串做预处理：\n\n```typescript\nimport { TransformEnum } from 'egg/ajv';\n\nType.String({\n  transform: [TransformEnum.trim], // 去除首尾空格\n});\n\nType.String({\n  transform: [TransformEnum.trim, TransformEnum.toLowerCase], // 去空格 + 转小写\n});\n```\n\n---\n\n## 最佳实践\n\n- **Schema 与 Controller 同文件** — Schema 定义放在使用它的 Controller 文件中，保持就近原则\n- **校验在 Controller 层** — 不要在 Service 层做入参校验，Service 信任上层传入的数据\n- **善用 Optional** — 非必填字段用 `Type.Optional()` 包装，避免前端遗漏字段导致校验失败\n- **善用 transform** — 对用户输入做 trim 预处理，减少脏数据\n","isInternal":false,"tokens":1366,"sizeBytes":4853},{"name":"http-controller.md","path":"packages/skills/egg-controller/references/http-controller.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/egg-controller/references/http-controller.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# HTTPController 开发指南\n\n## 快速开始\n\n### 创建基本 HTTP 接口\n\n使用 `@HTTPController` 和 `@HTTPMethod` 装饰器创建 HTTP 接口：\n\n```typescript\nimport { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPParam } from 'egg';\n\n@HTTPController()\nexport class DemoController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/hello/:name' })\n  async hello(@HTTPParam() name: string) {\n    return { message: 'hello ' + name };\n  }\n}\n```\n\n### 设置路径前缀\n\n为控制器设置统一路径前缀：\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class PathController {\n  // 最终路径: GET /api/hello\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: 'hello' })\n  async hello() {}\n\n  // 最终路径: POST /api/create\n  @HTTPMethod({ method: HTTPMethodEnum.POST, path: 'create' })\n  async create() {}\n}\n```\n\n---\n\n## 参数装饰器决策\n\n### 决策树：选择合适的参数装饰器\n\n```\n需要从 HTTP 请求获取什么信息？\n\n├─ URL 路径参数\n│  └─ → @HTTPParam()\n│\n├─ URL 查询参数\n│  ├─ 单个值 → @HTTPQuery()\n│  └─ 多个值（数组） → @HTTPQueries()\n│\n├─ 请求体（POST/PUT）\n│  └─ → @HTTPBody()\n│     ├─ json → 对象\n│     ├─ text → 字符串\n│\n├─ 请求头\n│  └─ → @HTTPHeaders()\n│     └─ 注意：key 自动转小写\n│\n├─ Cookie\n│  └─ → @Cookies()\n│\n├─ 原始 HTTP 请求对象\n│  └─ → @Request()\n│     └─ 注意：不要和 @HTTPBody 一起消费请求体\n│\n└─ Egg Context（框架功能）\n   └─ → @Context()\n```\n\n### 参考对照表（参数装饰器）\n\n| 装饰器           | 获取内容         | 类型                  | 默认值 | 支持选项            |\n| ---------------- | ---------------- | --------------------- | ------ | ------------------- |\n| `@HTTPParam()`   | URL 路径参数     | `string`              | 变量名 | `{ name?: string }` |\n| `@HTTPQuery()`   | 查询参数（单个） | `string`              | 变量名 | `{ name?: string }` |\n| `@HTTPQueries()` | 查询参数（多个） | `string[]`            | 变量名 | `{ name?: string }` |\n| `@HTTPBody()`    | 请求体           | `object \\| string`    | -      | -                   |\n| `@HTTPHeaders()` | 请求头           | `IncomingHttpHeaders` | -      | -                   |\n| `@Cookies()`     | Cookie           | `HTTPCookies`         | -      | -                   |\n| `@Request()`     | HTTP 请求对象    | `HTTPRequest`         | -      | -                   |\n| `@Context()`     | Egg Context      | `EggContext`          | -      | -                   |\n\n### 快速选择指南\n\n**需要从 URL 获取参数（id）** → `@HTTPParam`\n**需要从查询字符串获取参数（category=books）** → `@HTTPQuery`\n**需要从查询字符串获取全部值（tag=tech&tag=dev）** → `@HTTPQueries`\n**需要获取请求体（POST JSON）** → `@HTTPBody`\n**需要获取请求头字段** → `@HTTPHeaders`（注意：使用小写key）\n**需要读取 Cookie** → `@Cookies`\n**需要访问原始请求对象** → `@Request`\n\n---\n\n## HTTP 响应\n\n### JSON 响应（默认）\n\n直接返回对象，框架自动序列化为 JSON：\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class JsonController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/data' })\n  async getData() {\n    return { result: 'hello world' };\n  }\n}\n```\n\n### 自定义响应\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class ResponseController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/custom' })\n  async customResponse(@Context() ctx: EggContext) {\n    ctx.status = 201;\n    ctx.set('X-Custom', 'value');\n    ctx.type = 'json';\n    return { message: 'Created' };\n  }\n}\n```\n\n---\n\n## 服务端渲染（SSR）\n\n```typescript\n@HTTPController({ path: '/' })\nexport class SSRController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })\n  async render(@Context() ctx: EggContext) {\n    ctx.type = 'html';\n    return `\n      <!DOCTYPE html>\n      <html>\n        <head><title>Home</title></head>\n        <body><h1>Hello World</h1></body>\n      </html>\n    `;\n  }\n}\n```\n\n---\n\n## 流式响应（Streaming）\n\n```typescript\nimport { Readable } from 'node:stream';\nimport { setTimeout } from 'node:timers/promises';\n\nasync function* generateHtml() {\n  yield '<html><body>';\n  for (let i = 1; i <= 5; i++) {\n    yield `<p>Chunk ${i}</p>`;\n    await setTimeout(1000);\n  }\n  yield '</body></html>';\n}\n\n@HTTPController({ path: '/api' })\nexport class StreamController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/stream' })\n  async streamHtml(@Context() ctx: EggContext) {\n    ctx.type = 'html';\n    return Readable.from(generateHtml());\n  }\n}\n```\n\n---\n\n## 路由优先级管理\n\n### 默认优先级规则\n\n| Path                            | RegExp Index | Priority | 说明             |\n| ------------------------------- | ------------ | -------- | ---------------- |\n| `/*`                            | `[0]`        | 0        | 通配符，最低     |\n| `/hello/:name`                  | `[1]`        | 1000     | 单参数           |\n| `/hello/world/message/:message` | `[3]`        | 3000     | 三参数           |\n| `/hello/:name/message/:message` | `[1, 3]`     | 4000     | 多参数，索引更大 |\n| `/hello/world`                  | `[]`         | 100000   | 静态路径，最高   |\n\n### 手动设置优先级\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class PriorityController {\n  @HTTPMethod({\n    method: HTTPMethodEnum.GET,\n    path: '/(api|openapi)/version',\n    priority: 100000, // 提升优先级\n  })\n  async high() {}\n\n  @HTTPMethod({\n    method: HTTPMethodEnum.POST,\n    path: '/(api|openapi)/(.+)',\n  })\n  async low() {}\n}\n```\n\n---\n\n## 参数装饰器详解\n\n### @HTTPParam\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：从 URL 路径中提取参数\n\n**语法**：`@HTTPParam(param?: HTTPParamParams)`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api/users' })\nexport class UserController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: ':userId/posts/:postId' })\n  async getPost(@HTTPParam() userId: string, @HTTPParam() postId: string) {\n    return { userId, postId };\n  }\n}\n```\n\n#### 使用要点\n\n- 参数类型必须是 `string`\n- 参数名默认和变量名相同\n- 支持正则表达式捕获：`path: '/files/(.*)'`\n- 使用 `{ name: '0' }` 获取正则第一个匹配\n- 支持多参数路径\n\n#### 示例\n\n```typescript\n// 路径参数\n@HTTPController({ path: '/api/users' })\nexport class UserController {\n  // GET /users/:id\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: ':id' })\n  async getUser(@HTTPParam() id: string) {\n    return { userId: id };\n  }\n\n  // GET /users/:userId/posts/:postId\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: ':userId/posts/:postId' })\n  async getPost(@HTTPParam() userId: string, @HTTPParam() postId: string) {\n    return { userId, postId };\n  }\n\n  // 获取正则匹配\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/files/(.*)' })\n  async getFile(@HTTPParam({ name: '0' }) path: string) {\n    return { path };\n  }\n}\n```\n\n---\n\n### @HTTPQuery\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：从 URL 查询字符串提取参数（`?key=value`）\n\n**语法**：\n\n- `@HTTPQuery(param?: HTTPQueryParams)` - 返回首个匹配的值（`string`）\n- `@HTTPQueries(param?: HTTPQueriesParams)` - 返回全部值的数组（`string[]`）\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api/search' })\nexport class SearchController {\n  // GET /api/search?category=books\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })\n  async searchByCategory(@HTTPQuery() category: string) {\n    return { category };\n  }\n\n  // GET /api/search?tag=tech&tag=dev\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })\n  async searchByTags(@HTTPQueries({ name: 'tag' }) tags: string[]) {\n    return { tags };\n  }\n}\n```\n\n#### 使用要点\n\n- `@HTTPQuery`：返回首个匹配的值\n- `@HTTPQueries`：返回全部值的数组\n- 参数名默认与变量名匹配\n- 使用 `{ name: 'key' }` 指定查询参数名\n\n---\n\n### @HTTPBody\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：从请求体提取数据（POST/PUT 请求的 body）\n\n**语法**：`@HTTPBody()`\n\n**类型**：`object | string | FormData`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api/users' })\nexport class UserController {\n  @HTTPMethod({ method: HTTPMethodEnum.POST, path: '/' })\n  async createUser(@HTTPBody() body: { name: string; email: string }) {\n    return { userId: '123', ...body };\n  }\n}\n```\n\n#### Content-Type 解析\n\n| Content-Type                        | 解析结果        |\n| ----------------------------------- | --------------- |\n| `application/json`                  | 对象 `object`   |\n| `text/plain`                        | 字符串 `string` |\n| `application/x-www-form-urlencoded` | 对象 `object`   |\n\n**注意**：其他类型注入空值，需用 `@Request` 手动处理\n\n---\n\n### @HTTPHeaders\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：获取 HTTP 请求头中的字段\n\n**语法**：`@HTTPHeaders()`\n\n**类型**：`IncomingHttpHeaders`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class HeaderController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/info' })\n  async getInfo(@HTTPHeaders() headers: IncomingHttpHeaders) {\n    const auth = headers['authorization'];\n    const custom = headers['x-custom'];\n    return { auth, custom };\n  }\n}\n```\n\n#### 使用要点\n\n- Headers key 会自动转为小写，取值时使用小写字符\n- 获取单个值：`headers['x-custom']`\n\n---\n\n### @Cookies\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：从 HTTP Cookie 中读取会话数据\n\n**语法**：`@Cookies()`\n\n**类型**：`HTTPCookies`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class SessionController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/session' })\n  async getSession(@Cookies() cookies: HTTPCookies) {\n    const session = cookies.get('sessionId');\n    return { session };\n  }\n}\n```\n\n#### 使用要点\n\n- 使用 `cookies.get(key)` 读取 Cookie\n- 使用 `{ signed: false }` 读取未签名的 Cookie\n\n---\n\n### @Request\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：访问完整的 HTTP 请求对象\n\n**语法**：`@Request()`\n\n**类型**：`HTTPRequest`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class RequestController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/debug' })\n  async getDebug(@Request() request: HTTPRequest) {\n    const url = request.url;\n    const method = request.method;\n    const contentType = request.headers.get('content-type');\n    const rawBody = await request.text();\n    return { url, method, contentType, bodyLength: rawBody.length };\n  }\n}\n```\n\n#### 使用要点\n\n- `request.url`：请求 URL\n- `request.method`：请求方法\n- `request.headers`：Headers 对象\n- `request.text()`：读取请求体为文本\n- `request.arrayBuffer()`：读取请求体为 ArrayBuffer\n\n### @Context\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：访问 Egg 框架的 Context 对象\n\n**语法**：`@Context()`\n\n**类型**：`EggContext`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class DebugController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/debug' })\n  async debug(@Context() ctx: EggContext) {\n    return {\n      app: ctx.app.name,\n      ip: ctx.ip,\n      userAgent: ctx.get('user-agent'),\n    };\n  }\n}\n```\n","isInternal":false,"tokens":3327,"sizeBytes":11796},{"name":"mcp-controller.md","path":"packages/skills/egg-controller/references/mcp-controller.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/egg-controller/references/mcp-controller.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# MCPController 开发指南\n\n## 常见错误\n\n生成 MCPController 代码时，**必须**注意以下易错点：\n\n| 错误写法                         | 正确写法                              | 说明                                     |\n| -------------------------------- | ------------------------------------- | ---------------------------------------- |\n| `from 'egg'`                     | `from '@eggjs/tegg'`                  | 所有 MCP 装饰器和类型来自 `@eggjs/tegg`  |\n| `import z from 'zod'`            | `import { z } from '@eggjs/tegg/zod'` | 框架内置 zod，必须使用具名导入           |\n| `z.object({ name: z.string() })` | `{ name: z.string() }`                | Schema 使用普通对象，不要用 `z.object()` |\n| `args: ToolArgs<MySchema>`       | `args: ToolArgs<typeof MySchema>`     | 类型参数必须用 `typeof`                  |\n| `@MCPController` 不加括号        | `@MCPController()`                    | 装饰器必须带括号调用                     |\n\n---\n\n## 文件约定\n\n### 文件位置与命名\n\nMCPController 放在 module 的 `controller/` 目录下，命名规则为 `{Name}MCPController.ts`：\n\n```\napp/module-name/\n├── controller/\n│   ├── PackageMCPController.ts    ← MCP 控制器\n│   └── PackageHTTPController.ts   ← 同模块可共存 HTTP 控制器\n└── service/\n    └── PackageService.ts\n```\n\n### 插件配置\n\n在 `config/plugin.ts` 中启用：\n\n```typescript\nplugin.mcpProxy = true;\n```\n\n### 路径配置\n\n在 `config/config.default.ts` 中配置 MCP 路径（通常不需要修改，以下为默认值）：\n\n```typescript\nimport { randomUUID } from 'node:crypto';\n\nexport default () => {\n  const config = {\n    mcp: {\n      sseInitPath: '/mcp/sse',\n      sseMessagePath: '/mcp/message',\n      streamPath: '/mcp/stream',\n      statelessStreamPath: '/mcp/stateless/stream',\n      sessionIdGenerator: randomUUID,\n    },\n  };\n  return config;\n};\n```\n\n当使用 `@MCPController({ name: 'myServer' })` 声明命名服务时，路径自动变为：\n\n- `/mcp/myServer/sse`\n- `/mcp/myServer/message`\n- `/mcp/myServer/stream`\n- `/mcp/myServer/stateless/stream`\n\n### AccessLevel\n\n`@MCPController` 装饰器内部已默认设置 AccessLevel（PUBLIC），不需要再手动声明。\n\n---\n\n## 场景决策树\n\n```\n用户需要什么？\n\n├─ \"让 AI 能查数据 / 执行操作\"\n│  └─ → @MCPTool + @Inject Service 处理业务\n│\n├─ \"给 AI 一个提示词模板\"\n│  └─ → @MCPPrompt\n│\n├─ \"让 AI 读取某类资源数据\"\n│  ├─ 资源地址固定 → @MCPResource({ uri: '...' })\n│  └─ 资源地址动态 → @MCPResource({ template: [...] })\n│\n├─ \"Tool 执行中要推送进度\"\n│  └─ → @MCPTool + @Extra() 获取 sendNotification（见下方 @Extra 章节）\n│\n└─ \"Tool 中需要读取自定义请求头\"\n   └─ → @MCPTool + @Extra() 获取 requestInfo.headers\n```\n\n---\n\n## 端到端完整示例\n\n以下展示一个完整的 MCP 功能从配置到测试的所有文件：\n\n### 1. 插件配置 — `config/plugin.ts`\n\n```typescript\nplugin.mcpProxy = true;\n```\n\n### 2. 控制器 — `app/npm/controller/PackageMCPController.ts`\n\n```typescript\nimport {\n  MCPController,\n  MCPTool,\n  MCPToolResponse,\n  MCPPrompt,\n  MCPPromptResponse,\n  MCPResource,\n  MCPResourceResponse,\n  ToolArgs,\n  ToolArgsSchema,\n  PromptArgs,\n  PromptArgsSchema,\n  Inject,\n} from '@eggjs/tegg';\nimport { z } from '@eggjs/tegg/zod';\n\nimport { PackageService } from '../service/PackageService.ts';\n\nconst SearchSchema = {\n  name: z.string({ description: 'npm package name' }),\n};\n\nconst SummarySchema = {\n  name: z.string(),\n};\n\n@MCPController()\nexport class PackageMCPController {\n  @Inject()\n  private readonly packageService: PackageService;\n\n  @MCPTool({ description: 'Search npm package info' })\n  async searchPackage(@ToolArgsSchema(SearchSchema) args: ToolArgs<typeof SearchSchema>): Promise<MCPToolResponse> {\n    const pkg = await this.packageService.findByName(args.name);\n    if (!pkg) {\n      return { content: [{ type: 'text', text: `Package ${args.name} not found` }] };\n    }\n    return { content: [{ type: 'text', text: JSON.stringify(pkg) }] };\n  }\n\n  @MCPPrompt({ description: 'Generate package summary' })\n  async summarize(@PromptArgsSchema(SummarySchema) args: PromptArgs<typeof SummarySchema>): Promise<MCPPromptResponse> {\n    return {\n      messages: [\n        {\n          role: 'user',\n          content: {\n            type: 'text',\n            text: `Summarize the npm package: ${args.name}`,\n          },\n        },\n      ],\n    };\n  }\n\n  @MCPResource({\n    template: ['npm://{name}/{?version}', { list: undefined }],\n  })\n  async getPackageReadme(uri: URL): Promise<MCPResourceResponse> {\n    const name = uri.hostname;\n    const readme = await this.packageService.getReadme(name);\n    return { contents: [{ uri: uri.toString(), text: readme }] };\n  }\n}\n```\n\n### 3. Service — `app/npm/service/PackageService.ts`\n\n```typescript\nimport { SingletonProto } from 'egg';\n\n@SingletonProto()\nexport class PackageService {\n  async findByName(name: string) {\n    // 业务逻辑\n  }\n\n  async getReadme(name: string): Promise<string> {\n    // 业务逻辑\n  }\n}\n```\n\n### 4. 单元测试 — `test/npm/controller/PackageMCPController.test.ts`\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from 'egg-mock/bootstrap';\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\n\ndescribe('PackageMCPController', () => {\n  it('should search package via tool', async () => {\n    app.mockCsrf();\n    const client: Client = await app.mcpClient();\n\n    const tools = await client.listTools();\n    assert(tools.tools.some((t) => t.name === 'searchPackage'));\n\n    const res = await client.callTool({\n      name: 'searchPackage',\n      arguments: { name: 'egg' },\n    });\n    assert(res.content[0].type === 'text');\n  });\n\n  it('should get prompt', async () => {\n    app.mockCsrf();\n    const client: Client = await app.mcpClient();\n\n    const res = await client.getPrompt({\n      name: 'summarize',\n      arguments: { name: 'egg' },\n    });\n    assert(res.messages.length > 0);\n  });\n\n  it('should read resource', async () => {\n    app.mockCsrf();\n    const client: Client = await app.mcpClient();\n\n    const res = await client.readResource({\n      uri: 'npm://egg?version=4.0.0',\n    });\n    assert(res.contents.length > 0);\n  });\n});\n```\n\n---\n\n## @Extra() 的使用场景\n\n`@Extra()` 装饰器注入 `ToolExtra` 对象，提供两个能力：\n\n### 发送通知（长任务进度推送）\n\n```typescript\n@MCPTool()\nasync longTask(\n  @ToolArgsSchema(Schema) args: ToolArgs<typeof Schema>,\n  @Extra() extra: ToolExtra,\n): Promise<MCPToolResponse> {\n  const { sendNotification } = extra;\n  for (let i = 0; i < 10; i++) {\n    await sendNotification({\n      method: 'notifications/message',\n      params: { level: 'info', data: `Step ${i + 1}/10` },\n    });\n    // ... 执行步骤\n  }\n  return { content: [{ type: 'text', text: 'Done' }] };\n}\n```\n\n### 读取自定义请求头\n\n```typescript\n@MCPTool()\nasync myTool(\n  @ToolArgsSchema(Schema) args: ToolArgs<typeof Schema>,\n  @Extra() extra: ToolExtra,\n): Promise<MCPToolResponse> {\n  const headers = extra.requestInfo?.headers;\n  // 处理自定义 header\n}\n```\n\n---\n\n## 装饰器参考\n\n| 装饰器                | 用途         | 常用参数                                   | 返回类型              |\n| --------------------- | ------------ | ------------------------------------------ | --------------------- |\n| `@MCPController()`    | 声明控制器   | `{ name?: string }`                        | -                     |\n| `@MCPTool()`          | 声明工具     | `{ name?: string, description?: string }`  | `MCPToolResponse`     |\n| `@MCPPrompt()`        | 声明提示词   | `{ name?: string, description?: string }`  | `MCPPromptResponse`   |\n| `@MCPResource()`      | 声明资源     | `{ uri: string }` 或 `{ template: [...] }` | `MCPResourceResponse` |\n| `@ToolArgsSchema()`   | Tool 参数    | Zod Schema 普通对象                        | -                     |\n| `@PromptArgsSchema()` | Prompt 参数  | Zod Schema 普通对象                        | -                     |\n| `@Extra()`            | 额外上下文   | -                                          | `ToolExtra`           |\n| `@Inject()`           | 注入 Service | -                                          | -                     |\n\n**注意**：`@MCPController` 的 `version`、`timeout` 等参数通常不需要配置。\n","isInternal":false,"tokens":2332,"sizeBytes":8563},{"name":"middleware.md","path":"packages/skills/egg-controller/references/middleware.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/egg-controller/references/middleware.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Middleware 中间件指南\n\n## 常见错误\n\n| 错误写法                                   | 正确写法                           | 说明                                            |\n| ------------------------------------------ | ---------------------------------- | ----------------------------------------------- |\n| `import { Middleware } from '@eggjs/tegg'` | `import { Middleware } from 'egg'` | Middleware 从 `egg` 导入                        |\n| `import { Advice } from 'egg'`             | `import { Advice } from 'egg/aop'` | AOP 装饰器从 `egg/aop` 导入                     |\n| `@Middleware(funcMw, AdviceClass)`         | 分开写两个 `@Middleware`           | 同一个 `@Middleware()` 中不能混用函数式和 AOP   |\n| AOP Advice 中用实例属性存请求级状态        | 使用 `ctx.set()`/`ctx.get()`       | Advice 默认 Singleton，实例属性会被并发请求共享 |\n| 把中间件文件放在 `app/middleware/`         | 放在模块目录下                     | 函数式中间件放在模块中，通过 import 引用        |\n\n---\n\n## 两种中间件模式\n\nEgg 的 `@Middleware` 装饰器支持两种中间件写法，根据传入参数类型自动识别：\n\n- **AOP 写法（推荐）**：使用 `@Advice` 类，支持 `@Inject` 注入 Proto 依赖，拥有丰富的生命周期钩子\n- **函数式写法（旧版兼容）**：标准 Koa 中间件函数，需要从 `ctx` 对象上手动获取依赖\n\n新项目应优先使用 AOP 写法。函数式写法主要用于兼容旧的 egg 中间件或非常简单的场景。\n\n---\n\n## 实现中间件\n\n### AOP 写法（推荐）\n\n使用 `@Advice()` 装饰器定义类，实现 `IAdvice` 的 `around` 方法，写法与 Koa 中间件一致（`next` 调用目标方法）。Advice 本身是 Proto，支持 `@Inject` 注入依赖。`around` 中可以修改入参（`ctx.args`）和返回值：\n\n```typescript\n// app/modules/foo/advice/LogAdvice.ts\nimport { AccessLevel, Inject, Logger } from 'egg';\nimport { Advice, IAdvice, AdviceContext } from 'egg/aop';\n\n// 跨模块使用时需设置 accessLevel: AccessLevel.PUBLIC\n@Advice({ accessLevel: AccessLevel.PUBLIC })\nexport class LogAdvice implements IAdvice {\n  @Inject()\n  logger: Logger;\n\n  async around(ctx: AdviceContext, next: () => Promise<any>): Promise<any> {\n    // 修改入参：ctx.args 对应控制器方法的参数列表\n    // ctx.args[0] = sanitize(ctx.args[0]);\n\n    const start = Date.now();\n    const result = await next();\n    this.logger.info('%s cost %dms', ctx.method, Date.now() - start);\n\n    // 修改返回值：直接返回新的值即可\n    return { success: true, data: result };\n  }\n}\n```\n\n### 函数式写法（旧版兼容）\n\n标准 Koa 中间件函数，签名为 `(ctx: Context, next: Next) => Promise<void>`。旧版 egg 写法，无法使用 `@Inject`，需要从 `ctx` 对象上手动获取依赖：\n\n```typescript\n// app/modules/foo/middleware/count.ts\nimport type { Context, Next } from 'egg';\n\nexport async function countMw(ctx: Context, next: Next): Promise<void> {\n  const start = Date.now();\n  await next();\n  ctx.set('X-Response-Time', `${Date.now() - start}ms`);\n}\n```\n\n---\n\n## 应用中间件\n\n通过 `@Middleware()` 装饰器将中间件应用到控制器，支持类级别和方法级别：\n\n```typescript\n// app/modules/foo/FooController.ts\nimport { HTTPController, HTTPMethod, HTTPMethodEnum, Middleware } from 'egg';\nimport { LogAdvice } from '../common/advice/LogAdvice.ts';\nimport { countMw } from './middleware/count.ts';\n\n@HTTPController({ path: '/api' })\n@Middleware(LogAdvice) // 类级别：所有方法都会执行\nexport class FooController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/profile' })\n  @Middleware(countMw) // 方法级别：仅此方法执行\n  async getProfile() {\n    return { name: 'test' };\n  }\n}\n```\n\n---\n\n## 执行顺序\n\n遵循洋葱模型，类级别先执行，方法级别后执行：\n\n```typescript\n@Middleware(globalMw)\nexport class FooController {\n  // 进：globalMw → methodMw → hello()\n  // 出：hello() → methodMw → globalMw\n  @Middleware(methodMw)\n  async hello() {}\n\n  // 多个 @Middleware 从下往上执行（靠近方法的先注册）\n  // 进：globalMw → mw3 → mw2 → mw1 → multiple()\n  // 出：multiple() → mw1 → mw2 → mw3 → globalMw\n  @Middleware(mw1)\n  @Middleware(mw2)\n  @Middleware(mw3)\n  async multiple() {}\n}\n```\n\n**若混用函数式和 AOP 中间件，所有函数式中间件（无论类级别还是方法级别）会先于所有 AOP 中间件执行。** 即函数式和 AOP 分属两个独立的执行阶段，函数式阶段在前，AOP 阶段在后：\n\n```typescript\n@Middleware(countMw) // 函数式 - 类级别\n@Middleware(LogAdvice) // AOP - 类级别\nexport class FooController {\n  @Middleware(timeMw) // 函数式 - 方法级别\n  @Middleware(AuthAdvice) // AOP - 方法级别\n  async hello() {}\n}\n\n// 实际执行顺序：\n// countMw → timeMw → LogAdvice → AuthAdvice → hello()\n// （先所有函数式，再所有 AOP；各阶段内类级别先于方法级别）\n```\n","isInternal":false,"tokens":1480,"sizeBytes":5082},{"name":"schedule.md","path":"packages/skills/egg-controller/references/schedule.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/egg-controller/references/schedule.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# 定时任务开发指南\n\n## 注意事项\n\n- **不要将代码放在 `app/schedule` 目录下**，egg 默认会扫描该路径注册定时任务，会和装饰器方式冲突\n- 定时任务类必须包含一个 `subscribe` 方法，框架调度时会调用该方法\n- import 路径是 `egg/schedule`，不是 `egg`\n\n## Step 1: 创建定时任务\n\n使用 `@Schedule` 装饰器标识一个类为定时任务，支持 interval 和 cron 两种调度模式。\n\n### interval 模式\n\n按固定间隔执行。`interval` 支持毫秒数或 [ms](https://github.com/vercel/ms) 格式字符串（如 `'5s'`、`'1m'`）。\n\n```typescript\n// app/{moduleName}/schedule/Demo.ts\nimport { Inject, Logger } from 'egg';\nimport { IntervalParams, Schedule, ScheduleType } from 'egg/schedule';\n\n@Schedule<IntervalParams>({\n  type: ScheduleType.WORKER,\n  scheduleData: {\n    interval: '5s',\n  },\n})\nexport class DemoScheduler {\n  @Inject()\n  private logger: Logger;\n\n  async subscribe() {\n    this.logger.info('schedule called');\n  }\n}\n```\n\n### cron 模式\n\n按 cron 表达式执行，格式参考 [cron-parser](https://github.com/harrisiirak/cron-parser)：\n\n```text\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```typescript\n// app/{moduleName}/schedule/CronDemo.ts\nimport { Inject, Logger } from 'egg';\nimport { CronParams, Schedule, ScheduleType } from 'egg/schedule';\n\n@Schedule<CronParams>({\n  type: ScheduleType.WORKER,\n  scheduleData: {\n    cron: '0 0 3 * * *', // 每日 3 点执行\n  },\n})\nexport class CronScheduler {\n  @Inject()\n  private logger: Logger;\n\n  async subscribe() {\n    this.logger.info('schedule called');\n  }\n}\n```\n\n## Step 2: 选择工作模式\n\n| 模式                  | 说明                                     | 使用场景                             |\n| --------------------- | ---------------------------------------- | ------------------------------------ |\n| `ScheduleType.WORKER` | 每台机器只有一个 worker 执行（随机选择） | 大多数场景，如数据同步、缓存刷新     |\n| `ScheduleType.ALL`    | 每台机器的所有 worker 都执行             | 需要每个 worker 都更新本地状态的场景 |\n\n## Step 3: 配置运行参数\n\n`@Schedule` 装饰器支持第二个参数，控制定时任务的运行行为：\n\n```typescript\n@Schedule<IntervalParams>(\n  {\n    type: ScheduleType.WORKER,\n    scheduleData: {\n      interval: '1m',\n    },\n  },\n  {\n    immediate: true, // 应用启动后立即执行一次\n    // disable: true,           // 禁用该定时任务\n    env: ['devserver', 'test'], // 仅在指定环境下启动\n  },\n)\nexport class MyScheduler {\n  async subscribe() {\n    // ...\n  }\n}\n```\n\n| 参数        | 类型     | 说明                            |\n| ----------- | -------- | ------------------------------- |\n| `immediate` | boolean  | 应用启动并 ready 后立即执行一次 |\n| `disable`   | boolean  | 设为 true 时不启动该定时任务    |\n| `env`       | string[] | 仅在指定环境下启动              |\n","isInternal":false,"tokens":989,"sizeBytes":3504},{"name":"SKILL.md","path":"packages/skills/egg-controller/SKILL.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/egg-controller/SKILL.md","title":"egg-controller","category":"anthropic-skill","format":"markdown","content":"---\nname: egg-controller\ndescription: Use when creating API endpoints, implementing protocol handlers, exposing interfaces for specific clients, adding parameter validation, or applying middleware to controllers. Covers HTTP, MCP and Schedule controllers, Ajv/TypeBox parameter validation, and Middleware (function-style and AOP) for EGG framework applications.\nallowed-tools: Read\n---\n\n# EGG 控制器\n\n---\n\n## 控制器选择决策树\n\n```\n需要暴露什么接口/客户端协议？\n\n1. HTTP 接口？例如 HTML/JSON/SSR/SSE，可以使用 HTTPController，参考 `references/http-controller.md`\n\n2. 定时任务，可以使用 Schedule，参考 `references/schedule.md`\n\n3. MCP 接口，可以使用 MCPController，参考 `references/mcp-controller.md`\n\n需要做参数校验？\n\n4. 使用 Ajv + TypeBox 做入参校验，参考 `references/ajv-validate.md`\n\n需要给控制器加中间件（日志、鉴权、耗时统计等横切逻辑）？\n\n5. 使用 Middleware，支持函数式写法和 AOP 写法，参考 `references/middleware.md`\n```\n\n---\n\n## 控制器快速参考\n\n### HTTPController\n\n- **装饰器**：`@HTTPController`、`@HTTPMethod`\n- **参数**：`@HTTPParam`、`@HTTPQuery`、`@HTTPBody`、`@HTTPHeaders`、`@Cookies`、`@Request`、`@Context`\n- **详细文档**：`references/http-controller.md`\n\n### MCPController\n\n- **装饰器**：`@MCPController`、`@MCPTool`、`@MCPPrompt`、`@MCPResource`\n- **特点**：集成 LLM、Zod 验证、登录态支持\n- **详细文档**：`references/mcp-controller.md`\n\n### Schedule\n\n- **装饰器**：`@Schedule<T>`\n- **模式**：Worker/All\n- **详细文档**：`references/schedule.md`\n\n### Ajv 参数校验\n\n- **导入**：`import { Ajv, Type, Static } from 'egg/ajv'`\n- **方式**：`@Inject() ajv: Ajv`，调用 `ajv.validate(schema, data)`\n- **特点**：TypeBox 定义一次 Schema，同时获得校验和 TypeScript 类型\n- **详细文档**：`references/ajv-validate.md`\n\n### Middleware 中间件\n\n- **导入**：`import { Middleware } from 'egg'`（AOP 类从 `import { Advice } from 'egg/aop'`）\n- **写法**：函数式（Koa 中间件函数）和 AOP（@Advice 类），通过 `@Middleware()` 应用\n- **特点**：支持类级别和方法级别，函数式和 AOP 不能在同一个 `@Middleware()` 中混用\n- **详细文档**：`references/middleware.md`\n\n---\n\n## 常见问题排查\n\n| 现象                   | 原因                                     | 解决方案                                                           |\n| ---------------------- | ---------------------------------------- | ------------------------------------------------------------------ |\n| MCP 装饰器 import 报错 | 从 `'egg'` 导入                          | MCP 装饰器从 `'@eggjs/tegg'` 导入，zod 从 `'@eggjs/tegg/zod'` 导入 |\n| MCP Schema 报错        | 用了 `z.object()` 包装                   | 直接用普通对象 `{ name: z.string() }`                              |\n| 定时任务不生效         | 放在 `app/schedule` 目录                 | 放在模块目录中，避免和 egg 默认扫描冲突                            |\n| Ajv 校验 import 报错   | 从 `typebox` 或 `ajv` 导入               | 统一从 `'egg/ajv'` 导入 Type、Static、Ajv 等                       |\n| type 推导不完整        | 用 `type` 定义                           | 用 `interface Foo extends Static<typeof Schema> {}` 代替           |\n| Middleware 混用报错    | 函数和 Advice 类放同一个 `@Middleware()` | 分开写多个 `@Middleware()`，每个内部类型一致                       |\n| AOP Middleware 不生效  | Advice 类没加 `@Advice()` 装饰器         | 必须同时有 `@Advice()` 装饰器才能被识别为 AOP                      |\n\n---\n\n## 最佳实践\n\n- **控制器精简**：业务逻辑委托给 Service 层\n- **参数验证**：使用装饰器和类型定义\n- **错误处理**：根据协议转换错误和响应码\n- **RESTful 设计**：遵循 HTTP 方法和资源命名\n- **响应一致性**：统一响应格式\n\n---\n\n## 参考资料\n\n详细的控制器开发文档：\n\n- `references/http-controller.md` - HTTP 接口完整指南\n- `references/mcp-controller.md` - MCP 接口开发\n- `references/schedule.md` - 定时任务\n- `references/ajv-validate.md` - Ajv 参数校验\n- `references/middleware.md` - Middleware 中间件（函数式 + AOP）\n\n核心概念（`egg-core` skill）：模块、依赖注入、对象生命周期\n\n单元测试（`egg-unittest` skill）：HTTP 接口测试、Service 测试、Mock\n","frontmatter":{"name":"egg-controller","description":"Use when creating API endpoints, implementing protocol handlers, exposing interfaces for specific clients, adding parameter validation, or applying middleware to controllers. Covers HTTP, MCP and Schedule controllers, Ajv/TypeBox parameter validation, and Middleware (function-style and AOP) for EGG framework applications.","allowed-tools":"Read","allowedTools":["Read"]},"allowedTools":["Read"],"isInternal":false,"tokens":1371,"sizeBytes":4548},{"name":"aop.md","path":"packages/skills/egg-core/references/aop.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/egg-core/references/aop.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# AOP 切面编程指南\n\n## 常见错误\n\n| 错误写法                                   | 正确写法                              | 说明                                                                                   |\n| ------------------------------------------ | ------------------------------------- | -------------------------------------------------------------------------------------- |\n| `import { Advice } from 'egg'`             | `import { Advice } from 'egg/aop'`    | AOP 装饰器从 `egg/aop` 导入，不是 `egg`                                                |\n| `import { Advice } from '@eggjs/tegg/aop'` | `import { Advice } from 'egg/aop'`    | 统一从 `egg/aop` 导入                                                                  |\n| Advice 类没有加 `@Advice()` 装饰器         | 必须同时有 `@Advice()`                | `@Pointcut` 和 `@Crosscut` 都要求目标是 Advice 类                                      |\n| `@Crosscut` 直接切 Egg 内置对象            | 只能切 tegg Proto 对象                | Egg 中的对象（如 app、ctx）无法被 Crosscut                                             |\n| Advice 中用实例属性存状态                  | 使用 `ctx.set()`/`ctx.get()` 共享状态 | Advice 默认是 Singleton，实例属性会被并发请求共享，必须用 AdviceContext 传递调用级状态 |\n\n---\n\n## 核心概念\n\n### Advice（切面逻辑）\n\nAdvice 是 AOP 的核心，定义了在目标方法执行前后要做什么。Advice 本身也是一种 Proto，默认 initType 为 Singleton（全局单例），可以使用 `@Inject` 注入依赖。如需每请求一个实例，显式指定 `@Advice({ initType: ObjectInitType.CONTEXT })`。\n\n```typescript\nimport { Advice, IAdvice, AdviceContext } from 'egg/aop';\nimport { Inject, Logger } from 'egg';\n\n@Advice()\nexport class LogAdvice implements IAdvice {\n  @Inject()\n  private logger: Logger;\n\n  async beforeCall(ctx: AdviceContext): Promise<void> {\n    ctx.set('startTime', Date.now());\n  }\n\n  async afterReturn(ctx: AdviceContext, result: any): Promise<void> {\n    // 方法成功返回后执行\n  }\n\n  async afterThrow(ctx: AdviceContext, error: Error): Promise<void> {\n    // 方法抛出异常后执行\n  }\n\n  async afterFinally(ctx: AdviceContext): Promise<void> {\n    const duration = Date.now() - ctx.get('startTime');\n    this.logger.info('%s cost %dms', String(ctx.method), duration);\n  }\n\n  async around(ctx: AdviceContext, next: () => Promise<any>): Promise<any> {\n    // 类似 koa 中间件，可以包裹方法执行\n    return await next();\n  }\n}\n```\n\n**执行顺序：**\n\n```typescript\nawait beforeCall(ctx);\ntry {\n  const result = await around(ctx, next); // next 执行目标方法\n  await afterReturn(ctx, result);\n  return result;\n} catch (e) {\n  await afterThrow(ctx, e);\n  throw e;\n} finally {\n  await afterFinally(ctx);\n}\n```\n\n**关键点：**\n\n- 所有 hook 方法都是可选的，按需实现\n- 只有 `around` 可以修改返回值\n- `beforeCall` 中可以通过修改 `ctx.args` 改变方法入参\n- 多个 Advice 之间通过 `ctx.set(key, value)` / `ctx.get(key)` 共享状态\n\n### AdviceContext\n\n```typescript\ninterface AdviceContext<T = object, K = any> {\n  that: T; // 被切的对象实例\n  method: PropertyKey; // 被切的方法名\n  args: any[]; // 方法参数（可修改）\n  adviceParams?: K; // 装饰器透传的参数\n  get(key: PropertyKey): any; // 获取共享状态\n  set(key: PropertyKey, value: any): this; // 设置共享状态\n}\n```\n\n---\n\n## Pointcut vs Crosscut\n\n### Pointcut — 精确切入\n\n在特定类的特定方法上声明 Advice，适合精确控制：\n\n```typescript\nimport { SingletonProto } from 'egg';\nimport { Pointcut } from 'egg/aop';\n\n@SingletonProto()\nexport class OrderService {\n  @Pointcut(LogAdvice)\n  async createOrder(data: any) {\n    // 业务逻辑\n  }\n\n  // 可以传参给 Advice\n  @Pointcut(TransactionAdvice, { adviceParams: { propagation: 'REQUIRED' } })\n  async updateOrder(id: string, data: any) {\n    // 业务逻辑\n  }\n}\n```\n\n### Crosscut — 批量切入\n\n在 Advice 类上声明切入规则，适合横切多个类/方法：\n\n```typescript\nimport { Crosscut, Advice, IAdvice, PointcutType } from 'egg/aop';\n\n// 模式 1：指定类和方法\n@Crosscut({\n  type: PointcutType.CLASS,\n  clazz: OrderService,\n  methodName: 'createOrder',\n})\n@Advice()\nexport class AuditAdvice implements IAdvice {\n  async afterReturn(ctx: AdviceContext): Promise<void> {\n    // 审计日志\n  }\n}\n\n// 模式 2：正则匹配\n@Crosscut({\n  type: PointcutType.NAME,\n  className: /.*Service$/i,\n  methodName: /^(create|update|delete)/,\n})\n@Advice()\nexport class OperationLogAdvice implements IAdvice {\n  async around(ctx: AdviceContext, next: () => Promise<any>): Promise<any> {\n    // 记录所有 Service 的写操作\n    return await next();\n  }\n}\n\n// 模式 3：自定义回调\n@Crosscut({\n  type: PointcutType.CUSTOM,\n  callback: (clazz, method) => {\n    return clazz.name.endsWith('Repository') && method !== 'constructor';\n  },\n})\n@Advice()\nexport class DbMetricsAdvice implements IAdvice {}\n```\n\n### 执行顺序\n\n- `@Crosscut` 默认 order: `100`\n- `@Pointcut` 默认 order: `1000`\n- order 越小越先执行\n- 同一方法上多个 Advice 按 order 升序排列\n\n通过 `order` 参数自定义顺序：\n\n```typescript\n// Pointcut：第二个参数中设置 order\n@Pointcut(LogAdvice, { order: 50 })\nasync createOrder(data: any) {}\n\n// Crosscut：第二个参数中设置 order\n@Crosscut(\n  { type: PointcutType.NAME, className: /.*Service$/i, methodName: /.+/ },\n  { order: 200 },\n)\n@Advice()\nexport class MetricsAdvice implements IAdvice {}\n```\n\n---\n\n## 参数透传\n\n同一个 Advice 在不同方法上可能需要不同的行为，通过 `adviceParams` 传参：\n\n```typescript\n@Advice()\nexport class CacheAdvice implements IAdvice {\n  async around(ctx: AdviceContext<any, { ttl: number }>, next: () => Promise<any>): Promise<any> {\n    const ttl = ctx.adviceParams?.ttl ?? 60;\n    // 根据 ttl 实现缓存逻辑\n    return await next();\n  }\n}\n\n@SingletonProto()\nexport class UserService {\n  @Pointcut(CacheAdvice, { adviceParams: { ttl: 3600 } })\n  async getUser(id: string) {\n    /* ... */\n  }\n\n  @Pointcut(CacheAdvice, { adviceParams: { ttl: 60 } })\n  async getUserList() {\n    /* ... */\n  }\n}\n```\n\n---\n\n## 典型场景\n\n| 场景                        | 推荐方式                 | 说明              |\n| --------------------------- | ------------------------ | ----------------- |\n| 特定方法加日志/缓存         | `@Pointcut(Advice)`      | 精确控制          |\n| 所有 Service 的写操作加审计 | `@Crosscut(NAME)` + 正则 | 批量匹配          |\n| 事务包裹                    | `@Pointcut(TxAdvice)`    | around 中管理事务 |\n","isInternal":false,"tokens":1843,"sizeBytes":6788},{"name":"background-task.md","path":"packages/skills/egg-core/references/background-task.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/egg-core/references/background-task.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# BackgroundTask 异步任务指南\n\n## 常见错误\n\n| 错误写法                                                    | 正确写法                                        | 说明                                                      |\n| ----------------------------------------------------------- | ----------------------------------------------- | --------------------------------------------------------- |\n| `setTimeout(() => { ... }, 0)`                              | `backgroundTaskHelper.run(async () => { ... })` | setTimeout 执行时上下文已释放，会导致访问已销毁对象的错误 |\n| `setImmediate(() => { ... })`                               | `backgroundTaskHelper.run(async () => { ... })` | 同上，框架不会等待 setImmediate 的回调                    |\n| `process.nextTick(() => { ... })`                           | `backgroundTaskHelper.run(async () => { ... })` | 同上                                                      |\n| 在 Service 中 `await` 后台任务                              | 直接调用 `run()` 不 await                       | `run()` 是\"发射后不管\"，不返回 Promise                    |\n| `import { BackgroundTaskHelper } from '@eggjs/tegg/helper'` | `import { BackgroundTaskHelper } from 'egg'`    | 统一从 `egg` 导入，不要从 `@eggjs/tegg` 子路径导入        |\n\n---\n\n## 使用方式\n\n### 基本用法\n\n通过 `@Inject()` 注入 `BackgroundTaskHelper`，调用 `run()` 方法。`run()` 接受一个异步函数，任务会立即开始执行但不阻塞当前流程。框架在请求结束（Context preDestroy）时会等待所有后台任务完成（最多等待 timeout），然后再释放上下文。\n\n```typescript\nimport { BackgroundTaskHelper, Inject, SingletonProto, AccessLevel } from 'egg';\n\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class MetricsService {\n  @Inject()\n  private backgroundTaskHelper: BackgroundTaskHelper;\n\n  async reportAfterResponse(data: Record<string, unknown>) {\n    // run() 是非阻塞的，任务立即开始执行但不会被 await\n    this.backgroundTaskHelper.run(async () => {\n      // 框架会保持上下文存活直到任务完成或超时\n      await this.sendMetrics(data);\n    });\n  }\n\n  private async sendMetrics(data: Record<string, unknown>) {\n    // 实际上报逻辑\n  }\n}\n```\n\n---\n\n## 超时配置\n\n框架默认等待 5 秒，超时后放弃等待并释放上下文。\n\n### 按请求调整超时\n\n```typescript\nthis.backgroundTaskHelper.timeout = 10000; // 10 秒\nthis.backgroundTaskHelper.run(async () => {\n  await heavyWork();\n});\n```\n\n注意：timeout 是 context 级别的设置，同一请求中多次设置以最后一次为准。\n\n### 全局配置\n\n```typescript\n// config/config.default.ts\nexport default {\n  backgroundTask: {\n    timeout: 10000, // 全局默认 10 秒\n  },\n};\n```\n\n### 无限等待\n\n```typescript\n// 不推荐在生产环境使用，除非确定任务一定会完成\nthis.backgroundTaskHelper.timeout = Infinity;\n```\n\n---\n\n## 错误处理\n\nBackgroundTaskHelper 内部会捕获所有错误并记录日志，**不会**抛出异常或影响其他后台任务。\n\n```typescript\nthis.backgroundTaskHelper.run(async () => {\n  // 即使这里抛出异常，也不会影响其他后台任务或框架\n  throw new Error('something went wrong');\n  // 错误会被记录为: [BackgroundTaskHelper] background throw error:something went wrong\n});\n\n// 如果需要自定义错误处理\nthis.backgroundTaskHelper.run(async () => {\n  try {\n    await unreliableOperation();\n  } catch (e) {\n    // 自定义错误处理：重试、告警等\n    await this.alertService.notify(e);\n  }\n});\n```\n\n---\n\n## 原理\n\n请求处理完成后，框架会立即释放上下文（防止内存泄漏）。如果用 `setTimeout` 等方式在上下文释放后访问注入的服务，会因为上下文已销毁而出错。\n\nBackgroundTaskHelper 的作用是：\n\n1. `run()` 被调用时，任务立即开始执行（不是延后到响应发送后）\n2. 任务不会被 await，因此不阻塞当前请求的返回\n3. 请求结束时（Context preDestroy），框架等待所有后台任务完成（最多等待 timeout）\n4. 等待结束后才释放上下文\n\n这就是为什么必须用 `backgroundTaskHelper.run()` 而不是 `setTimeout` — 后者绕过了框架的上下文生命周期管理，执行时上下文可能已被释放。\n\n---\n\n## 单元测试\n\nBackgroundTaskHelper 的测试方法参考 `egg-unittest` skill 的 `references/background-task-test.md`。\n","isInternal":false,"tokens":1174,"sizeBytes":4525},{"name":"dynamic-inject.md","path":"packages/skills/egg-core/references/dynamic-inject.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/egg-core/references/dynamic-inject.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# 动态注入开发指南\n\n## 何时使用\n\n- **用动态注入**：同一抽象有多种实现，运行时按参数选择（如多种支付方式、多种存储后端）\n- **用普通 `@Inject()`**：依赖只有一个实现，编译时就能确定\n\n## Step 1: 定义抽象类和类型枚举\n\n```typescript\n// AbstractHello.ts\nexport abstract class AbstractHello {\n  abstract hello(): string;\n}\n\n// HelloType.ts\nexport enum HelloType {\n  FOO = 'FOO',\n  BAR = 'BAR',\n}\n```\n\n如果类型是无限扩展的，没有固定枚举，可以用 `Record<string, string>` 代替：\n\n```typescript\ntype AnyEnum = Record<string, string>;\n```\n\n## Step 2: 创建自定义装饰器\n\n```typescript\n// decorator/Hello.ts\nimport { ImplDecorator, QualifierImplDecoratorUtil } from 'egg';\nimport { HelloType } from '../HelloType.ts';\nimport { AbstractHello } from '../AbstractHello.ts';\n\nexport const HELLO_ATTRIBUTE = Symbol('HELLO_ATTRIBUTE');\n\nexport const Hello: ImplDecorator<AbstractHello, typeof HelloType> = QualifierImplDecoratorUtil.generatorDecorator(\n  AbstractHello,\n  HELLO_ATTRIBUTE,\n);\n```\n\n**注意事项：**\n\n- **ATTRIBUTE（Symbol）不要重复**，重复会导致实现被覆盖\n- **抽象类不要指定错**，否则可能导致实现被覆盖\n\n## Step 3: 实现抽象类\n\n每个实现加上 `@SingletonProto()`（或 `@ContextProto()`）和自定义装饰器：\n\n```typescript\n// impl/FooHello.ts\nimport { SingletonProto } from 'egg';\nimport { Hello } from '../decorator/Hello.ts';\nimport { HelloType } from '../HelloType.ts';\nimport { AbstractHello } from '../AbstractHello.ts';\n\n@SingletonProto()\n@Hello(HelloType.FOO)\nexport class FooHello extends AbstractHello {\n  hello(): string {\n    return 'hello, foo';\n  }\n}\n```\n\n## Step 4: 动态获取实现\n\n通过 `EggObjectFactory` 在运行时按类型获取对应实现：\n\n```typescript\n// HelloService.ts\nimport { EggObjectFactory, SingletonProto, Inject } from 'egg';\nimport { HelloType } from './HelloType.ts';\nimport { AbstractHello } from './AbstractHello.ts';\n\n@SingletonProto()\nexport class HelloService {\n  @Inject()\n  private eggObjectFactory: EggObjectFactory;\n\n  async hello(type: HelloType): Promise<string> {\n    const helloImpl = await this.eggObjectFactory.getEggObject(AbstractHello, type);\n    return helloImpl.hello();\n  }\n}\n```\n","isInternal":false,"tokens":642,"sizeBytes":2305},{"name":"eventbus.md","path":"packages/skills/egg-core/references/eventbus.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/egg-core/references/eventbus.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# EventBus 事件总线指南\n\n## 常见错误\n\n| 错误写法                                         | 正确写法                                    | 说明                                             |\n| ------------------------------------------------ | ------------------------------------------- | ------------------------------------------------ |\n| `import { EventBus } from '@eggjs/tegg'`         | `import { EventBus } from 'egg'`            | 统一从 `egg` 导入                                |\n| `import { Event } from '@eggjs/tegg'`            | `import { Event } from 'egg'`               | 装饰器也从 `egg` 导入                            |\n| 在 handler 的 `handle` 方法中直接访问请求上下文  | 使用 `@EventContext()` 注入 `IEventContext` | handler 运行在独立的上下文中，不是触发者的上下文 |\n| `@Event('hello')` 但没有声明 Events 接口         | 先在 `declare module 'egg'` 中声明事件类型  | 不声明会导致类型检查报错                         |\n| handler 的 `handle` 方法签名与 Events 声明不一致 | 确保参数类型与 Events 中定义一致            | TypeScript 会检查签名是否匹配                    |\n\n---\n\n## 使用步骤\n\n### 1. 声明事件类型\n\n在模块中创建类型声明文件，定义事件名称和参数签名：\n\n```typescript\n// app/myModule/event.ts\nimport 'egg';\n\ndeclare module 'egg' {\n  interface Events {\n    orderCreated: (orderId: string, userId: string) => void;\n    paymentCompleted: (orderId: string, amount: number) => void;\n  }\n}\n```\n\n**注意：** 文件开头必须有 `import 'egg'`，否则 `declare module` 会覆盖而非合并模块声明。\n\n### 2. 触发事件\n\n通过 `@Inject()` 注入 `EventBus` 或 `ContextEventBus`，调用 `emit()` 触发事件：\n\n```typescript\nimport { SingletonProto, Inject, EventBus, AccessLevel } from 'egg';\n\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class OrderService {\n  @Inject()\n  private eventBus: EventBus;\n\n  async createOrder(userId: string, items: Item[]): Promise<string> {\n    const orderId = await this.saveOrder(userId, items);\n    // 触发事件，不阻塞当前流程\n    this.eventBus.emit('orderCreated', orderId, userId);\n    return orderId;\n  }\n}\n```\n\n`emit()` 的参数类型由 Events 接口约束，TypeScript 会自动提示和校验。\n\n### 3. 消费事件\n\n用 `@Event()` 装饰器标记 handler 类。handler 必须实现 `handle` 方法，参数签名与 Events 声明一致：\n\n```typescript\nimport { Event, Inject } from 'egg';\nimport type { EggLogger } from 'egg';\n\n@Event('orderCreated')\nexport class OrderNotificationHandler {\n  @Inject()\n  private logger: EggLogger;\n\n  async handle(orderId: string, userId: string): Promise<void> {\n    this.logger.info('[OrderNotification] order %s created by user %s', orderId, userId);\n    // 发送通知等业务逻辑\n  }\n}\n```\n\n**handler 的关键特性：**\n\n- handler 运行在独立的上下文中，不是触发者的上下文\n- 同一事件可以有多个 handler，它们并行执行\n- handler 文件放在模块目录中，框架自动扫描和注册\n\n### 4. 消费多个事件\n\n一个 handler 可以处理多个事件，通过 `@EventContext()` 获取事件上下文来区分：\n\n```typescript\nimport { Event, EventContext, type IEventContext } from 'egg';\n\n@Event('orderCreated')\n@Event('paymentCompleted')\nexport class AuditLogHandler {\n  async handle(@EventContext() ctx: IEventContext, ...args: unknown[]): Promise<void> {\n    console.log('event:', ctx.eventName, 'args:', args);\n  }\n}\n```\n\n`@EventContext()` 只能修饰 `handle` 方法的第一个参数。不需要区分事件时可以省略。\n\n---\n\n## Cork/Uncork 事件缓冲\n\n当需要在一个操作中触发多个事件，但希望它们在操作全部完成后才被处理时，使用 cork/uncork：\n\n```typescript\nimport { ContextProto, Inject, type ContextEventBus } from 'egg';\n\n@ContextProto()\nexport class BatchService {\n  @Inject()\n  private eventBus: ContextEventBus; // 注意：cork/uncork 需要 ContextEventBus\n\n  async processBatch(items: string[]): Promise<void> {\n    this.eventBus.cork(); // 开始缓冲，事件不会立即派发\n\n    for (const item of items) {\n      this.eventBus.emit('orderCreated', item, 'batch-user');\n    }\n\n    this.eventBus.uncork(); // 释放缓冲，所有事件一次性派发\n  }\n}\n```\n\n**Cork/Uncork 行为：**\n\n- 支持嵌套调用 — 内层 uncork 不会释放事件，只有最外层 uncork 才会\n- `ContextEventBus` 的 cork/uncork 自动管理 corkId，无需手动指定\n- cork 期间 emit 的事件会被暂存，uncork 后按顺序派发\n\n---\n\n## 单元测试\n\nEventBus 的测试方法参考 `egg-unittest` skill 的 `references/eventbus-test.md`。\n","isInternal":false,"tokens":1305,"sizeBytes":4761},{"name":"inject.md","path":"packages/skills/egg-core/references/inject.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/egg-core/references/inject.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Inject 开发指南\n\n## 基本用法\n\n使用 `@Inject()` 注入其他 Proto 对象或 Egg 内置对象：\n\n```typescript\nimport { SingletonProto, Inject } from 'egg';\n\n@SingletonProto()\nexport class OrderService {\n  @Inject()\n  userService: UserService; // 按类型自动匹配\n\n  @Inject({ name: 'customName' })\n  config: any; // 按名称匹配\n\n  @Inject('shorthand')\n  other: any; // 字符串简写，等同于 { name: 'shorthand' }\n}\n```\n\n## 名称解析规则\n\n框架按以下优先级确定注入目标：\n\n1. **显式指定 name** — `@Inject({ name: 'xxx' })` 或 `@Inject('xxx')`\n2. **类型元数据自动关联** — 当属性类型是使用了 `@SingletonProto()` 或 `@ContextProto()` 装饰器的 class 时，框架自动从类型元数据匹配对应的 Proto 对象。此时属性名可以任意取值，不影响注入结果\n3. **属性名兜底** — 当类型为 `interface`、`any`、`unknown` 等无法获取运行时元信息的类型时，使用属性名作为注入名称\n\n```typescript\n@SingletonProto()\nexport class MyService {\n  @Inject()\n  fooService: FooService; // 优先级 2：从 FooService 类型自动关联\n\n  @Inject()\n  whatever: FooService; // 优先级 2：同样生效，属性名不影响匹配\n\n  @Inject({ name: 'bar' })\n  baz: FooService; // 优先级 1：显式 name → \"bar\"\n\n  @Inject()\n  something: any; // 优先级 3：类型无元数据，回退到属性名 → \"something\"\n}\n```\n\n## 可选注入\n\n默认情况下，注入目标不存在会在启动时报错。使用 `optional` 可以跳过不存在的依赖：\n\n```typescript\nimport { SingletonProto, Inject, InjectOptional } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  @Inject({ optional: true })\n  maybeService?: SomeService; // 不存在时为 undefined\n\n  @InjectOptional()\n  anotherOptional?: OtherService; // 简写方式，效果相同\n}\n```\n\n## 构造函数注入\n\n除了属性注入，也支持构造函数参数注入：\n\n```typescript\nimport { SingletonProto, Inject } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  constructor(\n    @Inject() readonly fooService: FooService,\n    @Inject({ optional: true }) readonly barService?: BarService,\n  ) {}\n}\n```\n\n**注意：构造函数注入和属性注入不能混用。** 一个 class 只能选择其中一种方式，否则框架会报错。\n\n## 注入 Egg 内置对象\n\n框架会自动遍历 `Application` 和 `Context` 对象的所有属性，均可通过 `@Inject()` 注入。\n\n#### 注入配置\n\n```typescript\nimport { Inject, SingletonProto, EggAppConfig } from 'egg';\n\n@SingletonProto()\nclass Foo {\n  @Inject()\n  config: EggAppConfig;\n\n  bar(): void {\n    console.log('current env is %s', this.config.env);\n  }\n}\n```\n\n#### 注入 Logger\n\n专为 logger 做了优化，可以直接注入 custom logger：\n\n```typescript\nimport { Inject, SingletonProto, Logger } from 'egg';\n\n@SingletonProto()\nclass FooService {\n  @Inject()\n  logger: Logger; // 注入 ${appname}-web.log\n\n  @Inject()\n  coreLogger: Logger; // 注入 egg-web.log\n\n  @Inject()\n  fooLogger: Logger; // 注入 customLogger 中配置的 fooLogger\n}\n```\n\n#### 注入 Service\n\n> 强烈建议把 Egg Service 的代码通过 Proto 重新封装再注入。对于已有的 Service，可以通过以下方式引入：\n\n```typescript\nimport { Service, Inject, SingletonProto } from 'egg';\n\n@SingletonProto()\nclass FooService {\n  @Inject()\n  service: Service; // 注入整个 ctx.service\n\n  get xxxService() {\n    return this.service.xxxService;\n  }\n}\n```\n\n#### 注入 HttpClient\n\n```typescript\nimport { Inject, SingletonProto, HttpClient } from 'egg';\n\n@SingletonProto()\nclass Foo {\n  @Inject()\n  httpClient: HttpClient;\n\n  async bar(): Promise<void> {\n    await this.httpClient.request('https://example.com');\n  }\n}\n```\n\n## 注入模块配置\n\n在模块根目录创建 `module.yml`，通过 `moduleConfig` 名称注入，框架自动注入当前模块的配置：\n\n```yaml\n# module.yml\napiEndpoint: https://api.example.com\nretryCount: 3\n```\n\n```typescript\nimport { SingletonProto, Inject } from 'egg';\n\ninterface ModuleConfig {\n  apiEndpoint: string;\n  retryCount: number;\n}\n\n@SingletonProto()\nexport class ApiService {\n  @Inject()\n  moduleConfig: ModuleConfig;\n\n  async call(): Promise<void> {\n    // this.moduleConfig.apiEndpoint → \"https://api.example.com\"\n  }\n}\n```\n\n如需注入其他模块的配置，使用 `@ConfigSourceQualifier` 指定模块名：\n\n```typescript\nimport { SingletonProto, Inject, ConfigSourceQualifier } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  @Inject()\n  @ConfigSourceQualifier('otherModule')\n  moduleConfig: OtherModuleConfig; // 注入 otherModule 的配置\n}\n```\n\n## Qualifier 限定符\n\n当同名对象存在多个实现时，使用限定符消歧义：\n\n### @InitTypeQualifier\n\n指定注入 Singleton 还是 Context 实例：\n\n```typescript\nimport { SingletonProto, Inject, InitTypeQualifier, ObjectInitType } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  @Inject()\n  @InitTypeQualifier(ObjectInitType.CONTEXT)\n  barService: BarService; // 强制注入 ContextProto 版本\n}\n```\n\n> 大多数情况下不需要手动指定，框架会根据类型元数据自动推导。\n\n### @EggQualifier\n\n当 `app` 和 `ctx` 上存在同名属性时，框架默认优先匹配 `ctx`。使用 `@EggQualifier` 显式指定来源：\n\n```typescript\nimport { SingletonProto, Inject, EggQualifier, EggType } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  @Inject()\n  @EggQualifier(EggType.APP)\n  someProp: any; // 强制从 app 注入\n\n  @Inject()\n  @EggQualifier(EggType.CONTEXT)\n  someProp2: any; // 强制从 ctx 注入\n}\n```\n\n### @ModuleQualifier\n\n指定从哪个模块注入：\n\n```typescript\nimport { SingletonProto, Inject, ModuleQualifier } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  @Inject()\n  @ModuleQualifier('userModule')\n  userService: UserService; // 明确从 userModule 注入\n}\n```\n\n## 重要约束\n\n- **不能有循环依赖**：Proto 之间或模块之间都不能形成循环引用\n- **不能有同名对象**：同一模块内不能存在相同名称和相同初始化类型的 Proto\n- **按需注入**：不要直接注入 `app` 或 `ctx`，按需注入具体的属性（如 `logger`、`config`）\n","isInternal":false,"tokens":1777,"sizeBytes":6242},{"name":"module.md","path":"packages/skills/egg-core/references/module.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/egg-core/references/module.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# module 开发指南\n\n## 创建 module\n\n在 `app` 目录中，创建 module 目录，并在该目录中添加 `package.json`。\n\n```json\n{\n  \"name\": \"moduleName\",\n  \"eggModule\": {\n    \"name\": \"moduleName\"\n  }\n}\n```\n\n**重要提示**：\n\n- 模块名称不能包含 `-` 或其他特殊字符；使用驼峰命名规则。\n- module 的 `package.json` 文件中，仅包含 `name` 以及 `eggModule.name` 字段，不应该有其他额外内容。\n\n## module 发现机制\n\n框架支持两种模式：自动扫描（默认）和手动声明。两者互斥，当 `config/module.json` 存在时，自动扫描完全禁用。\n\n### 模式一：自动扫描（默认）\n\n当 `config/module.json` **不存在**时，框架通过以下两种方式发现模块：\n\n**1. 目录扫描**\n\n从项目根目录开始扫描，查找含有 `eggModule.name` 的 `package.json`。\n\n- 默认扫描深度：10 层\n- 自动排除：隐藏目录（`.` 开头）、`node_modules/`、`coverage/`\n\n可通过应用配置调整扫描深度和排除路径：\n\n```typescript\n// config/config.default.ts\nexport default {\n  tegg: {\n    readModuleOptions: {\n      deep: 5, // 默认 10\n      extraFilePattern: ['!**/dist'], // 额外排除 dist 目录\n    },\n  },\n};\n```\n\n`extraFilePattern` 使用 [globby](https://github.com/sindresorhus/globby) 语法，以 `!` 开头表示排除。\n\n```\napp/\n├── fooModule/          ✅ 被扫描到\n│   └── package.json    { \"eggModule\": { \"name\": \"fooModule\" } }\n├── barModule/          ✅ 被扫描到\n│   └── package.json    { \"eggModule\": { \"name\": \"barModule\" } }\n├── .hidden/            ❌ 隐藏目录，自动排除\n│   └── package.json\n└── common/             ❌ 无 package.json，不会加载\n    └── utils.ts\n```\n\n**2. npm 包扫描**\n\n框架会遍历项目 `package.json` 中 `dependencies` 的每个包（不含 `devDependencies`），检查其 `package.json` 是否含有 `eggModule.name`，如果有则自动作为模块加载。\n\n### 模式二：手动声明\n\n创建 `config/module.json` 后，自动扫描**完全禁用**，仅加载文件中声明的模块：\n\n```json\n[\n  { \"path\": \"../app/module-a\" }, // 相对于 config 目录的路径\n  { \"package\": \"@eggjs/common-module\" } // npm 包名\n]\n```\n\n## module 配置\n\n在模块根目录创建 `module.yml` 存放模块专属配置：\n\n```\napp/\n└── userModule/\n    ├── package.json\n    ├── module.yml          # 基础配置\n    ├── module.unittest.yml # 环境特定配置（可选）\n    └── UserService.ts\n```\n\n### 配置文件格式\n\n支持 YAML 和 JSON 两种格式，优先加载 YAML：\n\n```yaml\n# module.yml\nfeatures:\n  dynamic:\n    foo: bar\n```\n\n### 环境配置合并\n\n框架会按 `module.yml` → `module.{env}.yml` 的顺序深度合并：\n\n```yaml\n# module.yml\nfeatures:\n  dynamic:\n    foo: bar\n\n# module.unittest.yml\nfeatures:\n  dynamic:\n    testMode: true\n```\n\nunittest 环境下合并结果：\n\n```json\n{\n  \"features\": {\n    \"dynamic\": {\n      \"foo\": \"bar\",\n      \"testMode\": true\n    }\n  }\n}\n```\n\n### 注入配置\n\n通过 `@Inject()` 注入 `moduleConfig`，框架自动注入当前模块的配置：\n\n```typescript\nimport { SingletonProto, Inject } from 'egg';\n\ninterface ModuleConfig {\n  features: {\n    dynamic: {\n      foo: string;\n    };\n  };\n}\n\n@SingletonProto()\nexport class UserService {\n  @Inject()\n  moduleConfig: ModuleConfig;\n\n  async getFeatureFlag(): Promise<string> {\n    return this.moduleConfig.features.dynamic.foo; // 'bar'\n  }\n}\n```\n\n## module 目录组织\n\n### 新应用\n\n直接在 `app/` 目录下平铺 module。可按功能划分，也可按业务划分：\n\n**按功能划分：**\n\n```\napp/\n├── authModule/\n│   └── package.json\n├── logModule/\n│   └── package.json\n└── storageModule/\n    └── package.json\n```\n\n**按业务划分：**\n\n```\napp/\n├── userModule/\n│   └── package.json\n├── orderModule/\n│   └── package.json\n└── paymentModule/\n    └── package.json\n```\n\n### 存量应用（包含老的 egg 写法）\n\n保留原有 `app/controller`、`app/service` 等目录不动，将新增的 module 统一放在 `app/module/` 下：\n\n```\napp/\n├── controller/           # 老的 egg 代码，保持不变\n├── service/\n├── module/               # 新增 module 统一放这里\n│   ├── userModule/\n│   │   └── package.json\n│   └── orderModule/\n│       └── package.json\n└── router.ts\n```\n","isInternal":false,"tokens":1396,"sizeBytes":4517},{"name":"proto.md","path":"packages/skills/egg-core/references/proto.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/egg-core/references/proto.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Proto 开发指南\n\n## SingletonProto vs ContextProto\n\n|          | SingletonProto     | ContextProto       |\n| -------- | ------------------ | ------------------ |\n| 创建时机 | 应用启动时立即创建 | 请求到达时按需创建 |\n| 实例数量 | 整个应用 1 个      | 每个请求 1 个      |\n| 销毁时机 | 应用关闭           | 请求结束           |\n\n**决策逻辑：默认使用 `@SingletonProto()`**，性能更好。只有当需要在多个服务间共享请求级隔离状态时，才使用 `@ContextProto()`。\n\n```typescript\nimport { SingletonProto, ContextProto } from 'egg';\n\n// ✅ 默认选择：无状态服务用 SingletonProto\n@SingletonProto()\nexport class UserService {\n  async findUser(id: string): Promise<User> {\n    // 无需请求级状态，适合 Singleton\n  }\n}\n\n// 仅在需要请求级隔离时使用 ContextProto\n@ContextProto()\nexport class RequestContext {\n  traceId: string;\n  userId: string;\n  // 每个请求独立的状态\n}\n```\n\n## 装饰器参数\n\n`@SingletonProto()` 和 `@ContextProto()` 接受相同的可选参数：\n\n| 参数          | 类型        | 默认值         | 说明                                 |\n| ------------- | ----------- | -------------- | ------------------------------------ |\n| name          | string      | 类名首字母小写 | 实例名称                             |\n| accessLevel   | AccessLevel | PRIVATE        | 跨模块可见性                         |\n| protoImplType | string      | 'DEFAULT'      | 实现类型标记（高级用法，一般不需要） |\n\n名称推导规则：类名首字母自动转小写，如 `MyService` → `myService`。\n\n```typescript\nimport { SingletonProto, AccessLevel } from 'egg';\n\n// 使用默认参数：name 为 \"helloService\"，accessLevel 为 PRIVATE\n@SingletonProto()\nexport class HelloService {}\n\n// 自定义参数\n@SingletonProto({\n  name: 'customName',\n  accessLevel: AccessLevel.PUBLIC,\n})\nexport class HelloService {}\n```\n\n## 注入规则\n\n**SingletonProto 可以注入 ContextProto**，框架会自动处理生命周期差异。\n\n实现机制：框架不会直接注入 ContextProto 实例，而是通过 lazy getter/Proxy，在每次访问时从当前请求上下文动态解析，确保每个请求拿到各自的实例。\n\n```typescript\nimport { SingletonProto, ContextProto, Inject } from 'egg';\n\n@ContextProto()\nexport class RequestContext {\n  userId: string;\n}\n\n@SingletonProto()\nexport class AppService {\n  @Inject()\n  requestContext: RequestContext; // ✅ 每次访问自动解析当前请求的实例\n}\n```\n\nContextProto 也可以注入 SingletonProto：\n\n```typescript\n@ContextProto()\nexport class RequestHandler {\n  @Inject()\n  appService: AppService; // ✅ 直接注入\n}\n```\n\n## AccessLevel 跨模块访问\n\n- `AccessLevel.PRIVATE`（默认）：仅同模块内可注入\n- `AccessLevel.PUBLIC`：跨模块可注入\n\n模块边界由 `package.json` 中的 `eggModule.name` 决定。\n\n```typescript\n// userModule 中\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class UserService {}\n\n// orderModule 中\n@SingletonProto()\nexport class OrderService {\n  @Inject()\n  userService: UserService; // ✅ UserService 是 PUBLIC，跨模块可注入\n}\n```\n\n```typescript\n// userModule 中\n@SingletonProto() // 默认 PRIVATE\nexport class UserHelper {}\n\n// orderModule 中\n@SingletonProto()\nexport class OrderService {\n  @Inject()\n  userHelper: UserHelper; // ❌ 报错：UserHelper 是 PRIVATE，不能跨模块注入\n}\n```\n\n## 生命周期\n\nProto 对象支持以下生命周期钩子，按执行顺序排列：\n\n| 阶段 | 装饰器                      | 说明                             |\n| ---- | --------------------------- | -------------------------------- |\n| 1    | constructor                 | 构造函数                         |\n| 2    | `@LifecyclePostConstruct()` | 构造完成后                       |\n| 3    | `@LifecyclePreInject()`     | 注入前                           |\n| 4    | —                           | 依赖注入                         |\n| 5    | `@LifecyclePostInject()`    | 注入完成后                       |\n| 6    | `@LifecycleInit()`          | 异步初始化（对象就绪前最后一步） |\n| 7    | —                           | **对象就绪，可被使用**           |\n| 8    | `@LifecyclePreDestroy()`    | 销毁前                           |\n| 9    | `@LifecycleDestroy()`       | 销毁，用于清理资源               |\n\n常用的是 `@LifecycleInit()` 和 `@LifecycleDestroy()`。\n\n**示例 1：SingletonProto 异步初始化和资源清理**\n\n```typescript\nimport { SingletonProto, LifecycleInit, LifecycleDestroy } from 'egg';\n\n@SingletonProto()\nexport class DatabaseService {\n  private connection: Connection;\n\n  @LifecycleInit()\n  async init(): Promise<void> {\n    this.connection = await createConnection();\n  }\n\n  @LifecycleDestroy()\n  async destroy(): Promise<void> {\n    await this.connection.close();\n  }\n}\n```\n\n**示例 2：ContextProto 在注入完成后初始化状态**\n\n```typescript\nimport { ContextProto, Inject, LifecyclePostInject } from 'egg';\n\n@ContextProto()\nexport class RequestTracer {\n  @Inject()\n  traceService: TraceService;\n\n  traceId: string;\n\n  @LifecyclePostInject()\n  async postInject(): Promise<void> {\n    // 依赖注入完成后，利用注入的服务初始化状态\n    this.traceId = await this.traceService.generateTraceId();\n  }\n}\n```\n","isInternal":false,"tokens":1386,"sizeBytes":5429},{"name":"SKILL.md","path":"packages/skills/egg-core/SKILL.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/egg-core/SKILL.md","title":"egg-core","category":"anthropic-skill","format":"markdown","content":"---\nname: egg-core\ndescription: 本技能用于处理 EGG 基础核心概念，包括模块架构、@SingletonProto、@ContextProto、@Inject 装饰器、动态注入、BackgroundTaskHelper 后台任务、EventBus 事件总线和 AOP 切面编程。用于理解 EGG 的基础构建块、依赖注入、对象生命周期管理、运行时多实现动态选择、请求返回后的异步任务处理、事件驱动架构和横切关注点。\nallowed-tools: Read\n---\n\n# egg 核心概念\n\n## 代码组织与依赖注入\n\n### Step 1: 代码写在 module 中\n\n#### 什么是模块？\n\n模块是 EGG 中基础的代码组织单元。只有模块内的代码会被框架扫描和加载。模块之间相互独立，但可以通过 `@Inject` 装饰器访问其他模块的对象。\n\n#### 定义 module\n\n在目录中添加包含 `eggModule.name` 字段的 `package.json` 文件来声明该目录为模块：\n\n```json\n{\n  \"name\": \"foo\",\n  \"eggModule\": {\n    \"name\": \"foo\"\n  }\n}\n```\n\n**重要提示**：模块名称不能包含 `-` 或其他特殊字符；使用驼峰命名规则。\n\n#### 正确示例\n\n```\napp/\n└── userModule/           ✅ 驼峰命名\n    ├── package.json\n    │   └── { \"eggModule\": { \"name\": \"userModule\" } }\n    └── service.ts        ✅ 会被框架加载\n```\n\n#### 错误示例\n\n```\napp/\n├── user-module/          ❌ 名称包含 `-`\n│   └── package.json\n│       └── { \"eggModule\": { \"name\": \"user-module\" } }\n│\n└── common/               ❌ 缺少 package.json（不是 module）\n    └── utils.ts          ❌ 不会被框架加载\n```\n\n#### 模块配置\n\n在模块根目录创建 `module.yml` 用于模块特定配置：\n\n```yaml\nfoo: bar\n```\n\n通过 `@Inject()` 注入配置，使用 `moduleConfig`：\n\n```typescript\nimport { SingletonProto, Inject } from 'egg';\n\ninterface ModuleConfig {\n  foo: string;\n}\n\n@SingletonProto()\nexport class ConfigService {\n  @Inject()\n  private readonly moduleConfig: ModuleConfig;\n\n  async hello(): Promise<string> {\n    return `hello ${this.moduleConfig.foo}`;\n  }\n}\n```\n\n#### 模块组织最佳实践\n\n- 新应用：按功能在 `app/` 目录中组织\n- 存量应用：保留老的 egg 代码在 `app/controller`/`app/service`，将新增的 module 代码放在 `app/module/`\n- 可以在 `dependencies` 中导入 npm 包作为额外模块\n\n### 导入路径\n\n所有装饰器和类型统一从 `egg` 导入，不要从 `@eggjs/tegg` 导入：\n\n```typescript\n// ✅ 正确\nimport { SingletonProto, ContextProto, Inject, AccessLevel } from 'egg';\n\n// ❌ 错误 — 不要从 @eggjs/tegg 导入\nimport { SingletonProto } from '@eggjs/tegg';\n```\n\n---\n\n### Step 2: 用 Proto 实现 Service\n\n#### SingletonProto\n\n应用启动时立即创建，整个应用生命周期内只有一个实例，性能更好，应该作为默认选择。\n\n```typescript\nimport { SingletonProto } from 'egg';\n\n@SingletonProto()\nexport class HelloService {\n  async hello(): Promise<string> {\n    return 'hello';\n  }\n}\n```\n\n#### ContextProto\n\n请求到达时按需创建，每个请求一个实例，请求结束自动销毁。仅在需要隔离不同请求的上下文信息时使用。\n\n```typescript\nimport { ContextProto } from 'egg';\n\n@ContextProto()\nexport class RequestContext {\n  userId: string;\n}\n```\n\n**重要提示**：大多数服务应该使用 `SingletonProto` 以获得更好的性能。只有当请求上下文必须在服务之间共享以确保请求之间隔离时，才使用 `ContextProto`。\n\n#### AccessLevel\n\nproto 对象默认 accessLevel 为 `AccessLevel.PRIVATE`，仅在当前 module 内使用。可以设置为 `AccessLevel.PUBLIC`，进行跨模块访问。\n\n```typescript\nimport { AccessLevel, ContextProto, SingletonProto } from 'egg';\n\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class SharedService {}\n\n@ContextProto({ accessLevel: AccessLevel.PUBLIC })\nexport class SharedContextService {}\n```\n\n### Step 3: 通过 Inject 使用 Service\n\n#### 基本用法\n\n使用 `@Inject()` 注入其他 Proto 或 Egg 对象：\n\n```typescript\nimport { Inject, Logger, SingletonProto } from 'egg';\nimport { FooService } from './FooService.ts';\n\n@SingletonProto()\nexport class HelloService {\n  @Inject()\n  fooService: FooService; // 注入另一个 Proto\n\n  @Inject()\n  logger: Logger; // 注入 Egg 对象\n\n  async hello(): Promise<string> {\n    this.logger.info(`[HelloService] ${this.fooService.hello()}`);\n  }\n}\n```\n\n#### 动态注入\n\n当同一个抽象有多种实现，需要在运行时动态选择时，通过 `EggObjectFactory` 按类型获取实现，无需 if/else。详见 `references/dynamic-inject.md`。\n\n#### 重要约束\n\n- **不能有循环依赖**：Proto 或模块之间都不能有循环依赖\n- **不能有同名对象**：一个模块不能有相同名称和初始化类型的 Proto\n- **按需注入**：不要直接注入 `app` 或 `ctx`，按需注入特定对象\n\n### 快速决策指南\n\n| 场景                             | 使用方式                                               |\n| -------------------------------- | ------------------------------------------------------ |\n| 无状态服务                       | `@SingletonProto()`                                    |\n| 跨服务共享的请求级状态           | `@ContextProto()`                                      |\n| 需要跨模块访问                   | `@SingletonProto({ accessLevel: AccessLevel.PUBLIC })` |\n| 注入依赖                         | `@Inject()`                                            |\n| 使用自定义名称注入               | `@Inject({ name: 'customName' })`                      |\n| 同一抽象多种实现，运行时动态选择 | `QualifierImplDecoratorUtil` + `EggObjectFactory`      |\n\n## 异步任务\n\n| 特性       | BackgroundTaskHelper          | EventBus                                  |\n| ---------- | ----------------------------- | ----------------------------------------- |\n| **耦合**   | 高 — 异步逻辑写在触发者代码中 | 低 — handler 独立，新增处理者不修改触发者 |\n| **上下文** | 共享触发者的请求上下文        | handler 运行在独立的新上下文中            |\n| **扩展**   | 需要修改触发者代码            | 新增 `@Event` handler 类即可              |\n\n```\n需要在请求之外执行任务？\n│\n├─ 请求返回后执行，依赖当前请求上下文\n│  └─ → BackgroundTaskHelper（references/background-task.md）\n│\n├─ 请求返回后执行，不依赖当前请求上下文，需要解耦\n│  └─ → EventBus（references/eventbus.md）\n│\n└─ 定时或周期执行\n   └─ → Schedule（参考 egg-controller skill）\n```\n\n## AOP 切面编程\n\nAOP 用于将日志、鉴权、缓存、事务等横切关注点从业务代码中分离。AOP 装饰器从 `egg/aop` 导入（不是 `egg`）。\n\n```\n需要在方法执行前后添加通用逻辑？\n│\n├─ 针对特定方法 → @Pointcut（在目标方法上声明）\n│\n└─ 批量切入多个类/方法 → @Crosscut（在 Advice 类上声明匹配规则）\n```\n\n详细用法（Advice 生命周期、AdviceContext、Pointcut/Crosscut 选型、参数透传、执行顺序）请参阅 `references/aop.md`。\n\n## 常见问题排查\n\n| 现象                   | 原因                                       | 解决方案                                     |\n| ---------------------- | ------------------------------------------ | -------------------------------------------- |\n| 模块没被加载           | `eggModule.name` 包含 `-` 等特殊字符       | 改为驼峰命名                                 |\n| `EggPrototypeNotFound` | 跨模块注入但 accessLevel 为 PRIVATE        | 改为 `AccessLevel.PUBLIC`                    |\n| 注入对象不对           | 类型为 `interface`/`any`，回退到属性名匹配 | 改用 class 类型或 `@Inject({ name: 'xxx' })` |\n| 混用注入方式报错       | 属性注入和构造函数注入不能混用             | 统一使用一种方式                             |\n| 可选依赖启动报错       | 缺少 optional 标记                         | `@Inject({ optional: true })`                |\n\n## 参考资料\n\n- 详细的 module 文档，请参阅：`references/module.md`\n- Inject 装饰器使用，请参阅：`references/inject.md`\n- SingletonProto 和 ContextProto 详情，请参阅：`references/proto.md`\n- 动态注入（Qualifier 动态注入），请参阅：`references/dynamic-inject.md`\n- 请求后异步任务（BackgroundTaskHelper），请参阅：`references/background-task.md`\n- 事件总线（EventBus），请参阅：`references/eventbus.md`\n- AOP 切面编程（Advice、Pointcut、Crosscut），请参阅：`references/aop.md`\n\n单元测试（`egg-unittest` skill）：Service 测试、BackgroundTask 测试、EventBus 测试\n","frontmatter":{"name":"egg-core","description":"本技能用于处理 EGG 基础核心概念，包括模块架构、@SingletonProto、@ContextProto、@Inject 装饰器、动态注入、BackgroundTaskHelper 后台任务、EventBus 事件总线和 AOP 切面编程。用于理解 EGG 的基础构建块、依赖注入、对象生命周期管理、运行时多实现动态选择、请求返回后的异步任务处理、事件驱动架构和横切关注点。","allowed-tools":"Read","allowedTools":["Read"]},"allowedTools":["Read"],"isInternal":false,"tokens":2512,"sizeBytes":8851},{"name":"background-task-test.md","path":"packages/skills/egg-unittest/references/background-task-test.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/egg-unittest/references/background-task-test.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# BackgroundTaskHelper 测试\n\n## 常见错误\n\n| 错误写法                                           | 正确写法                                                                              | 说明                                                 |\n| -------------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------- |\n| 不等待就断言                                       | 用 `mockModuleContextScope`（自动等待）或 `app.backgroundTasksFinished()`（手动等待） | 后台任务异步执行，必须等待完成后再断言               |\n| 在 `mockModuleContextScope` 回调内断言后台任务结果 | 在 `mockModuleContextScope` 返回后断言                                                | 回调内任务尚未完成，返回后才会等待完成               |\n| 用 `TimerUtil.sleep` 等待                          | 用 `app.backgroundTasksFinished()`                                                    | sleep 时间不确定，`backgroundTasksFinished` 精确等待 |\n\n---\n\n## 使用 mockModuleContextScope\n\n`mockModuleContextScope` 退出时会自动等待所有后台任务完成（内部触发 `doPreDestroy`），scope 退出后直接断言即可：\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\nimport { CountService } from '../app/modules/count/CountService.ts';\n\nit('should complete background task', async () => {\n  await app.mockModuleContextScope(async (ctx) => {\n    const countService = await ctx.getEggObject(CountService);\n    // countService 内部通过 backgroundTaskHelper.run() 触发后台任务\n    await countService.doSomething();\n  });\n\n  // scope 退出后，后台任务已完成，直接断言\n  const countService = await app.getEggObject(CountService);\n  assert.equal(countService.count, 1);\n});\n```\n\n## 使用 backgroundTasksFinished\n\n不通过 `mockModuleContextScope` 触发的场景（如 HTTP 接口测试），scope 退出的自动等待机制不适用，需要手动调用 `app.backgroundTasksFinished()` 等待所有后台任务完成后，再做断言：\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\nimport { CountService } from '../app/modules/count/CountService.ts';\n\nit('should complete background task', async () => {\n  await app.httpRequest().get('/api/trigger-task').expect(200);\n\n  // 等待后台任务完成\n  await app.backgroundTasksFinished();\n\n  const countService = await app.getEggObject(CountService);\n  assert.equal(countService.count, 1);\n});\n```\n","isInternal":false,"tokens":576,"sizeBytes":2670},{"name":"eventbus-test.md","path":"packages/skills/egg-unittest/references/eventbus-test.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/egg-unittest/references/eventbus-test.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# EventBus 测试\n\n## 常见错误\n\n| 错误写法                         | 正确写法                                           | 说明                                         |\n| -------------------------------- | -------------------------------------------------- | -------------------------------------------- |\n| 先 emit 再 `eventWaiter.await()` | 先 `eventWaiter.await()` 再触发业务逻辑            | await 注册监听器，必须在事件发出前           |\n| 不等待事件处理完成就断言         | 使用 `eventWaiter.await('eventName')` 等待后再断言 | handler 异步执行，不等待则断言时可能尚未完成 |\n\n---\n\n## 基本测试模式\n\n使用 `app.getEventWaiter()` 等待事件被处理完成：\n\n```typescript\nimport assert from 'node:assert';\nimport { app, mm } from '@eggjs/mock/bootstrap';\nimport { HelloService } from '../app/modules/hello/HelloService.ts';\nimport { HelloHandler } from '../app/modules/hello/HelloHandler.ts';\n\ndescribe('EventBus', () => {\n  it('should handle event', async () => {\n    // mock handler 捕获调用参数\n    const mockFn = async (msg: string) => {};\n    mm(HelloHandler.prototype, 'handle', mockFn);\n\n    await app.mockModuleContextScope(async (ctx) => {\n      const helloService = await ctx.getEggObject(HelloService);\n      const eventWaiter = await app.getEventWaiter();\n\n      // 1. 先注册等待（必须在 emit 之前）\n      const eventPromise = eventWaiter.await('helloEgg');\n\n      // 2. 触发业务逻辑（内部会 emit 事件）\n      helloService.hello();\n\n      // 3. 等待 handler 执行完成\n      await eventPromise;\n    });\n\n    // 4. 验证 handler 被调用及参数\n    assert.equal(mockFn.called, 1);\n    assert.deepStrictEqual(mockFn.lastCalledArguments, ['hello']);\n  });\n});\n```\n","isInternal":false,"tokens":449,"sizeBytes":1801},{"name":"http-test.md","path":"packages/skills/egg-unittest/references/http-test.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/egg-unittest/references/http-test.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# HTTP 接口测试\n\n## 常见错误\n\n| 错误写法                                                 | 正确写法                        | 说明                                          |\n| -------------------------------------------------------- | ------------------------------- | --------------------------------------------- |\n| POST 测试不加 `app.mockCsrf()`                           | 在 POST 前调用 `app.mockCsrf()` | 安全插件默认开启 CSRF，不 mock 会返回 403     |\n| `app.httpRequest().get('/').expect(200)` 不 return/await | 必须 `return` 或 `await`        | 否则断言不会执行，测试永远通过                |\n| `.expect({ foo: 'bar' })` 用于部分匹配                   | 使用 `result.body` 手动断言     | `.expect(body)` 是全量匹配（deepStrictEqual） |\n\n---\n\n## 基本用法\n\n通过 `app.httpRequest()` 发起 HTTP 请求，返回 SuperTest 对象：\n\n```typescript\nimport { app } from '@eggjs/mock/bootstrap';\n\ndescribe('UserController', () => {\n  it('should GET /api/users', () => {\n    return app.httpRequest().get('/api/users').expect(200).expect({ users: [] });\n  });\n});\n```\n\n---\n\n## POST 请求 + CSRF\n\nPOST/PUT/DELETE 请求需要先调用 `app.mockCsrf()` 跳过 CSRF 校验：\n\n```typescript\nit('should POST /api/users', () => {\n  app.mockCsrf();\n  return app\n    .httpRequest()\n    .post('/api/users')\n    .send({ name: 'test', email: 'test@example.com' })\n    .expect(200)\n    .expect({ id: '1', name: 'test' });\n});\n```\n\n表单提交使用 `.type('form')`：\n\n```typescript\nit('should POST form data', () => {\n  app.mockCsrf();\n  return app.httpRequest().post('/api/login').type('form').send({ username: 'admin', password: '123' }).expect(200);\n});\n```\n\n---\n\n## 请求构造\n\n```typescript\napp\n  .httpRequest()\n  .get('/api/users')\n  .set('Authorization', 'Bearer token123') // 设置 header\n  .set('Accept', 'application/json') // 设置 Accept\n  .query({ page: 1, limit: 10 }) // 查询参数\n  .expect(200);\n```\n\n---\n\n## 响应断言\n\n使用 `.expect()` 链式断言：\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\n\nit('should validate response', () => {\n  return app\n    .httpRequest()\n    .get('/api/users/1')\n    .expect(200) // 只校验状态码\n    .expect({ id: '1', name: 'test' }) // 只校验 body（deepStrictEqual）\n    .expect(200, { id: '1', name: 'test' }) // 状态码 + body 合并\n    .expect('hello world') // body 字符串匹配\n    .expect(/hello/) // body 正则匹配\n    .expect('content-type', /json/) // header 匹配\n    .expect([200, 302]) // 多状态码匹配（任一即可）\n    .expect((res) => {\n      // 自定义断言函数\n      assert(res.body.id);\n    });\n});\n```\n\n使用 `result` 做更灵活的断言：\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\n\nit('should validate response', async () => {\n  const result = await app.httpRequest().get('/api/users/1');\n\n  assert.equal(result.status, 200);\n  assert.equal(result.body.name, 'test');\n  assert(result.body.id);\n  assert.match(result.headers['content-type'], /json/);\n});\n```\n\n完整的请求构造和断言 API 可查看项目 node_modules 中 `@eggjs/supertest` 的类型定义。\n\n---\n\n## 端到端示例\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\n\ndescribe('test/controller/user.test.ts', () => {\n  describe('GET /api/users/:id', () => {\n    it('should return user', () => {\n      return app.httpRequest().get('/api/users/1').expect(200).expect({ id: '1', name: 'test' });\n    });\n\n    it('should return 404 when user not found', () => {\n      return app.httpRequest().get('/api/users/999').expect(404);\n    });\n  });\n\n  describe('POST /api/users', () => {\n    it('should create user', () => {\n      app.mockCsrf();\n      return app.httpRequest().post('/api/users').send({ name: 'new user', email: 'new@example.com' }).expect(201);\n    });\n\n    it('should return 422 with invalid params', () => {\n      app.mockCsrf();\n      return app.httpRequest().post('/api/users').send({ name: '' }).expect(422);\n    });\n  });\n});\n```\n","isInternal":false,"tokens":1134,"sizeBytes":4156},{"name":"mock.md","path":"packages/skills/egg-unittest/references/mock.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/egg-unittest/references/mock.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Mock 模式\n\n## 常见错误\n\n| 错误写法                       | 正确写法                                   | 说明                           |\n| ------------------------------ | ------------------------------------------ | ------------------------------ |\n| `mm(service, 'method', fn)`    | `mm(ServiceClass.prototype, 'method', fn)` | DI 对象需 mock 原型，不是实例  |\n| 手动写 `afterEach(mm.restore)` | 不需要                                     | egg-bin 自动注入 mock 恢复     |\n| `new Ajv()` mock 单独实例      | mock 原型方法                              | DI 容器管理的对象通过原型 mock |\n\n---\n\n## mm() — Mock Proto 方法\n\n最常用的 mock 方式，mock DI 对象的原型方法：\n\n```typescript\nimport assert from 'node:assert';\nimport { app, mm } from '@eggjs/mock/bootstrap';\nimport { UserService } from '../app/modules/user/UserService.ts';\nimport { OrderService } from '../app/modules/order/OrderService.ts';\n\ndescribe('OrderService', () => {\n  it('should mock user service', async () => {\n    mm(UserService.prototype, 'getById', async () => {\n      return { id: '1', name: 'mocked user' };\n    });\n\n    const orderService = await app.getEggObject(OrderService);\n    const result = await orderService.createForUser('1');\n    assert.equal(result.userName, 'mocked user');\n  });\n});\n```\n\nmock 函数会自动记录调用信息，可以用来断言调用参数：\n\n```typescript\nimport assert from 'node:assert';\nimport { app, mm } from '@eggjs/mock/bootstrap';\nimport { NotifyService } from '../app/modules/notify/NotifyService.ts';\nimport { OrderService } from '../app/modules/order/OrderService.ts';\n\nit('should call notify with correct args', async () => {\n  const mockFn = async (userId: string, message: string) => {};\n  mm(NotifyService.prototype, 'send', mockFn);\n\n  const orderService = await app.getEggObject(OrderService);\n  await orderService.create({ productId: '1' });\n\n  assert.equal(mockFn.called, 1); // 调用次数\n  assert.deepStrictEqual(mockFn.lastCalledArguments, ['user-1', '订单创建成功']); // 最后一次调用参数\n  // mockFn.calledArguments — 所有调用参数的数组\n});\n```\n\n---\n\n## mm.spy() — 不替换实现，只记录调用\n\n```typescript\nit('should spy on method', async () => {\n  mm.spy(NotifyService.prototype, 'send');\n\n  const orderService = await app.getEggObject(OrderService);\n  await orderService.create({ productId: '1' });\n\n  // 原方法正常执行，同时记录了调用信息\n  const sendFn = NotifyService.prototype.send;\n  assert.equal(sendFn.called, 1);\n  assert.equal(sendFn.lastCalledArguments[0], 'user-1');\n});\n```\n\n---\n\n## app.mockHttpclient() — Mock HttpClient 请求\n\nMock 通过 `@Inject() httpclient: HttpClient` 注入的 HttpClient 发送的请求：\n\n```typescript\nit('should mock external API', () => {\n  app.mockHttpclient('https://api.example.com/users', {\n    data: JSON.stringify({ name: 'test' }),\n  });\n\n  return app.httpRequest().get('/api/proxy/users').expect(200).expect({ name: 'test' });\n});\n```\n\n---\n\n## app.mockCsrf() — 跳过 CSRF\n\nPOST/PUT/DELETE 测试时跳过 CSRF 校验：\n\n```typescript\nit('should POST without CSRF error', () => {\n  app.mockCsrf();\n  return app.httpRequest().post('/api/users').send({ name: 'test' }).expect(200);\n});\n```\n\n---\n\n## Mock 恢复\n\negg-bin 自动注入 `@eggjs/mock/setup_vitest`，会在 `afterEach` 钩子中自动调用 `mm.restore()`，无需手动编写。\n","isInternal":false,"tokens":884,"sizeBytes":3466},{"name":"service-test.md","path":"packages/skills/egg-unittest/references/service-test.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/egg-unittest/references/service-test.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Service / DI 对象测试\n\n## 常见错误\n\n| 错误写法                 | 正确写法                                  | 说明                |\n| ------------------------ | ----------------------------------------- | ------------------- |\n| `ctx.service.user.get()` | `ctx.getEggObject(UserService)`           | 旧写法，新项目用 DI |\n| 不 await `getEggObject`  | `const svc = await ctx.getEggObject(Svc)` | 返回 Promise        |\n\n---\n\n## Singleton 测试\n\n`@SingletonProto` 对象直接通过 `app.getEggObject()` 获取：\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\nimport { ConfigService } from '../app/modules/foo/ConfigService.ts';\n\ndescribe('ConfigService', () => {\n  it('should get config', async () => {\n    const configService = await app.getEggObject(ConfigService);\n    const value = configService.get('key');\n    assert.equal(value, 'expected');\n  });\n});\n```\n\n---\n\n## ContextProto 测试\n\n`@ContextProto` 对象可以直接通过 `app.getEggObject()` 获取，也可以在 `app.mockModuleContextScope` 中通过 `ctx.getEggObject()` 获取。后者会创建带 DI 生命周期的 ctx，退出时自动销毁：\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\nimport { UserService } from '../app/modules/user/UserService.ts';\n\ndescribe('UserService', () => {\n  it('should get user in context scope', async () => {\n    await app.mockModuleContextScope(async (ctx) => {\n      const userService = await ctx.getEggObject(UserService);\n      const user = await userService.getById('1');\n      assert(user);\n    });\n  });\n});\n```\n\n---\n\n## Mock 被注入的依赖\n\n当 Service A 依赖 Service B 时，mock B 的原型方法：\n\n```typescript\nimport assert from 'node:assert';\nimport { app, mm } from '@eggjs/mock/bootstrap';\nimport { OrderService } from '../app/modules/order/OrderService.ts';\nimport { PaymentService } from '../app/modules/payment/PaymentService.ts';\n\ndescribe('OrderService', () => {\n  it('should create order with mocked payment', async () => {\n    mm(PaymentService.prototype, 'charge', async () => {\n      return { transactionId: 'mock-tx-001' };\n    });\n\n    const orderService = await app.getEggObject(OrderService);\n    const order = await orderService.create({ productId: '1', amount: 100 });\n    assert.equal(order.transactionId, 'mock-tx-001');\n  });\n});\n```\n","isInternal":false,"tokens":583,"sizeBytes":2407},{"name":"SKILL.md","path":"packages/skills/egg-unittest/SKILL.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/egg-unittest/SKILL.md","title":"egg-unittest","category":"anthropic-skill","format":"markdown","content":"---\nname: egg-unittest\ndescription: 本技能用于编写 EGG 应用的单元测试。覆盖 HTTP 接口测试、Service/DI 对象测试、Mock 数据模拟、BackgroundTask 和 EventBus 测试。使用 @eggjs/mock、app.httpRequest()、app.getEggObject()、mm() 等 API。\nallowed-tools: Read\n---\n\n# EGG 单元测试\n\n---\n\n## 原理\n\n`egg-bin test` 使用 Vitest 运行测试，自动完成以下工作：\n\n- 以当前项目目录为 baseDir，创建并启动一个 MockApplication 实例（即 `app`）\n- 注入 `@eggjs/mock/setup_vitest` 管理 app 生命周期（`beforeAll` 启动 app、`afterEach` 恢复 mock、`afterAll` 关闭 app）\n- 注入 Vitest 全局变量（`describe`、`it`、`beforeAll` 等），无需手动 import\n  测试代码中通过 `import { app, mm } from '@eggjs/mock/bootstrap'` 获取已启动的 app 实例和 mock 工具，直接使用即可。\n\n---\n\n## 配置检查\n\n确保项目中有以下配置：\n\n**package.json：**\n\n```json\n{\n  \"scripts\": {\n    \"test\": \"egg-bin test\"\n  },\n  \"devDependencies\": {\n    \"@eggjs/bin\": \"^8\",\n    \"@eggjs/mock\": \"^8\"\n  }\n}\n```\n\n**测试文件约定：**\n\n- 测试目录：`test/`\n- 文件命名：`*.test.ts`\n\n**自定义 setup 文件（可选）：**\n\n如果存在 `test/.setup.ts`，egg-bin 会自动将其加入 vitest setupFiles，在 `@eggjs/mock/setup_vitest` 之前执行（即 app 启动之前）。可用于设置环境变量等全局初始化：\n\n```typescript\n// test/.setup.ts\nbeforeAll(() => {\n  process.env.SOME_CONFIG = 'test-value';\n});\n```\n\n---\n\n## 简单示例\n\n### HTTP 接口测试\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\n\ndescribe('test/controller/home.test.ts', () => {\n  it('should GET /', () => {\n    return app.httpRequest().get('/').expect(200).expect('hello world');\n  });\n});\n```\n\n### Service 测试\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\nimport { UserService } from '../app/modules/user/UserService.ts';\n\ndescribe('test/service/user.test.ts', () => {\n  it('should get user', async () => {\n    const userService = await app.getEggObject(UserService);\n    const user = await userService.getById('1');\n    assert(user);\n    assert.equal(user.name, 'test');\n  });\n});\n```\n\n---\n\n## 测试场景决策树\n\n```\n要测什么？\n\n1. HTTP 接口（GET/POST/PUT/DELETE）？\n   → 参考 references/http-test.md\n\n2. Service / DI 对象的方法？\n   → 参考 references/service-test.md\n\n3. 需要 mock 外部依赖？（HTTP 调用、Service 方法、Session、CSRF）\n   → 参考 references/mock.md\n\n4. BackgroundTaskHelper（后台异步任务）？\n   → 参考 references/background-task-test.md\n\n5. EventBus（事件驱动）？\n   → 参考 references/eventbus-test.md\n```\n\n---\n\n## 快速参考\n\n| API                                                  | 说明                                    |\n| ---------------------------------------------------- | --------------------------------------- |\n| `import { app, mm } from '@eggjs/mock/bootstrap'`    | 标准测试入口                            |\n| `app.httpRequest().get('/path').expect(200)`         | HTTP 接口测试                           |\n| `app.getEggObject(Class)`                            | 获取 SingletonProto / ContextProto 实例 |\n| `app.mockModuleContextScope(async (ctx) => { ... })` | ContextProto 测试作用域                 |\n| `mm(Class.prototype, 'method', fn)`                  | Mock Proto 方法                         |\n| `app.mockCsrf()`                                     | 跳过 CSRF 校验（POST 测试必备）         |\n| `app.mockHttpclient(url, data)`                      | Mock 外部 HTTP 调用                     |\n| `app.getEventWaiter()`                               | 获取 EventBus 事件等待器                |\n\n---\n\n## 常见错误\n\n| 错误写法                           | 正确写法                                      | 说明                        |\n| ---------------------------------- | --------------------------------------------- | --------------------------- |\n| `import { app } from 'egg'`        | `import { app } from '@eggjs/mock/bootstrap'` | 测试使用 mock 包            |\n| `before()` / `after()`             | `beforeAll()` / `afterAll()`                  | Vitest 钩子，不是 Mocha     |\n| POST 测试报 403                    | 加 `app.mockCsrf()`                           | 安全插件默认开启 CSRF       |\n| 手动写 `afterEach(mm.restore)`     | 不需要                                        | egg-bin 自动注入 mock 恢复  |\n| 代码写在 describe 内、hooks 外     | 放入 `beforeAll` / `beforeEach`               | describe 体在加载阶段就执行 |\n| `await app.ready()` 配合 bootstrap | 不需要                                        | bootstrap 自动处理生命周期  |\n\n---\n\n## 参考资料\n\n- `references/http-test.md` — HTTP 接口测试\n- `references/service-test.md` — Service/DI 对象测试\n- `references/mock.md` — Mock 模式\n- `references/background-task-test.md` — BackgroundTaskHelper 测试\n- `references/eventbus-test.md` — EventBus 测试\n","frontmatter":{"name":"egg-unittest","description":"本技能用于编写 EGG 应用的单元测试。覆盖 HTTP 接口测试、Service/DI 对象测试、Mock 数据模拟、BackgroundTask 和 EventBus 测试。使用 @eggjs/mock、app.httpRequest()、app.getEggObject()、mm() 等 API。","allowed-tools":"Read","allowedTools":["Read"]},"allowedTools":["Read"],"isInternal":false,"tokens":1315,"sizeBytes":5167},{"name":"SKILL.md","path":"packages/skills/egg/SKILL.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/egg/SKILL.md","title":"egg","category":"anthropic-skill","format":"markdown","content":"---\nname: egg\ndescription: 本技能用于处理 EGG 框架。它提供基于用户意图在核心概念、控制器和单元测试之间做选择的决策指导。作为所有 EGG 相关问题的入口点使用。覆盖模块架构、依赖注入、后台任务、EventBus 事件总线、AOP 切面编程、HTTP/MCP/Schedule 控制器、Ajv 参数校验、单元测试等。\nallowed-tools: Read\n---\n\n# EGG 决策指南\n\n## 概述\n\n本技能帮助根据用户意图和任务类型确定使用哪个专用的 EGG 技能。EGG 文档组织为两个主要领域：\n\n1. **核心概念**（`egg-core` skill）：模块架构、依赖注入、对象生命周期、EventBus 事件总线、AOP 切面编程\n2. **控制器**（`egg-controller` skill）：用于 API 端点的各种协议特定控制器\n3. **单元测试**（`egg-unittest` skill）：HTTP 接口测试、Service/DI 对象测试、Mock 模拟、BackgroundTask 和 EventBus 测试\n\n## 技能选择逻辑\n\n### 使用 `egg-core` skill 当用户询问：\n\n**用户询问关于：**\n\n- 模块架构和组织\n- `@SingletonProto` vs `@ContextProto` 的使用\n- 使用 `@Inject` 的依赖注入\n- 对象生命周期和实例化\n- 模块之间的访问控制（`AccessLevel`）\n- 模块配置（`module.yml`、`package.json`）\n- 使用限定符解决命名冲突\n- 请求返回后执行异步任务（BackgroundTaskHelper）\n- 事件驱动架构（EventBus、@Event）\n- AOP 切面编程（@Advice、@Pointcut、@Crosscut）\n\n**触发关键词：**\n\n- module、workspace、modules\n- singleton、单例、@SingletonProto\n- context、request context、@ContextProto\n- inject、injection、dependency injection、@Inject\n- prototype、lifecycle、实例化\n- background task、异步任务、后台任务、BackgroundTaskHelper\n- eventbus、event bus、事件总线、事件驱动、@Event、emit、发布订阅、解耦\n- aop、切面、aspect、advice、pointcut、crosscut、拦截器、横切关注点\n- access level、private、public、@ModuleQualifier\n- configuration、module config\n\n**示例查询：**\n\n- \"如何在 EGG 中创建模块？\"\n- \"SingletonProto 和 ContextProto 有什么区别？\"\n- \"如何注入服务？\"\n- \"如何访问其他模块的对象？\"\n- \"EGG 中的 AccessLevel 是什么？\"\n- \"如何用 EventBus 解耦异步任务？\"\n- \"EventBus 和 BackgroundTaskHelper 有什么区别？\"\n- \"如何用 AOP 给所有 Service 加日志？\"\n\n### 使用 `egg-controller` skill 当用户询问：\n\n**用户询问关于：**\n\n- 创建 API 端点或接口\n- 实现特定协议处理器（HTTP、MCP 等）\n- 连接到外部系统或客户端\n- 处理传入的请求/响应\n- 控制器级别的装饰器和模式\n- 参数校验（Ajv + TypeBox）\n- 控制器中间件（Middleware，函数式或 AOP 写法）\n\n**触发关键词：**\n\n- controller、控制器\n- HTTP、API、REST、endpoint\n- MCP、LLM、AI、tool\n- schedule、timer、cron、scheduled、定时\n- SSE、streaming、server-sent events\n- validate、校验、参数校验、ajv、typebox、schema\n- middleware、中间件、拦截器、洋葱模型、@Middleware\n\n**示例查询：**\n\n- \"如何创建 HTTP controller？\"\n- \"如何实现 MCP 接口？\"\n- \"怎么实现定时任务？\"\n- \"帮我给接口加上参数校验\"\n- \"如何给控制器加中间件？\"\n- \"怎么写一个鉴权中间件？\"\n\n### 使用 `egg-unittest` skill 当用户询问：\n\n**用户询问关于：**\n\n- 编写单元测试或集成测试\n- 使用 @eggjs/mock 进行测试\n- 测试 HTTP 接口（app.httpRequest）\n- 测试 Service/DI 对象\n- Mock 数据或依赖\n- 测试 BackgroundTaskHelper 或 EventBus\n\n**触发关键词：**\n\n- test、测试、单测、单元测试、unittest、unit test\n- mock、mm、@eggjs/mock、mockCsrf、mockHttpclient、mockSession、mockContext\n- httpRequest、supertest\n- getEggObject、mockModuleContextScope\n- eventWaiter、BackgroundTaskHelper 测试\n- vitest、describe、it、beforeAll\n\n**示例查询：**\n\n- \"如何写单元测试？\"\n- \"如何测试 HTTP 接口？\"\n- \"如何 mock 一个 Service？\"\n- \"POST 请求测试报 CSRF 错误\"\n- \"如何测试 ContextProto 的 Service？\"\n- \"怎么测试后台任务是否执行完成？\"\n- \"怎么测试 EventBus 事件是否被正确处理？\"\n\n---\n\n## 决策框架\n\n### 步骤 1：识别意图类型\n\n询问：**用户是在询问构建块/框架内部 OR 实现特定接口？**\n\n**构建块/框架内部** → 使用 `egg-core` skill\n\n- 理解 EGG 如何工作\n- 组织代码结构\n- 管理对象生命周期\n- 设置模块\n\n**实现特定接口** → 使用 `egg-controller` skill\n\n- 创建 API/端点\n- 处理不同协议\n- 处理请求/响应\n\n### 步骤 2：检查模糊意图\n\n如果用户的意图可能是核心 OR 控制器（例如，\"如何实现一个需要跨模块访问的服务？\"）：\n\n**决策优先级**：核心概念优先\n\n理由：即使服务将在控制器中使用，关于跨模块访问（`AccessLevel`）的基本问题是一个核心概念。一旦理解了核心结构，用户就可以在控制器中应用它。\n\n**行动**：\n\n1. 使用 `egg-core` skill\n2. 解释概念（例如，`AccessLevel.PUBLIC`）\n3. 核心解释后，建议：\"如果你需要在特定控制器中使用它，请使用 `egg-controller` skill\n\n### 步骤 3：协议/用例特定指示器\n\n| 协议/用例               | 主要技能         | 次要技能 |\n| ----------------------- | ---------------- | -------- |\n| HTTP API                | `egg-controller` | -        |\n| MCP                     | `egg-controller` | -        |\n| Scheduled Tasks         | `egg-controller` | -        |\n| Parameter validation    | `egg-controller` | -        |\n| Controller Middleware   | `egg-controller` | -        |\n| Background tasks        | `egg-core`       | -        |\n| Event-driven / EventBus | `egg-core`       | -        |\n| AOP / 切面编程          | `egg-core`       | -        |\n| Cross-module injection  | `egg-core`       | -        |\n| Module structure        | `egg-core`       | -        |\n| Object lifecycle        | `egg-core`       | -        |\n| Unit Testing            | `egg-unittest`   | -        |\n| Mock / Test helpers     | `egg-unittest`   | -        |\n\n## 冲突解决规则\n\n### 规则 1：基础优先\n\n当问题同时涉及核心概念 AND 控制器实现时：\n\n- **示例**：\"如何实现一个 HTTP 控制器可以使用的单例服务？\"\n- **决策**：从 `egg-core` skill 开始解释 SingletonProto 和 AccessLevel\n- **后续**：\"现在你理解了服务定义，使用 `egg-controller` skill 实现注入此服务的 HTTP 控制器。\"\n\n### 规则 2：显式覆盖\n\n如果用户明确提及特定控制器类型：\n\n- **示例**：\"如何使用 HTTPController 配合 ContextProto 服务？\"\n- **决策**：使用 `egg-controller` skill（HTTPController 是显式的）\n- **后续**：解释 HTTPController 实现，如果需要简要提及来自核心概念的 ContextProto\n\n### 规则 3：学习语境\n\n如果用户问\"什么是 X？\"或\"Y 如何工作？\"：\n\n- **核心概念问题** → 使用 `egg-core` skill\n- **控制器类型问题** → 使用 `egg-controller` skill\n- **一般 EGG 问题** → 使用本技能的决策框架\n\n如果用户问\"如何实现 X？\"或\"给我看 Y 的代码？\"：\n\n- **实现特定问题** → 根据决策框架加载特定 skill\n\n## 快速参考表\n\n| 用户意图             | 关键词                                  | 使用技能               |\n| -------------------- | --------------------------------------- | ---------------------- |\n| Module architecture  | module、workspace、organization         | `egg-core` skill       |\n| Object lifecycle     | singleton、context、lifecycle           | `egg-core` skill       |\n| Dependency injection | inject、@Inject、dependency             | `egg-core` skill       |\n| Access control       | private、public、cross-module           | `egg-core` skill       |\n| Background tasks     | background task、异步任务、后台任务     | `egg-core` skill       |\n| Event-driven         | eventbus、事件总线、@Event、emit        | `egg-core` skill       |\n| AOP 切面编程         | aop、切面、advice、pointcut、crosscut   | `egg-core` skill       |\n| HTTP endpoints       | HTTP、API、REST                         | `egg-controller` skill |\n| LLM/AI integration   | MCP、tool、prompt                       | `egg-controller` skill |\n| Scheduling           | schedule、cron、timer                   | `egg-controller` skill |\n| Param validation     | validate、校验、ajv、typebox、schema    | `egg-controller` skill |\n| Middleware 中间件    | middleware、中间件、拦截器、@Middleware | `egg-controller` skill |\n| Unit testing         | test、mock、unittest、单测              | `egg-unittest` skill   |\n| Mock dependencies    | mock、mm、mockCsrf、mockHttpclient      | `egg-unittest` skill   |\n\n---\n\n## 示例\n\n### 示例 1：明确的核心意图\n\n**用户**：\"@SingletonProto 和 @ContextProto 有什么区别？\"\n\n**分析**：问题关于核心装饰器和对象生命周期\n**决策**：使用 `egg-core` skill\n\n**用户**：\"如何在 EGG 中创建模块？\"\n\n**分析**：问题关于模块架构（核心概念）\n**决策**：使用 `egg-core` skill\n\n### 示例 2：明确的控制器意图\n\n**用户**：\"如何创建返回 JSON 的 HTTP controller？\"\n\n**分析**：问题关于实现特定协议（HTTP）\n**决策**：使用 `egg-controller` skill\n\n### 示例 3：模糊意图（核心 > 控制器）\n\n**用户**：\"我需要创建一个可以被 HTTP 控制器使用的服务。如何实现？\"\n\n**分析**：用户需要理解核心概念（跨模块访问）AND 控制器实现\n**决策**：首先使用 `egg-core` skill（基础）\n\n**响应**：\n\n1. 解释 `@SingletonProto` 配合 `AccessLevel.PUBLIC` 使服务可访问\n2. 展示如何注入服务：`@Inject() myService: MyService`\n3. 后续：\"现在你可以在 HTTPController 中注入此服务。实现详情请使用 `egg-controller` skill。\"\n\n### 示例 4：显式控制器加上核心知识\n\n**用户**：\"如何在 HTTPController 中使用 @Inject 访问用户服务？\"\n\n**分析**：用户明确提及 HTTPController（控制器类型）但询问 @Inject（核心概念）\n**决策**：使用 `egg-controller` skill（HTTPController 是明确意图）\n\n**响应**：\n\n1. 展示配合 `@Inject` 的 HTTPController 实现\n2. 简要解释 @Inject 如何工作（核心概念摘要）\n3. 注意：\"包含限定符的详细 @Inject 使用，请使用 `egg-core` skill\"\n\n### 示例 5：测试相关\n\n**用户**：\"帮我写个 UserController 的单元测试\"\n\n**分析**：问题关于编写测试代码\n**决策**：使用 `egg-unittest` skill\n\n**用户**：\"POST 请求测试报 403 错误\"\n\n**分析**：测试中遇到 CSRF 问题\n**决策**：使用 `egg-unittest` skill\n\n## 路由最佳实践\n\n1. **优先考虑显式控制器提及**：如果用户命名特定控制器（HTTP），即使涉及核心概念也使用控制器技能\n\n2. **基础先行**：如果实现前需要理解核心概念，先解释核心概念\n\n3. **简短上下文可以接受**：当路由到一个技能时，提及另一个技能的存在以供后续问题\n\n4. **混合响应可接受**：当意图真正混合时，提供两个技能的简短上下文但专注于主要意图\n\n5. **渐进式披露**：除非明确要求，不要同时加载两个技能。让用户引导探索\n\n---\n\n## 技能交互\n\n本技能（`egg` skill）应该：\n\n- 为框架内部概念路由到 `egg-core` skill\n- 为协议特定实现路由到 `egg-controller` skill\n- 为测试相关问题路由到 `egg-unittest` skill\n- 当意图模糊时提供决策指导\n- 当存在有用的上下文时交叉引用技能\n","frontmatter":{"name":"egg","description":"本技能用于处理 EGG 框架。它提供基于用户意图在核心概念、控制器和单元测试之间做选择的决策指导。作为所有 EGG 相关问题的入口点使用。覆盖模块架构、依赖注入、后台任务、EventBus 事件总线、AOP 切面编程、HTTP/MCP/Schedule 控制器、Ajv 参数校验、单元测试等。","allowed-tools":"Read","allowedTools":["Read"]},"allowedTools":["Read"],"isInternal":false,"tokens":3664,"sizeBytes":11737},{"name":"evals-egg-controller.json","path":"packages/skills/eval/evals-egg-controller.json","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/eval/evals-egg-controller.json","title":"Eval Skill","category":"anthropic-skill","format":"json","content":"{\n  \"skill_name\": \"egg-controller\",\n  \"description\": \"控制器评测：覆盖 http-controller、mcp-controller、schedule、ajv-validate、middleware\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"帮我写一个 POST /api/users 接口，接收 JSON body 创建用户，返回 201\",\n      \"expected_output\": \"@HTTPController + @HTTPMethod POST + @HTTPBody + ctx.status = 201，完整可运行代码\"\n    },\n    {\n      \"id\": 2,\n      \"prompt\": \"帮我实现 GET /api/users/:userId/posts/:postId，获取用户的某篇文章\",\n      \"expected_output\": \"@HTTPParam() userId + @HTTPParam() postId，路径参数定义和获取的完整代码\"\n    },\n    {\n      \"id\": 3,\n      \"prompt\": \"我有个搜索接口 /api/search?tag=tech&tag=dev，需要获取 tag 的全部值\",\n      \"expected_output\": \"使用 @HTTPQueries({ name: 'tag' }) 获取 string[] 数组，不是 @HTTPQuery\"\n    },\n    {\n      \"id\": 4,\n      \"prompt\": \"帮我写个 SSR 页面，GET / 返回完整的 HTML\",\n      \"expected_output\": \"@HTTPController + ctx.type = 'html' + return HTML 字符串\"\n    },\n    {\n      \"id\": 5,\n      \"prompt\": \"帮我实现流式输出，一段一段返回 HTML 内容\",\n      \"expected_output\": \"async generator + Readable.from()，设置 ctx.type，完整代码\"\n    },\n    {\n      \"id\": 6,\n      \"prompt\": \"我有两个路由 /api/:name 和 /api/health，请求 /api/health 匹配到了 :name，帮我看看代码\",\n      \"expected_output\": \"静态路径优先级高于参数路径应该自动匹配，如果冲突可用 priority 参数调整\",\n      \"files\": [\n        {\n          \"path\": \"app/apiModule/ApiController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPParam } from 'egg';\\n\\n@HTTPController({ path: '/api' })\\nexport class ApiController {\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/:name' })\\n  async getByName(@HTTPParam() name: string) {\\n    return { name };\\n  }\\n\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/health' })\\n  async health() {\\n    return { status: 'ok' };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 7,\n      \"prompt\": \"帮我给这个 Controller 加一个认证中间件的逻辑，从 header 里取 Authorization 做校验\",\n      \"expected_output\": \"使用 @HTTPHeaders() 获取 headers，key 用小写 headers['authorization']\",\n      \"files\": [\n        {\n          \"path\": \"app/apiModule/UserController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum } from 'egg';\\n\\n@HTTPController({ path: '/api/users' })\\nexport class UserController {\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })\\n  async list() {\\n    return { users: [] };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 8,\n      \"prompt\": \"帮我写一个完整的 CRUD 模块：userModule，包含 UserService 和 UserController，支持 GET/POST/PUT/DELETE\",\n      \"expected_output\": \"完整文件结构：package.json + UserService(@SingletonProto + PUBLIC) + UserController(@HTTPController)，四个 HTTP 方法的完整代码\"\n    },\n    {\n      \"id\": 9,\n      \"prompt\": \"基于这个 Service，帮我写一个 HTTPController 暴露查询和创建接口\",\n      \"expected_output\": \"@HTTPController + 两个 @HTTPMethod（GET 用 @HTTPParam，POST 用 @HTTPBody）+ @Inject 注入 ProductService\",\n      \"files\": [\n        {\n          \"path\": \"app/productModule/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"productModule\\\",\\n  \\\"eggModule\\\": { \\\"name\\\": \\\"productModule\\\" }\\n}\"\n        },\n        {\n          \"path\": \"app/productModule/ProductService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class ProductService {\\n  async findById(id: string) {\\n    return { id, name: 'Product ' + id, price: 99 };\\n  }\\n\\n  async create(data: { name: string; price: number }) {\\n    return { id: 'new-id', ...data };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 10,\n      \"prompt\": \"帮我实现一个 MCP Tool，让 AI 能搜索我们的内部文档\",\n      \"expected_output\": \"@MCPController() + @MCPTool() + @ToolArgsSchema + Service 注入，import 从 @eggjs/tegg，zod 从 @eggjs/tegg/zod\"\n    },\n    {\n      \"id\": 11,\n      \"prompt\": \"MCP Tool 的参数 Schema 写法有问题，帮我看看\",\n      \"expected_output\": \"指出不应该用 z.object() 包装，直接用普通对象 { name: z.string() }\",\n      \"files\": [\n        {\n          \"path\": \"app/docModule/DocMCPController.ts\",\n          \"content\": \"import { MCPController, MCPTool, MCPToolResponse, ToolArgs, ToolArgsSchema, Inject } from '@eggjs/tegg';\\nimport { z } from '@eggjs/tegg/zod';\\n\\nconst SearchSchema = z.object({\\n  keyword: z.string({ description: 'search keyword' }),\\n  limit: z.number().optional(),\\n});\\n\\n@MCPController()\\nexport class DocMCPController {\\n  @MCPTool({ description: 'Search internal docs' })\\n  async search(\\n    @ToolArgsSchema(SearchSchema) args: ToolArgs<typeof SearchSchema>,\\n  ): Promise<MCPToolResponse> {\\n    return { content: [{ type: 'text', text: 'results' }] };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 12,\n      \"prompt\": \"我的 MCP Tool 要处理一个大数据导出，可能要跑几分钟，怎么给客户端推送进度\",\n      \"expected_output\": \"@Extra() 获取 ToolExtra，用 sendNotification 推送进度，给出完整代码\"\n    },\n    {\n      \"id\": 13,\n      \"prompt\": \"帮我写一个 MCP Resource，让 AI 能读取指定用户的 profile\",\n      \"expected_output\": \"@MCPResource({ template: [...] }) + uri 参数解析，完整代码\"\n    },\n    {\n      \"id\": 14,\n      \"prompt\": \"帮我写个定时任务，每天凌晨 3 点清理过期数据\",\n      \"expected_output\": \"@Schedule<CronParams> + cron: '0 0 3 * * *' + ScheduleType.WORKER，import 从 egg/schedule\"\n    },\n    {\n      \"id\": 15,\n      \"prompt\": \"定时任务放在 app/schedule 目录下不生效，帮我看看\",\n      \"expected_output\": \"指出不能放在 app/schedule 下（和 egg 默认扫描冲突），应放在模块目录中\",\n      \"files\": [\n        {\n          \"path\": \"app/schedule/CleanTask.ts\",\n          \"content\": \"import { Inject, Logger } from 'egg';\\nimport { IntervalParams, Schedule, ScheduleType } from 'egg/schedule';\\n\\n@Schedule<IntervalParams>({\\n  type: ScheduleType.WORKER,\\n  scheduleData: { interval: '1h' },\\n})\\nexport class CleanTask {\\n  @Inject()\\n  private logger: Logger;\\n\\n  async subscribe() {\\n    this.logger.info('cleaning...');\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 16,\n      \"prompt\": \"帮我写一个每 30 秒刷新缓存的定时任务，要求每个 worker 都执行\",\n      \"expected_output\": \"@Schedule<IntervalParams> + interval: '30s' + ScheduleType.ALL\"\n    },\n    {\n      \"id\": 17,\n      \"prompt\": \"基于这个 CacheService，帮我写一个定时任务每 5 分钟刷新一次缓存\",\n      \"expected_output\": \"@Schedule<IntervalParams> + interval: '5m' + @Inject() cacheService，import 从 egg/schedule\",\n      \"files\": [\n        {\n          \"path\": \"app/cacheModule/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"cacheModule\\\",\\n  \\\"eggModule\\\": { \\\"name\\\": \\\"cacheModule\\\" }\\n}\"\n        },\n        {\n          \"path\": \"app/cacheModule/CacheService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class CacheService {\\n  private cache = new Map<string, any>();\\n\\n  async refresh() {\\n    this.cache.clear();\\n    // reload from db\\n  }\\n\\n  get(key: string) {\\n    return this.cache.get(key);\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 18,\n      \"prompt\": \"帮我把这个老的 egg controller 改成 HTTPController 写法\",\n      \"expected_output\": \"用 @HTTPController + @HTTPMethod 重写，参数用装饰器获取，注入 Service 替代 ctx.service\",\n      \"files\": [\n        {\n          \"path\": \"app/controller/user.ts\",\n          \"content\": \"import { Controller } from 'egg';\\n\\nexport default class UserController extends Controller {\\n  async show() {\\n    const { ctx } = this;\\n    const id = ctx.params.id;\\n    const user = await ctx.service.user.find(id);\\n    ctx.body = { success: true, data: user };\\n  }\\n\\n  async create() {\\n    const { ctx } = this;\\n    const body = ctx.request.body;\\n    const user = await ctx.service.user.create(body);\\n    ctx.status = 201;\\n    ctx.body = { success: true, data: user };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 19,\n      \"prompt\": \"帮我给这个 POST /api/users 接口加上参数校验，name 必填且最多50字符，email 必须是邮箱格式\",\n      \"expected_output\": \"从 egg/ajv 导入 Type/Ajv/Static，定义 TypeBox Schema（name: Type.String + maxLength, email: Type.String + format: email），@Inject() ajv: Ajv，ajv.validate()\",\n      \"files\": [\n        {\n          \"path\": \"app/userModule/UserController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPBody } from 'egg';\\n\\n@HTTPController({ path: '/api/users' })\\nexport class UserController {\\n  @HTTPMethod({ method: HTTPMethodEnum.POST, path: '/' })\\n  async create(@HTTPBody() body: { name: string; email: string }) {\\n    return { id: '1', ...body };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 20,\n      \"prompt\": \"参数校验跑不起来，帮我看看代码哪里有问题\",\n      \"expected_output\": \"指出三个错误：1. 导入路径应从 egg/ajv 而非 typebox/ajv 2. 不应 new Ajv() 手动创建 3. 应使用 @Inject() 注入全局单例\",\n      \"files\": [\n        {\n          \"path\": \"app/userModule/UserController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPBody, Inject } from 'egg';\\nimport { Type, Static } from 'typebox';\\nimport Ajv from 'ajv';\\n\\nconst CreateUserSchema = Type.Object({\\n  name: Type.String({ maxLength: 50 }),\\n  email: Type.String({ format: 'email' }),\\n});\\n\\ntype CreateUserParams = Static<typeof CreateUserSchema>;\\n\\nconst ajv = new Ajv();\\n\\n@HTTPController({ path: '/api/users' })\\nexport class UserController {\\n  @HTTPMethod({ method: HTTPMethodEnum.POST, path: '/' })\\n  async create(@HTTPBody() body: CreateUserParams) {\\n    const valid = ajv.validate(CreateUserSchema, body);\\n    if (!valid) throw new Error('Validation failed');\\n    return { id: '1', ...body };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 21,\n      \"prompt\": \"帮我写一个注册接口的参数校验，要校验 email 格式、生日要是日期格式、用户ID要是 UUID 格式\",\n      \"expected_output\": \"使用 Type.String({ format: 'email' })、Type.String({ format: 'date' })、Type.String({ format: 'uuid' })，从 egg/ajv 导入\"\n    },\n    {\n      \"id\": 22,\n      \"prompt\": \"用户输入的名字前后经常有空格，怎么在参数校验时自动去掉空格\",\n      \"expected_output\": \"使用 TransformEnum.trim，从 egg/ajv 导入 TransformEnum，Type.String({ transform: [TransformEnum.trim] })\"\n    },\n    {\n      \"id\": 23,\n      \"prompt\": \"有些字段是选填的，比如用户的昵称和个人简介，校验时不传不应该报错\",\n      \"expected_output\": \"使用 Type.Optional() 包装选填字段，如 Type.Optional(Type.String())\"\n    },\n    {\n      \"id\": 24,\n      \"prompt\": \"帮我写一个校验 Schema，要求 tags 字段是一个元组，第一个元素是字符串类型的分类名，第二个元素是数字类型的权重\",\n      \"expected_output\": \"使用 Type.Tuple([Type.String(), Type.Number()])，AI 需要从 TypeBox 文档链接查找 Tuple 用法\"\n    },\n    {\n      \"id\": 25,\n      \"prompt\": \"基于这个 OrderService，帮我写一个创建订单的接口，需要做参数校验：productId 必填字符串，quantity 必填正整数，remark 选填\",\n      \"expected_output\": \"定义 Schema + @Inject() ajv: Ajv + ajv.validate() + @Inject() orderService，完整可运行代码\",\n      \"files\": [\n        {\n          \"path\": \"app/orderModule/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"orderModule\\\",\\n  \\\"eggModule\\\": { \\\"name\\\": \\\"orderModule\\\" }\\n}\"\n        },\n        {\n          \"path\": \"app/orderModule/OrderService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class OrderService {\\n  async create(data: { productId: string; quantity: number; remark?: string }) {\\n    return { orderId: 'order-' + Date.now(), ...data };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 26,\n      \"prompt\": \"帮我给这个 Controller 加个日志中间件，记录每个请求的耗时\",\n      \"expected_output\": \"使用 AOP 写法：定义 @Advice 类实现 IAdvice 的 around 方法，@Inject Logger，用 @Middleware 应用到 Controller。import Advice 从 egg/aop，Middleware 从 egg\",\n      \"files\": [\n        {\n          \"path\": \"app/modules/user/UserController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum } from 'egg';\\n\\n@HTTPController({ path: '/api/users' })\\nexport class UserController {\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })\\n  async list() {\\n    return { users: [] };\\n  }\\n\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/:id' })\\n  async show() {\\n    return { id: '1', name: 'test' };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 27,\n      \"prompt\": \"帮我写一个鉴权中间件，从 header 取 token 校验，校验失败抛异常，只应用到部分接口\",\n      \"expected_output\": \"AOP @Advice 类 + around 方法中校验 token + @Middleware 应用到方法级别（而非类级别），import Advice/IAdvice/AdviceContext 从 egg/aop\"\n    },\n    {\n      \"id\": 28,\n      \"prompt\": \"中间件代码报错 AOP and MiddlewareFunc can not be mixed，帮我看看哪里写错了\",\n      \"expected_output\": \"指出不能在同一个 @Middleware() 中混用函数和 Advice 类，需要分开写两个 @Middleware\",\n      \"files\": [\n        {\n          \"path\": \"app/modules/api/ApiController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum, Middleware } from 'egg';\\nimport { LogAdvice } from '../common/advice/LogAdvice.ts';\\nimport { countMw } from './middleware/count.ts';\\n\\n@HTTPController({ path: '/api' })\\n@Middleware(LogAdvice, countMw)\\nexport class ApiController {\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/hello' })\\n  async hello() {\\n    return { message: 'hello' };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 29,\n      \"prompt\": \"我想写一个中间件统一包装接口返回值为 { success: true, data: ... } 的格式\",\n      \"expected_output\": \"AOP @Advice 类 + around 方法中 await next() 拿到返回值后包装为 { success: true, data: result }，用 @Middleware 应用\"\n    },\n    {\n      \"id\": 30,\n      \"prompt\": \"AOP 中间件和函数式中间件有什么区别？什么时候用哪种\",\n      \"expected_output\": \"AOP 为推荐写法，支持 @Inject 注入依赖；函数式为旧版兼容写法，需从 ctx 手动获取依赖。新项目优先用 AOP\"\n    },\n    {\n      \"id\": 31,\n      \"prompt\": \"帮我把这个老的 egg 函数式中间件改成 AOP 写法\",\n      \"expected_output\": \"定义 @Advice 类 + 实现 around 方法，用 @Inject 注入 Logger 替代 ctx.logger，import 从 egg/aop\",\n      \"files\": [\n        {\n          \"path\": \"app/modules/api/middleware/log.ts\",\n          \"content\": \"import type { Context, Next } from 'egg';\\n\\nexport async function logMw(ctx: Context, next: Next): Promise<void> {\\n  const start = Date.now();\\n  await next();\\n  ctx.logger.info('request %s %s cost %dms', ctx.method, ctx.url, Date.now() - start);\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 32,\n      \"prompt\": \"我在 Advice 中用了一个实例属性 count 来统计请求数，但是多个请求之间数据串了，帮我看看\",\n      \"expected_output\": \"指出 Advice 默认是 Singleton，实例属性会被并发请求共享。请求级状态应使用 ctx.set()/ctx.get() 传递\",\n      \"files\": [\n        {\n          \"path\": \"app/modules/api/advice/RequestCountAdvice.ts\",\n          \"content\": \"import { Advice, IAdvice, AdviceContext } from 'egg/aop';\\n\\n@Advice()\\nexport class RequestCountAdvice implements IAdvice {\\n  private requestId = '';\\n\\n  async around(ctx: AdviceContext, next: () => Promise<any>): Promise<any> {\\n    this.requestId = Math.random().toString(36).slice(2);\\n    const result = await next();\\n    return { ...result, requestId: this.requestId };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 33,\n      \"prompt\": \"帮我看看这个 Controller 上的中间件执行顺序是什么样的\",\n      \"expected_output\": \"函数式和 AOP 分属两个独立执行阶段：先执行所有函数式中间件（类级别 countMw → 方法级别 timeMw），再执行所有 AOP 中间件（类级别 LogAdvice → 方法级别 AuthAdvice），最后执行 hello()。即 countMw → timeMw → LogAdvice → AuthAdvice → hello()，与装饰器书写顺序无关\",\n      \"files\": [\n        {\n          \"path\": \"app/modules/api/ApiController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum, Middleware } from 'egg';\\nimport { LogAdvice } from '../common/advice/LogAdvice.ts';\\nimport { AuthAdvice } from '../common/advice/AuthAdvice.ts';\\nimport { countMw } from './middleware/count.ts';\\nimport { timeMw } from './middleware/time.ts';\\n\\n@HTTPController({ path: '/api' })\\n@Middleware(countMw)\\n@Middleware(LogAdvice)\\nexport class ApiController {\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/hello' })\\n  @Middleware(timeMw)\\n  @Middleware(AuthAdvice)\\n  async hello() {\\n    return { message: 'hello' };\\n  }\\n}\"\n        }\n      ]\n    }\n  ]\n}\n","frontmatter":{"skill_name":"egg-controller","description":"控制器评测：覆盖 http-controller、mcp-controller、schedule、ajv-validate、middleware","evals":[{"id":1,"prompt":"帮我写一个 POST /api/users 接口，接收 JSON body 创建用户，返回 201","expected_output":"@HTTPController + @HTTPMethod POST + @HTTPBody + ctx.status = 201，完整可运行代码"},{"id":2,"prompt":"帮我实现 GET /api/users/:userId/posts/:postId，获取用户的某篇文章","expected_output":"@HTTPParam() userId + @HTTPParam() postId，路径参数定义和获取的完整代码"},{"id":3,"prompt":"我有个搜索接口 /api/search?tag=tech&tag=dev，需要获取 tag 的全部值","expected_output":"使用 @HTTPQueries({ name: 'tag' }) 获取 string[] 数组，不是 @HTTPQuery"},{"id":4,"prompt":"帮我写个 SSR 页面，GET / 返回完整的 HTML","expected_output":"@HTTPController + ctx.type = 'html' + return HTML 字符串"},{"id":5,"prompt":"帮我实现流式输出，一段一段返回 HTML 内容","expected_output":"async generator + Readable.from()，设置 ctx.type，完整代码"},{"id":6,"prompt":"我有两个路由 /api/:name 和 /api/health，请求 /api/health 匹配到了 :name，帮我看看代码","expected_output":"静态路径优先级高于参数路径应该自动匹配，如果冲突可用 priority 参数调整","files":[{"path":"app/apiModule/ApiController.ts","content":"import { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPParam } from 'egg';\n\n@HTTPController({ path: '/api' })\nexport class ApiController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/:name' })\n  async getByName(@HTTPParam() name: string) {\n    return { name };\n  }\n\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/health' })\n  async health() {\n    return { status: 'ok' };\n  }\n}"}]},{"id":7,"prompt":"帮我给这个 Controller 加一个认证中间件的逻辑，从 header 里取 Authorization 做校验","expected_output":"使用 @HTTPHeaders() 获取 headers，key 用小写 headers['authorization']","files":[{"path":"app/apiModule/UserController.ts","content":"import { HTTPController, HTTPMethod, HTTPMethodEnum } from 'egg';\n\n@HTTPController({ path: '/api/users' })\nexport class UserController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })\n  async list() {\n    return { users: [] };\n  }\n}"}]},{"id":8,"prompt":"帮我写一个完整的 CRUD 模块：userModule，包含 UserService 和 UserController，支持 GET/POST/PUT/DELETE","expected_output":"完整文件结构：package.json + UserService(@SingletonProto + PUBLIC) + UserController(@HTTPController)，四个 HTTP 方法的完整代码"},{"id":9,"prompt":"基于这个 Service，帮我写一个 HTTPController 暴露查询和创建接口","expected_output":"@HTTPController + 两个 @HTTPMethod（GET 用 @HTTPParam，POST 用 @HTTPBody）+ @Inject 注入 ProductService","files":[{"path":"app/productModule/package.json","content":"{\n  \"name\": \"productModule\",\n  \"eggModule\": { \"name\": \"productModule\" }\n}"},{"path":"app/productModule/ProductService.ts","content":"import { SingletonProto, AccessLevel } from 'egg';\n\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class ProductService {\n  async findById(id: string) {\n    return { id, name: 'Product ' + id, price: 99 };\n  }\n\n  async create(data: { name: string; price: number }) {\n    return { id: 'new-id', ...data };\n  }\n}"}]},{"id":10,"prompt":"帮我实现一个 MCP Tool，让 AI 能搜索我们的内部文档","expected_output":"@MCPController() + @MCPTool() + @ToolArgsSchema + Service 注入，import 从 @eggjs/tegg，zod 从 @eggjs/tegg/zod"},{"id":11,"prompt":"MCP Tool 的参数 Schema 写法有问题，帮我看看","expected_output":"指出不应该用 z.object() 包装，直接用普通对象 { name: z.string() }","files":[{"path":"app/docModule/DocMCPController.ts","content":"import { MCPController, MCPTool, MCPToolResponse, ToolArgs, ToolArgsSchema, Inject } from '@eggjs/tegg';\nimport { z } from '@eggjs/tegg/zod';\n\nconst SearchSchema = z.object({\n  keyword: z.string({ description: 'search keyword' }),\n  limit: z.number().optional(),\n});\n\n@MCPController()\nexport class DocMCPController {\n  @MCPTool({ description: 'Search internal docs' })\n  async search(\n    @ToolArgsSchema(SearchSchema) args: ToolArgs<typeof SearchSchema>,\n  ): Promise<MCPToolResponse> {\n    return { content: [{ type: 'text', text: 'results' }] };\n  }\n}"}]},{"id":12,"prompt":"我的 MCP Tool 要处理一个大数据导出，可能要跑几分钟，怎么给客户端推送进度","expected_output":"@Extra() 获取 ToolExtra，用 sendNotification 推送进度，给出完整代码"},{"id":13,"prompt":"帮我写一个 MCP Resource，让 AI 能读取指定用户的 profile","expected_output":"@MCPResource({ template: [...] }) + uri 参数解析，完整代码"},{"id":14,"prompt":"帮我写个定时任务，每天凌晨 3 点清理过期数据","expected_output":"@Schedule<CronParams> + cron: '0 0 3 * * *' + ScheduleType.WORKER，import 从 egg/schedule"},{"id":15,"prompt":"定时任务放在 app/schedule 目录下不生效，帮我看看","expected_output":"指出不能放在 app/schedule 下（和 egg 默认扫描冲突），应放在模块目录中","files":[{"path":"app/schedule/CleanTask.ts","content":"import { Inject, Logger } from 'egg';\nimport { IntervalParams, Schedule, ScheduleType } from 'egg/schedule';\n\n@Schedule<IntervalParams>({\n  type: ScheduleType.WORKER,\n  scheduleData: { interval: '1h' },\n})\nexport class CleanTask {\n  @Inject()\n  private logger: Logger;\n\n  async subscribe() {\n    this.logger.info('cleaning...');\n  }\n}"}]},{"id":16,"prompt":"帮我写一个每 30 秒刷新缓存的定时任务，要求每个 worker 都执行","expected_output":"@Schedule<IntervalParams> + interval: '30s' + ScheduleType.ALL"},{"id":17,"prompt":"基于这个 CacheService，帮我写一个定时任务每 5 分钟刷新一次缓存","expected_output":"@Schedule<IntervalParams> + interval: '5m' + @Inject() cacheService，import 从 egg/schedule","files":[{"path":"app/cacheModule/package.json","content":"{\n  \"name\": \"cacheModule\",\n  \"eggModule\": { \"name\": \"cacheModule\" }\n}"},{"path":"app/cacheModule/CacheService.ts","content":"import { SingletonProto, AccessLevel } from 'egg';\n\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class CacheService {\n  private cache = new Map<string, any>();\n\n  async refresh() {\n    this.cache.clear();\n    // reload from db\n  }\n\n  get(key: string) {\n    return this.cache.get(key);\n  }\n}"}]},{"id":18,"prompt":"帮我把这个老的 egg controller 改成 HTTPController 写法","expected_output":"用 @HTTPController + @HTTPMethod 重写，参数用装饰器获取，注入 Service 替代 ctx.service","files":[{"path":"app/controller/user.ts","content":"import { Controller } from 'egg';\n\nexport default class UserController extends Controller {\n  async show() {\n    const { ctx } = this;\n    const id = ctx.params.id;\n    const user = await ctx.service.user.find(id);\n    ctx.body = { success: true, data: user };\n  }\n\n  async create() {\n    const { ctx } = this;\n    const body = ctx.request.body;\n    const user = await ctx.service.user.create(body);\n    ctx.status = 201;\n    ctx.body = { success: true, data: user };\n  }\n}"}]},{"id":19,"prompt":"帮我给这个 POST /api/users 接口加上参数校验，name 必填且最多50字符，email 必须是邮箱格式","expected_output":"从 egg/ajv 导入 Type/Ajv/Static，定义 TypeBox Schema（name: Type.String + maxLength, email: Type.String + format: email），@Inject() ajv: Ajv，ajv.validate()","files":[{"path":"app/userModule/UserController.ts","content":"import { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPBody } from 'egg';\n\n@HTTPController({ path: '/api/users' })\nexport class UserController {\n  @HTTPMethod({ method: HTTPMethodEnum.POST, path: '/' })\n  async create(@HTTPBody() body: { name: string; email: string }) {\n    return { id: '1', ...body };\n  }\n}"}]},{"id":20,"prompt":"参数校验跑不起来，帮我看看代码哪里有问题","expected_output":"指出三个错误：1. 导入路径应从 egg/ajv 而非 typebox/ajv 2. 不应 new Ajv() 手动创建 3. 应使用 @Inject() 注入全局单例","files":[{"path":"app/userModule/UserController.ts","content":"import { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPBody, Inject } from 'egg';\nimport { Type, Static } from 'typebox';\nimport Ajv from 'ajv';\n\nconst CreateUserSchema = Type.Object({\n  name: Type.String({ maxLength: 50 }),\n  email: Type.String({ format: 'email' }),\n});\n\ntype CreateUserParams = Static<typeof CreateUserSchema>;\n\nconst ajv = new Ajv();\n\n@HTTPController({ path: '/api/users' })\nexport class UserController {\n  @HTTPMethod({ method: HTTPMethodEnum.POST, path: '/' })\n  async create(@HTTPBody() body: CreateUserParams) {\n    const valid = ajv.validate(CreateUserSchema, body);\n    if (!valid) throw new Error('Validation failed');\n    return { id: '1', ...body };\n  }\n}"}]},{"id":21,"prompt":"帮我写一个注册接口的参数校验，要校验 email 格式、生日要是日期格式、用户ID要是 UUID 格式","expected_output":"使用 Type.String({ format: 'email' })、Type.String({ format: 'date' })、Type.String({ format: 'uuid' })，从 egg/ajv 导入"},{"id":22,"prompt":"用户输入的名字前后经常有空格，怎么在参数校验时自动去掉空格","expected_output":"使用 TransformEnum.trim，从 egg/ajv 导入 TransformEnum，Type.String({ transform: [TransformEnum.trim] })"},{"id":23,"prompt":"有些字段是选填的，比如用户的昵称和个人简介，校验时不传不应该报错","expected_output":"使用 Type.Optional() 包装选填字段，如 Type.Optional(Type.String())"},{"id":24,"prompt":"帮我写一个校验 Schema，要求 tags 字段是一个元组，第一个元素是字符串类型的分类名，第二个元素是数字类型的权重","expected_output":"使用 Type.Tuple([Type.String(), Type.Number()])，AI 需要从 TypeBox 文档链接查找 Tuple 用法"},{"id":25,"prompt":"基于这个 OrderService，帮我写一个创建订单的接口，需要做参数校验：productId 必填字符串，quantity 必填正整数，remark 选填","expected_output":"定义 Schema + @Inject() ajv: Ajv + ajv.validate() + @Inject() orderService，完整可运行代码","files":[{"path":"app/orderModule/package.json","content":"{\n  \"name\": \"orderModule\",\n  \"eggModule\": { \"name\": \"orderModule\" }\n}"},{"path":"app/orderModule/OrderService.ts","content":"import { SingletonProto, AccessLevel } from 'egg';\n\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class OrderService {\n  async create(data: { productId: string; quantity: number; remark?: string }) {\n    return { orderId: 'order-' + Date.now(), ...data };\n  }\n}"}]},{"id":26,"prompt":"帮我给这个 Controller 加个日志中间件，记录每个请求的耗时","expected_output":"使用 AOP 写法：定义 @Advice 类实现 IAdvice 的 around 方法，@Inject Logger，用 @Middleware 应用到 Controller。import Advice 从 egg/aop，Middleware 从 egg","files":[{"path":"app/modules/user/UserController.ts","content":"import { HTTPController, HTTPMethod, HTTPMethodEnum } from 'egg';\n\n@HTTPController({ path: '/api/users' })\nexport class UserController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })\n  async list() {\n    return { users: [] };\n  }\n\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/:id' })\n  async show() {\n    return { id: '1', name: 'test' };\n  }\n}"}]},{"id":27,"prompt":"帮我写一个鉴权中间件，从 header 取 token 校验，校验失败抛异常，只应用到部分接口","expected_output":"AOP @Advice 类 + around 方法中校验 token + @Middleware 应用到方法级别（而非类级别），import Advice/IAdvice/AdviceContext 从 egg/aop"},{"id":28,"prompt":"中间件代码报错 AOP and MiddlewareFunc can not be mixed，帮我看看哪里写错了","expected_output":"指出不能在同一个 @Middleware() 中混用函数和 Advice 类，需要分开写两个 @Middleware","files":[{"path":"app/modules/api/ApiController.ts","content":"import { HTTPController, HTTPMethod, HTTPMethodEnum, Middleware } from 'egg';\nimport { LogAdvice } from '../common/advice/LogAdvice.ts';\nimport { countMw } from './middleware/count.ts';\n\n@HTTPController({ path: '/api' })\n@Middleware(LogAdvice, countMw)\nexport class ApiController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/hello' })\n  async hello() {\n    return { message: 'hello' };\n  }\n}"}]},{"id":29,"prompt":"我想写一个中间件统一包装接口返回值为 { success: true, data: ... } 的格式","expected_output":"AOP @Advice 类 + around 方法中 await next() 拿到返回值后包装为 { success: true, data: result }，用 @Middleware 应用"},{"id":30,"prompt":"AOP 中间件和函数式中间件有什么区别？什么时候用哪种","expected_output":"AOP 为推荐写法，支持 @Inject 注入依赖；函数式为旧版兼容写法，需从 ctx 手动获取依赖。新项目优先用 AOP"},{"id":31,"prompt":"帮我把这个老的 egg 函数式中间件改成 AOP 写法","expected_output":"定义 @Advice 类 + 实现 around 方法，用 @Inject 注入 Logger 替代 ctx.logger，import 从 egg/aop","files":[{"path":"app/modules/api/middleware/log.ts","content":"import type { Context, Next } from 'egg';\n\nexport async function logMw(ctx: Context, next: Next): Promise<void> {\n  const start = Date.now();\n  await next();\n  ctx.logger.info('request %s %s cost %dms', ctx.method, ctx.url, Date.now() - start);\n}"}]},{"id":32,"prompt":"我在 Advice 中用了一个实例属性 count 来统计请求数，但是多个请求之间数据串了，帮我看看","expected_output":"指出 Advice 默认是 Singleton，实例属性会被并发请求共享。请求级状态应使用 ctx.set()/ctx.get() 传递","files":[{"path":"app/modules/api/advice/RequestCountAdvice.ts","content":"import { Advice, IAdvice, AdviceContext } from 'egg/aop';\n\n@Advice()\nexport class RequestCountAdvice implements IAdvice {\n  private requestId = '';\n\n  async around(ctx: AdviceContext, next: () => Promise<any>): Promise<any> {\n    this.requestId = Math.random().toString(36).slice(2);\n    const result = await next();\n    return { ...result, requestId: this.requestId };\n  }\n}"}]},{"id":33,"prompt":"帮我看看这个 Controller 上的中间件执行顺序是什么样的","expected_output":"函数式和 AOP 分属两个独立执行阶段：先执行所有函数式中间件（类级别 countMw → 方法级别 timeMw），再执行所有 AOP 中间件（类级别 LogAdvice → 方法级别 AuthAdvice），最后执行 hello()。即 countMw → timeMw → LogAdvice → AuthAdvice → hello()，与装饰器书写顺序无关","files":[{"path":"app/modules/api/ApiController.ts","content":"import { HTTPController, HTTPMethod, HTTPMethodEnum, Middleware } from 'egg';\nimport { LogAdvice } from '../common/advice/LogAdvice.ts';\nimport { AuthAdvice } from '../common/advice/AuthAdvice.ts';\nimport { countMw } from './middleware/count.ts';\nimport { timeMw } from './middleware/time.ts';\n\n@HTTPController({ path: '/api' })\n@Middleware(countMw)\n@Middleware(LogAdvice)\nexport class ApiController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/hello' })\n  @Middleware(timeMw)\n  @Middleware(AuthAdvice)\n  async hello() {\n    return { message: 'hello' };\n  }\n}"}]}]},"isInternal":false,"tokens":5145,"sizeBytes":17747},{"name":"evals-egg-core.json","path":"packages/skills/eval/evals-egg-core.json","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/eval/evals-egg-core.json","title":"Eval Skill","category":"anthropic-skill","format":"json","content":"{\n  \"skill_name\": \"egg-core\",\n  \"description\": \"核心概念评测：覆盖 module、proto、inject、dynamic-inject、background-task、eventbus、aop\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"帮我新建一个 userModule，我是新项目从零开始\",\n      \"expected_output\": \"创建 package.json(eggModule.name)，驼峰命名，放在 app/ 下，给出完整目录结构\"\n    },\n    {\n      \"id\": 2,\n      \"prompt\": \"我们是老的 egg 项目，有大量 app/controller 和 app/service 代码，现在想引入 module 开发新功能，怎么组织目录？\",\n      \"expected_output\": \"保留原有 app/controller、app/service 不动，新增 module 放在 app/module/ 下\"\n    },\n    {\n      \"id\": 3,\n      \"prompt\": \"帮我写一个 module.yml 配置，本地和线上的数据库地址不同，怎么分环境管理\",\n      \"expected_output\": \"创建 module.yml 基础配置 + module.{env}.yml 环境配置，框架自动深度合并\"\n    },\n    {\n      \"id\": 4,\n      \"prompt\": \"我新加了一个模块，启动后发现没被加载，帮我看看什么问题\",\n      \"expected_output\": \"指出 eggModule.name 包含 - 不合法，应使用驼峰命名\",\n      \"files\": [\n        {\n          \"path\": \"app/user-module/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"user-module\\\",\\n  \\\"eggModule\\\": {\\n    \\\"name\\\": \\\"user-module\\\"\\n  }\\n}\"\n        },\n        {\n          \"path\": \"app/user-module/UserService.ts\",\n          \"content\": \"import { SingletonProto } from 'egg';\\n\\n@SingletonProto()\\nexport class UserService {\\n  async getUser() { return 'hello'; }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 5,\n      \"prompt\": \"帮我写一个 UserService，查询用户信息，需要被其他模块调用\",\n      \"expected_output\": \"使用 @SingletonProto({ accessLevel: AccessLevel.PUBLIC })，包含完整代码\"\n    },\n    {\n      \"id\": 6,\n      \"prompt\": \"帮我实现一个请求级别的 TraceContext，存 traceId 和 userId，多个 Service 之间共享\",\n      \"expected_output\": \"使用 @ContextProto()，每个请求独立实例，其他 Service 通过 @Inject 共享同一个实例\"\n    },\n    {\n      \"id\": 7,\n      \"prompt\": \"我的 SingletonProto 需要在启动时连接 Redis，连接成功后才能对外提供服务，怎么写？\",\n      \"expected_output\": \"使用 @LifecycleInit() 做异步初始化，@LifecycleDestroy() 关闭连接，给出完整代码\"\n    },\n    {\n      \"id\": 8,\n      \"prompt\": \"SingletonProto 能注入 ContextProto 吗？我需要在单例服务里获取当前请求的用户信息\",\n      \"expected_output\": \"可以注入，框架通过 lazy getter/Proxy 自动处理，每次访问从当前请求上下文动态解析\"\n    },\n    {\n      \"id\": 9,\n      \"prompt\": \"启动报错 EggPrototypeNotFound，帮我看看代码哪里有问题\",\n      \"expected_output\": \"指出 OrderService 跨模块注入了 UserService 但 UserService 的 accessLevel 是 PRIVATE，应改为 PUBLIC\",\n      \"files\": [\n        {\n          \"path\": \"app/userModule/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"userModule\\\",\\n  \\\"eggModule\\\": { \\\"name\\\": \\\"userModule\\\" }\\n}\"\n        },\n        {\n          \"path\": \"app/userModule/UserService.ts\",\n          \"content\": \"import { SingletonProto } from 'egg';\\n\\n@SingletonProto()\\nexport class UserService {\\n  async findUser(id: string) {\\n    return { id, name: 'test' };\\n  }\\n}\"\n        },\n        {\n          \"path\": \"app/orderModule/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"orderModule\\\",\\n  \\\"eggModule\\\": { \\\"name\\\": \\\"orderModule\\\" }\\n}\"\n        },\n        {\n          \"path\": \"app/orderModule/OrderService.ts\",\n          \"content\": \"import { SingletonProto, Inject } from 'egg';\\nimport { UserService } from '../userModule/UserService.ts';\\n\\n@SingletonProto()\\nexport class OrderService {\\n  @Inject()\\n  userService: UserService;\\n\\n  async createOrder(userId: string) {\\n    const user = await this.userService.findUser(userId);\\n    return { orderId: '1', user };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 10,\n      \"prompt\": \"注入的对象不对，我期望注入 FooService 但拿到的不是，帮我看看\",\n      \"expected_output\": \"指出 IFooService 是 interface 无运行时元信息，回退到属性名 fooHelper 匹配，应改用 class 类型或 @Inject({ name: 'fooService' })\",\n      \"files\": [\n        {\n          \"path\": \"app/bizModule/FooService.ts\",\n          \"content\": \"import { SingletonProto } from 'egg';\\n\\nexport interface IFooService {\\n  doWork(): Promise<string>;\\n}\\n\\n@SingletonProto()\\nexport class FooService implements IFooService {\\n  async doWork() { return 'done'; }\\n}\"\n        },\n        {\n          \"path\": \"app/bizModule/BarService.ts\",\n          \"content\": \"import { SingletonProto, Inject } from 'egg';\\nimport type { IFooService } from './FooService.ts';\\n\\n@SingletonProto()\\nexport class BarService {\\n  @Inject()\\n  fooHelper: IFooService;  // 期望注入 FooService\\n\\n  async run() {\\n    return this.fooHelper.doWork();\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 11,\n      \"prompt\": \"帮我写注入 config 和 logger 的代码，还要注入自定义的 bizLogger\",\n      \"expected_output\": \"@Inject() config: EggAppConfig + @Inject() logger: Logger + @Inject() bizLogger: Logger，给出完整代码\"\n    },\n    {\n      \"id\": 12,\n      \"prompt\": \"帮我把这个老的 egg Service 改成 module 写法\",\n      \"expected_output\": \"用 @SingletonProto 重写，@Inject() 注入依赖替代 this.ctx/this.app，去掉 extends Service\",\n      \"files\": [\n        {\n          \"path\": \"app/service/OrderService.ts\",\n          \"content\": \"import { Service } from 'egg';\\n\\nexport default class OrderService extends Service {\\n  async create(data: any) {\\n    const user = await this.ctx.service.user.find(data.userId);\\n    this.ctx.logger.info('creating order for %s', user.name);\\n    const result = await this.app.httpclient.request('https://api.example.com/orders', {\\n      method: 'POST',\\n      data,\\n    });\\n    return result.data;\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 13,\n      \"prompt\": \"启动报错说注入方式冲突了，帮我看看\",\n      \"expected_output\": \"指出构造函数注入和属性注入混用了，不能混用，需改为统一使用一种方式\",\n      \"files\": [\n        {\n          \"path\": \"app/bizModule/MixedService.ts\",\n          \"content\": \"import { SingletonProto, Inject } from 'egg';\\n\\n@SingletonProto()\\nexport class MixedService {\\n  @Inject()\\n  logger: Logger;\\n\\n  constructor(\\n    @Inject() readonly config: EggAppConfig,\\n  ) {}\\n\\n  async run() {\\n    this.logger.info('config: %j', this.config);\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 14,\n      \"prompt\": \"有个依赖在某些环境不一定存在，注入时不想让应用挂掉\",\n      \"expected_output\": \"使用 @Inject({ optional: true }) 或 @InjectOptional()，不存在时为 undefined\"\n    },\n    {\n      \"id\": 15,\n      \"prompt\": \"两个模块都有 DataService，注入时报冲突了，帮我看看怎么改\",\n      \"expected_output\": \"使用 @ModuleQualifier('moduleName') 指定从哪个模块注入\",\n      \"files\": [\n        {\n          \"path\": \"app/cacheModule/DataService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class DataService {\\n  async get(key: string) { return 'from cache'; }\\n}\"\n        },\n        {\n          \"path\": \"app/dbModule/DataService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class DataService {\\n  async get(key: string) { return 'from db'; }\\n}\"\n        },\n        {\n          \"path\": \"app/bizModule/BizService.ts\",\n          \"content\": \"import { SingletonProto, Inject } from 'egg';\\n\\n@SingletonProto()\\nexport class BizService {\\n  @Inject()\\n  dataService: DataService;  // 报冲突\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 16,\n      \"prompt\": \"帮我实现策略模式，有邮件、短信、钉钉三种通知方式，运行时根据配置选择\",\n      \"expected_output\": \"使用动态注入：抽象类 AbstractNotifier + 枚举 + QualifierImplDecoratorUtil 装饰器 + EggObjectFactory.getEggObject()，给出所有文件完整代码\"\n    },\n    {\n      \"id\": 17,\n      \"prompt\": \"动态注入不生效，getEggObject 报找不到实现，帮我看看代码\",\n      \"expected_output\": \"指出 EmailNotifier 缺少 @SingletonProto() 装饰器，实现类必须同时有 Proto 装饰器和自定义装饰器\",\n      \"files\": [\n        {\n          \"path\": \"app/notifyModule/AbstractNotifier.ts\",\n          \"content\": \"export abstract class AbstractNotifier {\\n  abstract send(msg: string): Promise<void>;\\n}\"\n        },\n        {\n          \"path\": \"app/notifyModule/NotifyType.ts\",\n          \"content\": \"export enum NotifyType {\\n  EMAIL = 'EMAIL',\\n  SMS = 'SMS',\\n}\"\n        },\n        {\n          \"path\": \"app/notifyModule/decorator/Notify.ts\",\n          \"content\": \"import { ImplDecorator, QualifierImplDecoratorUtil } from 'egg';\\nimport { NotifyType } from '../NotifyType.ts';\\nimport { AbstractNotifier } from '../AbstractNotifier.ts';\\n\\nexport const NOTIFY_ATTRIBUTE = Symbol('NOTIFY_ATTRIBUTE');\\n\\nexport const Notify: ImplDecorator<AbstractNotifier, typeof NotifyType> =\\n  QualifierImplDecoratorUtil.generatorDecorator(AbstractNotifier, NOTIFY_ATTRIBUTE);\"\n        },\n        {\n          \"path\": \"app/notifyModule/impl/EmailNotifier.ts\",\n          \"content\": \"import { Notify } from '../decorator/Notify.ts';\\nimport { NotifyType } from '../NotifyType.ts';\\nimport { AbstractNotifier } from '../AbstractNotifier.ts';\\n\\n@Notify(NotifyType.EMAIL)\\nexport class EmailNotifier extends AbstractNotifier {\\n  async send(msg: string) {\\n    console.log('send email:', msg);\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 18,\n      \"prompt\": \"我想读取另一个模块的 moduleConfig，不是当前模块的\",\n      \"expected_output\": \"使用 @ConfigSourceQualifier('otherModule')，给出完整代码\"\n    },\n    {\n      \"id\": 19,\n      \"prompt\": \"请求返回后还要做一些事情，比如记日志、发通知，怎么实现\",\n      \"expected_output\": \"使用 BackgroundTaskHelper，@Inject() 注入，调用 run() 方法，从 egg 导入\"\n    },\n    {\n      \"id\": 20,\n      \"prompt\": \"用 BackgroundTaskHelper 在请求返回后异步上报埋点数据，帮我写个 MetricsService\",\n      \"expected_output\": \"@SingletonProto + @Inject() backgroundTaskHelper: BackgroundTaskHelper + run(async () => { ... })，从 egg 导入\"\n    },\n    {\n      \"id\": 21,\n      \"prompt\": \"用了 setTimeout 做后台任务，偶尔报错，帮我看看什么问题\",\n      \"expected_output\": \"指出 setTimeout 执行时上下文已释放，应改用 BackgroundTaskHelper.run()\",\n      \"files\": [\n        {\n          \"path\": \"app/orderModule/OrderService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel, Inject } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class OrderService {\\n  @Inject()\\n  private logger: any;\\n\\n  async createOrder(data: any) {\\n    const order = { id: Date.now(), ...data };\\n    // 请求返回后异步记录日志\\n    setTimeout(async () => {\\n      this.logger.info('Order created:', order.id);\\n      await this.sendNotification(order);\\n    }, 0);\\n    return order;\\n  }\\n\\n  private async sendNotification(order: any) {\\n    // 发送通知\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 22,\n      \"prompt\": \"后台任务默认超时是多少？我有个任务比较慢，怎么改成更长的时间\",\n      \"expected_output\": \"默认 5 秒，可通过 backgroundTaskHelper.timeout 按请求设置，或在 config.default.ts 中 backgroundTask.timeout 全局配置\"\n    },\n    {\n      \"id\": 23,\n      \"prompt\": \"帮我写一个订单创建接口，创建成功后异步发送通知邮件，不影响接口响应速度\",\n      \"expected_output\": \"OrderService 中 @Inject() backgroundTaskHelper，在 createOrder 方法中调用 run() 异步发送邮件\"\n    },\n    {\n      \"id\": 24,\n      \"prompt\": \"帮我把这个用 setTimeout 的后台逻辑改成正确的写法\",\n      \"expected_output\": \"将 setTimeout 替换为 backgroundTaskHelper.run()，注入 BackgroundTaskHelper\",\n      \"files\": [\n        {\n          \"path\": \"app/cacheModule/CacheService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel, Inject } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class CacheService {\\n  @Inject()\\n  private redisService: any;\\n\\n  async getData(key: string) {\\n    const data = await this.redisService.get(key);\\n    if (!data) {\\n      const fresh = await this.fetchFromDB(key);\\n      // 异步回写缓存，不阻塞响应\\n      setTimeout(async () => {\\n        await this.redisService.set(key, fresh, 3600);\\n      }, 0);\\n      return fresh;\\n    }\\n    return data;\\n  }\\n\\n  private async fetchFromDB(key: string) {\\n    return { key, value: 'from-db' };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 25,\n      \"prompt\": \"后台任务里抛了异常，但接口没报错也没看到日志，怎么回事\",\n      \"expected_output\": \"BackgroundTaskHelper 内部捕获所有异常并通过 logger.error 记录，日志前缀为 [BackgroundTaskHelper]，检查日志配置是否正确\",\n      \"files\": [\n        {\n          \"path\": \"app/reportModule/ReportService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel, Inject, BackgroundTaskHelper } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class ReportService {\\n  @Inject()\\n  private backgroundTaskHelper: BackgroundTaskHelper;\\n\\n  async generateReport(data: any) {\\n    this.backgroundTaskHelper.run(async () => {\\n      // 这里可能会抛异常\\n      const result = await this.callExternalAPI(data);\\n      await this.saveReport(result);\\n    });\\n    return { status: 'processing' };\\n  }\\n\\n  private async callExternalAPI(data: any) {\\n    throw new Error('API timeout');\\n  }\\n\\n  private async saveReport(result: any) {}\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 26,\n      \"prompt\": \"我有个后台任务可能要跑 30 秒，怎么配置超时不被框架提前中断\",\n      \"expected_output\": \"设置 backgroundTaskHelper.timeout = 30000 或全局 config backgroundTask.timeout: 30000\"\n    },\n    {\n      \"id\": 27,\n      \"prompt\": \"订单创建后要发通知、记日志、更新统计，但不想耦合在一起，怎么做\",\n      \"expected_output\": \"使用 EventBus 解耦：声明 Events 接口 + emit 触发事件 + 多个 @Event handler 分别处理通知、日志、统计\"\n    },\n    {\n      \"id\": 28,\n      \"prompt\": \"用 EventBus 实现订单事件，要求类型安全，帮我写 Events 声明和 handler\",\n      \"expected_output\": \"declare module 'egg' { interface Events { ... } } + @Event handler + 从 egg 导入 EventBus/Event，事件声明文件开头有 import 'egg'\"\n    },\n    {\n      \"id\": 29,\n      \"prompt\": \"EventBus 和 BackgroundTaskHelper 什么区别，我该用哪个\",\n      \"expected_output\": \"对比耦合度（强耦合 vs 解耦）、上下文（共享 vs 独立）、扩展性（改触发者 vs 加 handler），给出选型建议\"\n    },\n    {\n      \"id\": 30,\n      \"prompt\": \"EventBus 的 handler 没有被触发，帮我看看\",\n      \"expected_output\": \"指出 handler 文件不在 module 目录中，框架无法扫描到，应移到模块目录下\",\n      \"files\": [\n        {\n          \"path\": \"app/orderModule/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"orderModule\\\",\\n  \\\"eggModule\\\": { \\\"name\\\": \\\"orderModule\\\" }\\n}\"\n        },\n        {\n          \"path\": \"app/orderModule/OrderService.ts\",\n          \"content\": \"import { SingletonProto, Inject, EventBus } from 'egg';\\n\\n@SingletonProto()\\nexport class OrderService {\\n  @Inject()\\n  private eventBus: EventBus;\\n\\n  async createOrder(userId: string) {\\n    this.eventBus.emit('orderCreated', '123', userId);\\n    return { orderId: '123' };\\n  }\\n}\"\n        },\n        {\n          \"path\": \"app/handler/OrderNotificationHandler.ts\",\n          \"content\": \"import { Event } from 'egg';\\n\\n@Event('orderCreated')\\nexport class OrderNotificationHandler {\\n  async handle(orderId: string, userId: string): Promise<void> {\\n    console.log('notify:', orderId, userId);\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 31,\n      \"prompt\": \"帮我用 EventBus 实现用户注册后异步发送欢迎邮件\",\n      \"expected_output\": \"完整代码：Events 类型声明（import 'egg' + declare module）+ 注册 Service 中 emit + @Event handler 发送邮件\"\n    },\n    {\n      \"id\": 32,\n      \"prompt\": \"帮我把这个 BackgroundTaskHelper 的写法改成 EventBus 解耦\",\n      \"expected_output\": \"拆分为 EventBus.emit() + 独立 @Event handler 类，去掉 BackgroundTaskHelper\",\n      \"files\": [\n        {\n          \"path\": \"app/orderModule/OrderService.ts\",\n          \"content\": \"import { SingletonProto, AccessLevel, Inject, BackgroundTaskHelper } from 'egg';\\n\\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\\nexport class OrderService {\\n  @Inject()\\n  private backgroundTaskHelper: BackgroundTaskHelper;\\n\\n  @Inject()\\n  private notifyService: any;\\n\\n  @Inject()\\n  private statsService: any;\\n\\n  async createOrder(data: any) {\\n    const order = { id: Date.now(), ...data };\\n    this.backgroundTaskHelper.run(async () => {\\n      await this.notifyService.send(order);\\n      await this.statsService.record(order);\\n    });\\n    return order;\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 33,\n      \"prompt\": \"EventBus 的 cork/uncork 怎么用，我想批量操作完成后再统一触发事件\",\n      \"expected_output\": \"注入 ContextEventBus，cork() 开始缓冲 → emit 多个事件 → uncork() 释放，支持嵌套调用\"\n    },\n    {\n      \"id\": 34,\n      \"prompt\": \"所有 Service 的方法调用都要记录耗时日志，不想在每个方法里写\",\n      \"expected_output\": \"使用 AOP @Crosscut + PointcutType.NAME 正则匹配 Service，Advice 中 beforeCall 记录开始时间，afterFinally 计算耗时并打日志\"\n    },\n    {\n      \"id\": 35,\n      \"prompt\": \"用 @Pointcut 给 OrderService.createOrder 加事务包裹\",\n      \"expected_output\": \"@Advice 实现 around hook 管理事务（beginTransaction/commit/rollback）+ @Pointcut(TxAdvice) 装饰目标方法，从 egg/aop 导入\"\n    },\n    {\n      \"id\": 36,\n      \"prompt\": \"Pointcut 和 Crosscut 有什么区别，怎么选\",\n      \"expected_output\": \"Pointcut 在目标方法上声明，精确切入单个方法；Crosscut 在 Advice 类上声明匹配规则，批量切入多个类/方法\"\n    },\n    {\n      \"id\": 37,\n      \"prompt\": \"AOP 的 Advice 不生效，帮我看看\",\n      \"expected_output\": \"指出缺少 @Advice() 装饰器，@Crosscut 和 @Advice() 必须同时使用\",\n      \"files\": [\n        {\n          \"path\": \"app/logModule/package.json\",\n          \"content\": \"{\\n  \\\"name\\\": \\\"logModule\\\",\\n  \\\"eggModule\\\": { \\\"name\\\": \\\"logModule\\\" }\\n}\"\n        },\n        {\n          \"path\": \"app/logModule/OperationLogAdvice.ts\",\n          \"content\": \"import { Crosscut, IAdvice, AdviceContext, PointcutType } from 'egg/aop';\\nimport { Inject, Logger } from 'egg';\\n\\n@Crosscut({\\n  type: PointcutType.NAME,\\n  className: /.*Service$/i,\\n  methodName: /^(create|update|delete)/,\\n})\\nexport class OperationLogAdvice implements IAdvice {\\n  @Inject()\\n  private logger: Logger;\\n\\n  async afterFinally(ctx: AdviceContext): Promise<void> {\\n    this.logger.info('called %s', String(ctx.method));\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 38,\n      \"prompt\": \"帮我用 AOP 实现统一的异常捕获和错误日志\",\n      \"expected_output\": \"Advice 实现 afterThrow hook 记录异常信息，使用 @Crosscut 或 @Pointcut 应用到目标方法，从 egg/aop 导入\"\n    },\n    {\n      \"id\": 39,\n      \"prompt\": \"adviceParams 怎么用，同一个 Advice 不同方法需要不同参数\",\n      \"expected_output\": \"@Pointcut 第二个参数传 { adviceParams: {...} }，Advice 中通过 ctx.adviceParams 获取\"\n    },\n    {\n      \"id\": 40,\n      \"prompt\": \"同一个方法上有 Crosscut 和 Pointcut 两个 Advice，执行顺序是什么，能控制吗\",\n      \"expected_output\": \"Crosscut 默认 order 100 先执行，Pointcut 默认 order 1000 后执行，可通过 order 参数调整顺序\"\n    },\n    {\n      \"id\": 41,\n      \"prompt\": \"帮我用 Crosscut 的 CUSTOM 模式，对所有 Repository 类的非 constructor 方法加监控\",\n      \"expected_output\": \"使用 PointcutType.CUSTOM + callback 函数匹配 Repository 类并排除 constructor，从 egg/aop 导入\"\n    }\n  ]\n}\n","frontmatter":{"skill_name":"egg-core","description":"核心概念评测：覆盖 module、proto、inject、dynamic-inject、background-task、eventbus、aop","evals":[{"id":1,"prompt":"帮我新建一个 userModule，我是新项目从零开始","expected_output":"创建 package.json(eggModule.name)，驼峰命名，放在 app/ 下，给出完整目录结构"},{"id":2,"prompt":"我们是老的 egg 项目，有大量 app/controller 和 app/service 代码，现在想引入 module 开发新功能，怎么组织目录？","expected_output":"保留原有 app/controller、app/service 不动，新增 module 放在 app/module/ 下"},{"id":3,"prompt":"帮我写一个 module.yml 配置，本地和线上的数据库地址不同，怎么分环境管理","expected_output":"创建 module.yml 基础配置 + module.{env}.yml 环境配置，框架自动深度合并"},{"id":4,"prompt":"我新加了一个模块，启动后发现没被加载，帮我看看什么问题","expected_output":"指出 eggModule.name 包含 - 不合法，应使用驼峰命名","files":[{"path":"app/user-module/package.json","content":"{\n  \"name\": \"user-module\",\n  \"eggModule\": {\n    \"name\": \"user-module\"\n  }\n}"},{"path":"app/user-module/UserService.ts","content":"import { SingletonProto } from 'egg';\n\n@SingletonProto()\nexport class UserService {\n  async getUser() { return 'hello'; }\n}"}]},{"id":5,"prompt":"帮我写一个 UserService，查询用户信息，需要被其他模块调用","expected_output":"使用 @SingletonProto({ accessLevel: AccessLevel.PUBLIC })，包含完整代码"},{"id":6,"prompt":"帮我实现一个请求级别的 TraceContext，存 traceId 和 userId，多个 Service 之间共享","expected_output":"使用 @ContextProto()，每个请求独立实例，其他 Service 通过 @Inject 共享同一个实例"},{"id":7,"prompt":"我的 SingletonProto 需要在启动时连接 Redis，连接成功后才能对外提供服务，怎么写？","expected_output":"使用 @LifecycleInit() 做异步初始化，@LifecycleDestroy() 关闭连接，给出完整代码"},{"id":8,"prompt":"SingletonProto 能注入 ContextProto 吗？我需要在单例服务里获取当前请求的用户信息","expected_output":"可以注入，框架通过 lazy getter/Proxy 自动处理，每次访问从当前请求上下文动态解析"},{"id":9,"prompt":"启动报错 EggPrototypeNotFound，帮我看看代码哪里有问题","expected_output":"指出 OrderService 跨模块注入了 UserService 但 UserService 的 accessLevel 是 PRIVATE，应改为 PUBLIC","files":[{"path":"app/userModule/package.json","content":"{\n  \"name\": \"userModule\",\n  \"eggModule\": { \"name\": \"userModule\" }\n}"},{"path":"app/userModule/UserService.ts","content":"import { SingletonProto } from 'egg';\n\n@SingletonProto()\nexport class UserService {\n  async findUser(id: string) {\n    return { id, name: 'test' };\n  }\n}"},{"path":"app/orderModule/package.json","content":"{\n  \"name\": \"orderModule\",\n  \"eggModule\": { \"name\": \"orderModule\" }\n}"},{"path":"app/orderModule/OrderService.ts","content":"import { SingletonProto, Inject } from 'egg';\nimport { UserService } from '../userModule/UserService.ts';\n\n@SingletonProto()\nexport class OrderService {\n  @Inject()\n  userService: UserService;\n\n  async createOrder(userId: string) {\n    const user = await this.userService.findUser(userId);\n    return { orderId: '1', user };\n  }\n}"}]},{"id":10,"prompt":"注入的对象不对，我期望注入 FooService 但拿到的不是，帮我看看","expected_output":"指出 IFooService 是 interface 无运行时元信息，回退到属性名 fooHelper 匹配，应改用 class 类型或 @Inject({ name: 'fooService' })","files":[{"path":"app/bizModule/FooService.ts","content":"import { SingletonProto } from 'egg';\n\nexport interface IFooService {\n  doWork(): Promise<string>;\n}\n\n@SingletonProto()\nexport class FooService implements IFooService {\n  async doWork() { return 'done'; }\n}"},{"path":"app/bizModule/BarService.ts","content":"import { SingletonProto, Inject } from 'egg';\nimport type { IFooService } from './FooService.ts';\n\n@SingletonProto()\nexport class BarService {\n  @Inject()\n  fooHelper: IFooService;  // 期望注入 FooService\n\n  async run() {\n    return this.fooHelper.doWork();\n  }\n}"}]},{"id":11,"prompt":"帮我写注入 config 和 logger 的代码，还要注入自定义的 bizLogger","expected_output":"@Inject() config: EggAppConfig + @Inject() logger: Logger + @Inject() bizLogger: Logger，给出完整代码"},{"id":12,"prompt":"帮我把这个老的 egg Service 改成 module 写法","expected_output":"用 @SingletonProto 重写，@Inject() 注入依赖替代 this.ctx/this.app，去掉 extends Service","files":[{"path":"app/service/OrderService.ts","content":"import { Service } from 'egg';\n\nexport default class OrderService extends Service {\n  async create(data: any) {\n    const user = await this.ctx.service.user.find(data.userId);\n    this.ctx.logger.info('creating order for %s', user.name);\n    const result = await this.app.httpclient.request('https://api.example.com/orders', {\n      method: 'POST',\n      data,\n    });\n    return result.data;\n  }\n}"}]},{"id":13,"prompt":"启动报错说注入方式冲突了，帮我看看","expected_output":"指出构造函数注入和属性注入混用了，不能混用，需改为统一使用一种方式","files":[{"path":"app/bizModule/MixedService.ts","content":"import { SingletonProto, Inject } from 'egg';\n\n@SingletonProto()\nexport class MixedService {\n  @Inject()\n  logger: Logger;\n\n  constructor(\n    @Inject() readonly config: EggAppConfig,\n  ) {}\n\n  async run() {\n    this.logger.info('config: %j', this.config);\n  }\n}"}]},{"id":14,"prompt":"有个依赖在某些环境不一定存在，注入时不想让应用挂掉","expected_output":"使用 @Inject({ optional: true }) 或 @InjectOptional()，不存在时为 undefined"},{"id":15,"prompt":"两个模块都有 DataService，注入时报冲突了，帮我看看怎么改","expected_output":"使用 @ModuleQualifier('moduleName') 指定从哪个模块注入","files":[{"path":"app/cacheModule/DataService.ts","content":"import { SingletonProto, AccessLevel } from 'egg';\n\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class DataService {\n  async get(key: string) { return 'from cache'; }\n}"},{"path":"app/dbModule/DataService.ts","content":"import { SingletonProto, AccessLevel } from 'egg';\n\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class DataService {\n  async get(key: string) { return 'from db'; }\n}"},{"path":"app/bizModule/BizService.ts","content":"import { SingletonProto, Inject } from 'egg';\n\n@SingletonProto()\nexport class BizService {\n  @Inject()\n  dataService: DataService;  // 报冲突\n}"}]},{"id":16,"prompt":"帮我实现策略模式，有邮件、短信、钉钉三种通知方式，运行时根据配置选择","expected_output":"使用动态注入：抽象类 AbstractNotifier + 枚举 + QualifierImplDecoratorUtil 装饰器 + EggObjectFactory.getEggObject()，给出所有文件完整代码"},{"id":17,"prompt":"动态注入不生效，getEggObject 报找不到实现，帮我看看代码","expected_output":"指出 EmailNotifier 缺少 @SingletonProto() 装饰器，实现类必须同时有 Proto 装饰器和自定义装饰器","files":[{"path":"app/notifyModule/AbstractNotifier.ts","content":"export abstract class AbstractNotifier {\n  abstract send(msg: string): Promise<void>;\n}"},{"path":"app/notifyModule/NotifyType.ts","content":"export enum NotifyType {\n  EMAIL = 'EMAIL',\n  SMS = 'SMS',\n}"},{"path":"app/notifyModule/decorator/Notify.ts","content":"import { ImplDecorator, QualifierImplDecoratorUtil } from 'egg';\nimport { NotifyType } from '../NotifyType.ts';\nimport { AbstractNotifier } from '../AbstractNotifier.ts';\n\nexport const NOTIFY_ATTRIBUTE = Symbol('NOTIFY_ATTRIBUTE');\n\nexport const Notify: ImplDecorator<AbstractNotifier, typeof NotifyType> =\n  QualifierImplDecoratorUtil.generatorDecorator(AbstractNotifier, NOTIFY_ATTRIBUTE);"},{"path":"app/notifyModule/impl/EmailNotifier.ts","content":"import { Notify } from '../decorator/Notify.ts';\nimport { NotifyType } from '../NotifyType.ts';\nimport { AbstractNotifier } from '../AbstractNotifier.ts';\n\n@Notify(NotifyType.EMAIL)\nexport class EmailNotifier extends AbstractNotifier {\n  async send(msg: string) {\n    console.log('send email:', msg);\n  }\n}"}]},{"id":18,"prompt":"我想读取另一个模块的 moduleConfig，不是当前模块的","expected_output":"使用 @ConfigSourceQualifier('otherModule')，给出完整代码"},{"id":19,"prompt":"请求返回后还要做一些事情，比如记日志、发通知，怎么实现","expected_output":"使用 BackgroundTaskHelper，@Inject() 注入，调用 run() 方法，从 egg 导入"},{"id":20,"prompt":"用 BackgroundTaskHelper 在请求返回后异步上报埋点数据，帮我写个 MetricsService","expected_output":"@SingletonProto + @Inject() backgroundTaskHelper: BackgroundTaskHelper + run(async () => { ... })，从 egg 导入"},{"id":21,"prompt":"用了 setTimeout 做后台任务，偶尔报错，帮我看看什么问题","expected_output":"指出 setTimeout 执行时上下文已释放，应改用 BackgroundTaskHelper.run()","files":[{"path":"app/orderModule/OrderService.ts","content":"import { SingletonProto, AccessLevel, Inject } from 'egg';\n\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class OrderService {\n  @Inject()\n  private logger: any;\n\n  async createOrder(data: any) {\n    const order = { id: Date.now(), ...data };\n    // 请求返回后异步记录日志\n    setTimeout(async () => {\n      this.logger.info('Order created:', order.id);\n      await this.sendNotification(order);\n    }, 0);\n    return order;\n  }\n\n  private async sendNotification(order: any) {\n    // 发送通知\n  }\n}"}]},{"id":22,"prompt":"后台任务默认超时是多少？我有个任务比较慢，怎么改成更长的时间","expected_output":"默认 5 秒，可通过 backgroundTaskHelper.timeout 按请求设置，或在 config.default.ts 中 backgroundTask.timeout 全局配置"},{"id":23,"prompt":"帮我写一个订单创建接口，创建成功后异步发送通知邮件，不影响接口响应速度","expected_output":"OrderService 中 @Inject() backgroundTaskHelper，在 createOrder 方法中调用 run() 异步发送邮件"},{"id":24,"prompt":"帮我把这个用 setTimeout 的后台逻辑改成正确的写法","expected_output":"将 setTimeout 替换为 backgroundTaskHelper.run()，注入 BackgroundTaskHelper","files":[{"path":"app/cacheModule/CacheService.ts","content":"import { SingletonProto, AccessLevel, Inject } from 'egg';\n\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class CacheService {\n  @Inject()\n  private redisService: any;\n\n  async getData(key: string) {\n    const data = await this.redisService.get(key);\n    if (!data) {\n      const fresh = await this.fetchFromDB(key);\n      // 异步回写缓存，不阻塞响应\n      setTimeout(async () => {\n        await this.redisService.set(key, fresh, 3600);\n      }, 0);\n      return fresh;\n    }\n    return data;\n  }\n\n  private async fetchFromDB(key: string) {\n    return { key, value: 'from-db' };\n  }\n}"}]},{"id":25,"prompt":"后台任务里抛了异常，但接口没报错也没看到日志，怎么回事","expected_output":"BackgroundTaskHelper 内部捕获所有异常并通过 logger.error 记录，日志前缀为 [BackgroundTaskHelper]，检查日志配置是否正确","files":[{"path":"app/reportModule/ReportService.ts","content":"import { SingletonProto, AccessLevel, Inject, BackgroundTaskHelper } from 'egg';\n\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class ReportService {\n  @Inject()\n  private backgroundTaskHelper: BackgroundTaskHelper;\n\n  async generateReport(data: any) {\n    this.backgroundTaskHelper.run(async () => {\n      // 这里可能会抛异常\n      const result = await this.callExternalAPI(data);\n      await this.saveReport(result);\n    });\n    return { status: 'processing' };\n  }\n\n  private async callExternalAPI(data: any) {\n    throw new Error('API timeout');\n  }\n\n  private async saveReport(result: any) {}\n}"}]},{"id":26,"prompt":"我有个后台任务可能要跑 30 秒，怎么配置超时不被框架提前中断","expected_output":"设置 backgroundTaskHelper.timeout = 30000 或全局 config backgroundTask.timeout: 30000"},{"id":27,"prompt":"订单创建后要发通知、记日志、更新统计，但不想耦合在一起，怎么做","expected_output":"使用 EventBus 解耦：声明 Events 接口 + emit 触发事件 + 多个 @Event handler 分别处理通知、日志、统计"},{"id":28,"prompt":"用 EventBus 实现订单事件，要求类型安全，帮我写 Events 声明和 handler","expected_output":"declare module 'egg' { interface Events { ... } } + @Event handler + 从 egg 导入 EventBus/Event，事件声明文件开头有 import 'egg'"},{"id":29,"prompt":"EventBus 和 BackgroundTaskHelper 什么区别，我该用哪个","expected_output":"对比耦合度（强耦合 vs 解耦）、上下文（共享 vs 独立）、扩展性（改触发者 vs 加 handler），给出选型建议"},{"id":30,"prompt":"EventBus 的 handler 没有被触发，帮我看看","expected_output":"指出 handler 文件不在 module 目录中，框架无法扫描到，应移到模块目录下","files":[{"path":"app/orderModule/package.json","content":"{\n  \"name\": \"orderModule\",\n  \"eggModule\": { \"name\": \"orderModule\" }\n}"},{"path":"app/orderModule/OrderService.ts","content":"import { SingletonProto, Inject, EventBus } from 'egg';\n\n@SingletonProto()\nexport class OrderService {\n  @Inject()\n  private eventBus: EventBus;\n\n  async createOrder(userId: string) {\n    this.eventBus.emit('orderCreated', '123', userId);\n    return { orderId: '123' };\n  }\n}"},{"path":"app/handler/OrderNotificationHandler.ts","content":"import { Event } from 'egg';\n\n@Event('orderCreated')\nexport class OrderNotificationHandler {\n  async handle(orderId: string, userId: string): Promise<void> {\n    console.log('notify:', orderId, userId);\n  }\n}"}]},{"id":31,"prompt":"帮我用 EventBus 实现用户注册后异步发送欢迎邮件","expected_output":"完整代码：Events 类型声明（import 'egg' + declare module）+ 注册 Service 中 emit + @Event handler 发送邮件"},{"id":32,"prompt":"帮我把这个 BackgroundTaskHelper 的写法改成 EventBus 解耦","expected_output":"拆分为 EventBus.emit() + 独立 @Event handler 类，去掉 BackgroundTaskHelper","files":[{"path":"app/orderModule/OrderService.ts","content":"import { SingletonProto, AccessLevel, Inject, BackgroundTaskHelper } from 'egg';\n\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class OrderService {\n  @Inject()\n  private backgroundTaskHelper: BackgroundTaskHelper;\n\n  @Inject()\n  private notifyService: any;\n\n  @Inject()\n  private statsService: any;\n\n  async createOrder(data: any) {\n    const order = { id: Date.now(), ...data };\n    this.backgroundTaskHelper.run(async () => {\n      await this.notifyService.send(order);\n      await this.statsService.record(order);\n    });\n    return order;\n  }\n}"}]},{"id":33,"prompt":"EventBus 的 cork/uncork 怎么用，我想批量操作完成后再统一触发事件","expected_output":"注入 ContextEventBus，cork() 开始缓冲 → emit 多个事件 → uncork() 释放，支持嵌套调用"},{"id":34,"prompt":"所有 Service 的方法调用都要记录耗时日志，不想在每个方法里写","expected_output":"使用 AOP @Crosscut + PointcutType.NAME 正则匹配 Service，Advice 中 beforeCall 记录开始时间，afterFinally 计算耗时并打日志"},{"id":35,"prompt":"用 @Pointcut 给 OrderService.createOrder 加事务包裹","expected_output":"@Advice 实现 around hook 管理事务（beginTransaction/commit/rollback）+ @Pointcut(TxAdvice) 装饰目标方法，从 egg/aop 导入"},{"id":36,"prompt":"Pointcut 和 Crosscut 有什么区别，怎么选","expected_output":"Pointcut 在目标方法上声明，精确切入单个方法；Crosscut 在 Advice 类上声明匹配规则，批量切入多个类/方法"},{"id":37,"prompt":"AOP 的 Advice 不生效，帮我看看","expected_output":"指出缺少 @Advice() 装饰器，@Crosscut 和 @Advice() 必须同时使用","files":[{"path":"app/logModule/package.json","content":"{\n  \"name\": \"logModule\",\n  \"eggModule\": { \"name\": \"logModule\" }\n}"},{"path":"app/logModule/OperationLogAdvice.ts","content":"import { Crosscut, IAdvice, AdviceContext, PointcutType } from 'egg/aop';\nimport { Inject, Logger } from 'egg';\n\n@Crosscut({\n  type: PointcutType.NAME,\n  className: /.*Service$/i,\n  methodName: /^(create|update|delete)/,\n})\nexport class OperationLogAdvice implements IAdvice {\n  @Inject()\n  private logger: Logger;\n\n  async afterFinally(ctx: AdviceContext): Promise<void> {\n    this.logger.info('called %s', String(ctx.method));\n  }\n}"}]},{"id":38,"prompt":"帮我用 AOP 实现统一的异常捕获和错误日志","expected_output":"Advice 实现 afterThrow hook 记录异常信息，使用 @Crosscut 或 @Pointcut 应用到目标方法，从 egg/aop 导入"},{"id":39,"prompt":"adviceParams 怎么用，同一个 Advice 不同方法需要不同参数","expected_output":"@Pointcut 第二个参数传 { adviceParams: {...} }，Advice 中通过 ctx.adviceParams 获取"},{"id":40,"prompt":"同一个方法上有 Crosscut 和 Pointcut 两个 Advice，执行顺序是什么，能控制吗","expected_output":"Crosscut 默认 order 100 先执行，Pointcut 默认 order 1000 后执行，可通过 order 参数调整顺序"},{"id":41,"prompt":"帮我用 Crosscut 的 CUSTOM 模式，对所有 Repository 类的非 constructor 方法加监控","expected_output":"使用 PointcutType.CUSTOM + callback 函数匹配 Repository 类并排除 constructor，从 egg/aop 导入"}]},"isInternal":false,"tokens":5872,"sizeBytes":20785},{"name":"evals-egg-unittest.json","path":"packages/skills/eval/evals-egg-unittest.json","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/eval/evals-egg-unittest.json","title":"Eval Skill","category":"anthropic-skill","format":"json","content":"{\n  \"skill_name\": \"egg-unittest\",\n  \"description\": \"单元测试评测：覆盖 http-test、service-test、mock、background-task-test、eventbus-test\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"帮我给这个 UserController 写个单元测试，要测 GET 和 POST 接口\",\n      \"expected_output\": \"import { app } from '@eggjs/mock/bootstrap'，app.httpRequest() 测 GET 和 POST，POST 前调用 app.mockCsrf()，return 或 await httpRequest\",\n      \"files\": [\n        {\n          \"path\": \"app/modules/user/UserController.ts\",\n          \"content\": \"import { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPParam, HTTPBody, Inject } from 'egg';\\nimport { UserService } from './UserService.ts';\\n\\n@HTTPController({ path: '/api/users' })\\nexport class UserController {\\n  @Inject()\\n  userService: UserService;\\n\\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/:id' })\\n  async show(@HTTPParam() id: string) {\\n    return await this.userService.getById(id);\\n  }\\n\\n  @HTTPMethod({ method: HTTPMethodEnum.POST, path: '/' })\\n  async create(@HTTPBody() body: { name: string; email: string }) {\\n    return await this.userService.create(body);\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 2,\n      \"prompt\": \"POST 接口测试报 403 错误，帮我看看\",\n      \"expected_output\": \"指出需要调用 app.mockCsrf() 跳过 CSRF 校验\",\n      \"files\": [\n        {\n          \"path\": \"test/controller/user.test.ts\",\n          \"content\": \"import { app } from '@eggjs/mock/bootstrap';\\n\\ndescribe('UserController', () => {\\n  it('should create user', () => {\\n    return app.httpRequest()\\n      .post('/api/users')\\n      .send({ name: 'test', email: 'test@example.com' })\\n      .expect(200);\\n  });\\n});\"\n        }\n      ]\n    },\n    {\n      \"id\": 3,\n      \"prompt\": \"httpRequest 的 expect 有哪些用法\",\n      \"expected_output\": \"expect(status)、expect(body)、expect(status, body) 合并、expect('header', value)、expect(/regex/)、expect([200, 302]) 多状态码、expect(fn) 自定义断言\"\n    },\n    {\n      \"id\": 4,\n      \"prompt\": \"测试写了但永远通过，不管接口返回啥都不报错\",\n      \"expected_output\": \"httpRequest 必须 return 或 await，否则断言不会执行\",\n      \"files\": [\n        {\n          \"path\": \"test/controller/home.test.ts\",\n          \"content\": \"import { app } from '@eggjs/mock/bootstrap';\\n\\ndescribe('HomeController', () => {\\n  it('should return 200', () => {\\n    app.httpRequest()\\n      .get('/')\\n      .expect(200)\\n      .expect('hello');\\n  });\\n});\"\n        }\n      ]\n    },\n    {\n      \"id\": 5,\n      \"prompt\": \"帮我写一个完整的 CRUD 接口测试\",\n      \"expected_output\": \"GET/POST/PUT/DELETE 完整测试，POST/PUT/DELETE 都要 mockCsrf，import 从 @eggjs/mock/bootstrap\"\n    },\n    {\n      \"id\": 6,\n      \"prompt\": \"基于这个 OrderService 帮我写完整的单元测试\",\n      \"expected_output\": \"app.getEggObject(OrderService) 获取实例 + mm mock PaymentService 依赖 + 断言返回值\",\n      \"files\": [\n        {\n          \"path\": \"app/modules/order/OrderService.ts\",\n          \"content\": \"import { SingletonProto, Inject } from 'egg';\\nimport { PaymentService } from '../payment/PaymentService.ts';\\n\\n@SingletonProto()\\nexport class OrderService {\\n  @Inject()\\n  paymentService: PaymentService;\\n\\n  async create(data: { productId: string; amount: number }) {\\n    const tx = await this.paymentService.charge(data.amount);\\n    return { orderId: 'order-1', transactionId: tx.id };\\n  }\\n}\"\n        }\n      ]\n    },\n    {\n      \"id\": 7,\n      \"prompt\": \"ContextProto 的 Service 怎么在测试里获取实例\",\n      \"expected_output\": \"app.getEggObject(ContextProtoClass) 可以直接获取，也可以在 app.mockModuleContextScope 回调内用 ctx.getEggObject 获取（后者提供上下文生命周期管理，退出时自动销毁）\"\n    },\n    {\n      \"id\": 8,\n      \"prompt\": \"Singleton 和 ContextProto 的测试方式有什么区别\",\n      \"expected_output\": \"两者都可以用 app.getEggObject 直接获取。ContextProto 还可以在 app.mockModuleContextScope 回调内用 ctx.getEggObject 获取，提供上下文生命周期管理（退出时自动销毁、自动等待后台任务）\"\n    },\n    {\n      \"id\": 9,\n      \"prompt\": \"怎么测试一个依赖其他 Service 的 Service\",\n      \"expected_output\": \"mm(DependencyClass.prototype, 'method', fn) mock 依赖的原型方法，再 getEggObject 获取被测 Service\"\n    },\n    {\n      \"id\": 10,\n      \"prompt\": \"mockModuleContextScope 回调里的断言通过了但实际上没执行，测试直接过了\",\n      \"expected_output\": \"mockModuleContextScope 的回调必须 await，否则 scope 提前退出\",\n      \"files\": [\n        {\n          \"path\": \"test/service/user.test.ts\",\n          \"content\": \"import { app } from '@eggjs/mock/bootstrap';\\nimport assert from 'node:assert';\\nimport { UserService } from '../app/modules/user/UserService.ts';\\n\\ndescribe('UserService', () => {\\n  it('should get user', () => {\\n    app.mockModuleContextScope(async (ctx) => {\\n      const userService = await ctx.getEggObject(UserService);\\n      const user = await userService.getById('1');\\n      assert.equal(user.name, 'test');\\n    });\\n  });\\n});\"\n        }\n      ]\n    },\n    {\n      \"id\": 11,\n      \"prompt\": \"怎么 mock 一个被注入的 Service 的方法\",\n      \"expected_output\": \"mm(ServiceClass.prototype, 'methodName', async () => { ... })，import mm 从 @eggjs/mock/bootstrap，mock 原型而非实例\"\n    },\n    {\n      \"id\": 12,\n      \"prompt\": \"mm mock 了但没生效，帮我看看\",\n      \"expected_output\": \"mock 的是实例而非原型，应该用 mm(ServiceClass.prototype, ...)\",\n      \"files\": [\n        {\n          \"path\": \"test/service/order.test.ts\",\n          \"content\": \"import { app, mm } from '@eggjs/mock/bootstrap';\\nimport assert from 'node:assert';\\nimport { PaymentService } from '../app/modules/payment/PaymentService.ts';\\nimport { OrderService } from '../app/modules/order/OrderService.ts';\\n\\ndescribe('OrderService', () => {\\n  it('should mock payment', async () => {\\n    const paymentService = await app.getEggObject(PaymentService);\\n    mm(paymentService, 'charge', async () => ({ id: 'tx-mock' }));\\n\\n    const orderService = await app.getEggObject(OrderService);\\n    const order = await orderService.create({ productId: '1', amount: 100 });\\n    assert.equal(order.transactionId, 'tx-mock');\\n  });\\n});\"\n        }\n      ]\n    },\n    {\n      \"id\": 13,\n      \"prompt\": \"怎么验证 mock 的方法被调用了几次、传了什么参数\",\n      \"expected_output\": \"mockFn.called 调用次数，mockFn.lastCalledArguments 最后一次参数，mockFn.calledArguments 所有参数\"\n    },\n    {\n      \"id\": 14,\n      \"prompt\": \"怎么 mock 外部 HTTP 请求，我的 Service 里用了 httpclient 调第三方 API\",\n      \"expected_output\": \"app.mockHttpclient(url, { data: ... })，mock 通过 @Inject 注入的 HttpClient 发送的请求\"\n    },\n    {\n      \"id\": 15,\n      \"prompt\": \"mm 和 mm.spy 有什么区别\",\n      \"expected_output\": \"mm 替换方法实现，spy 保留原实现不替换，两者都会记录调用信息（called、lastCalledArguments）\"\n    },\n    {\n      \"id\": 16,\n      \"prompt\": \"怎么 mock 一个方法让它抛出异常\",\n      \"expected_output\": \"mm.error(Class.prototype, 'method', new Error('...'))\"\n    },\n    {\n      \"id\": 17,\n      \"prompt\": \"怎么测试后台任务是否执行完成\",\n      \"expected_output\": \"两种方式：1. mockModuleContextScope 退出时自动等待（内部触发 doPreDestroy） 2. app.backgroundTasksFinished() 手动等待。不应该用 sleep\"\n    },\n    {\n      \"id\": 18,\n      \"prompt\": \"用 httpRequest 触发的后台任务怎么等它完成\",\n      \"expected_output\": \"await app.backgroundTasksFinished()\"\n    },\n    {\n      \"id\": 19,\n      \"prompt\": \"测试里用了 TimerUtil.sleep 等后台任务，CI 上经常超时不稳定\",\n      \"expected_output\": \"用 app.backgroundTasksFinished() 替代 sleep，精确等待任务完成\",\n      \"files\": [\n        {\n          \"path\": \"test/service/task.test.ts\",\n          \"content\": \"import { app } from '@eggjs/mock/bootstrap';\\nimport assert from 'node:assert';\\nimport { TimerUtil } from '@eggjs/tegg-common-util';\\nimport { CountService } from '../app/modules/count/CountService.ts';\\n\\nit('should complete task', async () => {\\n  await app.httpRequest().get('/api/trigger-task').expect(200);\\n  await TimerUtil.sleep(3000);\\n  const countService = await app.getEggObject(CountService);\\n  assert.equal(countService.count, 1);\\n});\"\n        }\n      ]\n    },\n    {\n      \"id\": 20,\n      \"prompt\": \"backgroundTasksFinished 调了但断言还是失败，count 一直是 0\",\n      \"expected_output\": \"检查用于验证的 Service 是否是 @SingletonProto，ContextProto 每次 getEggObject 拿到的是不同实例\"\n    },\n    {\n      \"id\": 21,\n      \"prompt\": \"怎么测试 EventBus 事件是否被正确处理\",\n      \"expected_output\": \"app.getEventWaiter() 获取 waiter，eventWaiter.await('eventName') 必须在 emit 之前注册，用 mm mock handler 验证调用\"\n    },\n    {\n      \"id\": 22,\n      \"prompt\": \"eventWaiter.await 一直挂着不返回，测试超时了\",\n      \"expected_output\": \"eventWaiter.await 必须在 emit 之前调用，先注册等待再触发业务逻辑\",\n      \"files\": [\n        {\n          \"path\": \"test/event/hello.test.ts\",\n          \"content\": \"import { app } from '@eggjs/mock/bootstrap';\\nimport { HelloService } from '../app/modules/hello/HelloService.ts';\\n\\nit('should handle event', async () => {\\n  await app.mockModuleContextScope(async (ctx) => {\\n    const helloService = await ctx.getEggObject(HelloService);\\n    helloService.hello();\\n\\n    const eventWaiter = await app.getEventWaiter();\\n    await eventWaiter.await('helloEgg');\\n  });\\n});\"\n        }\n      ]\n    },\n    {\n      \"id\": 23,\n      \"prompt\": \"怎么验证 EventBus handler 收到的参数是否正确\",\n      \"expected_output\": \"mm mock handler 原型方法 + mockFn.lastCalledArguments 断言参数\"\n    },\n    {\n      \"id\": 24,\n      \"prompt\": \"帮我写一个 EventBus 事件的完整测试\",\n      \"expected_output\": \"完整示例：import bootstrap + mockModuleContextScope + getEventWaiter + await 在 emit 前 + mm mock handler + 断言\"\n    }\n  ]\n}\n","frontmatter":{"skill_name":"egg-unittest","description":"单元测试评测：覆盖 http-test、service-test、mock、background-task-test、eventbus-test","evals":[{"id":1,"prompt":"帮我给这个 UserController 写个单元测试，要测 GET 和 POST 接口","expected_output":"import { app } from '@eggjs/mock/bootstrap'，app.httpRequest() 测 GET 和 POST，POST 前调用 app.mockCsrf()，return 或 await httpRequest","files":[{"path":"app/modules/user/UserController.ts","content":"import { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPParam, HTTPBody, Inject } from 'egg';\nimport { UserService } from './UserService.ts';\n\n@HTTPController({ path: '/api/users' })\nexport class UserController {\n  @Inject()\n  userService: UserService;\n\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/:id' })\n  async show(@HTTPParam() id: string) {\n    return await this.userService.getById(id);\n  }\n\n  @HTTPMethod({ method: HTTPMethodEnum.POST, path: '/' })\n  async create(@HTTPBody() body: { name: string; email: string }) {\n    return await this.userService.create(body);\n  }\n}"}]},{"id":2,"prompt":"POST 接口测试报 403 错误，帮我看看","expected_output":"指出需要调用 app.mockCsrf() 跳过 CSRF 校验","files":[{"path":"test/controller/user.test.ts","content":"import { app } from '@eggjs/mock/bootstrap';\n\ndescribe('UserController', () => {\n  it('should create user', () => {\n    return app.httpRequest()\n      .post('/api/users')\n      .send({ name: 'test', email: 'test@example.com' })\n      .expect(200);\n  });\n});"}]},{"id":3,"prompt":"httpRequest 的 expect 有哪些用法","expected_output":"expect(status)、expect(body)、expect(status, body) 合并、expect('header', value)、expect(/regex/)、expect([200, 302]) 多状态码、expect(fn) 自定义断言"},{"id":4,"prompt":"测试写了但永远通过，不管接口返回啥都不报错","expected_output":"httpRequest 必须 return 或 await，否则断言不会执行","files":[{"path":"test/controller/home.test.ts","content":"import { app } from '@eggjs/mock/bootstrap';\n\ndescribe('HomeController', () => {\n  it('should return 200', () => {\n    app.httpRequest()\n      .get('/')\n      .expect(200)\n      .expect('hello');\n  });\n});"}]},{"id":5,"prompt":"帮我写一个完整的 CRUD 接口测试","expected_output":"GET/POST/PUT/DELETE 完整测试，POST/PUT/DELETE 都要 mockCsrf，import 从 @eggjs/mock/bootstrap"},{"id":6,"prompt":"基于这个 OrderService 帮我写完整的单元测试","expected_output":"app.getEggObject(OrderService) 获取实例 + mm mock PaymentService 依赖 + 断言返回值","files":[{"path":"app/modules/order/OrderService.ts","content":"import { SingletonProto, Inject } from 'egg';\nimport { PaymentService } from '../payment/PaymentService.ts';\n\n@SingletonProto()\nexport class OrderService {\n  @Inject()\n  paymentService: PaymentService;\n\n  async create(data: { productId: string; amount: number }) {\n    const tx = await this.paymentService.charge(data.amount);\n    return { orderId: 'order-1', transactionId: tx.id };\n  }\n}"}]},{"id":7,"prompt":"ContextProto 的 Service 怎么在测试里获取实例","expected_output":"app.getEggObject(ContextProtoClass) 可以直接获取，也可以在 app.mockModuleContextScope 回调内用 ctx.getEggObject 获取（后者提供上下文生命周期管理，退出时自动销毁）"},{"id":8,"prompt":"Singleton 和 ContextProto 的测试方式有什么区别","expected_output":"两者都可以用 app.getEggObject 直接获取。ContextProto 还可以在 app.mockModuleContextScope 回调内用 ctx.getEggObject 获取，提供上下文生命周期管理（退出时自动销毁、自动等待后台任务）"},{"id":9,"prompt":"怎么测试一个依赖其他 Service 的 Service","expected_output":"mm(DependencyClass.prototype, 'method', fn) mock 依赖的原型方法，再 getEggObject 获取被测 Service"},{"id":10,"prompt":"mockModuleContextScope 回调里的断言通过了但实际上没执行，测试直接过了","expected_output":"mockModuleContextScope 的回调必须 await，否则 scope 提前退出","files":[{"path":"test/service/user.test.ts","content":"import { app } from '@eggjs/mock/bootstrap';\nimport assert from 'node:assert';\nimport { UserService } from '../app/modules/user/UserService.ts';\n\ndescribe('UserService', () => {\n  it('should get user', () => {\n    app.mockModuleContextScope(async (ctx) => {\n      const userService = await ctx.getEggObject(UserService);\n      const user = await userService.getById('1');\n      assert.equal(user.name, 'test');\n    });\n  });\n});"}]},{"id":11,"prompt":"怎么 mock 一个被注入的 Service 的方法","expected_output":"mm(ServiceClass.prototype, 'methodName', async () => { ... })，import mm 从 @eggjs/mock/bootstrap，mock 原型而非实例"},{"id":12,"prompt":"mm mock 了但没生效，帮我看看","expected_output":"mock 的是实例而非原型，应该用 mm(ServiceClass.prototype, ...)","files":[{"path":"test/service/order.test.ts","content":"import { app, mm } from '@eggjs/mock/bootstrap';\nimport assert from 'node:assert';\nimport { PaymentService } from '../app/modules/payment/PaymentService.ts';\nimport { OrderService } from '../app/modules/order/OrderService.ts';\n\ndescribe('OrderService', () => {\n  it('should mock payment', async () => {\n    const paymentService = await app.getEggObject(PaymentService);\n    mm(paymentService, 'charge', async () => ({ id: 'tx-mock' }));\n\n    const orderService = await app.getEggObject(OrderService);\n    const order = await orderService.create({ productId: '1', amount: 100 });\n    assert.equal(order.transactionId, 'tx-mock');\n  });\n});"}]},{"id":13,"prompt":"怎么验证 mock 的方法被调用了几次、传了什么参数","expected_output":"mockFn.called 调用次数，mockFn.lastCalledArguments 最后一次参数，mockFn.calledArguments 所有参数"},{"id":14,"prompt":"怎么 mock 外部 HTTP 请求，我的 Service 里用了 httpclient 调第三方 API","expected_output":"app.mockHttpclient(url, { data: ... })，mock 通过 @Inject 注入的 HttpClient 发送的请求"},{"id":15,"prompt":"mm 和 mm.spy 有什么区别","expected_output":"mm 替换方法实现，spy 保留原实现不替换，两者都会记录调用信息（called、lastCalledArguments）"},{"id":16,"prompt":"怎么 mock 一个方法让它抛出异常","expected_output":"mm.error(Class.prototype, 'method', new Error('...'))"},{"id":17,"prompt":"怎么测试后台任务是否执行完成","expected_output":"两种方式：1. mockModuleContextScope 退出时自动等待（内部触发 doPreDestroy） 2. app.backgroundTasksFinished() 手动等待。不应该用 sleep"},{"id":18,"prompt":"用 httpRequest 触发的后台任务怎么等它完成","expected_output":"await app.backgroundTasksFinished()"},{"id":19,"prompt":"测试里用了 TimerUtil.sleep 等后台任务，CI 上经常超时不稳定","expected_output":"用 app.backgroundTasksFinished() 替代 sleep，精确等待任务完成","files":[{"path":"test/service/task.test.ts","content":"import { app } from '@eggjs/mock/bootstrap';\nimport assert from 'node:assert';\nimport { TimerUtil } from '@eggjs/tegg-common-util';\nimport { CountService } from '../app/modules/count/CountService.ts';\n\nit('should complete task', async () => {\n  await app.httpRequest().get('/api/trigger-task').expect(200);\n  await TimerUtil.sleep(3000);\n  const countService = await app.getEggObject(CountService);\n  assert.equal(countService.count, 1);\n});"}]},{"id":20,"prompt":"backgroundTasksFinished 调了但断言还是失败，count 一直是 0","expected_output":"检查用于验证的 Service 是否是 @SingletonProto，ContextProto 每次 getEggObject 拿到的是不同实例"},{"id":21,"prompt":"怎么测试 EventBus 事件是否被正确处理","expected_output":"app.getEventWaiter() 获取 waiter，eventWaiter.await('eventName') 必须在 emit 之前注册，用 mm mock handler 验证调用"},{"id":22,"prompt":"eventWaiter.await 一直挂着不返回，测试超时了","expected_output":"eventWaiter.await 必须在 emit 之前调用，先注册等待再触发业务逻辑","files":[{"path":"test/event/hello.test.ts","content":"import { app } from '@eggjs/mock/bootstrap';\nimport { HelloService } from '../app/modules/hello/HelloService.ts';\n\nit('should handle event', async () => {\n  await app.mockModuleContextScope(async (ctx) => {\n    const helloService = await ctx.getEggObject(HelloService);\n    helloService.hello();\n\n    const eventWaiter = await app.getEventWaiter();\n    await eventWaiter.await('helloEgg');\n  });\n});"}]},{"id":23,"prompt":"怎么验证 EventBus handler 收到的参数是否正确","expected_output":"mm mock handler 原型方法 + mockFn.lastCalledArguments 断言参数"},{"id":24,"prompt":"帮我写一个 EventBus 事件的完整测试","expected_output":"完整示例：import bootstrap + mockModuleContextScope + getEventWaiter + await 在 emit 前 + mm mock handler + 断言"}]},"isInternal":false,"tokens":2929,"sizeBytes":10343},{"name":"evals-routing.json","path":"packages/skills/eval/evals-routing.json","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/eval/evals-routing.json","title":"Eval Skill","category":"anthropic-skill","format":"json","content":"{\n  \"skill_name\": \"egg\",\n  \"description\": \"入口 skill 路由决策评测\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"帮我写一个接口\",\n      \"expected_output\": \"路由到 egg-controller skill\"\n    },\n    {\n      \"id\": 2,\n      \"prompt\": \"帮我写一个 GET /api/users/:id 的接口，返回用户信息\",\n      \"expected_output\": \"路由到 egg-controller skill\"\n    },\n    {\n      \"id\": 3,\n      \"prompt\": \"我想写个服务，不知道用 SingletonProto 还是 ContextProto\",\n      \"expected_output\": \"路由到 egg-core skill\"\n    },\n    {\n      \"id\": 4,\n      \"prompt\": \"帮我实现一个定时同步数据的功能，每 10 分钟跑一次\",\n      \"expected_output\": \"路由到 egg-controller skill\"\n    },\n    {\n      \"id\": 5,\n      \"prompt\": \"我的 @Inject 注入报错了，找不到对象\",\n      \"expected_output\": \"路由到 egg-core skill\"\n    },\n    {\n      \"id\": 6,\n      \"prompt\": \"帮我搭一个新模块，包含增删改查接口和 Service\",\n      \"expected_output\": \"路由到 egg-core skill（基础优先，先建模块再写接口）\"\n    },\n    {\n      \"id\": 7,\n      \"prompt\": \"我要给 AI 暴露几个 MCP Tool，查询订单和退款\",\n      \"expected_output\": \"路由到 egg-controller skill\"\n    },\n    {\n      \"id\": 8,\n      \"prompt\": \"不同环境的模块配置怎么管理\",\n      \"expected_output\": \"路由到 egg-core skill\"\n    },\n    {\n      \"id\": 9,\n      \"prompt\": \"帮我用事件总线解耦订单通知\",\n      \"expected_output\": \"路由到 egg-core skill\"\n    },\n    {\n      \"id\": 10,\n      \"prompt\": \"EventBus 和 BackgroundTaskHelper 怎么选\",\n      \"expected_output\": \"路由到 egg-core skill\"\n    },\n    {\n      \"id\": 11,\n      \"prompt\": \"帮我用 AOP 给所有 Service 加日志\",\n      \"expected_output\": \"路由到 egg-core skill\"\n    }\n  ]\n}\n","frontmatter":{"skill_name":"egg","description":"入口 skill 路由决策评测","evals":[{"id":1,"prompt":"帮我写一个接口","expected_output":"路由到 egg-controller skill"},{"id":2,"prompt":"帮我写一个 GET /api/users/:id 的接口，返回用户信息","expected_output":"路由到 egg-controller skill"},{"id":3,"prompt":"我想写个服务，不知道用 SingletonProto 还是 ContextProto","expected_output":"路由到 egg-core skill"},{"id":4,"prompt":"帮我实现一个定时同步数据的功能，每 10 分钟跑一次","expected_output":"路由到 egg-controller skill"},{"id":5,"prompt":"我的 @Inject 注入报错了，找不到对象","expected_output":"路由到 egg-core skill"},{"id":6,"prompt":"帮我搭一个新模块，包含增删改查接口和 Service","expected_output":"路由到 egg-core skill（基础优先，先建模块再写接口）"},{"id":7,"prompt":"我要给 AI 暴露几个 MCP Tool，查询订单和退款","expected_output":"路由到 egg-controller skill"},{"id":8,"prompt":"不同环境的模块配置怎么管理","expected_output":"路由到 egg-core skill"},{"id":9,"prompt":"帮我用事件总线解耦订单通知","expected_output":"路由到 egg-core skill"},{"id":10,"prompt":"EventBus 和 BackgroundTaskHelper 怎么选","expected_output":"路由到 egg-core skill"},{"id":11,"prompt":"帮我用 AOP 给所有 Service 加日志","expected_output":"路由到 egg-core skill"}]},"isInternal":false,"tokens":557,"sizeBytes":1820},{"name":"package.json","path":"packages/skills/package.json","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/package.json","title":"@eggjs/skills","category":"anthropic-skill","format":"json","content":"{\n  \"name\": \"@eggjs/skills\",\n  \"version\": \"4.2.0\",\n  \"description\": \"agent skills for egg\",\n  \"keywords\": [\n    \"egg\",\n    \"skill\"\n  ],\n  \"homepage\": \"https://github.com/eggjs/egg/tree/next/packages/skills\",\n  \"bugs\": {\n    \"url\": \"https://github.com/eggjs/egg/issues\"\n  },\n  \"license\": \"MIT\",\n  \"author\": \"eggjs\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/eggjs/egg.git\",\n    \"directory\": \"packages/skills\"\n  },\n  \"files\": [\n    \"egg-controller/**/*.md\",\n    \"egg-core/**/*.md\",\n    \"egg-unittest/**/*.md\",\n    \"egg/**/*.md\"\n  ],\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"engines\": {\n    \"node\": \">=22.18.0\"\n  }\n}\n","frontmatter":{"name":"@eggjs/skills","version":"4.2.0","description":"agent skills for egg","keywords":["egg","skill"],"homepage":"https://github.com/eggjs/egg/tree/next/packages/skills","bugs":{"url":"https://github.com/eggjs/egg/issues"},"license":"MIT","author":"eggjs","repository":{"type":"git","url":"git+https://github.com/eggjs/egg.git","directory":"packages/skills"},"files":["egg-controller/**/*.md","egg-core/**/*.md","egg-unittest/**/*.md","egg/**/*.md"],"publishConfig":{"access":"public"},"engines":{"node":">=22.18.0"}},"isInternal":false,"tokens":218,"sizeBytes":655},{"name":"PLAN.md","path":"packages/skills/PLAN.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/PLAN.md","title":"Skills Skill","category":"anthropic-skill","format":"markdown","content":"# Skills 评测方案设计\n\n## 目标\n\n为 `packages/skills/` 设计一套评测体系，覆盖两个层面：\n\n1. **静态校验** — 验证 Skill 文件的结构正确性、引用完整性\n2. **动态评测** — 用 LLM-as-Judge 评估 AI 基于 Skill 生成回答的质量\n\n全部手动触发运行，不集成 CI。\n\n---\n\n## 目录结构\n\n```\npackages/skills/\n├── egg/\n├── controller/\n├── tegg-core/\n├── eval/                          # 新增：评测目录\n│   ├── static/\n│   │   └── validate.test.ts       # 静态校验测试\n│   ├── dynamic/\n│   │   ├── routing.eval.ts        # 入口路由评测\n│   │   └── quality.eval.ts        # 内容质量评测\n│   ├── fixtures/\n│   │   ├── routing-cases.ts       # 路由测试用例\n│   │   └── quality-cases.ts       # 质量测试用例\n│   └── lib/\n│       ├── skill-loader.ts        # Skill 文件加载器\n│       ├── judge.ts               # LLM-as-Judge 核心逻辑\n│       └── types.ts               # 共享类型定义\n├── vitest.config.ts\n├── package.json\n└── tsconfig.json\n```\n\n---\n\n## 第一部分：静态校验\n\n### 校验项\n\n| 校验项               | 说明                                                          |\n| -------------------- | ------------------------------------------------------------- |\n| **Frontmatter 格式** | 每个 SKILL.md 必须包含 `name`、`description`、`allowed-tools` |\n| **引用文件存在性**   | SKILL.md 中提到的 `references/*.md` 文件必须存在              |\n| **交叉引用一致性**   | 入口 skill 提到的子 skill 目录必须存在且包含 SKILL.md         |\n| **Markdown 结构**    | 标题层级合理（以 `# ` 开头，不跳级）                          |\n| **决策表完整性**     | 入口 skill 的路由表中每个 skill 都有对应目录                  |\n\n### 实现方式\n\n使用 vitest + node:assert 编写测试，通过 Node.js fs API 读取文件并解析：\n\n```typescript\n// eval/static/validate.test.ts\nimport { describe, it } from 'vitest';\nimport assert from 'node:assert/strict';\nimport { loadAllSkills } from '../lib/skill-loader.ts';\n\ndescribe('Skill 静态校验', () => {\n  describe('Frontmatter', () => {\n    it('每个 SKILL.md 包含必填字段: name, description, allowed-tools', ...);\n  });\n\n  describe('引用完整性', () => {\n    it('SKILL.md 中引用的 references/ 文件均存在', ...);\n    it('入口 skill 引用的子 skill 目录均存在', ...);\n  });\n\n  describe('Markdown 结构', () => {\n    it('标题层级不跳级', ...);\n  });\n});\n```\n\n---\n\n## 第二部分：动态评测（LLM-as-Judge）\n\n### 评测维度\n\n动态评测分为两个子场景：\n\n#### 2.1 路由评测 — 入口 Skill 是否正确路由\n\n测试 `egg/SKILL.md` 的决策逻辑：给定用户查询，判断 AI 是否路由到正确的子 skill。\n\n**测试用例结构：**\n\n```typescript\n// eval/fixtures/routing-cases.ts\nexport const routingCases: RoutingCase[] = [\n  {\n    query: '如何创建 HTTP controller？',\n    expectedSkill: 'controller',\n    reason: '明确提到 controller，属于协议实现',\n  },\n  {\n    query: '@SingletonProto 和 @ContextProto 有什么区别？',\n    expectedSkill: 'tegg-core',\n    reason: '关于对象生命周期，属于核心概念',\n  },\n  {\n    query: '我需要创建一个可以被 HTTP 控制器使用的服务',\n    expectedSkill: 'tegg-core',\n    reason: '模糊意图，按规则 1（基础优先）应路由到 core',\n  },\n  // ... 更多用例\n];\n```\n\n**测试实现：**\n\n```typescript\n// eval/dynamic/routing.eval.ts\nimport { describe, it } from 'vitest';\nimport assert from 'node:assert/strict';\nimport Anthropic from '@anthropic-ai/sdk';\nimport { loadSkillContent } from '../lib/skill-loader.ts';\nimport { routingCases } from '../fixtures/routing-cases.ts';\n\nconst client = new Anthropic();\nconst AVAILABLE_SKILLS = ['controller', 'tegg-core'];\n\ndescribe('路由评测', () => {\n  // 加载入口 skill 作为 system prompt\n  const entrySkillContent = loadSkillContent('egg');\n\n  for (const { query, expectedSkill, reason } of routingCases) {\n    it(`\"${query}\" → ${expectedSkill}`, async () => {\n      // 1. 将 SKILL.md 作为 system prompt，发送用户查询\n      const response = await client.messages.create({\n        model: 'claude-sonnet-4-20250514',\n        max_tokens: 1024,\n        system: [\n          entrySkillContent,\n          // 约束输出格式，让 AI 只做路由决策\n          `你是 EGG 框架技能路由器。根据上面的决策指南，分析用户查询并选择应该加载的技能。`,\n          `可选技能: ${AVAILABLE_SKILLS.join(', ')}`,\n          `只输出 JSON: {\"skill\": \"<技能名>\", \"reason\": \"<简要理由>\"}`,\n        ].join('\\n\\n'),\n        messages: [{ role: 'user', content: query }],\n      });\n\n      // 2. 解析 AI 回答中的路由选择\n      const text = response.content[0].type === 'text' ? response.content[0].text : '';\n      const parsed = JSON.parse(text);\n\n      // 3. 断言路由正确性\n      assert.equal(\n        parsed.skill,\n        expectedSkill,\n        `路由错误: 期望 \"${expectedSkill}\" 但得到 \"${parsed.skill}\"` +\n          `\\n  用例理由: ${reason}` +\n          `\\n  AI 理由: ${parsed.reason}`,\n      );\n    });\n  }\n});\n```\n\n#### 2.2 内容质量评测 — 子 Skill 回答质量\n\n测试各子 skill 对领域问题的回答质量。\n\n**测试用例结构：**\n\n```typescript\n// eval/fixtures/quality-cases.ts\nexport const qualityCases: QualityCase[] = [\n  {\n    skill: 'controller',\n    query: '如何创建一个 POST 接口接收 JSON body？',\n    criteria: [\n      '使用 @HTTPController 装饰器',\n      '使用 @HTTPMethod 且 method 为 POST',\n      '使用 @HTTPBody() 获取请求体',\n      '包含完整可运行的代码示例',\n    ],\n    references: ['references/http-controller.md'], // 需要加载的参考文档\n  },\n  {\n    skill: 'tegg-core',\n    query: '如何让一个服务可以被其他模块访问？',\n    criteria: ['提到 AccessLevel.PUBLIC', '使用 @SingletonProto 装饰器', '解释跨模块访问机制'],\n    references: [],\n  },\n  // ... 更多用例\n];\n```\n\n**测试实现：**\n\n```typescript\n// eval/dynamic/quality.eval.ts\nimport { describe, it } from 'vitest';\nimport assert from 'node:assert/strict';\nimport Anthropic from '@anthropic-ai/sdk';\nimport { loadSkillContent, loadReference } from '../lib/skill-loader.ts';\nimport { qualityCases } from '../fixtures/quality-cases.ts';\nimport { judge } from '../lib/judge.ts';\n\nconst client = new Anthropic();\n\ndescribe('内容质量评测', () => {\n  for (const testCase of qualityCases) {\n    describe(`[${testCase.skill}] ${testCase.query}`, () => {\n      let aiResponse: string;\n\n      // Step 1: 加载 skill 内容作为 system prompt，向被测 LLM 提问\n      it('生成回答', async () => {\n        const skillContent = loadSkillContent(testCase.skill);\n        const refContents = testCase.references.map((ref) => loadReference(testCase.skill, ref));\n\n        const systemPrompt = [skillContent, ...refContents].join('\\n\\n---\\n\\n');\n\n        const response = await client.messages.create({\n          model: 'claude-sonnet-4-20250514',\n          max_tokens: 2048,\n          system: systemPrompt,\n          messages: [{ role: 'user', content: testCase.query }],\n        });\n\n        aiResponse = response.content[0].type === 'text' ? response.content[0].text : '';\n        assert.ok(aiResponse.length > 0, 'AI 应该返回非空回答');\n      });\n\n      // Step 2: 用 Judge LLM 对回答逐项评分\n      it('通过质量评审', async () => {\n        const result = await judge(client, {\n          query: testCase.query,\n          response: aiResponse,\n          criteria: testCase.criteria,\n        });\n\n        // 输出详细评分到 console 供人工查看\n        console.log(`    得分: ${result.totalScore} (${result.passed}/${result.total})`);\n        for (const item of result.details) {\n          const icon = item.score === 1 ? '✓' : '✗';\n          console.log(`      ${icon} ${item.criterion}: ${item.reason}`);\n        }\n\n        // 断言：所有 criteria 都应满足\n        assert.ok(\n          result.totalScore >= 0.8,\n          `质量不达标: ${result.totalScore} < 0.8\\n` +\n            result.details\n              .filter((d) => d.score === 0)\n              .map((d) => `  ✗ ${d.criterion}: ${d.reason}`)\n              .join('\\n'),\n        );\n      });\n    });\n  }\n});\n```\n\n### LLM-as-Judge 实现\n\n```typescript\n// eval/lib/judge.ts\nimport type Anthropic from '@anthropic-ai/sdk';\nimport type { JudgeInput, JudgeResult, JudgeDetail } from './types.ts';\n\nexport async function judge(client: Anthropic, input: JudgeInput): Promise<JudgeResult> {\n  const criteriaList = input.criteria.map((c, i) => `${i + 1}. ${c}`).join('\\n');\n\n  const response = await client.messages.create({\n    model: 'claude-sonnet-4-20250514',\n    max_tokens: 1024,\n    system: '你是 AI 回答质量评估专家。严格按照 JSON 格式输出评分结果。',\n    messages: [\n      {\n        role: 'user',\n        content: `请根据评分标准，对以下 AI 回答逐项评分。\n\n## 评分标准\n${criteriaList}\n\n## 用户问题\n${input.query}\n\n## AI 回答\n${input.response}\n\n## 输出格式（严格 JSON）\n{\n  \"details\": [\n    { \"criterion\": \"标准内容\", \"score\": 0 或 1, \"reason\": \"简要理由\" }\n  ]\n}`,\n      },\n    ],\n  });\n\n  const text = response.content[0].type === 'text' ? response.content[0].text : '';\n  const parsed = JSON.parse(text);\n  const details: JudgeDetail[] = parsed.details;\n  const passed = details.filter((d) => d.score === 1).length;\n\n  return {\n    details,\n    passed,\n    total: details.length,\n    totalScore: passed / details.length,\n  };\n}\n```\n\n### 评测报告\n\n运行评测后生成 JSON 报告：\n\n```json\n{\n  \"timestamp\": \"2026-02-05T10:00:00Z\",\n  \"routing\": {\n    \"total\": 10,\n    \"correct\": 9,\n    \"accuracy\": 0.9,\n    \"failures\": [\n      {\n        \"query\": \"...\",\n        \"expected\": \"tegg-core\",\n        \"actual\": \"controller\",\n        \"reason\": \"...\"\n      }\n    ]\n  },\n  \"quality\": {\n    \"controller\": {\n      \"cases\": 5,\n      \"avg_score\": 0.85,\n      \"details\": [...]\n    },\n    \"tegg-core\": {\n      \"cases\": 5,\n      \"avg_score\": 0.90,\n      \"details\": [...]\n    }\n  }\n}\n```\n\n---\n\n## 第三部分：技术选型与依赖\n\n| 组件                  | 选型               | 理由                           |\n| --------------------- | ------------------ | ------------------------------ |\n| 测试框架              | vitest             | 遵循 monorepo 标准             |\n| 断言库                | node:assert/strict | Node.js 内置，零依赖           |\n| YAML frontmatter 解析 | gray-matter        | 成熟的 frontmatter 解析库      |\n| LLM 调用              | @anthropic-ai/sdk  | 使用 Claude API 做评测和 Judge |\n| 报告输出              | JSON 文件          | 简单可读，方便后续扩展为可视化 |\n\n### package.json scripts\n\n```json\n{\n  \"scripts\": {\n    \"test\": \"vitest run --config vitest.config.ts eval/static/\",\n    \"eval\": \"vitest run --config vitest.config.ts eval/dynamic/\",\n    \"eval:routing\": \"vitest run --config vitest.config.ts eval/dynamic/routing.eval.ts\",\n    \"eval:quality\": \"vitest run --config vitest.config.ts eval/dynamic/quality.eval.ts\"\n  }\n}\n```\n\n- `test` — 运行静态校验（快速，无 API 调用）\n- `eval` — 运行全部动态评测\n- `eval:routing` — 仅运行路由评测\n- `eval:quality` — 仅运行内容质量评测\n\n动态评测需设置 `ANTHROPIC_API_KEY` 环境变量。\n\n---\n\n## 实施步骤\n\n1. 在 worktree (`egg-skills-eval`) 中添加依赖：vitest、gray-matter、@anthropic-ai/sdk\n2. 添加 `vitest.config.ts` 和更新 `package.json` scripts\n3. 创建 `eval/lib/` 基础工具：skill-loader、types、judge\n4. 实现 `eval/static/validate.test.ts` 静态校验\n5. 编写路由测试用例 `eval/fixtures/routing-cases.ts`\n6. 实现 `eval/dynamic/routing.eval.ts` 路由评测\n7. 编写质量测试用例 `eval/fixtures/quality-cases.ts`\n8. 实现 `eval/dynamic/quality.eval.ts` 内容质量评测\n","isInternal":false,"tokens":3561,"sizeBytes":12250},{"name":"output-structure.md","path":"tools/egg-bundler/docs/output-structure.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/tools/egg-bundler/docs/output-structure.md","title":"Docs Skill","category":"anthropic-skill","format":"markdown","content":"# Bundle output structure\n\n`@eggjs/egg-bundler` produces a runnable CJS bundle under the configured\n`outputDir`. Everything except configured or auto-detected externals is inlined\ninto the generated worker file by default.\n\n## Layout\n\n```\n<outputDir>/\n├── worker.js             # self-contained entry produced from worker.entry.ts\n├── app/port/binary.html  # runtime asset copied from <baseDir>/app/port/binary.html\n├── app/port/login.html   # runtime asset copied from <baseDir>/app/port/login.html\n├── package.json          # `{ \"type\": \"commonjs\" }`, so Node parses generated *.js as CJS\n└── bundle-manifest.json  # reference/debug metadata written by Bundler\n```\n\n`pack.singleFile` defaults to `true`. When a programmatic caller explicitly\nsets it to `false`, `@utoo/pack` may also emit opaque module-graph and runtime\nchunks such as `_root-of-the-server___<hash>.js` and\n`_turbopack__runtime.js`. Snapshot builds always force single-file output. The\ncompiler `tsconfig.json` is written to the bundler's generated entry directory,\nnot to the deploy output.\n\nFor `target: 'cluster'`, `worker.js` is replaced by two role-specific entry\nfiles:\n\n```text\n<outputDir>/\n├── app_worker.js         # self-contained application worker entry\n├── agent_worker.js       # self-contained agent worker entry\n├── app/...               # copied runtime assets, when present\n├── package.json          # `{ \"type\": \"commonjs\" }`\n└── bundle-manifest.json\n```\n\nThe source generator shares its rendering logic, but each role is a separate\nentry. With the default single-file setting, `@utoo/pack` inlines the complete\ndependency graph into both outputs; snapshot builds rely on this so each file\ncan independently build and restore its own V8 snapshot blob.\n\n## Running the bundle\n\n```bash\ncd <outputDir>\nnode worker.js\n```\n\nThe worker entry installs `ManifestStore.setBundleStore(...)` and\n`globalThis.__EGG_BUNDLE_MODULE_LOADER__` before calling\n`startEgg({ baseDir: outputDir, framework, mode: 'single' })`, so framework\nspecifier lookup is served by the already imported bundled framework module,\nwithout adding framework path aliases. Runtime lookup keeps\nthe deploy output directory separate from the original app paths: the bundle map\nis keyed by relKey, output-dir absolute paths, precomputed original app absolute\npaths, and manifest `resolveCache` request aliases. Application code and plugins\nmay still use `fs` for resources such as config, views, or assets.\n\nThe supported ordinary cluster workflow is:\n\n```bash\negg-bin bundle --cluster\negg-scripts start --bundle --bundle-dir ./dist-bundle\n```\n\n`egg-scripts` resolves `app_worker.js` and `agent_worker.js` from\n`--bundle-dir`, then supplies them to the cluster launcher. Plain bundles\nsupport process and `worker_threads` start modes. To build and restore\nrole-specific V8 snapshots:\n\n```bash\negg-bin snapshot build --cluster\negg-scripts start --bundle \\\n  --app-snapshot-blob ./dist-bundle/app.snapshot.blob \\\n  --agent-snapshot-blob ./dist-bundle/agent.snapshot.blob\n```\n\nEither role blob may be omitted; that role then starts from its bundle\nJavaScript. Snapshot blobs require process mode.\n\n## Runtime assets\n\nThe bundler copies application runtime assets from `<baseDir>/app` by default\ninto the same relative path under `outputDir`, excluding manifest-known module\nfiles and source-like files such as `.js`, `.ts`, `.mjs`, and `.cjs` outside\nstatic asset directories. For example, `<baseDir>/app/port/binary.html` is\nemitted as `<outputDir>/app/port/binary.html`. Since bundled workers start Egg with\n`baseDir: outputDir`, existing reads such as\n`fs.readFile(path.join(app.config.baseDir, 'app/port/binary.html'))` resolve to\nthe copied file in bundle mode and continue to resolve to the original file in\nnon-bundle mode. Static asset directories such as `app/public`, `app/assets`,\nand `app/static` are copied verbatim so frontend `.js` and `.css` files remain\nservable from the bundled app. Applications may replace those force-copy\ndirectories with `bundle.runtimeAssets.forceCopyDirs` in `module.yml`, for\nexample:\n\n```yaml\nbundle:\n  runtimeAssets:\n    roots:\n      - app\n    forceCopyDirs:\n      - app/public\n      - app/assets\n      - app/static\n      - app/port\n```\n\nApplications may also replace the scanned roots with\n`bundle.runtimeAssets.roots`. Root entries are baseDir-relative directories, and\nthe output keeps the same baseDir-relative path.\n\n## `bundle-manifest.json`\n\nA reference file produced by `Bundler` (not consumed at runtime). Shape:\n\n```json\n{\n  \"version\": 1,\n  \"generatedAt\": \"2026-04-11T00:00:00.000Z\",\n  \"mode\": \"production\",\n  \"target\": \"single\",\n  \"baseDir\": \"/abs/path/to/app\",\n  \"framework\": \"egg\",\n  \"entries\": [{ \"name\": \"worker\", \"source\": \"/abs/path/to/app/.egg-bundle/entries/worker.entry.ts\" }],\n  \"externals\": [\"some-native-addon\", \"...\"],\n  \"chunks\": [\"package.json\", \"worker.js\", \"...\"]\n}\n```\n\nUse it to inspect what went into the bundle or to drive deterministic-bundle\nchecks (T17).\n\n## Externals\n\nPackages classified as external by `ExternalsResolver` are **not** inlined.\nThis includes the user's `externals.force` list plus auto-detected entries from\nroot `peerDependencies`, root `optionalDependencies`, root dependency packages\nwith native addons/native binaries, root dependency packages whose optional peer\ndependencies cannot be resolved, and the names of those missing optional peer\npackages as `extraExternals`, plus native optional platform packages declared by\nroot dependencies.\nThe native addon and missing optional peer checks run only while resolving the\napp's root dependencies/optionalDependencies; `ExternalsResolver` does not\nrecursively scan every transitive dependency. `externals.inline`\nremoves an auto-detected external unless the same name is also present in\n`externals.force`. External packages must be resolvable from the output worker.\nA typical deployment keeps `outputDir` below the application or deployment root\nand installs production dependencies at that root. Do not overwrite the\ngenerated output `package.json`: its `{ \"type\": \"commonjs\" }` marker is required\nwhen the parent application package uses ESM. ESM-only packages, `egg`,\n`@swc/helpers`, and `@eggjs/*`\npackages are bundled by default unless an explicit or auto-detected external\nrule applies, such as `externals.force`, peer/optional dependency metadata,\nnative addon detection, or missing optional peer detection. For wrappers around\nnative optional platform packages that cannot be loaded through `createRequire`,\nthe wrapper stays bundled so the CommonJS standalone output does not emit a\nplain `require(wrapper)`, while the optional native platform packages remain\nexternal.\n\n## Known limitations\n\n- **Cluster bootstrap modules**: bundled cluster workers do not support\n  `options.require`; the supported launcher fails before spawning workers.\n- **Snapshot blobs and worker threads**: plain cluster bundles support\n  `worker_threads`, but custom V8 snapshot blobs require process mode.\n- **Native addons**: always external. If a native module is missing from the\n  deployment target, the bundle will fail to start at runtime with the usual\n  Node module resolution error.\n- **Tegg**: decorated files listed in `manifest.extensions.tegg` are included\n  as side-effect imports in the worker entry; if `tegg` is disabled in\n  `BundlerConfig`, tegg collection is intended to be skipped (not yet wired).\n","isInternal":false,"tokens":1708,"sizeBytes":7458},{"name":"core.md","path":"wiki/packages/core.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/wiki/packages/core.md","title":"Packages Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Core Package\ntype: package\nsummary: Loader, lifecycle, and application core primitives used by Egg runtime packages.\nsource_files:\n  - packages/core/src/index.ts\n  - packages/core/src/loader/file_loader.ts\n  - packages/core/src/loader/context_loader.ts\n  - packages/core/src/loader/egg_loader.ts\nupdated_at: 2026-05-10\nstatus: active\n---\n\n# Core Package\n\n`@eggjs/core` exports the runtime loader primitives used to assemble an Egg\napplication, including `EggLoader`, `FileLoader`, `ContextLoader`, manifest\nsupport, lifecycle, and base context classes.\n\n## LoaderFS\n\n`@eggjs/core` consumes and re-exports `LoaderFS` and `RealLoaderFS` from\n`@eggjs/loader-fs`. The abstraction remains the minimal filesystem boundary for\nloader-facing file access: `exists`, `stat`, `realpath`, `readJSON`, `glob`, and\n`loadFile`, without trying to polyfill the full Node.js `fs` module.\n\n`EggLoaderOptions`, `FileLoaderOptions`, and `ContextLoaderOptions` can carry a\ncustom `loaderFS`. `EggLoader` passes its loader FS into `loadToApp()` and\n`loadToContext()` so later bundled loaders can replace file discovery and module\nloading without changing the public loader call sites.\n","frontmatter":{"title":"Core Package","type":"package","summary":"Loader, lifecycle, and application core primitives used by Egg runtime packages.","source_files":["packages/core/src/index.ts","packages/core/src/loader/file_loader.ts","packages/core/src/loader/context_loader.ts","packages/core/src/loader/egg_loader.ts"],"updated_at":"2026-05-10","status":"active"},"isInternal":false,"tokens":286,"sizeBytes":1173},{"name":"egg-bundler.md","path":"wiki/packages/egg-bundler.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/wiki/packages/egg-bundler.md","title":"Packages Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Egg Bundler\ntype: package\nsummary: Bundles Egg applications for Node startup snapshots and tegg standalone service-worker targets.\nsource_files:\n  - packages/core/src/lifecycle.ts\n  - packages/egg/src/lib/egg.ts\n  - packages/cluster/src/worker_protocol/worker-thread.ts\n  - packages/cluster/src/utils/mode/impl/worker_threads/agent.ts\n  - packages/cluster/src/utils/mode/impl/worker_threads/app.ts\n  - plugins/watcher/src/lib/boot.ts\n  - tools/egg-bundler/src/index.ts\n  - tools/egg-bundler/src/compat/leoric/index.ts\n  - tools/egg-bundler/src/compat/leoric/runtime-require-loader.cjs\n  - tools/egg-bundler/src/lib/Bundler.ts\n  - tools/egg-bundler/src/lib/EntryGenerator.ts\n  - tools/egg-bundler/src/lib/ExternalsResolver.ts\n  - tools/egg-bundler/src/lib/prelude.ts\n  - tools/egg-bundler/src/lib/PackRunner.ts\n  - tools/egg-bundler/src/lib/StandaloneWorkerBundler.ts\n  - tools/egg-bundler/src/lib/importMetaPatch.ts\n  - tools/egg-bin/src/commands/bundle.ts\n  - tools/egg-bin/src/commands/snapshot.ts\n  - tools/scripts/src/commands/start.ts\n  - packages/loader-fs/src/index.ts\n  - packages/loader-fs/src/manifest_loader_fs.ts\n  - tegg/core/types/src/metadata/model/TeggManifest.ts\n  - tegg/core/loader/src/LoaderFactory.ts\n  - tegg/core/loader/src/LoaderUtil.ts\n  - tegg/core/loader/src/TeggManifestLoaderFS.ts\n  - tegg/core/loader/src/impl/ModuleLoader.ts\n  - tegg/plugin/dal/src/lib/DataSource.ts\n  - tegg/plugin/tegg/src/lib/EggModuleLoader.ts\n  - tegg/standalone/standalone/src/EggModuleLoader.ts\n  - tools/egg-bundler/docs/output-structure.md\n  - site/docs/advanced/snapshot.md\n  - site/docs/zh-CN/advanced/snapshot.md\n  - examples/helloworld-service-worker\nupdated_at: 2026-10-06\nstatus: active\n---\n\n# Egg Bundler\n\n`@eggjs/egg-bundler` is a developer tooling package under `tools/egg-bundler/`.\nIt exposes `bundle(config)` and the `Bundler` class for producing a deployable\nCommonJS artifact from an Egg application, plus `StandaloneWorkerBundler` for the\ntegg standalone service worker (Cloudflare Workers) — see the section below.\n\nThe bundler engine (`PackRunner`) is `@utoo/pack`, whose engine is **Turbopack**\n(not mako). `@utoo/pack` only emits **CommonJS** — `output.type` is `standalone`\nor `export` (single-file self-executing IIFE); there is no ESM output. In\nsingle-file (`export`) mode the IIFE ends with `module.exports = factory()` where\n`factory()` returns the entry's ES-module namespace, so a Node `require()` of the\noutput exposes the entry's `export default`/named exports; `library.name`/`export`\nonly affect the `exports[name]`/`globalThis[name]` fallback branches. That CJS-only\nconstraint is why worker output always needs a thin ESM wrapper.\n\n## Public Surfaces\n\n- `tools/egg-bundler/src/index.ts` exports `bundle`, `Bundler`, the helper\n  classes, and the public config/result types.\n- `tools/egg-bin/src/commands/bundle.ts` wires the package into\n  `egg-bin bundle`.\n\n## Bundle Flow\n\n1. `ManifestLoader` loads the app startup manifest, defaulting to\n   `<baseDir>/.egg/manifest.json`.\n2. `ExternalsResolver` classifies packages that should stay external.\n3. `EntryGenerator` writes either one single-process worker entry or separate\n   app-worker and agent-worker entries. Each installs the bundle manifest/module\n   loader before starting its Egg runtime role.\n4. `PackRunner` invokes `@utoo/pack`.\n5. `Bundler` writes `bundle-manifest.json` and returns absolute output paths.\n\n## Current Behavior\n\n- Relative `outputDir` values are resolved from `baseDir`.\n- Default mode is `production`; `development` is also accepted.\n- `PackRunner` defaults to `singleFile: true`, so each normal worker entry is a\n  self-contained CommonJS file. Snapshot builds require and force that setting.\n  A programmatic caller may opt into opaque `@utoo/pack` runtime/module chunks\n  for a non-snapshot build with `singleFile: false`.\n- If `<baseDir>/.egg/manifest.json` is missing, `ManifestLoader` starts the app\n  with `metadataOnly: true` to generate it. This skips the agent and normal boot\n  lifecycle, runs `loadMetadata()` hooks, and the manifest generation child\n  process exits after writing the manifest, so registered `beforeClose` hooks do\n  not run.\n- The default `single` target runs in Egg single-process mode. Its worker entry\n  treats the deploy output directory as the runtime Egg `baseDir`, passes the\n  framework specifier explicitly to `startEgg`, maps that specifier to the\n  already bundled framework module, and precomputes original app absolute\n  aliases so bundled module lookup can serve relKeys, output-dir absolute paths,\n  original app absolute paths, and manifest `resolveCache` request aliases.\n- The `cluster` target emits `app_worker.js` and `agent_worker.js`. Their roles\n  are fixed while generating the entries rather than selected by\n  `EGG_PROCESS_TYPE` or another runtime switch. Both entries use the shared\n  `@eggjs/cluster/worker_protocol` implementation, accept the master's normal\n  JSON argv contract, and select process IPC or `worker_threads.parentPort`\n  from `startMode`. Snapshot builds force each output to remain independently\n  self-contained, so no common runtime chunk is emitted between the two files.\n  Custom V8 snapshot blobs remain process-only because Node does not expose a\n  per-Worker snapshot-blob API; option parsing rejects that combination before\n  creating a worker thread. Sticky-session mode is also process-only because its\n  socket handoff uses process IPC. Worker-thread shutdown is an explicit protocol:\n  the master requests graceful exit, the worker awaits `app.close()` or\n  `agent.close()`, and the master falls back to `Worker.terminate()` immediately\n  if the worker emits an error or after the configured close timeout. Successful\n  shutdown cancels its timeout timer so it does not keep the event loop alive.\n  A nonzero graceful-exit code or worker error is propagated after all app\n  workers and the agent have been given their cleanup attempt; the master exits\n  with code `1`. Timeout termination retains its existing successful master-exit\n  behavior, and process-mode shutdown behavior is unchanged.\n  With worker-thread `reusePort`, an explicit master port of `0` defers to the\n  application's configured shared listen port rather than being rejected before\n  the worker protocol runs.\n- `egg-bin bundle --cluster` is the ordinary cluster-bundle producer. It selects\n  the bundler's `cluster` target and writes `app_worker.js`, `agent_worker.js`,\n  and the bundle manifest without constructing snapshot blobs. Its output can\n  be passed directly to `egg-scripts start --bundle`. `--cluster` is an Egg app\n  mode and is rejected when standalone mode is selected explicitly or inferred\n  from `--entry`.\n- `egg-bin snapshot build --cluster` selects that target and runs two independent\n  V8 snapshot builds: `app_worker.js` produces `app.snapshot.blob`, while\n  `agent_worker.js` produces `agent.snapshot.blob`. Their paths can be set\n  independently with `--app-snapshot-blob` and `--agent-snapshot-blob`; the\n  existing `--blob` flag remains exclusive to single-process builds. The role\n  comes from the entry filename/code, so snapshot construction does not use\n  `EGG_SNAPSHOT_ROLE`. The command rejects identical role blob paths and removes\n  each existing target immediately before building (except in dry-run mode), so\n  a stale blob cannot satisfy the post-build existence check.\n- A `snapshot: true` output remains a normal runnable bundle; generating it does\n  not dedicate the JavaScript file to blob restore. The prelude and generated\n  entry use `v8.startupSnapshot.isBuildingSnapshot()` to detect a real\n  `--build-snapshot` process. A plain invocation keeps Node's web globals intact,\n  installs `__RUNTIME_REQUIRE` before the bundle IIFE, and follows the ordinary\n  bundle startup path. Only snapshot construction installs build-time stubs. On\n  restore, V8 does not re-evaluate the file and instead invokes the serialized\n  deserialize main, which installs the same runtime require hook before resuming.\n- `egg-scripts start --bundle` explicitly selects bundled cluster workers.\n  `--bundle-dir` defaults to `./dist-bundle`, and supplies the fixed defaults\n  `<bundle-dir>/app_worker.js` and `<bundle-dir>/agent_worker.js`; either worker\n  path can be overridden explicitly. Snapshot restore is enabled per role only\n  when `--app-snapshot-blob` or `--agent-snapshot-blob` is provided. Blob\n  locations never determine worker paths, and one role's arguments never\n  determine the other's. Bundle path and role options are ignored unless\n  `--bundle` is present. The existing `--snapshot-blob` remains the single-process\n  launcher and takes precedence when it is supplied together with `--bundle`.\n- Snapshot-backed single-process launches and bundled cluster workers reject\n  non-empty `options.require` before startup. A runtime bootstrap module cannot\n  preserve the source worker's before-framework ordering once the bundle graph\n  has been statically evaluated; silently ignoring it would disable\n  instrumentation or patches without failing the deployment.\n- Explicit `externals.force` entries are external, and `ExternalsResolver`\n  auto-detects root `peerDependencies`, root `optionalDependencies`, root\n  dependency packages with native addons, root dependency packages whose optional\n  peer dependencies cannot be resolved, the missing optional peer package names\n  themselves as `extraExternals`, and native optional platform packages as\n  external.\n- `externals.inline` removes an auto-detected external unless the same package\n  name is also listed in `externals.force`.\n- ESM-only packages, `egg`, `@swc/helpers`, and `@eggjs/*` packages are bundled\n  by default unless `externals.force` or dependency metadata applies. If a\n  wrapper around native optional platform packages cannot be loaded through\n  `createRequire`, the wrapper stays bundled and the platform packages are kept\n  external.\n- `BundlerConfig.tegg` is accepted but intentionally not wired into the current\n  implementation yet.\n\n### Snapshot lifecycle boundary\n\nWith `snapshot: true`, Egg executes `configWillLoad` during snapshot construction\nand stops before `configDidLoad`. After V8 restores the heap, Egg runs the\nregistered `snapshotDidDeserialize` hooks and then resumes the ordinary lifecycle\nfrom `configDidLoad`. Function-style app and agent boot hooks are registered as\n`configDidLoad` hooks, so they already run on the runtime side of this boundary.\n\nPlugin constructors and `configWillLoad` may prepare only serializable\nconfiguration or metadata. Runtime resources such as cluster clients, sockets,\nservers, filesystem watchers, timers, and native clients must be created in\n`configDidLoad` or later. A plugin that consumes another plugin's runtime object\nmust also declare that plugin dependency; this makes the corresponding\n`configDidLoad` order explicit instead of relying on incidental discovery order.\n\n`EggApplicationCore.clusterWrapper()` enforces this contract by throwing during\nsnapshot construction and becoming available after restore. The watcher plugin\nis the reference pattern: its boot constructor stores the app only,\n`configDidLoad` creates and wires the watcher cluster client, and `didLoad` waits\nfor readiness. A generic deferred proxy is intentionally not used because it\nwould silently record arbitrary calls, cover only clients created through\n`clusterWrapper()`, and move failures away from the actual lifecycle violation.\n\nFramework-owned resources that necessarily exist before the cutoff may instead\nimplement a symmetric `snapshotWillSerialize`/`snapshotDidDeserialize` pair, as\nEgg does for its messenger and logger transports. That mechanism is for explicit\nresource ownership, not a substitute for moving plugin runtime initialization to\n`configDidLoad`.\n\n### Snapshot lazy-external defaults\n\nIn `snapshot: true` mode the bundler keeps a set of modules **lazy-external** so a\nV8 startup snapshot stays serializable: each is emitted as an `externalRequire`,\nleft out of the build-time heap (a member-proxy stub from the prelude's\n`__makeLazyExt`), and forwarded to the real module at restore via\n`globalThis.__RUNTIME_REQUIRE`. The injected hook also lazy-stubs any **non-builtin**\nexternal at build (`!__isBuiltin(id)`); the explicit list mainly exists to (a) cover\nbuiltins the `!isBuiltin` rule skips and (b) **force npm packages external** that\nwould otherwise be inlined.\n\n- `DEFAULT_SNAPSHOT_LAZY_MODULES` (in `src/lib/prelude.ts`) covers the Node network\n  stack (`http`/`https`/`http2`/`tls`/`dns`), `inspector`, `cluster`/`node:cluster`,\n  **and egg's HTTP client stack `undici` + `urllib`**. `cluster` is runtime-sensitive:\n  Node chooses its primary or worker implementation when the module is first loaded,\n  while snapshot construction happens outside a cluster worker. Keeping both module\n  specifiers lazy ensures each restored worker loads the worker implementation\n  instead of retaining the builder's primary implementation. Egg builds its\n  `HttpClient` (urllib → undici) during boot, and undici instantiates an llhttp\n  `WebAssembly` (disabled under\n  `--build-snapshot`) + `HTTPParser` that cannot be serialized. As npm packages\n  urllib/undici would be inlined; listing them forces them external (`Bundler` adds\n  the lazy ids to the externals map) so the member-proxy stub is used at build — an\n  app gets a serializable snapshot without listing them in `egg.snapshot.lazyModules`.\n- The member-proxy records the build-time access path (`get`/`apply`/`construct`)\n  and replays it against the real module on restore, so\n  `class HttpClient extends urllib.HttpClient` (and urllib's own\n  `class BaseAgent extends undici.Agent`) keep\n  working: the `extends` is evaluated against the build stub, then `super(...)` /\n  inherited methods resolve to the real base class after deserialization. The\n  first successful replay is memoized, so an instance returned by a recorded call\n  or constructor keeps its identity and subsequent writes are observable.\n- Proxy reads and writes preserve the inherited receiver at restore. Static or\n  prototype accessors on the real base therefore observe the application subclass\n  or instance as `this`, while direct proxy access still uses the real exported\n  object. This is required by symbol-backed model state such as Leoric's\n  `Bone.synchronized` accessor.\n\n#### Leoric compatibility boundary\n\nSnapshot mode intentionally forces the Leoric core into the bundle. Keeping the\nwhole package external was rejected because application/TEGG model modules are\nevaluated while constructing the snapshot: subclasses, prototype chains, and ORM\nmetadata would then be created against a lazy external proxy. Restore can forward\nmember access to the real package, but it cannot replace prototype and metadata\nidentity that is already frozen into the heap. Deferring every model module to\nruntime would avoid that proxy boundary, but would also move the ORM model graph\nout of the snapshot and require Leoric/TEGG-specific lazy-loading behavior.\n\nUnmodified Leoric cannot be safely inlined either. Its driver, realm, and migration\npaths contain expression-based CommonJS `require(...)` calls that `@utoo/pack`\ncannot statically enumerate. A single-file build would otherwise fail on the\ndynamic expression or retain runtime loads for optional drivers and filesystem\nmodules that are not represented by the bundle graph.\n\nThe compatibility layer is therefore a scoped compromise:\n\n- `resolveLeoricSnapshotCompatibility()` removes `leoric` from auto-detected\n  externals and rejects attempts to force it external or add it to snapshot lazy\n  modules.\n- Snapshot builds resolve both imports and requires to Leoric's CommonJS entry.\n  Leoric 2.16 also exports an ESM entry under `dist/`; selecting it would bypass\n  the compatibility rule for `lib/` and expose optional database imports to the\n  bundler.\n- A module rule applies only to the known Leoric source files and rewrites their\n  runtime-selected requires to `globalThis.__RUNTIME_REQUIRE(...)`. Leoric core and\n  model semantics stay bundled, while database clients remain runtime dependencies.\n  The rule also handles TypeScript's compiled dynamic-import callback while\n  preserving its Promise and module namespace conversion.\n- The loader fails the build if an unrecognized expression-based require remains,\n  rather than silently emitting a partial artifact. Migration files used at runtime\n  still need the documented runtime-asset copy configuration.\n\nInference: this is deliberately package- and version-sensitive. A Leoric release\nthat moves these files or changes the require expressions can invalidate the path\ncondition or rewrite patterns, so upgrades require the unit and real-build coverage\nto pass. The shim should be removed when Leoric no longer needs expression-based\nruntime loading, or when `@utoo/pack` provides an equivalent supported runtime\ndynamic-require contract without modifying Turbopack.\n\n#### When does a new dependency need adding?\n\nOnly a package that **directly** creates non-serializable native/WASM state at\nmodule-eval or boot-time instantiation needs a list entry (like `undici`, which\ncompiles llhttp WASM). A package that only reaches the network/native stack\n**transitively** is already covered, because the underlying builtins are lazy:\n\n- `@modelcontextprotocol/sdk` (a default tegg-controller dep, loaded at boot via\n  `tegg/plugin/controller` → `MCPControllerRegister`) → its StreamableHTTP\n  transport `require`s `@hono/node-server`, which does a top-level\n  `require(\"http2\")` + `class extends globalThis.Request`. `http2` is already lazy\n  and `globalThis.Request` is stubbed by the prelude, and no MCP transport/server\n  is instantiated at boot — so the SDK does **not** need a list entry.\n- `@grpc/grpc-js`, `ws` — not present in OSS tegg; gRPC would route through the\n  already-lazy `http2` anyway. No entry needed.\n\n`Inference:` audited 2026-06-28 against `tegg/plugin/controller`, default egg\nplugins, and the suspect packages' module-eval closures. The opt-in\n`mcp-client`/`mcp-proxy`/`langchain` plugins pull the SDK _client_ transports\n(`eventsource`/`cross-spawn`/`pkce-challenge`); if an app enables those and\nsnapshots, re-assess via `egg.snapshot.lazyModules` — that is an app concern, not\na framework default.\n\n## Standalone Worker Bundle (Cloudflare Workers)\n\n`StandaloneWorkerBundler` (`src/lib/StandaloneWorkerBundler.ts`) bundles a tegg\n**standalone service worker** (`@eggjs/service-worker`, see the\n[service worker page](./service-worker.md)) for Cloudflare workerd. This is a\nseparate path from the Egg app `Bundler`/`EntryGenerator` flow above — there is no\n`startEgg`/`app.listen`; the artifact is a fetch handler.\n\n**CLI.** `egg-bin bundle` drives it as a one-shot command (mirroring the egg-app\n`egg-bin bundle`): a standalone bundle is selected by `--entry` (or `--target\nstandalone`); `--framework` names the app package (e.g. `@eggjs/service-worker`),\nwhich must export `loadMetadata` (the standalone counterpart of the egg app's\nframework specifier — no separate app-module/app-export flags). The command runs\n`loadMetadata` + `StandaloneWorkerBundler` internally, so the caller never threads\nthe manifest by hand. The metadata scan runs in an `egg-bin` child process, where\nthe CLI's detected TypeScript compiler and configured `--require`/`--import`\nhooks are active; a TypeScript standalone app therefore does not need to wrap the\nbundle command in its own `NODE_OPTIONS`. `tools/egg-bin/src/commands/bundle.ts`\nbranches on the target, and `tools/egg-bin/scripts/standalone-metadata.mjs`\nperforms the scan.\n\n**Injection-based seam.** The bundler does NOT synthesize the host entry. The user\nauthors a plain, locally-runnable `worker.ts`\n(`new ServiceWorkerApp(dir)` + `export default { fetch }`, or `addEventListener`).\n`run()` then:\n\n1. Filters the caller-supplied tegg manifest (`ServiceWorkerApp.loadMetadata`)\n   by `excludeModules` — a general escape hatch (teggDal no longer needs it: the\n   bundle-mode dynamic-loader fix below makes a scanned-but-unused DAL module load\n   cleanly).\n2. Writes a **build-managed copy of the user's `entry` beside it** (a\n   `.egg-worker-entry.ts` sibling, so the user's relative imports and\n   `import.meta` resolve unchanged), prepending an injected prelude that\n   static-imports every decorated file (so Turbopack bundles them) and installs\n   `globalThis.__EGG_BUNDLE_MODULE_LOADER__` + `globalThis.__EGG_BUNDLE_MANIFEST__`.\n   Evaluation order is safe because import declarations load before module bodies,\n   so the globals are set before the user's `new ServiceWorkerApp(dir)` runs.\n   `StandaloneApp.init` falls back to `__EGG_BUNDLE_MANIFEST__` when no `manifest`\n   option is passed, so the same `worker.ts` runs bundled (global manifest) and\n   unbundled under Node (runtime fs scan).\n3. Runs `PackRunner` (`singleFile: true`, `useDefineForClassFields: true` — the\n   `false` egg-app default would erase uninitialized `#x?: T;` private fields while\n   the code still references `this.#x`), with node builtins external and\n   `resolve.alias` mapping `globby`/`os`/`node:os` to inert stubs (bundle mode never\n   globs; workerd's `nodejs_compat` has no `node:os`).\n4. Applies `patchImportMetaInContent` (`src/lib/importMetaPatch.ts`) to fix\n   Turbopack's broken `import.meta` shim (`__turbopack_context__.F` is otherwise\n   undefined — breaks in Node AND workerd), writes the result as `worker.cjs`, and\n   deletes the injected copy.\n5. Emits the entry per `format`: `module` (default) → an ESM wrapper `index.mjs`\n   (`import worker from './worker.cjs'; export default worker.default;` — a\n   Cloudflare module worker); `service-worker` → no wrapper, the entry is\n   `worker.cjs` itself (a classic, non-module script whose\n   `addEventListener('fetch')` registered on evaluation; an ESM wrapper would move\n   it to module scope where workerd does not dispatch fetch events).\n\n`wrangler.jsonc` sets `nodejs_compat` (tegg needs `AsyncLocalStorage`) and points\n`main` at the wrapper. Verified on Node and workerd (`wrangler dev`): the example's\n`GET /hello/` and `POST /mcp/calc/stream` both return 200. Example:\n`examples/helloworld-service-worker/{worker.ts,wrangler.jsonc}`, built via\n`npm run bundle:cf` (`egg-bin bundle --framework @eggjs/service-worker --entry worker.ts`).\n\n**Bundle-mode dynamic module loading.** Egg and standalone adapt the shared\n`TeggManifest.moduleDescriptors[].decoratedFiles` data through the same\n`createTeggManifestLoaderFS()` helper. The result is a `ManifestLoaderFS`\noverlay: manifest-indexed decorated files are authoritative, while the host's\nnormal loader view may remain as a fallback for unrelated files.\n\nThe host passes that view into the initial `LoaderFactory.loadApp()` scan.\n`ModuleLoader.createModuleLoader()` then installs an explicitly supplied view in\nthe current application's `TeggScope`; later loader creation in the same app\nreuses it. This matters for dynamic multi-instance discovery: DAL's\n`DataSource.getObjects()` still creates a module loader and calls `load()` to\nfind table classes, but in a bundle that scan now reads the scoped manifest view\ninstead of the unavailable runtime filesystem. No class list is added to\n`MultiInstancePrototypeGetObjectsContext`, and generic `LoaderFS` construction\nremains side-effect free.\n\nThe common `ModuleLoader` does not read bundle globals. It first asks its\n`LoaderFS` for an authoritative file list. `ManifestLoaderFS` returns the exact\nmanifest-indexed decorated files, including TypeScript-origin keys and an\nauthoritative empty list; only a source without such a view falls back to\n`LoaderUtil.filePattern()` plus `LoaderFS.glob()`. The standalone host uses\n`globalThis.__EGG_BUNDLE_MANIFEST__` only as an entry source for the shared\nmanifest and constructs the same loader view as Egg.\n\nThis source-level distinction matters when `egg-scripts start` sets\n`EGG_TS_ENABLE=false`: real filesystem discovery still excludes TypeScript,\nwhile `.ts`/`.mts`/`.cts` manifest keys remain discoverable because they resolve\nfrom the in-memory bundle module map rather than files Node must execute. No\nbundle-specific condition is added to `LoaderUtil`.\n\n### Startup benchmark methodology\n\nThe module graph and lifecycle through `configWillLoad` are already captured in\nthe blob, so restore mainly runs runtime initialization from `configDidLoad`\nonward, including `didReady` and connection/listening. The cnpmcore comparison\nexecutes the snapshot-ready JavaScript artifacts normally for the bundle\nbaseline, then restores blobs built from those same files. On Node.js 24.18.1\nand Apple M1 Pro, it runs one warm-up plus ten interleaved measurements per mode.\nSingle process measures direct Node.js spawn through listening; cluster uses the\nmaster's internal orchestration-to-ready timer to exclude launcher and master\nbootstrap. The recorded medians were 947 ms vs 379 ms for single process and\n1356 ms vs 591 ms for cluster, or 2.50x and 2.30x faster restores.\n\nModule identity follows a separate path. Bundle hosts obtain the name from the\nshared `TeggManifest`, normal hosts resolve it while scanning module config, and\nboth store it on `ModuleDescriptor`. `GlobalGraph.moduleConfigList` then carries\nthat name into `LoadUnitFactory`; neither `LoaderFS` nor `LoaderFactory` resolves\n`unitName`. Manifest reference/descriptor names for the same unit path are\nvalidated for consistency.\n\n**Format targets differ.** The `module` format runs on Cloudflare workerd. The\n`service-worker` format targets Web Service Worker / edge runtimes that expose a\nglobal `addEventListener('fetch')` — it is **not** a workerd target: workerd's\n`nodejs_compat` (required for tegg's `AsyncLocalStorage`) only supports the\nmodule-worker format, and `wrangler` rejects a service-worker-format script that\nimports Node builtins (`Unexpected external import of \"assert\"…, no default\nexport`). The minimal `helloworld-service-worker` example intentionally demonstrates\nonly the deployable module-worker path; the classic format remains available through\nthe bundler and CLI for compatible hosts.\n","frontmatter":{"title":"Egg Bundler","type":"package","summary":"Bundles Egg applications for Node startup snapshots and tegg standalone service-worker targets.","source_files":["packages/core/src/lifecycle.ts","packages/egg/src/lib/egg.ts","packages/cluster/src/worker_protocol/worker-thread.ts","packages/cluster/src/utils/mode/impl/worker_threads/agent.ts","packages/cluster/src/utils/mode/impl/worker_threads/app.ts","plugins/watcher/src/lib/boot.ts","tools/egg-bundler/src/index.ts","tools/egg-bundler/src/compat/leoric/index.ts","tools/egg-bundler/src/compat/leoric/runtime-require-loader.cjs","tools/egg-bundler/src/lib/Bundler.ts","tools/egg-bundler/src/lib/EntryGenerator.ts","tools/egg-bundler/src/lib/ExternalsResolver.ts","tools/egg-bundler/src/lib/prelude.ts","tools/egg-bundler/src/lib/PackRunner.ts","tools/egg-bundler/src/lib/StandaloneWorkerBundler.ts","tools/egg-bundler/src/lib/importMetaPatch.ts","tools/egg-bin/src/commands/bundle.ts","tools/egg-bin/src/commands/snapshot.ts","tools/scripts/src/commands/start.ts","packages/loader-fs/src/index.ts","packages/loader-fs/src/manifest_loader_fs.ts","tegg/core/types/src/metadata/model/TeggManifest.ts","tegg/core/loader/src/LoaderFactory.ts","tegg/core/loader/src/LoaderUtil.ts","tegg/core/loader/src/TeggManifestLoaderFS.ts","tegg/core/loader/src/impl/ModuleLoader.ts","tegg/plugin/dal/src/lib/DataSource.ts","tegg/plugin/tegg/src/lib/EggModuleLoader.ts","tegg/standalone/standalone/src/EggModuleLoader.ts","tools/egg-bundler/docs/output-structure.md","site/docs/advanced/snapshot.md","site/docs/zh-CN/advanced/snapshot.md","examples/helloworld-service-worker"],"updated_at":"2026-10-06","status":"active"},"isInternal":false,"tokens":6027,"sizeBytes":26189},{"name":"loader-fs.md","path":"wiki/packages/loader-fs.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/wiki/packages/loader-fs.md","title":"Packages Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Loader FS Package\ntype: package\nsummary: Shared loader-facing filesystem boundary for Egg loaders and future bundled runtimes.\nsource_files:\n  - packages/loader-fs/src/index.ts\n  - packages/loader-fs/src/manifest_loader_fs.ts\n  - tegg/core/loader/src/LoaderFactory.ts\n  - tegg/core/loader/src/TeggManifestLoaderFS.ts\n  - tegg/core/types/src/scope/TeggScope.ts\n  - tegg/plugin/dal/src/lib/DataSource.ts\n  - packages/loader-fs/package.json\nupdated_at: 2026-08-03\nstatus: active\n---\n\n# Loader FS Package\n\n`@eggjs/loader-fs` owns the small filesystem interface used by loader code. It\nexports `LoaderFS`, `LoaderFSGlobOptions`, `RealLoaderFS`, and\n`ManifestLoaderFS`.\n\n`LoaderFS` intentionally covers only loader-facing operations: `exists`, `stat`,\n`realpath`, `readJSON`, `glob`, and `loadFile`. It is not a Node.js `fs`\npolyfill. A source may additionally implement `getKnownFiles(directory)` when\nit has an authoritative, precomputed view. `undefined` means the source has no\nsuch view and discovery should fall back to `glob`; an empty array is an\nauthoritative empty directory.\n\n`RealLoaderFS` is the default implementation for normal non-bundled runtime. It\ndelegates file checks to Node `fs`, JSON reads to `utility.readJSONSync`, glob\ndiscovery to `globby.sync`, and module loading to the same `@eggjs/utils`\n`importModule()` path used by the core loader.\n\n`ManifestLoaderFS` presents a host-neutral manifest file index through the same\ninterface. Its input contains only `baseDir`, `fileDiscovery`, and\n`resolveCache`; it does not depend on Egg's `ManifestStore` or on tegg types.\nEgg can pass `ManifestStore` structurally, while standalone uses\n`createTeggManifestLoaderFS()` to adapt\n`TeggManifest.moduleDescriptors[].decoratedFiles` into the same file index.\nManifest directories expose their exact indexed files through `getKnownFiles`,\nincluding TypeScript-origin source keys and authoritative empty lists. Unknown\ndirectories delegate to the fallback source.\n\n`@eggjs/core` depends on this package and re-exports its public API so existing\ncore consumers can still import the loader filesystem boundary from core while\ntegg and later bundled runtime packages can depend on the smaller package\ndirectly.\n\n`ModuleLoader` first asks its `LoaderFS` for an authoritative file list. This\nlets a manifest source control discovery without leaking bundle state into the\ngeneric loader. Sources without such a list keep the normal behavior:\n`LoaderUtil.filePattern()` selects supported runtime extensions and\n`LoaderFS.glob()` scans the directory. Consequently `EGG_TS_ENABLE=false`\ncontinues to exclude real TypeScript files, but it does not discard `.ts` keys\nthat a manifest resolves from an in-memory bundle module map.\n\n`LoaderFS` intentionally does not resolve a tegg module name. The host resolves\nidentity onto `ModuleDescriptor`, `GlobalGraph.moduleConfigList` carries it, and\n`LoadUnitFactory` consumes it. LoaderFS only controls which files are visible to\n`ModuleLoader`.\n\nEach host supplies its selected file view when it starts the initial module\nload. `ModuleLoader.createModuleLoader()` is the integration point that installs\nthat view as the current `TeggScope`'s module-loader filesystem; without a\nhost-supplied view it initializes one `RealLoaderFS`. A later explicit view is\nauthoritative, so a manifest-backed view can replace an earlier real-filesystem\ndefault. Generic `LoaderFS` constructors remain side-effect free and\nhost-neutral. Later module loaders created without an explicit filesystem see\nthe same per-app file view as the initial module scan. This lets dynamic\nmulti-instance callbacks such as DAL keep their existing loader-based discovery\nwithout adding host or loader state to their public context. The scoped value is\ndropped with the app's scope bag and remains isolated between concurrent apps.\n","frontmatter":{"title":"Loader FS Package","type":"package","summary":"Shared loader-facing filesystem boundary for Egg loaders and future bundled runtimes.","source_files":["packages/loader-fs/src/index.ts","packages/loader-fs/src/manifest_loader_fs.ts","tegg/core/loader/src/LoaderFactory.ts","tegg/core/loader/src/TeggManifestLoaderFS.ts","tegg/core/types/src/scope/TeggScope.ts","tegg/plugin/dal/src/lib/DataSource.ts","packages/loader-fs/package.json"],"updated_at":"2026-08-03","status":"active"},"isInternal":false,"tokens":858,"sizeBytes":3848},{"name":"onerror.md","path":"wiki/packages/onerror.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/wiki/packages/onerror.md","title":"Packages Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Onerror Plugin\ntype: package\nsummary: Default Egg error-handling plugin and configurable response negotiation layer.\nsource_files:\n  - plugins/onerror/README.md\n  - plugins/onerror/src/app.ts\n  - plugins/onerror/src/lib/onerror.ts\n  - plugins/onerror/src/lib/error_view.ts\n  - plugins/onerror/src/config/config.default.ts\nupdated_at: 2026-05-06\nstatus: active\n---\n\n# Onerror Plugin\n\n`@eggjs/onerror` is the default Egg error-handling plugin. It installs\n`ctx.onerror` and supports configurable handlers for HTML, text, JSON, JS\n(JSONP), redirect, and catch-all error responses.\n\n## Public Configuration\n\n- `errorPageUrl` redirects production HTML requests after unexpected errors.\n- `accepts` customizes content negotiation.\n- `all`, `html`, `text`, `json`, and `js` customize response handling for\n  specific response types.\n- `appErrorFilter` can suppress logging for selected errors.\n\n## Current Behavior\n\nThe plugin owns its Koa-style `onerror()` implementation in\n`plugins/onerror/src/lib/onerror.ts` instead of importing it from\n`koa-onerror`. This keeps the response behavior local to the plugin and avoids\nreading `koa-onerror` package templates when the plugin app boot hook is\nimported, which is important for static bundling.\n\nDefault HTML error-page rendering still flows through `ErrorView` and the\nplugin's local default template. Applications can continue to provide custom\ntemplates and handlers through the documented `config.onerror` options.\n","frontmatter":{"title":"Onerror Plugin","type":"package","summary":"Default Egg error-handling plugin and configurable response negotiation layer.","source_files":["plugins/onerror/README.md","plugins/onerror/src/app.ts","plugins/onerror/src/lib/onerror.ts","plugins/onerror/src/lib/error_view.ts","plugins/onerror/src/config/config.default.ts"],"updated_at":"2026-05-06","status":"active"},"isInternal":false,"tokens":333,"sizeBytes":1472},{"name":"service-worker.md","path":"wiki/packages/service-worker.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/wiki/packages/service-worker.md","title":"Packages Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Standalone service worker stack\ntype: package\nsummary: Fetch-semantics standalone runtime — HTTP controllers and MCP tools served from a tegg module without an egg application\nsource_files:\n  - tegg/standalone/service-worker-runtime/src\n  - tegg/standalone/service-worker/src\n  - tegg/standalone/service-worker-controller/src\n  - tegg/standalone/standalone/package.json\n  - tegg/standalone/standalone/src/StandaloneApp.ts\n  - tegg/core/controller-runtime/src\n  - tegg/core/controller-decorator/src\n  - tegg/core/types/src/controller-decorator\n  - tegg/plugin/controller/src\n  - examples/helloworld-service-worker\n  - tools/egg-bundler/src/lib/StandaloneWorkerBundler.ts\nupdated_at: 2026-08-06\nstatus: active\n---\n\nThree packages provide the standalone service worker runtime on top of the\nmodule-plugin mechanism (declarative `@InnerObjectProto` /\n`@XxxLifecycleProto` hooks, see the module-plugin pages):\n\n- `@eggjs/service-worker-runtime` — protocol-agnostic: `@Runner()` entry\n  dispatching events to `@EventHandlerProto('<type>')` handlers, event\n  injection into ContextProtos, `BackgroundTaskHelper` re-export\n  (ctx-destroy draining).\n- `@eggjs/service-worker-controller` — the fetch controller transport (the\n  `serviceWorker` eggModule): `FetchEventHandler`, `FetchRouter` + fetch\n  parameter binding (body/param/query/queries/headers/cookies/request via the\n  `@HTTP*` decorators), `HTTP/MCP RegisterProvider`, `ServiceWorkerMcpRouter`,\n  MCP stateless streamable HTTP under `/mcp[/name]/stream`. `@HTTPCookies()`\n  binds a fetch-native `ServiceWorkerCookies` — an `@eggjs/cookies`-compatible\n  `get`/`set` (reads the `Cookie` header, writes `Set-Cookie` onto\n  `ctx.responseHeaders`) — chosen over `@eggjs/cookies`, which has app coupling\n  (`app.emit('cookieLimitExceed')`, unguarded) + heavy egg-flavored deps unwanted\n  at the edge. Signing/encryption are not implemented (inject `@eggjs/cookies` if\n  needed). Users annotate with the `Cookies` type from `@eggjs/tegg`;\n  `ServiceWorkerCookies` is the internal impl.\n- `@eggjs/service-worker` — the host app only: the `ServiceWorkerApp` facade\n  over `StandaloneApp` with `serve()` (node:http bridge) and embedded\n  `handleEvent()`. Its single frameworkDep is its OWN package root, so the\n  framework modules (runtime + controller) are auto-discovered from its package\n  deps via the node_modules eggModule convention — no hand-listed/ordered\n  packages. Its `index` exports only `ServiceWorkerApp`; consumers import\n  controller decorators and middleware contracts from `@eggjs/tegg`, and the\n  fetch context type from `@eggjs/tegg/standalone`. The transport package remains\n  a host adapter rather than a user-facing controller API.\n\nKey mechanics and constraints:\n\n- **App-wide `config` inner object = the entry app module's `module.yml`**:\n  `StandaloneApp` designates the module scanned from `baseDir` (its module dir IS\n  the cwd) as the entry app module and exposes its `module.yml` as the app-wide\n  `config` inner object (`@Inject() config`), the single config surface — there is\n  no programmatic `config` override. Subsystems read their own slice:\n  `config.backgroundTask.timeout` (BackgroundTaskHelper), `config.mcp` (transport\n  selection + DNS-rebinding options `allowedHosts`/`allowedOrigins`/\n  `enableDnsRebindingProtection`). Config VALUES live in the app's `module.yml`\n  (env variants via `module.<env>.yml`); capability OBJECTS (`mcpAuthHandler`,\n  `fetchContextFactory`, `errorResponseMapper`) are `@InjectOptional()` inner\n  objects a host supplies through the generic `innerObjectHandlers` seam, each\n  with a default when absent (allow-all auth, plain context, unmapped error). So\n  `ServiceWorkerApp` has no options of its own — `ServiceWorkerAppOptions` is an\n  alias of `StandaloneAppOptions`. `config` joins `logger`/`moduleConfigs`/\n  `moduleConfig`/`runtimeConfig` as a framework-owned inner object (a host-provided\n  `config` handler is ignored).\n- **Four-package controller layering** (mirrors the egg host): host-agnostic\n  runtime `@eggjs/controller-runtime` (a plain LIBRARY, not an eggModule — base\n  register classes, collect-only `MCPControllerRegister`, `McpRouter`/`Router`\n  abstractions, `MCPServerHelper`, and the controller inner-object prototypes)\n  → egg transport `@eggjs/controller-plugin` (the `teggController` plugin\n  module) → fetch transport `@eggjs/service-worker-controller` (the\n  `serviceWorker` module) → host app `@eggjs/service-worker`. Each HOST package\n  owns the scanned eggModule and re-exports the runtime's inner-object protos\n  into it (`runtimeProtos.ts`); the runtime library itself is never scanned.\n  The service worker depends only on the egg-free runtime, never on the egg\n  plugin.\n- **Controller Advice executes at the bound invocation boundary**: applications\n  apply an `@Advice` class with `@Middleware` on a controller or method. The shared\n  runtime resolves every such class through the active host container and composes\n  only its `around()` hook around the terminal HTTP or MCP invocation; a class with\n  no `around()` hook simply advances to the next middleware. Argument binding\n  happens before the chain. HTTP writes the method result to the host response\n  before `next()` unwinds. MCP method-level Advice wraps the bound SDK callback;\n  controller-level Advice remains outside transport dispatch so the legacy\n  middleware contract can inspect or replace the streaming response after\n  `next()`.\n  `AbstractControllerAdvice.around()` forwards the host context, `next`, and\n  `AdviceContext` to the existing `middleware()` contract. Explicit `@Pointcut`\n  remains an independent AOP path. Controller packages do not depend on the AOP\n  package, and host context is passed directly with no process-global context map.\n- **Host-agnostic MCP register + `McpRouter` boundary**: the shared\n  `MCPControllerRegister` (`@eggjs/controller-runtime`) only COLLECTS tool/resource/prompt\n  records and delegates transport to an injected `McpRouter`\n  (`registerServer(reg)`); it no longer touches `app`. The egg host's node-HTTP\n  transport is `EggMcpRouter` (built imperatively in the controller plugin's\n  `app.ts`, which holds `app`), the service worker's fetch transport is\n  `ServiceWorkerMcpRouter`. Both provide the `mcpRouter` DI object; the two host\n  plugins are never used together, so the shared name does not collide. Because\n  the register needs no `app`, the egg host no longer provides `eggApp` as a\n  module inner object — the Egg `Application` is out of the module DI graph.\n  Like HTTP, per-proto `register()` only collects during load-unit creation;\n  one `MCPControllerRegister.doRegister()` groups the complete records by\n  server name and calls `mcpRouter.registerServer()` once per server. Egg\n  finalizes on the last `CONTROLLER_LOAD_UNIT`; the fetch host finalizes while\n  constructing its routes on the first event. Each host's `registerServer()`\n  mounts directly — there is no second router `doRegister()` phase.\n  Each collected record contains only its controller proto and MCP metadata;\n  the host passes its container factory once when it creates an\n  `MCPServerHelper`, matching the HTTP register's dependency flow. The helper's\n  request callback resolves the controller lazily in the active context.\n- **HTTP registration has separate validation and mutation phases**:\n  `HTTPControllerRegister` creates each `HTTPMethodRegister` once in priority\n  order, calls `checkDuplicate()` on all of them, then calls `register()` on all\n  of them. `checkDuplicate()` checks the real host router and, for host-gated\n  routes, tracks same-host rules in the existing `checkRouters` map. The first\n  phase is not a temporary copy of the complete host router.\n- **MCP SDK >= 1.29 stateless transports are single-shot** (reuse throws), so\n  each MCP request builds a fresh `MCPServerHelper` + web-standard transport\n  from register records collected at boot. The service worker's\n  `ServiceWorkerMcpRouter` uses `WebStandardStreamableHTTPServerTransport`\n  (Request in, Response out) — no node req/res bridging. Auth is an\n  `mcpAuthHandler` extension point (`@InjectOptional`, via `innerObjectHandlers`;\n  absent → allow); DNS-rebinding options are\n  read from `config.mcp` (see the app-wide config bullet).\n- **Config-selected transport provider** (`config.mcp.transport` +\n  `ServiceWorkerMcpRouter.registerTransport`): `config.mcp.transport` (from the\n  app's `module.yml`) selects which transport mounts per app — the built-in\n  `'web'` (web-standard streamable HTTP, the default) or a host-registered\n  alternative by name. A host registers an alternative via\n  `registerTransport(name, provider)` and points its app config at it\n  (`mcp.transport: <name>`); the provider then fully OWNS the server's transport\n  (mutually exclusive with the built-in), so a host can select an alternative\n  transport (e.g. a node-based SSE `/sse`+`/messages`, which the SDK has no\n  web-standard equivalent for, plus streamable) WHOLESALE without forking the\n  router, a facade `mcp` option, an IoC override, or a module swap. The provider\n  receives an `McpServerMountContext` (the `FetchRouter`, live `registration`,\n  `serverName`/`basePath`, and shared\n  `authenticate`/`createServerHelper`/`selectMiddlewares`/`compose` so it reuses\n  the same auth gate, MCP server helper, and middleware pipeline). The registry\n  is `TeggScope`-scoped per app; an unknown name falls back to the built-in, and\n  node:http stays entirely in the host that registers the alternative. Egg's\n  separate `EggMcpRouter` keeps its extension hooks directly on the router\n  instance.\n- **Streaming lifecycle**: a streaming body keeps pulling from ContextProto\n  objects after the runner returns, but the tegg context is destroyed at return.\n  `FetchEventHandler.#guardResponseStream` tees the body — the client consumes\n  one branch, and the request context's `preDestroy`\n  (`EggContextLifecycleUtil.registerObjectLifecycle`) awaits the other branch\n  draining to a sink — so the ContextProto objects the stream reads from stay\n  alive until the source is fully produced, then the context tears down.\n  (No `config.backgroundTask.timeout` cap, so a legitimately long stream — SSE —\n  is never cut short.)\n- **Unified errors**: framework failures reply `{ code, message }` JSON\n  (`NOT_FOUND` / `INTERNAL_SERVER_ERROR`); `ctx.responseHeaders` merge onto\n  the final response.\n- **frameworkDeps scan excludes `test/**`\\*\\*: the framework packages are\n  themselves modules; without the exclusion their test fixtures load as\n  business modules (duplicate controller names) in workspace layouts.\n- **The runner and business modules share the PUBLIC `eggObjectFactory`**:\n  `@eggjs/standalone` directly depends on `@eggjs/dynamic-inject-runtime`, whose\n  module provides the canonical PUBLIC factory. `StandaloneApp` always scans its\n  own package root as an independent built-in root before host `frameworkDeps`,\n  so this does not rely on recursively scanning framework dependency trees.\n  `ServiceWorkerRunner` injects that public factory to dispatch\n  `AbstractEventHandler` by event type; the service-worker runtime does not\n  provide a second PRIVATE factory. `@eggjs/ajv-plugin` remains opt-in and is\n  discovered only when an application or selected framework root brings it into\n  the standalone module references.\n- Per-app mutable state is owned by the app's TeggScope bag, either through its\n  inner objects or explicit scope slots such as the manifest loader view and MCP\n  transport registry. Two concurrent `ServiceWorkerApp`s are isolated\n  (`test/MultiApp.test.ts`).\n- **Cloudflare Workers bundle (injection-based)**: Node runs a `ServiceWorkerApp`\n  directly (runtime fs scan); workerd has no runtime filesystem, so modules are\n  discovered at build time and inlined by `@eggjs/egg-bundler`'s\n  `StandaloneWorkerBundler` (see the [egg-bundler page](./egg-bundler.md)). The\n  seam keeps the host entry as USER code: a plain `worker.ts`\n  (`new ServiceWorkerApp(dir)` + `export default { fetch }`) that also runs\n  unbundled under Node. The bundler injects the scanned imports + manifest into a\n  build-managed copy beside `worker.ts`; `StandaloneApp.init` falls back to\n  `globalThis.__EGG_BUNDLE_MANIFEST__` (mirroring the existing\n  `__EGG_BUNDLE_MODULE_LOADER__` global) when no `manifest` option is passed, so a\n  single `worker.ts` serves both environments with no build-only import. Verified\n  on workerd (`wrangler dev`): `GET /hello/` + `POST /mcp/calc/stream` both 200.\n\nExample: `examples/helloworld-service-worker` (entry `main.ts` lives outside\nthe scanned `app/` module dir on purpose — the scan imports every module\nfile, and importing an entry that boots the app recurses; `worker.ts` is the\nCloudflare Workers entry, built by `npm run bundle:cf` through `egg-bin bundle`).\n","frontmatter":{"title":"Standalone service worker stack","type":"package","summary":"Fetch-semantics standalone runtime — HTTP controllers and MCP tools served from a tegg module without an egg application","source_files":["tegg/standalone/service-worker-runtime/src","tegg/standalone/service-worker/src","tegg/standalone/service-worker-controller/src","tegg/standalone/standalone/package.json","tegg/standalone/standalone/src/StandaloneApp.ts","tegg/core/controller-runtime/src","tegg/core/controller-decorator/src","tegg/core/types/src/controller-decorator","tegg/plugin/controller/src","examples/helloworld-service-worker","tools/egg-bundler/src/lib/StandaloneWorkerBundler.ts"],"updated_at":"2026-08-06","status":"active"},"isInternal":false,"tokens":3122,"sizeBytes":12890},{"name":"tegg-vitest.md","path":"wiki/packages/tegg-vitest.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/wiki/packages/tegg-vitest.md","title":"Packages Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Tegg Vitest Adapter\ntype: package\nsummary: Vitest 5 runner for Egg request contexts and tegg module scopes.\nsource_files:\n  - .github/workflows/ci.yml\n  - .utoo.toml\n  - tegg/core/vitest/package.json\n  - tegg/core/vitest/src/runner.ts\n  - tegg/core/vitest/src/index.ts\n  - tegg/core/vitest/src/setup.ts\n  - tegg/core/vitest/test/fixture_app.test.ts\n  - tegg/core/vitest/test/runner-multi-app.test.ts\n  - plugins/mock/package.json\n  - tools/egg-bin/src/commands/test.ts\n  - tools/egg-bin/src/commands/cov.ts\n  - tools/egg-bin/test/commands/test.test.ts\n  - tools/egg-bin/test/commands/cov.test.ts\n  - tools/create-egg/src/templates/simple-ts/package.json\n  - tools/create-egg/src/templates/tegg/package.json\nupdated_at: 2026-10-06\nstatus: active\n---\n\n## Compatibility\n\n`@eggjs/tegg-vitest` requires `vitest@^5.0.1`. The runner extends `TestRunner`\nfrom `vitest`. `@eggjs/mock` has the same range for its optional Vitest peer.\nVitest 4 is no longer supported.\n\nLifecycle overrides use Vitest 5 argument types and\nforward all arguments, including the file list passed to `onAfterRunFiles`\nand the retry options passed to `onBeforeTryTask`.\nThe monorepo catalog uses `^5.0.1` for Vitest, its V8 coverage provider, and its\nUI. The TypeScript and tegg application templates also use Vitest `^5.0.1`.\n\nThe `test-tegg-vitest` CI job installs the catalog versions on Node.js 24 and\n26, typechecks the adapter, and runs its tests with isolated workers and with\none shared thread worker (`--pool threads --no-isolate --maxWorkers 1`).\n\n`egg-bin` uses the Vitest 5 `startVitest` API with `config: false` to prevent\nparent config discovery. Coverage exclusions are relative to the application\nroot; absolute user patterns are converted to relative patterns before Vitest\nmatches them. `TEST_REPORTER=json` uses Vitest 5's default output file,\n`.vitest/json/output.json` under the application root.\n\n## Context lifecycle\n\n`egg-bin` automatically selects the adapter's `runner` and `setup` entry points.\n`configureTeggRunner()` lets a test file choose its application and mock cleanup\nbehavior. During collection, the runner resolves that application and waits for\n`app.ready()`.\n\nThe runner holds a module scope for each file and each test. Before each test\nattempt, it creates a request context and enters it synchronously, before any\nawaited cleanup. Otherwise, the caller's async continuation retains the previous\ncontext on a retry. The previous attempt's scope is released before the new\nscope starts. After the test, the runner releases the test scope and restores\nthe file's context.\n\nThe fixture application tests cover injected service identity and scope cleanup\nacross retries. The concurrent-app regression drives the runner hooks for two\napps that share a service class. It checks separate request contexts and service\ninstances, retry argument forwarding, and independent scope cleanup.\nRun the adapter suite from the repository root with\n`vitest run --root tegg/core/vitest --config vitest.config.ts`.\n","frontmatter":{"title":"Tegg Vitest Adapter","type":"package","summary":"Vitest 5 runner for Egg request contexts and tegg module scopes.","source_files":[".github/workflows/ci.yml",".utoo.toml","tegg/core/vitest/package.json","tegg/core/vitest/src/runner.ts","tegg/core/vitest/src/index.ts","tegg/core/vitest/src/setup.ts","tegg/core/vitest/test/fixture_app.test.ts","tegg/core/vitest/test/runner-multi-app.test.ts","plugins/mock/package.json","tools/egg-bin/src/commands/test.ts","tools/egg-bin/src/commands/cov.ts","tools/egg-bin/test/commands/test.test.ts","tools/egg-bin/test/commands/cov.test.ts","tools/create-egg/src/templates/simple-ts/package.json","tools/create-egg/src/templates/tegg/package.json"],"updated_at":"2026-10-06","status":"active"},"isInternal":false,"tokens":741,"sizeBytes":3013},{"name":"typings.md","path":"wiki/packages/typings.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/wiki/packages/typings.md","title":"Packages Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Typings Package\ntype: package\nsummary: Shared TypeScript type surface for cross-package Egg typings.\nsource_files:\n  - packages/typings/package.json\n  - packages/typings/src/index.ts\n  - packages/typings/src/global.ts\nupdated_at: 2026-04-26\nstatus: seed\n---\n\n# Typings Package\n\n`@eggjs/typings` is the shared package for type contracts that must be consumed\nby more than one Egg package without creating a dependency on a specific runtime\npackage.\n\nCurrent shared types:\n\n- `BundleModuleLoader` in `packages/typings/src/index.ts`\n- `globalThis.__EGG_BUNDLE_MODULE_LOADER__` augmentation in `packages/typings/src/global.ts`\n\nImport `@eggjs/typings/global` from a package entrypoint when that package needs\nthe shared global augmentation in its type surface.\n","frontmatter":{"title":"Typings Package","type":"package","summary":"Shared TypeScript type surface for cross-package Egg typings.","source_files":["packages/typings/package.json","packages/typings/src/index.ts","packages/typings/src/global.ts"],"updated_at":"2026-04-26","status":"seed"},"isInternal":false,"tokens":180,"sizeBytes":768},{"name":"utils.md","path":"wiki/packages/utils.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/wiki/packages/utils.md","title":"Packages Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Utils Package\ntype: package\nsummary: Shared utility package for Egg module loading, config/plugin helpers, and bundled module-loader integration.\nsource_files:\n  - packages/utils/README.md\n  - packages/utils/src/import.ts\n  - packages/utils/test/bundle-import.test.ts\n  - packages/utils/test/module-importer.test.ts\n  - packages/typings/src/index.ts\n  - packages/typings/src/global.ts\nupdated_at: 2026-06-27\nstatus: active\n---\n\n# Utils Package\n\n`@eggjs/utils` is a shared utility package used across Egg packages.\n\n## Public Surfaces\n\n- `getPlugins(options)`, `getLoadUnits(options)`, `getConfig(options)`, and\n  `getFrameworkPath(options)` expose application/framework discovery helpers.\n- `importModule(filepath, options)` and `importResolve(filepath, options)`\n  centralize runtime module loading.\n- `setBundleModuleLoader(loader)` registers the bundled runtime loader hook.\n\n## Bundled Module Loading\n\n`setBundleModuleLoader(loader)` stores the hook on `globalThis` so bundled and\nexternal copies of `@eggjs/utils` share the same loader. The loader receives the\noriginal `importModule()` filepath after POSIX separator normalization, or a\nvirtual specifier. Returning `undefined` falls back to the normal resolution\npath; other return values follow the same default-export unwrapping rules as\n`importModule()`.\n\nWhen a bundle loader is installed and `importModule()` falls through to native\nESM loading, `packages/utils/src/import.ts` uses an opaque native dynamic import\ncreated with `new Function('specifier', 'return import(specifier);')`. This\nprevents bundlers such as Turbopack from rewriting non-static dynamic import\nexpressions and preserves Node's native fallback for external ESM modules.\n\n## Module-loader hook priority\n\n`importModule()` resolves through three hooks before the native `import()` /\n`require()` fallback (see `packages/utils/src/import.ts`):\n\n1. **`__EGG_BUNDLE_MODULE_LOADER__`** (`setBundleModuleLoader`) — synchronous\n   `globalThis` bundle-map lookup, runs before on-disk resolution and receives\n   the POSIX-normalized filepath. Returning `undefined` falls through.\n2. **Snapshot loader** (`setSnapshotModuleLoader`) — a module-local hook (not a\n   `globalThis` global) used by the V8 snapshot entry generator, keyed by the\n   resolved path. Once registered it handles every load that reaches it, so the\n   importer below is bypassed while it is active.\n3. **`__EGG_MODULE_IMPORTER__`** — async (or sync, since awaited) `globalThis`\n   importer that receives the resolved path (the `importResolve()` result, with\n   OS-native separators — _not_ normalized) and replaces native `await import()`.\n\nThe bundle loader and importer globals are typed in `@eggjs/typings`\n(`BundleModuleLoader`, `ModuleImporter`) and augmented onto `globalThis` in\n`@eggjs/typings/global`. `@eggjs/core`'s `ManifestLoaderFS` (`#loadBundledModule`)\nconsults **only** `__EGG_BUNDLE_MODULE_LOADER__`; the importer/native path is\nreached transitively through its `@eggjs/loader-fs` fallback, which calls back\ninto `importModule()`. The tegg loader (`LoaderUtil.loadFile`) consults both\nglobals directly, but passes the _original_ loader filepath separator-normalized\nto POSIX — not the `importResolve()` output — so the two callers differ in both\nwhich path they pass and whether it is normalized.\n\n`__EGG_MODULE_IMPORTER__` has two contract uses:\n\n- **Bundler-based test runners (Vitest):** route loading through the runner's\n  module graph so loader and test file share one module instance (otherwise\n  `ctx.getEggObject(ClassRef)` throws \"can not get proto\").\n- **V8 startup-snapshot restore:** the deserialized main function has no host\n  dynamic-import callback, so native `import()` throws\n  (`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`). The snapshot entry installs a\n  synchronous `require()`-based importer (`createRequire()` over the output\n  dir); `require()` loads ESM on Node >= 22. Covered by\n  `test/module-importer.test.ts` (inline require importer +\n  `fixtures/module-importer-require-esm/run.mjs`, which asserts the no-callback\n  premise via `node:vm`).\n","frontmatter":{"title":"Utils Package","type":"package","summary":"Shared utility package for Egg module loading, config/plugin helpers, and bundled module-loader integration.","source_files":["packages/utils/README.md","packages/utils/src/import.ts","packages/utils/test/bundle-import.test.ts","packages/utils/test/module-importer.test.ts","packages/typings/src/index.ts","packages/typings/src/global.ts"],"updated_at":"2026-06-27","status":"active"},"isInternal":false,"tokens":956,"sizeBytes":4101},{"name":"README.md","path":"packages/cluster/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/cluster/README.md","title":"cluster Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fcluster%2FREADME.md -->","isInternal":false,"tokens":913,"sizeBytes":4562},{"name":"README.md","path":"packages/cookies/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/cookies/README.md","title":"cookies Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fcookies%2FREADME.md -->","isInternal":false,"tokens":441,"sizeBytes":1663},{"name":"README.md","path":"packages/core/benchmark/middleware/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/core/benchmark/middleware/README.md","title":"middleware Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fcore%2Fbenchmark%2Fmiddleware%2FREADME.md -->","isInternal":false,"tokens":3581,"sizeBytes":12430},{"name":"README.md","path":"packages/core/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/core/README.md","title":"core Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fcore%2FREADME.md -->","isInternal":false,"tokens":1919,"sizeBytes":8101},{"name":"README.md","path":"packages/egg/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/egg/README.md","title":"egg Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fegg%2FREADME.md -->","isInternal":false,"tokens":579,"sizeBytes":1970},{"name":"README.md","path":"packages/errors/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/errors/README.md","title":"errors Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Ferrors%2FREADME.md -->","isInternal":false,"tokens":1728,"sizeBytes":6822},{"name":"README.md","path":"packages/extend2/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/extend2/README.md","title":"extend2 Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fextend2%2FREADME.md -->","isInternal":false,"tokens":369,"sizeBytes":1266},{"name":"README.md","path":"packages/koa-static-cache/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/koa-static-cache/README.md","title":"koa-static-cache Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fkoa-static-cache%2FREADME.md -->","isInternal":false,"tokens":1237,"sizeBytes":4713},{"name":"Readme.md","path":"packages/koa/Readme.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/koa/Readme.md","title":"koa Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fkoa%2FReadme.md -->","isInternal":false,"tokens":1610,"sizeBytes":6441},{"name":"README.md","path":"packages/loader-fs/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/loader-fs/README.md","title":"loader-fs Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Floader-fs%2FREADME.md -->","isInternal":false,"tokens":140,"sizeBytes":590},{"name":"README.md","path":"packages/path-matching/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/path-matching/README.md","title":"path-matching Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fpath-matching%2FREADME.md -->","isInternal":false,"tokens":601,"sizeBytes":2199},{"name":"README.md","path":"packages/router/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/router/README.md","title":"router Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Frouter%2FREADME.md -->","isInternal":false,"tokens":3578,"sizeBytes":14213},{"name":"README.md","path":"packages/skills/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/skills/README.md","title":"skills Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fskills%2FREADME.md -->","isInternal":false,"tokens":7,"sizeBytes":16},{"name":"README.md","path":"packages/supertest/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/supertest/README.md","title":"supertest Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Fsupertest%2FREADME.md -->","isInternal":false,"tokens":2434,"sizeBytes":9322},{"name":"README.md","path":"packages/tsconfig/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/tsconfig/README.md","title":"tsconfig Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Ftsconfig%2FREADME.md -->","isInternal":false,"tokens":343,"sizeBytes":1186},{"name":"README.md","path":"packages/utils/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/packages/utils/README.md","title":"utils Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=packages%2Futils%2FREADME.md -->","isInternal":false,"tokens":1144,"sizeBytes":4700},{"name":"README.md","path":"plugins/cors/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/plugins/cors/README.md","title":"cors Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fcors%2FREADME.md -->","isInternal":false,"tokens":1316,"sizeBytes":5196},{"name":"README.md","path":"plugins/development/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/plugins/development/README.md","title":"development Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fdevelopment%2FREADME.md -->","isInternal":false,"tokens":716,"sizeBytes":2814},{"name":"README.md","path":"plugins/jsonp/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/plugins/jsonp/README.md","title":"jsonp Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fjsonp%2FREADME.md -->","isInternal":false,"tokens":880,"sizeBytes":3139},{"name":"README.md","path":"plugins/logrotator/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/plugins/logrotator/README.md","title":"logrotator Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Flogrotator%2FREADME.md -->","isInternal":false,"tokens":1041,"sizeBytes":3835},{"name":"README.md","path":"plugins/mock/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/plugins/mock/README.md","title":"mock Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fmock%2FREADME.md -->","isInternal":false,"tokens":2887,"sizeBytes":11033},{"name":"README.md","path":"plugins/multipart/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/plugins/multipart/README.md","title":"multipart Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fmultipart%2FREADME.md -->","isInternal":false,"tokens":3145,"sizeBytes":12330},{"name":"README.md","path":"plugins/onerror/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/plugins/onerror/README.md","title":"onerror Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fonerror%2FREADME.md -->","isInternal":false,"tokens":625,"sizeBytes":2347},{"name":"README.md","path":"plugins/redis/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/plugins/redis/README.md","title":"redis Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fredis%2FREADME.md -->","isInternal":false,"tokens":2286,"sizeBytes":8480},{"name":"README.md","path":"plugins/schedule/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/plugins/schedule/README.md","title":"schedule Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fschedule%2FREADME.md -->","isInternal":false,"tokens":2149,"sizeBytes":8405},{"name":"README.md","path":"plugins/security/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/plugins/security/README.md","title":"security Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fsecurity%2FREADME.md -->","isInternal":false,"tokens":4240,"sizeBytes":16960},{"name":"README.md","path":"plugins/session/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/plugins/session/README.md","title":"session Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fsession%2FREADME.md -->","isInternal":false,"tokens":947,"sizeBytes":3605},{"name":"README.md","path":"plugins/static/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/plugins/static/README.md","title":"static Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fstatic%2FREADME.md -->","isInternal":false,"tokens":707,"sizeBytes":2576},{"name":"README.md","path":"plugins/status/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/plugins/status/README.md","title":"status Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fstatus%2FREADME.md -->","isInternal":false,"tokens":668,"sizeBytes":2907},{"name":"README.md","path":"plugins/tracer/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/plugins/tracer/README.md","title":"tracer Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Ftracer%2FREADME.md -->","isInternal":false,"tokens":544,"sizeBytes":1896},{"name":"README.md","path":"plugins/typebox-validate/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/plugins/typebox-validate/README.md","title":"typebox-validate Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Ftypebox-validate%2FREADME.md -->","isInternal":false,"tokens":3842,"sizeBytes":13199},{"name":"README.md","path":"plugins/view-nunjucks/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/plugins/view-nunjucks/README.md","title":"view-nunjucks Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fview-nunjucks%2FREADME.md -->","isInternal":false,"tokens":1085,"sizeBytes":3963},{"name":"README.md","path":"plugins/view/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/plugins/view/README.md","title":"view Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fview%2FREADME.md -->","isInternal":false,"tokens":1907,"sizeBytes":7393},{"name":"README.md","path":"plugins/watcher/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/plugins/watcher/README.md","title":"watcher Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=plugins%2Fwatcher%2FREADME.md -->","isInternal":false,"tokens":941,"sizeBytes":3789},{"name":"README.md","path":"tools/create-egg/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/tools/create-egg/README.md","title":"create-egg Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fcreate-egg%2FREADME.md -->","isInternal":false,"tokens":520,"sizeBytes":2004},{"name":"README.md","path":"tools/create-egg/src/templates/egg3-simple-js/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/tools/create-egg/src/templates/egg3-simple-js/README.md","title":"egg3-simple-js Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fcreate-egg%2Fsrc%2Ftemplates%2Fegg3-simple-js%2FREADME.md -->","isInternal":false,"tokens":113,"sizeBytes":367},{"name":"README.md","path":"tools/create-egg/src/templates/egg3-simple-ts/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/tools/create-egg/src/templates/egg3-simple-ts/README.md","title":"egg3-simple-ts Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fcreate-egg%2Fsrc%2Ftemplates%2Fegg3-simple-ts%2FREADME.md -->","isInternal":false,"tokens":172,"sizeBytes":581},{"name":"README.md","path":"tools/create-egg/src/templates/egg3-tegg/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/tools/create-egg/src/templates/egg3-tegg/README.md","title":"egg3-tegg Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fcreate-egg%2Fsrc%2Ftemplates%2Fegg3-tegg%2FREADME.md -->","isInternal":false,"tokens":175,"sizeBytes":582},{"name":"README.md","path":"tools/create-egg/src/templates/simple-ts/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/tools/create-egg/src/templates/simple-ts/README.md","title":"simple-ts Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fcreate-egg%2Fsrc%2Ftemplates%2Fsimple-ts%2FREADME.md -->","isInternal":false,"tokens":167,"sizeBytes":563},{"name":"README.md","path":"tools/create-egg/src/templates/tegg/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/tools/create-egg/src/templates/tegg/README.md","title":"tegg Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fcreate-egg%2Fsrc%2Ftemplates%2Ftegg%2FREADME.md -->","isInternal":false,"tokens":142,"sizeBytes":489},{"name":"README.md","path":"tools/egg-bin/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/tools/egg-bin/README.md","title":"egg-bin Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fegg-bin%2FREADME.md -->","isInternal":false,"tokens":2462,"sizeBytes":9394},{"name":"README.md","path":"tools/egg-bundler/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/tools/egg-bundler/README.md","title":"egg-bundler Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fegg-bundler%2FREADME.md -->","isInternal":false,"tokens":1118,"sizeBytes":5432},{"name":"README.md","path":"tools/scripts/README.md","rawUrl":"https://raw.githubusercontent.com/eggjs/egg/HEAD/tools/scripts/README.md","title":"scripts Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/eggjs/egg?path=tools%2Fscripts%2FREADME.md -->","isInternal":false,"tokens":978,"sizeBytes":3573}],"systemPromptSnippet":"<yakaai_skills repo=\"eggjs/egg\">\n<!-- File: AGENTS.md (Tokens: ~2693 | Category: root-instruction) -->\n# AGENTS.md\n\nThis is the canonical shared instruction file for coding agents working in this repository.\n\nIf another agent-specific file exists, it should import or defer to this file for shared repository guidance.\n\n## Project Map\n\nEgg is maintained as a utoo monorepo.\n\n- `packages/` contains core framework packages and shared internals.\n- `plugins/` contains optional Egg integrations.\n- `tools/` contains developer tooling such as CLI packages.\n- `tegg/` contains the tegg ecosystem.\n- `examples/` contains sample applications.\n- `site/docs/` contains the English and Chinese documentation site.\n- tests usually live beside packages under `test/`, often with fixtures under `test/fixtures/`.\n\n## Core Commands\n\nThe repository runs on [utoo](https://github.com/utooland/utoo) (`ut`); workspace patterns and overrides live in `package.json`, and shared catalogs live in the tracked `.utoo.toml`. CI installs the latest utoo release; the old pnpm configuration is retained only as a migration reference.\n\n- `npm install --global utoo@latest` installs the latest utoo release on a clean machine.\n- `ut install` hydrates the workspace.\n- `ut run build` builds all packages.\n- `ut run test` runs the main test suite.\n- `ut run lint` runs linting.\n- `ut run typecheck` runs TypeScript checking.\n- use filtered commands for focused work, for example `ut run test --workspace @eggjs/bin` or `ut run build --workspace @eggjs/bin`; prefer the package-name form of `--workspace` (the `./tools/...` path form does not match on Windows); a package without its own script (for example `build` in @eggjs/scripts) needs the root script plus the tsdown workspace path filter instead: `ut run build -- --workspace ./tools/scripts`.\n\n### Local CI\n\nRun tests **without building first**. The CI workflow (`ut install → ut run ci`) never runs `build` before tests. If `dist/` directories exist from a prior build, tegg plugin tests will fail with `duplicate proto` errors because globby scans both `src/*.ts` and `dist/*.js`, loading the same decorated class twice.\n\nWhen you see `duplicate proto` failures locally:\n\n```bash\nfind tegg packages plugins tools -name dist -type d \\\n  -not -path '*/node_modules/*' -not -path '*/test/*' -not -path '*/fixtures/*' \\\n  -exec rm -rf {} +\n```\n\nThen re-run tests.\n\n## Coding Conventions\n\n- prefer existing repo patterns over inventing new ones\n- prefer ESM and TypeScript-first changes where applicable\n- keep file names lowercase with hyphens\n- keep public API changes deliberate and documented\n- use `oxfmt` and `oxlint --type-aware` conventions already present in the repo\n- **tegg multi-app isolation**: do NOT introduce new process-global mutable\n  runtime state in `tegg/`; per-app state must be backed by a `TeggScope` slot.\n  Hooks registered through the bag-pinned `app.*LifecycleUtil` getters need no\n  extra wrap; detached/escape-point access (timers, emitter listeners, proxy\n  handlers, module-level lifecycle-util statics) must run inside\n  `TeggScope.run(app._teggScopeBag, ...)`. See the \"Multi-App Isolation\n  (TeggScope)\" section in `tegg/AGENTS.md` for the full rules.\n- **V8 startup snapshot lifecycle**: a snapshot build runs through\n  `configWillLoad` and resumes from `configDidLoad` only after restore. Plugin\n  constructors and `configWillLoad` must therefore keep only serializable\n  configuration and metadata; create cluster clients, sockets, servers, file\n  watchers, timers, native clients, and other runtime resources in\n  `configDidLoad` or a later hook. If one plugin consumes another plugin's\n  runtime instance, declare that plugin dependency so their `configDidLoad`\n  ordering is deterministic. Do not hide an early initialization violation\n  behind a placeholder/deferred proxy or recorded-call replay; fail fast and\n  move the initialization to the correct lifecycle phase. Use\n  `snapshotWillSerialize`/`snapshotDidDeserialize` only for framework-owned\n  resources that must exist before the cutoff and have an explicit symmetric\n  release/restore implementation.\n- **V8 startup snapshot dependencies**: the egg-bundler can build a V8 startup\n  snapshot (`snapshot: true`), where the app boots only to `configWillLoad` at\n  BUILD time. Any module loaded or instantiated during that boot that creates a\n  non-serializable native binding — llhttp `HTTPParser` (http/https/undici),\n  `nghttp2` (http2, and anything built on it), tls `SecureContext`, dns\n  `ChannelWrap`, a `WebAssembly` instance (undici's llhttp; WASM is disabled under\n  `--build-snapshot`), fs watchers, native addons, open sockets — makes the\n  snapshot build FATAL (\"global handle not serialized\"). Such modules must be kept\n  EXTERNAL (not inlined) so the prelude stubs them at build and forwards to the\n  real module via `globalThis.__RUNTIME_REQUIRE` at restore. The framework default\n  list is `DEFAULT_SNAPSHOT_LAZY_MODULES` in `tools/egg-bundler/src/lib/prelude.ts`\n  (network builtins + `inspector` + `undici` + `urllib`); apps extend it via\n  `egg.snapshot.lazyModules` in `package.json`. **When adding a framework\n  dependency that touches the network/native stack during boot, check whether it\n  must be added to that list.** A package that only reaches the network stack\n  _transitively_ is already covered because those builtins are lazy (e.g.\n  `@modelcontextprotocol/sdk` → `@hono/node-server` → `http2`, `@grpc/grpc-js` →\n  `http2`); only a package that DIRECTLY creates native/WASM state at module-eval\n  or boot-time instantiation (like `undici`) needs adding. See the \"Snapshot\n  lazy-external defaults\" section in `wiki/packages/egg-bundler.md` for details.\n\n## TypeScript Global Types\n\n- put package-wide global augmentations in a dedicated `src/global.ts` or `src/global.d.ts`\n- shared cross-package global types belong in `@eggjs/typings`, not in one consumer package\n- import shared global augmentations from the package entry that needs the type surface, for example `import '@eggjs/typings/global'`\n- keep `declare global` files as modules by using an `import type` or `export {}`\n\n## Testing And PR Expectations\n\n- run the most targeted tests that validate the touched area\n- include regression coverage when changing loader, cluster, agent, HTTP, or process behavior\n- use Angular-style commit messages such as `fix(loader): ensure middleware order`\n- keep PR descriptions clear about motivation, scope, and test evidence\n\n## Security And Config\n\n- review `SECURITY.md` before handling vulnerability-related work\n- do not commit secrets, credentials, or local-only URLs\n- keep local Node.js aligned with `engines.node` and use the latest utoo release\n\n## Shared Knowledge Workflow\n\nThis repository also maintains an LLM-owned wiki for durable project knowledge.\n\n### Explicit Wiki Write Trigger\n\nCreate, update, reorganize, or log wiki content only when the user explicitly\nrequests a wiki build or maintenance task. Reading the wiki for context does\nnot authorize writing to it.\n\nOrdinary coding, debugging, testing, reviews, documentation edits, and PR work\nmust not produce wiki changes, even when they reveal durable findings or change\nAPIs, architecture, contributor workflows, or testing expectations. Do not\ninclude incidental wiki updates in commits or PRs. Report relevant findings in\nthe task response or PR description instead.\n\nThe wiki writing rules below apply only within an explicitly requested wiki\ntask. Use this three-layer model:\n\n### Raw Sources\n\nRaw sources are the source of truth.\n\nThey include:\n\n- repository code under `packages/`, `plugins/`, `tools/`, `tegg/`, `examples/`, and `scripts/`\n- user-facing docs under `site/docs/`\n- root markdown files such as `README.md`, `README.zh-CN.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, and `SECURITY.md`\n- explicit external artifacts referenced by the user or task\n- imported materials stored under `raw/`\n\nRules:\n\n- do not treat wiki summaries as authoritative when raw sources disagree\n- do not rewrite raw sources unless the task requires it\n- during an explicitly requested wiki task, capture repeatedly used external sources in the wiki\n\n### Wiki\n\nThe wiki lives under `wiki/` and stores durable synthesized knowledge.\n\n### Schema\n\nShared workflow rules live here in `AGENTS.md`.\n\nAgent-specific files should stay thin and point back to this file instead of duplicating the schema.\n\n## Wiki Layout\n\n- when consulting the wiki, read `wiki/index.md` first\n- `wiki/log.md` is the append-only chronological log\n- `wiki/packages/` holds package, plugin, tool, and subsystem pages\n- `wiki/concepts/` holds architectural and cross-cutting pages\n- `wiki/workflows/` holds repeatable procedures\n- `wiki/decisions/` holds notable tradeoffs and decisions\n- `wiki/sources/` holds summaries of major source documents or external materials\n\n## Wiki Page Rules\n\nUse these page types:\n\n- `package`\n- `concept`\n- `workflow`\n- `decision`\n- `source`\n\nEvery substantive page should begin with frontmatter:\n\n```yaml\n---\ntitle: Short human-readable title\ntype: package|concept|workflow|decision|source\nsummary: One-line summary\nsource_files:\n  - path/or/url\nupdated_at: YYYY-MM-DD\nstatus: seed|active|stale\n---\n```\n\nUse lowercase kebab-case filenames and keep one topic per page.\n\nUse the workspace-local Asia/Shanghai calendar date for wiki log headings and\nfrontmatter `updated_at` values.\n\n## Citation And Freshness Rules\n\n- every nontrivial wiki claim should be traceable to raw sources\n- list major source paths in `source_files`\n- label non-obvious synthesis as `Inference:`\n- record conflicts explicitly instead of flattening them\n- mark stale or unresolved claims when freshness is uncertain\n\n## Index And Log Rules\n\n`wiki/index.md` should:\n\n- list durable pages by category\n- give each page a one-line summary\n- stay concise enough to scan quickly\n\n`wiki/log.md` should record:\n\n- ingestion of substantial new sources\n- durable findings produced during a query\n- material wiki refactors or lint passes\n- code or docs changes that alter previously recorded understanding\n\nDo not log trivial typo-only edits.\n\n## Standard Workflows\n\nIngest and Lint are wiki maintenance workflows and require an explicit user\nrequest. Query is read-only by default; its write-back steps apply only when\nthe user also explicitly requests wiki construction or maintenance.\n\n### Ingest\n\n1. Read `wiki/index.md`.\n2. Find existing pages that should absorb the new information.\n3. Create or update a `source` page if the source is substantial.\n4. Update impacted wiki pages.\n5. Update `wiki/index.md`.\n6. Append to `wiki/log.md` if the wiki changed materially.\n\n### Query\n\n1. Read `wiki/index.md` and relevant wiki pages first.\n2. Use the wiki as the starting point, not the final authority.\n3. Read raw sources for verification, detail, or freshness.\n4. Only within an explicitly requested wiki task, write durable findings back into the wiki.\n5. Only within that wiki task, update the log when the wiki changes materially.\n\n### Lint\n\nCheck the wiki for:\n\n- orphan pages\n- stale claims after code or docs changes\n- duplicated concepts\n- missing source references\n- missing pages for frequently touched areas\n- contradictions between pages\n\nPrefer restructuring pages over making them longer.\n\n## Repo-Specific Wiki Priorities\n\nPrioritize durable wiki coverage for:\n\n- `packages/egg`, `packages/core`, `packages/utils`, and other foundational packages\n- plugins under `plugins/`\n- tools under `tools/`\n- `tegg/`\n- docs structure under `site/docs/`\n- loading, lifecycle, plugin model, testing, release, and docs-maintenance concepts\n\n## Change Trigger\n\nOnly an explicit user request to build or maintain the wiki triggers wiki\noutput. Code or documentation changes and newly discovered findings do not\ntrigger it automatically. Leave existing wiki files unchanged during ordinary\ntasks.\n\n<!-- File: tegg/AGENTS.md (Tokens: ~3664 | Category: root-instruction) -->\n# AGENTS.md\n\nFollow the shared repository guidance in [the root AGENTS.md](../AGENTS.md).\nThis file adds guidance for working in `tegg/`.\n\n## Overview\n\nTegg is a modular IoC (Inversion of Control) framework for Egg.js, providing dependency injection, lifecycle management, and plugin architecture. It's designed for building large-scale, maintainable Node.js applications using TypeScript decorators.\n\n**Requirements:**\n\n- Node.js >= 22.18.0\n- ESM only (no CommonJS)\n- egg >= 4.1.0\n\n## Monorepo Structure\n\n**IMPORTANT:** Tegg is part of the main [Egg.js monorepo](https://github.com/eggjs/egg). All build, test, and version management commands should be run from the monorepo root.\n\nThe tegg packages are organized as follows within the main monorepo:\n\n```\ncore/          # 24 core packages - decorators, runtime, metadata, loaders\nplugin/        # 10 plugin packages - Egg.js plugins that integrate core functionality\nstandalone/    # 1 standalone package - standalone runtime without Egg.js\n```\n\n**Dependency Management:**\n\n- Uses utoo workspaces with `catalog:` protocol for shared external dependencies\n- Uses `workspace:*` protocol for internal monorepo dependencies (both tegg and egg packages)\n- All shared dependency versions centralized in the root `.utoo.toml` (not in tegg/)\n- `catalogMode: prefer` set in root `.npmrc` for automatic catalog usage\n- Tegg workspace patterns are defined in root package.json as `tegg/core/*`, `tegg/plugin/*`, `tegg/standalone/*`\n\n### Key Core Packages\n\n- **core-decorator**: Basic decorators (`@Inject`, `@ContextProto`, `@SingletonProto`)\n- **metadata**: Metadata management for prototypes, modules, and dependency graphs\n- **runtime**: Runtime container and object lifecycle management\n- **loader**: Module discovery and loading system\n- **lifecycle**: Lifecycle hooks and management\n- **types**: TypeScript type definitions\n- **common-util**: Shared utilities\n\n### Key Plugin Packages\n\n- **plugin/tegg**: Main Egg.js plugin integrating tegg runtime\n- **plugin/config**: Module configuration support\n- **plugin/controller**: HTTP controller decorator support\n- **plugin/aop**: AOP runtime integration\n- **plugin/eventbus**: Event bus system\n- **plugin/schedule**: Scheduled task support\n- **plugin/dal**: Data access layer\n- **plugin/orm**: Leoric ORM integration\n\n## Development Commands\n\n**Note:** All commands below should be run from the **monorepo root** (`../egg`), not from the tegg directory.\n\n### Build & Clean\n\n```bash\nut run build               # Build all packages including tegg (runs build in all workspaces)\nut run clean-dist          # Clean all build artifacts including tegg (removes dist, tsbuildinfo)\n```\n\n### Testing\n\nAll tegg packages use **Vitest** for testing and are integrated with the main Egg.js monorepo test suite.\n\n```bash\nut run test                # Run vitest tests for all packages (from monorepo root)\nut run test:cov            # Run tests with coverage\nut run ci                  # Full CI: vitest with coverage and bail on first failure\n```\n\nRun tegg tests from clean sources, not immediately after `pnpm run build`. If\nlocal tests fail with `duplicate proto` after a build, remove stale `dist/`\ndirectories outside fixtures as described in the root `AGENTS.md` Local CI\nsection, then re-run tests.\n\n**Note:** Tests are configured in the monorepo root `vitest.config.ts` which includes all tegg packages (`tegg/core/*`, `tegg/plugin/*`, `tegg/standalone/*`).\n\n### Type Checking & Linting\n\n```bash\nut run typecheck           # Clean and type check all workspaces (including tegg)\nut run lint                # Run oxlint with type-aware checking on all packages\nut run fmtcheck            # Check code formatting with oxfmt\n```\n\n**Note:** oxlint automatically runs with `--type-aware` flag for enhanced TypeScript checking.\n\n### Version Management\n\n**Note:** Run these commands from the monorepo root (`../egg`).\n\n```bash\nut run version:patch       # Bump patch version (0.0.X)\nut run version:minor       # Bump minor version (0.X.0)\nut run version:major       # Bump major version (X.0.0)\nut run version:prepatch    # Bump to next prerelease patch version\nut run version:preminor    # Bump to next prerelease minor version\nut run version:premajor    # Bump to next prerelease major version\nut run version:alpha       # Bump prerelease alpha version\nut run version:beta        # Bump prerelease beta version\nut run version:rc          # Bump prerelease rc version\n```\n\n### Working with Individual Packages\n\n**Note:** Run from the monorepo root to work with individual tegg packages.\n\n```bash\n# Install dependencies\nut install              # Install all dependencies from native utoo configuration\n\n# Type check specific packages\nut run typecheck                    # Type check all packages\nut --filter @eggjs/tegg-runtime run typecheck\n\n# Build specific packages\nut --filter @eggjs/metadata run build\nut --filter @eggjs/tegg-runtime run build\n\n# Clean specific package\nnode -e \"require('node:fs').rmSync('tegg/core/runtime/dist', { recursive: true, force: true })\"\n```\n\n**Note:** Individual tegg packages don't have test scripts in their package.json. Tests are run via the monorepo root vitest configuration.\n\n## Architecture Concepts\n\n### Prototype System\n\nTegg uses a prototype-based system where classes are decorated to define how they should be instantiated:\n\n- **ContextProto**: Instance per request context (scoped to HTTP request)\n- **SingletonProto**: Single instance for entire application lifecycle\n- **MultiInstanceProto**: Multiple instances of same class with different qualifiers\n\nEach prototype has:\n\n- **AccessLevel**: `PRIVATE` (module-only) or `PUBLIC` (globally accessible)\n- **InitType**: Defines lifecycle scope (`CONTEXT`, `SINGLETON`)\n- **Name**: Instance identifier (defaults to camelCase class name)\n\n### Dependency Injection\n\nDependencies are resolved through `@Inject()` decorator:\n\n- Property injection: `@Inject() logger: Logger`\n- Constructor injection: `constructor(@Inject() logger: Logger)`\n- Optional injection: `@InjectOptional()` or `@Inject({ optional: true })`\n\n**Injection Rules:**\n\n- ContextProto can inject any prototype\n- SingletonProto cannot inject ContextProto\n- No circular dependencies allowed (between prototypes or modules)\n- Cannot inject `ctx`/`app` directly - inject specific services instead\n\n### Qualifiers\n\nWhen multiple implementations exist, use qualifiers to disambiguate:\n\n- `@InitTypeQualifier(ObjectInitType.CONTEXT)`: Specify init type\n- `@ModuleQualifier('moduleName')`: Specify source module\n- `@EggQualifier(EggType.CONTEXT)`: Specify egg context vs app\n- Custom qualifiers for dynamic injection patterns\n\n### Module System\n\nModules are organizational units discovered by scanning:\n\n- `app/modules/` directory (auto-discovered)\n- `config/module.json` (manual declaration for npm packages)\n\nEach module contains:\n\n- Prototype classes with decorators\n- Optional `module.json` or `package.json` with tegg metadata\n- Module-level dependencies on other modules\n\nThe **GlobalGraph** builds a dependency graph of all modules and validates:\n\n- No circular module dependencies\n- All prototype dependencies are resolvable\n- Access level constraints are respected\n\n### Lifecycle Hooks\n\nObjects can implement `EggObjectLifecycle` interface or use decorators:\n\n- `@LifecyclePostConstruct()`: After constructor\n- `@LifecyclePreInject()`: Before dependency injection\n- `@LifecyclePostInject()`: After dependency injection\n- `@LifecycleInit()`: Custom async initialization\n- `@LifecyclePreDestroy()`: Before object destruction\n- `@LifecycleDestroy()`: Resource cleanup\n\n### Runtime Object Management\n\nThe runtime manages object instances through:\n\n- **EggObjectFactory**: Creates and retrieves object instances\n- **LoadUnitInstance**: Manages module instances and their objects\n- **EggContext**: Request-scoped context holding ContextProto instances\n- **ContextObjectGraph**: Dependency graph for a specific context\n\n## Testing Patterns\n\n### Testing with MockApplication\n\n```typescript\nimport { MockApplication } from '@eggjs/mock';\n\n// Create context scope\nawait app.mockModuleContextScope(async (ctx: Context) => {\n  // Get object by class\n  const service = await ctx.getEggObject(HelloService);\n\n  // Get object by name with qualifiers\n  const logger = await ctx.getEggObjectFromName('logger', {\n    qualifier: 'bizLogger',\n  });\n});\n```\n\n### Async Tasks in Tests\n\nUse `BackgroundTaskHelper` instead of `setTimeout`/`setImmediate`:\n\n```typescript\n@ContextProto()\nclass MyService {\n  @Inject()\n  backgroundTaskHelper: BackgroundTaskHelper;\n\n  async doWork() {\n    this.backgroundTaskHelper.run(async () => {\n      // Async work here\n    });\n  }\n}\n```\n\n## Important Implementation Details\n\n### Metadata Registration\n\nDecorators register metadata on classes that is later used by the loader:\n\n- Prototype metadata: `PrototypeUtil.setXXX()` stores on class\n- Injection metadata: `InjectObjectInfo` stored per property/parameter\n- Qualifier metadata: `QualifierUtil.addProperQualifier()` for disambiguation\n\n### Loading Process\n\n1. **Loader** scans directories and discovers modules\n2. **EggPrototypeFactory** creates `EggPrototype` from decorated classes\n3. **GlobalGraph** validates and builds dependency graph\n4. **LoadUnitFactory** creates `LoadUnit` for each module\n5. **Runtime** instantiates objects on-demand based on graph\n\n### Dynamic Injection\n\nFor selecting implementations at runtime:\n\n```typescript\n// Define abstract class and enum\nabstract class AbstractHello { abstract hello(): string; }\nenum HelloType { FOO = 'FOO', BAR = 'BAR' }\n\n// Create decorator\nconst Hello = QualifierImplDecoratorUtil.generatorDecorator(\n  AbstractHello,\n  'HELLO_ATTRIBUTE'\n);\n\n// Apply to implementations\n@ContextProto()\n@Hello(HelloType.FOO)\nclass FooHello extends AbstractHello { ... }\n\n// Get instance dynamically\nconst impl = await eggObjectFactory.getEggObject(\n  AbstractHello,\n  HelloType.FOO\n);\n```\n\n## Multi-App Isolation (TeggScope) — MUST follow\n\nTegg supports multiple apps booting and serving requests **concurrently in one\nprocess** without cross-talk. This is built on `TeggScope`\n(`@eggjs/tegg-types`), a type-free `AsyncLocalStorage<Map<symbol, unknown>>`.\nEach app owns a per-app \"bag\" (`app._teggScopeBag`); per-app state lives in\nslots inside that bag, and the active bag is established with\n`TeggScope.run(app._teggScopeBag, ...)`. Per-app singletons resolve via the same\nold static call sites (e.g. `EggPrototypeFactory.instance`) — they now read the\ncurrent scope instead of a process global.\n\nWhen you touch tegg core/plugins, follow these rules:\n\n1. **Never add new process-global mutable runtime state.** A `static` field /\n   `Map` / singleton that holds per-app data WILL leak across concurrent apps.\n   If you need such state, back it with a `TeggScope` slot:\n\n   ```ts\n   import { TeggScope } from '@eggjs/tegg-types';\n   const X_SLOT = Symbol('tegg:<pkg>:<name>'); // module-private, never exported\n   export class X {\n     static get instance(): X {\n       return TeggScope.resolve(X_SLOT, () => new X(), 'X.instance');\n     }\n   }\n   ```\n\n   Import `TeggScope` **only** from `@eggjs/tegg-types`; never import another\n   package's slot. A package that imports `TeggScope` must declare\n   `@eggjs/tegg-types` as a direct dependency.\n\n2. **Shared, app-agnostic registries stay global.** Class/type-keyed maps\n   populated at import time with app-agnostic values (e.g.\n   `EggPrototypeCreatorFactory` creator map, `registerEggObjectCreateMethod`,\n   `registerLoadUnitInstanceClass`) must NOT be scoped. Only state that holds\n   per-app instances/data is scoped. (`LoadUnitFactory`'s creator map is\n   two-tier: a global base for import-time creators + a per-app overlay for\n   boot-time, app-capturing creators.)\n\n3. **Lifecycle-hook registration via `app.*LifecycleUtil` is bag-pinned.**\n   Calling `app.{loadUnit,eggPrototype,eggObject,eggContext,loadUnitInstance}LifecycleUtil.registerLifecycle(hook)`\n   (and the matching `deleteLifecycle` in `beforeClose`) does **not** need a\n   `TeggScope.run` wrap — these app getters are pinned to this app's bag (via\n   `xxxLifecycleUtilFromBag`), so they resolve the correct per-app util even with\n   no active scope. Wrapping is still fine when the same block does other\n   scope-dependent work (as the tegg plugin's own boot does). Do **not** register\n   lifecycle hooks in the boot **constructor** — `app._teggScopeBag` does not\n   exist yet; do it in `configWillLoad`/`configDidLoad`/`didLoad`. (Accessing a\n   lifecycle util through a module-level static instead of `app.*LifecycleUtil`\n   still needs an active scope.)\n\n4. **Resolve egg objects per-app.** To get a proto from a class, prefer\n   `EggPrototypeFactory.instance.getPrototypeByClazz(clazz)` (per-app) before\n   falling back to `PrototypeUtil.getClazzProto(clazz)` (a process-global slot\n   on the class, overwritten by concurrent boot). `ctx.getEggObject` /\n   `app.getEggObject` already do this and wrap in the app scope.\n\n5. **Escape points** — code that runs **detached** from the request must\n   re-establish the scope. Capture `const bag = TeggScope.current()` at\n   registration/scheduling and re-enter `TeggScope.run(bag, cb)` inside the\n   callback for: emitter listeners triggered later (`res.on('close')`,\n   `signal.addEventListener('abort')`), fire-and-forget `EventBus.emit` from a\n   detached context, and timers created outside a scope. Timers/promises created\n   **inside** an active scope inherit it automatically — no wrap needed.\n\n6. **Strict-mode fuse.** Under true multi-app (`> 1` live app) any access that\n   escapes to the process-default bag throws in dev / warns in prod. If you see\n   `[tegg] TeggScope escaped to the process-default bag`, you have an unwrapped\n   access — wrap the relevant boot/request/escape path in `TeggScope.run`.\n\n7. **Single app is unchanged.** With one app the default bag is used silently\n   and the fuse never fires, so existing single-app behavior and tests are\n   unaffected. Add multi-app regression coverage (two concurrent apps sharing a\n   module) when you change loader/runtime/lifecycle/eventbus behavior — see\n   `tegg/plugin/tegg/test/MultiApp.test.ts`.\n\n**Performance:** `TeggScope.resolve` adds ~8 ns/access and `TeggScope.run`\n~5 ns/call over a plain static read (Node 22); egg already runs on\nAsyncLocalStorage, so there is no new process-wide async penalty. The cost is\nnegligible relative to real request work. The per-app lifecycle-util facade is an\nexplicit delegating object (not a `Proxy`) — each method is a direct slot-resolve\nplus a method call, with no per-access trap or bound-function allocation.\n\n## Common Patterns\n\n### Creating a New Core Package\n\n1. Add to `tegg/core/` directory within the main monorepo\n2. Include `tsconfig.json` extending `@eggjs/tsconfig`\n3. Add standard scripts to `package.json`:\n   - `\"typecheck\": \"tsc --noEmit\"`\n4. Export public API through `src/index.ts`\n5. Use `workspace:*` for internal dependencies and `catalog:` for external dependencies\n\n### Creating a New Plugin\n\n1. Add to `tegg/plugin/` directory within the main monorepo\n2. Define `eggPlugin` in `package.json` with dependencies\n3. Create `app.ts` for initialization\n4. Add tests using Vitest and `@eggjs/mock`\n5. Tests will be automatically discovered by the root `vitest.config.ts`\n\n### Working with TypeScript\n\n- Use `emitDecoratorMetadata` for type inference in injection\n- `design:type` and `design:paramtypes` are used for automatic dependency resolution\n- All packages target ESM with `.js` extensions in imports\n\n<!-- File: .github/copilot-instructions.md (Tokens: ~2563 | Category: copilot-instructions) -->\n# Eggjs Framework - GitHub Copilot Development Instructions\n\n**Always reference these instructions first and fallback to search or additional context gathering only when you encounter unexpected information that does not match the information provided here.**\n\n## Overview\n\nEggjs is a progressive Node.js framework for building enterprise-class server-side applications. Built on top of Koa.js, it provides a plugin system, conventions over configuration, and enterprise-grade features like clustering, logging, and security.\n\nThis is a **utoo monorepo** with multiple packages using utoo workspaces and catalog mode for centralized dependency management.\n\n## Prerequisites and Environment Setup\n\n- **Node.js >= 22.18.0 required** - This is a hard requirement\n- Enable utoo first: `npm install --global utoo@latest`\n- **NEVER CANCEL** any build or test commands - they can take several minutes to complete\n\n## Bootstrap and Build Process\n\n**Run these commands after a fresh clone:**\n\n```bash\n# 1. Enable utoo (required first)\nnpm install --global utoo@latest\n\n# 2. Install all dependencies - takes ~63 seconds. NEVER CANCEL. Set timeout to 120+ seconds.\nut install\n\n# 3. Run lint to check code quality across all packages - takes ~2 seconds\nut run lint\n\n# 4. Build all packages when validating build output - takes ~14 seconds. NEVER CANCEL. Set timeout to 60+ seconds.\nut run build\n```\n\nRun unit tests from a clean source tree, not immediately after `ut run build`.\nThe main CI test job installs dependencies with `ut install` and\nruns tests with `ut run ci`; it does not run `build` before tests.\n\n## Monorepo Structure\n\n### Key Packages (all in `packages/` directory)\n\n- **`packages/egg/`** - Main Eggjs framework (TypeScript, uses tsdown for builds)\n- **`packages/core/`** - Core plugin framework\n- **`packages/utils/`** - Utility functions\n- **`packages/mock/`** - Testing utilities\n- **`packages/cluster/`** - Cluster management\n- **`packages/koa/`** - Koa web framework\n- **`packages/supertest/`** - HTTP testing utilities\n- **`packages/extend2/`** - Object extension utility\n\n### Supporting Directories\n\n- **`examples/`** - Two example apps: `helloworld-commonjs` and `helloworld-typescript` (currently have runtime issues)\n- **`site/`** - Documentation website built with VitePress\n\n## Essential Commands and Timing\n\n### Build Commands\n\n- `ut run build` - **Build all packages (~14 seconds). NEVER CANCEL. Set timeout to 60+ seconds.**\n- `ut run clean-dist` - Clean all dist directories\n\n### Testing Commands\n\n- `ut run test` - **Run all tests (~2 minutes). NEVER CANCEL. Set timeout to 180+ seconds.**\n- `ut run test:cov` - **Run tests with coverage (~2 minutes). NEVER CANCEL. Set timeout to 180+ seconds.**\n- `ut run ci` - **Run tests with coverage (~2.1 minutes). NEVER CANCEL. Set timeout to 180+ seconds.**\n\n### Linting Commands\n\n- `ut run lint` - Run oxlint across all packages (~2 seconds)\n\n### Documentation Commands\n\n- `ut run site:dev` - Start documentation dev server (defaults to VitePress port 5173)\n- `ut run site:build` - **Build documentation site (~24 seconds). NEVER CANCEL. Set timeout to 60+ seconds.**\n\n### Example Applications (Currently Not Working)\n\n- `ut run example:dev:commonjs` - Start CommonJS example (has runtime issues)\n- `ut run example:dev:typescript` - Start TypeScript example (has runtime issues)\n\n## Package-Specific Commands\n\nRun commands for specific packages using `ut --filter=<package>`:\n\n```bash\n# Examples\nut --filter=egg run test\nut --filter=@eggjs/core run build\nut --filter=site run dev\n```\n\n## Development Workflow\n\n### 1. Making Changes\n\n- Work from the source tree first. Build when you need to validate package output, but do not run build immediately before unit tests.\n- Work primarily in `packages/egg/src/` for core framework features\n- Use TypeScript throughout - all packages are TypeScript-based\n- Follow the existing directory conventions in `packages/egg/src/`:\n  - `lib/` - Core classes (Application, Agent, EggApplicationCore)\n  - `app/extend/` - Framework extensions (context, helper, request, response)\n  - `config/` - Default configurations and plugins\n  - `lib/core/` - Core components (httpclient, logger, messenger)\n  - `lib/loader/` - Application loaders\n\n### 2. Validation Steps\n\n**Always perform these validation steps after making changes:**\n\n```bash\n# 1. Run lint to check code quality across all packages\nut run lint\n\n# 2. Run tests from a clean tree (some failures are expected in fresh environment)\nut run test\n\n# 3. Build all packages when build output or packaging behavior is relevant\nut run build\n\n# 4. Test documentation site when docs changed\nut run site:dev\n```\n\n### 3. Testing Strategy\n\n- **All packages use Vitest for testing** - this is the standard test runner\n- Test files follow pattern: `test/**/*.test.ts`\n- Use `import { describe, it } from 'vitest'` for test functions\n- Use Node.js built-in `assert` module for assertions\n- Create test fixtures in `packages/egg/test/fixtures/apps/` for scenario testing\n\n## Key Framework Concepts\n\n### Architecture\n\n- **EggApplicationCore** - Base application class with core functionality\n- **Application** - Main app class for worker processes\n- **Agent** - Agent process class for background tasks\n- **Context** - Extended Koa context with Egg-specific features\n- **BaseContextClass** - Base for controllers, services, subscriptions\n\n### Loading Convention (Automatic Discovery Order)\n\n1. Plugin system\n2. Configurations\n3. Application/Request/Response/Context extensions\n4. Custom loaders\n5. Services\n6. Middlewares\n7. Controllers\n8. Router\n\n### Cluster vs Single Mode\n\n- **Cluster Mode** (default) - Multi-process with master, agent, and worker processes\n- **Single Mode** - Single process for development/testing\n\n## Working with TypeScript\n\n- All packages use strict TypeScript mode\n- Uses tsdown for unbundled ESM builds (preserves file structure)\n- Each package has `tsdown.config.ts` for build configuration\n- **All sub-project tsconfig.json files MUST extend from root:** `\"extends\": \"../../tsconfig.json\"`\n- Root tsconfig.json includes all packages in `references` array\n\n## utoo Workspace & Catalog Dependencies\n\n- Dependencies defined in `.utoo.toml` catalog section\n- Reference catalog entries: `\"package-name\": \"catalog:\"`\n- Internal workspace dependencies: `\"package-name\": \"workspace:*\"`\n- This ensures consistent versions across all packages\n\n## Common Issues and Troubleshooting\n\n### Test Failures\n\n- Some tests may fail in fresh environments - this is normal\n- Focus on fixing only failures related to your changes\n- Examples may have runtime issues - don't use them for validation\n- If tegg tests fail with `duplicate proto` after a local build, remove stale\n  `dist/` directories outside fixtures and re-run tests. Built `dist/*.js`\n  files can be scanned alongside `src/*.ts`, loading the same decorated class\n  twice.\n\n### Build Issues\n\n- Run `ut run build` when validating build output, package exports, or changes\n  that affect generated artifacts.\n- TypeScript compilation errors will show clearly\n- Build warnings are generally acceptable\n\n### ESM/CommonJS Issues\n\n- Framework supports both ESM and CommonJS\n- Main package exports both formats\n- If you see \"ERR_UNKNOWN_FILE_EXTENSION\" errors, ensure packages are built first\n\n## File Locations Reference\n\n### Key Configuration Files\n\n- `package.json` / `.utoo.toml` - Workspace patterns and catalog configuration\n- `package.json` - Root monorepo scripts and devDependencies\n- `packages/egg/package.json` - Main framework package configuration\n- `packages/egg/tsdown.config.ts` - Build configuration\n- `packages/egg/src/config/plugin.ts` - Built-in plugin configurations\n- `packages/egg/src/config/config.default.ts` - Default framework configuration\n\n### Important Source Files\n\n- `packages/egg/src/lib/application.ts` - Main Application class\n- `packages/egg/src/lib/agent.ts` - Agent process manager\n- `packages/egg/src/lib/egg.ts` - Core EggApplicationCore\n- `packages/egg/src/lib/start.ts` - Application startup logic\n- `packages/egg/src/lib/loader/` - Convention-based loaders\n\n### Build Outputs\n\n- `packages/*/dist/` - Built JavaScript and TypeScript definitions\n- `site/dist/` - Built documentation site\n\n## Validation Scenarios\n\nAfter making changes, always verify:\n\n1. **Linting Passes**: `ut run lint` shows no new errors\n2. **Tests Run From Clean Sources**: `ut run test` executes without stale build artifacts interfering\n3. **Documentation Loads**: `ut run site:dev` starts successfully and the printed VitePress URL responds\n4. **Build Success When Relevant**: `ut run build` completes without errors\n\n**Remember**: This is a complex enterprise framework. Validate incrementally, keep unit tests isolated from stale build artifacts, and focus on the core packages (`egg`, `core`, `utils`) for most development work.\n\n## Commit Message Format\n\n**CRITICAL: All commits MUST follow the [Angular Commit Message Format](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines) as specified in CONTRIBUTING.md.**\n\n### Required Format Structure\n\n```\n<type>(<scope>): <subject>\n<BLANK LINE>\n<body>\n<BLANK LINE>\n<footer>\n```\n\n### Mandatory Types\n\n- **feat**: A new feature\n- **fix**: A bug fix\n- **docs**: Documentation-only changes\n- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)\n- **refactor**: A code change that neither fixes a bug nor adds a feature\n- **perf**: A code change that improves performance\n- **test**: Adding missing tests\n- **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation\n- **deps**: Updates about dependencies\n\n### Scope Guidelines\n\n- **Package-specific changes**: Use package names like `core`, `mock`, `cluster`, `utils`, `tsconfig`, `extend2`\n- **Cross-package changes**: Use feature areas like `loader`, `plugin`, `config`, `build`\n- **Component-specific**: Use component names like `application`, `agent`, `context`\n\n### Subject Rules\n\n- Use imperative, present tense: \"change\" not \"changed\" nor \"changes\"\n- Don't capitalize first letter\n- No period (.) at the end\n- Be succinct and descriptive\n\n### Examples\n\n```\nfeat(tsconfig): integrate package into monorepo with vitest\n\nMerge @eggjs/tsconfig repository into packages/tsconfig/ and refactor\nto use vitest testing framework instead of Node.js test runner.\n\n- Update all consuming packages to use workspace:* dependencies\n- Add vitest configuration and convert test assertions\n- Remove external catalog dependency in favor of workspace package\n\nCloses #123\n```\n\n```\nfix(core): resolve loader initialization race condition\n\nThe loader was attempting to initialize plugins before configurations\nwere fully loaded, causing intermittent startup failures.\n\nFixes #456\n```\n\n```\nchore: update dependencies to latest versions\n\nUpdate catalog dependencies and rebuild packages to ensure\ncompatibility with latest versions.\n```\n\n**NEVER commit without following this format - it breaks the project's automated changelog and release process.**\n\n<!-- File: packages/koa/docs/api/context.md (Tokens: ~1710 | Category: anthropic-skill) -->\n# Context\n\nA Koa Context encapsulates node's `request` and `response` objects\ninto a single object which provides many helpful methods for writing\nweb applications and APIs.\nThese operations are used so frequently in HTTP server development\nthat they are added at this level instead of a higher level framework,\nwhich would force middleware to re-implement this common functionality.\n\nA `Context` is created _per_ request, and is referenced in middleware\nas the receiver, or the `ctx` identifier, as shown in the following\nsnippet:\n\n```js\napp.use(async (ctx) => {\n  ctx; // is the Context\n  ctx.request; // is a Koa Request\n  ctx.response; // is a Koa Response\n});\n```\n\nMany of the context's accessors and methods simply delegate to their `ctx.request` or `ctx.response`\nequivalents for convenience, and are otherwise identical. For example `ctx.type` and `ctx.length`\ndelegate to the `response` object, and `ctx.path` and `ctx.method` delegate to the `request`.\n\n## API\n\n`Context` specific methods and accessors.\n\n### ctx.req\n\nNode's `request` object.\n\n### ctx.res\n\nNode's `response` object.\n\nBypassing Koa's response handling is **not supported**. Avoid using the following node properties:\n\n- `res.statusCode`\n- `res.writeHead()`\n- `res.write()`\n- `res.end()`\n\n### ctx.request\n\nA Koa `Request` object.\n\n### ctx.response\n\nA Koa `Response` object.\n\n### ctx.state\n\nThe recommended namespace for passing information through middleware and to your frontend views.\n\n```js\nctx.state.user = await User.find(id);\n```\n\n### ctx.app\n\nApplication instance reference.\n\n### ctx.app.emit\n\nKoa applications extend an internal [EventEmitter](https://nodejs.org/dist/latest-v11.x/docs/api/events.html). `ctx.app.emit` emits an event with a type, defined by the first argument. For each event you can hook up \"listeners\", which is a function that is called when the event is emitted. Consult the [error handling docs](https://koajs.com/#error-handling) for more information.\n\n### ctx.cookies.get(name, [options])\n\nGet cookie `name` with `options`:\n\n- `signed` the cookie requested should be signed\n\nKoa uses the [cookies](https://github.com/pillarjs/cookies) module where options are simply passed.\n\n### ctx.cookies.set(name, value, [options])\n\nSet cookie `name` to `value` with `options`:\n\n- `maxAge`: a number representing the milliseconds from `Date.now()` for expiry.\n- `expires`: a `Date` object indicating the cookie's expiration date (expires at the end of session by default).\n- `path`: a string indicating the path of the cookie (`/` by default).\n- `domain`: a string indicating the domain of the cookie (no default).\n- `secure`: a boolean indicating whether the cookie is only to be sent over HTTPS (`false` by default for HTTP, `true` by default for HTTPS). [Read more about this option](https://github.com/pillarjs/cookies#secure-cookies).\n- `httpOnly`: a boolean indicating whether the cookie is only to be sent over HTTP(S), and not made available to client JavaScript (`true` by default).\n- `sameSite`: a boolean or string indicating whether the cookie is a \"same site\" cookie (`false` by default). This can be set to `'strict'`, `'lax'`, `'none'`, or `true` (which maps to `'strict'`).\n- `signed`: a boolean indicating whether the cookie is to be signed (`false` by default). If this is true, another cookie of the same name with the `.sig` suffix appended will also be sent, with a 27-byte url-safe base64 SHA1 value representing the hash of _cookie-name_=_cookie-value_ against the first [Keygrip](https://www.npmjs.com/package/keygrip) key. This signature key is used to detect tampering the next time a cookie is received.\n- `overwrite`: a boolean indicating whether to overwrite previously set cookies of the same name (`false` by default). If this is true, all cookies set during the same request with the same name (regardless of path or domain) are filtered out of the Set-Cookie header when setting this cookie.\n\nKoa uses the [cookies](https://github.com/pillarjs/cookies) module where options are simply passed.\n\n### ctx.throw([status], [msg], [properties])\n\nHelper method to throw an error with a `.status` property\ndefaulting to `500` that will allow Koa to respond appropriately.\nThe following combinations are allowed:\n\n```js\nctx.throw(400);\nctx.throw(400, 'name required');\nctx.throw(400, 'name required', { user: user });\n```\n\nFor example `ctx.throw(400, 'name required')` is equivalent to:\n\n```js\nconst err = new Error('name required');\nerr.status = 400;\nerr.expose = true;\nthrow err;\n```\n\nNote that these are user-level errors and are flagged with\n`err.expose` meaning the messages are appropriate for\nclient responses, which is typically not the case for\nerror messages since you do not want to leak failure\ndetails.\n\nYou may optionally pass a `properties` object which is merged into the error as-is, useful for decorating machine-friendly errors which are reported to the requester upstream.\n\n```js\nctx.throw(401, 'access_denied', { user: user });\n```\n\nKoa uses [http-errors](https://github.com/jshttp/http-errors) to create errors. `status` should only be passed as the first parameter.\n\n### ctx.assert(value, [status], [msg], [properties])\n\nHelper method to throw an error similar to `.throw()`\nwhen `!value`. Similar to node's [assert()](http://nodejs.org/api/assert.html)\nmethod.\n\n```js\nctx.assert(ctx.state.user, 401, 'User not found. Please login!');\n```\n\n### ctx.respond\n\nTo bypass Koa's built-in response handling, you may explicitly set `ctx.respond = false;`. Use this if you want to write to the raw `res` object instead of letting Koa handle the response for you.\n\nNote that using this is **not** supported by Koa. This may break intended functionality of Koa middleware and Koa itself. Using this property is considered a hack and is only a convenience to those wishing to use traditional `fn(req, res)` functions and middleware within Koa.\n\n## Request aliases\n\nThe following accessors and alias [Request](request.md) equivalents:\n\n- `ctx.header`\n- `ctx.headers`\n- `ctx.method`\n- `ctx.method=`\n- `ctx.url`\n- `ctx.url=`\n- `ctx.originalUrl`\n- `ctx.origin`\n- `ctx.href`\n- `ctx.path`\n- `ctx.path=`\n- `ctx.query`\n- `ctx.query=`\n- `ctx.querystring`\n- `ctx.querystring=`\n- `ctx.host`\n- `ctx.hostname`\n- `ctx.fresh`\n- `ctx.stale`\n- `ctx.socket`\n- `ctx.protocol`\n- `ctx.secure`\n- `ctx.ip`\n- `ctx.ips`\n- `ctx.subdomains`\n- `ctx.is()`\n- `ctx.accepts()`\n- `ctx.acceptsEncodings()`\n- `ctx.acceptsCharsets()`\n- `ctx.acceptsLanguages()`\n- `ctx.get()`\n\n## Response aliases\n\nThe following accessors and alias [Response](response.md) equivalents:\n\n- `ctx.body`\n- `ctx.body=`\n- `ctx.status`\n- `ctx.status=`\n- `ctx.message`\n- `ctx.message=`\n- `ctx.length=`\n- `ctx.length`\n- `ctx.type=`\n- `ctx.type`\n- `ctx.headerSent`\n- `ctx.redirect()`\n- `ctx.attachment()`\n- `ctx.set()`\n- `ctx.append()`\n- `ctx.remove()`\n- `ctx.lastModified=`\n- `ctx.etag=`\n\n<!-- File: packages/koa/docs/api/index.md (Tokens: ~1943 | Category: anthropic-skill) -->\n# Installation\n\nKoa requires **node v7.6.0** or higher for ES2015 and async function support.\n\nYou can quickly install a supported version of node with your favorite version manager:\n\n```bash\n$ nvm install 7\n$ npm i koa\n$ node my-koa-app.js\n```\n\n# Application\n\nA Koa application is an object containing an array of middleware functions\nwhich are composed and executed in a stack-like manner upon request. Koa is similar to many\nother middleware systems that you may have encountered such as Ruby's Rack, Connect, and so on -\nhowever a key design decision was made to provide high level \"sugar\" at the otherwise low-level\nmiddleware layer. This improves interoperability, robustness, and makes writing middleware much\nmore enjoyable.\n\nThis includes methods for common tasks like content-negotiation, cache freshness, proxy support, and redirection\namong others. Despite supplying a reasonably large number of helpful methods Koa maintains a small footprint, as\nno middleware are bundled.\n\nThe obligatory hello world application:\n\n<!-- runkit:endpoint -->\n\n```js\nconst Koa = require('koa');\nconst app = new Koa();\n\napp.use(async (ctx) => {\n  ctx.body = 'Hello World';\n});\n\napp.listen(3000);\n```\n\n## Cascading\n\nKoa middleware cascade in a more traditional way as you may be used to with similar tools -\nthis was previously difficult to make user friendly with node's use of callbacks.\nHowever with async functions we can achieve \"true\" middleware. Contrasting Connect's implementation which\nsimply passes control through series of functions until one returns, Koa invoke \"downstream\", then\ncontrol flows back \"upstream\".\n\nThe following example responds with \"Hello World\", however first the request flows through\nthe `x-response-time` and `logging` middleware to mark when the request started, then yields control through the response middleware. When a middleware invokes `next()`\nthe function suspends and passes control to the next middleware defined. After there are no more\nmiddleware to execute downstream, the stack will unwind and each middleware is resumed to perform\nits upstream behaviour.\n\n<!-- runkit:endpoint -->\n\n```js\nconst Koa = require('koa');\nconst app = new Koa();\n\n// logger\n\napp.use(async (ctx, next) => {\n  await next();\n  const rt = ctx.response.get('X-Response-Time');\n  console.log(`${ctx.method} ${ctx.url} - ${rt}`);\n});\n\n// x-response-time\n\napp.use(async (ctx, next) => {\n  const start = Date.now();\n  await next();\n  const ms = Date.now() - start;\n  ctx.set('X-Response-Time', `${ms}ms`);\n});\n\n// response\n\napp.use(async (ctx) => {\n  ctx.body = 'Hello World';\n});\n\napp.listen(3000);\n```\n\n## Settings\n\nApplication settings are properties on the `app` instance, currently\nthe following are supported:\n\n- `app.env` defaulting to the **NODE_ENV** or \"development\"\n- `app.keys` array of signed cookie keys\n- `app.proxy` when true proxy header fields will be trusted\n- `app.subdomainOffset` offset of `.subdomains` to ignore, default to 2\n- `app.proxyIpHeader` proxy ip header, default to `X-Forwarded-For`\n- `app.maxIpsCount` max ips read from proxy ip header, default to 0 (means infinity)\n\nYou can pass the settings to the constructor:\n\n```js\nconst Koa = require('koa');\nconst app = new Koa({ proxy: true });\n```\n\nor dynamically:\n\n```js\nconst Koa = require('koa');\nconst app = new Koa();\napp.proxy = true;\n```\n\n## app.listen(...)\n\nA Koa application is not a 1-to-1 representation of an HTTP server.\nOne or more Koa applications may be mounted together to form larger\napplications with a single HTTP server.\n\nCreate and return an HTTP server, passing the given arguments to\n`Server#listen()`. These arguments are documented on [nodejs.org](http://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback). The following is a useless Koa application bound to port `3000`:\n\n```js\nconst Koa = require('koa');\nconst app = new Koa();\napp.listen(3000);\n```\n\nThe `app.listen(...)` method is simply sugar for the following:\n\n```js\nconst http = require('http');\nconst Koa = require('koa');\nconst app = new Koa();\nhttp.createServer(app.callback()).listen(3000);\n```\n\nThis means you can spin up the same application as both HTTP and HTTPS\nor on multiple addresses:\n\n```js\nconst http = require('http');\nconst https = require('https');\nconst Koa = require('koa');\nconst app = new Koa();\nhttp.createServer(app.callback()).listen(3000);\nhttps.createServer(app.callback()).listen(3001);\n```\n\n## app.callback()\n\nReturn a callback function suitable for the `http.createServer()`\nmethod to handle a request.\nYou may also use this callback function to mount your Koa app in a\nConnect/Express app.\n\n## app.use(function)\n\nAdd the given middleware function to this application.\n`app.use()` returns `this`, so is chainable.\n\n```js\napp.use(someMiddleware);\napp.use(someOtherMiddleware);\napp.listen(3000);\n```\n\nIs the same as\n\n```js\napp.use(someMiddleware).use(someOtherMiddleware).listen(3000);\n```\n\nSee [Middleware](https://github.com/koajs/koa/wiki#middleware) for\nmore information.\n\n## app.keys=\n\nSet signed cookie keys.\n\nThese are passed to [KeyGrip](https://github.com/crypto-utils/keygrip),\nhowever you may also pass your own `KeyGrip` instance. For\nexample the following are acceptable:\n\n```js\napp.keys = [\n  'OEK5zjaAMPc3L6iK7PyUjCOziUH3rsrMKB9u8H07La1SkfwtuBoDnHaaPCkG5Brg',\n  'MNKeIebviQnCPo38ufHcSfw3FFv8EtnAe1xE02xkN1wkCV1B2z126U44yk2BQVK7',\n];\napp.keys = new KeyGrip(\n  [\n    'OEK5zjaAMPc3L6iK7PyUjCOziUH3rsrMKB9u8H07La1SkfwtuBoDnHaaPCkG5Brg',\n    'MNKeIebviQnCPo38ufHcSfw3FFv8EtnAe1xE02xkN1wkCV1B2z126U44yk2BQVK7',\n  ],\n  'sha256',\n);\n```\n\nFor security reasons, please ensure that the key is long enough and random.\n\nThese keys may be rotated and are used when signing cookies\nwith the `{ signed: true }` option:\n\n```js\nctx.cookies.set('name', 'tobi', { signed: true });\n```\n\n## app.context\n\n`app.context` is the prototype from which `ctx` is created.\nYou may add additional properties to `ctx` by editing `app.context`.\nThis is useful for adding properties or methods to `ctx` to be used across your entire app,\nwhich may be more performant (no middleware) and/or easier (fewer `require()`s)\nat the expense of relying more on `ctx`, which could be considered an anti-pattern.\n\nFor example, to add a reference to your database from `ctx`:\n\n```js\napp.context.db = db();\n\napp.use(async (ctx) => {\n  console.log(ctx.db);\n});\n```\n\nNote:\n\n- Many properties on `ctx` are defined using getters, setters, and `Object.defineProperty()`. You can only edit these properties (not recommended) by using `Object.defineProperty()` on `app.context`. See https://github.com/koajs/koa/issues/652.\n- Mounted apps currently use their parent's `ctx` and settings. Thus, mounted apps are really just groups of middleware.\n\n## Error Handling\n\nBy default outputs all errors to stderr unless `app.silent` is `true`.\nThe default error handler also won't output errors when `err.status` is `404` or `err.expose` is `true`.\nTo perform custom error-handling logic such as centralized logging you can add an \"error\" event listener:\n\n```js\napp.on('error', (err) => {\n  log.error('server error', err);\n});\n```\n\nIf an error is in the req/res cycle and it is _not_ possible to respond to the client, the `Context` instance is also passed:\n\n```js\napp.on('error', (err, ctx) => {\n  log.error('server error', err, ctx);\n});\n```\n\nWhen an error occurs _and_ it is still possible to respond to the client, aka no data has been written to the socket, Koa will respond\nappropriately with a 500 \"Internal Server Error\". In either case\nan app-level \"error\" is emitted for logging purposes.\n\n<!-- File: packages/koa/docs/api/request.md (Tokens: ~2716 | Category: anthropic-skill) -->\n# Request\n\nA Koa `Request` object is an abstraction on top of node's vanilla request object,\nproviding additional functionality that is useful for every day HTTP server\ndevelopment.\n\n## API\n\n### request.header\n\nRequest header object. This is the same as the [`headers`](https://nodejs.org/api/http.html#http_message_headers) field\non node's [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage).\n\n### request.header=\n\nSet request header object.\n\n### request.headers\n\nRequest header object. Alias as `request.header`.\n\n### request.headers=\n\nSet request header object. Alias as `request.header=`.\n\n### request.method\n\nRequest method.\n\n### request.method=\n\nSet request method, useful for implementing middleware\nsuch as `methodOverride()`.\n\n### request.length\n\nReturn request Content-Length as a number when present, or `undefined`.\n\n### request.url\n\nGet request URL.\n\n### request.url=\n\nSet request URL, useful for url rewrites.\n\n### request.originalUrl\n\nGet request original URL.\n\n### request.origin\n\nGet origin of URL, include `protocol` and `host`.\n\n```js\nctx.request.origin;\n// => http://example.com\n```\n\n### request.href\n\nGet full request URL, include `protocol`, `host` and `url`.\n\n```js\nctx.request.href;\n// => http://example.com/foo/bar?q=1\n```\n\n### request.path\n\nGet request pathname.\n\n### request.path=\n\nSet request pathname and retain query-string when present.\n\n### request.querystring\n\nGet raw query string void of `?`.\n\n### request.querystring=\n\nSet raw query string.\n\n### request.search\n\nGet raw query string with the `?`.\n\n### request.search=\n\nSet raw query string.\n\n### request.host\n\nGet host (hostname:port) when present. Supports `X-Forwarded-Host`\nwhen `app.proxy` is **true**, otherwise `Host` is used.\n\n### request.hostname\n\nGet hostname when present. Supports `X-Forwarded-Host`\nwhen `app.proxy` is **true**, otherwise `Host` is used.\n\nIf host is IPv6, Koa delegates parsing to\n[WHATWG URL API](https://nodejs.org/dist/latest-v8.x/docs/api/url.html#url_the_whatwg_url_api),\n_Note_ This may impact performance.\n\n### request.URL\n\nGet WHATWG parsed URL object.\n\n### request.type\n\nGet request `Content-Type` void of parameters such as \"charset\".\n\n```js\nconst ct = ctx.request.type;\n// => \"image/png\"\n```\n\n### request.charset\n\nGet request charset when present, or `undefined`:\n\n```js\nctx.request.charset;\n// => \"utf-8\"\n```\n\n### request.query\n\nGet parsed query-string, returning an empty object when no\nquery-string is present. Note that this getter does _not_\nsupport nested parsing.\n\nFor example \"color=blue&size=small\":\n\n```js\n{\n  color: 'blue',\n  size: 'small'\n}\n```\n\n### request.query=\n\nSet query-string to the given object. Note that this\nsetter does _not_ support nested objects.\n\n```js\nctx.query = { next: '/login' };\n```\n\n### request.fresh\n\nCheck if a request cache is \"fresh\", aka the contents have not changed. This\nmethod is for cache negotiation between `If-None-Match` / `ETag`, and `If-Modified-Since` and `Last-Modified`. It should be referenced after setting one or more of these response headers.\n\n```js\n// freshness check requires status 20x or 304\nctx.status = 200;\nctx.set('ETag', '123');\n\n// cache is ok\nif (ctx.fresh) {\n  ctx.status = 304;\n  return;\n}\n\n// cache is stale\n// fetch new data\nctx.body = await db.find('something');\n```\n\n### request.stale\n\nInverse of `request.fresh`.\n\n### request.protocol\n\nReturn request protocol, \"https\" or \"http\". Supports `X-Forwarded-Proto`\nwhen `app.proxy` is **true**.\n\n### request.secure\n\nShorthand for `ctx.protocol == \"https\"` to check if a request was\nissued via TLS.\n\n### request.ip\n\nRequest remote address. Supports `X-Forwarded-For` when `app.proxy`\nis **true**.\n\n### request.ips\n\nWhen `X-Forwarded-For` is present and `app.proxy` is enabled an array\nof these ips is returned, ordered from upstream -> downstream. When\ndisabled an empty array is returned.\n\nFor example if the value were \"client, proxy1, proxy2\",\nyou would receive the array `[\"client\", \"proxy1\", \"proxy2\"]`.\n\nMost of the reverse proxy(nginx) set x-forwarded-for via\n`proxy_add_x_forwarded_for`, which poses a certain security risk.\nA malicious attacker can forge a client's ip address by forging\na `X-Forwarded-For`request header. The request sent by the client\nhas an `X-Forwarded-For` request header for 'forged'. After being\nforwarded by the reverse proxy, `request.ips` will be\n['forged', 'client', 'proxy1', 'proxy2'].\n\nKoa offers two options to avoid being bypassed.\n\nIf you can control the reverse proxy, you can avoid bypassing\nby adjusting the configuration, or use the `app.proxyIpHeader`\nprovided by koa to avoid reading `x-forwarded-for` to get ips.\n\n```js\nconst app = new Koa({\n  proxy: true,\n  proxyIpHeader: 'X-Real-IP',\n});\n```\n\nIf you know exactly how many reverse proxies are in front of\nthe server, you can avoid reading the user's forged request\nheader by configuring `app.maxIpsCount`:\n\n```js\nconst app = new Koa({\n  proxy: true,\n  maxIpsCount: 1, // only one proxy in front of the server\n});\n\n// request.header['X-Forwarded-For'] === [ '127.0.0.1', '127.0.0.2' ];\n// ctx.ips === [ '127.0.0.2' ];\n```\n\n### request.subdomains\n\nReturn subdomains as an array.\n\nSubdomains are the dot-separated parts of the host before the main domain of\nthe app. By default, the domain of the app is assumed to be the last two\nparts of the host. This can be changed by setting `app.subdomainOffset`.\n\nFor example, if the domain is \"tobi.ferrets.example.com\":\nIf `app.subdomainOffset` is not set, `ctx.subdomains` is `[\"ferrets\", \"tobi\"]`.\nIf `app.subdomainOffset` is 3, `ctx.subdomains` is `[\"tobi\"]`.\n\n### request.is(types...)\n\nCheck if the incoming request contains the \"Content-Type\"\nheader field, and it contains any of the give mime `type`s.\nIf there is no request body, `null` is returned.\nIf there is no content type, or the match fails `false` is returned.\nOtherwise, it returns the matching content-type.\n\n```js\n// With Content-Type: text/html; charset=utf-8\nctx.is('html'); // => 'html'\nctx.is('text/html'); // => 'text/html'\nctx.is('text/*', 'text/html'); // => 'text/html'\n\n// When Content-Type is application/json\nctx.is('json', 'urlencoded'); // => 'json'\nctx.is('application/json'); // => 'application/json'\nctx.is('html', 'application/*'); // => 'application/json'\n\nctx.is('html'); // => false\n```\n\nFor example if you want to ensure that\nonly images are sent to a given route:\n\n```js\nif (ctx.is('image/*')) {\n  // process\n} else {\n  ctx.throw(415, 'images only!');\n}\n```\n\n### Content Negotiation\n\nKoa's `request` object includes helpful content negotiation utilities powered by [accepts](http://github.com/expressjs/accepts) and [negotiator](https://github.com/federomero/negotiator). These utilities are:\n\n- `request.accepts(types)`\n- `request.acceptsEncodings(types)`\n- `request.acceptsCharsets(charsets)`\n- `request.acceptsLanguages(langs)`\n\nIf no types are supplied, **all** acceptable types are returned.\n\nIf multiple types are supplied, the best match will be returned. If no matches are found, a `false` is returned, and you should send a `406 \"Not Acceptable\"` response to the client.\n\nIn the case of missing accept headers where any type is acceptable, the first type will be returned. Thus, the order of types you supply is important.\n\n### request.accepts(types)\n\nCheck if the given `type(s)` is acceptable, returning the best match when true, otherwise `false`. The `type` value may be one or more mime type string\nsuch as \"application/json\", the extension name\nsuch as \"json\", or an array `[\"json\", \"html\", \"text/plain\"]`.\n\n```js\n// Accept: text/html\nctx.accepts('html');\n// => \"html\"\n\n// Accept: text/*, application/json\nctx.accepts('html');\n// => \"html\"\nctx.accepts('text/html');\n// => \"text/html\"\nctx.accepts('json', 'text');\n// => \"json\"\nctx.accepts('application/json');\n// => \"application/json\"\n\n// Accept: text/*, application/json\nctx.accepts('image/png');\nctx.accepts('png');\n// => false\n\n// Accept: text/*;q=.5, application/json\nctx.accepts(['html', 'json']);\nctx.accepts('html', 'json');\n// => \"json\"\n\n// No Accept header\nctx.accepts('html', 'json');\n// => \"html\"\nctx.accepts('json', 'html');\n// => \"json\"\n```\n\nYou may call `ctx.accepts()` as many times as you like,\nor use a switch:\n\n```js\nswitch (ctx.accepts('json', 'html', 'text')) {\n  case 'json':\n    break;\n  case 'html':\n    break;\n  case 'text':\n    break;\n  default:\n    ctx.throw(406, 'json, html, or text only');\n}\n```\n\n### request.acceptsEncodings(encodings)\n\nCheck if `encodings` are acceptable, returning the best match when true, otherwise `false`. Note that you should include `identity` as one of the encodings!\n\n```js\n// Accept-Encoding: gzip\nctx.acceptsEncodings('gzip', 'deflate', 'identity');\n// => \"gzip\"\n\nctx.acceptsEncodings(['gzip', 'deflate', 'identity']);\n// => \"gzip\"\n```\n\nWhen no arguments are given all accepted encodings\nare returned as an array:\n\n```js\n// Accept-Encoding: gzip, deflate\nctx.acceptsEncodings();\n// => [\"gzip\", \"deflate\", \"identity\"]\n```\n\nNote that the `identity` encoding (which means no encoding) could be unacceptable if the client explicitly sends `identity;q=0`. Although this is an edge case, you should still handle the case where this method returns `false`.\n\n### request.acceptsCharsets(charsets)\n\nCheck if `charsets` are acceptable, returning\nthe best match when true, otherwise `false`.\n\n```js\n// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5\nctx.acceptsCharsets('utf-8', 'utf-7');\n// => \"utf-8\"\n\nctx.acceptsCharsets(['utf-7', 'utf-8']);\n// => \"utf-8\"\n```\n\nWhen no arguments are given all accepted charsets\nare returned as an array:\n\n```js\n// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5\nctx.acceptsCharsets();\n// => [\"utf-8\", \"utf-7\", \"iso-8859-1\"]\n```\n\n### request.acceptsLanguages(langs)\n\nCheck if `langs` are acceptable, returning\nthe best match when true, otherwise `false`.\n\n```js\n// Accept-Language: en;q=0.8, es, pt\nctx.acceptsLanguages('es', 'en');\n// => \"es\"\n\nctx.acceptsLanguages(['en', 'es']);\n// => \"es\"\n```\n\nWhen no arguments are given all accepted languages\nare returned as an array:\n\n```js\n// Accept-Language: en;q=0.8, es, pt\nctx.acceptsLanguages();\n// => [\"es\", \"pt\", \"en\"]\n```\n\n### request.idempotent\n\nCheck if the request is idempotent.\n\n### request.socket\n\nReturn the request socket.\n\n### request.get(field)\n\nReturn request header with case-insensitive `field`.\n\n<!-- File: packages/koa/docs/api/response.md (Tokens: ~2243 | Category: anthropic-skill) -->\n# Response\n\nA Koa `Response` object is an abstraction on top of node's vanilla response object,\nproviding additional functionality that is useful for every day HTTP server\ndevelopment.\n\n## API\n\n### response.header\n\nResponse header object.\n\n### response.headers\n\nResponse header object. Alias as `response.header`.\n\n### response.socket\n\nResponse socket. Points to net.Socket instance as `request.socket`.\n\n### response.status\n\nGet response status. By default, `response.status` is set to `404` unlike node's `res.statusCode` which defaults to `200`.\n\n### response.status=\n\nSet response status via numeric code:\n\n- 100 \"continue\"\n- 101 \"switching protocols\"\n- 102 \"processing\"\n- 200 \"ok\"\n- 201 \"created\"\n- 202 \"accepted\"\n- 203 \"non-authoritative information\"\n- 204 \"no content\"\n- 205 \"reset content\"\n- 206 \"partial content\"\n- 207 \"multi-status\"\n- 208 \"already reported\"\n- 226 \"im used\"\n- 300 \"multiple choices\"\n- 301 \"moved permanently\"\n- 302 \"found\"\n- 303 \"see other\"\n- 304 \"not modified\"\n- 305 \"use proxy\"\n- 307 \"temporary redirect\"\n- 308 \"permanent redirect\"\n- 400 \"bad request\"\n- 401 \"unauthorized\"\n- 402 \"payment required\"\n- 403 \"forbidden\"\n- 404 \"not found\"\n- 405 \"method not allowed\"\n- 406 \"not acceptable\"\n- 407 \"proxy authentication required\"\n- 408 \"request timeout\"\n- 409 \"conflict\"\n- 410 \"gone\"\n- 411 \"length required\"\n- 412 \"precondition failed\"\n- 413 \"payload too large\"\n- 414 \"uri too long\"\n- 415 \"unsupported media type\"\n- 416 \"range not satisfiable\"\n- 417 \"expectation failed\"\n- 418 \"I'm a teapot\"\n- 422 \"unprocessable entity\"\n- 423 \"locked\"\n- 424 \"failed dependency\"\n- 426 \"upgrade required\"\n- 428 \"precondition required\"\n- 429 \"too many requests\"\n- 431 \"request header fields too large\"\n- 500 \"internal server error\"\n- 501 \"not implemented\"\n- 502 \"bad gateway\"\n- 503 \"service unavailable\"\n- 504 \"gateway timeout\"\n- 505 \"http version not supported\"\n- 506 \"variant also negotiates\"\n- 507 \"insufficient storage\"\n- 508 \"loop detected\"\n- 510 \"not extended\"\n- 511 \"network authentication required\"\n\n**NOTE**: don't worry too much about memorizing these strings,\nif you have a typo an error will be thrown, displaying this list\nso you can make a correction.\n\nSince `response.status` default is set to `404`, to send a response\nwithout a body and with a different status is to be done like this:\n\n```js\nctx.response.status = 200;\n\n// Or whatever other status\nctx.response.status = 204;\n```\n\n### response.message\n\nGet response status message. By default, `response.message` is\nassociated with `response.status`.\n\n### response.message=\n\nSet response status message to the given value.\n\n### response.length=\n\nSet response Content-Length to the given value.\n\n### response.length\n\nReturn response Content-Length as a number when present, or deduce\nfrom `ctx.body` when possible, or `undefined`.\n\n### response.body\n\nGet response body.\n\n### response.body=\n\nSet response body to one of the following:\n\n- `string` written\n- `Buffer` written\n- `Stream` piped\n- `Object` || `Array` json-stringified\n- `null` || `undefined` no content response\n\nIf `response.status` has not been set, Koa will automatically set the status to `200` or `204` depending on `response.body`. Specifically, if `response.body` has not been set or has been set as `null` or `undefined`, Koa will automatically set `response.status` to `204`. If you really want to send no content response with other status, you should override the `204` status as the following way:\n\n```js\n// This must be always set first before status, since null | undefined\n// body automatically sets the status to 204\nctx.body = null;\n// Now we override the 204 status with the desired one\nctx.status = 200;\n```\n\nKoa doesn't guard against everything that could be put as a response body -- a function doesn't serialise meaningfully, returning a boolean may make sense based on your application, and while an error works, it may not work as intended as some properties of an error are not enumerable. We recommend adding middleware in your app that asserts body types per app. A sample middleware might be:\n\n```js\napp.use(async (ctx, next) => {\n  await next();\n\n  ctx.assert.equal('object', typeof ctx.body, 500, 'some dev did something wrong');\n});\n```\n\n#### String\n\nThe Content-Type is defaulted to text/html or text/plain, both with\na default charset of utf-8. The Content-Length field is also set.\n\n#### Buffer\n\nThe Content-Type is defaulted to application/octet-stream, and Content-Length\nis also set.\n\n#### Stream\n\nThe Content-Type is defaulted to application/octet-stream.\n\nWhenever a stream is set as the response body, `.onerror` is automatically added as a listener to the `error` event to catch any errors.\nIn addition, whenever the request is closed (even prematurely), the stream is destroyed.\nIf you do not want these two features, do not set the stream as the body directly.\nFor example, you may not want this when setting the body as an HTTP stream in a proxy as it would destroy the underlying connection.\n\nSee: https://github.com/koajs/koa/pull/612 for more information.\n\nHere's an example of stream error handling without automatically destroying the stream:\n\n```js\nconst PassThrough = require('stream').PassThrough;\n\napp.use(async (ctx) => {\n  ctx.body = someHTTPStream.on('error', (err) => ctx.onerror(err)).pipe(PassThrough());\n});\n```\n\n#### Object\n\nThe Content-Type is defaulted to application/json. This includes plain objects `{ foo: 'bar' }` and arrays `['foo', 'bar']`.\n\n### response.get(field)\n\nGet a response header field value with case-insensitive `field`.\n\n```js\nconst etag = ctx.response.get('ETag');\n```\n\n### response.has(field)\n\nReturns `true` if the header identified by name is currently set in the outgoing headers.\nThe header name matching is case-insensitive.\n\n```js\nconst rateLimited = ctx.response.has('X-RateLimit-Limit');\n```\n\n### response.set(field, value)\n\nSet response header `field` to `value`:\n\n```js\nctx.set('Cache-Control', 'no-cache');\n```\n\n### response.append(field, value)\n\nAppend additional header `field` with value `val`.\n\n```js\nctx.append('Link', '<http://127.0.0.1/>');\n```\n\n### response.set(fields)\n\nSet several response header `fields` with an object:\n\n```js\nctx.set({\n  Etag: '1234',\n  'Last-Modified': date,\n});\n```\n\nThis delegates to [setHeader](https://nodejs.org/dist/latest/docs/api/http.html#http_request_setheader_name_value) which sets or updates headers by specified keys and doesn't reset the entire header.\n\n### response.remove(field)\n\nRemove header `field`.\n\n### response.type\n\nGet response `Content-Type` void of parameters such as \"charset\".\n\n```js\nconst ct = ctx.type;\n// => \"image/png\"\n```\n\n### response.type=\n\nSet response `Content-Type` via mime string or file extension.\n\n```js\nctx.type = 'text/plain; charset=utf-8';\nctx.type = 'image/png';\nctx.type = '.png';\nctx.type = 'png';\n```\n\nNote: when appropriate a `charset` is selected for you, for\nexample `response.type = 'html'` will default to \"utf-8\". If you need to overwrite `charset`,\nuse `ctx.set('Content-Type', 'text/html')` to set response header field to value directly.\n\n### response.is(types...)\n\nVery similar to `ctx.request.is()`.\nCheck whether the response type is one of the supplied types.\nThis is particularly useful for creating middleware that\nmanipulate responses.\n\nFor example, this is a middleware that minifies\nall HTML responses except for streams.\n\n```js\nconst minify = require('html-minifier');\n\napp.use(async (ctx, next) => {\n  await next();\n\n  if (!ctx.response.is('html')) return;\n\n  let body = ctx.body;\n  if (!body || body.pipe) return;\n\n  if (Buffer.isBuffer(body)) body = body.toString();\n  ctx.body = minify(body);\n});\n```\n\n### response.redirect(url, [alt])\n\nPerform a [302] redirect to `url`.\n\nThe string \"back\" is special-cased\nto provide Referrer support, when Referrer\nis not present `alt` or \"/\" is used.\n\n```js\nctx.redirect('back');\nctx.redirect('back', '/index.html');\nctx.redirect('/login');\nctx.redirect('http://google.com');\n```\n\nTo alter the default status of `302`, simply assign the status\nbefore or after this call. To alter the body, assign it after this call:\n\n```js\nctx.status = 301;\nctx.redirect('/cart');\nctx.body = 'Redirecting to shopping cart';\n```\n\n### response.attachment([filename], [options])\n\nSet `Content-Disposition` to \"attachment\" to signal the client\nto prompt for download. Optionally specify the `filename` of the\ndownload and some [options](https://github.com/jshttp/content-disposition#options).\n\n### response.headerSent\n\nCheck if a response header has already been sent. Useful for seeing\nif the client may be notified on error.\n\n### response.lastModified\n\nReturn the `Last-Modified` header as a `Date`, if it exists.\n\n### response.lastModified=\n\nSet the `Last-Modified` header as an appropriate UTC string.\nYou can either set it as a `Date` or date string.\n\n```js\nctx.response.lastModified = new Date();\n```\n\n### response.etag=\n\nSet the ETag of a response including the wrapped `\"`s.\nNote that there is no corresponding `response.etag` getter.\n\n```js\nctx.response.etag = crypto.createHash('md5').update(ctx.body).digest('hex');\n```\n\n### response.vary(field)\n\nVary on `field`.\n\n### response.flushHeaders()\n\nFlush any set headers, and begin the body.\n\n<!-- File: packages/koa/docs/error-handling.md (Tokens: ~470 | Category: anthropic-skill) -->\n# Error Handling\n\n## Try-Catch\n\nUsing async functions means that you can try-catch `next`.\nThis example adds a `.status` to all errors:\n\n```js\napp.use(async (ctx, next) => {\n  try {\n    await next();\n  } catch (err) {\n    err.status = err.statusCode || err.status || 500;\n    throw err;\n  }\n});\n```\n\n### Default Error Handler\n\nThe default error handler is essentially a `try-catch` at\nthe very beginning of the middleware chain. To use a\ndifferent error handler, simply put another `try-catch` at\nthe beginning of the middleware chain, and handle the error\nthere. However, the default error handler is good enough for\nmost use cases. It will use a status code of `err.status`,\nor by default 500. If `err.expose` is true, then `err.message`\nwill be the reply. Otherwise, a message generated from the\nerror code will be used (e.g. for the code 500 the message\n\"Internal Server Error\" will be used). All headers will be\ncleared from the request, but any headers in `err.headers`\nwill then be set. You can use a `try-catch`, as specified\nabove, to add a header to this list.\n\nHere is an example of creating your own error handler:\n\n```js\napp.use(async (ctx, next) => {\n  try {\n    await next();\n  } catch (err) {\n    // will only respond with JSON\n    ctx.status = err.statusCode || err.status || 500;\n    ctx.body = {\n      message: err.message,\n    };\n  }\n});\n```\n\n## The Error Event\n\nError event listeners can be specified with `app.on('error')`.\nIf no error listener is specified, a default error listener\nis used. Error listeners receive all errors that make their\nway back through the middleware chain, if an error is caught\nand not thrown again, it will not be passed to the error\nlistener. If no error event listener is specified, then\n`app.onerror` will be used, which simply log the error unless\n`error.expose`is true or `app.silent` is true or `error.status`\nis 404.\n\n<!-- File: packages/koa/docs/faq.md (Tokens: ~436 | Category: anthropic-skill) -->\n# Frequently Asked Questions\n\n## Does Koa replace Express?\n\nIt's more like Connect, but a lot of the Express goodies\nwere moved to the middleware level in Koa to help form\na stronger foundation. This makes middleware more enjoyable\nand less error-prone to write, for the entire stack, not\njust the end application code.\n\nTypically many middleware would\nre-implement similar features, or even worse incorrectly implement them,\nwhen features like signed cookie secrets among others are typically application-specific,\nnot middleware specific.\n\n## Does Koa replace Connect?\n\nNo, just a different take on similar functionality\nnow that async functions allow us to write code with fewer\ncallbacks. Connect is equally capable, and some may still prefer it,\nit's up to what you prefer.\n\n## Does Koa include routing?\n\nNo - out of the box Koa has no form of routing, however\nmany routing middleware are available: https://github.com/koajs/koa/wiki\n\n## Why isn't Koa just Express 4.0?\n\nKoa is a pretty large departure from what people know about Express,\nthe design is fundamentally much different, so the migration from\nExpress 3.0 to this Express 4.0 would effectively mean rewriting\nthe entire application, so we thought it would be more appropriate\nto create a new library.\n\n## What custom properties do the Koa objects have?\n\nKoa uses its own custom objects: `ctx`, `ctx.request`, and `ctx.response`.\nThese objects abstract node's `req` and `res` objects with convenience methods and getters/setters.\nGenerally, properties added to these objects must obey the following rules:\n\n- They must be either very commonly used and/or must do something useful\n- If a property exists as a setter, then it will also exist as a getter, but not vice versa\n\nMany of `ctx.request` and `ctx.response`'s properties are delegated to `ctx`.\nIf it's a getter/setter, then both the getter and the setter will strictly\ncorrespond to either `ctx.request` or `ctx.response`.\n\nPlease think about these rules before suggesting additional properties.\n\n<!-- File: packages/koa/docs/guide.md (Tokens: ~1686 | Category: anthropic-skill) -->\n# Guide\n\nThis guide covers Koa topics that are not directly API related, such as best practices for writing middleware and application structure suggestions. In these examples we use async functions as middleware - you can also use commonFunction or generatorFunction which will be a little different.\n\n## Table of Contents\n\n- [Guide](#guide)\n  - [Table of Contents](#table-of-contents)\n  - [Writing Middleware](#writing-middleware)\n  - [Middleware Best Practices](#middleware-best-practices)\n    - [Middleware options](#middleware-options)\n    - [Named middleware](#named-middleware)\n    - [Combining multiple middleware with koa-compose](#combining-multiple-middleware-with-koa-compose)\n    - [Response Middleware](#response-middleware)\n  - [Async operations](#async-operations)\n  - [Debugging Koa](#debugging-koa)\n\n## Writing Middleware\n\nKoa middleware are simple functions which return a `MiddlewareFunction` with signature (ctx, next). When\nthe middleware is run, it must manually invoke `next()` to run the \"downstream\" middleware.\n\nFor example if you wanted to track how long it takes for a request to propagate through Koa by adding an\n`X-Response-Time` header field the middleware would look like the following:\n\n```js\nasync function responseTime(ctx, next) {\n  const start = Date.now();\n  await next();\n  const ms = Date.now() - start;\n  ctx.set('X-Response-Time', `${ms}ms`);\n}\n\napp.use(responseTime);\n```\n\nIf you're a front-end developer you can think any code before `next();` as the \"capture\" phase,\nwhile any code after is the \"bubble\" phase. This crude gif illustrates how async function allow us\nto properly utilize stack flow to implement request and response flows:\n\n![Koa middleware](/docs/middleware.gif)\n\n1.  Create a date to track response time\n2.  Await control to the next middleware\n3.  Create another date to track duration\n4.  Await control to the next middleware\n5.  Set the response body to \"Hello World\"\n6.  Calculate duration time\n7.  Output log line\n8.  Calculate response time\n9.  Set `X-Response-Time` header field\n10. Hand off to Koa to handle the response\n\nNext we'll look at the best practices for creating Koa middleware.\n\n## Middleware Best Practices\n\nThis section covers middleware authoring best practices, such as middleware\naccepting options, named middleware for debugging, among others.\n\n### Middleware options\n\nWhen creating a public middleware, it's useful to conform to the convention of\nwrapping the middleware in a function that accepts options, allowing users to\nextend functionality. Even if your middleware accepts _no_ options, this is still\na good idea to keep things uniform.\n\nHere our contrived `logger` middleware accepts a `format` string for customization,\nand returns the middleware itself:\n\n```js\nfunction logger(format) {\n  format = format || ':method \":url\"';\n\n  return async function (ctx, next) {\n    const str = format.replace(':method', ctx.method).replace(':url', ctx.url);\n\n    console.log(str);\n\n    await next();\n  };\n}\n\napp.use(logger());\napp.use(logger(':method :url'));\n```\n\n### Named middleware\n\nNaming middleware is optional, however it's useful for debugging purposes to assign a name.\n\n```js\nfunction logger(format) {\n  return async function logger(ctx, next) {};\n}\n```\n\n### Combining multiple middleware with koa-compose\n\nSometimes you want to \"compose\" multiple middleware into a single middleware for easy re-use or exporting. You can use [koa-compose](https://github.com/koajs/compose)\n\n```js\nconst compose = require('koa-compose');\n\nasync function random(ctx, next) {\n  if ('/random' == ctx.path) {\n    ctx.body = Math.floor(Math.random() * 10);\n  } else {\n    await next();\n  }\n}\n\nasync function backwards(ctx, next) {\n  if ('/backwards' == ctx.path) {\n    ctx.body = 'sdrawkcab';\n  } else {\n    await next();\n  }\n}\n\nasync function pi(ctx, next) {\n  if ('/pi' == ctx.path) {\n    ctx.body = String(Math.PI);\n  } else {\n    await next();\n  }\n}\n\nconst all = compose([random, backwards, pi]);\n\napp.use(all);\n```\n\n### Response Middleware\n\nMiddleware that decide to respond to a request and wish to bypass downstream middleware may\nsimply omit `next()`. Typically this will be in routing middleware, but this can be performed by\nany. For example the following will respond with \"two\", however all three are executed, giving the\ndownstream \"three\" middleware a chance to manipulate the response.\n\n```js\napp.use(async function (ctx, next) {\n  console.log('>> one');\n  await next();\n  console.log('<< one');\n});\n\napp.use(async function (ctx, next) {\n  console.log('>> two');\n  ctx.body = 'two';\n  await next();\n  console.log('<< two');\n});\n\napp.use(async function (ctx, next) {\n  console.log('>> three');\n  await next();\n  console.log('<< three');\n});\n```\n\nThe following configuration omits `next()` in the second middleware, and will still respond\nwith \"two\", however the third (and any other downstream middleware) will be ignored:\n\n```js\napp.use(async function (ctx, next) {\n  console.log('>> one');\n  await next();\n  console.log('<< one');\n});\n\napp.use(async function (ctx, next) {\n  console.log('>> two');\n  ctx.body = 'two';\n  console.log('<< two');\n});\n\napp.use(async function (ctx, next) {\n  console.log('>> three');\n  await next();\n  console.log('<< three');\n});\n```\n\nWhen the furthest downstream middleware executes `next();`, it's really yielding to a noop\nfunction, allowing the middleware to compose correctly anywhere in the stack.\n\n## Async operations\n\nAsync function and promise forms Koa's foundation, allowing\nyou to write non-blocking sequential code. For example this middleware reads the filenames from `./docs`,\nand then reads the contents of each markdown file in parallel before assigning the body to the joint result.\n\n```js\nconst fs = require('mz/fs');\n\napp.use(async function (ctx, next) {\n  const paths = await fs.readdir('docs');\n  const files = await Promise.all(paths.map((path) => fs.readFile(`docs/${path}`, 'utf8')));\n\n  ctx.type = 'markdown';\n  ctx.body = files.join('');\n});\n```\n\n## Debugging Koa\n\nKoa along with many of the libraries it's built with support the **DEBUG** environment variable from [debug](https://github.com/visionmedia/debug) which provides simple conditional logging.\n\nFor example\nto see all Koa-specific debugging information just pass `DEBUG=koa*` and upon boot you'll see the list of middleware used, among other things.\n\n```\n$ DEBUG=koa* node --harmony examples/simple\n  koa:application use responseTime +0ms\n  koa:application use logger +4ms\n  koa:application use contentLength +0ms\n  koa:application use notfound +0ms\n  koa:application use response +0ms\n  koa:application listen +0ms\n```\n\nSince JavaScript does not allow defining function names at\nruntime, you can also set a middleware's name as `._name`.\nThis is useful when you don't have control of a middleware's name.\nFor example:\n\n```js\nconst path = require('path');\nconst serve = require('koa-static');\n\nconst publicFiles = serve(path.join(__dirname, 'public'));\npublicFiles._name = 'static /public';\n\napp.use(publicFiles);\n```\n\nNow, instead of just seeing \"serve\" when debugging, you will see:\n\n```\n  koa:application use static /public +0ms\n```\n\n<!-- File: packages/koa/docs/koa-vs-express.md (Tokens: ~771 | Category: anthropic-skill) -->\n# Koa vs Express\n\nPhilosophically, Koa aims to \"fix and replace node\", whereas Express \"augments node\".\nKoa uses promises and async functions to rid apps of callback hell and simplify error handling.\nIt exposes its own `ctx.request` and `ctx.response` objects instead of node's `req` and `res` objects.\n\nExpress, on the other hand, augments node's `req` and `res` objects with additional properties and methods\nand includes many other \"framework\" features, such as routing and templating, which Koa does not.\n\nThus, Koa can be viewed as an abstraction of node.js's `http` modules, where as Express is an application framework for node.js.\n\n|           Feature | Koa | Express | Connect |\n| ----------------: | --- | ------- | ------- |\n| Middleware Kernel | ✓   | ✓       | ✓       |\n|           Routing |     | ✓       |         |\n|        Templating |     | ✓       |         |\n|     Sending Files |     | ✓       |         |\n|             JSONP |     | ✓       |         |\n\nThus, if you'd like to be closer to node.js and traditional node.js-style coding, you probably want to stick to Connect/Express or similar frameworks.\nIf you want to get rid of callbacks, use Koa.\n\nAs result of this different philosophy is that traditional node.js \"middleware\", i.e. functions of the form `(req, res, next)`, are incompatible with Koa. Your application will essentially have to be rewritten from the ground, up.\n\n## Does Koa replace Express?\n\nIt's more like Connect, but a lot of the Express goodies\nwere moved to the middleware level in Koa to help form\na stronger foundation. This makes middleware more enjoyable\nand less error-prone to write, for the entire stack, not\njust the end application code.\n\nTypically many middleware would\nre-implement similar features, or even worse incorrectly implement them,\nwhen features like signed cookie secrets among others are typically application-specific,\nnot middleware specific.\n\n## Does Koa replace Connect?\n\nNo, just a different take on similar functionality\nnow that generators allow us to write code with less\ncallbacks. Connect is equally capable, and some may still prefer it,\nit's up to what you prefer.\n\n## Why isn't Koa just Express 4.0?\n\nKoa is a pretty large departure from what people know about Express,\nthe design is fundamentally much different, so the migration from\nExpress 3.0 to this Express 4.0 would effectively mean rewriting\nthe entire application, so we thought it would be more appropriate\nto create a new library.\n\n## How is Koa different than Connect/Express?\n\n### Promises-based control flow\n\nNo callback hell.\n\nBetter error handling through try/catch.\n\nNo need for domains.\n\n### Koa is barebones\n\nUnlike both Connect and Express, Koa does not include any middleware.\n\nUnlike Express, routing is not provided.\n\nUnlike Express, many convenience utilities are not provided. For example, sending files.\n\nKoa is more modular.\n\n### Koa relies less on middleware\n\nFor example, instead of a \"body parsing\" middleware, you would instead use a body parsing function.\n\n### Koa abstracts node's request/response\n\nLess hackery.\n\nBetter user experience.\n\nProper stream handling.\n\n### Koa routing (third party libraries support)\n\nSince Express comes with its own routing, but Koa does not have\nany in-built routing, there are third party libraries available such as\nkoa-router and koa-route.\nSimilarly, just like we have helmet for security in Express, for Koa\nwe have koa-helmet available and the list goes on for Koa available third\nparty libraries.\n\n<!-- File: packages/koa/docs/migration.md (Tokens: ~969 | Category: anthropic-skill) -->\n# Migrating from Koa v1.x to v2.x\n\n## New middleware signature\n\nKoa v2 introduces a new signature for middleware.\n\n**Old signature middleware (v1.x) support will be removed in v3**\n\nThe new middleware signature is:\n\n```js\n// uses async arrow functions\napp.use(async (ctx, next) => {\n  try {\n    await next(); // next is now a function\n  } catch (err) {\n    ctx.body = { message: err.message };\n    ctx.status = err.status || 500;\n  }\n});\n\napp.use(async (ctx) => {\n  const user = await User.getById(this.session.userid); // await instead of yield\n  ctx.body = user; // ctx instead of this\n});\n```\n\nYou don't have to use asynchronous functions - you just have to pass a function that returns a promise.\nA regular function that returns a promise works too!\n\nThe signature has changed to pass `Context` via an explicit parameter, `ctx` above, instead of via\n`this`. The context passing change makes Koa more compatible with es6 arrow functions, which capture `this`.\n\n## Using v1.x Middleware in v2.x\n\nKoa v2.x will try to convert legacy signature, generator middleware on `app.use`, using [koa-convert](https://github.com/koajs/convert).\nIt is however recommended that you choose to migrate all v1.x middleware as soon as possible.\n\n```js\n// Koa will convert\napp.use(function* (next) {\n  const start = Date.now();\n  yield next;\n  const ms = Date.now() - start;\n  console.log(`${this.method} ${this.url} - ${ms}ms`);\n});\n```\n\nYou could do it manually as well, in which case Koa will not convert.\n\n```js\nconst convert = require('koa-convert');\n\napp.use(\n  convert(function* (next) {\n    const start = Date.now();\n    yield next;\n    const ms = Date.now() - start;\n    console.log(`${this.method} ${this.url} - ${ms}ms`);\n  }),\n);\n```\n\n## Upgrading middleware\n\nYou will have to convert your generators to async functions with the new middleware signature:\n\n```js\napp.use(async (ctx, next) => {\n  const user = await Users.getById(this.session.user_id);\n  await next();\n  ctx.body = { message: 'some message' };\n});\n```\n\nUpgrading your middleware may require some work. One migration path is to update them one-by-one.\n\n1. Wrap all your current middleware in `koa-convert`\n2. Test\n3. `npm outdated` to see which Koa middleware is outdated\n4. Update one outdated middleware, remove using `koa-convert`\n5. Test\n6. Repeat steps 3-5 until you're done\n\n## Updating your code\n\nYou should start refactoring your code now to ease migrating to Koa v2:\n\n- Return promises everywhere!\n- Do not use `yield*`\n- Do not use `yield {}` or `yield []`.\n  - Convert `yield []` into `yield Promise.all([])`\n  - Convert `yield {}` into `yield Bluebird.props({})`\n\nYou could also refactor your logic outside of Koa middleware functions. Create functions like\n`function* someLogic(ctx) {}` and call it in your middleware as\n`const result = yield someLogic(this)`.\nNot using `this` will help migrations to the new middleware signature, which does not use `this`.\n\n## Application object constructor requires new\n\nIn v1.x, the Application constructor function could be called directly, without `new` to\ninstantiate an instance of an application. For example:\n\n```js\nvar koa = require('koa');\nvar app = (module.exports = koa());\n```\n\nv2.x uses es6 classes which require the `new` keyword to be used.\n\n```js\nvar koa = require('koa');\nvar app = (module.exports = new koa());\n```\n\n## ENV specific logging behavior removed\n\nAn explicit check for the `test` environment was removed from error handling.\n\n## Dependency changes\n\n- [co](https://github.com/tj/co) is no longer bundled with Koa. Require or import it directly.\n- [composition](https://github.com/thenables/composition) is no longer used and deprecated.\n\n## v1.x support\n\nThe v1.x branch is still supported but should not receive feature updates. Except for this migration\nguide, documentation will target the latest version.\n\n## Help out\n\nIf you encounter migration related issues not covered by this migration guide, please consider\nsubmitting a documentation pull request.\n\n<!-- File: packages/koa/docs/troubleshooting.md (Tokens: ~1580 | Category: anthropic-skill) -->\n# Troubleshooting Koa\n\n- [Whenever I try to access my route, it sends back a 404](#whenever-i-try-to-access-my-route-it-sends-back-a-404)\n- [My response or context changes have no effect](#my-response-or-context-changes-have-no-effect)\n- [My middleware is not called](#my-middleware-is-not-called)\n\nSee also [debugging Koa](guide.md#debugging-koa).\n\nIf you encounter a problem and later learn how to fix it, and think others might also encounter that problem, please\nconsider contributing to this documentation.\n\n## Whenever I try to access my route, it sends back a 404\n\nThis is a common but troublesome problem when working with Koa middleware. First, it is critical to understand when Koa generates a 404. Koa does not care which or how much middleware was run, in many cases a 200 and 404 trigger the same number of middleware. Instead, the default status for any response is 404. The most obvious way this is changed is through `ctx.status`. However, if `ctx.body` is set when the status has not been explicitly defined (through `ctx.status`), the status is set to 200. This explains why simply setting the body results in a 200. Once the middleware is done (when the middleware and any returned promises are complete), Koa sends out the response. After that, nothing can alter the response. If it was a 404 at the time, it will be a 404 at the end, even if `ctx.status` or `ctx.body` are set afterwords.\n\nEven though we now understand the basis of a 404, it might not be as clear why a 404 is generated in a specific case. This can be especially troublesome when it seems that `ctx.status` or `ctx.body` are set.\n\nThe unexpected 404 is a specific symptom of one of these more general problems:\n\n- [My response or context changes have no effect](#my-response-or-context-changes-have-no-effect)\n- [My middleware is not called](#my-middleware-is-not-called)\n\n## My response or context changes have no effect\n\nThis can be caused when the response is sent before the code making the change is\nexecuted. If the change is to the `ctx.body` or `ctx.status` setter, this can cause a 404 and\nis by far the most common cause of these problems.\n\n### Problematic code\n\n```js\nrouter.get('/fetch', function (ctx, next) {\n  models.Book.findById(parseInt(ctx.query.id)).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\nWhen used, this route will always send back a 404, even though `ctx.body` is set.\n\nThe same behavior would occur in this `async` version:\n\n```js\nrouter.get('/fetch', async (ctx, next) => {\n  models.Book.findById(parseInt(ctx.query.id)).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\n### Cause\n\n`ctx.body` is not set until _after_ the response has been sent. The code doesn't tell Koa to wait for the database to return the record. Koa sends the response after the middleware has been run, but not after the callback inside the middleware has been run. In the gap there, `ctx.body` has not yet been set, so Koa responds with a 404.\n\n### Identifying this as the issue\n\nAdding another piece of middleware and some logging can be extremely helpful in identifying this issue.\n\n```js\nrouter.use('/fetch', function (ctx, next) {\n  return next().then(function () {\n    console.log('Middleware done');\n  });\n});\n\nrouter.get('/fetch', function (ctx, next) {\n  models.Book.findById(parseInt(ctx.query.id)).then(function (book) {\n    ctx.body = book;\n    console.log('Body set');\n  });\n});\n```\n\nIf you see this in the logs:\n\n```\nMiddleware done\nBody set\n```\n\nIt means that the body is being set after the middleware is done, and after the response has been sent. If you see only one or none of these logs, proceed to [My middleware is not called](#my-middleware-is-not-called). If they are in the right order, make sure you haven't explicitly set the status to 404, make sure that it actually is a 404, and if that fails feel free to ask for help.\n\n### Solution\n\n```js\nrouter.get('/fetch', function (ctx, next) {\n  return models.Book.findById(parseInt(ctx.query.id)).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\nReturning the promise given by the database interface tells Koa to wait for the promise to finish before responding. At that time, the body will have been set. This results in Koa sending back a 200 with a proper response.\n\nThe fix in the `async` version is to add an `await` statement:\n\n```js\nrouter.get('/fetch', async (ctx, next) => {\n  await models.Book.findById(parseInt(ctx.query.id)).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\n## My middleware is not called\n\nThis can be due to an interrupted chain of middleware calls. This can cause a 404 if the\nmiddleware that is skipped is responsible for the `ctx.body` or `ctx.status` setter.\nThis is less common than [My response or context changes have no effect](#my-response-or-context-changes-have-no-effect),\nbut it can be a much bigger pain to troubleshoot.\n\n### Problematic code\n\n```js\nrouter.use(function (ctx, next) {\n  // Don't Repeat Yourself! Let's parse the ID here for all our middleware\n  if (ctx.query.id) {\n    ctx.state.id = parseInt(ctx.query.id);\n  }\n});\n\nrouter.get('/fetch', function (ctx, next) {\n  return models.Book.findById(ctx.state.id).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\n### Cause\n\nIn the code above, the book is never fetched from the database, and in fact our route was never called. Look closely at our helper middleware. We forgot to `return next()`! This causes the middleware flow to never reach our route, ending our \"helper\" middleware.\n\n### Identifying this as the issue\n\nIdentifying this problem is easier than most, add a log at the beginning of the route. If it doesn't trigger, your route was never reached in the middleware chain.\n\n```js\nrouter.use(function (ctx, next) {\n  // Don't Repeat Yourself! Let's parse the ID here for all our middleware\n  if (ctx.query.id) {\n    ctx.state.id = parseInt(ctx.query.id);\n  }\n});\n\nrouter.get('/fetch', function (ctx, next) {\n  console.log('Route called'); // Never happens\n  return models.Book.findById(ctx.state.id).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\nTo find the middleware causing the problem, try adding logging at various points in the middleware chain.\n\n### Solution\n\nThe solution for this is rather easy, simply add `return next()` to the end of your helper middleware.\n\n```js\nrouter.use(function (ctx, next) {\n  if (ctx.query.id) {\n    ctx.state.id = parseInt(ctx.query.id);\n  }\n  return next();\n});\n\nrouter.get('/fetch', function (ctx, next) {\n  return models.Book.findById(ctx.state.id).then(function (book) {\n    ctx.body = book;\n  });\n});\n```\n\n<!-- File: packages/skills/AGENTS.md (Tokens: ~3711 | Category: anthropic-skill) -->\n# Skills 编写与评测规范\n\n遵循[根目录 AGENTS.md](../../AGENTS.md) 中的共享仓库指引。以下补充 `packages/skills/` 的专属约定。\n\n`packages/skills/` 目录包含 AI agent skills — 纯 markdown 文档，指导 AI 助手使用 Egg 框架。以 `@eggjs/skills` npm 包发布，仅含 `.md` 文件。\n\n> **Skill 编写基础知识**：SKILL.md 格式、frontmatter 规范、目录结构、progressive disclosure、写作风格等通用知识请使用 `/skill-creator` skill 获取指导。以下仅记录 Egg 项目特有的约定。\n\n## Egg Skills 架构\n\nSkills 采用分层路由模式：\n\n- **入口 skill** (`egg/`) — 分析用户意图，通过关键词匹配和决策逻辑路由到专业 skill\n- **专业 skills** — 提供特定领域的深度指导：\n  - `egg-core/` — 核心概念：模块、依赖注入、生命周期、AccessLevel、后台任务\n  - `egg-controller/` — 实现指导：HTTPController、MCPController、Schedule、Ajv 校验\n\n## Egg Skill Frontmatter 约定\n\n- **`name`**：入口 skill 使用 `egg`，专业 skill 以 `egg-` 为前缀（如 `egg-controller`、`egg-core`）\n- **`allowed-tools`**：统一使用 `Read`（纯文档指导型，不修改文件）\n- **`description`**：\n  - 中文：以\"本技能用于...\"开头，包含触发关键词\n  - 英文：以\"Use when...\"开头，包含触发关键词\n\n## 专业 Skill 编写规范\n\n**入口 Skill（如 `egg/`）编写要点：**\n\n对应 /skill-creator 的 **Workflow-Based** 模式。\n\n1. 决策框架包含明确步骤：识别意图 → 检查模糊意图 → 协议/用例特定指示\n2. 为每个专业 skill 列出中英文触发关键词\n3. 冲突解决规则：明确意图模糊时的优先级（如\"基础优先\"——核心概念优先于控制器实现）\n4. 示例分析：多个完整示例，格式为 用户查询 → 分析 → 决策 → 响应策略\n5. 快速参考表：用户意图 → 关键词 → 推荐 skill 映射\n6. 交叉引用话术：回答完主问题后，附\"如需了解 X，请参阅 `@eggjs/skills-xxx`\"\n\n**专业 Skill 两种组织模式：**\n\n| 模式                               | /skill-creator 对应 | 适用场景           | SKILL.md 内容           | references/ 用途   |\n| ---------------------------------- | ------------------- | ------------------ | ----------------------- | ------------------ |\n| **概念型**（如 `egg-core/`）       | Reference-Based     | 概念解释、架构理解 | 自包含的深度内容        | 更深入的专题文档   |\n| **索引型**（如 `egg-controller/`） | Workflow-Based      | 多种实现方式的选择 | 精简的决策树 + 快速参考 | 每种实现的详细指南 |\n\n**概念型 Skill 内容结构：**\n\n1. 概述（一段话说明覆盖范围）\n2. 按概念分块，每个概念包含：定义 → 使用场景 → 装饰器/API 模式 → 代码示例\n3. 重要约束（反模式、限制）\n4. 快速决策指南表（场景 → 推荐方式）\n5. 最佳实践\n6. 参考资料链接\n\n**索引型 Skill 内容结构：**\n\n1. 决策树（根据需求选择实现方式）\n2. 每种类型的快速参考（装饰器、参数、特点）\n3. 最佳实践\n4. 参考资料链接\n\n## Reference 文档编写要点\n\nReference 文档（`references/*.md`）**不需要 YAML frontmatter**，只有 `SKILL.md` 才需要。\n\n**核心原则：Skill 不是文档的重新排版，而是填补\"文档到生产代码\"之间的缝隙。**\n\nSkill 的价值 = 文档 + 实践经验 - 重复内容。如果内容和 `site/docs/` 中的文档高度重复，说明 skill 写得不对。\n\n| 内容类型            | 该放文档（site/docs/） | 该放 Skill（packages/skills/） |\n| ------------------- | ---------------------- | ------------------------------ |\n| API 签名、参数说明  | Yes                    | No（引用文档即可）             |\n| 易错点 / 常见错误   | 部分                   | **Yes（重点）**                |\n| 完整端到端模式      | No                     | **Yes**                        |\n| 跨模块集成知识      | 散落各处               | **Yes（聚合）**                |\n| 文件放置 / 命名约定 | 部分                   | **Yes**                        |\n| 场景化决策树        | No                     | **Yes**                        |\n\n**编写 Reference 文档的具体步骤：**\n\n1. **先读对应的文档**（如 `site/docs/zh-CN/basics/mcpcontroller.md`），理解已有内容\n2. **向维护者提问，收集文档未覆盖的知识**，重点关注：\n   - 导入路径、命名约定等易错点（AI 最容易犯的错）\n   - 文件应该放在哪个目录？命名规则是什么？\n   - 和其他模块（Module、Service、DI）的集成关系\n   - 哪些配置参数实际开发中需要关心，哪些用默认值即可\n   - 哪些是内部扩展机制不需要暴露给应用开发者\n3. **以常见错误表开头** — 把 AI 最容易写错的地方放在最醒目的位置（如错误的导入路径、错误的 API 用法）\n4. **文件约定** — 目录结构、命名规则、配置文件位置，这些文档里通常不会详细说明\n5. **场景化决策树** — 从用户意图出发（\"让 AI 查数据\"），而非从 API 出发（\"@MCPTool\"）\n6. **端到端完整示例** — 从配置文件到控制器到 Service 到测试，展示所有相关文件和它们的关系\n7. **精简的装饰器对照表放末尾** — 仅作为速查，不展开 API 详解\n\n## 添加新 Skill\n\n1. 在 `packages/skills/` 下创建目录：`packages/skills/<skill-name>/`\n2. 创建 `SKILL.md`（格式规范参考 `/skill-creator`，frontmatter 遵循上述 Egg 约定）\n3. 创建 `references/` 目录（初始为空时放置 `.gitkeep`）\n4. 按需在 `references/*.md` 中添加详细参考文档\n5. 更新入口 skill（`egg/SKILL.md`）的路由逻辑以包含新 skill\n6. 如果 skill 涉及 controller 类型，同时更新 `egg-controller/SKILL.md` 决策树\n\n## 添加新 Reference 文档\n\n1. 在 skill 的 `references/` 目录中创建 `.md` 文件\n2. 遵循命名规范（kebab-case，描述性命名：`http-controller.md`、`mcp-controller.md`）\n3. 包含完整的代码示例和决策树\n4. 更新父级 `SKILL.md` 引用新文档\n5. 如果 `references/` 中已有文件，移除 `.gitkeep`\n\n## Skill 评测\n\n评测用例存放在 `packages/skills/eval/` 目录下，用于验证 AI 使用 skill 后的回答质量。\n\n**评测文件结构：**\n\n```text\npackages/skills/eval/\n├── evals-egg-core.json        # egg-core skill 评测用例\n├── evals-egg-controller.json  # egg-controller skill 评测用例\n├── evals-routing.json         # 入口路由评测用例\n├── .gitignore                 # 忽略 *-workspace/ 目录\n└── <skill-name>-workspace/    # 评测输出（gitignored），由 /skill-creator 管理\n    └── iteration-N/\n        ├── REPORT.md          # 对比评分报告\n        ├── GRADING.md         # with-skill 通过率报告\n        └── {prefix}-{id}/     # 每个用例一个目录\n            ├── eval_metadata.json\n            ├── with_skill/outputs/\n            └── without_skill/outputs/\n```\n\n**评测用例 JSON 格式：**\n\n```json\n{\n  \"skill_name\": \"egg-controller\",\n  \"description\": \"控制器评测：覆盖 http-controller、mcp-controller、schedule、ajv-validate\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"用户的任务描述\",\n      \"expected_output\": \"期望输出的关键要素描述\",\n      \"files\": [{ \"path\": \"相对路径\", \"content\": \"文件内容（可选，用于提供上下文或有 bug 的代码）\" }]\n    }\n  ]\n}\n```\n\n**评测流程：**\n\n使用 `/skill-creator` skill 运行评测和生成结果展示。评测流程概述：\n\n1. **编写评测用例** — 在对应的 `evals-*.json` 中添加用例\n2. **运行评测** — 通过 `/skill-creator` 为每个用例启动两个并行 subagent（with-skill 和 site-docs），使用下方 prompt 模板\n3. **评分和展示** — `/skill-creator` 负责评分、生成对比报告、启动可视化 viewer 供人工 review\n4. **改进 skill** — 根据评分结果和人工 feedback 改进 skill 内容，开启新的 iteration\n\n**评测对比的两组环境：**\n\n每个评测用例需要在两种环境下分别运行，对比 skill 是否有效。Prompt 中不应包含任何流程指引（如\"先判断使用哪个 skill\"），只提供参考资料和访问约束，让 AI 自然行动。\n\n| 环境           | system prompt                                                                         | 可访问范围（prompt 约束）  |\n| -------------- | ------------------------------------------------------------------------------------- | -------------------------- |\n| **with-skill** | `egg/SKILL.md`（入口 skill）的完整内容                                                | 仅 `packages/skills/` 目录 |\n| **site-docs**  | 角色声明 + `site/docs/` 的完整文件目录列表（通过 `find site/docs -name '*.md'` 生成） | 仅 `site/docs/` 目录       |\n\n**Prompt 模板：**\n\nwith-skill 环境：\n\n```text\n你是 EGG 框架开发专家。你只能通过 Read 工具读取 packages/skills/ 目录下的文件，不能访问 site/docs/ 或项目源码。\n\n{egg/SKILL.md 的完整内容}\n\n---\n{eval prompt}\n```\n\nsite-docs 环境：\n\n```text\n你是 EGG 框架开发专家。你只能通过 Read 工具读取 site/docs/ 目录下的文件，不能访问 packages/skills/ 或项目源码。项目文档目录如下：\n\n{完整的 site/docs/ 文件列表，通过 find site/docs -name '*.md' | sort 生成}\n\n---\n{eval prompt}\n```\n\n两组 prompt 的差异仅在于参考资料不同，不包含额外的流程提示。subagent 均具备 Read 工具权限。访问范围通过 prompt 约束（软限制，非技术硬限制）。\n\n两组使用相同的 eval prompt，对比输出质量差异。如果 with-skill 没有明显优于 site-docs，说明 skill 内容需要改进——要么缺少文档未覆盖的知识，要么存在与文档的不必要重复。\n\n**输出目录：**\n\n评测结果保存到 `packages/skills/eval/<skill-name>-workspace/iteration-N/` 目录下（已在 `.gitignore` 中通过 `*-workspace/` 忽略），具体目录结构由 `/skill-creator` 管理。\n\n**评测用例设计原则：**\n\n每个 reference 文档至少覆盖 5 个以上评测用例，需覆盖以下场景类型：\n\n| 场景类型             | 说明                                    | 示例                                                     |\n| -------------------- | --------------------------------------- | -------------------------------------------------------- |\n| **泛化需求描述**     | 不了解框架术语，用口语化描述需求        | \"帮我加个参数校验\"                                       |\n| **精确需求描述**     | 明确指定技术方案和约束                  | \"用 TypeBox 定义 Schema，email 用 format: email\"         |\n| **使用咨询**         | 询问用法、区别、选型                    | \"Optional 和 Null 有什么区别\"                            |\n| **问题排查**         | 提供有 bug 的代码（附 files），要求诊断 | \"校验跑不起来，帮我看看\"                                 |\n| **新项目代码生成**   | 在全新项目中从零开始生成功能代码        | \"帮我写一个创建订单的接口，需要做参数校验\"               |\n| **存量项目代码生成** | 在包含老 egg 代码的项目中生成或迁移代码 | \"帮我把这个老的 egg controller 改成 HTTPController 写法\" |\n| **不常用 API**       | 需要查外部文档链接才能回答              | \"用 Tuple 定义元组校验\"                                  |\n\n<!-- File: packages/skills/egg-controller/references/ajv-validate.md (Tokens: ~1366 | Category: anthropic-skill) -->\n# Ajv 参数校验指南\n\n## 常见错误\n\n| 错误写法                         | 正确写法                          | 说明                                           |\n| -------------------------------- | --------------------------------- | ---------------------------------------------- |\n| `import { Type } from 'typebox'` | `import { Type } from 'egg/ajv'`  | 必须从 `egg/ajv` 导入，内部已封装 typebox      |\n| `import { Ajv } from 'ajv'`      | `import { Ajv } from 'egg/ajv'`   | Ajv 实例通过 `egg/ajv` 导出                    |\n| `new Ajv()` 手动创建实例         | `@Inject() ajv: Ajv` 注入全局单例 | 框架已配置好 formats 和 keywords，不要自行创建 |\n| 在 Service 中做参数校验          | 在 Controller 中做参数校验        | 入参校验应在 Controller 层完成                 |\n\n---\n\n## 核心概念\n\nEgg 通过 `@eggjs/typebox-validate` 插件集成 Ajv（JSON Schema 校验库）和 TypeBox（TypeScript-first 的 JSON Schema 构建器）。**定义一次 Schema，同时获得参数校验和 TypeScript 类型推导。**\n\n### 导入路径\n\n```typescript\n// 所有 Ajv/TypeBox 相关导入统一从 egg/ajv\nimport { Ajv, Type, Static, TransformEnum } from 'egg/ajv';\n```\n\n---\n\n## 使用方式\n\n通过 `@Inject()` 注入全局 Ajv 单例，在 Controller 方法中调用 `ajv.validate()` 进行校验。\n\n### 完整示例\n\n```typescript\n// app/userModule/UserController.ts\nimport { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPBody, Inject } from 'egg';\nimport { Ajv, Type, Static, TransformEnum } from 'egg/ajv';\n\n// 1. 定义 Schema\nconst CreateUserSchema = Type.Object({\n  name: Type.String({\n    transform: [TransformEnum.trim],\n    minLength: 1,\n    maxLength: 50,\n  }),\n  email: Type.String({ format: 'email' }),\n  age: Type.Optional(Type.Integer({ minimum: 0, maximum: 150 })),\n});\n\n// 2. 从 Schema 推导类型\ntype CreateUserParams = Static<typeof CreateUserSchema>;\n\n// 3. 在 Controller 中注入 Ajv 并校验\n@HTTPController()\nexport class UserController {\n  @Inject()\n  private readonly ajv: Ajv;\n\n  @HTTPMethod({\n    method: HTTPMethodEnum.POST,\n    path: '/api/users',\n  })\n  async create(@HTTPBody() body: CreateUserParams) {\n    // 校验失败自动抛出 AjvInvalidParamError\n    this.ajv.validate(CreateUserSchema, body);\n\n    // 校验通过，body 已经有完整类型提示\n    return { name: body.name, email: body.email };\n  }\n}\n```\n\n---\n\n## Schema 定义\n\n### 常用类型\n\n```typescript\nimport { Type } from 'egg/ajv';\n\nType.String(); // string\nType.Number(); // number\nType.Integer(); // 整数\nType.Boolean(); // boolean\nType.Optional(Type.String()); // string | undefined\nType.Array(Type.String()); // string[]\nType.Object({ name: Type.String() }); // { name: string }\nType.Union([Type.String(), Type.Number()]); // string | number\nType.Literal('admin'); // 'admin'\n```\n\n完整的 TypeBox JSON Schema 类型定义参考：https://github.com/sinclairzx81/typebox#json-types\n\n### 内置 format 校验\n\n框架通过 `ajv-formats` 预注册了以下格式：\n\n| format      | 说明       | 示例                                   |\n| ----------- | ---------- | -------------------------------------- |\n| `email`     | 邮箱       | `user@example.com`                     |\n| `uri`       | URI        | `https://example.com`                  |\n| `uuid`      | UUID       | `550e8400-e29b-41d4-a716-446655440000` |\n| `date`      | 日期       | `2024-01-01`                           |\n| `date-time` | 日期时间   | `2024-01-01T00:00:00Z`                 |\n| `time`      | 时间       | `12:00:00`                             |\n| `ipv4`      | IPv4       | `192.168.1.1`                          |\n| `ipv6`      | IPv6       | `::1`                                  |\n| `hostname`  | 主机名     | `example.com`                          |\n| `regex`     | 正则表达式 | `^\\\\d+$`                               |\n\n```typescript\nType.String({ format: 'email' });\nType.String({ format: 'uuid' });\nType.String({ format: 'date-time' });\n```\n\n### transform 预处理\n\n通过 `ajv-keywords` 的 `transform` 关键字，在校验前对字符串做预处理：\n\n```typescript\nimport { TransformEnum } from 'egg/ajv';\n\nType.String({\n  transform: [TransformEnum.trim], // 去除首尾空格\n});\n\nType.String({\n  transform: [TransformEnum.trim, TransformEnum.toLowerCase], // 去空格 + 转小写\n});\n```\n\n---\n\n## 最佳实践\n\n- **Schema 与 Controller 同文件** — Schema 定义放在使用它的 Controller 文件中，保持就近原则\n- **校验在 Controller 层** — 不要在 Service 层做入参校验，Service 信任上层传入的数据\n- **善用 Optional** — 非必填字段用 `Type.Optional()` 包装，避免前端遗漏字段导致校验失败\n- **善用 transform** — 对用户输入做 trim 预处理，减少脏数据\n\n<!-- File: packages/skills/egg-controller/references/http-controller.md (Tokens: ~3327 | Category: anthropic-skill) -->\n# HTTPController 开发指南\n\n## 快速开始\n\n### 创建基本 HTTP 接口\n\n使用 `@HTTPController` 和 `@HTTPMethod` 装饰器创建 HTTP 接口：\n\n```typescript\nimport { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPParam } from 'egg';\n\n@HTTPController()\nexport class DemoController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/hello/:name' })\n  async hello(@HTTPParam() name: string) {\n    return { message: 'hello ' + name };\n  }\n}\n```\n\n### 设置路径前缀\n\n为控制器设置统一路径前缀：\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class PathController {\n  // 最终路径: GET /api/hello\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: 'hello' })\n  async hello() {}\n\n  // 最终路径: POST /api/create\n  @HTTPMethod({ method: HTTPMethodEnum.POST, path: 'create' })\n  async create() {}\n}\n```\n\n---\n\n## 参数装饰器决策\n\n### 决策树：选择合适的参数装饰器\n\n```\n需要从 HTTP 请求获取什么信息？\n\n├─ URL 路径参数\n│  └─ → @HTTPParam()\n│\n├─ URL 查询参数\n│  ├─ 单个值 → @HTTPQuery()\n│  └─ 多个值（数组） → @HTTPQueries()\n│\n├─ 请求体（POST/PUT）\n│  └─ → @HTTPBody()\n│     ├─ json → 对象\n│     ├─ text → 字符串\n│\n├─ 请求头\n│  └─ → @HTTPHeaders()\n│     └─ 注意：key 自动转小写\n│\n├─ Cookie\n│  └─ → @Cookies()\n│\n├─ 原始 HTTP 请求对象\n│  └─ → @Request()\n│     └─ 注意：不要和 @HTTPBody 一起消费请求体\n│\n└─ Egg Context（框架功能）\n   └─ → @Context()\n```\n\n### 参考对照表（参数装饰器）\n\n| 装饰器           | 获取内容         | 类型                  | 默认值 | 支持选项            |\n| ---------------- | ---------------- | --------------------- | ------ | ------------------- |\n| `@HTTPParam()`   | URL 路径参数     | `string`              | 变量名 | `{ name?: string }` |\n| `@HTTPQuery()`   | 查询参数（单个） | `string`              | 变量名 | `{ name?: string }` |\n| `@HTTPQueries()` | 查询参数（多个） | `string[]`            | 变量名 | `{ name?: string }` |\n| `@HTTPBody()`    | 请求体           | `object \\| string`    | -      | -                   |\n| `@HTTPHeaders()` | 请求头           | `IncomingHttpHeaders` | -      | -                   |\n| `@Cookies()`     | Cookie           | `HTTPCookies`         | -      | -                   |\n| `@Request()`     | HTTP 请求对象    | `HTTPRequest`         | -      | -                   |\n| `@Context()`     | Egg Context      | `EggContext`          | -      | -                   |\n\n### 快速选择指南\n\n**需要从 URL 获取参数（id）** → `@HTTPParam`\n**需要从查询字符串获取参数（category=books）** → `@HTTPQuery`\n**需要从查询字符串获取全部值（tag=tech&tag=dev）** → `@HTTPQueries`\n**需要获取请求体（POST JSON）** → `@HTTPBody`\n**需要获取请求头字段** → `@HTTPHeaders`（注意：使用小写key）\n**需要读取 Cookie** → `@Cookies`\n**需要访问原始请求对象** → `@Request`\n\n---\n\n## HTTP 响应\n\n### JSON 响应（默认）\n\n直接返回对象，框架自动序列化为 JSON：\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class JsonController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/data' })\n  async getData() {\n    return { result: 'hello world' };\n  }\n}\n```\n\n### 自定义响应\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class ResponseController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/custom' })\n  async customResponse(@Context() ctx: EggContext) {\n    ctx.status = 201;\n    ctx.set('X-Custom', 'value');\n    ctx.type = 'json';\n    return { message: 'Created' };\n  }\n}\n```\n\n---\n\n## 服务端渲染（SSR）\n\n```typescript\n@HTTPController({ path: '/' })\nexport class SSRController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })\n  async render(@Context() ctx: EggContext) {\n    ctx.type = 'html';\n    return `\n      <!DOCTYPE html>\n      <html>\n        <head><title>Home</title></head>\n        <body><h1>Hello World</h1></body>\n      </html>\n    `;\n  }\n}\n```\n\n---\n\n## 流式响应（Streaming）\n\n```typescript\nimport { Readable } from 'node:stream';\nimport { setTimeout } from 'node:timers/promises';\n\nasync function* generateHtml() {\n  yield '<html><body>';\n  for (let i = 1; i <= 5; i++) {\n    yield `<p>Chunk ${i}</p>`;\n    await setTimeout(1000);\n  }\n  yield '</body></html>';\n}\n\n@HTTPController({ path: '/api' })\nexport class StreamController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/stream' })\n  async streamHtml(@Context() ctx: EggContext) {\n    ctx.type = 'html';\n    return Readable.from(generateHtml());\n  }\n}\n```\n\n---\n\n## 路由优先级管理\n\n### 默认优先级规则\n\n| Path                            | RegExp Index | Priority | 说明             |\n| ------------------------------- | ------------ | -------- | ---------------- |\n| `/*`                            | `[0]`        | 0        | 通配符，最低     |\n| `/hello/:name`                  | `[1]`        | 1000     | 单参数           |\n| `/hello/world/message/:message` | `[3]`        | 3000     | 三参数           |\n| `/hello/:name/message/:message` | `[1, 3]`     | 4000     | 多参数，索引更大 |\n| `/hello/world`                  | `[]`         | 100000   | 静态路径，最高   |\n\n### 手动设置优先级\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class PriorityController {\n  @HTTPMethod({\n    method: HTTPMethodEnum.GET,\n    path: '/(api|openapi)/version',\n    priority: 100000, // 提升优先级\n  })\n  async high() {}\n\n  @HTTPMethod({\n    method: HTTPMethodEnum.POST,\n    path: '/(api|openapi)/(.+)',\n  })\n  async low() {}\n}\n```\n\n---\n\n## 参数装饰器详解\n\n### @HTTPParam\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：从 URL 路径中提取参数\n\n**语法**：`@HTTPParam(param?: HTTPParamParams)`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api/users' })\nexport class UserController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: ':userId/posts/:postId' })\n  async getPost(@HTTPParam() userId: string, @HTTPParam() postId: string) {\n    return { userId, postId };\n  }\n}\n```\n\n#### 使用要点\n\n- 参数类型必须是 `string`\n- 参数名默认和变量名相同\n- 支持正则表达式捕获：`path: '/files/(.*)'`\n- 使用 `{ name: '0' }` 获取正则第一个匹配\n- 支持多参数路径\n\n#### 示例\n\n```typescript\n// 路径参数\n@HTTPController({ path: '/api/users' })\nexport class UserController {\n  // GET /users/:id\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: ':id' })\n  async getUser(@HTTPParam() id: string) {\n    return { userId: id };\n  }\n\n  // GET /users/:userId/posts/:postId\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: ':userId/posts/:postId' })\n  async getPost(@HTTPParam() userId: string, @HTTPParam() postId: string) {\n    return { userId, postId };\n  }\n\n  // 获取正则匹配\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/files/(.*)' })\n  async getFile(@HTTPParam({ name: '0' }) path: string) {\n    return { path };\n  }\n}\n```\n\n---\n\n### @HTTPQuery\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：从 URL 查询字符串提取参数（`?key=value`）\n\n**语法**：\n\n- `@HTTPQuery(param?: HTTPQueryParams)` - 返回首个匹配的值（`string`）\n- `@HTTPQueries(param?: HTTPQueriesParams)` - 返回全部值的数组（`string[]`）\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api/search' })\nexport class SearchController {\n  // GET /api/search?category=books\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })\n  async searchByCategory(@HTTPQuery() category: string) {\n    return { category };\n  }\n\n  // GET /api/search?tag=tech&tag=dev\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })\n  async searchByTags(@HTTPQueries({ name: 'tag' }) tags: string[]) {\n    return { tags };\n  }\n}\n```\n\n#### 使用要点\n\n- `@HTTPQuery`：返回首个匹配的值\n- `@HTTPQueries`：返回全部值的数组\n- 参数名默认与变量名匹配\n- 使用 `{ name: 'key' }` 指定查询参数名\n\n---\n\n### @HTTPBody\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：从请求体提取数据（POST/PUT 请求的 body）\n\n**语法**：`@HTTPBody()`\n\n**类型**：`object | string | FormData`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api/users' })\nexport class UserController {\n  @HTTPMethod({ method: HTTPMethodEnum.POST, path: '/' })\n  async createUser(@HTTPBody() body: { name: string; email: string }) {\n    return { userId: '123', ...body };\n  }\n}\n```\n\n#### Content-Type 解析\n\n| Content-Type                        | 解析结果        |\n| ----------------------------------- | --------------- |\n| `application/json`                  | 对象 `object`   |\n| `text/plain`                        | 字符串 `string` |\n| `application/x-www-form-urlencoded` | 对象 `object`   |\n\n**注意**：其他类型注入空值，需用 `@Request` 手动处理\n\n---\n\n### @HTTPHeaders\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：获取 HTTP 请求头中的字段\n\n**语法**：`@HTTPHeaders()`\n\n**类型**：`IncomingHttpHeaders`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class HeaderController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/info' })\n  async getInfo(@HTTPHeaders() headers: IncomingHttpHeaders) {\n    const auth = headers['authorization'];\n    const custom = headers['x-custom'];\n    return { auth, custom };\n  }\n}\n```\n\n#### 使用要点\n\n- Headers key 会自动转为小写，取值时使用小写字符\n- 获取单个值：`headers['x-custom']`\n\n---\n\n### @Cookies\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：从 HTTP Cookie 中读取会话数据\n\n**语法**：`@Cookies()`\n\n**类型**：`HTTPCookies`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class SessionController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/session' })\n  async getSession(@Cookies() cookies: HTTPCookies) {\n    const session = cookies.get('sessionId');\n    return { session };\n  }\n}\n```\n\n#### 使用要点\n\n- 使用 `cookies.get(key)` 读取 Cookie\n- 使用 `{ signed: false }` 读取未签名的 Cookie\n\n---\n\n### @Request\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：访问完整的 HTTP 请求对象\n\n**语法**：`@Request()`\n\n**类型**：`HTTPRequest`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class RequestController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/debug' })\n  async getDebug(@Request() request: HTTPRequest) {\n    const url = request.url;\n    const method = request.method;\n    const contentType = request.headers.get('content-type');\n    const rawBody = await request.text();\n    return { url, method, contentType, bodyLength: rawBody.length };\n  }\n}\n```\n\n#### 使用要点\n\n- `request.url`：请求 URL\n- `request.method`：请求方法\n- `request.headers`：Headers 对象\n- `request.text()`：读取请求体为文本\n- `request.arrayBuffer()`：读取请求体为 ArrayBuffer\n\n### @Context\n\n**装饰器类型**：参数装饰器（Parameter Decorator）\n\n**使用场景**：访问 Egg 框架的 Context 对象\n\n**语法**：`@Context()`\n\n**类型**：`EggContext`\n\n#### 快速参考\n\n```typescript\n@HTTPController({ path: '/api' })\nexport class DebugController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/debug' })\n  async debug(@Context() ctx: EggContext) {\n    return {\n      app: ctx.app.name,\n      ip: ctx.ip,\n      userAgent: ctx.get('user-agent'),\n    };\n  }\n}\n```\n\n<!-- File: packages/skills/egg-controller/references/mcp-controller.md (Tokens: ~2332 | Category: anthropic-skill) -->\n# MCPController 开发指南\n\n## 常见错误\n\n生成 MCPController 代码时，**必须**注意以下易错点：\n\n| 错误写法                         | 正确写法                              | 说明                                     |\n| -------------------------------- | ------------------------------------- | ---------------------------------------- |\n| `from 'egg'`                     | `from '@eggjs/tegg'`                  | 所有 MCP 装饰器和类型来自 `@eggjs/tegg`  |\n| `import z from 'zod'`            | `import { z } from '@eggjs/tegg/zod'` | 框架内置 zod，必须使用具名导入           |\n| `z.object({ name: z.string() })` | `{ name: z.string() }`                | Schema 使用普通对象，不要用 `z.object()` |\n| `args: ToolArgs<MySchema>`       | `args: ToolArgs<typeof MySchema>`     | 类型参数必须用 `typeof`                  |\n| `@MCPController` 不加括号        | `@MCPController()`                    | 装饰器必须带括号调用                     |\n\n---\n\n## 文件约定\n\n### 文件位置与命名\n\nMCPController 放在 module 的 `controller/` 目录下，命名规则为 `{Name}MCPController.ts`：\n\n```\napp/module-name/\n├── controller/\n│   ├── PackageMCPController.ts    ← MCP 控制器\n│   └── PackageHTTPController.ts   ← 同模块可共存 HTTP 控制器\n└── service/\n    └── PackageService.ts\n```\n\n### 插件配置\n\n在 `config/plugin.ts` 中启用：\n\n```typescript\nplugin.mcpProxy = true;\n```\n\n### 路径配置\n\n在 `config/config.default.ts` 中配置 MCP 路径（通常不需要修改，以下为默认值）：\n\n```typescript\nimport { randomUUID } from 'node:crypto';\n\nexport default () => {\n  const config = {\n    mcp: {\n      sseInitPath: '/mcp/sse',\n      sseMessagePath: '/mcp/message',\n      streamPath: '/mcp/stream',\n      statelessStreamPath: '/mcp/stateless/stream',\n      sessionIdGenerator: randomUUID,\n    },\n  };\n  return config;\n};\n```\n\n当使用 `@MCPController({ name: 'myServer' })` 声明命名服务时，路径自动变为：\n\n- `/mcp/myServer/sse`\n- `/mcp/myServer/message`\n- `/mcp/myServer/stream`\n- `/mcp/myServer/stateless/stream`\n\n### AccessLevel\n\n`@MCPController` 装饰器内部已默认设置 AccessLevel（PUBLIC），不需要再手动声明。\n\n---\n\n## 场景决策树\n\n```\n用户需要什么？\n\n├─ \"让 AI 能查数据 / 执行操作\"\n│  └─ → @MCPTool + @Inject Service 处理业务\n│\n├─ \"给 AI 一个提示词模板\"\n│  └─ → @MCPPrompt\n│\n├─ \"让 AI 读取某类资源数据\"\n│  ├─ 资源地址固定 → @MCPResource({ uri: '...' })\n│  └─ 资源地址动态 → @MCPResource({ template: [...] })\n│\n├─ \"Tool 执行中要推送进度\"\n│  └─ → @MCPTool + @Extra() 获取 sendNotification（见下方 @Extra 章节）\n│\n└─ \"Tool 中需要读取自定义请求头\"\n   └─ → @MCPTool + @Extra() 获取 requestInfo.headers\n```\n\n---\n\n## 端到端完整示例\n\n以下展示一个完整的 MCP 功能从配置到测试的所有文件：\n\n### 1. 插件配置 — `config/plugin.ts`\n\n```typescript\nplugin.mcpProxy = true;\n```\n\n### 2. 控制器 — `app/npm/controller/PackageMCPController.ts`\n\n```typescript\nimport {\n  MCPController,\n  MCPTool,\n  MCPToolResponse,\n  MCPPrompt,\n  MCPPromptResponse,\n  MCPResource,\n  MCPResourceResponse,\n  ToolArgs,\n  ToolArgsSchema,\n  PromptArgs,\n  PromptArgsSchema,\n  Inject,\n} from '@eggjs/tegg';\nimport { z } from '@eggjs/tegg/zod';\n\nimport { PackageService } from '../service/PackageService.ts';\n\nconst SearchSchema = {\n  name: z.string({ description: 'npm package name' }),\n};\n\nconst SummarySchema = {\n  name: z.string(),\n};\n\n@MCPController()\nexport class PackageMCPController {\n  @Inject()\n  private readonly packageService: PackageService;\n\n  @MCPTool({ description: 'Search npm package info' })\n  async searchPackage(@ToolArgsSchema(SearchSchema) args: ToolArgs<typeof SearchSchema>): Promise<MCPToolResponse> {\n    const pkg = await this.packageService.findByName(args.name);\n    if (!pkg) {\n      return { content: [{ type: 'text', text: `Package ${args.name} not found` }] };\n    }\n    return { content: [{ type: 'text', text: JSON.stringify(pkg) }] };\n  }\n\n  @MCPPrompt({ description: 'Generate package summary' })\n  async summarize(@PromptArgsSchema(SummarySchema) args: PromptArgs<typeof SummarySchema>): Promise<MCPPromptResponse> {\n    return {\n      messages: [\n        {\n          role: 'user',\n          content: {\n            type: 'text',\n            text: `Summarize the npm package: ${args.name}`,\n          },\n        },\n      ],\n    };\n  }\n\n  @MCPResource({\n    template: ['npm://{name}/{?version}', { list: undefined }],\n  })\n  async getPackageReadme(uri: URL): Promise<MCPResourceResponse> {\n    const name = uri.hostname;\n    const readme = await this.packageService.getReadme(name);\n    return { contents: [{ uri: uri.toString(), text: readme }] };\n  }\n}\n```\n\n### 3. Service — `app/npm/service/PackageService.ts`\n\n```typescript\nimport { SingletonProto } from 'egg';\n\n@SingletonProto()\nexport class PackageService {\n  async findByName(name: string) {\n    // 业务逻辑\n  }\n\n  async getReadme(name: string): Promise<string> {\n    // 业务逻辑\n  }\n}\n```\n\n### 4. 单元测试 — `test/npm/controller/PackageMCPController.test.ts`\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from 'egg-mock/bootstrap';\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\n\ndescribe('PackageMCPController', () => {\n  it('should search package via tool', async () => {\n    app.mockCsrf();\n    const client: Client = await app.mcpClient();\n\n    const tools = await client.listTools();\n    assert(tools.tools.some((t) => t.name === 'searchPackage'));\n\n    const res = await client.callTool({\n      name: 'searchPackage',\n      arguments: { name: 'egg' },\n    });\n    assert(res.content[0].type === 'text');\n  });\n\n  it('should get prompt', async () => {\n    app.mockCsrf();\n    const client: Client = await app.mcpClient();\n\n    const res = await client.getPrompt({\n      name: 'summarize',\n      arguments: { name: 'egg' },\n    });\n    assert(res.messages.length > 0);\n  });\n\n  it('should read resource', async () => {\n    app.mockCsrf();\n    const client: Client = await app.mcpClient();\n\n    const res = await client.readResource({\n      uri: 'npm://egg?version=4.0.0',\n    });\n    assert(res.contents.length > 0);\n  });\n});\n```\n\n---\n\n## @Extra() 的使用场景\n\n`@Extra()` 装饰器注入 `ToolExtra` 对象，提供两个能力：\n\n### 发送通知（长任务进度推送）\n\n```typescript\n@MCPTool()\nasync longTask(\n  @ToolArgsSchema(Schema) args: ToolArgs<typeof Schema>,\n  @Extra() extra: ToolExtra,\n): Promise<MCPToolResponse> {\n  const { sendNotification } = extra;\n  for (let i = 0; i < 10; i++) {\n    await sendNotification({\n      method: 'notifications/message',\n      params: { level: 'info', data: `Step ${i + 1}/10` },\n    });\n    // ... 执行步骤\n  }\n  return { content: [{ type: 'text', text: 'Done' }] };\n}\n```\n\n### 读取自定义请求头\n\n```typescript\n@MCPTool()\nasync myTool(\n  @ToolArgsSchema(Schema) args: ToolArgs<typeof Schema>,\n  @Extra() extra: ToolExtra,\n): Promise<MCPToolResponse> {\n  const headers = extra.requestInfo?.headers;\n  // 处理自定义 header\n}\n```\n\n---\n\n## 装饰器参考\n\n| 装饰器                | 用途         | 常用参数                                   | 返回类型              |\n| --------------------- | ------------ | ------------------------------------------ | --------------------- |\n| `@MCPController()`    | 声明控制器   | `{ name?: string }`                        | -                     |\n| `@MCPTool()`          | 声明工具     | `{ name?: string, description?: string }`  | `MCPToolResponse`     |\n| `@MCPPrompt()`        | 声明提示词   | `{ name?: string, description?: string }`  | `MCPPromptResponse`   |\n| `@MCPResource()`      | 声明资源     | `{ uri: string }` 或 `{ template: [...] }` | `MCPResourceResponse` |\n| `@ToolArgsSchema()`   | Tool 参数    | Zod Schema 普通对象                        | -                     |\n| `@PromptArgsSchema()` | Prompt 参数  | Zod Schema 普通对象                        | -                     |\n| `@Extra()`            | 额外上下文   | -                                          | `ToolExtra`           |\n| `@Inject()`           | 注入 Service | -                                          | -                     |\n\n**注意**：`@MCPController` 的 `version`、`timeout` 等参数通常不需要配置。\n\n<!-- File: packages/skills/egg-controller/references/middleware.md (Tokens: ~1480 | Category: anthropic-skill) -->\n# Middleware 中间件指南\n\n## 常见错误\n\n| 错误写法                                   | 正确写法                           | 说明                                            |\n| ------------------------------------------ | ---------------------------------- | ----------------------------------------------- |\n| `import { Middleware } from '@eggjs/tegg'` | `import { Middleware } from 'egg'` | Middleware 从 `egg` 导入                        |\n| `import { Advice } from 'egg'`             | `import { Advice } from 'egg/aop'` | AOP 装饰器从 `egg/aop` 导入                     |\n| `@Middleware(funcMw, AdviceClass)`         | 分开写两个 `@Middleware`           | 同一个 `@Middleware()` 中不能混用函数式和 AOP   |\n| AOP Advice 中用实例属性存请求级状态        | 使用 `ctx.set()`/`ctx.get()`       | Advice 默认 Singleton，实例属性会被并发请求共享 |\n| 把中间件文件放在 `app/middleware/`         | 放在模块目录下                     | 函数式中间件放在模块中，通过 import 引用        |\n\n---\n\n## 两种中间件模式\n\nEgg 的 `@Middleware` 装饰器支持两种中间件写法，根据传入参数类型自动识别：\n\n- **AOP 写法（推荐）**：使用 `@Advice` 类，支持 `@Inject` 注入 Proto 依赖，拥有丰富的生命周期钩子\n- **函数式写法（旧版兼容）**：标准 Koa 中间件函数，需要从 `ctx` 对象上手动获取依赖\n\n新项目应优先使用 AOP 写法。函数式写法主要用于兼容旧的 egg 中间件或非常简单的场景。\n\n---\n\n## 实现中间件\n\n### AOP 写法（推荐）\n\n使用 `@Advice()` 装饰器定义类，实现 `IAdvice` 的 `around` 方法，写法与 Koa 中间件一致（`next` 调用目标方法）。Advice 本身是 Proto，支持 `@Inject` 注入依赖。`around` 中可以修改入参（`ctx.args`）和返回值：\n\n```typescript\n// app/modules/foo/advice/LogAdvice.ts\nimport { AccessLevel, Inject, Logger } from 'egg';\nimport { Advice, IAdvice, AdviceContext } from 'egg/aop';\n\n// 跨模块使用时需设置 accessLevel: AccessLevel.PUBLIC\n@Advice({ accessLevel: AccessLevel.PUBLIC })\nexport class LogAdvice implements IAdvice {\n  @Inject()\n  logger: Logger;\n\n  async around(ctx: AdviceContext, next: () => Promise<any>): Promise<any> {\n    // 修改入参：ctx.args 对应控制器方法的参数列表\n    // ctx.args[0] = sanitize(ctx.args[0]);\n\n    const start = Date.now();\n    const result = await next();\n    this.logger.info('%s cost %dms', ctx.method, Date.now() - start);\n\n    // 修改返回值：直接返回新的值即可\n    return { success: true, data: result };\n  }\n}\n```\n\n### 函数式写法（旧版兼容）\n\n标准 Koa 中间件函数，签名为 `(ctx: Context, next: Next) => Promise<void>`。旧版 egg 写法，无法使用 `@Inject`，需要从 `ctx` 对象上手动获取依赖：\n\n```typescript\n// app/modules/foo/middleware/count.ts\nimport type { Context, Next } from 'egg';\n\nexport async function countMw(ctx: Context, next: Next): Promise<void> {\n  const start = Date.now();\n  await next();\n  ctx.set('X-Response-Time', `${Date.now() - start}ms`);\n}\n```\n\n---\n\n## 应用中间件\n\n通过 `@Middleware()` 装饰器将中间件应用到控制器，支持类级别和方法级别：\n\n```typescript\n// app/modules/foo/FooController.ts\nimport { HTTPController, HTTPMethod, HTTPMethodEnum, Middleware } from 'egg';\nimport { LogAdvice } from '../common/advice/LogAdvice.ts';\nimport { countMw } from './middleware/count.ts';\n\n@HTTPController({ path: '/api' })\n@Middleware(LogAdvice) // 类级别：所有方法都会执行\nexport class FooController {\n  @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/profile' })\n  @Middleware(countMw) // 方法级别：仅此方法执行\n  async getProfile() {\n    return { name: 'test' };\n  }\n}\n```\n\n---\n\n## 执行顺序\n\n遵循洋葱模型，类级别先执行，方法级别后执行：\n\n```typescript\n@Middleware(globalMw)\nexport class FooController {\n  // 进：globalMw → methodMw → hello()\n  // 出：hello() → methodMw → globalMw\n  @Middleware(methodMw)\n  async hello() {}\n\n  // 多个 @Middleware 从下往上执行（靠近方法的先注册）\n  // 进：globalMw → mw3 → mw2 → mw1 → multiple()\n  // 出：multiple() → mw1 → mw2 → mw3 → globalMw\n  @Middleware(mw1)\n  @Middleware(mw2)\n  @Middleware(mw3)\n  async multiple() {}\n}\n```\n\n**若混用函数式和 AOP 中间件，所有函数式中间件（无论类级别还是方法级别）会先于所有 AOP 中间件执行。** 即函数式和 AOP 分属两个独立的执行阶段，函数式阶段在前，AOP 阶段在后：\n\n```typescript\n@Middleware(countMw) // 函数式 - 类级别\n@Middleware(LogAdvice) // AOP - 类级别\nexport class FooController {\n  @Middleware(timeMw) // 函数式 - 方法级别\n  @Middleware(AuthAdvice) // AOP - 方法级别\n  async hello() {}\n}\n\n// 实际执行顺序：\n// countMw → timeMw → LogAdvice → AuthAdvice → hello()\n// （先所有函数式，再所有 AOP；各阶段内类级别先于方法级别）\n```\n\n<!-- File: packages/skills/egg-controller/references/schedule.md (Tokens: ~989 | Category: anthropic-skill) -->\n# 定时任务开发指南\n\n## 注意事项\n\n- **不要将代码放在 `app/schedule` 目录下**，egg 默认会扫描该路径注册定时任务，会和装饰器方式冲突\n- 定时任务类必须包含一个 `subscribe` 方法，框架调度时会调用该方法\n- import 路径是 `egg/schedule`，不是 `egg`\n\n## Step 1: 创建定时任务\n\n使用 `@Schedule` 装饰器标识一个类为定时任务，支持 interval 和 cron 两种调度模式。\n\n### interval 模式\n\n按固定间隔执行。`interval` 支持毫秒数或 [ms](https://github.com/vercel/ms) 格式字符串（如 `'5s'`、`'1m'`）。\n\n```typescript\n// app/{moduleName}/schedule/Demo.ts\nimport { Inject, Logger } from 'egg';\nimport { IntervalParams, Schedule, ScheduleType } from 'egg/schedule';\n\n@Schedule<IntervalParams>({\n  type: ScheduleType.WORKER,\n  scheduleData: {\n    interval: '5s',\n  },\n})\nexport class DemoScheduler {\n  @Inject()\n  private logger: Logger;\n\n  async subscribe() {\n    this.logger.info('schedule called');\n  }\n}\n```\n\n### cron 模式\n\n按 cron 表达式执行，格式参考 [cron-parser](https://github.com/harrisiirak/cron-parser)：\n\n```text\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```typescript\n// app/{moduleName}/schedule/CronDemo.ts\nimport { Inject, Logger } from 'egg';\nimport { CronParams, Schedule, ScheduleType } from 'egg/schedule';\n\n@Schedule<CronParams>({\n  type: ScheduleType.WORKER,\n  scheduleData: {\n    cron: '0 0 3 * * *', // 每日 3 点执行\n  },\n})\nexport class CronScheduler {\n  @Inject()\n  private logger: Logger;\n\n  async subscribe() {\n    this.logger.info('schedule called');\n  }\n}\n```\n\n## Step 2: 选择工作模式\n\n| 模式                  | 说明                                     | 使用场景                             |\n| --------------------- | ---------------------------------------- | ------------------------------------ |\n| `ScheduleType.WORKER` | 每台机器只有一个 worker 执行（随机选择） | 大多数场景，如数据同步、缓存刷新     |\n| `ScheduleType.ALL`    | 每台机器的所有 worker 都执行             | 需要每个 worker 都更新本地状态的场景 |\n\n## Step 3: 配置运行参数\n\n`@Schedule` 装饰器支持第二个参数，控制定时任务的运行行为：\n\n```typescript\n@Schedule<IntervalParams>(\n  {\n    type: ScheduleType.WORKER,\n    scheduleData: {\n      interval: '1m',\n    },\n  },\n  {\n    immediate: true, // 应用启动后立即执行一次\n    // disable: true,           // 禁用该定时任务\n    env: ['devserver', 'test'], // 仅在指定环境下启动\n  },\n)\nexport class MyScheduler {\n  async subscribe() {\n    // ...\n  }\n}\n```\n\n| 参数        | 类型     | 说明                            |\n| ----------- | -------- | ------------------------------- |\n| `immediate` | boolean  | 应用启动并 ready 后立即执行一次 |\n| `disable`   | boolean  | 设为 true 时不启动该定时任务    |\n| `env`       | string[] | 仅在指定环境下启动              |\n\n<!-- File: packages/skills/egg-controller/SKILL.md (Tokens: ~1371 | Category: anthropic-skill) -->\n---\nname: egg-controller\ndescription: Use when creating API endpoints, implementing protocol handlers, exposing interfaces for specific clients, adding parameter validation, or applying middleware to controllers. Covers HTTP, MCP and Schedule controllers, Ajv/TypeBox parameter validation, and Middleware (function-style and AOP) for EGG framework applications.\nallowed-tools: Read\n---\n\n# EGG 控制器\n\n---\n\n## 控制器选择决策树\n\n```\n需要暴露什么接口/客户端协议？\n\n1. HTTP 接口？例如 HTML/JSON/SSR/SSE，可以使用 HTTPController，参考 `references/http-controller.md`\n\n2. 定时任务，可以使用 Schedule，参考 `references/schedule.md`\n\n3. MCP 接口，可以使用 MCPController，参考 `references/mcp-controller.md`\n\n需要做参数校验？\n\n4. 使用 Ajv + TypeBox 做入参校验，参考 `references/ajv-validate.md`\n\n需要给控制器加中间件（日志、鉴权、耗时统计等横切逻辑）？\n\n5. 使用 Middleware，支持函数式写法和 AOP 写法，参考 `references/middleware.md`\n```\n\n---\n\n## 控制器快速参考\n\n### HTTPController\n\n- **装饰器**：`@HTTPController`、`@HTTPMethod`\n- **参数**：`@HTTPParam`、`@HTTPQuery`、`@HTTPBody`、`@HTTPHeaders`、`@Cookies`、`@Request`、`@Context`\n- **详细文档**：`references/http-controller.md`\n\n### MCPController\n\n- **装饰器**：`@MCPController`、`@MCPTool`、`@MCPPrompt`、`@MCPResource`\n- **特点**：集成 LLM、Zod 验证、登录态支持\n- **详细文档**：`references/mcp-controller.md`\n\n### Schedule\n\n- **装饰器**：`@Schedule<T>`\n- **模式**：Worker/All\n- **详细文档**：`references/schedule.md`\n\n### Ajv 参数校验\n\n- **导入**：`import { Ajv, Type, Static } from 'egg/ajv'`\n- **方式**：`@Inject() ajv: Ajv`，调用 `ajv.validate(schema, data)`\n- **特点**：TypeBox 定义一次 Schema，同时获得校验和 TypeScript 类型\n- **详细文档**：`references/ajv-validate.md`\n\n### Middleware 中间件\n\n- **导入**：`import { Middleware } from 'egg'`（AOP 类从 `import { Advice } from 'egg/aop'`）\n- **写法**：函数式（Koa 中间件函数）和 AOP（@Advice 类），通过 `@Middleware()` 应用\n- **特点**：支持类级别和方法级别，函数式和 AOP 不能在同一个 `@Middleware()` 中混用\n- **详细文档**：`references/middleware.md`\n\n---\n\n## 常见问题排查\n\n| 现象                   | 原因                                     | 解决方案                                                           |\n| ---------------------- | ---------------------------------------- | ------------------------------------------------------------------ |\n| MCP 装饰器 import 报错 | 从 `'egg'` 导入                          | MCP 装饰器从 `'@eggjs/tegg'` 导入，zod 从 `'@eggjs/tegg/zod'` 导入 |\n| MCP Schema 报错        | 用了 `z.object()` 包装                   | 直接用普通对象 `{ name: z.string() }`                              |\n| 定时任务不生效         | 放在 `app/schedule` 目录                 | 放在模块目录中，避免和 egg 默认扫描冲突                            |\n| Ajv 校验 import 报错   | 从 `typebox` 或 `ajv` 导入               | 统一从 `'egg/ajv'` 导入 Type、Static、Ajv 等                       |\n| type 推导不完整        | 用 `type` 定义                           | 用 `interface Foo extends Static<typeof Schema> {}` 代替           |\n| Middleware 混用报错    | 函数和 Advice 类放同一个 `@Middleware()` | 分开写多个 `@Middleware()`，每个内部类型一致                       |\n| AOP Middleware 不生效  | Advice 类没加 `@Advice()` 装饰器         | 必须同时有 `@Advice()` 装饰器才能被识别为 AOP                      |\n\n---\n\n## 最佳实践\n\n- **控制器精简**：业务逻辑委托给 Service 层\n- **参数验证**：使用装饰器和类型定义\n- **错误处理**：根据协议转换错误和响应码\n- **RESTful 设计**：遵循 HTTP 方法和资源命名\n- **响应一致性**：统一响应格式\n\n---\n\n## 参考资料\n\n详细的控制器开发文档：\n\n- `references/http-controller.md` - HTTP 接口完整指南\n- `references/mcp-controller.md` - MCP 接口开发\n- `references/schedule.md` - 定时任务\n- `references/ajv-validate.md` - Ajv 参数校验\n- `references/middleware.md` - Middleware 中间件（函数式 + AOP）\n\n核心概念（`egg-core` skill）：模块、依赖注入、对象生命周期\n\n单元测试（`egg-unittest` skill）：HTTP 接口测试、Service 测试、Mock\n\n<!-- File: packages/skills/egg-core/references/aop.md (Tokens: ~1843 | Category: anthropic-skill) -->\n# AOP 切面编程指南\n\n## 常见错误\n\n| 错误写法                                   | 正确写法                              | 说明                                                                                   |\n| ------------------------------------------ | ------------------------------------- | -------------------------------------------------------------------------------------- |\n| `import { Advice } from 'egg'`             | `import { Advice } from 'egg/aop'`    | AOP 装饰器从 `egg/aop` 导入，不是 `egg`                                                |\n| `import { Advice } from '@eggjs/tegg/aop'` | `import { Advice } from 'egg/aop'`    | 统一从 `egg/aop` 导入                                                                  |\n| Advice 类没有加 `@Advice()` 装饰器         | 必须同时有 `@Advice()`                | `@Pointcut` 和 `@Crosscut` 都要求目标是 Advice 类                                      |\n| `@Crosscut` 直接切 Egg 内置对象            | 只能切 tegg Proto 对象                | Egg 中的对象（如 app、ctx）无法被 Crosscut                                             |\n| Advice 中用实例属性存状态                  | 使用 `ctx.set()`/`ctx.get()` 共享状态 | Advice 默认是 Singleton，实例属性会被并发请求共享，必须用 AdviceContext 传递调用级状态 |\n\n---\n\n## 核心概念\n\n### Advice（切面逻辑）\n\nAdvice 是 AOP 的核心，定义了在目标方法执行前后要做什么。Advice 本身也是一种 Proto，默认 initType 为 Singleton（全局单例），可以使用 `@Inject` 注入依赖。如需每请求一个实例，显式指定 `@Advice({ initType: ObjectInitType.CONTEXT })`。\n\n```typescript\nimport { Advice, IAdvice, AdviceContext } from 'egg/aop';\nimport { Inject, Logger } from 'egg';\n\n@Advice()\nexport class LogAdvice implements IAdvice {\n  @Inject()\n  private logger: Logger;\n\n  async beforeCall(ctx: AdviceContext): Promise<void> {\n    ctx.set('startTime', Date.now());\n  }\n\n  async afterReturn(ctx: AdviceContext, result: any): Promise<void> {\n    // 方法成功返回后执行\n  }\n\n  async afterThrow(ctx: AdviceContext, error: Error): Promise<void> {\n    // 方法抛出异常后执行\n  }\n\n  async afterFinally(ctx: AdviceContext): Promise<void> {\n    const duration = Date.now() - ctx.get('startTime');\n    this.logger.info('%s cost %dms', String(ctx.method), duration);\n  }\n\n  async around(ctx: AdviceContext, next: () => Promise<any>): Promise<any> {\n    // 类似 koa 中间件，可以包裹方法执行\n    return await next();\n  }\n}\n```\n\n**执行顺序：**\n\n```typescript\nawait beforeCall(ctx);\ntry {\n  const result = await around(ctx, next); // next 执行目标方法\n  await afterReturn(ctx, result);\n  return result;\n} catch (e) {\n  await afterThrow(ctx, e);\n  throw e;\n} finally {\n  await afterFinally(ctx);\n}\n```\n\n**关键点：**\n\n- 所有 hook 方法都是可选的，按需实现\n- 只有 `around` 可以修改返回值\n- `beforeCall` 中可以通过修改 `ctx.args` 改变方法入参\n- 多个 Advice 之间通过 `ctx.set(key, value)` / `ctx.get(key)` 共享状态\n\n### AdviceContext\n\n```typescript\ninterface AdviceContext<T = object, K = any> {\n  that: T; // 被切的对象实例\n  method: PropertyKey; // 被切的方法名\n  args: any[]; // 方法参数（可修改）\n  adviceParams?: K; // 装饰器透传的参数\n  get(key: PropertyKey): any; // 获取共享状态\n  set(key: PropertyKey, value: any): this; // 设置共享状态\n}\n```\n\n---\n\n## Pointcut vs Crosscut\n\n### Pointcut — 精确切入\n\n在特定类的特定方法上声明 Advice，适合精确控制：\n\n```typescript\nimport { SingletonProto } from 'egg';\nimport { Pointcut } from 'egg/aop';\n\n@SingletonProto()\nexport class OrderService {\n  @Pointcut(LogAdvice)\n  async createOrder(data: any) {\n    // 业务逻辑\n  }\n\n  // 可以传参给 Advice\n  @Pointcut(TransactionAdvice, { adviceParams: { propagation: 'REQUIRED' } })\n  async updateOrder(id: string, data: any) {\n    // 业务逻辑\n  }\n}\n```\n\n### Crosscut — 批量切入\n\n在 Advice 类上声明切入规则，适合横切多个类/方法：\n\n```typescript\nimport { Crosscut, Advice, IAdvice, PointcutType } from 'egg/aop';\n\n// 模式 1：指定类和方法\n@Crosscut({\n  type: PointcutType.CLASS,\n  clazz: OrderService,\n  methodName: 'createOrder',\n})\n@Advice()\nexport class AuditAdvice implements IAdvice {\n  async afterReturn(ctx: AdviceContext): Promise<void> {\n    // 审计日志\n  }\n}\n\n// 模式 2：正则匹配\n@Crosscut({\n  type: PointcutType.NAME,\n  className: /.*Service$/i,\n  methodName: /^(create|update|delete)/,\n})\n@Advice()\nexport class OperationLogAdvice implements IAdvice {\n  async around(ctx: AdviceContext, next: () => Promise<any>): Promise<any> {\n    // 记录所有 Service 的写操作\n    return await next();\n  }\n}\n\n// 模式 3：自定义回调\n@Crosscut({\n  type: PointcutType.CUSTOM,\n  callback: (clazz, method) => {\n    return clazz.name.endsWith('Repository') && method !== 'constructor';\n  },\n})\n@Advice()\nexport class DbMetricsAdvice implements IAdvice {}\n```\n\n### 执行顺序\n\n- `@Crosscut` 默认 order: `100`\n- `@Pointcut` 默认 order: `1000`\n- order 越小越先执行\n- 同一方法上多个 Advice 按 order 升序排列\n\n通过 `order` 参数自定义顺序：\n\n```typescript\n// Pointcut：第二个参数中设置 order\n@Pointcut(LogAdvice, { order: 50 })\nasync createOrder(data: any) {}\n\n// Crosscut：第二个参数中设置 order\n@Crosscut(\n  { type: PointcutType.NAME, className: /.*Service$/i, methodName: /.+/ },\n  { order: 200 },\n)\n@Advice()\nexport class MetricsAdvice implements IAdvice {}\n```\n\n---\n\n## 参数透传\n\n同一个 Advice 在不同方法上可能需要不同的行为，通过 `adviceParams` 传参：\n\n```typescript\n@Advice()\nexport class CacheAdvice implements IAdvice {\n  async around(ctx: AdviceContext<any, { ttl: number }>, next: () => Promise<any>): Promise<any> {\n    const ttl = ctx.adviceParams?.ttl ?? 60;\n    // 根据 ttl 实现缓存逻辑\n    return await next();\n  }\n}\n\n@SingletonProto()\nexport class UserService {\n  @Pointcut(CacheAdvice, { adviceParams: { ttl: 3600 } })\n  async getUser(id: string) {\n    /* ... */\n  }\n\n  @Pointcut(CacheAdvice, { adviceParams: { ttl: 60 } })\n  async getUserList() {\n    /* ... */\n  }\n}\n```\n\n---\n\n## 典型场景\n\n| 场景                        | 推荐方式                 | 说明              |\n| --------------------------- | ------------------------ | ----------------- |\n| 特定方法加日志/缓存         | `@Pointcut(Advice)`      | 精确控制          |\n| 所有 Service 的写操作加审计 | `@Crosscut(NAME)` + 正则 | 批量匹配          |\n| 事务包裹                    | `@Pointcut(TxAdvice)`    | around 中管理事务 |\n\n<!-- File: packages/skills/egg-core/references/background-task.md (Tokens: ~1174 | Category: anthropic-skill) -->\n# BackgroundTask 异步任务指南\n\n## 常见错误\n\n| 错误写法                                                    | 正确写法                                        | 说明                                                      |\n| ----------------------------------------------------------- | ----------------------------------------------- | --------------------------------------------------------- |\n| `setTimeout(() => { ... }, 0)`                              | `backgroundTaskHelper.run(async () => { ... })` | setTimeout 执行时上下文已释放，会导致访问已销毁对象的错误 |\n| `setImmediate(() => { ... })`                               | `backgroundTaskHelper.run(async () => { ... })` | 同上，框架不会等待 setImmediate 的回调                    |\n| `process.nextTick(() => { ... })`                           | `backgroundTaskHelper.run(async () => { ... })` | 同上                                                      |\n| 在 Service 中 `await` 后台任务                              | 直接调用 `run()` 不 await                       | `run()` 是\"发射后不管\"，不返回 Promise                    |\n| `import { BackgroundTaskHelper } from '@eggjs/tegg/helper'` | `import { BackgroundTaskHelper } from 'egg'`    | 统一从 `egg` 导入，不要从 `@eggjs/tegg` 子路径导入        |\n\n---\n\n## 使用方式\n\n### 基本用法\n\n通过 `@Inject()` 注入 `BackgroundTaskHelper`，调用 `run()` 方法。`run()` 接受一个异步函数，任务会立即开始执行但不阻塞当前流程。框架在请求结束（Context preDestroy）时会等待所有后台任务完成（最多等待 timeout），然后再释放上下文。\n\n```typescript\nimport { BackgroundTaskHelper, Inject, SingletonProto, AccessLevel } from 'egg';\n\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class MetricsService {\n  @Inject()\n  private backgroundTaskHelper: BackgroundTaskHelper;\n\n  async reportAfterResponse(data: Record<string, unknown>) {\n    // run() 是非阻塞的，任务立即开始执行但不会被 await\n    this.backgroundTaskHelper.run(async () => {\n      // 框架会保持上下文存活直到任务完成或超时\n      await this.sendMetrics(data);\n    });\n  }\n\n  private async sendMetrics(data: Record<string, unknown>) {\n    // 实际上报逻辑\n  }\n}\n```\n\n---\n\n## 超时配置\n\n框架默认等待 5 秒，超时后放弃等待并释放上下文。\n\n### 按请求调整超时\n\n```typescript\nthis.backgroundTaskHelper.timeout = 10000; // 10 秒\nthis.backgroundTaskHelper.run(async () => {\n  await heavyWork();\n});\n```\n\n注意：timeout 是 context 级别的设置，同一请求中多次设置以最后一次为准。\n\n### 全局配置\n\n```typescript\n// config/config.default.ts\nexport default {\n  backgroundTask: {\n    timeout: 10000, // 全局默认 10 秒\n  },\n};\n```\n\n### 无限等待\n\n```typescript\n// 不推荐在生产环境使用，除非确定任务一定会完成\nthis.backgroundTaskHelper.timeout = Infinity;\n```\n\n---\n\n## 错误处理\n\nBackgroundTaskHelper 内部会捕获所有错误并记录日志，**不会**抛出异常或影响其他后台任务。\n\n```typescript\nthis.backgroundTaskHelper.run(async () => {\n  // 即使这里抛出异常，也不会影响其他后台任务或框架\n  throw new Error('something went wrong');\n  // 错误会被记录为: [BackgroundTaskHelper] background throw error:something went wrong\n});\n\n// 如果需要自定义错误处理\nthis.backgroundTaskHelper.run(async () => {\n  try {\n    await unreliableOperation();\n  } catch (e) {\n    // 自定义错误处理：重试、告警等\n    await this.alertService.notify(e);\n  }\n});\n```\n\n---\n\n## 原理\n\n请求处理完成后，框架会立即释放上下文（防止内存泄漏）。如果用 `setTimeout` 等方式在上下文释放后访问注入的服务，会因为上下文已销毁而出错。\n\nBackgroundTaskHelper 的作用是：\n\n1. `run()` 被调用时，任务立即开始执行（不是延后到响应发送后）\n2. 任务不会被 await，因此不阻塞当前请求的返回\n3. 请求结束时（Context preDestroy），框架等待所有后台任务完成（最多等待 timeout）\n4. 等待结束后才释放上下文\n\n这就是为什么必须用 `backgroundTaskHelper.run()` 而不是 `setTimeout` — 后者绕过了框架的上下文生命周期管理，执行时上下文可能已被释放。\n\n---\n\n## 单元测试\n\nBackgroundTaskHelper 的测试方法参考 `egg-unittest` skill 的 `references/background-task-test.md`。\n\n<!-- File: packages/skills/egg-core/references/dynamic-inject.md (Tokens: ~642 | Category: anthropic-skill) -->\n# 动态注入开发指南\n\n## 何时使用\n\n- **用动态注入**：同一抽象有多种实现，运行时按参数选择（如多种支付方式、多种存储后端）\n- **用普通 `@Inject()`**：依赖只有一个实现，编译时就能确定\n\n## Step 1: 定义抽象类和类型枚举\n\n```typescript\n// AbstractHello.ts\nexport abstract class AbstractHello {\n  abstract hello(): string;\n}\n\n// HelloType.ts\nexport enum HelloType {\n  FOO = 'FOO',\n  BAR = 'BAR',\n}\n```\n\n如果类型是无限扩展的，没有固定枚举，可以用 `Record<string, string>` 代替：\n\n```typescript\ntype AnyEnum = Record<string, string>;\n```\n\n## Step 2: 创建自定义装饰器\n\n```typescript\n// decorator/Hello.ts\nimport { ImplDecorator, QualifierImplDecoratorUtil } from 'egg';\nimport { HelloType } from '../HelloType.ts';\nimport { AbstractHello } from '../AbstractHello.ts';\n\nexport const HELLO_ATTRIBUTE = Symbol('HELLO_ATTRIBUTE');\n\nexport const Hello: ImplDecorator<AbstractHello, typeof HelloType> = QualifierImplDecoratorUtil.generatorDecorator(\n  AbstractHello,\n  HELLO_ATTRIBUTE,\n);\n```\n\n**注意事项：**\n\n- **ATTRIBUTE（Symbol）不要重复**，重复会导致实现被覆盖\n- **抽象类不要指定错**，否则可能导致实现被覆盖\n\n## Step 3: 实现抽象类\n\n每个实现加上 `@SingletonProto()`（或 `@ContextProto()`）和自定义装饰器：\n\n```typescript\n// impl/FooHello.ts\nimport { SingletonProto } from 'egg';\nimport { Hello } from '../decorator/Hello.ts';\nimport { HelloType } from '../HelloType.ts';\nimport { AbstractHello } from '../AbstractHello.ts';\n\n@SingletonProto()\n@Hello(HelloType.FOO)\nexport class FooHello extends AbstractHello {\n  hello(): string {\n    return 'hello, foo';\n  }\n}\n```\n\n## Step 4: 动态获取实现\n\n通过 `EggObjectFactory` 在运行时按类型获取对应实现：\n\n```typescript\n// HelloService.ts\nimport { EggObjectFactory, SingletonProto, Inject } from 'egg';\nimport { HelloType } from './HelloType.ts';\nimport { AbstractHello } from './AbstractHello.ts';\n\n@SingletonProto()\nexport class HelloService {\n  @Inject()\n  private eggObjectFactory: EggObjectFactory;\n\n  async hello(type: HelloType): Promise<string> {\n    const helloImpl = await this.eggObjectFactory.getEggObject(AbstractHello, type);\n    return helloImpl.hello();\n  }\n}\n```\n\n<!-- File: packages/skills/egg-core/references/eventbus.md (Tokens: ~1305 | Category: anthropic-skill) -->\n# EventBus 事件总线指南\n\n## 常见错误\n\n| 错误写法                                         | 正确写法                                    | 说明                                             |\n| ------------------------------------------------ | ------------------------------------------- | ------------------------------------------------ |\n| `import { EventBus } from '@eggjs/tegg'`         | `import { EventBus } from 'egg'`            | 统一从 `egg` 导入                                |\n| `import { Event } from '@eggjs/tegg'`            | `import { Event } from 'egg'`               | 装饰器也从 `egg` 导入                            |\n| 在 handler 的 `handle` 方法中直接访问请求上下文  | 使用 `@EventContext()` 注入 `IEventContext` | handler 运行在独立的上下文中，不是触发者的上下文 |\n| `@Event('hello')` 但没有声明 Events 接口         | 先在 `declare module 'egg'` 中声明事件类型  | 不声明会导致类型检查报错                         |\n| handler 的 `handle` 方法签名与 Events 声明不一致 | 确保参数类型与 Events 中定义一致            | TypeScript 会检查签名是否匹配                    |\n\n---\n\n## 使用步骤\n\n### 1. 声明事件类型\n\n在模块中创建类型声明文件，定义事件名称和参数签名：\n\n```typescript\n// app/myModule/event.ts\nimport 'egg';\n\ndeclare module 'egg' {\n  interface Events {\n    orderCreated: (orderId: string, userId: string) => void;\n    paymentCompleted: (orderId: string, amount: number) => void;\n  }\n}\n```\n\n**注意：** 文件开头必须有 `import 'egg'`，否则 `declare module` 会覆盖而非合并模块声明。\n\n### 2. 触发事件\n\n通过 `@Inject()` 注入 `EventBus` 或 `ContextEventBus`，调用 `emit()` 触发事件：\n\n```typescript\nimport { SingletonProto, Inject, EventBus, AccessLevel } from 'egg';\n\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class OrderService {\n  @Inject()\n  private eventBus: EventBus;\n\n  async createOrder(userId: string, items: Item[]): Promise<string> {\n    const orderId = await this.saveOrder(userId, items);\n    // 触发事件，不阻塞当前流程\n    this.eventBus.emit('orderCreated', orderId, userId);\n    return orderId;\n  }\n}\n```\n\n`emit()` 的参数类型由 Events 接口约束，TypeScript 会自动提示和校验。\n\n### 3. 消费事件\n\n用 `@Event()` 装饰器标记 handler 类。handler 必须实现 `handle` 方法，参数签名与 Events 声明一致：\n\n```typescript\nimport { Event, Inject } from 'egg';\nimport type { EggLogger } from 'egg';\n\n@Event('orderCreated')\nexport class OrderNotificationHandler {\n  @Inject()\n  private logger: EggLogger;\n\n  async handle(orderId: string, userId: string): Promise<void> {\n    this.logger.info('[OrderNotification] order %s created by user %s', orderId, userId);\n    // 发送通知等业务逻辑\n  }\n}\n```\n\n**handler 的关键特性：**\n\n- handler 运行在独立的上下文中，不是触发者的上下文\n- 同一事件可以有多个 handler，它们并行执行\n- handler 文件放在模块目录中，框架自动扫描和注册\n\n### 4. 消费多个事件\n\n一个 handler 可以处理多个事件，通过 `@EventContext()` 获取事件上下文来区分：\n\n```typescript\nimport { Event, EventContext, type IEventContext } from 'egg';\n\n@Event('orderCreated')\n@Event('paymentCompleted')\nexport class AuditLogHandler {\n  async handle(@EventContext() ctx: IEventContext, ...args: unknown[]): Promise<void> {\n    console.log('event:', ctx.eventName, 'args:', args);\n  }\n}\n```\n\n`@EventContext()` 只能修饰 `handle` 方法的第一个参数。不需要区分事件时可以省略。\n\n---\n\n## Cork/Uncork 事件缓冲\n\n当需要在一个操作中触发多个事件，但希望它们在操作全部完成后才被处理时，使用 cork/uncork：\n\n```typescript\nimport { ContextProto, Inject, type ContextEventBus } from 'egg';\n\n@ContextProto()\nexport class BatchService {\n  @Inject()\n  private eventBus: ContextEventBus; // 注意：cork/uncork 需要 ContextEventBus\n\n  async processBatch(items: string[]): Promise<void> {\n    this.eventBus.cork(); // 开始缓冲，事件不会立即派发\n\n    for (const item of items) {\n      this.eventBus.emit('orderCreated', item, 'batch-user');\n    }\n\n    this.eventBus.uncork(); // 释放缓冲，所有事件一次性派发\n  }\n}\n```\n\n**Cork/Uncork 行为：**\n\n- 支持嵌套调用 — 内层 uncork 不会释放事件，只有最外层 uncork 才会\n- `ContextEventBus` 的 cork/uncork 自动管理 corkId，无需手动指定\n- cork 期间 emit 的事件会被暂存，uncork 后按顺序派发\n\n---\n\n## 单元测试\n\nEventBus 的测试方法参考 `egg-unittest` skill 的 `references/eventbus-test.md`。\n\n<!-- File: packages/skills/egg-core/references/inject.md (Tokens: ~1777 | Category: anthropic-skill) -->\n# Inject 开发指南\n\n## 基本用法\n\n使用 `@Inject()` 注入其他 Proto 对象或 Egg 内置对象：\n\n```typescript\nimport { SingletonProto, Inject } from 'egg';\n\n@SingletonProto()\nexport class OrderService {\n  @Inject()\n  userService: UserService; // 按类型自动匹配\n\n  @Inject({ name: 'customName' })\n  config: any; // 按名称匹配\n\n  @Inject('shorthand')\n  other: any; // 字符串简写，等同于 { name: 'shorthand' }\n}\n```\n\n## 名称解析规则\n\n框架按以下优先级确定注入目标：\n\n1. **显式指定 name** — `@Inject({ name: 'xxx' })` 或 `@Inject('xxx')`\n2. **类型元数据自动关联** — 当属性类型是使用了 `@SingletonProto()` 或 `@ContextProto()` 装饰器的 class 时，框架自动从类型元数据匹配对应的 Proto 对象。此时属性名可以任意取值，不影响注入结果\n3. **属性名兜底** — 当类型为 `interface`、`any`、`unknown` 等无法获取运行时元信息的类型时，使用属性名作为注入名称\n\n```typescript\n@SingletonProto()\nexport class MyService {\n  @Inject()\n  fooService: FooService; // 优先级 2：从 FooService 类型自动关联\n\n  @Inject()\n  whatever: FooService; // 优先级 2：同样生效，属性名不影响匹配\n\n  @Inject({ name: 'bar' })\n  baz: FooService; // 优先级 1：显式 name → \"bar\"\n\n  @Inject()\n  something: any; // 优先级 3：类型无元数据，回退到属性名 → \"something\"\n}\n```\n\n## 可选注入\n\n默认情况下，注入目标不存在会在启动时报错。使用 `optional` 可以跳过不存在的依赖：\n\n```typescript\nimport { SingletonProto, Inject, InjectOptional } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  @Inject({ optional: true })\n  maybeService?: SomeService; // 不存在时为 undefined\n\n  @InjectOptional()\n  anotherOptional?: OtherService; // 简写方式，效果相同\n}\n```\n\n## 构造函数注入\n\n除了属性注入，也支持构造函数参数注入：\n\n```typescript\nimport { SingletonProto, Inject } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  constructor(\n    @Inject() readonly fooService: FooService,\n    @Inject({ optional: true }) readonly barService?: BarService,\n  ) {}\n}\n```\n\n**注意：构造函数注入和属性注入不能混用。** 一个 class 只能选择其中一种方式，否则框架会报错。\n\n## 注入 Egg 内置对象\n\n框架会自动遍历 `Application` 和 `Context` 对象的所有属性，均可通过 `@Inject()` 注入。\n\n#### 注入配置\n\n```typescript\nimport { Inject, SingletonProto, EggAppConfig } from 'egg';\n\n@SingletonProto()\nclass Foo {\n  @Inject()\n  config: EggAppConfig;\n\n  bar(): void {\n    console.log('current env is %s', this.config.env);\n  }\n}\n```\n\n#### 注入 Logger\n\n专为 logger 做了优化，可以直接注入 custom logger：\n\n```typescript\nimport { Inject, SingletonProto, Logger } from 'egg';\n\n@SingletonProto()\nclass FooService {\n  @Inject()\n  logger: Logger; // 注入 ${appname}-web.log\n\n  @Inject()\n  coreLogger: Logger; // 注入 egg-web.log\n\n  @Inject()\n  fooLogger: Logger; // 注入 customLogger 中配置的 fooLogger\n}\n```\n\n#### 注入 Service\n\n> 强烈建议把 Egg Service 的代码通过 Proto 重新封装再注入。对于已有的 Service，可以通过以下方式引入：\n\n```typescript\nimport { Service, Inject, SingletonProto } from 'egg';\n\n@SingletonProto()\nclass FooService {\n  @Inject()\n  service: Service; // 注入整个 ctx.service\n\n  get xxxService() {\n    return this.service.xxxService;\n  }\n}\n```\n\n#### 注入 HttpClient\n\n```typescript\nimport { Inject, SingletonProto, HttpClient } from 'egg';\n\n@SingletonProto()\nclass Foo {\n  @Inject()\n  httpClient: HttpClient;\n\n  async bar(): Promise<void> {\n    await this.httpClient.request('https://example.com');\n  }\n}\n```\n\n## 注入模块配置\n\n在模块根目录创建 `module.yml`，通过 `moduleConfig` 名称注入，框架自动注入当前模块的配置：\n\n```yaml\n# module.yml\napiEndpoint: https://api.example.com\nretryCount: 3\n```\n\n```typescript\nimport { SingletonProto, Inject } from 'egg';\n\ninterface ModuleConfig {\n  apiEndpoint: string;\n  retryCount: number;\n}\n\n@SingletonProto()\nexport class ApiService {\n  @Inject()\n  moduleConfig: ModuleConfig;\n\n  async call(): Promise<void> {\n    // this.moduleConfig.apiEndpoint → \"https://api.example.com\"\n  }\n}\n```\n\n如需注入其他模块的配置，使用 `@ConfigSourceQualifier` 指定模块名：\n\n```typescript\nimport { SingletonProto, Inject, ConfigSourceQualifier } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  @Inject()\n  @ConfigSourceQualifier('otherModule')\n  moduleConfig: OtherModuleConfig; // 注入 otherModule 的配置\n}\n```\n\n## Qualifier 限定符\n\n当同名对象存在多个实现时，使用限定符消歧义：\n\n### @InitTypeQualifier\n\n指定注入 Singleton 还是 Context 实例：\n\n```typescript\nimport { SingletonProto, Inject, InitTypeQualifier, ObjectInitType } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  @Inject()\n  @InitTypeQualifier(ObjectInitType.CONTEXT)\n  barService: BarService; // 强制注入 ContextProto 版本\n}\n```\n\n> 大多数情况下不需要手动指定，框架会根据类型元数据自动推导。\n\n### @EggQualifier\n\n当 `app` 和 `ctx` 上存在同名属性时，框架默认优先匹配 `ctx`。使用 `@EggQualifier` 显式指定来源：\n\n```typescript\nimport { SingletonProto, Inject, EggQualifier, EggType } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  @Inject()\n  @EggQualifier(EggType.APP)\n  someProp: any; // 强制从 app 注入\n\n  @Inject()\n  @EggQualifier(EggType.CONTEXT)\n  someProp2: any; // 强制从 ctx 注入\n}\n```\n\n### @ModuleQualifier\n\n指定从哪个模块注入：\n\n```typescript\nimport { SingletonProto, Inject, ModuleQualifier } from 'egg';\n\n@SingletonProto()\nexport class MyService {\n  @Inject()\n  @ModuleQualifier('userModule')\n  userService: UserService; // 明确从 userModule 注入\n}\n```\n\n## 重要约束\n\n- **不能有循环依赖**：Proto 之间或模块之间都不能形成循环引用\n- **不能有同名对象**：同一模块内不能存在相同名称和相同初始化类型的 Proto\n- **按需注入**：不要直接注入 `app` 或 `ctx`，按需注入具体的属性（如 `logger`、`config`）\n\n<!-- File: packages/skills/egg-core/references/module.md (Tokens: ~1396 | Category: anthropic-skill) -->\n# module 开发指南\n\n## 创建 module\n\n在 `app` 目录中，创建 module 目录，并在该目录中添加 `package.json`。\n\n```json\n{\n  \"name\": \"moduleName\",\n  \"eggModule\": {\n    \"name\": \"moduleName\"\n  }\n}\n```\n\n**重要提示**：\n\n- 模块名称不能包含 `-` 或其他特殊字符；使用驼峰命名规则。\n- module 的 `package.json` 文件中，仅包含 `name` 以及 `eggModule.name` 字段，不应该有其他额外内容。\n\n## module 发现机制\n\n框架支持两种模式：自动扫描（默认）和手动声明。两者互斥，当 `config/module.json` 存在时，自动扫描完全禁用。\n\n### 模式一：自动扫描（默认）\n\n当 `config/module.json` **不存在**时，框架通过以下两种方式发现模块：\n\n**1. 目录扫描**\n\n从项目根目录开始扫描，查找含有 `eggModule.name` 的 `package.json`。\n\n- 默认扫描深度：10 层\n- 自动排除：隐藏目录（`.` 开头）、`node_modules/`、`coverage/`\n\n可通过应用配置调整扫描深度和排除路径：\n\n```typescript\n// config/config.default.ts\nexport default {\n  tegg: {\n    readModuleOptions: {\n      deep: 5, // 默认 10\n      extraFilePattern: ['!**/dist'], // 额外排除 dist 目录\n    },\n  },\n};\n```\n\n`extraFilePattern` 使用 [globby](https://github.com/sindresorhus/globby) 语法，以 `!` 开头表示排除。\n\n```\napp/\n├── fooModule/          ✅ 被扫描到\n│   └── package.json    { \"eggModule\": { \"name\": \"fooModule\" } }\n├── barModule/          ✅ 被扫描到\n│   └── package.json    { \"eggModule\": { \"name\": \"barModule\" } }\n├── .hidden/            ❌ 隐藏目录，自动排除\n│   └── package.json\n└── common/             ❌ 无 package.json，不会加载\n    └── utils.ts\n```\n\n**2. npm 包扫描**\n\n框架会遍历项目 `package.json` 中 `dependencies` 的每个包（不含 `devDependencies`），检查其 `package.json` 是否含有 `eggModule.name`，如果有则自动作为模块加载。\n\n### 模式二：手动声明\n\n创建 `config/module.json` 后，自动扫描**完全禁用**，仅加载文件中声明的模块：\n\n```json\n[\n  { \"path\": \"../app/module-a\" }, // 相对于 config 目录的路径\n  { \"package\": \"@eggjs/common-module\" } // npm 包名\n]\n```\n\n## module 配置\n\n在模块根目录创建 `module.yml` 存放模块专属配置：\n\n```\napp/\n└── userModule/\n    ├── package.json\n    ├── module.yml          # 基础配置\n    ├── module.unittest.yml # 环境特定配置（可选）\n    └── UserService.ts\n```\n\n### 配置文件格式\n\n支持 YAML 和 JSON 两种格式，优先加载 YAML：\n\n```yaml\n# module.yml\nfeatures:\n  dynamic:\n    foo: bar\n```\n\n### 环境配置合并\n\n框架会按 `module.yml` → `module.{env}.yml` 的顺序深度合并：\n\n```yaml\n# module.yml\nfeatures:\n  dynamic:\n    foo: bar\n\n# module.unittest.yml\nfeatures:\n  dynamic:\n    testMode: true\n```\n\nunittest 环境下合并结果：\n\n```json\n{\n  \"features\": {\n    \"dynamic\": {\n      \"foo\": \"bar\",\n      \"testMode\": true\n    }\n  }\n}\n```\n\n### 注入配置\n\n通过 `@Inject()` 注入 `moduleConfig`，框架自动注入当前模块的配置：\n\n```typescript\nimport { SingletonProto, Inject } from 'egg';\n\ninterface ModuleConfig {\n  features: {\n    dynamic: {\n      foo: string;\n    };\n  };\n}\n\n@SingletonProto()\nexport class UserService {\n  @Inject()\n  moduleConfig: ModuleConfig;\n\n  async getFeatureFlag(): Promise<string> {\n    return this.moduleConfig.features.dynamic.foo; // 'bar'\n  }\n}\n```\n\n## module 目录组织\n\n### 新应用\n\n直接在 `app/` 目录下平铺 module。可按功能划分，也可按业务划分：\n\n**按功能划分：**\n\n```\napp/\n├── authModule/\n│   └── package.json\n├── logModule/\n│   └── package.json\n└── storageModule/\n    └── package.json\n```\n\n**按业务划分：**\n\n```\napp/\n├── userModule/\n│   └── package.json\n├── orderModule/\n│   └── package.json\n└── paymentModule/\n    └── package.json\n```\n\n### 存量应用（包含老的 egg 写法）\n\n保留原有 `app/controller`、`app/service` 等目录不动，将新增的 module 统一放在 `app/module/` 下：\n\n```\napp/\n├── controller/           # 老的 egg 代码，保持不变\n├── service/\n├── module/               # 新增 module 统一放这里\n│   ├── userModule/\n│   │   └── package.json\n│   └── orderModule/\n│       └── package.json\n└── router.ts\n```\n\n<!-- File: packages/skills/egg-core/references/proto.md (Tokens: ~1386 | Category: anthropic-skill) -->\n# Proto 开发指南\n\n## SingletonProto vs ContextProto\n\n|          | SingletonProto     | ContextProto       |\n| -------- | ------------------ | ------------------ |\n| 创建时机 | 应用启动时立即创建 | 请求到达时按需创建 |\n| 实例数量 | 整个应用 1 个      | 每个请求 1 个      |\n| 销毁时机 | 应用关闭           | 请求结束           |\n\n**决策逻辑：默认使用 `@SingletonProto()`**，性能更好。只有当需要在多个服务间共享请求级隔离状态时，才使用 `@ContextProto()`。\n\n```typescript\nimport { SingletonProto, ContextProto } from 'egg';\n\n// ✅ 默认选择：无状态服务用 SingletonProto\n@SingletonProto()\nexport class UserService {\n  async findUser(id: string): Promise<User> {\n    // 无需请求级状态，适合 Singleton\n  }\n}\n\n// 仅在需要请求级隔离时使用 ContextProto\n@ContextProto()\nexport class RequestContext {\n  traceId: string;\n  userId: string;\n  // 每个请求独立的状态\n}\n```\n\n## 装饰器参数\n\n`@SingletonProto()` 和 `@ContextProto()` 接受相同的可选参数：\n\n| 参数          | 类型        | 默认值         | 说明                                 |\n| ------------- | ----------- | -------------- | ------------------------------------ |\n| name          | string      | 类名首字母小写 | 实例名称                             |\n| accessLevel   | AccessLevel | PRIVATE        | 跨模块可见性                         |\n| protoImplType | string      | 'DEFAULT'      | 实现类型标记（高级用法，一般不需要） |\n\n名称推导规则：类名首字母自动转小写，如 `MyService` → `myService`。\n\n```typescript\nimport { SingletonProto, AccessLevel } from 'egg';\n\n// 使用默认参数：name 为 \"helloService\"，accessLevel 为 PRIVATE\n@SingletonProto()\nexport class HelloService {}\n\n// 自定义参数\n@SingletonProto({\n  name: 'customName',\n  accessLevel: AccessLevel.PUBLIC,\n})\nexport class HelloService {}\n```\n\n## 注入规则\n\n**SingletonProto 可以注入 ContextProto**，框架会自动处理生命周期差异。\n\n实现机制：框架不会直接注入 ContextProto 实例，而是通过 lazy getter/Proxy，在每次访问时从当前请求上下文动态解析，确保每个请求拿到各自的实例。\n\n```typescript\nimport { SingletonProto, ContextProto, Inject } from 'egg';\n\n@ContextProto()\nexport class RequestContext {\n  userId: string;\n}\n\n@SingletonProto()\nexport class AppService {\n  @Inject()\n  requestContext: RequestContext; // ✅ 每次访问自动解析当前请求的实例\n}\n```\n\nContextProto 也可以注入 SingletonProto：\n\n```typescript\n@ContextProto()\nexport class RequestHandler {\n  @Inject()\n  appService: AppService; // ✅ 直接注入\n}\n```\n\n## AccessLevel 跨模块访问\n\n- `AccessLevel.PRIVATE`（默认）：仅同模块内可注入\n- `AccessLevel.PUBLIC`：跨模块可注入\n\n模块边界由 `package.json` 中的 `eggModule.name` 决定。\n\n```typescript\n// userModule 中\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class UserService {}\n\n// orderModule 中\n@SingletonProto()\nexport class OrderService {\n  @Inject()\n  userService: UserService; // ✅ UserService 是 PUBLIC，跨模块可注入\n}\n```\n\n```typescript\n// userModule 中\n@SingletonProto() // 默认 PRIVATE\nexport class UserHelper {}\n\n// orderModule 中\n@SingletonProto()\nexport class OrderService {\n  @Inject()\n  userHelper: UserHelper; // ❌ 报错：UserHelper 是 PRIVATE，不能跨模块注入\n}\n```\n\n## 生命周期\n\nProto 对象支持以下生命周期钩子，按执行顺序排列：\n\n| 阶段 | 装饰器                      | 说明                             |\n| ---- | --------------------------- | -------------------------------- |\n| 1    | constructor                 | 构造函数                         |\n| 2    | `@LifecyclePostConstruct()` | 构造完成后                       |\n| 3    | `@LifecyclePreInject()`     | 注入前                           |\n| 4    | —                           | 依赖注入                         |\n| 5    | `@LifecyclePostInject()`    | 注入完成后                       |\n| 6    | `@LifecycleInit()`          | 异步初始化（对象就绪前最后一步） |\n| 7    | —                           | **对象就绪，可被使用**           |\n| 8    | `@LifecyclePreDestroy()`    | 销毁前                           |\n| 9    | `@LifecycleDestroy()`       | 销毁，用于清理资源               |\n\n常用的是 `@LifecycleInit()` 和 `@LifecycleDestroy()`。\n\n**示例 1：SingletonProto 异步初始化和资源清理**\n\n```typescript\nimport { SingletonProto, LifecycleInit, LifecycleDestroy } from 'egg';\n\n@SingletonProto()\nexport class DatabaseService {\n  private connection: Connection;\n\n  @LifecycleInit()\n  async init(): Promise<void> {\n    this.connection = await createConnection();\n  }\n\n  @LifecycleDestroy()\n  async destroy(): Promise<void> {\n    await this.connection.close();\n  }\n}\n```\n\n**示例 2：ContextProto 在注入完成后初始化状态**\n\n```typescript\nimport { ContextProto, Inject, LifecyclePostInject } from 'egg';\n\n@ContextProto()\nexport class RequestTracer {\n  @Inject()\n  traceService: TraceService;\n\n  traceId: string;\n\n  @LifecyclePostInject()\n  async postInject(): Promise<void> {\n    // 依赖注入完成后，利用注入的服务初始化状态\n    this.traceId = await this.traceService.generateTraceId();\n  }\n}\n```\n\n<!-- File: packages/skills/egg-core/SKILL.md (Tokens: ~2512 | Category: anthropic-skill) -->\n---\nname: egg-core\ndescription: 本技能用于处理 EGG 基础核心概念，包括模块架构、@SingletonProto、@ContextProto、@Inject 装饰器、动态注入、BackgroundTaskHelper 后台任务、EventBus 事件总线和 AOP 切面编程。用于理解 EGG 的基础构建块、依赖注入、对象生命周期管理、运行时多实现动态选择、请求返回后的异步任务处理、事件驱动架构和横切关注点。\nallowed-tools: Read\n---\n\n# egg 核心概念\n\n## 代码组织与依赖注入\n\n### Step 1: 代码写在 module 中\n\n#### 什么是模块？\n\n模块是 EGG 中基础的代码组织单元。只有模块内的代码会被框架扫描和加载。模块之间相互独立，但可以通过 `@Inject` 装饰器访问其他模块的对象。\n\n#### 定义 module\n\n在目录中添加包含 `eggModule.name` 字段的 `package.json` 文件来声明该目录为模块：\n\n```json\n{\n  \"name\": \"foo\",\n  \"eggModule\": {\n    \"name\": \"foo\"\n  }\n}\n```\n\n**重要提示**：模块名称不能包含 `-` 或其他特殊字符；使用驼峰命名规则。\n\n#### 正确示例\n\n```\napp/\n└── userModule/           ✅ 驼峰命名\n    ├── package.json\n    │   └── { \"eggModule\": { \"name\": \"userModule\" } }\n    └── service.ts        ✅ 会被框架加载\n```\n\n#### 错误示例\n\n```\napp/\n├── user-module/          ❌ 名称包含 `-`\n│   └── package.json\n│       └── { \"eggModule\": { \"name\": \"user-module\" } }\n│\n└── common/               ❌ 缺少 package.json（不是 module）\n    └── utils.ts          ❌ 不会被框架加载\n```\n\n#### 模块配置\n\n在模块根目录创建 `module.yml` 用于模块特定配置：\n\n```yaml\nfoo: bar\n```\n\n通过 `@Inject()` 注入配置，使用 `moduleConfig`：\n\n```typescript\nimport { SingletonProto, Inject } from 'egg';\n\ninterface ModuleConfig {\n  foo: string;\n}\n\n@SingletonProto()\nexport class ConfigService {\n  @Inject()\n  private readonly moduleConfig: ModuleConfig;\n\n  async hello(): Promise<string> {\n    return `hello ${this.moduleConfig.foo}`;\n  }\n}\n```\n\n#### 模块组织最佳实践\n\n- 新应用：按功能在 `app/` 目录中组织\n- 存量应用：保留老的 egg 代码在 `app/controller`/`app/service`，将新增的 module 代码放在 `app/module/`\n- 可以在 `dependencies` 中导入 npm 包作为额外模块\n\n### 导入路径\n\n所有装饰器和类型统一从 `egg` 导入，不要从 `@eggjs/tegg` 导入：\n\n```typescript\n// ✅ 正确\nimport { SingletonProto, ContextProto, Inject, AccessLevel } from 'egg';\n\n// ❌ 错误 — 不要从 @eggjs/tegg 导入\nimport { SingletonProto } from '@eggjs/tegg';\n```\n\n---\n\n### Step 2: 用 Proto 实现 Service\n\n#### SingletonProto\n\n应用启动时立即创建，整个应用生命周期内只有一个实例，性能更好，应该作为默认选择。\n\n```typescript\nimport { SingletonProto } from 'egg';\n\n@SingletonProto()\nexport class HelloService {\n  async hello(): Promise<string> {\n    return 'hello';\n  }\n}\n```\n\n#### ContextProto\n\n请求到达时按需创建，每个请求一个实例，请求结束自动销毁。仅在需要隔离不同请求的上下文信息时使用。\n\n```typescript\nimport { ContextProto } from 'egg';\n\n@ContextProto()\nexport class RequestContext {\n  userId: string;\n}\n```\n\n**重要提示**：大多数服务应该使用 `SingletonProto` 以获得更好的性能。只有当请求上下文必须在服务之间共享以确保请求之间隔离时，才使用 `ContextProto`。\n\n#### AccessLevel\n\nproto 对象默认 accessLevel 为 `AccessLevel.PRIVATE`，仅在当前 module 内使用。可以设置为 `AccessLevel.PUBLIC`，进行跨模块访问。\n\n```typescript\nimport { AccessLevel, ContextProto, SingletonProto } from 'egg';\n\n@SingletonProto({ accessLevel: AccessLevel.PUBLIC })\nexport class SharedService {}\n\n@ContextProto({ accessLevel: AccessLevel.PUBLIC })\nexport class SharedContextService {}\n```\n\n### Step 3: 通过 Inject 使用 Service\n\n#### 基本用法\n\n使用 `@Inject()` 注入其他 Proto 或 Egg 对象：\n\n```typescript\nimport { Inject, Logger, SingletonProto } from 'egg';\nimport { FooService } from './FooService.ts';\n\n@SingletonProto()\nexport class HelloService {\n  @Inject()\n  fooService: FooService; // 注入另一个 Proto\n\n  @Inject()\n  logger: Logger; // 注入 Egg 对象\n\n  async hello(): Promise<string> {\n    this.logger.info(`[HelloService] ${this.fooService.hello()}`);\n  }\n}\n```\n\n#### 动态注入\n\n当同一个抽象有多种实现，需要在运行时动态选择时，通过 `EggObjectFactory` 按类型获取实现，无需 if/else。详见 `references/dynamic-inject.md`。\n\n#### 重要约束\n\n- **不能有循环依赖**：Proto 或模块之间都不能有循环依赖\n- **不能有同名对象**：一个模块不能有相同名称和初始化类型的 Proto\n- **按需注入**：不要直接注入 `app` 或 `ctx`，按需注入特定对象\n\n### 快速决策指南\n\n| 场景                             | 使用方式                                               |\n| -------------------------------- | ------------------------------------------------------ |\n| 无状态服务                       | `@SingletonProto()`                                    |\n| 跨服务共享的请求级状态           | `@ContextProto()`                                      |\n| 需要跨模块访问                   | `@SingletonProto({ accessLevel: AccessLevel.PUBLIC })` |\n| 注入依赖                         | `@Inject()`                                            |\n| 使用自定义名称注入               | `@Inject({ name: 'customName' })`                      |\n| 同一抽象多种实现，运行时动态选择 | `QualifierImplDecoratorUtil` + `EggObjectFactory`      |\n\n## 异步任务\n\n| 特性       | BackgroundTaskHelper          | EventBus                                  |\n| ---------- | ----------------------------- | ----------------------------------------- |\n| **耦合**   | 高 — 异步逻辑写在触发者代码中 | 低 — handler 独立，新增处理者不修改触发者 |\n| **上下文** | 共享触发者的请求上下文        | handler 运行在独立的新上下文中            |\n| **扩展**   | 需要修改触发者代码            | 新增 `@Event` handler 类即可              |\n\n```\n需要在请求之外执行任务？\n│\n├─ 请求返回后执行，依赖当前请求上下文\n│  └─ → BackgroundTaskHelper（references/background-task.md）\n│\n├─ 请求返回后执行，不依赖当前请求上下文，需要解耦\n│  └─ → EventBus（references/eventbus.md）\n│\n└─ 定时或周期执行\n   └─ → Schedule（参考 egg-controller skill）\n```\n\n## AOP 切面编程\n\nAOP 用于将日志、鉴权、缓存、事务等横切关注点从业务代码中分离。AOP 装饰器从 `egg/aop` 导入（不是 `egg`）。\n\n```\n需要在方法执行前后添加通用逻辑？\n│\n├─ 针对特定方法 → @Pointcut（在目标方法上声明）\n│\n└─ 批量切入多个类/方法 → @Crosscut（在 Advice 类上声明匹配规则）\n```\n\n详细用法（Advice 生命周期、AdviceContext、Pointcut/Crosscut 选型、参数透传、执行顺序）请参阅 `references/aop.md`。\n\n## 常见问题排查\n\n| 现象                   | 原因                                       | 解决方案                                     |\n| ---------------------- | ------------------------------------------ | -------------------------------------------- |\n| 模块没被加载           | `eggModule.name` 包含 `-` 等特殊字符       | 改为驼峰命名                                 |\n| `EggPrototypeNotFound` | 跨模块注入但 accessLevel 为 PRIVATE        | 改为 `AccessLevel.PUBLIC`                    |\n| 注入对象不对           | 类型为 `interface`/`any`，回退到属性名匹配 | 改用 class 类型或 `@Inject({ name: 'xxx' })` |\n| 混用注入方式报错       | 属性注入和构造函数注入不能混用             | 统一使用一种方式                             |\n| 可选依赖启动报错       | 缺少 optional 标记                         | `@Inject({ optional: true })`                |\n\n## 参考资料\n\n- 详细的 module 文档，请参阅：`references/module.md`\n- Inject 装饰器使用，请参阅：`references/inject.md`\n- SingletonProto 和 ContextProto 详情，请参阅：`references/proto.md`\n- 动态注入（Qualifier 动态注入），请参阅：`references/dynamic-inject.md`\n- 请求后异步任务（BackgroundTaskHelper），请参阅：`references/background-task.md`\n- 事件总线（EventBus），请参阅：`references/eventbus.md`\n- AOP 切面编程（Advice、Pointcut、Crosscut），请参阅：`references/aop.md`\n\n单元测试（`egg-unittest` skill）：Service 测试、BackgroundTask 测试、EventBus 测试\n\n<!-- File: packages/skills/egg-unittest/references/background-task-test.md (Tokens: ~576 | Category: anthropic-skill) -->\n# BackgroundTaskHelper 测试\n\n## 常见错误\n\n| 错误写法                                           | 正确写法                                                                              | 说明                                                 |\n| -------------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------- |\n| 不等待就断言                                       | 用 `mockModuleContextScope`（自动等待）或 `app.backgroundTasksFinished()`（手动等待） | 后台任务异步执行，必须等待完成后再断言               |\n| 在 `mockModuleContextScope` 回调内断言后台任务结果 | 在 `mockModuleContextScope` 返回后断言                                                | 回调内任务尚未完成，返回后才会等待完成               |\n| 用 `TimerUtil.sleep` 等待                          | 用 `app.backgroundTasksFinished()`                                                    | sleep 时间不确定，`backgroundTasksFinished` 精确等待 |\n\n---\n\n## 使用 mockModuleContextScope\n\n`mockModuleContextScope` 退出时会自动等待所有后台任务完成（内部触发 `doPreDestroy`），scope 退出后直接断言即可：\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\nimport { CountService } from '../app/modules/count/CountService.ts';\n\nit('should complete background task', async () => {\n  await app.mockModuleContextScope(async (ctx) => {\n    const countService = await ctx.getEggObject(CountService);\n    // countService 内部通过 backgroundTaskHelper.run() 触发后台任务\n    await countService.doSomething();\n  });\n\n  // scope 退出后，后台任务已完成，直接断言\n  const countService = await app.getEggObject(CountService);\n  assert.equal(countService.count, 1);\n});\n```\n\n## 使用 backgroundTasksFinished\n\n不通过 `mockModuleContextScope` 触发的场景（如 HTTP 接口测试），scope 退出的自动等待机制不适用，需要手动调用 `app.backgroundTasksFinished()` 等待所有后台任务完成后，再做断言：\n\n```typescript\nimport assert from 'node:assert';\nimport { app } from '@eggjs/mock/bootstrap';\nimport { CountService } from '../app/modules/count/CountService.ts';\n\nit('should complete background task', async () => {\n  await app.httpRequest().get('/api/trigger-task').expect(200);\n\n  // 等待后台任务完成\n  await app.backgroundTasksFinished();\n\n  const countService = await app.getEggObject(CountService);\n  assert.equal(countService.count, 1);\n});\n```\n\n<!-- File: packages/skills/egg-unittest/references/eventbus-test.md (Tokens: ~449 | Category: anthropic-skill) -->\n# EventBus 测试\n\n## 常见错误\n\n| 错误写法                         | 正确写法                                           | 说明                                         |\n| -------------------------------- | -------------------------------------------------- | -------------------------------------------- |\n| 先 emit 再 `eventWaiter.await()` | 先 `eventWaiter.await()` 再触发业务逻辑            | await 注册监听器，必须在事件发出前           |\n| 不等待事件处理完成就断言         | 使用 `eventWaiter.await('eventName')` 等待后再断言 | handler 异步执行，不等待则断言时可能尚未完成 |\n\n---\n\n## 基本测试模式\n\n使用 `app.getEventWaiter()` 等待事件被处理完成：\n\n```typescript\nimport assert from 'node:assert';\nimport { app, mm } from '@eggjs/mock/bootstrap';\nimport { HelloService } from '../app/modules/hello/HelloService.ts';\nimport { HelloHandler } from '../app/modules/hello/HelloHandler.ts';\n\ndescribe('EventBus', () => {\n  it('should handle event', async () => {\n    // mock handler 捕获调用参数\n    const mockFn = async (msg: string) => {};\n    mm(HelloHandler.prototype, 'handle', mockFn);\n\n    await app.mockModuleContextScope(async (ctx) => {\n      const helloService = await ctx.getEggObject(HelloService);\n      const eventWaiter = await app.getEventWaiter();\n\n      // 1. 先注册等待（必须在 emit 之前）\n      const eventPromise = eventWaiter.await('helloEgg');\n\n      // 2. 触发业务逻辑（内部会 emit 事件）\n      helloService.hello();\n\n      // 3. 等待 handler 执行完成\n      await eventPromise;\n    });\n\n    // 4. 验证 handler 被调用及参数\n    assert.equal(mockFn.called, 1);\n    assert.deepStrictEqual(mockFn.lastCalledArguments, ['hello']);\n  });\n});\n```\n\n<!-- ... +63 more modular skills available via /api/skills/eggjs/egg?path={path} -->\n</yakaai_skills>"}