weifuwu 0.59.1 → 0.60.1
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 +20 -3
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +362 -129
- package/dist/client/types.d.ts +11 -0
- package/dist/client/use-chat.d.ts +90 -0
- package/dist/client/vnode.d.ts +1 -1
- package/dist/components/AiChat/AiChat.d.ts +47 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +147 -19
- package/dist/components/style.css +111 -0
- package/dist/index.js +40 -4
- package/dist/queue/index.d.ts +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ npm install weifuwu
|
|
|
50
50
|
|
|
51
51
|
**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
52
|
|
|
53
|
-
**AI 是一等公民** — 自研 OpenAI 兼容协议(`docs/ai-contract.md`)+ 零依赖流式客户端 + agent 工具循环 + HITL
|
|
53
|
+
**AI 是一等公民** — 自研 OpenAI 兼容协议(`docs/ai-contract.md`)+ 零依赖流式客户端 + agent 工具循环 + HITL 人工审批。后端 `ctx.ai` 一个入口:`chat()` / `stream()` / `agent()` / `approve()`;前端 `ctx.ui.useChat()`(会话语义)+ `AiChat` 组件(标准对话界面)——流式 token / 工具调用卡 / 审批卡开箱即用,协议对页面完全透明,不用 ai-sdk。
|
|
54
54
|
|
|
55
55
|
**SaaS 地基随包内置** — rateLimit(限流)/ email(邮件)/ userSystem(用户认证)/ queue(可靠队列)以中间件形态随包提供,`app.use(...)` 一行接入(详见文末[SaaS 地基模块](#saas-地基模块ratelimit--email--usersystem--queue))。
|
|
56
56
|
|
|
@@ -259,7 +259,7 @@ createApp().use(router({ routes })).mount('#root', RouteView, { hydrate: true })
|
|
|
259
259
|
| `weifuwu` | **email** | 邮件发送(Resend/SMTP 自研/自定义适配器)→ `ctx.email` | Router |
|
|
260
260
|
| `weifuwu` | **userSystem** | 用户系统(scrypt 密码哈希 + 混合会话)→ `ctx.user` / `ctx.auth` + `/api/auth/*` | Router, postgres |
|
|
261
261
|
| `weifuwu` | **queue** | 可靠任务队列(Redis Streams,at-least-once + DLQ)→ `ctx.queue` | Router, redis |
|
|
262
|
-
| `weifuwu` | **ai** | LLM 对话(自研 OpenAI 兼容协议 + 自研 SSE 解码,默认 DeepSeek)→ `ctx.ai` + `
|
|
262
|
+
| `weifuwu` | **ai** | LLM 对话(自研 OpenAI 兼容协议 + 自研 SSE 解码,默认 DeepSeek)→ `ctx.ai` + `ctx.ui.useChat` + `AiChat` | Router |
|
|
263
263
|
| `weifuwu/dev` | **dev loader** | Node loader:服务端直接跑 `.ts/.tsx`(`--import weifuwu/dev`) | esbuild |
|
|
264
264
|
| `weifuwu` | **graphql** | GraphQL 端点(支持 GraphiQL) | Router |
|
|
265
265
|
| `weifuwu` | **createMiddleware** | 类型安全中间件工厂 | — |
|
|
@@ -275,7 +275,7 @@ createApp().use(router({ routes })).mount('#root', RouteView, { hydrate: true })
|
|
|
275
275
|
| `weifuwu/client` | **ErrorBoundary** | 错误边界组件 | createApp |
|
|
276
276
|
| `weifuwu/client` | **lockScroll/trapFocus** | 滚动锁定 / 焦点陷阱工具 | — |
|
|
277
277
|
| `weifuwu/client` | **popup** | 弹层 fixed 定位工具(`computeFixedPos` / `computeFixedPosRect`) | — |
|
|
278
|
-
| `weifuwu/components` | **
|
|
278
|
+
| `weifuwu/components` | **47 个组件** | Button/Table/Modal/Confirm/Toast/... + `confirm()` / `toast()` 命令式中间件 | weifuwu/client |
|
|
279
279
|
| `weifuwu/layout` | **CSS 布局** | 67 个布局原语 + 115 个主题 Token(也支持 `weifuwu/layout/style.css`) | — |
|
|
280
280
|
|
|
281
281
|
---
|
|
@@ -2436,6 +2436,7 @@ props 变化 ──────────────────────
|
|
|
2436
2436
|
|
|
2437
2437
|
| 组件 | 导入名 | 关键 Props | 说明 |
|
|
2438
2438
|
|-----|--------|-----------|------|
|
|
2439
|
+
| AiChat | `AiChat` | `chat`, `maxHeight?`, `labels?`, `renderMessage?`, `renderToolArgs?` | 标准 AI 对话界面:气泡 + 工具卡 + 审批卡 + 自动滚动 + 错误重试(接收 `ctx.ui.useChat()` handle) |
|
|
2439
2440
|
| ToolCallCard | `ToolCallCard` | `call`, `progress?`, `result?`, `renderArgs?` | 工具调用卡片:running(进度条)/ ok / error 三态(协议 §4) |
|
|
2440
2441
|
| ApprovalCard | `ApprovalCard` | `request`, `status?`, `onApprove`, `onReject` | 人工审批卡片:待批(允许/拒绝+备注)/ 已批 / 已拒 / 超时(协议 §4.5) |
|
|
2441
2442
|
|
|
@@ -3065,6 +3066,22 @@ const handle = aiStream('/api/chat', { messages }, {
|
|
|
3065
3066
|
handle.abort() // 用户停止/组件卸载/导航跳走
|
|
3066
3067
|
```
|
|
3067
3068
|
|
|
3069
|
+
前端对话层(会话语义 + 标准界面,协议对页面透明):
|
|
3070
|
+
|
|
3071
|
+
```tsx
|
|
3072
|
+
// ctx.ui.useChat:会话语义(消息累积/工具内嵌/审批/重试),返回页面同一个 $
|
|
3073
|
+
const $ = ctx.ui.useChat({ url: '/api/chat', approveUrl: '/api/approve' })
|
|
3074
|
+
// $.messages / $.input / $.streaming / $.error / $.usage / $.step
|
|
3075
|
+
// $.send() / $.stop() / $.retry() / $.clear() / $.approve('approved', note?)
|
|
3076
|
+
|
|
3077
|
+
// AiChat:标准对话界面(气泡 / 工具卡 / 审批卡 / 自动滚动 / 错误重试)
|
|
3078
|
+
return () => <AiChat chat={$} />
|
|
3079
|
+
|
|
3080
|
+
// agent 模式消息内嵌:msg.toolCalls(ToolCallCard 直接消费)/ msg.approval(ApprovalCard)
|
|
3081
|
+
```
|
|
3082
|
+
|
|
3083
|
+
> 分层:`ctx.ai`(后端协议)→ `aiStream`(传输解码)→ `useChat`(会话语义)→ `AiChat`(标准界面)。要完全自定义 UI 的应用用 useChat + 自有渲染;要 5 分钟出界面用 AiChat。
|
|
3084
|
+
|
|
3068
3085
|
- **协议**:`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))
|
|
3069
3086
|
- **agent 引擎**:`a.agent({ systemPrompt, tools, humanInTheLoop })` 工具循环(LLM → tool_call → 执行 → 回喂 → 重复);工具可 `emit` 进度/自定义事件、接收 `signal` 取消;HITL 审批(`ctx.ai.approve` 响应,拒绝≠终止、modified 改参、超时兜底)
|
|
3070
3087
|
- **零依赖**:自研 OpenAI 兼容客户端(fetch + SSE 解析),默认 DeepSeek,`baseUrl` 可换任意 OpenAI 兼容端点(Ollama/vLLM/Moonshot…)
|
package/dist/client/index.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export { extendCtx } from './types.ts';
|
|
|
33
33
|
export type { WfuiContext, AppMiddleware, RouteDef } from './types.ts';
|
|
34
34
|
export { aiStream } from './ai.ts';
|
|
35
35
|
export type { AiStreamCallbacks, AiStreamOptions, AiStreamHandle } from './ai.ts';
|
|
36
|
+
export { toChatMessages } from './use-chat.ts';
|
|
37
|
+
export type { UiMessage, UiToolCall, UseChatOptions, UseChatState, UseChatHandle, ChatApi } from './use-chat.ts';
|
|
36
38
|
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';
|
|
37
39
|
export { ErrorBoundary } from './error-boundary.ts';
|
|
38
40
|
export type { ErrorBoundaryProps } from './error-boundary.ts';
|
package/dist/client/index.js
CHANGED
|
@@ -284,6 +284,18 @@ function setProp(el, key, value) {
|
|
|
284
284
|
el.setAttribute(key, String(value));
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
|
+
var _refChurn = /* @__PURE__ */ new WeakMap();
|
|
288
|
+
function warnRefChurn(el) {
|
|
289
|
+
const entry = _refChurn.get(el) ?? { count: 0, warned: false };
|
|
290
|
+
entry.count++;
|
|
291
|
+
if (entry.count >= 3 && !entry.warned) {
|
|
292
|
+
entry.warned = true;
|
|
293
|
+
console.warn(
|
|
294
|
+
"[weifuwu] ref \u51FD\u6570\u6BCF\u6B21\u6E32\u67D3\u90FD\u53D8\u5316\uFF08\u5185\u8054 ref\uFF09\u2014\u2014ref \u56DE\u8C03\u5728\u6BCF\u6B21\u6E32\u67D3\u540E\u91CD\u65B0\u6267\u884C\uFF08\u6027\u80FD\u635F\u8017\uFF09\u3002\u628A ref \u5B9A\u4E49\u5230 mount \u4F5C\u7528\u57DF\uFF08\u7A33\u5B9A\u5F15\u7528\uFF09\uFF1Aconst listRef = (el) => {...}; return h('div', { ref: listRef })"
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
_refChurn.set(el, entry);
|
|
298
|
+
}
|
|
287
299
|
function patchValue(parent, oldNode, oldInput, newInput, ctx) {
|
|
288
300
|
if (oldInput == null) {
|
|
289
301
|
if (newInput == null) return null;
|
|
@@ -379,8 +391,8 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
|
|
|
379
391
|
const oldRef = oldV.props?.ref;
|
|
380
392
|
const newRef = newV.props?.ref;
|
|
381
393
|
if (oldRef !== newRef) {
|
|
382
|
-
if (typeof oldRef === "function") oldRef(null);
|
|
383
394
|
if (typeof newRef === "function") newRef(oldNode);
|
|
395
|
+
if (typeof oldRef === "function" && typeof newRef === "function") warnRefChurn(oldNode);
|
|
384
396
|
}
|
|
385
397
|
patchProps(oldNode, oldV.props, newV.props);
|
|
386
398
|
patchChildren(oldNode, oldV, newV, ctx);
|
|
@@ -838,6 +850,7 @@ async function hydrateVNode(container, vnode, ctx) {
|
|
|
838
850
|
// src/client/reactive.ts
|
|
839
851
|
function createReactiveState(dirty) {
|
|
840
852
|
const proxyCache = /* @__PURE__ */ new WeakMap();
|
|
853
|
+
const watchers = /* @__PURE__ */ new Set();
|
|
841
854
|
const reactive = (target) => {
|
|
842
855
|
if (target === null || typeof target !== "object") return target;
|
|
843
856
|
if (proxyCache.has(target)) return proxyCache.get(target);
|
|
@@ -847,6 +860,7 @@ function createReactiveState(dirty) {
|
|
|
847
860
|
if (old === value) return true;
|
|
848
861
|
Reflect.set(target2, key, value);
|
|
849
862
|
dirty();
|
|
863
|
+
for (const w of watchers) w();
|
|
850
864
|
return true;
|
|
851
865
|
},
|
|
852
866
|
get(target2, key) {
|
|
@@ -858,6 +872,7 @@ function createReactiveState(dirty) {
|
|
|
858
872
|
if (Reflect.has(target2, key)) {
|
|
859
873
|
Reflect.deleteProperty(target2, key);
|
|
860
874
|
dirty();
|
|
875
|
+
for (const w of watchers) w();
|
|
861
876
|
}
|
|
862
877
|
return true;
|
|
863
878
|
}
|
|
@@ -865,7 +880,326 @@ function createReactiveState(dirty) {
|
|
|
865
880
|
proxyCache.set(target, proxy);
|
|
866
881
|
return proxy;
|
|
867
882
|
};
|
|
868
|
-
|
|
883
|
+
const root = reactive({});
|
|
884
|
+
Object.defineProperty(root, "__watch", {
|
|
885
|
+
value: (cb) => {
|
|
886
|
+
watchers.add(cb);
|
|
887
|
+
return () => {
|
|
888
|
+
watchers.delete(cb);
|
|
889
|
+
};
|
|
890
|
+
},
|
|
891
|
+
writable: false,
|
|
892
|
+
enumerable: false
|
|
893
|
+
});
|
|
894
|
+
return root;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// src/client/ai.ts
|
|
898
|
+
var RECORD_LIMIT = 1e3;
|
|
899
|
+
function aiStream(url, body, options) {
|
|
900
|
+
const controller = new AbortController();
|
|
901
|
+
const external = options?.signal;
|
|
902
|
+
if (external) {
|
|
903
|
+
if (external.aborted) controller.abort();
|
|
904
|
+
else external.addEventListener("abort", () => controller.abort(), { once: true });
|
|
905
|
+
}
|
|
906
|
+
const traceId = options?.traceId ?? randomId();
|
|
907
|
+
const record = options?.record !== false;
|
|
908
|
+
const events = [];
|
|
909
|
+
const done = (async () => {
|
|
910
|
+
let res;
|
|
911
|
+
try {
|
|
912
|
+
res = await fetch(url, {
|
|
913
|
+
method: "POST",
|
|
914
|
+
headers: {
|
|
915
|
+
"Content-Type": "application/json",
|
|
916
|
+
"X-Trace-Id": traceId,
|
|
917
|
+
Accept: "text/event-stream",
|
|
918
|
+
...options?.headers
|
|
919
|
+
},
|
|
920
|
+
body: JSON.stringify(body),
|
|
921
|
+
signal: controller.signal
|
|
922
|
+
});
|
|
923
|
+
} catch (err) {
|
|
924
|
+
if (controller.signal.aborted) return;
|
|
925
|
+
options?.onError?.({ code: "provider_error", message: err instanceof Error ? err.message : String(err) });
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (!res.ok) {
|
|
929
|
+
options?.onError?.({
|
|
930
|
+
code: httpErrorCode(res.status),
|
|
931
|
+
message: `HTTP ${res.status} ${res.statusText}`
|
|
932
|
+
});
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
try {
|
|
936
|
+
for await (const { name, data } of parseWfEvents(res.body)) {
|
|
937
|
+
if (controller.signal.aborted) return;
|
|
938
|
+
pushEvent(events, { name, data }, record);
|
|
939
|
+
dispatch(name, data, options ?? {});
|
|
940
|
+
}
|
|
941
|
+
} catch (err) {
|
|
942
|
+
if (controller.signal.aborted) return;
|
|
943
|
+
options?.onError?.({ code: "provider_error", message: err instanceof Error ? err.message : String(err) });
|
|
944
|
+
}
|
|
945
|
+
})();
|
|
946
|
+
return {
|
|
947
|
+
abort: () => controller.abort(),
|
|
948
|
+
done,
|
|
949
|
+
traceId,
|
|
950
|
+
events
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
function dispatch(name, data, o) {
|
|
954
|
+
switch (name) {
|
|
955
|
+
case "wf:token":
|
|
956
|
+
return o.onToken?.(data.text);
|
|
957
|
+
case "wf:tool_call":
|
|
958
|
+
return o.onToolCall?.(data);
|
|
959
|
+
case "wf:tool_result":
|
|
960
|
+
return o.onToolResult?.(data);
|
|
961
|
+
case "wf:tool_progress":
|
|
962
|
+
return o.onToolProgress?.(data);
|
|
963
|
+
case "wf:step":
|
|
964
|
+
return o.onStep?.(data);
|
|
965
|
+
case "wf:approval_request":
|
|
966
|
+
return o.onApproval?.(data);
|
|
967
|
+
case "wf:usage":
|
|
968
|
+
return o.onUsage?.(data);
|
|
969
|
+
case "wf:done":
|
|
970
|
+
return o.onDone?.(data);
|
|
971
|
+
case "wf:error":
|
|
972
|
+
return o.onError?.(data);
|
|
973
|
+
default:
|
|
974
|
+
return o.onEvent?.(name, data);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
async function* parseWfEvents(stream) {
|
|
978
|
+
const reader = stream.getReader();
|
|
979
|
+
const decoder = new TextDecoder();
|
|
980
|
+
let buffer = "";
|
|
981
|
+
try {
|
|
982
|
+
while (true) {
|
|
983
|
+
const { done, value } = await reader.read();
|
|
984
|
+
if (done) break;
|
|
985
|
+
buffer += decoder.decode(value, { stream: true });
|
|
986
|
+
const blocks = buffer.split("\n\n");
|
|
987
|
+
buffer = blocks.pop() ?? "";
|
|
988
|
+
for (const block of blocks) {
|
|
989
|
+
let name = "message";
|
|
990
|
+
let data = "";
|
|
991
|
+
for (const line of block.split("\n")) {
|
|
992
|
+
if (line.startsWith("event: ")) name = line.slice(7);
|
|
993
|
+
else if (line.startsWith("data: ")) data += line.slice(6);
|
|
994
|
+
}
|
|
995
|
+
if (!data) continue;
|
|
996
|
+
try {
|
|
997
|
+
yield { name, data: JSON.parse(data) };
|
|
998
|
+
} catch {
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
} finally {
|
|
1003
|
+
reader.releaseLock();
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function pushEvent(events, e, record) {
|
|
1007
|
+
if (!record) return;
|
|
1008
|
+
if (events.length >= RECORD_LIMIT) events.shift();
|
|
1009
|
+
events.push(e);
|
|
1010
|
+
}
|
|
1011
|
+
function httpErrorCode(status) {
|
|
1012
|
+
if (status === 401 || status === 403) return "auth_failed";
|
|
1013
|
+
if (status === 429) return "rate_limited";
|
|
1014
|
+
if (status >= 500) return "provider_error";
|
|
1015
|
+
return "invalid_request";
|
|
1016
|
+
}
|
|
1017
|
+
function randomId() {
|
|
1018
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
1019
|
+
return crypto.randomUUID();
|
|
1020
|
+
}
|
|
1021
|
+
return `trace-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// src/client/use-chat.ts
|
|
1025
|
+
function toChatMessages(msgs) {
|
|
1026
|
+
return msgs.map((m) => ({ role: m.role, content: m.content }));
|
|
1027
|
+
}
|
|
1028
|
+
function uid() {
|
|
1029
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
1030
|
+
return crypto.randomUUID();
|
|
1031
|
+
}
|
|
1032
|
+
return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1033
|
+
}
|
|
1034
|
+
function createChatSession(state, transport, options) {
|
|
1035
|
+
if (options.initialMessages) {
|
|
1036
|
+
state.messages = structuredClone(options.initialMessages);
|
|
1037
|
+
} else if (state.messages === void 0) state.messages = [];
|
|
1038
|
+
if (state.input === void 0) state.input = "";
|
|
1039
|
+
if (state.streaming === void 0) state.streaming = false;
|
|
1040
|
+
if (state.error === void 0) state.error = null;
|
|
1041
|
+
if (state.usage === void 0) state.usage = null;
|
|
1042
|
+
if (state.step === void 0) state.step = null;
|
|
1043
|
+
let handle = null;
|
|
1044
|
+
function current() {
|
|
1045
|
+
if (!state.streaming) return void 0;
|
|
1046
|
+
const m = state.messages[state.messages.length - 1];
|
|
1047
|
+
return m && m.role === "assistant" && m.status === "streaming" ? m : void 0;
|
|
1048
|
+
}
|
|
1049
|
+
function apply(name, data, onEvent) {
|
|
1050
|
+
const m = current();
|
|
1051
|
+
switch (name) {
|
|
1052
|
+
case "wf:token":
|
|
1053
|
+
if (m) m.content += data.text;
|
|
1054
|
+
break;
|
|
1055
|
+
case "wf:tool_call": {
|
|
1056
|
+
if (m) {
|
|
1057
|
+
if (!m.toolCalls) m.toolCalls = [];
|
|
1058
|
+
m.toolCalls.push({ call: data, status: "running" });
|
|
1059
|
+
}
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1062
|
+
case "wf:tool_progress": {
|
|
1063
|
+
const p = data;
|
|
1064
|
+
const tc = m?.toolCalls?.find((t) => t.call.id === p.toolCallId);
|
|
1065
|
+
if (tc) tc.progress = p;
|
|
1066
|
+
break;
|
|
1067
|
+
}
|
|
1068
|
+
case "wf:tool_result": {
|
|
1069
|
+
const r = data;
|
|
1070
|
+
const tc = m?.toolCalls?.find((t) => t.call.id === r.id);
|
|
1071
|
+
if (tc) {
|
|
1072
|
+
tc.result = r;
|
|
1073
|
+
tc.status = r.ok ? "ok" : "error";
|
|
1074
|
+
}
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
case "wf:approval_request":
|
|
1078
|
+
if (m) m.approval = data;
|
|
1079
|
+
break;
|
|
1080
|
+
case "wf:usage":
|
|
1081
|
+
if (m) m.usage = data;
|
|
1082
|
+
break;
|
|
1083
|
+
case "wf:step":
|
|
1084
|
+
state.step = data;
|
|
1085
|
+
break;
|
|
1086
|
+
case "wf:done":
|
|
1087
|
+
if (m) {
|
|
1088
|
+
m.status = "done";
|
|
1089
|
+
m.approval = void 0;
|
|
1090
|
+
}
|
|
1091
|
+
state.streaming = false;
|
|
1092
|
+
state.step = null;
|
|
1093
|
+
break;
|
|
1094
|
+
case "wf:error":
|
|
1095
|
+
if (m) {
|
|
1096
|
+
m.status = "error";
|
|
1097
|
+
m.error = data;
|
|
1098
|
+
m.approval = void 0;
|
|
1099
|
+
}
|
|
1100
|
+
state.error = data;
|
|
1101
|
+
state.streaming = false;
|
|
1102
|
+
state.step = null;
|
|
1103
|
+
break;
|
|
1104
|
+
default:
|
|
1105
|
+
onEvent?.(name, data);
|
|
1106
|
+
break;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
function startStream() {
|
|
1110
|
+
const body = options.body ? options.body(state.messages) : { messages: toChatMessages(state.messages) };
|
|
1111
|
+
handle = transport(options.url, body, {
|
|
1112
|
+
onToken: (t) => apply("wf:token", { text: t }),
|
|
1113
|
+
onToolCall: (c) => apply("wf:tool_call", c),
|
|
1114
|
+
onToolProgress: (p) => apply("wf:tool_progress", p),
|
|
1115
|
+
onToolResult: (r) => apply("wf:tool_result", r),
|
|
1116
|
+
onApproval: (req) => apply("wf:approval_request", req),
|
|
1117
|
+
onUsage: (u) => apply("wf:usage", u),
|
|
1118
|
+
onStep: (s) => apply("wf:step", s),
|
|
1119
|
+
onDone: (d) => apply("wf:done", d),
|
|
1120
|
+
onError: (e) => apply("wf:error", e),
|
|
1121
|
+
onEvent: (n, d) => apply(n, d, options.onEvent)
|
|
1122
|
+
}, { signal: options.signal, headers: options.headers });
|
|
1123
|
+
state.streaming = true;
|
|
1124
|
+
const m = current();
|
|
1125
|
+
if (m && handle?.traceId) m.id = handle.traceId;
|
|
1126
|
+
}
|
|
1127
|
+
function send() {
|
|
1128
|
+
if (state.streaming) return;
|
|
1129
|
+
const text = String(state.input ?? "").trim();
|
|
1130
|
+
if (!text) return;
|
|
1131
|
+
state.input = "";
|
|
1132
|
+
state.error = null;
|
|
1133
|
+
state.usage = null;
|
|
1134
|
+
state.step = null;
|
|
1135
|
+
state.messages.push(
|
|
1136
|
+
{ id: uid(), role: "user", content: text, status: "done" },
|
|
1137
|
+
{ id: uid(), role: "assistant", content: "", status: "streaming", toolCalls: [] }
|
|
1138
|
+
);
|
|
1139
|
+
startStream();
|
|
1140
|
+
}
|
|
1141
|
+
function stop() {
|
|
1142
|
+
handle?.abort();
|
|
1143
|
+
state.streaming = false;
|
|
1144
|
+
state.step = null;
|
|
1145
|
+
const m = state.messages[state.messages.length - 1];
|
|
1146
|
+
if (m && m.role === "assistant" && m.status === "streaming") {
|
|
1147
|
+
if (!m.content && (!m.toolCalls || m.toolCalls.length === 0)) {
|
|
1148
|
+
state.messages.pop();
|
|
1149
|
+
} else {
|
|
1150
|
+
m.status = "done";
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
function retry() {
|
|
1155
|
+
if (state.streaming) return;
|
|
1156
|
+
let idx = -1;
|
|
1157
|
+
for (let i = state.messages.length - 1; i >= 0; i--) {
|
|
1158
|
+
if (state.messages[i].role === "user") {
|
|
1159
|
+
idx = i;
|
|
1160
|
+
break;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
if (idx === -1) return;
|
|
1164
|
+
state.messages.splice(idx + 1);
|
|
1165
|
+
state.error = null;
|
|
1166
|
+
state.usage = null;
|
|
1167
|
+
state.step = null;
|
|
1168
|
+
state.messages.push({ id: uid(), role: "assistant", content: "", status: "streaming", toolCalls: [] });
|
|
1169
|
+
startStream();
|
|
1170
|
+
}
|
|
1171
|
+
function clear() {
|
|
1172
|
+
handle?.abort();
|
|
1173
|
+
state.messages = [];
|
|
1174
|
+
state.input = "";
|
|
1175
|
+
state.streaming = false;
|
|
1176
|
+
state.error = null;
|
|
1177
|
+
state.usage = null;
|
|
1178
|
+
state.step = null;
|
|
1179
|
+
}
|
|
1180
|
+
async function approve(decision, note) {
|
|
1181
|
+
const m = state.messages[state.messages.length - 1];
|
|
1182
|
+
const req = m?.role === "assistant" ? m.approval : void 0;
|
|
1183
|
+
if (!req) return;
|
|
1184
|
+
m.approval = void 0;
|
|
1185
|
+
if (!options.approveUrl) return;
|
|
1186
|
+
try {
|
|
1187
|
+
await fetch(options.approveUrl, {
|
|
1188
|
+
method: "POST",
|
|
1189
|
+
headers: { "Content-Type": "application/json", ...options.headers },
|
|
1190
|
+
body: JSON.stringify({ id: req.id, decision, note }),
|
|
1191
|
+
signal: options.signal
|
|
1192
|
+
});
|
|
1193
|
+
} catch (err) {
|
|
1194
|
+
if (options.signal?.aborted) return;
|
|
1195
|
+
state.error = { code: "provider_error", message: err instanceof Error ? err.message : String(err) };
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
function dispose() {
|
|
1199
|
+
handle?.abort();
|
|
1200
|
+
handle = null;
|
|
1201
|
+
}
|
|
1202
|
+
return { send, stop, retry, clear, approve, dispose };
|
|
869
1203
|
}
|
|
870
1204
|
|
|
871
1205
|
// src/client/app.ts
|
|
@@ -1050,6 +1384,31 @@ function createApp() {
|
|
|
1050
1384
|
}
|
|
1051
1385
|
return this._$cache;
|
|
1052
1386
|
},
|
|
1387
|
+
/**
|
|
1388
|
+
* AI 对话会话(会话语义 + 工具调用内嵌 + HITL 审批)
|
|
1389
|
+
*
|
|
1390
|
+
* 用法(mount 阶段):
|
|
1391
|
+
* const $ = ctx.ui.useChat({ url: '/api/chat', approveUrl: '/api/approve' })
|
|
1392
|
+
* // 状态:$.messages / $.input / $.streaming / $.error / $.usage / $.step
|
|
1393
|
+
* // 操作:$.send() / $.stop() / $.retry() / $.clear() / $.approve('approved', note?)
|
|
1394
|
+
* // agent:msg.toolCalls(ToolCallCard 直接消费) / msg.approval(ApprovalCard)
|
|
1395
|
+
*
|
|
1396
|
+
* 返回组件同一个 $(WeakMap 缓存复用):chat 状态与页面状态共处一个容器。
|
|
1397
|
+
* 卸载时调用 $.dispose()(或经 ref cleanup)中止流,防泄漏。
|
|
1398
|
+
*/
|
|
1399
|
+
useChat: function(options2) {
|
|
1400
|
+
const state = this.$();
|
|
1401
|
+
const api2 = createChatSession(state, aiStream, options2);
|
|
1402
|
+
Object.assign(state, {
|
|
1403
|
+
send: api2.send,
|
|
1404
|
+
stop: api2.stop,
|
|
1405
|
+
retry: api2.retry,
|
|
1406
|
+
clear: api2.clear,
|
|
1407
|
+
approve: api2.approve,
|
|
1408
|
+
dispose: api2.dispose
|
|
1409
|
+
});
|
|
1410
|
+
return state;
|
|
1411
|
+
},
|
|
1053
1412
|
/**
|
|
1054
1413
|
* 响应式媒体查询:注册监听,值变化时自动 dirty
|
|
1055
1414
|
*
|
|
@@ -1584,133 +1943,6 @@ function auth(options) {
|
|
|
1584
1943
|
};
|
|
1585
1944
|
}
|
|
1586
1945
|
|
|
1587
|
-
// src/client/ai.ts
|
|
1588
|
-
var RECORD_LIMIT = 1e3;
|
|
1589
|
-
function aiStream(url, body, options) {
|
|
1590
|
-
const controller = new AbortController();
|
|
1591
|
-
const external = options?.signal;
|
|
1592
|
-
if (external) {
|
|
1593
|
-
if (external.aborted) controller.abort();
|
|
1594
|
-
else external.addEventListener("abort", () => controller.abort(), { once: true });
|
|
1595
|
-
}
|
|
1596
|
-
const traceId = options?.traceId ?? randomId();
|
|
1597
|
-
const record = options?.record !== false;
|
|
1598
|
-
const events = [];
|
|
1599
|
-
const done = (async () => {
|
|
1600
|
-
let res;
|
|
1601
|
-
try {
|
|
1602
|
-
res = await fetch(url, {
|
|
1603
|
-
method: "POST",
|
|
1604
|
-
headers: {
|
|
1605
|
-
"Content-Type": "application/json",
|
|
1606
|
-
"X-Trace-Id": traceId,
|
|
1607
|
-
Accept: "text/event-stream",
|
|
1608
|
-
...options?.headers
|
|
1609
|
-
},
|
|
1610
|
-
body: JSON.stringify(body),
|
|
1611
|
-
signal: controller.signal
|
|
1612
|
-
});
|
|
1613
|
-
} catch (err) {
|
|
1614
|
-
if (controller.signal.aborted) return;
|
|
1615
|
-
options?.onError?.({ code: "provider_error", message: err instanceof Error ? err.message : String(err) });
|
|
1616
|
-
return;
|
|
1617
|
-
}
|
|
1618
|
-
if (!res.ok) {
|
|
1619
|
-
options?.onError?.({
|
|
1620
|
-
code: httpErrorCode(res.status),
|
|
1621
|
-
message: `HTTP ${res.status} ${res.statusText}`
|
|
1622
|
-
});
|
|
1623
|
-
return;
|
|
1624
|
-
}
|
|
1625
|
-
try {
|
|
1626
|
-
for await (const { name, data } of parseWfEvents(res.body)) {
|
|
1627
|
-
if (controller.signal.aborted) return;
|
|
1628
|
-
pushEvent(events, { name, data }, record);
|
|
1629
|
-
dispatch(name, data, options ?? {});
|
|
1630
|
-
}
|
|
1631
|
-
} catch (err) {
|
|
1632
|
-
if (controller.signal.aborted) return;
|
|
1633
|
-
options?.onError?.({ code: "provider_error", message: err instanceof Error ? err.message : String(err) });
|
|
1634
|
-
}
|
|
1635
|
-
})();
|
|
1636
|
-
return {
|
|
1637
|
-
abort: () => controller.abort(),
|
|
1638
|
-
done,
|
|
1639
|
-
traceId,
|
|
1640
|
-
events
|
|
1641
|
-
};
|
|
1642
|
-
}
|
|
1643
|
-
function dispatch(name, data, o) {
|
|
1644
|
-
switch (name) {
|
|
1645
|
-
case "wf:token":
|
|
1646
|
-
return o.onToken?.(data.text);
|
|
1647
|
-
case "wf:tool_call":
|
|
1648
|
-
return o.onToolCall?.(data);
|
|
1649
|
-
case "wf:tool_result":
|
|
1650
|
-
return o.onToolResult?.(data);
|
|
1651
|
-
case "wf:tool_progress":
|
|
1652
|
-
return o.onToolProgress?.(data);
|
|
1653
|
-
case "wf:step":
|
|
1654
|
-
return o.onStep?.(data);
|
|
1655
|
-
case "wf:approval_request":
|
|
1656
|
-
return o.onApproval?.(data);
|
|
1657
|
-
case "wf:usage":
|
|
1658
|
-
return o.onUsage?.(data);
|
|
1659
|
-
case "wf:done":
|
|
1660
|
-
return o.onDone?.(data);
|
|
1661
|
-
case "wf:error":
|
|
1662
|
-
return o.onError?.(data);
|
|
1663
|
-
default:
|
|
1664
|
-
return o.onEvent?.(name, data);
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
async function* parseWfEvents(stream) {
|
|
1668
|
-
const reader = stream.getReader();
|
|
1669
|
-
const decoder = new TextDecoder();
|
|
1670
|
-
let buffer = "";
|
|
1671
|
-
try {
|
|
1672
|
-
while (true) {
|
|
1673
|
-
const { done, value } = await reader.read();
|
|
1674
|
-
if (done) break;
|
|
1675
|
-
buffer += decoder.decode(value, { stream: true });
|
|
1676
|
-
const blocks = buffer.split("\n\n");
|
|
1677
|
-
buffer = blocks.pop() ?? "";
|
|
1678
|
-
for (const block of blocks) {
|
|
1679
|
-
let name = "message";
|
|
1680
|
-
let data = "";
|
|
1681
|
-
for (const line of block.split("\n")) {
|
|
1682
|
-
if (line.startsWith("event: ")) name = line.slice(7);
|
|
1683
|
-
else if (line.startsWith("data: ")) data += line.slice(6);
|
|
1684
|
-
}
|
|
1685
|
-
if (!data) continue;
|
|
1686
|
-
try {
|
|
1687
|
-
yield { name, data: JSON.parse(data) };
|
|
1688
|
-
} catch {
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
} finally {
|
|
1693
|
-
reader.releaseLock();
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
function pushEvent(events, e, record) {
|
|
1697
|
-
if (!record) return;
|
|
1698
|
-
if (events.length >= RECORD_LIMIT) events.shift();
|
|
1699
|
-
events.push(e);
|
|
1700
|
-
}
|
|
1701
|
-
function httpErrorCode(status) {
|
|
1702
|
-
if (status === 401 || status === 403) return "auth_failed";
|
|
1703
|
-
if (status === 429) return "rate_limited";
|
|
1704
|
-
if (status >= 500) return "provider_error";
|
|
1705
|
-
return "invalid_request";
|
|
1706
|
-
}
|
|
1707
|
-
function randomId() {
|
|
1708
|
-
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
1709
|
-
return crypto.randomUUID();
|
|
1710
|
-
}
|
|
1711
|
-
return `trace-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
1946
|
// src/client/error-boundary.ts
|
|
1715
1947
|
function ErrorBoundary(props, ctx) {
|
|
1716
1948
|
let error = null;
|
|
@@ -1987,6 +2219,7 @@ export {
|
|
|
1987
2219
|
jsxs,
|
|
1988
2220
|
lockScroll,
|
|
1989
2221
|
router,
|
|
2222
|
+
toChatMessages,
|
|
1990
2223
|
trapFocus,
|
|
1991
2224
|
unlockScroll,
|
|
1992
2225
|
ws,
|
package/dist/client/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* weifuwu/client 类型定义
|
|
3
3
|
*/
|
|
4
|
+
import type { UseChatHandle, UseChatOptions } from './use-chat.ts';
|
|
4
5
|
/** 弹层位置跟踪配置 — 供 ctx.ui.usePopupPosition 使用 */
|
|
5
6
|
export interface PopupPositionOptions {
|
|
6
7
|
/** 锚定元素 getter(通常是 ref 保存的触发元素) */
|
|
@@ -33,6 +34,16 @@ export interface WfuiContext {
|
|
|
33
34
|
dirty: (ids?: string[]) => void;
|
|
34
35
|
/** 创建响应式状态容器:$.x = val 自动触发 dirty() */
|
|
35
36
|
$: () => Record<string, any>;
|
|
37
|
+
/**
|
|
38
|
+
* AI 对话会话:$ 超集(会话语义 + 工具调用内嵌 + HITL 审批)
|
|
39
|
+
*
|
|
40
|
+
* ```tsx
|
|
41
|
+
* const $ = ctx.ui.useChat({ url: '/api/chat', approveUrl: '/api/approve' })
|
|
42
|
+
* // $.messages / $.input / $.streaming / $.error / $.usage / $.step
|
|
43
|
+
* // $.send() / $.stop() / $.retry() / $.clear() / $.approve(decision, note?)
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
useChat: (options: UseChatOptions) => UseChatHandle;
|
|
36
47
|
/** 弹层位置跟踪:滚动/resize 时自动重算 fixed 坐标 */
|
|
37
48
|
usePopupPosition: (options: PopupPositionOptions) => PopupPosition;
|
|
38
49
|
/** 注册组件实例的自定义语义 ID,同名冲突抛错 */
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ctx.ui.useChat — AI 会话语义层(client)
|
|
3
|
+
*
|
|
4
|
+
* 在 aiStream(传输解码)之上加一层「一段对话」的语义:
|
|
5
|
+
* 消息累积、工具调用内嵌、HITL 审批、错误恢复、stop/retry/clear。
|
|
6
|
+
*
|
|
7
|
+
* - 协议透明:消费 wf: 事件(docs/ai-contract.md),页面不需要知道事件名
|
|
8
|
+
* - 状态即 $:useChat 返回组件同一个响应式 Proxy,赋值自动渲染
|
|
9
|
+
* - 就地累积:token 经 state.messages[idx] 代理就地 append(O(1)/token,
|
|
10
|
+
* 不重建数组;配合 key 稳定引用 → VDOM 只 patch 文本节点)
|
|
11
|
+
* - 工具内嵌:wf:tool_call/progress/result 按 toolCallId/id 聚合到
|
|
12
|
+
* 消息的 toolCalls[],ToolCallCard 直接消费(call/progress/result)
|
|
13
|
+
* - 审批:approval 挂消息,approve() POST approveUrl(协议 §4.5)
|
|
14
|
+
* - 生命周期:dispose() 中止流(组件 ref cleanup 调用;卸载防泄漏)
|
|
15
|
+
*
|
|
16
|
+
* 核心与框架无关:transport 注入(默认 aiStream),node 直接测。
|
|
17
|
+
*/
|
|
18
|
+
import type { AiStreamCallbacks, AiStreamHandle } from './ai.ts';
|
|
19
|
+
import type { ChatMessage, WfApprovalDecision, WfApprovalRequest, WfError, WfStep, WfToolCall, WfToolProgress, WfToolResult, WfUsage } from '../ai/types.ts';
|
|
20
|
+
/** 工具调用显示条目:ToolCallCard 的三个 props 聚合 + 状态机 */
|
|
21
|
+
export interface UiToolCall {
|
|
22
|
+
call: WfToolCall;
|
|
23
|
+
progress?: WfToolProgress;
|
|
24
|
+
result?: WfToolResult;
|
|
25
|
+
status: 'running' | 'ok' | 'error';
|
|
26
|
+
}
|
|
27
|
+
/** 对话消息(显示模型):provider 形状 + UI 状态字段 */
|
|
28
|
+
export interface UiMessage {
|
|
29
|
+
id: string;
|
|
30
|
+
role: 'user' | 'assistant';
|
|
31
|
+
content: string;
|
|
32
|
+
status: 'streaming' | 'done' | 'error';
|
|
33
|
+
usage?: WfUsage;
|
|
34
|
+
toolCalls?: UiToolCall[];
|
|
35
|
+
approval?: WfApprovalRequest;
|
|
36
|
+
error?: WfError;
|
|
37
|
+
}
|
|
38
|
+
/** useChat 会话状态(挂在 $ 上,赋值自动渲染) */
|
|
39
|
+
export interface UseChatState {
|
|
40
|
+
messages: UiMessage[];
|
|
41
|
+
input: string;
|
|
42
|
+
streaming: boolean;
|
|
43
|
+
error: WfError | null;
|
|
44
|
+
usage: WfUsage | null;
|
|
45
|
+
/** 最近 wf:step(思考/工具指示),done/error 时清空 */
|
|
46
|
+
step: WfStep | null;
|
|
47
|
+
/** 页面自有状态与 chat 状态共处一个 $(与 ctx.ui.$() 一致) */
|
|
48
|
+
[key: string]: any;
|
|
49
|
+
}
|
|
50
|
+
/** useChat 操作(挂到 $ 上的方法;调用不触发渲染,内部 set 触发) */
|
|
51
|
+
export interface ChatApi {
|
|
52
|
+
/** 发送当前输入:追加 user + assistant 占位 → POST url */
|
|
53
|
+
send: () => void;
|
|
54
|
+
/** 中止当前流;空占位移除,有内容则标记 done */
|
|
55
|
+
stop: () => void;
|
|
56
|
+
/** 截断到最后一条 user 消息并重新生成其回复(错误恢复) */
|
|
57
|
+
retry: () => void;
|
|
58
|
+
/** 清空会话并中止 */
|
|
59
|
+
clear: () => void;
|
|
60
|
+
/** 响应 HITL 审批(协议 §4.5):清卡片 + POST approveUrl */
|
|
61
|
+
approve: (decision: WfApprovalDecision, note?: string) => Promise<void>;
|
|
62
|
+
/** 中止流并释放(组件卸载时调用) */
|
|
63
|
+
dispose: () => void;
|
|
64
|
+
/** 内部:订阅会话状态变更(AiChat 等共享 $ 的子组件用;返回退订)。
|
|
65
|
+
* 父组件 dirty 只驱动自身重渲染,共享 handle 的子组件需自行订阅。 */
|
|
66
|
+
__watch?: (cb: () => void) => () => void;
|
|
67
|
+
}
|
|
68
|
+
export type UseChatHandle = UseChatState & ChatApi;
|
|
69
|
+
export interface UseChatOptions {
|
|
70
|
+
/** POST 端点(返回 wf: SSE 流) */
|
|
71
|
+
url: string;
|
|
72
|
+
/** HITL 审批上行端点(协议 §4.5);缺省时 approve() 只清卡片不请求 */
|
|
73
|
+
approveUrl?: string;
|
|
74
|
+
/** 历史会话种子(app 数据注入;hook 不持有持久化) */
|
|
75
|
+
initialMessages?: UiMessage[];
|
|
76
|
+
/** 定制请求体(agent 模式携带 tools/mode 等);缺省 { messages } */
|
|
77
|
+
body?: (messages: UiMessage[]) => unknown;
|
|
78
|
+
headers?: Record<string, string>;
|
|
79
|
+
signal?: AbortSignal;
|
|
80
|
+
/** x:* 自定义事件透传(协议 §6) */
|
|
81
|
+
onEvent?: (name: string, data: unknown) => void;
|
|
82
|
+
}
|
|
83
|
+
/** 传输层(默认 aiStream):POST + SSE 解析 + abort */
|
|
84
|
+
export type ChatTransport = (url: string, body: unknown, callbacks: AiStreamCallbacks, opts?: {
|
|
85
|
+
signal?: AbortSignal;
|
|
86
|
+
headers?: Record<string, string>;
|
|
87
|
+
}) => AiStreamHandle;
|
|
88
|
+
/** UiMessage[] → provider ChatMessage[](剥离 UI 字段) */
|
|
89
|
+
export declare function toChatMessages(msgs: UiMessage[]): ChatMessage[];
|
|
90
|
+
export declare function createChatSession(state: UseChatState, transport: ChatTransport, options: UseChatOptions): ChatApi;
|
package/dist/client/vnode.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* --jsxImportSource=weifuwu/client
|
|
8
8
|
*/
|
|
9
9
|
import type { WfuiContext } from './types.ts';
|
|
10
|
-
export type VNodeType = string | Component | AsyncComponent | typeof Fragment | typeof Portal;
|
|
10
|
+
export type VNodeType = string | Component<any, any> | AsyncComponent | typeof Fragment | typeof Portal;
|
|
11
11
|
export interface VNode {
|
|
12
12
|
type: VNodeType;
|
|
13
13
|
props: Record<string, any>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AiChat — 标准 AI 对话组件(useChat 的标准展示层)
|
|
3
|
+
*
|
|
4
|
+
* 组件库第一个领域复合组件:接收 ctx.ui.useChat() 返回的 handle,
|
|
5
|
+
* 渲染完整对话界面:消息气泡、工具调用卡(ToolCallCard)、HITL 审批卡
|
|
6
|
+
* (ApprovalCard)、思考/工具状态、usage、错误 + 重试、输入条 + 自动滚动。
|
|
7
|
+
*
|
|
8
|
+
* 分工:
|
|
9
|
+
* - useChat(hook)= 会话语义 + 协议(数据层)
|
|
10
|
+
* - AiChat(组件)= 标准界面(展示层,5 分钟出可交互对话页)
|
|
11
|
+
* - 需要完全自定义 UI 的应用直接用 useChat + 自有渲染(hook 保持灵活路径)
|
|
12
|
+
*
|
|
13
|
+
* 约定(手动优先):内部无 $,let + 事件;自动滚动经 ref + scroll 事件。
|
|
14
|
+
* 文案默认中文(与 ToolCallCard/ApprovalCard 一致),labels 可覆盖。
|
|
15
|
+
*
|
|
16
|
+
* ```tsx
|
|
17
|
+
* const $ = ctx.ui.useChat({ url: '/api/chat', approveUrl: '/api/approve' })
|
|
18
|
+
* return () => h(AiChat, { chat: $ })
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
import type { Component } from '../../client/vnode.ts';
|
|
22
|
+
import type { UseChatHandle, UiMessage } from '../../client/use-chat.ts';
|
|
23
|
+
import type { WfError, WfUsage } from '../../ai/types.ts';
|
|
24
|
+
export interface AiChatLabels {
|
|
25
|
+
send: string;
|
|
26
|
+
stop: string;
|
|
27
|
+
retry: string;
|
|
28
|
+
thinking: string;
|
|
29
|
+
runningTool: (name?: string) => string;
|
|
30
|
+
tokens: (u: WfUsage) => string;
|
|
31
|
+
error: (e: WfError) => string;
|
|
32
|
+
placeholder: string;
|
|
33
|
+
empty: string;
|
|
34
|
+
}
|
|
35
|
+
export interface AiChatProps {
|
|
36
|
+
/** ctx.ui.useChat() 返回的会话 handle(同一 $,状态变化自动重渲染) */
|
|
37
|
+
chat: UseChatHandle;
|
|
38
|
+
/** 消息列表最大高度(默认 '70vh') */
|
|
39
|
+
maxHeight?: string;
|
|
40
|
+
/** 界面文案覆盖 */
|
|
41
|
+
labels?: Partial<AiChatLabels>;
|
|
42
|
+
/** 自定义气泡渲染逃生舱(默认纯文本) */
|
|
43
|
+
renderMessage?: (msg: UiMessage) => any;
|
|
44
|
+
/** 工具参数渲染(透传 ToolCallCard) */
|
|
45
|
+
renderToolArgs?: (args: Record<string, unknown>) => any;
|
|
46
|
+
}
|
|
47
|
+
export declare const AiChat: Component<AiChatProps>;
|
|
@@ -98,3 +98,5 @@ export { ToolCallCard } from './ToolCallCard/ToolCallCard.ts';
|
|
|
98
98
|
export type { ToolCallCardProps } from './ToolCallCard/ToolCallCard.ts';
|
|
99
99
|
export { ApprovalCard } from './ApprovalCard/ApprovalCard.ts';
|
|
100
100
|
export type { ApprovalCardProps, ApprovalStatus } from './ApprovalCard/ApprovalCard.ts';
|
|
101
|
+
export { AiChat } from './AiChat/AiChat.ts';
|
|
102
|
+
export type { AiChatProps, AiChatLabels } from './AiChat/AiChat.ts';
|
package/dist/components/index.js
CHANGED
|
@@ -987,6 +987,9 @@ var Tabs = (_init, _ctx) => (props) => {
|
|
|
987
987
|
// src/components/Dropdown/Dropdown.ts
|
|
988
988
|
var Dropdown = (_init, ctx) => {
|
|
989
989
|
let wrapEl = null;
|
|
990
|
+
const wrapRef = (el) => {
|
|
991
|
+
wrapEl = el;
|
|
992
|
+
};
|
|
990
993
|
let latestOpen = false;
|
|
991
994
|
let prevOpen = false;
|
|
992
995
|
const pos = ctx.ui.usePopupPosition({
|
|
@@ -1016,9 +1019,7 @@ var Dropdown = (_init, ctx) => {
|
|
|
1016
1019
|
const portalContent = open ? createPortal(menu, "dropdown") : null;
|
|
1017
1020
|
return h("div", {
|
|
1018
1021
|
class: `wf-dropdown${open ? " wf-dropdown--open" : ""}`,
|
|
1019
|
-
ref:
|
|
1020
|
-
wrapEl = el;
|
|
1021
|
-
}
|
|
1022
|
+
ref: wrapRef
|
|
1022
1023
|
}, [trigger, portalContent].filter(Boolean));
|
|
1023
1024
|
};
|
|
1024
1025
|
};
|
|
@@ -1402,6 +1403,10 @@ function formatSize(bytes) {
|
|
|
1402
1403
|
}
|
|
1403
1404
|
var FileUpload = (_init, ctx) => {
|
|
1404
1405
|
const FL = ctx?.i18n?.components?.FileUpload ?? {};
|
|
1406
|
+
let fileInput = null;
|
|
1407
|
+
const fileInputRef = (el) => {
|
|
1408
|
+
if (el) fileInput = el;
|
|
1409
|
+
};
|
|
1405
1410
|
return (props) => {
|
|
1406
1411
|
const { accept, multiple, maxSize, disabled, error, hint, value, onChange, children } = props;
|
|
1407
1412
|
const files = value ?? [];
|
|
@@ -1446,7 +1451,6 @@ var FileUpload = (_init, ctx) => {
|
|
|
1446
1451
|
const updated = files.filter((_, idx) => idx !== i);
|
|
1447
1452
|
onChange?.(updated);
|
|
1448
1453
|
};
|
|
1449
|
-
let fileInput = null;
|
|
1450
1454
|
const inputEl = h("input", {
|
|
1451
1455
|
type: "file",
|
|
1452
1456
|
class: "wf-upload-input",
|
|
@@ -1455,9 +1459,7 @@ var FileUpload = (_init, ctx) => {
|
|
|
1455
1459
|
disabled: disabled || void 0,
|
|
1456
1460
|
onChange: handleChange,
|
|
1457
1461
|
style: { display: "none" },
|
|
1458
|
-
ref:
|
|
1459
|
-
if (el) fileInput = el;
|
|
1460
|
-
}
|
|
1462
|
+
ref: fileInputRef
|
|
1461
1463
|
});
|
|
1462
1464
|
const dropZone = h("div", {
|
|
1463
1465
|
class: `wf-upload-zone${disabled ? " wf-upload-zone--disabled" : ""}${error ? " wf-upload-zone--err" : ""}`,
|
|
@@ -1525,6 +1527,9 @@ function computeFixedPosRect(rect, placement = "bottom", gap = 6, center = true)
|
|
|
1525
1527
|
var Tooltip = (_props, ctx) => {
|
|
1526
1528
|
let show = false;
|
|
1527
1529
|
let wrapEl = null;
|
|
1530
|
+
const wrapRef = (el) => {
|
|
1531
|
+
if (el) wrapEl = el;
|
|
1532
|
+
};
|
|
1528
1533
|
let latestPosition = "top";
|
|
1529
1534
|
let prevOpen = false;
|
|
1530
1535
|
const pos = ctx.ui.usePopupPosition({
|
|
@@ -1554,9 +1559,7 @@ var Tooltip = (_props, ctx) => {
|
|
|
1554
1559
|
const portalContent = !disabled ? createPortal(tip, "tooltip") : null;
|
|
1555
1560
|
return h("div", {
|
|
1556
1561
|
class: "wf-tooltip-wrap",
|
|
1557
|
-
ref:
|
|
1558
|
-
if (el) wrapEl = el;
|
|
1559
|
-
},
|
|
1562
|
+
ref: wrapRef,
|
|
1560
1563
|
onMouseEnter: showe,
|
|
1561
1564
|
onMouseLeave: hide,
|
|
1562
1565
|
onFocus: showe,
|
|
@@ -1622,6 +1625,9 @@ var Drawer = (_props, ctx) => {
|
|
|
1622
1625
|
var Popover = (_props, ctx) => {
|
|
1623
1626
|
let show = false;
|
|
1624
1627
|
let wrapEl = null;
|
|
1628
|
+
const wrapRef = (el) => {
|
|
1629
|
+
if (el) wrapEl = el;
|
|
1630
|
+
};
|
|
1625
1631
|
let latestOpen = false;
|
|
1626
1632
|
let latestPosition = "bottom";
|
|
1627
1633
|
let prevOpen = false;
|
|
@@ -1671,9 +1677,7 @@ var Popover = (_props, ctx) => {
|
|
|
1671
1677
|
const portalContent = isOpen ? createPortal([overlay, popover].filter(Boolean), "popover") : null;
|
|
1672
1678
|
return h("div", {
|
|
1673
1679
|
class: `wf-popover-wrap${isOpen ? " wf-popover-wrap--open" : ""}`,
|
|
1674
|
-
ref:
|
|
1675
|
-
if (el) wrapEl = el;
|
|
1676
|
-
},
|
|
1680
|
+
ref: wrapRef,
|
|
1677
1681
|
...hoverProps,
|
|
1678
1682
|
onClick
|
|
1679
1683
|
}, [children, portalContent].filter(Boolean));
|
|
@@ -1869,6 +1873,9 @@ var DatePicker = (_props, ctx) => {
|
|
|
1869
1873
|
let rangeStart = null;
|
|
1870
1874
|
let rangeEnd = null;
|
|
1871
1875
|
let inputEl = null;
|
|
1876
|
+
const inputRef = (el) => {
|
|
1877
|
+
inputEl = el;
|
|
1878
|
+
};
|
|
1872
1879
|
let prevOpen = false;
|
|
1873
1880
|
const pos = ctx.ui.usePopupPosition({
|
|
1874
1881
|
el: () => inputEl,
|
|
@@ -2131,9 +2138,7 @@ var DatePicker = (_props, ctx) => {
|
|
|
2131
2138
|
type: "text",
|
|
2132
2139
|
placeholder,
|
|
2133
2140
|
value: displayValue || "",
|
|
2134
|
-
ref:
|
|
2135
|
-
inputEl = el;
|
|
2136
|
-
},
|
|
2141
|
+
ref: inputRef,
|
|
2137
2142
|
readonly: true,
|
|
2138
2143
|
disabled,
|
|
2139
2144
|
onClick: toggle
|
|
@@ -2729,6 +2734,9 @@ var Editor = (_props, ctx) => {
|
|
|
2729
2734
|
let tableHoverCol = -1;
|
|
2730
2735
|
let sourceText = "";
|
|
2731
2736
|
let editorEl = null;
|
|
2737
|
+
const editorRef = (el) => {
|
|
2738
|
+
if (el) editorEl = el;
|
|
2739
|
+
};
|
|
2732
2740
|
let savedRange = null;
|
|
2733
2741
|
const saveSelection = () => {
|
|
2734
2742
|
const sel = window.getSelection();
|
|
@@ -3032,9 +3040,7 @@ var Editor = (_props, ctx) => {
|
|
|
3032
3040
|
onKeyUp: handleKeyUp,
|
|
3033
3041
|
onMouseUp: handleMouseUp,
|
|
3034
3042
|
onMouseDown: handleMouseDown,
|
|
3035
|
-
ref:
|
|
3036
|
-
if (el) editorEl = el;
|
|
3037
|
-
}
|
|
3043
|
+
ref: editorRef
|
|
3038
3044
|
});
|
|
3039
3045
|
} else {
|
|
3040
3046
|
editorBody = h("textarea", {
|
|
@@ -3205,8 +3211,130 @@ var ApprovalCard = (_init, ctx) => {
|
|
|
3205
3211
|
]);
|
|
3206
3212
|
};
|
|
3207
3213
|
};
|
|
3214
|
+
|
|
3215
|
+
// src/components/AiChat/AiChat.ts
|
|
3216
|
+
var defaultLabels = {
|
|
3217
|
+
send: "\u53D1\u9001",
|
|
3218
|
+
stop: "\u505C\u6B62",
|
|
3219
|
+
retry: "\u91CD\u8BD5",
|
|
3220
|
+
thinking: "\u{1F914} \u601D\u8003\u4E2D\u2026",
|
|
3221
|
+
runningTool: (name) => name ? `\u2699\uFE0F \u6267\u884C\u5DE5\u5177 ${name}` : "\u2699\uFE0F \u6267\u884C\u5DE5\u5177\u2026",
|
|
3222
|
+
tokens: (u) => `tokens: ${u.prompt_tokens}\u2192${u.completion_tokens}`,
|
|
3223
|
+
error: (e) => `${e.code}: ${e.message}`,
|
|
3224
|
+
placeholder: "\u8F93\u5165\u6D88\u606F\uFF0C\u56DE\u8F66\u53D1\u9001\u2026",
|
|
3225
|
+
empty: "\u8F93\u5165\u6D88\u606F\u5F00\u59CB\u5BF9\u8BDD\u3002"
|
|
3226
|
+
};
|
|
3227
|
+
var AiChat = (initProps, ctx) => {
|
|
3228
|
+
let listEl;
|
|
3229
|
+
let stickToBottom = true;
|
|
3230
|
+
const unwatch = initProps.chat.__watch?.(() => ctx.ui.dirty());
|
|
3231
|
+
const onScroll = () => {
|
|
3232
|
+
if (!listEl) return;
|
|
3233
|
+
stickToBottom = listEl.scrollHeight - listEl.scrollTop - listEl.clientHeight < 48;
|
|
3234
|
+
};
|
|
3235
|
+
const scrollToBottom = () => {
|
|
3236
|
+
if (listEl) listEl.scrollTop = listEl.scrollHeight;
|
|
3237
|
+
};
|
|
3238
|
+
const listRef = (el) => {
|
|
3239
|
+
if (el && !listEl) {
|
|
3240
|
+
listEl = el;
|
|
3241
|
+
el.addEventListener("scroll", onScroll);
|
|
3242
|
+
queueMicrotask(scrollToBottom);
|
|
3243
|
+
} else if (!el && listEl) {
|
|
3244
|
+
listEl.removeEventListener("scroll", onScroll);
|
|
3245
|
+
listEl = void 0;
|
|
3246
|
+
unwatch?.();
|
|
3247
|
+
}
|
|
3248
|
+
};
|
|
3249
|
+
return (props) => {
|
|
3250
|
+
const { chat } = props;
|
|
3251
|
+
const labels = { ...defaultLabels, ...props.labels };
|
|
3252
|
+
if (stickToBottom) queueMicrotask(scrollToBottom);
|
|
3253
|
+
const msgs = chat.messages ?? [];
|
|
3254
|
+
const nodes = [];
|
|
3255
|
+
if (msgs.length === 0) {
|
|
3256
|
+
nodes.push(h("p", { class: "wf-aichat-empty" }, labels.empty));
|
|
3257
|
+
}
|
|
3258
|
+
for (const m of msgs) {
|
|
3259
|
+
nodes.push(renderMessage(m, props, labels));
|
|
3260
|
+
}
|
|
3261
|
+
if (chat.step) {
|
|
3262
|
+
nodes.push(
|
|
3263
|
+
h(
|
|
3264
|
+
"div",
|
|
3265
|
+
{ class: "wf-aichat-status" },
|
|
3266
|
+
chat.step.type === "llm" ? labels.thinking : labels.runningTool(chat.step.name)
|
|
3267
|
+
)
|
|
3268
|
+
);
|
|
3269
|
+
}
|
|
3270
|
+
if (chat.usage) nodes.push(h("div", { class: "wf-aichat-usage" }, labels.tokens(chat.usage)));
|
|
3271
|
+
if (chat.error) nodes.push(h("div", { class: "wf-aichat-error" }, labels.error(chat.error)));
|
|
3272
|
+
return h("div", { class: "wf-aichat" }, [
|
|
3273
|
+
h("div", {
|
|
3274
|
+
class: "wf-aichat-list",
|
|
3275
|
+
style: { maxHeight: props.maxHeight ?? "70vh" },
|
|
3276
|
+
ref: listRef
|
|
3277
|
+
}, nodes),
|
|
3278
|
+
h("div", { class: "wf-aichat-inputbar" }, [
|
|
3279
|
+
h("input", {
|
|
3280
|
+
class: "wf-aichat-input",
|
|
3281
|
+
value: chat.input ?? "",
|
|
3282
|
+
placeholder: labels.placeholder,
|
|
3283
|
+
onInput: (e) => {
|
|
3284
|
+
chat.input = e.target.value;
|
|
3285
|
+
},
|
|
3286
|
+
onKeyDown: (e) => {
|
|
3287
|
+
if (e.key === "Enter") chat.send();
|
|
3288
|
+
}
|
|
3289
|
+
}),
|
|
3290
|
+
chat.streaming ? h("button", {
|
|
3291
|
+
class: "wf-btn wf-btn--primary wf-btn--sm",
|
|
3292
|
+
type: "button",
|
|
3293
|
+
onClick: () => chat.stop()
|
|
3294
|
+
}, labels.stop) : h("button", {
|
|
3295
|
+
class: "wf-btn wf-btn--primary wf-btn--sm",
|
|
3296
|
+
type: "button",
|
|
3297
|
+
onClick: () => chat.send()
|
|
3298
|
+
}, labels.send),
|
|
3299
|
+
!chat.streaming && chat.error ? h("button", {
|
|
3300
|
+
class: "wf-btn wf-btn--danger wf-btn--sm",
|
|
3301
|
+
type: "button",
|
|
3302
|
+
onClick: () => chat.retry()
|
|
3303
|
+
}, labels.retry) : null
|
|
3304
|
+
])
|
|
3305
|
+
]);
|
|
3306
|
+
};
|
|
3307
|
+
};
|
|
3308
|
+
function renderMessage(m, props, _labels) {
|
|
3309
|
+
const nodes = [];
|
|
3310
|
+
if (m.toolCalls?.length) {
|
|
3311
|
+
const cards = m.toolCalls.map((tc, i) => h(ToolCallCard, {
|
|
3312
|
+
key: `${m.id}-tool-${i}`,
|
|
3313
|
+
call: tc.call,
|
|
3314
|
+
progress: tc.progress,
|
|
3315
|
+
result: tc.result,
|
|
3316
|
+
renderArgs: props.renderToolArgs
|
|
3317
|
+
}));
|
|
3318
|
+
nodes.push(h("div", { class: "wf-aichat-tools" }, cards));
|
|
3319
|
+
}
|
|
3320
|
+
if (m.approval) {
|
|
3321
|
+
const card = h(ApprovalCard, {
|
|
3322
|
+
request: m.approval,
|
|
3323
|
+
onApprove: () => props.chat.approve("approved"),
|
|
3324
|
+
onReject: (note) => props.chat.approve("rejected", note ?? "\u7528\u6237\u62D2\u7EDD")
|
|
3325
|
+
});
|
|
3326
|
+
nodes.push(h("div", { class: "wf-aichat-approval" }, card));
|
|
3327
|
+
}
|
|
3328
|
+
nodes.push(h(
|
|
3329
|
+
"div",
|
|
3330
|
+
{ class: `wf-aichat-bubble wf-aichat-bubble--${m.role}` },
|
|
3331
|
+
props.renderMessage ? props.renderMessage(m) : m.content || "\u2026"
|
|
3332
|
+
));
|
|
3333
|
+
return h("div", { key: m.id, class: `wf-aichat-msg wf-aichat-msg--${m.role}` }, nodes);
|
|
3334
|
+
}
|
|
3208
3335
|
export {
|
|
3209
3336
|
Accordion,
|
|
3337
|
+
AiChat,
|
|
3210
3338
|
Alert,
|
|
3211
3339
|
ApprovalCard,
|
|
3212
3340
|
Avatar,
|
|
@@ -4445,4 +4445,115 @@ code {
|
|
|
4445
4445
|
color: var(--wf-color-text-secondary);
|
|
4446
4446
|
}
|
|
4447
4447
|
|
|
4448
|
+
/* weifuwu/components — AiChat(标准 AI 对话界面) */
|
|
4449
|
+
|
|
4450
|
+
.wf-aichat {
|
|
4451
|
+
display: flex;
|
|
4452
|
+
flex-direction: column;
|
|
4453
|
+
gap: var(--wf-gap-sm, 8px);
|
|
4454
|
+
font-family: var(--wf-font-sans);
|
|
4455
|
+
font-size: var(--wf-font-size-sm, 14px);
|
|
4456
|
+
line-height: var(--wf-line-height);
|
|
4457
|
+
color: var(--wf-color-text);
|
|
4458
|
+
}
|
|
4459
|
+
|
|
4460
|
+
.wf-aichat-list {
|
|
4461
|
+
overflow-y: auto;
|
|
4462
|
+
display: flex;
|
|
4463
|
+
flex-direction: column;
|
|
4464
|
+
gap: var(--wf-gap-sm, 8px);
|
|
4465
|
+
padding: var(--wf-space-sm, 8px);
|
|
4466
|
+
border: 1px solid var(--wf-color-border);
|
|
4467
|
+
border-radius: var(--wf-radius);
|
|
4468
|
+
background: var(--wf-color-bg);
|
|
4469
|
+
}
|
|
4470
|
+
|
|
4471
|
+
.wf-aichat-empty {
|
|
4472
|
+
margin: var(--wf-space-lg, 16px) 0;
|
|
4473
|
+
text-align: center;
|
|
4474
|
+
color: var(--wf-color-text-secondary);
|
|
4475
|
+
font-size: var(--wf-font-size-sm, 14px);
|
|
4476
|
+
}
|
|
4477
|
+
|
|
4478
|
+
.wf-aichat-msg {
|
|
4479
|
+
display: flex;
|
|
4480
|
+
flex-direction: column;
|
|
4481
|
+
gap: var(--wf-gap-xs, 4px);
|
|
4482
|
+
}
|
|
4483
|
+
|
|
4484
|
+
.wf-aichat-tools,
|
|
4485
|
+
.wf-aichat-approval {
|
|
4486
|
+
max-width: 100%;
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
.wf-aichat-bubble {
|
|
4490
|
+
display: inline-block;
|
|
4491
|
+
max-width: 85%;
|
|
4492
|
+
padding: var(--wf-space-sm, 8px) var(--wf-space-md, 12px);
|
|
4493
|
+
border-radius: var(--wf-radius);
|
|
4494
|
+
font-size: var(--wf-font-size-sm, 14px);
|
|
4495
|
+
white-space: pre-wrap;
|
|
4496
|
+
word-break: break-word;
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4499
|
+
.wf-aichat-msg--user {
|
|
4500
|
+
align-items: flex-end;
|
|
4501
|
+
}
|
|
4502
|
+
|
|
4503
|
+
.wf-aichat-msg--assistant {
|
|
4504
|
+
align-items: flex-start;
|
|
4505
|
+
}
|
|
4506
|
+
|
|
4507
|
+
.wf-aichat-bubble--user {
|
|
4508
|
+
background: var(--wf-color-primary);
|
|
4509
|
+
color: #fff;
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4512
|
+
.wf-aichat-bubble--assistant {
|
|
4513
|
+
background: var(--wf-color-bg-tertiary);
|
|
4514
|
+
color: var(--wf-color-text);
|
|
4515
|
+
}
|
|
4516
|
+
|
|
4517
|
+
.wf-aichat-status {
|
|
4518
|
+
font-size: var(--wf-font-size-xs, 12px);
|
|
4519
|
+
color: var(--wf-color-info);
|
|
4520
|
+
}
|
|
4521
|
+
|
|
4522
|
+
.wf-aichat-usage {
|
|
4523
|
+
font-size: var(--wf-font-size-xs, 12px);
|
|
4524
|
+
color: var(--wf-color-text-secondary);
|
|
4525
|
+
}
|
|
4526
|
+
|
|
4527
|
+
.wf-aichat-error {
|
|
4528
|
+
font-size: var(--wf-font-size-xs, 12px);
|
|
4529
|
+
color: var(--wf-color-error);
|
|
4530
|
+
background: var(--wf-color-error-bg);
|
|
4531
|
+
border-radius: var(--wf-radius);
|
|
4532
|
+
padding: var(--wf-space-sm, 8px);
|
|
4533
|
+
}
|
|
4534
|
+
|
|
4535
|
+
.wf-aichat-inputbar {
|
|
4536
|
+
display: flex;
|
|
4537
|
+
gap: var(--wf-gap-sm, 8px);
|
|
4538
|
+
align-items: center;
|
|
4539
|
+
}
|
|
4540
|
+
|
|
4541
|
+
.wf-aichat-input {
|
|
4542
|
+
flex: 1;
|
|
4543
|
+
min-width: 0;
|
|
4544
|
+
height: var(--wf-control-height, 36px);
|
|
4545
|
+
padding: var(--wf-control-pad-y, 6px) var(--wf-space-md, 12px);
|
|
4546
|
+
border: 1px solid var(--wf-color-border);
|
|
4547
|
+
border-radius: var(--wf-radius);
|
|
4548
|
+
font-family: var(--wf-font-sans);
|
|
4549
|
+
font-size: var(--wf-font-size-sm, 14px);
|
|
4550
|
+
color: var(--wf-color-text);
|
|
4551
|
+
background: var(--wf-color-bg);
|
|
4552
|
+
}
|
|
4553
|
+
|
|
4554
|
+
.wf-aichat-input:focus {
|
|
4555
|
+
outline: none;
|
|
4556
|
+
border-color: var(--wf-color-primary);
|
|
4557
|
+
}
|
|
4558
|
+
|
|
4448
4559
|
}
|
package/dist/index.js
CHANGED
|
@@ -1129,7 +1129,7 @@ function rateLimit(options) {
|
|
|
1129
1129
|
if (opts.algorithm === "fixed") {
|
|
1130
1130
|
const count2 = await redisPool.incr(PREFIX + key);
|
|
1131
1131
|
if (count2 === 1) {
|
|
1132
|
-
await redisPool.
|
|
1132
|
+
await redisPool.command("PEXPIRE", PREFIX + key, windowMs);
|
|
1133
1133
|
}
|
|
1134
1134
|
return {
|
|
1135
1135
|
allowed: count2 <= max,
|
|
@@ -2401,6 +2401,7 @@ function queue(options) {
|
|
|
2401
2401
|
const consumer = opts?.consumer ?? `${hostname()}-${process.pid}-${Math.random().toString(36).slice(2, 6)}`;
|
|
2402
2402
|
const concurrency = opts?.concurrency ?? 1;
|
|
2403
2403
|
const visibilityTimeout = opts?.visibilityTimeout ?? 3e4;
|
|
2404
|
+
const blockMs = opts?.blockMs ?? 1e3;
|
|
2404
2405
|
const s = stream(name);
|
|
2405
2406
|
const dead = deadStream(name);
|
|
2406
2407
|
const delayed = `${prefix}${name}:delayed`;
|
|
@@ -2520,7 +2521,7 @@ function queue(options) {
|
|
|
2520
2521
|
"COUNT",
|
|
2521
2522
|
String(concurrency),
|
|
2522
2523
|
"BLOCK",
|
|
2523
|
-
|
|
2524
|
+
String(blockMs),
|
|
2524
2525
|
"STREAMS",
|
|
2525
2526
|
s,
|
|
2526
2527
|
">"
|
|
@@ -2977,6 +2978,7 @@ var Portal = /* @__PURE__ */ Symbol("Portal");
|
|
|
2977
2978
|
// src/client/reactive.ts
|
|
2978
2979
|
function createReactiveState(dirty) {
|
|
2979
2980
|
const proxyCache = /* @__PURE__ */ new WeakMap();
|
|
2981
|
+
const watchers = /* @__PURE__ */ new Set();
|
|
2980
2982
|
const reactive = (target) => {
|
|
2981
2983
|
if (target === null || typeof target !== "object") return target;
|
|
2982
2984
|
if (proxyCache.has(target)) return proxyCache.get(target);
|
|
@@ -2986,6 +2988,7 @@ function createReactiveState(dirty) {
|
|
|
2986
2988
|
if (old === value) return true;
|
|
2987
2989
|
Reflect.set(target2, key, value);
|
|
2988
2990
|
dirty();
|
|
2991
|
+
for (const w of watchers) w();
|
|
2989
2992
|
return true;
|
|
2990
2993
|
},
|
|
2991
2994
|
get(target2, key) {
|
|
@@ -2997,6 +3000,7 @@ function createReactiveState(dirty) {
|
|
|
2997
3000
|
if (Reflect.has(target2, key)) {
|
|
2998
3001
|
Reflect.deleteProperty(target2, key);
|
|
2999
3002
|
dirty();
|
|
3003
|
+
for (const w of watchers) w();
|
|
3000
3004
|
}
|
|
3001
3005
|
return true;
|
|
3002
3006
|
}
|
|
@@ -3004,7 +3008,18 @@ function createReactiveState(dirty) {
|
|
|
3004
3008
|
proxyCache.set(target, proxy);
|
|
3005
3009
|
return proxy;
|
|
3006
3010
|
};
|
|
3007
|
-
|
|
3011
|
+
const root = reactive({});
|
|
3012
|
+
Object.defineProperty(root, "__watch", {
|
|
3013
|
+
value: (cb) => {
|
|
3014
|
+
watchers.add(cb);
|
|
3015
|
+
return () => {
|
|
3016
|
+
watchers.delete(cb);
|
|
3017
|
+
};
|
|
3018
|
+
},
|
|
3019
|
+
writable: false,
|
|
3020
|
+
enumerable: false
|
|
3021
|
+
});
|
|
3022
|
+
return root;
|
|
3008
3023
|
}
|
|
3009
3024
|
|
|
3010
3025
|
// src/ui/ssr.ts
|
|
@@ -3060,7 +3075,28 @@ function createSsrContext(serverCtx, dataStore) {
|
|
|
3060
3075
|
useBreakpoint: () => {
|
|
3061
3076
|
},
|
|
3062
3077
|
usePopupPosition: () => ({ top: 0, left: 0, refresh: () => {
|
|
3063
|
-
} })
|
|
3078
|
+
} }),
|
|
3079
|
+
// SSR 确定性空态:会话不启动(无事件/无网络),仅保证挂载不崩
|
|
3080
|
+
useChat: () => ({
|
|
3081
|
+
messages: [],
|
|
3082
|
+
input: "",
|
|
3083
|
+
streaming: false,
|
|
3084
|
+
error: null,
|
|
3085
|
+
usage: null,
|
|
3086
|
+
step: null,
|
|
3087
|
+
send: () => {
|
|
3088
|
+
},
|
|
3089
|
+
stop: () => {
|
|
3090
|
+
},
|
|
3091
|
+
retry: () => {
|
|
3092
|
+
},
|
|
3093
|
+
clear: () => {
|
|
3094
|
+
},
|
|
3095
|
+
approve: async () => {
|
|
3096
|
+
},
|
|
3097
|
+
dispose: () => {
|
|
3098
|
+
}
|
|
3099
|
+
})
|
|
3064
3100
|
};
|
|
3065
3101
|
const ctx = Object.create(serverCtx ?? {});
|
|
3066
3102
|
ctx.ui = ui2;
|
package/dist/queue/index.d.ts
CHANGED
|
@@ -57,6 +57,10 @@ export interface WorkerOptions {
|
|
|
57
57
|
visibilityTimeout?: number;
|
|
58
58
|
/** 消费组 consumer 名(多实例自动 hostname-pid,可覆盖) */
|
|
59
59
|
consumer?: string;
|
|
60
|
+
/** XREADGROUP 阻塞等待时长(ms)。默认 1000。
|
|
61
|
+
* 越小失败重投延迟越低(重投间隔 = max(visibilityTimeout, blockMs)),
|
|
62
|
+
* 测试用短值提速;生产保持默认。 */
|
|
63
|
+
blockMs?: number;
|
|
60
64
|
}
|
|
61
65
|
export interface QueueWorker {
|
|
62
66
|
/** 启动消费循环(阻塞直到 stop) */
|