weifuwu 0.22.2 → 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 +521 -76
- package/cli/template/.weifuwu/ssr/2e3a7e60.js +112 -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/auth.d.ts +14 -0
- package/dist/client-locale.d.ts +1 -1
- package/dist/client-router.d.ts +3 -3
- package/dist/client-theme.d.ts +1 -1
- 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 +15 -3
- package/dist/index.js +1597 -495
- package/dist/kb/index.d.ts +3 -0
- package/dist/kb/types.d.ts +64 -0
- package/dist/kb.d.ts +70 -0
- package/dist/oauth-client.d.ts +41 -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/s3.d.ts +68 -0
- package/dist/serve.d.ts +1 -1
- package/dist/session.d.ts +12 -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 +7 -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 +3 -1
package/README.md
CHANGED
|
@@ -60,10 +60,20 @@ await server.ready
|
|
|
60
60
|
| `shutdown` | `boolean` | `true` | Auto SIGTERM/SIGINT |
|
|
61
61
|
|
|
62
62
|
```ts
|
|
63
|
-
interface Server {
|
|
63
|
+
interface Server {
|
|
64
|
+
stop: (timeoutMs?: number) => Promise<void> // graceful: waits for in-flight, force-closes after timeoutMs (default 10s)
|
|
65
|
+
readonly port: number
|
|
66
|
+
readonly hostname: string
|
|
67
|
+
ready: Promise<void>
|
|
68
|
+
}
|
|
64
69
|
const { server, url } = await createTestServer(handler)
|
|
65
70
|
```
|
|
66
71
|
|
|
72
|
+
`server.stop()` performs a graceful shutdown: stops accepting new connections,
|
|
73
|
+
closes idle keep-alive sockets, then waits for in-flight requests to complete.
|
|
74
|
+
If they don't finish within `timeoutMs` (default 10 seconds), remaining connections
|
|
75
|
+
are forcibly closed. SIGTERM/SIGINT use the same graceful pattern.
|
|
76
|
+
|
|
67
77
|
### Router
|
|
68
78
|
|
|
69
79
|
```ts
|
|
@@ -113,7 +123,7 @@ Request → serve() → app.handler() → global middleware × N → path middle
|
|
|
113
123
|
|
|
114
124
|
1. `serve()` receives HTTP request
|
|
115
125
|
2. `app.handler()` creates `ctx = { params, query }` and routes to the matching trie node
|
|
116
|
-
3. **Global middleware** runs in `use()` order (e.g. `
|
|
126
|
+
3. **Global middleware** runs in `use()` order (e.g. `theme()`, `i18n()`, `postgres()`, `cors()`)
|
|
117
127
|
4. **Path‑scoped middleware** runs for matching paths (e.g. `app.use('/admin', authMW)`)
|
|
118
128
|
5. **Route‑level middleware** runs (e.g. `app.get('/admin', validate(...), handler)`)
|
|
119
129
|
6. **Route handler** returns `Response` — middleware chain unwinds
|
|
@@ -142,22 +152,23 @@ The `ctx` object accumulates properties as it passes through the middleware chai
|
|
|
142
152
|
| `csrfToken` | `csrf()` | `string` | CSRF token |
|
|
143
153
|
| `requestId` | `requestId()` | `string` | Request ID |
|
|
144
154
|
| `session` | `session()` | `Session` | Session data object |
|
|
145
|
-
| `sessionId` | `session()` | `string` | Session ID |
|
|
146
155
|
| `sql` | `postgres()` | `Sql<{}>` | PostgreSQL tagged-template client |
|
|
147
156
|
| `redis` | `redis()` | `Redis` | Redis client |
|
|
157
|
+
| `ai` | `aiProvider()` | `AIProvider` | AI model & embedding |
|
|
148
158
|
| `queue` | `queue()` | `Queue` | Job queue |
|
|
149
|
-
| `
|
|
150
|
-
| `
|
|
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 |
|
|
151
168
|
| `layoutStack` | `ssr()` internal | `LayoutEntry[]` | React layout component stack |
|
|
152
169
|
| `loaderData` | User middleware | `Record<string, unknown>` | SSR data passed to client |
|
|
153
|
-
| `
|
|
154
|
-
| `
|
|
155
|
-
| `t` | `preferences()` | `(key) => string` | Translation function |
|
|
156
|
-
| `setPref` | `preferences()` | `(key, val) => Response` | Set preference cookie + redirect |
|
|
157
|
-
| `compiledTailwindCss` | `ssr()` internal | `string` | Compiled CSS content (internal) |
|
|
158
|
-
| `tailwindCssUrl` | `ssr()` internal | `string` | Compiled CSS route URL (internal) |
|
|
159
|
-
| `session` | `session()` | `Session` | Session data object |
|
|
160
|
-
| `sessionId` | `session()` | `string` | Session ID |
|
|
170
|
+
| `mountPath` | `Router` | `string` | Sub-router mount path |
|
|
171
|
+
| `deploy` | `deploy()` | `{ appName? }` | Deploy gateway info |
|
|
161
172
|
|
|
162
173
|
### Type-Safe Context
|
|
163
174
|
|
|
@@ -186,7 +197,7 @@ All modules follow one of **2 patterns** — learn these and you know every modu
|
|
|
186
197
|
|
|
187
198
|
| Pattern | How to mount | Example |
|
|
188
199
|
|---------|-------------|---------|
|
|
189
|
-
| `[α]` | `app.use(mod())` | `compress()`, `
|
|
200
|
+
| `[α]` | `app.use(mod())` | `compress()`, `theme()`, `postgres()` |
|
|
190
201
|
| `[β]` | `app.use('/path', mod())` | `health()`, `ssr({dir})`, `graphql(handler)`, `user()` |
|
|
191
202
|
|
|
192
203
|
### Pattern α — Middleware
|
|
@@ -339,7 +350,8 @@ Uses `TEST_DATABASE_URL` or `DATABASE_URL`. Automatically skipped in CI if unset
|
|
|
339
350
|
### agent [β]
|
|
340
351
|
|
|
341
352
|
```ts
|
|
342
|
-
const
|
|
353
|
+
const provider = aiProvider()
|
|
354
|
+
const a = agent({ pg, provider })
|
|
343
355
|
await a.migrate()
|
|
344
356
|
app.use('/api', a)
|
|
345
357
|
await a.addKnowledge(agentId, 'Title', 'some knowledge content')
|
|
@@ -349,9 +361,10 @@ a.run(agentId, { input: 'summarize the data', stream: true })
|
|
|
349
361
|
| Option | Type | Default | Description |
|
|
350
362
|
|--------|------|---------|-------------|
|
|
351
363
|
| `pg` | `object` | — | PostgreSQL client |
|
|
352
|
-
| `
|
|
353
|
-
| `
|
|
354
|
-
| `
|
|
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 |
|
|
355
368
|
| `tools` | `object[]` | — | Custom tool definitions |
|
|
356
369
|
|
|
357
370
|
| Method | Description |
|
|
@@ -366,13 +379,15 @@ a.run(agentId, { input: 'summarize the data', stream: true })
|
|
|
366
379
|
Creates an AI streaming chat endpoint using the Vercel AI SDK.
|
|
367
380
|
|
|
368
381
|
```ts
|
|
369
|
-
const
|
|
382
|
+
const provider = aiProvider()
|
|
383
|
+
const chat = await aiStream(async (req) => ({ messages: (await req.json()).messages }), provider)
|
|
370
384
|
app.use('/chat', chat)
|
|
371
385
|
```
|
|
372
386
|
|
|
373
387
|
| Param | Type | Description |
|
|
374
388
|
|-------|------|-------------|
|
|
375
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 |
|
|
376
391
|
|
|
377
392
|
### analytics [β]
|
|
378
393
|
|
|
@@ -404,6 +419,16 @@ app.use(auth({ token: 'sk-123' })) // static token
|
|
|
404
419
|
app.use(auth({ header: 'X-API-Key', token: 'my-key' })) // custom header
|
|
405
420
|
app.use(auth({ verify: async (token, req) => ({ sub: 'abc' }) })) // custom verify → sets ctx.user
|
|
406
421
|
app.get('/protected', auth({ proxy: 'http://auth:3000/validate' }), handler)
|
|
422
|
+
|
|
423
|
+
// Session-based auth (must be placed after session() middleware)
|
|
424
|
+
app.use(session())
|
|
425
|
+
app.use(auth({
|
|
426
|
+
session: true,
|
|
427
|
+
resolveUser: async (userId) => { // load user from DB
|
|
428
|
+
const [user] = await sql`SELECT * FROM users WHERE id = ${userId}`
|
|
429
|
+
return user ?? null // null → destroy stale session
|
|
430
|
+
},
|
|
431
|
+
}))
|
|
407
432
|
```
|
|
408
433
|
|
|
409
434
|
| Option | Type | Default | Description |
|
|
@@ -412,6 +437,13 @@ app.get('/protected', auth({ proxy: 'http://auth:3000/validate' }), handler)
|
|
|
412
437
|
| `header` | `string` | `'Authorization'` | Header name |
|
|
413
438
|
| `verify` | `(token, req) => object\|null` | — | Verify function, return value sets `ctx.user` |
|
|
414
439
|
| `proxy` | `string` | — | Auth service URL to proxy requests to |
|
|
440
|
+
| `session` | `boolean` | `false` | Enable session-based auth. Checks `ctx.session.userId` first |
|
|
441
|
+
| `resolveUser` | `(userId) => object\|null` | — | Load user from userId (called when `session: true`). Return falsy to reject + auto-destroy stale session |
|
|
442
|
+
|
|
443
|
+
When `session: true`, auth checks `ctx.session.userId` before the
|
|
444
|
+
Authorization header. This lets logged-in users authenticate via their
|
|
445
|
+
session cookie without sending a token. Falls back to header/token auth
|
|
446
|
+
if no session userId is present.
|
|
415
447
|
|
|
416
448
|
### compress [α]
|
|
417
449
|
|
|
@@ -699,6 +731,60 @@ await engine.trigger({ function_id: 'orders::create', payload: { items: ['apple'
|
|
|
699
731
|
|
|
700
732
|
|
|
701
733
|
|
|
734
|
+
### knowledgeBase [β] — RAG with pgvector
|
|
735
|
+
|
|
736
|
+
```ts
|
|
737
|
+
import { knowledgeBase, aiProvider } from 'weifuwu'
|
|
738
|
+
|
|
739
|
+
const kb = knowledgeBase({
|
|
740
|
+
pg: postgres(),
|
|
741
|
+
provider: aiProvider(),
|
|
742
|
+
table: 'my_docs',
|
|
743
|
+
})
|
|
744
|
+
|
|
745
|
+
// Create table + HNSW index (safe to call multiple times)
|
|
746
|
+
await kb.migrate()
|
|
747
|
+
|
|
748
|
+
// Ingest a document (auto chunk → embed → store)
|
|
749
|
+
await kb.ingest('docs/intro.md', `# Welcome\n\nThis is the introduction...`, {
|
|
750
|
+
title: 'Introduction',
|
|
751
|
+
metadata: { source: 'docs', author: 'alice' },
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
// Semantic search
|
|
755
|
+
const results = await kb.search('how to get started?', { limit: 5 })
|
|
756
|
+
// → [{ key, title, content, score: 0.92, metadata }, ...]
|
|
757
|
+
|
|
758
|
+
// Delete
|
|
759
|
+
await kb.delete('docs/outdated.md')
|
|
760
|
+
|
|
761
|
+
// List all documents
|
|
762
|
+
const entries = await kb.list()
|
|
763
|
+
// → [{ key, title, chunks: 3 }, ...]
|
|
764
|
+
|
|
765
|
+
// Use as middleware (injects ctx.kb.search)
|
|
766
|
+
app.use(kb.middleware())
|
|
767
|
+
app.get('/search', async (req, ctx) => {
|
|
768
|
+
const results = await ctx.kb.search(ctx.query.q)
|
|
769
|
+
return Response.json(results)
|
|
770
|
+
})
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
| Option | Type | Default | Description |
|
|
774
|
+
|--------|------|---------|-------------|
|
|
775
|
+
| `pg` | `PostgresClient` | — | **Required.** PostgreSQL client |
|
|
776
|
+
| `provider` | `AIProvider` | — | **Required.** AI provider for embedding |
|
|
777
|
+
| `table` | `string` | `'_kb_docs'` | Database table name |
|
|
778
|
+
| `chunkSize` | `number` | `512` | Max characters per chunk |
|
|
779
|
+
| `chunkOverlap` | `number` | `64` | Overlap between chunks |
|
|
780
|
+
| `searchLimit` | `number` | `5` | Default search result count |
|
|
781
|
+
| `searchThreshold` | `number` | `0` | Minimum similarity (0–1) |
|
|
782
|
+
|
|
783
|
+
Documents are split on paragraph boundaries (`\n\n`). Re-ingesting the same key
|
|
784
|
+
replaces old chunks. Provider's `embed()` is used automatically.
|
|
785
|
+
The HNSW index enables fast approximate nearest-neighbor search (cosine distance).
|
|
786
|
+
|
|
787
|
+
|
|
702
788
|
### logdb [β]
|
|
703
789
|
|
|
704
790
|
PostgreSQL structured event logging with monthly partitioning.
|
|
@@ -746,6 +832,72 @@ await mail.send({ to: 'user@test.com', subject: 'Hello', text: 'Body', html: '<p
|
|
|
746
832
|
| `from` | `string` | — | Default sender address |
|
|
747
833
|
| `send` | `function` | — | Custom send function (alternative to transport) |
|
|
748
834
|
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
### oauthClient [β] — Social login (OAuth 2.0 client)
|
|
838
|
+
|
|
839
|
+
```ts
|
|
840
|
+
import { oauthClient } from 'weifuwu'
|
|
841
|
+
|
|
842
|
+
app.use(session()) // required — stores OAuth state
|
|
843
|
+
app.use(user({ pg, jwtSecret })) // required — user management
|
|
844
|
+
app.use('/auth', oauthClient({ // mounts /auth/google, /auth/google/callback
|
|
845
|
+
pg,
|
|
846
|
+
jwtSecret,
|
|
847
|
+
redirectUrl: '/dashboard',
|
|
848
|
+
providers: {
|
|
849
|
+
google: {
|
|
850
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
851
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
852
|
+
},
|
|
853
|
+
github: {
|
|
854
|
+
clientId: process.env.GITHUB_CLIENT_ID,
|
|
855
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
|
856
|
+
},
|
|
857
|
+
},
|
|
858
|
+
}))
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
**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).
|
|
862
|
+
|
|
863
|
+
Supports custom providers via `authUrl`, `tokenUrl`, `userUrl`, and `parseUser`:
|
|
864
|
+
|
|
865
|
+
```ts
|
|
866
|
+
app.use('/auth', oauthClient({
|
|
867
|
+
pg,
|
|
868
|
+
jwtSecret,
|
|
869
|
+
providers: {
|
|
870
|
+
discord: {
|
|
871
|
+
clientId: process.env.DISCORD_CLIENT_ID,
|
|
872
|
+
clientSecret: process.env.DISCORD_CLIENT_SECRET,
|
|
873
|
+
authUrl: 'https://discord.com/api/oauth2/authorize',
|
|
874
|
+
tokenUrl: 'https://discord.com/api/oauth2/token',
|
|
875
|
+
userUrl: 'https://discord.com/api/users/@me',
|
|
876
|
+
parseUser: (data) => ({
|
|
877
|
+
id: data.id,
|
|
878
|
+
email: data.email ?? '',
|
|
879
|
+
name: data.global_name ?? data.username,
|
|
880
|
+
avatarUrl: data.avatar
|
|
881
|
+
? `https://cdn.discordapp.com/avatars/${data.id}/${data.avatar}.png`
|
|
882
|
+
: '',
|
|
883
|
+
}),
|
|
884
|
+
},
|
|
885
|
+
},
|
|
886
|
+
}))
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
| Option | Type | Default | Description |
|
|
890
|
+
|--------|------|---------|-------------|
|
|
891
|
+
| `pg` | `PostgresClient` | — | **Required.** Database connection |
|
|
892
|
+
| `jwtSecret` | `string` | — | **Required.** Must match `user()` module's secret |
|
|
893
|
+
| `providers` | `Record<string, OAuthProviderConfig>` | — | **Required.** Provider configs (Google/GitHub built-in, any custom) |
|
|
894
|
+
| `redirectUrl` | `string` | `'/'` | Post-login redirect destination |
|
|
895
|
+
| `expiresIn` | `string \| number` | `'24h'` | JWT expiry |
|
|
896
|
+
| `table` | `string` | `'_auth_providers'` | Provider-user link table name |
|
|
897
|
+
|
|
898
|
+
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`.
|
|
899
|
+
|
|
900
|
+
|
|
749
901
|
### messager [β]
|
|
750
902
|
|
|
751
903
|
Real-time chat with channels, WebSocket, agent routing.
|
|
@@ -976,56 +1128,110 @@ await fts.dropIndex(pg.sql, articles)
|
|
|
976
1128
|
|
|
977
1129
|
Search options: `fields`, `limit` (20), `offset` (0), `headline` (false), `language` ('english'), `minRank`.
|
|
978
1130
|
|
|
979
|
-
###
|
|
1131
|
+
### theme [α]
|
|
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
|
+
```
|
|
980
1142
|
|
|
981
|
-
|
|
1143
|
+
| Option | Type | Default | Description |
|
|
1144
|
+
|--------|------|---------|-------------|
|
|
1145
|
+
| `default` | `string` | `'system'` | Default theme |
|
|
1146
|
+
| `cookie` | `string` | `'theme'` | Cookie name (empty to disable) |
|
|
982
1147
|
|
|
983
1148
|
```ts
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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
|
+
})
|
|
989
1154
|
```
|
|
990
1155
|
|
|
991
|
-
|
|
992
|
-
|--------|---------|-------------|
|
|
993
|
-
| `dir` | — | Translation JSON directory |
|
|
994
|
-
| `locale.default` | `'en'` | Fallback locale |
|
|
995
|
-
| `locale.cookie` | `'locale'` | Cookie name |
|
|
996
|
-
| `locale.fromAcceptLanguage` | `true` | Detect from header |
|
|
997
|
-
| `theme.default` | `'system'` | `'light'` \| `'dark'` \| `'system'` |
|
|
998
|
-
| `theme.cookie` | `'theme'` | Cookie name |
|
|
1156
|
+
See [`useTheme()`](#usetheme) for client-side usage.
|
|
999
1157
|
|
|
1000
|
-
|
|
1001
|
-
// Client-side no-refresh switching — import enables it automatically
|
|
1002
|
-
import { useLocale, useTheme } from 'weifuwu/react'
|
|
1158
|
+
### i18n [α]
|
|
1003
1159
|
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
//
|
|
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)
|
|
1008
1167
|
```
|
|
1009
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
|
+
|
|
1010
1187
|
### queue [α]
|
|
1011
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
|
+
|
|
1012
1195
|
```ts
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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()
|
|
1016
1219
|
```
|
|
1017
1220
|
|
|
1018
1221
|
| Option | Type | Default | Description |
|
|
1019
1222
|
|--------|------|---------|-------------|
|
|
1020
|
-
| `
|
|
1223
|
+
| `store` | `'memory' \| 'pg' \| 'redis'` | `'memory'` | Backend store |
|
|
1224
|
+
| `redis` | `object` | — | Redis client (required when `store: 'redis'`) |
|
|
1021
1225
|
| `url` | `string` | — | Redis URL (alternative to client) |
|
|
1022
|
-
| `
|
|
1023
|
-
| `
|
|
1226
|
+
| `pg` | `object` | — | PostgreSQL client (required when `store: 'pg'`) |
|
|
1227
|
+
| `prefix` | `string` | `'queue'` | Key/table prefix |
|
|
1228
|
+
| `pollInterval` | `number` | `200` | Poll interval (ms) |
|
|
1024
1229
|
|
|
1025
1230
|
| Method | Description |
|
|
1026
1231
|
|--------|-------------|
|
|
1027
|
-
| `.
|
|
1028
|
-
| `.
|
|
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 |
|
|
1029
1235
|
| `.run()` | Start processing |
|
|
1030
1236
|
| `.stop()` | Stop processing |
|
|
1031
1237
|
| `.jobs(limit?)` | List pending jobs |
|
|
@@ -1035,6 +1241,18 @@ await q.add('send-email', { to: 'user@test.com' }, { cron: '0 8 * * *' })
|
|
|
1035
1241
|
| `.dashboard()` | Returns a Router with management endpoints |
|
|
1036
1242
|
| `.close()` | Cleanup |
|
|
1037
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
|
+
|
|
1038
1256
|
**Dashboard endpoints** (mount via `app.use('/__queue', q.dashboard())`):
|
|
1039
1257
|
|
|
1040
1258
|
| Method | Path | Description |
|
|
@@ -1099,6 +1317,85 @@ app.use(requestId({ header: 'X-Request-Id', generator: () => crypto.randomUUID()
|
|
|
1099
1317
|
|
|
1100
1318
|
|
|
1101
1319
|
|
|
1320
|
+
### s3 [α] — S3-compatible object storage
|
|
1321
|
+
|
|
1322
|
+
```ts
|
|
1323
|
+
import { s3 } from 'weifuwu'
|
|
1324
|
+
|
|
1325
|
+
app.use(s3({
|
|
1326
|
+
bucket: 'my-app',
|
|
1327
|
+
region: 'us-east-1',
|
|
1328
|
+
endpoint: process.env.S3_URL, // MinIO / R2 / AWS
|
|
1329
|
+
forcePathStyle: true, // required for MinIO
|
|
1330
|
+
credentials: {
|
|
1331
|
+
accessKeyId: process.env.S3_ACCESS_KEY,
|
|
1332
|
+
secretAccessKey: process.env.S3_SECRET_KEY,
|
|
1333
|
+
},
|
|
1334
|
+
publicUrl: 'https://cdn.example.com', // for unsigned public URLs
|
|
1335
|
+
}))
|
|
1336
|
+
```
|
|
1337
|
+
|
|
1338
|
+
Injects `ctx.s3` with methods for S3-compatible object storage.
|
|
1339
|
+
|
|
1340
|
+
```ts
|
|
1341
|
+
// Upload
|
|
1342
|
+
await ctx.s3.put('images/logo.png', buffer, { contentType: 'image/png' })
|
|
1343
|
+
|
|
1344
|
+
// Download
|
|
1345
|
+
const buf = await ctx.s3.get('images/logo.png') // Buffer | null
|
|
1346
|
+
|
|
1347
|
+
// Delete
|
|
1348
|
+
await ctx.s3.delete('images/logo.png')
|
|
1349
|
+
|
|
1350
|
+
// Check existence
|
|
1351
|
+
if (await ctx.s3.exists('images/logo.png')) { ... }
|
|
1352
|
+
|
|
1353
|
+
// Signed URL (expires in 1 hour by default)
|
|
1354
|
+
const url = await ctx.s3.url('images/logo.png')
|
|
1355
|
+
const shortUrl = await ctx.s3.url('images/logo.png', { expiresIn: 60 })
|
|
1356
|
+
|
|
1357
|
+
// Public URL (requires publicUrl in options)
|
|
1358
|
+
const publicUrl = await ctx.s3.url('images/logo.png', { expiresIn: 0 })
|
|
1359
|
+
|
|
1360
|
+
// List objects under a prefix
|
|
1361
|
+
const keys = await ctx.s3.list('images/')
|
|
1362
|
+
```
|
|
1363
|
+
|
|
1364
|
+
| Option | Type | Default | Description |
|
|
1365
|
+
|--------|------|---------|-------------|
|
|
1366
|
+
| `bucket` | `string` | — | **Required.** S3 bucket name |
|
|
1367
|
+
| `region` | `string` | `'us-east-1'` | AWS region |
|
|
1368
|
+
| `endpoint` | `string` | — | Custom endpoint (MinIO, R2, B2) |
|
|
1369
|
+
| `forcePathStyle` | `boolean` | `false` | Path-style addressing (required for MinIO) |
|
|
1370
|
+
| `credentials` | `{ accessKeyId, secretAccessKey }` | — | Falls back to AWS env vars / IAM role |
|
|
1371
|
+
| `publicUrl` | `string` | — | Base URL for unsigned public URLs via `url(key, { expiresIn: 0 })` |
|
|
1372
|
+
|
|
1373
|
+
Credentials can be omitted to use AWS environment variables (`AWS_ACCESS_KEY_ID`,
|
|
1374
|
+
`AWS_SECRET_ACCESS_KEY`) or IAM roles (EC2, ECS, Lambda).
|
|
1375
|
+
|
|
1376
|
+
The module can also be used standalone without the middleware:
|
|
1377
|
+
|
|
1378
|
+
```ts
|
|
1379
|
+
const storage = s3({ bucket: 'my-app', endpoint: '...' })
|
|
1380
|
+
await storage.put('key', body)
|
|
1381
|
+
const data = await storage.get('key')
|
|
1382
|
+
```
|
|
1383
|
+
|
|
1384
|
+
Requires MinIO or another S3-compatible service for local development.
|
|
1385
|
+
Add to `docker-compose.yml`:
|
|
1386
|
+
|
|
1387
|
+
```yml
|
|
1388
|
+
minio:
|
|
1389
|
+
image: minio/minio
|
|
1390
|
+
ports:
|
|
1391
|
+
- '9000:9000'
|
|
1392
|
+
environment:
|
|
1393
|
+
MINIO_ROOT_USER: minioadmin
|
|
1394
|
+
MINIO_ROOT_PASSWORD: minioadmin
|
|
1395
|
+
command: server /data
|
|
1396
|
+
```
|
|
1397
|
+
|
|
1398
|
+
|
|
1102
1399
|
### seo [β] + seoMiddleware [α]
|
|
1103
1400
|
|
|
1104
1401
|
```ts
|
|
@@ -1155,6 +1452,16 @@ app.get('/logout', async (req, ctx) => {
|
|
|
1155
1452
|
| `cookie.sameSite` | `string` | `'lax'` | SameSite policy |
|
|
1156
1453
|
| `cookie.path` | `string` | `'/'` | Cookie path |
|
|
1157
1454
|
| `cookie.domain` | `string` | — | Cookie domain |
|
|
1455
|
+
| `secret` | `string` | — | HMAC-SHA256 sign the session cookie (`uuid.signature`). Prevents tampering **strongly recommended in production** |
|
|
1456
|
+
| `rotateInterval` | `number` | `900000` (15min) | Auto-rotate session ID to prevent fixation attacks. Set `0` to disable |
|
|
1457
|
+
|
|
1458
|
+
When `secret` is set, the cookie value is signed with HMAC-SHA256:
|
|
1459
|
+
`uuid.base64url(hmac)`. Tampered cookies are rejected and treated as new
|
|
1460
|
+
sessions (no error message, no data leak).
|
|
1461
|
+
|
|
1462
|
+
Session ID auto-rotation copies data to a new ID and deletes the old one
|
|
1463
|
+
from the store. Rotation happens transparently on the next request after
|
|
1464
|
+
`rotateInterval` has elapsed.
|
|
1158
1465
|
|
|
1159
1466
|
**Stores** are also exported for standalone use:
|
|
1160
1467
|
|
|
@@ -1171,7 +1478,7 @@ await redis.destroy('sid')
|
|
|
1171
1478
|
|
|
1172
1479
|
### ssr({ dir }) [β]
|
|
1173
1480
|
|
|
1174
|
-
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.
|
|
1175
1482
|
|
|
1176
1483
|
```ts
|
|
1177
1484
|
import { Router, ssr } from 'weifuwu'
|
|
@@ -1209,13 +1516,18 @@ app.use('/', ssr({ dir: './ui' }))
|
|
|
1209
1516
|
| `app/error.tsx` | Error boundary for that subtree |
|
|
1210
1517
|
| `app/globals.css` | Tailwind CSS entry (compiled via `@tailwindcss/postcss`) |
|
|
1211
1518
|
|
|
1212
|
-
**How
|
|
1519
|
+
**How hydration works:**
|
|
1213
1520
|
|
|
1214
1521
|
- Each page is lazy-resolved on first request — only the `page.tsx` and its layout chain are compiled
|
|
1215
|
-
-
|
|
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`
|
|
1216
1529
|
- Tailwind CSS served at `/__wfw/style/{hash}.css` (cached, content-hashed)
|
|
1217
|
-
- Dev mode:
|
|
1218
|
-
- 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
|
|
1219
1531
|
|
|
1220
1532
|
```ts
|
|
1221
1533
|
// Multiple independent SSR directories
|
|
@@ -1263,13 +1575,23 @@ app.post('/upload', upload({ dir: './uploads', maxFileSize: 10_485_760, allowedT
|
|
|
1263
1575
|
|
|
1264
1576
|
### user [β]
|
|
1265
1577
|
|
|
1266
|
-
Authentication: register, login, JWT, OAuth2
|
|
1578
|
+
Authentication: register, login, JWT, OAuth2 服务端, 社会化登录.
|
|
1267
1579
|
|
|
1268
1580
|
```ts
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
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)
|
|
1273
1595
|
```
|
|
1274
1596
|
|
|
1275
1597
|
| Option | Type | Default | Description |
|
|
@@ -1277,8 +1599,9 @@ app.get('/me', auth.middleware(), (req, ctx) => Response.json(ctx.user))
|
|
|
1277
1599
|
| `pg` | `object` | — | PostgreSQL client |
|
|
1278
1600
|
| `jwtSecret` | `string` | — | JWT signing secret |
|
|
1279
1601
|
| `table` | `string` | `'_users'` | Users table name |
|
|
1280
|
-
| `expiresIn` | `string` | `'
|
|
1281
|
-
| `oauth2` | `object` | — | OAuth2
|
|
1602
|
+
| `expiresIn` | `string` | `'24h'` | JWT expiration |
|
|
1603
|
+
| `oauth2` | `object` | — | OAuth2 服务端 config (PKCE flow) |
|
|
1604
|
+
| `oauthLogin` | `object` | — | 社会化登录: `{ providers: Record<string, OAuthProviderConfig>, redirectUrl? }` |
|
|
1282
1605
|
|
|
1283
1606
|
| Method | Description |
|
|
1284
1607
|
|--------|-------------|
|
|
@@ -1287,6 +1610,54 @@ app.get('/me', auth.middleware(), (req, ctx) => Response.json(ctx.user))
|
|
|
1287
1610
|
| `.verify(token)` | Verify JWT token |
|
|
1288
1611
|
| `.middleware()` | JWT verify middleware — sets `ctx.user` |
|
|
1289
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
|
+
|
|
1290
1661
|
### validate [α]
|
|
1291
1662
|
|
|
1292
1663
|
```ts
|
|
@@ -1412,7 +1783,7 @@ const count = useStore(s => s.count)
|
|
|
1412
1783
|
<Head><title>Page Title</title><meta name="description" content="..." /></Head>
|
|
1413
1784
|
```
|
|
1414
1785
|
|
|
1415
|
-
**`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.
|
|
1416
1787
|
|
|
1417
1788
|
### Locale & Theme
|
|
1418
1789
|
|
|
@@ -1426,7 +1797,7 @@ function LangSwitch() {
|
|
|
1426
1797
|
|
|
1427
1798
|
| Return | Description |
|
|
1428
1799
|
|--------|-------------|
|
|
1429
|
-
| `locale` | Current locale string (from `ctx.
|
|
1800
|
+
| `locale` | Current locale string (from `ctx.i18n.locale`) |
|
|
1430
1801
|
| `setLocale(locale)` | Switch locale (calls `navigate('/__lang/' + locale)`) |
|
|
1431
1802
|
| `t` | Translate a key using loaded locale messages |
|
|
1432
1803
|
|
|
@@ -1465,7 +1836,7 @@ function Page() {
|
|
|
1465
1836
|
}
|
|
1466
1837
|
```
|
|
1467
1838
|
|
|
1468
|
-
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.
|
|
1469
1840
|
|
|
1470
1841
|
**`addInterceptor(fn)`** — Register a URL interceptor. Interceptors run before SPA navigation; if one returns `true`, `navigate()` skips the fetch-and-swap.
|
|
1471
1842
|
|
|
@@ -1481,8 +1852,21 @@ addInterceptor(async (url) => {
|
|
|
1481
1852
|
### Flash messages
|
|
1482
1853
|
|
|
1483
1854
|
```ts
|
|
1484
|
-
|
|
1485
|
-
|
|
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
|
+
})
|
|
1486
1870
|
```
|
|
1487
1871
|
|
|
1488
1872
|
```tsx
|
|
@@ -1490,36 +1874,91 @@ return ctx.setPref('flash', JSON.stringify({ type: 'success', message: 'Done' })
|
|
|
1490
1874
|
import { useFlashMessage } from 'weifuwu/react'
|
|
1491
1875
|
|
|
1492
1876
|
function Toast() {
|
|
1493
|
-
const flash = useFlashMessage<{ type: string;
|
|
1877
|
+
const flash = useFlashMessage<{ type: string; text: string }>()
|
|
1494
1878
|
if (!flash) return null
|
|
1495
|
-
return <div className={`toast toast-${flash.type}`}>{flash.
|
|
1879
|
+
return <div className={`toast toast-${flash.type}`}>{flash.text}</div>
|
|
1496
1880
|
}
|
|
1497
1881
|
```
|
|
1498
1882
|
|
|
1883
|
+
| Option | Type | Default | Description |
|
|
1884
|
+
|--------|------|---------|-------------|
|
|
1885
|
+
| `name` | `string` | `'flash'` | Cookie name |
|
|
1886
|
+
|
|
1499
1887
|
### Dev mode
|
|
1500
1888
|
|
|
1501
|
-
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.
|
|
1502
1890
|
|
|
1503
|
-
|
|
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
|
|
1504
1896
|
|
|
1505
1897
|
---
|
|
1506
1898
|
|
|
1507
1899
|
## AI
|
|
1508
1900
|
|
|
1509
1901
|
```ts
|
|
1510
|
-
import { openai, streamText, generateText, streamObject, generateObject, tool, embed, embedMany } from 'weifuwu'
|
|
1902
|
+
import { openai, streamText, generateText, streamObject, generateObject, tool, embed, embedMany, aiProvider } from 'weifuwu'
|
|
1511
1903
|
import { runWorkflow } from 'weifuwu'
|
|
1904
|
+
|
|
1905
|
+
const provider = aiProvider()
|
|
1512
1906
|
```
|
|
1513
1907
|
|
|
1514
1908
|
For AI streaming endpoints see [`aiStream`](#aistream-β). For AI agent APIs see [`agent`](#agent-β).
|
|
1515
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
|
+
|
|
1516
1943
|
### DAG Workflow
|
|
1517
1944
|
|
|
1518
1945
|
```ts
|
|
1519
1946
|
const tools = { queryUser: tool({ ... }) }
|
|
1520
|
-
|
|
1947
|
+
|
|
1948
|
+
// Via provider:
|
|
1949
|
+
const wf = runWorkflow({ tools, provider })
|
|
1950
|
+
|
|
1951
|
+
// Or explicit model:
|
|
1952
|
+
const wf = runWorkflow({ tools, model: openai('gpt-4o') })
|
|
1521
1953
|
```
|
|
1522
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
|
+
|
|
1523
1962
|
---
|
|
1524
1963
|
|
|
1525
1964
|
## Server-Sent Events
|
|
@@ -1549,7 +1988,7 @@ currentTraceId, currentTrace, runWithTrace, traceElapsed, TraceContext,
|
|
|
1549
1988
|
|
|
1550
1989
|
```ts
|
|
1551
1990
|
auth, cors, csrf, compress, helmet, logger, rateLimit, requestId, validate, upload,
|
|
1552
|
-
|
|
1991
|
+
theme, i18n, flash, permissions, serveStatic, session, MemoryStore, RedisStore, SessionStore,
|
|
1553
1992
|
cache, MemoryCache, RedisCache, CacheStore
|
|
1554
1993
|
```
|
|
1555
1994
|
|
|
@@ -1571,7 +2010,7 @@ eq, ne, gt, gte, lt, lte, isNull, isNotNull, like, contains, in_, and, or, not
|
|
|
1571
2010
|
### Client-side (from `'weifuwu/react'`)
|
|
1572
2011
|
|
|
1573
2012
|
```ts
|
|
1574
|
-
TsxContext, useLoaderData,
|
|
2013
|
+
TsxContext, setCtx, useCtx, addCtxRebuilder, useLoaderData,
|
|
1575
2014
|
useWebsocket, useAction, useFetch, useQueryState, createStore,
|
|
1576
2015
|
Link, useNavigate, useNavigating, addInterceptor,
|
|
1577
2016
|
useLocale, useTheme, applyTheme, useFlashMessage,
|
|
@@ -1580,6 +2019,12 @@ Head
|
|
|
1580
2019
|
```
|
|
1581
2020
|
export type { UseAgentStreamOptions, UseAgentStreamReturn, AgentStreamState } from 'weifuwu/react'
|
|
1582
2021
|
|
|
2022
|
+
### AI Provider (framework abstraction)
|
|
2023
|
+
|
|
2024
|
+
```ts
|
|
2025
|
+
aiProvider, AIProvider, AIProviderOptions
|
|
2026
|
+
```
|
|
2027
|
+
|
|
1583
2028
|
### AI SDK (re-exported from `ai`)
|
|
1584
2029
|
|
|
1585
2030
|
```ts
|
|
@@ -1591,8 +2036,8 @@ openai, createOpenAI
|
|
|
1591
2036
|
### Other modules
|
|
1592
2037
|
|
|
1593
2038
|
```ts
|
|
1594
|
-
|
|
1595
|
-
user, mailer, graphql, aiStream, runWorkflow,
|
|
2039
|
+
theme, i18n, flash, health, analytics, seo, seoMiddleware, seoTags,
|
|
2040
|
+
user, mailer, graphql, aiStream, runWorkflow, knowledgeBase, permissions, queue,
|
|
1596
2041
|
logdb, messager, agent, iii, createWorker, registerWorker,
|
|
1597
2042
|
opencode, deploy, defineConfig, webhook,
|
|
1598
2043
|
testApp, TestApp, TestRequest, TestResponse,
|