weifuwu 0.33.8 → 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
@@ -10,333 +10,285 @@ One package. Backend + frontend. User system, messaging, RAG knowledge base, AI
10
10
 
11
11
  ---
12
12
 
13
- ## Module Overview
13
+ ## Core Concept: `ctx`
14
14
 
15
- ### Backend
15
+ **`ctx` 是整个框架的核心模式。** 后端和前端共享同一个理念:通过中间件向 `ctx` 注入能力,组件/handler 直接从 `ctx` 读取。
16
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`) |
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
+ ```
29
33
 
30
- ### Middleware
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
+ ```
31
43
 
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
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
+ ```
68
53
 
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 |
54
+ 这种模式让开发者**无需 import 任何工具函数**,所有能力在 `ctx` 中一站获取。
75
55
 
76
56
  ---
77
57
 
78
- ## Environment Variables
58
+ ## Module Overview
79
59
 
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()` |
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 |
88
121
 
89
122
  ---
90
123
 
91
124
  ## Quick Start
92
125
 
93
- ### Backend
126
+ ### 完整全栈示例 — ctx 贯穿前后端
94
127
 
128
+ **后端 `server.ts`:**
95
129
  ```ts
96
- import { serve, Router, postgres, user, kb, agent, messager } from 'weifuwu'
130
+ import { serve, Router, postgres, user, agent, kb, messager, ui, cors, logger } from 'weifuwu'
97
131
  import { openai } from '@ai-sdk/openai'
98
132
 
99
133
  const app = new Router()
100
- app.use(postgres())
101
- app.use(user())
102
- app.use(kb())
103
- 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
104
139
  app.use(agent({
105
140
  model: openai('deepseek-v4-flash', { baseURL: 'https://api.deepseek.com/v1' }),
106
141
  knowledge: { search: async (q, ctx) => ctx.kb.search(q) },
107
- }))
142
+ })) // → ctx.agent
143
+ app.use(messager()) // → ctx.messager
144
+ app.use(ui()) // → ctx.ui
108
145
 
146
+ // 业务 API — ctx 中所有能力可直接使用
109
147
  app.post('/api/chat', async (req, ctx) => {
110
148
  const { messages } = await req.json()
111
149
  return ctx.agent.chatStreamResponse({ messages })
112
150
  })
113
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
+
114
160
  serve(app, { port: 3000 })
115
161
  ```
116
162
 
117
- ### Frontend
118
-
163
+ **前端 `src/main.tsx` — ctx 驱动组件:**
119
164
  ```tsx
120
- 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'
121
166
  import type { WfuiContext } from 'weifuwu/client'
122
167
 
123
168
  function AppShell(_props: {}, ctx: WfuiContext) {
124
169
  if (!ctx.isAuthenticated) return <LoginForm />
125
170
  return (
126
171
  <div>
127
- <nav><a onClick={() => ctx.app.navigate('/chat')}>Chat</a></nav>
172
+ <nav><Link to="/chat">Chat</Link></nav>
128
173
  <main><RouteView /></main>
129
174
  </div>
130
175
  )
131
176
  }
132
177
 
133
178
  const app = createApp()
134
- app.use(api())
135
- app.use(auth())
136
- app.use(ws())
137
- app.use(router({ routes }))
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
138
183
  app.mount('#root', AppShell)
139
184
  ```
140
185
 
186
+ **`tsconfig.json`:**
141
187
  ```json
142
- // tsconfig.json
143
188
  { "jsx": "react-jsx", "jsxImportSource": "weifuwu/client" }
144
189
  ```
145
190
 
146
- Build (traditional, or use `ctx.ui.js()` for dynamic compilation):
147
-
148
- ```js
149
- import esbuild from 'esbuild'
150
- esbuild.build({
151
- entryPoints: ['src/main.tsx'],
152
- jsx: 'automatic',
153
- jsxImportSource: 'weifuwu/client',
154
- bundle: true,
155
- })
156
- ```
191
+ ---
157
192
 
158
- Or skip the build step entirely with server-side compilation:
193
+ ## Environment Variables
159
194
 
160
- ```ts
161
- app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
162
- app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
163
- ```
195
+ | Variable | Default | Used by |
196
+ |----------|---------|---------|
197
+ | `DATABASE_URL` | `postgres://root:123456@localhost:5432/demo` | `postgres()` |
198
+ | `REDIS_URL` | `redis://localhost:6379` | `redis()`, `queue()` |
199
+ | `JWT_SECRET` | — | `user()` |
200
+ | `DASHSCOPE_API_KEY` | — | `kb()` (embedding) |
201
+ | `DEEPSEEK_API_KEY` / `OPENAI_API_KEY` | — | `agent()` (LLM) |
202
+ | `DEEPSEEK_MODEL` | `deepseek-v4-flash` | `agent()` |
164
203
 
165
204
  ---
166
205
 
167
206
  ## Backend Modules
168
207
 
169
- Each module follows: import → usage → API table.
170
-
171
208
  ### postgres
172
-
173
209
  ```ts
174
210
  import { postgres } from 'weifuwu'
175
-
176
211
  const sql = postgres()
177
212
  app.use(sql) // → ctx.sql
178
-
179
213
  await ctx.sql`SELECT * FROM users WHERE id = ${id}`
180
214
  await ctx.sql.begin(async (sql) => { /* transaction */ })
181
215
  ```
182
-
183
216
  | Method | Description |
184
217
  |--------|-------------|
185
- | `ctx.sql\`...\`` | Tagged template SQL queries |
218
+ | `ctx.sql\`...\`` | Tagged template SQL |
186
219
  | `ctx.sql.begin(fn)` | Transaction |
187
220
  | `sql.close()` | Close pool |
188
221
 
189
- Reads `DATABASE_URL` env. Supports migrations via `postgres({ migrate: { directory: './migrations' } })`.
190
-
191
222
  ### redis
192
-
193
223
  ```ts
194
224
  import { redis } from 'weifuwu'
195
-
196
225
  const r = redis()
197
226
  app.use(r) // → ctx.redis
198
-
199
227
  await ctx.redis.set('key', 'value')
200
228
  await ctx.redis.get('key')
229
+ redis.close()
201
230
  ```
202
-
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 |
209
-
210
231
  Reads `REDIS_URL` env (default: `redis://localhost:6379`).
211
232
 
212
233
  ### user
213
-
214
234
  ```ts
215
235
  import { user, requireRole } from 'weifuwu'
216
-
217
236
  app.use(postgres())
218
237
  app.use(user({ secret: process.env.JWT_SECRET }))
238
+ // → ctx.user (UserRecord | undefined), ctx.userModule
219
239
 
220
- app.post('/api/register', async (req, ctx) =>
221
- Response.json(await ctx.userModule.register(await req.json())))
222
-
223
- app.post('/api/login', async (req, ctx) => {
224
- const { email, password } = await req.json()
225
- const result = await ctx.userModule.login(email, password)
226
- return result ? Response.json(result) : new Response('Unauthorized', { status: 401 })
240
+ // ctx.user Authorization Bearer token 或 token cookie 自动解析
241
+ app.get('/api/me', async (req, ctx) => {
242
+ if (!ctx.user) return new Response('Unauthorized', { status: 401 })
243
+ return Response.json(ctx.user) // ctx.user.name / .email / .role
227
244
  })
228
-
229
- app.get('/api/me', async (req, ctx) =>
230
- ctx.user ? Response.json(ctx.user) : new Response('Unauthorized', { status: 401 }))
231
245
  ```
232
-
233
- | Method | Returns | Description |
234
- |--------|---------|-------------|
246
+ | `ctx.userModule.*` | Returns | Description |
247
+ |--------------------|---------|-------------|
235
248
  | `register(input)` | `{ user, token }` | Register |
236
249
  | `login(email, pw)` | `{ user, token } \| null` | Login |
237
250
  | `getUserById(id)` | `UserRecord \| null` | Get by ID |
238
- | `getUserByEmail(email)` | `UserRecord \| null` | Get by email |
239
251
  | `updateUser(id, input)` | `UserRecord \| null` | Update |
240
252
  | `changePassword(id, oldPw, newPw)` | `boolean` | Change password |
241
253
  | `deleteUser(id)` | `boolean` | Soft delete |
242
254
  | `listUsers(inactive?)` | `UserRecord[]` | List users |
243
- | `generateToken(user)` | `string` | Issue JWT |
244
255
  | `verifyToken(token)` | `TokenPayload \| null` | Verify JWT |
245
256
 
246
- `ctx.user` — auto-resolved from `Authorization: Bearer` or `token` cookie. Fields: `id, name, email, role, [key: string]`.
247
-
248
257
  `requireRole('admin')` — guard middleware. No auth → 401, wrong role → 403.
249
258
 
250
- Password: scrypt + 32-byte random salt. Token: HMAC SHA-256, 7 day expiry.
251
-
252
259
  ### messager
253
-
254
260
  ```ts
255
261
  import { messager } from 'weifuwu'
256
-
257
262
  app.use(postgres())
258
263
  app.use(user())
259
- app.use(messager())
260
-
261
- app.ws('/ws', {
262
- async open(ws, ctx) {
263
- for (const c of await ctx.messager.getConversations())
264
- ctx.ws.join(`conversation:${c.id}`)
265
- },
266
- })
267
-
268
- app.post('/api/messages', async (req, ctx) => {
269
- const { conversationId, body } = await req.json()
270
- const msg = await ctx.messager.sendMessage(conversationId, body)
271
- return Response.json(msg, { status: 201 })
272
- })
273
-
274
- app.get('/api/conversations/:id/messages', async (req, ctx) => {
275
- const url = new URL(req.url)
276
- return Response.json(await ctx.messager.getMessages(ctx.params.id, {
277
- before: url.searchParams.get('before') || undefined,
278
- limit: parseInt(url.searchParams.get('limit') || '50'),
279
- }))
280
- })
264
+ app.use(messager()) // → ctx.messager
281
265
  ```
282
-
283
- | Method | Returns | Description |
284
- |--------|---------|-------------|
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 |
285
271
  | `createDirectConversation(userId)` | `Conversation` | Create/reuse DM |
286
272
  | `createGroupConversation(title, userIds)` | `Conversation` | Create group |
287
- | `sendMessage(convId, body)` | `Message` | Send + broadcast to room |
288
- | `getMessages(convId, opts?)` | `Message[]` | Cursor pagination |
289
- | `editMessage(msgId, body)` | `Message \| null` | Edit (24h window) |
273
+ | `editMessage(msgId, body)` | `Message \| null` | Edit (24h) |
290
274
  | `deleteMessage(msgId)` | `boolean` | Soft delete |
291
- | `getConversations()` | `Conversation[]` | List with unread + last message |
292
- | `getConversation(id)` | `Conversation \| null` | Get detail |
293
275
  | `markRead(convId)` | `void` | Mark as read |
294
- | `addParticipants(convId, userIds)` | `void` | Add members |
295
- | `removeParticipant(convId, userId?)` | `boolean` | Leave / kick |
296
-
297
- Storage: 3 auto-migrated tables (conversations, participants, messages). WebSocket push via rooms.
298
276
 
299
277
  ### kb — Knowledge Base
300
-
301
278
  ```ts
302
279
  import { kb } from 'weifuwu'
303
-
304
280
  app.use(postgres())
305
- app.use(kb())
306
-
307
- app.post('/api/kb/import', async (req, ctx) => {
308
- const { title, content } = await req.json()
309
- return Response.json(await ctx.kb.importText(title, content), { status: 201 })
310
- })
311
-
312
- app.post('/api/kb/search', async (req, ctx) => {
313
- const { query } = await req.json()
314
- return Response.json(await ctx.kb.search(query, { limit: 5 }))
315
- })
281
+ app.use(kb()) // → ctx.kb
316
282
  ```
317
-
318
- | Method | Returns | Description |
319
- |--------|---------|-------------|
320
- | `importText(title, text, opts?)` | `{ document, chunks }` | Import chunk → embed → store |
321
- | `importDocuments(docs)` | `Document[]` | Batch import |
322
- | `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 |
323
287
  | `list()` | `Document[]` | List documents |
324
288
  | `get(id)` | `Document \| null` | Get document |
325
289
  | `delete(id)` | `boolean` | Delete + cascade chunks |
326
290
 
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).
328
-
329
- Integration with agent:
330
-
331
- ```ts
332
- app.use(agent({
333
- model: openai('deepseek-v4-flash', ...),
334
- knowledge: { search: async (query, ctx) => ctx.kb.search(query) },
335
- }))
336
- ```
337
-
338
291
  ### agent
339
-
340
292
  ```ts
341
293
  import { agent } from 'weifuwu'
342
294
  import { openai } from '@ai-sdk/openai'
@@ -355,307 +307,268 @@ app.use(agent({
355
307
  }),
356
308
  },
357
309
  maxSteps: 5,
358
- }))
359
-
360
- app.post('/api/chat', async (req, ctx) => {
361
- const { messages } = await req.json()
362
- return ctx.agent.chatStreamResponse({ messages })
363
- })
310
+ })) // → ctx.agent
364
311
  ```
365
-
366
- | Method | Description |
367
- |-------------|-------------|
312
+ | `ctx.agent.*` | Description |
313
+ |--------------|-------------|
368
314
  | `chat(prompt, opts?)` | Non-streaming, returns text |
369
- | `chatStreamResponse({ messages })` | SSE stream (compatible with `useChat`) |
370
-
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`.
372
-
373
- Features: `knowledge.search` (RAG), `tools` (auto-loop with maxSteps), `sandbox: true` (filesystem isolation), `store` (session persistence), `agents` (multi-agent).
315
+ | `chatStreamResponse({ messages })` | SSE stream (useChat compatible) |
374
316
 
375
317
  ### cms
376
-
377
318
  ```ts
378
319
  import { cms, requireRole } from 'weifuwu'
379
-
380
320
  app.use(postgres())
381
321
  app.use(user())
382
- app.use(cms())
383
-
384
- app.get('/api/posts', async (req, ctx) =>
385
- Response.json(await ctx.cms.list({ type: 'post', status: 'published' })))
386
-
387
- app.get('/api/posts/:slug', async (req, ctx) => {
388
- const post = await ctx.cms.get(ctx.params.slug)
389
- return post ? Response.json(post) : new Response('Not found', { status: 404 })
390
- })
391
-
392
- app.post('/api/admin/posts', requireRole('admin'), async (req, ctx) =>
393
- Response.json(await ctx.cms.create(await req.json()), { status: 201 }))
322
+ app.use(cms()) // → ctx.cms
394
323
  ```
395
-
396
- | Method | Returns | Description |
397
- |--------|---------|-------------|
324
+ | `ctx.cms.*` | Returns | Description |
325
+ |-------------|---------|-------------|
398
326
  | `create(input)` | `Content` | Create (admin) |
399
327
  | `get(slug)` | `Content \| null` | Get by slug |
400
- | `getById(id)` | `Content \| null` | Get by ID |
401
- | `update(id, input)` | `Content \| null` | Update (admin) |
402
- | `delete(id)` | `boolean` | Delete (admin) |
403
- | `list(opts?)` | `Content[]` | List with cursor + filters |
404
- | `publish(id)` | `Content \| null` | Publish (admin) |
405
- | `unpublish(id)` | `Content \| null` | Unpublish (admin) |
406
- | `listTags()` | `TagWithCount[]` | List tags |
407
- | `createTag(name)` | `Tag` | Create tag |
408
-
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.
328
+ | `list(opts?)` | `Content[]` | List with filters |
329
+ | `publish(id)` / `unpublish(id)` | `Content \| null` | Toggle status |
410
330
 
411
331
  ### base — Dynamic Data Engine
412
-
413
332
  ```ts
414
- import { base } from 'weifuwu'
415
-
416
- app.use(postgres())
417
- app.use(user())
418
- app.use(base())
419
-
420
- app.post('/api/bases', async (req, ctx) =>
421
- Response.json(await ctx.base.create(await req.json()), { status: 201 }))
422
-
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 }))
425
-
426
- app.get('/api/bases/:id/:table', async (req, ctx) => {
427
- const url = new URL(req.url)
428
- return Response.json(await ctx.base.query(ctx.params.id, ctx.params.table, {
429
- filter: url.searchParams.get('filter') ? JSON.parse(url.searchParams.get('filter')!) : undefined,
430
- limit: parseInt(url.searchParams.get('limit') || '50'),
431
- }))
432
- })
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 })
433
337
  ```
434
338
 
435
- | Method | Returns | Description |
436
- |--------|---------|-------------|
437
- | `create({ name, tables })` | `BaseDef` | Create database |
438
- | `insert(baseId, table, data)` | `Row` | Insert row |
439
- | `getRow(baseId, table, id)` | `Row \| null` | Get row |
440
- | `updateRow(baseId, table, id, data)` | `Row \| null` | Update row |
441
- | `deleteRow(baseId, table, id)` | `boolean` | Delete row |
442
- | `query(baseId, table, opts?)` | `Row[]` | Query (filter/sort/limit/offset) |
443
- | `search(baseId, table, field, query)` | `Row[]` | Full-text search |
444
- | `similaritySearch(baseId, table, field, vector)` | `Row[]` | Vector search |
445
-
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.
447
-
448
339
  ### queue
449
-
450
340
  ```ts
451
- import { queue } from 'weifuwu'
452
-
453
341
  const q = queue()
454
- app.use(q) // → ctx.queue
455
-
456
- q.process('email', async (job) => { await sendEmail(job.payload) })
457
- q.cron('cleanup', '0 3 * * *', () => cleanup())
342
+ app.use(q)
343
+ q.process('email', async (job) => { ... })
344
+ q.cron('cleanup', '0 3 * * *', () => ...)
458
345
  await q.add('email', { to: 'user@example.com' })
459
346
  q.run()
460
347
  ```
461
348
 
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 |
469
-
470
349
  ### ui — SSR & SPA
471
-
472
350
  ```ts
473
- import { ui } from 'weifuwu'
474
-
475
- app.use(ui())
476
-
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>`)
483
-
484
- // Dynamic JS compilation (no build step needed)
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)
485
355
  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'))
356
+ // CSS with Tailwind v4 support
357
+ app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./public/style.css'))
489
358
  ```
490
359
 
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
-
500
360
  ---
501
361
 
502
362
  ## Frontend — weifuwu/client
503
363
 
504
- Reactive frontend framework. Zero virtual DOM, zero external dependencies. Component model: `(props, ctx) => Node`.
364
+ Reactive frontend framework. Zero virtual DOM, zero external dependencies.
505
365
 
506
- ### Concepts
366
+ ### Core APIs
507
367
 
508
368
  ```tsx
509
- // Signal — reactive data
369
+ // Signal — reactive state
510
370
  const count = signal(0)
511
- count.value = count.value + 1 // DOM updates automatically
512
-
513
- // Computed — derived signals
514
371
  const doubled = computed(() => count.value * 2)
515
-
516
- // Effect — auto-tracked side effects
517
372
  effect(() => console.log('count:', count.value))
518
373
 
519
- // Component(props, ctx) => JSX
520
- function MyComponent({ name }: { name: string }, ctx: WfuiContext) {
521
- return <div>Hello {name}</div>
522
- }
374
+ // Batchmerge multiple writes
375
+ batch(() => { a.value = 1; b.value = 2 })
376
+
377
+ // Untrack — read without subscribing
378
+ effect(() => { console.log(untrack(() => theme.value)) })
379
+
380
+ // Lifecycle
381
+ onMount(() => { init(); return () => cleanup() })
382
+ onCleanup(() => clearInterval(id))
523
383
  ```
524
384
 
525
- ### createApp + Middleware
385
+ ### Reactive Array
526
386
 
527
387
  ```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
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
+ ```
396
+
397
+ ### Control Flow
398
+
399
+ ```tsx
400
+ // Show — conditional rendering
401
+ <Show when={isLoggedIn} fallback={<LoginPage />}>
402
+ <Dashboard />
403
+ </Show>
404
+
405
+ // For — keyed list rendering
406
+ <For each={todos} keyBy="id">
407
+ {(todo) => <TodoItem todo={todo} />}
408
+ </For>
533
409
 
534
- app.mount('#root', AppShell) // SPA mode
535
- app.hydrate('#comments', Comments) // SSR hydration (doesn't clear DOM)
410
+ // Transition — animated enter/leave
411
+ <Transition show={isOpen} name="fade">
412
+ <Modal />
413
+ </Transition>
414
+
415
+ // ErrorBoundary — catch render errors
416
+ <ErrorBoundary fallback={(e) => <p>{e.message}</p>} onError={reportError}>
417
+ {() => <Dashboard />}
418
+ </ErrorBoundary>
536
419
  ```
537
420
 
538
- ### Router
421
+ ### Form Handling
539
422
 
540
423
  ```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
- ]
424
+ // useForm validation + submit
425
+ const form = useForm({
426
+ initial: { email: '', password: '' },
427
+ validate: { email: (v) => !v && '必填' },
428
+ validateOnInit: true,
429
+ })
430
+ <input {...form.field('email')} placeholder="邮箱" />
431
+ <button onClick={() => form.submit(data => ctx.login(data))}>登录</button>
432
+
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)} /> 同意
438
+ ```
547
439
 
548
- app.use(router({ routes, notFound: NotFound, mode: 'hash' }))
440
+ ### Async Data
549
441
 
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
442
+ ```tsx
443
+ const [posts, { loading, error, refetch }] = createResource(
444
+ () => ctx.api.get('/api/posts'),
445
+ { retry: 2, timeout: 5000 }, // optional
446
+ )
447
+
448
+ <Show when={loading}><Skeleton /></Show>
449
+ <Show when={error}><Error /></Show>
450
+ <For each={posts}>{(post) => <PostCard post={post} />}</For>
555
451
  ```
556
452
 
557
- Loader data flow: component renders immediately (shows loading state), loader completes → re-render with data.
453
+ ### Scoped CSS
454
+
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
+ ```
558
462
 
559
- ### wrap() — Third-party Library Integration
463
+ ### Type-Safe Context
560
464
 
561
465
  ```tsx
562
- import { wrap, effect } from 'weifuwu/client'
563
- import * as echarts from 'echarts'
466
+ const ThemeCtx = createContext<string>('theme')
467
+ ThemeCtx.provide(ctx, 'dark') // 提供
468
+ const theme = ThemeCtx.inject(ctx) // string | null
469
+ ```
564
470
 
565
- const PieChart = wrap('div', (el, props: { data: any[] }, ctx) => {
471
+ ### Third-Party Integration
472
+
473
+ ```tsx
474
+ const PieChart = wrap('div', (el, props, ctx) => {
566
475
  const chart = echarts.init(el)
567
476
  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
477
+ effect(() => chart.setOption(...))
478
+ return () => chart.dispose()
570
479
  })
571
-
572
- // Use as regular component:
573
480
  <Dashboard><PieChart data={salesData} /></Dashboard>
574
481
  ```
575
482
 
576
- ### useForm() — Form State Management
483
+ ### Pre-built Components
577
484
 
578
485
  ```tsx
579
- import { useForm } from 'weifuwu/client'
486
+ import { LoginForm, Chat, Link } from 'weifuwu/client'
580
487
 
581
- const form = useForm({
582
- initial: { email: '', password: '' },
583
- validate: {
584
- email: (v) => !v.includes('@') && 'Invalid email',
585
- },
586
- })
488
+ <LoginForm /> // 登录/注册(自动切换模式)
489
+ <Chat conversationId="123" /> // 实时消息聊天
490
+ <Link to="/about">关于</Link> // SPA 导航(支持右键新标签页)
491
+ ```
587
492
 
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>}
493
+ ### Router
494
+
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
+ ]
591
502
 
592
- // Auto-validates all fields before calling handler
593
- <button onClick={() => form.submit((data) => ctx.login(data.email, data.password))}>
594
- Login
595
- </button>
503
+ app.use(router({ routes, notFound: NotFound, mode: 'hash', transition: 'page' }))
504
+ // ctx.route.path / .params / .query / .data / .loading
596
505
 
597
- // Programmatic control
598
- form.setValue('email', 'a@b.com')
599
- form.setValues({ email: 'a@b.com', password: '123' })
600
- form.reset()
506
+ // 路由出口
507
+ function AppShell() { return <main><RouteView /></main> }
601
508
  ```
602
509
 
603
- ### createPortal & ErrorBoundary
510
+ ### DevTools
604
511
 
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>
512
+ ```ts
513
+ import { enableDevtools } from 'weifuwu/client'
514
+ if (import.meta.env.DEV) enableDevtools()
615
515
 
616
- // ErrorBoundary — catch render errors, children must be a thunk
617
- <ErrorBoundary fallback={(e) => <p>Error: {e.message}</p>}>
618
- {() => <Dashboard />}
619
- </ErrorBoundary>
516
+ // 浏览器控制台:
517
+ // __wefu__.inspect() 查看所有 signal
518
+ // __wefu__.warnings() 切换开发警告
620
519
  ```
621
520
 
622
- ### Pre-built Components
521
+ ---
623
522
 
624
- ```tsx
625
- import { LoginForm, Chat } from 'weifuwu/client'
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`
626
545
 
627
- function LoginPage(_, ctx) {
628
- if (ctx.isAuthenticated) return ctx.app.navigate('/')
629
- return <LoginForm />
630
- }
546
+ ---
631
547
 
632
- function ChatPage(_, ctx) {
633
- return <Chat conversationId="123" />
634
- }
635
- ```
548
+ ## Utils
549
+
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 |
636
556
 
637
557
  ---
638
558
 
639
- ## Router
559
+ ## Router (Backend)
640
560
 
641
561
  ```ts
642
562
  const app = new Router()
643
-
644
- // HTTP
645
563
  app.get(path, ...handlers)
646
564
  app.post / put / delete / patch / head / options(path, ...handlers)
647
565
  app.all(path, ...handlers)
648
-
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
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
659
572
  ```
660
573
 
661
574
  ---
@@ -664,49 +577,53 @@ app.routes() // debug: list all routes
664
577
 
665
578
  ```
666
579
  src/
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
580
+ ├── index.ts ← Entry, exports all modules
581
+ ├── types.ts ← Context, Handler, Middleware types
582
+ ├── core/ ← serve, router, ws, trace, logger
583
+ ├── middleware/ ← cors, helmet, compress, rate-limit, upload, static, sandbox
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
690
603
  │ ├── lib/
691
- │ │ └── 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)
692
609
  │ ├── middleware/
693
- │ │ ├── api.ts ← HTTP client
694
- │ │ ├── auth.ts Login / logout / token
695
- │ │ └── ws.ts ← WebSocket
610
+ │ │ ├── api.ts ← HTTP client → ctx.api
611
+ │ │ ├── auth.ts Auth → ctx.user/login/logout
612
+ │ │ └── ws.ts ← WebSocket → ctx.socket
696
613
  │ └── components/
697
- │ ├── LoginForm.ts
698
- └── Chat.ts
699
- └── test/ Tests
700
-
701
- apps/demo/ Full-stack demo
702
- ├── src/main.tsx ← SPA + SSR hydrate demo pages
703
- ├── server.ts weifuwu server
704
- ├── public/
705
- ├── index.html HTML skeleton
706
- │ └── style.css Demo styles (Tailwind)
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 未演示)
622
+ ├── src/main.tsx SPA + SSR hydrate demo pages
623
+ ├── public/ ← style.css (Tailwind + transition classes)
707
624
  └── tsconfig.json
708
625
 
709
- docker-compose.yml ← postgres (pgvector) + redis
626
+ docker-compose.yml ← postgres (pgvector) + redis
710
627
  ```
711
628
 
712
629
  ---
@@ -717,5 +634,18 @@ docker-compose.yml ← postgres (pgvector) + redis
717
634
  docker compose up -d # Start postgres + redis
718
635
  npm run build # esbuild → dist/
719
636
  npm run typecheck # tsc --noEmit
720
- npm test # Run tests
637
+ npm test # Run all tests
721
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 |