weifuwu 0.57.0 → 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/dist/index.js CHANGED
@@ -2578,6 +2578,377 @@ function isPoolClosed(e) {
2578
2578
  return /pool is closed|connection closed/.test(msg);
2579
2579
  }
2580
2580
 
2581
+ // src/ai/client.ts
2582
+ import { randomUUID as randomUUID2 } from "node:crypto";
2583
+
2584
+ // src/ai/sse.ts
2585
+ function sseResponse(run, options) {
2586
+ const encoder = new TextEncoder();
2587
+ const stream = new ReadableStream({
2588
+ async start(controller) {
2589
+ const emit = (name, data) => {
2590
+ controller.enqueue(encoder.encode(`event: ${name}
2591
+ data: ${JSON.stringify(data)}
2592
+
2593
+ `));
2594
+ };
2595
+ try {
2596
+ await run(emit);
2597
+ controller.close();
2598
+ } catch (err) {
2599
+ try {
2600
+ emit("wf:error", {
2601
+ code: "provider_error",
2602
+ message: err instanceof Error ? err.message : String(err)
2603
+ });
2604
+ controller.close();
2605
+ } catch {
2606
+ controller.error(err);
2607
+ }
2608
+ }
2609
+ },
2610
+ cancel() {
2611
+ options?.onAbort?.();
2612
+ }
2613
+ });
2614
+ return new Response(stream, {
2615
+ headers: {
2616
+ "Content-Type": "text/event-stream",
2617
+ "Cache-Control": "no-cache",
2618
+ Connection: "keep-alive"
2619
+ }
2620
+ });
2621
+ }
2622
+
2623
+ // src/ai/client.ts
2624
+ var AiError = class extends Error {
2625
+ code;
2626
+ constructor(code, message) {
2627
+ super(message);
2628
+ this.name = "AiError";
2629
+ this.code = code;
2630
+ }
2631
+ };
2632
+ async function* parseProviderSse(stream) {
2633
+ const reader = stream.getReader();
2634
+ const decoder = new TextDecoder();
2635
+ let buffer = "";
2636
+ try {
2637
+ while (true) {
2638
+ const { done, value } = await reader.read();
2639
+ if (done) break;
2640
+ buffer += decoder.decode(value, { stream: true });
2641
+ const lines = buffer.split("\n");
2642
+ buffer = lines.pop() ?? "";
2643
+ for (const line of lines) {
2644
+ const trimmed = line.trim();
2645
+ if (!trimmed || trimmed.startsWith(":")) continue;
2646
+ const data = trimmed.startsWith("data: ") ? trimmed.slice(6) : trimmed;
2647
+ if (data === "[DONE]") return;
2648
+ try {
2649
+ yield JSON.parse(data);
2650
+ } catch {
2651
+ }
2652
+ }
2653
+ }
2654
+ } finally {
2655
+ reader.releaseLock();
2656
+ }
2657
+ }
2658
+ async function providerError(res) {
2659
+ let message = "";
2660
+ try {
2661
+ const body = await res.json();
2662
+ message = body?.error?.message ?? JSON.stringify(body);
2663
+ } catch {
2664
+ message = await res.text().catch(() => "");
2665
+ }
2666
+ const status = res.status;
2667
+ let code;
2668
+ if (status === 401 || status === 403) code = "auth_failed";
2669
+ else if (status === 429) code = "rate_limited";
2670
+ else if (status >= 500) code = "provider_error";
2671
+ else if (/context|token|length/i.test(message)) code = "context_length";
2672
+ else if (status === 400) code = "invalid_request";
2673
+ else code = "provider_error";
2674
+ return { code, message: message || `provider error (${status})` };
2675
+ }
2676
+ function createAiClient(opts) {
2677
+ const endpoint = `${opts.baseUrl.replace(/\/$/, "")}/chat/completions`;
2678
+ const headers = { "Content-Type": "application/json", Authorization: `Bearer ${opts.apiKey}` };
2679
+ const approvals = /* @__PURE__ */ new Map();
2680
+ function approve(response) {
2681
+ const resolve3 = approvals.get(response.id);
2682
+ if (!resolve3) return false;
2683
+ approvals.delete(response.id);
2684
+ resolve3(response);
2685
+ return true;
2686
+ }
2687
+ async function waitApproval(req, emit, timeoutMs = DEFAULT_APPROVAL_TIMEOUT) {
2688
+ const expiresAt = Date.now() + timeoutMs;
2689
+ emit("wf:approval_request", { ...req, expiresAt });
2690
+ return new Promise((resolve3) => {
2691
+ const timer = setTimeout(() => {
2692
+ if (approvals.has(req.id)) {
2693
+ approvals.delete(req.id);
2694
+ resolve3({ id: req.id, decision: "rejected" });
2695
+ }
2696
+ }, timeoutMs);
2697
+ approvals.set(req.id, (resp) => {
2698
+ clearTimeout(timer);
2699
+ resolve3(resp);
2700
+ });
2701
+ });
2702
+ }
2703
+ function aggregateToolCalls(chunks) {
2704
+ const calls = [];
2705
+ for (const chunk of chunks) {
2706
+ const delta = chunk.choices[0]?.delta;
2707
+ if (!delta?.tool_calls) continue;
2708
+ for (const tc of delta.tool_calls) {
2709
+ if (tc.id) {
2710
+ calls.push(tc);
2711
+ } else if (calls.length > 0) {
2712
+ const last = calls[calls.length - 1];
2713
+ if (tc.function?.arguments) last.function.arguments += tc.function.arguments;
2714
+ }
2715
+ }
2716
+ }
2717
+ return calls;
2718
+ }
2719
+ async function chat(params, options) {
2720
+ const res = await fetch(endpoint, {
2721
+ method: "POST",
2722
+ headers,
2723
+ body: JSON.stringify({ ...params, model: params.model ?? opts.defaultModel, stream: false }),
2724
+ signal: options?.signal
2725
+ });
2726
+ if (!res.ok) {
2727
+ const { code, message } = await providerError(res);
2728
+ throw new AiError(code, message);
2729
+ }
2730
+ return res.json();
2731
+ }
2732
+ function stream(params, options) {
2733
+ const controller = new AbortController();
2734
+ const external = options?.signal;
2735
+ if (external) {
2736
+ if (external.aborted) controller.abort();
2737
+ else external.addEventListener("abort", () => controller.abort(), { once: true });
2738
+ }
2739
+ return sseResponse(
2740
+ async (emit) => {
2741
+ emit("wf:message_start", { id: options?.traceId ?? randomUUID2() });
2742
+ await streamStep(params, {
2743
+ emit,
2744
+ signal: controller.signal,
2745
+ onFinish: (r) => emit("wf:done", { content: r.content, usage: r.usage })
2746
+ });
2747
+ },
2748
+ { onAbort: () => controller.abort() }
2749
+ );
2750
+ }
2751
+ async function streamStep(params, stepOpts) {
2752
+ const { emit, signal } = stepOpts;
2753
+ let res;
2754
+ try {
2755
+ res = await fetch(endpoint, {
2756
+ method: "POST",
2757
+ headers,
2758
+ body: JSON.stringify({ ...params, model: params.model ?? opts.defaultModel, stream: true }),
2759
+ signal
2760
+ });
2761
+ } catch (err) {
2762
+ if (signal?.aborted) return;
2763
+ emit("wf:error", { code: "provider_error", message: err instanceof Error ? err.message : String(err) });
2764
+ return;
2765
+ }
2766
+ if (!res.ok) {
2767
+ const { code, message } = await providerError(res);
2768
+ emit("wf:error", { code, message });
2769
+ return;
2770
+ }
2771
+ let content = "";
2772
+ let reasoning = "";
2773
+ const chunks = [];
2774
+ let usage;
2775
+ try {
2776
+ for await (const chunk of parseProviderSse(res.body)) {
2777
+ if (signal?.aborted) return;
2778
+ const delta = chunk.choices[0]?.delta;
2779
+ if (delta?.content) {
2780
+ content += delta.content;
2781
+ emit("wf:token", { text: delta.content });
2782
+ }
2783
+ if (delta?.reasoning_content) reasoning += delta.reasoning_content;
2784
+ if (delta?.tool_calls) chunks.push(chunk);
2785
+ if (chunk.usage) usage = chunk.usage;
2786
+ }
2787
+ } catch (err) {
2788
+ if (signal?.aborted) return;
2789
+ emit("wf:error", { code: "provider_error", message: err instanceof Error ? err.message : String(err) });
2790
+ return;
2791
+ }
2792
+ const toolCalls = aggregateToolCalls(chunks);
2793
+ for (const tc of toolCalls) {
2794
+ emit("wf:tool_call", {
2795
+ id: tc.id,
2796
+ name: tc.function?.name ?? "",
2797
+ args: safeParseArgs(tc.function?.arguments ?? "")
2798
+ });
2799
+ }
2800
+ if (usage && stepOpts.emitUsage !== false) emit("wf:usage", usage);
2801
+ stepOpts.onFinish?.({ content, reasoning_content: reasoning || void 0, toolCalls, usage });
2802
+ }
2803
+ function sse(run, options) {
2804
+ const controller = new AbortController();
2805
+ const external = options?.signal;
2806
+ if (external) {
2807
+ if (external.aborted) controller.abort();
2808
+ else external.addEventListener("abort", () => controller.abort(), { once: true });
2809
+ }
2810
+ return sseResponse(run, { onAbort: () => controller.abort() });
2811
+ }
2812
+ return { chat, stream, sse, streamStep, waitApproval, approve };
2813
+ }
2814
+ var DEFAULT_APPROVAL_TIMEOUT = 5 * 6e4;
2815
+ function safeParseArgs(raw) {
2816
+ if (!raw) return {};
2817
+ try {
2818
+ return JSON.parse(raw);
2819
+ } catch {
2820
+ return {};
2821
+ }
2822
+ }
2823
+
2824
+ // src/ai/agent.ts
2825
+ import { randomUUID as randomUUID3 } from "node:crypto";
2826
+ function createAgent(client, config) {
2827
+ const maxSteps = config.maxSteps ?? 10;
2828
+ const toolDefs = config.tools.map((t) => ({
2829
+ type: "function",
2830
+ function: { name: t.name, description: t.description ?? "", parameters: t.parameters ?? {} }
2831
+ }));
2832
+ function run(messages, options) {
2833
+ const controller = new AbortController();
2834
+ const external = options?.signal;
2835
+ if (external) {
2836
+ if (external.aborted) controller.abort();
2837
+ else external.addEventListener("abort", () => controller.abort(), { once: true });
2838
+ }
2839
+ return sseResponse(
2840
+ async (emit) => {
2841
+ emit("wf:message_start", { id: options?.traceId ?? randomUUID3() });
2842
+ await loop(messages, emit, controller.signal);
2843
+ },
2844
+ { onAbort: () => controller.abort() }
2845
+ );
2846
+ }
2847
+ async function loop(messages, emit, signal) {
2848
+ const all = [{ role: "system", content: config.systemPrompt }, ...messages];
2849
+ const usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
2850
+ for (let step = 0; step < maxSteps; step++) {
2851
+ if (signal?.aborted) return;
2852
+ emit("wf:step", { type: "llm" });
2853
+ let finish = { content: "", toolCalls: [] };
2854
+ await client.streamStep(
2855
+ { model: config.model, messages: all, tools: toolDefs },
2856
+ {
2857
+ emit,
2858
+ signal,
2859
+ emitUsage: false,
2860
+ // agent 只发累积 usage(done 前统一发)
2861
+ onFinish: (r) => {
2862
+ finish = r;
2863
+ if (r.usage) {
2864
+ usage.prompt_tokens += r.usage.prompt_tokens ?? 0;
2865
+ usage.completion_tokens += r.usage.completion_tokens ?? 0;
2866
+ usage.total_tokens += r.usage.total_tokens ?? 0;
2867
+ }
2868
+ }
2869
+ }
2870
+ );
2871
+ if (signal?.aborted) return;
2872
+ if (!finish.toolCalls?.length) {
2873
+ emit("wf:usage", usage);
2874
+ emit("wf:done", { content: finish.content, usage });
2875
+ return;
2876
+ }
2877
+ all.push({
2878
+ role: "assistant",
2879
+ content: finish.content ?? "",
2880
+ reasoning_content: finish.reasoning_content,
2881
+ tool_calls: finish.toolCalls
2882
+ });
2883
+ for (const tc of finish.toolCalls) {
2884
+ if (signal?.aborted) return;
2885
+ const name = tc.function?.name ?? "";
2886
+ const args = safeParseArgs(tc.function?.arguments ?? "");
2887
+ emit("wf:step", { type: "tool", toolCallId: tc.id, name });
2888
+ let execArgs = args;
2889
+ if (config.humanInTheLoop) {
2890
+ const decision = await client.waitApproval(
2891
+ { id: randomUUID3(), toolCallId: tc.id, name, args },
2892
+ emit,
2893
+ config.approvalTimeoutMs
2894
+ );
2895
+ if (decision.decision === "rejected") {
2896
+ emit("wf:tool_result", { id: tc.id, ok: false, error: { code: "rejected", message: decision.note ?? "\u7528\u6237\u62D2\u7EDD" } });
2897
+ all.push({ role: "tool", content: `Human rejected: ${decision.note ?? ""}`, tool_call_id: tc.id, name });
2898
+ continue;
2899
+ }
2900
+ if (decision.decision === "modified" && decision.modifiedArgs) execArgs = decision.modifiedArgs;
2901
+ }
2902
+ const tool = config.tools.find((t) => t.name === name);
2903
+ if (!tool) {
2904
+ emit("wf:tool_result", { id: tc.id, ok: false, error: { code: "tool_error", message: `\u5DE5\u5177\u4E0D\u5B58\u5728: ${name}` } });
2905
+ all.push({ role: "tool", content: `Error: tool not found: ${name}`, tool_call_id: tc.id, name });
2906
+ continue;
2907
+ }
2908
+ try {
2909
+ const output = await tool.run(execArgs, { emit, signal });
2910
+ emit("wf:tool_result", { id: tc.id, ok: true, output });
2911
+ all.push({ role: "tool", content: JSON.stringify(output ?? ""), tool_call_id: tc.id, name });
2912
+ } catch (err) {
2913
+ const msg = err instanceof Error ? err.message : String(err);
2914
+ emit("wf:tool_result", { id: tc.id, ok: false, error: { code: "tool_error", message: msg } });
2915
+ all.push({ role: "tool", content: `Error: ${msg}`, tool_call_id: tc.id, name });
2916
+ }
2917
+ }
2918
+ }
2919
+ emit("wf:usage", usage);
2920
+ emit("wf:done", { content: "", usage });
2921
+ }
2922
+ return { run };
2923
+ }
2924
+
2925
+ // src/ai/index.ts
2926
+ function ai(options) {
2927
+ const apiKey = options?.apiKey ?? process.env.DEEPSEEK_API_KEY ?? "";
2928
+ const baseUrl = options?.baseUrl ?? process.env.DEEPSEEK_BASE_URL ?? "https://api.deepseek.com/v1";
2929
+ const defaultModel = options?.defaultModel ?? process.env.DEEPSEEK_MODEL ?? "deepseek-v4-flash";
2930
+ if (!apiKey) {
2931
+ throw new Error("ai: DEEPSEEK_API_KEY \u672A\u8BBE\u7F6E\u3002\u8BF7\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF\u6216\u4F20\u5165 apiKey");
2932
+ }
2933
+ const client = createAiClient({ apiKey, baseUrl, defaultModel });
2934
+ const mw = (req, ctx, next) => {
2935
+ ctx.ai = module;
2936
+ return next(req, ctx);
2937
+ };
2938
+ mw.__meta = { injects: ["ai"], depends: [] };
2939
+ const module = mw;
2940
+ module.chat = client.chat;
2941
+ module.stream = client.stream;
2942
+ module.sse = client.sse;
2943
+ module.streamStep = client.streamStep;
2944
+ module.waitApproval = client.waitApproval;
2945
+ module.approve = client.approve;
2946
+ module.agent = (config) => createAgent(client, config);
2947
+ module.close = async () => {
2948
+ };
2949
+ return module;
2950
+ }
2951
+
2581
2952
  // src/ui/index.ts
2582
2953
  import { build } from "esbuild";
2583
2954
  import { readFile, stat } from "node:fs/promises";
@@ -4148,6 +4519,7 @@ export {
4148
4519
  HttpError,
4149
4520
  MIGRATIONS_TABLE,
4150
4521
  Router,
4522
+ ai,
4151
4523
  badRequest,
4152
4524
  conflict,
4153
4525
  cors,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weifuwu",
3
3
  "type": "module",
4
- "version": "0.57.0",
4
+ "version": "0.58.0",
5
5
  "description": "AI SaaS framework — (req, ctx) => Response",
6
6
  "exports": {
7
7
  ".": {