weifuwu 0.32.1 → 0.33.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +469 -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 -5
- package/dist/index.js +2260 -559
- 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/middleware/esbuild-dev.d.ts +1 -15
- package/dist/middleware/esbuild-dev.js +17 -86
- package/dist/types.d.ts +2 -3
- 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 -21
- package/dist/middleware/auth.d.ts +0 -70
- package/dist/react/client.d.ts +0 -76
- package/dist/react/client.js +0 -182
- package/dist/react/compile.d.ts +0 -17
- package/dist/react/context.d.ts +0 -2
- package/dist/react/error-boundary.d.ts +0 -30
- package/dist/react/hooks.d.ts +0 -6
- package/dist/react/index.d.ts +0 -92
- package/dist/react/index.js +0 -910
- package/dist/react/types.d.ts +0 -146
package/README.md
CHANGED
|
@@ -1,325 +1,309 @@
|
|
|
1
1
|
# weifuwu
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**AI SaaS framework** — `(req, ctx) => Response`
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install weifuwu
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
User system, instant messaging, RAG knowledge base, AI Agent, CMS, dynamic data storage. Configure environment variables and go.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
9
15
|
```ts
|
|
10
|
-
import { serve, Router } from 'weifuwu'
|
|
16
|
+
import { serve, Router, postgres, user, kb, agent, messager } from 'weifuwu'
|
|
17
|
+
import { openai } from '@ai-sdk/openai'
|
|
11
18
|
|
|
12
19
|
const app = new Router()
|
|
13
|
-
app.
|
|
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
|
+
### Environment Variables
|
|
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()` | Auth, JWT, roles |
|
|
55
|
+
| Messager | `messager()` | `postgres()`, `user()` | IM + AI conversation layer |
|
|
56
|
+
| KB | `kb()` | `postgres()` | RAG knowledge base |
|
|
57
|
+
| Agent | `agent()` | — | LLM chat, tool calling, streaming |
|
|
58
|
+
| CMS | `cms()` | `postgres()`, `user()` | Blog, docs, changelog |
|
|
59
|
+
| Base | `base()` | `postgres()`, `user()` | Dynamic data engine |
|
|
55
60
|
|
|
56
61
|
### Middleware
|
|
57
62
|
|
|
58
|
-
|
|
|
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
|
|
81
|
-
|
|
82
|
-
```ts
|
|
83
|
-
import { postgres, MIGRATIONS_TABLE } from 'weifuwu'
|
|
63
|
+
| Import | Purpose |
|
|
64
|
+
|--------|---------|
|
|
65
|
+
| `cors()` | CORS headers |
|
|
66
|
+
| `helmet()` | Security headers |
|
|
67
|
+
| `compress()` | gzip / brotli / deflate |
|
|
68
|
+
| `rateLimit()` | Sliding-window rate limiter |
|
|
69
|
+
| `logger()` | Request logging |
|
|
70
|
+
| `upload()` | Multipart file upload |
|
|
71
|
+
| `serveStatic()` | Static files |
|
|
72
|
+
| `sandbox()` | Filesystem isolation |
|
|
84
73
|
|
|
85
|
-
|
|
86
|
-
app.use(sql) // injects ctx.sql
|
|
74
|
+
### Core
|
|
87
75
|
|
|
88
|
-
|
|
76
|
+
| Import | Purpose |
|
|
77
|
+
|--------|---------|
|
|
78
|
+
| `serve(app, opts?)` | Start HTTP server |
|
|
79
|
+
| `Router` | Trie-based router + WebSocket |
|
|
80
|
+
| `HttpError` | `throw new HttpError(msg, 404)` |
|
|
81
|
+
| `trace()` | Request tracing |
|
|
82
|
+
| `postgres()` | PostgreSQL client (`ctx.sql`) |
|
|
83
|
+
| `redis()` | Redis client (`ctx.redis`) |
|
|
84
|
+
| `queue()` | Job queue + cron |
|
|
85
|
+
| `createHub()` | WebSocket pub/sub |
|
|
89
86
|
|
|
90
|
-
|
|
91
|
-
await sql.migrate({
|
|
92
|
-
'001_init': async (s) => await s`CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)`,
|
|
93
|
-
})
|
|
94
|
-
```
|
|
87
|
+
### Utilities
|
|
95
88
|
|
|
96
|
-
|
|
89
|
+
| Import | Purpose |
|
|
90
|
+
|--------|---------|
|
|
91
|
+
| `requireRole('admin')` | Middleware: check `ctx.user.role` |
|
|
97
92
|
|
|
98
|
-
|
|
99
|
-
import { redis } from 'weifuwu'
|
|
93
|
+
---
|
|
100
94
|
|
|
101
|
-
|
|
102
|
-
app.use(r) // injects ctx.redis
|
|
103
|
-
await r.redis.set('key', 'value')
|
|
104
|
-
```
|
|
95
|
+
## user
|
|
105
96
|
|
|
106
|
-
|
|
97
|
+
Auth, registration, JWT, password management, roles.
|
|
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
|
+
// Register
|
|
106
|
+
app.post('/api/register', async (req, ctx) => {
|
|
107
|
+
const result = await ctx.userModule.register(await req.json())
|
|
108
|
+
return Response.json(result)
|
|
116
109
|
})
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
await
|
|
110
|
+
// Login
|
|
111
|
+
app.post('/api/login', async (req, ctx) => {
|
|
112
|
+
const { email, password } = await req.json()
|
|
113
|
+
const result = await ctx.userModule.login(email, password)
|
|
114
|
+
if (!result) return new Response('Unauthorized', { status: 401 })
|
|
115
|
+
return Response.json(result)
|
|
120
116
|
})
|
|
117
|
+
// Current user
|
|
118
|
+
app.get('/api/me', async (req, ctx) => {
|
|
119
|
+
if (!ctx.user) return new Response('Unauthorized', { status: 401 })
|
|
120
|
+
return Response.json(ctx.user)
|
|
121
|
+
})
|
|
122
|
+
// Admin only
|
|
123
|
+
app.get('/api/admin/users', requireRole('admin'), async (req, ctx) => {
|
|
124
|
+
return Response.json(await ctx.userModule.listUsers())
|
|
125
|
+
})
|
|
126
|
+
```
|
|
121
127
|
|
|
122
|
-
|
|
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 }` | Register |
|
|
133
|
+
| `login(email, pw)` | `{ user, token } \| null` | Login |
|
|
134
|
+
| `getUserById(id)` | `UserRecord \| null` | Get by ID |
|
|
135
|
+
| `getUserByEmail(email)` | `UserRecord \| null` | Get by email |
|
|
136
|
+
| `updateUser(id, input)` | `UserRecord \| null` | Update |
|
|
137
|
+
| `changePassword(id, oldPw, newPw)` | `boolean` | Change password |
|
|
138
|
+
| `deleteUser(id)` | `boolean` | Soft delete |
|
|
139
|
+
| `listUsers(inactive?)` | `UserRecord[]` | List users |
|
|
140
|
+
| `generateToken(user)` | `string` | Issue JWT |
|
|
141
|
+
| `verifyToken(token)` | `TokenPayload \| null` | Verify JWT |
|
|
142
|
+
| `refreshToken(token)` | `string \| null` | Refresh JWT |
|
|
128
143
|
|
|
129
|
-
###
|
|
144
|
+
### ctx.user
|
|
130
145
|
|
|
131
|
-
|
|
132
|
-
import { createHub } from 'weifuwu'
|
|
146
|
+
Auto-resolved from `Authorization: Bearer` or `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
|
|
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
|
+
// No auth → 401, wrong role → 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
|
+
### Security
|
|
170
166
|
|
|
171
|
-
|
|
172
|
-
|
|
167
|
+
- Password: scrypt + 32-byte random salt
|
|
168
|
+
- Token: HMAC SHA-256, 7 day expiry
|
|
173
169
|
|
|
174
|
-
|
|
170
|
+
---
|
|
175
171
|
|
|
176
|
-
|
|
172
|
+
## messager
|
|
177
173
|
|
|
178
|
-
|
|
179
|
-
npm install react react-dom
|
|
180
|
-
```
|
|
174
|
+
Instant messaging + AI conversation layer. Direct/group chat, message persistence, WebSocket push.
|
|
181
175
|
|
|
182
176
|
```ts
|
|
183
|
-
|
|
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 — auto-join all user conversations
|
|
184
|
+
app.ws('/ws', {
|
|
185
|
+
async open(ws, ctx) {
|
|
186
|
+
for (const c of await ctx.messager.getConversations()) {
|
|
187
|
+
ctx.ws.join(`conversation:${c.id}`)
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
// REST API
|
|
193
|
+
app.post('/api/messages', async (req, ctx) => {
|
|
194
|
+
const { conversationId, body } = await req.json()
|
|
195
|
+
const msg = await ctx.messager.sendMessage(conversationId, body)
|
|
196
|
+
return Response.json(msg, { status: 201 })
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
app.get('/api/conversations/:id/messages', async (req, ctx) => {
|
|
200
|
+
const url = new URL(req.url)
|
|
201
|
+
return Response.json(await ctx.messager.getMessages(ctx.params.id, {
|
|
202
|
+
before: url.searchParams.get('before') || undefined,
|
|
203
|
+
limit: parseInt(url.searchParams.get('limit') || '50'),
|
|
204
|
+
}))
|
|
205
|
+
})
|
|
203
206
|
```
|
|
204
207
|
|
|
205
|
-
|
|
208
|
+
### ctx.messager API
|
|
206
209
|
|
|
207
|
-
|
|
210
|
+
| Method | Returns | Description |
|
|
211
|
+
|--------|---------|-------------|
|
|
212
|
+
| `createDirectConversation(userId)` | `Conversation` | Create/reuse DM |
|
|
213
|
+
| `createGroupConversation(title, userIds)` | `Conversation` | Create group |
|
|
214
|
+
| `sendMessage(convId, body)` | `Message` | Send, auto-broadcast to `conversation:{id}` room |
|
|
215
|
+
| `getMessages(convId, opts?)` | `Message[]` | Cursor pagination |
|
|
216
|
+
| `editMessage(msgId, body)` | `Message \| null` | Edit (24h window) |
|
|
217
|
+
| `deleteMessage(msgId)` | `boolean` | Soft delete |
|
|
218
|
+
| `getConversations()` | `Conversation[]` | List with unread + last message |
|
|
219
|
+
| `getConversation(id)` | `Conversation \| null` | Get detail |
|
|
220
|
+
| `markRead(convId)` | `void` | Mark as read |
|
|
221
|
+
| `getUnreadCount()` | `{ total, byConversation }` | Unread stats |
|
|
222
|
+
| `addParticipants(convId, userIds)` | `void` | Add members |
|
|
223
|
+
| `removeParticipant(convId, userId?)` | `boolean` | Leave / kick |
|
|
208
224
|
|
|
209
|
-
|
|
210
|
-
// pages/UserDetail.tsx
|
|
211
|
-
import type { Context } from 'weifuwu'
|
|
212
|
-
import { HttpError } from 'weifuwu'
|
|
213
|
-
import { useServerData } from 'weifuwu/react'
|
|
225
|
+
### Storage
|
|
214
226
|
|
|
215
|
-
|
|
216
|
-
const user = await db.find(ctx.params.id)
|
|
217
|
-
if (!user) throw new HttpError('Not found', 404)
|
|
218
|
-
return { user }
|
|
219
|
-
}
|
|
227
|
+
3 tables: `conversations` / `participants` / `messages`. Auto-migration.
|
|
220
228
|
|
|
221
|
-
|
|
222
|
-
const { user } = useServerData<{ user: User }>()
|
|
223
|
-
return (
|
|
224
|
-
<div>
|
|
225
|
-
<title>{`${user.name} — My App`}</title>
|
|
226
|
-
<h1>{user.name}</h1>
|
|
227
|
-
<p>{user.email}</p>
|
|
228
|
-
</div>
|
|
229
|
-
)
|
|
230
|
-
}
|
|
231
|
-
```
|
|
229
|
+
### AI Conversations
|
|
232
230
|
|
|
233
|
-
|
|
234
|
-
- `throw new HttpError('Not found', 404)` — renders the NotFound page with correct status
|
|
235
|
-
- `<title>` auto-hoists to `<head>` via React 19
|
|
236
|
-
- `export default` is auto-detected; named exports work too
|
|
231
|
+
messager + agent = ChatGPT foundation. Messager handles sessions + push, agent handles LLM generation.
|
|
237
232
|
|
|
238
|
-
|
|
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 knowledge base. Import docs → auto-chunk → DashScope embedding → pgvector storage → semantic search.
|
|
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
|
+
// Import
|
|
246
|
+
app.post('/api/kb/import', async (req, ctx) => {
|
|
247
|
+
const { title, content } = await req.json()
|
|
248
|
+
const result = await ctx.kb.importText(title, content)
|
|
249
|
+
return Response.json(result, { status: 201 })
|
|
250
|
+
})
|
|
270
251
|
|
|
271
|
-
|
|
252
|
+
// Search
|
|
253
|
+
app.post('/api/kb/search', async (req, ctx) => {
|
|
254
|
+
const { query } = await req.json()
|
|
255
|
+
return Response.json(await ctx.kb.search(query, { limit: 5 }))
|
|
256
|
+
})
|
|
257
|
+
```
|
|
272
258
|
|
|
273
|
-
|
|
259
|
+
### ctx.kb API
|
|
274
260
|
|
|
275
|
-
|
|
261
|
+
| Method | Returns | Description |
|
|
262
|
+
|--------|---------|-------------|
|
|
263
|
+
| `importText(title, text, opts?)` | `{ document, chunks }` | Import → chunk → embed → store |
|
|
264
|
+
| `importDocuments(docs)` | `Document[]` | Batch import |
|
|
265
|
+
| `search(query, opts?)` | `SearchResult[]` | Semantic search (cosine) |
|
|
266
|
+
| `list()` | `Document[]` | List documents |
|
|
267
|
+
| `get(id)` | `Document \| null` | Get document |
|
|
268
|
+
| `getChunks(documentId)` | `Chunk[]` | Get chunks |
|
|
269
|
+
| `delete(id)` | `boolean` | Delete + cascade chunks |
|
|
276
270
|
|
|
277
|
-
|
|
278
|
-
import { Suspense, use } from 'react'
|
|
271
|
+
### Configuration
|
|
279
272
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
273
|
+
```ts
|
|
274
|
+
// Default: DashScope text-embedding-v4 (env: DASHSCOPE_API_KEY)
|
|
275
|
+
app.use(kb())
|
|
276
|
+
|
|
277
|
+
// Custom embedding
|
|
278
|
+
app.use(kb({
|
|
279
|
+
embed: async (text) => { /* return number[] */ },
|
|
280
|
+
dimensions: 1536,
|
|
281
|
+
chunkSize: 512, // tokens
|
|
282
|
+
chunkOverlap: 64,
|
|
283
|
+
}))
|
|
290
284
|
```
|
|
291
285
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
| Feature | Description |
|
|
295
|
-
|---|---|
|
|
296
|
-
| `react({ pages, layout, notFound, tailwind })` | One call: SSR + routing + client bundle + error handling |
|
|
297
|
-
| `export async function loader(ctx)` | Server-side data loading, auto-detected by the framework |
|
|
298
|
-
| `useServerData<T>()` | Type-safe access to loader data in any component |
|
|
299
|
-
| `Link` | `<a>` that does SPA navigation on the client |
|
|
300
|
-
| `ErrorBoundary` | Catches render errors on server and client |
|
|
301
|
-
| `<title>`, `<meta>` | Auto-hoisted to `<head>` via React 19 |
|
|
302
|
-
| `<Suspense>` | Streaming SSR, works out of the box |
|
|
303
|
-
| `client: false` | Disable client JS for static SSR only |
|
|
304
|
-
|
|
305
|
-
**Import paths:**
|
|
286
|
+
### Integration with Agent
|
|
306
287
|
|
|
307
288
|
```ts
|
|
308
|
-
|
|
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
|
+
### Storage
|
|
298
|
+
|
|
299
|
+
- `kb_documents` — document metadata
|
|
300
|
+
- `kb_chunks` — chunk content + VECTOR(1536) + TSVECTOR GIN index
|
|
316
301
|
|
|
317
|
-
|
|
302
|
+
---
|
|
318
303
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
> ```
|
|
304
|
+
## agent
|
|
305
|
+
|
|
306
|
+
AI Agent — LLM chat, tool calling, RAG, streaming.
|
|
323
307
|
|
|
324
308
|
```ts
|
|
325
309
|
import { agent } from 'weifuwu'
|
|
@@ -328,17 +312,10 @@ import { tool } from 'ai'
|
|
|
328
312
|
import { z } from 'zod'
|
|
329
313
|
|
|
330
314
|
app.use(agent({
|
|
331
|
-
model: openai('
|
|
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,293 @@ app.use(agent({
|
|
|
350
327
|
maxSteps: 5,
|
|
351
328
|
}))
|
|
352
329
|
|
|
330
|
+
// Streaming
|
|
353
331
|
app.post('/api/chat', async (req, ctx) => {
|
|
354
332
|
const { messages } = await req.json()
|
|
355
333
|
return ctx.agent.chatStreamResponse({ messages })
|
|
356
334
|
})
|
|
335
|
+
|
|
336
|
+
// Non-streaming
|
|
337
|
+
app.post('/api/chat/sync', async (req, ctx) => {
|
|
338
|
+
const { prompt } = await req.json()
|
|
339
|
+
const text = await ctx.agent.chat(prompt)
|
|
340
|
+
return Response.json({ text })
|
|
341
|
+
})
|
|
357
342
|
```
|
|
358
343
|
|
|
359
|
-
|
|
344
|
+
### ctx.agent API
|
|
345
|
+
|
|
346
|
+
| Method | Description |
|
|
347
|
+
|--------|-------------|
|
|
348
|
+
| `chat(prompt, opts?)` | Non-streaming, returns text |
|
|
349
|
+
| `chatStreamResponse({ messages })` | SSE stream (compatible with `useChat`) |
|
|
350
|
+
|
|
351
|
+
### Default Model
|
|
352
|
+
|
|
353
|
+
- LLM: DeepSeek-V4-Flash (via `@ai-sdk/openai` + `baseURL: 'https://api.deepseek.com/v1'`)
|
|
354
|
+
- Override with `DEEPSEEK_MODEL` env
|
|
355
|
+
- API key via `DEEPSEEK_API_KEY` or `OPENAI_API_KEY` env
|
|
356
|
+
|
|
357
|
+
### Features
|
|
360
358
|
|
|
361
359
|
| Feature | Description |
|
|
362
|
-
|
|
363
|
-
| `
|
|
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 callback, auto-injected into system prompt |
|
|
362
|
+
| `tools` | Tool definitions, auto-loop (maxSteps) |
|
|
363
|
+
| `sandbox: true` | Integrates with `ctx.sandbox` |
|
|
364
|
+
| `store` | Session persistence (save/load) |
|
|
365
|
+
| `agents` | Multi-agent orchestration |
|
|
370
366
|
|
|
371
|
-
|
|
367
|
+
---
|
|
372
368
|
|
|
373
|
-
|
|
369
|
+
## cms
|
|
374
370
|
|
|
375
|
-
|
|
376
|
-
import { auth } from 'weifuwu'
|
|
371
|
+
Content management — blog, docs, changelog.
|
|
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
|
+
// Public
|
|
381
|
+
app.get('/api/posts', async (req, ctx) => {
|
|
382
|
+
return Response.json(await ctx.cms.list({ type: 'post', status: 'published' }))
|
|
383
|
+
})
|
|
384
|
+
app.get('/api/posts/:slug', async (req, ctx) => {
|
|
385
|
+
const post = await ctx.cms.get(ctx.params.slug)
|
|
386
|
+
if (!post) return new Response('Not found', { status: 404 })
|
|
387
|
+
return Response.json(post)
|
|
388
|
+
})
|
|
394
389
|
|
|
395
|
-
//
|
|
396
|
-
app.
|
|
397
|
-
|
|
398
|
-
return Response.json(
|
|
390
|
+
// Admin
|
|
391
|
+
app.post('/api/admin/posts', requireRole('admin'), async (req, ctx) => {
|
|
392
|
+
const post = await ctx.cms.create(await req.json())
|
|
393
|
+
return Response.json(post, { status: 201 })
|
|
394
|
+
})
|
|
395
|
+
app.patch('/api/admin/posts/:id', requireRole('admin'), async (req, ctx) => {
|
|
396
|
+
const post = await ctx.cms.update(ctx.params.id, await req.json())
|
|
397
|
+
if (!post) return new Response('Not found', { status: 404 })
|
|
398
|
+
return Response.json(post)
|
|
399
399
|
})
|
|
400
400
|
```
|
|
401
401
|
|
|
402
|
-
|
|
402
|
+
### ctx.cms API
|
|
403
403
|
|
|
404
|
-
|
|
404
|
+
| Method | Returns | Description |
|
|
405
|
+
|--------|---------|-------------|
|
|
406
|
+
| `create(input)` | `Content` | Create (admin) |
|
|
407
|
+
| `get(slug)` | `Content \| null` | Get by slug |
|
|
408
|
+
| `getById(id)` | `Content \| null` | Get by ID |
|
|
409
|
+
| `update(id, input)` | `Content \| null` | Update (admin) |
|
|
410
|
+
| `delete(id)` | `boolean` | Delete (admin) |
|
|
411
|
+
| `list(opts?)` | `Content[]` | List with cursor |
|
|
412
|
+
| `publish(id)` | `Content \| null` | Publish (admin) |
|
|
413
|
+
| `unpublish(id)` | `Content \| null` | Unpublish (admin) |
|
|
414
|
+
| `listTags()` | `TagWithCount[]` | List tags |
|
|
415
|
+
| `createTag(name)` | `Tag` | Create tag |
|
|
405
416
|
|
|
406
|
-
|
|
407
|
-
import { sandbox } from 'weifuwu'
|
|
417
|
+
### Features
|
|
408
418
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
419
|
+
- Types: post / page / doc / changelog (any string)
|
|
420
|
+
- Status: draft / published / archived
|
|
421
|
+
- Slug: auto-generated, unique per type
|
|
422
|
+
- Tags: many-to-many, auto-created
|
|
423
|
+
- Tree: parent_id for hierarchy
|
|
424
|
+
- Auth: non-admin users see published only
|
|
414
425
|
|
|
415
|
-
|
|
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
|
+
Dynamic data storage engine — let users define their own data structures (like 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
|
+
// Define schema
|
|
440
|
+
app.post('/api/bases', async (req, ctx) => {
|
|
441
|
+
const b = await ctx.base.create(await req.json())
|
|
442
|
+
return Response.json(b, { status: 201 })
|
|
444
443
|
})
|
|
445
444
|
|
|
446
|
-
//
|
|
447
|
-
app.
|
|
448
|
-
const
|
|
449
|
-
return Response.json(
|
|
445
|
+
// CRUD
|
|
446
|
+
app.post('/api/bases/:id/:table', async (req, ctx) => {
|
|
447
|
+
const row = await ctx.base.insert(ctx.params.id, ctx.params.table, await req.json())
|
|
448
|
+
return Response.json(row, { status: 201 })
|
|
450
449
|
})
|
|
451
450
|
|
|
452
|
-
|
|
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` | Create database |
|
|
465
|
+
| `defineTable(baseId, schema)` | `BaseDef` | Add table |
|
|
466
|
+
| `updateTable(baseId, name, schema)` | `BaseDef \| null` | Update table |
|
|
467
|
+
| `removeTable(baseId, name)` | `BaseDef \| null` | Remove table |
|
|
468
|
+
| `insert(baseId, table, data)` | `Row` | Insert row |
|
|
469
|
+
| `getRow(baseId, table, id)` | `Row \| null` | Get row |
|
|
470
|
+
| `updateRow(baseId, table, id, data)` | `Row \| null` | Update row |
|
|
471
|
+
| `deleteRow(baseId, table, id)` | `boolean` | Delete row |
|
|
472
|
+
| `query(baseId, table, opts?)` | `Row[]` | Query (filter/sort/limit/offset) |
|
|
473
|
+
| `search(baseId, table, field, query)` | `Row[]` | Full-text search |
|
|
474
|
+
| `similaritySearch(baseId, table, field, vector)` | `Row[]` | Vector search |
|
|
475
|
+
| `list()` / `get(id)` / `getBySlug(slug)` / `delete(id)` | — | Manage databases |
|
|
476
|
+
|
|
477
|
+
### Architecture
|
|
478
|
+
|
|
479
|
+
Fixed Slot: a single `base_data` table with ~120 physical columns:
|
|
480
|
+
|
|
481
|
+
| Type | Count | PG Type |
|
|
482
|
+
|------|:-----:|:--------:|
|
|
483
|
+
| text001..064 | 64 | TEXT |
|
|
484
|
+
| number001..032 | 32 | DOUBLE PRECISION |
|
|
485
|
+
| date001..008 | 8 | TIMESTAMPTZ |
|
|
486
|
+
| vector001..004 | 4 | VECTOR(1536) |
|
|
487
|
+
| search001..004 | 4 | TEXT |
|
|
488
|
+
| ext | 1 | JSONB (overflow) |
|
|
489
|
+
|
|
490
|
+
Field name → physical column mapping stored in `base_column_map`. Fields beyond the physical columns overflow to ext JSONB.
|
|
491
|
+
|
|
492
|
+
pgvector auto-detected (included in docker image).
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## Router
|
|
499
|
+
|
|
500
|
+
```ts
|
|
501
|
+
const app = new Router()
|
|
502
|
+
|
|
503
|
+
// HTTP
|
|
504
|
+
app.get(path, ...handlers)
|
|
505
|
+
app.post / put / delete / patch / head / options(path, ...handlers)
|
|
506
|
+
app.all(path, ...handlers)
|
|
507
|
+
|
|
508
|
+
// WebSocket
|
|
509
|
+
app.ws(path, ...middlewares, handler)
|
|
510
|
+
// handler: { open?, message?, close?, error? }
|
|
511
|
+
|
|
512
|
+
// Middleware & mounting
|
|
513
|
+
app.use(middleware)
|
|
514
|
+
app.mount(prefix, router)
|
|
515
|
+
app.plugin(fn)
|
|
516
|
+
app.onError(handler)
|
|
517
|
+
app.routes() // debug: list all routes
|
|
474
518
|
```
|
|
475
519
|
|
|
476
|
-
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## Middleware
|
|
477
523
|
|
|
478
524
|
```ts
|
|
479
|
-
import {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
525
|
+
import { cors, helmet, compress, rateLimit, logger, upload, serveStatic, sandbox } from 'weifuwu'
|
|
526
|
+
|
|
527
|
+
app.use(cors({ origin: '*' }))
|
|
528
|
+
app.use(helmet())
|
|
529
|
+
app.use(compress())
|
|
530
|
+
app.use(rateLimit({ max: 100 }))
|
|
531
|
+
app.use(logger({ format: 'short' }))
|
|
532
|
+
app.use(upload())
|
|
533
|
+
app.use(serveStatic('./public'))
|
|
534
|
+
app.use(sandbox({ baseDir: '/tmp/workspaces' }))
|
|
535
|
+
```
|
|
483
536
|
|
|
484
|
-
|
|
485
|
-
.use(trace())
|
|
486
|
-
.use(logger())
|
|
487
|
-
.use(cors())
|
|
488
|
-
.use(helmet())
|
|
489
|
-
.use(compress())
|
|
490
|
-
.use(postgres())
|
|
491
|
-
.use(redis())
|
|
492
|
-
.use(auth({ jwt: { secret: process.env.JWT_SECRET! } }))
|
|
493
|
-
.use(sandbox({ isolateBy: 'user' }))
|
|
494
|
-
.use(agent({
|
|
495
|
-
model: openai('gpt-4o'),
|
|
496
|
-
system: 'You are a helpful assistant.',
|
|
497
|
-
knowledge: {
|
|
498
|
-
search: async (q, ctx) => ctx.sql`...`,
|
|
499
|
-
},
|
|
500
|
-
sandbox: true,
|
|
501
|
-
}))
|
|
502
|
-
.plugin(react({ pages: { '/': './Chat.tsx' }, layout: './Layout.tsx', tailwind: {} }))
|
|
537
|
+
---
|
|
503
538
|
|
|
504
|
-
|
|
505
|
-
if (!ctx.user) return new Response('Unauthorized', { status: 401 })
|
|
506
|
-
const { messages } = await req.json()
|
|
507
|
-
return ctx.agent.chatStreamResponse({ messages })
|
|
508
|
-
})
|
|
539
|
+
## Postgres
|
|
509
540
|
|
|
510
|
-
|
|
541
|
+
```ts
|
|
542
|
+
import { postgres } from 'weifuwu'
|
|
543
|
+
|
|
544
|
+
const sql = postgres()
|
|
545
|
+
app.use(sql) // → ctx.sql
|
|
546
|
+
|
|
547
|
+
await sql.sql`SELECT * FROM users WHERE id = ${id}`
|
|
548
|
+
await sql.sql.begin(async (sql) => { /* transaction */ })
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Reads `DATABASE_URL` env. Supports migrations, transactions, connection pool stats.
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
555
|
+
## Redis
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
import { redis } from 'weifuwu'
|
|
559
|
+
|
|
560
|
+
const r = redis()
|
|
561
|
+
app.use(r) // → ctx.redis
|
|
562
|
+
await r.redis.set('key', 'value')
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
Reads `REDIS_URL` env (default: `redis://localhost:6379`).
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## Queue & Cron
|
|
570
|
+
|
|
571
|
+
```ts
|
|
572
|
+
import { queue } from 'weifuwu'
|
|
573
|
+
|
|
574
|
+
const q = queue()
|
|
575
|
+
app.use(q) // → ctx.queue
|
|
576
|
+
|
|
577
|
+
q.process('email', async (job) => { await sendEmail(job.payload) })
|
|
578
|
+
q.cron('cleanup', '0 3 * * *', () => cleanup())
|
|
579
|
+
await q.add('email', { to: 'user@example.com' })
|
|
580
|
+
await q.add('remind', {}, { delay: 60_000 })
|
|
581
|
+
q.run()
|
|
511
582
|
```
|
|
512
583
|
|
|
584
|
+
---
|
|
585
|
+
|
|
513
586
|
## Project Structure
|
|
514
587
|
|
|
515
588
|
```
|
|
516
|
-
|
|
517
|
-
├──
|
|
518
|
-
├──
|
|
519
|
-
├──
|
|
520
|
-
├──
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
├──
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
│ └── test/ ← 131 tests (18 files)
|
|
536
|
-
├── examples/
|
|
537
|
-
│ └── react-ssr/ ← full SPA demo
|
|
538
|
-
└── dist/
|
|
589
|
+
src/
|
|
590
|
+
├── index.ts ← Entry, exports all modules
|
|
591
|
+
├── types.ts ← Context, Handler, Middleware types
|
|
592
|
+
├── core/ ← serve, router, ws, trace, logger
|
|
593
|
+
├── middleware/ ← cors, helmet, compress, rate-limit, upload, static, sandbox
|
|
594
|
+
├── user/ ← User system (CRUD, JWT, requireRole)
|
|
595
|
+
├── messager/ ← IM + AI conversation layer
|
|
596
|
+
├── kb/ ← RAG knowledge base (chunking, embedding, vector search)
|
|
597
|
+
├── ai/ ← AI Agent (LLM, tools, RAG)
|
|
598
|
+
├── cms/ ← Content management (blog, docs, changelog)
|
|
599
|
+
├── base/ ← Dynamic data engine (Fixed Slot)
|
|
600
|
+
├── postgres/ ← PostgreSQL client
|
|
601
|
+
├── redis/ ← Redis client
|
|
602
|
+
├── queue/ ← Job queue + cron
|
|
603
|
+
├── graphql.ts ← GraphQL
|
|
604
|
+
├── hub.ts ← WebSocket hub
|
|
605
|
+
└── test/ ← 281 tests
|
|
606
|
+
|
|
607
|
+
docker-compose.yml ← postgres (pgvector) + redis
|
|
539
608
|
```
|
|
540
609
|
|
|
610
|
+
---
|
|
611
|
+
|
|
541
612
|
## Development
|
|
542
613
|
|
|
543
614
|
```bash
|
|
544
|
-
|
|
545
|
-
npm run
|
|
546
|
-
npm
|
|
615
|
+
docker compose up -d # Start postgres + redis
|
|
616
|
+
npm run build # esbuild → dist/
|
|
617
|
+
npm run typecheck # tsc --noEmit
|
|
618
|
+
npm test # 281 tests
|
|
547
619
|
```
|