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 +468 -397
- package/dist/base/index.d.ts +59 -0
- package/dist/base/module.d.ts +42 -0
- package/dist/base/types.d.ts +116 -0
- package/dist/cms/index.d.ts +67 -0
- package/dist/cms/module.d.ts +38 -0
- package/dist/cms/types.d.ts +96 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.js +2239 -90
- package/dist/kb/index.d.ts +57 -0
- package/dist/kb/module.d.ts +37 -0
- package/dist/kb/types.d.ts +83 -0
- package/dist/messager/index.d.ts +72 -0
- package/dist/messager/module.d.ts +46 -0
- package/dist/messager/types.d.ts +99 -0
- package/dist/types.d.ts +2 -2
- package/dist/user/index.d.ts +61 -0
- package/dist/user/module.d.ts +64 -0
- package/dist/user/types.d.ts +98 -0
- package/package.json +1 -1
- package/dist/middleware/auth.d.ts +0 -70
package/README.md
CHANGED
|
@@ -1,325 +1,309 @@
|
|
|
1
1
|
# weifuwu
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
29
|
+
app.post('/api/chat', async (req, ctx) => {
|
|
30
|
+
const { messages } = await req.json()
|
|
31
|
+
return ctx.agent.chatStreamResponse({ messages })
|
|
32
|
+
})
|
|
20
33
|
|
|
21
|
-
|
|
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
|
-
###
|
|
37
|
+
### 环境变量
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
app.ws(path, ...middlewares, handler)
|
|
40
|
-
// handler: { open?, message?, close?, error? }
|
|
50
|
+
## Modules
|
|
41
51
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
|
59
|
-
|
|
60
|
-
| `cors(
|
|
61
|
-
| `helmet(
|
|
62
|
-
| `compress(
|
|
63
|
-
| `rateLimit(
|
|
64
|
-
| `logger(
|
|
65
|
-
| `upload(
|
|
66
|
-
| `serveStatic(
|
|
67
|
-
| `sandbox(
|
|
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
|
-
|
|
83
|
-
import { postgres, MIGRATIONS_TABLE } from 'weifuwu'
|
|
74
|
+
### Core
|
|
84
75
|
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
87
|
+
### Utilities
|
|
89
88
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
})
|
|
94
|
-
```
|
|
89
|
+
| Import | Purpose |
|
|
90
|
+
|--------|---------|
|
|
91
|
+
| `requireRole('admin')` | Middleware: check `ctx.user.role` |
|
|
95
92
|
|
|
96
|
-
|
|
93
|
+
---
|
|
97
94
|
|
|
98
|
-
|
|
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
|
-
|
|
97
|
+
身份认证、注册登录、JWT、密码管理。
|
|
107
98
|
|
|
108
99
|
```ts
|
|
109
|
-
import {
|
|
100
|
+
import { user, requireRole } from 'weifuwu'
|
|
110
101
|
|
|
111
|
-
|
|
112
|
-
app.use(
|
|
102
|
+
app.use(postgres())
|
|
103
|
+
app.use(user({ secret: process.env.JWT_SECRET }))
|
|
113
104
|
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
119
|
-
await
|
|
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
|
-
|
|
123
|
-
await q.add('remind', {}, { delay: 60_000 })
|
|
124
|
-
await q.add('report', {}, { schedule: '0 9 * * 1' })
|
|
128
|
+
### ctx.userModule API
|
|
125
129
|
|
|
126
|
-
|
|
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
|
-
###
|
|
144
|
+
### ctx.user
|
|
130
145
|
|
|
131
|
-
|
|
132
|
-
import { createHub } from 'weifuwu'
|
|
146
|
+
由中间件自动从 `Authorization: Bearer` 或 `token` cookie 解析。
|
|
133
147
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
###
|
|
158
|
+
### requireRole
|
|
147
159
|
|
|
148
160
|
```ts
|
|
149
|
-
|
|
161
|
+
app.get('/admin', requireRole('admin'), handler)
|
|
162
|
+
// 未登录 → 401,角色不匹配 → 403
|
|
163
|
+
```
|
|
150
164
|
|
|
151
|
-
|
|
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
|
-
|
|
172
|
-
|
|
167
|
+
- 密码:scrypt + 随机 32 字节盐
|
|
168
|
+
- 令牌:HMAC SHA-256,7 天过期
|
|
173
169
|
|
|
174
|
-
|
|
170
|
+
---
|
|
175
171
|
|
|
176
|
-
|
|
172
|
+
## messager
|
|
177
173
|
|
|
178
|
-
|
|
179
|
-
npm install react react-dom
|
|
180
|
-
```
|
|
174
|
+
即时消息 + AI 对话交互层。单聊、群聊、消息持久化、WebSocket 实时推送。
|
|
181
175
|
|
|
182
176
|
```ts
|
|
183
|
-
|
|
184
|
-
import { serve, Router } from 'weifuwu'
|
|
185
|
-
import { react } from 'weifuwu/react'
|
|
177
|
+
import { messager } from 'weifuwu'
|
|
186
178
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
208
|
+
### ctx.messager API
|
|
206
209
|
|
|
207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
+
---
|
|
239
234
|
|
|
240
|
-
|
|
241
|
-
// layouts/Root.tsx
|
|
242
|
-
export async function loader(ctx: Context) {
|
|
243
|
-
return { currentUser: await getCurrentUser(ctx) }
|
|
244
|
-
}
|
|
235
|
+
## kb
|
|
245
236
|
|
|
246
|
-
|
|
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
|
-
|
|
239
|
+
```ts
|
|
240
|
+
import { kb } from 'weifuwu'
|
|
262
241
|
|
|
263
|
-
|
|
242
|
+
app.use(postgres())
|
|
243
|
+
app.use(kb())
|
|
264
244
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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
|
-
|
|
259
|
+
### ctx.kb API
|
|
274
260
|
|
|
275
|
-
|
|
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
|
-
|
|
278
|
-
import { Suspense, use } from 'react'
|
|
271
|
+
### 配置
|
|
279
272
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
297
|
+
### 存储
|
|
298
|
+
|
|
299
|
+
- `kb_documents` — 原始文档元数据
|
|
300
|
+
- `kb_chunks` — 分片内容 + VECTOR(1536) + TSVECTOR GIN 索引
|
|
316
301
|
|
|
317
|
-
|
|
302
|
+
---
|
|
318
303
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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('
|
|
315
|
+
model: openai('deepseek-v4-flash', { baseURL: 'https://api.deepseek.com/v1' }),
|
|
332
316
|
system: 'You are a helpful assistant.',
|
|
333
317
|
knowledge: {
|
|
334
|
-
|
|
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
|
-
|
|
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
|
-
| `
|
|
364
|
-
| `
|
|
365
|
-
| `
|
|
366
|
-
| `
|
|
367
|
-
| `agents` |
|
|
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
|
-
|
|
367
|
+
---
|
|
372
368
|
|
|
373
|
-
|
|
369
|
+
## cms
|
|
374
370
|
|
|
375
|
-
|
|
376
|
-
import { auth } from 'weifuwu'
|
|
371
|
+
内容管理 — 博客、文档、公告、更新日志。
|
|
377
372
|
|
|
378
|
-
|
|
379
|
-
|
|
373
|
+
```ts
|
|
374
|
+
import { cms, requireRole } from 'weifuwu'
|
|
380
375
|
|
|
381
|
-
|
|
382
|
-
app.use(
|
|
383
|
-
|
|
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
|
-
//
|
|
390
|
-
app.
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
//
|
|
396
|
-
app.
|
|
397
|
-
|
|
398
|
-
return Response.json(
|
|
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
|
-
|
|
402
|
+
### ctx.cms API
|
|
403
403
|
|
|
404
|
-
|
|
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
|
-
|
|
407
|
-
import { sandbox } from 'weifuwu'
|
|
417
|
+
### Features
|
|
408
418
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
419
|
+
- 内容类型:post / page / doc / changelog(任意字符串)
|
|
420
|
+
- 状态:draft / published / archived
|
|
421
|
+
- Slug:自动生成,同类型唯一
|
|
422
|
+
- 标签:多对多,自动创建
|
|
423
|
+
- 树形:parent_id 支持层级
|
|
424
|
+
- 鉴权:非管理员只能读已发布
|
|
414
425
|
|
|
415
|
-
|
|
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
|
-
|
|
428
|
+
## base
|
|
423
429
|
|
|
424
|
-
|
|
430
|
+
动态数据存储引擎 — 让用户自定义数据结构(类似 Airtable)。
|
|
425
431
|
|
|
426
|
-
|
|
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
|
-
|
|
435
|
+
app.use(postgres())
|
|
436
|
+
app.use(user())
|
|
437
|
+
app.use(base())
|
|
438
438
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
//
|
|
447
|
-
app.
|
|
448
|
-
const
|
|
449
|
-
return Response.json(
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
## Middleware
|
|
477
521
|
|
|
478
522
|
```ts
|
|
479
|
-
import {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
517
|
-
├──
|
|
518
|
-
├──
|
|
519
|
-
├──
|
|
520
|
-
├──
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
├──
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
545
|
-
npm run
|
|
546
|
-
npm
|
|
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
|
```
|