koishi-plugin-dew-bot 1.0.261 → 1.0.263

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 (30) hide show
  1. package/lib/index.js +316 -105
  2. package/lib/plugin/characterCardAssets.d.ts +3 -1
  3. package/lib/plugin/characterCardStyles.d.ts +1 -1
  4. package/lib/plugin/serverHandel/imageMessageFollowup.d.ts +9 -0
  5. package/lib/plugin/serverHandel/qqMessageButtons.d.ts +27 -0
  6. package/lib/plugin/serverHandel/qqMessagePreview.d.ts +3 -0
  7. package/lib/plugin/serverHandel/qqPageMessage.d.ts +2 -1
  8. package/lib/shared/bot/analysisMsg.d.ts +5 -0
  9. package/lib/shared/bot/inlineCommands.d.ts +11 -0
  10. package/lib/shared/face/index.d.ts +16 -1
  11. package/package.json +1 -1
  12. package/src/plugin/bloodlineCardTemplate.ts +2 -4
  13. package/src/plugin/cardLayout.ts +15 -1
  14. package/src/plugin/characterCardAssets.ts +9 -1
  15. package/src/plugin/characterCardStyles.ts +6 -4
  16. package/src/plugin/characterCardTemplate.ts +9 -8
  17. package/src/plugin/serverHandel/image.ts +5 -5
  18. package/src/plugin/serverHandel/imageMessageFollowup.ts +23 -0
  19. package/src/plugin/serverHandel/qqCommandMessage.ts +10 -3
  20. package/src/plugin/serverHandel/qqMessageButtons.ts +38 -0
  21. package/src/plugin/serverHandel/qqMessagePreview.ts +19 -8
  22. package/src/plugin/serverHandel/qqPageMessage.ts +9 -21
  23. package/src/plugin/serverHandel/qqRewardMessage.ts +2 -0
  24. package/src/plugin/serverHandel/qqStoryMessage.ts +4 -1
  25. package/src/plugin/serverHandel/qqTransactionMessage.ts +4 -1
  26. package/src/plugin/serverHandel/text.ts +1 -1
  27. package/src/shared/bot/analysisMsg.ts +38 -1
  28. package/src/shared/bot/inlineCommands.ts +39 -0
  29. package/src/shared/bot/serviceProto.ts +80 -1
  30. package/src/shared/face/index.ts +18 -1
@@ -1,10 +1,13 @@
1
1
  import type { Session } from 'koishi'
2
2
  import { MessageType, type IMessage, type PageNavData } from '../../shared/face'
3
3
  import type { MsgMessage } from '../../shared/bot/MsgMessage'
4
+ import { withoutMessageButtonFallbacks } from '../../shared/bot/analysisMsg'
5
+ import { renderQQKeyboard, type QQKeyboard } from './qqMessageButtons'
4
6
  import { compactDisplayText, escapeMarkdown, renderQQTitle } from './qqMarkdownHelpers'
5
7
  import { isRewardMessage, renderRewardLine, renderRewardTitle } from './qqRewardMessage'
6
8
  import { artworkIdentity } from '../skillArtwork'
7
9
  import { battleLogLinkSpans, renderBattleLogLink } from './qqBattleLogLinks'
10
+ import { inlineCommandSpans as bracketedCommandSpans, type InlineCommandSpan } from '../../shared/bot/inlineCommands'
8
11
 
9
12
  export { escapeMarkdown, renderQQTitle } from './qqMarkdownHelpers'
10
13
 
@@ -12,6 +15,7 @@ type QQRequest = {
12
15
  content: string
13
16
  msg_type: 2
14
17
  markdown: { content: string }
18
+ keyboard?: QQKeyboard
15
19
  msg_id?: string
16
20
  msg_seq?: number
17
21
  }
@@ -54,27 +58,9 @@ export function inlineCommandUrl(command: string): string {
54
58
  }
55
59
 
56
60
 
57
- type InlineCommandSpan = { start: number; end: number; label: string; command: string }
58
-
59
61
  /** Bracketed commands may appear anywhere in prose, including escaped brackets. */
60
62
  function inlineCommandSpans(content: string): InlineCommandSpan[] {
61
- const spans: InlineCommandSpan[] = []
62
- let depth = 0, start = 0, labelStart = 0
63
- for (let index = 0; index < content.length; index++) {
64
- if (content[index] === '[') {
65
- if (depth === 0) {
66
- start = index > 0 && content[index - 1] === '\\' ? index - 1 : index
67
- labelStart = index + 1
68
- }
69
- depth++
70
- } else if (content[index] === ']' && depth > 0 && --depth === 0) {
71
- const label = content.slice(labelStart, index).replace(/\\$/, '')
72
- if (!label.trim() || /[\[\]\r\n]/.test(label) || /^\d+$/.test(label.trim())) continue
73
- // The refresh icon is part of the displayed label, not the command to send.
74
- const command = label.split(/[++]/, 1)[0].trim().replace(/^🔄\s*/u, '')
75
- if (command) spans.push({ start, end: index + 1, label, command })
76
- }
77
- }
63
+ const spans = bracketedCommandSpans(content)
78
64
  const logLinks = battleLogLinkSpans(content)
79
65
  return spans.filter(span => !logLinks.some(link => span.start < link.end && span.end > link.start))
80
66
  }
@@ -233,6 +219,7 @@ function renderQQInventoryMarkdown(data: IMessage, showGreeting: boolean): strin
233
219
  }
234
220
 
235
221
  export function renderQQPageMarkdown(data: IMessage, showGreeting: boolean): string {
222
+ data = withoutMessageButtonFallbacks(data)
236
223
  const hasEntryLabels = data.pageNav?.entries?.some(entry => entry.label !== undefined) ?? false
237
224
  if (data.task && !hasEntryLabels) return renderQQTaskMarkdown(data)
238
225
  if (!hasEntryLabels && data.pageNav && data.line.some(item => item.type === MessageType.title
@@ -375,11 +362,11 @@ export function canSendQQPage(session: Session | undefined, data: MsgMessage): s
375
362
  /** Send a native QQ Markdown page with inline command links. */
376
363
  export async function sendQQPage(session: Session, data: MsgMessage): Promise<void> {
377
364
  const markdown = renderQQPageMarkdown(data, session.event?.channel?.type === 0)
378
- await sendQQMarkdown(session, markdown)
365
+ await sendQQMarkdown(session, markdown, renderQQKeyboard(data))
379
366
  }
380
367
 
381
368
  /** Reuse the adapter's native reply API for structured Markdown messages. */
382
- export async function sendQQMarkdown(session: Session, markdown: string): Promise<void> {
369
+ export async function sendQQMarkdown(session: Session, markdown: string, keyboard?: QQKeyboard): Promise<void> {
383
370
  const internal = (session.bot as typeof session.bot & { internal: QQInternal }).internal
384
371
  const request: QQRequest = {
385
372
  // With msg_type=2, QQ requires the ordinary content field to be empty.
@@ -387,6 +374,7 @@ export async function sendQQMarkdown(session: Session, markdown: string): Promis
387
374
  msg_type: 2,
388
375
  markdown: { content: markdown },
389
376
  }
377
+ if (keyboard) request.keyboard = keyboard
390
378
  // Match the QQ adapter's passive-reply sequence.
391
379
  const age = Date.now() - session.timestamp
392
380
  if (session.messageId && Number.isFinite(age) && age >= 0 && age < 5 * 60 * 1000 - 2000) {
@@ -1,3 +1,4 @@
1
+ import { withoutMessageButtonFallbacks } from '../../shared/bot/analysisMsg'
1
2
  import { MessageType, type IMessage } from '../../shared/face'
2
3
  import { compactDisplayText, escapeMarkdown } from './qqMarkdownHelpers'
3
4
  import { appendMissingBattleLogLinks, renderBattleLogLinkText } from './qqBattleLogLinks'
@@ -68,6 +69,7 @@ function renderQQMailMarkdown(data: IMessage): string {
68
69
 
69
70
  /** Reward cards stay compact; mail keeps its content before the attachment quote. */
70
71
  export function renderQQRewardMarkdown(data: IMessage, showGreeting: boolean): string {
72
+ data = withoutMessageButtonFallbacks(data)
71
73
  if (data.mail) return appendMissingBattleLogLinks(renderQQMailMarkdown(data), data)
72
74
  const rewards = data.line.filter(item => item.type === MessageType.line && item.reward)
73
75
  .map(item => renderRewardLine(item.content)).filter(Boolean)
@@ -1,3 +1,5 @@
1
+ import { withoutMessageButtonFallbacks } from '../../shared/bot/analysisMsg'
2
+ import { renderQQKeyboard } from './qqMessageButtons'
1
3
  import type { Session } from 'koishi'
2
4
  import { MessageType, type IMessage } from '../../shared/face'
3
5
  import type { MsgMessage } from '../../shared/bot/MsgMessage'
@@ -20,6 +22,7 @@ function fencedStory(content: string): string {
20
22
 
21
23
  /** Marked stories keep their complete text; settled rewards use the shared reward card. */
22
24
  export function renderQQStoryMarkdown(data: IMessage, showGreeting: boolean): string {
25
+ data = withoutMessageButtonFallbacks(data)
23
26
  if (data.reward === true && data.line.some(item => item.type === MessageType.line && item.reward === true && item.content.trim())) {
24
27
  const stories = data.line.filter(item => item.type === MessageType.line && item.codeBlock === true)
25
28
  .map(item => fencedStory(item.content))
@@ -75,5 +78,5 @@ export function renderQQStoryMarkdown(data: IMessage, showGreeting: boolean): st
75
78
  }
76
79
 
77
80
  export async function sendQQStory(session: Session, data: MsgMessage): Promise<void> {
78
- await sendQQMarkdown(session, renderQQStoryMarkdown(data, session.event?.channel?.type === 0))
81
+ await sendQQMarkdown(session, renderQQStoryMarkdown(data, session.event?.channel?.type === 0), renderQQKeyboard(data))
79
82
  }
@@ -1,3 +1,5 @@
1
+ import { withoutMessageButtonFallbacks } from '../../shared/bot/analysisMsg'
2
+ import { renderQQKeyboard } from './qqMessageButtons'
1
3
  import type { Session } from 'koishi'
2
4
  import { MessageType, type IMessage } from '../../shared/face'
3
5
  import type { MsgMessage } from '../../shared/bot/MsgMessage'
@@ -6,6 +8,7 @@ import { isRewardMessage, renderRewardLine, renderRewardTitle } from './qqReward
6
8
 
7
9
  /** Render only explicitly marked transactions; line content never enables this channel. */
8
10
  export function renderQQTransactionMarkdown(data: IMessage, showGreeting: boolean): string {
11
+ data = withoutMessageButtonFallbacks(data)
9
12
  const rewardMessage = isRewardMessage(data)
10
13
  const shortageMessage = data.transaction === true && data.line.some(item =>
11
14
  item.type === MessageType.title && item.content === '❌材料不足')
@@ -90,5 +93,5 @@ export function renderQQTransactionMarkdown(data: IMessage, showGreeting: boolea
90
93
  }
91
94
 
92
95
  export async function sendQQTransaction(session: Session, data: MsgMessage): Promise<void> {
93
- await sendQQMarkdown(session, renderQQTransactionMarkdown(data, session.event?.channel?.type === 0))
96
+ await sendQQMarkdown(session, renderQQTransactionMarkdown(data, session.event?.channel?.type === 0), renderQQKeyboard(data))
94
97
  }
@@ -12,7 +12,7 @@ export default class handelr_text extends handel_base {
12
12
  if (preview.markdown !== undefined && canSendQQPage(session, data)) {
13
13
  const sendStartedAt = this.timing ? performance.now() : 0
14
14
  try {
15
- await sendQQMarkdown(session, preview.markdown)
15
+ await sendQQMarkdown(session, preview.markdown, preview.keyboard)
16
16
  if (this.timing) {
17
17
  this.timing.delivery = 'qq-markdown'
18
18
  this.timing.sendMs = performance.now() - sendStartedAt
@@ -1,5 +1,39 @@
1
1
  import { IMessage, MessageType, type PageNavData } from '../face'
2
+ import { renderPlainInlineCommandAliases } from './inlineCommands'
2
3
 
4
+ /** Platform-neutral button hints also work when Markdown or keyboards are unavailable. */
5
+ export function messageButtonFallbackLines(data: Pick<IMessage, 'buttons'>): string[] {
6
+ return [...new Set((data.buttons ?? []).flatMap(row => row.buttons.map(button =>
7
+ '[' + button.command + ']' + button.label)))]
8
+ }
9
+
10
+ function matchesButtonFallback(content: string, fallback: string): boolean {
11
+ const bracket = fallback.indexOf(']')
12
+ const prefix = fallback.slice(0, bracket + 1)
13
+ const line = content.trim()
14
+ return line.startsWith(prefix) && line.slice(prefix.length).trim() === fallback.slice(prefix.length).trim()
15
+ }
16
+
17
+ export function appendMessageButtonFallbacks(content: string, data: Pick<IMessage, 'buttons'>): string {
18
+ const lines = content.replace(/\r\n?/g, '\n').split('\n')
19
+ const missing = messageButtonFallbackLines(data).filter(fallback =>
20
+ !lines.some(line => matchesButtonFallback(line, fallback)))
21
+ if (!missing.length) return content
22
+ return content + (content && !content.endsWith('\n') ? '\n' : '') + missing.join('\n') + '\n'
23
+ }
24
+
25
+ /** Remove only complete action hints; keep source line indexes used by page metadata. */
26
+ export function withoutMessageButtonFallbacks<T extends IMessage>(data: T): T {
27
+ const fallbacks = messageButtonFallbackLines(data)
28
+ if (!fallbacks.length) return data
29
+ return { ...data, line: data.line.map(item => {
30
+ if (item.type !== MessageType.line || item.codeBlock || item.reward || item.mailPart !== undefined) return item
31
+ const lines = item.content.replace(/\r\n?/g, '\n').split('\n')
32
+ const kept = lines.filter(line => !fallbacks.some(fallback => matchesButtonFallback(line, fallback)))
33
+ return kept.length === lines.length ? item : { ...item, content: kept.join('\n') }
34
+ }) }
35
+ }
36
+
3
37
  /** 把服务器下发的结构化行拼成纯文本;第一阶段只处理 title / line / error **/
4
38
  export class AnalysisMsg {
5
39
  parse(data: IMessage) {
@@ -31,7 +65,9 @@ export class AnalysisMsg {
31
65
  ? labeledRow.map(entry => entry.label! + (entry.description ? ' · ' + entry.description : '')
32
66
  + ` [${entry.viewCommand}]`).join('')
33
67
  : withCommandHint(item.content, commands.get(lineIndex))
34
- msg += `${content}\n`
68
+ const plainContent = item.codeBlock || item.mailPart === 'body' || labeledRow
69
+ ? content : renderPlainInlineCommandAliases(content)
70
+ msg += `${plainContent}\n`
35
71
  break
36
72
  case MessageType.error:
37
73
  msg += `❌出错了!错误原因:${item.content},请反馈至联系开发者\n`
@@ -40,6 +76,7 @@ export class AnalysisMsg {
40
76
  break
41
77
  }
42
78
  }
79
+ msg = appendMessageButtonFallbacks(msg, data)
43
80
  if (haveTitle) {
44
81
  msg += '┄━════════━┄'
45
82
  }
@@ -0,0 +1,39 @@
1
+ export type InlineCommandSpan = { start: number; end: number; label: string; command: string; aliased?: true }
2
+
3
+ /** Boxed commands may use [label](command:input) to show a different action label. */
4
+ export function inlineCommandSpans(content: string): InlineCommandSpan[] {
5
+ const spans: InlineCommandSpan[] = []
6
+ let depth = 0, start = 0, labelStart = 0
7
+ for (let index = 0; index < content.length; index++) {
8
+ if (content[index] === '[') {
9
+ if (depth === 0) {
10
+ start = index > 0 && content[index - 1] === '\\' ? index - 1 : index
11
+ labelStart = index + 1
12
+ }
13
+ depth++
14
+ } else if (content[index] === ']' && depth > 0 && --depth === 0) {
15
+ const label = content.slice(labelStart, index).replace(/\\$/, '')
16
+ if (!label.trim() || /[\[\]\r\n]/.test(label) || /^\d+$/.test(label.trim())) continue
17
+ const alias = /^\(command:([^()[\]\r\n]+)\)/.exec(content.slice(index + 1))
18
+ // Ordinary templates keep their existing command extraction rules.
19
+ const command = alias ? alias[1].trim() : label.split(/[++]/, 1)[0].trim().replace(/^🔄\s*/u, '')
20
+ if (!command) continue
21
+ const end = index + 1 + (alias?.[0].length ?? 0)
22
+ spans.push({ start, end, label, command, ...(alias ? { aliased: true as const } : {}) })
23
+ if (alias) index = end - 1
24
+ }
25
+ }
26
+ return spans
27
+ }
28
+
29
+ /** Keep aliased actions usable when the platform falls back to ordinary text. */
30
+ export function renderPlainInlineCommandAliases(content: string): string {
31
+ let result = '', offset = 0
32
+ for (const span of inlineCommandSpans(content)) {
33
+ if (!span.aliased) continue
34
+ result += content.slice(offset, span.start)
35
+ + (span.label.trim() === span.command ? `[${span.command}]` : `${span.label}[${span.command}]`)
36
+ offset = span.end
37
+ }
38
+ return result + content.slice(offset)
39
+ }
@@ -25,7 +25,7 @@ export interface ServiceType {
25
25
  }
26
26
 
27
27
  export const serviceProto: ServiceProto<ServiceType> = {
28
- "version": 37,
28
+ "version": 39,
29
29
  "services": [
30
30
  {
31
31
  "id": 0,
@@ -253,6 +253,18 @@ export const serviceProto: ServiceProto<ServiceType> = {
253
253
  "target": "messageFace/TitleCardData"
254
254
  },
255
255
  "optional": true
256
+ },
257
+ {
258
+ "id": 17,
259
+ "name": "buttons",
260
+ "type": {
261
+ "type": "Array",
262
+ "elementType": {
263
+ "type": "Reference",
264
+ "target": "messageFace/MessageButtonRow"
265
+ }
266
+ },
267
+ "optional": true
256
268
  }
257
269
  ]
258
270
  },
@@ -709,6 +721,14 @@ export const serviceProto: ServiceProto<ServiceType> = {
709
721
  }
710
722
  },
711
723
  "optional": true
724
+ },
725
+ {
726
+ "id": 14,
727
+ "name": "explorationRank",
728
+ "type": {
729
+ "type": "String"
730
+ },
731
+ "optional": true
712
732
  }
713
733
  ]
714
734
  },
@@ -1849,6 +1869,65 @@ export const serviceProto: ServiceProto<ServiceType> = {
1849
1869
  }
1850
1870
  ]
1851
1871
  },
1872
+ "messageFace/MessageButtonRow": {
1873
+ "type": "Interface",
1874
+ "properties": [
1875
+ {
1876
+ "id": 0,
1877
+ "name": "buttons",
1878
+ "type": {
1879
+ "type": "Array",
1880
+ "elementType": {
1881
+ "type": "Reference",
1882
+ "target": "messageFace/MessageButton"
1883
+ }
1884
+ }
1885
+ }
1886
+ ]
1887
+ },
1888
+ "messageFace/MessageButton": {
1889
+ "type": "Interface",
1890
+ "properties": [
1891
+ {
1892
+ "id": 0,
1893
+ "name": "label",
1894
+ "type": {
1895
+ "type": "String"
1896
+ }
1897
+ },
1898
+ {
1899
+ "id": 1,
1900
+ "name": "command",
1901
+ "type": {
1902
+ "type": "String"
1903
+ }
1904
+ },
1905
+ {
1906
+ "id": 2,
1907
+ "name": "style",
1908
+ "type": {
1909
+ "type": "Union",
1910
+ "members": [
1911
+ {
1912
+ "id": 0,
1913
+ "type": {
1914
+ "type": "Literal",
1915
+ "literal": "primary"
1916
+ }
1917
+ },
1918
+ {
1919
+ "id": 1,
1920
+ "type": {
1921
+ "type": "Literal",
1922
+ "literal": "secondary"
1923
+ }
1924
+ }
1925
+ ]
1926
+ },
1927
+ "optional": true
1928
+ }
1929
+ ]
1930
+ },
1852
1931
  "PtlMessage/ReqMessage": {
1853
1932
  "type": "Interface",
1854
1933
  "extends": [
@@ -38,7 +38,7 @@ export interface CharacterCardData {
38
38
  bloodlines: Array<{ name: string; quality: string; artworkUrl?: string }>;
39
39
  /** 当前基因锁大阶的配置名称,不包含层级;未开启时为基因锁未开启。 */
40
40
  geneLockRankName?: string;
41
- /** 当前称号的图片链接;未获得称号或图片不可用时省略,客户端显示图片占位。 */
41
+ /** 当前称号的图片链接;未获得称号或图片不可用时省略,客户端不显示称号横幅。 */
42
42
  titleImageUrl?: string;
43
43
  /** 已获得的真实基因锁大阶(0~5);0 表示尚未解锁。 */
44
44
  geneLockTier?: number;
@@ -46,6 +46,8 @@ export interface CharacterCardData {
46
46
  equipment?: Array<{ id: string; name: string; quality: string; slot: string; enhancement?: number }>;
47
47
  /** 当前构筑的全部技能,包括装备、递归附带与限时增益来源。 */
48
48
  skills?: Array<{ id: string; name: string; rank: number; quality?: string; type?: CompanionSkillType; artworkUrl?: string }>;
49
+ /** 玩家当前的轮回探索品阶(F~SSS),与角色等级、基因锁阶级分开显示。 */
50
+ explorationRank?: string;
49
51
  }
50
52
  export enum CompanionCardKind {
51
53
  pet = 'pet',
@@ -196,6 +198,19 @@ export interface PageNavData {
196
198
  filters?: Array<{ name: string; emoji?: string; command: string }>;
197
199
  }
198
200
 
201
+ /** 平台无关的消息按钮;点击后发送普通指令,业务仍负责权限与状态校验。 */
202
+ export interface MessageButton {
203
+ label: string;
204
+ command: string;
205
+ /** 主操作蓝色线框,次操作灰色线框;缺省为次操作。 */
206
+ style?: 'primary' | 'secondary';
207
+ }
208
+
209
+ /** 同一行的按钮;QQ 原生键盘最多五行,每行最多五个。 */
210
+ export interface MessageButtonRow {
211
+ buttons: MessageButton[];
212
+ }
213
+
199
214
  /** 下发给客户端的消息主体 **/
200
215
  export interface IMessage {
201
216
  handleType: MessageHandleType;
@@ -222,4 +237,6 @@ export interface IMessage {
222
237
  line: IMessageItem[];
223
238
  companionCard?: CompanionCardData;
224
239
  titleCard?: TitleCardData;
240
+ /** 通用指令按钮;不支持原生按钮的平台保留文字指令。 */
241
+ buttons?: MessageButtonRow[];
225
242
  }