weifuwu 0.33.9 → 0.33.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +196 -530
  2. package/dist/client/app.d.ts +1 -3
  3. package/dist/client/index.d.ts +1 -171
  4. package/dist/client/index.js +76 -764
  5. package/dist/client/jsx-runtime.d.ts +11 -1
  6. package/dist/client/jsx-runtime.js +76 -764
  7. package/dist/client/middleware/ws.d.ts +2 -2
  8. package/dist/client/router.d.ts +1 -0
  9. package/dist/client/signal.d.ts +0 -36
  10. package/dist/client/types.d.ts +29 -30
  11. package/dist/core/router.d.ts +21 -11
  12. package/dist/core/ws.d.ts +9 -12
  13. package/dist/graphql.d.ts +2 -1
  14. package/dist/index.d.ts +2 -35
  15. package/dist/index.js +367 -3586
  16. package/dist/types.d.ts +0 -12
  17. package/package.json +2 -1
  18. package/dist/ai/agent.d.ts +0 -127
  19. package/dist/ai/index.d.ts +0 -26
  20. package/dist/ai/types.d.ts +0 -59
  21. package/dist/base/index.d.ts +0 -59
  22. package/dist/base/module.d.ts +0 -42
  23. package/dist/base/types.d.ts +0 -116
  24. package/dist/client/components/Chat.d.ts +0 -19
  25. package/dist/client/components/Link.d.ts +0 -27
  26. package/dist/client/components/LoginForm.d.ts +0 -16
  27. package/dist/client/components/Transition.d.ts +0 -42
  28. package/dist/client/lib/css.d.ts +0 -33
  29. package/dist/client/lib/dev.d.ts +0 -28
  30. package/dist/client/lib/form.d.ts +0 -56
  31. package/dist/client/lib/model.d.ts +0 -36
  32. package/dist/client/lib/resource.d.ts +0 -56
  33. package/dist/client/middleware/api.d.ts +0 -32
  34. package/dist/client/middleware/auth.d.ts +0 -32
  35. package/dist/cms/index.d.ts +0 -67
  36. package/dist/cms/module.d.ts +0 -38
  37. package/dist/cms/types.d.ts +0 -96
  38. package/dist/core/logger.d.ts +0 -16
  39. package/dist/core/trace.d.ts +0 -95
  40. package/dist/hub.d.ts +0 -36
  41. package/dist/kb/index.d.ts +0 -57
  42. package/dist/kb/module.d.ts +0 -37
  43. package/dist/kb/types.d.ts +0 -83
  44. package/dist/messager/index.d.ts +0 -72
  45. package/dist/messager/module.d.ts +0 -46
  46. package/dist/messager/types.d.ts +0 -99
  47. package/dist/middleware/compress.d.ts +0 -20
  48. package/dist/middleware/helmet.d.ts +0 -33
  49. package/dist/middleware/rate-limit.d.ts +0 -44
  50. package/dist/middleware/sandbox.d.ts +0 -52
  51. package/dist/middleware/upload.d.ts +0 -55
  52. package/dist/queue/cron.d.ts +0 -9
  53. package/dist/queue/index.d.ts +0 -12
  54. package/dist/queue/types.d.ts +0 -57
  55. package/dist/user/index.d.ts +0 -61
  56. package/dist/user/module.d.ts +0 -64
  57. package/dist/user/types.d.ts +0 -98
package/README.md CHANGED
@@ -1,575 +1,288 @@
1
1
  # weifuwu
2
2
 
3
- **AI SaaS full-stack framework** — `(req, ctx) => Response` + `(props, ctx) => JSX`
3
+ **Web framework + reactive frontend** — `(req, ctx) => Response` + `(props, ctx) => JSX`
4
4
 
5
5
  ```bash
6
6
  npm install weifuwu
7
7
  ```
8
8
 
9
- One package. Backend + frontend. User system, messaging, RAG knowledge base, AI Agent, CMS, dynamic data storage, reactive frontend. Set env vars and go.
9
+ One package. Backend (`weifuwu`) + frontend (`weifuwu/client`). Minimal, composable, no magic.
10
10
 
11
11
  ---
12
12
 
13
13
  ## Core Concept: `ctx`
14
14
 
15
- **`ctx` 是整个框架的核心模式。** 后端和前端共享同一个理念:通过中间件向 `ctx` 注入能力,组件/handler 直接从 `ctx` 读取。
15
+ Backend and frontend share the same pattern: middleware injects fields into `ctx`, handlers/components read from `ctx`.
16
16
 
17
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
18
+ Backend: Frontend:
19
+ Request → Middleware → Handler createApp() → MiddlewareComponent
20
+
21
+
22
+ ctx.sql ctx.ws
23
+ ctx.redis ctx.route
24
+ ctx.ui ctx.app.navigate
32
25
  ```
33
26
 
34
- **后端每个请求创建一个 ctx:**
27
+ **Backend:**
35
28
  ```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
- })
29
+ app.use(postgres()) // → ctx.sql
30
+ app.use(redis()) // ctx.redis
31
+ app.use(ui()) // ctx.ui.html / ctx.ui.js / ctx.ui.css
42
32
  ```
43
33
 
44
- **前端每个组件通过第二个参数接收 ctx:**
34
+ **Frontend:**
45
35
  ```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
- }
36
+ app.use(ws()) // ctx.ws.send / onMessage / isConnected
37
+ app.use(router({ routes })) // → ctx.route.path / params / query
52
38
  ```
53
39
 
54
- 这种模式让开发者**无需 import 任何工具函数**,所有能力在 `ctx` 中一站获取。
55
-
56
40
  ---
57
41
 
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 |
42
+ ## Backend
121
43
 
122
- ---
44
+ ### Exports
45
+
46
+ | Export | Type | Purpose |
47
+ |--------|------|---------|
48
+ | `Router` | class | HTTP router + middleware chain + `.ws()` + `.graphql()` |
49
+ | `serve` | function | HTTP server |
50
+ | `cors` | middleware | CORS headers |
51
+ | `postgres` | middleware | PostgreSQL client → `ctx.sql` |
52
+ | `redis` | middleware | Redis client → `ctx.redis` |
53
+ | `serveStatic` | middleware | Static file serving |
54
+ | `ui` | middleware | SSR/SPA rendering → `ctx.ui.html/css/js` |
55
+ | `HttpError` | class | HTTP error with status code |
56
+ | `DEFAULT_MAX_BODY` | constant | Default 10MB body limit |
57
+ | `MIGRATIONS_TABLE` | constant | Postgres migrations table name |
123
58
 
124
- ## Quick Start
59
+ ### Types
125
60
 
126
- ### 完整全栈示例 ctx 贯穿前后端
61
+ `Context`, `Handler`, `Middleware`, `ErrorHandler`, `WebSocket`, `WebSocketHandler`, `ServeOptions`, `Server`, `CORSOptions`, `ServeStaticOptions`, `PostgresOptions`, `PostgresClient`, `PostgresInjected`, `RedisOptions`, `RedisClient`, `RedisInjected`, `GraphQLOptions`, `GraphQLHandler`
62
+
63
+ ### Quick Start
127
64
 
128
- **后端 `server.ts`:**
129
65
  ```ts
130
- import { serve, Router, postgres, user, agent, kb, messager, ui, cors, logger } from 'weifuwu'
131
- import { openai } from '@ai-sdk/openai'
66
+ import { serve, Router, cors, serveStatic, ui } from 'weifuwu'
132
67
 
133
68
  const app = new Router()
134
69
  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
139
- app.use(agent({
140
- model: openai('deepseek-v4-flash', { baseURL: 'https://api.deepseek.com/v1' }),
141
- knowledge: { search: async (q, ctx) => ctx.kb.search(q) },
142
- })) // → ctx.agent
143
- app.use(messager()) // → ctx.messager
144
- app.use(ui()) // → ctx.ui
145
-
146
- // 业务 API — ctx 中所有能力可直接使用
147
- app.post('/api/chat', async (req, ctx) => {
148
- const { messages } = await req.json()
149
- return ctx.agent.chatStreamResponse({ messages })
150
- })
70
+ app.use(serveStatic('./public'))
71
+ app.use(ui())
151
72
 
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 })
73
+ // API routes
74
+ app.get('/api/hello', async (req, ctx) => {
75
+ return Response.json({ message: 'hello' })
155
76
  })
156
77
 
157
- // 客户端编译(无需本地构建步骤)
158
- app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
159
-
160
- serve(app, { port: 3000 })
161
- ```
78
+ // WebSocket
79
+ app.ws('/ws', {
80
+ open(ws) { ws.send('connected') },
81
+ message(ws, ctx, data) { ws.send(data.toString()) },
82
+ })
162
83
 
163
- **前端 `src/main.tsx` — ctx 驱动组件:**
164
- ```tsx
165
- import { signal, createApp, api, auth, socket, router, RouteView, LoginForm, Link } from 'weifuwu/client'
166
- import type { WfuiContext } from 'weifuwu/client'
84
+ // GraphQL
85
+ app.graphql(async (req, ctx) => ({
86
+ schema: `type Query { hello: String }`,
87
+ resolvers: { Query: { hello: () => 'world' } },
88
+ graphiql: true,
89
+ }))
167
90
 
168
- function AppShell(_props: {}, ctx: WfuiContext) {
169
- if (!ctx.isAuthenticated) return <LoginForm />
170
- return (
171
- <div>
172
- <nav><Link to="/chat">Chat</Link></nav>
173
- <main><RouteView /></main>
174
- </div>
175
- )
176
- }
91
+ // SSR page
92
+ app.get('/blog/:slug', async (req, ctx) => ctx.ui.html`
93
+ <!DOCTYPE html>
94
+ <html><body><h1>${post.title}</h1></body></html>
95
+ `)
177
96
 
178
- const app = createApp()
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
183
- app.mount('#root', AppShell)
184
- ```
97
+ // Dynamic JS compilation (no build step)
98
+ app.get('/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
185
99
 
186
- **`tsconfig.json`:**
187
- ```json
188
- { "jsx": "react-jsx", "jsxImportSource": "weifuwu/client" }
100
+ serve(app, { port: 3000 })
189
101
  ```
190
102
 
191
- ---
192
-
193
- ## Environment Variables
194
-
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()` |
203
-
204
- ---
205
-
206
- ## Backend Modules
103
+ ### Router
207
104
 
208
- ### postgres
209
105
  ```ts
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 */ })
106
+ const app = new Router()
107
+ app.get(path, ...handlers)
108
+ app.post / put / delete / patch / head / options(path, ...handlers)
109
+ app.all(path, ...handlers)
110
+ app.ws(path, handler) // WebSocket
111
+ app.graphql(handler) // GraphQL at /
112
+ app.graphql('/graphql', handler) // GraphQL at /graphql
113
+ app.use(middleware) // Global middleware
114
+ app.mount(prefix, subRouter) // Sub-router
115
+ app.onError(handler) // Error handler
116
+ app.routes() // Debug: list all routes
215
117
  ```
216
- | Method | Description |
217
- |--------|-------------|
218
- | `ctx.sql\`...\`` | Tagged template SQL |
219
- | `ctx.sql.begin(fn)` | Transaction |
220
- | `sql.close()` | Close pool |
221
118
 
222
- ### redis
223
- ```ts
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()
230
- ```
231
- Reads `REDIS_URL` env (default: `redis://localhost:6379`).
119
+ ### WebSocket
232
120
 
233
- ### user
234
121
  ```ts
235
- import { user, requireRole } from 'weifuwu'
236
- app.use(postgres())
237
- app.use(user({ secret: process.env.JWT_SECRET }))
238
- // ctx.user (UserRecord | undefined), ctx.userModule
239
-
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
122
+ app.ws('/ws', {
123
+ open(ws, ctx) { ws.send('connected') },
124
+ message(ws, ctx, data) { /* data: string | Buffer */ },
125
+ close(ws, ctx) { /* cleanup */ },
126
+ error(ws, ctx, err) { /* log */ },
244
127
  })
245
128
  ```
246
- | `ctx.userModule.*` | Returns | Description |
247
- |--------------------|---------|-------------|
248
- | `register(input)` | `{ user, token }` | Register |
249
- | `login(email, pw)` | `{ user, token } \| null` | Login |
250
- | `getUserById(id)` | `UserRecord \| null` | Get by ID |
251
- | `updateUser(id, input)` | `UserRecord \| null` | Update |
252
- | `changePassword(id, oldPw, newPw)` | `boolean` | Change password |
253
- | `deleteUser(id)` | `boolean` | Soft delete |
254
- | `listUsers(inactive?)` | `UserRecord[]` | List users |
255
- | `verifyToken(token)` | `TokenPayload \| null` | Verify JWT |
256
-
257
- `requireRole('admin')` — guard middleware. No auth → 401, wrong role → 403.
258
-
259
- ### messager
260
- ```ts
261
- import { messager } from 'weifuwu'
262
- app.use(postgres())
263
- app.use(user())
264
- app.use(messager()) // → ctx.messager
265
- ```
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 |
271
- | `createDirectConversation(userId)` | `Conversation` | Create/reuse DM |
272
- | `createGroupConversation(title, userIds)` | `Conversation` | Create group |
273
- | `editMessage(msgId, body)` | `Message \| null` | Edit (24h) |
274
- | `deleteMessage(msgId)` | `boolean` | Soft delete |
275
- | `markRead(convId)` | `void` | Mark as read |
276
-
277
- ### kb — Knowledge Base
278
- ```ts
279
- import { kb } from 'weifuwu'
280
- app.use(postgres())
281
- app.use(kb()) // → ctx.kb
282
- ```
283
- | `ctx.kb.*` | Returns | Description |
284
- |------------|---------|-------------|
285
- | `importText(title, text)` | `{ document, chunks }` | Import → chunk → embed |
286
- | `search(query, opts?)` | `SearchResult[]` | Semantic search |
287
- | `list()` | `Document[]` | List documents |
288
- | `get(id)` | `Document \| null` | Get document |
289
- | `delete(id)` | `boolean` | Delete + cascade chunks |
290
-
291
- ### agent
292
- ```ts
293
- import { agent } from 'weifuwu'
294
- import { openai } from '@ai-sdk/openai'
295
- import { tool } from 'ai'
296
- import { z } from 'zod'
297
-
298
- app.use(agent({
299
- model: openai('deepseek-v4-flash', { baseURL: 'https://api.deepseek.com/v1' }),
300
- system: 'You are a helpful assistant.',
301
- knowledge: { search: async (q, ctx) => ctx.kb.search(q) },
302
- tools: {
303
- getWeather: tool({
304
- description: 'Get weather for a city',
305
- parameters: z.object({ city: z.string() }),
306
- execute: async ({ city }) => ({ temp: 22, unit: 'C' }),
307
- }),
308
- },
309
- maxSteps: 5,
310
- })) // → ctx.agent
311
- ```
312
- | `ctx.agent.*` | Description |
313
- |--------------|-------------|
314
- | `chat(prompt, opts?)` | Non-streaming, returns text |
315
- | `chatStreamResponse({ messages })` | SSE stream (useChat compatible) |
316
129
 
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 |
330
-
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
- ```
130
+ ### GraphQL
338
131
 
339
- ### queue
340
132
  ```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()
133
+ // At root
134
+ app.graphql(async (req, ctx) => ({
135
+ schema: `type Query { hello: String }`,
136
+ resolvers: { Query: { hello: () => 'world' } },
137
+ graphiql: true,
138
+ }))
139
+
140
+ // Or at a custom path
141
+ app.graphql('/graphql', handler)
347
142
  ```
348
143
 
349
- ### ui — SSR & SPA
144
+ ### Graceful Shutdown
145
+
350
146
  ```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'))
147
+ const srv = serve(app)
148
+ // Ctrl+C / SIGTERM immediately closes all connections and exits
149
+ await srv.stop() // Programmatic stop
358
150
  ```
359
151
 
360
152
  ---
361
153
 
362
- ## Frontend weifuwu/client
154
+ ## Frontend (`weifuwu/client`)
155
+
156
+ 16 runtime exports, zero dependencies, zero virtual DOM.
157
+
158
+ | Export | Type | Purpose |
159
+ |--------|------|---------|
160
+ | `signal`, `computed`, `effect`, `batch` | function | Reactive state system |
161
+ | `jsx`/`jsxs`/`jsxDEV` | function | JSX compilation target |
162
+ | `Fragment` | component | `<></>` |
163
+ | `Show` | component | Conditional rendering |
164
+ | `For` | component | Keyed list rendering |
165
+ | `onMount`, `onCleanup` | function | Lifecycle hooks |
166
+ | `createApp` | function | App instance with middleware chain |
167
+ | `router` | middleware | Hash/history router → `ctx.route` |
168
+ | `RouteView` | component | Route outlet |
169
+ | `ws` | middleware | WebSocket client → `ctx.ws` |
363
170
 
364
- Reactive frontend framework. Zero virtual DOM, zero external dependencies.
171
+ Types: `Signal`, `Component`, `WfuiContext`, `AppMiddleware`, `RouteDef`
365
172
 
366
- ### Core APIs
173
+ ### Quick Start
367
174
 
368
175
  ```tsx
369
- // Signal reactive state
370
- const count = signal(0)
371
- const doubled = computed(() => count.value * 2)
372
- effect(() => console.log('count:', count.value))
176
+ import { signal, Show, For, createApp, ws, router, RouteView } from 'weifuwu/client'
177
+ import type { WfuiContext, RouteDef } from 'weifuwu/client'
373
178
 
374
- // Batch merge multiple writes
375
- batch(() => { a.value = 1; b.value = 2 })
179
+ const routes: RouteDef[] = [
180
+ { path: '/', component: HomePage },
181
+ { path: '/hello/:name', component: HelloPage },
182
+ ]
376
183
 
377
- // Untrack read without subscribing
378
- effect(() => { console.log(untrack(() => theme.value)) })
184
+ const app = createApp()
185
+ app.use(ws())
186
+ app.use(router({ routes, mode: 'hash' }))
187
+ app.mount('#root', AppShell)
379
188
 
380
- // Lifecycle
381
- onMount(() => { init(); return () => cleanup() })
382
- onCleanup(() => clearInterval(id))
189
+ function AppShell(_props: {}, ctx: WfuiContext) {
190
+ return (
191
+ <div>
192
+ <nav>...</nav>
193
+ <main><RouteView /></main>
194
+ </div>
195
+ )
196
+ }
383
197
  ```
384
198
 
385
- ### Reactive Array
199
+ ### Signal
386
200
 
387
201
  ```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()
202
+ const count = signal(0)
203
+ const doubled = computed(() => count.value * 2)
204
+ effect(() => console.log('count:', count.value))
205
+ batch(() => { a.value = 1; b.value = 2 })
395
206
  ```
396
207
 
397
208
  ### Control Flow
398
209
 
399
210
  ```tsx
400
- // Show conditional rendering
401
- <Show when={isLoggedIn} fallback={<LoginPage />}>
211
+ <Show when={isLoggedIn} fallback={<Login />}>
402
212
  <Dashboard />
403
213
  </Show>
404
214
 
405
- // For keyed list rendering
406
- <For each={todos} keyBy="id">
407
- {(todo) => <TodoItem todo={todo} />}
215
+ <For each={items} keyBy="id">
216
+ {(item) => <div>{item.name}</div>}
408
217
  </For>
409
-
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>
419
218
  ```
420
219
 
421
- ### Form Handling
220
+ ### WebSocket (`ctx.ws`)
422
221
 
423
222
  ```tsx
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
- ```
439
-
440
- ### Async Data
223
+ app.use(ws({ url: '/ws' }))
441
224
 
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>
451
- ```
452
-
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
- ```
462
-
463
- ### Type-Safe Context
464
-
465
- ```tsx
466
- const ThemeCtx = createContext<string>('theme')
467
- ThemeCtx.provide(ctx, 'dark') // 提供
468
- const theme = ThemeCtx.inject(ctx) // string | null
469
- ```
470
-
471
- ### Third-Party Integration
472
-
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()
225
+ // In component:
226
+ onMount(() => {
227
+ const unsub = ctx.ws.onMessage((data) => { ... })
228
+ onCleanup(() => unsub())
479
229
  })
480
- <Dashboard><PieChart data={salesData} /></Dashboard>
230
+ ctx.ws.send({ type: 'chat', body: 'hello' })
231
+ <Show when={ctx.ws.isConnected}>🟢 已连接</Show>
481
232
  ```
482
233
 
483
- ### Pre-built Components
234
+ ### Router (`ctx.route`)
484
235
 
485
236
  ```tsx
486
- import { LoginForm, Chat, Link } from 'weifuwu/client'
237
+ app.use(router({ routes, notFound: NotFound, mode: 'hash' }))
487
238
 
488
- <LoginForm /> // 登录/注册(自动切换模式)
489
- <Chat conversationId="123" /> // 实时消息聊天
490
- <Link to="/about">关于</Link> // SPA 导航(支持右键新标签页)
239
+ ctx.route.path // '/hello/world'
240
+ ctx.route.params // { name: 'world' }
241
+ ctx.route.query // { tab: 'intro' }
242
+ ctx.app.navigate('/hello/world')
491
243
  ```
492
244
 
493
- ### Router
245
+ ### Lifecycle
494
246
 
495
247
  ```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
- ]
502
-
503
- app.use(router({ routes, notFound: NotFound, mode: 'hash', transition: 'page' }))
504
- // ctx.route.path / .params / .query / .data / .loading
505
-
506
- // 路由出口
507
- function AppShell() { return <main><RouteView /></main> }
508
- ```
509
-
510
- ### DevTools
511
-
512
- ```ts
513
- import { enableDevtools } from 'weifuwu/client'
514
- if (import.meta.env.DEV) enableDevtools()
515
-
516
- // 浏览器控制台:
517
- // __wefu__.inspect() → 查看所有 signal
518
- // __wefu__.warnings() → 切换开发警告
248
+ onMount(() => {
249
+ init()
250
+ return () => cleanup() // Auto-cleanup on unmount
251
+ })
252
+ onCleanup(() => clearInterval(id))
519
253
  ```
520
254
 
521
- ---
522
-
523
- ## From React to weifuwu/client
255
+ ### From React
524
256
 
525
257
  | React | weifuwu/client |
526
258
  |-------|----------------|
527
259
  | `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>` |
260
+ | `useMemo(() => a*2, [a])` | `computed(() => a.value * 2)` |
261
+ | `useEffect(() => f, [])` | `onMount(f)` |
262
+ | `useEffect(() => f, [dep])` | `effect(f)` |
263
+ | `{cond && <X/>}` | `<Show when={cond}><X/></Show>` |
531
264
  | `{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)` |
265
+ | `useNavigate()` | `ctx.app.navigate()` |
535
266
  | `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`
545
267
 
546
268
  ---
547
269
 
548
- ## Utils
270
+ ## Demo
549
271
 
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 |
272
+ ```bash
273
+ cd apps/demo
274
+ node server.ts
275
+ # http://localhost:3000
276
+ ```
556
277
 
557
278
  ---
558
279
 
559
- ## Router (Backend)
280
+ ## Environment Variables
560
281
 
561
- ```ts
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
572
- ```
282
+ | Variable | Default | Used by |
283
+ |----------|---------|---------|
284
+ | `DATABASE_URL` | `postgres://root:123456@localhost:5432/demo` | `postgres()` |
285
+ | `REDIS_URL` | `redis://localhost:6379` | `redis()` |
573
286
 
574
287
  ---
575
288
 
@@ -577,75 +290,28 @@ app.routes() // Debug: list all routes
577
290
 
578
291
  ```
579
292
  src/
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
603
- │ ├── lib/
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)
609
- │ ├── middleware/
610
- │ │ ├── api.ts ← HTTP client → ctx.api
611
- │ │ ├── auth.ts ← Auth → ctx.user/login/logout
612
- │ │ └── ws.ts ← WebSocket → ctx.socket
613
- │ └── components/
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)
624
- └── tsconfig.json
625
-
626
- docker-compose.yml ← postgres (pgvector) + redis
293
+ ├── index.ts Entry, exports
294
+ ├── types.ts Context, Handler, Middleware
295
+ ├── core/ Router, serve, WebSocket upgrade
296
+ ├── middleware/ cors, serveStatic
297
+ ├── postgres/ PostgreSQL client
298
+ ├── redis/ Redis client
299
+ ├── ui/ ctx.ui.html/js/css
300
+ ├── graphql.ts GraphQL + router.graphql()
301
+ ├── client/ Frontend framework
302
+ ├── index.ts 16 runtime exports
303
+ ├── signal.ts signal/computed/effect/batch
304
+ ├── jsx-runtime.ts JSX DOM + Show/For/Fragment/Portal
305
+ ├── app.ts createApp
306
+ ├── router.ts Router + RouteView
307
+ ├── types.ts WfuiContext + types
308
+ │ └── middleware/ws.ts WebSocket client
309
+ ├── test/ 47 backend + 77 frontend tests
310
+ apps/demo/ Full-stack demo
627
311
  ```
628
312
 
629
- ---
630
-
631
- ## Development
632
-
633
313
  ```bash
634
- docker compose up -d # Start postgres + redis
635
- npm run build # esbuild → dist/
636
- npm run typecheck # tsc --noEmit
637
- npm test # Run all tests
314
+ npm run build # esbuild dist/
315
+ npm run typecheck # tsc --noEmit
316
+ npm test # Run all tests
638
317
  ```
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 |