mocode-ai 1.2.5 → 1.2.7
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 +6 -6
- package/README.zh-CN.md +4 -4
- package/dist/agent/core.js +77 -37
- 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 +138 -12
- package/dist/config/presets.js +29 -7
- package/dist/context/artifacts.js +20 -0
- package/dist/context/budget.js +15 -6
- package/dist/context/encoders/table.js +1 -1
- package/dist/context/index.js +1 -1
- package/dist/host/stdio.js +10 -1
- package/dist/i18n/index.js +4 -4
- package/dist/llm/index.js +22 -3
- package/dist/llm/providers/anthropic.js +370 -0
- package/dist/memory/discover.js +8 -8
- package/dist/memory/index.js +3 -2
- package/dist/repl/index.js +94 -49
- package/dist/session/compact.js +24 -2
- package/dist/session/notes.js +233 -0
- package/dist/session/persist.js +2 -2
- package/dist/session/scheduler.js +4 -4
- package/dist/skills/runner.js +0 -1
- 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/builtins/index.js +4 -0
- package/dist/tools/builtins/note-append.js +103 -0
- package/dist/tools/registry.js +18 -5
- package/dist/ui/diff.js +1 -1
- package/dist/ui/layout.js +12 -1
- package/dist/ui/render.js +7 -1
- package/dist/verification/prompt.js +55 -0
- package/package.json +4 -3
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';
|
|
@@ -66,9 +66,9 @@ function buildSlashCommands() {
|
|
|
66
66
|
{ name: 'off', value: '/memory_switch off', desc: d('commands.memoryOff') },
|
|
67
67
|
{ name: 'status', value: '/memory_status', desc: d('commands.memoryStatus') },
|
|
68
68
|
{ name: 'reflect', value: '/reflect', desc: d('commands.memoryReflect') },
|
|
69
|
-
{ name: 'init', value: '/init', desc: d('commands.memoryInit') },
|
|
70
69
|
],
|
|
71
70
|
},
|
|
71
|
+
{ name: '/init', desc: d('commands.memoryInit') },
|
|
72
72
|
{
|
|
73
73
|
name: '/subagent', desc: d('commands.subagent'), children: [
|
|
74
74
|
{ name: 'on', value: '/subagent on', desc: d('commands.subagentOn') },
|
|
@@ -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) {
|
|
@@ -173,7 +174,7 @@ function maskKey(k) {
|
|
|
173
174
|
return `${'='.repeat(Math.min(k.length - 4, 20))}${k.slice(-4)}`;
|
|
174
175
|
}
|
|
175
176
|
/**
|
|
176
|
-
* /init 指令:发给 agent 扫描项目并生成
|
|
177
|
+
* /init 指令:发给 agent 扫描项目并生成 AGENTS.md。已存在则让 agent 读后更新(不丢失事实)。写完供 memory 子系统下轮加载。
|
|
177
178
|
*
|
|
178
179
|
* 函数化(非 const):.codegraph/ 索引是否存在的探测放在调用瞬间,没索引时不提 codegraph,
|
|
179
180
|
* 避免 LLM 调出失败。/init 是冷启动动作,IO 开销可忽略。
|
|
@@ -182,14 +183,14 @@ function buildInitPrompt() {
|
|
|
182
183
|
const cg = hasCodegraphIndex()
|
|
183
184
|
? '- 若有 .codegraph/:用 use_skill 加载 codegraph skill 后用 run_command 调 codegraph explore "<架构或入口符号>" 一次拿相关源码+调用路径,别逐文件读!!!\n'
|
|
184
185
|
: '';
|
|
185
|
-
return `分析当前项目(process.cwd()),生成
|
|
186
|
+
return `分析当前项目(process.cwd()),生成 AGENTS.md 项目记忆文件,供 mocode 后续会话自动加载——目标是让后续会话无需重新摸索就能上手。
|
|
186
187
|
|
|
187
188
|
先探查(尽量少调用拿全貌):
|
|
188
189
|
${cg}- read_file package.json(或 Cargo.toml/pyproject.toml/go.mod 等):scripts、依赖、入口、模块类型。
|
|
189
190
|
- glob 顶层目录;read_file 入口文件 + 各子系统 index.ts/README。
|
|
190
|
-
- 若
|
|
191
|
+
- 若 AGENTS.md 已存在:read_file 读它,在其基础上更新(补缺、修正过时),不丢已有准确事实。
|
|
191
192
|
|
|
192
|
-
|
|
193
|
+
AGENTS.md 按以下结构写(每节简短,只写稳定、非显然的事实):
|
|
193
194
|
## 项目
|
|
194
195
|
一两句:是什么、技术栈、运行环境。
|
|
195
196
|
## 命令
|
|
@@ -204,7 +205,7 @@ install / dev / build / test / typecheck / lint 等——从 package.json script
|
|
|
204
205
|
硬要求:
|
|
205
206
|
- 从实际代码提炼,引用具体文件名/命令/符号;不编造、不泛泛。
|
|
206
207
|
- 总长 ≤ 3000 字;只写后续会话有用的稳定事实,不写易变项(当前 bug、临时文件、未决 TODO)。
|
|
207
|
-
- 用 write_file 写入项目根
|
|
208
|
+
- 用 write_file 写入项目根 AGENTS.md。
|
|
208
209
|
- 写完简述:写了哪几节 + 从代码里发现的 2-3 条非显然关键约定(供用户校验)。`;
|
|
209
210
|
}
|
|
210
211
|
/** 临时 readline 读一行(cooked,用于子提问;主输入走 promptWithSlashMenu)。 */
|
|
@@ -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());
|
|
@@ -768,7 +771,7 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
768
771
|
//
|
|
769
772
|
// 与开关联动:① base 用 buildBasePrompt() 取代 config.systemPrompt(后者是启动时一次性
|
|
770
773
|
// 求值的常量,运行时 /memory_switch 不会刷新);② plan suffix 走 getPlanModeSuffix() 现拼;
|
|
771
|
-
// ③
|
|
774
|
+
// ③ AGENTS.md 存在工作区根时由 base 无条件自动导入正文(超长截断,与 memory 开关无关);④ Memory Index 按开关注入。
|
|
772
775
|
const buildSystemMessage = (planMode) => effectiveSystemPrompt(buildBasePrompt(currentSessionId) +
|
|
773
776
|
(planMode ? getPlanModeSuffix() : '') +
|
|
774
777
|
buildMemoryIndexSection(isMemoryEnabled()));
|
|
@@ -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`);
|
|
@@ -1199,7 +1204,7 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
1199
1204
|
continue;
|
|
1200
1205
|
}
|
|
1201
1206
|
if (line === '/init') {
|
|
1202
|
-
// /init:把 init 指令当 user 输入发给 agent(扫描项目 + 生成
|
|
1207
|
+
// /init:把 init 指令当 user 输入发给 agent(扫描项目 + 生成 AGENTS.md),fall through 走 runAgent
|
|
1203
1208
|
joined = buildInitPrompt();
|
|
1204
1209
|
}
|
|
1205
1210
|
if (line === '/upgrade' || line.startsWith('/upgrade ')) {
|
|
@@ -1438,7 +1443,7 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
1438
1443
|
layout.contentWrite(` ${ui.accent}${e.id}${ui.reset} ${ui.dim}${e.name} — ${e.summary}${ui.reset}\n`);
|
|
1439
1444
|
}
|
|
1440
1445
|
if (active.length === 0)
|
|
1441
|
-
layout.contentWrite(`${ui.dim}(无 active 记忆;用 memory_save 存,或 /init 生成
|
|
1446
|
+
layout.contentWrite(`${ui.dim}(无 active 记忆;用 memory_save 存,或 /init 生成 AGENTS.md)${ui.reset}\n`);
|
|
1442
1447
|
layout.contentWrite(`${ui.dim}(详情用 memory_search;启动索引已注入 systemPrompt)${ui.reset}\n`);
|
|
1443
1448
|
continue;
|
|
1444
1449
|
}
|
|
@@ -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
|
}
|
package/dist/session/compact.js
CHANGED
|
@@ -292,10 +292,22 @@ export async function compactHistory(history, opts) {
|
|
|
292
292
|
let oldGroups = groups.slice(0, groups.length - kept.length);
|
|
293
293
|
// force(硬闸/手动强压):保护区不豁免——常规切分无旧区时只保最后一组,
|
|
294
294
|
// 其余全部进可压区(首轮/当前轮也一样)。仍按 group 边界切,不破坏 tool_call 配对。
|
|
295
|
+
// **必须保留最早 user 所在 group**:LLM API(OpenAI / Anthropic)要求 messages 至少
|
|
296
|
+
// 含一条非空 user 消息,否则 400。force 旧实现把所有 user 丢进摘要 → 重建后 history
|
|
297
|
+
// 无 user → 下一轮 chat() 被后端拒绝。保最早 user(而非最后一个)因为它是最原始的
|
|
298
|
+
// 请求上下文,摘要器已覆盖后续交互。
|
|
295
299
|
if (oldGroups.length === 0 && opts.force && groups.length >= 2) {
|
|
296
300
|
kept.length = 0;
|
|
297
|
-
|
|
298
|
-
|
|
301
|
+
const lastIdx = groups.length - 1;
|
|
302
|
+
const firstUserIdx = groups.findIndex((g) => g.assistant?.role === 'user');
|
|
303
|
+
if (firstUserIdx >= 0 && firstUserIdx !== lastIdx) {
|
|
304
|
+
kept.push(groups[firstUserIdx], groups[lastIdx]);
|
|
305
|
+
oldGroups = groups.filter((_, i) => i !== firstUserIdx && i !== lastIdx);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
kept.push(groups[lastIdx]);
|
|
309
|
+
oldGroups = groups.slice(0, groups.length - 1);
|
|
310
|
+
}
|
|
299
311
|
}
|
|
300
312
|
const noop = {
|
|
301
313
|
compacted: false,
|
|
@@ -319,6 +331,8 @@ export async function compactHistory(history, opts) {
|
|
|
319
331
|
const estimateAfter = estimatePromptTokens(history, activeTools, state.correction);
|
|
320
332
|
state.lastEstimate = estimateAfter;
|
|
321
333
|
state.lastUsage = undefined;
|
|
334
|
+
if (!layout.isLastContentRowBlank())
|
|
335
|
+
layout.contentWrite('\n');
|
|
322
336
|
layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}强制微压缩(单组)${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
|
|
323
337
|
return {
|
|
324
338
|
compacted: true,
|
|
@@ -339,6 +353,8 @@ export async function compactHistory(history, opts) {
|
|
|
339
353
|
}
|
|
340
354
|
// history 有内容但全在保护区(系统 + 当前轮)
|
|
341
355
|
if (estimateBefore >= opts.threshold * opts.window) {
|
|
356
|
+
if (!layout.isLastContentRowBlank())
|
|
357
|
+
layout.contentWrite('\n');
|
|
342
358
|
layout.contentWrite(` ${ui.yellow}●${ui.reset} ${ui.yellow}上下文已超阈但无可压缩项(全在保护区),建议 /clear 或缩短输入。${ui.reset}\n`);
|
|
343
359
|
return { ...noop, reason: 'noop-shrunk-too-large', protectedRatio };
|
|
344
360
|
}
|
|
@@ -383,6 +399,10 @@ export async function compactHistory(history, opts) {
|
|
|
383
399
|
const estimateAfter = estimatePromptTokens(history, activeTools, state.correction);
|
|
384
400
|
state.lastEstimate = estimateAfter;
|
|
385
401
|
state.lastUsage = undefined; // 压缩后旧 usage 失效,/context 改用校正估算
|
|
402
|
+
// 压缩行与上一个工具批次摘要行之间补空行分隔(compact 在 core step 循环顶部触发,
|
|
403
|
+
// 上一步的 batch 可能尚未 flush,缓冲末行仍是 ● 工具摘要行 → 两行黏在一起)。
|
|
404
|
+
if (!layout.isLastContentRowBlank())
|
|
405
|
+
layout.contentWrite('\n');
|
|
386
406
|
layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}压缩上下文${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
|
|
387
407
|
// 抖动保护:压缩后仍超阈 → 提示 /clear,不死循环
|
|
388
408
|
if (estimateAfter >= opts.threshold * opts.window) {
|
|
@@ -402,6 +422,8 @@ export async function compactHistory(history, opts) {
|
|
|
402
422
|
state.lastEstimate = estimateAfter;
|
|
403
423
|
state.lastUsage = undefined; // token 数已变,旧 usage 失效
|
|
404
424
|
if (microcompactDone) {
|
|
425
|
+
if (!layout.isLastContentRowBlank())
|
|
426
|
+
layout.contentWrite('\n');
|
|
405
427
|
layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}微压缩旧工具结果${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
|
|
406
428
|
return {
|
|
407
429
|
compacted: true,
|
package/dist/session/notes.js
CHANGED
|
@@ -105,3 +105,236 @@ export function writePlanToNotes(plan, sessionId = getCurrentSessionId()) {
|
|
|
105
105
|
}
|
|
106
106
|
return { path: p, settled: plan.steps.length > 0 && plan.steps.every((s) => s.status === 'completed') };
|
|
107
107
|
}
|
|
108
|
+
// ── Session notes(单会话永久记忆):note_append 写入,reinject 常驻 system ──────
|
|
109
|
+
// 设计:notes.md 不止放 Plan。note_append 往预设笔记段追加一条 finding/decision/
|
|
110
|
+
// open_question/risk;extractActiveNotesSections 读出活跃笔记段正文(排除 Plan/Done),
|
|
111
|
+
// 按 5k token 预算裁剪后由 reinjectSessionStateIntoSystem 注入 system prompt——compact
|
|
112
|
+
// 后仍能恢复,让 agent 始终记得本会话做过什么、发现过什么(单会话永久记忆)。
|
|
113
|
+
/** note_append 接受的预设段 key → 渲染标题。 */
|
|
114
|
+
const NOTE_SECTION_TITLES = {
|
|
115
|
+
findings: 'Findings',
|
|
116
|
+
decisions: 'Decisions',
|
|
117
|
+
open_questions: 'Open Questions',
|
|
118
|
+
risks: 'Risks',
|
|
119
|
+
};
|
|
120
|
+
/** 预设段 key 列表(供工具 schema enum 与校验用)。 */
|
|
121
|
+
export const NOTE_SECTION_KEYS = Object.keys(NOTE_SECTION_TITLES);
|
|
122
|
+
/** 段注入优先级:数值越大越先占预算、越后丢弃正文。Risks 最重要。 */
|
|
123
|
+
const SECTION_PRIORITY = {
|
|
124
|
+
risks: 4, findings: 3, decisions: 2, open_questions: 1,
|
|
125
|
+
};
|
|
126
|
+
/** 常驻笔记正文总预算(token)。5k:占百万级 context 的 0.5%,可常驻相当量笔记。 */
|
|
127
|
+
const NOTES_INJECT_BUDGET_TOKENS = 5000;
|
|
128
|
+
/** 单段正文上限(token):防单段独占预算。 */
|
|
129
|
+
const NOTES_PER_SECTION_TOKENS = 2000;
|
|
130
|
+
/** 单条笔记上限(token):防一条过长吃掉整段预算。 */
|
|
131
|
+
const NOTES_PER_ENTRY_TOKENS = 800;
|
|
132
|
+
/**
|
|
133
|
+
* 轻量 token 估算(启发式,不依赖 tokenizer):CJK ≈ 0.6 token/字,
|
|
134
|
+
* ASCII ≈ 0.25 token/字。对 GLM/DeepSeek/Qwen 等中文偏多的后端略偏保守
|
|
135
|
+
* (估算略高于实际 → 注入实际 token 略低于预算 → 安全侧)。仅供笔记 cap 用,
|
|
136
|
+
* 不替换 llm/estimatePromptTokens 的主口径。
|
|
137
|
+
*/
|
|
138
|
+
function estimateTokens(text) {
|
|
139
|
+
let cjk = 0;
|
|
140
|
+
let other = 0;
|
|
141
|
+
for (let i = 0; i < text.length; i++) {
|
|
142
|
+
const c = text.charCodeAt(i);
|
|
143
|
+
if ((c >= 0x3000 && c <= 0x9fff) || (c >= 0xff00 && c <= 0xffef))
|
|
144
|
+
cjk++;
|
|
145
|
+
else
|
|
146
|
+
other++;
|
|
147
|
+
}
|
|
148
|
+
return Math.ceil(cjk * 0.6 + other * 0.25);
|
|
149
|
+
}
|
|
150
|
+
/** 把标题映射回预设 key(非预设段返 '',优先级 0,最后注入)。 */
|
|
151
|
+
function matchSectionKey(title) {
|
|
152
|
+
const t = title.trim().toLowerCase();
|
|
153
|
+
for (const [k, v] of Object.entries(NOTE_SECTION_TITLES)) {
|
|
154
|
+
if (v.toLowerCase() === t)
|
|
155
|
+
return k;
|
|
156
|
+
}
|
|
157
|
+
return '';
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 往 notes.md 的指定笔记段追加一条。段存在则在其末尾追加(保留其它段不动);
|
|
161
|
+
* 段不存在则在文件末新建。返回 { path } 或 { error }。
|
|
162
|
+
*/
|
|
163
|
+
export function appendNoteToSection(section, entry, tag, sessionId = getCurrentSessionId()) {
|
|
164
|
+
const title = NOTE_SECTION_TITLES[section];
|
|
165
|
+
if (!title)
|
|
166
|
+
return { error: `unknown note section "${section}"` };
|
|
167
|
+
const p = getNotesFilePath(sessionId);
|
|
168
|
+
if (!p)
|
|
169
|
+
return { error: 'no active session' };
|
|
170
|
+
const line = tag ? `- **[${tag}]** ${entry}` : `- ${entry}`;
|
|
171
|
+
let existing = '';
|
|
172
|
+
try {
|
|
173
|
+
existing = fs.readFileSync(p, 'utf8').replace(/\r\n?/g, '\n');
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
existing = '';
|
|
177
|
+
}
|
|
178
|
+
const header = `## ${title}`;
|
|
179
|
+
const lines = existing.split('\n');
|
|
180
|
+
const start = lines.findIndex((l) => l.trim() === header);
|
|
181
|
+
let next;
|
|
182
|
+
if (start >= 0) {
|
|
183
|
+
// 段末 = 下一个 ## 或文件末
|
|
184
|
+
let end = lines.length;
|
|
185
|
+
for (let k = start + 1; k < lines.length; k++) {
|
|
186
|
+
if (/^##\s/.test(lines[k])) {
|
|
187
|
+
end = k;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const before = lines.slice(0, start).join('\n').replace(/\s+$/, '');
|
|
192
|
+
const sectionLines = lines.slice(start, end);
|
|
193
|
+
// 去段尾空行后追加新条目
|
|
194
|
+
while (sectionLines.length && sectionLines[sectionLines.length - 1].trim() === '')
|
|
195
|
+
sectionLines.pop();
|
|
196
|
+
sectionLines.push(line);
|
|
197
|
+
const section = sectionLines.join('\n');
|
|
198
|
+
const after = lines.slice(end).join('\n').replace(/^\s+/, '');
|
|
199
|
+
next = [before, section, after].filter((s) => s.length > 0).join('\n\n') + '\n';
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// 新建段:放文件末,与已有内容以空行分隔
|
|
203
|
+
const rest = existing.trim();
|
|
204
|
+
const newSection = `${header}\n${line}`;
|
|
205
|
+
next = rest ? `${rest}\n\n${newSection}\n` : `${newSection}\n`;
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
209
|
+
fs.writeFileSync(p, next, 'utf8');
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
213
|
+
}
|
|
214
|
+
return { path: p };
|
|
215
|
+
}
|
|
216
|
+
/** 按字符二分截断条目到 token 上限,加省略标记。条目不长,线性二分足够。 */
|
|
217
|
+
function truncateEntry(entry, maxTokens) {
|
|
218
|
+
if (estimateTokens(entry) <= maxTokens)
|
|
219
|
+
return entry;
|
|
220
|
+
let lo = 0;
|
|
221
|
+
let hi = entry.length;
|
|
222
|
+
while (lo < hi) {
|
|
223
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
224
|
+
if (estimateTokens(entry.slice(0, mid)) <= maxTokens)
|
|
225
|
+
lo = mid;
|
|
226
|
+
else
|
|
227
|
+
hi = mid - 1;
|
|
228
|
+
}
|
|
229
|
+
return entry.slice(0, lo).replace(/\s+$/, '') + ' …[truncated]';
|
|
230
|
+
}
|
|
231
|
+
/** 把单段正文按条目分割,从末尾(最近)保留,丢最旧,裁到 token 预算内。 */
|
|
232
|
+
function trimSectionToBudget(body, budgetTokens) {
|
|
233
|
+
if (budgetTokens <= 0)
|
|
234
|
+
return null;
|
|
235
|
+
const bodyLines = body.split('\n');
|
|
236
|
+
const header = bodyLines[0] ?? '';
|
|
237
|
+
const rest = bodyLines.slice(1);
|
|
238
|
+
// 分条目:以 "- " 开头为一条起始,后续非 "- " 行归入该条
|
|
239
|
+
const entries = [];
|
|
240
|
+
let cur = [];
|
|
241
|
+
for (const ln of rest) {
|
|
242
|
+
if (/^-\s+/.test(ln)) {
|
|
243
|
+
if (cur.length)
|
|
244
|
+
entries.push(cur.join('\n'));
|
|
245
|
+
cur = [ln];
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
cur.push(ln);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (cur.length)
|
|
252
|
+
entries.push(cur.join('\n'));
|
|
253
|
+
const kept = [];
|
|
254
|
+
let used = estimateTokens(header);
|
|
255
|
+
for (let k = entries.length - 1; k >= 0; k--) {
|
|
256
|
+
let e = entries[k];
|
|
257
|
+
if (estimateTokens(e) > NOTES_PER_ENTRY_TOKENS) {
|
|
258
|
+
e = truncateEntry(e, NOTES_PER_ENTRY_TOKENS);
|
|
259
|
+
}
|
|
260
|
+
const t = estimateTokens(e);
|
|
261
|
+
if (used + t > budgetTokens)
|
|
262
|
+
break;
|
|
263
|
+
kept.unshift(e);
|
|
264
|
+
used += t;
|
|
265
|
+
}
|
|
266
|
+
if (kept.length === 0)
|
|
267
|
+
return null;
|
|
268
|
+
return `${header}\n${kept.join('\n')}`;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* 读 notes.md,提取所有活跃笔记段正文(排除 `## Plan:` 与 `## Done:`),
|
|
272
|
+
* 按 NOTES_INJECT_BUDGET_TOKENS 裁剪后返回——供 reinject 注入 system prompt。
|
|
273
|
+
* 裁剪策略:段按优先级排序(Risks>Findings>Decisions>Open Questions>自定义),
|
|
274
|
+
* 逐段注入累计 token;单段超 per-section 则段内从最近条目保留丢最旧;
|
|
275
|
+
* 总预算用尽则后续段不注入正文(其标题仍由 buildNotepadSection 索引常驻,
|
|
276
|
+
* agent 可 read_file 取细节)。这样 5k 预算内"写了就常驻",超出降级为索引+按需 read。
|
|
277
|
+
*/
|
|
278
|
+
export function extractActiveNotesSections(budget = NOTES_INJECT_BUDGET_TOKENS, sessionId = getCurrentSessionId()) {
|
|
279
|
+
const p = getNotesFilePath(sessionId);
|
|
280
|
+
if (!p)
|
|
281
|
+
return '';
|
|
282
|
+
let content = '';
|
|
283
|
+
try {
|
|
284
|
+
content = fs.readFileSync(p, 'utf8').replace(/\r\n?/g, '\n');
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return '';
|
|
288
|
+
}
|
|
289
|
+
const lines = content.split('\n');
|
|
290
|
+
const sections = [];
|
|
291
|
+
let i = 0;
|
|
292
|
+
while (i < lines.length) {
|
|
293
|
+
const m = lines[i].match(/^##\s+(.+?)\s*$/);
|
|
294
|
+
if (!m) {
|
|
295
|
+
i++;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const title = m[1];
|
|
299
|
+
// 跳过 Plan/Done 段(Plan 有专属 ACTIVE_PLAN_MARKER 重注入;Done 是归档不常驻)
|
|
300
|
+
if (/^Plan:/.test(title) || /^Done:/.test(title)) {
|
|
301
|
+
i++;
|
|
302
|
+
while (i < lines.length && !/^##\s/.test(lines[i]))
|
|
303
|
+
i++;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const start = i;
|
|
307
|
+
i++;
|
|
308
|
+
while (i < lines.length && !/^##\s/.test(lines[i]))
|
|
309
|
+
i++;
|
|
310
|
+
const body = lines.slice(start, i).join('\n').trim();
|
|
311
|
+
if (body)
|
|
312
|
+
sections.push({ key: matchSectionKey(title), body });
|
|
313
|
+
}
|
|
314
|
+
// 按优先级降序(优先级高的先占预算)
|
|
315
|
+
sections.sort((a, b) => (SECTION_PRIORITY[b.key] ?? 0) - (SECTION_PRIORITY[a.key] ?? 0));
|
|
316
|
+
let used = 0;
|
|
317
|
+
const out = [];
|
|
318
|
+
for (const s of sections) {
|
|
319
|
+
const remaining = budget - used;
|
|
320
|
+
if (remaining <= 0)
|
|
321
|
+
break;
|
|
322
|
+
const bodyTokens = estimateTokens(s.body);
|
|
323
|
+
if (bodyTokens <= Math.min(remaining, NOTES_PER_SECTION_TOKENS)) {
|
|
324
|
+
out.push(s.body);
|
|
325
|
+
used += bodyTokens;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
// 段超预算:段内裁条目(从最近保留)
|
|
329
|
+
const cap = Math.min(remaining, NOTES_PER_SECTION_TOKENS);
|
|
330
|
+
const trimmed = trimSectionToBudget(s.body, cap);
|
|
331
|
+
if (trimmed) {
|
|
332
|
+
out.push(trimmed);
|
|
333
|
+
used += estimateTokens(trimmed);
|
|
334
|
+
}
|
|
335
|
+
// 预算用尽则后续段不再注入正文(降级为索引)
|
|
336
|
+
if (used >= budget)
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
return out.join('\n\n');
|
|
340
|
+
}
|
package/dist/session/persist.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { config } from '../config/index.js';
|
|
3
|
+
import { config, getActiveModel } from '../config/index.js';
|
|
4
4
|
import { truncateDisplay } from '../ui/render.js';
|
|
5
5
|
/** 会话目录(确保存在)。 */
|
|
6
6
|
export function sessionDir() {
|
|
@@ -49,7 +49,7 @@ export function saveSession(history, id, queryHistory = []) {
|
|
|
49
49
|
const meta = {
|
|
50
50
|
id,
|
|
51
51
|
createdAt: idToIso(id),
|
|
52
|
-
model:
|
|
52
|
+
model: getActiveModel(),
|
|
53
53
|
firstUser: history.length > 1
|
|
54
54
|
? firstUserOf(history)
|
|
55
55
|
: truncateDisplay((queryHistory[0] ?? '').replace(/\n/g, ' ').trim(), 40),
|