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