qq-bot-roost 1.0.2 → 1.0.4
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 +2 -2
- package/package.json +57 -57
- package/src/bot.js +10 -1
- package/src/config.js +741 -730
- package/src/ledger.js +905 -554
- package/src/message.js +998 -900
- package/src/outbound.js +791 -690
- package/src/plugin/api-record.js +129 -0
- package/src/plugin/context.js +990 -976
- package/src/storage/events-schema.js +124 -99
- package/src/web/server.js +3494 -3280
- package/web/app.js +4372 -4053
- package/web/index.html +1203 -1116
- package/web/style.css +44 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把 `ctx.api` 包成「发出去就补一笔账」的代理。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要:出站账本只在 `Outbound.send()` 的两个出口落账,而插件拿到的是
|
|
5
|
+
* **裸的 QQApi 实例**——`ctx.api.sendGroup(...)` 直接打接口,既不进账本、
|
|
6
|
+
* 「谁」列也不带插件名。复核把这条列成「绕过 outbound 的既有路径」,按主人
|
|
7
|
+
* 拍板的 C 档(Proxy 包装)在这里收口。
|
|
8
|
+
*
|
|
9
|
+
* ## 改这个文件前必须先读的三条
|
|
10
|
+
*
|
|
11
|
+
* 1. **不能把 `this` 绑成代理。** `QQApi` 内部大量用私有方法(`#request` /
|
|
12
|
+
* `#payload` / `#nextSeq`),私有字段只认「声明它的那个对象」——拿代理当
|
|
13
|
+
* receiver 会直接抛 `Cannot read private member from an object whose class
|
|
14
|
+
* did not declare it`。所以:
|
|
15
|
+
* · 不拦截的方法一律 `raw.bind(target)`,绑回原对象;
|
|
16
|
+
* · 拦截的方法用 `raw.apply(target, args)`,内部调用照旧走原对象。
|
|
17
|
+
* 2. **不改原对象。** 代理是独立的一层,`api.sendGroup` 一个字节都不动;
|
|
18
|
+
* 机器人自己持有的那个实例(`bot.js` 的 `this.api`)行为完全不变。
|
|
19
|
+
* 3. **只在成功之后记。** 抛错就当没发出去(与 `Outbound.send` 一致);
|
|
20
|
+
* 记账本身出错也只打日志,绝不冒泡到发送链路。
|
|
21
|
+
*
|
|
22
|
+
* ## 已知边界(写在这里,免得以后当 bug 查)
|
|
23
|
+
*
|
|
24
|
+
* · `ctx.api !== bot.api`(对象身份变了)。`instanceof` 仍成立(代理沿用同一
|
|
25
|
+
* 原型),但严格相等会失败——这是 C 档的已知代价。
|
|
26
|
+
* · 插件把方法解构成 `const { sendGroup } = ctx.api` 再调用,拿到的仍是绑好
|
|
27
|
+
* 的函数,照样记账。
|
|
28
|
+
* · `sendMedia` 内部调 `this.sendGroup`,因为 `this` 是原对象,**不会**被本
|
|
29
|
+
* 代理二次拦截——所以它只记一笔,不会双记。
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 哪几个方法算「真的发出去了」。
|
|
34
|
+
*
|
|
35
|
+
* `target` / `payload` 是实参下标(不是名字),因为这几个方法的签名并不统一:
|
|
36
|
+
* 普通发送是 `(target, opts)`,`sendMedia` 是 `(scene, target, opts)`。
|
|
37
|
+
* `finalOnly` 只给流式消息用:流式是「一条消息在长」,每片都记会让账本里
|
|
38
|
+
* 冒出十几行同一个回复。
|
|
39
|
+
*/
|
|
40
|
+
const SEND_CALLS = {
|
|
41
|
+
sendGroup: { kind: () => 'group', target: 0, payload: 1 },
|
|
42
|
+
sendC2C: { kind: () => 'c2c', target: 0, payload: 1 },
|
|
43
|
+
sendMedia: {
|
|
44
|
+
kind: (args) => (String(args[0] ?? '') === 'group' ? 'group' : 'c2c'),
|
|
45
|
+
target: 1,
|
|
46
|
+
payload: 2,
|
|
47
|
+
},
|
|
48
|
+
sendStreamMessage: {
|
|
49
|
+
kind: () => 'c2c',
|
|
50
|
+
target: 0,
|
|
51
|
+
payload: 1,
|
|
52
|
+
finalOnly: true,
|
|
53
|
+
/**
|
|
54
|
+
* 流式报文字段是 `contentRaw`,和普通发送的 `content` 不同名;
|
|
55
|
+
* 不转一下,账本摘要会是一条空行。`digestTextOf` 只认 `content`/`text`。
|
|
56
|
+
*/
|
|
57
|
+
digestOf: (args) => ({ content: String(args[1]?.contentRaw ?? '') }),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {object} api QQApi 实例(原对象,不会被修改)
|
|
63
|
+
* @param {object} opts
|
|
64
|
+
* @param {object} [opts.outbound] 出站对象,只用到 `recordDirect()`
|
|
65
|
+
* @param {string} [opts.plugin] 记账时写的插件名
|
|
66
|
+
* @param {object} [opts.log] logger,用于记账失败时的告警
|
|
67
|
+
* @returns {object} 代理;`api` 不是对象时原样返回
|
|
68
|
+
*/
|
|
69
|
+
export function wrapApiForLedger(api, { outbound = null, plugin = '', log = null } = {}) {
|
|
70
|
+
if (!api || typeof api !== 'object') return api;
|
|
71
|
+
|
|
72
|
+
/** 同一个原函数只造一次包装:`ctx.api.f === ctx.api.f` 要成立 */
|
|
73
|
+
const plain = new Map();
|
|
74
|
+
const recording = new Map();
|
|
75
|
+
|
|
76
|
+
function note(spec, args, r) {
|
|
77
|
+
try {
|
|
78
|
+
if (!outbound?.recordDirect) return;
|
|
79
|
+
const payload = args[spec.payload];
|
|
80
|
+
const p = payload && typeof payload === 'object' ? payload : {};
|
|
81
|
+
/** 流式只在结束片(state=10)记一笔 */
|
|
82
|
+
if (spec.finalOnly && Number(p.state) !== 10) return;
|
|
83
|
+
outbound.recordDirect({
|
|
84
|
+
kind: spec.kind(args),
|
|
85
|
+
target: String(args[spec.target] ?? ''),
|
|
86
|
+
payload: spec.digestOf ? spec.digestOf(args) : p,
|
|
87
|
+
plugin,
|
|
88
|
+
/**
|
|
89
|
+
* 用 `||` 不用 `??`:服务端可能给 `id: ''`,而空 id 会让账本的
|
|
90
|
+
* 去重退化成「同毫秒 + 同摘要」——连发几张图时会被 `INSERT OR IGNORE`
|
|
91
|
+
* 静默吞掉几行(复核实测:同毫秒 3 张图只剩 1 行)。
|
|
92
|
+
* `file_uuid` 是上传接口的返回,`sendMedia` 在部分宿主上只回它。
|
|
93
|
+
*/
|
|
94
|
+
id: String(r?.id || r?.stream_msg_id || r?.file_uuid || ''),
|
|
95
|
+
});
|
|
96
|
+
} catch (e) {
|
|
97
|
+
log?.warn?.(`出站没记进账本(不影响发送):${e?.message ?? e}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return new Proxy(api, {
|
|
102
|
+
get(target, prop) {
|
|
103
|
+
const raw = Reflect.get(target, prop, target);
|
|
104
|
+
/** `constructor` 是类本身,绑上去会让 `new ctx.api.constructor()` 失效 */
|
|
105
|
+
if (typeof raw !== 'function' || prop === 'constructor') return raw;
|
|
106
|
+
|
|
107
|
+
const spec = Object.hasOwn(SEND_CALLS, prop) ? SEND_CALLS[prop] : null;
|
|
108
|
+
if (!spec) {
|
|
109
|
+
let f = plain.get(raw);
|
|
110
|
+
if (!f) {
|
|
111
|
+
f = raw.bind(target);
|
|
112
|
+
plain.set(raw, f);
|
|
113
|
+
}
|
|
114
|
+
return f;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let f = recording.get(raw);
|
|
118
|
+
if (!f) {
|
|
119
|
+
f = async (...args) => {
|
|
120
|
+
const r = await raw.apply(target, args);
|
|
121
|
+
note(spec, args, r);
|
|
122
|
+
return r;
|
|
123
|
+
};
|
|
124
|
+
recording.set(raw, f);
|
|
125
|
+
}
|
|
126
|
+
return f;
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|