koishi-plugin-kkk 3.4.2 → 3.5.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/assets/web/assets/index-BRROK0vh.js +1 -1
  3. package/config/default_config/app.yaml +4 -6
  4. package/config/default_config/bilibili.yaml +1 -1
  5. package/config/default_config/config.json +2 -2
  6. package/lib/build-metadata.json +10 -0
  7. package/lib/compat/adapter-info.js +333 -0
  8. package/lib/compat/forward-collect.js +57 -4
  9. package/lib/compat/node-karin.js +25 -56
  10. package/lib/compat/sendError.js +27 -0
  11. package/lib/index.js +15 -8
  12. package/lib/karin/apps/help.js +23 -2
  13. package/lib/karin/module/utils/ErrorHandler/render.js +8 -2
  14. package/lib/karin/module/utils/InteractiveGraph.js +164 -0
  15. package/lib/karin/module/utils/InteractiveVideo.js +316 -0
  16. package/lib/karin/module/utils/ParseForward.js +12 -2
  17. package/lib/karin/module/utils/ParseSteps.js +1 -1
  18. package/lib/karin/module/utils/Render/index.js +10 -110
  19. package/lib/karin/module/utils/build-metadata.js +64 -19
  20. package/lib/karin/module/utils/runtime-report.js +16 -4
  21. package/lib/karin/platform/bilibili/bilibili.js +82 -5
  22. package/lib/karin/platform/bilibili/interactive-story.js +463 -0
  23. package/lib/ktr/registry.js +1 -0
  24. package/lib/ktr/template/bilibili/interactive/components/interactive.js +137 -0
  25. package/lib/ktr/template/bilibili/interactive/components/types.js +3 -0
  26. package/lib/ktr/template/bilibili/interactive/index.js +13 -0
  27. package/lib/ktr/template/other/handlerError/components/handlerError.js +1 -1
  28. package/lib/webui.js +40 -18
  29. package/package.json +1 -1
  30. package/scripts/build.mjs +36 -0
  31. package/scripts/patch-webui.mjs +126 -40
  32. package/scripts/probe-edge-cookie.cjs +50 -0
  33. package/scripts/probe-panel-options.cjs +15 -0
  34. package/scripts/probe-schema-options.cjs +12 -0
  35. package/scripts/smoke-adapter-info.cjs +149 -0
  36. package/scripts/smoke-config-schema.cjs +62 -0
  37. package/scripts/smoke-forward-chart.cjs +139 -0
  38. package/scripts/smoke-interactive-card.cjs +87 -0
  39. package/scripts/smoke-interactive-story.cjs +285 -0
  40. package/scripts/smoke-interactive.cjs +298 -0
  41. package/scripts/smoke-onebot-forward.cjs +10 -5
  42. package/scripts/smoke-panel-chart.cjs +92 -0
  43. package/scripts/smoke-parse-forward.cjs +1 -1
  44. package/scripts/smoke-render-path.cjs +44 -55
  45. package/src/compat/adapter-info.ts +346 -0
  46. package/src/compat/forward-collect.ts +69 -4
  47. package/src/compat/node-karin.ts +24 -54
  48. package/src/compat/sendError.ts +26 -0
  49. package/src/index.ts +15 -8
  50. package/src/karin/apps/help.ts +26 -3
  51. package/src/karin/module/utils/ErrorHandler/render.ts +8 -2
  52. package/src/karin/module/utils/InteractiveGraph.ts +199 -0
  53. package/src/karin/module/utils/InteractiveVideo.ts +400 -0
  54. package/src/karin/module/utils/ParseForward.ts +12 -3
  55. package/src/karin/module/utils/ParseSteps.ts +1 -1
  56. package/src/karin/module/utils/Render/index.ts +10 -113
  57. package/src/karin/module/utils/build-metadata.ts +61 -19
  58. package/src/karin/module/utils/runtime-report.ts +16 -4
  59. package/src/karin/platform/bilibili/bilibili.ts +81 -6
  60. package/src/karin/platform/bilibili/interactive-story.ts +519 -0
  61. package/src/karin/types/config/app.ts +7 -15
  62. package/src/karin/types/config/bilibili.ts +1 -1
  63. package/src/ktr/registry.ts +1 -0
  64. package/src/ktr/template/bilibili/interactive/components/interactive.tsx +202 -0
  65. package/src/ktr/template/bilibili/interactive/components/types.ts +37 -0
  66. package/src/ktr/template/bilibili/interactive/index.tsx +15 -0
  67. package/src/ktr/template/other/handlerError/components/handlerError.tsx +4 -1
  68. package/src/webui.ts +38 -16
  69. package/scripts/smoke-shotkit-compat.cjs +0 -102
@@ -379,7 +379,7 @@ 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: data.report
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 }), data.adapterInfo.version && ((0, jsx_runtime_1.jsxs)("p", { className: "text-2xl font-bold truncate", style: { color: mutedColor }, children: ["v", data.adapterInfo.version] }))] })] }))] }), (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
383
  ? '错误信息已上传,进群发送下面的上报编号或这张图片即可。'
384
384
  : '提交问题时请附上完整报错截图、复现步骤和环境版本信息。' })] }), (0, jsx_runtime_1.jsx)("span", { className: "text-xs font-black tracking-[0.2em] uppercase px-3 py-1 rounded-full", style: {
385
385
  color: primaryColor,
package/lib/webui.js CHANGED
@@ -303,6 +303,27 @@ function registerWebUi({ ctx, config, rawConfig, logger, pluginRoot }) {
303
303
  const match = new RegExp(COOKIE_NAME + '=([0-9a-f]+)').exec(cookie);
304
304
  return !!(match && tokens.has(match[1]));
305
305
  };
306
+ /**
307
+ * 请求有没有带**控制台发的面板令牌**。
308
+ *
309
+ * 这是「只能从控制台 iframe 进面板」的唯一凭据:令牌由 RPC \`kkk/panel-token\` 下发,
310
+ * 那个 RPC 走控制台登录态(authority 4),没登录控制台根本调不到 → 也就拿不到令牌。
311
+ * 这里**不看 webUiAuth,也不依赖宿主装没装 auth 插件**,所以任何部署下都堵得住。
312
+ */
313
+ const panelTokenOk = (request) => {
314
+ const panelCookie = new RegExp(COOKIE_NAME + '_panel=([0-9a-f]+)').exec(String(request?.headers?.cookie || ''));
315
+ if (panelCookie && panelTokenValid(panelCookie[1]))
316
+ return true;
317
+ const header = String(request?.headers?.authorization || request?.headers?.['x-access-token'] || '');
318
+ if (header && panelTokenValid(header.replace(/^Bearer\\s+/i, '')))
319
+ return true;
320
+ const url = String(request?.url || '');
321
+ const panel = /[?&]panel=([0-9a-f]+)/.exec(url);
322
+ if (panel && panelTokenValid(panel[1]))
323
+ return true;
324
+ const token = /[?&]token=([0-9a-f]+)/.exec(url);
325
+ return !!(token && panelTokenValid(token[1]));
326
+ };
306
327
  const ok = (response, data = null, message = '') => {
307
328
  response.type = 'application/json; charset=utf-8';
308
329
  response.body = JSON.stringify({ code: 200, data, message });
@@ -601,11 +622,16 @@ function registerWebUi({ ctx, config, rawConfig, logger, pluginRoot }) {
601
622
  * 这样未登录状态下连 SPA 的静态资源和 /kkk/assets/config 这种前端路由也拿不到。
602
623
  */
603
624
  const pageDenied = async (response) => {
625
+ /**
626
+ * 鉴权就用控制台登录态:装了 auth 插件(且 webUiAuth 开着)时认控制台 cookie,
627
+ * 没登录给 404。**额外加令牌校验会把控制台 iframe 也挡掉**(实测面板直接打不开),
628
+ * 按用户要求不做那层校验。
629
+ */
604
630
  if (!authRequired())
605
631
  return false;
606
- if (await consoleAuthed(response))
632
+ if (await consoleAuthed(response.request))
607
633
  return false;
608
- // 直接当成「没有这个页面」:面板只从控制台侧边栏进,独立 URL 不给任何提示
634
+ logger.warn('[kkk] 拒绝访问面板:需要先登录 Koishi 控制台');
609
635
  response.status = 404;
610
636
  response.type = 'text/plain; charset=utf-8';
611
637
  response.body = 'Not Found';
@@ -658,31 +684,27 @@ function registerWebUi({ ctx, config, rawConfig, logger, pluginRoot }) {
658
684
  response.set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
659
685
  response.body = file;
660
686
  });
661
- for (const route of ['/kkk', '/kkk/', '/kkk/login']) {
687
+ /**
688
+ * 面板页面:**只从控制台 iframe 进**(见 pageDenied 的说明)。
689
+ * 原来还有一条独立的 \`/kkk/login\` 兜底入口,按用户要求一并去掉 —— 它就是「单独放出来」的那条路。
690
+ */
691
+ for (const route of ['/kkk', '/kkk/']) {
662
692
  server.get(route, async (response) => {
663
693
  if (await pageDenied(response))
664
694
  return;
665
695
  rememberPanelToken(response);
696
+ // 只允许同源 iframe 嵌套:别人把面板嵌到自己站里也用不了
697
+ response.set('Content-Security-Policy', "frame-ancestors 'self'");
698
+ response.set('X-Frame-Options', 'SAMEORIGIN');
666
699
  response.type = 'text/html; charset=utf-8';
667
700
  response.body = indexHtml();
668
701
  });
669
702
  }
670
703
  /* ---------------- 简易编辑页(改插件配置) ---------------- */
671
- server.get('/kkk/edit', async (response) => {
672
- if (await pageDenied(response))
673
- return;
674
- let body;
675
- try {
676
- body = node_fs_1.default.readFileSync(node_path_1.default.join(pluginRoot, 'assets', 'webui.html'), 'utf-8');
677
- }
678
- catch {
679
- body = '<h1>kkk 配置</h1><p>assets/webui.html 缺失</p>';
680
- }
681
- const token = issueToken();
682
- response.set('Set-Cookie', COOKIE_NAME + '=' + token.accessToken + '; Path=/; HttpOnly; Max-Age=' + Math.floor(TOKEN_TTL / 1000));
683
- response.type = 'text/html; charset=utf-8';
684
- response.body = body;
685
- });
704
+ /**
705
+ * 原来的 `/kkk/edit` 是一个**独立的 HTML 配置编辑页**(assets/webui.html),
706
+ * 它绕开控制台、把配置表单单独放出来 —— 用户要求删掉,配置只从控制台里改。
707
+ */
686
708
  server.get('/kkk/api/config', (response) => {
687
709
  if (!authed(response))
688
710
  return fail(response, 401, '未登录');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-kkk",
3
- "version": "3.4.2",
3
+ "version": "3.5.0",
4
4
  "description": "抖音 / B站 / 快手 / 小红书 视频解析与动态推送,带 QQ 交互面板与弹幕烧录(karin-plugin-kkk 的 Koishi 移植版)",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
package/scripts/build.mjs CHANGED
@@ -8,6 +8,7 @@
8
8
  * - @kkk/richtext → 内置 richtext
9
9
  * - node-karin[/sub] → 兼容层实现(这样产物自带兼容层,不依赖 node_modules 里的转发包)
10
10
  */
11
+ import { execFileSync } from 'node:child_process'
11
12
  import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from 'node:fs'
12
13
  import path from 'node:path'
13
14
  import { fileURLToPath } from 'node:url'
@@ -118,7 +119,41 @@ const checkImportMeta = (dir) => {
118
119
  return offenders
119
120
  }
120
121
 
122
+ /**
123
+ * 写入构建元数据。
124
+ *
125
+ * 卡片上的「Built Time / Commit Hash」以前一直是空的:插件读的是 lib/build-metadata.json,
126
+ * 但没有任何地方生成它。构建时顺手写一份,随 lib 一起部署(也会被打进 npm 包里)。
127
+ */
128
+ const writeBuildMetadata = () => {
129
+ const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'))
130
+ let commitHash = ''
131
+ try {
132
+ commitHash = execFileSync('git', ['rev-parse', 'HEAD'], {
133
+ cwd: root,
134
+ encoding: 'utf8',
135
+ stdio: ['ignore', 'pipe', 'ignore']
136
+ }).trim()
137
+ } catch {
138
+ // 没装 git(或不在仓库里):CI 上通常给了 GITHUB_SHA
139
+ commitHash = String(process.env.GITHUB_SHA ?? '').trim()
140
+ }
141
+ const metadata = {
142
+ version: String(pkg.version ?? ''),
143
+ buildTime: new Date().toISOString(),
144
+ buildTimestamp: Date.now(),
145
+ name: String(pkg.name ?? ''),
146
+ description: String(pkg.description ?? ''),
147
+ homepage: String(pkg.homepage ?? ''),
148
+ commitHash,
149
+ shortCommitHash: commitHash.slice(0, 7)
150
+ }
151
+ writeFileSync(path.join(outDir, 'build-metadata.json'), JSON.stringify(metadata, null, 2) + '\n')
152
+ return metadata
153
+ }
154
+
121
155
  mkdirSync(outDir, { recursive: true })
156
+ const buildMetadata = writeBuildMetadata()
122
157
  const rewritten = rewriteAliases(outDir)
123
158
  const offenders = checkImportMeta(outDir)
124
159
  if (offenders.length) {
@@ -127,3 +162,4 @@ if (offenders.length) {
127
162
  process.exit(1)
128
163
  }
129
164
  console.log('构建完成:输出到 ' + path.relative(root, outDir) + ',重写别名文件 ' + rewritten + ' 个')
165
+ console.log('构建信息:v' + buildMetadata.version + ' · ' + (buildMetadata.shortCommitHash || '无 commit') + ' · ' + buildMetadata.buildTime)
@@ -146,42 +146,16 @@ const RENDERER_START = '/*KKK-RENDERER-START*/'
146
146
  const RENDERER_END = '/*KKK-RENDERER-END*/'
147
147
 
148
148
  /**
149
- * 「优先渲染器」这一项挂在「通用」分类下,**作为「渲染设置」的兄弟分组**插在它后面。
149
+ * 清理老版本注入过的「通用设置 → 优先渲染器」块。
150
150
  *
151
- * 存储位置是上游配置的 `app.renderer`(和 renderScale 同一个段),取值 'shotkit' | 'puppeteer'。
152
- * 面板里用下拉框(i = 该分类的选项渲染器),默认值放在上游 config.json / app.yaml 里,
153
- * 这里只负责把控件画出来。
154
- *
155
- * 文案里那句 https 的提醒是实测结论,不是猜测:预编译内核在 Windows 上拉不到 https 资源,
156
- * 而卡片里的封面、头像基本都是 https,选内核会缺图 —— 用户看到选项说明就知道该选哪个。
151
+ * 3.5.0 起插件只用浏览器渲染(不再支持 shotkit 内核),这个选项没有意义了 ——
152
+ * 注入代码删掉,但**保留这段清理**:已经装过的部署里那个块还躺在各自的前端包里,
153
+ * 不摘掉的话面板上会一直挂着一个早就失效的下拉框(改它也不会有任何效果)。
154
+ * 可反复执行(按标记切)。
157
155
  */
158
- const RENDERER_FIELD = 'i(' + arr(['app', 'renderer']) + ',' + q('优先渲染器') + ',' + q(
159
- '卡片优先用哪个渲染器渲染。两个渲染服务都在时会按这里选的走,缺一个就自动用另一个;',
160
- ) + ',['
161
- + '{label:' + q('shotkit 内核(默认)') + ',value:' + q('shotkit') + ',description:' + q(
162
- '不依赖浏览器,单张几十毫秒、内存低。注意:当前预编译内核在 Windows 上加载不了 https 资源,卡片里的远程封面、头像会缺图。',
163
- ) + '}'
164
- + ',{label:' + q('浏览器(Chrome / Edge)') + ',value:' + q('puppeteer') + ',description:' + q(
165
- '完整渲染、远程资源正常,单张通常 1 秒以上,需要浏览器渲染服务(koishi-plugin-puppeteer 或同类)。',
166
- ) + '}'
167
- + '],e=>e)'
168
-
169
- const RENDERER_CODE = 'o(' + q('通用设置') + ',(0,U.jsxs)(U.Fragment,{children:[' + RENDERER_FIELD + ']}))'
170
-
171
- /** 插到「渲染设置」这个分组后面(兄弟节点)。可重复执行:先按标记删上次插的,再插一次。 */
172
- function patchRendererSetting (text, name) {
173
- let out = stripMarkedWithComma(text, RENDERER_START, RENDERER_END)
174
- const at = out.indexOf('`渲染设置`')
175
- if (at < 0) return out
176
- let open = -1
177
- for (let i = at - 1; i >= 0 && i > at - 40; i--) {
178
- if (out[i] === '(') { open = i; break }
179
- }
180
- if (open < 0) return out
181
- const close = matchParen(out, open)
182
- if (close < 0) return out
183
- out = out.slice(0, close + 1) + ',' + RENDERER_START + RENDERER_CODE + RENDERER_END + out.slice(close + 1)
184
- console.log('[kkk] 「通用设置 → 优先渲染器」已注入到「渲染设置」之后: ' + name)
156
+ function stripLegacyRendererSetting (text, name) {
157
+ const out = stripMarkedWithComma(text, RENDERER_START, RENDERER_END)
158
+ if (out !== text) console.log('[kkk] 已摘掉面板里过期的「优先渲染器」分组: ' + name)
185
159
  return out
186
160
  }
187
161
 
@@ -784,12 +758,23 @@ function applyTextReplacements (text, name) {
784
758
  const FORWARD_START = '/*KKK-FORWARD-START*/'
785
759
  const FORWARD_END = '/*KKK-FORWARD-END*/'
786
760
 
761
+ const forwardOption = ([value, label]) => '{value:' + q(value) + ',label:' + q(label) + '}'
787
762
  const FORWARD_OPTIONS = '[' + [['text', '文字'], ['image', '图片'], ['video', '视频'], ['file', '文件']]
788
- .map(([value, label]) => '{value:' + q(value) + ',label:' + q(label) + '}').join(',') + ']'
763
+ .map(forwardOption).join(',') + ']'
764
+ /**
765
+ * 「流程图」只有 B站互动视频有(它是剧情图,本身是图片但可以单独控制),
766
+ * 所以只补在 **B站** 和 **通用(全局)** 这两处候选里:抖音 / 快手 / 小红书 加了也是永远匹配不到的假选项,
767
+ * 跟当初删掉「语音 / markdown」是同一个理由(见 ParseForward 的说明)。
768
+ */
769
+ const FORWARD_OPTIONS_CHART = '[' + [['text', '文字'], ['image', '图片'], ['video', '视频'], ['file', '文件'], ['chart', '流程图']]
770
+ .map(forwardOption).join(',') + ']'
789
771
 
790
772
  const FORWARD_SWITCH_DESC = '本平台单独打开合并转发。注意全局优先:通用里的「解析结果合并转发」打开时所有平台都会合并,这个开关开不开都一样;只有全局关着时它才起作用。默认关闭。'
791
773
  const FORWARD_CONTENT_DESC = '合并转发里放哪些内容:没勾的会单独发出去、不进聊天记录。留空表示用通用里那份全局设置。视频体积大时有些适配器(比如 NapCat)会拒绝整条聊天记录,这时会自动改成单独发送,不会丢内容。'
792
- const FORWARD_GLOBAL_CONTENT_DESC = '全局合并转发里放哪些内容(通用页那个开关打开时生效,所有平台共用):没勾的单独发。视频建议先不勾,聊天记录太大时适配器会整条拒绝。语音和 markdown 不在候选里:QQ 的聊天记录不支持语音气泡,markdown 只有官方 bot 认、而官方适配器没有合并转发能力。'
774
+ const FORWARD_GLOBAL_CONTENT_DESC = '全局合并转发里放哪些内容(通用页那个开关打开时生效,所有平台共用):没勾的单独发。视频建议先不勾,聊天记录太大时适配器会整条拒绝。语音和 markdown 不在候选里:QQ 的聊天记录不支持语音气泡,markdown 只有官方 bot 认、而官方适配器没有合并转发能力。「流程图」是 B站互动视频的剧情图,勾上才会进聊天记录。'
775
+
776
+ /** B站那一栏多说一句:流程图只有 B站有(别的平台页里连候选项都没有) */
777
+ const FORWARD_CONTENT_DESC_BILIBILI = FORWARD_CONTENT_DESC + '「流程图」是互动视频的剧情图:勾上就进聊天记录,不勾就单独发出去。'
793
778
 
794
779
  /** 平台页的锚点:紧跟在「解析开关」那一项之后插入 */
795
780
  const FORWARD_TABS = [
@@ -812,10 +797,13 @@ function insertAfterCall (text, needle, code) {
812
797
 
813
798
  /** 平台页里那一段:小标题 + 本平台开关 + 本平台内容多选 */
814
799
  function forwardSectionCode (platform, label) {
800
+ /** B站那一栏多一个「流程图」候选(只有它有互动视频剧情图) */
801
+ const options = platform === 'bilibili' ? FORWARD_OPTIONS_CHART : FORWARD_OPTIONS
802
+ const description = platform === 'bilibili' ? FORWARD_CONTENT_DESC_BILIBILI : FORWARD_CONTENT_DESC
815
803
  return 't.renderSubSection(' + q('合并转发') + ',(0,U.jsxs)(U.Fragment,{children:['
816
804
  // 分类名已经写着平台名了(哔哩哔哩页里再写一遍「合并转发(哔哩哔哩)」是重复)
817
805
  + 't.renderSwitch(' + arr([platform, 'forward']) + ',' + q('合并转发') + ',' + q(FORWARD_SWITCH_DESC) + ')'
818
- + ',t.renderCheckboxGroup(' + arr([platform, 'forwardContent']) + ',' + q('合并转发内容') + ',' + q(FORWARD_CONTENT_DESC) + ',' + FORWARD_OPTIONS + ')'
806
+ + ',t.renderCheckboxGroup(' + arr([platform, 'forwardContent']) + ',' + q('合并转发内容') + ',' + q(description) + ',' + options + ')'
819
807
  + ']}))'
820
808
  }
821
809
 
@@ -827,7 +815,7 @@ function patchForwardFields (text, name) {
827
815
  // ① 通用页:全局合并内容(开关本身是原版就有的 app.fakeForward)
828
816
  out = insertAfterCall(out, arr(['app', 'fakeForward']) + ',' + q('解析结果合并转发'),
829
817
  FORWARD_START + 't.renderCheckboxGroup(' + arr(['app', 'forwardContent']) + ',' + q('合并转发内容(全局)')
830
- + ',' + q(FORWARD_GLOBAL_CONTENT_DESC) + ',' + FORWARD_OPTIONS + ')' + FORWARD_END)
818
+ + ',' + q(FORWARD_GLOBAL_CONTENT_DESC) + ',' + FORWARD_OPTIONS_CHART + ')' + FORWARD_END)
831
819
 
832
820
  // ② 平台页:本平台开关 + 本平台内容
833
821
  for (const [platform, label, anchor] of FORWARD_TABS) {
@@ -887,9 +875,11 @@ for (const file of files) {
887
875
  // 必须在 patchPermFields 之后:那个函数会先剥掉旧的 APP 标记块再重写「交互设置」里的字段,
888
876
  // 我们的分组要插在「交互设置」**之后**,顺序反了会插到它里面去(就是这次要修的样式问题)
889
877
  text = patchAppSections(text, name)
890
- // 同样必须晚于 patchPermFields / patchAppSections:它锚定的是「渲染设置」的收尾括号
891
- text = patchRendererSetting(text, name)
878
+ // 老版本在这里注入过「优先渲染器」分组:3.5.0 起不再支持 shotkit 内核,只剩摘除
879
+ text = stripLegacyRendererSetting(text, name)
892
880
  text = patchForwardFields(text, name)
881
+ text = patchChartOption(text, name)
882
+ text = patchChartOptionList(text, name)
893
883
  text = patchDescriptions(text, name)
894
884
  text = applyTextReplacements(text, name)
895
885
 
@@ -897,4 +887,100 @@ for (const file of files) {
897
887
  if (text.length !== before || before !== fs.statSync(file).size || !isBundle) console.log('[kkk] 已处理: ' + name + '(' + before + ' → ' + text.length + ' 字节)')
898
888
  }
899
889
 
890
+
891
+ /* ---------------- 解析时发送的内容:补上「流程图」勾选项 ---------------- */
892
+
893
+ /**
894
+ * 面板里「解析时发送的内容」的候选项是**面板自己写死的一份列表**(打包进前端包里了),
895
+ * 不是按配置里的数组值渲染的 —— 所以新加的 `chart` 对一个已经存在的安装永远不会自己冒出来,
896
+ * 必须把选项塞进控件本身。
897
+ *
898
+ * ⚠️ 这份列表在四个平台页之间是**共用变量**( `jL` = 信息 / 评论图 / 视频 / 图片 ),
899
+ * B站那一栏是 `jL.filter(e=>e.value!==\`image\`)` 过滤出来的。上一版补丁按
900
+ * 「B站 sendContent 后面那个 `[` 开头的选项数组」去找,命中的却是**另一处**同名路径
901
+ * (判定表 / 补全表里的字符串数组),检查不通过就静默 return —— 于是面板里一直**没有**
902
+ * 「流程图」这个勾选项(控制台那边有,因为它读的是 YAML 注释)。
903
+ *
904
+ * 现在改成按「B站那一栏自己的渲染调用」定位:路径 + 标题「解析时发送的内容」,
905
+ * 再把它的第 4 个参数(选项数组)追加一项。
906
+ * 幂等:带上 /*KKK-CHART*\/ 标记,补过一次就不再补;万一找不到控件,**大声报警**而不是静默跳过。
907
+ */
908
+ /** 按「顶层逗号」切分一段参数列表(字符串与括号里的逗号不算) */
909
+ function splitTopLevel (code) {
910
+ const out = []
911
+ let current = ''
912
+ let depth = 0
913
+ let quote = ''
914
+ for (let i = 0; i < code.length; i++) {
915
+ const ch = code[i]
916
+ if (quote) {
917
+ current += ch
918
+ if (ch === '\\') { current += code[++i] ?? ''; continue }
919
+ if (ch === quote) quote = ''
920
+ continue
921
+ }
922
+ if (ch === BT || ch === '"' || ch === "'") { quote = ch; current += ch; continue }
923
+ if (ch === '(' || ch === '[' || ch === '{') depth++
924
+ else if (ch === ')' || ch === ']' || ch === '}') depth--
925
+ if (ch === ',' && depth === 0) { out.push(current); current = ''; continue }
926
+ current += ch
927
+ }
928
+ if (current.trim()) out.push(current)
929
+ return out
930
+ }
931
+
932
+ function patchChartOption (text, name) {
933
+ /**
934
+ * 补丁标记写在这里而不是模块顶层:主循环在本文件靠前的位置就跑完了,
935
+ * 顶层 `const` 那时候还在时间死区(TDZ)里 —— 函数声明倒是已经提升好了。
936
+ */
937
+ const CHART_MARK = '/*KKK-CHART*/'
938
+ if (text.includes(CHART_MARK)) return text
939
+ const option = '{value:' + q('chart') + ',label:' + q('流程图') + '}'
940
+ /** 锚点 = B站那一栏的渲染调用(第一个参数是路径,第二个是标题) */
941
+ const anchor = arr(['bilibili', 'sendContent']) + ',' + q('解析时发送的内容')
942
+ const at = text.indexOf(anchor)
943
+ if (at < 0) {
944
+ // 只有主包才该有这个控件:布局 / 主题 / index.html 里没有是正常的,别刷屏
945
+ if (text.includes('sendContent')) console.warn('[kkk] ' + name + ':没找到 B站「解析时发送的内容」控件,流程图勾选项没补上(前端包可能换版本了)')
946
+ return text
947
+ }
948
+ const open = text.lastIndexOf('(', at)
949
+ if (open < 0) return text
950
+ const close = matchParen(text, open)
951
+ if (close < 0) return text
952
+ const args = splitTopLevel(text.slice(open + 1, close))
953
+ // 参数顺序:路径 / 标题 / 说明 / 选项数组 / 是否禁用
954
+ if (args.length < 4) return text
955
+ const options = args[3]
956
+ /**
957
+ * 认一下:这个参数确实是「候选项数组」。
958
+ * 两种写法都要认:写死的数组(老版本包,里面有 info / video 这些字面量),
959
+ * 或者**过滤共用列表**(当前版本:\`jL.filter(e=>e.value!==\`image\`)\` —— 里面一个候选项名都没有,
960
+ * 上一版就是死在这一步:守着字面量、认不出表达式,于是又静默跳过了)。
961
+ */
962
+ if (!options.startsWith('[') && !/\b\w+\s*\.\s*filter\s*\(/.test(options)) return text
963
+ if (/chart/.test(options)) return text
964
+ args[3] = options.startsWith('[')
965
+ // 选项数组写死在这一处(老版本包)→ 直接补在数组里
966
+ ? options.slice(0, -1) + ',' + CHART_MARK + option + ']'
967
+ // 共用列表过滤出来的(当前版本:jL.filter(…))→ 追加一项
968
+ : options + '.concat([' + CHART_MARK + option + '])'
969
+ console.log('[kkk] 「解析时发送的内容」已补上「流程图」勾选项: ' + name)
970
+ return text.slice(0, open) + '(' + args.join(',') + ')' + text.slice(close + 1)
971
+ }
972
+
973
+ /**
974
+ * 面板还有一份**值白名单**(补全表):`{path:["bilibili","sendContent"],options:[…]}`。
975
+ * 保存前面板会拿它把配置里的数组洗一遍 —— **不在表里的值会被丢掉**。
976
+ * 所以 `chart` 也必须补进这张表,否则勾上「流程图」一保存就被面板自己清掉了。
977
+ */
978
+ function patchChartOptionList (text, name) {
979
+ const B = String.fromCharCode(96)
980
+ const before = '{path:[' + B + 'bilibili' + B + ',' + B + 'sendContent' + B + '],options:[' + B + 'info' + B + ',' + B + 'comment' + B + ',' + B + 'video' + B + ',' + B + 'image' + B + ']}'
981
+ const after = '{path:[' + B + 'bilibili' + B + ',' + B + 'sendContent' + B + '],options:[' + B + 'info' + B + ',' + B + 'comment' + B + ',' + B + 'video' + B + ',' + B + 'image' + B + ',' + B + 'chart' + B + ']}'
982
+ if (!text.includes(before)) return text
983
+ console.log('[kkk] 「解析时发送的内容」候选项已补上 chart: ' + name)
984
+ return text.split(before).join(after)
985
+ }
900
986
  console.log('[kkk] 完成,共处理 ' + files.length + ' 个文件')
@@ -0,0 +1,50 @@
1
+ const fs = require('node:fs')
2
+ const path = require('node:path')
3
+ const pluginRoot = path.resolve(__dirname, '..')
4
+ const libRoot = path.join(pluginRoot, 'lib')
5
+ const runtime = require(path.join(libRoot, 'compat', 'runtime.js'))
6
+ const noop = () => {}
7
+ runtime.bindRuntime({ ctx: { get: () => undefined, logger: () => ({ info: noop, warn: noop, error: noop, debug: noop, mark: noop }), bots: [], registry: new Map(), on: noop, middleware: noop }, config: { app: {} }, dataRoot: path.join(pluginRoot, 'data-probe-node'), pluginRoot, master: () => [] })
8
+ const { Networks } = require(path.join(libRoot, 'karin', 'module', 'utils', 'Network', 'index.js'))
9
+ const cfgPath = 'C:\\sj\\koishi\\data\\koishi-plugin-kkk\\config\\config.json'
10
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'))
11
+ const cookie = String(cfg.amagi?.cookies?.bilibili ?? '')
12
+ console.log('生产 Cookie: ' + (cookie ? '有(长度 ' + cookie.length + ',含 SESSDATA=' + /SESSDATA=/.test(cookie) + ')' : '无'))
13
+ const bvid = 'BV1xDgL6SEzk'
14
+ const rootCid = 41156151510
15
+ const edgeId = 47666682
16
+ const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36'
17
+ const baseHeaders = (withCookie) => ({ 'User-Agent': UA, Referer: 'https://www.bilibili.com/', ...(withCookie && cookie ? { Cookie: cookie } : {}) })
18
+ const urls = {
19
+ player: 'https://api.bilibili.com/x/player/wbi/v2?bvid=' + bvid + '&cid=' + rootCid,
20
+ edge: 'https://api.bilibili.com/x/stein/edgeinfo_v2?bvid=' + bvid + '&graph_version=1722536&cid=' + rootCid + '&edge_id=' + edgeId,
21
+ edgeNoEdge: 'https://api.bilibili.com/x/stein/edgeinfo_v2?bvid=' + bvid + '&graph_version=1722536&cid=' + rootCid
22
+ }
23
+ const summarize = (payload) => {
24
+ if (payload === false) return 'Networks 返回 false(请求本身失败)'
25
+ if (payload === null) return 'Networks 返回 null'
26
+ if (typeof payload !== 'object') return '类型 ' + typeof payload
27
+ const data = payload.data ?? {}
28
+ const questions = data.edges?.questions ?? []
29
+ return 'code=' + payload.code + ' message=' + JSON.stringify(payload.message) + ' 有 data=' + !!payload.data + ' 题目数=' + questions.length
30
+ }
31
+ const main = async () => {
32
+ for (const withCookie of [true, false]) {
33
+ console.log('')
34
+ console.log('=== ' + (withCookie ? '带生产 Cookie' : '匿名(不带 Cookie)') + ' ===')
35
+ for (const [name, url] of Object.entries(urls)) {
36
+ try {
37
+ const payload = await new Networks({ url, headers: baseHeaders(withCookie), timeout: 15000 }).getData()
38
+ console.log('Networks ' + name + ': ' + summarize(payload))
39
+ } catch (error) { console.log('Networks ' + name + ' 抛错: ' + String(error?.message ?? error)) }
40
+ try {
41
+ const response = await fetch(url, { headers: baseHeaders(withCookie) })
42
+ const text = await response.text()
43
+ let parsed = null
44
+ try { parsed = JSON.parse(text) } catch {}
45
+ console.log('fetch ' + name + ': HTTP ' + response.status + ' ' + (parsed ? 'code=' + parsed.code + ' message=' + JSON.stringify(parsed.message) : '非 JSON: ' + text.slice(0, 80)))
46
+ } catch (error) { console.log('fetch ' + name + ' 抛错: ' + String(error?.message ?? error)) }
47
+ }
48
+ }
49
+ }
50
+ main().catch((error) => { console.log('探针失败: ' + (error && error.stack ? error.stack : error)); process.exit(1) })
@@ -0,0 +1,15 @@
1
+ const fs = require('fs')
2
+ const file = process.argv[2]
3
+ const text = fs.readFileSync(file, 'utf8')
4
+ const keys = ['评论图', '解析时发送的内容', 'sendContent']
5
+ for (const key of keys) {
6
+ let from = 0, count = 0
7
+ while (count < 4) {
8
+ const at = text.indexOf(key, from)
9
+ if (at < 0) break
10
+ console.log('--- ' + key + ' @' + at + ' ---')
11
+ console.log(text.slice(Math.max(0, at - 240), at + 240).replace(/\n/g, ' '))
12
+ from = at + key.length
13
+ count++
14
+ }
15
+ }
@@ -0,0 +1,12 @@
1
+ const fs = require('fs')
2
+ const text = fs.readFileSync(process.argv[2], 'utf8')
3
+ for (const key of ['sendContent', 'default_config', 'yaml', 'YAML', 'extractOptions(']) {
4
+ let from = 0, n = 0
5
+ while (n < 3) {
6
+ const at = text.indexOf(key, from)
7
+ if (at < 0) break
8
+ console.log('--- ' + key + ' @' + at + ' ---')
9
+ console.log(text.slice(Math.max(0, at - 200), at + 200).replace(/\n/g, ' '))
10
+ from = at + key.length; n++
11
+ }
12
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * 适配器信息 / 构建信息冒烟测试。
3
+ *
4
+ * 两件线上被用户发现的事:
5
+ * 1. 卡片上「当前适配器」永远是一个空的 v —— Koishi 的 Bot 根本没有 version 字段,
6
+ * 以前读 bot.adapter.version 只会得到空串。
7
+ * 2. 卡片上的「Built Time / Commit Hash」一直空着 —— 没有任何地方生成 lib/build-metadata.json。
8
+ *
9
+ * 这个测试直接跑编译产物:造一个真的(临时)适配器包,验证构造函数反查、包名兜底、
10
+ * 实现端版本查询和构建元数据读取这几条链路。
11
+ *
12
+ * 用法:node scripts/smoke-adapter-info.cjs
13
+ */
14
+ const fs = require('node:fs')
15
+ const os = require('node:os')
16
+ const path = require('node:path')
17
+
18
+ const pluginRoot = path.resolve(__dirname, '..')
19
+ const libRoot = path.join(pluginRoot, 'lib')
20
+
21
+ const failures = []
22
+ let passed = 0
23
+ const check = (name, condition, detail) => {
24
+ if (condition) {
25
+ passed++
26
+ console.log(' ✓ ' + name)
27
+ } else {
28
+ failures.push(name + (detail ? ' → ' + detail : ''))
29
+ console.log(' ✗ ' + name + (detail ? ' → ' + detail : ''))
30
+ }
31
+ }
32
+
33
+ const noop = () => {}
34
+ /** 有些模块顶层会读运行时(配置 / 日志),先绑一个最小实现 */
35
+ const runtime = require(path.join(libRoot, 'compat', 'runtime.js'))
36
+ runtime.bindRuntime({
37
+ ctx: {
38
+ get: () => undefined,
39
+ logger: () => ({ info: noop, warn: noop, error: noop, debug: noop, mark: noop }),
40
+ bots: [],
41
+ registry: new Map(),
42
+ on: noop,
43
+ middleware: noop
44
+ },
45
+ config: { app: {} },
46
+ dataRoot: path.join(pluginRoot, 'data-smoke-adapter-info'),
47
+ pluginRoot,
48
+ master: () => []
49
+ })
50
+
51
+ /** 造一个假适配器包,require 之后它自然就进了 require.cache —— 正是反查要靠的东西 */
52
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'kkk-adapter-info-'))
53
+ const fakeDir = path.join(tmpRoot, 'node_modules', 'koishi-plugin-adapter-smoke')
54
+ fs.mkdirSync(path.join(fakeDir, 'lib'), { recursive: true })
55
+ fs.writeFileSync(path.join(fakeDir, 'package.json'), JSON.stringify({ name: 'koishi-plugin-adapter-smoke', version: '9.9.9' }))
56
+ fs.writeFileSync(path.join(fakeDir, 'lib', 'index.js'), 'class SmokeAdapter {}\nmodule.exports = { Adapter: SmokeAdapter, SmokeAdapter }\n')
57
+ const fake = require(path.join(fakeDir, 'lib', 'index.js'))
58
+
59
+ const adapterInfo = require(path.join(libRoot, 'compat', 'adapter-info.js'))
60
+ const { resolveAdapterInfo, queryAdapterImplementation, cachedAdapterImplementation } = adapterInfo
61
+
62
+ console.log('== 适配器信息 ==')
63
+ const bot = {
64
+ platform: 'smoke',
65
+ selfId: '10001',
66
+ status: 1,
67
+ config: { protocol: 'ws' },
68
+ adapter: { constructor: fake.SmokeAdapter },
69
+ internal: {}
70
+ }
71
+ const info = resolveAdapterInfo(bot)
72
+ check('按 Adapter 构造函数反查到适配器插件版本', info.version === '9.9.9', 'version=' + info.version)
73
+ check('带上适配器包名', info.packageName === 'koishi-plugin-adapter-smoke', info.packageName)
74
+ check('通信方式按协议翻译(ws → WebSocket)', info.communication === 'WebSocket', info.communication)
75
+ check('记录机器人上线时间(connectTime > 0)', info.connectTime > 0, String(info.connectTime))
76
+ check('身份字段保持原样(name = 平台名)', info.name === 'smoke' && info.protocol === 'smoke' && info.standard === 'smoke', info.name + '/' + info.protocol)
77
+ check('友好名单独放在 displayName', info.displayName === 'smoke', info.displayName)
78
+ check('结果被缓存,重复调用不重复扫目录', resolveAdapterInfo(bot).version === '9.9.9')
79
+ check('没有实现端信息时不编造名字', cachedAdapterImplementation(bot) === undefined, String(cachedAdapterImplementation(bot)))
80
+
81
+ console.log('== 平台兜底 ==')
82
+ const onebot = resolveAdapterInfo({ platform: 'onebot', selfId: '10002', status: 1, adapter: {}, config: { protocol: 'http' } })
83
+ check('OneBot 的友好名是 OneBot', onebot.displayName === 'OneBot', onebot.displayName)
84
+ check('OneBot 的身份字段仍然是平台名(面板/合并转发靠它判断)', onebot.name === 'onebot' && onebot.protocol === 'onebot', onebot.name + '/' + onebot.protocol)
85
+ const qqguild = resolveAdapterInfo({ platform: 'qqguild', selfId: '10007', status: 1, adapter: {}, config: {} })
86
+ check('QQ 频道这类平台名不会被改成中文(includes 判断必须继续生效)', qqguild.name === 'qqguild', qqguild.name)
87
+ check('中文友好名只出现在 displayName', qqguild.displayName === 'QQ 频道', qqguild.displayName)
88
+ check('HTTP 协议翻译', onebot.communication === 'HTTP', onebot.communication)
89
+ if (onebot.version === '未知') {
90
+ console.log(' · 跳过:这台机器没装 koishi-plugin-adapter-onebot,包名兜底无从验证')
91
+ } else {
92
+ check('按平台名兜底找到适配器包版本', /^\d+\.\d+\.\d+/.test(onebot.version), onebot.version)
93
+ check('兜底也能拿到包名', onebot.packageName === 'koishi-plugin-adapter-onebot', onebot.packageName)
94
+ }
95
+
96
+ console.log('== 离线机器人 ==')
97
+ const offline = resolveAdapterInfo({ platform: 'onebot', selfId: '10003', status: 0, adapter: {}, config: {} })
98
+ check('离线时 connectTime 是 0(不编造上线时间)', offline.connectTime === 0, String(offline.connectTime))
99
+
100
+ console.log('== 构建信息 ==')
101
+ const buildMetadata = require(path.join(libRoot, 'karin', 'module', 'utils', 'build-metadata.js'))
102
+ const pkg = JSON.parse(fs.readFileSync(path.join(pluginRoot, 'package.json'), 'utf8'))
103
+ const meta = buildMetadata.getBuildMetadata()
104
+ check('lib/build-metadata.json 存在', fs.existsSync(path.join(libRoot, 'build-metadata.json')))
105
+ check('构建信息不为空(旧安装包也会退回 package.json)', !!meta)
106
+ check('构建信息版本与 package.json 一致', meta.version === pkg.version, meta.version + ' vs ' + pkg.version)
107
+ check('构建信息带 commit', /^[0-9a-f]{7,}$/.test(meta.shortCommitHash), meta.shortCommitHash)
108
+ check('构建时间能格式化', buildMetadata.formatBuildTime(meta.buildTime).length > 0, buildMetadata.formatBuildTime(meta.buildTime))
109
+ check('空构建时间不会被格式化成 NaN', buildMetadata.formatBuildTime('') === '', buildMetadata.formatBuildTime(''))
110
+
111
+ const main = async () => {
112
+ console.log('== 实现端版本(OneBot get_version_info)==')
113
+ const napcat = await queryAdapterImplementation({
114
+ platform: 'onebot',
115
+ selfId: '10004',
116
+ internal: { get_version_info: async () => ({ app_name: 'NapCat', app_version: '4.8.1', protocol_version: 'v11' }) }
117
+ })
118
+ check('问到实现端名字与版本', napcat && napcat.name === 'NapCat' && napcat.version === '4.8.1', JSON.stringify(napcat))
119
+ const enriched = resolveAdapterInfo({
120
+ platform: 'onebot',
121
+ selfId: '10004',
122
+ status: 1,
123
+ adapter: {},
124
+ config: {}
125
+ })
126
+ check('实现端信息优先展示在卡片上(displayName)', enriched.displayName === 'NapCat' && enriched.version === '4.8.1', enriched.displayName + ' v' + enriched.version)
127
+ check('实现端信息不覆盖平台身份字段', enriched.name === 'onebot', enriched.name)
128
+ const noApi = await queryAdapterImplementation({ platform: 'x', selfId: '10005', internal: {} })
129
+ check('适配器没实现该接口时安静返回 null', noApi === null, String(noApi))
130
+ const broken = await queryAdapterImplementation({
131
+ platform: 'x',
132
+ selfId: '10006',
133
+ internal: { get_version_info: async () => { throw new Error('nope') } }
134
+ })
135
+ check('接口报错时安静返回 null', broken === null, String(broken))
136
+
137
+ console.log('')
138
+ console.log('通过 ' + passed + ' 项,失败 ' + failures.length + ' 项')
139
+ if (failures.length === 0) {
140
+ console.log('=== 通过:适配器版本与构建信息都能拿到 ===')
141
+ process.exit(0)
142
+ } else {
143
+ for (const item of failures) console.log(' ❌ ' + item)
144
+ console.log('=== 失败 ===')
145
+ process.exit(1)
146
+ }
147
+ }
148
+
149
+ main()