weifuwu 0.32.1 → 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,198 +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
- 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
+ 内容管理 — 博客、文档、公告、更新日志。
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
+ // 公开:已发布的文章
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
+ // 管理端
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` | 创建(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` | 创建标签 |
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
+ - 内容类型:post / page / doc / changelog(任意字符串)
420
+ - 状态:draft / published / archived
421
+ - Slug:自动生成,同类型唯一
422
+ - 标签:多对多,自动创建
423
+ - 树形:parent_id 支持层级
424
+ - 鉴权:非管理员只能读已发布
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
+ 动态数据存储引擎 — 让用户自定义数据结构(类似 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
+ // 定义数据结构
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
+ // 增删改查
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` | 创建数据库 |
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() // 调试:列出所有路由
474
516
  ```
475
517
 
476
- ## Complete example
518
+ ---
519
+
520
+ ## Middleware
477
521
 
478
522
  ```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'
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
+ ```
483
534
 
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: {} }))
535
+ ---
503
536
 
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
- })
537
+ ## Postgres
509
538
 
510
- 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 */ })
511
547
  ```
512
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
+
513
584
  ## Project Structure
514
585
 
515
586
  ```
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/
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
539
607
  ```
540
608
 
609
+ ---
610
+
541
611
  ## Development
542
612
 
543
613
  ```bash
544
- npm run build # esbuild dist/index.js
545
- npm run typecheck # tsc --noEmit
546
- 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
547
618
  ```