@alemonjs/qq-bot 2.1.40 → 2.1.42

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/README.md CHANGED
@@ -79,7 +79,7 @@ qq-bot:
79
79
  | `base_url_gateway` | string | — | 网关域名代理 |
80
80
  | `base_url_app_access_token` | string | — | 获取 access_token 的域名代理 |
81
81
  | `is_private` | boolean | `false` | 私域机器人,影响默认事件订阅 |
82
- | `sandbox` | boolean | `false` | 使用沙盒 API 环境 |
82
+ | `sandbox` | boolean | `false` | 显式使用旧版沙盒域名;默认正式环境使用 api.bot.qq.com |
83
83
  | `shard` | number[] | `[0, 1]` | WebSocket 分片 |
84
84
  | `intents` | string[] | 自动组装 | 自定义事件订阅,覆盖默认值,见 [intents.ts](./src/sdk/intents.ts);通常省略,不要填空数组 |
85
85
  | `autoInteractionAck` | boolean | `true` | 自动确认按钮、快捷菜单事件;关闭后由应用调用 `useInteraction().ack()` |
package/lib/format.js CHANGED
@@ -129,10 +129,10 @@ const mdFormatters = {
129
129
  return `[🔗${value?.text}](${value?.url}) `;
130
130
  },
131
131
  "MD.image": (value, options) => `\n![text #${options?.width || 208}px #${options?.height || 320}px](${value})\n`,
132
- "MD.mention": (value, options) => {
132
+ "MD.mention": (value, options, context) => {
133
133
  const { belong } = options || {};
134
134
  if (belong === "channel") return "";
135
- if (value === "everyone") return "<qqbot-at-everyone />";
135
+ if (value === "everyone") return context === "c2c" ? "" : "<qqbot-at-everyone />";
136
136
  if (belong === "user") return `<qqbot-at-user id="${value}" />`;
137
137
  return `<qqbot-at-user id="${value}" />`;
138
138
  },
@@ -21,10 +21,15 @@ const nonEmptyIds = (ids, maximum, name) => {
21
21
  if (!Array.isArray(ids) || ids.length < 1 || ids.length > maximum || ids.some((id) => typeof id !== "string" || !id)) throw new Error(`Expected 1 to ${maximum} non-empty ${name}`);
22
22
  return ids;
23
23
  };
24
- const textLength = (value) => Buffer.byteLength(value, "utf8");
24
+ const textLength = (value) => Array.from(value).reduce((length, character) => length + (character.codePointAt(0) <= 127 ? 1 : 2), 0);
25
+ const panelScope = (scope) => {
26
+ if (scope === "direct") return "dm";
27
+ if (scope === "group" || scope === "c2c" || scope === "channel") return scope;
28
+ throw new Error("QQ panel scope must be group, c2c, channel or direct");
29
+ };
25
30
  const menuItem = (item, child = false) => {
26
31
  const maximum = child ? 14 : 10;
27
- if (!item.name || textLength(item.name) > maximum) throw new Error(`Menu item name must be at most ${maximum} bytes`);
32
+ if (!item.name || textLength(item.name) > maximum) throw new Error(`Menu item name must be at most ${maximum} weighted characters`);
28
33
  if (child && item.type === "menu") throw new Error("Menu sub-items cannot contain another menu");
29
34
  if (item.type === "link" && !item.url.startsWith("https://")) throw new Error("Menu links must use https");
30
35
  if (item.type === "menu" && (item.items.length < 1 || item.items.length > 5)) throw new Error("A menu must contain 1 to 5 sub-items");
@@ -41,14 +46,18 @@ const menuItem = (item, child = false) => {
41
46
  };
42
47
  };
43
48
  const validatePanel = (panel) => {
44
- if (panel.items.length > 20 || panel.remark && textLength(panel.remark) > 255) throw new Error("Panel supports at most 20 items and a 255-byte remark");
49
+ if (panel.items.length > 20 || panel.remark && Array.from(panel.remark).length > 255) throw new Error("Panel supports at most 20 items and a 255-character remark");
45
50
  for (const item of panel.items) {
46
51
  if (!item.name || textLength(item.name) > 14 || item.description && textLength(item.description) > 30) throw new Error("Panel item name/description exceeds its limit");
47
52
  if (item.type === "link" && (!item.url || !item.url.startsWith("https://"))) throw new Error("Panel link items require an https URL");
48
53
  }
49
54
  };
50
55
  const validatePanelAudience = (scope, audience, userIds, guildIds) => {
51
- if (!["group", "c2c"].includes(scope)) throw new Error("QQ panel scope must be group or c2c");
56
+ if (audience !== void 0 && audience !== "all" && audience !== "specific") throw new Error("QQ panel audience must be all or specific");
57
+ if (scope === "channel" || scope === "dm") {
58
+ if (audience !== void 0 && audience !== "all" || userIds?.length || guildIds?.length) throw new Error("QQ channel/direct panels only support audience: all without audience IDs");
59
+ return;
60
+ }
52
61
  if (Boolean(userIds?.length) && Boolean(guildIds?.length)) throw new Error("A panel audience cannot mix users and groups");
53
62
  if (audience === "specific") nonEmptyIds(scope === "group" ? guildIds : userIds, 20, scope === "group" ? "group IDs" : "user IDs");
54
63
  else if (userIds?.length || guildIds?.length) throw new Error("Panel audience IDs require audience: specific");
@@ -125,10 +134,15 @@ const panelBody = (panel) => ({
125
134
  version: panel.version
126
135
  });
127
136
  const fromPanel = (r) => {
128
- if (!["group", "c2c"].includes(r.scope)) throw new Error(`QQ panel scope ${r.scope || "unknown"} is outside the non-channel framework surface`);
137
+ if (![
138
+ "group",
139
+ "c2c",
140
+ "channel",
141
+ "dm"
142
+ ].includes(r.scope)) throw new Error(`QQ panel scope ${r.scope || "unknown"} is not supported`);
129
143
  return {
130
144
  id: r.panel_id,
131
- scope: r.scope,
145
+ scope: r.scope === "dm" ? "direct" : r.scope,
132
146
  audience: r.target_type,
133
147
  userIds: r.user_openids,
134
148
  guildIds: r.group_openids,
@@ -293,19 +307,19 @@ const handleFrameworkAction = async (client, data, botId) => {
293
307
  return client.menuPut({ menu: { items: p.menu.items.map(menuItem) } });
294
308
  },
295
309
  "panel.list": async () => {
296
- if (!["group", "c2c"].includes(p.scope)) throw new Error("QQ panel scope must be group or c2c");
297
310
  const r = await client.panelsList({
298
- scope: p.scope,
311
+ scope: panelScope(p.scope),
299
312
  ...pagination(p.pagination)
300
313
  });
301
314
  return page(r.records.map(fromPanel), r);
302
315
  },
303
316
  "panel.create": async () => {
304
317
  validatePanel(p.panel);
305
- validatePanelAudience(p.scope, p.audience, p.userIds, p.guildIds);
318
+ const scope = panelScope(p.scope);
319
+ validatePanelAudience(scope, p.audience, p.userIds, p.guildIds);
306
320
  return { id: (await client.panelsCreate({
307
- scope: p.scope,
308
- target_type: p.audience,
321
+ scope,
322
+ target_type: p.audience ?? "all",
309
323
  user_openids: p.userIds,
310
324
  group_openids: p.guildIds,
311
325
  panel: panelBody(p.panel)
package/lib/index.d.ts CHANGED
@@ -6,7 +6,8 @@ import { QQBotAPI } from "./sdk/api.js";
6
6
  import { useClient, useMode, useValue } from "./hook.js";
7
7
  import { QQBotGroupEventMap } from "./sdk/message.group.js";
8
8
  import { QQBotPublicEventMap } from "./sdk/message.public.js";
9
+ import { QQMarkdown, QQMarkdownMathOptions, QQMarkdownTask } from "./markdown.js";
9
10
  //#region src/index.d.ts
10
11
  declare const _default: () => any;
11
12
  //#endregion
12
- export { QQBotAPI as API, type ApiRequestData, type ButtonType, type FileType, type GlobalMuteRule, type GroupAction, type JoinApprovalStrategy, type JoinRequest, type KeyboardType, type MarkdownType, type MemberMuteState, type MessageType, type Options, type QQBotArkData, type QQBotAttachment, type QQBotBlacklistUser, type QQBotGroupEventMap, type QQBotGroupMember, type QQBotInteractionResolved, type QQBotMenu, type QQBotMenuItem, type QQBotMessageContent, type QQBotMessageElement, type QQBotMessageResult, type QQBotMessageUser, type QQBotPage, type QQBotPanel, type QQBotPanelCreate, type QQBotPanelRecord, type QQBotPanelScope, type QQBotPublicEventMap, type QQTextSanitizerOptions, type SetMemberMuteState, type StreamMessageData, type StreamMessageResult, type UploadConfig, type UploadPart, type UploadPartFinishData, type UploadPrepareData, type UploadPrepareResult, type VerifyInfo, _default as default, platform, sanitizeQQText, useClient, useMode, useValue };
13
+ export { QQBotAPI as API, type ApiRequestData, type ButtonType, type FileType, type GlobalMuteRule, type GroupAction, type JoinApprovalStrategy, type JoinRequest, type KeyboardType, type MarkdownType, type MemberMuteState, type MessageType, type Options, type QQBotArkData, type QQBotAttachment, type QQBotBlacklistUser, type QQBotGroupEventMap, type QQBotGroupMember, type QQBotInteractionResolved, type QQBotMenu, type QQBotMenuItem, type QQBotMessageContent, type QQBotMessageElement, type QQBotMessageResult, type QQBotMessageUser, type QQBotPage, type QQBotPanel, type QQBotPanelCreate, type QQBotPanelRecord, type QQBotPanelScope, type QQBotPublicEventMap, QQMarkdown, type QQMarkdownMathOptions, type QQMarkdownTask, type QQTextSanitizerOptions, type SetMemberMuteState, type StreamMessageData, type StreamMessageResult, type UploadConfig, type UploadPart, type UploadPartFinishData, type UploadPrepareData, type UploadPrepareResult, type VerifyInfo, _default as default, platform, sanitizeQQText, useClient, useMode, useValue };
package/lib/index.js CHANGED
@@ -4,6 +4,7 @@ import { QQBotAPI } from "./sdk/api.js";
4
4
  import { start } from "./index.websoket.js";
5
5
  import { useClient, useMode, useValue } from "./hook.js";
6
6
  import { start as start$1 } from "./index.webhook.js";
7
+ import { QQMarkdown } from "./markdown.js";
7
8
  import { definePlatform } from "alemonjs";
8
9
 
9
10
  //#region src/index.ts
@@ -15,4 +16,4 @@ const main = () => {
15
16
  var src_default = definePlatform({ main });
16
17
 
17
18
  //#endregion
18
- export { QQBotAPI as API, src_default as default, platform, sanitizeQQText, useClient, useMode, useValue };
19
+ export { QQBotAPI as API, QQMarkdown, src_default as default, platform, sanitizeQQText, useClient, useMode, useValue };
@@ -0,0 +1,25 @@
1
+ //#region src/markdown.d.ts
2
+ /** QQ Markdown 专用扩展。仅生成字符串,不扩展 alemonjs 的通用节点或渲染协议。 */
3
+ type QQMarkdownMathOptions = {
4
+ block?: boolean;
5
+ };
6
+ type QQMarkdownTask = {
7
+ text: string;
8
+ checked?: boolean;
9
+ };
10
+ declare const math: (latex: string, options?: QQMarkdownMathOptions) => string;
11
+ /**
12
+ * 仅用于支持这些扩展的 QQ Markdown 消息;通过 addContent() 或原始 Markdown 发送。
13
+ * 不提供跨平台渲染或自动降级,调用方应根据发送平台选择内容。
14
+ */
15
+ declare const QQMarkdown: {
16
+ math: typeof math;
17
+ taskList(...items: QQMarkdownTask[]): string;
18
+ underline(text: string): string;
19
+ color(text: string, color: string): string;
20
+ ruby(text: string, annotation: string): string;
21
+ boxed(text: string): string;
22
+ cancel(text: string): string;
23
+ };
24
+ //#endregion
25
+ export { QQMarkdown, QQMarkdownMathOptions, QQMarkdownTask };
@@ -0,0 +1,47 @@
1
+ //#region src/markdown.ts
2
+ const latexText = (text) => {
3
+ const escapes = {
4
+ "\\": "\\textbackslash{}",
5
+ "{": "\\{",
6
+ "}": "\\}",
7
+ $: "\\$",
8
+ "&": "\\&",
9
+ "#": "\\#",
10
+ "%": "\\%",
11
+ _: "\\_",
12
+ "^": "\\textasciicircum{}",
13
+ "~": "\\textasciitilde{}"
14
+ };
15
+ return `\\text{${text.replace(/[\\{}$&#%_^~]/g, (character) => escapes[character]).replace(/\r\n?|\n/g, " ")}}`;
16
+ };
17
+ const math = (latex, options) => options?.block ? `\n\n$$\n${latex}\n$$\n\n` : `$${latex}$`;
18
+ /**
19
+ * 仅用于支持这些扩展的 QQ Markdown 消息;通过 addContent() 或原始 Markdown 发送。
20
+ * 不提供跨平台渲染或自动降级,调用方应根据发送平台选择内容。
21
+ */
22
+ const QQMarkdown = {
23
+ math,
24
+ taskList(...items) {
25
+ if (items.length === 0) return "";
26
+ return "\n\n" + items.map((item) => `- [${item.checked ? "x" : " "}] ${item.text.replace(/\r\n?|\n/g, " ")}`).join("\n") + "\n\n";
27
+ },
28
+ underline(text) {
29
+ return math(`\\underline{${latexText(text)}}`);
30
+ },
31
+ color(text, color) {
32
+ if (!/^(?:[a-zA-Z]+|#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?)$/.test(color)) throw new TypeError("QQ Markdown color must be a color name or #RGB / #RRGGBB");
33
+ return math(`\\color{${color}}{${latexText(text)}}`);
34
+ },
35
+ ruby(text, annotation) {
36
+ return math(`\\overset{${latexText(annotation)}}{${latexText(text)}}`);
37
+ },
38
+ boxed(text) {
39
+ return math(`\\boxed{${latexText(text)}}`);
40
+ },
41
+ cancel(text) {
42
+ return math(`\\cancel{${latexText(text)}}`);
43
+ }
44
+ };
45
+
46
+ //#endregion
47
+ export { QQMarkdown };
@@ -34,8 +34,8 @@ type QQBotPanel = {
34
34
  remark?: string;
35
35
  version?: number;
36
36
  };
37
- /** 本次仅开放非频道场景。 */
38
- type QQBotPanelScope = 'c2c' | 'group';
37
+ /** QQ 原生面板场景;框架 direct 对应 dm。 */
38
+ type QQBotPanelScope = 'c2c' | 'group' | 'channel' | 'dm';
39
39
  type QQBotPanelCreate = {
40
40
  scope: QQBotPanelScope;
41
41
  target_type?: 'all' | 'specific';
package/lib/sends.js CHANGED
@@ -182,7 +182,7 @@ const formatMention = (item, mode) => {
182
182
  if (item.options?.belong === "channel") return `<#${value}>`;
183
183
  return "";
184
184
  }
185
- if (isEmptyMention) return "";
185
+ if (isEmptyMention) return mode === "group" ? "<qqbot-at-everyone />" : "";
186
186
  if (item.options?.belong === "user") return `<qqbot-at-user id="${value}" />`;
187
187
  return "";
188
188
  };
@@ -352,7 +352,7 @@ const sendOpenApiMessage = async (content, val, baseParams, uploadMedia, sendMes
352
352
  const fileInfo = await resolveMediaUrl(media, uploadMedia);
353
353
  if (!fileInfo) return [createResult(ResultCode.Fail, "媒体上传失败", null)];
354
354
  const res = await sendMessage({
355
- content: stripMediaPlaceholders(formatQQContent(val, (item) => extractContent([item], "group"), true, config.hideUnsupported)),
355
+ content: stripMediaPlaceholders(formatQQContent(val, (item) => extractContent([item], context === "group" ? "group-media" : context), true, config.hideUnsupported)),
356
356
  media: { file_info: fileInfo },
357
357
  msg_type: 7,
358
358
  ...baseParams
@@ -368,7 +368,7 @@ const sendOpenApiMessage = async (content, val, baseParams, uploadMedia, sendMes
368
368
  return [];
369
369
  }
370
370
  if (mdToText) {
371
- const textContent = formatQQContent(val, (item) => extractContent([item], "group"), true, config.hideUnsupported);
371
+ const textContent = formatQQContent(val, (item) => extractContent([item], context), true, config.hideUnsupported);
372
372
  if (textContent) {
373
373
  const res = await sendMessage({
374
374
  content: textContent,
@@ -385,7 +385,7 @@ const sendOpenApiMessage = async (content, val, baseParams, uploadMedia, sendMes
385
385
  }
386
386
  const mdParams = buildMdAndButtonsParams(val, context);
387
387
  if (mdParams) {
388
- if (mdParams.markdown?.content) mdParams.markdown.content = formatQQContent(val, (item) => extractContent([item], "group"), false, void 0, context);
388
+ if (mdParams.markdown?.content) mdParams.markdown.content = formatQQContent(val, (item) => extractContent([item], context), false, void 0, context);
389
389
  if (mdParams.markdown && options?.forceVerifyImageResource !== void 0) mdParams.markdown.force_verify_image_resource = options.forceVerifyImageResource;
390
390
  const res = await sendMessage({
391
391
  msg_type: 2,
@@ -522,7 +522,7 @@ const GROUP_AT_MESSAGE_CREATE = async (client, event, val, options) => {
522
522
  */
523
523
  const C2C_MESSAGE_CREATE = async (client, event, val, options) => {
524
524
  const baseParams = buildBaseParams(event._tag, event.MessageId, "INTERACTION_CREATE_C2C");
525
- const content = extractContent(val, "group");
525
+ const content = extractContent(val, "c2c");
526
526
  try {
527
527
  return await sendOpenApiMessage(content, val, baseParams, (data) => client.postRichMediaByUser(event.UserId, data), (data) => client.usersOpenMessages(event.UserId, data), "client.usersOpenMessages", "c2c", options);
528
528
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alemonjs/qq-bot",
3
- "version": "2.1.40",
3
+ "version": "2.1.42",
4
4
  "description": "QQ官方平台",
5
5
  "author": "lemonade",
6
6
  "license": "MIT",
@@ -12,13 +12,15 @@
12
12
  "test:send": "yarn build && node tests/use-send.notice.mjs",
13
13
  "test:channel": "yarn --cwd ../alemonjs build && yarn build && node tests/channel.contract.mjs",
14
14
  "test:qr": "yarn build && node tests/qr-auth.mjs",
15
- "test:non-channel": "yarn build && node tests/markdown-buttons.contract.mjs && node tests/non-channel.contract.mjs && node tests/framework.contract.mjs && node tests/non-channel.transport.mjs && node tests/media-upload.contract.mjs && node tests/routing.contract.mjs && node tests/lifecycle.contract.mjs && node tests/http-diagnostics.contract.mjs && node tests/webhook-ingress.contract.mjs && node tests/text-sanitizer.contract.mjs && tsc -p tests/tsconfig.non-channel.json",
15
+ "test:non-channel": "yarn build && node tests/markdown-buttons.contract.mjs && node tests/markdown-extension.contract.mjs && node tests/non-channel.contract.mjs && node tests/framework.contract.mjs && node tests/changelog.contract.mjs && node tests/everyone.contract.mjs && node tests/non-channel.transport.mjs && node tests/media-upload.contract.mjs && node tests/routing.contract.mjs && node tests/lifecycle.contract.mjs && node tests/http-diagnostics.contract.mjs && node tests/webhook-ingress.contract.mjs && node tests/text-sanitizer.contract.mjs && tsc -p tests/tsconfig.non-channel.json",
16
16
  "docs:snapshot": "python3 scripts/snapshot-official.py",
17
17
  "test:http": "yarn build && node tests/http-diagnostics.contract.mjs && node tests/webhook-ingress.contract.mjs",
18
18
  "test:text": "yarn build && node tests/text-sanitizer.contract.mjs",
19
19
  "test:materials": "yarn build && node tests/materials.contract.mjs",
20
20
  "test:mention": "yarn build && node tests/everyone.contract.mjs",
21
- "test:buttons": "yarn build && node tests/markdown-buttons.contract.mjs"
21
+ "test:buttons": "yarn build && node tests/markdown-buttons.contract.mjs",
22
+ "test:markdown-extension": "yarn build && node tests/markdown-extension.contract.mjs && node ../../node_modules/typescript/bin/tsc --noEmit --strict --skipLibCheck --module ESNext --moduleResolution Bundler --target ES2022 tests/markdown-extension.types.ts",
23
+ "test:changelog": "yarn --cwd ../alemonjs build && yarn build && node tests/changelog.contract.mjs && node tests/everyone.contract.mjs"
22
24
  },
23
25
  "dependencies": {
24
26
  "@noble/curves": "^1.7.0",
@@ -45,7 +47,12 @@
45
47
  "require": "./lib/index.js"
46
48
  },
47
49
  "./package": "./package.json",
48
- "./desktop": "./lib/desktop.js"
50
+ "./desktop": "./lib/desktop.js",
51
+ "./markdown": {
52
+ "types": "./lib/markdown.d.ts",
53
+ "import": "./lib/markdown.js",
54
+ "require": "./lib/markdown.js"
55
+ }
49
56
  },
50
57
  "alemonjs": {
51
58
  "config": [