@unifan/pi-unifan-zh 1.0.32 → 1.0.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/README_zh.md +2 -1
- package/extensions/commit/index.ts +51 -48
- package/extensions/commit/package.json +1 -1
- package/extensions/commit/src/git.ts +7 -0
- package/extensions/commit/tests/commit.test.mjs +150 -0
- package/extensions/review/index.ts +12 -85
- package/extensions/review/package.json +1 -1
- package/extensions/review/src/orchestration.ts +90 -0
- package/extensions/review/tests/orchestration.test.mjs +52 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -91,6 +91,7 @@ pi install npm:@unifan/pi-commit-zh
|
|
|
91
91
|
- **Dual Execution Engine**:
|
|
92
92
|
- ① **Single-Model Native Review (Default & Recommended)**: 0 subagent dependencies, 0 network dropouts, ultra-fast & 100% reliable.
|
|
93
93
|
- ② **Multi-Subagent Concurrent Review**: Freely configure **2, 3, 4, 5, or 6** concurrent expert subagents (Bugbot, Security, Perf, Compliance, Comments, History).
|
|
94
|
+
- **Session Model & Thinking Level Inheritance**: Single-model review and final synthesis run directly in the current session. Multi-expert review launches through one asynchronous workflow and explicitly passes the current session's `provider/model:thinking-level` to every expert, preventing subagent defaults from overriding the session settings.
|
|
94
95
|
- **Commands**:
|
|
95
96
|
- **`/review`** 🔍: Launches interactive Chinese TUI menu with 6 review modes + settings:
|
|
96
97
|
- ① `Review uncommitted changes` (staged + unstaged working tree, smart default)
|
|
@@ -117,7 +118,7 @@ pi install npm:@unifan/pi-commit-zh
|
|
|
117
118
|
|
|
118
119
|
### 3. 📦 `commit` (Intelligent Conventional Commits Assistant)
|
|
119
120
|
- **Commands**:
|
|
120
|
-
- **`/commit`**: Analyzes `git diff
|
|
121
|
+
- **`/commit`**: Analyzes staged and working-tree changes (`git diff`), uses the current session's model and thinking level through the provider's unified parameter handling, and generates a Chinese Conventional Commit message from the actual staged changes before committing. Authentication, request, empty-response, or output-validation failures display the reason and stop the commit and push, preserving staged changes without substituting a generic fallback message.
|
|
121
122
|
- **`/commit-push`** 🚀: Generates Chinese commit and automatically executes `git push` to remote.
|
|
122
123
|
- **Usage & Flags**:
|
|
123
124
|
- `/commit`: Smart commit (commits staged changes if staged, or auto-stages all if unstaged).
|
package/README_zh.md
CHANGED
|
@@ -90,6 +90,7 @@ pi install npm:@unifan/pi-commit-zh
|
|
|
90
90
|
- **双执行引擎自由切换**:
|
|
91
91
|
- ① **单模型深度审查(默认推荐)**:0 子代理依赖,0 网络断流,极速且 100% 稳定。
|
|
92
92
|
- ② **多 Subagent 并发审查**:支持自由配置并发启动 **2、3、4、5、6** 个专家子代理协同会诊(Bug猎手、安全专家、性能探针、规范合规、注释质量、历史脉络)。
|
|
93
|
+
- **会话模型与思考档位继承**:单模型审查和主审汇总直接使用当前会话;多专家审查通过一个异步工作流启动,在每个专家请求中显式传入当前会话的 `提供方/模型:思考档位`,避免被子代理默认配置覆盖。
|
|
93
94
|
- **核心审查命令**:
|
|
94
95
|
- **`/review`** 🔍 **(交互式全功能审查)**:唤起全中文 TUI 选择弹窗,支持 6 种审查模式与配置中心:
|
|
95
96
|
- ① `审查未提交改动` (工作区已暂存 + 未暂存代码,智能默认推荐)
|
|
@@ -122,7 +123,7 @@ pi install npm:@unifan/pi-commit-zh
|
|
|
122
123
|
|
|
123
124
|
### 3. 📦 `commit`(智能 Git 提交助手·中文版)
|
|
124
125
|
- **命令**:
|
|
125
|
-
- **`/commit`**:自动分析暂存区/工作区代码变动(`git diff
|
|
126
|
+
- **`/commit`**:自动分析暂存区/工作区代码变动(`git diff`),使用当前会话模型与思考档位,由提供方统一转换模型参数,根据实际暂存内容生成标准的 Conventional Commits 纯中文提交信息后提交。模型认证、请求、空结果或输出校验失败时会显示原因并停止提交与推送,保留暂存内容,不再使用固定兜底文案。
|
|
126
127
|
- **`/commit-push`** 🚀:自动生成中文 Commit 提交,并**自动执行 `git push` 推送**到远端分支。
|
|
127
128
|
- **常用参数与用法**:
|
|
128
129
|
- `/commit`:智能提交(有暂存只提暂存,无暂存自动暂存全量)。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
3
|
commitWithMsg,
|
|
4
|
-
|
|
4
|
+
getStagedFiles,
|
|
5
5
|
getStagedDiff,
|
|
6
6
|
getUnpushedCommits,
|
|
7
7
|
getUnstagedDiff,
|
|
@@ -35,11 +35,12 @@ function parseArgs(raw: string): ParsedCommitArgs {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
async function generateCommitMessage(
|
|
38
|
+
export async function generateCommitMessage(
|
|
39
39
|
ctx: ExtensionCommandContext,
|
|
40
40
|
diff: string,
|
|
41
41
|
changedFiles: string[],
|
|
42
42
|
userHint?: string,
|
|
43
|
+
thinkingLevel = ctx.thinkingLevel,
|
|
43
44
|
): Promise<string> {
|
|
44
45
|
const truncatedDiff = diff.length > 30000 ? diff.slice(0, 30000) + "\n\n... (diff已截断)" : diff;
|
|
45
46
|
const userPrompt = [
|
|
@@ -55,50 +56,39 @@ async function generateCommitMessage(
|
|
|
55
56
|
.filter(Boolean)
|
|
56
57
|
.join("\n");
|
|
57
58
|
|
|
58
|
-
if (ctx.model
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
} catch (err) {
|
|
94
|
-
console.error("pi-commit streamSimple error:", err);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// 智能保底推断
|
|
99
|
-
const firstFile = changedFiles[0] ?? "core";
|
|
100
|
-
const scope = firstFile.split(/[/\\]/)[0] || "core";
|
|
101
|
-
return `chore(${scope}): 更新代码与相关配置\n\n- 更新了 ${changedFiles.length} 个文件`;
|
|
59
|
+
if (!ctx.model) throw new Error("当前未选择模型,请先使用 /model 选择模型。");
|
|
60
|
+
if (typeof ctx.modelRegistry?.getProvider !== "function" || typeof ctx.modelRegistry?.getProviderAuth !== "function")
|
|
61
|
+
throw new Error("当前 Pi 不支持统一模型调用接口,请更新 Pi 后重试。");
|
|
62
|
+
const modelLabel = `${ctx.model.provider}/${ctx.model.id}`;
|
|
63
|
+
const provider = ctx.modelRegistry.getProvider(ctx.model.provider);
|
|
64
|
+
if (!provider) throw new Error(`模型 ${modelLabel} 的提供方不可用,请重新选择模型。`);
|
|
65
|
+
const resolved = await ctx.modelRegistry.getProviderAuth(ctx.model.provider);
|
|
66
|
+
if (!resolved) throw new Error(`模型 ${modelLabel} 的认证未配置,请先完成登录或配置认证。`);
|
|
67
|
+
|
|
68
|
+
// 宿主解析认证、代理地址和环境;统一接口按模型能力转换会话思考档位,避免底层调用默认发送 none。
|
|
69
|
+
const model = resolved.auth.baseUrl ? { ...ctx.model, baseUrl: resolved.auth.baseUrl } : ctx.model;
|
|
70
|
+
const response = await provider.streamSimple(model, {
|
|
71
|
+
systemPrompt: COMMIT_SYSTEM_PROMPT,
|
|
72
|
+
messages: [{ role: "user", content: [{ type: "text", text: userPrompt }], timestamp: Date.now() }],
|
|
73
|
+
}, {
|
|
74
|
+
maxTokens: 4096,
|
|
75
|
+
signal: ctx.signal,
|
|
76
|
+
reasoning: thinkingLevel === "off" ? undefined : thinkingLevel,
|
|
77
|
+
apiKey: resolved.auth.apiKey,
|
|
78
|
+
headers: resolved.auth.headers,
|
|
79
|
+
env: resolved.env,
|
|
80
|
+
}).result();
|
|
81
|
+
if (response.stopReason !== "stop")
|
|
82
|
+
throw new Error(`模型 ${modelLabel} 未正常完成生成(${response.stopReason}):${response.errorMessage || "请检查模型状态后重试。"}`);
|
|
83
|
+
|
|
84
|
+
const text = response.content.map((c) => c.type === "text" ? c.text : "").join("").trim()
|
|
85
|
+
.replace(/^```[a-zA-Z]*\r?\n?/, "").replace(/\r?\n?```$/, "").trim();
|
|
86
|
+
if (!text) throw new Error("模型返回了空的提交信息。");
|
|
87
|
+
if (!/^(feat|fix|perf|refactor|ci|build|chore|docs|test|style|revert)(\([^()\r\n]+\))?: [^\r\n]+$/.test(text.split(/\r?\n/)[0]))
|
|
88
|
+
throw new Error("模型返回的提交标题不符合约定格式,请重试或补充修改说明。");
|
|
89
|
+
if (text.includes("更新代码与相关配置"))
|
|
90
|
+
throw new Error("模型返回了无具体业务含义的通用提交信息,请补充修改说明后重试。");
|
|
91
|
+
return text;
|
|
102
92
|
}
|
|
103
93
|
|
|
104
94
|
export default function (pi: ExtensionAPI) {
|
|
@@ -116,7 +106,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
116
106
|
const parsed = parseArgs(args);
|
|
117
107
|
let stagedDiff = await getStagedDiff(ctx.cwd);
|
|
118
108
|
const unstagedDiff = await getUnstagedDiff(ctx.cwd);
|
|
119
|
-
|
|
109
|
+
|
|
120
110
|
|
|
121
111
|
if (!stagedDiff && !unstagedDiff) {
|
|
122
112
|
if (andPush) {
|
|
@@ -163,7 +153,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
163
153
|
}
|
|
164
154
|
|
|
165
155
|
notify("正在深度分析代码改动并生成中文 Commit Message...", "info");
|
|
166
|
-
|
|
156
|
+
let commitMessage: string;
|
|
157
|
+
try {
|
|
158
|
+
if (!stagedDiff.trim()) throw new Error("暂存区没有可用于生成提交信息的差异。");
|
|
159
|
+
const changedFiles = await getStagedFiles(ctx.cwd);
|
|
160
|
+
commitMessage = await generateCommitMessage(
|
|
161
|
+
ctx, stagedDiff, changedFiles, parsed.hint, ctx.thinkingLevel ?? pi.getThinkingLevel(),
|
|
162
|
+
);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
// 生成失败必须停止,禁止用固定文案继续提交或推送。
|
|
165
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
166
|
+
notify(`提交信息生成失败,未执行提交或推送:${reason}`, "error");
|
|
167
|
+
pi.sendMessage({ customType: "pi-commit-error", content: `提交信息生成失败,未执行提交或推送。\n\n${reason}\n\n当前暂存内容保留,可修复模型配置后重新执行。`, display: true });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
167
170
|
|
|
168
171
|
const commitRes = await commitWithMsg(ctx.cwd, commitMessage);
|
|
169
172
|
if (!commitRes.ok) {
|
|
@@ -36,6 +36,13 @@ export async function getUnstagedDiff(cwd: string): Promise<string> {
|
|
|
36
36
|
return res.ok ? res.stdout : "";
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// 使用空字符分隔,保留中文、空格和特殊字符文件名;只描述本次真正提交的文件。
|
|
40
|
+
export async function getStagedFiles(cwd: string): Promise<string[]> {
|
|
41
|
+
const res = await runGit(cwd, ["diff", "--cached", "--name-only", "-z"]);
|
|
42
|
+
if (!res.ok) throw new Error(`读取暂存文件失败:${res.stderr}`);
|
|
43
|
+
return res.stdout.split("\0").filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
export async function getChangedFiles(cwd: string): Promise<string[]> {
|
|
40
47
|
const res = await runGit(cwd, ["status", "--porcelain"]);
|
|
41
48
|
if (!res.ok || !res.stdout.trim()) return [];
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { test, after } from 'node:test';
|
|
3
|
+
import { mkdtemp, mkdir, readFile, writeFile, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import ts from 'typescript';
|
|
9
|
+
import { stream, streamSimple } from '@earendil-works/pi-ai/api/openai-responses';
|
|
10
|
+
|
|
11
|
+
// 在临时目录编译并运行真实命令,不访问开发仓库的提交记录。
|
|
12
|
+
const root = await mkdtemp(join(tmpdir(), 'pi-commit-test-'));
|
|
13
|
+
after(() => rm(root, { recursive: true, force: true }));
|
|
14
|
+
await mkdir(join(root, 'src'));
|
|
15
|
+
await writeFile(join(root, 'package.json'), '{"type":"module"}');
|
|
16
|
+
for (const name of ['index', 'src/git', 'src/prompt']) {
|
|
17
|
+
const source = await readFile(new URL(`../${name}.ts`, import.meta.url), 'utf8');
|
|
18
|
+
const output = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } }).outputText;
|
|
19
|
+
await writeFile(join(root, `${name}.js`), output);
|
|
20
|
+
}
|
|
21
|
+
const { default: register, generateCommitMessage } = await import(pathToFileURL(join(root, 'index.js')));
|
|
22
|
+
const { getStagedFiles } = await import(pathToFileURL(join(root, 'src/git.js')));
|
|
23
|
+
const model = { id: '当前模型', provider: '当前提供方' };
|
|
24
|
+
function context(complete) {
|
|
25
|
+
const provider = { streamSimple: (...args) => ({ result: () => complete(...args) }) };
|
|
26
|
+
return {
|
|
27
|
+
model,
|
|
28
|
+
thinkingLevel: 'high',
|
|
29
|
+
modelRegistry: {
|
|
30
|
+
getProvider: () => provider,
|
|
31
|
+
getProviderAuth: async () => ({ auth: { apiKey: '测试凭据' } }),
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function response(text, stopReason = 'stop') { return { content: [{ type: 'text', text }], stopReason }; }
|
|
36
|
+
|
|
37
|
+
test('通过宿主统一接口继承当前模型并返回业务日志', async () => {
|
|
38
|
+
const ctx = context(async (actualModel, prompt, options) => {
|
|
39
|
+
assert.equal(actualModel, model);
|
|
40
|
+
assert.match(prompt.messages[0].content[0].text, /修复冲线残留/);
|
|
41
|
+
assert.equal(options.maxTokens, 4096);
|
|
42
|
+
assert.equal(options.reasoning, 'high');
|
|
43
|
+
return response('```text\nfix(racing): 清理冲线特效残留\n```');
|
|
44
|
+
});
|
|
45
|
+
assert.equal(await generateCommitMessage(ctx, '+ 清理特效', ['角色.cs'], '修复冲线残留'), 'fix(racing): 清理冲线特效残留');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('继承全部会话思考档位及宿主认证、代理与环境,不修改原模型', async () => {
|
|
49
|
+
for (const level of ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']) {
|
|
50
|
+
const signal = new AbortController().signal;
|
|
51
|
+
const auth = { apiKey: '测试凭据', baseUrl: 'https://proxy.example.invalid/v1', headers: { 'x-test': '测试头' } };
|
|
52
|
+
const env = { TEST_PROVIDER_REGION: '测试区域' };
|
|
53
|
+
const ctx = context(async (actualModel, _prompt, options) => {
|
|
54
|
+
assert.equal(actualModel.id, model.id);
|
|
55
|
+
assert.equal(actualModel.baseUrl, auth.baseUrl);
|
|
56
|
+
assert.equal(model.baseUrl, undefined);
|
|
57
|
+
assert.equal(options.reasoning, level === 'off' ? undefined : level);
|
|
58
|
+
assert.equal(options.apiKey, auth.apiKey);
|
|
59
|
+
assert.equal(options.headers, auth.headers);
|
|
60
|
+
assert.equal(options.env, env);
|
|
61
|
+
assert.equal(options.signal, signal);
|
|
62
|
+
return response('fix: 修复思考档位继承');
|
|
63
|
+
});
|
|
64
|
+
ctx.thinkingLevel = level;
|
|
65
|
+
ctx.signal = signal;
|
|
66
|
+
ctx.modelRegistry.getProviderAuth = async () => ({ auth, env });
|
|
67
|
+
await generateCommitMessage(ctx, 'diff', ['index.ts']);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('真实 OpenAI 适配器复现默认 none,并验证会话档位写入请求体', async () => {
|
|
72
|
+
const actualModel = {
|
|
73
|
+
...model, id: 'gpt-6-astra', api: 'openai-responses', baseUrl: 'https://example.invalid/v1',
|
|
74
|
+
reasoning: true, input: ['text'], contextWindow: 128000, maxTokens: 16384,
|
|
75
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
76
|
+
thinkingLevelMap: { xhigh: 'xhigh', max: 'max' },
|
|
77
|
+
};
|
|
78
|
+
let payload;
|
|
79
|
+
const onPayload = value => { payload = value; throw new Error('已捕获请求,禁止联网'); };
|
|
80
|
+
const oldResponse = await stream(actualModel, { messages: [] }, { apiKey: '测试凭据', onPayload }).result();
|
|
81
|
+
assert.match(oldResponse.errorMessage, /已捕获请求/);
|
|
82
|
+
assert.equal(payload.reasoning.effort, 'none');
|
|
83
|
+
for (const thinking of ['low', 'medium', 'high', 'xhigh', 'max']) {
|
|
84
|
+
const ctx = context();
|
|
85
|
+
ctx.model = actualModel;
|
|
86
|
+
ctx.thinkingLevel = thinking;
|
|
87
|
+
ctx.modelRegistry.getProvider = () => ({
|
|
88
|
+
streamSimple: (requestModel, prompt, options) => streamSimple(requestModel, prompt, { ...options, onPayload }),
|
|
89
|
+
});
|
|
90
|
+
await assert.rejects(generateCommitMessage(ctx, 'diff', ['index.ts']), /已捕获请求/);
|
|
91
|
+
assert.equal(payload.reasoning.effort, thinking);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('提供方或认证缺失时停止生成', async () => {
|
|
96
|
+
const ctx = context(() => { assert.fail('不应请求模型'); });
|
|
97
|
+
ctx.modelRegistry.getProviderAuth = async () => undefined;
|
|
98
|
+
await assert.rejects(generateCommitMessage(ctx, 'diff', []), /认证未配置/);
|
|
99
|
+
ctx.modelRegistry.getProvider = () => undefined;
|
|
100
|
+
await assert.rejects(generateCommitMessage(ctx, 'diff', []), /提供方不可用/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('拒绝空结果、异常完成、截断、非法格式和通用文案', async () => {
|
|
104
|
+
for (const result of [response(''), response('fix: 修复残留', 'error'), response('fix: 修复残留', 'length'), response('说明文字'), response('chore(GameClient): 更新代码与相关配置')]) {
|
|
105
|
+
await assert.rejects(generateCommitMessage(context(async () => result), 'diff', ['角色.cs']));
|
|
106
|
+
}
|
|
107
|
+
await assert.rejects(generateCommitMessage(context(async () => { throw new Error('认证失败'); }), 'diff', []), /认证失败/);
|
|
108
|
+
await assert.rejects(generateCommitMessage({ modelRegistry: {} }, 'diff', []), /未选择模型/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('生成失败不提交或推送,文件清单仅包含暂存区', async () => {
|
|
112
|
+
const repo = join(root, 'repo');
|
|
113
|
+
await mkdir(repo);
|
|
114
|
+
const git = (...args) => execFileSync('git', args, { cwd: repo, encoding: 'utf8' }).trim();
|
|
115
|
+
git('init', '-q');
|
|
116
|
+
git('config', 'user.name', '测试');
|
|
117
|
+
git('config', 'user.email', 'test@example.invalid');
|
|
118
|
+
await writeFile(join(repo, '角色 文件.cs'), '初始内容');
|
|
119
|
+
await writeFile(join(repo, '未暂存.cs'), '初始内容');
|
|
120
|
+
git('add', '.');
|
|
121
|
+
git('commit', '-qm', 'test: 初始化');
|
|
122
|
+
const head = git('rev-parse', 'HEAD');
|
|
123
|
+
await writeFile(join(repo, '角色 文件.cs'), '修改内容');
|
|
124
|
+
await writeFile(join(repo, '未暂存.cs'), '不应包含在提交中');
|
|
125
|
+
git('add', '--', '角色 文件.cs');
|
|
126
|
+
assert.deepEqual(await getStagedFiles(repo), ['角色 文件.cs']);
|
|
127
|
+
const commands = new Map();
|
|
128
|
+
const messages = [];
|
|
129
|
+
register({
|
|
130
|
+
registerCommand: (name, command) => commands.set(name, command),
|
|
131
|
+
sendMessage: message => messages.push(message),
|
|
132
|
+
getThinkingLevel: () => 'max',
|
|
133
|
+
});
|
|
134
|
+
const ctx = { ...context(async () => { throw new Error('认证失败'); }), cwd: repo, hasUI: true, ui: { notify() {} } };
|
|
135
|
+
await commands.get('commit-push').handler('', ctx);
|
|
136
|
+
assert.equal(git('rev-parse', 'HEAD'), head);
|
|
137
|
+
assert.match(messages.at(-1).content, /未执行提交或推送/);
|
|
138
|
+
assert.match(messages.at(-1).content, /认证失败/);
|
|
139
|
+
assert.deepEqual(await getStagedFiles(repo), ['角色 文件.cs']);
|
|
140
|
+
// 兼容尚未提供 ctx.thinkingLevel 的宿主,从公开会话接口获取实际档位。
|
|
141
|
+
delete ctx.thinkingLevel;
|
|
142
|
+
ctx.modelRegistry = context(async (_model, _prompt, options) => {
|
|
143
|
+
assert.equal(options.reasoning, 'max');
|
|
144
|
+
return response('fix: 修复角色表现');
|
|
145
|
+
}).modelRegistry;
|
|
146
|
+
await commands.get('commit').handler('', ctx);
|
|
147
|
+
assert.equal(git('log', '-1', '--format=%B'), 'fix: 修复角色表现');
|
|
148
|
+
assert.notEqual(git('rev-parse', 'HEAD'), head);
|
|
149
|
+
assert.ok(git('diff', '--name-only', '-z').includes('未暂存.cs'));
|
|
150
|
+
});
|
|
@@ -26,6 +26,7 @@ import { Container, type SelectItem, SelectList, Text } from "@earendil-works/pi
|
|
|
26
26
|
import path from "node:path";
|
|
27
27
|
import os from "node:os";
|
|
28
28
|
import { promises as fs } from "node:fs";
|
|
29
|
+
import { buildSubagentOrchestrationPrompt } from "./src/orchestration.js";
|
|
29
30
|
|
|
30
31
|
// 跟踪审查会话来源分支节点(保证单次仅一个活跃审查会话)
|
|
31
32
|
let reviewOriginId: string | undefined = undefined;
|
|
@@ -77,89 +78,6 @@ async function saveSettings(settings: ReviewSettings): Promise<void> {
|
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
interface ReviewExpert {
|
|
81
|
-
id: string;
|
|
82
|
-
label: string;
|
|
83
|
-
desc: string;
|
|
84
|
-
task: string;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const ALL_EXPERTS: ReviewExpert[] = [
|
|
88
|
-
{
|
|
89
|
-
id: "pi-review.bugbot",
|
|
90
|
-
label: "Bug 猎手 (Bugbot)",
|
|
91
|
-
desc: "逻辑缺陷、空指针、边界溢出、死锁与运行时崩溃",
|
|
92
|
-
task: "深入排查本次代码改动中的业务逻辑缺陷、空指针、边界异常、并发竞态与未捕获的运行时异常",
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
id: "pi-review.security-review",
|
|
96
|
-
label: "安全专家 (Security)",
|
|
97
|
-
desc: "输入参数合法性校验、跨目录边界防护与权限隔离",
|
|
98
|
-
task: "深入排查本次代码改动中的防御性编码缺陷、外部输入未做类型/范围校验、边界防护不足等健壮性隐患",
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
id: "pi-review.perf-review",
|
|
102
|
-
label: "性能探针 (Perf)",
|
|
103
|
-
desc: "循环内GC内存分配、CPU热点消耗、算法复杂度与资源泄露",
|
|
104
|
-
task: "深入排查本次代码改动中的性能退化、高频循环内无谓内存分配 (GC压力) 与算法复杂度",
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
id: "pi-review.claude-md-compliance",
|
|
108
|
-
label: "契约合规 (Compliance)",
|
|
109
|
-
desc: "架构契约、设计模式、模块边界与规范遵循",
|
|
110
|
-
task: "排查本次代码改动是否违反项目既有架构契约、模块封装规范与规范指南",
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
id: "pi-review.code-comments",
|
|
114
|
-
label: "注释与可读性 (Comments)",
|
|
115
|
-
desc: "注释与代码逻辑倒挂、误导性命名与维护性隐患",
|
|
116
|
-
task: "排查本次代码改动中的可读性隐患、注释与逻辑不符、误导性命名与维护风险",
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
id: "pi-review.history-context",
|
|
120
|
-
label: "历史脉络 (History)",
|
|
121
|
-
desc: "结合 Git 历史演进判断意图,防止历史问题回归",
|
|
122
|
-
task: "结合代码演变历史,排查本次改动是否破坏既有历史契约或重现已知缺陷",
|
|
123
|
-
},
|
|
124
|
-
];
|
|
125
|
-
|
|
126
|
-
function buildSubagentOrchestrationPrompt(
|
|
127
|
-
concurrency: number,
|
|
128
|
-
gateEnabled: boolean,
|
|
129
|
-
targetInstruction: string,
|
|
130
|
-
): string {
|
|
131
|
-
const count = Math.min(6, Math.max(2, concurrency));
|
|
132
|
-
const selected = ALL_EXPERTS.slice(0, count);
|
|
133
|
-
|
|
134
|
-
const listText = selected.map((exp, idx) => `${idx + 1}. **${exp.label}** (\`${exp.id}\`):${exp.desc}`).join("\n");
|
|
135
|
-
const callsExample = selected
|
|
136
|
-
.map((exp) => {
|
|
137
|
-
const fullTask = `${targetInstruction}\n\n【专项排查分工】:${exp.task}`;
|
|
138
|
-
return `subagent({ agent: ${JSON.stringify(exp.id)}, task: ${JSON.stringify(fullTask)} });`;
|
|
139
|
-
})
|
|
140
|
-
.join("\n");
|
|
141
|
-
|
|
142
|
-
const maxBackticks = (callsExample.match(/`+/g) || []).reduce((max, m) => Math.max(max, m.length), 2);
|
|
143
|
-
const fence = "`".repeat(maxBackticks + 1);
|
|
144
|
-
|
|
145
|
-
return `## 🚀 执行方式:多 Subagent 并发专家审查 (当前配置并发数: ${count} 个专家)
|
|
146
|
-
|
|
147
|
-
当前已配置并行启动以下 ${count} 个专家子代理进行分工审查:
|
|
148
|
-
|
|
149
|
-
${listText}
|
|
150
|
-
|
|
151
|
-
### 协作审查执行规范:
|
|
152
|
-
1. **并发调用子代理**:请在当前回合使用 \`subagent\` 工具**同时并行唤起**上述 ${count} 个专家子代理(单回合发起 ${count} 个并发 tool_call,严禁串行逐个调用):
|
|
153
|
-
${fence}js
|
|
154
|
-
${callsExample}
|
|
155
|
-
${fence}
|
|
156
|
-
2. **主审裁判长汇总整理**:当所有专家子代理执行完毕返回发现后,请你作为主审裁判长${gateEnabled ? "(门禁裁决)" : ""}:
|
|
157
|
-
- 全面综合各专家的审查意见,对相同问题进行去重,剔除误报和低置信度内容(注意:项目中合法的内部 GM / Debug 调试工具在确保与正式生产环境隔离的前提下免检)。
|
|
158
|
-
- 严格按照《核心代码审查准则》的 **[P0~P3]** 等级标准排布审查清单。
|
|
159
|
-
- 给出最终综合裁决与一句话中文总评。
|
|
160
|
-
3. **语言强制要求**:所有任务入参、思考分析过程、综合汇报与最终报告必须 100% 为纯正中文,严禁出现任何英文段落或未翻译小标题!`;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
81
|
function setReviewWidget(ctx: ExtensionContext, active: boolean) {
|
|
164
82
|
if (!ctx.hasUI) return;
|
|
165
83
|
if (!active) {
|
|
@@ -1062,6 +980,13 @@ export default function reviewExtension(pi: ExtensionAPI) {
|
|
|
1062
980
|
...runtimeSettings,
|
|
1063
981
|
};
|
|
1064
982
|
|
|
983
|
+
const sessionModel = ctx.model;
|
|
984
|
+
const thinkingLevel = ctx.thinkingLevel ?? pi.getThinkingLevel();
|
|
985
|
+
if (settings.mode === "subagents" && !sessionModel) {
|
|
986
|
+
ctx.ui.notify("当前未选择模型,请先使用 /model 选择模型。", "error");
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
|
|
1065
990
|
let effectiveFreshSession = useFreshSession;
|
|
1066
991
|
|
|
1067
992
|
if (effectiveFreshSession) {
|
|
@@ -1110,8 +1035,10 @@ export default function reviewExtension(pi: ExtensionAPI) {
|
|
|
1110
1035
|
|
|
1111
1036
|
let fullPrompt = REVIEW_RUBRIC;
|
|
1112
1037
|
|
|
1113
|
-
if (settings.mode === "subagents") {
|
|
1114
|
-
fullPrompt += `\n\n---\n\n${buildSubagentOrchestrationPrompt(
|
|
1038
|
+
if (settings.mode === "subagents" && sessionModel) {
|
|
1039
|
+
fullPrompt += `\n\n---\n\n${buildSubagentOrchestrationPrompt(
|
|
1040
|
+
settings.concurrency, settings.gateEnabled, prompt, sessionModel, thinkingLevel,
|
|
1041
|
+
)}`;
|
|
1115
1042
|
}
|
|
1116
1043
|
|
|
1117
1044
|
fullPrompt += `\n\n---\n\n## 本次审查任务指示\n\n${prompt}`;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
interface ReviewExpert {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
desc: string;
|
|
5
|
+
task: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const ALL_EXPERTS: ReviewExpert[] = [
|
|
9
|
+
{
|
|
10
|
+
id: "pi-review.bugbot",
|
|
11
|
+
label: "Bug 猎手 (Bugbot)",
|
|
12
|
+
desc: "逻辑缺陷、空指针、边界溢出、死锁与运行时崩溃",
|
|
13
|
+
task: "深入排查本次代码改动中的业务逻辑缺陷、空指针、边界异常、并发竞态与未捕获的运行时异常",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "pi-review.security-review",
|
|
17
|
+
label: "安全专家 (Security)",
|
|
18
|
+
desc: "输入参数合法性校验、跨目录边界防护与权限隔离",
|
|
19
|
+
task: "深入排查本次代码改动中的防御性编码缺陷、外部输入未做类型/范围校验、边界防护不足等健壮性隐患",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "pi-review.perf-review",
|
|
23
|
+
label: "性能探针 (Perf)",
|
|
24
|
+
desc: "循环内GC内存分配、CPU热点消耗、算法复杂度与资源泄露",
|
|
25
|
+
task: "深入排查本次代码改动中的性能退化、高频循环内无谓内存分配 (GC压力) 与算法复杂度",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "pi-review.claude-md-compliance",
|
|
29
|
+
label: "契约合规 (Compliance)",
|
|
30
|
+
desc: "架构契约、设计模式、模块边界与规范遵循",
|
|
31
|
+
task: "排查本次代码改动是否违反项目既有架构契约、模块封装规范与规范指南",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "pi-review.code-comments",
|
|
35
|
+
label: "注释与可读性 (Comments)",
|
|
36
|
+
desc: "注释与代码逻辑倒挂、误导性命名与维护性隐患",
|
|
37
|
+
task: "排查本次代码改动中的可读性隐患、注释与逻辑不符、误导性命名与维护风险",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "pi-review.history-context",
|
|
41
|
+
label: "历史脉络 (History)",
|
|
42
|
+
desc: "结合 Git 历史演进判断意图,防止历史问题回归",
|
|
43
|
+
task: "结合代码演变历史,排查本次改动是否破坏既有历史契约或重现已知缺陷",
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export function buildSubagentOrchestrationPrompt(
|
|
48
|
+
concurrency: number,
|
|
49
|
+
gateEnabled: boolean,
|
|
50
|
+
targetInstruction: string,
|
|
51
|
+
sessionModel: { provider: string; id: string },
|
|
52
|
+
thinkingLevel: string,
|
|
53
|
+
): string {
|
|
54
|
+
const count = Math.min(6, Math.max(2, concurrency));
|
|
55
|
+
const selected = ALL_EXPERTS.slice(0, count);
|
|
56
|
+
|
|
57
|
+
const listText = selected.map((exp, idx) => `${idx + 1}. **${exp.label}** (\`${exp.id}\`):${exp.desc}`).join("\n");
|
|
58
|
+
// 每次执行显式固定当前会话模型和档位,避免子代理默认配置覆盖会话选择。
|
|
59
|
+
const model = `${sessionModel.provider}/${sessionModel.id}:${thinkingLevel}`;
|
|
60
|
+
const tasks = selected.map((exp) => ({
|
|
61
|
+
key: exp.id,
|
|
62
|
+
agent: exp.id,
|
|
63
|
+
model,
|
|
64
|
+
task: `${targetInstruction}\n\n【专项排查分工】:${exp.task}`,
|
|
65
|
+
}));
|
|
66
|
+
const callsExample = `subagent(${JSON.stringify({
|
|
67
|
+
async: true,
|
|
68
|
+
workflowScript: `const results = await runs.all(${JSON.stringify(tasks)});\nreturn results;`,
|
|
69
|
+
}, null, 2)});`;
|
|
70
|
+
|
|
71
|
+
const maxBackticks = (callsExample.match(/`+/g) || []).reduce((max, m) => Math.max(max, m.length), 2);
|
|
72
|
+
const fence = "`".repeat(maxBackticks + 1);
|
|
73
|
+
|
|
74
|
+
return `## 🚀 执行方式:多 Subagent 并发专家审查 (当前配置并发数: ${count} 个专家)
|
|
75
|
+
|
|
76
|
+
当前已配置并行启动以下 ${count} 个专家子代理进行分工审查:
|
|
77
|
+
|
|
78
|
+
${listText}
|
|
79
|
+
|
|
80
|
+
### 协作审查执行规范:
|
|
81
|
+
1. **并发调用子代理**:先调用 \`subagent({ action: "list", capabilities: true })\`,确认上述专家均可执行且未禁用;再通过下方**一个异步工作流**并行启动全部专家。每个专家必须使用示例中的 \`model\` 参数,继承本次会话模型与思考档位(${thinkingLevel}),不得省略或改用代理默认档位。运行失败时报告具体原因并停止,不得静默更换模型或执行方式:
|
|
82
|
+
${fence}js
|
|
83
|
+
${callsExample}
|
|
84
|
+
${fence}
|
|
85
|
+
2. **主审裁判长汇总整理**:当所有专家子代理执行完毕返回发现后,请你作为主审裁判长${gateEnabled ? "(门禁裁决)" : ""}:
|
|
86
|
+
- 全面综合各专家的审查意见,对相同问题进行去重,剔除误报和低置信度内容(注意:项目中合法的内部 GM / Debug 调试工具在确保与正式生产环境隔离的前提下免检)。
|
|
87
|
+
- 严格按照《核心代码审查准则》的 **[P0~P3]** 等级标准排布审查清单。
|
|
88
|
+
- 给出最终综合裁决与一句话中文总评。
|
|
89
|
+
3. **语言强制要求**:所有任务入参、思考分析过程、综合汇报与最终报告必须 100% 为纯正中文,严禁出现任何英文段落或未翻译小标题!`;
|
|
90
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
|
|
6
|
+
const source = await readFile(new URL('../src/orchestration.ts', import.meta.url), 'utf8');
|
|
7
|
+
const compiled = ts.transpileModule(source, {
|
|
8
|
+
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
|
|
9
|
+
}).outputText;
|
|
10
|
+
const { buildSubagentOrchestrationPrompt } = await import(`data:text/javascript;base64,${Buffer.from(compiled).toString('base64')}`);
|
|
11
|
+
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
|
12
|
+
|
|
13
|
+
// 执行实际生成的工作流,使用假运行器捕获请求,不启动模型或子代理。
|
|
14
|
+
async function runExample(prompt) {
|
|
15
|
+
const example = prompt.match(/(`{3,})js\n([\s\S]*?)\n\1/);
|
|
16
|
+
assert.ok(example, '应包含可执行的工作流示例');
|
|
17
|
+
let request;
|
|
18
|
+
new Function('subagent', example[2])(value => { request = value; });
|
|
19
|
+
assert.equal(request.async, true);
|
|
20
|
+
let tasks;
|
|
21
|
+
const results = [{ output: '审查完成' }];
|
|
22
|
+
const actual = await new AsyncFunction('runs', request.workflowScript)({
|
|
23
|
+
all: async value => { tasks = value; return results; },
|
|
24
|
+
});
|
|
25
|
+
assert.equal(actual, results);
|
|
26
|
+
return tasks;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test('所有专家在不同并发数下继承会话模型及全部思考档位', async () => {
|
|
30
|
+
for (const thinking of ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']) {
|
|
31
|
+
for (const count of [2, 3, 4, 5, 6]) {
|
|
32
|
+
const prompt = buildSubagentOrchestrationPrompt(count, true, '检查当前差异', { provider: '会话提供方', id: '会话模型' }, thinking);
|
|
33
|
+
const tasks = await runExample(prompt);
|
|
34
|
+
assert.equal(tasks.length, count);
|
|
35
|
+
assert.equal(new Set(tasks.map(task => task.key)).size, count);
|
|
36
|
+
for (const task of tasks) {
|
|
37
|
+
assert.equal(task.model, `会话提供方/会话模型:${thinking}`);
|
|
38
|
+
assert.match(task.task, /检查当前差异/);
|
|
39
|
+
}
|
|
40
|
+
assert.match(prompt, /capabilities: true/);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('用户任务中的引号、换行和代码围栏不破坏工作流或模型参数', async () => {
|
|
46
|
+
const target = '检查 "路径"\n```ts\nconst value = `${input}`;\n```\n反斜线 \\';
|
|
47
|
+
const prompt = buildSubagentOrchestrationPrompt(3, false, target, { provider: '代理', id: '模型/别名' }, 'max');
|
|
48
|
+
const tasks = await runExample(prompt);
|
|
49
|
+
assert.ok(tasks.every(task => task.task.startsWith(target)));
|
|
50
|
+
assert.ok(tasks.every(task => task.model === '代理/模型/别名:max'));
|
|
51
|
+
assert.doesNotMatch(prompt, /(门禁裁决)/);
|
|
52
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unifan/pi-unifan-zh",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.34",
|
|
4
4
|
"description": "Pi Coding Agent 中文扩展全家桶与独立插件集",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "extensions/sessions/index.ts",
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
"url": "https://github.com/821869798/pi-unifan-zh.git"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"typecheck": "tsc --noEmit"
|
|
12
|
+
"typecheck": "tsc --noEmit",
|
|
13
|
+
"test:commit": "node --test extensions/commit/tests/commit.test.mjs",
|
|
14
|
+
"test:review": "node --test extensions/review/tests/orchestration.test.mjs"
|
|
13
15
|
},
|
|
14
16
|
"pi": {
|
|
15
17
|
"extensions": [
|