weifuwu 0.61.1 → 0.62.0

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.
Files changed (41) hide show
  1. package/README.md +148 -57
  2. package/dist/client/diff.d.ts +13 -1
  3. package/dist/client/index.js +3 -3
  4. package/dist/client/popup.d.ts +22 -0
  5. package/dist/client/types.d.ts +4 -0
  6. package/dist/components/AvatarGroup/AvatarGroup.d.ts +21 -0
  7. package/dist/components/CodeBlock/CodeBlock.d.ts +14 -0
  8. package/dist/components/Descriptions/Descriptions.d.ts +21 -0
  9. package/dist/components/Highlight/Highlight.d.ts +13 -0
  10. package/dist/components/Icon/Icon.d.ts +1 -1
  11. package/dist/components/InputNumber/InputNumber.d.ts +25 -0
  12. package/dist/components/List/List.d.ts +18 -0
  13. package/dist/components/Markdown/Markdown.d.ts +14 -0
  14. package/dist/components/Markdown/parser.d.ts +32 -0
  15. package/dist/components/Menu/Menu.d.ts +25 -0
  16. package/dist/components/MessageBubble/MessageBubble.d.ts +20 -0
  17. package/dist/components/PasswordInput/PasswordInput.d.ts +21 -0
  18. package/dist/components/Result/Result.d.ts +17 -0
  19. package/dist/components/TagsInput/TagsInput.d.ts +21 -0
  20. package/dist/components/Timeline/Timeline.d.ts +25 -0
  21. package/dist/components/index.d.ts +27 -0
  22. package/dist/components/index.js +7 -1
  23. package/dist/components/style.css +677 -1
  24. package/dist/core/router.d.ts +2 -7
  25. package/dist/core/ws.d.ts +20 -2
  26. package/dist/db/postgres/connection.d.ts +24 -3
  27. package/dist/db/postgres/pool.d.ts +30 -8
  28. package/dist/db/redis/client.d.ts +43 -0
  29. package/dist/db/redis/connection.d.ts +12 -0
  30. package/dist/db/redis/pipeline.d.ts +26 -0
  31. package/dist/db/redis/pool.d.ts +27 -2
  32. package/dist/index.d.ts +2 -1
  33. package/dist/index.js +1059 -97
  34. package/dist/layout/weifuwu-layout.css +11 -1
  35. package/dist/messager/index.d.ts +1 -1
  36. package/dist/postgres/types.d.ts +2 -1
  37. package/dist/redis/types.d.ts +6 -0
  38. package/dist/scheduler/cron.d.ts +26 -0
  39. package/dist/scheduler/index.d.ts +69 -0
  40. package/dist/types.d.ts +3 -0
  41. package/package.json +2 -4
package/README.md CHANGED
@@ -62,7 +62,7 @@ npm install weifuwu
62
62
 
63
63
  **自研数据层** — `ctx.sql`(PG v3 协议)与 `ctx.redis`(RESP2 协议)为**自研客户端**:确定性输出、行为可预测、统一错误模型。jsonb 自动解码、TTL 安全 API、schema 写前校验——高频痛点(双重编码/parseRow 样板/`'EX'` 参数顺序)从根上消除。
64
64
 
65
- > **实践验证**:多租户 AI 平台(`apps/agent-platform`——14 页 + 部门聊天 + 知识库 + HITL 审批)已完全运行在框架上:auth(userSystem)/ AI 引擎(ai)/ 实时消息(messager)/ UI(48 组件)/ 数据管道(ctx.api)零自研替代。框架哲学(中间件注入、诚实裁剪、机制与策略分离)经受住了真实复杂应用的检验——这也是我们确定「哪些进框架、哪些留应用层」的依据。
65
+ > **实践验证**:多租户 AI 平台(`apps/agent-platform`——14 页 + 部门聊天 + 知识库 + HITL 审批)已完全运行在框架上:auth(userSystem)/ AI 引擎(ai)/ 实时消息(messager)/ UI(61 组件)/ 数据管道(ctx.api)零自研替代。框架哲学(中间件注入、诚实裁剪、机制与策略分离)经受住了真实复杂应用的检验——这也是我们确定「哪些进框架、哪些留应用层」的依据。
66
66
 
67
67
  ---
68
68
 
@@ -164,7 +164,7 @@ createApp().use(router({ routes })).mount('#root', RouteView, { hydrate: true })
164
164
 
165
165
  - 访问页面:SPA 客户端渲染;SSR 页面内容直接进 HTML(`curl /` 可见,SEO)
166
166
  - 改组件刷新即生效,无需构建步骤
167
- - 完整可运行示例见 `apps/demo`(博客页 = SSR + Hydration,SPA 页 = 纯客户端)
167
+ - 完整可运行示例见 `apps/components-demo`(组件 cheatsheet)与 `apps/agent-platform`(全栈 SaaS 应用)
168
168
 
169
169
  > 想**零后端、零构建**最快跑起来?直接跳到下面的「CDN 快速原型」。
170
170
 
@@ -235,14 +235,14 @@ createApp().use(router({ routes })).mount('#root', RouteView, { hydrate: true })
235
235
  </html>
236
236
  ```
237
237
 
238
- 将此 HTML 保存到本地用浏览器打开即可运行。完整的 CDN 示例见 [`apps/html/test.html`](./apps/html/test.html)。
238
+ 将此 HTML 保存到本地用浏览器打开即可运行。
239
239
 
240
240
  ### CDN 资源地址说明
241
241
 
242
242
  | 资源 | CDN 地址 | 说明 |
243
243
  |------|---------|------|
244
244
  | `weifuwu/client` | `https://unpkg.com/weifuwu@latest/dist/client/index.js` | 客户端核心(createApp, h, 路由, 状态管理等) |
245
- | `weifuwu/components` | `https://unpkg.com/weifuwu@latest/dist/components/index.js` | 48 个 UI 组件(Button, Card, Table, Modal, Icon 等) |
245
+ | `weifuwu/components` | `https://unpkg.com/weifuwu@latest/dist/components/index.js` | 61 个 UI 组件(Button, Card, Table, Modal, Icon 等) |
246
246
  | `weifuwu/components` | `https://unpkg.com/weifuwu@latest/dist/components/style.css` | 组件 CSS + 141 个主题 Token + 67 个布局原语 |
247
247
  | 独立布局系统 | `https://unpkg.com/weifuwu@latest/dist/layout/weifuwu-layout.css` | 仅 CSS 布局,不依赖 JS |
248
248
 
@@ -266,6 +266,7 @@ createApp().use(router({ routes })).mount('#root', RouteView, { hydrate: true })
266
266
  | `weifuwu` | **userSystem** | 用户系统(scrypt 密码哈希 + 混合会话 + 多租户感知)→ `ctx.user` / `ctx.auth` / `ctx.tenantId` + `/api/auth/*` | Router, postgres |
267
267
  | `weifuwu` | **messager** | 消息系统(会话/消息持久化 + WS 实时投递 + Redis 跨进程广播)→ `ctx.msg` + `/api/messages/*` | Router, postgres, (redis) |
268
268
  | `weifuwu` | **queue** | 可靠任务队列(Redis Streams,at-least-once + DLQ)→ `ctx.queue` | Router, redis |
269
+ | `weifuwu` | **scheduler** | 计划任务(延时 `ctx.schedule` + cron `ctx.cron`/`ctx.cancelCron`,触发后入队) | Router, redis, queue |
269
270
  | `weifuwu` | **ai** | LLM 对话(自研 OpenAI 兼容协议 + 自研 SSE 解码,默认 DeepSeek)→ `ctx.ai` + embedding + `ctx.ui.useChat` + `AiChat` | Router |
270
271
  | `weifuwu/dev` | **dev loader** | Node loader:服务端直接跑 `.ts/.tsx`(`--import weifuwu/dev`) | esbuild |
271
272
  | `weifuwu` | **graphql** | GraphQL 端点(支持 GraphiQL) | Router |
@@ -282,7 +283,7 @@ createApp().use(router({ routes })).mount('#root', RouteView, { hydrate: true })
282
283
  | `weifuwu/client` | **ErrorBoundary** | 错误边界组件 | createApp |
283
284
  | `weifuwu/client` | **lockScroll/trapFocus** | 滚动锁定 / 焦点陷阱工具 | — |
284
285
  | `weifuwu/client` | **popup** | 弹层 fixed 定位工具(`computeFixedPos` / `computeFixedPosRect`) | — |
285
- | `weifuwu/components` | **48 个组件** | Button/Table/Modal/Confirm/Toast/... + `confirm()` / `toast()` 命令式中间件 | weifuwu/client |
286
+ | `weifuwu/components` | **61 个组件** | Button/Table/Modal/Confirm/Toast/... + `confirm()` / `toast()` 命令式中间件 | weifuwu/client |
286
287
  | `weifuwu/layout` | **CSS 布局** | 67 个布局原语 + 141 个主题 Token(也支持 `weifuwu/layout/style.css`) | — |
287
288
 
288
289
  ---
@@ -551,7 +552,7 @@ await server.stop(2000) // 超时毫秒
551
552
  | `hostname` | `string` | `'0.0.0.0'` | 监听地址 |
552
553
  | `signal` | `AbortSignal` | — | 通过信号停止 |
553
554
  | `maxBodySize` | `number` | `10MB` | 请求体上限(0=无限) |
554
- | `timeout` | `number` | `30000` | Socket 超时(ms |
555
+ | `timeout` | `number` | `120000` | Socket 超时(ms,2 分钟,适配 LLM 生成等长任务) |
555
556
  | `keepAliveTimeout` | `number` | `5000` | Keep-Alive 超时 |
556
557
  | `headersTimeout` | `number` | `6000` | 请求头超时 |
557
558
  | `shutdown` | `boolean` | `true` | 自动注册 SIGTERM/SIGINT |
@@ -669,7 +670,9 @@ app.post('/transfer', async (req, ctx) => {
669
670
  | **int8(超出安全范围)** | **`string`**(防静默丢精度,金额/ID 关键) |
670
671
  | float / numeric | `number` |
671
672
  | boolean | `boolean` |
672
- | text / varchar / uuid / date | `string` |
673
+ | text / varchar / uuid | `string` |
674
+ | **timestamptz** | **`Date`**(带时区,ISO 解析无本地时区魔法) |
675
+ | timestamp / date / interval | `string`(无时区语义——转 Date 按本地时区解析即时区魔法,诚实裁剪不转) |
673
676
  | NULL | `null` |
674
677
 
675
678
  ### 类型层(查询泛型 + schema 写前校验)
@@ -699,9 +702,28 @@ await ctx.sql.insert('decks', { title: 'x', status: 'INVALID' }) // → Validati
699
702
  | `ctx.sql.transaction(fn)` | 事务(回调收到 `{ query }`) |
700
703
  | `ctx.sql.register(table, schema)` | 注册表结构(写前校验) |
701
704
  | `ctx.sql.insert(table, row)` | schema 校验 + 参数化插入 |
705
+ | `ctx.sql.insertMany(table, rows[], { batchSize? })` | **批量插入**:多行 VALUES 单次往返(默认 500/批;所有行键必须一致) |
706
+ | `ctx.sql.update(table, set, where, { returning? })` | **参数化 UPDATE**:SET/WHERE 全部参数化,返回 `affectedRows` |
707
+ | `ctx.sql.delete(table, where)` | **参数化 DELETE**:WHERE 必填(防全表误删),返回 `affectedRows` |
702
708
  | `ctx.sql\`...\` 内嵌片段` | 条件 SQL 片段(嵌套过滤,参数自动重编号) |
703
709
  | `ctx.sql.close()` | 关闭连接池 |
704
710
 
711
+ ### 影响行数(affectedRows)
712
+
713
+ `INSERT / UPDATE / DELETE / MERGE` 的返回行数组带**非枚举** `affectedRows` 属性(不干扰 `deepEqual`/`JSON.stringify`):
714
+
715
+ ```ts
716
+ const r = await ctx.sql`UPDATE messages SET read = true WHERE id = ${id}`
717
+ if (r.affectedRows === 0) return new Response('not found', { status: 404 })
718
+ ```
719
+
720
+ ```ts
721
+ // 批量插入:100 行 1 次往返
722
+ await ctx.sql.insertMany('agent_logs', logs, { batchSize: 500 })
723
+ // 语义化更新/删除:WHERE 全参数化 + 返回影响行数
724
+ await ctx.sql.update('users', { role: 'admin' }, { id: userId })
725
+ await ctx.sql.delete('messages', { id: msgId })
726
+
705
727
  ### 条件片段(嵌套过滤)
706
728
 
707
729
  ```ts
@@ -721,7 +743,8 @@ const rows = await ctx.sql`
721
743
  | `max`(或 `poolSize`) | `number` | `10` | 连接池大小 |
722
744
  | `acquireTimeoutMs` | `number` | `30000` | 池全忙时 acquire 超时(防饿死,0=无限) |
723
745
  | `statementTimeoutMs`(或 `statementTimeout`) | `number` | `0` | 语句超时(慢查询保护,0=禁用) |
724
- | `onQuery` | `(sql, durationMs, rowCount) => void` | | 查询观测钩子(慢查询日志/审计) |
746
+ | `idleTimeoutMs` | `number` | `0` | 空闲连接回收(超时未用关闭,容量收缩后自动重建;0=禁用) |
747
+ | `onQuery` | `(sql, durationMs, rowCount, traceId?) => void` | — | 查询观测钩子;第 4 参数为请求级 traceId(`x-trace-id` 头经 ALS 传播) |
725
748
 
726
749
  ### 幂等迁移(内置)
727
750
 
@@ -816,6 +839,12 @@ await ctx.redis.set('user', 1) // 实际写入 'api:user'
816
839
  | `cache(key, fn, ttl)` | 缓存读-算-写(null 不缓存防穿透) |
817
840
  | `publish(channel, msg)` | Pub-Sub 发布 |
818
841
  | `createSubscriber()` | 独立订阅连接(`subscribe`/`psubscribe` 回调式) |
842
+ | `hset / hget / hgetall / hdel` | hash 字段读写(`hgetall` → `Record`,缺失 `{}`) |
843
+ | `lpush / rpush / lpop / rpop / lrange` | list 队列操作(`lrange` 支持负数区间) |
844
+ | `sadd / srem / smembers` | set 成员操作(`sadd` 重复不加) |
845
+ | `zadd / zrange` | zset 有序集(score 升序) |
846
+ | `mget / mset / exists / setnx / incrby` | 批量读写 / 存在性 / 原子设值(锁基础)/ 增量 |
847
+ | `pipeline()` | 管道:批量命令一次往返(池级,key 自动加前缀) |
819
848
  | `command(name, ...args)` | 底层命令透传 |
820
849
  | `close()` | 关闭连接池 |
821
850
 
@@ -824,11 +853,50 @@ await ctx.redis.set('user', 1) // 实际写入 'api:user'
824
853
  | `url` | `string` | `REDIS_URL` 环境变量 | 连接字符串 |
825
854
  | `poolSize` | `number` | `5` | 连接池大小 |
826
855
  | `keyPrefix` | `string` | `''` | 所有 key 自动加前缀(多应用隔离) |
856
+ | `commandTimeoutMs` | `number` | `0` | 命令超时(阻塞命令 resolve(null);防挂起。0=禁用) |
857
+ | `socketTimeoutMs` | `number` | `0` | socket 响应超时(僵尸连接自愈:pending 有命令且超时无数据 → 主动断开重连。0=禁用) |
858
+
859
+ > **连接健康**:断线自动剔除死连接并重建(池不萎缩);`CLIENT KILL`/网络抖动后服务自愈,命令不命中死连接。
827
860
 
828
861
  > **裁剪声明**:集群(MOVED 路由)/ 哨兵 / 自动管道不支持(standalone 优先)。
829
862
 
830
863
  ---
831
864
 
865
+ ## scheduler — 计划任务(即时/延时/cron)
866
+
867
+ > 依赖 `queue`(触发后入队执行)。三类任务:即时(queue.add 已有)、延时(`ctx.schedule`)、定时(`ctx.cron`)。
868
+
869
+ ```ts
870
+ import { queue, scheduler } from 'weifuwu'
871
+
872
+ const q = queue()
873
+ app.use(q)
874
+ app.use(scheduler({ queue: q })) // 依赖 ctx.queue(触发后入队)
875
+
876
+ // 延时任务(单次):delayMs 或指定时间
877
+ await ctx.schedule('email.send', { to, body }, { delayMs: 30_000 })
878
+ await ctx.schedule('report.build', {}, { when: new Date('2026-09-01T00:00:00Z') })
879
+
880
+ // cron 定时任务(重复):每分钟触发 → 入队执行
881
+ ctx.cron('* * * * *', 'heartbeat.check', { scope: 'health' })
882
+ // 改需求 = 重新注册(同 name 覆盖更新,旧定义不残留)
883
+ ctx.cron('*/5 * * * *', 'heartbeat.check', { scope: 'health' })
884
+ // 停用 = cancel(删定义 + 清理 pending 触发点)
885
+ await ctx.cancelCron('heartbeat.check')
886
+
887
+ // 执行端:与 queue 完全一致
888
+ const worker = ctx.queue.worker('email.send', async (job) => { ... })
889
+ ```
890
+
891
+ - **延时**:ZSET(score=触发时间戳)+ 守护循环(独立连接)→ 到期 `ZREM` 原子抢占(多实例不重复)→ `queue.add`
892
+ - **多应用隔离**:`scheduler({ prefix })`——ZSET/HASH 应用级共享,多应用共用 redis 时必须各自 prefix(同应用多实例共享 prefix = 协作消费)
893
+ - **cron**:HASH 注册表(**field = name,同 name 重新注册 = 覆盖更新**,改表达式不残留旧定义)+ 滚动生成触发点(`ZADD NX` 幂等)→ 复用延时链路;`nextRunAt` 原子推进
894
+ - **取消**:`ctx.cancelCron(name)` 删定义 + 清理 pending 触发点(停用 cron 必须 cancel——定义无 TTL 会累积)
895
+ - **崩溃恢复**:未消费触发点留在 ZSET,重启后补扫立即触发(at-least-once,幂等由业务保证)
896
+ - **cron 表达式**:5 字段(分 时 日 月 周),支持 `*`/步进/列表/范围;时区 = 服务器本地;非法表达式注册即抛错
897
+ - **裁剪**:❌ cron 秒/年/别名(@daily)/特殊字符(L/W/#)、时区配置、单次任务取消(v2)、分布式锁(原子命令抢占替代)
898
+ - **文档红线**:cron 定义持久化在 HASH——进程重启后守护循环恢复即继续触发(无需重新注册);**停用必须 `cancelCron`**(定义无 TTL,不取消会永久触发)
899
+
832
900
  ## ui — SSR 渲染 + JS/CSS 编译
833
901
 
834
902
  ```ts
@@ -1095,7 +1163,8 @@ app.get('/secure', () => {
1095
1163
  | API | 说明 |
1096
1164
  |-----|------|
1097
1165
  | `new HttpError(msg, status)` | 创建 HTTP 错误,name = 'HttpError' |
1098
- | `DEFAULT_MAX_BODY` | `10 * 1024 * 1024` (10MB) |
1166
+
1167
+ > 请求体上限常量 `DEFAULT_MAX_BODY`(10MB)见上方 serve 选项表 `maxBodySize`。
1099
1168
 
1100
1169
  ---
1101
1170
 
@@ -1991,7 +2060,7 @@ createApp()
1991
2060
  locale: 'zh-CN',
1992
2061
  messages: {
1993
2062
  'title': '仪表盘',
1994
- 'welcome': '欢迎, {name}',
2063
+ 'welcome': '欢迎光临',
1995
2064
  },
1996
2065
  }))
1997
2066
  .mount('#root', App)
@@ -2226,7 +2295,7 @@ import type { RouterOptions } from 'weifuwu/client'
2226
2295
 
2227
2296
  # 组件库 (`weifuwu/components`)
2228
2297
 
2229
- 46 个 HTML 原语组件。每个是 `(_init, ctx) => (props) => VNode`(两阶段组件,与前端框架同一模型),引用 `--wf-*` CSS 变量做主题。另含 `confirm()` / `toast()` 命令式中间件。
2298
+ 61 个 HTML 原语组件。每个是 `(_init, ctx) => (props) => VNode`(两阶段组件,与前端框架同一模型),引用 `--wf-*` CSS 变量做主题。另含 `confirm()` / `toast()` 命令式中间件。
2230
2299
 
2231
2300
  ```ts
2232
2301
  import { Button, Input, Table, Modal, Toast } from 'weifuwu/components'
@@ -2273,35 +2342,35 @@ import 'weifuwu/components/style.css' // 包含 Token + 67 布局原语 + 组
2273
2342
  <Alert variant="warning" closable>注意:磁盘空间不足</Alert>
2274
2343
 
2275
2344
  // ├─ 标签 / 徽标 / 头像
2276
- <Badge count={5}>消息</Badge>
2277
- <Badge variant="success">通过</Badge>
2278
- <Tag variant="blue" closable onClose={() => {}}>标签</Tag>
2345
+ <Badge variant="primary">消息</Badge>
2346
+ <Badge variant="success" dot>通过</Badge>
2347
+ <Tag variant="primary" closable onClose={() => {}}>标签</Tag>
2279
2348
  <Avatar name="张三" size="lg" />
2280
2349
 
2281
2350
  // ├─ 卡片 / 统计卡片
2282
- <Card title="卡片标题" extra={<a href="#">更多</a>}>卡片内容</Card>
2283
- <StatCard title="总用户" value="1,234" trend={12.5} variant="primary" />
2351
+ <Card variant="outlined" padding="md">卡片内容</Card>
2352
+ <StatCard label="总用户" value="1,234" trend="up" trendLabel="12%" />
2284
2353
 
2285
2354
  // ├─ 标签页 / 下拉菜单
2286
- <Tabs items={[{ key: 'a', label: '标签A' }, { key: 'b', label: '标签B' }]} activeKey="a" onChange={setTab} />
2287
- <Dropdown items={[{ label: '编辑', onClick: () => {} }, { label: '删除', danger: true }]}>操作</Dropdown>
2355
+ <Tabs items={[{ key: 'a', label: '标签A' }, { key: 'b', label: '标签B' }]} active="a" onChange={setTab} />
2356
+ <Dropdown items={[{ label: '编辑', onClick: () => {} }, { label: '删除', variant: 'danger' }]}>操作</Dropdown>
2288
2357
 
2289
2358
  // ├─ 分页 / 步骤条
2290
2359
  <Pagination total={100} page={1} pageSize={10} onChange={setPage} />
2291
- <Steps items={[{ title: '第一步' }, { title: '第二步' }]} current={1} />
2360
+ <Steps items={[{ key: 's1', label: '第一步' }, { key: 's2', label: '第二步' }]} current={1} />
2292
2361
 
2293
2362
  // ├─ 滑块 / 进度条
2294
2363
  <Slider min={0} max={100} value={50} onChange={setValue} />
2295
- <ProgressBar value={75} variant="success" label="75%" />
2364
+ <ProgressBar value={75} label="75%" />
2296
2365
 
2297
2366
  // ├─ 面包屑 / 分割线
2298
2367
  <Breadcrumb items={[{ label: '首页' }, { label: '用户管理' }]} />
2299
2368
  <Divider />
2300
- <Divider orientation="left">分割文字</Divider>
2369
+ <Divider>分割文字</Divider>
2301
2370
 
2302
2371
  // ├─ 加载 / 空状态 / 骨架屏
2303
2372
  <Loading text="加载中..." />
2304
- <EmptyState title="暂无数据" description="请先创建一条记录" action={<Button>新建</Button>} />
2373
+ <EmptyState text="暂无数据" hint="请先创建一条记录"><Button>新建</Button></EmptyState>
2305
2374
  <Skeleton variant="text" lines={3} />
2306
2375
  <Skeleton variant="table" lines={5} cols={4} />
2307
2376
  <Skeleton variant="avatar" />
@@ -2366,42 +2435,53 @@ props 变化 ──────────────────────
2366
2435
  |-----|--------|-----------|------|
2367
2436
  | Button | `Button` | `variant`, `size`, `loading`, `disabled`, `block`, `type` | 按钮 |
2368
2437
  | Input | `Input` | `label`, `name`, `type`, `value`, `placeholder`, `required`, `disabled`, `error`, `hint`, `onInput`, `onChange` | 输入框 |
2369
- | Textarea | `Textarea` | `rows`, `resize`, `maxLength`, `error` | 文本域 |
2438
+ | Textarea | `Textarea` | `rows`, `maxLength`, `showCount`, `error` | 文本域 |
2370
2439
  | Select | `Select` | `options: SelectOption[]`, `placeholder`, `searchable` | 下拉选择 |
2371
2440
 
2372
2441
  ### 表单选择
2373
2442
 
2374
2443
  | 组件 | 导入名 | 关键 Props | 说明 |
2375
2444
  |-----|--------|-----------|------|
2376
- | Checkbox | `Checkbox` | `checked`, `label`, `indeterminate` | 复选框 |
2377
- | Switch | `Switch` | `checked`, `size` | 开关 |
2445
+ | Checkbox | `Checkbox` | `checked`, `label`, `onChange` | 复选框 |
2446
+ | Switch | `Switch` | `checked`, `label`, `onChange` | 开关 |
2378
2447
  | RadioGroup | `RadioGroup` | `options: RadioOption[]`, `value`, `name` | 单选组 |
2379
- | Slider | `Slider` | `min`, `max`, `step`, `value`, `range` | 滑块 |
2448
+ | Slider | `Slider` | `min`, `max`, `step`, `value`, `onChange` | 滑块 |
2380
2449
 
2381
2450
  ### 表单增强
2382
2451
 
2383
2452
  | 组件 | 导入名 | 关键 Props | 说明 |
2384
2453
  |-----|--------|-----------|------|
2385
2454
  | Form | `Form` | `onSubmit`, `validation` | 表单容器 |
2386
- | Field | `Field` | `label`, `error`, `required`, `help` | 字段包装 |
2387
- | FileUpload | `FileUpload` | `accept`, `multiple`, `maxSize`, `onFiles` | 文件上传 |
2388
- | SearchInput | `SearchInput` | `value`, `placeholder`, `onSearch`, `loading` | 搜索框 |
2455
+ | Field | `Field` | `label`, `error`, `required`, `hint` | 字段包装 |
2456
+ | FileUpload | `FileUpload` | `accept`, `multiple`, `maxSize`, `onChange` | 文件上传 |
2457
+ | SearchInput | `SearchInput` | `value`, `placeholder`, `onInput`, `onClear` | 搜索框 |
2389
2458
  | SegmentedControl | `SegmentedControl` | `options: SegmentedOption[]`, `value`, `onChange`, `size` | 分段选择器 |
2390
- | ProgressBar | `ProgressBar` | `value`, `max`, `variant`, `size`, `label` | 进度条 |
2459
+ | ProgressBar | `ProgressBar` | `value`, `max`, `label`, `showValue` | 进度条 |
2460
+ | InputNumber | `InputNumber` | `value`, `min`, `max`, `step`, `precision`, `onChange` | 数字输入(增减按钮) |
2461
+ | PasswordInput | `PasswordInput` | `value`, `onInput`, `autoComplete` | 密码输入(可见性切换) |
2462
+ | TagsInput | `TagsInput` | `value: string[]`, `maxTags`, `allowDuplicates` | 标签输入(中文输入法感知) |
2391
2463
 
2392
2464
  ### 数据展示
2393
2465
 
2394
2466
  | 组件 | 导入名 | 关键 Props | 说明 |
2395
2467
  |-----|--------|-----------|------|
2396
- | Table | `Table` | `columns: TableColumn[]`, `data`, `loading`, `sortable`, `selectable` | 表格 |
2397
- | Card | `Card` | `title`, `extra`, `shadow`, `padding` | 卡片 |
2398
- | Badge | `Badge` | `variant: BadgeVariant`, `count`, `dot`, `max` | 徽标 |
2399
- | Tag | `Tag` | `variant`, `closable`, `onClose` | 标签 |
2400
- | Avatar | `Avatar` | `src`, `name`, `size`, `shape` | 头像 |
2401
- | Icon | `Icon` | `name: IconName`, `size`, `color`, `strokeWidth` | 图标(内置 100+ 图标,stroke 风格) |
2402
- | StatCard | `StatCard` | `title`, `value`, `trend`, `icon`, `variant` | 统计卡片 |
2403
- | PageHeader | `PageHeader` | `title`, `subtitle`, `actions`, `onBack`, `breadcrumb` | 页面标题 |
2404
- | Img | `Img` | `src`, `alt`, `fallback`, `lazy`, `fit` | 图片(含 fallback) |
2468
+ | Table | `Table` | `columns: TableColumn[]`, `data`, `loading`, `sortKey`, `sortOrder`, `onSort`, `onRowClick` | 表格 |
2469
+ | Card | `Card` | `variant`, `outlined`, `padding`, `clickable`, `hover`, `active`, `onClick` | 卡片 |
2470
+ | Badge | `Badge` | `variant: BadgeVariant`, `dot` | 徽标 |
2471
+ | Tag | `Tag` | `variant: 'default'\|'primary'\|'success'\|'danger'`, `closable`, `onClose` | 标签 |
2472
+ | Avatar | `Avatar` | `src`, `name`, `size`, `color` | 头像 |
2473
+ | AvatarGroup | `AvatarGroup` | `items`, `max`, `size` | 头像组(堆叠 + 溢出 +N) |
2474
+ | Timeline | `Timeline` | `items: TimelineItem[]`, `mode`, `reverse` | 时间线(执行日志/历史) |
2475
+ | Descriptions | `Descriptions` | `items: DescriptionItem[]`, `column`, `bordered` | 描述列表(详情页字段) |
2476
+ | Markdown | `Markdown` | `content` | AI 回复渲染(安全子集 parser) |
2477
+ | CodeBlock | `CodeBlock` | `code`, `lang`, `title` | 代码块(语言标签 + 复制) |
2478
+ | Highlight | `Highlight` | `text`, `query: string \| string[]` | 搜索词高亮(mark) |
2479
+ | List | `List` | `items`, `renderItem`, `divided`, `header/footer/empty` | 通用列表 |
2480
+ | Result | `Result` | `status`, `title`, `desc`, `extra` | 结果页(成功/失败/警告/信息) |
2481
+ | Icon | `Icon` | `name: IconName`, `size` | 图标(内置 25 个 stroke 图标,currentColor 随字号) |
2482
+ | StatCard | `StatCard` | `label`, `value`, `trend: 'up'\|'down'`, `trendLabel`, `icon`, `animate` | 统计卡片 |
2483
+ | PageHeader | `PageHeader` | `title`, `sub`, `display` | 页面标题(actions 放 children) |
2484
+ | Img | `Img` | `src`, `alt`, `fallback`, `loading`, `width`, `height` | 图片(含 fallback) |
2405
2485
  | InView | `InView` | `once`, `threshold`, `rootMargin`, `placeholder`, `onEnter` | 进入视窗后懒加载内容 |
2406
2486
 
2407
2487
  ### 数据反馈
@@ -2410,13 +2490,13 @@ props 变化 ──────────────────────
2410
2490
  |-----|--------|-----------|------|
2411
2491
  | Modal | `Modal` | `open`, `title`, `onClose`, `width`, `footer`, `closable` | 模态框 |
2412
2492
  | Confirm | `Confirm` | `open`, `message`, `confirmText`, `cancelText`, `variant`, `onConfirm`, `onCancel` | 确认对话框(同 `ctx.confirm()` 命令式) |
2413
- | Drawer | `Drawer` | `open`, `title`, `onClose`, `position: DrawerPosition`, `width` | 抽屉 |
2493
+ | Drawer | `Drawer` | `open`, `title`, `position: DrawerPosition`, `onClose`, `footer` | 抽屉 |
2414
2494
  | Tooltip | `Tooltip` | `content`, `position: TooltipPosition`, `disabled` | 工具提示(hover/focus 触发) |
2415
2495
  | Popover | `Popover` | `content`, `position: PopoverPosition`, `trigger`, `open`, `onOpenChange`, `disabled` | 弹出层 |
2416
- | Toast | `Toast` | `items: ToastItem[]`, `position`, `max` | 消息提示 |
2417
- | Alert | `Alert` | `variant: AlertVariant`, `title`, `closable`, `icon` | 警告提示 |
2418
- | Loading | `Loading` | `size`, `text`, `fullscreen` | 加载中 |
2419
- | EmptyState | `EmptyState` | `title`, `description`, `action`, `icon` | 空状态 |
2496
+ | Toast | `Toast` | `toasts: ToastItem[]`, `position`, `max`, `onRemove` | 消息提示 |
2497
+ | Alert | `Alert` | `variant: AlertVariant`, `closable`, `onClose` | 警告提示(内容放 children) |
2498
+ | Loading | `Loading` | `text` | 加载中 |
2499
+ | EmptyState | `EmptyState` | `icon`, `text`, `hint` | 空状态(操作放 children) |
2420
2500
  | Skeleton | `Skeleton` | `variant: SkeletonVariant`, `lines`, `cols`, `width`, `height` | 骨架屏 |
2421
2501
 
2422
2502
  ### 导航组件
@@ -2424,11 +2504,12 @@ props 变化 ──────────────────────
2424
2504
  | 组件 | 导入名 | 关键 Props | 说明 |
2425
2505
  |-----|--------|-----------|------|
2426
2506
  | Breadcrumb | `Breadcrumb` | `items: BreadcrumbItem[]` | 面包屑 |
2427
- | Tabs | `Tabs` | `items: TabItem[]`, `activeKey`, `onChange`, `type` | 标签页 |
2428
- | Dropdown | `Dropdown` | `trigger`, `items: DropdownItem[]`, `open` | 下拉菜单 |
2507
+ | Menu | `Menu` | `items: MenuItem[]`, `activeKey`, `onSelect` | 侧栏导航(分组 + 图标 + 方向键) |
2508
+ | Tabs | `Tabs` | `items: TabItem[]`, `active`, `onChange` | 标签页 |
2509
+ | Dropdown | `Dropdown` | `trigger`, `items: DropdownItem[]`, `open`, `onOpenChange` | 下拉菜单 |
2429
2510
  | Pagination | `Pagination` | `total`, `page`, `pageSize`, `onChange` | 分页 |
2430
- | Steps | `Steps` | `items: StepItem[]`, `current`, `direction`, `size` | 步骤条 |
2431
- | Accordion | `Accordion` | `items: AccordionItem[]`, `multiple`, `defaultActive` | 手风琴 |
2511
+ | Steps | `Steps` | `items: StepItem[]`(`{ key, label }`), `current`, `active` | 步骤条 |
2512
+ | Accordion | `Accordion` | `items: AccordionItem[]`, `multiple` | 手风琴 |
2432
2513
 
2433
2514
  ### 图表
2434
2515
 
@@ -2442,13 +2523,14 @@ props 变化 ──────────────────────
2442
2523
 
2443
2524
  | 组件 | 导入名 | 关键 Props | 说明 |
2444
2525
  |-----|--------|-----------|------|
2445
- | Divider | `Divider` | `orientation`, `plain` | 分割线(水平/垂直/带文字) |
2526
+ | Divider | `Divider` | `vertical` | 分割线(水平带文字放 children,`vertical` 垂直) |
2446
2527
 
2447
2528
  ### AI 交互原语(wf: 协议配套)
2448
2529
 
2449
2530
  | 组件 | 导入名 | 关键 Props | 说明 |
2450
2531
  |-----|--------|-----------|------|
2451
2532
  | AiChat | `AiChat` | `chat`, `maxHeight?`, `labels?`, `renderMessage?`, `renderToolArgs?` | 标准 AI 对话界面:气泡 + 工具卡 + 审批卡 + 自动滚动 + 错误重试(接收 `ctx.ui.useChat()` handle) |
2533
+ | MessageBubble | `MessageBubble` | `content`, `role`, `status`, `actions` | 独立消息气泡(业务聊天页复用) |
2452
2534
  | ToolCallCard | `ToolCallCard` | `call`, `progress?`, `result?`, `renderArgs?` | 工具调用卡片:running(进度条)/ ok / error 三态(协议 §4) |
2453
2535
  | ApprovalCard | `ApprovalCard` | `request`, `status?`, `onApprove`, `onReject` | 人工审批卡片:待批(允许/拒绝+备注)/ 已批 / 已拒 / 超时(协议 §4.5) |
2454
2536
 
@@ -2708,7 +2790,7 @@ app.get('/layout.css', (req, ctx) => ctx.ui.css('weifuwu/layout'))
2708
2790
  </style>
2709
2791
  ```
2710
2792
 
2711
- 完整的零样式示例:`apps/weifuwu-demo`(一个页面 = 组件 + 原语,无 style.css)。
2793
+ 完整的零样式示例:`apps/components-demo`(组件 + 原语即插即用,无手写样式)。
2712
2794
 
2713
2795
  诚实例外(合理场景,仍可内联 `<style>` 解决):打印/PDF 导出规则、第三方库宿主样式、
2714
2796
  业务特有的一次性视觉(如色板选择器交互)。
@@ -2799,7 +2881,7 @@ const LoginPage = (_init, ctx) => {
2799
2881
 
2800
2882
  return (props) =>
2801
2883
  h('div', { class: 'wf-stack', style: { maxWidth: 400, margin: '40px auto' } },
2802
- h(Card, { shadow: 'md' },
2884
+ h(Card, { padding: 'lg' },
2803
2885
  h('div', { class: 'wf-stack', style: { gap: 'var(--wf-space-md)' } },
2804
2886
  h('h2', {}, '登录'),
2805
2887
  h(Form, {
@@ -2847,7 +2929,7 @@ const UserList = (_init, ctx) => {
2847
2929
 
2848
2930
  return h('div', { class: 'wf-stack', style: { gap: 'var(--wf-space-md)' } },
2849
2931
  h('div', { class: 'wf-row', style: { justifyContent: 'space-between', alignItems: 'center' } },
2850
- h(SearchInput, { placeholder: '搜索用户...', value: $.keyword, onSearch: (v: string) => { $.keyword = v } }),
2932
+ h(SearchInput, { placeholder: '搜索用户...', value: $.keyword, onInput: (e: Event) => { $.keyword = (e.target as HTMLInputElement).value } }),
2851
2933
  h(Button, { variant: 'primary' }, '新建用户'),
2852
2934
  ),
2853
2935
  h(Table, {
@@ -2956,8 +3038,16 @@ app.get('/api/search', async (req, ctx) => {
2956
3038
  await ctx.limit('search', { max: 30, windowMs: 60_000 }) // 手动限流,超限抛 429
2957
3039
  })
2958
3040
 
2959
- // 登录防爆破(配合 userSystem):组合键 ip:email
2960
- app.use(rateLimit({ key: (req) => `login:${req.ip}:${req.email}`, max: 5, windowMs: 15 * 60_000 }))
3041
+ // 登录/注册防爆破:ctx.limit 默认按 IP 维度(每 IP 独立计数)
3042
+ app.post('/api/auth/register', async (req, ctx) => {
3043
+ await ctx.limit('register', { max: 5, windowMs: 60_000 }) // 每 IP 每分钟 5 次
3044
+ })
3045
+
3046
+ // 系统级总量限制:scope: 'global' 全局共享维度
3047
+ await ctx.limit('total-jobs', { max: 1000, windowMs: 60_000, scope: 'global' })
3048
+
3049
+ // 登录防爆破(配合 userSystem):组合键 ip:email(key 接收标准 Request,取头拿 IP)
3050
+ app.use(rateLimit({ key: (req) => `login:${req.headers.get('x-forwarded-for')}:${req.headers.get('x-user-email')}`, max: 5, windowMs: 15 * 60_000 }))
2961
3051
  ```
2962
3052
 
2963
3053
  | 选项 | 默认 | 说明 |
@@ -3023,7 +3113,7 @@ await msg.migrate() // 幂等建表(conversations + members + messa
3023
3113
  app.use(db)
3024
3114
  app.use(msg) // 注入 ctx.msg
3025
3115
  msg.routes(app) // /api/messages/*(会话/历史/发消息/已读)
3026
- app.ws('/ws', ctx.msg.handler()) // 标准 WS 协议内置
3116
+ app.ws('/ws', msg.handler()) // 标准 WS 协议内置
3027
3117
 
3028
3118
  // 业务代码:持久化 + 鉴权 + 广播 + 未读 + 历史,一次调用
3029
3119
  const conv = await ctx.msg.createConversation(ctx.user.id, { type: 'group', memberIds: ['u2'] })
@@ -3034,7 +3124,8 @@ ctx.msg.sendTo('u2', { type: 'mention' }) // 用户维度点对点
3034
3124
 
3035
3125
  - **数据模型**:`_weifuwu_conversations` / `_weifuwu_conversation_members` / `_weifuwu_messages`(`sender_type + sender_id` 不 FK users——user/agent/system 消息天然可存);direct 会话同对用户唯一、历史游标分页、未读数(`last_read_at`)、编辑/删除软删
3036
3126
  - **实时协议内置**:`handler()` 提供 `connected / subscribe→subscribed / unsubscribe / ping→pong`——前端 `ctx.ws.send({ type: 'subscribe', room })` 直接可用,两端协议由框架定义
3037
- - **跨进程**:`redis` 选项 → Redis pub/sub 广播(psubscribe 模式),多实例部署天然一致;无 redis 优雅降级单进程
3127
+ - **跨进程**:`redis` 选项 → Redis pub/sub 广播(psubscribe 模式),多实例部署天然一致;无 redis 优雅降级单进程。
3128
+ **环回去重**:`broadcast` = 本地直发 + Redis publish,本实例的 subscriber 会收到自己 publish 的消息——publish 携带实例唯一标识 `_pid`(`wf:{pid}:{seq}`),订阅回调跳过自己的环回,保证每个事件恰好投递一次(防 token 级事件重复/乱序)
3038
3129
  - **与 userSystem 咬合**:`sendTo(ctx.user.id)` 按身份路由、`createConversation(ctx.user.id)` 创建者即身份、成员校验自动对齐——身份是消息的路由,消息是身份的交互
3039
3130
  - **裁剪**:已读回执状态机(只做未读数)、附件存储、全文搜索、消息确认/重试(可靠投递用 queue)、移动端推送
3040
3131
 
@@ -3153,7 +3244,7 @@ return () => <AiChat chat={$} />
3153
3244
 
3154
3245
  ```ts
3155
3246
  app.use(redis())
3156
- app.use(rateLimit({ key: (req) => `login:${req.ip}`, max: 5, windowMs: 60_000 })) // 防爆破
3247
+ app.use(rateLimit({ key: (req) => `login:${req.headers.get('x-forwarded-for')}`, max: 5, windowMs: 60_000 })) // 防爆破
3157
3248
  app.use(email({ from: 'no-reply@x.com', adapter: 'resend', resend: { apiKey } }))
3158
3249
  app.use(db)
3159
3250
  app.use(users)
@@ -11,5 +11,17 @@ export declare function patchValue(parent: Node, oldNode: Node | null, oldInput:
11
11
  export declare function patchProps(el: Element, oldProps: any, newProps: any): void;
12
12
  /** 为无 key 的子节点自动分配位置 key,确保 keyed diff 正确性 */
13
13
  export declare function ensureKeys(oldChildren: any[], newChildren: any[]): void;
14
+ /**
15
+ * 将 children 映射到它们实际产生的 DOM 节点(每个 child 一个 Node[])。
16
+ *
17
+ * 1 VNode ≠ 1 DOM 节点:Fragment 展开成多个直属节点(_childNodes),
18
+ * null/boolean/Portal 产生 0 个。explicitNodes 提供限定范围(Fragment 子项
19
+ * 用其自身 _childNodes),否则从 source(parent.childNodes 或给定快照)按计数游标分配。
20
+ */
21
+ export declare function mapChildDomNodes(source: Node[], children: any[]): (Node[] | null)[];
14
22
  export declare function normalize(children: any): any[];
15
- export declare function patchKeyedChildren(parent: Node, oldChildren: any[], newChildren: any[], ctx: WfuiContext): void;
23
+ /**
24
+ * keyed 子节点 diff。返回每个 new child 的 DOM 节点范围(newNodes,可为空)。
25
+ * oldNodes:old children → 实际 DOM 节点(mapChildDomNodes 结果)。
26
+ */
27
+ export declare function patchKeyedChildren(parent: Node, oldChildren: any[], newChildren: any[], ctx: WfuiContext, oldNodes?: (Node[] | null)[], rangeStart?: Node | null): (Node[] | null)[];