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.
@@ -1,99 +1,124 @@
1
- /**
2
- * 事件账本的表结构
3
- * ==================
4
- *
5
- * 和 `schema.js`(统计库)分开:两套表在**两个文件**里,各有各的 `user_version`。
6
- * 混在一份里的话,加账本表就得动统计库的版本号,升级路径互相牵连。
7
- *
8
- * ── 为什么不复用 `messages` 表 ──
9
- *
10
- * `messages` 是「计数」,只记谁在哪天发了多少条,**刻意不存正文**。
11
- * 账本是「发生过什么」,必须存正文——这两件事的隐私承诺相反,
12
- * 合表等于把统计库那句「不存消息正文」变成假话。
13
- */
14
-
15
- export const EVENTS_SCHEMA_VERSION = 2;
16
-
17
- const V1 = `
18
- CREATE TABLE IF NOT EXISTS events (
19
- seq INTEGER PRIMARY KEY AUTOINCREMENT,
20
- id TEXT NOT NULL UNIQUE,
21
- bot_id TEXT NOT NULL,
22
- kind TEXT NOT NULL,
23
- chat_id TEXT NOT NULL,
24
- msg_id TEXT,
25
- user_id TEXT,
26
- user_name TEXT,
27
- -- 正文单独一列,不是「payload 里反正有」。
28
- -- 两个原因:① payload 有大小上限,截断时正文不能跟着丢;
29
- -- ② 将来要做全文检索(FTS/LIKE)必须有独立列,藏在 JSON 里查不了。
30
- content TEXT NOT NULL DEFAULT '',
31
- at INTEGER NOT NULL,
32
- observed_at INTEGER NOT NULL,
33
- time_basis TEXT NOT NULL,
34
- dedupe TEXT NOT NULL UNIQUE,
35
- msg_idx TEXT,
36
- ref_msg_idx TEXT,
37
- union_openid TEXT,
38
- quoted TEXT,
39
- payload TEXT NOT NULL
40
- );
41
-
42
- -- 邻文查询:按会话 + 时间取前后几条。
43
- -- 把 time_basis 放进索引的 WHERE,是为了**不让 unknown 进索引**——
44
- -- 时间不可靠的事件拼出来的上下文看着合理但是错的,比没有更糟。
45
- CREATE INDEX IF NOT EXISTS events_timeline
46
- ON events(bot_id, chat_id, at, seq) WHERE time_basis != 'unknown';
47
-
48
- CREATE INDEX IF NOT EXISTS events_user ON events(bot_id, user_id, at);
49
- CREATE INDEX IF NOT EXISTS events_at ON events(at);
50
-
51
- CREATE TABLE IF NOT EXISTS event_refs (
52
- from_id TEXT NOT NULL,
53
- to_id TEXT,
54
- to_msg_id TEXT,
55
- kind TEXT NOT NULL,
56
- resolved INTEGER NOT NULL DEFAULT 0
57
- );
58
- CREATE INDEX IF NOT EXISTS refs_from ON event_refs(from_id);
59
- CREATE INDEX IF NOT EXISTS refs_to ON event_refs(to_id) WHERE resolved = 1;
60
- `;
61
-
62
- /**
63
- * v2:链路 id。
64
- *
65
- * 一条消息从进站到回话的唯一标识(见 `src/trace.js`)。加它的理由很具体:
66
- * 用户说"刚才那条没回",现在只能翻四个日志文件对时间戳;有了这一列,
67
- * 拿 traceId 一条 SQL 就能找到那次请求对应的原文。
68
- *
69
- * 老库补空串——空串表示"这条早于 traceId 上线",不是"查不到"。
70
- */
71
- const V2 = `
72
- ALTER TABLE events ADD COLUMN trace TEXT NOT NULL DEFAULT '';
73
- CREATE INDEX IF NOT EXISTS events_trace ON events(bot_id, trace) WHERE trace != '';
74
- `;
75
-
76
- /** 表里有没有这一列(迁移要幂等,重复 ALTER 会抛) */
77
- function hasColumn(db, table, col) {
78
- try {
79
- return db.prepare(`PRAGMA table_info(${table})`).all().some((c) => c.name === col);
80
- } catch {
81
- return false;
82
- }
83
- }
84
-
85
- /**
86
- * @param {object} db DatabaseSync
87
- */
88
- export function migrateEvents(db) {
89
- const cur = Number(db.prepare('PRAGMA user_version').get()?.user_version ?? 0);
90
- if (cur >= EVENTS_SCHEMA_VERSION) return { from: cur, to: cur };
91
- if (cur < 1) db.exec(V1);
92
- /**
93
- * 幂等:老库里如果已经手工加过这一列,`ALTER` 会抛 duplicate column。
94
- * 版本号可能因为半途失败停在 1,所以这里必须按「列在不在」判断,不能只看版本。
95
- */
96
- if (!hasColumn(db, 'events', 'trace')) db.exec(V2);
97
- db.exec(`PRAGMA user_version = ${EVENTS_SCHEMA_VERSION}`);
98
- return { from: cur, to: EVENTS_SCHEMA_VERSION };
99
- }
1
+ /**
2
+ * 事件账本的表结构
3
+ * ==================
4
+ *
5
+ * 和 `schema.js`(统计库)分开:两套表在**两个文件**里,各有各的 `user_version`。
6
+ * 混在一份里的话,加账本表就得动统计库的版本号,升级路径互相牵连。
7
+ *
8
+ * ── 为什么不复用 `messages` 表 ──
9
+ *
10
+ * `messages` 是「计数」,只记谁在哪天发了多少条,**刻意不存正文**。
11
+ * 账本是「发生过什么」,必须存正文——这两件事的隐私承诺相反,
12
+ * 合表等于把统计库那句「不存消息正文」变成假话。
13
+ */
14
+
15
+ export const EVENTS_SCHEMA_VERSION = 3;
16
+
17
+ const V1 = `
18
+ CREATE TABLE IF NOT EXISTS events (
19
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
20
+ id TEXT NOT NULL UNIQUE,
21
+ bot_id TEXT NOT NULL,
22
+ kind TEXT NOT NULL,
23
+ chat_id TEXT NOT NULL,
24
+ msg_id TEXT,
25
+ user_id TEXT,
26
+ user_name TEXT,
27
+ -- 正文单独一列,不是「payload 里反正有」。
28
+ -- 两个原因:① payload 有大小上限,截断时正文不能跟着丢;
29
+ -- ② 将来要做全文检索(FTS/LIKE)必须有独立列,藏在 JSON 里查不了。
30
+ content TEXT NOT NULL DEFAULT '',
31
+ at INTEGER NOT NULL,
32
+ observed_at INTEGER NOT NULL,
33
+ time_basis TEXT NOT NULL,
34
+ dedupe TEXT NOT NULL UNIQUE,
35
+ msg_idx TEXT,
36
+ ref_msg_idx TEXT,
37
+ union_openid TEXT,
38
+ quoted TEXT,
39
+ payload TEXT NOT NULL
40
+ );
41
+
42
+ -- 邻文查询:按会话 + 时间取前后几条。
43
+ -- 把 time_basis 放进索引的 WHERE,是为了**不让 unknown 进索引**——
44
+ -- 时间不可靠的事件拼出来的上下文看着合理但是错的,比没有更糟。
45
+ CREATE INDEX IF NOT EXISTS events_timeline
46
+ ON events(bot_id, chat_id, at, seq) WHERE time_basis != 'unknown';
47
+
48
+ CREATE INDEX IF NOT EXISTS events_user ON events(bot_id, user_id, at);
49
+ CREATE INDEX IF NOT EXISTS events_at ON events(at);
50
+
51
+ CREATE TABLE IF NOT EXISTS event_refs (
52
+ from_id TEXT NOT NULL,
53
+ to_id TEXT,
54
+ to_msg_id TEXT,
55
+ kind TEXT NOT NULL,
56
+ resolved INTEGER NOT NULL DEFAULT 0
57
+ );
58
+ CREATE INDEX IF NOT EXISTS refs_from ON event_refs(from_id);
59
+ CREATE INDEX IF NOT EXISTS refs_to ON event_refs(to_id) WHERE resolved = 1;
60
+ `;
61
+
62
+ /**
63
+ * v2:链路 id。
64
+ *
65
+ * 一条消息从进站到回话的唯一标识(见 `src/trace.js`)。加它的理由很具体:
66
+ * 用户说"刚才那条没回",现在只能翻四个日志文件对时间戳;有了这一列,
67
+ * 拿 traceId 一条 SQL 就能找到那次请求对应的原文。
68
+ *
69
+ * 老库补空串——空串表示"这条早于 traceId 上线",不是"查不到"。
70
+ */
71
+ const V2 = `
72
+ ALTER TABLE events ADD COLUMN trace TEXT NOT NULL DEFAULT '';
73
+ CREATE INDEX IF NOT EXISTS events_trace ON events(bot_id, trace) WHERE trace != '';
74
+ `;
75
+
76
+ /**
77
+ * v3:谁说的。
78
+ *
79
+ * 出站记录(AI 或别的插件发的消息)要能标出**是哪个插件说的**——
80
+ * ai-chat 读账本时要能分清「这句是我说的」还是「回声说的」。
81
+ *
82
+ * 为什么是逗号分隔的字符串而不是 JSON 数组:一条出站消息可能由多个插件
83
+ * 合并而成(`message.js` 的 `flushReplies` 会把 `【回声】…【AI 对话】…` 拼成一条),
84
+ * 所以值可能是 `echo,ai-chat`。逗号串能直接 `LIKE '%ai-chat%'` 过滤,
85
+ * 也不用为此引 JSON1 函数。
86
+ *
87
+ * 空串的两种含义:入站消息(不是插件发的)、或早于本版(没记)。
88
+ */
89
+ const V3 = `
90
+ ALTER TABLE events ADD COLUMN plugin TEXT NOT NULL DEFAULT '';
91
+ CREATE INDEX IF NOT EXISTS events_plugin ON events(plugin) WHERE plugin != '';
92
+ `;
93
+
94
+ /** 表里有没有这一列(迁移要幂等,重复 ALTER 会抛) */
95
+ function hasColumn(db, table, col) {
96
+ try {
97
+ return db.prepare(`PRAGMA table_info(${table})`).all().some((c) => c.name === col);
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * @param {object} db DatabaseSync
105
+ */
106
+ export function migrateEvents(db) {
107
+ const cur = Number(db.prepare('PRAGMA user_version').get()?.user_version ?? 0);
108
+ if (cur >= EVENTS_SCHEMA_VERSION) return { from: cur, to: cur };
109
+ if (cur < 1) db.exec(V1);
110
+ /**
111
+ * 幂等:老库里如果已经手工加过这一列,`ALTER` 会抛 duplicate column。
112
+ * 版本号可能因为半途失败停在 1,所以这里必须按「列在不在」判断,不能只看版本。
113
+ */
114
+ if (!hasColumn(db, 'events', 'trace')) db.exec(V2);
115
+ /**
116
+ * 同样按「列在不在」判,不看版本号:测试里会用 `DROP COLUMN trace`
117
+ * + 把 user_version 改回 1 的方式造老库,那种库里可能已经有 plugin 列。
118
+ * 只看版本号会撞 `duplicate column name: plugin`,而 `open()` 会把异常
119
+ * 吞成一条 warn —— 表现是「账本静默不可用」,最难查。
120
+ */
121
+ if (!hasColumn(db, 'events', 'plugin')) db.exec(V3);
122
+ db.exec(`PRAGMA user_version = ${EVENTS_SCHEMA_VERSION}`);
123
+ return { from: cur, to: EVENTS_SCHEMA_VERSION };
124
+ }