✨ feat(vscode): 七份提示词统一走 Handlebars,头脑风暴三档合一

- 模板引擎换成 Handlebars(noEscape,提示词是纯文本不能被转义),单花括号写法彻底退场
- brainstorm-continue / brainstorm-continue-prototype 两份并进 brainstorm.md,
  用 {{#if issueNumber}} 分新建 / 续聊,续聊里用 {{#if prototypeFile}} 嵌原型段落,
  设置面板对应的两个 textarea 一并删掉
- 模板上下文 = 工单 state JSON 全量字段 + issueNumber / userRequest / nonce /
  planFile / prNumber / prRef,以后模板想用新字段不用改扩展代码
- 各会话入口共用一条 state 读取路径,读失败只记 warn 按空状态继续,不拦开会话
- 升级后首次启动把存量自定义提示词迁到双花括号并合并成一份,改写前把
  settings.json 备份成 settings.json.bak-<时间戳>,迁移动作全写日志
- 装 handlebars 时给 vsce 传递依赖的老 semver 加 trustPolicyExclude 豁免,
  否则 no-downgrade 策略卡住整个装包

Claude-Session: https://claude.ai/code/session_01Kgmb1LvrPmFX3NoUDB4zZC
This commit is contained in:
cruldra committed 2026-09-15 00:44:28 +08:00
1 parent efeac8c978
commit 7bac5c8b03
28 files changed
+853 -424

No files matched your search

+149
View File
@@ -0,0 +1,149 @@
import { readFileSync } from 'node:fs'
import * as path from 'node:path'
import { describe, expect, it } from 'vitest'
import {
composeBrainstormTemplate,
planPromptMigration,
PROMPT_TEMPLATE_VERSION,
splitIfBlock,
upgradePlaceholders,
} from './promptMigration'
const defaultBrainstorm = readFileSync(
path.resolve(__dirname, '../../prompts/brainstorm.md'),
'utf-8',
)
describe('upgradePlaceholders', () => {
it('已知占位符改写成双花括号', () => {
expect(upgradePlaceholders('实施 @{planFile} 的 #{issueNumber}')).toBe('实施 @{{planFile}} 的 #{{issueNumber}}')
})
it('已经是双花括号的不再加层', () => {
expect(upgradePlaceholders('{{issueNumber}}')).toBe('{{issueNumber}}')
})
it('名单外的花括号内容原样保留', () => {
expect(upgradePlaceholders('{foo} {planfile} {"a":1}')).toBe('{foo} {planfile} {"a":1}')
})
})
describe('composeBrainstormTemplate', () => {
it('两份正文拼成条件分支,原型段落嵌进 prototypeRef 的位置', () => {
const out = composeBrainstormTemplate({
newIssue: '新建 {{userRequest}}',
cont: '续聊 {{issueNumber}}\n\n{{prototypeRef}}\n\n收尾',
proto: '原型 {{prototypeFile}}\n- id:{{prototypeSessionId}}',
})
expect(out).toBe([
'{{#if issueNumber}}',
'续聊 {{issueNumber}}',
'',
'{{#if prototypeFile}}',
'原型 {{prototypeFile}}',
'{{#if prototypeSessionId}}',
'- id:{{prototypeSessionId}}',
'{{/if}}',
'{{/if}}',
'',
'收尾',
'{{else}}',
'新建 {{userRequest}}',
'{{/if}}',
'',
].join('\n'))
})
it('续聊正文里没有 prototypeRef 时原型段落接在末尾', () => {
const out = composeBrainstormTemplate({ newIssue: '新建', cont: '续聊', proto: '原型 {{prototypeFile}}' })
expect(out).toContain('续聊\n\n{{#if prototypeFile}}\n原型 {{prototypeFile}}\n{{/if}}')
})
it('没有原型段落时不留空的条件块', () => {
const out = composeBrainstormTemplate({ newIssue: '新建', cont: '续聊\n\n{{prototypeRef}}', proto: '' })
expect(out).not.toContain('prototypeFile')
expect(out).not.toContain('prototypeRef')
})
})
describe('splitIfBlock', () => {
it('只认第一层的 else,内层条件块不当分界', () => {
const branches = splitIfBlock(defaultBrainstorm, 'issueNumber')
expect(branches!.then).toContain('/ecc:grilling')
expect(branches!.then).toContain('{{#if prototypeFile}}')
expect(branches!.else).toContain('opencli spx issue create')
expect(branches!.else).not.toContain('{{else}}')
})
it('没有这个条件块时返回 null', () => {
expect(splitIfBlock('普通模板', 'issueNumber')).toBeNull()
})
})
describe('planPromptMigration', () => {
const legacy = {
brainstormPrompt: '建工单 {userRequest} nonce={nonce}',
brainstormContinuePrompt: '讨论 {issueNumber}\n\n{prototypeRef}\n\n收尾',
prototypeRefPrompt: '原型 {prototypeFile}\n- id:{prototypeSessionId}',
implementPlanPrompt: '实施 @{planFile} #{issueNumber}',
testPrompt: '验收 {issueNumber}({prRef})',
reviewPrompt: '审查 #{prNumber}',
prototypePrompt: '画原型 {issueNumber}',
webhookPort: 17421,
}
it('五份存量提示词全部换成双花括号,并合并成一份头脑风暴模板', () => {
const { blob, notes } = planPromptMigration(legacy, defaultBrainstorm)
expect(blob).not.toBeNull()
expect(blob!.implementPlanPrompt).toBe('实施 @{{planFile}} #{{issueNumber}}')
expect(blob!.testPrompt).toBe('验收 {{issueNumber}}({{prRef}})')
expect(blob!.reviewPrompt).toBe('审查 #{{prNumber}}')
expect(blob!.prototypePrompt).toBe('画原型 {{issueNumber}}')
expect(blob!.brainstormPrompt).toContain('{{#if issueNumber}}')
expect(blob!.brainstormPrompt).toContain('{{#if prototypeFile}}')
expect(blob!.brainstormPrompt).toContain('{{#if prototypeSessionId}}')
expect(blob!.brainstormPrompt).toContain('{{else}}')
expect(blob!.brainstormPrompt).toContain('建工单 {{userRequest}} nonce={{nonce}}')
expect(notes.some(n => n.level === 'warn')).toBe(false)
})
it('被删掉的两个字段迁移后不再留在 settings 里,其他设置不动', () => {
const { blob } = planPromptMigration(legacy, defaultBrainstorm)
expect(blob!.brainstormContinuePrompt).toBeUndefined()
expect(blob!.prototypeRefPrompt).toBeUndefined()
expect(blob!.webhookPort).toBe(17421)
expect(blob!.promptTemplateVersion).toBe(PROMPT_TEMPLATE_VERSION)
})
it('迁过一次就不再重复迁', () => {
const { blob } = planPromptMigration(legacy, defaultBrainstorm)
expect(planPromptMigration(blob!, defaultBrainstorm)).toEqual({ blob: null, notes: [] })
})
it('没存过任何提示词时什么都不做', () => {
expect(planPromptMigration({ webhookPort: 17421 }, defaultBrainstorm)).toEqual({ blob: null, notes: [] })
})
it('只改过新建工单提示词时,续聊分支用内置默认补齐', () => {
const { blob } = planPromptMigration(
{ brainstormPrompt: '建工单 {userRequest}', brainstormContinuePrompt: '' },
defaultBrainstorm,
)
const merged = String(blob!.brainstormPrompt)
expect(merged).toContain('建工单 {{userRequest}}')
expect(merged).toContain('/ecc:grilling 讨论下 {{issueNumber}} 号工单')
// 默认续聊分支自带原型段落,别再插第二份
expect(merged.match(/\{\{#if prototypeFile\}\}/g)).toHaveLength(1)
})
it('只改过续聊提示词时,新建分支和原型段落用内置默认补齐', () => {
const { blob } = planPromptMigration(
{ brainstormContinuePrompt: '讨论 {issueNumber}\n\n{prototypeRef}' },
defaultBrainstorm,
)
const merged = String(blob!.brainstormPrompt)
expect(merged).toContain('讨论 {{issueNumber}}')
expect(merged).toContain('opencli spx issue create')
expect(merged).toContain('工单已有原型插件 {{prototypeFile}}')
})
})
+190
View File
@@ -0,0 +1,190 @@
/**
* 存量自定义提示词的一次性迁移:单花括号 → Handlebars 双花括号,
* 并把「续聊」「续聊·原型段落」两份模板并进头脑风暴模板的条件分支里。
*
* 纯函数,不碰文件系统 / vscode——落盘与日志由 `store.ts` 的
* `migratePromptTemplates` 负责。
*/
/** 设置里的模板引擎版本;低于它就跑一次迁移。 */
export const PROMPT_TEMPLATE_VERSION = 2
/**
* 只重写这些名字的占位符。正文里其他花括号内容(比如 shell 的 `${x}`、
* 示例 JSON)保持原样,免得迁移把用户写的东西改坏。
*/
const KNOWN_PLACEHOLDERS = [
'userRequest',
'nonce',
'issueNumber',
'planFile',
'prNumber',
'prRef',
'prototypeFile',
'prototypeSessionId',
'prototypeRef',
] as const
/** 迁移后仍然存在的模板字段。 */
const PROMPT_FIELDS = [
'brainstormPrompt',
'implementPlanPrompt',
'reviewPrompt',
'testPrompt',
'prototypePrompt',
] as const
/** 已经并进 brainstormPrompt、不再单独存在的字段。 */
const MERGED_FIELDS = ['brainstormContinuePrompt', 'prototypeRefPrompt'] as const
export interface MigrationNote {
level: 'info' | 'warn'
message: string
}
export interface PromptMigrationPlan {
/** 要写回的完整 blob;null = 没什么可迁的,不写盘。 */
blob: Record<string, unknown> | null
notes: MigrationNote[]
}
/** `{foo}` → `{{foo}}`,只认已知占位符;已经是 `{{foo}}` 的不动。 */
export function upgradePlaceholders(text: string): string {
const names = KNOWN_PLACEHOLDERS.join('|')
return text.replace(new RegExp(`(?<!\\{)\\{(${names})\\}(?!\\})`, 'g'), '{{$1}}')
}
/**
* 把含 `{{name}}` 的整行包进 `{{#if name}}`。
*
* 旧引擎对原型段落的做法是「占位符取值为空就删掉整行」,只有原型会话 id
* 会真的为空;迁移后用条件块把这条语义原样搬过来。
*/
export function guardLinesWith(text: string, name: string): string {
const token = `{{${name}}}`
return text
.split('\n')
.map(line => line.includes(token) ? `{{#if ${name}}}\n${line}\n{{/if}}` : line)
.join('\n')
}
/**
* 从模板里切出 `{{#if cond}} … {{else}} … {{/if}}` 这一整块的两个分支。
*
* 迁移时要拿内置默认模板的「续聊」「新建工单」分支去补用户没存过的那一半,
* 所以得能把合并后的默认模板再拆开。按 `{{#if}}` / `{{/if}}` 记嵌套层数,
* 只认第一层的 `{{else}}`,嵌在里面的原型条件块不会被误当成分界。
*/
export function splitIfBlock(tpl: string, cond: string): { then: string, else: string } | null {
const lines = tpl.split('\n')
const start = lines.findIndex(line => line.trim() === `{{#if ${cond}}}`)
if (start < 0)
return null
let depth = 1
let elseAt = -1
for (let i = start + 1; i < lines.length; i++) {
const line = lines[i]
if (depth === 1 && line.trim() === '{{else}}')
elseAt = i
depth += (line.match(/\{\{#(?:if|unless)\s/g) ?? []).length
depth -= (line.match(/\{\{\/(?:if|unless)\}\}/g) ?? []).length
if (depth === 0) {
return {
then: lines.slice(start + 1, elseAt < 0 ? i : elseAt).join('\n').trim(),
else: elseAt < 0 ? '' : lines.slice(elseAt + 1, i).join('\n').trim(),
}
}
}
return null
}
/**
* 三份存量模板拼成一份:有工单号走续聊分支,没有走新建工单分支;
* 原型段落嵌在续聊分支里原来 `{{prototypeRef}}` 的位置(模板里没这个
* 占位符就接在末尾)。
*/
export function composeBrainstormTemplate(parts: { newIssue: string, cont: string, proto: string }): string {
const proto = parts.proto.trim()
const block = proto ? `{{#if prototypeFile}}\n${guardLinesWith(proto, 'prototypeSessionId')}\n{{/if}}` : ''
let cont = parts.cont
if (cont.includes('{{prototypeRef}}'))
cont = cont.split('{{prototypeRef}}').join(block)
else if (block)
cont = `${cont.trimEnd()}\n\n${block}`
return `{{#if issueNumber}}\n${cont.trim()}\n{{else}}\n${parts.newIssue.trim()}\n{{/if}}\n`
}
function storedText(stored: Record<string, unknown>, key: string): string {
const v = stored[key]
return typeof v === 'string' ? v : ''
}
/**
* 算出迁移后的 settings blob。`defaultBrainstorm` 是内置的合并版模板,用来补
* 用户没存过的那一半分支——比如只改过「新建工单」提示词的人,续聊分支直接用
* 内置默认,不然合并出来的模板会有一路是空的。
*/
export function planPromptMigration(
stored: Record<string, unknown>,
defaultBrainstorm: string,
): PromptMigrationPlan {
const version = stored.promptTemplateVersion
if (typeof version === 'number' && version >= PROMPT_TEMPLATE_VERSION)
return { blob: null, notes: [] }
const next: Record<string, unknown> = { ...stored }
const notes: MigrationNote[] = []
let changed = false
for (const field of [...PROMPT_FIELDS, ...MERGED_FIELDS]) {
const before = storedText(stored, field)
if (!before)
continue
const after = upgradePlaceholders(before)
if (after !== before) {
next[field] = after
changed = true
notes.push({ level: 'info', message: `${field}:占位符已改写成 {{双花括号}}` })
}
}
const storedNewIssue = storedText(next, 'brainstormPrompt')
const storedCont = storedText(next, 'brainstormContinuePrompt')
const storedProto = storedText(next, 'prototypeRefPrompt')
if (storedNewIssue || storedCont || storedProto) {
const branches = splitIfBlock(defaultBrainstorm, 'issueNumber')
if (!branches) {
notes.push({
level: 'warn',
message: '内置头脑风暴模板拆不出条件分支,存量的续聊 / 原型段落没能并进来,原文见备份文件',
})
}
else {
// 用户没存过续聊时,默认续聊分支里本来就带着原型段落,别再插一遍。
const proto = storedProto || (storedCont ? splitIfBlock(branches.then, 'prototypeFile')?.then ?? '' : '')
next.brainstormPrompt = composeBrainstormTemplate({
newIssue: storedNewIssue || branches.else,
cont: storedCont || branches.then,
proto,
})
changed = true
notes.push({
level: 'info',
message: `头脑风暴提示词已合并成一份条件模板(新建=${storedNewIssue ? '你的' : '内置'} / 续聊=${storedCont ? '你的' : '内置'} / 原型段落=${storedProto ? '你的' : '内置'})`,
})
}
}
for (const field of MERGED_FIELDS) {
if (field in next) {
delete next[field]
changed = true
}
}
if (!changed)
return { blob: null, notes: [] }
next.promptTemplateVersion = PROMPT_TEMPLATE_VERSION
return { blob: next, notes }
}
+59 -47
View File
@@ -12,11 +12,13 @@
*/
import type { ExtensionContext } from 'vscode'
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
import type { MigrationNote } from './promptMigration'
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { seedCommitProfilePath } from '../cc/commitProfile'
import { listClaudeProfiles } from '../cc/profiles'
import { normalizeReleasePrefix } from '../git/release'
import { planPromptMigration } from './promptMigration'
/**
* Fallback prompt strings used when the on-disk markdown can't be read.
@@ -24,27 +26,27 @@ import { normalizeReleasePrefix } from '../git/release'
* users can tweak them without rebuilding. These short fallbacks just keep
* the extension usable if the file is missing/corrupt.
*/
const FALLBACK_BRAINSTORM_PROMPT = `我现在有这样一个需求 {userRequest},你用 spx 命令创建 gitea 工单。工单 body 末尾必须包含 <!-- spx:nonce={nonce} -->。创建完工单立即停下汇报,不要擅自实施。`
const FALLBACK_BRAINSTORM_PROMPT = `{{#if issueNumber}}
/ecc:grilling 讨论下 {{issueNumber}} 号工单。
{{#if prototypeFile}}
工单已有原型插件 {{prototypeFile}},先读它;原型要改就 ListAgents 找到 issue-{{issueNumber}}-原型 会话{{#if prototypeSessionId}}(id {{prototypeSessionId}}){{/if}}并 SendMessage 给它,不要派子代理、不要自己动插件代码。
{{/if}}
达成共识后用 ecc:spec 写 .claude/specs/<stem>.md,我确认后用 ecc:plan 出 .claude/plans/<stem>.md,两个文件在 main 上提交并用 spx issue marker 同步 marker。不要 git checkout / 不要写代码 / 不要建 PR。
{{else}}
我现在有这样一个需求 {{userRequest}},你用 spx 命令创建 gitea 工单。工单 body 末尾必须包含 <!-- spx:nonce={{nonce}} -->。创建完工单立即停下汇报,不要擅自实施。
{{/if}}`
const FALLBACK_BRAINSTORM_CONTINUE_PROMPT = `/ecc:grilling 讨论下 {issueNumber} 号工单。
const FALLBACK_IMPLEMENT_PLAN_PROMPT = `/ecc:tdd @{{planFile}} 按计划实施,编码派子代理,发起 PR 时在 body 中包含 "Closes #{{issueNumber}}"。严禁合并 PR。`
{prototypeRef}
const FALLBACK_TEST_PROMPT = `/pr-acceptance-testing 验收 {{issueNumber}} 号工单({{prRef}})。验收目标以工单正文为准,不要再问验收哪个。`
达成共识后用 ecc:spec 写 .claude/specs/<stem>.md,我确认后用 ecc:plan 出 .claude/plans/<stem>.md,两个文件在 main 上提交并用 spx issue marker 同步 marker。不要 git checkout / 不要写代码 / 不要建 PR。`
const FALLBACK_REVIEW_PROMPT = `用 tea 拿到 #{{prNumber}} PR,diff 交给 ecc:code-reviewer 子代理审查。审查意见用 spx pr review-comment 发评论,body 第一行写 <!-- spx:review=1 -->。`
const FALLBACK_IMPLEMENT_PLAN_PROMPT = `/ecc:tdd @{planFile} 按计划实施,编码派子代理,发起 PR 时在 body 中包含 "Closes #{issueNumber}"。严禁合并 PR。`
const FALLBACK_PROTOTYPE_PROMPT = `/ecc:figma-plugin-prototyping 给 {{issueNumber}} 号工单画原型:先读工单正文锁定要画的界面和状态,再写本地 Figma 插件把画板和点击流做出来。画完汇报插件路径和怎么导入,不要改业务代码。
const FALLBACK_TEST_PROMPT = `/pr-acceptance-testing 验收 {issueNumber} 号工单({prRef})。验收目标以工单正文为准,不要再问验收哪个。`
画完用 \`opencli spx issue marker --issue {{issueNumber}} --type prototype --value <manifest.json 的仓库相对路径>\` 把原型插件挂到工单上,再汇报。`
const FALLBACK_PROTOTYPE_REF_PROMPT = `工单已有原型插件 {prototypeFile},先读它。原型要改就 ListAgents 找到 issue-{issueNumber}-原型 会话(id {prototypeSessionId})并 SendMessage 给它,不要派子代理、不要自己动插件代码。`
const FALLBACK_REVIEW_PROMPT = `用 tea 拿到 #{prNumber} PR,diff 交给 ecc:code-reviewer 子代理审查。审查意见用 spx pr review-comment 发评论,body 第一行写 <!-- spx:review=1 -->。`
const FALLBACK_PROTOTYPE_PROMPT = `/ecc:figma-plugin-prototyping 给 {issueNumber} 号工单画原型:先读工单正文锁定要画的界面和状态,再写本地 Figma 插件把画板和点击流做出来。画完汇报插件路径和怎么导入,不要改业务代码。
画完用 \`opencli spx issue marker --issue {issueNumber} --type prototype --value <manifest.json 的仓库相对路径>\` 把原型插件挂到工单上,再汇报。`
type PromptName = 'brainstorm' | 'brainstorm-continue' | 'brainstorm-continue-prototype' | 'implement-plan' | 'review' | 'test' | 'prototype'
type PromptName = 'brainstorm' | 'implement-plan' | 'review' | 'test' | 'prototype'
/**
* Read a default prompt template from `${extensionPath}/prompts/${name}.md`.
@@ -63,8 +65,6 @@ export function readDefaultPrompt(extensionPath: string, name: PromptName): stri
console.warn(`[spx] 读 ${filePath} 失败,使用内联 fallback:`, err)
switch (name) {
case 'brainstorm': return FALLBACK_BRAINSTORM_PROMPT
case 'brainstorm-continue': return FALLBACK_BRAINSTORM_CONTINUE_PROMPT
case 'brainstorm-continue-prototype': return FALLBACK_PROTOTYPE_REF_PROMPT
case 'implement-plan': return FALLBACK_IMPLEMENT_PLAN_PROMPT
case 'review': return FALLBACK_REVIEW_PROMPT
case 'test': return FALLBACK_TEST_PROMPT
@@ -85,32 +85,20 @@ export interface Settings {
*/
webhookSecret: string
/**
* Prompt template for the brainstorming flow (issue creation + ongoing
* session conventions for spec/plan body annotations). `{userRequest}`
* and `{nonce}` placeholders.
* 头脑风暴模板(Handlebars)。`{{#if issueNumber}}` 走续聊已有工单,
* `{{else}}` 走新建工单(用 `{{userRequest}}` / `{{nonce}}`);续聊分支里
* `{{#if prototypeFile}}` 嵌原型段落。
*/
brainstormPrompt: string
/**
* Prompt template for continuing brainstorm on an already-existing issue
* (when the user later wants to discuss / write spec/plan in the panel).
* 占位符 `{issueNumber}` / `{prototypeRef}`(后者由扩展按工单有无原型插件填,
* 无则连同所在段落一起删掉)。
*/
brainstormContinuePrompt: string
/**
* 续聊提示词里 `{prototypeRef}` 那段的正文;只在工单挂了原型插件时填进去。
* 占位符 {prototypeFile} / {issueNumber} / {prototypeSessionId}。
*/
prototypeRefPrompt: string
/** Prompt template for the implement-plan flow. `{planFile}` placeholder. */
/** 实施模板;常用 `{{planFile}}` / `{{issueNumber}}`。 */
implementPlanPrompt: string
/** Whether to automatically run `codex exec review` when a PR opens. */
autoReview: boolean
/** Prompt template for the auto-review flow. `{prNumber}` placeholder. */
/** 审查模板;常用 `{{prNumber}}`。 */
reviewPrompt: string
/** 测试会话首个提示词模板;占位符 {issueNumber} / {prRef}。 */
/** 测试会话首个提示词模板;常用 `{{issueNumber}}` / `{{prRef}}`。 */
testPrompt: string
/** 原型会话首个提示词模板;占位符 {issueNumber}。 */
/** 原型会话首个提示词模板;常用 `{{issueNumber}}`。 */
prototypePrompt: string
/** Day-to-day development branch (e.g. `main`). Jenkins doesn't watch it. */
devBranch: string
@@ -289,8 +277,6 @@ function defaults(ctx: ExtensionContext): Settings {
webhookPort: DEFAULT_WEBHOOK_PORT,
webhookSecret: '',
brainstormPrompt: readDefaultPrompt(ctx.extensionPath, 'brainstorm'),
brainstormContinuePrompt: readDefaultPrompt(ctx.extensionPath, 'brainstorm-continue'),
prototypeRefPrompt: readDefaultPrompt(ctx.extensionPath, 'brainstorm-continue-prototype'),
implementPlanPrompt: readDefaultPrompt(ctx.extensionPath, 'implement-plan'),
autoReview: true,
reviewPrompt: readDefaultPrompt(ctx.extensionPath, 'review'),
@@ -340,12 +326,6 @@ export function getSettings(ctx: ExtensionContext): Settings {
const brainstormPrompt = typeof stored.brainstormPrompt === 'string' && stored.brainstormPrompt.length > 0
? stored.brainstormPrompt
: base.brainstormPrompt
const brainstormContinuePrompt = typeof stored.brainstormContinuePrompt === 'string' && stored.brainstormContinuePrompt.length > 0
? stored.brainstormContinuePrompt
: base.brainstormContinuePrompt
const prototypeRefPrompt = typeof stored.prototypeRefPrompt === 'string' && stored.prototypeRefPrompt.length > 0
? stored.prototypeRefPrompt
: base.prototypeRefPrompt
const implementPlanPrompt = typeof stored.implementPlanPrompt === 'string' && stored.implementPlanPrompt.length > 0
? stored.implementPlanPrompt
: base.implementPlanPrompt
@@ -463,8 +443,6 @@ export function getSettings(ctx: ExtensionContext): Settings {
webhookPort,
webhookSecret,
brainstormPrompt,
brainstormContinuePrompt,
prototypeRefPrompt,
implementPlanPrompt,
autoReview,
reviewPrompt,
@@ -527,6 +505,40 @@ export async function saveSettings(ctx: ExtensionContext, next: Partial<Settings
writeStoredSettings(ctx, merged)
}
/**
* 一次性把存量自定义提示词迁到 Handlebars:单花括号改写成双花括号,续聊 /
* 原型段落并进头脑风暴模板。写盘前先把 settings.json 备份一份。
*
* 返回日志条目而不是自己写 logger——logger 依赖 vscode 运行时,store 要能在
* 单测里直接 import。
*/
export function migratePromptTemplates(ctx: ExtensionContext): MigrationNote[] {
const { blob, notes } = planPromptMigration(
readStoredSettings(ctx) as Record<string, unknown>,
readDefaultPrompt(ctx.extensionPath, 'brainstorm'),
)
if (!blob)
return []
const out: MigrationNote[] = []
const file = settingsFilePath(ctx)
if (existsSync(file)) {
const backup = `${file}.bak-${new Date().toISOString().replace(/[:.]/g, '-')}`
try {
copyFileSync(file, backup)
out.push({ level: 'info', message: `提示词迁移前备份设置:${backup}` })
}
catch (err) {
return [{
level: 'warn',
message: `settings.json 备份失败,本次不迁移提示词:${err instanceof Error ? err.message : String(err)}`,
}]
}
}
writeStoredSettings(ctx, blob as Partial<Settings>)
out.push(...notes, { level: 'info', message: '提示词模板已迁到 Handlebars' })
return out
}
/**
* 启动时:把旧 memento 迁到文件;commit / 改动说明 profile 缺 key 且能 seed 则写盘。
* 空串视为用户主动清空,不覆盖。