✨ feat(vscode): 审查通过写入工单状态并在卡片上区分

This commit is contained in:
cruldra committed 2026-10-03 03:22:20 +08:00
1 parent ac1b7ed641
commit 177e75910e
21 files changed
+203 -10

No files matched your search

@@ -0,0 +1,2 @@
/** 改「审查结论」之前的默认审查提示词。设置里若原样存着这份,整段换成新文件。 */
export const PREVIOUS_DEFAULT_REVIEW_PROMPT = '审查这个仓库的 PR #{{prNumber}}。\n\n用 `tea pulls {{prNumber}}` 看 PR 元信息(标题/描述/分支)。看代码差异用 git(当前目录就是 PR 分支的 worktree):先 `git fetch origin main`,再 `git diff origin/main...HEAD`(概览可加 --stat)。注意 tea 没有 `diff` 子命令,不要尝试 `tea pulls diff`。\n\n## 审查方式\n\nECC 的 `/ecc:review-pr` 和 `/ecc:code-review` 的 PR 模式都走 `gh`,对 Gitea 不可用,不要调。直接把上面的 diff 喂给 ECC 的审查子代理,按改动涉及的语言挑:\n\n- `ecc:code-reviewer`(必跑)\n- `ecc:silent-failure-hunter`、`ecc:pr-test-analyzer`(必跑)\n- 语言专项:TypeScript 用 `ecc:typescript-reviewer`,Python 用 `ecc:python-reviewer`,React 用 `ecc:react-reviewer`,其余同理\n\n汇总时去重、按严重程度排序,只报置信度高的问题。\n\n## 提交审查意见\n\n把审查意见(markdown 格式)写到 `/tmp/review-{{prNumber}}.md`,调 spx:\n\n```\nopencli spx pr review-comment --pr {{prNumber}} --body-file /tmp/review-{{prNumber}}.md\n```\n\n审查结束以后,删除所有审查过程中创建的临时 worktree(`git worktree remove --force <路径>`),你当前所在的 worktree 除外。\n\n## 严禁\n\n- 不要在审查意见里建议"合并 PR"或"merge"\n- 不要执行 `tea pulls merge` 或任何合并命令\n- 不要 push 到 main / dev 分支\n\n合并权完全在用户手上,你的工作只是指出问题或确认通过。\n'
@@ -1,9 +1,11 @@
import { readFileSync } from 'node:fs'
import * as path from 'node:path'
import { describe, expect, it } from 'vitest'
import { PREVIOUS_DEFAULT_REVIEW_PROMPT } from './previousReviewPrompt'
import {
composeBrainstormTemplate,
planPromptMigration,
planReviewPromptMigration,
PROMPT_TEMPLATE_VERSION,
splitIfBlock,
upgradePlaceholders,
@@ -147,3 +149,43 @@ describe('planPromptMigration', () => {
expect(merged).toContain('工单已有原型插件 {{prototypeFile}}')
})
})
describe('planReviewPromptMigration', () => {
const nextDefault = readFileSync(
path.resolve(__dirname, '../../prompts/review.md'),
'utf-8',
)
it('和改之前的默认一样时,整段换成现在的文件', () => {
const result = planReviewPromptMigration(
PREVIOUS_DEFAULT_REVIEW_PROMPT,
PREVIOUS_DEFAULT_REVIEW_PROMPT,
nextDefault,
)
expect(result).not.toBeNull()
expect(result!.prompt).toBe(nextDefault)
expect(result!.prompt).toContain('{"reviewPassed":true}')
expect(result!.notes[0]?.message).toContain('旧版默认')
})
it('用户改过、还没有 reviewPassed 时,只把审查结论接到末尾', () => {
const custom = '先看 diff,再写意见。\n不要跑无关命令。\n'
const result = planReviewPromptMigration(custom, PREVIOUS_DEFAULT_REVIEW_PROMPT, nextDefault)
expect(result).not.toBeNull()
expect(result!.prompt.startsWith('先看 diff,再写意见。\n不要跑无关命令。\n\n## 审查结论\n')).toBe(true)
expect(result!.prompt).toContain('reviewPassed')
expect(result!.prompt).toContain('小建议不算不通过')
expect(result!.prompt).not.toContain('## 严禁')
expect(result!.prompt).not.toContain('tea pulls')
})
it('已经写了 reviewPassed 的不改', () => {
const custom = '我自己的审查。\nreviewPassed 我写过了。\n'
expect(planReviewPromptMigration(custom, PREVIOUS_DEFAULT_REVIEW_PROMPT, nextDefault)).toBeNull()
})
it('没存审查提示词时不改', () => {
expect(planReviewPromptMigration('', PREVIOUS_DEFAULT_REVIEW_PROMPT, nextDefault)).toBeNull()
expect(planReviewPromptMigration(undefined, PREVIOUS_DEFAULT_REVIEW_PROMPT, nextDefault)).toBeNull()
})
})
+49
View File
@@ -188,3 +188,52 @@ export function planPromptMigration(
next.promptTemplateVersion = PROMPT_TEMPLATE_VERSION
return { blob: next, notes }
}
function normalizePrompt(text: string): string {
return text.replace(/\r\n/g, '\n').trim()
}
/** 新版审查提示词里「审查结论」那一节,到下一个同级标题为止。 */
export function reviewConclusionSection(nextDefault: string): string | null {
const normalized = nextDefault.replace(/\r\n/g, '\n')
const heading = '## 审查结论'
const start = normalized.indexOf(heading)
if (start < 0)
return null
const rest = normalized.slice(start)
const relative = rest.slice(heading.length).search(/\n## /)
const section = relative < 0 ? rest : rest.slice(0, heading.length + relative)
const trimmed = section.trim()
return trimmed.length > 0 ? trimmed : null
}
/**
* 设置里已经存了一份审查提示词时:和改之前的默认一样就整段换成新文件;
* 用户改过、且还没有 reviewPassed,只把「审查结论」接到末尾。
* 返回 null 表示不用写盘。
*/
export function planReviewPromptMigration(
stored: unknown,
previousDefault: string,
nextDefault: string,
): { prompt: string, notes: MigrationNote[] } | null {
if (typeof stored !== 'string' || stored.length === 0)
return null
if (normalizePrompt(stored) === normalizePrompt(previousDefault)) {
if (normalizePrompt(nextDefault) === normalizePrompt(previousDefault))
return null
return {
prompt: nextDefault,
notes: [{ level: 'info', message: '审查提示词与旧版默认一致,已换成带审查结论的新版' }],
}
}
if (stored.includes('reviewPassed'))
return null
const section = reviewConclusionSection(nextDefault)
if (!section)
return null
return {
prompt: `${stored.replace(/\s+$/, '')}\n\n${section}\n`,
notes: [{ level: 'info', message: '审查提示词末尾已追加「审查结论」,其余段落未改' }],
}
}
+18 -4
View File
@@ -18,7 +18,8 @@ import path from 'node:path'
import { seedCommitProfilePath } from '../cc/commitProfile'
import { listClaudeProfiles } from '../cc/profiles'
import { normalizeReleasePrefix } from '../git/release'
import { planPromptMigration } from './promptMigration'
import { PREVIOUS_DEFAULT_REVIEW_PROMPT } from './previousReviewPrompt'
import { planPromptMigration, planReviewPromptMigration } from './promptMigration'
/**
* Fallback prompt strings used when the on-disk markdown can't be read.
@@ -513,10 +514,19 @@ export async function saveSettings(ctx: ExtensionContext, next: Partial<Settings
* 单测里直接 import。
*/
export function migratePromptTemplates(ctx: ExtensionContext): MigrationNote[] {
const { blob, notes } = planPromptMigration(
readStoredSettings(ctx) as Record<string, unknown>,
const stored = readStoredSettings(ctx) as Record<string, unknown>
const planned = planPromptMigration(
stored,
readDefaultPrompt(ctx.extensionPath, 'brainstorm'),
)
const review = planReviewPromptMigration(
planned.blob?.reviewPrompt ?? stored.reviewPrompt,
PREVIOUS_DEFAULT_REVIEW_PROMPT,
readDefaultPrompt(ctx.extensionPath, 'review'),
)
const blob = review
? { ...(planned.blob ?? stored), reviewPrompt: review.prompt }
: planned.blob
if (!blob)
return []
const out: MigrationNote[] = []
@@ -535,7 +545,11 @@ export function migratePromptTemplates(ctx: ExtensionContext): MigrationNote[] {
}
}
writeStoredSettings(ctx, blob as Partial<Settings>)
out.push(...notes, { level: 'info', message: '提示词模板已迁到 Handlebars' })
out.push(...planned.notes)
if (review)
out.push(...review.notes)
if (planned.blob)
out.push({ level: 'info', message: '提示词模板已迁到 Handlebars' })
return out
}