weifuwu 0.31.2 → 0.32.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 +131 -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 +327 -40
- package/dist/middleware/auth.d.ts +68 -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,108 @@ 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
|
+
```ts
|
|
374
|
+
import { auth } from 'weifuwu'
|
|
375
|
+
|
|
376
|
+
// JWT
|
|
377
|
+
app.use(auth({ jwt: { secret: process.env.JWT_SECRET } }))
|
|
378
|
+
|
|
379
|
+
// Session cookie
|
|
380
|
+
app.use(auth({
|
|
381
|
+
session: {
|
|
382
|
+
secret: '...',
|
|
383
|
+
loadUser: async (data) => db.findUser(data.userId),
|
|
384
|
+
},
|
|
385
|
+
}))
|
|
386
|
+
|
|
387
|
+
// API key
|
|
388
|
+
app.use(auth({ apiKey: { validate: async (key) => db.findByApiKey(key) } }))
|
|
389
|
+
|
|
390
|
+
// ctx.user is now available
|
|
391
|
+
app.get('/me', (req, ctx) => {
|
|
392
|
+
if (!ctx.user) return new Response('Unauthorized', { status: 401 })
|
|
393
|
+
return Response.json(ctx.user)
|
|
394
|
+
})
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
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.
|
|
398
|
+
|
|
399
|
+
### Sandbox
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
import { sandbox } from 'weifuwu'
|
|
403
|
+
|
|
404
|
+
app.use(sandbox({
|
|
405
|
+
baseDir: '/tmp/workspaces',
|
|
406
|
+
timeout: 30000,
|
|
407
|
+
isolateBy: 'user', // one directory per ctx.user.id
|
|
408
|
+
}))
|
|
409
|
+
|
|
410
|
+
// ctx.sandbox provides isolated file + exec operations
|
|
411
|
+
await ctx.sandbox.writeFile('hello.txt', 'world')
|
|
412
|
+
const content = await ctx.sandbox.readFile('hello.txt')
|
|
413
|
+
const { stdout } = await ctx.sandbox.exec('ls -la')
|
|
414
|
+
await ctx.sandbox.destroy() // clean up workspace
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
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`.
|
|
418
|
+
|
|
316
419
|
### Types
|
|
317
420
|
|
|
318
421
|
| Export | Description |
|
|
@@ -368,50 +471,35 @@ app.onError((err, req, ctx) => {
|
|
|
368
471
|
## Complete example
|
|
369
472
|
|
|
370
473
|
```ts
|
|
371
|
-
import { serve, Router, cors, helmet, compress, logger, trace, rateLimit, postgres, redis,
|
|
474
|
+
import { serve, Router, cors, helmet, compress, logger, trace, rateLimit, postgres, redis, auth, sandbox, HttpError } from 'weifuwu'
|
|
475
|
+
import { agent } from 'weifuwu/agent'
|
|
476
|
+
import { react } from 'weifuwu/react'
|
|
477
|
+
import { openai } from '@ai-sdk/openai'
|
|
372
478
|
|
|
373
479
|
const app = new Router()
|
|
480
|
+
.use(trace())
|
|
481
|
+
.use(logger())
|
|
482
|
+
.use(cors())
|
|
483
|
+
.use(helmet())
|
|
484
|
+
.use(compress())
|
|
485
|
+
.use(postgres())
|
|
486
|
+
.use(redis())
|
|
487
|
+
.use(auth({ jwt: { secret: process.env.JWT_SECRET! } }))
|
|
488
|
+
.use(sandbox({ isolateBy: 'user' }))
|
|
489
|
+
.use(agent({
|
|
490
|
+
model: openai('gpt-4o'),
|
|
491
|
+
system: 'You are a helpful assistant.',
|
|
492
|
+
knowledge: {
|
|
493
|
+
search: async (q, ctx) => ctx.sql`...`,
|
|
494
|
+
},
|
|
495
|
+
sandbox: true,
|
|
496
|
+
}))
|
|
497
|
+
.plugin(react({ pages: { '/': './Chat.tsx' }, layout: './Layout.tsx', tailwind: {} }))
|
|
374
498
|
|
|
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 })
|
|
499
|
+
app.post('/api/chat', async (req, ctx) => {
|
|
500
|
+
if (!ctx.user) return new Response('Unauthorized', { status: 401 })
|
|
501
|
+
const { messages } = await req.json()
|
|
502
|
+
return ctx.agent.chatStreamResponse({ messages })
|
|
415
503
|
})
|
|
416
504
|
|
|
417
505
|
serve(app, { port: 3000 })
|
|
@@ -432,6 +520,7 @@ weifuwu/
|
|
|
432
520
|
│ ├── types.ts
|
|
433
521
|
│ ├── core/ ← serve, router, ws, trace, logger
|
|
434
522
|
│ ├── middleware/ ← cors, helmet, compress, rate-limit, static, upload
|
|
523
|
+
│ ├── ai/ ← AI + agent middleware
|
|
435
524
|
│ ├── postgres/
|
|
436
525
|
│ ├── redis/
|
|
437
526
|
│ ├── 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,78 @@ 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, "");
|
|
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);
|
|
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
|
+
const raw = req.headers.get(header);
|
|
971
|
+
if (raw?.startsWith(prefix)) {
|
|
972
|
+
const key = raw.slice(prefix.length);
|
|
973
|
+
const user = await opts.apiKey.validate(key);
|
|
974
|
+
if (user) ctx.user = user;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return next(req, ctx);
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
|
|
909
981
|
// src/middleware/static.ts
|
|
910
982
|
import { open, realpath } from "node:fs/promises";
|
|
911
983
|
import { extname, resolve, normalize, sep } from "node:path";
|
|
@@ -1113,9 +1185,77 @@ function upload(options) {
|
|
|
1113
1185
|
return mw;
|
|
1114
1186
|
}
|
|
1115
1187
|
|
|
1188
|
+
// src/middleware/sandbox.ts
|
|
1189
|
+
import { join as join2, resolve as resolve2, normalize as normalize2 } from "node:path";
|
|
1190
|
+
import { mkdir as mkdir2, readFile, writeFile as writeFile2, rm } from "node:fs/promises";
|
|
1191
|
+
import { exec as cpExec } from "node:child_process";
|
|
1192
|
+
import { promisify } from "node:util";
|
|
1193
|
+
var runExec = promisify(cpExec);
|
|
1194
|
+
function sandbox(opts = {}) {
|
|
1195
|
+
const base = resolve2(opts.baseDir ?? "/tmp/weifuwu-sandboxes");
|
|
1196
|
+
const defaultTimeout = opts.timeout ?? 3e4;
|
|
1197
|
+
return async (req, ctx, next) => {
|
|
1198
|
+
let sessionId;
|
|
1199
|
+
if (opts.isolateBy === "user" && ctx.user) {
|
|
1200
|
+
sessionId = ctx.user.id ?? "default";
|
|
1201
|
+
} else {
|
|
1202
|
+
sessionId = crypto.randomUUID();
|
|
1203
|
+
}
|
|
1204
|
+
const workDir = join2(base, encodeURIComponent(sessionId));
|
|
1205
|
+
await mkdir2(workDir, { recursive: true });
|
|
1206
|
+
function safePath(p) {
|
|
1207
|
+
const resolved = resolve2(workDir, normalize2(p).replace(/^[/\\]+/, ""));
|
|
1208
|
+
if (!resolved.startsWith(workDir + "/") && resolved !== workDir) {
|
|
1209
|
+
throw new Error(`Path escape rejected: ${p}`);
|
|
1210
|
+
}
|
|
1211
|
+
return resolved;
|
|
1212
|
+
}
|
|
1213
|
+
ctx.sandbox = {
|
|
1214
|
+
workDir,
|
|
1215
|
+
async readFile(path) {
|
|
1216
|
+
return readFile(safePath(path), "utf-8");
|
|
1217
|
+
},
|
|
1218
|
+
async writeFile(path, content) {
|
|
1219
|
+
const resolved = safePath(path);
|
|
1220
|
+
await mkdir2(join2(resolved, ".."), { recursive: true });
|
|
1221
|
+
return writeFile2(resolved, content, "utf-8");
|
|
1222
|
+
},
|
|
1223
|
+
async exec(cmd, execOpts) {
|
|
1224
|
+
const cwd = execOpts?.cwd ? safePath(execOpts.cwd) : workDir;
|
|
1225
|
+
const { stdout, stderr } = await runExec(cmd, {
|
|
1226
|
+
cwd,
|
|
1227
|
+
timeout: execOpts?.timeout ?? defaultTimeout,
|
|
1228
|
+
env: {
|
|
1229
|
+
...process.env,
|
|
1230
|
+
HOME: workDir,
|
|
1231
|
+
PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin"
|
|
1232
|
+
},
|
|
1233
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1234
|
+
// 10MB
|
|
1235
|
+
});
|
|
1236
|
+
return { stdout, stderr };
|
|
1237
|
+
},
|
|
1238
|
+
async grep(pattern) {
|
|
1239
|
+
const { stdout } = await runExec(
|
|
1240
|
+
`grep -rn --include='*' "${pattern.replace(/"/g, '\\"')}" . 2>/dev/null || true`,
|
|
1241
|
+
{ cwd: workDir, timeout: 1e4 }
|
|
1242
|
+
);
|
|
1243
|
+
return stdout;
|
|
1244
|
+
},
|
|
1245
|
+
async destroy() {
|
|
1246
|
+
await rm(workDir, { recursive: true, force: true });
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
try {
|
|
1250
|
+
return await next(req, ctx);
|
|
1251
|
+
} finally {
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1116
1256
|
// src/middleware/esbuild-dev.ts
|
|
1117
|
-
import { resolve as
|
|
1118
|
-
import { stat, readFile, realpath as realpath2 } from "node:fs/promises";
|
|
1257
|
+
import { resolve as resolve3, relative, dirname } from "node:path";
|
|
1258
|
+
import { stat, readFile as readFile2, realpath as realpath2 } from "node:fs/promises";
|
|
1119
1259
|
function escapeHtml(s) {
|
|
1120
1260
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1121
1261
|
}
|
|
@@ -1160,14 +1300,14 @@ async function collectDeps(entryPath) {
|
|
|
1160
1300
|
continue;
|
|
1161
1301
|
}
|
|
1162
1302
|
try {
|
|
1163
|
-
const content = await
|
|
1303
|
+
const content = await readFile2(p, "utf-8");
|
|
1164
1304
|
const importRe = /from\s+['"]([^'"]+)['"]|import\s+['"]([^'"]+)['"]/g;
|
|
1165
1305
|
let m;
|
|
1166
1306
|
while ((m = importRe.exec(content)) !== null) {
|
|
1167
1307
|
const spec = m[1] ?? m[2];
|
|
1168
1308
|
if (spec.startsWith(".") || spec.startsWith("/")) {
|
|
1169
1309
|
const dir = dirname(p);
|
|
1170
|
-
const resolved =
|
|
1310
|
+
const resolved = resolve3(dir, spec);
|
|
1171
1311
|
for (const ext of ["", ".ts", ".tsx", ".js", ".jsx", ".mjs", "/index.ts", "/index.tsx", "/index.js"]) {
|
|
1172
1312
|
const candidate = resolved + ext;
|
|
1173
1313
|
if (!visited.has(candidate)) {
|
|
@@ -1274,7 +1414,7 @@ ${entries2.join("\n")}
|
|
|
1274
1414
|
format: entry.format ?? "esm",
|
|
1275
1415
|
sourcemap: entry.sourcemap ?? false,
|
|
1276
1416
|
splitting: entry.splitting ?? false,
|
|
1277
|
-
...entry.splitting ? { outdir:
|
|
1417
|
+
...entry.splitting ? { outdir: resolve3(".weifuwu-esbuild-out") } : {},
|
|
1278
1418
|
define: entry.define,
|
|
1279
1419
|
loader: entry.loader,
|
|
1280
1420
|
write: false,
|
|
@@ -1295,7 +1435,7 @@ ${entries2.join("\n")}
|
|
|
1295
1435
|
}]
|
|
1296
1436
|
};
|
|
1297
1437
|
} else {
|
|
1298
|
-
entryAbs =
|
|
1438
|
+
entryAbs = resolve3(entry.entry);
|
|
1299
1439
|
buildOpts = {
|
|
1300
1440
|
entryPoints: [entryAbs],
|
|
1301
1441
|
bundle: entry.bundle ?? true,
|
|
@@ -1412,7 +1552,7 @@ ${entries2.join("\n")}
|
|
|
1412
1552
|
}
|
|
1413
1553
|
const { code, etag, chunks } = await compile(config);
|
|
1414
1554
|
const depEntry = config.clientRouter?.routes ?? (config.clientRouter?.pages ? Object.values(config.clientRouter.pages)[0] : void 0) ?? config.entry;
|
|
1415
|
-
const deps = await collectDeps(
|
|
1555
|
+
const deps = await collectDeps(resolve3(depEntry));
|
|
1416
1556
|
if (cache === "memory") {
|
|
1417
1557
|
cacheStore.set(pathname, { code, etag, files: deps, chunks });
|
|
1418
1558
|
if (chunks) {
|
|
@@ -1447,8 +1587,8 @@ function hashCode(s) {
|
|
|
1447
1587
|
}
|
|
1448
1588
|
|
|
1449
1589
|
// src/middleware/tailwind-dev.ts
|
|
1450
|
-
import { resolve as
|
|
1451
|
-
import { stat as stat2, readFile as
|
|
1590
|
+
import { resolve as resolve4 } from "node:path";
|
|
1591
|
+
import { stat as stat2, readFile as readFile3, glob } from "node:fs/promises";
|
|
1452
1592
|
function escapeHtml2(s) {
|
|
1453
1593
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1454
1594
|
}
|
|
@@ -1482,11 +1622,11 @@ var CLASS_CONTEXT_RE = /(?:class(?:Name)?\s*[=:]\s*["'`]([^"'`]+)["'`]|["'`]([^"
|
|
|
1482
1622
|
async function extractCandidates(patterns, root) {
|
|
1483
1623
|
const seen = /* @__PURE__ */ new Set();
|
|
1484
1624
|
for (const pattern of patterns) {
|
|
1485
|
-
const resolved =
|
|
1625
|
+
const resolved = resolve4(root, pattern);
|
|
1486
1626
|
try {
|
|
1487
1627
|
for await (const file of glob(resolved)) {
|
|
1488
1628
|
try {
|
|
1489
|
-
const content = await
|
|
1629
|
+
const content = await readFile3(file, "utf-8");
|
|
1490
1630
|
for (const m of content.matchAll(CLASS_CONTEXT_RE)) {
|
|
1491
1631
|
const str = m[1] ?? m[2];
|
|
1492
1632
|
if (!str) continue;
|
|
@@ -1553,9 +1693,9 @@ ${String(err)}`;
|
|
|
1553
1693
|
}
|
|
1554
1694
|
async function compileCss(entry) {
|
|
1555
1695
|
const compile = await getCompile();
|
|
1556
|
-
const entryAbs =
|
|
1557
|
-
const cssSource = await
|
|
1558
|
-
const baseDir =
|
|
1696
|
+
const entryAbs = resolve4(entry.entry);
|
|
1697
|
+
const cssSource = await readFile3(entryAbs, "utf-8");
|
|
1698
|
+
const baseDir = resolve4(entry.entry, "..");
|
|
1559
1699
|
const deps = [entryAbs];
|
|
1560
1700
|
const result = await compile(cssSource, {
|
|
1561
1701
|
base: baseDir,
|
|
@@ -1566,7 +1706,7 @@ ${String(err)}`;
|
|
|
1566
1706
|
const contentPatterns = entry.content ?? [];
|
|
1567
1707
|
for (const src of result.sources) {
|
|
1568
1708
|
if (!src.negated) {
|
|
1569
|
-
contentPatterns.push(
|
|
1709
|
+
contentPatterns.push(resolve4(src.base, src.pattern));
|
|
1570
1710
|
}
|
|
1571
1711
|
}
|
|
1572
1712
|
let candidates = [];
|
|
@@ -1576,7 +1716,7 @@ ${String(err)}`;
|
|
|
1576
1716
|
const css = result.build(candidates);
|
|
1577
1717
|
for (const pattern of contentPatterns) {
|
|
1578
1718
|
try {
|
|
1579
|
-
for await (const file of glob(
|
|
1719
|
+
for await (const file of glob(resolve4(baseDir, pattern))) {
|
|
1580
1720
|
if (!deps.includes(file)) deps.push(file);
|
|
1581
1721
|
}
|
|
1582
1722
|
} catch {
|
|
@@ -1746,10 +1886,10 @@ function addRateLimitHeaders(res, limit, remaining, reset) {
|
|
|
1746
1886
|
|
|
1747
1887
|
// src/middleware/compress.ts
|
|
1748
1888
|
import { constants, brotliCompress, gzip, deflate } from "node:zlib";
|
|
1749
|
-
import { promisify } from "node:util";
|
|
1750
|
-
var brotliCompressAsync =
|
|
1751
|
-
var gzipAsync =
|
|
1752
|
-
var deflateAsync =
|
|
1889
|
+
import { promisify as promisify2 } from "node:util";
|
|
1890
|
+
var brotliCompressAsync = promisify2(brotliCompress);
|
|
1891
|
+
var gzipAsync = promisify2(gzip);
|
|
1892
|
+
var deflateAsync = promisify2(deflate);
|
|
1753
1893
|
function compress(options) {
|
|
1754
1894
|
const level = options?.level ?? 6;
|
|
1755
1895
|
const threshold = options?.threshold ?? 1024;
|
|
@@ -2154,7 +2294,7 @@ function redis(opts) {
|
|
|
2154
2294
|
}
|
|
2155
2295
|
|
|
2156
2296
|
// src/queue/index.ts
|
|
2157
|
-
import
|
|
2297
|
+
import crypto3 from "node:crypto";
|
|
2158
2298
|
import { Redis as IORedis2 } from "ioredis";
|
|
2159
2299
|
|
|
2160
2300
|
// src/queue/cron.ts
|
|
@@ -2268,7 +2408,7 @@ function queue(opts) {
|
|
|
2268
2408
|
}
|
|
2269
2409
|
if (job.schedule) {
|
|
2270
2410
|
try {
|
|
2271
|
-
await insert({ ...job, id:
|
|
2411
|
+
await insert({ ...job, id: crypto3.randomUUID(), runAt: cronNext(job.schedule), createdAt: Date.now() });
|
|
2272
2412
|
} catch {
|
|
2273
2413
|
}
|
|
2274
2414
|
}
|
|
@@ -2294,7 +2434,7 @@ function queue(opts) {
|
|
|
2294
2434
|
});
|
|
2295
2435
|
const q = mw;
|
|
2296
2436
|
mw.add = function add(type, payload, opts2) {
|
|
2297
|
-
const id =
|
|
2437
|
+
const id = crypto3.randomUUID();
|
|
2298
2438
|
let runAt;
|
|
2299
2439
|
if (opts2?.schedule) {
|
|
2300
2440
|
try {
|
|
@@ -2412,7 +2552,7 @@ function queue(opts) {
|
|
|
2412
2552
|
return r;
|
|
2413
2553
|
};
|
|
2414
2554
|
q.cron = function(pattern, handler) {
|
|
2415
|
-
const id = "__cron_" + pattern.replace(/[^a-zA-Z0-9]/g, "_") + "_" +
|
|
2555
|
+
const id = "__cron_" + pattern.replace(/[^a-zA-Z0-9]/g, "_") + "_" + crypto3.randomUUID().slice(0, 8);
|
|
2416
2556
|
q.process(id, async () => {
|
|
2417
2557
|
await handler();
|
|
2418
2558
|
});
|
|
@@ -2427,8 +2567,8 @@ import { createElement as createElement2 } from "react";
|
|
|
2427
2567
|
import { renderToReadableStream } from "react-dom/server";
|
|
2428
2568
|
|
|
2429
2569
|
// src/react/compile.ts
|
|
2430
|
-
import { mkdir as
|
|
2431
|
-
import { resolve as
|
|
2570
|
+
import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile4 } from "node:fs/promises";
|
|
2571
|
+
import { resolve as resolve5, join as join3, dirname as dirname2 } from "node:path";
|
|
2432
2572
|
import { createHash } from "node:crypto";
|
|
2433
2573
|
import { existsSync } from "node:fs";
|
|
2434
2574
|
var EXTERNAL_PKGS = [
|
|
@@ -2481,14 +2621,14 @@ function setReactCacheDir(dir) {
|
|
|
2481
2621
|
}
|
|
2482
2622
|
function getCacheDir() {
|
|
2483
2623
|
if (_cacheDir) return _cacheDir;
|
|
2484
|
-
_cacheDir =
|
|
2624
|
+
_cacheDir = join3(process.cwd(), "node_modules", ".weifuwu", "react");
|
|
2485
2625
|
return _cacheDir;
|
|
2486
2626
|
}
|
|
2487
2627
|
async function loadTsxModule(entryPath) {
|
|
2488
|
-
const abs =
|
|
2628
|
+
const abs = resolve5(entryPath);
|
|
2489
2629
|
let source;
|
|
2490
2630
|
try {
|
|
2491
|
-
source = await
|
|
2631
|
+
source = await readFile4(abs, "utf-8");
|
|
2492
2632
|
} catch {
|
|
2493
2633
|
throw new Error(`Cannot read file: ${entryPath}`);
|
|
2494
2634
|
}
|
|
@@ -2498,7 +2638,7 @@ async function loadTsxModule(entryPath) {
|
|
|
2498
2638
|
return memCached.mod;
|
|
2499
2639
|
}
|
|
2500
2640
|
const tmpDir = getCacheDir();
|
|
2501
|
-
const tmpFile =
|
|
2641
|
+
const tmpFile = join3(tmpDir, `${sourceHash}.mjs`);
|
|
2502
2642
|
if (existsSync(tmpFile)) {
|
|
2503
2643
|
try {
|
|
2504
2644
|
const mod2 = await import(tmpFile + "?" + sourceHash);
|
|
@@ -2521,8 +2661,8 @@ async function loadTsxModule(entryPath) {
|
|
|
2521
2661
|
let code = result.outputFiles[0]?.text ?? "";
|
|
2522
2662
|
if (!code) throw new Error(`esbuild produced empty output for ${entryPath}`);
|
|
2523
2663
|
code = rewriteImports(code);
|
|
2524
|
-
await
|
|
2525
|
-
await
|
|
2664
|
+
await mkdir3(dirname2(tmpFile), { recursive: true });
|
|
2665
|
+
await writeFile3(tmpFile, code);
|
|
2526
2666
|
const mod = await import(tmpFile + "?" + sourceHash);
|
|
2527
2667
|
memCache.set(abs, { sourceHash, mod });
|
|
2528
2668
|
return mod;
|
|
@@ -2794,6 +2934,149 @@ function reactRouter(app, routes, opts = {}) {
|
|
|
2794
2934
|
function Link({ href, children, ...props }) {
|
|
2795
2935
|
return createElement2("a", { href, ...props }, children);
|
|
2796
2936
|
}
|
|
2937
|
+
|
|
2938
|
+
// src/ai/index.ts
|
|
2939
|
+
function ai(opts) {
|
|
2940
|
+
let aiModule = null;
|
|
2941
|
+
const model = opts.model;
|
|
2942
|
+
async function getAi() {
|
|
2943
|
+
if (!aiModule) aiModule = await import("ai");
|
|
2944
|
+
return aiModule;
|
|
2945
|
+
}
|
|
2946
|
+
return async (_req, ctx, next) => {
|
|
2947
|
+
ctx.ai = {
|
|
2948
|
+
model,
|
|
2949
|
+
system: opts.system,
|
|
2950
|
+
async generateText(params) {
|
|
2951
|
+
const { generateText } = await getAi();
|
|
2952
|
+
const messages = [...params.messages ?? []];
|
|
2953
|
+
const system = params.system ?? opts.system;
|
|
2954
|
+
if (system) {
|
|
2955
|
+
const hasSystem = messages.some((m) => m.role === "system");
|
|
2956
|
+
if (!hasSystem) {
|
|
2957
|
+
messages.unshift({ role: "system", content: system });
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
const baseOpts = {
|
|
2961
|
+
model,
|
|
2962
|
+
tools: opts.tools,
|
|
2963
|
+
maxSteps: params.maxSteps ?? opts.maxSteps ?? 1,
|
|
2964
|
+
temperature: params.temperature ?? opts.temperature,
|
|
2965
|
+
maxTokens: params.maxTokens ?? opts.maxTokens,
|
|
2966
|
+
abortSignal: _req.signal
|
|
2967
|
+
};
|
|
2968
|
+
if (messages.length > 0) {
|
|
2969
|
+
baseOpts.messages = messages;
|
|
2970
|
+
} else {
|
|
2971
|
+
baseOpts.prompt = params.prompt;
|
|
2972
|
+
}
|
|
2973
|
+
return generateText(baseOpts);
|
|
2974
|
+
}
|
|
2975
|
+
};
|
|
2976
|
+
return next(_req, ctx);
|
|
2977
|
+
};
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
// src/ai/agent.ts
|
|
2981
|
+
function agent(opts) {
|
|
2982
|
+
let aiModule = null;
|
|
2983
|
+
async function getAi() {
|
|
2984
|
+
if (!aiModule) aiModule = await import("ai");
|
|
2985
|
+
return aiModule;
|
|
2986
|
+
}
|
|
2987
|
+
return async (req, ctx, next) => {
|
|
2988
|
+
ctx.agent = {
|
|
2989
|
+
model: opts.model,
|
|
2990
|
+
system: opts.system,
|
|
2991
|
+
async chat(prompt, chatOpts = {}) {
|
|
2992
|
+
const { generateText } = await getAi();
|
|
2993
|
+
let knowledgeContext = "";
|
|
2994
|
+
if (opts.knowledge) {
|
|
2995
|
+
try {
|
|
2996
|
+
const results = await opts.knowledge.search(prompt, ctx);
|
|
2997
|
+
const topK = opts.knowledge.topK ?? 3;
|
|
2998
|
+
const minScore = opts.knowledge.minScore ?? 0;
|
|
2999
|
+
const filtered = results.filter((r) => (r.score ?? 1) >= minScore).slice(0, topK);
|
|
3000
|
+
if (filtered.length > 0) {
|
|
3001
|
+
knowledgeContext = "\n\n\u53C2\u8003\u4EE5\u4E0B\u77E5\u8BC6\uFF1A\n" + filtered.map((r, i) => `[${i + 1}] ${r.content}`).join("\n");
|
|
3002
|
+
}
|
|
3003
|
+
} catch {
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
const system = chatOpts.system ?? opts.system ?? "";
|
|
3007
|
+
const messages = [...chatOpts.messages ?? []];
|
|
3008
|
+
if (system) {
|
|
3009
|
+
const hasSystem = messages.some((m) => m.role === "system");
|
|
3010
|
+
if (!hasSystem) {
|
|
3011
|
+
messages.unshift({
|
|
3012
|
+
role: "system",
|
|
3013
|
+
content: knowledgeContext ? system + knowledgeContext : system
|
|
3014
|
+
});
|
|
3015
|
+
} else if (knowledgeContext) {
|
|
3016
|
+
const sysMsg = messages.find((m) => m.role === "system");
|
|
3017
|
+
if (sysMsg) sysMsg.content += knowledgeContext;
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
if (prompt && !messages.some((m) => m.role === "user")) {
|
|
3021
|
+
messages.push({ role: "user", content: prompt });
|
|
3022
|
+
}
|
|
3023
|
+
const result = await generateText({
|
|
3024
|
+
model: opts.model,
|
|
3025
|
+
messages,
|
|
3026
|
+
tools: opts.tools,
|
|
3027
|
+
maxSteps: chatOpts.maxSteps ?? opts.maxSteps ?? 1,
|
|
3028
|
+
temperature: chatOpts.temperature ?? opts.temperature,
|
|
3029
|
+
maxTokens: chatOpts.maxTokens ?? opts.maxTokens,
|
|
3030
|
+
abortSignal: req.signal
|
|
3031
|
+
});
|
|
3032
|
+
return {
|
|
3033
|
+
text: result.text,
|
|
3034
|
+
toolCalls: result.toolCalls,
|
|
3035
|
+
toolResults: result.toolResults,
|
|
3036
|
+
steps: result.steps,
|
|
3037
|
+
finishReason: result.finishReason,
|
|
3038
|
+
usage: result.usage
|
|
3039
|
+
};
|
|
3040
|
+
},
|
|
3041
|
+
chatStreamResponse(streamOpts) {
|
|
3042
|
+
const encoder = new TextEncoder();
|
|
3043
|
+
const stream = new ReadableStream({
|
|
3044
|
+
async start(controller) {
|
|
3045
|
+
try {
|
|
3046
|
+
const result = await ctx.agent.chat(
|
|
3047
|
+
streamOpts.messages.filter((m) => m.role === "user").pop()?.content ?? "",
|
|
3048
|
+
{
|
|
3049
|
+
messages: streamOpts.messages,
|
|
3050
|
+
system: streamOpts.system
|
|
3051
|
+
}
|
|
3052
|
+
);
|
|
3053
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "text", content: result.text })}
|
|
3054
|
+
|
|
3055
|
+
`));
|
|
3056
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
3057
|
+
controller.close();
|
|
3058
|
+
} catch (err) {
|
|
3059
|
+
controller.enqueue(encoder.encode(
|
|
3060
|
+
`data: ${JSON.stringify({ type: "error", error: String(err) })}
|
|
3061
|
+
|
|
3062
|
+
`
|
|
3063
|
+
));
|
|
3064
|
+
controller.close();
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
});
|
|
3068
|
+
return new Response(stream, {
|
|
3069
|
+
headers: {
|
|
3070
|
+
"Content-Type": "text/event-stream",
|
|
3071
|
+
"Cache-Control": "no-cache",
|
|
3072
|
+
Connection: "keep-alive"
|
|
3073
|
+
}
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
};
|
|
3077
|
+
return next(req, ctx);
|
|
3078
|
+
};
|
|
3079
|
+
}
|
|
2797
3080
|
export {
|
|
2798
3081
|
DEFAULT_MAX_BODY,
|
|
2799
3082
|
ErrorBoundary,
|
|
@@ -2802,6 +3085,9 @@ export {
|
|
|
2802
3085
|
MIGRATIONS_TABLE,
|
|
2803
3086
|
Router,
|
|
2804
3087
|
ServerDataContext,
|
|
3088
|
+
agent,
|
|
3089
|
+
ai,
|
|
3090
|
+
auth,
|
|
2805
3091
|
compress,
|
|
2806
3092
|
cors,
|
|
2807
3093
|
createHub,
|
|
@@ -2818,6 +3104,7 @@ export {
|
|
|
2818
3104
|
reactRouter,
|
|
2819
3105
|
redis,
|
|
2820
3106
|
runWithTrace,
|
|
3107
|
+
sandbox,
|
|
2821
3108
|
serve,
|
|
2822
3109
|
serveStatic,
|
|
2823
3110
|
tailwindDev,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Middleware, User } 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. */
|
|
27
|
+
loadUser: (data: Record<string, unknown>) => 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
|
+
/** Validate an API key → User or null. */
|
|
41
|
+
validate: (key: string) => Promise<User | null> | User | null;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Authentication middleware — injects `ctx.user`.
|
|
46
|
+
*
|
|
47
|
+
* Supports JWT (HS256), signed session cookies, and API keys.
|
|
48
|
+
* When no user is authenticated, requests still proceed —
|
|
49
|
+
* check `ctx.user` in your handlers to enforce auth.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* // JWT
|
|
54
|
+
* app.use(auth({ jwt: { secret: 'my-secret' } }))
|
|
55
|
+
*
|
|
56
|
+
* // Session cookie
|
|
57
|
+
* app.use(auth({
|
|
58
|
+
* session: {
|
|
59
|
+
* secret: '...',
|
|
60
|
+
* loadUser: async (data) => db.findUser(data.userId),
|
|
61
|
+
* },
|
|
62
|
+
* }))
|
|
63
|
+
*
|
|
64
|
+
* // API key
|
|
65
|
+
* app.use(auth({ apiKey: { validate: async (key) => db.findByApiKey(key) } }))
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
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.0",
|
|
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",
|