mocode-ai 1.2.6 → 1.2.8
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/agent/core.js +72 -32
- package/dist/agent/index.js +70 -34
- package/dist/agent/spawn.js +4 -0
- package/dist/commands/skill.js +230 -0
- package/dist/config/index.js +66 -4
- package/dist/config/presets.js +29 -7
- package/dist/context/artifacts.js +20 -0
- package/dist/context/budget.js +20 -7
- package/dist/context/index.js +1 -1
- package/dist/host/stdio.js +10 -1
- package/dist/llm/index.js +31 -6
- package/dist/llm/providers/anthropic.js +370 -0
- package/dist/repl/index.js +85 -40
- package/dist/session/compact.js +121 -34
- package/dist/session/persist.js +2 -2
- package/dist/session/scheduler.js +4 -4
- package/dist/skills/skill-eval.js +345 -0
- package/dist/skills/skill-improve.js +221 -0
- package/dist/skills/stats.js +102 -0
- package/dist/tools/constants.js +14 -0
- package/dist/tools/registry.js +5 -1
- package/dist/ui/layout.js +11 -0
- package/dist/verification/prompt.js +55 -0
- package/package.json +2 -2
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { config, getActiveModel } from '../../config/index.js';
|
|
2
|
+
let fetchImplOverride = null;
|
|
3
|
+
/** 仅供单测注入;生产路径使用 Node 18+ 全局 fetch。 */
|
|
4
|
+
export function __setAnthropicFetchImpl(impl) {
|
|
5
|
+
fetchImplOverride = impl;
|
|
6
|
+
}
|
|
7
|
+
function parseJsonObject(value) {
|
|
8
|
+
if (!value.trim())
|
|
9
|
+
return {};
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(value);
|
|
12
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
13
|
+
? parsed
|
|
14
|
+
: {};
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function textBlocks(content) {
|
|
21
|
+
if (content == null)
|
|
22
|
+
return [];
|
|
23
|
+
if (typeof content === 'string')
|
|
24
|
+
return content ? [{ type: 'text', text: content }] : [];
|
|
25
|
+
if (!Array.isArray(content))
|
|
26
|
+
return [{ type: 'text', text: String(content) }];
|
|
27
|
+
const blocks = [];
|
|
28
|
+
for (const raw of content) {
|
|
29
|
+
if (!raw || typeof raw !== 'object')
|
|
30
|
+
continue;
|
|
31
|
+
const part = raw;
|
|
32
|
+
if (part.type === 'text' && typeof part.text === 'string') {
|
|
33
|
+
blocks.push({ type: 'text', text: part.text });
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (part.type !== 'image_url')
|
|
37
|
+
continue;
|
|
38
|
+
const image = part.image_url;
|
|
39
|
+
if (typeof image?.url !== 'string')
|
|
40
|
+
continue;
|
|
41
|
+
const match = /^data:(image\/(?:jpeg|png|gif|webp));base64,(.+)$/s.exec(image.url);
|
|
42
|
+
if (!match)
|
|
43
|
+
continue;
|
|
44
|
+
blocks.push({
|
|
45
|
+
type: 'image',
|
|
46
|
+
source: { type: 'base64', media_type: match[1], data: match[2] },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return blocks;
|
|
50
|
+
}
|
|
51
|
+
function appendMessage(messages, role, blocks, cacheCandidates, cacheable = true) {
|
|
52
|
+
if (blocks.length === 0)
|
|
53
|
+
return;
|
|
54
|
+
const previous = messages[messages.length - 1];
|
|
55
|
+
if (previous?.role === role)
|
|
56
|
+
previous.content.push(...blocks);
|
|
57
|
+
else
|
|
58
|
+
messages.push({ role, content: [...blocks] });
|
|
59
|
+
if (cacheable)
|
|
60
|
+
cacheCandidates.push(...blocks);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 把会话落盘使用的 OpenAI 兼容消息转换为 Anthropic Messages 内容块。
|
|
64
|
+
* history 保持旧格式以兼容已有 session;provider 边界负责 tool_use/tool_result 编码。
|
|
65
|
+
*/
|
|
66
|
+
export function encodeAnthropicMessages(input, promptCache = config.anthropicPromptCache) {
|
|
67
|
+
const system = [];
|
|
68
|
+
const messages = [];
|
|
69
|
+
const cacheCandidates = [];
|
|
70
|
+
let dialogStarted = false;
|
|
71
|
+
for (const raw of input) {
|
|
72
|
+
const message = raw;
|
|
73
|
+
if (message.role === 'system' && !dialogStarted) {
|
|
74
|
+
system.push(...textBlocks(message.content));
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
dialogStarted = true;
|
|
78
|
+
if (message.role === 'system') {
|
|
79
|
+
const blocks = textBlocks(message.content).map((block) => ({
|
|
80
|
+
...block,
|
|
81
|
+
text: `[System reminder]\n${String(block.text ?? '')}`,
|
|
82
|
+
}));
|
|
83
|
+
// 动态尾部 reminder 每步可能变化,不把 cache breakpoint 放在它之后。
|
|
84
|
+
appendMessage(messages, 'user', blocks, cacheCandidates, false);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (message.role === 'user') {
|
|
88
|
+
appendMessage(messages, 'user', textBlocks(message.content), cacheCandidates);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (message.role === 'assistant') {
|
|
92
|
+
const blocks = textBlocks(message.content);
|
|
93
|
+
for (const call of message.tool_calls ?? []) {
|
|
94
|
+
blocks.push({
|
|
95
|
+
type: 'tool_use',
|
|
96
|
+
id: call.id ?? '',
|
|
97
|
+
name: call.function?.name ?? '',
|
|
98
|
+
input: parseJsonObject(call.function?.arguments ?? ''),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
appendMessage(messages, 'assistant', blocks, cacheCandidates);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (message.role === 'tool') {
|
|
105
|
+
appendMessage(messages, 'user', [{
|
|
106
|
+
type: 'tool_result',
|
|
107
|
+
tool_use_id: message.tool_call_id ?? '',
|
|
108
|
+
content: typeof message.content === 'string'
|
|
109
|
+
? message.content
|
|
110
|
+
: JSON.stringify(message.content ?? ''),
|
|
111
|
+
}], cacheCandidates);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (promptCache) {
|
|
115
|
+
// 最后一个稳定消息块形成会话前缀断点;动态 session reminder 明确不参与。
|
|
116
|
+
const last = cacheCandidates[cacheCandidates.length - 1];
|
|
117
|
+
if (last)
|
|
118
|
+
last.cache_control = { type: 'ephemeral' };
|
|
119
|
+
// 没有对话时仍缓存稳定 system prompt。
|
|
120
|
+
else if (system.length > 0)
|
|
121
|
+
system[system.length - 1].cache_control = { type: 'ephemeral' };
|
|
122
|
+
}
|
|
123
|
+
return { system, messages };
|
|
124
|
+
}
|
|
125
|
+
export function encodeAnthropicTools(tools, promptCache = config.anthropicPromptCache) {
|
|
126
|
+
const encoded = tools.map((tool) => ({
|
|
127
|
+
name: tool.function.name,
|
|
128
|
+
description: tool.function.description,
|
|
129
|
+
input_schema: tool.function.parameters ?? { type: 'object', properties: {} },
|
|
130
|
+
}));
|
|
131
|
+
if (promptCache && encoded.length > 0) {
|
|
132
|
+
encoded[encoded.length - 1] = {
|
|
133
|
+
...encoded[encoded.length - 1],
|
|
134
|
+
cache_control: { type: 'ephemeral' },
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return encoded;
|
|
138
|
+
}
|
|
139
|
+
function endpoint(baseURL) {
|
|
140
|
+
const base = baseURL.replace(/\/+$/, '');
|
|
141
|
+
if (/\/v1$/i.test(base))
|
|
142
|
+
return `${base}/messages`;
|
|
143
|
+
if (/\/v1\/messages$/i.test(base))
|
|
144
|
+
return base;
|
|
145
|
+
return `${base}/v1/messages`;
|
|
146
|
+
}
|
|
147
|
+
export function buildAnthropicRequest(messages, tools) {
|
|
148
|
+
const encoded = encodeAnthropicMessages(messages);
|
|
149
|
+
const anthropicTools = encodeAnthropicTools(tools);
|
|
150
|
+
return {
|
|
151
|
+
model: getActiveModel(),
|
|
152
|
+
max_tokens: config.maxTokens ?? 8192,
|
|
153
|
+
stream: true,
|
|
154
|
+
system: encoded.system,
|
|
155
|
+
messages: encoded.messages,
|
|
156
|
+
...(anthropicTools.length > 0 ? { tools: anthropicTools } : {}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
class AnthropicHttpError extends Error {
|
|
160
|
+
status;
|
|
161
|
+
code;
|
|
162
|
+
headers;
|
|
163
|
+
constructor(status, message, headers, code) {
|
|
164
|
+
super(message);
|
|
165
|
+
this.name = 'AnthropicAPIError';
|
|
166
|
+
this.status = status;
|
|
167
|
+
this.code = code;
|
|
168
|
+
this.headers = headers;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async function throwResponseError(response) {
|
|
172
|
+
let message = `Anthropic API HTTP ${response.status}`;
|
|
173
|
+
let code;
|
|
174
|
+
try {
|
|
175
|
+
const body = JSON.parse(await response.text());
|
|
176
|
+
if (typeof body.error?.message === 'string')
|
|
177
|
+
message = body.error.message;
|
|
178
|
+
if (typeof body.error?.type === 'string')
|
|
179
|
+
code = body.error.type;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// 非 JSON 错误页只保留状态码,避免把代理 HTML 大段写进 TUI。
|
|
183
|
+
}
|
|
184
|
+
throw new AnthropicHttpError(response.status, message, response.headers, code);
|
|
185
|
+
}
|
|
186
|
+
function decodeSseRecord(record) {
|
|
187
|
+
let event = '';
|
|
188
|
+
const data = [];
|
|
189
|
+
for (const line of record.split(/\r?\n/)) {
|
|
190
|
+
if (line.startsWith('event:'))
|
|
191
|
+
event = line.slice(6).trim();
|
|
192
|
+
else if (line.startsWith('data:'))
|
|
193
|
+
data.push(line.slice(5).trimStart());
|
|
194
|
+
}
|
|
195
|
+
if (data.length === 0)
|
|
196
|
+
return null;
|
|
197
|
+
return { event, data: data.join('\n') };
|
|
198
|
+
}
|
|
199
|
+
async function* readSse(response) {
|
|
200
|
+
if (!response.body)
|
|
201
|
+
throw new Error('Anthropic 流式响应缺少 body');
|
|
202
|
+
const reader = response.body.getReader();
|
|
203
|
+
const decoder = new TextDecoder();
|
|
204
|
+
let buffer = '';
|
|
205
|
+
while (true) {
|
|
206
|
+
const { value, done } = await reader.read();
|
|
207
|
+
buffer += decoder.decode(value, { stream: !done });
|
|
208
|
+
let match;
|
|
209
|
+
while ((match = /\r?\n\r?\n/.exec(buffer)) !== null) {
|
|
210
|
+
const record = buffer.slice(0, match.index);
|
|
211
|
+
buffer = buffer.slice(match.index + match[0].length);
|
|
212
|
+
const parsed = decodeSseRecord(record);
|
|
213
|
+
if (parsed)
|
|
214
|
+
yield parsed;
|
|
215
|
+
}
|
|
216
|
+
if (done)
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
const parsed = decodeSseRecord(buffer);
|
|
220
|
+
if (parsed)
|
|
221
|
+
yield parsed;
|
|
222
|
+
}
|
|
223
|
+
function finiteToken(value) {
|
|
224
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
225
|
+
}
|
|
226
|
+
function usageFromAnthropic(raw) {
|
|
227
|
+
const direct = finiteToken(raw.input_tokens);
|
|
228
|
+
const cacheRead = finiteToken(raw.cache_read_input_tokens);
|
|
229
|
+
const cacheCreation = finiteToken(raw.cache_creation_input_tokens);
|
|
230
|
+
const output = finiteToken(raw.output_tokens);
|
|
231
|
+
const prompt = direct + cacheRead + cacheCreation;
|
|
232
|
+
return {
|
|
233
|
+
promptTokens: prompt,
|
|
234
|
+
completionTokens: output,
|
|
235
|
+
totalTokens: prompt + output,
|
|
236
|
+
cachedTokens: cacheRead,
|
|
237
|
+
cacheCreationTokens: cacheCreation,
|
|
238
|
+
reasoningTokens: 0,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function addLiveCount(text, state) {
|
|
242
|
+
for (const ch of text) {
|
|
243
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
244
|
+
if ((cp >= 0x4e00 && cp <= 0x9fff)
|
|
245
|
+
|| (cp >= 0x3400 && cp <= 0x4dbf)
|
|
246
|
+
|| (cp >= 0x3040 && cp <= 0x30ff)
|
|
247
|
+
|| (cp >= 0xac00 && cp <= 0xd7a3))
|
|
248
|
+
state.cjk++;
|
|
249
|
+
else
|
|
250
|
+
state.other++;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** Anthropic Messages API 单次请求;外层 chat() 继续统一负责重试。 */
|
|
254
|
+
export async function anthropicChatOnce(messages, handlers, signal, tools) {
|
|
255
|
+
const fetchImpl = fetchImplOverride ?? fetch;
|
|
256
|
+
const response = await fetchImpl(endpoint(config.baseURL), {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
headers: {
|
|
259
|
+
'content-type': 'application/json',
|
|
260
|
+
accept: 'text/event-stream',
|
|
261
|
+
'x-api-key': config.apiKey,
|
|
262
|
+
'anthropic-version': process.env.ANTHROPIC_VERSION || '2023-06-01',
|
|
263
|
+
},
|
|
264
|
+
body: JSON.stringify(buildAnthropicRequest(messages, tools)),
|
|
265
|
+
signal,
|
|
266
|
+
});
|
|
267
|
+
if (!response.ok)
|
|
268
|
+
await throwResponseError(response);
|
|
269
|
+
let content = '';
|
|
270
|
+
let usage;
|
|
271
|
+
const toolAcc = new Map();
|
|
272
|
+
const live = { cjk: 0, other: 0 };
|
|
273
|
+
const reportLive = () => {
|
|
274
|
+
handlers.onProgress?.({ completionTokens: Math.ceil(live.cjk + live.other / 4) });
|
|
275
|
+
};
|
|
276
|
+
for await (const record of readSse(response)) {
|
|
277
|
+
if (record.data === '[DONE]')
|
|
278
|
+
continue;
|
|
279
|
+
let event;
|
|
280
|
+
try {
|
|
281
|
+
event = JSON.parse(record.data);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const type = typeof event.type === 'string' ? event.type : record.event;
|
|
287
|
+
if (type === 'error') {
|
|
288
|
+
const error = event.error;
|
|
289
|
+
const thrown = new Error(typeof error?.message === 'string' ? error.message : 'Anthropic stream error');
|
|
290
|
+
thrown.name = typeof error?.type === 'string' ? error.type : 'AnthropicStreamError';
|
|
291
|
+
throw thrown;
|
|
292
|
+
}
|
|
293
|
+
if (type === 'message_start') {
|
|
294
|
+
const message = event.message;
|
|
295
|
+
if (message?.usage)
|
|
296
|
+
usage = usageFromAnthropic(message.usage);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (type === 'content_block_start') {
|
|
300
|
+
const index = finiteToken(event.index);
|
|
301
|
+
const block = event.content_block;
|
|
302
|
+
if (block?.type === 'text' && typeof block.text === 'string' && block.text) {
|
|
303
|
+
content += block.text;
|
|
304
|
+
addLiveCount(block.text, live);
|
|
305
|
+
handlers.onText?.(block.text);
|
|
306
|
+
reportLive();
|
|
307
|
+
}
|
|
308
|
+
else if (block?.type === 'tool_use') {
|
|
309
|
+
const initialInput = block.input && typeof block.input === 'object'
|
|
310
|
+
? block.input
|
|
311
|
+
: undefined;
|
|
312
|
+
const call = {
|
|
313
|
+
id: typeof block.id === 'string' ? block.id : '',
|
|
314
|
+
name: typeof block.name === 'string' ? block.name : '',
|
|
315
|
+
// 流式 tool_use 通常以 input:{} 开始,真正 JSON 紧随 input_json_delta;
|
|
316
|
+
// 只有非空 input 才直接采用,避免累加成 `{}{...}`。
|
|
317
|
+
arguments: initialInput && Object.keys(initialInput).length > 0
|
|
318
|
+
? JSON.stringify(initialInput)
|
|
319
|
+
: '',
|
|
320
|
+
};
|
|
321
|
+
toolAcc.set(index, call);
|
|
322
|
+
if (call.name)
|
|
323
|
+
handlers.onToolCall?.(call.name);
|
|
324
|
+
}
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (type === 'content_block_delta') {
|
|
328
|
+
const index = finiteToken(event.index);
|
|
329
|
+
const delta = event.delta;
|
|
330
|
+
if (delta?.type === 'text_delta' && typeof delta.text === 'string') {
|
|
331
|
+
content += delta.text;
|
|
332
|
+
addLiveCount(delta.text, live);
|
|
333
|
+
handlers.onText?.(delta.text);
|
|
334
|
+
reportLive();
|
|
335
|
+
}
|
|
336
|
+
else if (delta?.type === 'input_json_delta' && typeof delta.partial_json === 'string') {
|
|
337
|
+
const call = toolAcc.get(index) ?? { id: '', name: '', arguments: '' };
|
|
338
|
+
call.arguments += delta.partial_json;
|
|
339
|
+
toolAcc.set(index, call);
|
|
340
|
+
addLiveCount(delta.partial_json, live);
|
|
341
|
+
reportLive();
|
|
342
|
+
}
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (type === 'message_delta') {
|
|
346
|
+
const raw = event.usage;
|
|
347
|
+
if (raw) {
|
|
348
|
+
const previous = usage ?? usageFromAnthropic({});
|
|
349
|
+
const output = finiteToken(raw.output_tokens);
|
|
350
|
+
usage = {
|
|
351
|
+
...previous,
|
|
352
|
+
completionTokens: output,
|
|
353
|
+
totalTokens: previous.promptTokens + output,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (usage) {
|
|
359
|
+
handlers.onProgress?.({
|
|
360
|
+
completionTokens: usage.completionTokens,
|
|
361
|
+
promptTokens: usage.promptTokens,
|
|
362
|
+
cachedTokens: usage.cachedTokens,
|
|
363
|
+
cacheCreationTokens: usage.cacheCreationTokens,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
const toolCalls = [...toolAcc.entries()]
|
|
367
|
+
.sort((a, b) => a[0] - b[0])
|
|
368
|
+
.map(([, call]) => ({ ...call, arguments: call.arguments || '{}' }));
|
|
369
|
+
return { content: content || null, toolCalls, usage };
|
|
370
|
+
}
|
package/dist/repl/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import readline from 'node:readline/promises';
|
|
2
2
|
import { emitKeypressEvents } from 'node:readline';
|
|
3
3
|
import { stdin, stdout } from 'node:process';
|
|
4
|
-
import { config, updateModelConfig, isModelConfigured, updateMemoryConfig, isMemoryEnabled, isSubAgentEnabled, updateSubAgentConfig, isFrontendToolsEnabled, updateFrontendToolsConfig, updateLanguageConfig, languageFromShell, buildBasePrompt, getPlanModeSuffix, hasCodegraphIndex,
|
|
4
|
+
import { config, updateModelConfig, isModelConfigured, updateMemoryConfig, isMemoryEnabled, isSubAgentEnabled, updateSubAgentConfig, isFrontendToolsEnabled, updateFrontendToolsConfig, updateLanguageConfig, languageFromShell, buildBasePrompt, getPlanModeSuffix, hasCodegraphIndex, DEFAULT_CONTEXT_WINDOW_TOKENS, pinSessionModel, } from '../config/index.js';
|
|
5
5
|
import { getLanguage, normalizeLanguage, t, } from '../i18n/index.js';
|
|
6
6
|
import { DEFAULT_BUDGET_POLICY } from '../context/budget.js';
|
|
7
7
|
import { updateConfigKey, writeConfigKeys, CONFIG_PATH } from '../config/file.js';
|
|
@@ -152,17 +152,18 @@ function themeDescription(name) {
|
|
|
152
152
|
rose: 1, emerald: 1, amber: 1, lavender: 1, sunset: 1,
|
|
153
153
|
} ? t(key) : '';
|
|
154
154
|
}
|
|
155
|
-
/** /model
|
|
155
|
+
/** /model 预设后端:同时声明原生协议;旧服务继续走 OpenAI-compatible。 */
|
|
156
156
|
const MODEL_PRESETS = [
|
|
157
|
-
{ label: '
|
|
158
|
-
{ label: '
|
|
159
|
-
{ label: '
|
|
157
|
+
{ label: 'Anthropic Claude', provider: 'anthropic', baseURL: 'https://api.anthropic.com', model: 'claude-sonnet-4-5', window: 200000, anthropicPromptCache: true },
|
|
158
|
+
{ label: 'GLM(智谱)', provider: 'openai', baseURL: 'https://open.bigmodel.cn/api/v3', model: 'glm-4.6', window: DEFAULT_CONTEXT_WINDOW_TOKENS, anthropicPromptCache: false },
|
|
159
|
+
{ label: 'DeepSeek', provider: 'openai', baseURL: 'https://api.deepseek.com', model: 'deepseek-chat', window: DEFAULT_CONTEXT_WINDOW_TOKENS, anthropicPromptCache: false },
|
|
160
|
+
{ label: 'Qwen(阿里)', provider: 'openai', baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-plus', window: DEFAULT_CONTEXT_WINDOW_TOKENS, anthropicPromptCache: false },
|
|
160
161
|
// MiniMax OpenAI 兼容端点(https://platform.minimax.io/docs/api-reference/text-openai-api)。
|
|
161
162
|
// MiniMax-M3 为唯一支持图片/视频输入的模型;M2 系列纯文本(见 llm/capabilities.ts KNOWN_TEXT_ONLY_PREFIXES)。
|
|
162
|
-
{ label: 'MiniMax', baseURL: 'https://api.minimax.io/v1', model: 'MiniMax-M3', window: DEFAULT_CONTEXT_WINDOW_TOKENS },
|
|
163
|
-
{ label: '本地 Ollama', baseURL: 'http://localhost:11434/v1', model: 'qwen2.5:7b', window: DEFAULT_CONTEXT_WINDOW_TOKENS },
|
|
164
|
-
{ label: '本地 vLLM', baseURL: 'http://localhost:8000/v1', model: 'default', window: DEFAULT_CONTEXT_WINDOW_TOKENS },
|
|
165
|
-
{ label: '自定义 base_url', baseURL: '', model: '', window: DEFAULT_CONTEXT_WINDOW_TOKENS },
|
|
163
|
+
{ label: 'MiniMax', provider: 'openai', baseURL: 'https://api.minimax.io/v1', model: 'MiniMax-M3', window: DEFAULT_CONTEXT_WINDOW_TOKENS, anthropicPromptCache: false },
|
|
164
|
+
{ label: '本地 Ollama', provider: 'openai', baseURL: 'http://localhost:11434/v1', model: 'qwen2.5:7b', window: DEFAULT_CONTEXT_WINDOW_TOKENS, anthropicPromptCache: false },
|
|
165
|
+
{ label: '本地 vLLM', provider: 'openai', baseURL: 'http://localhost:8000/v1', model: 'default', window: DEFAULT_CONTEXT_WINDOW_TOKENS, anthropicPromptCache: false },
|
|
166
|
+
{ label: '自定义 base_url', provider: 'openai', baseURL: '', model: '', window: DEFAULT_CONTEXT_WINDOW_TOKENS, anthropicPromptCache: false },
|
|
166
167
|
];
|
|
167
168
|
/** apiKey 脱敏:只露末 4 位,前面打星号(显示用,绝不把明文 key 写进内容区)。 */
|
|
168
169
|
function maskKey(k) {
|
|
@@ -750,6 +751,8 @@ export function renderHistory(history) {
|
|
|
750
751
|
export async function startRepl(initialHistory, sessionId, sandboxRootOverride, initialQueryHistory) {
|
|
751
752
|
// 模式重置:agentMode 不落盘,每个 REPL 会话从 auto 开始(/resume / --resume 亦重置)。
|
|
752
753
|
setAgentMode('auto');
|
|
754
|
+
// 钉死本会话模型:之后运行中 agent 一律用此值,其它窗口的 /model switch 不会影响本窗口。
|
|
755
|
+
pinSessionModel();
|
|
753
756
|
// 沙箱根:文件操作边界。优先级 --sandbox-root > SANDBOX_ROOT env > process.cwd()。
|
|
754
757
|
// 纯边界记录(不 chdir),jail.ts 内部 resolve。子 agent 同进程继承全局 root。
|
|
755
758
|
setSandboxRoot(sandboxRootOverride ?? config.sandboxRoot ?? process.cwd());
|
|
@@ -834,10 +837,12 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
834
837
|
if (listPresets().length === 0) {
|
|
835
838
|
try {
|
|
836
839
|
const migrated = migrateCurrentToPreset({
|
|
840
|
+
provider: config.provider,
|
|
837
841
|
baseURL: config.baseURL,
|
|
838
842
|
apiKey: config.apiKey,
|
|
839
843
|
model: config.model,
|
|
840
844
|
contextWindow: config.contextWindowTokens,
|
|
845
|
+
anthropicPromptCache: config.anthropicPromptCache,
|
|
841
846
|
});
|
|
842
847
|
if (migrated) {
|
|
843
848
|
layout.contentWrite(`${ui.dim} ↳ 检测到老配置,已自动迁为预设 “${migrated}”(${ui.cyan}/model list${ui.dim} 查看 · /model switch 切换)${ui.reset}\n`);
|
|
@@ -1545,9 +1550,9 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
1545
1550
|
// focus 透传到 compact_history action 的 LLM 摘要 prompt。
|
|
1546
1551
|
// 返回 SchedulerRunLog 给 UI 显示决策;退化路径(开关关时)在 manualCompact 内部走 compactHistory。
|
|
1547
1552
|
const log = await manualCompact(history, focus, { force });
|
|
1548
|
-
//
|
|
1549
|
-
|
|
1550
|
-
|
|
1553
|
+
// 会话状态(plan + 笔记段)不在此处回写 history[0]:agent/core 每步都在 requestHistory
|
|
1554
|
+
// 末尾注入最新副本(buildSessionStateReminder),压缩后下一步自然恢复,且系统提示保持
|
|
1555
|
+
// 逐字节稳定以命中 prompt 缓存。
|
|
1551
1556
|
const d = log.compactDetail;
|
|
1552
1557
|
appendCurrentSessionRuntimeEvent('compact', {
|
|
1553
1558
|
source: 'manual',
|
|
@@ -1716,16 +1721,20 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
1716
1721
|
// 共用:apply 一个预设到 config + 持久化 + 重建 client + 重显横幅。无参 /model 选菜单和 /model use 都走这里。
|
|
1717
1722
|
const applyPresetAndPersist = (target) => {
|
|
1718
1723
|
updateModelConfig({
|
|
1724
|
+
provider: target.provider,
|
|
1719
1725
|
model: target.model,
|
|
1720
1726
|
baseURL: target.baseURL,
|
|
1721
1727
|
apiKey: target.apiKey,
|
|
1722
1728
|
contextWindowTokens: target.contextWindow,
|
|
1729
|
+
anthropicPromptCache: target.anthropicPromptCache,
|
|
1723
1730
|
});
|
|
1724
1731
|
writeConfigKeys({
|
|
1732
|
+
LLM_PROVIDER: target.provider,
|
|
1725
1733
|
LLM_BASE_URL: target.baseURL,
|
|
1726
1734
|
LLM_API_KEY: target.apiKey,
|
|
1727
1735
|
LLM_MODEL: target.model,
|
|
1728
1736
|
CONTEXT_WINDOW_TOKENS: String(target.contextWindow),
|
|
1737
|
+
ANTHROPIC_PROMPT_CACHE: target.anthropicPromptCache ? 'true' : 'false',
|
|
1729
1738
|
});
|
|
1730
1739
|
reconfigureClient();
|
|
1731
1740
|
refreshStatusBase(history);
|
|
@@ -1736,20 +1745,22 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
1736
1745
|
else {
|
|
1737
1746
|
layout.writeBanner(bannerLines(banner()));
|
|
1738
1747
|
}
|
|
1739
|
-
|
|
1748
|
+
const cacheLabel = target.provider === 'anthropic'
|
|
1749
|
+
? ` · Prompt Cache ${target.anthropicPromptCache ? 'on' : 'off'}`
|
|
1750
|
+
: '';
|
|
1751
|
+
layout.contentWrite(`${ui.dim}(已切换到预设 “${target.name}” → ${target.model} · ${target.provider}${cacheLabel} @ ${target.baseURL})${ui.reset}\n`);
|
|
1740
1752
|
if (config.llmKeysFromShell.length > 0) {
|
|
1741
1753
|
layout.contentWrite(`${ui.dim}(shell 环境变量已设 ${config.llmKeysFromShell.join(' / ')},文件写入下次启动被其覆盖)${ui.reset}\n`);
|
|
1742
1754
|
}
|
|
1743
1755
|
};
|
|
1744
|
-
//
|
|
1745
|
-
|
|
1746
|
-
// sanitize 后仍空才退化到 'preset'。
|
|
1747
|
-
const uniquePresetName = (desired, baseURL, apiKey, model, contextWindow) => {
|
|
1756
|
+
// 决定自动存的预设名:协议、缓存配置和连接四元组都一致时不重复存。
|
|
1757
|
+
const uniquePresetName = (desired, provider, baseURL, apiKey, model, contextWindow, anthropicPromptCache) => {
|
|
1748
1758
|
const existing = listPresets();
|
|
1749
|
-
const sameEntry = existing.find((p) => p.
|
|
1759
|
+
const sameEntry = existing.find((p) => p.provider === provider && p.baseURL === baseURL && p.apiKey === apiKey
|
|
1760
|
+
&& p.model === model && p.contextWindow === contextWindow
|
|
1761
|
+
&& p.anthropicPromptCache === anthropicPromptCache);
|
|
1750
1762
|
if (sameEntry)
|
|
1751
|
-
return null;
|
|
1752
|
-
// sanitize:把非 [a-zA-Z0-9_-] 字符(如 glm-4.6 的 '.')替换为 -,压缩两端 -,裁 1-32。
|
|
1763
|
+
return null;
|
|
1753
1764
|
const sanitized = desired
|
|
1754
1765
|
.replace(/[^a-zA-Z0-9_-]+/g, '-')
|
|
1755
1766
|
.replace(/^-+|-+$/g, '')
|
|
@@ -1773,14 +1784,17 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
1773
1784
|
layout.contentWrite(`${ui.dim}(还没有预设;先跑 /model 添加一个)${ui.reset}\n`);
|
|
1774
1785
|
continue;
|
|
1775
1786
|
}
|
|
1776
|
-
const isCurrent = (p) => p.
|
|
1787
|
+
const isCurrent = (p) => p.provider === config.provider &&
|
|
1788
|
+
p.baseURL === config.baseURL &&
|
|
1777
1789
|
p.apiKey === config.apiKey &&
|
|
1778
1790
|
p.model === config.model &&
|
|
1779
|
-
p.contextWindow === config.contextWindowTokens
|
|
1791
|
+
p.contextWindow === config.contextWindowTokens &&
|
|
1792
|
+
p.anthropicPromptCache === (config.provider === 'anthropic' && config.anthropicPromptCache);
|
|
1780
1793
|
const cols = layout.getGeo().cols;
|
|
1781
1794
|
const labelFor = (p) => {
|
|
1782
1795
|
const tag = isCurrent(p) ? ' ★current' : '';
|
|
1783
|
-
const
|
|
1796
|
+
const cache = p.provider === 'anthropic' ? ` · cache ${p.anthropicPromptCache ? 'on' : 'off'}` : '';
|
|
1797
|
+
const right = `${p.provider}${cache} · ${p.model} @ ${p.baseURL}`;
|
|
1784
1798
|
const left = `${p.name}${tag}`;
|
|
1785
1799
|
const sep = left.length + 1 + right.length;
|
|
1786
1800
|
if (sep <= cols - 2)
|
|
@@ -1790,7 +1804,7 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
1790
1804
|
const choice = await promptIntervention({
|
|
1791
1805
|
type: 'choice',
|
|
1792
1806
|
title: '切换模型预设',
|
|
1793
|
-
detail: `当前: ${config.model} @ ${config.baseURL}(★ = 已匹配)`,
|
|
1807
|
+
detail: `当前: ${config.provider} · ${config.model} @ ${config.baseURL}(★ = 已匹配)`,
|
|
1794
1808
|
options: presets.map(labelFor),
|
|
1795
1809
|
allowCustom: false, // 纯切换,不需要「其他」干扰
|
|
1796
1810
|
});
|
|
@@ -1812,19 +1826,30 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
1812
1826
|
}
|
|
1813
1827
|
layout.contentWrite(`${ui.dim}已配置 ${ps.length} 个预设:${ui.reset}\n`);
|
|
1814
1828
|
for (const p of ps) {
|
|
1815
|
-
const
|
|
1816
|
-
|
|
1829
|
+
const current = p.provider === config.provider
|
|
1830
|
+
&& p.baseURL === config.baseURL
|
|
1831
|
+
&& p.apiKey === config.apiKey
|
|
1832
|
+
&& p.model === config.model
|
|
1833
|
+
&& p.contextWindow === config.contextWindowTokens
|
|
1834
|
+
&& p.anthropicPromptCache === (config.provider === 'anthropic' && config.anthropicPromptCache);
|
|
1835
|
+
const star = current ? ' ★' : '';
|
|
1836
|
+
const cache = p.provider === 'anthropic' ? ` · cache ${p.anthropicPromptCache ? 'on' : 'off'}` : '';
|
|
1837
|
+
layout.contentWrite(` ${ui.accent}${p.name}${ui.reset}${star} ${ui.dim}${p.provider}${cache} · ${p.model} @ ${p.baseURL}${ui.reset}\n`);
|
|
1817
1838
|
}
|
|
1818
|
-
layout.contentWrite(`${ui.dim}(★ =
|
|
1839
|
+
layout.contentWrite(`${ui.dim}(★ = 与当前协议及缓存配置一致;切换用 /model switch)${ui.reset}\n`);
|
|
1819
1840
|
continue;
|
|
1820
1841
|
}
|
|
1821
|
-
// /model show
|
|
1842
|
+
// /model show:显示当前协议、连接与缓存配置(apiKey 脱敏)。
|
|
1822
1843
|
if (arg === 'show') {
|
|
1823
1844
|
layout.contentWrite(`${ui.dim}当前模型配置:${ui.reset}\n`);
|
|
1824
|
-
layout.contentWrite(` ${ui.accent}
|
|
1825
|
-
layout.contentWrite(` ${ui.accent}
|
|
1826
|
-
layout.contentWrite(` ${ui.accent}
|
|
1827
|
-
layout.contentWrite(` ${ui.accent}
|
|
1845
|
+
layout.contentWrite(` ${ui.accent}provider${ui.reset} ${config.provider}\n`);
|
|
1846
|
+
layout.contentWrite(` ${ui.accent}baseURL ${ui.reset} ${config.baseURL}\n`);
|
|
1847
|
+
layout.contentWrite(` ${ui.accent}apiKey ${ui.reset} ${maskKey(config.apiKey)}\n`);
|
|
1848
|
+
layout.contentWrite(` ${ui.accent}model ${ui.reset} ${config.model}\n`);
|
|
1849
|
+
layout.contentWrite(` ${ui.accent}窗口 ${ui.reset} ${config.contextWindowTokens} tokens\n`);
|
|
1850
|
+
if (config.provider === 'anthropic') {
|
|
1851
|
+
layout.contentWrite(` ${ui.accent}缓存 ${ui.reset} Prompt Cache ${config.anthropicPromptCache ? 'on' : 'off'}\n`);
|
|
1852
|
+
}
|
|
1828
1853
|
layout.contentWrite(`${ui.dim}(配置文件: ${CONFIG_PATH})${ui.reset}\n`);
|
|
1829
1854
|
continue;
|
|
1830
1855
|
}
|
|
@@ -2017,25 +2042,42 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
2017
2042
|
}
|
|
2018
2043
|
}
|
|
2019
2044
|
}
|
|
2020
|
-
// 3)
|
|
2021
|
-
|
|
2045
|
+
// 3) 应用协议、连接与缓存配置;Anthropic 原生协议无需重建 OpenAI client,但统一刷新无害。
|
|
2046
|
+
const provider = preset.provider;
|
|
2047
|
+
const anthropicPromptCache = provider === 'anthropic' && preset.anthropicPromptCache;
|
|
2048
|
+
updateModelConfig({
|
|
2049
|
+
provider,
|
|
2050
|
+
model,
|
|
2051
|
+
baseURL,
|
|
2052
|
+
apiKey,
|
|
2053
|
+
contextWindowTokens: window,
|
|
2054
|
+
anthropicPromptCache,
|
|
2055
|
+
});
|
|
2022
2056
|
writeConfigKeys({
|
|
2057
|
+
LLM_PROVIDER: provider,
|
|
2023
2058
|
LLM_BASE_URL: baseURL,
|
|
2024
2059
|
LLM_API_KEY: apiKey,
|
|
2025
2060
|
LLM_MODEL: model,
|
|
2026
2061
|
CONTEXT_WINDOW_TOKENS: String(window),
|
|
2062
|
+
ANTHROPIC_PROMPT_CACHE: anthropicPromptCache ? 'true' : 'false',
|
|
2027
2063
|
});
|
|
2028
2064
|
reconfigureClient();
|
|
2029
|
-
// 3.5)
|
|
2030
|
-
// 让 /model 跑一次就多一份可切换的预设,/model switch 切回去。
|
|
2065
|
+
// 3.5) 自动存为命名预设;协议与缓存策略也是去重键的一部分。
|
|
2031
2066
|
let savedName = null;
|
|
2032
2067
|
try {
|
|
2033
|
-
const finalName = uniquePresetName(model, baseURL, apiKey, model, window);
|
|
2068
|
+
const finalName = uniquePresetName(model, provider, baseURL, apiKey, model, window, anthropicPromptCache);
|
|
2034
2069
|
if (finalName) {
|
|
2035
|
-
savePreset({
|
|
2070
|
+
savePreset({
|
|
2071
|
+
name: finalName,
|
|
2072
|
+
provider,
|
|
2073
|
+
baseURL,
|
|
2074
|
+
apiKey,
|
|
2075
|
+
model,
|
|
2076
|
+
contextWindow: window,
|
|
2077
|
+
anthropicPromptCache,
|
|
2078
|
+
});
|
|
2036
2079
|
savedName = finalName;
|
|
2037
2080
|
}
|
|
2038
|
-
// finalName === null 表示与某个已存在预设完全一致,不再重复保存。
|
|
2039
2081
|
}
|
|
2040
2082
|
catch (e) {
|
|
2041
2083
|
layout.contentWrite(`${ui.red}保存预设失败: ${e.message}${ui.reset}\n`);
|
|
@@ -2049,7 +2091,10 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
2049
2091
|
else {
|
|
2050
2092
|
layout.writeBanner(bannerLines(banner()));
|
|
2051
2093
|
}
|
|
2052
|
-
|
|
2094
|
+
const cacheLabel = provider === 'anthropic'
|
|
2095
|
+
? ` · Prompt Cache ${anthropicPromptCache ? 'on' : 'off'}`
|
|
2096
|
+
: '';
|
|
2097
|
+
layout.contentWrite(`${ui.dim}(已切换模型 → ${model} · ${provider}${cacheLabel} @ ${baseURL})${ui.reset}\n`);
|
|
2053
2098
|
if (savedName) {
|
|
2054
2099
|
layout.contentWrite(`${ui.dim}(已保存为预设 “${savedName}”,下次 /model use ${savedName} 一键切回)${ui.reset}\n`);
|
|
2055
2100
|
}
|