✨ 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
This commit is contained in:
cruldra committed 2026-09-15 00:11:49 +08:00
1 parent def5f4e88c
commit 142af57d9d
24 files changed
+628 -85

No files matched your search

+116
View File
@@ -0,0 +1,116 @@
/**
* Figma 桌面版「已导入的开发插件」清单的纯逻辑:算新 id、查重、造条目。
*
* 清单落在 `~/Library/Application Support/Figma/settings.json` 的
* `localFileExtensions` 数组里,一条 manifest 条目长这样:
*
* ```json
* { "id": 9, "manifestPath": "/abs/.../manifest.json",
* "lastKnownName": "Xxx Design Lab", "lastKnownPluginId": "",
* "fileMetadata": { "type": "manifest", "codeFileId": 10, "uiFileIds": [] } }
* ```
*
* 坑:id 是**整个数组共享**的递增计数器,manifest 条目占一个 id,Figma 之后
* 给它的 code.js 建条目时占 `fileMetadata.codeFileId` 那个 id。所以算下一个
* id 时不能只看已有条目的 id,还得把被 codeFileId / manifestFileId 预定掉的
* 号算进去,否则新插件会和 Figma 迟早要建的 code 条目撞号。
*
* 这里不碰文件系统,好单测;读写与关 Figma 在 importPlugin.ts。
*/
export interface FigmaFileMetadata {
type: 'manifest' | 'code' | string
/** manifest 条目专有:预留给同插件 code.js 条目的 id。 */
codeFileId?: number
/** code 条目专有:反指所属 manifest 条目的 id。 */
manifestFileId?: number
uiFileIds?: number[]
}
export interface FigmaLocalFileExtension {
id: number
manifestPath: string
lastKnownName?: string
lastKnownPluginId?: string
fileMetadata?: FigmaFileMetadata
[key: string]: unknown
}
/** 数组里混着 Figma 写的杂项,只把「有数字 id + 字符串 manifestPath」的当条目。 */
export function asExtensionEntries(value: unknown): FigmaLocalFileExtension[] {
if (!Array.isArray(value))
return []
return value.filter((e): e is FigmaLocalFileExtension =>
!!e
&& typeof e === 'object'
&& typeof (e as FigmaLocalFileExtension).id === 'number'
&& typeof (e as FigmaLocalFileExtension).manifestPath === 'string',
)
}
/**
* 下一个可用 id:已有条目 id 与被 codeFileId / manifestFileId 预定的 id 取最大再 +1。
* 空清单从 1 开始(Figma 自己也是从 1 起)。
*/
export function nextExtensionId(entries: FigmaLocalFileExtension[]): number {
let max = 0
for (const e of entries) {
for (const n of [e.id, e.fileMetadata?.codeFileId, e.fileMetadata?.manifestFileId]) {
if (typeof n === 'number' && Number.isFinite(n) && n > max)
max = n
}
}
return max + 1
}
/** 按 manifestPath 精确查重;已导入过就返回那条,用来跳过写入。 */
export function findManifestEntry(
entries: FigmaLocalFileExtension[],
manifestPath: string,
): FigmaLocalFileExtension | undefined {
return entries.find(e => e.manifestPath === manifestPath)
}
/** 造一条 manifest 条目;code 条目留给 Figma 自己建,这里只把 id 号占住。 */
export function buildManifestEntry(opts: {
id: number
manifestPath: string
name: string
}): FigmaLocalFileExtension {
return {
id: opts.id,
manifestPath: opts.manifestPath,
lastKnownName: opts.name,
lastKnownPluginId: '',
fileMetadata: {
type: 'manifest',
codeFileId: opts.id + 1,
uiFileIds: [],
},
}
}
export interface AppendResult {
/** 追加后的完整条目数组;已存在时原样返回。 */
entries: FigmaLocalFileExtension[]
/** false = 已导入过,调用方跳过写文件直接拉起 Figma。 */
added: boolean
/** 这条插件在 Figma 里的显示名(新建用传入名,已存在用清单里记的名)。 */
name: string
}
/** 查重 → 算 id → 追加,一次算完,写文件的那端只管落盘。 */
export function appendManifestEntry(
entries: FigmaLocalFileExtension[],
opts: { manifestPath: string, name: string },
): AppendResult {
const existing = findManifestEntry(entries, opts.manifestPath)
if (existing)
return { entries, added: false, name: existing.lastKnownName || opts.name }
const entry = buildManifestEntry({
id: nextExtensionId(entries),
manifestPath: opts.manifestPath,
name: opts.name,
})
return { entries: [...entries, entry], added: true, name: opts.name }
}