chore: initial commit with Phase 0 setup
This commit is contained in:
230
packages/contracts/src/enums/index.ts
Normal file
230
packages/contracts/src/enums/index.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* @agent-bounty/contracts — Enums
|
||||
*
|
||||
* 所有狀態機 enum、分類常數的唯一真實來源。
|
||||
* MCP server、後端 API、前端 UI 都必須 import 這裡,禁止各自重新定義。
|
||||
*/
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Task 任務生命週期狀態機
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* TaskStatus:任務在系統中的完整生命週期。
|
||||
*
|
||||
* 狀態轉換規則(前後端、MCP、Judge Agent 必遵守):
|
||||
*
|
||||
* OPEN
|
||||
* → EXECUTING (claim_task:DB SELECT FOR UPDATE + Stripe Auth-Hold + Redis setex TTL 3600s)
|
||||
* → CANCELLED (client_cancel)
|
||||
*
|
||||
* EXECUTING
|
||||
* → VERIFYING (submit_solution)
|
||||
* → OPEN (Redis TTL expired → rollback + 釋放 Auth-Hold)
|
||||
* → CANCELLED (agent_cancel / client_cancel)
|
||||
* → FAILED (agent_abandon)
|
||||
*
|
||||
* VERIFYING
|
||||
* → COMPLETED (judge exit_code=0 → Stripe Capture → Ledger)
|
||||
* → FAILED_RETRYABLE (judge fail retryable, or timeout ≤ 5min)
|
||||
* → FAILED (judge fail non-retryable)
|
||||
*
|
||||
* FAILED_RETRYABLE
|
||||
* → OPEN (retry_count ≤ 3, auto)
|
||||
* → FAILED (retry_count > 3)
|
||||
*
|
||||
* FAILED
|
||||
* → OPEN (policy=auto_open)
|
||||
* → ARCHIVED (policy=archive)
|
||||
* → DISPUTED (client_dispute)
|
||||
*
|
||||
* COMPLETED
|
||||
* → DISPUTED (client_dispute, within 72h)
|
||||
* → PAYOUT_READY (dispute window closed, no dispute)
|
||||
* → PAYOUT_SETTLED (payout processed)
|
||||
*
|
||||
* DISPUTED
|
||||
* → OPEN (dispute_resolved: reopen)
|
||||
* → COMPLETED (dispute_resolved: keep)
|
||||
* → REFUND_PENDING (dispute_resolved: refund)
|
||||
* → ARCHIVED (dispute_resolved: archive)
|
||||
*
|
||||
* REFUND_PENDING → ARCHIVED
|
||||
* CANCELLED → ARCHIVED
|
||||
* PAYOUT_SETTLED → ARCHIVED
|
||||
* ARCHIVED → (終態)
|
||||
*/
|
||||
export const TaskStatus = {
|
||||
OPEN: "OPEN",
|
||||
EXECUTING: "EXECUTING",
|
||||
VERIFYING: "VERIFYING",
|
||||
COMPLETED: "COMPLETED",
|
||||
FAILED: "FAILED",
|
||||
FAILED_RETRYABLE: "FAILED_RETRYABLE",
|
||||
CANCELLED: "CANCELLED",
|
||||
DISPUTED: "DISPUTED",
|
||||
REFUND_PENDING: "REFUND_PENDING",
|
||||
PAYOUT_READY: "PAYOUT_READY",
|
||||
PAYOUT_SETTLED: "PAYOUT_SETTLED",
|
||||
ARCHIVED: "ARCHIVED",
|
||||
} as const;
|
||||
export type TaskStatus = (typeof TaskStatus)[keyof typeof TaskStatus];
|
||||
|
||||
/** 終態集合:進入後不可再轉換(除人工介入) */
|
||||
export const TERMINAL_TASK_STATUSES: ReadonlySet<TaskStatus> = new Set([
|
||||
TaskStatus.ARCHIVED,
|
||||
]);
|
||||
|
||||
/** 可從 FAILED/FAILED_RETRYABLE 自動重開的最大重試次數 */
|
||||
export const MAX_TASK_RETRY_COUNT = 3;
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Task 難度分級
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* TaskDifficulty:對應種子任務的定價與驗收複雜度。
|
||||
* HELLO_WORLD = $1 / COMPONENT = $5 / VIEW = $10 / EPIC = $20+
|
||||
*/
|
||||
export const TaskDifficulty = {
|
||||
HELLO_WORLD: "HELLO_WORLD",
|
||||
COMPONENT: "COMPONENT",
|
||||
VIEW: "VIEW",
|
||||
EPIC: "EPIC",
|
||||
} as const;
|
||||
export type TaskDifficulty = (typeof TaskDifficulty)[keyof typeof TaskDifficulty];
|
||||
|
||||
/** 難度對應的預設獎金(USD cents) */
|
||||
export const TASK_DIFFICULTY_DEFAULT_REWARD_CENTS: Record<TaskDifficulty, number> = {
|
||||
HELLO_WORLD: 100,
|
||||
COMPONENT: 500,
|
||||
VIEW: 1000,
|
||||
EPIC: 2000,
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Task 驗收模式
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
export const ValidationMode = {
|
||||
VITEST_UNIT: "VITEST_UNIT",
|
||||
PLAYWRIGHT_E2E: "PLAYWRIGHT_E2E",
|
||||
AST_PARSING: "AST_PARSING", // 僅用於靜態 HTML/CSS,不得用於 React 元件
|
||||
VISUAL_REGRESSION: "VISUAL_REGRESSION",
|
||||
} as const;
|
||||
export type ValidationMode = (typeof ValidationMode)[keyof typeof ValidationMode];
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Judge 執行結果
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
export const JudgeOverallResult = {
|
||||
PASS: "pass",
|
||||
FAIL: "fail",
|
||||
TIMEOUT: "timeout",
|
||||
} as const;
|
||||
export type JudgeOverallResult = (typeof JudgeOverallResult)[keyof typeof JudgeOverallResult];
|
||||
|
||||
export const JudgeTestStatus = {
|
||||
PASSED: "passed",
|
||||
FAILED: "failed",
|
||||
SKIPPED: "skipped",
|
||||
} as const;
|
||||
export type JudgeTestStatus = (typeof JudgeTestStatus)[keyof typeof JudgeTestStatus];
|
||||
|
||||
/**
|
||||
* JudgeErrorClassification:標準化失敗分類。
|
||||
* 影響後端決策:是否重試 / 扣押 Agent 分數 / 進入人工仲裁。
|
||||
*/
|
||||
export const JudgeErrorClassification = {
|
||||
TEST_FAIL: "TEST_FAIL", // 邏輯/行為不符(可 retryable)
|
||||
LINT_FAIL: "LINT_FAIL", // lint 錯誤(可 retryable)
|
||||
TIMEOUT: "TIMEOUT", // 超過 5 分鐘(可 retryable)
|
||||
RESOURCE_EXHAUSTED: "RESOURCE_EXHAUSTED", // CPU/Memory 超限(可 retryable)
|
||||
ENVIRONMENT_ERROR: "ENVIRONMENT_ERROR", // 沙盒或工具層失敗(不計 Agent 分)
|
||||
NETWORK_DENIED: "NETWORK_DENIED", // 試圖違規網路存取(non-retryable)
|
||||
SANDBOX_CRASH: "SANDBOX_CRASH", // E2B 內部崩潰(不計 Agent 分)
|
||||
TEST_SETUP_FAIL: "TEST_SETUP_FAIL", // 映像檔缺少依賴(不計 Agent 分)
|
||||
ENV_MISCONFIG: "ENV_MISCONFIG", // 設定錯誤(不計 Agent 分)
|
||||
} as const;
|
||||
export type JudgeErrorClassification = (typeof JudgeErrorClassification)[keyof typeof JudgeErrorClassification];
|
||||
|
||||
/** 不計入 Agent 失敗分數的環境層錯誤 */
|
||||
export const JUDGE_ENVIRONMENT_ERROR_TYPES: ReadonlySet<JudgeErrorClassification> = new Set([
|
||||
JudgeErrorClassification.ENVIRONMENT_ERROR,
|
||||
JudgeErrorClassification.SANDBOX_CRASH,
|
||||
JudgeErrorClassification.TEST_SETUP_FAIL,
|
||||
JudgeErrorClassification.ENV_MISCONFIG,
|
||||
]);
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 金流 / Settlement
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
export const SettlementPhase = {
|
||||
AUTH_HOLD: "auth_hold",
|
||||
CAPTURE: "capture",
|
||||
RELEASE: "release",
|
||||
REFUND: "refund",
|
||||
PAYOUT: "payout",
|
||||
DISPUTE: "dispute",
|
||||
CORRECTION: "correction",
|
||||
} as const;
|
||||
export type SettlementPhase = (typeof SettlementPhase)[keyof typeof SettlementPhase];
|
||||
|
||||
export const SettlementCaptureMode = {
|
||||
STRIPE_AUTH_CAPTURE: "STRIPE_AUTH_CAPTURE",
|
||||
BASE_SMART_CONTRACT: "BASE_SMART_CONTRACT",
|
||||
} as const;
|
||||
export type SettlementCaptureMode = (typeof SettlementCaptureMode)[keyof typeof SettlementCaptureMode];
|
||||
|
||||
export const SupportedCurrency = {
|
||||
USD: "USD",
|
||||
TWD: "TWD",
|
||||
USDC: "USDC",
|
||||
} as const;
|
||||
export type SupportedCurrency = (typeof SupportedCurrency)[keyof typeof SupportedCurrency];
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Lead / Scout
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
export const LeadStatus = {
|
||||
DRAFT: "DRAFT",
|
||||
CONFIRMED: "CONFIRMED",
|
||||
PAYMENT_AUTHORIZED: "PAYMENT_AUTHORIZED",
|
||||
TASK_CREATED: "TASK_CREATED",
|
||||
EXPIRED: "EXPIRED",
|
||||
CANCELLED: "CANCELLED",
|
||||
} as const;
|
||||
export type LeadStatus = (typeof LeadStatus)[keyof typeof LeadStatus];
|
||||
|
||||
/** Scout 引流歸因模式。Phase 1 強制使用 LAST_CLICK。 */
|
||||
export const AttributionModel = {
|
||||
LAST_CLICK: "LAST_CLICK",
|
||||
FIRST_CLICK: "FIRST_CLICK",
|
||||
LINEAR: "LINEAR",
|
||||
} as const;
|
||||
export type AttributionModel = (typeof AttributionModel)[keyof typeof AttributionModel];
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Error 分類
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
export const TaskErrorClassification = {
|
||||
RETRYABLE: "retryable",
|
||||
NON_RETRYABLE: "non_retryable",
|
||||
} as const;
|
||||
export type TaskErrorClassification = (typeof TaskErrorClassification)[keyof typeof TaskErrorClassification];
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// MCP Tool 名稱(唯一常數來源)
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
export const MCPToolName = {
|
||||
LIST_OPEN_TASKS: "list_open_tasks",
|
||||
CLAIM_TASK: "claim_task",
|
||||
SUBMIT_SOLUTION: "submit_solution",
|
||||
CHECK_PAYOUT_STATUS: "check_payout_status",
|
||||
} as const;
|
||||
export type MCPToolName = (typeof MCPToolName)[keyof typeof MCPToolName];
|
||||
101
packages/contracts/src/errors/index.ts
Normal file
101
packages/contracts/src/errors/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @agent-bounty/contracts — Error Codes
|
||||
*
|
||||
* 所有 API / MCP 層的標準錯誤碼。
|
||||
* 格式:VW_<DOMAIN>_<CODE>
|
||||
*/
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Task 操作錯誤
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
export const TaskErrorCode = {
|
||||
// claim_task
|
||||
TASK_NOT_FOUND: "VW_TASK_NOT_FOUND",
|
||||
TASK_NOT_OPEN: "VW_TASK_NOT_OPEN", // 狀態不是 OPEN,無法 claim
|
||||
TASK_ALREADY_CLAIMED: "VW_TASK_ALREADY_CLAIMED", // 409 Race Condition(SELECT FOR UPDATE 失敗)
|
||||
TASK_CLAIM_LIMIT_REACHED: "VW_TASK_CLAIM_LIMIT_REACHED", // Agent 已達同時接案上限
|
||||
TASK_AGENT_SUSPENDED: "VW_TASK_AGENT_SUSPENDED", // Agent 被降權或封鎖
|
||||
|
||||
// submit_solution
|
||||
TASK_NOT_EXECUTING: "VW_TASK_NOT_EXECUTING", // 無法提交,任務不在 EXECUTING
|
||||
TASK_SUBMIT_UNAUTHORIZED: "VW_TASK_SUBMIT_UNAUTHORIZED", // 403,非任務持有者提交
|
||||
TASK_PAYLOAD_TOO_LARGE: "VW_TASK_PAYLOAD_TOO_LARGE", // 413,超過 payload 上限
|
||||
TASK_FILE_COUNT_EXCEEDED: "VW_TASK_FILE_COUNT_EXCEEDED", // 超過最大檔案數(20)
|
||||
|
||||
// 通用
|
||||
TASK_INVALID_INPUT: "VW_TASK_INVALID_INPUT",
|
||||
TASK_FORBIDDEN: "VW_TASK_FORBIDDEN",
|
||||
} as const;
|
||||
export type TaskErrorCode = (typeof TaskErrorCode)[keyof typeof TaskErrorCode];
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Judge 執行錯誤
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
export const JudgeErrorCode = {
|
||||
JUDGE_TIMEOUT: "VW_JUDGE_TIMEOUT",
|
||||
JUDGE_SANDBOX_UNAVAILABLE: "VW_JUDGE_SANDBOX_UNAVAILABLE",
|
||||
JUDGE_IMAGE_NOT_FOUND: "VW_JUDGE_IMAGE_NOT_FOUND",
|
||||
JUDGE_INVALID_RESULT: "VW_JUDGE_INVALID_RESULT",
|
||||
JUDGE_ALREADY_RUNNING: "VW_JUDGE_ALREADY_RUNNING", // 冪等保護:同一任務重複觸發
|
||||
} as const;
|
||||
export type JudgeErrorCode = (typeof JudgeErrorCode)[keyof typeof JudgeErrorCode];
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 金流錯誤
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
export const SettlementErrorCode = {
|
||||
PAYMENT_INTENT_NOT_FOUND: "VW_PAYMENT_INTENT_NOT_FOUND",
|
||||
AUTH_HOLD_FAILED: "VW_AUTH_HOLD_FAILED",
|
||||
CAPTURE_FAILED: "VW_CAPTURE_FAILED",
|
||||
RELEASE_FAILED: "VW_RELEASE_FAILED",
|
||||
REFUND_FAILED: "VW_REFUND_FAILED",
|
||||
IDEMPOTENCY_CONFLICT: "VW_IDEMPOTENCY_CONFLICT", // 同 key 但不同金額
|
||||
LEDGER_DRIFT_DETECTED: "VW_LEDGER_DRIFT_DETECTED", // 對帳漂移,啟動 incident
|
||||
DUPLICATE_WEBHOOK: "VW_DUPLICATE_WEBHOOK", // webhook 重複接收
|
||||
} as const;
|
||||
export type SettlementErrorCode = (typeof SettlementErrorCode)[keyof typeof SettlementErrorCode];
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Lead / Scout 錯誤
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
export const LeadErrorCode = {
|
||||
LEAD_NOT_FOUND: "VW_LEAD_NOT_FOUND",
|
||||
LEAD_EXPIRED: "VW_LEAD_EXPIRED",
|
||||
LEAD_INVALID_TEMPLATE: "VW_LEAD_INVALID_TEMPLATE", // Scout 使用未授權模板
|
||||
SCOUT_RATE_LIMIT: "VW_SCOUT_RATE_LIMIT",
|
||||
SCOUT_SUSPENDED: "VW_SCOUT_SUSPENDED",
|
||||
ATTRIBUTION_TOKEN_INVALID: "VW_ATTRIBUTION_TOKEN_INVALID",
|
||||
} as const;
|
||||
export type LeadErrorCode = (typeof LeadErrorCode)[keyof typeof LeadErrorCode];
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 通用 API 錯誤
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
export const CommonErrorCode = {
|
||||
UNAUTHORIZED: "VW_UNAUTHORIZED",
|
||||
FORBIDDEN: "VW_FORBIDDEN",
|
||||
VALIDATION_ERROR: "VW_VALIDATION_ERROR",
|
||||
RATE_LIMITED: "VW_RATE_LIMITED",
|
||||
INTERNAL_ERROR: "VW_INTERNAL_ERROR",
|
||||
SERVICE_UNAVAILABLE: "VW_SERVICE_UNAVAILABLE",
|
||||
} as const;
|
||||
export type CommonErrorCode = (typeof CommonErrorCode)[keyof typeof CommonErrorCode];
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 統一 error response 結構(MCP + REST 共用)
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
export interface VWError {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: Record<string, unknown>;
|
||||
/** 若為 true,呼叫方可在稍後重試 */
|
||||
retryable?: boolean;
|
||||
/** 建議的重試等待秒數 */
|
||||
retryAfterSeconds?: number;
|
||||
}
|
||||
22
packages/contracts/src/index.ts
Normal file
22
packages/contracts/src/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @agent-bounty/contracts
|
||||
*
|
||||
* VibeWork 共用資料契約套件
|
||||
* 版本:0.1.0
|
||||
*
|
||||
* 這是整個 VibeWork 系統的「單一資料契約真實來源」。
|
||||
* MCP server、後端 API、前端表單都必須從這裡 import,
|
||||
* 禁止在各自的程式庫中重新定義任何 VibeWork 資料結構。
|
||||
*/
|
||||
|
||||
// Enums — 狀態機、分類常數
|
||||
export * from "./enums/index.js";
|
||||
|
||||
// Schemas — Zod 驗證 schema
|
||||
export * from "./schemas/index.js";
|
||||
|
||||
// Types — TypeScript 型別(從 Zod 推導)
|
||||
export * from "./types/index.js";
|
||||
|
||||
// Errors — 標準錯誤碼與 VWError 介面
|
||||
export * from "./errors/index.js";
|
||||
377
packages/contracts/src/schemas/index.ts
Normal file
377
packages/contracts/src/schemas/index.ts
Normal file
@@ -0,0 +1,377 @@
|
||||
/**
|
||||
* @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_score:AI 評估任務清晰度,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,
|
||||
]),
|
||||
/** 由平台提供的最小測試檔內容(string);Agent 不得修改此欄位 */
|
||||
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 核心 Schema(Open 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 Schema(E2B 沙盒回傳)
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
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 ID(payment_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 Schema(Scout 導流任務草案)
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
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(),
|
||||
});
|
||||
|
||||
/** 精簡版 Task(list 用,避免暴露內部欄位) */
|
||||
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(),
|
||||
});
|
||||
146
packages/contracts/src/types/index.ts
Normal file
146
packages/contracts/src/types/index.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* @agent-bounty/contracts — TypeScript Types
|
||||
*
|
||||
* 從 Zod schema 推導出的 TypeScript 型別。
|
||||
* 永遠不要手寫這些型別——只從 z.infer 推導,確保型別與 schema 永遠同步。
|
||||
*/
|
||||
|
||||
import type { z } from "zod";
|
||||
import type {
|
||||
TaskBountySchema,
|
||||
TaskSummarySchema,
|
||||
AcceptanceCriteriaSchema,
|
||||
ClaimTaskRequestSchema,
|
||||
ClaimTaskResponseSchema,
|
||||
SubmitSolutionRequestSchema,
|
||||
SubmitSolutionResponseSchema,
|
||||
JudgeResultSchema,
|
||||
JudgeTestResultSchema,
|
||||
SettlementLedgerEntrySchema,
|
||||
LeadSchema,
|
||||
ListOpenTasksRequestSchema,
|
||||
ListOpenTasksResponseSchema,
|
||||
} from "../schemas/index.js";
|
||||
import type { VWError } from "../errors/index.js";
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Task
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
export type TaskBounty = z.infer<typeof TaskBountySchema>;
|
||||
export type TaskSummary = z.infer<typeof TaskSummarySchema>;
|
||||
export type AcceptanceCriteria = z.infer<typeof AcceptanceCriteriaSchema>;
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// MCP Tool I/O
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
export type ClaimTaskRequest = z.infer<typeof ClaimTaskRequestSchema>;
|
||||
export type ClaimTaskResponse = z.infer<typeof ClaimTaskResponseSchema>;
|
||||
export type SubmitSolutionRequest = z.infer<typeof SubmitSolutionRequestSchema>;
|
||||
export type SubmitSolutionResponse = z.infer<typeof SubmitSolutionResponseSchema>;
|
||||
export type ListOpenTasksRequest = z.infer<typeof ListOpenTasksRequestSchema>;
|
||||
export type ListOpenTasksResponse = z.infer<typeof ListOpenTasksResponseSchema>;
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Judge
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
export type JudgeResult = z.infer<typeof JudgeResultSchema>;
|
||||
export type JudgeTestResult = z.infer<typeof JudgeTestResultSchema>;
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Settlement
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
export type SettlementLedgerEntry = z.infer<typeof SettlementLedgerEntrySchema>;
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Lead / Scout
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
export type Lead = z.infer<typeof LeadSchema>;
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 通用 API Response Envelope
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/** 統一 API 成功回應格式 */
|
||||
export interface ApiSuccess<T> {
|
||||
ok: true;
|
||||
data: T;
|
||||
/** 請求追蹤 ID,供 SRE 查 log */
|
||||
request_id: string;
|
||||
}
|
||||
|
||||
/** 統一 API 錯誤回應格式 */
|
||||
export interface ApiFailure {
|
||||
ok: false;
|
||||
error: VWError;
|
||||
request_id: string;
|
||||
}
|
||||
|
||||
export type ApiResponse<T> = ApiSuccess<T> | ApiFailure;
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// State Machine 合法轉換圖(型別層面防護)
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
import type { TaskStatus } from "../enums/index.js";
|
||||
|
||||
/**
|
||||
* 合法的狀態轉換對。
|
||||
* 後端 API 層應驗證每次轉換是否在此集合內。
|
||||
*/
|
||||
export type TaskStateTransition =
|
||||
| { from: "OPEN"; to: "EXECUTING" }
|
||||
| { from: "OPEN"; to: "CANCELLED" }
|
||||
| { from: "EXECUTING"; to: "VERIFYING" }
|
||||
| { from: "EXECUTING"; to: "OPEN" } // Redis TTL timeout rollback
|
||||
| { from: "EXECUTING"; to: "CANCELLED" }
|
||||
| { from: "EXECUTING"; to: "FAILED" } // agent_abandon
|
||||
| { from: "VERIFYING"; to: "COMPLETED" }
|
||||
| { from: "VERIFYING"; to: "FAILED_RETRYABLE" }
|
||||
| { from: "VERIFYING"; to: "FAILED" }
|
||||
| { from: "FAILED_RETRYABLE"; to: "OPEN" }
|
||||
| { from: "FAILED_RETRYABLE"; to: "FAILED" }
|
||||
| { from: "FAILED"; to: "OPEN" }
|
||||
| { from: "FAILED"; to: "ARCHIVED" }
|
||||
| { from: "FAILED"; to: "DISPUTED" }
|
||||
| { from: "COMPLETED"; to: "DISPUTED" }
|
||||
| { from: "COMPLETED"; to: "PAYOUT_READY" }
|
||||
| { from: "PAYOUT_READY"; to: "PAYOUT_SETTLED" }
|
||||
| { from: "PAYOUT_SETTLED"; to: "ARCHIVED" }
|
||||
| { from: "DISPUTED"; to: "OPEN" }
|
||||
| { from: "DISPUTED"; to: "COMPLETED" }
|
||||
| { from: "DISPUTED"; to: "REFUND_PENDING" }
|
||||
| { from: "DISPUTED"; to: "ARCHIVED" }
|
||||
| { from: "REFUND_PENDING"; to: "ARCHIVED" }
|
||||
| { from: "CANCELLED"; to: "ARCHIVED" };
|
||||
|
||||
/** 取得從某狀態出發的合法下一狀態集合(runtime 用) */
|
||||
export function getAllowedNextStatuses(current: TaskStatus): TaskStatus[] {
|
||||
const map: Partial<Record<TaskStatus, TaskStatus[]>> = {
|
||||
OPEN: ["EXECUTING", "CANCELLED"],
|
||||
EXECUTING: ["VERIFYING", "OPEN", "CANCELLED", "FAILED"],
|
||||
VERIFYING: ["COMPLETED", "FAILED_RETRYABLE", "FAILED"],
|
||||
FAILED_RETRYABLE: ["OPEN", "FAILED"],
|
||||
FAILED: ["OPEN", "ARCHIVED", "DISPUTED"],
|
||||
COMPLETED: ["DISPUTED", "PAYOUT_READY"],
|
||||
PAYOUT_READY: ["PAYOUT_SETTLED"],
|
||||
PAYOUT_SETTLED: ["ARCHIVED"],
|
||||
DISPUTED: ["OPEN", "COMPLETED", "REFUND_PENDING", "ARCHIVED"],
|
||||
REFUND_PENDING: ["ARCHIVED"],
|
||||
CANCELLED: ["ARCHIVED"],
|
||||
ARCHIVED: [],
|
||||
};
|
||||
return map[current] ?? [];
|
||||
}
|
||||
|
||||
/** 驗證某狀態轉換是否合法 */
|
||||
export function isValidStateTransition(
|
||||
from: TaskStatus,
|
||||
to: TaskStatus
|
||||
): boolean {
|
||||
return getAllowedNextStatuses(from).includes(to);
|
||||
}
|
||||
Reference in New Issue
Block a user