koishi-plugin-oni-sync-bot 0.8.6 → 0.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -186,30 +186,19 @@ var WikiBotService = class _WikiBotService extends import_koishi2.Service {
186
186
  }
187
187
  async loginWithRetry(siteConfig, attempt = 1) {
188
188
  try {
189
- logger.info(
190
- `正在登录 ${siteConfig.name}... (尝试 ${attempt}/${_WikiBotService.MAX_RETRIES})`
191
- );
192
189
  const bot = await this.login(siteConfig);
193
190
  return bot;
194
191
  } catch (error) {
195
192
  const errorMsg = getErrorMessage(error);
196
193
  if (attempt < _WikiBotService.MAX_RETRIES) {
197
- logger.warn(
198
- `登录 ${siteConfig.name} 失败,${_WikiBotService.RETRY_DELAY / 1e3}秒后重试...`,
199
- errorMsg
200
- );
201
194
  await this.delay(_WikiBotService.RETRY_DELAY);
202
195
  return this.loginWithRetry(siteConfig, attempt + 1);
203
196
  }
204
- logger.error(
205
- `登录 ${siteConfig.name} 失败,已达到最大重试次数`,
206
- errorMsg
207
- );
197
+ logger.error(`❌ ${siteConfig.name} 登录失败`, errorMsg);
208
198
  throw error;
209
199
  }
210
200
  }
211
201
  async login(siteConfig) {
212
- logger.info(`正在连接 ${siteConfig.name} API: ${siteConfig.api}`);
213
202
  const bot = new import_mwn.Mwn({
214
203
  apiUrl: siteConfig.api,
215
204
  username: siteConfig.username,
@@ -225,9 +214,7 @@ var WikiBotService = class _WikiBotService extends import_koishi2.Service {
225
214
  const cookieString = "SESSDATA=666; Domain=wiki.biligame.com; Path=/oni; HttpOnly; Secure;";
226
215
  bot.cookieJar.setCookie(cookieString, bot.options.apiUrl, (err) => {
227
216
  if (err) {
228
- logger.warn("Cookie 注入失败:", err);
229
- } else {
230
- logger.info("Cookie 注入成功");
217
+ logger.warn("bwiki Cookie 注入失败:", err);
231
218
  }
232
219
  });
233
220
  bot.setRequestOptions({
@@ -237,96 +224,63 @@ var WikiBotService = class _WikiBotService extends import_koishi2.Service {
237
224
  }
238
225
  });
239
226
  }
240
- logger.info(`正在执行 ${siteConfig.name} 登录...`);
241
227
  await bot.login();
242
- logger.info(`✅ 成功登录 ${siteConfig.name}`);
243
228
  return bot;
244
229
  }
245
230
  async start() {
246
- logger.info("WikiBotService 正在初始化...");
247
231
  try {
248
232
  const sitesConfig = this.getSitesConfig();
249
- logger.info("开始登录 WIKIGG...");
250
233
  try {
251
234
  this.ggbot = await this.loginWithRetry(sitesConfig.gg);
235
+ logger.info("✅ WIKIGG 登录成功");
252
236
  } catch (error) {
253
- const errorMsg = getErrorMessage(error);
254
- logger.error(
255
- "WIKIGG 登录失败,服务将继续运行,但 WIKIGG 相关功能不可用",
256
- errorMsg
257
- );
237
+ this.ggbot = null;
258
238
  }
259
- logger.info("开始登录 bwiki...");
260
239
  try {
261
240
  this.bwikibot = await this.loginWithRetry(sitesConfig.bwiki);
241
+ logger.info("✅ bwiki 登录成功");
262
242
  } catch (error) {
263
- const errorMsg = getErrorMessage(error);
264
- logger.error(
265
- "bwiki 登录失败,服务将继续运行,但 bwiki 相关功能不可用",
266
- errorMsg
267
- );
243
+ this.bwikibot = null;
268
244
  }
269
- if (this.ggbot && this.bwikibot) {
270
- this.isReady = true;
271
- logger.info("WikiBotService 初始化成功,两个 Wiki 已登录");
272
- } else if (this.ggbot || this.bwikibot) {
273
- this.isReady = true;
274
- logger.warn(
275
- `WikiBotService 部分初始化成功,已登录: ${this.ggbot ? "WIKIGG" : ""} ${this.bwikibot ? "bwiki" : ""}`
276
- );
277
- } else {
278
- logger.error("WikiBotService 初始化失败,所有登录都失败");
245
+ this.isReady = this.ggbot !== null || this.bwikibot !== null;
246
+ if (!this.isReady) {
247
+ logger.error("WikiBotService 初始化失败,两个 Wiki 均无法登录");
279
248
  }
280
249
  } catch (error) {
281
250
  const errorMsg = getErrorMessage(error);
282
- logger.error("WikiBotService 初始化出错:", errorMsg);
251
+ logger.error("WikiBotService 初始化异常:", errorMsg);
283
252
  }
284
253
  }
285
254
  stop() {
286
255
  this.isReady = false;
287
256
  this.ggbot = null;
288
257
  this.bwikibot = null;
289
- logger.info("WikiBotService 已停止");
258
+ this.ggbotProxy = null;
259
+ this.bwikibotProxy = null;
290
260
  }
291
261
  async relogin() {
292
262
  const sitesConfig = this.getSitesConfig();
293
263
  let ggSuccess = false;
294
264
  let bwikiSuccess = false;
295
- logger.info("开始重新登录 WIKIGG...");
296
265
  try {
297
266
  this.ggbot = await this.loginWithRetry(sitesConfig.gg);
298
267
  ggSuccess = true;
299
- logger.info("✅ 成功重新登录 WIKIGG");
268
+ logger.info("✅ WIKIGG 重新登录成功");
300
269
  } catch (error) {
301
270
  this.ggbot = null;
302
- const errorMsg = getErrorMessage(error);
303
- logger.error("❌ 重新登录 WIKIGG 失败", errorMsg);
304
271
  }
305
- logger.info("开始重新登录 bwiki...");
306
272
  try {
307
273
  this.bwikibot = await this.loginWithRetry(sitesConfig.bwiki);
308
274
  bwikiSuccess = true;
309
- logger.info("✅ 成功重新登录 bwiki");
275
+ logger.info("✅ bwiki 重新登录成功");
310
276
  } catch (error) {
311
277
  this.bwikibot = null;
312
- const errorMsg = getErrorMessage(error);
313
- logger.error("❌ 重新登录 bwiki 失败", errorMsg);
314
- }
315
- if (this.ggbot && this.bwikibot) {
316
- this.isReady = true;
317
- logger.info("WikiBotService 重新登录成功,两个 Wiki 已登录");
318
- } else if (this.ggbot || this.bwikibot) {
319
- this.isReady = true;
320
- logger.warn(
321
- `WikiBotService 部分重新登录成功,已登录: ${this.ggbot ? "WIKIGG" : ""} ${this.bwikibot ? "bwiki" : ""}`
322
- );
323
- } else {
324
- this.isReady = false;
325
- logger.error("WikiBotService 重新登录失败,所有登录都失败");
326
278
  }
279
+ this.isReady = this.ggbot !== null || this.bwikibot !== null;
327
280
  return { gg: ggSuccess, bwiki: bwikiSuccess };
328
281
  }
329
282
  async reloginSite(site) {
283
+ const siteName = site === "gg" ? "WIKIGG" : "bwiki";
330
284
  const sitesConfig = this.getSitesConfig();
331
285
  const siteConfig = site === "gg" ? sitesConfig.gg : sitesConfig.bwiki;
332
286
  try {
@@ -336,15 +290,13 @@ var WikiBotService = class _WikiBotService extends import_koishi2.Service {
336
290
  } else {
337
291
  this.bwikibot = bot;
338
292
  }
339
- logger.info(`✅ ${siteConfig.name} 自动重新登录成功`);
293
+ logger.info(`✅ ${siteName} 自动重新登录成功`);
340
294
  } catch (error) {
341
- const errorMsg = getErrorMessage(error);
342
295
  if (site === "gg") {
343
296
  this.ggbot = null;
344
297
  } else {
345
298
  this.bwikibot = null;
346
299
  }
347
- logger.error(`❌ ${siteConfig.name} 自动重新登录失败`, errorMsg);
348
300
  throw error;
349
301
  }
350
302
  }
@@ -354,71 +306,77 @@ var WikiBotService = class _WikiBotService extends import_koishi2.Service {
354
306
  isBWikiBotReady() {
355
307
  return this.bwikibot !== null;
356
308
  }
357
- createBotProxy(bot, site) {
309
+ createBotProxy(site) {
358
310
  const self = this;
359
- const METHODS_TO_SKIP = /* @__PURE__ */ new Set([
360
- "cookieJar",
361
- "setRequestOptions"
362
- ]);
363
- return new Proxy(bot, {
364
- get(target, prop) {
365
- const originalMethod = target[prop];
366
- if (typeof originalMethod !== "function") {
367
- return originalMethod;
311
+ const METHODS_TO_SKIP = /* @__PURE__ */ new Set(["cookieJar", "setRequestOptions"]);
312
+ const getCurrentBot = /* @__PURE__ */ __name(() => {
313
+ const bot = site === "gg" ? self.ggbot : self.bwikibot;
314
+ if (!bot) {
315
+ throw new Error(
316
+ `${site === "gg" ? "WIKIGG" : "bwiki"} bot 尚未就绪,请检查登录配置或查看日志`
317
+ );
318
+ }
319
+ return bot;
320
+ }, "getCurrentBot");
321
+ return new Proxy({}, {
322
+ get(_target, prop) {
323
+ const propKey = prop;
324
+ const currentBot = getCurrentBot();
325
+ const value = currentBot[propKey];
326
+ if (typeof value !== "function") {
327
+ return value;
368
328
  }
369
329
  if (METHODS_TO_SKIP.has(prop)) {
370
- return originalMethod.bind(target);
330
+ return value.bind(currentBot);
371
331
  }
372
332
  if (prop === "continuedQueryGen") {
373
333
  return function(...args) {
374
- const originalGen = originalMethod.apply(target, args);
334
+ let activeBot = getCurrentBot();
335
+ let activeMethod = activeBot.continuedQueryGen;
336
+ let activeIterator = activeMethod.apply(activeBot, args)[Symbol.asyncIterator]();
375
337
  return {
376
338
  [Symbol.asyncIterator]() {
377
- const it = originalGen[Symbol.asyncIterator]();
378
- return {
379
- async next() {
380
- try {
381
- return await it.next();
382
- } catch (error) {
383
- if (error.code === "assertuserfailed") {
384
- logger.warn(
385
- `检测到 ${site === "gg" ? "WIKIGG" : "bwiki"} 登录过期,正在自动重新登录...`
386
- );
387
- await self.reloginSite(site);
388
- const newBot = site === "gg" ? self.ggbot : self.bwikibot;
389
- if (!newBot) {
390
- throw new Error(
391
- `${site === "gg" ? "WIKIGG" : "bwiki"} 自动重新登录失败`
392
- );
393
- }
394
- const newGen = newBot.continuedQueryGen(...args);
395
- const newIt = newGen[Symbol.asyncIterator]();
396
- return await newIt.next();
397
- }
398
- throw error;
399
- }
339
+ return this;
340
+ },
341
+ async next() {
342
+ try {
343
+ return await activeIterator.next();
344
+ } catch (error) {
345
+ if (error.code === "assertuserfailed") {
346
+ await self.reloginSite(site);
347
+ activeBot = getCurrentBot();
348
+ activeMethod = activeBot.continuedQueryGen;
349
+ const newGen = activeMethod.apply(activeBot, args);
350
+ activeIterator = newGen[Symbol.asyncIterator]();
351
+ return await activeIterator.next();
400
352
  }
401
- };
353
+ throw error;
354
+ }
402
355
  }
403
356
  };
404
357
  };
405
358
  }
406
359
  return async function(...args) {
360
+ let executeBot = getCurrentBot();
361
+ let method = executeBot[propKey];
362
+ if (typeof method !== "function") {
363
+ throw new Error(
364
+ `${site === "gg" ? "WIKIGG" : "bwiki"} bot 缺少方法 ${String(prop)}`
365
+ );
366
+ }
407
367
  try {
408
- return await originalMethod.apply(target, args);
368
+ return await method.apply(executeBot, args);
409
369
  } catch (error) {
410
370
  if (error.code === "assertuserfailed") {
411
- logger.warn(
412
- `检测到 ${site === "gg" ? "WIKIGG" : "bwiki"} 登录过期,正在自动重新登录...`
413
- );
414
371
  await self.reloginSite(site);
415
- const newBot = site === "gg" ? self.ggbot : self.bwikibot;
416
- if (!newBot) {
372
+ executeBot = getCurrentBot();
373
+ method = executeBot[propKey];
374
+ if (typeof method !== "function") {
417
375
  throw new Error(
418
- `${site === "gg" ? "WIKIGG" : "bwiki"} 自动重新登录失败`
376
+ `${site === "gg" ? "WIKIGG" : "bwiki"} 新 bot 缺少方法 ${String(prop)}`
419
377
  );
420
378
  }
421
- return await newBot[prop].apply(newBot, args);
379
+ return await method.apply(executeBot, args);
422
380
  }
423
381
  throw error;
424
382
  }
@@ -426,17 +384,25 @@ var WikiBotService = class _WikiBotService extends import_koishi2.Service {
426
384
  }
427
385
  });
428
386
  }
387
+ ggbotProxy = null;
388
+ bwikibotProxy = null;
429
389
  getGGBot() {
430
390
  if (!this.ggbot) {
431
391
  throw new Error("WIKIGG bot 尚未就绪,请检查登录配置或查看日志");
432
392
  }
433
- return this.createBotProxy(this.ggbot, "gg");
393
+ if (!this.ggbotProxy) {
394
+ this.ggbotProxy = this.createBotProxy("gg");
395
+ }
396
+ return this.ggbotProxy;
434
397
  }
435
398
  getBWikiBot() {
436
399
  if (!this.bwikibot) {
437
400
  throw new Error("bwiki bot 尚未就绪,请检查登录配置或查看日志");
438
401
  }
439
- return this.createBotProxy(this.bwikibot, "bwiki");
402
+ if (!this.bwikibotProxy) {
403
+ this.bwikibotProxy = this.createBotProxy("bwiki");
404
+ }
405
+ return this.bwikibotProxy;
440
406
  }
441
407
  };
442
408
  ((WikiBotService2) => {
@@ -537,12 +503,6 @@ var RouteRedirect = class {
537
503
  )}`;
538
504
  router.redirect(targetUrl);
539
505
  });
540
- ctx.server.get("/ggwiki/*", async (router) => {
541
- const suffix = router.params[""] || "";
542
- const queryString = router.querystring ? `?${router.querystring}` : "";
543
- const targetUrl = `https://${this.config.main_site}/${suffix}${queryString}`;
544
- router.redirect(targetUrl);
545
- });
546
506
  }
547
507
  };
548
508
  ((RouteRedirect2) => {
@@ -570,11 +530,21 @@ var CONFIG = {
570
530
  UPLOAD_COMMENT: "从 WikiGG 自动同步转存",
571
531
  UPLOAD_TEXT: "== 授权说明 ==\n本文件从 WikiGG 转存,遵循原站点授权协议。"
572
532
  };
533
+ function normalizeImageTitle(fileName) {
534
+ const trimmed = fileName.trim();
535
+ const lower = trimmed.toLowerCase();
536
+ if (lower.startsWith("file:") || lower.startsWith("image:") || lower.startsWith("文件:")) {
537
+ return trimmed;
538
+ }
539
+ return `File:${trimmed}`;
540
+ }
541
+ __name(normalizeImageTitle, "normalizeImageTitle");
573
542
  async function getImageInfo(site, fileName) {
574
543
  try {
544
+ const normalizedTitle = normalizeImageTitle(fileName);
575
545
  const response = await site.query({
576
546
  action: "query",
577
- titles: fileName,
547
+ titles: normalizedTitle,
578
548
  prop: "imageinfo",
579
549
  iiprop: "url|sha1|size|mime"
580
550
  });
@@ -598,9 +568,10 @@ async function getImageInfo(site, fileName) {
598
568
  __name(getImageInfo, "getImageInfo");
599
569
  async function deleteOldVersions(bot, fileName) {
600
570
  try {
571
+ const normalizedTitle = normalizeImageTitle(fileName);
601
572
  const response = await bot.query({
602
573
  action: "query",
603
- titles: fileName,
574
+ titles: normalizedTitle,
604
575
  prop: "revisions",
605
576
  rvprop: "ids|timestamp",
606
577
  rvlimit: "max",
@@ -640,19 +611,32 @@ async function deleteOldVersions(bot, fileName) {
640
611
  }
641
612
  }
642
613
  __name(deleteOldVersions, "deleteOldVersions");
614
+ function stripImagePrefix(fileName) {
615
+ const trimmed = fileName.trim();
616
+ const lower = trimmed.toLowerCase();
617
+ const prefixes = ["file:", "image:", "文件:"];
618
+ for (const p of prefixes) {
619
+ if (lower.startsWith(p)) {
620
+ return trimmed.substring(p.length);
621
+ }
622
+ }
623
+ return trimmed;
624
+ }
625
+ __name(stripImagePrefix, "stripImagePrefix");
643
626
  async function syncSingleImage(sourceBot, targetBot, fileName, config) {
644
627
  if (CONFIG.IGNORED_IMAGES.includes(fileName)) {
645
628
  logger.info(`[SyncImg] 🚫 图片 ${fileName} 在忽略列表,跳过`);
646
629
  return { success: true, reason: "ignored" };
647
630
  }
631
+ const bareFileName = stripImagePrefix(fileName);
648
632
  try {
649
- logger.info(`[SyncImg] 🚀 开始处理: ${fileName}`);
650
- const sourceImageInfo = await getImageInfo(sourceBot, fileName);
633
+ logger.info(`[SyncImg] 🚀 开始处理: ${bareFileName}`);
634
+ const sourceImageInfo = await getImageInfo(sourceBot, bareFileName);
651
635
  if (!sourceImageInfo) {
652
636
  logger.info(`[SyncImg] ❌ 源站未找到图片: ${fileName}`);
653
637
  return { success: false, reason: "source_missing" };
654
638
  }
655
- const targetImageInfo = await getImageInfo(targetBot, fileName);
639
+ const targetImageInfo = await getImageInfo(targetBot, bareFileName);
656
640
  logger.info(`原图片sha1: ${sourceImageInfo.sha1}`);
657
641
  logger.info(`目标图片sha1: ${targetImageInfo?.sha1}`);
658
642
  if (targetImageInfo && targetImageInfo.sha1 === sourceImageInfo.sha1) {
@@ -670,19 +654,19 @@ async function syncSingleImage(sourceBot, targetBot, fileName, config) {
670
654
  }
671
655
  const imageBuffer = Buffer.from(await imageResponse.arrayBuffer());
672
656
  logger.info(
673
- `[SyncImg] 📤 上传图片: ${fileName} (大小: ${(imageBuffer.length / 1024).toFixed(1)} KB)`
657
+ `[SyncImg] 📤 上传图片: ${bareFileName} (大小: ${(imageBuffer.length / 1024).toFixed(1)} KB)`
674
658
  );
675
659
  const token = await targetBot.getCsrfToken();
676
660
  const form = new import_form_data.default();
677
661
  form.append("action", "upload");
678
- form.append("filename", fileName);
662
+ form.append("filename", bareFileName);
679
663
  form.append("text", CONFIG.UPLOAD_TEXT);
680
664
  form.append("comment", CONFIG.UPLOAD_COMMENT);
681
665
  form.append("token", token);
682
666
  form.append("ignorewarnings", "0");
683
667
  form.append("format", "json");
684
668
  form.append("file", imageBuffer, {
685
- filename: fileName.split(":").pop() || fileName,
669
+ filename: bareFileName,
686
670
  contentType: imageResponse.headers.get("content-type") || "application/octet-stream"
687
671
  });
688
672
  const rawResponse = await targetBot.rawRequest({
@@ -987,7 +971,7 @@ async function syncPages(oldSite, newSite) {
987
971
  }
988
972
  }
989
973
  __name(syncPages, "syncPages");
990
- async function incrementalUpdate(oldSite, newSite, config) {
974
+ async function incrementalUpdate(oldSite, newSite, config, onSynced) {
991
975
  try {
992
976
  const now = /* @__PURE__ */ new Date();
993
977
  const threeHoursAgo = new Date(now.getTime() - 3 * 60 * 60 * 1e3);
@@ -1005,6 +989,7 @@ async function incrementalUpdate(oldSite, newSite, config) {
1005
989
  const processedTitles = /* @__PURE__ */ new Set();
1006
990
  let totalProcessed = 0;
1007
991
  let totalSkipped = 0;
992
+ const syncedItems = [];
1008
993
  for await (const res of queryGen) {
1009
994
  const pages = res.query?.recentchanges || [];
1010
995
  for (const page of pages) {
@@ -1025,19 +1010,45 @@ async function incrementalUpdate(oldSite, newSite, config) {
1025
1010
  processedTitles.add(title);
1026
1011
  totalProcessed++;
1027
1012
  try {
1013
+ const rcUser = page.user || "未知";
1014
+ const rcTimestamp = page.timestamp || "";
1015
+ const rcComment = page.comment || "";
1028
1016
  if (title.startsWith(CONFIG2.FILE_NAMESPACE_PREFIX)) {
1029
1017
  const fileName = title.replace(CONFIG2.FILE_NAMESPACE_PREFIX, "");
1030
1018
  logger.info(
1031
1019
  `[增量更新流程] 🖼️ 检查到图片: ${title},正在尝试转存`
1032
1020
  );
1033
- await syncSingleImage(oldSite, newSite, fileName, config);
1021
+ const result = await syncSingleImage(
1022
+ oldSite,
1023
+ newSite,
1024
+ fileName,
1025
+ config
1026
+ );
1027
+ if (result.success && result.reason === "synced") {
1028
+ syncedItems.push({
1029
+ title,
1030
+ type: "image",
1031
+ user: rcUser,
1032
+ timestamp: rcTimestamp,
1033
+ comment: rcComment
1034
+ });
1035
+ }
1034
1036
  } else {
1035
- await syncSinglePage(
1037
+ const result = await syncSinglePage(
1036
1038
  oldSite,
1037
1039
  newSite,
1038
1040
  title,
1039
- CONFIG2.INCREMENTAL_USER
1041
+ rcUser
1040
1042
  );
1043
+ if (result.success && result.reason === "synced") {
1044
+ syncedItems.push({
1045
+ title,
1046
+ type: "page",
1047
+ user: rcUser,
1048
+ timestamp: rcTimestamp,
1049
+ comment: rcComment
1050
+ });
1051
+ }
1041
1052
  }
1042
1053
  await (0, import_koishi6.sleep)(CONFIG2.SYNC_INTERVAL_SUCCESS);
1043
1054
  } catch (error) {
@@ -1048,8 +1059,11 @@ async function incrementalUpdate(oldSite, newSite, config) {
1048
1059
  }
1049
1060
  }
1050
1061
  logger.info(
1051
- `[增量更新流程] ✅ 增量更新完成!处理: ${totalProcessed}, 跳过: ${totalSkipped}`
1062
+ `[增量更新流程] ✅ 增量更新完成!处理: ${totalProcessed}, 跳过: ${totalSkipped}, 通知: ${syncedItems.length}`
1052
1063
  );
1064
+ if (syncedItems.length > 0) {
1065
+ onSynced?.(syncedItems);
1066
+ }
1053
1067
  } catch (globalError) {
1054
1068
  logger.error(`[增量更新流程] 💥 增量更新流程异常终止:`, globalError);
1055
1069
  throw globalError;
@@ -1224,21 +1238,40 @@ async function syncModules(oldSite, newSite) {
1224
1238
  __name(syncModules, "syncModules");
1225
1239
 
1226
1240
  // src/plugins/syncCommands.ts
1241
+ function formatWikiTime(isoString) {
1242
+ try {
1243
+ const date = new Date(isoString);
1244
+ if (Number.isNaN(date.getTime())) return isoString;
1245
+ const pad = /* @__PURE__ */ __name((n) => String(n).padStart(2, "0"), "pad");
1246
+ const y = date.getFullYear();
1247
+ const m = pad(date.getMonth() + 1);
1248
+ const d = pad(date.getDate());
1249
+ const hh = pad(date.getHours());
1250
+ const mm = pad(date.getMinutes());
1251
+ return `${y}-${m}-${d} ${hh}:${mm}`;
1252
+ } catch {
1253
+ return isoString;
1254
+ }
1255
+ }
1256
+ __name(formatWikiTime, "formatWikiTime");
1227
1257
  var SyncCommands = class {
1228
1258
  static {
1229
1259
  __name(this, "SyncCommands");
1230
1260
  }
1231
1261
  static inject = ["wikiBot", "cron"];
1232
1262
  config;
1263
+ notifyFn;
1233
1264
  constructor(ctx, config) {
1234
1265
  this.config = config;
1235
1266
  logger.info("WikiBot 服务已就绪,初始化定时任务和指令");
1267
+ this.notifyFn = this.buildNotifyFn(ctx);
1236
1268
  ctx.cron("15 * * * *", async () => {
1237
1269
  if (!await this.ensureBotsReady(ctx, "增量更新")) return;
1238
1270
  await incrementalUpdate(
1239
1271
  ctx.wikiBot.getGGBot(),
1240
1272
  ctx.wikiBot.getBWikiBot(),
1241
- config
1273
+ config,
1274
+ this.notifyFn
1242
1275
  );
1243
1276
  });
1244
1277
  ctx.cron("30 8 * * 4", async () => {
@@ -1263,6 +1296,86 @@ var SyncCommands = class {
1263
1296
  });
1264
1297
  this.registerCommands(ctx);
1265
1298
  }
1299
+ /**
1300
+ * 规范化 channelId:兼容多种前缀格式
1301
+ */
1302
+ normalizeChannelId(rawId) {
1303
+ const trimmed = rawId.trim();
1304
+ if (!trimmed) return "";
1305
+ const lower = trimmed.toLowerCase();
1306
+ if (lower.startsWith("group:")) return trimmed.slice(6);
1307
+ if (lower.startsWith("channel:")) return trimmed.slice(8);
1308
+ if (lower.startsWith("qq:")) return trimmed.slice(3);
1309
+ return trimmed;
1310
+ }
1311
+ /**
1312
+ * 截断摘要到指定长度,超出部分用 ...
1313
+ */
1314
+ truncateComment(text, max = 10) {
1315
+ if (!text) return "";
1316
+ const clean = text.trim();
1317
+ if (clean.length <= max) return clean;
1318
+ return clean.slice(0, max) + "...";
1319
+ }
1320
+ /**
1321
+ * 构建增量更新同步成功的通知回调
1322
+ * 当配置了 notifyGroupIds 时,将同步成功的页面/图片合并成一条消息推送到指定 QQ 群
1323
+ */
1324
+ buildNotifyFn(ctx) {
1325
+ const groupIds = this.config.notifyGroupIds;
1326
+ if (!groupIds || groupIds.length === 0) return void 0;
1327
+ const platform = this.config.notifyBotPlatform || "qq";
1328
+ const self = this;
1329
+ return async (items) => {
1330
+ try {
1331
+ if (!items || items.length === 0) return;
1332
+ const bots = [];
1333
+ for (const bot of ctx.bots) {
1334
+ if (bot.platform === platform && bot.isActive) {
1335
+ bots.push(bot);
1336
+ }
1337
+ }
1338
+ if (bots.length === 0) {
1339
+ logger.warn(`[通知] 未找到可用的 ${platform} 平台 bot,跳过群通知`);
1340
+ return;
1341
+ }
1342
+ const lines = [];
1343
+ lines.push(`Wiki 同步更新(${items.length} 条)`);
1344
+ lines.push("");
1345
+ for (let i = 0; i < items.length; i++) {
1346
+ const item = items[i];
1347
+ const label = item.type === "image" ? "🖼️" : "📖";
1348
+ const zhTime = item.timestamp ? formatWikiTime(item.timestamp) : "";
1349
+ const user = item.user || "未知";
1350
+ const shortComment = self.truncateComment(item.comment || "", 10);
1351
+ const commentLine = shortComment ? `|${shortComment}` : "";
1352
+ lines.push(
1353
+ `${label} ${i + 1}. ${item.title}|${user}|${zhTime}${commentLine}`
1354
+ );
1355
+ }
1356
+ const message = lines.join("\n");
1357
+ for (const bot of bots) {
1358
+ for (const rawChannelId of groupIds) {
1359
+ const channelId = self.normalizeChannelId(rawChannelId);
1360
+ if (!channelId) continue;
1361
+ try {
1362
+ await bot.sendMessage(channelId, message);
1363
+ logger.info(
1364
+ `[通知] 向 ${bot.platform}/${channelId} 推送成功 (${items.length} 条)`
1365
+ );
1366
+ } catch (error) {
1367
+ logger.error(
1368
+ `[通知] 向 ${bot.platform}/${channelId} 发送消息失败:`,
1369
+ getErrorMessage(error)
1370
+ );
1371
+ }
1372
+ }
1373
+ }
1374
+ } catch (error) {
1375
+ logger.error(`[通知] 处理通知过程出错:`, getErrorMessage(error));
1376
+ }
1377
+ };
1378
+ }
1266
1379
  /**
1267
1380
  * 确保机器人就绪,如果未就绪则尝试重新登录
1268
1381
  * @param ctx Koishi 上下文
@@ -1317,7 +1430,8 @@ var SyncCommands = class {
1317
1430
  await incrementalUpdate(
1318
1431
  ctx.wikiBot.getGGBot(),
1319
1432
  ctx.wikiBot.getBWikiBot(),
1320
- this.config
1433
+ this.config,
1434
+ this.notifyFn
1321
1435
  );
1322
1436
  return `✅ 已尝试获取三小时前的编辑并同步,请前往控制台查看:${this.config.logsUrl}`;
1323
1437
  } catch (err) {
@@ -1412,7 +1526,11 @@ var SyncCommands = class {
1412
1526
  domain: import_koishi8.Schema.string().description("你的短链域名(必填,如:klei.vip)").default("klei.vip"),
1413
1527
  main_site: import_koishi8.Schema.string().description("主站域名(必填,如:oxygennotincluded.wiki.gg)").default("oxygennotincluded.wiki.gg/zh"),
1414
1528
  bwiki_site: import_koishi8.Schema.string().description("镜像站域名(必填,如:wiki.biligame.com)").default("wiki.biligame.com/oni"),
1415
- logsUrl: import_koishi8.Schema.string().description("日志查看地址").default("https://klei.vip/onilogs")
1529
+ logsUrl: import_koishi8.Schema.string().description("日志查看地址").default("https://klei.vip/onilogs"),
1530
+ notifyGroupIds: import_koishi8.Schema.array(import_koishi8.Schema.string()).description(
1531
+ '增量更新同步成功时通知的群频道ID列表,如 ["group:123456"],留空则不发送通知'
1532
+ ).default([]),
1533
+ notifyBotPlatform: import_koishi8.Schema.string().description("通知 bot 的平台标识,默认 qq").default("qq")
1416
1534
  });
1417
1535
  })(SyncCommands || (SyncCommands = {}));
1418
1536
 
@@ -8,11 +8,27 @@ export interface SyncCommandsConfig {
8
8
  domain: string;
9
9
  main_site: string;
10
10
  bwiki_site: string;
11
+ notifyGroupIds?: string[];
12
+ notifyBotPlatform?: string;
11
13
  }
12
14
  export declare class SyncCommands {
13
15
  static readonly inject: string[];
14
16
  config: SyncCommandsConfig;
17
+ private notifyFn?;
15
18
  constructor(ctx: Context, config: SyncCommandsConfig);
19
+ /**
20
+ * 规范化 channelId:兼容多种前缀格式
21
+ */
22
+ private normalizeChannelId;
23
+ /**
24
+ * 截断摘要到指定长度,超出部分用 ...
25
+ */
26
+ private truncateComment;
27
+ /**
28
+ * 构建增量更新同步成功的通知回调
29
+ * 当配置了 notifyGroupIds 时,将同步成功的页面/图片合并成一条消息推送到指定 QQ 群
30
+ */
31
+ private buildNotifyFn;
16
32
  /**
17
33
  * 确保机器人就绪,如果未就绪则尝试重新登录
18
34
  * @param ctx Koishi 上下文
@@ -36,6 +36,8 @@ export declare class WikiBotService extends Service {
36
36
  isGGBotReady(): boolean;
37
37
  isBWikiBotReady(): boolean;
38
38
  private createBotProxy;
39
+ private ggbotProxy;
40
+ private bwikibotProxy;
39
41
  getGGBot(): Mwn;
40
42
  getBWikiBot(): Mwn;
41
43
  }
@@ -20,11 +20,22 @@ declare function syncSinglePage(oldSite: Mwn, newSite: Mwn, pageTitle: string, u
20
20
  * @returns null
21
21
  */
22
22
  declare function syncPages(oldSite: Mwn, newSite: Mwn): Promise<void>;
23
+ /**
24
+ * 同步成功事件信息
25
+ */
26
+ export interface SyncNotifyItem {
27
+ title: string;
28
+ type: "page" | "image";
29
+ user?: string;
30
+ timestamp?: string;
31
+ comment?: string;
32
+ }
23
33
  /**
24
34
  * 增量更新
25
35
  * @param oldSite 源站点机器人实例
26
36
  * @param newSite 目标站点机器人实例
27
37
  * @param config KOISHI用户配置的项
38
+ * @param onSynced 同步成功批量回调(仅 reason==="synced" 的条目,函数结束时一次调用)
28
39
  */
29
- declare function incrementalUpdate(oldSite: Mwn, newSite: Mwn, config: Config): Promise<void>;
40
+ declare function incrementalUpdate(oldSite: Mwn, newSite: Mwn, config: Config, onSynced?: (items: SyncNotifyItem[]) => void): Promise<void>;
30
41
  export { syncSinglePage, syncPages, incrementalUpdate };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-oni-sync-bot",
3
3
  "description": "缺氧Wiki站镜像点同步-测试",
4
- "version": "0.8.6",
4
+ "version": "0.8.8",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [