weifuwu 0.57.0 → 0.59.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.
package/README.md CHANGED
@@ -211,8 +211,8 @@ createApp().use(router({ routes })).mount('#root', RouteView, { hydrate: true })
211
211
  | 资源 | CDN 地址 | 说明 |
212
212
  |------|---------|------|
213
213
  | `weifuwu/client` | `https://unpkg.com/weifuwu@latest/dist/client/index.js` | 客户端核心(createApp, h, 路由, 状态管理等) |
214
- | `weifuwu/components` | `https://unpkg.com/weifuwu@latest/dist/components/index.js` | 43 个 UI 组件(Button, Card, Table, Modal 等) |
215
- | 组件样式 | `https://unpkg.com/weifuwu@latest/dist/components/style.css` | 组件 CSS + 91 个主题 Token + 35 个布局原语 |
214
+ | `weifuwu/components` | `https://unpkg.com/weifuwu@latest/dist/components/index.js` | 44 个 UI 组件(Button, Card, Table, Modal 等) |
215
+ | 组件样式 | `https://unpkg.com/weifuwu@latest/dist/components/style.css` | 组件 CSS + 115 个主题 Token + 67 个布局原语 |
216
216
  | 独立布局系统 | `https://unpkg.com/weifuwu@latest/dist/layout/weifuwu-layout.css` | 仅 CSS 布局,不依赖 JS |
217
217
 
218
218
 
@@ -234,6 +234,7 @@ createApp().use(router({ routes })).mount('#root', RouteView, { hydrate: true })
234
234
  | `weifuwu` | **email** | 邮件发送(Resend/SMTP 自研/自定义适配器)→ `ctx.email` | Router |
235
235
  | `weifuwu` | **userSystem** | 用户系统(scrypt 密码哈希 + 混合会话)→ `ctx.user` / `ctx.auth` + `/api/auth/*` | Router, postgres |
236
236
  | `weifuwu` | **queue** | 可靠任务队列(Redis Streams,at-least-once + DLQ)→ `ctx.queue` | Router, redis |
237
+ | `weifuwu` | **ai** | LLM 对话(自研 OpenAI 兼容协议 + 自研 SSE 解码,默认 DeepSeek)→ `ctx.ai` + `aiStream` | Router |
237
238
  | `weifuwu/dev` | **dev loader** | Node loader:服务端直接跑 `.ts/.tsx`(`--import weifuwu/dev`) | esbuild |
238
239
  | `weifuwu` | **graphql** | GraphQL 端点(支持 GraphiQL) | Router |
239
240
  | `weifuwu` | **createMiddleware** | 类型安全中间件工厂 | — |
@@ -249,8 +250,8 @@ createApp().use(router({ routes })).mount('#root', RouteView, { hydrate: true })
249
250
  | `weifuwu/client` | **ErrorBoundary** | 错误边界组件 | createApp |
250
251
  | `weifuwu/client` | **lockScroll/trapFocus** | 滚动锁定 / 焦点陷阱工具 | — |
251
252
  | `weifuwu/client` | **popup** | 弹层 fixed 定位工具(`computeFixedPos` / `computeFixedPosRect`) | — |
252
- | `weifuwu/components` | **43 个组件** | Button/Table/Modal/Confirm/Toast/... + `confirm()` / `toast()` 命令式中间件 | weifuwu/client |
253
- | `weifuwu/layout` | **CSS 布局** | 35 个布局原语 + 91 个主题 Token(也支持 `weifuwu/layout/style.css`) | — |
253
+ | `weifuwu/components` | **44 个组件** | Button/Table/Modal/Confirm/Toast/... + `confirm()` / `toast()` 命令式中间件 | weifuwu/client |
254
+ | `weifuwu/layout` | **CSS 布局** | 67 个布局原语 + 115 个主题 Token(也支持 `weifuwu/layout/style.css`) | — |
254
255
 
255
256
  ---
256
257
 
@@ -2147,11 +2148,11 @@ import type { RouterOptions } from 'weifuwu/client'
2147
2148
 
2148
2149
  # 组件库 (`weifuwu/components`)
2149
2150
 
2150
- 43 个 HTML 原语组件。每个是 `(_init, ctx) => (props) => VNode`(两阶段组件,与前端框架同一模型),引用 `--wf-*` CSS 变量做主题。另含 `confirm()` / `toast()` 命令式中间件。
2151
+ 44 个 HTML 原语组件。每个是 `(_init, ctx) => (props) => VNode`(两阶段组件,与前端框架同一模型),引用 `--wf-*` CSS 变量做主题。另含 `confirm()` / `toast()` 命令式中间件。
2151
2152
 
2152
2153
  ```ts
2153
2154
  import { Button, Input, Table, Modal, Toast } from 'weifuwu/components'
2154
- import 'weifuwu/components/style.css' // 包含 Token + 35 布局原语 + 组件样式,一次性引入
2155
+ import 'weifuwu/components/style.css' // 包含 Token + 67 布局原语 + 组件样式,一次性引入
2155
2156
  ```
2156
2157
 
2157
2158
  ### 使用示例
@@ -2363,6 +2364,13 @@ props 变化 ──────────────────────
2363
2364
  |-----|--------|-----------|------|
2364
2365
  | Divider | `Divider` | `orientation`, `plain` | 分割线(水平/垂直/带文字) |
2365
2366
 
2367
+ ### AI 交互原语(wf: 协议配套)
2368
+
2369
+ | 组件 | 导入名 | 关键 Props | 说明 |
2370
+ |-----|--------|-----------|------|
2371
+ | ToolCallCard | `ToolCallCard` | `call`, `progress?`, `result?`, `renderArgs?` | 工具调用卡片:running(进度条)/ ok / error 三态(协议 §4) |
2372
+ | ApprovalCard | `ApprovalCard` | `request`, `status?`, `onApprove`, `onReject` | 人工审批卡片:待批(允许/拒绝+备注)/ 已批 / 已拒 / 超时(协议 §4.5) |
2373
+
2366
2374
  ### 全局工具
2367
2375
 
2368
2376
  | 组件 | 导入名 | 关键 Props | 说明 |
@@ -2373,7 +2381,9 @@ props 变化 ──────────────────────
2373
2381
 
2374
2382
  # 布局系统 (`weifuwu/layout`)
2375
2383
 
2376
- 纯 CSS 布局原语 + 91 个主题 Token。不绑定任何 JS 框架。
2384
+ 纯 CSS 布局原语 + 工具类 + 115 个主题 Token。不绑定任何 JS 框架。
2385
+
2386
+ > **学习路径与命名规范**:见 [`docs/style-guide.md`](./docs/style-guide.md)——统一语法 `wf-<域>-<名>`、三档学习(组件 → 10 核心原语 → 完整速查)、场景速查、变量定制。
2377
2387
 
2378
2388
  > **全栈 weifuwu 项目**:`weifuwu/components/style.css` 已包含布局系统,一条 import 就够了,无需单独引用本页。
2379
2389
  > 本页仅适用于**非 weifuwu 项目**或**只需 CSS 布局**的场景。
@@ -2395,7 +2405,7 @@ app.get('/layout.css', (req, ctx) => ctx.ui.css('weifuwu/layout'))
2395
2405
  也支持相对路径:`ctx.ui.css('./src/style.css')`。
2396
2406
  ```
2397
2407
 
2398
- ## 35 个布局原语
2408
+ ## 67 个布局原语
2399
2409
 
2400
2410
  | 类别 | 原语 | 效果 |
2401
2411
  |------|------|------|
@@ -2432,13 +2442,56 @@ app.get('/layout.css', (req, ctx) => ctx.ui.css('weifuwu/layout'))
2432
2442
  | | `wf-inline` | display: inline |
2433
2443
  | | `wf-inline-block` | display: inline-block |
2434
2444
  | | `wf-contents` | display: contents |
2435
-
2436
- ## 91 个主题 Token
2445
+ | **间距** | `wf-p-*` / `wf-px-*` / `wf-py-*`(xs~2xl) | padding:全/水平/垂直,引用 `--wf-space-*` |
2446
+ | | `wf-mt-*` / `wf-mb-*` / `wf-my-*`(xs~2xl) | margin:top/bottom/垂直 |
2447
+ | | `wf-mx-auto` / `wf-my-auto` | margin: auto 居中 |
2448
+ | | `wf-gap-*`(xs~2xl) | 为 flex/grid 原语设置 `--wf-gap` |
2449
+ | **尺寸** | `wf-w-full` / `wf-h-full` / `wf-w-auto` | 宽/高 100%、auto |
2450
+ | **边框** | `wf-border` / `wf-border-t/b/l/r` | 1px 边框(`--wf-border-width` + `--wf-color-border`) |
2451
+ | **面工具** | `wf-bg-secondary/tertiary/brand/success/warning/error/info` | 语义背景色(`--wf-color-*-bg`) |
2452
+ | | `wf-pill` | 胶囊圆角(999px,状态徽章/标签/色块) |
2453
+ | | `wf-rounded-sm` `wf-rounded` `wf-rounded-md` `wf-rounded-lg` | 圆角工具(`--wf-radius-*`) |
2454
+ | **气泡** | `wf-bubble` / `wf-bubble--own` / `wf-bubble--ai` | 聊天气泡(pre-wrap + 折行内建) |
2455
+ | **打印** | `wf-print-hidden` / `wf-print-block` | 导出 PDF 时隐藏工具区 / 恢复块级 |
2456
+ | **行高** | `wf-leading-tight` `wf-leading-base` `wf-leading-relaxed` | line-height(`--wf-line-height-*`) |
2457
+ | **指针** | `wf-pointer` / `wf-not-allowed` | cursor: pointer / not-allowed |
2458
+ | **内容排版** | `wf-prose` | 富文本正文(文章/博客/文档,一个类包 h2/p/ul/blockquote/pre…) |
2459
+ | **外壳** | `wf-app-shell` | 应用外壳:侧边栏 + 主区 grid(`--wf-sidebar-width`) |
2460
+ | | `wf-sidebar` `wf-sidebar-header` `wf-sidebar-body` `wf-sidebar-footer` | 侧边栏:品牌区/导航区/底部用户区,sticky 全高 |
2461
+ | | `wf-nav` `wf-nav-group` `wf-nav-item` `wf-nav-icon` | 导航:分组标题 + 链接项(`--active` 激活态) |
2462
+ | | `wf-main` | 主内容区(padding + min-width: 0) |
2463
+ | | `wf-text-*` 排版工具 | 见下文「排版工具」 |
2464
+
2465
+ ### 排版工具(`wf-text-*`)
2466
+
2467
+ | 工具 | 效果 |
2468
+ |------|------|
2469
+ | `wf-text-left/center/right` | text-align |
2470
+ | `wf-text-xs…5xl` | 字号(`--wf-font-size-*`) |
2471
+ | `wf-text-secondary/tertiary/disabled/brand` | 中性色阶 |
2472
+ | `wf-text-success/warning/error/info` | 语义色文本(`--wf-color-*`) |
2473
+ | `wf-text-medium/semibold/bold` | 字重 |
2474
+ | `wf-tracking-normal/wide/wider` | letter-spacing |
2475
+ | `wf-uppercase/lowercase/capitalize` | text-transform |
2476
+ | `wf-pre-wrap` | white-space: pre-wrap + word-break(聊天气泡/代码) |
2477
+ | `wf-break-word` | overflow-wrap + word-break |
2478
+ | `wf-text-nowrap` | white-space: nowrap |
2479
+ | `wf-truncate` | 单行省略(ellipsis) |
2480
+ | `wf-line-clamp-2/3` | 多行截断 |
2481
+
2482
+ ## 115 个主题 Token
2483
+
2484
+ **双层结构**:原始层(Primitive,色值只定义一次,品牌/暗色调校改这里)+ 语义层(Semantic,组件消费)。
2437
2485
 
2438
2486
  ```css
2487
+ /* ── 原始层 — 品牌/中性色值 + 暗色值,主题定制改这一层 ── */
2488
+ --wf-brand-500 / --wf-brand-600 / --wf-brand-50 /* 品牌主色/悬停/浅底 */
2489
+ --wf-slate-900…50 / --wf-white /* 中性阶 */
2490
+ --wf-dark-* /* 暗色值(暗色模式经间接层引用,零硬编码) */
2491
+
2492
+ /* ── 语义层 — 组件消费,暗色/主题切换覆盖这里 ── */
2439
2493
  /* 品牌色 */
2440
2494
  --wf-color-primary / --wf-color-primary-hover / --wf-color-primary-bg
2441
- --wf-color-secondary / --wf-color-secondary-bg
2442
2495
 
2443
2496
  /* 语义色 */
2444
2497
  --wf-color-success / --wf-color-success-bg
@@ -2488,6 +2541,9 @@ app.get('/layout.css', (req, ctx) => ctx.ui.css('weifuwu/layout'))
2488
2541
  --wf-accent-color / --wf-caret-color
2489
2542
  --wf-opacity-disabled / --wf-opacity-overlay
2490
2543
  --wf-pop-z / --wf-cover-z
2544
+
2545
+ /* 应用外壳 */
2546
+ --wf-sidebar-width
2491
2547
  ```
2492
2548
 
2493
2549
  ### 暗色模式
@@ -2503,10 +2559,64 @@ app.get('/layout.css', (req, ctx) => ctx.ui.css('weifuwu/layout'))
2503
2559
  // 系统为暗色时自动生效;加 data-theme="light" 可强制亮色
2504
2560
  ```
2505
2561
 
2562
+ 暗色值定义在原始层 `--wf-dark-*`(只写一次),`_dark.css` 两段仅做语义映射——改暗色调校只动原始层,无硬编码色值。
2563
+
2506
2564
  ---
2507
2565
 
2508
2566
  # 样式定制指南
2509
2567
 
2568
+ ## 组件定制钩子(零覆盖 CSS)
2569
+
2570
+ 关键组件视觉全部变量化——定制只需设一个变量(默认值 = 现有 token):
2571
+
2572
+ ```html
2573
+ <style>
2574
+ :root {
2575
+ --wf-brand-500: #7c3aed; /* 品牌换色:改原始层一个值,全站跟随 */
2576
+ --wf-dark-brand-500: #a78bfa; /* 暗色品牌(可选) */
2577
+ --wf-modal-width: 640px; /* 组件定制:设一个变量 */
2578
+ --wf-btn-radius: 999px;
2579
+ --wf-field-height: 44px;
2580
+ --wf-card-shadow: 0 8px 24px rgba(0,0,0,.12);
2581
+ }
2582
+ </style>
2583
+ ```
2584
+
2585
+ 钩子清单:`--wf-btn-*` `--wf-card-*` `--wf-field-*` `--wf-modal-*` `--wf-drawer-width` `--wf-toast-*` `--wf-alert-radius` `--wf-badge-radius` `--wf-tag-radius` `--wf-switch-radius` `--wf-popover-*` `--wf-tooltip-radius` `--wf-dropdown-min-width` `--wf-datepicker-*`。
2586
+
2587
+ **覆盖优先级(@layer)**:`@layer tokens, base, layout, utilities, components`——你写的未分层 CSS 天然最高优先级;也可用 `@layer utilities` 精准覆盖我们。
2588
+
2589
+ ## 零自定义 CSS 模式(推荐)
2590
+
2591
+ 一个项目只需要引用**一个 CSS 文件**(`weifuwu/components/style.css`,内含 Token + 布局原语 + 组件样式),
2592
+ 业务代码全部由组件 + `wf-*` 原语承担,**不需要再写 `style.css`**:
2593
+
2594
+ ```tsx
2595
+ // 组件 → 页面功能块;wf-* 原语 → 块之间的空间关系;--wf-* → 业务色值
2596
+ <PageHeader title="仪表盘" sub="欢迎回来">
2597
+ <Button variant="primary">+ 新建</Button>
2598
+ </PageHeader>
2599
+ <div class="wf-row wf-gap-lg">
2600
+ <StatCard label="总用户" value="1,234" trend="up" trendLabel="12%" />
2601
+ </div>
2602
+ ```
2603
+
2604
+ 主题定制(品牌色/圆角/字体)不需要独立文件——**内联在 HTML 的 `<style>` 里即可**:
2605
+
2606
+ ```html
2607
+ <style>
2608
+ :root {
2609
+ --wf-color-primary: #6366f1;
2610
+ --wf-radius: 8px;
2611
+ }
2612
+ </style>
2613
+ ```
2614
+
2615
+ 完整的零样式示例:`apps/weifuwu-demo`(一个页面 = 组件 + 原语,无 style.css)。
2616
+
2617
+ 诚实例外(合理场景,仍可内联 `<style>` 解决):打印/PDF 导出规则、第三方库宿主样式、
2618
+ 业务特有的一次性视觉(如色板选择器交互)。
2619
+
2510
2620
  ## 全局主题变量
2511
2621
 
2512
2622
  所有组件引用 `--wf-*` CSS 变量。在根元素覆盖即可定制主题:
@@ -2826,6 +2936,74 @@ await worker.stop() // 优雅停止
2826
2936
  - **可靠性**:失败 → 延迟重试(间隔 = `visibilityTimeout`,ZSET 延迟队列)→ attempts 用尽 → DLQ;worker 崩溃 → pending 由其他实例 `XAUTOCLAIM` 接管
2827
2937
  - 裁剪:延迟调度(除重试外)、cron、优先级、指数退避、速率限制不支持
2828
2938
 
2939
+ ## ai — LLM 对话(自研协议 + 零依赖客户端)
2940
+
2941
+ ```ts
2942
+ import { ai } from 'weifuwu'
2943
+
2944
+ const a = ai() // DEEPSEEK_API_KEY / BASE_URL / MODEL 自动读 env,默认 deepseek-v4-flash
2945
+ app.use(a) // 注入 ctx.ai(worker/非请求场景也可直接 a.chat())
2946
+
2947
+ // 流式对话:路由一行返回 SSE(wf: 协议,详见 docs/ai-contract.md)
2948
+ app.post('/api/chat', async (req, ctx) => {
2949
+ const { messages } = await req.json()
2950
+ return ctx.ai.stream({ messages }, {
2951
+ signal: req.signal, // 断开即取消 provider 请求
2952
+ traceId: req.headers.get('x-trace-id') ?? undefined, // 追踪关联(协议 §7)
2953
+ })
2954
+ })
2955
+
2956
+ // 非流式(worker/后台):
2957
+ const res = await a.chat({ messages: [{ role: 'user', content: 'hi' }] })
2958
+
2959
+ // agent 引擎:工具循环 + 人工审批(HITL)
2960
+ const agent = a.agent({
2961
+ systemPrompt: '你是助手。查询天气时调用 query_weather 工具。',
2962
+ tools: [{
2963
+ name: 'query_weather',
2964
+ description: '查询城市天气',
2965
+ parameters: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
2966
+ run: async (args, { emit }) => {
2967
+ emit('wf:tool_progress', { toolCallId: 'x', step: 1, total: 2, message: '查询中…', status: 'running' })
2968
+ return { city: args.city, temp: 25 }
2969
+ },
2970
+ }],
2971
+ humanInTheLoop: true, // 每个工具执行前等人工审批
2972
+ })
2973
+
2974
+ app.post('/api/agent', async (req, ctx) => {
2975
+ const { messages } = await req.json()
2976
+ return agent.run(messages, { signal: req.signal, traceId: req.headers.get('x-trace-id') ?? undefined })
2977
+ })
2978
+
2979
+ // HITL 审批响应(前端点"允许/拒绝" → POST 到这里)
2980
+ app.post('/api/approve', async (req, ctx) => {
2981
+ ctx.ai.approve(await req.json()) // { id, decision, modifiedArgs?, note? }
2982
+ return new Response(null, { status: 200 })
2983
+ })
2984
+ ```
2985
+
2986
+ 前端解码(`weifuwu/client`):
2987
+
2988
+ ```ts
2989
+ import { aiStream } from 'weifuwu/client'
2990
+
2991
+ const handle = aiStream('/api/chat', { messages }, {
2992
+ onToken: (text) => { /* 增量 append 到消息 */ },
2993
+ onToolCall: (call) => { /* 渲染工具卡片 */ },
2994
+ onDone: () => { /* 收尾 */ },
2995
+ onError: (e) => { /* 按 e.code 降级 */ },
2996
+ onEvent: (name, data) => { /* x:* 自定义事件透传 */ },
2997
+ })
2998
+ handle.abort() // 用户停止/组件卸载/导航跳走
2999
+ ```
3000
+
3001
+ - **协议**:`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))
3002
+ - **agent 引擎**:`a.agent({ systemPrompt, tools, humanInTheLoop })` 工具循环(LLM → tool_call → 执行 → 回喂 → 重复);工具可 `emit` 进度/自定义事件、接收 `signal` 取消;HITL 审批(`ctx.ai.approve` 响应,拒绝≠终止、modified 改参、超时兜底)
3003
+ - **零依赖**:自研 OpenAI 兼容客户端(fetch + SSE 解析),默认 DeepSeek,`baseUrl` 可换任意 OpenAI 兼容端点(Ollama/vLLM/Moonshot…)
3004
+ - **追踪**:前端自动生成 `X-Trace-Id` → 后端以之作为 `message_start.id` → 工具内请求继承同一 traceId,整个 agent run 一次搜完
3005
+ - **裁剪**:embeddings、Anthropic 原生协议、审批持久化(连接断=会话亡)暂不支持;多 agent 编排不承诺(子 agent = 工具已支持)
3006
+
2829
3007
  ## 组合示例:注册 → 验证邮件 → 欢迎任务 → 登录防爆破
2830
3008
 
2831
3009
  ```ts
@@ -0,0 +1,50 @@
1
+ /**
2
+ * weifuwu AI — agent 工具循环引擎(协议 §5,agent 扩展实现)
3
+ *
4
+ * 循环:LLM 流式(emit wf:token)→ tool_calls → 执行工具 → 结果回喂 → 重复
5
+ *
6
+ * - 工具执行期间可 emit(wf:tool_progress / x:* 自定义)与接收 signal(取消)
7
+ * - HITL 审批(协议 §4.5):humanInTheLoop 时每个工具执行前挂起等待
8
+ * ctx.ai.approve() 响应(或超时按拒绝处理)——拒绝 ≠ 终止,agent 换方案
9
+ * - 事件序列:message_start → (step:llm → token* → tool_call → step:tool
10
+ * → [approval_request → approve] → tool_result)* → usage → done
11
+ *
12
+ * 子 agent = 工具:委派工具的 run 内部调另一个 createAgent().run()(异步),
13
+ * 其最终输出即 tool_result——多 agent 沟通不新增协议事件(协议 §5.2)。
14
+ */
15
+ import { type WfEmitter } from './sse.ts';
16
+ import type { AiClient } from './client.ts';
17
+ import type { ChatMessage } from './types.ts';
18
+ export interface ToolContext {
19
+ /** 工具执行声道:emit('wf:tool_progress', ...) 或 emit('x:*', ...) */
20
+ emit: WfEmitter;
21
+ /** 用户取消 → abort(长任务应响应此 signal) */
22
+ signal?: AbortSignal;
23
+ }
24
+ export interface AgentTool {
25
+ name: string;
26
+ description?: string;
27
+ parameters?: Record<string, unknown>;
28
+ /** args 来自 LLM(JSON),未类型化——工具内部自行收窄 */
29
+ run: (args: Record<string, unknown>, tool: ToolContext) => unknown;
30
+ }
31
+ export interface AgentConfig {
32
+ model?: string;
33
+ systemPrompt: string;
34
+ tools: AgentTool[];
35
+ /** 默认 10 */
36
+ maxSteps?: number;
37
+ /** 每个工具执行前要求人工审批(协议 §4.5) */
38
+ humanInTheLoop?: boolean;
39
+ /** 审批超时(默认 5 分钟),到期按拒绝处理 */
40
+ approvalTimeoutMs?: number;
41
+ }
42
+ export interface AgentRunOptions {
43
+ signal?: AbortSignal;
44
+ traceId?: string;
45
+ }
46
+ export interface AgentRunner {
47
+ /** 运行 agent → SSE Response(wf: 协议事件流),路由直接 return */
48
+ run: (messages: ChatMessage[], options?: AgentRunOptions) => Response;
49
+ }
50
+ export declare function createAgent(client: AiClient, config: AgentConfig): AgentRunner;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * weifuwu AI — OpenAI 兼容客户端(自研,零依赖)
3
+ *
4
+ * 协议(docs/ai-contract.md)的后端参考实现:把 provider 的
5
+ * chat/completions 流归一化成 wf: 事件。
6
+ *
7
+ * - 零依赖:fetch + 自研 SSE 解析
8
+ * - 默认 DeepSeek(baseUrl 可换 → 任意 OpenAI 兼容端点:Ollama/vLLM/Moonshot…)
9
+ * - 错误映射:provider HTTP 状态/错误体 → WfErrorCode(错误即值)
10
+ * - tool_calls 聚合:id 只在首 chunk(DeepSeek),后端聚合成完整 wf:tool_call
11
+ * - abort:外部 signal + 客户端断开 → 取消 provider 请求
12
+ *
13
+ * 诚实裁剪(CS-05):embeddings 不做(DeepSeek 无此 API)、
14
+ * reasoning 事件不进 v1 协议(reasoning_content 仅随消息往返)。
15
+ */
16
+ import { type WfEmitter } from './sse.ts';
17
+ import type { ChatMessage, ChatParams, ToolCall, WfApprovalResponse, WfErrorCode } from './types.ts';
18
+ export interface ChatChunk {
19
+ id: string;
20
+ model: string;
21
+ choices: {
22
+ index: number;
23
+ delta: {
24
+ role?: string;
25
+ content?: string;
26
+ reasoning_content?: string;
27
+ tool_calls?: ToolCall[];
28
+ };
29
+ finish_reason: 'stop' | 'length' | 'tool_calls' | null;
30
+ }[];
31
+ usage?: {
32
+ prompt_tokens: number;
33
+ completion_tokens: number;
34
+ total_tokens: number;
35
+ };
36
+ }
37
+ export interface ChatResponse {
38
+ id: string;
39
+ model: string;
40
+ choices: {
41
+ index: number;
42
+ message: ChatMessage;
43
+ finish_reason: 'stop' | 'length' | 'tool_calls' | null;
44
+ }[];
45
+ usage?: {
46
+ prompt_tokens: number;
47
+ completion_tokens: number;
48
+ total_tokens: number;
49
+ };
50
+ }
51
+ /** 协议错误:chat() 非流式场景抛出,stream() 场景编码为 wf:error 事件 */
52
+ export declare class AiError extends Error {
53
+ code: WfErrorCode;
54
+ constructor(code: WfErrorCode, message: string);
55
+ }
56
+ export interface AiClientOptions {
57
+ baseUrl: string;
58
+ apiKey: string;
59
+ defaultModel: string;
60
+ }
61
+ /** 单轮 LLM 流式调用的聚合结果(agent 循环用) */
62
+ export interface StreamFinishResult {
63
+ content: string;
64
+ /** DeepSeek thinking 模式:必须随 assistant 消息回传(协议陷阱清单 #4) */
65
+ reasoning_content?: string;
66
+ toolCalls: ToolCall[];
67
+ usage?: ChatResponse['usage'];
68
+ }
69
+ export interface AiClient {
70
+ /** 非流式对话(worker/后台场景) */
71
+ chat(params: ChatParams, options?: {
72
+ signal?: AbortSignal;
73
+ }): Promise<ChatResponse>;
74
+ /** 流式对话 → SSE Response(协议 §1.1),路由直接 return */
75
+ stream(params: ChatParams, options?: {
76
+ signal?: AbortSignal;
77
+ traceId?: string;
78
+ }): Response;
79
+ /** 低层:app 完全控制事件序列(自定义 x:* 事件、HITL 等) */
80
+ sse(run: (emit: WfEmitter) => Promise<void> | void, options?: {
81
+ signal?: AbortSignal;
82
+ }): Response;
83
+ /** 内部:单轮 LLM 流式 → emit 事件 + onFinish 聚合结果(agent 引擎用) */
84
+ streamStep(params: ChatParams, opts: {
85
+ emit: WfEmitter;
86
+ signal?: AbortSignal;
87
+ onFinish?: (r: StreamFinishResult) => void;
88
+ emitUsage?: boolean;
89
+ }): Promise<void>;
90
+ /** 响应一个挂起的 HITL 审批(协议 §4.5,app 的 POST /approve 路由调用) */
91
+ approve(response: WfApprovalResponse): boolean;
92
+ /** 内部:agent 循环挂起等待审批 */
93
+ waitApproval(req: {
94
+ id: string;
95
+ toolCallId: string;
96
+ name: string;
97
+ args: Record<string, unknown>;
98
+ }, emit: WfEmitter, timeoutMs?: number): Promise<WfApprovalResponse>;
99
+ }
100
+ export declare function createAiClient(opts: AiClientOptions): AiClient;
101
+ /** 审批默认超时:5 分钟 */
102
+ export declare const DEFAULT_APPROVAL_TIMEOUT: number;
103
+ /** 工具参数可能是 JSON 字符串;解析失败给空对象(不抛错) */
104
+ export declare function safeParseArgs(raw: string): Record<string, unknown>;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * weifuwu AI — 中间件工厂(queue 式混合:模块即中间件,也独立可用)
3
+ *
4
+ * ```ts
5
+ * import { ai } from 'weifuwu'
6
+ *
7
+ * const a = ai() // DEEPSEEK_API_KEY / BASE_URL / MODEL 自动读 env
8
+ * app.use(a) // → ctx.ai.chat / ctx.ai.stream / ctx.ai.sse
9
+ *
10
+ * app.post('/api/chat', async (req, ctx) => {
11
+ * const { messages } = await req.json()
12
+ * return ctx.ai.stream({ messages }, {
13
+ * signal: req.signal,
14
+ * traceId: req.headers.get('x-trace-id') ?? undefined, // 追踪关联(协议 §7)
15
+ * })
16
+ * })
17
+ *
18
+ * // worker / 非请求场景:同一个实例直接调用
19
+ * q.worker('llm.batch', async (job) => {
20
+ * await a.chat({ messages: job.data.messages })
21
+ * })
22
+ * ```
23
+ *
24
+ * 配置优先级:显式参数 > env > 默认值。
25
+ * apiKey: DEEPSEEK_API_KEY
26
+ * baseUrl: DEEPSEEK_BASE_URL → 'https://api.deepseek.com/v1'
27
+ * defaultModel: DEEPSEEK_MODEL → 'deepseek-v4-flash'
28
+ */
29
+ import type { Context, Middleware } from '../types.ts';
30
+ import { type AiClient, type AiClientOptions } from './client.ts';
31
+ import { type AgentConfig, type AgentRunner } from './agent.ts';
32
+ export interface AiOptions extends Partial<AiClientOptions> {
33
+ }
34
+ export interface AiInjected {
35
+ ai: AiClient;
36
+ }
37
+ /** 模块 = 中间件 + 客户端(queue 式混合:app.use(a) + worker 直接 a.chat()) */
38
+ export interface AiClientModule extends Middleware<Context, Context & AiInjected>, AiClient {
39
+ /** agent 引擎(工具循环 + HITL 审批) */
40
+ agent: (config: AgentConfig) => AgentRunner;
41
+ close: () => Promise<void>;
42
+ }
43
+ declare module '../types.ts' {
44
+ interface Context {
45
+ /** 注入模块本身(含 agent / approve),worker 场景直接 a.chat() */
46
+ ai?: AiClientModule;
47
+ }
48
+ }
49
+ export declare function ai(options?: AiOptions): AiClientModule;
50
+ export type { WfStreamEvent, WfMessageStart, WfToken, WfUsage, WfDone, WfError, WfErrorCode, WfToolCall, WfToolResult, WfToolProgress, WfStep, WfApprovalRequest, WfApprovalResponse, WfApprovalDecision, ChatMessage, ChatParams, MessageRole, ToolCall, ToolDefinition, } from './types.ts';
51
+ export type { WfEmitter } from './sse.ts';
52
+ export { AiError } from './client.ts';
53
+ export type { AiClient, ChatResponse } from './client.ts';
54
+ export type { AgentConfig, AgentTool, AgentRunner, ToolContext } from './agent.ts';
@@ -0,0 +1,26 @@
1
+ /**
2
+ * weifuwu AI — SSE 编码器
3
+ *
4
+ * 把 `wf:` 事件流编码成 text/event-stream Response(协议 §1.1)。
5
+ * 与 docs/ai-contract.md 对应。
6
+ *
7
+ * - 错误即值:run 内部抛错 → 编码为 wf:error 事件,而非断流
8
+ * - abort:客户端断开(cancel)→ onAbort 回调(用于取消 provider 请求)
9
+ */
10
+ export type WfEmitter = (name: string, data: unknown) => void;
11
+ export interface SseResponseOptions {
12
+ /** 客户端断开回调(取消上游请求) */
13
+ onAbort?: () => void;
14
+ }
15
+ /**
16
+ * 构造 SSE Response。run 收到 emit,负责输出完整事件序列。
17
+ *
18
+ * ```ts
19
+ * return sseResponse(async (emit) => {
20
+ * emit('wf:message_start', { id })
21
+ * emit('wf:token', { text: '你好' })
22
+ * emit('wf:done', { content })
23
+ * })
24
+ * ```
25
+ */
26
+ export declare function sseResponse(run: (emit: WfEmitter) => Promise<void> | void, options?: SseResponseOptions): Response;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * weifuwu AI 协议共享类型 —— 与 docs/ai-contract.md 规范一一对应
3
+ *
4
+ * 纯类型,零运行时成本。两端同源:
5
+ * - 后端:从 weifuwu 主包导入(src/index.ts re-export)
6
+ * - 前端:从 weifuwu/client 导入(src/client/index.ts re-export)
7
+ *
8
+ * 修改本文件 = 修改协议,需同步更新 docs/ai-contract.md。
9
+ */
10
+ export interface WfMessageStart {
11
+ id: string;
12
+ }
13
+ export interface WfToken {
14
+ /** 增量文本,前端直接 append */
15
+ text: string;
16
+ }
17
+ export interface WfUsage {
18
+ prompt_tokens: number;
19
+ completion_tokens: number;
20
+ total_tokens?: number;
21
+ }
22
+ export interface WfDone {
23
+ content: string;
24
+ usage?: WfUsage;
25
+ }
26
+ export type WfErrorCode = 'auth_failed' | 'rate_limited' | 'context_length' | 'timeout' | 'provider_error' | 'invalid_request' | 'unsupported' | 'aborted';
27
+ export interface WfError {
28
+ code: WfErrorCode;
29
+ message: string;
30
+ }
31
+ export interface WfToolCall {
32
+ /** 工具调用 id(provider 给 / 后端生成),聚合完成后才发 */
33
+ id: string;
34
+ /** 工具名(app 定义的业务语义,协议不解释) */
35
+ name: string;
36
+ /** 完整参数 */
37
+ args: Record<string, unknown>;
38
+ }
39
+ export interface WfToolResult {
40
+ id: string;
41
+ ok: boolean;
42
+ output?: unknown;
43
+ /** ok:false 时:rejected(人工拒绝)/ timeout(审批超时)/ tool_error / app 自定义 */
44
+ error?: {
45
+ code: string;
46
+ message: string;
47
+ };
48
+ }
49
+ export interface WfToolProgress {
50
+ toolCallId: string;
51
+ step: number;
52
+ total: number;
53
+ message?: string;
54
+ status: 'running' | 'error' | 'done';
55
+ }
56
+ export interface WfStep {
57
+ type: 'llm' | 'tool';
58
+ content?: string;
59
+ toolCallId?: string;
60
+ name?: string;
61
+ }
62
+ export interface WfApprovalRequest {
63
+ id: string;
64
+ toolCallId: string;
65
+ name: string;
66
+ args: Record<string, unknown>;
67
+ reason?: string;
68
+ /** 审批超时;到期按 rejected 处理(error.code: 'timeout') */
69
+ expiresAt?: number;
70
+ }
71
+ export type WfApprovalDecision = 'approved' | 'rejected' | 'modified';
72
+ /** 上行 POST 载荷(非 SSE 事件) */
73
+ export interface WfApprovalResponse {
74
+ id: string;
75
+ decision: WfApprovalDecision;
76
+ /** 仅 modified:按修改后的参数执行 */
77
+ modifiedArgs?: Record<string, unknown>;
78
+ /** 进 agent 上下文 */
79
+ note?: string;
80
+ }
81
+ /** 所有框架事件联合类型(前端 switch 收窄用) */
82
+ export type WfStreamEvent = {
83
+ name: 'wf:message_start';
84
+ data: WfMessageStart;
85
+ } | {
86
+ name: 'wf:token';
87
+ data: WfToken;
88
+ } | {
89
+ name: 'wf:usage';
90
+ data: WfUsage;
91
+ } | {
92
+ name: 'wf:done';
93
+ data: WfDone;
94
+ } | {
95
+ name: 'wf:error';
96
+ data: WfError;
97
+ } | {
98
+ name: 'wf:tool_call';
99
+ data: WfToolCall;
100
+ } | {
101
+ name: 'wf:tool_result';
102
+ data: WfToolResult;
103
+ } | {
104
+ name: 'wf:tool_progress';
105
+ data: WfToolProgress;
106
+ } | {
107
+ name: 'wf:step';
108
+ data: WfStep;
109
+ } | {
110
+ name: 'wf:approval_request';
111
+ data: WfApprovalRequest;
112
+ };
113
+ export type MessageRole = 'system' | 'user' | 'assistant' | 'tool';
114
+ export interface ChatMessage {
115
+ role: MessageRole;
116
+ content: string;
117
+ /** DeepSeek thinking mode:前一轮的 reasoning_content 必须回传 */
118
+ reasoning_content?: string;
119
+ tool_call_id?: string;
120
+ tool_calls?: ToolCall[];
121
+ name?: string;
122
+ }
123
+ export interface ToolCall {
124
+ id: string;
125
+ type: 'function';
126
+ function: {
127
+ name: string;
128
+ arguments: string;
129
+ };
130
+ }
131
+ export interface ToolDefinition {
132
+ type: 'function';
133
+ function: {
134
+ name: string;
135
+ description: string;
136
+ parameters: Record<string, unknown>;
137
+ };
138
+ }
139
+ export interface ChatParams {
140
+ model?: string;
141
+ messages: ChatMessage[];
142
+ temperature?: number;
143
+ max_tokens?: number;
144
+ stream?: boolean;
145
+ tools?: ToolDefinition[];
146
+ tool_choice?: 'auto' | 'none' | 'required';
147
+ stop?: string[];
148
+ }