@tencent-connect/dsh-qqbot 0.5.0 → 0.5.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/CHANGELOG_EN.md +18 -0
  3. package/README.md +12 -1
  4. package/dist/commands/help.d.ts +1 -1
  5. package/dist/commands/help.js +1 -1
  6. package/dist/config.d.ts +2 -0
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +10 -8
  9. package/dist/config.js.map +1 -1
  10. package/dist/gateway/bootstrap.d.ts.map +1 -1
  11. package/dist/gateway/bootstrap.js +17 -0
  12. package/dist/gateway/bootstrap.js.map +1 -1
  13. package/dist/index.d.ts +2 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +41 -22
  16. package/dist/index.js.map +1 -1
  17. package/dist/setup/bind-routes.d.ts +78 -0
  18. package/dist/setup/bind-routes.d.ts.map +1 -0
  19. package/dist/setup/bind-routes.js +311 -0
  20. package/dist/setup/bind-routes.js.map +1 -0
  21. package/dist/setup/binding-service.d.ts +86 -0
  22. package/dist/setup/binding-service.d.ts.map +1 -0
  23. package/dist/setup/binding-service.js +175 -0
  24. package/dist/setup/binding-service.js.map +1 -0
  25. package/dist/setup/index.d.ts +50 -0
  26. package/dist/setup/index.d.ts.map +1 -0
  27. package/dist/setup/index.js +98 -0
  28. package/dist/setup/index.js.map +1 -0
  29. package/dist/setup/persist.d.ts +41 -0
  30. package/dist/setup/persist.d.ts.map +1 -0
  31. package/dist/setup/persist.js +201 -0
  32. package/dist/setup/persist.js.map +1 -0
  33. package/dist/setup/presenters.d.ts +40 -0
  34. package/dist/setup/presenters.d.ts.map +1 -0
  35. package/dist/setup/presenters.js +84 -0
  36. package/dist/setup/presenters.js.map +1 -0
  37. package/dist/setup/qr-svg.d.ts +8 -0
  38. package/dist/setup/qr-svg.d.ts.map +1 -0
  39. package/dist/setup/qr-svg.js +20 -0
  40. package/dist/setup/qr-svg.js.map +1 -0
  41. package/dist/shared/bind-protocol.d.ts +42 -0
  42. package/dist/shared/bind-protocol.d.ts.map +1 -0
  43. package/dist/shared/bind-protocol.js +19 -0
  44. package/dist/shared/bind-protocol.js.map +1 -0
  45. package/dist/shared/utils.d.ts +8 -3
  46. package/dist/shared/utils.d.ts.map +1 -1
  47. package/dist/shared/utils.js +45 -4
  48. package/dist/shared/utils.js.map +1 -1
  49. package/dist/transport/reply-limiter.d.ts +1 -1
  50. package/dist/transport/reply-limiter.js +1 -1
  51. package/lib/client.js +996 -0
  52. package/lib/client.js.map +1 -0
  53. package/package.json +26 -6
  54. package/pnpm-workspace.yaml +1 -0
  55. package/tsconfig.client.json +19 -0
  56. package/tsdown.client.config.ts +58 -0
  57. package/dist/setup.d.ts +0 -24
  58. package/dist/setup.d.ts.map +0 -1
  59. package/dist/setup.js +0 -170
  60. package/dist/setup.js.map +0 -1
@@ -0,0 +1,98 @@
1
+ import { BindingService } from "./binding-service.js";
2
+ import { registerBindRoutes } from "./bind-routes.js";
3
+ import { createBrowserPresenter, createTerminalPresenter, isDesktopHost } from "./presenters.js";
4
+ import { persistCredentialsToProfile } from "./persist.js";
5
+ import { getProfileDir } from "../shared/index.js";
6
+ export { persistCredentialsToProfile } from "./persist.js";
7
+ export { isDesktopHost } from "./presenters.js";
8
+ /** 进程内活动绑定服务(HMR 接管用) */
9
+ let activeService = null;
10
+ /**
11
+ * 执行扫码绑定编排,返回凭据就绪的 Promise;失败/取消 resolve null。
12
+ *
13
+ * 【同步契约】函数体必须保持无 await——绑定编排(服务接管、路由注册、
14
+ * 呈现策略、ctx 生命周期挂载)在调用后的同步阶段全部完成,随后返回
15
+ * `service.start()` 的挂起 Promise。调用方(插件 apply)因此可以不等待
16
+ * 扫码结果直接完成加载:boot 的 `loader.await()` 会等所有插件 fiber
17
+ * settle,若 apply 无限期等待用户扫码,Host ready 会被阻塞
18
+ * (Desktop 界面无法进入、Web URL 延迟输出)。
19
+ *
20
+ * 凭据持久化由调用方在 Promise 的 then 链中执行——成功写入
21
+ * cordis.patch.yml 会触发 HMR 热重载,新 fiber 以凭据启动网关。
22
+ */
23
+ export async function ensureCredentials(ctx, options) {
24
+ const logger = options.logger;
25
+ const source = options.source ?? 'dsh-qqbot';
26
+ // 单例接管:HMR 双 fiber 窗口下,新实例终止旧实例的轮询
27
+ const service = new BindingService({ source, logger });
28
+ if (activeService !== null && activeService !== service) {
29
+ activeService.dispose();
30
+ }
31
+ activeService = service;
32
+ // 生命周期:fiber dispose 时 abort 轮询
33
+ ctx.effect(() => () => {
34
+ service.dispose();
35
+ if (activeService === service)
36
+ activeService = null;
37
+ }, 'im-qqbot.binding');
38
+ // 呈现策略:
39
+ // - 终端通道(CLI / dsh web 运营者);Desktop 无可见终端,跳过
40
+ // - 浏览器降级通道(QQBOT_BIND_OPEN_BROWSER=1 显式启用)
41
+ if (!isDesktopHost()) {
42
+ service.attach(createTerminalPresenter());
43
+ }
44
+ if (process.env.QQBOT_BIND_OPEN_BROWSER === '1') {
45
+ service.attach(createBrowserPresenter(logger));
46
+ }
47
+ // 端内绑定页路由:webServer 可选注入。
48
+ // cordis 访问守卫要求服务经 inject 声明后方可访问;deps 未就绪时
49
+ // 回调不运行但插件照常加载(CLI profile),服务就绪/替换时回调
50
+ // 重跑(自动清理旧路由、注册新路由),与 webserver 的注入生命周期对齐。
51
+ ctx.inject(['webServer'], (scoped) => {
52
+ const webServer = scoped.webServer;
53
+ if (webServer === undefined || typeof webServer.register !== 'function')
54
+ return;
55
+ const disposeRoutes = registerBindRoutes(webServer, service, logger, {
56
+ // 未绑定流程中的手动提交:终止扫码轮询,走同一持久化路径
57
+ onManualCredentials: (credentials) => {
58
+ service.dispose();
59
+ return persistCredentials(credentials, logger);
60
+ },
61
+ currentConfig: () => options.config,
62
+ });
63
+ // 回调作用域(服务卸载/替换、插件卸载)连带注销路由
64
+ scoped.effect(() => {
65
+ return disposeRoutes;
66
+ }, 'im-qqbot.bind-routes');
67
+ });
68
+ return service.start();
69
+ }
70
+ /**
71
+ * 凭据已配置的 fiber 注册常驻路由(状态回显 + 手动编辑/更换)。
72
+ *
73
+ * 在 apply 的 bootstrapGateway 之前调用;写入经同一 persist 路径,
74
+ * 保存后触发 HMR 重载、新 fiber 以新凭据重启网关。
75
+ */
76
+ export function registerBoundCredentialRoutes(ctx, logger, appId, config) {
77
+ ctx.inject(['webServer'], (scoped) => {
78
+ const webServer = scoped.webServer;
79
+ if (webServer === undefined || typeof webServer.register !== 'function')
80
+ return;
81
+ const disposeRoutes = registerBindRoutes(webServer, null, logger, {
82
+ boundAppId: () => appId,
83
+ onManualCredentials: (credentials) => persistCredentials(credentials, logger),
84
+ currentConfig: () => config,
85
+ });
86
+ scoped.effect(() => {
87
+ return disposeRoutes;
88
+ }, 'im-qqbot.bind-routes');
89
+ });
90
+ }
91
+ /** 手动凭据持久化(失败时返回可展示的错误) */
92
+ function persistCredentials(credentials, logger) {
93
+ const persisted = persistCredentialsToProfile(credentials, getProfileDir() ?? undefined, logger);
94
+ return persisted
95
+ ? { persisted: true }
96
+ : { persisted: false, error: '无法定位 profile 目录或写入失败,请改用环境变量配置' };
97
+ }
98
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/setup/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /** 凭据结果 */
2
+ export interface SetupCredentials {
3
+ appId: string;
4
+ appSecret: string;
5
+ }
6
+ /**
7
+ * 将凭据写入 dsh profile 的 cordis.patch.yml
8
+ *
9
+ * 用 js-yaml 解析现有文件后更新/追加 im-qqbot 条目,再 dump 写回,
10
+ * 保证输出始终是合法 YAML。文件不存在或为空时安全重建;
11
+ * 解析失败或结构异常时拒绝写入、保留原文件(避免覆盖用户其他配置)。
12
+ */
13
+ export declare function persistCredentialsToProfile(credentials: SetupCredentials, profileDir?: string, logger?: {
14
+ info(msg: string, ...args: unknown[]): void;
15
+ warn(msg: string, ...args: unknown[]): void;
16
+ }): boolean;
17
+ /**
18
+ * 从 dsh profile 的 cordis.patch.yml 中移除已保存的凭据(解绑)。
19
+ *
20
+ * 保留 im-qqbot 条目的其余 config(requireMention 等用户配置),
21
+ * 仅删除 appId/appSecret 字段;条目或字段不存在时幂等成功。
22
+ * 移除后写入触发 HMR 重载,插件回到未绑定态(绑定流程重新可用)。
23
+ */
24
+ export declare function removeCredentialsFromProfile(profileDir?: string, logger?: {
25
+ info(msg: string, ...args: unknown[]): void;
26
+ warn(msg: string, ...args: unknown[]): void;
27
+ }): boolean;
28
+ /**
29
+ * 将配置更新合并进 dsh profile 的 cordis.patch.yml(im-qqbot 条目)。
30
+ *
31
+ * 最小写入原则:只把「本次提交的顶层键」合并进条目 config——
32
+ * 不固化 bundle patch 的默认值(保持其版本演进的覆盖能力),
33
+ * 不触碰已保存的凭据字段(appId/appSecret 由绑定/更换/解绑流程管理)。
34
+ * 嵌套对象(如 access)按顶层键整体替换,避免部分合并出混合态。
35
+ * 写入触发 HMR 重载,新 fiber 以新配置启动。
36
+ */
37
+ export declare function updateProfileConfig(configPatch: Record<string, unknown>, profileDir?: string, logger?: {
38
+ info(msg: string, ...args: unknown[]): void;
39
+ warn(msg: string, ...args: unknown[]): void;
40
+ }): boolean;
41
+ //# sourceMappingURL=persist.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persist.d.ts","sourceRoot":"","sources":["../../src/setup/persist.ts"],"names":[],"mappings":"AAWA,WAAW;AACX,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AASD;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,gBAAgB,EAC7B,UAAU,CAAC,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE;IAAE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAAC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;CAAE,GACpG,OAAO,CAoDT;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,UAAU,CAAC,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE;IAAE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAAC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;CAAE,GACpG,OAAO,CA8BT;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,UAAU,CAAC,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE;IAAE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAAC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;CAAE,GACpG,OAAO,CAuCT"}
@@ -0,0 +1,201 @@
1
+ /**
2
+ * QQ Bot 凭据持久化 — 写入 dsh profile 的 cordis.patch.yml
3
+ *
4
+ * 用 js-yaml 解析现有文件后更新/追加 im-qqbot 条目,再 dump 写回,
5
+ * 保证输出始终是合法 YAML。文件不存在或为空时安全重建;
6
+ * 解析失败或结构异常时拒绝写入、保留原文件(避免覆盖用户其他配置)。
7
+ */
8
+ import { resolve } from 'node:path';
9
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
10
+ import yaml from 'js-yaml';
11
+ /**
12
+ * 将凭据写入 dsh profile 的 cordis.patch.yml
13
+ *
14
+ * 用 js-yaml 解析现有文件后更新/追加 im-qqbot 条目,再 dump 写回,
15
+ * 保证输出始终是合法 YAML。文件不存在或为空时安全重建;
16
+ * 解析失败或结构异常时拒绝写入、保留原文件(避免覆盖用户其他配置)。
17
+ */
18
+ export function persistCredentialsToProfile(credentials, profileDir, logger) {
19
+ const log = logger ?? console;
20
+ const dir = profileDir;
21
+ if (!dir) {
22
+ // 开发模式:插件从源码加载、不在 node_modules 下,无法定位 profile 目录
23
+ printEnvInstructions(credentials);
24
+ return false;
25
+ }
26
+ const patchPath = resolve(dir, 'cordis.patch.yml');
27
+ try {
28
+ // 1. 解析现有条目(文件不存在/为空才重建;解析失败会抛错,保留原文件)
29
+ let entries = [];
30
+ if (existsSync(patchPath)) {
31
+ entries = parsePatchEntries(readFileSync(patchPath, 'utf8'));
32
+ }
33
+ // 2. 查找已有 im-qqbot 条目
34
+ const existing = entries.find((e) => e.id === 'im-qqbot');
35
+ if (existing) {
36
+ // 更新已有条目的 config
37
+ existing.config = {
38
+ ...(existing.config ?? {}),
39
+ appId: credentials.appId,
40
+ appSecret: credentials.appSecret,
41
+ };
42
+ }
43
+ else {
44
+ // 追加新条目
45
+ entries.push({
46
+ id: 'im-qqbot',
47
+ config: {
48
+ appId: credentials.appId,
49
+ appSecret: credentials.appSecret,
50
+ },
51
+ });
52
+ }
53
+ // 3. dump 写回(保证合法 YAML)
54
+ // 先确保 profile 目录存在(文件不存在时也能正常创建,而非误入 ENOENT)
55
+ const output = `# QQ 机器人凭据(扫码绑定自动生成)\n${yaml.dump(entries)}`;
56
+ mkdirSync(dir, { recursive: true });
57
+ writeFileSync(patchPath, output, 'utf8');
58
+ log.info(`✔ 凭据已写入: ${patchPath}`);
59
+ log.info(` 下次启动将自动使用保存的凭据`);
60
+ return true;
61
+ }
62
+ catch (err) {
63
+ log.warn(`写入配置失败: ${err instanceof Error ? err.message : String(err)}`);
64
+ printYamlInstructions(credentials, patchPath);
65
+ return false;
66
+ }
67
+ }
68
+ /**
69
+ * 从 dsh profile 的 cordis.patch.yml 中移除已保存的凭据(解绑)。
70
+ *
71
+ * 保留 im-qqbot 条目的其余 config(requireMention 等用户配置),
72
+ * 仅删除 appId/appSecret 字段;条目或字段不存在时幂等成功。
73
+ * 移除后写入触发 HMR 重载,插件回到未绑定态(绑定流程重新可用)。
74
+ */
75
+ export function removeCredentialsFromProfile(profileDir, logger) {
76
+ const log = logger ?? console;
77
+ if (!profileDir) {
78
+ return false;
79
+ }
80
+ const patchPath = resolve(profileDir, 'cordis.patch.yml');
81
+ try {
82
+ if (!existsSync(patchPath))
83
+ return true; // 无 patch 文件 = 无凭据,幂等成功
84
+ const entries = parsePatchEntries(readFileSync(patchPath, 'utf8'));
85
+ const entry = entries.find((e) => e.id === 'im-qqbot');
86
+ if (entry?.config !== undefined) {
87
+ delete entry.config.appId;
88
+ delete entry.config.appSecret;
89
+ }
90
+ // 无条件写回:幂等场景(凭据字段已不存在)也更新 mtime——
91
+ // 宿主 HMR 靠文件变化触发重载,不写则 bound fiber 永不卸载,
92
+ // 解绑后状态会一直停留在 bound
93
+ mkdirSync(profileDir, { recursive: true });
94
+ writeFileSync(patchPath, yaml.dump(entries), 'utf8');
95
+ log.info(`✔ 凭据已移除: ${patchPath}`);
96
+ log.info(` 插件将重新进入绑定流程`);
97
+ return true;
98
+ }
99
+ catch (err) {
100
+ log.warn(`移除凭据失败: ${err instanceof Error ? err.message : String(err)}`);
101
+ return false;
102
+ }
103
+ }
104
+ /**
105
+ * 将配置更新合并进 dsh profile 的 cordis.patch.yml(im-qqbot 条目)。
106
+ *
107
+ * 最小写入原则:只把「本次提交的顶层键」合并进条目 config——
108
+ * 不固化 bundle patch 的默认值(保持其版本演进的覆盖能力),
109
+ * 不触碰已保存的凭据字段(appId/appSecret 由绑定/更换/解绑流程管理)。
110
+ * 嵌套对象(如 access)按顶层键整体替换,避免部分合并出混合态。
111
+ * 写入触发 HMR 重载,新 fiber 以新配置启动。
112
+ */
113
+ export function updateProfileConfig(configPatch, profileDir, logger) {
114
+ const log = logger ?? console;
115
+ if (!profileDir) {
116
+ log.warn('未定位到 profile 目录,配置无法持久化');
117
+ return false;
118
+ }
119
+ const patchPath = resolve(profileDir, 'cordis.patch.yml');
120
+ try {
121
+ let entries = [];
122
+ if (existsSync(patchPath)) {
123
+ entries = parsePatchEntries(readFileSync(patchPath, 'utf8'));
124
+ }
125
+ const entry = entries.find((e) => e.id === 'im-qqbot');
126
+ if (entry !== undefined) {
127
+ const existing = (entry.config ?? {});
128
+ // 只覆盖提交键;appId/appSecret 由绑定流程管理,防御性保留
129
+ const next = { ...existing };
130
+ for (const [key, value] of Object.entries(configPatch)) {
131
+ if (key === 'appId' || key === 'appSecret')
132
+ continue;
133
+ next[key] = value;
134
+ }
135
+ entry.config = next;
136
+ }
137
+ else {
138
+ const { appId: _a, appSecret: _s, ...rest } = configPatch;
139
+ entries.push({ id: 'im-qqbot', config: rest });
140
+ }
141
+ mkdirSync(profileDir, { recursive: true });
142
+ writeFileSync(patchPath, yaml.dump(entries), 'utf8');
143
+ log.info(`✔ 配置已保存: ${patchPath}`);
144
+ log.info(` 插件将自动重载`);
145
+ return true;
146
+ }
147
+ catch (err) {
148
+ log.warn(`保存配置失败: ${err instanceof Error ? err.message : String(err)}`);
149
+ return false;
150
+ }
151
+ }
152
+ /**
153
+ * 解析 cordis.patch.yml 为条目数组
154
+ *
155
+ * - 空内容 / 仅注释(yaml.load 返回 null/undefined)→ 空数组(允许安全重建)
156
+ * - 空数组 `[]` / 条目列表 → 过滤出含 id 的对象
157
+ * - 非空但过滤后无任何可识别条目(如 bundle patch 的 `- insert: [...]`
158
+ * 结构)→ 抛错(由调用方保留原文件——此类文件不是 profile 用户 patch,
159
+ * 整体重写会破坏其原有内容)
160
+ * - 非数组结构 / YAML 语法错误 → 抛错(由调用方保留原文件,避免覆盖用户其他配置)
161
+ */
162
+ function parsePatchEntries(content) {
163
+ if (!content.trim())
164
+ return [];
165
+ const parsed = yaml.load(content);
166
+ // 仅注释/空白 → null/undefined,等价于空文件,允许重建
167
+ if (parsed === null || parsed === undefined)
168
+ return [];
169
+ if (!Array.isArray(parsed)) {
170
+ throw new Error('cordis.patch.yml 顶层必须是 YAML 数组');
171
+ }
172
+ const entries = parsed.filter((e) => typeof e === 'object' && e !== null && typeof e.id === 'string');
173
+ if (parsed.length > 0 && entries.length === 0) {
174
+ throw new Error('cordis.patch.yml 含条目但无带 id 的 patch 行(可能是 bundle patch,拒绝写入以保留原文件)');
175
+ }
176
+ return entries;
177
+ }
178
+ /** 开发模式引导:未定位到 profile 目录,引导用环境变量配置(console.log 确保可见) */
179
+ function printEnvInstructions(credentials) {
180
+ console.log('未检测到 profile 目录(开发模式),请通过环境变量配置凭据:');
181
+ if (process.platform === 'win32') {
182
+ // Windows CMD 语法(PowerShell 请用 $env:QQBOT_APPID="xxx")
183
+ console.log(` set QQBOT_APPID=${credentials.appId}`);
184
+ console.log(` set QQBOT_SECRET=${credentials.appSecret}`);
185
+ }
186
+ else {
187
+ console.log(` export QQBOT_APPID="${credentials.appId}"`);
188
+ console.log(` export QQBOT_SECRET="${credentials.appSecret}"`);
189
+ }
190
+ }
191
+ /** 正式安装引导:自动写入失败,引导手动在 cordis.patch.yml 配置(console.log 确保可见) */
192
+ function printYamlInstructions(credentials, patchPath) {
193
+ console.log('无法自动保存凭据,请手动打开以下文件添加配置:');
194
+ console.log(` ${patchPath}`);
195
+ console.log('');
196
+ console.log(' - id: im-qqbot');
197
+ console.log(' config:');
198
+ console.log(` appId: "${credentials.appId}"`);
199
+ console.log(` appSecret: "${credentials.appSecret}"`);
200
+ }
201
+ //# sourceMappingURL=persist.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persist.js","sourceRoot":"","sources":["../../src/setup/persist.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import type { Logger } from '../types.ts';
2
+ /** 呈现策略接口:二维码事件的通知接收方 */
3
+ export interface QrPresenter {
4
+ /** 实现标识(诊断用) */
5
+ readonly id: string;
6
+ /** 是否让 connector 在控制台打印 ASCII 二维码(仅终端可见的宿主开启) */
7
+ readonly printQrToConsole: boolean;
8
+ /** 二维码 URL 就绪(首次与过期刷新均会触发) */
9
+ onQrDisplayed(url: string): void;
10
+ /** 二维码过期,即将刷新 */
11
+ onQrExpired?(): void;
12
+ }
13
+ /**
14
+ * 是否运行在 DSH Desktop 宿主中。
15
+ *
16
+ * Desktop 的 Host 以 ELECTRON_RUN_AS_NODE=1 的 Electron 子进程运行
17
+ * (Desktop 宿主的启动约定),stdout 对用户不可见,终端呈现无意义。
18
+ */
19
+ export declare function isDesktopHost(): boolean;
20
+ /** 终端可点击的 OSC 8 超链接(不支持 OSC 8 的终端会忽略控制序列,退化为纯 URL 文本) */
21
+ export declare function clickableLink(url: string): string;
22
+ /**
23
+ * 终端呈现(CLI / `dsh web` 运营者通道)
24
+ *
25
+ * ASCII 二维码由 connector 打印(printQrToConsole=true),
26
+ * 本呈现只补打可点击链接与过期提示。
27
+ *
28
+ * 绑定引导是用户直接交互内容,必须用普通 console.log 输出:
29
+ * 不带时间戳/级别前缀(避免打断二维码块),不受日志级别配置影响。
30
+ */
31
+ export declare function createTerminalPresenter(): QrPresenter;
32
+ /**
33
+ * 系统浏览器呈现(降级通道)
34
+ *
35
+ * Desktop 端内绑定页不可用时的自救入口:每次绑定会话只自动拉起一次
36
+ * 浏览器(二维码过期刷新不重复弹窗)。默认关闭,通过环境变量
37
+ * QQBOT_BIND_OPEN_BROWSER=1 显式启用。
38
+ */
39
+ export declare function createBrowserPresenter(logger: Logger): QrPresenter;
40
+ //# sourceMappingURL=presenters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presenters.d.ts","sourceRoot":"","sources":["../../src/setup/presenters.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,yBAAyB;AACzB,MAAM,WAAW,WAAW;IAC1B,gBAAgB;IAChB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC,8BAA8B;IAC9B,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,iBAAiB;IACjB,WAAW,CAAC,IAAI,IAAI,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED,yDAAyD;AACzD,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,IAAI,WAAW,CAYrD;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,CAelE"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * 扫码呈现策略(QrPresenter)
3
+ *
4
+ * 绑定协议(qqbot-connector 轮询)与呈现解耦:BindingService 只依赖
5
+ * 本接口,具体实现由宿主形态决定——终端宿主打印 ASCII 二维码与可点击
6
+ * 链接;Desktop 宿主无可见终端,改由端内绑定页(bind-routes)承载。
7
+ */
8
+ import { spawn } from 'node:child_process';
9
+ /**
10
+ * 是否运行在 DSH Desktop 宿主中。
11
+ *
12
+ * Desktop 的 Host 以 ELECTRON_RUN_AS_NODE=1 的 Electron 子进程运行
13
+ * (Desktop 宿主的启动约定),stdout 对用户不可见,终端呈现无意义。
14
+ */
15
+ export function isDesktopHost() {
16
+ return process.env.ELECTRON_RUN_AS_NODE === '1' && typeof process.versions?.electron === 'string';
17
+ }
18
+ /** 终端可点击的 OSC 8 超链接(不支持 OSC 8 的终端会忽略控制序列,退化为纯 URL 文本) */
19
+ export function clickableLink(url) {
20
+ return `\u001b]8;;${url}\u001b\\${url}\u001b]8;;\u001b\\`;
21
+ }
22
+ /**
23
+ * 终端呈现(CLI / `dsh web` 运营者通道)
24
+ *
25
+ * ASCII 二维码由 connector 打印(printQrToConsole=true),
26
+ * 本呈现只补打可点击链接与过期提示。
27
+ *
28
+ * 绑定引导是用户直接交互内容,必须用普通 console.log 输出:
29
+ * 不带时间戳/级别前缀(避免打断二维码块),不受日志级别配置影响。
30
+ */
31
+ export function createTerminalPresenter() {
32
+ return {
33
+ id: 'terminal',
34
+ printQrToConsole: true,
35
+ onQrDisplayed(url) {
36
+ console.log('二维码显示异常?点击下方链接,用浏览器打开即可完成扫码:');
37
+ console.log(` ${clickableLink(url)}\n`);
38
+ },
39
+ onQrExpired() {
40
+ console.log('二维码已过期,正在刷新…\n');
41
+ },
42
+ };
43
+ }
44
+ /**
45
+ * 系统浏览器呈现(降级通道)
46
+ *
47
+ * Desktop 端内绑定页不可用时的自救入口:每次绑定会话只自动拉起一次
48
+ * 浏览器(二维码过期刷新不重复弹窗)。默认关闭,通过环境变量
49
+ * QQBOT_BIND_OPEN_BROWSER=1 显式启用。
50
+ */
51
+ export function createBrowserPresenter(logger) {
52
+ let opened = false;
53
+ return {
54
+ id: 'browser',
55
+ printQrToConsole: false,
56
+ onQrDisplayed(url) {
57
+ console.log(`扫码绑定链接: ${url}`);
58
+ if (opened)
59
+ return;
60
+ opened = true;
61
+ openInBrowser(url, logger);
62
+ },
63
+ onQrExpired() {
64
+ console.log('二维码已过期,正在刷新…(如浏览器中页面失效,请点击「刷新二维码」)');
65
+ },
66
+ };
67
+ }
68
+ /** 跨平台拉起系统浏览器打开 URL(失败静默降级为日志输出) */
69
+ function openInBrowser(url, logger) {
70
+ const platform = process.platform;
71
+ const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open';
72
+ const args = platform === 'win32' ? ['/c', 'start', '', url] : [url];
73
+ try {
74
+ const child = spawn(command, args, { detached: true, stdio: 'ignore', windowsHide: true });
75
+ child.on('error', (err) => {
76
+ logger.warn(`拉起浏览器失败: ${err.message}`);
77
+ });
78
+ child.unref();
79
+ }
80
+ catch (err) {
81
+ logger.warn(`拉起浏览器失败: ${err instanceof Error ? err.message : String(err)}`);
82
+ }
83
+ }
84
+ //# sourceMappingURL=presenters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presenters.js","sourceRoot":"","sources":["../../src/setup/presenters.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAe3C;;;;;GAKG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,OAAO,CAAC,GAAG,CAAC,oBAAoB,KAAK,GAAG,IAAI,OAAO,OAAO,CAAC,QAAQ,EAAE,QAAQ,KAAK,QAAQ,CAAC;AACpG,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,OAAO,aAAa,GAAG,WAAW,GAAG,oBAAoB,CAAC;AAC5D,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,uBAAuB;IACrC,OAAO;QACL,EAAE,EAAE,UAAU;QACd,gBAAgB,EAAE,IAAI;QACtB,aAAa,CAAC,GAAW;YACvB,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;YAC5C,OAAO,CAAC,GAAG,CAAC,KAAK,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC3C,CAAC;QACD,WAAW;YACT,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;QAChC,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAc;IACnD,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,OAAO;QACL,EAAE,EAAE,SAAS;QACb,gBAAgB,EAAE,KAAK;QACvB,aAAa,CAAC,GAAW;YACvB,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,EAAE,CAAC,CAAC;YAC9B,IAAI,MAAM;gBAAE,OAAO;YACnB,MAAM,GAAG,IAAI,CAAC;YACd,aAAa,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC7B,CAAC;QACD,WAAW;YACT,OAAO,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;QACpD,CAAC;KACF,CAAC;AACJ,CAAC;AAED,oCAAoC;AACpC,SAAS,aAAa,CAAC,GAAW,EAAE,MAAc;IAChD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;IAClC,MAAM,OAAO,GAAG,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC;IAC3F,MAAM,IAAI,GAAG,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACrE,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3F,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAU,EAAE,EAAE;YAC/B,MAAM,CAAC,IAAI,CAAC,YAAY,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,KAAK,EAAE,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,IAAI,CAAC,YAAY,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC9E,CAAC;AACH,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 将绑定 URL 渲染为 SVG data URL,供端内绑定页 <img> 直接使用。
3
+ *
4
+ * @param url - 二维码内容(q.qq.com 的 connect.html 链接)
5
+ * @returns `data:image/svg+xml;base64,...` 形式的 data URL
6
+ */
7
+ export declare function renderQrSvgDataUrl(url: string): Promise<string>;
8
+ //# sourceMappingURL=qr-svg.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qr-svg.d.ts","sourceRoot":"","sources":["../../src/setup/qr-svg.ts"],"names":[],"mappings":"AAWA;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGrE"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * 扫码 URL → SVG data URL(宿主侧渲染)
3
+ *
4
+ * 二维码图像在宿主生成而非浏览器:客户端 bundle 保持零第三方依赖
5
+ * (externals 仅平台种子表),且二维码由可信源编码,页面无伪造面。
6
+ */
7
+ import QRCode from 'qrcode';
8
+ /** 二维码渲染参数(输出 SVG,含 1 单位留白,240px 宽度适合侧栏展示) */
9
+ const QR_OPTIONS = { type: 'svg', margin: 1, width: 240 };
10
+ /**
11
+ * 将绑定 URL 渲染为 SVG data URL,供端内绑定页 <img> 直接使用。
12
+ *
13
+ * @param url - 二维码内容(q.qq.com 的 connect.html 链接)
14
+ * @returns `data:image/svg+xml;base64,...` 形式的 data URL
15
+ */
16
+ export async function renderQrSvgDataUrl(url) {
17
+ const svg = await QRCode.toString(url, QR_OPTIONS);
18
+ return `data:image/svg+xml;base64,${Buffer.from(svg, 'utf8').toString('base64')}`;
19
+ }
20
+ //# sourceMappingURL=qr-svg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qr-svg.js","sourceRoot":"","sources":["../../src/setup/qr-svg.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,8CAA8C;AAC9C,MAAM,UAAU,GAAG,EAAE,IAAI,EAAE,KAAc,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;AAEnE;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,GAAW;IAClD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;IACnD,OAAO,6BAA6B,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;AACpF,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * 端内绑定协议 —— 宿主路由与客户端面板的共享契约
3
+ *
4
+ * 类型与路径常量的唯一事实源:宿主(setup/bind-routes.ts)与浏览器
5
+ * (client/bind-page.ts)各自 import,杜绝两端协议漂移。
6
+ * 本模块必须保持纯类型 + 纯常量(零 node 依赖、零副作用):
7
+ * 客户端 tsdown 构建会将其内联进 bundle。
8
+ */
9
+ /** 状态查询(绑定页轮询) */
10
+ export declare const BIND_STATE_PATH = "/qqbot/im-qqbot/bind";
11
+ /** 刷新二维码(绑定页按钮) */
12
+ export declare const BIND_REFRESH_PATH = "/qqbot/im-qqbot/bind/refresh";
13
+ /** 手动填写/编辑凭据(绑定页表单) */
14
+ export declare const BIND_MANUAL_PATH = "/qqbot/im-qqbot/bind/manual";
15
+ /** 解绑:移除已保存凭据,回到绑定流程 */
16
+ export declare const BIND_UNBIND_PATH = "/qqbot/im-qqbot/bind/unbind";
17
+ /** 配置读写(设置面板表单) */
18
+ export declare const BIND_CONFIG_PATH = "/qqbot/im-qqbot/config";
19
+ /** 线上协议(HTTP 路由序列化形态) */
20
+ export type BindStateWire = {
21
+ phase: 'starting';
22
+ } | {
23
+ phase: 'waiting';
24
+ url: string;
25
+ qrSvg?: string;
26
+ issuedAt: number;
27
+ } | {
28
+ phase: 'saving';
29
+ } | {
30
+ phase: 'failed';
31
+ reason: string;
32
+ } | {
33
+ phase: 'bound';
34
+ appId: string;
35
+ };
36
+ /** manual / unbind 操作的统一响应 */
37
+ export interface BindActionResponse {
38
+ ok: boolean;
39
+ message?: string;
40
+ error?: string;
41
+ }
42
+ //# sourceMappingURL=bind-protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bind-protocol.d.ts","sourceRoot":"","sources":["../../src/shared/bind-protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,kBAAkB;AAClB,eAAO,MAAM,eAAe,yBAAyB,CAAC;AAEtD,mBAAmB;AACnB,eAAO,MAAM,iBAAiB,iCAAiC,CAAC;AAEhE,uBAAuB;AACvB,eAAO,MAAM,gBAAgB,gCAAgC,CAAC;AAE9D,wBAAwB;AACxB,eAAO,MAAM,gBAAgB,gCAAgC,CAAC;AAE9D,mBAAmB;AACnB,eAAO,MAAM,gBAAgB,2BAA2B,CAAC;AAEzD,yBAAyB;AACzB,MAAM,MAAM,aAAa,GACrB;IAAE,KAAK,EAAE,UAAU,CAAA;CAAE,GACrB;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACnE;IAAE,KAAK,EAAE,QAAQ,CAAA;CAAE,GACnB;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC,8BAA8B;AAC9B,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 端内绑定协议 —— 宿主路由与客户端面板的共享契约
3
+ *
4
+ * 类型与路径常量的唯一事实源:宿主(setup/bind-routes.ts)与浏览器
5
+ * (client/bind-page.ts)各自 import,杜绝两端协议漂移。
6
+ * 本模块必须保持纯类型 + 纯常量(零 node 依赖、零副作用):
7
+ * 客户端 tsdown 构建会将其内联进 bundle。
8
+ */
9
+ /** 状态查询(绑定页轮询) */
10
+ export const BIND_STATE_PATH = '/qqbot/im-qqbot/bind';
11
+ /** 刷新二维码(绑定页按钮) */
12
+ export const BIND_REFRESH_PATH = '/qqbot/im-qqbot/bind/refresh';
13
+ /** 手动填写/编辑凭据(绑定页表单) */
14
+ export const BIND_MANUAL_PATH = '/qqbot/im-qqbot/bind/manual';
15
+ /** 解绑:移除已保存凭据,回到绑定流程 */
16
+ export const BIND_UNBIND_PATH = '/qqbot/im-qqbot/bind/unbind';
17
+ /** 配置读写(设置面板表单) */
18
+ export const BIND_CONFIG_PATH = '/qqbot/im-qqbot/config';
19
+ //# sourceMappingURL=bind-protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bind-protocol.js","sourceRoot":"","sources":["../../src/shared/bind-protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,kBAAkB;AAClB,MAAM,CAAC,MAAM,eAAe,GAAG,sBAAsB,CAAC;AAEtD,mBAAmB;AACnB,MAAM,CAAC,MAAM,iBAAiB,GAAG,8BAA8B,CAAC;AAEhE,uBAAuB;AACvB,MAAM,CAAC,MAAM,gBAAgB,GAAG,6BAA6B,CAAC;AAE9D,wBAAwB;AACxB,MAAM,CAAC,MAAM,gBAAgB,GAAG,6BAA6B,CAAC;AAE9D,mBAAmB;AACnB,MAAM,CAAC,MAAM,gBAAgB,GAAG,wBAAwB,CAAC"}
@@ -7,10 +7,15 @@ export declare const PLUGIN_VERSION: string;
7
7
  */
8
8
  export declare function buildUserAgent(): string;
9
9
  /**
10
- * 从插件安装路径推导 profile 目录(node_modules 的父目录)
10
+ * 从插件安装路径推导 profile 目录
11
11
  *
12
- * 逐级向上查找名为 node_modules 的目录并返回其父目录。
13
- * 用 basename/dirname 而非字符串分隔符匹配,兼容 Windows(\)与 pnpm 嵌套结构。
12
+ * 主策略:逐级向上探测 profile 标志文件(cordis.patch.yml / pnpm-workspace.yaml)。
13
+ * Desktop profile 使用 pnpm workspace 布局(profiles/desktop/dependencies/
14
+ * node_modules/.pnpm/<pkg>/node_modules/...),插件嵌套于多级 node_modules,
15
+ * 仅按 node_modules 匹配会命中内层目录而返回错误位置。
16
+ *
17
+ * 兜底:无标志文件时沿用旧启发式(名为 node_modules 的目录的父目录),
18
+ * 覆盖 CLI profile 的浅层 npm 布局与未写入过 patch 的全新 profile。
14
19
  *
15
20
  * @param baseDir - 起始目录,缺省为插件根(可注入便于测试)
16
21
  */
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/shared/utils.ts"],"names":[],"mappings":"AAcA,sDAAsD;AACtD,eAAO,MAAM,cAAc,QAAsB,CAAC;AAYlD;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,MAAoB,GAAG,MAAM,GAAG,IAAI,CAS1E;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAKtE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAOtD"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/shared/utils.ts"],"names":[],"mappings":"AAcA,sDAAsD;AACtD,eAAO,MAAM,cAAc,QAAsB,CAAC;AAYlD;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,MAAoB,GAAG,MAAM,GAAG,IAAI,CAwB1E;AAuBD;;GAEG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAKtE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAOtD"}