import type { Issue, IssueColumn } from '../gitea/types' const COLUMNS: readonly IssueColumn[] = ['todo', 'in-progress', 'review', 'done'] /** 非空字符串才算有值:状态里写空串表示把字段清掉。 */ const STRING_FIELDS = [ 'sessionId', 'implementSessionId', 'reviewSessionId', 'testSessionId', 'prototypeSessionId', 'profilePath', 'brainstormProfilePath', 'testProfilePath', 'prototypeProfilePath', 'pr', 'prMergedAt', 'branch', 'worktreePath', 'color', 'handoffBundleKey', 'handoffFrom', 'handoffRequestFrom', 'handoffRequestError', ] as const const BOOLEAN_FIELDS = ['prMerged', 'autoReview', 'reviewPassed'] as const /** 状态里能直接放到卡片上的字段;读不出合法值的字段不出现。 */ export type IssueStateFields = Partial> type CardStateField = 'column' | 'specFile' | 'planFile' | 'prototypeFile' | 'prDiffFile' | 'implementStatus' | typeof STRING_FIELDS[number] | typeof BOOLEAN_FIELDS[number] /** * 过滤 specFile / planFile 里的垃圾值:真实路径必须含 `/` 且以 `.md` 结尾。 * * `...`、`…` 是 cc 在头脑风暴阶段往工单正文里写的占位说明,旧版 webhook 曾把它们当路径存进状态,读出时在这里清掉。 */ export function isValidSpxFilePath(v: unknown): v is string { return typeof v === 'string' && v.length > 0 && v.includes('/') && v.endsWith('.md') && v !== '...' && v !== '…' } /** prototypeFile 指向 Figma 插件的 manifest.json,规则同 [isValidSpxFilePath],只是后缀换成 `.json`。 */ function isValidPrototypeFilePath(v: unknown): v is string { return typeof v === 'string' && v.length > 0 && v.includes('/') && v.endsWith('.json') && v !== '...' && v !== '…' } function isValidPrDiffFilePath(v: unknown): v is string { return typeof v === 'string' && /^docs\/pr-diff\/\S+\.md$/.test(v) } function isIssueColumn(v: unknown): v is IssueColumn { return typeof v === 'string' && (COLUMNS as readonly string[]).includes(v) } /** 把一份工单状态翻成卡片字段,只留合法的值,代码托管仓库和 YouTrack 的卡片都按它读。 */ export function issueStateFields(state: Record): IssueStateFields { const out: Record = {} if (isIssueColumn(state.column)) out.column = state.column for (const field of STRING_FIELDS) { const v = state[field] if (typeof v === 'string' && v.length > 0) out[field] = v } for (const field of BOOLEAN_FIELDS) { if (typeof state[field] === 'boolean') out[field] = state[field] } if (isValidSpxFilePath(state.specFile)) out.specFile = state.specFile if (isValidSpxFilePath(state.planFile)) out.planFile = state.planFile if (isValidPrototypeFilePath(state.prototypeFile)) out.prototypeFile = state.prototypeFile if (isValidPrDiffFilePath(state.prDiffFile)) out.prDiffFile = state.prDiffFile if (state.implementStatus === 'running' || state.implementStatus === 'done' || state.implementStatus === 'failed') out.implementStatus = state.implementStatus return out as IssueStateFields }