weifuwu 0.61.0 → 0.61.2
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 +103 -57
- package/dist/core/router.d.ts +2 -7
- package/dist/core/ws.d.ts +20 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -3
- package/dist/messager/index.d.ts +1 -1
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ npm install weifuwu
|
|
|
19
19
|
|
|
20
20
|
### 一句话
|
|
21
21
|
|
|
22
|
-
**weifuwu =
|
|
22
|
+
**weifuwu = 一个包的全栈框架:全自研、零配置、消灭样板、SaaS 地基随包内置。** 下面四条核心哲学与十一条技术原则都是这句话的展开——我们不做缝合框架,每一层都自研且可预测。
|
|
23
23
|
|
|
24
24
|
### 核心哲学
|
|
25
25
|
|
|
@@ -36,6 +36,8 @@ npm install weifuwu
|
|
|
36
36
|
| 数据样板 | `ctx.data.get` 一个 API 覆盖 SSR 预取 / hydration 命中 / SPA fetch,写数据像写同步代码 |
|
|
37
37
|
| 协议样板 | 自研 PG/Redis 客户端消灭双重编码、parseRow 样板、`'EX'` 参数顺序陷阱 |
|
|
38
38
|
|
|
39
|
+
**④ SaaS 地基,应用必须的一等能力。** 不只是库——rateLimit / email / userSystem / messager / queue 五个中间件随包内置,且互相咬合:**身份是消息的路由,消息是身份的交互**(`sendTo(ctx.user.id)` 按身份路由、`createConversation(ctx.user.id)` 创建者即身份、成员校验自动对齐),AI 对话走同一协议。开发者从「自建基础设施」变「声明业务」——`app.use(...)` 一行接入,一个多租户 AI 平台(agent-platform)已完整消费这层地基(auth / AI / 消息 / UI / 数据管道全部框架能力)。
|
|
40
|
+
|
|
39
41
|
### 技术原则(哲学的展开)
|
|
40
42
|
|
|
41
43
|
**零运行时依赖** — 前端无 npm 运行时依赖(自研 VDOM,不引入 Virtual DOM 库、rxjs、immer 等)。后端仅依赖 `esbuild`(TSX→JS 编译)+ `graphql` + `ws`(语言/协议本身)——**数据库客户端(PostgreSQL/Redis 协议)、GraphQL schema 工具全部自研**。esbuild 作为运行时依赖随 `npm install weifuwu` 自动安装,`ctx.ui.js()` 开箱即用。
|
|
@@ -44,20 +46,24 @@ npm install weifuwu
|
|
|
44
46
|
|
|
45
47
|
**Proxy 驱动渲染** — `ctx.ui.$()` 返回深度 Proxy,`$.x = val` 自动触发当前组件的 VDOM patch;也支持手动 `ctx.ui.render()` 精确控制渲染时机。**组件库手动优先、业务层自动优先**——同一框架内按角色选模式(详见[组件库](#组件库-weifuwucomponents))。
|
|
46
48
|
|
|
47
|
-
**中间件注入一切** — 后端和前端共用同一理念:中间件向 `ctx` 注入能力(`ctx.sql` / `ctx.redis` / `ctx.api` / `ctx.auth` / `ctx.i18n` / `ctx.limit` / `ctx.email` / `ctx.queue` / `ctx.ai` 等),Handler/组件从 `ctx` 读取。
|
|
49
|
+
**中间件注入一切** — 后端和前端共用同一理念:中间件向 `ctx` 注入能力(`ctx.sql` / `ctx.redis` / `ctx.api` / `ctx.auth` / `ctx.i18n` / `ctx.limit` / `ctx.email` / `ctx.queue` / `ctx.ai` / `ctx.msg` 等),Handler/组件从 `ctx` 读取。
|
|
48
50
|
|
|
49
51
|
**async 工厂组件** — `async (ctx) => (initProps, ctx) => (props) => VNode`:工厂层声明数据(`await ctx.data.get`)、mount 初始化状态(`$`)、render 输出视图。异步只在工厂边界,mount/render 保持同步;数据经闭包注入,写数据像写同步代码。三条纪律见[核心概念 · async 组件](#核心概念)。
|
|
50
52
|
|
|
51
53
|
**SPA/SSR/Hydration 统一透明** — 同一份路由定义(`routes`)一个组件三场景自动适配:后端 `uiSsr({ routes })` 匹配即自动 SSR(完整 HTML + `__DATA__`),客户端 `router({ routes })` + `RouteView` + `mount(..., { hydrate: true })` 按 URL 同源匹配并收养服务端 HTML(不重建、无闪跳)。`ctx.data.get` 一个 API:SSR 预取 / hydration 命中(不重复请求)/ SPA 触发 fetch。服务端直接用 `.tsx`(`weifuwu/dev` Node loader),前后端同一 JSX 运行时。
|
|
52
54
|
|
|
53
|
-
**AI 是一等公民** — 自研 OpenAI 兼容协议(`docs/ai-contract.md`)+ 零依赖流式客户端 + agent 工具循环 + HITL
|
|
55
|
+
**AI 是一等公民** — 自研 OpenAI 兼容协议(`docs/ai-contract.md`)+ 零依赖流式客户端 + agent 工具循环 + HITL 人工审批 + embedding 向量化。后端 `ctx.ai` 一个入口:`chat()` / `stream()` / `agent()`(`stream(messages, { emit })` emitter 抽象——事件可接任意通道,`runToResult()` 结构化结果)/ `approve()` / `embed()` / `embedMany()`;前端 `ctx.ui.useChat()`(会话语义)+ `AiChat` 组件(标准对话界面)——流式 token / 工具调用卡 / 审批卡开箱即用,协议对页面完全透明,不用 ai-sdk。
|
|
56
|
+
|
|
57
|
+
**SaaS 地基随包内置** — rateLimit(限流)/ email(邮件)/ userSystem(用户认证)/ messager(消息系统)/ queue(可靠队列)以中间件形态随包提供,`app.use(...)` 一行接入(详见文末[SaaS 地基模块](#saas-地基模块ratelimit--email--usersystem--messager--queue))。互相咬合成协作基础:身份(userSystem)+ 消息(messager)的组合让「谁能跟谁说话、消息如何送达」天然对齐,不再需要第三套权限系统。
|
|
54
58
|
|
|
55
|
-
|
|
59
|
+
**机制与策略分离** — 框架管**机制**(token 怎么签、消息怎么送达、agent 循环怎么跑),开发者管**策略**(谁能建群、租户隔离 SQL、技能注册表)。这是「诚实裁剪」的积极面:**框架不越界,应用层不被绑架**——agent-platform 迁移验证了边界:多租户隔离(`WHERE tenant_id`)、技能编排、聊天产品模型留在应用层,框架守住通用能力(auth / ai / messager / UI / 数据管道)。
|
|
56
60
|
|
|
57
61
|
**零自定义 CSS 设计系统** — 一个 CSS 文件 = 双层 Token + 布局原语 + 工具类 + 组件样式。业务页面不写 style.css:组件 + `wf-*` 原语写业务,品牌/组件定制改变量(`--wf-brand-500` / `--wf-btn-radius`),暗色自动(详见[布局系统](#布局系统-weifuwulayout))。
|
|
58
62
|
|
|
59
63
|
**自研数据层** — `ctx.sql`(PG v3 协议)与 `ctx.redis`(RESP2 协议)为**自研客户端**:确定性输出、行为可预测、统一错误模型。jsonb 自动解码、TTL 安全 API、schema 写前校验——高频痛点(双重编码/parseRow 样板/`'EX'` 参数顺序)从根上消除。
|
|
60
64
|
|
|
65
|
+
> **实践验证**:多租户 AI 平台(`apps/agent-platform`——14 页 + 部门聊天 + 知识库 + HITL 审批)已完全运行在框架上:auth(userSystem)/ AI 引擎(ai)/ 实时消息(messager)/ UI(48 组件)/ 数据管道(ctx.api)零自研替代。框架哲学(中间件注入、诚实裁剪、机制与策略分离)经受住了真实复杂应用的检验——这也是我们确定「哪些进框架、哪些留应用层」的依据。
|
|
66
|
+
|
|
61
67
|
---
|
|
62
68
|
|
|
63
69
|
## 快速开始
|
|
@@ -237,7 +243,7 @@ createApp().use(router({ routes })).mount('#root', RouteView, { hydrate: true })
|
|
|
237
243
|
|------|---------|------|
|
|
238
244
|
| `weifuwu/client` | `https://unpkg.com/weifuwu@latest/dist/client/index.js` | 客户端核心(createApp, h, 路由, 状态管理等) |
|
|
239
245
|
| `weifuwu/components` | `https://unpkg.com/weifuwu@latest/dist/components/index.js` | 48 个 UI 组件(Button, Card, Table, Modal, Icon 等) |
|
|
240
|
-
|
|
|
246
|
+
| `weifuwu/components` | `https://unpkg.com/weifuwu@latest/dist/components/style.css` | 组件 CSS + 141 个主题 Token + 67 个布局原语 |
|
|
241
247
|
| 独立布局系统 | `https://unpkg.com/weifuwu@latest/dist/layout/weifuwu-layout.css` | 仅 CSS 布局,不依赖 JS |
|
|
242
248
|
|
|
243
249
|
|
|
@@ -257,9 +263,10 @@ createApp().use(router({ routes })).mount('#root', RouteView, { hydrate: true })
|
|
|
257
263
|
| `weifuwu` | **uiSsr** | 路由级 SSR:匹配 routes → 自动完整 HTML + `__DATA__` + bundle | Router, ui |
|
|
258
264
|
| `weifuwu` | **rateLimit** | 限流中间件(fixed/sliding,redis 多实例原子)→ `ctx.limit` | Router, redis |
|
|
259
265
|
| `weifuwu` | **email** | 邮件发送(Resend/SMTP 自研/自定义适配器)→ `ctx.email` | Router |
|
|
260
|
-
| `weifuwu` | **userSystem** | 用户系统(scrypt 密码哈希 +
|
|
266
|
+
| `weifuwu` | **userSystem** | 用户系统(scrypt 密码哈希 + 混合会话 + 多租户感知)→ `ctx.user` / `ctx.auth` / `ctx.tenantId` + `/api/auth/*` | Router, postgres |
|
|
267
|
+
| `weifuwu` | **messager** | 消息系统(会话/消息持久化 + WS 实时投递 + Redis 跨进程广播)→ `ctx.msg` + `/api/messages/*` | Router, postgres, (redis) |
|
|
261
268
|
| `weifuwu` | **queue** | 可靠任务队列(Redis Streams,at-least-once + DLQ)→ `ctx.queue` | Router, redis |
|
|
262
|
-
| `weifuwu` | **ai** | LLM 对话(自研 OpenAI 兼容协议 + 自研 SSE 解码,默认 DeepSeek)→ `ctx.ai` + `ctx.ui.useChat` + `AiChat` | Router |
|
|
269
|
+
| `weifuwu` | **ai** | LLM 对话(自研 OpenAI 兼容协议 + 自研 SSE 解码,默认 DeepSeek)→ `ctx.ai` + embedding + `ctx.ui.useChat` + `AiChat` | Router |
|
|
263
270
|
| `weifuwu/dev` | **dev loader** | Node loader:服务端直接跑 `.ts/.tsx`(`--import weifuwu/dev`) | esbuild |
|
|
264
271
|
| `weifuwu` | **graphql** | GraphQL 端点(支持 GraphiQL) | Router |
|
|
265
272
|
| `weifuwu` | **createMiddleware** | 类型安全中间件工厂 | — |
|
|
@@ -544,7 +551,7 @@ await server.stop(2000) // 超时毫秒
|
|
|
544
551
|
| `hostname` | `string` | `'0.0.0.0'` | 监听地址 |
|
|
545
552
|
| `signal` | `AbortSignal` | — | 通过信号停止 |
|
|
546
553
|
| `maxBodySize` | `number` | `10MB` | 请求体上限(0=无限) |
|
|
547
|
-
| `timeout` | `number` | `
|
|
554
|
+
| `timeout` | `number` | `120000` | Socket 超时(ms,2 分钟,适配 LLM 生成等长任务) |
|
|
548
555
|
| `keepAliveTimeout` | `number` | `5000` | Keep-Alive 超时 |
|
|
549
556
|
| `headersTimeout` | `number` | `6000` | 请求头超时 |
|
|
550
557
|
| `shutdown` | `boolean` | `true` | 自动注册 SIGTERM/SIGINT |
|
|
@@ -1070,6 +1077,8 @@ app.wsHub(redisHub)
|
|
|
1070
1077
|
|
|
1071
1078
|
WebSocket 原生 `ws.send()` 发送,`ws.on('message', cb)` WebSocket 接收。
|
|
1072
1079
|
|
|
1080
|
+
> **实时应用推荐用 `messager()`**(SaaS 地基模块):协议内置(`connected/subscribe/ping`)+ 持久化 + 跨进程广播 + 点对点,不必自写 Hub/协议——见[消息系统章节](#messager--消息系统)。
|
|
1081
|
+
|
|
1073
1082
|
---
|
|
1074
1083
|
|
|
1075
1084
|
## HttpError — HTTP 错误
|
|
@@ -1086,13 +1095,14 @@ app.get('/secure', () => {
|
|
|
1086
1095
|
| API | 说明 |
|
|
1087
1096
|
|-----|------|
|
|
1088
1097
|
| `new HttpError(msg, status)` | 创建 HTTP 错误,name = 'HttpError' |
|
|
1089
|
-
|
|
1098
|
+
|
|
1099
|
+
> 请求体上限常量 `DEFAULT_MAX_BODY`(10MB)见上方 serve 选项表 `maxBodySize`。
|
|
1090
1100
|
|
|
1091
1101
|
---
|
|
1092
1102
|
|
|
1093
1103
|
## 响应辅助函数
|
|
1094
1104
|
|
|
1095
|
-
> 以下为完整 API
|
|
1105
|
+
> 以下为完整 API 参考,按需查阅。五个 SaaS 地基模块(rateLimit / email / userSystem / messager / queue)见文末「SaaS 地基模块」章节。
|
|
1096
1106
|
|
|
1097
1107
|
消除 `Response.json(...)` 重复模式:
|
|
1098
1108
|
|
|
@@ -1161,6 +1171,7 @@ import type { CORSOptions } from 'weifuwu'
|
|
|
1161
1171
|
import type { ServeStaticOptions } from 'weifuwu'
|
|
1162
1172
|
import type { PostgresOptions, PostgresClient, PostgresInjected } from 'weifuwu'
|
|
1163
1173
|
import type { RedisOptions, RedisClient, RedisInjected } from 'weifuwu'
|
|
1174
|
+
import type { MessagerOptions, MessagerClient, MessagerInjected } from 'weifuwu'
|
|
1164
1175
|
import type { GraphQLOptions, GraphQLHandler } from 'weifuwu'
|
|
1165
1176
|
```
|
|
1166
1177
|
|
|
@@ -1981,7 +1992,7 @@ createApp()
|
|
|
1981
1992
|
locale: 'zh-CN',
|
|
1982
1993
|
messages: {
|
|
1983
1994
|
'title': '仪表盘',
|
|
1984
|
-
'welcome': '
|
|
1995
|
+
'welcome': '欢迎光临',
|
|
1985
1996
|
},
|
|
1986
1997
|
}))
|
|
1987
1998
|
.mount('#root', App)
|
|
@@ -2216,7 +2227,7 @@ import type { RouterOptions } from 'weifuwu/client'
|
|
|
2216
2227
|
|
|
2217
2228
|
# 组件库 (`weifuwu/components`)
|
|
2218
2229
|
|
|
2219
|
-
|
|
2230
|
+
48 个 HTML 原语组件。每个是 `(_init, ctx) => (props) => VNode`(两阶段组件,与前端框架同一模型),引用 `--wf-*` CSS 变量做主题。另含 `confirm()` / `toast()` 命令式中间件。
|
|
2220
2231
|
|
|
2221
2232
|
```ts
|
|
2222
2233
|
import { Button, Input, Table, Modal, Toast } from 'weifuwu/components'
|
|
@@ -2263,35 +2274,35 @@ import 'weifuwu/components/style.css' // 包含 Token + 67 布局原语 + 组
|
|
|
2263
2274
|
<Alert variant="warning" closable>注意:磁盘空间不足</Alert>
|
|
2264
2275
|
|
|
2265
2276
|
// ├─ 标签 / 徽标 / 头像
|
|
2266
|
-
<Badge
|
|
2267
|
-
<Badge variant="success">通过</Badge>
|
|
2268
|
-
<Tag variant="
|
|
2277
|
+
<Badge variant="primary">消息</Badge>
|
|
2278
|
+
<Badge variant="success" dot>通过</Badge>
|
|
2279
|
+
<Tag variant="primary" closable onClose={() => {}}>标签</Tag>
|
|
2269
2280
|
<Avatar name="张三" size="lg" />
|
|
2270
2281
|
|
|
2271
2282
|
// ├─ 卡片 / 统计卡片
|
|
2272
|
-
<Card
|
|
2273
|
-
<StatCard
|
|
2283
|
+
<Card variant="outlined" padding="md">卡片内容</Card>
|
|
2284
|
+
<StatCard label="总用户" value="1,234" trend="up" trendLabel="12%" />
|
|
2274
2285
|
|
|
2275
2286
|
// ├─ 标签页 / 下拉菜单
|
|
2276
|
-
<Tabs items={[{ key: 'a', label: '标签A' }, { key: 'b', label: '标签B' }]}
|
|
2277
|
-
<Dropdown items={[{ label: '编辑', onClick: () => {} }, { label: '删除',
|
|
2287
|
+
<Tabs items={[{ key: 'a', label: '标签A' }, { key: 'b', label: '标签B' }]} active="a" onChange={setTab} />
|
|
2288
|
+
<Dropdown items={[{ label: '编辑', onClick: () => {} }, { label: '删除', variant: 'danger' }]}>操作</Dropdown>
|
|
2278
2289
|
|
|
2279
2290
|
// ├─ 分页 / 步骤条
|
|
2280
2291
|
<Pagination total={100} page={1} pageSize={10} onChange={setPage} />
|
|
2281
|
-
<Steps items={[{
|
|
2292
|
+
<Steps items={[{ key: 's1', label: '第一步' }, { key: 's2', label: '第二步' }]} current={1} />
|
|
2282
2293
|
|
|
2283
2294
|
// ├─ 滑块 / 进度条
|
|
2284
2295
|
<Slider min={0} max={100} value={50} onChange={setValue} />
|
|
2285
|
-
<ProgressBar value={75}
|
|
2296
|
+
<ProgressBar value={75} label="75%" />
|
|
2286
2297
|
|
|
2287
2298
|
// ├─ 面包屑 / 分割线
|
|
2288
2299
|
<Breadcrumb items={[{ label: '首页' }, { label: '用户管理' }]} />
|
|
2289
2300
|
<Divider />
|
|
2290
|
-
<Divider
|
|
2301
|
+
<Divider>分割文字</Divider>
|
|
2291
2302
|
|
|
2292
2303
|
// ├─ 加载 / 空状态 / 骨架屏
|
|
2293
2304
|
<Loading text="加载中..." />
|
|
2294
|
-
<EmptyState
|
|
2305
|
+
<EmptyState text="暂无数据" hint="请先创建一条记录"><Button>新建</Button></EmptyState>
|
|
2295
2306
|
<Skeleton variant="text" lines={3} />
|
|
2296
2307
|
<Skeleton variant="table" lines={5} cols={4} />
|
|
2297
2308
|
<Skeleton variant="avatar" />
|
|
@@ -2356,40 +2367,42 @@ props 变化 ──────────────────────
|
|
|
2356
2367
|
|-----|--------|-----------|------|
|
|
2357
2368
|
| Button | `Button` | `variant`, `size`, `loading`, `disabled`, `block`, `type` | 按钮 |
|
|
2358
2369
|
| Input | `Input` | `label`, `name`, `type`, `value`, `placeholder`, `required`, `disabled`, `error`, `hint`, `onInput`, `onChange` | 输入框 |
|
|
2359
|
-
| Textarea | `Textarea` | `rows`, `
|
|
2370
|
+
| Textarea | `Textarea` | `rows`, `maxLength`, `showCount`, `error` | 文本域 |
|
|
2360
2371
|
| Select | `Select` | `options: SelectOption[]`, `placeholder`, `searchable` | 下拉选择 |
|
|
2361
2372
|
|
|
2362
2373
|
### 表单选择
|
|
2363
2374
|
|
|
2364
2375
|
| 组件 | 导入名 | 关键 Props | 说明 |
|
|
2365
2376
|
|-----|--------|-----------|------|
|
|
2366
|
-
| Checkbox | `Checkbox` | `checked`, `label`, `
|
|
2367
|
-
| Switch | `Switch` | `checked`, `
|
|
2377
|
+
| Checkbox | `Checkbox` | `checked`, `label`, `onChange` | 复选框 |
|
|
2378
|
+
| Switch | `Switch` | `checked`, `label`, `onChange` | 开关 |
|
|
2368
2379
|
| RadioGroup | `RadioGroup` | `options: RadioOption[]`, `value`, `name` | 单选组 |
|
|
2369
|
-
| Slider | `Slider` | `min`, `max`, `step`, `value`, `
|
|
2380
|
+
| Slider | `Slider` | `min`, `max`, `step`, `value`, `onChange` | 滑块 |
|
|
2370
2381
|
|
|
2371
2382
|
### 表单增强
|
|
2372
2383
|
|
|
2373
2384
|
| 组件 | 导入名 | 关键 Props | 说明 |
|
|
2374
2385
|
|-----|--------|-----------|------|
|
|
2375
2386
|
| Form | `Form` | `onSubmit`, `validation` | 表单容器 |
|
|
2376
|
-
| Field | `Field` | `label`, `error`, `required`, `
|
|
2377
|
-
| FileUpload | `FileUpload` | `accept`, `multiple`, `maxSize`, `
|
|
2378
|
-
| SearchInput | `SearchInput` | `value`, `placeholder`, `
|
|
2379
|
-
|
|
|
2387
|
+
| Field | `Field` | `label`, `error`, `required`, `hint` | 字段包装 |
|
|
2388
|
+
| FileUpload | `FileUpload` | `accept`, `multiple`, `maxSize`, `onChange` | 文件上传 |
|
|
2389
|
+
| SearchInput | `SearchInput` | `value`, `placeholder`, `onInput`, `onClear` | 搜索框 |
|
|
2390
|
+
| SegmentedControl | `SegmentedControl` | `options: SegmentedOption[]`, `value`, `onChange`, `size` | 分段选择器 |
|
|
2391
|
+
| ProgressBar | `ProgressBar` | `value`, `max`, `label`, `showValue` | 进度条 |
|
|
2380
2392
|
|
|
2381
2393
|
### 数据展示
|
|
2382
2394
|
|
|
2383
2395
|
| 组件 | 导入名 | 关键 Props | 说明 |
|
|
2384
2396
|
|-----|--------|-----------|------|
|
|
2385
|
-
| Table | `Table` | `columns: TableColumn[]`, `data`, `loading`, `
|
|
2386
|
-
| Card | `Card` | `
|
|
2387
|
-
| Badge | `Badge` | `variant: BadgeVariant`, `
|
|
2388
|
-
| Tag | `Tag` | `variant`, `closable`, `onClose` | 标签 |
|
|
2389
|
-
| Avatar | `Avatar` | `src`, `name`, `size`, `
|
|
2390
|
-
|
|
|
2391
|
-
|
|
|
2392
|
-
|
|
|
2397
|
+
| Table | `Table` | `columns: TableColumn[]`, `data`, `loading`, `sortKey`, `sortOrder`, `onSort`, `onRowClick` | 表格 |
|
|
2398
|
+
| Card | `Card` | `variant`, `outlined`, `padding`, `clickable`, `hover`, `active`, `onClick` | 卡片 |
|
|
2399
|
+
| Badge | `Badge` | `variant: BadgeVariant`, `dot` | 徽标 |
|
|
2400
|
+
| Tag | `Tag` | `variant: 'default'\|'primary'\|'success'\|'danger'`, `closable`, `onClose` | 标签 |
|
|
2401
|
+
| Avatar | `Avatar` | `src`, `name`, `size`, `color` | 头像 |
|
|
2402
|
+
| Icon | `Icon` | `name: IconName`, `size` | 图标(内置 25 个 stroke 图标,currentColor 随字号) |
|
|
2403
|
+
| StatCard | `StatCard` | `label`, `value`, `trend: 'up'\|'down'`, `trendLabel`, `icon`, `animate` | 统计卡片 |
|
|
2404
|
+
| PageHeader | `PageHeader` | `title`, `sub`, `display` | 页面标题(actions 放 children) |
|
|
2405
|
+
| Img | `Img` | `src`, `alt`, `fallback`, `loading`, `width`, `height` | 图片(含 fallback) |
|
|
2393
2406
|
| InView | `InView` | `once`, `threshold`, `rootMargin`, `placeholder`, `onEnter` | 进入视窗后懒加载内容 |
|
|
2394
2407
|
|
|
2395
2408
|
### 数据反馈
|
|
@@ -2398,13 +2411,13 @@ props 变化 ──────────────────────
|
|
|
2398
2411
|
|-----|--------|-----------|------|
|
|
2399
2412
|
| Modal | `Modal` | `open`, `title`, `onClose`, `width`, `footer`, `closable` | 模态框 |
|
|
2400
2413
|
| Confirm | `Confirm` | `open`, `message`, `confirmText`, `cancelText`, `variant`, `onConfirm`, `onCancel` | 确认对话框(同 `ctx.confirm()` 命令式) |
|
|
2401
|
-
| Drawer | `Drawer` | `open`, `title`, `
|
|
2414
|
+
| Drawer | `Drawer` | `open`, `title`, `position: DrawerPosition`, `onClose`, `footer` | 抽屉 |
|
|
2402
2415
|
| Tooltip | `Tooltip` | `content`, `position: TooltipPosition`, `disabled` | 工具提示(hover/focus 触发) |
|
|
2403
2416
|
| Popover | `Popover` | `content`, `position: PopoverPosition`, `trigger`, `open`, `onOpenChange`, `disabled` | 弹出层 |
|
|
2404
|
-
| Toast | `Toast` | `
|
|
2405
|
-
| Alert | `Alert` | `variant: AlertVariant`, `
|
|
2406
|
-
| Loading | `Loading` | `
|
|
2407
|
-
| EmptyState | `EmptyState` | `
|
|
2417
|
+
| Toast | `Toast` | `toasts: ToastItem[]`, `position`, `max`, `onRemove` | 消息提示 |
|
|
2418
|
+
| Alert | `Alert` | `variant: AlertVariant`, `closable`, `onClose` | 警告提示(内容放 children) |
|
|
2419
|
+
| Loading | `Loading` | `text` | 加载中 |
|
|
2420
|
+
| EmptyState | `EmptyState` | `icon`, `text`, `hint` | 空状态(操作放 children) |
|
|
2408
2421
|
| Skeleton | `Skeleton` | `variant: SkeletonVariant`, `lines`, `cols`, `width`, `height` | 骨架屏 |
|
|
2409
2422
|
|
|
2410
2423
|
### 导航组件
|
|
@@ -2412,11 +2425,11 @@ props 变化 ──────────────────────
|
|
|
2412
2425
|
| 组件 | 导入名 | 关键 Props | 说明 |
|
|
2413
2426
|
|-----|--------|-----------|------|
|
|
2414
2427
|
| Breadcrumb | `Breadcrumb` | `items: BreadcrumbItem[]` | 面包屑 |
|
|
2415
|
-
| Tabs | `Tabs` | `items: TabItem[]`, `
|
|
2416
|
-
| Dropdown | `Dropdown` | `trigger`, `items: DropdownItem[]`, `open` | 下拉菜单 |
|
|
2428
|
+
| Tabs | `Tabs` | `items: TabItem[]`, `active`, `onChange` | 标签页 |
|
|
2429
|
+
| Dropdown | `Dropdown` | `trigger`, `items: DropdownItem[]`, `open`, `onOpenChange` | 下拉菜单 |
|
|
2417
2430
|
| Pagination | `Pagination` | `total`, `page`, `pageSize`, `onChange` | 分页 |
|
|
2418
|
-
| Steps | `Steps` | `items: StepItem[]
|
|
2419
|
-
| Accordion | `Accordion` | `items: AccordionItem[]`, `multiple
|
|
2431
|
+
| Steps | `Steps` | `items: StepItem[]`(`{ key, label }`), `current`, `active` | 步骤条 |
|
|
2432
|
+
| Accordion | `Accordion` | `items: AccordionItem[]`, `multiple` | 手风琴 |
|
|
2420
2433
|
|
|
2421
2434
|
### 图表
|
|
2422
2435
|
|
|
@@ -2430,7 +2443,7 @@ props 变化 ──────────────────────
|
|
|
2430
2443
|
|
|
2431
2444
|
| 组件 | 导入名 | 关键 Props | 说明 |
|
|
2432
2445
|
|-----|--------|-----------|------|
|
|
2433
|
-
| Divider | `Divider` | `
|
|
2446
|
+
| Divider | `Divider` | `vertical` | 分割线(水平带文字放 children,`vertical` 垂直) |
|
|
2434
2447
|
|
|
2435
2448
|
### AI 交互原语(wf: 协议配套)
|
|
2436
2449
|
|
|
@@ -2787,7 +2800,7 @@ const LoginPage = (_init, ctx) => {
|
|
|
2787
2800
|
|
|
2788
2801
|
return (props) =>
|
|
2789
2802
|
h('div', { class: 'wf-stack', style: { maxWidth: 400, margin: '40px auto' } },
|
|
2790
|
-
h(Card, {
|
|
2803
|
+
h(Card, { padding: 'lg' },
|
|
2791
2804
|
h('div', { class: 'wf-stack', style: { gap: 'var(--wf-space-md)' } },
|
|
2792
2805
|
h('h2', {}, '登录'),
|
|
2793
2806
|
h(Form, {
|
|
@@ -2835,7 +2848,7 @@ const UserList = (_init, ctx) => {
|
|
|
2835
2848
|
|
|
2836
2849
|
return h('div', { class: 'wf-stack', style: { gap: 'var(--wf-space-md)' } },
|
|
2837
2850
|
h('div', { class: 'wf-row', style: { justifyContent: 'space-between', alignItems: 'center' } },
|
|
2838
|
-
h(SearchInput, { placeholder: '搜索用户...', value: $.keyword,
|
|
2851
|
+
h(SearchInput, { placeholder: '搜索用户...', value: $.keyword, onInput: (e: Event) => { $.keyword = (e.target as HTMLInputElement).value } }),
|
|
2839
2852
|
h(Button, { variant: 'primary' }, '新建用户'),
|
|
2840
2853
|
),
|
|
2841
2854
|
h(Table, {
|
|
@@ -2925,7 +2938,9 @@ docker compose up -d
|
|
|
2925
2938
|
|
|
2926
2939
|
---
|
|
2927
2940
|
|
|
2928
|
-
# SaaS 地基模块(rateLimit / email / userSystem / queue)
|
|
2941
|
+
# SaaS 地基模块(rateLimit / email / userSystem / messager / queue)
|
|
2942
|
+
|
|
2943
|
+
五个模块(限流 / 邮件 / 用户系统 / 消息系统 / 队列)以中间件形态随包提供,`app.use(...)` 一行接入。
|
|
2929
2944
|
|
|
2930
2945
|
四个内建模块组成一个"基本 SaaS 底座":认证、异步任务、限流、邮件——零新增依赖
|
|
2931
2946
|
(只依赖已自研的 redis / postgres 客户端与 node 标准库)。
|
|
@@ -2942,8 +2957,8 @@ app.get('/api/search', async (req, ctx) => {
|
|
|
2942
2957
|
await ctx.limit('search', { max: 30, windowMs: 60_000 }) // 手动限流,超限抛 429
|
|
2943
2958
|
})
|
|
2944
2959
|
|
|
2945
|
-
// 登录防爆破(配合 userSystem):组合键 ip:email
|
|
2946
|
-
app.use(rateLimit({ key: (req) => `login:${req.
|
|
2960
|
+
// 登录防爆破(配合 userSystem):组合键 ip:email(key 接收标准 Request,取头拿 IP)
|
|
2961
|
+
app.use(rateLimit({ key: (req) => `login:${req.headers.get('x-forwarded-for')}:${req.headers.get('x-user-email')}`, max: 5, windowMs: 15 * 60_000 }))
|
|
2947
2962
|
```
|
|
2948
2963
|
|
|
2949
2964
|
| 选项 | 默认 | 说明 |
|
|
@@ -2993,7 +3008,36 @@ app.post('/secure', (req, ctx) => { ctx.auth.requireAuth(); ... })
|
|
|
2993
3008
|
- **安全基线**:scrypt 密码哈希(per-user salt + timing-safe,异步不阻塞);access token = HMAC-SHA256 JWT(与 `weifuwu/client` 的 `auth()` 天然配对);refresh token = 不透明随机串,DB 只存哈希,logout/轮换即撤销
|
|
2994
3009
|
- **防枚举**:登录失败统一 401(不泄露邮箱是否存在)
|
|
2995
3010
|
- **`ctx.auth` 方法面**:`register` / `login` / `logout` / `requireAuth` / `setPassword(userId, newPwd)` / `createToken(type, payload, { ttlSeconds })`(邮箱验证/密码重置自接)
|
|
2996
|
-
-
|
|
3011
|
+
- **多租户感知**:`issueSession` 的 token payload 携带 `tenantId`(来自 `user.tenant`)——中间件自动注入 `ctx.tenantId`,并将会话字段(userId/tenantId/email/name/role)合并到 `ctx.auth`,多租户应用免写 token 解码/租户中间件(数据隔离 SQL 是应用职责)
|
|
3012
|
+
- **`routes` 支持 `exclude`**:`users.routes(app, { exclude: ['register'] })`——应用自定义注册流程(如注册时建租户)时跳过框架路由
|
|
3013
|
+
- **裁剪**:OAuth、邮箱验证邮件(给底层 API 自接)、多因素、RBAC 权限引擎(只留 `role` 字段)、租户隔离 SQL(框架只做感知,`WHERE tenant_id` 属应用层)
|
|
3014
|
+
|
|
3015
|
+
## messager — 消息系统
|
|
3016
|
+
|
|
3017
|
+
```ts
|
|
3018
|
+
import { messager } from 'weifuwu'
|
|
3019
|
+
|
|
3020
|
+
const db = postgres()
|
|
3021
|
+
await db.migrate()
|
|
3022
|
+
const msg = messager({ sql: db.sql, redis: rds }) // redis 可选:跨进程广播
|
|
3023
|
+
await msg.migrate() // 幂等建表(conversations + members + messages)
|
|
3024
|
+
app.use(db)
|
|
3025
|
+
app.use(msg) // 注入 ctx.msg
|
|
3026
|
+
msg.routes(app) // /api/messages/*(会话/历史/发消息/已读)
|
|
3027
|
+
app.ws('/ws', msg.handler()) // 标准 WS 协议内置
|
|
3028
|
+
|
|
3029
|
+
// 业务代码:持久化 + 鉴权 + 广播 + 未读 + 历史,一次调用
|
|
3030
|
+
const conv = await ctx.msg.createConversation(ctx.user.id, { type: 'group', memberIds: ['u2'] })
|
|
3031
|
+
await ctx.msg.sendMessage(conv.id, { senderType: 'user', senderId: ctx.user.id, content: '你好' })
|
|
3032
|
+
ctx.msg.broadcast(`conv:${conv.id}`, { type: 'order_chat', orderId: 'o1' }) // 任意实时事件
|
|
3033
|
+
ctx.msg.sendTo('u2', { type: 'mention' }) // 用户维度点对点
|
|
3034
|
+
```
|
|
3035
|
+
|
|
3036
|
+
- **数据模型**:`_weifuwu_conversations` / `_weifuwu_conversation_members` / `_weifuwu_messages`(`sender_type + sender_id` 不 FK users——user/agent/system 消息天然可存);direct 会话同对用户唯一、历史游标分页、未读数(`last_read_at`)、编辑/删除软删
|
|
3037
|
+
- **实时协议内置**:`handler()` 提供 `connected / subscribe→subscribed / unsubscribe / ping→pong`——前端 `ctx.ws.send({ type: 'subscribe', room })` 直接可用,两端协议由框架定义
|
|
3038
|
+
- **跨进程**:`redis` 选项 → Redis pub/sub 广播(psubscribe 模式),多实例部署天然一致;无 redis 优雅降级单进程
|
|
3039
|
+
- **与 userSystem 咬合**:`sendTo(ctx.user.id)` 按身份路由、`createConversation(ctx.user.id)` 创建者即身份、成员校验自动对齐——身份是消息的路由,消息是身份的交互
|
|
3040
|
+
- **裁剪**:已读回执状态机(只做未读数)、附件存储、全文搜索、消息确认/重试(可靠投递用 queue)、移动端推送
|
|
2997
3041
|
|
|
2998
3042
|
## queue — 可靠任务队列
|
|
2999
3043
|
|
|
@@ -3100,15 +3144,17 @@ return () => <AiChat chat={$} />
|
|
|
3100
3144
|
|
|
3101
3145
|
- **协议**:`wf:` 命名空间(message_start/token/tool_call/tool_progress/usage/done/error + agent 扩展 step/approval_request),SSE 下行 + POST 上行,错误即值、未知事件透传、`x:*` 自定义事件(详见 [docs/ai-contract.md](./docs/ai-contract.md))
|
|
3102
3146
|
- **agent 引擎**:`a.agent({ systemPrompt, tools, humanInTheLoop })` 工具循环(LLM → tool_call → 执行 → 回喂 → 重复);工具可 `emit` 进度/自定义事件、接收 `signal` 取消;HITL 审批(`ctx.ai.approve` 响应,拒绝≠终止、modified 改参、超时兜底)
|
|
3147
|
+
- **emitter 抽象**:`agent.stream(messages, { emit })`——`wf:*` 事件(step/token/tool_result/usage/done)可接任意通道(SSE/WS/回调),协议不焊死在传输层;`agent.runToResult(messages)` 返回结构化结果 `{ content, steps, usage }`(非流式/worker 场景)
|
|
3148
|
+
- **embedding**:`ctx.ai.embed(text)` / `embedMany(texts)` 向量化(默认 `DASHSCOPE_API_KEY` + `text-embedding-v4`,compatible-mode 端点);未配置抛 `AiError('unsupported')`(惰性检查,不静默降级)——知识库/语义检索开箱即用
|
|
3103
3149
|
- **零依赖**:自研 OpenAI 兼容客户端(fetch + SSE 解析),默认 DeepSeek,`baseUrl` 可换任意 OpenAI 兼容端点(Ollama/vLLM/Moonshot…)
|
|
3104
3150
|
- **追踪**:前端自动生成 `X-Trace-Id` → 后端以之作为 `message_start.id` → 工具内请求继承同一 traceId,整个 agent run 一次搜完
|
|
3105
|
-
- **裁剪**:
|
|
3151
|
+
- **裁剪**:Anthropic 原生协议、审批持久化(连接断=会话亡)暂不支持;多 agent 编排不承诺(子 agent = 工具已支持);embedding 仅文本(图片/多模态不做)
|
|
3106
3152
|
|
|
3107
3153
|
## 组合示例:注册 → 验证邮件 → 欢迎任务 → 登录防爆破
|
|
3108
3154
|
|
|
3109
3155
|
```ts
|
|
3110
3156
|
app.use(redis())
|
|
3111
|
-
app.use(rateLimit({ key: (req) => `login:${req.
|
|
3157
|
+
app.use(rateLimit({ key: (req) => `login:${req.headers.get('x-forwarded-for')}`, max: 5, windowMs: 60_000 })) // 防爆破
|
|
3112
3158
|
app.use(email({ from: 'no-reply@x.com', adapter: 'resend', resend: { apiKey } }))
|
|
3113
3159
|
app.use(db)
|
|
3114
3160
|
app.use(users)
|
package/dist/core/router.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Context, type Handler, type Middleware, type ErrorHandler, type Closeable } from '../types.ts';
|
|
2
|
-
import { type WebSocketHandler, type WsUpgradeHandler } from './ws.ts';
|
|
2
|
+
import { type WebSocketHandler, type WsUpgradeHandler, type Hub } from './ws.ts';
|
|
3
3
|
import type { GraphQLHandler } from '../graphql.ts';
|
|
4
4
|
/**
|
|
5
5
|
* WebSocket room hub — manages pub/sub groups for real-time messaging.
|
|
@@ -10,12 +10,7 @@ import type { GraphQLHandler } from '../graphql.ts';
|
|
|
10
10
|
* The default implementation is in-memory (single process).
|
|
11
11
|
* Pass a custom Hub with Redis backend for multi-instance deployments.
|
|
12
12
|
*/
|
|
13
|
-
export
|
|
14
|
-
join(key: string, ws: import('ws').WebSocket): void;
|
|
15
|
-
leave(ws: import('ws').WebSocket): void;
|
|
16
|
-
send(key: string, message: string): void;
|
|
17
|
-
close(): Promise<void>;
|
|
18
|
-
}
|
|
13
|
+
export type { Hub } from './ws.ts';
|
|
19
14
|
export declare class Router<T extends object = Context> {
|
|
20
15
|
private root;
|
|
21
16
|
private wsRoot;
|
package/dist/core/ws.d.ts
CHANGED
|
@@ -8,6 +8,21 @@ import { WebSocketServer } from 'ws';
|
|
|
8
8
|
import { Duplex } from 'node:stream';
|
|
9
9
|
import type { IncomingMessage } from 'node:http';
|
|
10
10
|
import type { Context } from '../types.ts';
|
|
11
|
+
/**
|
|
12
|
+
* WebSocket room hub — manages pub/sub groups for real-time messaging.
|
|
13
|
+
*
|
|
14
|
+
* Rooms are identified by string keys. Multiple WebSocket connections
|
|
15
|
+
* can join/leave rooms, and messages are broadcast to all members.
|
|
16
|
+
*
|
|
17
|
+
* The default implementation is in-memory (single process).
|
|
18
|
+
* Pass a custom Hub with Redis backend for multi-instance deployments.
|
|
19
|
+
*/
|
|
20
|
+
export interface Hub {
|
|
21
|
+
join(key: string, ws: import('ws').WebSocket): void;
|
|
22
|
+
leave(ws: import('ws').WebSocket): void;
|
|
23
|
+
send(key: string, message: string): void;
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
}
|
|
11
26
|
/** WebSocket lifecycle handler. */
|
|
12
27
|
export type WebSocketHandler = {
|
|
13
28
|
open?: (ws: import('ws').WebSocket, ctx: Context) => void | Promise<void>;
|
|
@@ -20,6 +35,9 @@ type WsMatch = {
|
|
|
20
35
|
params: Record<string, string>;
|
|
21
36
|
};
|
|
22
37
|
export type WsUpgradeHandler = (req: IncomingMessage, socket: Duplex, head: Buffer) => void;
|
|
23
|
-
/**
|
|
24
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Minimal context shape for WS handler execution.
|
|
40
|
+
* ctx.hub = 路由级 Hub(默认内存,可用 app.wsHub() 替换为 Redis 后端)。
|
|
41
|
+
*/
|
|
42
|
+
export declare function createWsUpgradeHandler(wss: WebSocketServer, matchWs: (segments: string[]) => WsMatch | null, hub: Hub): WsUpgradeHandler;
|
|
25
43
|
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export type { User } from './types.ts';
|
|
|
4
4
|
export { serve, DEFAULT_MAX_BODY } from './core/serve.ts';
|
|
5
5
|
export type { ServeOptions, Server } from './core/serve.ts';
|
|
6
6
|
export { Router } from './core/router.ts';
|
|
7
|
-
export type { Hub } from './core/
|
|
7
|
+
export type { Hub } from './core/ws.ts';
|
|
8
8
|
export type { WebSocketHandler } from './core/ws.ts';
|
|
9
9
|
export type { WebSocket } from './types.ts';
|
|
10
10
|
export { cors } from './middleware/cors.ts';
|
package/dist/index.js
CHANGED
|
@@ -213,7 +213,7 @@ function serve(router, options) {
|
|
|
213
213
|
import { WebSocketServer } from "ws";
|
|
214
214
|
|
|
215
215
|
// src/core/ws.ts
|
|
216
|
-
function createWsUpgradeHandler(wss, matchWs) {
|
|
216
|
+
function createWsUpgradeHandler(wss, matchWs, hub) {
|
|
217
217
|
return (req, socket, head) => {
|
|
218
218
|
const segments = req.url?.split("/").filter(Boolean) ?? [];
|
|
219
219
|
const match = matchWs(segments);
|
|
@@ -223,7 +223,7 @@ function createWsUpgradeHandler(wss, matchWs) {
|
|
|
223
223
|
}
|
|
224
224
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
225
225
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
226
|
-
const ctx = { params: match.params, query: Object.fromEntries(url.searchParams) };
|
|
226
|
+
const ctx = { params: match.params, query: Object.fromEntries(url.searchParams), hub };
|
|
227
227
|
if (match.handler.open) match.handler.open(ws, ctx);
|
|
228
228
|
ws.on("message", (data) => {
|
|
229
229
|
if (match.handler.message) match.handler.message(ws, ctx, data);
|
|
@@ -608,7 +608,8 @@ var Router = class _Router {
|
|
|
608
608
|
websocketHandler() {
|
|
609
609
|
return createWsUpgradeHandler(
|
|
610
610
|
this.wss,
|
|
611
|
-
(segments) => this.matchWsTrie(this.wsRoot, segments)
|
|
611
|
+
(segments) => this.matchWsTrie(this.wsRoot, segments),
|
|
612
|
+
this.hub
|
|
612
613
|
);
|
|
613
614
|
}
|
|
614
615
|
// ── Debug ──────────────────────────────────────────────────
|
package/dist/messager/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* app.use(db)
|
|
7
7
|
* app.use(userSystem({ sql })) // 可选依赖:sender_id 来自 ctx.user
|
|
8
8
|
* app.use(messager({ sql, redis })) // redis 可选:多进程广播
|
|
9
|
-
* app.ws('/ws',
|
|
9
|
+
* app.ws('/ws', msg.handler()) // 实时协议(P2)
|
|
10
10
|
*
|
|
11
11
|
* 数据模型:_weifuwu_conversations / _weifuwu_conversation_members / _weifuwu_messages
|
|
12
12
|
* sender_type + sender_id(不 FK users)——user/agent/system 消息天然可存。
|
package/dist/types.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface User {
|
|
|
24
24
|
tenant?: string;
|
|
25
25
|
[key: string]: unknown;
|
|
26
26
|
}
|
|
27
|
+
import type { Hub } from './core/ws.ts';
|
|
27
28
|
export interface Context {
|
|
28
29
|
params: Record<string, string>;
|
|
29
30
|
query: Record<string, string>;
|
|
@@ -33,6 +34,8 @@ export interface Context {
|
|
|
33
34
|
loaderData?: Record<string, unknown>;
|
|
34
35
|
/** Public environment variables. */
|
|
35
36
|
env?: Record<string, string>;
|
|
37
|
+
/** WebSocket 房间 Hub(仅 WS 连接 ctx 注入;HTTP 请求 ctx 为 undefined) */
|
|
38
|
+
hub?: Hub;
|
|
36
39
|
[key: string]: unknown;
|
|
37
40
|
}
|
|
38
41
|
export type Handler<T extends object = Context> = (req: Request, ctx: T) => Response | Promise<Response>;
|