weifuwu 0.33.8 → 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 (51) hide show
  1. package/README.md +200 -604
  2. package/dist/client/app.d.ts +1 -3
  3. package/dist/client/index.d.ts +121 -11
  4. package/dist/client/index.js +326 -488
  5. package/dist/client/jsx-runtime.d.ts +64 -4
  6. package/dist/client/jsx-runtime.js +326 -488
  7. package/dist/client/middleware/ws.d.ts +2 -2
  8. package/dist/client/router.d.ts +11 -0
  9. package/dist/client/signal.d.ts +46 -0
  10. package/dist/client/types.d.ts +56 -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 +4 -2
  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 -17
  25. package/dist/client/components/LoginForm.d.ts +0 -14
  26. package/dist/client/lib/form.d.ts +0 -54
  27. package/dist/client/middleware/api.d.ts +0 -32
  28. package/dist/client/middleware/auth.d.ts +0 -32
  29. package/dist/cms/index.d.ts +0 -67
  30. package/dist/cms/module.d.ts +0 -38
  31. package/dist/cms/types.d.ts +0 -96
  32. package/dist/core/logger.d.ts +0 -16
  33. package/dist/core/trace.d.ts +0 -95
  34. package/dist/hub.d.ts +0 -36
  35. package/dist/kb/index.d.ts +0 -57
  36. package/dist/kb/module.d.ts +0 -37
  37. package/dist/kb/types.d.ts +0 -83
  38. package/dist/messager/index.d.ts +0 -72
  39. package/dist/messager/module.d.ts +0 -46
  40. package/dist/messager/types.d.ts +0 -99
  41. package/dist/middleware/compress.d.ts +0 -20
  42. package/dist/middleware/helmet.d.ts +0 -33
  43. package/dist/middleware/rate-limit.d.ts +0 -44
  44. package/dist/middleware/sandbox.d.ts +0 -52
  45. package/dist/middleware/upload.d.ts +0 -55
  46. package/dist/queue/cron.d.ts +0 -9
  47. package/dist/queue/index.d.ts +0 -12
  48. package/dist/queue/types.d.ts +0 -57
  49. package/dist/user/index.d.ts +0 -61
  50. package/dist/user/module.d.ts +0 -64
  51. package/dist/user/types.d.ts +0 -98
package/README.md CHANGED
@@ -1,662 +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
- ## Module Overview
14
-
15
- ### Backend
16
-
17
- | Module | Import | Depends on | Purpose |
18
- |--------|--------|-----------|---------|
19
- | `postgres()` | `weifuwu` | `DATABASE_URL` | PostgreSQL client (`ctx.sql`) |
20
- | `redis()` | `weifuwu` | `REDIS_URL` | Redis client (`ctx.redis`) |
21
- | `user()` | `weifuwu` | `postgres()`, `JWT_SECRET` | Auth, JWT, roles (`ctx.user`) |
22
- | `messager()` | `weifuwu` | `postgres()`, `user()` | IM + AI conversation layer |
23
- | `kb()` | `weifuwu` | `postgres()`, `DASHSCOPE_API_KEY` | RAG knowledge base |
24
- | `agent()` | `weifuwu` | — | LLM chat, tools, streaming |
25
- | `cms()` | `weifuwu` | `postgres()`, `user()` | Blog, docs, changelog |
26
- | `base()` | `weifuwu` | `postgres()`, `user()` | Dynamic data engine |
27
- | `queue()` | `weifuwu` | `REDIS_URL` | Job queue + cron |
28
- | `ui()` | `weifuwu` | — | SSR/SPA rendering (`ctx.ui.html`, `ctx.ui.js`, `ctx.ui.css`) |
29
-
30
- ### Middleware
31
-
32
- | Import | Purpose |
33
- |--------|---------|
34
- | `cors()` | CORS headers |
35
- | `helmet()` | Security headers |
36
- | `compress()` | gzip / brotli / deflate |
37
- | `rateLimit()` | Sliding-window rate limiter |
38
- | `logger()` | Request logging |
39
- | `upload()` | Multipart file upload |
40
- | `serveStatic()` | Static files |
41
- | `sandbox()` | Filesystem isolation |
42
-
43
- ### Frontend (`weifuwu/client`)
44
-
45
- | Import | Type | Purpose |
46
- |--------|------|---------|
47
- | `signal()` | function | Reactive state |
48
- | `computed()` | function | Derived signals |
49
- | `effect()` | function | Auto-tracked side effects |
50
- | `<Show>` | component | Conditional rendering |
51
- | `<For>` | component | List rendering |
52
- | `<ErrorBoundary>` | component | Catch render errors |
53
- | `<RouteView>` | component | Route outlet |
54
- | `createApp()` | function | App instance with middleware chain |
55
- | `mount()` | method | Mount SPA |
56
- | `hydrate()` | method | SSR hydration |
57
- | `router()` | middleware | Hash/history router |
58
- | `api()` | middleware | HTTP client (`ctx.api`) |
59
- | `auth()` | middleware | Auth state (`ctx.user/login/logout`) |
60
- | `ws()` | middleware | WebSocket (`ctx.ws`) |
61
- | `wrap()` | function | Third-party library integration |
62
- | `useForm()` | function | Form state management |
63
- | `createPortal()` | function | Render outside parent DOM |
64
- | `LoginForm` | component | Login/register form |
65
- | `Chat` | component | Real-time messaging |
66
-
67
- ### Utils
68
-
69
- | Import | Purpose |
70
- |--------|---------|
71
- | `requireRole('admin')` | Middleware factory: check `ctx.user.role` |
72
- | `createHub()` | WebSocket pub/sub |
73
- | `HttpError` | `throw new HttpError(msg, 404)` |
74
- | `trace()` | Request tracing |
75
-
76
- ---
77
-
78
- ## Environment Variables
79
-
80
- | Variable | Default | Used by |
81
- |----------|---------|---------|
82
- | `DATABASE_URL` | `postgres://root:123456@localhost:5432/demo` | `postgres()` |
83
- | `REDIS_URL` | `redis://localhost:6379` | `redis()`, `queue()` |
84
- | `JWT_SECRET` | — | `user()` |
85
- | `DASHSCOPE_API_KEY` | — | `kb()` (embedding) |
86
- | `DEEPSEEK_API_KEY` / `OPENAI_API_KEY` | — | `agent()` (LLM) |
87
- | `DEEPSEEK_MODEL` | `deepseek-v4-flash` | `agent()` |
88
-
89
- ---
90
-
91
- ## Quick Start
13
+ ## Core Concept: `ctx`
92
14
 
93
- ### Backend
15
+ Backend and frontend share the same pattern: middleware injects fields into `ctx`, handlers/components read from `ctx`.
94
16
 
95
- ```ts
96
- import { serve, Router, postgres, user, kb, agent, messager } from 'weifuwu'
97
- import { openai } from '@ai-sdk/openai'
98
-
99
- const app = new Router()
100
- app.use(postgres())
101
- app.use(user())
102
- app.use(kb())
103
- app.use(messager())
104
- app.use(agent({
105
- model: openai('deepseek-v4-flash', { baseURL: 'https://api.deepseek.com/v1' }),
106
- knowledge: { search: async (q, ctx) => ctx.kb.search(q) },
107
- }))
108
-
109
- app.post('/api/chat', async (req, ctx) => {
110
- const { messages } = await req.json()
111
- return ctx.agent.chatStreamResponse({ messages })
112
- })
113
-
114
- serve(app, { port: 3000 })
115
17
  ```
116
-
117
- ### Frontend
118
-
119
- ```tsx
120
- import { signal, createApp, api, auth, ws, router, RouteView, LoginForm } from 'weifuwu/client'
121
- import type { WfuiContext } from 'weifuwu/client'
122
-
123
- function AppShell(_props: {}, ctx: WfuiContext) {
124
- if (!ctx.isAuthenticated) return <LoginForm />
125
- return (
126
- <div>
127
- <nav><a onClick={() => ctx.app.navigate('/chat')}>Chat</a></nav>
128
- <main><RouteView /></main>
129
- </div>
130
- )
131
- }
132
-
133
- const app = createApp()
134
- app.use(api())
135
- app.use(auth())
136
- app.use(ws())
137
- app.use(router({ routes }))
138
- app.mount('#root', AppShell)
18
+ Backend: Frontend:
19
+ Request → Middleware → Handler createApp() → Middleware → Component
20
+ │ │
21
+ ▼ ▼
22
+ ctx.sql ctx.ws
23
+ ctx.redis ctx.route
24
+ ctx.ui ctx.app.navigate
139
25
  ```
140
26
 
141
- ```json
142
- // tsconfig.json
143
- { "jsx": "react-jsx", "jsxImportSource": "weifuwu/client" }
144
- ```
145
-
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
- ```
157
-
158
- Or skip the build step entirely with server-side compilation:
159
-
27
+ **Backend:**
160
28
  ```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'))
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
163
32
  ```
164
33
 
165
- ---
166
-
167
- ## Backend Modules
168
-
169
- Each module follows: import → usage → API table.
170
-
171
- ### postgres
172
-
173
- ```ts
174
- import { postgres } from 'weifuwu'
175
-
176
- const sql = postgres()
177
- app.use(sql) // → ctx.sql
178
-
179
- await ctx.sql`SELECT * FROM users WHERE id = ${id}`
180
- await ctx.sql.begin(async (sql) => { /* transaction */ })
34
+ **Frontend:**
35
+ ```tsx
36
+ app.use(ws()) // ctx.ws.send / onMessage / isConnected
37
+ app.use(router({ routes })) // → ctx.route.path / params / query
181
38
  ```
182
39
 
183
- | Method | Description |
184
- |--------|-------------|
185
- | `ctx.sql\`...\`` | Tagged template SQL queries |
186
- | `ctx.sql.begin(fn)` | Transaction |
187
- | `sql.close()` | Close pool |
188
-
189
- Reads `DATABASE_URL` env. Supports migrations via `postgres({ migrate: { directory: './migrations' } })`.
190
-
191
- ### redis
40
+ ---
192
41
 
193
- ```ts
194
- import { redis } from 'weifuwu'
42
+ ## Backend
195
43
 
196
- const r = redis()
197
- app.use(r) // → ctx.redis
44
+ ### Exports
198
45
 
199
- await ctx.redis.set('key', 'value')
200
- await ctx.redis.get('key')
201
- ```
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 |
202
58
 
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 |
59
+ ### Types
209
60
 
210
- Reads `REDIS_URL` env (default: `redis://localhost:6379`).
61
+ `Context`, `Handler`, `Middleware`, `ErrorHandler`, `WebSocket`, `WebSocketHandler`, `ServeOptions`, `Server`, `CORSOptions`, `ServeStaticOptions`, `PostgresOptions`, `PostgresClient`, `PostgresInjected`, `RedisOptions`, `RedisClient`, `RedisInjected`, `GraphQLOptions`, `GraphQLHandler`
211
62
 
212
- ### user
63
+ ### Quick Start
213
64
 
214
65
  ```ts
215
- import { user, requireRole } from 'weifuwu'
216
-
217
- app.use(postgres())
218
- app.use(user({ secret: process.env.JWT_SECRET }))
66
+ import { serve, Router, cors, serveStatic, ui } from 'weifuwu'
219
67
 
220
- app.post('/api/register', async (req, ctx) =>
221
- Response.json(await ctx.userModule.register(await req.json())))
68
+ const app = new Router()
69
+ app.use(cors())
70
+ app.use(serveStatic('./public'))
71
+ app.use(ui())
222
72
 
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 })
73
+ // API routes
74
+ app.get('/api/hello', async (req, ctx) => {
75
+ return Response.json({ message: 'hello' })
227
76
  })
228
77
 
229
- app.get('/api/me', async (req, ctx) =>
230
- ctx.user ? Response.json(ctx.user) : new Response('Unauthorized', { status: 401 }))
231
- ```
232
-
233
- | Method | Returns | Description |
234
- |--------|---------|-------------|
235
- | `register(input)` | `{ user, token }` | Register |
236
- | `login(email, pw)` | `{ user, token } \| null` | Login |
237
- | `getUserById(id)` | `UserRecord \| null` | Get by ID |
238
- | `getUserByEmail(email)` | `UserRecord \| null` | Get by email |
239
- | `updateUser(id, input)` | `UserRecord \| null` | Update |
240
- | `changePassword(id, oldPw, newPw)` | `boolean` | Change password |
241
- | `deleteUser(id)` | `boolean` | Soft delete |
242
- | `listUsers(inactive?)` | `UserRecord[]` | List users |
243
- | `generateToken(user)` | `string` | Issue JWT |
244
- | `verifyToken(token)` | `TokenPayload \| null` | Verify JWT |
245
-
246
- `ctx.user` — auto-resolved from `Authorization: Bearer` or `token` cookie. Fields: `id, name, email, role, [key: string]`.
247
-
248
- `requireRole('admin')` — guard middleware. No auth → 401, wrong role → 403.
249
-
250
- Password: scrypt + 32-byte random salt. Token: HMAC SHA-256, 7 day expiry.
251
-
252
- ### messager
253
-
254
- ```ts
255
- import { messager } from 'weifuwu'
256
-
257
- app.use(postgres())
258
- app.use(user())
259
- app.use(messager())
260
-
78
+ // WebSocket
261
79
  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 })
80
+ open(ws) { ws.send('connected') },
81
+ message(ws, ctx, data) { ws.send(data.toString()) },
272
82
  })
273
83
 
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
- })
281
- ```
282
-
283
- | Method | Returns | Description |
284
- |--------|---------|-------------|
285
- | `createDirectConversation(userId)` | `Conversation` | Create/reuse DM |
286
- | `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) |
290
- | `deleteMessage(msgId)` | `boolean` | Soft delete |
291
- | `getConversations()` | `Conversation[]` | List with unread + last message |
292
- | `getConversation(id)` | `Conversation \| null` | Get detail |
293
- | `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
-
299
- ### kb — Knowledge Base
84
+ // GraphQL
85
+ app.graphql(async (req, ctx) => ({
86
+ schema: `type Query { hello: String }`,
87
+ resolvers: { Query: { hello: () => 'world' } },
88
+ graphiql: true,
89
+ }))
300
90
 
301
- ```ts
302
- import { kb } from 'weifuwu'
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
+ `)
303
96
 
304
- app.use(postgres())
305
- app.use(kb())
97
+ // Dynamic JS compilation (no build step)
98
+ app.get('/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
306
99
 
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
- })
100
+ serve(app, { port: 3000 })
316
101
  ```
317
102
 
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) |
323
- | `list()` | `Document[]` | List documents |
324
- | `get(id)` | `Document \| null` | Get document |
325
- | `delete(id)` | `boolean` | Delete + cascade chunks |
326
-
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:
103
+ ### Router
330
104
 
331
105
  ```ts
332
- app.use(agent({
333
- model: openai('deepseek-v4-flash', ...),
334
- knowledge: { search: async (query, ctx) => ctx.kb.search(query) },
335
- }))
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
336
117
  ```
337
118
 
338
- ### agent
119
+ ### WebSocket
339
120
 
340
121
  ```ts
341
- import { agent } from 'weifuwu'
342
- import { openai } from '@ai-sdk/openai'
343
- import { tool } from 'ai'
344
- import { z } from 'zod'
345
-
346
- app.use(agent({
347
- model: openai('deepseek-v4-flash', { baseURL: 'https://api.deepseek.com/v1' }),
348
- system: 'You are a helpful assistant.',
349
- knowledge: { search: async (q, ctx) => ctx.kb.search(q) },
350
- tools: {
351
- getWeather: tool({
352
- description: 'Get weather for a city',
353
- parameters: z.object({ city: z.string() }),
354
- execute: async ({ city }) => ({ temp: 22, unit: 'C' }),
355
- }),
356
- },
357
- maxSteps: 5,
358
- }))
359
-
360
- app.post('/api/chat', async (req, ctx) => {
361
- const { messages } = await req.json()
362
- return ctx.agent.chatStreamResponse({ messages })
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 */ },
363
127
  })
364
128
  ```
365
129
 
366
- | Method | Description |
367
- |-------------|-------------|
368
- | `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).
374
-
375
- ### cms
130
+ ### GraphQL
376
131
 
377
132
  ```ts
378
- import { cms, requireRole } from 'weifuwu'
379
-
380
- app.use(postgres())
381
- 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
- })
133
+ // At root
134
+ app.graphql(async (req, ctx) => ({
135
+ schema: `type Query { hello: String }`,
136
+ resolvers: { Query: { hello: () => 'world' } },
137
+ graphiql: true,
138
+ }))
391
139
 
392
- app.post('/api/admin/posts', requireRole('admin'), async (req, ctx) =>
393
- Response.json(await ctx.cms.create(await req.json()), { status: 201 }))
140
+ // Or at a custom path
141
+ app.graphql('/graphql', handler)
394
142
  ```
395
143
 
396
- | Method | Returns | Description |
397
- |--------|---------|-------------|
398
- | `create(input)` | `Content` | Create (admin) |
399
- | `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.
410
-
411
- ### base — Dynamic Data Engine
144
+ ### Graceful Shutdown
412
145
 
413
146
  ```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
- })
147
+ const srv = serve(app)
148
+ // Ctrl+C / SIGTERM immediately closes all connections and exits
149
+ await srv.stop() // Programmatic stop
433
150
  ```
434
151
 
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
- ### queue
449
-
450
- ```ts
451
- import { queue } from 'weifuwu'
152
+ ---
452
153
 
453
- const q = queue()
454
- app.use(q) // → ctx.queue
154
+ ## Frontend (`weifuwu/client`)
455
155
 
456
- q.process('email', async (job) => { await sendEmail(job.payload) })
457
- q.cron('cleanup', '0 3 * * *', () => cleanup())
458
- await q.add('email', { to: 'user@example.com' })
459
- q.run()
460
- ```
156
+ 16 runtime exports, zero dependencies, zero virtual DOM.
461
157
 
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 |
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` |
469
170
 
470
- ### ui SSR & SPA
171
+ Types: `Signal`, `Component`, `WfuiContext`, `AppMiddleware`, `RouteDef`
471
172
 
472
- ```ts
473
- import { ui } from 'weifuwu'
173
+ ### Quick Start
474
174
 
475
- app.use(ui())
175
+ ```tsx
176
+ import { signal, Show, For, createApp, ws, router, RouteView } from 'weifuwu/client'
177
+ import type { WfuiContext, RouteDef } from 'weifuwu/client'
476
178
 
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>`)
179
+ const routes: RouteDef[] = [
180
+ { path: '/', component: HomePage },
181
+ { path: '/hello/:name', component: HelloPage },
182
+ ]
483
183
 
484
- // Dynamic JS compilation (no build step needed)
485
- app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
184
+ const app = createApp()
185
+ app.use(ws())
186
+ app.use(router({ routes, mode: 'hash' }))
187
+ app.mount('#root', AppShell)
486
188
 
487
- // CSS with Tailwind support (auto-detects tailwindcss + postcss)
488
- app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
189
+ function AppShell(_props: {}, ctx: WfuiContext) {
190
+ return (
191
+ <div>
192
+ <nav>...</nav>
193
+ <main><RouteView /></main>
194
+ </div>
195
+ )
196
+ }
489
197
  ```
490
198
 
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
- ---
501
-
502
- ## Frontend — weifuwu/client
503
-
504
- Reactive frontend framework. Zero virtual DOM, zero external dependencies. Component model: `(props, ctx) => Node`.
505
-
506
- ### Concepts
199
+ ### Signal
507
200
 
508
201
  ```tsx
509
- // Signal — reactive data
510
202
  const count = signal(0)
511
- count.value = count.value + 1 // DOM updates automatically
512
-
513
- // Computed — derived signals
514
203
  const doubled = computed(() => count.value * 2)
515
-
516
- // Effect — auto-tracked side effects
517
204
  effect(() => console.log('count:', count.value))
518
-
519
- // Component — (props, ctx) => JSX
520
- function MyComponent({ name }: { name: string }, ctx: WfuiContext) {
521
- return <div>Hello {name}</div>
522
- }
205
+ batch(() => { a.value = 1; b.value = 2 })
523
206
  ```
524
207
 
525
- ### createApp + Middleware
208
+ ### Control Flow
526
209
 
527
210
  ```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
211
+ <Show when={isLoggedIn} fallback={<Login />}>
212
+ <Dashboard />
213
+ </Show>
533
214
 
534
- app.mount('#root', AppShell) // SPA mode
535
- app.hydrate('#comments', Comments) // SSR hydration (doesn't clear DOM)
215
+ <For each={items} keyBy="id">
216
+ {(item) => <div>{item.name}</div>}
217
+ </For>
536
218
  ```
537
219
 
538
- ### Router
220
+ ### WebSocket (`ctx.ws`)
539
221
 
540
222
  ```tsx
541
- const routes: RouteDef[] = [
542
- { path: '/', component: HomePage, title: 'Home' },
543
- { path: '/post/:id', component: PostPage, loader: async (ctx) => ({
544
- post: await ctx.api.get(`/api/posts/${ctx.route.params.id}`),
545
- })},
546
- ]
547
-
548
- app.use(router({ routes, notFound: NotFound, mode: 'hash' }))
223
+ app.use(ws({ url: '/ws' }))
549
224
 
550
225
  // 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
226
+ onMount(() => {
227
+ const unsub = ctx.ws.onMessage((data) => { ... })
228
+ onCleanup(() => unsub())
229
+ })
230
+ ctx.ws.send({ type: 'chat', body: 'hello' })
231
+ <Show when={ctx.ws.isConnected}>🟢 已连接</Show>
555
232
  ```
556
233
 
557
- Loader data flow: component renders immediately (shows loading state), loader completes → re-render with data.
558
-
559
- ### wrap() — Third-party Library Integration
234
+ ### Router (`ctx.route`)
560
235
 
561
236
  ```tsx
562
- import { wrap, effect } from 'weifuwu/client'
563
- import * as echarts from 'echarts'
564
-
565
- const PieChart = wrap('div', (el, props: { data: any[] }, ctx) => {
566
- const chart = echarts.init(el)
567
- chart.setOption({ series: [{ type: 'pie', data: props.data }] })
568
- effect(() => chart.setOption({ series: [{ type: 'pie', data: props.data }] }))
569
- return () => chart.dispose() // cleanup when element is removed
570
- })
237
+ app.use(router({ routes, notFound: NotFound, mode: 'hash' }))
571
238
 
572
- // Use as regular component:
573
- <Dashboard><PieChart data={salesData} /></Dashboard>
239
+ ctx.route.path // '/hello/world'
240
+ ctx.route.params // { name: 'world' }
241
+ ctx.route.query // { tab: 'intro' }
242
+ ctx.app.navigate('/hello/world')
574
243
  ```
575
244
 
576
- ### useForm() — Form State Management
245
+ ### Lifecycle
577
246
 
578
247
  ```tsx
579
- import { useForm } from 'weifuwu/client'
580
-
581
- const form = useForm({
582
- initial: { email: '', password: '' },
583
- validate: {
584
- email: (v) => !v.includes('@') && 'Invalid email',
585
- },
248
+ onMount(() => {
249
+ init()
250
+ return () => cleanup() // Auto-cleanup on unmount
586
251
  })
587
-
588
- // Bind to input — {...form.field('name')} sets value + onInput
589
- <input {...form.field('email')} placeholder="Email" />
590
- {form.errors.email && <span>{form.errors.email}</span>}
591
-
592
- // Auto-validates all fields before calling handler
593
- <button onClick={() => form.submit((data) => ctx.login(data.email, data.password))}>
594
- Login
595
- </button>
596
-
597
- // Programmatic control
598
- form.setValue('email', 'a@b.com')
599
- form.setValues({ email: 'a@b.com', password: '123' })
600
- form.reset()
252
+ onCleanup(() => clearInterval(id))
601
253
  ```
602
254
 
603
- ### createPortal & ErrorBoundary
604
-
605
- ```tsx
606
- import { createPortal, Show, ErrorBoundary } from 'weifuwu/client'
607
-
608
- // Portal — render outside parent (modals, dropdowns)
609
- <Show when={showModal}>
610
- {createPortal(
611
- <div class="fixed inset-0 bg-black/50">...</div>,
612
- document.body,
613
- )}
614
- </Show>
615
-
616
- // ErrorBoundary — catch render errors, children must be a thunk
617
- <ErrorBoundary fallback={(e) => <p>Error: {e.message}</p>}>
618
- {() => <Dashboard />}
619
- </ErrorBoundary>
620
- ```
255
+ ### From React
621
256
 
622
- ### Pre-built Components
257
+ | React | weifuwu/client |
258
+ |-------|----------------|
259
+ | `useState(0)` | `signal(0)` |
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>` |
264
+ | `{items.map(i => <X/>)}` | `<For each={items}>{(i) => <X/>}</For>` |
265
+ | `useNavigate()` | `ctx.app.navigate()` |
266
+ | `useParams()` | `ctx.route.params` |
623
267
 
624
- ```tsx
625
- import { LoginForm, Chat } from 'weifuwu/client'
268
+ ---
626
269
 
627
- function LoginPage(_, ctx) {
628
- if (ctx.isAuthenticated) return ctx.app.navigate('/')
629
- return <LoginForm />
630
- }
270
+ ## Demo
631
271
 
632
- function ChatPage(_, ctx) {
633
- return <Chat conversationId="123" />
634
- }
272
+ ```bash
273
+ cd apps/demo
274
+ node server.ts
275
+ # http://localhost:3000
635
276
  ```
636
277
 
637
278
  ---
638
279
 
639
- ## Router
640
-
641
- ```ts
642
- const app = new Router()
643
-
644
- // HTTP
645
- app.get(path, ...handlers)
646
- app.post / put / delete / patch / head / options(path, ...handlers)
647
- app.all(path, ...handlers)
280
+ ## Environment Variables
648
281
 
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
659
- ```
282
+ | Variable | Default | Used by |
283
+ |----------|---------|---------|
284
+ | `DATABASE_URL` | `postgres://root:123456@localhost:5432/demo` | `postgres()` |
285
+ | `REDIS_URL` | `redis://localhost:6379` | `redis()` |
660
286
 
661
287
  ---
662
288
 
@@ -664,58 +290,28 @@ app.routes() // debug: list all routes
664
290
 
665
291
  ```
666
292
  src/
667
- ├── index.tsEntry, exports all modules
668
- ├── types.tsContext, 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
690
- │ ├── lib/
691
- │ │ └── form.ts ← useForm
692
- │ ├── middleware/
693
- │ │ ├── api.ts ← HTTP client
694
- │ │ ├── auth.ts ← Login / logout / token
695
- │ │ └── ws.ts ← WebSocket
696
- │ └── 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)
707
- └── tsconfig.json
708
-
709
- 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
710
311
  ```
711
312
 
712
- ---
713
-
714
- ## Development
715
-
716
313
  ```bash
717
- docker compose up -d # Start postgres + redis
718
- npm run build # esbuild → dist/
719
- npm run typecheck # tsc --noEmit
720
- npm test # Run tests
314
+ npm run build # esbuild dist/
315
+ npm run typecheck # tsc --noEmit
316
+ npm test # Run all tests
721
317
  ```