kie-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/dist/models.js ADDED
@@ -0,0 +1,223 @@
1
+ import chalk from 'chalk';
2
+ import select from '@inquirer/select';
3
+ /** Kie Claude 兼容接口默认模型 */
4
+ export const DEFAULT_CHAT_MODEL = 'claude-opus-4-7';
5
+ export const CONFIG_KEY_CHAT_MODEL = 'chatModel';
6
+ export const CONFIG_KEY_IMAGE_MODEL = 'imageModel';
7
+ export { DEFAULT_IMAGE_MODEL, getImageModel, setImageModel } from './image-models.js';
8
+ export function getProviderFamily(provider) {
9
+ if (provider === 'openai')
10
+ return 'GPT';
11
+ if (provider === 'codex')
12
+ return 'Codex';
13
+ return 'Claude';
14
+ }
15
+ /** 内置推荐模型(亦可通过 config 设置任意 model id) */
16
+ export const CHAT_MODELS = [
17
+ {
18
+ id: 'claude-opus-4-7',
19
+ label: 'Claude Opus 4.7',
20
+ provider: 'claude',
21
+ tier: 'flagship',
22
+ description: '最强推理与代码能力,适合复杂任务',
23
+ },
24
+ {
25
+ id: 'claude-opus-4-6',
26
+ label: 'Claude Opus 4.6',
27
+ provider: 'claude',
28
+ tier: 'flagship',
29
+ description: '上一代旗舰模型',
30
+ },
31
+ {
32
+ id: 'claude-sonnet-4-6',
33
+ label: 'Claude Sonnet 4.6',
34
+ provider: 'claude',
35
+ tier: 'balanced',
36
+ description: '速度与质量均衡,适合日常开发',
37
+ },
38
+ {
39
+ id: 'claude-sonnet-4-5',
40
+ label: 'Claude Sonnet 4.5',
41
+ provider: 'claude',
42
+ tier: 'balanced',
43
+ description: 'Sonnet 4.5,日常开发推荐',
44
+ },
45
+ {
46
+ id: 'claude-haiku-4-5',
47
+ label: 'Claude Haiku 4.5',
48
+ provider: 'claude',
49
+ tier: 'fast',
50
+ description: '响应更快、成本更低,适合简单问答',
51
+ },
52
+ {
53
+ id: 'gpt-5-2',
54
+ label: 'GPT 5.2',
55
+ provider: 'openai',
56
+ apiSegment: 'gpt-5-2',
57
+ tier: 'flagship',
58
+ description: 'Chat Completions 接口(CLI 暂为纯文本)',
59
+ },
60
+ {
61
+ id: 'gpt-5-4',
62
+ label: 'GPT 5.4',
63
+ provider: 'codex',
64
+ responsesPath: '/codex/v1/responses',
65
+ tier: 'flagship',
66
+ description: 'Codex Responses(/codex/v1/responses)',
67
+ },
68
+ {
69
+ id: 'gpt-5-5',
70
+ label: 'GPT 5.5',
71
+ provider: 'codex',
72
+ responsesPath: '/codex/v1/responses',
73
+ tier: 'flagship',
74
+ description: 'Codex Responses(/codex/v1/responses)',
75
+ },
76
+ {
77
+ id: 'gpt-5.1-codex',
78
+ label: 'GPT 5.1 Codex',
79
+ provider: 'codex',
80
+ responsesPath: '/api/v1/responses',
81
+ tier: 'flagship',
82
+ description: 'Responses API(/api/v1/responses)',
83
+ },
84
+ ];
85
+ export function resolveProvider(modelId) {
86
+ const known = findChatModel(modelId);
87
+ if (known)
88
+ return known.provider;
89
+ if (/codex/i.test(modelId))
90
+ return 'codex';
91
+ if (modelId === 'gpt-5-2')
92
+ return 'openai';
93
+ if (/^gpt-5-/i.test(modelId))
94
+ return 'codex';
95
+ if (/^gpt-/i.test(modelId))
96
+ return 'openai';
97
+ return 'claude';
98
+ }
99
+ /** Responses API 端点(按模型区分 /codex/v1/responses 与 /api/v1/responses) */
100
+ export function getResponsesEndpoint(baseUrl, modelId) {
101
+ const trimmed = baseUrl.replace(/\/+$/, '');
102
+ const known = findChatModel(modelId);
103
+ let suffix = known?.responsesPath;
104
+ if (!suffix) {
105
+ suffix = /codex/i.test(modelId) ? '/api/v1/responses' : '/codex/v1/responses';
106
+ }
107
+ if (trimmed.endsWith(suffix))
108
+ return trimmed;
109
+ return `${trimmed}${suffix}`;
110
+ }
111
+ /** @deprecated 使用 getResponsesEndpoint(baseUrl, modelId) */
112
+ export function getCodexResponsesEndpoint(baseUrl, modelId) {
113
+ return getResponsesEndpoint(baseUrl, modelId ?? 'gpt-5-4');
114
+ }
115
+ export function getOpenAIChatEndpoint(baseUrl, modelId) {
116
+ const trimmed = baseUrl.replace(/\/+$/, '');
117
+ const segment = findChatModel(modelId)?.apiSegment ?? modelId;
118
+ const suffix = `/${segment}/v1/chat/completions`;
119
+ if (trimmed.endsWith(suffix))
120
+ return trimmed;
121
+ return `${trimmed}${suffix}`;
122
+ }
123
+ export function getChatModel(config) {
124
+ return config.get(CONFIG_KEY_CHAT_MODEL) || DEFAULT_CHAT_MODEL;
125
+ }
126
+ export function setChatModel(config, modelId) {
127
+ config.set(CONFIG_KEY_CHAT_MODEL, modelId.trim());
128
+ }
129
+ /** CLI 参数 > 当前会话 > 全局配置 > 默认值 */
130
+ export function resolveChatModel(config, overrides) {
131
+ return overrides?.cli?.trim() || overrides?.session?.trim() || getChatModel(config);
132
+ }
133
+ export function findChatModel(modelId) {
134
+ return CHAT_MODELS.find((m) => m.id === modelId);
135
+ }
136
+ export function formatModelLabel(modelId) {
137
+ const known = findChatModel(modelId);
138
+ if (!known)
139
+ return modelId;
140
+ const tag = getProviderFamily(known.provider);
141
+ return `${known.label} (${modelId}) [${tag}]`;
142
+ }
143
+ function providerTag(provider) {
144
+ if (provider === 'openai')
145
+ return chalk.yellow('GPT');
146
+ if (provider === 'codex')
147
+ return chalk.magenta('Codex');
148
+ return chalk.blue('Claude');
149
+ }
150
+ export function maskSecret(value, visible = 4) {
151
+ if (!value || value.length <= visible * 2)
152
+ return '****';
153
+ return `${value.slice(0, visible)}****${value.slice(-visible)}`;
154
+ }
155
+ /** 交互式选择模型(↑↓ 移动,Enter 确认) */
156
+ export async function promptSelectChatModel(currentModelId, message = '选择对话 Chat 模型 (↑↓ 移动,Enter 确认)') {
157
+ const choices = CHAT_MODELS.map((m) => {
158
+ const tierTag = m.tier === 'flagship' ? '旗舰' : m.tier === 'balanced' ? '均衡' : '快速';
159
+ const family = getProviderFamily(m.provider);
160
+ return {
161
+ name: `[${family}] ${m.label} (${m.id}) — ${tierTag}`,
162
+ value: m.id,
163
+ description: m.description,
164
+ };
165
+ });
166
+ if (!findChatModel(currentModelId)) {
167
+ choices.unshift({
168
+ name: `当前自定义: ${currentModelId}`,
169
+ value: currentModelId,
170
+ description: '当前正在使用',
171
+ });
172
+ }
173
+ return select({
174
+ message,
175
+ choices,
176
+ default: currentModelId,
177
+ pageSize: Math.min(choices.length, 12),
178
+ });
179
+ }
180
+ /** REPL 内选模型:打印序号菜单(不占用 stdin,避免与 readline 冲突) */
181
+ export function printModelPickerMenu(currentModelId) {
182
+ console.log(chalk.yellow('\n选择对话模型 (Chat) — 输入序号或模型 ID,0 取消:\n'));
183
+ CHAT_MODELS.forEach((m, i) => {
184
+ const current = m.id === currentModelId ? chalk.green(' ← 当前') : '';
185
+ const family = getProviderFamily(m.provider);
186
+ console.log(` ${chalk.cyan(String(i + 1).padStart(2))}. [${family}] ${chalk.bold(m.id)} — ${m.label}${current}`);
187
+ });
188
+ console.log(chalk.gray('\n 0. 取消\n'));
189
+ }
190
+ export function resolveModelPickInput(input) {
191
+ const trimmed = input.trim();
192
+ if (!trimmed || trimmed === '0')
193
+ return null;
194
+ const n = Number.parseInt(trimmed, 10);
195
+ if (!Number.isNaN(n) && n >= 1 && n <= CHAT_MODELS.length) {
196
+ return CHAT_MODELS[n - 1].id;
197
+ }
198
+ return trimmed;
199
+ }
200
+ export function printChatModels(currentModelId) {
201
+ console.log(chalk.yellow('\n可用对话模型 (Chat,用于 kie chat):\n'));
202
+ for (const m of CHAT_MODELS) {
203
+ const current = m.id === currentModelId;
204
+ const marker = current ? chalk.green('● ') : chalk.gray(' ');
205
+ const tier = m.tier === 'flagship'
206
+ ? chalk.magenta('旗舰')
207
+ : m.tier === 'balanced'
208
+ ? chalk.blue('均衡')
209
+ : chalk.cyan('快速');
210
+ console.log(`${marker}${providerTag(m.provider)} ${chalk.bold(m.id.padEnd(20))} ${m.label.padEnd(16)} ${tier} ${chalk.dim(m.description)}`);
211
+ }
212
+ if (!findChatModel(currentModelId)) {
213
+ console.log(chalk.gray(`\n当前使用自定义模型: ${chalk.cyan(currentModelId)}`));
214
+ }
215
+ console.log(chalk.gray('\n切换: kie models | /model | kie config --set-model <id>\n'));
216
+ }
217
+ export function getClaudeMessagesEndpoint(baseUrl) {
218
+ const trimmed = baseUrl.replace(/\/+$/, '');
219
+ if (trimmed.endsWith('/claude/v1/messages')) {
220
+ return trimmed;
221
+ }
222
+ return `${trimmed}/claude/v1/messages`;
223
+ }
@@ -0,0 +1,63 @@
1
+ import { kiePostStream } from '../kie-http.js';
2
+ import { getClaudeMessagesEndpoint } from '../models.js';
3
+ export async function sendClaudeChat(options) {
4
+ const { apiKey, baseUrl, messages, model, onChunk, maxTokens = 4096, thinkingFlag = true, signal, } = options;
5
+ const endpoint = getClaudeMessagesEndpoint(baseUrl);
6
+ const streamBody = await kiePostStream(endpoint, {
7
+ apiKey,
8
+ signal,
9
+ body: {
10
+ model,
11
+ messages,
12
+ thinkingFlag,
13
+ max_tokens: maxTokens,
14
+ stream: true,
15
+ },
16
+ });
17
+ const reader = streamBody.getReader();
18
+ const decoder = new TextDecoder('utf-8');
19
+ let buffer = '';
20
+ try {
21
+ while (true) {
22
+ if (signal?.aborted) {
23
+ await reader.cancel();
24
+ throw new DOMException('The operation was aborted.', 'AbortError');
25
+ }
26
+ const { done, value } = await reader.read();
27
+ if (done)
28
+ break;
29
+ buffer += decoder.decode(value, { stream: true });
30
+ const lines = buffer.split('\n');
31
+ buffer = lines.pop() || '';
32
+ for (const line of lines) {
33
+ const trimmedLine = line.trim();
34
+ if (!trimmedLine || !trimmedLine.startsWith('data: '))
35
+ continue;
36
+ const dataStr = trimmedLine.slice(6).trim();
37
+ if (dataStr === '[DONE]')
38
+ return;
39
+ try {
40
+ const data = JSON.parse(dataStr);
41
+ if (data.type === 'error' || data.error) {
42
+ const errMsg = data.error?.message ?? data.message ?? JSON.stringify(data.error ?? data);
43
+ throw new Error(String(errMsg));
44
+ }
45
+ if (data.type === 'content_block_delta' && data.delta?.text) {
46
+ onChunk(data.delta.text);
47
+ }
48
+ else if (data.type === 'message_delta' && data.delta?.text) {
49
+ onChunk(data.delta.text);
50
+ }
51
+ }
52
+ catch (e) {
53
+ if (e instanceof SyntaxError)
54
+ continue;
55
+ throw e;
56
+ }
57
+ }
58
+ }
59
+ }
60
+ finally {
61
+ reader.releaseLock();
62
+ }
63
+ }
@@ -0,0 +1,59 @@
1
+ import { kiePostJson } from '../kie-http.js';
2
+ import { getResponsesEndpoint } from '../models.js';
3
+ function toCodexInput(messages) {
4
+ return messages.map((m) => {
5
+ if (m.role === 'assistant') {
6
+ return {
7
+ role: 'assistant',
8
+ content: [{ type: 'output_text', text: m.content }],
9
+ };
10
+ }
11
+ if (m.role === 'system') {
12
+ return {
13
+ role: 'user',
14
+ content: [{ type: 'input_text', text: `[System]\n${m.content}` }],
15
+ };
16
+ }
17
+ return {
18
+ role: 'user',
19
+ content: [{ type: 'input_text', text: m.content }],
20
+ };
21
+ });
22
+ }
23
+ function parseCodexResponse(data) {
24
+ if (data.status && data.status !== 'completed' && data.status !== 'success') {
25
+ const err = data.msg ??
26
+ data.message ??
27
+ (typeof data.error === 'string' ? data.error : data.error?.message) ??
28
+ `任务状态: ${data.status}`;
29
+ throw new Error(String(err));
30
+ }
31
+ const parts = [];
32
+ for (const item of data.output ?? []) {
33
+ if (item.type !== 'message' || !item.content)
34
+ continue;
35
+ for (const block of item.content) {
36
+ if (block.type === 'output_text' && block.text) {
37
+ parts.push(block.text);
38
+ }
39
+ }
40
+ }
41
+ return parts.join('');
42
+ }
43
+ export async function sendCodexChat(options) {
44
+ const { apiKey, baseUrl, messages, model, onChunk, signal } = options;
45
+ const endpoint = getResponsesEndpoint(baseUrl, model);
46
+ const data = await kiePostJson(endpoint, {
47
+ apiKey,
48
+ signal,
49
+ body: {
50
+ model,
51
+ stream: false,
52
+ input: toCodexInput(messages),
53
+ },
54
+ });
55
+ const text = parseCodexResponse(data);
56
+ if (text) {
57
+ onChunk(text);
58
+ }
59
+ }
@@ -0,0 +1,14 @@
1
+ import { resolveProvider } from '../models.js';
2
+ export async function sendChatByProvider(options) {
3
+ const provider = resolveProvider(options.model);
4
+ if (provider === 'openai') {
5
+ const { sendOpenAIChat } = await import('./openai.js');
6
+ return sendOpenAIChat(options);
7
+ }
8
+ if (provider === 'codex') {
9
+ const { sendCodexChat } = await import('./codex.js');
10
+ return sendCodexChat(options);
11
+ }
12
+ const { sendClaudeChat } = await import('./claude.js');
13
+ return sendClaudeChat(options);
14
+ }
@@ -0,0 +1,64 @@
1
+ import { kiePostStream } from '../kie-http.js';
2
+ import { getOpenAIChatEndpoint } from '../models.js';
3
+ function toOpenAIMessages(messages) {
4
+ return messages.map((m) => ({
5
+ role: m.role,
6
+ content: m.content,
7
+ }));
8
+ }
9
+ export async function sendOpenAIChat(options) {
10
+ const { apiKey, baseUrl, messages, model, onChunk, signal } = options;
11
+ const endpoint = getOpenAIChatEndpoint(baseUrl, model);
12
+ const streamBody = await kiePostStream(endpoint, {
13
+ apiKey,
14
+ signal,
15
+ body: {
16
+ messages: toOpenAIMessages(messages),
17
+ stream: true,
18
+ },
19
+ });
20
+ const reader = streamBody.getReader();
21
+ const decoder = new TextDecoder('utf-8');
22
+ let buffer = '';
23
+ try {
24
+ while (true) {
25
+ if (signal?.aborted) {
26
+ await reader.cancel();
27
+ throw new DOMException('The operation was aborted.', 'AbortError');
28
+ }
29
+ const { done, value } = await reader.read();
30
+ if (done)
31
+ break;
32
+ buffer += decoder.decode(value, { stream: true });
33
+ const lines = buffer.split('\n');
34
+ buffer = lines.pop() || '';
35
+ for (const line of lines) {
36
+ const trimmedLine = line.trim();
37
+ if (!trimmedLine || !trimmedLine.startsWith('data: '))
38
+ continue;
39
+ const dataStr = trimmedLine.slice(6).trim();
40
+ if (dataStr === '[DONE]')
41
+ return;
42
+ try {
43
+ const data = JSON.parse(dataStr);
44
+ if (data.error) {
45
+ const errMsg = data.error.message ?? JSON.stringify(data.error);
46
+ throw new Error(String(errMsg));
47
+ }
48
+ const delta = data.choices?.[0]?.delta?.content;
49
+ if (typeof delta === 'string' && delta) {
50
+ onChunk(delta);
51
+ }
52
+ }
53
+ catch (e) {
54
+ if (e instanceof SyntaxError)
55
+ continue;
56
+ throw e;
57
+ }
58
+ }
59
+ }
60
+ }
61
+ finally {
62
+ reader.releaseLock();
63
+ }
64
+ }
@@ -0,0 +1,235 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import { getChatCompletion } from './api.js';
5
+ const DEFAULT_SESSION_ID = 'default';
6
+ const DEFAULT_MAX_CHARS = 16_000;
7
+ const DEFAULT_KEEP_RECENT_PAIRS = 6;
8
+ export function getSessionsDir(config) {
9
+ const dir = path.join(path.dirname(config.path), 'sessions');
10
+ if (!fs.existsSync(dir)) {
11
+ fs.mkdirSync(dir, { recursive: true });
12
+ }
13
+ return dir;
14
+ }
15
+ function getSessionFilePath(config, sessionId) {
16
+ const safeId = sessionId.replace(/[^a-zA-Z0-9_-]/g, '_');
17
+ return path.join(getSessionsDir(config), `${safeId}.json`);
18
+ }
19
+ export function getActiveSessionId(config) {
20
+ return config.get('activeSessionId') || DEFAULT_SESSION_ID;
21
+ }
22
+ export function setActiveSessionId(config, sessionId) {
23
+ config.set('activeSessionId', sessionId);
24
+ }
25
+ export function createSessionId() {
26
+ return `session-${Date.now()}`;
27
+ }
28
+ export function loadSession(config, sessionId) {
29
+ const filePath = getSessionFilePath(config, sessionId);
30
+ if (!fs.existsSync(filePath))
31
+ return [];
32
+ try {
33
+ const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
34
+ return Array.isArray(raw.messages) ? raw.messages : [];
35
+ }
36
+ catch {
37
+ return [];
38
+ }
39
+ }
40
+ export function saveSession(config, sessionId, messages) {
41
+ const filePath = getSessionFilePath(config, sessionId);
42
+ const updatedAt = new Date().toISOString();
43
+ const data = {
44
+ id: sessionId,
45
+ updatedAt,
46
+ messages,
47
+ };
48
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
49
+ // Update metadata cache
50
+ const metadata = loadMetadata(config);
51
+ metadata[sessionId] = {
52
+ id: sessionId,
53
+ updatedAt,
54
+ messageCount: messages.length,
55
+ sizeChars: estimateSessionSize(messages),
56
+ };
57
+ saveMetadata(config, metadata);
58
+ }
59
+ export function clearSession(config, sessionId) {
60
+ saveSession(config, sessionId, []);
61
+ }
62
+ function getMetadataPath(config) {
63
+ return path.join(getSessionsDir(config), 'metadata.json');
64
+ }
65
+ function loadMetadata(config) {
66
+ const filePath = getMetadataPath(config);
67
+ if (!fs.existsSync(filePath))
68
+ return {};
69
+ try {
70
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
71
+ }
72
+ catch {
73
+ return {};
74
+ }
75
+ }
76
+ function saveMetadata(config, metadata) {
77
+ const filePath = getMetadataPath(config);
78
+ fs.writeFileSync(filePath, JSON.stringify(metadata, null, 2), 'utf-8');
79
+ }
80
+ export function listSessions(config) {
81
+ const dir = getSessionsDir(config);
82
+ let metadata = loadMetadata(config);
83
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json') && f !== 'metadata.json');
84
+ let changed = false;
85
+ const currentIds = new Set(files.map(f => f.replace(/\.json$/, '')));
86
+ // Remove deleted sessions from metadata
87
+ for (const id in metadata) {
88
+ if (!currentIds.has(id)) {
89
+ delete metadata[id];
90
+ changed = true;
91
+ }
92
+ }
93
+ // Add or update missing sessions
94
+ for (const file of files) {
95
+ const id = file.replace(/\.json$/, '');
96
+ if (!metadata[id]) {
97
+ try {
98
+ const raw = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf-8'));
99
+ const messages = Array.isArray(raw.messages) ? raw.messages : [];
100
+ metadata[id] = {
101
+ id,
102
+ updatedAt: raw.updatedAt || '',
103
+ messageCount: messages.length,
104
+ sizeChars: estimateSessionSize(messages),
105
+ };
106
+ changed = true;
107
+ }
108
+ catch {
109
+ /* skip corrupt files */
110
+ }
111
+ }
112
+ }
113
+ if (changed) {
114
+ saveMetadata(config, metadata);
115
+ }
116
+ return Object.values(metadata).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
117
+ }
118
+ export function resolveSessionPick(input, sessions) {
119
+ const trimmed = input.trim();
120
+ if (!trimmed || trimmed === 'q' || trimmed === 'cancel')
121
+ return null;
122
+ const n = parseInt(trimmed, 10);
123
+ if (!Number.isNaN(n) && n >= 1 && n <= sessions.length) {
124
+ return sessions[n - 1].id;
125
+ }
126
+ const exact = sessions.find((s) => s.id === trimmed);
127
+ if (exact)
128
+ return exact.id;
129
+ const partial = sessions.filter((s) => s.id.includes(trimmed));
130
+ if (partial.length === 1)
131
+ return partial[0].id;
132
+ return null;
133
+ }
134
+ export function printSessionList(sessions, activeId) {
135
+ if (sessions.length === 0) {
136
+ console.log(chalk.yellow('\n暂无已保存的会话。使用 kie chat --new 可创建新会话。\n'));
137
+ return;
138
+ }
139
+ console.log(chalk.cyan('\n已保存的会话:\n'));
140
+ sessions.forEach((s, i) => {
141
+ const activeMark = s.id === activeId ? chalk.green(' (当前)') : '';
142
+ const updated = s.updatedAt ? s.updatedAt.slice(0, 19).replace('T', ' ') : '未知';
143
+ console.log(`${chalk.white(`${i + 1}.`)} ${chalk.bold(s.id)}${activeMark}` +
144
+ chalk.gray(` — ${s.messageCount} 条, ~${s.sizeChars} 字符, 更新 ${updated}`));
145
+ });
146
+ console.log(chalk.gray('\n输入序号切换,或使用 /sessions <id>;q 取消\n'));
147
+ }
148
+ export function estimateSessionSize(messages) {
149
+ return messages.reduce((sum, m) => sum + m.content.length, 0);
150
+ }
151
+ function formatMessagesForSummary(messages) {
152
+ return messages
153
+ .map((m) => `[${m.role}]\n${m.content}`)
154
+ .join('\n\n---\n\n');
155
+ }
156
+ /**
157
+ * 将较早的对话压缩为一条 system 摘要,保留最近若干轮原文。
158
+ */
159
+ export async function compressMessages(messages, options) {
160
+ const { apiKey, baseUrl, force = false } = options;
161
+ const maxChars = options.maxChars ?? DEFAULT_MAX_CHARS;
162
+ const keepRecentPairs = options.keepRecentPairs ?? DEFAULT_KEEP_RECENT_PAIRS;
163
+ const keepCount = keepRecentPairs * 2;
164
+ // 手动 /compress:对话较短时也压缩,但至少保留最近 1 轮
165
+ const effectiveKeepCount = force
166
+ ? Math.min(keepCount, Math.max(2, messages.length - 1))
167
+ : keepCount;
168
+ if (!force && messages.length <= keepCount) {
169
+ return [...messages];
170
+ }
171
+ if (force && messages.length <= 2) {
172
+ return [...messages];
173
+ }
174
+ const recent = messages.slice(-effectiveKeepCount);
175
+ const older = messages.slice(0, -effectiveKeepCount);
176
+ if (older.length === 0) {
177
+ return [...messages];
178
+ }
179
+ const summary = await getChatCompletion({
180
+ apiKey,
181
+ baseUrl,
182
+ model: options.model,
183
+ signal: options.signal,
184
+ messages: [
185
+ {
186
+ role: 'user',
187
+ content: [
188
+ '请将以下对话历史压缩为简洁摘要,保留:',
189
+ '1) 用户目标与约束 2) 已做出的关键决定 3) 重要代码/路径/命令/文件名 4) 未解决的问题。',
190
+ '用中文输出,控制在 800 字以内,不要编造未出现的信息。',
191
+ '',
192
+ '--- 对话历史 ---',
193
+ formatMessagesForSummary(older),
194
+ ].join('\n'),
195
+ },
196
+ ],
197
+ });
198
+ const compressed = [
199
+ {
200
+ role: 'system',
201
+ content: `以下是此前对话的压缩摘要,请在此基础上继续协助用户:\n\n${summary.trim()}`,
202
+ },
203
+ ...recent,
204
+ ];
205
+ if (estimateSessionSize(compressed) > maxChars && keepRecentPairs > 2) {
206
+ return compressMessages(compressed, {
207
+ ...options,
208
+ keepRecentPairs: Math.max(2, keepRecentPairs - 2),
209
+ force: false,
210
+ });
211
+ }
212
+ return compressed;
213
+ }
214
+ /** 手动压缩时是否会有实际变化 */
215
+ export function canCompress(messages, force = false) {
216
+ const keepCount = DEFAULT_KEEP_RECENT_PAIRS * 2;
217
+ if (force)
218
+ return messages.length > 2;
219
+ return messages.length > keepCount;
220
+ }
221
+ export async function maybeCompressSession(messages, options) {
222
+ const config = options.config;
223
+ const maxChars = config?.get('sessionMaxChars') ?? DEFAULT_MAX_CHARS;
224
+ const keepRecentPairs = config?.get('sessionKeepRecent') ?? DEFAULT_KEEP_RECENT_PAIRS;
225
+ if (estimateSessionSize(messages) <= maxChars) {
226
+ return [...messages];
227
+ }
228
+ return compressMessages(messages, {
229
+ apiKey: options.apiKey,
230
+ baseUrl: options.baseUrl,
231
+ model: options.model,
232
+ maxChars,
233
+ keepRecentPairs,
234
+ });
235
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ import { marked } from 'marked';
2
+ import { markedTerminal } from 'marked-terminal';
3
+ let initialized = false;
4
+ export function initRenderer() {
5
+ if (initialized)
6
+ return;
7
+ marked.use(markedTerminal({
8
+ // Custom theme for a premium feel
9
+ code: (text) => `\x1b[38;5;220m${text}\x1b[0m`, // Golden code blocks
10
+ blockquote: (text) => `\x1b[38;5;244m\x1b[3m${text}\x1b[0m`, // Grey italic quotes
11
+ firstHeading: (text) => `\x1b[1m\x1b[38;5;33m${text}\x1b[0m`, // Bold Blue first heading
12
+ heading: (text) => `\x1b[1m\x1b[38;5;39m${text}\x1b[0m`, // Bold Light Blue
13
+ hr: () => `\x1b[38;5;240m${'─'.repeat(process.stdout.columns || 40)}\x1b[0m`,
14
+ listitem: (text) => ` \x1b[38;5;39m•\x1b[0m ${text}`,
15
+ tab: 2,
16
+ }));
17
+ initialized = true;
18
+ }
19
+ export { marked };