Files
superwork/vscode/src/youtrack/issueLoader.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

113 lines
4.3 KiB
TypeScript

/**
* Loads YouTrack-sourced issues into the panel's `Issue` shape — the YouTrack
* counterpart of `src/gitea/issueLoader.ts`.
*
* Returns `[]` (not an error) when YouTrack isn't configured, so the panel can
* always call this alongside the Gitea loader and just merge the results.
*/
import type { ExtensionContext } from 'vscode'
import type { Issue, IssueColumn } from '../gitea/types'
import type { YouTrackIssue } from './api'
import { workspace } from 'vscode'
import { getYouTrackToken } from '../auth/secrets'
import { getSettings } from '../settings/store'
import { issueHtmlUrl, listIssues } from './api'
import { readImportedIds } from './importStore'
import { findLatestState } from './stateComment'
const VALID_COLUMNS = new Set<IssueColumn>(['todo', 'in-progress', 'review', 'done'])
/** Host part of the configured base URL — the SecretStorage token key. */
export function youtrackHost(baseUrl: string): string {
try {
return new URL(baseUrl).host
}
catch {
return baseUrl.replace(/^https?:\/\//, '').replace(/\/.*$/, '')
}
}
function str(v: unknown): string | undefined {
return typeof v === 'string' && v.length > 0 ? v : undefined
}
/**
* Offset added to a YouTrack issue's numeric id so its board `number` never
* collides with a gitea issue number (which is the routing key for
* webview↔extension messages and the `number → IssueRef` map).
* ponytail: assumes gitea numbers < 1,000,000 and a single mirrored project
* (readable ids are unique within one project). Widen / hash if either breaks.
*/
const YOUTRACK_NUMBER_OFFSET = 1_000_000
/** `LXF-12` → `1_000_012`; falls back to the offset when there's no numeric tail. */
function syntheticNumber(idReadable: string): number {
const m = idReadable.match(/-(\d+)$/)
return YOUTRACK_NUMBER_OFFSET + (m ? Number(m[1]) : 0)
}
function pickColumn(rawColumn: unknown, resolved: number | null): IssueColumn {
if (typeof rawColumn === 'string' && VALID_COLUMNS.has(rawColumn as IssueColumn))
return rawColumn as IssueColumn
return resolved != null ? 'done' : 'todo'
}
function toIssue(baseUrl: string, it: YouTrackIssue): Issue {
const state = findLatestState(it.comments) ?? {}
return {
id: `youtrack:${it.idReadable}`,
externalId: it.idReadable,
source: 'youtrack',
number: syntheticNumber(it.idReadable),
title: it.summary,
column: pickColumn(state.column, it.resolved),
htmlUrl: issueHtmlUrl(baseUrl, it.idReadable),
attachments: it.attachments,
sessionId: str(state.sessionId),
implementSessionId: str(state.implementSessionId),
reviewSessionId: str(state.reviewSessionId),
testSessionId: str(state.testSessionId),
prototypeSessionId: str(state.prototypeSessionId),
profilePath: str(state.profilePath),
brainstormProfilePath: str(state.brainstormProfilePath),
testProfilePath: str(state.testProfilePath),
prototypeProfilePath: str(state.prototypeProfilePath),
specFile: str(state.specFile),
planFile: str(state.planFile),
prototypeFile: str(state.prototypeFile),
prDiffFile: str(state.prDiffFile),
pr: str(state.pr),
branch: str(state.branch),
worktreePath: str(state.worktreePath),
color: str(state.color),
implementStatus: state.implementStatus === 'running' || state.implementStatus === 'done' || state.implementStatus === 'failed'
? state.implementStatus
: undefined,
autoReview: typeof state.autoReview === 'boolean' ? state.autoReview : undefined,
}
}
export async function loadYouTrackIssues(ctx: ExtensionContext): Promise<Issue[]> {
const settings = getSettings(ctx)
const baseUrl = settings.youtrackBaseUrl.trim()
const project = settings.youtrackProjectShortName.trim()
if (!baseUrl || !project)
return []
const token = await getYouTrackToken(ctx, youtrackHost(baseUrl))
if (!token)
return []
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
if (!workspaceRoot)
return []
// 只镜像用户主动勾选导入的工单(见 importStore),不再加载即全量同步。
// import 集为空 ⇒ 返回 []。
const importedSet = new Set(await readImportedIds(workspaceRoot, project))
if (importedSet.size === 0)
return []
const raw = await listIssues({ baseUrl, token }, project)
return raw
.filter(it => importedSet.has(it.idReadable))
.map(it => toIssue(baseUrl, it))
}