koishi-plugin-kkk 3.1.0 → 3.2.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.
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ /**
3
+ * 把「长得离谱的文本」折起来再截断 —— 错误卡片和错误上报共用的止血钳。
4
+ *
5
+ * 线上实测(2026-09-22,用户那条 send_group_msg 失败):OneBot 适配器会把**整个请求参数**
6
+ * 拼进错误信息,里面是一张 4.6MB 的 base64 图片:
7
+ *
8
+ * Error with request send_group_msg, args: {"group_id":1050229473,"message":[{"type":"image","data":{"file":"base64://<4.6MB>"}}]}, retcode: 1200
9
+ *
10
+ * 于是:
11
+ * - 错误卡片的 HTML 被撑到 **9.1MB**(其中 6.3MB 就是这一行),渲染跑十几分钟都出不来;
12
+ * - 上报给服务器的日志同样会带上这 4.6MB,白占带宽和磁盘。
13
+ * 两边都只需要「看出错在哪」,完整内容本来就在日志文件里。
14
+ *
15
+ * 先折叠**再**截断:不折的话前 N 个字符全是 base64,调用栈反而被挤没了。
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.MAX_CARD_LOG_LINES = exports.MAX_CAPTURED_LOG_CHARS = exports.MAX_CAPTURED_LOG_LINES = exports.foldAndTruncate = exports.truncateWithNote = exports.foldLongRuns = void 0;
19
+ /** 连续多长的「无分隔符长串」算载荷(base64 / hash / 转义过的 JSON) */
20
+ const LONG_RUN = 256;
21
+ /**
22
+ * 折叠超长载荷串。
23
+ * @param text - 原始文本
24
+ * @returns 折叠后的文本(长度不变的部分原样保留)
25
+ */
26
+ const foldLongRuns = (text) => String(text ?? '')
27
+ // data URI / base64:// 后面的载荷
28
+ .replace(/((?:data:[\w/.+-]+;)?base64:\/\/)[A-Za-z0-9+/=]{64,}/gi, (_all, head) => head + '…')
29
+ // 兜底:任何没有分隔符的超长串(JSON 里被转义过的载荷也走这条)
30
+ .replace(/[A-Za-z0-9+/=]{256,}/g, (all) => '…[省略 ' + all.length + ' 字符的长串]…');
31
+ exports.foldLongRuns = foldLongRuns;
32
+ /**
33
+ * 截断并注明原文长度。
34
+ * @param text - 原始文本
35
+ * @param limit - 保留的字符数
36
+ * @returns 截断后的文本
37
+ */
38
+ const truncateWithNote = (text, limit) => {
39
+ const value = String(text ?? '');
40
+ if (value.length <= limit)
41
+ return value;
42
+ return value.slice(0, limit) + '\n…(已截断,完整内容见日志;原文共 ' + value.length + ' 字符)';
43
+ };
44
+ exports.truncateWithNote = truncateWithNote;
45
+ /**
46
+ * 折叠 + 截断,一步到位。
47
+ * @param text - 原始文本
48
+ * @param limit - 保留的字符数
49
+ * @returns 安全的文本
50
+ */
51
+ const foldAndTruncate = (text, limit) => (0, exports.truncateWithNote)((0, exports.foldLongRuns)(text), limit);
52
+ exports.foldAndTruncate = foldAndTruncate;
53
+ /** 日志缓冲区最多留多少行(超了丢最早的) */
54
+ exports.MAX_CAPTURED_LOG_LINES = 400;
55
+ /** 单条日志最多留多少字符 —— 4.6MB 的 base64 就是从这里进来的 */
56
+ exports.MAX_CAPTURED_LOG_CHARS = 4000;
57
+ /** 错误卡片上最多显示多少行日志 */
58
+ exports.MAX_CARD_LOG_LINES = 200;
59
+ exports.default = { foldLongRuns: exports.foldLongRuns, truncateWithNote: exports.truncateWithNote, foldAndTruncate: exports.foldAndTruncate, MAX_CAPTURED_LOG_LINES: exports.MAX_CAPTURED_LOG_LINES, MAX_CAPTURED_LOG_CHARS: exports.MAX_CAPTURED_LOG_CHARS, MAX_CARD_LOG_LINES: exports.MAX_CARD_LOG_LINES };
@@ -4,6 +4,7 @@ exports.logger = void 0;
4
4
  exports.setLogger = setLogger;
5
5
  /** karin `logger` 的 Koishi 兼容实现:保留 mark/彩色包装等调用习惯 */
6
6
  const node_util_1 = require("node:util");
7
+ const fold_1 = require("./fold");
7
8
  const runtime_1 = require("./runtime");
8
9
  let koishiLogger = null;
9
10
  function setLogger(logger) {
@@ -62,8 +63,16 @@ const emit = (level, args) => {
62
63
  const logger = getLogger();
63
64
  const message = safeFormat(args);
64
65
  const capture = captureStack[captureStack.length - 1];
65
- if (capture)
66
- capture.push('[' + stamp() + '][' + (KARIN_LEVEL[level] ?? 'INFO') + '] ' + message);
66
+ if (capture) {
67
+ /**
68
+ * 进缓冲区的每一行都要先折叠 + 截断:这条缓冲区后来会进错误卡片、也会随错误上报上传,
69
+ * 而 OneBot 那条 send_group_msg 失败会把 4.6MB 的 base64 整个塞进日志(实测卡片 HTML 因此到 9.1MB)。
70
+ * 行数也要封顶,长解析跑下来几百行就够定位问题了。
71
+ */
72
+ capture.push((0, fold_1.foldAndTruncate)('[' + stamp() + '][' + (KARIN_LEVEL[level] ?? 'INFO') + '] ' + message, fold_1.MAX_CAPTURED_LOG_CHARS));
73
+ if (capture.length > fold_1.MAX_CAPTURED_LOG_LINES)
74
+ capture.splice(0, capture.length - fold_1.MAX_CAPTURED_LOG_LINES);
75
+ }
67
76
  if (!logger) {
68
77
  // 兼容层未初始化时退化到 stdout,避免丢日志
69
78
  // eslint-disable-next-line no-console
@@ -4,6 +4,7 @@ exports.wrapWithErrorHandler = exports.handleBusinessError = void 0;
4
4
  const node_karin_1 = require("../../../../compat/node-karin.js");
5
5
  const module_1 = require("../../../module");
6
6
  const EmojiReaction_1 = require("../../../module/utils/EmojiReaction");
7
+ const ErrorReport_1 = require("../ErrorReport");
7
8
  const ImageSlice_1 = require("../ImageSlice");
8
9
  const render_1 = require("./render");
9
10
  const sender_1 = require("./sender");
@@ -14,13 +15,33 @@ const handleBusinessError = async (error, options, logs, event) => {
14
15
  node_karin_1.logger.debug(`[ErrorHandler] 开始处理业务错误: ${options.businessName}`);
15
16
  const buildMetadata = (0, module_1.getBuildMetadata)();
16
17
  const adapterInfo = event.bot?.adapter ? event.bot.adapter : undefined;
18
+ /**
19
+ * **先上报,再渲染卡片**:上报编号要印在卡片上、也要拼进消息里,
20
+ * 让用户拿着编号进群提问。上传失败/被关掉都只是返回 null,不挡流程。
21
+ */
22
+ let report = null;
23
+ try {
24
+ report = await (0, ErrorReport_1.uploadErrorReport)({
25
+ error,
26
+ business: options.businessName,
27
+ event,
28
+ adapterInfo,
29
+ // 本次请求捕获到的日志行:structuredLogs 里带着原始串(已在 parseLogsToStructured 里逐行折叠 + 截断)
30
+ logs: (logs ?? []).map((entry) => String(entry?.raw ?? entry?.message ?? ''))
31
+ });
32
+ }
33
+ catch (reportError) {
34
+ // 用 warn:这一条以前写成 debug,结果一个 ReferenceError 被静静吞掉,只有「上传失败」四个字能看到
35
+ node_karin_1.logger.warn('[ErrorHandler] 错误上报异常(忽略,不影响报错本身): ' + String(reportError?.message ?? reportError));
36
+ }
17
37
  const ctx = {
18
38
  error,
19
39
  options,
20
40
  logs,
21
41
  event,
22
42
  buildMetadata,
23
- adapterInfo
43
+ adapterInfo,
44
+ report
24
45
  };
25
46
  for (const strategy of (0, strategy_1.getStrategies)()) {
26
47
  if (strategy.match(ctx)) {
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.renderErrorImage = void 0;
7
7
  const node_util_1 = __importDefault(require("node:util"));
8
+ const fold_1 = require("../../../../compat/fold");
9
+ const ErrorReport_1 = require("../ErrorReport");
8
10
  const Render_1 = require("../../../module/utils/Render");
9
11
  const module_1 = require("../../../module");
10
12
  const amagiClient_1 = require("../../../module/utils/amagiClient");
@@ -111,12 +113,7 @@ const MAX_STACK_CHARS = 4000;
111
113
  const MAX_DUMP_CHARS = 4000;
112
114
  /** 错误标题那一行同样要限长(见 foldBlobs 的说明) */
113
115
  const MAX_MESSAGE_CHARS = 1200;
114
- const truncateText = (text, limit) => {
115
- const value = String(text ?? '');
116
- if (value.length <= limit)
117
- return value;
118
- return value.slice(0, limit) + '\n…(已截断,完整内容见日志;原文共 ' + value.length + ' 字符)';
119
- };
116
+ const truncateText = fold_1.truncateWithNote;
120
117
  /**
121
118
  * **把错误文本里的超长 base64 / 长串折叠掉**。
122
119
  *
@@ -130,11 +127,7 @@ const truncateText = (text, limit) => {
130
127
  * 卡片只需要让人看出错在哪:base64 折成 `base64://…(省略 N 字符)`,完整内容仍在日志里。
131
128
  * 注意先折叠**再**截断,否则前 4000 个字符全是 base64,调用栈反而被挤没了。
132
129
  */
133
- const foldBlobs = (text) => String(text ?? '')
134
- // data URI / base64:// 后面的载荷
135
- .replace(/((?:data:[\w/.+-]+;)?base64:\/\/)[A-Za-z0-9+/=]{64,}/gi, (_all, head) => head + '…')
136
- // 兜底:任何没有分隔符的超长串(JSON 里被转义过的载荷也走这条)
137
- .replace(/[A-Za-z0-9+/=]{256,}/g, (all) => '…[省略 ' + all.length + ' 字符的长串]…');
130
+ const foldBlobs = fold_1.foldLongRuns;
138
131
  const stackPartsOf = (error, override) => {
139
132
  if (override)
140
133
  return { stack: truncateText(foldBlobs(override), MAX_STACK_CHARS) };
@@ -188,7 +181,20 @@ const renderErrorImage = async (ctx, opts = {}) => {
188
181
  amagi,
189
182
  method: options.businessName,
190
183
  timestamp: new Date().toISOString(),
191
- logs: logs?.slice().reverse(),
184
+ /**
185
+ * 卡片上只留最近这些行,**并且逐行折叠 + 截断**。
186
+ *
187
+ * compat/logger 在收集阶段已经折过一遍,这里再兜一次底:这条数据还可能来自
188
+ * 别的调用方(比如自定义错误处理器直接把原始日志数组塞进来),线上那次
189
+ * 「4.6MB base64 撑出 9MB 卡片」就是从这种没折过的路径进来的。
190
+ */
191
+ logs: logs
192
+ ? logs.slice(-fold_1.MAX_CARD_LOG_LINES).reverse().map((entry) => ({
193
+ ...entry,
194
+ message: (0, fold_1.truncateWithNote)((0, fold_1.foldLongRuns)(entry.message ?? ''), 4000),
195
+ raw: (0, fold_1.truncateWithNote)((0, fold_1.foldLongRuns)(entry.raw ?? ''), 4000)
196
+ }))
197
+ : [],
192
198
  triggerCommand: event?.msg || '未知命令或处于非消息环境',
193
199
  frameworkVersion: module_1.Root.karinVersion,
194
200
  // 和主布局用同一份 logo(跨模块导入以免又出现「旧头像」)
@@ -198,6 +204,10 @@ const renderErrorImage = async (ctx, opts = {}) => {
198
204
  commitHash: buildMetadata?.commitHash,
199
205
  // 之前这里可能传 undefined,模板读 adapterInfo.version.startsWith 直接 SSR 崩掉
200
206
  adapterInfo: adapterInfo ?? { name: '未知适配器', version: '' },
207
+ // 上报成功的编号与反馈群:印在卡片上,用户照着进群提问
208
+ report: ctx.report
209
+ ? { id: ctx.report.id, url: ctx.report.url, group: (0, ErrorReport_1.reportConfig)().group, groupUrl: (0, ErrorReport_1.groupLinkOf)((0, ErrorReport_1.reportConfig)().group) }
210
+ : undefined,
201
211
  isVerification: opts.isVerification,
202
212
  verificationUrl: opts.verificationUrl,
203
213
  share_url: opts.share_url
@@ -37,6 +37,7 @@ exports.sendErrorToConfiguredIds = exports.sendErrorToAdmins = exports.sendError
37
37
  const node_karin_1 = __importStar(require("../../../../compat/node-karin.js"));
38
38
  const bot_1 = require("../bot");
39
39
  const Config_1 = require("../Config");
40
+ const ErrorReport_1 = require("../ErrorReport");
40
41
  const utils_1 = require("./utils");
41
42
  /**
42
43
  * 发送错误图片给触发者
@@ -56,7 +57,8 @@ const sendErrorToTrigger = async (ctx, img) => {
56
57
  if (!Config_1.Config.app.errorLogSendTo.some((item) => item === 'trigger'))
57
58
  return;
58
59
  try {
59
- await event.reply(img);
60
+ // 卡片 + 「错误信息已上传,ID:xxx / 可以前往 QQ 群内寻找帮助」——群号在 QQ 官方那边是可点链接
61
+ await event.reply([...img, ...(0, ErrorReport_1.errorHelpSegments)(ctx.report ?? null, platformOfEvent(event))]);
60
62
  }
61
63
  catch (err) {
62
64
  node_karin_1.logger.error(`[ErrorHandler] 发送错误消息给触发者失败: ${err}`);
@@ -130,7 +132,7 @@ const sendErrorToAllMasters = async (ctx, img, customPrefix) => {
130
132
  if (notifiedSet.has(key))
131
133
  continue;
132
134
  try {
133
- await node_karin_1.default.sendMaster(target.botId, target.master, [node_karin_1.segment.text(prefix), ...img]);
135
+ await node_karin_1.default.sendMaster(target.botId, target.master, [node_karin_1.segment.text(prefix), ...img, ...(0, ErrorReport_1.errorHelpSegments)(ctx.report ?? null, platformOfEvent(event))]);
134
136
  notifiedSet.add(key);
135
137
  node_karin_1.logger.debug(`[ErrorHandler] 已发送错误消息给主人: ${target.master} (via ${target.botId})`);
136
138
  }
@@ -194,6 +196,8 @@ const resolveAllMasterTargets = async (event, isPush) => {
194
196
  return [];
195
197
  return masters.map((master) => ({ master, botId }));
196
198
  };
199
+ /** 出错时所在平台(决定提示语用 markdown 还是纯文本) */
200
+ const platformOfEvent = (event) => String(event?.bot?.platform ?? event?.platform ?? event?.bot?.adapter?.platform ?? '');
197
201
  const buildErrorPrefix = async (ctx, isPush, botId) => {
198
202
  const { options, event } = ctx;
199
203
  if (isPush) {
@@ -222,7 +226,7 @@ const sendErrorToConfiguredIds = async (ctx, img, customPrefix) => {
222
226
  const prefix = customPrefix || (await buildErrorPrefix(ctx, isPush, botId));
223
227
  for (const id of new Set(ids)) {
224
228
  try {
225
- await node_karin_1.default.sendMaster(botId, String(id), [node_karin_1.segment.text(prefix), ...img]);
229
+ await node_karin_1.default.sendMaster(botId, String(id), [node_karin_1.segment.text(prefix), ...img, ...(0, ErrorReport_1.errorHelpSegments)(ctx.report ?? null, platformOfEvent(event))]);
226
230
  node_karin_1.logger.debug(`[ErrorHandler] 已发送错误消息给 ${id}`);
227
231
  }
228
232
  catch (err) {
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.injectBotToEventForPushTask = exports.getPushTaskBotId = exports.isPushTask = exports.statBotId = exports.parseLogsToStructured = void 0;
7
7
  const node_karin_1 = require("../../../../compat/node-karin.js");
8
8
  const node_karin_2 = __importDefault(require("../../../../compat/node-karin.js"));
9
+ const fold_1 = require("../../../../compat/fold");
9
10
  const bot_1 = require("../bot");
10
11
  const Config_1 = require("../Config");
11
12
  /**
@@ -24,7 +25,10 @@ const Config_1 = require("../Config");
24
25
  const parseLogsToStructured = (logs) => {
25
26
  const logRegex = /\[(\d{2}:\d{2}:\d{2}\.\d{3})\]\[([A-Z]{4})\]\s(.+)/s;
26
27
  return logs
27
- .map((log) => {
28
+ .map((raw) => {
29
+ // 逐行折叠 + 截断:OneBot 那条报错会把 4.6MB 的 base64 塞进日志,
30
+ // 不折的话错误卡片能撑到 9MB、渲染十几分钟(见 compat/fold 的说明)
31
+ const log = (0, fold_1.foldAndTruncate)(raw, fold_1.MAX_CAPTURED_LOG_CHARS);
28
32
  const match = log.match(logRegex);
29
33
  if (match) {
30
34
  return {
@@ -0,0 +1,311 @@
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.errorHelpSegments = exports.uploadErrorReport = exports.groupLinkOf = exports.REPORT_LOG_LINES = exports.REPORT_GROUP = exports.REPORT_TOKEN = exports.REPORT_URL = void 0;
7
+ exports.reportConfig = reportConfig;
8
+ /**
9
+ * 错误上报。
10
+ *
11
+ * 出错的瞬间把「错误 + 运行环境 + 最近的日志」POST 到错误收集站(默认 \`https://err.tangbot.xyz\`),
12
+ * 站长在网页上就能看到全部细节,不用让用户截图、也不用让用户翻日志文件。
13
+ *
14
+ * 几条硬性约定:
15
+ * 1. **绝不影响报错本身**:整条链路失败只写一行日志,不抛异常、不拖住错误卡片的发送;
16
+ * 2. **有超时**:默认 8 秒,超时就放弃(错误卡片该发还得发);
17
+ * 3. **有上限**:日志逐行折叠 + 截断(见 compat/fold),总行数、总字节数都封顶;
18
+ * 4. **可关闭**:面板「通用 → 错误上报」里关掉就完全不上传(默认开启)。
19
+ */
20
+ const node_os_1 = __importDefault(require("node:os"));
21
+ const node_zlib_1 = require("node:zlib");
22
+ const node_karin_1 = require("../../../compat/node-karin.js");
23
+ const fold_1 = require("../../../compat/fold");
24
+ const runtime_1 = require("../../../compat/runtime");
25
+ const qqOptions_1 = require("../../../qqOptions");
26
+ const root_1 = require("../../root");
27
+ const build_metadata_1 = require("./build-metadata");
28
+ /**
29
+ * 收集站地址:**写死,不开放配置**。
30
+ * 自建的人改这一行重新构建即可 —— 面板里只留「是否上传」一个开关,少一个能填错的地方。
31
+ */
32
+ exports.REPORT_URL = 'https://err.tangbot.xyz';
33
+ /**
34
+ * 上报令牌:**写死**。
35
+ * 服务端 \`KKK_ERR_TOKEN\` 的默认值就是它,所以两边开箱即用、什么都不用配。
36
+ * 它写在开源插件里,等于公开 —— 服务端那边另有 IP 限流兜着,挡的是随机刷接口的流量。
37
+ */
38
+ exports.REPORT_TOKEN = 'kkk-err-9f3c1d7a5b2e4c80';
39
+ /** 反馈群:写死(QQ 客户端点群链接就能进) */
40
+ exports.REPORT_GROUP = '1050229473';
41
+ /** 上报多少行日志:写死(本次请求的日志 + 宿主日志文件尾部各取这么多行) */
42
+ exports.REPORT_LOG_LINES = 200;
43
+ /**
44
+ * 上传超时(毫秒):超了就放弃,不拖住错误卡片。
45
+ *
46
+ * 上报是**在渲染错误卡片之前**做的(编号要印在卡片上),所以站点挂掉时这 5 秒会加到
47
+ * 报错送达上 —— 正常情况一次上传 50 毫秒左右,只有连不上站点才会等到超时。
48
+ */
49
+ const UPLOAD_TIMEOUT_MS = 5000;
50
+ /** 上报日志单行最长字符数 */
51
+ const LOG_LINE_LIMIT = 2000;
52
+ /** 整份上报最多多大(压缩前),超了继续砍日志 */
53
+ const PAYLOAD_LIMIT_BYTES = 2 * 1024 * 1024;
54
+ /** 读取宿主日志文件时最多读多少字节 */
55
+ const LOG_FILE_TAIL_BYTES = 512 * 1024;
56
+ /**
57
+ * 上报配置。
58
+ *
59
+ * 面板里**只有「上传错误信息」一个开关**(通用 → 错误上报):地址、令牌、群号、日志行数全部写死,
60
+ * 用户没有可以填错的地方。关掉开关就完全不上传。
61
+ */
62
+ function reportConfig() {
63
+ let enabled = true;
64
+ try {
65
+ const options = (0, qqOptions_1.readQqOptions)(((0, runtime_1.tryGetRuntime)()?.config ?? {}));
66
+ enabled = options.errorReportUpload !== false;
67
+ }
68
+ catch {
69
+ enabled = (0, runtime_1.tryGetRuntime)()?.config?.errorReportUpload !== false;
70
+ }
71
+ return { enabled, url: exports.REPORT_URL, token: exports.REPORT_TOKEN, logLines: exports.REPORT_LOG_LINES, group: exports.REPORT_GROUP };
72
+ }
73
+ /** 反馈群链接:QQ 官方的加群链接格式,面板里只填群号 */
74
+ const groupLinkOf = (group) => 'https://qm.qq.com/q/' + group;
75
+ exports.groupLinkOf = groupLinkOf;
76
+ /**
77
+ * 已安装插件清单(名字 + 版本)。
78
+ *
79
+ * 插件版本号不在 Koishi 的注册表里,只能按名字去 \`node_modules/<name>/package.json\` 读;
80
+ * 读不到就留空 —— 名字本身已经足够定位「装了哪些插件」。
81
+ * 结果缓存 60 秒:一次报错读上百个文件没必要。
82
+ */
83
+ let pluginCache = null;
84
+ const collectPlugins = () => {
85
+ if (pluginCache && Date.now() - pluginCache.at < 60_000)
86
+ return pluginCache.list;
87
+ const names = new Set();
88
+ try {
89
+ const registry = (0, runtime_1.tryGetRuntime)()?.ctx?.registry;
90
+ const entries = typeof registry?.values === 'function' ? [...registry.values()] : Array.isArray(registry) ? registry : [];
91
+ for (const entry of entries) {
92
+ const name = entry?.name ?? entry?.plugin?.name;
93
+ if (name)
94
+ names.add(String(name));
95
+ }
96
+ }
97
+ catch { /* 拿不到就只报 kkk 自己 */ }
98
+ names.add(root_1.Root.pluginName);
99
+ const list = [...names].sort().map((name) => {
100
+ let version = '';
101
+ try {
102
+ // 编译产物是 CJS,这里的 require 会沿插件的 node_modules 往上找宿主依赖
103
+ version = String(require(name + '/package.json')?.version ?? '');
104
+ }
105
+ catch {
106
+ version = '';
107
+ }
108
+ return { name, version };
109
+ });
110
+ pluginCache = { at: Date.now(), list };
111
+ return list;
112
+ };
113
+ /** 宿主日志文件的尾部:出问题时整台机器在干什么,比单个请求的日志更能说明问题 */
114
+ const readLogFileTail = (lines) => {
115
+ if (lines <= 0)
116
+ return { lines: [], truncated: false };
117
+ try {
118
+ const fs = require('node:fs');
119
+ const path = require('node:path');
120
+ const dataRoot = (0, runtime_1.tryGetRuntime)()?.dataRoot;
121
+ if (!dataRoot)
122
+ return { lines: [], truncated: false };
123
+ const dir = path.join(dataRoot, 'logs');
124
+ if (!fs.existsSync(dir))
125
+ return { lines: [], truncated: false };
126
+ const files = fs.readdirSync(dir).filter((name) => name.endsWith('.log'));
127
+ if (!files.length)
128
+ return { lines: [], truncated: false };
129
+ // 文件名就是时间序(2026-09-22-33.log),按修改时间取最新的那个
130
+ const newest = files
131
+ .map((name) => ({ name, at: fs.statSync(path.join(dir, name)).mtimeMs }))
132
+ .sort((a, b) => b.at - a.at)[0];
133
+ const full = path.join(dir, newest.name);
134
+ const size = fs.statSync(full).size;
135
+ const start = Math.max(0, size - LOG_FILE_TAIL_BYTES);
136
+ const fd = fs.openSync(full, 'r');
137
+ try {
138
+ const buffer = Buffer.alloc(size - start);
139
+ fs.readSync(fd, buffer, 0, buffer.length, start);
140
+ const text = buffer.toString('utf8');
141
+ const all = text.split(/\r?\n/).filter(Boolean);
142
+ const kept = all.slice(-lines).map((line) => (0, fold_1.foldAndTruncate)(line, LOG_LINE_LIMIT));
143
+ return { file: newest.name, lines: kept, truncated: all.length > kept.length || start > 0 };
144
+ }
145
+ finally {
146
+ fs.closeSync(fd);
147
+ }
148
+ }
149
+ catch (error) {
150
+ node_karin_1.logger.debug('[错误上报] 读宿主日志失败: ' + String(error?.message ?? error));
151
+ return { lines: [], truncated: false };
152
+ }
153
+ };
154
+ /** 简短的适配器信息:出问题时是哪个平台、哪个机器人 */
155
+ const collectAdapters = (event) => {
156
+ const out = [];
157
+ try {
158
+ const bots = (0, runtime_1.tryGetRuntime)()?.ctx?.bots ?? [];
159
+ for (const bot of bots) {
160
+ out.push({
161
+ platform: String(bot?.platform ?? ''),
162
+ selfId: String(bot?.selfId ?? ''),
163
+ status: bot?.status ?? '',
164
+ adapter: String(bot?.adapter?.name ?? '')
165
+ });
166
+ }
167
+ }
168
+ catch { /* 忽略 */ }
169
+ const adapter = event?.bot?.adapter;
170
+ if (adapter && !out.some((item) => item.adapter === adapter.name)) {
171
+ out.push({ platform: String(adapter.platform ?? ''), selfId: String(event?.selfId ?? ''), status: '', adapter: String(adapter.name ?? '') });
172
+ }
173
+ return out;
174
+ };
175
+ /** 生成一个不重复的上报编号:时间 + 随机后缀,按时间排序天然有序 */
176
+ const makeReportId = () => {
177
+ const now = new Date();
178
+ const pad = (value, size = 2) => String(value).padStart(size, '0');
179
+ const stamp = now.getFullYear() + pad(now.getMonth() + 1) + pad(now.getDate()) + '-' + pad(now.getHours()) + pad(now.getMinutes()) + pad(now.getSeconds());
180
+ return stamp + '-' + Math.random().toString(36).slice(2, 8);
181
+ };
182
+ /**
183
+ * 上传一份错误报告。
184
+ *
185
+ * @returns 成功时给出收集站的编号与详情页地址;关掉上报或上传失败时返回 \`null\`(调用方照常发错误卡片)
186
+ */
187
+ const uploadErrorReport = async (input) => {
188
+ const config = reportConfig();
189
+ if (!config.enabled)
190
+ return null;
191
+ if (typeof fetch !== 'function')
192
+ return null;
193
+ const error = input.error;
194
+ const buildMetadata = (0, build_metadata_1.getBuildMetadata)();
195
+ const adapterInfo = input.adapterInfo;
196
+ const id = makeReportId();
197
+ const lines = (input.logs ?? []).slice(-Math.max(config.logLines, 0))
198
+ .map((line) => (0, fold_1.foldAndTruncate)(line, LOG_LINE_LIMIT));
199
+ const envLogs = readLogFileTail(Math.min(config.logLines, 300));
200
+ const payload = {
201
+ id,
202
+ time: Date.now(),
203
+ plugin: { name: root_1.Root.pluginName, version: root_1.Root.pluginVersion },
204
+ source: {
205
+ platform: String(input.event?.bot?.platform ?? input.event?.platform ?? adapterInfo?.platform ?? ''),
206
+ route: String(input.route ?? ''),
207
+ stage: String(input.stage ?? ''),
208
+ business: String(input.business ?? ''),
209
+ guildId: String(input.event?.guildId ?? ''),
210
+ channelId: String(input.event?.channelId ?? ''),
211
+ userId: String(input.event?.userId ?? ''),
212
+ selfId: String(input.event?.selfId ?? ''),
213
+ command: (0, fold_1.foldAndTruncate)(String(input.event?.msg ?? input.event?.content ?? ''), 300)
214
+ },
215
+ error: {
216
+ name: String(error?.name ?? 'Error'),
217
+ message: (0, fold_1.foldAndTruncate)(error?.message ?? String(error), fold_1.MAX_CAPTURED_LOG_CHARS),
218
+ stack: (0, fold_1.foldAndTruncate)(error?.stack ?? '', fold_1.MAX_CAPTURED_LOG_CHARS * 2),
219
+ code: String(error?.code ?? error?.err_code ?? ''),
220
+ kind: String(error?.kkkKind ?? '')
221
+ },
222
+ env: {
223
+ node: process.version,
224
+ os: node_os_1.default.type() + ' ' + node_os_1.default.release() + ' ' + node_os_1.default.arch(),
225
+ koishi: root_1.Root.karinVersion,
226
+ pluginVersion: root_1.Root.pluginVersion,
227
+ buildTime: buildMetadata?.buildTime ?? '',
228
+ commit: buildMetadata?.shortCommitHash ?? '',
229
+ uptimeSec: Math.round(process.uptime()),
230
+ memoryMB: Math.round(process.memoryUsage().rss / 1024 / 1024),
231
+ cwd: process.cwd(),
232
+ dataRoot: String((0, runtime_1.tryGetRuntime)()?.dataRoot ?? ''),
233
+ plugins: collectPlugins(),
234
+ adapters: collectAdapters(input.event)
235
+ },
236
+ logs: {
237
+ lines,
238
+ count: lines.length,
239
+ truncated: (input.logs ?? []).length > lines.length,
240
+ // 宿主日志文件的尾部:整台机器最近的动静
241
+ envFile: envLogs.file ?? '',
242
+ envLines: envLogs.lines,
243
+ envTruncated: envLogs.truncated
244
+ },
245
+ extra: input.extra ?? {}
246
+ };
247
+ // 体积兜底:还是太大就继续砍日志(先把宿主日志去掉,再砍本次日志)
248
+ let body = Buffer.from(JSON.stringify(payload));
249
+ if (body.length > PAYLOAD_LIMIT_BYTES) {
250
+ payload.logs.envLines = [];
251
+ body = Buffer.from(JSON.stringify(payload));
252
+ }
253
+ if (body.length > PAYLOAD_LIMIT_BYTES) {
254
+ payload.logs.lines = payload.logs.lines.slice(-50);
255
+ payload.logs.count = payload.logs.lines.length;
256
+ payload.logs.truncated = true;
257
+ body = Buffer.from(JSON.stringify(payload));
258
+ }
259
+ const gzipped = (0, node_zlib_1.gzipSync)(body);
260
+ try {
261
+ const response = await fetch(config.url + '/api/v1/reports', {
262
+ method: 'POST',
263
+ headers: {
264
+ 'content-type': 'application/json',
265
+ 'content-encoding': 'gzip',
266
+ 'content-length': String(gzipped.length),
267
+ 'x-kkk-token': config.token,
268
+ 'user-agent': 'koishi-plugin-kkk/' + root_1.Root.pluginVersion
269
+ },
270
+ body: gzipped,
271
+ signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
272
+ });
273
+ if (!response.ok) {
274
+ node_karin_1.logger.warn('[错误上报] 上传失败 HTTP ' + response.status + ':' + (0, fold_1.foldAndTruncate)(await response.text().catch(() => ''), 200));
275
+ return null;
276
+ }
277
+ const result = await response.json().catch(() => null);
278
+ const reportId = String(result?.id ?? id);
279
+ const url = config.url + '/reports/' + reportId;
280
+ node_karin_1.logger.info('[错误上报] 已上传:' + url + '(' + Math.round(body.length / 1024) + 'KB → ' + Math.round(gzipped.length / 1024) + 'KB)');
281
+ return { id: reportId, url };
282
+ }
283
+ catch (uploadError) {
284
+ node_karin_1.logger.warn('[错误上报] 上传失败:' + String(uploadError?.message ?? uploadError));
285
+ return null;
286
+ }
287
+ };
288
+ exports.uploadErrorReport = uploadErrorReport;
289
+ /**
290
+ * 附加在错误消息里的「求助」片段。
291
+ *
292
+ * QQ 官方机器人认 markdown,就把群号和上报链接做成**可点**的;个人号(OneBot / NapCat)
293
+ * 不渲染 markdown,直接给纯文本 —— QQ 客户端会把裸链接变成可点的蓝色链接。
294
+ */
295
+ const errorHelpSegments = (report, platform) => {
296
+ const config = reportConfig();
297
+ const group = config.group;
298
+ const link = (0, exports.groupLinkOf)(group);
299
+ const officialQq = /^qq/i.test(String(platform ?? ''));
300
+ const head = report
301
+ ? '错误信息已上传,ID:' + report.id
302
+ : '错误信息上传失败(不影响使用)';
303
+ const ask = '可以前往 QQ 群内寻找帮助,在群内发送错误 ID 或该错误图片';
304
+ if (officialQq) {
305
+ // QQ 官方机器人认 markdown:群号做成可点的加群链接
306
+ return [node_karin_1.segment.markdown(head + '\n' + '可以前往 [QQ 群 ' + group + '](' + link + ') 内寻找帮助,在群内发送错误 ID 或该错误图片')];
307
+ }
308
+ // 个人号(OneBot / NapCat)不渲染 markdown:裸链接 QQ 客户端自己会变蓝可点
309
+ return [node_karin_1.segment.text(head + '\nQQ 群 ' + group + ':' + ask + '\n' + link)];
310
+ };
311
+ exports.errorHelpSegments = errorHelpSegments;
@@ -379,13 +379,15 @@ const handlerError = (props) => {
379
379
  }, children: (0, jsx_runtime_1.jsx)("pre", { className: "text-2xl leading-relaxed whitespace-pre-wrap break-all font-mono", style: { color: dark ? 'rgba(255,255,255,0.85)' : 'rgba(127,29,29,0.9)' }, dangerouslySetInnerHTML: { __html: stackHtml } }) })] }), data.error?.dump && ((0, jsx_runtime_1.jsxs)("div", { className: "mb-14", children: [(0, jsx_runtime_1.jsx)(SectionTitle, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Braces, { size: 36, style: { color: mutedColor } }), en: "Object Dump", zh: "\u5BF9\u8C61\u8F6C\u50A8", color: mutedColor }), (0, jsx_runtime_1.jsx)("div", { className: "p-10 rounded-[36px]", style: { backgroundColor: dark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.5)' }, children: (0, jsx_runtime_1.jsx)("pre", { className: "text-2xl leading-relaxed whitespace-pre-wrap break-all font-mono", style: { color: dark ? 'rgba(255,255,255,0.85)' : 'rgba(127,29,29,0.9)' }, dangerouslySetInnerHTML: { __html: convertAnsiToHtml(data.error.dump) } }) })] })), data.logs && data.logs.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "mb-14", children: [(0, jsx_runtime_1.jsx)(SectionTitle, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.FileText, { size: 36, style: { color: mutedColor } }), en: "Execution Logs", zh: "\u6267\u884C\u65E5\u5FD7", color: mutedColor }), (0, jsx_runtime_1.jsx)("div", { className: "space-y-6", children: data.logs.map((log, index) => {
380
380
  const theme = getLogLevelTheme(log.level, dark);
381
381
  return ((0, jsx_runtime_1.jsxs)("fieldset", { className: `relative rounded-3xl ${theme.bgClass} border-2 ${theme.borderClass} p-6`, children: [(0, jsx_runtime_1.jsxs)("legend", { className: "flex items-center gap-2 ml-4", children: [(0, jsx_runtime_1.jsx)("span", { className: `w-2 h-6 rounded-full -mr-1.5 ${theme.dotClass}` }), (0, jsx_runtime_1.jsxs)("span", { className: "flex items-center gap-2 px-3", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Clock, { size: 18, className: theme.iconClass }), (0, jsx_runtime_1.jsx)("span", { className: `text-xl font-mono font-medium ${theme.textClass}`, children: log.timestamp })] }), (0, jsx_runtime_1.jsx)("span", { className: `w-2 h-6 rounded-full -ml-1.5 ${theme.dotClass}` })] }), (0, jsx_runtime_1.jsx)("div", { className: "absolute bottom-2 right-6 pointer-events-none", children: (0, jsx_runtime_1.jsxs)("span", { className: `text-6xl font-black uppercase leading-none tracking-tight ${theme.levelClass}`, children: [log.level, "\u300D"] }) }), (0, jsx_runtime_1.jsx)("div", { className: "relative z-1 text-2xl font-mono whitespace-pre-wrap break-all leading-relaxed", style: { color: dark ? 'rgba(255,255,255,0.88)' : 'rgba(0,0,0,0.82)' }, dangerouslySetInnerHTML: { __html: convertAnsiToHtml(log.message) } })] }, index));
382
- }) })] })), (0, jsx_runtime_1.jsxs)("div", { className: "mt-auto pt-12", style: { borderTop: `2px solid ${dark ? 'rgba(248,113,113,0.15)' : 'rgba(252,165,165,0.3)'}` }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center flex-wrap gap-x-16 gap-y-6 mb-12", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-6", children: [(0, jsx_runtime_1.jsx)("img", { src: data.frameworkLogo ?? '/image/frame-logo.png', className: "h-16 w-auto", alt: "Framework" }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "text-xl", style: { color: mutedColor }, children: "Framework / \u6846\u67B6\u7248\u672C" }), (0, jsx_runtime_1.jsx)("p", { className: "text-3xl font-bold", style: { color: accentColor }, children: data.frameworkVersion })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-6", children: [(0, jsx_runtime_1.jsxs)("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 230 221", className: "h-16 w-auto", style: { color: accentColor }, children: [(0, jsx_runtime_1.jsx)("path", { d: "M132.75,87.37l-53.72-53.37c-4.66-4.63-1.38-12.58,5.18-12.58h115.13c6.57,0,9.84,7.95,5.18,12.58l-53.72,53.37c-4.99,4.96-13.06,4.96-18.05,0Z", fill: "currentColor" }), (0, jsx_runtime_1.jsx)("path", { d: "M28.49,186.89l.03-51.42c-.02-6.57,7.92-9.87,12.56-5.23l57.02,57.02c4.64,4.64,1.34,12.41-5.23,12.39h-51.42c-7.04-.02-12.94-5.72-12.96-12.76Z", fill: "currentColor" }), (0, jsx_runtime_1.jsx)("path", { d: "M41.54,23.68l163.04,163.05c4.78,4.78,1.39,12.95-5.36,12.94h-47.88c-9.69,0-18.99-3.86-25.84-10.71L39.3,102.75c-6.85-6.85-10.7-16.15-10.7-25.84V29.04c0-6.76,8.16-10.14,12.94-5.36Z", fill: "currentColor" })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "text-xl", style: { color: mutedColor }, children: "Plugin / \u63D2\u4EF6\u7248\u672C" }), (0, jsx_runtime_1.jsx)("p", { className: "text-3xl font-bold", style: { color: accentColor }, children: data.pluginVersion })] })] }), data.adapterInfo && ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-6", children: [getAdapterLogo(data.adapterInfo.name), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "text-xl", style: { color: mutedColor }, children: "Adapter / \u9002\u914D\u5668" }), (0, jsx_runtime_1.jsx)("p", { className: "text-3xl font-bold truncate", style: { color: accentColor }, children: data.adapterInfo.name })] })] }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-10 text-xl mb-12", style: { color: mutedColor }, children: [data.buildTime && ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)(md_1.MdSchedule, { size: 24 }), (0, jsx_runtime_1.jsxs)("span", { children: ["Built Time: ", data.buildTime, " \u4E8E", ' ', (0, date_fns_1.formatDistanceToNow)((0, date_fns_1.parse)(data.buildTime, 'yyyy年MM月dd日 HH:mm', new Date()), { locale: locale_1.zhCN }), "\u524D"] })] })), data.commitHash && ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.GitBranch, { size: 24 }), (0, jsx_runtime_1.jsxs)("span", { children: ["Commit Hash: ", data.commitHash] })] }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "p-10 rounded-[36px]", style: { backgroundColor: dark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.5)' }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-end justify-between mb-6", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "text-3xl font-semibold mb-2", style: { color: accentColor }, children: "Need Help? / \u9700\u8981\u5E2E\u52A9\uFF1F" }), (0, jsx_runtime_1.jsx)("p", { className: "text-2xl", style: { color: secondaryColor }, children: "\u63D0\u4EA4\u95EE\u9898\u65F6\u8BF7\u9644\u4E0A\u5B8C\u6574\u62A5\u9519\u622A\u56FE\u3001\u590D\u73B0\u6B65\u9AA4\u548C\u73AF\u5883\u7248\u672C\u4FE1\u606F\u3002" })] }), (0, jsx_runtime_1.jsx)("span", { className: "text-xs font-black tracking-[0.2em] uppercase px-3 py-1 rounded-full", style: {
382
+ }) })] })), (0, jsx_runtime_1.jsxs)("div", { className: "mt-auto pt-12", style: { borderTop: `2px solid ${dark ? 'rgba(248,113,113,0.15)' : 'rgba(252,165,165,0.3)'}` }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center flex-wrap gap-x-16 gap-y-6 mb-12", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-6", children: [(0, jsx_runtime_1.jsx)("img", { src: data.frameworkLogo ?? '/image/frame-logo.png', className: "h-16 w-auto", alt: "Framework" }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "text-xl", style: { color: mutedColor }, children: "Framework / \u6846\u67B6\u7248\u672C" }), (0, jsx_runtime_1.jsx)("p", { className: "text-3xl font-bold", style: { color: accentColor }, children: data.frameworkVersion })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-6", children: [(0, jsx_runtime_1.jsxs)("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 230 221", className: "h-16 w-auto", style: { color: accentColor }, children: [(0, jsx_runtime_1.jsx)("path", { d: "M132.75,87.37l-53.72-53.37c-4.66-4.63-1.38-12.58,5.18-12.58h115.13c6.57,0,9.84,7.95,5.18,12.58l-53.72,53.37c-4.99,4.96-13.06,4.96-18.05,0Z", fill: "currentColor" }), (0, jsx_runtime_1.jsx)("path", { d: "M28.49,186.89l.03-51.42c-.02-6.57,7.92-9.87,12.56-5.23l57.02,57.02c4.64,4.64,1.34,12.41-5.23,12.39h-51.42c-7.04-.02-12.94-5.72-12.96-12.76Z", fill: "currentColor" }), (0, jsx_runtime_1.jsx)("path", { d: "M41.54,23.68l163.04,163.05c4.78,4.78,1.39,12.95-5.36,12.94h-47.88c-9.69,0-18.99-3.86-25.84-10.71L39.3,102.75c-6.85-6.85-10.7-16.15-10.7-25.84V29.04c0-6.76,8.16-10.14,12.94-5.36Z", fill: "currentColor" })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "text-xl", style: { color: mutedColor }, children: "Plugin / \u63D2\u4EF6\u7248\u672C" }), (0, jsx_runtime_1.jsx)("p", { className: "text-3xl font-bold", style: { color: accentColor }, children: data.pluginVersion })] })] }), data.adapterInfo && ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-6", children: [getAdapterLogo(data.adapterInfo.name), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "text-xl", style: { color: mutedColor }, children: "Adapter / \u9002\u914D\u5668" }), (0, jsx_runtime_1.jsx)("p", { className: "text-3xl font-bold truncate", style: { color: accentColor }, children: data.adapterInfo.name })] })] }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-10 text-xl mb-12", style: { color: mutedColor }, children: [data.buildTime && ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)(md_1.MdSchedule, { size: 24 }), (0, jsx_runtime_1.jsxs)("span", { children: ["Built Time: ", data.buildTime, " \u4E8E", ' ', (0, date_fns_1.formatDistanceToNow)((0, date_fns_1.parse)(data.buildTime, 'yyyy年MM月dd日 HH:mm', new Date()), { locale: locale_1.zhCN }), "\u524D"] })] })), data.commitHash && ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.GitBranch, { size: 24 }), (0, jsx_runtime_1.jsxs)("span", { children: ["Commit Hash: ", data.commitHash] })] }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "p-10 rounded-[36px]", style: { backgroundColor: dark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.5)' }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-end justify-between mb-6", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "text-3xl font-semibold mb-2", style: { color: accentColor }, children: "Need Help? / \u9700\u8981\u5E2E\u52A9\uFF1F" }), (0, jsx_runtime_1.jsx)("p", { className: "text-2xl", style: { color: secondaryColor }, children: data.report
383
+ ? '错误信息已上传,进群发送下面的上报编号或这张图片即可。'
384
+ : '提交问题时请附上完整报错截图、复现步骤和环境版本信息。' })] }), (0, jsx_runtime_1.jsx)("span", { className: "text-xs font-black tracking-[0.2em] uppercase px-3 py-1 rounded-full", style: {
383
385
  color: primaryColor,
384
386
  backgroundColor: dark ? 'rgba(248,113,113,0.12)' : 'rgba(220,38,38,0.08)'
385
387
  }, children: "Support" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-2 gap-x-6 gap-y-6 text-2xl leading-relaxed py-6", style: {
386
388
  borderTop: `1px solid ${dark ? 'rgba(248,113,113,0.2)' : 'rgba(220,38,38,0.12)'}`,
387
389
  borderBottom: `1px solid ${dark ? 'rgba(248,113,113,0.2)' : 'rgba(220,38,38,0.12)'}`
388
- }, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "font-semibold mb-1", style: { color: accentColor }, children: "GitHub Issue" }), (0, jsx_runtime_1.jsx)("p", { className: "text-xl break-all", style: { color: secondaryColor }, children: "https://github.com/maimai993/koishi-plugin-kkk/issues/new/choose" })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "font-semibold mb-1", style: { color: accentColor }, children: "GitHub Repository" }), (0, jsx_runtime_1.jsx)("p", { className: "text-xl break-all", style: { color: secondaryColor }, children: "https://github.com/maimai993/koishi-plugin-kkk" })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "font-semibold mb-1", style: { color: accentColor }, children: "QQ \u7FA4" }), (0, jsx_runtime_1.jsx)("p", { className: "text-xl", style: { color: secondaryColor }, children: "1050229473" })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "font-semibold mb-1", style: { color: accentColor }, children: "\u9644\u5E26\u4FE1\u606F" }), (0, jsx_runtime_1.jsx)("p", { className: "text-xl", style: { color: secondaryColor }, children: "\u6B64\u56FE\u7247 + \u89E6\u53D1\u547D\u4EE4 + \u5BF9\u5E94\u914D\u7F6E\uFF08\u81EA\u884C\u8131\u654F\u5904\u7406\uFF09" })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-4 mt-6 text-xl", style: { color: mutedColor }, children: [(0, jsx_runtime_1.jsx)("span", { className: "font-mono", children: "Tips:" }), (0, jsx_runtime_1.jsx)("span", { children: "\u4FE1\u606F\u8D8A\u5B8C\u6574\uFF0C\u5B9A\u4F4D\u8D8A\u5FEB\u3002" })] })] })] })] })] }));
390
+ }, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "font-semibold mb-1", style: { color: accentColor }, children: "GitHub Issue" }), (0, jsx_runtime_1.jsx)("p", { className: "text-xl break-all", style: { color: secondaryColor }, children: "https://github.com/maimai993/koishi-plugin-kkk/issues/new/choose" })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "font-semibold mb-1", style: { color: accentColor }, children: "GitHub Repository" }), (0, jsx_runtime_1.jsx)("p", { className: "text-xl break-all", style: { color: secondaryColor }, children: "https://github.com/maimai993/koishi-plugin-kkk" })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "font-semibold mb-1", style: { color: accentColor }, children: "QQ \u7FA4" }), (0, jsx_runtime_1.jsx)("p", { className: "text-xl", style: { color: secondaryColor }, children: data.report?.group ?? '1050229473' }), (0, jsx_runtime_1.jsx)("p", { className: "text-lg break-all opacity-80", style: { color: secondaryColor }, children: data.report?.groupUrl ?? 'https://qm.qq.com/q/1050229473' })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "font-semibold mb-1", style: { color: accentColor }, children: "\u9644\u5E26\u4FE1\u606F" }), (0, jsx_runtime_1.jsx)("p", { className: "text-xl", style: { color: secondaryColor }, children: "\u6B64\u56FE\u7247 + \u89E6\u53D1\u547D\u4EE4 + \u5BF9\u5E94\u914D\u7F6E\uFF08\u81EA\u884C\u8131\u654F\u5904\u7406\uFF09" })] }), data.report && ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "font-semibold mb-1", style: { color: accentColor }, children: "\u9519\u8BEF\u4E0A\u62A5\u7F16\u53F7" }), (0, jsx_runtime_1.jsx)("p", { className: "text-xl break-all font-mono", style: { color: secondaryColor }, children: data.report.id }), (0, jsx_runtime_1.jsx)("p", { className: "text-lg break-all opacity-80", style: { color: secondaryColor }, children: data.report.url })] }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-4 mt-6 text-xl", style: { color: mutedColor }, children: [(0, jsx_runtime_1.jsx)("span", { className: "font-mono", children: "Tips:" }), (0, jsx_runtime_1.jsx)("span", { children: "\u4FE1\u606F\u8D8A\u5B8C\u6574\uFF0C\u5B9A\u4F4D\u8D8A\u5FEB\u3002" })] })] })] })] })] }));
389
391
  };
390
392
  exports.handlerError = handlerError;
391
393
  exports.handlerError.displayName = 'handlerError';
package/lib/qqFields.json CHANGED
@@ -214,5 +214,15 @@
214
214
  "default": "bilibili",
215
215
  "label": "强制在线播放的适配器",
216
216
  "description": "填**适配器平台名**(多个用逗号分隔,例如 bilibili 或 bilibili,onebot),**默认就是 bilibili**;留空表示不强制任何适配器。填了之后,**从这些适配器进来的消息**解析完一律只给在线播放链接:视频不发出去、也不占群文件,跟用户在面板上点的是「解析」还是「在线看」、体积有没有超限都无关。典型用法:B站私聊机器人(adapter-bilibili-dm)的平台名就是 bilibili,它发不了视频文件,把它填进去即可。注意这里填的是**适配器**的名字,不是解析来源平台(抖音 / B站 这些由「发送内容」那些开关管)。要求「在线播放器总开关」开着。"
217
+ },
218
+ {
219
+ "key": "errorReportUpload",
220
+ "group": "通用",
221
+ "renderIn": "app",
222
+ "section": "错误上报",
223
+ "type": "boolean",
224
+ "default": true,
225
+ "label": "上传错误信息",
226
+ "description": "插件出错时,自动把**错误信息、运行环境(Koishi 版本、装了哪些插件、适配器)、最近的日志**上传到收集站,站长在网页上就能查到,不用你截图、也不用翻日志文件。**默认开启**。上传失败不影响报错本身(错误卡片照常发)。不想要就在这里关掉。"
217
227
  }
218
228
  ]