weifuwu 0.9.6 → 0.11.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 +265 -164
- package/dist/agent/migrate.d.ts +1 -1
- package/dist/agent/rest.d.ts +1 -1
- package/dist/agent/run.d.ts +2 -2
- package/dist/agent/types.d.ts +2 -2
- package/dist/ai/workflow.d.ts +14 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2292 -11035
- package/dist/messager/migrate.d.ts +1 -1
- package/dist/messager/rest.d.ts +1 -1
- package/dist/messager/types.d.ts +2 -1
- package/dist/messager/ws.d.ts +1 -1
- package/dist/opencode/client.d.ts +2 -0
- package/dist/opencode/index.d.ts +2 -0
- package/dist/opencode/migrate.d.ts +2 -0
- package/dist/opencode/permissions.d.ts +5 -0
- package/dist/opencode/prompt.d.ts +8 -0
- package/dist/opencode/rest.d.ts +15 -0
- package/dist/opencode/run.d.ts +13 -0
- package/dist/opencode/session.d.ts +26 -0
- package/dist/opencode/skills.d.ts +4 -0
- package/dist/opencode/tools/bash.d.ts +6 -0
- package/dist/opencode/tools/edit.d.ts +19 -0
- package/dist/opencode/tools/glob.d.ts +9 -0
- package/dist/opencode/tools/grep.d.ts +17 -0
- package/dist/opencode/tools/index.d.ts +12 -0
- package/dist/opencode/tools/question.d.ts +5 -0
- package/dist/opencode/tools/read.d.ts +16 -0
- package/dist/opencode/tools/skill.d.ts +18 -0
- package/dist/opencode/tools/web.d.ts +18 -0
- package/dist/opencode/tools/write.d.ts +13 -0
- package/dist/opencode/types.d.ts +90 -0
- package/dist/opencode/ws.d.ts +22 -0
- package/dist/postgres/index.d.ts +3 -1
- package/dist/postgres/module.d.ts +9 -0
- package/dist/postgres/schema/columns.d.ts +34 -0
- package/dist/postgres/schema/index.d.ts +4 -0
- package/dist/postgres/schema/sql.d.ts +7 -0
- package/dist/postgres/schema/table.d.ts +49 -0
- package/dist/postgres/types.d.ts +11 -35
- package/dist/queue/types.d.ts +1 -1
- package/dist/redis/types.d.ts +1 -1
- package/dist/router.d.ts +1 -1
- package/dist/sse.d.ts +10 -0
- package/dist/tenant/graphql.d.ts +1 -1
- package/dist/tenant/migrate.d.ts +1 -1
- package/dist/tenant/rest.d.ts +1 -1
- package/dist/tenant/types.d.ts +2 -1
- package/dist/tsx-context.d.ts +12 -0
- package/dist/tsx-instance.d.ts +32 -0
- package/dist/tsx.d.ts +5 -16
- package/dist/user/oauth2.d.ts +2 -1
- package/dist/user/types.d.ts +2 -1
- package/dist/vendor.d.ts +3 -0
- package/dist/workflow/engine.d.ts +1 -1
- package/dist/workflow/route.d.ts +1 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
weifuwu doesn't invent its own request/response abstraction. `Request` and `Response` are the same objects you use in `fetch()` — what you learn in the browser applies directly on the server. `ctx` is the only framework object, and it only carries what the router parsed for you (`params`, `query`).
|
|
8
8
|
|
|
9
|
-
Everything follows the same `(req, ctx) => Response` contract. The Router handles HTTP routing and WebSocket. All other features — auth, validation, database, GraphQL, AI
|
|
9
|
+
Everything follows the same `(req, ctx) => Response` contract. The Router handles HTTP routing and WebSocket. All other features — auth, validation, database, GraphQL, AI — are standalone modules you import and mount with `app.use()`.
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
@@ -18,8 +18,8 @@ Everything follows the same `(req, ctx) => Response` contract. The Router handle
|
|
|
18
18
|
- **WebSocket** — `router.ws()` with upgrade middleware (auth before connect)
|
|
19
19
|
- **GraphQL** — `graphql(handler)` sub-Router with GraphiQL IDE
|
|
20
20
|
- **AI streaming** — `ai(handler)` sub-Router via Vercel AI SDK
|
|
21
|
-
- **
|
|
22
|
-
- **AI Agent** — `agent()` — server-side AI agents with chat/
|
|
21
|
+
- **DAG workflow tool** — `runWorkflow()` — multi-step execution engine as a single AI SDK `Tool`
|
|
22
|
+
- **AI Agent** — `agent()` — server-side AI agents with chat/tool-use/knowledge types, OpenAI-compatible, Ollama-ready
|
|
23
23
|
- **Messaging** — `messager()` — real-time chat with channels, WebSocket, agent routing, webhook support
|
|
24
24
|
- **Tenant BaaS** — `tenant()` — multi-tenant dynamic tables, auto REST + GraphQL, row-level isolation, pgvector/HNSW
|
|
25
25
|
- **Redis** — `redis()` — ioredis client, `ctx.redis`, middleware
|
|
@@ -221,83 +221,247 @@ Features: MIME type detection (20+ types), ETag + If-None-Match (304), directory
|
|
|
221
221
|
|
|
222
222
|
## PostgreSQL
|
|
223
223
|
|
|
224
|
-
Built-in PostgreSQL —
|
|
224
|
+
Built-in PostgreSQL client — connection management, type-safe DDL, transactions, and module lifecycle.
|
|
225
225
|
|
|
226
226
|
```ts
|
|
227
227
|
import { serve, Router, postgres } from 'weifuwu'
|
|
228
|
-
import { z } from 'zod'
|
|
229
228
|
|
|
230
229
|
const app = new Router()
|
|
231
|
-
const pg = postgres()
|
|
230
|
+
const pg = postgres() // reads DATABASE_URL
|
|
231
|
+
app.use(pg) // injects ctx.sql into handlers
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Type-safe DDL with schema builder
|
|
232
235
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
236
|
+
Define tables declaratively with type inference — no raw SQL for common operations, no Zod needed:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import { pgTable, serial, uuid, text, integer, boolean, timestamptz, jsonb, sql } from 'weifuwu'
|
|
240
|
+
|
|
241
|
+
const users = pgTable('_users', {
|
|
242
|
+
id: serial('id').primaryKey(),
|
|
243
|
+
name: text('name').notNull(),
|
|
244
|
+
email: text('email').unique().notNull(),
|
|
245
|
+
age: integer('age'),
|
|
246
|
+
active: boolean('active').default(true),
|
|
247
|
+
createdAt: timestamptz('created_at').default(sql`NOW()`),
|
|
248
|
+
metadata: jsonb<{ role: string }>('metadata'),
|
|
238
249
|
})
|
|
250
|
+
```
|
|
239
251
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
252
|
+
Supports 10 column types:
|
|
253
|
+
| Builder | DDL | TS Type |
|
|
254
|
+
|---------|-----|---------|
|
|
255
|
+
| `serial()` | `SERIAL` | `number` |
|
|
256
|
+
| `uuid()` | `UUID` | `string` |
|
|
257
|
+
| `text()` | `TEXT` | `string` |
|
|
258
|
+
| `integer()` | `INTEGER` | `number` |
|
|
259
|
+
| `boolean()` | `BOOLEAN` | `boolean` |
|
|
260
|
+
| `timestamptz()` | `TIMESTAMPTZ` | `string` |
|
|
261
|
+
| `jsonb<T>()` | `JSONB` | `T` |
|
|
262
|
+
| `textArray()` | `TEXT[]` | `string[]` |
|
|
263
|
+
| `vector(name, dims)` | `vector(N)` | `number[]` |
|
|
264
|
+
|
|
265
|
+
Column constraints chainable: `.primaryKey()`, `.notNull()`, `.nullable()`, `.default(value | sql\`...\`)`, `.unique()`, `.references(table, column?, onDelete?)`.
|
|
266
|
+
|
|
267
|
+
### DDL execution
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
await users.create() // CREATE TABLE IF NOT EXISTS
|
|
271
|
+
await users.createIndex('email') // CREATE INDEX
|
|
272
|
+
await users.createUniqueIndex('slug') // CREATE UNIQUE INDEX
|
|
273
|
+
await users.createIndex('created_at', { desc: true })
|
|
274
|
+
await users.createIndex(['a', 'b']) // multi-column
|
|
275
|
+
await users.createIndex('embedding', { // pgvector HNSW
|
|
276
|
+
type: 'hnsw', operator: 'vector_cosine_ops',
|
|
277
|
+
})
|
|
278
|
+
await users.drop({ cascade: true })
|
|
243
279
|
```
|
|
244
280
|
|
|
245
|
-
###
|
|
281
|
+
### Type-safe CRUD with BoundTable
|
|
282
|
+
|
|
283
|
+
Two usage paths — use `pg.table()` when you have a `pg` handle, or `pgTable()` with explicit `sql`:
|
|
246
284
|
|
|
247
285
|
```ts
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
286
|
+
// pg.table() — auto-binds sql, no need to pass it
|
|
287
|
+
const users = pg.table('_users', {
|
|
288
|
+
id: serial('id').primaryKey(),
|
|
289
|
+
name: text('name').notNull(),
|
|
290
|
+
email: text('email').unique(),
|
|
291
|
+
active: boolean('active').default(true),
|
|
292
|
+
createdAt: timestamptz('created_at').default(sql`NOW()`),
|
|
293
|
+
})
|
|
252
294
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
295
|
+
// INSERT ... RETURNING * — auto-strips serial id
|
|
296
|
+
const user = await users.insert({ name: 'Alice', email: 'alice@test.com' })
|
|
297
|
+
// → { id: 1, name: 'Alice', email: 'alice@test.com', active: true, ... }
|
|
298
|
+
|
|
299
|
+
// SELECT ... WHERE id = ? LIMIT 1
|
|
300
|
+
const found = await users.findById(1)
|
|
301
|
+
|
|
302
|
+
// SELECT ... WHERE ... [ORDER BY ...] [LIMIT ...] [OFFSET ...]
|
|
303
|
+
const admins = await users.find({ role: 'admin' })
|
|
304
|
+
const sorted = await users.find({ active: true }, { orderBy: { name: 'asc' } })
|
|
305
|
+
const page = await users.find(undefined, { limit: 10, offset: 0 })
|
|
306
|
+
const filtered = await users.find({ role: 'admin' }, { orderBy: { name: 'desc' }, limit: 5 })
|
|
307
|
+
|
|
308
|
+
// UPDATE ... SET ... WHERE ... RETURNING *
|
|
309
|
+
const updated = await users.update({ id: 1 }, { name: 'Bob' })
|
|
310
|
+
// With SQL expressions:
|
|
311
|
+
await users.update({ id: 1 }, { name: 'Bob', updated_at: sql`NOW()` })
|
|
312
|
+
|
|
313
|
+
// DELETE ... WHERE ... RETURNING 1
|
|
314
|
+
const ok = await users.delete({ id: 1 })
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
When using `pgTable()` directly (without `pg`), pass `sql` as the first argument:
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
const t = pgTable('_users', { ... })
|
|
321
|
+
await t.insert(ctx.sql, { name: 'Alice' })
|
|
322
|
+
await t.find(ctx.sql, { role: 'admin' }, { orderBy: { name: 'asc' } })
|
|
256
323
|
```
|
|
257
324
|
|
|
258
|
-
|
|
325
|
+
### Complex queries use raw SQL
|
|
259
326
|
|
|
260
327
|
```ts
|
|
261
328
|
app.get('/users/stats', async (req, ctx) => {
|
|
262
329
|
const rows = await ctx.sql`
|
|
263
330
|
SELECT u.*, count(p.id) as posts
|
|
264
|
-
FROM users u LEFT JOIN posts p ON p.user_id = u.id
|
|
331
|
+
FROM ${users} u LEFT JOIN posts p ON p.user_id = u.id
|
|
265
332
|
GROUP BY u.id
|
|
266
333
|
`
|
|
267
334
|
return Response.json(rows)
|
|
268
335
|
})
|
|
269
336
|
```
|
|
270
337
|
|
|
271
|
-
###
|
|
338
|
+
### Transactions
|
|
272
339
|
|
|
273
|
-
|
|
340
|
+
```ts
|
|
341
|
+
const result = await pg.transaction(async (tx) => {
|
|
342
|
+
const [user] = await tx`INSERT INTO "_users" (...) VALUES (...) RETURNING *`
|
|
343
|
+
const [wallet] = await tx`INSERT INTO "_wallets" ("user_id") VALUES (${user.id}) RETURNING *`
|
|
344
|
+
return { user, wallet }
|
|
345
|
+
})
|
|
346
|
+
```
|
|
274
347
|
|
|
275
|
-
|
|
276
|
-
- **Column missing** → `ALTER TABLE ADD COLUMN IF NOT EXISTS`
|
|
277
|
-
- **Existing** → no-op
|
|
348
|
+
### Connection lifecycle
|
|
278
349
|
|
|
279
|
-
|
|
350
|
+
```ts
|
|
351
|
+
const pg = postgres() // reads DATABASE_URL
|
|
352
|
+
const pg = postgres('postgres://...') // explicit connection
|
|
353
|
+
const pg = postgres({
|
|
354
|
+
connection: 'postgres://...',
|
|
355
|
+
max: 10, // pool size
|
|
356
|
+
ssl: { rejectUnauthorized: false }, // SSL options
|
|
357
|
+
idle_timeout: 30, // idle timeout (s)
|
|
358
|
+
connect_timeout: 10, // connection timeout (s)
|
|
359
|
+
closeTimeout: 5, // close grace period (s)
|
|
360
|
+
signal: ac.signal, // abort → sql.end()
|
|
361
|
+
})
|
|
362
|
+
await pg.close()
|
|
363
|
+
```
|
|
280
364
|
|
|
281
|
-
###
|
|
365
|
+
### Module base class
|
|
366
|
+
|
|
367
|
+
Every database module (`opencode`, `messager`, `tenant`, `agent`, `user`) extends `PgModule`:
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
import { PgModule } from 'weifuwu'
|
|
371
|
+
|
|
372
|
+
class MyModule extends PgModule {
|
|
373
|
+
constructor(pg: PostgresClient) {
|
|
374
|
+
super(pg) // sets this.sql = pg.sql
|
|
375
|
+
}
|
|
376
|
+
async migrate() { /* override */ }
|
|
377
|
+
// close() inherited — calls pg.close() automatically
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## Opencode
|
|
382
|
+
|
|
383
|
+
AI programming assistant — chat with LLM agents that have access to filesystem tools, skills, and isolated session workspaces.
|
|
282
384
|
|
|
283
385
|
```ts
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const
|
|
287
|
-
|
|
386
|
+
import { serve, Router, postgres, opencode } from 'weifuwu'
|
|
387
|
+
|
|
388
|
+
const app = new Router()
|
|
389
|
+
const pg = postgres()
|
|
390
|
+
const oc = await opencode({ pg, permissions: { ... } })
|
|
391
|
+
|
|
392
|
+
await oc.migrate()
|
|
393
|
+
app.use('/opencode', await oc.router())
|
|
394
|
+
app.ws('/opencode', oc.wsHandler())
|
|
395
|
+
|
|
396
|
+
serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Session-isolated workspaces
|
|
400
|
+
|
|
401
|
+
Each session gets its own sandbox directory — tools operate within it, files cannot escape:
|
|
402
|
+
|
|
288
403
|
```
|
|
404
|
+
cwd/.sessions/opencode/1/ ← session 1's workspace
|
|
405
|
+
cwd/.sessions/opencode/2/ ← session 2's workspace
|
|
406
|
+
cwd/.sessions/chat/3/ ← different mount point
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Workspaces are computed from `cwd { ctx.mountPath } { sessionId }`. The system prompt shows the session's workspace so the LLM knows where it is.
|
|
289
410
|
|
|
290
|
-
###
|
|
411
|
+
### Tools
|
|
291
412
|
|
|
292
|
-
|
|
|
293
|
-
|
|
294
|
-
| `
|
|
295
|
-
| `
|
|
296
|
-
| `
|
|
413
|
+
| Tool | Description |
|
|
414
|
+
|------|-------------|
|
|
415
|
+
| `bash` | Execute shell commands in the workspace |
|
|
416
|
+
| `read` | Read files with offset/limit |
|
|
417
|
+
| `write` | Create or overwrite files |
|
|
418
|
+
| `edit` | Exact string replacements |
|
|
419
|
+
| `grep` | Regex content search |
|
|
420
|
+
| `glob` | Glob pattern file search |
|
|
421
|
+
| `web` | Fetch URL content |
|
|
422
|
+
| `question` | Ask the user for input |
|
|
423
|
+
| `skill` | Load a skill on demand |
|
|
297
424
|
|
|
298
|
-
|
|
425
|
+
### Skills
|
|
299
426
|
|
|
300
|
-
|
|
427
|
+
Skills are discovered from filesystem and loaded on demand via the `skill` tool — no system prompt bloat:
|
|
428
|
+
|
|
429
|
+
- Project: `.opencode/skills/{name}/SKILL.md`
|
|
430
|
+
- Global: `~/.config/opencode/skills/{name}/SKILL.md`
|
|
431
|
+
- Also reads: `.claude/skills/`, `.agents/skills/` (project + global)
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
const oc = await opencode({
|
|
435
|
+
pg,
|
|
436
|
+
skills: [{ name: 'git', description: 'Git workflow', content: '...' }],
|
|
437
|
+
})
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Permissions
|
|
441
|
+
|
|
442
|
+
Control tool access per conversation:
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
const oc = await opencode({
|
|
446
|
+
pg,
|
|
447
|
+
permissions: {
|
|
448
|
+
bash: { allow: true },
|
|
449
|
+
read: { allow: true },
|
|
450
|
+
write: { allow: false },
|
|
451
|
+
edit: { allow: false },
|
|
452
|
+
skill: { '*': { allow: true }, 'internal-*': { allow: false } },
|
|
453
|
+
},
|
|
454
|
+
})
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### Workspace isolation
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
const oc = await opencode({ pg, permissions })
|
|
461
|
+
// All sessions inherit the instance's workspace (default: process.cwd())
|
|
462
|
+
// Sessions cannot override their workspace
|
|
463
|
+
// Different mount points = different opencode() instances = isolated workspaces
|
|
464
|
+
```
|
|
301
465
|
|
|
302
466
|
```ts
|
|
303
467
|
import { serve, Router, postgres, user } from 'weifuwu'
|
|
@@ -570,7 +734,7 @@ await fetch('http://localhost/api/sys/tenants/invite', {
|
|
|
570
734
|
|
|
571
735
|
## AI Agent
|
|
572
736
|
|
|
573
|
-
Server-side AI agents with OpenAI-compatible API. Built-in chat,
|
|
737
|
+
Server-side AI agents with OpenAI-compatible API. Built-in chat, tool-use (tool-calling), and knowledge (RAG) types. Works out of the box with Ollama or any OpenAI-compatible provider.
|
|
574
738
|
|
|
575
739
|
```ts
|
|
576
740
|
import { agent } from 'weifuwu'
|
|
@@ -584,7 +748,7 @@ app.use('/api', agents.router())
|
|
|
584
748
|
| Type | Description | Execution |
|
|
585
749
|
|------|-------------|-----------|
|
|
586
750
|
| `chat` | Pure conversation | `streamText()` / `generateText()` |
|
|
587
|
-
| `
|
|
751
|
+
| `tool-use` | Tool-calling agent | `streamText({ tools })` |
|
|
588
752
|
|
|
589
753
|
### Knowledge (RAG)
|
|
590
754
|
|
|
@@ -721,18 +885,18 @@ app.use('/chat', ai(async (req, ctx) => {
|
|
|
721
885
|
serve(app.handler(), { port: 3000 })
|
|
722
886
|
```
|
|
723
887
|
|
|
724
|
-
##
|
|
888
|
+
## runWorkflow
|
|
725
889
|
|
|
726
|
-
|
|
890
|
+
Multi-step DAG execution engine — packaged as a single AI SDK `Tool`. Use it with `streamText()` or `generateText()` when the LLM needs conditional logic, loops, or multi-step tool orchestration.
|
|
727
891
|
|
|
728
892
|
```ts
|
|
729
|
-
import {
|
|
893
|
+
import { tool, streamText } from 'ai'
|
|
894
|
+
import { runWorkflow } from 'weifuwu'
|
|
730
895
|
import { z } from 'zod'
|
|
731
896
|
|
|
732
|
-
// 1. Define tools (business capabilities)
|
|
733
897
|
const tools = {
|
|
734
898
|
queryUser: tool({
|
|
735
|
-
description: 'Query user info
|
|
899
|
+
description: 'Query user info',
|
|
736
900
|
inputSchema: z.object({ userId: z.string() }),
|
|
737
901
|
execute: async ({ userId }) => ({ id: userId, email: 'user@test.com', name: 'Test' }),
|
|
738
902
|
}),
|
|
@@ -741,139 +905,57 @@ const tools = {
|
|
|
741
905
|
inputSchema: z.object({ to: z.string(), subject: z.string() }),
|
|
742
906
|
execute: async ({ to, subject }) => ({ sent: true }),
|
|
743
907
|
}),
|
|
908
|
+
runWF: runWorkflow({ tools: { queryUser, sendEmail } }),
|
|
744
909
|
}
|
|
745
910
|
|
|
746
|
-
//
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
// POST /agent { nodes: [...] } → 200 { workflow: {...}, result: ... }
|
|
750
|
-
|
|
751
|
-
// With SSE streaming:
|
|
752
|
-
app.use('/agent-stream', workflow(() => ({ tools, stream: true })))
|
|
753
|
-
// POST /agent-stream { nodes: [...] }
|
|
754
|
-
// → 200 { workflowId: "xxx", eventsUrl: "/xxx/events" }
|
|
755
|
-
// GET /agent-stream/:workflowId/events
|
|
756
|
-
// → SSE: workflow-start → node-start → node-end → complete
|
|
757
|
-
|
|
758
|
-
// With LLM model (generates workflow from goal):
|
|
759
|
-
app.use('/agent-llm', workflow(() => ({
|
|
911
|
+
// Use in any streamText call — the LLM can decide when to trigger a workflow
|
|
912
|
+
const result = await streamText({
|
|
913
|
+
model,
|
|
760
914
|
tools,
|
|
761
|
-
|
|
762
|
-
})))
|
|
763
|
-
// POST /agent-llm { goal: "给用户123发欢迎邮件" }
|
|
764
|
-
// ← LLM generates → executes → returns result
|
|
765
|
-
```
|
|
766
|
-
|
|
767
|
-
### Tool
|
|
768
|
-
|
|
769
|
-
```ts
|
|
770
|
-
import { tool } from 'weifuwu'
|
|
771
|
-
import { z } from 'zod'
|
|
772
|
-
|
|
773
|
-
const myTool = tool({
|
|
774
|
-
description: '做什么的,返回什么',
|
|
775
|
-
inputSchema: z.object({ key: z.string() }),
|
|
776
|
-
execute: async (input, ctx) => {
|
|
777
|
-
return { result: input.key }
|
|
778
|
-
},
|
|
779
|
-
})
|
|
780
|
-
```
|
|
781
|
-
|
|
782
|
-
`ctx.onStream` 用于流式推送(如 LLM token 输出):
|
|
783
|
-
|
|
784
|
-
```ts
|
|
785
|
-
const llmTool = tool({
|
|
786
|
-
description: '生成文本',
|
|
787
|
-
inputSchema: z.object({ prompt: z.string() }),
|
|
788
|
-
execute: async (input, ctx) => {
|
|
789
|
-
const stream = await openai.chat.completions.create({ ... })
|
|
790
|
-
let full = ''
|
|
791
|
-
for await (const chunk of stream) {
|
|
792
|
-
full += chunk.choices[0]?.delta?.content || ''
|
|
793
|
-
ctx.onStream?.({ type: 'llm-stream', chunk, accumulated: full })
|
|
794
|
-
}
|
|
795
|
-
return { text: full }
|
|
796
|
-
},
|
|
915
|
+
messages: [{ role: 'user', content: '查询用户123,如果存在则发送欢迎邮件' }],
|
|
797
916
|
})
|
|
798
917
|
```
|
|
799
918
|
|
|
800
|
-
###
|
|
919
|
+
### Node types
|
|
801
920
|
|
|
802
|
-
7 built-in node types:
|
|
921
|
+
7 built-in node types for defining the execution graph:
|
|
803
922
|
|
|
804
923
|
| Node | Purpose | Input |
|
|
805
924
|
|------|---------|-------|
|
|
806
|
-
| `call` | Call a
|
|
807
|
-
| `set` |
|
|
925
|
+
| `call` | Call a registered AI SDK Tool | `{ tool: "name", args: {...} }` |
|
|
926
|
+
| `set` | Assign a variable | `{ name: "x", value: 42 }` |
|
|
808
927
|
| `get` | Read a variable | `{ name: "x" }` |
|
|
809
928
|
| `eval` | Evaluate an expression | `{ expression: "$var.x + 1" }` |
|
|
810
929
|
| `if` | Conditional branch | `{ conditions: [{ test: ..., body: [nodes] }] }` |
|
|
811
930
|
| `while` | Loop | `{ condition: "$var.i < 5" }, body: [nodes]` |
|
|
812
931
|
| `http` | HTTP request | `{ url: "https://...", method: "GET" }` |
|
|
813
932
|
|
|
814
|
-
###
|
|
933
|
+
### Reference syntax
|
|
815
934
|
|
|
816
935
|
| Pattern | Meaning | Example |
|
|
817
936
|
|---------|---------|---------|
|
|
818
937
|
| `$var.x` | Variable `x` | `$var.counter` |
|
|
819
938
|
| `$nodes.u.output` | Full output of node `u` | `$nodes.u.output` |
|
|
820
939
|
| `$nodes.u.output.field` | Specific field | `$nodes.u.output.email` |
|
|
821
|
-
| `$input.userId` |
|
|
822
|
-
| `42`, `true`, `"hello"` | Literal values | Passed as-is |
|
|
823
|
-
|
|
824
|
-
### Engine API
|
|
825
|
-
|
|
826
|
-
For programmatic use outside of Router:
|
|
827
|
-
|
|
828
|
-
```ts
|
|
829
|
-
import { createWorkflowEngine, createSSEManager } from 'weifuwu'
|
|
830
|
-
|
|
831
|
-
const sse = createSSEManager()
|
|
832
|
-
const engine = createWorkflowEngine({ tools, sseManager: sse })
|
|
940
|
+
| `$input.userId` | Input param | `$input.userId` |
|
|
833
941
|
|
|
834
|
-
|
|
835
|
-
const result = await engine.execute({ nodes: [...] })
|
|
942
|
+
### LLM generation
|
|
836
943
|
|
|
837
|
-
|
|
838
|
-
engine.runAsync('wf-1', { nodes: [...] })
|
|
839
|
-
```
|
|
840
|
-
|
|
841
|
-
### SSE Events
|
|
944
|
+
Pass a `model` to `runWorkflow` — the LLM generates the workflow JSON from a goal:
|
|
842
945
|
|
|
843
946
|
```ts
|
|
844
|
-
const
|
|
845
|
-
|
|
947
|
+
const runWF = runWorkflow({
|
|
948
|
+
tools: { queryUser, sendEmail },
|
|
949
|
+
model: openai('gpt-4o'),
|
|
950
|
+
})
|
|
846
951
|
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
// event: llm-stream — { nodeId, chunk, accumulated }
|
|
852
|
-
// event: complete — { result, duration }
|
|
853
|
-
// event: error — { error }
|
|
952
|
+
const result = await streamText({
|
|
953
|
+
model,
|
|
954
|
+
tools: { runWF },
|
|
955
|
+
})
|
|
854
956
|
```
|
|
855
957
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
Define reusable sub-workflows in the `functions` field:
|
|
859
|
-
|
|
860
|
-
```json
|
|
861
|
-
{
|
|
862
|
-
"functions": {
|
|
863
|
-
"double": {
|
|
864
|
-
"inputSchema": { "type": "object", "properties": { "x": { "type": "number" } } },
|
|
865
|
-
"workflow": {
|
|
866
|
-
"nodes": [
|
|
867
|
-
{ "id": "calc", "tool": "eval", "input": { "expression": "$input.x * 2" } }
|
|
868
|
-
]
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
},
|
|
872
|
-
"nodes": [
|
|
873
|
-
{ "id": "call_double", "tool": "call", "input": { "function": "double", "args": { "x": 21 } } }
|
|
874
|
-
]
|
|
875
|
-
}
|
|
876
|
-
```
|
|
958
|
+
The LLM calls `runWF` with a goal, and `runWorkflow` internally calls `generateText` to produce the workflow nodes, then executes them.
|
|
877
959
|
|
|
878
960
|
## React pages with tsx()
|
|
879
961
|
|
|
@@ -1074,14 +1156,12 @@ export default function NotFound() {
|
|
|
1074
1156
|
## Usage within a full app
|
|
1075
1157
|
|
|
1076
1158
|
```ts
|
|
1077
|
-
import { serve, Router, ai, graphql
|
|
1078
|
-
import { tsx } from 'weifuwu/tsx'
|
|
1159
|
+
import { serve, Router, ai, graphql } from 'weifuwu'
|
|
1079
1160
|
|
|
1080
1161
|
const app = new Router()
|
|
1081
1162
|
app.use('/', await tsx({ dir: './pages/' }))
|
|
1082
1163
|
app.use('/chat', ai(async (req) => ({ model: openai('gpt-4o'), messages: (await req.json()).messages })))
|
|
1083
1164
|
app.use('/graphql', graphql(() => ({ schema: `type Query { hello: String }`, resolvers: { Query: { hello: () => 'world' } } })))
|
|
1084
|
-
app.use('/agent', workflow(() => ({ tools: myTools, stream: true })))
|
|
1085
1165
|
app.ws('/chat', { message(ws, _, data) { ws.send(data) } })
|
|
1086
1166
|
|
|
1087
1167
|
serve(app.handler(), { websocket: app.websocketHandler() })
|
|
@@ -1214,10 +1294,25 @@ Returns `TenantModule` — `{ migrate, middleware, router, graphql, close }`.
|
|
|
1214
1294
|
| `model` | env `OPENAI_MODEL` → Ollama | `LanguageModel` from ai SDK |
|
|
1215
1295
|
| `embeddingModel` | env `OPENAI_EMBEDDING_MODEL` → Ollama | `EmbeddingModel` for knowledge RAG |
|
|
1216
1296
|
| `embeddingDimension` | `1024` | Vector dimension for pgvector |
|
|
1217
|
-
| `tools` | — | Tools for
|
|
1297
|
+
| `tools` | — | Tools for tool-use agents (ai SDK `Tool` objects) |
|
|
1218
1298
|
|
|
1219
1299
|
Returns `AgentModule` — `{ migrate, router, run, addKnowledge, close }`.
|
|
1220
1300
|
|
|
1301
|
+
### `opencode(options)`
|
|
1302
|
+
|
|
1303
|
+
| Option | Default | Description |
|
|
1304
|
+
|--------|---------|-------------|
|
|
1305
|
+
| `pg` | — | PostgreSQL client from `postgres()` |
|
|
1306
|
+
| `workspace` | `process.cwd()` | Base directory for `.sessions` |
|
|
1307
|
+
| `model` | `'deepseek-v4-flash'` | LLM model name |
|
|
1308
|
+
| `baseURL` | env `DEEPSEEK_BASE_URL` | API base URL |
|
|
1309
|
+
| `apiKey` | env `DEEPSEEK_API_KEY` | API key |
|
|
1310
|
+
| `systemPrompt` | — | Custom system prompt |
|
|
1311
|
+
| `skills` | `[]` | Static skill definitions |
|
|
1312
|
+
| `permissions` | — | Tool permission config |
|
|
1313
|
+
|
|
1314
|
+
Returns `OpencodeModule` — `{ migrate, router, wsHandler, close }`.
|
|
1315
|
+
|
|
1221
1316
|
### `messager(options)`
|
|
1222
1317
|
|
|
1223
1318
|
| Option | Default | Description |
|
|
@@ -1278,16 +1373,17 @@ serve(app.handler(), { websocket: app.websocketHandler() })
|
|
|
1278
1373
|
|
|
1279
1374
|
| Import | Description |
|
|
1280
1375
|
|--------|-------------|
|
|
1281
|
-
| `postgres(options?)` | PostgreSQL connection +
|
|
1376
|
+
| `postgres(options?)` | PostgreSQL connection + DDL schema builder + transactions + module lifecycle |
|
|
1282
1377
|
| `redis(options?)` | Redis client (ioredis) — injects `ctx.redis` |
|
|
1283
1378
|
| `queue(options?)` | Redis-backed job queue — immediate, delayed, cron scheduling |
|
|
1284
1379
|
| `user(options)` | Built-in authentication (password + OAuth2 Server + JWT, middleware) |
|
|
1285
1380
|
| `tenant(options)` | Multi-tenant BaaS — dynamic tables, REST + GraphQL auto-generation, row-level isolation |
|
|
1286
|
-
| `agent(options)` | AI Agent — chat/
|
|
1381
|
+
| `agent(options)` | AI Agent — chat/tool-use/knowledge, Ollama-ready, programmatic API |
|
|
1287
1382
|
| `messager(options)` | Real-time messaging — channels, WebSocket, agent routing, webhooks |
|
|
1383
|
+
| `opencode(options)` | AI programming assistant — chat agents with tools, skills, permissions, isolated workspaces |
|
|
1288
1384
|
| `graphql(handler)` | GraphQL endpoint (GET/POST + GraphiQL) |
|
|
1289
1385
|
| `ai(handler)` | AI streaming endpoint (POST) |
|
|
1290
|
-
| `
|
|
1386
|
+
| `runWorkflow(options)` | DAG execution engine as an AI SDK `Tool` — use with `streamText()` |
|
|
1291
1387
|
|
|
1292
1388
|
### Deploy
|
|
1293
1389
|
|
|
@@ -1305,9 +1401,14 @@ serve(app.handler(), { websocket: app.websocketHandler() })
|
|
|
1305
1401
|
| `setCookie(res, name, value, options?)` | Set cookie (returns new Response) |
|
|
1306
1402
|
| `deleteCookie(res, name)` | Delete cookie (returns new Response) |
|
|
1307
1403
|
| `useTsx()` | Hook returning `{ params, query, user, parsed }` from `TsxContext` |
|
|
1308
|
-
| `
|
|
1309
|
-
| `
|
|
1310
|
-
| `
|
|
1404
|
+
| `runWorkflow(options)` | Create a DAG execution AI SDK `Tool` — `{ tools?, model?, maxSteps? }` |
|
|
1405
|
+
| `pgTable(name, columns)` | Type-safe table schema definition with DDL + CRUD |
|
|
1406
|
+
| `pg.table(name, columns)` | Pre-bound table (no `sql` parameter needed for CRUD) |
|
|
1407
|
+
| `serial()`, `uuid()`, `text()`, `integer()`, `boolean()`, `timestamptz()`, `jsonb()`, `textArray()`, `vector()` | Column type builders |
|
|
1408
|
+
| `sql(strings, ...)` | SQL expression literal for defaults and SET values (e.g. `sql\`NOW()\``) |
|
|
1409
|
+
| `PgModule` | Base class for database-backed modules (provides `sql`, `close()`) |
|
|
1410
|
+
| `BoundTable` | Table with pre-bound `sql` — returned by `pg.table()` |
|
|
1411
|
+
| `FindOptions` | Query options: `{ orderBy?, limit?, offset? }` for `find()` |
|
|
1311
1412
|
|
|
1312
1413
|
Import `useTsx` and `TsxContext` from `'weifuwu'`.
|
|
1313
1414
|
|
package/dist/agent/migrate.d.ts
CHANGED
package/dist/agent/rest.d.ts
CHANGED
package/dist/agent/run.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type { Sql } from '
|
|
1
|
+
import { type LanguageModel, type EmbeddingModel, type Tool } from 'ai';
|
|
2
|
+
import type { Sql } from '../vendor.ts';
|
|
3
3
|
import type { RunParams, RunResult, KnowledgeDoc } from './types.ts';
|
|
4
4
|
interface RunnerDeps {
|
|
5
5
|
sql: Sql<{}>;
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export interface AgentConfig {
|
|
|
4
4
|
tenant_id: string | null;
|
|
5
5
|
name: string;
|
|
6
6
|
description: string;
|
|
7
|
-
type: 'chat' | '
|
|
7
|
+
type: 'chat' | 'tool-use';
|
|
8
8
|
model: string;
|
|
9
9
|
system_prompt: string;
|
|
10
10
|
owner_id: number;
|
|
@@ -36,7 +36,7 @@ export type RunResult = {
|
|
|
36
36
|
stream: ReadableStream<Uint8Array>;
|
|
37
37
|
};
|
|
38
38
|
export interface AgentOptions {
|
|
39
|
-
pg:
|
|
39
|
+
pg: import('../postgres/types.ts').PostgresClient;
|
|
40
40
|
model?: LanguageModel;
|
|
41
41
|
embeddingModel?: EmbeddingModel;
|
|
42
42
|
embeddingDimension?: number;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { LanguageModel } from 'ai';
|
|
2
|
+
export declare function runWorkflow(opts?: {
|
|
3
|
+
tools?: Record<string, any>;
|
|
4
|
+
model?: LanguageModel;
|
|
5
|
+
maxSteps?: number;
|
|
6
|
+
}): import("ai").Tool<{
|
|
7
|
+
goal: string;
|
|
8
|
+
nodes?: any[];
|
|
9
|
+
}, {
|
|
10
|
+
result: unknown;
|
|
11
|
+
nodeOutputs: {
|
|
12
|
+
[k: string]: unknown;
|
|
13
|
+
};
|
|
14
|
+
}>;
|