✨ feat(vscode): 自带提示词变了就问要不要换成新版
This commit is contained in:
1 parent
12d9080248
commit
38d06153bb
7 files changed
+138
-54
No files matched your search
@@ -11,6 +11,7 @@ import { setProfilesDirOverride } from './cc/profiles'
|
||||
import { registerCodeReader } from './codeReader'
|
||||
import { logger } from './logging/logger'
|
||||
import { KanbanWebviewPanel } from './panel/KanbanPanel'
|
||||
import { offerPromptUpdates } from './settings/promptUpdate'
|
||||
import { getSettings, migratePromptTemplates, persistSeededProfiles } from './settings/store'
|
||||
import { registerCreateSessionUriHandler } from './uri/createSessionUriHandler'
|
||||
import { webhookCoordinator } from './webhook/coordinator'
|
||||
@@ -43,6 +44,7 @@ export function activate(context: ExtensionContext): SuperpowersApi {
|
||||
// 存量提示词先迁到 Handlebars,再让 persistSeededProfiles 在迁移结果上补 seed。
|
||||
for (const note of migratePromptTemplates(context))
|
||||
logger.add({ level: note.level, source: 'settings', message: note.message })
|
||||
void offerPromptUpdates(context)
|
||||
void persistSeededProfiles(context)
|
||||
// Start the webhook server immediately so PR callbacks are received even
|
||||
// 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 {
|
||||
composeBrainstormTemplate,
|
||||
planDefaultPromptReplacement,
|
||||
planPromptMigration,
|
||||
planReviewPromptMigration,
|
||||
PREVIOUS_PROTOTYPE_PROMPTS,
|
||||
PROMPT_TEMPLATE_VERSION,
|
||||
splitIfBlock,
|
||||
upgradePlaceholders,
|
||||
@@ -191,15 +189,3 @@ describe('planReviewPromptMigration', () => {
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -237,31 +237,3 @@ export function planReviewPromptMigration(
|
||||
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 的仓库相对路径>\` 把原型插件挂到工单上,再汇报。`,
|
||||
]
|
||||
@@ -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([])
|
||||
})
|
||||
})
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import { seedCommitProfilePath } from '../cc/commitProfile'
|
||||
import { listClaudeProfiles } from '../cc/profiles'
|
||||
import { normalizeReleasePrefix } from '../git/release'
|
||||
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.
|
||||
@@ -101,6 +101,11 @@ export interface Settings {
|
||||
testPrompt: string
|
||||
/** 原型会话首个提示词模板;常用 `{{issueNumber}}`。 */
|
||||
prototypePrompt: string
|
||||
/**
|
||||
* 某一版自带提示词用户已经选过「使用新版」或「保留现有」。
|
||||
* 键是提示词字段,值是那一版自带正文的摘要。正文再变就再问。
|
||||
*/
|
||||
promptDefaultChoices?: Record<string, string>
|
||||
/** Day-to-day development branch (e.g. `main`). Jenkins doesn't watch it. */
|
||||
devBranch: string
|
||||
/**
|
||||
@@ -524,17 +529,9 @@ export function migratePromptTemplates(ctx: ExtensionContext): MigrationNote[] {
|
||||
PREVIOUS_DEFAULT_REVIEW_PROMPT,
|
||||
readDefaultPrompt(ctx.extensionPath, 'review'),
|
||||
)
|
||||
let blob = review
|
||||
const blob = review
|
||||
? { ...(planned.blob ?? stored), reviewPrompt: review.prompt }
|
||||
: 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)
|
||||
return []
|
||||
const out: MigrationNote[] = []
|
||||
@@ -556,8 +553,6 @@ export function migratePromptTemplates(ctx: ExtensionContext): MigrationNote[] {
|
||||
out.push(...planned.notes)
|
||||
if (review)
|
||||
out.push(...review.notes)
|
||||
if (prototype)
|
||||
out.push(...prototype.notes)
|
||||
if (planned.blob)
|
||||
out.push({ level: 'info', message: '提示词模板已迁到 Handlebars' })
|
||||
return out
|
||||
|
||||
Reference in new issue
Block a user