import type { TapdConfig } from "../types.js"; import { apiUrl, tapdGet, tapdPost } from "../core/http.js"; import { longTapdObjectId } from "../core/object-id.js"; import type { LinkedTapdObject, TapdGitKind } from "./types.js"; interface TapdDataResponse { status: number; data: T; info?: string; } interface BugFieldInfo { name: string; label: string; options?: Record; html_type?: string; } export interface MergeVersionMatch { fieldName?: string; value?: string; candidates?: string[]; reason?: string; } export async function fetchCommitKeyword( config: TapdConfig, object: LinkedTapdObject, ): Promise { const response = await tapdGet>( apiUrl(config, "/svn_commits/get_scm_copy_keywords", { workspace_id: object.workspaceId, object_id: longTapdObjectId(object.workspaceId, object.objectId), type: object.kind, }), config, ); if (!response?.data) throw new Error("无法获取 TAPD 源码提交关键字"); return response.data; } export async function fetchTaskEstimatedEffort( config: TapdConfig, object: LinkedTapdObject, ): Promise { const response = await tapdGet< TapdDataResponse<{ Task: { effort?: string } }[]> >( apiUrl(config, "/tasks", { workspace_id: object.workspaceId, id: longTapdObjectId(object.workspaceId, object.objectId), fields: "id,effort", limit: "1", }), config, ); const effort = response?.data?.[0]?.Task?.effort?.trim(); return effort && Number.isFinite(Number(effort)) && Number(effort) > 0 ? effort : undefined; } function objectApi(kind: TapdGitKind) { if (kind === "bug") return { path: "/bugs", key: "Bug" }; if (kind === "task") return { path: "/tasks", key: "Task" }; return { path: "/stories", key: "Story" }; } export async function updateTapdStatus( config: TapdConfig, object: LinkedTapdObject, status: string, currentOwner?: string, extraFields: Record = {}, ): Promise { const body: Record = { workspace_id: object.workspaceId, id: longTapdObjectId(object.workspaceId, object.objectId), v_status: status, ...extraFields, }; if (currentOwner) body.current_owner = currentOwner; const { path, key } = objectApi(object.kind); const response = await tapdPost>( apiUrl(config, path), config, body, ); if (!response) throw new Error(`TAPD ${object.kind} 状态更新失败`); const effort = extraFields.effort_completed; if (effort === undefined) return; const readEffort = async () => { const verify = await tapdGet< TapdDataResponse>[]> >( apiUrl(config, path, { workspace_id: object.workspaceId, id: String(body.id), fields: "id,effort_completed", limit: "1", }), config, ); return verify?.data?.[0]?.[key]?.effort_completed; }; if ((await readEffort()) === effort) return; const retry = await tapdPost>( apiUrl(config, path), config, { workspace_id: object.workspaceId, id: body.id, effort_completed: effort }, ); const saved = retry ? await readEffort() : undefined; if (saved !== effort) throw new Error( `TAPD ${object.kind} 完成工时回读不一致:期望 ${effort},实际 ${saved ?? "缺失"}`, ); } function normalizeVersion(value: string): string { return value.trim().replace(/^refs\/tags\//, ""); } function baseVersion(value: string): string { return normalizeVersion(value).replace(/\s*[((]\s*迭代[^))]*[))]\s*$/, ""); } function iterationCode(value: string): string | null { return ( value.match(/(?:迭代\s*)?(\d+\s*-\s*\d+)/)?.[1]?.replace(/\s/g, "") ?? null ); } function findMergeVersionField( data: Record, ): BugFieldInfo | null { return ( Object.values(data).find((field) => field.label === "合入版本") ?? null ); } async function fetchMergeVersionField( config: TapdConfig, workspaceId: string, ): Promise { const response = await tapdGet< TapdDataResponse> >( apiUrl(config, "/bugs/get_fields_info", { workspace_id: workspaceId }), config, ); return response?.data ? findMergeVersionField(response.data) : null; } function historicalVersion(field: BugFieldInfo): string | undefined { return Object.keys(field.options ?? {}).find( (option) => option === "其他(历史缺陷)" || option === "其他(历史缺陷)", ); } export async function matchHistoricalBugMergeVersion( config: TapdConfig, workspaceId: string, ): Promise { const field = await fetchMergeVersionField(config, workspaceId); if (!field) return { reason: "TAPD 未提供合入版本字段" }; const value = historicalVersion(field); return value ? { fieldName: field.name, value, reason: "未能定位引入 commit" } : { fieldName: field.name, reason: "合入版本候选值中没有“其他(历史缺陷)”", }; } export async function matchBugMergeVersion( config: TapdConfig, workspaceId: string, tag: string, iterationCodes: string[], ): Promise { const field = await fetchMergeVersionField(config, workspaceId); if (!field) return { reason: "TAPD 未提供合入版本字段" }; if (!field.options || Object.keys(field.options).length === 0) { return { fieldName: field.name, reason: "合入版本字段没有候选值" }; } const normalizedTag = normalizeVersion(tag); if (normalizedTag in field.options) { return { fieldName: field.name, value: normalizedTag }; } const candidates = Object.keys(field.options).filter( (option) => baseVersion(option) === normalizedTag, ); if (candidates.length === 0) { const historical = historicalVersion(field); if (historical) { return { fieldName: field.name, value: historical, reason: `候选值中不存在 ${tag},按规则使用 ${historical}`, }; } return { fieldName: field.name, reason: `候选值中不存在 ${tag},且没有“其他(历史缺陷)”`, }; } if (candidates.length === 1) { return { fieldName: field.name, value: candidates[0] }; } const uniqueCodes = Array.from(new Set(iterationCodes.filter(Boolean))); if (uniqueCodes.length !== 1) { return { fieldName: field.name, candidates: uniqueCodes.length === 0 ? candidates : undefined, reason: uniqueCodes.length === 0 ? `${tag} 有多个候选值,但引入 commit 的 TAPD 事项没有可用迭代,请人工选择` : `引入 commit 关联了多个迭代: ${uniqueCodes.join(", ")}`, }; } const matches = candidates.filter( (candidate) => iterationCode(candidate) === uniqueCodes[0], ); if (matches.length !== 1) { return { fieldName: field.name, reason: `${tag} 与迭代 ${uniqueCodes[0]} 无法唯一匹配合入版本候选值`, }; } return { fieldName: field.name, value: matches[0] }; } export async function fetchObjectIterationCode( config: TapdConfig, object: LinkedTapdObject, ): Promise { let path = "/stories"; let wrapper = "Story"; if (object.kind === "bug") { path = "/bugs"; wrapper = "Bug"; } else if (object.kind === "task") { path = "/tasks"; wrapper = "Task"; } const response = await tapdGet< TapdDataResponse>[]> >( apiUrl(config, path, { workspace_id: object.workspaceId, id: longTapdObjectId(object.workspaceId, object.objectId), fields: "id,iteration_id", limit: "1", }), config, ); const iterationId = response?.data?.[0]?.[wrapper]?.iteration_id; if (!iterationId) return null; const iterations = await tapdGet< TapdDataResponse<{ Iteration: Record }[]> >( apiUrl(config, "/iterations", { workspace_id: object.workspaceId, id: iterationId, fields: "id,name", limit: "1", }), config, ); const name = iterations?.data?.[0]?.Iteration?.name; return name ? iterationCode(name) : null; } export async function createBugRemark( config: TapdConfig, object: LinkedTapdObject, author: string, description: string, ): Promise { const response = await tapdPost>( apiUrl(config, "/comments"), config, { workspace_id: object.workspaceId, entry_id: longTapdObjectId(object.workspaceId, object.objectId), entry_type: "bug_remark", author, description, }, ); if (!response) throw new Error("TAPD Bug 流转备注写入失败"); } export function tapdKindLabel(kind: TapdGitKind): string { if (kind === "bug") return "Bug"; if (kind === "task") return "任务"; return "需求"; }