@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.
- package/CHANGELOG.md +18 -0
- package/CHANGELOG_EN.md +18 -0
- package/README.md +12 -1
- package/dist/commands/help.d.ts +1 -1
- package/dist/commands/help.js +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -8
- package/dist/config.js.map +1 -1
- package/dist/gateway/bootstrap.d.ts.map +1 -1
- package/dist/gateway/bootstrap.js +17 -0
- package/dist/gateway/bootstrap.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +41 -22
- package/dist/index.js.map +1 -1
- package/dist/setup/bind-routes.d.ts +78 -0
- package/dist/setup/bind-routes.d.ts.map +1 -0
- package/dist/setup/bind-routes.js +311 -0
- package/dist/setup/bind-routes.js.map +1 -0
- package/dist/setup/binding-service.d.ts +86 -0
- package/dist/setup/binding-service.d.ts.map +1 -0
- package/dist/setup/binding-service.js +175 -0
- package/dist/setup/binding-service.js.map +1 -0
- package/dist/setup/index.d.ts +50 -0
- package/dist/setup/index.d.ts.map +1 -0
- package/dist/setup/index.js +98 -0
- package/dist/setup/index.js.map +1 -0
- package/dist/setup/persist.d.ts +41 -0
- package/dist/setup/persist.d.ts.map +1 -0
- package/dist/setup/persist.js +201 -0
- package/dist/setup/persist.js.map +1 -0
- package/dist/setup/presenters.d.ts +40 -0
- package/dist/setup/presenters.d.ts.map +1 -0
- package/dist/setup/presenters.js +84 -0
- package/dist/setup/presenters.js.map +1 -0
- package/dist/setup/qr-svg.d.ts +8 -0
- package/dist/setup/qr-svg.d.ts.map +1 -0
- package/dist/setup/qr-svg.js +20 -0
- package/dist/setup/qr-svg.js.map +1 -0
- package/dist/shared/bind-protocol.d.ts +42 -0
- package/dist/shared/bind-protocol.d.ts.map +1 -0
- package/dist/shared/bind-protocol.js +19 -0
- package/dist/shared/bind-protocol.js.map +1 -0
- package/dist/shared/utils.d.ts +8 -3
- package/dist/shared/utils.d.ts.map +1 -1
- package/dist/shared/utils.js +45 -4
- package/dist/shared/utils.js.map +1 -1
- package/dist/transport/reply-limiter.d.ts +1 -1
- package/dist/transport/reply-limiter.js +1 -1
- package/lib/client.js +996 -0
- package/lib/client.js.map +1 -0
- package/package.json +26 -6
- package/pnpm-workspace.yaml +1 -0
- package/tsconfig.client.json +19 -0
- package/tsdown.client.config.ts +58 -0
- package/dist/setup.d.ts +0 -24
- package/dist/setup.d.ts.map +0 -1
- package/dist/setup.js +0 -170
- 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,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIABAMAAAAGVsnJAAAAKlBMVEUAAAAAif8AiP8Aiv8Aif8AiP8Agv8AiP8AiP8AiP8AiP8AiP8Ah/8AiP/n+LNDAAAADXRSTlMAv2Qi36QQ70CAkM9QSlIWegAADuJJREFUeNrs3dvrDVEUB/BltjPm4MElEabIXZTLcYmUSyQ1JSK3KURuT4goU66RUvKAB6fceXBKubwpkZTygB/j9v1fCLk7M3vM2XvPXvvzB/D7rd+etfdes2ZvchzHcRzHcRzHcRzHUWvqzCPTiSv/8NoIn3UNGLue+PFPRvjuw03iZutA/GLPQ2JlW4LfdC0nRg7hLxhFYDLAOgLb8A+niYUgwj90DSMG/Bb+6T2HBUEDbSwg63kJ2kjtfwiaaOsdWa6Gb7jOBDEyvCar1ZBpKdksRqb3ZLE6PuOcBSYih5dkLYE80klkqwZyWUS2ivEZ4zToIaeDZKcGwPsZiPEdyw2BwGec54EavuG6HD6F3N6SjWJ8x3JLKCDBxtpgDd9w3RBtgYS9ZJ+JAO8sGELCC7KOgIzUvizo4Qu++6EapFwk23SHlLlkmzmQ8oZs8xxSPpJtWviOZ2k4xGect0MPIOUD2QZyUrKMD0lkGQFJtpUFAxcA8N4MuAC4ALgA8E6C7KdBwX0hxH4pTBHAuywcc98Ot/AF3x6JidxLYuyLoj0g5TbZpobPODfJeJBi35e0PsC8RSTkvQwgmsj7vQjRFt7vRol68p4EiALO1YAvQt45UKpV9hXZqBvvFEAkmKcAohbfYsBXm7h/MtMbOVl7iECL+WdztIn3E0AkEsafTH0xkWc91H0+/0OL7yLgqxrHPnF3iMrP6qwzgDtI6bPe7Y/Ssu99gNwpAneIgSbz4/TaHahoW2+c5EyQniUmQi59QS4ALgAuAC4ALgAuAC4ALgAuAC4ALgAuAC4ALgAuAC4ALgAuAC4AJKZcu4t/mLfa9nvnxKO1CdpKbb57b8qlfC0ygx+ThfyTA5HbzvO2DQN/vOxBSrdsCoF/MoK0DzetCcGUEIW8GEE22HoDhS2s/ttSf1SC/5Cuo2rbOhDSbLqN9ViC/5auoqoKzqEUCyvaPLwxRElejKYKOpmgNGn1LqT1h6NU/aha/HMo2ZJKJYIgRuleVygCQYgOeFGZZWEQoiM+VORrMi/Cf6r2zdQ//f4sI/BLIyzDCJT1/Fe1nfivvz+j2VDE+B2rCBRd/1lz0t4YKDGfDHUIJarghzXbocx1MlDvBBnsXg6IEBksnwoyE6DliXAyFOtPRvESKJYalQZECOVemJQGciYAa9PAZmhhTK08iKBFlykPQQsy7NsWyc2A9s2FQQJJltWHmpBi3UOwAVqtJM1EBFl2zQRjoNkB0qoO7U6TRn6MgtJ5a85MODH1ixOHx10bkuAPFTh+cTaKSO9dfryefjdz3NrKVQgLZcCFE/6Zt8ThtdXKg8chq2vZdGpr66gHkLWANPGADrSAi2ffQ2D6KbRNSOnK2wHvP4kqsR6sQ0o/iUdVDK/CVNiChJ2jScqU2PghUEN+6dD18m3mht9I4cedbnWdYvaNDDuQ2/71VIi4avCu0M8/Vw2iwp7kn2Ezg6xgAHTgjLxpEXK6T0r5DxR1unuRmUNgh7L2Vi80cQj4D9S19waxgRNBTWV7s4jNWwvEStu7RWzaEKgrbm8PHhi2I2jmSssHqTReZNSOoK6+gcGLTKoLTNTQyTbNoGsahZbXlrPNuZ6lgWy7qHQjTakO/lQKznlIurrJsEvFeriX3FtrtYmwD3VeS1sr73YjXhPVkWkxdchRE2bC55IJQHEaeEsSiqdAXdeEeIn2mXCT3salhvayQKw5DTU1//91ZEgPUkd5kd40OEf7y/oGMuymDhKRlhngZ35L52qwpn8aprp0aUxhCtpHCjzVtykWRjSriEjbUmATjGhXami7tTYj/7xeT0r4saalgGfKZZk90d5B6oyGIUVZoqaeZyA2pSxPdS2vSDwjSrK5hsBD6oSG9jVQ7j/GXuqE2JwBQDRR/TPgGTQAiDz180DDpAFANFH5PBCbNACIPNVrocCsAUDUVPwMdDdlDZBvLXCbytY0ZBH4Q1PpkBRm7AJyV2fS9VSumiHdOT8LVdaFnmdMOjpsUvmKKNJeCJIsDb2gUnkZhWg9ZqhbmTQy9l56eOo2RE3T5kDFP5b4n3wrxq/ou2YsyfEPX+s74EJWcummaiKs/cebmGNJgQtDNg78+m+vyghTpGhxMqdwCvTPFTn1ZlaCb+YXT4OvFO0Eh+U+X+BKkWag+YXT4As1KeB9/hNmunJOF0GUv+GipWRH2K3oKjBIiiTmpsQya5OSnrlP7J29ahtBFIX3T7IkS+AXEAhDIEUWVDjpFgTpAgKRIk1YUKc0Ar3AgqtUUZVaoBcQ5AUCJr3BP7Ijx9a7BAyBNBOy39yJb+C6lzz6dubce86MNMu9+29YJ7ZZE8W9opNzJ+c6oO1sI9dU1lKZHIiAZBg0qeeg1mTf6wEsT1ERiKEPSiuSU6zq7XenVXgRKOAKyNyS4f5L6+ZbeXgRKOEKWJGsLqvb1sfBg5qUrgCU1eX7vdwaGMoYAbgCGsSiNMe12/o8dFhdwBVQkJwiqe/t49CZwApOsRVR5szh7dkauAscB96B1+3IdLuHT+h74NxpBLTzDp3DAn5ALK3LoBNOUH/aB/+pFbYVWsIy20CzEsVu/aCtUAkjl0MinCna6yxCbpOnNHQ7IA6lRdQm6oRMRhO6IdQTFI79xZ+7pyrg9khGd6ALWQCwEB4F1MCRHgAzoIL+GjjQAyAJp4JNtwREegBEY6yCXAOvNAHIsQryOGykCUCGVZB74YEmAO6JehvqQOY20gQgGoc6LvbV+ca6AORwc4A3wiNdAGY8F4Rd9kAXgHagXDBzGwFdAJpVmDJQuLMAXQCiDTPtWFuutQEokFrzIjDRBiBGZYAXgVNtANzB4DSEE9hG2gBEFahXfF5d6QOwCnFuvOfUQH0A5q6xLkLEQRN9AGIQCvE46EwfgC4IhXAV3Eb6ADh7wfsAVfBGIQB3LziVr4I7jQCWoA7SsyEjjQAOwA4W9YITjQA64HFRg3GqEUBL3g/mziKgEUBUifvB0lUEdALYgEaAtQE7nQCWYMKybbGFTgAz0Aiw5nKiE0AMNJvVlYFOAAkYLyM61AkglU4EDpzuQieAaAw0i3yEH1oBbEAnRH4z4EErgBwMmAC91QpgDs40kwOYX7QCyEDnSnrrtVYAHdAKkkbwTCuABLSCxF4OtQJIwYjBYLaRVgDORTsQ7YRv9AIoRUOx2C9fmD8BgI1oL3zot9PSAxLqC+BcNBXs+Z28y8S/L8CX3YWoFVj4TaAp0fEjbt+4GVj6DaaDakjlpWKxqBnI/SQ1QTWkdAgHh85z4Y1fUW2iseRerVwi6oZKz4StT5bj3OugU1fUDfU9G+uclOTYawqnoifFxp7fx52R+dPyq+OVpB30pdlGr+4D2QEPrcZ0wuupOSYVeen1AEtBP9zy3mk7J115w6uKbQT9cMu7pnbIIfPm2MfM5YJ7Q4m7q+Jr4Jo0oNupN4ABAeB/7O6EDCSpcZesg54MgLa/sUjHpCNbefxqbyH4tZGGgLU8IddxJ84JwFOYtSCABb8k9DXBdjkFGYZEJBRLpCvtCtzFl5b4EvfsHwA4opemX/51Ler+ph3vSIzHBy3/Xi8r9+d3E+j/ev6fwVMDu3khZ1P3/eMneVurG02fPXJ78xELF1+38svpw7fj57V70ean4xevougJAfT0XCdiALy7twsDYADqA9Byp9RTASj+RwCJATAABsAAGAADYAAMgAEwAAbAABgAA2AADIABMAAGwAAYAANgAAyAATAABsAAGICf7N3PSxRhGAfwxxldV+2QVETQwmpRQSxsJhiEYC11E7KSAlmIUpJiDyUFgQN7kwJBOgQehE51EoKwmyBdooOH1dkWk+//UkhdpGfX9HlnnseZzx+w7D478/yYd2beNABpANIApAFIA5AGYK8qFgOQ3iaXBsB9AG6RYh2C75NrtxiAtghul/9Eikk+MNHDBlOxTu4xF8Fg/iDFKlwABPPJJik2L5i4+BeSKLYuWLs99vF5xZYjeHi6RorlBB+f99k9/RWTfKFghv8wtbKir5QM7I2DnuhRu8rWVLXaRPNWGf/2ktSqiFauAvsODbUKXO8i2lTUSa2y6H9W4fczV8qH6PzSDmtlwIPoCN8Ba2WgR3aHBQ/WsuCw7DGbhbVxqCy7y0omMJYFfQiPLznYujLeAeEBdgm2ekHxzafnwdgilcrS2811gtFQORH74Izs/5yytDgi/3U9mOoE1sFZk95tDiEpNCq//XoOhgphFyA+vhYAO6sjw4D4SkYFnFBdM5gZBcQX89rAek/KdMBBzToCwMoCWQHiRWBnHLLSC/lwUrKWwSqRKoNg1Wn/5sHaVpUG/VE4qVht4H0hRZ6Cd5ToYFnQwtXxHHh5OoAAsDARTQGO2vYl8OpqskBmFa4K9mM08ZmUuIombgpMGIxQSS/AlACRuS0TAOp7gTmhf4lPArqn4i40s0XkLgmgruAk8HNopuQ0vLhOsZuD2EHKJwHeHYrZFOA2US+hqTBPsfICMKTG9kk0F1YpRtkcmhsRufVObSJs+fuRF5szePUqCZD6/fIT2xBa2c5TLB7m0MqmZJ/BC+9TDN4EaGlB9noz70aVIjb9Ea2F0k9h8Bq3Iw3BdF8Ahvz6TTf2ZoXZFEXco/4JMJgzQPgc4IXXep07/h9fR2zVzahNkuHBqDwJycGkOkmZhEklkpKFSUUSU4BBGySnGwYtkKBVmLNNkgZhTokk+QGMkV61GYIxM/RHUrvBNdqR2Eq4QdK6YEqexC3BkC36LcmHwE4TlOBD4G8GSGwhyJMT32DEDLnhj8KEXU1g8iaCErmSKcOAXbfvJa8ULpBD56Hec9otWXnQ9c2LT6DcB3LsLFSbJdd81asktSI55ym+OtbIUwQuQa1jxElGGpilaPhKG8KfXAuYkAjwCVCep7AfqlWJl4AItLhp+dBHoOXvP+QRqFUpcllFLWGtSjHw70KJV0WKRx9UOP2A4jKg4MaJ2juKkX8RMTtVpHgN3EOMXpyj+L0+iZg8u0A6DEwgeo2VM6SH/30sQITClbdF0ubK18XxsV7nTowv9l+mVCqV+tUeHAgAAAAACPK3HmGBCgAAAAAAAAAAYAWpVR6jggdeCwAAAABJRU5ErkJggg==";
|
|
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
|