weifuwu 0.31.2 → 0.32.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 +136 -42
- package/dist/ai/agent.d.ts +127 -0
- package/dist/ai/index.d.ts +26 -0
- package/dist/ai/types.d.ts +59 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +331 -40
- package/dist/middleware/auth.d.ts +70 -0
- package/dist/middleware/sandbox.d.ts +52 -0
- package/dist/types.d.ts +7 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -64,7 +64,8 @@ app.get('/users/:id', (req, ctx) => {
|
|
|
64
64
|
| `logger(opts?)` | Request logging. `format: 'short' | 'combined' | 'json'`. |
|
|
65
65
|
| `upload(opts?)` | Multipart file upload via `req.formData()`. Injects `ctx.parsed`. |
|
|
66
66
|
| `serveStatic(root, opts?)` | Static file handler. `cacheControl`, `index`. |
|
|
67
|
-
| `
|
|
67
|
+
| `sandbox(opts?)` | Filesystem isolation for agent operations. `baseDir`, `timeout`, `isolateBy`. |
|
|
68
|
+
| `auth(opts?)` | Authentication: JWT, session cookies, API keys. Injects `ctx.user`. |
|
|
68
69
|
|
|
69
70
|
### Tracing
|
|
70
71
|
|
|
@@ -313,6 +314,113 @@ import { createBrowserRouter, hydrate, navigate } from 'weifuwu/react/client'
|
|
|
313
314
|
|
|
314
315
|
See [examples/react-ssr/](examples/react-ssr/) for the full demo.
|
|
315
316
|
|
|
317
|
+
### AI Agent
|
|
318
|
+
|
|
319
|
+
> Requires `ai` (Vercel AI SDK). Install with a model provider:
|
|
320
|
+
> ```bash
|
|
321
|
+
> npm install ai @ai-sdk/openai
|
|
322
|
+
> ```
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import { agent } from 'weifuwu'
|
|
326
|
+
import { openai } from '@ai-sdk/openai'
|
|
327
|
+
import { tool } from 'ai'
|
|
328
|
+
import { z } from 'zod'
|
|
329
|
+
|
|
330
|
+
app.use(agent({
|
|
331
|
+
model: openai('gpt-4o'),
|
|
332
|
+
system: 'You are a helpful assistant.',
|
|
333
|
+
knowledge: {
|
|
334
|
+
// RAG — search a knowledge base before each response
|
|
335
|
+
search: async (query, ctx) => {
|
|
336
|
+
const { embeddings } = await embedModel.doEmbed({ values: [query] })
|
|
337
|
+
return ctx.sql`
|
|
338
|
+
SELECT content, 1 - (embedding <=> ${embeddings[0]}::vector) AS score
|
|
339
|
+
FROM docs ORDER BY embedding <=> ${embeddings[0]}::vector LIMIT 3
|
|
340
|
+
`
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
tools: {
|
|
344
|
+
getWeather: tool({
|
|
345
|
+
description: 'Get weather for a city',
|
|
346
|
+
parameters: z.object({ city: z.string() }),
|
|
347
|
+
execute: async ({ city }) => ({ temp: 22, unit: 'C' }),
|
|
348
|
+
}),
|
|
349
|
+
},
|
|
350
|
+
maxSteps: 5,
|
|
351
|
+
}))
|
|
352
|
+
|
|
353
|
+
app.post('/api/chat', async (req, ctx) => {
|
|
354
|
+
const { messages } = await req.json()
|
|
355
|
+
return ctx.agent.chatStreamResponse({ messages })
|
|
356
|
+
})
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**`agent()` handles:** multi-turn conversations, automatic tool-calling loops (maxSteps), knowledge retrieval (RAG) injected into the system prompt, and SSE streaming compatible with `useChat` from `@ai-sdk/react`.
|
|
360
|
+
|
|
361
|
+
| Feature | Description |
|
|
362
|
+
|---|---|
|
|
363
|
+
| `ctx.agent.chat(prompt, opts?)` | Non-streaming chat with tool calling and RAG |
|
|
364
|
+
| `ctx.agent.chatStreamResponse({ messages })` | SSE streaming response (useChat-compatible) |
|
|
365
|
+
| `knowledge.search` | User-defined RAG callback — query any data source via `ctx` |
|
|
366
|
+
| `tools` | Tool definitions (from `ai` package). Executed in automatic loops |
|
|
367
|
+
| `agents` | Named sub-agents with different models/tools |
|
|
368
|
+
| `sandbox: true` | Auto-integrate with `ctx.sandbox` for file operations |
|
|
369
|
+
| `store` | Session persistence (save/load conversation history) |
|
|
370
|
+
|
|
371
|
+
### Auth
|
|
372
|
+
|
|
373
|
+
JWT extraction priority: `cookie` → `Authorization: Bearer` → `?access_token=`. All callbacks receive `ctx` so `ctx.sql` is directly available.
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
import { auth } from 'weifuwu'
|
|
377
|
+
|
|
378
|
+
// JWT — cookie, header, or ?access_token=
|
|
379
|
+
app.use(auth({ jwt: { secret: process.env.JWT_SECRET } }))
|
|
380
|
+
|
|
381
|
+
// Session cookie — loadUser gets ctx
|
|
382
|
+
app.use(auth({
|
|
383
|
+
session: {
|
|
384
|
+
secret: '...',
|
|
385
|
+
loadUser: async (data, ctx) => ctx.sql`SELECT * FROM users WHERE id = ${data.userId}`,
|
|
386
|
+
},
|
|
387
|
+
}))
|
|
388
|
+
|
|
389
|
+
// API key — header or ?api_key=
|
|
390
|
+
app.use(auth({ apiKey: {
|
|
391
|
+
query: 'api_key',
|
|
392
|
+
validate: async (key, ctx) => ctx.sql`SELECT * FROM users WHERE api_key = ${key}`,
|
|
393
|
+
} }))
|
|
394
|
+
|
|
395
|
+
// ctx.user is now available
|
|
396
|
+
app.get('/me', (req, ctx) => {
|
|
397
|
+
if (!ctx.user) return new Response('Unauthorized', { status: 401 })
|
|
398
|
+
return Response.json(ctx.user)
|
|
399
|
+
})
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Supports JWT (HS256 via cookie or Authorization header), signed session cookies with `loadUser` callback, and API key validation. All are optional — requests proceed without a user identity unless handlers enforce it.
|
|
403
|
+
|
|
404
|
+
### Sandbox
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
import { sandbox } from 'weifuwu'
|
|
408
|
+
|
|
409
|
+
app.use(sandbox({
|
|
410
|
+
baseDir: '/tmp/workspaces',
|
|
411
|
+
timeout: 30000,
|
|
412
|
+
isolateBy: 'user', // one directory per ctx.user.id
|
|
413
|
+
}))
|
|
414
|
+
|
|
415
|
+
// ctx.sandbox provides isolated file + exec operations
|
|
416
|
+
await ctx.sandbox.writeFile('hello.txt', 'world')
|
|
417
|
+
const content = await ctx.sandbox.readFile('hello.txt')
|
|
418
|
+
const { stdout } = await ctx.sandbox.exec('ls -la')
|
|
419
|
+
await ctx.sandbox.destroy() // clean up workspace
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
All file paths are validated — escapes (`../`) are rejected. `exec()` enforces timeout and sets `HOME` to the workspace directory. When `isolateBy: 'user'` is set, each user gets their own directory under `baseDir`.
|
|
423
|
+
|
|
316
424
|
### Types
|
|
317
425
|
|
|
318
426
|
| Export | Description |
|
|
@@ -368,50 +476,35 @@ app.onError((err, req, ctx) => {
|
|
|
368
476
|
## Complete example
|
|
369
477
|
|
|
370
478
|
```ts
|
|
371
|
-
import { serve, Router, cors, helmet, compress, logger, trace, rateLimit, postgres, redis,
|
|
479
|
+
import { serve, Router, cors, helmet, compress, logger, trace, rateLimit, postgres, redis, auth, sandbox, HttpError } from 'weifuwu'
|
|
480
|
+
import { agent } from 'weifuwu/agent'
|
|
481
|
+
import { react } from 'weifuwu/react'
|
|
482
|
+
import { openai } from '@ai-sdk/openai'
|
|
372
483
|
|
|
373
484
|
const app = new Router()
|
|
485
|
+
.use(trace())
|
|
486
|
+
.use(logger())
|
|
487
|
+
.use(cors())
|
|
488
|
+
.use(helmet())
|
|
489
|
+
.use(compress())
|
|
490
|
+
.use(postgres())
|
|
491
|
+
.use(redis())
|
|
492
|
+
.use(auth({ jwt: { secret: process.env.JWT_SECRET! } }))
|
|
493
|
+
.use(sandbox({ isolateBy: 'user' }))
|
|
494
|
+
.use(agent({
|
|
495
|
+
model: openai('gpt-4o'),
|
|
496
|
+
system: 'You are a helpful assistant.',
|
|
497
|
+
knowledge: {
|
|
498
|
+
search: async (q, ctx) => ctx.sql`...`,
|
|
499
|
+
},
|
|
500
|
+
sandbox: true,
|
|
501
|
+
}))
|
|
502
|
+
.plugin(react({ pages: { '/': './Chat.tsx' }, layout: './Layout.tsx', tailwind: {} }))
|
|
374
503
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
app.use(helmet())
|
|
380
|
-
app.use(compress())
|
|
381
|
-
app.use(rateLimit({ windowMs: 60_000, max: 100 }))
|
|
382
|
-
|
|
383
|
-
// Database
|
|
384
|
-
const sql = postgres()
|
|
385
|
-
app.use(sql)
|
|
386
|
-
|
|
387
|
-
// Routes
|
|
388
|
-
app.get('/api/users', async (req, ctx) => {
|
|
389
|
-
const users = await ctx.sql`SELECT id, name FROM users ORDER BY id`
|
|
390
|
-
return Response.json(users)
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
app.post('/api/users', async (req, ctx) => {
|
|
394
|
-
const { name, email } = await req.json()
|
|
395
|
-
const [user] = await ctx.sql`INSERT INTO users (name,email) VALUES (${name},${email}) RETURNING *`
|
|
396
|
-
return Response.json(user, { status: 201 })
|
|
397
|
-
})
|
|
398
|
-
|
|
399
|
-
app.get('/api/users/:id', async (req, ctx) => {
|
|
400
|
-
const [user] = await ctx.sql`SELECT * FROM users WHERE id = ${ctx.params.id}`
|
|
401
|
-
if (!user) throw new HttpError('Not found', 404)
|
|
402
|
-
return Response.json(user)
|
|
403
|
-
})
|
|
404
|
-
|
|
405
|
-
// WebSocket
|
|
406
|
-
app.ws('/chat', {
|
|
407
|
-
open(ws, ctx) { ctx.ws.join('lobby') },
|
|
408
|
-
message(ws, ctx, data) { ctx.ws.sendRoom('lobby', { text: data.toString() }) },
|
|
409
|
-
})
|
|
410
|
-
|
|
411
|
-
// Error handling
|
|
412
|
-
app.onError((err) => {
|
|
413
|
-
if (err instanceof HttpError) return Response.json({ error: err.message }, { status: err.status })
|
|
414
|
-
return Response.json({ error: 'Internal error' }, { status: 500 })
|
|
504
|
+
app.post('/api/chat', async (req, ctx) => {
|
|
505
|
+
if (!ctx.user) return new Response('Unauthorized', { status: 401 })
|
|
506
|
+
const { messages } = await req.json()
|
|
507
|
+
return ctx.agent.chatStreamResponse({ messages })
|
|
415
508
|
})
|
|
416
509
|
|
|
417
510
|
serve(app, { port: 3000 })
|
|
@@ -432,6 +525,7 @@ weifuwu/
|
|
|
432
525
|
│ ├── types.ts
|
|
433
526
|
│ ├── core/ ← serve, router, ws, trace, logger
|
|
434
527
|
│ ├── middleware/ ← cors, helmet, compress, rate-limit, static, upload
|
|
528
|
+
│ ├── ai/ ← AI + agent middleware
|
|
435
529
|
│ ├── postgres/
|
|
436
530
|
│ ├── redis/
|
|
437
531
|
│ ├── react/ ← react SSR (render, navigation, client)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Middleware, Context } from '../types.ts';
|
|
2
|
+
import type { AiOptions } from '../ai/types.ts';
|
|
3
|
+
declare module '../types.ts' {
|
|
4
|
+
interface Context {
|
|
5
|
+
/**
|
|
6
|
+
* AI agent — LLM integration with session management, tool calling,
|
|
7
|
+
* knowledge retrieval, and streaming.
|
|
8
|
+
* Injected by the `agent()` middleware.
|
|
9
|
+
*/
|
|
10
|
+
agent: Agent;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export interface Agent {
|
|
14
|
+
/**
|
|
15
|
+
* Chat with the agent (non-streaming). Returns the final response
|
|
16
|
+
* after tool calls and knowledge retrieval.
|
|
17
|
+
*/
|
|
18
|
+
chat(prompt: string, opts?: AgentChatOptions): Promise<AgentChatResult>;
|
|
19
|
+
/**
|
|
20
|
+
* Stream the agent's response as an SSE Response.
|
|
21
|
+
* Compatible with `useChat` from @ai-sdk/react.
|
|
22
|
+
*/
|
|
23
|
+
chatStreamResponse(opts: AgentStreamOptions): Response;
|
|
24
|
+
/** The underlying language model. */
|
|
25
|
+
model: unknown;
|
|
26
|
+
/** Default system prompt. */
|
|
27
|
+
system: string | undefined;
|
|
28
|
+
}
|
|
29
|
+
export interface AgentChatOptions {
|
|
30
|
+
/** Override system prompt for this call. */
|
|
31
|
+
system?: string;
|
|
32
|
+
/** Chat history messages (from useChat). */
|
|
33
|
+
messages?: Array<{
|
|
34
|
+
role: string;
|
|
35
|
+
content: string;
|
|
36
|
+
[key: string]: unknown;
|
|
37
|
+
}>;
|
|
38
|
+
/** Override temperature. */
|
|
39
|
+
temperature?: number;
|
|
40
|
+
/** Override max tokens. */
|
|
41
|
+
maxTokens?: number;
|
|
42
|
+
/** Override max steps. */
|
|
43
|
+
maxSteps?: number;
|
|
44
|
+
}
|
|
45
|
+
export interface AgentStreamOptions {
|
|
46
|
+
/** User prompt or full message list. */
|
|
47
|
+
messages: Array<{
|
|
48
|
+
role: string;
|
|
49
|
+
content: string;
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
}>;
|
|
52
|
+
/** Override system prompt. */
|
|
53
|
+
system?: string;
|
|
54
|
+
}
|
|
55
|
+
export type AgentChatResult = Record<string, any>;
|
|
56
|
+
export interface AgentOptions extends AiOptions {
|
|
57
|
+
/**
|
|
58
|
+
* Knowledge base for RAG. Called before every chat to inject context.
|
|
59
|
+
*/
|
|
60
|
+
knowledge?: {
|
|
61
|
+
/**
|
|
62
|
+
* Search for relevant documents given a user query.
|
|
63
|
+
* Results are injected into the system prompt.
|
|
64
|
+
*/
|
|
65
|
+
search: (query: string, ctx: Context) => Promise<Array<{
|
|
66
|
+
content: string;
|
|
67
|
+
score?: number;
|
|
68
|
+
}>>;
|
|
69
|
+
/** Maximum results to inject (default: 3). */
|
|
70
|
+
topK?: number;
|
|
71
|
+
/** Minimum relevance score (default: 0). */
|
|
72
|
+
minScore?: number;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Named sub-agents with their own model, system prompt, and tools.
|
|
76
|
+
* Access via ctx.agent.agents[name].
|
|
77
|
+
*/
|
|
78
|
+
agents?: Record<string, {
|
|
79
|
+
model?: unknown;
|
|
80
|
+
system?: string;
|
|
81
|
+
tools?: Record<string, unknown>;
|
|
82
|
+
}>;
|
|
83
|
+
/**
|
|
84
|
+
* Enable sandbox integration. When true and ctx.sandbox is available,
|
|
85
|
+
* the agent automatically uses it for file operations.
|
|
86
|
+
*/
|
|
87
|
+
sandbox?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Store for session persistence. Save/load conversation history.
|
|
90
|
+
*/
|
|
91
|
+
store?: {
|
|
92
|
+
save: (sessionId: string, messages: unknown[]) => Promise<void>;
|
|
93
|
+
load: (sessionId: string) => Promise<unknown[]>;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* AI Agent middleware — injects `ctx.agent` for LLM-powered conversations.
|
|
98
|
+
*
|
|
99
|
+
* Built on the Vercel AI SDK. Supports:
|
|
100
|
+
* - Multi-turn conversations with session history
|
|
101
|
+
* - Tool calling with automatic loops (maxSteps)
|
|
102
|
+
* - Knowledge retrieval (RAG) via `knowledge.search`
|
|
103
|
+
* - Streaming SSE responses compatible with `useChat` from @ai-sdk/react
|
|
104
|
+
* - Named sub-agents for role-based delegation
|
|
105
|
+
* - Sandbox integration
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* import { agent } from 'weifuwu'
|
|
110
|
+
* import { openai } from '@ai-sdk/openai'
|
|
111
|
+
*
|
|
112
|
+
* app.use(agent({
|
|
113
|
+
* model: openai('gpt-4o'),
|
|
114
|
+
* system: 'You are a helpful assistant.',
|
|
115
|
+
* knowledge: {
|
|
116
|
+
* search: async (query, ctx) => ctx.sql`...pgvector...`,
|
|
117
|
+
* },
|
|
118
|
+
* tools: { getWeather: tool({...}) },
|
|
119
|
+
* }))
|
|
120
|
+
*
|
|
121
|
+
* app.post('/api/chat', async (req, ctx) => {
|
|
122
|
+
* const { messages } = await req.json()
|
|
123
|
+
* return ctx.agent.chatStreamResponse({ messages })
|
|
124
|
+
* })
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export declare function agent(opts: AgentOptions): Middleware;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Middleware } from '../types.ts';
|
|
2
|
+
import type { AiOptions } from './types.ts';
|
|
3
|
+
export type { AiOptions, Ai } from './types.ts';
|
|
4
|
+
/**
|
|
5
|
+
* AI middleware — injects `ctx.ai` for LLM integration via the Vercel AI SDK.
|
|
6
|
+
*
|
|
7
|
+
* Requires a language model provider (e.g. `@ai-sdk/openai`, `@ai-sdk/anthropic`).
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { ai } from 'weifuwu'
|
|
12
|
+
* import { openai } from '@ai-sdk/openai'
|
|
13
|
+
*
|
|
14
|
+
* app.use(ai({
|
|
15
|
+
* model: openai('gpt-4o'),
|
|
16
|
+
* system: 'You are a helpful assistant.',
|
|
17
|
+
* }))
|
|
18
|
+
*
|
|
19
|
+
* app.post('/api/chat', async (req, ctx) => {
|
|
20
|
+
* const { prompt } = await req.json()
|
|
21
|
+
* const result = await ctx.ai.generateText({ prompt })
|
|
22
|
+
* return Response.json({ text: result.text })
|
|
23
|
+
* })
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function ai(opts: AiOptions): Middleware;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
declare module '../types.ts' {
|
|
2
|
+
interface Context {
|
|
3
|
+
/** AI / LLM integration. Injected by the `ai()` middleware. */
|
|
4
|
+
ai: Ai;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export interface Ai {
|
|
8
|
+
/**
|
|
9
|
+
* Generate text (non-streaming). See `generateText` from the `ai` package.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const { text, toolCalls, steps } = await ctx.ai.generateText({
|
|
13
|
+
* prompt: 'What is the weather?',
|
|
14
|
+
* })
|
|
15
|
+
*/
|
|
16
|
+
generateText(opts: GenerateTextParams): Promise<AiGenerateTextResult>;
|
|
17
|
+
/** The language model instance. */
|
|
18
|
+
model: unknown;
|
|
19
|
+
/** Default system prompt. */
|
|
20
|
+
system: string | undefined;
|
|
21
|
+
}
|
|
22
|
+
export type AiGenerateTextResult = Awaited<ReturnType<typeof import('ai').generateText<any, any, any>>>;
|
|
23
|
+
export interface AiOptions {
|
|
24
|
+
/**
|
|
25
|
+
* Language model instance (from @ai-sdk/openai, @ai-sdk/anthropic, etc.).
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* import { openai } from '@ai-sdk/openai'
|
|
29
|
+
* ai({ model: openai('gpt-4o') })
|
|
30
|
+
*/
|
|
31
|
+
model: unknown;
|
|
32
|
+
/** System prompt added to every request. */
|
|
33
|
+
system?: string;
|
|
34
|
+
/** Tools available for tool calling. */
|
|
35
|
+
tools?: any;
|
|
36
|
+
/** Maximum agent steps for tool-calling loops (default: 1). */
|
|
37
|
+
maxSteps?: number;
|
|
38
|
+
/** Default temperature (0–2). */
|
|
39
|
+
temperature?: number;
|
|
40
|
+
/** Default max output tokens. */
|
|
41
|
+
maxTokens?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface GenerateTextParams {
|
|
44
|
+
/** User prompt. */
|
|
45
|
+
prompt: string;
|
|
46
|
+
/** System prompt override. */
|
|
47
|
+
system?: string;
|
|
48
|
+
/** Chat history messages. */
|
|
49
|
+
messages?: Array<{
|
|
50
|
+
role: string;
|
|
51
|
+
content: string;
|
|
52
|
+
} & Record<string, any>>;
|
|
53
|
+
/** Override temperature. */
|
|
54
|
+
temperature?: number;
|
|
55
|
+
/** Override max tokens. */
|
|
56
|
+
maxTokens?: number;
|
|
57
|
+
/** Override max steps. */
|
|
58
|
+
maxSteps?: number;
|
|
59
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type { Context, Handler, Middleware, ErrorHandler, WsContext, WebSocket } from './types.ts';
|
|
2
2
|
export { HttpError } from './types.ts';
|
|
3
|
+
export type { User } from './types.ts';
|
|
3
4
|
export { currentTraceId, currentTrace, runWithTrace, traceElapsed, trace } from './core/trace.ts';
|
|
4
5
|
export type { TraceContext, TraceInjected, TraceOptions } from './core/trace.ts';
|
|
5
6
|
export { serve, DEFAULT_MAX_BODY } from './core/serve.ts';
|
|
@@ -10,10 +11,14 @@ export { logger } from './core/logger.ts';
|
|
|
10
11
|
export type { LoggerOptions } from './core/logger.ts';
|
|
11
12
|
export { cors } from './middleware/cors.ts';
|
|
12
13
|
export type { CORSOptions } from './middleware/cors.ts';
|
|
14
|
+
export { auth } from './middleware/auth.ts';
|
|
15
|
+
export type { AuthOptions } from './middleware/auth.ts';
|
|
13
16
|
export { serveStatic } from './middleware/static.ts';
|
|
14
17
|
export type { ServeStaticOptions } from './middleware/static.ts';
|
|
15
18
|
export { upload } from './middleware/upload.ts';
|
|
16
19
|
export type { UploadOptions, UploadedFile, UploadModule } from './middleware/upload.ts';
|
|
20
|
+
export { sandbox } from './middleware/sandbox.ts';
|
|
21
|
+
export type { SandboxOptions, Sandbox } from './middleware/sandbox.ts';
|
|
17
22
|
export { esbuildDev } from './middleware/esbuild-dev.ts';
|
|
18
23
|
export type { EsbuildDevEntry, EsbuildDevOptions } from './middleware/esbuild-dev.ts';
|
|
19
24
|
export { tailwindDev } from './middleware/tailwind-dev.ts';
|
|
@@ -37,3 +42,7 @@ export type { QueueOptions, QueueJob, Queue, QueueInjected } from './queue/types
|
|
|
37
42
|
export { react, reactRouter } from './react/index.ts';
|
|
38
43
|
export type { ReactOptions, RenderOptions, ReactRouterOptions, ReactAppOptions } from './react/types.ts';
|
|
39
44
|
export { useServerData, ServerDataContext, Link, ErrorBoundary } from './react/index.ts';
|
|
45
|
+
export { ai } from './ai/index.ts';
|
|
46
|
+
export type { AiOptions, Ai } from './ai/index.ts';
|
|
47
|
+
export { agent } from './ai/agent.ts';
|
|
48
|
+
export type { AgentOptions, Agent } from './ai/agent.ts';
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ var HttpError = class extends Error {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
// src/core/trace.ts
|
|
12
|
-
import
|
|
12
|
+
import crypto2 from "node:crypto";
|
|
13
13
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
14
14
|
var als = new AsyncLocalStorage();
|
|
15
15
|
function currentTraceId() {
|
|
@@ -19,7 +19,7 @@ function currentTrace() {
|
|
|
19
19
|
return als.getStore();
|
|
20
20
|
}
|
|
21
21
|
function runWithTrace(incomingTraceId, fn) {
|
|
22
|
-
const traceId = incomingTraceId ||
|
|
22
|
+
const traceId = incomingTraceId || crypto2.randomUUID();
|
|
23
23
|
const startTime = Date.now();
|
|
24
24
|
return als.run({ traceId, startTime }, fn);
|
|
25
25
|
}
|
|
@@ -30,7 +30,7 @@ function traceElapsed() {
|
|
|
30
30
|
}
|
|
31
31
|
function trace(options) {
|
|
32
32
|
const header = options?.header ?? "X-Request-ID";
|
|
33
|
-
const gen = options?.generator ?? (() =>
|
|
33
|
+
const gen = options?.generator ?? (() => crypto2.randomUUID());
|
|
34
34
|
return async (req, ctx, next) => {
|
|
35
35
|
const existing = req.headers.get(header);
|
|
36
36
|
const requestId = existing ?? gen();
|
|
@@ -225,14 +225,14 @@ function serve(router, options) {
|
|
|
225
225
|
if (!server.listening) return;
|
|
226
226
|
server.close();
|
|
227
227
|
server.closeIdleConnections();
|
|
228
|
-
return new Promise((
|
|
228
|
+
return new Promise((resolve6) => {
|
|
229
229
|
const timer = setTimeout(() => {
|
|
230
230
|
server.closeAllConnections();
|
|
231
|
-
|
|
231
|
+
resolve6();
|
|
232
232
|
}, timeoutMs);
|
|
233
233
|
server.on("close", () => {
|
|
234
234
|
clearTimeout(timer);
|
|
235
|
-
|
|
235
|
+
resolve6();
|
|
236
236
|
});
|
|
237
237
|
});
|
|
238
238
|
}
|
|
@@ -277,7 +277,7 @@ function createHub(opts) {
|
|
|
277
277
|
}
|
|
278
278
|
});
|
|
279
279
|
}
|
|
280
|
-
function
|
|
280
|
+
function join4(key, ws) {
|
|
281
281
|
if (!channels.has(key)) {
|
|
282
282
|
channels.set(key, /* @__PURE__ */ new Set());
|
|
283
283
|
redisSub?.subscribe(`${prefix}${key}`);
|
|
@@ -339,7 +339,7 @@ function createHub(opts) {
|
|
|
339
339
|
redisPub = void 0;
|
|
340
340
|
redisSub = null;
|
|
341
341
|
}
|
|
342
|
-
return { join:
|
|
342
|
+
return { join: join4, leave, broadcast, close };
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
// src/core/ws.ts
|
|
@@ -906,6 +906,82 @@ function cors(options) {
|
|
|
906
906
|
};
|
|
907
907
|
}
|
|
908
908
|
|
|
909
|
+
// src/middleware/auth.ts
|
|
910
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
911
|
+
function base64urlDecode(str) {
|
|
912
|
+
return Buffer.from(str.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf-8");
|
|
913
|
+
}
|
|
914
|
+
function sign(payload, secret) {
|
|
915
|
+
return createHmac("sha256", secret).update(payload).digest("base64url");
|
|
916
|
+
}
|
|
917
|
+
function parseCookie(cookieHeader, name) {
|
|
918
|
+
if (!cookieHeader) return null;
|
|
919
|
+
for (const c of cookieHeader.split(";")) {
|
|
920
|
+
const [key, ...rest] = c.trim().split("=");
|
|
921
|
+
if (key === name) return rest.join("=");
|
|
922
|
+
}
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
function auth(opts) {
|
|
926
|
+
return async (req, ctx, next) => {
|
|
927
|
+
if (opts.jwt) {
|
|
928
|
+
const cookie = opts.jwt.cookie ?? "token";
|
|
929
|
+
const token = parseCookie(req.headers.get("cookie"), cookie) ?? req.headers.get("authorization")?.replace(/^Bearer\s+/i, "") ?? ctx.query.access_token;
|
|
930
|
+
if (token) {
|
|
931
|
+
try {
|
|
932
|
+
const [headerB64, payloadB64, sigB64] = token.split(".");
|
|
933
|
+
const expectedSig = sign(`${headerB64}.${payloadB64}`, opts.jwt.secret);
|
|
934
|
+
const sigBuf = Buffer.from(sigB64, "base64url");
|
|
935
|
+
const expectedBuf = Buffer.from(expectedSig, "base64url");
|
|
936
|
+
if (sigBuf.length === expectedBuf.length && timingSafeEqual(sigBuf, expectedBuf)) {
|
|
937
|
+
const payload = JSON.parse(base64urlDecode(payloadB64));
|
|
938
|
+
ctx.user = {
|
|
939
|
+
id: payload.sub ?? payload.id ?? "unknown",
|
|
940
|
+
role: payload.role,
|
|
941
|
+
tenant: payload.tenant,
|
|
942
|
+
...payload
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
} catch {
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (opts.session && !ctx.user) {
|
|
950
|
+
const cookie = opts.session.cookie ?? "session";
|
|
951
|
+
const raw = parseCookie(req.headers.get("cookie"), cookie);
|
|
952
|
+
if (raw) {
|
|
953
|
+
try {
|
|
954
|
+
const [dataB64, sigB64] = raw.split(".");
|
|
955
|
+
const expectedSig = sign(dataB64, opts.session.secret);
|
|
956
|
+
const sigBuf = Buffer.from(sigB64, "base64url");
|
|
957
|
+
const expectedBuf = Buffer.from(expectedSig, "base64url");
|
|
958
|
+
if (sigBuf.length === expectedBuf.length && timingSafeEqual(sigBuf, expectedBuf)) {
|
|
959
|
+
const data = JSON.parse(base64urlDecode(dataB64));
|
|
960
|
+
const user = await opts.session.loadUser(data, ctx);
|
|
961
|
+
if (user) ctx.user = user;
|
|
962
|
+
}
|
|
963
|
+
} catch {
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
if (opts.apiKey && !ctx.user) {
|
|
968
|
+
const header = opts.apiKey.header ?? "authorization";
|
|
969
|
+
const prefix = opts.apiKey.prefix ?? "Bearer ";
|
|
970
|
+
let raw = req.headers.get(header);
|
|
971
|
+
if (!raw && opts.apiKey.query) {
|
|
972
|
+
raw = ctx.query[opts.apiKey.query];
|
|
973
|
+
if (raw) raw = prefix + raw;
|
|
974
|
+
}
|
|
975
|
+
if (raw?.startsWith(prefix)) {
|
|
976
|
+
const key = raw.slice(prefix.length);
|
|
977
|
+
const user = await opts.apiKey.validate(key, ctx);
|
|
978
|
+
if (user) ctx.user = user;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return next(req, ctx);
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
|
|
909
985
|
// src/middleware/static.ts
|
|
910
986
|
import { open, realpath } from "node:fs/promises";
|
|
911
987
|
import { extname, resolve, normalize, sep } from "node:path";
|
|
@@ -1113,9 +1189,77 @@ function upload(options) {
|
|
|
1113
1189
|
return mw;
|
|
1114
1190
|
}
|
|
1115
1191
|
|
|
1192
|
+
// src/middleware/sandbox.ts
|
|
1193
|
+
import { join as join2, resolve as resolve2, normalize as normalize2 } from "node:path";
|
|
1194
|
+
import { mkdir as mkdir2, readFile, writeFile as writeFile2, rm } from "node:fs/promises";
|
|
1195
|
+
import { exec as cpExec } from "node:child_process";
|
|
1196
|
+
import { promisify } from "node:util";
|
|
1197
|
+
var runExec = promisify(cpExec);
|
|
1198
|
+
function sandbox(opts = {}) {
|
|
1199
|
+
const base = resolve2(opts.baseDir ?? "/tmp/weifuwu-sandboxes");
|
|
1200
|
+
const defaultTimeout = opts.timeout ?? 3e4;
|
|
1201
|
+
return async (req, ctx, next) => {
|
|
1202
|
+
let sessionId;
|
|
1203
|
+
if (opts.isolateBy === "user" && ctx.user) {
|
|
1204
|
+
sessionId = ctx.user.id ?? "default";
|
|
1205
|
+
} else {
|
|
1206
|
+
sessionId = crypto.randomUUID();
|
|
1207
|
+
}
|
|
1208
|
+
const workDir = join2(base, encodeURIComponent(sessionId));
|
|
1209
|
+
await mkdir2(workDir, { recursive: true });
|
|
1210
|
+
function safePath(p) {
|
|
1211
|
+
const resolved = resolve2(workDir, normalize2(p).replace(/^[/\\]+/, ""));
|
|
1212
|
+
if (!resolved.startsWith(workDir + "/") && resolved !== workDir) {
|
|
1213
|
+
throw new Error(`Path escape rejected: ${p}`);
|
|
1214
|
+
}
|
|
1215
|
+
return resolved;
|
|
1216
|
+
}
|
|
1217
|
+
ctx.sandbox = {
|
|
1218
|
+
workDir,
|
|
1219
|
+
async readFile(path) {
|
|
1220
|
+
return readFile(safePath(path), "utf-8");
|
|
1221
|
+
},
|
|
1222
|
+
async writeFile(path, content) {
|
|
1223
|
+
const resolved = safePath(path);
|
|
1224
|
+
await mkdir2(join2(resolved, ".."), { recursive: true });
|
|
1225
|
+
return writeFile2(resolved, content, "utf-8");
|
|
1226
|
+
},
|
|
1227
|
+
async exec(cmd, execOpts) {
|
|
1228
|
+
const cwd = execOpts?.cwd ? safePath(execOpts.cwd) : workDir;
|
|
1229
|
+
const { stdout, stderr } = await runExec(cmd, {
|
|
1230
|
+
cwd,
|
|
1231
|
+
timeout: execOpts?.timeout ?? defaultTimeout,
|
|
1232
|
+
env: {
|
|
1233
|
+
...process.env,
|
|
1234
|
+
HOME: workDir,
|
|
1235
|
+
PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin"
|
|
1236
|
+
},
|
|
1237
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1238
|
+
// 10MB
|
|
1239
|
+
});
|
|
1240
|
+
return { stdout, stderr };
|
|
1241
|
+
},
|
|
1242
|
+
async grep(pattern) {
|
|
1243
|
+
const { stdout } = await runExec(
|
|
1244
|
+
`grep -rn --include='*' "${pattern.replace(/"/g, '\\"')}" . 2>/dev/null || true`,
|
|
1245
|
+
{ cwd: workDir, timeout: 1e4 }
|
|
1246
|
+
);
|
|
1247
|
+
return stdout;
|
|
1248
|
+
},
|
|
1249
|
+
async destroy() {
|
|
1250
|
+
await rm(workDir, { recursive: true, force: true });
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
try {
|
|
1254
|
+
return await next(req, ctx);
|
|
1255
|
+
} finally {
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1116
1260
|
// src/middleware/esbuild-dev.ts
|
|
1117
|
-
import { resolve as
|
|
1118
|
-
import { stat, readFile, realpath as realpath2 } from "node:fs/promises";
|
|
1261
|
+
import { resolve as resolve3, relative, dirname } from "node:path";
|
|
1262
|
+
import { stat, readFile as readFile2, realpath as realpath2 } from "node:fs/promises";
|
|
1119
1263
|
function escapeHtml(s) {
|
|
1120
1264
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1121
1265
|
}
|
|
@@ -1160,14 +1304,14 @@ async function collectDeps(entryPath) {
|
|
|
1160
1304
|
continue;
|
|
1161
1305
|
}
|
|
1162
1306
|
try {
|
|
1163
|
-
const content = await
|
|
1307
|
+
const content = await readFile2(p, "utf-8");
|
|
1164
1308
|
const importRe = /from\s+['"]([^'"]+)['"]|import\s+['"]([^'"]+)['"]/g;
|
|
1165
1309
|
let m;
|
|
1166
1310
|
while ((m = importRe.exec(content)) !== null) {
|
|
1167
1311
|
const spec = m[1] ?? m[2];
|
|
1168
1312
|
if (spec.startsWith(".") || spec.startsWith("/")) {
|
|
1169
1313
|
const dir = dirname(p);
|
|
1170
|
-
const resolved =
|
|
1314
|
+
const resolved = resolve3(dir, spec);
|
|
1171
1315
|
for (const ext of ["", ".ts", ".tsx", ".js", ".jsx", ".mjs", "/index.ts", "/index.tsx", "/index.js"]) {
|
|
1172
1316
|
const candidate = resolved + ext;
|
|
1173
1317
|
if (!visited.has(candidate)) {
|
|
@@ -1274,7 +1418,7 @@ ${entries2.join("\n")}
|
|
|
1274
1418
|
format: entry.format ?? "esm",
|
|
1275
1419
|
sourcemap: entry.sourcemap ?? false,
|
|
1276
1420
|
splitting: entry.splitting ?? false,
|
|
1277
|
-
...entry.splitting ? { outdir:
|
|
1421
|
+
...entry.splitting ? { outdir: resolve3(".weifuwu-esbuild-out") } : {},
|
|
1278
1422
|
define: entry.define,
|
|
1279
1423
|
loader: entry.loader,
|
|
1280
1424
|
write: false,
|
|
@@ -1295,7 +1439,7 @@ ${entries2.join("\n")}
|
|
|
1295
1439
|
}]
|
|
1296
1440
|
};
|
|
1297
1441
|
} else {
|
|
1298
|
-
entryAbs =
|
|
1442
|
+
entryAbs = resolve3(entry.entry);
|
|
1299
1443
|
buildOpts = {
|
|
1300
1444
|
entryPoints: [entryAbs],
|
|
1301
1445
|
bundle: entry.bundle ?? true,
|
|
@@ -1412,7 +1556,7 @@ ${entries2.join("\n")}
|
|
|
1412
1556
|
}
|
|
1413
1557
|
const { code, etag, chunks } = await compile(config);
|
|
1414
1558
|
const depEntry = config.clientRouter?.routes ?? (config.clientRouter?.pages ? Object.values(config.clientRouter.pages)[0] : void 0) ?? config.entry;
|
|
1415
|
-
const deps = await collectDeps(
|
|
1559
|
+
const deps = await collectDeps(resolve3(depEntry));
|
|
1416
1560
|
if (cache === "memory") {
|
|
1417
1561
|
cacheStore.set(pathname, { code, etag, files: deps, chunks });
|
|
1418
1562
|
if (chunks) {
|
|
@@ -1447,8 +1591,8 @@ function hashCode(s) {
|
|
|
1447
1591
|
}
|
|
1448
1592
|
|
|
1449
1593
|
// src/middleware/tailwind-dev.ts
|
|
1450
|
-
import { resolve as
|
|
1451
|
-
import { stat as stat2, readFile as
|
|
1594
|
+
import { resolve as resolve4 } from "node:path";
|
|
1595
|
+
import { stat as stat2, readFile as readFile3, glob } from "node:fs/promises";
|
|
1452
1596
|
function escapeHtml2(s) {
|
|
1453
1597
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1454
1598
|
}
|
|
@@ -1482,11 +1626,11 @@ var CLASS_CONTEXT_RE = /(?:class(?:Name)?\s*[=:]\s*["'`]([^"'`]+)["'`]|["'`]([^"
|
|
|
1482
1626
|
async function extractCandidates(patterns, root) {
|
|
1483
1627
|
const seen = /* @__PURE__ */ new Set();
|
|
1484
1628
|
for (const pattern of patterns) {
|
|
1485
|
-
const resolved =
|
|
1629
|
+
const resolved = resolve4(root, pattern);
|
|
1486
1630
|
try {
|
|
1487
1631
|
for await (const file of glob(resolved)) {
|
|
1488
1632
|
try {
|
|
1489
|
-
const content = await
|
|
1633
|
+
const content = await readFile3(file, "utf-8");
|
|
1490
1634
|
for (const m of content.matchAll(CLASS_CONTEXT_RE)) {
|
|
1491
1635
|
const str = m[1] ?? m[2];
|
|
1492
1636
|
if (!str) continue;
|
|
@@ -1553,9 +1697,9 @@ ${String(err)}`;
|
|
|
1553
1697
|
}
|
|
1554
1698
|
async function compileCss(entry) {
|
|
1555
1699
|
const compile = await getCompile();
|
|
1556
|
-
const entryAbs =
|
|
1557
|
-
const cssSource = await
|
|
1558
|
-
const baseDir =
|
|
1700
|
+
const entryAbs = resolve4(entry.entry);
|
|
1701
|
+
const cssSource = await readFile3(entryAbs, "utf-8");
|
|
1702
|
+
const baseDir = resolve4(entry.entry, "..");
|
|
1559
1703
|
const deps = [entryAbs];
|
|
1560
1704
|
const result = await compile(cssSource, {
|
|
1561
1705
|
base: baseDir,
|
|
@@ -1566,7 +1710,7 @@ ${String(err)}`;
|
|
|
1566
1710
|
const contentPatterns = entry.content ?? [];
|
|
1567
1711
|
for (const src of result.sources) {
|
|
1568
1712
|
if (!src.negated) {
|
|
1569
|
-
contentPatterns.push(
|
|
1713
|
+
contentPatterns.push(resolve4(src.base, src.pattern));
|
|
1570
1714
|
}
|
|
1571
1715
|
}
|
|
1572
1716
|
let candidates = [];
|
|
@@ -1576,7 +1720,7 @@ ${String(err)}`;
|
|
|
1576
1720
|
const css = result.build(candidates);
|
|
1577
1721
|
for (const pattern of contentPatterns) {
|
|
1578
1722
|
try {
|
|
1579
|
-
for await (const file of glob(
|
|
1723
|
+
for await (const file of glob(resolve4(baseDir, pattern))) {
|
|
1580
1724
|
if (!deps.includes(file)) deps.push(file);
|
|
1581
1725
|
}
|
|
1582
1726
|
} catch {
|
|
@@ -1746,10 +1890,10 @@ function addRateLimitHeaders(res, limit, remaining, reset) {
|
|
|
1746
1890
|
|
|
1747
1891
|
// src/middleware/compress.ts
|
|
1748
1892
|
import { constants, brotliCompress, gzip, deflate } from "node:zlib";
|
|
1749
|
-
import { promisify } from "node:util";
|
|
1750
|
-
var brotliCompressAsync =
|
|
1751
|
-
var gzipAsync =
|
|
1752
|
-
var deflateAsync =
|
|
1893
|
+
import { promisify as promisify2 } from "node:util";
|
|
1894
|
+
var brotliCompressAsync = promisify2(brotliCompress);
|
|
1895
|
+
var gzipAsync = promisify2(gzip);
|
|
1896
|
+
var deflateAsync = promisify2(deflate);
|
|
1753
1897
|
function compress(options) {
|
|
1754
1898
|
const level = options?.level ?? 6;
|
|
1755
1899
|
const threshold = options?.threshold ?? 1024;
|
|
@@ -2154,7 +2298,7 @@ function redis(opts) {
|
|
|
2154
2298
|
}
|
|
2155
2299
|
|
|
2156
2300
|
// src/queue/index.ts
|
|
2157
|
-
import
|
|
2301
|
+
import crypto3 from "node:crypto";
|
|
2158
2302
|
import { Redis as IORedis2 } from "ioredis";
|
|
2159
2303
|
|
|
2160
2304
|
// src/queue/cron.ts
|
|
@@ -2268,7 +2412,7 @@ function queue(opts) {
|
|
|
2268
2412
|
}
|
|
2269
2413
|
if (job.schedule) {
|
|
2270
2414
|
try {
|
|
2271
|
-
await insert({ ...job, id:
|
|
2415
|
+
await insert({ ...job, id: crypto3.randomUUID(), runAt: cronNext(job.schedule), createdAt: Date.now() });
|
|
2272
2416
|
} catch {
|
|
2273
2417
|
}
|
|
2274
2418
|
}
|
|
@@ -2294,7 +2438,7 @@ function queue(opts) {
|
|
|
2294
2438
|
});
|
|
2295
2439
|
const q = mw;
|
|
2296
2440
|
mw.add = function add(type, payload, opts2) {
|
|
2297
|
-
const id =
|
|
2441
|
+
const id = crypto3.randomUUID();
|
|
2298
2442
|
let runAt;
|
|
2299
2443
|
if (opts2?.schedule) {
|
|
2300
2444
|
try {
|
|
@@ -2412,7 +2556,7 @@ function queue(opts) {
|
|
|
2412
2556
|
return r;
|
|
2413
2557
|
};
|
|
2414
2558
|
q.cron = function(pattern, handler) {
|
|
2415
|
-
const id = "__cron_" + pattern.replace(/[^a-zA-Z0-9]/g, "_") + "_" +
|
|
2559
|
+
const id = "__cron_" + pattern.replace(/[^a-zA-Z0-9]/g, "_") + "_" + crypto3.randomUUID().slice(0, 8);
|
|
2416
2560
|
q.process(id, async () => {
|
|
2417
2561
|
await handler();
|
|
2418
2562
|
});
|
|
@@ -2427,8 +2571,8 @@ import { createElement as createElement2 } from "react";
|
|
|
2427
2571
|
import { renderToReadableStream } from "react-dom/server";
|
|
2428
2572
|
|
|
2429
2573
|
// src/react/compile.ts
|
|
2430
|
-
import { mkdir as
|
|
2431
|
-
import { resolve as
|
|
2574
|
+
import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile4 } from "node:fs/promises";
|
|
2575
|
+
import { resolve as resolve5, join as join3, dirname as dirname2 } from "node:path";
|
|
2432
2576
|
import { createHash } from "node:crypto";
|
|
2433
2577
|
import { existsSync } from "node:fs";
|
|
2434
2578
|
var EXTERNAL_PKGS = [
|
|
@@ -2481,14 +2625,14 @@ function setReactCacheDir(dir) {
|
|
|
2481
2625
|
}
|
|
2482
2626
|
function getCacheDir() {
|
|
2483
2627
|
if (_cacheDir) return _cacheDir;
|
|
2484
|
-
_cacheDir =
|
|
2628
|
+
_cacheDir = join3(process.cwd(), "node_modules", ".weifuwu", "react");
|
|
2485
2629
|
return _cacheDir;
|
|
2486
2630
|
}
|
|
2487
2631
|
async function loadTsxModule(entryPath) {
|
|
2488
|
-
const abs =
|
|
2632
|
+
const abs = resolve5(entryPath);
|
|
2489
2633
|
let source;
|
|
2490
2634
|
try {
|
|
2491
|
-
source = await
|
|
2635
|
+
source = await readFile4(abs, "utf-8");
|
|
2492
2636
|
} catch {
|
|
2493
2637
|
throw new Error(`Cannot read file: ${entryPath}`);
|
|
2494
2638
|
}
|
|
@@ -2498,7 +2642,7 @@ async function loadTsxModule(entryPath) {
|
|
|
2498
2642
|
return memCached.mod;
|
|
2499
2643
|
}
|
|
2500
2644
|
const tmpDir = getCacheDir();
|
|
2501
|
-
const tmpFile =
|
|
2645
|
+
const tmpFile = join3(tmpDir, `${sourceHash}.mjs`);
|
|
2502
2646
|
if (existsSync(tmpFile)) {
|
|
2503
2647
|
try {
|
|
2504
2648
|
const mod2 = await import(tmpFile + "?" + sourceHash);
|
|
@@ -2521,8 +2665,8 @@ async function loadTsxModule(entryPath) {
|
|
|
2521
2665
|
let code = result.outputFiles[0]?.text ?? "";
|
|
2522
2666
|
if (!code) throw new Error(`esbuild produced empty output for ${entryPath}`);
|
|
2523
2667
|
code = rewriteImports(code);
|
|
2524
|
-
await
|
|
2525
|
-
await
|
|
2668
|
+
await mkdir3(dirname2(tmpFile), { recursive: true });
|
|
2669
|
+
await writeFile3(tmpFile, code);
|
|
2526
2670
|
const mod = await import(tmpFile + "?" + sourceHash);
|
|
2527
2671
|
memCache.set(abs, { sourceHash, mod });
|
|
2528
2672
|
return mod;
|
|
@@ -2794,6 +2938,149 @@ function reactRouter(app, routes, opts = {}) {
|
|
|
2794
2938
|
function Link({ href, children, ...props }) {
|
|
2795
2939
|
return createElement2("a", { href, ...props }, children);
|
|
2796
2940
|
}
|
|
2941
|
+
|
|
2942
|
+
// src/ai/index.ts
|
|
2943
|
+
function ai(opts) {
|
|
2944
|
+
let aiModule = null;
|
|
2945
|
+
const model = opts.model;
|
|
2946
|
+
async function getAi() {
|
|
2947
|
+
if (!aiModule) aiModule = await import("ai");
|
|
2948
|
+
return aiModule;
|
|
2949
|
+
}
|
|
2950
|
+
return async (_req, ctx, next) => {
|
|
2951
|
+
ctx.ai = {
|
|
2952
|
+
model,
|
|
2953
|
+
system: opts.system,
|
|
2954
|
+
async generateText(params) {
|
|
2955
|
+
const { generateText } = await getAi();
|
|
2956
|
+
const messages = [...params.messages ?? []];
|
|
2957
|
+
const system = params.system ?? opts.system;
|
|
2958
|
+
if (system) {
|
|
2959
|
+
const hasSystem = messages.some((m) => m.role === "system");
|
|
2960
|
+
if (!hasSystem) {
|
|
2961
|
+
messages.unshift({ role: "system", content: system });
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
const baseOpts = {
|
|
2965
|
+
model,
|
|
2966
|
+
tools: opts.tools,
|
|
2967
|
+
maxSteps: params.maxSteps ?? opts.maxSteps ?? 1,
|
|
2968
|
+
temperature: params.temperature ?? opts.temperature,
|
|
2969
|
+
maxTokens: params.maxTokens ?? opts.maxTokens,
|
|
2970
|
+
abortSignal: _req.signal
|
|
2971
|
+
};
|
|
2972
|
+
if (messages.length > 0) {
|
|
2973
|
+
baseOpts.messages = messages;
|
|
2974
|
+
} else {
|
|
2975
|
+
baseOpts.prompt = params.prompt;
|
|
2976
|
+
}
|
|
2977
|
+
return generateText(baseOpts);
|
|
2978
|
+
}
|
|
2979
|
+
};
|
|
2980
|
+
return next(_req, ctx);
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
// src/ai/agent.ts
|
|
2985
|
+
function agent(opts) {
|
|
2986
|
+
let aiModule = null;
|
|
2987
|
+
async function getAi() {
|
|
2988
|
+
if (!aiModule) aiModule = await import("ai");
|
|
2989
|
+
return aiModule;
|
|
2990
|
+
}
|
|
2991
|
+
return async (req, ctx, next) => {
|
|
2992
|
+
ctx.agent = {
|
|
2993
|
+
model: opts.model,
|
|
2994
|
+
system: opts.system,
|
|
2995
|
+
async chat(prompt, chatOpts = {}) {
|
|
2996
|
+
const { generateText } = await getAi();
|
|
2997
|
+
let knowledgeContext = "";
|
|
2998
|
+
if (opts.knowledge) {
|
|
2999
|
+
try {
|
|
3000
|
+
const results = await opts.knowledge.search(prompt, ctx);
|
|
3001
|
+
const topK = opts.knowledge.topK ?? 3;
|
|
3002
|
+
const minScore = opts.knowledge.minScore ?? 0;
|
|
3003
|
+
const filtered = results.filter((r) => (r.score ?? 1) >= minScore).slice(0, topK);
|
|
3004
|
+
if (filtered.length > 0) {
|
|
3005
|
+
knowledgeContext = "\n\n\u53C2\u8003\u4EE5\u4E0B\u77E5\u8BC6\uFF1A\n" + filtered.map((r, i) => `[${i + 1}] ${r.content}`).join("\n");
|
|
3006
|
+
}
|
|
3007
|
+
} catch {
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
const system = chatOpts.system ?? opts.system ?? "";
|
|
3011
|
+
const messages = [...chatOpts.messages ?? []];
|
|
3012
|
+
if (system) {
|
|
3013
|
+
const hasSystem = messages.some((m) => m.role === "system");
|
|
3014
|
+
if (!hasSystem) {
|
|
3015
|
+
messages.unshift({
|
|
3016
|
+
role: "system",
|
|
3017
|
+
content: knowledgeContext ? system + knowledgeContext : system
|
|
3018
|
+
});
|
|
3019
|
+
} else if (knowledgeContext) {
|
|
3020
|
+
const sysMsg = messages.find((m) => m.role === "system");
|
|
3021
|
+
if (sysMsg) sysMsg.content += knowledgeContext;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
if (prompt && !messages.some((m) => m.role === "user")) {
|
|
3025
|
+
messages.push({ role: "user", content: prompt });
|
|
3026
|
+
}
|
|
3027
|
+
const result = await generateText({
|
|
3028
|
+
model: opts.model,
|
|
3029
|
+
messages,
|
|
3030
|
+
tools: opts.tools,
|
|
3031
|
+
maxSteps: chatOpts.maxSteps ?? opts.maxSteps ?? 1,
|
|
3032
|
+
temperature: chatOpts.temperature ?? opts.temperature,
|
|
3033
|
+
maxTokens: chatOpts.maxTokens ?? opts.maxTokens,
|
|
3034
|
+
abortSignal: req.signal
|
|
3035
|
+
});
|
|
3036
|
+
return {
|
|
3037
|
+
text: result.text,
|
|
3038
|
+
toolCalls: result.toolCalls,
|
|
3039
|
+
toolResults: result.toolResults,
|
|
3040
|
+
steps: result.steps,
|
|
3041
|
+
finishReason: result.finishReason,
|
|
3042
|
+
usage: result.usage
|
|
3043
|
+
};
|
|
3044
|
+
},
|
|
3045
|
+
chatStreamResponse(streamOpts) {
|
|
3046
|
+
const encoder = new TextEncoder();
|
|
3047
|
+
const stream = new ReadableStream({
|
|
3048
|
+
async start(controller) {
|
|
3049
|
+
try {
|
|
3050
|
+
const result = await ctx.agent.chat(
|
|
3051
|
+
streamOpts.messages.filter((m) => m.role === "user").pop()?.content ?? "",
|
|
3052
|
+
{
|
|
3053
|
+
messages: streamOpts.messages,
|
|
3054
|
+
system: streamOpts.system
|
|
3055
|
+
}
|
|
3056
|
+
);
|
|
3057
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "text", content: result.text })}
|
|
3058
|
+
|
|
3059
|
+
`));
|
|
3060
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
3061
|
+
controller.close();
|
|
3062
|
+
} catch (err) {
|
|
3063
|
+
controller.enqueue(encoder.encode(
|
|
3064
|
+
`data: ${JSON.stringify({ type: "error", error: String(err) })}
|
|
3065
|
+
|
|
3066
|
+
`
|
|
3067
|
+
));
|
|
3068
|
+
controller.close();
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
});
|
|
3072
|
+
return new Response(stream, {
|
|
3073
|
+
headers: {
|
|
3074
|
+
"Content-Type": "text/event-stream",
|
|
3075
|
+
"Cache-Control": "no-cache",
|
|
3076
|
+
Connection: "keep-alive"
|
|
3077
|
+
}
|
|
3078
|
+
});
|
|
3079
|
+
}
|
|
3080
|
+
};
|
|
3081
|
+
return next(req, ctx);
|
|
3082
|
+
};
|
|
3083
|
+
}
|
|
2797
3084
|
export {
|
|
2798
3085
|
DEFAULT_MAX_BODY,
|
|
2799
3086
|
ErrorBoundary,
|
|
@@ -2802,6 +3089,9 @@ export {
|
|
|
2802
3089
|
MIGRATIONS_TABLE,
|
|
2803
3090
|
Router,
|
|
2804
3091
|
ServerDataContext,
|
|
3092
|
+
agent,
|
|
3093
|
+
ai,
|
|
3094
|
+
auth,
|
|
2805
3095
|
compress,
|
|
2806
3096
|
cors,
|
|
2807
3097
|
createHub,
|
|
@@ -2818,6 +3108,7 @@ export {
|
|
|
2818
3108
|
reactRouter,
|
|
2819
3109
|
redis,
|
|
2820
3110
|
runWithTrace,
|
|
3111
|
+
sandbox,
|
|
2821
3112
|
serve,
|
|
2822
3113
|
serveStatic,
|
|
2823
3114
|
tailwindDev,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Middleware, User, Context } from '../types.ts';
|
|
2
|
+
export interface AuthOptions {
|
|
3
|
+
/**
|
|
4
|
+
* JWT authentication.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* auth({ jwt: { secret: process.env.JWT_SECRET } })
|
|
8
|
+
*/
|
|
9
|
+
jwt?: {
|
|
10
|
+
/** HMAC secret for HS256. */
|
|
11
|
+
secret: string;
|
|
12
|
+
/** Cookie name for the token (default: 'token'). */
|
|
13
|
+
cookie?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Simple session authentication via signed cookie.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* auth({ session: { secret: '...' } })
|
|
20
|
+
*/
|
|
21
|
+
session?: {
|
|
22
|
+
/** Secret for HMAC cookie signing. */
|
|
23
|
+
secret: string;
|
|
24
|
+
/** Cookie name (default: 'session'). */
|
|
25
|
+
cookie?: string;
|
|
26
|
+
/** Load user from session data. `ctx` has `ctx.sql`, `ctx.redis`, etc. */
|
|
27
|
+
loadUser: (data: Record<string, unknown>, ctx: Context) => Promise<User | null> | User | null;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* API key authentication via header.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* auth({ apiKey: { header: 'x-api-key', keys: ['sk-xxx'] } })
|
|
34
|
+
*/
|
|
35
|
+
apiKey?: {
|
|
36
|
+
/** Header name (default: 'authorization'). */
|
|
37
|
+
header?: string;
|
|
38
|
+
/** Prefix to strip (default: 'Bearer '). */
|
|
39
|
+
prefix?: string;
|
|
40
|
+
/** Query parameter name (e.g. 'api_key'). */
|
|
41
|
+
query?: string;
|
|
42
|
+
/** Validate an API key → User or null. `ctx` has `ctx.sql`, `ctx.redis`, etc. */
|
|
43
|
+
validate: (key: string, ctx: Context) => Promise<User | null> | User | null;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Authentication middleware — injects `ctx.user`.
|
|
48
|
+
*
|
|
49
|
+
* Supports JWT (HS256), signed session cookies, and API keys.
|
|
50
|
+
* When no user is authenticated, requests still proceed —
|
|
51
|
+
* check `ctx.user` in your handlers to enforce auth.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* // JWT
|
|
56
|
+
* app.use(auth({ jwt: { secret: 'my-secret' } }))
|
|
57
|
+
*
|
|
58
|
+
* // Session cookie
|
|
59
|
+
* app.use(auth({
|
|
60
|
+
* session: {
|
|
61
|
+
* secret: '...',
|
|
62
|
+
* loadUser: async (data, ctx) => ctx.sql`SELECT * FROM users WHERE id = ${data.userId}`,
|
|
63
|
+
* },
|
|
64
|
+
* }))
|
|
65
|
+
*
|
|
66
|
+
* // API key
|
|
67
|
+
* app.use(auth({ apiKey: { validate: async (key, ctx) => ctx.sql`...` } }))
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export declare function auth(opts: AuthOptions): Middleware;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Middleware } from '../types.ts';
|
|
2
|
+
declare module '../types.ts' {
|
|
3
|
+
interface Context {
|
|
4
|
+
/** Filesystem sandbox for agent operations. Injected by sandbox(). */
|
|
5
|
+
sandbox: Sandbox;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface SandboxOptions {
|
|
9
|
+
/** Root directory for all workspaces (default: '/tmp/weifuwu-sandboxes'). */
|
|
10
|
+
baseDir?: string;
|
|
11
|
+
/** Default timeout for exec() in ms (default: 30000). */
|
|
12
|
+
timeout?: number;
|
|
13
|
+
/** Isolate workspaces by a ctx field. If 'user', uses ctx.user.id. */
|
|
14
|
+
isolateBy?: 'user';
|
|
15
|
+
}
|
|
16
|
+
export interface ExecResult {
|
|
17
|
+
stdout: string;
|
|
18
|
+
stderr: string;
|
|
19
|
+
}
|
|
20
|
+
export interface Sandbox {
|
|
21
|
+
/** The workspace directory for the current request. */
|
|
22
|
+
workDir: string;
|
|
23
|
+
/** Read a file inside the workspace. */
|
|
24
|
+
readFile(path: string): Promise<string>;
|
|
25
|
+
/** Write a file inside the workspace. Creates parent directories. */
|
|
26
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
27
|
+
/** Execute a command inside the workspace. */
|
|
28
|
+
exec(cmd: string, opts?: {
|
|
29
|
+
timeout?: number;
|
|
30
|
+
cwd?: string;
|
|
31
|
+
}): Promise<ExecResult>;
|
|
32
|
+
/** Grep for a pattern inside the workspace. */
|
|
33
|
+
grep(pattern: string): Promise<string>;
|
|
34
|
+
/** Destroy the workspace (called when the session ends). */
|
|
35
|
+
destroy(): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Filesystem sandbox middleware.
|
|
39
|
+
*
|
|
40
|
+
* Each request gets an isolated workspace directory. File and exec
|
|
41
|
+
* operations are restricted to the workspace — path escapes are rejected.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* app.use(sandbox({ baseDir: '/tmp/workspaces', isolateBy: 'user' }))
|
|
46
|
+
*
|
|
47
|
+
* // In a handler:
|
|
48
|
+
* await ctx.sandbox.writeFile('hello.txt', 'world')
|
|
49
|
+
* const content = await ctx.sandbox.readFile('hello.txt')
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare function sandbox(opts?: SandboxOptions): Middleware;
|
package/dist/types.d.ts
CHANGED
|
@@ -19,6 +19,13 @@ export interface WebSocket {
|
|
|
19
19
|
removeEventListener(event: string, handler: (...args: unknown[]) => void): void;
|
|
20
20
|
}
|
|
21
21
|
export type { Redis, RedisOptions } from 'ioredis';
|
|
22
|
+
/** User injected by auth() middleware. */
|
|
23
|
+
export interface User {
|
|
24
|
+
id: string;
|
|
25
|
+
role?: string;
|
|
26
|
+
tenant?: string;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
22
29
|
export interface WsContext {
|
|
23
30
|
/** Per-connection state object */
|
|
24
31
|
state: Record<string, unknown>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weifuwu",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.32.1",
|
|
5
5
|
"description": "Web-standard HTTP microframework for Node.js — (req, ctx) => Response",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@graphql-tools/schema": "^10",
|
|
35
|
+
"ai": "^7.0.14",
|
|
35
36
|
"graphql": "^16",
|
|
36
37
|
"ioredis": "^5.11.0",
|
|
37
38
|
"postgres": "^3.4.9",
|