weifuwu 0.32.1 → 0.33.1

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
@@ -1,325 +1,309 @@
1
1
  # weifuwu
2
2
 
3
- Web-standard HTTP microframework for Node.js — `(req, ctx) => Response`.
3
+ **AI SaaS framework** — `(req, ctx) => Response`
4
4
 
5
5
  ```bash
6
6
  npm install weifuwu
7
7
  ```
8
8
 
9
+ User system, instant messaging, RAG knowledge base, AI Agent, CMS, dynamic data storage. Configure environment variables and go.
10
+
11
+ ---
12
+
13
+ ## Quick Start
14
+
9
15
  ```ts
10
- import { serve, Router } from 'weifuwu'
16
+ import { serve, Router, postgres, user, kb, agent, messager } from 'weifuwu'
17
+ import { openai } from '@ai-sdk/openai'
11
18
 
12
19
  const app = new Router()
13
- app.get('/', () => new Response('Hello'))
14
- serve(app, { port: 3000 })
15
- ```
16
-
17
- ## Exports
20
+ app.use(postgres())
21
+ app.use(user())
22
+ app.use(kb())
23
+ app.use(messager())
24
+ app.use(agent({
25
+ model: openai('deepseek-v4-flash', { baseURL: 'https://api.deepseek.com/v1' }),
26
+ knowledge: { search: async (q, ctx) => ctx.kb.search(q) },
27
+ }))
18
28
 
19
- ### Core
29
+ app.post('/api/chat', async (req, ctx) => {
30
+ const { messages } = await req.json()
31
+ return ctx.agent.chatStreamResponse({ messages })
32
+ })
20
33
 
21
- | Export | Description |
22
- |---|---|
23
- | `serve(app, opts?)` | Start HTTP server. Returns `Server` with `port`, `hostname`, `ready`, `close()`. |
24
- | `Router` | Trie-based HTTP router with WebSocket support and `plugin()` method. |
25
- | `HttpError` | `new HttpError(message, status)`. Throw to return that status code. |
26
- | `DEFAULT_MAX_BODY` | `10 * 1024 * 1024` (10MB). |
34
+ serve(app, { port: 3000 })
35
+ ```
27
36
 
28
- ### Router API
37
+ ### Environment Variables
29
38
 
30
- ```ts
31
- const app = new Router()
39
+ | Variable | Default | Used by |
40
+ |----------|---------|---------|
41
+ | `DATABASE_URL` | `postgres://root:123456@localhost:5432/demo` | `postgres()` |
42
+ | `REDIS_URL` | `redis://localhost:6379` | `redis()` |
43
+ | `JWT_SECRET` | — | `user()` |
44
+ | `DASHSCOPE_API_KEY` | — | `kb()` (embedding) |
45
+ | `DEEPSEEK_API_KEY` / `OPENAI_API_KEY` | — | `agent()` (LLM) |
46
+ | `DEEPSEEK_MODEL` | `deepseek-v4-flash` | `agent()` |
32
47
 
33
- // HTTP methods
34
- app.get(path, ...handlers)
35
- app.post / put / delete / patch / head / options(path, ...handlers)
36
- app.all(path, ...handlers) // any method
48
+ ---
37
49
 
38
- // WebSocket
39
- app.ws(path, ...middlewares, handler)
40
- // handler: { open?, message?, close?, error? }
50
+ ## Modules
41
51
 
42
- // Middleware & mounting
43
- app.use(middleware) // global middleware
44
- app.mount(prefix, router) // sub-router at prefix
45
- app.plugin(fn) // extension: (app) => { app.get(), app.use(), ... }
46
- app.onError(handler) // error handler: (error, req, ctx) => Response
47
- app.routes() // debug: list all registered routes
48
-
49
- // Path params
50
- app.get('/users/:id', (req, ctx) => {
51
- ctx.params.id // string
52
- ctx.query.search // from ?search=...
53
- })
54
- ```
52
+ | Module | Import | Dependency | Purpose |
53
+ |--------|--------|-----------|---------|
54
+ | User | `user()` | `postgres()` | Auth, JWT, roles |
55
+ | Messager | `messager()` | `postgres()`, `user()` | IM + AI conversation layer |
56
+ | KB | `kb()` | `postgres()` | RAG knowledge base |
57
+ | Agent | `agent()` | | LLM chat, tool calling, streaming |
58
+ | CMS | `cms()` | `postgres()`, `user()` | Blog, docs, changelog |
59
+ | Base | `base()` | `postgres()`, `user()` | Dynamic data engine |
55
60
 
56
61
  ### Middleware
57
62
 
58
- | Export | Description |
59
- |---|---|
60
- | `cors(opts?)` | CORS headers. `origin`, `methods`, `headers`, `credentials`, `maxAge`. |
61
- | `helmet(opts?)` | Security headers: CSP, HSTS, X-Frame-Options, X-Content-Type-Options, etc. |
62
- | `compress(opts?)` | gzip / brotli / deflate response compression. |
63
- | `rateLimit(opts?)` | Sliding-window rate limiter. `windowMs`, `max`, `keyGenerator`, Redis backend. |
64
- | `logger(opts?)` | Request logging. `format: 'short' | 'combined' | 'json'`. |
65
- | `upload(opts?)` | Multipart file upload via `req.formData()`. Injects `ctx.parsed`. |
66
- | `serveStatic(root, opts?)` | Static file handler. `cacheControl`, `index`. |
67
- | `sandbox(opts?)` | Filesystem isolation for agent operations. `baseDir`, `timeout`, `isolateBy`. |
68
- | `auth(opts?)` | Authentication: JWT, session cookies, API keys. Injects `ctx.user`. |
69
-
70
- ### Tracing
71
-
72
- | Export | Description |
73
- |---|---|
74
- | `currentTraceId()` | Current request's trace ID (AsyncLocalStorage). |
75
- | `currentTrace()` | Current trace context `{ traceId, startTime }`. |
76
- | `runWithTrace(id, fn)` | Run `fn` in a trace context. Auto-generates UUID if id is null. |
77
- | `traceElapsed()` | Milliseconds since trace start. |
78
- | `trace(opts?)` | Middleware that injects `ctx.trace`. |
79
-
80
- ### Postgres
81
-
82
- ```ts
83
- import { postgres, MIGRATIONS_TABLE } from 'weifuwu'
63
+ | Import | Purpose |
64
+ |--------|---------|
65
+ | `cors()` | CORS headers |
66
+ | `helmet()` | Security headers |
67
+ | `compress()` | gzip / brotli / deflate |
68
+ | `rateLimit()` | Sliding-window rate limiter |
69
+ | `logger()` | Request logging |
70
+ | `upload()` | Multipart file upload |
71
+ | `serveStatic()` | Static files |
72
+ | `sandbox()` | Filesystem isolation |
84
73
 
85
- const sql = postgres({ url: process.env.DATABASE_URL })
86
- app.use(sql) // injects ctx.sql
74
+ ### Core
87
75
 
88
- const rows = await sql.sql`SELECT * FROM users WHERE id = ${id}`
76
+ | Import | Purpose |
77
+ |--------|---------|
78
+ | `serve(app, opts?)` | Start HTTP server |
79
+ | `Router` | Trie-based router + WebSocket |
80
+ | `HttpError` | `throw new HttpError(msg, 404)` |
81
+ | `trace()` | Request tracing |
82
+ | `postgres()` | PostgreSQL client (`ctx.sql`) |
83
+ | `redis()` | Redis client (`ctx.redis`) |
84
+ | `queue()` | Job queue + cron |
85
+ | `createHub()` | WebSocket pub/sub |
89
86
 
90
- // Migrations
91
- await sql.migrate({
92
- '001_init': async (s) => await s`CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)`,
93
- })
94
- ```
87
+ ### Utilities
95
88
 
96
- ### Redis
89
+ | Import | Purpose |
90
+ |--------|---------|
91
+ | `requireRole('admin')` | Middleware: check `ctx.user.role` |
97
92
 
98
- ```ts
99
- import { redis } from 'weifuwu'
93
+ ---
100
94
 
101
- const r = redis({ url: process.env.REDIS_URL })
102
- app.use(r) // injects ctx.redis
103
- await r.redis.set('key', 'value')
104
- ```
95
+ ## user
105
96
 
106
- ### Queue & Cron
97
+ Auth, registration, JWT, password management, roles.
107
98
 
108
99
  ```ts
109
- import { queue } from 'weifuwu'
100
+ import { user, requireRole } from 'weifuwu'
110
101
 
111
- const q = queue()
112
- app.use(q) // injects ctx.queue
102
+ app.use(postgres())
103
+ app.use(user({ secret: process.env.JWT_SECRET }))
113
104
 
114
- q.process('email', async (job) => {
115
- await sendEmail(job.payload.to)
105
+ // Register
106
+ app.post('/api/register', async (req, ctx) => {
107
+ const result = await ctx.userModule.register(await req.json())
108
+ return Response.json(result)
116
109
  })
117
-
118
- q.cron('cleanup', '0 3 * * *', async () => {
119
- await cleanupOldSessions()
110
+ // Login
111
+ app.post('/api/login', async (req, ctx) => {
112
+ const { email, password } = await req.json()
113
+ const result = await ctx.userModule.login(email, password)
114
+ if (!result) return new Response('Unauthorized', { status: 401 })
115
+ return Response.json(result)
120
116
  })
117
+ // Current user
118
+ app.get('/api/me', async (req, ctx) => {
119
+ if (!ctx.user) return new Response('Unauthorized', { status: 401 })
120
+ return Response.json(ctx.user)
121
+ })
122
+ // Admin only
123
+ app.get('/api/admin/users', requireRole('admin'), async (req, ctx) => {
124
+ return Response.json(await ctx.userModule.listUsers())
125
+ })
126
+ ```
121
127
 
122
- await q.add('email', { to: 'user@example.com' })
123
- await q.add('remind', {}, { delay: 60_000 })
124
- await q.add('report', {}, { schedule: '0 9 * * 1' })
128
+ ### ctx.userModule API
125
129
 
126
- q.run() // start processing
127
- ```
130
+ | Method | Returns | Description |
131
+ |--------|---------|-------------|
132
+ | `register(input)` | `{ user, token }` | Register |
133
+ | `login(email, pw)` | `{ user, token } \| null` | Login |
134
+ | `getUserById(id)` | `UserRecord \| null` | Get by ID |
135
+ | `getUserByEmail(email)` | `UserRecord \| null` | Get by email |
136
+ | `updateUser(id, input)` | `UserRecord \| null` | Update |
137
+ | `changePassword(id, oldPw, newPw)` | `boolean` | Change password |
138
+ | `deleteUser(id)` | `boolean` | Soft delete |
139
+ | `listUsers(inactive?)` | `UserRecord[]` | List users |
140
+ | `generateToken(user)` | `string` | Issue JWT |
141
+ | `verifyToken(token)` | `TokenPayload \| null` | Verify JWT |
142
+ | `refreshToken(token)` | `string \| null` | Refresh JWT |
128
143
 
129
- ### WebSocket Hub
144
+ ### ctx.user
130
145
 
131
- ```ts
132
- import { createHub } from 'weifuwu'
146
+ Auto-resolved from `Authorization: Bearer` or `token` cookie.
133
147
 
134
- const hub = createHub()
135
- app.ws('/chat', {
136
- open(ws, ctx) {
137
- ctx.ws.join('lobby')
138
- ctx.ws.json({ type: 'join' })
139
- },
140
- message(ws, ctx, data) {
141
- ctx.ws.sendRoom('lobby', { text: data.toString() })
142
- },
143
- })
148
+ ```ts
149
+ interface User {
150
+ id: string
151
+ name: string
152
+ email: string
153
+ role: string
154
+ [key: string]: unknown
155
+ }
144
156
  ```
145
157
 
146
- ### GraphQL
158
+ ### requireRole
147
159
 
148
160
  ```ts
149
- import { graphql } from 'weifuwu'
161
+ app.get('/admin', requireRole('admin'), handler)
162
+ // No auth → 401, wrong role → 403
163
+ ```
150
164
 
151
- const gql = graphql((req, ctx) => ({
152
- schema: `
153
- type Query {
154
- hello: String
155
- user(id: ID!): User
156
- }
157
- type User {
158
- id: ID
159
- name: String
160
- }
161
- `,
162
- resolvers: {
163
- Query: {
164
- hello: () => 'world',
165
- user: (_, { id }) => ctx.sql`SELECT * FROM users WHERE id = ${id}`.then(r => r[0]),
166
- },
167
- },
168
- graphiql: true,
169
- }))
165
+ ### Security
170
166
 
171
- app.mount('/graphql', gql)
172
- ```
167
+ - Password: scrypt + 32-byte random salt
168
+ - Token: HMAC SHA-256, 7 day expiry
173
169
 
174
- ### React SSR
170
+ ---
175
171
 
176
- > Requires `react >= 19`, `react-dom >= 19` (optional peerDependencies).
172
+ ## messager
177
173
 
178
- ```bash
179
- npm install react react-dom
180
- ```
174
+ Instant messaging + AI conversation layer. Direct/group chat, message persistence, WebSocket push.
181
175
 
182
176
  ```ts
183
- // server.ts the only file you need
184
- import { serve, Router } from 'weifuwu'
185
- import { react } from 'weifuwu/react'
177
+ import { messager } from 'weifuwu'
186
178
 
187
- const app = new Router()
188
- .use(trace())
189
- .use(logger())
190
- .plugin(react({
191
- pages: {
192
- '/': './pages/Home.tsx',
193
- '/users': './pages/Users.tsx',
194
- '/users/:id': './pages/UserDetail.tsx',
195
- },
196
- layout: './layouts/Root.tsx',
197
- notFound: './pages/NotFound.tsx',
198
- tailwind: { entry: './styles/input.css' },
199
- }))
179
+ app.use(postgres())
180
+ app.use(user())
181
+ app.use(messager())
200
182
 
201
- app.get('/api/hello', () => Response.json({ message: 'hi' }))
202
- serve(app, { port: 3000 })
183
+ // WebSocket auto-join all user conversations
184
+ app.ws('/ws', {
185
+ async open(ws, ctx) {
186
+ for (const c of await ctx.messager.getConversations()) {
187
+ ctx.ws.join(`conversation:${c.id}`)
188
+ }
189
+ },
190
+ })
191
+
192
+ // REST API
193
+ app.post('/api/messages', async (req, ctx) => {
194
+ const { conversationId, body } = await req.json()
195
+ const msg = await ctx.messager.sendMessage(conversationId, body)
196
+ return Response.json(msg, { status: 201 })
197
+ })
198
+
199
+ app.get('/api/conversations/:id/messages', async (req, ctx) => {
200
+ const url = new URL(req.url)
201
+ return Response.json(await ctx.messager.getMessages(ctx.params.id, {
202
+ before: url.searchParams.get('before') || undefined,
203
+ limit: parseInt(url.searchParams.get('limit') || '50'),
204
+ }))
205
+ })
203
206
  ```
204
207
 
205
- **One `react()` call handles:** SSR rendering, routing, data loading, Tailwind CSS, client bundle auto-generation, and error pages. No `client.ts`, `routes.ts`, or manual middleware setup needed.
208
+ ### ctx.messager API
206
209
 
207
- #### Page components
210
+ | Method | Returns | Description |
211
+ |--------|---------|-------------|
212
+ | `createDirectConversation(userId)` | `Conversation` | Create/reuse DM |
213
+ | `createGroupConversation(title, userIds)` | `Conversation` | Create group |
214
+ | `sendMessage(convId, body)` | `Message` | Send, auto-broadcast to `conversation:{id}` room |
215
+ | `getMessages(convId, opts?)` | `Message[]` | Cursor pagination |
216
+ | `editMessage(msgId, body)` | `Message \| null` | Edit (24h window) |
217
+ | `deleteMessage(msgId)` | `boolean` | Soft delete |
218
+ | `getConversations()` | `Conversation[]` | List with unread + last message |
219
+ | `getConversation(id)` | `Conversation \| null` | Get detail |
220
+ | `markRead(convId)` | `void` | Mark as read |
221
+ | `getUnreadCount()` | `{ total, byConversation }` | Unread stats |
222
+ | `addParticipants(convId, userIds)` | `void` | Add members |
223
+ | `removeParticipant(convId, userId?)` | `boolean` | Leave / kick |
208
224
 
209
- ```tsx
210
- // pages/UserDetail.tsx
211
- import type { Context } from 'weifuwu'
212
- import { HttpError } from 'weifuwu'
213
- import { useServerData } from 'weifuwu/react'
225
+ ### Storage
214
226
 
215
- export async function loader(ctx: Context) {
216
- const user = await db.find(ctx.params.id)
217
- if (!user) throw new HttpError('Not found', 404)
218
- return { user }
219
- }
227
+ 3 tables: `conversations` / `participants` / `messages`. Auto-migration.
220
228
 
221
- export default function UserDetailPage() {
222
- const { user } = useServerData<{ user: User }>()
223
- return (
224
- <div>
225
- <title>{`${user.name} — My App`}</title>
226
- <h1>{user.name}</h1>
227
- <p>{user.email}</p>
228
- </div>
229
- )
230
- }
231
- ```
229
+ ### AI Conversations
232
230
 
233
- - `export async function loader(ctx)` runs on the server, returns data for `useServerData()`
234
- - `throw new HttpError('Not found', 404)` — renders the NotFound page with correct status
235
- - `<title>` auto-hoists to `<head>` via React 19
236
- - `export default` is auto-detected; named exports work too
231
+ messager + agent = ChatGPT foundation. Messager handles sessions + push, agent handles LLM generation.
237
232
 
238
- #### Layout with shared data
233
+ ---
239
234
 
240
- ```tsx
241
- // layouts/Root.tsx
242
- export async function loader(ctx: Context) {
243
- return { currentUser: await getCurrentUser(ctx) }
244
- }
235
+ ## kb
245
236
 
246
- export function Root({ children }: { children: ReactNode }) {
247
- const { currentUser } = useServerData()
248
- return (
249
- <>
250
- <nav>
251
- <a href="/">Home</a>
252
- <a href="/users">Users</a>
253
- <span>{currentUser?.name}</span>
254
- </nav>
255
- <main>{children}</main>
256
- </>
257
- )
258
- }
259
- ```
237
+ RAG knowledge base. Import docs auto-chunk DashScope embedding → pgvector storage → semantic search.
260
238
 
261
- Layout `loader` data merges with page data. Page loader overrides same keys.
239
+ ```ts
240
+ import { kb } from 'weifuwu'
262
241
 
263
- #### Data flow
242
+ app.use(postgres())
243
+ app.use(kb())
264
244
 
265
- ```
266
- layout loader ──┐
267
- ├──→ merge useServerData() ──→ Layout + Page
268
- page loader ───┘
269
- ```
245
+ // Import
246
+ app.post('/api/kb/import', async (req, ctx) => {
247
+ const { title, content } = await req.json()
248
+ const result = await ctx.kb.importText(title, content)
249
+ return Response.json(result, { status: 201 })
250
+ })
270
251
 
271
- #### Client-side SPA
252
+ // Search
253
+ app.post('/api/kb/search', async (req, ctx) => {
254
+ const { query } = await req.json()
255
+ return Response.json(await ctx.kb.search(query, { limit: 5 }))
256
+ })
257
+ ```
272
258
 
273
- Every page is automatically code-split into a separate chunk. On first visit the server renders HTML and the client hydrates. Subsequent navigations intercept `<a>` clicks, `import()` the page chunk, and fetch fresh server data — all automatic, zero config.
259
+ ### ctx.kb API
274
260
 
275
- #### Streaming SSR
261
+ | Method | Returns | Description |
262
+ |--------|---------|-------------|
263
+ | `importText(title, text, opts?)` | `{ document, chunks }` | Import → chunk → embed → store |
264
+ | `importDocuments(docs)` | `Document[]` | Batch import |
265
+ | `search(query, opts?)` | `SearchResult[]` | Semantic search (cosine) |
266
+ | `list()` | `Document[]` | List documents |
267
+ | `get(id)` | `Document \| null` | Get document |
268
+ | `getChunks(documentId)` | `Chunk[]` | Get chunks |
269
+ | `delete(id)` | `boolean` | Delete + cascade chunks |
276
270
 
277
- ```tsx
278
- import { Suspense, use } from 'react'
271
+ ### Configuration
279
272
 
280
- export default function StreamingPage() {
281
- return (
282
- <div>
283
- <h1>Instant shell</h1>
284
- <Suspense fallback={<Spinner />}>
285
- <SlowData promise={fetchSlowData()} />
286
- </Suspense>
287
- </div>
288
- )
289
- }
273
+ ```ts
274
+ // Default: DashScope text-embedding-v4 (env: DASHSCOPE_API_KEY)
275
+ app.use(kb())
276
+
277
+ // Custom embedding
278
+ app.use(kb({
279
+ embed: async (text) => { /* return number[] */ },
280
+ dimensions: 1536,
281
+ chunkSize: 512, // tokens
282
+ chunkOverlap: 64,
283
+ }))
290
284
  ```
291
285
 
292
- `<Suspense>` boundaries stream to the browser as data resolves.
293
-
294
- | Feature | Description |
295
- |---|---|
296
- | `react({ pages, layout, notFound, tailwind })` | One call: SSR + routing + client bundle + error handling |
297
- | `export async function loader(ctx)` | Server-side data loading, auto-detected by the framework |
298
- | `useServerData<T>()` | Type-safe access to loader data in any component |
299
- | `Link` | `<a>` that does SPA navigation on the client |
300
- | `ErrorBoundary` | Catches render errors on server and client |
301
- | `<title>`, `<meta>` | Auto-hoisted to `<head>` via React 19 |
302
- | `<Suspense>` | Streaming SSR, works out of the box |
303
- | `client: false` | Disable client JS for static SSR only |
304
-
305
- **Import paths:**
286
+ ### Integration with Agent
306
287
 
307
288
  ```ts
308
- // Server-side
309
- import { react, Link, ErrorBoundary, useServerData } from 'weifuwu'
310
-
311
- // Client-side (for custom advanced setups)
312
- import { createBrowserRouter, hydrate, navigate } from 'weifuwu/react/client'
289
+ app.use(agent({
290
+ model: openai('deepseek-v4-flash', { baseURL: 'https://api.deepseek.com/v1' }),
291
+ knowledge: {
292
+ search: async (query, ctx) => ctx.kb.search(query),
293
+ },
294
+ }))
313
295
  ```
314
296
 
315
- See [examples/react-ssr/](examples/react-ssr/) for the full demo.
297
+ ### Storage
298
+
299
+ - `kb_documents` — document metadata
300
+ - `kb_chunks` — chunk content + VECTOR(1536) + TSVECTOR GIN index
316
301
 
317
- ### AI Agent
302
+ ---
318
303
 
319
- > Requires `ai` (Vercel AI SDK). Install with a model provider:
320
- > ```bash
321
- > npm install ai @ai-sdk/openai
322
- > ```
304
+ ## agent
305
+
306
+ AI Agent LLM chat, tool calling, RAG, streaming.
323
307
 
324
308
  ```ts
325
309
  import { agent } from 'weifuwu'
@@ -328,17 +312,10 @@ import { tool } from 'ai'
328
312
  import { z } from 'zod'
329
313
 
330
314
  app.use(agent({
331
- model: openai('gpt-4o'),
315
+ model: openai('deepseek-v4-flash', { baseURL: 'https://api.deepseek.com/v1' }),
332
316
  system: 'You are a helpful assistant.',
333
317
  knowledge: {
334
- // RAG — search a knowledge base before each response
335
- search: async (query, ctx) => {
336
- const { embeddings } = await embedModel.doEmbed({ values: [query] })
337
- return ctx.sql`
338
- SELECT content, 1 - (embedding <=> ${embeddings[0]}::vector) AS score
339
- FROM docs ORDER BY embedding <=> ${embeddings[0]}::vector LIMIT 3
340
- `
341
- },
318
+ search: async (query, ctx) => ctx.kb.search(query),
342
319
  },
343
320
  tools: {
344
321
  getWeather: tool({
@@ -350,198 +327,293 @@ app.use(agent({
350
327
  maxSteps: 5,
351
328
  }))
352
329
 
330
+ // Streaming
353
331
  app.post('/api/chat', async (req, ctx) => {
354
332
  const { messages } = await req.json()
355
333
  return ctx.agent.chatStreamResponse({ messages })
356
334
  })
335
+
336
+ // Non-streaming
337
+ app.post('/api/chat/sync', async (req, ctx) => {
338
+ const { prompt } = await req.json()
339
+ const text = await ctx.agent.chat(prompt)
340
+ return Response.json({ text })
341
+ })
357
342
  ```
358
343
 
359
- **`agent()` handles:** multi-turn conversations, automatic tool-calling loops (maxSteps), knowledge retrieval (RAG) injected into the system prompt, and SSE streaming compatible with `useChat` from `@ai-sdk/react`.
344
+ ### ctx.agent API
345
+
346
+ | Method | Description |
347
+ |--------|-------------|
348
+ | `chat(prompt, opts?)` | Non-streaming, returns text |
349
+ | `chatStreamResponse({ messages })` | SSE stream (compatible with `useChat`) |
350
+
351
+ ### Default Model
352
+
353
+ - LLM: DeepSeek-V4-Flash (via `@ai-sdk/openai` + `baseURL: 'https://api.deepseek.com/v1'`)
354
+ - Override with `DEEPSEEK_MODEL` env
355
+ - API key via `DEEPSEEK_API_KEY` or `OPENAI_API_KEY` env
356
+
357
+ ### Features
360
358
 
361
359
  | Feature | Description |
362
- |---|---|
363
- | `ctx.agent.chat(prompt, opts?)` | Non-streaming chat with tool calling and RAG |
364
- | `ctx.agent.chatStreamResponse({ messages })` | SSE streaming response (useChat-compatible) |
365
- | `knowledge.search` | User-defined RAG callback — query any data source via `ctx` |
366
- | `tools` | Tool definitions (from `ai` package). Executed in automatic loops |
367
- | `agents` | Named sub-agents with different models/tools |
368
- | `sandbox: true` | Auto-integrate with `ctx.sandbox` for file operations |
369
- | `store` | Session persistence (save/load conversation history) |
360
+ |---------|-------------|
361
+ | `knowledge.search` | RAG callback, auto-injected into system prompt |
362
+ | `tools` | Tool definitions, auto-loop (maxSteps) |
363
+ | `sandbox: true` | Integrates with `ctx.sandbox` |
364
+ | `store` | Session persistence (save/load) |
365
+ | `agents` | Multi-agent orchestration |
370
366
 
371
- ### Auth
367
+ ---
372
368
 
373
- JWT extraction priority: `cookie` → `Authorization: Bearer` → `?access_token=`. All callbacks receive `ctx` so `ctx.sql` is directly available.
369
+ ## cms
374
370
 
375
- ```ts
376
- import { auth } from 'weifuwu'
371
+ Content management — blog, docs, changelog.
377
372
 
378
- // JWT — cookie, header, or ?access_token=
379
- app.use(auth({ jwt: { secret: process.env.JWT_SECRET } }))
373
+ ```ts
374
+ import { cms, requireRole } from 'weifuwu'
380
375
 
381
- // Session cookie — loadUser gets ctx
382
- app.use(auth({
383
- session: {
384
- secret: '...',
385
- loadUser: async (data, ctx) => ctx.sql`SELECT * FROM users WHERE id = ${data.userId}`,
386
- },
387
- }))
376
+ app.use(postgres())
377
+ app.use(user())
378
+ app.use(cms())
388
379
 
389
- // API key — header or ?api_key=
390
- app.use(auth({ apiKey: {
391
- query: 'api_key',
392
- validate: async (key, ctx) => ctx.sql`SELECT * FROM users WHERE api_key = ${key}`,
393
- } }))
380
+ // Public
381
+ app.get('/api/posts', async (req, ctx) => {
382
+ return Response.json(await ctx.cms.list({ type: 'post', status: 'published' }))
383
+ })
384
+ app.get('/api/posts/:slug', async (req, ctx) => {
385
+ const post = await ctx.cms.get(ctx.params.slug)
386
+ if (!post) return new Response('Not found', { status: 404 })
387
+ return Response.json(post)
388
+ })
394
389
 
395
- // ctx.user is now available
396
- app.get('/me', (req, ctx) => {
397
- if (!ctx.user) return new Response('Unauthorized', { status: 401 })
398
- return Response.json(ctx.user)
390
+ // Admin
391
+ app.post('/api/admin/posts', requireRole('admin'), async (req, ctx) => {
392
+ const post = await ctx.cms.create(await req.json())
393
+ return Response.json(post, { status: 201 })
394
+ })
395
+ app.patch('/api/admin/posts/:id', requireRole('admin'), async (req, ctx) => {
396
+ const post = await ctx.cms.update(ctx.params.id, await req.json())
397
+ if (!post) return new Response('Not found', { status: 404 })
398
+ return Response.json(post)
399
399
  })
400
400
  ```
401
401
 
402
- Supports JWT (HS256 via cookie or Authorization header), signed session cookies with `loadUser` callback, and API key validation. All are optional — requests proceed without a user identity unless handlers enforce it.
402
+ ### ctx.cms API
403
403
 
404
- ### Sandbox
404
+ | Method | Returns | Description |
405
+ |--------|---------|-------------|
406
+ | `create(input)` | `Content` | Create (admin) |
407
+ | `get(slug)` | `Content \| null` | Get by slug |
408
+ | `getById(id)` | `Content \| null` | Get by ID |
409
+ | `update(id, input)` | `Content \| null` | Update (admin) |
410
+ | `delete(id)` | `boolean` | Delete (admin) |
411
+ | `list(opts?)` | `Content[]` | List with cursor |
412
+ | `publish(id)` | `Content \| null` | Publish (admin) |
413
+ | `unpublish(id)` | `Content \| null` | Unpublish (admin) |
414
+ | `listTags()` | `TagWithCount[]` | List tags |
415
+ | `createTag(name)` | `Tag` | Create tag |
405
416
 
406
- ```ts
407
- import { sandbox } from 'weifuwu'
417
+ ### Features
408
418
 
409
- app.use(sandbox({
410
- baseDir: '/tmp/workspaces',
411
- timeout: 30000,
412
- isolateBy: 'user', // one directory per ctx.user.id
413
- }))
419
+ - Types: post / page / doc / changelog (any string)
420
+ - Status: draft / published / archived
421
+ - Slug: auto-generated, unique per type
422
+ - Tags: many-to-many, auto-created
423
+ - Tree: parent_id for hierarchy
424
+ - Auth: non-admin users see published only
414
425
 
415
- // ctx.sandbox provides isolated file + exec operations
416
- await ctx.sandbox.writeFile('hello.txt', 'world')
417
- const content = await ctx.sandbox.readFile('hello.txt')
418
- const { stdout } = await ctx.sandbox.exec('ls -la')
419
- await ctx.sandbox.destroy() // clean up workspace
420
- ```
426
+ ---
421
427
 
422
- All file paths are validated — escapes (`../`) are rejected. `exec()` enforces timeout and sets `HOME` to the workspace directory. When `isolateBy: 'user'` is set, each user gets their own directory under `baseDir`.
428
+ ## base
423
429
 
424
- ### Types
430
+ Dynamic data storage engine — let users define their own data structures (like Airtable).
425
431
 
426
- | Export | Description |
427
- |---|---|
428
- | `Context` | `{ params, query, mountPath?, [key: string] }` |
429
- | `Handler<T>` | `(req: Request, ctx: T) => Response \| Promise<Response>` |
430
- | `Middleware` | `(req, ctx, next) => Response \| Promise<Response>` |
431
- | `WebSocketHandler` | `{ open?, message?, close?, error? }` |
432
- | `HttpError` | `Error` subclass with `.status: number` |
433
- | `Closeable` | `{ close(): Promise<void> }` |
434
- | `Server` | `{ close, port, hostname, ready }` |
435
- | `ServeOptions` | `{ port, hostname, signal, maxBodySize, timeout, keepAliveTimeout, headersTimeout, shutdown }` |
432
+ ```ts
433
+ import { base } from 'weifuwu'
436
434
 
437
- ## Handler / Middleware patterns
435
+ app.use(postgres())
436
+ app.use(user())
437
+ app.use(base())
438
438
 
439
- ```ts
440
- // Handler standard Web API
441
- app.get('/api/data', (req, ctx) => {
442
- const q = ctx.query.q
443
- return Response.json({ q })
439
+ // Define schema
440
+ app.post('/api/bases', async (req, ctx) => {
441
+ const b = await ctx.base.create(await req.json())
442
+ return Response.json(b, { status: 201 })
444
443
  })
445
444
 
446
- // Async
447
- app.get('/db/users', async (req, ctx) => {
448
- const rows = await ctx.sql`SELECT * FROM users`
449
- return Response.json(rows)
445
+ // CRUD
446
+ app.post('/api/bases/:id/:table', async (req, ctx) => {
447
+ const row = await ctx.base.insert(ctx.params.id, ctx.params.table, await req.json())
448
+ return Response.json(row, { status: 201 })
450
449
  })
451
450
 
452
- // Throwing HttpError
453
- app.get('/item/:id', (req, ctx) => {
454
- const item = db.get(ctx.params.id)
455
- if (!item) throw new HttpError('Not found', 404)
456
- return Response.json(item)
451
+ app.get('/api/bases/:id/:table', async (req, ctx) => {
452
+ const url = new URL(req.url)
453
+ return Response.json(await ctx.base.query(ctx.params.id, ctx.params.table, {
454
+ filter: url.searchParams.get('filter') ? JSON.parse(url.searchParams.get('filter')!) : undefined,
455
+ limit: parseInt(url.searchParams.get('limit') || '50'),
456
+ }))
457
457
  })
458
+ ```
458
459
 
459
- // Middleware
460
- app.use(async (req, ctx, next) => {
461
- const start = Date.now()
462
- const res = await next(req, ctx)
463
- console.log(`${req.method} ${req.url} ${Date.now() - start}ms`)
464
- return res
465
- })
460
+ ### ctx.base API
466
461
 
467
- // Error handler
468
- app.onError((err, req, ctx) => {
469
- if (err instanceof HttpError) {
470
- return Response.json({ error: err.message }, { status: err.status })
471
- }
472
- return new Response('Internal error', { status: 500 })
473
- })
462
+ | Method | Returns | Description |
463
+ |--------|---------|-------------|
464
+ | `create({ name, tables })` | `BaseDef` | Create database |
465
+ | `defineTable(baseId, schema)` | `BaseDef` | Add table |
466
+ | `updateTable(baseId, name, schema)` | `BaseDef \| null` | Update table |
467
+ | `removeTable(baseId, name)` | `BaseDef \| null` | Remove table |
468
+ | `insert(baseId, table, data)` | `Row` | Insert row |
469
+ | `getRow(baseId, table, id)` | `Row \| null` | Get row |
470
+ | `updateRow(baseId, table, id, data)` | `Row \| null` | Update row |
471
+ | `deleteRow(baseId, table, id)` | `boolean` | Delete row |
472
+ | `query(baseId, table, opts?)` | `Row[]` | Query (filter/sort/limit/offset) |
473
+ | `search(baseId, table, field, query)` | `Row[]` | Full-text search |
474
+ | `similaritySearch(baseId, table, field, vector)` | `Row[]` | Vector search |
475
+ | `list()` / `get(id)` / `getBySlug(slug)` / `delete(id)` | — | Manage databases |
476
+
477
+ ### Architecture
478
+
479
+ Fixed Slot: a single `base_data` table with ~120 physical columns:
480
+
481
+ | Type | Count | PG Type |
482
+ |------|:-----:|:--------:|
483
+ | text001..064 | 64 | TEXT |
484
+ | number001..032 | 32 | DOUBLE PRECISION |
485
+ | date001..008 | 8 | TIMESTAMPTZ |
486
+ | vector001..004 | 4 | VECTOR(1536) |
487
+ | search001..004 | 4 | TEXT |
488
+ | ext | 1 | JSONB (overflow) |
489
+
490
+ Field name → physical column mapping stored in `base_column_map`. Fields beyond the physical columns overflow to ext JSONB.
491
+
492
+ pgvector auto-detected (included in docker image).
493
+
494
+ ---
495
+
496
+ ---
497
+
498
+ ## Router
499
+
500
+ ```ts
501
+ const app = new Router()
502
+
503
+ // HTTP
504
+ app.get(path, ...handlers)
505
+ app.post / put / delete / patch / head / options(path, ...handlers)
506
+ app.all(path, ...handlers)
507
+
508
+ // WebSocket
509
+ app.ws(path, ...middlewares, handler)
510
+ // handler: { open?, message?, close?, error? }
511
+
512
+ // Middleware & mounting
513
+ app.use(middleware)
514
+ app.mount(prefix, router)
515
+ app.plugin(fn)
516
+ app.onError(handler)
517
+ app.routes() // debug: list all routes
474
518
  ```
475
519
 
476
- ## Complete example
520
+ ---
521
+
522
+ ## Middleware
477
523
 
478
524
  ```ts
479
- import { serve, Router, cors, helmet, compress, logger, trace, rateLimit, postgres, redis, auth, sandbox, HttpError } from 'weifuwu'
480
- import { agent } from 'weifuwu/agent'
481
- import { react } from 'weifuwu/react'
482
- import { openai } from '@ai-sdk/openai'
525
+ import { cors, helmet, compress, rateLimit, logger, upload, serveStatic, sandbox } from 'weifuwu'
526
+
527
+ app.use(cors({ origin: '*' }))
528
+ app.use(helmet())
529
+ app.use(compress())
530
+ app.use(rateLimit({ max: 100 }))
531
+ app.use(logger({ format: 'short' }))
532
+ app.use(upload())
533
+ app.use(serveStatic('./public'))
534
+ app.use(sandbox({ baseDir: '/tmp/workspaces' }))
535
+ ```
483
536
 
484
- const app = new Router()
485
- .use(trace())
486
- .use(logger())
487
- .use(cors())
488
- .use(helmet())
489
- .use(compress())
490
- .use(postgres())
491
- .use(redis())
492
- .use(auth({ jwt: { secret: process.env.JWT_SECRET! } }))
493
- .use(sandbox({ isolateBy: 'user' }))
494
- .use(agent({
495
- model: openai('gpt-4o'),
496
- system: 'You are a helpful assistant.',
497
- knowledge: {
498
- search: async (q, ctx) => ctx.sql`...`,
499
- },
500
- sandbox: true,
501
- }))
502
- .plugin(react({ pages: { '/': './Chat.tsx' }, layout: './Layout.tsx', tailwind: {} }))
537
+ ---
503
538
 
504
- app.post('/api/chat', async (req, ctx) => {
505
- if (!ctx.user) return new Response('Unauthorized', { status: 401 })
506
- const { messages } = await req.json()
507
- return ctx.agent.chatStreamResponse({ messages })
508
- })
539
+ ## Postgres
509
540
 
510
- serve(app, { port: 3000 })
541
+ ```ts
542
+ import { postgres } from 'weifuwu'
543
+
544
+ const sql = postgres()
545
+ app.use(sql) // → ctx.sql
546
+
547
+ await sql.sql`SELECT * FROM users WHERE id = ${id}`
548
+ await sql.sql.begin(async (sql) => { /* transaction */ })
549
+ ```
550
+
551
+ Reads `DATABASE_URL` env. Supports migrations, transactions, connection pool stats.
552
+
553
+ ---
554
+
555
+ ## Redis
556
+
557
+ ```ts
558
+ import { redis } from 'weifuwu'
559
+
560
+ const r = redis()
561
+ app.use(r) // → ctx.redis
562
+ await r.redis.set('key', 'value')
563
+ ```
564
+
565
+ Reads `REDIS_URL` env (default: `redis://localhost:6379`).
566
+
567
+ ---
568
+
569
+ ## Queue & Cron
570
+
571
+ ```ts
572
+ import { queue } from 'weifuwu'
573
+
574
+ const q = queue()
575
+ app.use(q) // → ctx.queue
576
+
577
+ q.process('email', async (job) => { await sendEmail(job.payload) })
578
+ q.cron('cleanup', '0 3 * * *', () => cleanup())
579
+ await q.add('email', { to: 'user@example.com' })
580
+ await q.add('remind', {}, { delay: 60_000 })
581
+ q.run()
511
582
  ```
512
583
 
584
+ ---
585
+
513
586
  ## Project Structure
514
587
 
515
588
  ```
516
- weifuwu/
517
- ├── package.json
518
- ├── tsconfig.json
519
- ├── docker-compose.yml postgres + redis for tests
520
- ├── scripts/
521
- ├── build.mjs
522
- │ └── release.mjs
523
- ├── src/
524
- ├── index.ts
525
- ├── types.ts
526
- ├── core/ serve, router, ws, trace, logger
527
- ├── middleware/ cors, helmet, compress, rate-limit, static, upload
528
- ├── ai/ AI + agent middleware
529
- ├── postgres/
530
- ├── redis/
531
- ├── react/ react SSR (render, navigation, client)
532
- │ ├── queue/ cron, index, types
533
- │ ├── graphql.ts
534
- │ ├── hub.ts
535
- │ └── test/ ← 131 tests (18 files)
536
- ├── examples/
537
- │ └── react-ssr/ ← full SPA demo
538
- └── dist/
589
+ src/
590
+ ├── index.ts ← Entry, exports all modules
591
+ ├── types.ts ← Context, Handler, Middleware types
592
+ ├── core/ serve, router, ws, trace, logger
593
+ ├── middleware/ ← cors, helmet, compress, rate-limit, upload, static, sandbox
594
+ ├── user/ ← User system (CRUD, JWT, requireRole)
595
+ ├── messager/ ← IM + AI conversation layer
596
+ ├── kb/ ← RAG knowledge base (chunking, embedding, vector search)
597
+ ├── ai/ ← AI Agent (LLM, tools, RAG)
598
+ ├── cms/ ← Content management (blog, docs, changelog)
599
+ ├── base/ Dynamic data engine (Fixed Slot)
600
+ ├── postgres/ PostgreSQL client
601
+ ├── redis/ Redis client
602
+ ├── queue/ ← Job queue + cron
603
+ ├── graphql.ts ← GraphQL
604
+ ├── hub.ts WebSocket hub
605
+ └── test/ 281 tests
606
+
607
+ docker-compose.yml ← postgres (pgvector) + redis
539
608
  ```
540
609
 
610
+ ---
611
+
541
612
  ## Development
542
613
 
543
614
  ```bash
544
- npm run build # esbuild dist/index.js
545
- npm run typecheck # tsc --noEmit
546
- npm test # 131 tests (requires docker compose)
615
+ docker compose up -d # Start postgres + redis
616
+ npm run build # esbuild → dist/
617
+ npm run typecheck # tsc --noEmit
618
+ npm test # 281 tests
547
619
  ```