koishi-plugin-group-analysis-mx 1.2.8 → 1.3.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.
@@ -76,42 +76,34 @@ class RendererService extends koishi_1.Service {
76
76
  async injectEmojiFont(html) {
77
77
  if (!html || typeof html !== 'string')
78
78
  return html;
79
- let emojiCss = '';
80
- try {
81
- emojiCss = await this.buildEmojiFontCss();
82
- }
83
- catch (e) {
84
- this.ctx.logger.warn('构建 emoji 字体 CSS 失败:', e);
85
- }
86
- const style = `<style>${emojiCss}html,body{font-family:-apple-system,'Segoe UI','Segoe UI Emoji','NotoColorEmoji','Noto Sans SC','Noto Color Emoji','Apple Color Emoji','Microsoft YaHei','Hiragino Sans GB',sans-serif !important;}</style>`;
79
+ const emojiCss = this.buildEmojiFontCss();
80
+ const style = `<style>${emojiCss}html,body{font-family:-apple-system,'Segoe UI','Segoe UI Emoji','Noto Color Emoji','NotoEmoji','Apple Color Emoji','Microsoft YaHei','Hiragino Sans GB',sans-serif !important;}</style>`;
87
81
  if (html.includes('</head>'))
88
82
  return html.replace('</head>', style + '</head>');
89
83
  return html;
90
84
  }
91
- /** 读取内嵌的 Noto Color Emoji 字体分片并生成 @font-face(base64 内联,跨环境可靠)。 */
92
- async buildEmojiFontCss() {
93
- if (_emojiFontCssCache)
94
- return _emojiFontCssCache;
95
- const dir = __dirname + '/../../resources/fonts';
96
- let css = '';
97
- for (let i = 0; i < EMOJI_RANGES.length; i++) {
98
- const file = path_1.default.join(dir, `${String(i).padStart(2, '0')}.woff2`);
99
- try {
100
- const buf = await fs_1.promises.readFile(file);
101
- const b64 = buf.toString('base64');
102
- css += `@font-face{font-family:'NotoColorEmoji';font-style:normal;font-weight:400;src:url(data:font/woff2;base64,${b64}) format('woff2');unicode-range:${EMOJI_RANGES[i]};}`;
103
- }
104
- catch (e) {
105
- this.ctx.logger.warn(`加载 emoji 字体分片 ${i} 失败: ${e}`);
106
- }
107
- }
108
- _emojiFontCssCache = css;
109
- return css;
85
+ /**
86
+ * 生成引用同层 fonts/NotoEmoji.ttf 的 @font-face(单文件 ttf,Chromium 稳定加载)。
87
+ * 注意:必须与渲染 HTML 处于同一目录树(HTML 在 <skin>/,字体在 <skin>/fonts/),跨目录 file:// 加载会被 Chromium 拒绝。
88
+ */
89
+ buildEmojiFontCss() {
90
+ return `@font-face{font-family:'NotoEmoji';font-style:normal;font-weight:400;src:url('fonts/NotoEmoji.ttf') format('truetype');}`;
110
91
  }
111
92
  async init() {
112
93
  const resourcesDir = __dirname + '/../../resources';
113
94
  const templateDir = this.templateDir;
114
95
  const skin = this.config.skin || 'md3';
96
+ // 拷贝内嵌的 emoji 字体到当前皮肤目录的 fonts/ 子目录(与渲染 HTML 同层;跨目录 file:// 加载会被 Chromium 拒绝)
97
+ const skinFontsDir = ORIGINAL_SKINS.includes(skin)
98
+ ? path_1.default.resolve(templateDir, 'original', skin, 'fonts')
99
+ : path_1.default.resolve(templateDir, skin, 'fonts');
100
+ await fs_1.promises.mkdir(skinFontsDir, { recursive: true });
101
+ await fs_1.promises.cp(path_1.default.resolve(resourcesDir, 'fonts'), skinFontsDir, { recursive: true });
102
+ // 创建空 HTML 占位:渲染时先 goto 它(建立 file:// base),再 setContent 注入内容,@font-face 相对字体才能被加载
103
+ const emptyHtmlPath = ORIGINAL_SKINS.includes(skin)
104
+ ? path_1.default.resolve(templateDir, 'original', skin, 'emptyHtml.html')
105
+ : path_1.default.resolve(templateDir, skin, 'emptyHtml.html');
106
+ await fs_1.promises.writeFile(emptyHtmlPath, '');
115
107
  // 拷贝原项目皮肤模板(nunjucks 渲染用)
116
108
  const originalSourceDir = path_1.default.resolve(resourcesDir, 'original');
117
109
  const originalDestDir = path_1.default.resolve(templateDir, 'original');
@@ -156,6 +148,13 @@ class RendererService extends koishi_1.Service {
156
148
  }
157
149
  }, 3 * 60 * 1000);
158
150
  }
151
+ /** 当前皮肤目录下用于建立 file:// base 的空 HTML 占位路径。 */
152
+ emptyHtmlPath() {
153
+ const skin = this.config.skin || 'md3';
154
+ return ORIGINAL_SKINS.includes(skin)
155
+ ? path_1.default.resolve(this.templateDir, 'original', skin, 'emptyHtml.html')
156
+ : path_1.default.resolve(this.templateDir, skin, 'emptyHtml.html');
157
+ }
159
158
  async renderGroupAnalysisToPdf(data) {
160
159
  let theme = this.config.theme;
161
160
  if (theme === 'auto') {
@@ -241,8 +240,11 @@ class RendererService extends koishi_1.Service {
241
240
  await fs_1.promises.writeFile(outTemplateHtmlPath, await this.injectEmojiFont(filledHtml));
242
241
  this.ctx.logger.info('HTML 模板填充完成,正在调用 Puppeteer 进行渲染...');
243
242
  const page = await this.ctx.puppeteer.page();
244
- // 重新加载页面并使用 goto 访问本地文件
245
- await page.goto('file://' + outTemplateHtmlPath, {
243
+ // goto 空 HTML 建立 file:// base,再 setContent 注入内容(@font-face 相对字体才能被加载)
244
+ await page.goto('file://' + this.emptyHtmlPath(), {
245
+ waitUntil: 'domcontentloaded'
246
+ });
247
+ await page.setContent(await this.injectEmojiFont(filledHtml), {
246
248
  waitUntil: 'domcontentloaded'
247
249
  });
248
250
  this.ctx.logger.info('网页加载完成,开始等待字体加载。');
@@ -309,14 +311,18 @@ class RendererService extends koishi_1.Service {
309
311
  const html = env.render('html_template.html', context);
310
312
  const randomId = Math.random().toString(36).substring(2, 15);
311
313
  const outTemplateHtmlPath = path_1.default.resolve(skinDir, `${randomId}.html`);
312
- await fs_1.promises.writeFile(outTemplateHtmlPath, await this.injectEmojiFont(html));
314
+ const emojiHtmlOriginal = await this.injectEmojiFont(html);
315
+ await fs_1.promises.writeFile(outTemplateHtmlPath, emojiHtmlOriginal);
313
316
  this.ctx.logger.info(`原项目皮肤 ${skin} HTML 生成完成,正在调用 Puppeteer 渲染...`);
314
317
  const page = await this.ctx.puppeteer.page();
315
318
  await page.setViewport({ width: 1080, height: 1920, deviceScaleFactor: 2 });
316
- await page.goto('file://' + outTemplateHtmlPath, {
319
+ await page.goto('file://' + this.emptyHtmlPath(), {
320
+ waitUntil: 'domcontentloaded'
321
+ });
322
+ await page.setContent(emojiHtmlOriginal, {
317
323
  waitUntil: 'networkidle0',
318
324
  timeout: 60000
319
- }).catch(() => page.goto('file://' + outTemplateHtmlPath, {
325
+ }).catch(() => page.setContent(emojiHtmlOriginal, {
320
326
  waitUntil: 'domcontentloaded',
321
327
  timeout: 60000
322
328
  }));
@@ -528,8 +534,11 @@ class RendererService extends koishi_1.Service {
528
534
  await fs_1.promises.writeFile(outTemplateHtmlPath, await this.injectEmojiFont(filledHtml));
529
535
  this.ctx.logger.info('用户画像 HTML 模板填充完成,正在调用 Puppeteer 进行渲染...');
530
536
  const page = await this.ctx.puppeteer.page();
531
- // 重新加载页面并使用 goto 访问本地文件
532
- await page.goto('file://' + outTemplateHtmlPath, {
537
+ // goto 空 HTML 建立 file:// base,再 setContent 注入内容(@font-face 相对字体才能被加载)
538
+ await page.goto('file://' + this.emptyHtmlPath(), {
539
+ waitUntil: 'domcontentloaded'
540
+ });
541
+ await page.setContent(await this.injectEmojiFont(filledHtml), {
533
542
  waitUntil: 'domcontentloaded'
534
543
  });
535
544
  this.ctx.logger.info('网页加载完成,开始等待字体加载。');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-group-analysis-mx",
3
3
  "description": "群聊分析插件 - 生成精美群聊统计报告,支持 AI 话题总结(独立版,不依赖 ChatLuna)",
4
- "version": "1.2.8",
4
+ "version": "1.3.0",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/index.mjs",
7
7
  "typings": "lib/index.d.ts",
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file