@zaofan/dsh-qqbot 1.4.5 → 1.4.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 +158 -535
- package/README_EN.md +66 -0
- package/client/qqbot-settings.js +161 -14
- package/dist/channel-tools.d.ts.map +1 -1
- package/dist/channel-tools.js +106 -1
- package/dist/channel-tools.js.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/features/agg-score.d.ts +73 -0
- package/dist/features/agg-score.d.ts.map +1 -0
- package/dist/features/agg-score.js +63 -0
- package/dist/features/agg-score.js.map +1 -0
- package/dist/features/attitude.d.ts +132 -0
- package/dist/features/attitude.d.ts.map +1 -0
- package/dist/features/attitude.js +318 -0
- package/dist/features/attitude.js.map +1 -0
- package/dist/features/bot-reply-memo.d.ts +8 -0
- package/dist/features/bot-reply-memo.d.ts.map +1 -0
- package/dist/features/bot-reply-memo.js +34 -0
- package/dist/features/bot-reply-memo.js.map +1 -0
- package/dist/features/emo-samples.d.ts +27 -0
- package/dist/features/emo-samples.d.ts.map +1 -0
- package/dist/features/emo-samples.js +116 -0
- package/dist/features/emo-samples.js.map +1 -0
- package/dist/features/four-source.d.ts +114 -0
- package/dist/features/four-source.d.ts.map +1 -0
- package/dist/features/four-source.js +260 -0
- package/dist/features/four-source.js.map +1 -0
- package/dist/features/image-url-ledger.d.ts +2 -0
- package/dist/features/image-url-ledger.d.ts.map +1 -1
- package/dist/features/image-url-ledger.js +23 -0
- package/dist/features/image-url-ledger.js.map +1 -1
- package/dist/features/intimacy-ledger.d.ts +31 -0
- package/dist/features/intimacy-ledger.d.ts.map +1 -0
- package/dist/features/intimacy-ledger.js +113 -0
- package/dist/features/intimacy-ledger.js.map +1 -0
- package/dist/features/local-embed.js +3 -3
- package/dist/features/local-embed.js.map +1 -1
- package/dist/features/local-signals.d.ts +130 -0
- package/dist/features/local-signals.d.ts.map +1 -0
- package/dist/features/local-signals.js +445 -0
- package/dist/features/local-signals.js.map +1 -0
- package/dist/features/people-memo.d.ts +52 -0
- package/dist/features/people-memo.d.ts.map +1 -0
- package/dist/features/people-memo.js +239 -0
- package/dist/features/people-memo.js.map +1 -0
- package/dist/features/quote-cache.d.ts +19 -0
- package/dist/features/quote-cache.d.ts.map +1 -0
- package/dist/features/quote-cache.js +67 -0
- package/dist/features/quote-cache.js.map +1 -0
- package/dist/features/self-disclosure.d.ts +28 -0
- package/dist/features/self-disclosure.d.ts.map +1 -0
- package/dist/features/self-disclosure.js +60 -0
- package/dist/features/self-disclosure.js.map +1 -0
- package/dist/features/tendency-samples.d.ts +22 -0
- package/dist/features/tendency-samples.d.ts.map +1 -0
- package/dist/features/tendency-samples.js +194 -0
- package/dist/features/tendency-samples.js.map +1 -0
- package/dist/features/thinking-log.d.ts +22 -0
- package/dist/features/thinking-log.d.ts.map +1 -0
- package/dist/features/thinking-log.js +70 -0
- package/dist/features/thinking-log.js.map +1 -0
- package/dist/features/xlsx.d.ts +28 -0
- package/dist/features/xlsx.d.ts.map +1 -0
- package/dist/features/xlsx.js +159 -0
- package/dist/features/xlsx.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -1
- package/dist/index.js.map +1 -1
- package/dist/session/session-manager.d.ts.map +1 -1
- package/dist/session/session-manager.js +25 -3
- package/dist/session/session-manager.js.map +1 -1
- package/dist/transport/events.d.ts +12 -0
- package/dist/transport/events.d.ts.map +1 -1
- package/dist/transport/events.js +15 -2
- package/dist/transport/events.js.map +1 -1
- package/dist/transport/inbound.d.ts.map +1 -1
- package/dist/transport/inbound.js +344 -20
- package/dist/transport/inbound.js.map +1 -1
- package/dist/transport/outbound-buffer.d.ts.map +1 -1
- package/dist/transport/outbound-buffer.js +8 -0
- package/dist/transport/outbound-buffer.js.map +1 -1
- package/dist/transport/outbound.d.ts +12 -0
- package/dist/transport/outbound.d.ts.map +1 -1
- package/dist/transport/outbound.js +170 -5
- package/dist/transport/outbound.js.map +1 -1
- package/docs/USER-GUIDE.md +429 -0
- package/package.json +2 -1
- package/scripts/download-model.mjs +3 -1
- package/settings-host.js +154 -0
package/settings-host.js
CHANGED
|
@@ -982,11 +982,26 @@ export function apply(ctx) {
|
|
|
982
982
|
const mod = await import('./dist/api/group-admin.js');
|
|
983
983
|
return { bot, client: mod.createGroupAdmin({ appId: bot.appId, appSecret: bot.appSecret }) };
|
|
984
984
|
}
|
|
985
|
+
/** 已判定「群不存在」的 gid → 判定时刻(死群退避用,24h TTL;内存即可,重启重探一次) */
|
|
986
|
+
const DEAD_GROUP_CACHE = new Map();
|
|
987
|
+
/**
|
|
988
|
+
* 审计日志滚动上限(2026-09-14 补):原来是纯 append,没有上限 ——
|
|
989
|
+
* 面板群列表接口每刷一次就要为**每个已死的群**写一条 group.dead(实测 9 天 4.2 万行 / 7.5MB)。
|
|
990
|
+
* 这里加**大小上限**:超过 2MB 就只保留最近 2000 行,任何一类事件刷屏都挡得住。
|
|
991
|
+
*/
|
|
992
|
+
const AUDIT_MAX_BYTES = 2 * 1024 * 1024;
|
|
993
|
+
const AUDIT_KEEP_LINES = 2000;
|
|
985
994
|
function audit(cwd, entry) {
|
|
986
995
|
try {
|
|
987
996
|
mkdirSync(join(cwd, '.qqbot'), { recursive: true });
|
|
988
997
|
const af = join(cwd, '.qqbot', 'group-audit.jsonl');
|
|
989
998
|
writeFileSync(af, JSON.stringify({ ts: new Date().toISOString(), ...entry }) + '\n', { flag: 'a' });
|
|
999
|
+
try {
|
|
1000
|
+
if (statSync(af).size > AUDIT_MAX_BYTES) {
|
|
1001
|
+
const lines = readFileSync(af, 'utf8').split('\n').filter(Boolean);
|
|
1002
|
+
writeFileSync(af, lines.slice(-AUDIT_KEEP_LINES).join('\n') + '\n', 'utf8');
|
|
1003
|
+
}
|
|
1004
|
+
} catch { /* 压缩失败不影响主链 */ }
|
|
990
1005
|
} catch { /* ignore */ }
|
|
991
1006
|
}
|
|
992
1007
|
function readGroupsJson(cwd) {
|
|
@@ -1161,6 +1176,11 @@ export function apply(ctx) {
|
|
|
1161
1176
|
const aliveReg = {};
|
|
1162
1177
|
if (gc) {
|
|
1163
1178
|
for (const g of [...map.values()].sort((a, b) => (b.lastAt || 0) - (a.lastAt || 0))) {
|
|
1179
|
+
// 死群退避(2026-09-14 补):已判「群不存在」的 gid 24h 内不再重复校验 ——
|
|
1180
|
+
// 原来这里每次都全量校验,面板每刷一次就为每个死群写一条 group.dead(实测 9 天 4.2 万行)。
|
|
1181
|
+
// 命中缓存直接跳过(**也不写审计**),过 24h 再探一次(万一群复活/又被拉回去)。
|
|
1182
|
+
const deadAt = DEAD_GROUP_CACHE.get(g.gid);
|
|
1183
|
+
if (deadAt && Date.now() - deadAt < 24 * 3600_000) continue;
|
|
1164
1184
|
try {
|
|
1165
1185
|
const info = await gc.client.getGroupInfo(g.gid);
|
|
1166
1186
|
if (info.ok && info.data) {
|
|
@@ -1170,6 +1190,7 @@ export function apply(ctx) {
|
|
|
1170
1190
|
aliveReg[g.gid] = { name: official || (reg[g.gid] && reg[g.gid].name) || '', lastAt: (reg[g.gid] && reg[g.gid].lastAt) || Date.now() };
|
|
1171
1191
|
} else {
|
|
1172
1192
|
// 11255 等 = 群已注销/不存在 → 不返回给 UI(避免选中后调用报错), 仅保留审计
|
|
1193
|
+
DEAD_GROUP_CACHE.set(g.gid, Date.now());
|
|
1173
1194
|
audit(bot.cwd, { ev: 'group.dead', ns: bot.id, gid: g.gid, code: info.err && info.err.code, human: info.err && info.err.human });
|
|
1174
1195
|
}
|
|
1175
1196
|
} catch (e2) {
|
|
@@ -1293,6 +1314,139 @@ export function apply(ctx) {
|
|
|
1293
1314
|
writeJson(res, 200, { ok: true, done, modelDir: dir });
|
|
1294
1315
|
} catch (e) { writeJson(res, 500, { ok: false, error: String((e && e.message) || e) }); }
|
|
1295
1316
|
});
|
|
1317
|
+
// 导出 Excel(2026-09-14 主人:"好感度熟识度可以一键导出 excel 表格,i need to share")
|
|
1318
|
+
// 零依赖:xlsx 由 dist/features/xlsx.js 现场拼(见那个文件头:不为了导出拖进 exceljs)
|
|
1319
|
+
// 三张表:熟识度 / 好感度 / 口径说明(分享给别人时看得懂)
|
|
1320
|
+
route(ctx, 'GET', '/api/qqbot-settings/export.xlsx', async (req, res) => {
|
|
1321
|
+
try {
|
|
1322
|
+
const u = new URL(req.url ?? '/', 'http://x');
|
|
1323
|
+
const bot = nsBot(NSQ(u));
|
|
1324
|
+
const dataRoot = (bot && bot.cfg && typeof bot.cfg.dataRoot === 'string' && bot.cfg.dataRoot) ? bot.cfg.dataRoot : ((bot && bot.cwd) || '');
|
|
1325
|
+
if (!dataRoot) return writeJson(res, 400, { ok: false, error: '找不到数据根目录' });
|
|
1326
|
+
const [{ buildXlsx }, affMod, attMod] = await Promise.all([
|
|
1327
|
+
import('./dist/features/xlsx.js'),
|
|
1328
|
+
import('./dist/features/local-signals.js'),
|
|
1329
|
+
import('./dist/features/attitude.js'),
|
|
1330
|
+
]);
|
|
1331
|
+
const limit = Math.max(1, Math.min(500, Number(u.searchParams.get('limit')) || 200));
|
|
1332
|
+
const aff = affMod.topAffinity(dataRoot, limit) || [];
|
|
1333
|
+
const att = attMod.topAttitude(dataRoot, limit) || [];
|
|
1334
|
+
const fmt = (ts) => (ts ? new Date(ts).toLocaleString('zh-CN') : '');
|
|
1335
|
+
const shortId = (k) => String(k || '').replace(/^(person|group|c2c):/, '');
|
|
1336
|
+
|
|
1337
|
+
const rows1 = [['#', '昵称', 'openid', '熟识度', '档位', '来过(天)', '消息数', '被点名', '接话', '最近活跃']];
|
|
1338
|
+
aff.forEach((x, i) => rows1.push([i + 1, x.name || '', shortId(x.key), x.score ?? '', x.tier || '', x.reviews ?? 0, x.msgs ?? 0, x.mentions ?? 0, x.replies ?? 0, fmt(x.lastAt)]));
|
|
1339
|
+
|
|
1340
|
+
const rows2 = [['#', '昵称', 'openid', '好感度', '档位', '好感占比', '事件数', '最近一次涨跌原因', '最近活跃']];
|
|
1341
|
+
att.forEach((x, i) => {
|
|
1342
|
+
let tier = '';
|
|
1343
|
+
let ratio = '';
|
|
1344
|
+
try {
|
|
1345
|
+
const g = attMod.attitudeGateFor(dataRoot, x.key);
|
|
1346
|
+
tier = g.tier;
|
|
1347
|
+
ratio = Math.round(g.ratio * 1000) / 1000;
|
|
1348
|
+
} catch { /* 算不出留空 */ }
|
|
1349
|
+
rows2.push([i + 1, x.name || '', shortId(x.key), x.a ?? '', tier, ratio, x.events ?? 0, x.lastWhy || '', fmt(x.lastAt)]);
|
|
1350
|
+
});
|
|
1351
|
+
|
|
1352
|
+
// 总览:两个维度**按人合并成一行**放第一张(2026-09-14 主人打开文件问"好感度呢?"——
|
|
1353
|
+
// 分表藏在底部标签页里容易漏看;分享场景下,一张总表最直观)
|
|
1354
|
+
const attByKey = new Map(att.map((x) => [x.key, x]));
|
|
1355
|
+
const seenKeys = new Set();
|
|
1356
|
+
const merged = [];
|
|
1357
|
+
for (const x of aff) { merged.push({ key: x.key, name: x.name, aff: x, att: attByKey.get(x.key) }); seenKeys.add(x.key); }
|
|
1358
|
+
for (const x of att) { if (!seenKeys.has(x.key)) merged.push({ key: x.key, name: x.name, aff: undefined, att: x }); }
|
|
1359
|
+
merged.sort((a, b) => (b.att?.a ?? -99) - (a.att?.a ?? -99));
|
|
1360
|
+
const rows0 = [['#', '昵称', 'openid', '熟识度', '熟识档位', '好感度', '好感档位', '好感占比', '来过(天)', '消息数', '被点名', '接话', '事件数', '最近一次涨跌原因', '最近活跃']];
|
|
1361
|
+
merged.forEach((m, i) => {
|
|
1362
|
+
let tier = '';
|
|
1363
|
+
let ratio = '';
|
|
1364
|
+
if (m.att) {
|
|
1365
|
+
try {
|
|
1366
|
+
const g = attMod.attitudeGateFor(dataRoot, m.key);
|
|
1367
|
+
tier = g.tier;
|
|
1368
|
+
ratio = Math.round(g.ratio * 1000) / 1000;
|
|
1369
|
+
} catch { /* 算不出留空 */ }
|
|
1370
|
+
}
|
|
1371
|
+
rows0.push([i + 1, m.name || '', shortId(m.key), m.aff?.score ?? '', m.aff?.tier ?? '', m.att?.a ?? '', tier, ratio,
|
|
1372
|
+
m.aff?.reviews ?? '', m.aff?.msgs ?? '', m.aff?.mentions ?? '', m.aff?.replies ?? '', m.att?.events ?? '', m.att?.lastWhy ?? '',
|
|
1373
|
+
fmt(m.att?.lastAt || m.aff?.lastAt)]);
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
const rows3 = [
|
|
1377
|
+
['一键导出 · 口径说明', ''], ['生成时间', new Date().toLocaleString('zh-CN')],
|
|
1378
|
+
['熟识度', '她把你记得多牢 —— 客观计数 + 记忆曲线,慢变(来过几天、说了多少、被点名、接话)'],
|
|
1379
|
+
['好感度', '她对你什么态度 —— 随事件可升可降(内心倾向判「亲近/拒绝/任务」+ 正文情绪判「暖/冷/中性」)'],
|
|
1380
|
+
['“最近一次涨跌原因”', '形如「内心亲近 +0.5」「内心拒绝 −0.5 / 又烦又冷(重罚) −0.5」「两好相凑 ×1.2」'],
|
|
1381
|
+
['判定把握', '本地小模型对不上号时会**弃权**(不给标签、不动分),所以有些格子是空的'],
|
|
1382
|
+
['红线', '负好感只退礼貌档:少主动,绝不冷落、阴阳、攻击'],
|
|
1383
|
+
['来源', 'dsh-qqbot 插件 · 数据文件 .qqbot/affinity.json 与 attitude.json'],
|
|
1384
|
+
];
|
|
1385
|
+
|
|
1386
|
+
const buf = buildXlsx([
|
|
1387
|
+
{ name: '总览', rows: rows0, widths: [5, 18, 34, 9, 10, 9, 10, 10, 9, 9, 9, 8, 9, 44, 20] },
|
|
1388
|
+
{ name: '熟识度', rows: rows1, widths: [5, 18, 34, 10, 10, 10, 9, 9, 8, 20] },
|
|
1389
|
+
{ name: '好感度', rows: rows2, widths: [5, 18, 34, 10, 10, 11, 9, 44, 20] },
|
|
1390
|
+
{ name: '说明', rows: rows3, widths: [20, 90] },
|
|
1391
|
+
]);
|
|
1392
|
+
const when = new Date();
|
|
1393
|
+
const p2 = (n) => String(n).padStart(2, '0');
|
|
1394
|
+
const cn = `熟识度好感度_${when.getFullYear()}${p2(when.getMonth() + 1)}${p2(when.getDate())}_${p2(when.getHours())}${p2(when.getMinutes())}.xlsx`;
|
|
1395
|
+
res.writeHead(200, {
|
|
1396
|
+
'content-type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
1397
|
+
'content-disposition': `attachment; filename="qqbot-stats.xlsx"; filename*=UTF-8''${encodeURIComponent(cn)}`,
|
|
1398
|
+
'content-length': String(buf.length),
|
|
1399
|
+
'cache-control': 'no-store',
|
|
1400
|
+
});
|
|
1401
|
+
res.end(buf);
|
|
1402
|
+
audit(dataRoot, { ev: 'export.xlsx', ns: bot && bot.id, rows: aff.length + att.length });
|
|
1403
|
+
} catch (e) { writeJson(res, 500, { ok: false, error: String((e && e.message) || e) }); }
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
// 好感度台账(2026-09-13 主人定, 观察期): 只读, 供面板显示"谁跟她最熟"
|
|
1407
|
+
// 数据源: {dataRoot}/.qqbot/affinity.json (每条群消息累计 互动/被点名/接话; 熟度现算, 公式透明)
|
|
1408
|
+
route(ctx, 'GET', '/api/qqbot-settings/affinity', async (req, res) => {
|
|
1409
|
+
try {
|
|
1410
|
+
const u = new URL(req.url ?? '/', 'http://x');
|
|
1411
|
+
const bot = nsBot(NSQ(u));
|
|
1412
|
+
const dataRoot = (bot && bot.cfg && typeof bot.cfg.dataRoot === 'string' && bot.cfg.dataRoot) ? bot.cfg.dataRoot : ((bot && bot.cwd) || '');
|
|
1413
|
+
if (!dataRoot) return writeJson(res, 200, { ok: true, items: [] });
|
|
1414
|
+
const mod = await import('./dist/features/local-signals.js');
|
|
1415
|
+
const limit = Math.max(1, Math.min(50, Math.round(Number(u.searchParams.get('limit'))) || 8));
|
|
1416
|
+
const items = (mod.topAffinity(dataRoot, limit) || []).map((x) => ({
|
|
1417
|
+
key: x.key, name: x.name || '', score: x.score, tier: x.tier || '', reviews: x.reviews || 0, msgs: x.msgs, mentions: x.mentions, replies: x.replies, lastAt: x.lastAt,
|
|
1418
|
+
}));
|
|
1419
|
+
writeJson(res, 200, { ok: true, items });
|
|
1420
|
+
} catch (e) { writeJson(res, 500, { ok: false, error: String((e && e.message) || e) }); }
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
// 好感度(2026-09-14 主人"直接推进"): A 值台账 —— 与「熟识度」是**两个维度**, 面板分两列
|
|
1424
|
+
// 熟识度 = 她把他记多牢(客观计数+记忆曲线, 慢变) | 好感度 = 她对他什么态度(随事件可升可降)
|
|
1425
|
+
// 数据源: {dataRoot}/.qqbot/attitude.json (内心倾向 + 正文情绪 → Δ → A 值, 见 features/attitude.ts)
|
|
1426
|
+
route(ctx, 'GET', '/api/qqbot-settings/attitude', async (req, res) => {
|
|
1427
|
+
try {
|
|
1428
|
+
const u = new URL(req.url ?? '/', 'http://x');
|
|
1429
|
+
const bot = nsBot(NSQ(u));
|
|
1430
|
+
const dataRoot = (bot && bot.cfg && typeof bot.cfg.dataRoot === 'string' && bot.cfg.dataRoot) ? bot.cfg.dataRoot : ((bot && bot.cwd) || '');
|
|
1431
|
+
if (!dataRoot) return writeJson(res, 200, { ok: true, items: [] });
|
|
1432
|
+
const mod = await import('./dist/features/attitude.js');
|
|
1433
|
+
const limit = Math.max(1, Math.min(50, Math.round(Number(u.searchParams.get('limit'))) || 8));
|
|
1434
|
+
const items = (mod.topAttitude(dataRoot, limit) || []).map((x) => {
|
|
1435
|
+
// 档位名(面板显示用,2026-09-14 主人要求):按"好感度占其范围的比例"分 很亲近/亲近/中立/冷淡/疏远
|
|
1436
|
+
let tier = '';
|
|
1437
|
+
let ratio = 0;
|
|
1438
|
+
try {
|
|
1439
|
+
const g = mod.attitudeGateFor(dataRoot, x.key);
|
|
1440
|
+
if (g) { tier = g.tier; ratio = Math.round(g.ratio * 1000) / 1000; }
|
|
1441
|
+
} catch { /* 算不出就不显示档位 */ }
|
|
1442
|
+
return {
|
|
1443
|
+
key: x.key, name: x.name || '', a: x.a, events: x.events || 0, lastAt: x.lastAt || 0, why: x.lastWhy || '', codes: x.lastCodes || [], tier, ratio,
|
|
1444
|
+
};
|
|
1445
|
+
});
|
|
1446
|
+
writeJson(res, 200, { ok: true, items });
|
|
1447
|
+
} catch (e) { writeJson(res, 500, { ok: false, error: String((e && e.message) || e) }); }
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1296
1450
|
// 价值样例库读写(2026-09-13 主人定): {dataRoot}/.qqbot/value-samples.jsonl —— 用户可直接编辑
|
|
1297
1451
|
function valueSamplesPathOf(bot) {
|
|
1298
1452
|
const dataRoot = (bot && bot.cfg && typeof bot.cfg.dataRoot === 'string' && bot.cfg.dataRoot)
|