✨ feat(vscode): 自带提示词变了就问要不要换成新版

This commit is contained in:
cruldra committed 2026-10-04 10:01:41 +08:00
1 parent 12d9080248
commit 38d06153bb
7 files changed
+138 -54

No files matched your search

+2
View File
@@ -11,6 +11,7 @@ import { setProfilesDirOverride } from './cc/profiles'
import { registerCodeReader } from './codeReader' import { registerCodeReader } from './codeReader'
import { logger } from './logging/logger' import { logger } from './logging/logger'
import { KanbanWebviewPanel } from './panel/KanbanPanel' import { KanbanWebviewPanel } from './panel/KanbanPanel'
import { offerPromptUpdates } from './settings/promptUpdate'
import { getSettings, migratePromptTemplates, persistSeededProfiles } from './settings/store' import { getSettings, migratePromptTemplates, persistSeededProfiles } from './settings/store'
import { registerCreateSessionUriHandler } from './uri/createSessionUriHandler' import { registerCreateSessionUriHandler } from './uri/createSessionUriHandler'
import { webhookCoordinator } from './webhook/coordinator' import { webhookCoordinator } from './webhook/coordinator'
@@ -43,6 +44,7 @@ export function activate(context: ExtensionContext): SuperpowersApi {
// 存量提示词先迁到 Handlebars,再让 persistSeededProfiles 在迁移结果上补 seed。 // 存量提示词先迁到 Handlebars,再让 persistSeededProfiles 在迁移结果上补 seed。
for (const note of migratePromptTemplates(context)) for (const note of migratePromptTemplates(context))
logger.add({ level: note.level, source: 'settings', message: note.message }) logger.add({ level: note.level, source: 'settings', message: note.message })
void offerPromptUpdates(context)
void persistSeededProfiles(context) void persistSeededProfiles(context)
// Start the webhook server immediately so PR callbacks are received even // Start the webhook server immediately so PR callbacks are received even
// when the Kanban panel is closed. Disposed automatically via subscriptions. // when the Kanban panel is closed. Disposed automatically via subscriptions.
@@ -4,10 +4,8 @@ import { describe, expect, it } from 'vitest'
import { PREVIOUS_DEFAULT_REVIEW_PROMPT } from './previousReviewPrompt' import { PREVIOUS_DEFAULT_REVIEW_PROMPT } from './previousReviewPrompt'
import { import {
composeBrainstormTemplate, composeBrainstormTemplate,
planDefaultPromptReplacement,
planPromptMigration, planPromptMigration,
planReviewPromptMigration, planReviewPromptMigration,
PREVIOUS_PROTOTYPE_PROMPTS,
PROMPT_TEMPLATE_VERSION, PROMPT_TEMPLATE_VERSION,
splitIfBlock, splitIfBlock,
upgradePlaceholders, upgradePlaceholders,
@@ -191,15 +189,3 @@ describe('planReviewPromptMigration', () => {
expect(planReviewPromptMigration(undefined, PREVIOUS_DEFAULT_REVIEW_PROMPT, nextDefault)).toBeNull() expect(planReviewPromptMigration(undefined, PREVIOUS_DEFAULT_REVIEW_PROMPT, nextDefault)).toBeNull()
}) })
}) })
describe('planDefaultPromptReplacement', () => {
it('旧的 Figma 插件提示词换成通用技能', () => {
const next = '/ecc:ui-prototyping 给 {{issueNumber}} 号工单画原型。\n'
const out = planDefaultPromptReplacement(PREVIOUS_PROTOTYPE_PROMPTS[0], PREVIOUS_PROTOTYPE_PROMPTS, next, '已换成通用技能')
expect(out?.prompt).toBe(next)
})
it('改过的提示词不动', () => {
expect(planDefaultPromptReplacement('我自己写的原型提示词', PREVIOUS_PROTOTYPE_PROMPTS, '新的', '已换')).toBeNull()
})
})
-28
View File
@@ -237,31 +237,3 @@ export function planReviewPromptMigration(
notes: [{ level: 'info', message: '审查提示词末尾已追加「审查结论」,其余段落未改' }], notes: [{ level: 'info', message: '审查提示词末尾已追加「审查结论」,其余段落未改' }],
} }
} }
/** 存的提示词和旧默认一样时,整段换成新默认。改过的不动。 */
export function planDefaultPromptReplacement(
stored: unknown,
previousDefaults: string[],
nextDefault: string,
note: string,
): { prompt: string, notes: MigrationNote[] } | null {
if (typeof stored !== 'string' || stored.length === 0)
return null
const now = normalizePrompt(stored)
const next = normalizePrompt(nextDefault)
if (!next || now === next)
return null
if (!previousDefaults.some(prev => normalizePrompt(prev) === now))
return null
return {
prompt: nextDefault.endsWith('\n') ? nextDefault : `${nextDefault}\n`,
notes: [{ level: 'info', message: note }],
}
}
export const PREVIOUS_PROTOTYPE_PROMPTS = [
'/ecc:figma-plugin-prototyping 给 {{issueNumber}} 号工单画原型:先读工单正文锁定要画的界面和状态,再写本地 Figma 插件把画板和点击流做出来。画完汇报插件路径和怎么导入,不要改业务代码。',
`/ecc:figma-plugin-prototyping 给 {{issueNumber}} 号工单画原型:先读工单正文锁定要画的界面和状态,再写本地 Figma 插件把画板和点击流做出来。画完汇报插件路径和怎么导入,不要改业务代码。
画完用 \`opencli spx issue marker --issue {{issueNumber}} --type prototype --value <manifest.json 的仓库相对路径>\` 把原型插件挂到工单上,再汇报。`,
]
+38
View File
@@ -0,0 +1,38 @@
import { describe, expect, it } from 'vitest'
import { promptDigest, promptUpdatesToAsk } from './promptUpdatePlan'
describe('promptUpdatesToAsk', () => {
it('和自带的不一样就问', () => {
const next = '新版原型提示词'
const out = promptUpdatesToAsk({
stored: { prototypePrompt: '旧的 Figma 插件提示词' },
defaults: { prototypePrompt: next },
decided: {},
})
expect(out).toEqual([{
key: 'prototypePrompt',
label: '原型提示词',
next,
hash: promptDigest(next),
}])
})
it('空的、一样的、这一版已经选过的不问', () => {
const next = '新版'
expect(promptUpdatesToAsk({
stored: { testPrompt: '' },
defaults: { testPrompt: next },
decided: {},
})).toEqual([])
expect(promptUpdatesToAsk({
stored: { testPrompt: next },
defaults: { testPrompt: `${next}\n` },
decided: {},
})).toEqual([])
expect(promptUpdatesToAsk({
stored: { testPrompt: '我改过' },
defaults: { testPrompt: next },
decided: { testPrompt: promptDigest(next) },
})).toEqual([])
})
})
+45
View File
@@ -0,0 +1,45 @@
import type { ExtensionContext } from 'vscode'
import type { PromptUpdateKey } from './promptUpdatePlan'
import { window } from 'vscode'
import { PROMPT_UPDATE_FIELDS, promptUpdatesToAsk } from './promptUpdatePlan'
import { readDefaultPrompt, readStoredSettings, saveSettings } from './store'
function decisionsOf(stored: Record<string, unknown>): Record<string, string> {
const raw = stored.promptDefaultChoices
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
return {}
const out: Record<string, string> = {}
for (const [key, value] of Object.entries(raw)) {
if (typeof value === 'string')
out[key] = value
}
return out
}
/** 自带提示词和设置里的不一样时问一句。选过的同一版不再问。 */
export async function offerPromptUpdates(ctx: ExtensionContext): Promise<void> {
const stored = readStoredSettings(ctx) as Record<string, unknown>
const defaults = Object.fromEntries(
PROMPT_UPDATE_FIELDS.map(field => [field.key, readDefaultPrompt(ctx.extensionPath, field.file)]),
) as Partial<Record<PromptUpdateKey, string>>
const pending = promptUpdatesToAsk({ stored, defaults, decided: decisionsOf(stored) })
if (pending.length === 0)
return
const choices = { ...decisionsOf(stored) }
const patch: Record<string, string> = {}
for (const item of pending) {
const pick = await window.showInformationMessage(
`${item.label}和这一版自带的不一样。要换成新版吗?`,
'使用新版',
'保留现有',
)
if (pick !== '使用新版' && pick !== '保留现有')
continue
choices[item.key] = item.hash
if (pick === '使用新版')
patch[item.key] = item.next
}
if (Object.keys(patch).length === 0 && Object.keys(choices).length === Object.keys(decisionsOf(stored)).length)
return
await saveSettings(ctx, { ...patch, promptDefaultChoices: choices })
}
+46
View File
@@ -0,0 +1,46 @@
import { createHash } from 'node:crypto'
export const PROMPT_UPDATE_FIELDS = [
{ key: 'brainstormPrompt', file: 'brainstorm', label: '头脑风暴提示词' },
{ key: 'implementPlanPrompt', file: 'implement-plan', label: '实施提示词' },
{ key: 'reviewPrompt', file: 'review', label: '审查提示词' },
{ key: 'testPrompt', file: 'test', label: '测试提示词' },
{ key: 'prototypePrompt', file: 'prototype', label: '原型提示词' },
] as const
export type PromptUpdateKey = typeof PROMPT_UPDATE_FIELDS[number]['key']
export interface PromptUpdateCandidate {
key: PromptUpdateKey
label: string
next: string
hash: string
}
export function promptDigest(text: string): string {
return createHash('sha256').update(text.replace(/\r\n/g, '\n').trim()).digest('hex')
}
/** 设置里有内容,且和这一版自带的不一样,并且这一版还没问过。 */
export function promptUpdatesToAsk(opts: {
stored: Record<string, unknown>
defaults: Partial<Record<PromptUpdateKey, string>>
decided: Record<string, string>
}): PromptUpdateCandidate[] {
const out: PromptUpdateCandidate[] = []
for (const field of PROMPT_UPDATE_FIELDS) {
const saved = opts.stored[field.key]
const next = opts.defaults[field.key]
if (typeof saved !== 'string' || saved.trim() === '')
continue
if (typeof next !== 'string' || next.trim() === '')
continue
if (promptDigest(saved) === promptDigest(next))
continue
const hash = promptDigest(next)
if (opts.decided[field.key] === hash)
continue
out.push({ key: field.key, label: field.label, next, hash })
}
return out
}
+7 -12
View File
@@ -19,7 +19,7 @@ import { seedCommitProfilePath } from '../cc/commitProfile'
import { listClaudeProfiles } from '../cc/profiles' import { listClaudeProfiles } from '../cc/profiles'
import { normalizeReleasePrefix } from '../git/release' import { normalizeReleasePrefix } from '../git/release'
import { PREVIOUS_DEFAULT_REVIEW_PROMPT } from './previousReviewPrompt' import { PREVIOUS_DEFAULT_REVIEW_PROMPT } from './previousReviewPrompt'
import { planDefaultPromptReplacement, planPromptMigration, planReviewPromptMigration, PREVIOUS_PROTOTYPE_PROMPTS } from './promptMigration' import { planPromptMigration, planReviewPromptMigration } from './promptMigration'
/** /**
* Fallback prompt strings used when the on-disk markdown can't be read. * Fallback prompt strings used when the on-disk markdown can't be read.
@@ -101,6 +101,11 @@ export interface Settings {
testPrompt: string testPrompt: string
/** 原型会话首个提示词模板;常用 `{{issueNumber}}`。 */ /** 原型会话首个提示词模板;常用 `{{issueNumber}}`。 */
prototypePrompt: string prototypePrompt: string
/**
* 某一版自带提示词用户已经选过「使用新版」或「保留现有」。
* 键是提示词字段,值是那一版自带正文的摘要。正文再变就再问。
*/
promptDefaultChoices?: Record<string, string>
/** Day-to-day development branch (e.g. `main`). Jenkins doesn't watch it. */ /** Day-to-day development branch (e.g. `main`). Jenkins doesn't watch it. */
devBranch: string devBranch: string
/** /**
@@ -524,17 +529,9 @@ export function migratePromptTemplates(ctx: ExtensionContext): MigrationNote[] {
PREVIOUS_DEFAULT_REVIEW_PROMPT, PREVIOUS_DEFAULT_REVIEW_PROMPT,
readDefaultPrompt(ctx.extensionPath, 'review'), readDefaultPrompt(ctx.extensionPath, 'review'),
) )
let blob = review const blob = review
? { ...(planned.blob ?? stored), reviewPrompt: review.prompt } ? { ...(planned.blob ?? stored), reviewPrompt: review.prompt }
: planned.blob : planned.blob
const prototype = planDefaultPromptReplacement(
(blob ?? stored).prototypePrompt,
PREVIOUS_PROTOTYPE_PROMPTS,
readDefaultPrompt(ctx.extensionPath, 'prototype'),
'原型提示词还是旧的 Figma 插件技能,已换成通用的 ui-prototyping',
)
if (prototype)
blob = { ...(blob ?? stored), prototypePrompt: prototype.prompt }
if (!blob) if (!blob)
return [] return []
const out: MigrationNote[] = [] const out: MigrationNote[] = []
@@ -556,8 +553,6 @@ export function migratePromptTemplates(ctx: ExtensionContext): MigrationNote[] {
out.push(...planned.notes) out.push(...planned.notes)
if (review) if (review)
out.push(...review.notes) out.push(...review.notes)
if (prototype)
out.push(...prototype.notes)
if (planned.blob) if (planned.blob)
out.push({ level: 'info', message: '提示词模板已迁到 Handlebars' }) out.push({ level: 'info', message: '提示词模板已迁到 Handlebars' })
return out return out