koishi-plugin-satori-ai 2.0.0 → 2.0.1
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/lib/index.js +325 -98
- package/lib/memory/index.d.ts +1 -0
- package/lib/memory/profile.d.ts +1 -0
- package/lib/memory/repository.d.ts +4 -1
- package/lib/memory/summarizer.d.ts +39 -4
- package/lib/types.d.ts +1 -0
- package/lib/utils.d.ts +4 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -170,6 +170,10 @@ __name(extendDatabase, "extendDatabase");
|
|
|
170
170
|
var fs = __toESM(require("fs"));
|
|
171
171
|
var path = __toESM(require("path"));
|
|
172
172
|
var logger = new import_koishi.Logger("satori-utils");
|
|
173
|
+
function escapeRegExp(str) {
|
|
174
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
175
|
+
}
|
|
176
|
+
__name(escapeRegExp, "escapeRegExp");
|
|
173
177
|
function parseTimeToMinutes(timeStr) {
|
|
174
178
|
const [hours, minutes] = timeStr.split(":").map(Number);
|
|
175
179
|
return hours * 60 + (minutes || 0);
|
|
@@ -252,16 +256,36 @@ function isErrorWithMessage(error) {
|
|
|
252
256
|
return typeof error === "object" && error !== null && "message" in error;
|
|
253
257
|
}
|
|
254
258
|
__name(isErrorWithMessage, "isErrorWithMessage");
|
|
255
|
-
function processPrompt(prompt) {
|
|
259
|
+
function processPrompt(prompt, botNames = [], botIds = []) {
|
|
256
260
|
if (!prompt)
|
|
257
261
|
return "";
|
|
258
262
|
if (prompt.includes(":poke"))
|
|
259
263
|
return "戳戳";
|
|
264
|
+
const names = botNames.filter(Boolean);
|
|
265
|
+
const ids = botIds.filter(Boolean);
|
|
266
|
+
prompt = prompt.replace(/<at\b[^>]*?name="([^\"]*)"[^>]*>/gi, (tag, name) => {
|
|
267
|
+
const id = /\bid="([^"]*)"/i.exec(tag)?.[1];
|
|
268
|
+
if (ids.includes(id) || names.includes(name))
|
|
269
|
+
return "";
|
|
270
|
+
return name;
|
|
271
|
+
});
|
|
272
|
+
prompt = prompt.replace(/<at\b[^>]*\/?>/gi, "");
|
|
260
273
|
prompt = prompt.replace(/<[^>]*?avatar[^>]*>/g, "。回复:");
|
|
261
274
|
prompt = prompt.replace(/<[^>]*?img[^>]*>/g, "[图片]");
|
|
262
275
|
prompt = prompt.replace(/<[^>]*?name="([^\"]*)"[^>]*>/g, (_, name) => `${name}`);
|
|
276
|
+
if (names.length) {
|
|
277
|
+
const pattern = [...new Set(names.map((name) => name.replace(/[\s,,.。!!??~~::;;、]+$/, "")))].filter(Boolean).sort((a, b) => b.length - a.length).map(escapeRegExp).join("|");
|
|
278
|
+
if (pattern) {
|
|
279
|
+
const stripped = prompt.replace(
|
|
280
|
+
new RegExp(`^\\s*(?:${pattern})(?:\\s+|[,,。.!!??~~::;;、]+|$)`),
|
|
281
|
+
""
|
|
282
|
+
);
|
|
283
|
+
if (stripped.trim())
|
|
284
|
+
prompt = stripped;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
263
287
|
prompt = prompt.replace(/\*\*/g, "");
|
|
264
|
-
if (!prompt)
|
|
288
|
+
if (!prompt.trim())
|
|
265
289
|
return "**";
|
|
266
290
|
return prompt.trim();
|
|
267
291
|
}
|
|
@@ -1205,13 +1229,10 @@ var TOPIC_MAX_TURNS = 20;
|
|
|
1205
1229
|
var TOPIC_MAX_AGE_HOURS = 24;
|
|
1206
1230
|
var MAX_ARCHIVE_DIALOGUES = 200;
|
|
1207
1231
|
var MAX_CHILDREN_PER_SUMMARY = 100;
|
|
1208
|
-
var
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
season: 90,
|
|
1213
|
-
year: 365
|
|
1214
|
-
};
|
|
1232
|
+
var MAX_LEVEL_NODES_PER_TICK = 10;
|
|
1233
|
+
var DEFAULT_MAX_CALLS_PER_TICK = 8;
|
|
1234
|
+
var HOUR_MS = 36e5;
|
|
1235
|
+
var DAY_MS = 864e5;
|
|
1215
1236
|
var LEVEL_ORDER = ["day", "week", "month", "season", "year"];
|
|
1216
1237
|
var CHILD_OF = {
|
|
1217
1238
|
day: "topic",
|
|
@@ -1220,12 +1241,62 @@ var CHILD_OF = {
|
|
|
1220
1241
|
season: "month",
|
|
1221
1242
|
year: "season"
|
|
1222
1243
|
};
|
|
1244
|
+
var DIALOGUE_KINDS = ["turn", "legacy"];
|
|
1223
1245
|
function formatTime(date, timezoneOffsetHours) {
|
|
1224
1246
|
const shifted = new Date(new Date(date).getTime() + timezoneOffsetHours * 36e5);
|
|
1225
1247
|
const pad = /* @__PURE__ */ __name((value) => String(value).padStart(2, "0"), "pad");
|
|
1226
1248
|
return `${shifted.getUTCFullYear()}/${pad(shifted.getUTCMonth() + 1)}/${pad(shifted.getUTCDate())} ${pad(shifted.getUTCHours())}:${pad(shifted.getUTCMinutes())}`;
|
|
1227
1249
|
}
|
|
1228
1250
|
__name(formatTime, "formatTime");
|
|
1251
|
+
function dayStart(date, timezoneOffset, boundaryHour) {
|
|
1252
|
+
return lastBoundary(date, timezoneOffset, boundaryHour);
|
|
1253
|
+
}
|
|
1254
|
+
__name(dayStart, "dayStart");
|
|
1255
|
+
function nextDayStart(date, timezoneOffset, boundaryHour) {
|
|
1256
|
+
return new Date(lastBoundary(date, timezoneOffset, boundaryHour).getTime() + DAY_MS);
|
|
1257
|
+
}
|
|
1258
|
+
__name(nextDayStart, "nextDayStart");
|
|
1259
|
+
function periodStart(level, date, timezoneOffset, boundaryHour) {
|
|
1260
|
+
const start = lastBoundary(date, timezoneOffset, boundaryHour);
|
|
1261
|
+
if (level === "day")
|
|
1262
|
+
return start;
|
|
1263
|
+
const offsetMs = timezoneOffset * HOUR_MS;
|
|
1264
|
+
const local = new Date(start.getTime() + offsetMs);
|
|
1265
|
+
const year = local.getUTCFullYear();
|
|
1266
|
+
const month = local.getUTCMonth();
|
|
1267
|
+
if (level === "week") {
|
|
1268
|
+
const back = (local.getUTCDay() + 6) % 7;
|
|
1269
|
+
return new Date(start.getTime() - back * DAY_MS);
|
|
1270
|
+
}
|
|
1271
|
+
if (level === "month") {
|
|
1272
|
+
return new Date(Date.UTC(year, month, 1, boundaryHour) - offsetMs);
|
|
1273
|
+
}
|
|
1274
|
+
if (level === "season") {
|
|
1275
|
+
return new Date(Date.UTC(year, Math.floor(month / 3) * 3, 1, boundaryHour) - offsetMs);
|
|
1276
|
+
}
|
|
1277
|
+
return new Date(Date.UTC(year, 0, 1, boundaryHour) - offsetMs);
|
|
1278
|
+
}
|
|
1279
|
+
__name(periodStart, "periodStart");
|
|
1280
|
+
function periodEnd(level, start, timezoneOffset, boundaryHour) {
|
|
1281
|
+
if (level === "day")
|
|
1282
|
+
return new Date(start.getTime() + DAY_MS);
|
|
1283
|
+
if (level === "week")
|
|
1284
|
+
return new Date(start.getTime() + 7 * DAY_MS);
|
|
1285
|
+
const offsetMs = timezoneOffset * HOUR_MS;
|
|
1286
|
+
const local = new Date(start.getTime() + offsetMs);
|
|
1287
|
+
let year = local.getUTCFullYear();
|
|
1288
|
+
let month = local.getUTCMonth();
|
|
1289
|
+
if (level === "month")
|
|
1290
|
+
month += 1;
|
|
1291
|
+
else if (level === "season")
|
|
1292
|
+
month += 3;
|
|
1293
|
+
else
|
|
1294
|
+
year += 1;
|
|
1295
|
+
year += Math.floor(month / 12);
|
|
1296
|
+
month = (month % 12 + 12) % 12;
|
|
1297
|
+
return new Date(Date.UTC(year, month, 1, boundaryHour) - offsetMs);
|
|
1298
|
+
}
|
|
1299
|
+
__name(periodEnd, "periodEnd");
|
|
1229
1300
|
var Summarizer = class {
|
|
1230
1301
|
constructor(ctx, repo, api, profile, config) {
|
|
1231
1302
|
this.ctx = ctx;
|
|
@@ -1239,6 +1310,12 @@ var Summarizer = class {
|
|
|
1239
1310
|
}
|
|
1240
1311
|
/** 同一 scope 同一时刻只跑一个汇总任务,避免心跳与话题切换并发写入 */
|
|
1241
1312
|
busy = /* @__PURE__ */ new Set();
|
|
1313
|
+
get tz() {
|
|
1314
|
+
return this.config.timezoneOffset;
|
|
1315
|
+
}
|
|
1316
|
+
get boundaryHour() {
|
|
1317
|
+
return this.config.dayBoundaryHour;
|
|
1318
|
+
}
|
|
1242
1319
|
// ---------- 话题 ----------
|
|
1243
1320
|
/**
|
|
1244
1321
|
* 每轮对话后调用:必要时归档当前话题。
|
|
@@ -1251,7 +1328,7 @@ var Summarizer = class {
|
|
|
1251
1328
|
const user = await this.lookupUser(scopeId);
|
|
1252
1329
|
const meta = await this.repo.ensureMeta(scopeId);
|
|
1253
1330
|
const dialogues = await this.repo.getDialoguesAfter(scopeId, meta.topicStart, {
|
|
1254
|
-
kinds:
|
|
1331
|
+
kinds: DIALOGUE_KINDS,
|
|
1255
1332
|
// 只需要判断是否达到阈值,不把迁移后的全部历史读进内存
|
|
1256
1333
|
limit: TOPIC_MAX_TURNS
|
|
1257
1334
|
});
|
|
@@ -1261,7 +1338,7 @@ var Summarizer = class {
|
|
|
1261
1338
|
if (!shouldSwitch) {
|
|
1262
1339
|
const text = this.formatDialogues(dialogues);
|
|
1263
1340
|
const result = await this.api.auxiliaryChat(
|
|
1264
|
-
buildTopicSwitchPrompt(text, formatTime(meta.topicStart, this.
|
|
1341
|
+
buildTopicSwitchPrompt(text, formatTime(meta.topicStart, this.tz)),
|
|
1265
1342
|
user
|
|
1266
1343
|
);
|
|
1267
1344
|
if (result.error)
|
|
@@ -1273,123 +1350,255 @@ var Summarizer = class {
|
|
|
1273
1350
|
await this.archiveTopicLocked(scopeId, user);
|
|
1274
1351
|
});
|
|
1275
1352
|
}
|
|
1276
|
-
/**
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
return;
|
|
1282
|
-
const dialogues = await this.repo.getDialoguesAfter(scopeId, meta.topicStart, {
|
|
1283
|
-
kinds: ["turn", "legacy"],
|
|
1284
|
-
limit: 1
|
|
1285
|
-
});
|
|
1286
|
-
if (!dialogues.length)
|
|
1287
|
-
return;
|
|
1288
|
-
await this.archiveTopicLocked(scopeId, user);
|
|
1289
|
-
}
|
|
1290
|
-
/** 把 [topicStart, 末尾] 的对话归档为一条 topic 记忆。调用方需持有 busy 锁。 */
|
|
1353
|
+
/**
|
|
1354
|
+
* 把游标所在自然日内、尚未归档的对话归档为一条 topic 记忆。
|
|
1355
|
+
* 超过 MAX_ARCHIVE_DIALOGUES 时只取前 200 条(同一天拆成多块)。
|
|
1356
|
+
* 返回是否真的发起并完成了归档。
|
|
1357
|
+
*/
|
|
1291
1358
|
async archiveTopicLocked(scopeId, user) {
|
|
1292
1359
|
const meta = await this.repo.ensureMeta(scopeId);
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1295
|
-
|
|
1360
|
+
const cursor = new Date(meta.topicStart);
|
|
1361
|
+
const dayEnd = nextDayStart(cursor, this.tz, this.boundaryHour);
|
|
1362
|
+
const dialogues = await this.repo.getDialoguesAfter(scopeId, cursor, {
|
|
1363
|
+
kinds: DIALOGUE_KINDS,
|
|
1364
|
+
limit: MAX_ARCHIVE_DIALOGUES,
|
|
1365
|
+
before: dayEnd
|
|
1296
1366
|
});
|
|
1297
1367
|
if (!dialogues.length)
|
|
1298
|
-
return;
|
|
1368
|
+
return false;
|
|
1299
1369
|
const text = this.formatDialogues(dialogues);
|
|
1300
1370
|
if (this.config.dryRun) {
|
|
1301
|
-
logger4.info(`[dry-run] topic 归档 scope=${scopeId} 对话=${dialogues.length} 字符=${text.length}`);
|
|
1302
|
-
return;
|
|
1371
|
+
logger4.info(`[dry-run] topic 归档 scope=${scopeId} 对话=${dialogues.length} 字符=${text.length} 日=${formatTime(cursor, this.tz)}`);
|
|
1372
|
+
return false;
|
|
1303
1373
|
}
|
|
1304
1374
|
const result = await this.api.auxiliaryChat(buildTopicArchivePrompt(text), user);
|
|
1305
1375
|
if (result.error) {
|
|
1306
1376
|
logger4.warn(`topic 归档失败 scope=${scopeId}: ${result.content}`);
|
|
1307
|
-
return;
|
|
1377
|
+
return false;
|
|
1308
1378
|
}
|
|
1309
1379
|
const parsed = parseLlmJson(result.content);
|
|
1310
1380
|
const summary = parsed?.summary?.trim();
|
|
1311
1381
|
if (!summary) {
|
|
1312
1382
|
logger4.warn(`topic 归档解析失败 scope=${scopeId}: ${result.content?.slice(0, 200)}`);
|
|
1313
|
-
return;
|
|
1383
|
+
return false;
|
|
1314
1384
|
}
|
|
1315
|
-
const
|
|
1385
|
+
const periodStart2 = dialogues[0].createdAt;
|
|
1316
1386
|
const last = dialogues[dialogues.length - 1];
|
|
1387
|
+
let nextCursor = new Date(last.createdAt);
|
|
1388
|
+
if (dayEnd.getTime() <= Date.now()) {
|
|
1389
|
+
const remaining2 = await this.repo.countDialoguesInRange(
|
|
1390
|
+
scopeId,
|
|
1391
|
+
nextCursor,
|
|
1392
|
+
dayEnd,
|
|
1393
|
+
DIALOGUE_KINDS
|
|
1394
|
+
);
|
|
1395
|
+
if (!remaining2)
|
|
1396
|
+
nextCursor = dayEnd;
|
|
1397
|
+
}
|
|
1317
1398
|
await this.repo.commitTopicArchive({
|
|
1318
1399
|
scopeId,
|
|
1319
1400
|
granularity: "topic",
|
|
1320
1401
|
topic: (parsed.topic || "").trim(),
|
|
1321
1402
|
content: summary,
|
|
1322
1403
|
userState: (parsed.user_state || "").trim(),
|
|
1323
|
-
periodStart,
|
|
1404
|
+
periodStart: periodStart2,
|
|
1324
1405
|
periodEnd: last.createdAt,
|
|
1325
1406
|
sourceCount: dialogues.length,
|
|
1326
1407
|
createdAt: /* @__PURE__ */ new Date()
|
|
1327
|
-
}, scopeId,
|
|
1328
|
-
const remaining = await this.repo.countDialoguesAfter(scopeId,
|
|
1329
|
-
logger4.info(`topic 归档完成 scope=${scopeId} 对话=${dialogues.length}
|
|
1408
|
+
}, scopeId, nextCursor);
|
|
1409
|
+
const remaining = await this.repo.countDialoguesAfter(scopeId, nextCursor);
|
|
1410
|
+
logger4.info(`topic 归档完成 scope=${scopeId} 对话=${dialogues.length} 日=${formatTime(dayStart(cursor, this.tz, this.boundaryHour), this.tz)}` + (remaining ? ` 剩余${remaining}条待处理` : ""));
|
|
1411
|
+
return true;
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* 按自然日消费积压:每次归档一个日块,直到预算用完。
|
|
1415
|
+
* - 全是空白的日期用一次查询跨过去,不逐天循环;
|
|
1416
|
+
* - 已关闭的日子必须归档干净(哪怕只剩 1 条),否则日级永远等不到闭合;
|
|
1417
|
+
* - 进行中的当天沿用旧语义:话题超过 24 小时才兜底归档。
|
|
1418
|
+
*/
|
|
1419
|
+
async drainTopics(scopeId, user, budget) {
|
|
1420
|
+
if (this.config.dryRun) {
|
|
1421
|
+
const meta = await this.repo.ensureMeta(scopeId);
|
|
1422
|
+
const [next] = await this.repo.getDialoguesAfter(scopeId, meta.topicStart, {
|
|
1423
|
+
kinds: DIALOGUE_KINDS,
|
|
1424
|
+
limit: 1
|
|
1425
|
+
});
|
|
1426
|
+
if (next)
|
|
1427
|
+
logger4.info(`[dry-run] 将归档 scope=${scopeId} 起点=${formatTime(next.createdAt, this.tz)}`);
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
while (budget.calls > 0) {
|
|
1431
|
+
const meta = await this.repo.ensureMeta(scopeId);
|
|
1432
|
+
const cursor = new Date(meta.topicStart);
|
|
1433
|
+
const dayEnd = nextDayStart(cursor, this.tz, this.boundaryHour);
|
|
1434
|
+
const [next] = await this.repo.getDialoguesAfter(scopeId, cursor, {
|
|
1435
|
+
kinds: DIALOGUE_KINDS,
|
|
1436
|
+
limit: 1
|
|
1437
|
+
});
|
|
1438
|
+
if (!next) {
|
|
1439
|
+
const todayStart = dayStart(/* @__PURE__ */ new Date(), this.tz, this.boundaryHour);
|
|
1440
|
+
if (todayStart.getTime() > cursor.getTime()) {
|
|
1441
|
+
await this.repo.setTopicStart(scopeId, todayStart);
|
|
1442
|
+
}
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
const nextDay = dayStart(new Date(next.createdAt), this.tz, this.boundaryHour);
|
|
1446
|
+
if (nextDay.getTime() > cursor.getTime()) {
|
|
1447
|
+
await this.repo.setTopicStart(scopeId, nextDay);
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
if (dayEnd.getTime() > Date.now()) {
|
|
1451
|
+
const ageHours = (Date.now() - cursor.getTime()) / HOUR_MS;
|
|
1452
|
+
if (ageHours < TOPIC_MAX_AGE_HOURS)
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
const archived = await this.archiveTopicLocked(scopeId, user);
|
|
1456
|
+
if (!archived)
|
|
1457
|
+
return;
|
|
1458
|
+
budget.calls -= 1;
|
|
1459
|
+
}
|
|
1330
1460
|
}
|
|
1331
1461
|
// ---------- 分层汇总 ----------
|
|
1332
1462
|
async summarizeScope(scopeId) {
|
|
1333
1463
|
return this.runExclusive(scopeId, async () => {
|
|
1334
1464
|
const user = await this.lookupUser(scopeId);
|
|
1335
1465
|
await this.repo.touchMeta(scopeId);
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1466
|
+
const total = Math.max(2, this.config.maxCallsPerTick ?? DEFAULT_MAX_CALLS_PER_TICK);
|
|
1467
|
+
const archiveQuota = Math.max(1, Math.floor(total / 2));
|
|
1468
|
+
const archiveBudget = { calls: archiveQuota };
|
|
1469
|
+
await this.drainTopics(scopeId, user, archiveBudget);
|
|
1470
|
+
const levelBudget = {
|
|
1471
|
+
calls: Math.max(1, total - (archiveQuota - archiveBudget.calls))
|
|
1472
|
+
};
|
|
1473
|
+
await this.buildLevels(scopeId, user, levelBudget);
|
|
1340
1474
|
});
|
|
1341
1475
|
}
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1476
|
+
/**
|
|
1477
|
+
* 自下而上补齐已闭合的周期,每个周期最多一条节点。
|
|
1478
|
+
*
|
|
1479
|
+
* 起算点来自该层最新节点的 periodEnd(没有则取最早子节点所在周期),
|
|
1480
|
+
* 因此不会重复生成同一周期;空周期没有子节点,会被自然跳过。
|
|
1481
|
+
* 周期末尾晚于归档游标时说明周期内的对话还没归档完,立即停止该层。
|
|
1482
|
+
*/
|
|
1483
|
+
async buildLevels(scopeId, user, budget) {
|
|
1484
|
+
const meta = await this.repo.ensureMeta(scopeId);
|
|
1485
|
+
const frontier = new Date(meta.topicStart);
|
|
1486
|
+
for (const level of LEVEL_ORDER) {
|
|
1487
|
+
let nodes = 0;
|
|
1488
|
+
let reached = false;
|
|
1489
|
+
while (nodes < MAX_LEVEL_NODES_PER_TICK) {
|
|
1490
|
+
const parent = await this.repo.getLatestMemory(scopeId, level);
|
|
1491
|
+
const from = parent ? new Date(parent.periodEnd) : null;
|
|
1492
|
+
const [child] = await this.repo.getMemoriesInRange(
|
|
1493
|
+
scopeId,
|
|
1494
|
+
CHILD_OF[level],
|
|
1495
|
+
from,
|
|
1496
|
+
frontier,
|
|
1497
|
+
1
|
|
1498
|
+
);
|
|
1499
|
+
if (!child) {
|
|
1500
|
+
reached = true;
|
|
1501
|
+
break;
|
|
1502
|
+
}
|
|
1503
|
+
const start = periodStart(level, new Date(new Date(child.periodEnd).getTime() - 1), this.tz, this.boundaryHour);
|
|
1504
|
+
const end = periodEnd(level, start, this.tz, this.boundaryHour);
|
|
1505
|
+
if (end.getTime() > frontier.getTime()) {
|
|
1506
|
+
reached = true;
|
|
1507
|
+
break;
|
|
1508
|
+
}
|
|
1509
|
+
const children = await this.repo.getMemoriesInRange(
|
|
1510
|
+
scopeId,
|
|
1511
|
+
CHILD_OF[level],
|
|
1512
|
+
start,
|
|
1513
|
+
end
|
|
1514
|
+
);
|
|
1515
|
+
if (!children.length)
|
|
1516
|
+
break;
|
|
1517
|
+
if (level === "day") {
|
|
1518
|
+
const stray = children.find((row) => dayStart(new Date(row.periodStart), this.tz, this.boundaryHour).getTime() !== dayStart(new Date(new Date(row.periodEnd).getTime() - 1), this.tz, this.boundaryHour).getTime());
|
|
1519
|
+
if (stray) {
|
|
1520
|
+
logger4.warn(`day 汇总暂停:存在跨日 topic 记忆 scope=${scopeId} 记忆区间=${formatTime(stray.periodStart, this.tz)} ~ ${formatTime(stray.periodEnd, this.tz)}`);
|
|
1521
|
+
break;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
const range = `${formatTime(start, this.tz)} ~ ${formatTime(end, this.tz)}`;
|
|
1525
|
+
const single = children.length === 1;
|
|
1526
|
+
if (single) {
|
|
1527
|
+
if (this.config.dryRun) {
|
|
1528
|
+
logger4.info(`[dry-run] ${level} 晋升 scope=${scopeId} 周期=${range} 子记忆=1`);
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
if (level === "day" && this.profile.isEnabled()) {
|
|
1532
|
+
if (budget.calls <= 0)
|
|
1533
|
+
return;
|
|
1534
|
+
budget.calls -= 1;
|
|
1535
|
+
await this.profile.summarize(scopeId, children[0].content, children, user);
|
|
1536
|
+
}
|
|
1537
|
+
await this.repo.createMemory({
|
|
1538
|
+
scopeId,
|
|
1539
|
+
granularity: level,
|
|
1540
|
+
topic: "",
|
|
1541
|
+
content: children[0].content,
|
|
1542
|
+
userState: children[0].userState || "",
|
|
1543
|
+
periodStart: start,
|
|
1544
|
+
periodEnd: end,
|
|
1545
|
+
sourceCount: 1,
|
|
1546
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
1547
|
+
});
|
|
1548
|
+
logger4.info(`${level} 晋升 scope=${scopeId} 周期=${range}`);
|
|
1549
|
+
nodes += 1;
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
const need = 1 + (level === "day" && this.profile.isEnabled() ? 1 : 0);
|
|
1553
|
+
if (budget.calls < need)
|
|
1554
|
+
return;
|
|
1555
|
+
const input = children.slice(0, MAX_CHILDREN_PER_SUMMARY);
|
|
1556
|
+
if (children.length > MAX_CHILDREN_PER_SUMMARY) {
|
|
1557
|
+
logger4.warn(`${level} 子记忆过多,只汇总前 ${MAX_CHILDREN_PER_SUMMARY} 条 scope=${scopeId} 实际=${children.length}`);
|
|
1558
|
+
}
|
|
1559
|
+
const text = this.formatChildren(input);
|
|
1560
|
+
if (this.config.dryRun) {
|
|
1561
|
+
logger4.info(`[dry-run] ${level} 汇总 scope=${scopeId} 周期=${range} 子记忆=${children.length}`);
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
const result = await this.api.auxiliaryChat(buildSummaryPrompt(level, text), user);
|
|
1565
|
+
budget.calls -= 1;
|
|
1566
|
+
if (result.error) {
|
|
1567
|
+
logger4.warn(`${level} 汇总失败 scope=${scopeId}: ${result.content}`);
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
const parsed = parseLlmJson(result.content);
|
|
1571
|
+
const summary = parsed?.summary?.trim();
|
|
1572
|
+
if (!summary) {
|
|
1573
|
+
logger4.warn(`${level} 汇总解析失败 scope=${scopeId}: ${result.content?.slice(0, 200)}`);
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
await this.repo.createMemory({
|
|
1577
|
+
scopeId,
|
|
1578
|
+
granularity: level,
|
|
1579
|
+
topic: "",
|
|
1580
|
+
content: summary,
|
|
1581
|
+
userState: (parsed.user_state || "").trim(),
|
|
1582
|
+
periodStart: start,
|
|
1583
|
+
periodEnd: end,
|
|
1584
|
+
sourceCount: children.length,
|
|
1585
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
1586
|
+
});
|
|
1587
|
+
logger4.info(`${level} 汇总完成 scope=${scopeId} 周期=${range} 子记忆=${children.length}`);
|
|
1588
|
+
nodes += 1;
|
|
1589
|
+
if (level === "day" && this.profile.isEnabled()) {
|
|
1590
|
+
budget.calls -= 1;
|
|
1591
|
+
await this.profile.summarize(scopeId, summary, children, user);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
if (!reached)
|
|
1595
|
+
return;
|
|
1387
1596
|
}
|
|
1388
1597
|
}
|
|
1389
1598
|
// ---------- 工具 ----------
|
|
1390
1599
|
formatDialogues(rows) {
|
|
1391
1600
|
return rows.map((row) => {
|
|
1392
|
-
const time = formatTime(row.createdAt, this.
|
|
1601
|
+
const time = formatTime(row.createdAt, this.tz);
|
|
1393
1602
|
const user = row.username || "用户";
|
|
1394
1603
|
const ai = row.aiContent && row.aiContent !== "…" ? `
|
|
1395
1604
|
AI:${row.aiContent}` : "";
|
|
@@ -1398,7 +1607,7 @@ AI:${row.aiContent}` : "";
|
|
|
1398
1607
|
}
|
|
1399
1608
|
formatChildren(children) {
|
|
1400
1609
|
return children.map((child) => {
|
|
1401
|
-
const range = `${formatTime(child.periodStart, this.
|
|
1610
|
+
const range = `${formatTime(child.periodStart, this.tz)} ~ ${formatTime(child.periodEnd, this.tz)}`;
|
|
1402
1611
|
const topic = child.topic ? `(${child.topic})` : "";
|
|
1403
1612
|
const state = child.userState ? `
|
|
1404
1613
|
用户状态:${child.userState}` : "";
|
|
@@ -2024,6 +2233,9 @@ var ProfileManager = class {
|
|
|
2024
2233
|
static {
|
|
2025
2234
|
__name(this, "ProfileManager");
|
|
2026
2235
|
}
|
|
2236
|
+
isEnabled() {
|
|
2237
|
+
return this.config.enabled;
|
|
2238
|
+
}
|
|
2027
2239
|
/** 日总结完成后调用;持票用户走自己的 key(与 chat 一致) */
|
|
2028
2240
|
async summarize(scopeId, summary, memories, user = null) {
|
|
2029
2241
|
if (!this.config.enabled)
|
|
@@ -2134,7 +2346,10 @@ var MemoryRepository = class {
|
|
|
2134
2346
|
});
|
|
2135
2347
|
}
|
|
2136
2348
|
async getDialoguesAfter(scopeId, after, options) {
|
|
2137
|
-
const
|
|
2349
|
+
const createdAt = { $gt: after };
|
|
2350
|
+
if (options?.before)
|
|
2351
|
+
createdAt.$lt = options.before;
|
|
2352
|
+
const query = { scopeId, createdAt };
|
|
2138
2353
|
if (options?.kinds?.length)
|
|
2139
2354
|
query.kind = { $in: options.kinds };
|
|
2140
2355
|
return this.ctx.database.get("sat_dialogue", query, {
|
|
@@ -2149,6 +2364,14 @@ var MemoryRepository = class {
|
|
|
2149
2364
|
const rows = await this.ctx.database.get("sat_dialogue", query, { fields: ["id"] });
|
|
2150
2365
|
return rows.length;
|
|
2151
2366
|
}
|
|
2367
|
+
/** after < createdAt < before 的对话数,用于判断一个已关闭的日块是否归档干净 */
|
|
2368
|
+
async countDialoguesInRange(scopeId, after, before, kinds) {
|
|
2369
|
+
const query = { scopeId, createdAt: { $gt: after, $lt: before } };
|
|
2370
|
+
if (kinds?.length)
|
|
2371
|
+
query.kind = { $in: kinds };
|
|
2372
|
+
const rows = await this.ctx.database.get("sat_dialogue", query, { fields: ["id"] });
|
|
2373
|
+
return rows.length;
|
|
2374
|
+
}
|
|
2152
2375
|
async countDialogues(scopeId) {
|
|
2153
2376
|
const rows = await this.ctx.database.get("sat_dialogue", { scopeId }, { fields: ["id"] });
|
|
2154
2377
|
return rows.length;
|
|
@@ -2250,12 +2473,13 @@ var MemoryRepository = class {
|
|
|
2250
2473
|
return rows[0]?.periodStart || null;
|
|
2251
2474
|
}
|
|
2252
2475
|
/** 区间查询:from < periodEnd <= to,走真正的 SQL 条件而不是 JS 侧过滤 */
|
|
2253
|
-
async getMemoriesInRange(scopeId, granularity, from, to) {
|
|
2254
|
-
const
|
|
2476
|
+
async getMemoriesInRange(scopeId, granularity, from, to, limit) {
|
|
2477
|
+
const periodEnd2 = { $lte: to };
|
|
2255
2478
|
if (from)
|
|
2256
|
-
|
|
2257
|
-
return this.ctx.database.get("sat_memory", { scopeId, granularity, periodEnd }, {
|
|
2258
|
-
sort: { periodEnd: "asc" }
|
|
2479
|
+
periodEnd2.$gt = from;
|
|
2480
|
+
return this.ctx.database.get("sat_memory", { scopeId, granularity, periodEnd: periodEnd2 }, {
|
|
2481
|
+
sort: { periodEnd: "asc" },
|
|
2482
|
+
limit
|
|
2259
2483
|
});
|
|
2260
2484
|
}
|
|
2261
2485
|
async createMemory(row) {
|
|
@@ -2468,6 +2692,7 @@ var MemoryService = class {
|
|
|
2468
2692
|
topicSwitchCheck: config.topic_switch_check,
|
|
2469
2693
|
timezoneOffset: config.memory_timezone_offset,
|
|
2470
2694
|
dayBoundaryHour: config.memory_day_boundary_hour,
|
|
2695
|
+
maxCallsPerTick: config.summary_max_calls_per_tick,
|
|
2471
2696
|
resolveUser
|
|
2472
2697
|
});
|
|
2473
2698
|
this.contextBuilder = new ContextBuilder(this.repo, this.shortTerm, this.profile, {
|
|
@@ -3013,6 +3238,7 @@ var Sat = class extends import_koishi10.Service {
|
|
|
3013
3238
|
topic_switch_check: import_koishi10.Schema.boolean().default(true).description("是否用辅助模型判断话题切换"),
|
|
3014
3239
|
summary_heartbeat_minutes: import_koishi10.Schema.number().default(10).description("后台汇总心跳间隔(分钟)"),
|
|
3015
3240
|
summary_max_scopes_per_tick: import_koishi10.Schema.number().default(3).description("每次心跳最多处理的用户/群数量"),
|
|
3241
|
+
summary_max_calls_per_tick: import_koishi10.Schema.number().default(8).description("每个用户/群每次心跳最多发起的辅助模型调用数(归档与分层汇总各占一半预算;历史积压时调大可加快补齐)"),
|
|
3016
3242
|
memory_active_window_days: import_koishi10.Schema.number().default(7).description("只对最近多少天内活跃过的用户/群做汇总"),
|
|
3017
3243
|
memory_retention_days: import_koishi10.Schema.number().default(180).description("原始对话保留天数"),
|
|
3018
3244
|
memory_recall_topN: import_koishi10.Schema.number().default(5).description("关键词召回的历史记忆条数"),
|
|
@@ -5382,6 +5608,7 @@ var SAT = class extends Sat {
|
|
|
5382
5608
|
summary_heartbeat_minutes: this.config.summary_heartbeat_minutes,
|
|
5383
5609
|
summary_dry_run: this.config.summary_dry_run,
|
|
5384
5610
|
summary_max_scopes_per_tick: this.config.summary_max_scopes_per_tick,
|
|
5611
|
+
summary_max_calls_per_tick: this.config.summary_max_calls_per_tick,
|
|
5385
5612
|
memory_active_window_days: this.config.memory_active_window_days,
|
|
5386
5613
|
memory_retention_days: this.config.memory_retention_days,
|
|
5387
5614
|
memory_context_max_chars: this.config.memory_context_max_chars,
|
|
@@ -5627,7 +5854,7 @@ var SAT = class extends Sat {
|
|
|
5627
5854
|
}
|
|
5628
5855
|
// 处理输入
|
|
5629
5856
|
async processInput(session, prompt) {
|
|
5630
|
-
const processedPrompt = processPrompt(prompt);
|
|
5857
|
+
const processedPrompt = processPrompt(prompt, this.config.nick_name_list, [session.selfId]);
|
|
5631
5858
|
let censored = processedPrompt;
|
|
5632
5859
|
if (this.ctx.censor) {
|
|
5633
5860
|
censored = await this.ctx.censor.transform(processedPrompt, session);
|
|
@@ -6010,7 +6237,7 @@ ${useNoReasoner ? promptForNoReasoner : promptForReasoner}
|
|
|
6010
6237
|
}
|
|
6011
6238
|
// 频道记忆中间件转接
|
|
6012
6239
|
async handleChannelMemoryManager(session) {
|
|
6013
|
-
const processedPrompt = processPrompt(session.content);
|
|
6240
|
+
const processedPrompt = processPrompt(session.content, this.config.nick_name_list, [session.selfId]);
|
|
6014
6241
|
if (this.performPreChecks(session, processedPrompt))
|
|
6015
6242
|
return null;
|
|
6016
6243
|
let censored = processedPrompt;
|
package/lib/memory/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface MemoryServiceConfig extends MemoryConfig {
|
|
|
10
10
|
summary_heartbeat_minutes: number;
|
|
11
11
|
summary_dry_run: boolean;
|
|
12
12
|
summary_max_scopes_per_tick: number;
|
|
13
|
+
summary_max_calls_per_tick: number;
|
|
13
14
|
memory_active_window_days: number;
|
|
14
15
|
memory_retention_days: number;
|
|
15
16
|
memory_context_max_chars: number;
|
package/lib/memory/profile.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare class ProfileManager {
|
|
|
15
15
|
private api;
|
|
16
16
|
private config;
|
|
17
17
|
constructor(ctx: Context, repo: MemoryRepository, api: APIClient, config: ProfileConfig);
|
|
18
|
+
isEnabled(): boolean;
|
|
18
19
|
/** 日总结完成后调用;持票用户走自己的 key(与 chat 一致) */
|
|
19
20
|
summarize(scopeId: string, summary: string, memories: SatMemory[], user?: User | null): Promise<void>;
|
|
20
21
|
getContext(scopeId: string): Promise<string>;
|
|
@@ -12,8 +12,11 @@ export declare class MemoryRepository {
|
|
|
12
12
|
getDialoguesAfter(scopeId: string, after: Date, options?: {
|
|
13
13
|
kinds?: string[];
|
|
14
14
|
limit?: number;
|
|
15
|
+
before?: Date;
|
|
15
16
|
}): Promise<SatDialogue[]>;
|
|
16
17
|
countDialoguesAfter(scopeId: string, after: Date, kinds?: string[]): Promise<number>;
|
|
18
|
+
/** after < createdAt < before 的对话数,用于判断一个已关闭的日块是否归档干净 */
|
|
19
|
+
countDialoguesInRange(scopeId: string, after: Date, before: Date, kinds?: string[]): Promise<number>;
|
|
17
20
|
countDialogues(scopeId: string): Promise<number>;
|
|
18
21
|
/** 只清理普通对话,legacy 迁移数据保留(一次性数据,量有界) */
|
|
19
22
|
pruneDialogues(before: Date): Promise<number>;
|
|
@@ -44,7 +47,7 @@ export declare class MemoryRepository {
|
|
|
44
47
|
/** 子粒度的最早起点(服务端 ORDER BY periodStart ASC LIMIT 1,等价于 MIN(periodStart)) */
|
|
45
48
|
getEarliestChildStart(scopeId: string, granularity: Granularity): Promise<Date | null>;
|
|
46
49
|
/** 区间查询:from < periodEnd <= to,走真正的 SQL 条件而不是 JS 侧过滤 */
|
|
47
|
-
getMemoriesInRange(scopeId: string, granularity: Granularity, from: Date | null, to: Date): Promise<SatMemory[]>;
|
|
50
|
+
getMemoriesInRange(scopeId: string, granularity: Granularity, from: Date | null, to: Date, limit?: number): Promise<SatMemory[]>;
|
|
48
51
|
createMemory(row: Omit<SatMemory, 'id'>): Promise<void>;
|
|
49
52
|
/** 该 scope 是否已经产过分层记忆(用于判断迁移是否第一次跑) */
|
|
50
53
|
hasAnyMemory(scopeId: string): Promise<boolean>;
|
|
@@ -8,15 +8,31 @@ export declare const TOPIC_MAX_TURNS = 20;
|
|
|
8
8
|
export declare const TOPIC_MAX_AGE_HOURS = 24;
|
|
9
9
|
export declare const MAX_ARCHIVE_DIALOGUES = 200;
|
|
10
10
|
export declare const MAX_CHILDREN_PER_SUMMARY = 100;
|
|
11
|
+
/** 单个 scope 一次心跳里最多建立多少个层级节点,防止历史积压把 tick 拖死 */
|
|
12
|
+
export declare const MAX_LEVEL_NODES_PER_TICK = 10;
|
|
13
|
+
/** 每次心跳默认的辅助模型调用上限(归档 + 多子节点汇总 + 日画像) */
|
|
14
|
+
export declare const DEFAULT_MAX_CALLS_PER_TICK = 8;
|
|
15
|
+
declare const LEVEL_ORDER: readonly ["day", "week", "month", "season", "year"];
|
|
16
|
+
type Level = typeof LEVEL_ORDER[number];
|
|
11
17
|
export interface SummarizerConfig {
|
|
12
18
|
dryRun: boolean;
|
|
13
19
|
topicSwitchCheck: boolean;
|
|
14
20
|
timezoneOffset: number;
|
|
15
21
|
dayBoundaryHour: number;
|
|
22
|
+
/** 每个 scope 每次汇总最多发起多少次辅助模型调用 */
|
|
23
|
+
maxCallsPerTick?: number;
|
|
16
24
|
/** scopeId -> 用户;用于让持“地灵殿通行证”的用户走自己的 key */
|
|
17
25
|
resolveUser?: (scopeId: string) => Promise<User | null>;
|
|
18
26
|
}
|
|
19
27
|
export declare function formatTime(date: Date, timezoneOffsetHours: number): string;
|
|
28
|
+
/** date 所在自然日的起点:最近一个已过的日界线 */
|
|
29
|
+
export declare function dayStart(date: Date, timezoneOffset: number, boundaryHour: number): Date;
|
|
30
|
+
/** date 所在自然日的终点(下一条日界线) */
|
|
31
|
+
export declare function nextDayStart(date: Date, timezoneOffset: number, boundaryHour: number): Date;
|
|
32
|
+
/** date 所属的 level 周期起点:日 / 周一 / 月 / 季 / 年,均落在日界线上 */
|
|
33
|
+
export declare function periodStart(level: Level, date: Date, timezoneOffset: number, boundaryHour: number): Date;
|
|
34
|
+
/** level 周期的下一个起点(= 当前周期的终点) */
|
|
35
|
+
export declare function periodEnd(level: Level, start: Date, timezoneOffset: number, boundaryHour: number): Date;
|
|
20
36
|
export declare class Summarizer {
|
|
21
37
|
private ctx;
|
|
22
38
|
private repo;
|
|
@@ -26,20 +42,39 @@ export declare class Summarizer {
|
|
|
26
42
|
/** 同一 scope 同一时刻只跑一个汇总任务,避免心跳与话题切换并发写入 */
|
|
27
43
|
private busy;
|
|
28
44
|
constructor(ctx: Context, repo: MemoryRepository, api: APIClient, profile: ProfileManager, config: SummarizerConfig);
|
|
45
|
+
private get tz();
|
|
46
|
+
private get boundaryHour();
|
|
29
47
|
/**
|
|
30
48
|
* 每轮对话后调用:必要时归档当前话题。
|
|
31
49
|
* 注意:这里不阻塞回复,由调用方以 fire-and-forget 方式触发。
|
|
32
50
|
*/
|
|
33
51
|
maybeSwitchTopic(scopeId: string): Promise<void>;
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
52
|
+
/**
|
|
53
|
+
* 把游标所在自然日内、尚未归档的对话归档为一条 topic 记忆。
|
|
54
|
+
* 超过 MAX_ARCHIVE_DIALOGUES 时只取前 200 条(同一天拆成多块)。
|
|
55
|
+
* 返回是否真的发起并完成了归档。
|
|
56
|
+
*/
|
|
37
57
|
private archiveTopicLocked;
|
|
58
|
+
/**
|
|
59
|
+
* 按自然日消费积压:每次归档一个日块,直到预算用完。
|
|
60
|
+
* - 全是空白的日期用一次查询跨过去,不逐天循环;
|
|
61
|
+
* - 已关闭的日子必须归档干净(哪怕只剩 1 条),否则日级永远等不到闭合;
|
|
62
|
+
* - 进行中的当天沿用旧语义:话题超过 24 小时才兜底归档。
|
|
63
|
+
*/
|
|
64
|
+
private drainTopics;
|
|
38
65
|
summarizeScope(scopeId: string): Promise<void>;
|
|
39
|
-
|
|
66
|
+
/**
|
|
67
|
+
* 自下而上补齐已闭合的周期,每个周期最多一条节点。
|
|
68
|
+
*
|
|
69
|
+
* 起算点来自该层最新节点的 periodEnd(没有则取最早子节点所在周期),
|
|
70
|
+
* 因此不会重复生成同一周期;空周期没有子节点,会被自然跳过。
|
|
71
|
+
* 周期末尾晚于归档游标时说明周期内的对话还没归档完,立即停止该层。
|
|
72
|
+
*/
|
|
73
|
+
private buildLevels;
|
|
40
74
|
private formatDialogues;
|
|
41
75
|
private formatChildren;
|
|
42
76
|
/** 解析失败不应让后台归档整体失败,退回默认辅助模型即可。 */
|
|
43
77
|
private lookupUser;
|
|
44
78
|
private runExclusive;
|
|
45
79
|
}
|
|
80
|
+
export {};
|
package/lib/types.d.ts
CHANGED
|
@@ -196,6 +196,7 @@ export declare namespace Sat {
|
|
|
196
196
|
summary_heartbeat_minutes: number;
|
|
197
197
|
summary_dry_run: boolean;
|
|
198
198
|
summary_max_scopes_per_tick: number;
|
|
199
|
+
summary_max_calls_per_tick: number;
|
|
199
200
|
memory_active_window_days: number;
|
|
200
201
|
memory_retention_days: number;
|
|
201
202
|
memory_context_max_chars: number;
|
package/lib/utils.d.ts
CHANGED
|
@@ -15,9 +15,12 @@ export declare function isErrorWithMessage(error: unknown): error is {
|
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
17
|
* @param prompt 包含待处理标签的原始字符串
|
|
18
|
+
* @param botNames bot 的称呼列表;用于去掉 @bot / 以昵称开场这类「称呼」,
|
|
19
|
+
* 它们只是唤起方式,不应该进入对话记录与总结
|
|
20
|
+
* @param botIds bot 的账号 id;群名片可能改得和正文很像,优先用 id 判断 @ 的是不是 bot
|
|
18
21
|
* @returns 处理后的字符串,所有匹配标签被替换为对应名称
|
|
19
22
|
*/
|
|
20
|
-
export declare function processPrompt(prompt: string): string;
|
|
23
|
+
export declare function processPrompt(prompt: string, botNames?: string[], botIds?: string[]): string;
|
|
21
24
|
/**
|
|
22
25
|
* @param prompt 包含待处理标签的原始字符串
|
|
23
26
|
* @param words 需要过滤的关键词
|