@unifan/pi-unifan-zh 1.0.32 → 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.14",
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
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unifan/pi-review-zh",
3
- "version": "1.0.24",
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.32",
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": [