✨ feat(vscode): 审查通过写入工单状态并在卡片上区分
This commit is contained in:
1 parent
ac1b7ed641
commit
177e75910e
21 files changed
+203
-10
No files matched your search
@@ -425,6 +425,7 @@ var knownStateFields = map[string]struct{}{
|
|||||||
"implementStatus": {},
|
"implementStatus": {},
|
||||||
"color": {},
|
"color": {},
|
||||||
"autoReview": {},
|
"autoReview": {},
|
||||||
|
"reviewPassed": {},
|
||||||
"handoffAttachmentId": {},
|
"handoffAttachmentId": {},
|
||||||
"handoffFrom": {},
|
"handoffFrom": {},
|
||||||
"handoffRequestFrom": {},
|
"handoffRequestFrom": {},
|
||||||
|
|||||||
@@ -118,6 +118,10 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "本工单是否启用自动审查(覆盖全局 autoReview 设置)。"
|
"description": "本工单是否启用自动审查(覆盖全局 autoReview 设置)。"
|
||||||
},
|
},
|
||||||
|
"reviewPassed": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "最近一次审查是否认为可以过。true 表示通过;false 或不写表示还在审,或审出了必须改的问题。PR 有新提交时合并成 false。不改变看板列。"
|
||||||
|
},
|
||||||
"handoffAttachmentId": {
|
"handoffAttachmentId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "移交会话包在 Gitea 的附件 id;存在即「待接管」,接管完成后清空;清空时写空串。"
|
"description": "移交会话包在 Gitea 的附件 id;存在即「待接管」,接管完成后清空;清空时写空串。"
|
||||||
|
|||||||
@@ -87,13 +87,23 @@ func TestValidateAcceptsFullState(t *testing.T) {
|
|||||||
"worktreePath":"/home/x/Sources/worktree/proj/slug",
|
"worktreePath":"/home/x/Sources/worktree/proj/slug",
|
||||||
"implementStatus":"done",
|
"implementStatus":"done",
|
||||||
"color":"terminal.ansiBlue",
|
"color":"terminal.ansiBlue",
|
||||||
"autoReview":true
|
"autoReview":true,
|
||||||
|
"reviewPassed":true
|
||||||
}`
|
}`
|
||||||
if err := Validate([]byte(full)); err != nil {
|
if err := Validate([]byte(full)); err != nil {
|
||||||
t.Fatalf("完整合法 state 应当通过校验,得到: %v", err)
|
t.Fatalf("完整合法 state 应当通过校验,得到: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateAcceptsReviewPassed(t *testing.T) {
|
||||||
|
if err := Validate([]byte(`{"reviewPassed":true}`)); err != nil {
|
||||||
|
t.Fatalf("reviewPassed=true 应当通过校验,得到: %v", err)
|
||||||
|
}
|
||||||
|
if err := Validate([]byte(`{"reviewPassed":false}`)); err != nil {
|
||||||
|
t.Fatalf("reviewPassed=false 应当通过校验,得到: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateRejectsBadPrototypeFile(t *testing.T) {
|
func TestValidateRejectsBadPrototypeFile(t *testing.T) {
|
||||||
err := Validate([]byte(`{"prototypeFile":"docs/prototype/login/figma-plugin/manifest.md"}`))
|
err := Validate([]byte(`{"prototypeFile":"docs/prototype/login/figma-plugin/manifest.md"}`))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -22,6 +22,20 @@ opencli spx pr review-comment --pr {{prNumber}} --body-file /tmp/review-{{prNumb
|
|||||||
|
|
||||||
审查结束以后,删除所有审查过程中创建的临时 worktree(`git worktree remove --force <路径>`),你当前所在的 worktree 除外。
|
审查结束以后,删除所有审查过程中创建的临时 worktree(`git worktree remove --force <路径>`),你当前所在的 worktree 除外。
|
||||||
|
|
||||||
|
## 审查结论
|
||||||
|
|
||||||
|
没有必须改的问题才算通过。小建议不算不通过。
|
||||||
|
|
||||||
|
通过:
|
||||||
|
|
||||||
|
```
|
||||||
|
opencli spx issue state merge --issue {{issueNumber}} --state-json '{"reviewPassed":true}'
|
||||||
|
```
|
||||||
|
|
||||||
|
有必须改的问题:同一条命令,把 `reviewPassed` 写成 `false`。
|
||||||
|
|
||||||
|
这条合并发生在审查评论发出之后。不要建议合并 PR,不要执行合并。
|
||||||
|
|
||||||
## 严禁
|
## 严禁
|
||||||
|
|
||||||
- 不要在审查意见里建议"合并 PR"或"merge"
|
- 不要在审查意见里建议"合并 PR"或"merge"
|
||||||
|
|||||||
@@ -118,6 +118,10 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "本工单是否启用自动审查(覆盖全局 autoReview 设置)。"
|
"description": "本工单是否启用自动审查(覆盖全局 autoReview 设置)。"
|
||||||
},
|
},
|
||||||
|
"reviewPassed": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "最近一次审查是否认为可以过。true 表示通过;false 或不写表示还在审,或审出了必须改的问题。PR 有新提交时合并成 false。不改变看板列。"
|
||||||
|
},
|
||||||
"handoffAttachmentId": {
|
"handoffAttachmentId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "移交会话包在 Gitea 的附件 id;存在即「待接管」,接管完成后清空;清空时写空串。"
|
"description": "移交会话包在 Gitea 的附件 id;存在即「待接管」,接管完成后清空;清空时写空串。"
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ describe('systemCommentText', () => {
|
|||||||
expect(systemCommentText('{"column":"review","color":"terminal.ansiBlue"}')).toBe('更新了工单状态,放到审查')
|
expect(systemCommentText('{"column":"review","color":"terminal.ansiBlue"}')).toBe('更新了工单状态,放到审查')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('审查是否通过不单写一句', () => {
|
||||||
|
expect(systemCommentText('{"reviewPassed":true}')).toBe('更新了工单状态')
|
||||||
|
expect(systemCommentText('{"column":"review","reviewPassed":false}')).toBe('更新了工单状态,放到审查')
|
||||||
|
})
|
||||||
|
|
||||||
it('普通评论不是系统消息', () => {
|
it('普通评论不是系统消息', () => {
|
||||||
expect(systemCommentText('这个方案可以')).toBeUndefined()
|
expect(systemCommentText('这个方案可以')).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const STATE_KEYS = new Set([
|
|||||||
'worktreePath',
|
'worktreePath',
|
||||||
'color',
|
'color',
|
||||||
'autoReview',
|
'autoReview',
|
||||||
|
'reviewPassed',
|
||||||
])
|
])
|
||||||
|
|
||||||
/** 插件自己写的评论。返回系统气泡文案;普通讨论返回 undefined。 */
|
/** 插件自己写的评论。返回系统气泡文案;普通讨论返回 undefined。 */
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ function parseColumnFromComments(comments: GiteaComment[]): {
|
|||||||
prototypeSessionId?: string
|
prototypeSessionId?: string
|
||||||
color?: string
|
color?: string
|
||||||
autoReview?: boolean
|
autoReview?: boolean
|
||||||
|
reviewPassed?: boolean
|
||||||
handoffAttachmentId?: string
|
handoffAttachmentId?: string
|
||||||
handoffFrom?: string
|
handoffFrom?: string
|
||||||
handoffRequestFrom?: string
|
handoffRequestFrom?: string
|
||||||
@@ -150,6 +151,7 @@ function parseColumnFromComments(comments: GiteaComment[]): {
|
|||||||
prototypeSessionId?: unknown
|
prototypeSessionId?: unknown
|
||||||
color?: unknown
|
color?: unknown
|
||||||
autoReview?: unknown
|
autoReview?: unknown
|
||||||
|
reviewPassed?: unknown
|
||||||
handoffAttachmentId?: unknown
|
handoffAttachmentId?: unknown
|
||||||
handoffFrom?: unknown
|
handoffFrom?: unknown
|
||||||
handoffRequestFrom?: unknown
|
handoffRequestFrom?: unknown
|
||||||
@@ -209,6 +211,7 @@ function parseColumnFromComments(comments: GiteaComment[]): {
|
|||||||
? obj.color
|
? obj.color
|
||||||
: undefined
|
: undefined
|
||||||
const autoReview = typeof obj.autoReview === 'boolean' ? obj.autoReview : undefined
|
const autoReview = typeof obj.autoReview === 'boolean' ? obj.autoReview : undefined
|
||||||
|
const reviewPassed = typeof obj.reviewPassed === 'boolean' ? obj.reviewPassed : undefined
|
||||||
const handoffAttachmentId = typeof obj.handoffAttachmentId === 'string' && obj.handoffAttachmentId.length > 0
|
const handoffAttachmentId = typeof obj.handoffAttachmentId === 'string' && obj.handoffAttachmentId.length > 0
|
||||||
? obj.handoffAttachmentId
|
? obj.handoffAttachmentId
|
||||||
: undefined
|
: undefined
|
||||||
@@ -244,6 +247,7 @@ function parseColumnFromComments(comments: GiteaComment[]): {
|
|||||||
prototypeSessionId,
|
prototypeSessionId,
|
||||||
color,
|
color,
|
||||||
autoReview,
|
autoReview,
|
||||||
|
reviewPassed,
|
||||||
handoffAttachmentId,
|
handoffAttachmentId,
|
||||||
handoffFrom,
|
handoffFrom,
|
||||||
handoffRequestFrom,
|
handoffRequestFrom,
|
||||||
@@ -390,6 +394,7 @@ async function buildIssue(opts: {
|
|||||||
prototypeSessionId,
|
prototypeSessionId,
|
||||||
color,
|
color,
|
||||||
autoReview,
|
autoReview,
|
||||||
|
reviewPassed,
|
||||||
handoffAttachmentId,
|
handoffAttachmentId,
|
||||||
handoffFrom,
|
handoffFrom,
|
||||||
handoffRequestFrom,
|
handoffRequestFrom,
|
||||||
@@ -450,6 +455,7 @@ async function buildIssue(opts: {
|
|||||||
...(prerequisite !== undefined ? { prerequisite } : {}),
|
...(prerequisite !== undefined ? { prerequisite } : {}),
|
||||||
...(color ? { color } : {}),
|
...(color ? { color } : {}),
|
||||||
...(autoReview !== undefined ? { autoReview } : {}),
|
...(autoReview !== undefined ? { autoReview } : {}),
|
||||||
|
...(reviewPassed !== undefined ? { reviewPassed } : {}),
|
||||||
...(handoffAttachmentId ? { handoffAttachmentId } : {}),
|
...(handoffAttachmentId ? { handoffAttachmentId } : {}),
|
||||||
...(handoffFrom ? { handoffFrom } : {}),
|
...(handoffFrom ? { handoffFrom } : {}),
|
||||||
...(handoffRequestFrom ? { handoffRequestFrom } : {}),
|
...(handoffRequestFrom ? { handoffRequestFrom } : {}),
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { listIssueComments, postIssueComment } from './api'
|
|||||||
* 普通文本评论、审查评论、Gitea 自动关联评论都不含这些字段,因此从尾往前
|
* 普通文本评论、审查评论、Gitea 自动关联评论都不含这些字段,因此从尾往前
|
||||||
* 扫描时会被跳过,避免它们插队后下一次 merge 从空状态开始丢历史。
|
* 扫描时会被跳过,避免它们插队后下一次 merge 从空状态开始丢历史。
|
||||||
*/
|
*/
|
||||||
const KNOWN_STATE_FIELDS = ['column', 'sessionId', 'implementSessionId', 'reviewSessionId', 'testSessionId', 'prototypeSessionId', 'profilePath', 'brainstormProfilePath', 'testProfilePath', 'prototypeProfilePath', 'specFile', 'planFile', 'prototypeFile', 'prDiffFile', 'pr', 'prMerged', 'prMergedAt', 'branch', 'worktreePath', 'implementStatus', 'color', 'autoReview', 'handoffAttachmentId', 'handoffFrom', 'handoffRequestFrom', 'handoffRequestError'] as const
|
const KNOWN_STATE_FIELDS = ['column', 'sessionId', 'implementSessionId', 'reviewSessionId', 'testSessionId', 'prototypeSessionId', 'profilePath', 'brainstormProfilePath', 'testProfilePath', 'prototypeProfilePath', 'specFile', 'planFile', 'prototypeFile', 'prDiffFile', 'pr', 'prMerged', 'prMergedAt', 'branch', 'worktreePath', 'implementStatus', 'color', 'autoReview', 'reviewPassed', 'handoffAttachmentId', 'handoffFrom', 'handoffRequestFrom', 'handoffRequestError'] as const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* body 能 parse 成 object 且含至少一个已知 state 字段 → 认为是 state JSON。
|
* body 能 parse 成 object 且含至少一个已知 state 字段 → 认为是 state JSON。
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ export interface Issue {
|
|||||||
* in the issue's state-JSON comment so it survives reloads.
|
* in the issue's state-JSON comment so it survives reloads.
|
||||||
*/
|
*/
|
||||||
autoReview?: boolean
|
autoReview?: boolean
|
||||||
|
/** 最近一次审查是否通过。只影响审查列卡片上的小字,不改列。 */
|
||||||
|
reviewPassed?: boolean
|
||||||
/** Gitea assignee login 列表;接管按钮只对 assignee 含当前登录者的工单显示。 */
|
/** Gitea assignee login 列表;接管按钮只对 assignee 含当前登录者的工单显示。 */
|
||||||
assignees?: string[]
|
assignees?: string[]
|
||||||
/** Gitea 工单创建者 login;无 assignee 的工单靠它判定归属与卡片头像。 */
|
/** Gitea 工单创建者 login;无 assignee 的工单靠它判定归属与卡片头像。 */
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export type ExtensionToWebview
|
|||||||
| { type: 'issues/loaded' }
|
| { type: 'issues/loaded' }
|
||||||
| { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean, me?: string, avatars?: Record<string, string> }
|
| { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean, me?: string, avatars?: Record<string, string> }
|
||||||
| { type: 'issues/error', message: string }
|
| { type: 'issues/error', message: string }
|
||||||
| { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prototypeFile?: string, prDiffFile?: string | null, sessionId?: string | null, implementSessionId?: string | null, reviewSessionId?: string | null, missingSessionIds?: string[], testSessionId?: string | null, prototypeSessionId?: string | null, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed' | null, column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string | null, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, prototypeTabOpen?: boolean, profilePath?: string | null, brainstormProfilePath?: string | null, testProfilePath?: string | null, prototypeProfilePath?: string | null, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null, handoffRequestFrom?: string | null, handoffRequestError?: string | null } }
|
| { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, reviewPassed?: boolean, specFile?: string, planFile?: string, prototypeFile?: string, prDiffFile?: string | null, sessionId?: string | null, implementSessionId?: string | null, reviewSessionId?: string | null, missingSessionIds?: string[], testSessionId?: string | null, prototypeSessionId?: string | null, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed' | null, column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string | null, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, prototypeTabOpen?: boolean, profilePath?: string | null, brainstormProfilePath?: string | null, testProfilePath?: string | null, prototypeProfilePath?: string | null, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null, handoffRequestFrom?: string | null, handoffRequestError?: string | null } }
|
||||||
/** 某个会话 id 的开 tab 请求已处理完(开成、复用或被拒都算),webview 据此停掉行内 spinner。 */
|
/** 某个会话 id 的开 tab 请求已处理完(开成、复用或被拒都算),webview 据此停掉行内 spinner。 */
|
||||||
| { type: 'session/tab-settled', sessionId: string }
|
| { type: 'session/tab-settled', sessionId: string }
|
||||||
| { type: 'issue/pr-diff-summary-done', issueNumber: number }
|
| { type: 'issue/pr-diff-summary-done', issueNumber: number }
|
||||||
|
|||||||
@@ -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 { readFileSync } from 'node:fs'
|
||||||
import * as path from 'node:path'
|
import * as path from 'node:path'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { PREVIOUS_DEFAULT_REVIEW_PROMPT } from './previousReviewPrompt'
|
||||||
import {
|
import {
|
||||||
composeBrainstormTemplate,
|
composeBrainstormTemplate,
|
||||||
planPromptMigration,
|
planPromptMigration,
|
||||||
|
planReviewPromptMigration,
|
||||||
PROMPT_TEMPLATE_VERSION,
|
PROMPT_TEMPLATE_VERSION,
|
||||||
splitIfBlock,
|
splitIfBlock,
|
||||||
upgradePlaceholders,
|
upgradePlaceholders,
|
||||||
@@ -147,3 +149,43 @@ describe('planPromptMigration', () => {
|
|||||||
expect(merged).toContain('工单已有原型插件 {{prototypeFile}}')
|
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()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -188,3 +188,52 @@ export function planPromptMigration(
|
|||||||
next.promptTemplateVersion = PROMPT_TEMPLATE_VERSION
|
next.promptTemplateVersion = PROMPT_TEMPLATE_VERSION
|
||||||
return { blob: next, notes }
|
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,7 +18,8 @@ import path from 'node:path'
|
|||||||
import { seedCommitProfilePath } from '../cc/commitProfile'
|
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 { 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.
|
* 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。
|
* 单测里直接 import。
|
||||||
*/
|
*/
|
||||||
export function migratePromptTemplates(ctx: ExtensionContext): MigrationNote[] {
|
export function migratePromptTemplates(ctx: ExtensionContext): MigrationNote[] {
|
||||||
const { blob, notes } = planPromptMigration(
|
const stored = readStoredSettings(ctx) as Record<string, unknown>
|
||||||
readStoredSettings(ctx) as Record<string, unknown>,
|
const planned = planPromptMigration(
|
||||||
|
stored,
|
||||||
readDefaultPrompt(ctx.extensionPath, 'brainstorm'),
|
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)
|
if (!blob)
|
||||||
return []
|
return []
|
||||||
const out: MigrationNote[] = []
|
const out: MigrationNote[] = []
|
||||||
@@ -535,7 +545,11 @@ export function migratePromptTemplates(ctx: ExtensionContext): MigrationNote[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
writeStoredSettings(ctx, blob as Partial<Settings>)
|
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
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1158,8 +1158,9 @@ class WebhookCoordinator {
|
|||||||
// Per-issue override takes precedence over the global setting.
|
// Per-issue override takes precedence over the global setting.
|
||||||
let effectiveAutoReview = getSettings(this.ctx).autoReview
|
let effectiveAutoReview = getSettings(this.ctx).autoReview
|
||||||
let autoReviewSource: 'issue' | 'global' = 'global'
|
let autoReviewSource: 'issue' | 'global' = 'global'
|
||||||
|
let stateJson: Record<string, unknown> | undefined
|
||||||
try {
|
try {
|
||||||
const stateJson = await readStateJsonComment({
|
stateJson = await readStateJsonComment({
|
||||||
host: ctx.host,
|
host: ctx.host,
|
||||||
token: ctx.token,
|
token: ctx.token,
|
||||||
owner: ctx.owner,
|
owner: ctx.owner,
|
||||||
@@ -1180,6 +1181,34 @@ class WebhookCoordinator {
|
|||||||
details: message,
|
details: message,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// 新提交让上一次「已通过」作废。已经是 false 或没写过就不再发一条状态评论。
|
||||||
|
// 读失败时不知道旧值,也清一次,避免卡片继续挂着已通过。
|
||||||
|
if (stateJson === undefined || stateJson.reviewPassed === true) {
|
||||||
|
try {
|
||||||
|
await mergeStateJsonComment({
|
||||||
|
host: ctx.host,
|
||||||
|
token: ctx.token,
|
||||||
|
owner: ctx.owner,
|
||||||
|
repo: ctx.repo,
|
||||||
|
issueNumber: event.issueNumber,
|
||||||
|
extra: { reviewPassed: false },
|
||||||
|
})
|
||||||
|
this.activePanel?.postMessage({
|
||||||
|
type: 'issue/patch',
|
||||||
|
issueNumber: event.issueNumber,
|
||||||
|
patch: { reviewPassed: false },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
|
logger.add({
|
||||||
|
level: 'warn',
|
||||||
|
source: 'webhook',
|
||||||
|
message: `新提交未能清掉审查通过 #${event.issueNumber}`,
|
||||||
|
details: message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!effectiveAutoReview) {
|
if (!effectiveAutoReview) {
|
||||||
logger.add({
|
logger.add({
|
||||||
level: 'info',
|
level: 'info',
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ function toIssue(baseUrl: string, it: YouTrackIssue): Issue {
|
|||||||
? state.implementStatus
|
? state.implementStatus
|
||||||
: undefined,
|
: undefined,
|
||||||
autoReview: typeof state.autoReview === 'boolean' ? state.autoReview : undefined,
|
autoReview: typeof state.autoReview === 'boolean' ? state.autoReview : undefined,
|
||||||
|
reviewPassed: typeof state.reviewPassed === 'boolean' ? state.reviewPassed : undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { addComment, listComments } from './api'
|
|||||||
export const STATE_MARKER = '<!--spx-state-->'
|
export const STATE_MARKER = '<!--spx-state-->'
|
||||||
|
|
||||||
/** Same field set as the Gitea state blob — kept independent to avoid coupling. */
|
/** Same field set as the Gitea state blob — kept independent to avoid coupling. */
|
||||||
const KNOWN_STATE_FIELDS = ['column', 'sessionId', 'implementSessionId', 'reviewSessionId', 'testSessionId', 'prototypeSessionId', 'profilePath', 'brainstormProfilePath', 'testProfilePath', 'prototypeProfilePath', 'specFile', 'planFile', 'prototypeFile', 'prDiffFile', 'pr', 'prMerged', 'prMergedAt', 'branch', 'worktreePath', 'implementStatus', 'color', 'autoReview', 'handoffAttachmentId', 'handoffFrom', 'handoffRequestFrom', 'handoffRequestError'] as const
|
const KNOWN_STATE_FIELDS = ['column', 'sessionId', 'implementSessionId', 'reviewSessionId', 'testSessionId', 'prototypeSessionId', 'profilePath', 'brainstormProfilePath', 'testProfilePath', 'prototypeProfilePath', 'specFile', 'planFile', 'prototypeFile', 'prDiffFile', 'pr', 'prMerged', 'prMergedAt', 'branch', 'worktreePath', 'implementStatus', 'color', 'autoReview', 'reviewPassed', 'handoffAttachmentId', 'handoffFrom', 'handoffRequestFrom', 'handoffRequestError'] as const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a comment body into a state object, or null if it isn't a state
|
* Parse a comment body into a state object, or null if it isn't a state
|
||||||
|
|||||||
@@ -118,6 +118,13 @@ export const IssueCard = forwardRef<HTMLDivElement, IssueCardProps>(
|
|||||||
{' '}
|
{' '}
|
||||||
<span>{issue.title}</span>
|
<span>{issue.title}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{issue.column === 'review'
|
||||||
|
? (
|
||||||
|
<div className="mt-0.5 text-[10px] leading-none opacity-60">
|
||||||
|
{issue.reviewPassed === true ? '已通过' : '审查中'}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export type ExtensionToWebview
|
|||||||
| { type: 'issues/loaded' }
|
| { type: 'issues/loaded' }
|
||||||
| { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean, me?: string, avatars?: Record<string, string> }
|
| { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean, me?: string, avatars?: Record<string, string> }
|
||||||
| { type: 'issues/error', message: string }
|
| { type: 'issues/error', message: string }
|
||||||
| { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prototypeFile?: string, prDiffFile?: string | null, sessionId?: string | null, implementSessionId?: string | null, reviewSessionId?: string | null, missingSessionIds?: string[], testSessionId?: string | null, prototypeSessionId?: string | null, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed' | null, column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string | null, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, prototypeTabOpen?: boolean, profilePath?: string | null, brainstormProfilePath?: string | null, testProfilePath?: string | null, prototypeProfilePath?: string | null, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null, handoffRequestFrom?: string | null, handoffRequestError?: string | null } }
|
| { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, reviewPassed?: boolean, specFile?: string, planFile?: string, prototypeFile?: string, prDiffFile?: string | null, sessionId?: string | null, implementSessionId?: string | null, reviewSessionId?: string | null, missingSessionIds?: string[], testSessionId?: string | null, prototypeSessionId?: string | null, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed' | null, column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string | null, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, prototypeTabOpen?: boolean, profilePath?: string | null, brainstormProfilePath?: string | null, testProfilePath?: string | null, prototypeProfilePath?: string | null, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null, handoffRequestFrom?: string | null, handoffRequestError?: string | null } }
|
||||||
/** 某个会话 id 的开 tab 请求已处理完(开成、复用或被拒都算),webview 据此停掉行内 spinner。 */
|
/** 某个会话 id 的开 tab 请求已处理完(开成、复用或被拒都算),webview 据此停掉行内 spinner。 */
|
||||||
| { type: 'session/tab-settled', sessionId: string }
|
| { type: 'session/tab-settled', sessionId: string }
|
||||||
| { type: 'issue/pr-diff-summary-done', issueNumber: number }
|
| { type: 'issue/pr-diff-summary-done', issueNumber: number }
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ export interface Issue {
|
|||||||
* global setting; true/false = explicit override stored in state JSON.
|
* global setting; true/false = explicit override stored in state JSON.
|
||||||
*/
|
*/
|
||||||
autoReview?: boolean
|
autoReview?: boolean
|
||||||
|
/** 最近一次审查是否通过。只影响审查列卡片上的小字,不改列。 */
|
||||||
|
reviewPassed?: boolean
|
||||||
/** Gitea assignee login 列表;接管按钮只对 assignee 含当前登录者的工单显示。 */
|
/** Gitea assignee login 列表;接管按钮只对 assignee 含当前登录者的工单显示。 */
|
||||||
assignees?: string[]
|
assignees?: string[]
|
||||||
/** Gitea 工单创建者 login;无 assignee 的工单靠它判定归属与卡片头像。 */
|
/** Gitea 工单创建者 login;无 assignee 的工单靠它判定归属与卡片头像。 */
|
||||||
|
|||||||
Reference in new issue
Block a user