weifuwu 0.33.8 → 0.33.9
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 +390 -460
- package/dist/client/components/Chat.d.ts +7 -5
- package/dist/client/components/Link.d.ts +27 -0
- package/dist/client/components/LoginForm.d.ts +6 -4
- package/dist/client/components/Transition.d.ts +42 -0
- package/dist/client/index.d.ts +288 -8
- package/dist/client/index.js +667 -141
- package/dist/client/jsx-runtime.d.ts +53 -3
- package/dist/client/jsx-runtime.js +667 -141
- package/dist/client/lib/css.d.ts +33 -0
- package/dist/client/lib/dev.d.ts +28 -0
- package/dist/client/lib/form.d.ts +2 -0
- package/dist/client/lib/model.d.ts +36 -0
- package/dist/client/lib/resource.d.ts +56 -0
- package/dist/client/router.d.ts +10 -0
- package/dist/client/signal.d.ts +82 -0
- package/dist/client/types.d.ts +27 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -10,333 +10,285 @@ One package. Backend + frontend. User system, messaging, RAG knowledge base, AI
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Core Concept: `ctx`
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
**`ctx` 是整个框架的核心模式。** 后端和前端共享同一个理念:通过中间件向 `ctx` 注入能力,组件/handler 直接从 `ctx` 读取。
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
```
|
|
18
|
+
后端: 前端:
|
|
19
|
+
Request → 中间件链 → Handler createApp() → 中间件链 → 组件
|
|
20
|
+
│ │
|
|
21
|
+
▼ ▼
|
|
22
|
+
ctx.sql ctx.api
|
|
23
|
+
ctx.user ctx.socket
|
|
24
|
+
ctx.kb ctx.route
|
|
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
|
|
32
|
+
```
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
**后端每个请求创建一个 ctx:**
|
|
35
|
+
```ts
|
|
36
|
+
// 中间件注入 → handler 直接使用
|
|
37
|
+
app.get('/api/chat', async (req, ctx) => {
|
|
38
|
+
const user = ctx.user // 当前用户(auto-resolve from JWT)
|
|
39
|
+
const result = await ctx.kb.search(query) // RAG 知识库
|
|
40
|
+
return ctx.agent.chatStreamResponse({ messages }) // AI 流式响应
|
|
41
|
+
})
|
|
42
|
+
```
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
44
|
+
**前端每个组件通过第二个参数接收 ctx:**
|
|
45
|
+
```tsx
|
|
46
|
+
function ChatPage(_props: {}, ctx: WfuiContext) {
|
|
47
|
+
ctx.api.get('/api/posts') // HTTP 客户端
|
|
48
|
+
ctx.socket.send(data) // WebSocket
|
|
49
|
+
ctx.route.path // 当前路由
|
|
50
|
+
ctx.app.navigate('/about') // 页面导航
|
|
51
|
+
}
|
|
52
|
+
```
|
|
68
53
|
|
|
69
|
-
|
|
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 |
|
|
54
|
+
这种模式让开发者**无需 import 任何工具函数**,所有能力在 `ctx` 中一站获取。
|
|
75
55
|
|
|
76
56
|
---
|
|
77
57
|
|
|
78
|
-
##
|
|
58
|
+
## Module Overview
|
|
79
59
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
|
83
|
-
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
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 |
|
|
88
121
|
|
|
89
122
|
---
|
|
90
123
|
|
|
91
124
|
## Quick Start
|
|
92
125
|
|
|
93
|
-
###
|
|
126
|
+
### 完整全栈示例 — ctx 贯穿前后端
|
|
94
127
|
|
|
128
|
+
**后端 `server.ts`:**
|
|
95
129
|
```ts
|
|
96
|
-
import { serve, Router, postgres, user,
|
|
130
|
+
import { serve, Router, postgres, user, agent, kb, messager, ui, cors, logger } from 'weifuwu'
|
|
97
131
|
import { openai } from '@ai-sdk/openai'
|
|
98
132
|
|
|
99
133
|
const app = new Router()
|
|
100
|
-
app.use(
|
|
101
|
-
app.use(
|
|
102
|
-
app.use(
|
|
103
|
-
app.use(
|
|
134
|
+
app.use(cors())
|
|
135
|
+
app.use(logger())
|
|
136
|
+
app.use(postgres()) // → ctx.sql
|
|
137
|
+
app.use(user()) // → ctx.user, ctx.userModule
|
|
138
|
+
app.use(kb()) // → ctx.kb
|
|
104
139
|
app.use(agent({
|
|
105
140
|
model: openai('deepseek-v4-flash', { baseURL: 'https://api.deepseek.com/v1' }),
|
|
106
141
|
knowledge: { search: async (q, ctx) => ctx.kb.search(q) },
|
|
107
|
-
}))
|
|
142
|
+
})) // → ctx.agent
|
|
143
|
+
app.use(messager()) // → ctx.messager
|
|
144
|
+
app.use(ui()) // → ctx.ui
|
|
108
145
|
|
|
146
|
+
// 业务 API — ctx 中所有能力可直接使用
|
|
109
147
|
app.post('/api/chat', async (req, ctx) => {
|
|
110
148
|
const { messages } = await req.json()
|
|
111
149
|
return ctx.agent.chatStreamResponse({ messages })
|
|
112
150
|
})
|
|
113
151
|
|
|
152
|
+
app.post('/api/messages', async (req, ctx) => {
|
|
153
|
+
const msg = await ctx.messager.sendMessage(ctx.params.conversationId, req.body)
|
|
154
|
+
return Response.json(msg, { status: 201 })
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// 客户端编译(无需本地构建步骤)
|
|
158
|
+
app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
|
|
159
|
+
|
|
114
160
|
serve(app, { port: 3000 })
|
|
115
161
|
```
|
|
116
162
|
|
|
117
|
-
|
|
118
|
-
|
|
163
|
+
**前端 `src/main.tsx` — ctx 驱动组件:**
|
|
119
164
|
```tsx
|
|
120
|
-
import { signal, createApp, api, auth,
|
|
165
|
+
import { signal, createApp, api, auth, socket, router, RouteView, LoginForm, Link } from 'weifuwu/client'
|
|
121
166
|
import type { WfuiContext } from 'weifuwu/client'
|
|
122
167
|
|
|
123
168
|
function AppShell(_props: {}, ctx: WfuiContext) {
|
|
124
169
|
if (!ctx.isAuthenticated) return <LoginForm />
|
|
125
170
|
return (
|
|
126
171
|
<div>
|
|
127
|
-
<nav><
|
|
172
|
+
<nav><Link to="/chat">Chat</Link></nav>
|
|
128
173
|
<main><RouteView /></main>
|
|
129
174
|
</div>
|
|
130
175
|
)
|
|
131
176
|
}
|
|
132
177
|
|
|
133
178
|
const app = createApp()
|
|
134
|
-
app.use(api())
|
|
135
|
-
app.use(auth())
|
|
136
|
-
app.use(
|
|
137
|
-
app.use(router({ routes }))
|
|
179
|
+
app.use(api()) // → ctx.api.get/post/...
|
|
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
|
|
138
183
|
app.mount('#root', AppShell)
|
|
139
184
|
```
|
|
140
185
|
|
|
186
|
+
**`tsconfig.json`:**
|
|
141
187
|
```json
|
|
142
|
-
// tsconfig.json
|
|
143
188
|
{ "jsx": "react-jsx", "jsxImportSource": "weifuwu/client" }
|
|
144
189
|
```
|
|
145
190
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
```js
|
|
149
|
-
import esbuild from 'esbuild'
|
|
150
|
-
esbuild.build({
|
|
151
|
-
entryPoints: ['src/main.tsx'],
|
|
152
|
-
jsx: 'automatic',
|
|
153
|
-
jsxImportSource: 'weifuwu/client',
|
|
154
|
-
bundle: true,
|
|
155
|
-
})
|
|
156
|
-
```
|
|
191
|
+
---
|
|
157
192
|
|
|
158
|
-
|
|
193
|
+
## Environment Variables
|
|
159
194
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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()` |
|
|
164
203
|
|
|
165
204
|
---
|
|
166
205
|
|
|
167
206
|
## Backend Modules
|
|
168
207
|
|
|
169
|
-
Each module follows: import → usage → API table.
|
|
170
|
-
|
|
171
208
|
### postgres
|
|
172
|
-
|
|
173
209
|
```ts
|
|
174
210
|
import { postgres } from 'weifuwu'
|
|
175
|
-
|
|
176
211
|
const sql = postgres()
|
|
177
212
|
app.use(sql) // → ctx.sql
|
|
178
|
-
|
|
179
213
|
await ctx.sql`SELECT * FROM users WHERE id = ${id}`
|
|
180
214
|
await ctx.sql.begin(async (sql) => { /* transaction */ })
|
|
181
215
|
```
|
|
182
|
-
|
|
183
216
|
| Method | Description |
|
|
184
217
|
|--------|-------------|
|
|
185
|
-
| `ctx.sql\`...\`` | Tagged template SQL
|
|
218
|
+
| `ctx.sql\`...\`` | Tagged template SQL |
|
|
186
219
|
| `ctx.sql.begin(fn)` | Transaction |
|
|
187
220
|
| `sql.close()` | Close pool |
|
|
188
221
|
|
|
189
|
-
Reads `DATABASE_URL` env. Supports migrations via `postgres({ migrate: { directory: './migrations' } })`.
|
|
190
|
-
|
|
191
222
|
### redis
|
|
192
|
-
|
|
193
223
|
```ts
|
|
194
224
|
import { redis } from 'weifuwu'
|
|
195
|
-
|
|
196
225
|
const r = redis()
|
|
197
226
|
app.use(r) // → ctx.redis
|
|
198
|
-
|
|
199
227
|
await ctx.redis.set('key', 'value')
|
|
200
228
|
await ctx.redis.get('key')
|
|
229
|
+
redis.close()
|
|
201
230
|
```
|
|
202
|
-
|
|
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 |
|
|
209
|
-
|
|
210
231
|
Reads `REDIS_URL` env (default: `redis://localhost:6379`).
|
|
211
232
|
|
|
212
233
|
### user
|
|
213
|
-
|
|
214
234
|
```ts
|
|
215
235
|
import { user, requireRole } from 'weifuwu'
|
|
216
|
-
|
|
217
236
|
app.use(postgres())
|
|
218
237
|
app.use(user({ secret: process.env.JWT_SECRET }))
|
|
238
|
+
// → ctx.user (UserRecord | undefined), ctx.userModule
|
|
219
239
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const { email, password } = await req.json()
|
|
225
|
-
const result = await ctx.userModule.login(email, password)
|
|
226
|
-
return result ? Response.json(result) : new Response('Unauthorized', { status: 401 })
|
|
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
|
|
227
244
|
})
|
|
228
|
-
|
|
229
|
-
app.get('/api/me', async (req, ctx) =>
|
|
230
|
-
ctx.user ? Response.json(ctx.user) : new Response('Unauthorized', { status: 401 }))
|
|
231
245
|
```
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|--------|---------|-------------|
|
|
246
|
+
| `ctx.userModule.*` | Returns | Description |
|
|
247
|
+
|--------------------|---------|-------------|
|
|
235
248
|
| `register(input)` | `{ user, token }` | Register |
|
|
236
249
|
| `login(email, pw)` | `{ user, token } \| null` | Login |
|
|
237
250
|
| `getUserById(id)` | `UserRecord \| null` | Get by ID |
|
|
238
|
-
| `getUserByEmail(email)` | `UserRecord \| null` | Get by email |
|
|
239
251
|
| `updateUser(id, input)` | `UserRecord \| null` | Update |
|
|
240
252
|
| `changePassword(id, oldPw, newPw)` | `boolean` | Change password |
|
|
241
253
|
| `deleteUser(id)` | `boolean` | Soft delete |
|
|
242
254
|
| `listUsers(inactive?)` | `UserRecord[]` | List users |
|
|
243
|
-
| `generateToken(user)` | `string` | Issue JWT |
|
|
244
255
|
| `verifyToken(token)` | `TokenPayload \| null` | Verify JWT |
|
|
245
256
|
|
|
246
|
-
`ctx.user` — auto-resolved from `Authorization: Bearer` or `token` cookie. Fields: `id, name, email, role, [key: string]`.
|
|
247
|
-
|
|
248
257
|
`requireRole('admin')` — guard middleware. No auth → 401, wrong role → 403.
|
|
249
258
|
|
|
250
|
-
Password: scrypt + 32-byte random salt. Token: HMAC SHA-256, 7 day expiry.
|
|
251
|
-
|
|
252
259
|
### messager
|
|
253
|
-
|
|
254
260
|
```ts
|
|
255
261
|
import { messager } from 'weifuwu'
|
|
256
|
-
|
|
257
262
|
app.use(postgres())
|
|
258
263
|
app.use(user())
|
|
259
|
-
app.use(messager())
|
|
260
|
-
|
|
261
|
-
app.ws('/ws', {
|
|
262
|
-
async open(ws, ctx) {
|
|
263
|
-
for (const c of await ctx.messager.getConversations())
|
|
264
|
-
ctx.ws.join(`conversation:${c.id}`)
|
|
265
|
-
},
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
app.post('/api/messages', async (req, ctx) => {
|
|
269
|
-
const { conversationId, body } = await req.json()
|
|
270
|
-
const msg = await ctx.messager.sendMessage(conversationId, body)
|
|
271
|
-
return Response.json(msg, { status: 201 })
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
app.get('/api/conversations/:id/messages', async (req, ctx) => {
|
|
275
|
-
const url = new URL(req.url)
|
|
276
|
-
return Response.json(await ctx.messager.getMessages(ctx.params.id, {
|
|
277
|
-
before: url.searchParams.get('before') || undefined,
|
|
278
|
-
limit: parseInt(url.searchParams.get('limit') || '50'),
|
|
279
|
-
}))
|
|
280
|
-
})
|
|
264
|
+
app.use(messager()) // → ctx.messager
|
|
281
265
|
```
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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 |
|
|
285
271
|
| `createDirectConversation(userId)` | `Conversation` | Create/reuse DM |
|
|
286
272
|
| `createGroupConversation(title, userIds)` | `Conversation` | Create group |
|
|
287
|
-
| `
|
|
288
|
-
| `getMessages(convId, opts?)` | `Message[]` | Cursor pagination |
|
|
289
|
-
| `editMessage(msgId, body)` | `Message \| null` | Edit (24h window) |
|
|
273
|
+
| `editMessage(msgId, body)` | `Message \| null` | Edit (24h) |
|
|
290
274
|
| `deleteMessage(msgId)` | `boolean` | Soft delete |
|
|
291
|
-
| `getConversations()` | `Conversation[]` | List with unread + last message |
|
|
292
|
-
| `getConversation(id)` | `Conversation \| null` | Get detail |
|
|
293
275
|
| `markRead(convId)` | `void` | Mark as read |
|
|
294
|
-
| `addParticipants(convId, userIds)` | `void` | Add members |
|
|
295
|
-
| `removeParticipant(convId, userId?)` | `boolean` | Leave / kick |
|
|
296
|
-
|
|
297
|
-
Storage: 3 auto-migrated tables (conversations, participants, messages). WebSocket push via rooms.
|
|
298
276
|
|
|
299
277
|
### kb — Knowledge Base
|
|
300
|
-
|
|
301
278
|
```ts
|
|
302
279
|
import { kb } from 'weifuwu'
|
|
303
|
-
|
|
304
280
|
app.use(postgres())
|
|
305
|
-
app.use(kb())
|
|
306
|
-
|
|
307
|
-
app.post('/api/kb/import', async (req, ctx) => {
|
|
308
|
-
const { title, content } = await req.json()
|
|
309
|
-
return Response.json(await ctx.kb.importText(title, content), { status: 201 })
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
app.post('/api/kb/search', async (req, ctx) => {
|
|
313
|
-
const { query } = await req.json()
|
|
314
|
-
return Response.json(await ctx.kb.search(query, { limit: 5 }))
|
|
315
|
-
})
|
|
281
|
+
app.use(kb()) // → ctx.kb
|
|
316
282
|
```
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
| `
|
|
321
|
-
| `importDocuments(docs)` | `Document[]` | Batch import |
|
|
322
|
-
| `search(query, opts?)` | `SearchResult[]` | Semantic search (cosine) |
|
|
283
|
+
| `ctx.kb.*` | Returns | Description |
|
|
284
|
+
|------------|---------|-------------|
|
|
285
|
+
| `importText(title, text)` | `{ document, chunks }` | Import → chunk → embed |
|
|
286
|
+
| `search(query, opts?)` | `SearchResult[]` | Semantic search |
|
|
323
287
|
| `list()` | `Document[]` | List documents |
|
|
324
288
|
| `get(id)` | `Document \| null` | Get document |
|
|
325
289
|
| `delete(id)` | `boolean` | Delete + cascade chunks |
|
|
326
290
|
|
|
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).
|
|
328
|
-
|
|
329
|
-
Integration with agent:
|
|
330
|
-
|
|
331
|
-
```ts
|
|
332
|
-
app.use(agent({
|
|
333
|
-
model: openai('deepseek-v4-flash', ...),
|
|
334
|
-
knowledge: { search: async (query, ctx) => ctx.kb.search(query) },
|
|
335
|
-
}))
|
|
336
|
-
```
|
|
337
|
-
|
|
338
291
|
### agent
|
|
339
|
-
|
|
340
292
|
```ts
|
|
341
293
|
import { agent } from 'weifuwu'
|
|
342
294
|
import { openai } from '@ai-sdk/openai'
|
|
@@ -355,307 +307,268 @@ app.use(agent({
|
|
|
355
307
|
}),
|
|
356
308
|
},
|
|
357
309
|
maxSteps: 5,
|
|
358
|
-
}))
|
|
359
|
-
|
|
360
|
-
app.post('/api/chat', async (req, ctx) => {
|
|
361
|
-
const { messages } = await req.json()
|
|
362
|
-
return ctx.agent.chatStreamResponse({ messages })
|
|
363
|
-
})
|
|
310
|
+
})) // → ctx.agent
|
|
364
311
|
```
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|-------------|-------------|
|
|
312
|
+
| `ctx.agent.*` | Description |
|
|
313
|
+
|--------------|-------------|
|
|
368
314
|
| `chat(prompt, opts?)` | Non-streaming, returns text |
|
|
369
|
-
| `chatStreamResponse({ messages })` | SSE stream (compatible
|
|
370
|
-
|
|
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`.
|
|
372
|
-
|
|
373
|
-
Features: `knowledge.search` (RAG), `tools` (auto-loop with maxSteps), `sandbox: true` (filesystem isolation), `store` (session persistence), `agents` (multi-agent).
|
|
315
|
+
| `chatStreamResponse({ messages })` | SSE stream (useChat compatible) |
|
|
374
316
|
|
|
375
317
|
### cms
|
|
376
|
-
|
|
377
318
|
```ts
|
|
378
319
|
import { cms, requireRole } from 'weifuwu'
|
|
379
|
-
|
|
380
320
|
app.use(postgres())
|
|
381
321
|
app.use(user())
|
|
382
|
-
app.use(cms())
|
|
383
|
-
|
|
384
|
-
app.get('/api/posts', async (req, ctx) =>
|
|
385
|
-
Response.json(await ctx.cms.list({ type: 'post', status: 'published' })))
|
|
386
|
-
|
|
387
|
-
app.get('/api/posts/:slug', async (req, ctx) => {
|
|
388
|
-
const post = await ctx.cms.get(ctx.params.slug)
|
|
389
|
-
return post ? Response.json(post) : new Response('Not found', { status: 404 })
|
|
390
|
-
})
|
|
391
|
-
|
|
392
|
-
app.post('/api/admin/posts', requireRole('admin'), async (req, ctx) =>
|
|
393
|
-
Response.json(await ctx.cms.create(await req.json()), { status: 201 }))
|
|
322
|
+
app.use(cms()) // → ctx.cms
|
|
394
323
|
```
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|--------|---------|-------------|
|
|
324
|
+
| `ctx.cms.*` | Returns | Description |
|
|
325
|
+
|-------------|---------|-------------|
|
|
398
326
|
| `create(input)` | `Content` | Create (admin) |
|
|
399
327
|
| `get(slug)` | `Content \| null` | Get by slug |
|
|
400
|
-
| `
|
|
401
|
-
| `
|
|
402
|
-
| `delete(id)` | `boolean` | Delete (admin) |
|
|
403
|
-
| `list(opts?)` | `Content[]` | List with cursor + filters |
|
|
404
|
-
| `publish(id)` | `Content \| null` | Publish (admin) |
|
|
405
|
-
| `unpublish(id)` | `Content \| null` | Unpublish (admin) |
|
|
406
|
-
| `listTags()` | `TagWithCount[]` | List tags |
|
|
407
|
-
| `createTag(name)` | `Tag` | Create tag |
|
|
408
|
-
|
|
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.
|
|
328
|
+
| `list(opts?)` | `Content[]` | List with filters |
|
|
329
|
+
| `publish(id)` / `unpublish(id)` | `Content \| null` | Toggle status |
|
|
410
330
|
|
|
411
331
|
### base — Dynamic Data Engine
|
|
412
|
-
|
|
413
332
|
```ts
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
app.use(base())
|
|
419
|
-
|
|
420
|
-
app.post('/api/bases', async (req, ctx) =>
|
|
421
|
-
Response.json(await ctx.base.create(await req.json()), { status: 201 }))
|
|
422
|
-
|
|
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 }))
|
|
425
|
-
|
|
426
|
-
app.get('/api/bases/:id/:table', async (req, ctx) => {
|
|
427
|
-
const url = new URL(req.url)
|
|
428
|
-
return Response.json(await ctx.base.query(ctx.params.id, ctx.params.table, {
|
|
429
|
-
filter: url.searchParams.get('filter') ? JSON.parse(url.searchParams.get('filter')!) : undefined,
|
|
430
|
-
limit: parseInt(url.searchParams.get('limit') || '50'),
|
|
431
|
-
}))
|
|
432
|
-
})
|
|
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 })
|
|
433
337
|
```
|
|
434
338
|
|
|
435
|
-
| Method | Returns | Description |
|
|
436
|
-
|--------|---------|-------------|
|
|
437
|
-
| `create({ name, tables })` | `BaseDef` | Create database |
|
|
438
|
-
| `insert(baseId, table, data)` | `Row` | Insert row |
|
|
439
|
-
| `getRow(baseId, table, id)` | `Row \| null` | Get row |
|
|
440
|
-
| `updateRow(baseId, table, id, data)` | `Row \| null` | Update row |
|
|
441
|
-
| `deleteRow(baseId, table, id)` | `boolean` | Delete row |
|
|
442
|
-
| `query(baseId, table, opts?)` | `Row[]` | Query (filter/sort/limit/offset) |
|
|
443
|
-
| `search(baseId, table, field, query)` | `Row[]` | Full-text search |
|
|
444
|
-
| `similaritySearch(baseId, table, field, vector)` | `Row[]` | Vector search |
|
|
445
|
-
|
|
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.
|
|
447
|
-
|
|
448
339
|
### queue
|
|
449
|
-
|
|
450
340
|
```ts
|
|
451
|
-
import { queue } from 'weifuwu'
|
|
452
|
-
|
|
453
341
|
const q = queue()
|
|
454
|
-
app.use(q)
|
|
455
|
-
|
|
456
|
-
q.
|
|
457
|
-
q.cron('cleanup', '0 3 * * *', () => cleanup())
|
|
342
|
+
app.use(q)
|
|
343
|
+
q.process('email', async (job) => { ... })
|
|
344
|
+
q.cron('cleanup', '0 3 * * *', () => ...)
|
|
458
345
|
await q.add('email', { to: 'user@example.com' })
|
|
459
346
|
q.run()
|
|
460
347
|
```
|
|
461
348
|
|
|
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 |
|
|
469
|
-
|
|
470
349
|
### ui — SSR & SPA
|
|
471
|
-
|
|
472
350
|
```ts
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
app.
|
|
476
|
-
|
|
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>`)
|
|
483
|
-
|
|
484
|
-
// Dynamic JS compilation (no build step needed)
|
|
351
|
+
app.use(ui()) // → ctx.ui
|
|
352
|
+
// SSR page
|
|
353
|
+
app.get('/blog/:slug', async (req, ctx) => ctx.ui.html`<!DOCTYPE html>...`)
|
|
354
|
+
// Dynamic JS compilation (no build step)
|
|
485
355
|
app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
|
|
356
|
+
// CSS with Tailwind v4 support
|
|
357
|
+
app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./public/style.css'))
|
|
489
358
|
```
|
|
490
359
|
|
|
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
|
-
|
|
500
360
|
---
|
|
501
361
|
|
|
502
362
|
## Frontend — weifuwu/client
|
|
503
363
|
|
|
504
|
-
Reactive frontend framework. Zero virtual DOM, zero external dependencies.
|
|
364
|
+
Reactive frontend framework. Zero virtual DOM, zero external dependencies.
|
|
505
365
|
|
|
506
|
-
###
|
|
366
|
+
### Core APIs
|
|
507
367
|
|
|
508
368
|
```tsx
|
|
509
|
-
// Signal — reactive
|
|
369
|
+
// Signal — reactive state
|
|
510
370
|
const count = signal(0)
|
|
511
|
-
count.value = count.value + 1 // DOM updates automatically
|
|
512
|
-
|
|
513
|
-
// Computed — derived signals
|
|
514
371
|
const doubled = computed(() => count.value * 2)
|
|
515
|
-
|
|
516
|
-
// Effect — auto-tracked side effects
|
|
517
372
|
effect(() => console.log('count:', count.value))
|
|
518
373
|
|
|
519
|
-
//
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
374
|
+
// Batch — merge multiple writes
|
|
375
|
+
batch(() => { a.value = 1; b.value = 2 })
|
|
376
|
+
|
|
377
|
+
// Untrack — read without subscribing
|
|
378
|
+
effect(() => { console.log(untrack(() => theme.value)) })
|
|
379
|
+
|
|
380
|
+
// Lifecycle
|
|
381
|
+
onMount(() => { init(); return () => cleanup() })
|
|
382
|
+
onCleanup(() => clearInterval(id))
|
|
523
383
|
```
|
|
524
384
|
|
|
525
|
-
###
|
|
385
|
+
### Reactive Array
|
|
526
386
|
|
|
527
387
|
```tsx
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
388
|
+
const items = reactiveArray([1, 2, 3])
|
|
389
|
+
items.push(4) // [1, 2, 3, 4]
|
|
390
|
+
items.pop() // [1, 2, 3]
|
|
391
|
+
items.remove(1) // [1, 3]
|
|
392
|
+
items.clear() // []
|
|
393
|
+
items.sort()
|
|
394
|
+
items.reverse()
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Control Flow
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
// Show — conditional rendering
|
|
401
|
+
<Show when={isLoggedIn} fallback={<LoginPage />}>
|
|
402
|
+
<Dashboard />
|
|
403
|
+
</Show>
|
|
404
|
+
|
|
405
|
+
// For — keyed list rendering
|
|
406
|
+
<For each={todos} keyBy="id">
|
|
407
|
+
{(todo) => <TodoItem todo={todo} />}
|
|
408
|
+
</For>
|
|
533
409
|
|
|
534
|
-
|
|
535
|
-
|
|
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>
|
|
536
419
|
```
|
|
537
420
|
|
|
538
|
-
###
|
|
421
|
+
### Form Handling
|
|
539
422
|
|
|
540
423
|
```tsx
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
{
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
424
|
+
// useForm — validation + submit
|
|
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
|
+
```
|
|
547
439
|
|
|
548
|
-
|
|
440
|
+
### Async Data
|
|
549
441
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
ctx.
|
|
553
|
-
|
|
554
|
-
|
|
442
|
+
```tsx
|
|
443
|
+
const [posts, { loading, error, refetch }] = createResource(
|
|
444
|
+
() => ctx.api.get('/api/posts'),
|
|
445
|
+
{ retry: 2, timeout: 5000 }, // optional
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
<Show when={loading}><Skeleton /></Show>
|
|
449
|
+
<Show when={error}><Error /></Show>
|
|
450
|
+
<For each={posts}>{(post) => <PostCard post={post} />}</For>
|
|
555
451
|
```
|
|
556
452
|
|
|
557
|
-
|
|
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
|
+
```
|
|
558
462
|
|
|
559
|
-
###
|
|
463
|
+
### Type-Safe Context
|
|
560
464
|
|
|
561
465
|
```tsx
|
|
562
|
-
|
|
563
|
-
|
|
466
|
+
const ThemeCtx = createContext<string>('theme')
|
|
467
|
+
ThemeCtx.provide(ctx, 'dark') // 提供
|
|
468
|
+
const theme = ThemeCtx.inject(ctx) // string | null
|
|
469
|
+
```
|
|
564
470
|
|
|
565
|
-
|
|
471
|
+
### Third-Party Integration
|
|
472
|
+
|
|
473
|
+
```tsx
|
|
474
|
+
const PieChart = wrap('div', (el, props, ctx) => {
|
|
566
475
|
const chart = echarts.init(el)
|
|
567
476
|
chart.setOption({ series: [{ type: 'pie', data: props.data }] })
|
|
568
|
-
effect(() => chart.setOption(
|
|
569
|
-
return () => chart.dispose()
|
|
477
|
+
effect(() => chart.setOption(...))
|
|
478
|
+
return () => chart.dispose()
|
|
570
479
|
})
|
|
571
|
-
|
|
572
|
-
// Use as regular component:
|
|
573
480
|
<Dashboard><PieChart data={salesData} /></Dashboard>
|
|
574
481
|
```
|
|
575
482
|
|
|
576
|
-
###
|
|
483
|
+
### Pre-built Components
|
|
577
484
|
|
|
578
485
|
```tsx
|
|
579
|
-
import {
|
|
486
|
+
import { LoginForm, Chat, Link } from 'weifuwu/client'
|
|
580
487
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
},
|
|
586
|
-
})
|
|
488
|
+
<LoginForm /> // 登录/注册(自动切换模式)
|
|
489
|
+
<Chat conversationId="123" /> // 实时消息聊天
|
|
490
|
+
<Link to="/about">关于</Link> // SPA 导航(支持右键新标签页)
|
|
491
|
+
```
|
|
587
492
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
493
|
+
### Router
|
|
494
|
+
|
|
495
|
+
```tsx
|
|
496
|
+
const routes: RouteDef[] = [
|
|
497
|
+
{ path: '/', component: HomePage, title: 'Home' },
|
|
498
|
+
{ path: '/post/:id', component: PostPage,
|
|
499
|
+
loader: async (ctx) => ({ post: await ctx.api.get(`/api/posts/${ctx.route.params.id}`) }),
|
|
500
|
+
},
|
|
501
|
+
]
|
|
591
502
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
Login
|
|
595
|
-
</button>
|
|
503
|
+
app.use(router({ routes, notFound: NotFound, mode: 'hash', transition: 'page' }))
|
|
504
|
+
// ctx.route.path / .params / .query / .data / .loading
|
|
596
505
|
|
|
597
|
-
//
|
|
598
|
-
|
|
599
|
-
form.setValues({ email: 'a@b.com', password: '123' })
|
|
600
|
-
form.reset()
|
|
506
|
+
// 路由出口
|
|
507
|
+
function AppShell() { return <main><RouteView /></main> }
|
|
601
508
|
```
|
|
602
509
|
|
|
603
|
-
###
|
|
510
|
+
### DevTools
|
|
604
511
|
|
|
605
|
-
```
|
|
606
|
-
import {
|
|
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>
|
|
512
|
+
```ts
|
|
513
|
+
import { enableDevtools } from 'weifuwu/client'
|
|
514
|
+
if (import.meta.env.DEV) enableDevtools()
|
|
615
515
|
|
|
616
|
-
//
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
</ErrorBoundary>
|
|
516
|
+
// 浏览器控制台:
|
|
517
|
+
// __wefu__.inspect() → 查看所有 signal
|
|
518
|
+
// __wefu__.warnings() → 切换开发警告
|
|
620
519
|
```
|
|
621
520
|
|
|
622
|
-
|
|
521
|
+
---
|
|
623
522
|
|
|
624
|
-
|
|
625
|
-
|
|
523
|
+
## From React to weifuwu/client
|
|
524
|
+
|
|
525
|
+
| React | weifuwu/client |
|
|
526
|
+
|-------|----------------|
|
|
527
|
+
| `useState(0)` | `signal(0)` |
|
|
528
|
+
| `useMemo(() => a * 2, [a])` | `computed(() => a.value * 2)` |
|
|
529
|
+
| `useEffect(() => {...}, [])` | `effect(() => {...})` + `onCleanup(() => {...})` |
|
|
530
|
+
| `{condition && <X/>}` | `<Show when={condition}><X/></Show>` |
|
|
531
|
+
| `{items.map(i => <X/>)}` | `<For each={items}>{(i) => <X/>}</For>` |
|
|
532
|
+
| `className={...}` | `class={...}` |
|
|
533
|
+
| Context Provider | `ctx.provide(key, val)` / `createContext()` |
|
|
534
|
+
| `useNavigate()` | `ctx.app.navigate(path)` |
|
|
535
|
+
| `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`
|
|
626
545
|
|
|
627
|
-
|
|
628
|
-
if (ctx.isAuthenticated) return ctx.app.navigate('/')
|
|
629
|
-
return <LoginForm />
|
|
630
|
-
}
|
|
546
|
+
---
|
|
631
547
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
548
|
+
## Utils
|
|
549
|
+
|
|
550
|
+
| Import | Purpose |
|
|
551
|
+
|--------|---------|
|
|
552
|
+
| `requireRole('admin')` | Middleware factory: check `ctx.user.role` |
|
|
553
|
+
| `createHub()` | WebSocket pub/sub |
|
|
554
|
+
| `HttpError` | `throw new HttpError(msg, 404)` |
|
|
555
|
+
| `trace()` | Request tracing |
|
|
636
556
|
|
|
637
557
|
---
|
|
638
558
|
|
|
639
|
-
## Router
|
|
559
|
+
## Router (Backend)
|
|
640
560
|
|
|
641
561
|
```ts
|
|
642
562
|
const app = new Router()
|
|
643
|
-
|
|
644
|
-
// HTTP
|
|
645
563
|
app.get(path, ...handlers)
|
|
646
564
|
app.post / put / delete / patch / head / options(path, ...handlers)
|
|
647
565
|
app.all(path, ...handlers)
|
|
648
|
-
|
|
649
|
-
//
|
|
650
|
-
app.
|
|
651
|
-
//
|
|
652
|
-
|
|
653
|
-
//
|
|
654
|
-
app.use(middleware)
|
|
655
|
-
app.mount(prefix, router)
|
|
656
|
-
app.plugin(fn)
|
|
657
|
-
app.onError(handler)
|
|
658
|
-
app.routes() // debug: list all routes
|
|
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
|
|
659
572
|
```
|
|
660
573
|
|
|
661
574
|
---
|
|
@@ -664,49 +577,53 @@ app.routes() // debug: list all routes
|
|
|
664
577
|
|
|
665
578
|
```
|
|
666
579
|
src/
|
|
667
|
-
├── index.ts
|
|
668
|
-
├── types.ts
|
|
669
|
-
├── core/
|
|
670
|
-
├── middleware/
|
|
671
|
-
├── user/
|
|
672
|
-
├── messager/
|
|
673
|
-
├── kb/
|
|
674
|
-
├── ai/
|
|
675
|
-
├── cms/
|
|
676
|
-
├── base/
|
|
677
|
-
├── postgres/
|
|
678
|
-
├── redis/
|
|
679
|
-
├── queue/
|
|
680
|
-
├── graphql.ts
|
|
681
|
-
├── hub.ts
|
|
682
|
-
├── ui/
|
|
683
|
-
├── client/
|
|
684
|
-
│ ├── index.ts
|
|
685
|
-
│ ├── signal.ts
|
|
686
|
-
│ ├── jsx-runtime.ts
|
|
687
|
-
│ ├── app.ts
|
|
688
|
-
│ ├── router.ts
|
|
689
|
-
│ ├── types.ts
|
|
580
|
+
├── index.ts ← Entry, exports all modules
|
|
581
|
+
├── types.ts ← Context, Handler, Middleware types
|
|
582
|
+
├── core/ ← serve, router, ws, trace, logger
|
|
583
|
+
├── middleware/ ← cors, helmet, compress, rate-limit, upload, static, sandbox
|
|
584
|
+
├── user/ ← User system (CRUD, JWT, requireRole)
|
|
585
|
+
├── messager/ ← IM + AI conversation layer
|
|
586
|
+
├── kb/ ← RAG knowledge base
|
|
587
|
+
├── ai/ ← AI Agent (LLM, tools, RAG)
|
|
588
|
+
├── cms/ ← Content management
|
|
589
|
+
├── base/ ← Dynamic data engine
|
|
590
|
+
├── postgres/ ← PostgreSQL client
|
|
591
|
+
├── redis/ ← Redis client
|
|
592
|
+
├── queue/ ← Job queue + cron
|
|
593
|
+
├── graphql.ts ← GraphQL
|
|
594
|
+
├── hub.ts ← WebSocket hub
|
|
595
|
+
├── ui/ ← ctx.ui.html / ctx.ui.js / ctx.ui.css
|
|
596
|
+
├── client/ ← Frontend framework
|
|
597
|
+
│ ├── index.ts ← Exports all client APIs (33 exports)
|
|
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
|
|
690
603
|
│ ├── lib/
|
|
691
|
-
│ │
|
|
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)
|
|
692
609
|
│ ├── middleware/
|
|
693
|
-
│ │ ├── api.ts
|
|
694
|
-
│ │ ├── auth.ts
|
|
695
|
-
│ │ └── ws.ts
|
|
610
|
+
│ │ ├── api.ts ← HTTP client → ctx.api
|
|
611
|
+
│ │ ├── auth.ts ← Auth → ctx.user/login/logout
|
|
612
|
+
│ │ └── ws.ts ← WebSocket → ctx.socket
|
|
696
613
|
│ └── components/
|
|
697
|
-
│ ├── LoginForm.
|
|
698
|
-
│
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
├──
|
|
705
|
-
|
|
706
|
-
|
|
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)
|
|
707
624
|
└── tsconfig.json
|
|
708
625
|
|
|
709
|
-
docker-compose.yml
|
|
626
|
+
docker-compose.yml ← postgres (pgvector) + redis
|
|
710
627
|
```
|
|
711
628
|
|
|
712
629
|
---
|
|
@@ -717,5 +634,18 @@ docker-compose.yml ← postgres (pgvector) + redis
|
|
|
717
634
|
docker compose up -d # Start postgres + redis
|
|
718
635
|
npm run build # esbuild → dist/
|
|
719
636
|
npm run typecheck # tsc --noEmit
|
|
720
|
-
npm test # Run tests
|
|
637
|
+
npm test # Run all tests
|
|
721
638
|
```
|
|
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 |
|