@git-ai/cli 1.0.0
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 +369 -0
- package/bin/index.cjs +15 -0
- package/bin/index.mjs +41 -0
- package/package.json +46 -0
- package/src/actions/BaseAction.mjs +44 -0
- package/src/actions/BaseUrlAction.mjs +24 -0
- package/src/actions/CommitAction.mjs +484 -0
- package/src/actions/MaxTokenAction.mjs +38 -0
- package/src/actions/ModelAction.mjs +131 -0
- package/src/actions/SelectModelAction.mjs +161 -0
- package/src/actions/TokenAction.mjs +25 -0
- package/src/const.mjs +41 -0
- package/src/index.mjs +69 -0
- package/src/services/AIService.mjs +113 -0
- package/src/services/GitService.mjs +388 -0
- package/src/utils/ConflictUtils.mjs +39 -0
- package/src/utils/Log.mjs +146 -0
- package/src/utils/Logger.mjs +34 -0
- package/src/utils/MessageUtils.mjs +197 -0
- package/src/utils/OpenAI.mjs +67 -0
- package/src/utils/Spinner.mjs +39 -0
- package/src/utils/Storage.mjs +7 -0
- package/src/utils/Utils.mjs +72 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { promises as fs } from "fs";
|
|
3
|
+
import { isAbsolute, resolve } from "path";
|
|
4
|
+
import { chat } from "../utils/OpenAI.mjs";
|
|
5
|
+
import { config } from "../utils/Storage.mjs";
|
|
6
|
+
import inquirer from "inquirer";
|
|
7
|
+
import { formatCompletions } from "../utils/MessageUtils.mjs";
|
|
8
|
+
import { OPENAI_MODEL_LIST_URL } from "../const.mjs";
|
|
9
|
+
import Logger from "../utils/Logger.mjs";
|
|
10
|
+
import Spinner from "../utils/Spinner.mjs";
|
|
11
|
+
import BaseAction from "./BaseAction.mjs";
|
|
12
|
+
|
|
13
|
+
class SelectModelAction extends BaseAction {
|
|
14
|
+
async execute() {
|
|
15
|
+
await this.setModel();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async getModelList() {
|
|
19
|
+
if (!OPENAI_MODEL_LIST_URL) {
|
|
20
|
+
Logger.notice(`需要配置环境变量 \`OPENAI_MODEL_LIST_URL\` 。
|
|
21
|
+
此变量指定 OpenAI 模型列表的来源,支持 HTTP(S) URL 或本地文件路径。
|
|
22
|
+
|
|
23
|
+
** OPENAI_MODEL_LIST_URL 示例:**
|
|
24
|
+
* URL: \`https://raw.githubusercontent.com/xx025/carrot/main/model_list.json\`
|
|
25
|
+
* Linux/macOS 路径: \`/path/to/your/local/model_list.json\`
|
|
26
|
+
* Windows 路径: \`C:\\Path\\To\\Your\\Local\\model_list.json\`
|
|
27
|
+
|
|
28
|
+
** JSON 配置示例:**
|
|
29
|
+
模型id、baseURL、key 配置多个会随机取,必填(id、baseURL)、非必填(keys)
|
|
30
|
+
\`\`\`json
|
|
31
|
+
{
|
|
32
|
+
"data": [
|
|
33
|
+
{
|
|
34
|
+
"id": "modelId,modelId,modelId",
|
|
35
|
+
"baseURL": "baseURL,baseURL,baseURL",
|
|
36
|
+
"keys": "key,key,key"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
\`\`\`
|
|
41
|
+
`);
|
|
42
|
+
throw "未配置环境变量 `OPENAI_MODEL_LIST_URL`";
|
|
43
|
+
}
|
|
44
|
+
const source = OPENAI_MODEL_LIST_URL;
|
|
45
|
+
const isHttpSource = /^https?:\/\//i.test(source);
|
|
46
|
+
const spinner = new Spinner("正在加载模型列表...");
|
|
47
|
+
spinner.start();
|
|
48
|
+
try {
|
|
49
|
+
if (isHttpSource) {
|
|
50
|
+
const { data } = await axios.get(source);
|
|
51
|
+
spinner.stop();
|
|
52
|
+
return data;
|
|
53
|
+
}
|
|
54
|
+
const filePath = isAbsolute(source)
|
|
55
|
+
? source
|
|
56
|
+
: resolve(process.cwd(), source);
|
|
57
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
58
|
+
spinner.stop();
|
|
59
|
+
return JSON.parse(content);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
spinner.stop();
|
|
62
|
+
throw error && error.message ? error.message : error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async selectModels() {
|
|
67
|
+
const modelConfig = {
|
|
68
|
+
list: [],
|
|
69
|
+
origin: [],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
modelConfig.origin = (await this.getModelList()).data || [];
|
|
74
|
+
modelConfig.list = modelConfig.origin.map((item) => item.id);
|
|
75
|
+
if (!modelConfig.list.length) {
|
|
76
|
+
throw "未找到可用的模型";
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw error && error.message ? error.message : error;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const answers = await inquirer.prompt([
|
|
83
|
+
{
|
|
84
|
+
type: "rawlist",
|
|
85
|
+
name: "model",
|
|
86
|
+
message: `请选择模型`,
|
|
87
|
+
choices: modelConfig.list,
|
|
88
|
+
default: "",
|
|
89
|
+
loop: false,
|
|
90
|
+
},
|
|
91
|
+
]);
|
|
92
|
+
const item = modelConfig.origin.find((item) => item.id === answers.model);
|
|
93
|
+
if (!item.baseURL || !item.id) {
|
|
94
|
+
throw `模型信息不完整: 请检查 \`baseURL\` 和 \`id\``;
|
|
95
|
+
}
|
|
96
|
+
config.set("baseURL", item.baseURL);
|
|
97
|
+
config.set("model", item.id);
|
|
98
|
+
config.set("key", item.keys);
|
|
99
|
+
Logger.success(
|
|
100
|
+
`已设置模型: \n - ${item.id.split(`,`).join("\n - ")}`
|
|
101
|
+
);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
throw `设置模型失败: ${error && error.message ? error.message : error}`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// 验证模型是否可用
|
|
107
|
+
async validateModel() {
|
|
108
|
+
const spinner = new Spinner("正在验证模型是否可用...");
|
|
109
|
+
spinner.start();
|
|
110
|
+
|
|
111
|
+
const startTimestamp = Date.now();
|
|
112
|
+
const configModel = config.get("model")
|
|
113
|
+
? config.get("model").split(",")
|
|
114
|
+
: [];
|
|
115
|
+
const total = configModel.length;
|
|
116
|
+
const errTotal = [];
|
|
117
|
+
while (configModel.length) {
|
|
118
|
+
const model = configModel.shift();
|
|
119
|
+
try {
|
|
120
|
+
const result = await chat({
|
|
121
|
+
model: model,
|
|
122
|
+
messages: [
|
|
123
|
+
{
|
|
124
|
+
role: "user",
|
|
125
|
+
content: "1+1=?",
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
});
|
|
129
|
+
formatCompletions(result);
|
|
130
|
+
} catch {
|
|
131
|
+
errTotal.push(model);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
spinner.stop();
|
|
135
|
+
if (total - errTotal.length > 0) {
|
|
136
|
+
Logger.success(`模型验证通过 ${total - errTotal.length} 个`);
|
|
137
|
+
}
|
|
138
|
+
if (errTotal.length) {
|
|
139
|
+
Logger.error(
|
|
140
|
+
`模型验证失败 ${errTotal.length} 个,分别是:\n${errTotal.join(
|
|
141
|
+
"\n - "
|
|
142
|
+
)}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const endTimestamp = Date.now();
|
|
147
|
+
const duration = endTimestamp - startTimestamp;
|
|
148
|
+
|
|
149
|
+
Logger.success(`本次检查模型用时: ${(duration / 1000).toFixed(3)} 秒`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async setModel() {
|
|
153
|
+
await this.selectModels();
|
|
154
|
+
await this.validateModel();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export default async function () {
|
|
159
|
+
const action = new SelectModelAction();
|
|
160
|
+
await action.run();
|
|
161
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { config } from "../utils/Storage.mjs";
|
|
2
|
+
import Logger from "../utils/Logger.mjs";
|
|
3
|
+
import { formatToken } from "../utils/Utils.mjs";
|
|
4
|
+
import BaseAction from "./BaseAction.mjs";
|
|
5
|
+
|
|
6
|
+
class SetTokenAction extends BaseAction {
|
|
7
|
+
constructor(key) {
|
|
8
|
+
super(key);
|
|
9
|
+
this.key = key || "";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async execute() {
|
|
13
|
+
config.set("key", this.key);
|
|
14
|
+
if (this.key) {
|
|
15
|
+
Logger.success(`OpenAI Api Key 已设置: ${formatToken(this.key)}`);
|
|
16
|
+
} else {
|
|
17
|
+
Logger.warn(`OpenAI Api Key 已设置为空`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default async function (key) {
|
|
23
|
+
const action = new SetTokenAction(key);
|
|
24
|
+
await action.run();
|
|
25
|
+
}
|
package/src/const.mjs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { resolve } from 'path';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
// 读取 package.json 文件
|
|
4
|
+
const pkgPath = resolve(process.argv[1], '../../package.json');
|
|
5
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
6
|
+
export const BIN = Object.keys(pkg.bin)[0].split('-').join(' ')
|
|
7
|
+
export const NAME = pkg.name
|
|
8
|
+
export const DESCRIPTION = pkg.description
|
|
9
|
+
export const VERSION = pkg.version
|
|
10
|
+
export const AUTHOR = pkg.author
|
|
11
|
+
export const LOWEST_NODE_VERSION = pkg.engines.node
|
|
12
|
+
export const OPENAI_BASE_URL = "" //"https://api.siliconflow.cn/v1"
|
|
13
|
+
export const OPENAI_API_KEYS_URL = "" //"https://cloud.siliconflow.cn/me/account/ak"
|
|
14
|
+
export const OPENAI_MODELS_URL = "" //"https://cloud.siliconflow.cn/me/models"
|
|
15
|
+
export const OPENAI_MODEL_DEFAULT = "" // "Qwen/Qwen2.5-Coder-7B-Instruct"
|
|
16
|
+
export const OPENAI_TIMEOUT = 60 * 1000 * 3
|
|
17
|
+
export const OPENAI_MAX_TOKEN_DEFAULT = 128000
|
|
18
|
+
export const OPENAI_MODEL_LIST_URL = process.env.OPENAI_MODEL_LIST_URL
|
|
19
|
+
export const OPENAI_FREE_BASE_URL = "https://open.bigmodel.cn/api/paas/v4"
|
|
20
|
+
export const OPENAI_FREE_MODEL_ID = "glm-4-flash-250414,glm-4.5-flash"
|
|
21
|
+
export const OPENAI_COMMIT_MESSAGE_TYPES = [
|
|
22
|
+
'feat',
|
|
23
|
+
'fix',
|
|
24
|
+
'docs',
|
|
25
|
+
'style',
|
|
26
|
+
'refactor',
|
|
27
|
+
'perf',
|
|
28
|
+
'test',
|
|
29
|
+
'build',
|
|
30
|
+
'ci',
|
|
31
|
+
'chore',
|
|
32
|
+
'revert',
|
|
33
|
+
]
|
|
34
|
+
export const asciiArt = `
|
|
35
|
+
██████╗ ██╗████████╗ █████╗ ██╗
|
|
36
|
+
██╔════╝ ██║╚══██╔══╝ ██╔══██╗██║
|
|
37
|
+
██║ ███╗██║ ██║ ███████║██║
|
|
38
|
+
██║ ██║██║ ██║ ██╔══██║██║
|
|
39
|
+
╚██████╔╝██║ ██║ ██║ ██║██║
|
|
40
|
+
╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝
|
|
41
|
+
`
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import CommitAction from './actions/CommitAction.mjs';
|
|
3
|
+
import ModelAction from './actions/ModelAction.mjs';
|
|
4
|
+
import TokenAction from './actions/TokenAction.mjs';
|
|
5
|
+
import BaseUrlAction from './actions/BaseUrlAction.mjs';
|
|
6
|
+
import MaxTokenAction from './actions/MaxTokenAction.mjs';
|
|
7
|
+
import { exitProcess } from './utils/Utils.mjs';
|
|
8
|
+
import SelectModelAction from './actions/SelectModelAction.mjs';
|
|
9
|
+
import { BIN, VERSION, DESCRIPTION, NAME, OPENAI_MAX_TOKEN_DEFAULT, asciiArt } from './const.mjs';
|
|
10
|
+
import Logger from './utils/Logger.mjs';
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
|
|
13
|
+
let startTimestamp;
|
|
14
|
+
function registerCommand() {
|
|
15
|
+
const program = new Command();
|
|
16
|
+
program
|
|
17
|
+
.name(BIN)
|
|
18
|
+
// .description(`${asciiArt}`)
|
|
19
|
+
// .description(`${asciiArt}\n${DESCRIPTION}`)
|
|
20
|
+
.version(VERSION)
|
|
21
|
+
.helpOption('-h', chalk.cyan(`显示帮助信息 (baseURL、model、token 分别支持多个,英文逗号隔开)`));
|
|
22
|
+
program
|
|
23
|
+
.command('set-baseURL [baseURL]')
|
|
24
|
+
.description(`设置 OpenAI compatible Base URL,请执行 ${chalk.cyan('`' + BIN + ' set-baseURL [baseURL]`')}\n如:https://api.siliconflow.cn/v1`)
|
|
25
|
+
.action(BaseUrlAction);
|
|
26
|
+
program
|
|
27
|
+
.command('set-key [key]')
|
|
28
|
+
.description(`设置 OpenAI compatible Api Key,请执行 ${chalk.cyan('`' + BIN + ' set-key [key]`')}`)
|
|
29
|
+
.action(TokenAction);
|
|
30
|
+
program
|
|
31
|
+
.command('set-model [model]')
|
|
32
|
+
.description(`设置您的模型,请执行 ${chalk.cyan('`' + BIN + ' set-model [model]`')}`)
|
|
33
|
+
.action(ModelAction);
|
|
34
|
+
program
|
|
35
|
+
.command('set-max-token <maxToken>')
|
|
36
|
+
.description(`设置您的最大 token 数,默认:${OPENAI_MAX_TOKEN_DEFAULT}(${Math.round(OPENAI_MAX_TOKEN_DEFAULT / 1000)}k)`)
|
|
37
|
+
.action(MaxTokenAction);
|
|
38
|
+
program
|
|
39
|
+
.command('select-model')
|
|
40
|
+
.description(`从模型列表选择,请执行 ${chalk.cyan('`' + BIN + ' select-model`')}`)
|
|
41
|
+
.action(SelectModelAction);
|
|
42
|
+
program
|
|
43
|
+
.option('-d, --dry-run', `等同于 ${chalk.cyan('`git commit --dry-run -m <message>`')}`)
|
|
44
|
+
.option('-e, --allow-empty', `等同于 ${chalk.cyan('`git commit --allow-empty -m <message>`')}`)
|
|
45
|
+
.option('-n, --no-verify', `等同于 ${chalk.cyan('`git commit --no-verify -m <message>`')}`)
|
|
46
|
+
.option('-s, --skip', `跳过 ${chalk.cyan('`git add`')} 命令, 只提交已暂存的更改,如:${chalk.cyan('`' + BIN + ' --skip\`')}`)
|
|
47
|
+
.description(`${DESCRIPTION}\n${asciiArt}\n使用 AI 自动生成提交信息并推送
|
|
48
|
+
→ ${chalk.cyan('检查环境')} → ${chalk.cyan('检查目录')} → ${chalk.cyan('检查冲突')} → ${chalk.cyan('处理合并')}\n → ${chalk.cyan('git add')} → ${chalk.cyan('获取diff')} → ${chalk.cyan('AI 生成 commit message')}\n → ${chalk.cyan('git fetch')} → ${chalk.cyan('git merge')} → ${chalk.cyan('检查冲突')} → ${chalk.cyan('git push')}`)
|
|
49
|
+
.action(CommitAction);
|
|
50
|
+
program.parse();
|
|
51
|
+
}
|
|
52
|
+
async function prepare() {
|
|
53
|
+
// 第一步检查版本号
|
|
54
|
+
checkPkgVersion();
|
|
55
|
+
}
|
|
56
|
+
function checkPkgVersion() {
|
|
57
|
+
Logger.verbose(`${NAME}@${VERSION}`);
|
|
58
|
+
}
|
|
59
|
+
export default async function core() {
|
|
60
|
+
startTimestamp = Date.now()
|
|
61
|
+
try {
|
|
62
|
+
await prepare();
|
|
63
|
+
// 命令注册
|
|
64
|
+
registerCommand()
|
|
65
|
+
} catch (e) {
|
|
66
|
+
Logger.error(e.message);
|
|
67
|
+
exitProcess(1, startTimestamp);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { chat, freeChat } from "../utils/OpenAI.mjs";
|
|
2
|
+
import {
|
|
3
|
+
generateSystemMessage,
|
|
4
|
+
formatCompletions,
|
|
5
|
+
} from "../utils/MessageUtils.mjs";
|
|
6
|
+
import { config } from "../utils/Storage.mjs";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
/**
|
|
9
|
+
* AI 服务类
|
|
10
|
+
* 封装所有 AI 相关操作
|
|
11
|
+
*/
|
|
12
|
+
export class AIService {
|
|
13
|
+
constructor(userName) {
|
|
14
|
+
this.userName = userName;
|
|
15
|
+
this.usageMessage = "";
|
|
16
|
+
}
|
|
17
|
+
// 检查是否配置 baseUrl model
|
|
18
|
+
checkConfig() {
|
|
19
|
+
const baseURL = config.get("baseURL");
|
|
20
|
+
const key = config.get("key");
|
|
21
|
+
return baseURL && key;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 生成提交消息
|
|
26
|
+
*/
|
|
27
|
+
async generateCommitMessage(diffString) {
|
|
28
|
+
const request = this.checkConfig() ? chat : freeChat;
|
|
29
|
+
const result = await request({
|
|
30
|
+
messages: [
|
|
31
|
+
{
|
|
32
|
+
role: "system",
|
|
33
|
+
content: generateSystemMessage(this.userName),
|
|
34
|
+
},
|
|
35
|
+
{ role: "user", content: diffString },
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
if (result.data && result.data.error && !result.data.choices) {
|
|
39
|
+
throw result.data.error;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (result.data.usage) {
|
|
43
|
+
this.usageMessage = chalk.underline(
|
|
44
|
+
`本次模型消耗统计:总数 ${result.data.usage.total_tokens} tokens、输入 ${result.data.usage.prompt_tokens} tokens、输出 ${result.data.usage.completion_tokens} tokens`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return formatCompletions(result);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 让 AI 帮忙分析 git commit 失败原因
|
|
53
|
+
* @param {Object} payload
|
|
54
|
+
* @param {string} payload.errorMessage git commit 抛出的错误
|
|
55
|
+
* @param {string} payload.gitStatus 当前 git status 输出
|
|
56
|
+
* @param {string} payload.hookLogs 如果有 hook 输出,可传入
|
|
57
|
+
*/
|
|
58
|
+
async analyzeCommitFailure({
|
|
59
|
+
errorMessage = "",
|
|
60
|
+
gitStatus = "",
|
|
61
|
+
hookLogs = "",
|
|
62
|
+
} = {}) {
|
|
63
|
+
this.usageMessage = "";
|
|
64
|
+
const request = this.checkConfig() ? chat : freeChat;
|
|
65
|
+
const contextParts = [];
|
|
66
|
+
if (errorMessage) {
|
|
67
|
+
contextParts.push(`Git 命令报错:\n${errorMessage}`);
|
|
68
|
+
}
|
|
69
|
+
if (gitStatus) {
|
|
70
|
+
contextParts.push(`git status 输出:\n${gitStatus}`);
|
|
71
|
+
}
|
|
72
|
+
if (hookLogs) {
|
|
73
|
+
contextParts.push(`Hook 输出:\n${hookLogs}`);
|
|
74
|
+
}
|
|
75
|
+
const userPrompt = contextParts.length
|
|
76
|
+
? contextParts.join("\n\n")
|
|
77
|
+
: "未提供额外日志,只知道 git commit 失败。";
|
|
78
|
+
|
|
79
|
+
const result = await request({
|
|
80
|
+
messages: [
|
|
81
|
+
{
|
|
82
|
+
role: "system",
|
|
83
|
+
content:
|
|
84
|
+
"你是一个资深的 Git 专家,请分析用户提供的 git commit 失败日志,输出造成失败的最可能原因,并给出简洁的排查或修复步骤。请使用中文回答,结构可以是:原因 + 处理建议。",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
role: "user",
|
|
88
|
+
content: userPrompt,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
temperature: 0.2,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (result.data && result.data.usage) {
|
|
95
|
+
this.usageMessage = chalk.underline(
|
|
96
|
+
`本次模型消耗统计:总数 ${result.data.usage.total_tokens} tokens、输入 ${result.data.usage.prompt_tokens} tokens、输出 ${result.data.usage.completion_tokens} tokens`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (result.data && result.data.error && !result.data.choices) {
|
|
101
|
+
throw result.data.error;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return formatCompletions(result);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 获取使用统计信息
|
|
109
|
+
*/
|
|
110
|
+
getUsageMessage() {
|
|
111
|
+
return this.usageMessage;
|
|
112
|
+
}
|
|
113
|
+
}
|