weifuwu 0.33.9 → 0.33.10
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 +196 -530
- package/dist/client/app.d.ts +1 -3
- package/dist/client/index.d.ts +1 -171
- package/dist/client/index.js +76 -764
- package/dist/client/jsx-runtime.d.ts +11 -1
- package/dist/client/jsx-runtime.js +76 -764
- package/dist/client/middleware/ws.d.ts +2 -2
- package/dist/client/router.d.ts +1 -0
- package/dist/client/signal.d.ts +0 -36
- package/dist/client/types.d.ts +29 -30
- package/dist/core/router.d.ts +21 -11
- package/dist/core/ws.d.ts +9 -12
- package/dist/graphql.d.ts +2 -1
- package/dist/index.d.ts +2 -35
- package/dist/index.js +367 -3586
- package/dist/types.d.ts +0 -12
- package/package.json +2 -1
- package/dist/ai/agent.d.ts +0 -127
- package/dist/ai/index.d.ts +0 -26
- package/dist/ai/types.d.ts +0 -59
- package/dist/base/index.d.ts +0 -59
- package/dist/base/module.d.ts +0 -42
- package/dist/base/types.d.ts +0 -116
- package/dist/client/components/Chat.d.ts +0 -19
- package/dist/client/components/Link.d.ts +0 -27
- package/dist/client/components/LoginForm.d.ts +0 -16
- package/dist/client/components/Transition.d.ts +0 -42
- package/dist/client/lib/css.d.ts +0 -33
- package/dist/client/lib/dev.d.ts +0 -28
- package/dist/client/lib/form.d.ts +0 -56
- package/dist/client/lib/model.d.ts +0 -36
- package/dist/client/lib/resource.d.ts +0 -56
- package/dist/client/middleware/api.d.ts +0 -32
- package/dist/client/middleware/auth.d.ts +0 -32
- package/dist/cms/index.d.ts +0 -67
- package/dist/cms/module.d.ts +0 -38
- package/dist/cms/types.d.ts +0 -96
- package/dist/core/logger.d.ts +0 -16
- package/dist/core/trace.d.ts +0 -95
- package/dist/hub.d.ts +0 -36
- package/dist/kb/index.d.ts +0 -57
- package/dist/kb/module.d.ts +0 -37
- package/dist/kb/types.d.ts +0 -83
- package/dist/messager/index.d.ts +0 -72
- package/dist/messager/module.d.ts +0 -46
- package/dist/messager/types.d.ts +0 -99
- package/dist/middleware/compress.d.ts +0 -20
- package/dist/middleware/helmet.d.ts +0 -33
- package/dist/middleware/rate-limit.d.ts +0 -44
- package/dist/middleware/sandbox.d.ts +0 -52
- package/dist/middleware/upload.d.ts +0 -55
- package/dist/queue/cron.d.ts +0 -9
- package/dist/queue/index.d.ts +0 -12
- package/dist/queue/types.d.ts +0 -57
- package/dist/user/index.d.ts +0 -61
- package/dist/user/module.d.ts +0 -64
- package/dist/user/types.d.ts +0 -98
package/README.md
CHANGED
|
@@ -1,575 +1,288 @@
|
|
|
1
1
|
# weifuwu
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Web framework + reactive frontend** — `(req, ctx) => Response` + `(props, ctx) => JSX`
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install weifuwu
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
One package. Backend + frontend.
|
|
9
|
+
One package. Backend (`weifuwu`) + frontend (`weifuwu/client`). Minimal, composable, no magic.
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
## Core Concept: `ctx`
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Backend and frontend share the same pattern: middleware injects fields into `ctx`, handlers/components read from `ctx`.
|
|
16
16
|
|
|
17
17
|
```
|
|
18
|
-
|
|
19
|
-
Request →
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
ctx.agent ctx.user
|
|
26
|
-
ctx.redis ctx.app.navigate
|
|
27
|
-
ctx.messager ctx.provide/inject
|
|
28
|
-
ctx.cms
|
|
29
|
-
ctx.base
|
|
30
|
-
ctx.queue
|
|
31
|
-
ctx.ui
|
|
18
|
+
Backend: Frontend:
|
|
19
|
+
Request → Middleware → Handler createApp() → Middleware → Component
|
|
20
|
+
│ │
|
|
21
|
+
▼ ▼
|
|
22
|
+
ctx.sql ctx.ws
|
|
23
|
+
ctx.redis ctx.route
|
|
24
|
+
ctx.ui ctx.app.navigate
|
|
32
25
|
```
|
|
33
26
|
|
|
34
|
-
|
|
27
|
+
**Backend:**
|
|
35
28
|
```ts
|
|
36
|
-
//
|
|
37
|
-
app.
|
|
38
|
-
|
|
39
|
-
const result = await ctx.kb.search(query) // RAG 知识库
|
|
40
|
-
return ctx.agent.chatStreamResponse({ messages }) // AI 流式响应
|
|
41
|
-
})
|
|
29
|
+
app.use(postgres()) // → ctx.sql
|
|
30
|
+
app.use(redis()) // → ctx.redis
|
|
31
|
+
app.use(ui()) // → ctx.ui.html / ctx.ui.js / ctx.ui.css
|
|
42
32
|
```
|
|
43
33
|
|
|
44
|
-
|
|
34
|
+
**Frontend:**
|
|
45
35
|
```tsx
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
ctx.socket.send(data) // WebSocket
|
|
49
|
-
ctx.route.path // 当前路由
|
|
50
|
-
ctx.app.navigate('/about') // 页面导航
|
|
51
|
-
}
|
|
36
|
+
app.use(ws()) // → ctx.ws.send / onMessage / isConnected
|
|
37
|
+
app.use(router({ routes })) // → ctx.route.path / params / query
|
|
52
38
|
```
|
|
53
39
|
|
|
54
|
-
这种模式让开发者**无需 import 任何工具函数**,所有能力在 `ctx` 中一站获取。
|
|
55
|
-
|
|
56
40
|
---
|
|
57
41
|
|
|
58
|
-
##
|
|
59
|
-
|
|
60
|
-
### Backend — 每个模块向 ctx 注入什么
|
|
61
|
-
|
|
62
|
-
| Module | Import | Injects into `ctx` | Depends on | Purpose |
|
|
63
|
-
|--------|--------|-------------------|------------|---------|
|
|
64
|
-
| `postgres()` | `weifuwu` | `ctx.sql` | `DATABASE_URL` | PostgreSQL client |
|
|
65
|
-
| `redis()` | `weifuwu` | `ctx.redis` | `REDIS_URL` | Redis client |
|
|
66
|
-
| `user()` | `weifuwu` | `ctx.user`, `ctx.userModule` | `postgres()`, `JWT_SECRET` | Auth, JWT, roles |
|
|
67
|
-
| `messager()` | `weifuwu` | `ctx.messager` | `postgres()`, `user()` | IM + AI conversation |
|
|
68
|
-
| `kb()` | `weifuwu` | `ctx.kb` | `postgres()`, `DASHSCOPE_API_KEY` | RAG knowledge base |
|
|
69
|
-
| `agent()` | `weifuwu` | `ctx.agent` | — | LLM chat, tools, streaming |
|
|
70
|
-
| `cms()` | `weifuwu` | `ctx.cms` | `postgres()`, `user()` | Blog, docs, changelog |
|
|
71
|
-
| `base()` | `weifuwu` | `ctx.base` | `postgres()`, `user()` | Dynamic data engine |
|
|
72
|
-
| `queue()` | `weifuwu` | `ctx.queue` | `REDIS_URL` | Job queue + cron |
|
|
73
|
-
| `ui()` | `weifuwu` | `ctx.ui.html/js/css` | — | SSR/SPA rendering |
|
|
74
|
-
|
|
75
|
-
### Backend Middleware
|
|
76
|
-
|
|
77
|
-
| Middleware | Injects into `ctx` | Purpose |
|
|
78
|
-
|-----------|-------------------|---------|
|
|
79
|
-
| `cors()` | — | CORS headers |
|
|
80
|
-
| `helmet()` | — | Security headers |
|
|
81
|
-
| `compress()` | — | gzip / brotli / deflate |
|
|
82
|
-
| `rateLimit()` | — | Sliding-window rate limiter |
|
|
83
|
-
| `logger()` | — | Request logging |
|
|
84
|
-
| `upload()` | `ctx.upload` | Multipart file upload |
|
|
85
|
-
| `serveStatic()` | — | Static files |
|
|
86
|
-
| `sandbox()` | — | Filesystem isolation |
|
|
87
|
-
|
|
88
|
-
### Frontend (`weifuwu/client`) — 每个中间件向 ctx 注入什么
|
|
89
|
-
|
|
90
|
-
| Import | Type | Injects into `ctx` | Purpose |
|
|
91
|
-
|--------|------|-------------------|---------|
|
|
92
|
-
| `signal()` | function | — | Reactive state container |
|
|
93
|
-
| `computed()` | function | — | Derived signals |
|
|
94
|
-
| `effect()` | function | — | Auto-tracked side effects |
|
|
95
|
-
| `batch()` | function | — | Batch multiple signal writes |
|
|
96
|
-
| `untrack()` | function | — | Read signal without subscribing |
|
|
97
|
-
| `onMount()` | function | — | Component mount callback |
|
|
98
|
-
| `onCleanup()` | function | — | Component unmount callback |
|
|
99
|
-
| `api()` | **middleware** | `ctx.api.get/post/...` | HTTP client |
|
|
100
|
-
| `auth()` | **middleware** | `ctx.user/login/logout` | Auth state management |
|
|
101
|
-
| `socket()` | **middleware** | `ctx.socket.send/onMessage/...` | WebSocket client |
|
|
102
|
-
| `router()` | **middleware** | `ctx.route.path/params/query`, `ctx.app.navigate` | Hash/history router |
|
|
103
|
-
| `createApp()` | function | — | App instance |
|
|
104
|
-
| `<RouteView>` | component | — | Route outlet |
|
|
105
|
-
| `<Show>` | component | — | Conditional rendering |
|
|
106
|
-
| `<For>` | component | — | Keyed list rendering |
|
|
107
|
-
| `<Transition>` | component | — | Animated enter/leave |
|
|
108
|
-
| `<ErrorBoundary>` | component | — | Catch render errors |
|
|
109
|
-
| `<Link>` | component | — | SPA navigation link |
|
|
110
|
-
| `<LoginForm>` | component | — | Login/register form |
|
|
111
|
-
| `<Chat>` | component | — | Real-time messaging |
|
|
112
|
-
| `useForm()` | function | — | Form state management |
|
|
113
|
-
| `useModel()` | function | — | Two-way form binding |
|
|
114
|
-
| `reactiveArray()` | function | — | Reactive array with mut methods |
|
|
115
|
-
| `createResource()` | function | — | Async data (loading/error/data) |
|
|
116
|
-
| `createStyles()` | function | — | Scoped CSS |
|
|
117
|
-
| `createContext()` | function | — | Type-safe provide/inject |
|
|
118
|
-
| `createPortal()` | function | — | Render outside parent DOM |
|
|
119
|
-
| `wrap()` | function | — | Third-party lib integration |
|
|
120
|
-
| `enableDevtools()` | function | — | Dev warnings + browser inspector |
|
|
42
|
+
## Backend
|
|
121
43
|
|
|
122
|
-
|
|
44
|
+
### Exports
|
|
45
|
+
|
|
46
|
+
| Export | Type | Purpose |
|
|
47
|
+
|--------|------|---------|
|
|
48
|
+
| `Router` | class | HTTP router + middleware chain + `.ws()` + `.graphql()` |
|
|
49
|
+
| `serve` | function | HTTP server |
|
|
50
|
+
| `cors` | middleware | CORS headers |
|
|
51
|
+
| `postgres` | middleware | PostgreSQL client → `ctx.sql` |
|
|
52
|
+
| `redis` | middleware | Redis client → `ctx.redis` |
|
|
53
|
+
| `serveStatic` | middleware | Static file serving |
|
|
54
|
+
| `ui` | middleware | SSR/SPA rendering → `ctx.ui.html/css/js` |
|
|
55
|
+
| `HttpError` | class | HTTP error with status code |
|
|
56
|
+
| `DEFAULT_MAX_BODY` | constant | Default 10MB body limit |
|
|
57
|
+
| `MIGRATIONS_TABLE` | constant | Postgres migrations table name |
|
|
123
58
|
|
|
124
|
-
|
|
59
|
+
### Types
|
|
125
60
|
|
|
126
|
-
|
|
61
|
+
`Context`, `Handler`, `Middleware`, `ErrorHandler`, `WebSocket`, `WebSocketHandler`, `ServeOptions`, `Server`, `CORSOptions`, `ServeStaticOptions`, `PostgresOptions`, `PostgresClient`, `PostgresInjected`, `RedisOptions`, `RedisClient`, `RedisInjected`, `GraphQLOptions`, `GraphQLHandler`
|
|
62
|
+
|
|
63
|
+
### Quick Start
|
|
127
64
|
|
|
128
|
-
**后端 `server.ts`:**
|
|
129
65
|
```ts
|
|
130
|
-
import { serve, Router,
|
|
131
|
-
import { openai } from '@ai-sdk/openai'
|
|
66
|
+
import { serve, Router, cors, serveStatic, ui } from 'weifuwu'
|
|
132
67
|
|
|
133
68
|
const app = new Router()
|
|
134
69
|
app.use(cors())
|
|
135
|
-
app.use(
|
|
136
|
-
app.use(
|
|
137
|
-
app.use(user()) // → ctx.user, ctx.userModule
|
|
138
|
-
app.use(kb()) // → ctx.kb
|
|
139
|
-
app.use(agent({
|
|
140
|
-
model: openai('deepseek-v4-flash', { baseURL: 'https://api.deepseek.com/v1' }),
|
|
141
|
-
knowledge: { search: async (q, ctx) => ctx.kb.search(q) },
|
|
142
|
-
})) // → ctx.agent
|
|
143
|
-
app.use(messager()) // → ctx.messager
|
|
144
|
-
app.use(ui()) // → ctx.ui
|
|
145
|
-
|
|
146
|
-
// 业务 API — ctx 中所有能力可直接使用
|
|
147
|
-
app.post('/api/chat', async (req, ctx) => {
|
|
148
|
-
const { messages } = await req.json()
|
|
149
|
-
return ctx.agent.chatStreamResponse({ messages })
|
|
150
|
-
})
|
|
70
|
+
app.use(serveStatic('./public'))
|
|
71
|
+
app.use(ui())
|
|
151
72
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return Response.json(
|
|
73
|
+
// API routes
|
|
74
|
+
app.get('/api/hello', async (req, ctx) => {
|
|
75
|
+
return Response.json({ message: 'hello' })
|
|
155
76
|
})
|
|
156
77
|
|
|
157
|
-
//
|
|
158
|
-
app.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
78
|
+
// WebSocket
|
|
79
|
+
app.ws('/ws', {
|
|
80
|
+
open(ws) { ws.send('connected') },
|
|
81
|
+
message(ws, ctx, data) { ws.send(data.toString()) },
|
|
82
|
+
})
|
|
162
83
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
84
|
+
// GraphQL
|
|
85
|
+
app.graphql(async (req, ctx) => ({
|
|
86
|
+
schema: `type Query { hello: String }`,
|
|
87
|
+
resolvers: { Query: { hello: () => 'world' } },
|
|
88
|
+
graphiql: true,
|
|
89
|
+
}))
|
|
167
90
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
<main><RouteView /></main>
|
|
174
|
-
</div>
|
|
175
|
-
)
|
|
176
|
-
}
|
|
91
|
+
// SSR page
|
|
92
|
+
app.get('/blog/:slug', async (req, ctx) => ctx.ui.html`
|
|
93
|
+
<!DOCTYPE html>
|
|
94
|
+
<html><body><h1>${post.title}</h1></body></html>
|
|
95
|
+
`)
|
|
177
96
|
|
|
178
|
-
|
|
179
|
-
app.
|
|
180
|
-
app.use(auth()) // → ctx.user/login/logout
|
|
181
|
-
app.use(socket()) // → ctx.socket.send/onMessage
|
|
182
|
-
app.use(router({ routes })) // → ctx.route.path/params/query
|
|
183
|
-
app.mount('#root', AppShell)
|
|
184
|
-
```
|
|
97
|
+
// Dynamic JS compilation (no build step)
|
|
98
|
+
app.get('/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
|
|
185
99
|
|
|
186
|
-
|
|
187
|
-
```json
|
|
188
|
-
{ "jsx": "react-jsx", "jsxImportSource": "weifuwu/client" }
|
|
100
|
+
serve(app, { port: 3000 })
|
|
189
101
|
```
|
|
190
102
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
## Environment Variables
|
|
194
|
-
|
|
195
|
-
| Variable | Default | Used by |
|
|
196
|
-
|----------|---------|---------|
|
|
197
|
-
| `DATABASE_URL` | `postgres://root:123456@localhost:5432/demo` | `postgres()` |
|
|
198
|
-
| `REDIS_URL` | `redis://localhost:6379` | `redis()`, `queue()` |
|
|
199
|
-
| `JWT_SECRET` | — | `user()` |
|
|
200
|
-
| `DASHSCOPE_API_KEY` | — | `kb()` (embedding) |
|
|
201
|
-
| `DEEPSEEK_API_KEY` / `OPENAI_API_KEY` | — | `agent()` (LLM) |
|
|
202
|
-
| `DEEPSEEK_MODEL` | `deepseek-v4-flash` | `agent()` |
|
|
203
|
-
|
|
204
|
-
---
|
|
205
|
-
|
|
206
|
-
## Backend Modules
|
|
103
|
+
### Router
|
|
207
104
|
|
|
208
|
-
### postgres
|
|
209
105
|
```ts
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
app.
|
|
213
|
-
|
|
214
|
-
|
|
106
|
+
const app = new Router()
|
|
107
|
+
app.get(path, ...handlers)
|
|
108
|
+
app.post / put / delete / patch / head / options(path, ...handlers)
|
|
109
|
+
app.all(path, ...handlers)
|
|
110
|
+
app.ws(path, handler) // WebSocket
|
|
111
|
+
app.graphql(handler) // GraphQL at /
|
|
112
|
+
app.graphql('/graphql', handler) // GraphQL at /graphql
|
|
113
|
+
app.use(middleware) // Global middleware
|
|
114
|
+
app.mount(prefix, subRouter) // Sub-router
|
|
115
|
+
app.onError(handler) // Error handler
|
|
116
|
+
app.routes() // Debug: list all routes
|
|
215
117
|
```
|
|
216
|
-
| Method | Description |
|
|
217
|
-
|--------|-------------|
|
|
218
|
-
| `ctx.sql\`...\`` | Tagged template SQL |
|
|
219
|
-
| `ctx.sql.begin(fn)` | Transaction |
|
|
220
|
-
| `sql.close()` | Close pool |
|
|
221
118
|
|
|
222
|
-
###
|
|
223
|
-
```ts
|
|
224
|
-
import { redis } from 'weifuwu'
|
|
225
|
-
const r = redis()
|
|
226
|
-
app.use(r) // → ctx.redis
|
|
227
|
-
await ctx.redis.set('key', 'value')
|
|
228
|
-
await ctx.redis.get('key')
|
|
229
|
-
redis.close()
|
|
230
|
-
```
|
|
231
|
-
Reads `REDIS_URL` env (default: `redis://localhost:6379`).
|
|
119
|
+
### WebSocket
|
|
232
120
|
|
|
233
|
-
### user
|
|
234
121
|
```ts
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
// ctx.user 由 Authorization Bearer token 或 token cookie 自动解析
|
|
241
|
-
app.get('/api/me', async (req, ctx) => {
|
|
242
|
-
if (!ctx.user) return new Response('Unauthorized', { status: 401 })
|
|
243
|
-
return Response.json(ctx.user) // ctx.user.name / .email / .role
|
|
122
|
+
app.ws('/ws', {
|
|
123
|
+
open(ws, ctx) { ws.send('connected') },
|
|
124
|
+
message(ws, ctx, data) { /* data: string | Buffer */ },
|
|
125
|
+
close(ws, ctx) { /* cleanup */ },
|
|
126
|
+
error(ws, ctx, err) { /* log */ },
|
|
244
127
|
})
|
|
245
128
|
```
|
|
246
|
-
| `ctx.userModule.*` | Returns | Description |
|
|
247
|
-
|--------------------|---------|-------------|
|
|
248
|
-
| `register(input)` | `{ user, token }` | Register |
|
|
249
|
-
| `login(email, pw)` | `{ user, token } \| null` | Login |
|
|
250
|
-
| `getUserById(id)` | `UserRecord \| null` | Get by ID |
|
|
251
|
-
| `updateUser(id, input)` | `UserRecord \| null` | Update |
|
|
252
|
-
| `changePassword(id, oldPw, newPw)` | `boolean` | Change password |
|
|
253
|
-
| `deleteUser(id)` | `boolean` | Soft delete |
|
|
254
|
-
| `listUsers(inactive?)` | `UserRecord[]` | List users |
|
|
255
|
-
| `verifyToken(token)` | `TokenPayload \| null` | Verify JWT |
|
|
256
|
-
|
|
257
|
-
`requireRole('admin')` — guard middleware. No auth → 401, wrong role → 403.
|
|
258
|
-
|
|
259
|
-
### messager
|
|
260
|
-
```ts
|
|
261
|
-
import { messager } from 'weifuwu'
|
|
262
|
-
app.use(postgres())
|
|
263
|
-
app.use(user())
|
|
264
|
-
app.use(messager()) // → ctx.messager
|
|
265
|
-
```
|
|
266
|
-
| `ctx.messager.*` | Returns | Description |
|
|
267
|
-
|------------------|---------|-------------|
|
|
268
|
-
| `sendMessage(convId, body)` | `Message` | Send + WebSocket broadcast |
|
|
269
|
-
| `getMessages(convId, opts?)` | `Message[]` | Cursor pagination |
|
|
270
|
-
| `getConversations()` | `Conversation[]` | List with unread |
|
|
271
|
-
| `createDirectConversation(userId)` | `Conversation` | Create/reuse DM |
|
|
272
|
-
| `createGroupConversation(title, userIds)` | `Conversation` | Create group |
|
|
273
|
-
| `editMessage(msgId, body)` | `Message \| null` | Edit (24h) |
|
|
274
|
-
| `deleteMessage(msgId)` | `boolean` | Soft delete |
|
|
275
|
-
| `markRead(convId)` | `void` | Mark as read |
|
|
276
|
-
|
|
277
|
-
### kb — Knowledge Base
|
|
278
|
-
```ts
|
|
279
|
-
import { kb } from 'weifuwu'
|
|
280
|
-
app.use(postgres())
|
|
281
|
-
app.use(kb()) // → ctx.kb
|
|
282
|
-
```
|
|
283
|
-
| `ctx.kb.*` | Returns | Description |
|
|
284
|
-
|------------|---------|-------------|
|
|
285
|
-
| `importText(title, text)` | `{ document, chunks }` | Import → chunk → embed |
|
|
286
|
-
| `search(query, opts?)` | `SearchResult[]` | Semantic search |
|
|
287
|
-
| `list()` | `Document[]` | List documents |
|
|
288
|
-
| `get(id)` | `Document \| null` | Get document |
|
|
289
|
-
| `delete(id)` | `boolean` | Delete + cascade chunks |
|
|
290
|
-
|
|
291
|
-
### agent
|
|
292
|
-
```ts
|
|
293
|
-
import { agent } from 'weifuwu'
|
|
294
|
-
import { openai } from '@ai-sdk/openai'
|
|
295
|
-
import { tool } from 'ai'
|
|
296
|
-
import { z } from 'zod'
|
|
297
|
-
|
|
298
|
-
app.use(agent({
|
|
299
|
-
model: openai('deepseek-v4-flash', { baseURL: 'https://api.deepseek.com/v1' }),
|
|
300
|
-
system: 'You are a helpful assistant.',
|
|
301
|
-
knowledge: { search: async (q, ctx) => ctx.kb.search(q) },
|
|
302
|
-
tools: {
|
|
303
|
-
getWeather: tool({
|
|
304
|
-
description: 'Get weather for a city',
|
|
305
|
-
parameters: z.object({ city: z.string() }),
|
|
306
|
-
execute: async ({ city }) => ({ temp: 22, unit: 'C' }),
|
|
307
|
-
}),
|
|
308
|
-
},
|
|
309
|
-
maxSteps: 5,
|
|
310
|
-
})) // → ctx.agent
|
|
311
|
-
```
|
|
312
|
-
| `ctx.agent.*` | Description |
|
|
313
|
-
|--------------|-------------|
|
|
314
|
-
| `chat(prompt, opts?)` | Non-streaming, returns text |
|
|
315
|
-
| `chatStreamResponse({ messages })` | SSE stream (useChat compatible) |
|
|
316
129
|
|
|
317
|
-
###
|
|
318
|
-
```ts
|
|
319
|
-
import { cms, requireRole } from 'weifuwu'
|
|
320
|
-
app.use(postgres())
|
|
321
|
-
app.use(user())
|
|
322
|
-
app.use(cms()) // → ctx.cms
|
|
323
|
-
```
|
|
324
|
-
| `ctx.cms.*` | Returns | Description |
|
|
325
|
-
|-------------|---------|-------------|
|
|
326
|
-
| `create(input)` | `Content` | Create (admin) |
|
|
327
|
-
| `get(slug)` | `Content \| null` | Get by slug |
|
|
328
|
-
| `list(opts?)` | `Content[]` | List with filters |
|
|
329
|
-
| `publish(id)` / `unpublish(id)` | `Content \| null` | Toggle status |
|
|
330
|
-
|
|
331
|
-
### base — Dynamic Data Engine
|
|
332
|
-
```ts
|
|
333
|
-
app.use(base()) // → ctx.base
|
|
334
|
-
ctx.base.create({ name, tables })
|
|
335
|
-
ctx.base.insert(baseId, table, data)
|
|
336
|
-
ctx.base.query(baseId, table, { filter, limit })
|
|
337
|
-
```
|
|
130
|
+
### GraphQL
|
|
338
131
|
|
|
339
|
-
### queue
|
|
340
132
|
```ts
|
|
341
|
-
|
|
342
|
-
app.
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
133
|
+
// At root
|
|
134
|
+
app.graphql(async (req, ctx) => ({
|
|
135
|
+
schema: `type Query { hello: String }`,
|
|
136
|
+
resolvers: { Query: { hello: () => 'world' } },
|
|
137
|
+
graphiql: true,
|
|
138
|
+
}))
|
|
139
|
+
|
|
140
|
+
// Or at a custom path
|
|
141
|
+
app.graphql('/graphql', handler)
|
|
347
142
|
```
|
|
348
143
|
|
|
349
|
-
###
|
|
144
|
+
### Graceful Shutdown
|
|
145
|
+
|
|
350
146
|
```ts
|
|
351
|
-
app
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
// Dynamic JS compilation (no build step)
|
|
355
|
-
app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
|
|
356
|
-
// CSS with Tailwind v4 support
|
|
357
|
-
app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./public/style.css'))
|
|
147
|
+
const srv = serve(app)
|
|
148
|
+
// Ctrl+C / SIGTERM immediately closes all connections and exits
|
|
149
|
+
await srv.stop() // Programmatic stop
|
|
358
150
|
```
|
|
359
151
|
|
|
360
152
|
---
|
|
361
153
|
|
|
362
|
-
## Frontend
|
|
154
|
+
## Frontend (`weifuwu/client`)
|
|
155
|
+
|
|
156
|
+
16 runtime exports, zero dependencies, zero virtual DOM.
|
|
157
|
+
|
|
158
|
+
| Export | Type | Purpose |
|
|
159
|
+
|--------|------|---------|
|
|
160
|
+
| `signal`, `computed`, `effect`, `batch` | function | Reactive state system |
|
|
161
|
+
| `jsx`/`jsxs`/`jsxDEV` | function | JSX compilation target |
|
|
162
|
+
| `Fragment` | component | `<></>` |
|
|
163
|
+
| `Show` | component | Conditional rendering |
|
|
164
|
+
| `For` | component | Keyed list rendering |
|
|
165
|
+
| `onMount`, `onCleanup` | function | Lifecycle hooks |
|
|
166
|
+
| `createApp` | function | App instance with middleware chain |
|
|
167
|
+
| `router` | middleware | Hash/history router → `ctx.route` |
|
|
168
|
+
| `RouteView` | component | Route outlet |
|
|
169
|
+
| `ws` | middleware | WebSocket client → `ctx.ws` |
|
|
363
170
|
|
|
364
|
-
|
|
171
|
+
Types: `Signal`, `Component`, `WfuiContext`, `AppMiddleware`, `RouteDef`
|
|
365
172
|
|
|
366
|
-
###
|
|
173
|
+
### Quick Start
|
|
367
174
|
|
|
368
175
|
```tsx
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const doubled = computed(() => count.value * 2)
|
|
372
|
-
effect(() => console.log('count:', count.value))
|
|
176
|
+
import { signal, Show, For, createApp, ws, router, RouteView } from 'weifuwu/client'
|
|
177
|
+
import type { WfuiContext, RouteDef } from 'weifuwu/client'
|
|
373
178
|
|
|
374
|
-
|
|
375
|
-
|
|
179
|
+
const routes: RouteDef[] = [
|
|
180
|
+
{ path: '/', component: HomePage },
|
|
181
|
+
{ path: '/hello/:name', component: HelloPage },
|
|
182
|
+
]
|
|
376
183
|
|
|
377
|
-
|
|
378
|
-
|
|
184
|
+
const app = createApp()
|
|
185
|
+
app.use(ws())
|
|
186
|
+
app.use(router({ routes, mode: 'hash' }))
|
|
187
|
+
app.mount('#root', AppShell)
|
|
379
188
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
189
|
+
function AppShell(_props: {}, ctx: WfuiContext) {
|
|
190
|
+
return (
|
|
191
|
+
<div>
|
|
192
|
+
<nav>...</nav>
|
|
193
|
+
<main><RouteView /></main>
|
|
194
|
+
</div>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
383
197
|
```
|
|
384
198
|
|
|
385
|
-
###
|
|
199
|
+
### Signal
|
|
386
200
|
|
|
387
201
|
```tsx
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
items.clear() // []
|
|
393
|
-
items.sort()
|
|
394
|
-
items.reverse()
|
|
202
|
+
const count = signal(0)
|
|
203
|
+
const doubled = computed(() => count.value * 2)
|
|
204
|
+
effect(() => console.log('count:', count.value))
|
|
205
|
+
batch(() => { a.value = 1; b.value = 2 })
|
|
395
206
|
```
|
|
396
207
|
|
|
397
208
|
### Control Flow
|
|
398
209
|
|
|
399
210
|
```tsx
|
|
400
|
-
|
|
401
|
-
<Show when={isLoggedIn} fallback={<LoginPage />}>
|
|
211
|
+
<Show when={isLoggedIn} fallback={<Login />}>
|
|
402
212
|
<Dashboard />
|
|
403
213
|
</Show>
|
|
404
214
|
|
|
405
|
-
|
|
406
|
-
<
|
|
407
|
-
{(todo) => <TodoItem todo={todo} />}
|
|
215
|
+
<For each={items} keyBy="id">
|
|
216
|
+
{(item) => <div>{item.name}</div>}
|
|
408
217
|
</For>
|
|
409
|
-
|
|
410
|
-
// Transition — animated enter/leave
|
|
411
|
-
<Transition show={isOpen} name="fade">
|
|
412
|
-
<Modal />
|
|
413
|
-
</Transition>
|
|
414
|
-
|
|
415
|
-
// ErrorBoundary — catch render errors
|
|
416
|
-
<ErrorBoundary fallback={(e) => <p>{e.message}</p>} onError={reportError}>
|
|
417
|
-
{() => <Dashboard />}
|
|
418
|
-
</ErrorBoundary>
|
|
419
218
|
```
|
|
420
219
|
|
|
421
|
-
###
|
|
220
|
+
### WebSocket (`ctx.ws`)
|
|
422
221
|
|
|
423
222
|
```tsx
|
|
424
|
-
|
|
425
|
-
const form = useForm({
|
|
426
|
-
initial: { email: '', password: '' },
|
|
427
|
-
validate: { email: (v) => !v && '必填' },
|
|
428
|
-
validateOnInit: true,
|
|
429
|
-
})
|
|
430
|
-
<input {...form.field('email')} placeholder="邮箱" />
|
|
431
|
-
<button onClick={() => form.submit(data => ctx.login(data))}>登录</button>
|
|
432
|
-
|
|
433
|
-
// useModel — two-way binding (shorter)
|
|
434
|
-
const name = signal('')
|
|
435
|
-
const agreed = signal(false)
|
|
436
|
-
<input {...useModel(name)} placeholder="姓名" />
|
|
437
|
-
<input type="checkbox" {...useModel(agreed)} /> 同意
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
### Async Data
|
|
223
|
+
app.use(ws({ url: '/ws' }))
|
|
441
224
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
)
|
|
447
|
-
|
|
448
|
-
<Show when={loading}><Skeleton /></Show>
|
|
449
|
-
<Show when={error}><Error /></Show>
|
|
450
|
-
<For each={posts}>{(post) => <PostCard post={post} />}</For>
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
### Scoped CSS
|
|
454
|
-
|
|
455
|
-
```tsx
|
|
456
|
-
const s = createStyles({
|
|
457
|
-
card: 'background: white; border-radius: 8px; padding: 16px;',
|
|
458
|
-
title: 'font-size: 18px; color: #333;',
|
|
459
|
-
})
|
|
460
|
-
<div class={s.card}><h2 class={s.title}>...</h2></div>
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
### Type-Safe Context
|
|
464
|
-
|
|
465
|
-
```tsx
|
|
466
|
-
const ThemeCtx = createContext<string>('theme')
|
|
467
|
-
ThemeCtx.provide(ctx, 'dark') // 提供
|
|
468
|
-
const theme = ThemeCtx.inject(ctx) // string | null
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
### Third-Party Integration
|
|
472
|
-
|
|
473
|
-
```tsx
|
|
474
|
-
const PieChart = wrap('div', (el, props, ctx) => {
|
|
475
|
-
const chart = echarts.init(el)
|
|
476
|
-
chart.setOption({ series: [{ type: 'pie', data: props.data }] })
|
|
477
|
-
effect(() => chart.setOption(...))
|
|
478
|
-
return () => chart.dispose()
|
|
225
|
+
// In component:
|
|
226
|
+
onMount(() => {
|
|
227
|
+
const unsub = ctx.ws.onMessage((data) => { ... })
|
|
228
|
+
onCleanup(() => unsub())
|
|
479
229
|
})
|
|
480
|
-
|
|
230
|
+
ctx.ws.send({ type: 'chat', body: 'hello' })
|
|
231
|
+
<Show when={ctx.ws.isConnected}>🟢 已连接</Show>
|
|
481
232
|
```
|
|
482
233
|
|
|
483
|
-
###
|
|
234
|
+
### Router (`ctx.route`)
|
|
484
235
|
|
|
485
236
|
```tsx
|
|
486
|
-
|
|
237
|
+
app.use(router({ routes, notFound: NotFound, mode: 'hash' }))
|
|
487
238
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
239
|
+
ctx.route.path // '/hello/world'
|
|
240
|
+
ctx.route.params // { name: 'world' }
|
|
241
|
+
ctx.route.query // { tab: 'intro' }
|
|
242
|
+
ctx.app.navigate('/hello/world')
|
|
491
243
|
```
|
|
492
244
|
|
|
493
|
-
###
|
|
245
|
+
### Lifecycle
|
|
494
246
|
|
|
495
247
|
```tsx
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
]
|
|
502
|
-
|
|
503
|
-
app.use(router({ routes, notFound: NotFound, mode: 'hash', transition: 'page' }))
|
|
504
|
-
// ctx.route.path / .params / .query / .data / .loading
|
|
505
|
-
|
|
506
|
-
// 路由出口
|
|
507
|
-
function AppShell() { return <main><RouteView /></main> }
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
### DevTools
|
|
511
|
-
|
|
512
|
-
```ts
|
|
513
|
-
import { enableDevtools } from 'weifuwu/client'
|
|
514
|
-
if (import.meta.env.DEV) enableDevtools()
|
|
515
|
-
|
|
516
|
-
// 浏览器控制台:
|
|
517
|
-
// __wefu__.inspect() → 查看所有 signal
|
|
518
|
-
// __wefu__.warnings() → 切换开发警告
|
|
248
|
+
onMount(() => {
|
|
249
|
+
init()
|
|
250
|
+
return () => cleanup() // Auto-cleanup on unmount
|
|
251
|
+
})
|
|
252
|
+
onCleanup(() => clearInterval(id))
|
|
519
253
|
```
|
|
520
254
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
## From React to weifuwu/client
|
|
255
|
+
### From React
|
|
524
256
|
|
|
525
257
|
| React | weifuwu/client |
|
|
526
258
|
|-------|----------------|
|
|
527
259
|
| `useState(0)` | `signal(0)` |
|
|
528
|
-
| `useMemo(() => a
|
|
529
|
-
| `useEffect(() =>
|
|
530
|
-
| `
|
|
260
|
+
| `useMemo(() => a*2, [a])` | `computed(() => a.value * 2)` |
|
|
261
|
+
| `useEffect(() => f, [])` | `onMount(f)` |
|
|
262
|
+
| `useEffect(() => f, [dep])` | `effect(f)` |
|
|
263
|
+
| `{cond && <X/>}` | `<Show when={cond}><X/></Show>` |
|
|
531
264
|
| `{items.map(i => <X/>)}` | `<For each={items}>{(i) => <X/>}</For>` |
|
|
532
|
-
| `
|
|
533
|
-
| Context Provider | `ctx.provide(key, val)` / `createContext()` |
|
|
534
|
-
| `useNavigate()` | `ctx.app.navigate(path)` |
|
|
265
|
+
| `useNavigate()` | `ctx.app.navigate()` |
|
|
535
266
|
| `useParams()` | `ctx.route.params` |
|
|
536
|
-
| `fetch / axios` | `ctx.api.get/post/...` |
|
|
537
|
-
| WebSocket | `ctx.socket.send/onMessage` |
|
|
538
|
-
|
|
539
|
-
**What you DON'T need:**
|
|
540
|
-
- No hooks rules — no `use` prefix, no rules-of-hooks
|
|
541
|
-
- No virtual DOM — JSX creates real DOM directly
|
|
542
|
-
- No dependency arrays — `effect()` auto-tracks
|
|
543
|
-
- No state management library — signal is the state management
|
|
544
|
-
- No Context.Provider component — just `ctx.provide/inject`
|
|
545
267
|
|
|
546
268
|
---
|
|
547
269
|
|
|
548
|
-
##
|
|
270
|
+
## Demo
|
|
549
271
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
| `trace()` | Request tracing |
|
|
272
|
+
```bash
|
|
273
|
+
cd apps/demo
|
|
274
|
+
node server.ts
|
|
275
|
+
# http://localhost:3000
|
|
276
|
+
```
|
|
556
277
|
|
|
557
278
|
---
|
|
558
279
|
|
|
559
|
-
##
|
|
280
|
+
## Environment Variables
|
|
560
281
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
app.all(path, ...handlers)
|
|
566
|
-
app.ws(path, ...handler) // WebSocket
|
|
567
|
-
app.use(middleware) // Global middleware
|
|
568
|
-
app.mount(prefix, router) // Sub-router
|
|
569
|
-
app.plugin(fn) // Plugin
|
|
570
|
-
app.onError(handler) // Error handler
|
|
571
|
-
app.routes() // Debug: list all routes
|
|
572
|
-
```
|
|
282
|
+
| Variable | Default | Used by |
|
|
283
|
+
|----------|---------|---------|
|
|
284
|
+
| `DATABASE_URL` | `postgres://root:123456@localhost:5432/demo` | `postgres()` |
|
|
285
|
+
| `REDIS_URL` | `redis://localhost:6379` | `redis()` |
|
|
573
286
|
|
|
574
287
|
---
|
|
575
288
|
|
|
@@ -577,75 +290,28 @@ app.routes() // Debug: list all routes
|
|
|
577
290
|
|
|
578
291
|
```
|
|
579
292
|
src/
|
|
580
|
-
├── index.ts
|
|
581
|
-
├── types.ts
|
|
582
|
-
├── core/
|
|
583
|
-
├── middleware/
|
|
584
|
-
├──
|
|
585
|
-
├──
|
|
586
|
-
├──
|
|
587
|
-
├──
|
|
588
|
-
├──
|
|
589
|
-
├──
|
|
590
|
-
├──
|
|
591
|
-
├──
|
|
592
|
-
├──
|
|
593
|
-
├──
|
|
594
|
-
├──
|
|
595
|
-
|
|
596
|
-
├──
|
|
597
|
-
|
|
598
|
-
│ ├── signal.ts ← Signal / effect / computed / batch / untrack
|
|
599
|
-
│ ├── jsx-runtime.ts ← JSX → DOM + types + Show/For/wrap/ErrorBoundary/Portal/Transition
|
|
600
|
-
│ ├── app.ts ← createApp / hydrate / middleware chain
|
|
601
|
-
│ ├── router.ts ← Route matching / RouteView / loader / transition
|
|
602
|
-
│ ├── types.ts ← WfuiContext / RouteDef / createContext
|
|
603
|
-
│ ├── lib/
|
|
604
|
-
│ │ ├── form.ts ← useForm (validated forms)
|
|
605
|
-
│ │ ├── model.ts ← useModel (two-way binding)
|
|
606
|
-
│ │ ├── resource.ts ← createResource (async data)
|
|
607
|
-
│ │ ├── css.ts ← createStyles (scoped CSS)
|
|
608
|
-
│ │ └── dev.ts ← enableDevtools (dev warnings)
|
|
609
|
-
│ ├── middleware/
|
|
610
|
-
│ │ ├── api.ts ← HTTP client → ctx.api
|
|
611
|
-
│ │ ├── auth.ts ← Auth → ctx.user/login/logout
|
|
612
|
-
│ │ └── ws.ts ← WebSocket → ctx.socket
|
|
613
|
-
│ └── components/
|
|
614
|
-
│ ├── LoginForm.tsx ← Login/register
|
|
615
|
-
│ ├── Chat.tsx ← Real-time chat
|
|
616
|
-
│ ├── Link.tsx ← SPA navigation
|
|
617
|
-
│ └── Transition.tsx ← Animated enter/leave
|
|
618
|
-
└── test/ ← 66 unit tests + 10 benchmarks
|
|
619
|
-
|
|
620
|
-
apps/demo/ ← Full-stack demo
|
|
621
|
-
├── server.ts ← weifuwu server (user, kb, agent, messager 未演示)
|
|
622
|
-
├── src/main.tsx ← SPA + SSR hydrate demo pages
|
|
623
|
-
├── public/ ← style.css (Tailwind + transition classes)
|
|
624
|
-
└── tsconfig.json
|
|
625
|
-
|
|
626
|
-
docker-compose.yml ← postgres (pgvector) + redis
|
|
293
|
+
├── index.ts Entry, exports
|
|
294
|
+
├── types.ts Context, Handler, Middleware
|
|
295
|
+
├── core/ Router, serve, WebSocket upgrade
|
|
296
|
+
├── middleware/ cors, serveStatic
|
|
297
|
+
├── postgres/ PostgreSQL client
|
|
298
|
+
├── redis/ Redis client
|
|
299
|
+
├── ui/ ctx.ui.html/js/css
|
|
300
|
+
├── graphql.ts GraphQL + router.graphql()
|
|
301
|
+
├── client/ Frontend framework
|
|
302
|
+
│ ├── index.ts 16 runtime exports
|
|
303
|
+
│ ├── signal.ts signal/computed/effect/batch
|
|
304
|
+
│ ├── jsx-runtime.ts JSX → DOM + Show/For/Fragment/Portal
|
|
305
|
+
│ ├── app.ts createApp
|
|
306
|
+
│ ├── router.ts Router + RouteView
|
|
307
|
+
│ ├── types.ts WfuiContext + types
|
|
308
|
+
│ └── middleware/ws.ts WebSocket client
|
|
309
|
+
├── test/ 47 backend + 77 frontend tests
|
|
310
|
+
apps/demo/ Full-stack demo
|
|
627
311
|
```
|
|
628
312
|
|
|
629
|
-
---
|
|
630
|
-
|
|
631
|
-
## Development
|
|
632
|
-
|
|
633
313
|
```bash
|
|
634
|
-
|
|
635
|
-
npm run
|
|
636
|
-
npm
|
|
637
|
-
npm test # Run all tests
|
|
314
|
+
npm run build # esbuild → dist/
|
|
315
|
+
npm run typecheck # tsc --noEmit
|
|
316
|
+
npm test # Run all tests
|
|
638
317
|
```
|
|
639
|
-
|
|
640
|
-
---
|
|
641
|
-
|
|
642
|
-
## Performance (Client Benchmarks)
|
|
643
|
-
|
|
644
|
-
| Operation | Throughput |
|
|
645
|
-
|-----------|-----------|
|
|
646
|
-
| Signal creation | ~10,000 ops/ms |
|
|
647
|
-
| Signal read/write | ~9,600 ops/ms |
|
|
648
|
-
| Notify 10,000 effects | ~2,600 ops/ms |
|
|
649
|
-
| batch merge 10,000 writes | ~0.6ms |
|
|
650
|
-
| JSX div creation | ~200 ops/ms |
|
|
651
|
-
| For render 10,000 items | ~109 ops/ms |
|