weifuwu 0.33.7 → 0.33.8

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.
Files changed (2) hide show
  1. package/README.md +367 -646
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -6,7 +6,85 @@
6
6
  npm install weifuwu
7
7
  ```
8
8
 
9
- One package. Backend + frontend. User system, instant messaging, RAG knowledge base, AI Agent, CMS, dynamic data storage, and a reactive frontend framework. Configure environment variables and go.
9
+ One package. Backend + frontend. User system, messaging, RAG knowledge base, AI Agent, CMS, dynamic data storage, reactive frontend. Set env vars and go.
10
+
11
+ ---
12
+
13
+ ## Module Overview
14
+
15
+ ### Backend
16
+
17
+ | Module | Import | Depends on | Purpose |
18
+ |--------|--------|-----------|---------|
19
+ | `postgres()` | `weifuwu` | `DATABASE_URL` | PostgreSQL client (`ctx.sql`) |
20
+ | `redis()` | `weifuwu` | `REDIS_URL` | Redis client (`ctx.redis`) |
21
+ | `user()` | `weifuwu` | `postgres()`, `JWT_SECRET` | Auth, JWT, roles (`ctx.user`) |
22
+ | `messager()` | `weifuwu` | `postgres()`, `user()` | IM + AI conversation layer |
23
+ | `kb()` | `weifuwu` | `postgres()`, `DASHSCOPE_API_KEY` | RAG knowledge base |
24
+ | `agent()` | `weifuwu` | — | LLM chat, tools, streaming |
25
+ | `cms()` | `weifuwu` | `postgres()`, `user()` | Blog, docs, changelog |
26
+ | `base()` | `weifuwu` | `postgres()`, `user()` | Dynamic data engine |
27
+ | `queue()` | `weifuwu` | `REDIS_URL` | Job queue + cron |
28
+ | `ui()` | `weifuwu` | — | SSR/SPA rendering (`ctx.ui.html`, `ctx.ui.js`, `ctx.ui.css`) |
29
+
30
+ ### Middleware
31
+
32
+ | Import | Purpose |
33
+ |--------|---------|
34
+ | `cors()` | CORS headers |
35
+ | `helmet()` | Security headers |
36
+ | `compress()` | gzip / brotli / deflate |
37
+ | `rateLimit()` | Sliding-window rate limiter |
38
+ | `logger()` | Request logging |
39
+ | `upload()` | Multipart file upload |
40
+ | `serveStatic()` | Static files |
41
+ | `sandbox()` | Filesystem isolation |
42
+
43
+ ### Frontend (`weifuwu/client`)
44
+
45
+ | Import | Type | Purpose |
46
+ |--------|------|---------|
47
+ | `signal()` | function | Reactive state |
48
+ | `computed()` | function | Derived signals |
49
+ | `effect()` | function | Auto-tracked side effects |
50
+ | `<Show>` | component | Conditional rendering |
51
+ | `<For>` | component | List rendering |
52
+ | `<ErrorBoundary>` | component | Catch render errors |
53
+ | `<RouteView>` | component | Route outlet |
54
+ | `createApp()` | function | App instance with middleware chain |
55
+ | `mount()` | method | Mount SPA |
56
+ | `hydrate()` | method | SSR hydration |
57
+ | `router()` | middleware | Hash/history router |
58
+ | `api()` | middleware | HTTP client (`ctx.api`) |
59
+ | `auth()` | middleware | Auth state (`ctx.user/login/logout`) |
60
+ | `ws()` | middleware | WebSocket (`ctx.ws`) |
61
+ | `wrap()` | function | Third-party library integration |
62
+ | `useForm()` | function | Form state management |
63
+ | `createPortal()` | function | Render outside parent DOM |
64
+ | `LoginForm` | component | Login/register form |
65
+ | `Chat` | component | Real-time messaging |
66
+
67
+ ### Utils
68
+
69
+ | Import | Purpose |
70
+ |--------|---------|
71
+ | `requireRole('admin')` | Middleware factory: check `ctx.user.role` |
72
+ | `createHub()` | WebSocket pub/sub |
73
+ | `HttpError` | `throw new HttpError(msg, 404)` |
74
+ | `trace()` | Request tracing |
75
+
76
+ ---
77
+
78
+ ## Environment Variables
79
+
80
+ | Variable | Default | Used by |
81
+ |----------|---------|---------|
82
+ | `DATABASE_URL` | `postgres://root:123456@localhost:5432/demo` | `postgres()` |
83
+ | `REDIS_URL` | `redis://localhost:6379` | `redis()`, `queue()` |
84
+ | `JWT_SECRET` | — | `user()` |
85
+ | `DASHSCOPE_API_KEY` | — | `kb()` (embedding) |
86
+ | `DEEPSEEK_API_KEY` / `OPENAI_API_KEY` | — | `agent()` (LLM) |
87
+ | `DEEPSEEK_MODEL` | `deepseek-v4-flash` | `agent()` |
10
88
 
11
89
  ---
12
90
 
@@ -46,17 +124,17 @@ function AppShell(_props: {}, ctx: WfuiContext) {
46
124
  if (!ctx.isAuthenticated) return <LoginForm />
47
125
  return (
48
126
  <div>
49
- <nav><a onClick={() => ctx.app.navigate('/chat')}>聊天</a></nav>
127
+ <nav><a onClick={() => ctx.app.navigate('/chat')}>Chat</a></nav>
50
128
  <main><RouteView /></main>
51
129
  </div>
52
130
  )
53
131
  }
54
132
 
55
133
  const app = createApp()
56
- app.use(api()) // ← ctx.api.get/post
57
- app.use(auth()) // ← ctx.user / ctx.login / ctx.logout
58
- app.use(ws()) // ← ctx.ws.send / onMessage
59
- app.use(router({ routes })) // ← ctx.route / 路由
134
+ app.use(api())
135
+ app.use(auth())
136
+ app.use(ws())
137
+ app.use(router({ routes }))
60
138
  app.mount('#root', AppShell)
61
139
  ```
62
140
 
@@ -65,9 +143,10 @@ app.mount('#root', AppShell)
65
143
  { "jsx": "react-jsx", "jsxImportSource": "weifuwu/client" }
66
144
  ```
67
145
 
146
+ Build (traditional, or use `ctx.ui.js()` for dynamic compilation):
147
+
68
148
  ```js
69
- // build.mjs 传统构建方式(可选)
70
- // 推荐:使用 ctx.ui.js() 服务端动态编译,无需独立构建脚本
149
+ import esbuild from 'esbuild'
71
150
  esbuild.build({
72
151
  entryPoints: ['src/main.tsx'],
73
152
  jsx: 'automatic',
@@ -76,381 +155,61 @@ esbuild.build({
76
155
  })
77
156
  ```
78
157
 
79
- 或用服务端动态编译——一行代码,无需构建步骤、无需 watch 模式:
158
+ Or skip the build step entirely with server-side compilation:
159
+
80
160
  ```ts
81
161
  app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
82
162
  app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
83
163
  ```
84
164
 
85
- ### Environment Variables
86
-
87
- | Variable | Default | Used by |
88
- |----------|---------|---------|
89
- | `DATABASE_URL` | `postgres://root:123456@localhost:5432/demo` | `postgres()` |
90
- | `REDIS_URL` | `redis://localhost:6379` | `redis()` |
91
- | `JWT_SECRET` | — | `user()` |
92
- | `DASHSCOPE_API_KEY` | — | `kb()` (embedding) |
93
- | `DEEPSEEK_API_KEY` / `OPENAI_API_KEY` | — | `agent()` (LLM) |
94
- | `DEEPSEEK_MODEL` | `deepseek-v4-flash` | `agent()` |
95
-
96
- ---
97
-
98
- ## Modules
99
-
100
- ### Backend
101
-
102
- | Module | Import | Dependency | Purpose |
103
- |--------|--------|-----------|---------|
104
- | User | `user()` | `postgres()` | Auth, JWT, roles |
105
- | Messager | `messager()` | `postgres()`, `user()` | IM + AI conversation layer |
106
- | KB | `kb()` | `postgres()` | RAG knowledge base |
107
- | Agent | `agent()` | — | LLM chat, tool calling, streaming |
108
- | CMS | `cms()` | `postgres()`, `user()` | Blog, docs, changelog |
109
- | Base | `base()` | `postgres()`, `user()` | Dynamic data engine |
110
-
111
- ### Middleware
112
-
113
- | Import | Purpose |
114
- |--------|---------|
115
- | `cors()` | CORS headers |
116
- | `helmet()` | Security headers |
117
- | `compress()` | gzip / brotli / deflate |
118
- | `rateLimit()` | Sliding-window rate limiter |
119
- | `logger()` | Request logging |
120
- | `upload()` | Multipart file upload |
121
- | `serveStatic()` | Static files |
122
- | `sandbox()` | Filesystem isolation |
123
-
124
- ### Core
125
-
126
- | Import | Purpose |
127
- |--------|---------|
128
- | `serve(app, opts?)` | Start HTTP server |
129
- | `Router` | Trie-based router + WebSocket |
130
- | `HttpError` | `throw new HttpError(msg, 404)` |
131
- | `trace()` | Request tracing |
132
- | `postgres()` | PostgreSQL client (`ctx.sql`) |
133
- | `redis()` | Redis client (`ctx.redis`) |
134
- | `queue()` | Job queue + cron |
135
- | `createHub()` | WebSocket pub/sub |
136
- | `ui()` | SSR + SPA rendering (`ctx.ui.html`, `ctx.ui.js`, `ctx.ui.css`) |
137
-
138
- ### Frontend (weifuwu/client)
139
-
140
- | Import | Purpose |
141
- |--------|---------|
142
- | `signal()` | Reactive state |
143
- | `computed()` | Derived signals |
144
- | `effect()` | Auto-tracked side effects |
145
- | `<Show>` | Conditional rendering |
146
- | `<For>` | List rendering |
147
- | `<RouteView>` | Route outlet |
148
- | `createApp()` | App instance with middleware chain |
149
- | `router()` | Hash/history router with params + query |
150
- | `api()` | HTTP client (`ctx.api.get/post`) |
151
- | `auth()` | Auth state (`ctx.user/login/logout`) |
152
- | `ws()` | WebSocket (`ctx.ws.send/onMessage`) |
153
- | `LoginForm` | Login/register form component |
154
- | `Chat` | Real-time messaging component |
155
- | `domMount()` | Direct DOM mounting |
156
- | `wrap()` | Third-party library integration |
157
- | `useForm()` | Form state management (validation, submit, reset) |
158
- | `createPortal()` | Render outside parent DOM hierarchy |
159
- | `<ErrorBoundary>` | Catch render errors, show fallback |
160
-
161
- ### Utilities
162
-
163
- | Import | Purpose |
164
- |--------|---------|
165
- | `requireRole('admin')` | Middleware: check `ctx.user.role` |
166
-
167
165
  ---
168
166
 
169
- ## Frontend (weifuwu/client)
167
+ ## Backend Modules
170
168
 
171
- **weifuwu/client** is a reactive frontend framework built on Signal + TSX. Zero virtual DOM, zero dependencies, ~600 lines total.
169
+ Each module follows: import usage API table.
172
170
 
173
- ### Concepts
171
+ ### postgres
174
172
 
175
173
  ```ts
176
- // 1. Signal reactive data
177
- const count = signal(0)
178
- count.value = count.value + 1 // DOM updates automatically
179
-
180
- // 2. Computed — derived signals
181
- const doubled = computed(() => count.value * 2)
182
-
183
- // 3. Effect — auto-tracked side effects
184
- effect(() => console.log('count:', count.value))
185
-
186
- // 4. Component — (props, ctx) => JSX
187
- function MyComponent({ name }: { name: string }, ctx: WfuiContext) {
188
- return <div>Hello {name}</div>
189
- }
190
- ```
191
-
192
- ### createApp + Middleware
193
-
194
- ```tsx
195
- import { createApp, router, RouteView } from 'weifuwu/client'
196
- import type { WfuiContext, RouteDef } from 'weifuwu/client'
197
-
198
- const app = createApp()
199
- app.use(router({ routes, mode: 'hash' }))
200
- app.mount('#root', AppShell)
201
- ```
202
-
203
- ### Router
204
-
205
- ```tsx
206
- const routes: RouteDef[] = [
207
- { path: '/', component: HomePage, title: '首页' },
208
- { path: '/chat/:id', component: ChatPage, title: '聊天' },
209
- { path: '/user/:name', component: UserPage, title: '用户' },
210
- ]
211
-
212
- app.use(router({
213
- routes,
214
- notFound: NotFound,
215
- mode: 'hash', // or 'history'
216
- }))
217
-
218
- // In layout:
219
- function AppShell(_, ctx) {
220
- return (
221
- <div>
222
- <nav>
223
- <a onClick={() => ctx.app.navigate('/')}>首页</a>
224
- <a onClick={() => ctx.app.navigate('/chat/123')}>聊天</a>
225
- </nav>
226
- <main>
227
- <RouteView /> {/* ← renders matched route */}
228
- </main>
229
- </div>
230
- )
231
- }
232
-
233
- // Route params and query:
234
- ctx.route.path // "/chat/123"
235
- ctx.route.params // { id: "123" }
236
- ctx.route.query // { tab: "settings" }
237
-
238
- #### Route Loader — 数据预取
174
+ import { postgres } from 'weifuwu'
239
175
 
240
- ```tsx
241
- const routes: RouteDef[] = [
242
- {
243
- path: '/post/:id',
244
- component: PostPage,
245
- loader: async (ctx) => ({
246
- post: await ctx.api.get(`/api/posts/${ctx.route.params.id}`),
247
- }),
248
- },
249
- ]
176
+ const sql = postgres()
177
+ app.use(sql) // ctx.sql
250
178
 
251
- // In component:
252
- function PostPage(_, ctx) {
253
- const post = ctx.route.data.post
254
- if (!post) return <p class="text-gray-400">加载中...</p>
255
- return <h1 class="text-2xl font-bold">{post.title}</h1>
256
- }
179
+ await ctx.sql`SELECT * FROM users WHERE id = ${id}`
180
+ await ctx.sql.begin(async (sql) => { /* transaction */ })
257
181
  ```
258
182
 
259
- 组件先渲染(显示 loading),loader 完成后自动重渲染。
260
-
261
- ### Middleware: api / auth / ws
262
-
263
- ```tsx
264
- import { api, auth, ws } from 'weifuwu/client'
265
-
266
- app.use(api()) // ctx.api.get/post/put/patch/delete
267
- app.use(auth()) // ctx.user / ctx.login / ctx.logout / ctx.register
268
- app.use(ws()) // ctx.ws.send / onMessage / join / leave
269
- ```
270
-
271
- `api()` creates a fetch client with automatic token injection.
272
- `auth()` persists sessions to localStorage, validates tokens on startup.
273
- `ws()` manages WebSocket connections with auto-reconnect.
274
-
275
- ### Pre-built Components
183
+ | Method | Description |
184
+ |--------|-------------|
185
+ | `ctx.sql\`...\`` | Tagged template SQL queries |
186
+ | `ctx.sql.begin(fn)` | Transaction |
187
+ | `sql.close()` | Close pool |
276
188
 
277
- ```tsx
278
- import { LoginForm, Chat } from 'weifuwu/client'
189
+ Reads `DATABASE_URL` env. Supports migrations via `postgres({ migrate: { directory: './migrations' } })`.
279
190
 
280
- // Login / Register form
281
- function LoginPage(_, ctx) {
282
- if (ctx.isAuthenticated) return ctx.app.navigate('/')
283
- return <LoginForm />
284
- }
285
-
286
- // Real-time chat
287
- function ChatPage(_, ctx) {
288
- return <Chat conversationId="123" />
289
- }
290
- ```
291
-
292
- ### SSR & SPA (ctx.ui.html / ctx.ui.js / ctx.ui.css)
191
+ ### redis
293
192
 
294
193
  ```ts
295
- import { ui } from 'weifuwu'
296
-
297
- app.use(ui())
298
-
299
- // SSR page — ctx.ui.html`` returns complete HTML Response
300
- app.get('/blog/:slug', async (req, ctx) => ctx.ui.html`
301
- <!DOCTYPE html>
302
- <html>
303
- <head>
304
- <title>${post.title}</title>
305
- <link rel="stylesheet" href="/static/style.css">
306
- </head>
307
- <body>
308
- <div id="root">
309
- <h1>${post.title}</h1>
310
- <div>${ctx.ui.html.unsafe(post.body)}</div>
311
- <div data-hydrate="like"></div>
312
- </div>
313
- <script>window.__WFUI_PROPS__=${ctx.ui.html.unsafe(JSON.stringify({ post }))}</script>
314
- <script src="/static/app.js"></script>
315
- </body>
316
- </html>
317
- `)
318
-
319
- // Dynamic JS compilation — ctx.ui.js() compiles TSX on demand
320
- app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
321
-
322
- // Dynamic CSS serving — ctx.ui.css() reads and serves CSS
323
- app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
324
-
325
- // Client hydrates interactive sections, skips SSR content
326
- const app = createApp()
327
- app.use(api())
328
-
329
- const root = document.getElementById('root')
330
- if (root && root.children.length > 0) {
331
- // SSR page — hydrate only interactive areas
332
- app.hydrate('[data-hydrate="like"]', LikeButton)
333
- } else {
334
- // SPA page — full mount
335
- app.mount('#root', AppShell)
336
- }
337
- ```
338
-
339
- ### wrap() — Third-party Library Integration
340
-
341
- ```tsx
342
- import { wrap, effect } from 'weifuwu/client'
343
- import * as echarts from 'echarts'
344
-
345
- // wrap(tagName, setup) creates a component:
346
- // - Creates a <div> container
347
- // - Calls setup(el, props, ctx) when element enters the document
348
- // - Runs cleanup when element is removed
349
- const PieChart = wrap('div', (el, props: { data: any[] }, ctx) => {
350
- const chart = echarts.init(el)
351
- chart.setOption({ series: [{ type: 'pie', data: props.data }] })
352
- effect(() => chart.setOption({ series: [{ type: 'pie', data: props.data }] }))
353
- return () => chart.dispose()
354
- })
355
-
356
- // Use in JSX like any component
357
- <Dashboard>
358
- <PieChart data={salesData} />
359
- </Dashboard>
360
- ```
361
-
362
- ### useForm() — 表单状态管理
363
-
364
- ```tsx
365
- import { useForm } from 'weifuwu/client'
366
-
367
- const form = useForm({
368
- initial: { email: '', password: '' },
369
- validate: {
370
- email: (v) => !v.includes('@') && '请输入有效邮箱',
371
- password: (v) => v.length < 6 && '至少 6 位',
372
- },
373
- })
374
-
375
- // 绑定到 input:{...form.field('name')} 自动设置 value + onInput
376
- <input {...form.field('email')} placeholder="邮箱" />
377
- {form.errors.email && <span class="text-red-500">{form.errors.email}</span>}
378
-
379
- // 提交时自动验证所有字段
380
- <button onClick={() => form.submit((data) => ctx.login(data.email, data.password))}>
381
- 登录
382
- </button>
383
-
384
- // 重置
385
- <button onClick={form.reset}>重置</button>
386
-
387
- // 编程设置
388
- form.setValue('email', 'a@b.com')
389
- form.setValues({ email: 'a@b.com', password: '123' })
390
- ```
391
-
392
- ### createPortal() — 渲染到父容器外
393
-
394
- 适用于 Modal、Dropdown、Tooltip 等需突破 `overflow: hidden` 或 z-index 层级的情况。
395
-
396
- ```tsx
397
- import { createPortal, Show } from 'weifuwu/client'
398
-
399
- function Modal({ show, title, children }) {
400
- return <Show when={show}>
401
- {createPortal(
402
- <div class="fixed inset-0 bg-black/50 flex items-center justify-center">
403
- <div class="bg-white rounded-xl p-6 min-w-[400px]">
404
- <h2 class="text-lg font-bold mb-4">{title}</h2>
405
- {children}
406
- </div>
407
- </div>,
408
- document.body
409
- )}
410
- </Show>
411
- }
412
- ```
413
-
414
- ### ErrorBoundary — 错误边界
415
-
416
- 子组件渲染异常时捕获,显示 fallback 而非白屏。children 必须是 thunk(延迟执行)。
417
-
418
- ```tsx
419
- import { ErrorBoundary } from 'weifuwu/client'
420
-
421
- function AppShell(_, ctx) {
422
- return (
423
- <div>
424
- <nav>...</nav>
425
- <main>
426
- <ErrorBoundary fallback={(e) => <p>出错了: {e.message}</p>}>
427
- {() => <RouteView />}
428
- </ErrorBoundary>
429
- </main>
430
- </div>
431
- )
432
- }
433
- ```
434
-
435
- ### Show / For
194
+ import { redis } from 'weifuwu'
436
195
 
437
- ```tsx
438
- // Conditional rendering (supports Signal)
439
- <Show when={isLoggedIn} fallback={<LoginPage />}>
440
- <Dashboard />
441
- </Show>
196
+ const r = redis()
197
+ app.use(r) // ctx.redis
442
198
 
443
- // List rendering (supports Signal)
444
- <For each={filteredItems}>
445
- {(item) => <div>{item.name}</div>}
446
- </For>
199
+ await ctx.redis.set('key', 'value')
200
+ await ctx.redis.get('key')
447
201
  ```
448
202
 
449
- ---
203
+ | Method | Description |
204
+ |--------|-------------|
205
+ | `ctx.redis.set(key, value)` | Set key |
206
+ | `ctx.redis.get(key)` | Get key |
207
+ | `ctx.redis.del(key)` | Delete key |
208
+ | `redis.close()` | Close connection |
450
209
 
451
- ## user
210
+ Reads `REDIS_URL` env (default: `redis://localhost:6379`).
452
211
 
453
- Auth, registration, JWT, password management, roles.
212
+ ### user
454
213
 
455
214
  ```ts
456
215
  import { user, requireRole } from 'weifuwu'
@@ -458,30 +217,18 @@ import { user, requireRole } from 'weifuwu'
458
217
  app.use(postgres())
459
218
  app.use(user({ secret: process.env.JWT_SECRET }))
460
219
 
461
- // Register
462
- app.post('/api/register', async (req, ctx) => {
463
- const result = await ctx.userModule.register(await req.json())
464
- return Response.json(result)
465
- })
466
- // Login
220
+ app.post('/api/register', async (req, ctx) =>
221
+ Response.json(await ctx.userModule.register(await req.json())))
222
+
467
223
  app.post('/api/login', async (req, ctx) => {
468
224
  const { email, password } = await req.json()
469
225
  const result = await ctx.userModule.login(email, password)
470
- if (!result) return new Response('Unauthorized', { status: 401 })
471
- return Response.json(result)
472
- })
473
- // Current user
474
- app.get('/api/me', async (req, ctx) => {
475
- if (!ctx.user) return new Response('Unauthorized', { status: 401 })
476
- return Response.json(ctx.user)
226
+ return result ? Response.json(result) : new Response('Unauthorized', { status: 401 })
477
227
  })
478
- // Admin only
479
- app.get('/api/admin/users', requireRole('admin'), async (req, ctx) => {
480
- return Response.json(await ctx.userModule.listUsers())
481
- })
482
- ```
483
228
 
484
- ### ctx.userModule API
229
+ app.get('/api/me', async (req, ctx) =>
230
+ ctx.user ? Response.json(ctx.user) : new Response('Unauthorized', { status: 401 }))
231
+ ```
485
232
 
486
233
  | Method | Returns | Description |
487
234
  |--------|---------|-------------|
@@ -495,39 +242,14 @@ app.get('/api/admin/users', requireRole('admin'), async (req, ctx) => {
495
242
  | `listUsers(inactive?)` | `UserRecord[]` | List users |
496
243
  | `generateToken(user)` | `string` | Issue JWT |
497
244
  | `verifyToken(token)` | `TokenPayload \| null` | Verify JWT |
498
- | `refreshToken(token)` | `string \| null` | Refresh JWT |
499
245
 
500
- ### ctx.user
246
+ `ctx.user` — auto-resolved from `Authorization: Bearer` or `token` cookie. Fields: `id, name, email, role, [key: string]`.
501
247
 
502
- Auto-resolved from `Authorization: Bearer` or `token` cookie.
248
+ `requireRole('admin')` guard middleware. No auth → 401, wrong role → 403.
503
249
 
504
- ```ts
505
- interface User {
506
- id: string
507
- name: string
508
- email: string
509
- role: string
510
- [key: string]: unknown
511
- }
512
- ```
250
+ Password: scrypt + 32-byte random salt. Token: HMAC SHA-256, 7 day expiry.
513
251
 
514
- ### requireRole
515
-
516
- ```ts
517
- app.get('/admin', requireRole('admin'), handler)
518
- // No auth → 401, wrong role → 403
519
- ```
520
-
521
- ### Security
522
-
523
- - Password: scrypt + 32-byte random salt
524
- - Token: HMAC SHA-256, 7 day expiry
525
-
526
- ---
527
-
528
- ## messager
529
-
530
- Instant messaging + AI conversation layer. Direct/group chat, message persistence, WebSocket push.
252
+ ### messager
531
253
 
532
254
  ```ts
533
255
  import { messager } from 'weifuwu'
@@ -536,16 +258,13 @@ app.use(postgres())
536
258
  app.use(user())
537
259
  app.use(messager())
538
260
 
539
- // WebSocket — auto-join all user conversations
540
261
  app.ws('/ws', {
541
262
  async open(ws, ctx) {
542
- for (const c of await ctx.messager.getConversations()) {
263
+ for (const c of await ctx.messager.getConversations())
543
264
  ctx.ws.join(`conversation:${c.id}`)
544
- }
545
265
  },
546
266
  })
547
267
 
548
- // REST API
549
268
  app.post('/api/messages', async (req, ctx) => {
550
269
  const { conversationId, body } = await req.json()
551
270
  const msg = await ctx.messager.sendMessage(conversationId, body)
@@ -561,36 +280,23 @@ app.get('/api/conversations/:id/messages', async (req, ctx) => {
561
280
  })
562
281
  ```
563
282
 
564
- ### ctx.messager API
565
-
566
283
  | Method | Returns | Description |
567
284
  |--------|---------|-------------|
568
285
  | `createDirectConversation(userId)` | `Conversation` | Create/reuse DM |
569
286
  | `createGroupConversation(title, userIds)` | `Conversation` | Create group |
570
- | `sendMessage(convId, body)` | `Message` | Send, auto-broadcast to `conversation:{id}` room |
287
+ | `sendMessage(convId, body)` | `Message` | Send + broadcast to room |
571
288
  | `getMessages(convId, opts?)` | `Message[]` | Cursor pagination |
572
289
  | `editMessage(msgId, body)` | `Message \| null` | Edit (24h window) |
573
290
  | `deleteMessage(msgId)` | `boolean` | Soft delete |
574
291
  | `getConversations()` | `Conversation[]` | List with unread + last message |
575
292
  | `getConversation(id)` | `Conversation \| null` | Get detail |
576
293
  | `markRead(convId)` | `void` | Mark as read |
577
- | `getUnreadCount()` | `{ total, byConversation }` | Unread stats |
578
294
  | `addParticipants(convId, userIds)` | `void` | Add members |
579
295
  | `removeParticipant(convId, userId?)` | `boolean` | Leave / kick |
580
296
 
581
- ### Storage
582
-
583
- 3 tables: `conversations` / `participants` / `messages`. Auto-migration.
584
-
585
- ### AI Conversations
586
-
587
- messager + agent = ChatGPT foundation. Messager handles sessions + push, agent handles LLM generation.
588
-
589
- ---
590
-
591
- ## kb
297
+ Storage: 3 auto-migrated tables (conversations, participants, messages). WebSocket push via rooms.
592
298
 
593
- RAG knowledge base. Import docs → auto-chunk → DashScope embedding → pgvector storage → semantic search.
299
+ ### kb Knowledge Base
594
300
 
595
301
  ```ts
596
302
  import { kb } from 'weifuwu'
@@ -598,22 +304,17 @@ import { kb } from 'weifuwu'
598
304
  app.use(postgres())
599
305
  app.use(kb())
600
306
 
601
- // Import
602
307
  app.post('/api/kb/import', async (req, ctx) => {
603
308
  const { title, content } = await req.json()
604
- const result = await ctx.kb.importText(title, content)
605
- return Response.json(result, { status: 201 })
309
+ return Response.json(await ctx.kb.importText(title, content), { status: 201 })
606
310
  })
607
311
 
608
- // Search
609
312
  app.post('/api/kb/search', async (req, ctx) => {
610
313
  const { query } = await req.json()
611
314
  return Response.json(await ctx.kb.search(query, { limit: 5 }))
612
315
  })
613
316
  ```
614
317
 
615
- ### ctx.kb API
616
-
617
318
  | Method | Returns | Description |
618
319
  |--------|---------|-------------|
619
320
  | `importText(title, text, opts?)` | `{ document, chunks }` | Import → chunk → embed → store |
@@ -621,45 +322,20 @@ app.post('/api/kb/search', async (req, ctx) => {
621
322
  | `search(query, opts?)` | `SearchResult[]` | Semantic search (cosine) |
622
323
  | `list()` | `Document[]` | List documents |
623
324
  | `get(id)` | `Document \| null` | Get document |
624
- | `getChunks(documentId)` | `Chunk[]` | Get chunks |
625
325
  | `delete(id)` | `boolean` | Delete + cascade chunks |
626
326
 
627
- ### Configuration
327
+ Default: DashScope `text-embedding-v4`. Customizable via `kb({ embed: async (text) => number[], dimensions: 1536 })`. Storage: `kb_documents` + `kb_chunks` (VECTOR(1536) + TSVECTOR GIN index).
628
328
 
629
- ```ts
630
- // Default: DashScope text-embedding-v4 (env: DASHSCOPE_API_KEY)
631
- app.use(kb())
632
-
633
- // Custom embedding
634
- app.use(kb({
635
- embed: async (text) => { /* return number[] */ },
636
- dimensions: 1536,
637
- chunkSize: 512, // tokens
638
- chunkOverlap: 64,
639
- }))
640
- ```
641
-
642
- ### Integration with Agent
329
+ Integration with agent:
643
330
 
644
331
  ```ts
645
332
  app.use(agent({
646
- model: openai('deepseek-v4-flash', { baseURL: 'https://api.deepseek.com/v1' }),
647
- knowledge: {
648
- search: async (query, ctx) => ctx.kb.search(query),
649
- },
333
+ model: openai('deepseek-v4-flash', ...),
334
+ knowledge: { search: async (query, ctx) => ctx.kb.search(query) },
650
335
  }))
651
336
  ```
652
337
 
653
- ### Storage
654
-
655
- - `kb_documents` — document metadata
656
- - `kb_chunks` — chunk content + VECTOR(1536) + TSVECTOR GIN index
657
-
658
- ---
659
-
660
- ## agent
661
-
662
- AI Agent — LLM chat, tool calling, RAG, streaming.
338
+ ### agent
663
339
 
664
340
  ```ts
665
341
  import { agent } from 'weifuwu'
@@ -670,9 +346,7 @@ import { z } from 'zod'
670
346
  app.use(agent({
671
347
  model: openai('deepseek-v4-flash', { baseURL: 'https://api.deepseek.com/v1' }),
672
348
  system: 'You are a helpful assistant.',
673
- knowledge: {
674
- search: async (query, ctx) => ctx.kb.search(query),
675
- },
349
+ knowledge: { search: async (q, ctx) => ctx.kb.search(q) },
676
350
  tools: {
677
351
  getWeather: tool({
678
352
  description: 'Get weather for a city',
@@ -683,48 +357,22 @@ app.use(agent({
683
357
  maxSteps: 5,
684
358
  }))
685
359
 
686
- // Streaming
687
360
  app.post('/api/chat', async (req, ctx) => {
688
361
  const { messages } = await req.json()
689
362
  return ctx.agent.chatStreamResponse({ messages })
690
363
  })
691
-
692
- // Non-streaming
693
- app.post('/api/chat/sync', async (req, ctx) => {
694
- const { prompt } = await req.json()
695
- const text = await ctx.agent.chat(prompt)
696
- return Response.json({ text })
697
- })
698
364
  ```
699
365
 
700
- ### ctx.agent API
701
-
702
366
  | Method | Description |
703
367
  |-------------|-------------|
704
368
  | `chat(prompt, opts?)` | Non-streaming, returns text |
705
369
  | `chatStreamResponse({ messages })` | SSE stream (compatible with `useChat`) |
706
370
 
707
- ### Default Model
371
+ Default model: DeepSeek-V4-Flash via `@ai-sdk/openai`. Override with `DEEPSEEK_MODEL` env. API key via `DEEPSEEK_API_KEY` or `OPENAI_API_KEY`.
708
372
 
709
- - LLM: DeepSeek-V4-Flash (via `@ai-sdk/openai` + `baseURL: 'https://api.deepseek.com/v1'`)
710
- - Override with `DEEPSEEK_MODEL` env
711
- - API key via `DEEPSEEK_API_KEY` or `OPENAI_API_KEY` env
373
+ Features: `knowledge.search` (RAG), `tools` (auto-loop with maxSteps), `sandbox: true` (filesystem isolation), `store` (session persistence), `agents` (multi-agent).
712
374
 
713
- ### Features
714
-
715
- | Feature | Description |
716
- |---------|-------------|
717
- | `knowledge.search` | RAG callback, auto-injected into system prompt |
718
- | `tools` | Tool definitions, auto-loop (maxSteps) |
719
- | `sandbox: true` | Integrates with `ctx.sandbox` |
720
- | `store` | Session persistence (save/load) |
721
- | `agents` | Multi-agent orchestration |
722
-
723
- ---
724
-
725
- ## cms
726
-
727
- Content management — blog, docs, changelog.
375
+ ### cms
728
376
 
729
377
  ```ts
730
378
  import { cms, requireRole } from 'weifuwu'
@@ -733,30 +381,18 @@ app.use(postgres())
733
381
  app.use(user())
734
382
  app.use(cms())
735
383
 
736
- // Public
737
- app.get('/api/posts', async (req, ctx) => {
738
- return Response.json(await ctx.cms.list({ type: 'post', status: 'published' }))
739
- })
384
+ app.get('/api/posts', async (req, ctx) =>
385
+ Response.json(await ctx.cms.list({ type: 'post', status: 'published' })))
386
+
740
387
  app.get('/api/posts/:slug', async (req, ctx) => {
741
388
  const post = await ctx.cms.get(ctx.params.slug)
742
- if (!post) return new Response('Not found', { status: 404 })
743
- return Response.json(post)
389
+ return post ? Response.json(post) : new Response('Not found', { status: 404 })
744
390
  })
745
391
 
746
- // Admin
747
- app.post('/api/admin/posts', requireRole('admin'), async (req, ctx) => {
748
- const post = await ctx.cms.create(await req.json())
749
- return Response.json(post, { status: 201 })
750
- })
751
- app.patch('/api/admin/posts/:id', requireRole('admin'), async (req, ctx) => {
752
- const post = await ctx.cms.update(ctx.params.id, await req.json())
753
- if (!post) return new Response('Not found', { status: 404 })
754
- return Response.json(post)
755
- })
392
+ app.post('/api/admin/posts', requireRole('admin'), async (req, ctx) =>
393
+ Response.json(await ctx.cms.create(await req.json()), { status: 201 }))
756
394
  ```
757
395
 
758
- ### ctx.cms API
759
-
760
396
  | Method | Returns | Description |
761
397
  |--------|---------|-------------|
762
398
  | `create(input)` | `Content` | Create (admin) |
@@ -764,26 +400,15 @@ app.patch('/api/admin/posts/:id', requireRole('admin'), async (req, ctx) => {
764
400
  | `getById(id)` | `Content \| null` | Get by ID |
765
401
  | `update(id, input)` | `Content \| null` | Update (admin) |
766
402
  | `delete(id)` | `boolean` | Delete (admin) |
767
- | `list(opts?)` | `Content[]` | List with cursor |
403
+ | `list(opts?)` | `Content[]` | List with cursor + filters |
768
404
  | `publish(id)` | `Content \| null` | Publish (admin) |
769
405
  | `unpublish(id)` | `Content \| null` | Unpublish (admin) |
770
406
  | `listTags()` | `TagWithCount[]` | List tags |
771
407
  | `createTag(name)` | `Tag` | Create tag |
772
408
 
773
- ### Features
774
-
775
- - Types: post / page / doc / changelog (any string)
776
- - Status: draft / published / archived
777
- - Slug: auto-generated, unique per type
778
- - Tags: many-to-many, auto-created
779
- - Tree: parent_id for hierarchy
780
- - Auth: non-admin users see published only
781
-
782
- ---
783
-
784
- ## base
409
+ Types: post / page / doc / changelog (any string). Status: draft / published / archived. Tags: many-to-many, auto-created. Parent_id for hierarchy. Auth: non-admin users see published only.
785
410
 
786
- Dynamic data storage engine let users define their own data structures (like Airtable).
411
+ ### baseDynamic Data Engine
787
412
 
788
413
  ```ts
789
414
  import { base } from 'weifuwu'
@@ -792,17 +417,11 @@ app.use(postgres())
792
417
  app.use(user())
793
418
  app.use(base())
794
419
 
795
- // Define schema
796
- app.post('/api/bases', async (req, ctx) => {
797
- const b = await ctx.base.create(await req.json())
798
- return Response.json(b, { status: 201 })
799
- })
420
+ app.post('/api/bases', async (req, ctx) =>
421
+ Response.json(await ctx.base.create(await req.json()), { status: 201 }))
800
422
 
801
- // CRUD
802
- app.post('/api/bases/:id/:table', async (req, ctx) => {
803
- const row = await ctx.base.insert(ctx.params.id, ctx.params.table, await req.json())
804
- return Response.json(row, { status: 201 })
805
- })
423
+ app.post('/api/bases/:id/:table', async (req, ctx) =>
424
+ Response.json(await ctx.base.insert(ctx.params.id, ctx.params.table, await req.json()), { status: 201 }))
806
425
 
807
426
  app.get('/api/bases/:id/:table', async (req, ctx) => {
808
427
  const url = new URL(req.url)
@@ -813,14 +432,9 @@ app.get('/api/bases/:id/:table', async (req, ctx) => {
813
432
  })
814
433
  ```
815
434
 
816
- ### ctx.base API
817
-
818
435
  | Method | Returns | Description |
819
436
  |--------|---------|-------------|
820
437
  | `create({ name, tables })` | `BaseDef` | Create database |
821
- | `defineTable(baseId, schema)` | `BaseDef` | Add table |
822
- | `updateTable(baseId, name, schema)` | `BaseDef \| null` | Update table |
823
- | `removeTable(baseId, name)` | `BaseDef \| null` | Remove table |
824
438
  | `insert(baseId, table, data)` | `Row` | Insert row |
825
439
  | `getRow(baseId, table, id)` | `Row \| null` | Get row |
826
440
  | `updateRow(baseId, table, id, data)` | `Row \| null` | Update row |
@@ -828,113 +442,220 @@ app.get('/api/bases/:id/:table', async (req, ctx) => {
828
442
  | `query(baseId, table, opts?)` | `Row[]` | Query (filter/sort/limit/offset) |
829
443
  | `search(baseId, table, field, query)` | `Row[]` | Full-text search |
830
444
  | `similaritySearch(baseId, table, field, vector)` | `Row[]` | Vector search |
831
- | `list()` / `get(id)` / `getBySlug(slug)` / `delete(id)` | — | Manage databases |
832
-
833
- ### Architecture
834
445
 
835
- Fixed Slot: a single `base_data` table with ~120 physical columns:
446
+ Fixed Slot architecture: a single `base_data` table with ~120 physical columns (64 text, 32 number, 8 date, 4 vector, 4 search). Field → physical column mapping stored in `base_column_map`. Overflow to JSONB. pgvector auto-detected.
836
447
 
837
- | Type | Count | PG Type |
838
- |------|:-----:|:--------:|
839
- | text001..064 | 64 | TEXT |
840
- | number001..032 | 32 | DOUBLE PRECISION |
841
- | date001..008 | 8 | TIMESTAMPTZ |
842
- | vector001..004 | 4 | VECTOR(1536) |
843
- | search001..004 | 4 | TEXT |
844
- | ext | 1 | JSONB (overflow) |
448
+ ### queue
845
449
 
846
- Field name → physical column mapping stored in `base_column_map`. Fields beyond the physical columns overflow to ext JSONB.
450
+ ```ts
451
+ import { queue } from 'weifuwu'
847
452
 
848
- pgvector auto-detected (included in docker image).
453
+ const q = queue()
454
+ app.use(q) // → ctx.queue
849
455
 
850
- ---
456
+ q.process('email', async (job) => { await sendEmail(job.payload) })
457
+ q.cron('cleanup', '0 3 * * *', () => cleanup())
458
+ await q.add('email', { to: 'user@example.com' })
459
+ q.run()
460
+ ```
851
461
 
852
- ---
462
+ | Method | Description |
463
+ |--------|-------------|
464
+ | `q.process(name, handler)` | Register job processor |
465
+ | `q.add(name, payload, opts?)` | Enqueue job |
466
+ | `q.cron(name, schedule, fn)` | Schedule recurring job |
467
+ | `q.run()` | Start processing |
468
+ | `q.close()` | Shutdown |
853
469
 
854
- ## Router
470
+ ### ui — SSR & SPA
855
471
 
856
472
  ```ts
857
- const app = new Router()
473
+ import { ui } from 'weifuwu'
858
474
 
859
- // HTTP
860
- app.get(path, ...handlers)
861
- app.post / put / delete / patch / head / options(path, ...handlers)
862
- app.all(path, ...handlers)
475
+ app.use(ui())
863
476
 
864
- // WebSocket
865
- app.ws(path, ...middlewares, handler)
866
- // handler: { open?, message?, close?, error? }
477
+ // SSR page — tagged template returns Response
478
+ app.get('/blog/:slug', async (req, ctx) => ctx.ui.html`
479
+ <!DOCTYPE html><html>
480
+ <head><title>${post.title}</title></head>
481
+ <body><div id="root">${ctx.ui.html.unsafe(post.body)}</div>
482
+ <script src="/static/app.js"></script></body></html>`)
867
483
 
868
- // Middleware & mounting
869
- app.use(middleware)
870
- app.mount(prefix, router)
871
- app.plugin(fn)
872
- app.onError(handler)
873
- app.routes() // debug: list all routes
484
+ // Dynamic JS compilation (no build step needed)
485
+ app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
486
+
487
+ // CSS with Tailwind support (auto-detects tailwindcss + postcss)
488
+ app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
874
489
  ```
875
490
 
491
+ | API | Returns | Description |
492
+ |-----|---------|-------------|
493
+ | `ctx.ui.html\`...\`` | `Response` | Tagged template → HTML |
494
+ | `ctx.ui.html.unsafe(str)` | `string` | Mark as safe (skip escaping) |
495
+ | `ctx.ui.js(entryPath)` | `Response` | Compile TSX → JS bundle |
496
+ | `ctx.ui.css(entryPath)` | `Response` | Read/compile CSS (Tailwind v4) |
497
+
498
+ Tailwind CSS v4 support: add `@import 'tailwindcss'` to your CSS entry file. `ctx.ui.css` auto-detects `postcss` + `@tailwindcss/postcss`. Falls back to raw file serving if not installed.
499
+
876
500
  ---
877
501
 
878
- ## Middleware
502
+ ## Frontend — weifuwu/client
879
503
 
880
- ```ts
881
- import { cors, helmet, compress, rateLimit, logger, upload, serveStatic, sandbox } from 'weifuwu'
882
-
883
- app.use(cors({ origin: '*' }))
884
- app.use(helmet())
885
- app.use(compress())
886
- app.use(rateLimit({ max: 100 }))
887
- app.use(logger({ format: 'short' }))
888
- app.use(upload())
889
- app.use(serveStatic('./public'))
890
- app.use(sandbox({ baseDir: '/tmp/workspaces' }))
504
+ Reactive frontend framework. Zero virtual DOM, zero external dependencies. Component model: `(props, ctx) => Node`.
505
+
506
+ ### Concepts
507
+
508
+ ```tsx
509
+ // Signal — reactive data
510
+ const count = signal(0)
511
+ count.value = count.value + 1 // DOM updates automatically
512
+
513
+ // Computed — derived signals
514
+ const doubled = computed(() => count.value * 2)
515
+
516
+ // Effect — auto-tracked side effects
517
+ effect(() => console.log('count:', count.value))
518
+
519
+ // Component — (props, ctx) => JSX
520
+ function MyComponent({ name }: { name: string }, ctx: WfuiContext) {
521
+ return <div>Hello {name}</div>
522
+ }
891
523
  ```
892
524
 
893
- ---
525
+ ### createApp + Middleware
894
526
 
895
- ## Postgres
527
+ ```tsx
528
+ const app = createApp()
529
+ app.use(api()) // ctx.api.get/post/put/patch/delete
530
+ app.use(auth()) // ctx.user / ctx.login / ctx.logout / ctx.register
531
+ app.use(ws()) // ctx.ws.send / onMessage / join / leave
532
+ app.use(router({ routes })) // ctx.route.path/params/query
896
533
 
897
- ```ts
898
- import { postgres } from 'weifuwu'
534
+ app.mount('#root', AppShell) // SPA mode
535
+ app.hydrate('#comments', Comments) // SSR hydration (doesn't clear DOM)
536
+ ```
899
537
 
900
- const sql = postgres()
901
- app.use(sql) // → ctx.sql
538
+ ### Router
902
539
 
903
- await sql.sql`SELECT * FROM users WHERE id = ${id}`
904
- await sql.sql.begin(async (sql) => { /* transaction */ })
540
+ ```tsx
541
+ const routes: RouteDef[] = [
542
+ { path: '/', component: HomePage, title: 'Home' },
543
+ { path: '/post/:id', component: PostPage, loader: async (ctx) => ({
544
+ post: await ctx.api.get(`/api/posts/${ctx.route.params.id}`),
545
+ })},
546
+ ]
547
+
548
+ app.use(router({ routes, notFound: NotFound, mode: 'hash' }))
549
+
550
+ // In component:
551
+ ctx.route.path // "/post/123"
552
+ ctx.route.params // { id: "123" }
553
+ ctx.route.query // { tab: "settings" }
554
+ ctx.route.data // { post: {...} } — from loader
905
555
  ```
906
556
 
907
- Reads `DATABASE_URL` env. Supports migrations, transactions, connection pool stats.
557
+ Loader data flow: component renders immediately (shows loading state), loader completes → re-render with data.
908
558
 
909
- ---
559
+ ### wrap() — Third-party Library Integration
910
560
 
911
- ## Redis
561
+ ```tsx
562
+ import { wrap, effect } from 'weifuwu/client'
563
+ import * as echarts from 'echarts'
912
564
 
913
- ```ts
914
- import { redis } from 'weifuwu'
565
+ const PieChart = wrap('div', (el, props: { data: any[] }, ctx) => {
566
+ const chart = echarts.init(el)
567
+ chart.setOption({ series: [{ type: 'pie', data: props.data }] })
568
+ effect(() => chart.setOption({ series: [{ type: 'pie', data: props.data }] }))
569
+ return () => chart.dispose() // cleanup when element is removed
570
+ })
915
571
 
916
- const r = redis()
917
- app.use(r) // ctx.redis
918
- await r.redis.set('key', 'value')
572
+ // Use as regular component:
573
+ <Dashboard><PieChart data={salesData} /></Dashboard>
919
574
  ```
920
575
 
921
- Reads `REDIS_URL` env (default: `redis://localhost:6379`).
576
+ ### useForm() Form State Management
577
+
578
+ ```tsx
579
+ import { useForm } from 'weifuwu/client'
580
+
581
+ const form = useForm({
582
+ initial: { email: '', password: '' },
583
+ validate: {
584
+ email: (v) => !v.includes('@') && 'Invalid email',
585
+ },
586
+ })
587
+
588
+ // Bind to input — {...form.field('name')} sets value + onInput
589
+ <input {...form.field('email')} placeholder="Email" />
590
+ {form.errors.email && <span>{form.errors.email}</span>}
591
+
592
+ // Auto-validates all fields before calling handler
593
+ <button onClick={() => form.submit((data) => ctx.login(data.email, data.password))}>
594
+ Login
595
+ </button>
596
+
597
+ // Programmatic control
598
+ form.setValue('email', 'a@b.com')
599
+ form.setValues({ email: 'a@b.com', password: '123' })
600
+ form.reset()
601
+ ```
602
+
603
+ ### createPortal & ErrorBoundary
604
+
605
+ ```tsx
606
+ import { createPortal, Show, ErrorBoundary } from 'weifuwu/client'
607
+
608
+ // Portal — render outside parent (modals, dropdowns)
609
+ <Show when={showModal}>
610
+ {createPortal(
611
+ <div class="fixed inset-0 bg-black/50">...</div>,
612
+ document.body,
613
+ )}
614
+ </Show>
615
+
616
+ // ErrorBoundary — catch render errors, children must be a thunk
617
+ <ErrorBoundary fallback={(e) => <p>Error: {e.message}</p>}>
618
+ {() => <Dashboard />}
619
+ </ErrorBoundary>
620
+ ```
621
+
622
+ ### Pre-built Components
623
+
624
+ ```tsx
625
+ import { LoginForm, Chat } from 'weifuwu/client'
626
+
627
+ function LoginPage(_, ctx) {
628
+ if (ctx.isAuthenticated) return ctx.app.navigate('/')
629
+ return <LoginForm />
630
+ }
631
+
632
+ function ChatPage(_, ctx) {
633
+ return <Chat conversationId="123" />
634
+ }
635
+ ```
922
636
 
923
637
  ---
924
638
 
925
- ## Queue & Cron
639
+ ## Router
926
640
 
927
641
  ```ts
928
- import { queue } from 'weifuwu'
642
+ const app = new Router()
929
643
 
930
- const q = queue()
931
- app.use(q) // → ctx.queue
644
+ // HTTP
645
+ app.get(path, ...handlers)
646
+ app.post / put / delete / patch / head / options(path, ...handlers)
647
+ app.all(path, ...handlers)
932
648
 
933
- q.process('email', async (job) => { await sendEmail(job.payload) })
934
- q.cron('cleanup', '0 3 * * *', () => cleanup())
935
- await q.add('email', { to: 'user@example.com' })
936
- await q.add('remind', {}, { delay: 60_000 })
937
- q.run()
649
+ // WebSocket
650
+ app.ws(path, ...middlewares, handler)
651
+ // handler: { open?, message?, close?, error? }
652
+
653
+ // Middleware & mounting
654
+ app.use(middleware)
655
+ app.mount(prefix, router)
656
+ app.plugin(fn)
657
+ app.onError(handler)
658
+ app.routes() // debug: list all routes
938
659
  ```
939
660
 
940
661
  ---
@@ -943,49 +664,49 @@ q.run()
943
664
 
944
665
  ```
945
666
  src/
946
- ├── index.ts ← Entry, exports all modules
947
- ├── types.ts ← Context, Handler, Middleware types
948
- ├── core/ ← serve, router, ws, trace, logger
949
- ├── middleware/ ← cors, helmet, compress, rate-limit, upload, static, sandbox
950
- ├── user/ ← User system (CRUD, JWT, requireRole)
951
- ├── messager/ ← IM + AI conversation layer
952
- ├── kb/ ← RAG knowledge base (chunking, embedding, vector search)
953
- ├── ai/ ← AI Agent (LLM, tools, RAG)
954
- ├── cms/ ← Content management (blog, docs, changelog)
955
- ├── base/ ← Dynamic data engine (Fixed Slot)
956
- ├── postgres/ ← PostgreSQL client
957
- ├── redis/ ← Redis client
958
- ├── queue/ ← Job queue + cron
959
- ├── graphql.ts ← GraphQL
960
- ├── hub.ts ← WebSocket hub
961
- ├── ui/ ← ctx.ui.html / ctx.ui.js / ctx.ui.css
962
- ├── client/ ← Frontend framework
963
- │ ├── index.ts Entry (exports signal, useForm, wrap, createApp, ...)
964
- │ ├── signal.ts ← Signal / effect / computed
965
- │ ├── jsx-runtime.ts ← JSX → DOM / Show / For / wrap / ErrorBoundary / createPortal
966
- │ ├── app.ts ← createApp / hydrate / middleware chain
967
- │ ├── router.ts ← Route matching / RouteView / loader
968
- │ ├── types.ts ← WfuiContext / RouteDef
667
+ ├── index.ts ← Entry, exports all modules
668
+ ├── types.ts ← Context, Handler, Middleware types
669
+ ├── core/ ← serve, router, ws, trace, logger
670
+ ├── middleware/ ← cors, helmet, compress, rate-limit, upload, static, sandbox
671
+ ├── user/ ← User system (CRUD, JWT, requireRole)
672
+ ├── messager/ ← IM + AI conversation layer
673
+ ├── kb/ ← RAG knowledge base
674
+ ├── ai/ ← AI Agent (LLM, tools, RAG)
675
+ ├── cms/ ← Content management
676
+ ├── base/ ← Dynamic data engine
677
+ ├── postgres/ ← PostgreSQL client
678
+ ├── redis/ ← Redis client
679
+ ├── queue/ ← Job queue + cron
680
+ ├── graphql.ts ← GraphQL
681
+ ├── hub.ts ← WebSocket hub
682
+ ├── ui/ ← ctx.ui.html / ctx.ui.js / ctx.ui.css
683
+ ├── client/ ← Frontend framework
684
+ │ ├── index.ts Exports all client APIs
685
+ │ ├── signal.ts ← Signal / effect / computed
686
+ │ ├── jsx-runtime.ts ← JSX → DOM + types + Show/For/wrap/ErrorBoundary/createPortal
687
+ │ ├── app.ts ← createApp / hydrate / middleware chain
688
+ │ ├── router.ts ← Route matching / RouteView / loader
689
+ │ ├── types.ts ← WfuiContext / RouteDef
969
690
  │ ├── lib/
970
691
  │ │ └── form.ts ← useForm
971
692
  │ ├── middleware/
972
- │ │ ├── api.ts ← HTTP client
973
- │ │ ├── auth.ts ← Login / logout / token
974
- │ │ └── ws.ts ← WebSocket
693
+ │ │ ├── api.ts ← HTTP client
694
+ │ │ ├── auth.ts ← Login / logout / token
695
+ │ │ └── ws.ts ← WebSocket
975
696
  │ └── components/
976
- │ ├── LoginForm.ts ← Login / register form
977
- │ └── Chat.ts ← Real-time messaging
978
- └── test/ ← Tests
697
+ │ ├── LoginForm.ts
698
+ │ └── Chat.ts
699
+ └── test/ ← Tests
979
700
 
980
- apps/demo/ ← Full-stack demo
981
- ├── src/main.tsx ← SPA + SSR hydrate demo pages
982
- ├── server.ts ← weifuwu server
701
+ apps/demo/ ← Full-stack demo
702
+ ├── src/main.tsx ← SPA + SSR hydrate demo pages
703
+ ├── server.ts ← weifuwu server
983
704
  ├── public/
984
- │ ├── index.html ← HTML skeleton with placeholders
985
- │ └── style.css ← Demo styles
705
+ │ ├── index.html ← HTML skeleton
706
+ │ └── style.css ← Demo styles (Tailwind)
986
707
  └── tsconfig.json
987
708
 
988
- docker-compose.yml ← postgres (pgvector) + redis
709
+ docker-compose.yml ← postgres (pgvector) + redis
989
710
  ```
990
711
 
991
712
  ---
@@ -996,5 +717,5 @@ docker-compose.yml ← postgres (pgvector) + redis
996
717
  docker compose up -d # Start postgres + redis
997
718
  npm run build # esbuild → dist/
998
719
  npm run typecheck # tsc --noEmit
999
- npm test # 281 tests
720
+ npm test # Run tests
1000
721
  ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weifuwu",
3
3
  "type": "module",
4
- "version": "0.33.7",
4
+ "version": "0.33.8",
5
5
  "description": "AI SaaS framework — (req, ctx) => Response",
6
6
  "exports": {
7
7
  ".": {