Files
superwork/vscode/src/issues/stateFields.ts
T

92 lines
3.1 KiB
TypeScript

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<Pick<Issue, CardStateField>>
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<string, unknown>): IssueStateFields {
const out: Record<string, unknown> = {}
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
}