weifuwu 0.33.8 → 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 +200 -604
- package/dist/client/app.d.ts +1 -3
- package/dist/client/index.d.ts +121 -11
- package/dist/client/index.js +326 -488
- package/dist/client/jsx-runtime.d.ts +64 -4
- package/dist/client/jsx-runtime.js +326 -488
- package/dist/client/middleware/ws.d.ts +2 -2
- package/dist/client/router.d.ts +11 -0
- package/dist/client/signal.d.ts +46 -0
- package/dist/client/types.d.ts +56 -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 +4 -2
- 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 -17
- package/dist/client/components/LoginForm.d.ts +0 -14
- package/dist/client/lib/form.d.ts +0 -54
- 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,662 +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
|
-
##
|
|
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()` |
|
|
88
|
-
|
|
89
|
-
---
|
|
90
|
-
|
|
91
|
-
## Quick Start
|
|
13
|
+
## Core Concept: `ctx`
|
|
92
14
|
|
|
93
|
-
|
|
15
|
+
Backend and frontend share the same pattern: middleware injects fields into `ctx`, handlers/components read from `ctx`.
|
|
94
16
|
|
|
95
|
-
```ts
|
|
96
|
-
import { serve, Router, postgres, user, kb, agent, messager } from 'weifuwu'
|
|
97
|
-
import { openai } from '@ai-sdk/openai'
|
|
98
|
-
|
|
99
|
-
const app = new Router()
|
|
100
|
-
app.use(postgres())
|
|
101
|
-
app.use(user())
|
|
102
|
-
app.use(kb())
|
|
103
|
-
app.use(messager())
|
|
104
|
-
app.use(agent({
|
|
105
|
-
model: openai('deepseek-v4-flash', { baseURL: 'https://api.deepseek.com/v1' }),
|
|
106
|
-
knowledge: { search: async (q, ctx) => ctx.kb.search(q) },
|
|
107
|
-
}))
|
|
108
|
-
|
|
109
|
-
app.post('/api/chat', async (req, ctx) => {
|
|
110
|
-
const { messages } = await req.json()
|
|
111
|
-
return ctx.agent.chatStreamResponse({ messages })
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
serve(app, { port: 3000 })
|
|
115
17
|
```
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
function AppShell(_props: {}, ctx: WfuiContext) {
|
|
124
|
-
if (!ctx.isAuthenticated) return <LoginForm />
|
|
125
|
-
return (
|
|
126
|
-
<div>
|
|
127
|
-
<nav><a onClick={() => ctx.app.navigate('/chat')}>Chat</a></nav>
|
|
128
|
-
<main><RouteView /></main>
|
|
129
|
-
</div>
|
|
130
|
-
)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const app = createApp()
|
|
134
|
-
app.use(api())
|
|
135
|
-
app.use(auth())
|
|
136
|
-
app.use(ws())
|
|
137
|
-
app.use(router({ routes }))
|
|
138
|
-
app.mount('#root', AppShell)
|
|
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
|
|
139
25
|
```
|
|
140
26
|
|
|
141
|
-
|
|
142
|
-
// tsconfig.json
|
|
143
|
-
{ "jsx": "react-jsx", "jsxImportSource": "weifuwu/client" }
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
Build (traditional, or use `ctx.ui.js()` for dynamic compilation):
|
|
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
|
-
```
|
|
157
|
-
|
|
158
|
-
Or skip the build step entirely with server-side compilation:
|
|
159
|
-
|
|
27
|
+
**Backend:**
|
|
160
28
|
```ts
|
|
161
|
-
app.
|
|
162
|
-
app.
|
|
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
|
|
163
32
|
```
|
|
164
33
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
Each module follows: import → usage → API table.
|
|
170
|
-
|
|
171
|
-
### postgres
|
|
172
|
-
|
|
173
|
-
```ts
|
|
174
|
-
import { postgres } from 'weifuwu'
|
|
175
|
-
|
|
176
|
-
const sql = postgres()
|
|
177
|
-
app.use(sql) // → ctx.sql
|
|
178
|
-
|
|
179
|
-
await ctx.sql`SELECT * FROM users WHERE id = ${id}`
|
|
180
|
-
await ctx.sql.begin(async (sql) => { /* transaction */ })
|
|
34
|
+
**Frontend:**
|
|
35
|
+
```tsx
|
|
36
|
+
app.use(ws()) // → ctx.ws.send / onMessage / isConnected
|
|
37
|
+
app.use(router({ routes })) // → ctx.route.path / params / query
|
|
181
38
|
```
|
|
182
39
|
|
|
183
|
-
|
|
184
|
-
|--------|-------------|
|
|
185
|
-
| `ctx.sql\`...\`` | Tagged template SQL queries |
|
|
186
|
-
| `ctx.sql.begin(fn)` | Transaction |
|
|
187
|
-
| `sql.close()` | Close pool |
|
|
188
|
-
|
|
189
|
-
Reads `DATABASE_URL` env. Supports migrations via `postgres({ migrate: { directory: './migrations' } })`.
|
|
190
|
-
|
|
191
|
-
### redis
|
|
40
|
+
---
|
|
192
41
|
|
|
193
|
-
|
|
194
|
-
import { redis } from 'weifuwu'
|
|
42
|
+
## Backend
|
|
195
43
|
|
|
196
|
-
|
|
197
|
-
app.use(r) // → ctx.redis
|
|
44
|
+
### Exports
|
|
198
45
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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 |
|
|
202
58
|
|
|
203
|
-
|
|
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 |
|
|
59
|
+
### Types
|
|
209
60
|
|
|
210
|
-
|
|
61
|
+
`Context`, `Handler`, `Middleware`, `ErrorHandler`, `WebSocket`, `WebSocketHandler`, `ServeOptions`, `Server`, `CORSOptions`, `ServeStaticOptions`, `PostgresOptions`, `PostgresClient`, `PostgresInjected`, `RedisOptions`, `RedisClient`, `RedisInjected`, `GraphQLOptions`, `GraphQLHandler`
|
|
211
62
|
|
|
212
|
-
###
|
|
63
|
+
### Quick Start
|
|
213
64
|
|
|
214
65
|
```ts
|
|
215
|
-
import {
|
|
216
|
-
|
|
217
|
-
app.use(postgres())
|
|
218
|
-
app.use(user({ secret: process.env.JWT_SECRET }))
|
|
66
|
+
import { serve, Router, cors, serveStatic, ui } from 'weifuwu'
|
|
219
67
|
|
|
220
|
-
app
|
|
221
|
-
|
|
68
|
+
const app = new Router()
|
|
69
|
+
app.use(cors())
|
|
70
|
+
app.use(serveStatic('./public'))
|
|
71
|
+
app.use(ui())
|
|
222
72
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return result ? Response.json(result) : new Response('Unauthorized', { status: 401 })
|
|
73
|
+
// API routes
|
|
74
|
+
app.get('/api/hello', async (req, ctx) => {
|
|
75
|
+
return Response.json({ message: 'hello' })
|
|
227
76
|
})
|
|
228
77
|
|
|
229
|
-
|
|
230
|
-
ctx.user ? Response.json(ctx.user) : new Response('Unauthorized', { status: 401 }))
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
| Method | Returns | Description |
|
|
234
|
-
|--------|---------|-------------|
|
|
235
|
-
| `register(input)` | `{ user, token }` | Register |
|
|
236
|
-
| `login(email, pw)` | `{ user, token } \| null` | Login |
|
|
237
|
-
| `getUserById(id)` | `UserRecord \| null` | Get by ID |
|
|
238
|
-
| `getUserByEmail(email)` | `UserRecord \| null` | Get by email |
|
|
239
|
-
| `updateUser(id, input)` | `UserRecord \| null` | Update |
|
|
240
|
-
| `changePassword(id, oldPw, newPw)` | `boolean` | Change password |
|
|
241
|
-
| `deleteUser(id)` | `boolean` | Soft delete |
|
|
242
|
-
| `listUsers(inactive?)` | `UserRecord[]` | List users |
|
|
243
|
-
| `generateToken(user)` | `string` | Issue JWT |
|
|
244
|
-
| `verifyToken(token)` | `TokenPayload \| null` | Verify JWT |
|
|
245
|
-
|
|
246
|
-
`ctx.user` — auto-resolved from `Authorization: Bearer` or `token` cookie. Fields: `id, name, email, role, [key: string]`.
|
|
247
|
-
|
|
248
|
-
`requireRole('admin')` — guard middleware. No auth → 401, wrong role → 403.
|
|
249
|
-
|
|
250
|
-
Password: scrypt + 32-byte random salt. Token: HMAC SHA-256, 7 day expiry.
|
|
251
|
-
|
|
252
|
-
### messager
|
|
253
|
-
|
|
254
|
-
```ts
|
|
255
|
-
import { messager } from 'weifuwu'
|
|
256
|
-
|
|
257
|
-
app.use(postgres())
|
|
258
|
-
app.use(user())
|
|
259
|
-
app.use(messager())
|
|
260
|
-
|
|
78
|
+
// WebSocket
|
|
261
79
|
app.ws('/ws', {
|
|
262
|
-
|
|
263
|
-
|
|
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 })
|
|
80
|
+
open(ws) { ws.send('connected') },
|
|
81
|
+
message(ws, ctx, data) { ws.send(data.toString()) },
|
|
272
82
|
})
|
|
273
83
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
})
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
| Method | Returns | Description |
|
|
284
|
-
|--------|---------|-------------|
|
|
285
|
-
| `createDirectConversation(userId)` | `Conversation` | Create/reuse DM |
|
|
286
|
-
| `createGroupConversation(title, userIds)` | `Conversation` | Create group |
|
|
287
|
-
| `sendMessage(convId, body)` | `Message` | Send + broadcast to room |
|
|
288
|
-
| `getMessages(convId, opts?)` | `Message[]` | Cursor pagination |
|
|
289
|
-
| `editMessage(msgId, body)` | `Message \| null` | Edit (24h window) |
|
|
290
|
-
| `deleteMessage(msgId)` | `boolean` | Soft delete |
|
|
291
|
-
| `getConversations()` | `Conversation[]` | List with unread + last message |
|
|
292
|
-
| `getConversation(id)` | `Conversation \| null` | Get detail |
|
|
293
|
-
| `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
|
-
|
|
299
|
-
### kb — Knowledge Base
|
|
84
|
+
// GraphQL
|
|
85
|
+
app.graphql(async (req, ctx) => ({
|
|
86
|
+
schema: `type Query { hello: String }`,
|
|
87
|
+
resolvers: { Query: { hello: () => 'world' } },
|
|
88
|
+
graphiql: true,
|
|
89
|
+
}))
|
|
300
90
|
|
|
301
|
-
|
|
302
|
-
|
|
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
|
+
`)
|
|
303
96
|
|
|
304
|
-
|
|
305
|
-
app.
|
|
97
|
+
// Dynamic JS compilation (no build step)
|
|
98
|
+
app.get('/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
|
|
306
99
|
|
|
307
|
-
app
|
|
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
|
-
})
|
|
100
|
+
serve(app, { port: 3000 })
|
|
316
101
|
```
|
|
317
102
|
|
|
318
|
-
|
|
319
|
-
|--------|---------|-------------|
|
|
320
|
-
| `importText(title, text, opts?)` | `{ document, chunks }` | Import → chunk → embed → store |
|
|
321
|
-
| `importDocuments(docs)` | `Document[]` | Batch import |
|
|
322
|
-
| `search(query, opts?)` | `SearchResult[]` | Semantic search (cosine) |
|
|
323
|
-
| `list()` | `Document[]` | List documents |
|
|
324
|
-
| `get(id)` | `Document \| null` | Get document |
|
|
325
|
-
| `delete(id)` | `boolean` | Delete + cascade chunks |
|
|
326
|
-
|
|
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:
|
|
103
|
+
### Router
|
|
330
104
|
|
|
331
105
|
```ts
|
|
332
|
-
app
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
|
336
117
|
```
|
|
337
118
|
|
|
338
|
-
###
|
|
119
|
+
### WebSocket
|
|
339
120
|
|
|
340
121
|
```ts
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
app.use(agent({
|
|
347
|
-
model: openai('deepseek-v4-flash', { baseURL: 'https://api.deepseek.com/v1' }),
|
|
348
|
-
system: 'You are a helpful assistant.',
|
|
349
|
-
knowledge: { search: async (q, ctx) => ctx.kb.search(q) },
|
|
350
|
-
tools: {
|
|
351
|
-
getWeather: tool({
|
|
352
|
-
description: 'Get weather for a city',
|
|
353
|
-
parameters: z.object({ city: z.string() }),
|
|
354
|
-
execute: async ({ city }) => ({ temp: 22, unit: 'C' }),
|
|
355
|
-
}),
|
|
356
|
-
},
|
|
357
|
-
maxSteps: 5,
|
|
358
|
-
}))
|
|
359
|
-
|
|
360
|
-
app.post('/api/chat', async (req, ctx) => {
|
|
361
|
-
const { messages } = await req.json()
|
|
362
|
-
return ctx.agent.chatStreamResponse({ messages })
|
|
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 */ },
|
|
363
127
|
})
|
|
364
128
|
```
|
|
365
129
|
|
|
366
|
-
|
|
367
|
-
|-------------|-------------|
|
|
368
|
-
| `chat(prompt, opts?)` | Non-streaming, returns text |
|
|
369
|
-
| `chatStreamResponse({ messages })` | SSE stream (compatible with `useChat`) |
|
|
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).
|
|
374
|
-
|
|
375
|
-
### cms
|
|
130
|
+
### GraphQL
|
|
376
131
|
|
|
377
132
|
```ts
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
})
|
|
133
|
+
// At root
|
|
134
|
+
app.graphql(async (req, ctx) => ({
|
|
135
|
+
schema: `type Query { hello: String }`,
|
|
136
|
+
resolvers: { Query: { hello: () => 'world' } },
|
|
137
|
+
graphiql: true,
|
|
138
|
+
}))
|
|
391
139
|
|
|
392
|
-
|
|
393
|
-
|
|
140
|
+
// Or at a custom path
|
|
141
|
+
app.graphql('/graphql', handler)
|
|
394
142
|
```
|
|
395
143
|
|
|
396
|
-
|
|
397
|
-
|--------|---------|-------------|
|
|
398
|
-
| `create(input)` | `Content` | Create (admin) |
|
|
399
|
-
| `get(slug)` | `Content \| null` | Get by slug |
|
|
400
|
-
| `getById(id)` | `Content \| null` | Get by ID |
|
|
401
|
-
| `update(id, input)` | `Content \| null` | Update (admin) |
|
|
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.
|
|
410
|
-
|
|
411
|
-
### base — Dynamic Data Engine
|
|
144
|
+
### Graceful Shutdown
|
|
412
145
|
|
|
413
146
|
```ts
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
app.use(user())
|
|
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
|
-
})
|
|
147
|
+
const srv = serve(app)
|
|
148
|
+
// Ctrl+C / SIGTERM immediately closes all connections and exits
|
|
149
|
+
await srv.stop() // Programmatic stop
|
|
433
150
|
```
|
|
434
151
|
|
|
435
|
-
|
|
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
|
-
### queue
|
|
449
|
-
|
|
450
|
-
```ts
|
|
451
|
-
import { queue } from 'weifuwu'
|
|
152
|
+
---
|
|
452
153
|
|
|
453
|
-
|
|
454
|
-
app.use(q) // → ctx.queue
|
|
154
|
+
## Frontend (`weifuwu/client`)
|
|
455
155
|
|
|
456
|
-
|
|
457
|
-
q.cron('cleanup', '0 3 * * *', () => cleanup())
|
|
458
|
-
await q.add('email', { to: 'user@example.com' })
|
|
459
|
-
q.run()
|
|
460
|
-
```
|
|
156
|
+
16 runtime exports, zero dependencies, zero virtual DOM.
|
|
461
157
|
|
|
462
|
-
|
|
|
463
|
-
|
|
464
|
-
| `
|
|
465
|
-
| `
|
|
466
|
-
| `
|
|
467
|
-
| `
|
|
468
|
-
| `
|
|
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` |
|
|
469
170
|
|
|
470
|
-
|
|
171
|
+
Types: `Signal`, `Component`, `WfuiContext`, `AppMiddleware`, `RouteDef`
|
|
471
172
|
|
|
472
|
-
|
|
473
|
-
import { ui } from 'weifuwu'
|
|
173
|
+
### Quick Start
|
|
474
174
|
|
|
475
|
-
|
|
175
|
+
```tsx
|
|
176
|
+
import { signal, Show, For, createApp, ws, router, RouteView } from 'weifuwu/client'
|
|
177
|
+
import type { WfuiContext, RouteDef } from 'weifuwu/client'
|
|
476
178
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
<body><div id="root">${ctx.ui.html.unsafe(post.body)}</div>
|
|
482
|
-
<script src="/static/app.js"></script></body></html>`)
|
|
179
|
+
const routes: RouteDef[] = [
|
|
180
|
+
{ path: '/', component: HomePage },
|
|
181
|
+
{ path: '/hello/:name', component: HelloPage },
|
|
182
|
+
]
|
|
483
183
|
|
|
484
|
-
|
|
485
|
-
app.
|
|
184
|
+
const app = createApp()
|
|
185
|
+
app.use(ws())
|
|
186
|
+
app.use(router({ routes, mode: 'hash' }))
|
|
187
|
+
app.mount('#root', AppShell)
|
|
486
188
|
|
|
487
|
-
|
|
488
|
-
|
|
189
|
+
function AppShell(_props: {}, ctx: WfuiContext) {
|
|
190
|
+
return (
|
|
191
|
+
<div>
|
|
192
|
+
<nav>...</nav>
|
|
193
|
+
<main><RouteView /></main>
|
|
194
|
+
</div>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
489
197
|
```
|
|
490
198
|
|
|
491
|
-
|
|
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
|
-
---
|
|
501
|
-
|
|
502
|
-
## Frontend — weifuwu/client
|
|
503
|
-
|
|
504
|
-
Reactive frontend framework. Zero virtual DOM, zero external dependencies. Component model: `(props, ctx) => Node`.
|
|
505
|
-
|
|
506
|
-
### Concepts
|
|
199
|
+
### Signal
|
|
507
200
|
|
|
508
201
|
```tsx
|
|
509
|
-
// Signal — reactive data
|
|
510
202
|
const count = signal(0)
|
|
511
|
-
count.value = count.value + 1 // DOM updates automatically
|
|
512
|
-
|
|
513
|
-
// Computed — derived signals
|
|
514
203
|
const doubled = computed(() => count.value * 2)
|
|
515
|
-
|
|
516
|
-
// Effect — auto-tracked side effects
|
|
517
204
|
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
|
-
}
|
|
205
|
+
batch(() => { a.value = 1; b.value = 2 })
|
|
523
206
|
```
|
|
524
207
|
|
|
525
|
-
###
|
|
208
|
+
### Control Flow
|
|
526
209
|
|
|
527
210
|
```tsx
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
app.use(ws()) // ctx.ws.send / onMessage / join / leave
|
|
532
|
-
app.use(router({ routes })) // ctx.route.path/params/query
|
|
211
|
+
<Show when={isLoggedIn} fallback={<Login />}>
|
|
212
|
+
<Dashboard />
|
|
213
|
+
</Show>
|
|
533
214
|
|
|
534
|
-
|
|
535
|
-
|
|
215
|
+
<For each={items} keyBy="id">
|
|
216
|
+
{(item) => <div>{item.name}</div>}
|
|
217
|
+
</For>
|
|
536
218
|
```
|
|
537
219
|
|
|
538
|
-
###
|
|
220
|
+
### WebSocket (`ctx.ws`)
|
|
539
221
|
|
|
540
222
|
```tsx
|
|
541
|
-
|
|
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' }))
|
|
223
|
+
app.use(ws({ url: '/ws' }))
|
|
549
224
|
|
|
550
225
|
// In component:
|
|
551
|
-
|
|
552
|
-
ctx.
|
|
553
|
-
|
|
554
|
-
|
|
226
|
+
onMount(() => {
|
|
227
|
+
const unsub = ctx.ws.onMessage((data) => { ... })
|
|
228
|
+
onCleanup(() => unsub())
|
|
229
|
+
})
|
|
230
|
+
ctx.ws.send({ type: 'chat', body: 'hello' })
|
|
231
|
+
<Show when={ctx.ws.isConnected}>🟢 已连接</Show>
|
|
555
232
|
```
|
|
556
233
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
### wrap() — Third-party Library Integration
|
|
234
|
+
### Router (`ctx.route`)
|
|
560
235
|
|
|
561
236
|
```tsx
|
|
562
|
-
|
|
563
|
-
import * as echarts from 'echarts'
|
|
564
|
-
|
|
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
|
-
})
|
|
237
|
+
app.use(router({ routes, notFound: NotFound, mode: 'hash' }))
|
|
571
238
|
|
|
572
|
-
//
|
|
573
|
-
|
|
239
|
+
ctx.route.path // '/hello/world'
|
|
240
|
+
ctx.route.params // { name: 'world' }
|
|
241
|
+
ctx.route.query // { tab: 'intro' }
|
|
242
|
+
ctx.app.navigate('/hello/world')
|
|
574
243
|
```
|
|
575
244
|
|
|
576
|
-
###
|
|
245
|
+
### Lifecycle
|
|
577
246
|
|
|
578
247
|
```tsx
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
initial: { email: '', password: '' },
|
|
583
|
-
validate: {
|
|
584
|
-
email: (v) => !v.includes('@') && 'Invalid email',
|
|
585
|
-
},
|
|
248
|
+
onMount(() => {
|
|
249
|
+
init()
|
|
250
|
+
return () => cleanup() // Auto-cleanup on unmount
|
|
586
251
|
})
|
|
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()
|
|
252
|
+
onCleanup(() => clearInterval(id))
|
|
601
253
|
```
|
|
602
254
|
|
|
603
|
-
###
|
|
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
|
-
```
|
|
255
|
+
### From React
|
|
621
256
|
|
|
622
|
-
|
|
257
|
+
| React | weifuwu/client |
|
|
258
|
+
|-------|----------------|
|
|
259
|
+
| `useState(0)` | `signal(0)` |
|
|
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>` |
|
|
264
|
+
| `{items.map(i => <X/>)}` | `<For each={items}>{(i) => <X/>}</For>` |
|
|
265
|
+
| `useNavigate()` | `ctx.app.navigate()` |
|
|
266
|
+
| `useParams()` | `ctx.route.params` |
|
|
623
267
|
|
|
624
|
-
|
|
625
|
-
import { LoginForm, Chat } from 'weifuwu/client'
|
|
268
|
+
---
|
|
626
269
|
|
|
627
|
-
|
|
628
|
-
if (ctx.isAuthenticated) return ctx.app.navigate('/')
|
|
629
|
-
return <LoginForm />
|
|
630
|
-
}
|
|
270
|
+
## Demo
|
|
631
271
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
272
|
+
```bash
|
|
273
|
+
cd apps/demo
|
|
274
|
+
node server.ts
|
|
275
|
+
# http://localhost:3000
|
|
635
276
|
```
|
|
636
277
|
|
|
637
278
|
---
|
|
638
279
|
|
|
639
|
-
##
|
|
640
|
-
|
|
641
|
-
```ts
|
|
642
|
-
const app = new Router()
|
|
643
|
-
|
|
644
|
-
// HTTP
|
|
645
|
-
app.get(path, ...handlers)
|
|
646
|
-
app.post / put / delete / patch / head / options(path, ...handlers)
|
|
647
|
-
app.all(path, ...handlers)
|
|
280
|
+
## Environment Variables
|
|
648
281
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
|
659
|
-
```
|
|
282
|
+
| Variable | Default | Used by |
|
|
283
|
+
|----------|---------|---------|
|
|
284
|
+
| `DATABASE_URL` | `postgres://root:123456@localhost:5432/demo` | `postgres()` |
|
|
285
|
+
| `REDIS_URL` | `redis://localhost:6379` | `redis()` |
|
|
660
286
|
|
|
661
287
|
---
|
|
662
288
|
|
|
@@ -664,58 +290,28 @@ app.routes() // debug: list all routes
|
|
|
664
290
|
|
|
665
291
|
```
|
|
666
292
|
src/
|
|
667
|
-
├── index.ts
|
|
668
|
-
├── types.ts
|
|
669
|
-
├── core/
|
|
670
|
-
├── middleware/
|
|
671
|
-
├──
|
|
672
|
-
├──
|
|
673
|
-
├──
|
|
674
|
-
├──
|
|
675
|
-
├──
|
|
676
|
-
├──
|
|
677
|
-
├──
|
|
678
|
-
├──
|
|
679
|
-
├──
|
|
680
|
-
├──
|
|
681
|
-
├──
|
|
682
|
-
|
|
683
|
-
├──
|
|
684
|
-
|
|
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
|
|
690
|
-
│ ├── lib/
|
|
691
|
-
│ │ └── form.ts ← useForm
|
|
692
|
-
│ ├── middleware/
|
|
693
|
-
│ │ ├── api.ts ← HTTP client
|
|
694
|
-
│ │ ├── auth.ts ← Login / logout / token
|
|
695
|
-
│ │ └── ws.ts ← WebSocket
|
|
696
|
-
│ └── components/
|
|
697
|
-
│ ├── LoginForm.ts
|
|
698
|
-
│ └── Chat.ts
|
|
699
|
-
└── test/ ← Tests
|
|
700
|
-
|
|
701
|
-
apps/demo/ ← Full-stack demo
|
|
702
|
-
├── src/main.tsx ← SPA + SSR hydrate demo pages
|
|
703
|
-
├── server.ts ← weifuwu server
|
|
704
|
-
├── public/
|
|
705
|
-
│ ├── index.html ← HTML skeleton
|
|
706
|
-
│ └── style.css ← Demo styles (Tailwind)
|
|
707
|
-
└── tsconfig.json
|
|
708
|
-
|
|
709
|
-
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
|
|
710
311
|
```
|
|
711
312
|
|
|
712
|
-
---
|
|
713
|
-
|
|
714
|
-
## Development
|
|
715
|
-
|
|
716
313
|
```bash
|
|
717
|
-
|
|
718
|
-
npm run
|
|
719
|
-
npm
|
|
720
|
-
npm test # Run tests
|
|
314
|
+
npm run build # esbuild → dist/
|
|
315
|
+
npm run typecheck # tsc --noEmit
|
|
316
|
+
npm test # Run all tests
|
|
721
317
|
```
|