weifuwu 0.56.1 → 0.58.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 +203 -0
- package/dist/ai/agent.d.ts +50 -0
- package/dist/ai/client.d.ts +104 -0
- package/dist/ai/index.d.ts +54 -0
- package/dist/ai/sse.d.ts +26 -0
- package/dist/ai/types.d.ts +148 -0
- package/dist/client/ai.d.ts +59 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.js +128 -0
- package/dist/components/ApprovalCard/ApprovalCard.d.ts +30 -0
- package/dist/components/ToolCallCard/ToolCallCard.d.ts +23 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.js +86 -0
- package/dist/components/style.css +138 -0
- package/dist/core/router.d.ts +1 -1
- package/dist/email/index.d.ts +72 -0
- package/dist/email/smtp.d.ts +34 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +3109 -1967
- package/dist/middleware/rate-limit.d.ts +70 -0
- package/dist/queue/index.d.ts +90 -0
- package/dist/types.d.ts +2 -2
- package/dist/user/index.d.ts +97 -0
- package/dist/user/password.d.ts +8 -0
- package/dist/user/token.d.ts +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -230,6 +230,11 @@ createApp().use(router({ routes })).mount('#root', RouteView, { hydrate: true })
|
|
|
230
230
|
| `weifuwu` | **redis** | Redis 客户端(自研 RESP2 协议)→ `ctx.redis` | Router, REDIS_URL |
|
|
231
231
|
| `weifuwu` | **ui** | SSR 渲染 + esbuild JS/CSS 动态编译 → `ctx.ui` | Router |
|
|
232
232
|
| `weifuwu` | **uiSsr** | 路由级 SSR:匹配 routes → 自动完整 HTML + `__DATA__` + bundle | Router, ui |
|
|
233
|
+
| `weifuwu` | **rateLimit** | 限流中间件(fixed/sliding,redis 多实例原子)→ `ctx.limit` | Router, redis |
|
|
234
|
+
| `weifuwu` | **email** | 邮件发送(Resend/SMTP 自研/自定义适配器)→ `ctx.email` | Router |
|
|
235
|
+
| `weifuwu` | **userSystem** | 用户系统(scrypt 密码哈希 + 混合会话)→ `ctx.user` / `ctx.auth` + `/api/auth/*` | Router, postgres |
|
|
236
|
+
| `weifuwu` | **queue** | 可靠任务队列(Redis Streams,at-least-once + DLQ)→ `ctx.queue` | Router, redis |
|
|
237
|
+
| `weifuwu` | **ai** | LLM 对话(自研 OpenAI 兼容协议 + 自研 SSE 解码,默认 DeepSeek)→ `ctx.ai` + `aiStream` | Router |
|
|
233
238
|
| `weifuwu/dev` | **dev loader** | Node loader:服务端直接跑 `.ts/.tsx`(`--import weifuwu/dev`) | esbuild |
|
|
234
239
|
| `weifuwu` | **graphql** | GraphQL 端点(支持 GraphiQL) | Router |
|
|
235
240
|
| `weifuwu` | **createMiddleware** | 类型安全中间件工厂 | — |
|
|
@@ -1019,6 +1024,8 @@ app.get('/secure', () => {
|
|
|
1019
1024
|
|
|
1020
1025
|
## 响应辅助函数
|
|
1021
1026
|
|
|
1027
|
+
> 以下为完整 API 参考,按需查阅。四个 SaaS 地基模块(rateLimit / email / userSystem / queue)见文末「SaaS 地基模块」章节。
|
|
1028
|
+
|
|
1022
1029
|
消除 `Response.json(...)` 重复模式:
|
|
1023
1030
|
|
|
1024
1031
|
```ts
|
|
@@ -2357,6 +2364,13 @@ props 变化 ──────────────────────
|
|
|
2357
2364
|
|-----|--------|-----------|------|
|
|
2358
2365
|
| Divider | `Divider` | `orientation`, `plain` | 分割线(水平/垂直/带文字) |
|
|
2359
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
|
+
|
|
2360
2374
|
### 全局工具
|
|
2361
2375
|
|
|
2362
2376
|
| 组件 | 导入名 | 关键 Props | 说明 |
|
|
@@ -2722,3 +2736,192 @@ node scripts/release.mjs <version> # 发布
|
|
|
2722
2736
|
# 测试前启动依赖服务
|
|
2723
2737
|
docker compose up -d
|
|
2724
2738
|
```
|
|
2739
|
+
|
|
2740
|
+
---
|
|
2741
|
+
|
|
2742
|
+
# SaaS 地基模块(rateLimit / email / userSystem / queue)
|
|
2743
|
+
|
|
2744
|
+
四个内建模块组成一个"基本 SaaS 底座":认证、异步任务、限流、邮件——零新增依赖
|
|
2745
|
+
(只依赖已自研的 redis / postgres 客户端与 node 标准库)。
|
|
2746
|
+
|
|
2747
|
+
## rateLimit — 限流
|
|
2748
|
+
|
|
2749
|
+
```ts
|
|
2750
|
+
import { rateLimit } from 'weifuwu'
|
|
2751
|
+
|
|
2752
|
+
app.use(redis()) // 依赖 ctx.redis
|
|
2753
|
+
app.use(rateLimit({ windowMs: 60_000, max: 100 })) // 全局限流(默认固定窗口)
|
|
2754
|
+
|
|
2755
|
+
app.get('/api/search', async (req, ctx) => {
|
|
2756
|
+
await ctx.limit('search', { max: 30, windowMs: 60_000 }) // 手动限流,超限抛 429
|
|
2757
|
+
})
|
|
2758
|
+
|
|
2759
|
+
// 登录防爆破(配合 userSystem):组合键 ip:email
|
|
2760
|
+
app.use(rateLimit({ key: (req) => `login:${req.ip}:${req.email}`, max: 5, windowMs: 15 * 60_000 }))
|
|
2761
|
+
```
|
|
2762
|
+
|
|
2763
|
+
| 选项 | 默认 | 说明 |
|
|
2764
|
+
|------|------|------|
|
|
2765
|
+
| `windowMs` | `60000` | 时间窗口 |
|
|
2766
|
+
| `max` | `100` | 窗口内最大请求 |
|
|
2767
|
+
| `key` | X-Forwarded-For | 限流键(生产环境配置反向代理注入) |
|
|
2768
|
+
| `algorithm` | `fixed` | `fixed`(INCR+EXPIRE,原子)\| `sliding`(ZSET,仅 redis) |
|
|
2769
|
+
| `store` | `redis` | `redis`(多实例一致)\| `memory`(仅单实例/开发) |
|
|
2770
|
+
|
|
2771
|
+
- 响应自动带 `RateLimit-Limit` / `RateLimit-Remaining` / `RateLimit-Reset` / `Retry-After`
|
|
2772
|
+
- 多实例共享计数:计数在 redis,水平扩展天然一致
|
|
2773
|
+
|
|
2774
|
+
## email — 邮件发送
|
|
2775
|
+
|
|
2776
|
+
```ts
|
|
2777
|
+
import { email } from 'weifuwu'
|
|
2778
|
+
|
|
2779
|
+
app.use(email({ from: 'no-reply@your.app', adapter: 'resend', resend: { apiKey: process.env.RESEND_API_KEY } }))
|
|
2780
|
+
// 或 adapter: 'smtp' + smtp: { host, port, user, pass }(自研 SMTP 客户端,零依赖)
|
|
2781
|
+
|
|
2782
|
+
app.post('/api/notify', async (req, ctx) => {
|
|
2783
|
+
await ctx.email.send({ to: 'user@x.com', subject: '通知', html: '<h1>hi</h1>' })
|
|
2784
|
+
})
|
|
2785
|
+
```
|
|
2786
|
+
|
|
2787
|
+
- 适配器:`resend`(默认,一个 POST)/ `smtp`(自研 node:net + node:tls:EHLO/STARTTLS/AUTH PLAIN/DATA/dot-stuffing,非 ASCII subject 自动 RFC2047 编码)/ 自定义函数
|
|
2788
|
+
- 裁剪:附件、退信/送达率(服务商职责)、批量营销不支持
|
|
2789
|
+
|
|
2790
|
+
## userSystem — 用户系统
|
|
2791
|
+
|
|
2792
|
+
```ts
|
|
2793
|
+
import { userSystem } from 'weifuwu'
|
|
2794
|
+
|
|
2795
|
+
const db = postgres()
|
|
2796
|
+
await db.migrate()
|
|
2797
|
+
const users = userSystem({ sql: db.sql, secret: process.env.AUTH_SECRET })
|
|
2798
|
+
await users.migrate() // 幂等建表(users + sessions)
|
|
2799
|
+
app.use(db)
|
|
2800
|
+
app.use(users) // 注入 ctx.user / ctx.auth
|
|
2801
|
+
users.routes(app) // POST /api/auth/register|login|logout|refresh + GET /api/auth/me
|
|
2802
|
+
|
|
2803
|
+
app.get('/me', (req, ctx) => ok(ctx.user)) // 已注入
|
|
2804
|
+
app.post('/secure', (req, ctx) => { ctx.auth.requireAuth(); ... })
|
|
2805
|
+
```
|
|
2806
|
+
|
|
2807
|
+
- **安全基线**:scrypt 密码哈希(per-user salt + timing-safe,异步不阻塞);access token = HMAC-SHA256 JWT(与 `weifuwu/client` 的 `auth()` 天然配对);refresh token = 不透明随机串,DB 只存哈希,logout/轮换即撤销
|
|
2808
|
+
- **防枚举**:登录失败统一 401(不泄露邮箱是否存在)
|
|
2809
|
+
- **`ctx.auth` 方法面**:`register` / `login` / `logout` / `requireAuth` / `setPassword(userId, newPwd)` / `createToken(type, payload, { ttlSeconds })`(邮箱验证/密码重置自接)
|
|
2810
|
+
- **裁剪**:OAuth、邮箱验证邮件(给底层 API 自接)、多因素、RBAC 权限引擎(只留 `role` 字段)、多租户语义(tenant-ready:`tenant` 字段 + token claim 已预留)
|
|
2811
|
+
|
|
2812
|
+
## queue — 可靠任务队列
|
|
2813
|
+
|
|
2814
|
+
```ts
|
|
2815
|
+
import { queue } from 'weifuwu'
|
|
2816
|
+
|
|
2817
|
+
const q = queue() // 默认 REDIS_URL
|
|
2818
|
+
app.use(q) // 注入 ctx.queue
|
|
2819
|
+
|
|
2820
|
+
app.post('/api/generate', async (req, ctx) => {
|
|
2821
|
+
await ctx.queue.add('llm.batch', { prompt: '...' }, { attempts: 3 })
|
|
2822
|
+
return new Response(null, { status: 202 }) // 立即 202,任务后台执行
|
|
2823
|
+
})
|
|
2824
|
+
|
|
2825
|
+
// 消费者(独立进程或同进程均可,多开安全)
|
|
2826
|
+
const worker = q.worker('llm.batch', async (job) => {
|
|
2827
|
+
await runLLM(job.data) // 失败自动重试 → 用尽进 DLQ(q:llm.batch:dead)
|
|
2828
|
+
}, { concurrency: 5, visibilityTimeout: 30_000 })
|
|
2829
|
+
await worker.start()
|
|
2830
|
+
await worker.stop() // 优雅停止
|
|
2831
|
+
```
|
|
2832
|
+
|
|
2833
|
+
- **语义**:at-least-once(handler 可能重复执行——幂等由业务保证);Redis Streams 消费组,多 worker 实例不重复消费
|
|
2834
|
+
- **可靠性**:失败 → 延迟重试(间隔 = `visibilityTimeout`,ZSET 延迟队列)→ attempts 用尽 → DLQ;worker 崩溃 → pending 由其他实例 `XAUTOCLAIM` 接管
|
|
2835
|
+
- 裁剪:延迟调度(除重试外)、cron、优先级、指数退避、速率限制不支持
|
|
2836
|
+
|
|
2837
|
+
## ai — LLM 对话(自研协议 + 零依赖客户端)
|
|
2838
|
+
|
|
2839
|
+
```ts
|
|
2840
|
+
import { ai } from 'weifuwu'
|
|
2841
|
+
|
|
2842
|
+
const a = ai() // DEEPSEEK_API_KEY / BASE_URL / MODEL 自动读 env,默认 deepseek-v4-flash
|
|
2843
|
+
app.use(a) // 注入 ctx.ai(worker/非请求场景也可直接 a.chat())
|
|
2844
|
+
|
|
2845
|
+
// 流式对话:路由一行返回 SSE(wf: 协议,详见 docs/ai-contract.md)
|
|
2846
|
+
app.post('/api/chat', async (req, ctx) => {
|
|
2847
|
+
const { messages } = await req.json()
|
|
2848
|
+
return ctx.ai.stream({ messages }, {
|
|
2849
|
+
signal: req.signal, // 断开即取消 provider 请求
|
|
2850
|
+
traceId: req.headers.get('x-trace-id') ?? undefined, // 追踪关联(协议 §7)
|
|
2851
|
+
})
|
|
2852
|
+
})
|
|
2853
|
+
|
|
2854
|
+
// 非流式(worker/后台):
|
|
2855
|
+
const res = await a.chat({ messages: [{ role: 'user', content: 'hi' }] })
|
|
2856
|
+
|
|
2857
|
+
// agent 引擎:工具循环 + 人工审批(HITL)
|
|
2858
|
+
const agent = a.agent({
|
|
2859
|
+
systemPrompt: '你是助手。查询天气时调用 query_weather 工具。',
|
|
2860
|
+
tools: [{
|
|
2861
|
+
name: 'query_weather',
|
|
2862
|
+
description: '查询城市天气',
|
|
2863
|
+
parameters: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
|
|
2864
|
+
run: async (args, { emit }) => {
|
|
2865
|
+
emit('wf:tool_progress', { toolCallId: 'x', step: 1, total: 2, message: '查询中…', status: 'running' })
|
|
2866
|
+
return { city: args.city, temp: 25 }
|
|
2867
|
+
},
|
|
2868
|
+
}],
|
|
2869
|
+
humanInTheLoop: true, // 每个工具执行前等人工审批
|
|
2870
|
+
})
|
|
2871
|
+
|
|
2872
|
+
app.post('/api/agent', async (req, ctx) => {
|
|
2873
|
+
const { messages } = await req.json()
|
|
2874
|
+
return agent.run(messages, { signal: req.signal, traceId: req.headers.get('x-trace-id') ?? undefined })
|
|
2875
|
+
})
|
|
2876
|
+
|
|
2877
|
+
// HITL 审批响应(前端点"允许/拒绝" → POST 到这里)
|
|
2878
|
+
app.post('/api/approve', async (req, ctx) => {
|
|
2879
|
+
ctx.ai.approve(await req.json()) // { id, decision, modifiedArgs?, note? }
|
|
2880
|
+
return new Response(null, { status: 200 })
|
|
2881
|
+
})
|
|
2882
|
+
```
|
|
2883
|
+
|
|
2884
|
+
前端解码(`weifuwu/client`):
|
|
2885
|
+
|
|
2886
|
+
```ts
|
|
2887
|
+
import { aiStream } from 'weifuwu/client'
|
|
2888
|
+
|
|
2889
|
+
const handle = aiStream('/api/chat', { messages }, {
|
|
2890
|
+
onToken: (text) => { /* 增量 append 到消息 */ },
|
|
2891
|
+
onToolCall: (call) => { /* 渲染工具卡片 */ },
|
|
2892
|
+
onDone: () => { /* 收尾 */ },
|
|
2893
|
+
onError: (e) => { /* 按 e.code 降级 */ },
|
|
2894
|
+
onEvent: (name, data) => { /* x:* 自定义事件透传 */ },
|
|
2895
|
+
})
|
|
2896
|
+
handle.abort() // 用户停止/组件卸载/导航跳走
|
|
2897
|
+
```
|
|
2898
|
+
|
|
2899
|
+
- **协议**:`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))
|
|
2900
|
+
- **agent 引擎**:`a.agent({ systemPrompt, tools, humanInTheLoop })` 工具循环(LLM → tool_call → 执行 → 回喂 → 重复);工具可 `emit` 进度/自定义事件、接收 `signal` 取消;HITL 审批(`ctx.ai.approve` 响应,拒绝≠终止、modified 改参、超时兜底)
|
|
2901
|
+
- **零依赖**:自研 OpenAI 兼容客户端(fetch + SSE 解析),默认 DeepSeek,`baseUrl` 可换任意 OpenAI 兼容端点(Ollama/vLLM/Moonshot…)
|
|
2902
|
+
- **追踪**:前端自动生成 `X-Trace-Id` → 后端以之作为 `message_start.id` → 工具内请求继承同一 traceId,整个 agent run 一次搜完
|
|
2903
|
+
- **裁剪**:embeddings、Anthropic 原生协议、审批持久化(连接断=会话亡)暂不支持;多 agent 编排不承诺(子 agent = 工具已支持)
|
|
2904
|
+
|
|
2905
|
+
## 组合示例:注册 → 验证邮件 → 欢迎任务 → 登录防爆破
|
|
2906
|
+
|
|
2907
|
+
```ts
|
|
2908
|
+
app.use(redis())
|
|
2909
|
+
app.use(rateLimit({ key: (req) => `login:${req.ip}`, max: 5, windowMs: 60_000 })) // 防爆破
|
|
2910
|
+
app.use(email({ from: 'no-reply@x.com', adapter: 'resend', resend: { apiKey } }))
|
|
2911
|
+
app.use(db)
|
|
2912
|
+
app.use(users)
|
|
2913
|
+
users.routes(app)
|
|
2914
|
+
|
|
2915
|
+
// 注册:限流守卫 → 用户系统 → 验证邮件 → 欢迎任务入队
|
|
2916
|
+
app.post('/api/auth/register', async (req, ctx) => {
|
|
2917
|
+
await ctx.limit(`register:${req.ip}`, { max: 10, windowMs: 60_000 })
|
|
2918
|
+
const result = await ctx.auth.register(await req.json())
|
|
2919
|
+
const token = ctx.auth.createToken('verify', { sub: result.user.id }, { ttlSeconds: 86400 })
|
|
2920
|
+
await ctx.email.send({ to: result.user.email, subject: '验证邮箱', html: `...?token=${token}` })
|
|
2921
|
+
await ctx.queue.add('welcome.flow', { userId: result.user.id })
|
|
2922
|
+
return created(result)
|
|
2923
|
+
})
|
|
2924
|
+
|
|
2925
|
+
const worker = q.worker('welcome.flow', async (job) => { /* 欢迎流程 */ })
|
|
2926
|
+
await worker.start()
|
|
2927
|
+
```
|
|
@@ -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';
|
package/dist/ai/sse.d.ts
ADDED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/client AI 解码器 — 消费 wf: 协议(docs/ai-contract.md)
|
|
3
|
+
*
|
|
4
|
+
* 协议的前端参考实现:POST → 解析 SSE → 按事件名分发回调。
|
|
5
|
+
*
|
|
6
|
+
* - 零依赖:fetch + ReadableStream + 自研 SSE 解析
|
|
7
|
+
* - 未知事件透传不抛错(协议 §6):x:* 走 onEvent,未订阅的跳过
|
|
8
|
+
* - 事件录制(副产品):record 开启时记录完整事件序列,可导出为测试 fixture
|
|
9
|
+
* - trace 桥(协议 §7):默认自动生成 X-Trace-Id 并随请求发送,
|
|
10
|
+
* 后端以之作为 wf:message_start.id → 整个 agent run 一次搜完
|
|
11
|
+
* - abort:handle.abort() 或外部 signal → 取消请求 → 后端断开 → provider 取消
|
|
12
|
+
*/
|
|
13
|
+
import type { WfApprovalRequest, WfDone, WfError, WfStreamEvent, WfToolCall, WfToolProgress, WfToolResult, WfUsage } from '../ai/types.ts';
|
|
14
|
+
export interface AiStreamCallbacks {
|
|
15
|
+
/** wf:token — 增量文本,直接 append */
|
|
16
|
+
onToken?: (text: string) => void;
|
|
17
|
+
/** wf:tool_call — 完整工具调用(后端已聚合) */
|
|
18
|
+
onToolCall?: (call: WfToolCall) => void;
|
|
19
|
+
/** wf:tool_result — ok:false 不代表会话结束 */
|
|
20
|
+
onToolResult?: (result: WfToolResult) => void;
|
|
21
|
+
/** wf:tool_progress — 长任务进度 */
|
|
22
|
+
onToolProgress?: (p: WfToolProgress) => void;
|
|
23
|
+
/** wf:step — 步骤可视化(agent 扩展) */
|
|
24
|
+
onStep?: (s: {
|
|
25
|
+
type: 'llm' | 'tool';
|
|
26
|
+
content?: string;
|
|
27
|
+
toolCallId?: string;
|
|
28
|
+
name?: string;
|
|
29
|
+
}) => void;
|
|
30
|
+
/** wf:approval_request — 渲染审批卡片 */
|
|
31
|
+
onApproval?: (req: WfApprovalRequest) => void;
|
|
32
|
+
/** wf:usage — token 计数 */
|
|
33
|
+
onUsage?: (u: WfUsage) => void;
|
|
34
|
+
/** wf:done — 收尾 */
|
|
35
|
+
onDone?: (d: WfDone) => void;
|
|
36
|
+
/** wf:error — 错误即值,结构化降级 */
|
|
37
|
+
onError?: (e: WfError) => void;
|
|
38
|
+
/** x:* 自定义事件透传兜底(协议 §6:框架不解释) */
|
|
39
|
+
onEvent?: (name: string, data: unknown) => void;
|
|
40
|
+
}
|
|
41
|
+
export interface AiStreamOptions extends AiStreamCallbacks {
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
headers?: Record<string, string>;
|
|
44
|
+
/** 显式指定 traceId;默认自动生成(协议 §7) */
|
|
45
|
+
traceId?: string;
|
|
46
|
+
/** 录制事件序列(调试/导出 fixture),默认开启,环形上限 1000 */
|
|
47
|
+
record?: boolean;
|
|
48
|
+
}
|
|
49
|
+
export interface AiStreamHandle {
|
|
50
|
+
/** 取消请求(用户停止/组件卸载/导航跳走时调用) */
|
|
51
|
+
abort: () => void;
|
|
52
|
+
/** 流结束(正常 done / error / abort 都 resolve) */
|
|
53
|
+
done: Promise<void>;
|
|
54
|
+
/** 本次请求使用的 traceId(自动生成的也能拿到) */
|
|
55
|
+
traceId: string;
|
|
56
|
+
/** 录制的事件序列(record: true 时) */
|
|
57
|
+
events: WfStreamEvent[];
|
|
58
|
+
}
|
|
59
|
+
export declare function aiStream(url: string, body: unknown, options?: AiStreamOptions): AiStreamHandle;
|
package/dist/client/index.d.ts
CHANGED
|
@@ -31,6 +31,9 @@ export { ApiError } from './middleware/api.ts';
|
|
|
31
31
|
export type { AuthClient, AuthOptions, AuthInjected } from './middleware/auth.ts';
|
|
32
32
|
export { extendCtx } from './types.ts';
|
|
33
33
|
export type { WfuiContext, AppMiddleware, RouteDef } from './types.ts';
|
|
34
|
+
export { aiStream } from './ai.ts';
|
|
35
|
+
export type { AiStreamCallbacks, AiStreamOptions, AiStreamHandle } from './ai.ts';
|
|
36
|
+
export type { WfStreamEvent, WfMessageStart, WfToken, WfUsage, WfDone, WfError, WfErrorCode, WfToolCall, WfToolResult, WfToolProgress, WfStep, WfApprovalRequest, WfApprovalResponse, WfApprovalDecision, ChatMessage, ChatParams, MessageRole, ToolCall, ToolDefinition, } from '../ai/types.ts';
|
|
34
37
|
export { ErrorBoundary } from './error-boundary.ts';
|
|
35
38
|
export type { ErrorBoundaryProps } from './error-boundary.ts';
|
|
36
39
|
export { i18n } from './i18n.ts';
|