koishi-plugin-dew-bot 1.0.258 → 1.0.259

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
@@ -1422,6 +1422,7 @@ async function renderUncached(ctx, card, leaderboard, itemCard, characterCard, e
1422
1422
  let timeout;
1423
1423
  let workPromise;
1424
1424
  let expired = false;
1425
+ let finished = false;
1425
1426
  const release = /* @__PURE__ */ __name(() => {
1426
1427
  const next = waiting.shift();
1427
1428
  if (next) next();
@@ -1429,12 +1430,26 @@ async function renderUncached(ctx, card, leaderboard, itemCard, characterCard, e
1429
1430
  }, "release");
1430
1431
  try {
1431
1432
  const work = /* @__PURE__ */ __name(async () => {
1432
- const entryArtworkPromise = loadCardEntryArtwork(!titleCard && !bloodlineCard && !equipmentCard ? companionCard : void 0);
1433
1433
  const remoteAddress = titleCard ? titleCard.artworkUrl : bloodlineCard ? bloodlineCard.artworkUrl : equipmentCard ? equipmentCard.artworkUrl : companionCard ? companionCard.artworkUrl : characterCard ? characterCard.artworkUrl : itemCard ? itemCard.artworkUrl : leaderboard ? leaderboard.backgroundUrl : card?.artworkUrl;
1434
- const dedicatedArt = !titleCard && !bloodlineCard && equipmentCard ? await loadSkillArtwork(equipmentCard.artworkUrl) : void 0;
1435
- const unknownArt = !titleCard && !bloodlineCard && equipmentCard && !dedicatedArt ? await equipmentFallback() : void 0;
1436
- const art = !titleCard && !bloodlineCard && equipmentCard ? dedicatedArt ?? unknownArt : await loadSkillArtwork(remoteAddress);
1437
- const entryArtwork = await entryArtworkPromise;
1434
+ const isEquipment = !titleCard && !bloodlineCard && Boolean(equipmentCard);
1435
+ const isCharacter = !titleCard && !bloodlineCard && !equipmentCard && !companionCard && Boolean(characterCard);
1436
+ const [remoteArt, fallbackArt, entryArtwork, characterAssets, header] = await Promise.all([
1437
+ loadSkillArtwork(remoteAddress),
1438
+ isEquipment ? equipmentFallback() : void 0,
1439
+ loadCardEntryArtwork(!titleCard && !bloodlineCard && !equipmentCard ? companionCard : void 0),
1440
+ isCharacter ? Promise.all([characterAsset(), loadSkillArtwork(characterCard.titleImageUrl)]) : void 0,
1441
+ !titleCard && !bloodlineCard && !equipmentCard && !companionCard && !characterCard && !itemCard && leaderboard ? leaderboardHeader() : void 0,
1442
+ ctx.puppeteer.page().then(async (created) => {
1443
+ if (expired || finished) {
1444
+ await created.close();
1445
+ throw new Error(expired ? "绘图超时" : "绘图已结束");
1446
+ }
1447
+ page = created;
1448
+ })
1449
+ ]);
1450
+ const dedicatedArt = isEquipment ? remoteArt : void 0;
1451
+ const unknownArt = isEquipment && !dedicatedArt ? fallbackArt : void 0;
1452
+ const art = isEquipment ? dedicatedArt ?? unknownArt : remoteArt;
1438
1453
  let html;
1439
1454
  let loadedCharacterTitle;
1440
1455
  let localAssetsReady = true;
@@ -1448,27 +1463,19 @@ async function renderUncached(ctx, card, leaderboard, itemCard, characterCard, e
1448
1463
  } else if (companionCard) {
1449
1464
  html = companionCardHtml(companionCard, art, entryArtwork.icons);
1450
1465
  } else if (characterCard) {
1451
- const [defaultArtwork, titleArtwork] = await Promise.all([
1452
- characterAsset(),
1453
- loadSkillArtwork(characterCard.titleImageUrl)
1454
- ]);
1466
+ const [defaultArtwork, titleArtwork] = characterAssets;
1455
1467
  loadedCharacterTitle = titleArtwork;
1456
1468
  localAssetsReady = Boolean(art || defaultArtwork) && (!characterCard.titleImageUrl || Boolean(titleArtwork));
1457
1469
  html = characterCardHtml(characterCard, art, void 0, defaultArtwork, titleArtwork, entryArtwork.icons);
1458
1470
  } else if (itemCard) {
1459
1471
  html = itemCard.type === "形象卡" ? appearanceCardHtml(itemCard, art) : itemCardHtml(itemCard, art);
1460
1472
  } else if (leaderboard) {
1461
- html = leaderboardHtml(leaderboard, await leaderboardHeader(), art);
1473
+ html = leaderboardHtml(leaderboard, header, art);
1462
1474
  } else {
1463
1475
  html = skillCardHtml(card, "", art);
1464
1476
  }
1465
1477
  if (expired) throw new Error("绘图超时");
1466
- page = await ctx.puppeteer.page();
1467
- if (expired) {
1468
- await page.close();
1469
- page = void 0;
1470
- throw new Error("绘图超时");
1471
- }
1478
+ if (!page) throw new Error("卡片页面缺失");
1472
1479
  await page.setViewport({ width: 760, height: 1107, deviceScaleFactor: titleCard || (companionCard || characterCard) && !bloodlineCard && !equipmentCard ? 2 : 1 });
1473
1480
  await page.setJavaScriptEnabled(false);
1474
1481
  await page.setRequestInterception(true);
@@ -1518,6 +1525,7 @@ async function renderUncached(ctx, card, leaderboard, itemCard, characterCard, e
1518
1525
  }, 15e3);
1519
1526
  })]);
1520
1527
  } finally {
1528
+ finished = true;
1521
1529
  if (timeout) clearTimeout(timeout);
1522
1530
  try {
1523
1531
  await page?.close();
@@ -1851,7 +1859,7 @@ function renderQQInventoryMarkdown(data, showGreeting) {
1851
1859
  const sections = [];
1852
1860
  const items = [];
1853
1861
  const flushItems = /* @__PURE__ */ __name(() => {
1854
- if (items.length) sections.push(items.splice(0).map((line) => "> " + line).join(" \n"));
1862
+ if (items.length) sections.push(...items.splice(0));
1855
1863
  }, "flushItems");
1856
1864
  if (showGreeting) sections.push(escapeMarkdown("Ciallo~(∠・ω< )"));
1857
1865
  for (const [lineIndex, item] of data.line.entries()) {
@@ -4455,6 +4463,7 @@ var Server = class {
4455
4463
  ctx;
4456
4464
  onConnectedCallback;
4457
4465
  messageListenerSetup = false;
4466
+ lastUserMessageAt = /* @__PURE__ */ new Map();
4458
4467
  _init = false;
4459
4468
  async init(ctx, config) {
4460
4469
  this.ctx = ctx;
@@ -4464,6 +4473,12 @@ var Server = class {
4464
4473
  if (this.wsClient) {
4465
4474
  await this.dispose();
4466
4475
  }
4476
+ ctx.setInterval(() => {
4477
+ const now = performance.now();
4478
+ for (const [key, lastMessageAt] of this.lastUserMessageAt) {
4479
+ if (now - lastMessageAt > 500) this.lastUserMessageAt.delete(key);
4480
+ }
4481
+ }, 6e4);
4467
4482
  const serverUrl = config.服务器地址;
4468
4483
  if (!serverUrl || serverUrl.trim() === "") {
4469
4484
  throw new Error("服务器地址不能为空");
@@ -4481,6 +4496,7 @@ var Server = class {
4481
4496
  }
4482
4497
  async dispose() {
4483
4498
  this._init = false;
4499
+ this.lastUserMessageAt.clear();
4484
4500
  if (this.wsClient) {
4485
4501
  this.wsClient.unlistenMsgAll(/.*/);
4486
4502
  this.wsClient.disconnect();
@@ -4556,7 +4572,7 @@ var Server = class {
4556
4572
  }
4557
4573
  this.ctx.logger.warn("请求出错", apiName, req.err.message);
4558
4574
  }
4559
- /** 过滤机器人发送者后,缓存会话并把原始内容交给游戏服。 **/
4575
+ /** 过滤机器人发送者和用户连续消息后,缓存会话并把原始内容交给游戏服。 **/
4560
4576
  uploadMessage(session) {
4561
4577
  if (isBotMessage(session, this.ctx.bots)) return;
4562
4578
  const userId = session.userId;
@@ -4565,6 +4581,11 @@ var Server = class {
4565
4581
  this.ctx.logger.warn("消息缺少 userId 或 channelId,已忽略");
4566
4582
  return;
4567
4583
  }
4584
+ const userKey = JSON.stringify([session.platform ?? session.bot.platform, userId]);
4585
+ const now = performance.now();
4586
+ const lastMessageAt = this.lastUserMessageAt.get(userKey);
4587
+ this.lastUserMessageAt.set(userKey, now);
4588
+ if (lastMessageAt !== void 0 && now - lastMessageAt <= 500) return;
4568
4589
  sessions_default.set(sessionKey(session.bot.userId, userId, channelId), session);
4569
4590
  this.api("Message", {
4570
4591
  bot: {
@@ -6,6 +6,7 @@ declare class Server {
6
6
  private ctx;
7
7
  private onConnectedCallback?;
8
8
  private messageListenerSetup;
9
+ private readonly lastUserMessageAt;
9
10
  _init: boolean;
10
11
  init(ctx: Context, config: Config): Promise<void>;
11
12
  onConnected(callback: () => void): void;
@@ -14,7 +15,7 @@ declare class Server {
14
15
  setWsUrl(link: string, maxRetries?: number): Promise<void>;
15
16
  private reconnect;
16
17
  api<T extends keyof ServiceType['api']>(apiName: T, posData: ServiceType['api'][T]['req']): Promise<ServiceType['api'][T]['res'] | undefined>;
17
- /** 过滤机器人发送者后,缓存会话并把原始内容交给游戏服。 **/
18
+ /** 过滤机器人发送者和用户连续消息后,缓存会话并把原始内容交给游戏服。 **/
18
19
  uploadMessage(session: Session): void;
19
20
  }
20
21
  declare const _default: Server;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-dew-bot",
3
3
  "description": "基于《无限恐怖》小说改编的 RPG 机器人。踏入主神空间,与伙伴迎接轮回试炼,书写属于你的无限冒险。",
4
- "version": "1.0.258",
4
+ "version": "1.0.259",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -10,6 +10,7 @@ class Server {
10
10
  private ctx!: Context
11
11
  private onConnectedCallback?: () => void
12
12
  private messageListenerSetup = false
13
+ private readonly lastUserMessageAt = new Map<string, number>()
13
14
  _init = false
14
15
 
15
16
  async init(ctx: Context, config: Config) {
@@ -20,6 +21,12 @@ class Server {
20
21
  if (this.wsClient) {
21
22
  await this.dispose()
22
23
  }
24
+ ctx.setInterval(() => {
25
+ const now = performance.now()
26
+ for (const [key, lastMessageAt] of this.lastUserMessageAt) {
27
+ if (now - lastMessageAt > 500) this.lastUserMessageAt.delete(key)
28
+ }
29
+ }, 60_000)
23
30
  const serverUrl = config.服务器地址
24
31
  if (!serverUrl || serverUrl.trim() === '') {
25
32
  throw new Error('服务器地址不能为空')
@@ -39,6 +46,7 @@ class Server {
39
46
 
40
47
  async dispose() {
41
48
  this._init = false
49
+ this.lastUserMessageAt.clear()
42
50
  if (this.wsClient) {
43
51
  this.wsClient.unlistenMsgAll(/.*/)
44
52
  this.wsClient.disconnect()
@@ -125,7 +133,7 @@ class Server {
125
133
  this.ctx.logger.warn('请求出错', apiName, req.err.message)
126
134
  }
127
135
 
128
- /** 过滤机器人发送者后,缓存会话并把原始内容交给游戏服。 **/
136
+ /** 过滤机器人发送者和用户连续消息后,缓存会话并把原始内容交给游戏服。 **/
129
137
  uploadMessage(session: Session) {
130
138
  if (isBotMessage(session, this.ctx.bots)) return
131
139
  const userId = session.userId
@@ -134,6 +142,13 @@ class Server {
134
142
  this.ctx.logger.warn('消息缺少 userId 或 channelId,已忽略')
135
143
  return
136
144
  }
145
+ const userKey = JSON.stringify([session.platform ?? session.bot.platform, userId])
146
+ const now = performance.now()
147
+ const lastMessageAt = this.lastUserMessageAt.get(userKey)
148
+ // 被忽略的消息也计入上一条,连续快速发送时只有第一条上传。
149
+ this.lastUserMessageAt.set(userKey, now)
150
+ if (lastMessageAt !== undefined && now - lastMessageAt <= 500) return
151
+
137
152
  sessions.set(sessionKey(session.bot.userId, userId, channelId), session)
138
153
  this.api('Message', {
139
154
  bot: {
@@ -155,7 +155,7 @@ function renderQQTaskMarkdown(data: IMessage): string {
155
155
  return sections.join('\n\n') || ' '
156
156
  }
157
157
 
158
- /** Backpack cards use a compact filter row and one quoted, clickable item list. */
158
+ /** Backpack cards use a compact filter row and separate clickable item paragraphs. */
159
159
  const inventoryFilterCategories = [
160
160
  { name: '装备', emoji: '⚔️' },
161
161
  { name: '装备图纸', emoji: '📐' },
@@ -193,7 +193,9 @@ function renderQQInventoryMarkdown(data: IMessage, showGreeting: boolean): strin
193
193
  const sections: string[] = []
194
194
  const items: string[] = []
195
195
  const flushItems = () => {
196
- if (items.length) sections.push(items.splice(0).map(line => '> ' + line).join(' \n'))
196
+ // Avoid a shared blockquote/paragraph around mobile QQ command links.
197
+ // Each item keeps its image and single command in an independent paragraph.
198
+ if (items.length) sections.push(...items.splice(0))
197
199
  }
198
200
  if (showGreeting) sections.push(escapeMarkdown('Ciallo~(∠・ω< )'))
199
201
  for (const [lineIndex, item] of data.line.entries()) {
@@ -126,17 +126,29 @@ async function renderUncached(ctx: Context, card?: SkillCardData, leaderboard?:
126
126
  let timeout: ReturnType<typeof setTimeout> | undefined
127
127
  let workPromise: Promise<{ image: Buffer; cacheable: boolean }> | undefined
128
128
  let expired = false
129
+ let finished = false
129
130
  const release = () => { const next = waiting.shift(); if (next) next(); else running-- }
130
131
  try {
131
132
  const work = async () => {
132
- const entryArtworkPromise = loadCardEntryArtwork(!titleCard && !bloodlineCard && !equipmentCard ? companionCard : undefined)
133
133
  const remoteAddress = titleCard ? titleCard.artworkUrl : bloodlineCard ? bloodlineCard.artworkUrl : equipmentCard ? equipmentCard.artworkUrl : companionCard ? companionCard.artworkUrl : characterCard ? characterCard.artworkUrl : itemCard ? itemCard.artworkUrl : leaderboard ? leaderboard.backgroundUrl : card?.artworkUrl
134
- const dedicatedArt = !titleCard && !bloodlineCard && equipmentCard ? await loadSkillArtwork(equipmentCard.artworkUrl) : undefined
135
- const unknownArt = !titleCard && !bloodlineCard && equipmentCard && !dedicatedArt ? await equipmentFallback() : undefined
136
- const art = !titleCard && !bloodlineCard && equipmentCard
137
- ? dedicatedArt ?? unknownArt
138
- : await loadSkillArtwork(remoteAddress)
139
- const entryArtwork = await entryArtworkPromise
134
+ const isEquipment = !titleCard && !bloodlineCard && Boolean(equipmentCard)
135
+ const isCharacter = !titleCard && !bloodlineCard && !equipmentCard && !companionCard && Boolean(characterCard)
136
+ // Download independent artwork and open the browser together. A cold portrait
137
+ // must not postpone its title download or browser startup.
138
+ const [remoteArt, fallbackArt, entryArtwork, characterAssets, header] = await Promise.all([
139
+ loadSkillArtwork(remoteAddress),
140
+ isEquipment ? equipmentFallback() : undefined,
141
+ loadCardEntryArtwork(!titleCard && !bloodlineCard && !equipmentCard ? companionCard : undefined),
142
+ isCharacter ? Promise.all([characterAsset(), loadSkillArtwork(characterCard!.titleImageUrl)]) : undefined,
143
+ !titleCard && !bloodlineCard && !equipmentCard && !companionCard && !characterCard && !itemCard && leaderboard ? leaderboardHeader() : undefined,
144
+ ctx.puppeteer.page().then(async created => {
145
+ if (expired || finished) { await created.close(); throw new Error(expired ? '绘图超时' : '绘图已结束') }
146
+ page = created
147
+ }),
148
+ ])
149
+ const dedicatedArt = isEquipment ? remoteArt : undefined
150
+ const unknownArt = isEquipment && !dedicatedArt ? fallbackArt : undefined
151
+ const art = isEquipment ? dedicatedArt ?? unknownArt : remoteArt
140
152
  let html: string
141
153
  let loadedCharacterTitle: string | undefined
142
154
  let localAssetsReady = true
@@ -150,23 +162,19 @@ async function renderUncached(ctx: Context, card?: SkillCardData, leaderboard?:
150
162
  } else if (companionCard) {
151
163
  html = companionCardHtml(companionCard, art, entryArtwork.icons)
152
164
  } else if (characterCard) {
153
- const [defaultArtwork, titleArtwork] = await Promise.all([
154
- characterAsset(),
155
- loadSkillArtwork(characterCard.titleImageUrl),
156
- ])
165
+ const [defaultArtwork, titleArtwork] = characterAssets!
157
166
  loadedCharacterTitle = titleArtwork
158
167
  localAssetsReady = Boolean(art || defaultArtwork) && (!characterCard.titleImageUrl || Boolean(titleArtwork))
159
168
  html = characterCardHtml(characterCard, art, undefined, defaultArtwork, titleArtwork, entryArtwork.icons)
160
169
  } else if (itemCard) {
161
170
  html = itemCard.type === '形象卡' ? appearanceCardHtml(itemCard, art) : itemCardHtml(itemCard, art)
162
171
  } else if (leaderboard) {
163
- html = leaderboardHtml(leaderboard, await leaderboardHeader(), art)
172
+ html = leaderboardHtml(leaderboard, header, art)
164
173
  } else {
165
174
  html = skillCardHtml(card!, '', art)
166
175
  }
167
176
  if (expired) throw new Error('绘图超时')
168
- page = await ctx.puppeteer.page()
169
- if (expired) { await page.close(); page = undefined; throw new Error('绘图超时') }
177
+ if (!page) throw new Error('卡片页面缺失')
170
178
  await page.setViewport({ width:760,height:1107,deviceScaleFactor:titleCard || (companionCard || characterCard) && !bloodlineCard && !equipmentCard ? 2 : 1 })
171
179
  await page.setJavaScriptEnabled(false)
172
180
  await page.setRequestInterception(true)
@@ -206,6 +214,7 @@ async function renderUncached(ctx: Context, card?: SkillCardData, leaderboard?:
206
214
  workPromise = work()
207
215
  return await Promise.race([workPromise, new Promise<never>((_,reject) => {timeout=setTimeout(()=>{ expired=true; reject(new Error('绘图超时')) },15000)})])
208
216
  } finally {
217
+ finished = true
209
218
  if(timeout) clearTimeout(timeout)
210
219
  try { await page?.close() } catch {}
211
220
  if (expired && workPromise) {