weifuwu 0.33.6 → 0.33.8
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 +367 -646
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,85 @@
|
|
|
6
6
|
npm install weifuwu
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
One package. Backend + frontend. User system,
|
|
9
|
+
One package. Backend + frontend. User system, messaging, RAG knowledge base, AI Agent, CMS, dynamic data storage, reactive frontend. Set env vars and go.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Module Overview
|
|
14
|
+
|
|
15
|
+
### Backend
|
|
16
|
+
|
|
17
|
+
| Module | Import | Depends on | Purpose |
|
|
18
|
+
|--------|--------|-----------|---------|
|
|
19
|
+
| `postgres()` | `weifuwu` | `DATABASE_URL` | PostgreSQL client (`ctx.sql`) |
|
|
20
|
+
| `redis()` | `weifuwu` | `REDIS_URL` | Redis client (`ctx.redis`) |
|
|
21
|
+
| `user()` | `weifuwu` | `postgres()`, `JWT_SECRET` | Auth, JWT, roles (`ctx.user`) |
|
|
22
|
+
| `messager()` | `weifuwu` | `postgres()`, `user()` | IM + AI conversation layer |
|
|
23
|
+
| `kb()` | `weifuwu` | `postgres()`, `DASHSCOPE_API_KEY` | RAG knowledge base |
|
|
24
|
+
| `agent()` | `weifuwu` | — | LLM chat, tools, streaming |
|
|
25
|
+
| `cms()` | `weifuwu` | `postgres()`, `user()` | Blog, docs, changelog |
|
|
26
|
+
| `base()` | `weifuwu` | `postgres()`, `user()` | Dynamic data engine |
|
|
27
|
+
| `queue()` | `weifuwu` | `REDIS_URL` | Job queue + cron |
|
|
28
|
+
| `ui()` | `weifuwu` | — | SSR/SPA rendering (`ctx.ui.html`, `ctx.ui.js`, `ctx.ui.css`) |
|
|
29
|
+
|
|
30
|
+
### Middleware
|
|
31
|
+
|
|
32
|
+
| Import | Purpose |
|
|
33
|
+
|--------|---------|
|
|
34
|
+
| `cors()` | CORS headers |
|
|
35
|
+
| `helmet()` | Security headers |
|
|
36
|
+
| `compress()` | gzip / brotli / deflate |
|
|
37
|
+
| `rateLimit()` | Sliding-window rate limiter |
|
|
38
|
+
| `logger()` | Request logging |
|
|
39
|
+
| `upload()` | Multipart file upload |
|
|
40
|
+
| `serveStatic()` | Static files |
|
|
41
|
+
| `sandbox()` | Filesystem isolation |
|
|
42
|
+
|
|
43
|
+
### Frontend (`weifuwu/client`)
|
|
44
|
+
|
|
45
|
+
| Import | Type | Purpose |
|
|
46
|
+
|--------|------|---------|
|
|
47
|
+
| `signal()` | function | Reactive state |
|
|
48
|
+
| `computed()` | function | Derived signals |
|
|
49
|
+
| `effect()` | function | Auto-tracked side effects |
|
|
50
|
+
| `<Show>` | component | Conditional rendering |
|
|
51
|
+
| `<For>` | component | List rendering |
|
|
52
|
+
| `<ErrorBoundary>` | component | Catch render errors |
|
|
53
|
+
| `<RouteView>` | component | Route outlet |
|
|
54
|
+
| `createApp()` | function | App instance with middleware chain |
|
|
55
|
+
| `mount()` | method | Mount SPA |
|
|
56
|
+
| `hydrate()` | method | SSR hydration |
|
|
57
|
+
| `router()` | middleware | Hash/history router |
|
|
58
|
+
| `api()` | middleware | HTTP client (`ctx.api`) |
|
|
59
|
+
| `auth()` | middleware | Auth state (`ctx.user/login/logout`) |
|
|
60
|
+
| `ws()` | middleware | WebSocket (`ctx.ws`) |
|
|
61
|
+
| `wrap()` | function | Third-party library integration |
|
|
62
|
+
| `useForm()` | function | Form state management |
|
|
63
|
+
| `createPortal()` | function | Render outside parent DOM |
|
|
64
|
+
| `LoginForm` | component | Login/register form |
|
|
65
|
+
| `Chat` | component | Real-time messaging |
|
|
66
|
+
|
|
67
|
+
### Utils
|
|
68
|
+
|
|
69
|
+
| Import | Purpose |
|
|
70
|
+
|--------|---------|
|
|
71
|
+
| `requireRole('admin')` | Middleware factory: check `ctx.user.role` |
|
|
72
|
+
| `createHub()` | WebSocket pub/sub |
|
|
73
|
+
| `HttpError` | `throw new HttpError(msg, 404)` |
|
|
74
|
+
| `trace()` | Request tracing |
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Environment Variables
|
|
79
|
+
|
|
80
|
+
| Variable | Default | Used by |
|
|
81
|
+
|----------|---------|---------|
|
|
82
|
+
| `DATABASE_URL` | `postgres://root:123456@localhost:5432/demo` | `postgres()` |
|
|
83
|
+
| `REDIS_URL` | `redis://localhost:6379` | `redis()`, `queue()` |
|
|
84
|
+
| `JWT_SECRET` | — | `user()` |
|
|
85
|
+
| `DASHSCOPE_API_KEY` | — | `kb()` (embedding) |
|
|
86
|
+
| `DEEPSEEK_API_KEY` / `OPENAI_API_KEY` | — | `agent()` (LLM) |
|
|
87
|
+
| `DEEPSEEK_MODEL` | `deepseek-v4-flash` | `agent()` |
|
|
10
88
|
|
|
11
89
|
---
|
|
12
90
|
|
|
@@ -46,17 +124,17 @@ function AppShell(_props: {}, ctx: WfuiContext) {
|
|
|
46
124
|
if (!ctx.isAuthenticated) return <LoginForm />
|
|
47
125
|
return (
|
|
48
126
|
<div>
|
|
49
|
-
<nav><a onClick={() => ctx.app.navigate('/chat')}
|
|
127
|
+
<nav><a onClick={() => ctx.app.navigate('/chat')}>Chat</a></nav>
|
|
50
128
|
<main><RouteView /></main>
|
|
51
129
|
</div>
|
|
52
130
|
)
|
|
53
131
|
}
|
|
54
132
|
|
|
55
133
|
const app = createApp()
|
|
56
|
-
app.use(api())
|
|
57
|
-
app.use(auth())
|
|
58
|
-
app.use(ws())
|
|
59
|
-
app.use(router({ routes }))
|
|
134
|
+
app.use(api())
|
|
135
|
+
app.use(auth())
|
|
136
|
+
app.use(ws())
|
|
137
|
+
app.use(router({ routes }))
|
|
60
138
|
app.mount('#root', AppShell)
|
|
61
139
|
```
|
|
62
140
|
|
|
@@ -65,9 +143,10 @@ app.mount('#root', AppShell)
|
|
|
65
143
|
{ "jsx": "react-jsx", "jsxImportSource": "weifuwu/client" }
|
|
66
144
|
```
|
|
67
145
|
|
|
146
|
+
Build (traditional, or use `ctx.ui.js()` for dynamic compilation):
|
|
147
|
+
|
|
68
148
|
```js
|
|
69
|
-
|
|
70
|
-
// 推荐:使用 ctx.ui.js() 服务端动态编译,无需独立构建脚本
|
|
149
|
+
import esbuild from 'esbuild'
|
|
71
150
|
esbuild.build({
|
|
72
151
|
entryPoints: ['src/main.tsx'],
|
|
73
152
|
jsx: 'automatic',
|
|
@@ -76,381 +155,61 @@ esbuild.build({
|
|
|
76
155
|
})
|
|
77
156
|
```
|
|
78
157
|
|
|
79
|
-
|
|
158
|
+
Or skip the build step entirely with server-side compilation:
|
|
159
|
+
|
|
80
160
|
```ts
|
|
81
161
|
app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
|
|
82
162
|
app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
|
|
83
163
|
```
|
|
84
164
|
|
|
85
|
-
### Environment Variables
|
|
86
|
-
|
|
87
|
-
| Variable | Default | Used by |
|
|
88
|
-
|----------|---------|---------|
|
|
89
|
-
| `DATABASE_URL` | `postgres://root:123456@localhost:5432/demo` | `postgres()` |
|
|
90
|
-
| `REDIS_URL` | `redis://localhost:6379` | `redis()` |
|
|
91
|
-
| `JWT_SECRET` | — | `user()` |
|
|
92
|
-
| `DASHSCOPE_API_KEY` | — | `kb()` (embedding) |
|
|
93
|
-
| `DEEPSEEK_API_KEY` / `OPENAI_API_KEY` | — | `agent()` (LLM) |
|
|
94
|
-
| `DEEPSEEK_MODEL` | `deepseek-v4-flash` | `agent()` |
|
|
95
|
-
|
|
96
|
-
---
|
|
97
|
-
|
|
98
|
-
## Modules
|
|
99
|
-
|
|
100
|
-
### Backend
|
|
101
|
-
|
|
102
|
-
| Module | Import | Dependency | Purpose |
|
|
103
|
-
|--------|--------|-----------|---------|
|
|
104
|
-
| User | `user()` | `postgres()` | Auth, JWT, roles |
|
|
105
|
-
| Messager | `messager()` | `postgres()`, `user()` | IM + AI conversation layer |
|
|
106
|
-
| KB | `kb()` | `postgres()` | RAG knowledge base |
|
|
107
|
-
| Agent | `agent()` | — | LLM chat, tool calling, streaming |
|
|
108
|
-
| CMS | `cms()` | `postgres()`, `user()` | Blog, docs, changelog |
|
|
109
|
-
| Base | `base()` | `postgres()`, `user()` | Dynamic data engine |
|
|
110
|
-
|
|
111
|
-
### Middleware
|
|
112
|
-
|
|
113
|
-
| Import | Purpose |
|
|
114
|
-
|--------|---------|
|
|
115
|
-
| `cors()` | CORS headers |
|
|
116
|
-
| `helmet()` | Security headers |
|
|
117
|
-
| `compress()` | gzip / brotli / deflate |
|
|
118
|
-
| `rateLimit()` | Sliding-window rate limiter |
|
|
119
|
-
| `logger()` | Request logging |
|
|
120
|
-
| `upload()` | Multipart file upload |
|
|
121
|
-
| `serveStatic()` | Static files |
|
|
122
|
-
| `sandbox()` | Filesystem isolation |
|
|
123
|
-
|
|
124
|
-
### Core
|
|
125
|
-
|
|
126
|
-
| Import | Purpose |
|
|
127
|
-
|--------|---------|
|
|
128
|
-
| `serve(app, opts?)` | Start HTTP server |
|
|
129
|
-
| `Router` | Trie-based router + WebSocket |
|
|
130
|
-
| `HttpError` | `throw new HttpError(msg, 404)` |
|
|
131
|
-
| `trace()` | Request tracing |
|
|
132
|
-
| `postgres()` | PostgreSQL client (`ctx.sql`) |
|
|
133
|
-
| `redis()` | Redis client (`ctx.redis`) |
|
|
134
|
-
| `queue()` | Job queue + cron |
|
|
135
|
-
| `createHub()` | WebSocket pub/sub |
|
|
136
|
-
| `ui()` | SSR + SPA rendering (`ctx.ui.html`, `ctx.ui.js`, `ctx.ui.css`) |
|
|
137
|
-
|
|
138
|
-
### Frontend (weifuwu/client)
|
|
139
|
-
|
|
140
|
-
| Import | Purpose |
|
|
141
|
-
|--------|---------|
|
|
142
|
-
| `signal()` | Reactive state |
|
|
143
|
-
| `computed()` | Derived signals |
|
|
144
|
-
| `effect()` | Auto-tracked side effects |
|
|
145
|
-
| `<Show>` | Conditional rendering |
|
|
146
|
-
| `<For>` | List rendering |
|
|
147
|
-
| `<RouteView>` | Route outlet |
|
|
148
|
-
| `createApp()` | App instance with middleware chain |
|
|
149
|
-
| `router()` | Hash/history router with params + query |
|
|
150
|
-
| `api()` | HTTP client (`ctx.api.get/post`) |
|
|
151
|
-
| `auth()` | Auth state (`ctx.user/login/logout`) |
|
|
152
|
-
| `ws()` | WebSocket (`ctx.ws.send/onMessage`) |
|
|
153
|
-
| `LoginForm` | Login/register form component |
|
|
154
|
-
| `Chat` | Real-time messaging component |
|
|
155
|
-
| `domMount()` | Direct DOM mounting |
|
|
156
|
-
| `wrap()` | Third-party library integration |
|
|
157
|
-
| `useForm()` | Form state management (validation, submit, reset) |
|
|
158
|
-
| `createPortal()` | Render outside parent DOM hierarchy |
|
|
159
|
-
| `<ErrorBoundary>` | Catch render errors, show fallback |
|
|
160
|
-
|
|
161
|
-
### Utilities
|
|
162
|
-
|
|
163
|
-
| Import | Purpose |
|
|
164
|
-
|--------|---------|
|
|
165
|
-
| `requireRole('admin')` | Middleware: check `ctx.user.role` |
|
|
166
|
-
|
|
167
165
|
---
|
|
168
166
|
|
|
169
|
-
##
|
|
167
|
+
## Backend Modules
|
|
170
168
|
|
|
171
|
-
|
|
169
|
+
Each module follows: import → usage → API table.
|
|
172
170
|
|
|
173
|
-
###
|
|
171
|
+
### postgres
|
|
174
172
|
|
|
175
173
|
```ts
|
|
176
|
-
|
|
177
|
-
const count = signal(0)
|
|
178
|
-
count.value = count.value + 1 // DOM updates automatically
|
|
179
|
-
|
|
180
|
-
// 2. Computed — derived signals
|
|
181
|
-
const doubled = computed(() => count.value * 2)
|
|
182
|
-
|
|
183
|
-
// 3. Effect — auto-tracked side effects
|
|
184
|
-
effect(() => console.log('count:', count.value))
|
|
185
|
-
|
|
186
|
-
// 4. Component — (props, ctx) => JSX
|
|
187
|
-
function MyComponent({ name }: { name: string }, ctx: WfuiContext) {
|
|
188
|
-
return <div>Hello {name}</div>
|
|
189
|
-
}
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
### createApp + Middleware
|
|
193
|
-
|
|
194
|
-
```tsx
|
|
195
|
-
import { createApp, router, RouteView } from 'weifuwu/client'
|
|
196
|
-
import type { WfuiContext, RouteDef } from 'weifuwu/client'
|
|
197
|
-
|
|
198
|
-
const app = createApp()
|
|
199
|
-
app.use(router({ routes, mode: 'hash' }))
|
|
200
|
-
app.mount('#root', AppShell)
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
### Router
|
|
204
|
-
|
|
205
|
-
```tsx
|
|
206
|
-
const routes: RouteDef[] = [
|
|
207
|
-
{ path: '/', component: HomePage, title: '首页' },
|
|
208
|
-
{ path: '/chat/:id', component: ChatPage, title: '聊天' },
|
|
209
|
-
{ path: '/user/:name', component: UserPage, title: '用户' },
|
|
210
|
-
]
|
|
211
|
-
|
|
212
|
-
app.use(router({
|
|
213
|
-
routes,
|
|
214
|
-
notFound: NotFound,
|
|
215
|
-
mode: 'hash', // or 'history'
|
|
216
|
-
}))
|
|
217
|
-
|
|
218
|
-
// In layout:
|
|
219
|
-
function AppShell(_, ctx) {
|
|
220
|
-
return (
|
|
221
|
-
<div>
|
|
222
|
-
<nav>
|
|
223
|
-
<a onClick={() => ctx.app.navigate('/')}>首页</a>
|
|
224
|
-
<a onClick={() => ctx.app.navigate('/chat/123')}>聊天</a>
|
|
225
|
-
</nav>
|
|
226
|
-
<main>
|
|
227
|
-
<RouteView /> {/* ← renders matched route */}
|
|
228
|
-
</main>
|
|
229
|
-
</div>
|
|
230
|
-
)
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Route params and query:
|
|
234
|
-
ctx.route.path // "/chat/123"
|
|
235
|
-
ctx.route.params // { id: "123" }
|
|
236
|
-
ctx.route.query // { tab: "settings" }
|
|
237
|
-
|
|
238
|
-
#### Route Loader — 数据预取
|
|
174
|
+
import { postgres } from 'weifuwu'
|
|
239
175
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
{
|
|
243
|
-
path: '/post/:id',
|
|
244
|
-
component: PostPage,
|
|
245
|
-
loader: async (ctx) => ({
|
|
246
|
-
post: await ctx.api.get(`/api/posts/${ctx.route.params.id}`),
|
|
247
|
-
}),
|
|
248
|
-
},
|
|
249
|
-
]
|
|
176
|
+
const sql = postgres()
|
|
177
|
+
app.use(sql) // → ctx.sql
|
|
250
178
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const post = ctx.route.data.post
|
|
254
|
-
if (!post) return <p class="text-gray-400">加载中...</p>
|
|
255
|
-
return <h1 class="text-2xl font-bold">{post.title}</h1>
|
|
256
|
-
}
|
|
179
|
+
await ctx.sql`SELECT * FROM users WHERE id = ${id}`
|
|
180
|
+
await ctx.sql.begin(async (sql) => { /* transaction */ })
|
|
257
181
|
```
|
|
258
182
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
import { api, auth, ws } from 'weifuwu/client'
|
|
265
|
-
|
|
266
|
-
app.use(api()) // ctx.api.get/post/put/patch/delete
|
|
267
|
-
app.use(auth()) // ctx.user / ctx.login / ctx.logout / ctx.register
|
|
268
|
-
app.use(ws()) // ctx.ws.send / onMessage / join / leave
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
`api()` creates a fetch client with automatic token injection.
|
|
272
|
-
`auth()` persists sessions to localStorage, validates tokens on startup.
|
|
273
|
-
`ws()` manages WebSocket connections with auto-reconnect.
|
|
274
|
-
|
|
275
|
-
### Pre-built Components
|
|
183
|
+
| Method | Description |
|
|
184
|
+
|--------|-------------|
|
|
185
|
+
| `ctx.sql\`...\`` | Tagged template SQL queries |
|
|
186
|
+
| `ctx.sql.begin(fn)` | Transaction |
|
|
187
|
+
| `sql.close()` | Close pool |
|
|
276
188
|
|
|
277
|
-
|
|
278
|
-
import { LoginForm, Chat } from 'weifuwu/client'
|
|
189
|
+
Reads `DATABASE_URL` env. Supports migrations via `postgres({ migrate: { directory: './migrations' } })`.
|
|
279
190
|
|
|
280
|
-
|
|
281
|
-
function LoginPage(_, ctx) {
|
|
282
|
-
if (ctx.isAuthenticated) return ctx.app.navigate('/')
|
|
283
|
-
return <LoginForm />
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Real-time chat
|
|
287
|
-
function ChatPage(_, ctx) {
|
|
288
|
-
return <Chat conversationId="123" />
|
|
289
|
-
}
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
### SSR & SPA (ctx.ui.html / ctx.ui.js / ctx.ui.css)
|
|
191
|
+
### redis
|
|
293
192
|
|
|
294
193
|
```ts
|
|
295
|
-
import {
|
|
296
|
-
|
|
297
|
-
app.use(ui())
|
|
298
|
-
|
|
299
|
-
// SSR page — ctx.ui.html`` returns complete HTML Response
|
|
300
|
-
app.get('/blog/:slug', async (req, ctx) => ctx.ui.html`
|
|
301
|
-
<!DOCTYPE html>
|
|
302
|
-
<html>
|
|
303
|
-
<head>
|
|
304
|
-
<title>${post.title}</title>
|
|
305
|
-
<link rel="stylesheet" href="/static/style.css">
|
|
306
|
-
</head>
|
|
307
|
-
<body>
|
|
308
|
-
<div id="root">
|
|
309
|
-
<h1>${post.title}</h1>
|
|
310
|
-
<div>${ctx.ui.html.unsafe(post.body)}</div>
|
|
311
|
-
<div data-hydrate="like"></div>
|
|
312
|
-
</div>
|
|
313
|
-
<script>window.__WFUI_PROPS__=${ctx.ui.html.unsafe(JSON.stringify({ post }))}</script>
|
|
314
|
-
<script src="/static/app.js"></script>
|
|
315
|
-
</body>
|
|
316
|
-
</html>
|
|
317
|
-
`)
|
|
318
|
-
|
|
319
|
-
// Dynamic JS compilation — ctx.ui.js() compiles TSX on demand
|
|
320
|
-
app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
|
|
321
|
-
|
|
322
|
-
// Dynamic CSS serving — ctx.ui.css() reads and serves CSS
|
|
323
|
-
app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
|
|
324
|
-
|
|
325
|
-
// Client hydrates interactive sections, skips SSR content
|
|
326
|
-
const app = createApp()
|
|
327
|
-
app.use(api())
|
|
328
|
-
|
|
329
|
-
const root = document.getElementById('root')
|
|
330
|
-
if (root && root.children.length > 0) {
|
|
331
|
-
// SSR page — hydrate only interactive areas
|
|
332
|
-
app.hydrate('[data-hydrate="like"]', LikeButton)
|
|
333
|
-
} else {
|
|
334
|
-
// SPA page — full mount
|
|
335
|
-
app.mount('#root', AppShell)
|
|
336
|
-
}
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
### wrap() — Third-party Library Integration
|
|
340
|
-
|
|
341
|
-
```tsx
|
|
342
|
-
import { wrap, effect } from 'weifuwu/client'
|
|
343
|
-
import * as echarts from 'echarts'
|
|
344
|
-
|
|
345
|
-
// wrap(tagName, setup) creates a component:
|
|
346
|
-
// - Creates a <div> container
|
|
347
|
-
// - Calls setup(el, props, ctx) when element enters the document
|
|
348
|
-
// - Runs cleanup when element is removed
|
|
349
|
-
const PieChart = wrap('div', (el, props: { data: any[] }, ctx) => {
|
|
350
|
-
const chart = echarts.init(el)
|
|
351
|
-
chart.setOption({ series: [{ type: 'pie', data: props.data }] })
|
|
352
|
-
effect(() => chart.setOption({ series: [{ type: 'pie', data: props.data }] }))
|
|
353
|
-
return () => chart.dispose()
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
// Use in JSX like any component
|
|
357
|
-
<Dashboard>
|
|
358
|
-
<PieChart data={salesData} />
|
|
359
|
-
</Dashboard>
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
### useForm() — 表单状态管理
|
|
363
|
-
|
|
364
|
-
```tsx
|
|
365
|
-
import { useForm } from 'weifuwu/client'
|
|
366
|
-
|
|
367
|
-
const form = useForm({
|
|
368
|
-
initial: { email: '', password: '' },
|
|
369
|
-
validate: {
|
|
370
|
-
email: (v) => !v.includes('@') && '请输入有效邮箱',
|
|
371
|
-
password: (v) => v.length < 6 && '至少 6 位',
|
|
372
|
-
},
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
// 绑定到 input:{...form.field('name')} 自动设置 value + onInput
|
|
376
|
-
<input {...form.field('email')} placeholder="邮箱" />
|
|
377
|
-
{form.errors.email && <span class="text-red-500">{form.errors.email}</span>}
|
|
378
|
-
|
|
379
|
-
// 提交时自动验证所有字段
|
|
380
|
-
<button onClick={() => form.submit((data) => ctx.login(data.email, data.password))}>
|
|
381
|
-
登录
|
|
382
|
-
</button>
|
|
383
|
-
|
|
384
|
-
// 重置
|
|
385
|
-
<button onClick={form.reset}>重置</button>
|
|
386
|
-
|
|
387
|
-
// 编程设置
|
|
388
|
-
form.setValue('email', 'a@b.com')
|
|
389
|
-
form.setValues({ email: 'a@b.com', password: '123' })
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
### createPortal() — 渲染到父容器外
|
|
393
|
-
|
|
394
|
-
适用于 Modal、Dropdown、Tooltip 等需突破 `overflow: hidden` 或 z-index 层级的情况。
|
|
395
|
-
|
|
396
|
-
```tsx
|
|
397
|
-
import { createPortal, Show } from 'weifuwu/client'
|
|
398
|
-
|
|
399
|
-
function Modal({ show, title, children }) {
|
|
400
|
-
return <Show when={show}>
|
|
401
|
-
{createPortal(
|
|
402
|
-
<div class="fixed inset-0 bg-black/50 flex items-center justify-center">
|
|
403
|
-
<div class="bg-white rounded-xl p-6 min-w-[400px]">
|
|
404
|
-
<h2 class="text-lg font-bold mb-4">{title}</h2>
|
|
405
|
-
{children}
|
|
406
|
-
</div>
|
|
407
|
-
</div>,
|
|
408
|
-
document.body
|
|
409
|
-
)}
|
|
410
|
-
</Show>
|
|
411
|
-
}
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
### ErrorBoundary — 错误边界
|
|
415
|
-
|
|
416
|
-
子组件渲染异常时捕获,显示 fallback 而非白屏。children 必须是 thunk(延迟执行)。
|
|
417
|
-
|
|
418
|
-
```tsx
|
|
419
|
-
import { ErrorBoundary } from 'weifuwu/client'
|
|
420
|
-
|
|
421
|
-
function AppShell(_, ctx) {
|
|
422
|
-
return (
|
|
423
|
-
<div>
|
|
424
|
-
<nav>...</nav>
|
|
425
|
-
<main>
|
|
426
|
-
<ErrorBoundary fallback={(e) => <p>出错了: {e.message}</p>}>
|
|
427
|
-
{() => <RouteView />}
|
|
428
|
-
</ErrorBoundary>
|
|
429
|
-
</main>
|
|
430
|
-
</div>
|
|
431
|
-
)
|
|
432
|
-
}
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
### Show / For
|
|
194
|
+
import { redis } from 'weifuwu'
|
|
436
195
|
|
|
437
|
-
|
|
438
|
-
//
|
|
439
|
-
<Show when={isLoggedIn} fallback={<LoginPage />}>
|
|
440
|
-
<Dashboard />
|
|
441
|
-
</Show>
|
|
196
|
+
const r = redis()
|
|
197
|
+
app.use(r) // → ctx.redis
|
|
442
198
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
{(item) => <div>{item.name}</div>}
|
|
446
|
-
</For>
|
|
199
|
+
await ctx.redis.set('key', 'value')
|
|
200
|
+
await ctx.redis.get('key')
|
|
447
201
|
```
|
|
448
202
|
|
|
449
|
-
|
|
203
|
+
| Method | Description |
|
|
204
|
+
|--------|-------------|
|
|
205
|
+
| `ctx.redis.set(key, value)` | Set key |
|
|
206
|
+
| `ctx.redis.get(key)` | Get key |
|
|
207
|
+
| `ctx.redis.del(key)` | Delete key |
|
|
208
|
+
| `redis.close()` | Close connection |
|
|
450
209
|
|
|
451
|
-
|
|
210
|
+
Reads `REDIS_URL` env (default: `redis://localhost:6379`).
|
|
452
211
|
|
|
453
|
-
|
|
212
|
+
### user
|
|
454
213
|
|
|
455
214
|
```ts
|
|
456
215
|
import { user, requireRole } from 'weifuwu'
|
|
@@ -458,30 +217,18 @@ import { user, requireRole } from 'weifuwu'
|
|
|
458
217
|
app.use(postgres())
|
|
459
218
|
app.use(user({ secret: process.env.JWT_SECRET }))
|
|
460
219
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
return Response.json(result)
|
|
465
|
-
})
|
|
466
|
-
// Login
|
|
220
|
+
app.post('/api/register', async (req, ctx) =>
|
|
221
|
+
Response.json(await ctx.userModule.register(await req.json())))
|
|
222
|
+
|
|
467
223
|
app.post('/api/login', async (req, ctx) => {
|
|
468
224
|
const { email, password } = await req.json()
|
|
469
225
|
const result = await ctx.userModule.login(email, password)
|
|
470
|
-
|
|
471
|
-
return Response.json(result)
|
|
472
|
-
})
|
|
473
|
-
// Current user
|
|
474
|
-
app.get('/api/me', async (req, ctx) => {
|
|
475
|
-
if (!ctx.user) return new Response('Unauthorized', { status: 401 })
|
|
476
|
-
return Response.json(ctx.user)
|
|
226
|
+
return result ? Response.json(result) : new Response('Unauthorized', { status: 401 })
|
|
477
227
|
})
|
|
478
|
-
// Admin only
|
|
479
|
-
app.get('/api/admin/users', requireRole('admin'), async (req, ctx) => {
|
|
480
|
-
return Response.json(await ctx.userModule.listUsers())
|
|
481
|
-
})
|
|
482
|
-
```
|
|
483
228
|
|
|
484
|
-
|
|
229
|
+
app.get('/api/me', async (req, ctx) =>
|
|
230
|
+
ctx.user ? Response.json(ctx.user) : new Response('Unauthorized', { status: 401 }))
|
|
231
|
+
```
|
|
485
232
|
|
|
486
233
|
| Method | Returns | Description |
|
|
487
234
|
|--------|---------|-------------|
|
|
@@ -495,39 +242,14 @@ app.get('/api/admin/users', requireRole('admin'), async (req, ctx) => {
|
|
|
495
242
|
| `listUsers(inactive?)` | `UserRecord[]` | List users |
|
|
496
243
|
| `generateToken(user)` | `string` | Issue JWT |
|
|
497
244
|
| `verifyToken(token)` | `TokenPayload \| null` | Verify JWT |
|
|
498
|
-
| `refreshToken(token)` | `string \| null` | Refresh JWT |
|
|
499
245
|
|
|
500
|
-
|
|
246
|
+
`ctx.user` — auto-resolved from `Authorization: Bearer` or `token` cookie. Fields: `id, name, email, role, [key: string]`.
|
|
501
247
|
|
|
502
|
-
|
|
248
|
+
`requireRole('admin')` — guard middleware. No auth → 401, wrong role → 403.
|
|
503
249
|
|
|
504
|
-
|
|
505
|
-
interface User {
|
|
506
|
-
id: string
|
|
507
|
-
name: string
|
|
508
|
-
email: string
|
|
509
|
-
role: string
|
|
510
|
-
[key: string]: unknown
|
|
511
|
-
}
|
|
512
|
-
```
|
|
250
|
+
Password: scrypt + 32-byte random salt. Token: HMAC SHA-256, 7 day expiry.
|
|
513
251
|
|
|
514
|
-
###
|
|
515
|
-
|
|
516
|
-
```ts
|
|
517
|
-
app.get('/admin', requireRole('admin'), handler)
|
|
518
|
-
// No auth → 401, wrong role → 403
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
### Security
|
|
522
|
-
|
|
523
|
-
- Password: scrypt + 32-byte random salt
|
|
524
|
-
- Token: HMAC SHA-256, 7 day expiry
|
|
525
|
-
|
|
526
|
-
---
|
|
527
|
-
|
|
528
|
-
## messager
|
|
529
|
-
|
|
530
|
-
Instant messaging + AI conversation layer. Direct/group chat, message persistence, WebSocket push.
|
|
252
|
+
### messager
|
|
531
253
|
|
|
532
254
|
```ts
|
|
533
255
|
import { messager } from 'weifuwu'
|
|
@@ -536,16 +258,13 @@ app.use(postgres())
|
|
|
536
258
|
app.use(user())
|
|
537
259
|
app.use(messager())
|
|
538
260
|
|
|
539
|
-
// WebSocket — auto-join all user conversations
|
|
540
261
|
app.ws('/ws', {
|
|
541
262
|
async open(ws, ctx) {
|
|
542
|
-
for (const c of await ctx.messager.getConversations())
|
|
263
|
+
for (const c of await ctx.messager.getConversations())
|
|
543
264
|
ctx.ws.join(`conversation:${c.id}`)
|
|
544
|
-
}
|
|
545
265
|
},
|
|
546
266
|
})
|
|
547
267
|
|
|
548
|
-
// REST API
|
|
549
268
|
app.post('/api/messages', async (req, ctx) => {
|
|
550
269
|
const { conversationId, body } = await req.json()
|
|
551
270
|
const msg = await ctx.messager.sendMessage(conversationId, body)
|
|
@@ -561,36 +280,23 @@ app.get('/api/conversations/:id/messages', async (req, ctx) => {
|
|
|
561
280
|
})
|
|
562
281
|
```
|
|
563
282
|
|
|
564
|
-
### ctx.messager API
|
|
565
|
-
|
|
566
283
|
| Method | Returns | Description |
|
|
567
284
|
|--------|---------|-------------|
|
|
568
285
|
| `createDirectConversation(userId)` | `Conversation` | Create/reuse DM |
|
|
569
286
|
| `createGroupConversation(title, userIds)` | `Conversation` | Create group |
|
|
570
|
-
| `sendMessage(convId, body)` | `Message` | Send
|
|
287
|
+
| `sendMessage(convId, body)` | `Message` | Send + broadcast to room |
|
|
571
288
|
| `getMessages(convId, opts?)` | `Message[]` | Cursor pagination |
|
|
572
289
|
| `editMessage(msgId, body)` | `Message \| null` | Edit (24h window) |
|
|
573
290
|
| `deleteMessage(msgId)` | `boolean` | Soft delete |
|
|
574
291
|
| `getConversations()` | `Conversation[]` | List with unread + last message |
|
|
575
292
|
| `getConversation(id)` | `Conversation \| null` | Get detail |
|
|
576
293
|
| `markRead(convId)` | `void` | Mark as read |
|
|
577
|
-
| `getUnreadCount()` | `{ total, byConversation }` | Unread stats |
|
|
578
294
|
| `addParticipants(convId, userIds)` | `void` | Add members |
|
|
579
295
|
| `removeParticipant(convId, userId?)` | `boolean` | Leave / kick |
|
|
580
296
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
3 tables: `conversations` / `participants` / `messages`. Auto-migration.
|
|
584
|
-
|
|
585
|
-
### AI Conversations
|
|
586
|
-
|
|
587
|
-
messager + agent = ChatGPT foundation. Messager handles sessions + push, agent handles LLM generation.
|
|
588
|
-
|
|
589
|
-
---
|
|
590
|
-
|
|
591
|
-
## kb
|
|
297
|
+
Storage: 3 auto-migrated tables (conversations, participants, messages). WebSocket push via rooms.
|
|
592
298
|
|
|
593
|
-
|
|
299
|
+
### kb — Knowledge Base
|
|
594
300
|
|
|
595
301
|
```ts
|
|
596
302
|
import { kb } from 'weifuwu'
|
|
@@ -598,22 +304,17 @@ import { kb } from 'weifuwu'
|
|
|
598
304
|
app.use(postgres())
|
|
599
305
|
app.use(kb())
|
|
600
306
|
|
|
601
|
-
// Import
|
|
602
307
|
app.post('/api/kb/import', async (req, ctx) => {
|
|
603
308
|
const { title, content } = await req.json()
|
|
604
|
-
|
|
605
|
-
return Response.json(result, { status: 201 })
|
|
309
|
+
return Response.json(await ctx.kb.importText(title, content), { status: 201 })
|
|
606
310
|
})
|
|
607
311
|
|
|
608
|
-
// Search
|
|
609
312
|
app.post('/api/kb/search', async (req, ctx) => {
|
|
610
313
|
const { query } = await req.json()
|
|
611
314
|
return Response.json(await ctx.kb.search(query, { limit: 5 }))
|
|
612
315
|
})
|
|
613
316
|
```
|
|
614
317
|
|
|
615
|
-
### ctx.kb API
|
|
616
|
-
|
|
617
318
|
| Method | Returns | Description |
|
|
618
319
|
|--------|---------|-------------|
|
|
619
320
|
| `importText(title, text, opts?)` | `{ document, chunks }` | Import → chunk → embed → store |
|
|
@@ -621,45 +322,20 @@ app.post('/api/kb/search', async (req, ctx) => {
|
|
|
621
322
|
| `search(query, opts?)` | `SearchResult[]` | Semantic search (cosine) |
|
|
622
323
|
| `list()` | `Document[]` | List documents |
|
|
623
324
|
| `get(id)` | `Document \| null` | Get document |
|
|
624
|
-
| `getChunks(documentId)` | `Chunk[]` | Get chunks |
|
|
625
325
|
| `delete(id)` | `boolean` | Delete + cascade chunks |
|
|
626
326
|
|
|
627
|
-
|
|
327
|
+
Default: DashScope `text-embedding-v4`. Customizable via `kb({ embed: async (text) => number[], dimensions: 1536 })`. Storage: `kb_documents` + `kb_chunks` (VECTOR(1536) + TSVECTOR GIN index).
|
|
628
328
|
|
|
629
|
-
|
|
630
|
-
// Default: DashScope text-embedding-v4 (env: DASHSCOPE_API_KEY)
|
|
631
|
-
app.use(kb())
|
|
632
|
-
|
|
633
|
-
// Custom embedding
|
|
634
|
-
app.use(kb({
|
|
635
|
-
embed: async (text) => { /* return number[] */ },
|
|
636
|
-
dimensions: 1536,
|
|
637
|
-
chunkSize: 512, // tokens
|
|
638
|
-
chunkOverlap: 64,
|
|
639
|
-
}))
|
|
640
|
-
```
|
|
641
|
-
|
|
642
|
-
### Integration with Agent
|
|
329
|
+
Integration with agent:
|
|
643
330
|
|
|
644
331
|
```ts
|
|
645
332
|
app.use(agent({
|
|
646
|
-
model: openai('deepseek-v4-flash',
|
|
647
|
-
knowledge: {
|
|
648
|
-
search: async (query, ctx) => ctx.kb.search(query),
|
|
649
|
-
},
|
|
333
|
+
model: openai('deepseek-v4-flash', ...),
|
|
334
|
+
knowledge: { search: async (query, ctx) => ctx.kb.search(query) },
|
|
650
335
|
}))
|
|
651
336
|
```
|
|
652
337
|
|
|
653
|
-
###
|
|
654
|
-
|
|
655
|
-
- `kb_documents` — document metadata
|
|
656
|
-
- `kb_chunks` — chunk content + VECTOR(1536) + TSVECTOR GIN index
|
|
657
|
-
|
|
658
|
-
---
|
|
659
|
-
|
|
660
|
-
## agent
|
|
661
|
-
|
|
662
|
-
AI Agent — LLM chat, tool calling, RAG, streaming.
|
|
338
|
+
### agent
|
|
663
339
|
|
|
664
340
|
```ts
|
|
665
341
|
import { agent } from 'weifuwu'
|
|
@@ -670,9 +346,7 @@ import { z } from 'zod'
|
|
|
670
346
|
app.use(agent({
|
|
671
347
|
model: openai('deepseek-v4-flash', { baseURL: 'https://api.deepseek.com/v1' }),
|
|
672
348
|
system: 'You are a helpful assistant.',
|
|
673
|
-
knowledge: {
|
|
674
|
-
search: async (query, ctx) => ctx.kb.search(query),
|
|
675
|
-
},
|
|
349
|
+
knowledge: { search: async (q, ctx) => ctx.kb.search(q) },
|
|
676
350
|
tools: {
|
|
677
351
|
getWeather: tool({
|
|
678
352
|
description: 'Get weather for a city',
|
|
@@ -683,48 +357,22 @@ app.use(agent({
|
|
|
683
357
|
maxSteps: 5,
|
|
684
358
|
}))
|
|
685
359
|
|
|
686
|
-
// Streaming
|
|
687
360
|
app.post('/api/chat', async (req, ctx) => {
|
|
688
361
|
const { messages } = await req.json()
|
|
689
362
|
return ctx.agent.chatStreamResponse({ messages })
|
|
690
363
|
})
|
|
691
|
-
|
|
692
|
-
// Non-streaming
|
|
693
|
-
app.post('/api/chat/sync', async (req, ctx) => {
|
|
694
|
-
const { prompt } = await req.json()
|
|
695
|
-
const text = await ctx.agent.chat(prompt)
|
|
696
|
-
return Response.json({ text })
|
|
697
|
-
})
|
|
698
364
|
```
|
|
699
365
|
|
|
700
|
-
### ctx.agent API
|
|
701
|
-
|
|
702
366
|
| Method | Description |
|
|
703
367
|
|-------------|-------------|
|
|
704
368
|
| `chat(prompt, opts?)` | Non-streaming, returns text |
|
|
705
369
|
| `chatStreamResponse({ messages })` | SSE stream (compatible with `useChat`) |
|
|
706
370
|
|
|
707
|
-
|
|
371
|
+
Default model: DeepSeek-V4-Flash via `@ai-sdk/openai`. Override with `DEEPSEEK_MODEL` env. API key via `DEEPSEEK_API_KEY` or `OPENAI_API_KEY`.
|
|
708
372
|
|
|
709
|
-
-
|
|
710
|
-
- Override with `DEEPSEEK_MODEL` env
|
|
711
|
-
- API key via `DEEPSEEK_API_KEY` or `OPENAI_API_KEY` env
|
|
373
|
+
Features: `knowledge.search` (RAG), `tools` (auto-loop with maxSteps), `sandbox: true` (filesystem isolation), `store` (session persistence), `agents` (multi-agent).
|
|
712
374
|
|
|
713
|
-
###
|
|
714
|
-
|
|
715
|
-
| Feature | Description |
|
|
716
|
-
|---------|-------------|
|
|
717
|
-
| `knowledge.search` | RAG callback, auto-injected into system prompt |
|
|
718
|
-
| `tools` | Tool definitions, auto-loop (maxSteps) |
|
|
719
|
-
| `sandbox: true` | Integrates with `ctx.sandbox` |
|
|
720
|
-
| `store` | Session persistence (save/load) |
|
|
721
|
-
| `agents` | Multi-agent orchestration |
|
|
722
|
-
|
|
723
|
-
---
|
|
724
|
-
|
|
725
|
-
## cms
|
|
726
|
-
|
|
727
|
-
Content management — blog, docs, changelog.
|
|
375
|
+
### cms
|
|
728
376
|
|
|
729
377
|
```ts
|
|
730
378
|
import { cms, requireRole } from 'weifuwu'
|
|
@@ -733,30 +381,18 @@ app.use(postgres())
|
|
|
733
381
|
app.use(user())
|
|
734
382
|
app.use(cms())
|
|
735
383
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
})
|
|
384
|
+
app.get('/api/posts', async (req, ctx) =>
|
|
385
|
+
Response.json(await ctx.cms.list({ type: 'post', status: 'published' })))
|
|
386
|
+
|
|
740
387
|
app.get('/api/posts/:slug', async (req, ctx) => {
|
|
741
388
|
const post = await ctx.cms.get(ctx.params.slug)
|
|
742
|
-
|
|
743
|
-
return Response.json(post)
|
|
389
|
+
return post ? Response.json(post) : new Response('Not found', { status: 404 })
|
|
744
390
|
})
|
|
745
391
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
const post = await ctx.cms.create(await req.json())
|
|
749
|
-
return Response.json(post, { status: 201 })
|
|
750
|
-
})
|
|
751
|
-
app.patch('/api/admin/posts/:id', requireRole('admin'), async (req, ctx) => {
|
|
752
|
-
const post = await ctx.cms.update(ctx.params.id, await req.json())
|
|
753
|
-
if (!post) return new Response('Not found', { status: 404 })
|
|
754
|
-
return Response.json(post)
|
|
755
|
-
})
|
|
392
|
+
app.post('/api/admin/posts', requireRole('admin'), async (req, ctx) =>
|
|
393
|
+
Response.json(await ctx.cms.create(await req.json()), { status: 201 }))
|
|
756
394
|
```
|
|
757
395
|
|
|
758
|
-
### ctx.cms API
|
|
759
|
-
|
|
760
396
|
| Method | Returns | Description |
|
|
761
397
|
|--------|---------|-------------|
|
|
762
398
|
| `create(input)` | `Content` | Create (admin) |
|
|
@@ -764,26 +400,15 @@ app.patch('/api/admin/posts/:id', requireRole('admin'), async (req, ctx) => {
|
|
|
764
400
|
| `getById(id)` | `Content \| null` | Get by ID |
|
|
765
401
|
| `update(id, input)` | `Content \| null` | Update (admin) |
|
|
766
402
|
| `delete(id)` | `boolean` | Delete (admin) |
|
|
767
|
-
| `list(opts?)` | `Content[]` | List with cursor |
|
|
403
|
+
| `list(opts?)` | `Content[]` | List with cursor + filters |
|
|
768
404
|
| `publish(id)` | `Content \| null` | Publish (admin) |
|
|
769
405
|
| `unpublish(id)` | `Content \| null` | Unpublish (admin) |
|
|
770
406
|
| `listTags()` | `TagWithCount[]` | List tags |
|
|
771
407
|
| `createTag(name)` | `Tag` | Create tag |
|
|
772
408
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
- Types: post / page / doc / changelog (any string)
|
|
776
|
-
- Status: draft / published / archived
|
|
777
|
-
- Slug: auto-generated, unique per type
|
|
778
|
-
- Tags: many-to-many, auto-created
|
|
779
|
-
- Tree: parent_id for hierarchy
|
|
780
|
-
- Auth: non-admin users see published only
|
|
781
|
-
|
|
782
|
-
---
|
|
783
|
-
|
|
784
|
-
## base
|
|
409
|
+
Types: post / page / doc / changelog (any string). Status: draft / published / archived. Tags: many-to-many, auto-created. Parent_id for hierarchy. Auth: non-admin users see published only.
|
|
785
410
|
|
|
786
|
-
|
|
411
|
+
### base — Dynamic Data Engine
|
|
787
412
|
|
|
788
413
|
```ts
|
|
789
414
|
import { base } from 'weifuwu'
|
|
@@ -792,17 +417,11 @@ app.use(postgres())
|
|
|
792
417
|
app.use(user())
|
|
793
418
|
app.use(base())
|
|
794
419
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
const b = await ctx.base.create(await req.json())
|
|
798
|
-
return Response.json(b, { status: 201 })
|
|
799
|
-
})
|
|
420
|
+
app.post('/api/bases', async (req, ctx) =>
|
|
421
|
+
Response.json(await ctx.base.create(await req.json()), { status: 201 }))
|
|
800
422
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
const row = await ctx.base.insert(ctx.params.id, ctx.params.table, await req.json())
|
|
804
|
-
return Response.json(row, { status: 201 })
|
|
805
|
-
})
|
|
423
|
+
app.post('/api/bases/:id/:table', async (req, ctx) =>
|
|
424
|
+
Response.json(await ctx.base.insert(ctx.params.id, ctx.params.table, await req.json()), { status: 201 }))
|
|
806
425
|
|
|
807
426
|
app.get('/api/bases/:id/:table', async (req, ctx) => {
|
|
808
427
|
const url = new URL(req.url)
|
|
@@ -813,14 +432,9 @@ app.get('/api/bases/:id/:table', async (req, ctx) => {
|
|
|
813
432
|
})
|
|
814
433
|
```
|
|
815
434
|
|
|
816
|
-
### ctx.base API
|
|
817
|
-
|
|
818
435
|
| Method | Returns | Description |
|
|
819
436
|
|--------|---------|-------------|
|
|
820
437
|
| `create({ name, tables })` | `BaseDef` | Create database |
|
|
821
|
-
| `defineTable(baseId, schema)` | `BaseDef` | Add table |
|
|
822
|
-
| `updateTable(baseId, name, schema)` | `BaseDef \| null` | Update table |
|
|
823
|
-
| `removeTable(baseId, name)` | `BaseDef \| null` | Remove table |
|
|
824
438
|
| `insert(baseId, table, data)` | `Row` | Insert row |
|
|
825
439
|
| `getRow(baseId, table, id)` | `Row \| null` | Get row |
|
|
826
440
|
| `updateRow(baseId, table, id, data)` | `Row \| null` | Update row |
|
|
@@ -828,113 +442,220 @@ app.get('/api/bases/:id/:table', async (req, ctx) => {
|
|
|
828
442
|
| `query(baseId, table, opts?)` | `Row[]` | Query (filter/sort/limit/offset) |
|
|
829
443
|
| `search(baseId, table, field, query)` | `Row[]` | Full-text search |
|
|
830
444
|
| `similaritySearch(baseId, table, field, vector)` | `Row[]` | Vector search |
|
|
831
|
-
| `list()` / `get(id)` / `getBySlug(slug)` / `delete(id)` | — | Manage databases |
|
|
832
|
-
|
|
833
|
-
### Architecture
|
|
834
445
|
|
|
835
|
-
Fixed Slot: a single `base_data` table with ~120 physical columns
|
|
446
|
+
Fixed Slot architecture: a single `base_data` table with ~120 physical columns (64 text, 32 number, 8 date, 4 vector, 4 search). Field → physical column mapping stored in `base_column_map`. Overflow to JSONB. pgvector auto-detected.
|
|
836
447
|
|
|
837
|
-
|
|
838
|
-
|------|:-----:|:--------:|
|
|
839
|
-
| text001..064 | 64 | TEXT |
|
|
840
|
-
| number001..032 | 32 | DOUBLE PRECISION |
|
|
841
|
-
| date001..008 | 8 | TIMESTAMPTZ |
|
|
842
|
-
| vector001..004 | 4 | VECTOR(1536) |
|
|
843
|
-
| search001..004 | 4 | TEXT |
|
|
844
|
-
| ext | 1 | JSONB (overflow) |
|
|
448
|
+
### queue
|
|
845
449
|
|
|
846
|
-
|
|
450
|
+
```ts
|
|
451
|
+
import { queue } from 'weifuwu'
|
|
847
452
|
|
|
848
|
-
|
|
453
|
+
const q = queue()
|
|
454
|
+
app.use(q) // → ctx.queue
|
|
849
455
|
|
|
850
|
-
|
|
456
|
+
q.process('email', async (job) => { await sendEmail(job.payload) })
|
|
457
|
+
q.cron('cleanup', '0 3 * * *', () => cleanup())
|
|
458
|
+
await q.add('email', { to: 'user@example.com' })
|
|
459
|
+
q.run()
|
|
460
|
+
```
|
|
851
461
|
|
|
852
|
-
|
|
462
|
+
| Method | Description |
|
|
463
|
+
|--------|-------------|
|
|
464
|
+
| `q.process(name, handler)` | Register job processor |
|
|
465
|
+
| `q.add(name, payload, opts?)` | Enqueue job |
|
|
466
|
+
| `q.cron(name, schedule, fn)` | Schedule recurring job |
|
|
467
|
+
| `q.run()` | Start processing |
|
|
468
|
+
| `q.close()` | Shutdown |
|
|
853
469
|
|
|
854
|
-
|
|
470
|
+
### ui — SSR & SPA
|
|
855
471
|
|
|
856
472
|
```ts
|
|
857
|
-
|
|
473
|
+
import { ui } from 'weifuwu'
|
|
858
474
|
|
|
859
|
-
|
|
860
|
-
app.get(path, ...handlers)
|
|
861
|
-
app.post / put / delete / patch / head / options(path, ...handlers)
|
|
862
|
-
app.all(path, ...handlers)
|
|
475
|
+
app.use(ui())
|
|
863
476
|
|
|
864
|
-
//
|
|
865
|
-
app.
|
|
866
|
-
|
|
477
|
+
// SSR page — tagged template returns Response
|
|
478
|
+
app.get('/blog/:slug', async (req, ctx) => ctx.ui.html`
|
|
479
|
+
<!DOCTYPE html><html>
|
|
480
|
+
<head><title>${post.title}</title></head>
|
|
481
|
+
<body><div id="root">${ctx.ui.html.unsafe(post.body)}</div>
|
|
482
|
+
<script src="/static/app.js"></script></body></html>`)
|
|
867
483
|
|
|
868
|
-
//
|
|
869
|
-
app.
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
app.
|
|
873
|
-
app.routes() // debug: list all routes
|
|
484
|
+
// Dynamic JS compilation (no build step needed)
|
|
485
|
+
app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
|
|
486
|
+
|
|
487
|
+
// CSS with Tailwind support (auto-detects tailwindcss + postcss)
|
|
488
|
+
app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
|
|
874
489
|
```
|
|
875
490
|
|
|
491
|
+
| API | Returns | Description |
|
|
492
|
+
|-----|---------|-------------|
|
|
493
|
+
| `ctx.ui.html\`...\`` | `Response` | Tagged template → HTML |
|
|
494
|
+
| `ctx.ui.html.unsafe(str)` | `string` | Mark as safe (skip escaping) |
|
|
495
|
+
| `ctx.ui.js(entryPath)` | `Response` | Compile TSX → JS bundle |
|
|
496
|
+
| `ctx.ui.css(entryPath)` | `Response` | Read/compile CSS (Tailwind v4) |
|
|
497
|
+
|
|
498
|
+
Tailwind CSS v4 support: add `@import 'tailwindcss'` to your CSS entry file. `ctx.ui.css` auto-detects `postcss` + `@tailwindcss/postcss`. Falls back to raw file serving if not installed.
|
|
499
|
+
|
|
876
500
|
---
|
|
877
501
|
|
|
878
|
-
##
|
|
502
|
+
## Frontend — weifuwu/client
|
|
879
503
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
504
|
+
Reactive frontend framework. Zero virtual DOM, zero external dependencies. Component model: `(props, ctx) => Node`.
|
|
505
|
+
|
|
506
|
+
### Concepts
|
|
507
|
+
|
|
508
|
+
```tsx
|
|
509
|
+
// Signal — reactive data
|
|
510
|
+
const count = signal(0)
|
|
511
|
+
count.value = count.value + 1 // DOM updates automatically
|
|
512
|
+
|
|
513
|
+
// Computed — derived signals
|
|
514
|
+
const doubled = computed(() => count.value * 2)
|
|
515
|
+
|
|
516
|
+
// Effect — auto-tracked side effects
|
|
517
|
+
effect(() => console.log('count:', count.value))
|
|
518
|
+
|
|
519
|
+
// Component — (props, ctx) => JSX
|
|
520
|
+
function MyComponent({ name }: { name: string }, ctx: WfuiContext) {
|
|
521
|
+
return <div>Hello {name}</div>
|
|
522
|
+
}
|
|
891
523
|
```
|
|
892
524
|
|
|
893
|
-
|
|
525
|
+
### createApp + Middleware
|
|
894
526
|
|
|
895
|
-
|
|
527
|
+
```tsx
|
|
528
|
+
const app = createApp()
|
|
529
|
+
app.use(api()) // ctx.api.get/post/put/patch/delete
|
|
530
|
+
app.use(auth()) // ctx.user / ctx.login / ctx.logout / ctx.register
|
|
531
|
+
app.use(ws()) // ctx.ws.send / onMessage / join / leave
|
|
532
|
+
app.use(router({ routes })) // ctx.route.path/params/query
|
|
896
533
|
|
|
897
|
-
|
|
898
|
-
|
|
534
|
+
app.mount('#root', AppShell) // SPA mode
|
|
535
|
+
app.hydrate('#comments', Comments) // SSR hydration (doesn't clear DOM)
|
|
536
|
+
```
|
|
899
537
|
|
|
900
|
-
|
|
901
|
-
app.use(sql) // → ctx.sql
|
|
538
|
+
### Router
|
|
902
539
|
|
|
903
|
-
|
|
904
|
-
|
|
540
|
+
```tsx
|
|
541
|
+
const routes: RouteDef[] = [
|
|
542
|
+
{ path: '/', component: HomePage, title: 'Home' },
|
|
543
|
+
{ path: '/post/:id', component: PostPage, loader: async (ctx) => ({
|
|
544
|
+
post: await ctx.api.get(`/api/posts/${ctx.route.params.id}`),
|
|
545
|
+
})},
|
|
546
|
+
]
|
|
547
|
+
|
|
548
|
+
app.use(router({ routes, notFound: NotFound, mode: 'hash' }))
|
|
549
|
+
|
|
550
|
+
// In component:
|
|
551
|
+
ctx.route.path // "/post/123"
|
|
552
|
+
ctx.route.params // { id: "123" }
|
|
553
|
+
ctx.route.query // { tab: "settings" }
|
|
554
|
+
ctx.route.data // { post: {...} } — from loader
|
|
905
555
|
```
|
|
906
556
|
|
|
907
|
-
|
|
557
|
+
Loader data flow: component renders immediately (shows loading state), loader completes → re-render with data.
|
|
908
558
|
|
|
909
|
-
|
|
559
|
+
### wrap() — Third-party Library Integration
|
|
910
560
|
|
|
911
|
-
|
|
561
|
+
```tsx
|
|
562
|
+
import { wrap, effect } from 'weifuwu/client'
|
|
563
|
+
import * as echarts from 'echarts'
|
|
912
564
|
|
|
913
|
-
|
|
914
|
-
|
|
565
|
+
const PieChart = wrap('div', (el, props: { data: any[] }, ctx) => {
|
|
566
|
+
const chart = echarts.init(el)
|
|
567
|
+
chart.setOption({ series: [{ type: 'pie', data: props.data }] })
|
|
568
|
+
effect(() => chart.setOption({ series: [{ type: 'pie', data: props.data }] }))
|
|
569
|
+
return () => chart.dispose() // cleanup when element is removed
|
|
570
|
+
})
|
|
915
571
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
await r.redis.set('key', 'value')
|
|
572
|
+
// Use as regular component:
|
|
573
|
+
<Dashboard><PieChart data={salesData} /></Dashboard>
|
|
919
574
|
```
|
|
920
575
|
|
|
921
|
-
|
|
576
|
+
### useForm() — Form State Management
|
|
577
|
+
|
|
578
|
+
```tsx
|
|
579
|
+
import { useForm } from 'weifuwu/client'
|
|
580
|
+
|
|
581
|
+
const form = useForm({
|
|
582
|
+
initial: { email: '', password: '' },
|
|
583
|
+
validate: {
|
|
584
|
+
email: (v) => !v.includes('@') && 'Invalid email',
|
|
585
|
+
},
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
// Bind to input — {...form.field('name')} sets value + onInput
|
|
589
|
+
<input {...form.field('email')} placeholder="Email" />
|
|
590
|
+
{form.errors.email && <span>{form.errors.email}</span>}
|
|
591
|
+
|
|
592
|
+
// Auto-validates all fields before calling handler
|
|
593
|
+
<button onClick={() => form.submit((data) => ctx.login(data.email, data.password))}>
|
|
594
|
+
Login
|
|
595
|
+
</button>
|
|
596
|
+
|
|
597
|
+
// Programmatic control
|
|
598
|
+
form.setValue('email', 'a@b.com')
|
|
599
|
+
form.setValues({ email: 'a@b.com', password: '123' })
|
|
600
|
+
form.reset()
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### createPortal & ErrorBoundary
|
|
604
|
+
|
|
605
|
+
```tsx
|
|
606
|
+
import { createPortal, Show, ErrorBoundary } from 'weifuwu/client'
|
|
607
|
+
|
|
608
|
+
// Portal — render outside parent (modals, dropdowns)
|
|
609
|
+
<Show when={showModal}>
|
|
610
|
+
{createPortal(
|
|
611
|
+
<div class="fixed inset-0 bg-black/50">...</div>,
|
|
612
|
+
document.body,
|
|
613
|
+
)}
|
|
614
|
+
</Show>
|
|
615
|
+
|
|
616
|
+
// ErrorBoundary — catch render errors, children must be a thunk
|
|
617
|
+
<ErrorBoundary fallback={(e) => <p>Error: {e.message}</p>}>
|
|
618
|
+
{() => <Dashboard />}
|
|
619
|
+
</ErrorBoundary>
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### Pre-built Components
|
|
623
|
+
|
|
624
|
+
```tsx
|
|
625
|
+
import { LoginForm, Chat } from 'weifuwu/client'
|
|
626
|
+
|
|
627
|
+
function LoginPage(_, ctx) {
|
|
628
|
+
if (ctx.isAuthenticated) return ctx.app.navigate('/')
|
|
629
|
+
return <LoginForm />
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function ChatPage(_, ctx) {
|
|
633
|
+
return <Chat conversationId="123" />
|
|
634
|
+
}
|
|
635
|
+
```
|
|
922
636
|
|
|
923
637
|
---
|
|
924
638
|
|
|
925
|
-
##
|
|
639
|
+
## Router
|
|
926
640
|
|
|
927
641
|
```ts
|
|
928
|
-
|
|
642
|
+
const app = new Router()
|
|
929
643
|
|
|
930
|
-
|
|
931
|
-
app.
|
|
644
|
+
// HTTP
|
|
645
|
+
app.get(path, ...handlers)
|
|
646
|
+
app.post / put / delete / patch / head / options(path, ...handlers)
|
|
647
|
+
app.all(path, ...handlers)
|
|
932
648
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
649
|
+
// WebSocket
|
|
650
|
+
app.ws(path, ...middlewares, handler)
|
|
651
|
+
// handler: { open?, message?, close?, error? }
|
|
652
|
+
|
|
653
|
+
// Middleware & mounting
|
|
654
|
+
app.use(middleware)
|
|
655
|
+
app.mount(prefix, router)
|
|
656
|
+
app.plugin(fn)
|
|
657
|
+
app.onError(handler)
|
|
658
|
+
app.routes() // debug: list all routes
|
|
938
659
|
```
|
|
939
660
|
|
|
940
661
|
---
|
|
@@ -943,49 +664,49 @@ q.run()
|
|
|
943
664
|
|
|
944
665
|
```
|
|
945
666
|
src/
|
|
946
|
-
├── index.ts
|
|
947
|
-
├── types.ts
|
|
948
|
-
├── core/
|
|
949
|
-
├── middleware/
|
|
950
|
-
├── user/
|
|
951
|
-
├── messager/
|
|
952
|
-
├── kb/
|
|
953
|
-
├── ai/
|
|
954
|
-
├── cms/
|
|
955
|
-
├── base/
|
|
956
|
-
├── postgres/
|
|
957
|
-
├── redis/
|
|
958
|
-
├── queue/
|
|
959
|
-
├── graphql.ts
|
|
960
|
-
├── hub.ts
|
|
961
|
-
├── ui/
|
|
962
|
-
├── client/
|
|
963
|
-
│ ├── index.ts
|
|
964
|
-
│ ├── signal.ts
|
|
965
|
-
│ ├── jsx-runtime.ts
|
|
966
|
-
│ ├── app.ts
|
|
967
|
-
│ ├── router.ts
|
|
968
|
-
│ ├── types.ts
|
|
667
|
+
├── index.ts ← Entry, exports all modules
|
|
668
|
+
├── types.ts ← Context, Handler, Middleware types
|
|
669
|
+
├── core/ ← serve, router, ws, trace, logger
|
|
670
|
+
├── middleware/ ← cors, helmet, compress, rate-limit, upload, static, sandbox
|
|
671
|
+
├── user/ ← User system (CRUD, JWT, requireRole)
|
|
672
|
+
├── messager/ ← IM + AI conversation layer
|
|
673
|
+
├── kb/ ← RAG knowledge base
|
|
674
|
+
├── ai/ ← AI Agent (LLM, tools, RAG)
|
|
675
|
+
├── cms/ ← Content management
|
|
676
|
+
├── base/ ← Dynamic data engine
|
|
677
|
+
├── postgres/ ← PostgreSQL client
|
|
678
|
+
├── redis/ ← Redis client
|
|
679
|
+
├── queue/ ← Job queue + cron
|
|
680
|
+
├── graphql.ts ← GraphQL
|
|
681
|
+
├── hub.ts ← WebSocket hub
|
|
682
|
+
├── ui/ ← ctx.ui.html / ctx.ui.js / ctx.ui.css
|
|
683
|
+
├── client/ ← Frontend framework
|
|
684
|
+
│ ├── index.ts ← Exports all client APIs
|
|
685
|
+
│ ├── signal.ts ← Signal / effect / computed
|
|
686
|
+
│ ├── jsx-runtime.ts ← JSX → DOM + types + Show/For/wrap/ErrorBoundary/createPortal
|
|
687
|
+
│ ├── app.ts ← createApp / hydrate / middleware chain
|
|
688
|
+
│ ├── router.ts ← Route matching / RouteView / loader
|
|
689
|
+
│ ├── types.ts ← WfuiContext / RouteDef
|
|
969
690
|
│ ├── lib/
|
|
970
691
|
│ │ └── form.ts ← useForm
|
|
971
692
|
│ ├── middleware/
|
|
972
|
-
│ │ ├── api.ts
|
|
973
|
-
│ │ ├── auth.ts
|
|
974
|
-
│ │ └── ws.ts
|
|
693
|
+
│ │ ├── api.ts ← HTTP client
|
|
694
|
+
│ │ ├── auth.ts ← Login / logout / token
|
|
695
|
+
│ │ └── ws.ts ← WebSocket
|
|
975
696
|
│ └── components/
|
|
976
|
-
│ ├── LoginForm.ts
|
|
977
|
-
│ └── Chat.ts
|
|
978
|
-
└── test/
|
|
697
|
+
│ ├── LoginForm.ts
|
|
698
|
+
│ └── Chat.ts
|
|
699
|
+
└── test/ ← Tests
|
|
979
700
|
|
|
980
|
-
apps/demo/
|
|
981
|
-
├── src/main.tsx
|
|
982
|
-
├── server.ts
|
|
701
|
+
apps/demo/ ← Full-stack demo
|
|
702
|
+
├── src/main.tsx ← SPA + SSR hydrate demo pages
|
|
703
|
+
├── server.ts ← weifuwu server
|
|
983
704
|
├── public/
|
|
984
|
-
│ ├── index.html
|
|
985
|
-
│ └── style.css
|
|
705
|
+
│ ├── index.html ← HTML skeleton
|
|
706
|
+
│ └── style.css ← Demo styles (Tailwind)
|
|
986
707
|
└── tsconfig.json
|
|
987
708
|
|
|
988
|
-
docker-compose.yml
|
|
709
|
+
docker-compose.yml ← postgres (pgvector) + redis
|
|
989
710
|
```
|
|
990
711
|
|
|
991
712
|
---
|
|
@@ -996,5 +717,5 @@ docker-compose.yml ← postgres (pgvector) + redis
|
|
|
996
717
|
docker compose up -d # Start postgres + redis
|
|
997
718
|
npm run build # esbuild → dist/
|
|
998
719
|
npm run typecheck # tsc --noEmit
|
|
999
|
-
npm test #
|
|
720
|
+
npm test # Run tests
|
|
1000
721
|
```
|