mioku-adapter-qq-official 1.0.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mioku Lab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # mioku-adapter-qq-official
2
+
3
+ QQ 开放平台官方机器人适配器(mioku),支持 QQ 群聊、单聊场景,含 Markdown 消息、交互按钮、富媒体发送与按钮回调事件。
4
+
5
+ ## 特性
6
+
7
+ - **官方通道**:OpenAPI v2 + 官方 WebSocket 网关(op 10/2/1/11/6/7/9,断线自动 Resume 补发)
8
+ - **Token 自动管理**:`access_token` 7200s,提前 2 分钟自动刷新,401 自动重试
9
+ - **Markdown + 按钮**:核心 `segment.markdown()` / `segment.button()` → 官方 msg_type=2,按钮自动 5 个一行组装 keyboard(最多 5×5)
10
+ - **图片双模式**(配置切换):
11
+ - `markdown`:图片嵌入 markdown 单卡片发送,适合有公网的机器
12
+ - `media`:图片走 base64 上传独立富媒体消息,无公网要求
13
+ - markdown 转存/发送失败自动按 media 路径重试;markdown 权限类错误自动降级纯文本
14
+ - **被动回复自动接管**:事件后 5 分钟窗口内的发送自动携带 `msg_id`/`msg_seq`(最多 5 次),插件零感知;互动事件回复走 `event_id`
15
+ - **按钮回调合成消息事件**:`INTERACTION_CREATE`(type=11/12)自动回应并合成为 `sub_type: "callback"` 的普通消息事件(内容为 `button_data`),走现有指令管道;先应答再派发,规避 3 秒超时
16
+ - **群全量消息**:支持 `GROUP_MESSAGE_CREATE`(需在管理端开启「接收所有消息」),`author.member_role` 直接映射 `sender.role`
17
+ - **能力降级约定**:官方不支持的群管/查询类能力按约定返回假完成(动作类,带一次性 warn)或空值(查询类),插件不会崩
18
+
19
+ ## 安装
20
+
21
+ 仓库内 workspace 包,`bun install` 即可安装。
22
+
23
+ ```bash
24
+ bun install
25
+ ```
26
+
27
+ ## 配置
28
+
29
+ 适配器配置存在机器人根目录 `package.json` 的 `mioku.adapters` 字段下(JSON):
30
+
31
+ ```json
32
+ {
33
+ "mioku": {
34
+ "adapters": {
35
+ "qq-official": {
36
+ "instances": [
37
+ {
38
+ "appId": "你的AppID",
39
+ "appSecret": "你的AppSecret"
40
+ }
41
+ ]
42
+ }
43
+ }
44
+ }
45
+ }
46
+ ```
47
+
48
+ 也可以在机器人项目根目录运行 `npx mioku-adapter-qq-official` 交互式写入,或在 WebUI 配置页按 [config.md](./config.md) 声明的字段表单化编辑。
49
+
50
+ ## 平台前置条件
51
+
52
+ 1. [QQ 开放平台](https://q.qq.com) 创建机器人,拿到 `AppID` / `AppSecret`
53
+ 2. **IP 白名单**:新机器人默认强制启用,部署机公网 IP 必须加入白名单,否则连不上网关
54
+ 3. **群全量消息**:如需接收群里非 @ 消息,在管理端开启「接收所有消息」
55
+ 4. **消息 URL 白名单**:消息文本中的链接域名必须报备(需 ICP 备案);markdown 图片由平台转存,一般无需报白
56
+
57
+ ## 能力支持矩阵
58
+
59
+ | 能力 | 状态 | 说明 |
60
+ |---|---|---|
61
+ | message.send | ✅ | 文本/markdown/图片/语音/视频/文件/按钮 |
62
+ | message.recall | ✅ | 2 分钟内;仅撤回已见过的消息(发送响应或收到的事件) |
63
+ | message.forwardsend | ⚠️ | 合并转发拍平为多条普通消息 |
64
+ | message.get / getforward | ⬜ 返回空 | 官方无对应 API |
65
+ | group.getinfo / getlist | ⚠️ | 仅返回运行期间见过的群(openid),无群名 |
66
+ | group.getmembers / member.* 查询 | ⬜ 返回空 | 官方无对应 API |
67
+ | member.ban / kick / card / admin / title / poke 等管理动作 | ⚠️ 假完成 | 官方无对应 API,调用成功但记一次性 warn |
68
+ | conversation.history | ⬜ 返回空 | 官方无对应 API |
69
+ | friend.* | ⬜/⚠️ | 同上约定 |
70
+ | bot.status | ✅ | |
71
+
72
+ ✅ 完整支持 · ⚠️ 降级实现 · ⬜ 空值降级
73
+
74
+ ## 消息转换规则
75
+
76
+ 官方一次请求只能携带一种内容(`msg_type` 0/2/7 互斥),适配器按以下规则拆分:
77
+
78
+ - `text` / `at` / `markdown` 段合并进 markdown 文本池;`at` 转官方文本链(`<qqbot-at-user>` / `<qqbot-at-everyone>`)
79
+ - `image`:markdown 模式且有 URL → 嵌入 markdown;否则上传 `/files` 走 msg_type=7 独立发送(图文会拆成多条)
80
+ - `video` / `record` / `file` → 上传后 msg_type=7 独立发送(语音支持 silk/mp3/wav/ogg)
81
+ - `button` 段汇总挂到第一个 markdown 单元;没有 markdown 单元时自动补一条空白 markdown 承载键盘
82
+ - `face` / `json` / `forward` 等无对应物的段丢弃(记 debug);`node` 递归拍平
83
+ - 发送报 `50037`/`50056`/`304036` 等 markdown 权限/格式错误 → 自动降级纯文本重试
84
+
85
+ ## 已知限制
86
+
87
+ - 所有 id 均为 **openid**,且同一用户在不同 appId 下 openid 不同,请按 bot 维度隔离存储
88
+ - 群里默认只能收到 @机器人 的消息;全量消息需平台侧开关
89
+ - 主动消息有频控(认证 60/qpm,单关系 20/qpm,每群 1000 条/天),超限错误码会翻译进日志
90
+ - 按钮仅能挂在 markdown 消息上;回调按钮的互动应答使用 code 0
package/dist/cli.cjs ADDED
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ Object.defineProperty(exports, '__esModule', { value: true });
3
+ //#region rolldown:runtime
4
+ var __create = Object.create;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
13
+ key = keys[i];
14
+ if (!__hasOwnProp.call(to, key) && key !== except) {
15
+ __defProp(to, key, {
16
+ get: ((k) => from[k]).bind(null, key),
17
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
18
+ });
19
+ }
20
+ }
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
25
+ value: mod,
26
+ enumerable: true
27
+ }) : target, mod));
28
+
29
+ //#endregion
30
+ let node_fs = require("node:fs");
31
+ node_fs = __toESM(node_fs);
32
+ let node_path = require("node:path");
33
+ node_path = __toESM(node_path);
34
+ let node_url = require("node:url");
35
+ let consola = require("consola");
36
+ consola = __toESM(consola);
37
+
38
+ //#region src/cli.ts
39
+ const confirm = async (message, options) => await consola.default.prompt(message, {
40
+ type: "confirm",
41
+ cancel: "reject",
42
+ ...options
43
+ });
44
+ const input = async (message, options) => {
45
+ let result;
46
+ do {
47
+ result = await consola.default.prompt(message, {
48
+ type: "text",
49
+ cancel: "reject",
50
+ ...options
51
+ });
52
+ if (options?.required && !result) continue;
53
+ break;
54
+ } while (true);
55
+ return result;
56
+ };
57
+ const select = (message, options) => consola.default.prompt(message, {
58
+ type: "select",
59
+ cancel: "reject",
60
+ options
61
+ });
62
+ const run = async (ctx) => {
63
+ const log = ctx.logger ?? consola.default;
64
+ log.info("正在配置 qq-official 适配器(AppID / AppSecret 见开放平台开发设置)");
65
+ log.info("");
66
+ const instances = [];
67
+ let addMore = true;
68
+ while (addMore) {
69
+ const appId = await input("AppID", { required: true });
70
+ const appSecret = await input("AppSecret", { required: true });
71
+ const sandbox = await confirm("使用沙箱环境?", { initial: false });
72
+ const imageMode = await select("图片发送方式", [{
73
+ label: "markdown(单卡片,图片需公网可访问)",
74
+ value: "markdown"
75
+ }, {
76
+ label: "media(图文分离,base64 上传)",
77
+ value: "media"
78
+ }]);
79
+ instances.push({
80
+ appId,
81
+ appSecret,
82
+ sandbox,
83
+ imageMode
84
+ });
85
+ addMore = await confirm("是否继续添加机器人实例?", { initial: false });
86
+ if (addMore) log.info("");
87
+ }
88
+ return { instances };
89
+ };
90
+ const isRunningAsMain = () => {
91
+ if (!process.argv[1]) return false;
92
+ try {
93
+ return require("url").pathToFileURL(__filename).href === (0, node_url.pathToFileURL)(node_fs.default.realpathSync(process.argv[1])).href;
94
+ } catch {
95
+ return false;
96
+ }
97
+ };
98
+ if (isRunningAsMain()) (async () => {
99
+ const cwd = process.cwd();
100
+ const pkgPath = node_path.default.join(cwd, "package.json");
101
+ if (!node_fs.default.existsSync(pkgPath)) {
102
+ consola.default.error("未找到 package.json,请在机器人项目根目录运行此向导");
103
+ process.exit(1);
104
+ }
105
+ const config = await run({ cwd });
106
+ const pkg = JSON.parse(node_fs.default.readFileSync(pkgPath, "utf-8"));
107
+ pkg.mioku = pkg.mioku ?? {};
108
+ const adapters = pkg.mioku.adapters ?? {};
109
+ pkg.mioku.adapters = {
110
+ ...adapters,
111
+ "qq-official": config
112
+ };
113
+ node_fs.default.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf-8");
114
+ consola.default.success("已写入 qq-official 适配器配置");
115
+ })();
116
+ var cli_default = run;
117
+
118
+ //#endregion
119
+ exports.__toESM = __toESM;
120
+ exports.default = cli_default;
121
+ exports.run = run;
122
+ //# sourceMappingURL=cli.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.cjs","names":["result: string","instances: QQOfficialInstanceInput[]","fs","path"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport consola from \"consola\";\n\nimport type { ConfirmPromptOptions, TextPromptOptions } from \"consola\";\n\ntype ConfirmOpts = Omit<ConfirmPromptOptions, \"type\" | \"required\"> & {\n required?: boolean;\n};\ntype TextOpts = Omit<TextPromptOptions, \"type\" | \"required\"> & {\n required?: boolean;\n};\n\nconst confirm = async (\n message: string,\n options?: ConfirmOpts,\n): Promise<boolean> =>\n (await consola.prompt(message, {\n type: \"confirm\",\n cancel: \"reject\",\n ...options,\n })) as boolean;\n\nconst input = async (message: string, options?: TextOpts): Promise<string> => {\n let result: string;\n do {\n result = (await consola.prompt(message, {\n type: \"text\",\n cancel: \"reject\",\n ...options,\n })) as string;\n if (options?.required && !result) continue;\n break;\n } while (true);\n return result;\n};\n\nconst select = <T extends string>(\n message: string,\n options: Array<{ label: string; value: T }>,\n): Promise<T> =>\n consola.prompt(message, {\n type: \"select\",\n cancel: \"reject\",\n options,\n }) as Promise<T>;\n\nexport interface QQOfficialCliContext {\n readonly cwd: string;\n readonly logger?: typeof consola;\n}\n\nexport interface QQOfficialInstanceInput {\n appId: string;\n appSecret: string;\n sandbox: boolean;\n imageMode: \"markdown\" | \"media\";\n}\n\nexport interface QQOfficialCliConfig {\n instances: QQOfficialInstanceInput[];\n}\n\nexport const run = async (\n ctx: QQOfficialCliContext,\n): Promise<QQOfficialCliConfig> => {\n const log = ctx.logger ?? consola;\n log.info(\"正在配置 qq-official 适配器(AppID / AppSecret 见开放平台开发设置)\");\n log.info(\"\");\n\n const instances: QQOfficialInstanceInput[] = [];\n let addMore = true;\n while (addMore) {\n const appId = await input(\"AppID\", { required: true });\n const appSecret = await input(\"AppSecret\", { required: true });\n const sandbox = await confirm(\"使用沙箱环境?\", { initial: false });\n const imageMode = await select(\"图片发送方式\", [\n {\n label: \"markdown(单卡片,图片需公网可访问)\",\n value: \"markdown\" as const,\n },\n { label: \"media(图文分离,base64 上传)\", value: \"media\" as const },\n ]);\n\n instances.push({ appId, appSecret, sandbox, imageMode });\n addMore = await confirm(\"是否继续添加机器人实例?\", { initial: false });\n if (addMore) log.info(\"\");\n }\n\n return { instances };\n};\n\nconst isRunningAsMain = (): boolean => {\n if (!process.argv[1]) return false;\n try {\n return (\n import.meta.url === pathToFileURL(fs.realpathSync(process.argv[1])).href\n );\n } catch {\n return false;\n }\n};\n\nif (isRunningAsMain()) {\n void (async () => {\n const cwd = process.cwd();\n const pkgPath = path.join(cwd, \"package.json\");\n if (!fs.existsSync(pkgPath)) {\n consola.error(\"未找到 package.json,请在机器人项目根目录运行此向导\");\n process.exit(1);\n }\n const config = await run({ cwd });\n const pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf-8\")) as {\n mioku?: Record<string, unknown>;\n };\n pkg.mioku = pkg.mioku ?? {};\n const adapters =\n (pkg.mioku.adapters as Record<string, unknown> | undefined) ?? {};\n pkg.mioku.adapters = { ...adapters, \"qq-official\": config };\n fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\\n`, \"utf-8\");\n consola.success(\"已写入 qq-official 适配器配置\");\n })();\n}\n\nexport default run;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAeA,MAAM,UAAU,OACd,SACA,YAEC,MAAM,gBAAQ,OAAO,SAAS;CAC7B,MAAM;CACN,QAAQ;CACR,GAAG;CACJ,CAAC;AAEJ,MAAM,QAAQ,OAAO,SAAiB,YAAwC;CAC5E,IAAIA;AACJ,IAAG;AACD,WAAU,MAAM,gBAAQ,OAAO,SAAS;GACtC,MAAM;GACN,QAAQ;GACR,GAAG;GACJ,CAAC;AACF,MAAI,SAAS,YAAY,CAAC,OAAQ;AAClC;UACO;AACT,QAAO;;AAGT,MAAM,UACJ,SACA,YAEA,gBAAQ,OAAO,SAAS;CACtB,MAAM;CACN,QAAQ;CACR;CACD,CAAC;AAkBJ,MAAa,MAAM,OACjB,QACiC;CACjC,MAAM,MAAM,IAAI,UAAU;AAC1B,KAAI,KAAK,oDAAoD;AAC7D,KAAI,KAAK,GAAG;CAEZ,MAAMC,YAAuC,EAAE;CAC/C,IAAI,UAAU;AACd,QAAO,SAAS;EACd,MAAM,QAAQ,MAAM,MAAM,SAAS,EAAE,UAAU,MAAM,CAAC;EACtD,MAAM,YAAY,MAAM,MAAM,aAAa,EAAE,UAAU,MAAM,CAAC;EAC9D,MAAM,UAAU,MAAM,QAAQ,WAAW,EAAE,SAAS,OAAO,CAAC;EAC5D,MAAM,YAAY,MAAM,OAAO,UAAU,CACvC;GACE,OAAO;GACP,OAAO;GACR,EACD;GAAE,OAAO;GAAyB,OAAO;GAAkB,CAC5D,CAAC;AAEF,YAAU,KAAK;GAAE;GAAO;GAAW;GAAS;GAAW,CAAC;AACxD,YAAU,MAAM,QAAQ,gBAAgB,EAAE,SAAS,OAAO,CAAC;AAC3D,MAAI,QAAS,KAAI,KAAK,GAAG;;AAG3B,QAAO,EAAE,WAAW;;AAGtB,MAAM,wBAAiC;AACrC,KAAI,CAAC,QAAQ,KAAK,GAAI,QAAO;AAC7B,KAAI;AACF,uFACoCC,gBAAG,aAAa,QAAQ,KAAK,GAAG,CAAC,CAAC;SAEhE;AACN,SAAO;;;AAIX,IAAI,iBAAiB,CACnB,EAAM,YAAY;CAChB,MAAM,MAAM,QAAQ,KAAK;CACzB,MAAM,UAAUC,kBAAK,KAAK,KAAK,eAAe;AAC9C,KAAI,CAACD,gBAAG,WAAW,QAAQ,EAAE;AAC3B,kBAAQ,MAAM,mCAAmC;AACjD,UAAQ,KAAK,EAAE;;CAEjB,MAAM,SAAS,MAAM,IAAI,EAAE,KAAK,CAAC;CACjC,MAAM,MAAM,KAAK,MAAMA,gBAAG,aAAa,SAAS,QAAQ,CAAC;AAGzD,KAAI,QAAQ,IAAI,SAAS,EAAE;CAC3B,MAAM,WACH,IAAI,MAAM,YAAoD,EAAE;AACnE,KAAI,MAAM,WAAW;EAAE,GAAG;EAAU,eAAe;EAAQ;AAC3D,iBAAG,cAAc,SAAS,GAAG,KAAK,UAAU,KAAK,MAAM,EAAE,CAAC,KAAK,QAAQ;AACvE,iBAAQ,QAAQ,wBAAwB;IACtC;AAGN,kBAAe"}
package/dist/cli.d.cts ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import consola from "consola";
3
+
4
+ //#region src/cli.d.ts
5
+ interface QQOfficialCliContext {
6
+ readonly cwd: string;
7
+ readonly logger?: typeof consola;
8
+ }
9
+ interface QQOfficialInstanceInput {
10
+ appId: string;
11
+ appSecret: string;
12
+ sandbox: boolean;
13
+ imageMode: "markdown" | "media";
14
+ }
15
+ interface QQOfficialCliConfig {
16
+ instances: QQOfficialInstanceInput[];
17
+ }
18
+ declare const run: (ctx: QQOfficialCliContext) => Promise<QQOfficialCliConfig>;
19
+ //#endregion
20
+ export { QQOfficialCliConfig, QQOfficialCliContext, QQOfficialInstanceInput, run as default, run };
21
+ //# sourceMappingURL=cli.d.cts.map
package/dist/cli.d.mts ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import consola from "consola";
3
+
4
+ //#region src/cli.d.ts
5
+ interface QQOfficialCliContext {
6
+ readonly cwd: string;
7
+ readonly logger?: typeof consola;
8
+ }
9
+ interface QQOfficialInstanceInput {
10
+ appId: string;
11
+ appSecret: string;
12
+ sandbox: boolean;
13
+ imageMode: "markdown" | "media";
14
+ }
15
+ interface QQOfficialCliConfig {
16
+ instances: QQOfficialInstanceInput[];
17
+ }
18
+ declare const run: (ctx: QQOfficialCliContext) => Promise<QQOfficialCliConfig>;
19
+ //#endregion
20
+ export { QQOfficialCliConfig, QQOfficialCliContext, QQOfficialInstanceInput, run as default, run };
21
+ //# sourceMappingURL=cli.d.mts.map
package/dist/cli.mjs ADDED
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import consola from "consola";
6
+
7
+ //#region src/cli.ts
8
+ const confirm = async (message, options) => await consola.prompt(message, {
9
+ type: "confirm",
10
+ cancel: "reject",
11
+ ...options
12
+ });
13
+ const input = async (message, options) => {
14
+ let result;
15
+ do {
16
+ result = await consola.prompt(message, {
17
+ type: "text",
18
+ cancel: "reject",
19
+ ...options
20
+ });
21
+ if (options?.required && !result) continue;
22
+ break;
23
+ } while (true);
24
+ return result;
25
+ };
26
+ const select = (message, options) => consola.prompt(message, {
27
+ type: "select",
28
+ cancel: "reject",
29
+ options
30
+ });
31
+ const run = async (ctx) => {
32
+ const log = ctx.logger ?? consola;
33
+ log.info("正在配置 qq-official 适配器(AppID / AppSecret 见开放平台开发设置)");
34
+ log.info("");
35
+ const instances = [];
36
+ let addMore = true;
37
+ while (addMore) {
38
+ const appId = await input("AppID", { required: true });
39
+ const appSecret = await input("AppSecret", { required: true });
40
+ const sandbox = await confirm("使用沙箱环境?", { initial: false });
41
+ const imageMode = await select("图片发送方式", [{
42
+ label: "markdown(单卡片,图片需公网可访问)",
43
+ value: "markdown"
44
+ }, {
45
+ label: "media(图文分离,base64 上传)",
46
+ value: "media"
47
+ }]);
48
+ instances.push({
49
+ appId,
50
+ appSecret,
51
+ sandbox,
52
+ imageMode
53
+ });
54
+ addMore = await confirm("是否继续添加机器人实例?", { initial: false });
55
+ if (addMore) log.info("");
56
+ }
57
+ return { instances };
58
+ };
59
+ const isRunningAsMain = () => {
60
+ if (!process.argv[1]) return false;
61
+ try {
62
+ return import.meta.url === pathToFileURL(fs.realpathSync(process.argv[1])).href;
63
+ } catch {
64
+ return false;
65
+ }
66
+ };
67
+ if (isRunningAsMain()) (async () => {
68
+ const cwd = process.cwd();
69
+ const pkgPath = path.join(cwd, "package.json");
70
+ if (!fs.existsSync(pkgPath)) {
71
+ consola.error("未找到 package.json,请在机器人项目根目录运行此向导");
72
+ process.exit(1);
73
+ }
74
+ const config = await run({ cwd });
75
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
76
+ pkg.mioku = pkg.mioku ?? {};
77
+ const adapters = pkg.mioku.adapters ?? {};
78
+ pkg.mioku.adapters = {
79
+ ...adapters,
80
+ "qq-official": config
81
+ };
82
+ fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf-8");
83
+ consola.success("已写入 qq-official 适配器配置");
84
+ })();
85
+ var cli_default = run;
86
+
87
+ //#endregion
88
+ export { cli_default as default, run };
89
+ //# sourceMappingURL=cli.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.mjs","names":["result: string","instances: QQOfficialInstanceInput[]"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport consola from \"consola\";\n\nimport type { ConfirmPromptOptions, TextPromptOptions } from \"consola\";\n\ntype ConfirmOpts = Omit<ConfirmPromptOptions, \"type\" | \"required\"> & {\n required?: boolean;\n};\ntype TextOpts = Omit<TextPromptOptions, \"type\" | \"required\"> & {\n required?: boolean;\n};\n\nconst confirm = async (\n message: string,\n options?: ConfirmOpts,\n): Promise<boolean> =>\n (await consola.prompt(message, {\n type: \"confirm\",\n cancel: \"reject\",\n ...options,\n })) as boolean;\n\nconst input = async (message: string, options?: TextOpts): Promise<string> => {\n let result: string;\n do {\n result = (await consola.prompt(message, {\n type: \"text\",\n cancel: \"reject\",\n ...options,\n })) as string;\n if (options?.required && !result) continue;\n break;\n } while (true);\n return result;\n};\n\nconst select = <T extends string>(\n message: string,\n options: Array<{ label: string; value: T }>,\n): Promise<T> =>\n consola.prompt(message, {\n type: \"select\",\n cancel: \"reject\",\n options,\n }) as Promise<T>;\n\nexport interface QQOfficialCliContext {\n readonly cwd: string;\n readonly logger?: typeof consola;\n}\n\nexport interface QQOfficialInstanceInput {\n appId: string;\n appSecret: string;\n sandbox: boolean;\n imageMode: \"markdown\" | \"media\";\n}\n\nexport interface QQOfficialCliConfig {\n instances: QQOfficialInstanceInput[];\n}\n\nexport const run = async (\n ctx: QQOfficialCliContext,\n): Promise<QQOfficialCliConfig> => {\n const log = ctx.logger ?? consola;\n log.info(\"正在配置 qq-official 适配器(AppID / AppSecret 见开放平台开发设置)\");\n log.info(\"\");\n\n const instances: QQOfficialInstanceInput[] = [];\n let addMore = true;\n while (addMore) {\n const appId = await input(\"AppID\", { required: true });\n const appSecret = await input(\"AppSecret\", { required: true });\n const sandbox = await confirm(\"使用沙箱环境?\", { initial: false });\n const imageMode = await select(\"图片发送方式\", [\n {\n label: \"markdown(单卡片,图片需公网可访问)\",\n value: \"markdown\" as const,\n },\n { label: \"media(图文分离,base64 上传)\", value: \"media\" as const },\n ]);\n\n instances.push({ appId, appSecret, sandbox, imageMode });\n addMore = await confirm(\"是否继续添加机器人实例?\", { initial: false });\n if (addMore) log.info(\"\");\n }\n\n return { instances };\n};\n\nconst isRunningAsMain = (): boolean => {\n if (!process.argv[1]) return false;\n try {\n return (\n import.meta.url === pathToFileURL(fs.realpathSync(process.argv[1])).href\n );\n } catch {\n return false;\n }\n};\n\nif (isRunningAsMain()) {\n void (async () => {\n const cwd = process.cwd();\n const pkgPath = path.join(cwd, \"package.json\");\n if (!fs.existsSync(pkgPath)) {\n consola.error(\"未找到 package.json,请在机器人项目根目录运行此向导\");\n process.exit(1);\n }\n const config = await run({ cwd });\n const pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf-8\")) as {\n mioku?: Record<string, unknown>;\n };\n pkg.mioku = pkg.mioku ?? {};\n const adapters =\n (pkg.mioku.adapters as Record<string, unknown> | undefined) ?? {};\n pkg.mioku.adapters = { ...adapters, \"qq-official\": config };\n fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\\n`, \"utf-8\");\n consola.success(\"已写入 qq-official 适配器配置\");\n })();\n}\n\nexport default run;\n"],"mappings":";;;;;;;AAeA,MAAM,UAAU,OACd,SACA,YAEC,MAAM,QAAQ,OAAO,SAAS;CAC7B,MAAM;CACN,QAAQ;CACR,GAAG;CACJ,CAAC;AAEJ,MAAM,QAAQ,OAAO,SAAiB,YAAwC;CAC5E,IAAIA;AACJ,IAAG;AACD,WAAU,MAAM,QAAQ,OAAO,SAAS;GACtC,MAAM;GACN,QAAQ;GACR,GAAG;GACJ,CAAC;AACF,MAAI,SAAS,YAAY,CAAC,OAAQ;AAClC;UACO;AACT,QAAO;;AAGT,MAAM,UACJ,SACA,YAEA,QAAQ,OAAO,SAAS;CACtB,MAAM;CACN,QAAQ;CACR;CACD,CAAC;AAkBJ,MAAa,MAAM,OACjB,QACiC;CACjC,MAAM,MAAM,IAAI,UAAU;AAC1B,KAAI,KAAK,oDAAoD;AAC7D,KAAI,KAAK,GAAG;CAEZ,MAAMC,YAAuC,EAAE;CAC/C,IAAI,UAAU;AACd,QAAO,SAAS;EACd,MAAM,QAAQ,MAAM,MAAM,SAAS,EAAE,UAAU,MAAM,CAAC;EACtD,MAAM,YAAY,MAAM,MAAM,aAAa,EAAE,UAAU,MAAM,CAAC;EAC9D,MAAM,UAAU,MAAM,QAAQ,WAAW,EAAE,SAAS,OAAO,CAAC;EAC5D,MAAM,YAAY,MAAM,OAAO,UAAU,CACvC;GACE,OAAO;GACP,OAAO;GACR,EACD;GAAE,OAAO;GAAyB,OAAO;GAAkB,CAC5D,CAAC;AAEF,YAAU,KAAK;GAAE;GAAO;GAAW;GAAS;GAAW,CAAC;AACxD,YAAU,MAAM,QAAQ,gBAAgB,EAAE,SAAS,OAAO,CAAC;AAC3D,MAAI,QAAS,KAAI,KAAK,GAAG;;AAG3B,QAAO,EAAE,WAAW;;AAGtB,MAAM,wBAAiC;AACrC,KAAI,CAAC,QAAQ,KAAK,GAAI,QAAO;AAC7B,KAAI;AACF,SACE,OAAO,KAAK,QAAQ,cAAc,GAAG,aAAa,QAAQ,KAAK,GAAG,CAAC,CAAC;SAEhE;AACN,SAAO;;;AAIX,IAAI,iBAAiB,CACnB,EAAM,YAAY;CAChB,MAAM,MAAM,QAAQ,KAAK;CACzB,MAAM,UAAU,KAAK,KAAK,KAAK,eAAe;AAC9C,KAAI,CAAC,GAAG,WAAW,QAAQ,EAAE;AAC3B,UAAQ,MAAM,mCAAmC;AACjD,UAAQ,KAAK,EAAE;;CAEjB,MAAM,SAAS,MAAM,IAAI,EAAE,KAAK,CAAC;CACjC,MAAM,MAAM,KAAK,MAAM,GAAG,aAAa,SAAS,QAAQ,CAAC;AAGzD,KAAI,QAAQ,IAAI,SAAS,EAAE;CAC3B,MAAM,WACH,IAAI,MAAM,YAAoD,EAAE;AACnE,KAAI,MAAM,WAAW;EAAE,GAAG;EAAU,eAAe;EAAQ;AAC3D,IAAG,cAAc,SAAS,GAAG,KAAK,UAAU,KAAK,MAAM,EAAE,CAAC,KAAK,QAAQ;AACvE,SAAQ,QAAQ,wBAAwB;IACtC;AAGN,kBAAe"}