✨ 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:
1 parent
def5f4e88c
commit
142af57d9d
24 files changed
+628
-85
No files matched your search
@@ -48,6 +48,38 @@ export interface PendingHook {
|
||||
feature: string
|
||||
}
|
||||
|
||||
/**
|
||||
* issue body 里的 spx marker → state JSON 字段。三种 marker 走同一套同步
|
||||
* 逻辑,这张表是唯一真源(与 cli 的 markerTypeToStateField 对应)。
|
||||
*
|
||||
* 路径必须含 `/` 且以对应后缀结尾:cc 在头脑风暴阶段会往 body 里写
|
||||
* `<!-- spx:spec=... -->` 这种占位说明文,不卡死就会被当成真路径存进去。
|
||||
*/
|
||||
const BODY_MARKERS = [
|
||||
{ type: 'spec', field: 'specFile', ext: 'md' },
|
||||
{ type: 'plan', field: 'planFile', ext: 'md' },
|
||||
{ type: 'prototype', field: 'prototypeFile', ext: 'json' },
|
||||
] as const
|
||||
|
||||
/** 本次从 body 里扫出来的 marker 值,按 state JSON 字段名索引。 */
|
||||
type MarkerPatch = { [K in typeof BODY_MARKERS[number]['field']]?: string }
|
||||
|
||||
function parseBodyMarkers(body: string): MarkerPatch {
|
||||
const out: MarkerPatch = {}
|
||||
for (const { type, field, ext } of BODY_MARKERS) {
|
||||
const re = new RegExp(String.raw`<!--\s*spx:${type}=([^\s/>]*\/[^\s>]*\.${ext})\s*-->`)
|
||||
const m = body.match(re)
|
||||
if (m)
|
||||
out[field] = m[1]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** 日志用的一行摘要,例如 `spec=a.md plan=<空> prototype=<空>`。 */
|
||||
function describeMarkers(patch: MarkerPatch): string {
|
||||
return BODY_MARKERS.map(({ type, field }) => `${type}=${patch[field] ?? '<空>'}`).join(' ')
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link PrWebhookEvent} whose `issueNumber` has been resolved (either from
|
||||
* the legacy `/webhook/:n` path or via PR-body / branch heuristics).
|
||||
@@ -779,11 +811,10 @@ class WebhookCoordinator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue `edited` payload: cc updates the issue body with `<!-- spx:spec=... -->`
|
||||
* / `<!-- spx:plan=... -->` annotation lines as it discovers spec/plan files.
|
||||
* We scan the body for those markers, diff against the state-JSON comment,
|
||||
* and (if anything changed) merge the new paths in + patch the open panel
|
||||
* so the detail view refreshes without a full kanban reload.
|
||||
* Issue `edited` payload: cc 会往 issue body 里追加 `<!-- spx:spec=... -->`
|
||||
* / `<!-- spx:plan=... -->` / `<!-- spx:prototype=... -->` 三种 marker。
|
||||
* 这里把 body 里的 marker 扫出来,与 state JSON comment 里的当前值比对,
|
||||
* 有变化就合并写回,并给打开着的面板推一次 issue/patch,省掉整板刷新。
|
||||
*/
|
||||
private async handleIssueEdited(event: IssueWebhookEvent): Promise<void> {
|
||||
if (!this.ctx)
|
||||
@@ -817,17 +848,12 @@ class WebhookCoordinator {
|
||||
return
|
||||
}
|
||||
|
||||
// 路径必须含 `/` 且以 `.md` 结尾,避免 cc 写的 `<!-- spx:spec=... -->` 占位被当真。
|
||||
const specMatch = event.body.match(/<!--\s*spx:spec=([^\s/>]*\/[^\s>]*\.md)\s*-->/)
|
||||
const planMatch = event.body.match(/<!--\s*spx:plan=([^\s/>]*\/[^\s>]*\.md)\s*-->/)
|
||||
const specFile = specMatch ? specMatch[1] : undefined
|
||||
const planFile = planMatch ? planMatch[1] : undefined
|
||||
|
||||
if (!specFile && !planFile) {
|
||||
const found = parseBodyMarkers(event.body)
|
||||
if (Object.keys(found).length === 0) {
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'webhook',
|
||||
message: `issue=#${event.issueNumber} edited body 无 spx:spec/spx:plan 注释,跳过`,
|
||||
message: `issue=#${event.issueNumber} edited body 无 spx marker 注释,跳过`,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -852,15 +878,19 @@ class WebhookCoordinator {
|
||||
})
|
||||
}
|
||||
|
||||
const currentSpec = typeof currentState.specFile === 'string' ? currentState.specFile : undefined
|
||||
const currentPlan = typeof currentState.planFile === 'string' ? currentState.planFile : undefined
|
||||
|
||||
// cc only adds or updates; it never deletes. Treat an absent marker
|
||||
// (undefined) as "no signal", not "clear" — that way deleting a spec
|
||||
// line by mistake won't lose the existing pointer.
|
||||
const nextSpec = specFile ?? currentSpec
|
||||
const nextPlan = planFile ?? currentPlan
|
||||
if (currentSpec === nextSpec && currentPlan === nextPlan) {
|
||||
// cc 只增不删:body 里没有某个 marker 当「没信号」,不当「清空」,
|
||||
// 免得手滑删掉一行就把已有指针丢了。
|
||||
const next: MarkerPatch = {}
|
||||
let changed = false
|
||||
for (const { field } of BODY_MARKERS) {
|
||||
const current = typeof currentState[field] === 'string' ? currentState[field] as string : undefined
|
||||
const value = found[field] ?? current
|
||||
if (value)
|
||||
next[field] = value
|
||||
if (value !== current)
|
||||
changed = true
|
||||
}
|
||||
if (!changed) {
|
||||
// Gitea state JSON 已是最新(spx 同步写过的情况),跳过 mergeStateJsonComment
|
||||
// 节省一次 API 写。但 webview 内存里可能还是旧值(webview 不轮询,全靠
|
||||
// coordinator 推 issue/patch),所以**仍然要推一次 patch** 把当前值同步过去,
|
||||
@@ -868,24 +898,17 @@ class WebhookCoordinator {
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'webhook',
|
||||
message: `issue=#${event.issueNumber} edited spec/plan 在 state JSON 已同步,跳过 merge 但推 issue/patch 刷 webview (spec=${nextSpec ?? '<空>'} plan=${nextPlan ?? '<空>'})`,
|
||||
message: `issue=#${event.issueNumber} edited marker 在 state JSON 已同步,跳过 merge 但推 issue/patch 刷 webview (${describeMarkers(next)})`,
|
||||
})
|
||||
this.activePanel?.postMessage({
|
||||
type: 'issue/patch',
|
||||
issueNumber: event.issueNumber,
|
||||
patch: next,
|
||||
})
|
||||
if (this.activePanel) {
|
||||
const patch: { specFile?: string, planFile?: string } = {}
|
||||
if (nextSpec)
|
||||
patch.specFile = nextSpec
|
||||
if (nextPlan)
|
||||
patch.planFile = nextPlan
|
||||
this.activePanel.postMessage({
|
||||
type: 'issue/patch',
|
||||
issueNumber: event.issueNumber,
|
||||
patch,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// spec/plan 落库由责任人机器执行;规划阶段还没有实施终端,兜底证据
|
||||
// marker 落库由责任人机器执行;规划阶段还没有实施终端,兜底证据
|
||||
// 额外认规划终端。observe 机器只把新值推给本机 webview。
|
||||
const ownership = await this.resolveOwnership(
|
||||
event.issueNumber,
|
||||
@@ -893,27 +916,14 @@ class WebhookCoordinator {
|
||||
['实施', '测试', '规划'],
|
||||
)
|
||||
if (ownership === 'observe') {
|
||||
if (this.activePanel) {
|
||||
const patch: { specFile?: string, planFile?: string } = {}
|
||||
if (specFile)
|
||||
patch.specFile = specFile
|
||||
if (planFile)
|
||||
patch.planFile = planFile
|
||||
this.activePanel.postMessage({
|
||||
type: 'issue/patch',
|
||||
issueNumber: event.issueNumber,
|
||||
patch,
|
||||
})
|
||||
}
|
||||
this.activePanel?.postMessage({
|
||||
type: 'issue/patch',
|
||||
issueNumber: event.issueNumber,
|
||||
patch: found,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const extra: Record<string, unknown> = {}
|
||||
if (specFile)
|
||||
extra.specFile = specFile
|
||||
if (planFile)
|
||||
extra.planFile = planFile
|
||||
|
||||
try {
|
||||
await mergeStateJsonComment({
|
||||
host: remote.host,
|
||||
@@ -921,7 +931,7 @@ class WebhookCoordinator {
|
||||
repo: remote.repo,
|
||||
token,
|
||||
issueNumber: event.issueNumber,
|
||||
extra,
|
||||
extra: found,
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
@@ -938,22 +948,15 @@ class WebhookCoordinator {
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'webhook',
|
||||
message: `issue=#${event.issueNumber} edited spec/plan 已同步`,
|
||||
details: `spec=${specFile ?? '<未变>'} plan=${planFile ?? '<未变>'}`,
|
||||
message: `issue=#${event.issueNumber} edited marker 已同步`,
|
||||
details: describeMarkers(found),
|
||||
})
|
||||
|
||||
if (this.activePanel) {
|
||||
const patch: { specFile?: string, planFile?: string } = {}
|
||||
if (specFile)
|
||||
patch.specFile = specFile
|
||||
if (planFile)
|
||||
patch.planFile = planFile
|
||||
this.activePanel.postMessage({
|
||||
type: 'issue/patch',
|
||||
issueNumber: event.issueNumber,
|
||||
patch,
|
||||
})
|
||||
}
|
||||
this.activePanel?.postMessage({
|
||||
type: 'issue/patch',
|
||||
issueNumber: event.issueNumber,
|
||||
patch: found,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user