weifuwu 0.32.0 → 0.33.0

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 框架** — `(req, ctx) => Response`
4
4
 
5
5
  ```bash
6
6
  npm install weifuwu
7
7
  ```
8
8
 
9
+ 用户系统、即时消息、RAG 知识库、AI Agent、内容管理、动态数据存储。配好环境变量就能跑。
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
+ ### 环境变量
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()` | 注册、登录、JWT、角色 |
55
+ | Messager | `messager()` | `postgres()`, `user()` | 即时消息、AI 对话交互层 |
56
+ | KB | `kb()` | `postgres()` | RAG 知识库、分片、向量搜索 |
57
+ | Agent | `agent()` | | LLM 对话、工具调用、流式输出 |
58
+ | CMS | `cms()` | `postgres()`, `user()` | 博客、文档、公告 |
59
+ | Base | `base()` | `postgres()`, `user()` | 动态数据存储引擎 |
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
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 |
81
73
 
82
- ```ts
83
- import { postgres, MIGRATIONS_TABLE } from 'weifuwu'
74
+ ### Core
84
75
 
85
- const sql = postgres({ url: process.env.DATABASE_URL })
86
- app.use(sql) // injects ctx.sql
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 |
87
86
 
88
- const rows = await sql.sql`SELECT * FROM users WHERE id = ${id}`
87
+ ### Utilities
89
88
 
90
- // Migrations
91
- await sql.migrate({
92
- '001_init': async (s) => await s`CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)`,
93
- })
94
- ```
89
+ | Import | Purpose |
90
+ |--------|---------|
91
+ | `requireRole('admin')` | Middleware: check `ctx.user.role` |
95
92
 
96
- ### Redis
93
+ ---
97
94
 
98
- ```ts
99
- import { redis } from 'weifuwu'
100
-
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
+ 身份认证、注册登录、JWT、密码管理。
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
+ // 注册
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
+ // 登录
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
+ // 当前用户
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
+ // 仅管理员
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 }` | 注册 |
133
+ | `login(email, pw)` | `{ user, token } \| null` | 登录 |
134
+ | `getUserById(id)` | `UserRecord \| null` | 查用户 |
135
+ | `getUserByEmail(email)` | `UserRecord \| null` | 查邮箱 |
136
+ | `updateUser(id, input)` | `UserRecord \| null` | 更新 |
137
+ | `changePassword(id, oldPw, newPw)` | `boolean` | 改密码 |
138
+ | `deleteUser(id)` | `boolean` | 软删除 |
139
+ | `listUsers(inactive?)` | `UserRecord[]` | 用户列表 |
140
+ | `generateToken(user)` | `string` | 签发 JWT |
141
+ | `verifyToken(token)` | `TokenPayload \| null` | 验证 JWT |
142
+ | `refreshToken(token)` | `string \| null` | 刷新 JWT |
128
143
 
129
- ### WebSocket Hub
144
+ ### ctx.user
130
145
 
131
- ```ts
132
- import { createHub } from 'weifuwu'
146
+ 由中间件自动从 `Authorization: Bearer` 或 `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 // 'user' | 'admin' | ...
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
+ // 未登录 → 401,角色不匹配 → 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
+ ### 安全
170
166
 
171
- app.mount('/graphql', gql)
172
- ```
167
+ - 密码:scrypt + 随机 32 字节盐
168
+ - 令牌:HMAC SHA-256,7 天过期
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
+ 即时消息 + AI 对话交互层。单聊、群聊、消息持久化、WebSocket 实时推送。
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 自动加入用户的所有会话
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` | 创建/复用私聊 |
213
+ | `createGroupConversation(title, userIds)` | `Conversation` | 建群 |
214
+ | `sendMessage(convId, body)` | `Message` | 发消息,自动广播到 `conversation:{id}` 房间 |
215
+ | `getMessages(convId, opts?)` | `Message[]` | 游标分页 |
216
+ | `editMessage(msgId, body)` | `Message \| null` | 编辑(24h 内) |
217
+ | `deleteMessage(msgId)` | `boolean` | 软删除 |
218
+ | `getConversations()` | `Conversation[]` | 会话列表(含未读、最后消息预览) |
219
+ | `getConversation(id)` | `Conversation \| null` | 会话详情 |
220
+ | `markRead(convId)` | `void` | 标记已读 |
221
+ | `getUnreadCount()` | `{ total, byConversation }` | 未读统计 |
222
+ | `addParticipants(convId, userIds)` | `void` | 加人 |
223
+ | `removeParticipant(convId, userId?)` | `boolean` | 退出/踢出 |
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
+ ### 存储
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 张表:`conversations` / `participants` / `messages`。自动建表迁移。
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 对话
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 基础架构。messager 管会话和推送,agent LLM 生成。
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 知识库。文档导入 自动分片 DashScope embedding pgvector 存储 → 语义搜索。
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
+ // 导入
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
+ // 搜索
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 }` | 导入 → 分片 → embedding → 存储 |
264
+ | `importDocuments(docs)` | `Document[]` | 批量导入 |
265
+ | `search(query, opts?)` | `SearchResult[]` | 语义搜索(cosine 相似度) |
266
+ | `list()` | `Document[]` | 文档列表 |
267
+ | `get(id)` | `Document \| null` | 文档详情 |
268
+ | `getChunks(documentId)` | `Chunk[]` | 分片列表 |
269
+ | `delete(id)` | `boolean` | 删除文档 + 级联删除分片 |
276
270
 
277
- ```tsx
278
- import { Suspense, use } from 'react'
271
+ ### 配置
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
+ // 默认:DashScope text-embedding-v4(环境变量 DASHSCOPE_API_KEY)
275
+ app.use(kb())
276
+
277
+ // 自定义 embedding
278
+ app.use(kb({
279
+ embed: async (text) => { /* 返回 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
+ ### 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
+ ### 存储
298
+
299
+ - `kb_documents` — 原始文档元数据
300
+ - `kb_chunks` — 分片内容 + VECTOR(1536) + TSVECTOR GIN 索引
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 对话、工具调用、RAG、流式输出。
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,193 +327,292 @@ app.use(agent({
350
327
  maxSteps: 5,
351
328
  }))
352
329
 
330
+ // 流式对话
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
+ // 非流式对话
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?)` | 非流式对话,返回文本 |
349
+ | `chatStreamResponse({ messages })` | SSE 流式回复(兼容 `useChat`) |
350
+
351
+ ### 默认模型
352
+
353
+ - LLM: DeepSeek-V4-Flash(`@ai-sdk/openai` + `baseURL: 'https://api.deepseek.com/v1'`)
354
+ - 环境变量 `DEEPSEEK_MODEL` 可覆盖模型名称
355
+ - 环境变量 `DEEPSEEK_API_KEY` 或 `OPENAI_API_KEY` 为 API 密钥
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 回调,自动注入 system prompt |
362
+ | `tools` | 工具定义,自动循环调用(maxSteps) |
363
+ | `sandbox: true` | `ctx.sandbox` 集成(文件读写) |
364
+ | `store` | 对话持久化(save/load) |
365
+ | `agents` | Agent 编排 |
370
366
 
371
- ### Auth
367
+ ---
372
368
 
373
- ```ts
374
- import { auth } from 'weifuwu'
369
+ ## cms
375
370
 
376
- // JWT
377
- app.use(auth({ jwt: { secret: process.env.JWT_SECRET } }))
371
+ 内容管理 — 博客、文档、公告、更新日志。
378
372
 
379
- // Session cookie
380
- app.use(auth({
381
- session: {
382
- secret: '...',
383
- loadUser: async (data) => db.findUser(data.userId),
384
- },
385
- }))
373
+ ```ts
374
+ import { cms, requireRole } from 'weifuwu'
386
375
 
387
- // API key
388
- app.use(auth({ apiKey: { validate: async (key) => db.findByApiKey(key) } }))
376
+ app.use(postgres())
377
+ app.use(user())
378
+ app.use(cms())
389
379
 
390
- // ctx.user is now available
391
- app.get('/me', (req, ctx) => {
392
- if (!ctx.user) return new Response('Unauthorized', { status: 401 })
393
- return Response.json(ctx.user)
380
+ // 公开:已发布的文章
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
+ })
389
+
390
+ // 管理端
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)
394
399
  })
395
400
  ```
396
401
 
397
- 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
398
403
 
399
- ### Sandbox
404
+ | Method | Returns | Description |
405
+ |--------|---------|-------------|
406
+ | `create(input)` | `Content` | 创建(admin) |
407
+ | `get(slug)` | `Content \| null` | 按 slug 获取 |
408
+ | `getById(id)` | `Content \| null` | 按 id 获取 |
409
+ | `update(id, input)` | `Content \| null` | 更新(admin) |
410
+ | `delete(id)` | `boolean` | 删除(admin) |
411
+ | `list(opts?)` | `Content[]` | 列表(游标分页) |
412
+ | `publish(id)` | `Content \| null` | 发布(admin) |
413
+ | `unpublish(id)` | `Content \| null` | 下线(admin) |
414
+ | `listTags()` | `TagWithCount[]` | 标签列表 |
415
+ | `createTag(name)` | `Tag` | 创建标签 |
400
416
 
401
- ```ts
402
- import { sandbox } from 'weifuwu'
417
+ ### Features
403
418
 
404
- app.use(sandbox({
405
- baseDir: '/tmp/workspaces',
406
- timeout: 30000,
407
- isolateBy: 'user', // one directory per ctx.user.id
408
- }))
419
+ - 内容类型:post / page / doc / changelog(任意字符串)
420
+ - 状态:draft / published / archived
421
+ - Slug:自动生成,同类型唯一
422
+ - 标签:多对多,自动创建
423
+ - 树形:parent_id 支持层级
424
+ - 鉴权:非管理员只能读已发布
409
425
 
410
- // ctx.sandbox provides isolated file + exec operations
411
- await ctx.sandbox.writeFile('hello.txt', 'world')
412
- const content = await ctx.sandbox.readFile('hello.txt')
413
- const { stdout } = await ctx.sandbox.exec('ls -la')
414
- await ctx.sandbox.destroy() // clean up workspace
415
- ```
426
+ ---
416
427
 
417
- 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
418
429
 
419
- ### Types
430
+ 动态数据存储引擎 — 让用户自定义数据结构(类似 Airtable)。
420
431
 
421
- | Export | Description |
422
- |---|---|
423
- | `Context` | `{ params, query, mountPath?, [key: string] }` |
424
- | `Handler<T>` | `(req: Request, ctx: T) => Response \| Promise<Response>` |
425
- | `Middleware` | `(req, ctx, next) => Response \| Promise<Response>` |
426
- | `WebSocketHandler` | `{ open?, message?, close?, error? }` |
427
- | `HttpError` | `Error` subclass with `.status: number` |
428
- | `Closeable` | `{ close(): Promise<void> }` |
429
- | `Server` | `{ close, port, hostname, ready }` |
430
- | `ServeOptions` | `{ port, hostname, signal, maxBodySize, timeout, keepAliveTimeout, headersTimeout, shutdown }` |
432
+ ```ts
433
+ import { base } from 'weifuwu'
431
434
 
432
- ## Handler / Middleware patterns
435
+ app.use(postgres())
436
+ app.use(user())
437
+ app.use(base())
433
438
 
434
- ```ts
435
- // Handler standard Web API
436
- app.get('/api/data', (req, ctx) => {
437
- const q = ctx.query.q
438
- return Response.json({ q })
439
+ // 定义数据结构
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 })
439
443
  })
440
444
 
441
- // Async
442
- app.get('/db/users', async (req, ctx) => {
443
- const rows = await ctx.sql`SELECT * FROM users`
444
- return Response.json(rows)
445
+ // 增删改查
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 })
445
449
  })
446
450
 
447
- // Throwing HttpError
448
- app.get('/item/:id', (req, ctx) => {
449
- const item = db.get(ctx.params.id)
450
- if (!item) throw new HttpError('Not found', 404)
451
- 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
+ }))
452
457
  })
458
+ ```
453
459
 
454
- // Middleware
455
- app.use(async (req, ctx, next) => {
456
- const start = Date.now()
457
- const res = await next(req, ctx)
458
- console.log(`${req.method} ${req.url} ${Date.now() - start}ms`)
459
- return res
460
- })
460
+ ### ctx.base API
461
461
 
462
- // Error handler
463
- app.onError((err, req, ctx) => {
464
- if (err instanceof HttpError) {
465
- return Response.json({ error: err.message }, { status: err.status })
466
- }
467
- return new Response('Internal error', { status: 500 })
468
- })
462
+ | Method | Returns | Description |
463
+ |--------|---------|-------------|
464
+ | `create({ name, tables })` | `BaseDef` | 创建数据库 |
465
+ | `defineTable(baseId, schema)` | `BaseDef` | 加表 |
466
+ | `updateTable(baseId, name, schema)` | `BaseDef \| null` | 改表 |
467
+ | `removeTable(baseId, name)` | `BaseDef \| null` | 删表 |
468
+ | `insert(baseId, table, data)` | `Row` | 插入行 |
469
+ | `getRow(baseId, table, id)` | `Row \| null` | 查行 |
470
+ | `updateRow(baseId, table, id, data)` | `Row \| null` | 改行 |
471
+ | `deleteRow(baseId, table, id)` | `boolean` | 删行 |
472
+ | `query(baseId, table, opts?)` | `Row[]` | 查询(filter/sort/limit/offset) |
473
+ | `search(baseId, table, field, query)` | `Row[]` | 全文搜索 |
474
+ | `similaritySearch(baseId, table, field, vector)` | `Row[]` | 向量搜索 |
475
+ | `list()` / `get(id)` / `getBySlug(slug)` / `delete(id)` | — | 数据库管理 |
476
+
477
+ ### 架构
478
+
479
+ Fixed Slot:一张 `base_data` 表,预分配 ~120 个物理列:
480
+
481
+ | 类型 | 列数 | PG 类型 |
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(溢出兜底) |
489
+
490
+ 字段名 → 物理列号 的映射存储在 `base_column_map` 表。超出物理列的字段自动溢出到 ext JSONB。
491
+
492
+ pgvector 自动检测:docker 镜像默认支持。
493
+
494
+ ---
495
+
496
+ ## Router
497
+
498
+ ```ts
499
+ const app = new Router()
500
+
501
+ // HTTP
502
+ app.get(path, ...handlers)
503
+ app.post / put / delete / patch / head / options(path, ...handlers)
504
+ app.all(path, ...handlers)
505
+
506
+ // WebSocket
507
+ app.ws(path, ...middlewares, handler)
508
+ // handler: { open?, message?, close?, error? }
509
+
510
+ // 中间件 & 挂载
511
+ app.use(middleware)
512
+ app.mount(prefix, router)
513
+ app.plugin(fn)
514
+ app.onError(handler)
515
+ app.routes() // 调试:列出所有路由
469
516
  ```
470
517
 
471
- ## Complete example
518
+ ---
519
+
520
+ ## Middleware
472
521
 
473
522
  ```ts
474
- import { serve, Router, cors, helmet, compress, logger, trace, rateLimit, postgres, redis, auth, sandbox, HttpError } from 'weifuwu'
475
- import { agent } from 'weifuwu/agent'
476
- import { react } from 'weifuwu/react'
477
- import { openai } from '@ai-sdk/openai'
523
+ import { cors, helmet, compress, rateLimit, logger, upload, serveStatic, sandbox } from 'weifuwu'
524
+
525
+ app.use(cors({ origin: '*' }))
526
+ app.use(helmet())
527
+ app.use(compress())
528
+ app.use(rateLimit({ max: 100 }))
529
+ app.use(logger({ format: 'short' }))
530
+ app.use(upload())
531
+ app.use(serveStatic('./public'))
532
+ app.use(sandbox({ baseDir: '/tmp/workspaces' }))
533
+ ```
478
534
 
479
- const app = new Router()
480
- .use(trace())
481
- .use(logger())
482
- .use(cors())
483
- .use(helmet())
484
- .use(compress())
485
- .use(postgres())
486
- .use(redis())
487
- .use(auth({ jwt: { secret: process.env.JWT_SECRET! } }))
488
- .use(sandbox({ isolateBy: 'user' }))
489
- .use(agent({
490
- model: openai('gpt-4o'),
491
- system: 'You are a helpful assistant.',
492
- knowledge: {
493
- search: async (q, ctx) => ctx.sql`...`,
494
- },
495
- sandbox: true,
496
- }))
497
- .plugin(react({ pages: { '/': './Chat.tsx' }, layout: './Layout.tsx', tailwind: {} }))
535
+ ---
498
536
 
499
- app.post('/api/chat', async (req, ctx) => {
500
- if (!ctx.user) return new Response('Unauthorized', { status: 401 })
501
- const { messages } = await req.json()
502
- return ctx.agent.chatStreamResponse({ messages })
503
- })
537
+ ## Postgres
504
538
 
505
- serve(app, { port: 3000 })
539
+ ```ts
540
+ import { postgres } from 'weifuwu'
541
+
542
+ const sql = postgres()
543
+ app.use(sql) // → ctx.sql
544
+
545
+ await sql.sql`SELECT * FROM users WHERE id = ${id}`
546
+ await sql.sql.begin(async (sql) => { /* transaction */ })
506
547
  ```
507
548
 
549
+ `DATABASE_URL` 环境变量。支持迁移、事务、连接池统计。
550
+
551
+ ---
552
+
553
+ ## Redis
554
+
555
+ ```ts
556
+ import { redis } from 'weifuwu'
557
+
558
+ const r = redis()
559
+ app.use(r) // → ctx.redis
560
+ await r.redis.set('key', 'value')
561
+ ```
562
+
563
+ `REDIS_URL` 环境变量,默认 `redis://localhost:6379`。
564
+
565
+ ---
566
+
567
+ ## Queue & Cron
568
+
569
+ ```ts
570
+ import { queue } from 'weifuwu'
571
+
572
+ const q = queue()
573
+ app.use(q) // → ctx.queue
574
+
575
+ q.process('email', async (job) => { await sendEmail(job.payload) })
576
+ q.cron('cleanup', '0 3 * * *', () => cleanup())
577
+ await q.add('email', { to: 'user@example.com' })
578
+ await q.add('remind', {}, { delay: 60_000 })
579
+ q.run()
580
+ ```
581
+
582
+ ---
583
+
508
584
  ## Project Structure
509
585
 
510
586
  ```
511
- weifuwu/
512
- ├── package.json
513
- ├── tsconfig.json
514
- ├── docker-compose.yml postgres + redis for tests
515
- ├── scripts/
516
- ├── build.mjs
517
- │ └── release.mjs
518
- ├── src/
519
- ├── index.ts
520
- ├── types.ts
521
- ├── core/ serve, router, ws, trace, logger
522
- ├── middleware/ cors, helmet, compress, rate-limit, static, upload
523
- ├── ai/ AI + agent middleware
524
- ├── postgres/
525
- ├── redis/
526
- ├── react/ react SSR (render, navigation, client)
527
- ├── queue/ cron, index, types
528
- │ ├── graphql.ts
529
- │ ├── hub.ts
530
- │ └── test/ 131 tests (18 files)
531
- ├── examples/
532
- │ └── react-ssr/ ← full SPA demo
533
- └── dist/
587
+ src/
588
+ ├── index.ts ← 入口,导出所有模块
589
+ ├── types.ts ← Context, Handler, Middleware 定义
590
+ ├── core/ serve, router, ws, trace, logger
591
+ ├── middleware/ ← cors, helmet, compress, rate-limit, upload, static, sandbox
592
+ ├── user/ ← 用户系统(CRUD、JWT、requireRole)
593
+ ├── messager/ ← 即时消息 + AI 对话交互层
594
+ ├── kb/ ← RAG 知识库(分片、embedding、向量搜索)
595
+ ├── ai/ ← AI Agent(LLM、tools、RAG)
596
+ ├── cms/ ← 内容管理(博客、文档、公告)
597
+ ├── base/ 动态数据存储引擎(Fixed Slot)
598
+ ├── postgres/ PostgreSQL 客户端
599
+ ├── redis/ Redis 客户端
600
+ ├── queue/ ← 任务队列 + cron
601
+ ├── react/ ← React SSR
602
+ ├── graphql.ts GraphQL
603
+ ├── hub.ts WebSocket hub
604
+ └── test/ ← 281 tests
605
+
606
+ docker-compose.yml postgres (pgvector) + redis
534
607
  ```
535
608
 
609
+ ---
610
+
536
611
  ## Development
537
612
 
538
613
  ```bash
539
- npm run build # esbuild dist/index.js
540
- npm run typecheck # tsc --noEmit
541
- npm test # 131 tests (requires docker compose)
614
+ docker compose up -d # 启动 postgres + redis
615
+ npm run build # esbuild → dist/
616
+ npm run typecheck # tsc --noEmit
617
+ npm test # 281 tests
542
618
  ```