@unifan/pi-unifan-zh 1.0.31 → 1.0.33

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_zh.md CHANGED
@@ -122,7 +122,7 @@ pi install npm:@unifan/pi-commit-zh
122
122
 
123
123
  ### 3. 📦 `commit`(智能 Git 提交助手·中文版)
124
124
  - **命令**:
125
- - **`/commit`**:自动分析暂存区/工作区代码变动(`git diff`),生成标准的 Conventional Commits 纯中文提交信息并 1 秒完成提交。
125
+ - **`/commit`**:自动分析暂存区/工作区代码变动(`git diff`),使用当前会话模型,根据实际暂存内容生成标准的 Conventional Commits 纯中文提交信息后提交。模型认证、请求、空结果或输出校验失败时会显示原因并停止提交与推送,保留暂存内容,不再使用固定兜底文案。
126
126
  - **`/commit-push`** 🚀:自动生成中文 Commit 提交,并**自动执行 `git push` 推送**到远端分支。
127
127
  - **常用参数与用法**:
128
128
  - `/commit`:智能提交(有暂存只提暂存,无暂存自动暂存全量)。
@@ -1,7 +1,7 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  commitWithMsg,
4
- getChangedFiles,
4
+ getStagedFiles,
5
5
  getStagedDiff,
6
6
  getUnpushedCommits,
7
7
  getUnstagedDiff,
@@ -35,7 +35,7 @@ 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[],
@@ -55,50 +55,26 @@ async function generateCommitMessage(
55
55
  .filter(Boolean)
56
56
  .join("\n");
57
57
 
58
- if (ctx.model && ctx.modelRegistry) {
59
- try {
60
- const provider = ctx.modelRegistry.getProvider(ctx.model.provider);
61
- if (provider) {
62
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
63
- if (!auth.ok) {
64
- console.error("pi-commit getApiKeyAndHeaders error:", auth.error);
65
- } else {
66
- const response = await provider
67
- .streamSimple(
68
- ctx.model,
69
- {
70
- systemPrompt: COMMIT_SYSTEM_PROMPT,
71
- messages: [
72
- {
73
- role: "user",
74
- content: [{ type: "text", text: userPrompt }],
75
- timestamp: Date.now(),
76
- },
77
- ],
78
- },
79
- { apiKey: auth.apiKey, headers: auth.headers, maxTokens: 800 },
80
- )
81
- .result();
82
-
83
- const text = response.content
84
- ?.map((c) => (c.type === "text" ? c.text : ""))
85
- .join("")
86
- .trim();
87
-
88
- if (text) {
89
- return text.replace(/^```[a-zA-Z]*\n?/, "").replace(/\n?```$/, "").trim();
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} 个文件`;
58
+ if (!ctx.model) throw new Error("当前未选择模型,请先使用 /model 选择模型。");
59
+ if (typeof ctx.modelRegistry?.complete !== "function")
60
+ throw new Error("当前 Pi 不支持统一模型调用接口,请更新 Pi 后重试。");
61
+
62
+ // 由宿主统一解析认证、代理地址及提供方环境,避免手动调用遗漏配置。
63
+ const response = await ctx.modelRegistry.complete(ctx.model, {
64
+ systemPrompt: COMMIT_SYSTEM_PROMPT,
65
+ messages: [{ role: "user", content: [{ type: "text", text: userPrompt }], timestamp: Date.now() }],
66
+ }, { maxTokens: 4096, signal: ctx.signal });
67
+ if (response.stopReason !== "stop")
68
+ throw new Error(`模型未正常完成生成(${response.stopReason}):${response.errorMessage || "请检查模型状态后重试。"}`);
69
+
70
+ const text = response.content.map((c) => c.type === "text" ? c.text : "").join("").trim()
71
+ .replace(/^```[a-zA-Z]*\r?\n?/, "").replace(/\r?\n?```$/, "").trim();
72
+ if (!text) throw new Error("模型返回了空的提交信息。");
73
+ if (!/^(feat|fix|perf|refactor|ci|build|chore|docs|test|style|revert)(\([^()\r\n]+\))?: [^\r\n]+$/.test(text.split(/\r?\n/)[0]))
74
+ throw new Error("模型返回的提交标题不符合约定格式,请重试或补充修改说明。");
75
+ if (text.includes("更新代码与相关配置"))
76
+ throw new Error("模型返回了无具体业务含义的通用提交信息,请补充修改说明后重试。");
77
+ return text;
102
78
  }
103
79
 
104
80
  export default function (pi: ExtensionAPI) {
@@ -116,7 +92,7 @@ export default function (pi: ExtensionAPI) {
116
92
  const parsed = parseArgs(args);
117
93
  let stagedDiff = await getStagedDiff(ctx.cwd);
118
94
  const unstagedDiff = await getUnstagedDiff(ctx.cwd);
119
- const changedFiles = await getChangedFiles(ctx.cwd);
95
+
120
96
 
121
97
  if (!stagedDiff && !unstagedDiff) {
122
98
  if (andPush) {
@@ -163,7 +139,18 @@ export default function (pi: ExtensionAPI) {
163
139
  }
164
140
 
165
141
  notify("正在深度分析代码改动并生成中文 Commit Message...", "info");
166
- const commitMessage = await generateCommitMessage(ctx, stagedDiff || unstagedDiff, changedFiles, parsed.hint);
142
+ let commitMessage: string;
143
+ try {
144
+ if (!stagedDiff.trim()) throw new Error("暂存区没有可用于生成提交信息的差异。");
145
+ const changedFiles = await getStagedFiles(ctx.cwd);
146
+ commitMessage = await generateCommitMessage(ctx, stagedDiff, changedFiles, parsed.hint);
147
+ } catch (error) {
148
+ // 生成失败必须停止,禁止用固定文案继续提交或推送。
149
+ const reason = error instanceof Error ? error.message : String(error);
150
+ notify(`提交信息生成失败,未执行提交或推送:${reason}`, "error");
151
+ pi.sendMessage({ customType: "pi-commit-error", content: `提交信息生成失败,未执行提交或推送。\n\n${reason}\n\n当前暂存内容保留,可修复模型配置后重新执行。`, display: true });
152
+ return;
153
+ }
167
154
 
168
155
  const commitRes = await commitWithMsg(ctx.cwd, commitMessage);
169
156
  if (!commitRes.ok) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unifan/pi-commit-zh",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "Pi 智能 Git 提交助手(规范化 Conventional Commits 纯中文版,支持一键提审与推流)",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -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,74 @@
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
+
10
+ // 在临时目录编译并运行真实命令,不访问开发仓库的提交记录。
11
+ const root = await mkdtemp(join(tmpdir(), 'pi-commit-test-'));
12
+ after(() => rm(root, { recursive: true, force: true }));
13
+ await mkdir(join(root, 'src'));
14
+ await writeFile(join(root, 'package.json'), '{"type":"module"}');
15
+ for (const name of ['index', 'src/git', 'src/prompt']) {
16
+ const source = await readFile(new URL(`../${name}.ts`, import.meta.url), 'utf8');
17
+ const output = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } }).outputText;
18
+ await writeFile(join(root, `${name}.js`), output);
19
+ }
20
+ const { default: register, generateCommitMessage } = await import(pathToFileURL(join(root, 'index.js')));
21
+ const { getStagedFiles } = await import(pathToFileURL(join(root, 'src/git.js')));
22
+ const model = { id: '当前模型', provider: '当前提供方' };
23
+ function context(complete) { return { model, modelRegistry: { complete } }; }
24
+ function response(text, stopReason = 'stop') { return { content: [{ type: 'text', text }], stopReason }; }
25
+
26
+ test('通过宿主统一接口继承当前模型并返回业务日志', async () => {
27
+ const ctx = context(async (actualModel, prompt, options) => {
28
+ assert.equal(actualModel, model);
29
+ assert.match(prompt.messages[0].content[0].text, /修复冲线残留/);
30
+ assert.equal(options.maxTokens, 4096);
31
+ return response('```text\nfix(racing): 清理冲线特效残留\n```');
32
+ });
33
+ assert.equal(await generateCommitMessage(ctx, '+ 清理特效', ['角色.cs'], '修复冲线残留'), 'fix(racing): 清理冲线特效残留');
34
+ });
35
+
36
+ test('拒绝空结果、异常完成、截断、非法格式和通用文案', async () => {
37
+ for (const result of [response(''), response('fix: 修复残留', 'error'), response('fix: 修复残留', 'length'), response('说明文字'), response('chore(GameClient): 更新代码与相关配置')]) {
38
+ await assert.rejects(generateCommitMessage(context(async () => result), 'diff', ['角色.cs']));
39
+ }
40
+ await assert.rejects(generateCommitMessage(context(async () => { throw new Error('认证失败'); }), 'diff', []), /认证失败/);
41
+ await assert.rejects(generateCommitMessage({ modelRegistry: {} }, 'diff', []), /未选择模型/);
42
+ });
43
+
44
+ test('生成失败不提交或推送,文件清单仅包含暂存区', async () => {
45
+ const repo = join(root, 'repo');
46
+ await mkdir(repo);
47
+ const git = (...args) => execFileSync('git', args, { cwd: repo, encoding: 'utf8' }).trim();
48
+ git('init', '-q');
49
+ git('config', 'user.name', '测试');
50
+ git('config', 'user.email', 'test@example.invalid');
51
+ await writeFile(join(repo, '角色 文件.cs'), '初始内容');
52
+ await writeFile(join(repo, '未暂存.cs'), '初始内容');
53
+ git('add', '.');
54
+ git('commit', '-qm', 'test: 初始化');
55
+ const head = git('rev-parse', 'HEAD');
56
+ await writeFile(join(repo, '角色 文件.cs'), '修改内容');
57
+ await writeFile(join(repo, '未暂存.cs'), '不应包含在提交中');
58
+ git('add', '--', '角色 文件.cs');
59
+ assert.deepEqual(await getStagedFiles(repo), ['角色 文件.cs']);
60
+ const commands = new Map();
61
+ const messages = [];
62
+ register({ registerCommand: (name, command) => commands.set(name, command), sendMessage: message => messages.push(message) });
63
+ const ctx = { ...context(async () => { throw new Error('认证失败'); }), cwd: repo, hasUI: true, ui: { notify() {} } };
64
+ await commands.get('commit-push').handler('', ctx);
65
+ assert.equal(git('rev-parse', 'HEAD'), head);
66
+ assert.match(messages.at(-1).content, /未执行提交或推送/);
67
+ assert.match(messages.at(-1).content, /认证失败/);
68
+ assert.deepEqual(await getStagedFiles(repo), ['角色 文件.cs']);
69
+ ctx.modelRegistry.complete = async () => response('fix: 修复角色表现');
70
+ await commands.get('commit').handler('', ctx);
71
+ assert.equal(git('log', '-1', '--format=%B'), 'fix: 修复角色表现');
72
+ assert.notEqual(git('rev-parse', 'HEAD'), head);
73
+ assert.ok(git('diff', '--name-only', '-z').includes('未暂存.cs'));
74
+ });
@@ -7,9 +7,11 @@ systemPromptMode: replace
7
7
  inheritProjectContext: false
8
8
  inheritSkills: false
9
9
  ---
10
+ > 💡【工程环境与上下文声明】:本次审查属于常规受信任的日常软件工程质量保障与健壮性排查,聚焦于业务参数防御性校验与健壮性保障,不涉及任何网络渗透攻击或未经授权的系统操作。
11
+ >
10
12
  > 🚨【最高优先级语言要求】:你的所有思维链(Thinking)、推理分析、状态汇报与最终 Markdown 报告必须 100% 全程使用纯正中文!绝对严禁输出英文段落、英文标题或英文思考!
11
13
 
12
- 你是**代码安全与合规审查专家**。你的任务是审查本次代码改动是否存在防御性不足、边界校验缺失或配置安全隐患。
14
+ 你是**代码健壮性与合规审查专家**。你的任务是审查本次代码改动是否存在防御性不足、边界校验缺失或配置安全隐患。
13
15
 
14
16
  ## 审查重点
15
17
  1. **配置合规**:排查是否存在意外提交的私有凭证、明文密钥或不安全的默认配置。
@@ -243,7 +243,10 @@ const FOLDER_REVIEW_PROMPT =
243
243
  "请对以下目录/文件路径的代码进行快照审查:{paths}。直接读取这些文件并结合上下文直接输出审查发现。严禁输出任何步骤清单。所有输出必须使用纯正中文。";
244
244
 
245
245
  // 权威的中文代码审查准则 (基于 Codex 准则精炼与本土化)
246
- const REVIEW_RUBRIC = `> 🚨【最高执行原则】:
246
+ const REVIEW_RUBRIC = `> 💡【工程环境与上下文声明】:
247
+ > 本次代码审查属于常规受信任的日常软件工程质量保障流程。排查内容聚焦于业务逻辑健壮性、边界异常与运行时性能,不涉及任何越权利用、网络渗透或未经授权的操作。
248
+ >
249
+ > 🚨【最高执行原则】:
247
250
  > 1. **全流程纯中文**:思维链(Thinking)、推理分析与最终报告必须 100% 使用纯正中文,严禁包含任何英文段落或英文小标题!
248
251
  > 2. **结合上下文,拒绝断章取义**:仅看 diff 的片段容易产生误判。请**先运行 \`git diff\` 锁定变动,再针对改动涉及的关键方法、类生命周期或调用方,按需使用 \`read\` 或 \`grep\` 查阅必要的上下文代码**(精准核验,切勿无目的漫游遍历),结合真实完整的业务上下文做出准确裁决!
249
252
  > 3. **直接审查,拒绝繁琐步骤**:严禁打印任何工作流待办清单(Checklist)或环境测试命令!
@@ -1390,12 +1393,14 @@ export default function reviewExtension(pi: ExtensionAPI) {
1390
1393
  });
1391
1394
 
1392
1395
  // 审查总结专用提示词
1393
- const REVIEW_SUMMARY_PROMPT = `> 🚨【语言指令】:总结报告必须 100% 使用纯正中文,严禁使用英文!
1396
+ const REVIEW_SUMMARY_PROMPT = `> 💡【工程环境与上下文声明】:
1397
+ > 本次总结属于受信任的日常软件工程开发流程(代码审查总结归档)。
1398
+ > 语言要求:全流程使用纯正中文。
1394
1399
 
1395
1400
  我们即将结束代码审查并切回开发主对话。
1396
1401
  请将本次代码审查分支中发现的所有核心问题、缺陷与建议生成一份结构清晰的中文整改总结。
1397
1402
 
1398
- 必须严格按以下格式输出总结(确保原样保留文件路径、行号与缺陷等级):
1403
+ 请按以下格式输出总结(确保原样保留文件路径、行号与缺陷等级):
1399
1404
 
1400
1405
  ## 待办修复清单
1401
1406
  1. [需优先解决的 P0/P1 问题]
@@ -1575,8 +1580,12 @@ export default function reviewExtension(pi: ExtensionAPI) {
1575
1580
  });
1576
1581
 
1577
1582
  // 针对【代码开发会话·修复方】的提示词模板
1578
- const DEV_FIX_PROMPT_TEMPLATE = `> 语言要求:全流程使用纯正中文。
1579
- > 提示:无需执行 git commit 或 push,只需修改代码并说明,由用户确认后自行提交。
1583
+ const DEV_FIX_PROMPT_TEMPLATE = `> 💡【工程环境与上下文声明】:
1584
+ > 本次交互属于受信任的日常软件工程开发流程(代码审查与缺陷整改)。
1585
+ > 审查涉及的模块为常规应用业务逻辑(如图形渲染、特效生命周期、状态机同步与判空保护等),属于正常的日常软件工程研发活动,不涉及任何网络安全攻击、越权渗透或未经授权的系统变更。请作为资深软件研发专家协助开发者客观核验代码。
1586
+ >
1587
+ > 语言要求:全流程使用纯正中文。
1588
+ > 协作约定:本次交互聚焦于代码逻辑核验与局部整改,修改完成后向开发者说明变更,由开发者确认后自行提交版本。
1580
1589
 
1581
1590
  # 代码审查意见整改与回复
1582
1591
 
@@ -1585,10 +1594,10 @@ export default function reviewExtension(pi: ExtensionAPI) {
1585
1594
  {content}
1586
1595
  ---
1587
1596
 
1588
- ## 核心处理原则:
1589
- 1. **有道理的就修改,并总结修改了啥**:结合代码上下文逐条仔细核验。确实存在缺陷或隐患的,直接修改代码并保证逻辑自洽;在总结中明确说明修改了哪些文件、函数及具体改了什么。
1590
- 2. **没有道理的就在总结里解释**:若某些意见属于误报、不符合实际业务场景或既有架构已有防护,请在总结中清晰解释为什么没有道理,无需修改。
1591
- 3. **输出整改报告**(供复核人员查验):在最终回复末尾,严格按以下格式总结:
1597
+ ## 处理指引:
1598
+ 1. **客观核验与代码整改**:结合上下文代码核对上述意见。确实存在缺陷或隐患的,直接进行代码修改并保证逻辑自洽;在总结中明确说明修改了哪些文件、函数及具体改了什么。
1599
+ 2. **技术甄别与误报说明**:若某些意见属于误报、不符合实际业务场景或既有架构已有防护,请在总结中清晰解释技术原因,无需盲目修改。
1600
+ 3. **输出整改报告**(供复核人员查验):在最终回复末尾,按以下格式总结:
1592
1601
 
1593
1602
  ### 🛠️ 整改与回复报告
1594
1603
  - **[已修复] [问题标题]**
@@ -1601,8 +1610,10 @@ export default function reviewExtension(pi: ExtensionAPI) {
1601
1610
  `;
1602
1611
 
1603
1612
  // 针对【审查复核会话·审查方】的提示词模板
1604
- const REVIEWER_RECHECK_PROMPT_TEMPLATE = `> 语言要求:全流程使用纯正中文。
1605
- > 提示:当前为只读复核模式,请勿修改代码或执行提交。
1613
+ const REVIEWER_RECHECK_PROMPT_TEMPLATE = `> 💡【工程环境与上下文声明】:
1614
+ > 本次交互属于受信任的日常软件工程代码复核流程。
1615
+ > 语言要求:全流程使用纯正中文。
1616
+ > 协作约定:当前为只读复核模式,请查阅 diff 与代码实现核验整改效果,不执行代码写入或提交。
1606
1617
 
1607
1618
  # 复核开发者的代码修复与整改说明
1608
1619
 
@@ -1611,9 +1622,8 @@ export default function reviewExtension(pi: ExtensionAPI) {
1611
1622
  {content}
1612
1623
  ---
1613
1624
 
1614
- ## 核心复核原则:
1615
- 1. **不要只听信总结报告,必须实际去看改了啥**:
1616
- - 绝不能因为报告声称“已修复”就盲目相信。必须立即运行 \`git diff\` 亲眼核对实际改动代码,并按需使用 \`read\` 查看改动前后的完整方法与类上下文。
1625
+ ## 复核指引:
1626
+ 1. **核验实际代码改动**:运行 \`git diff\` 亲眼核对实际改动代码,并按需使用 \`read\` 查看改动前后的完整方法与类上下文。
1617
1627
  2. **逐项实事求是核验**:
1618
1628
  - 对照开发者声称“已修复”的项,检查实际 diff 是否真正彻底解决了缺陷,且未引入新的逻辑隐患或并发风险。
1619
1629
  - 对照开发者声称“无需修改”的解释,客观评估其理由是否合乎技术事实。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unifan/pi-review-zh",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
4
4
  "description": "Pi 交互式 AI 代码审查扩展(支持单模型与多 Subagent 并发 2~6 专家自由切换,Codex 经典架构)",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unifan/pi-unifan-zh",
3
- "version": "1.0.31",
3
+ "version": "1.0.33",
4
4
  "description": "Pi Coding Agent 中文扩展全家桶与独立插件集",
5
5
  "type": "module",
6
6
  "main": "extensions/sessions/index.ts",
@@ -9,7 +9,8 @@
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"
13
14
  },
14
15
  "pi": {
15
16
  "extensions": [