本文定义 task3/ 三层接口架构及其调用关系 参考:TRUTH.md ADR-012 数据模型:01-data-model.md
┌─────────────────────────────────────────────────────────────────┐
│ Workflow Adapters │
│ (spec-kit-mcp-adapter, observer-adapter, ...) │
│ │
│ 职责: │
│ 1. 实现 workflow 特定的 dataOperator(上传/下载任务数据) │
│ 2. 调用 task3Operator 完整流程 │
│ 3. 暴露 MCP 工具给用户 │
└─────────────────────────────────────────────────────────────────┘
↓ 调用
┌─────────────────────────────────────────────────────────────────┐
│ task3Operator │
│ (task3/orchestration/) │
│ │
│ 职责:完整流程编排(业务逻辑) │
│ - publishFlow() — 幂等性检查 + 创建任务 + 创建 bounty │
│ - acceptFlow() — 状态验证 + 下载任务 + 接受 bounty │
│ - submitFlow() — 状态验证 + 上传提交 + 更新链上 │
│ - confirmFlow() — 验证提交 + 确认 bounty + 进入冷静期 │
│ - claimFlow() — 冷静期验证 + 领取赏金 │
└─────────────────────────────────────────────────────────────────┘
↓ 调用 ↓ 调用
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ bountyOperator │ │ dataOperator │
│ (task3/bounty-operator/) │ │ (由 adapter 实现) │
│ │ │ │
│ 职责:链上操作(技术实现) │ │ 职责:任务数据操作 │
│ - createBounty() │ │ - uploadTaskData() │
│ - acceptBounty() │ │ - downloadTaskData() │
│ - submitBounty() │ │ - uploadSubmission() │
│ - confirmBounty() │ │ - getTaskMetadata() │
│ - claimPayout() │ │ - updateTaskMetadata() │
│ - getBounty() │ │ │
│ - getBountyByTaskHash() │ │ │
│ - ... │ │ │
└──────────────────────────────┘ └──────────────────────────────┘
| 层次 | 职责 | 实现位置 | 可扩展性 |
|---|---|---|---|
| Workflow Adapters | 实现 workflow 特定的 DataOperator | task3/adapters/ |
新增 workflow = 新增 adapter |
| task3Operator | 流程编排 + 状态验证 + 业务逻辑 | task3/orchestration/ |
固定(通用逻辑) |
| bountyOperator | 链上操作(创建/查询/更新) | task3/bounty-operator/ |
新增链 = 新增实现 |
| dataOperator | 任务数据操作(上传/下载) | adapter 中实现 | 新增数据层 = 新实现 |
- 抽象层:定义链上操作的统一接口
- 实现层:每条链有独立实现
task3/bounty-operator/aptos/— Aptos 实现task3/bounty-operator/ethereum/— Ethereum 实现task3/bounty-operator/sui/— Sui 实现(未来)
文件位置: task3/bounty-operator/interface.ts
export interface BountyOperator {
// ========== 写入操作(6 个)==========
/**
* 创建 bounty
*/
createBounty(params: CreateBountyParams): Promise<CreateBountyResult>;
/**
* 接受 bounty(requester 将任务分配给 worker)
*/
acceptBounty(params: AcceptBountyParams): Promise<AcceptBountyResult>;
/**
* 提交工作成果(worker 提交,更新链上状态为 Submitted)
*/
submitBounty(params: SubmitBountyParams): Promise<SubmitBountyResult>;
/**
* 确认工作成果(requester 确认,更新链上状态为 Confirmed,进入冷静期)
*/
confirmBounty(params: ConfirmBountyParams): Promise<ConfirmBountyResult>;
/**
* 领取赏金(worker 领取,冷静期后)
*/
claimPayout(params: ClaimPayoutParams): Promise<ClaimPayoutResult>;
/**
* 取消 bounty(仅 user/requester 可调用,仅 Open 状态)
*/
cancelBounty(params: CancelBountyParams): Promise<CancelBountyResult>;
// ========== 查询操作(5 个)==========
/**
* 通过 bounty_id 获取完整信息
*/
getBounty(params: GetBountyParams): Promise<Bounty>;
/**
* 通过 task_hash 获取 bounty_id(幂等性检查)
*/
getBountyByTaskHash(params: GetBountyByTaskHashParams): Promise<GetBountyByTaskHashResult>;
/**
* 列出所有 bounty IDs
*/
listBounties(params?: ListBountiesParams): Promise<ListBountiesResult>;
/**
* 按 user (requester) 查询 bounties
*/
getBountiesByUser(params: GetBountiesByUserParams): Promise<GetBountiesByUserResult>;
/**
* 按 worker 查询 bounties
*/
getBountiesByWorker(params: GetBountiesByWorkerParams): Promise<GetBountiesByWorkerResult>;
}类型定义: 见 01-data-model.md Section 2 和 Section 5
文件位置: task3/bounty-operator/aptos/src/bounty-operator.ts
import { BountyOperator, CreateBountyParams, CreateBountyResult, Bounty } from '@code3/task3/bounty-operator';
import { AptosClient } from '@aptos-labs/ts-sdk';
export class AptosBountyOperator implements BountyOperator {
private client: AptosClient;
private contractAddress: string;
constructor(config: {
privateKey: string;
network: 'testnet' | 'mainnet';
contractAddress: string;
}) {
this.client = new AptosClient({
privateKey: config.privateKey,
network: config.network
});
this.contractAddress = config.contractAddress;
}
async createBounty(params: CreateBountyParams): Promise<CreateBountyResult> {
// 调用 Aptos 合约 create_bounty
const result = await this.client.submitTransaction({
function: `${this.contractAddress}::bounty::create_bounty`,
type_arguments: [],
arguments: [params.taskId, params.taskHash, params.amount, params.asset]
});
return {
bountyId: result.bountyId,
txHash: result.txHash
};
}
async getBounty(params: GetBountyParams): Promise<Bounty> {
// 调用 Aptos 合约 view function get_bounty
const bounty = await this.client.view({
function: `${this.contractAddress}::bounty::get_bounty`,
type_arguments: [],
arguments: [params.bountyId]
});
return bounty as Bounty;
}
async getBountyByTaskHash(params: GetBountyByTaskHashParams): Promise<GetBountyByTaskHashResult> {
const bountyId = await this.client.view({
function: `${this.contractAddress}::bounty::get_bounty_by_task_hash`,
type_arguments: [],
arguments: [params.taskHash]
});
return {
found: bountyId !== '0',
bountyId: bountyId !== '0' ? bountyId : undefined
};
}
// ... 其他方法实现
}- 抽象层:定义任务数据操作的统一接口(与具体数据层无关)
- 实现层:由各个 workflow-adapter 实现
spec-kit-mcp-adapter— SpecKitDataOperator(GitHub Issue/PR)observer-adapter— ObserverDataOperator(IPFS)- 未来扩展:IPFSDataOperator, ArweaveDataOperator, S3DataOperator 等
文件位置: task3/data-operator/interface.ts
export interface DataOperator {
/**
* 上传任务数据到数据层
* @returns taskUrl - 任务数据的 URL(可以是 GitHub Issue URL, IPFS CID, Arweave TX ID 等)
*/
uploadTaskData(params: UploadTaskDataParams): Promise<UploadTaskDataResult>;
/**
* 从数据层下载任务数据
*/
downloadTaskData(params: DownloadTaskDataParams): Promise<DownloadTaskDataResult>;
/**
* 上传提交内容到数据层
* @returns submissionUrl - 提交内容的 URL(GitHub PR URL, IPFS CID 等)
*/
uploadSubmission(params: UploadSubmissionParams): Promise<UploadSubmissionResult>;
/**
* 获取任务元数据
*/
getTaskMetadata(params: GetTaskMetadataParams): Promise<TaskMetadata>;
/**
* 更新任务元数据
*/
updateTaskMetadata(params: UpdateTaskMetadataParams): Promise<UpdateTaskMetadataResult>;
}类型定义: 见 01-data-model.md Section 6
文件位置: task3/adapters/spec-kit-mcp-adapter/src/data-operator.ts
import { DataOperator, UploadTaskDataParams, UploadTaskDataResult } from '@code3/task3/data-operator';
import { GitHubDataLayer } from '@code3/task3/data-layers/github';
import fs from 'fs/promises';
import path from 'path';
export class SpecKitDataOperator implements DataOperator {
private github: GitHubDataLayer;
private localSpecsDir: string;
constructor(config: {
githubToken: string;
localSpecsDir: string; // 默认 "specs/"
}) {
this.github = new GitHubDataLayer({ token: config.githubToken });
this.localSpecsDir = config.localSpecsDir;
}
async uploadTaskData(params: UploadTaskDataParams): Promise<UploadTaskDataResult> {
const { taskData, metadata } = params;
// 1. spec-kit 特定:读取本地 spec.md 文件
const specContent = taskData.content; // Markdown string
// 2. 通过 GitHubDataLayer 上传到 GitHub Issue
const result = await this.github.createIssue({
repo: metadata.repo,
title: `[Code3 Bounty] ${this.extractTitle(specContent)}`,
body: this.serializeContent(specContent, metadata),
labels: ['code3-bounty', 'spec-kit', metadata.chain.name]
});
return {
taskUrl: result.issueUrl,
taskId: result.taskId,
metadata
};
}
async downloadTaskData(params: DownloadTaskDataParams): Promise<DownloadTaskDataResult> {
const { taskUrl } = params;
// 1. 通过 GitHubDataLayer 下载任务数据
const result = await this.github.getIssue({ issueUrl: taskUrl });
// 2. spec-kit 特定:写入本地 specs/{bountyId}/spec.md
const bountyId = result.metadata.chain.bountyId;
const localPath = path.join(this.localSpecsDir, bountyId, 'spec.md');
await fs.mkdir(path.dirname(localPath), { recursive: true });
await fs.writeFile(localPath, result.content, 'utf-8');
return {
taskData: { content: result.content },
localPath,
metadata: result.metadata
};
}
async uploadSubmission(params: UploadSubmissionParams): Promise<UploadSubmissionResult> {
const { taskUrl, submissionData } = params;
// 通过 GitHubDataLayer 创建 PR
const result = await this.github.createPR({
taskUrl,
branchName: submissionData.branchName,
title: `[Code3 Submission] ${submissionData.summary}`,
body: this.generatePRBody(submissionData)
});
return {
submissionUrl: result.prUrl,
submissionId: result.prNumber
};
}
async getTaskMetadata(params: GetTaskMetadataParams): Promise<TaskMetadata> {
const result = await this.github.getIssueMetadata({ issueUrl: params.taskUrl });
return result.metadata;
}
async updateTaskMetadata(params: UpdateTaskMetadataParams): Promise<UpdateTaskMetadataResult> {
await this.github.updateIssueMetadata({
issueUrl: params.taskUrl,
metadata: params.metadata
});
return { success: true };
}
// Helper methods
private extractTitle(specContent: string): string {
const match = specContent.match(/^#\s+(.+)$/m);
return match ? match[1] : 'Untitled';
}
private serializeContent(content: string, metadata: TaskMetadata): string {
// 将 metadata 序列化为 YAML frontmatter
return `---\n${this.toYAML(metadata)}\n---\n\n${content}`;
}
private generatePRBody(submissionData: any): string {
return `## Summary\n${submissionData.summary}\n\n---\nGenerated by Code3`;
}
private toYAML(obj: any): string {
// 简化版 YAML 序列化
return JSON.stringify(obj, null, 2);
}
}- 职责:完整流程编排,协调 bountyOperator 和 dataOperator
- 实现位置:
task3/orchestration/ - 特点:与 workflow 和数据层无关,只关心业务逻辑
- 设计模式:抽象类(直接实现通用逻辑)
为什么是抽象类而不是接口?
- 5 个流程的核心逻辑对所有 workflow 都是相同的
- 只有
dataOperator和bountyOperator是通过依赖注入传入的 - 无需为每个 workflow 重新实现流程逻辑
文件位置: task3/orchestration/task3-operator.ts
export abstract class Task3Operator {
/**
* 发布流程:幂等性检查 + 上传任务数据 + 创建 bounty
*/
async publishFlow(params: PublishFlowParams): Promise<PublishFlowResult> {
const { dataOperator, bountyOperator, taskData, metadata, amount, asset } = params;
// 1. 计算 task_hash(幂等性检查)
const taskHash = crypto.createHash('sha256').update(JSON.stringify(taskData)).digest('hex');
// 2. 检查是否已存在 bounty(幂等性)
const existingBounty = await bountyOperator.getBountyByTaskHash({ taskHash });
if (existingBounty.found) {
const taskMetadata = await dataOperator.getTaskMetadata({ taskUrl: metadata.dataLayer.url });
return { taskUrl: taskMetadata.dataLayer.url, bountyId: existingBounty.bountyId, txHash: null, isNew: false };
}
// 3. 上传任务数据到数据层
const uploadResult = await dataOperator.uploadTaskData({ taskData, metadata: { ...metadata, taskHash } });
// 4. 创建链上 bounty
const bountyResult = await bountyOperator.createBounty({ taskId: uploadResult.taskId, taskHash, amount, asset });
// 5. 更新任务元数据(回写 bounty_id)
await dataOperator.updateTaskMetadata({
taskUrl: uploadResult.taskUrl,
metadata: { chain: { ...metadata.chain, bountyId: bountyResult.bountyId } }
});
return { taskUrl: uploadResult.taskUrl, bountyId: bountyResult.bountyId, txHash: bountyResult.txHash, isNew: true };
}
/**
* 接受流程:状态验证 + 下载任务数据 + 接受 bounty
*/
async acceptFlow(params: AcceptFlowParams): Promise<AcceptFlowResult> {
const { dataOperator, bountyOperator, taskUrl } = params;
const metadata = await dataOperator.getTaskMetadata({ taskUrl });
const { bountyId } = metadata.chain;
const bounty = await bountyOperator.getBounty({ bountyId });
if (bounty.status !== BountyStatus.Open) {
throw new Error(`Bounty is not Open (current: ${bounty.status})`);
}
const downloadResult = await dataOperator.downloadTaskData({ taskUrl });
const acceptResult = await bountyOperator.acceptBounty({ bountyId });
return { taskData: downloadResult.taskData, localPath: downloadResult.localPath, bountyId, txHash: acceptResult.txHash };
}
/**
* 提交流程:状态验证 + 上传提交 + 更新链上
*/
async submitFlow(params: SubmitFlowParams): Promise<SubmitFlowResult> {
const { dataOperator, bountyOperator, taskUrl, submissionData } = params;
const metadata = await dataOperator.getTaskMetadata({ taskUrl });
const { bountyId } = metadata.chain;
const bounty = await bountyOperator.getBounty({ bountyId });
if (bounty.status !== BountyStatus.Accepted) {
throw new Error(`Bounty is not Accepted (current: ${bounty.status})`);
}
const uploadResult = await dataOperator.uploadSubmission({ taskUrl, submissionData });
const submitResult = await bountyOperator.submitBounty({
bountyId,
submissionHash: crypto.createHash('sha256').update(JSON.stringify(submissionData)).digest('hex')
});
return { submissionUrl: uploadResult.submissionUrl, txHash: submitResult.txHash };
}
/**
* 确认流程:验证提交 + 确认 bounty(进入冷静期)
*/
async confirmFlow(params: ConfirmFlowParams): Promise<ConfirmFlowResult> {
const { bountyOperator, dataOperator, taskUrl } = params;
const metadata = await dataOperator.getTaskMetadata({ taskUrl });
const { bountyId } = metadata.chain;
const bounty = await bountyOperator.getBounty({ bountyId });
if (bounty.status !== BountyStatus.Submitted) {
throw new Error(`Bounty is not Submitted (current: ${bounty.status})`);
}
const confirmResult = await bountyOperator.confirmBounty({ bountyId, confirmedAt: Math.floor(Date.now() / 1000) });
await dataOperator.updateTaskMetadata({
taskUrl,
metadata: { bounty: { ...metadata.bounty, confirmedAt: confirmResult.confirmedAt, coolingUntil: confirmResult.coolingUntil } }
});
return { txHash: confirmResult.txHash, coolingUntil: confirmResult.coolingUntil };
}
/**
* 领取流程:冷静期验证 + 领取赏金
*/
async claimFlow(params: ClaimFlowParams): Promise<ClaimFlowResult> {
const { bountyOperator, dataOperator, taskUrl } = params;
const metadata = await dataOperator.getTaskMetadata({ taskUrl });
const { bountyId } = metadata.chain;
const bounty = await bountyOperator.getBounty({ bountyId });
if (bounty.status !== BountyStatus.Confirmed) {
throw new Error(`Bounty is not Confirmed (current: ${bounty.status})`);
}
const now = Math.floor(Date.now() / 1000);
if (bounty.coolingUntil && now < bounty.coolingUntil) {
throw new Error(`Cooling period not ended (${bounty.coolingUntil - now}s remaining)`);
}
const claimResult = await bountyOperator.claimPayout({ bountyId });
return { txHash: claimResult.txHash, amount: bounty.amount, asset: bounty.asset };
}
}类型定义: 见 01-data-model.md Section 4
Requester Worker
──────── ──────
│ │
│ 1. publish (发布任务) │
├─→ publishFlow() │
│ ├─ getBountyByTaskHash() (幂等性) │
│ ├─ uploadTaskData() │
│ ├─ createBounty() │
│ └─ updateTaskMetadata() │
│ │
│ │ 2. accept (接单)
│ ├─→ acceptFlow()
│ │ ├─ getTaskMetadata()
│ │ ├─ getBounty() (验证 Open)
│ │ ├─ downloadTaskData()
│ │ └─ acceptBounty()
│ │
│ │ 3. submit (提交工作)
│ ├─→ submitFlow()
│ │ ├─ getTaskMetadata()
│ │ ├─ getBounty() (验证 Accepted)
│ │ ├─ uploadSubmission()
│ │ └─ submitBounty()
│ │
│ 4. confirm (确认工作) │
├─→ confirmFlow() │
│ ├─ getTaskMetadata() │
│ ├─ getBounty() (验证 Submitted) │
│ ├─ confirmBounty() (进入冷静期) │
│ └─ updateTaskMetadata() │
│ │
│ │ 5. claim (领取赏金)
│ ├─→ claimFlow()
│ │ ├─ getTaskMetadata()
│ │ ├─ getBounty() (验证 Confirmed + 冷静期)
│ │ └─ claimPayout()
│ │
spec-kit-mcp-adapter.publish()
↓
1. 读取本地 specs/00x/spec.md
2. 创建 SpecKitDataOperator
3. 创建 AptosBountyOperator
↓
task3Operator.publishFlow({
dataOperator,
bountyOperator,
taskData,
metadata,
amount,
asset
})
↓
├─→ bountyOperator.getBountyByTaskHash() — 幂等性检查
│ └─→ 如果已存在,返回现有 bounty
│
├─→ dataOperator.uploadTaskData() — 上传任务数据
│ └─→ SpecKitDataOperator → GitHubDataLayer.createIssue()
│ └─→ 创建 GitHub Issue
│
├─→ bountyOperator.createBounty() — 创建链上 bounty
│ └─→ AptosBountyOperator → Aptos 合约 create_bounty()
│
└─→ dataOperator.updateTaskMetadata() — 回写 bounty_id
└─→ GitHubDataLayer.updateIssueMetadata()
↓
返回 { taskUrl, bountyId, txHash, isNew }
spec-kit-mcp-adapter.accept()
↓
task3Operator.acceptFlow({
dataOperator,
bountyOperator,
taskUrl
})
↓
├─→ dataOperator.getTaskMetadata() — 获取任务元数据
│
├─→ bountyOperator.getBounty() — 验证 bounty 状态
│ └─→ 如果状态不是 Open,抛出错误
│
├─→ dataOperator.downloadTaskData() — 下载任务数据
│ └─→ SpecKitDataOperator:
│ ├─ GitHubDataLayer.getIssue()
│ └─ 写入 specs/{bountyId}/spec.md
│
└─→ bountyOperator.acceptBounty() — 接受链上 bounty
└─→ Aptos 合约 accept_bounty()
↓
返回 { taskData, localPath, bountyId, txHash }
-
bountyOperator:只关心链上操作,不知道任务数据格式和数据层
- 输入:bountyId, taskHash, amount, asset
- 输出:txHash, bounty status
- 不知道 GitHub / IPFS / Arweave
-
dataOperator:只关心任务数据操作,不知道链上状态
- 输入:taskData, taskUrl, metadata
- 输出:taskUrl, submissionUrl, metadata
- 不知道 Aptos / Ethereum / Sui
-
task3Operator:协调两者,实现完整业务逻辑
- 依赖注入:接收 dataOperator 和 bountyOperator
- 流程编排:协调数据层和链上操作
- 状态验证:确保 bounty 状态正确
- ✅ 使用通用术语:
taskHash,taskId,worker,submissionHash - ❌ 避免特定概念:
,issueHash,prUrlmergedAt - 状态机:
Open→Accepted→Submitted→Confirmed→Claimed
- ✅ 使用通用术语:
taskUrl,submissionUrl,taskData,metadata - ❌ 避免特定概念:
,issueUrl,prUrl,IssuePR - 核心方法:
uploadTaskData,downloadTaskData,uploadSubmission
// adapter 创建实例并注入
const dataOperator = new SpecKitDataOperator({
githubToken: process.env.GITHUB_TOKEN,
localSpecsDir: 'specs/'
});
const bountyOperator = new AptosBountyOperator({
privateKey: process.env.APTOS_PRIVATE_KEY,
network: 'testnet'
});
// 注入到 orchestration
const result = await publishFlow({
dataOperator,
bountyOperator,
taskData,
metadata,
amount,
asset
});优势:
- task3Operator 不依赖具体实现
- 易于测试(Mock dataOperator 和 bountyOperator)
- 易于扩展(新增链或数据层不影响 orchestration)
- 实现
BountyOperator接口:
export class SuiBountyOperator implements BountyOperator {
async createBounty(params: CreateBountyParams): Promise<CreateBountyResult> {
// 调用 Sui 合约
}
// ... 其他 10 个方法
}- 在 adapter 中使用:
const bountyOperator = new SuiBountyOperator({ ... });
await publishFlow({ bountyOperator, ... });- 实现 DataOperator:
export class IPFSDataOperator implements DataOperator {
async uploadTaskData(params): Promise<UploadTaskDataResult> {
const cid = await this.ipfs.add(content);
return { taskUrl: `ipfs://${cid}`, taskId: cid };
}
}- 在 adapter 中使用:
const dataOperator = new IPFSDataOperator({ ... });- 实现 DataOperator:
export class CodeReviewDataOperator implements DataOperator {
async uploadTaskData(params): Promise<UploadTaskDataResult> {
// 上传代码审查任务
}
}- 暴露 MCP 工具:
export async function publishReview(args) {
const dataOperator = new CodeReviewDataOperator({ ... });
const bountyOperator = new AptosBountyOperator({ ... });
return await publishFlow({
dataOperator,
bountyOperator,
taskData: args.reviewData,
metadata: { workflow: { name: 'code-review' }, ... },
amount: args.amount,
asset: args.asset
});
}- 高内聚低耦合:每个接口职责单一,互不依赖具体实现
- 易于测试:可以 Mock 任何层的接口进行单元测试
- 易于扩展:新增链/数据层/workflow 不影响现有代码
- 类型安全:TypeScript 接口提供编译时类型检查
- 幂等性保障:通过
taskHash和getBountyByTaskHash确保幂等 - 状态验证:每个流程都验证 bounty 状态,防止非法操作
- 数据模型: 01-data-model.md
- ADR-012: TRUTH.md ADR-012
- 包结构: 05-packages-structure.md
- 工作流: 08-workflow.md