Files
superwork/vscode/src/youtrack/stateComment.ts
T
cruldra 142af57d9d ✨ feat(vscode): 工单挂原型插件,详情面板一键导入 Figma
- prototypeFile 走通 state → Issue → webview 全链路(gitea/youtrack 两个
  loader、两份 KNOWN_STATE_FIELDS、issue/patch、webview types),按 planFile
  同款当共享字段;路径校验换成 .json 版,允许绝对路径
- 详情面板「计划文件」下加「原型插件路径」行:双击开 manifest.json,
  行尾按钮「在 Figma 中导入并打开」;没挂插件时只显示空态
- src/figma/:把 manifest 写进 Figma 桌面版的 localFileExtensions 再拉起
  Figma。坑是 Figma 退出会用内存状态覆盖 settings.json,所以写之前先
  osascript 优雅退出并轮询等它真没了(10s 等不到就让用户手动关,不强杀);
  写前备份 settings.json.bak-<时间戳>;已导入过就跳过写入直接打开;
  非 macOS 只把路径复制到剪贴板
- webhook 里 spec/plan 的 marker 同步改成表驱动,顺带认第三种 prototype
- 提示词:prototype.md 要求画完挂 marker;brainstorm-continue.md 新增
  {prototypeRef} 占位符,有原型插件时让续聊先读它别从零重画,没有时
  连同整段抹掉

Claude-Session: https://claude.ai/code/session_01Kgmb1LvrPmFX3NoUDB4zZC
2026-09-15 00:11:49 +08:00

86 lines
3.2 KiB
TypeScript

/**
* YouTrack equivalent of `src/gitea/stateJson.ts`.
*
* Workflow state (column, sessionId, specFile, …) for a YouTrack-sourced issue
* is persisted as a comment on the issue. The comment body is a marker line
* followed by the JSON blob:
*
* <!--spx-state-->
* {"column":"in-progress","sessionId":"..."}
*
* The marker makes the internal blob identifiable in the customer-facing
* tracker (the user accepted that this state lives in comments). We scan
* comments from the tail and pick the latest one carrying the marker, so
* ordinary customer replies posted afterwards never shadow the real state.
*/
import type { YouTrackAuth, YouTrackComment } from './api'
import { addComment, listComments } from './api'
export const STATE_MARKER = '<!--spx-state-->'
/** Same field set as the Gitea state blob — kept independent to avoid coupling. */
const KNOWN_STATE_FIELDS = ['column', 'sessionId', 'implementSessionId', 'reviewSessionId', 'testSessionId', 'prototypeSessionId', 'profilePath', 'brainstormProfilePath', 'testProfilePath', 'prototypeProfilePath', 'specFile', 'planFile', 'prototypeFile', 'prDiffFile', 'pr', 'prMerged', 'prMergedAt', 'branch', 'worktreePath', 'implementStatus', 'color', 'autoReview', 'handoffAttachmentId', 'handoffFrom'] as const
/**
* Parse a comment body into a state object, or null if it isn't a state
* comment. Accepts either a marker-prefixed body or a bare JSON object that
* carries a known state field (tolerant of older blobs written without the
* marker).
*/
export function parseStateComment(body: string | undefined | null): Record<string, unknown> | null {
let text = (body ?? '').trim()
if (!text)
return null
if (text.startsWith(STATE_MARKER))
text = text.slice(STATE_MARKER.length).trim()
let parsed: unknown
try {
parsed = JSON.parse(text)
}
catch {
return null
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
return null
const obj = parsed as Record<string, unknown>
return KNOWN_STATE_FIELDS.some(f => f in obj) ? obj : null
}
/** Latest state blob across a comment list (tail-first), or null. */
export function findLatestState(comments: YouTrackComment[]): Record<string, unknown> | null {
for (let i = comments.length - 1; i >= 0; i--) {
const parsed = parseStateComment(comments[i].text)
if (parsed)
return parsed
}
return null
}
function serialize(state: Record<string, unknown>): string {
return `${STATE_MARKER}\n${JSON.stringify(state)}`
}
/** Read the issue's latest state blob, or `{}` when none exists. */
export async function readStateComment(
auth: YouTrackAuth,
issueId: string,
): Promise<Record<string, unknown>> {
const comments = await listComments(auth, issueId)
return findLatestState(comments) ?? {}
}
/**
* Merge `extra` into the issue's latest state blob and post a new state
* comment. Re-reads comments first so we never merge onto a stale blob.
*/
export async function mergeStateComment(
auth: YouTrackAuth,
issueId: string,
extra: Record<string, unknown>,
): Promise<void> {
const comments = await listComments(auth, issueId)
const current = findLatestState(comments) ?? {}
await addComment(auth, issueId, serialize({ ...current, ...extra }))
}