@git-ai/cli 1.0.2 → 1.0.4

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.
@@ -1,197 +1,189 @@
1
- import { OPENAI_COMMIT_MESSAGE_TYPES } from "../const.mjs";
2
-
3
- /**
4
- * 获取前几段内容
5
- */
6
- const getFirstParagraphs = (newStr, paragraphCount = 3) => {
7
- const normalizedText = newStr
8
- .replace(/\r\n?|\n/g, "\n")
9
- .replace(/\n<BLANK LINE>\n/g, "\n\n")
10
- .replace(/\n<body>\n/g, "\n\n")
11
- .replace(/\nbody\n/g, "\n\n");
12
- const newStrList = normalizedText.split("\n-");
13
- const str = newStrList
14
- .map((item, index) => {
15
- if (!index) return item;
16
- if (item.startsWith(" ") && item.endsWith("\n")) return item.slice(1, -1);
17
- return item;
18
- })
19
- .join("\n-");
20
- const paragraphs = str
21
- .split(/\n\s*\n/)
22
- .filter((para) => para.trim().length > 0 && !para.startsWith("// "))
23
- .slice(0, paragraphCount);
24
- return paragraphs.join("\n\n");
25
- };
26
-
27
- /**
28
- * 格式化提交消息
29
- */
30
- export const formatMessage = (message, OPENAI_COMMIT_MESSAGE_REGEXP) => {
31
- // 判断是否为推理模型,去掉 think 标签
32
- let newStr = message.includes("</think>")
33
- ? message.replace("</think>", "").trim()
34
- : message.trim();
35
- while (newStr && !OPENAI_COMMIT_MESSAGE_REGEXP.test(newStr)) {
36
- newStr = newStr.slice(1);
37
- }
38
- // 判断是否包含 ```
39
- if (newStr.includes("```")) {
40
- const arr = newStr.split("```").filter((e) => e != "");
41
- newStr = arr[0];
42
- }
43
- if (newStr.includes("---")) {
44
- const arr = newStr.split("---").filter((e) => e != "");
45
- newStr = arr[0];
46
- }
47
- return getFirstParagraphs(getFirstParagraphs(newStr), 2).trim();
48
- };
49
-
50
- /**
51
- * 生成系统消息
52
- */
53
- export const generateSystemMessage = (scope) => {
54
- return `您是一个提交消息生成器,通过差异字符串创建恰好一条提交消息,不添加不必要的信息!以下是来自 https://karma-runner.github.io/6.4/dev/git-commit-msg.html 指南的良好提交消息格式:
55
-
56
- ---
57
- <type>(${scope}): <subject>
58
- <BLANK LINE>
59
- <body>
60
- ---
61
-
62
- (${scope})是固定值不允许修改,允许的 <type> 值有 ${OPENAI_COMMIT_MESSAGE_TYPES.join(
63
- ""
64
- )}。以下是一个良好提交消息的中文示例:
65
-
66
- ---
67
- fix(${scope}): <subject>
68
-
69
- <BLANK LINE>
70
-
71
- <body>
72
- ---`;
73
- };
74
-
75
- /**
76
- * 解析流式数据
77
- */
78
- const parseStreamData = (data) => {
79
- if (!data || typeof data !== "string") return [];
80
- // 标准化换行,兼容 \r\n 并移除多余空白
81
- const lines = data.replace(/\r\n?/g, "\n").split("\n");
82
- const chunks = lines
83
- .map((line) => line.trim())
84
- // 仅保留以 data: 开头的 SSE 行(允许可选空格)
85
- .filter((line) => /^data:\s*/.test(line))
86
- // 提取 data: 之后的负载
87
- .map((line) => line.replace(/^data:\s*/, ""))
88
- // 忽略结束标记
89
- .filter((payload) => payload && payload !== "[DONE]")
90
- // 仅尝试解析形如 JSON 的负载
91
- .map((payload) => {
92
- if (payload[0] !== "{" && payload[0] !== "[") return null;
93
- try {
94
- return JSON.parse(payload);
95
- } catch (e) {
96
- console.error("解析JSON失败:", e);
97
- return null;
98
- }
99
- })
100
- .filter(Boolean); // 过滤掉解析失败的数据
101
-
102
- // 若没有有效块,返回空对象
103
- if (!chunks.length) return {};
104
-
105
- // 将多段 SSE 合并为一个 JSON(聚合 choices.delta.content)
106
- const merged = chunks.reduce((acc, cur) => {
107
- if (!acc) {
108
- acc = {
109
- id: cur.id,
110
- object: "chat.completion",
111
- created: cur.created,
112
- model: cur.model,
113
- system_fingerprint: cur.system_fingerprint,
114
- choices: [],
115
- };
116
- }
117
- // 元信息尽量沿用最新
118
- acc.id = cur.id || acc.id;
119
- acc.created = cur.created || acc.created;
120
- acc.model = cur.model || acc.model;
121
- acc.system_fingerprint = cur.system_fingerprint || acc.system_fingerprint;
122
-
123
- if (Array.isArray(cur.choices)) {
124
- cur.choices.forEach((choice) => {
125
- const idx = typeof choice.index === "number" ? choice.index : 0;
126
- if (!acc.choices[idx]) {
127
- acc.choices[idx] = {
128
- index: idx,
129
- message: { role: "assistant", content: "" },
130
- finish_reason: null,
131
- };
132
- }
133
- const target = acc.choices[idx];
134
-
135
- // 兼容两种返回:stream delta 与 非 stream message
136
- if (choice.delta) {
137
- const { role, content, reasoning_content } = choice.delta;
138
- if (
139
- role &&
140
- (!target.message.role || target.message.role === "assistant")
141
- ) {
142
- target.message.role = role;
143
- }
144
- if (typeof content === "string") {
145
- target.message.content += content;
146
- }
147
- // 如果需要保留推理,可附加在末尾或放入扩展字段
148
- if (typeof reasoning_content === "string") {
149
- target.reasoning_content =
150
- (target.reasoning_content || "") + reasoning_content;
151
- }
152
- }
153
- if (choice.message && typeof choice.message.content === "string") {
154
- // 非流式一次性结果
155
- target.message = {
156
- role: choice.message.role || target.message.role || "assistant",
157
- content: choice.message.content,
158
- };
159
- }
160
- if (choice.finish_reason) {
161
- target.finish_reason = choice.finish_reason;
162
- }
163
- });
164
- }
165
- return acc;
166
- }, null);
167
-
168
- // 清理 undefined 空洞
169
- merged.choices = merged.choices.filter(Boolean);
170
- return merged;
171
- };
172
-
173
- /**
174
- * 格式化完成结果
175
- */
176
- export const formatCompletions = (result) => {
177
- if (typeof result.data === "string") {
178
- const data = parseStreamData(result.data);
179
- // 兼容:如果旧实现返回数组,取最后一项
180
- if (Array.isArray(data)) {
181
- return data[data.length - 1].choices[0].message.content;
182
- }
183
- // 新实现:合并为一个 JSON
184
- if (data && data.choices && data.choices.length) {
185
- const choice = data.choices[0];
186
- if (
187
- choice &&
188
- choice.message &&
189
- typeof choice.message.content === "string"
190
- ) {
191
- return choice.message.content;
192
- }
193
- }
194
- throw new Error("流式结果解析失败,未获取到内容");
195
- }
196
- return result.data.choices[0].message.content;
197
- };
1
+ import { OPENAI_COMMIT_MESSAGE_TYPES } from '../const.mjs';
2
+
3
+ /**
4
+ * 获取前几段内容
5
+ */
6
+ const getFirstParagraphs = (newStr, paragraphCount = 3) => {
7
+ const normalizedText = newStr
8
+ .replace(/\r\n?|\n/g, '\n')
9
+ .replace(/\n<BLANK LINE>\n/g, '\n\n')
10
+ .replace(/\n<body>\n/g, '\n\n')
11
+ .replace(/\nbody\n/g, '\n\n');
12
+ const newStrList = normalizedText.split('\n-');
13
+ const str = newStrList
14
+ .map((item, index) => {
15
+ if (!index) return item;
16
+ if (item.startsWith(' ') && item.endsWith('\n')) return item.slice(1, -1);
17
+ return item;
18
+ })
19
+ .join('\n-');
20
+ const paragraphs = str
21
+ .split(/\n\s*\n/)
22
+ .filter((para) => para.trim().length > 0 && !para.startsWith('// '))
23
+ .slice(0, paragraphCount);
24
+ return paragraphs.join('\n\n');
25
+ };
26
+
27
+ /**
28
+ * 格式化提交消息
29
+ */
30
+ export const formatMessage = (message, OPENAI_COMMIT_MESSAGE_REGEXP) => {
31
+ // 判断是否为推理模型,去掉 think 标签
32
+ let newStr = message.includes('</think>')
33
+ ? message.replace('</think>', '').trim()
34
+ : message.trim();
35
+ while (newStr && !OPENAI_COMMIT_MESSAGE_REGEXP.test(newStr)) {
36
+ newStr = newStr.slice(1);
37
+ }
38
+ // 判断是否包含 ```
39
+ if (newStr.includes('```')) {
40
+ const arr = newStr.split('```').filter((e) => e != '');
41
+ newStr = arr[0];
42
+ }
43
+ if (newStr.includes('---')) {
44
+ const arr = newStr.split('---').filter((e) => e != '');
45
+ newStr = arr[0];
46
+ }
47
+ return getFirstParagraphs(getFirstParagraphs(newStr), 2).trim();
48
+ };
49
+
50
+ /**
51
+ * 生成系统消息
52
+ */
53
+ export const generateSystemMessage = (scope) => {
54
+ return `您是一个提交消息生成器,通过差异字符串创建恰好一条提交消息,不添加不必要的信息!以下是来自 https://karma-runner.github.io/6.4/dev/git-commit-msg.html 指南的良好提交消息格式:
55
+
56
+ ---
57
+ <type>(${scope}): <subject>
58
+ <BLANK LINE>
59
+ <body>
60
+ ---
61
+
62
+ (${scope})是固定值不允许修改,允许的 <type> 值有 ${OPENAI_COMMIT_MESSAGE_TYPES.join(
63
+ ''
64
+ )}。以下是一个良好提交消息的中文示例:
65
+
66
+ ---
67
+ fix(${scope}): <subject>
68
+
69
+ <BLANK LINE>
70
+
71
+ <body>
72
+ ---`;
73
+ };
74
+
75
+ /**
76
+ * 解析流式数据
77
+ */
78
+ const parseStreamData = (data) => {
79
+ if (!data || typeof data !== 'string') return [];
80
+ // 标准化换行,兼容 \r\n 并移除多余空白
81
+ const lines = data.replace(/\r\n?/g, '\n').split('\n');
82
+ const chunks = lines
83
+ .map((line) => line.trim())
84
+ // 仅保留以 data: 开头的 SSE 行(允许可选空格)
85
+ .filter((line) => /^data:\s*/.test(line))
86
+ // 提取 data: 之后的负载
87
+ .map((line) => line.replace(/^data:\s*/, ''))
88
+ // 忽略结束标记
89
+ .filter((payload) => payload && payload !== '[DONE]')
90
+ // 仅尝试解析形如 JSON 的负载
91
+ .map((payload) => {
92
+ if (payload[0] !== '{' && payload[0] !== '[') return null;
93
+ try {
94
+ return JSON.parse(payload);
95
+ } catch (e) {
96
+ console.error('解析JSON失败:', e);
97
+ return null;
98
+ }
99
+ })
100
+ .filter(Boolean); // 过滤掉解析失败的数据
101
+
102
+ // 若没有有效块,返回空对象
103
+ if (!chunks.length) return {};
104
+
105
+ // 将多段 SSE 合并为一个 JSON(聚合 choices.delta.content)
106
+ const merged = chunks.reduce((acc, cur) => {
107
+ if (!acc) {
108
+ acc = {
109
+ id: cur.id,
110
+ object: 'chat.completion',
111
+ created: cur.created,
112
+ model: cur.model,
113
+ system_fingerprint: cur.system_fingerprint,
114
+ choices: [],
115
+ };
116
+ }
117
+ // 元信息尽量沿用最新
118
+ acc.id = cur.id || acc.id;
119
+ acc.created = cur.created || acc.created;
120
+ acc.model = cur.model || acc.model;
121
+ acc.system_fingerprint = cur.system_fingerprint || acc.system_fingerprint;
122
+
123
+ if (Array.isArray(cur.choices)) {
124
+ cur.choices.forEach((choice) => {
125
+ const idx = typeof choice.index === 'number' ? choice.index : 0;
126
+ if (!acc.choices[idx]) {
127
+ acc.choices[idx] = {
128
+ index: idx,
129
+ message: { role: 'assistant', content: '' },
130
+ finish_reason: null,
131
+ };
132
+ }
133
+ const target = acc.choices[idx];
134
+
135
+ // 兼容两种返回:stream delta 与 非 stream message
136
+ if (choice.delta) {
137
+ const { role, content, reasoning_content } = choice.delta;
138
+ if (role && (!target.message.role || target.message.role === 'assistant')) {
139
+ target.message.role = role;
140
+ }
141
+ if (typeof content === 'string') {
142
+ target.message.content += content;
143
+ }
144
+ // 如果需要保留推理,可附加在末尾或放入扩展字段
145
+ if (typeof reasoning_content === 'string') {
146
+ target.reasoning_content = (target.reasoning_content || '') + reasoning_content;
147
+ }
148
+ }
149
+ if (choice.message && typeof choice.message.content === 'string') {
150
+ // 非流式一次性结果
151
+ target.message = {
152
+ role: choice.message.role || target.message.role || 'assistant',
153
+ content: choice.message.content,
154
+ };
155
+ }
156
+ if (choice.finish_reason) {
157
+ target.finish_reason = choice.finish_reason;
158
+ }
159
+ });
160
+ }
161
+ return acc;
162
+ }, null);
163
+
164
+ // 清理 undefined 空洞
165
+ merged.choices = merged.choices.filter(Boolean);
166
+ return merged;
167
+ };
168
+
169
+ /**
170
+ * 格式化完成结果
171
+ */
172
+ export const formatCompletions = (result) => {
173
+ if (typeof result.data === 'string') {
174
+ const data = parseStreamData(result.data);
175
+ // 兼容:如果旧实现返回数组,取最后一项
176
+ if (Array.isArray(data)) {
177
+ return data[data.length - 1].choices[0].message.content;
178
+ }
179
+ // 新实现:合并为一个 JSON
180
+ if (data && data.choices && data.choices.length) {
181
+ const choice = data.choices[0];
182
+ if (choice && choice.message && typeof choice.message.content === 'string') {
183
+ return choice.message.content;
184
+ }
185
+ }
186
+ throw new Error('流式结果解析失败,未获取到内容');
187
+ }
188
+ return result.data.choices[0].message.content;
189
+ };
@@ -1,68 +1,64 @@
1
- import axios from "axios";
2
- import { config } from "./Storage.mjs";
3
- import {
4
- OPENAI_TIMEOUT,
5
- OPENAI_FREE_BASE_URL,
6
- OPENAI_FREE_MODEL_ID,
7
- } from "../const.mjs";
8
- import { getDeviceId, getOpenAiConfig, getRandomItem } from "./Utils.mjs";
9
-
10
- export const headers = {
11
- Accept: "application/json",
12
- "Content-Type": "application/json",
13
- };
14
- export const getFreeModelInfo = async () => {
15
- return axios.get(
16
- `https://api2.immersivetranslate.com/big-model/get-token?deviceId=${getDeviceId()}`
17
- );
18
- };
19
- export const getModelList = (options) => {
20
- const conf = {};
21
- const baseURL = config.get("baseURL").split(",")[0];
22
- const key = config.get("key");
23
- if (key) {
24
- conf["Authorization"] = `Bearer ${key.split(",")[0]}`;
25
- }
26
- return axios({
27
- url: `/models`,
28
- method: "get",
29
- baseURL,
30
- headers: {
31
- ...headers,
32
- ...conf,
33
- },
34
- ...options,
35
- }).then((res) =>
36
- res.data.data
37
- .filter(
38
- (item) =>
39
- !item.id.toLocaleLowerCase().includes("embedding") &&
40
- !item.id.toLocaleLowerCase().includes("reranker")
41
- )
42
- .map((item) => ({ ...item, value: item.id, name: item.id }))
43
- );
44
- };
45
-
46
- export const chat = (data, baseURL, apiKey) => {
47
- const key = apiKey || getOpenAiConfig("key");
48
- const headersConf = { ...headers, Authorization: key ? `Bearer ${key}` : `` };
49
- return axios({
50
- url: `/chat/completions`,
51
- method: "post",
52
- baseURL: baseURL || getOpenAiConfig("baseURL"),
53
- headers: headersConf,
54
- timeout: OPENAI_TIMEOUT,
55
- data: {
56
- think: false,
57
- stream: false,
58
- thinking: { type: "disabled" },
59
- model: data.model || getOpenAiConfig("model"),
60
- ...data,
61
- },
62
- });
63
- };
64
- export const freeChat = async (data) => {
65
- const config = await getFreeModelInfo();
66
- data.model = getRandomItem(OPENAI_FREE_MODEL_ID);
67
- return chat(data, OPENAI_FREE_BASE_URL, config.data.apiToken);
68
- };
1
+ import axios from 'axios';
2
+ import { config } from './Storage.mjs';
3
+ import { OPENAI_TIMEOUT, OPENAI_FREE_BASE_URL, OPENAI_FREE_MODEL_ID } from '../const.mjs';
4
+ import { getDeviceId, getOpenAiConfig, getRandomItem } from './Utils.mjs';
5
+
6
+ export const headers = {
7
+ Accept: 'application/json',
8
+ 'Content-Type': 'application/json',
9
+ };
10
+ export const getFreeModelInfo = async () => {
11
+ return axios.get(
12
+ `https://api2.immersivetranslate.com/big-model/get-token?deviceId=${getDeviceId()}`
13
+ );
14
+ };
15
+ export const getModelList = (options) => {
16
+ const conf = {};
17
+ const baseURL = config.get('baseURL').split(',')[0];
18
+ const key = config.get('key');
19
+ if (key) {
20
+ conf['Authorization'] = `Bearer ${key.split(',')[0]}`;
21
+ }
22
+ return axios({
23
+ url: `/models`,
24
+ method: 'get',
25
+ baseURL,
26
+ headers: {
27
+ ...headers,
28
+ ...conf,
29
+ },
30
+ ...options,
31
+ }).then((res) =>
32
+ res.data.data
33
+ .filter(
34
+ (item) =>
35
+ !item.id.toLocaleLowerCase().includes('embedding') &&
36
+ !item.id.toLocaleLowerCase().includes('reranker')
37
+ )
38
+ .map((item) => ({ ...item, value: item.id, name: item.id }))
39
+ );
40
+ };
41
+
42
+ export const chat = (data, baseURL, apiKey) => {
43
+ const key = apiKey || getOpenAiConfig('key');
44
+ const headersConf = { ...headers, Authorization: key ? `Bearer ${key}` : `` };
45
+ return axios({
46
+ url: `/chat/completions`,
47
+ method: 'post',
48
+ baseURL: baseURL || getOpenAiConfig('baseURL'),
49
+ headers: headersConf,
50
+ timeout: OPENAI_TIMEOUT,
51
+ data: {
52
+ think: false,
53
+ stream: false,
54
+ thinking: { type: 'disabled' },
55
+ model: data.model || getOpenAiConfig('model'),
56
+ ...data,
57
+ },
58
+ });
59
+ };
60
+ export const freeChat = async (data) => {
61
+ const config = await getFreeModelInfo();
62
+ data.model = getRandomItem(OPENAI_FREE_MODEL_ID);
63
+ return chat(data, OPENAI_FREE_BASE_URL, config.data.apiToken);
64
+ };
@@ -1,39 +1,39 @@
1
- import Logger from "./Logger.mjs";
2
- import { collectSpinnerState } from "./Log.mjs";
3
- import cliSpinner from "cli-spinner";
4
- export default class Spinner {
5
- constructor(text = "loading", spinnerString = "|/-\\") {
6
- this.spinner = new cliSpinner.Spinner(text + ".. %s");
7
- this.spinner.setSpinnerString(spinnerString);
8
- }
9
-
10
- start() {
11
- this.spinner.start();
12
- collectSpinnerState("start", this.spinner.text);
13
- return this;
14
- }
15
-
16
- success(text) {
17
- this.stop();
18
- collectSpinnerState("success", text || this.spinner.text);
19
- Logger.success(text);
20
- return this;
21
- }
22
-
23
- error(text) {
24
- this.stop();
25
- collectSpinnerState("error", text || this.spinner.text);
26
- Logger.error(text);
27
- return this;
28
- }
29
- sleep(ms = 1000) {
30
- return new Promise((resolve) => setTimeout(resolve, ms));
31
- }
32
-
33
- stop() {
34
- this.spinner.stop(true);
35
- // 停止
36
- collectSpinnerState("stop", "停止转圈动画输出");
37
- return this;
38
- }
39
- }
1
+ import Logger from './Logger.mjs';
2
+ import { collectSpinnerState } from './Log.mjs';
3
+ import cliSpinner from 'cli-spinner';
4
+ export default class Spinner {
5
+ constructor(text = 'loading', spinnerString = '|/-\\') {
6
+ this.spinner = new cliSpinner.Spinner(text + '.. %s');
7
+ this.spinner.setSpinnerString(spinnerString);
8
+ }
9
+
10
+ start() {
11
+ this.spinner.start();
12
+ collectSpinnerState('start', this.spinner.text);
13
+ return this;
14
+ }
15
+
16
+ success(text) {
17
+ this.stop();
18
+ collectSpinnerState('success', text || this.spinner.text);
19
+ Logger.success(text);
20
+ return this;
21
+ }
22
+
23
+ error(text) {
24
+ this.stop();
25
+ collectSpinnerState('error', text || this.spinner.text);
26
+ Logger.error(text);
27
+ return this;
28
+ }
29
+ sleep(ms = 1000) {
30
+ return new Promise((resolve) => setTimeout(resolve, ms));
31
+ }
32
+
33
+ stop() {
34
+ this.spinner.stop(true);
35
+ // 停止
36
+ collectSpinnerState('stop', '停止转圈动画输出');
37
+ return this;
38
+ }
39
+ }
@@ -1,7 +1,7 @@
1
- import Configstore from "configstore";
2
- import { NAME } from "../const.mjs";
3
- export const config = new Configstore(NAME, {
4
- style: "long",
5
- description: "bullet",
6
- prefix: true,
7
- });
1
+ import Configstore from 'configstore';
2
+ import { NAME } from '../const.mjs';
3
+ export const config = new Configstore(NAME, {
4
+ style: 'long',
5
+ description: 'bullet',
6
+ prefix: true,
7
+ });