weifuwu 0.33.7 → 0.33.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,88 +6,196 @@
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
+ ## Core Concept: `ctx`
14
+
15
+ **`ctx` 是整个框架的核心模式。** 后端和前端共享同一个理念:通过中间件向 `ctx` 注入能力,组件/handler 直接从 `ctx` 读取。
16
+
17
+ ```
18
+ 后端: 前端:
19
+ Request → 中间件链 → Handler createApp() → 中间件链 → 组件
20
+ │ │
21
+ ▼ ▼
22
+ ctx.sql ctx.api
23
+ ctx.user ctx.socket
24
+ ctx.kb ctx.route
25
+ ctx.agent ctx.user
26
+ ctx.redis ctx.app.navigate
27
+ ctx.messager ctx.provide/inject
28
+ ctx.cms
29
+ ctx.base
30
+ ctx.queue
31
+ ctx.ui
32
+ ```
33
+
34
+ **后端每个请求创建一个 ctx:**
35
+ ```ts
36
+ // 中间件注入 → handler 直接使用
37
+ app.get('/api/chat', async (req, ctx) => {
38
+ const user = ctx.user // 当前用户(auto-resolve from JWT)
39
+ const result = await ctx.kb.search(query) // RAG 知识库
40
+ return ctx.agent.chatStreamResponse({ messages }) // AI 流式响应
41
+ })
42
+ ```
43
+
44
+ **前端每个组件通过第二个参数接收 ctx:**
45
+ ```tsx
46
+ function ChatPage(_props: {}, ctx: WfuiContext) {
47
+ ctx.api.get('/api/posts') // HTTP 客户端
48
+ ctx.socket.send(data) // WebSocket
49
+ ctx.route.path // 当前路由
50
+ ctx.app.navigate('/about') // 页面导航
51
+ }
52
+ ```
53
+
54
+ 这种模式让开发者**无需 import 任何工具函数**,所有能力在 `ctx` 中一站获取。
55
+
56
+ ---
57
+
58
+ ## Module Overview
59
+
60
+ ### Backend — 每个模块向 ctx 注入什么
61
+
62
+ | Module | Import | Injects into `ctx` | Depends on | Purpose |
63
+ |--------|--------|-------------------|------------|---------|
64
+ | `postgres()` | `weifuwu` | `ctx.sql` | `DATABASE_URL` | PostgreSQL client |
65
+ | `redis()` | `weifuwu` | `ctx.redis` | `REDIS_URL` | Redis client |
66
+ | `user()` | `weifuwu` | `ctx.user`, `ctx.userModule` | `postgres()`, `JWT_SECRET` | Auth, JWT, roles |
67
+ | `messager()` | `weifuwu` | `ctx.messager` | `postgres()`, `user()` | IM + AI conversation |
68
+ | `kb()` | `weifuwu` | `ctx.kb` | `postgres()`, `DASHSCOPE_API_KEY` | RAG knowledge base |
69
+ | `agent()` | `weifuwu` | `ctx.agent` | — | LLM chat, tools, streaming |
70
+ | `cms()` | `weifuwu` | `ctx.cms` | `postgres()`, `user()` | Blog, docs, changelog |
71
+ | `base()` | `weifuwu` | `ctx.base` | `postgres()`, `user()` | Dynamic data engine |
72
+ | `queue()` | `weifuwu` | `ctx.queue` | `REDIS_URL` | Job queue + cron |
73
+ | `ui()` | `weifuwu` | `ctx.ui.html/js/css` | — | SSR/SPA rendering |
74
+
75
+ ### Backend Middleware
76
+
77
+ | Middleware | Injects into `ctx` | Purpose |
78
+ |-----------|-------------------|---------|
79
+ | `cors()` | — | CORS headers |
80
+ | `helmet()` | — | Security headers |
81
+ | `compress()` | — | gzip / brotli / deflate |
82
+ | `rateLimit()` | — | Sliding-window rate limiter |
83
+ | `logger()` | — | Request logging |
84
+ | `upload()` | `ctx.upload` | Multipart file upload |
85
+ | `serveStatic()` | — | Static files |
86
+ | `sandbox()` | — | Filesystem isolation |
87
+
88
+ ### Frontend (`weifuwu/client`) — 每个中间件向 ctx 注入什么
89
+
90
+ | Import | Type | Injects into `ctx` | Purpose |
91
+ |--------|------|-------------------|---------|
92
+ | `signal()` | function | — | Reactive state container |
93
+ | `computed()` | function | — | Derived signals |
94
+ | `effect()` | function | — | Auto-tracked side effects |
95
+ | `batch()` | function | — | Batch multiple signal writes |
96
+ | `untrack()` | function | — | Read signal without subscribing |
97
+ | `onMount()` | function | — | Component mount callback |
98
+ | `onCleanup()` | function | — | Component unmount callback |
99
+ | `api()` | **middleware** | `ctx.api.get/post/...` | HTTP client |
100
+ | `auth()` | **middleware** | `ctx.user/login/logout` | Auth state management |
101
+ | `socket()` | **middleware** | `ctx.socket.send/onMessage/...` | WebSocket client |
102
+ | `router()` | **middleware** | `ctx.route.path/params/query`, `ctx.app.navigate` | Hash/history router |
103
+ | `createApp()` | function | — | App instance |
104
+ | `<RouteView>` | component | — | Route outlet |
105
+ | `<Show>` | component | — | Conditional rendering |
106
+ | `<For>` | component | — | Keyed list rendering |
107
+ | `<Transition>` | component | — | Animated enter/leave |
108
+ | `<ErrorBoundary>` | component | — | Catch render errors |
109
+ | `<Link>` | component | — | SPA navigation link |
110
+ | `<LoginForm>` | component | — | Login/register form |
111
+ | `<Chat>` | component | — | Real-time messaging |
112
+ | `useForm()` | function | — | Form state management |
113
+ | `useModel()` | function | — | Two-way form binding |
114
+ | `reactiveArray()` | function | — | Reactive array with mut methods |
115
+ | `createResource()` | function | — | Async data (loading/error/data) |
116
+ | `createStyles()` | function | — | Scoped CSS |
117
+ | `createContext()` | function | — | Type-safe provide/inject |
118
+ | `createPortal()` | function | — | Render outside parent DOM |
119
+ | `wrap()` | function | — | Third-party lib integration |
120
+ | `enableDevtools()` | function | — | Dev warnings + browser inspector |
10
121
 
11
122
  ---
12
123
 
13
124
  ## Quick Start
14
125
 
15
- ### Backend
126
+ ### 完整全栈示例 — ctx 贯穿前后端
16
127
 
128
+ **后端 `server.ts`:**
17
129
  ```ts
18
- import { serve, Router, postgres, user, kb, agent, messager } from 'weifuwu'
130
+ import { serve, Router, postgres, user, agent, kb, messager, ui, cors, logger } from 'weifuwu'
19
131
  import { openai } from '@ai-sdk/openai'
20
132
 
21
133
  const app = new Router()
22
- app.use(postgres())
23
- app.use(user())
24
- app.use(kb())
25
- app.use(messager())
134
+ app.use(cors())
135
+ app.use(logger())
136
+ app.use(postgres()) // → ctx.sql
137
+ app.use(user()) // → ctx.user, ctx.userModule
138
+ app.use(kb()) // → ctx.kb
26
139
  app.use(agent({
27
140
  model: openai('deepseek-v4-flash', { baseURL: 'https://api.deepseek.com/v1' }),
28
141
  knowledge: { search: async (q, ctx) => ctx.kb.search(q) },
29
- }))
142
+ })) // → ctx.agent
143
+ app.use(messager()) // → ctx.messager
144
+ app.use(ui()) // → ctx.ui
30
145
 
146
+ // 业务 API — ctx 中所有能力可直接使用
31
147
  app.post('/api/chat', async (req, ctx) => {
32
148
  const { messages } = await req.json()
33
149
  return ctx.agent.chatStreamResponse({ messages })
34
150
  })
35
151
 
152
+ app.post('/api/messages', async (req, ctx) => {
153
+ const msg = await ctx.messager.sendMessage(ctx.params.conversationId, req.body)
154
+ return Response.json(msg, { status: 201 })
155
+ })
156
+
157
+ // 客户端编译(无需本地构建步骤)
158
+ app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
159
+
36
160
  serve(app, { port: 3000 })
37
161
  ```
38
162
 
39
- ### Frontend
40
-
163
+ **前端 `src/main.tsx` — ctx 驱动组件:**
41
164
  ```tsx
42
- import { signal, createApp, api, auth, ws, router, RouteView, LoginForm } from 'weifuwu/client'
165
+ import { signal, createApp, api, auth, socket, router, RouteView, LoginForm, Link } from 'weifuwu/client'
43
166
  import type { WfuiContext } from 'weifuwu/client'
44
167
 
45
168
  function AppShell(_props: {}, ctx: WfuiContext) {
46
169
  if (!ctx.isAuthenticated) return <LoginForm />
47
170
  return (
48
171
  <div>
49
- <nav><a onClick={() => ctx.app.navigate('/chat')}>聊天</a></nav>
172
+ <nav><Link to="/chat">Chat</Link></nav>
50
173
  <main><RouteView /></main>
51
174
  </div>
52
175
  )
53
176
  }
54
177
 
55
178
  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 / 路由
179
+ app.use(api()) // ctx.api.get/post/...
180
+ app.use(auth()) // ctx.user/login/logout
181
+ app.use(socket()) // ctx.socket.send/onMessage
182
+ app.use(router({ routes })) // ctx.route.path/params/query
60
183
  app.mount('#root', AppShell)
61
184
  ```
62
185
 
186
+ **`tsconfig.json`:**
63
187
  ```json
64
- // tsconfig.json
65
188
  { "jsx": "react-jsx", "jsxImportSource": "weifuwu/client" }
66
189
  ```
67
190
 
68
- ```js
69
- // build.mjs — 传统构建方式(可选)
70
- // 推荐:使用 ctx.ui.js() 服务端动态编译,无需独立构建脚本
71
- esbuild.build({
72
- entryPoints: ['src/main.tsx'],
73
- jsx: 'automatic',
74
- jsxImportSource: 'weifuwu/client',
75
- bundle: true,
76
- })
77
- ```
78
-
79
- 或用服务端动态编译——一行代码,无需构建步骤、无需 watch 模式:
80
- ```ts
81
- app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
82
- app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
83
- ```
191
+ ---
84
192
 
85
- ### Environment Variables
193
+ ## Environment Variables
86
194
 
87
195
  | Variable | Default | Used by |
88
196
  |----------|---------|---------|
89
197
  | `DATABASE_URL` | `postgres://root:123456@localhost:5432/demo` | `postgres()` |
90
- | `REDIS_URL` | `redis://localhost:6379` | `redis()` |
198
+ | `REDIS_URL` | `redis://localhost:6379` | `redis()`, `queue()` |
91
199
  | `JWT_SECRET` | — | `user()` |
92
200
  | `DASHSCOPE_API_KEY` | — | `kb()` (embedding) |
93
201
  | `DEEPSEEK_API_KEY` / `OPENAI_API_KEY` | — | `agent()` (LLM) |
@@ -95,572 +203,92 @@ app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
95
203
 
96
204
  ---
97
205
 
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
- ---
168
-
169
- ## Frontend (weifuwu/client)
170
-
171
- **weifuwu/client** is a reactive frontend framework built on Signal + TSX. Zero virtual DOM, zero dependencies, ~600 lines total.
172
-
173
- ### Concepts
206
+ ## Backend Modules
174
207
 
208
+ ### postgres
175
209
  ```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 — 数据预取
239
-
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
- ]
250
-
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
- }
257
- ```
258
-
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
276
-
277
- ```tsx
278
- import { LoginForm, Chat } from 'weifuwu/client'
279
-
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
- }
210
+ import { postgres } from 'weifuwu'
211
+ const sql = postgres()
212
+ app.use(sql) // ctx.sql
213
+ await ctx.sql`SELECT * FROM users WHERE id = ${id}`
214
+ await ctx.sql.begin(async (sql) => { /* transaction */ })
290
215
  ```
216
+ | Method | Description |
217
+ |--------|-------------|
218
+ | `ctx.sql\`...\`` | Tagged template SQL |
219
+ | `ctx.sql.begin(fn)` | Transaction |
220
+ | `sql.close()` | Close pool |
291
221
 
292
- ### SSR & SPA (ctx.ui.html / ctx.ui.js / ctx.ui.css)
293
-
222
+ ### redis
294
223
  ```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
436
-
437
- ```tsx
438
- // Conditional rendering (supports Signal)
439
- <Show when={isLoggedIn} fallback={<LoginPage />}>
440
- <Dashboard />
441
- </Show>
442
-
443
- // List rendering (supports Signal)
444
- <For each={filteredItems}>
445
- {(item) => <div>{item.name}</div>}
446
- </For>
224
+ import { redis } from 'weifuwu'
225
+ const r = redis()
226
+ app.use(r) // → ctx.redis
227
+ await ctx.redis.set('key', 'value')
228
+ await ctx.redis.get('key')
229
+ redis.close()
447
230
  ```
231
+ Reads `REDIS_URL` env (default: `redis://localhost:6379`).
448
232
 
449
- ---
450
-
451
- ## user
452
-
453
- Auth, registration, JWT, password management, roles.
454
-
233
+ ### user
455
234
  ```ts
456
235
  import { user, requireRole } from 'weifuwu'
457
-
458
236
  app.use(postgres())
459
237
  app.use(user({ secret: process.env.JWT_SECRET }))
238
+ // → ctx.user (UserRecord | undefined), ctx.userModule
460
239
 
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
467
- app.post('/api/login', async (req, ctx) => {
468
- const { email, password } = await req.json()
469
- 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
240
+ // ctx.user 由 Authorization Bearer token 或 token cookie 自动解析
474
241
  app.get('/api/me', async (req, ctx) => {
475
242
  if (!ctx.user) return new Response('Unauthorized', { status: 401 })
476
- return Response.json(ctx.user)
477
- })
478
- // Admin only
479
- app.get('/api/admin/users', requireRole('admin'), async (req, ctx) => {
480
- return Response.json(await ctx.userModule.listUsers())
243
+ return Response.json(ctx.user) // ctx.user.name / .email / .role
481
244
  })
482
245
  ```
483
-
484
- ### ctx.userModule API
485
-
486
- | Method | Returns | Description |
487
- |--------|---------|-------------|
246
+ | `ctx.userModule.*` | Returns | Description |
247
+ |--------------------|---------|-------------|
488
248
  | `register(input)` | `{ user, token }` | Register |
489
249
  | `login(email, pw)` | `{ user, token } \| null` | Login |
490
250
  | `getUserById(id)` | `UserRecord \| null` | Get by ID |
491
- | `getUserByEmail(email)` | `UserRecord \| null` | Get by email |
492
251
  | `updateUser(id, input)` | `UserRecord \| null` | Update |
493
252
  | `changePassword(id, oldPw, newPw)` | `boolean` | Change password |
494
253
  | `deleteUser(id)` | `boolean` | Soft delete |
495
254
  | `listUsers(inactive?)` | `UserRecord[]` | List users |
496
- | `generateToken(user)` | `string` | Issue JWT |
497
255
  | `verifyToken(token)` | `TokenPayload \| null` | Verify JWT |
498
- | `refreshToken(token)` | `string \| null` | Refresh JWT |
499
-
500
- ### ctx.user
501
-
502
- Auto-resolved from `Authorization: Bearer` or `token` cookie.
503
-
504
- ```ts
505
- interface User {
506
- id: string
507
- name: string
508
- email: string
509
- role: string
510
- [key: string]: unknown
511
- }
512
- ```
513
-
514
- ### requireRole
515
-
516
- ```ts
517
- app.get('/admin', requireRole('admin'), handler)
518
- // No auth → 401, wrong role → 403
519
- ```
520
-
521
- ### Security
522
256
 
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.
257
+ `requireRole('admin')` guard middleware. No auth → 401, wrong role → 403.
531
258
 
259
+ ### messager
532
260
  ```ts
533
261
  import { messager } from 'weifuwu'
534
-
535
262
  app.use(postgres())
536
263
  app.use(user())
537
- app.use(messager())
538
-
539
- // WebSocket — auto-join all user conversations
540
- app.ws('/ws', {
541
- async open(ws, ctx) {
542
- for (const c of await ctx.messager.getConversations()) {
543
- ctx.ws.join(`conversation:${c.id}`)
544
- }
545
- },
546
- })
547
-
548
- // REST API
549
- app.post('/api/messages', async (req, ctx) => {
550
- const { conversationId, body } = await req.json()
551
- const msg = await ctx.messager.sendMessage(conversationId, body)
552
- return Response.json(msg, { status: 201 })
553
- })
554
-
555
- app.get('/api/conversations/:id/messages', async (req, ctx) => {
556
- const url = new URL(req.url)
557
- return Response.json(await ctx.messager.getMessages(ctx.params.id, {
558
- before: url.searchParams.get('before') || undefined,
559
- limit: parseInt(url.searchParams.get('limit') || '50'),
560
- }))
561
- })
264
+ app.use(messager()) // → ctx.messager
562
265
  ```
563
-
564
- ### ctx.messager API
565
-
566
- | Method | Returns | Description |
567
- |--------|---------|-------------|
266
+ | `ctx.messager.*` | Returns | Description |
267
+ |------------------|---------|-------------|
268
+ | `sendMessage(convId, body)` | `Message` | Send + WebSocket broadcast |
269
+ | `getMessages(convId, opts?)` | `Message[]` | Cursor pagination |
270
+ | `getConversations()` | `Conversation[]` | List with unread |
568
271
  | `createDirectConversation(userId)` | `Conversation` | Create/reuse DM |
569
272
  | `createGroupConversation(title, userIds)` | `Conversation` | Create group |
570
- | `sendMessage(convId, body)` | `Message` | Send, auto-broadcast to `conversation:{id}` room |
571
- | `getMessages(convId, opts?)` | `Message[]` | Cursor pagination |
572
- | `editMessage(msgId, body)` | `Message \| null` | Edit (24h window) |
273
+ | `editMessage(msgId, body)` | `Message \| null` | Edit (24h) |
573
274
  | `deleteMessage(msgId)` | `boolean` | Soft delete |
574
- | `getConversations()` | `Conversation[]` | List with unread + last message |
575
- | `getConversation(id)` | `Conversation \| null` | Get detail |
576
275
  | `markRead(convId)` | `void` | Mark as read |
577
- | `getUnreadCount()` | `{ total, byConversation }` | Unread stats |
578
- | `addParticipants(convId, userIds)` | `void` | Add members |
579
- | `removeParticipant(convId, userId?)` | `boolean` | Leave / kick |
580
-
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
592
-
593
- RAG knowledge base. Import docs → auto-chunk → DashScope embedding → pgvector storage → semantic search.
594
276
 
277
+ ### kb — Knowledge Base
595
278
  ```ts
596
279
  import { kb } from 'weifuwu'
597
-
598
280
  app.use(postgres())
599
- app.use(kb())
600
-
601
- // Import
602
- app.post('/api/kb/import', async (req, ctx) => {
603
- const { title, content } = await req.json()
604
- const result = await ctx.kb.importText(title, content)
605
- return Response.json(result, { status: 201 })
606
- })
607
-
608
- // Search
609
- app.post('/api/kb/search', async (req, ctx) => {
610
- const { query } = await req.json()
611
- return Response.json(await ctx.kb.search(query, { limit: 5 }))
612
- })
281
+ app.use(kb()) // → ctx.kb
613
282
  ```
614
-
615
- ### ctx.kb API
616
-
617
- | Method | Returns | Description |
618
- |--------|---------|-------------|
619
- | `importText(title, text, opts?)` | `{ document, chunks }` | Import → chunk → embed → store |
620
- | `importDocuments(docs)` | `Document[]` | Batch import |
621
- | `search(query, opts?)` | `SearchResult[]` | Semantic search (cosine) |
283
+ | `ctx.kb.*` | Returns | Description |
284
+ |------------|---------|-------------|
285
+ | `importText(title, text)` | `{ document, chunks }` | Import → chunk → embed |
286
+ | `search(query, opts?)` | `SearchResult[]` | Semantic search |
622
287
  | `list()` | `Document[]` | List documents |
623
288
  | `get(id)` | `Document \| null` | Get document |
624
- | `getChunks(documentId)` | `Chunk[]` | Get chunks |
625
289
  | `delete(id)` | `boolean` | Delete + cascade chunks |
626
290
 
627
- ### Configuration
628
-
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
643
-
644
- ```ts
645
- 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
- },
650
- }))
651
- ```
652
-
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.
663
-
291
+ ### agent
664
292
  ```ts
665
293
  import { agent } from 'weifuwu'
666
294
  import { openai } from '@ai-sdk/openai'
@@ -670,9 +298,7 @@ import { z } from 'zod'
670
298
  app.use(agent({
671
299
  model: openai('deepseek-v4-flash', { baseURL: 'https://api.deepseek.com/v1' }),
672
300
  system: 'You are a helpful assistant.',
673
- knowledge: {
674
- search: async (query, ctx) => ctx.kb.search(query),
675
- },
301
+ knowledge: { search: async (q, ctx) => ctx.kb.search(q) },
676
302
  tools: {
677
303
  getWeather: tool({
678
304
  description: 'Get weather for a city',
@@ -681,260 +307,268 @@ app.use(agent({
681
307
  }),
682
308
  },
683
309
  maxSteps: 5,
684
- }))
685
-
686
- // Streaming
687
- app.post('/api/chat', async (req, ctx) => {
688
- const { messages } = await req.json()
689
- return ctx.agent.chatStreamResponse({ messages })
690
- })
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
- })
310
+ })) // → ctx.agent
698
311
  ```
699
-
700
- ### ctx.agent API
701
-
702
- | Method | Description |
703
- |-------------|-------------|
312
+ | `ctx.agent.*` | Description |
313
+ |--------------|-------------|
704
314
  | `chat(prompt, opts?)` | Non-streaming, returns text |
705
- | `chatStreamResponse({ messages })` | SSE stream (compatible with `useChat`) |
315
+ | `chatStreamResponse({ messages })` | SSE stream (useChat compatible) |
706
316
 
707
- ### Default Model
317
+ ### cms
318
+ ```ts
319
+ import { cms, requireRole } from 'weifuwu'
320
+ app.use(postgres())
321
+ app.use(user())
322
+ app.use(cms()) // → ctx.cms
323
+ ```
324
+ | `ctx.cms.*` | Returns | Description |
325
+ |-------------|---------|-------------|
326
+ | `create(input)` | `Content` | Create (admin) |
327
+ | `get(slug)` | `Content \| null` | Get by slug |
328
+ | `list(opts?)` | `Content[]` | List with filters |
329
+ | `publish(id)` / `unpublish(id)` | `Content \| null` | Toggle status |
708
330
 
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
331
+ ### base Dynamic Data Engine
332
+ ```ts
333
+ app.use(base()) // ctx.base
334
+ ctx.base.create({ name, tables })
335
+ ctx.base.insert(baseId, table, data)
336
+ ctx.base.query(baseId, table, { filter, limit })
337
+ ```
712
338
 
713
- ### Features
339
+ ### queue
340
+ ```ts
341
+ const q = queue()
342
+ app.use(q)
343
+ q.process('email', async (job) => { ... })
344
+ q.cron('cleanup', '0 3 * * *', () => ...)
345
+ await q.add('email', { to: 'user@example.com' })
346
+ q.run()
347
+ ```
714
348
 
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 |
349
+ ### ui SSR & SPA
350
+ ```ts
351
+ app.use(ui()) // ctx.ui
352
+ // SSR page
353
+ app.get('/blog/:slug', async (req, ctx) => ctx.ui.html`<!DOCTYPE html>...`)
354
+ // Dynamic JS compilation (no build step)
355
+ app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
356
+ // CSS with Tailwind v4 support
357
+ app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./public/style.css'))
358
+ ```
722
359
 
723
360
  ---
724
361
 
725
- ## cms
362
+ ## Frontend — weifuwu/client
726
363
 
727
- Content management blog, docs, changelog.
364
+ Reactive frontend framework. Zero virtual DOM, zero external dependencies.
728
365
 
729
- ```ts
730
- import { cms, requireRole } from 'weifuwu'
366
+ ### Core APIs
731
367
 
732
- app.use(postgres())
733
- app.use(user())
734
- app.use(cms())
368
+ ```tsx
369
+ // Signal — reactive state
370
+ const count = signal(0)
371
+ const doubled = computed(() => count.value * 2)
372
+ effect(() => console.log('count:', count.value))
735
373
 
736
- // Public
737
- app.get('/api/posts', async (req, ctx) => {
738
- return Response.json(await ctx.cms.list({ type: 'post', status: 'published' }))
739
- })
740
- app.get('/api/posts/:slug', async (req, ctx) => {
741
- const post = await ctx.cms.get(ctx.params.slug)
742
- if (!post) return new Response('Not found', { status: 404 })
743
- return Response.json(post)
744
- })
374
+ // Batch — merge multiple writes
375
+ batch(() => { a.value = 1; b.value = 2 })
745
376
 
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
- })
377
+ // Untrack — read without subscribing
378
+ effect(() => { console.log(untrack(() => theme.value)) })
379
+
380
+ // Lifecycle
381
+ onMount(() => { init(); return () => cleanup() })
382
+ onCleanup(() => clearInterval(id))
756
383
  ```
757
384
 
758
- ### ctx.cms API
385
+ ### Reactive Array
759
386
 
760
- | Method | Returns | Description |
761
- |--------|---------|-------------|
762
- | `create(input)` | `Content` | Create (admin) |
763
- | `get(slug)` | `Content \| null` | Get by slug |
764
- | `getById(id)` | `Content \| null` | Get by ID |
765
- | `update(id, input)` | `Content \| null` | Update (admin) |
766
- | `delete(id)` | `boolean` | Delete (admin) |
767
- | `list(opts?)` | `Content[]` | List with cursor |
768
- | `publish(id)` | `Content \| null` | Publish (admin) |
769
- | `unpublish(id)` | `Content \| null` | Unpublish (admin) |
770
- | `listTags()` | `TagWithCount[]` | List tags |
771
- | `createTag(name)` | `Tag` | Create tag |
772
-
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
387
+ ```tsx
388
+ const items = reactiveArray([1, 2, 3])
389
+ items.push(4) // [1, 2, 3, 4]
390
+ items.pop() // [1, 2, 3]
391
+ items.remove(1) // [1, 3]
392
+ items.clear() // []
393
+ items.sort()
394
+ items.reverse()
395
+ ```
781
396
 
782
- ---
397
+ ### Control Flow
783
398
 
784
- ## base
399
+ ```tsx
400
+ // Show — conditional rendering
401
+ <Show when={isLoggedIn} fallback={<LoginPage />}>
402
+ <Dashboard />
403
+ </Show>
785
404
 
786
- Dynamic data storage engine let users define their own data structures (like Airtable).
405
+ // Forkeyed list rendering
406
+ <For each={todos} keyBy="id">
407
+ {(todo) => <TodoItem todo={todo} />}
408
+ </For>
787
409
 
788
- ```ts
789
- import { base } from 'weifuwu'
410
+ // Transition — animated enter/leave
411
+ <Transition show={isOpen} name="fade">
412
+ <Modal />
413
+ </Transition>
790
414
 
791
- app.use(postgres())
792
- app.use(user())
793
- app.use(base())
415
+ // ErrorBoundary — catch render errors
416
+ <ErrorBoundary fallback={(e) => <p>{e.message}</p>} onError={reportError}>
417
+ {() => <Dashboard />}
418
+ </ErrorBoundary>
419
+ ```
794
420
 
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
- })
421
+ ### Form Handling
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 })
423
+ ```tsx
424
+ // useForm validation + submit
425
+ const form = useForm({
426
+ initial: { email: '', password: '' },
427
+ validate: { email: (v) => !v && '必填' },
428
+ validateOnInit: true,
805
429
  })
430
+ <input {...form.field('email')} placeholder="邮箱" />
431
+ <button onClick={() => form.submit(data => ctx.login(data))}>登录</button>
806
432
 
807
- app.get('/api/bases/:id/:table', async (req, ctx) => {
808
- const url = new URL(req.url)
809
- return Response.json(await ctx.base.query(ctx.params.id, ctx.params.table, {
810
- filter: url.searchParams.get('filter') ? JSON.parse(url.searchParams.get('filter')!) : undefined,
811
- limit: parseInt(url.searchParams.get('limit') || '50'),
812
- }))
813
- })
433
+ // useModel two-way binding (shorter)
434
+ const name = signal('')
435
+ const agreed = signal(false)
436
+ <input {...useModel(name)} placeholder="姓名" />
437
+ <input type="checkbox" {...useModel(agreed)} /> 同意
814
438
  ```
815
439
 
816
- ### ctx.base API
440
+ ### Async Data
817
441
 
818
- | Method | Returns | Description |
819
- |--------|---------|-------------|
820
- | `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
- | `insert(baseId, table, data)` | `Row` | Insert row |
825
- | `getRow(baseId, table, id)` | `Row \| null` | Get row |
826
- | `updateRow(baseId, table, id, data)` | `Row \| null` | Update row |
827
- | `deleteRow(baseId, table, id)` | `boolean` | Delete row |
828
- | `query(baseId, table, opts?)` | `Row[]` | Query (filter/sort/limit/offset) |
829
- | `search(baseId, table, field, query)` | `Row[]` | Full-text search |
830
- | `similaritySearch(baseId, table, field, vector)` | `Row[]` | Vector search |
831
- | `list()` / `get(id)` / `getBySlug(slug)` / `delete(id)` | — | Manage databases |
832
-
833
- ### Architecture
834
-
835
- Fixed Slot: a single `base_data` table with ~120 physical columns:
442
+ ```tsx
443
+ const [posts, { loading, error, refetch }] = createResource(
444
+ () => ctx.api.get('/api/posts'),
445
+ { retry: 2, timeout: 5000 }, // optional
446
+ )
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
+ <Show when={loading}><Skeleton /></Show>
449
+ <Show when={error}><Error /></Show>
450
+ <For each={posts}>{(post) => <PostCard post={post} />}</For>
451
+ ```
845
452
 
846
- Field name → physical column mapping stored in `base_column_map`. Fields beyond the physical columns overflow to ext JSONB.
453
+ ### Scoped CSS
847
454
 
848
- pgvector auto-detected (included in docker image).
455
+ ```tsx
456
+ const s = createStyles({
457
+ card: 'background: white; border-radius: 8px; padding: 16px;',
458
+ title: 'font-size: 18px; color: #333;',
459
+ })
460
+ <div class={s.card}><h2 class={s.title}>...</h2></div>
461
+ ```
849
462
 
850
- ---
463
+ ### Type-Safe Context
851
464
 
852
- ---
465
+ ```tsx
466
+ const ThemeCtx = createContext<string>('theme')
467
+ ThemeCtx.provide(ctx, 'dark') // 提供
468
+ const theme = ThemeCtx.inject(ctx) // string | null
469
+ ```
853
470
 
854
- ## Router
471
+ ### Third-Party Integration
855
472
 
856
- ```ts
857
- const app = new Router()
473
+ ```tsx
474
+ const PieChart = wrap('div', (el, props, ctx) => {
475
+ const chart = echarts.init(el)
476
+ chart.setOption({ series: [{ type: 'pie', data: props.data }] })
477
+ effect(() => chart.setOption(...))
478
+ return () => chart.dispose()
479
+ })
480
+ <Dashboard><PieChart data={salesData} /></Dashboard>
481
+ ```
858
482
 
859
- // HTTP
860
- app.get(path, ...handlers)
861
- app.post / put / delete / patch / head / options(path, ...handlers)
862
- app.all(path, ...handlers)
483
+ ### Pre-built Components
863
484
 
864
- // WebSocket
865
- app.ws(path, ...middlewares, handler)
866
- // handler: { open?, message?, close?, error? }
485
+ ```tsx
486
+ import { LoginForm, Chat, Link } from 'weifuwu/client'
867
487
 
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
488
+ <LoginForm /> // 登录/注册(自动切换模式)
489
+ <Chat conversationId="123" /> // 实时消息聊天
490
+ <Link to="/about">关于</Link> // SPA 导航(支持右键新标签页)
874
491
  ```
875
492
 
876
- ---
493
+ ### Router
877
494
 
878
- ## Middleware
495
+ ```tsx
496
+ const routes: RouteDef[] = [
497
+ { path: '/', component: HomePage, title: 'Home' },
498
+ { path: '/post/:id', component: PostPage,
499
+ loader: async (ctx) => ({ post: await ctx.api.get(`/api/posts/${ctx.route.params.id}`) }),
500
+ },
501
+ ]
879
502
 
880
- ```ts
881
- import { cors, helmet, compress, rateLimit, logger, upload, serveStatic, sandbox } from 'weifuwu'
503
+ app.use(router({ routes, notFound: NotFound, mode: 'hash', transition: 'page' }))
504
+ // ctx.route.path / .params / .query / .data / .loading
882
505
 
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' }))
506
+ // 路由出口
507
+ function AppShell() { return <main><RouteView /></main> }
891
508
  ```
892
509
 
893
- ---
894
-
895
- ## Postgres
510
+ ### DevTools
896
511
 
897
512
  ```ts
898
- import { postgres } from 'weifuwu'
899
-
900
- const sql = postgres()
901
- app.use(sql) // → ctx.sql
513
+ import { enableDevtools } from 'weifuwu/client'
514
+ if (import.meta.env.DEV) enableDevtools()
902
515
 
903
- await sql.sql`SELECT * FROM users WHERE id = ${id}`
904
- await sql.sql.begin(async (sql) => { /* transaction */ })
516
+ // 浏览器控制台:
517
+ // __wefu__.inspect() 查看所有 signal
518
+ // __wefu__.warnings() → 切换开发警告
905
519
  ```
906
520
 
907
- Reads `DATABASE_URL` env. Supports migrations, transactions, connection pool stats.
908
-
909
521
  ---
910
522
 
911
- ## Redis
523
+ ## From React to weifuwu/client
524
+
525
+ | React | weifuwu/client |
526
+ |-------|----------------|
527
+ | `useState(0)` | `signal(0)` |
528
+ | `useMemo(() => a * 2, [a])` | `computed(() => a.value * 2)` |
529
+ | `useEffect(() => {...}, [])` | `effect(() => {...})` + `onCleanup(() => {...})` |
530
+ | `{condition && <X/>}` | `<Show when={condition}><X/></Show>` |
531
+ | `{items.map(i => <X/>)}` | `<For each={items}>{(i) => <X/>}</For>` |
532
+ | `className={...}` | `class={...}` |
533
+ | Context Provider | `ctx.provide(key, val)` / `createContext()` |
534
+ | `useNavigate()` | `ctx.app.navigate(path)` |
535
+ | `useParams()` | `ctx.route.params` |
536
+ | `fetch / axios` | `ctx.api.get/post/...` |
537
+ | WebSocket | `ctx.socket.send/onMessage` |
538
+
539
+ **What you DON'T need:**
540
+ - No hooks rules — no `use` prefix, no rules-of-hooks
541
+ - No virtual DOM — JSX creates real DOM directly
542
+ - No dependency arrays — `effect()` auto-tracks
543
+ - No state management library — signal is the state management
544
+ - No Context.Provider component — just `ctx.provide/inject`
912
545
 
913
- ```ts
914
- import { redis } from 'weifuwu'
546
+ ---
915
547
 
916
- const r = redis()
917
- app.use(r) // → ctx.redis
918
- await r.redis.set('key', 'value')
919
- ```
548
+ ## Utils
920
549
 
921
- Reads `REDIS_URL` env (default: `redis://localhost:6379`).
550
+ | Import | Purpose |
551
+ |--------|---------|
552
+ | `requireRole('admin')` | Middleware factory: check `ctx.user.role` |
553
+ | `createHub()` | WebSocket pub/sub |
554
+ | `HttpError` | `throw new HttpError(msg, 404)` |
555
+ | `trace()` | Request tracing |
922
556
 
923
557
  ---
924
558
 
925
- ## Queue & Cron
559
+ ## Router (Backend)
926
560
 
927
561
  ```ts
928
- import { queue } from 'weifuwu'
929
-
930
- const q = queue()
931
- app.use(q) // → ctx.queue
932
-
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()
562
+ const app = new Router()
563
+ app.get(path, ...handlers)
564
+ app.post / put / delete / patch / head / options(path, ...handlers)
565
+ app.all(path, ...handlers)
566
+ app.ws(path, ...handler) // WebSocket
567
+ app.use(middleware) // Global middleware
568
+ app.mount(prefix, router) // Sub-router
569
+ app.plugin(fn) // Plugin
570
+ app.onError(handler) // Error handler
571
+ app.routes() // Debug: list all routes
938
572
  ```
939
573
 
940
574
  ---
@@ -943,49 +577,53 @@ q.run()
943
577
 
944
578
  ```
945
579
  src/
946
- ├── index.ts ← Entry, exports all modules
947
- ├── types.ts ← Context, Handler, Middleware types
948
- ├── core/ ← serve, router, ws, trace, logger
580
+ ├── index.ts ← Entry, exports all modules
581
+ ├── types.ts ← Context, Handler, Middleware types
582
+ ├── core/ ← serve, router, ws, trace, logger
949
583
  ├── 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
584
+ ├── user/ ← User system (CRUD, JWT, requireRole)
585
+ ├── messager/ ← IM + AI conversation layer
586
+ ├── kb/ ← RAG knowledge base
587
+ ├── ai/ ← AI Agent (LLM, tools, RAG)
588
+ ├── cms/ ← Content management
589
+ ├── base/ ← Dynamic data engine
590
+ ├── postgres/ ← PostgreSQL client
591
+ ├── redis/ ← Redis client
592
+ ├── queue/ ← Job queue + cron
593
+ ├── graphql.ts ← GraphQL
594
+ ├── hub.ts ← WebSocket hub
595
+ ├── ui/ ← ctx.ui.html / ctx.ui.js / ctx.ui.css
596
+ ├── client/ ← Frontend framework
597
+ │ ├── index.ts Exports all client APIs (33 exports)
598
+ │ ├── signal.ts ← Signal / effect / computed / batch / untrack
599
+ │ ├── jsx-runtime.ts ← JSX → DOM + types + Show/For/wrap/ErrorBoundary/Portal/Transition
600
+ │ ├── app.ts ← createApp / hydrate / middleware chain
601
+ │ ├── router.ts ← Route matching / RouteView / loader / transition
602
+ │ ├── types.ts ← WfuiContext / RouteDef / createContext
969
603
  │ ├── lib/
970
- │ │ └── form.ts ← useForm
604
+ │ │ ├── form.ts ← useForm (validated forms)
605
+ │ │ ├── model.ts ← useModel (two-way binding)
606
+ │ │ ├── resource.ts ← createResource (async data)
607
+ │ │ ├── css.ts ← createStyles (scoped CSS)
608
+ │ │ └── dev.ts ← enableDevtools (dev warnings)
971
609
  │ ├── middleware/
972
- │ │ ├── api.ts ← HTTP client
973
- │ │ ├── auth.ts Login / logout / token
974
- │ │ └── ws.ts ← WebSocket
610
+ │ │ ├── api.ts ← HTTP client → ctx.api
611
+ │ │ ├── auth.ts Auth → ctx.user/login/logout
612
+ │ │ └── ws.ts ← WebSocket → ctx.socket
975
613
  │ └── components/
976
- │ ├── LoginForm.ts ← Login / register form
977
- └── Chat.ts ← Real-time messaging
978
- └── test/ Tests
979
-
980
- apps/demo/ Full-stack demo
614
+ │ ├── LoginForm.tsx ← Login/register
615
+ ├── Chat.tsx ← Real-time chat
616
+ │ ├── Link.tsx SPA navigation
617
+ │ └── Transition.tsx ← Animated enter/leave
618
+ └── test/ 66 unit tests + 10 benchmarks
619
+
620
+ apps/demo/ ← Full-stack demo
621
+ ├── server.ts ← weifuwu server (user, kb, agent, messager 未演示)
981
622
  ├── src/main.tsx ← SPA + SSR hydrate demo pages
982
- ├── server.ts weifuwu server
983
- ├── public/
984
- │ ├── index.html ← HTML skeleton with placeholders
985
- │ └── style.css ← Demo styles
623
+ ├── public/ style.css (Tailwind + transition classes)
986
624
  └── tsconfig.json
987
625
 
988
- docker-compose.yml ← postgres (pgvector) + redis
626
+ docker-compose.yml ← postgres (pgvector) + redis
989
627
  ```
990
628
 
991
629
  ---
@@ -996,5 +634,18 @@ docker-compose.yml ← postgres (pgvector) + redis
996
634
  docker compose up -d # Start postgres + redis
997
635
  npm run build # esbuild → dist/
998
636
  npm run typecheck # tsc --noEmit
999
- npm test # 281 tests
637
+ npm test # Run all tests
1000
638
  ```
639
+
640
+ ---
641
+
642
+ ## Performance (Client Benchmarks)
643
+
644
+ | Operation | Throughput |
645
+ |-----------|-----------|
646
+ | Signal creation | ~10,000 ops/ms |
647
+ | Signal read/write | ~9,600 ops/ms |
648
+ | Notify 10,000 effects | ~2,600 ops/ms |
649
+ | batch merge 10,000 writes | ~0.6ms |
650
+ | JSX div creation | ~200 ops/ms |
651
+ | For render 10,000 items | ~109 ops/ms |