koishi-plugin-kkk 3.4.2 → 3.6.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 +105 -0
- package/assets/web/assets/index-BRROK0vh.js +1 -1
- package/config/default_config/app.yaml +4 -6
- package/config/default_config/bilibili.yaml +1 -1
- package/config/default_config/config.json +2 -2
- package/lib/build-metadata.json +10 -0
- package/lib/compat/adapter-info.js +333 -0
- package/lib/compat/forward-collect.js +57 -4
- package/lib/compat/node-karin.js +25 -56
- package/lib/compat/sendError.js +27 -0
- package/lib/index.js +15 -8
- package/lib/karin/apps/help.js +23 -2
- package/lib/karin/module/utils/Base.js +30 -8
- package/lib/karin/module/utils/ErrorHandler/render.js +8 -2
- package/lib/karin/module/utils/InteractiveGraph.js +164 -0
- package/lib/karin/module/utils/InteractiveVideo.js +316 -0
- package/lib/karin/module/utils/Network/Downloader.js +159 -13
- package/lib/karin/module/utils/Network/Network.js +5 -1
- package/lib/karin/module/utils/ParseForward.js +12 -2
- package/lib/karin/module/utils/ParseSteps.js +1 -1
- package/lib/karin/module/utils/Render/index.js +10 -110
- package/lib/karin/module/utils/build-metadata.js +64 -19
- package/lib/karin/module/utils/runtime-report.js +16 -4
- package/lib/karin/platform/bilibili/bilibili.js +137 -11
- package/lib/karin/platform/bilibili/interactive-story.js +463 -0
- package/lib/karin/platform/bilibili/push.js +12 -5
- package/lib/ktr/registry.js +1 -0
- package/lib/ktr/template/bilibili/interactive/components/interactive.js +137 -0
- package/lib/ktr/template/bilibili/interactive/components/types.js +3 -0
- package/lib/ktr/template/bilibili/interactive/index.js +13 -0
- package/lib/ktr/template/other/handlerError/components/handlerError.js +1 -1
- package/lib/webui.js +40 -18
- package/package.json +1 -1
- package/scripts/build.mjs +36 -0
- package/scripts/patch-webui.mjs +126 -40
- package/scripts/probe-edge-cookie.cjs +50 -0
- package/scripts/probe-panel-options.cjs +15 -0
- package/scripts/probe-schema-options.cjs +12 -0
- package/scripts/smoke-adapter-info.cjs +149 -0
- package/scripts/smoke-config-schema.cjs +62 -0
- package/scripts/smoke-download-source-fallback.cjs +166 -0
- package/scripts/smoke-forward-chart.cjs +139 -0
- package/scripts/smoke-interactive-card.cjs +87 -0
- package/scripts/smoke-interactive-story.cjs +285 -0
- package/scripts/smoke-interactive.cjs +298 -0
- package/scripts/smoke-onebot-forward.cjs +10 -5
- package/scripts/smoke-panel-chart.cjs +92 -0
- package/scripts/smoke-parse-forward.cjs +1 -1
- package/scripts/smoke-render-path.cjs +44 -55
- package/src/compat/adapter-info.ts +346 -0
- package/src/compat/forward-collect.ts +69 -4
- package/src/compat/node-karin.ts +24 -54
- package/src/compat/sendError.ts +26 -0
- package/src/index.ts +15 -8
- package/src/karin/apps/help.ts +26 -3
- package/src/karin/module/utils/Base.ts +39 -9
- package/src/karin/module/utils/ErrorHandler/render.ts +8 -2
- package/src/karin/module/utils/InteractiveGraph.ts +199 -0
- package/src/karin/module/utils/InteractiveVideo.ts +400 -0
- package/src/karin/module/utils/Network/Downloader.ts +164 -14
- package/src/karin/module/utils/Network/Network.ts +6 -2
- package/src/karin/module/utils/ParseForward.ts +12 -3
- package/src/karin/module/utils/ParseSteps.ts +1 -1
- package/src/karin/module/utils/Render/index.ts +10 -113
- package/src/karin/module/utils/build-metadata.ts +61 -19
- package/src/karin/module/utils/runtime-report.ts +16 -4
- package/src/karin/platform/bilibili/bilibili.ts +149 -20
- package/src/karin/platform/bilibili/interactive-story.ts +519 -0
- package/src/karin/platform/bilibili/push.ts +12 -6
- package/src/karin/types/config/app.ts +7 -15
- package/src/karin/types/config/bilibili.ts +1 -1
- package/src/karin/types/netrorks/NetworksConfigType.ts +5 -0
- package/src/ktr/registry.ts +1 -0
- package/src/ktr/template/bilibili/interactive/components/interactive.tsx +202 -0
- package/src/ktr/template/bilibili/interactive/components/types.ts +37 -0
- package/src/ktr/template/bilibili/interactive/index.tsx +15 -0
- package/src/ktr/template/other/handlerError/components/handlerError.tsx +4 -1
- package/src/webui.ts +38 -16
- package/scripts/smoke-shotkit-compat.cjs +0 -102
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
"videoTool": true,
|
|
27
27
|
"priority": 800,
|
|
28
28
|
"removeCache": true,
|
|
29
|
-
"renderer": "shotkit",
|
|
30
29
|
"renderScale": 100,
|
|
31
30
|
"noiseOverlay": "off",
|
|
32
31
|
"forwardContent": [
|
|
@@ -114,7 +113,8 @@
|
|
|
114
113
|
"sendContent": [
|
|
115
114
|
"info",
|
|
116
115
|
"video",
|
|
117
|
-
"comment"
|
|
116
|
+
"comment",
|
|
117
|
+
"chart"
|
|
118
118
|
],
|
|
119
119
|
"numcomment": 5,
|
|
120
120
|
"realCommentCount": false,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "3.6.0",
|
|
3
|
+
"buildTime": "2026-09-25T02:51:07.813Z",
|
|
4
|
+
"buildTimestamp": 1790304667814,
|
|
5
|
+
"name": "koishi-plugin-kkk",
|
|
6
|
+
"description": "抖音 / B站 / 快手 / 小红书 视频解析与动态推送,带 QQ 交互面板与弹幕烧录(karin-plugin-kkk 的 Koishi 移植版)",
|
|
7
|
+
"homepage": "https://github.com/maimai993/koishi-plugin-kkk#readme",
|
|
8
|
+
"commitHash": "",
|
|
9
|
+
"shortCommitHash": ""
|
|
10
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveAdapterInfo = exports.queryAdapterImplementation = exports.cachedAdapterImplementation = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* 适配器信息(错误卡片与「#kkk版本」海报上的「适配器」一栏)。
|
|
9
|
+
*
|
|
10
|
+
* Koishi 的 Bot 只暴露 platform / adapterName,**没有版本号字段** —— 早期实现直接读
|
|
11
|
+
* bot.adapter.version,结果卡片上永远是一个空的「v」,也看不出到底用的哪个适配器。
|
|
12
|
+
* 这里分三步把真实信息挖出来:
|
|
13
|
+
*
|
|
14
|
+
* 1. 精准:bot.adapter 是 Adapter 实例,它的构造函数就定义在适配器插件模块里,
|
|
15
|
+
* 拿构造函数去 require.cache 里反查模块路径,再读那个包的 package.json 版本;
|
|
16
|
+
* 2. 兜底:按「平台名 + 常见包名前缀」在宿主 node_modules 里找适配器包
|
|
17
|
+
* (社区版 koishi-plugin-adapter-x,官方版 @koishijs/plugin-adapter-x);
|
|
18
|
+
* 3. 可选异步:OneBot 适配器可以问一句 get_version_info,拿到 NapCat / Lagrange
|
|
19
|
+
* 这类**实现端**的名字和版本号 —— 那才是用户真正装的软件版本。
|
|
20
|
+
*
|
|
21
|
+
* 全部结果都缓存,卡片渲染路径上不会重复扫目录、也不会发网络请求。
|
|
22
|
+
*/
|
|
23
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
24
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
25
|
+
/** 平台名 → 展示名 / 协议。表里没有的平台就把平台名原样当展示名 */
|
|
26
|
+
const PLATFORM_META = {
|
|
27
|
+
onebot: { name: 'OneBot', protocol: 'OneBot 11' },
|
|
28
|
+
qq: { name: 'QQ 机器人', protocol: 'QQ 开放平台' },
|
|
29
|
+
qqguild: { name: 'QQ 频道', protocol: 'QQ 频道' },
|
|
30
|
+
qqbot: { name: 'QQ 机器人', protocol: 'QQ 开放平台' },
|
|
31
|
+
bilibili: { name: '哔哩哔哩私信', protocol: 'Bilibili 私信' },
|
|
32
|
+
discord: { name: 'Discord', protocol: 'Discord' },
|
|
33
|
+
telegram: { name: 'Telegram', protocol: 'Telegram' },
|
|
34
|
+
kook: { name: 'KOOK', protocol: 'KOOK' },
|
|
35
|
+
lark: { name: '飞书', protocol: 'Lark' },
|
|
36
|
+
satori: { name: 'Satori', protocol: 'Satori' },
|
|
37
|
+
milky: { name: 'Milky', protocol: 'Milky' },
|
|
38
|
+
nextchat: { name: 'NextChat', protocol: 'NextChat' },
|
|
39
|
+
github: { name: 'GitHub', protocol: 'GitHub' }
|
|
40
|
+
};
|
|
41
|
+
/** 包名里出现这些关键字时,展示名以包名为准(同一个 qq 平台有好几个适配器) */
|
|
42
|
+
const PACKAGE_NAME_HINTS = [
|
|
43
|
+
{ key: 'adapter-qq-crack', name: 'QQ(Crack)' },
|
|
44
|
+
{ key: 'adapter-onebot', name: 'OneBot' },
|
|
45
|
+
{ key: 'adapter-bilibili-dm', name: '哔哩哔哩私信' },
|
|
46
|
+
{ key: 'adapter-qq', name: 'QQ 机器人' },
|
|
47
|
+
{ key: 'adapter-satori', name: 'Satori' }
|
|
48
|
+
];
|
|
49
|
+
const PACKAGE_PREFIXES = [
|
|
50
|
+
'koishi-plugin-adapter-',
|
|
51
|
+
'@koishijs/plugin-adapter-',
|
|
52
|
+
'koishi-plugin-',
|
|
53
|
+
'@koishijs/plugin-'
|
|
54
|
+
];
|
|
55
|
+
const COMMUNICATION_NAMES = {
|
|
56
|
+
ws: 'WebSocket',
|
|
57
|
+
websocket: 'WebSocket',
|
|
58
|
+
'ws-reverse': 'WebSocket 反向',
|
|
59
|
+
wsreverse: 'WebSocket 反向',
|
|
60
|
+
http: 'HTTP',
|
|
61
|
+
'http-reverse': 'HTTP 反向',
|
|
62
|
+
httpreverse: 'HTTP 反向',
|
|
63
|
+
sse: 'SSE',
|
|
64
|
+
webhook: 'Webhook'
|
|
65
|
+
};
|
|
66
|
+
/** 包版本缓存:key 是平台名 + adapterName */
|
|
67
|
+
const PACKAGE_CACHE = new Map();
|
|
68
|
+
/** 实现端版本缓存:key 是平台名 + selfId,值是 null 表示问过了但没问到 */
|
|
69
|
+
const IMPLEMENTATION_CACHE = new Map();
|
|
70
|
+
/** 机器人上线时间:Koishi 没有这个字段,第一次看到它在线时记一笔 */
|
|
71
|
+
const ONLINE_SINCE = new Map();
|
|
72
|
+
const botKey = (bot) => String(bot?.platform ?? bot?.adapterName ?? '') + ':' + String(bot?.selfId ?? '');
|
|
73
|
+
const readPackage = (dir) => {
|
|
74
|
+
try {
|
|
75
|
+
const file = node_path_1.default.join(dir, 'package.json');
|
|
76
|
+
if (!node_fs_1.default.existsSync(file))
|
|
77
|
+
return null;
|
|
78
|
+
return JSON.parse(node_fs_1.default.readFileSync(file, 'utf-8'));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
/** 从模块文件往上找最近的 package.json 目录,且必须在 node_modules 里(别爬到宿主项目自己的包) */
|
|
85
|
+
const packageDirOf = (file) => {
|
|
86
|
+
try {
|
|
87
|
+
let dir = node_path_1.default.dirname(file);
|
|
88
|
+
for (let i = 0; i < 8; i++) {
|
|
89
|
+
const isModule = dir.split(/[\\/]/).includes('node_modules');
|
|
90
|
+
if (isModule && node_fs_1.default.existsSync(node_path_1.default.join(dir, 'package.json')))
|
|
91
|
+
return dir;
|
|
92
|
+
const parent = node_path_1.default.dirname(dir);
|
|
93
|
+
if (parent === dir)
|
|
94
|
+
break;
|
|
95
|
+
dir = parent;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch { /* 取不到就算了 */ }
|
|
99
|
+
return null;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* 用 Adapter 构造函数反查适配器插件模块。
|
|
103
|
+
*
|
|
104
|
+
* 先只扫路径里带 adapter 的模块(覆盖 99% 的情况),扫不到再退化成全表扫描。
|
|
105
|
+
*/
|
|
106
|
+
const findAdapterModule = (ctor) => {
|
|
107
|
+
if (typeof ctor !== 'function')
|
|
108
|
+
return null;
|
|
109
|
+
let ids = [];
|
|
110
|
+
try {
|
|
111
|
+
ids = Object.keys(require.cache);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
for (const pass of [0, 1]) {
|
|
117
|
+
for (const id of ids) {
|
|
118
|
+
if (pass === 0 && !/adapter/i.test(id))
|
|
119
|
+
continue;
|
|
120
|
+
let exportsOf;
|
|
121
|
+
try {
|
|
122
|
+
exportsOf = require.cache[id]?.exports;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!exportsOf)
|
|
128
|
+
continue;
|
|
129
|
+
if (exportsOf === ctor)
|
|
130
|
+
return id;
|
|
131
|
+
let values = [];
|
|
132
|
+
try {
|
|
133
|
+
values = Object.values(exportsOf);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (values.length > 200)
|
|
139
|
+
continue;
|
|
140
|
+
if (values.includes(ctor))
|
|
141
|
+
return id;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* 收集可能装着适配器插件的 node_modules 目录。
|
|
148
|
+
*
|
|
149
|
+
* 三个来源:插件自己往上找的各级 node_modules、require.cache 里真实加载过的
|
|
150
|
+
* node_modules(pnpm / 全局安装也逃不掉)、以及进程工作目录。
|
|
151
|
+
*/
|
|
152
|
+
const moduleRoots = () => {
|
|
153
|
+
const roots = [];
|
|
154
|
+
try {
|
|
155
|
+
let dir = __dirname;
|
|
156
|
+
for (let i = 0; i < 8; i++) {
|
|
157
|
+
roots.push(node_path_1.default.join(dir, 'node_modules'));
|
|
158
|
+
const parent = node_path_1.default.dirname(dir);
|
|
159
|
+
if (parent === dir)
|
|
160
|
+
break;
|
|
161
|
+
dir = parent;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch { /* 忽略 */ }
|
|
165
|
+
try {
|
|
166
|
+
for (const id of Object.keys(require.cache)) {
|
|
167
|
+
const marker = id.toLowerCase().lastIndexOf('node_modules');
|
|
168
|
+
if (marker < 0)
|
|
169
|
+
continue;
|
|
170
|
+
roots.push(id.slice(0, marker + 'node_modules'.length));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch { /* 忽略 */ }
|
|
174
|
+
try {
|
|
175
|
+
roots.push(node_path_1.default.join(process.cwd(), 'node_modules'));
|
|
176
|
+
}
|
|
177
|
+
catch { /* 忽略 */ }
|
|
178
|
+
const unique = [];
|
|
179
|
+
for (const root of roots)
|
|
180
|
+
if (!unique.includes(root))
|
|
181
|
+
unique.push(root);
|
|
182
|
+
return unique;
|
|
183
|
+
};
|
|
184
|
+
/** 平台名 / adapterName → 候选包名 */
|
|
185
|
+
const packageCandidates = (tokens) => {
|
|
186
|
+
const out = [];
|
|
187
|
+
for (const token of tokens) {
|
|
188
|
+
const name = String(token ?? '').trim();
|
|
189
|
+
if (!name)
|
|
190
|
+
continue;
|
|
191
|
+
if (name.startsWith('@') || name.includes('koishi-'))
|
|
192
|
+
out.push(name);
|
|
193
|
+
const bare = name.replace(/^@[^/]+\//, '');
|
|
194
|
+
for (const prefix of PACKAGE_PREFIXES)
|
|
195
|
+
out.push(prefix + bare);
|
|
196
|
+
}
|
|
197
|
+
return Array.from(new Set(out));
|
|
198
|
+
};
|
|
199
|
+
/** 按候选包名去 node_modules 里翻版本号 */
|
|
200
|
+
const lookupPackage = (tokens) => {
|
|
201
|
+
const key = tokens.join('|');
|
|
202
|
+
const cached = PACKAGE_CACHE.get(key);
|
|
203
|
+
if (cached)
|
|
204
|
+
return cached;
|
|
205
|
+
const empty = { packageName: '', version: '' };
|
|
206
|
+
const roots = moduleRoots();
|
|
207
|
+
for (const candidate of packageCandidates(tokens)) {
|
|
208
|
+
for (const root of roots) {
|
|
209
|
+
const pkg = readPackage(node_path_1.default.join(root, ...candidate.split('/')));
|
|
210
|
+
const version = String(pkg?.version ?? '');
|
|
211
|
+
if (!version)
|
|
212
|
+
continue;
|
|
213
|
+
const hit = { packageName: String(pkg.name ?? candidate), version };
|
|
214
|
+
PACKAGE_CACHE.set(key, hit);
|
|
215
|
+
return hit;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
PACKAGE_CACHE.set(key, empty);
|
|
219
|
+
return empty;
|
|
220
|
+
};
|
|
221
|
+
/** 适配器插件包名与版本(缓存,扫不到就返回空) */
|
|
222
|
+
const resolvePackage = (bot) => {
|
|
223
|
+
const key = botKey(bot);
|
|
224
|
+
const cached = PACKAGE_CACHE.get(key);
|
|
225
|
+
if (cached)
|
|
226
|
+
return cached;
|
|
227
|
+
const file = findAdapterModule(bot?.adapter?.constructor);
|
|
228
|
+
const dir = file ? packageDirOf(file) : null;
|
|
229
|
+
const pkg = dir ? readPackage(dir) : null;
|
|
230
|
+
if (pkg?.version) {
|
|
231
|
+
const hit = { packageName: String(pkg.name ?? ''), version: String(pkg.version) };
|
|
232
|
+
PACKAGE_CACHE.set(key, hit);
|
|
233
|
+
return hit;
|
|
234
|
+
}
|
|
235
|
+
const fallback = lookupPackage([bot?.platform, bot?.adapterName]);
|
|
236
|
+
PACKAGE_CACHE.set(key, fallback);
|
|
237
|
+
return fallback;
|
|
238
|
+
};
|
|
239
|
+
/** 通信方式:读适配器配置里的 protocol,翻成好认的名字 */
|
|
240
|
+
const communicationOf = (bot) => {
|
|
241
|
+
const config = bot?.config ?? {};
|
|
242
|
+
const raw = String(config.protocol ?? config.connection ?? config.mode ?? '').toLowerCase();
|
|
243
|
+
if (!raw)
|
|
244
|
+
return '未知';
|
|
245
|
+
return COMMUNICATION_NAMES[raw] ?? raw;
|
|
246
|
+
};
|
|
247
|
+
/** 上线时间:Koishi 不提供,第一次读到「在线」时记一笔,之后一直用它 */
|
|
248
|
+
const onlineSince = (bot, force = false) => {
|
|
249
|
+
const key = botKey(bot);
|
|
250
|
+
const known = ONLINE_SINCE.get(key);
|
|
251
|
+
if (known && !force)
|
|
252
|
+
return known;
|
|
253
|
+
const status = bot?.status;
|
|
254
|
+
const online = status === 1 || status === 2 || status === 'online' || status === 'connect';
|
|
255
|
+
if (online && !known) {
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
ONLINE_SINCE.set(key, now);
|
|
258
|
+
return now;
|
|
259
|
+
}
|
|
260
|
+
return known ?? 0;
|
|
261
|
+
};
|
|
262
|
+
const displayNameOf = (platform, packageName) => {
|
|
263
|
+
const lower = packageName.toLowerCase();
|
|
264
|
+
for (const hint of PACKAGE_NAME_HINTS) {
|
|
265
|
+
if (lower.includes(hint.key))
|
|
266
|
+
return hint.name;
|
|
267
|
+
}
|
|
268
|
+
return PLATFORM_META[platform]?.name ?? (platform || '未知');
|
|
269
|
+
};
|
|
270
|
+
/** 已经问到的实现端信息(没问过就是 undefined,卡片渲染路径不会触发网络请求) */
|
|
271
|
+
const cachedAdapterImplementation = (bot) => IMPLEMENTATION_CACHE.get(botKey(bot)) ?? undefined;
|
|
272
|
+
exports.cachedAdapterImplementation = cachedAdapterImplementation;
|
|
273
|
+
/**
|
|
274
|
+
* 问适配器要实现端名字与版本(只有 OneBot 一整类支持 get_version_info)。
|
|
275
|
+
*
|
|
276
|
+
* 带 1.5 秒超时:适配器没实现这个接口、或者对面不回,都直接放弃,绝不让海报卡住。
|
|
277
|
+
*/
|
|
278
|
+
const queryAdapterImplementation = async (bot) => {
|
|
279
|
+
const key = botKey(bot);
|
|
280
|
+
if (IMPLEMENTATION_CACHE.has(key))
|
|
281
|
+
return IMPLEMENTATION_CACHE.get(key) ?? null;
|
|
282
|
+
const result = await (async () => {
|
|
283
|
+
const internal = bot?.internal;
|
|
284
|
+
if (!internal || typeof internal.get_version_info !== 'function')
|
|
285
|
+
return null;
|
|
286
|
+
try {
|
|
287
|
+
const timeout = new Promise((resolve) => {
|
|
288
|
+
const timer = setTimeout(() => resolve(null), 1500);
|
|
289
|
+
timer.unref?.();
|
|
290
|
+
});
|
|
291
|
+
const raw = await Promise.race([internal.get_version_info(), timeout]);
|
|
292
|
+
if (!raw || typeof raw !== 'object')
|
|
293
|
+
return null;
|
|
294
|
+
const name = String(raw.app_name ?? raw.appName ?? '').trim();
|
|
295
|
+
const version = String(raw.app_version ?? raw.appVersion ?? '').trim();
|
|
296
|
+
const protocol = String(raw.protocol_version ?? raw.protocolVersion ?? '').trim();
|
|
297
|
+
if (!name && !version)
|
|
298
|
+
return null;
|
|
299
|
+
return { name, version, protocol };
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
})();
|
|
305
|
+
IMPLEMENTATION_CACHE.set(key, result);
|
|
306
|
+
return result;
|
|
307
|
+
};
|
|
308
|
+
exports.queryAdapterImplementation = queryAdapterImplementation;
|
|
309
|
+
/**
|
|
310
|
+
* 同步采集适配器信息(错误卡片、上报用的都是这一份)。
|
|
311
|
+
*
|
|
312
|
+
* @param bot Koishi 的原生 Bot(不是兼容层的包装)
|
|
313
|
+
*/
|
|
314
|
+
const resolveAdapterInfo = (bot) => {
|
|
315
|
+
const platform = String(bot?.platform ?? bot?.adapterName ?? '').trim();
|
|
316
|
+
const pkg = resolvePackage(bot);
|
|
317
|
+
const implementation = (0, exports.cachedAdapterImplementation)(bot);
|
|
318
|
+
const version = implementation?.version || pkg.version || '未知';
|
|
319
|
+
return {
|
|
320
|
+
// 前四个字段是「身份」,一律保持 Koishi 的原值;只有下面这些是这次修好的展示信息
|
|
321
|
+
name: platform || '未知',
|
|
322
|
+
displayName: implementation?.name || displayNameOf(platform, pkg.packageName),
|
|
323
|
+
version,
|
|
324
|
+
platform: platform || '未知',
|
|
325
|
+
protocol: platform,
|
|
326
|
+
standard: platform,
|
|
327
|
+
communication: communicationOf(bot),
|
|
328
|
+
connectTime: onlineSince(bot),
|
|
329
|
+
packageName: pkg.packageName,
|
|
330
|
+
raw: bot?.adapter
|
|
331
|
+
};
|
|
332
|
+
};
|
|
333
|
+
exports.resolveAdapterInfo = resolveAdapterInfo;
|
|
@@ -5,6 +5,8 @@ exports.runWithForwardBag = runWithForwardBag;
|
|
|
5
5
|
exports.currentForwardBag = currentForwardBag;
|
|
6
6
|
exports.isForwardCollecting = isForwardCollecting;
|
|
7
7
|
exports.withoutForwardCollect = withoutForwardCollect;
|
|
8
|
+
exports.withForwardKind = withForwardKind;
|
|
9
|
+
exports.forwardKindOf = forwardKindOf;
|
|
8
10
|
exports.collectForward = collectForward;
|
|
9
11
|
exports.drainForward = drainForward;
|
|
10
12
|
exports.drainForwardGroups = drainForwardGroups;
|
|
@@ -23,18 +25,26 @@ exports.drainForwardGroups = drainForwardGroups;
|
|
|
23
25
|
* - **只收发往本次解析那个频道的消息**:错误日志要发给主人(另一个频道/另一个 bot),
|
|
24
26
|
* 那种不能被吞进触发者的转发里;
|
|
25
27
|
* - **过程提示不收**(用户要求:转发里不包含过程提示):调 `withoutForwardCollect()`
|
|
26
|
-
*
|
|
28
|
+
* 包一下那次发送即可,例如「收到请求,开始下载」「发送中…」「加载中…」;
|
|
29
|
+
* - **看不清类别的特殊内容**(互动视频的剧情流程图)用 `withForwardKind('chart', …)` 包一下,
|
|
30
|
+
* 它照常被收集,只是带着类别标签,交给 ParseForward 按「合并转发内容」里勾没勾来分流。
|
|
27
31
|
*
|
|
28
32
|
* 收集用的 `AsyncLocalStorage` 是**按解析链路**隔离的:定时推送、其它会话同时解析
|
|
29
33
|
* 都不会串味。
|
|
30
34
|
*/
|
|
31
35
|
const node_async_hooks_1 = require("node:async_hooks");
|
|
32
36
|
const storage = new node_async_hooks_1.AsyncLocalStorage();
|
|
37
|
+
/**
|
|
38
|
+
* 「这个元素属于哪一类内容」的标记表(见 {@link withForwardKind})。
|
|
39
|
+
*
|
|
40
|
+
* 用 WeakMap 挂在**原对象**上:元素还要原样交给适配器发送,不能往段对象上加字段。
|
|
41
|
+
*/
|
|
42
|
+
const KIND_TAGS = new WeakMap();
|
|
33
43
|
/** 被收集时给调用方回的假消息 ID:调用方普遍只判断「有没有拿到 ID」 */
|
|
34
44
|
exports.COLLECTED_MESSAGE_ID = 'forward-collected';
|
|
35
45
|
/** 在收集上下文里执行(peer 为空则不收任何东西) */
|
|
36
46
|
function runWithForwardBag(peer, fn) {
|
|
37
|
-
return storage.run({ peer: String(peer ?? ''), elements: [], groups: [], bypass: 0 }, fn);
|
|
47
|
+
return storage.run({ peer: String(peer ?? ''), elements: [], groups: [], bypass: 0, kind: null, closed: false }, fn);
|
|
38
48
|
}
|
|
39
49
|
/** 当前收集袋(不在解析链路里时是 undefined) */
|
|
40
50
|
function currentForwardBag() {
|
|
@@ -51,7 +61,7 @@ function currentForwardBag() {
|
|
|
51
61
|
*/
|
|
52
62
|
function isForwardCollecting() {
|
|
53
63
|
const bag = storage.getStore();
|
|
54
|
-
return !!bag && bag.bypass === 0;
|
|
64
|
+
return !!bag && !bag.closed && bag.bypass === 0;
|
|
55
65
|
}
|
|
56
66
|
/**
|
|
57
67
|
* 这段代码里的发送都不进转发(过程提示 / 兜底文本用)。
|
|
@@ -69,6 +79,38 @@ async function withoutForwardCollect(fn) {
|
|
|
69
79
|
bag.bypass -= 1;
|
|
70
80
|
}
|
|
71
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* 这一段发出去的东西标一个**内容类别**(例如互动视频的 `chart` 剧情流程图)。
|
|
84
|
+
*
|
|
85
|
+
* 为什么需要它:ParseForward 是按**段类型**判断内容的(img→图片、video→视频…),
|
|
86
|
+
* 而剧情流程图和普通图片都是 `img`,用户却想在「合并转发内容」里分别控制 ——
|
|
87
|
+
* 只好由发送方自己说清楚「这条是流程图」。
|
|
88
|
+
*
|
|
89
|
+
* 不在收集上下文里(或者袋子已经冲刷过了)时原样执行,
|
|
90
|
+
* 于是**不会影响**「合并转发关着」这条常见路径:那些场景下它就是个普通的发送。
|
|
91
|
+
*
|
|
92
|
+
* @param kind 内容类别(`chart`)
|
|
93
|
+
* @param fn 这一段里发出的内容都按这个类别记账
|
|
94
|
+
*/
|
|
95
|
+
async function withForwardKind(kind, fn) {
|
|
96
|
+
const bag = storage.getStore();
|
|
97
|
+
if (!bag || bag.closed || bag.bypass > 0)
|
|
98
|
+
return await fn();
|
|
99
|
+
const previous = bag.kind;
|
|
100
|
+
bag.kind = kind;
|
|
101
|
+
try {
|
|
102
|
+
return await fn();
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
bag.kind = previous;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** 取某个元素被标记的内容类别(没标记就是 undefined,由 ParseForward 按段类型判断) */
|
|
109
|
+
function forwardKindOf(element) {
|
|
110
|
+
if (!element || typeof element !== 'object')
|
|
111
|
+
return undefined;
|
|
112
|
+
return KIND_TAGS.get(element);
|
|
113
|
+
}
|
|
72
114
|
/**
|
|
73
115
|
* 发送漏斗。
|
|
74
116
|
* @param peer 这条消息要发到哪个频道
|
|
@@ -77,7 +119,7 @@ async function withoutForwardCollect(fn) {
|
|
|
77
119
|
*/
|
|
78
120
|
function collectForward(peer, content) {
|
|
79
121
|
const bag = storage.getStore();
|
|
80
|
-
if (!bag || bag.bypass > 0)
|
|
122
|
+
if (!bag || bag.bypass > 0 || bag.closed)
|
|
81
123
|
return false;
|
|
82
124
|
const target = String(peer ?? '');
|
|
83
125
|
if (!bag.peer || !target || target !== bag.peer)
|
|
@@ -88,6 +130,9 @@ function collectForward(peer, content) {
|
|
|
88
130
|
for (const element of list) {
|
|
89
131
|
if (element === undefined || element === null || element === '')
|
|
90
132
|
continue;
|
|
133
|
+
/** 这一段带类别标记(见 withForwardKind)时记在元素上,供 ParseForward 分流 */
|
|
134
|
+
if (bag.kind && element && typeof element === 'object')
|
|
135
|
+
KIND_TAGS.set(element, bag.kind);
|
|
91
136
|
bag.elements.push(element);
|
|
92
137
|
group.push(element);
|
|
93
138
|
}
|
|
@@ -103,6 +148,8 @@ function drainForward() {
|
|
|
103
148
|
const elements = bag.elements;
|
|
104
149
|
bag.elements = [];
|
|
105
150
|
bag.groups = [];
|
|
151
|
+
/** 取走 = 这次解析结束了:之后再发的东西一律直发,不许再进这个袋子 */
|
|
152
|
+
bag.closed = true;
|
|
106
153
|
return elements;
|
|
107
154
|
}
|
|
108
155
|
/**
|
|
@@ -117,5 +164,11 @@ function drainForwardGroups() {
|
|
|
117
164
|
const groups = bag.groups;
|
|
118
165
|
bag.groups = [];
|
|
119
166
|
bag.elements = [];
|
|
167
|
+
/**
|
|
168
|
+
* **取走就关袋子**:解析到这里已经结束,转发马上要发出去了。
|
|
169
|
+
* 剧情图这类后台任务还在跑,之后发出来的内容必须直发 ——
|
|
170
|
+
* 否则会被收进一个再也没人 drain 的袋子,用户什么都收不到(静默丢内容)。
|
|
171
|
+
*/
|
|
172
|
+
bag.closed = true;
|
|
120
173
|
return groups;
|
|
121
174
|
}
|
package/lib/compat/node-karin.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.syncUpstreamToKoishi = exports.segment = exports.logger = exports.withoutForwardCollect = exports.runWithForwardBag = exports.isForwardCollecting = exports.drainForwardGroups = exports.drainForward = exports.currentForwardBag = exports.collectForward = exports.COLLECTED_MESSAGE_ID = exports.karin = exports.app = exports.isFfmpegAvailable = exports.ffprobe = exports.ffmpeg = exports.createServerErrorResponse = exports.createNotFoundResponse = exports.createBadRequestResponse = exports.createSuccessResponse = exports.authMiddleware = exports.defineConfig = exports.checkPort = exports.isDocker = exports.mkdirSync = exports.updatePkg = exports.checkPkgUpdate = exports.restart = exports.watch = exports.hooks = exports.config = exports.common = exports.render = exports.db = exports.contactFriend = exports.contactGroup = exports.ForwardPayload = exports.Message = exports.KkkBot = exports.NEXT = exports.BOT_READY = exports.BOT_DISCONNECT = exports.BOT_CONNECT = exports.getKoishiContext = void 0;
|
|
6
|
+
exports.syncUpstreamToKoishi = exports.segment = exports.logger = exports.withoutForwardCollect = exports.withForwardKind = exports.runWithForwardBag = exports.isForwardCollecting = exports.forwardKindOf = exports.drainForwardGroups = exports.drainForward = exports.currentForwardBag = exports.collectForward = exports.COLLECTED_MESSAGE_ID = exports.karin = exports.app = exports.isFfmpegAvailable = exports.ffprobe = exports.ffmpeg = exports.createServerErrorResponse = exports.createNotFoundResponse = exports.createBadRequestResponse = exports.createSuccessResponse = exports.authMiddleware = exports.defineConfig = exports.checkPort = exports.isDocker = exports.mkdirSync = exports.updatePkg = exports.checkPkgUpdate = exports.restart = exports.watch = exports.hooks = exports.config = exports.common = exports.render = exports.db = exports.contactFriend = exports.contactGroup = exports.ForwardPayload = exports.Message = exports.KkkBot = exports.NEXT = exports.BOT_READY = exports.BOT_DISCONNECT = exports.BOT_CONNECT = exports.getKoishiContext = void 0;
|
|
7
7
|
exports.makeForward = makeForward;
|
|
8
8
|
exports.isForwardSupported = isForwardSupported;
|
|
9
9
|
exports.logs = logs;
|
|
@@ -30,6 +30,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
30
30
|
const koishi_1 = require("koishi");
|
|
31
31
|
// 版本比较复用注入器那边的实现(它处理了 -beta.1 这类预发布号的先后)
|
|
32
32
|
const semver_1 = require("../karin/module/utils/semver");
|
|
33
|
+
const adapter_info_1 = require("./adapter-info");
|
|
33
34
|
const logger_1 = require("./logger");
|
|
34
35
|
Object.defineProperty(exports, "logger", { enumerable: true, get: function () { return logger_1.logger; } });
|
|
35
36
|
const forward_collect_1 = require("./forward-collect");
|
|
@@ -85,20 +86,15 @@ class KkkBot {
|
|
|
85
86
|
avatar: this.bot.user?.avatar
|
|
86
87
|
};
|
|
87
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* 适配器信息(卡片上那一栏)。
|
|
91
|
+
*
|
|
92
|
+
* 以前这里直接读 `bot.adapter.version` —— 但 Koishi 的 Adapter 实例上根本没有版本号,
|
|
93
|
+
* 于是错误卡片和「#kkk版本」海报上永远是一个空的「v」。现在交给 compat/adapter-info:
|
|
94
|
+
* 按 Adapter 构造函数反查适配器插件、读它的 package.json,必要时再问一次实现端版本。
|
|
95
|
+
*/
|
|
88
96
|
get adapter() {
|
|
89
|
-
|
|
90
|
-
const raw = this.bot.adapter || {};
|
|
91
|
-
const cfg = raw.config ?? {};
|
|
92
|
-
return {
|
|
93
|
-
name: platform,
|
|
94
|
-
protocol: platform,
|
|
95
|
-
standard: platform,
|
|
96
|
-
// 错误卡片模板还会读这三项,之前没给 → 卡片上显示 undefined 和一个空的「v」
|
|
97
|
-
platform: String(raw.platform ?? cfg.platform ?? (platform || '未知')),
|
|
98
|
-
communication: String(raw.communication ?? cfg.communication ?? cfg.protocol ?? '未知'),
|
|
99
|
-
version: String(raw.version ?? cfg.version ?? ''),
|
|
100
|
-
raw: this.bot.adapter
|
|
101
|
-
};
|
|
97
|
+
return (0, adapter_info_1.resolveAdapterInfo)(this.bot);
|
|
102
98
|
}
|
|
103
99
|
get ctx() {
|
|
104
100
|
return this.bot.ctx;
|
|
@@ -551,8 +547,9 @@ class Message {
|
|
|
551
547
|
const failure = (0, sendError_1.classifySendFailure)(error);
|
|
552
548
|
// 只有「被动回复额度/时间窗超了」才值得换通道重发;体积超限、被禁言、无权限这类
|
|
553
549
|
// 换通道也一样失败,直接抛给调用方(它拿到错误码会去切片 / 降级 / 报错)
|
|
550
|
+
// 空 message 的异常(qq-chat 编码器那种)在这里补一段可读说明,别让错误卡片上一片空白
|
|
554
551
|
if (!(0, sendError_1.isPassiveLimitFailure)(failure))
|
|
555
|
-
throw error;
|
|
552
|
+
throw (0, sendError_1.decorateSendError)(error, failure);
|
|
556
553
|
// 用 mark 级别:这是「视频明明下好了却发不出去」的关键兜底,日志里要看得见
|
|
557
554
|
logger_1.logger.mark('[compat] 被动回复受限,改用主动消息发送: ' + (0, sendError_1.describeSendFailure)(failure));
|
|
558
555
|
return await sendActive();
|
|
@@ -825,52 +822,20 @@ exports.db = {
|
|
|
825
822
|
exports.render = {
|
|
826
823
|
/**
|
|
827
824
|
* karin 的 render.render:给 HTML 文件截图。
|
|
828
|
-
*
|
|
825
|
+
*
|
|
826
|
+
* **只用浏览器渲染服务**(koishi-plugin-puppeteer / puppeteer-without-canvas 之类)。
|
|
827
|
+
* 3.5.0 起不再支持 shotkit 内核:它在 Windows 上加载不了 https 资源,卡片里的远程封面、
|
|
828
|
+
* 头像一律是空白框 —— 用户看到的是「卡片坏了」,快那一点不值得。
|
|
829
829
|
*/
|
|
830
830
|
async render(options) {
|
|
831
831
|
const runtime = (0, runtime_1.getRuntime)();
|
|
832
832
|
const puppeteer = runtime.ctx.puppeteer;
|
|
833
833
|
/**
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
* 和卡片主渲染走同一个开关:选了谁谁先上,没装或渲染失败就落到另一个。
|
|
837
|
-
* 这里用延迟 require 读配置:Config 依赖本文件(node-karin 兼容层),
|
|
838
|
-
* 顶部 import 会和本文件形成循环依赖,放到调用时读最稳。
|
|
834
|
+
* 渲染只走浏览器这一条路(3.5.0 起不再支持 shotkit 内核,见 Render/index.ts 的说明)。
|
|
835
|
+
* 以前这里会按配置项 app.renderer 在「内核 / 浏览器」之间挑一个,现在没得挑了。
|
|
839
836
|
*/
|
|
840
|
-
let preferShotkit = true;
|
|
841
|
-
try {
|
|
842
|
-
const { Config } = require('../karin/module/utils/Config');
|
|
843
|
-
preferShotkit = String(Config?.app?.renderer ?? 'shotkit').toLowerCase() !== 'puppeteer';
|
|
844
|
-
}
|
|
845
|
-
catch { /* 读不到配置就按默认值 shotkit */ }
|
|
846
|
-
const useShotkit = preferShotkit || !puppeteer;
|
|
847
|
-
const shotkit = useShotkit
|
|
848
|
-
? (typeof runtime.ctx.get === 'function'
|
|
849
|
-
? runtime.ctx.get('shotkit')
|
|
850
|
-
: runtime.ctx.shotkit)
|
|
851
|
-
: undefined;
|
|
852
|
-
if (shotkit && typeof shotkit.renderFile === 'function') {
|
|
853
|
-
try {
|
|
854
|
-
const request = {
|
|
855
|
-
// fullPage 和 selector 互斥:要整页就别给选择器
|
|
856
|
-
fullPage: options.fullPage ?? false,
|
|
857
|
-
omitBackground: options.omitBackground ?? true,
|
|
858
|
-
type: options.type ?? 'png',
|
|
859
|
-
scale: 2,
|
|
860
|
-
pageGotoParams: options.pageGotoParams
|
|
861
|
-
};
|
|
862
|
-
if (!request.fullPage)
|
|
863
|
-
request.selector = options.selector ?? '#container';
|
|
864
|
-
const buffer = await shotkit.renderFile(options.file, request);
|
|
865
|
-
if (buffer && buffer.length)
|
|
866
|
-
return buffer.toString('base64');
|
|
867
|
-
}
|
|
868
|
-
catch (error) {
|
|
869
|
-
logger_1.logger.debug('[kkk] shotkit 渲染失败,回退到 puppeteer:' + String(error?.message ?? error));
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
837
|
if (!puppeteer)
|
|
873
|
-
throw new Error('[kkk]
|
|
838
|
+
throw new Error('[kkk] 渲染失败:没有可用的浏览器渲染服务(需要 koishi-plugin-puppeteer 或同类插件)');
|
|
874
839
|
const screenshotOptions = {
|
|
875
840
|
file: options.file,
|
|
876
841
|
selector: options.selector ?? '#container',
|
|
@@ -1645,8 +1610,10 @@ exports.karin = {
|
|
|
1645
1610
|
/**
|
|
1646
1611
|
* 解析结果合并转发用的收集器(见 compat/forward-collect)。
|
|
1647
1612
|
*
|
|
1648
|
-
*
|
|
1649
|
-
*
|
|
1613
|
+
* 业务侧一般只用得到两个:
|
|
1614
|
+
* - `withoutForwardCollect`:把「过程提示」那次发送包起来,让它不要被收进最终那条转发里;
|
|
1615
|
+
* - `withForwardKind`:段类型看不出类别的内容(互动视频剧情流程图)标一个类别,
|
|
1616
|
+
* 交给 ParseForward 按「合并转发内容」分流。
|
|
1650
1617
|
*/
|
|
1651
1618
|
var forward_collect_2 = require("./forward-collect");
|
|
1652
1619
|
Object.defineProperty(exports, "COLLECTED_MESSAGE_ID", { enumerable: true, get: function () { return forward_collect_2.COLLECTED_MESSAGE_ID; } });
|
|
@@ -1654,7 +1621,9 @@ Object.defineProperty(exports, "collectForward", { enumerable: true, get: functi
|
|
|
1654
1621
|
Object.defineProperty(exports, "currentForwardBag", { enumerable: true, get: function () { return forward_collect_2.currentForwardBag; } });
|
|
1655
1622
|
Object.defineProperty(exports, "drainForward", { enumerable: true, get: function () { return forward_collect_2.drainForward; } });
|
|
1656
1623
|
Object.defineProperty(exports, "drainForwardGroups", { enumerable: true, get: function () { return forward_collect_2.drainForwardGroups; } });
|
|
1624
|
+
Object.defineProperty(exports, "forwardKindOf", { enumerable: true, get: function () { return forward_collect_2.forwardKindOf; } });
|
|
1657
1625
|
Object.defineProperty(exports, "isForwardCollecting", { enumerable: true, get: function () { return forward_collect_2.isForwardCollecting; } });
|
|
1658
1626
|
Object.defineProperty(exports, "runWithForwardBag", { enumerable: true, get: function () { return forward_collect_2.runWithForwardBag; } });
|
|
1627
|
+
Object.defineProperty(exports, "withForwardKind", { enumerable: true, get: function () { return forward_collect_2.withForwardKind; } });
|
|
1659
1628
|
Object.defineProperty(exports, "withoutForwardCollect", { enumerable: true, get: function () { return forward_collect_2.withoutForwardCollect; } });
|
|
1660
1629
|
exports.default = exports.karin;
|
package/lib/compat/sendError.js
CHANGED
|
@@ -30,6 +30,7 @@ exports.UnconfirmedSendError = void 0;
|
|
|
30
30
|
exports.classifySendFailure = classifySendFailure;
|
|
31
31
|
exports.failureFromReplyResult = failureFromReplyResult;
|
|
32
32
|
exports.describeSendFailure = describeSendFailure;
|
|
33
|
+
exports.decorateSendError = decorateSendError;
|
|
33
34
|
exports.isPassiveLimitFailure = isPassiveLimitFailure;
|
|
34
35
|
/**
|
|
35
36
|
* 错误码表。
|
|
@@ -200,6 +201,32 @@ function describeSendFailure(failure) {
|
|
|
200
201
|
const code = failure.code !== undefined && !hasCode ? '[' + failure.code + '] ' : '';
|
|
201
202
|
return code + (text || failure.kind);
|
|
202
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* 给「完全没有说明」的发送异常补一段人话。
|
|
206
|
+
*
|
|
207
|
+
* qq-chat 的编码器有时抛一个 message 为空的 Error(栈里只有 `_QQMessageEncoder.send`),
|
|
208
|
+
* 这种异常一路冒到错误卡片上就是一片空白 —— 用户看不出发生了什么,我们也无从排查。
|
|
209
|
+
* 有 message 的错误原样返回(错误码就在里面,调用方还要用);空 message 的才包一层,
|
|
210
|
+
* 把异常类型、判错结论和栈顶帧拼进 message,原异常挂在 cause 上(判错逻辑会继续往里挖)。
|
|
211
|
+
*
|
|
212
|
+
* @param error 适配器抛出的原始异常
|
|
213
|
+
* @param failure 已经判好的失败分类
|
|
214
|
+
*/
|
|
215
|
+
function decorateSendError(error, failure) {
|
|
216
|
+
if (String(error?.message ?? '').trim())
|
|
217
|
+
return error;
|
|
218
|
+
const name = String(error?.name || error?.constructor?.name || 'Error');
|
|
219
|
+
const frame = String(error?.stack ?? '').split('\n').slice(1, 3).join(' ').trim();
|
|
220
|
+
const reason = describeSendFailure(failure);
|
|
221
|
+
const message = '消息发送失败(' + name + '):'
|
|
222
|
+
+ (reason && reason !== failure.kind ? reason : '适配器没有给出原因')
|
|
223
|
+
+ (frame ? '|' + frame.slice(0, 200) : '');
|
|
224
|
+
const wrapped = new Error(message);
|
|
225
|
+
wrapped.name = 'SendFailedError';
|
|
226
|
+
wrapped.cause = error;
|
|
227
|
+
wrapped.sendFailureKind = failure.kind;
|
|
228
|
+
return wrapped;
|
|
229
|
+
}
|
|
203
230
|
/** 是不是「换主动消息通道就能救」的那种失败(只有被动回复超限算) */
|
|
204
231
|
function isPassiveLimitFailure(failure) {
|
|
205
232
|
return failure.kind === 'passive-limit';
|