@ww_nero/mini-cli 1.0.56
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/bin/mini.js +3 -0
- package/package.json +38 -0
- package/src/chat.js +1008 -0
- package/src/config.js +371 -0
- package/src/index.js +38 -0
- package/src/llm.js +147 -0
- package/src/prompt/tool.js +18 -0
- package/src/request.js +328 -0
- package/src/tools/bash.js +241 -0
- package/src/tools/convert.js +297 -0
- package/src/tools/index.js +66 -0
- package/src/tools/mcp.js +478 -0
- package/src/tools/python/html_to_png.py +100 -0
- package/src/tools/python/html_to_pptx.py +163 -0
- package/src/tools/python/pdf_to_png.py +58 -0
- package/src/tools/python/pptx_to_pdf.py +107 -0
- package/src/tools/read.js +44 -0
- package/src/tools/replace.js +135 -0
- package/src/tools/todos.js +90 -0
- package/src/tools/write.js +52 -0
- package/src/utils/cliOptions.js +8 -0
- package/src/utils/commands.js +89 -0
- package/src/utils/git.js +89 -0
- package/src/utils/helpers.js +93 -0
- package/src/utils/history.js +181 -0
- package/src/utils/model.js +127 -0
- package/src/utils/output.js +76 -0
- package/src/utils/renderer.js +92 -0
- package/src/utils/settings.js +90 -0
- package/src/utils/think.js +211 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
|
|
3
|
+
const SLASH_COMMANDS = [
|
|
4
|
+
{ key: 'model', value: '/model', description: '查看或切换模型' },
|
|
5
|
+
{ key: 'clear', value: '/clear', description: '清空上下文' },
|
|
6
|
+
{ key: 'resume', value: '/resume', description: '查看或恢复历史会话' },
|
|
7
|
+
{ key: 'exit', value: '/exit', description: '退出对话' }
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const getSlashCommandListText = () => SLASH_COMMANDS.map((entry) => entry.value).join('、');
|
|
11
|
+
|
|
12
|
+
const createCommandHandler = ({ modelManager, resetMessages, closeSession, handleResume }) => {
|
|
13
|
+
const handleClearCommand = () => {
|
|
14
|
+
resetMessages();
|
|
15
|
+
console.log(chalk.green('已清空上下文。'));
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const handleModelCommand = (rawArgs = '') => {
|
|
19
|
+
const args = rawArgs.trim();
|
|
20
|
+
if (!args || ['list', 'ls'].includes(args.toLowerCase())) {
|
|
21
|
+
modelManager.listModels();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const numeric = Number.parseInt(args, 10);
|
|
26
|
+
if (!Number.isNaN(numeric) && String(numeric) === args) {
|
|
27
|
+
const index = numeric - 1;
|
|
28
|
+
if (!modelManager.setActiveModel(index, { updateDefault: true, announce: true })) {
|
|
29
|
+
console.error(chalk.red(`序号 ${args} 不在 1-${modelManager.getTotalModels()} 范围内,已保持 ${modelManager.getDefaultModelDescription()}。`));
|
|
30
|
+
modelManager.resetToDefault();
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
modelManager.applyModelByName(args, { source: 'command', announceSuccess: true, showError: true });
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const handleResumeCommand = (rawArgs = '') => {
|
|
39
|
+
if (typeof handleResume === 'function') {
|
|
40
|
+
handleResume(rawArgs);
|
|
41
|
+
} else {
|
|
42
|
+
console.log(chalk.yellow('当前构建未启用历史记录功能。'));
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const handleSlashCommand = (raw = '') => {
|
|
47
|
+
if (!raw.startsWith('/')) {
|
|
48
|
+
return { handled: false, shouldExit: false };
|
|
49
|
+
}
|
|
50
|
+
const trimmed = raw.slice(1).trim();
|
|
51
|
+
const availableCommandsText = getSlashCommandListText() || '/model、/clear、/exit';
|
|
52
|
+
if (!trimmed) {
|
|
53
|
+
console.log(chalk.yellow(`请输入完整命令,例如 ${availableCommandsText}。`));
|
|
54
|
+
return { handled: true, shouldExit: false };
|
|
55
|
+
}
|
|
56
|
+
const [command] = trimmed.split(' ');
|
|
57
|
+
const args = trimmed.slice(command.length).trim();
|
|
58
|
+
switch ((command || '').toLowerCase()) {
|
|
59
|
+
case 'model':
|
|
60
|
+
handleModelCommand(args);
|
|
61
|
+
return { handled: true, shouldExit: false };
|
|
62
|
+
case 'clear':
|
|
63
|
+
handleClearCommand();
|
|
64
|
+
return { handled: true, shouldExit: false };
|
|
65
|
+
case 'resume':
|
|
66
|
+
handleResumeCommand(args);
|
|
67
|
+
return { handled: true, shouldExit: false };
|
|
68
|
+
case 'exit':
|
|
69
|
+
closeSession();
|
|
70
|
+
return { handled: true, shouldExit: true };
|
|
71
|
+
default:
|
|
72
|
+
console.log(chalk.yellow(`未识别的命令 /${command},可用命令:${availableCommandsText}。`));
|
|
73
|
+
return { handled: true, shouldExit: false };
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
handleSlashCommand,
|
|
79
|
+
handleClearCommand,
|
|
80
|
+
handleModelCommand,
|
|
81
|
+
handleResumeCommand
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
SLASH_COMMANDS,
|
|
87
|
+
getSlashCommandListText,
|
|
88
|
+
createCommandHandler
|
|
89
|
+
};
|
package/src/utils/git.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_GITIGNORE_ENTRIES = [
|
|
7
|
+
'venv',
|
|
8
|
+
'node_modules',
|
|
9
|
+
'logs',
|
|
10
|
+
'build',
|
|
11
|
+
'coverage',
|
|
12
|
+
'*.log',
|
|
13
|
+
'*.tmp',
|
|
14
|
+
'*.temp',
|
|
15
|
+
'*.DS_Store',
|
|
16
|
+
'Thumbs.db',
|
|
17
|
+
'package-lock.json',
|
|
18
|
+
'dist',
|
|
19
|
+
'.idea',
|
|
20
|
+
'*.pyc'
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const ensureGitRepository = (workspaceRoot) => {
|
|
24
|
+
if (!workspaceRoot) {
|
|
25
|
+
return { gitInitialized: false, gitignoreCreated: false };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const gitDirPath = path.join(workspaceRoot, '.git');
|
|
29
|
+
const gitignorePath = path.join(workspaceRoot, '.gitignore');
|
|
30
|
+
let gitInitialized = false;
|
|
31
|
+
let gitignoreCreated = false;
|
|
32
|
+
let initialCommitCreated = false;
|
|
33
|
+
|
|
34
|
+
if (!fs.existsSync(gitDirPath)) {
|
|
35
|
+
try {
|
|
36
|
+
execSync('git init', { cwd: workspaceRoot, stdio: 'ignore' });
|
|
37
|
+
gitInitialized = true;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.log(chalk.yellow(`自动初始化 Git 仓库失败:${error.message}`));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
44
|
+
try {
|
|
45
|
+
const content = DEFAULT_GITIGNORE_ENTRIES.join('\n') + '\n';
|
|
46
|
+
fs.writeFileSync(gitignorePath, content, 'utf8');
|
|
47
|
+
gitignoreCreated = true;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.log(chalk.yellow(`创建 .gitignore 失败:${error.message}`));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 如果完成了 git init 或 .gitignore 创建,检查是否需要初始提交
|
|
54
|
+
if ((gitInitialized || gitignoreCreated) && fs.existsSync(gitDirPath)) {
|
|
55
|
+
try {
|
|
56
|
+
// 检查是否有任何提交
|
|
57
|
+
const hasCommits = (() => {
|
|
58
|
+
try {
|
|
59
|
+
execSync('git rev-parse HEAD', { cwd: workspaceRoot, stdio: 'ignore' });
|
|
60
|
+
return true;
|
|
61
|
+
} catch (_) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
})();
|
|
65
|
+
|
|
66
|
+
if (!hasCommits) {
|
|
67
|
+
// 添加所有文件并创建初始提交
|
|
68
|
+
execSync('git add .', { cwd: workspaceRoot, stdio: 'ignore' });
|
|
69
|
+
execSync('git commit -m "init project"', { cwd: workspaceRoot, stdio: 'ignore' });
|
|
70
|
+
initialCommitCreated = true;
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.log(chalk.yellow(`创建初始提交失败:${error.message}`));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
gitInitialized,
|
|
79
|
+
gitignoreCreated,
|
|
80
|
+
initialCommitCreated,
|
|
81
|
+
gitignorePath,
|
|
82
|
+
gitDirPath
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
ensureGitRepository,
|
|
88
|
+
DEFAULT_GITIGNORE_ENTRIES
|
|
89
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const fsPromises = require('fs/promises');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const normalizeLineBreaks = (text = '') => {
|
|
5
|
+
if (!text) return '';
|
|
6
|
+
const normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
7
|
+
return /^\n*$/.test(normalized) ? '' : normalized;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const resolveWorkspacePath = (workspaceRoot, targetPath = '.', options = {}) => {
|
|
11
|
+
if (!workspaceRoot) {
|
|
12
|
+
throw new Error('未找到工作区目录');
|
|
13
|
+
}
|
|
14
|
+
const base = path.resolve(workspaceRoot);
|
|
15
|
+
const resolved = path.resolve(base, targetPath || '.');
|
|
16
|
+
const relative = path.relative(base, resolved);
|
|
17
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
18
|
+
throw new Error('路径必须位于当前工作区内');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!options.allowGit) {
|
|
22
|
+
const segments = relative.split(path.sep).filter(Boolean);
|
|
23
|
+
if (segments.includes('.git')) {
|
|
24
|
+
throw new Error('不允许操作 .git 目录');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return resolved;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const ensureDirectory = async (filePath) => {
|
|
32
|
+
const dir = path.dirname(filePath);
|
|
33
|
+
await fsPromises.mkdir(dir, { recursive: true });
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const readTextFile = async (filePath) => {
|
|
37
|
+
try {
|
|
38
|
+
return await fsPromises.readFile(filePath, 'utf8');
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error.code === 'ENOENT') {
|
|
41
|
+
throw new Error('文件不存在');
|
|
42
|
+
}
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const writeTextFile = async (filePath, content) => {
|
|
48
|
+
await ensureDirectory(filePath);
|
|
49
|
+
await fsPromises.writeFile(filePath, content, 'utf8');
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const isCodeIdentical = (prev, next) => {
|
|
53
|
+
return normalizeLineBreaks(prev) === normalizeLineBreaks(next);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const processContent = (content) => {
|
|
57
|
+
if (!content) return '';
|
|
58
|
+
let processed = content.replace(/\r/g, '\n');
|
|
59
|
+
processed = processed
|
|
60
|
+
.split('\n')
|
|
61
|
+
.map(line => (line.trim() === '' ? '' : line))
|
|
62
|
+
.join('\n');
|
|
63
|
+
processed = processed.replace(/^\n+|\n+$/g, '');
|
|
64
|
+
processed = processed.replace(/\n{3,}/g, '\n\n');
|
|
65
|
+
return processed + '\n';
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const safeJsonParse = (text) => {
|
|
69
|
+
if (!text || typeof text !== 'string') return null;
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(text);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
try {
|
|
74
|
+
const parts = text.split('}{');
|
|
75
|
+
if (parts.length > 1) {
|
|
76
|
+
return JSON.parse(parts[0] + '}');
|
|
77
|
+
}
|
|
78
|
+
} catch (_) {
|
|
79
|
+
// ignore secondary error
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
normalizeLineBreaks,
|
|
87
|
+
resolveWorkspacePath,
|
|
88
|
+
readTextFile,
|
|
89
|
+
writeTextFile,
|
|
90
|
+
isCodeIdentical,
|
|
91
|
+
processContent,
|
|
92
|
+
safeJsonParse
|
|
93
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
|
|
6
|
+
const MAX_HISTORY_RECORDS = 50;
|
|
7
|
+
const HISTORY_BASE_DIR = path.join(os.homedir(), '.mini', 'cli');
|
|
8
|
+
const HISTORY_FILENAME_PATTERN = /^\d{14}\.json$/;
|
|
9
|
+
|
|
10
|
+
const resolveProjectPath = (projectPath) => path.resolve(projectPath || process.cwd());
|
|
11
|
+
|
|
12
|
+
const getProjectMd5 = (projectPath) => (
|
|
13
|
+
crypto
|
|
14
|
+
.createHash('md5')
|
|
15
|
+
.update(resolveProjectPath(projectPath), 'utf8')
|
|
16
|
+
.digest('hex')
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const getProjectHistoryDir = (projectPath) => path.join(HISTORY_BASE_DIR, getProjectMd5(projectPath));
|
|
20
|
+
|
|
21
|
+
const ensureHistoryDir = (projectPath) => {
|
|
22
|
+
const dir = getProjectHistoryDir(projectPath);
|
|
23
|
+
if (!fs.existsSync(dir)) {
|
|
24
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
return dir;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const pad = (value, length = 2) => String(value).padStart(length, '0');
|
|
30
|
+
|
|
31
|
+
const formatTimestamp = (date = new Date()) => {
|
|
32
|
+
const year = date.getFullYear();
|
|
33
|
+
const month = pad(date.getMonth() + 1);
|
|
34
|
+
const day = pad(date.getDate());
|
|
35
|
+
const hour = pad(date.getHours());
|
|
36
|
+
const minute = pad(date.getMinutes());
|
|
37
|
+
const second = pad(date.getSeconds());
|
|
38
|
+
return `${year}${month}${day}${hour}${minute}${second}`;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const createHistoryFilePath = (projectPath) => {
|
|
42
|
+
const historyDir = ensureHistoryDir(projectPath);
|
|
43
|
+
let attempt = new Date();
|
|
44
|
+
const maxAttempts = 120;
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < maxAttempts; i += 1) {
|
|
47
|
+
const name = `${formatTimestamp(attempt)}.json`;
|
|
48
|
+
const filePath = path.join(historyDir, name);
|
|
49
|
+
if (!fs.existsSync(filePath)) {
|
|
50
|
+
return filePath;
|
|
51
|
+
}
|
|
52
|
+
attempt = new Date(attempt.getTime() + 1000);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
throw new Error('无法生成唯一的历史记录文件名');
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const normalizeMessagesPayload = (payload) => {
|
|
59
|
+
if (Array.isArray(payload)) {
|
|
60
|
+
return payload;
|
|
61
|
+
}
|
|
62
|
+
if (payload && typeof payload === 'object' && Array.isArray(payload.messages)) {
|
|
63
|
+
return payload.messages;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const saveHistoryMessages = (filePath, messages) => {
|
|
69
|
+
if (!filePath || !Array.isArray(messages)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const dir = path.dirname(filePath);
|
|
73
|
+
if (!fs.existsSync(dir)) {
|
|
74
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
fs.writeFileSync(filePath, `${JSON.stringify(messages, null, 2)}\n`, 'utf8');
|
|
77
|
+
return true;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const readHistoryMessages = (filePath) => {
|
|
81
|
+
try {
|
|
82
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
83
|
+
return normalizeMessagesPayload(JSON.parse(raw));
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const listHistoryFiles = (projectPath) => {
|
|
90
|
+
const historyDir = ensureHistoryDir(projectPath);
|
|
91
|
+
try {
|
|
92
|
+
const entries = fs.readdirSync(historyDir, { withFileTypes: true });
|
|
93
|
+
return entries
|
|
94
|
+
.filter((entry) => entry.isFile() && HISTORY_FILENAME_PATTERN.test(entry.name))
|
|
95
|
+
.sort((a, b) => b.name.localeCompare(a.name))
|
|
96
|
+
.map((entry) => ({
|
|
97
|
+
name: entry.name,
|
|
98
|
+
filePath: path.join(historyDir, entry.name)
|
|
99
|
+
}));
|
|
100
|
+
} catch (error) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const trimHistoryFiles = (maxEntries = MAX_HISTORY_RECORDS, projectPath) => {
|
|
106
|
+
if (!Number.isFinite(maxEntries) || maxEntries <= 0) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
const files = listHistoryFiles(projectPath);
|
|
110
|
+
if (files.length <= maxEntries) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
const toRemove = files.slice(maxEntries);
|
|
114
|
+
toRemove.forEach((entry) => {
|
|
115
|
+
try {
|
|
116
|
+
fs.unlinkSync(entry.filePath);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// ignore deletion errors
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
return toRemove.map((entry) => entry.filePath);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const refreshHistoryFilePath = (filePath, projectPath) => {
|
|
125
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
if (!fs.existsSync(filePath)) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const dir = ensureHistoryDir(projectPath);
|
|
132
|
+
if (path.dirname(filePath) !== dir) {
|
|
133
|
+
return filePath;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const nextPath = createHistoryFilePath(projectPath);
|
|
137
|
+
fs.renameSync(filePath, nextPath);
|
|
138
|
+
return nextPath;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
return filePath;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const extractFirstUserQuestion = (messages) => {
|
|
145
|
+
if (!Array.isArray(messages)) {
|
|
146
|
+
return '';
|
|
147
|
+
}
|
|
148
|
+
const firstUserMessage = messages.find((msg) => msg && msg.role === 'user');
|
|
149
|
+
const text = firstUserMessage && typeof firstUserMessage.content === 'string'
|
|
150
|
+
? firstUserMessage.content.trim()
|
|
151
|
+
: '';
|
|
152
|
+
return text;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const formatPreviewText = (text, maxLength = 20) => {
|
|
156
|
+
if (!text) {
|
|
157
|
+
return '(空问题)';
|
|
158
|
+
}
|
|
159
|
+
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
160
|
+
if (normalized.length <= maxLength) {
|
|
161
|
+
return normalized;
|
|
162
|
+
}
|
|
163
|
+
return `${normalized.slice(0, maxLength)}...`;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
HISTORY_BASE_DIR,
|
|
168
|
+
MAX_HISTORY_RECORDS,
|
|
169
|
+
getProjectMd5,
|
|
170
|
+
getProjectHistoryDir,
|
|
171
|
+
ensureHistoryDir,
|
|
172
|
+
createHistoryFilePath,
|
|
173
|
+
saveHistoryMessages,
|
|
174
|
+
readHistoryMessages,
|
|
175
|
+
listHistoryFiles,
|
|
176
|
+
trimHistoryFiles,
|
|
177
|
+
refreshHistoryFilePath,
|
|
178
|
+
extractFirstUserQuestion,
|
|
179
|
+
formatPreviewText,
|
|
180
|
+
formatTimestamp
|
|
181
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const { promoteEndpointInConfig } = require('../config');
|
|
3
|
+
|
|
4
|
+
const describeModel = (endpoint) => {
|
|
5
|
+
if (!endpoint) {
|
|
6
|
+
return '未知模型';
|
|
7
|
+
}
|
|
8
|
+
const alias = typeof endpoint.name === 'string' ? endpoint.name.trim() : '';
|
|
9
|
+
const label = alias || endpoint.model;
|
|
10
|
+
if (alias && alias !== endpoint.model) {
|
|
11
|
+
return `${label} (${endpoint.model})`;
|
|
12
|
+
}
|
|
13
|
+
return label;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const normalizeModelInput = (value = '') => {
|
|
17
|
+
const trimmed = value.trim();
|
|
18
|
+
if (!trimmed) {
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
const quote = trimmed[0];
|
|
22
|
+
if ((quote === '"' || quote === '\'') && trimmed.endsWith(quote) && trimmed.length > 1) {
|
|
23
|
+
return trimmed.slice(1, -1).trim();
|
|
24
|
+
}
|
|
25
|
+
return trimmed;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const findModelIndexByName = (endpoints, name = '') => {
|
|
29
|
+
const normalized = name.toLowerCase();
|
|
30
|
+
return endpoints.findIndex((endpoint) => {
|
|
31
|
+
const candidates = [endpoint.name, endpoint.model];
|
|
32
|
+
return candidates.some((candidate) => typeof candidate === 'string' && candidate.toLowerCase() === normalized);
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const createModelManager = (endpoints, { configPath } = {}) => {
|
|
37
|
+
let defaultEndpointIndex = 0;
|
|
38
|
+
let activeEndpointIndex = 0;
|
|
39
|
+
|
|
40
|
+
const getActiveEndpoint = () => endpoints[activeEndpointIndex];
|
|
41
|
+
const getDefaultEndpoint = () => endpoints[defaultEndpointIndex];
|
|
42
|
+
const getTotalModels = () => endpoints.length;
|
|
43
|
+
const getDefaultModelDescription = () => describeModel(getDefaultEndpoint());
|
|
44
|
+
|
|
45
|
+
const resetToDefault = () => {
|
|
46
|
+
setActiveModel(defaultEndpointIndex, { updateDefault: false, announce: false, persistConfig: false });
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const setActiveModel = (
|
|
50
|
+
index,
|
|
51
|
+
{ updateDefault = false, announce = true, announcePrefix = '已切换至', reason = '', persistConfig = true } = {}
|
|
52
|
+
) => {
|
|
53
|
+
if (index < 0 || index >= endpoints.length) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
activeEndpointIndex = index;
|
|
57
|
+
if (updateDefault) {
|
|
58
|
+
defaultEndpointIndex = index;
|
|
59
|
+
if (persistConfig && configPath) {
|
|
60
|
+
const endpoint = endpoints[index];
|
|
61
|
+
if (endpoint && endpoint.signature) {
|
|
62
|
+
promoteEndpointInConfig(configPath, endpoint.signature);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (announce) {
|
|
67
|
+
console.log(chalk.green(`${announcePrefix}模型 ${describeModel(endpoints[index])}`));
|
|
68
|
+
if (reason) {
|
|
69
|
+
console.log(chalk.gray(reason));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const applyModelByName = (rawName, { source = 'command', announceSuccess = true, showError = true } = {}) => {
|
|
76
|
+
const normalizedInput = normalizeModelInput(rawName || '');
|
|
77
|
+
if (!normalizedInput) {
|
|
78
|
+
if (showError) {
|
|
79
|
+
console.log(chalk.yellow('请输入要切换的模型名称。'));
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const index = findModelIndexByName(endpoints, normalizedInput);
|
|
84
|
+
if (index === -1) {
|
|
85
|
+
if (showError) {
|
|
86
|
+
console.error(chalk.red(`未找到模型 ${normalizedInput},已保持 ${describeModel(getDefaultEndpoint())}。`));
|
|
87
|
+
}
|
|
88
|
+
setActiveModel(defaultEndpointIndex, { updateDefault: false, announce: false, persistConfig: false });
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const announcePrefix = source === 'cli' ? '已应用启动' : '已切换至';
|
|
92
|
+
setActiveModel(index, { updateDefault: true, announce: announceSuccess, announcePrefix });
|
|
93
|
+
return true;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const listModels = () => {
|
|
97
|
+
console.log(chalk.gray('可用模型列表:'));
|
|
98
|
+
endpoints.forEach((endpoint, idx) => {
|
|
99
|
+
const markers = [];
|
|
100
|
+
if (idx === activeEndpointIndex) {
|
|
101
|
+
markers.push(chalk.green('当前'));
|
|
102
|
+
}
|
|
103
|
+
if (idx === defaultEndpointIndex) {
|
|
104
|
+
markers.push(chalk.cyan('默认'));
|
|
105
|
+
}
|
|
106
|
+
const markerText = markers.length > 0 ? ` [${markers.join('/')}]` : '';
|
|
107
|
+
console.log(` ${idx + 1}. ${describeModel(endpoint)}${markerText}`);
|
|
108
|
+
});
|
|
109
|
+
console.log(chalk.gray('使用 /model <名称|序号> 切换模型。'));
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
getActiveEndpoint,
|
|
114
|
+
getDefaultEndpoint,
|
|
115
|
+
getTotalModels,
|
|
116
|
+
getDefaultModelDescription,
|
|
117
|
+
resetToDefault,
|
|
118
|
+
setActiveModel,
|
|
119
|
+
applyModelByName,
|
|
120
|
+
listModels
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
describeModel,
|
|
126
|
+
createModelManager
|
|
127
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const { splitThinkContent, summarizeReasoning } = require('./think');
|
|
3
|
+
|
|
4
|
+
const mergeReasoningContent = (...segments) => {
|
|
5
|
+
const normalized = segments
|
|
6
|
+
.map((segment) => (typeof segment === 'string' ? segment.trim() : ''))
|
|
7
|
+
.filter(Boolean);
|
|
8
|
+
if (normalized.length === 0) {
|
|
9
|
+
return '';
|
|
10
|
+
}
|
|
11
|
+
return normalized.join('\n');
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const printReasoningSummary = (text, ensureNewline) => {
|
|
15
|
+
if (!text) return;
|
|
16
|
+
ensureNewline();
|
|
17
|
+
console.log(chalk.gray(`思考:${summarizeReasoning(text)}`));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const printAssistantContent = (text, ensureNewline) => {
|
|
21
|
+
if (typeof text !== 'string' || !text) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
ensureNewline();
|
|
25
|
+
const output = chalk.blue(text);
|
|
26
|
+
process.stdout.write(output);
|
|
27
|
+
if (!text.endsWith('\n')) {
|
|
28
|
+
process.stdout.write('\n');
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const renderAssistantOutput = (reasoning, content, ensureNewline) => {
|
|
33
|
+
printReasoningSummary(reasoning, ensureNewline);
|
|
34
|
+
printAssistantContent(content, ensureNewline);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const sanitizeContentWithThink = (primary = '', fallback = '') => {
|
|
38
|
+
const normalizedPrimary = typeof primary === 'string' ? primary.trim() : '';
|
|
39
|
+
if (normalizedPrimary) {
|
|
40
|
+
return normalizedPrimary;
|
|
41
|
+
}
|
|
42
|
+
if (typeof fallback !== 'string' || !fallback.trim()) {
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
const { content } = splitThinkContent(fallback);
|
|
46
|
+
return (content || '').trim();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const formatTokensToK = (totalTokens) => {
|
|
50
|
+
if (typeof totalTokens !== 'number' || Number.isNaN(totalTokens)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return `${(totalTokens / 1000).toFixed(1)}k`;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const printUsageTokens = (usage) => {
|
|
57
|
+
if (!usage || typeof usage !== 'object') {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const totalTokens = usage.total_tokens;
|
|
61
|
+
const formatted = formatTokensToK(totalTokens);
|
|
62
|
+
if (!formatted) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const colorFn = typeof chalk.hex === 'function' ? chalk.hex('#9A32CD') : chalk.magenta;
|
|
66
|
+
console.log(colorFn(`已使用tokens: ${formatted}`));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
mergeReasoningContent,
|
|
71
|
+
printReasoningSummary,
|
|
72
|
+
printAssistantContent,
|
|
73
|
+
renderAssistantOutput,
|
|
74
|
+
sanitizeContentWithThink,
|
|
75
|
+
printUsageTokens
|
|
76
|
+
};
|