koishi-plugin-dew-bot 1.0.248 → 1.0.250

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
@@ -310,6 +310,7 @@ function equipmentCardHtml(card, artwork, unknownFallback = false) {
310
310
  const notes = card.notes?.filter(Boolean).length ? `<div class="notes">${card.notes.filter(Boolean).map((note) => `<p>${escape6(note)}</p>`).join("")}</div>` : "";
311
311
  const enhancement = Number(card.enhancement) > 0 ? `<span class="enhance"><small>强化</small>+${escape6(card.enhancement)}</span>` : "";
312
312
  const price = card.referencePrice ? `<div class="price"><span>参考价格</span><strong>${escape6(card.referencePrice)}</strong></div>` : "";
313
+ const author = `<span class="author"><small>打造者</small>${escape6(card.authorName?.trim() || "未知")}</span>`;
313
314
  return `<!doctype html><html><head><meta charset="utf-8"><style>
314
315
  *{box-sizing:border-box}
315
316
  html,body{margin:0;width:760px;background:#f1eee5;font-family:'Microsoft YaHei','Noto Sans CJK SC',sans-serif;color:#343b35}
@@ -318,14 +319,14 @@ html,body{margin:0;width:760px;background:#f1eee5;font-family:'Microsoft YaHei',
318
319
  #capture:after{content:'';position:absolute;inset:17px;border:1px solid #8c8b6f22;pointer-events:none}
319
320
  .heading{display:flex;align-items:center;gap:24px}.heading-copy{flex:1;min-width:0}.overline{font-size:14px;letter-spacing:6px;color:#868a76;margin-bottom:14px}.name{font:700 43px/1.35 'KaiTi','STKaiti','Noto Serif CJK SC','SimSun',serif;margin:0;color:#303a31;overflow-wrap:anywhere}
320
321
  .seal{flex-shrink:0;width:66px;min-height:74px;transform:rotate(3deg);padding:8px 3px;color:#a04b40;border:2px solid #a04b40;outline:1px solid #a04b40;outline-offset:-6px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;border-radius:3px 1px 4px 2px}.seal strong{font:700 24px Georgia,'KaiTi',serif;letter-spacing:-1px}.seal span{font:16px 'KaiTi','SimSun',serif;letter-spacing:4px;padding-left:4px}
321
- .meta{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px 26px;margin:24px 0 0;padding:15px 0;border-top:1px solid #8a8a7144;border-bottom:1px solid #8a8a7128;font-size:19px;color:#535d50;line-height:1.5}.meta .slot{color:#a04b40;font:21px 'KaiTi','SimSun',serif;letter-spacing:2px}.meta small{font-size:16px;color:#92917f;margin-right:9px}
322
+ .meta{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px 26px;margin:24px 0 0;padding:15px 0;border-top:1px solid #8a8a7144;border-bottom:1px solid #8a8a7128;font-size:19px;color:#535d50;line-height:1.5}.meta .slot{color:#a04b40;font:21px 'KaiTi','SimSun',serif;letter-spacing:2px}.meta .author{min-width:0;overflow-wrap:anywhere}.meta small{font-size:16px;color:#92917f;margin-right:9px}
322
323
  .art{position:relative;width:610px;height:610px;max-width:100%;margin:18px auto 11px;display:grid;place-items:center;isolation:isolate;overflow:hidden}.art:before{content:'';position:absolute;inset:4%;border-radius:48% 52% 56% 44%;background:radial-gradient(ellipse,#aab4a443,transparent 69%);transform:rotate(-12deg)}.equipment-artwork{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center;filter:saturate(.72) contrast(.97);mix-blend-mode:multiply;mask-image:linear-gradient(to bottom,transparent 0,#000 8%,#000 84%,transparent 100%),linear-gradient(to right,transparent 0,#000 7%,#000 93%,transparent 100%);mask-composite:intersect}.art-fallback{position:relative;width:360px;height:360px;max-width:90%;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#7d8b7d;border:1px solid #7c887d3b;border-radius:50%;background:radial-gradient(circle,#b1b9a435,transparent 70%);font-family:'KaiTi','SimSun',serif}.art-fallback:before,.art-fallback:after{content:'';position:absolute;inset:21px;border:1px solid #7c887d22;border-radius:50%}.art-fallback:after{inset:48px;border-color:#9a9e8533}.art-fallback .glyph{font-size:126px;line-height:1.2;opacity:.46}.art-fallback .kind{font-size:20px;letter-spacing:4px;opacity:.55}.art:has(.equipment-artwork) .art-fallback{visibility:hidden}.placeholder-note{position:absolute;right:18px;bottom:16px;z-index:2;padding:5px 9px;background:#f4f1e8ce;border:1px solid #9d9b8755;color:#536055;font:18px 'KaiTi','SimSun',serif;letter-spacing:2px}
323
324
  .power{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:18px 0 22px;border-top:1px solid #8a8a7128;border-bottom:1px solid #8a8a7144;color:#31463e}.power span{font:24px 'KaiTi','SimSun',serif;letter-spacing:12px}.power strong{font:700 45px/1.1 Georgia,'Times New Roman',serif;letter-spacing:2px;font-variant-numeric:lining-nums}
324
325
  .section{margin-top:29px}.section h2{display:flex;align-items:center;gap:16px;margin:0 0 17px;color:#626e5e;font:21px 'KaiTi','SimSun',serif;letter-spacing:5px}.section h2:before{content:'';width:5px;height:17px;background:#a04b40;transform:rotate(8deg);opacity:.75}.section h2:after{content:'';height:1px;flex:1;background:linear-gradient(to right,#7f897c55,transparent)}
325
326
  .affixes{display:grid;grid-template-columns:1fr 1fr;column-gap:26px}.affix{display:flex;align-items:baseline;justify-content:space-between;gap:10px;min-width:0;padding:9px 0 12px;border-bottom:1px solid #8a8a7124;font-size:18px;color:#697064}.affix span{overflow-wrap:anywhere}.affix strong{flex-shrink:0;font:700 22px Georgia,'KaiTi',serif;color:#344b42}.empty-line{font-size:18px;color:#92917f;margin:4px 0 12px}
326
327
  .skill{display:flex;align-items:baseline;justify-content:space-between;gap:18px;padding:5px 0 10px;font-size:20px;color:#304b40}.skill-name{font-weight:700;overflow-wrap:anywhere}.skill-name i{display:inline-block;width:13px;height:13px;margin:0 17px 1px 4px;background:#a04b40;transform:rotate(45deg)}.rank{font:18px Georgia,'KaiTi',serif;color:#7f8d7b;white-space:nowrap}
327
328
  .description p{margin:0;font:22px/1.75 'KaiTi','STKaiti','Noto Serif CJK SC','SimSun',serif;white-space:pre-wrap;overflow-wrap:anywhere;color:#313f38}.price{display:flex;align-items:baseline;gap:16px;margin-top:24px;padding-top:17px;border-top:1px solid #8a8a7128;font-size:18px;color:#8b907d}.price strong{font-size:18px;color:#354c42}.notes{margin-top:17px;padding:10px 15px;border-left:3px solid #a04b4080;background:#e8e8db70;color:#3e5549}.notes p{margin:5px 0;font-size:17px;line-height:1.6;overflow-wrap:anywhere}.edition{display:flex;align-items:center;gap:12px;justify-content:center;margin-top:34px;color:#a6a38e;font:14px 'KaiTi','SimSun',serif;letter-spacing:5px}.edition:before,.edition:after{content:'';height:1px;width:32px;background:#9b9c833d}
328
- </style></head><body><div id="capture" class="equipment-sheet ink-equipment-sheet"><header><div class="heading"><div class="heading-copy"><div class="overline">装 备 图 录</div><h1 class="name">${escape6(card.name)}</h1></div><div class="seal"><strong>${escape6(card.quality)}</strong><span>品阶</span></div></div><div class="meta"><span class="slot">${escape6(card.slot)}</span>${enhancement}</div></header><div class="art"><div class="art-fallback"><span class="glyph">?</span><span class="kind">未知外观</span></div>${artwork ? `<img class="skill-artwork equipment-artwork" alt="" src="${escape6(artwork)}">` : ""}${unknownFallback || !artwork ? '<span class="placeholder-note">外观暂未显现</span>' : ""}</div><div class="power"><span>装 备 战 力</span><strong>${escape6(power)}</strong></div><section class="section attributes"><h2>附加属性</h2><div class="affixes">${affixes}</div></section>${skills}${description}${price}${notes}<div class="edition">露 · 装备录</div></div></body></html>`;
329
+ </style></head><body><div id="capture" class="equipment-sheet ink-equipment-sheet"><header><div class="heading"><div class="heading-copy"><div class="overline">装 备 图 录</div><h1 class="name">${escape6(card.name)}</h1></div><div class="seal"><strong>${escape6(card.quality)}</strong><span>品阶</span></div></div><div class="meta"><span class="slot">${escape6(card.slot)}</span>${enhancement}${author}</div></header><div class="art"><div class="art-fallback"><span class="glyph">?</span><span class="kind">未知外观</span></div>${artwork ? `<img class="skill-artwork equipment-artwork" alt="" src="${escape6(artwork)}">` : ""}${unknownFallback || !artwork ? '<span class="placeholder-note">外观暂未显现</span>' : ""}</div><div class="power"><span>装 备 战 力</span><strong>${escape6(power)}</strong></div><section class="section attributes"><h2>附加属性</h2><div class="affixes">${affixes}</div></section>${skills}${description}${price}${notes}<div class="edition">露 · 装备录</div></div></body></html>`;
329
330
  }
330
331
  __name(equipmentCardHtml, "equipmentCardHtml");
331
332
 
@@ -783,9 +784,9 @@ function renderQQPageMarkdown(data, showGreeting) {
783
784
  const listEntry = listPattern.exec(item.content);
784
785
  if (listEntry) {
785
786
  flushBody();
786
- const command = entryCommands.get(lineIndex);
787
+ const command2 = entryCommands.get(lineIndex);
787
788
  const label = escapeMarkdown(compactListLabel(listEntry[2]));
788
- const name2 = command ? `[${label}](${inlineCommandUrl(command)})` : label;
789
+ const name2 = command2 ? `[${label}](${inlineCommandUrl(command2)})` : label;
789
790
  listLines.push(`${listEntry[1]}. ${name2}`);
790
791
  break;
791
792
  }
@@ -806,7 +807,8 @@ function renderQQPageMarkdown(data, showGreeting) {
806
807
  hasPageSummary = true;
807
808
  break;
808
809
  }
809
- bodyLines.push(escapeMarkdown(item.content));
810
+ const command = entryCommands.get(lineIndex);
811
+ bodyLines.push(command ? `[${escapeMarkdown(compactDisplayText(item.content))}](${inlineCommandUrl(command)})` : escapeMarkdown(item.content));
810
812
  break;
811
813
  }
812
814
  case 2 /* error */:
@@ -837,20 +839,17 @@ function canSendQQPage(session, data) {
837
839
  }
838
840
  __name(canSendQQPage, "canSendQQPage");
839
841
  async function sendQQPage(session, data) {
840
- const internal = session.bot.internal;
841
- const nav = data.pageNav;
842
842
  const markdown = renderQQPageMarkdown(data, session.event?.channel?.type === 0);
843
- const hasInlineNavigation = !!(nav.previousCommand || nav.nextCommand || nav.entries?.length);
844
- const hasInlineHelp = data.line.some((item) => item.type === 1 /* line */ && item.content === "[指令]相关指令");
843
+ await sendQQMarkdown(session, markdown);
844
+ }
845
+ __name(sendQQPage, "sendQQPage");
846
+ async function sendQQMarkdown(session, markdown) {
847
+ const internal = session.bot.internal;
845
848
  const request = {
846
849
  // With msg_type=2, QQ requires the ordinary content field to be empty.
847
850
  content: "",
848
851
  msg_type: 2,
849
- markdown: {
850
- content: !session.isDirect && (hasInlineNavigation || hasInlineHelp) ? `${markdown}
851
-
852
- 点击后如未自动发送,请点发送。` : markdown
853
- }
852
+ markdown: { content: markdown }
854
853
  };
855
854
  const age = Date.now() - session.timestamp;
856
855
  if (session.messageId && Number.isFinite(age) && age >= 0 && age < 5 * 60 * 1e3 - 2e3) {
@@ -863,7 +862,63 @@ async function sendQQPage(session, data) {
863
862
  const pendingAudit = response?.code === 304023 && !!response.data?.message_audit?.audit_id;
864
863
  if (!response?.id && !response?.audit_id && !pendingAudit) throw new Error("QQ 未确认消息已接收");
865
864
  }
866
- __name(sendQQPage, "sendQQPage");
865
+ __name(sendQQMarkdown, "sendQQMarkdown");
866
+
867
+ // src/plugin/serverHandel/qqTransactionMessage.ts
868
+ function renderQQTransactionMarkdown(data, showGreeting) {
869
+ const sections = [];
870
+ const bodyLines = [];
871
+ const actions = [];
872
+ const flushBody = /* @__PURE__ */ __name(() => {
873
+ if (bodyLines.length) sections.push(bodyLines.splice(0).join(" \n"));
874
+ }, "flushBody");
875
+ const flushActions = /* @__PURE__ */ __name(() => {
876
+ if (actions.length) sections.push(actions.splice(0).join(" · "));
877
+ }, "flushActions");
878
+ const flush = /* @__PURE__ */ __name(() => {
879
+ flushBody();
880
+ flushActions();
881
+ }, "flush");
882
+ if (showGreeting) sections.push(escapeMarkdown("Ciallo~(∠・ω< )"));
883
+ for (const item of data.line) {
884
+ if (item.type === 0 /* title */) {
885
+ flush();
886
+ sections.push(`**${escapeMarkdown(item.content)}**`);
887
+ continue;
888
+ }
889
+ if (item.type === 2 /* error */) {
890
+ flush();
891
+ sections.push(`❌ 出错了!错误原因:${escapeMarkdown(item.content)},请反馈至联系开发者`);
892
+ continue;
893
+ }
894
+ if (item.type !== 1 /* line */) continue;
895
+ const action = data.transaction && (item.content === "[是]确认交易" ? { command: "是", label: "确认交易" } : item.content === "[否]取消交易" ? { command: "否", label: "取消交易" } : void 0);
896
+ if (action) {
897
+ flushBody();
898
+ actions.push(`[${action.command}](${inlineCommandUrl(action.command)}) ${action.label}`);
899
+ continue;
900
+ }
901
+ flushActions();
902
+ if (!item.content.trim()) {
903
+ flushBody();
904
+ continue;
905
+ }
906
+ const section = /^┄━═(.+)═━┄$/.exec(item.content);
907
+ if (section || item.content === "【还缺】") {
908
+ flushBody();
909
+ sections.push(`**${escapeMarkdown(section?.[1] ?? "还缺")}**`);
910
+ } else {
911
+ bodyLines.push(escapeMarkdown(item.content));
912
+ }
913
+ }
914
+ flush();
915
+ return sections.join("\n\n") || " ";
916
+ }
917
+ __name(renderQQTransactionMarkdown, "renderQQTransactionMarkdown");
918
+ async function sendQQTransaction(session, data) {
919
+ await sendQQMarkdown(session, renderQQTransactionMarkdown(data, session.event?.channel?.type === 0));
920
+ }
921
+ __name(sendQQTransaction, "sendQQTransaction");
867
922
 
868
923
  // src/plugin/serverHandel/text.ts
869
924
  var handelr_text = class extends handel_base {
@@ -873,10 +928,11 @@ var handelr_text = class extends handel_base {
873
928
  async start(data) {
874
929
  const content = new AnalysisMsg().parse(data);
875
930
  const session = sessions_default.get(sessionKey(data.botId, data.userId, data.channelId));
876
- if (data.pageNav && canSendQQPage(session, data)) {
931
+ if ((data.transaction || data.pageNav) && canSendQQPage(session, data)) {
877
932
  const sendStartedAt = this.timing ? performance.now() : 0;
878
933
  try {
879
- await sendQQPage(session, data);
934
+ if (data.transaction) await sendQQTransaction(session, data);
935
+ else await sendQQPage(session, data);
880
936
  if (this.timing) {
881
937
  this.timing.delivery = "qq-markdown";
882
938
  this.timing.sendMs = performance.now() - sendStartedAt;
@@ -885,7 +941,7 @@ var handelr_text = class extends handel_base {
885
941
  } catch (error) {
886
942
  this.ctx.logger.warn("QQ Markdown 发送失败,改用普通文字", error instanceof Error ? error.message : error);
887
943
  }
888
- const commands = pageCommandLines(data.pageNav);
944
+ const commands = !data.transaction && data.pageNav ? pageCommandLines(data.pageNav) : [];
889
945
  await this.send(data, commands.length ? `${content.trimEnd()}
890
946
  ${commands.join("\n")}` : content);
891
947
  return;
@@ -958,7 +1014,7 @@ var import_tsrpc = require("tsrpc");
958
1014
 
959
1015
  // src/shared/bot/serviceProto.ts
960
1016
  var serviceProto = {
961
- "version": 10,
1017
+ "version": 12,
962
1018
  "services": [
963
1019
  {
964
1020
  "id": 0,
@@ -1093,6 +1149,14 @@ var serviceProto = {
1093
1149
  },
1094
1150
  "optional": true
1095
1151
  },
1152
+ {
1153
+ "id": 8,
1154
+ "name": "transaction",
1155
+ "type": {
1156
+ "type": "Boolean"
1157
+ },
1158
+ "optional": true
1159
+ },
1096
1160
  {
1097
1161
  "id": 1,
1098
1162
  "name": "line",
@@ -1550,6 +1614,14 @@ var serviceProto = {
1550
1614
  }
1551
1615
  },
1552
1616
  "optional": true
1617
+ },
1618
+ {
1619
+ "id": 12,
1620
+ "name": "authorName",
1621
+ "type": {
1622
+ "type": "String"
1623
+ },
1624
+ "optional": true
1553
1625
  }
1554
1626
  ]
1555
1627
  },
@@ -1,9 +1,14 @@
1
1
  import type { Session } from 'koishi';
2
2
  import { type PageNavData } from '../../shared/face';
3
3
  import type { MsgMessage } from '../../shared/bot/MsgMessage';
4
+ /** Escape server-provided lines before embedding them in native QQ Markdown. */
5
+ export declare function escapeMarkdown(value: string): string;
6
+ export declare function inlineCommandUrl(command: string): string;
4
7
  export declare function pageCommandLines(nav: PageNavData): string[];
5
8
  export declare function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean): string;
6
9
  /** Returns false if this is not the matching QQ session or the adapter has no native API. */
7
10
  export declare function canSendQQPage(session: Session | undefined, data: MsgMessage): session is Session;
8
11
  /** Send a native QQ Markdown page with inline command links. */
9
12
  export declare function sendQQPage(session: Session, data: MsgMessage): Promise<void>;
13
+ /** Reuse the adapter's native reply API for structured Markdown messages. */
14
+ export declare function sendQQMarkdown(session: Session, markdown: string): Promise<void>;
@@ -0,0 +1,5 @@
1
+ import type { Session } from 'koishi';
2
+ import type { MsgMessage } from '../../shared/bot/MsgMessage';
3
+ /** Render only explicitly marked transactions; line content never enables this channel. */
4
+ export declare function renderQQTransactionMarkdown(data: MsgMessage, showGreeting: boolean): string;
5
+ export declare function sendQQTransaction(session: Session, data: MsgMessage): Promise<void>;
@@ -44,6 +44,7 @@ export interface EquipmentCardData {
44
44
  rank: string;
45
45
  }>;
46
46
  notes?: string[];
47
+ authorName?: string;
47
48
  }
48
49
  export interface LeaderboardImageData {
49
50
  backgroundUrl?: string;
@@ -129,6 +130,8 @@ export interface IMessage {
129
130
  equipmentCard?: EquipmentCardData;
130
131
  leaderboard?: LeaderboardImageData;
131
132
  pageNav?: PageNavData;
133
+ /** 通用交易确认、结果与失败消息;客户端可使用交易 Markdown 展示。 */
134
+ transaction?: boolean;
132
135
  /** 消息项列表 **/
133
136
  line: IMessageItem[];
134
137
  }
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.248",
4
+ "version": "1.0.250",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -20,6 +20,7 @@ export function equipmentCardHtml(card: EquipmentCardData, artwork?: string, unk
20
20
  : ''
21
21
  const enhancement = Number(card.enhancement) > 0 ? `<span class="enhance"><small>强化</small>+${escape(card.enhancement)}</span>` : ''
22
22
  const price = card.referencePrice ? `<div class="price"><span>参考价格</span><strong>${escape(card.referencePrice)}</strong></div>` : ''
23
+ const author = `<span class="author"><small>打造者</small>${escape(card.authorName?.trim() || '未知')}</span>`
23
24
 
24
25
  return `<!doctype html><html><head><meta charset="utf-8"><style>
25
26
  *{box-sizing:border-box}
@@ -29,12 +30,12 @@ html,body{margin:0;width:760px;background:#f1eee5;font-family:'Microsoft YaHei',
29
30
  #capture:after{content:'';position:absolute;inset:17px;border:1px solid #8c8b6f22;pointer-events:none}
30
31
  .heading{display:flex;align-items:center;gap:24px}.heading-copy{flex:1;min-width:0}.overline{font-size:14px;letter-spacing:6px;color:#868a76;margin-bottom:14px}.name{font:700 43px/1.35 'KaiTi','STKaiti','Noto Serif CJK SC','SimSun',serif;margin:0;color:#303a31;overflow-wrap:anywhere}
31
32
  .seal{flex-shrink:0;width:66px;min-height:74px;transform:rotate(3deg);padding:8px 3px;color:#a04b40;border:2px solid #a04b40;outline:1px solid #a04b40;outline-offset:-6px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;border-radius:3px 1px 4px 2px}.seal strong{font:700 24px Georgia,'KaiTi',serif;letter-spacing:-1px}.seal span{font:16px 'KaiTi','SimSun',serif;letter-spacing:4px;padding-left:4px}
32
- .meta{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px 26px;margin:24px 0 0;padding:15px 0;border-top:1px solid #8a8a7144;border-bottom:1px solid #8a8a7128;font-size:19px;color:#535d50;line-height:1.5}.meta .slot{color:#a04b40;font:21px 'KaiTi','SimSun',serif;letter-spacing:2px}.meta small{font-size:16px;color:#92917f;margin-right:9px}
33
+ .meta{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px 26px;margin:24px 0 0;padding:15px 0;border-top:1px solid #8a8a7144;border-bottom:1px solid #8a8a7128;font-size:19px;color:#535d50;line-height:1.5}.meta .slot{color:#a04b40;font:21px 'KaiTi','SimSun',serif;letter-spacing:2px}.meta .author{min-width:0;overflow-wrap:anywhere}.meta small{font-size:16px;color:#92917f;margin-right:9px}
33
34
  .art{position:relative;width:610px;height:610px;max-width:100%;margin:18px auto 11px;display:grid;place-items:center;isolation:isolate;overflow:hidden}.art:before{content:'';position:absolute;inset:4%;border-radius:48% 52% 56% 44%;background:radial-gradient(ellipse,#aab4a443,transparent 69%);transform:rotate(-12deg)}.equipment-artwork{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center;filter:saturate(.72) contrast(.97);mix-blend-mode:multiply;mask-image:linear-gradient(to bottom,transparent 0,#000 8%,#000 84%,transparent 100%),linear-gradient(to right,transparent 0,#000 7%,#000 93%,transparent 100%);mask-composite:intersect}.art-fallback{position:relative;width:360px;height:360px;max-width:90%;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#7d8b7d;border:1px solid #7c887d3b;border-radius:50%;background:radial-gradient(circle,#b1b9a435,transparent 70%);font-family:'KaiTi','SimSun',serif}.art-fallback:before,.art-fallback:after{content:'';position:absolute;inset:21px;border:1px solid #7c887d22;border-radius:50%}.art-fallback:after{inset:48px;border-color:#9a9e8533}.art-fallback .glyph{font-size:126px;line-height:1.2;opacity:.46}.art-fallback .kind{font-size:20px;letter-spacing:4px;opacity:.55}.art:has(.equipment-artwork) .art-fallback{visibility:hidden}.placeholder-note{position:absolute;right:18px;bottom:16px;z-index:2;padding:5px 9px;background:#f4f1e8ce;border:1px solid #9d9b8755;color:#536055;font:18px 'KaiTi','SimSun',serif;letter-spacing:2px}
34
35
  .power{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:18px 0 22px;border-top:1px solid #8a8a7128;border-bottom:1px solid #8a8a7144;color:#31463e}.power span{font:24px 'KaiTi','SimSun',serif;letter-spacing:12px}.power strong{font:700 45px/1.1 Georgia,'Times New Roman',serif;letter-spacing:2px;font-variant-numeric:lining-nums}
35
36
  .section{margin-top:29px}.section h2{display:flex;align-items:center;gap:16px;margin:0 0 17px;color:#626e5e;font:21px 'KaiTi','SimSun',serif;letter-spacing:5px}.section h2:before{content:'';width:5px;height:17px;background:#a04b40;transform:rotate(8deg);opacity:.75}.section h2:after{content:'';height:1px;flex:1;background:linear-gradient(to right,#7f897c55,transparent)}
36
37
  .affixes{display:grid;grid-template-columns:1fr 1fr;column-gap:26px}.affix{display:flex;align-items:baseline;justify-content:space-between;gap:10px;min-width:0;padding:9px 0 12px;border-bottom:1px solid #8a8a7124;font-size:18px;color:#697064}.affix span{overflow-wrap:anywhere}.affix strong{flex-shrink:0;font:700 22px Georgia,'KaiTi',serif;color:#344b42}.empty-line{font-size:18px;color:#92917f;margin:4px 0 12px}
37
38
  .skill{display:flex;align-items:baseline;justify-content:space-between;gap:18px;padding:5px 0 10px;font-size:20px;color:#304b40}.skill-name{font-weight:700;overflow-wrap:anywhere}.skill-name i{display:inline-block;width:13px;height:13px;margin:0 17px 1px 4px;background:#a04b40;transform:rotate(45deg)}.rank{font:18px Georgia,'KaiTi',serif;color:#7f8d7b;white-space:nowrap}
38
39
  .description p{margin:0;font:22px/1.75 'KaiTi','STKaiti','Noto Serif CJK SC','SimSun',serif;white-space:pre-wrap;overflow-wrap:anywhere;color:#313f38}.price{display:flex;align-items:baseline;gap:16px;margin-top:24px;padding-top:17px;border-top:1px solid #8a8a7128;font-size:18px;color:#8b907d}.price strong{font-size:18px;color:#354c42}.notes{margin-top:17px;padding:10px 15px;border-left:3px solid #a04b4080;background:#e8e8db70;color:#3e5549}.notes p{margin:5px 0;font-size:17px;line-height:1.6;overflow-wrap:anywhere}.edition{display:flex;align-items:center;gap:12px;justify-content:center;margin-top:34px;color:#a6a38e;font:14px 'KaiTi','SimSun',serif;letter-spacing:5px}.edition:before,.edition:after{content:'';height:1px;width:32px;background:#9b9c833d}
39
- </style></head><body><div id="capture" class="equipment-sheet ink-equipment-sheet"><header><div class="heading"><div class="heading-copy"><div class="overline">装 备 图 录</div><h1 class="name">${escape(card.name)}</h1></div><div class="seal"><strong>${escape(card.quality)}</strong><span>品阶</span></div></div><div class="meta"><span class="slot">${escape(card.slot)}</span>${enhancement}</div></header><div class="art"><div class="art-fallback"><span class="glyph">?</span><span class="kind">未知外观</span></div>${artwork ? `<img class="skill-artwork equipment-artwork" alt="" src="${escape(artwork)}">` : ''}${unknownFallback || !artwork ? '<span class="placeholder-note">外观暂未显现</span>' : ''}</div><div class="power"><span>装 备 战 力</span><strong>${escape(power)}</strong></div><section class="section attributes"><h2>附加属性</h2><div class="affixes">${affixes}</div></section>${skills}${description}${price}${notes}<div class="edition">露 · 装备录</div></div></body></html>`
40
+ </style></head><body><div id="capture" class="equipment-sheet ink-equipment-sheet"><header><div class="heading"><div class="heading-copy"><div class="overline">装 备 图 录</div><h1 class="name">${escape(card.name)}</h1></div><div class="seal"><strong>${escape(card.quality)}</strong><span>品阶</span></div></div><div class="meta"><span class="slot">${escape(card.slot)}</span>${enhancement}${author}</div></header><div class="art"><div class="art-fallback"><span class="glyph">?</span><span class="kind">未知外观</span></div>${artwork ? `<img class="skill-artwork equipment-artwork" alt="" src="${escape(artwork)}">` : ''}${unknownFallback || !artwork ? '<span class="placeholder-note">外观暂未显现</span>' : ''}</div><div class="power"><span>装 备 战 力</span><strong>${escape(power)}</strong></div><section class="section attributes"><h2>附加属性</h2><div class="affixes">${affixes}</div></section>${skills}${description}${price}${notes}<div class="edition">露 · 装备录</div></div></body></html>`
40
41
  }
@@ -23,7 +23,7 @@ type QQResponse = {
23
23
  }
24
24
 
25
25
  /** Escape server-provided lines before embedding them in native QQ Markdown. */
26
- function escapeMarkdown(value: string): string {
26
+ export function escapeMarkdown(value: string): string {
27
27
  return value.replace(/[\\`*_{}\[\]()#+\-.!|>~<&]/g, '\\$&')
28
28
  }
29
29
 
@@ -44,7 +44,7 @@ function compactListLabel(value: string): string {
44
44
  : label
45
45
  }
46
46
 
47
- function inlineCommandUrl(command: string): string {
47
+ export function inlineCommandUrl(command: string): string {
48
48
  // encodeURIComponent leaves parentheses unescaped, which would close the
49
49
  // surrounding Markdown link if an item ID ever contained one.
50
50
  const encoded = encodeURIComponent(command).replace(/[!'()*]/g, character =>
@@ -129,7 +129,10 @@ export function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean): s
129
129
  hasPageSummary = true
130
130
  break
131
131
  }
132
- bodyLines.push(escapeMarkdown(item.content))
132
+ const command = entryCommands.get(lineIndex)
133
+ bodyLines.push(command
134
+ ? `[${escapeMarkdown(compactDisplayText(item.content))}](${inlineCommandUrl(command)})`
135
+ : escapeMarkdown(item.content))
133
136
  break
134
137
  }
135
138
  case MessageType.error:
@@ -164,20 +167,18 @@ export function canSendQQPage(session: Session | undefined, data: MsgMessage): s
164
167
 
165
168
  /** Send a native QQ Markdown page with inline command links. */
166
169
  export async function sendQQPage(session: Session, data: MsgMessage): Promise<void> {
167
- const internal = (session.bot as typeof session.bot & { internal: QQInternal }).internal
168
- const nav = data.pageNav!
169
170
  const markdown = renderQQPageMarkdown(data, session.event?.channel?.type === 0)
170
- const hasInlineNavigation = !!(nav.previousCommand || nav.nextCommand || nav.entries?.length)
171
- const hasInlineHelp = data.line.some(item => item.type === MessageType.line && item.content === '[指令]相关指令')
171
+ await sendQQMarkdown(session, markdown)
172
+ }
173
+
174
+ /** Reuse the adapter's native reply API for structured Markdown messages. */
175
+ export async function sendQQMarkdown(session: Session, markdown: string): Promise<void> {
176
+ const internal = (session.bot as typeof session.bot & { internal: QQInternal }).internal
172
177
  const request: QQRequest = {
173
178
  // With msg_type=2, QQ requires the ordinary content field to be empty.
174
179
  content: '',
175
180
  msg_type: 2,
176
- markdown: {
177
- content: !session.isDirect && (hasInlineNavigation || hasInlineHelp)
178
- ? `${markdown}\n\n点击后如未自动发送,请点发送。`
179
- : markdown,
180
- },
181
+ markdown: { content: markdown },
181
182
  }
182
183
  // Match the QQ adapter's passive-reply sequence.
183
184
  const age = Date.now() - session.timestamp
@@ -0,0 +1,62 @@
1
+ import type { Session } from 'koishi'
2
+ import { MessageType } from '../../shared/face'
3
+ import type { MsgMessage } from '../../shared/bot/MsgMessage'
4
+ import { escapeMarkdown, inlineCommandUrl, sendQQMarkdown } from './qqPageMessage'
5
+
6
+ /** Render only explicitly marked transactions; line content never enables this channel. */
7
+ export function renderQQTransactionMarkdown(data: MsgMessage, showGreeting: boolean): string {
8
+ const sections: string[] = []
9
+ const bodyLines: string[] = []
10
+ const actions: string[] = []
11
+ const flushBody = () => {
12
+ if (bodyLines.length) sections.push(bodyLines.splice(0).join(' \n'))
13
+ }
14
+ const flushActions = () => {
15
+ if (actions.length) sections.push(actions.splice(0).join(' · '))
16
+ }
17
+ const flush = () => { flushBody(); flushActions() }
18
+ if (showGreeting) sections.push(escapeMarkdown('Ciallo~(∠・ω< )'))
19
+ for (const item of data.line) {
20
+ if (item.type === MessageType.title) {
21
+ flush()
22
+ sections.push(`**${escapeMarkdown(item.content)}**`)
23
+ continue
24
+ }
25
+ if (item.type === MessageType.error) {
26
+ flush()
27
+ sections.push(`❌ 出错了!错误原因:${escapeMarkdown(item.content)},请反馈至联系开发者`)
28
+ continue
29
+ }
30
+ if (item.type !== MessageType.line) continue
31
+ // These are the common trade actions. Names, reasons and arbitrary bracketed
32
+ // material text remain escaped display text rather than executable commands.
33
+ const action = data.transaction && (item.content === '[是]确认交易'
34
+ ? { command: '是', label: '确认交易' }
35
+ : item.content === '[否]取消交易' ? { command: '否', label: '取消交易' } : undefined)
36
+ if (action) {
37
+ flushBody()
38
+ actions.push(`[${action.command}](${inlineCommandUrl(action.command)}) ${action.label}`)
39
+ continue
40
+ }
41
+ flushActions()
42
+ if (!item.content.trim()) {
43
+ flushBody()
44
+ continue
45
+ }
46
+ const section = /^┄━═(.+)═━┄$/.exec(item.content)
47
+ if (section || item.content === '【还缺】') {
48
+ flushBody()
49
+ sections.push(`**${escapeMarkdown(section?.[1] ?? '还缺')}**`)
50
+ } else {
51
+ // Preserve the server's quoted amounts, names and descriptions verbatim
52
+ // apart from Markdown escaping; rendering never recalculates payments.
53
+ bodyLines.push(escapeMarkdown(item.content))
54
+ }
55
+ }
56
+ flush()
57
+ return sections.join('\n\n') || ' '
58
+ }
59
+
60
+ export async function sendQQTransaction(session: Session, data: MsgMessage): Promise<void> {
61
+ await sendQQMarkdown(session, renderQQTransactionMarkdown(data, session.event?.channel?.type === 0))
62
+ }
@@ -3,15 +3,17 @@ import { MsgMessage } from '../../shared/bot/MsgMessage'
3
3
  import sessions, { sessionKey } from '../sessions'
4
4
  import handel_base from './handel_base'
5
5
  import { canSendQQPage, pageCommandLines, sendQQPage } from './qqPageMessage'
6
+ import { sendQQTransaction } from './qqTransactionMessage'
6
7
 
7
8
  export default class handelr_text extends handel_base {
8
9
  async start(data: MsgMessage) {
9
10
  const content = new AnalysisMsg().parse(data)
10
11
  const session = sessions.get(sessionKey(data.botId, data.userId, data.channelId))
11
- if (data.pageNav && canSendQQPage(session, data)) {
12
+ if ((data.transaction || data.pageNav) && canSendQQPage(session, data)) {
12
13
  const sendStartedAt = this.timing ? performance.now() : 0
13
14
  try {
14
- await sendQQPage(session, data)
15
+ if (data.transaction) await sendQQTransaction(session, data)
16
+ else await sendQQPage(session, data)
15
17
  if (this.timing) {
16
18
  this.timing.delivery = 'qq-markdown'
17
19
  this.timing.sendMs = performance.now() - sendStartedAt
@@ -20,7 +22,7 @@ export default class handelr_text extends handel_base {
20
22
  } catch (error) {
21
23
  this.ctx.logger.warn('QQ Markdown 发送失败,改用普通文字', error instanceof Error ? error.message : error)
22
24
  }
23
- const commands = pageCommandLines(data.pageNav)
25
+ const commands = !data.transaction && data.pageNav ? pageCommandLines(data.pageNav) : []
24
26
  await this.send(data, commands.length ? `${content.trimEnd()}\n${commands.join('\n')}` : content)
25
27
  return
26
28
  }
@@ -20,7 +20,7 @@ export interface ServiceType {
20
20
  }
21
21
 
22
22
  export const serviceProto: ServiceProto<ServiceType> = {
23
- "version": 10,
23
+ "version": 12,
24
24
  "services": [
25
25
  {
26
26
  "id": 0,
@@ -155,6 +155,14 @@ export const serviceProto: ServiceProto<ServiceType> = {
155
155
  },
156
156
  "optional": true
157
157
  },
158
+ {
159
+ "id": 8,
160
+ "name": "transaction",
161
+ "type": {
162
+ "type": "Boolean"
163
+ },
164
+ "optional": true
165
+ },
158
166
  {
159
167
  "id": 1,
160
168
  "name": "line",
@@ -612,6 +620,14 @@ export const serviceProto: ServiceProto<ServiceType> = {
612
620
  }
613
621
  },
614
622
  "optional": true
623
+ },
624
+ {
625
+ "id": 12,
626
+ "name": "authorName",
627
+ "type": {
628
+ "type": "String"
629
+ },
630
+ "optional": true
615
631
  }
616
632
  ]
617
633
  },
@@ -1042,4 +1058,4 @@ export const serviceProto: ServiceProto<ServiceType> = {
1042
1058
  ]
1043
1059
  }
1044
1060
  }
1045
- };
1061
+ };
@@ -23,6 +23,7 @@ export interface EquipmentCardData {
23
23
  affixes: Array<{ name: string; value: string }>;
24
24
  skills: Array<{ name: string; rank: string }>;
25
25
  notes?: string[];
26
+ authorName?: string;
26
27
  }
27
28
  export interface LeaderboardImageData {
28
29
  backgroundUrl?: string;
@@ -102,6 +103,8 @@ export interface IMessage {
102
103
  equipmentCard?: EquipmentCardData;
103
104
  leaderboard?: LeaderboardImageData;
104
105
  pageNav?: PageNavData;
106
+ /** 通用交易确认、结果与失败消息;客户端可使用交易 Markdown 展示。 */
107
+ transaction?: boolean;
105
108
  /** 消息项列表 **/
106
109
  line: IMessageItem[];
107
110
  }