From 8bb983eb03f11e414462da800625ee8bb8df4c63 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Fri, 9 Jan 2026 14:17:23 +0800 Subject: [PATCH 1/5] fix(bioforest): add beginEpochTime to transaction timestamps BioForest chain timestamps are relative to the genesis block's beginEpochTime, not absolute Unix timestamps. This fix adds the epoch time offset to correctly display transaction times. Before: timestamps showed 1970 (only relative seconds * 1000) After: timestamps show correct dates (relative seconds * 1000 + epochTime) Changes: - Add getBeginEpochTime() to fetch and cache epoch time from SDK - Add toAbsoluteTimestamp() helper for timestamp conversion - Fix getTransaction() to use absolute timestamps - Fix getTransactionHistory() to use absolute timestamps --- .../bioforest/transaction-service.ts | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) 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 From f97d0ecfa45596d7e6693751877d8d835cbe9120 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Fri, 9 Jan 2026 14:29:27 +0800 Subject: [PATCH 2/5] feat(agent-flow): add label management with auto-creation support - Add top-level await to load labels from GitHub at startup - Add getLabels() to list all available labels with colors - Add createLabel() and ensureLabels() for label management - Add --create-labels flag to auto-create missing labels - Add --list-labels flag to display available labels grouped by prefix - Update createIssue/createPr to validate labels before creation Usage: pnpm agent task start --list-labels pnpm agent task start --type service --title 'Feature' --create-labels --- scripts/agent-flow/mcps/git-workflow.mcp.ts | 116 +++++++++++++++++- scripts/agent-flow/workflows/task.workflow.ts | 38 +++++- 2 files changed, 149 insertions(+), 5 deletions(-) diff --git a/scripts/agent-flow/mcps/git-workflow.mcp.ts b/scripts/agent-flow/mcps/git-workflow.mcp.ts index 396e4d544..914aff77a 100755 --- a/scripts/agent-flow/mcps/git-workflow.mcp.ts +++ b/scripts/agent-flow/mcps/git-workflow.mcp.ts @@ -46,8 +46,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 +169,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..d7ba59633 100755 --- a/scripts/agent-flow/workflows/task.workflow.ts +++ b/scripts/agent-flow/workflows/task.workflow.ts @@ -22,6 +22,7 @@ import { createWorktree, pushWorktree, updateIssue, + getLabels, } from "../mcps/git-workflow.mcp.ts"; import { getRelatedChapters } from "../mcps/whitebook.mcp.ts"; @@ -98,15 +99,44 @@ const startWorkflow = defineWorkflow({ name: "start", description: "启动新任务 (Issue -> Branch -> Worktree -> Draft PR)", 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 +144,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 +156,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 +170,7 @@ const startWorkflow = defineWorkflow({ title, body: description, labels, + createLabels, }); console.log(` ✅ Issue #${issueId} Created: ${issueUrl}`); @@ -168,6 +201,7 @@ const startWorkflow = defineWorkflow({ base: "main", draft: true, labels, + createLabels, }); console.log(` ✅ Draft PR Created: ${prUrl}`); @@ -280,6 +314,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", "提交任务"], ], From 2bf22358ca2dd91925a8ff92f8aff448d3a4bacd Mon Sep 17 00:00:00 2001 From: Gaubee Date: Fri, 9 Jan 2026 14:32:06 +0800 Subject: [PATCH 3/5] feat(agent-flow): skip CI on initial draft PR commit Add [skip ci] to initial commit message to avoid triggering CI runners for draft PRs. CI will run normally when task submit is called. --- scripts/agent-flow/workflows/task.workflow.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/agent-flow/workflows/task.workflow.ts b/scripts/agent-flow/workflows/task.workflow.ts index d7ba59633..b3f04a470 100755 --- a/scripts/agent-flow/workflows/task.workflow.ts +++ b/scripts/agent-flow/workflows/task.workflow.ts @@ -185,11 +185,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 From b5b2bc1254cc689ced0b3300506e5a1c28c87666 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Fri, 9 Jan 2026 14:34:05 +0800 Subject: [PATCH 4/5] docs(agent-flow): add detailed documentation for workflow and mcp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Document workflow stages (start → develop → submit) - Explain CI skip behavior for draft PRs - Document label management features - Add color inference rules for new labels --- scripts/agent-flow/mcps/git-workflow.mcp.ts | 21 ++++++++++ scripts/agent-flow/workflows/task.workflow.ts | 42 +++++++++++++++---- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/scripts/agent-flow/mcps/git-workflow.mcp.ts b/scripts/agent-flow/mcps/git-workflow.mcp.ts index 914aff77a..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"; diff --git a/scripts/agent-flow/workflows/task.workflow.ts b/scripts/agent-flow/workflows/task.workflow.ts index b3f04a470..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"; @@ -97,7 +125,7 @@ ${desc} */ const startWorkflow = defineWorkflow({ name: "start", - description: "启动新任务 (Issue -> Branch -> Worktree -> Draft PR)", + description: "启动新任务 (Issue + Worktree + Draft PR,不触发 CI)", args: { title: { type: "string", description: "任务标题", required: false }, type: { @@ -250,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) { From f8eb97e693757bdcdcfeee1f232253f179ff8c0d Mon Sep 17 00:00:00 2001 From: Gaubee Date: Fri, 9 Jan 2026 14:40:18 +0800 Subject: [PATCH 5/5] refactor(flow): rename workflow tool to 'use' Rename the MCP tool from 'workflow' to 'use' for cleaner API: - workflow___workflow -> workflow___use - Update examples in dynamic description --- packages/flow/src/meta/meta.mcp.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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