weifuwu 0.31.1 → 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 +238 -120
- 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/core/router.d.ts +11 -0
- package/dist/index.d.ts +16 -7
- package/dist/index.js +1186 -315
- package/dist/middleware/auth.d.ts +68 -0
- package/dist/middleware/esbuild-dev.d.ts +69 -0
- package/dist/middleware/esbuild-dev.js +335 -0
- package/dist/middleware/sandbox.d.ts +52 -0
- package/dist/middleware/tailwind-dev.d.ts +43 -0
- package/dist/middleware/tailwind-dev.js +199 -0
- package/dist/react/client.d.ts +64 -27
- package/dist/react/client.js +130 -248
- package/dist/react/compile.d.ts +17 -0
- package/dist/react/error-boundary.d.ts +18 -19
- package/dist/react/index.d.ts +80 -16
- package/dist/react/index.js +835 -267
- package/dist/react/types.d.ts +134 -34
- package/dist/types.d.ts +7 -0
- package/package.json +17 -6
- package/dist/react/navigation.d.ts +0 -63
- package/dist/react/navigation.js +0 -154
- package/dist/react/render.d.ts +0 -8
- package/dist/react/route-utils.d.ts +0 -31
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ serve(app, { port: 3000 })
|
|
|
21
21
|
| Export | Description |
|
|
22
22
|
|---|---|
|
|
23
23
|
| `serve(app, opts?)` | Start HTTP server. Returns `Server` with `port`, `hostname`, `ready`, `close()`. |
|
|
24
|
-
| `Router` | Trie-based HTTP router with WebSocket support. |
|
|
24
|
+
| `Router` | Trie-based HTTP router with WebSocket support and `plugin()` method. |
|
|
25
25
|
| `HttpError` | `new HttpError(message, status)`. Throw to return that status code. |
|
|
26
26
|
| `DEFAULT_MAX_BODY` | `10 * 1024 * 1024` (10MB). |
|
|
27
27
|
|
|
@@ -42,6 +42,7 @@ app.ws(path, ...middlewares, handler)
|
|
|
42
42
|
// Middleware & mounting
|
|
43
43
|
app.use(middleware) // global middleware
|
|
44
44
|
app.mount(prefix, router) // sub-router at prefix
|
|
45
|
+
app.plugin(fn) // extension: (app) => { app.get(), app.use(), ... }
|
|
45
46
|
app.onError(handler) // error handler: (error, req, ctx) => Response
|
|
46
47
|
app.routes() // debug: list all registered routes
|
|
47
48
|
|
|
@@ -63,7 +64,8 @@ app.get('/users/:id', (req, ctx) => {
|
|
|
63
64
|
| `logger(opts?)` | Request logging. `format: 'short' | 'combined' | 'json'`. |
|
|
64
65
|
| `upload(opts?)` | Multipart file upload via `req.formData()`. Injects `ctx.parsed`. |
|
|
65
66
|
| `serveStatic(root, opts?)` | Static file handler. `cacheControl`, `index`. |
|
|
66
|
-
| `
|
|
67
|
+
| `sandbox(opts?)` | Filesystem isolation for agent operations. `baseDir`, `timeout`, `isolateBy`. |
|
|
68
|
+
| `auth(opts?)` | Authentication: JWT, session cookies, API keys. Injects `ctx.user`. |
|
|
67
69
|
|
|
68
70
|
### Tracing
|
|
69
71
|
|
|
@@ -178,111 +180,241 @@ npm install react react-dom
|
|
|
178
180
|
```
|
|
179
181
|
|
|
180
182
|
```ts
|
|
181
|
-
|
|
182
|
-
import {
|
|
183
|
+
// server.ts — the only file you need
|
|
184
|
+
import { serve, Router } from 'weifuwu'
|
|
185
|
+
import { react } from 'weifuwu/react'
|
|
183
186
|
|
|
184
|
-
|
|
187
|
+
const app = new Router()
|
|
188
|
+
.use(trace())
|
|
189
|
+
.use(logger())
|
|
190
|
+
.plugin(react({
|
|
191
|
+
pages: {
|
|
192
|
+
'/': './pages/Home.tsx',
|
|
193
|
+
'/users': './pages/Users.tsx',
|
|
194
|
+
'/users/:id': './pages/UserDetail.tsx',
|
|
195
|
+
},
|
|
196
|
+
layout: './layouts/Root.tsx',
|
|
197
|
+
notFound: './pages/NotFound.tsx',
|
|
198
|
+
tailwind: { entry: './styles/input.css' },
|
|
199
|
+
}))
|
|
185
200
|
|
|
186
|
-
app.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
201
|
+
app.get('/api/hello', () => Response.json({ message: 'hi' }))
|
|
202
|
+
serve(app, { port: 3000 })
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**One `react()` call handles:** SSR rendering, routing, data loading, Tailwind CSS, client bundle auto-generation, and error pages. No `client.ts`, `routes.ts`, or manual middleware setup needed.
|
|
206
|
+
|
|
207
|
+
#### Page components
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
// pages/UserDetail.tsx
|
|
211
|
+
import type { Context } from 'weifuwu'
|
|
212
|
+
import { HttpError } from 'weifuwu'
|
|
213
|
+
import { useServerData } from 'weifuwu/react'
|
|
193
214
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
data: { greeting: 'Hello' },
|
|
199
|
-
})
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
// Streaming SSR
|
|
203
|
-
app.get('/dashboard', (_req, ctx) =>
|
|
204
|
-
ctx.renderStream(h(Dashboard))
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
// Layout nesting via mount
|
|
208
|
-
const admin = new Router()
|
|
209
|
-
admin.use(react({ layout: AdminLayout }))
|
|
210
|
-
admin.get('/dashboard', (_req, ctx) => ctx.render(h(AdminDashboard)))
|
|
211
|
-
app.mount('/admin', admin)
|
|
212
|
-
|
|
213
|
-
// ── Shared components ──────────────────────────────────
|
|
214
|
-
|
|
215
|
-
// Use the same component on server and client
|
|
216
|
-
function Page() {
|
|
217
|
-
const { greeting } = useServerData<{ greeting: string }>()
|
|
218
|
-
return h('h1', null, greeting)
|
|
215
|
+
export async function loader(ctx: Context) {
|
|
216
|
+
const user = await db.find(ctx.params.id)
|
|
217
|
+
if (!user) throw new HttpError('Not found', 404)
|
|
218
|
+
return { user }
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return
|
|
224
|
-
|
|
225
|
-
|
|
221
|
+
export default function UserDetailPage() {
|
|
222
|
+
const { user } = useServerData<{ user: User }>()
|
|
223
|
+
return (
|
|
224
|
+
<div>
|
|
225
|
+
<title>{`${user.name} — My App`}</title>
|
|
226
|
+
<h1>{user.name}</h1>
|
|
227
|
+
<p>{user.email}</p>
|
|
228
|
+
</div>
|
|
226
229
|
)
|
|
227
230
|
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
- `export async function loader(ctx)` — runs on the server, returns data for `useServerData()`
|
|
234
|
+
- `throw new HttpError('Not found', 404)` — renders the NotFound page with correct status
|
|
235
|
+
- `<title>` auto-hoists to `<head>` via React 19
|
|
236
|
+
- `export default` is auto-detected; named exports work too
|
|
228
237
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
238
|
+
#### Layout with shared data
|
|
239
|
+
|
|
240
|
+
```tsx
|
|
241
|
+
// layouts/Root.tsx
|
|
242
|
+
export async function loader(ctx: Context) {
|
|
243
|
+
return { currentUser: await getCurrentUser(ctx) }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function Root({ children }: { children: ReactNode }) {
|
|
247
|
+
const { currentUser } = useServerData()
|
|
248
|
+
return (
|
|
249
|
+
<>
|
|
250
|
+
<nav>
|
|
251
|
+
<a href="/">Home</a>
|
|
252
|
+
<a href="/users">Users</a>
|
|
253
|
+
<span>{currentUser?.name}</span>
|
|
254
|
+
</nav>
|
|
255
|
+
<main>{children}</main>
|
|
256
|
+
</>
|
|
233
257
|
)
|
|
234
258
|
}
|
|
259
|
+
```
|
|
235
260
|
|
|
236
|
-
|
|
261
|
+
Layout `loader` data merges with page data. Page loader overrides same keys.
|
|
237
262
|
|
|
238
|
-
|
|
239
|
-
import { hydrate, createClientRouter, defineRoute } from 'weifuwu/react/client'
|
|
263
|
+
#### Data flow
|
|
240
264
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
265
|
+
```
|
|
266
|
+
layout loader ──┐
|
|
267
|
+
├──→ merge → useServerData() ──→ Layout + Page
|
|
268
|
+
page loader ───┘
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
#### Client-side SPA
|
|
272
|
+
|
|
273
|
+
Every page is automatically code-split into a separate chunk. On first visit the server renders HTML and the client hydrates. Subsequent navigations intercept `<a>` clicks, `import()` the page chunk, and fetch fresh server data — all automatic, zero config.
|
|
246
274
|
|
|
247
|
-
|
|
248
|
-
{ path: '/', component: HomePage },
|
|
249
|
-
userRoute,
|
|
250
|
-
])
|
|
275
|
+
#### Streaming SSR
|
|
251
276
|
|
|
252
|
-
|
|
277
|
+
```tsx
|
|
278
|
+
import { Suspense, use } from 'react'
|
|
279
|
+
|
|
280
|
+
export default function StreamingPage() {
|
|
281
|
+
return (
|
|
282
|
+
<div>
|
|
283
|
+
<h1>Instant shell</h1>
|
|
284
|
+
<Suspense fallback={<Spinner />}>
|
|
285
|
+
<SlowData promise={fetchSlowData()} />
|
|
286
|
+
</Suspense>
|
|
287
|
+
</div>
|
|
288
|
+
)
|
|
289
|
+
}
|
|
253
290
|
```
|
|
254
291
|
|
|
255
|
-
|
|
292
|
+
`<Suspense>` boundaries stream to the browser as data resolves.
|
|
256
293
|
|
|
257
294
|
| Feature | Description |
|
|
258
295
|
|---|---|
|
|
259
|
-
| `
|
|
260
|
-
| `ctx
|
|
261
|
-
| `useServerData<T>()` |
|
|
262
|
-
| `Link` |
|
|
263
|
-
| `
|
|
264
|
-
|
|
|
265
|
-
|
|
|
266
|
-
| `
|
|
267
|
-
| `defineRoute()` | Type-safe route config — captures loader return type. |
|
|
268
|
-
| `createClientRouter()` | Client-side SPA router with loader-based data fetching. |
|
|
269
|
-
| `hydrate(App)` | Hydrate server-rendered HTML on the client. |
|
|
270
|
-
| `head: { title, meta }` | Inject dynamic `<title>` and `<meta>` tags. |
|
|
296
|
+
| `react({ pages, layout, notFound, tailwind })` | One call: SSR + routing + client bundle + error handling |
|
|
297
|
+
| `export async function loader(ctx)` | Server-side data loading, auto-detected by the framework |
|
|
298
|
+
| `useServerData<T>()` | Type-safe access to loader data in any component |
|
|
299
|
+
| `Link` | `<a>` that does SPA navigation on the client |
|
|
300
|
+
| `ErrorBoundary` | Catches render errors on server and client |
|
|
301
|
+
| `<title>`, `<meta>` | Auto-hoisted to `<head>` via React 19 |
|
|
302
|
+
| `<Suspense>` | Streaming SSR, works out of the box |
|
|
303
|
+
| `client: false` | Disable client JS for static SSR only |
|
|
271
304
|
|
|
272
305
|
**Import paths:**
|
|
273
306
|
|
|
274
307
|
```ts
|
|
275
308
|
// Server-side
|
|
276
|
-
import { react, Link,
|
|
309
|
+
import { react, Link, ErrorBoundary, useServerData } from 'weifuwu'
|
|
310
|
+
|
|
311
|
+
// Client-side (for custom advanced setups)
|
|
312
|
+
import { createBrowserRouter, hydrate, navigate } from 'weifuwu/react/client'
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
See [examples/react-ssr/](examples/react-ssr/) for the full demo.
|
|
277
316
|
|
|
278
|
-
|
|
279
|
-
import { hydrate, createClientRouter, defineRoute, Link } from 'weifuwu/react/client'
|
|
317
|
+
### AI Agent
|
|
280
318
|
|
|
281
|
-
|
|
282
|
-
|
|
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
|
+
})
|
|
283
357
|
```
|
|
284
358
|
|
|
285
|
-
|
|
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`.
|
|
286
418
|
|
|
287
419
|
### Types
|
|
288
420
|
|
|
@@ -339,50 +471,35 @@ app.onError((err, req, ctx) => {
|
|
|
339
471
|
## Complete example
|
|
340
472
|
|
|
341
473
|
```ts
|
|
342
|
-
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'
|
|
343
478
|
|
|
344
479
|
const app = new Router()
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
})
|
|
363
|
-
|
|
364
|
-
app.post('/api/
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
return
|
|
368
|
-
})
|
|
369
|
-
|
|
370
|
-
app.get('/api/users/:id', async (req, ctx) => {
|
|
371
|
-
const [user] = await ctx.sql`SELECT * FROM users WHERE id = ${ctx.params.id}`
|
|
372
|
-
if (!user) throw new HttpError('Not found', 404)
|
|
373
|
-
return Response.json(user)
|
|
374
|
-
})
|
|
375
|
-
|
|
376
|
-
// WebSocket
|
|
377
|
-
app.ws('/chat', {
|
|
378
|
-
open(ws, ctx) { ctx.ws.join('lobby') },
|
|
379
|
-
message(ws, ctx, data) { ctx.ws.sendRoom('lobby', { text: data.toString() }) },
|
|
380
|
-
})
|
|
381
|
-
|
|
382
|
-
// Error handling
|
|
383
|
-
app.onError((err) => {
|
|
384
|
-
if (err instanceof HttpError) return Response.json({ error: err.message }, { status: err.status })
|
|
385
|
-
return Response.json({ error: 'Internal error' }, { status: 500 })
|
|
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: {} }))
|
|
498
|
+
|
|
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 })
|
|
386
503
|
})
|
|
387
504
|
|
|
388
505
|
serve(app, { port: 3000 })
|
|
@@ -403,6 +520,7 @@ weifuwu/
|
|
|
403
520
|
│ ├── types.ts
|
|
404
521
|
│ ├── core/ ← serve, router, ws, trace, logger
|
|
405
522
|
│ ├── middleware/ ← cors, helmet, compress, rate-limit, static, upload
|
|
523
|
+
│ ├── ai/ ← AI + agent middleware
|
|
406
524
|
│ ├── postgres/
|
|
407
525
|
│ ├── redis/
|
|
408
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/core/router.d.ts
CHANGED
|
@@ -25,6 +25,17 @@ export declare class Router<T extends Context = Context> {
|
|
|
25
25
|
private get hub();
|
|
26
26
|
wsHub(hub: Hub): this;
|
|
27
27
|
use(mw: Middleware<Context, Context>): Router<T>;
|
|
28
|
+
/**
|
|
29
|
+
* Install a plugin — a function that configures the Router with
|
|
30
|
+
* routes, middleware, and error handlers. Use when `.use()` isn't
|
|
31
|
+
* enough because you need to call `app.get()`, `app.onError()`, etc.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* app.plugin(app => createReactApp(app, { pages: {...}, layout: '...', tailwind: {...} }))
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
plugin(fn: (app: this) => void): this;
|
|
28
39
|
mount(path: string, router: Router<Context>): Router<T>;
|
|
29
40
|
onError(handler: ErrorHandler<T>): Router<T>;
|
|
30
41
|
get(path: string, ...rest: [...Middleware[], Handler | Router<Context>]): Router<T>;
|