@cxyhhhhh/dsh-qqbot 0.4.0 → 0.5.0-dev.202610101544

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 (53) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/CHANGELOG_EN.md +84 -0
  3. package/README.md +86 -5
  4. package/README_EN.md +74 -4
  5. package/dist/commands/misc.d.ts +1 -1
  6. package/dist/commands/misc.d.ts.map +1 -1
  7. package/dist/commands/misc.js +25 -3
  8. package/dist/commands/misc.js.map +1 -1
  9. package/dist/features/approval-channel.d.ts +3 -1
  10. package/dist/features/approval-channel.d.ts.map +1 -1
  11. package/dist/features/approval-channel.js +1 -1
  12. package/dist/features/approval-channel.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 +2 -3
  16. package/dist/index.js.map +1 -1
  17. package/dist/setup/bind-routes.d.ts +35 -0
  18. package/dist/setup/bind-routes.d.ts.map +1 -0
  19. package/dist/setup/bind-routes.js +79 -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 +31 -0
  26. package/dist/setup/index.d.ts.map +1 -0
  27. package/dist/setup/index.js +54 -0
  28. package/dist/setup/index.js.map +1 -0
  29. package/dist/{setup.d.ts → setup/persist.d.ts} +1 -8
  30. package/dist/setup/persist.d.ts.map +1 -0
  31. package/dist/{setup.js → setup/persist.js} +13 -66
  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/utils.d.ts +8 -3
  42. package/dist/shared/utils.d.ts.map +1 -1
  43. package/dist/shared/utils.js +45 -4
  44. package/dist/shared/utils.js.map +1 -1
  45. package/docs/assets/qqchannel.jpg +0 -0
  46. package/docs/assets/qqgroup.jpg +0 -0
  47. package/lib/client.js +278 -0
  48. package/lib/client.js.map +1 -0
  49. package/package.json +23 -9
  50. package/tsconfig.client.json +19 -0
  51. package/tsdown.client.config.ts +52 -0
  52. package/dist/setup.d.ts.map +0 -1
  53. package/dist/setup.js.map +0 -1
@@ -0,0 +1,79 @@
1
+ /** 状态查询(绑定页轮询) */
2
+ export const BIND_STATE_PATH = '/qqbot/im-qqbot/bind';
3
+ /** 刷新二维码(绑定页按钮) */
4
+ export const BIND_REFRESH_PATH = '/qqbot/im-qqbot/bind/refresh';
5
+ /**
6
+ * 注册绑定路由。
7
+ *
8
+ * @returns 注销函数(与插件 fiber 生命周期绑定)
9
+ */
10
+ export function registerBindRoutes(webServer, service, logger) {
11
+ const disposers = [
12
+ webServer.register({
13
+ kind: 'exact',
14
+ path: BIND_STATE_PATH,
15
+ handler: (req, res) => { handleState(req, res, service); },
16
+ }),
17
+ webServer.register({
18
+ kind: 'exact',
19
+ path: BIND_REFRESH_PATH,
20
+ handler: (req, res) => { handleRefresh(req, res, service); },
21
+ }),
22
+ ];
23
+ logger.info(`im-qqbot: 端内绑定页已就绪: ${BIND_STATE_PATH}`);
24
+ return () => { for (const dispose of disposers)
25
+ dispose(); };
26
+ }
27
+ /**
28
+ * 请求来源校验(最小 CSRF 防护,参照 api-request-trust 的 Origin 语义)。
29
+ *
30
+ * 带 Origin 的请求必须与 Host 同源;同源 fetch 与 Desktop IPC 桥
31
+ * (后者会删除 origin 头)均放行。本路由数据不敏感且 refresh 动作无害,
32
+ * 该级别的防护足够。
33
+ */
34
+ function trusted(req) {
35
+ const origin = req.headers.origin;
36
+ if (origin === undefined)
37
+ return true;
38
+ try {
39
+ return new URL(origin).host === req.headers.host;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ function sendJson(res, status, body) {
46
+ const payload = JSON.stringify(body);
47
+ res.writeHead(status, {
48
+ 'content-type': 'application/json; charset=utf-8',
49
+ 'cache-control': 'no-store',
50
+ });
51
+ res.end(payload);
52
+ }
53
+ function handleState(req, res, service) {
54
+ if (!trusted(req)) {
55
+ sendJson(res, 403, { error: 'forbidden' });
56
+ return;
57
+ }
58
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
59
+ sendJson(res, 405, { error: 'method not allowed' });
60
+ return;
61
+ }
62
+ sendJson(res, 200, service.wireState());
63
+ }
64
+ function handleRefresh(req, res, service) {
65
+ if (!trusted(req)) {
66
+ sendJson(res, 403, { error: 'forbidden' });
67
+ return;
68
+ }
69
+ if (req.method !== 'POST') {
70
+ sendJson(res, 405, { error: 'method not allowed' });
71
+ return;
72
+ }
73
+ // 丢弃可能的请求体(refresh 无参数)
74
+ req.resume();
75
+ service.refresh();
76
+ // 新二维码异步生成,统一由状态轮询收敛
77
+ sendJson(res, 200, { phase: 'starting' });
78
+ }
79
+ //# sourceMappingURL=bind-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bind-routes.js","sourceRoot":"","sources":["../../src/setup/bind-routes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ import type { QrConnectCallbacks, QrConnectOptions } from '@tencent-connect/qqbot-connector';
2
+ import type { Logger } from '../types.ts';
3
+ import type { QrPresenter } from './presenters.ts';
4
+ import { renderQrSvgDataUrl } from './qr-svg.ts';
5
+ import type { SetupCredentials } from './persist.ts';
6
+ /** connector 连接函数签名(可注入便于测试) */
7
+ export type ConnectFn = (callbacks: QrConnectCallbacks, options?: QrConnectOptions) => () => void;
8
+ /** 绑定状态机相位 */
9
+ export type BindPhase = {
10
+ kind: 'idle';
11
+ } | {
12
+ kind: 'waiting';
13
+ url: string;
14
+ qrSvg?: string;
15
+ issuedAt: number;
16
+ } | {
17
+ kind: 'saving';
18
+ } | {
19
+ kind: 'failed';
20
+ reason: string;
21
+ };
22
+ /** HTTP 路由序列化形态(线上协议,供端内绑定页消费) */
23
+ export type BindStateWire = {
24
+ phase: 'starting';
25
+ } | {
26
+ phase: 'waiting';
27
+ url: string;
28
+ qrSvg?: string;
29
+ issuedAt: number;
30
+ } | {
31
+ phase: 'saving';
32
+ } | {
33
+ phase: 'failed';
34
+ reason: string;
35
+ };
36
+ /** 构造参数 */
37
+ export interface BindingServiceOptions {
38
+ /** 调用来源标识,拼入二维码 URL */
39
+ source: string;
40
+ logger: Logger;
41
+ /** connector 注入点(默认真实实现) */
42
+ connect?: ConnectFn;
43
+ /** 二维码渲染注入点(默认真实实现) */
44
+ renderQr?: typeof renderQrSvgDataUrl;
45
+ }
46
+ /**
47
+ * 单个绑定会话的状态机。
48
+ *
49
+ * start() 幂等;refresh() 通过 abort 当前 connectOnce 使 run 循环
50
+ * 重建绑定任务(新二维码);dispose() 终止一切轮询。
51
+ */
52
+ export declare class BindingService {
53
+ private phase;
54
+ private readonly listeners;
55
+ private readonly presenters;
56
+ private readonly connectFn;
57
+ private readonly renderQrFn;
58
+ private readonly source;
59
+ private readonly logger;
60
+ private readonly lifetime;
61
+ private refreshCtl;
62
+ private runId;
63
+ private startPromise;
64
+ private disposed;
65
+ constructor(options: BindingServiceOptions);
66
+ /** 当前相位(路由轮询读取) */
67
+ get currentPhase(): BindPhase;
68
+ /** 状态订阅(预留给 SSE 推送等增强) */
69
+ subscribe(listener: () => void): () => void;
70
+ /** 挂载呈现策略(须在 start() 前调用) */
71
+ attach(presenter: QrPresenter): void;
72
+ /** 序列化为线上协议 */
73
+ wireState(): BindStateWire;
74
+ /** 启动绑定流程(幂等) */
75
+ start(): Promise<SetupCredentials | null>;
76
+ /** 用户请求刷新:废弃当前二维码任务,run 循环会重建 */
77
+ refresh(): void;
78
+ /** 终止绑定(插件卸载 / HMR 接管 / 替换) */
79
+ dispose(): void;
80
+ private run;
81
+ private connectOnce;
82
+ private handleQrDisplayed;
83
+ private handleQrExpired;
84
+ private setPhase;
85
+ }
86
+ //# sourceMappingURL=binding-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binding-service.d.ts","sourceRoot":"","sources":["../../src/setup/binding-service.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,kBAAkB,EAAwB,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACnH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,gCAAgC;AAChC,MAAM,MAAM,SAAS,GAAG,CAAC,SAAS,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE,gBAAgB,KAAK,MAAM,IAAI,CAAC;AAElG,cAAc;AACd,MAAM,MAAM,SAAS,GACjB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAClE;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvC,kCAAkC;AAClC,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,CAAC;AAExC,WAAW;AACX,MAAM,WAAW,qBAAqB;IACpC,uBAAuB;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,4BAA4B;IAC5B,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,uBAAuB;IACvB,QAAQ,CAAC,EAAE,OAAO,kBAAkB,CAAC;CACtC;AAED;;;;;GAKG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyB;IACnD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAChD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA4B;IACvD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;IAClD,OAAO,CAAC,UAAU,CAAgC;IAClD,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,YAAY,CAAiD;IACrE,OAAO,CAAC,QAAQ,CAAS;gBAEb,OAAO,EAAE,qBAAqB;IAO1C,mBAAmB;IACnB,IAAI,YAAY,IAAI,SAAS,CAE5B;IAED,0BAA0B;IAC1B,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAK3C,6BAA6B;IAC7B,MAAM,CAAC,SAAS,EAAE,WAAW,GAAG,IAAI;IAIpC,eAAe;IACf,SAAS,IAAI,aAAa;IAa1B,iBAAiB;IACjB,KAAK,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IASzC,iCAAiC;IACjC,OAAO,IAAI,IAAI;IAMf,+BAA+B;IAC/B,OAAO,IAAI,IAAI;YASD,GAAG;IAwBjB,OAAO,CAAC,WAAW;YAqCL,iBAAiB;IAgB/B,OAAO,CAAC,eAAe;IAKvB,OAAO,CAAC,QAAQ;CAIjB"}
@@ -0,0 +1,175 @@
1
+ /**
2
+ * QQ Bot 绑定服务 — 扫码状态机(宿主内唯一事实源)
3
+ *
4
+ * - 协议轮询只在宿主执行(appSecret 解密后绝不出宿主进程)
5
+ * - 端内绑定页(bind-routes)只读投影本服务状态
6
+ * - 生命周期可取消(HMR / 插件卸载时 abort 轮询,不留孤儿任务)
7
+ * - refresh() 仅废弃当前二维码任务并重建(用户点击「刷新二维码」)
8
+ */
9
+ import { startQrConnect } from '@tencent-connect/qqbot-connector';
10
+ import { renderQrSvgDataUrl } from "./qr-svg.js";
11
+ /**
12
+ * 单个绑定会话的状态机。
13
+ *
14
+ * start() 幂等;refresh() 通过 abort 当前 connectOnce 使 run 循环
15
+ * 重建绑定任务(新二维码);dispose() 终止一切轮询。
16
+ */
17
+ export class BindingService {
18
+ phase = { kind: 'idle' };
19
+ listeners = new Set();
20
+ presenters = [];
21
+ connectFn;
22
+ renderQrFn;
23
+ source;
24
+ logger;
25
+ lifetime = new AbortController();
26
+ refreshCtl = null;
27
+ runId = 0;
28
+ startPromise = null;
29
+ disposed = false;
30
+ constructor(options) {
31
+ this.source = options.source;
32
+ this.logger = options.logger;
33
+ this.connectFn = options.connect ?? startQrConnect;
34
+ this.renderQrFn = options.renderQr ?? renderQrSvgDataUrl;
35
+ }
36
+ /** 当前相位(路由轮询读取) */
37
+ get currentPhase() {
38
+ return this.phase;
39
+ }
40
+ /** 状态订阅(预留给 SSE 推送等增强) */
41
+ subscribe(listener) {
42
+ this.listeners.add(listener);
43
+ return () => { this.listeners.delete(listener); };
44
+ }
45
+ /** 挂载呈现策略(须在 start() 前调用) */
46
+ attach(presenter) {
47
+ this.presenters.push(presenter);
48
+ }
49
+ /** 序列化为线上协议 */
50
+ wireState() {
51
+ switch (this.phase.kind) {
52
+ case 'idle':
53
+ return { phase: 'starting' };
54
+ case 'waiting':
55
+ return { phase: 'waiting', url: this.phase.url, qrSvg: this.phase.qrSvg, issuedAt: this.phase.issuedAt };
56
+ case 'saving':
57
+ return { phase: 'saving' };
58
+ case 'failed':
59
+ return { phase: 'failed', reason: this.phase.reason };
60
+ }
61
+ }
62
+ /** 启动绑定流程(幂等) */
63
+ start() {
64
+ if (this.startPromise === null) {
65
+ this.startPromise = this.run().finally(() => {
66
+ this.refreshCtl = null;
67
+ });
68
+ }
69
+ return this.startPromise;
70
+ }
71
+ /** 用户请求刷新:废弃当前二维码任务,run 循环会重建 */
72
+ refresh() {
73
+ if (this.disposed || this.startPromise === null)
74
+ return;
75
+ console.log('正在刷新二维码…\n');
76
+ this.refreshCtl?.abort();
77
+ }
78
+ /** 终止绑定(插件卸载 / HMR 接管 / 替换) */
79
+ dispose() {
80
+ if (this.disposed)
81
+ return;
82
+ this.disposed = true;
83
+ this.refreshCtl?.abort();
84
+ this.lifetime.abort();
85
+ this.presenters.length = 0;
86
+ this.listeners.clear();
87
+ }
88
+ async run() {
89
+ while (!this.disposed) {
90
+ const generation = ++this.runId;
91
+ let credentials;
92
+ try {
93
+ credentials = await this.connectOnce(generation);
94
+ }
95
+ catch (err) {
96
+ if (this.disposed)
97
+ return null;
98
+ const reason = err instanceof Error ? err.message : String(err);
99
+ this.setPhase({ kind: 'failed', reason });
100
+ console.error(`[im-qqbot] 扫码绑定失败: ${reason}`);
101
+ return null;
102
+ }
103
+ if (this.disposed)
104
+ return null;
105
+ if (credentials !== null) {
106
+ console.log(`\n✔ 扫码绑定成功! AppID: ${credentials.appId}\n`);
107
+ this.setPhase({ kind: 'saving' });
108
+ return credentials;
109
+ }
110
+ // null = refresh 触发的重启或空凭据软失败,循环重建新二维码任务
111
+ }
112
+ return null;
113
+ }
114
+ connectOnce(generation) {
115
+ return new Promise((resolve, reject) => {
116
+ const refreshCtl = new AbortController();
117
+ this.refreshCtl = refreshCtl;
118
+ const signal = AbortSignal.any([this.lifetime.signal, refreshCtl.signal]);
119
+ this.connectFn({
120
+ onSuccess: (credentials) => {
121
+ const cred = credentials[0];
122
+ if (!cred) {
123
+ // 软失败:以 failed 终结(不进入 refresh 循环,避免无限重试)
124
+ reject(new Error('扫码未返回有效凭据'));
125
+ return;
126
+ }
127
+ resolve({ appId: cred.appId, appSecret: cred.appSecret });
128
+ },
129
+ onFailure: (error) => {
130
+ // refresh / dispose 触发的取消按「重启」或「静默退出」处理
131
+ if (this.lifetime.signal.aborted || refreshCtl.signal.aborted) {
132
+ resolve(null);
133
+ return;
134
+ }
135
+ reject(error);
136
+ },
137
+ onQrDisplayed: (url) => { void this.handleQrDisplayed(generation, url); },
138
+ onQrExpired: () => { this.handleQrExpired(generation); },
139
+ }, {
140
+ displayQrCodeToConsole: this.presenters.some((p) => p.printQrToConsole),
141
+ signal,
142
+ source: this.source,
143
+ });
144
+ });
145
+ }
146
+ async handleQrDisplayed(generation, url) {
147
+ // 终端链接指引先于 SVG 渲染:指引是二维码渲染错位时的自救通道,
148
+ // 必须与 connector 的二维码打印同步衔接,不隔任何异步间隙
149
+ for (const presenter of this.presenters)
150
+ presenter.onQrDisplayed(url);
151
+ let qrSvg;
152
+ try {
153
+ qrSvg = await this.renderQrFn(url);
154
+ }
155
+ catch (err) {
156
+ this.logger.warn(`im-qqbot: 二维码 SVG 渲染失败(页面将降级为链接): ${err instanceof Error ? err.message : String(err)}`);
157
+ }
158
+ // 渲染期间可能已被 refresh/dispose 淘汰
159
+ if (generation !== this.runId || this.disposed)
160
+ return;
161
+ this.setPhase({ kind: 'waiting', url, qrSvg, issuedAt: Date.now() });
162
+ }
163
+ handleQrExpired(generation) {
164
+ if (generation !== this.runId)
165
+ return;
166
+ for (const presenter of this.presenters)
167
+ presenter.onQrExpired?.();
168
+ }
169
+ setPhase(phase) {
170
+ this.phase = phase;
171
+ for (const listener of this.listeners)
172
+ listener();
173
+ }
174
+ }
175
+ //# sourceMappingURL=binding-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binding-service.js","sourceRoot":"","sources":["../../src/setup/binding-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 凭据绑定编排 — setup/ 模块入口
3
+ *
4
+ * apply() 检测到凭据缺失时调用 ensureCredentials:
5
+ * 1. 创建 BindingService(单例接管,HMR 双 fiber 安全)
6
+ * 2. Web 宿主(dsh web / Desktop)注册端内绑定页路由
7
+ * —— 经 ctx.inject(['webServer'], ...) 可选注入:服务未就绪时插件
8
+ * 照常加载(CLI profile),就绪/替换时自动(重)注册路由
9
+ * 3. 按宿主形态挂呈现策略(终端 / 浏览器降级)
10
+ * 4. 生命周期与 ctx fiber 绑定(dispose 时 abort 轮询、注销路由)
11
+ */
12
+ import type { Context } from '@deepseek-ai/cordis';
13
+ import type { Logger } from '../types.ts';
14
+ import type { SetupCredentials } from './persist.ts';
15
+ export type { SetupCredentials } from './persist.ts';
16
+ export { persistCredentialsToProfile } from './persist.ts';
17
+ export { isDesktopHost } from './presenters.ts';
18
+ /** ensureCredentials 参数 */
19
+ export interface EnsureCredentialsOptions {
20
+ logger: Logger;
21
+ /** 二维码 URL 的 source 标识 */
22
+ source?: string;
23
+ }
24
+ /**
25
+ * 执行扫码绑定编排,返回凭据;失败/取消返回 null。
26
+ *
27
+ * 凭据持久化由调用方(插件入口)执行——成功写入 cordis.patch.yml 会
28
+ * 触发 HMR 热重载,新 fiber 以凭据启动网关。
29
+ */
30
+ export declare function ensureCredentials(ctx: Context, options: EnsureCredentialsOptions): Promise<SetupCredentials | null>;
31
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/setup/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAI1C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAKhD,2BAA2B;AAC3B,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CA4ClC"}
@@ -0,0 +1,54 @@
1
+ import { BindingService } from "./binding-service.js";
2
+ import { registerBindRoutes } from "./bind-routes.js";
3
+ import { createBrowserPresenter, createTerminalPresenter, isDesktopHost } from "./presenters.js";
4
+ export { persistCredentialsToProfile } from "./persist.js";
5
+ export { isDesktopHost } from "./presenters.js";
6
+ /** 进程内活动绑定服务(HMR 接管用) */
7
+ let activeService = null;
8
+ /**
9
+ * 执行扫码绑定编排,返回凭据;失败/取消返回 null。
10
+ *
11
+ * 凭据持久化由调用方(插件入口)执行——成功写入 cordis.patch.yml 会
12
+ * 触发 HMR 热重载,新 fiber 以凭据启动网关。
13
+ */
14
+ export async function ensureCredentials(ctx, options) {
15
+ const logger = options.logger;
16
+ const source = options.source ?? 'dsh-qqbot';
17
+ // 单例接管:HMR 双 fiber 窗口下,新实例终止旧实例的轮询
18
+ const service = new BindingService({ source, logger });
19
+ if (activeService !== null && activeService !== service) {
20
+ activeService.dispose();
21
+ }
22
+ activeService = service;
23
+ // 生命周期:fiber dispose 时 abort 轮询
24
+ ctx.effect(() => () => {
25
+ service.dispose();
26
+ if (activeService === service)
27
+ activeService = null;
28
+ }, 'im-qqbot.binding');
29
+ // 呈现策略:
30
+ // - 终端通道(CLI / dsh web 运营者);Desktop 无可见终端,跳过
31
+ // - 浏览器降级通道(QQBOT_BIND_OPEN_BROWSER=1 显式启用)
32
+ if (!isDesktopHost()) {
33
+ service.attach(createTerminalPresenter());
34
+ }
35
+ if (process.env.QQBOT_BIND_OPEN_BROWSER === '1') {
36
+ service.attach(createBrowserPresenter(logger));
37
+ }
38
+ // 端内绑定页路由:webServer 可选注入。
39
+ // cordis 访问守卫要求服务经 inject 声明后方可访问;deps 未就绪时
40
+ // 回调不运行但插件照常加载(CLI profile),服务就绪/替换时回调
41
+ // 重跑(自动清理旧路由、注册新路由),与 webserver 的注入生命周期对齐。
42
+ ctx.inject(['webServer'], (scoped) => {
43
+ const webServer = scoped.webServer;
44
+ if (webServer === undefined || typeof webServer.register !== 'function')
45
+ return;
46
+ const disposeRoutes = registerBindRoutes(webServer, service, logger);
47
+ // 回调作用域(服务卸载/替换、插件卸载)连带注销路由
48
+ scoped.effect(() => {
49
+ return disposeRoutes;
50
+ }, 'im-qqbot.bind-routes');
51
+ });
52
+ return service.start();
53
+ }
54
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/setup/index.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAsB,MAAM,kBAAkB,CAAC;AAC1E,OAAO,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAIjG,OAAO,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,yBAAyB;AACzB,IAAI,aAAa,GAA0B,IAAI,CAAC;AAShD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,GAAY,EACZ,OAAiC;IAEjC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC9B,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,WAAW,CAAC;IAE7C,mCAAmC;IACnC,MAAM,OAAO,GAAG,IAAI,cAAc,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACvD,IAAI,aAAa,KAAK,IAAI,IAAI,aAAa,KAAK,OAAO,EAAE,CAAC;QACxD,aAAa,CAAC,OAAO,EAAE,CAAC;IAC1B,CAAC;IACD,aAAa,GAAG,OAAO,CAAC;IAExB,gCAAgC;IAC/B,GAAwE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE;QAC1F,OAAO,CAAC,OAAO,EAAE,CAAC;QAClB,IAAI,aAAa,KAAK,OAAO;YAAE,aAAa,GAAG,IAAI,CAAC;IACtD,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAEvB,QAAQ;IACR,6CAA6C;IAC7C,4CAA4C;IAC5C,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC;QACrB,OAAO,CAAC,MAAM,CAAC,uBAAuB,EAAE,CAAC,CAAC;IAC5C,CAAC;IACD,IAAI,OAAO,CAAC,GAAG,CAAC,uBAAuB,KAAK,GAAG,EAAE,CAAC;QAChD,OAAO,CAAC,MAAM,CAAC,sBAAsB,CAAC,MAAM,CAAC,CAAC,CAAC;IACjD,CAAC;IAED,0BAA0B;IAC1B,4CAA4C;IAC5C,uCAAuC;IACvC,2CAA2C;IAC1C,GAEC,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE;QAClC,MAAM,SAAS,GAAI,MAA6C,CAAC,SAAsC,CAAC;QACxG,IAAI,SAAS,KAAK,SAAS,IAAI,OAAO,SAAS,CAAC,QAAQ,KAAK,UAAU;YAAE,OAAO;QAChF,MAAM,aAAa,GAAG,kBAAkB,CAAC,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QACrE,4BAA4B;QAC3B,MAA2E,CAAC,MAAM,CAAC,GAAG,EAAE;YACvF,OAAO,aAAa,CAAC;QACvB,CAAC,EAAE,sBAAsB,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;IAEH,OAAO,OAAO,CAAC,KAAK,EAAE,CAAC;AACzB,CAAC"}
@@ -3,13 +3,6 @@ export interface SetupCredentials {
3
3
  appId: string;
4
4
  appSecret: string;
5
5
  }
6
- /**
7
- * 执行 QR 扫码绑定,获取 QQ Bot 凭据
8
- *
9
- * 在终端打印二维码等待用户扫码;同时输出扫码 URL,
10
- * 供二维码因系统字符问题渲染错位时点击/复制到浏览器打开扫码。
11
- */
12
- export declare function runQrSetup(source?: string): Promise<SetupCredentials | null>;
13
6
  /**
14
7
  * 将凭据写入 dsh profile 的 cordis.patch.yml
15
8
  *
@@ -21,4 +14,4 @@ export declare function persistCredentialsToProfile(credentials: SetupCredential
21
14
  info(msg: string, ...args: unknown[]): void;
22
15
  warn(msg: string, ...args: unknown[]): void;
23
16
  }): boolean;
24
- //# sourceMappingURL=setup.d.ts.map
17
+ //# 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"}
@@ -1,73 +1,13 @@
1
1
  /**
2
- * QQ Bot 凭据初始化 — 扫码绑定
2
+ * QQ Bot 凭据持久化 — 写入 dsh profile 的 cordis.patch.yml
3
3
  *
4
- * 当 appId/appSecret 未配置时,通过 @tencent-connect/qqbot-connector
5
- * 唤起终端扫码流程获取凭据,并写入 dsh profile 配置。
4
+ * 用 js-yaml 解析现有文件后更新/追加 im-qqbot 条目,再 dump 写回,
5
+ * 保证输出始终是合法 YAML。文件不存在或为空时安全重建;
6
+ * 解析失败或结构异常时拒绝写入、保留原文件(避免覆盖用户其他配置)。
6
7
  */
7
8
  import { resolve } from 'node:path';
8
9
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
9
10
  import yaml from 'js-yaml';
10
- import { startQrConnect } from '@tencent-connect/qqbot-connector';
11
- /** 终端可点击的 OSC 8 超链接(不支持 OSC 8 的终端会忽略控制序列,退化为纯 URL 文本) */
12
- function clickableLink(url) {
13
- return `\u001b]8;;${url}\u001b\\${url}\u001b]8;;\u001b\\`;
14
- }
15
- /**
16
- * 执行 QR 扫码绑定,获取 QQ Bot 凭据
17
- *
18
- * 在终端打印二维码等待用户扫码;同时输出扫码 URL,
19
- * 供二维码因系统字符问题渲染错位时点击/复制到浏览器打开扫码。
20
- */
21
- export async function runQrSetup(source = 'dsh-qqbot') {
22
- console.log('\n══════════════════════════════════════════════════════');
23
- console.log(' QQ Bot 凭据未配置,启动扫码绑定');
24
- console.log('══════════════════════════════════════════════════════\n');
25
- try {
26
- console.log('请使用手机 QQ 扫描下方二维码完成绑定...\n');
27
- // 回调风格:onQrDisplayed 会在二维码打印后、轮询开始前回调扫码 URL,
28
- // 让用户在二维码错位时也能通过链接打开扫码页(二维码过期刷新会再次触发)。
29
- const credentials = await new Promise((resolve, reject) => {
30
- startQrConnect({
31
- onSuccess: resolve,
32
- onFailure: reject,
33
- onQrDisplayed: (url) => {
34
- console.log('二维码显示异常?点击下方链接,用浏览器打开即可完成扫码:');
35
- console.log(` ${clickableLink(url)}\n`);
36
- },
37
- onQrExpired: () => {
38
- console.log('二维码已过期,正在刷新…\n');
39
- },
40
- }, { source });
41
- });
42
- if (!credentials || credentials.length === 0) {
43
- console.error('[im-qqbot] 扫码未返回凭据');
44
- return null;
45
- }
46
- const cred = credentials[0];
47
- if (!cred) {
48
- console.error('[im-qqbot] 扫码未返回有效凭据');
49
- return null;
50
- }
51
- console.log(`\n✔ 绑定成功!AppID: ${cred.appId}\n`);
52
- return {
53
- appId: cred.appId,
54
- appSecret: cred.appSecret,
55
- };
56
- }
57
- catch (err) {
58
- const msg = err instanceof Error ? err.message : String(err);
59
- if (msg.includes('Cannot find') || msg.includes('ERR_MODULE_NOT_FOUND')) {
60
- console.error('[im-qqbot] @tencent-connect/qqbot-connector 未安装,无法扫码绑定');
61
- console.error('[im-qqbot] 请通过环境变量配置:');
62
- console.error(' export QQBOT_APPID="你的AppID"');
63
- console.error(' export QQBOT_SECRET="你的AppSecret"');
64
- }
65
- else {
66
- console.error(`[im-qqbot] 扫码绑定失败: ${msg}`);
67
- }
68
- return null;
69
- }
70
- }
71
11
  /**
72
12
  * 将凭据写入 dsh profile 的 cordis.patch.yml
73
13
  *
@@ -130,6 +70,9 @@ export function persistCredentialsToProfile(credentials, profileDir, logger) {
130
70
  *
131
71
  * - 空内容 / 仅注释(yaml.load 返回 null/undefined)→ 空数组(允许安全重建)
132
72
  * - 空数组 `[]` / 条目列表 → 过滤出含 id 的对象
73
+ * - 非空但过滤后无任何可识别条目(如 bundle patch 的 `- insert: [...]`
74
+ * 结构)→ 抛错(由调用方保留原文件——此类文件不是 profile 用户 patch,
75
+ * 整体重写会破坏其原有内容)
133
76
  * - 非数组结构 / YAML 语法错误 → 抛错(由调用方保留原文件,避免覆盖用户其他配置)
134
77
  */
135
78
  function parsePatchEntries(content) {
@@ -142,7 +85,11 @@ function parsePatchEntries(content) {
142
85
  if (!Array.isArray(parsed)) {
143
86
  throw new Error('cordis.patch.yml 顶层必须是 YAML 数组');
144
87
  }
145
- return parsed.filter((e) => typeof e === 'object' && e !== null && typeof e.id === 'string');
88
+ const entries = parsed.filter((e) => typeof e === 'object' && e !== null && typeof e.id === 'string');
89
+ if (parsed.length > 0 && entries.length === 0) {
90
+ throw new Error('cordis.patch.yml 含条目但无带 id 的 patch 行(可能是 bundle patch,拒绝写入以保留原文件)');
91
+ }
92
+ return entries;
146
93
  }
147
94
  /** 开发模式引导:未定位到 profile 目录,引导用环境变量配置(console.log 确保可见) */
148
95
  function printEnvInstructions(credentials) {
@@ -167,4 +114,4 @@ function printYamlInstructions(credentials, patchPath) {
167
114
  console.log(` appId: "${credentials.appId}"`);
168
115
  console.log(` appSecret: "${credentials.appSecret}"`);
169
116
  }
170
- //# sourceMappingURL=setup.js.map
117
+ //# 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
+ * (官方启动约定),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"}