diff --git a/packages/flow/src/meta/meta.mcp.ts b/packages/flow/src/meta/meta.mcp.ts index 4f49ff84a..28dac9e08 100644 --- a/packages/flow/src/meta/meta.mcp.ts +++ b/packages/flow/src/meta/meta.mcp.ts @@ -3,7 +3,7 @@ * Meta MCP Server * * Provides workflow execution capability to AI agents. - * - workflow(name, args): Execute any workflow + * - use(name, args): Execute any workflow by name * - reload(): Refresh workflow list and return updated description * - buildMetaMcp(): Package workflows into an MCP server * @@ -185,7 +185,7 @@ async function buildToolDescription( .join("\n"); // 动态生成 examples - const examples = workflows.slice(0, 3).map((w) => ` workflow("${w.name}", ["--help"])`).join("\n"); + const examples = workflows.slice(0, 3).map((w) => ` use("${w.name}", ["--help"])`).join("\n"); return `Execute a workflow by name with arguments. @@ -197,7 +197,7 @@ async function buildToolDescription( ${workflowList || "(none)"} ## Examples -${examples || ' workflow("", ["--help"])'}`; +${examples || ' use("", ["--help"])'}`; } // ============================================================================= @@ -341,7 +341,7 @@ async function createWorkflowTool( const description = await buildToolDescription(directories, filter); return defineTool({ - name: "workflow", + name: "use", description, inputSchema: z.object({ name: z diff --git a/scripts/agent-flow/mcps/git-workflow.mcp.ts b/scripts/agent-flow/mcps/git-workflow.mcp.ts index 396e4d544..2e9ca0f89 100755 --- a/scripts/agent-flow/mcps/git-workflow.mcp.ts +++ b/scripts/agent-flow/mcps/git-workflow.mcp.ts @@ -3,6 +3,27 @@ * Git Workflow MCP - GitHub & Git 操作封装 * * 提供原子化的 Git/GitHub 操作工具,供 workflow 调用。 + * + * ## 标签管理 + * + * 模块加载时通过 top-level await 从 GitHub 获取所有标签: + * - `getLabels()` - 获取标签列表(含 name, color, description) + * - `createLabel()` - 创建新标签(自动推断颜色) + * - `ensureLabels()` - 确保标签存在,可选自动创建 + * + * 颜色推断规则: + * - `area/*` → #c5def5 (蓝色) + * - `type/*` → #0e8a16 (绿色) + * - `priority/*` → #d93f0b (红色) + * - 其他 → #ededed (灰色) + * + * ## Issue/PR 创建 + * + * `createIssue()` 和 `createPr()` 支持: + * - `labels` - 要添加的标签数组 + * - `createLabels` - 是否自动创建缺失的标签 + * + * 如果标签不存在且 `createLabels=false`,会抛出错误提示。 */ import { execSync } from "node:child_process"; @@ -46,8 +67,107 @@ function safeExec(cmd: string, cwd?: string): string | null { // Pure Functions (Exports) // ============================================================================= -export async function createIssue(args: { title: string; body: string; labels?: string[]; assignee?: string }) { - const { title, body, labels, assignee = "@me" } = args; +/** Label info type */ +interface LabelInfo { + name: string; + color: string; + description: string; +} + +/** Cache for available labels with their colors */ +let labelsCache: LabelInfo[] | null = null; + +/** + * Load labels from GitHub (called once at startup via top-level await) + */ +async function loadLabelsFromGitHub(): Promise { + const output = safeExec(`gh label list --repo ${REPO} --limit 100 --json name,color,description`); + if (!output) return []; + try { + return JSON.parse(output) as LabelInfo[]; + } catch { + return []; + } +} + +// Top-level await: load labels at module initialization +labelsCache = await loadLabelsFromGitHub(); +console.log(`📋 Loaded ${labelsCache.length} labels from GitHub`); + +/** + * Get all available labels in the repository + */ +export async function getLabels(args?: { refresh?: boolean }): Promise<{ labels: LabelInfo[] }> { + if (args?.refresh || !labelsCache) { + labelsCache = await loadLabelsFromGitHub(); + } + return { labels: labelsCache }; +} + +/** + * Get color for a label (from cache or generate based on prefix) + */ +function getLabelColor(name: string): string { + // Check if label exists in cache + const existing = labelsCache?.find(l => l.name === name); + if (existing) return existing.color; + + // Generate color based on prefix + if (name.startsWith("area/")) return "c5def5"; + if (name.startsWith("type/")) return "0e8a16"; + if (name.startsWith("priority/")) return "d93f0b"; + return "ededed"; // Default gray +} + +/** + * Create a label if it doesn't exist + */ +export async function createLabel(args: { name: string; description?: string; color?: string }): Promise<{ created: boolean }> { + const { name, description = "", color = getLabelColor(name) } = args; + const { labels } = await getLabels(); + + if (labels.some(l => l.name === name)) { + return { created: false }; + } + + const descFlag = description ? `--description "${description}"` : ""; + exec(`gh label create "${name}" --repo ${REPO} --color "${color}" ${descFlag}`); + labelsCache = null; // Invalidate cache + return { created: true }; +} + +/** + * Ensure all specified labels exist, creating missing ones if requested + */ +export async function ensureLabels(args: { labels: string[]; create?: boolean }): Promise<{ missing: string[]; created: string[] }> { + const { labels: requestedLabels, create = false } = args; + const { labels: existingLabels } = await getLabels(); + const existingNames = existingLabels.map(l => l.name); + + const missing = requestedLabels.filter(l => !existingNames.includes(l)); + const created: string[] = []; + + if (create && missing.length > 0) { + for (const label of missing) { + await createLabel({ name: label }); + created.push(label); + } + } + + return { missing: create ? [] : missing, created }; +} + +export async function createIssue(args: { title: string; body: string; labels?: string[]; assignee?: string; createLabels?: boolean }) { + const { title, body, labels, assignee = "@me", createLabels = false } = args; + + // Ensure labels exist before creating issue + if (labels && labels.length > 0) { + const { missing } = await ensureLabels({ labels, create: createLabels }); + if (missing.length > 0) { + throw new Error(`Labels not found: ${missing.join(", ")}. Use --create-labels to auto-create them.`); + } + } + const labelFlag = labels ? labels.map(l => `--label "${l}"`).join(" ") : ""; const assigneeFlag = assignee ? `--assignee "${assignee}"` : ""; const safeBody = body.replace(/"/g, '\\"'); @@ -70,8 +190,17 @@ export async function updateIssue(args: { issueId: string; body?: string; state? return { success: true }; } -export async function createPr(args: { title: string; body: string; head: string; base?: string; draft?: boolean; labels?: string[] }) { - const { title, body, head, base = "main", draft = true, labels } = args; +export async function createPr(args: { title: string; body: string; head: string; base?: string; draft?: boolean; labels?: string[]; createLabels?: boolean }) { + const { title, body, head, base = "main", draft = true, labels, createLabels = false } = args; + + // Ensure labels exist before creating PR + if (labels && labels.length > 0) { + const { missing } = await ensureLabels({ labels, create: createLabels }); + if (missing.length > 0) { + throw new Error(`Labels not found: ${missing.join(", ")}. Use --create-labels to auto-create them.`); + } + } + const draftFlag = draft ? "--draft" : ""; const labelFlag = labels ? labels.map(l => `--label "${l}"`).join(" ") : ""; diff --git a/scripts/agent-flow/workflows/task.workflow.ts b/scripts/agent-flow/workflows/task.workflow.ts index bbdf63d4f..159eedbc4 100755 --- a/scripts/agent-flow/workflows/task.workflow.ts +++ b/scripts/agent-flow/workflows/task.workflow.ts @@ -4,10 +4,38 @@ * * 核心理念:AI 的计划即 Issue,AI 的执行即 PR。 * - * 主要功能: - * 1. start: 一键启动 (Issue -> Branch -> Worktree -> Draft PR) - * 2. sync: 同步进度 (Local Todo -> Issue Body) - * 3. submit: 提交任务 (Push -> Ready PR) + * ## 工作流程 + * + * ``` + * task start task submit + * │ │ + * ▼ ▼ + * Issue + Branch + Worktree Push + Ready PR + * + Draft PR [skip ci] (触发 CI) + * ``` + * + * ## 主要功能 + * + * 1. **start**: 一键启动开发环境 + * - 创建 GitHub Issue (根据 type 选择模板) + * - 创建 Git Branch + Worktree + * - 创建 Draft PR (带 [skip ci],不触发 CI) + * - 支持 --list-labels 列出可用标签 + * - 支持 --create-labels 自动创建缺失标签 + * + * 2. **sync**: 同步进度到 Issue + * - 将本地 Todo/进度更新到 Issue Description + * + * 3. **submit**: 提交任务触发 CI + * - 推送代码 (不带 [skip ci],触发 CI) + * - 标记 PR 为 Ready for Review + * + * ## 标签管理 + * + * 标签在模块加载时从 GitHub 动态获取,支持: + * - 按前缀分组显示 (type/, area/, etc.) + * - 自动推断新标签颜色 + * - 创建前验证标签是否存在 */ import { existsSync } from "jsr:@std/fs"; @@ -22,6 +50,7 @@ import { createWorktree, pushWorktree, updateIssue, + getLabels, } from "../mcps/git-workflow.mcp.ts"; import { getRelatedChapters } from "../mcps/whitebook.mcp.ts"; @@ -96,17 +125,46 @@ ${desc} */ const startWorkflow = defineWorkflow({ name: "start", - description: "启动新任务 (Issue -> Branch -> Worktree -> Draft PR)", + description: "启动新任务 (Issue + Worktree + Draft PR,不触发 CI)", args: { - title: { type: "string", description: "任务标题", required: true }, + title: { type: "string", description: "任务标题", required: false }, type: { type: "string", description: "任务类型 (ui|service|page|hybrid)", default: "hybrid", }, description: { type: "string", description: "任务描述", required: false }, + "create-labels": { + type: "boolean", + description: "自动创建不存在的标签", + default: false, + }, + "list-labels": { + type: "boolean", + description: "列出所有可用标签", + default: false, + }, }, handler: async (args) => { + // Handle --list-labels flag + if (args["list-labels"]) { + const { labels } = await getLabels({ refresh: true }); + console.log("📋 可用标签列表:\n"); + const grouped = new Map(); + for (const label of labels) { + const prefix = label.name.includes("/") ? label.name.split("/")[0] : "other"; + if (!grouped.has(prefix)) grouped.set(prefix, []); + grouped.get(prefix)!.push(label); + } + for (const [prefix, items] of grouped) { + console.log(` [${prefix}]`); + for (const item of items) { + console.log(` - ${item.name} (#${item.color})${item.description ? ` - ${item.description}` : ""}`); + } + } + return; + } + const title = args.title || args._.join(" "); if (!title) { console.error("❌ 错误: 请提供任务标题"); @@ -114,6 +172,7 @@ const startWorkflow = defineWorkflow({ } const type = (args.type || "hybrid") as keyof typeof TEMPLATES; const rawDesc = args.description || "Start development..."; + const createLabels = args["create-labels"] as boolean; // 1. 组装 Description const template = TEMPLATES[type] || TEMPLATES.hybrid; @@ -125,6 +184,7 @@ const startWorkflow = defineWorkflow({ if (type === "service") labels.push("area/core"); console.log(`🚀 启动任务: ${title} [${type}]\n`); + console.log(`🏷️ 标签: ${labels.join(", ")}${createLabels ? " (自动创建)" : ""}\n`); // 3. 上下文注入 console.log("📚 推荐阅读白皮书章节:"); @@ -138,6 +198,7 @@ const startWorkflow = defineWorkflow({ title, body: description, labels, + createLabels, }); console.log(` ✅ Issue #${issueId} Created: ${issueUrl}`); @@ -152,11 +213,11 @@ const startWorkflow = defineWorkflow({ console.log(` ✅ Worktree Created: ${path}`); console.log(` ✅ Branch Created: ${branch}`); - // 6. 初始化提交 & 推送 + // 6. 初始化提交 & 推送 (skip CI for draft) console.log("\n3️⃣ 初始化 Git 环境..."); await pushWorktree({ path, - message: `chore: start issue #${issueId}`, + message: `chore: start issue #${issueId} [skip ci]`, }); // 7. 创建 Draft PR @@ -168,6 +229,7 @@ const startWorkflow = defineWorkflow({ base: "main", draft: true, labels, + createLabels, }); console.log(` ✅ Draft PR Created: ${prUrl}`); @@ -216,11 +278,11 @@ const syncWorkflow = defineWorkflow({ /** * 提交任务 - * Push 代码 -> 标记 PR 为 Ready + * Push 代码 (触发 CI) -> 标记 PR 为 Ready */ const submitWorkflow = defineWorkflow({ name: "submit", - description: "提交任务 (Push -> Ready PR)", + description: "提交任务并触发 CI (Push + Ready PR)", handler: async () => { const wt = getCurrentWorktreeInfo(); if (!wt || !wt.path) { @@ -280,6 +342,8 @@ export const workflow = createRouter({ examples: [ ['task start --type ui --title "Button Component"', "启动 UI 任务"], ['task start --type service --title "Auth Service"', "启动服务任务"], + ['task start --type service --title "New Feature" --create-labels', "启动任务并自动创建缺失标签"], + ['task start --list-labels', "列出所有可用标签"], ['task sync "- [x] Step 1"', "同步进度"], ["task submit", "提交任务"], ], diff --git a/src/services/chain-adapter/bioforest/transaction-service.ts b/src/services/chain-adapter/bioforest/transaction-service.ts index 92adbe9c0..fe44e7ea7 100644 --- a/src/services/chain-adapter/bioforest/transaction-service.ts +++ b/src/services/chain-adapter/bioforest/transaction-service.ts @@ -29,11 +29,41 @@ export class BioforestTransactionService implements ITransactionService { private readonly chainId: string private config: ChainConfig | null = null private baseUrl: string = '' + private beginEpochTime: number | null = null constructor(chainId: string) { this.chainId = chainId } + /** + * Get the genesis block's beginEpochTime (in milliseconds). + * BioForest chain timestamps are relative to this epoch time. + */ + private async getBeginEpochTime(): Promise { + if (this.beginEpochTime !== null) { + return this.beginEpochTime + } + try { + const core = await getBioforestCore(this.chainId) + // Access config through the core's internal structure + // The SDK stores beginEpochTime in milliseconds + const config = (core as unknown as { config?: { beginEpochTime?: number } }).config + this.beginEpochTime = config?.beginEpochTime ?? 0 + } catch { + this.beginEpochTime = 0 + } + return this.beginEpochTime + } + + /** + * Convert BioForest relative timestamp to absolute milliseconds. + * BioForest timestamps are seconds since beginEpochTime. + */ + private async toAbsoluteTimestamp(relativeTimestamp: number): Promise { + const epochTime = await this.getBeginEpochTime() + return relativeTimestamp * 1000 + epochTime + } + private getConfig(): ChainConfig { if (!this.config) { const config = chainConfigService.getConfig(this.chainId) @@ -321,7 +351,7 @@ export class BioforestTransactionService implements ITransactionService { confirmations: 1, requiredConfirmations: 1, }, - timestamp: tx.timestamp * 1000, // Convert to milliseconds + timestamp: await this.toAbsoluteTimestamp(tx.timestamp), blockNumber: BigInt(item.height), type: 'transfer', } @@ -412,6 +442,9 @@ export class BioforestTransactionService implements ITransactionService { const { decimals, symbol } = config + // Get beginEpochTime once for all transactions + const beginEpochTime = await this.getBeginEpochTime() + // Show all transaction types for the address return transactions .map((item) => { @@ -437,7 +470,7 @@ export class BioforestTransactionService implements ITransactionService { confirmations: 1, requiredConfirmations: 1, }, - timestamp: tx.timestamp * 1000, // Convert to milliseconds + timestamp: tx.timestamp * 1000 + beginEpochTime, blockNumber: BigInt(item.height), type: 'transfer' as const, rawType: txType, // Pass the original chain transaction type