koishi-plugin-kkk 3.9.0 → 3.10.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,83 @@
1
1
  # 更新日志
2
2
 
3
+ ## 3.10.0
4
+
5
+ > 解析消息底部的「打开原站」链接换成了**代码块形式的跳转卡片**:
6
+ > 整段复制后打开对应 App 会直接跳到那条作品,不再依赖 QQ 会不会把链接渲染成可点的东西。
7
+ > 快手和小红书以前完全没有这条链接,现在补上了。
8
+
9
+ ### 原站跳转改成「复制码」
10
+
11
+ - 发出的东西从 `[打开原站](链接)` 变成代码块:
12
+
13
+ ````
14
+ ```复制后打开抖音自动跳转 →
15
+ https://v.douyin.com/iQabcdef/ :1pm Mai:/ 2237886846@qq.com
16
+ ```
17
+ ````
18
+
19
+ 各家 App 都认「剪贴板里有一条自家链接」——复制整段文本打开 App 会弹直达询问,
20
+ 比点链接稳;官方 QQ 对 markdown 里的链接也经常直接拒收整条消息,代码块没有这个问题。
21
+ - 提示文案放在**围栏的语种位**(` ``` ` 后面那截),不占正文一行,
22
+ 代码块里就只有链接本身,全选复制不会带上多余文字。
23
+ - **抖音附带唤起口令**:光有链接 App 不一定弹跳转,接一段口令才稳。
24
+ 这是抖音 App 生成的固定串(每条作品都一样),不是模板。
25
+ - **快手、小红书单独发一条**:这两个平台没有清晰度面板——原来的跳转代码挂在画质表格下面,
26
+ 它们压根走不到,等于一条都没发过。现在在解析内容发完后补一条同格式的消息。
27
+ - 开关沿用 WebUI 的「原站跳转」(QQ 适配器分组,`qqPanelSourceLink`,默认开);
28
+ 跳转发送失败只记日志,**不影响解析结果**。
29
+
30
+ ### 跳转链接用用户发的那条
31
+
32
+ - 一开始让跳转链接和卡片右下角的二维码保持一致(`b23.tv/<bvid>`、抖音 `share_url`),
33
+ 实测行不通:那种链接带 `xsec_token` / `u_code` 一串参数,**太长,App 剪贴板识别不了**。
34
+ - 改成优先用用户原样发的那条(`b23.tv/xxx`、`v.douyin.com/xxx`、`v.kuaishou.com/xxx`、
35
+ `xhslink.com/o/xxx`)——那本来就是各家 App 自己生成的分享形态,最短也最容易被识别。
36
+ 取不到时才退回插件拼的标准链接。
37
+ - 卡片二维码**没动**:扫码不吃 URL 长度,换个链接反而要重测,保持原样。
38
+ - 顺带修了快手卡片二维码:以前填的是视频 CDN 直链,扫出来是段播放地址而不是作品页,
39
+ 现在指向作品页(`photoId` 取不到时仍退回原逻辑)。
40
+
41
+ ### markdown 只对 QQ / QQ 频道发
42
+
43
+ - 能渲染 markdown 的只有官方 QQ 适配器和 QQ 频道,其它平台(OneBot / Discord / Telegram /
44
+ KOOK…)会把代码块的 ` ``` ` 围栏原样显示成一串反引号。
45
+ - 判定从 OneBot 黑名单改成**平台白名单**(`qq` / `qqguild`):这两个发 markdown 代码块,
46
+ 其余一律发去围栏的纯文本(提示一行 + 链接一行),照样整段复制、App 一样认。
47
+
48
+ ## 3.9.1
49
+
50
+ > 修 MySQL 上启动直接报 `ER_TOO_LONG_KEY`、插件起不来的问题。
51
+ > 3.9.0 建的索引太宽了:索引字段现在按实际数据长度定长,最宽的索引从 4080 字节降到 1152 字节。
52
+
53
+ ### 修复 MySQL 索引过长(ER_TOO_LONG_KEY)
54
+
55
+ - **现象**:MySQL 环境启动就报 `ER_TOO_LONG_KEY`(Specified key was too long),索引超过 3072 字节,表建不出来。
56
+ - **原因**:字符串字段没给长度,MySQL 按 `VARCHAR(255)` 建,`utf8mb4` 下每字符 4 字节,
57
+ 单个字段就是 1020 字节。抖音作品缓存表那个由 **四个** 字段组成的唯一键
58
+ (作品 id + sec_uid + 群号 + 推送类型)算下来 4080 字节,直接超了上限。
59
+ 另有几张统计表卡在 3060 字节 —— 离上限只差 12 字节,随时会爆。
60
+ - **处理**:凡是进索引(主键 / 唯一键)的字段,都按实际数据长度定长:
61
+
62
+ | 字段 | 长度 | 说明 |
63
+ | --- | --- | --- |
64
+ | 群号 / QQ 号 / botId / 作品 id / 动态 id | 64 | 最多几十个字符 |
65
+ | 抖音 `sec_uid` | 128 | 实际最长见过 76 |
66
+ | 平台 / 内容形态 / 日期 / 指标名 | 16–32 | 固定枚举、`YYYY-MM-DD` |
67
+ | 过滤词 / 过滤标签 | 128 | 自定义文本 |
68
+ | 备注、昵称等 | 不限制 | 不进索引,不影响 |
69
+
70
+ 现在最宽的索引是 1152 字节(原来是 4080),其它都在 1024 以内。
71
+ - **在 MySQL 上已经建出过半截 `kkk` 表的**,需要先删掉再启动 —— 已有索引不一定能被自动改过来:
72
+
73
+ ```sql
74
+ SELECT TABLE_NAME FROM information_schema.TABLES
75
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME LIKE 'kkk%';
76
+ ```
77
+
78
+ 确认是插件的表之后 `DROP` 掉,重启 Koishi 会按新定义重建。
79
+ - sqlite / postgres 不受影响,也不用做上面这一步(字段长度本来就够)。
80
+
3
81
  ## 3.9.0
4
82
 
5
83
  > 插件不再自己建数据库了,全部改用 Koishi 自带的数据库服务。
@@ -1,10 +1,10 @@
1
1
  {
2
- "version": "3.9.0",
3
- "buildTime": "2026-09-29T04:52:03.418Z",
4
- "buildTimestamp": 1790657523454,
2
+ "version": "3.10.0",
3
+ "buildTime": "2026-09-29T18:43:09.611Z",
4
+ "buildTimestamp": 1790707389633,
5
5
  "name": "koishi-plugin-kkk",
6
6
  "description": "抖音 / B站 / 快手 / 小红书 视频解析与动态推送,带 QQ 交互面板与弹幕烧录(karin-plugin-kkk 的 Koishi 移植版)",
7
7
  "homepage": "https://github.com/maimai993/koishi-plugin-kkk#readme",
8
- "commitHash": "e58bb629886c203412e751fa87ef69b52dc44f52",
9
- "shortCommitHash": "e58bb62"
8
+ "commitHash": "ec43387a40b337a0c1430c7b2eef36668fa92fc5",
9
+ "shortCommitHash": "ec43387"
10
10
  }
@@ -229,7 +229,13 @@ const handleDouyin = (0, ParseForward_1.withParseForward)((0, ErrorHandler_1.wra
229
229
  * 面板按钮里继续用**原始分享链接**:`getDouyinID` 是靠跟随跳转拿 aweme_id 的,
230
230
  * 换成 `www.douyin.com/video/{id}` 反而可能撞上抖音的验证页。
231
231
  */
232
- if (iddata.type === 'one_work' && await tryQqPanel(e, { platform: 'douyin', url, id: String(iddata.aweme_id) }, flags)) {
232
+ if (iddata.type === 'one_work' && await tryQqPanel(e, {
233
+ platform: 'douyin',
234
+ url,
235
+ id: String(iddata.aweme_id),
236
+ // 跳转块用用户原样发的那条(App 分享出来的短链,识别率最高)
237
+ jumpUrl: url
238
+ }, flags)) {
233
239
  return;
234
240
  }
235
241
  // 真的开始解析了才提示「本部署烧不了弹幕」——切换面板时要重发面板,那时提示是多余的
@@ -320,7 +326,12 @@ const handleBilibili = (0, ParseForward_1.withParseForward)((0, ErrorHandler_1.w
320
326
  platform: 'bilibili',
321
327
  url: 'https://www.bilibili.com/video/' + iddata.bvid + (iddata.p ? '?p=' + iddata.p : ''),
322
328
  id: String(iddata.bvid),
323
- page: iddata.p
329
+ page: iddata.p,
330
+ /**
331
+ * 跳转块用**用户发出来的那条**(通常是 b23.tv 短链):
332
+ * 上面那条规范链接带 ?p= 参数,长度上去了 App 剪贴板就认不出来。
333
+ */
334
+ jumpUrl: String(url)
324
335
  }, flags)) {
325
336
  return;
326
337
  }
@@ -385,7 +396,13 @@ const handleKuaishou = (0, ParseForward_1.withParseForward)((0, ErrorHandler_1.w
385
396
  }
386
397
  const iddata = await (0, kuaishou_1.getKuaishouID)(String(kuaishouUrl));
387
398
  const WorkData = await (0, kuaishou_1.fetchKuaishouData)(iddata.type, iddata);
388
- const kuaishou = new kuaishou_1.Kuaishou(e, iddata);
399
+ /**
400
+ * 跳转块要用**用户发出来的那条**:`match(...g)` 拿到的是数组,
401
+ * `String(数组)` 会把多条链接用逗号拼成一串(一条链接时碰巧正确),
402
+ * 这里显式取第一个,保证只发出一条干净的原链接。
403
+ */
404
+ const kuaishouOrigin = Array.isArray(kuaishouUrl) && kuaishouUrl.length ? String(kuaishouUrl[0]) : String(kuaishouUrl ?? '');
405
+ const kuaishou = new kuaishou_1.Kuaishou(e, iddata, kuaishouOrigin);
389
406
  await (0, ParseOverride_1.runWithParseOverride)({
390
407
  ...flags.override,
391
408
  /** 面板按钮点出来的:只回「收到请求,开始下载」,不再发「检测到链接,开始解析」 */
@@ -427,7 +444,7 @@ const handleXiaohongshu = (0, ParseForward_1.withParseForward)((0, ErrorHandler_
427
444
  return;
428
445
  }
429
446
  const iddata = await (0, xiaohongshu_1.getXiaohongshuID)(url);
430
- const xiaohongshu = new xiaohongshu_1.Xiaohongshu(e, iddata);
447
+ const xiaohongshu = new xiaohongshu_1.Xiaohongshu(e, iddata, url);
431
448
  await (0, ParseOverride_1.runWithParseOverride)({
432
449
  ...flags.override,
433
450
  fromPanel: flags.panelToken !== undefined || flags.panel !== undefined
@@ -27,6 +27,14 @@ exports.TABLE = {
27
27
  statsMetric: 'kkk.statistics.metric',
28
28
  };
29
29
  /* ------------------------------ 建表 ------------------------------ */
30
+ /**
31
+ * 定长字符串字段。
32
+ *
33
+ * 走索引的字段必须给长度(见文件头「为什么索引字段都写了明确长度」),
34
+ * 这里的数字都是按实际数据留的余量:群号 / QQ 号 / 作品 id 这类最多几十个字符,
35
+ * 抖音 sec_uid 最长见过 76,给 128。
36
+ */
37
+ const str = (length) => ({ type: 'string', length });
30
38
  /** 进程内只建一次(插件热重载时 apply 会再跑一遍,重复 extend 没意义) */
31
39
  let modelsReady = false;
32
40
  /**
@@ -39,162 +47,162 @@ const extendModels = (ctx) => {
39
47
  modelsReady = true;
40
48
  // ---------- 抖音 ----------
41
49
  ctx.model.extend(exports.TABLE.douyinBot, {
42
- id: 'string',
43
- createdAt: 'string',
44
- updatedAt: 'string',
50
+ id: str(64),
51
+ createdAt: str(32),
52
+ updatedAt: str(32),
45
53
  }, { primary: 'id' });
46
54
  ctx.model.extend(exports.TABLE.douyinGroup, {
47
- id: 'string',
48
- botId: 'string',
49
- createdAt: 'string',
50
- updatedAt: 'string',
55
+ id: str(64),
56
+ botId: str(64),
57
+ createdAt: str(32),
58
+ updatedAt: str(32),
51
59
  }, { primary: ['id', 'botId'] });
52
60
  ctx.model.extend(exports.TABLE.douyinUser, {
53
- sec_uid: 'string',
54
- short_id: 'string',
61
+ sec_uid: str(128),
62
+ short_id: str(64),
55
63
  remark: 'string',
56
64
  living: 'boolean',
57
- filterMode: 'string',
58
- createdAt: 'string',
59
- updatedAt: 'string',
65
+ filterMode: str(16),
66
+ createdAt: str(32),
67
+ updatedAt: str(32),
60
68
  }, { primary: 'sec_uid' });
61
69
  ctx.model.extend(exports.TABLE.douyinSubscription, {
62
- groupId: 'string',
63
- sec_uid: 'string',
64
- createdAt: 'string',
65
- updatedAt: 'string',
70
+ groupId: str(64),
71
+ sec_uid: str(128),
72
+ createdAt: str(32),
73
+ updatedAt: str(32),
66
74
  }, { primary: ['groupId', 'sec_uid'] });
67
75
  ctx.model.extend(exports.TABLE.douyinAwemeCache, {
68
76
  id: 'unsigned',
69
- aweme_id: 'string',
70
- sec_uid: 'string',
71
- groupId: 'string',
72
- pushType: 'string',
73
- createdAt: 'string',
74
- updatedAt: 'string',
77
+ aweme_id: str(64),
78
+ sec_uid: str(128),
79
+ groupId: str(64),
80
+ pushType: str(32),
81
+ createdAt: str(32),
82
+ updatedAt: str(32),
75
83
  }, { autoInc: true, unique: [['aweme_id', 'sec_uid', 'groupId', 'pushType']] });
76
84
  ctx.model.extend(exports.TABLE.douyinFilterWord, {
77
85
  id: 'unsigned',
78
- sec_uid: 'string',
79
- word: 'string',
80
- createdAt: 'string',
81
- updatedAt: 'string',
86
+ sec_uid: str(128),
87
+ word: str(128),
88
+ createdAt: str(32),
89
+ updatedAt: str(32),
82
90
  }, { autoInc: true, unique: [['sec_uid', 'word']] });
83
91
  ctx.model.extend(exports.TABLE.douyinFilterTag, {
84
92
  id: 'unsigned',
85
- sec_uid: 'string',
86
- tag: 'string',
87
- createdAt: 'string',
88
- updatedAt: 'string',
93
+ sec_uid: str(128),
94
+ tag: str(128),
95
+ createdAt: str(32),
96
+ updatedAt: str(32),
89
97
  }, { autoInc: true, unique: [['sec_uid', 'tag']] });
90
98
  ctx.model.extend(exports.TABLE.douyinListSnapshot, {
91
99
  id: 'unsigned',
92
- sec_uid: 'string',
93
- pushType: 'string',
94
- aweme_id: 'string',
95
- createdAt: 'string',
96
- updatedAt: 'string',
100
+ sec_uid: str(128),
101
+ pushType: str(32),
102
+ aweme_id: str(64),
103
+ createdAt: str(32),
104
+ updatedAt: str(32),
97
105
  }, { autoInc: true, unique: [['sec_uid', 'pushType', 'aweme_id']] });
98
106
  // ---------- B站 ----------
99
107
  ctx.model.extend(exports.TABLE.bilibiliBot, {
100
- id: 'string',
101
- createdAt: 'string',
102
- updatedAt: 'string',
108
+ id: str(64),
109
+ createdAt: str(32),
110
+ updatedAt: str(32),
103
111
  }, { primary: 'id' });
104
112
  ctx.model.extend(exports.TABLE.bilibiliGroup, {
105
- id: 'string',
106
- botId: 'string',
107
- createdAt: 'string',
108
- updatedAt: 'string',
113
+ id: str(64),
114
+ botId: str(64),
115
+ createdAt: str(32),
116
+ updatedAt: str(32),
109
117
  }, { primary: ['id', 'botId'] });
110
118
  ctx.model.extend(exports.TABLE.bilibiliUser, {
111
119
  host_mid: 'unsigned',
112
120
  remark: 'string',
113
- filterMode: 'string',
114
- createdAt: 'string',
115
- updatedAt: 'string',
121
+ filterMode: str(16),
122
+ createdAt: str(32),
123
+ updatedAt: str(32),
116
124
  }, { primary: 'host_mid' });
117
125
  ctx.model.extend(exports.TABLE.bilibiliSubscription, {
118
- groupId: 'string',
126
+ groupId: str(64),
119
127
  host_mid: 'unsigned',
120
- createdAt: 'string',
121
- updatedAt: 'string',
128
+ createdAt: str(32),
129
+ updatedAt: str(32),
122
130
  }, { primary: ['groupId', 'host_mid'] });
123
131
  ctx.model.extend(exports.TABLE.bilibiliDynamicCache, {
124
132
  id: 'unsigned',
125
- dynamic_id: 'string',
133
+ dynamic_id: str(64),
126
134
  host_mid: 'unsigned',
127
- groupId: 'string',
128
- dynamic_type: 'string',
129
- createdAt: 'string',
130
- updatedAt: 'string',
135
+ groupId: str(64),
136
+ dynamic_type: str(32),
137
+ createdAt: str(32),
138
+ updatedAt: str(32),
131
139
  }, { autoInc: true, unique: [['dynamic_id', 'host_mid', 'groupId']] });
132
140
  ctx.model.extend(exports.TABLE.bilibiliFilterWord, {
133
141
  id: 'unsigned',
134
142
  host_mid: 'unsigned',
135
- word: 'string',
136
- createdAt: 'string',
137
- updatedAt: 'string',
143
+ word: str(128),
144
+ createdAt: str(32),
145
+ updatedAt: str(32),
138
146
  }, { autoInc: true, unique: [['host_mid', 'word']] });
139
147
  ctx.model.extend(exports.TABLE.bilibiliFilterTag, {
140
148
  id: 'unsigned',
141
149
  host_mid: 'unsigned',
142
- tag: 'string',
143
- createdAt: 'string',
144
- updatedAt: 'string',
150
+ tag: str(128),
151
+ createdAt: str(32),
152
+ updatedAt: str(32),
145
153
  }, { autoInc: true, unique: [['host_mid', 'tag']] });
146
154
  // ---------- 统计 ----------
147
155
  ctx.model.extend(exports.TABLE.statsParse, {
148
156
  id: 'unsigned',
149
- groupId: 'string',
150
- userId: 'string',
151
- platform: 'string',
157
+ groupId: str(64),
158
+ userId: str(64),
159
+ platform: str(32),
152
160
  parseCount: 'unsigned',
153
- createdAt: 'string',
154
- updatedAt: 'string',
161
+ createdAt: str(32),
162
+ updatedAt: str(32),
155
163
  }, { autoInc: true, unique: [['groupId', 'userId', 'platform']] });
156
164
  ctx.model.extend(exports.TABLE.statsHistory, {
157
165
  id: 'unsigned',
158
- date: 'string',
166
+ date: str(16),
159
167
  totalParses: 'unsigned',
160
168
  douyin: 'unsigned',
161
169
  bilibili: 'unsigned',
162
170
  kuaishou: 'unsigned',
163
171
  xiaohongshu: 'unsigned',
164
- createdAt: 'string',
172
+ createdAt: str(32),
165
173
  }, { autoInc: true, unique: [['date']] });
166
174
  ctx.model.extend(exports.TABLE.statsGlobal, {
167
- key: 'string',
175
+ key: str(64),
168
176
  value: 'unsigned',
169
- updatedAt: 'string',
177
+ updatedAt: str(32),
170
178
  }, { primary: 'key' });
171
179
  ctx.model.extend(exports.TABLE.statsGroupHistory, {
172
- groupId: 'string',
173
- date: 'string',
174
- platform: 'string',
180
+ groupId: str(64),
181
+ date: str(16),
182
+ platform: str(32),
175
183
  parseCount: 'unsigned',
176
- updatedAt: 'string',
184
+ updatedAt: str(32),
177
185
  }, { primary: ['groupId', 'date', 'platform'] });
178
186
  ctx.model.extend(exports.TABLE.statsHour, {
179
- groupId: 'string',
187
+ groupId: str(64),
180
188
  hour: 'unsigned',
181
- platform: 'string',
189
+ platform: str(32),
182
190
  parseCount: 'unsigned',
183
- updatedAt: 'string',
191
+ updatedAt: str(32),
184
192
  }, { primary: ['groupId', 'hour', 'platform'] });
185
193
  ctx.model.extend(exports.TABLE.statsWorkType, {
186
- groupId: 'string',
187
- platform: 'string',
188
- workType: 'string',
194
+ groupId: str(64),
195
+ platform: str(32),
196
+ workType: str(32),
189
197
  parseCount: 'unsigned',
190
- updatedAt: 'string',
198
+ updatedAt: str(32),
191
199
  }, { primary: ['groupId', 'platform', 'workType'] });
192
200
  ctx.model.extend(exports.TABLE.statsMetric, {
193
- groupId: 'string',
194
- metric: 'string',
195
- bucket: 'string',
201
+ groupId: str(64),
202
+ metric: str(32),
203
+ bucket: str(32),
196
204
  parseCount: 'unsigned',
197
- updatedAt: 'string',
205
+ updatedAt: str(32),
198
206
  }, { primary: ['groupId', 'metric', 'bucket'] });
199
207
  };
200
208
  exports.extendModels = extendModels;