weifuwu 0.24.0 → 0.24.2
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/README.md +970 -756
- package/cli/template/app.ts +5 -1
- package/cli/template/index.ts +4 -1
- package/cli/template/locales/en.json +6 -1
- package/cli/template/locales/zh-CN.json +6 -1
- package/cli/template/locales/zh-TW.json +6 -1
- package/cli/template/locales/zh.json +6 -1
- package/cli/template/ui/app/globals.css +1 -1
- package/cli/template/ui/app/page.tsx +55 -16
- package/cli.ts +148 -104
- package/dist/agent/rest.d.ts +1 -1
- package/dist/agent/run.d.ts +2 -2
- package/dist/agent/types.d.ts +2 -1
- package/dist/ai/workflow.d.ts +1 -1
- package/dist/ai-sdk.d.ts +1 -1
- package/dist/analytics.d.ts +2 -2
- package/dist/cache.d.ts +4 -4
- package/dist/cli.js +135 -97
- package/dist/cookie.d.ts +24 -0
- package/dist/cors.d.ts +2 -2
- package/dist/deploy/types.d.ts +2 -2
- package/dist/fts.d.ts +5 -5
- package/dist/helmet.d.ts +2 -2
- package/dist/hub.d.ts +2 -1
- package/dist/iii/index.d.ts +1 -1
- package/dist/iii/register-worker.d.ts +1 -1
- package/dist/iii/types.d.ts +4 -4
- package/dist/index.d.ts +5 -5
- package/dist/index.js +905 -442
- package/dist/kb/types.d.ts +8 -0
- package/dist/live.d.ts +2 -3
- package/dist/logdb/rest.d.ts +1 -1
- package/dist/logdb/types.d.ts +2 -1
- package/dist/mailer.d.ts +3 -2
- package/dist/messager/agent.d.ts +2 -2
- package/dist/messager/rest.d.ts +3 -3
- package/dist/messager/types.d.ts +2 -1
- package/dist/messager/ws.d.ts +3 -3
- package/dist/opencode/index.d.ts +1 -1
- package/dist/opencode/permissions.d.ts +1 -1
- package/dist/opencode/run.d.ts +1 -1
- package/dist/opencode/session.d.ts +9 -9
- package/dist/opencode/tools/web.d.ts +1 -1
- package/dist/opencode/types.d.ts +2 -1
- package/dist/opencode/ws.d.ts +1 -2
- package/dist/permissions.d.ts +4 -4
- package/dist/postgres/module.d.ts +5 -4
- package/dist/postgres/schema/index.d.ts +1 -1
- package/dist/postgres/schema/table.d.ts +22 -20
- package/dist/postgres/types.d.ts +6 -6
- package/dist/queue/types.d.ts +3 -3
- package/dist/rate-limit.d.ts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +141 -96
- package/dist/redis/types.d.ts +2 -2
- package/dist/router.d.ts +10 -10
- package/dist/seo.d.ts +2 -2
- package/dist/serve.d.ts +1 -1
- package/dist/session.d.ts +8 -5
- package/dist/tailwind.d.ts +9 -0
- package/dist/tenant/graphql.d.ts +2 -2
- package/dist/tenant/index.d.ts +1 -1
- package/dist/tenant/rest.d.ts +2 -2
- package/dist/tenant/types.d.ts +3 -3
- package/dist/test-utils.d.ts +3 -3
- package/dist/types.d.ts +8 -0
- package/dist/upload.d.ts +4 -2
- package/dist/user/index.d.ts +1 -1
- package/dist/user/oauth-login.d.ts +2 -2
- package/dist/user/types.d.ts +2 -2
- package/dist/vendor.d.ts +4 -0
- package/opencode/ui/app/globals.css +1 -1
- package/opencode/ui/app/layout.tsx +2 -3
- package/opencode/ui/app/page.tsx +302 -73
- package/package.json +33 -10
- package/cli/template/.weifuwu/ssr/2e3a7e60.js +0 -112
package/README.md
CHANGED
|
@@ -30,7 +30,20 @@ npx weifuwu init my-app && cd my-app && npm run dev
|
|
|
30
30
|
### Typical Full App
|
|
31
31
|
|
|
32
32
|
```ts
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
serve,
|
|
35
|
+
Router,
|
|
36
|
+
postgres,
|
|
37
|
+
session,
|
|
38
|
+
user,
|
|
39
|
+
aiProvider,
|
|
40
|
+
ssr,
|
|
41
|
+
flash,
|
|
42
|
+
i18n,
|
|
43
|
+
theme,
|
|
44
|
+
logger,
|
|
45
|
+
rateLimit,
|
|
46
|
+
} from 'weifuwu'
|
|
34
47
|
|
|
35
48
|
const app = new Router()
|
|
36
49
|
|
|
@@ -50,14 +63,14 @@ app.use(pg)
|
|
|
50
63
|
app.use(session({ store: 'redis', redis: myRedis }))
|
|
51
64
|
const auth = user({ pg, jwtSecret: process.env.JWT_SECRET })
|
|
52
65
|
await auth.migrate()
|
|
53
|
-
app.use(auth)
|
|
54
|
-
app.use('/auth', auth)
|
|
66
|
+
app.use(auth) // auto-registers middleware + /register, /login
|
|
67
|
+
app.use('/auth', auth) // explicit path mounts for more control
|
|
55
68
|
|
|
56
69
|
// 5. API protection
|
|
57
70
|
app.use('/api', rateLimit({ max: 60, window: 60_000 }))
|
|
58
71
|
|
|
59
72
|
// 6. AI
|
|
60
|
-
app.use(aiProvider())
|
|
73
|
+
app.use(aiProvider()) // ctx.ai
|
|
61
74
|
|
|
62
75
|
// 7. SSR
|
|
63
76
|
app.use('/', ssr({ dir: './ui' }))
|
|
@@ -94,21 +107,21 @@ const server = serve(handler, { port: 3000 })
|
|
|
94
107
|
await server.ready
|
|
95
108
|
```
|
|
96
109
|
|
|
97
|
-
| Option
|
|
98
|
-
|
|
99
|
-
| `port`
|
|
100
|
-
| `hostname`
|
|
101
|
-
| `signal`
|
|
102
|
-
| `websocket`
|
|
103
|
-
| `maxBodySize`
|
|
104
|
-
| `timeout`
|
|
105
|
-
| `keepAliveTimeout` | `number`
|
|
106
|
-
| `headersTimeout`
|
|
107
|
-
| `shutdown`
|
|
110
|
+
| Option | Type | Default | Description |
|
|
111
|
+
| ------------------ | ------------------ | ----------- | ------------------------------ |
|
|
112
|
+
| `port` | `number` | `0` | Listen port |
|
|
113
|
+
| `hostname` | `string` | `'0.0.0.0'` | Listen address |
|
|
114
|
+
| `signal` | `AbortSignal` | — | Shutdown on abort |
|
|
115
|
+
| `websocket` | `WsUpgradeHandler` | — | WebSocket upgrade handler |
|
|
116
|
+
| `maxBodySize` | `number` | `10MB` | Max body bytes (0 = unlimited) |
|
|
117
|
+
| `timeout` | `number` | `30_000` | Socket inactivity timeout (ms) |
|
|
118
|
+
| `keepAliveTimeout` | `number` | `5_000` | Keep-Alive idle timeout (ms) |
|
|
119
|
+
| `headersTimeout` | `number` | `6_000` | Headers read timeout (ms) |
|
|
120
|
+
| `shutdown` | `boolean` | `true` | Auto SIGTERM/SIGINT |
|
|
108
121
|
|
|
109
122
|
```ts
|
|
110
123
|
interface Server {
|
|
111
|
-
stop: (timeoutMs?: number) => Promise<void>
|
|
124
|
+
stop: (timeoutMs?: number) => Promise<void> // graceful: waits for in-flight, force-closes after timeoutMs (default 10s)
|
|
112
125
|
readonly port: number
|
|
113
126
|
readonly hostname: string
|
|
114
127
|
ready: Promise<void>
|
|
@@ -126,16 +139,27 @@ are forcibly closed. SIGTERM/SIGINT use the same graceful pattern.
|
|
|
126
139
|
```ts
|
|
127
140
|
const app = new Router()
|
|
128
141
|
app.get('/hello/:name', (req, ctx) => Response.json({ message: `Hello, ${ctx.params.name}!` }))
|
|
129
|
-
app.post('/data', async (req, ctx) => {
|
|
130
|
-
|
|
131
|
-
|
|
142
|
+
app.post('/data', async (req, ctx) => {
|
|
143
|
+
const body = await req.json()
|
|
144
|
+
return Response.json(body, { status: 201 })
|
|
145
|
+
})
|
|
146
|
+
app.use('/admin', authMW) // path-scoped middleware
|
|
147
|
+
app.use('/admin', adminRouter) // sub-router (flattened into parent trie)
|
|
132
148
|
app.ws('/echo', {
|
|
133
|
-
open(ws, ctx) {
|
|
134
|
-
|
|
149
|
+
open(ws, ctx) {
|
|
150
|
+
ctx.ws.json({ type: 'connected' })
|
|
151
|
+
},
|
|
152
|
+
message(ws, ctx, data) {
|
|
153
|
+
ctx.ws.json({ echo: data.toString() })
|
|
154
|
+
},
|
|
135
155
|
})
|
|
136
156
|
app.ws('/chat', {
|
|
137
|
-
open(ws, ctx) {
|
|
138
|
-
|
|
157
|
+
open(ws, ctx) {
|
|
158
|
+
ctx.ws.join('room')
|
|
159
|
+
},
|
|
160
|
+
message(ws, ctx, data) {
|
|
161
|
+
ctx.ws.sendRoom('room', JSON.parse(data.toString()))
|
|
162
|
+
},
|
|
139
163
|
})
|
|
140
164
|
app.onError((err, req, ctx) => Response.json({ error: err.message }, { status: 500 }))
|
|
141
165
|
|
|
@@ -152,11 +176,11 @@ const wsHandler = app.websocketHandler()
|
|
|
152
176
|
serve(handler, { port: 3000, websocket: wsHandler })
|
|
153
177
|
```
|
|
154
178
|
|
|
155
|
-
| Pattern
|
|
156
|
-
|
|
157
|
-
| Static
|
|
158
|
-
| Param
|
|
159
|
-
| Wildcard | `/static/*`
|
|
179
|
+
| Pattern | Example | Match |
|
|
180
|
+
| -------- | ------------ | ----------------------------- |
|
|
181
|
+
| Static | `/about` | exact |
|
|
182
|
+
| Param | `/users/:id` | `/users/42` → `ctx.params.id` |
|
|
183
|
+
| Wildcard | `/static/*` | `/static/js/app.js` |
|
|
160
184
|
|
|
161
185
|
Query params → `ctx.query`.
|
|
162
186
|
|
|
@@ -170,7 +194,7 @@ Request → serve() → app.handler() → global middleware × N → path middle
|
|
|
170
194
|
|
|
171
195
|
1. `serve()` receives HTTP request
|
|
172
196
|
2. `app.handler()` creates `ctx = { params, query }` and routes to the matching trie node
|
|
173
|
-
3. **Global middleware** runs in `use()` order (e.g. `theme()`, `i18n()`, `postgres()`, `cors()`)
|
|
197
|
+
3. **Global middleware** runs in `use()` order (e.g. `theme()`, `i18n()`, `postgres()`, `cors()`)
|
|
174
198
|
4. **Path‑scoped middleware** runs for matching paths (e.g. `app.use('/admin', authMW)`)
|
|
175
199
|
5. **Route‑level middleware** runs (e.g. `app.get('/admin', validate(...), handler)`)
|
|
176
200
|
6. **Route handler** returns `Response` — middleware chain unwinds
|
|
@@ -181,41 +205,40 @@ Sub-routers (`app.use('/admin', adminRouter)`) are **flattened** into the parent
|
|
|
181
205
|
|
|
182
206
|
```ts
|
|
183
207
|
type Middleware = (req: Request, ctx: Context, next: Handler) => Response | Promise<Response>
|
|
184
|
-
app.use(mw)
|
|
185
|
-
app.use('/admin', mw)
|
|
186
|
-
app.get('/admin', mw, handler)
|
|
208
|
+
app.use(mw) // global
|
|
209
|
+
app.use('/admin', mw) // path-scoped
|
|
210
|
+
app.get('/admin', mw, handler) // route-level
|
|
187
211
|
```
|
|
188
212
|
|
|
189
213
|
### Context
|
|
190
214
|
|
|
191
215
|
The `ctx` object accumulates properties as it passes through the middleware chain. Below are all documented properties:
|
|
192
216
|
|
|
193
|
-
| Property
|
|
194
|
-
|
|
195
|
-
| `params`
|
|
196
|
-
| `query`
|
|
197
|
-
| `mountPath`
|
|
198
|
-
| `env`
|
|
199
|
-
| `
|
|
200
|
-
| `requestId`
|
|
201
|
-
| `session`
|
|
202
|
-
| `sql`
|
|
203
|
-
| `redis`
|
|
204
|
-
| `ai`
|
|
205
|
-
| `queue`
|
|
206
|
-
| `
|
|
207
|
-
| `
|
|
208
|
-
| `
|
|
209
|
-
| `
|
|
210
|
-
| `
|
|
211
|
-
| `
|
|
212
|
-
| `
|
|
213
|
-
| `
|
|
214
|
-
| `
|
|
215
|
-
| `
|
|
216
|
-
| `
|
|
217
|
-
| `
|
|
218
|
-
| `deploy` | `deploy()` | `{ appName? }` | Deploy gateway info |
|
|
217
|
+
| Property | Set by | Type | Description |
|
|
218
|
+
| ------------- | -------------------------------- | ------------------------- | ------------------------------------ |
|
|
219
|
+
| `params` | Router | `Record<string, string>` | URL path parameters |
|
|
220
|
+
| `query` | Router | `Record<string, string>` | URL query parameters |
|
|
221
|
+
| `mountPath` | Router | `string` | Current sub-router mount prefix |
|
|
222
|
+
| `env` | `loadEnv()` | `Record<string, string>` | Public env vars (`WEIFUWU_PUBLIC_*`) |
|
|
223
|
+
| `csrf.token` | `csrf()` | `string` | CSRF token (namespace) |
|
|
224
|
+
| `requestId` | `requestId()` | `string` | Request ID |
|
|
225
|
+
| `session` | `session()` | `Session` | Session data object |
|
|
226
|
+
| `sql` | `postgres()` | `Sql<{}>` | PostgreSQL tagged-template client |
|
|
227
|
+
| `redis` | `redis()` | `Redis` | Redis client |
|
|
228
|
+
| `ai` | `aiProvider()` | `AIProvider` | AI model & embedding |
|
|
229
|
+
| `queue` | `queue()` | `Queue` | Job queue |
|
|
230
|
+
| `user` | `auth()` / `user().middleware()` | `{ id?: string }` | Authenticated user |
|
|
231
|
+
| `permissions` | `permissions()` | `{ roles, permissions }` | RBAC roles & permissions sets |
|
|
232
|
+
| `theme` | `theme()` | `{ value, set }` | Current theme + switcher |
|
|
233
|
+
| `i18n` | `i18n()` | `{ locale, t, set }` | Locale, translation, switcher |
|
|
234
|
+
| `flash` | `flash()` | `{ value, set }` | Flash message + setter |
|
|
235
|
+
| `tailwind` | `tailwindContext()` | `{ css, url }` | Compiled Tailwind CSS |
|
|
236
|
+
| `tenant` | `tenant()` | `TenantContext` | Current tenant info |
|
|
237
|
+
| `parsed` | `validate()` / `upload()` | `{ body, files }` | Validated/parsed request data |
|
|
238
|
+
| `layoutStack` | `ssr()` internal | `LayoutEntry[]` | React layout component stack |
|
|
239
|
+
| `loaderData` | User middleware | `Record<string, unknown>` | SSR data passed to client |
|
|
240
|
+
| `mountPath` | `Router` | `string` | Sub-router mount path |
|
|
241
|
+
| `deploy` | `deploy()` | `{ appName? }` | Deploy gateway info |
|
|
219
242
|
|
|
220
243
|
### Type-Safe Context
|
|
221
244
|
|
|
@@ -223,13 +246,13 @@ Middleware-injected properties are **automatically typed** through chained `use(
|
|
|
223
246
|
|
|
224
247
|
```ts
|
|
225
248
|
const app = new Router()
|
|
226
|
-
.use(csrf())
|
|
227
|
-
.use(requestId())
|
|
228
|
-
.use(postgres())
|
|
249
|
+
.use(csrf()) // → Router<Context & { csrf: { token: string } }>
|
|
250
|
+
.use(requestId()) // → Router<Context & { csrf: ..., requestId }>
|
|
251
|
+
.use(postgres()) // → Router<Context & { csrf: ..., requestId, sql }>
|
|
229
252
|
|
|
230
253
|
app.get('/me', (_req, ctx) => {
|
|
231
|
-
ctx.
|
|
232
|
-
ctx.requestId
|
|
254
|
+
ctx.csrf.token // ✅ string (IDE autocomplete)
|
|
255
|
+
ctx.requestId // ✅ string
|
|
233
256
|
ctx.sql`SELECT 1` // ✅ Sql<{}>
|
|
234
257
|
})
|
|
235
258
|
```
|
|
@@ -242,43 +265,44 @@ Each module exports an `XxxInjected` type (e.g. `PostgresInjected`, `UserInjecte
|
|
|
242
265
|
|
|
243
266
|
All modules follow one of **4 patterns** — learn these and you know every module.
|
|
244
267
|
|
|
245
|
-
| Pattern | How to mount
|
|
246
|
-
|
|
247
|
-
| `[α]`
|
|
248
|
-
| `[β]`
|
|
249
|
-
| `[γ]`
|
|
250
|
-
| `[δ]`
|
|
268
|
+
| Pattern | How to mount | Example |
|
|
269
|
+
| ------- | ---------------------------------------- | ------------------------------------------------------ |
|
|
270
|
+
| `[α]` | `app.use(mod())` | `compress()`, `theme()`, `postgres()` |
|
|
271
|
+
| `[β]` | `app.use('/path', mod())` | `health()`, `ssr({dir})`, `graphql(handler)`, `user()` |
|
|
272
|
+
| `[γ]` | Import and call directly | `mailer()`, `fts`, `cron-utils` |
|
|
273
|
+
| `[δ]` | `import { useXxx } from 'weifuwu/react'` | `useTheme()`, `useLocale()`, `useWebsocket()` |
|
|
251
274
|
|
|
252
275
|
### Pattern α — Middleware
|
|
253
276
|
|
|
254
277
|
```ts
|
|
255
|
-
app.use(compress())
|
|
256
|
-
const pg = postgres()
|
|
278
|
+
app.use(compress()) // basic
|
|
279
|
+
const pg = postgres() // with extras: .sql, .table, .migrate(), .close()
|
|
257
280
|
app.use(pg)
|
|
258
|
-
app.use(rateLimit({ max: 100 }))
|
|
281
|
+
app.use(rateLimit({ max: 100 })) // with .close()
|
|
259
282
|
```
|
|
260
283
|
|
|
261
284
|
### Pattern β — Router
|
|
262
285
|
|
|
263
286
|
```ts
|
|
264
|
-
app.use('/health', health())
|
|
287
|
+
app.use('/health', health()) // with path
|
|
265
288
|
app.use('/graphql', graphql(handler))
|
|
266
|
-
app.use('/logs', logdb({ pg }))
|
|
267
|
-
app.use('/auth', user({ pg, jwtSecret }))
|
|
289
|
+
app.use('/logs', logdb({ pg })) // with .log(), .migrate()
|
|
290
|
+
app.use('/auth', user({ pg, jwtSecret })) // with .middleware(), .register()
|
|
268
291
|
app.ws('/ws', messager({ pg }).wsHandler())
|
|
269
292
|
```
|
|
270
293
|
|
|
271
294
|
β modules that need **separate middleware** use `.middleware()`. Most can auto-register both middleware and routes in one call:
|
|
295
|
+
|
|
272
296
|
```ts
|
|
273
|
-
app.use(theme())
|
|
274
|
-
app.use(i18n({ dir: './locales' }))
|
|
297
|
+
app.use(theme()) // auto: middleware + /__theme/:value
|
|
298
|
+
app.use(i18n({ dir: './locales' })) // auto: middleware + /__lang/:locale
|
|
275
299
|
app.use(analytics({ pg })) // auto: middleware + /__analytics
|
|
276
|
-
app.use(auth)
|
|
300
|
+
app.use(auth) // auto: middleware + /register, /login (user())
|
|
277
301
|
|
|
278
302
|
// Explicit form when more control is needed:
|
|
279
303
|
const a = analytics()
|
|
280
|
-
app.use(a.middleware())
|
|
281
|
-
app.use('/', a)
|
|
304
|
+
app.use(a.middleware()) // tracking only
|
|
305
|
+
app.use('/', a) // dashboard at custom path
|
|
282
306
|
```
|
|
283
307
|
|
|
284
308
|
### Pattern γ — Standalone
|
|
@@ -291,7 +315,7 @@ import { mailer, cronNext, fts } from 'weifuwu'
|
|
|
291
315
|
const email = mailer({ transport: 'smtp://...', from: 'noreply@example.com' })
|
|
292
316
|
await email.send({ to: 'user@test.com', subject: 'Hello', text: 'Body' })
|
|
293
317
|
|
|
294
|
-
const next = cronNext('0 9 * * 1-5')
|
|
318
|
+
const next = cronNext('0 9 * * 1-5') // next weekday at 09:00
|
|
295
319
|
```
|
|
296
320
|
|
|
297
321
|
### Pattern δ — Client-side
|
|
@@ -347,38 +371,54 @@ graph TD
|
|
|
347
371
|
|
|
348
372
|
## Quick Module Selection
|
|
349
373
|
|
|
350
|
-
| What do you want to do?
|
|
351
|
-
|
|
352
|
-
| **User registration / login**
|
|
353
|
-
| **Simple token/header auth**
|
|
354
|
-
| **JWT verification**
|
|
355
|
-
| **Role-based access control**
|
|
356
|
-
| **AI chat / generate / stream**
|
|
357
|
-
| **AI agent with knowledge**
|
|
358
|
-
| **Send email**
|
|
359
|
-
| **File upload**
|
|
360
|
-
| **Object storage (S3/MinIO)**
|
|
361
|
-
| **Rate limiting**
|
|
362
|
-
| **Response caching**
|
|
363
|
-
| **Periodic / delayed jobs**
|
|
364
|
-
| **Page view analytics**
|
|
365
|
-
| **Structured logging**
|
|
366
|
-
| **Real-time chat / messager**
|
|
367
|
-
| **Full-text search**
|
|
368
|
-
| **Theme switching**
|
|
369
|
-
| **i18n / localization**
|
|
370
|
-
| **Flash messages**
|
|
371
|
-
| **Server-Sent Events**
|
|
372
|
-
| **GraphQL endpoint**
|
|
373
|
-
| **Webhook receiver**
|
|
374
|
-
| **SSR with React**
|
|
375
|
-
| **Health check**
|
|
376
|
-
| **SEO (robots.txt, sitemap)**
|
|
377
|
-
| **Multi-process deploy**
|
|
378
|
-
| **Distributed functions (iii)**
|
|
379
|
-
| **Multi-tenant BaaS**
|
|
380
|
-
| **Client-side routing**
|
|
381
|
-
| **WebSocket in React**
|
|
374
|
+
| What do you want to do? | Module | Pattern |
|
|
375
|
+
| -------------------------------- | ----------------------------------------------- | ---------------------- |
|
|
376
|
+
| **User registration / login** | `user()` | β |
|
|
377
|
+
| **Simple token/header auth** | `auth()` | α |
|
|
378
|
+
| **JWT verification** | `user().middleware()` | α |
|
|
379
|
+
| **Role-based access control** | `permissions()` | α |
|
|
380
|
+
| **AI chat / generate / stream** | `ctx.ai.generateText()` / `ctx.ai.streamText()` | α (via `aiProvider()`) |
|
|
381
|
+
| **AI agent with knowledge** | `agent()` + `knowledgeBase()` | β |
|
|
382
|
+
| **Send email** | `mailer()` | γ |
|
|
383
|
+
| **File upload** | `upload()` | α |
|
|
384
|
+
| **Object storage (S3/MinIO)** | `s3()` | α |
|
|
385
|
+
| **Rate limiting** | `rateLimit()` | α |
|
|
386
|
+
| **Response caching** | `cache()` | α |
|
|
387
|
+
| **Periodic / delayed jobs** | `queue()` | α |
|
|
388
|
+
| **Page view analytics** | `analytics()` | β |
|
|
389
|
+
| **Structured logging** | `logdb()` | β |
|
|
390
|
+
| **Real-time chat / messager** | `messager()` | β |
|
|
391
|
+
| **Full-text search** | `fts` | γ |
|
|
392
|
+
| **Theme switching** | `theme()` | α |
|
|
393
|
+
| **i18n / localization** | `i18n()` | α |
|
|
394
|
+
| **Flash messages** | `flash()` | α |
|
|
395
|
+
| **Server-Sent Events** | `createSSEStream()` | γ |
|
|
396
|
+
| **GraphQL endpoint** | `graphql()` | β |
|
|
397
|
+
| **Webhook receiver** | `webhook()` | β |
|
|
398
|
+
| **SSR with React** | `ssr()` | β |
|
|
399
|
+
| **Health check** | `health()` | β |
|
|
400
|
+
| **SEO (robots.txt, sitemap)** | `seo()` | β |
|
|
401
|
+
| **Multi-process deploy** | `deploy()` | γ |
|
|
402
|
+
| **Distributed functions (iii)** | `iii()` | β |
|
|
403
|
+
| **Multi-tenant BaaS** | `tenant()` | β |
|
|
404
|
+
| **Client-side routing** | `useNavigate()`, `<Link>` | δ |
|
|
405
|
+
| **WebSocket in React** | `useWebsocket()` | δ |
|
|
406
|
+
| **Compression (brotli/gzip)** | `compress()` | α |
|
|
407
|
+
| **Security headers (CSP, HSTS)** | `helmet()` | α |
|
|
408
|
+
| **CORS** | `cors()` | α |
|
|
409
|
+
| **CSRF protection** | `csrf()` | α |
|
|
410
|
+
| **Request ID tracing** | `requestId()` | α |
|
|
411
|
+
| **Environment variables** | `env()` / `loadEnv()` | α |
|
|
412
|
+
| **Static file serving** | `serveStatic()` | α |
|
|
413
|
+
| **Object storage (S3/MinIO)** | `s3()` | α |
|
|
414
|
+
| **Send email** | `mailer()` | γ |
|
|
415
|
+
| **Scheduled / cron tasks** | `cron-utils` (`cronNext()`) | γ |
|
|
416
|
+
| **Server-Sent Events** | `createSSEStream()` | γ |
|
|
417
|
+
| **Multi-process deploy** | `deploy()` | γ |
|
|
418
|
+
| **Distributed functions (iii)** | `iii()` | β |
|
|
419
|
+
| **Webhook receiver** | `webhook()` | β |
|
|
420
|
+
| **Social login (OAuth)** | `user({ oauthLogin })` | β |
|
|
421
|
+
| **Database migrations** | `pg.migrate()` | — |
|
|
382
422
|
|
|
383
423
|
---
|
|
384
424
|
|
|
@@ -397,7 +437,16 @@ app.get('/api', (req, ctx) => {
|
|
|
397
437
|
**Structured logging** — `logger({ format: 'json' })` outputs JSON to stderr with `traceId`, `timestamp`, `elapsed_ms`:
|
|
398
438
|
|
|
399
439
|
```json
|
|
400
|
-
{
|
|
440
|
+
{
|
|
441
|
+
"level": "info",
|
|
442
|
+
"message": "request",
|
|
443
|
+
"method": "GET",
|
|
444
|
+
"path": "/api/users",
|
|
445
|
+
"status": 200,
|
|
446
|
+
"elapsed_ms": 42,
|
|
447
|
+
"traceId": "f240a3f3-...",
|
|
448
|
+
"timestamp": "2025-01-15T10:30:00.000Z"
|
|
449
|
+
}
|
|
401
450
|
```
|
|
402
451
|
|
|
403
452
|
Default format is `'short'` (human-readable). `'combined'` includes query strings.
|
|
@@ -465,12 +514,12 @@ assert.equal(res.status, 200)
|
|
|
465
514
|
assert.deepEqual(await res.json(), { id: '42', user: { id: 1 } })
|
|
466
515
|
```
|
|
467
516
|
|
|
468
|
-
| Method
|
|
469
|
-
|
|
470
|
-
| `app.getReq(path)` `postReq` `putReq` `patchReq` `deleteReq` | Start building a request
|
|
471
|
-
| `.withUser(u)` `.withTenant(t)` `.with(ctx)`
|
|
472
|
-
| `.header(k,v)` `.body(data)` `.rawBody(str)`
|
|
473
|
-
| `.send()` → `TestResponse`
|
|
517
|
+
| Method | Description |
|
|
518
|
+
| ------------------------------------------------------------ | ----------------------------------------------------- |
|
|
519
|
+
| `app.getReq(path)` `postReq` `putReq` `patchReq` `deleteReq` | Start building a request |
|
|
520
|
+
| `.withUser(u)` `.withTenant(t)` `.with(ctx)` | Simulate middleware injection |
|
|
521
|
+
| `.header(k,v)` `.body(data)` `.rawBody(str)` | Set request properties |
|
|
522
|
+
| `.send()` → `TestResponse` | Execute and get `{ status, headers, json(), text() }` |
|
|
474
523
|
|
|
475
524
|
### Database test isolation
|
|
476
525
|
|
|
@@ -481,7 +530,7 @@ import { createTestDb, withTestDb } from 'weifuwu'
|
|
|
481
530
|
const db = await createTestDb()
|
|
482
531
|
await db.sql`CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)`
|
|
483
532
|
await db.sql`INSERT INTO users (name) VALUES ('Alice')`
|
|
484
|
-
await db.destroy()
|
|
533
|
+
await db.destroy() // DROP SCHEMA ... CASCADE
|
|
485
534
|
|
|
486
535
|
// Transaction rollback — all changes are rolled back after callback
|
|
487
536
|
await withTestDb(async (sql) => {
|
|
@@ -490,10 +539,10 @@ await withTestDb(async (sql) => {
|
|
|
490
539
|
})
|
|
491
540
|
```
|
|
492
541
|
|
|
493
|
-
| Function
|
|
494
|
-
|
|
495
|
-
| `createTestDb(opts?)`
|
|
496
|
-
| `withTestDb(url?, fn)` | Run callback in a transaction, auto-rollback
|
|
542
|
+
| Function | Description |
|
|
543
|
+
| ---------------------- | --------------------------------------------------------------- |
|
|
544
|
+
| `createTestDb(opts?)` | Create isolated schema, returns `{ sql, url, schema, destroy }` |
|
|
545
|
+
| `withTestDb(url?, fn)` | Run callback in a transaction, auto-rollback |
|
|
497
546
|
|
|
498
547
|
Uses `TEST_DATABASE_URL` or `DATABASE_URL`. Automatically skipped in CI if unset.
|
|
499
548
|
|
|
@@ -518,21 +567,21 @@ await a.addKnowledge(agentId, 'Title', 'some knowledge content')
|
|
|
518
567
|
a.run(agentId, { input: 'summarize the data', stream: true })
|
|
519
568
|
```
|
|
520
569
|
|
|
521
|
-
| Option
|
|
522
|
-
|
|
523
|
-
| `pg`
|
|
524
|
-
| `provider`
|
|
525
|
-
| `model`
|
|
526
|
-
| `embeddingModel`
|
|
527
|
-
| `embeddingDimension` | `number`
|
|
528
|
-
| `tools`
|
|
570
|
+
| Option | Type | Default | Description |
|
|
571
|
+
| -------------------- | ------------ | ------------------------- | --------------------------------------------- |
|
|
572
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
573
|
+
| `provider` | `AIProvider` | `aiProvider()` (from env) | AI provider for model & embedding resolution |
|
|
574
|
+
| `model` | `object` | — | Explicit AI model (overrides provider) |
|
|
575
|
+
| `embeddingModel` | `object` | — | Explicit embedding model (overrides provider) |
|
|
576
|
+
| `embeddingDimension` | `number` | `provider.dimension` | Embedding vector dimension |
|
|
577
|
+
| `tools` | `object[]` | — | Custom tool definitions |
|
|
529
578
|
|
|
530
|
-
| Method
|
|
531
|
-
|
|
579
|
+
| Method | Description |
|
|
580
|
+
| ---------------------------------------------- | ------------------------ |
|
|
532
581
|
| `.run(agentId, { input, stream?, messages? })` | Execute agent with input |
|
|
533
|
-
| `.addKnowledge(agentId, title, content)`
|
|
534
|
-
| `.migrate()`
|
|
535
|
-
| `.close()`
|
|
582
|
+
| `.addKnowledge(agentId, title, content)` | Add knowledge document |
|
|
583
|
+
| `.migrate()` | DB setup |
|
|
584
|
+
| `.close()` | Cleanup |
|
|
536
585
|
|
|
537
586
|
### aiStream [β] [AI]
|
|
538
587
|
|
|
@@ -544,10 +593,10 @@ const chat = await aiStream(async (req) => ({ messages: (await req.json()).messa
|
|
|
544
593
|
app.use('/chat', chat)
|
|
545
594
|
```
|
|
546
595
|
|
|
547
|
-
| Param
|
|
548
|
-
|
|
549
|
-
| `handler`
|
|
550
|
-
| `provider` | `AIProvider`
|
|
596
|
+
| Param | Type | Description |
|
|
597
|
+
| ---------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
598
|
+
| `handler` | `(req, ctx) => AIStreamOptions \| Promise<AIStreamOptions>` | Returns AI SDK options (model, messages, schema, etc.) |
|
|
599
|
+
| `provider` | `AIProvider` | Optional. If provided and handler omits `model`, `provider.model()` is used as default |
|
|
551
600
|
|
|
552
601
|
### analytics [β] [API]
|
|
553
602
|
|
|
@@ -556,49 +605,52 @@ In-memory or PostgreSQL page view tracking with built-in dashboard.
|
|
|
556
605
|
```ts
|
|
557
606
|
const a = analytics()
|
|
558
607
|
app.use(a.middleware())
|
|
559
|
-
app.use('/', a)
|
|
608
|
+
app.use('/', a) // GET /__analytics (dashboard), GET /__analytics/data?days=7 (JSON)
|
|
560
609
|
```
|
|
561
610
|
|
|
562
|
-
| Option
|
|
563
|
-
|
|
564
|
-
| `pg`
|
|
565
|
-
| `excluded` | `string[]` | `['/__analytics', '/__wfw', '/static']` | Paths to skip
|
|
611
|
+
| Option | Type | Default | Description |
|
|
612
|
+
| ---------- | ---------- | --------------------------------------- | --------------------------------- |
|
|
613
|
+
| `pg` | `object` | — | PostgreSQL client for persistence |
|
|
614
|
+
| `excluded` | `string[]` | `['/__analytics', '/__wfw', '/static']` | Paths to skip |
|
|
566
615
|
|
|
567
616
|
```ts
|
|
568
617
|
// With PostgreSQL
|
|
569
618
|
const a = analytics({ pg })
|
|
570
619
|
await a.migrate()
|
|
571
620
|
app.use(a.middleware())
|
|
572
|
-
app.use('/', a)
|
|
621
|
+
app.use('/', a) // dashboard routes
|
|
573
622
|
```
|
|
574
623
|
|
|
575
624
|
### auth [α] [Security]
|
|
576
625
|
|
|
577
626
|
```ts
|
|
578
|
-
app.use(auth({ token: 'sk-123' }))
|
|
579
|
-
app.use(auth({ header: 'X-API-Key', token: 'my-key' }))
|
|
627
|
+
app.use(auth({ token: 'sk-123' })) // static token
|
|
628
|
+
app.use(auth({ header: 'X-API-Key', token: 'my-key' })) // custom header
|
|
580
629
|
app.use(auth({ verify: async (token, req) => ({ sub: 'abc' }) })) // custom verify → sets ctx.user
|
|
581
630
|
app.get('/protected', auth({ proxy: 'http://auth:3000/validate' }), handler)
|
|
582
631
|
|
|
583
632
|
// Session-based auth (must be placed after session() middleware)
|
|
584
633
|
app.use(session())
|
|
585
|
-
app.use(
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
634
|
+
app.use(
|
|
635
|
+
auth({
|
|
636
|
+
session: true,
|
|
637
|
+
resolveUser: async (userId) => {
|
|
638
|
+
// load user from DB
|
|
639
|
+
const [user] = await sql`SELECT * FROM users WHERE id = ${userId}`
|
|
640
|
+
return user ?? null // null → destroy stale session
|
|
641
|
+
},
|
|
642
|
+
}),
|
|
643
|
+
)
|
|
592
644
|
```
|
|
593
645
|
|
|
594
|
-
| Option
|
|
595
|
-
|
|
596
|
-
| `token`
|
|
597
|
-
| `header`
|
|
598
|
-
| `verify`
|
|
599
|
-
| `proxy`
|
|
600
|
-
| `session`
|
|
601
|
-
| `resolveUser` | `(userId) => object\|null`
|
|
646
|
+
| Option | Type | Default | Description |
|
|
647
|
+
| ------------- | ------------------------------ | ----------------- | -------------------------------------------------------------------------------------------------------- |
|
|
648
|
+
| `token` | `string` | — | Static token to match |
|
|
649
|
+
| `header` | `string` | `'Authorization'` | Header name |
|
|
650
|
+
| `verify` | `(token, req) => object\|null` | — | Verify function, return value sets `ctx.user` |
|
|
651
|
+
| `proxy` | `string` | — | Auth service URL to proxy requests to |
|
|
652
|
+
| `session` | `boolean` | `false` | Enable session-based auth. Checks `ctx.session.userId` first |
|
|
653
|
+
| `resolveUser` | `(userId) => object\|null` | — | Load user from userId (called when `session: true`). Return falsy to reject + auto-destroy stale session |
|
|
602
654
|
|
|
603
655
|
When `session: true`, auth checks `ctx.session.userId` before the
|
|
604
656
|
Authorization header. This lets logged-in users authenticate via their
|
|
@@ -608,32 +660,32 @@ if no session userId is present.
|
|
|
608
660
|
### compress [α] [DevTools]
|
|
609
661
|
|
|
610
662
|
```ts
|
|
611
|
-
app.use(compress())
|
|
612
|
-
app.use(compress({ threshold: 2048, level: 4 }))
|
|
663
|
+
app.use(compress()) // brotli > gzip > deflate (min 1KB)
|
|
664
|
+
app.use(compress({ threshold: 2048, level: 4 })) // custom threshold and level
|
|
613
665
|
```
|
|
614
666
|
|
|
615
|
-
| Option
|
|
616
|
-
|
|
617
|
-
| `threshold` | `number` | `1024`
|
|
618
|
-
| `level`
|
|
667
|
+
| Option | Type | Default | Description |
|
|
668
|
+
| ----------- | -------- | ------- | ----------------------------- |
|
|
669
|
+
| `threshold` | `number` | `1024` | Minimum byte size to compress |
|
|
670
|
+
| `level` | `number` | `6` | Compression level (zlib) |
|
|
619
671
|
|
|
620
672
|
### cors [α] [DevTools]
|
|
621
673
|
|
|
622
674
|
```ts
|
|
623
|
-
app.use(cors())
|
|
624
|
-
app.use(cors({ origin: ['https://example.com'] }))
|
|
675
|
+
app.use(cors()) // allow all
|
|
676
|
+
app.use(cors({ origin: ['https://example.com'] })) // whitelist
|
|
625
677
|
app.use(cors({ origin: (o) => o.endsWith('.trusted.com') && o }))
|
|
626
678
|
app.use(cors({ credentials: true, maxAge: 3600 }))
|
|
627
679
|
```
|
|
628
680
|
|
|
629
|
-
| Option
|
|
630
|
-
|
|
631
|
-
| `origin`
|
|
632
|
-
| `methods`
|
|
633
|
-
| `allowedHeaders` | `string[]`
|
|
634
|
-
| `exposedHeaders` | `string[]`
|
|
635
|
-
| `credentials`
|
|
636
|
-
| `maxAge`
|
|
681
|
+
| Option | Type | Default | Description |
|
|
682
|
+
| ---------------- | ---------------------------- | -------------------------------------------------------- | ---------------------------------- |
|
|
683
|
+
| `origin` | `string\|string[]\|function` | `'*'` | Allowed origins |
|
|
684
|
+
| `methods` | `string[]` | `['GET','POST','PUT','DELETE','PATCH','HEAD','OPTIONS']` | Allowed methods |
|
|
685
|
+
| `allowedHeaders` | `string[]` | — | Custom allowed headers |
|
|
686
|
+
| `exposedHeaders` | `string[]` | — | Response headers exposed to client |
|
|
687
|
+
| `credentials` | `boolean` | `false` | Allow cookies/credentials |
|
|
688
|
+
| `maxAge` | `number` | — | Preflight cache duration (seconds) |
|
|
637
689
|
|
|
638
690
|
### flash [α] [UX]
|
|
639
691
|
|
|
@@ -643,7 +695,7 @@ Cookie-based flash message. Read from request, write via redirect.
|
|
|
643
695
|
app.use(flash())
|
|
644
696
|
|
|
645
697
|
app.get('/', (req, ctx) => {
|
|
646
|
-
const msg = ctx.flash.value
|
|
698
|
+
const msg = ctx.flash.value // { type: 'success', text: 'Saved!' } or undefined
|
|
647
699
|
})
|
|
648
700
|
|
|
649
701
|
app.post('/save', (req, ctx) => {
|
|
@@ -651,8 +703,8 @@ app.post('/save', (req, ctx) => {
|
|
|
651
703
|
})
|
|
652
704
|
```
|
|
653
705
|
|
|
654
|
-
| Option | Type
|
|
655
|
-
|
|
706
|
+
| Option | Type | Default | Description |
|
|
707
|
+
| ------ | -------- | --------- | ----------- |
|
|
656
708
|
| `name` | `string` | `'flash'` | Cookie name |
|
|
657
709
|
|
|
658
710
|
### cache [α] [DevTools]
|
|
@@ -660,30 +712,32 @@ app.post('/save', (req, ctx) => {
|
|
|
660
712
|
Response caching middleware with memory and Redis stores. Caches GET/HEAD responses, with tag-based invalidation.
|
|
661
713
|
|
|
662
714
|
```ts
|
|
663
|
-
app.use(cache())
|
|
664
|
-
app.use(cache({ ttl: 60_000, store: 'redis', redis: ctx.redis }))
|
|
665
|
-
app.use(
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
})
|
|
715
|
+
app.use(cache()) // in-memory, 5min TTL
|
|
716
|
+
app.use(cache({ ttl: 60_000, store: 'redis', redis: ctx.redis })) // Redis store
|
|
717
|
+
app.use(
|
|
718
|
+
cache({
|
|
719
|
+
ttl: 30_000,
|
|
720
|
+
tag: (req, ctx) => (ctx.user ? `user:${ctx.user.id}` : undefined), // per-user invalidation
|
|
721
|
+
}),
|
|
722
|
+
)
|
|
669
723
|
|
|
670
724
|
// Programmatic invalidation
|
|
671
725
|
const c = cache({ store: 'redis', redis: ctx.redis })
|
|
672
726
|
app.use(c)
|
|
673
|
-
await c.invalidate('users')
|
|
674
|
-
await c.flush()
|
|
727
|
+
await c.invalidate('users') // invalidate all entries tagged with 'users'
|
|
728
|
+
await c.flush() // clear entire cache
|
|
675
729
|
```
|
|
676
730
|
|
|
677
|
-
| Option
|
|
678
|
-
|
|
679
|
-
| `ttl`
|
|
680
|
-
| `store`
|
|
681
|
-
| `redis`
|
|
682
|
-
| `key`
|
|
683
|
-
| `tag`
|
|
684
|
-
| `cacheCookies` | `boolean`
|
|
685
|
-
| `cacheStatus`
|
|
686
|
-
| `maxBodySize`
|
|
731
|
+
| Option | Type | Default | Description |
|
|
732
|
+
| -------------- | ----------------------------------- | ------------------ | --------------------------------------------- |
|
|
733
|
+
| `ttl` | `number` | `300000` (5min) | Cache TTL in ms |
|
|
734
|
+
| `store` | `'memory' \| 'redis' \| CacheStore` | `'memory'` | Cache store backend |
|
|
735
|
+
| `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
|
|
736
|
+
| `key` | `(req) => string` | SHA256(method+URL) | Custom cache key |
|
|
737
|
+
| `tag` | `(req, ctx) => string \| string[]` | — | Tag for grouped invalidation |
|
|
738
|
+
| `cacheCookies` | `boolean` | `false` | Cache responses with Set-Cookie |
|
|
739
|
+
| `cacheStatus` | `number[]` | `[200]` | Status codes to cache |
|
|
740
|
+
| `maxBodySize` | `number` | `1048576` (1MB) | Max body bytes to cache |
|
|
687
741
|
|
|
688
742
|
Cached responses include `X-Cache: HIT` and `Age` headers. Requests with `Authorization` or `Cookie` headers are never cached. Binary content types (image, audio, video) are skipped.
|
|
689
743
|
|
|
@@ -691,7 +745,11 @@ Cached responses include `X-Cache: HIT` and `Age` headers. Requests with `Author
|
|
|
691
745
|
import { MemoryCache, RedisCache } from 'weifuwu'
|
|
692
746
|
|
|
693
747
|
const mem = new MemoryCache()
|
|
694
|
-
await mem.set(
|
|
748
|
+
await mem.set(
|
|
749
|
+
'key',
|
|
750
|
+
{ status: 200, statusText: 'OK', headers: {}, body: '...', createdAt: Date.now(), tags: [] },
|
|
751
|
+
300_000,
|
|
752
|
+
)
|
|
695
753
|
mem.close()
|
|
696
754
|
```
|
|
697
755
|
|
|
@@ -699,17 +757,17 @@ mem.close()
|
|
|
699
757
|
|
|
700
758
|
```ts
|
|
701
759
|
app.use(csrf())
|
|
702
|
-
// ctx.
|
|
760
|
+
// ctx.csrf.token — set on GET/HEAD/OPTIONS
|
|
703
761
|
// Auto-validates x-csrf-token or x-xsrf-token header on POST/PUT/DELETE/PATCH
|
|
704
762
|
// Falls back to body field matching the key name
|
|
705
763
|
```
|
|
706
764
|
|
|
707
|
-
| Option
|
|
708
|
-
|
|
709
|
-
| `cookie`
|
|
710
|
-
| `header`
|
|
711
|
-
| `key`
|
|
712
|
-
| `excludeMethods` | `['GET','HEAD','OPTIONS']` | Skip validation
|
|
765
|
+
| Option | Default | Description |
|
|
766
|
+
| ---------------- | -------------------------- | ----------------------------------------- |
|
|
767
|
+
| `cookie` | `'_csrf'` | Cookie name |
|
|
768
|
+
| `header` | `'x-csrf-token'` | Header name (also accepts `x-xsrf-token`) |
|
|
769
|
+
| `key` | `'_csrf'` | Body field fallback |
|
|
770
|
+
| `excludeMethods` | `['GET','HEAD','OPTIONS']` | Skip validation |
|
|
713
771
|
|
|
714
772
|
### deploy [β] [Networking]
|
|
715
773
|
|
|
@@ -719,26 +777,30 @@ Multi-process manager with reverse proxy, health checks, auto-restart, and zero-
|
|
|
719
777
|
import { deploy, defineConfig } from 'weifuwu'
|
|
720
778
|
|
|
721
779
|
// Local
|
|
722
|
-
await deploy(
|
|
723
|
-
|
|
724
|
-
}
|
|
780
|
+
await deploy(
|
|
781
|
+
defineConfig({
|
|
782
|
+
apps: { blog: {}, api: {} },
|
|
783
|
+
}),
|
|
784
|
+
)
|
|
725
785
|
|
|
726
786
|
// Production
|
|
727
|
-
await deploy(
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
}
|
|
787
|
+
await deploy(
|
|
788
|
+
defineConfig({
|
|
789
|
+
domain: 'example.com',
|
|
790
|
+
deployToken: process.env.DEPLOY_TOKEN,
|
|
791
|
+
apps: { blog: {}, api: {} },
|
|
792
|
+
}),
|
|
793
|
+
)
|
|
732
794
|
```
|
|
733
795
|
|
|
734
796
|
**Auto-derived defaults** — each app key derives `dir`, `port`, `entry`, and `path`:
|
|
735
797
|
|
|
736
|
-
| Field
|
|
737
|
-
|
|
738
|
-
| `dir`
|
|
739
|
-
| `entry` | `'index.ts'` | Default entry file
|
|
740
|
-
| `port`
|
|
741
|
-
| `path`
|
|
798
|
+
| Field | Default | Rule |
|
|
799
|
+
| ------- | ------------ | -------------------------- |
|
|
800
|
+
| `dir` | App key | `blog` → `'./blog'` |
|
|
801
|
+
| `entry` | `'index.ts'` | Default entry file |
|
|
802
|
+
| `port` | `3001+` | Auto-incremented from 3001 |
|
|
803
|
+
| `path` | `'/key'` | Only for localhost domain |
|
|
742
804
|
|
|
743
805
|
Override any field explicitly:
|
|
744
806
|
|
|
@@ -762,7 +824,11 @@ apps: {
|
|
|
762
824
|
**Blue-green** — zero-downtime via `ports`:
|
|
763
825
|
|
|
764
826
|
```ts
|
|
765
|
-
apps: {
|
|
827
|
+
apps: {
|
|
828
|
+
blog: {
|
|
829
|
+
ports: [3001, 3002]
|
|
830
|
+
}
|
|
831
|
+
}
|
|
766
832
|
```
|
|
767
833
|
|
|
768
834
|
**WebSocket** — automatically bridged through the gateway.
|
|
@@ -771,15 +837,15 @@ apps: { blog: { ports: [3001, 3002] } }
|
|
|
771
837
|
|
|
772
838
|
**Management API** — all endpoints require `Authorization: Bearer <deployToken>`:
|
|
773
839
|
|
|
774
|
-
| Endpoint
|
|
775
|
-
|
|
776
|
-
| `/_deploy/apps`
|
|
777
|
-
| `/_deploy/apps/:name`
|
|
778
|
-
| `/_deploy/apps/:name/deploy`
|
|
779
|
-
| `/_deploy/apps/:name/restart` | POST
|
|
780
|
-
| `/_deploy/apps/:name/stop`
|
|
781
|
-
| `/_deploy/apps/:name/start`
|
|
782
|
-
| `/_deploy/apps/:name/logs`
|
|
840
|
+
| Endpoint | Method | Description |
|
|
841
|
+
| ----------------------------- | ------ | -------------- |
|
|
842
|
+
| `/_deploy/apps` | GET | List apps |
|
|
843
|
+
| `/_deploy/apps/:name` | GET | App details |
|
|
844
|
+
| `/_deploy/apps/:name/deploy` | POST | Restart |
|
|
845
|
+
| `/_deploy/apps/:name/restart` | POST | Restart |
|
|
846
|
+
| `/_deploy/apps/:name/stop` | POST | Stop |
|
|
847
|
+
| `/_deploy/apps/:name/start` | POST | Start |
|
|
848
|
+
| `/_deploy/apps/:name/logs` | GET | SSE log stream |
|
|
783
849
|
|
|
784
850
|
```bash
|
|
785
851
|
curl -H "Authorization: Bearer my-token" http://localhost:3000/_deploy/apps
|
|
@@ -796,28 +862,61 @@ Restart=always
|
|
|
796
862
|
|
|
797
863
|
**DeployConfig:**
|
|
798
864
|
|
|
799
|
-
| Option
|
|
800
|
-
|
|
801
|
-
| `domain`
|
|
802
|
-
| `port`
|
|
803
|
-
| `deployToken` | —
|
|
804
|
-
| `defaultApp`
|
|
805
|
-
| `apps`
|
|
865
|
+
| Option | Default | Description |
|
|
866
|
+
| ------------- | ------------- | ------------------------------- |
|
|
867
|
+
| `domain` | `'localhost'` | Root domain |
|
|
868
|
+
| `port` | `3000` | Gateway port |
|
|
869
|
+
| `deployToken` | — | Bearer token for management API |
|
|
870
|
+
| `defaultApp` | — | Fallback route |
|
|
871
|
+
| `apps` | — | `Record<string, AppConfig>` |
|
|
806
872
|
|
|
807
873
|
**AppConfig:**
|
|
808
874
|
|
|
809
|
-
| Field
|
|
810
|
-
|
|
811
|
-
| `dir`
|
|
812
|
-
| `port`
|
|
813
|
-
| `entry`
|
|
814
|
-
| `path`
|
|
815
|
-
| `env`
|
|
816
|
-
| `healthEndpoint` | `/`
|
|
817
|
-
| `buildCommand`
|
|
818
|
-
| `ports`
|
|
875
|
+
| Field | Default | Description |
|
|
876
|
+
| ---------------- | ---------------- | ------------------------------- |
|
|
877
|
+
| `dir` | App key | Directory containing the app |
|
|
878
|
+
| `port` | Auto (3001+) | Internal port |
|
|
879
|
+
| `entry` | `'index.ts'` | Entry file |
|
|
880
|
+
| `path` | `'/key'` (local) | URL path prefix |
|
|
881
|
+
| `env` | — | Environment variables |
|
|
882
|
+
| `healthEndpoint` | `/` | Health check path |
|
|
883
|
+
| `buildCommand` | — | Build command |
|
|
884
|
+
| `ports` | — | `[port, port+1]` for blue-green |
|
|
819
885
|
|
|
886
|
+
### env [α] [DevTools]
|
|
820
887
|
|
|
888
|
+
Environment variable middleware. Injects `ctx.env` with all `WEIFUWU_PUBLIC_*` variables (prefix stripped).
|
|
889
|
+
Safe to expose to the client.
|
|
890
|
+
|
|
891
|
+
```ts
|
|
892
|
+
import { env, loadEnv } from 'weifuwu'
|
|
893
|
+
loadEnv() // Load .env into process.env
|
|
894
|
+
app.use(env()) // → ctx.env
|
|
895
|
+
|
|
896
|
+
app.get('/config', (req, ctx) => {
|
|
897
|
+
return Response.json({ apiUrl: ctx.env.API_URL })
|
|
898
|
+
})
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
Helper utilities:
|
|
902
|
+
|
|
903
|
+
```ts
|
|
904
|
+
import { isDev, isProd, isBundled, getPublicEnv } from 'weifuwu'
|
|
905
|
+
|
|
906
|
+
isDev() // NODE_ENV === 'development'
|
|
907
|
+
isProd() // NODE_ENV === 'production'
|
|
908
|
+
isBundled() // Running from compiled dist/index.js?
|
|
909
|
+
getPublicEnv() // { API_URL: '...' } — no middleware needed
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
| Function | Description |
|
|
913
|
+
| ---------------- | ---------------------------------------------------------------- |
|
|
914
|
+
| `loadEnv(path?)` | Load `.env` file into `process.env` (does not override existing) |
|
|
915
|
+
| `env()` | Middleware — injects `ctx.env` with public vars |
|
|
916
|
+
| `getPublicEnv()` | Returns `WEIFUWU_PUBLIC_*` vars with prefix stripped |
|
|
917
|
+
| `isDev()` | `true` when `NODE_ENV === 'development'` |
|
|
918
|
+
| `isProd()` | `true` when `NODE_ENV === 'production'` |
|
|
919
|
+
| `isBundled()` | `true` when running from compiled bundle |
|
|
821
920
|
|
|
822
921
|
### graphql [β] [API]
|
|
823
922
|
|
|
@@ -825,22 +924,22 @@ Restart=always
|
|
|
825
924
|
const handler: GraphQLHandler = () => ({
|
|
826
925
|
schema: `type Query { hello: String }`,
|
|
827
926
|
resolvers: { Query: { hello: () => 'world' } },
|
|
828
|
-
graphiql: true,
|
|
829
|
-
maxDepth: 10,
|
|
830
|
-
timeout: 30_000,
|
|
927
|
+
graphiql: true, // GET / returns GraphiQL IDE
|
|
928
|
+
maxDepth: 10, // max query nesting (default 10, 0 = disable)
|
|
929
|
+
timeout: 30_000, // execution timeout in ms
|
|
831
930
|
})
|
|
832
931
|
app.use('/graphql', graphql(handler))
|
|
833
932
|
```
|
|
834
933
|
|
|
835
|
-
| Option
|
|
836
|
-
|
|
837
|
-
| `schema`
|
|
838
|
-
| `resolvers` | `object`
|
|
839
|
-
| `rootValue` | `any`
|
|
840
|
-
| `context`
|
|
841
|
-
| `graphiql`
|
|
842
|
-
| `maxDepth`
|
|
843
|
-
| `timeout`
|
|
934
|
+
| Option | Type | Default | Description |
|
|
935
|
+
| ----------- | ------------------------- | -------- | ------------------------------ |
|
|
936
|
+
| `schema` | `string \| GraphQLSchema` | — | SDL string or pre-built schema |
|
|
937
|
+
| `resolvers` | `object` | — | Resolver map |
|
|
938
|
+
| `rootValue` | `any` | — | Root value for queries |
|
|
939
|
+
| `context` | `(req, ctx) => object` | — | Per-request context factory |
|
|
940
|
+
| `graphiql` | `boolean` | `false` | Serve GraphiQL IDE at GET / |
|
|
941
|
+
| `maxDepth` | `number` | `10` | Max query nesting depth |
|
|
942
|
+
| `timeout` | `number` | `30_000` | Execution timeout (ms) |
|
|
844
943
|
|
|
845
944
|
### health [β] [API]
|
|
846
945
|
|
|
@@ -849,10 +948,10 @@ app.use('/health', health())
|
|
|
849
948
|
// Returns 200 on success, 503 when check throws
|
|
850
949
|
```
|
|
851
950
|
|
|
852
|
-
| Option
|
|
853
|
-
|
|
854
|
-
| `path`
|
|
855
|
-
| `check` | `() => Promise<void>` | —
|
|
951
|
+
| Option | Type | Default | Description |
|
|
952
|
+
| ------- | --------------------- | ----------- | ---------------------------- |
|
|
953
|
+
| `path` | `string` | `'/health'` | Health check endpoint |
|
|
954
|
+
| `check` | `() => Promise<void>` | — | Async function; throws → 503 |
|
|
856
955
|
|
|
857
956
|
### helmet [α] [Security]
|
|
858
957
|
|
|
@@ -863,17 +962,17 @@ app.use(helmet())
|
|
|
863
962
|
app.use(helmet({ contentSecurityPolicy: "default-src 'self'", xFrameOptions: 'DENY' }))
|
|
864
963
|
```
|
|
865
964
|
|
|
866
|
-
| Option
|
|
867
|
-
|
|
868
|
-
| `contentSecurityPolicy`
|
|
869
|
-
| `xFrameOptions`
|
|
870
|
-
| `strictTransportSecurity`
|
|
871
|
-
| `referrerPolicy`
|
|
872
|
-
| `xContentTypeOptions`
|
|
873
|
-
| `permissionsPolicy`
|
|
874
|
-
| `crossOriginEmbedderPolicy` | —
|
|
875
|
-
| `crossOriginOpenerPolicy`
|
|
876
|
-
| `crossOriginResourcePolicy` | —
|
|
965
|
+
| Option | Default | Description |
|
|
966
|
+
| --------------------------- | --------------------------------------- | -------------------------- |
|
|
967
|
+
| `contentSecurityPolicy` | `"default-src 'self'"` | CSP policy |
|
|
968
|
+
| `xFrameOptions` | `'SAMEORIGIN'` | Frame-embedding policy |
|
|
969
|
+
| `strictTransportSecurity` | `'max-age=15552000; includeSubDomains'` | HSTS |
|
|
970
|
+
| `referrerPolicy` | `'no-referrer'` | Referrer header |
|
|
971
|
+
| `xContentTypeOptions` | `'nosniff'` | MIME sniffing protection |
|
|
972
|
+
| `permissionsPolicy` | — | Feature permissions policy |
|
|
973
|
+
| `crossOriginEmbedderPolicy` | — | COEP header |
|
|
974
|
+
| `crossOriginOpenerPolicy` | — | COOP header |
|
|
975
|
+
| `crossOriginResourcePolicy` | — | CORP header |
|
|
877
976
|
|
|
878
977
|
### iii [β] — Worker / Function / Trigger [API]
|
|
879
978
|
|
|
@@ -886,30 +985,30 @@ app.use('/iii', engine)
|
|
|
886
985
|
app.ws('/iii', engine.wsHandler())
|
|
887
986
|
|
|
888
987
|
const w = createWorker('orders')
|
|
889
|
-
w.registerFunction('orders::create', async (payload) =>
|
|
988
|
+
w.registerFunction('orders::create', async (payload) =>
|
|
989
|
+
db.query('INSERT INTO orders ...', [payload.items]),
|
|
990
|
+
)
|
|
890
991
|
engine.addWorker(w)
|
|
891
992
|
await engine.trigger({ function_id: 'orders::create', payload: { items: ['apple'] } })
|
|
892
993
|
```
|
|
893
994
|
|
|
894
|
-
| Option
|
|
895
|
-
|
|
896
|
-
| `pg`
|
|
897
|
-
| `redis`
|
|
898
|
-
| `streamTTL` | `number` | `3600`
|
|
899
|
-
|
|
900
|
-
| Method
|
|
901
|
-
|
|
902
|
-
| `.addWorker(w)`
|
|
903
|
-
| `.removeWorker(w)`
|
|
904
|
-
| `.trigger({ function_id, payload, action?, timeout_ms? })` | Invoke a function
|
|
905
|
-
| `.listWorkers()`
|
|
906
|
-
| `.listFunctions()`
|
|
907
|
-
| `.listTriggers()`
|
|
908
|
-
| `.wsHandler()`
|
|
909
|
-
| `.migrate()`
|
|
910
|
-
| `.shutdown()`
|
|
911
|
-
|
|
912
|
-
|
|
995
|
+
| Option | Type | Default | Description |
|
|
996
|
+
| ----------- | -------- | ------- | --------------------------------------------- |
|
|
997
|
+
| `pg` | `object` | — | PostgreSQL client for persistent triggers |
|
|
998
|
+
| `redis` | `object` | — | Redis client for streams |
|
|
999
|
+
| `streamTTL` | `number` | `3600` | Redis stream key TTL (seconds, 0 = no expiry) |
|
|
1000
|
+
|
|
1001
|
+
| Method | Description |
|
|
1002
|
+
| ---------------------------------------------------------- | ------------------------- |
|
|
1003
|
+
| `.addWorker(w)` | Register a worker |
|
|
1004
|
+
| `.removeWorker(w)` | Remove a worker |
|
|
1005
|
+
| `.trigger({ function_id, payload, action?, timeout_ms? })` | Invoke a function |
|
|
1006
|
+
| `.listWorkers()` | List registered workers |
|
|
1007
|
+
| `.listFunctions()` | List registered functions |
|
|
1008
|
+
| `.listTriggers()` | List registered triggers |
|
|
1009
|
+
| `.wsHandler()` | WebSocket handler |
|
|
1010
|
+
| `.migrate()` | DB setup |
|
|
1011
|
+
| `.shutdown()` | Clean shutdown |
|
|
913
1012
|
|
|
914
1013
|
### knowledgeBase [β] — RAG with pgvector [AI]
|
|
915
1014
|
|
|
@@ -950,21 +1049,20 @@ app.get('/search', async (req, ctx) => {
|
|
|
950
1049
|
})
|
|
951
1050
|
```
|
|
952
1051
|
|
|
953
|
-
| Option
|
|
954
|
-
|
|
955
|
-
| `pg`
|
|
956
|
-
| `provider`
|
|
957
|
-
| `table`
|
|
958
|
-
| `chunkSize`
|
|
959
|
-
| `chunkOverlap`
|
|
960
|
-
| `searchLimit`
|
|
961
|
-
| `searchThreshold` | `number`
|
|
1052
|
+
| Option | Type | Default | Description |
|
|
1053
|
+
| ----------------- | ---------------- | ------------ | --------------------------------------- |
|
|
1054
|
+
| `pg` | `PostgresClient` | — | **Required.** PostgreSQL client |
|
|
1055
|
+
| `provider` | `AIProvider` | — | **Required.** AI provider for embedding |
|
|
1056
|
+
| `table` | `string` | `'_kb_docs'` | Database table name |
|
|
1057
|
+
| `chunkSize` | `number` | `512` | Max characters per chunk |
|
|
1058
|
+
| `chunkOverlap` | `number` | `64` | Overlap between chunks |
|
|
1059
|
+
| `searchLimit` | `number` | `5` | Default search result count |
|
|
1060
|
+
| `searchThreshold` | `number` | `0` | Minimum similarity (0–1) |
|
|
962
1061
|
|
|
963
1062
|
Documents are split on paragraph boundaries (`\n\n`). Re-ingesting the same key
|
|
964
1063
|
replaces old chunks. Provider's `embed()` is used automatically.
|
|
965
1064
|
The HNSW index enables fast approximate nearest-neighbor search (cosine distance).
|
|
966
1065
|
|
|
967
|
-
|
|
968
1066
|
### logdb [β] [API]
|
|
969
1067
|
|
|
970
1068
|
PostgreSQL structured event logging with monthly partitioning.
|
|
@@ -973,69 +1071,79 @@ PostgreSQL structured event logging with monthly partitioning.
|
|
|
973
1071
|
const logger = logdb({ pg })
|
|
974
1072
|
await logger.migrate()
|
|
975
1073
|
app.use('/logs', logger)
|
|
976
|
-
await logger.clean(12)
|
|
1074
|
+
await logger.clean(12) // drop partitions older than 12 months
|
|
977
1075
|
await logger.log({ level: 'info', source: 'app', message: 'hello', metadata: { userId: 1 } })
|
|
978
1076
|
```
|
|
979
1077
|
|
|
980
|
-
| Option
|
|
981
|
-
|
|
982
|
-
| `pg`
|
|
983
|
-
| `table` | `string` | `'_log_entries'` | Table name
|
|
1078
|
+
| Option | Type | Default | Description |
|
|
1079
|
+
| ------- | -------- | ---------------- | ----------------- |
|
|
1080
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
1081
|
+
| `table` | `string` | `'_log_entries'` | Table name |
|
|
984
1082
|
|
|
985
|
-
| Method | Path
|
|
986
|
-
|
|
987
|
-
| POST
|
|
988
|
-
| GET
|
|
989
|
-
| GET
|
|
1083
|
+
| Method | Path | Description |
|
|
1084
|
+
| ------ | ------ | ---------------------------------------------------------------- |
|
|
1085
|
+
| POST | `/` | Create log entry |
|
|
1086
|
+
| GET | `/` | Query (`?level=`, `?source=`, `?after=`, `?before=`, `?meta.*=`) |
|
|
1087
|
+
| GET | `/:id` | Get single entry |
|
|
990
1088
|
|
|
991
1089
|
### logger [α] [DevTools]
|
|
992
1090
|
|
|
993
1091
|
```ts
|
|
994
|
-
app.use(logger())
|
|
995
|
-
app.use(logger({ format: 'combined' }))
|
|
1092
|
+
app.use(logger()) // GET /hello 200 5ms
|
|
1093
|
+
app.use(logger({ format: 'combined' })) // with query params
|
|
996
1094
|
```
|
|
997
1095
|
|
|
998
|
-
| Option
|
|
999
|
-
|
|
1000
|
-
| `format` | `'short' \| 'combined'` | `'short'` | Log format: path only,
|
|
1096
|
+
| Option | Type | Default | Description |
|
|
1097
|
+
| -------- | --------------------------------- | --------- | ------------------------------------------------------------- |
|
|
1098
|
+
| `format` | `'short' \| 'combined' \| 'json'` | `'short'` | Log format: path only, path + query params, or JSON to stderr |
|
|
1001
1099
|
|
|
1002
1100
|
### mailer [γ] [Networking]
|
|
1003
1101
|
|
|
1004
1102
|
```ts
|
|
1005
|
-
const mail = mailer({
|
|
1006
|
-
|
|
1103
|
+
const mail = mailer({
|
|
1104
|
+
from: 'noreply@example.com',
|
|
1105
|
+
transport: 'smtp://user:pass@smtp.example.com:587',
|
|
1106
|
+
})
|
|
1107
|
+
await mail.send({
|
|
1108
|
+
to: 'user@test.com',
|
|
1109
|
+
subject: 'Hello',
|
|
1110
|
+
text: 'Body',
|
|
1111
|
+
html: '<p>Body</p>',
|
|
1112
|
+
cc: 'admin@test.com',
|
|
1113
|
+
})
|
|
1007
1114
|
```
|
|
1008
1115
|
|
|
1009
|
-
| Option
|
|
1010
|
-
|
|
1011
|
-
| `transport` | `string\|object` | —
|
|
1012
|
-
| `from`
|
|
1013
|
-
| `send`
|
|
1116
|
+
| Option | Type | Default | Description |
|
|
1117
|
+
| ----------- | ---------------- | ------- | ------------------------------------------------ |
|
|
1118
|
+
| `transport` | `string\|object` | — | Nodemailer transport config or connection string |
|
|
1119
|
+
| `from` | `string` | — | Default sender address |
|
|
1120
|
+
| `send` | `function` | — | Custom send function (alternative to transport) |
|
|
1014
1121
|
|
|
1122
|
+
### oauthLogin (via user()) — Social login (OAuth 2.0 client) [Security]
|
|
1015
1123
|
|
|
1016
|
-
|
|
1017
|
-
### oauthClient [β] — Social login (OAuth 2.0 client) [Security]
|
|
1124
|
+
Social login is built into the [`user()`](#user-β) module via the `oauthLogin` option — no separate import needed.
|
|
1018
1125
|
|
|
1019
1126
|
```ts
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
app.use(session()) // required — stores OAuth state
|
|
1023
|
-
app.use(user({ pg, jwtSecret })) // required — user management
|
|
1024
|
-
app.use('/auth', oauthClient({ // mounts /auth/google, /auth/google/callback
|
|
1127
|
+
app.use(session()) // required — stores OAuth state
|
|
1128
|
+
const u = user({
|
|
1025
1129
|
pg,
|
|
1026
|
-
jwtSecret
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1130
|
+
jwtSecret: process.env.JWT_SECRET!,
|
|
1131
|
+
oauthLogin: {
|
|
1132
|
+
redirectUrl: '/dashboard',
|
|
1133
|
+
providers: {
|
|
1134
|
+
google: {
|
|
1135
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
1136
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
1137
|
+
},
|
|
1138
|
+
github: {
|
|
1139
|
+
clientId: process.env.GITHUB_CLIENT_ID,
|
|
1140
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
|
1141
|
+
},
|
|
1036
1142
|
},
|
|
1037
1143
|
},
|
|
1038
|
-
})
|
|
1144
|
+
})
|
|
1145
|
+
await u.migrate()
|
|
1146
|
+
app.use(u) // POST /register, POST /login, GET /auth/:provider, GET /auth/:provider/callback
|
|
1039
1147
|
```
|
|
1040
1148
|
|
|
1041
1149
|
**Flow:** User clicks "Login with Google" → redirected to Google → back to app → user created/linked in database → JWT signed → session created → redirected to `redirectUrl` with `?token=` (or JSON response for API clients).
|
|
@@ -1043,40 +1151,37 @@ app.use('/auth', oauthClient({ // mounts /auth/google, /auth/google/ca
|
|
|
1043
1151
|
Supports custom providers via `authUrl`, `tokenUrl`, `userUrl`, and `parseUser`:
|
|
1044
1152
|
|
|
1045
1153
|
```ts
|
|
1046
|
-
|
|
1154
|
+
const u = user({
|
|
1047
1155
|
pg,
|
|
1048
|
-
jwtSecret
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1156
|
+
jwtSecret: process.env.JWT_SECRET!,
|
|
1157
|
+
oauthLogin: {
|
|
1158
|
+
providers: {
|
|
1159
|
+
discord: {
|
|
1160
|
+
clientId: process.env.DISCORD_CLIENT_ID,
|
|
1161
|
+
clientSecret: process.env.DISCORD_CLIENT_SECRET,
|
|
1162
|
+
authUrl: 'https://discord.com/api/oauth2/authorize',
|
|
1163
|
+
tokenUrl: 'https://discord.com/api/oauth2/token',
|
|
1164
|
+
userUrl: 'https://discord.com/api/users/@me',
|
|
1165
|
+
parseUser: (data) => ({
|
|
1166
|
+
id: data.id,
|
|
1167
|
+
email: data.email ?? '',
|
|
1168
|
+
name: data.global_name ?? data.username,
|
|
1169
|
+
avatarUrl: data.avatar
|
|
1170
|
+
? `https://cdn.discordapp.com/avatars/${data.id}/${data.avatar}.png`
|
|
1171
|
+
: '',
|
|
1172
|
+
}),
|
|
1173
|
+
},
|
|
1064
1174
|
},
|
|
1065
1175
|
},
|
|
1066
|
-
})
|
|
1176
|
+
})
|
|
1067
1177
|
```
|
|
1068
1178
|
|
|
1069
|
-
| Option | Type
|
|
1070
|
-
|
|
1071
|
-
| `
|
|
1072
|
-
| `
|
|
1073
|
-
| `providers` | `Record<string, OAuthProviderConfig>` | — | **Required.** Provider configs (Google/GitHub built-in, any custom) |
|
|
1074
|
-
| `redirectUrl` | `string` | `'/'` | Post-login redirect destination |
|
|
1075
|
-
| `expiresIn` | `string \| number` | `'24h'` | JWT expiry |
|
|
1076
|
-
| `table` | `string` | `'_auth_providers'` | Provider-user link table name |
|
|
1077
|
-
|
|
1078
|
-
The module auto-creates a `_auth_providers` table (`user_id`, `provider`, `provider_id`, `email`, `name`, `avatar_url`) on first request. Built-in providers (Google, GitHub) have preset URLs — you only need to provide `clientId` and `clientSecret`.
|
|
1179
|
+
| Option (oauthLogin) | Type | Default | Description |
|
|
1180
|
+
| ------------------- | ------------------------------------- | ------- | ------------------------------------------------------------------- |
|
|
1181
|
+
| `providers` | `Record<string, OAuthProviderConfig>` | — | **Required.** Provider configs (Google/GitHub built-in, any custom) |
|
|
1182
|
+
| `redirectUrl` | `string` | `'/'` | Post-login redirect destination |
|
|
1079
1183
|
|
|
1184
|
+
Built-in providers (Google, GitHub) have preset URLs — you only need to provide `clientId` and `clientSecret`. The module auto-creates a `_auth_providers` table on first request.
|
|
1080
1185
|
|
|
1081
1186
|
### messager [β] [Networking]
|
|
1082
1187
|
|
|
@@ -1090,20 +1195,18 @@ app.ws('/ws', msg.wsHandler())
|
|
|
1090
1195
|
await msg.send(channelId, 'System message', { sender_type: 'system', sender_id: 'bot' })
|
|
1091
1196
|
```
|
|
1092
1197
|
|
|
1093
|
-
| Option
|
|
1094
|
-
|
|
1095
|
-
| `pg`
|
|
1096
|
-
| `agents`
|
|
1097
|
-
| `webhookTimeout` | `number`
|
|
1098
|
-
| `redis`
|
|
1099
|
-
|
|
1100
|
-
| Method | Description |
|
|
1101
|
-
|--------|-------------|
|
|
1102
|
-
| `.wsHandler()` | WebSocket handler (channels, typing, read receipts) |
|
|
1103
|
-
| `.send(channel, content, opts?)` | Send message to channel |
|
|
1104
|
-
| `.close()` | Cleanup |
|
|
1105
|
-
|
|
1198
|
+
| Option | Type | Default | Description |
|
|
1199
|
+
| ---------------- | ------------- | ------- | ------------------------ |
|
|
1200
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
1201
|
+
| `agents` | `AgentModule` | — | Agent module for routing |
|
|
1202
|
+
| `webhookTimeout` | `number` | — | Webhook timeout |
|
|
1203
|
+
| `redis` | `object` | — | Redis client |
|
|
1106
1204
|
|
|
1205
|
+
| Method | Description |
|
|
1206
|
+
| -------------------------------- | --------------------------------------------------- |
|
|
1207
|
+
| `.wsHandler()` | WebSocket handler (channels, typing, read receipts) |
|
|
1208
|
+
| `.send(channel, content, opts?)` | Send message to channel |
|
|
1209
|
+
| `.close()` | Cleanup |
|
|
1107
1210
|
|
|
1108
1211
|
### opencode [β] [AI]
|
|
1109
1212
|
|
|
@@ -1121,35 +1224,35 @@ app.use('/opencode', oc)
|
|
|
1121
1224
|
app.ws('/opencode', oc.wsHandler())
|
|
1122
1225
|
```
|
|
1123
1226
|
|
|
1124
|
-
| Option
|
|
1125
|
-
|
|
1126
|
-
| `pg`
|
|
1127
|
-
| `model`
|
|
1128
|
-
| `baseURL`
|
|
1129
|
-
| `apiKey`
|
|
1130
|
-
| `workspace`
|
|
1131
|
-
| `systemPrompt` | `string`
|
|
1132
|
-
| `skills`
|
|
1133
|
-
| `permissions`
|
|
1227
|
+
| Option | Type | Default | Description |
|
|
1228
|
+
| -------------- | ---------- | ------- | ------------------------------------------------------ |
|
|
1229
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
1230
|
+
| `model` | `string` | — | AI model name (e.g. `'gpt-4o'`, `'deepseek-v4-flash'`) |
|
|
1231
|
+
| `baseURL` | `string` | — | OpenAI-compatible API base URL |
|
|
1232
|
+
| `apiKey` | `string` | — | API key for the model |
|
|
1233
|
+
| `workspace` | `string` | — | Project directory |
|
|
1234
|
+
| `systemPrompt` | `string` | — | Custom system prompt |
|
|
1235
|
+
| `skills` | `object[]` | — | Custom skill definitions |
|
|
1236
|
+
| `permissions` | `object` | — | Tool permission rules |
|
|
1134
1237
|
|
|
1135
1238
|
### postgres [α] [Database]
|
|
1136
1239
|
|
|
1137
1240
|
Type-safe PostgreSQL client with schema builder, CRUD, migrations, soft delete, and JSONB/vector support.
|
|
1138
1241
|
|
|
1139
1242
|
```ts
|
|
1140
|
-
const pg = postgres()
|
|
1141
|
-
app.use(pg)
|
|
1243
|
+
const pg = postgres() // reads DATABASE_URL
|
|
1244
|
+
app.use(pg) // injects ctx.sql
|
|
1142
1245
|
```
|
|
1143
1246
|
|
|
1144
|
-
| Option
|
|
1145
|
-
|
|
1146
|
-
| `connection`
|
|
1147
|
-
| `max`
|
|
1148
|
-
| `ssl`
|
|
1149
|
-
| `idle_timeout`
|
|
1150
|
-
| `connect_timeout`
|
|
1151
|
-
| `statementTimeout` | `number`
|
|
1152
|
-
| `onQuery`
|
|
1247
|
+
| Option | Type | Default | Description |
|
|
1248
|
+
| ------------------ | --------------------------- | ------------------ | --------------------------------------- |
|
|
1249
|
+
| `connection` | `string` | `DATABASE_URL` env | PostgreSQL connection string |
|
|
1250
|
+
| `max` | `number` | `10` | Max pool connections |
|
|
1251
|
+
| `ssl` | `boolean\|object` | — | SSL options |
|
|
1252
|
+
| `idle_timeout` | `number` | `30` | Idle timeout (seconds) |
|
|
1253
|
+
| `connect_timeout` | `number` | `30` | Connection timeout |
|
|
1254
|
+
| `statementTimeout` | `number` | `30_000` | Per-statement timeout (ms, 0 = disable) |
|
|
1255
|
+
| `onQuery` | `(query, ms, rows) => void` | — | Query logging callback |
|
|
1153
1256
|
|
|
1154
1257
|
```ts
|
|
1155
1258
|
// Raw SQL via tagged template
|
|
@@ -1165,40 +1268,46 @@ const users = pg.table('_users', {
|
|
|
1165
1268
|
active: boolean('active').default(true),
|
|
1166
1269
|
...timestamps(),
|
|
1167
1270
|
})
|
|
1168
|
-
await users.create()
|
|
1271
|
+
await users.create() // DDL — no need to pass sql
|
|
1169
1272
|
await users.createIndex('email')
|
|
1170
1273
|
|
|
1171
1274
|
// CRUD — sql already bound
|
|
1172
1275
|
await users.insert({ name: 'Alice' })
|
|
1173
|
-
const { count, data } = await users.readMany(
|
|
1276
|
+
const { count, data } = await users.readMany(
|
|
1277
|
+
{ role: 'admin' },
|
|
1278
|
+
{ orderBy: { name: 'asc' }, limit: 10 },
|
|
1279
|
+
)
|
|
1174
1280
|
await users.upsert({ email: 'alice@test.com' }, 'email')
|
|
1175
1281
|
|
|
1176
1282
|
// Reuse schema without redefining fields
|
|
1177
1283
|
import { pgTable } from 'weifuwu'
|
|
1178
|
-
const usersSchema = pgTable('_users', { id: serial('id'), name: text('name') })
|
|
1179
|
-
const users = pg.table(usersSchema)
|
|
1284
|
+
const usersSchema = pgTable('_users', { id: serial('id'), name: text('name') }) // define once
|
|
1285
|
+
const users = pg.table(usersSchema) // bind — no field duplication
|
|
1180
1286
|
|
|
1181
1287
|
// Transactions — with auto-retry on deadlock/serialization failure
|
|
1182
|
-
await pg.transaction(
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1288
|
+
await pg.transaction(
|
|
1289
|
+
async (sql) => {
|
|
1290
|
+
const txUsers = users.withSql(sql)
|
|
1291
|
+
return txUsers.insert({ name: 'Bob' })
|
|
1292
|
+
},
|
|
1293
|
+
{ maxRetries: 3 },
|
|
1294
|
+
)
|
|
1186
1295
|
|
|
1187
1296
|
// Soft delete — automatic if deleted_at column exists
|
|
1188
|
-
await users.delete(1)
|
|
1189
|
-
await users.hardDelete(1)
|
|
1190
|
-
await users.read(1)
|
|
1297
|
+
await users.delete(1) // SET deleted_at = NOW()
|
|
1298
|
+
await users.hardDelete(1) // DELETE FROM
|
|
1299
|
+
await users.read(1) // auto-filters deleted_at IS NULL (use withDeleted: true to include)
|
|
1191
1300
|
|
|
1192
1301
|
// JSONB queries
|
|
1193
1302
|
const logs = pg.table('logs', { meta: jsonb<{ service: string }>('meta') })
|
|
1194
1303
|
await logs.readMany(contains('meta', { service: 'auth' }))
|
|
1195
1304
|
|
|
1196
1305
|
// Connection pool visibility
|
|
1197
|
-
console.log(pg.poolStats())
|
|
1306
|
+
console.log(pg.poolStats()) // { active: 3, idle: 7, waiting: 0, max: 10 }
|
|
1198
1307
|
|
|
1199
1308
|
// Migration tracking
|
|
1200
|
-
await pg.migrate()
|
|
1201
|
-
await pg.markMigrated('myModule')
|
|
1309
|
+
await pg.migrate() // creates _weifuwu_migrations
|
|
1310
|
+
await pg.markMigrated('myModule') // idempotent
|
|
1202
1311
|
const done = await pg.isMigrated('myModule')
|
|
1203
1312
|
|
|
1204
1313
|
// Partitioned tables
|
|
@@ -1212,61 +1321,65 @@ await logs.create({ partitionBy: partitionBy('range', 'created_at') })
|
|
|
1212
1321
|
| `pg.table(schema)` | Reusing a schema without duplicating field definitions |
|
|
1213
1322
|
| `pgTable('t', cols)` | No `pg` reference (utility modules, standalone schema files) |
|
|
1214
1323
|
|
|
1215
|
-
| Column builder
|
|
1216
|
-
|
|
1217
|
-
| `serial(name)`
|
|
1218
|
-
| `uuid(name)`
|
|
1219
|
-
| `text(name)`
|
|
1220
|
-
| `integer(name)`
|
|
1221
|
-
| `boolean(name)` / `boolean_(name)` | `boolean`
|
|
1222
|
-
| `timestamptz(name)`
|
|
1223
|
-
| `jsonb<T>(name)`
|
|
1224
|
-
| `textArray(name)`
|
|
1225
|
-
| `vector(name, dims)`
|
|
1324
|
+
| Column builder | Type | Notes |
|
|
1325
|
+
| ---------------------------------- | ---------- | ------------------------------- |
|
|
1326
|
+
| `serial(name)` | `number` | Auto-increment |
|
|
1327
|
+
| `uuid(name)` | `string` | — |
|
|
1328
|
+
| `text(name)` | `string` | — |
|
|
1329
|
+
| `integer(name)` | `number` | — |
|
|
1330
|
+
| `boolean(name)` / `boolean_(name)` | `boolean` | `_` suffix for JS reserved word |
|
|
1331
|
+
| `timestamptz(name)` | `string` | — |
|
|
1332
|
+
| `jsonb<T>(name)` | `T` | Generic for typed JSONB access |
|
|
1333
|
+
| `textArray(name)` | `string[]` | TEXT[] |
|
|
1334
|
+
| `vector(name, dims)` | `number[]` | pgvector support |
|
|
1226
1335
|
|
|
1227
1336
|
**Column modifiers:** `.primaryKey()`, `.notNull()`, `.nullable()`, `.default(val)`, `.unique()`, `.references(table, column?, onDelete?)`.
|
|
1228
1337
|
|
|
1229
1338
|
**CRUD methods:**
|
|
1230
1339
|
|
|
1231
|
-
| Method
|
|
1232
|
-
|
|
1233
|
-
| `insert(data)`
|
|
1234
|
-
| `insertMany(data)`
|
|
1235
|
-
| `read(id, opts?)`
|
|
1236
|
-
| `readMany(where?, opts?)`
|
|
1237
|
-
| `update(id, data)`
|
|
1238
|
-
| `updateMany(where, data)`
|
|
1239
|
-
| `delete(id)`
|
|
1240
|
-
| `hardDelete(id)`
|
|
1241
|
-
| `deleteMany(where)`
|
|
1242
|
-
| `hardDeleteMany(where)`
|
|
1243
|
-
| `upsert(data, conflict)`
|
|
1244
|
-
| `count(where?)`
|
|
1245
|
-
| `create(opts?)`
|
|
1246
|
-
| `drop(opts?)`
|
|
1247
|
-
| `createIndex(columns, opts?)` | CREATE INDEX
|
|
1248
|
-
| `createUniqueIndex(columns)`
|
|
1249
|
-
| `withSql(sql)`
|
|
1340
|
+
| Method | Description |
|
|
1341
|
+
| ----------------------------- | ----------------------------------------------------------------- |
|
|
1342
|
+
| `insert(data)` | INSERT + RETURNING \*, returns the inserted row |
|
|
1343
|
+
| `insertMany(data)` | Bulk INSERT + RETURNING \*, returns rows |
|
|
1344
|
+
| `read(id, opts?)` | SELECT by detected primary key + auto soft-delete filter |
|
|
1345
|
+
| `readMany(where?, opts?)` | Filtered query with `{ count, data }` — auto-filters soft-deleted |
|
|
1346
|
+
| `update(id, data)` | UPDATE by primary key + RETURNING \*, returns updated row |
|
|
1347
|
+
| `updateMany(where, data)` | Bulk UPDATE, returns affected row count |
|
|
1348
|
+
| `delete(id)` | Soft delete if `deleted_at` exists, else hard delete |
|
|
1349
|
+
| `hardDelete(id)` | Always DELETE FROM |
|
|
1350
|
+
| `deleteMany(where)` | Soft bulk delete if `deleted_at` exists |
|
|
1351
|
+
| `hardDeleteMany(where)` | Always DELETE FROM |
|
|
1352
|
+
| `upsert(data, conflict)` | INSERT ON CONFLICT DO UPDATE, returns row |
|
|
1353
|
+
| `count(where?)` | SELECT COUNT(\*) — auto-filters soft-deleted |
|
|
1354
|
+
| `create(opts?)` | CREATE TABLE IF NOT EXISTS |
|
|
1355
|
+
| `drop(opts?)` | DROP TABLE IF EXISTS |
|
|
1356
|
+
| `createIndex(columns, opts?)` | CREATE INDEX |
|
|
1357
|
+
| `createUniqueIndex(columns)` | CREATE UNIQUE INDEX |
|
|
1358
|
+
| `withSql(sql)` | Returns copy bound to a different sql (for transactions) |
|
|
1250
1359
|
|
|
1251
1360
|
**Where helpers** — composable query conditions:
|
|
1252
1361
|
|
|
1253
|
-
| Helper
|
|
1254
|
-
|
|
1255
|
-
| `eq(col, val)`
|
|
1256
|
-
| `ne(col, val)`
|
|
1257
|
-
| `gt` / `gte` / `lt` / `lte`
|
|
1258
|
-
| `isNull(col)` / `isNotNull(col)`
|
|
1259
|
-
| `like(col, pattern)`
|
|
1260
|
-
| `contains(col, val)`
|
|
1261
|
-
| `in_(col, vals)`
|
|
1262
|
-
| `and(...)` / `or(...)` / `not(...)` | Boolean composition
|
|
1362
|
+
| Helper | SQL |
|
|
1363
|
+
| ----------------------------------- | ------------------------- |
|
|
1364
|
+
| `eq(col, val)` | `"col" = val` |
|
|
1365
|
+
| `ne(col, val)` | `"col" != val` |
|
|
1366
|
+
| `gt` / `gte` / `lt` / `lte` | Comparison operators |
|
|
1367
|
+
| `isNull(col)` / `isNotNull(col)` | `IS NULL` / `IS NOT NULL` |
|
|
1368
|
+
| `like(col, pattern)` | `LIKE` |
|
|
1369
|
+
| `contains(col, val)` | `@>` JSONB containment |
|
|
1370
|
+
| `in_(col, vals)` | `= ANY(...)` |
|
|
1371
|
+
| `and(...)` / `or(...)` / `not(...)` | Boolean composition |
|
|
1263
1372
|
|
|
1264
1373
|
**PgModule** — base class for modules that need DB access:
|
|
1265
1374
|
|
|
1266
1375
|
```ts
|
|
1267
1376
|
class MyModule extends PgModule {
|
|
1268
|
-
async migrate() {
|
|
1269
|
-
|
|
1377
|
+
async migrate() {
|
|
1378
|
+
/* run DDL */
|
|
1379
|
+
}
|
|
1380
|
+
async getUsers() {
|
|
1381
|
+
return this.table('users', {}).readMany()
|
|
1382
|
+
}
|
|
1270
1383
|
}
|
|
1271
1384
|
```
|
|
1272
1385
|
|
|
@@ -1284,10 +1397,10 @@ const next = cronNext('0 9 * * 1-5')
|
|
|
1284
1397
|
console.log(new Date(next))
|
|
1285
1398
|
```
|
|
1286
1399
|
|
|
1287
|
-
| Function
|
|
1288
|
-
|
|
1289
|
-
| `parsePattern(pattern)` | Parse 5-field cron pattern into `Set<number>[]`
|
|
1290
|
-
| `matches(fields, date)` | Check if a date matches a parsed pattern
|
|
1400
|
+
| Function | Description |
|
|
1401
|
+
| ----------------------- | ---------------------------------------------------------- |
|
|
1402
|
+
| `parsePattern(pattern)` | Parse 5-field cron pattern into `Set<number>[]` |
|
|
1403
|
+
| `matches(fields, date)` | Check if a date matches a parsed pattern |
|
|
1291
1404
|
| `cronNext(expr, from?)` | Calculate next matching timestamp (`from` defaults to now) |
|
|
1292
1405
|
|
|
1293
1406
|
### fts — Full-Text Search (PostgreSQL) [Database]
|
|
@@ -1318,11 +1431,11 @@ const results = await fts.search(pg.sql, articles, 'node.js framework', {
|
|
|
1318
1431
|
await fts.dropIndex(pg.sql, articles)
|
|
1319
1432
|
```
|
|
1320
1433
|
|
|
1321
|
-
| Function
|
|
1322
|
-
|
|
1434
|
+
| Function | Description |
|
|
1435
|
+
| ---------------------------------------- | ------------------------------ |
|
|
1323
1436
|
| `createIndex(sql, table, fields, opts?)` | Create GIN/GiST tsvector index |
|
|
1324
|
-
| `search(sql, table, query, opts?)`
|
|
1325
|
-
| `dropIndex(sql, table, opts?)`
|
|
1437
|
+
| `search(sql, table, query, opts?)` | Search with ts_rank ordering |
|
|
1438
|
+
| `dropIndex(sql, table, opts?)` | Drop the index |
|
|
1326
1439
|
|
|
1327
1440
|
Search options: `fields`, `limit` (20), `offset` (0), `headline` (false), `language` ('english'), `minRank`.
|
|
1328
1441
|
|
|
@@ -1342,10 +1455,10 @@ app.use(theme({ default: 'dark' }))
|
|
|
1342
1455
|
// app.use('/', t)
|
|
1343
1456
|
```
|
|
1344
1457
|
|
|
1345
|
-
| Option
|
|
1346
|
-
|
|
1347
|
-
| `default` | `string` | `'system'` | Default theme
|
|
1348
|
-
| `cookie`
|
|
1458
|
+
| Option | Type | Default | Description |
|
|
1459
|
+
| --------- | -------- | ---------- | ------------------------------ |
|
|
1460
|
+
| `default` | `string` | `'system'` | Default theme |
|
|
1461
|
+
| `cookie` | `string` | `'theme'` | Cookie name (empty to disable) |
|
|
1349
1462
|
|
|
1350
1463
|
```ts
|
|
1351
1464
|
// Server-side switching
|
|
@@ -1373,15 +1486,14 @@ app.use(i18n({ default: 'zh', dir: './locales' }))
|
|
|
1373
1486
|
// app.use(l.middleware())
|
|
1374
1487
|
// app.use('/', l)
|
|
1375
1488
|
```
|
|
1376
|
-
```
|
|
1377
1489
|
|
|
1378
|
-
| Option
|
|
1379
|
-
|
|
1380
|
-
| `default`
|
|
1381
|
-
| `dir`
|
|
1382
|
-
| `messages`
|
|
1383
|
-
| `cookie`
|
|
1384
|
-
| `fromAcceptLanguage` | `boolean` | `true`
|
|
1490
|
+
| Option | Type | Default | Description |
|
|
1491
|
+
| -------------------- | --------- | ---------- | -------------------------------------------------- |
|
|
1492
|
+
| `default` | `string` | `'en'` | Default locale |
|
|
1493
|
+
| `dir` | `string` | — | Directory with `{locale}.json` files |
|
|
1494
|
+
| `messages` | `object` | — | Inline translations: `{ zh: { welcome: '欢迎' } }` |
|
|
1495
|
+
| `cookie` | `string` | `'locale'` | Cookie name (empty to disable) |
|
|
1496
|
+
| `fromAcceptLanguage` | `boolean` | `true` | Detect from Accept-Language header |
|
|
1385
1497
|
|
|
1386
1498
|
```ts
|
|
1387
1499
|
// Handler
|
|
@@ -1408,7 +1520,9 @@ const q = queue({ store: 'memory' })
|
|
|
1408
1520
|
// const q = queue({ store: 'redis', redis })
|
|
1409
1521
|
|
|
1410
1522
|
// Register cron job (uses the same backend for persistence)
|
|
1411
|
-
q.cron('*/5 * * * *', async () => {
|
|
1523
|
+
q.cron('*/5 * * * *', async () => {
|
|
1524
|
+
await cleanCache()
|
|
1525
|
+
})
|
|
1412
1526
|
|
|
1413
1527
|
// Or use process/add for full queue semantics
|
|
1414
1528
|
q.process('send-email', async (job) => {
|
|
@@ -1427,55 +1541,55 @@ q.add('weekly-report', {}, { schedule: '0 9 * * 1' })
|
|
|
1427
1541
|
q.run()
|
|
1428
1542
|
```
|
|
1429
1543
|
|
|
1430
|
-
| Option
|
|
1431
|
-
|
|
1432
|
-
| `store`
|
|
1433
|
-
| `redis`
|
|
1434
|
-
| `url`
|
|
1435
|
-
| `pg`
|
|
1436
|
-
| `prefix`
|
|
1437
|
-
| `pollInterval` | `number`
|
|
1438
|
-
|
|
1439
|
-
| Method
|
|
1440
|
-
|
|
1441
|
-
| `.cron(pattern, handler)`
|
|
1442
|
-
| `.add(type, payload, opts?)` | Add job (opts: `delay`, `schedule`)
|
|
1443
|
-
| `.process(type, handler)`
|
|
1444
|
-
| `.run()`
|
|
1445
|
-
| `.stop()`
|
|
1446
|
-
| `.jobs(limit?)`
|
|
1447
|
-
| `.failedJobs(limit?)`
|
|
1448
|
-
| `.retryFailed(jobId)`
|
|
1449
|
-
| `.retryAllFailed(type?)`
|
|
1450
|
-
| `.dashboard()`
|
|
1451
|
-
| `.close()`
|
|
1544
|
+
| Option | Type | Default | Description |
|
|
1545
|
+
| -------------- | ----------------------------- | ---------- | ----------------------------------------------- |
|
|
1546
|
+
| `store` | `'memory' \| 'pg' \| 'redis'` | `'memory'` | Backend store |
|
|
1547
|
+
| `redis` | `object` | — | Redis client (required when `store: 'redis'`) |
|
|
1548
|
+
| `url` | `string` | — | Redis URL (alternative to client) |
|
|
1549
|
+
| `pg` | `object` | — | PostgreSQL client (required when `store: 'pg'`) |
|
|
1550
|
+
| `prefix` | `string` | `'queue'` | Key/table prefix |
|
|
1551
|
+
| `pollInterval` | `number` | `200` | Poll interval (ms) |
|
|
1552
|
+
|
|
1553
|
+
| Method | Description |
|
|
1554
|
+
| ---------------------------- | --------------------------------------------------- |
|
|
1555
|
+
| `.cron(pattern, handler)` | Register a cron job (uses process + add internally) |
|
|
1556
|
+
| `.add(type, payload, opts?)` | Add job (opts: `delay`, `schedule`) |
|
|
1557
|
+
| `.process(type, handler)` | Register job processor |
|
|
1558
|
+
| `.run()` | Start processing |
|
|
1559
|
+
| `.stop()` | Stop processing |
|
|
1560
|
+
| `.jobs(limit?)` | List pending jobs |
|
|
1561
|
+
| `.failedJobs(limit?)` | List failed jobs with error messages |
|
|
1562
|
+
| `.retryFailed(jobId)` | Retry a specific failed job |
|
|
1563
|
+
| `.retryAllFailed(type?)` | Retry all failed jobs (optionally by type) |
|
|
1564
|
+
| `.dashboard()` | Returns a Router with management endpoints |
|
|
1565
|
+
| `.close()` | Cleanup |
|
|
1452
1566
|
|
|
1453
1567
|
**Schedule (cron) field reference:**
|
|
1454
1568
|
|
|
1455
|
-
| Field
|
|
1456
|
-
|
|
1457
|
-
| minute
|
|
1458
|
-
| hour
|
|
1459
|
-
| day of month | 1–31
|
|
1460
|
-
| month
|
|
1461
|
-
| day of week
|
|
1569
|
+
| Field | Range |
|
|
1570
|
+
| ------------ | -------------- |
|
|
1571
|
+
| minute | 0–59 |
|
|
1572
|
+
| hour | 0–23 |
|
|
1573
|
+
| day of month | 1–31 |
|
|
1574
|
+
| month | 1–12 |
|
|
1575
|
+
| day of week | 0–6 (0=Sunday) |
|
|
1462
1576
|
|
|
1463
1577
|
Supported cron syntax: `*` (any), `*/n` (every n), `n-m` (range), `n,m,o` (list), `n` (exact).
|
|
1464
1578
|
|
|
1465
1579
|
**Dashboard endpoints** (mount via `app.use('/__queue', q.dashboard())`):
|
|
1466
1580
|
|
|
1467
|
-
| Method | Path
|
|
1468
|
-
|
|
1469
|
-
| GET
|
|
1470
|
-
| GET
|
|
1471
|
-
| POST
|
|
1472
|
-
| POST
|
|
1581
|
+
| Method | Path | Description |
|
|
1582
|
+
| ------ | --------------- | ------------------------------------------- |
|
|
1583
|
+
| GET | `/` | Queue stats + pending/failed counts by type |
|
|
1584
|
+
| GET | `/:type/failed` | List failed jobs for a type |
|
|
1585
|
+
| POST | `/:type/retry` | Retry all failed jobs of a type |
|
|
1586
|
+
| POST | `/retry/:id` | Retry a specific failed job by ID |
|
|
1473
1587
|
|
|
1474
1588
|
### rateLimit [α] [Security]
|
|
1475
1589
|
|
|
1476
1590
|
```ts
|
|
1477
|
-
app.use(rateLimit({ max: 100, window: 60_000 }))
|
|
1478
|
-
app.get('/api', rateLimit({ max: 10 }), handler)
|
|
1591
|
+
app.use(rateLimit({ max: 100, window: 60_000 })) // 100 req/min, in-memory
|
|
1592
|
+
app.get('/api', rateLimit({ max: 10 }), handler) // per-route
|
|
1479
1593
|
app.use(rateLimit({ key: (req) => req.headers.get('x-api-key') ?? 'anonymous' }))
|
|
1480
1594
|
|
|
1481
1595
|
// Multi-process: Redis-backed rate limiting
|
|
@@ -1485,31 +1599,31 @@ app.use(rateLimit({ max: 100, store: 'redis', redis: ctx.redis }))
|
|
|
1485
1599
|
// m.stop() — clear interval (memory) or Redis cleanup
|
|
1486
1600
|
```
|
|
1487
1601
|
|
|
1488
|
-
| Option
|
|
1489
|
-
|
|
1490
|
-
| `max`
|
|
1491
|
-
| `window`
|
|
1492
|
-
| `key`
|
|
1493
|
-
| `message` | `string`
|
|
1494
|
-
| `store`
|
|
1495
|
-
| `redis`
|
|
1496
|
-
| `prefix`
|
|
1602
|
+
| Option | Type | Default | Description |
|
|
1603
|
+
| --------- | --------------------- | --------------------- | --------------------------------------------- |
|
|
1604
|
+
| `max` | `number` | `100` | Max requests per window |
|
|
1605
|
+
| `window` | `number` | `60_000` | Window duration (ms) |
|
|
1606
|
+
| `key` | `(req) => string` | IP-based | Key function |
|
|
1607
|
+
| `message` | `string` | `'Too Many Requests'` | 429 response body |
|
|
1608
|
+
| `store` | `'memory' \| 'redis'` | `'memory'` | Backend store |
|
|
1609
|
+
| `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
|
|
1610
|
+
| `prefix` | `string` | `'ratelimit:'` | Redis key prefix |
|
|
1497
1611
|
|
|
1498
1612
|
Redis mode uses `INCR` + `EXPIRE` for atomic counting, enabling accurate rate limiting across multiple server processes. Memory mode is ideal for single-process deployments.
|
|
1499
1613
|
|
|
1500
1614
|
### redis [α] [Database]
|
|
1501
1615
|
|
|
1502
1616
|
```ts
|
|
1503
|
-
const r = redis()
|
|
1504
|
-
app.use(r)
|
|
1617
|
+
const r = redis() // reads REDIS_URL
|
|
1618
|
+
app.use(r) // injects ctx.redis
|
|
1505
1619
|
await ctx.redis.set('key', 'value')
|
|
1506
1620
|
// r.close() — cleanup
|
|
1507
1621
|
```
|
|
1508
1622
|
|
|
1509
|
-
| Option
|
|
1510
|
-
|
|
1511
|
-
| `url`
|
|
1512
|
-
| (all ioredis options) | —
|
|
1623
|
+
| Option | Type | Default | Description |
|
|
1624
|
+
| --------------------- | -------- | --------------- | -------------------------- |
|
|
1625
|
+
| `url` | `string` | `REDIS_URL` env | Redis connection string |
|
|
1626
|
+
| (all ioredis options) | — | — | Passed directly to ioredis |
|
|
1513
1627
|
|
|
1514
1628
|
### requestId [α] [DevTools]
|
|
1515
1629
|
|
|
@@ -1519,46 +1633,67 @@ app.use(requestId({ header: 'X-Request-Id', generator: () => crypto.randomUUID()
|
|
|
1519
1633
|
// Sets X-Request-ID header on responses, available as ctx.requestId
|
|
1520
1634
|
```
|
|
1521
1635
|
|
|
1522
|
-
| Option
|
|
1523
|
-
|
|
1524
|
-
| `header`
|
|
1525
|
-
| `generator` | `() => string` | `crypto.randomUUID()` | ID generator
|
|
1636
|
+
| Option | Type | Default | Description |
|
|
1637
|
+
| ----------- | -------------- | --------------------- | ------------------------- |
|
|
1638
|
+
| `header` | `string` | `'X-Request-ID'` | Header name to read/write |
|
|
1639
|
+
| `generator` | `() => string` | `crypto.randomUUID()` | ID generator |
|
|
1640
|
+
|
|
1641
|
+
### trace [α] [DevTools]
|
|
1526
1642
|
|
|
1527
|
-
|
|
1643
|
+
Request-scoped tracing via `AsyncLocalStorage`. Use as middleware to inject `ctx.trace`:
|
|
1528
1644
|
|
|
1529
|
-
|
|
1645
|
+
```ts
|
|
1646
|
+
import { trace } from 'weifuwu'
|
|
1647
|
+
app.use(trace()) // → ctx.trace
|
|
1648
|
+
app.use(trace({ header: 'X-Trace-Id' })) // custom header
|
|
1649
|
+
|
|
1650
|
+
app.get('/', (req, ctx) => {
|
|
1651
|
+
console.log(ctx.trace.requestId) // 550e8400-e29b-...
|
|
1652
|
+
console.log(ctx.trace.traceId) // trace UUID
|
|
1653
|
+
console.log(ctx.trace.elapsed()) // ms since request start
|
|
1654
|
+
})
|
|
1655
|
+
```
|
|
1656
|
+
|
|
1657
|
+
| Option | Type | Default | Description |
|
|
1658
|
+
| ----------- | -------------- | --------------------- | ---------------------- |
|
|
1659
|
+
| `header` | `string` | `'X-Request-ID'` | Request ID header name |
|
|
1660
|
+
| `generator` | `() => string` | `crypto.randomUUID()` | Custom ID generator |
|
|
1661
|
+
|
|
1662
|
+
Utility functions (also available standalone):
|
|
1530
1663
|
|
|
1531
1664
|
```ts
|
|
1532
|
-
import { currentTraceId, runWithTrace, traceElapsed } from 'weifuwu'
|
|
1665
|
+
import { currentTraceId, runWithTrace, traceElapsed, currentTrace } from 'weifuwu'
|
|
1533
1666
|
|
|
1534
|
-
// Inside a middleware or handler
|
|
1535
1667
|
const traceId = currentTraceId() // UUID or incoming X-Trace-Id
|
|
1536
1668
|
const elapsed = traceElapsed() // ms since request started
|
|
1669
|
+
runWithTrace(incomingId, () => { ... }) // manual scope
|
|
1537
1670
|
```
|
|
1538
1671
|
|
|
1539
|
-
| Function
|
|
1540
|
-
|
|
1541
|
-
| `currentTraceId()`
|
|
1542
|
-
| `currentTrace()`
|
|
1543
|
-
| `traceElapsed()`
|
|
1544
|
-
| `runWithTrace(traceId, fn)` | Execute `fn` inside a trace scope
|
|
1672
|
+
| Function | Description |
|
|
1673
|
+
| --------------------------- | ---------------------------------------------------------- |
|
|
1674
|
+
| `currentTraceId()` | Current request trace ID, or `undefined` outside a request |
|
|
1675
|
+
| `currentTrace()` | Full `{ traceId, startTime }` context |
|
|
1676
|
+
| `traceElapsed()` | Milliseconds elapsed since the trace started |
|
|
1677
|
+
| `runWithTrace(traceId, fn)` | Execute `fn` inside a trace scope |
|
|
1545
1678
|
|
|
1546
1679
|
### s3 [α] — S3-compatible object storage [Networking]
|
|
1547
1680
|
|
|
1548
1681
|
```ts
|
|
1549
1682
|
import { s3 } from 'weifuwu'
|
|
1550
1683
|
|
|
1551
|
-
app.use(
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1684
|
+
app.use(
|
|
1685
|
+
s3({
|
|
1686
|
+
bucket: 'my-app',
|
|
1687
|
+
region: 'us-east-1',
|
|
1688
|
+
endpoint: process.env.S3_URL, // MinIO / R2 / AWS
|
|
1689
|
+
forcePathStyle: true, // required for MinIO
|
|
1690
|
+
credentials: {
|
|
1691
|
+
accessKeyId: process.env.S3_ACCESS_KEY,
|
|
1692
|
+
secretAccessKey: process.env.S3_SECRET_KEY,
|
|
1693
|
+
},
|
|
1694
|
+
publicUrl: 'https://cdn.example.com', // for unsigned public URLs
|
|
1695
|
+
}),
|
|
1696
|
+
)
|
|
1562
1697
|
```
|
|
1563
1698
|
|
|
1564
1699
|
Injects `ctx.s3` with methods for S3-compatible object storage.
|
|
@@ -1587,14 +1722,14 @@ const publicUrl = await ctx.s3.url('images/logo.png', { expiresIn: 0 })
|
|
|
1587
1722
|
const keys = await ctx.s3.list('images/')
|
|
1588
1723
|
```
|
|
1589
1724
|
|
|
1590
|
-
| Option
|
|
1591
|
-
|
|
1592
|
-
| `bucket`
|
|
1593
|
-
| `region`
|
|
1594
|
-
| `endpoint`
|
|
1595
|
-
| `forcePathStyle` | `boolean`
|
|
1596
|
-
| `credentials`
|
|
1597
|
-
| `publicUrl`
|
|
1725
|
+
| Option | Type | Default | Description |
|
|
1726
|
+
| ---------------- | ---------------------------------- | ------------- | ------------------------------------------------------------------ |
|
|
1727
|
+
| `bucket` | `string` | — | **Required.** S3 bucket name |
|
|
1728
|
+
| `region` | `string` | `'us-east-1'` | AWS region |
|
|
1729
|
+
| `endpoint` | `string` | — | Custom endpoint (MinIO, R2, B2) |
|
|
1730
|
+
| `forcePathStyle` | `boolean` | `false` | Path-style addressing (required for MinIO) |
|
|
1731
|
+
| `credentials` | `{ accessKeyId, secretAccessKey }` | — | Falls back to AWS env vars / IAM role |
|
|
1732
|
+
| `publicUrl` | `string` | — | Base URL for unsigned public URLs via `url(key, { expiresIn: 0 })` |
|
|
1598
1733
|
|
|
1599
1734
|
Credentials can be omitted to use AWS environment variables (`AWS_ACCESS_KEY_ID`,
|
|
1600
1735
|
`AWS_SECRET_ACCESS_KEY`) or IAM roles (EC2, ECS, Lambda).
|
|
@@ -1621,32 +1756,42 @@ minio:
|
|
|
1621
1756
|
command: server /data
|
|
1622
1757
|
```
|
|
1623
1758
|
|
|
1624
|
-
|
|
1625
1759
|
### seo [β] + seoMiddleware [α] [API]
|
|
1626
1760
|
|
|
1627
1761
|
```ts
|
|
1628
|
-
app.use(
|
|
1762
|
+
app.use(
|
|
1763
|
+
'/',
|
|
1764
|
+
seo({
|
|
1765
|
+
baseUrl: 'https://example.com',
|
|
1766
|
+
robots: [{ userAgent: '*', allow: '/' }],
|
|
1767
|
+
sitemap: { urls: [{ loc: '/' }] },
|
|
1768
|
+
}),
|
|
1769
|
+
)
|
|
1629
1770
|
// GET /robots.txt, GET /sitemap.xml
|
|
1630
1771
|
|
|
1631
|
-
app.use(
|
|
1772
|
+
app.use(
|
|
1773
|
+
seoMiddleware({
|
|
1774
|
+
headers: { 'X-Robots-Tag': (path) => (path.startsWith('/admin') ? 'noindex' : undefined) },
|
|
1775
|
+
}),
|
|
1776
|
+
)
|
|
1632
1777
|
```
|
|
1633
1778
|
|
|
1634
1779
|
Also exports `seoTags(config)` for generating meta/og/twitter tags as an HTML string.
|
|
1635
1780
|
|
|
1636
|
-
| Option
|
|
1637
|
-
|
|
1638
|
-
| `baseUrl` | `string`
|
|
1639
|
-
| `robots`
|
|
1640
|
-
| `sitemap` | `SitemapConfig`
|
|
1641
|
-
| `headers` | `SeoHeadersConfig` | —
|
|
1781
|
+
| Option | Type | Default | Description |
|
|
1782
|
+
| --------- | ------------------ | ---------------------------------- | ----------------------------------------------- |
|
|
1783
|
+
| `baseUrl` | `string` | — | Base URL for sitemap URLs |
|
|
1784
|
+
| `robots` | `RobotsRule[]` | `[{ userAgent: '*', allow: '/' }]` | Robots.txt rules |
|
|
1785
|
+
| `sitemap` | `SitemapConfig` | — | Sitemap configuration (urls, resolve, cacheTTL) |
|
|
1786
|
+
| `headers` | `SeoHeadersConfig` | — | Response headers (e.g. `X-Robots-Tag`) |
|
|
1642
1787
|
|
|
1643
1788
|
### session [α] [Security]
|
|
1644
1789
|
|
|
1645
1790
|
Cookie-based server-side session management with memory and Redis stores.
|
|
1646
1791
|
|
|
1647
1792
|
```ts
|
|
1648
|
-
app.use(session())
|
|
1649
|
-
app.use(session({ store: 'redis', redis: ctx.redis }))
|
|
1793
|
+
app.use(session()) // in-memory store (default)
|
|
1794
|
+
app.use(session({ store: 'redis', redis: ctx.redis })) // Redis store
|
|
1650
1795
|
app.use(session({ store: 'redis', redis, ttl: 30 * 60_000, cookieName: 'sid' }))
|
|
1651
1796
|
|
|
1652
1797
|
app.get('/login', async (req, ctx) => {
|
|
@@ -1657,7 +1802,7 @@ app.get('/login', async (req, ctx) => {
|
|
|
1657
1802
|
})
|
|
1658
1803
|
|
|
1659
1804
|
app.get('/logout', async (req, ctx) => {
|
|
1660
|
-
ctx.session.destroy()
|
|
1805
|
+
ctx.session.destroy() // or ctx.session = null
|
|
1661
1806
|
return Response.json({ ok: true })
|
|
1662
1807
|
})
|
|
1663
1808
|
|
|
@@ -1667,19 +1812,19 @@ app.get('/logout', async (req, ctx) => {
|
|
|
1667
1812
|
// Session mutations are auto-detected on property set/delete
|
|
1668
1813
|
```
|
|
1669
1814
|
|
|
1670
|
-
| Option
|
|
1671
|
-
|
|
1672
|
-
| `store`
|
|
1673
|
-
| `redis`
|
|
1674
|
-
| `ttl`
|
|
1675
|
-
| `cookieName`
|
|
1676
|
-
| `cookie.httpOnly` | `boolean`
|
|
1677
|
-
| `cookie.secure`
|
|
1678
|
-
| `cookie.sameSite` | `string`
|
|
1679
|
-
| `cookie.path`
|
|
1680
|
-
| `cookie.domain`
|
|
1681
|
-
| `secret`
|
|
1682
|
-
| `rotateInterval`
|
|
1815
|
+
| Option | Type | Default | Description |
|
|
1816
|
+
| ----------------- | ------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
1817
|
+
| `store` | `'memory' \| 'redis' \| SessionStore` | `'memory'` | Session store backend |
|
|
1818
|
+
| `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
|
|
1819
|
+
| `ttl` | `number` | `86400000` (24h) | Session TTL in ms |
|
|
1820
|
+
| `cookieName` | `string` | `'__session'` | Cookie name |
|
|
1821
|
+
| `cookie.httpOnly` | `boolean` | `true` | Cookie httpOnly flag |
|
|
1822
|
+
| `cookie.secure` | `boolean` | `auto` | Cookie Secure flag (true in production) |
|
|
1823
|
+
| `cookie.sameSite` | `string` | `'lax'` | SameSite policy |
|
|
1824
|
+
| `cookie.path` | `string` | `'/'` | Cookie path |
|
|
1825
|
+
| `cookie.domain` | `string` | — | Cookie domain |
|
|
1826
|
+
| `secret` | `string` | — | HMAC-SHA256 sign the session cookie (`uuid.signature`). Prevents tampering **strongly recommended in production** |
|
|
1827
|
+
| `rotateInterval` | `number` | `900000` (15min) | Auto-rotate session ID to prevent fixation attacks. Set `0` to disable |
|
|
1683
1828
|
|
|
1684
1829
|
When `secret` is set, the cookie value is signed with HMAC-SHA256:
|
|
1685
1830
|
`uuid.base64url(hmac)`. Tampered cookies are rejected and treated as new
|
|
@@ -1694,7 +1839,7 @@ from the store. Rotation happens transparently on the next request after
|
|
|
1694
1839
|
```ts
|
|
1695
1840
|
import { MemoryStore, RedisStore } from 'weifuwu'
|
|
1696
1841
|
|
|
1697
|
-
const mem = new MemoryStore()
|
|
1842
|
+
const mem = new MemoryStore() // auto-cleanup every 60s
|
|
1698
1843
|
await mem.set('sid', { userId: 1 }, 86400000)
|
|
1699
1844
|
mem.close()
|
|
1700
1845
|
|
|
@@ -1733,14 +1878,14 @@ app.use('/', ssr({ dir: './ui' }))
|
|
|
1733
1878
|
└── lib/ ← utilities (does not affect routing)
|
|
1734
1879
|
```
|
|
1735
1880
|
|
|
1736
|
-
| Location
|
|
1737
|
-
|
|
1738
|
-
| `app/page.tsx`
|
|
1739
|
-
| `app/[param]/page.tsx` | `GET /:param`
|
|
1740
|
-
| `app/layout.tsx`
|
|
1741
|
-
| `app/not-found.tsx`
|
|
1742
|
-
| `app/error.tsx`
|
|
1743
|
-
| `app/globals.css`
|
|
1881
|
+
| Location | Route |
|
|
1882
|
+
| ---------------------- | -------------------------------------------------------- |
|
|
1883
|
+
| `app/page.tsx` | `GET /` |
|
|
1884
|
+
| `app/[param]/page.tsx` | `GET /:param` |
|
|
1885
|
+
| `app/layout.tsx` | Root layout (wraps all pages in its subtree) |
|
|
1886
|
+
| `app/not-found.tsx` | 404 fallback for that subtree |
|
|
1887
|
+
| `app/error.tsx` | Error boundary for that subtree |
|
|
1888
|
+
| `app/globals.css` | Tailwind CSS entry (compiled via `@tailwindcss/postcss`) |
|
|
1744
1889
|
|
|
1745
1890
|
**How hydration works:**
|
|
1746
1891
|
|
|
@@ -1751,13 +1896,13 @@ app.use('/', ssr({ dir: './ui' }))
|
|
|
1751
1896
|
- The vendor bundle (react + react-dom + weifuwu client libs) is compiled once and cached
|
|
1752
1897
|
- Page components are pre-compiled to `/__ssr/{hash}.js` — no runtime esbuild after first request
|
|
1753
1898
|
- **Dev:** `createRoot` + render; **Production:** `hydrateRoot` (reuses SSR DOM)
|
|
1754
|
-
-
|
|
1899
|
+
- The hydration script and page component share the same SSR context store — data flows seamlessly from server to client
|
|
1755
1900
|
- Tailwind CSS served at `/__wfw/style/{hash}.css` (cached, content-hashed)
|
|
1756
1901
|
- Dev mode extras: HMR WebSocket, file watcher, hot component replacement
|
|
1757
1902
|
|
|
1758
1903
|
```ts
|
|
1759
1904
|
// Multiple independent SSR directories
|
|
1760
|
-
app.use('/',
|
|
1905
|
+
app.use('/', ssr({ dir: './www' }))
|
|
1761
1906
|
app.use('/admin', ssr({ dir: './admin' }))
|
|
1762
1907
|
|
|
1763
1908
|
// API routes coexist normally
|
|
@@ -1773,31 +1918,35 @@ Multi-tenant BaaS with dynamic table API and GraphQL.
|
|
|
1773
1918
|
```ts
|
|
1774
1919
|
const t = tenant({ pg, usersTable: '_users' })
|
|
1775
1920
|
await t.migrate()
|
|
1776
|
-
app.use('/api', t.middleware())
|
|
1777
|
-
app.use('/api', t)
|
|
1921
|
+
app.use('/api', t.middleware()) // → ctx.tenant
|
|
1922
|
+
app.use('/api', t) // dynamic CRUD
|
|
1778
1923
|
app.use('/graphql', t.graphql()) // dynamic GraphQL
|
|
1779
1924
|
```
|
|
1780
1925
|
|
|
1781
|
-
| Option
|
|
1782
|
-
|
|
1783
|
-
| `pg`
|
|
1784
|
-
| `usersTable` | `string` | —
|
|
1926
|
+
| Option | Type | Default | Description |
|
|
1927
|
+
| ------------ | -------- | ------- | --------------------------------------------- |
|
|
1928
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
1929
|
+
| `usersTable` | `string` | — | Users table name for tenant membership lookup |
|
|
1785
1930
|
|
|
1786
1931
|
### upload [α] [DevTools]
|
|
1787
1932
|
|
|
1788
1933
|
```ts
|
|
1789
|
-
app.post(
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
})
|
|
1934
|
+
app.post(
|
|
1935
|
+
'/upload',
|
|
1936
|
+
upload({ dir: './uploads', maxFileSize: 10_485_760, allowedTypes: ['image/jpeg', 'image/png'] }),
|
|
1937
|
+
(req, ctx) => {
|
|
1938
|
+
// ctx.parsed.files.avatar → { name, type, size, path } or { name, type, size, buffer } (when no dir)
|
|
1939
|
+
// Multiple files with same field name → array
|
|
1940
|
+
// ctx.parsed.fields.title → 'hello'
|
|
1941
|
+
},
|
|
1942
|
+
)
|
|
1794
1943
|
```
|
|
1795
1944
|
|
|
1796
|
-
| Option
|
|
1797
|
-
|
|
1798
|
-
| `dir`
|
|
1799
|
-
| `maxFileSize`
|
|
1800
|
-
| `allowedTypes` | `string[]` | —
|
|
1945
|
+
| Option | Type | Default | Description |
|
|
1946
|
+
| -------------- | ---------- | ------- | ---------------------------------------- |
|
|
1947
|
+
| `dir` | `string` | — | Write files to disk (omit for in-memory) |
|
|
1948
|
+
| `maxFileSize` | `number` | — | Max bytes per file |
|
|
1949
|
+
| `allowedTypes` | `string[]` | — | Allowed MIME types |
|
|
1801
1950
|
|
|
1802
1951
|
### user [β] [Security]
|
|
1803
1952
|
|
|
@@ -1807,7 +1956,8 @@ Authentication: register, login, JWT, OAuth2 服务端, 社会化登录.
|
|
|
1807
1956
|
const u = user({
|
|
1808
1957
|
pg,
|
|
1809
1958
|
jwtSecret: process.env.JWT_SECRET!,
|
|
1810
|
-
oauthLogin: {
|
|
1959
|
+
oauthLogin: {
|
|
1960
|
+
// 可选 — 社会化登录
|
|
1811
1961
|
providers: {
|
|
1812
1962
|
github: { clientId: '...', clientSecret: '...' },
|
|
1813
1963
|
google: { clientId: '...', clientSecret: '...' },
|
|
@@ -1815,26 +1965,26 @@ const u = user({
|
|
|
1815
1965
|
},
|
|
1816
1966
|
})
|
|
1817
1967
|
await u.migrate()
|
|
1818
|
-
app.use(u)
|
|
1819
|
-
app.use(u.middleware())
|
|
1968
|
+
app.use(u) // POST /register, POST /login
|
|
1969
|
+
app.use(u.middleware()) // ctx.user
|
|
1820
1970
|
// GET /auth/github, GET /auth/github/callback (如配置 oauthLogin)
|
|
1821
1971
|
```
|
|
1822
1972
|
|
|
1823
|
-
| Option
|
|
1824
|
-
|
|
1825
|
-
| `pg`
|
|
1826
|
-
| `jwtSecret`
|
|
1827
|
-
| `table`
|
|
1828
|
-
| `expiresIn`
|
|
1829
|
-
| `oauth2`
|
|
1830
|
-
| `oauthLogin` | `object` | —
|
|
1973
|
+
| Option | Type | Default | Description |
|
|
1974
|
+
| ------------ | -------- | ---------- | ------------------------------------------------------------------------------ |
|
|
1975
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
1976
|
+
| `jwtSecret` | `string` | — | JWT signing secret |
|
|
1977
|
+
| `table` | `string` | `'_users'` | Users table name |
|
|
1978
|
+
| `expiresIn` | `string` | `'24h'` | JWT expiration |
|
|
1979
|
+
| `oauth2` | `object` | — | OAuth2 服务端 config (PKCE flow) |
|
|
1980
|
+
| `oauthLogin` | `object` | — | 社会化登录: `{ providers: Record<string, OAuthProviderConfig>, redirectUrl? }` |
|
|
1831
1981
|
|
|
1832
|
-
| Method
|
|
1833
|
-
|
|
1834
|
-
| `.register(data)` | Register a new user programmatically
|
|
1835
|
-
| `.login(data)`
|
|
1836
|
-
| `.verify(token)`
|
|
1837
|
-
| `.middleware()`
|
|
1982
|
+
| Method | Description |
|
|
1983
|
+
| ----------------- | --------------------------------------- |
|
|
1984
|
+
| `.register(data)` | Register a new user programmatically |
|
|
1985
|
+
| `.login(data)` | Log in programmatically |
|
|
1986
|
+
| `.verify(token)` | Verify JWT token |
|
|
1987
|
+
| `.middleware()` | JWT verify middleware — sets `ctx.user` |
|
|
1838
1988
|
|
|
1839
1989
|
### permissions [α] — RBAC [Security]
|
|
1840
1990
|
|
|
@@ -1848,11 +1998,14 @@ await perm.migrate()
|
|
|
1848
1998
|
await perm.assignRole(userId, 'admin')
|
|
1849
1999
|
await perm.grantPermission('admin', 'posts:create')
|
|
1850
2000
|
await perm.grantPermission('admin', 'posts:edit')
|
|
1851
|
-
await perm.grantPermission('admin', '*')
|
|
2001
|
+
await perm.grantPermission('admin', '*') // wildcard — all permissions
|
|
1852
2002
|
|
|
1853
2003
|
// Use as middleware
|
|
1854
|
-
app.use((req, ctx, next) => {
|
|
1855
|
-
|
|
2004
|
+
app.use((req, ctx, next) => {
|
|
2005
|
+
ctx.user = { id: userId }
|
|
2006
|
+
return next(req, ctx)
|
|
2007
|
+
})
|
|
2008
|
+
app.use(perm) // → ctx.roles, ctx.permissions
|
|
1856
2009
|
|
|
1857
2010
|
// Route guards
|
|
1858
2011
|
app.get('/admin', perm.requireRole('admin'), adminHandler)
|
|
@@ -1867,26 +2020,26 @@ app.get('/posts/:id', async (req, ctx) => {
|
|
|
1867
2020
|
})
|
|
1868
2021
|
```
|
|
1869
2022
|
|
|
1870
|
-
| Option
|
|
1871
|
-
|
|
1872
|
-
| `pg`
|
|
1873
|
-
| `prefix` | `string` | `''`
|
|
2023
|
+
| Option | Type | Default | Description |
|
|
2024
|
+
| -------- | -------- | ------- | --------------------------------------------- |
|
|
2025
|
+
| `pg` | `object` | — | PostgreSQL client |
|
|
2026
|
+
| `prefix` | `string` | `''` | Table prefix (e.g. `'myapp'` → `myapp_roles`) |
|
|
1874
2027
|
|
|
1875
|
-
| Method
|
|
1876
|
-
|
|
1877
|
-
| `.assignRole(userId, role)`
|
|
1878
|
-
| `.removeRole(userId, role)`
|
|
1879
|
-
| `.grantPermission(role, permission)`
|
|
1880
|
-
| `.revokePermission(role, permission)` | Revoke permission from role
|
|
1881
|
-
| `.getUserRoles(userId)`
|
|
1882
|
-
| `.getUserPermissions(userId)`
|
|
1883
|
-
| `.requireRole(...roles)`
|
|
1884
|
-
| `.requirePermission(...perms)`
|
|
1885
|
-
| `.migrate()`
|
|
2028
|
+
| Method | Description |
|
|
2029
|
+
| ------------------------------------- | --------------------------------------------------- |
|
|
2030
|
+
| `.assignRole(userId, role)` | Assign role to user (creates role if missing) |
|
|
2031
|
+
| `.removeRole(userId, role)` | Remove role from user |
|
|
2032
|
+
| `.grantPermission(role, permission)` | Grant permission to role |
|
|
2033
|
+
| `.revokePermission(role, permission)` | Revoke permission from role |
|
|
2034
|
+
| `.getUserRoles(userId)` | List user's roles |
|
|
2035
|
+
| `.getUserPermissions(userId)` | List user's permissions (union of all roles) |
|
|
2036
|
+
| `.requireRole(...roles)` | Middleware — rejects if user lacks any of the roles |
|
|
2037
|
+
| `.requirePermission(...perms)` | Middleware — rejects if user lacks any permission |
|
|
2038
|
+
| `.migrate()` | Create tables |
|
|
1886
2039
|
|
|
1887
2040
|
### validate [α] [DevTools]
|
|
1888
2041
|
|
|
1889
|
-
|
|
2042
|
+
````ts
|
|
1890
2043
|
import { z } from 'zod'
|
|
1891
2044
|
const CreateUser = z.object({ name: z.string().min(1), email: z.string().email() })
|
|
1892
2045
|
app.post('/users', validate({ body: CreateUser, query: z.object({ ref: z.string().optional() }) }), (req, ctx) => {
|
|
@@ -1909,14 +2062,14 @@ app.post('/contact', validate(), (req, ctx) => {
|
|
|
1909
2062
|
|
|
1910
2063
|
// Or validate with Zod
|
|
1911
2064
|
app.post('/contact', validate({ body: z.object({ email: z.string().email() }) }), handler)
|
|
1912
|
-
|
|
2065
|
+
````
|
|
1913
2066
|
|
|
1914
|
-
| Option
|
|
1915
|
-
|
|
1916
|
-
| `body`
|
|
1917
|
-
| `query`
|
|
1918
|
-
| `params`
|
|
1919
|
-
| `headers` | `ZodSchema` | —
|
|
2067
|
+
| Option | Type | Default | Description |
|
|
2068
|
+
| --------- | ----------- | ------- | ------------------------------------- |
|
|
2069
|
+
| `body` | `ZodSchema` | — | Body validation schema (omit to skip) |
|
|
2070
|
+
| `query` | `ZodSchema` | — | Query validation schema |
|
|
2071
|
+
| `params` | `ZodSchema` | — | URL params validation schema |
|
|
2072
|
+
| `headers` | `ZodSchema` | — | Header validation schema |
|
|
1920
2073
|
|
|
1921
2074
|
### webhook [β] [API]
|
|
1922
2075
|
|
|
@@ -1946,28 +2099,30 @@ wh.on('*', (event) => {
|
|
|
1946
2099
|
})
|
|
1947
2100
|
```
|
|
1948
2101
|
|
|
1949
|
-
| Option
|
|
1950
|
-
|
|
1951
|
-
| `stripe`
|
|
1952
|
-
| `github`
|
|
1953
|
-
| `slack`
|
|
1954
|
-
| `custom`
|
|
1955
|
-
| `replayProtection` | `boolean`
|
|
1956
|
-
| `idempotencyTTL`
|
|
2102
|
+
| Option | Type | Default | Description |
|
|
2103
|
+
| ------------------ | ------------------------ | --------- | ----------------------------------- |
|
|
2104
|
+
| `stripe` | `PlatformConfig` | — | Stripe webhook config with `secret` |
|
|
2105
|
+
| `github` | `PlatformConfig` | — | GitHub webhook config |
|
|
2106
|
+
| `slack` | `PlatformConfig` | — | Slack webhook config |
|
|
2107
|
+
| `custom` | `CustomVerifierConfig[]` | — | Custom signature verifiers |
|
|
2108
|
+
| `replayProtection` | `boolean` | `true` | Deduplicate by event ID |
|
|
2109
|
+
| `idempotencyTTL` | `number` | `3600000` | Dedup TTL (ms) |
|
|
1957
2110
|
|
|
1958
2111
|
Built-in verifiers handle HMAC-SHA256, timestamp validation (Slack's 5-min window), and Stripe's `t=` / `v1=` signature format. Slack URL verification challenges are auto-responded.
|
|
1959
2112
|
|
|
1960
2113
|
### Client-side navigation [δ] [Client]
|
|
1961
2114
|
|
|
1962
2115
|
```tsx
|
|
1963
|
-
import { Link, useNavigate, useNavigating } from 'weifuwu/react'
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
const
|
|
2116
|
+
import { Link, navigate, useNavigate, useNavigating } from 'weifuwu/react'
|
|
2117
|
+
;<Link href="/about" prefetch>
|
|
2118
|
+
About
|
|
2119
|
+
</Link> // client-side nav + prefetch on hover/visible
|
|
2120
|
+
const n = useNavigate() // hook: n('/contact')
|
|
2121
|
+
navigate('/contact') // bare function (no hook needed)
|
|
2122
|
+
const loading = useNavigating() // reactive loading state
|
|
1968
2123
|
```
|
|
1969
2124
|
|
|
1970
|
-
`navigate()` fetches SSR, extracts
|
|
2125
|
+
`navigate()` fetches the SSR page, extracts the root container content, and replaces it in-place. Middleware runs on server each nav — data is always fresh.
|
|
1971
2126
|
|
|
1972
2127
|
**Preference URLs** (`/__lang/`, `/__theme/`) are intercepted by modular interceptors registered via `addInterceptor()` — no page reload needed. Importing `useLocale` or `useTheme` registers the interceptor automatically.
|
|
1973
2128
|
|
|
@@ -2021,11 +2176,11 @@ function LangSwitch() {
|
|
|
2021
2176
|
}
|
|
2022
2177
|
```
|
|
2023
2178
|
|
|
2024
|
-
| Return
|
|
2025
|
-
|
|
2026
|
-
| `locale`
|
|
2179
|
+
| Return | Description |
|
|
2180
|
+
| ------------------- | ----------------------------------------------------- |
|
|
2181
|
+
| `locale` | Current locale string (from `ctx.i18n.locale`) |
|
|
2027
2182
|
| `setLocale(locale)` | Switch locale (calls `navigate('/__lang/' + locale)`) |
|
|
2028
|
-
| `t`
|
|
2183
|
+
| `t` | Translate a key using loaded locale messages |
|
|
2029
2184
|
|
|
2030
2185
|
```tsx
|
|
2031
2186
|
import { useTheme } from 'weifuwu/react'
|
|
@@ -2033,8 +2188,8 @@ function ThemeToggle() {
|
|
|
2033
2188
|
const { theme, resolvedTheme, setTheme } = useTheme()
|
|
2034
2189
|
return (
|
|
2035
2190
|
<>
|
|
2036
|
-
<span>Current: {resolvedTheme}</span>
|
|
2037
|
-
<select value={theme} onChange={e => setTheme(e.target.value)}>
|
|
2191
|
+
<span>Current: {resolvedTheme}</span> {/* 'dark' | 'light' — never 'system' */}
|
|
2192
|
+
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
|
|
2038
2193
|
<option value="light">☀ Light</option>
|
|
2039
2194
|
<option value="dark">🌙 Dark</option>
|
|
2040
2195
|
<option value="system">💻 System</option>
|
|
@@ -2044,11 +2199,11 @@ function ThemeToggle() {
|
|
|
2044
2199
|
}
|
|
2045
2200
|
```
|
|
2046
2201
|
|
|
2047
|
-
| Return
|
|
2048
|
-
|
|
2049
|
-
| `theme`
|
|
2050
|
-
| `resolvedTheme`
|
|
2051
|
-
| `setTheme(theme)` | Switch theme (calls `navigate('/__theme/' + theme)`)
|
|
2202
|
+
| Return | Description |
|
|
2203
|
+
| ----------------- | ---------------------------------------------------------------- |
|
|
2204
|
+
| `theme` | Raw preference (`'light'` \| `'dark'` \| `'system'`) |
|
|
2205
|
+
| `resolvedTheme` | Resolved value (`'light'` \| `'dark'`) — `'system'` → matchMedia |
|
|
2206
|
+
| `setTheme(theme)` | Switch theme (calls `navigate('/__theme/' + theme)`) |
|
|
2052
2207
|
|
|
2053
2208
|
**`applyTheme(theme)`** — DOM-only theme application. Sets `data-theme` on `<html>`, registers `matchMedia` listener for `'system'`. Used by the interceptor; exported for custom scenarios.
|
|
2054
2209
|
|
|
@@ -2058,11 +2213,17 @@ function ThemeToggle() {
|
|
|
2058
2213
|
import { useLoaderData } from 'weifuwu/react'
|
|
2059
2214
|
function Page() {
|
|
2060
2215
|
const data = useLoaderData<{ posts: Post[] }>()
|
|
2061
|
-
return
|
|
2216
|
+
return (
|
|
2217
|
+
<ul>
|
|
2218
|
+
{data.posts.map((p) => (
|
|
2219
|
+
<li key={p.id}>{p.title}</li>
|
|
2220
|
+
))}
|
|
2221
|
+
</ul>
|
|
2222
|
+
)
|
|
2062
2223
|
}
|
|
2063
2224
|
```
|
|
2064
2225
|
|
|
2065
|
-
On the server, data flows from middleware → `ctx` → `setCtx(ctxValue)` (serialized via JSON). On the client, the hydration script calls `setCtx(ctxData)` which populates the shared store
|
|
2226
|
+
On the server, data flows from middleware → `ctx` → `setCtx(ctxValue)` (serialized via JSON). On the client, the hydration script calls `setCtx(ctxData)` which populates the shared context store. `useLoaderData()` reads from the snapshot via `useSyncExternalStore` — no SSR-specific code needed in your components.
|
|
2066
2227
|
|
|
2067
2228
|
**`addInterceptor(fn)`** — Register a URL interceptor. Interceptors run before SPA navigation; if one returns `true`, `navigate()` skips the fetch-and-swap.
|
|
2068
2229
|
|
|
@@ -2084,7 +2245,7 @@ app.use(flash())
|
|
|
2084
2245
|
|
|
2085
2246
|
// Read flash
|
|
2086
2247
|
app.get('/', (req, ctx) => {
|
|
2087
|
-
const msg = ctx.flash.value
|
|
2248
|
+
const msg = ctx.flash.value // { type: 'success', text: 'Saved!' }
|
|
2088
2249
|
})
|
|
2089
2250
|
|
|
2090
2251
|
// Set flash + redirect
|
|
@@ -2106,8 +2267,8 @@ function Toast() {
|
|
|
2106
2267
|
}
|
|
2107
2268
|
```
|
|
2108
2269
|
|
|
2109
|
-
| Option | Type
|
|
2110
|
-
|
|
2270
|
+
| Option | Type | Default | Description |
|
|
2271
|
+
| ------ | -------- | --------- | ----------- |
|
|
2111
2272
|
| `name` | `string` | `'flash'` | Cookie name |
|
|
2112
2273
|
|
|
2113
2274
|
### Dev mode [δ] [Client]
|
|
@@ -2116,7 +2277,7 @@ Auto-detected when `NODE_ENV === 'development'`. `ssr({dir})` automatically regi
|
|
|
2116
2277
|
|
|
2117
2278
|
- Inline hydration script uses `createRoot` + render (replaces SSR DOM)
|
|
2118
2279
|
- Vendor bundle served at `/__wfw/v/bundle?h=<hash>` — compiled from source, unminified
|
|
2119
|
-
- Hot component replacement: file changes → WebSocket message → browser imports hot bundle →
|
|
2280
|
+
- Hot component replacement: file changes → WebSocket message → browser imports hot bundle → component refreshed in place — `useState` values preserved
|
|
2120
2281
|
- Tailwind CSS hot-reloads without page refresh
|
|
2121
2282
|
- Layout changes trigger a full page reload
|
|
2122
2283
|
|
|
@@ -2125,7 +2286,17 @@ Auto-detected when `NODE_ENV === 'development'`. `ssr({dir})` automatically regi
|
|
|
2125
2286
|
## AI
|
|
2126
2287
|
|
|
2127
2288
|
```ts
|
|
2128
|
-
import {
|
|
2289
|
+
import {
|
|
2290
|
+
openai,
|
|
2291
|
+
streamText,
|
|
2292
|
+
generateText,
|
|
2293
|
+
streamObject,
|
|
2294
|
+
generateObject,
|
|
2295
|
+
tool,
|
|
2296
|
+
embed,
|
|
2297
|
+
embedMany,
|
|
2298
|
+
aiProvider,
|
|
2299
|
+
} from 'weifuwu'
|
|
2129
2300
|
import { runWorkflow } from 'weifuwu'
|
|
2130
2301
|
|
|
2131
2302
|
const provider = aiProvider()
|
|
@@ -2148,23 +2319,23 @@ app.post('/ask', async (req, ctx) => {
|
|
|
2148
2319
|
})
|
|
2149
2320
|
```
|
|
2150
2321
|
|
|
2151
|
-
| Option
|
|
2152
|
-
|
|
2153
|
-
| `baseURL`
|
|
2154
|
-
| `apiKey`
|
|
2155
|
-
| `model`
|
|
2156
|
-
| `embeddingModel`
|
|
2157
|
-
| `embeddingDimension` | `number` | `EMBEDDING_DIMENSION` env or `1024`
|
|
2158
|
-
|
|
2159
|
-
| Method
|
|
2160
|
-
|
|
2161
|
-
| `.model(name?)`
|
|
2162
|
-
| `.embeddingModel(name?)` | Get `EmbeddingModel` instance
|
|
2163
|
-
| `.embed(text)`
|
|
2164
|
-
| `.embedMany(texts)`
|
|
2165
|
-
| `.generateText(params)`
|
|
2166
|
-
| `.streamText(params)`
|
|
2167
|
-
| `.dimension`
|
|
2322
|
+
| Option | Type | Default | Description |
|
|
2323
|
+
| -------------------- | -------- | -------------------------------------------------------- | -------------------- |
|
|
2324
|
+
| `baseURL` | `string` | `OPENAI_BASE_URL` env or `http://localhost:11434/v1` | API base URL |
|
|
2325
|
+
| `apiKey` | `string` | `OPENAI_API_KEY` env or `'ollama'` | API key |
|
|
2326
|
+
| `model` | `string` | `OPENAI_MODEL` env or `'qwen3:0.6b'` | Chat model name |
|
|
2327
|
+
| `embeddingModel` | `string` | `OPENAI_EMBEDDING_MODEL` env or `'qwen3-embedding:0.6b'` | Embedding model name |
|
|
2328
|
+
| `embeddingDimension` | `number` | `EMBEDDING_DIMENSION` env or `1024` | Vector dimension |
|
|
2329
|
+
|
|
2330
|
+
| Method | Description |
|
|
2331
|
+
| ------------------------ | --------------------------------------- |
|
|
2332
|
+
| `.model(name?)` | Get `LanguageModel` instance |
|
|
2333
|
+
| `.embeddingModel(name?)` | Get `EmbeddingModel` instance |
|
|
2334
|
+
| `.embed(text)` | Embed single text → `Promise<number[]>` |
|
|
2335
|
+
| `.embedMany(texts)` | Batch embed → `Promise<number[][]>` |
|
|
2336
|
+
| `.generateText(params)` | Generate text (model auto-injected) |
|
|
2337
|
+
| `.streamText(params)` | Stream text (model auto-injected) |
|
|
2338
|
+
| `.dimension` | Configured embedding dimension |
|
|
2168
2339
|
|
|
2169
2340
|
### DAG Workflow [AI]
|
|
2170
2341
|
|
|
@@ -2178,12 +2349,12 @@ const wf = runWorkflow({ tools, provider })
|
|
|
2178
2349
|
const wf = runWorkflow({ tools, model: openai('gpt-4o') })
|
|
2179
2350
|
```
|
|
2180
2351
|
|
|
2181
|
-
| Option
|
|
2182
|
-
|
|
2183
|
-
| `tools`
|
|
2184
|
-
| `provider` | `AIProvider`
|
|
2185
|
-
| `model`
|
|
2186
|
-
| `maxSteps` | `number`
|
|
2352
|
+
| Option | Type | Default | Description |
|
|
2353
|
+
| ---------- | --------------- | ------- | ---------------------------------------------------------------- |
|
|
2354
|
+
| `tools` | `object` | — | Registered tool definitions |
|
|
2355
|
+
| `provider` | `AIProvider` | — | AI provider (uses `provider.model()` for LLM-generated workflow) |
|
|
2356
|
+
| `model` | `LanguageModel` | — | Explicit model (overrides provider) |
|
|
2357
|
+
| `maxSteps` | `number` | `200` | Max execution steps |
|
|
2187
2358
|
|
|
2188
2359
|
---
|
|
2189
2360
|
|
|
@@ -2191,7 +2362,10 @@ const wf = runWorkflow({ tools, model: openai('gpt-4o') })
|
|
|
2191
2362
|
|
|
2192
2363
|
```ts
|
|
2193
2364
|
import { createSSEStream, formatSSE, formatSSEData } from 'weifuwu'
|
|
2194
|
-
async function* events() {
|
|
2365
|
+
async function* events() {
|
|
2366
|
+
yield formatSSE('chat', 'Hello')
|
|
2367
|
+
yield formatSSE('chat', 'World')
|
|
2368
|
+
}
|
|
2195
2369
|
app.get('/stream', (req, ctx) => createSSEStream(events()))
|
|
2196
2370
|
```
|
|
2197
2371
|
|
|
@@ -2206,16 +2380,24 @@ Every public symbol can be imported from `'weifuwu'`:
|
|
|
2206
2380
|
```ts
|
|
2207
2381
|
serve, createTestServer, Router, ssr,
|
|
2208
2382
|
Context, Handler, Middleware, ErrorHandler, ServeOptions, Server,
|
|
2209
|
-
loadEnv,
|
|
2210
|
-
currentTraceId, currentTrace, runWithTrace, traceElapsed, TraceContext,
|
|
2383
|
+
loadEnv, env, isDev, isProd, isBundled, getPublicEnv,
|
|
2384
|
+
currentTraceId, currentTrace, runWithTrace, traceElapsed, trace, TraceContext,
|
|
2385
|
+
testApp, TestApp, TestRequest, TestResponse,
|
|
2386
|
+
createTestDb, withTestDb,
|
|
2387
|
+
getCookies, setCookie, deleteCookie,
|
|
2388
|
+
createSSEStream, formatSSE, formatSSEData, SSEEvent,
|
|
2389
|
+
DEFAULT_MAX_BODY, MIGRATIONS_TABLE,
|
|
2211
2390
|
```
|
|
2212
2391
|
|
|
2213
|
-
### Middleware
|
|
2392
|
+
### Middleware / DevTools
|
|
2214
2393
|
|
|
2215
2394
|
```ts
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2395
|
+
logger, cors, compress, helmet,
|
|
2396
|
+
rateLimit, requestId, validate, upload,
|
|
2397
|
+
csrf, session, MemoryStore, RedisStore, SessionStore,
|
|
2398
|
+
cache, MemoryCache, RedisCache, CacheStore,
|
|
2399
|
+
flash, permissions,
|
|
2400
|
+
serveStatic, s3,
|
|
2219
2401
|
```
|
|
2220
2402
|
|
|
2221
2403
|
### Database
|
|
@@ -2225,55 +2407,87 @@ postgres, PostgresOptions, PostgresClient,
|
|
|
2225
2407
|
redis, RedisOptions, RedisClient,
|
|
2226
2408
|
queue, QueueOptions, QueueJob, Queue,
|
|
2227
2409
|
PostgresInjected, RedisInjected, QueueInjected,
|
|
2228
|
-
// Schema helpers
|
|
2410
|
+
// Schema helpers:
|
|
2229
2411
|
pgTable, SQL, sql,
|
|
2230
2412
|
ColumnBuilder, serial, uuid, text, integer, boolean, boolean_, timestamptz, jsonb, textArray, vector,
|
|
2231
2413
|
partitionBy, timestamps, toDDL, PartitionByDef,
|
|
2232
2414
|
Table, BoundTable, IndexOptions, FindOptions, CreateOptions,
|
|
2233
|
-
eq, ne, gt, gte, lt, lte, isNull, isNotNull, like, contains, in_, and, or, not
|
|
2415
|
+
eq, ne, gt, gte, lt, lte, isNull, isNotNull, like, contains, in_, and, or, not,
|
|
2416
|
+
fts,
|
|
2234
2417
|
```
|
|
2235
2418
|
|
|
2236
|
-
###
|
|
2419
|
+
### Security / Auth
|
|
2237
2420
|
|
|
2238
2421
|
```ts
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2422
|
+
auth,
|
|
2423
|
+
user, UserModule, UserData, UserOptions, UserInjected, OAuthProviderConfig, OAuth2Client,
|
|
2424
|
+
permissions, PermissionsModule, PermissionsOptions,
|
|
2425
|
+
csrf, CsrfOptions, CsrfInjected,
|
|
2426
|
+
helmet, HelmetOptions,
|
|
2427
|
+
session, SessionStore, SessionOptions, SessionData, SessionInjected,
|
|
2428
|
+
rateLimit, RateLimitOptions,
|
|
2245
2429
|
```
|
|
2246
|
-
export type { UseAgentStreamOptions, UseAgentStreamReturn, AgentStreamState } from 'weifuwu/react'
|
|
2247
2430
|
|
|
2248
|
-
###
|
|
2431
|
+
### UX Middleware
|
|
2249
2432
|
|
|
2250
2433
|
```ts
|
|
2251
|
-
|
|
2434
|
+
theme, ThemeOptions, ThemeInjected,
|
|
2435
|
+
i18n, I18nOptions, I18nInjected,
|
|
2436
|
+
flash, FlashOptions, FlashInjected,
|
|
2252
2437
|
```
|
|
2253
2438
|
|
|
2254
|
-
### AI
|
|
2439
|
+
### AI
|
|
2255
2440
|
|
|
2256
2441
|
```ts
|
|
2442
|
+
aiProvider, AIProvider, AIProviderOptions, AIProviderInjected,
|
|
2257
2443
|
streamText, generateText, streamObject, generateObject,
|
|
2258
2444
|
tool, embed, embedMany, smoothStream,
|
|
2259
|
-
openai, createOpenAI
|
|
2445
|
+
openai, createOpenAI,
|
|
2446
|
+
aiStream, AIHandler,
|
|
2447
|
+
runWorkflow,
|
|
2448
|
+
agent, AgentModule, AgentOptions,
|
|
2449
|
+
knowledgeBase, KBModule, KBOptions,
|
|
2450
|
+
opencode, OpencodeModule, OpencodeOptions,
|
|
2260
2451
|
```
|
|
2261
2452
|
|
|
2262
|
-
###
|
|
2453
|
+
### API / Routing
|
|
2263
2454
|
|
|
2264
2455
|
```ts
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2456
|
+
analytics, AnalyticsModule, AnalyticsOptions,
|
|
2457
|
+
health, HealthOptions,
|
|
2458
|
+
graphql, GraphQLOptions, GraphQLHandler,
|
|
2459
|
+
logdb, LogdbModule, LogdbOptions,
|
|
2460
|
+
seo, seoMiddleware, seoTags, SeoOptions,
|
|
2461
|
+
webhook, WebhookModule, WebhookOptions,
|
|
2462
|
+
iii, createWorker, registerWorker, IIIModule, IIIOptions,
|
|
2463
|
+
```
|
|
2464
|
+
|
|
2465
|
+
### Networking / Storage
|
|
2466
|
+
|
|
2467
|
+
```ts
|
|
2468
|
+
s3, S3Options, S3Module, S3Body,
|
|
2469
|
+
mailer, MailerOptions, Mailer,
|
|
2470
|
+
messager, MessagerModule, MessagerOptions,
|
|
2471
|
+
hub, createHub, Hub, HubOptions,
|
|
2472
|
+
deploy, defineConfig, DeployConfig, AppConfig,
|
|
2473
|
+
tenant, TenantModule, TenantOptions, TenantContext,
|
|
2474
|
+
```
|
|
2475
|
+
|
|
2476
|
+
### Client-side (from `'weifuwu/react'`)
|
|
2477
|
+
|
|
2478
|
+
```ts
|
|
2479
|
+
TsxContext, setCtx, useCtx, addCtxRebuilder, useLoaderData,
|
|
2480
|
+
useWebsocket, useAction, useFetch, useQueryState, createStore,
|
|
2481
|
+
Link, useNavigate, useNavigating, addInterceptor,
|
|
2482
|
+
useLocale, useTheme, applyTheme, useFlashMessage,
|
|
2483
|
+
useAgentStream,
|
|
2484
|
+
Head,
|
|
2485
|
+
|
|
2486
|
+
// Types:
|
|
2487
|
+
StoreApi,
|
|
2488
|
+
UseActionOptions, UseActionReturn,
|
|
2489
|
+
UseWebsocketOptions, UseWebsocketReturn,
|
|
2490
|
+
UseAgentStreamOptions, UseAgentStreamReturn, AgentStreamState,
|
|
2277
2491
|
```
|
|
2278
2492
|
|
|
2279
2493
|
---
|