weifuwu 0.2.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,16 +2,23 @@
2
2
 
3
3
  **Web-standard HTTP framework for Node.js.** `(req, ctx) => Response` — no framework-specific objects, just the Web API your browser already speaks.
4
4
 
5
+ ### Design
6
+
7
+ weifuwu doesn't invent its own request/response abstraction. `Request` and `Response` are the same objects you use in `fetch()` — what you learn in the browser applies directly on the server. `ctx` is the only framework object, and it only carries what the router parsed for you (`params`, `query`).
8
+
9
+ Features like `tsx()`, WebSocket, GraphQL, and AI streaming all follow the same `(req, ctx) => Response` contract. There is no separate concept for "page route" vs "API route" — everything is a handler that returns a `Response`. `tsx()` just generates a `Response` from a React component the same way `router.get()` returns a `Response` from a handler function.
10
+
5
11
  ## Features
6
12
 
7
13
  - **Web Standard** — `Request` / `Response` / `ReadableStream`, zero abstractions
8
14
  - **Trie router** — static > param > wildcard, sub-router mounting, path params
9
15
  - **Middleware** — global, path-scoped, route-level — onion model, short-circuit
10
16
  - **Built-in middleware** — `auth()`, `cors()`, `logger()`, `rateLimit()`, `compress()`
11
- - **React SSR + Hydration** — `tsx({ dir })` — page.tsx / load.ts / layout.tsx / route.ts
17
+ - **React SSR + Hydration** — `tsx({ dir })` — page.tsx / load.ts / layout.tsx / route.ts / not-found.tsx
12
18
  - **WebSocket** — `router.ws()` with upgrade middleware (auth before connect)
13
19
  - **GraphQL** — `router.graphql()` with GraphiQL IDE
14
20
  - **AI streaming** — `router.ai()` via Vercel AI SDK
21
+ - **AI workflows** — `router.workflow()` — intent-to-execution pipelines with `tool()` + SSE
15
22
  - **Static files** — `serveStatic()` with ETag, 304, MIME, directory index
16
23
  - **Request validation** — `validate()` with Zod (body / query / params / headers)
17
24
  - **File upload** — `upload()` multipart parser with disk save, size & type limits
@@ -46,12 +53,15 @@ serve(app.handler(), { port: 3000 })
46
53
  pages/
47
54
  page.tsx → GET / (React component, default export)
48
55
  layout.tsx → root layout (HTML shell, receives req/ctx, NOT hydrated)
56
+ not-found.tsx → 404 error page (rendered for unmatched routes, wrapped in layout)
49
57
  about/page.tsx → GET /about
50
58
  blog/[slug]/
51
59
  page.tsx → GET /blog/:slug
52
60
  load.ts → data fetching (server-only, default export)
53
- route.ts → POST /blog/:slug (API, named exports GET/POST/...)
61
+ route.ts → POST /blog/:slug (API, named exports POST/PUT/DELETE/...)
54
62
  blog/layout.tsx → /blog/* layout (UI structure, receives children, hydrated)
63
+ api/search/
64
+ route.ts → GET /api/search (standalone API, no page.tsx needed)
55
65
  ```
56
66
 
57
67
  ### page.tsx — page component
@@ -155,7 +165,18 @@ export const POST: Handler = async (req, ctx) => {
155
165
  }
156
166
  ```
157
167
 
158
- Route.ts exports `POST`/`PUT`/`DELETE`/`PATCH` (GET is handled by page.tsx). The same `route.ts` file coexists with `page.tsx` in the same directory for handling form submissions or AJAX requests.
168
+ Route.ts exports `POST`/`PUT`/`DELETE`/`PATCH` (GET is handled by page.tsx). The same `route.ts` file coexists with `page.tsx` in the same directory for handling form submissions or AJAX requests. Standalone `route.ts` (without a co-located `page.tsx`) registers all methods including `GET`.
169
+
170
+ ### not-found.tsx — 404 page
171
+
172
+ ```tsx
173
+ // pages/not-found.tsx
174
+ export default function NotFound() {
175
+ return <h1 class="text-4xl">404 – Not Found</h1>
176
+ }
177
+ ```
178
+
179
+ Automatically rendered for unmatched routes, wrapped in the full layout chain. Works with `use('/')` mounting and standalone usage.
159
180
 
160
181
  ### Usage within a full app
161
182
 
@@ -224,6 +245,9 @@ app.get('/protected', auth({ proxy: 'http://auth:3000/validate' }), handler)
224
245
 
225
246
  // Custom header
226
247
  app.use(auth({ header: 'X-API-Key', token: 'my-key' }))
248
+
249
+ // Token can also be passed via query string ?access_token=xxx
250
+ // Proxy forwards using the same method the client used (header ↔ query)
227
251
  ```
228
252
 
229
253
  ### CORS
@@ -407,6 +431,35 @@ const app = new Router()
407
431
  serve(app.handler(), { port: 3000 })
408
432
  ```
409
433
 
434
+ ## Workflow
435
+
436
+ Define tools (business capabilities) and let AI generate and execute multi-step workflows.
437
+
438
+ ```ts
439
+ import { Router, tool, createWorkflowEngine } from 'weifuwu'
440
+ import { z } from 'zod'
441
+
442
+ const tools = {
443
+ queryUser: tool({
444
+ description: '查询用户信息,返回 email, name',
445
+ inputSchema: z.object({ userId: z.string() }),
446
+ execute: async ({ userId }) => ({ id: userId, email: 'user@test.com', name: 'Test' }),
447
+ }),
448
+ }
449
+
450
+ const app = new Router()
451
+
452
+ // Router method — accepts { nodes } or { goal } with model
453
+ app.workflow('/api/agent', { tools })
454
+
455
+ // Or manual execution
456
+ const engine = createWorkflowEngine({ tools })
457
+ const result = await engine.execute({ nodes: [
458
+ { id: 's1', tool: 'set', input: { name: 'msg', value: 'hello' } },
459
+ { id: 'g1', tool: 'get', input: { name: 'msg' } },
460
+ ]})
461
+ ```
462
+
410
463
  ## Graceful shutdown
411
464
 
412
465
  ```ts
package/dist/index.d.ts CHANGED
@@ -19,3 +19,5 @@ export { rateLimit } from './rate-limit.ts';
19
19
  export type { RateLimitOptions } from './rate-limit.ts';
20
20
  export { compress } from './compress.ts';
21
21
  export type { CompressOptions } from './compress.ts';
22
+ export { tool, createWorkflowEngine, createSSEManager, generateWorkflow } from './workflow/index.ts';
23
+ export type { Tool, Workflow, WorkflowEngine, WorkflowState, SSEEvent } from './workflow/types.ts';
package/dist/index.js CHANGED
@@ -110,6 +110,490 @@ import { WebSocketServer } from "ws";
110
110
  import { buildSchema, graphql } from "graphql";
111
111
  import { makeExecutableSchema } from "@graphql-tools/schema";
112
112
  import { streamText } from "ai";
113
+
114
+ // workflow/tool.ts
115
+ function tool(def) {
116
+ return {
117
+ name: def.name ?? "",
118
+ description: def.description,
119
+ inputSchema: def.inputSchema,
120
+ execute: def.execute
121
+ };
122
+ }
123
+
124
+ // workflow/reference.ts
125
+ function getByPath(obj, path) {
126
+ let current = obj;
127
+ for (const key of path) {
128
+ if (current === null || current === void 0) return void 0;
129
+ if (typeof current === "object" && key in current) {
130
+ current = current[key];
131
+ } else {
132
+ return void 0;
133
+ }
134
+ }
135
+ return current;
136
+ }
137
+ function resolveRef(path, ctx) {
138
+ if (path.startsWith("$nodes.")) {
139
+ const afterNodes = path.slice(7);
140
+ const dotIdx = afterNodes.indexOf(".");
141
+ if (dotIdx === -1) {
142
+ return ctx.nodeOutputs.get(afterNodes);
143
+ }
144
+ const id2 = afterNodes.slice(0, dotIdx);
145
+ const propPath = afterNodes.slice(dotIdx + 1);
146
+ const output = ctx.nodeOutputs.get(id2);
147
+ if (output === void 0) {
148
+ throw new Error(`Node "${id2}" has no output yet`);
149
+ }
150
+ if (propPath.startsWith("output")) {
151
+ return getByPath(output, propPath.slice(7).split(".").filter(Boolean));
152
+ }
153
+ return getByPath(output, propPath.split("."));
154
+ }
155
+ if (path.startsWith("$var.")) {
156
+ const name = path.slice(5);
157
+ if (!ctx.variables.has(name)) {
158
+ throw new Error(`Variable "${name}" is not defined`);
159
+ }
160
+ return ctx.variables.get(name);
161
+ }
162
+ if (path.startsWith("$input.")) {
163
+ const key = path.slice(7);
164
+ return ctx.input[key];
165
+ }
166
+ if (path === "true") return true;
167
+ if (path === "false") return false;
168
+ if (path === "null") return null;
169
+ const num = Number(path);
170
+ if (!isNaN(num) && path.trim() !== "") return num;
171
+ return path;
172
+ }
173
+ function resolveValue(v, ctx) {
174
+ if (typeof v === "string" && v.startsWith("$")) {
175
+ return resolveRef(v, ctx);
176
+ }
177
+ if (Array.isArray(v)) {
178
+ return v.map((item) => resolveValue(item, ctx));
179
+ }
180
+ if (typeof v === "object" && v !== null) {
181
+ const result = {};
182
+ for (const [k, val] of Object.entries(v)) {
183
+ result[k] = resolveValue(val, ctx);
184
+ }
185
+ return result;
186
+ }
187
+ return v;
188
+ }
189
+
190
+ // workflow/nodes.ts
191
+ function evaluateExpression(expr, ctx) {
192
+ const operators = [
193
+ { op: "===", fn: (a, b) => a === b },
194
+ { op: "!==", fn: (a, b) => a !== b },
195
+ { op: ">=", fn: (a, b) => Number(a) >= Number(b) },
196
+ { op: "<=", fn: (a, b) => Number(a) <= Number(b) },
197
+ { op: ">", fn: (a, b) => Number(a) > Number(b) },
198
+ { op: "<", fn: (a, b) => Number(a) < Number(b) },
199
+ { op: "==", fn: (a, b) => a == b },
200
+ { op: "!=", fn: (a, b) => a != b },
201
+ { op: "+", fn: (a, b) => Number(a) + Number(b) },
202
+ { op: "-", fn: (a, b) => Number(a) - Number(b) },
203
+ { op: "*", fn: (a, b) => Number(a) * Number(b) },
204
+ { op: "/", fn: (a, b) => Number(a) / Number(b) },
205
+ { op: "%", fn: (a, b) => Number(a) % Number(b) },
206
+ { op: "&&", fn: (a, b) => Boolean(a) && Boolean(b) },
207
+ { op: "||", fn: (a, b) => Boolean(a) || Boolean(b) }
208
+ ];
209
+ for (const { op, fn } of operators) {
210
+ const idx = expr.indexOf(op);
211
+ if (idx > 0) {
212
+ const leftRaw = expr.slice(0, idx).trim();
213
+ const rightRaw = expr.slice(idx + op.length).trim();
214
+ const left = resolveValue(leftRaw, ctx);
215
+ const right = resolveValue(rightRaw, ctx);
216
+ return fn(left, right);
217
+ }
218
+ }
219
+ const trimmed = expr.trim();
220
+ if (trimmed === "true") return true;
221
+ if (trimmed === "false") return false;
222
+ if (trimmed === "null") return null;
223
+ const num = Number(trimmed);
224
+ if (!isNaN(num) && trimmed !== "") return num;
225
+ return resolveValue(expr, ctx);
226
+ }
227
+ async function executeEval(node, ctx) {
228
+ const expression = node.input.expression;
229
+ if (!expression) throw new Error('eval node requires "expression" field');
230
+ const result = evaluateExpression(expression, ctx);
231
+ return { result };
232
+ }
233
+ async function executeSet(node, ctx) {
234
+ const name = node.input.name;
235
+ const value = node.input.value;
236
+ if (!name) throw new Error('set node requires "name" field');
237
+ let resolved;
238
+ if (typeof value === "string") {
239
+ resolved = evaluateExpression(value, ctx);
240
+ } else {
241
+ resolved = resolveValue(value ?? null, ctx);
242
+ }
243
+ ctx.variables.set(name, resolved);
244
+ return resolved;
245
+ }
246
+ async function executeGet(node, ctx) {
247
+ const name = node.input.name;
248
+ if (!name) throw new Error('get node requires "name" field');
249
+ if (!ctx.variables.has(name)) {
250
+ throw new Error(`Variable "${name}" is not defined`);
251
+ }
252
+ return ctx.variables.get(name);
253
+ }
254
+ async function executeIf(node, ctx) {
255
+ const conditions = node.conditions ?? [];
256
+ for (const condition of conditions) {
257
+ const test = typeof condition.test === "string" ? Boolean(resolveValue(condition.test, ctx)) : condition.test;
258
+ if (test && condition.body) {
259
+ let lastOutput = void 0;
260
+ for (const bodyNode of condition.body) {
261
+ lastOutput = await executeNode(bodyNode, ctx);
262
+ }
263
+ return lastOutput;
264
+ }
265
+ }
266
+ return void 0;
267
+ }
268
+ async function executeWhile(node, ctx) {
269
+ const conditionExpr = node.input.condition;
270
+ if (!conditionExpr) throw new Error('while node requires "condition" field');
271
+ let lastOutput = void 0;
272
+ let iterations = 0;
273
+ const maxIterations = 1e3;
274
+ while (iterations < maxIterations) {
275
+ iterations++;
276
+ ctx.stepCount++;
277
+ if (ctx.stepCount > ctx.maxSteps) {
278
+ throw new Error(`Step limit exceeded (${ctx.maxSteps})`);
279
+ }
280
+ const condition = Boolean(evaluateExpression(conditionExpr, ctx));
281
+ if (!condition) break;
282
+ for (const bodyNode of node.body ?? []) {
283
+ lastOutput = await executeNode(bodyNode, ctx);
284
+ }
285
+ }
286
+ return lastOutput;
287
+ }
288
+ async function executeCall(node, ctx) {
289
+ const toolName = node.input.tool;
290
+ const args = node.input.args ?? {};
291
+ if (toolName && ctx.toolRegistry.has(toolName)) {
292
+ const tool3 = ctx.toolRegistry.get(toolName);
293
+ const resolvedInput = resolveValue(args, ctx);
294
+ const parsed = tool3.inputSchema.parse(resolvedInput);
295
+ return tool3.execute(parsed, {
296
+ nodeId: node.id,
297
+ workflowId: ctx.workflowId,
298
+ onStream: async (event) => {
299
+ if (ctx.sseManager && ctx.workflowId) {
300
+ ctx.sseManager.send(ctx.workflowId, { event: "llm-stream", data: { nodeId: node.id, ...event } });
301
+ }
302
+ }
303
+ });
304
+ }
305
+ const functionName = node.input.function;
306
+ if (functionName && ctx.functions[functionName]) {
307
+ const fn = ctx.functions[functionName];
308
+ const prevFunctions = ctx.functions;
309
+ const prevInput = ctx.input;
310
+ ctx.input = resolveValue(args, ctx);
311
+ let lastOutput = void 0;
312
+ for (const bodyNode of fn.workflow.nodes) {
313
+ lastOutput = await executeNode(bodyNode, ctx);
314
+ }
315
+ ctx.input = prevInput;
316
+ return lastOutput;
317
+ }
318
+ throw new Error(`call node: tool "${toolName ?? functionName}" not found`);
319
+ }
320
+ async function executeHttp(node, ctx) {
321
+ const input = resolveValue(node.input, ctx);
322
+ const url = input.url;
323
+ if (!url) throw new Error('http node requires "url" field');
324
+ const controller = new AbortController();
325
+ const timeout = input.timeout ?? 3e4;
326
+ const timer = setTimeout(() => controller.abort(), timeout);
327
+ try {
328
+ const fetchInit = {
329
+ method: input.method ?? "GET",
330
+ headers: input.headers ?? {},
331
+ signal: controller.signal
332
+ };
333
+ if (input.body && fetchInit.method !== "GET") {
334
+ fetchInit.body = JSON.stringify(input.body);
335
+ }
336
+ const response = await fetch(url, fetchInit);
337
+ const contentType = response.headers.get("content-type") ?? "";
338
+ const body = contentType.includes("application/json") ? await response.json() : await response.text();
339
+ return {
340
+ status: response.status,
341
+ statusText: response.statusText,
342
+ headers: Object.fromEntries(response.headers.entries()),
343
+ body
344
+ };
345
+ } finally {
346
+ clearTimeout(timer);
347
+ }
348
+ }
349
+ var executors = {
350
+ eval: executeEval,
351
+ set: executeSet,
352
+ get: executeGet,
353
+ if: executeIf,
354
+ while: executeWhile,
355
+ call: executeCall,
356
+ http: executeHttp
357
+ };
358
+ async function executeNode(node, ctx) {
359
+ const executor = executors[node.tool];
360
+ if (!executor) {
361
+ throw new Error(`Unknown node type: "${node.tool}"`);
362
+ }
363
+ return executor(node, ctx);
364
+ }
365
+
366
+ // workflow/llm.ts
367
+ function buildToolsDescription(tools) {
368
+ return Object.entries(tools).map(([key, t]) => {
369
+ const name = t.name || key;
370
+ const schema = t.inputSchema;
371
+ return `- ${name}: ${t.description}
372
+ Input schema: describe as JSON object fields`;
373
+ }).join("\n");
374
+ }
375
+ var SYSTEM_PROMPT_TEMPLATE = `You are a workflow generator. Given a user goal and available tools, output a workflow JSON.
376
+
377
+ Available tools:
378
+ {{TOOLS}}
379
+
380
+ Workflow format:
381
+ {
382
+ "name": "workflow name",
383
+ "nodes": [
384
+ {
385
+ "id": "step1",
386
+ "tool": "set",
387
+ "input": { "name": "varName", "value": "initialValue" }
388
+ },
389
+ {
390
+ "id": "step2",
391
+ "tool": "call",
392
+ "input": { "tool": "toolName", "args": { "param1": "$var.varName" } }
393
+ },
394
+ {
395
+ "id": "step3",
396
+ "tool": "if",
397
+ "input": {},
398
+ "conditions": [
399
+ { "test": "$nodes.step2.output.someField", "body": [
400
+ { "id": "step4", "tool": "call", "input": { "tool": "toolName", "args": {} } }
401
+ ]}
402
+ ]
403
+ }
404
+ ]
405
+ }
406
+
407
+ Node types:
408
+ - eval: evaluate an expression. input: { expression: "..." }
409
+ - set: assign a variable. input: { name, value }
410
+ - get: read a variable. input: { name }
411
+ - if: conditional branch. input: {}, conditions: [{ test, body }]
412
+ - while: loop. input: { condition }, body: [nodes]
413
+ - call: call a registered tool. input: { tool, args }
414
+ - http: HTTP request. input: { url, method?, headers?, body? }
415
+
416
+ Reference syntax:
417
+ - $var.name - read a variable
418
+ - $nodes.id.output - output of a previous node
419
+ - $nodes.id.output.field - specific field of a node's output
420
+ - $input.field - workflow input parameter
421
+
422
+ Output ONLY valid JSON. No explanation, no markdown.`;
423
+ async function generateWorkflow(goal, tools, generateFn) {
424
+ const toolsDesc = buildToolsDescription(tools);
425
+ const system = SYSTEM_PROMPT_TEMPLATE.replace("{{TOOLS}}", toolsDesc);
426
+ const result = await generateFn({
427
+ system,
428
+ messages: [{ role: "user", content: goal }]
429
+ });
430
+ const text = result.text.trim();
431
+ const jsonStart = text.indexOf("{");
432
+ const jsonEnd = text.lastIndexOf("}");
433
+ if (jsonStart === -1 || jsonEnd === -1) {
434
+ throw new Error(`LLM output is not valid JSON: ${text.slice(0, 200)}`);
435
+ }
436
+ const jsonStr = text.slice(jsonStart, jsonEnd + 1);
437
+ try {
438
+ const workflow = JSON.parse(jsonStr);
439
+ if (!workflow.nodes || !Array.isArray(workflow.nodes)) {
440
+ throw new Error("Generated workflow has no nodes array");
441
+ }
442
+ return workflow;
443
+ } catch (err) {
444
+ if (err instanceof SyntaxError) {
445
+ throw new Error(`Failed to parse LLM output as JSON: ${err.message}`);
446
+ }
447
+ throw err;
448
+ }
449
+ }
450
+
451
+ // workflow/engine.ts
452
+ import { generateText } from "ai";
453
+ function createWorkflowEngine(options) {
454
+ const toolRegistry = /* @__PURE__ */ new Map();
455
+ for (const [key, t] of Object.entries(options.tools)) {
456
+ t.name = t.name || key;
457
+ toolRegistry.set(t.name, t);
458
+ }
459
+ const states = /* @__PURE__ */ new Map();
460
+ async function execute(workflow, opts) {
461
+ const ctx = {
462
+ variables: /* @__PURE__ */ new Map(),
463
+ nodeOutputs: /* @__PURE__ */ new Map(),
464
+ functions: workflow.functions ?? {},
465
+ stepCount: 0,
466
+ maxSteps: opts?.maxSteps ?? 1e3,
467
+ input: opts?.initialInput ?? {},
468
+ toolRegistry,
469
+ sseManager: options.sseManager,
470
+ workflowId: opts?.workflowId
471
+ };
472
+ let lastOutput = void 0;
473
+ for (const node of workflow.nodes) {
474
+ ctx.stepCount++;
475
+ if (ctx.stepCount > ctx.maxSteps) {
476
+ throw new Error(`Step limit exceeded (${ctx.maxSteps})`);
477
+ }
478
+ options.sseManager?.send(ctx.workflowId ?? "", { event: "node-start", data: { nodeId: node.id, tool: node.tool, input: node.input } });
479
+ const output = await executeNode(node, ctx);
480
+ ctx.nodeOutputs.set(node.id, output);
481
+ lastOutput = output;
482
+ options.sseManager?.send(ctx.workflowId ?? "", { event: "node-end", data: { nodeId: node.id, output } });
483
+ }
484
+ return lastOutput;
485
+ }
486
+ async function runAsync(workflowId, workflow, opts) {
487
+ const state = {
488
+ workflowId,
489
+ status: "running",
490
+ goal: workflow.name ?? "",
491
+ startTime: Date.now()
492
+ };
493
+ states.set(workflowId, state);
494
+ const sse = options.sseManager;
495
+ sse?.send(workflowId, { event: "workflow-start", data: { workflowId, goal: state.goal } });
496
+ try {
497
+ const result = await execute(workflow, { ...opts, workflowId });
498
+ state.status = "completed";
499
+ state.result = result;
500
+ state.endTime = Date.now();
501
+ sse?.send(workflowId, { event: "complete", data: { result, duration: state.endTime - state.startTime } });
502
+ } catch (err) {
503
+ state.status = "error";
504
+ state.error = err instanceof Error ? err.message : String(err);
505
+ state.endTime = Date.now();
506
+ sse?.send(workflowId, { event: "error", data: { error: state.error } });
507
+ } finally {
508
+ sse?.close(workflowId);
509
+ }
510
+ }
511
+ async function generateWorkflow2(goal) {
512
+ if (!options.model) {
513
+ throw new Error('LLM model is required for generateWorkflow. Pass "model" to createWorkflowEngine.');
514
+ }
515
+ return generateWorkflow(goal, options.tools, async (prompt) => {
516
+ const result = await generateText({
517
+ model: options.model,
518
+ system: prompt.system,
519
+ messages: prompt.messages
520
+ });
521
+ return { text: result.text };
522
+ });
523
+ }
524
+ return {
525
+ execute,
526
+ runAsync,
527
+ generateWorkflow: generateWorkflow2,
528
+ getState(workflowId) {
529
+ return states.get(workflowId);
530
+ }
531
+ };
532
+ }
533
+
534
+ // workflow/sse.ts
535
+ function createSSEManager() {
536
+ const streams = /* @__PURE__ */ new Map();
537
+ const encoder = new TextEncoder();
538
+ function createStream(workflowId) {
539
+ const state = {
540
+ controller: null,
541
+ encoder,
542
+ closed: false,
543
+ buffer: []
544
+ };
545
+ const stream = new ReadableStream({
546
+ start(controller) {
547
+ state.controller = controller;
548
+ streams.set(workflowId, state);
549
+ for (const event of state.buffer) {
550
+ try {
551
+ controller.enqueue(encoder.encode(event));
552
+ } catch {
553
+ break;
554
+ }
555
+ }
556
+ state.buffer = [];
557
+ },
558
+ cancel() {
559
+ state.closed = true;
560
+ streams.delete(workflowId);
561
+ }
562
+ });
563
+ return stream;
564
+ }
565
+ function send(workflowId, event) {
566
+ const state = streams.get(workflowId);
567
+ if (!state || state.closed) return;
568
+ const data = `event: ${event.event}
569
+ data: ${JSON.stringify(event.data)}
570
+
571
+ `;
572
+ if (state.controller) {
573
+ try {
574
+ state.controller.enqueue(encoder.encode(data));
575
+ } catch {
576
+ state.closed = true;
577
+ streams.delete(workflowId);
578
+ }
579
+ } else {
580
+ state.buffer.push(data);
581
+ }
582
+ }
583
+ function close(workflowId) {
584
+ const state = streams.get(workflowId);
585
+ if (!state) return;
586
+ state.closed = true;
587
+ streams.delete(workflowId);
588
+ try {
589
+ state.controller?.close();
590
+ } catch {
591
+ }
592
+ }
593
+ return { createStream, send, close };
594
+ }
595
+
596
+ // router.ts
113
597
  var createTrieNode = () => ({
114
598
  children: /* @__PURE__ */ new Map(),
115
599
  handlers: /* @__PURE__ */ new Map(),
@@ -302,6 +786,50 @@ var Router = class _Router {
302
786
  };
303
787
  return this.post(path, ...middlewares, routeHandler);
304
788
  }
789
+ workflow(path, options) {
790
+ const sseManager = options.stream ? createSSEManager() : void 0;
791
+ if (options.stream && sseManager) {
792
+ this.get(`${path}/:workflowId/events`, async (req, ctx) => {
793
+ const stream = sseManager.createStream(ctx.params.workflowId);
794
+ return new Response(stream, {
795
+ headers: {
796
+ "Content-Type": "text/event-stream",
797
+ "Cache-Control": "no-cache",
798
+ "Connection": "keep-alive"
799
+ }
800
+ });
801
+ });
802
+ }
803
+ this.post(path, async (req) => {
804
+ const body = await req.json();
805
+ const engine = createWorkflowEngine({
806
+ tools: options.tools,
807
+ model: options.model,
808
+ sseManager
809
+ });
810
+ let wf;
811
+ if (body.goal && options.model) {
812
+ wf = await engine.generateWorkflow(body.goal);
813
+ } else if (body.workflow) {
814
+ wf = body.workflow;
815
+ } else if (body.nodes) {
816
+ wf = { nodes: body.nodes };
817
+ } else {
818
+ return Response.json(
819
+ { error: 'Provide "goal" (with model) or "workflow"/"nodes"' },
820
+ { status: 400 }
821
+ );
822
+ }
823
+ if (options.stream && sseManager) {
824
+ const workflowId = crypto.randomUUID();
825
+ engine.runAsync(workflowId, wf);
826
+ return Response.json({ workflowId, eventsUrl: `${path}/${workflowId}/events` });
827
+ }
828
+ const result = await engine.execute(wf);
829
+ return Response.json({ workflow: wf, result });
830
+ });
831
+ return this;
832
+ }
305
833
  handler() {
306
834
  return (req, ctx) => {
307
835
  const url = new URL(req.url);
@@ -1472,13 +2000,17 @@ export {
1472
2000
  auth,
1473
2001
  compress,
1474
2002
  cors,
2003
+ createSSEManager,
2004
+ createWorkflowEngine,
1475
2005
  deleteCookie,
2006
+ generateWorkflow,
1476
2007
  getCookies,
1477
2008
  logger,
1478
2009
  rateLimit,
1479
2010
  serve,
1480
2011
  serveStatic,
1481
2012
  setCookie,
2013
+ tool,
1482
2014
  tsx,
1483
2015
  upload,
1484
2016
  useTsx,
package/dist/router.d.ts CHANGED
@@ -2,8 +2,9 @@ import { type WebSocket } from 'ws';
2
2
  import type { IncomingMessage } from 'node:http';
3
3
  import type { Duplex } from 'node:stream';
4
4
  import { type GraphQLSchema } from 'graphql';
5
- import { streamText } from 'ai';
5
+ import { streamText, generateText } from 'ai';
6
6
  import type { Context, Handler, Middleware, ErrorHandler } from './types.ts';
7
+ import type { Tool as WfTool } from './workflow/types.ts';
7
8
  type StreamTextParams = Parameters<typeof streamText>[0];
8
9
  export type WebSocketHandler = {
9
10
  open?: (ws: WebSocket, ctx: Context) => void | Promise<void>;
@@ -44,6 +45,11 @@ export declare class Router {
44
45
  ws(path: string, ...args: [...Middleware[], WebSocketHandler]): this;
45
46
  graphql(path: string, ...args: [...Middleware[], GraphQLOptions]): this;
46
47
  ai(path: string, ...args: [...Middleware[], AIHandler]): this;
48
+ workflow(path: string, options: {
49
+ tools: Record<string, WfTool>;
50
+ model?: Parameters<typeof generateText>[0]['model'];
51
+ stream?: boolean;
52
+ }): this;
47
53
  handler(): Handler;
48
54
  websocketHandler(): WsUpgradeHandler;
49
55
  private splitPath;
@@ -0,0 +1,7 @@
1
+ import type { Tool, WorkflowEngine, SSEManager } from './types.ts';
2
+ import { type LanguageModel } from 'ai';
3
+ export declare function createWorkflowEngine(options: {
4
+ tools: Record<string, Tool>;
5
+ sseManager?: SSEManager;
6
+ model?: LanguageModel;
7
+ }): WorkflowEngine;
@@ -0,0 +1,6 @@
1
+ export { tool } from './tool.ts';
2
+ export { createWorkflowEngine } from './engine.ts';
3
+ export { createSSEManager } from './sse.ts';
4
+ export { resolveRef, resolveValue } from './reference.ts';
5
+ export { generateWorkflow } from './llm.ts';
6
+ export type { Tool, ToolContext, Workflow, Node, WorkflowState, SSEEvent, SSEManager, WorkflowEngine, ExecuteOptions, StreamEvent, Condition, SubWorkflow } from './types.ts';
@@ -0,0 +1,10 @@
1
+ import type { Tool, Workflow } from './types.ts';
2
+ export declare function generateWorkflow(goal: string, tools: Record<string, Tool>, generateFn: (prompt: {
3
+ system: string;
4
+ messages: {
5
+ role: string;
6
+ content: string;
7
+ }[];
8
+ }) => Promise<{
9
+ text: string;
10
+ }>): Promise<Workflow>;
@@ -0,0 +1,10 @@
1
+ import type { Node, WorkflowContext } from './types.ts';
2
+ export type NodeExecutor = (node: Node, ctx: WorkflowContext) => Promise<unknown>;
3
+ export declare function executeEval(node: Node, ctx: WorkflowContext): Promise<unknown>;
4
+ export declare function executeSet(node: Node, ctx: WorkflowContext): Promise<unknown>;
5
+ export declare function executeGet(node: Node, ctx: WorkflowContext): Promise<unknown>;
6
+ export declare function executeIf(node: Node, ctx: WorkflowContext): Promise<unknown>;
7
+ export declare function executeWhile(node: Node, ctx: WorkflowContext): Promise<unknown>;
8
+ export declare function executeCall(node: Node, ctx: WorkflowContext): Promise<unknown>;
9
+ export declare function executeHttp(node: Node, ctx: WorkflowContext): Promise<unknown>;
10
+ export declare function executeNode(node: Node, ctx: WorkflowContext): Promise<unknown>;
@@ -0,0 +1,3 @@
1
+ import type { WorkflowContext } from './types.ts';
2
+ export declare function resolveRef(path: string, ctx: WorkflowContext): unknown;
3
+ export declare function resolveValue(v: unknown, ctx: WorkflowContext): unknown;
@@ -0,0 +1,2 @@
1
+ import type { SSEManager } from './types.ts';
2
+ export declare function createSSEManager(): SSEManager;
@@ -0,0 +1,8 @@
1
+ import type { z } from 'zod';
2
+ import type { Tool, ToolContext } from './types.ts';
3
+ export declare function tool<TInput = unknown, TOutput = unknown>(def: {
4
+ name?: string;
5
+ description: string;
6
+ inputSchema: z.ZodSchema<TInput>;
7
+ execute: (input: TInput, ctx: ToolContext) => Promise<TOutput>;
8
+ }): Tool<TInput, TOutput>;
@@ -0,0 +1,86 @@
1
+ import type { z } from 'zod';
2
+ export type NodeType = 'eval' | 'set' | 'get' | 'if' | 'while' | 'call' | 'http';
3
+ export interface Tool<TInput = unknown, TOutput = unknown> {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: z.ZodSchema<TInput>;
7
+ execute: (input: TInput, ctx: ToolContext) => Promise<TOutput>;
8
+ }
9
+ export interface ToolContext {
10
+ workflowId?: string;
11
+ nodeId: string;
12
+ onStream?: (event: StreamEvent) => Promise<void>;
13
+ }
14
+ export interface StreamEvent {
15
+ type: string;
16
+ chunk?: string;
17
+ accumulated?: string;
18
+ [key: string]: unknown;
19
+ }
20
+ export interface Node {
21
+ id: string;
22
+ tool: NodeType | string;
23
+ input: Record<string, unknown>;
24
+ conditions?: Condition[];
25
+ body?: Node[];
26
+ }
27
+ export interface Condition {
28
+ test: string | boolean;
29
+ body: Node[];
30
+ }
31
+ export interface Workflow {
32
+ name?: string;
33
+ nodes: Node[];
34
+ functions?: Record<string, SubWorkflow>;
35
+ }
36
+ export interface SubWorkflow {
37
+ inputSchema: Record<string, unknown>;
38
+ workflow: {
39
+ nodes: Node[];
40
+ };
41
+ }
42
+ export interface WorkflowState {
43
+ workflowId: string;
44
+ status: 'running' | 'completed' | 'error';
45
+ goal: string;
46
+ result?: unknown;
47
+ error?: string;
48
+ startTime: number;
49
+ endTime?: number;
50
+ }
51
+ export interface SSEEvent {
52
+ event: string;
53
+ data: unknown;
54
+ }
55
+ export interface ExecuteOptions {
56
+ initialInput?: Record<string, unknown>;
57
+ maxSteps?: number;
58
+ }
59
+ export interface WorkflowContext {
60
+ variables: Map<string, unknown>;
61
+ nodeOutputs: Map<string, unknown>;
62
+ functions: Record<string, SubWorkflow>;
63
+ stepCount: number;
64
+ maxSteps: number;
65
+ input: Record<string, unknown>;
66
+ toolRegistry: Map<string, Tool>;
67
+ sseManager?: SSEManager;
68
+ workflowId?: string;
69
+ onNodeEvent?: (event: SSEEvent) => void;
70
+ }
71
+ export interface SSEManager {
72
+ createStream: (workflowId: string) => ReadableStream<Uint8Array>;
73
+ send: (workflowId: string, event: SSEEvent) => void;
74
+ close: (workflowId: string) => void;
75
+ }
76
+ export interface EngineOptions {
77
+ tools: Record<string, Tool>;
78
+ model?: unknown;
79
+ sseManager?: SSEManager;
80
+ }
81
+ export interface WorkflowEngine {
82
+ execute: (workflow: Workflow, options?: ExecuteOptions) => Promise<unknown>;
83
+ getState: (workflowId: string) => WorkflowState | undefined;
84
+ runAsync: (workflowId: string, workflow: Workflow, options?: ExecuteOptions) => Promise<void>;
85
+ generateWorkflow: (goal: string) => Promise<Workflow>;
86
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weifuwu",
3
- "version": "0.2.4",
3
+ "version": "0.4.0",
4
4
  "description": "Web-standard HTTP framework for Node.js — (req, ctx) => Response",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",