weifuwu 0.63.0 → 0.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/saas.md ADDED
@@ -0,0 +1,246 @@
1
+ # SaaS 地基模块(weifuwu)
2
+
3
+ 五个模块(限流 / 邮件 / 用户系统 / 消息系统 / 队列)+ AI 对话,以中间件形态随包提供,`app.use(...)` 一行接入。
4
+
5
+ > 本页为 weifuwu 官方文档拆分页 · [返回 README](../README.md)
6
+
7
+ 四个内建模块组成一个"基本 SaaS 底座":认证、异步任务、限流、邮件——零新增依赖
8
+ (只依赖已自研的 redis / postgres 客户端与 node 标准库)。
9
+
10
+ ## rateLimit — 限流
11
+
12
+ ```ts
13
+ import { rateLimit } from 'weifuwu'
14
+
15
+ app.use(redis()) // 依赖 ctx.redis
16
+ app.use(rateLimit({ windowMs: 60_000, max: 100 })) // 全局限流(默认固定窗口)
17
+
18
+ app.get('/api/search', async (req, ctx) => {
19
+ await ctx.limit('search', { max: 30, windowMs: 60_000 }) // 手动限流,超限抛 429
20
+ })
21
+
22
+ // 登录/注册防爆破:ctx.limit 默认按 IP 维度(每 IP 独立计数)
23
+ app.post('/api/auth/register', async (req, ctx) => {
24
+ await ctx.limit('register', { max: 5, windowMs: 60_000 }) // 每 IP 每分钟 5 次
25
+ })
26
+
27
+ // 系统级总量限制:scope: 'global' 全局共享维度
28
+ await ctx.limit('total-jobs', { max: 1000, windowMs: 60_000, scope: 'global' })
29
+
30
+ // 登录防爆破(配合 userSystem):组合键 ip:email(key 接收标准 Request,取头拿 IP)
31
+ app.use(rateLimit({ key: (req) => `login:${req.headers.get('x-forwarded-for')}:${req.headers.get('x-user-email')}`, max: 5, windowMs: 15 * 60_000 }))
32
+ ```
33
+
34
+ | 选项 | 默认 | 说明 |
35
+ |------|------|------|
36
+ | `windowMs` | `60000` | 时间窗口 |
37
+ | `max` | `100` | 窗口内最大请求 |
38
+ | `key` | X-Forwarded-For | 限流键(生产环境配置反向代理注入) |
39
+ | `algorithm` | `fixed` | `fixed`(INCR+EXPIRE,原子)\| `sliding`(ZSET,仅 redis) |
40
+ | `store` | `redis` | `redis`(多实例一致)\| `memory`(仅单实例/开发) |
41
+
42
+ - 响应自动带 `RateLimit-Limit` / `RateLimit-Remaining` / `RateLimit-Reset` / `Retry-After`
43
+ - 多实例共享计数:计数在 redis,水平扩展天然一致
44
+
45
+ ## email — 邮件发送
46
+
47
+ ```ts
48
+ import { email } from 'weifuwu'
49
+
50
+ app.use(email({ from: 'no-reply@your.app', adapter: 'resend', resend: { apiKey: process.env.RESEND_API_KEY } }))
51
+ // 或 adapter: 'smtp' + smtp: { host, port, user, pass }(自研 SMTP 客户端,零依赖)
52
+
53
+ app.post('/api/notify', async (req, ctx) => {
54
+ await ctx.email.send({ to: 'user@x.com', subject: '通知', html: '<h1>hi</h1>' })
55
+ })
56
+ ```
57
+
58
+ - 适配器:`resend`(默认,一个 POST)/ `smtp`(自研 node:net + node:tls:EHLO/STARTTLS/AUTH PLAIN/DATA/dot-stuffing,非 ASCII subject 自动 RFC2047 编码)/ 自定义函数
59
+ - 裁剪:附件、退信/送达率(服务商职责)、批量营销不支持
60
+
61
+ ## userSystem — 用户系统
62
+
63
+ ```ts
64
+ import { userSystem } from 'weifuwu'
65
+
66
+ const db = postgres()
67
+ await db.migrate()
68
+ const users = userSystem({ sql: db.sql, secret: process.env.AUTH_SECRET })
69
+ await users.migrate() // 幂等建表(users + sessions)
70
+ app.use(db)
71
+ app.use(users) // 注入 ctx.user / ctx.auth
72
+ users.routes(app) // POST /api/auth/register|login|logout|refresh + GET /api/auth/me
73
+
74
+ app.get('/me', (req, ctx) => ok(ctx.user)) // 已注入
75
+ app.post('/secure', (req, ctx) => { ctx.auth.requireAuth(); ... })
76
+ ```
77
+
78
+ - **安全基线**:scrypt 密码哈希(per-user salt + timing-safe,异步不阻塞);access token = HMAC-SHA256 JWT(与 `weifuwu/client` 的 `auth()` 天然配对);refresh token = 不透明随机串,DB 只存哈希,logout/轮换即撤销
79
+ - **防枚举**:登录失败统一 401(不泄露邮箱是否存在)
80
+ - **`ctx.auth` 方法面**:`register` / `login` / `logout` / `requireAuth` / `setPassword(userId, newPwd)` / `createToken(type, payload, { ttlSeconds })`(邮箱验证/密码重置自接)
81
+ - **多租户感知**:`issueSession` 的 token payload 携带 `tenantId`(来自 `user.tenant`)——中间件自动注入 `ctx.tenantId`,并将会话字段(userId/tenantId/email/name/role)合并到 `ctx.auth`,多租户应用免写 token 解码/租户中间件(数据隔离 SQL 是应用职责)
82
+ - **`routes` 支持 `exclude`**:`users.routes(app, { exclude: ['register'] })`——应用自定义注册流程(如注册时建租户)时跳过框架路由
83
+ - **裁剪**:OAuth、邮箱验证邮件(给底层 API 自接)、多因素、RBAC 权限引擎(只留 `role` 字段)、租户隔离 SQL(框架只做感知,`WHERE tenant_id` 属应用层)
84
+
85
+ ## messager — 消息系统
86
+
87
+ ```ts
88
+ import { messager } from 'weifuwu'
89
+
90
+ const db = postgres()
91
+ await db.migrate()
92
+ const msg = messager({ sql: db.sql, redis: rds }) // redis 可选:跨进程广播
93
+ await msg.migrate() // 幂等建表(conversations + members + messages)
94
+ app.use(db)
95
+ app.use(msg) // 注入 ctx.msg
96
+ msg.routes(app) // /api/messages/*(会话/历史/发消息/已读)
97
+ app.ws('/ws', msg.handler()) // 标准 WS 协议内置
98
+
99
+ // 业务代码:持久化 + 鉴权 + 广播 + 未读 + 历史,一次调用
100
+ const conv = await ctx.msg.createConversation(ctx.user.id, { type: 'group', memberIds: ['u2'] })
101
+ await ctx.msg.sendMessage(conv.id, { senderType: 'user', senderId: ctx.user.id, content: '你好' })
102
+ ctx.msg.broadcast(`conv:${conv.id}`, { type: 'order_chat', orderId: 'o1' }) // 任意实时事件
103
+ ctx.msg.sendTo('u2', { type: 'mention' }) // 用户维度点对点
104
+ ```
105
+
106
+ - **数据模型**:`_weifuwu_conversations` / `_weifuwu_conversation_members` / `_weifuwu_messages`(`sender_type + sender_id` 不 FK users——user/agent/system 消息天然可存);direct 会话同对用户唯一、历史游标分页、未读数(`last_read_at`)、编辑/删除软删
107
+ - **实时协议内置**:`handler()` 提供 `connected / subscribe→subscribed / unsubscribe / ping→pong`——前端 `ctx.ws.send({ type: 'subscribe', room })` 直接可用,两端协议由框架定义
108
+ - **跨进程**:`redis` 选项 → Redis pub/sub 广播(psubscribe 模式),多实例部署天然一致;无 redis 优雅降级单进程。
109
+ **环回去重**:`broadcast` = 本地直发 + Redis publish,本实例的 subscriber 会收到自己 publish 的消息——publish 携带实例唯一标识 `_pid`(`wf:{pid}:{seq}`),订阅回调跳过自己的环回,保证每个事件恰好投递一次(防 token 级事件重复/乱序)
110
+ - **与 userSystem 咬合**:`sendTo(ctx.user.id)` 按身份路由、`createConversation(ctx.user.id)` 创建者即身份、成员校验自动对齐——身份是消息的路由,消息是身份的交互
111
+ - **裁剪**:已读回执状态机(只做未读数)、附件存储、全文搜索、消息确认/重试(可靠投递用 queue)、移动端推送
112
+
113
+ ## queue — 可靠任务队列
114
+
115
+ ```ts
116
+ import { queue } from 'weifuwu'
117
+
118
+ const q = queue() // 默认 REDIS_URL
119
+ app.use(q) // 注入 ctx.queue
120
+
121
+ app.post('/api/generate', async (req, ctx) => {
122
+ await ctx.queue.add('llm.batch', { prompt: '...' }, { attempts: 3 })
123
+ return new Response(null, { status: 202 }) // 立即 202,任务后台执行
124
+ })
125
+
126
+ // 消费者(独立进程或同进程均可,多开安全)
127
+ const worker = q.worker('llm.batch', async (job) => {
128
+ await runLLM(job.data) // 失败自动重试 → 用尽进 DLQ(q:llm.batch:dead)
129
+ }, { concurrency: 5, visibilityTimeout: 30_000 })
130
+ await worker.start()
131
+ await worker.stop() // 优雅停止
132
+ ```
133
+
134
+ - **语义**:at-least-once(handler 可能重复执行——幂等由业务保证);Redis Streams 消费组,多 worker 实例不重复消费
135
+ - **可靠性**:失败 → 延迟重试(间隔 = `visibilityTimeout`,ZSET 延迟队列)→ attempts 用尽 → DLQ;worker 崩溃 → pending 由其他实例 `XAUTOCLAIM` 接管
136
+ - 裁剪:延迟调度(除重试外)、cron、优先级、指数退避、速率限制不支持
137
+
138
+ ## ai — LLM 对话(自研协议 + 零依赖客户端)
139
+
140
+ ```ts
141
+ import { ai } from 'weifuwu'
142
+
143
+ const a = ai() // DEEPSEEK_API_KEY / BASE_URL / MODEL 自动读 env,默认 deepseek-v4-flash
144
+ app.use(a) // 注入 ctx.ai(worker/非请求场景也可直接 a.chat())
145
+
146
+ // 流式对话:路由一行返回 SSE(wf: 协议,详见 design/ai-contract.md)
147
+ app.post('/api/chat', async (req, ctx) => {
148
+ const { messages } = await req.json()
149
+ return ctx.ai.stream({ messages }, {
150
+ signal: req.signal, // 断开即取消 provider 请求
151
+ traceId: req.headers.get('x-trace-id') ?? undefined, // 追踪关联(协议 §7)
152
+ })
153
+ })
154
+
155
+ // 非流式(worker/后台):
156
+ const res = await a.chat({ messages: [{ role: 'user', content: 'hi' }] })
157
+
158
+ // agent 引擎:工具循环 + 人工审批(HITL)
159
+ const agent = a.agent({
160
+ systemPrompt: '你是助手。查询天气时调用 query_weather 工具。',
161
+ tools: [{
162
+ name: 'query_weather',
163
+ description: '查询城市天气',
164
+ parameters: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
165
+ run: async (args, { emit }) => {
166
+ emit('wf:tool_progress', { toolCallId: 'x', step: 1, total: 2, message: '查询中…', status: 'running' })
167
+ return { city: args.city, temp: 25 }
168
+ },
169
+ }],
170
+ humanInTheLoop: true, // 每个工具执行前等人工审批
171
+ })
172
+
173
+ app.post('/api/agent', async (req, ctx) => {
174
+ const { messages } = await req.json()
175
+ return agent.run(messages, { signal: req.signal, traceId: req.headers.get('x-trace-id') ?? undefined })
176
+ })
177
+
178
+ // HITL 审批响应(前端点"允许/拒绝" → POST 到这里)
179
+ app.post('/api/approve', async (req, ctx) => {
180
+ ctx.ai.approve(await req.json()) // { id, decision, modifiedArgs?, note? }
181
+ return new Response(null, { status: 200 })
182
+ })
183
+ ```
184
+
185
+ 前端解码(`weifuwu/client`):
186
+
187
+ ```ts
188
+ import { aiStream } from 'weifuwu/client'
189
+
190
+ const handle = aiStream('/api/chat', { messages }, {
191
+ onToken: (text) => { /* 增量 append 到消息 */ },
192
+ onToolCall: (call) => { /* 渲染工具卡片 */ },
193
+ onDone: () => { /* 收尾 */ },
194
+ onError: (e) => { /* 按 e.code 降级 */ },
195
+ onEvent: (name, data) => { /* x:* 自定义事件透传 */ },
196
+ })
197
+ handle.abort() // 用户停止/组件卸载/导航跳走
198
+ ```
199
+
200
+ 前端对话层(会话语义 + 标准界面,协议对页面透明):
201
+
202
+ ```tsx
203
+ // ctx.ui.useChat:会话语义(消息累积/工具内嵌/审批/重试),返回页面同一个 $
204
+ const $ = ctx.ui.useChat({ url: '/api/chat', approveUrl: '/api/approve' })
205
+ // $.messages / $.input / $.streaming / $.error / $.usage / $.step
206
+ // $.send() / $.stop() / $.retry() / $.clear() / $.approve('approved', note?)
207
+
208
+ // AiChat:标准对话界面(气泡 / 工具卡 / 审批卡 / 自动滚动 / 错误重试)
209
+ return () => <AiChat chat={$} />
210
+
211
+ // agent 模式消息内嵌:msg.toolCalls(ToolCallCard 直接消费)/ msg.approval(ApprovalCard)
212
+ ```
213
+
214
+ > 分层:`ctx.ai`(后端协议)→ `aiStream`(传输解码)→ `useChat`(会话语义)→ `AiChat`(标准界面)。要完全自定义 UI 的应用用 useChat + 自有渲染;要 5 分钟出界面用 AiChat。
215
+
216
+ - **协议**:`wf:` 命名空间(message_start/token/tool_call/tool_progress/usage/done/error + agent 扩展 step/approval_request),SSE 下行 + POST 上行,错误即值、未知事件透传、`x:*` 自定义事件(详见 [design/ai-contract.md](../design/ai-contract.md))
217
+ - **agent 引擎**:`a.agent({ systemPrompt, tools, humanInTheLoop })` 工具循环(LLM → tool_call → 执行 → 回喂 → 重复);工具可 `emit` 进度/自定义事件、接收 `signal` 取消;HITL 审批(`ctx.ai.approve` 响应,拒绝≠终止、modified 改参、超时兜底)
218
+ - **emitter 抽象**:`agent.stream(messages, { emit })`——`wf:*` 事件(step/token/tool_result/usage/done)可接任意通道(SSE/WS/回调),协议不焊死在传输层;`agent.runToResult(messages)` 返回结构化结果 `{ content, steps, usage }`(非流式/worker 场景)
219
+ - **embedding**:`ctx.ai.embed(text)` / `embedMany(texts)` 向量化(默认 `DASHSCOPE_API_KEY` + `text-embedding-v4`,compatible-mode 端点);未配置抛 `AiError('unsupported')`(惰性检查,不静默降级)——知识库/语义检索开箱即用
220
+ - **零依赖**:自研 OpenAI 兼容客户端(fetch + SSE 解析),默认 DeepSeek,`baseUrl` 可换任意 OpenAI 兼容端点(Ollama/vLLM/Moonshot…)
221
+ - **追踪**:前端自动生成 `X-Trace-Id` → 后端以之作为 `message_start.id` → 工具内请求继承同一 traceId,整个 agent run 一次搜完
222
+ - **裁剪**:Anthropic 原生协议、审批持久化(连接断=会话亡)暂不支持;多 agent 编排不承诺(子 agent = 工具已支持);embedding 仅文本(图片/多模态不做)
223
+
224
+ ## 组合示例:注册 → 验证邮件 → 欢迎任务 → 登录防爆破
225
+
226
+ ```ts
227
+ app.use(redis())
228
+ app.use(rateLimit({ key: (req) => `login:${req.headers.get('x-forwarded-for')}`, max: 5, windowMs: 60_000 })) // 防爆破
229
+ app.use(email({ from: 'no-reply@x.com', adapter: 'resend', resend: { apiKey } }))
230
+ app.use(db)
231
+ app.use(users)
232
+ users.routes(app)
233
+
234
+ // 注册:限流守卫 → 用户系统 → 验证邮件 → 欢迎任务入队
235
+ app.post('/api/auth/register', async (req, ctx) => {
236
+ await ctx.limit(`register:${req.ip}`, { max: 10, windowMs: 60_000 })
237
+ const result = await ctx.auth.register(await req.json())
238
+ const token = ctx.auth.createToken('verify', { sub: result.user.id }, { ttlSeconds: 86400 })
239
+ await ctx.email.send({ to: result.user.email, subject: '验证邮箱', html: `...?token=${token}` })
240
+ await ctx.queue.add('welcome.flow', { userId: result.user.id })
241
+ return created(result)
242
+ })
243
+
244
+ const worker = q.worker('welcome.flow', async (job) => { /* 欢迎流程 */ })
245
+ await worker.start()
246
+ ```
package/docs/server.md ADDED
@@ -0,0 +1,356 @@
1
+ # 后端 API — HTTP 服务层(weifuwu)
2
+
3
+ > 以下为完整 API 参考,按需查阅。新手建议先阅读 README 的「核心概念」和「快速开始」。
4
+
5
+ > 本页为 weifuwu 官方文档拆分页 · [返回 README](../README.md)
6
+
7
+ ## Router
8
+
9
+ Trie 路由,支持 URL 参数、通配符、中间件链、WebSocket、GraphQL。
10
+
11
+ ```ts
12
+ import { Router } from 'weifuwu'
13
+
14
+ const app = new Router()
15
+ ```
16
+
17
+ ### 路由方法
18
+
19
+ 每个路由方法接受 `path, ...middlewares[], handler`:
20
+
21
+ ```ts
22
+ app.get('/users', handler)
23
+ app.post('/users', handler)
24
+ app.put('/users/:id', handler)
25
+ app.patch('/users/:id', handler) // PATCH
26
+ app.delete('/users/:id', handler) // DELETE
27
+ app.head('/users', handler) // HEAD
28
+ app.options('/users', handler) // OPTIONS
29
+ app.all('/users', handler) // 所有 HTTP 方法
30
+ ```
31
+
32
+ | 参数 | 类型 | 说明 |
33
+ |------|------|------|
34
+ | `path` | `string` | 路由路径,支持 `:param` 和 `*` 通配符 |
35
+ | `...middlewares` | `Middleware[]` | 路由级中间件(可选) |
36
+ | `handler` | `Handler` 或 `Router` | 处理器或子路由器 |
37
+
38
+ ### 路由级中间件
39
+
40
+ ```ts
41
+ declare module 'weifuwu' { interface Context { auth: { userId: string } } }
42
+
43
+ const requireAuth: Middleware = (req, ctx, next) => {
44
+ if (!req.headers.get('authorization')) return new Response('Unauthorized', { status: 401 })
45
+ ;(ctx as any).auth = { userId: '123' }
46
+ return next(req, ctx)
47
+ }
48
+
49
+ app.get('/users/:id', requireAuth, (req, ctx) => {
50
+ return Response.json({ id: ctx.params.id, auth: ctx.auth })
51
+ })
52
+ ```
53
+
54
+ ### 参数与查询
55
+
56
+ ```ts
57
+ app.get('/posts/:category/:slug', (req, ctx) => {
58
+ ctx.params.category // URL 参数
59
+ ctx.params.slug // URL 参数
60
+ ctx.query.page // 查询参数 ?page=1
61
+ return Response.json(ctx.params)
62
+ })
63
+ ```
64
+
65
+ ### 通配符
66
+
67
+ ```ts
68
+ app.get('/files/*', (req, ctx) => {
69
+ ctx.params['*'] // 剩余路径 "a/b/c.txt"
70
+ return Response.json({ path: ctx.params['*'] })
71
+ })
72
+ ```
73
+
74
+ ### 子路由挂载
75
+
76
+ ```ts
77
+ const users = new Router()
78
+ users.get('/', listUsers)
79
+ users.get('/:id', getUser)
80
+
81
+ app.mount('/api/users', users)
82
+ // → GET /api/users, GET /api/users/:id
83
+ ```
84
+
85
+ ### 插件模式
86
+
87
+ ```ts
88
+ app.plugin(app => {
89
+ app.get('/health', () => Response.json({ ok: true }))
90
+ app.use(cors())
91
+ })
92
+ ```
93
+
94
+ ### 类型安全中间件工厂
95
+
96
+ ```ts
97
+ import { createMiddleware } from 'weifuwu'
98
+ declare module 'weifuwu' { interface Context { greeting: string } }
99
+
100
+ const greet = createMiddleware({
101
+ injects: ['greeting'], // 注入的 ctx 字段
102
+ depends: ['sql'], // 前置依赖(可选)
103
+ setup: async (ctx) => ({
104
+ greeting: 'Hello ' + ctx.params.name
105
+ }),
106
+ })
107
+
108
+ app.use(greet)
109
+ app.get('/hello/:name', (req, ctx) => Response.json({ msg: ctx.greeting }))
110
+ ```
111
+
112
+ `createMiddleware` 自动生成 `__meta` 元数据用于运行时依赖检查。
113
+
114
+ ### 查看路由表
115
+
116
+ ```ts
117
+ console.log(app.routes())
118
+ // → [
119
+ // "GET /users",
120
+ // "POST /users",
121
+ // "WS /chat",
122
+ // "MIDDLEWARE [2 global]"
123
+ // ]
124
+ ```
125
+
126
+ ### 路由方法速查
127
+
128
+ | 方法 | 说明 |
129
+ |------|------|
130
+ | `app.get(path, ...mws, handler)` | GET |
131
+ | `app.post(path, ...mws, handler)` | POST |
132
+ | `app.put(path, ...mws, handler)` | PUT |
133
+ | `app.patch(path, ...mws, handler)` | PATCH |
134
+ | `app.delete(path, ...mws, handler)` | DELETE |
135
+ | `app.head(path, ...mws, handler)` | HEAD |
136
+ | `app.options(path, ...mws, handler)` | OPTIONS |
137
+ | `app.all(path, ...mws, handler)` | 任意方法 |
138
+ | `app.use(mw)` | 全局中间件 |
139
+ | `app.ws(path, ...mws, handler)` | WebSocket |
140
+ | `app.graphql(path?, handler)` | GraphQL |
141
+ | `app.mount(path, subRouter)` | 挂载子路由 |
142
+ | `app.plugin(fn)` | 插件扩展现有 Router |
143
+ | `app.onError(handler)` | 全局错误处理 |
144
+ | `app.wsHub(hub)` | 注入自定义 Hub(多进程 WebSocket) |
145
+ | `app.onClose(closeable)` | 注册关闭回调 |
146
+ | `app.routes()` | 打印路由表 |
147
+ | `app.close()` | 关闭所有注册的 Closeable 资源 |
148
+
149
+ ---
150
+
151
+ ## serve — HTTP 服务器
152
+
153
+ ```ts
154
+ import { serve, Router } from 'weifuwu'
155
+
156
+ const app = new Router()
157
+ const server = serve(app, { port: 3000 })
158
+
159
+ // 等待就绪
160
+ await server.ready
161
+ console.log(server.port) // 实际端口
162
+
163
+ // 停止
164
+ await server.close()
165
+ // 或
166
+ await server.stop(2000) // 超时毫秒
167
+ ```
168
+
169
+ | 选项 | 类型 | 默认值 | 说明 |
170
+ |------|------|--------|------|
171
+ | `port` | `number` | `0`(随机) | 监听端口 |
172
+ | `hostname` | `string` | `'0.0.0.0'` | 监听地址 |
173
+ | `signal` | `AbortSignal` | — | 通过信号停止 |
174
+ | `maxBodySize` | `number` | `10MB` | 请求体上限(0=无限) |
175
+ | `timeout` | `number` | `120000` | Socket 超时(ms,2 分钟,适配 LLM 生成等长任务) |
176
+ | `keepAliveTimeout` | `number` | `5000` | Keep-Alive 超时 |
177
+ | `headersTimeout` | `number` | `6000` | 请求头超时 |
178
+ | `shutdown` | `boolean` | `true` | 自动注册 SIGTERM/SIGINT |
179
+
180
+ | 属性/方法 | 类型/签名 | 说明 |
181
+ |-----------|----------|------|
182
+ | `server.port` | `number` | 实际监听端口(未就绪时 0) |
183
+ | `server.hostname` | `string` | 监听地址 |
184
+ | `server.ready` | `Promise<void>` | 服务器就绪 |
185
+ | `server.close(timeoutMs?)` | `() => Promise<void>` | 优雅关闭 |
186
+ | `server.stop(timeoutMs?)` | `() => Promise<void>` | `close` 别名 |
187
+
188
+ `sig-server` 自动注册 SIGTERM/SIGINT → `server.closeAllConnections()` → `router.close()` → `process.exit(0)`。
189
+
190
+ ---
191
+
192
+ ## cors — CORS 中间件
193
+
194
+ ```ts
195
+ import { cors } from 'weifuwu'
196
+
197
+ // 默认:允许所有来源
198
+ app.use(cors())
199
+
200
+ // 自定义
201
+ app.use(cors({
202
+ origin: 'https://app.example.com',
203
+ methods: ['GET', 'POST'],
204
+ allowedHeaders: ['Content-Type', 'Authorization'],
205
+ credentials: true,
206
+ exposedHeaders: ['X-Total-Count'],
207
+ maxAge: 86400,
208
+ }))
209
+ ```
210
+
211
+ | 选项 | 类型 | 默认值 | 说明 |
212
+ |------|------|--------|------|
213
+ | `origin` | `string \| string[] \| (origin) => string` | `'*'` | `credentials: true` 时自动回显请求 origin |
214
+ | `methods` | `string[]` | `GET,HEAD,PUT,PATCH,POST,DELETE` | 允许的方法 |
215
+ | `allowedHeaders` | `string[]` | `Content-Type, Authorization` | 允许的请求头 |
216
+ | `exposedHeaders` | `string[]` | — | 暴露的响应头 |
217
+ | `credentials` | `boolean` | — | 是否允许凭据 |
218
+ | `maxAge` | `number` | — | 预检缓存秒数 |
219
+
220
+ ---
221
+
222
+ ## serveStatic — 静态文件服务
223
+
224
+ ```ts
225
+ import { serveStatic } from 'weifuwu'
226
+
227
+ // 作为全局中间件(未匹配到文件时走下一个中间件)
228
+ app.use(serveStatic('./public'))
229
+
230
+ // 或挂载到特定路径
231
+ app.get('/assets/*', serveStatic('./assets', {
232
+ index: 'index.html',
233
+ maxAge: 31536000,
234
+ immutable: true,
235
+ }))
236
+ ```
237
+
238
+ | 选项 | 类型 | 默认值 | 说明 |
239
+ |------|------|--------|------|
240
+ | `index` | `string` | `'index.html'` | 目录索引文件名 |
241
+ | `maxAge` | `number` | `0` | Cache-Control max-age(秒) |
242
+ | `immutable` | `boolean` | — | 添加 `immutable` 指令(需 maxAge) |
243
+
244
+ 特性:
245
+ - ETag/304 缓存协商(`if-none-match` + `if-modified-since`)
246
+ - MIME 类型自动检测(支持 30+ 扩展名)
247
+ - 目录遍历保护(`..` / symlink 逃逸 → 403)
248
+ - 目录自动跳转到 index 文件
249
+
250
+ ---
251
+
252
+ ## HttpError — HTTP 错误
253
+
254
+ ```ts
255
+ import { HttpError } from 'weifuwu'
256
+
257
+ app.get('/secure', () => {
258
+ if (!condition) throw new HttpError('Forbidden', 403)
259
+ // serve() 自动捕获并返回对应状态码
260
+ })
261
+ ```
262
+
263
+ | API | 说明 |
264
+ |-----|------|
265
+ | `new HttpError(msg, status)` | 创建 HTTP 错误,name = 'HttpError' |
266
+
267
+ > 请求体上限常量 `DEFAULT_MAX_BODY`(10MB)见上方 serve 选项表 `maxBodySize`。
268
+
269
+ ---
270
+
271
+ ## 响应辅助函数
272
+
273
+ > 以下为完整 API 参考,按需查阅。五个 SaaS 地基模块(rateLimit / email / userSystem / messager / queue)见文末「SaaS 地基模块」章节。
274
+
275
+ 消除 `Response.json(...)` 重复模式:
276
+
277
+ ```ts
278
+ import { ok, created, noContent, badRequest, unauthorized, forbidden, notFound, conflict, unprocessable, tooManyRequests, serverError, redirect } from 'weifuwu'
279
+
280
+ app.get('/users/:id', async (req, ctx) => {
281
+ const user = await findUser(ctx.params.id)
282
+ if (!user) return notFound('用户不存在')
283
+ return ok(user)
284
+ })
285
+
286
+ app.post('/users', async (req, ctx) => {
287
+ const body = await parseBody(req)
288
+ const user = await createUser(body)
289
+ return created(user)
290
+ })
291
+ ```
292
+
293
+ | 函数 | 状态码 | Content-Type |
294
+ |------|--------|-------------|
295
+ | `ok(data, init?)` | 200 | `application/json` |
296
+ | `created(data, init?)` | 201 | `application/json` |
297
+ | `noContent(init?)` | 204 | — |
298
+ | `badRequest(msg?)` | 400 | `application/json` |
299
+ | `unauthorized(msg?)` | 401 | `application/json` |
300
+ | `forbidden(msg?)` | 403 | `application/json` |
301
+ | `notFound(msg?)` | 404 | `application/json` |
302
+ | `conflict(msg?)` | 409 | `application/json` |
303
+ | `unprocessable(msg?)` | 422 | `application/json` |
304
+ | `tooManyRequests(msg?)` | 429 | `application/json` |
305
+ | `serverError(msg?)` | 500 | `application/json` |
306
+ | `redirect(url, status?)` | 302 (默认) | — |
307
+
308
+ ---
309
+
310
+ ## parseBody — 请求体解析
311
+
312
+ ```ts
313
+ import { parseBody } from 'weifuwu'
314
+
315
+ app.post('/users', async (req, ctx) => {
316
+ const body = await parseBody<{ name: string; email: string }>(req)
317
+ // JSON 解析失败自动 throw HttpError(400)
318
+ return ok(body)
319
+ })
320
+ ```
321
+
322
+ | 行为 | 说明 |
323
+ |------|------|
324
+ | JSON 格式正确 | 返回解析后的数据 |
325
+ | JSON 格式错误 | `throw new HttpError('Invalid JSON body', 400)` |
326
+ | GET/HEAD 请求 | 返回 `{}` |
327
+
328
+ ---
329
+
330
+ ## 后端类型
331
+
332
+ ```ts
333
+ import type { Context, Handler, Middleware, ErrorHandler, User, Closeable } from 'weifuwu'
334
+ import type { HttpError } from 'weifuwu'
335
+ import type { ServeOptions, Server } from 'weifuwu'
336
+ import type { Hub, WebSocketHandler } from 'weifuwu'
337
+ import type { WebSocket } from 'weifuwu'
338
+ import type { CORSOptions } from 'weifuwu'
339
+ import type { ServeStaticOptions } from 'weifuwu'
340
+ import type { PostgresOptions, PostgresClient, PostgresInjected } from 'weifuwu'
341
+ import type { RedisOptions, RedisClient, RedisInjected } from 'weifuwu'
342
+ import type { MessagerOptions, MessagerClient, MessagerInjected } from 'weifuwu'
343
+ import type { GraphQLOptions, GraphQLHandler } from 'weifuwu'
344
+ ```
345
+
346
+ | 类型 | 签名 | 说明 |
347
+ |------|------|------|
348
+ | `Context` | `interface` | `{ params, query, mountPath, user, loaderData?, env?, [key]: unknown }` |
349
+ | `Handler<T>` | `(req: Request, ctx: T) => Response \| Promise<Response>` | 请求处理器 |
350
+ | `Middleware<In, Out>` | `(req, ctx: In, next) => Response` | 中间件,含 `__meta` |
351
+ | `ErrorHandler<T>` | `(error, req, ctx: T) => Response` | 错误处理器 |
352
+ | `Closeable` | `interface` | `{ close(): Promise<void> }` |
353
+ | `User` | `interface` | `{ id, role?, tenant?, [key]: unknown }` |
354
+ | `HttpError` | `class` | `extends Error`,含 `status` 属性 |
355
+
356
+ ---