weifuwu 0.22.3 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -123,7 +123,7 @@ Request → serve() → app.handler() → global middleware × N → path middle
123
123
 
124
124
  1. `serve()` receives HTTP request
125
125
  2. `app.handler()` creates `ctx = { params, query }` and routes to the matching trie node
126
- 3. **Global middleware** runs in `use()` order (e.g. `preferences()`, `postgres()`, `cors()`)
126
+ 3. **Global middleware** runs in `use()` order (e.g. `theme()`, `i18n()`, `postgres()`, `cors()`)
127
127
  4. **Path‑scoped middleware** runs for matching paths (e.g. `app.use('/admin', authMW)`)
128
128
  5. **Route‑level middleware** runs (e.g. `app.get('/admin', validate(...), handler)`)
129
129
  6. **Route handler** returns `Response` — middleware chain unwinds
@@ -152,22 +152,23 @@ The `ctx` object accumulates properties as it passes through the middleware chai
152
152
  | `csrfToken` | `csrf()` | `string` | CSRF token |
153
153
  | `requestId` | `requestId()` | `string` | Request ID |
154
154
  | `session` | `session()` | `Session` | Session data object |
155
- | `sessionId` | `session()` | `string` | Session ID |
156
155
  | `sql` | `postgres()` | `Sql<{}>` | PostgreSQL tagged-template client |
157
156
  | `redis` | `redis()` | `Redis` | Redis client |
157
+ | `ai` | `aiProvider()` | `AIProvider` | AI model & embedding |
158
158
  | `queue` | `queue()` | `Queue` | Job queue |
159
- | `prefs` | `preferences()` | `{ locale, theme }` | User preferences (locale, theme) |
160
- | `deploy` | `deploy()` | `{ appName? }` | Deploy gateway info |
159
+ | `session` | `session()` | `Session` | Session data object |
160
+ | `user` | `auth()` / `user().middleware()` | `{ id?: string }` | Authenticated user |
161
+ | `permissions` | `permissions()` | `{ roles, permissions }` | RBAC roles & permissions sets |
162
+ | `theme` | `theme()` | `{ value, set }` | Current theme + switcher |
163
+ | `i18n` | `i18n()` | `{ locale, t, set }` | Locale, translation, switcher |
164
+ | `flash` | `flash()` | `{ value, set }` | Flash message + setter |
165
+ | `tailwind` | `tailwindContext()` | `{ css, url }` | Compiled Tailwind CSS |
166
+ | `tenant` | `tenant()` | `TenantContext` | Current tenant info |
167
+ | `parsed` | `validate()` / `upload()` | `{ body, files }` | Validated/parsed request data |
161
168
  | `layoutStack` | `ssr()` internal | `LayoutEntry[]` | React layout component stack |
162
169
  | `loaderData` | User middleware | `Record<string, unknown>` | SSR data passed to client |
163
- | `user` | `auth()` / `user().middleware()` | `{ id?: string }` | Authenticated user |
164
- | `parsed` | `validate()` / `upload()` | `{ body, query, params, headers, files }` | Validated/parsed request data |
165
- | `t` | `preferences()` | `(key) => string` | Translation function |
166
- | `setPref` | `preferences()` | `(key, val) => Response` | Set preference cookie + redirect |
167
- | `compiledTailwindCss` | `ssr()` internal | `string` | Compiled CSS content (internal) |
168
- | `tailwindCssUrl` | `ssr()` internal | `string` | Compiled CSS route URL (internal) |
169
- | `session` | `session()` | `Session` | Session data object |
170
- | `sessionId` | `session()` | `string` | Session ID |
170
+ | `mountPath` | `Router` | `string` | Sub-router mount path |
171
+ | `deploy` | `deploy()` | `{ appName? }` | Deploy gateway info |
171
172
 
172
173
  ### Type-Safe Context
173
174
 
@@ -196,7 +197,7 @@ All modules follow one of **2 patterns** — learn these and you know every modu
196
197
 
197
198
  | Pattern | How to mount | Example |
198
199
  |---------|-------------|---------|
199
- | `[α]` | `app.use(mod())` | `compress()`, `preferences()`, `postgres()` |
200
+ | `[α]` | `app.use(mod())` | `compress()`, `theme()`, `postgres()` |
200
201
  | `[β]` | `app.use('/path', mod())` | `health()`, `ssr({dir})`, `graphql(handler)`, `user()` |
201
202
 
202
203
  ### Pattern α — Middleware
@@ -349,7 +350,8 @@ Uses `TEST_DATABASE_URL` or `DATABASE_URL`. Automatically skipped in CI if unset
349
350
  ### agent [β]
350
351
 
351
352
  ```ts
352
- const a = agent({ pg, model: openai('gpt-4o'), embeddingModel: openai.embedding('text-embedding-3-small') })
353
+ const provider = aiProvider()
354
+ const a = agent({ pg, provider })
353
355
  await a.migrate()
354
356
  app.use('/api', a)
355
357
  await a.addKnowledge(agentId, 'Title', 'some knowledge content')
@@ -359,9 +361,10 @@ a.run(agentId, { input: 'summarize the data', stream: true })
359
361
  | Option | Type | Default | Description |
360
362
  |--------|------|---------|-------------|
361
363
  | `pg` | `object` | — | PostgreSQL client |
362
- | `model` | `object` | | AI model (e.g. `openai('gpt-4o')`) |
363
- | `embeddingModel` | `object` | — | Embedding model for knowledge search |
364
- | `embeddingDimension` | `number` | `1536` | Embedding vector dimension |
364
+ | `provider` | `AIProvider` | `aiProvider()` (from env) | AI provider for model & embedding resolution |
365
+ | `model` | `object` | — | Explicit AI model (overrides provider) |
366
+ | `embeddingModel` | `object` | | Explicit embedding model (overrides provider) |
367
+ | `embeddingDimension` | `number` | `provider.dimension` | Embedding vector dimension |
365
368
  | `tools` | `object[]` | — | Custom tool definitions |
366
369
 
367
370
  | Method | Description |
@@ -376,13 +379,15 @@ a.run(agentId, { input: 'summarize the data', stream: true })
376
379
  Creates an AI streaming chat endpoint using the Vercel AI SDK.
377
380
 
378
381
  ```ts
379
- const chat = await aiStream(async (req) => ({ model: openai('gpt-4o'), messages: (await req.json()).messages }))
382
+ const provider = aiProvider()
383
+ const chat = await aiStream(async (req) => ({ messages: (await req.json()).messages }), provider)
380
384
  app.use('/chat', chat)
381
385
  ```
382
386
 
383
387
  | Param | Type | Description |
384
388
  |-------|------|-------------|
385
389
  | `handler` | `(req, ctx) => AIStreamOptions \| Promise<AIStreamOptions>` | Returns AI SDK options (model, messages, schema, etc.) |
390
+ | `provider` | `AIProvider` | Optional. If provided and handler omits `model`, `provider.model()` is used as default |
386
391
 
387
392
  ### analytics [β]
388
393
 
@@ -729,15 +734,12 @@ await engine.trigger({ function_id: 'orders::create', payload: { items: ['apple'
729
734
  ### knowledgeBase [β] — RAG with pgvector
730
735
 
731
736
  ```ts
732
- import { knowledgeBase } from 'weifuwu'
733
- import { embed } from 'ai'
737
+ import { knowledgeBase, aiProvider } from 'weifuwu'
734
738
 
735
739
  const kb = knowledgeBase({
736
- sql: ctx.sql,
737
- embedding: (text) =>
738
- embed({ model: openai.embedding('text-embedding-3-small'), value: text })
739
- .then(r => r.embedding),
740
- dimensions: 1536,
740
+ pg: postgres(),
741
+ provider: aiProvider(),
742
+ table: 'my_docs',
741
743
  })
742
744
 
743
745
  // Create table + HNSW index (safe to call multiple times)
@@ -770,9 +772,8 @@ app.get('/search', async (req, ctx) => {
770
772
 
771
773
  | Option | Type | Default | Description |
772
774
  |--------|------|---------|-------------|
773
- | `sql` | `Sql<{}>` | — | **Required.** Postgres client with pgvector |
774
- | `embedding` | `(text) => Promise<number[]>` | — | **Required.** Embedding function |
775
- | `dimensions` | `number` | `1536` | Vector dimensions |
775
+ | `pg` | `PostgresClient` | — | **Required.** PostgreSQL client |
776
+ | `provider` | `AIProvider` | — | **Required.** AI provider for embedding |
776
777
  | `table` | `string` | `'_kb_docs'` | Database table name |
777
778
  | `chunkSize` | `number` | `512` | Max characters per chunk |
778
779
  | `chunkOverlap` | `number` | `64` | Overlap between chunks |
@@ -780,8 +781,8 @@ app.get('/search', async (req, ctx) => {
780
781
  | `searchThreshold` | `number` | `0` | Minimum similarity (0–1) |
781
782
 
782
783
  Documents are split on paragraph boundaries (`\n\n`). Re-ingesting the same key
783
- replaces old chunks. The HNSW index enables fast approximate nearest-neighbor
784
- search (cosine distance).
784
+ replaces old chunks. Provider's `embed()` is used automatically.
785
+ The HNSW index enables fast approximate nearest-neighbor search (cosine distance).
785
786
 
786
787
 
787
788
  ### logdb [β]
@@ -1127,56 +1128,110 @@ await fts.dropIndex(pg.sql, articles)
1127
1128
 
1128
1129
  Search options: `fields`, `limit` (20), `offset` (0), `headline` (false), `language` ('english'), `minRank`.
1129
1130
 
1130
- ### preferences [α]
1131
+ ### theme [α]
1131
1132
 
1132
- Locale detection + theme + translations. `/__lang/:locale` and `/__theme/:theme` auto-routed.
1133
+ ```ts
1134
+ app.use(theme({ default: 'dark' }))
1135
+ // → ctx.theme = { value: 'dark', set: fn }
1136
+ // → ctx.theme.value — 'dark'
1137
+ // → ctx.theme.set('light', '/settings') — 302 + Set-Cookie
1138
+
1139
+ // Client-side switching (interceptor auto-handles /__theme/:value)
1140
+ // → GET /__theme/dark — 302 + Set-Cookie
1141
+ ```
1142
+
1143
+ | Option | Type | Default | Description |
1144
+ |--------|------|---------|-------------|
1145
+ | `default` | `string` | `'system'` | Default theme |
1146
+ | `cookie` | `string` | `'theme'` | Cookie name (empty to disable) |
1133
1147
 
1134
1148
  ```ts
1135
- app.use(preferences({ dir: './locales', locale: { default: 'en' }, theme: { default: 'system' } }))
1136
- // ctx.prefs.locale, ctx.prefs.theme, ctx.t('key'), ctx.setPref('locale', 'zh')
1137
- // ctx.setPref() returns a 302 Response with Set-Cookie — return it from your handler
1138
- // GET /__lang/zh → 302 + Set-Cookie (or JSON if Accept: application/json)
1139
- // GET /__theme/dark → same pattern
1149
+ // Server-side switching
1150
+ app.post('/settings', async (req, ctx) => {
1151
+ const { theme } = await req.json()
1152
+ return ctx.theme.set(theme, '/settings')
1153
+ })
1140
1154
  ```
1141
1155
 
1142
- | Option | Default | Description |
1143
- |--------|---------|-------------|
1144
- | `dir` | — | Translation JSON directory |
1145
- | `locale.default` | `'en'` | Fallback locale |
1146
- | `locale.cookie` | `'locale'` | Cookie name |
1147
- | `locale.fromAcceptLanguage` | `true` | Detect from header |
1148
- | `theme.default` | `'system'` | `'light'` \| `'dark'` \| `'system'` |
1149
- | `theme.cookie` | `'theme'` | Cookie name |
1156
+ See [`useTheme()`](#usetheme) for client-side usage.
1150
1157
 
1151
- ```tsx
1152
- // Client-side no-refresh switching — import enables it automatically
1153
- import { useLocale, useTheme } from 'weifuwu/react'
1158
+ ### i18n [α]
1154
1159
 
1155
- <Link href="/__lang/zh">中文</Link> // <Link> handles it via interceptor
1156
- <button onClick={() => setLocale('en')}>EN</button> // or programmatic
1157
- const { theme, resolvedTheme, setTheme } = useTheme()
1158
- // resolvedTheme resolves 'system' → 'dark'|'light' based on prefers-color-scheme
1160
+ ```ts
1161
+ app.use(i18n({ default: 'zh', dir: './locales' }))
1162
+ // → ctx.i18n = { locale: 'zh', t: (key) => string, set: (locale) => Response }
1163
+ // ctx.i18n.t('welcome') → '欢迎'
1164
+ // → ctx.i18n.locale → 'zh'
1165
+ // → ctx.i18n.set('en', '/settings') — 302 + Set-Cookie
1166
+ // → GET /__lang/en — switch locale (client-side interceptor)
1159
1167
  ```
1160
1168
 
1169
+ | Option | Type | Default | Description |
1170
+ |--------|------|---------|-------------|
1171
+ | `default` | `string` | `'en'` | Default locale |
1172
+ | `dir` | `string` | — | Directory with `{locale}.json` files |
1173
+ | `messages` | `object` | — | Inline translations: `{ zh: { welcome: '欢迎' } }` |
1174
+ | `cookie` | `string` | `'locale'` | Cookie name (empty to disable) |
1175
+ | `fromAcceptLanguage` | `boolean` | `true` | Detect from Accept-Language header |
1176
+
1177
+ ```ts
1178
+ // Handler
1179
+ app.get('/greet', async (req, ctx) => {
1180
+ const greeting = ctx.i18n?.t('welcome', { name: 'Alice' })
1181
+ return Response.json({ greeting, locale: ctx.i18n?.locale })
1182
+ })
1183
+ ```
1184
+
1185
+ **Client-side:** import `useLocale` from `weifuwu/react`, `useTheme` from `weifuwu/react`.
1186
+
1161
1187
  ### queue [α]
1162
1188
 
1189
+ Async job queue. Supports immediate, delayed, and recurring (cron) tasks with three backends:
1190
+
1191
+ - `{ store: 'memory' }` — in-memory, zero dependency, suitable for dev & cron-like tasks
1192
+ - `{ store: 'pg', pg }` — PostgreSQL-backed, persistent, multi-instance safe via `FOR UPDATE SKIP LOCKED`
1193
+ - `{ store: 'redis', redis }` — Redis-backed, production-grade, distributed
1194
+
1163
1195
  ```ts
1164
- const q = queue({ redis })
1165
- app.use(q) // injects ctx.queue
1166
- await q.add('send-email', { to: 'user@test.com' }, { cron: '0 8 * * *' })
1196
+ // Create queue
1197
+ const q = queue({ store: 'memory' })
1198
+ // const q = queue({ store: 'pg', pg })
1199
+ // const q = queue({ store: 'redis', redis })
1200
+
1201
+ // Register cron job (uses the same backend for persistence)
1202
+ q.cron('*/5 * * * *', async () => { await cleanCache() })
1203
+
1204
+ // Or use process/add for full queue semantics
1205
+ q.process('send-email', async (job) => {
1206
+ await sendMail(job.payload)
1207
+ })
1208
+
1209
+ // Immediate
1210
+ q.add('send-email', { to: 'user@test.com' })
1211
+
1212
+ // Delayed
1213
+ q.add('send-email', { to: 'user@test.com' }, { delay: 60_000 })
1214
+
1215
+ // Scheduled (cron) — re-queues automatically
1216
+ q.add('weekly-report', {}, { schedule: '0 9 * * 1' })
1217
+
1218
+ q.run()
1167
1219
  ```
1168
1220
 
1169
1221
  | Option | Type | Default | Description |
1170
1222
  |--------|------|---------|-------------|
1171
- | `redis` | `object` | | Redis client |
1223
+ | `store` | `'memory' \| 'pg' \| 'redis'` | `'memory'` | Backend store |
1224
+ | `redis` | `object` | — | Redis client (required when `store: 'redis'`) |
1172
1225
  | `url` | `string` | — | Redis URL (alternative to client) |
1173
- | `prefix` | `string` | `'queue:'` | Redis key prefix |
1174
- | `pollInterval` | `number` | `1000` | Poll interval (ms) |
1226
+ | `pg` | `object` | | PostgreSQL client (required when `store: 'pg'`) |
1227
+ | `prefix` | `string` | `'queue'` | Key/table prefix |
1228
+ | `pollInterval` | `number` | `200` | Poll interval (ms) |
1175
1229
 
1176
1230
  | Method | Description |
1177
1231
  |--------|-------------|
1178
- | `.add(name, data, opts?)` | Add job to queue |
1179
- | `.process(handler)` | Register job processor |
1232
+ | `.cron(pattern, handler)` | Register a cron job (uses process + add internally) |
1233
+ | `.add(type, payload, opts?)` | Add job (opts: `delay`, `schedule`) |
1234
+ | `.process(type, handler)` | Register job processor |
1180
1235
  | `.run()` | Start processing |
1181
1236
  | `.stop()` | Stop processing |
1182
1237
  | `.jobs(limit?)` | List pending jobs |
@@ -1186,6 +1241,18 @@ await q.add('send-email', { to: 'user@test.com' }, { cron: '0 8 * * *' })
1186
1241
  | `.dashboard()` | Returns a Router with management endpoints |
1187
1242
  | `.close()` | Cleanup |
1188
1243
 
1244
+ **Schedule (cron) field reference:**
1245
+
1246
+ | Field | Range |
1247
+ |-------|-------|
1248
+ | minute | 0–59 |
1249
+ | hour | 0–23 |
1250
+ | day of month | 1–31 |
1251
+ | month | 1–12 |
1252
+ | day of week | 0–6 (0=Sunday) |
1253
+
1254
+ Supported cron syntax: `*` (any), `*/n` (every n), `n-m` (range), `n,m,o` (list), `n` (exact).
1255
+
1189
1256
  **Dashboard endpoints** (mount via `app.use('/__queue', q.dashboard())`):
1190
1257
 
1191
1258
  | Method | Path | Description |
@@ -1411,7 +1478,7 @@ await redis.destroy('sid')
1411
1478
 
1412
1479
  ### ssr({ dir }) [β]
1413
1480
 
1414
- One-stop Server-Side Rendering. Accepts a directory and returns a Router that handles all SSR routes, tailwind CSS, hydration bundles, and livereload — using Next.js-style file conventions.
1481
+ One-stop Server-Side Rendering. Accepts a directory and returns a Router that handles all SSR routes, tailwind CSS, hydration, and livereload — using Next.js-style file conventions.
1415
1482
 
1416
1483
  ```ts
1417
1484
  import { Router, ssr } from 'weifuwu'
@@ -1449,13 +1516,18 @@ app.use('/', ssr({ dir: './ui' }))
1449
1516
  | `app/error.tsx` | Error boundary for that subtree |
1450
1517
  | `app/globals.css` | Tailwind CSS entry (compiled via `@tailwindcss/postcss`) |
1451
1518
 
1452
- **How it works:**
1519
+ **How hydration works:**
1453
1520
 
1454
1521
  - Each page is lazy-resolved on first request — only the `page.tsx` and its layout chain are compiled
1455
- - Hydration bundle generated per-page at `/__ssr/{hash}.js`
1522
+ - An inline `<script type="module">` in the HTML handles hydration
1523
+ - It imports `{ setCtx, TsxContext }` from the vendor bundle (`/__wfw/v/bundle`) via importmap
1524
+ - Then dynamically imports the page component: `await import('/__ssr/[hash].js')`
1525
+ - The vendor bundle (react + react-dom + weifuwu client libs) is compiled once and cached
1526
+ - Page components are pre-compiled to `/__ssr/{hash}.js` — no runtime esbuild after first request
1527
+ - **Dev:** `createRoot` + render; **Production:** `hydrateRoot` (reuses SSR DOM)
1528
+ - Both the hydration script and the page component share the same store via `globalThis.__WEIFUWU_CTX_STORE`
1456
1529
  - Tailwind CSS served at `/__wfw/style/{hash}.css` (cached, content-hashed)
1457
- - Dev mode: vendor bundle, HMR WebSocket, file watcher all automatic
1458
- - Page components and layouts are compiled via esbuild at runtime — no build step needed
1530
+ - Dev mode extras: HMR WebSocket, file watcher, hot component replacement
1459
1531
 
1460
1532
  ```ts
1461
1533
  // Multiple independent SSR directories
@@ -1503,13 +1575,23 @@ app.post('/upload', upload({ dir: './uploads', maxFileSize: 10_485_760, allowedT
1503
1575
 
1504
1576
  ### user [β]
1505
1577
 
1506
- Authentication: register, login, JWT, OAuth2.
1578
+ Authentication: register, login, JWT, OAuth2 服务端, 社会化登录.
1507
1579
 
1508
1580
  ```ts
1509
- const auth = user({ pg, jwtSecret: process.env.JWT_SECRET! })
1510
- await auth.migrate()
1511
- app.use('/auth', auth) // POST /register, POST /login, OAuth2 routes
1512
- app.get('/me', auth.middleware(), (req, ctx) => Response.json(ctx.user))
1581
+ const u = user({
1582
+ pg,
1583
+ jwtSecret: process.env.JWT_SECRET!,
1584
+ oauthLogin: { // 可选 社会化登录
1585
+ providers: {
1586
+ github: { clientId: '...', clientSecret: '...' },
1587
+ google: { clientId: '...', clientSecret: '...' },
1588
+ },
1589
+ },
1590
+ })
1591
+ await u.migrate()
1592
+ app.use(u) // POST /register, POST /login
1593
+ app.use(u.middleware()) // ctx.user
1594
+ // GET /auth/github, GET /auth/github/callback (如配置 oauthLogin)
1513
1595
  ```
1514
1596
 
1515
1597
  | Option | Type | Default | Description |
@@ -1517,8 +1599,9 @@ app.get('/me', auth.middleware(), (req, ctx) => Response.json(ctx.user))
1517
1599
  | `pg` | `object` | — | PostgreSQL client |
1518
1600
  | `jwtSecret` | `string` | — | JWT signing secret |
1519
1601
  | `table` | `string` | `'_users'` | Users table name |
1520
- | `expiresIn` | `string` | `'7d'` | JWT expiration |
1521
- | `oauth2` | `object` | — | OAuth2 client config (PKCE flow) |
1602
+ | `expiresIn` | `string` | `'24h'` | JWT expiration |
1603
+ | `oauth2` | `object` | — | OAuth2 服务端 config (PKCE flow) |
1604
+ | `oauthLogin` | `object` | — | 社会化登录: `{ providers: Record<string, OAuthProviderConfig>, redirectUrl? }` |
1522
1605
 
1523
1606
  | Method | Description |
1524
1607
  |--------|-------------|
@@ -1527,6 +1610,54 @@ app.get('/me', auth.middleware(), (req, ctx) => Response.json(ctx.user))
1527
1610
  | `.verify(token)` | Verify JWT token |
1528
1611
  | `.middleware()` | JWT verify middleware — sets `ctx.user` |
1529
1612
 
1613
+ ### permissions [α] — RBAC
1614
+
1615
+ Role-based access control.
1616
+
1617
+ ```ts
1618
+ const perm = permissions({ pg })
1619
+ await perm.migrate()
1620
+
1621
+ // Assign roles & permissions
1622
+ await perm.assignRole(userId, 'admin')
1623
+ await perm.grantPermission('admin', 'posts:create')
1624
+ await perm.grantPermission('admin', 'posts:edit')
1625
+ await perm.grantPermission('admin', '*') // wildcard — all permissions
1626
+
1627
+ // Use as middleware
1628
+ app.use((req, ctx, next) => { ctx.user = { id: userId }; return next(req, ctx) })
1629
+ app.use(perm) // → ctx.roles, ctx.permissions
1630
+
1631
+ // Route guards
1632
+ app.get('/admin', perm.requireRole('admin'), adminHandler)
1633
+ app.post('/posts', perm.requirePermission('posts:create'), createHandler)
1634
+
1635
+ // Handler-level check
1636
+ app.get('/posts/:id', async (req, ctx) => {
1637
+ if (!ctx.permissions.has('posts:read')) {
1638
+ return Response.json({ error: 'forbidden' }, { status: 403 })
1639
+ }
1640
+ return Response.json(post)
1641
+ })
1642
+ ```
1643
+
1644
+ | Option | Type | Default | Description |
1645
+ |--------|------|---------|-------------|
1646
+ | `pg` | `object` | — | PostgreSQL client |
1647
+ | `prefix` | `string` | `''` | Table prefix (e.g. `'myapp'` → `myapp_roles`) |
1648
+
1649
+ | Method | Description |
1650
+ |--------|-------------|
1651
+ | `.assignRole(userId, role)` | Assign role to user (creates role if missing) |
1652
+ | `.removeRole(userId, role)` | Remove role from user |
1653
+ | `.grantPermission(role, permission)` | Grant permission to role |
1654
+ | `.revokePermission(role, permission)` | Revoke permission from role |
1655
+ | `.getUserRoles(userId)` | List user's roles |
1656
+ | `.getUserPermissions(userId)` | List user's permissions (union of all roles) |
1657
+ | `.requireRole(...roles)` | Middleware — rejects if user lacks any of the roles |
1658
+ | `.requirePermission(...perms)` | Middleware — rejects if user lacks any permission |
1659
+ | `.migrate()` | Create tables |
1660
+
1530
1661
  ### validate [α]
1531
1662
 
1532
1663
  ```ts
@@ -1652,7 +1783,7 @@ const count = useStore(s => s.count)
1652
1783
  <Head><title>Page Title</title><meta name="description" content="..." /></Head>
1653
1784
  ```
1654
1785
 
1655
- **`TsxContext`** — React context holding page data (`params`, `query`, `user`, `parsed`, `prefs`, `env`). Used internally by hooks; rarely needed directly.
1786
+ **`TsxContext`** — React context holding page data (`params`, `query`, `user`, `parsed`, `theme`, `i18n`, `flash`, `loaderData`, `env`). Used internally by hooks; rarely needed directly.
1656
1787
 
1657
1788
  ### Locale & Theme
1658
1789
 
@@ -1666,7 +1797,7 @@ function LangSwitch() {
1666
1797
 
1667
1798
  | Return | Description |
1668
1799
  |--------|-------------|
1669
- | `locale` | Current locale string (from `ctx.prefs.locale`) |
1800
+ | `locale` | Current locale string (from `ctx.i18n.locale`) |
1670
1801
  | `setLocale(locale)` | Switch locale (calls `navigate('/__lang/' + locale)`) |
1671
1802
  | `t` | Translate a key using loaded locale messages |
1672
1803
 
@@ -1705,7 +1836,7 @@ function Page() {
1705
1836
  }
1706
1837
  ```
1707
1838
 
1708
- On the server, data flows from middleware → `ctx` → `ctx.loaderData` (serialized). On the client, it's restored from `window.__WEIFUWU_CTX`. Under the hood, `useLoaderData()` uses `AsyncLocalStorage` on the server and `window.__WEIFUWU_CTX` on the client — no SSR-specific code needed in your components.
1839
+ 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 (`globalThis.__WEIFUWU_CTX_STORE`). `useLoaderData()` reads from the snapshot via `useSyncExternalStore` — no SSR-specific code needed in your components.
1709
1840
 
1710
1841
  **`addInterceptor(fn)`** — Register a URL interceptor. Interceptors run before SPA navigation; if one returns `true`, `navigate()` skips the fetch-and-swap.
1711
1842
 
@@ -1721,8 +1852,21 @@ addInterceptor(async (url) => {
1721
1852
  ### Flash messages
1722
1853
 
1723
1854
  ```ts
1724
- // Server — set flash cookie on redirect, auto-cleared after first read
1725
- return ctx.setPref('flash', JSON.stringify({ type: 'success', message: 'Done' })) // 302 + Set-Cookie
1855
+ import { flash } from 'weifuwu'
1856
+
1857
+ app.use(flash())
1858
+
1859
+ // Read flash
1860
+ app.get('/', (req, ctx) => {
1861
+ const msg = ctx.flash.value // { type: 'success', text: 'Saved!' }
1862
+ })
1863
+
1864
+ // Set flash + redirect
1865
+ app.post('/save', async (req, ctx) => {
1866
+ await saveArticle()
1867
+ return ctx.flash.set({ type: 'success', text: '已保存' }, '/articles')
1868
+ // → 302 /articles + Set-Cookie flash=...
1869
+ })
1726
1870
  ```
1727
1871
 
1728
1872
  ```tsx
@@ -1730,36 +1874,91 @@ return ctx.setPref('flash', JSON.stringify({ type: 'success', message: 'Done' })
1730
1874
  import { useFlashMessage } from 'weifuwu/react'
1731
1875
 
1732
1876
  function Toast() {
1733
- const flash = useFlashMessage<{ type: string; message: string }>()
1877
+ const flash = useFlashMessage<{ type: string; text: string }>()
1734
1878
  if (!flash) return null
1735
- return <div className={`toast toast-${flash.type}`}>{flash.message}</div>
1879
+ return <div className={`toast toast-${flash.type}`}>{flash.text}</div>
1736
1880
  }
1737
1881
  ```
1738
1882
 
1883
+ | Option | Type | Default | Description |
1884
+ |--------|------|---------|-------------|
1885
+ | `name` | `string` | `'flash'` | Cookie name |
1886
+
1739
1887
  ### Dev mode
1740
1888
 
1741
- Auto-detected when `NODE_ENV !== 'production'`. `ssr({dir})` automatically registers vendor bundle, HMR WebSocket, and file watcher. No explicit setup needed.
1889
+ Auto-detected when `NODE_ENV === 'development'`. `ssr({dir})` automatically registers importmap, vendor bundle, HMR WebSocket, and file watcher. No explicit setup needed.
1742
1890
 
1743
- When a `.tsx` or `.css` file changes under the `ssr` dir, the browser hot-updates without refreshing — `useState` values are preserved. Layout changes trigger a full page reload.
1891
+ - Inline hydration script uses `createRoot` + render (replaces SSR DOM)
1892
+ - Vendor bundle served at `/__wfw/v/bundle?h=<hash>` — compiled from source, unminified
1893
+ - Hot component replacement: file changes → WebSocket message → browser imports hot bundle → `__WFW_REFRESH(NewComponent)` — `useState` values preserved
1894
+ - Tailwind CSS hot-reloads without page refresh
1895
+ - Layout changes trigger a full page reload
1744
1896
 
1745
1897
  ---
1746
1898
 
1747
1899
  ## AI
1748
1900
 
1749
1901
  ```ts
1750
- import { openai, streamText, generateText, streamObject, generateObject, tool, embed, embedMany } from 'weifuwu'
1902
+ import { openai, streamText, generateText, streamObject, generateObject, tool, embed, embedMany, aiProvider } from 'weifuwu'
1751
1903
  import { runWorkflow } from 'weifuwu'
1904
+
1905
+ const provider = aiProvider()
1752
1906
  ```
1753
1907
 
1754
1908
  For AI streaming endpoints see [`aiStream`](#aistream-β). For AI agent APIs see [`agent`](#agent-β).
1755
1909
 
1910
+ ### aiProvider [α] — AI model & embedding configuration
1911
+
1912
+ ```ts
1913
+ const provider = aiProvider() // auto from env
1914
+ app.use(provider) // → ctx.ai
1915
+
1916
+ // Handler
1917
+ app.post('/ask', async (req, ctx) => {
1918
+ const { text } = await ctx.ai.generateText({ prompt: 'hello' })
1919
+ const vec = await ctx.ai.embed('some text')
1920
+ const stream = ctx.ai.streamText({ system: 'assistant', messages: [...] })
1921
+ return stream.toTextStreamResponse()
1922
+ })
1923
+ ```
1924
+
1925
+ | Option | Type | Default | Description |
1926
+ |--------|------|---------|-------------|
1927
+ | `baseURL` | `string` | `OPENAI_BASE_URL` env or `http://localhost:11434/v1` | API base URL |
1928
+ | `apiKey` | `string` | `OPENAI_API_KEY` env or `'ollama'` | API key |
1929
+ | `model` | `string` | `OPENAI_MODEL` env or `'qwen3:0.6b'` | Chat model name |
1930
+ | `embeddingModel` | `string` | `OPENAI_EMBEDDING_MODEL` env or `'qwen3-embedding:0.6b'` | Embedding model name |
1931
+ | `embeddingDimension` | `number` | `EMBEDDING_DIMENSION` env or `1024` | Vector dimension |
1932
+
1933
+ | Method | Description |
1934
+ |--------|-------------|
1935
+ | `.model(name?)` | Get `LanguageModel` instance |
1936
+ | `.embeddingModel(name?)` | Get `EmbeddingModel` instance |
1937
+ | `.embed(text)` | Embed single text → `Promise<number[]>` |
1938
+ | `.embedMany(texts)` | Batch embed → `Promise<number[][]>` |
1939
+ | `.generateText(params)` | Generate text (model auto-injected) |
1940
+ | `.streamText(params)` | Stream text (model auto-injected) |
1941
+ | `.dimension` | Configured embedding dimension |
1942
+
1756
1943
  ### DAG Workflow
1757
1944
 
1758
1945
  ```ts
1759
1946
  const tools = { queryUser: tool({ ... }) }
1760
- const wf = runWorkflow({ tools })
1947
+
1948
+ // Via provider:
1949
+ const wf = runWorkflow({ tools, provider })
1950
+
1951
+ // Or explicit model:
1952
+ const wf = runWorkflow({ tools, model: openai('gpt-4o') })
1761
1953
  ```
1762
1954
 
1955
+ | Option | Type | Default | Description |
1956
+ |--------|------|---------|-------------|
1957
+ | `tools` | `object` | — | Registered tool definitions |
1958
+ | `provider` | `AIProvider` | — | AI provider (uses `provider.model()` for LLM-generated workflow) |
1959
+ | `model` | `LanguageModel` | — | Explicit model (overrides provider) |
1960
+ | `maxSteps` | `number` | `200` | Max execution steps |
1961
+
1763
1962
  ---
1764
1963
 
1765
1964
  ## Server-Sent Events
@@ -1789,7 +1988,7 @@ currentTraceId, currentTrace, runWithTrace, traceElapsed, TraceContext,
1789
1988
 
1790
1989
  ```ts
1791
1990
  auth, cors, csrf, compress, helmet, logger, rateLimit, requestId, validate, upload,
1792
- preferences, serveStatic, session, MemoryStore, RedisStore, SessionStore,
1991
+ theme, i18n, flash, permissions, serveStatic, session, MemoryStore, RedisStore, SessionStore,
1793
1992
  cache, MemoryCache, RedisCache, CacheStore
1794
1993
  ```
1795
1994
 
@@ -1811,7 +2010,7 @@ eq, ne, gt, gte, lt, lte, isNull, isNotNull, like, contains, in_, and, or, not
1811
2010
  ### Client-side (from `'weifuwu/react'`)
1812
2011
 
1813
2012
  ```ts
1814
- TsxContext, useLoaderData,
2013
+ TsxContext, setCtx, useCtx, addCtxRebuilder, useLoaderData,
1815
2014
  useWebsocket, useAction, useFetch, useQueryState, createStore,
1816
2015
  Link, useNavigate, useNavigating, addInterceptor,
1817
2016
  useLocale, useTheme, applyTheme, useFlashMessage,
@@ -1820,6 +2019,12 @@ Head
1820
2019
  ```
1821
2020
  export type { UseAgentStreamOptions, UseAgentStreamReturn, AgentStreamState } from 'weifuwu/react'
1822
2021
 
2022
+ ### AI Provider (framework abstraction)
2023
+
2024
+ ```ts
2025
+ aiProvider, AIProvider, AIProviderOptions
2026
+ ```
2027
+
1823
2028
  ### AI SDK (re-exported from `ai`)
1824
2029
 
1825
2030
  ```ts
@@ -1831,8 +2036,8 @@ openai, createOpenAI
1831
2036
  ### Other modules
1832
2037
 
1833
2038
  ```ts
1834
- preferences, health, analytics, seo, seoMiddleware, seoTags,
1835
- user, mailer, graphql, aiStream, runWorkflow,
2039
+ theme, i18n, flash, health, analytics, seo, seoMiddleware, seoTags,
2040
+ user, mailer, graphql, aiStream, runWorkflow, knowledgeBase, permissions, queue,
1836
2041
  logdb, messager, agent, iii, createWorker, registerWorker,
1837
2042
  opencode, deploy, defineConfig, webhook,
1838
2043
  testApp, TestApp, TestRequest, TestResponse,