feat: schema + execute abstraction for Agent-friendly deploy
- Agent provides schema (JSON Schema) + execute (function body) - Sigil auto-generates full Worker code via codegen.ts - Input parsing: GET query params + POST JSON body, auto type conversion - Required field validation, default values - find mode returns schema so Agent knows how to call - Backward compatible: raw code deploy still works
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import type { InputSchema } from '../codegen.js'
|
||||
|
||||
export interface DeployParams {
|
||||
name: string | null // null = 自动生成 t-{hash}
|
||||
code: string
|
||||
code?: string // 模式 A:完整 Worker 代码
|
||||
schema?: InputSchema // 模式 B:输入 schema
|
||||
execute?: string // 模式 B:函数体
|
||||
type: 'persistent' | 'normal' | 'ephemeral'
|
||||
ttl?: number // 秒,仅 ephemeral
|
||||
bindings?: string[]
|
||||
@@ -29,6 +33,7 @@ export interface Capability {
|
||||
description?: string
|
||||
tags?: string[]
|
||||
examples?: string[]
|
||||
schema?: InputSchema // 新增:find 模式返回,让 Agent 知道怎么调用
|
||||
}
|
||||
|
||||
export interface QueryParams {
|
||||
@@ -47,6 +52,7 @@ export interface QueryItem {
|
||||
deployed?: boolean
|
||||
access_count?: number
|
||||
score: number
|
||||
schema?: InputSchema // 新增:find 模式返回
|
||||
}
|
||||
|
||||
export interface QueryResult {
|
||||
|
||||
@@ -44,7 +44,11 @@ export class WorkerPool implements SigilBackend {
|
||||
}
|
||||
|
||||
async deploy(params: DeployParams): Promise<DeployResult> {
|
||||
const { name, code, type, ttl, bindings, description, tags, examples } = params
|
||||
const { name, code, schema, type, ttl, bindings, description, tags, examples } = params
|
||||
|
||||
if (!code) {
|
||||
throw new Error('deploy: code is required (should be pre-generated by router)')
|
||||
}
|
||||
|
||||
// Determine capability name
|
||||
let capability: string
|
||||
@@ -97,6 +101,7 @@ export class WorkerPool implements SigilBackend {
|
||||
description,
|
||||
tags,
|
||||
examples,
|
||||
schema,
|
||||
})
|
||||
await this.kv.setLru(capability, {
|
||||
last_access: now,
|
||||
@@ -396,6 +401,7 @@ export class WorkerPool implements SigilBackend {
|
||||
description: meta.description,
|
||||
tags: meta.tags,
|
||||
examples: meta.examples,
|
||||
schema: meta.schema,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -419,6 +425,7 @@ export class WorkerPool implements SigilBackend {
|
||||
deployed: cap.deployed,
|
||||
access_count: cap.access_count,
|
||||
score: 0.5, // Default score for fallback
|
||||
schema: cap.schema,
|
||||
}))
|
||||
|
||||
const effectiveMode = (mode === 'find' && !q) ? 'explore' : mode
|
||||
@@ -443,6 +450,7 @@ export class WorkerPool implements SigilBackend {
|
||||
deployed: c.lru.deployed,
|
||||
access_count: c.lru.access_count,
|
||||
score: Math.round(c.score * 1000) / 1000,
|
||||
schema: c.meta.schema,
|
||||
}))
|
||||
|
||||
// Merge embedding results with fallback results (embedding takes priority)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// KV key prefixes and data types
|
||||
|
||||
import type { InputSchema } from './codegen.js'
|
||||
|
||||
export interface KvCodeValue {
|
||||
code: string
|
||||
}
|
||||
@@ -12,6 +14,7 @@ export interface KvMetaValue {
|
||||
description?: string
|
||||
tags?: string[]
|
||||
examples?: string[]
|
||||
schema?: InputSchema // 新增:模式 B deploy 时存储,find 模式返回
|
||||
}
|
||||
|
||||
export interface KvLruValue {
|
||||
|
||||
+30
-2
@@ -2,6 +2,8 @@ import type { SigilBackend } from './backend/types.js'
|
||||
import { AuthModule, AuthError, DeployCooldownError } from './auth.js'
|
||||
import { KvStore } from './kv.js'
|
||||
import { PageRateLimitError } from './lru.js'
|
||||
import { generateWorkerCode } from './codegen.js'
|
||||
import type { InputSchema } from './codegen.js'
|
||||
|
||||
export interface RouterEnv {
|
||||
SIGIL_KV: KVNamespace
|
||||
@@ -64,7 +66,9 @@ async function handleDeploy(request: Request, env: RouterEnv): Promise<Response>
|
||||
|
||||
const body = await request.json() as {
|
||||
name: string | null
|
||||
code: string
|
||||
code?: string
|
||||
schema?: InputSchema
|
||||
execute?: string
|
||||
type: 'persistent' | 'normal' | 'ephemeral'
|
||||
ttl?: number
|
||||
bindings?: string[]
|
||||
@@ -73,12 +77,36 @@ async function handleDeploy(request: Request, env: RouterEnv): Promise<Response>
|
||||
examples?: string[]
|
||||
}
|
||||
|
||||
// Route validation
|
||||
if (body.code && (body.schema || body.execute)) {
|
||||
return jsonError(400, 'Cannot specify both code and schema/execute')
|
||||
}
|
||||
if (!body.code && !body.execute) {
|
||||
return jsonError(400, 'Must specify either code or schema+execute')
|
||||
}
|
||||
|
||||
let code: string
|
||||
let schema: InputSchema | undefined
|
||||
|
||||
if (body.code) {
|
||||
// 模式 A:直接部署
|
||||
code = body.code
|
||||
} else {
|
||||
// 模式 B:schema + execute
|
||||
if (!body.execute) {
|
||||
return jsonError(400, 'execute is required when using schema mode')
|
||||
}
|
||||
schema = body.schema || { type: 'object', properties: {} }
|
||||
code = generateWorkerCode(schema, body.execute)
|
||||
}
|
||||
|
||||
// Check deploy cooldown
|
||||
await env.auth.checkDeployCooldown()
|
||||
|
||||
const result = await env.backend.deploy({
|
||||
name: body.name,
|
||||
code: body.code,
|
||||
code,
|
||||
schema,
|
||||
type: body.type,
|
||||
ttl: body.ttl,
|
||||
bindings: body.bindings,
|
||||
|
||||
@@ -331,4 +331,44 @@ describe('Query API', () => {
|
||||
expect(Math.abs(item.score - rounded)).toBeLessThan(0.0001)
|
||||
}
|
||||
})
|
||||
|
||||
// Test 15: find 模式返回 schema(如果有)
|
||||
it('find 模式返回 schema 字段(via fallback path)', async () => {
|
||||
const now = Date.now()
|
||||
const kv2 = new KvStore(mockKv)
|
||||
const testSchema = {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
from: { type: 'string', description: 'Source currency' },
|
||||
to: { type: 'string', description: 'Target currency' },
|
||||
},
|
||||
required: ['from', 'to'],
|
||||
}
|
||||
|
||||
// Insert capability with schema manually (simulating schema+execute deploy)
|
||||
await kv2.setMeta('schema-cap', {
|
||||
type: 'persistent',
|
||||
created_at: now,
|
||||
description: 'schema capability for testing',
|
||||
tags: ['schema-test'],
|
||||
schema: testSchema,
|
||||
})
|
||||
await kv2.setLru('schema-cap', { last_access: now, access_count: 0, deployed: true })
|
||||
|
||||
const result = await pool.query({ q: 'schema-test', mode: 'find' })
|
||||
const item = result.items.find(i => i.capability === 'schema-cap')
|
||||
expect(item).toBeDefined()
|
||||
expect(item!.schema).toBeDefined()
|
||||
expect(item!.schema?.properties.from.type).toBe('string')
|
||||
expect(item!.schema?.required).toContain('from')
|
||||
expect(item!.schema?.required).toContain('to')
|
||||
})
|
||||
|
||||
// Test 16: explore 模式不返回 schema
|
||||
it('explore 模式不返回 schema 字段', async () => {
|
||||
const result = await pool.query({ mode: 'explore' })
|
||||
for (const item of result.items) {
|
||||
expect(item).not.toHaveProperty('schema')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -78,4 +78,121 @@ describe('S1: 部署能力', () => {
|
||||
const route = await kv.getRoute('ping')
|
||||
expect(route?.worker_name).toBe('s-ping')
|
||||
})
|
||||
|
||||
// --- 模式 B: schema + execute ---
|
||||
|
||||
it('模式 B: schema + execute 通过 API 部署', async () => {
|
||||
const req = makeRequest('POST', '/_api/deploy', {
|
||||
token: 'deploy-token',
|
||||
body: {
|
||||
name: 'adder',
|
||||
type: 'normal',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
a: { type: 'number', description: 'First number' },
|
||||
b: { type: 'number', description: 'Second number' },
|
||||
},
|
||||
required: ['a', 'b'],
|
||||
},
|
||||
execute: 'return JSON.stringify({ sum: input.a + input.b })',
|
||||
},
|
||||
})
|
||||
|
||||
const resp = await handleRequest(req, { SIGIL_KV: mockKv, backend: pool, auth, kv })
|
||||
expect(resp.status).toBe(201)
|
||||
|
||||
const body = await resp.json() as { capability: string; url: string }
|
||||
expect(body.capability).toBe('adder')
|
||||
expect(body.url).toBe('https://sigil.shazhou.workers.dev/run/adder')
|
||||
})
|
||||
|
||||
it('模式 B: 生成的 code 存入 KV(包含 export default)', async () => {
|
||||
const req = makeRequest('POST', '/_api/deploy', {
|
||||
token: 'deploy-token',
|
||||
body: {
|
||||
name: 'greeter',
|
||||
type: 'normal',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', default: 'World' },
|
||||
},
|
||||
},
|
||||
execute: 'return "Hello, " + input.name + "!"',
|
||||
},
|
||||
})
|
||||
|
||||
await handleRequest(req, { SIGIL_KV: mockKv, backend: pool, auth, kv })
|
||||
|
||||
const code = await kv.getCode('greeter')
|
||||
expect(code).toBeTruthy()
|
||||
expect(code).toContain('export default')
|
||||
expect(code).toContain('async fetch(request)')
|
||||
})
|
||||
|
||||
it('模式 B: schema 存入 KV meta', async () => {
|
||||
const schema = {
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
from: { type: 'string', description: 'Source currency' },
|
||||
to: { type: 'string', description: 'Target currency' },
|
||||
amount: { type: 'number', description: 'Amount', default: 1 },
|
||||
},
|
||||
required: ['from', 'to'],
|
||||
}
|
||||
|
||||
const req = makeRequest('POST', '/_api/deploy', {
|
||||
token: 'deploy-token',
|
||||
body: {
|
||||
name: 'currency',
|
||||
type: 'persistent',
|
||||
description: 'Currency converter',
|
||||
tags: ['finance'],
|
||||
schema,
|
||||
execute: 'return JSON.stringify({ from: input.from, to: input.to, amount: input.amount })',
|
||||
},
|
||||
})
|
||||
|
||||
await handleRequest(req, { SIGIL_KV: mockKv, backend: pool, auth, kv })
|
||||
|
||||
const meta = await kv.getMeta('currency')
|
||||
expect(meta?.schema).toBeDefined()
|
||||
expect(meta?.schema?.properties.from.type).toBe('string')
|
||||
expect(meta?.schema?.required).toContain('from')
|
||||
expect(meta?.schema?.required).toContain('to')
|
||||
})
|
||||
|
||||
it('模式 B + A 同时提供 → 400 错误', async () => {
|
||||
const req = makeRequest('POST', '/_api/deploy', {
|
||||
token: 'deploy-token',
|
||||
body: {
|
||||
name: 'bad',
|
||||
type: 'normal',
|
||||
code: 'export default { fetch() { return new Response("hi") } }',
|
||||
schema: { properties: {} },
|
||||
execute: 'return "hello"',
|
||||
},
|
||||
})
|
||||
|
||||
const resp = await handleRequest(req, { SIGIL_KV: mockKv, backend: pool, auth, kv })
|
||||
expect(resp.status).toBe(400)
|
||||
const body = await resp.json() as { error: string }
|
||||
expect(body.error).toContain('Cannot specify both code and schema/execute')
|
||||
})
|
||||
|
||||
it('code 和 execute 都不提供 → 400 错误', async () => {
|
||||
const req = makeRequest('POST', '/_api/deploy', {
|
||||
token: 'deploy-token',
|
||||
body: {
|
||||
name: 'bad',
|
||||
type: 'normal',
|
||||
},
|
||||
})
|
||||
|
||||
const resp = await handleRequest(req, { SIGIL_KV: mockKv, backend: pool, auth, kv })
|
||||
expect(resp.status).toBe(400)
|
||||
const body = await resp.json() as { error: string }
|
||||
expect(body.error).toContain('Must specify either code or schema+execute')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user