@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
package/lib/client.js ADDED
@@ -0,0 +1,996 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@tencent-connect/dsh-qqbot",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ //#region src/shared/bind-protocol.ts
9
+ /**
10
+ * 端内绑定协议 —— 宿主路由与客户端面板的共享契约
11
+ *
12
+ * 类型与路径常量的唯一事实源:宿主(setup/bind-routes.ts)与浏览器
13
+ * (client/bind-page.ts)各自 import,杜绝两端协议漂移。
14
+ * 本模块必须保持纯类型 + 纯常量(零 node 依赖、零副作用):
15
+ * 客户端 tsdown 构建会将其内联进 bundle。
16
+ */
17
+ /** 状态查询(绑定页轮询) */
18
+ const BIND_STATE_PATH = "/qqbot/im-qqbot/bind";
19
+ /** 刷新二维码(绑定页按钮) */
20
+ const BIND_REFRESH_PATH = "/qqbot/im-qqbot/bind/refresh";
21
+ /** 手动填写/编辑凭据(绑定页表单) */
22
+ const BIND_MANUAL_PATH = "/qqbot/im-qqbot/bind/manual";
23
+ /** 解绑:移除已保存凭据,回到绑定流程 */
24
+ const BIND_UNBIND_PATH = "/qqbot/im-qqbot/bind/unbind";
25
+ /** 配置读写(设置面板表单) */
26
+ const BIND_CONFIG_PATH = "/qqbot/im-qqbot/config";
27
+ //#endregion
28
+ //#region src/client/bind-page.ts
29
+ /**
30
+ * 端内绑定面板 — 扫码 / 手动填写 / 已绑定编辑
31
+ *
32
+ * 宿主插件注册 /qqbot/im-qqbot/bind 路由(待绑定与已绑定 fiber 均常驻):
33
+ * - 未绑定:waiting(二维码)/ starting / saving / failed 相位 + manual 提交
34
+ * - 已绑定:bound 相位(回显脱敏 appId,secret 不回传)+ manual 更换 + 解绑
35
+ * 扫码成功或手动保存均写入 profile patch 并经 HMR 重载生效。
36
+ *
37
+ * 样式为注入式 CSS 类(imqb-* 命名空间,随面板挂载注入一次):
38
+ * hover/过渡/focus 样式无法用 inline style 表达,统一走类;
39
+ * 颜色尽量取 DSW 主题变量(--dsw-alias-*),缺省值兜底。
40
+ *
41
+ * 同一组件复用于两种展示位(侧栏绑定页 / 设置页 QQ Bot 栏目)。
42
+ * 运行时仅依赖 react(dsh 客户端平台种子表提供);宿主通信走同源 fetch,
43
+ * Desktop 下由壳的 IPC 桥自动附加 Host 认证转发。
44
+ */
45
+ /** 轮询间隔(毫秒):活跃轮询 / 失败退避 */
46
+ const ACTIVE_POLL_MS = 2e3;
47
+ const RETRY_POLL_MS = 3e3;
48
+ /** 样式注入标记(幂等) */
49
+ const STYLE_ID$1 = "im-qqbot-bind-panel-styles";
50
+ /**
51
+ * 注入面板样式(一次)。交互态(hover/active/focus)无法用 inline style
52
+ * 表达,统一收敛到类;DSW 主题变量缺失时用中性值兜底。
53
+ */
54
+ function usePanelStyles() {
55
+ (0, react.useEffect)(() => {
56
+ if (typeof document === "undefined") return;
57
+ if (document.getElementById(STYLE_ID$1) !== null) return;
58
+ const style = document.createElement("style");
59
+ style.id = STYLE_ID$1;
60
+ style.textContent = `
61
+ .imqb-panel {
62
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
63
+ gap: 14px; padding: 24px; min-height: 320px; text-align: center; font-size: 14px;
64
+ }
65
+ .imqb-title { margin: 0; font-size: 16px; font-weight: 600; }
66
+ .imqb-hint { margin: 0; opacity: 0.75; line-height: 1.6; }
67
+ .imqb-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; }
68
+ .imqb-link { font-size: 13px; opacity: 0.85; color: var(--dsw-alias-brand, #4d6bfe); text-decoration: none; }
69
+ .imqb-link:hover { text-decoration: underline; }
70
+ .imqb-icon { font-size: 32px; line-height: 1; }
71
+ .imqb-qr { border-radius: 10px; background: #fff; padding: 10px; max-width: 100%; }
72
+ .imqb-row { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
73
+ .imqb-btn, .imqb-pill {
74
+ font: inherit; cursor: pointer; color: inherit;
75
+ transition: background-color .15s ease, border-color .15s ease, color .15s ease, opacity .15s ease;
76
+ }
77
+ .imqb-btn:disabled, .imqb-pill:disabled { opacity: 0.5; cursor: default; }
78
+ .imqb-btn {
79
+ padding: 7px 18px; border-radius: 8px; font-size: 13px;
80
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));
81
+ background: transparent;
82
+ }
83
+ .imqb-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.12)); }
84
+ .imqb-btn--primary {
85
+ border-color: transparent;
86
+ background: var(--dsw-alias-brand, #4d6bfe); color: #fff; font-weight: 500;
87
+ }
88
+ .imqb-btn--primary:hover:not(:disabled) { background: var(--dsw-alias-brand-strong, #3b5cf0); }
89
+ .imqb-btn--danger { color: var(--dsw-alias-danger, #f26d6d); border-color: var(--dsw-alias-danger, rgba(242, 109, 109, 0.5)); }
90
+ .imqb-btn--danger:hover:not(:disabled) { background: var(--dsw-alias-danger, rgba(242, 109, 109, 0.12)); }
91
+ .imqb-ghost {
92
+ border: none; background: transparent; opacity: 0.65; padding: 4px 8px;
93
+ font: inherit; font-size: 13px; cursor: pointer; color: inherit;
94
+ }
95
+ .imqb-ghost:hover { opacity: 1; text-decoration: underline; }
96
+ .imqb-pill {
97
+ padding: 5px 16px; border-radius: 999px; font-size: 13px;
98
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4)); background: transparent;
99
+ }
100
+ .imqb-pill:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.12)); }
101
+ .imqb-btn:focus-visible, .imqb-pill:focus-visible, .imqb-input:focus-visible, .imqb-ghost:focus-visible {
102
+ outline: 2px solid var(--dsw-alias-brand, #4d6bfe); outline-offset: 1px;
103
+ }
104
+ .imqb-form { display: flex; flex-direction: column; gap: 12px; width: min(320px, 100%); box-sizing: border-box; }
105
+ .imqb-field { display: flex; flex-direction: column; gap: 5px; text-align: left; font-size: 13px; }
106
+ .imqb-input {
107
+ padding: 8px 10px; border-radius: 8px; box-sizing: border-box; width: 100%;
108
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));
109
+ background: var(--dsw-alias-bg-input, rgba(128, 128, 128, 0.08));
110
+ color: inherit; font: inherit; font-size: 13px; outline: none;
111
+ }
112
+ .imqb-input:focus { border-color: var(--dsw-alias-brand, #4d6bfe); }
113
+ .imqb-error { margin: 0; font-size: 12px; color: var(--dsw-alias-danger, #f26d6d); }
114
+ /* 紧凑状态视图(已绑定 / 过渡态):设置行风格,无居中大卡留白 */
115
+ .imqb-status { display: flex; flex-direction: column; text-align: left; font-size: 14px; }
116
+ .imqb-status-row { display: flex; align-items: center; gap: 12px; padding: 12px 0; }
117
+ .imqb-status-icon { font-size: 24px; line-height: 1; flex: none; }
118
+ .imqb-qqicon {
119
+ flex: none; display: block; border-radius: 8px;
120
+ background: #fff; /* 深色企鹅 logo 需浅底,暗色主题下保持可见 */
121
+ }
122
+ .imqb-status-meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
123
+ .imqb-status-title { margin: 0; font-size: 14px; font-weight: 600; line-height: 1.5; }
124
+ .imqb-status-sub { margin: 0; font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-tertiary, inherit); opacity: .85; }
125
+ .imqb-status-actions { display: flex; align-items: center; gap: 8px; padding: 4px 0 12px; }
126
+ /* 扫码成功后的连接过渡:二维码模糊变暗 + 覆盖层 */
127
+ .imqb-scanned { position: relative; display: inline-block; }
128
+ .imqb-scanned-qr {
129
+ display: block; border-radius: 10px; background: #fff; padding: 10px;
130
+ filter: blur(6px) saturate(0.6); opacity: 0.55;
131
+ }
132
+ .imqb-scanned-veil {
133
+ position: absolute; inset: 0; display: flex; flex-direction: column;
134
+ align-items: center; justify-content: center; gap: 6px; text-align: center;
135
+ }
136
+ .imqb-scanned-title { margin: 0; font-size: 15px; font-weight: 600; }
137
+ .imqb-scanned-sub { margin: 0; font-size: 12px; opacity: 0.75; line-height: 1.6; }
138
+ `;
139
+ document.head.appendChild(style);
140
+ return () => {
141
+ style.remove();
142
+ };
143
+ }, []);
144
+ }
145
+ /**
146
+ * 绑定状态轮询 hook。
147
+ *
148
+ * 404 语义分两种:从未拿到过路由 = 插件未启用(终态);
149
+ * 曾经拿到过 = HMR 重载窗口期(manual/unbind 保存后路由短暂消失),
150
+ * 低频等待恢复而不是判定终态——否则保存成功提示会被
151
+ * 「插件未启用」覆盖且无法自愈。
152
+ *
153
+ * bound 为稳定态:轮询大幅降频(30s)。因此解绑等本地动作后必须
154
+ * 调用 recheck() 立即重置节拍——否则视图最长停留 30s+ 才收敛到
155
+ * 新相位(如解绑后的 waiting 二维码)。
156
+ *
157
+ * @returns 轮询快照与 recheck:state 为宿主相位(null = 加载中),
158
+ * gone 表示插件未启用(从未注册过路由),recheck 废弃当前
159
+ * 定时器并立即重新轮询(新轮询链取代旧链,不产生双计时器)。
160
+ */
161
+ function useBindState() {
162
+ const [state, setState] = (0, react.useState)(null);
163
+ const [gone, setGone] = (0, react.useState)(false);
164
+ const recheckRef = (0, react.useRef)(() => {});
165
+ (0, react.useEffect)(() => {
166
+ let stopped = false;
167
+ let sawRoute = false;
168
+ let generation = 0;
169
+ let running = false;
170
+ let restartAfterFlight = false;
171
+ let urgentUntil = 0;
172
+ let timer;
173
+ const schedule = (ms) => {
174
+ timer = setTimeout(() => {
175
+ tick();
176
+ }, ms);
177
+ };
178
+ const intervalFor = (phase) => {
179
+ const base = phase === "saving" ? 500 : phase === "bound" ? 3e4 : ACTIVE_POLL_MS;
180
+ return Date.now() < urgentUntil ? Math.min(base, ACTIVE_POLL_MS) : base;
181
+ };
182
+ const tick = async () => {
183
+ if (stopped || running) return;
184
+ running = true;
185
+ const myGeneration = generation;
186
+ const alive = () => myGeneration === generation && !stopped;
187
+ try {
188
+ const res = await fetch(BIND_STATE_PATH, { cache: "no-store" });
189
+ if (res.status === 404) {
190
+ if (!stopped && !sawRoute) setGone(true);
191
+ else if (alive()) schedule(Date.now() < urgentUntil ? 1e3 : 3e3);
192
+ return;
193
+ }
194
+ if (!res.ok) {
195
+ if (alive()) schedule(RETRY_POLL_MS);
196
+ return;
197
+ }
198
+ sawRoute = true;
199
+ const body = await res.json();
200
+ if (!alive()) return;
201
+ setState(body);
202
+ schedule(intervalFor(body.phase));
203
+ return;
204
+ } catch {} finally {
205
+ running = false;
206
+ if (restartAfterFlight && !stopped) {
207
+ restartAfterFlight = false;
208
+ tick();
209
+ }
210
+ }
211
+ if (alive()) schedule(RETRY_POLL_MS);
212
+ };
213
+ recheckRef.current = () => {
214
+ generation += 1;
215
+ urgentUntil = Date.now() + 15e3;
216
+ if (timer !== void 0) clearTimeout(timer);
217
+ if (running) {
218
+ restartAfterFlight = true;
219
+ return;
220
+ }
221
+ tick();
222
+ };
223
+ tick();
224
+ return () => {
225
+ stopped = true;
226
+ if (timer !== void 0) clearTimeout(timer);
227
+ };
228
+ }, []);
229
+ return {
230
+ state,
231
+ gone,
232
+ recheck: (0, react.useCallback)(() => {
233
+ recheckRef.current();
234
+ }, [])
235
+ };
236
+ }
237
+ /** 手动提交凭据(填写 / 更换共用) */
238
+ async function submitManualCredentials(appId, appSecret) {
239
+ try {
240
+ const res = await fetch(BIND_MANUAL_PATH, {
241
+ method: "POST",
242
+ headers: { "content-type": "application/json" },
243
+ body: JSON.stringify({
244
+ appId,
245
+ appSecret
246
+ })
247
+ });
248
+ const body = await res.json().catch(() => ({}));
249
+ if (res.ok && body.ok) return {
250
+ ok: true,
251
+ message: body.message
252
+ };
253
+ return {
254
+ ok: false,
255
+ error: body.error ?? `保存失败(HTTP ${res.status})`
256
+ };
257
+ } catch (err) {
258
+ return {
259
+ ok: false,
260
+ error: err instanceof Error ? err.message : String(err)
261
+ };
262
+ }
263
+ }
264
+ /**
265
+ * 手动凭据表单(填写 / 更换共用)。
266
+ *
267
+ * appSecret 永不回显(占位符提示),提交成功后展示保存结果;
268
+ * 宿主写入 profile patch 并经 HMR 重载,状态轮询随后收敛为 bound。
269
+ */
270
+ function ManualForm({ initialAppId }) {
271
+ const [appId, setAppId] = (0, react.useState)(initialAppId ?? "");
272
+ const [appSecret, setAppSecret] = (0, react.useState)("");
273
+ const [busy, setBusy] = (0, react.useState)(false);
274
+ const [result, setResult] = (0, react.useState)(null);
275
+ const submit = (0, react.useCallback)((event) => {
276
+ event.preventDefault();
277
+ if (busy) return;
278
+ const trimmedId = appId.trim();
279
+ const trimmedSecret = appSecret.trim();
280
+ if (trimmedId.length === 0 || trimmedSecret.length === 0) {
281
+ setResult({
282
+ ok: false,
283
+ error: "appId 与 appSecret 均为必填"
284
+ });
285
+ return;
286
+ }
287
+ setBusy(true);
288
+ submitManualCredentials(trimmedId, trimmedSecret).then((outcome) => {
289
+ setResult(outcome);
290
+ setBusy(false);
291
+ });
292
+ }, [
293
+ appId,
294
+ appSecret,
295
+ busy
296
+ ]);
297
+ if (result !== null && result.ok) return (0, react.createElement)("div", { className: "imqb-status" }, (0, react.createElement)("div", { className: "imqb-status-row" }, (0, react.createElement)("div", { className: "imqb-status-icon" }, "✅"), (0, react.createElement)("div", { className: "imqb-status-meta" }, (0, react.createElement)("p", { className: "imqb-status-title" }, result.message ?? "凭据已保存"), (0, react.createElement)("p", { className: "imqb-status-sub" }, "插件将自动重载;稍后即可开始对话"))));
298
+ return (0, react.createElement)("form", {
299
+ className: "imqb-form",
300
+ onSubmit: submit
301
+ }, (0, react.createElement)("label", { className: "imqb-field" }, "AppID", (0, react.createElement)("input", {
302
+ className: "imqb-input",
303
+ value: appId,
304
+ placeholder: "QQ 机器人 AppID",
305
+ onChange: (event) => {
306
+ setAppId(event.currentTarget.value);
307
+ },
308
+ autoComplete: "off",
309
+ spellCheck: false
310
+ })), (0, react.createElement)("label", { className: "imqb-field" }, "AppSecret", (0, react.createElement)("input", {
311
+ className: "imqb-input",
312
+ type: "password",
313
+ value: appSecret,
314
+ placeholder: initialAppId === void 0 ? "QQ 机器人 AppSecret" : "输入新的 AppSecret(不回显)",
315
+ onChange: (event) => {
316
+ setAppSecret(event.currentTarget.value);
317
+ },
318
+ autoComplete: "new-password"
319
+ })), result !== null && !result.ok ? (0, react.createElement)("p", { className: "imqb-error" }, result.error) : null, (0, react.createElement)("button", {
320
+ className: "imqb-btn imqb-btn--primary",
321
+ type: "submit",
322
+ disabled: busy
323
+ }, busy ? "正在保存…" : "保存"));
324
+ }
325
+ /** QQ 企鹅 logo(256×256 PNG data URI;深色图案配白底圆角,明暗主题下均可见) */
326
+ const QQ_BOT_ICON_PNG = "data:image/png;base64,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";
327
+ /** QQ 机器人图标(logo 图片,白底圆角容器内呈现) */
328
+ function QqBotIcon({ size = 28 }) {
329
+ return (0, react.createElement)("img", {
330
+ src: QQ_BOT_ICON_PNG,
331
+ alt: "QQ",
332
+ width: size,
333
+ height: size,
334
+ className: "imqb-qqicon"
335
+ });
336
+ }
337
+ /** 已绑定视图:回显 appId + 更换凭据 / 解绑入口 */
338
+ function BoundView({ appId, recheck }) {
339
+ const [editing, setEditing] = (0, react.useState)(false);
340
+ const [unbindResult, setUnbindResult] = (0, react.useState)(null);
341
+ const unbind = (0, react.useCallback)(() => {
342
+ if (!window.confirm("确定解绑吗?\n解绑后机器人立即下线,插件将重新进入扫码绑定流程。")) return;
343
+ fetch(BIND_UNBIND_PATH, { method: "POST" }).then(async (res) => {
344
+ const body = await res.json().catch(() => ({}));
345
+ if (res.ok && body.ok === true) {
346
+ setUnbindResult({
347
+ ok: true,
348
+ message: body.message ?? "已解绑,插件将重载并重新进入绑定流程"
349
+ });
350
+ recheck();
351
+ return;
352
+ }
353
+ setUnbindResult({
354
+ ok: false,
355
+ message: body.error ?? `解绑失败(HTTP ${res.status})`
356
+ });
357
+ }).catch((err) => {
358
+ setUnbindResult({
359
+ ok: false,
360
+ message: err instanceof Error ? err.message : String(err)
361
+ });
362
+ });
363
+ }, [recheck]);
364
+ if (unbindResult !== null) return (0, react.createElement)("div", { className: "imqb-status" }, (0, react.createElement)("div", { className: "imqb-status-row" }, (0, react.createElement)("div", { className: "imqb-status-icon" }, unbindResult.ok ? "👋" : "⚠️"), (0, react.createElement)("div", { className: "imqb-status-meta" }, (0, react.createElement)("p", { className: "imqb-status-title" }, unbindResult.message)), unbindResult.ok ? (0, react.createElement)("button", {
365
+ className: "imqb-ghost",
366
+ onClick: recheck
367
+ }, "刷新状态") : (0, react.createElement)("button", {
368
+ className: "imqb-ghost",
369
+ onClick: () => {
370
+ setUnbindResult(null);
371
+ }
372
+ }, "返回")));
373
+ if (editing) return (0, react.createElement)("div", { className: "imqb-status" }, (0, react.createElement)("div", { className: "imqb-status-row" }, (0, react.createElement)("div", { className: "imqb-status-meta" }, (0, react.createElement)("p", { className: "imqb-status-title" }, "更换凭据")), (0, react.createElement)("button", {
374
+ className: "imqb-ghost",
375
+ onClick: () => {
376
+ setEditing(false);
377
+ }
378
+ }, "返回")), (0, react.createElement)(ManualForm, { initialAppId: appId }));
379
+ return (0, react.createElement)("div", { className: "imqb-status" }, (0, react.createElement)("div", { className: "imqb-status-row" }, (0, react.createElement)(QqBotIcon, { size: 28 }), (0, react.createElement)("div", { className: "imqb-status-meta" }, (0, react.createElement)("p", { className: "imqb-status-title" }, "已绑定"), (0, react.createElement)("p", { className: "imqb-status-sub imqb-mono" }, `AppID ${appId} · 私聊与群聊已接入`))), (0, react.createElement)("div", { className: "imqb-status-actions" }, (0, react.createElement)("button", {
380
+ className: "imqb-btn imqb-btn--primary",
381
+ onClick: () => {
382
+ setEditing(true);
383
+ }
384
+ }, "更换凭据"), (0, react.createElement)("button", {
385
+ className: "imqb-btn imqb-btn--danger",
386
+ onClick: unbind
387
+ }, "解绑")));
388
+ }
389
+ /** 未绑定视图:扫码 / 手动填写切换 */
390
+ function UnboundView({ state }) {
391
+ const [mode, setMode] = (0, react.useState)("qr");
392
+ const refresh = (0, react.useCallback)(() => {
393
+ fetch(BIND_REFRESH_PATH, { method: "POST" }).catch(() => {});
394
+ }, []);
395
+ if (mode === "manual") return (0, react.createElement)("div", { className: "imqb-panel" }, (0, react.createElement)("p", { className: "imqb-title" }, "手动填写凭据"), (0, react.createElement)(ManualForm, {}), (0, react.createElement)("button", {
396
+ className: "imqb-ghost",
397
+ onClick: () => {
398
+ setMode("qr");
399
+ }
400
+ }, "改用扫码绑定"));
401
+ return (0, react.createElement)("div", { className: "imqb-panel" }, state.qrSvg === void 0 ? null : (0, react.createElement)("img", {
402
+ src: state.qrSvg,
403
+ alt: "QQ 扫码二维码",
404
+ width: 220,
405
+ height: 220,
406
+ className: "imqb-qr"
407
+ }), (0, react.createElement)("p", { className: "imqb-title" }, "扫码绑定 QQ 机器人"), (0, react.createElement)("p", { className: "imqb-hint" }, "使用手机 QQ 扫描二维码完成绑定"), (0, react.createElement)("a", {
408
+ href: state.url,
409
+ target: "_blank",
410
+ rel: "noreferrer noopener",
411
+ className: "imqb-link"
412
+ }, "二维码显示异常?在浏览器中打开"), (0, react.createElement)("div", { className: "imqb-row" }, (0, react.createElement)("button", {
413
+ className: "imqb-pill",
414
+ onClick: refresh
415
+ }, "刷新二维码"), (0, react.createElement)("button", {
416
+ className: "imqb-pill",
417
+ onClick: () => {
418
+ setMode("manual");
419
+ }
420
+ }, "手动填写")));
421
+ }
422
+ /** 依据宿主相位渲染面板主体(各展示位共用) */
423
+ function BindPanelBody() {
424
+ usePanelStyles();
425
+ const { state, gone, recheck } = useBindState();
426
+ const [lastQr, setLastQr] = (0, react.useState)(null);
427
+ (0, react.useEffect)(() => {
428
+ if (state === null) return;
429
+ if (state.phase === "waiting") {
430
+ if (state.qrSvg !== void 0) setLastQr(state.qrSvg);
431
+ } else if (state.phase === "bound") setLastQr(null);
432
+ }, [state]);
433
+ if (gone) return (0, react.createElement)("div", { className: "imqb-panel" }, (0, react.createElement)("div", { className: "imqb-icon" }, "❔"), (0, react.createElement)("p", { className: "imqb-hint" }, "插件未启用,或当前版本不支持端内配置"));
434
+ if (state === null || state.phase === "starting") return (0, react.createElement)("div", { className: "imqb-panel" }, (0, react.createElement)("p", { className: "imqb-hint" }, "正在获取绑定状态…"));
435
+ if (state.phase === "saving") {
436
+ if (lastQr !== null) return (0, react.createElement)("div", { className: "imqb-panel" }, (0, react.createElement)("div", { className: "imqb-scanned" }, (0, react.createElement)("img", {
437
+ src: lastQr,
438
+ alt: "QQ 扫码二维码",
439
+ width: 220,
440
+ height: 220,
441
+ className: "imqb-scanned-qr"
442
+ }), (0, react.createElement)("div", { className: "imqb-scanned-veil" }, (0, react.createElement)("p", { className: "imqb-scanned-title" }, "✅ 扫码成功"), (0, react.createElement)("p", { className: "imqb-scanned-sub" }, "正在连接机器人,稍候…"))));
443
+ return (0, react.createElement)("div", { className: "imqb-status" }, (0, react.createElement)("div", { className: "imqb-status-row" }, (0, react.createElement)("div", { className: "imqb-status-icon" }, "🎉"), (0, react.createElement)("div", { className: "imqb-status-meta" }, (0, react.createElement)("p", { className: "imqb-status-title" }, "扫码成功"), (0, react.createElement)("p", { className: "imqb-status-sub" }, "凭据已保存,插件正在重载,稍后即可开始对话"))));
444
+ }
445
+ if (state.phase === "failed") return (0, react.createElement)("div", { className: "imqb-panel" }, (0, react.createElement)("div", { className: "imqb-icon" }, "⚠️"), (0, react.createElement)("p", { className: "imqb-title" }, "绑定失败"), (0, react.createElement)("p", { className: "imqb-hint" }, state.reason), (0, react.createElement)("div", { className: "imqb-row" }, (0, react.createElement)("button", {
446
+ className: "imqb-btn imqb-btn--primary",
447
+ onClick: () => {
448
+ window.location.reload();
449
+ }
450
+ }, "重试")));
451
+ if (state.phase === "bound") return (0, react.createElement)(BoundView, {
452
+ appId: state.appId,
453
+ recheck
454
+ });
455
+ return (0, react.createElement)(UnboundView, { state });
456
+ }
457
+ //#endregion
458
+ //#region src/client/config-panel.ts
459
+ /**
460
+ * QQ 机器人配置面板 — 必要配置的 UI 编辑
461
+ *
462
+ * UI 复刻 DSW 通用设置(settings-form 原语)的视觉风格:
463
+ * 左对齐设置列、0.5px 行分隔、label 13px/500、控件右置;
464
+ * Switch 为 36×20 aria-checked 结构(thumb translateX 动画);
465
+ * 三值选择用分段控件(滑动指示器按段数/索引的 CSS 变量定位);
466
+ * 保存按钮为 DSW 反色主按钮(label-primary 底 + bg-layer-3 字)。
467
+ *
468
+ * 数据流:GET /qqbot/im-qqbot/config 回显当前生效配置(appSecret
469
+ * 已由宿主剔除)→ 表单编辑 → dirty diff → POST 仅提交变更键 →
470
+ * 宿主白名单 + Schema 校验 → 最小写入 profile patch → HMR 重载生效。
471
+ *
472
+ * 字段策略:只开放用户必要的配置;cwd 只读展示;超时与性能调优
473
+ * 类字段走 profile patch / 环境变量。
474
+ */
475
+ /** 样式注入标记(幂等) */
476
+ const STYLE_ID = "im-qqbot-config-panel-styles";
477
+ /** DSW 通用设置风格(settings-form / Switch / SegmentedControl 原语复刻) */
478
+ function useConfigPanelStyles() {
479
+ (0, react.useEffect)(() => {
480
+ if (typeof document === "undefined") return;
481
+ if (document.getElementById(STYLE_ID) !== null) return;
482
+ const style = document.createElement("style");
483
+ style.id = STYLE_ID;
484
+ style.textContent = `
485
+ .imqb2-form { display: flex; flex-direction: column; width: 100%; }
486
+ .imqb2-group-title {
487
+ padding: 14px 0 2px; font-size: 12px; font-weight: 600; line-height: 18px;
488
+ color: var(--dsw-alias-label-tertiary); text-transform: uppercase; letter-spacing: .04em;
489
+ }
490
+ .imqb2-field { display: flex; flex-direction: column; gap: 6px; padding: 12px 0; }
491
+ .imqb2-field + .imqb2-field { border-top: 0.5px solid var(--dsw-alias-border-l2); }
492
+ .imqb2-head { display: flex; align-items: center; gap: 8px; min-height: 28px; }
493
+ .imqb2-label {
494
+ flex: 1; min-width: 0; font-size: 13px; font-weight: 500; line-height: 1.5;
495
+ color: var(--dsw-alias-label-primary);
496
+ }
497
+ .imqb2-help { margin: 0; font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); }
498
+ .imqb2-hint { margin: 0; font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-tertiary); }
499
+ /* Switch(aria-checked 驱动,视觉态与无障碍态同源) */
500
+ .imqb2-switch {
501
+ box-sizing: border-box; position: relative; flex: 0 0 auto;
502
+ width: 36px; height: 20px; padding: 2px; border: 0; border-radius: 999px;
503
+ background: var(--dsw-alias-border-l3); cursor: pointer; font: inherit;
504
+ }
505
+ .imqb2-switch[aria-checked='true'] { background: var(--dsw-alias-brand-primary); }
506
+ .imqb2-switch:disabled { cursor: default; opacity: 0.5; }
507
+ .imqb2-switch:focus-visible {
508
+ outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
509
+ outline-offset: 2px;
510
+ }
511
+ .imqb2-switch-thumb {
512
+ display: block; width: 16px; height: 16px; border-radius: 50%;
513
+ background: var(--dsw-alias-switch-thumb, #ffffff);
514
+ transition: transform 120ms ease;
515
+ }
516
+ .imqb2-switch[aria-checked='true'] .imqb2-switch-thumb {
517
+ background: var(--dsw-alias-label-primary-foreground, #ffffff);
518
+ transform: translateX(16px);
519
+ }
520
+ /* 输入(官方 fields.input 复刻) */
521
+ .imqb2-input {
522
+ box-sizing: border-box; width: 100%; height: 34px; padding: 0 12px;
523
+ border: 0.5px solid var(--dsw-alias-border-l4); border-radius: var(--dsw-radius-md, 8px);
524
+ background: var(--dsw-alias-bg-layer-3); font: inherit; font-size: 13px; line-height: 1.5;
525
+ color: var(--dsw-alias-label-primary); outline: none;
526
+ }
527
+ .imqb2-input:focus { outline: none; border-color: var(--dsw-alias-state-business-primary); }
528
+ .imqb2-input:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
529
+ .imqb2-textarea { height: auto; min-height: 68px; padding: 8px 12px; resize: vertical; line-height: 1.5; }
530
+ /* 分段控件(官方 SegmentedControl 复刻:指示器按段数/索引定位) */
531
+ .imqb2-seg {
532
+ position: relative; display: inline-grid; grid-auto-flow: column; grid-auto-columns: 1fr;
533
+ gap: 2px; padding: 4px; border-radius: var(--dsw-radius-md, 8px);
534
+ background: var(--dsw-alias-interactive-bg-hover);
535
+ }
536
+ .imqb2-seg-indicator {
537
+ position: absolute; top: 4px; left: 4px;
538
+ width: calc((100% - 8px - 2px * (var(--imqb2-seg-count) - 1)) / var(--imqb2-seg-count));
539
+ height: calc(100% - 8px);
540
+ border: 0; border-radius: var(--dsw-radius-sm, 6px);
541
+ background: var(--dsw-alias-bg-layer-1);
542
+ box-shadow: var(--dsw-elevation-soft, 0 1px 3px rgba(0,0,0,.08));
543
+ transform: translateX(calc(var(--imqb2-seg-index) * (100% + 2px)));
544
+ transition: transform 160ms ease;
545
+ pointer-events: none;
546
+ }
547
+ .imqb2-seg-tab {
548
+ box-sizing: border-box; position: relative; z-index: 1; height: 28px; padding: 0 16px;
549
+ border: 0; border-radius: var(--dsw-radius-sm, 6px); background: transparent;
550
+ color: var(--dsw-alias-label-secondary); font: inherit; font-size: 13px; line-height: 20px;
551
+ font-weight: 500; white-space: nowrap; cursor: pointer; transition: color 120ms ease;
552
+ }
553
+ .imqb2-seg-tab:hover, .imqb2-seg-tab[aria-selected='true'] { color: var(--dsw-alias-label-primary); }
554
+ .imqb2-seg-tab:focus-visible {
555
+ outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
556
+ outline-offset: -2px;
557
+ }
558
+ /* 底栏(官方 SettingsForm.footer 复刻) */
559
+ .imqb2-footer { display: flex; align-items: center; gap: 8px; padding-top: 16px; }
560
+ .imqb2-note { flex: 1; min-width: 0; margin: 0; font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-tertiary); }
561
+ .imqb2-note--error { color: var(--dsw-alias-state-error-primary, var(--dsw-alias-label-error)); }
562
+ .imqb2-note--ok { color: var(--dsw-alias-label-secondary); }
563
+ .imqb2-save {
564
+ appearance: none; flex: none; border: 1px solid transparent;
565
+ border-radius: var(--dsw-radius-md, 8px); padding: 5px 14px;
566
+ font: inherit; font-size: 13px; line-height: 1.5; cursor: pointer;
567
+ background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-layer-3);
568
+ }
569
+ .imqb2-save:disabled { opacity: 0.4; cursor: default; }
570
+ .imqb2-save:focus-visible {
571
+ outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
572
+ outline-offset: 1px;
573
+ }
574
+ /* cwd 只读行 */
575
+ .imqb2-readonly { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--dsw-alias-label-secondary); word-break: break-all; margin: 0; }
576
+ `;
577
+ document.head.appendChild(style);
578
+ return () => {
579
+ style.remove();
580
+ };
581
+ }, []);
582
+ }
583
+ /** 开关(官方 Switch 原语结构:aria-checked 驱动视觉态) */
584
+ function Switch({ checked, label, onChange }) {
585
+ return (0, react.createElement)("button", {
586
+ type: "button",
587
+ role: "switch",
588
+ "aria-checked": checked,
589
+ "aria-label": label,
590
+ className: "imqb2-switch",
591
+ onClick: () => {
592
+ onChange(!checked);
593
+ }
594
+ }, (0, react.createElement)("span", { className: "imqb2-switch-thumb" }));
595
+ }
596
+ /** 访问模式分段选择(官方 SegmentedControl 结构:CSS 变量定位滑动指示器) */
597
+ function Segmented({ value, options, onChange, label }) {
598
+ const index = Math.max(0, options.findIndex((o) => o.value === value));
599
+ const trackStyle = {
600
+ "--imqb2-seg-count": options.length,
601
+ "--imqb2-seg-index": index
602
+ };
603
+ return (0, react.createElement)("div", {
604
+ className: "imqb2-seg",
605
+ role: "tablist",
606
+ "aria-label": label,
607
+ style: trackStyle
608
+ }, (0, react.createElement)("span", { className: "imqb2-seg-indicator" }), options.map((o) => (0, react.createElement)("button", {
609
+ key: o.value,
610
+ type: "button",
611
+ role: "tab",
612
+ "aria-selected": o.value === value,
613
+ className: "imqb2-seg-tab",
614
+ onClick: () => {
615
+ onChange(o.value);
616
+ }
617
+ }, o.label)));
618
+ }
619
+ /** 开关行(label 左 + Switch 右 + 可选 help) */
620
+ function SwitchRow({ label, help, checked, onChange }) {
621
+ return (0, react.createElement)("div", { className: "imqb2-field" }, (0, react.createElement)("div", { className: "imqb2-head" }, (0, react.createElement)("span", { className: "imqb2-label" }, label), (0, react.createElement)(Switch, {
622
+ checked,
623
+ label,
624
+ onChange
625
+ })), help === void 0 ? null : (0, react.createElement)("p", { className: "imqb2-help" }, help));
626
+ }
627
+ /** 文本行(label 上 + 输入下,与官方 fields 布局一致) */
628
+ function FieldRow({ label, help, value, placeholder, onChange }) {
629
+ return (0, react.createElement)("div", { className: "imqb2-field" }, (0, react.createElement)("span", { className: "imqb2-label" }, label), (0, react.createElement)("input", {
630
+ className: "imqb2-input",
631
+ value,
632
+ placeholder,
633
+ spellCheck: false,
634
+ onChange: (event) => {
635
+ onChange(event.currentTarget.value);
636
+ }
637
+ }), help === void 0 ? null : (0, react.createElement)("p", { className: "imqb2-hint" }, help));
638
+ }
639
+ /** 多行文本行 */
640
+ function AreaRow({ label, help, value, rows, onChange }) {
641
+ return (0, react.createElement)("div", { className: "imqb2-field" }, (0, react.createElement)("span", { className: "imqb2-label" }, label), (0, react.createElement)("textarea", {
642
+ className: "imqb2-input imqb2-textarea",
643
+ value,
644
+ rows: rows ?? 3,
645
+ onChange: (event) => {
646
+ onChange(event.currentTarget.value);
647
+ }
648
+ }), help === void 0 ? null : (0, react.createElement)("p", { className: "imqb2-hint" }, help));
649
+ }
650
+ /** 访问模式分段选择行 */
651
+ function SegmentedRow({ label, help, value, options, onChange }) {
652
+ return (0, react.createElement)("div", { className: "imqb2-field" }, (0, react.createElement)("div", { className: "imqb2-head" }, (0, react.createElement)("span", { className: "imqb2-label" }, label), (0, react.createElement)(Segmented, {
653
+ value,
654
+ options,
655
+ onChange,
656
+ label
657
+ })), help === void 0 ? null : (0, react.createElement)("p", { className: "imqb2-hint" }, help));
658
+ }
659
+ const MODE_OPTIONS = [
660
+ {
661
+ value: "open",
662
+ label: "开放"
663
+ },
664
+ {
665
+ value: "allowlist",
666
+ label: "白名单"
667
+ },
668
+ {
669
+ value: "disabled",
670
+ label: "停用"
671
+ }
672
+ ];
673
+ /** 深比较(表单值与初始回显值的 dirty 判断) */
674
+ function shallowEqual(a, b) {
675
+ return JSON.stringify(a) === JSON.stringify(b);
676
+ }
677
+ /**
678
+ * 配置面板主体。
679
+ *
680
+ * 保存仅提交变更键(服务端按白名单合并),保存成功后提示
681
+ * HMR 重载语义(进行中的机器人会话将重置)。
682
+ */
683
+ function ConfigPanel() {
684
+ useConfigPanelStyles();
685
+ const [initial, setInitial] = (0, react.useState)(null);
686
+ const [form, setForm] = (0, react.useState)(null);
687
+ const [busy, setBusy] = (0, react.useState)(false);
688
+ const [result, setResult] = (0, react.useState)(null);
689
+ const [loadError, setLoadError] = (0, react.useState)(null);
690
+ (0, react.useEffect)(() => {
691
+ let stopped = false;
692
+ fetch(BIND_CONFIG_PATH, { cache: "no-store" }).then(async (res) => {
693
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
694
+ const body = await res.json();
695
+ if (stopped) return;
696
+ setInitial(body.config);
697
+ setForm(body.config);
698
+ }).catch((err) => {
699
+ if (!stopped) setLoadError(err instanceof Error ? err.message : String(err));
700
+ });
701
+ return () => {
702
+ stopped = true;
703
+ };
704
+ }, []);
705
+ const patch = (0, react.useCallback)((key, value) => {
706
+ setForm((current) => current === null ? current : {
707
+ ...current,
708
+ [key]: value
709
+ });
710
+ setResult(null);
711
+ }, []);
712
+ const submit = (0, react.useCallback)((event) => {
713
+ event.preventDefault();
714
+ if (busy || initial === null || form === null) return;
715
+ const changed = {};
716
+ for (const key of Object.keys(form)) if (!shallowEqual(form[key], initial[key])) changed[key] = form[key];
717
+ if (Object.keys(changed).length === 0) return;
718
+ setBusy(true);
719
+ fetch(BIND_CONFIG_PATH, {
720
+ method: "POST",
721
+ headers: { "content-type": "application/json" },
722
+ body: JSON.stringify(changed)
723
+ }).then(async (res) => {
724
+ const body = await res.json().catch(() => ({}));
725
+ if (res.ok && body.ok === true) {
726
+ setResult({
727
+ ok: true,
728
+ message: body.message ?? "配置已保存,插件将自动重载"
729
+ });
730
+ setInitial(form);
731
+ return;
732
+ }
733
+ setResult({
734
+ ok: false,
735
+ message: body.error ?? `保存失败(HTTP ${res.status})`
736
+ });
737
+ }).catch((err) => {
738
+ setResult({
739
+ ok: false,
740
+ message: err instanceof Error ? err.message : String(err)
741
+ });
742
+ }).finally(() => {
743
+ setBusy(false);
744
+ });
745
+ }, [
746
+ busy,
747
+ form,
748
+ initial
749
+ ]);
750
+ if (loadError !== null) return (0, react.createElement)("p", {
751
+ className: "imqb2-hint",
752
+ style: { padding: "12px 0" }
753
+ }, `配置加载失败:${loadError}`);
754
+ if (initial === null || form === null) return (0, react.createElement)("p", {
755
+ className: "imqb2-hint",
756
+ style: { padding: "12px 0" }
757
+ }, "正在加载配置…");
758
+ const dirty = Object.keys(form).some((key) => !shallowEqual(form[key], initial[key]));
759
+ const access = form.access ?? {
760
+ c2cMode: "open",
761
+ c2cAllow: [],
762
+ groupMode: "open",
763
+ groupAllow: []
764
+ };
765
+ return (0, react.createElement)("form", {
766
+ className: "imqb2-form",
767
+ onSubmit: submit
768
+ }, (0, react.createElement)("div", { className: "imqb2-group-title" }, "访问控制"), (0, react.createElement)(SegmentedRow, {
769
+ label: "私聊",
770
+ value: access.c2cMode,
771
+ options: MODE_OPTIONS,
772
+ onChange: (v) => {
773
+ patch("access", {
774
+ ...access,
775
+ c2cMode: v
776
+ });
777
+ }
778
+ }), access.c2cMode === "allowlist" ? (0, react.createElement)(AreaRow, {
779
+ label: "私聊白名单",
780
+ help: "每行一个 openid",
781
+ rows: 3,
782
+ value: access.c2cAllow.join("\n"),
783
+ onChange: (v) => {
784
+ patch("access", {
785
+ ...access,
786
+ c2cAllow: v.split("\n").map((s) => s.trim()).filter(Boolean)
787
+ });
788
+ }
789
+ }) : null, (0, react.createElement)(SegmentedRow, {
790
+ label: "群聊",
791
+ value: access.groupMode,
792
+ options: MODE_OPTIONS,
793
+ onChange: (v) => {
794
+ patch("access", {
795
+ ...access,
796
+ groupMode: v
797
+ });
798
+ }
799
+ }), access.groupMode === "allowlist" ? (0, react.createElement)(AreaRow, {
800
+ label: "群聊白名单",
801
+ help: "每行一个群 openid",
802
+ rows: 3,
803
+ value: access.groupAllow.join("\n"),
804
+ onChange: (v) => {
805
+ patch("access", {
806
+ ...access,
807
+ groupAllow: v.split("\n").map((s) => s.trim()).filter(Boolean)
808
+ });
809
+ }
810
+ }) : null, (0, react.createElement)("div", { className: "imqb2-group-title" }, "基本"), (0, react.createElement)(SwitchRow, {
811
+ label: "群聊需要 @机器人",
812
+ checked: form.requireMention ?? true,
813
+ help: "开启后群聊中只有 @机器人 的消息会触发回复",
814
+ onChange: (v) => {
815
+ patch("requireMention", v);
816
+ }
817
+ }), (0, react.createElement)(SwitchRow, {
818
+ label: "流式输出",
819
+ checked: form.streaming ?? true,
820
+ help: "回复内容分段实时推送到聊天",
821
+ onChange: (v) => {
822
+ patch("streaming", v);
823
+ }
824
+ }), (0, react.createElement)(SwitchRow, {
825
+ label: "显示工具调用结果",
826
+ checked: form.showToolResults ?? false,
827
+ help: "在聊天中展示 agent 工具调用的结构化结果",
828
+ onChange: (v) => {
829
+ patch("showToolResults", v);
830
+ }
831
+ }), (0, react.createElement)("div", { className: "imqb2-group-title" }, "模型"), (0, react.createElement)(FieldRow, {
832
+ label: "Provider",
833
+ value: form.provider ?? "",
834
+ placeholder: "留空继承宿主默认",
835
+ onChange: (v) => {
836
+ patch("provider", v);
837
+ }
838
+ }), (0, react.createElement)(FieldRow, {
839
+ label: "Model",
840
+ value: form.model ?? "",
841
+ placeholder: "留空继承宿主默认",
842
+ onChange: (v) => {
843
+ patch("model", v);
844
+ }
845
+ }), (0, react.createElement)(FieldRow, {
846
+ label: "Preset",
847
+ value: form.preset ?? "",
848
+ placeholder: "Agent preset id",
849
+ onChange: (v) => {
850
+ patch("preset", v);
851
+ }
852
+ }), (0, react.createElement)("div", { className: "imqb2-group-title" }, "补充指令"), (0, react.createElement)(AreaRow, {
853
+ label: "群聊额外 system prompt",
854
+ value: form.groupPrompt ?? "",
855
+ onChange: (v) => {
856
+ patch("groupPrompt", v);
857
+ }
858
+ }), (0, react.createElement)(AreaRow, {
859
+ label: "私聊额外 system prompt",
860
+ value: form.directPrompt ?? "",
861
+ onChange: (v) => {
862
+ patch("directPrompt", v);
863
+ }
864
+ }), form.cwd !== void 0 ? (0, react.createElement)("div", { className: "imqb2-field" }, (0, react.createElement)("div", { className: "imqb2-head" }, (0, react.createElement)("span", { className: "imqb2-label" }, "Agent 工作目录"), (0, react.createElement)("span", { className: "imqb2-hint" }, "只读")), (0, react.createElement)("p", { className: "imqb2-readonly" }, form.cwd)) : null, (0, react.createElement)("div", { className: "imqb2-footer" }, result !== null ? (0, react.createElement)("p", { className: `imqb2-note${result.ok ? " imqb2-note--ok" : " imqb2-note--error"}` }, (result.ok ? "" : "⚠️ ") + result.message) : dirty ? (0, react.createElement)("p", { className: "imqb2-note" }, "保存后插件将重载,进行中的机器人会话将重置") : (0, react.createElement)("p", { className: "imqb2-note" }), (0, react.createElement)("button", {
865
+ className: "imqb2-save",
866
+ type: "submit",
867
+ disabled: busy || !dirty
868
+ }, busy ? "正在保存…" : "保存")));
869
+ }
870
+ //#endregion
871
+ //#region src/client/index.ts
872
+ const name = "im-qqbot-client";
873
+ const inject = [
874
+ "slots",
875
+ "sidebarRight",
876
+ "sidebarRightTabs"
877
+ ];
878
+ /** 侧栏 Tab 类型(ui-sidebar-right 契约的局部投影) */
879
+ const TAB_KIND = "qqbot-bind";
880
+ /** 席位注册 key(Tab 类型 id,席位按其分发;只需全局唯一,与包名解耦) */
881
+ const TAB_ID = "im-qqbot/bind";
882
+ /** 绑定入口图标(QQ 机器人,跟随主题图标色变量) */
883
+ function BindGuideIcon() {
884
+ return (0, react.createElement)("svg", {
885
+ width: 24,
886
+ height: 24,
887
+ viewBox: "0 0 24 24",
888
+ fill: "none",
889
+ "aria-hidden": true
890
+ }, (0, react.createElement)("path", {
891
+ d: "M9.50039 11.1504C9.96983 11.1504 10.3504 11.5309 10.3504 12.0004V14.0004C10.3504 14.4698 9.96983 14.8504 9.50039 14.8504C9.03095 14.8504 8.65039 14.4698 8.65039 14.0004V12.0004C8.65039 11.5309 9.03095 11.1504 9.50039 11.1504Z",
892
+ fill: "var(------icon_primary, #1A1A1A)"
893
+ }), (0, react.createElement)("path", {
894
+ d: "M14.5004 11.1504C14.9698 11.1504 15.3504 11.5309 15.3504 12.0004V14.0004C15.3504 14.4698 14.9698 14.8504 14.5004 14.8504C14.0309 14.8504 13.6504 14.4698 13.6504 14.0004V12.0004C13.6504 11.5309 14.0309 11.1504 14.5004 11.1504Z",
895
+ fill: "var(------icon_primary, #1A1A1A)"
896
+ }), (0, react.createElement)("path", {
897
+ fillRule: "evenodd",
898
+ clipRule: "evenodd",
899
+ d: "M12.8504 5.3109C13.5299 4.99138 14.0004 4.30063 14.0004 3.5C14.0004 2.39543 13.1049 1.5 12.0004 1.5C10.8958 1.5 10.0004 2.39543 10.0004 3.5C10.0004 4.30065 10.4708 4.99141 11.1504 5.31092V6.18851C6.38487 6.61795 2.65039 10.6231 2.65039 15.5004V17.0004C2.65039 19.679 4.82181 21.8504 7.50039 21.8504H16.5004C19.179 21.8504 21.3504 19.679 21.3504 17.0004V15.5004C21.3504 10.6231 17.6159 6.61795 12.8504 6.18851V5.3109ZM12.0004 7.85039C7.77541 7.85039 4.35039 11.2754 4.35039 15.5004V17.0004C4.35039 18.7401 5.76069 20.1504 7.50039 20.1504H16.5004C18.2401 20.1504 19.6504 18.7401 19.6504 17.0004V15.5004C19.6504 11.2754 16.2254 7.85039 12.0004 7.85039Z",
900
+ fill: "var(------icon_primary, #1A1A1A)"
901
+ }));
902
+ }
903
+ /** 设置页「QQ Bot」栏目:面板常驻展示绑定状态 */
904
+ function QQBotSettingsSection() {
905
+ return (0, react.createElement)("div", null, (0, react.createElement)(BindPanelBody), (0, react.createElement)(ConfigPanel));
906
+ }
907
+ function apply(ctx) {
908
+ const tabs = ctx.sidebarRightTabs;
909
+ const right = ctx.sidebarRight;
910
+ const slots = ctx.slots;
911
+ const disposers = [];
912
+ if (slots !== void 0) disposers.push(slots.inject("settings.section", () => slots.register({
913
+ name: "settings.section",
914
+ id: "qqbot",
915
+ order: 7,
916
+ label: "QQ 机器人"
917
+ }, QQBotSettingsSection)));
918
+ if (tabs !== void 0 && slots !== void 0) {
919
+ disposers.push(tabs.register({
920
+ id: TAB_ID,
921
+ kind: TAB_KIND,
922
+ title: () => "QQ 机器人绑定",
923
+ guide: [{
924
+ id: "bind",
925
+ order: 90,
926
+ title: () => "QQ 机器人绑定",
927
+ description: () => "扫码或手动填写,绑定 QQ 机器人",
928
+ icon: BindGuideIcon
929
+ }]
930
+ }));
931
+ disposers.push(slots.inject("sidebar.right.pane.tab", () => slots.register({
932
+ name: "sidebar.right.pane.tab",
933
+ key: TAB_ID
934
+ }, BindPanelBody)));
935
+ disposers.push(watchAndPrompt(right));
936
+ }
937
+ ctx.effect(() => () => {
938
+ for (const dispose of disposers.reverse()) dispose();
939
+ }, "im-qqbot-client.binding");
940
+ }
941
+ /**
942
+ * 轮询绑定状态:待绑定时自动打开绑定页。
943
+ *
944
+ * - 页面隐藏时降频轮询(可见时立即恢复节拍)
945
+ * - 404(已绑定/未安装)时退避到 30s,保持低成本常驻探测
946
+ * - 每次页面加载至多打开一次(用户手动关闭后不打扰);
947
+ * 打开失败(如启动早期 Session surface 尚未挂载)不消费机会,
948
+ * 由下一轮轮询(3s)自动重试
949
+ */
950
+ function watchAndPrompt(right) {
951
+ let stopped = false;
952
+ let opened = right === void 0;
953
+ let timer;
954
+ const schedule = (ms) => {
955
+ timer = setTimeout(() => {
956
+ tick();
957
+ }, ms);
958
+ };
959
+ const tick = async () => {
960
+ if (stopped) return;
961
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
962
+ schedule(5e3);
963
+ return;
964
+ }
965
+ try {
966
+ const res = await fetch(BIND_STATE_PATH, { cache: "no-store" });
967
+ if (res.ok) {
968
+ if ((await res.json()).phase === "waiting" && !opened && right !== void 0) try {
969
+ right.openTab(TAB_KIND);
970
+ opened = true;
971
+ } catch (err) {
972
+ console.error("[im-qqbot] 打开绑定页失败,稍后重试", err);
973
+ }
974
+ schedule(3e3);
975
+ return;
976
+ }
977
+ schedule(3e4);
978
+ return;
979
+ } catch {}
980
+ schedule(1e4);
981
+ };
982
+ tick();
983
+ return () => {
984
+ stopped = true;
985
+ if (timer !== void 0) clearTimeout(timer);
986
+ };
987
+ }
988
+ //#endregion
989
+ exports.apply = apply;
990
+ exports.inject = inject;
991
+ exports.name = name;
992
+ return module.exports;
993
+ }
994
+ });
995
+
996
+ //# sourceMappingURL=client.js.map