mocode-ai 1.6.3 → 1.6.5
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 +376 -365
- package/README.zh-CN.md +15 -4
- package/dist/agent/model-turn.js +14 -1
- package/dist/agent/run-coordinator.js +129 -84
- package/dist/agent/spawn.js +38 -5
- package/dist/agent/stages/model-runner.js +1 -1
- package/dist/agent/stages/tool-dispatcher.js +117 -79
- package/dist/agent/work-discipline.js +3 -1
- package/dist/config/index.js +61 -21
- package/dist/config/presets.js +40 -1
- package/dist/config/profiles.js +9 -1
- package/dist/context/clearing.js +53 -0
- package/dist/context/encoders/passthrough.js +2 -2
- package/dist/context/pipeline.js +4 -3
- package/dist/context/relevance.js +58 -13
- package/dist/context/text-search.js +113 -0
- package/dist/llm/index.js +35 -8
- package/dist/llm/providers/anthropic.js +14 -4
- package/dist/llm/reasoning.js +141 -0
- package/dist/memory/reflect.js +1 -1
- package/dist/memory/store.js +31 -28
- package/dist/models/budgets.js +11 -0
- package/dist/models/catalog.js +130 -0
- package/dist/models/map-preset.js +50 -0
- package/dist/models/model-panel.js +227 -0
- package/dist/models/protocol.js +44 -0
- package/dist/models/reasoning-cap.js +88 -0
- package/dist/models/search.js +58 -0
- package/dist/models/types.js +8 -0
- package/dist/repl/commands/effort.js +81 -0
- package/dist/repl/commands/model-actions.js +138 -0
- package/dist/repl/commands/model-catalog.js +67 -0
- package/dist/repl/commands/model-panel-ui.js +107 -0
- package/dist/repl/commands/model.js +17 -0
- package/dist/repl/commands/registry.js +6 -0
- package/dist/repl/commands/session.js +54 -0
- package/dist/repl/commands/stats.js +28 -0
- package/dist/repl/commands.js +3 -0
- package/dist/repl/runtime.js +5 -1
- package/dist/repl/status-bar.js +4 -2
- package/dist/rollback/store.js +17 -0
- package/dist/runtime/runtime.js +31 -1
- package/dist/session/compact.js +84 -3
- package/dist/session/index.js +2 -0
- package/dist/session/retention.js +297 -0
- package/dist/session/scheduler.js +30 -3
- package/dist/session/store.js +108 -6
- package/dist/session/usage-stats.js +90 -0
- package/dist/skills/activation.js +2 -0
- package/dist/skills/builtin-skills.js +153 -0
- package/dist/skills/discover.js +3 -1
- package/dist/skills/runner.js +4 -1
- package/dist/tools/builtins/edit-file.js +2 -4
- package/dist/tools/builtins/grep.js +2 -4
- package/dist/tools/builtins/index.js +4 -0
- package/dist/tools/builtins/read-file.js +21 -5
- package/dist/tools/builtins/session-search.js +33 -0
- package/dist/tools/builtins/task.js +2 -0
- package/dist/tools/constants.js +8 -0
- package/dist/tools/policy.js +1 -1
- package/dist/tools/read-dedup.js +62 -0
- package/dist/tools/tool-runtime.js +1 -0
- package/dist/ui/batch.js +109 -103
- package/dist/ui/fuzzy-picker.js +227 -0
- package/dist/ui/hierarchical-picker.js +302 -0
- package/dist/ui/intervention.js +11 -1
- package/dist/ui/layout-internal/statusbar.js +18 -7
- package/dist/ui/render.js +2 -0
- package/package.json +3 -2
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
import { canonicalizePath, extractPath, isToolResultSuccess, toText } from './utils.js';
|
|
4
4
|
/** Shared prefix lets /context count read and observation supersession together. */
|
|
5
5
|
const STUB_PREFIX = '⌦[已过时:';
|
|
6
|
-
|
|
6
|
+
/** read→read:仅同 path 同区间(offset+limit)的更新 read 才淘汰旧页。 */
|
|
7
|
+
const READ_RANGE_STUB_REASON = '同 path 同区间已有新 read';
|
|
8
|
+
/** mutation→read:文件被改写,该 path 旧 read 全区间淘汰。 */
|
|
9
|
+
const READ_MUTATION_STUB_REASON = '同 path 已被 mutation 覆写';
|
|
10
|
+
/** read_file 默认分页(对齐 tools/builtins/read-file.ts 的 DEFAULT_READ_LIMIT)。 */
|
|
11
|
+
const READ_DEFAULT_LIMIT = 300;
|
|
12
|
+
/** read_file 单次硬上限(对齐 MAX_FILE_LINES),把超传 limit 归一化到真实拿到的区间。 */
|
|
13
|
+
const READ_LIMIT_CAP = 2000;
|
|
7
14
|
function parseArgs(raw) {
|
|
8
15
|
try {
|
|
9
16
|
const parsed = raw.trim() ? JSON.parse(raw) : {};
|
|
@@ -17,6 +24,16 @@ function normalizedInteger(value, fallback) {
|
|
|
17
24
|
const n = Number(value);
|
|
18
25
|
return Number.isFinite(n) ? Math.trunc(n) : fallback;
|
|
19
26
|
}
|
|
27
|
+
/** read_file 实际拿到的行区间:offset 默认 1;limit 默认 300 且钳到 2000(同工具 execute 的归一化)。
|
|
28
|
+
* 无法解析参数时返 null(调用方保守跳过,不做区间淘汰)。 */
|
|
29
|
+
function readRange(argsRaw) {
|
|
30
|
+
const args = parseArgs(argsRaw);
|
|
31
|
+
if (!args)
|
|
32
|
+
return null;
|
|
33
|
+
const offset = Math.max(1, normalizedInteger(args.offset, 1));
|
|
34
|
+
const limit = Math.max(1, Math.min(normalizedInteger(args.limit, READ_DEFAULT_LIMIT), READ_LIMIT_CAP));
|
|
35
|
+
return { offset, limit };
|
|
36
|
+
}
|
|
20
37
|
/** Only complete semantic-query equality is safe for whole-message replacement. */
|
|
21
38
|
function observationKey(call) {
|
|
22
39
|
const args = call.args;
|
|
@@ -46,7 +63,11 @@ function observationLabel(call) {
|
|
|
46
63
|
}
|
|
47
64
|
/**
|
|
48
65
|
* Cross-message relevance pruning:
|
|
49
|
-
* - read_file: a newer successful read of the same canonical path
|
|
66
|
+
* - read_file: a newer successful read of the same canonical path AND the same
|
|
67
|
+
* line range (offset+limit) supersedes old reads; different pages of one file
|
|
68
|
+
* are distinct and never prune each other.
|
|
69
|
+
* - edit_file/write_file: a mutation of a path supersedes that path's old reads
|
|
70
|
+
* across ALL ranges (the whole file changed).
|
|
50
71
|
* - grep: a newer successful call with the exact same semantic arguments
|
|
51
72
|
* supersedes old results. Partial file overlap is intentionally not enough.
|
|
52
73
|
*/
|
|
@@ -114,8 +135,10 @@ export class RelevancePruner {
|
|
|
114
135
|
*/
|
|
115
136
|
pruneSuperseded(history, coldBoundary) {
|
|
116
137
|
try {
|
|
138
|
+
// read→read:键 = path + 区间(offset+limit),只淘汰真正重复的同区间旧 read。
|
|
117
139
|
const latestRead = new Map();
|
|
118
140
|
const latestObservation = new Map();
|
|
141
|
+
// mutation→read:按 path 记录,稍后淘汰该文件所有区间旧 read。
|
|
119
142
|
const mutations = [];
|
|
120
143
|
for (let idx = 1; idx < history.length; idx++) {
|
|
121
144
|
const message = history[idx];
|
|
@@ -127,8 +150,10 @@ export class RelevancePruner {
|
|
|
127
150
|
continue;
|
|
128
151
|
if (call.name === 'read_file') {
|
|
129
152
|
const path = canonicalizePath(extractPath(call.argsRaw));
|
|
130
|
-
|
|
131
|
-
|
|
153
|
+
const range = readRange(call.argsRaw);
|
|
154
|
+
// path 或区间无法确定 → 保守跳过,不做 read→read 淘汰。
|
|
155
|
+
if (path && range)
|
|
156
|
+
latestRead.set(`${path}|${range.offset}|${range.limit}`, { path, range, index: idx });
|
|
132
157
|
}
|
|
133
158
|
else if (call.name === 'edit_file' || call.name === 'write_file') {
|
|
134
159
|
const path = canonicalizePath(extractPath(call.argsRaw));
|
|
@@ -140,11 +165,11 @@ export class RelevancePruner {
|
|
|
140
165
|
latestObservation.set(key, { tool: call.name, index: idx });
|
|
141
166
|
}
|
|
142
167
|
let pruned = 0;
|
|
143
|
-
for (const
|
|
144
|
-
pruned += this.stubPriorReads(history,
|
|
168
|
+
for (const entry of latestRead.values()) {
|
|
169
|
+
pruned += this.stubPriorReads(history, entry, coldBoundary, 'range');
|
|
145
170
|
}
|
|
146
171
|
for (const mutation of mutations) {
|
|
147
|
-
pruned += this.stubPriorReads(history, mutation.path, mutation.index, coldBoundary);
|
|
172
|
+
pruned += this.stubPriorReads(history, { path: mutation.path, index: mutation.index }, coldBoundary, 'mutation');
|
|
148
173
|
}
|
|
149
174
|
for (const [key, latest] of latestObservation) {
|
|
150
175
|
pruned += this.stubPriorObservations(history, latest.tool, key, latest.index, coldBoundary);
|
|
@@ -155,12 +180,16 @@ export class RelevancePruner {
|
|
|
155
180
|
return 0;
|
|
156
181
|
}
|
|
157
182
|
}
|
|
158
|
-
|
|
159
|
-
|
|
183
|
+
/**
|
|
184
|
+
* @param mode 'range'(read→read:仅同区间淘汰) | 'mutation'(文件被改:该 path 全区间淘汰)
|
|
185
|
+
*/
|
|
186
|
+
stubPriorReads(history, target, coldBoundary, mode) {
|
|
187
|
+
const targetPath = canonicalizePath(target.path);
|
|
160
188
|
if (!targetPath)
|
|
161
189
|
return 0;
|
|
190
|
+
const reason = mode === 'range' ? READ_RANGE_STUB_REASON : READ_MUTATION_STUB_REASON;
|
|
162
191
|
let pruned = 0;
|
|
163
|
-
for (let idx = 1; idx < Math.min(
|
|
192
|
+
for (let idx = 1; idx < Math.min(target.index, coldBoundary); idx++) {
|
|
164
193
|
const message = history[idx];
|
|
165
194
|
if (!message || message.role !== 'tool')
|
|
166
195
|
continue;
|
|
@@ -168,12 +197,22 @@ export class RelevancePruner {
|
|
|
168
197
|
if (content.startsWith('⌦['))
|
|
169
198
|
continue;
|
|
170
199
|
const call = this.callAt(history, idx);
|
|
171
|
-
if (call?.name !== 'read_file')
|
|
200
|
+
if (call?.name !== 'read_file' || !message.tool_call_id)
|
|
172
201
|
continue;
|
|
173
|
-
if (canonicalizePath(extractPath(call.argsRaw)) !== targetPath
|
|
202
|
+
if (canonicalizePath(extractPath(call.argsRaw)) !== targetPath)
|
|
174
203
|
continue;
|
|
204
|
+
// read→read 必须同区间;mutation→read 不看区间(整文件已变)。
|
|
205
|
+
if (mode === 'range') {
|
|
206
|
+
const priorRange = readRange(call.argsRaw);
|
|
207
|
+
if (!priorRange ||
|
|
208
|
+
!target.range ||
|
|
209
|
+
priorRange.offset !== target.range.offset ||
|
|
210
|
+
priorRange.limit !== target.range.limit) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
175
214
|
message.content =
|
|
176
|
-
`${STUB_PREFIX}${
|
|
215
|
+
`${STUB_PREFIX}${reason}] read_file(${targetPath}) ${content.length} 字符 ` +
|
|
177
216
|
`→ 已被新 read / mutation 替代 · id …${message.tool_call_id.slice(-6)}⌫`;
|
|
178
217
|
pruned++;
|
|
179
218
|
}
|
|
@@ -219,10 +258,15 @@ export function computePruneStats(history) {
|
|
|
219
258
|
let stubbed = 0;
|
|
220
259
|
let originalChars = 0;
|
|
221
260
|
let stubChars = 0;
|
|
261
|
+
let cleared = 0;
|
|
222
262
|
for (const message of history) {
|
|
223
263
|
if (message.role !== 'tool')
|
|
224
264
|
continue;
|
|
225
265
|
const content = toText(message.content);
|
|
266
|
+
if (content.startsWith('⌦[已清除:')) {
|
|
267
|
+
cleared++;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
226
270
|
const isPruneStub = content.startsWith(STUB_PREFIX);
|
|
227
271
|
const isDigest = content.startsWith('⌦[摘要:');
|
|
228
272
|
if (!isPruneStub && !isDigest)
|
|
@@ -241,5 +285,6 @@ export function computePruneStats(history) {
|
|
|
241
285
|
originalTokens,
|
|
242
286
|
stubChars,
|
|
243
287
|
freedTokens: Math.max(0, originalTokens - stubTokens),
|
|
288
|
+
cleared,
|
|
244
289
|
};
|
|
245
290
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// 共享全文检索:CJK 感知分词 + BM25。零依赖纯函数,供 memory 与 archive 搜索复用。
|
|
2
|
+
//
|
|
3
|
+
// 分词:ASCII/数字按词;CJK 字符同时生成 unigram 与相邻 bigram——中文词多为两字,
|
|
4
|
+
// 单字召回噪声大、纯 bigram 又漏掉单字成词的情况,unigram+bigram 是 Elasticsearch
|
|
5
|
+
// cjk analyzer 的成熟折中。
|
|
6
|
+
//
|
|
7
|
+
// 多字段加权 BM25:每个字段独立长度归一,字段分按 weight 加权求和。
|
|
8
|
+
const CJK_RE = /[㐀-䶿一-鿿豈--ヿ가-]/;
|
|
9
|
+
/** CJK 感知分词(小写)。 */
|
|
10
|
+
export function tokenize(input) {
|
|
11
|
+
const text = (input ?? '').toLowerCase();
|
|
12
|
+
const tokens = [];
|
|
13
|
+
const chars = Array.from(text);
|
|
14
|
+
let word = '';
|
|
15
|
+
const flushWord = () => {
|
|
16
|
+
if (word) {
|
|
17
|
+
tokens.push(word);
|
|
18
|
+
word = '';
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
for (let i = 0; i < chars.length; i++) {
|
|
22
|
+
const ch = chars[i];
|
|
23
|
+
if (/[a-z0-9]/.test(ch)) {
|
|
24
|
+
word += ch;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
flushWord();
|
|
28
|
+
if (CJK_RE.test(ch)) {
|
|
29
|
+
tokens.push(ch); // unigram
|
|
30
|
+
if (i + 1 < chars.length && CJK_RE.test(chars[i + 1])) {
|
|
31
|
+
tokens.push(ch + chars[i + 1]); // bigram
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
flushWord();
|
|
36
|
+
return tokens;
|
|
37
|
+
}
|
|
38
|
+
/** BM25 多字段索引。文档量小(memory ≤百条、archive 索引),构建成本可忽略。 */
|
|
39
|
+
export class TextSearchIndex {
|
|
40
|
+
k1;
|
|
41
|
+
b;
|
|
42
|
+
ids;
|
|
43
|
+
fieldStats;
|
|
44
|
+
fieldWeights;
|
|
45
|
+
constructor(docs, k1 = 1.5, b = 0.75) {
|
|
46
|
+
this.k1 = k1;
|
|
47
|
+
this.b = b;
|
|
48
|
+
this.ids = docs.map((d) => d.id);
|
|
49
|
+
const fieldCount = Math.max(1, ...docs.map((d) => d.fields.length));
|
|
50
|
+
this.fieldWeights = Array.from({ length: fieldCount }, (_, f) => docs[0]?.fields[f]?.weight ?? 1);
|
|
51
|
+
this.fieldStats = Array.from({ length: fieldCount }, () => ({
|
|
52
|
+
df: new Map(),
|
|
53
|
+
tf: new Map(),
|
|
54
|
+
dl: new Map(),
|
|
55
|
+
avgdl: 0,
|
|
56
|
+
}));
|
|
57
|
+
docs.forEach((doc, docId) => {
|
|
58
|
+
doc.fields.forEach((field, f) => {
|
|
59
|
+
const stats = this.fieldStats[f];
|
|
60
|
+
const toks = tokenize(field.text);
|
|
61
|
+
stats.dl.set(docId, toks.length);
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
for (const t of toks) {
|
|
64
|
+
let perDoc = stats.tf.get(t);
|
|
65
|
+
if (!perDoc) {
|
|
66
|
+
perDoc = new Map();
|
|
67
|
+
stats.tf.set(t, perDoc);
|
|
68
|
+
}
|
|
69
|
+
perDoc.set(docId, (perDoc.get(docId) ?? 0) + 1);
|
|
70
|
+
seen.add(t);
|
|
71
|
+
}
|
|
72
|
+
for (const t of seen)
|
|
73
|
+
stats.df.set(t, (stats.df.get(t) ?? 0) + 1);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
const n = docs.length || 1;
|
|
77
|
+
for (const stats of this.fieldStats) {
|
|
78
|
+
let total = 0;
|
|
79
|
+
for (const len of stats.dl.values())
|
|
80
|
+
total += len;
|
|
81
|
+
stats.avgdl = total / n;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** 检索;无有效查询词时返回空数组(调用方自行决定是否回退全量)。 */
|
|
85
|
+
search(query, limit = 10) {
|
|
86
|
+
const terms = tokenize(query);
|
|
87
|
+
if (terms.length === 0 || this.ids.length === 0)
|
|
88
|
+
return [];
|
|
89
|
+
const n = this.ids.length;
|
|
90
|
+
const hits = [];
|
|
91
|
+
for (let docId = 0; docId < n; docId++) {
|
|
92
|
+
let score = 0;
|
|
93
|
+
this.fieldStats.forEach((stats, f) => {
|
|
94
|
+
const dl = stats.dl.get(docId);
|
|
95
|
+
if (dl === undefined)
|
|
96
|
+
return;
|
|
97
|
+
const norm = this.k1 * (1 - this.b + (this.b * dl) / Math.max(stats.avgdl, 1));
|
|
98
|
+
for (const term of terms) {
|
|
99
|
+
const tf = stats.tf.get(term)?.get(docId);
|
|
100
|
+
if (!tf)
|
|
101
|
+
continue;
|
|
102
|
+
const df = stats.df.get(term) ?? 0;
|
|
103
|
+
const idf = Math.log(1 + (n - df + 0.5) / (df + 0.5));
|
|
104
|
+
score += ((this.fieldWeights[f] ?? 1) * (idf * (tf * (this.k1 + 1)))) / (tf + norm);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
if (score > 0)
|
|
108
|
+
hits.push({ id: this.ids[docId], score });
|
|
109
|
+
}
|
|
110
|
+
hits.sort((a, b) => b.score - a.score);
|
|
111
|
+
return hits.slice(0, Math.max(1, limit));
|
|
112
|
+
}
|
|
113
|
+
}
|
package/dist/llm/index.js
CHANGED
|
@@ -8,6 +8,9 @@ import { sanitizeToolSchemas } from './tool-schema.js';
|
|
|
8
8
|
import { isMarkedStreamInterrupted } from './stream-interrupt.js';
|
|
9
9
|
import { defaultAnthropicFetch, anthropicChatOnce } from './providers/anthropic.js';
|
|
10
10
|
import { registerModelProvider, getModelProvider, listModelProviders } from './provider.js';
|
|
11
|
+
import { effectiveReasoningEffort } from '../config/index.js';
|
|
12
|
+
import { resolveReasoningParams } from './reasoning.js';
|
|
13
|
+
import { getActivePresetName, readPreset } from '../config/presets.js';
|
|
11
14
|
// 强制关闭第三方调试日志泄漏:openai SDK 在 process.env.DEBUG === 'true' 时用裸
|
|
12
15
|
// console.log 把请求/响应直写 stdout,会污染 TUI 输入框(并泄露 headers/URL)。
|
|
13
16
|
// 仅拦截 'true' 这一开关值——保留 namespace 形式的 DEBUG(如 DEBUG=express:*) 调试能力。
|
|
@@ -394,11 +397,11 @@ export function refreshChatTools() {
|
|
|
394
397
|
// 新增 provider 只需 registerModelProvider,不必改 chat()。
|
|
395
398
|
registerModelProvider({
|
|
396
399
|
name: 'openai',
|
|
397
|
-
chatOnce: (messages, handlers, signal, tools, runtime) => chatOnce(messages, handlers, signal, tools, runtime),
|
|
400
|
+
chatOnce: (messages, handlers, signal, tools, runtime, overrides) => chatOnce(messages, handlers, signal, tools, runtime, overrides),
|
|
398
401
|
});
|
|
399
402
|
registerModelProvider({
|
|
400
403
|
name: 'anthropic',
|
|
401
|
-
chatOnce: (messages, handlers, signal, tools, runtime) => anthropicChatOnce(messages, handlers, signal, tools ?? chatTools, runtime),
|
|
404
|
+
chatOnce: (messages, handlers, signal, tools, runtime, overrides) => anthropicChatOnce(messages, handlers, signal, tools ?? chatTools, runtime, overrides),
|
|
402
405
|
});
|
|
403
406
|
/**
|
|
404
407
|
* 多 provider 兼容的 cache / reasoning 字段提取。
|
|
@@ -474,14 +477,14 @@ function retryErrorCode(error) {
|
|
|
474
477
|
}
|
|
475
478
|
/** Bind chat dispatch, retries and built-in providers to one explicit runtime. */
|
|
476
479
|
export function createChatTransport(runtime) {
|
|
477
|
-
return (messages, handlers = {}, signal, toolsOverride) => chatWithRuntime(runtime, messages, handlers, signal, toolsOverride);
|
|
480
|
+
return (messages, handlers = {}, signal, toolsOverride, overrides) => chatWithRuntime(runtime, messages, handlers, signal, toolsOverride, overrides);
|
|
478
481
|
}
|
|
479
482
|
export function chat(messages, handlers = {}, signal,
|
|
480
483
|
/** 覆盖默认工具 schema;plan 模式传 planChatTools(只读子集),缺省=全量 chatTools。 */
|
|
481
|
-
toolsOverride) {
|
|
482
|
-
return chatWithRuntime(defaultProviderRuntime, messages, handlers, signal, toolsOverride);
|
|
484
|
+
toolsOverride, overrides) {
|
|
485
|
+
return chatWithRuntime(defaultProviderRuntime, messages, handlers, signal, toolsOverride, overrides);
|
|
483
486
|
}
|
|
484
|
-
async function chatWithRuntime(runtime, messages, handlers, signal, toolsOverride) {
|
|
487
|
+
async function chatWithRuntime(runtime, messages, handlers, signal, toolsOverride, overrides) {
|
|
485
488
|
let lastErr;
|
|
486
489
|
// 本次尝试是否已向调用方产出过内容(可见文本 / 工具名)。用于判断「流中途故障可否安全重试」:
|
|
487
490
|
// 重试会从请求头重放,已流出并显示在内容区的半截文本会再写一遍(history 因异常未落,倒是不脏)。
|
|
@@ -511,7 +514,7 @@ async function chatWithRuntime(runtime, messages, handlers, signal, toolsOverrid
|
|
|
511
514
|
// 未注册的 provider:给出可用名单,避免悄悄落到错误实现。
|
|
512
515
|
throw new Error(`未知的 LLM provider "${runtime.config.provider}";已注册:${listModelProviders().join(', ') || '(空)'}`);
|
|
513
516
|
}
|
|
514
|
-
return await provider.chatOnce(messages, guardedHandlers, signal, toolsOverride, runtime);
|
|
517
|
+
return await provider.chatOnce(messages, guardedHandlers, signal, toolsOverride, runtime, overrides);
|
|
515
518
|
}
|
|
516
519
|
catch (err) {
|
|
517
520
|
lastErr = err;
|
|
@@ -584,7 +587,7 @@ export function normalizeImageDetail(messages) {
|
|
|
584
587
|
return changed ? next : messages;
|
|
585
588
|
}
|
|
586
589
|
/** 单次流式 LLM 请求(无重试);chat() 的内部实现,可被 __setChatCreateImpl 注入桩以做单测。 */
|
|
587
|
-
async function chatOnce(messages, handlers, signal, toolsOverride, runtime = defaultProviderRuntime) {
|
|
590
|
+
async function chatOnce(messages, handlers, signal, toolsOverride, runtime = defaultProviderRuntime, overrides) {
|
|
588
591
|
// 防御:messages 必须至少含一条非空 user 消息,否则 OpenAI/Anthropic 都会 400。
|
|
589
592
|
// compact force 分支曾把所有 user 丢进摘要 → 重建 history 无 user → 下一轮 400。
|
|
590
593
|
// 在 transport 边界拦住所有类似回归(compact / 外部注入 / resume 损坏)。
|
|
@@ -611,6 +614,29 @@ async function chatOnce(messages, handlers, signal, toolsOverride, runtime = def
|
|
|
611
614
|
// transport 边界消毒:剔除部分后端(kimi-k3@dashscope 实测)整请求 400 拒绝的
|
|
612
615
|
// uniqueItems 关键字。无需改写时返回原引用,前缀缓存逐字节稳定不受影响。
|
|
613
616
|
const activeTools = sanitizeToolSchemas(toolsOverride ?? chatTools);
|
|
617
|
+
const effort = effectiveReasoningEffort(overrides?.reasoningEffort);
|
|
618
|
+
// 实时读激活预设(#model-catalog M3):capabilities 只存在预设文件里,启动快照不反映切换。
|
|
619
|
+
const activePreset = (() => {
|
|
620
|
+
const n = getActivePresetName();
|
|
621
|
+
return n ? readPreset(n) : null;
|
|
622
|
+
})();
|
|
623
|
+
const reasoningParams = effort === 'auto'
|
|
624
|
+
? {}
|
|
625
|
+
: resolveReasoningParams(effort, {
|
|
626
|
+
provider: runtimeConfig.provider === 'anthropic' ? 'anthropic' : 'openai',
|
|
627
|
+
model: runtime.getModel(),
|
|
628
|
+
maxTokens: runtimeConfig.maxTokens,
|
|
629
|
+
// 仅当激活预设来自目录且对应当前模型时携带,否则走正则兜底。
|
|
630
|
+
...(activePreset && activePreset.catalogProvider && activePreset.model === runtime.getModel()
|
|
631
|
+
? {
|
|
632
|
+
catalogProviderId: activePreset.catalogProvider,
|
|
633
|
+
catalogReasoning: activePreset.capabilities ? activePreset.capabilities.reasoning : undefined,
|
|
634
|
+
catalogReasoningOptions: activePreset.capabilities
|
|
635
|
+
? activePreset.capabilities.reasoningOptions
|
|
636
|
+
: undefined,
|
|
637
|
+
}
|
|
638
|
+
: {}),
|
|
639
|
+
});
|
|
614
640
|
const stream = await create({
|
|
615
641
|
model: runtime.getModel(),
|
|
616
642
|
messages: normalizeImageDetail(messages),
|
|
@@ -626,6 +652,7 @@ async function chatOnce(messages, handlers, signal, toolsOverride, runtime = def
|
|
|
626
652
|
: {}),
|
|
627
653
|
...(runtimeConfig.maxTokens ? { max_tokens: runtimeConfig.maxTokens } : {}),
|
|
628
654
|
...(runtimeConfig.includeUsage ? { stream_options: { include_usage: true } } : {}),
|
|
655
|
+
...reasoningParams,
|
|
629
656
|
}, signal ? { signal } : undefined);
|
|
630
657
|
// content 内嵌 think 标签由独立增量状态机过滤。它只暂存“可能组成标签”的后缀,
|
|
631
658
|
// 因而既能覆盖标签任意位置跨 chunk,也不会让普通正文固定延迟数个字符。
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { config, getActiveModel } from '../../config/index.js';
|
|
1
|
+
import { config, effectiveReasoningEffort, getActiveModel } from '../../config/index.js';
|
|
2
|
+
import { resolveReasoningParams } from '../reasoning.js';
|
|
2
3
|
import { markStreamInterrupted } from '../stream-interrupt.js';
|
|
3
4
|
let fetchImplOverride = null;
|
|
4
5
|
/** 仅供单测注入;生产路径使用 Node 18+ 全局 fetch。 */
|
|
@@ -179,10 +180,18 @@ function endpoint(baseURL) {
|
|
|
179
180
|
return base;
|
|
180
181
|
return `${base}/v1/messages`;
|
|
181
182
|
}
|
|
182
|
-
export function buildAnthropicRequest(messages, tools, runtime) {
|
|
183
|
+
export function buildAnthropicRequest(messages, tools, runtime, overrides) {
|
|
183
184
|
const runtimeConfig = runtime?.config ?? config;
|
|
184
185
|
const encoded = encodeAnthropicMessages(messages, runtimeConfig.anthropicPromptCache);
|
|
185
186
|
const anthropicTools = encodeAnthropicTools(tools, runtimeConfig.anthropicPromptCache);
|
|
187
|
+
const effort = effectiveReasoningEffort(overrides?.reasoningEffort);
|
|
188
|
+
const reasoningParams = effort === 'auto'
|
|
189
|
+
? {}
|
|
190
|
+
: resolveReasoningParams(effort, {
|
|
191
|
+
provider: 'anthropic',
|
|
192
|
+
model: runtime?.getModel() ?? getActiveModel(),
|
|
193
|
+
maxTokens: runtimeConfig.maxTokens,
|
|
194
|
+
});
|
|
186
195
|
return {
|
|
187
196
|
model: runtime?.getModel() ?? getActiveModel(),
|
|
188
197
|
max_tokens: runtimeConfig.maxTokens ?? 8192,
|
|
@@ -190,6 +199,7 @@ export function buildAnthropicRequest(messages, tools, runtime) {
|
|
|
190
199
|
system: encoded.system,
|
|
191
200
|
messages: encoded.messages,
|
|
192
201
|
...(anthropicTools.length > 0 ? { tools: anthropicTools } : {}),
|
|
202
|
+
...reasoningParams,
|
|
193
203
|
};
|
|
194
204
|
}
|
|
195
205
|
class AnthropicHttpError extends Error {
|
|
@@ -287,7 +297,7 @@ function addLiveCount(text, state) {
|
|
|
287
297
|
}
|
|
288
298
|
}
|
|
289
299
|
/** Anthropic Messages API 单次请求;外层 chat() 继续统一负责重试。 */
|
|
290
|
-
export async function anthropicChatOnce(messages, handlers, signal, tools, runtime) {
|
|
300
|
+
export async function anthropicChatOnce(messages, handlers, signal, tools, runtime, overrides) {
|
|
291
301
|
const runtimeConfig = runtime?.config ?? config;
|
|
292
302
|
const fetchImpl = runtime?.clientState.anthropicFetchImpl ?? defaultAnthropicFetch;
|
|
293
303
|
const response = await fetchImpl(endpoint(runtimeConfig.baseURL), {
|
|
@@ -298,7 +308,7 @@ export async function anthropicChatOnce(messages, handlers, signal, tools, runti
|
|
|
298
308
|
'x-api-key': runtimeConfig.apiKey,
|
|
299
309
|
'anthropic-version': process.env.ANTHROPIC_VERSION || '2023-06-01',
|
|
300
310
|
},
|
|
301
|
-
body: JSON.stringify(buildAnthropicRequest(messages, tools, runtime)),
|
|
311
|
+
body: JSON.stringify(buildAnthropicRequest(messages, tools, runtime, overrides)),
|
|
302
312
|
signal,
|
|
303
313
|
});
|
|
304
314
|
if (!response.ok)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reasoning Effort 适配层(#token-efficiency P3)。
|
|
3
|
+
*
|
|
4
|
+
* 用户只面对归一化五档 `off | low | medium | high | auto`,本模块按 provider +
|
|
5
|
+
* 模型名模式匹配,翻译成各家方言字段:
|
|
6
|
+
* - Anthropic 4.6+:`output_config.effort` + `thinking:{type:'adaptive'}`(新)
|
|
7
|
+
* - Anthropic 旧版:`thinking:{type:'enabled', budget_tokens:N}`(budget 严格小于 max_tokens)
|
|
8
|
+
* - OpenAI o 系 / gpt-5:`reasoning_effort`
|
|
9
|
+
* - Qwen3(vLLM/OpenAI 兼容):`enable_thinking` + `thinking_budget`
|
|
10
|
+
* - GLM-4.5/4.6/5:`thinking:{type:'enabled'|'disabled'}`
|
|
11
|
+
*
|
|
12
|
+
* 白名单驱动:未匹配到已知族的模型一律返回 {} —— 第三方中转可能因严格 schema
|
|
13
|
+
* 校验对未知字段直接 400,不下发是唯一在所有后端都安全的策略。
|
|
14
|
+
*/
|
|
15
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
16
|
+
import { paramsFromCatalog } from '../models/reasoning-cap.js';
|
|
17
|
+
/**
|
|
18
|
+
* Skill 激活期 effort scope(P3 §4.8):嵌套异步树(含子 agent)自动继承。
|
|
19
|
+
* 取值优先级:显式 per-request overrides 参数 > 本 scope > 会话级 getActiveEffort()。
|
|
20
|
+
* 刻意低于显式参数 —— compact/reflect 显式传 low 不被 skill effort 改写。
|
|
21
|
+
*/
|
|
22
|
+
const effortScope = new AsyncLocalStorage();
|
|
23
|
+
export function getScopedEffort() {
|
|
24
|
+
return effortScope.getStore();
|
|
25
|
+
}
|
|
26
|
+
/** 在 skill effort scope 内执行 fn;effort=auto 等同于不包。 */
|
|
27
|
+
export function runWithEffortScope(effort, fn) {
|
|
28
|
+
if (effort === 'auto')
|
|
29
|
+
return fn();
|
|
30
|
+
return effortScope.run(effort, fn);
|
|
31
|
+
}
|
|
32
|
+
const EFFORTS = ['off', 'low', 'medium', 'high', 'auto'];
|
|
33
|
+
/** 解析外部输入(env / 命令行);非法值返回 undefined(调用方决定回落策略)。 */
|
|
34
|
+
export function parseReasoningEffort(value) {
|
|
35
|
+
if (typeof value !== 'string')
|
|
36
|
+
return undefined;
|
|
37
|
+
let v = value.trim();
|
|
38
|
+
if (v.length >= 2 && (v[0] === '"' || v[0] === "'") && v[v.length - 1] === v[0])
|
|
39
|
+
v = v.slice(1, -1);
|
|
40
|
+
v = v.trim().toLowerCase();
|
|
41
|
+
return EFFORTS.includes(v) ? v : undefined;
|
|
42
|
+
}
|
|
43
|
+
/** 各档对应的思考预算(token):Anthropic budget_tokens / Qwen thinking_budget。 */
|
|
44
|
+
const THINK_BUDGET = {
|
|
45
|
+
low: 1024,
|
|
46
|
+
medium: 4096,
|
|
47
|
+
high: 10240,
|
|
48
|
+
};
|
|
49
|
+
/** Anthropic:为正文预留的最小 token;budget = min(raw, maxTokens - RESERVED)。 */
|
|
50
|
+
const ANTHROPIC_OUTPUT_RESERVED = 1024;
|
|
51
|
+
const ANTHROPIC_DEFAULT_MAX = 8192;
|
|
52
|
+
/**
|
|
53
|
+
* 目标模型是否识别 reasoning 参数(白名单命中)。
|
|
54
|
+
* 供 /model show 给出「参数不会下发」提示,避免用户设了 high 却静默无效。
|
|
55
|
+
*/
|
|
56
|
+
export function recognizesReasoning(target) {
|
|
57
|
+
// 目录来源显式声明了思考能力 → 直接采纳(含声明 false 时不识别)。
|
|
58
|
+
if (target.catalogProviderId && typeof target.catalogReasoning === 'boolean') {
|
|
59
|
+
return target.catalogReasoning;
|
|
60
|
+
}
|
|
61
|
+
if (target.provider === 'anthropic')
|
|
62
|
+
return true;
|
|
63
|
+
const m = target.model.toLowerCase();
|
|
64
|
+
return (/\bo[134]\b/.test(m) ||
|
|
65
|
+
/gpt-?5/.test(m) ||
|
|
66
|
+
/qwen[-_]?3/.test(m) ||
|
|
67
|
+
/deepseek[-_]?r1/.test(m) ||
|
|
68
|
+
/glm[-_]?4[.-]?[56]/.test(m) ||
|
|
69
|
+
/glm[-_]?5/.test(m) ||
|
|
70
|
+
/doubao[-_]?seed/.test(m));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 归一化 effort + 目标 → 要 merge 进请求体的字段(空对象 = 不下发)。纯函数。
|
|
74
|
+
*/
|
|
75
|
+
export function resolveReasoningParams(effort, target) {
|
|
76
|
+
if (effort === 'auto')
|
|
77
|
+
return {};
|
|
78
|
+
// 目录优先:带 catalogProviderId 时按目录能力+厂商方言产出。
|
|
79
|
+
// 注意 anthropic 路径的思考在 anthropic provider 内仍走它自己的 target.provider,
|
|
80
|
+
// 目录只对 OpenAI 兼容路径有意义;Anthropic 官方协议继续走下方既有分支。
|
|
81
|
+
if (target.catalogProviderId && target.provider !== 'anthropic') {
|
|
82
|
+
const fromCatalog = paramsFromCatalog({
|
|
83
|
+
effort,
|
|
84
|
+
catalogProviderId: target.catalogProviderId,
|
|
85
|
+
reasoningOptions: target.catalogReasoningOptions,
|
|
86
|
+
maxTokens: target.maxTokens,
|
|
87
|
+
});
|
|
88
|
+
// 目录能产出非空字段就用;方言未知/返 {} 则落到正则兜底(而不是直接放弃)。
|
|
89
|
+
if (Object.keys(fromCatalog).length > 0)
|
|
90
|
+
return fromCatalog;
|
|
91
|
+
}
|
|
92
|
+
if (target.provider === 'anthropic') {
|
|
93
|
+
const m = target.model.toLowerCase();
|
|
94
|
+
// 仅认显式分隔的版本号 4-6/4.6/4_6/4-10…;宽松写法会误匹配名字里的日期(…20241022 中的 410)。
|
|
95
|
+
const is46Plus = /4[.\-_](?:[6-9]|[1-9]\d)(?!\d)/.test(m);
|
|
96
|
+
if (is46Plus) {
|
|
97
|
+
if (effort === 'off')
|
|
98
|
+
return { thinking: { type: 'disabled' } };
|
|
99
|
+
return { output_config: { effort }, thinking: { type: 'adaptive' } };
|
|
100
|
+
}
|
|
101
|
+
if (effort === 'off')
|
|
102
|
+
return {};
|
|
103
|
+
// 旧版:budget 必须严格小于 max_tokens,clamp 后非正 → 无法启用,不下发。
|
|
104
|
+
const maxTokens = target.maxTokens ?? ANTHROPIC_DEFAULT_MAX;
|
|
105
|
+
const budget = Math.min(THINK_BUDGET[effort], maxTokens - ANTHROPIC_OUTPUT_RESERVED);
|
|
106
|
+
if (budget <= 0)
|
|
107
|
+
return {};
|
|
108
|
+
return { thinking: { type: 'enabled', budget_tokens: budget } };
|
|
109
|
+
}
|
|
110
|
+
// ── OpenAI 兼容路径 ──
|
|
111
|
+
const m = target.model.toLowerCase();
|
|
112
|
+
// Qwen3(先于通用规则:名字里不含冲突模式,但显式更清晰)。
|
|
113
|
+
if (/qwen[-_]?3/.test(m)) {
|
|
114
|
+
if (effort === 'off')
|
|
115
|
+
return { enable_thinking: false };
|
|
116
|
+
return { enable_thinking: true, thinking_budget: THINK_BUDGET[effort] };
|
|
117
|
+
}
|
|
118
|
+
// GLM-4.5/4.6/5。
|
|
119
|
+
if (/glm[-_]?4[.-]?[56]/.test(m) || /glm[-_]?5/.test(m)) {
|
|
120
|
+
if (effort === 'off')
|
|
121
|
+
return { thinking: { type: 'disabled' } };
|
|
122
|
+
return { thinking: { type: 'enabled' } };
|
|
123
|
+
}
|
|
124
|
+
// 豆包 Seed(火山 Ark):思考是开关式,无 low/medium/high 预算档——非 off 即启用。
|
|
125
|
+
if (/doubao[-_]?seed/.test(m)) {
|
|
126
|
+
if (effort === 'off')
|
|
127
|
+
return { thinking: { type: 'disabled' } };
|
|
128
|
+
return { thinking: { type: 'enabled' } };
|
|
129
|
+
}
|
|
130
|
+
// DeepSeek-R1:无开关参数,任何档位都不下发。
|
|
131
|
+
if (/deepseek[-_]?r1/.test(m))
|
|
132
|
+
return {};
|
|
133
|
+
// OpenAI o1/o3/o4 + gpt-5:off 无法真关,官方最低档 low。
|
|
134
|
+
if (/\bo[134]\b/.test(m) || /gpt-?5/.test(m)) {
|
|
135
|
+
if (effort === 'off')
|
|
136
|
+
return { reasoning_effort: 'low' };
|
|
137
|
+
return { reasoning_effort: effort };
|
|
138
|
+
}
|
|
139
|
+
// 未知模型/第三方网关:白名单策略,不下发。
|
|
140
|
+
return {};
|
|
141
|
+
}
|
package/dist/memory/reflect.js
CHANGED
|
@@ -147,7 +147,7 @@ export async function runReflection(transcript, signal) {
|
|
|
147
147
|
};
|
|
148
148
|
let content = null;
|
|
149
149
|
try {
|
|
150
|
-
const r = await chat([sys, user], {}, signal ?? AbortSignal.timeout(60000));
|
|
150
|
+
const r = await chat([sys, user], {}, signal ?? AbortSignal.timeout(60000), undefined, { reasoningEffort: 'low' });
|
|
151
151
|
// 推理模型偶发只返 reasoning_content(content null)或幻觉 tool_calls → 视为无产出
|
|
152
152
|
if (!r.toolCalls.length && r.content)
|
|
153
153
|
content = r.content;
|
package/dist/memory/store.js
CHANGED
|
@@ -12,6 +12,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from '
|
|
|
12
12
|
import os from 'node:os';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { MAX_ACTIVE, MAX_INDEX_ENTRIES, MAX_MEMORY_ENTRY, DECAY_DAYS, GC_DAYS } from '../tools/constants.js';
|
|
15
|
+
import { TextSearchIndex } from '../context/text-search.js';
|
|
15
16
|
// ── 路径 ──────────────────────────────────────────────────────────────────
|
|
16
17
|
function globalPath() {
|
|
17
18
|
return path.join(os.homedir(), '.mocode', 'memory.jsonl');
|
|
@@ -139,57 +140,59 @@ export function saveEntry(input) {
|
|
|
139
140
|
writeAtomic(p, fileEntries);
|
|
140
141
|
return { ok: true, id };
|
|
141
142
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const summary = e.summary.toLowerCase();
|
|
148
|
-
const body = e.body.toLowerCase();
|
|
149
|
-
let s = 0;
|
|
150
|
-
for (const t of terms) {
|
|
151
|
-
if (id.includes(t))
|
|
152
|
-
s += 8; // 含 id 匹配:模型常按索引里的 id 取详情(slug 带连字符,名字带空格,不单独匹配 id 会漏)
|
|
153
|
-
if (name.includes(t))
|
|
154
|
-
s += 10;
|
|
155
|
-
if (summary.includes(t))
|
|
156
|
-
s += 5;
|
|
157
|
-
if (body.includes(t))
|
|
158
|
-
s += 1;
|
|
159
|
-
}
|
|
160
|
-
return s;
|
|
161
|
-
}
|
|
162
|
-
/** 关键词搜索:多词子串匹配(name 权重最高)。命中即 bump recallCount/lastRecalledAt 写回(遗忘衰减依据)。 */
|
|
143
|
+
/**
|
|
144
|
+
* 关键词搜索:CJK 感知 BM25(共享 context/text-search),字段权重 name 10 > id 8 >
|
|
145
|
+
* summary 5 > body 1。命中即 bump recallCount/lastRecalledAt 写回(遗忘衰减依据)。
|
|
146
|
+
* 空查询:按旧行为返回前 limit 条(全命中)。
|
|
147
|
+
*/
|
|
163
148
|
export function searchEntries(query, opts = {}) {
|
|
164
149
|
const all = loadAll();
|
|
165
150
|
const status = opts.status ?? 'active';
|
|
166
151
|
const pool = all
|
|
167
152
|
.filter((e) => (status === 'any' ? true : e.status === status))
|
|
168
153
|
.filter((e) => (opts.type ? e.type === opts.type : true));
|
|
169
|
-
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
170
|
-
const scored = pool.map((e) => ({ e, s: scoreEntry(e, terms) })).filter((x) => x.s > 0);
|
|
171
|
-
scored.sort((a, b) => b.s - a.s);
|
|
172
154
|
const limit = Math.max(1, Math.min(opts.limit ?? 5, 20));
|
|
173
|
-
const
|
|
155
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
156
|
+
const top = [];
|
|
157
|
+
if (terms.length === 0) {
|
|
158
|
+
top.push(...pool.slice(0, limit));
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
const index = new TextSearchIndex(pool.map((e) => ({
|
|
162
|
+
id: e.id,
|
|
163
|
+
fields: [
|
|
164
|
+
{ text: e.name, weight: 10 },
|
|
165
|
+
{ text: e.id, weight: 8 },
|
|
166
|
+
{ text: e.summary, weight: 5 },
|
|
167
|
+
{ text: e.body, weight: 1 },
|
|
168
|
+
],
|
|
169
|
+
})));
|
|
170
|
+
const byId = new Map(pool.map((e) => [e.id, e]));
|
|
171
|
+
for (const hit of index.search(query, limit)) {
|
|
172
|
+
const e = byId.get(hit.id);
|
|
173
|
+
if (e)
|
|
174
|
+
top.push(e);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
174
177
|
if (top.length === 0)
|
|
175
178
|
return [];
|
|
176
179
|
// bump recall:只写有命中的 scope 文件
|
|
177
180
|
const now = nowIso();
|
|
178
|
-
const hitIds = new Set(top.map((x) => x.
|
|
181
|
+
const hitIds = new Set(top.map((x) => x.id));
|
|
179
182
|
for (const e of all) {
|
|
180
183
|
if (hitIds.has(e.id)) {
|
|
181
184
|
e.recallCount++;
|
|
182
185
|
e.lastRecalledAt = now;
|
|
183
186
|
}
|
|
184
187
|
}
|
|
185
|
-
const hitScopes = new Set(top.map((x) => x.
|
|
188
|
+
const hitScopes = new Set(top.map((x) => x.scope));
|
|
186
189
|
for (const scope of hitScopes) {
|
|
187
190
|
const p = pathForScope(scope);
|
|
188
191
|
const entries = all.filter((e) => e.scope === scope);
|
|
189
192
|
ensureDir(p);
|
|
190
193
|
writeAtomic(p, entries);
|
|
191
194
|
}
|
|
192
|
-
return top
|
|
195
|
+
return top;
|
|
193
196
|
}
|
|
194
197
|
/** 索引(无 body、不 bump recall)。 */
|
|
195
198
|
export function listEntries(opts = {}) {
|