koishi-plugin-kkk 3.4.0 → 3.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # 更新日志
2
2
 
3
+ ## 3.4.1
4
+
5
+ ### 抖音解析失败时的提示看得懂了
6
+
7
+ - 抖音接口没返回数据时(常见原因:Cookie 过期、被风控,或者链接 / 作品已失效),以前会直接抛一个英文错误
8
+ TypeError: Cannot read properties of undefined (reading 'aweme_detail'),你收到的是一张满屏英文的错误卡片。
9
+ 现在会明说是哪一类问题:抖音没有返回这条作品的数据(错误码 8:风控校验失败),常见原因:Cookie 失效、被风控,或者这条链接已经失效 / 作品已删除。
10
+ - 日志里会留下接口的原始错误码和返回内容,排查时一眼就能看出是风控还是 Cookie 过期。
11
+ - 顺带:抖音的冒烟测试补了「接口被风控」这个场景,以后不会再退回那种英文报错。
12
+
13
+ ---
14
+
3
15
  ## 3.4.0
4
16
 
5
17
  > 这一版修的都是「用起来别扭」的地方:图片不再重复发、面板不再和卡片挤在一起、
@@ -19,6 +31,7 @@
19
31
  ### 出错的时候:上报照旧,发什么你说了算
20
32
 
21
33
  - 新增开关「**出错只发文字(不渲染错误卡片)**」(面板 → 通用 → 错误上报,默认关闭)。
34
+ 想法来自 @Jingmozhiyu 的 PR #1
22
35
  打开后出错只在群里发一行:错误信息已上传,ID:xxx —— 上传的东西一点不少,只是省掉十几秒渲染和一张几 MB 的大图。
23
36
  - 以前错误提示里那个加群链接是拿群号拼的,点开是 404;现在换成真正的加群链接,能直接点进去。
24
37
  - 以前遇到「一行日志里塞了几 MB 的图片数据」这种报错,错误卡片要渲染十几分钟、最后还发不出去;现在这种超长内容会自动折起来,几秒钟出图。
@@ -91,8 +91,24 @@ class DouYin extends utils_1.Base {
91
91
  const VideoData = await this.amagi.douyin.fetcher.parseWork({
92
92
  aweme_id: data.aweme_id
93
93
  });
94
- if (VideoData.data.aweme_detail === null) {
95
- throw new Error('获取作品详情失败,可能是因为该作品已被删除或设置为私密。');
94
+ /**
95
+ * **接口没给出作品详情时的兜底**(线上真实报错):
96
+ *
97
+ * TypeError: Cannot read properties of undefined (reading 'aweme_detail')
98
+ * at DouYin.DouyinHandler (douyin.js:94)
99
+ *
100
+ * 以前只挡了「aweme_detail === null」这一种 —— 而接口被风控 / Cookie 失效 / 链接失效时,
101
+ * 返回体里**连 data 都没有**,于是直接 TypeError,用户拿到的是一张满屏英文的错误卡片。
102
+ * 现在把整段摊开检查,并给一句人话 + 把原始响应写进日志(排查时才看得到错误码)。
103
+ */
104
+ if (!VideoData || !VideoData.data || VideoData.data.aweme_detail === null || VideoData.data.aweme_detail === undefined) {
105
+ const raw = VideoData ?? {};
106
+ const code = raw.code ?? raw.data?.code;
107
+ const message = raw.message ?? raw.msg ?? raw.data?.message ?? raw.data?.msg;
108
+ node_karin_2.logger.error('[抖音] 接口没有返回作品详情: ' + JSON.stringify({ code, message, 顶层键: Object.keys(raw) }).slice(0, 300));
109
+ throw new Error('抖音没有返回这条作品的数据'
110
+ + (code !== undefined ? '(错误码 ' + String(code) + (message ? ':' + String(message) : '') + ')' : '')
111
+ + ',常见原因:Cookie 失效、被风控,或者这条链接已经失效 / 作品已删除。');
96
112
  }
97
113
  // 根据 API 返回的数据判断作品类型,而不是依赖 URL
98
114
  // aweme_type: 0=视频, 68=图集, 163=文章
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-kkk",
3
- "version": "3.4.0",
3
+ "version": "3.4.1",
4
4
  "description": "抖音 / B站 / 快手 / 小红书 视频解析与动态推送,带 QQ 交互面板与弹幕烧录(karin-plugin-kkk 的 Koishi 移植版)",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
@@ -40,6 +40,9 @@ const LONG_URL = 'https://www.douyin.com/video/7123456789012345678'
40
40
  axios.get = async () => ({ request: { res: { responseUrl: LONG_URL } }, data: '' })
41
41
 
42
42
  // 2) 替换 amagi 的客户端工厂(每个 Base 实例都会新建 client,必须包住工厂)
43
+ /** 由末尾的第二个场景打开:让 parseWork 返回「风控」响应 */
44
+ let windy = false
45
+
43
46
  const amagi = require('@ikenxuan/amagi')
44
47
  const realFactory = amagi.default
45
48
 
@@ -94,7 +97,10 @@ function makeDetail () {
94
97
 
95
98
  amagi.default = function (options) {
96
99
  const client = realFactory(options)
97
- client.douyin.fetcher.parseWork = async () => ({ success: true, code: 200, message: 'OK', data: { aweme_detail: makeDetail() } })
100
+ // windy = true 时模拟「接口被风控」:返回体里连 data 都没有(见文件末尾的第二个场景)
101
+ client.douyin.fetcher.parseWork = async () => (windy
102
+ ? { success: false, code: 8, message: '风控校验失败' }
103
+ : { success: true, code: 200, message: 'OK', data: { aweme_detail: makeDetail() } })
98
104
  client.douyin.fetcher.fetchWorkComments = async () => ({ success: true, code: 200, message: 'OK', data: { comments: [], cursor: 0, has_more: 0, total: 0 } })
99
105
  client.douyin.fetcher.fetchEmojiList = async () => ({ success: true, code: 200, message: 'OK', data: { emoji_list: [] } })
100
106
  client.douyin.fetcher.fetchUserProfile = async () => ({
@@ -134,7 +140,8 @@ const middlewares = []
134
140
  const originalMiddleware = ctx.middleware.bind(ctx)
135
141
  ctx.middleware = (fn, ...rest) => { middlewares.push(fn); return originalMiddleware(fn, ...rest) }
136
142
 
137
- ctx.plugin(plugin, { dataPath: dataRoot, debug: true, masters: ['12345'], webui: false })
143
+ // parseDedupe: false —— 下面有两个场景会解析同一条链接,去重会把第二个场景挡掉
144
+ ctx.plugin(plugin, { dataPath: dataRoot, debug: true, masters: ['12345'], webui: false, parseDedupe: false })
138
145
 
139
146
  const makeSession = (content) => ({
140
147
  content, selfId: '10000', userId: '12345', guildId: '456', channelId: '456',
@@ -170,6 +177,27 @@ setTimeout(async () => {
170
177
  return '[' + type + '] ' + attrs
171
178
  }).join(' | ').slice(0, 400))
172
179
  }
180
+ /**
181
+ * 第二个场景:**接口被风控**(返回体里连 data 都没有)。
182
+ *
183
+ * 线上真实报错:以前只挡了 aweme_detail === null,风控时直接
184
+ * TypeError: Cannot read properties of undefined (reading 'aweme_detail') —— 用户拿到一张满屏英文的错误卡片。
185
+ * 现在应当给一句人话(并且日志里留下错误码)。
186
+ */
187
+ console.log('\n=== 抖音接口被风控时的提示 ===')
188
+ windy = true
189
+ const before = sent.length
190
+ try {
191
+ await dispatch('https://www.douyin.com/video/7123456789012345678')
192
+ console.log(' ❌ 期望它抛错(这样才会走错误提示),但它正常跑完了')
193
+ process.exitCode = 1
194
+ } catch (error) {
195
+ const message = String(error?.message ?? error)
196
+ const friendly = /抖音没有返回这条作品的数据/.test(message) && /风控校验失败/.test(message)
197
+ console.log((friendly ? ' ✅' : ' ❌') + ' 提示是人话: ' + message.slice(0, 140))
198
+ if (!friendly) process.exitCode = 1
199
+ if (sent.length !== before) console.log(' (顺带:还多发了 ' + (sent.length - before) + ' 条消息)')
200
+ }
173
201
  } catch (error) {
174
202
  console.error('抖音冒烟测试失败:', error && error.stack ? error.stack : error)
175
203
  process.exitCode = 1
@@ -115,8 +115,24 @@ export class DouYin extends Base {
115
115
  aweme_id: data.aweme_id
116
116
  })
117
117
 
118
- if (VideoData.data.aweme_detail === null) {
119
- throw new Error('获取作品详情失败,可能是因为该作品已被删除或设置为私密。')
118
+ /**
119
+ * **接口没给出作品详情时的兜底**(线上真实报错):
120
+ *
121
+ * TypeError: Cannot read properties of undefined (reading 'aweme_detail')
122
+ * at DouYin.DouyinHandler (douyin.js:94)
123
+ *
124
+ * 以前只挡了「aweme_detail === null」这一种 —— 而接口被风控 / Cookie 失效 / 链接失效时,
125
+ * 返回体里**连 data 都没有**,于是直接 TypeError,用户拿到的是一张满屏英文的错误卡片。
126
+ * 现在把整段摊开检查,并给一句人话 + 把原始响应写进日志(排查时才看得到错误码)。
127
+ */
128
+ if (!VideoData || !(VideoData as any).data || (VideoData as any).data.aweme_detail === null || (VideoData as any).data.aweme_detail === undefined) {
129
+ const raw: any = VideoData ?? {}
130
+ const code = raw.code ?? raw.data?.code
131
+ const message = raw.message ?? raw.msg ?? raw.data?.message ?? raw.data?.msg
132
+ logger.error('[抖音] 接口没有返回作品详情: ' + JSON.stringify({ code, message, 顶层键: Object.keys(raw) }).slice(0, 300))
133
+ throw new Error('抖音没有返回这条作品的数据'
134
+ + (code !== undefined ? '(错误码 ' + String(code) + (message ? ':' + String(message) : '') + ')' : '')
135
+ + ',常见原因:Cookie 失效、被风控,或者这条链接已经失效 / 作品已删除。')
120
136
  }
121
137
  // 根据 API 返回的数据判断作品类型,而不是依赖 URL
122
138
  // aweme_type: 0=视频, 68=图集, 163=文章