@cxyhhhhh/dsh-qqbot 0.5.0-dev.202610101632 → 0.5.0-dev.202610101921

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -5,81 +5,148 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
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
8
28
  //#region src/client/bind-page.ts
9
29
  /**
10
- * 端内绑定面板 — 二维码展示与状态轮询
30
+ * 端内绑定面板 — 扫码 / 手动填写 / 已绑定编辑
31
+ *
32
+ * 宿主插件注册 /qqbot/im-qqbot/bind 路由(待绑定与已绑定 fiber 均常驻):
33
+ * - 未绑定:waiting(二维码)/ starting / saving / failed 相位 + manual 提交
34
+ * - 已绑定:bound 相位(回显脱敏 appId,secret 不回传)+ manual 更换 + 解绑
35
+ * 扫码成功或手动保存均写入 profile patch 并经 HMR 重载生效。
11
36
  *
12
- * 宿主插件在「待绑定」时注册 /qqbot/im-qqbot/bind 状态路由,本组件
13
- * 轮询该路由渲染二维码与实时状态;扫码成功后宿主写入 profile 并经
14
- * HMR 重载,路由随之消失(404),页面显示完成态。
37
+ * 样式为注入式 CSS 类(imqb-* 命名空间,随面板挂载注入一次):
38
+ * hover/过渡/focus 样式无法用 inline style 表达,统一走类;
39
+ * 颜色尽量取 DSW 主题变量(--dsw-alias-*),缺省值兜底。
15
40
  *
16
41
  * 同一组件复用于两种展示位(侧栏绑定页 / 设置页 QQ Bot 栏目)。
17
42
  * 运行时仅依赖 react(dsh 客户端平台种子表提供);宿主通信走同源 fetch,
18
43
  * Desktop 下由壳的 IPC 桥自动附加 Host 认证转发。
19
44
  */
20
- /** 状态查询路由(宿主 bind-routes 注册) */
21
- const BIND_STATE_URL = "/qqbot/im-qqbot/bind";
22
- /** 刷新路由 */
23
- const BIND_REFRESH_URL = "/qqbot/im-qqbot/bind/refresh";
24
45
  /** 轮询间隔(毫秒):活跃轮询 / 失败退避 */
25
46
  const ACTIVE_POLL_MS = 2e3;
26
47
  const RETRY_POLL_MS = 3e3;
27
- /** 面板容器(设置页 / 侧栏通用) */
28
- const CONTAINER = {
29
- display: "flex",
30
- flexDirection: "column",
31
- alignItems: "center",
32
- justifyContent: "center",
33
- gap: "14px",
34
- padding: "24px",
35
- minHeight: "320px",
36
- textAlign: "center",
37
- fontSize: "14px"
38
- };
39
- const QR_IMAGE = {
40
- borderRadius: "8px",
41
- background: "#fff",
42
- padding: "10px",
43
- maxWidth: "100%"
44
- };
45
- const TITLE = {
46
- margin: 0,
47
- fontSize: "16px",
48
- fontWeight: 600
49
- };
50
- const HINT = {
51
- margin: 0,
52
- opacity: .75,
53
- lineHeight: 1.6
54
- };
55
- const LINK = {
56
- fontSize: "13px",
57
- opacity: .85
58
- };
59
- const BUTTON = {
60
- padding: "6px 16px",
61
- borderRadius: "6px",
62
- border: "1px solid currentColor",
63
- background: "transparent",
64
- color: "inherit",
65
- cursor: "pointer",
66
- fontSize: "13px"
67
- };
68
- const STATUS_ICON = {
69
- fontSize: "32px",
70
- lineHeight: 1
71
- };
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
+ document.head.appendChild(style);
128
+ return () => {
129
+ style.remove();
130
+ };
131
+ }, []);
132
+ }
72
133
  /**
73
134
  * 绑定状态轮询 hook。
74
135
  *
136
+ * 404 语义分两种:从未拿到过路由 = 插件未启用(终态);
137
+ * 曾经拿到过 = HMR 重载窗口期(manual/unbind 保存后路由短暂消失),
138
+ * 低频等待恢复而不是判定终态——否则保存成功提示会被
139
+ * 「插件未启用」覆盖且无法自愈。
140
+ *
75
141
  * @returns 轮询快照:state 为宿主相位(null = 加载中),gone 表示
76
- * 路由已消失(已绑定并重载,或插件未启用)。
142
+ * 插件未启用(从未注册过路由)。
77
143
  */
78
144
  function useBindState() {
79
145
  const [state, setState] = (0, react.useState)(null);
80
146
  const [gone, setGone] = (0, react.useState)(false);
81
147
  (0, react.useEffect)(() => {
82
148
  let stopped = false;
149
+ let sawRoute = false;
83
150
  let timer;
84
151
  const schedule = (ms) => {
85
152
  timer = setTimeout(() => {
@@ -89,19 +156,21 @@ window.__ModuleLoader__.load({
89
156
  const tick = async () => {
90
157
  if (stopped) return;
91
158
  try {
92
- const res = await fetch(BIND_STATE_URL, { cache: "no-store" });
159
+ const res = await fetch(BIND_STATE_PATH, { cache: "no-store" });
93
160
  if (res.status === 404) {
94
- if (!stopped) setGone(true);
161
+ if (!stopped && !sawRoute) setGone(true);
162
+ if (!stopped && sawRoute) schedule(1e4);
95
163
  return;
96
164
  }
97
165
  if (!res.ok) {
98
166
  schedule(RETRY_POLL_MS);
99
167
  return;
100
168
  }
169
+ sawRoute = true;
101
170
  const body = await res.json();
102
171
  if (stopped) return;
103
172
  setState(body);
104
- schedule(body.phase === "saving" ? 500 : ACTIVE_POLL_MS);
173
+ schedule(body.phase === "saving" ? 500 : body.phase === "bound" ? 3e4 : ACTIVE_POLL_MS);
105
174
  return;
106
175
  } catch {}
107
176
  schedule(RETRY_POLL_MS);
@@ -117,42 +186,610 @@ window.__ModuleLoader__.load({
117
186
  gone
118
187
  };
119
188
  }
120
- /** 请求宿主刷新二维码任务 */
121
- async function refreshBindTask() {
189
+ /** 手动提交凭据(填写 / 更换共用) */
190
+ async function submitManualCredentials(appId, appSecret) {
122
191
  try {
123
- await fetch(BIND_REFRESH_URL, { method: "POST" });
192
+ const res = await fetch(BIND_MANUAL_PATH, {
193
+ method: "POST",
194
+ headers: { "content-type": "application/json" },
195
+ body: JSON.stringify({
196
+ appId,
197
+ appSecret
198
+ })
199
+ });
200
+ const body = await res.json().catch(() => ({}));
201
+ if (res.ok && body.ok) return {
202
+ ok: true,
203
+ message: body.message
204
+ };
205
+ return {
206
+ ok: false,
207
+ error: body.error ?? `保存失败(HTTP ${res.status})`
208
+ };
124
209
  } catch (err) {
125
- console.error("[im-qqbot] 刷新二维码失败", err);
210
+ return {
211
+ ok: false,
212
+ error: err instanceof Error ? err.message : String(err)
213
+ };
126
214
  }
127
215
  }
128
- /** 依据宿主相位渲染面板主体(各展示位共用) */
129
- function BindPanelBody() {
130
- const { state, gone } = useBindState();
216
+ /**
217
+ * 手动凭据表单(填写 / 更换共用)。
218
+ *
219
+ * appSecret 永不回显(占位符提示),提交成功后展示保存结果;
220
+ * 宿主写入 profile patch 并经 HMR 重载,状态轮询随后收敛为 bound。
221
+ */
222
+ function ManualForm({ initialAppId }) {
223
+ const [appId, setAppId] = (0, react.useState)(initialAppId ?? "");
224
+ const [appSecret, setAppSecret] = (0, react.useState)("");
225
+ const [busy, setBusy] = (0, react.useState)(false);
226
+ const [result, setResult] = (0, react.useState)(null);
227
+ const submit = (0, react.useCallback)((event) => {
228
+ event.preventDefault();
229
+ if (busy) return;
230
+ const trimmedId = appId.trim();
231
+ const trimmedSecret = appSecret.trim();
232
+ if (trimmedId.length === 0 || trimmedSecret.length === 0) {
233
+ setResult({
234
+ ok: false,
235
+ error: "appId 与 appSecret 均为必填"
236
+ });
237
+ return;
238
+ }
239
+ setBusy(true);
240
+ submitManualCredentials(trimmedId, trimmedSecret).then((outcome) => {
241
+ setResult(outcome);
242
+ setBusy(false);
243
+ });
244
+ }, [
245
+ appId,
246
+ appSecret,
247
+ busy
248
+ ]);
249
+ 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" }, "插件将自动重载;稍后即可开始对话"))));
250
+ return (0, react.createElement)("form", {
251
+ className: "imqb-form",
252
+ onSubmit: submit
253
+ }, (0, react.createElement)("label", { className: "imqb-field" }, "AppID", (0, react.createElement)("input", {
254
+ className: "imqb-input",
255
+ value: appId,
256
+ placeholder: "QQ 机器人 AppID",
257
+ onChange: (event) => {
258
+ setAppId(event.currentTarget.value);
259
+ },
260
+ autoComplete: "off",
261
+ spellCheck: false
262
+ })), (0, react.createElement)("label", { className: "imqb-field" }, "AppSecret", (0, react.createElement)("input", {
263
+ className: "imqb-input",
264
+ type: "password",
265
+ value: appSecret,
266
+ placeholder: initialAppId === void 0 ? "QQ 机器人 AppSecret" : "输入新的 AppSecret(不回显)",
267
+ onChange: (event) => {
268
+ setAppSecret(event.currentTarget.value);
269
+ },
270
+ autoComplete: "new-password"
271
+ })), result !== null && !result.ok ? (0, react.createElement)("p", { className: "imqb-error" }, result.error) : null, (0, react.createElement)("button", {
272
+ className: "imqb-btn imqb-btn--primary",
273
+ type: "submit",
274
+ disabled: busy
275
+ }, busy ? "正在保存…" : "保存"));
276
+ }
277
+ /** QQ 企鹅 logo(256×256 PNG data URI;深色图案配白底圆角,明暗主题下均可见) */
278
+ const QQ_BOT_ICON_PNG = "data:image/png;base64,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";
279
+ /** QQ 机器人图标(logo 图片,白底圆角容器内呈现) */
280
+ function QqBotIcon({ size = 28 }) {
281
+ return (0, react.createElement)("img", {
282
+ src: QQ_BOT_ICON_PNG,
283
+ alt: "QQ",
284
+ width: size,
285
+ height: size,
286
+ className: "imqb-qqicon"
287
+ });
288
+ }
289
+ /** 已绑定视图:回显 appId + 更换凭据 / 解绑入口 */
290
+ function BoundView({ appId }) {
291
+ const [editing, setEditing] = (0, react.useState)(false);
292
+ const [unbindResult, setUnbindResult] = (0, react.useState)(null);
293
+ const unbind = (0, react.useCallback)(() => {
294
+ if (!window.confirm("确定解绑吗?\n解绑后机器人立即下线,插件将重新进入扫码绑定流程。")) return;
295
+ fetch(BIND_UNBIND_PATH, { method: "POST" }).then(async (res) => {
296
+ const body = await res.json().catch(() => ({}));
297
+ if (res.ok && body.ok === true) {
298
+ setUnbindResult({
299
+ ok: true,
300
+ message: body.message ?? "已解绑,插件将重载并重新进入绑定流程"
301
+ });
302
+ return;
303
+ }
304
+ setUnbindResult({
305
+ ok: false,
306
+ message: body.error ?? `解绑失败(HTTP ${res.status})`
307
+ });
308
+ }).catch((err) => {
309
+ setUnbindResult({
310
+ ok: false,
311
+ message: err instanceof Error ? err.message : String(err)
312
+ });
313
+ });
314
+ }, []);
315
+ 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))));
316
+ 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", {
317
+ className: "imqb-ghost",
318
+ onClick: () => {
319
+ setEditing(false);
320
+ }
321
+ }, "返回")), (0, react.createElement)(ManualForm, { initialAppId: appId }));
322
+ 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", {
323
+ className: "imqb-btn imqb-btn--primary",
324
+ onClick: () => {
325
+ setEditing(true);
326
+ }
327
+ }, "更换凭据"), (0, react.createElement)("button", {
328
+ className: "imqb-btn imqb-btn--danger",
329
+ onClick: unbind
330
+ }, "解绑")));
331
+ }
332
+ /** 未绑定视图:扫码 / 手动填写切换 */
333
+ function UnboundView({ state }) {
334
+ const [mode, setMode] = (0, react.useState)("qr");
131
335
  const refresh = (0, react.useCallback)(() => {
132
- refreshBindTask();
336
+ fetch(BIND_REFRESH_PATH, { method: "POST" }).catch(() => {});
133
337
  }, []);
134
- if (gone) return (0, react.createElement)("div", { style: CONTAINER }, (0, react.createElement)("div", { style: STATUS_ICON }, "✅"), (0, react.createElement)("p", { style: HINT }, "绑定已完成,或插件未启用。"));
135
- if (state === null || state.phase === "starting") return (0, react.createElement)("div", { style: CONTAINER }, (0, react.createElement)("p", { style: HINT }, "正在生成二维码…"));
136
- if (state.phase === "saving") return (0, react.createElement)("div", { style: CONTAINER }, (0, react.createElement)("div", { style: STATUS_ICON }, "🎉"), (0, react.createElement)("p", { style: TITLE }, "扫码成功"), (0, react.createElement)("p", { style: HINT }, "凭据已保存,插件正在重载,稍后即可开始对话。"));
137
- if (state.phase === "failed") return (0, react.createElement)("div", { style: CONTAINER }, (0, react.createElement)("div", { style: STATUS_ICON }, "⚠️"), (0, react.createElement)("p", { style: TITLE }, "绑定失败"), (0, react.createElement)("p", { style: HINT }, state.reason), (0, react.createElement)("button", {
138
- style: BUTTON,
139
- onClick: refresh
140
- }, "重试"));
141
- return (0, react.createElement)("div", { style: CONTAINER }, state.qrSvg === void 0 ? null : (0, react.createElement)("img", {
338
+ 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", {
339
+ className: "imqb-ghost",
340
+ onClick: () => {
341
+ setMode("qr");
342
+ }
343
+ }, "改用扫码绑定"));
344
+ return (0, react.createElement)("div", { className: "imqb-panel" }, state.qrSvg === void 0 ? null : (0, react.createElement)("img", {
142
345
  src: state.qrSvg,
143
346
  alt: "QQ 扫码二维码",
144
347
  width: 220,
145
348
  height: 220,
146
- style: QR_IMAGE
147
- }), (0, react.createElement)("p", { style: TITLE }, "QQ Bot 绑定"), (0, react.createElement)("p", { style: HINT }, "请使用手机 QQ 扫描上方二维码,完成机器人绑定"), (0, react.createElement)("a", {
349
+ className: "imqb-qr"
350
+ }), (0, react.createElement)("p", { className: "imqb-title" }, "扫码绑定 QQ 机器人"), (0, react.createElement)("p", { className: "imqb-hint" }, "使用手机 QQ 扫描二维码完成绑定"), (0, react.createElement)("a", {
148
351
  href: state.url,
149
352
  target: "_blank",
150
353
  rel: "noreferrer noopener",
151
- style: LINK
152
- }, "在浏览器中打开扫码页"), (0, react.createElement)("button", {
153
- style: BUTTON,
354
+ className: "imqb-link"
355
+ }, "二维码显示异常?在浏览器中打开"), (0, react.createElement)("div", { className: "imqb-row" }, (0, react.createElement)("button", {
356
+ className: "imqb-pill",
154
357
  onClick: refresh
155
- }, "刷新二维码"));
358
+ }, "刷新二维码"), (0, react.createElement)("button", {
359
+ className: "imqb-pill",
360
+ onClick: () => {
361
+ setMode("manual");
362
+ }
363
+ }, "手动填写")));
364
+ }
365
+ /** 依据宿主相位渲染面板主体(各展示位共用) */
366
+ function BindPanelBody() {
367
+ usePanelStyles();
368
+ const { state, gone } = useBindState();
369
+ if (gone) return (0, react.createElement)("div", { className: "imqb-panel" }, (0, react.createElement)("div", { className: "imqb-icon" }, "❔"), (0, react.createElement)("p", { className: "imqb-hint" }, "插件未启用,或当前版本不支持端内配置"));
370
+ if (state === null || state.phase === "starting") return (0, react.createElement)("div", { className: "imqb-panel" }, (0, react.createElement)("p", { className: "imqb-hint" }, "正在获取绑定状态…"));
371
+ if (state.phase === "saving") 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" }, "凭据已保存,插件正在重载,稍后即可开始对话"))));
372
+ 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", {
373
+ className: "imqb-btn imqb-btn--primary",
374
+ onClick: () => {
375
+ window.location.reload();
376
+ }
377
+ }, "重试")));
378
+ if (state.phase === "bound") return (0, react.createElement)(BoundView, { appId: state.appId });
379
+ return (0, react.createElement)(UnboundView, { state });
380
+ }
381
+ //#endregion
382
+ //#region src/client/config-panel.ts
383
+ /**
384
+ * QQ 机器人配置面板 — 必要配置的 UI 编辑
385
+ *
386
+ * UI 复刻 DSW 通用设置(settings-form 原语)的视觉风格:
387
+ * 左对齐设置列、0.5px 行分隔、label 13px/500、控件右置;
388
+ * Switch 为 36×20 aria-checked 结构(thumb translateX 动画);
389
+ * 三值选择用分段控件(滑动指示器按段数/索引的 CSS 变量定位);
390
+ * 保存按钮为 DSW 反色主按钮(label-primary 底 + bg-layer-3 字)。
391
+ *
392
+ * 数据流:GET /qqbot/im-qqbot/config 回显当前生效配置(appSecret
393
+ * 已由宿主剔除)→ 表单编辑 → dirty diff → POST 仅提交变更键 →
394
+ * 宿主白名单 + Schema 校验 → 最小写入 profile patch → HMR 重载生效。
395
+ *
396
+ * 字段策略:只开放用户必要的配置;cwd 只读展示;超时与性能调优
397
+ * 类字段走 profile patch / 环境变量。
398
+ */
399
+ /** 样式注入标记(幂等) */
400
+ const STYLE_ID = "im-qqbot-config-panel-styles";
401
+ /** DSW 通用设置风格(settings-form / Switch / SegmentedControl 原语复刻) */
402
+ function useConfigPanelStyles() {
403
+ (0, react.useEffect)(() => {
404
+ if (typeof document === "undefined") return;
405
+ if (document.getElementById(STYLE_ID) !== null) return;
406
+ const style = document.createElement("style");
407
+ style.id = STYLE_ID;
408
+ style.textContent = `
409
+ .imqb2-form { display: flex; flex-direction: column; width: 100%; }
410
+ .imqb2-group-title {
411
+ padding: 14px 0 2px; font-size: 12px; font-weight: 600; line-height: 18px;
412
+ color: var(--dsw-alias-label-tertiary); text-transform: uppercase; letter-spacing: .04em;
413
+ }
414
+ .imqb2-field { display: flex; flex-direction: column; gap: 6px; padding: 12px 0; }
415
+ .imqb2-field + .imqb2-field { border-top: 0.5px solid var(--dsw-alias-border-l2); }
416
+ .imqb2-head { display: flex; align-items: center; gap: 8px; min-height: 28px; }
417
+ .imqb2-label {
418
+ flex: 1; min-width: 0; font-size: 13px; font-weight: 500; line-height: 1.5;
419
+ color: var(--dsw-alias-label-primary);
420
+ }
421
+ .imqb2-help { margin: 0; font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); }
422
+ .imqb2-hint { margin: 0; font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-tertiary); }
423
+ /* Switch(aria-checked 驱动,视觉态与无障碍态同源) */
424
+ .imqb2-switch {
425
+ box-sizing: border-box; position: relative; flex: 0 0 auto;
426
+ width: 36px; height: 20px; padding: 2px; border: 0; border-radius: 999px;
427
+ background: var(--dsw-alias-border-l3); cursor: pointer; font: inherit;
428
+ }
429
+ .imqb2-switch[aria-checked='true'] { background: var(--dsw-alias-brand-primary); }
430
+ .imqb2-switch:disabled { cursor: default; opacity: 0.5; }
431
+ .imqb2-switch:focus-visible {
432
+ outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
433
+ outline-offset: 2px;
434
+ }
435
+ .imqb2-switch-thumb {
436
+ display: block; width: 16px; height: 16px; border-radius: 50%;
437
+ background: var(--dsw-alias-switch-thumb, #ffffff);
438
+ transition: transform 120ms ease;
439
+ }
440
+ .imqb2-switch[aria-checked='true'] .imqb2-switch-thumb {
441
+ background: var(--dsw-alias-label-primary-foreground, #ffffff);
442
+ transform: translateX(16px);
443
+ }
444
+ /* 输入(官方 fields.input 复刻) */
445
+ .imqb2-input {
446
+ box-sizing: border-box; width: 100%; height: 34px; padding: 0 12px;
447
+ border: 0.5px solid var(--dsw-alias-border-l4); border-radius: var(--dsw-radius-md, 8px);
448
+ background: var(--dsw-alias-bg-layer-3); font: inherit; font-size: 13px; line-height: 1.5;
449
+ color: var(--dsw-alias-label-primary); outline: none;
450
+ }
451
+ .imqb2-input:focus { outline: none; border-color: var(--dsw-alias-state-business-primary); }
452
+ .imqb2-input:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
453
+ .imqb2-textarea { height: auto; min-height: 68px; padding: 8px 12px; resize: vertical; line-height: 1.5; }
454
+ /* 分段控件(官方 SegmentedControl 复刻:指示器按段数/索引定位) */
455
+ .imqb2-seg {
456
+ position: relative; display: inline-grid; grid-auto-flow: column; grid-auto-columns: 1fr;
457
+ gap: 2px; padding: 4px; border-radius: var(--dsw-radius-md, 8px);
458
+ background: var(--dsw-alias-interactive-bg-hover);
459
+ }
460
+ .imqb2-seg-indicator {
461
+ position: absolute; top: 4px; left: 4px;
462
+ width: calc((100% - 8px - 2px * (var(--imqb2-seg-count) - 1)) / var(--imqb2-seg-count));
463
+ height: calc(100% - 8px);
464
+ border: 0; border-radius: var(--dsw-radius-sm, 6px);
465
+ background: var(--dsw-alias-bg-layer-1);
466
+ box-shadow: var(--dsw-elevation-soft, 0 1px 3px rgba(0,0,0,.08));
467
+ transform: translateX(calc(var(--imqb2-seg-index) * (100% + 2px)));
468
+ transition: transform 160ms ease;
469
+ pointer-events: none;
470
+ }
471
+ .imqb2-seg-tab {
472
+ box-sizing: border-box; position: relative; z-index: 1; height: 28px; padding: 0 16px;
473
+ border: 0; border-radius: var(--dsw-radius-sm, 6px); background: transparent;
474
+ color: var(--dsw-alias-label-secondary); font: inherit; font-size: 13px; line-height: 20px;
475
+ font-weight: 500; white-space: nowrap; cursor: pointer; transition: color 120ms ease;
476
+ }
477
+ .imqb2-seg-tab:hover, .imqb2-seg-tab[aria-selected='true'] { color: var(--dsw-alias-label-primary); }
478
+ .imqb2-seg-tab:focus-visible {
479
+ outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
480
+ outline-offset: -2px;
481
+ }
482
+ /* 底栏(官方 SettingsForm.footer 复刻) */
483
+ .imqb2-footer { display: flex; align-items: center; gap: 8px; padding-top: 16px; }
484
+ .imqb2-note { flex: 1; min-width: 0; margin: 0; font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-tertiary); }
485
+ .imqb2-note--error { color: var(--dsw-alias-state-error-primary, var(--dsw-alias-label-error)); }
486
+ .imqb2-note--ok { color: var(--dsw-alias-label-secondary); }
487
+ .imqb2-save {
488
+ appearance: none; flex: none; border: 1px solid transparent;
489
+ border-radius: var(--dsw-radius-md, 8px); padding: 5px 14px;
490
+ font: inherit; font-size: 13px; line-height: 1.5; cursor: pointer;
491
+ background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-layer-3);
492
+ }
493
+ .imqb2-save:disabled { opacity: 0.4; cursor: default; }
494
+ .imqb2-save:focus-visible {
495
+ outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
496
+ outline-offset: 1px;
497
+ }
498
+ /* cwd 只读行 */
499
+ .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; }
500
+ `;
501
+ document.head.appendChild(style);
502
+ return () => {
503
+ style.remove();
504
+ };
505
+ }, []);
506
+ }
507
+ /** 开关(官方 Switch 原语结构:aria-checked 驱动视觉态) */
508
+ function Switch({ checked, label, onChange }) {
509
+ return (0, react.createElement)("button", {
510
+ type: "button",
511
+ role: "switch",
512
+ "aria-checked": checked,
513
+ "aria-label": label,
514
+ className: "imqb2-switch",
515
+ onClick: () => {
516
+ onChange(!checked);
517
+ }
518
+ }, (0, react.createElement)("span", { className: "imqb2-switch-thumb" }));
519
+ }
520
+ /** 访问模式分段选择(官方 SegmentedControl 结构:CSS 变量定位滑动指示器) */
521
+ function Segmented({ value, options, onChange, label }) {
522
+ const index = Math.max(0, options.findIndex((o) => o.value === value));
523
+ const trackStyle = {
524
+ "--imqb2-seg-count": options.length,
525
+ "--imqb2-seg-index": index
526
+ };
527
+ return (0, react.createElement)("div", {
528
+ className: "imqb2-seg",
529
+ role: "tablist",
530
+ "aria-label": label,
531
+ style: trackStyle
532
+ }, (0, react.createElement)("span", { className: "imqb2-seg-indicator" }), options.map((o) => (0, react.createElement)("button", {
533
+ key: o.value,
534
+ type: "button",
535
+ role: "tab",
536
+ "aria-selected": o.value === value,
537
+ className: "imqb2-seg-tab",
538
+ onClick: () => {
539
+ onChange(o.value);
540
+ }
541
+ }, o.label)));
542
+ }
543
+ /** 开关行(label 左 + Switch 右 + 可选 help) */
544
+ function SwitchRow({ label, help, checked, onChange }) {
545
+ 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, {
546
+ checked,
547
+ label,
548
+ onChange
549
+ })), help === void 0 ? null : (0, react.createElement)("p", { className: "imqb2-help" }, help));
550
+ }
551
+ /** 文本行(label 上 + 输入下,与官方 fields 布局一致) */
552
+ function FieldRow({ label, help, value, placeholder, onChange }) {
553
+ return (0, react.createElement)("div", { className: "imqb2-field" }, (0, react.createElement)("span", { className: "imqb2-label" }, label), (0, react.createElement)("input", {
554
+ className: "imqb2-input",
555
+ value,
556
+ placeholder,
557
+ spellCheck: false,
558
+ onChange: (event) => {
559
+ onChange(event.currentTarget.value);
560
+ }
561
+ }), help === void 0 ? null : (0, react.createElement)("p", { className: "imqb2-hint" }, help));
562
+ }
563
+ /** 多行文本行 */
564
+ function AreaRow({ label, help, value, rows, onChange }) {
565
+ return (0, react.createElement)("div", { className: "imqb2-field" }, (0, react.createElement)("span", { className: "imqb2-label" }, label), (0, react.createElement)("textarea", {
566
+ className: "imqb2-input imqb2-textarea",
567
+ value,
568
+ rows: rows ?? 3,
569
+ onChange: (event) => {
570
+ onChange(event.currentTarget.value);
571
+ }
572
+ }), help === void 0 ? null : (0, react.createElement)("p", { className: "imqb2-hint" }, help));
573
+ }
574
+ /** 访问模式分段选择行 */
575
+ function SegmentedRow({ label, help, value, options, onChange }) {
576
+ 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, {
577
+ value,
578
+ options,
579
+ onChange,
580
+ label
581
+ })), help === void 0 ? null : (0, react.createElement)("p", { className: "imqb2-hint" }, help));
582
+ }
583
+ const MODE_OPTIONS = [
584
+ {
585
+ value: "open",
586
+ label: "开放"
587
+ },
588
+ {
589
+ value: "allowlist",
590
+ label: "白名单"
591
+ },
592
+ {
593
+ value: "disabled",
594
+ label: "停用"
595
+ }
596
+ ];
597
+ /** 深比较(表单值与初始回显值的 dirty 判断) */
598
+ function shallowEqual(a, b) {
599
+ return JSON.stringify(a) === JSON.stringify(b);
600
+ }
601
+ /**
602
+ * 配置面板主体。
603
+ *
604
+ * 保存仅提交变更键(服务端按白名单合并),保存成功后提示
605
+ * HMR 重载语义(进行中的机器人会话将重置)。
606
+ */
607
+ function ConfigPanel() {
608
+ useConfigPanelStyles();
609
+ const [initial, setInitial] = (0, react.useState)(null);
610
+ const [form, setForm] = (0, react.useState)(null);
611
+ const [busy, setBusy] = (0, react.useState)(false);
612
+ const [result, setResult] = (0, react.useState)(null);
613
+ const [loadError, setLoadError] = (0, react.useState)(null);
614
+ (0, react.useEffect)(() => {
615
+ let stopped = false;
616
+ fetch(BIND_CONFIG_PATH, { cache: "no-store" }).then(async (res) => {
617
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
618
+ const body = await res.json();
619
+ if (stopped) return;
620
+ setInitial(body.config);
621
+ setForm(body.config);
622
+ }).catch((err) => {
623
+ if (!stopped) setLoadError(err instanceof Error ? err.message : String(err));
624
+ });
625
+ return () => {
626
+ stopped = true;
627
+ };
628
+ }, []);
629
+ const patch = (0, react.useCallback)((key, value) => {
630
+ setForm((current) => current === null ? current : {
631
+ ...current,
632
+ [key]: value
633
+ });
634
+ setResult(null);
635
+ }, []);
636
+ const submit = (0, react.useCallback)((event) => {
637
+ event.preventDefault();
638
+ if (busy || initial === null || form === null) return;
639
+ const changed = {};
640
+ for (const key of Object.keys(form)) if (!shallowEqual(form[key], initial[key])) changed[key] = form[key];
641
+ if (Object.keys(changed).length === 0) return;
642
+ setBusy(true);
643
+ fetch(BIND_CONFIG_PATH, {
644
+ method: "POST",
645
+ headers: { "content-type": "application/json" },
646
+ body: JSON.stringify(changed)
647
+ }).then(async (res) => {
648
+ const body = await res.json().catch(() => ({}));
649
+ if (res.ok && body.ok === true) {
650
+ setResult({
651
+ ok: true,
652
+ message: body.message ?? "配置已保存,插件将自动重载"
653
+ });
654
+ setInitial(form);
655
+ return;
656
+ }
657
+ setResult({
658
+ ok: false,
659
+ message: body.error ?? `保存失败(HTTP ${res.status})`
660
+ });
661
+ }).catch((err) => {
662
+ setResult({
663
+ ok: false,
664
+ message: err instanceof Error ? err.message : String(err)
665
+ });
666
+ }).finally(() => {
667
+ setBusy(false);
668
+ });
669
+ }, [
670
+ busy,
671
+ form,
672
+ initial
673
+ ]);
674
+ if (loadError !== null) return (0, react.createElement)("p", {
675
+ className: "imqb2-hint",
676
+ style: { padding: "12px 0" }
677
+ }, `配置加载失败:${loadError}`);
678
+ if (initial === null || form === null) return (0, react.createElement)("p", {
679
+ className: "imqb2-hint",
680
+ style: { padding: "12px 0" }
681
+ }, "正在加载配置…");
682
+ const dirty = Object.keys(form).some((key) => !shallowEqual(form[key], initial[key]));
683
+ const access = form.access ?? {
684
+ c2cMode: "open",
685
+ c2cAllow: [],
686
+ groupMode: "open",
687
+ groupAllow: []
688
+ };
689
+ return (0, react.createElement)("form", {
690
+ className: "imqb2-form",
691
+ onSubmit: submit
692
+ }, (0, react.createElement)("div", { className: "imqb2-group-title" }, "访问控制"), (0, react.createElement)(SegmentedRow, {
693
+ label: "私聊",
694
+ value: access.c2cMode,
695
+ options: MODE_OPTIONS,
696
+ onChange: (v) => {
697
+ patch("access", {
698
+ ...access,
699
+ c2cMode: v
700
+ });
701
+ }
702
+ }), access.c2cMode === "allowlist" ? (0, react.createElement)(AreaRow, {
703
+ label: "私聊白名单",
704
+ help: "每行一个 openid",
705
+ rows: 3,
706
+ value: access.c2cAllow.join("\n"),
707
+ onChange: (v) => {
708
+ patch("access", {
709
+ ...access,
710
+ c2cAllow: v.split("\n").map((s) => s.trim()).filter(Boolean)
711
+ });
712
+ }
713
+ }) : null, (0, react.createElement)(SegmentedRow, {
714
+ label: "群聊",
715
+ value: access.groupMode,
716
+ options: MODE_OPTIONS,
717
+ onChange: (v) => {
718
+ patch("access", {
719
+ ...access,
720
+ groupMode: v
721
+ });
722
+ }
723
+ }), access.groupMode === "allowlist" ? (0, react.createElement)(AreaRow, {
724
+ label: "群聊白名单",
725
+ help: "每行一个群 openid",
726
+ rows: 3,
727
+ value: access.groupAllow.join("\n"),
728
+ onChange: (v) => {
729
+ patch("access", {
730
+ ...access,
731
+ groupAllow: v.split("\n").map((s) => s.trim()).filter(Boolean)
732
+ });
733
+ }
734
+ }) : null, (0, react.createElement)("div", { className: "imqb2-group-title" }, "基本"), (0, react.createElement)(SwitchRow, {
735
+ label: "群聊需要 @机器人",
736
+ checked: form.requireMention ?? true,
737
+ help: "开启后群聊中只有 @机器人 的消息会触发回复",
738
+ onChange: (v) => {
739
+ patch("requireMention", v);
740
+ }
741
+ }), (0, react.createElement)(SwitchRow, {
742
+ label: "流式输出",
743
+ checked: form.streaming ?? true,
744
+ help: "回复内容分段实时推送到聊天",
745
+ onChange: (v) => {
746
+ patch("streaming", v);
747
+ }
748
+ }), (0, react.createElement)(SwitchRow, {
749
+ label: "显示工具调用结果",
750
+ checked: form.showToolResults ?? false,
751
+ help: "在聊天中展示 agent 工具调用的结构化结果",
752
+ onChange: (v) => {
753
+ patch("showToolResults", v);
754
+ }
755
+ }), (0, react.createElement)("div", { className: "imqb2-group-title" }, "模型"), (0, react.createElement)(FieldRow, {
756
+ label: "Provider",
757
+ value: form.provider ?? "",
758
+ placeholder: "留空继承宿主默认",
759
+ onChange: (v) => {
760
+ patch("provider", v);
761
+ }
762
+ }), (0, react.createElement)(FieldRow, {
763
+ label: "Model",
764
+ value: form.model ?? "",
765
+ placeholder: "留空继承宿主默认",
766
+ onChange: (v) => {
767
+ patch("model", v);
768
+ }
769
+ }), (0, react.createElement)(FieldRow, {
770
+ label: "Preset",
771
+ value: form.preset ?? "",
772
+ placeholder: "Agent preset id",
773
+ onChange: (v) => {
774
+ patch("preset", v);
775
+ }
776
+ }), (0, react.createElement)("div", { className: "imqb2-group-title" }, "补充指令"), (0, react.createElement)(AreaRow, {
777
+ label: "群聊额外 system prompt",
778
+ value: form.groupPrompt ?? "",
779
+ onChange: (v) => {
780
+ patch("groupPrompt", v);
781
+ }
782
+ }), (0, react.createElement)(AreaRow, {
783
+ label: "私聊额外 system prompt",
784
+ value: form.directPrompt ?? "",
785
+ onChange: (v) => {
786
+ patch("directPrompt", v);
787
+ }
788
+ }), 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", {
789
+ className: "imqb2-save",
790
+ type: "submit",
791
+ disabled: busy || !dirty
792
+ }, busy ? "正在保存…" : "保存")));
156
793
  }
157
794
  //#endregion
158
795
  //#region src/client/index.ts
@@ -166,7 +803,7 @@ window.__ModuleLoader__.load({
166
803
  const TAB_KIND = "qqbot-bind";
167
804
  /** 席位注册 key(Tab 类型 id,席位按其分发;只需全局唯一,与包名解耦) */
168
805
  const TAB_ID = "im-qqbot/bind";
169
- /** 绑定入口图标(机器人轮廓,跟随主题 currentColor) */
806
+ /** 绑定入口图标(QQ 机器人,跟随主题图标色变量) */
170
807
  function BindGuideIcon() {
171
808
  return (0, react.createElement)("svg", {
172
809
  width: 24,
@@ -175,34 +812,21 @@ window.__ModuleLoader__.load({
175
812
  fill: "none",
176
813
  "aria-hidden": true
177
814
  }, (0, react.createElement)("path", {
178
- d: "M12 3a6 6 0 0 1 6 6v1.5a4.5 4.5 0 0 1 0 9H6a4.5 4.5 0 0 1 0-9V9a6 6 0 0 1 6-6Z",
179
- fill: "currentColor",
180
- fillOpacity: .9
181
- }), (0, react.createElement)("circle", {
182
- cx: 9.5,
183
- cy: 12,
184
- r: 1.4,
185
- fill: "#fff"
186
- }), (0, react.createElement)("circle", {
187
- cx: 14.5,
188
- cy: 12,
189
- r: 1.4,
190
- fill: "#fff"
815
+ 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",
816
+ fill: "var(------icon_primary, #1A1A1A)"
817
+ }), (0, react.createElement)("path", {
818
+ 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",
819
+ fill: "var(------icon_primary, #1A1A1A)"
191
820
  }), (0, react.createElement)("path", {
192
- d: "M9.5 15.5a3 3 0 0 0 5 0",
193
- stroke: "#fff",
194
- strokeWidth: 1.2,
195
- strokeLinecap: "round"
821
+ fillRule: "evenodd",
822
+ clipRule: "evenodd",
823
+ 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",
824
+ fill: "var(------icon_primary, #1A1A1A)"
196
825
  }));
197
826
  }
198
827
  /** 设置页「QQ Bot」栏目:面板常驻展示绑定状态 */
199
828
  function QQBotSettingsSection() {
200
- return (0, react.createElement)("div", null, (0, react.createElement)("p", { style: {
201
- margin: "0 0 8px",
202
- opacity: .75,
203
- lineHeight: 1.6,
204
- fontSize: "13px"
205
- } }, "扫码将 QQ 机器人绑定到当前配置;绑定完成后机器人接入私聊与群聊。"), (0, react.createElement)(BindPanelBody));
829
+ return (0, react.createElement)("div", null, (0, react.createElement)(BindPanelBody), (0, react.createElement)(ConfigPanel));
206
830
  }
207
831
  function apply(ctx) {
208
832
  const tabs = ctx.sidebarRightTabs;
@@ -213,18 +837,18 @@ window.__ModuleLoader__.load({
213
837
  name: "settings.section",
214
838
  id: "qqbot",
215
839
  order: 7,
216
- label: "QQ Bot"
840
+ label: "QQ 机器人"
217
841
  }, QQBotSettingsSection)));
218
842
  if (tabs !== void 0 && slots !== void 0) {
219
843
  disposers.push(tabs.register({
220
844
  id: TAB_ID,
221
845
  kind: TAB_KIND,
222
- title: () => "QQ Bot 绑定",
846
+ title: () => "QQ 机器人绑定",
223
847
  guide: [{
224
848
  id: "bind",
225
849
  order: 90,
226
- title: () => "QQ Bot 绑定",
227
- description: () => "扫码将 QQ 机器人绑定到当前配置",
850
+ title: () => "QQ 机器人绑定",
851
+ description: () => "扫码或手动填写,绑定 QQ 机器人",
228
852
  icon: BindGuideIcon
229
853
  }]
230
854
  }));
@@ -263,7 +887,7 @@ window.__ModuleLoader__.load({
263
887
  return;
264
888
  }
265
889
  try {
266
- const res = await fetch("/qqbot/im-qqbot/bind", { cache: "no-store" });
890
+ const res = await fetch(BIND_STATE_PATH, { cache: "no-store" });
267
891
  if (res.ok) {
268
892
  if ((await res.json()).phase === "waiting" && !opened && right !== void 0) try {
269
893
  right.openTab(TAB_KIND);