@wu529778790/open-im 1.11.8-beta.2 → 1.11.8-beta.21
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/dist/adapters/codebuddy-adapter.js +3 -17
- package/dist/adapters/codex-adapter.js +3 -16
- package/dist/adapters/opencode-adapter.d.ts +0 -5
- package/dist/adapters/opencode-adapter.js +10 -25
- package/dist/adapters/registry.js +10 -1
- package/dist/clawbot/client.js +72 -78
- package/dist/clawbot/qr-login.d.ts +1 -0
- package/dist/clawbot/qr-login.js +7 -0
- package/dist/codebuddy/cli-runner.d.ts +2 -22
- package/dist/codebuddy/cli-runner.js +9 -27
- package/dist/codex/cli-runner.d.ts +2 -22
- package/dist/codex/cli-runner.js +24 -75
- package/dist/commands/handler.d.ts +1 -0
- package/dist/commands/handler.js +34 -0
- package/dist/config/types.d.ts +23 -0
- package/dist/config-web-auth.d.ts +13 -0
- package/dist/config-web-auth.js +114 -0
- package/dist/config-web-browser.d.ts +4 -0
- package/dist/config-web-browser.js +48 -0
- package/dist/config-web-cors.d.ts +5 -0
- package/dist/config-web-cors.js +48 -0
- package/dist/config-web-health.d.ts +7 -0
- package/dist/config-web-health.js +65 -0
- package/dist/config-web-http.d.ts +3 -0
- package/dist/config-web-http.js +31 -0
- package/dist/config-web-page-i18n.d.ts +36 -2
- package/dist/config-web-page-i18n.js +36 -2
- package/dist/config-web-payload.d.ts +84 -0
- package/dist/config-web-payload.js +260 -0
- package/dist/config-web-probes.d.ts +2 -0
- package/dist/config-web-probes.js +271 -0
- package/dist/config-web.d.ts +3 -11
- package/dist/config-web.js +45 -815
- package/dist/config.js +22 -1
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +11 -0
- package/dist/opencode/cli-runner.d.ts +2 -22
- package/dist/opencode/cli-runner.js +47 -68
- package/dist/opencode/sdk-manager.d.ts +11 -0
- package/dist/opencode/sdk-manager.js +46 -0
- package/dist/opencode/sdk-runner.d.ts +7 -0
- package/dist/opencode/sdk-runner.js +225 -0
- package/dist/platform/handle-ai-request.js +18 -1
- package/dist/qq/client.js +45 -53
- package/dist/shared/ai-task.d.ts +15 -0
- package/dist/shared/ai-task.js +174 -2
- package/dist/shared/cli-runner-base.d.ts +39 -0
- package/dist/shared/cli-runner-base.js +108 -0
- package/dist/shared/connection-manager.d.ts +111 -0
- package/dist/shared/connection-manager.js +157 -0
- package/dist/shared/session-invalid-detector.d.ts +5 -0
- package/dist/shared/session-invalid-detector.js +20 -0
- package/dist/wework/client.js +79 -127
- package/dist/workbuddy/client.js +40 -74
- package/package.json +4 -1
- package/web/dist/assets/index-6e51Wtaa.css +1 -0
- package/web/dist/assets/index-Da9qKWA2.js +57 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-XKVTb-0p.css +0 -1
- package/web/dist/assets/index-yDVGC_84.js +0 -57
package/dist/workbuddy/client.js
CHANGED
|
@@ -9,7 +9,8 @@ import { existsSync, mkdirSync } from 'node:fs';
|
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { hostname, homedir } from 'node:os';
|
|
11
11
|
import { createLogger } from '../logger.js';
|
|
12
|
-
import {
|
|
12
|
+
import { isFatalReconnectError } from '../shared/reconnect.js';
|
|
13
|
+
import { ReconnectManager, HeartbeatMonitor, StateManager } from '../shared/connection-manager.js';
|
|
13
14
|
import { WorkBuddyOAuth } from './oauth.js';
|
|
14
15
|
import { WorkBuddyCentrifugeClient } from './centrifuge-client.js';
|
|
15
16
|
const log = createLogger('WorkBuddy');
|
|
@@ -18,19 +19,29 @@ const CHANNEL_HEARTBEAT_MS = 30_000;
|
|
|
18
19
|
// Global state
|
|
19
20
|
let oauthClient = null;
|
|
20
21
|
let centrifugeClient = null;
|
|
21
|
-
let channelState = 'disconnected';
|
|
22
22
|
let messageHandler = null;
|
|
23
|
-
let stateChangeHandler = null;
|
|
24
|
-
let heartbeatTimer = null;
|
|
25
|
-
let reconnectTimer = null;
|
|
26
|
-
let reconnectAttempt = 0;
|
|
27
|
-
let fatalSlowProbe = false;
|
|
28
23
|
let stopped = false;
|
|
29
24
|
let platformConfig = null;
|
|
30
25
|
/** 单飞刷新 token 的在途 Promise,避免并发 401 重复刷新 */
|
|
31
26
|
let refreshInFlight = null;
|
|
27
|
+
// Connection infrastructure (shared managers replace raw timers/counters)
|
|
28
|
+
const stateManager = new StateManager('disconnected');
|
|
29
|
+
const heartbeatMonitor = new HeartbeatMonitor();
|
|
30
|
+
const reconnectManager = new ReconnectManager({
|
|
31
|
+
name: 'WorkBuddy',
|
|
32
|
+
backoff: { mode: 'stepped', delays: RECONNECT_DELAYS_MS },
|
|
33
|
+
onReconnect: async () => {
|
|
34
|
+
try {
|
|
35
|
+
await connect();
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
log.error('Reconnect attempt failed:', err);
|
|
39
|
+
reconnectManager.schedule();
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
32
43
|
function getChannelState() {
|
|
33
|
-
return
|
|
44
|
+
return stateManager.current;
|
|
34
45
|
}
|
|
35
46
|
export async function initWorkBuddy(config, eventHandler, onStateChange) {
|
|
36
47
|
const pc = config.platforms?.workbuddy;
|
|
@@ -42,9 +53,10 @@ export async function initWorkBuddy(config, eventHandler, onStateChange) {
|
|
|
42
53
|
}
|
|
43
54
|
platformConfig = pc;
|
|
44
55
|
messageHandler = eventHandler;
|
|
45
|
-
|
|
56
|
+
stateManager.setOnChange(onStateChange);
|
|
46
57
|
stopped = false;
|
|
47
|
-
|
|
58
|
+
reconnectManager.reset();
|
|
59
|
+
reconnectManager.resume();
|
|
48
60
|
const baseDir = config.logDir ?? join(process.env.HOME ?? '', '.open-im');
|
|
49
61
|
const dataDir = join(baseDir, 'data');
|
|
50
62
|
if (!existsSync(dataDir))
|
|
@@ -83,10 +95,10 @@ async function connect() {
|
|
|
83
95
|
catch (err) {
|
|
84
96
|
log.error('Host workspace registration failed:', err);
|
|
85
97
|
if (isFatalReconnectError(err)) {
|
|
86
|
-
|
|
98
|
+
reconnectManager.setFatal(true);
|
|
87
99
|
log.warn('WorkBuddy 致命错误(鉴权失败),转慢探测(token 可能已过期):', err);
|
|
88
100
|
}
|
|
89
|
-
|
|
101
|
+
reconnectManager.schedule();
|
|
90
102
|
return;
|
|
91
103
|
}
|
|
92
104
|
// sessionId = userId_hostId_clawPath (matches plugin's buildSessionId format)
|
|
@@ -137,9 +149,8 @@ async function connect() {
|
|
|
137
149
|
onConnected: () => {
|
|
138
150
|
log.info('WorkBuddy Centrifuge connected');
|
|
139
151
|
log.info(`WeChat KF sessionId: ${workspaceSessionId}`);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
updateState('connected');
|
|
152
|
+
reconnectManager.reset();
|
|
153
|
+
stateManager.set('connected');
|
|
143
154
|
// Step 2: Register Claw workspace to get WeChat KF routing channel + sessionId
|
|
144
155
|
oauth.registerWorkspace({
|
|
145
156
|
userId: pc.userId ?? '',
|
|
@@ -152,7 +163,7 @@ async function connect() {
|
|
|
152
163
|
// Subscribe to Claw channel — WeChat KF messages are published here
|
|
153
164
|
centrifugeClient?.subscribeChannel(clawParams.channel, clawParams.subscriptionToken);
|
|
154
165
|
const doRegister = () => {
|
|
155
|
-
if (stopped ||
|
|
166
|
+
if (stopped || stateManager.current !== 'connected')
|
|
156
167
|
return;
|
|
157
168
|
if (replyLock) {
|
|
158
169
|
log.debug('Heartbeat skipped (reply in progress)');
|
|
@@ -168,14 +179,12 @@ async function connect() {
|
|
|
168
179
|
.catch((err) => log.warn(`registerChannel failed: ${String(err)}`));
|
|
169
180
|
};
|
|
170
181
|
doRegister();
|
|
171
|
-
|
|
172
|
-
clearInterval(heartbeatTimer);
|
|
173
|
-
heartbeatTimer = setInterval(doRegister, CHANNEL_HEARTBEAT_MS);
|
|
182
|
+
heartbeatMonitor.start(CHANNEL_HEARTBEAT_MS, doRegister);
|
|
174
183
|
}).catch((err) => {
|
|
175
184
|
log.error('Claw workspace registration failed:', err);
|
|
176
185
|
// Fallback: register with host sessionId
|
|
177
186
|
const doRegister = () => {
|
|
178
|
-
if (stopped ||
|
|
187
|
+
if (stopped || stateManager.current !== 'connected')
|
|
179
188
|
return;
|
|
180
189
|
if (replyLock) {
|
|
181
190
|
log.debug('Heartbeat skipped (reply in progress, fallback path)');
|
|
@@ -191,19 +200,14 @@ async function connect() {
|
|
|
191
200
|
.catch((e) => log.warn(`registerChannel failed: ${String(e)}`));
|
|
192
201
|
};
|
|
193
202
|
doRegister();
|
|
194
|
-
|
|
195
|
-
clearInterval(heartbeatTimer);
|
|
196
|
-
heartbeatTimer = setInterval(doRegister, CHANNEL_HEARTBEAT_MS);
|
|
203
|
+
heartbeatMonitor.start(CHANNEL_HEARTBEAT_MS, doRegister);
|
|
197
204
|
});
|
|
198
205
|
},
|
|
199
206
|
onDisconnected: (reason) => {
|
|
200
207
|
log.info(`WorkBuddy Centrifuge disconnected: ${reason}`);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
updateState('disconnected');
|
|
206
|
-
scheduleReconnect();
|
|
208
|
+
heartbeatMonitor.stop();
|
|
209
|
+
stateManager.set('disconnected');
|
|
210
|
+
reconnectManager.schedule();
|
|
207
211
|
},
|
|
208
212
|
onError: (error) => {
|
|
209
213
|
const msg = error instanceof Error ? error.message : JSON.stringify(error);
|
|
@@ -214,16 +218,13 @@ async function connect() {
|
|
|
214
218
|
else {
|
|
215
219
|
log.error(`WorkBuddy Centrifuge error: ${msg}`);
|
|
216
220
|
}
|
|
217
|
-
|
|
221
|
+
stateManager.set('error');
|
|
218
222
|
},
|
|
219
223
|
onPersistentFailure: () => {
|
|
220
224
|
log.warn('WorkBuddy Centrifuge persistent failure detected — doing full re-registration');
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
updateState('disconnected');
|
|
226
|
-
scheduleReconnect();
|
|
225
|
+
heartbeatMonitor.stop();
|
|
226
|
+
stateManager.set('disconnected');
|
|
227
|
+
reconnectManager.schedule();
|
|
227
228
|
},
|
|
228
229
|
onMessage: async (chatId, msgId, content) => {
|
|
229
230
|
if (messageHandler) {
|
|
@@ -238,35 +239,6 @@ async function connect() {
|
|
|
238
239
|
});
|
|
239
240
|
centrifugeClient.start();
|
|
240
241
|
}
|
|
241
|
-
function scheduleReconnect() {
|
|
242
|
-
if (stopped)
|
|
243
|
-
return;
|
|
244
|
-
if (reconnectTimer) {
|
|
245
|
-
clearTimeout(reconnectTimer);
|
|
246
|
-
reconnectTimer = null;
|
|
247
|
-
}
|
|
248
|
-
const baseDelay = RECONNECT_DELAYS_MS[Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
|
|
249
|
-
const delay = fatalSlowProbe ? jitteredDelay(SLOW_PROBE_MS) : jitteredDelay(baseDelay);
|
|
250
|
-
reconnectAttempt++;
|
|
251
|
-
log.info(`Reconnecting in ${delay}ms (attempt ${reconnectAttempt})${fatalSlowProbe ? ' [slow-probe]' : ''}...`);
|
|
252
|
-
reconnectTimer = setTimeout(async () => {
|
|
253
|
-
reconnectTimer = null;
|
|
254
|
-
if (stopped)
|
|
255
|
-
return;
|
|
256
|
-
try {
|
|
257
|
-
await connect();
|
|
258
|
-
}
|
|
259
|
-
catch (err) {
|
|
260
|
-
log.error('Reconnect attempt failed:', err);
|
|
261
|
-
scheduleReconnect();
|
|
262
|
-
}
|
|
263
|
-
}, delay);
|
|
264
|
-
}
|
|
265
|
-
function updateState(state) {
|
|
266
|
-
channelState = state;
|
|
267
|
-
stateChangeHandler?.(state);
|
|
268
|
-
log.debug(`WorkBuddy state: ${state}`);
|
|
269
|
-
}
|
|
270
242
|
export function getCentrifugeClient() {
|
|
271
243
|
return centrifugeClient;
|
|
272
244
|
}
|
|
@@ -301,20 +273,14 @@ async function refreshWorkBuddyToken() {
|
|
|
301
273
|
export function stopWorkBuddy() {
|
|
302
274
|
log.info('Stopping WorkBuddy client...');
|
|
303
275
|
stopped = true;
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
heartbeatTimer = null;
|
|
307
|
-
}
|
|
308
|
-
if (reconnectTimer) {
|
|
309
|
-
clearTimeout(reconnectTimer);
|
|
310
|
-
reconnectTimer = null;
|
|
311
|
-
}
|
|
276
|
+
heartbeatMonitor.stop();
|
|
277
|
+
reconnectManager.stop();
|
|
312
278
|
if (centrifugeClient) {
|
|
313
279
|
centrifugeClient.stop();
|
|
314
280
|
centrifugeClient = null;
|
|
315
281
|
}
|
|
316
282
|
oauthClient = null;
|
|
317
283
|
platformConfig = null;
|
|
318
|
-
|
|
284
|
+
stateManager.set('disconnected');
|
|
319
285
|
log.info('WorkBuddy client stopped');
|
|
320
286
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wu529778790/open-im",
|
|
3
|
-
"version": "1.11.8-beta.
|
|
3
|
+
"version": "1.11.8-beta.21",
|
|
4
4
|
"description": "Your AI coding assistant, in every chat app. Multi-platform IM bridge for Claude Code, Codex, and CodeBuddy.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -55,13 +55,16 @@
|
|
|
55
55
|
"@anthropic-ai/sdk": "^0.104.2",
|
|
56
56
|
"@aws-sdk/client-s3": "^3.1035.0",
|
|
57
57
|
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
58
|
+
"@opencode-ai/sdk": "^1.17.9",
|
|
58
59
|
"@sentry/node": "^10.58.0",
|
|
60
|
+
"@types/qrcode": "^1.5.6",
|
|
59
61
|
"centrifuge": "^5.5.3",
|
|
60
62
|
"dingtalk-stream": "^2.1.4",
|
|
61
63
|
"edge-tts": "^1.0.1",
|
|
62
64
|
"https-proxy-agent": "^9.1.0",
|
|
63
65
|
"node-edge-tts": "^1.2.10",
|
|
64
66
|
"prompts": "^2.4.2",
|
|
67
|
+
"qrcode": "^1.5.4",
|
|
65
68
|
"say": "^0.16.0",
|
|
66
69
|
"telegraf": "^4.16.3",
|
|
67
70
|
"ws": "^8.21.0"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import"https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap";:root{--c-bg: #fafafa;--c-surface: #ffffff;--c-surface-alt: #f5f5f5;--c-border: #e5e5e5;--c-border-foc: #a3a3a3;--c-text: #1a1a2e;--c-text-2: #525252;--c-text-3: #a3a3a3;--c-accent: #6366f1;--c-accent-h: #4f46e5;--c-accent-bg: #eef2ff;--c-accent-bdr: #c7d2fe;--c-ok: #10b981;--c-ok-bg: #ecfdf5;--c-ok-bdr: #a7f3d0;--c-warn: #f59e0b;--c-warn-bg: #fffbeb;--c-warn-bdr: #fde68a;--c-err: #ef4444;--c-err-bg: #fef2f2;--c-err-bdr: #fecaca;--shadow-xs: 0 1px 2px rgba(0,0,0,.05);--shadow-s: 0 1px 3px rgba(0,0,0,.1), 0 1px 2px rgba(0,0,0,.06);--shadow-m: 0 4px 6px -1px rgba(0,0,0,.1), 0 2px 4px -2px rgba(0,0,0,.1);--shadow-l: 0 10px 15px -3px rgba(0,0,0,.1), 0 4px 6px -4px rgba(0,0,0,.1);--shadow-glow: 0 0 20px rgba(99,102,241,.15);--r-s: 6px;--r-m: 8px;--r-l: 12px;--r-xl: 16px;--font-display: "Space Grotesk", -apple-system, sans-serif;--font-body: "Inter", -apple-system, sans-serif;--font-mono: "JetBrains Mono", "SF Mono", monospace;--fast: .12s ease;--norm: .2s ease;--slow: .3s ease}:root.dark{--c-bg: #0a0a0f;--c-surface: #141418;--c-surface-alt: #1e1e24;--c-border: #2a2a32;--c-border-foc: #52525e;--c-text: #f0f0f5;--c-text-2: #a0a0ae;--c-text-3: #5a5a68;--c-accent: #818cf8;--c-accent-h: #6366f1;--c-accent-bg: rgba(99,102,241,.12);--c-accent-bdr: rgba(99,102,241,.3);--c-ok-bg: rgba(16,185,129,.12);--c-ok-bdr: rgba(16,185,129,.3);--c-warn-bg: rgba(245,158,11,.12);--c-warn-bdr: rgba(245,158,11,.3);--c-err-bg: rgba(239,68,68,.12);--c-err-bdr: rgba(239,68,68,.3);--shadow-xs: 0 1px 2px rgba(0,0,0,.3);--shadow-s: 0 1px 3px rgba(0,0,0,.4), 0 1px 2px rgba(0,0,0,.3);--shadow-m: 0 4px 6px -1px rgba(0,0,0,.4), 0 2px 4px -2px rgba(0,0,0,.3);--shadow-l: 0 10px 15px -3px rgba(0,0,0,.4), 0 4px 6px -4px rgba(0,0,0,.3);--shadow-glow: 0 0 20px rgba(129,140,248,.2)}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}body{font-family:var(--font-body);font-size:14px;line-height:1.5;color:var(--c-text);background:var(--c-bg);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}:focus-visible{outline:2px solid var(--c-accent);outline-offset:2px}.content{padding:32px 40px 64px;max-width:800px;width:100%;margin:0 auto}.header{position:sticky;top:0;z-index:15;background:var(--c-surface);border-bottom:1px solid var(--c-border);padding:0 40px;height:48px;display:flex;align-items:center;justify-content:space-between}.header-left{display:flex;align-items:center;gap:12px}.header-right{display:flex;align-items:center;gap:8px}.header-status{font-size:13px;font-weight:500;display:inline-flex;align-items:center;gap:6px;color:var(--c-text-2)}.header-status .dot{width:8px;height:8px;border-radius:50%;display:inline-block}.header-status .dot.running{background:var(--c-ok);box-shadow:0 0 12px var(--c-ok);animation:pulse 2s infinite}.header-status .dot.idle{background:var(--c-text-3)}.header-divider{width:1px;height:20px;background:var(--c-border);margin:0 4px}@keyframes pulse{0%,to{opacity:1}50%{opacity:.6}}.stats-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:32px}.stat-card{background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r-l);padding:20px;transition:all var(--norm)}.stat-card:hover{box-shadow:var(--shadow-m);transform:translateY(-2px)}.stat-label{font-size:11px;font-weight:600;color:var(--c-text-3);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px}.stat-value{font-family:var(--font-display);font-size:32px;font-weight:700;color:var(--c-text);line-height:1;letter-spacing:-.02em}.stat-meta{font-size:12px;color:var(--c-text-3);margin-top:6px}.section{margin-bottom:40px}.section-head{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:20px;flex-wrap:wrap;gap:8px}.section-title{font-family:var(--font-display);font-size:18px;font-weight:600;color:var(--c-text);letter-spacing:-.01em}.section-desc{font-size:13px;color:var(--c-text-3)}.card{background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r-l);overflow:hidden;transition:all var(--norm)}.card:hover{box-shadow:var(--shadow-s)}.card-head{padding:16px 20px;border-bottom:1px solid var(--c-border);display:flex;justify-content:space-between;align-items:center;gap:12px;cursor:pointer}.card-head:hover{background:var(--c-surface-alt)}.card-title{font-family:var(--font-display);font-size:14px;font-weight:600;color:var(--c-text)}.card-body{padding:20px}.platform-grid{display:grid;grid-template-columns:1fr;gap:16px}.platform-card{background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r-l);overflow:visible;transition:all var(--norm)}.platform-card:hover{box-shadow:var(--shadow-m);border-color:var(--c-border-foc)}.platform-card.disabled{opacity:.85}.platform-card.enabled{border-color:var(--c-accent-bdr);box-shadow:0 0 0 1px var(--c-accent-bdr)}.platform-card-head{padding:16px 20px;display:flex;align-items:center;justify-content:space-between;gap:12px;cursor:pointer;border-radius:var(--r-l) var(--r-l) 0 0}.platform-card-head:hover{background:var(--c-surface-alt)}.platform-card-meta{display:flex;align-items:center;gap:12px;min-width:0}.platform-card-icon{width:40px;height:40px;border-radius:var(--r-m);background:var(--c-surface-alt);display:flex;align-items:center;justify-content:center;font-size:22px;line-height:1;flex-shrink:0}.platform-card-title-block{display:flex;flex-direction:column;gap:2px;min-width:0}.platform-card-name{font-family:var(--font-display);font-size:14px;font-weight:600;color:var(--c-text)}.platform-card-desc{font-size:12px;color:var(--c-text-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.platform-card-right{display:flex;align-items:center;gap:10px;flex-shrink:0}.platform-status-badge{font-size:11px;font-weight:500;padding:3px 8px;border-radius:999px;border:1px solid transparent;white-space:nowrap}.platform-status-badge.on{background:var(--c-ok-bg);color:var(--c-ok);border-color:var(--c-ok-bdr)}.platform-status-badge.off{color:var(--c-text-3)}.platform-card-chevron{font-size:12px;color:var(--c-text-3);transition:transform var(--fast)}.platform-card-body{padding:16px 20px 20px;border-top:1px solid var(--c-border);border-radius:0 0 var(--r-l) var(--r-l)}.ai-command-picker{background:var(--c-surface-alt);border:1px solid var(--c-border);border-radius:var(--r-m);padding:12px 14px;display:flex;flex-direction:column;gap:6px;margin-bottom:16px}.ai-command-picker-title{font-family:var(--font-display);font-size:13px;font-weight:600;color:var(--c-text)}.ai-command-picker-hint{font-size:12px;color:var(--c-text-2)}.ai-command-picker .form-select{margin-top:2px}.platform-card-divider{border:0;border-top:1px solid var(--c-border);margin:0 0 16px}.platform-card-hint{font-size:12px;color:var(--c-text-3);line-height:1.5;margin-bottom:16px}.platform-card-help{margin-top:16px;padding:10px 14px;background:var(--c-surface-alt);border-radius:var(--r-m);font-size:12px;color:var(--c-text-2);line-height:1.6}.platform-card-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:16px}.platform-card-ai-tag{position:relative;font-size:11px;font-weight:500;padding:3px 10px;border-radius:999px;background:var(--c-surface-alt);color:var(--c-text-2);border:1px solid var(--c-border);cursor:pointer;white-space:nowrap;transition:all var(--fast)}.platform-card-ai-tag:hover{border-color:var(--c-accent-bdr);color:var(--c-text)}.platform-card-ai-dropdown{position:absolute;top:calc(100% + 4px);right:0;min-width:140px;background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r-m);box-shadow:var(--shadow-m, 0 8px 24px rgba(0, 0, 0, .12));z-index:20;padding:4px}.platform-card-ai-option{display:block;width:100%;text-align:left;background:transparent;border:0;padding:6px 10px;font-size:12px;color:var(--c-text);cursor:pointer;border-radius:var(--r-s, 4px)}.platform-card-ai-option:hover{background:var(--c-surface-alt)}.platform-card-ai-option.active{color:var(--c-accent);font-weight:600}.platform-card-menu{position:relative;flex-shrink:0}.platform-card-menu-btn{background:transparent;border:0;padding:4px 8px;font-size:18px;line-height:1;color:var(--c-text-3);cursor:pointer;border-radius:var(--r-s, 4px);transition:all var(--fast)}.platform-card-menu-btn:hover{background:var(--c-surface-alt);color:var(--c-text)}.platform-card-menu-dropdown{position:absolute;top:calc(100% + 4px);right:0;min-width:160px;background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r-m);box-shadow:var(--shadow-m, 0 8px 24px rgba(0, 0, 0, .12));z-index:20;padding:4px}.platform-card-menu-item{display:block;width:100%;text-align:left;background:transparent;border:0;padding:8px 12px;font-size:13px;color:var(--c-text);cursor:pointer;border-radius:var(--r-s, 4px)}.platform-card-menu-item:hover{background:var(--c-surface-alt)}.platform-card-menu-item.danger{color:var(--c-err, #d33)}.platform-card-menu-item.danger:hover{background:#dc353514}.platform-card-menu-item:disabled{opacity:.4;cursor:not-allowed}.qr-modal-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000073;display:flex;align-items:center;justify-content:center;z-index:1000;padding:16px}.qr-modal{position:relative;background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r-l);box-shadow:var(--shadow-l, 0 12px 40px rgba(0, 0, 0, .2));padding:28px 28px 24px;width:100%;max-width:360px;text-align:center}.qr-modal-close{position:absolute;top:8px;right:12px;background:transparent;border:0;font-size:22px;line-height:1;color:var(--c-text-3);cursor:pointer;padding:4px 8px;border-radius:var(--r-m)}.qr-modal-close:hover{background:var(--c-surface-alt);color:var(--c-text)}.qr-modal-title{font-family:var(--font-display);font-size:16px;font-weight:600;color:var(--c-text);margin-bottom:16px}.qr-modal-img-wrap{display:flex;align-items:center;justify-content:center;margin-bottom:14px}.qr-modal-img{width:220px;height:220px;border:1px solid var(--c-border);border-radius:var(--r-m)}.qr-modal-hint{font-size:13px;color:var(--c-text-2);line-height:1.5;margin-bottom:8px}.qr-modal-status{font-size:13px;color:var(--c-text-2);line-height:1.5}.qr-modal-status.muted{color:var(--c-text-3)}.qr-modal-status.ok{color:var(--c-ok);font-weight:600}.qr-modal-status.err{color:var(--c-err, var(--c-text));font-weight:500;word-break:break-word}.qr-modal-actions{display:flex;gap:8px;justify-content:center;margin-top:16px}.form-group{margin-bottom:14px}.form-group:last-child{margin-bottom:0}.form-label{display:block;font-size:12px;font-weight:500;color:var(--c-text-2);margin-bottom:5px}.form-input,.form-select,.form-textarea{width:100%;padding:8px 10px;font-family:inherit;font-size:13px;color:var(--c-text);background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r-s);transition:border-color var(--fast),box-shadow var(--fast);outline:none}.form-input:focus,.form-select:focus,.form-textarea:focus{border-color:var(--c-accent);box-shadow:0 0 0 3px var(--c-accent-bg)}.form-input.mono,.form-textarea.mono{font-family:var(--font-mono);font-size:12px}.form-textarea{min-height:80px;resize:vertical}.form-hint{font-size:11px;color:var(--c-text-3);margin-top:4px;line-height:1.4}.form-hint a{color:var(--c-accent)}.form-hint code{font-family:var(--font-mono);font-size:10px;padding:1px 4px;background:var(--c-surface-alt);border-radius:3px}.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:7px 14px;font-family:var(--font-body);font-size:13px;font-weight:500;border-radius:var(--r-s);border:1px solid transparent;cursor:pointer;text-decoration:none;transition:all var(--fast);white-space:nowrap}.btn:disabled{opacity:.5;cursor:not-allowed}.btn-p{background:var(--c-accent);color:#fff}.btn-p:hover:not(:disabled){background:var(--c-accent-h);box-shadow:var(--shadow-glow)}.btn-s{background:var(--c-surface);color:var(--c-text);border-color:var(--c-border)}.btn-s:hover:not(:disabled){background:var(--c-surface-alt);border-color:var(--c-border-foc)}.btn-g{background:transparent;color:var(--c-text-2)}.btn-g:hover:not(:disabled){background:var(--c-surface-alt);color:var(--c-text)}.btn-d{background:var(--c-err-bg);color:var(--c-err);border-color:var(--c-err-bdr)}.btn-d:hover:not(:disabled){background:var(--c-err);color:#fff}.btn-w{background:var(--c-warn-bg);color:var(--c-warn);border-color:var(--c-warn-bdr)}.btn-w:hover:not(:disabled){background:var(--c-warn);color:#fff}.btn-sm{padding:5px 10px;font-size:12px}.btn-lg{padding:10px 20px;font-size:14px}.toggle{display:inline-flex;align-items:center;gap:8px;cursor:pointer;flex-shrink:0}.toggle-input{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.toggle-track{position:relative;width:36px;height:20px;background:var(--c-border-foc);border-radius:999px;transition:background var(--fast);flex-shrink:0}.toggle-track:after{content:"";position:absolute;top:2px;left:2px;width:16px;height:16px;background:#fff;border-radius:50%;transition:transform var(--fast);box-shadow:var(--shadow-xs)}.toggle-input:checked+.toggle-track{background:var(--c-accent);box-shadow:0 0 10px var(--c-accent)}.toggle-input:checked+.toggle-track:after{transform:translate(16px)}.tabs{display:inline-flex;gap:2px;padding:3px;background:var(--c-surface-alt);border:1px solid var(--c-border);border-radius:var(--r-m)}.tab{padding:6px 12px;font-size:12px;font-weight:500;color:var(--c-text-2);background:transparent;border:none;border-radius:var(--r-s);cursor:pointer;transition:all var(--fast)}.tab:hover{color:var(--c-text)}.tab.active{background:var(--c-surface);color:var(--c-text);box-shadow:var(--shadow-xs)}.msg{padding:10px 14px;border-radius:var(--r-m);font-size:13px;line-height:1.5}.msg-ok{background:var(--c-ok-bg);color:var(--c-ok)}.msg-err{background:var(--c-err-bg);color:var(--c-err)}.msg-w{background:var(--c-warn-bg);color:var(--c-warn)}.badge{display:inline-flex;align-items:center;padding:2px 8px;font-size:11px;font-weight:500;border-radius:999px}.badge-ok{background:var(--c-ok-bg);color:var(--c-ok)}.badge-err{background:var(--c-err-bg);color:var(--c-err)}.badge-w{background:var(--c-warn-bg);color:var(--c-warn)}.badge-info{background:var(--c-accent-bg);color:var(--c-accent)}.field-tip{font-size:11px;color:var(--c-text-3);line-height:1.45;margin-top:5px}.field-tip a{color:var(--c-accent)}.field-tip code{font-family:var(--font-mono);font-size:10px;padding:1px 4px;background:var(--c-surface-alt);border-radius:3px}.mt-4{margin-top:16px}.mt-6{margin-top:24px}.gap-2{gap:8px}.gap-3{gap:12px}.text-center{text-align:center}.mono{font-family:var(--font-mono)}.hidden{display:none!important}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.flash{margin-bottom:20px}.dark-btn{padding:4px 8px;border-radius:var(--r-s);border:1px solid var(--c-border);background:var(--c-surface);color:var(--c-text-2);cursor:pointer;font-size:14px;line-height:1;transition:all var(--fast);display:inline-flex;align-items:center;justify-content:center}.dark-btn:hover{border-color:var(--c-border-foc);color:var(--c-text)}.config-stack{display:flex;flex-direction:column;gap:16px}.config-card .card-title{font-family:var(--font-mono);font-size:13px}.config-card textarea{width:100%}.wizard-wrap{display:flex;justify-content:center;padding-top:16px}.wizard{width:100%;max-width:580px;background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r-xl);box-shadow:var(--shadow-l);overflow:hidden}.wizard-bar{height:3px;background:var(--c-border)}.wizard-bar-fill{height:100%;background:var(--c-accent);transition:width .4s cubic-bezier(.4,0,.2,1)}.wizard-steps{display:flex;padding:20px 24px 0;gap:0}.wizard-step-ind{flex:1;display:flex;flex-direction:column;align-items:center;gap:6px;position:relative}.wizard-step-ind+.wizard-step-ind:before{content:"";position:absolute;top:12px;right:calc(50% + 16px);width:calc(100% - 32px);height:1px;background:var(--c-border)}.wizard-step-ind.done+.wizard-step-ind:before{background:var(--c-accent)}.wizard-step-dot{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;background:var(--c-surface-alt);color:var(--c-text-3);border:2px solid var(--c-border);position:relative;z-index:1;transition:all var(--norm)}.wizard-step-ind.active .wizard-step-dot{background:var(--c-accent);color:#fff;border-color:var(--c-accent);box-shadow:0 0 12px var(--c-accent)}.wizard-step-ind.done .wizard-step-dot{background:var(--c-ok);color:#fff;border-color:var(--c-ok)}.wizard-step-label{font-size:11px;font-weight:500;color:var(--c-text-3);white-space:nowrap}.wizard-step-ind.active .wizard-step-label{color:var(--c-accent);font-weight:600}.wizard-step-ind.done .wizard-step-label{color:var(--c-ok)}.wizard-body{padding:28px 32px;min-height:260px}.wizard-icon{font-size:36px;margin-bottom:12px}.wizard-title{font-family:var(--font-display);font-size:20px;font-weight:700;color:var(--c-text);margin-bottom:6px;letter-spacing:-.02em}.wizard-desc{font-size:13px;color:var(--c-text-2);line-height:1.6;margin-bottom:24px}.wizard-checklist{display:flex;flex-direction:column;gap:10px}.wizard-check{display:flex;align-items:center;gap:10px;font-size:13px;color:var(--c-text)}.wizard-check-num{width:24px;height:24px;border-radius:50%;background:var(--c-accent-bg);color:var(--c-accent);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;flex-shrink:0}.wizard-chips{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px}.wizard-platform-card{background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r-l);overflow:hidden;transition:border-color var(--fast)}.wizard-platform-card.on{border-color:var(--c-accent)}.wizard-platform-head{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;cursor:pointer;gap:8px}.wizard-platform-head:hover{background:var(--c-surface-alt)}.wizard-platform-meta{display:flex;align-items:center;gap:10px;min-width:0}.wizard-platform-icon{width:36px;height:36px;border-radius:var(--r-m);background:var(--c-surface-alt);display:flex;align-items:center;justify-content:center;font-size:20px;line-height:1;flex-shrink:0}.wizard-platform-title-block{display:flex;flex-direction:column;gap:1px;min-width:0}.wizard-platform-name{font-family:var(--font-display);font-size:14px;font-weight:600}.wizard-platform-desc{font-size:12px;color:var(--c-text-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.wizard-platform-right{display:flex;align-items:center;gap:8px;flex-shrink:0}.wizard-platform-chevron{font-size:12px;color:var(--c-text-3)}.wizard-platform-body{padding:0 16px 16px;border-top:1px solid var(--c-border)}.wizard-cred{background:var(--c-surface-alt);border:1px solid var(--c-border);border-radius:var(--r-l);padding:20px;margin-bottom:14px}.wizard-cred:last-child{margin-bottom:0}.wizard-cred-name{font-family:var(--font-display);font-size:14px;font-weight:600;margin-bottom:14px;color:var(--c-text)}.wizard-radio-group{display:flex;flex-direction:column;gap:8px}.wizard-radio{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border:2px solid var(--c-border);border-radius:var(--r-m);cursor:pointer;transition:all var(--fast)}.wizard-radio:hover{border-color:var(--c-accent-bdr)}.wizard-radio.on{border-color:var(--c-accent);background:var(--c-accent-bg)}.wizard-radio input[type=radio]{margin-top:2px;accent-color:var(--c-accent)}.wizard-radio-body{display:flex;flex-direction:column;gap:2px}.wizard-radio-label{font-size:13px;font-weight:600;color:var(--c-text)}.wizard-radio-desc{font-size:12px;color:var(--c-text-3)}.wizard-review-block{margin-bottom:20px}.wizard-review-head{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--c-text-3);margin-bottom:10px}.wizard-kv{display:flex;justify-content:space-between;align-items:center;padding:7px 0;border-bottom:1px solid var(--c-border);font-size:12px}.wizard-kv span{color:var(--c-text-2)}.wizard-kv code{font-family:var(--font-mono);font-size:11px;color:var(--c-text);background:var(--c-surface-alt);padding:2px 6px;border-radius:4px}.wizard-platforms{display:flex;flex-wrap:wrap;gap:6px}.wizard-platform-pill{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:var(--c-surface-alt);border-radius:999px;font-size:12px;font-weight:500}.wizard-platform-pill .cmd{font-size:10px;padding:1px 6px;border-radius:999px;background:var(--c-accent-bg);color:var(--c-accent);font-weight:500}.wizard-done{text-align:center;padding:20px 0}.wizard-nav{display:flex;align-items:center;padding:16px 32px 24px;gap:10px}.wizard-nav-spacer{flex:1}.wizard-pulse{animation:wiz-pulse 1.2s ease-in-out infinite}@keyframes wiz-pulse{0%,to{opacity:1}50%{opacity:.4}}.conn{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:32px}.conn-card{width:100%;max-width:420px;background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--r-xl);box-shadow:var(--shadow-l);padding:32px}.conn-logo{width:40px;height:40px;border-radius:var(--r-m);background:var(--c-accent);display:flex;align-items:center;justify-content:center;margin-bottom:20px}.conn-logo svg{width:22px;height:22px;color:#fff}.conn-title{font-family:var(--font-display);font-size:20px;font-weight:700;color:var(--c-text);margin-bottom:6px;letter-spacing:-.02em}.conn-desc{font-size:13px;color:var(--c-text-2);margin-bottom:24px;line-height:1.5}.conn-row{display:flex;gap:8px}.conn-input{flex:1;padding:8px 10px;font-family:var(--font-mono);font-size:13px;color:var(--c-text);background:var(--c-surface-alt);border:1px solid var(--c-border);border-radius:var(--r-s);outline:none;transition:border-color var(--fast)}.conn-input:focus{border-color:var(--c-accent);box-shadow:0 0 0 3px var(--c-accent-bg)}.conn-status{font-size:12px;margin-top:10px;color:var(--c-text-3)}.conn-status.ok{color:var(--c-ok)}.conn-status.err{color:var(--c-err)}@media(max-width:1024px){.stats-grid{grid-template-columns:1fr}}@media(max-width:768px){.content{padding:20px 16px 40px}.header{padding:0 16px}.header-toolbar{padding:10px 16px}.platform-grid,.wizard-chips{grid-template-columns:1fr}}
|