cortico-world-memory 0.1.0 → 0.1.2
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/package.json +2 -2
- package/src/ENV_PROMPT.md +1 -1
- package/src/config.ts +17 -1
- package/src/fact-store.ts +29 -0
- package/src/vector-store.ts +70 -0
- package/src/world.ts +78 -5
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cortico-world-memory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Cortico World
|
|
5
|
+
"description": "Cortico World 扩展:五层记忆(事实库/知识库/反思/人格/档案)+ 三级记忆底座 + Hybrid Recall 向量混合检索(移植自 E:/qq_bot 的记忆系统与 memory_server 向量 sidecar,qq_bot 源码零改动)。",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"cortico-world",
|
package/src/ENV_PROMPT.md
CHANGED
package/src/config.ts
CHANGED
|
@@ -18,6 +18,16 @@ export interface MemoryConfig {
|
|
|
18
18
|
vectorTopK: number;
|
|
19
19
|
/** 环境提示词里常驻注入的「重要信息」条数上限。 */
|
|
20
20
|
notesInPrompt: number;
|
|
21
|
+
/**
|
|
22
|
+
* 自动捕获:开启后周期扫描事件流,把外部入站用户消息自动沉淀进事实库/向量库。
|
|
23
|
+
* world 无关——只认 `origin:'external'` 的入站事件,QQ 没开也能抓其它 world 的对话,
|
|
24
|
+
* QQ 开了就自动沉淀 QQ 聊天(无需 bot 手动调 mem_remember)。
|
|
25
|
+
*/
|
|
26
|
+
autoCapture: boolean;
|
|
27
|
+
/** 自动捕获的轮询间隔(毫秒)。 */
|
|
28
|
+
captureIntervalMs: number;
|
|
29
|
+
/** 短于此长度的入站消息不沉淀(过滤表情/口令噪声)。 */
|
|
30
|
+
captureMinLength: number;
|
|
21
31
|
/**
|
|
22
32
|
* 向量来源:
|
|
23
33
|
* - `off` 纯 BM25 关键词检索(零依赖,永远可用);
|
|
@@ -43,6 +53,9 @@ export const MEMORY_DEFAULTS: MemoryConfig = {
|
|
|
43
53
|
observeIntervalMs: 30000,
|
|
44
54
|
vectorTopK: 5,
|
|
45
55
|
notesInPrompt: 10,
|
|
56
|
+
autoCapture: true,
|
|
57
|
+
captureIntervalMs: 20000,
|
|
58
|
+
captureMinLength: 2,
|
|
46
59
|
embeddingProvider: 'off',
|
|
47
60
|
embeddingEndpoint: '',
|
|
48
61
|
embeddingModel: '',
|
|
@@ -57,13 +70,16 @@ export const MEMORY_CONFIG_GROUP: ConfigGroup = {
|
|
|
57
70
|
schema: {
|
|
58
71
|
type: 'object',
|
|
59
72
|
title: '记忆系统',
|
|
60
|
-
description: '
|
|
73
|
+
description: '五层记忆(事实库/知识库/反思/人格/档案)+ 三级记忆底座(短期/摘要/话题)与 Hybrid Recall 向量混合检索(移植自 qq_bot)。',
|
|
61
74
|
properties: {
|
|
62
75
|
'worlds.memory.maxHistory': { type: 'number', title: '短期窗口(条)', description: '每个 scope 保留的最近对话条数。' },
|
|
63
76
|
'worlds.memory.maxVectorsPerScope': { type: 'number', title: '向量上限(条/scope)', description: '超出截掉最旧的。' },
|
|
64
77
|
'worlds.memory.observeIntervalMs': { type: 'number', title: '记忆推送周期(ms)', description: '把话题/摘要/要点推给人格的周期。', 'x-hot': true },
|
|
65
78
|
'worlds.memory.vectorTopK': { type: 'number', title: '检索条数', description: 'mem_recall 默认返回条数。' },
|
|
66
79
|
'worlds.memory.notesInPrompt': { type: 'number', title: '要点注入条数', description: '环境提示词常驻的「重要信息」上限。' },
|
|
80
|
+
'worlds.memory.autoCapture': { type: 'boolean', title: '自动捕获对话', description: '周期扫描事件流,把外部入站用户消息自动沉淀进记忆(world 无关,QQ 没开也能用)。', 'x-hot': true },
|
|
81
|
+
'worlds.memory.captureIntervalMs': { type: 'number', title: '捕获扫描周期(ms)', description: '自动捕获的轮询间隔,默认 20s。', 'x-hot': true },
|
|
82
|
+
'worlds.memory.captureMinLength': { type: 'number', title: '捕获最小长度', description: '短于此长度的入站消息不沉淀。' },
|
|
67
83
|
'worlds.memory.embeddingProvider': {
|
|
68
84
|
type: 'string', title: '向量来源', enum: ['off', 'http', 'sidecar'],
|
|
69
85
|
description: 'off=纯 BM25;http=OpenAI 兼容 /embeddings;sidecar=qq_bot memory_server。失败自动降级 BM25。',
|
package/src/fact-store.ts
CHANGED
|
@@ -111,4 +111,33 @@ export class FactStore {
|
|
|
111
111
|
if (!scope) return Object.values(this.data).reduce((s, l) => s + l.length, 0);
|
|
112
112
|
return (this.data[scope] ?? []).length;
|
|
113
113
|
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 跨 scope 召回:扫描全部 scope 的事实,按词频打分、按 hash 去重、取 top_k。
|
|
117
|
+
* 用于「自动捕获的对话」与显式事实共存的场景——scope 留空时也能搜到对话域。
|
|
118
|
+
*/
|
|
119
|
+
recallAll(query: string, limit = 5): Fact[] {
|
|
120
|
+
const q = query.trim().toLowerCase();
|
|
121
|
+
if (!q) return [];
|
|
122
|
+
const terms = q.split(/\s+/).filter(Boolean);
|
|
123
|
+
const scored: Array<{ f: Fact; score: number }> = [];
|
|
124
|
+
for (const list of Object.values(this.data)) {
|
|
125
|
+
for (const f of list) {
|
|
126
|
+
const text = f.text.toLowerCase();
|
|
127
|
+
let score = 0;
|
|
128
|
+
for (const term of terms) if (text.includes(term)) score += 1;
|
|
129
|
+
if (score > 0) scored.push({ f, score });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// 同一事实可能命中多 scope(文本相同 hash 不同),按 hash 去重保留最高分
|
|
133
|
+
const best = new Map<string, { f: Fact; score: number }>();
|
|
134
|
+
for (const x of scored) {
|
|
135
|
+
const cur = best.get(x.f.hash);
|
|
136
|
+
if (!cur || x.score > cur.score) best.set(x.f.hash, x);
|
|
137
|
+
}
|
|
138
|
+
return [...best.values()]
|
|
139
|
+
.sort((a, b) => b.score - a.score)
|
|
140
|
+
.slice(0, Math.max(1, limit))
|
|
141
|
+
.map((x) => x.f);
|
|
142
|
+
}
|
|
114
143
|
}
|
package/src/vector-store.ts
CHANGED
|
@@ -288,6 +288,76 @@ export class VectorStore {
|
|
|
288
288
|
}));
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
+
/** BM25 检索(全局条目池),返回 [下标, 分数] 降序(>0)。 */
|
|
292
|
+
private bm25SearchAll(items: VectorItem[], query: string, topK: number): Array<{ idx: number; score: number }> {
|
|
293
|
+
if (!items.length) return [];
|
|
294
|
+
const queryTokens = tokenizeCjkNgram(query);
|
|
295
|
+
if (!queryTokens.length) return [];
|
|
296
|
+
const docTokens = items.map((it) => tokenizeCjkNgram(it.text));
|
|
297
|
+
const scores = bm25Score(queryTokens, docTokens);
|
|
298
|
+
return scores
|
|
299
|
+
.map((score, idx) => ({ idx, score }))
|
|
300
|
+
.filter((x) => x.score > 0)
|
|
301
|
+
.sort((a, b) => b.score - a.score)
|
|
302
|
+
.slice(0, topK);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** 点积检索(全局条目池,逐条解码构建矩阵),返回 [下标, 分数] 降序(>0.3)。 */
|
|
306
|
+
private cosineSearchAll(items: VectorItem[], queryVector: readonly number[], topK: number): Array<{ idx: number; score: number }> {
|
|
307
|
+
let dim = 0;
|
|
308
|
+
const rows: number[] = [];
|
|
309
|
+
const chunks: Float32Array[] = [];
|
|
310
|
+
for (let i = 0; i < items.length; i++) {
|
|
311
|
+
const enc = items[i]!.vector;
|
|
312
|
+
if (!enc) continue;
|
|
313
|
+
const v = l2Normalize(decodeVectorFp16(enc));
|
|
314
|
+
if (dim === 0) dim = v.length;
|
|
315
|
+
if (v.length !== dim) continue; // 维度不齐(换过模型)跳过
|
|
316
|
+
chunks.push(v);
|
|
317
|
+
rows.push(i);
|
|
318
|
+
}
|
|
319
|
+
if (!rows.length || queryVector.length !== dim) return [];
|
|
320
|
+
const q = l2Normalize(Float32Array.from(queryVector));
|
|
321
|
+
const matrix = new Float32Array(rows.length * dim);
|
|
322
|
+
chunks.forEach((v, r) => matrix.set(v, r * dim));
|
|
323
|
+
const scores = new Float32Array(rows.length);
|
|
324
|
+
for (let r = 0; r < rows.length; r++) {
|
|
325
|
+
let dot = 0;
|
|
326
|
+
const base = r * dim;
|
|
327
|
+
for (let d = 0; d < dim; d++) dot += matrix[base + d]! * q[d]!;
|
|
328
|
+
scores[r] = dot;
|
|
329
|
+
}
|
|
330
|
+
return [...Array(rows.length).keys()]
|
|
331
|
+
.filter((r) => scores[r]! > 0.3)
|
|
332
|
+
.sort((a, b) => scores[b]! - scores[a]!)
|
|
333
|
+
.slice(0, topK)
|
|
334
|
+
.map((r) => ({ idx: rows[r]!, score: scores[r]! }));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* 跨 scope Hybrid Recall:把所有 scope 的条目汇总成一个全局池,跑 BM25 + Cosine + RRF。
|
|
339
|
+
* 用于「自动捕获的对话」与显式事实共存时,scope 留空也能搜到对话域。
|
|
340
|
+
*/
|
|
341
|
+
hybridSearchAll(query: string, queryVector: readonly number[] | null, topK: number): SearchHit[] {
|
|
342
|
+
const all: VectorItem[] = [];
|
|
343
|
+
for (const list of Object.values(this.data.vectors)) for (const it of list) all.push(it);
|
|
344
|
+
if (!all.length) return [];
|
|
345
|
+
const bm25 = this.bm25SearchAll(all, query, topK * 2);
|
|
346
|
+
const cosine = queryVector ? this.cosineSearchAll(all, queryVector, topK * 2) : [];
|
|
347
|
+
if (!bm25.length && !cosine.length) return [];
|
|
348
|
+
const fused = rrfFusion([bm25, cosine]);
|
|
349
|
+
const viaVector = new Set(cosine.map((c) => c.idx));
|
|
350
|
+
const viaBm25 = new Set(bm25.map((c) => c.idx));
|
|
351
|
+
return [...fused.entries()]
|
|
352
|
+
.sort((a, b) => b[1] - a[1])
|
|
353
|
+
.slice(0, topK)
|
|
354
|
+
.map(([idx, score]) => ({
|
|
355
|
+
text: all[idx]!.text,
|
|
356
|
+
score: +score.toFixed(4),
|
|
357
|
+
via: viaBm25.has(idx) && viaVector.has(idx) ? 'both' : viaVector.has(idx) ? 'vector' : 'bm25',
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
|
|
291
361
|
/** 统计信息,对齐 `get_vector_stats`。 */
|
|
292
362
|
stats(): { scopes: number; total: number; indexed: number; model: string } {
|
|
293
363
|
const vectors = this.data.vectors;
|
package/src/world.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import type { World, WorldHost, ToolDef, ToolTag } from 'cortico/core/types.ts';
|
|
3
|
+
import type { World, WorldHost, ToolDef, ToolTag, EventEnvelope } from 'cortico/core/types.ts';
|
|
4
4
|
import type { WorldContext } from 'cortico/world.ts';
|
|
5
5
|
import type { MemoryConfig } from './config.ts';
|
|
6
6
|
import { MemoryBase } from './memory.ts';
|
|
@@ -15,6 +15,22 @@ import { buildEventText, buildView, promptVars } from './observe.ts';
|
|
|
15
15
|
|
|
16
16
|
const ENV_PROMPT_FILE = fileURLToPath(new URL('./ENV_PROMPT.md', import.meta.url));
|
|
17
17
|
|
|
18
|
+
/** 清理自动捕获的入站文本:去掉 QQ 的「【QQ群 ...】」前缀(对其它 world 无前缀,无副作用),压缩空白。 */
|
|
19
|
+
function cleanCaptureText(t: string): string {
|
|
20
|
+
return (t ?? '')
|
|
21
|
+
.replace(/^【[^】]*】\s*/u, '')
|
|
22
|
+
.replace(/\s+/g, ' ')
|
|
23
|
+
.trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 自动捕获时判断事件是否为「真人入站消息」(world 无关:只认 origin='external' + 非 bot 自言)。 */
|
|
27
|
+
function isInboundUser(e: EventEnvelope): boolean {
|
|
28
|
+
if (e.origin !== 'external') return false; // 排除 internal 回灌 / 记忆自身推送
|
|
29
|
+
const role = (e.meta?.role as string) ?? '';
|
|
30
|
+
if (role === 'assistant' || role === 'bot' || e.meta?.self === true) return false; // 排除 bot 自己
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
18
34
|
export class MemoryWorld implements World {
|
|
19
35
|
readonly id = 'memory';
|
|
20
36
|
private host: WorldHost | null = null;
|
|
@@ -28,6 +44,8 @@ export class MemoryWorld implements World {
|
|
|
28
44
|
private persona: PersonaStore | null = null;
|
|
29
45
|
private profile: ProfileStore | null = null;
|
|
30
46
|
private embedder: EmbeddingClient | null = null;
|
|
47
|
+
private lastCursor = 0; // 自动捕获已扫到的事件游标(启动=当前尾,不回灌历史)
|
|
48
|
+
private captureTimer: ReturnType<typeof setInterval> | null = null;
|
|
31
49
|
|
|
32
50
|
constructor(private readonly ctx: WorldContext<MemoryConfig>) {}
|
|
33
51
|
|
|
@@ -94,12 +112,17 @@ export class MemoryWorld implements World {
|
|
|
94
112
|
}, Math.max(10_000, this.cfg.observeIntervalMs));
|
|
95
113
|
|
|
96
114
|
this.push('memory.context', buildEventText(this.view()));
|
|
115
|
+
|
|
116
|
+
// 自动捕获:从当前事件尾开始(不回灌历史),周期扫描外部入站消息沉淀进记忆。
|
|
117
|
+
try { this.lastCursor = (host.store?.latestCursor?.() ?? 0) || 0; } catch { this.lastCursor = 0; }
|
|
118
|
+
if (this.cfg.autoCapture) this.startCapture();
|
|
97
119
|
}
|
|
98
120
|
|
|
99
121
|
/** stop 必须 async 返回 Promise(框架 withDeadline 直接 work.then)。 */
|
|
100
122
|
async stop(): Promise<void> {
|
|
101
123
|
if (this.timer) clearInterval(this.timer);
|
|
102
124
|
this.timer = null;
|
|
125
|
+
if (this.captureTimer) { clearInterval(this.captureTimer); this.captureTimer = null; }
|
|
103
126
|
this.mem?.flush();
|
|
104
127
|
this.vectors?.flush(); // 防抖写盘强制落盘
|
|
105
128
|
this.facts?.flush();
|
|
@@ -109,6 +132,48 @@ export class MemoryWorld implements World {
|
|
|
109
132
|
this.profile?.flush();
|
|
110
133
|
}
|
|
111
134
|
|
|
135
|
+
/** 启动自动捕获轮询(尊重暂停开关)。 */
|
|
136
|
+
private startCapture(): void {
|
|
137
|
+
const ms = Math.max(5000, Number(this.cfg.captureIntervalMs) || 20000);
|
|
138
|
+
if (this.captureTimer) clearInterval(this.captureTimer);
|
|
139
|
+
this.captureTimer = setInterval(() => { void this.captureIncoming(); }, ms);
|
|
140
|
+
this.captureTimer.unref?.();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 扫描事件流、把外部入站用户消息自动沉淀进事实库/向量库。
|
|
145
|
+
* world 无关:只认 `origin:'external'` + 非 bot 自言——QQ 没开时抓其它 world,QQ 开了就自动沉淀 QQ 聊天。
|
|
146
|
+
* 记忆自身的 internal 推送不在此列(被 origin 过滤),不会自我循环捕获。
|
|
147
|
+
* 返回本次新增条数。
|
|
148
|
+
*/
|
|
149
|
+
private async captureIncoming(): Promise<number> {
|
|
150
|
+
const host = this.host;
|
|
151
|
+
if (!host) return 0;
|
|
152
|
+
if (host.isPaused?.() === true) return 0; // 与框架暂停语义一致
|
|
153
|
+
const store = host.store;
|
|
154
|
+
if (!store || !store.range) return 0;
|
|
155
|
+
const to = (store.latestCursor?.() ?? this.lastCursor) || 0;
|
|
156
|
+
if (!Number.isFinite(to) || to <= this.lastCursor) return 0;
|
|
157
|
+
const events = (store.range({ fromCursor: this.lastCursor + 1, toCursor: to }) ?? []) as EventEnvelope[];
|
|
158
|
+
let captured = 0;
|
|
159
|
+
for (const e of events) {
|
|
160
|
+
if (!isInboundUser(e)) continue;
|
|
161
|
+
const text = cleanCaptureText(e.text ?? '');
|
|
162
|
+
if (text.length < (this.cfg.captureMinLength ?? 2)) continue;
|
|
163
|
+
// scope 取对话地址(senderKey,如群号/私聊号);没有则按来源(source)分域——QQ 没开也不报错。
|
|
164
|
+
const scope = (typeof e.senderKey === 'string' && e.senderKey.trim())
|
|
165
|
+
? e.senderKey.trim()
|
|
166
|
+
: (typeof e.source === 'string' && e.source ? e.source : 'main');
|
|
167
|
+
this.facts!.add(scope, [text]);
|
|
168
|
+
const vec = this.cfg.embeddingProvider === 'off' ? null : await this.embedder!.embed(text);
|
|
169
|
+
this.vectors!.add(scope, text, vec, this.cfg.maxVectorsPerScope);
|
|
170
|
+
captured++;
|
|
171
|
+
}
|
|
172
|
+
this.lastCursor = to;
|
|
173
|
+
if (captured > 0) { this.facts!.flush(); this.vectors!.flush(); }
|
|
174
|
+
return captured;
|
|
175
|
+
}
|
|
176
|
+
|
|
112
177
|
private view() {
|
|
113
178
|
return buildView(this.mem!, this.vectors!, 'main', this.cfg.notesInPrompt, this.profile);
|
|
114
179
|
}
|
|
@@ -160,20 +225,23 @@ export class MemoryWorld implements World {
|
|
|
160
225
|
this.vectors!.add(scope, text, this.cfg.embeddingProvider === 'off' ? null : await this.embedder!.embed(text), this.cfg.maxVectorsPerScope);
|
|
161
226
|
return added ? `OK 已记住(${scope}): ${text.slice(0, 80)}` : `OK 已存在,跳过重复(${scope})`;
|
|
162
227
|
}),
|
|
163
|
-
mem_recall: mk('mem_recall', '按语义+关键词混合检索长期记忆(事实库全文 + BM25+向量+RRF 融合),找旧账/回忆约定时用。', { query: str('查询'), top_k: num('条数'), scope: str('记忆域') }, ['query'], ['read'],
|
|
228
|
+
mem_recall: mk('mem_recall', '按语义+关键词混合检索长期记忆(事实库全文 + BM25+向量+RRF 融合),找旧账/回忆约定时用。scope 留空或填 main = 跨全部记忆域(含自动捕获的对话);填具体 scope 只搜该域。', { query: str('查询'), top_k: num('条数'), scope: str('记忆域(留空=全部)') }, ['query'], ['read'],
|
|
164
229
|
async (a) => {
|
|
165
230
|
const scope = scopeOf(a);
|
|
166
231
|
const topK = Math.min(20, Number(a.top_k) || this.cfg.vectorTopK);
|
|
167
232
|
const query = String(a.query);
|
|
168
233
|
const vector = this.cfg.embeddingProvider === 'off' ? null : await this.embedder!.embed(query);
|
|
169
|
-
const
|
|
170
|
-
const
|
|
234
|
+
const global = scope === 'main'; // main = 搜全部域(对话域也含)
|
|
235
|
+
const hits = global
|
|
236
|
+
? this.vectors!.hybridSearchAll(query, vector, topK)
|
|
237
|
+
: this.vectors!.hybridSearch(scope, query, vector, topK);
|
|
238
|
+
const facts = global ? this.facts!.recallAll(query, topK) : this.facts!.recall(scope, query, topK);
|
|
171
239
|
// 合并两路结果,按 text 去重,事实库命中的优先(更精确)。
|
|
172
240
|
const seen = new Set<string>();
|
|
173
241
|
const out: string[] = [];
|
|
174
242
|
for (const f of facts) if (!seen.has(f.text)) { seen.add(f.text); out.push(f.text); }
|
|
175
243
|
for (const h of hits) if (!seen.has(h.text)) { seen.add(h.text); out.push(h.text); }
|
|
176
|
-
if (!out.length) return `OK ${scope} 里没有匹配「${query}」的记忆`;
|
|
244
|
+
if (!out.length) return `OK ${global ? '全部域' : scope} 里没有匹配「${query}」的记忆`;
|
|
177
245
|
return `OK ${out.slice(0, topK).map((t, i) => `${i + 1}. ${t}`).join('\n')}`;
|
|
178
246
|
}),
|
|
179
247
|
mem_recent: mk('mem_recent', '回看本域最近的短期对话(最多 maxHistory 条)。', { n: num('条数'), scope: str('记忆域') }, [], ['read', 'snapshot'],
|
|
@@ -329,6 +397,11 @@ export class MemoryWorld implements World {
|
|
|
329
397
|
maxHistory: this.cfg.maxHistory,
|
|
330
398
|
})}`;
|
|
331
399
|
}),
|
|
400
|
+
mem_capture_now: mk('mem_capture_now', '立刻扫描事件流,把自上次扫描以来的外部入站用户消息沉淀进记忆(自动捕获的手动触发,无需等轮询周期)。QQ 没开时也可抓其它 world 的对话。', {}, [], ['act'],
|
|
401
|
+
async () => {
|
|
402
|
+
const captured = await this.captureIncoming();
|
|
403
|
+
return `OK 本次捕获 ${captured} 条;当前游标 ${this.lastCursor}`;
|
|
404
|
+
}),
|
|
332
405
|
mem_flush: mk('mem_flush', '把记忆立即落盘(平时自动防抖落盘,一般不用调)。', {}, [], ['act'],
|
|
333
406
|
async () => { this.mem?.flush(); this.vectors?.flush(); this.facts?.flush(); this.kb?.flush(); this.reflect?.flush(); this.persona?.flush(); this.profile?.flush(); return 'OK 已落盘'; }),
|
|
334
407
|
};
|