weifuwu 0.22.3 → 0.23.1
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 +292 -87
- package/cli/template/.weifuwu/ssr/2e3a7e60.js +112 -0
- package/cli/template/.weifuwu/ssr/560568d7.js +14 -0
- package/cli/template/app.ts +3 -2
- package/cli/template/index.ts +2 -1
- package/dist/agent/run.d.ts +4 -3
- package/dist/agent/types.d.ts +3 -0
- package/dist/ai/provider.d.ts +36 -0
- package/dist/ai/utils.d.ts +5 -0
- package/dist/ai/workflow.d.ts +3 -0
- package/dist/ai.d.ts +9 -1
- package/dist/client-locale.d.ts +1 -1
- package/dist/client-router.d.ts +3 -3
- package/dist/compile.d.ts +6 -0
- package/dist/cron-utils.d.ts +8 -0
- package/dist/flash.d.ts +24 -0
- package/dist/i18n.d.ts +14 -0
- package/dist/index.d.ts +13 -7
- package/dist/index.js +1351 -820
- package/dist/kb/index.d.ts +3 -0
- package/dist/kb/types.d.ts +64 -0
- package/dist/permissions.d.ts +49 -0
- package/dist/queue/types.d.ts +12 -6
- package/dist/react.d.ts +1 -1
- package/dist/react.js +91 -86
- package/dist/session.d.ts +0 -1
- package/dist/ssr.d.ts +0 -1
- package/dist/stream.d.ts +5 -5
- package/dist/theme.d.ts +8 -0
- package/dist/tsx-context.d.ts +15 -1
- package/dist/types.d.ts +5 -3
- package/dist/user/index.d.ts +1 -1
- package/dist/user/oauth-login.d.ts +21 -0
- package/dist/user/types.d.ts +31 -0
- package/package.json +1 -1
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. `
|
|
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
|
-
| `
|
|
160
|
-
| `
|
|
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
|
-
| `
|
|
164
|
-
| `
|
|
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()`, `
|
|
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
|
|
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
|
-
| `
|
|
363
|
-
| `
|
|
364
|
-
| `
|
|
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
|
|
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
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
-
| `
|
|
774
|
-
| `
|
|
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.
|
|
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
|
-
###
|
|
1131
|
+
### theme [α]
|
|
1131
1132
|
|
|
1132
|
-
|
|
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
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1152
|
-
// Client-side no-refresh switching — import enables it automatically
|
|
1153
|
-
import { useLocale, useTheme } from 'weifuwu/react'
|
|
1158
|
+
### i18n [α]
|
|
1154
1159
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
//
|
|
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
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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
|
-
| `
|
|
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
|
-
| `
|
|
1174
|
-
| `
|
|
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
|
-
| `.
|
|
1179
|
-
| `.
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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:
|
|
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
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
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` | `'
|
|
1521
|
-
| `oauth2` | `object` | — | OAuth2
|
|
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`, `
|
|
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.
|
|
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` → `
|
|
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
|
-
|
|
1725
|
-
|
|
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;
|
|
1877
|
+
const flash = useFlashMessage<{ type: string; text: string }>()
|
|
1734
1878
|
if (!flash) return null
|
|
1735
|
-
return <div className={`toast toast-${flash.type}`}>{flash.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|