Files
agent-bounty-protocol/packages/contracts/src/schemas/index.ts
2026-06-06 22:56:21 +08:00

378 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @agent-bounty/contracts — Zod Validation Schemas
*
* 唯一的驗證真實來源。
* MCP server、後端 API、前端表單都必須 import 這裡的 schema
* 禁止在各自的程式庫中重新定義任何 VibeWork 資料結構。
*/
import { z } from "zod";
import {
TaskStatus,
TaskDifficulty,
ValidationMode,
JudgeOverallResult,
JudgeTestStatus,
JudgeErrorClassification,
SettlementPhase,
SettlementCaptureMode,
SupportedCurrency,
LeadStatus,
AttributionModel,
TaskErrorClassification,
MAX_TASK_RETRY_COUNT,
} from "../enums/index.js";
// ─────────────────────────────────────────────
// 通用基礎型別
// ─────────────────────────────────────────────
const UUIDSchema = z.string().uuid("無效的 UUID 格式");
const CUIDSchema = z.string().cuid("無效的 CUID 格式");
const PositiveIntSchema = z.number().int().positive();
const NonNegativeIntSchema = z.number().int().nonnegative();
/** 金額(以最小單位計,例如美分 / 台幣分) */
const MoneyAmountSchema = z
.number()
.int()
.nonnegative("金額不得為負數")
.max(1_000_000_00, "單筆金額超過上限($1,000,000");
/** scope_clarity_scoreAI 評估任務清晰度Phase 1 要求 ≥ 0.90 */
const ScopeScoreSchema = z
.number()
.min(0)
.max(1)
.refine((v) => v >= 0.9, {
message: "scope_clarity_score 必須 ≥ 0.90 才能進入主任務池",
});
// ─────────────────────────────────────────────
// Task 驗收條件 Schema
// ─────────────────────────────────────────────
export const AcceptanceCriteriaSchema = z.object({
validation_mode: z.enum([
ValidationMode.VITEST_UNIT,
ValidationMode.PLAYWRIGHT_E2E,
ValidationMode.AST_PARSING,
ValidationMode.VISUAL_REGRESSION,
]),
/** 由平台提供的最小測試檔內容stringAgent 不得修改此欄位 */
test_file_content: z.string().min(20, "測試檔內容不得為空"),
/** 關鍵斷言規則(可選,補充說明) */
rules: z
.array(
z.object({
assertion: z.string(),
expected: z.unknown(),
description: z.string().optional(),
})
)
.optional(),
});
// ─────────────────────────────────────────────
// Task Bounty 核心 SchemaOpen Bounty Board
// ─────────────────────────────────────────────
export const TaskBountySchema = z.object({
task_id: UUIDSchema,
title: z.string().min(5).max(120),
description: z.string().min(20).max(2000),
status: z.enum([
TaskStatus.OPEN,
TaskStatus.EXECUTING,
TaskStatus.VERIFYING,
TaskStatus.COMPLETED,
TaskStatus.FAILED,
TaskStatus.FAILED_RETRYABLE,
TaskStatus.CANCELLED,
TaskStatus.DISPUTED,
TaskStatus.REFUND_PENDING,
TaskStatus.PAYOUT_READY,
TaskStatus.PAYOUT_SETTLED,
TaskStatus.ARCHIVED,
]),
difficulty: z.enum([
TaskDifficulty.HELLO_WORLD,
TaskDifficulty.COMPONENT,
TaskDifficulty.VIEW,
TaskDifficulty.EPIC,
]),
/** 最小可行要求 ≥ 0.90 才能進入公開任務池 */
scope_clarity_score: ScopeScoreSchema,
error_classification: z.enum([
TaskErrorClassification.RETRYABLE,
TaskErrorClassification.NON_RETRYABLE,
]),
reward: z.object({
/** 金額整數以最小貨幣單位計USD cents / TWD 元) */
amount: MoneyAmountSchema,
currency: z.enum([
SupportedCurrency.USD,
SupportedCurrency.TWD,
SupportedCurrency.USDC,
]),
/** 顯示用格式化金額(例如 "NT$30" / "$1.00"),由後端產生 */
display_amount: z.string().optional(),
}),
acceptance_criteria: AcceptanceCriteriaSchema,
/** 要求使用的技術棧 */
required_stack: z.array(z.string()).min(1).default(["React", "Tailwind CSS"]),
/** 已重試次數 */
retry_count: NonNegativeIntSchema.max(MAX_TASK_RETRY_COUNT).default(0),
/** Auth-Hold 的 Stripe payment_intent_id */
stripe_payment_intent_id: z.string().optional(),
/** 任務到期時間(對應 Redis TTL */
expires_at: z.string().datetime().optional(),
created_at: z.string().datetime(),
updated_at: z.string().datetime(),
});
// ─────────────────────────────────────────────
// claim_task Request/Response
// ─────────────────────────────────────────────
export const ClaimTaskRequestSchema = z.object({
task_id: UUIDSchema,
/** Agent 收款錢包Stripe Connect account 或 EVM 地址) */
developer_wallet: z
.string()
.regex(
/^(0x[a-fA-F0-9]{40}|acct_[a-zA-Z0-9]+)$/,
"developer_wallet 必須是有效的 EVM 地址或 Stripe Connect ID"
),
});
export const ClaimTaskResponseSchema = z.object({
task_id: UUIDSchema,
status: z.literal(TaskStatus.EXECUTING),
/** Stripe Auth-Hold 的金額(供 Agent 確認) */
held_amount: MoneyAmountSchema,
held_currency: z.enum([SupportedCurrency.USD, SupportedCurrency.TWD]),
/** Redis TTL 過期時間 */
expires_at: z.string().datetime(),
/** 接案 Agent 的冪等憑證 */
claim_token: z.string().uuid(),
});
// ─────────────────────────────────────────────
// submit_solution Request/Response
// ─────────────────────────────────────────────
const DeliverableFileSchema = z.object({
path: z.string().min(1).max(260, "路徑長度超過限制"),
content: z
.string()
.min(10, "檔案內容不得為空")
.max(500_000, "單檔案內容超過 500KB 限制"),
language: z.enum(["typescript", "javascript", "tsx", "jsx", "css", "html"]),
});
export const SubmitSolutionRequestSchema = z.object({
task_id: UUIDSchema,
/** 接案時取得的冪等憑證,防止重複提交 */
claim_token: z.string().uuid(),
deliverables: z.object({
files: z
.array(DeliverableFileSchema)
.min(1, "至少需要提交一個檔案")
.max(20, "單次提交不得超過 20 個檔案"),
notes: z.string().max(1000).optional(),
}),
});
export const SubmitSolutionResponseSchema = z.object({
task_id: UUIDSchema,
submission_id: UUIDSchema,
status: z.literal(TaskStatus.VERIFYING),
/** Judge 預計完成時間ISO 8601 */
estimated_judge_complete_at: z.string().datetime().optional(),
});
// ─────────────────────────────────────────────
// Judge Result SchemaE2B 沙盒回傳)
// ─────────────────────────────────────────────
export const JudgeTestResultSchema = z.object({
name: z.string(),
status: z.enum([
JudgeTestStatus.PASSED,
JudgeTestStatus.FAILED,
JudgeTestStatus.SKIPPED,
]),
duration_ms: NonNegativeIntSchema,
logs: z.string().optional(),
assertion_diff: z.string().optional(),
});
export const JudgeResultSchema = z.object({
attempt_id: UUIDSchema,
task_id: UUIDSchema,
submission_id: UUIDSchema,
overall_result: z.enum([
JudgeOverallResult.PASS,
JudgeOverallResult.FAIL,
JudgeOverallResult.TIMEOUT,
]),
tests: z.array(JudgeTestResultSchema),
artifacts: z
.object({
screenshot_url: z.string().url().optional(),
logs_url: z.string().url().optional(),
coverage_summary: z.string().optional(),
diff_url: z.string().url().optional(),
})
.optional(),
/** 失敗類型overall_result=fail/timeout 時必填) */
error_classification: z
.enum([
JudgeErrorClassification.TEST_FAIL,
JudgeErrorClassification.LINT_FAIL,
JudgeErrorClassification.TIMEOUT,
JudgeErrorClassification.RESOURCE_EXHAUSTED,
JudgeErrorClassification.ENVIRONMENT_ERROR,
JudgeErrorClassification.NETWORK_DENIED,
JudgeErrorClassification.SANDBOX_CRASH,
JudgeErrorClassification.TEST_SETUP_FAIL,
JudgeErrorClassification.ENV_MISCONFIG,
])
.optional(),
/** 供 Builder/Scout 質量分數模型使用的錯誤指紋 */
error_signature: z.string().optional(),
/** 若為 true後端可以安排重試不計入 Agent 失敗分數) */
retryable: z.boolean(),
resource_usage: z.object({
cpu_ms: NonNegativeIntSchema,
mem_peak_mb: NonNegativeIntSchema,
io_bytes: NonNegativeIntSchema,
}),
judge_completed_at: z.string().datetime(),
});
// ─────────────────────────────────────────────
// Settlement Ledger Schema對帳台帳
// ─────────────────────────────────────────────
export const SettlementLedgerEntrySchema = z.object({
id: CUIDSchema,
task_id: UUIDSchema,
agent_id: z.string(),
human_client_id: z.string(),
/** Stripe 冪等 key格式 "{task_id}_{phase}_{attempt}" */
idempotency_key: z.string().min(10).max(255),
phase: z.enum([
SettlementPhase.AUTH_HOLD,
SettlementPhase.CAPTURE,
SettlementPhase.RELEASE,
SettlementPhase.REFUND,
SettlementPhase.PAYOUT,
SettlementPhase.DISPUTE,
SettlementPhase.CORRECTION,
]),
capture_mode: z.enum([
SettlementCaptureMode.STRIPE_AUTH_CAPTURE,
SettlementCaptureMode.BASE_SMART_CONTRACT,
]),
/** Stripe object IDpayment_intent_id / charge_id / refund_id */
stripe_object_id: z.string().optional(),
amount: MoneyAmountSchema,
currency: z.enum([SupportedCurrency.USD, SupportedCurrency.TWD]),
request_payload_hash: z.string(),
response_status: z.string(),
http_status: z.number().int().min(100).max(599),
attempt: PositiveIntSchema,
source: z.enum(["api", "webhook", "manual_replay"]),
/** 分潤快照capture 時寫入,不得事後修改) */
split: z
.object({
platform_amount: MoneyAmountSchema,
builder_amount: MoneyAmountSchema,
scout_amount: MoneyAmountSchema.optional(),
platform_rate: z.number().min(0).max(1),
builder_rate: z.number().min(0).max(1),
scout_rate: z.number().min(0).max(1).optional(),
})
.optional(),
created_at: z.string().datetime(),
updated_at: z.string().datetime(),
});
// ─────────────────────────────────────────────
// Lead SchemaScout 導流任務草案)
// ─────────────────────────────────────────────
export const LeadSchema = z.object({
lead_id: UUIDSchema,
scout_agent_id: z.string().optional(),
/** Scout 的 affiliate token用於歸因 */
affiliate_token: z.string().uuid().optional(),
attribution_model: z.enum([
AttributionModel.LAST_CLICK,
AttributionModel.FIRST_CLICK,
AttributionModel.LINEAR,
]),
status: z.enum([
LeadStatus.DRAFT,
LeadStatus.CONFIRMED,
LeadStatus.PAYMENT_AUTHORIZED,
LeadStatus.TASK_CREATED,
LeadStatus.EXPIRED,
LeadStatus.CANCELLED,
]),
/** 需求者原始需求(純文字,不超過 500 字) */
raw_requirement: z.string().min(10).max(500),
/** AI 生成的 PRD 草稿JSON */
prd_draft: z.record(z.string(), z.unknown()).optional(),
/** 付款連結Stripe Checkout URL */
payment_link: z.string().url().optional(),
/** 付款連結 TTL 過期時間 */
payment_link_expires_at: z.string().datetime().optional(),
/** 成功後建立的正式 task_id */
task_id: UUIDSchema.optional(),
created_at: z.string().datetime(),
updated_at: z.string().datetime(),
});
// ─────────────────────────────────────────────
// list_open_tasks Response Schema
// ─────────────────────────────────────────────
export const ListOpenTasksRequestSchema = z.object({
skills: z
.array(z.string().min(1).max(50))
.min(1, "至少需要指定一個技能"),
limit: z.number().int().min(1).max(20).default(5),
difficulty: z
.enum([
TaskDifficulty.HELLO_WORLD,
TaskDifficulty.COMPONENT,
TaskDifficulty.VIEW,
TaskDifficulty.EPIC,
])
.optional(),
});
/** 精簡版 Tasklist 用,避免暴露內部欄位) */
export const TaskSummarySchema = TaskBountySchema.pick({
task_id: true,
title: true,
status: true,
difficulty: true,
reward: true,
required_stack: true,
scope_clarity_score: true,
created_at: true,
}).extend({
description_preview: z.string().max(200),
});
export const ListOpenTasksResponseSchema = z.object({
tasks: z.array(TaskSummarySchema),
total_open: z.number().int().nonnegative(),
/** 任務池是否告警(供 MCP client 顯示警示) */
stockout_warning: z.boolean(),
});