dsh-bridge-gateway 0.1.0
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 +250 -0
- package/LICENSE +21 -0
- package/README.en.md +408 -0
- package/README.md +430 -0
- package/client/build.mjs +43 -0
- package/client/client.js +5209 -0
- package/client/index.js +4967 -0
- package/cordis.patch.yml +4 -0
- package/docs/banner.jpg +0 -0
- package/docs/custom-tunnel.md +220 -0
- package/docs/feishu-usage.md +116 -0
- package/docs/fix-tunnel-sse-and-session-list.md +134 -0
- package/docs/platform-abstraction-design.md +473 -0
- package/docs/qq-usage.md +387 -0
- package/docs/screenshots/admin-lock-screen.jpg +0 -0
- package/docs/screenshots/feishu-bot-config.jpg +0 -0
- package/docs/screenshots/feishu-chat.jpg +0 -0
- package/docs/screenshots/lan-access.jpg +0 -0
- package/docs/screenshots/mobile-chat.jpg +0 -0
- package/docs/screenshots/mobile-drawer.jpg +0 -0
- package/docs/screenshots/mobile-remote-settings.jpg +0 -0
- package/docs/screenshots/mobile-settings-im.jpg +0 -0
- package/docs/screenshots/mobile-settings-lan.jpg +0 -0
- package/docs/screenshots/mobile-settings-security.jpg +0 -0
- package/docs/screenshots/mobile-settings-tunnel.jpg +0 -0
- package/docs/screenshots/mobile-workspace-picker.jpg +0 -0
- package/docs/screenshots/qq-bot-config.jpg +0 -0
- package/docs/screenshots/qq-chat.jpg +0 -0
- package/docs/screenshots/qq-group.jpg +0 -0
- package/docs/screenshots/qr-scan.jpg +0 -0
- package/docs/screenshots/remote-auth-login.jpg +0 -0
- package/docs/screenshots/remote-web-mobile.jpg +0 -0
- package/docs/screenshots/security-auth-config.jpg +0 -0
- package/docs/screenshots/telegram-bot-config.jpg +0 -0
- package/docs/screenshots/tunnel-access.jpg +0 -0
- package/docs/screenshots/wechat-bot-config.jpg +0 -0
- package/docs/screenshots/wechat-chat.jpg +0 -0
- package/docs/telegram-usage.md +93 -0
- package/docs/wechat-usage.md +104 -0
- package/lib/auth/login-template.js +382 -0
- package/lib/auth/manager.js +470 -0
- package/lib/bridge-rpc-constants.js +55 -0
- package/lib/bridge-rpc.js +553 -0
- package/lib/cloudflared-manager.mjs +345 -0
- package/lib/feishu/gateway.js +550 -0
- package/lib/feishu/index.js +222 -0
- package/lib/feishu/lark-bundled.mjs +125547 -0
- package/lib/feishu/node.js +409 -0
- package/lib/gateway/cert.js +214 -0
- package/lib/index.js +1963 -0
- package/lib/platform/base.js +156 -0
- package/lib/platform/conversation-bridge.js +1570 -0
- package/lib/platform/index.js +10 -0
- package/lib/platform/manager.js +74 -0
- package/lib/qq/gateway.js +616 -0
- package/lib/qq/index.js +309 -0
- package/lib/qq/node.js +533 -0
- package/lib/security/path-validator.js +208 -0
- package/lib/security/rate-limiter.js +93 -0
- package/lib/telegram/gateway.js +630 -0
- package/lib/telegram/index.js +213 -0
- package/lib/telegram/node.js +351 -0
- package/lib/tunnel-client.mjs +402 -0
- package/lib/wechat/gateway.js +960 -0
- package/lib/wechat/index.js +241 -0
- package/lib/wechat/media.js +281 -0
- package/lib/wechat/node.js +350 -0
- package/package.json +102 -0
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
// DSH Bridge - RPC Interface (server side)
|
|
2
|
+
// Loopback-only RPC methods for browser UI
|
|
3
|
+
|
|
4
|
+
import QRCode from 'qrcode';
|
|
5
|
+
import { BRIDGE_RPC_CHANNEL, BRIDGE_ENDPOINTS } from './bridge-rpc-constants.js';
|
|
6
|
+
import { RateLimiter } from './security/rate-limiter.js';
|
|
7
|
+
|
|
8
|
+
export { BRIDGE_RPC_CHANNEL, BRIDGE_ENDPOINTS };
|
|
9
|
+
|
|
10
|
+
const rpcRateLimiter = new RateLimiter({ maxRequests: 30, windowMs: 60000 });
|
|
11
|
+
|
|
12
|
+
function ok(value) {
|
|
13
|
+
return { ok: true, value };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function fail(code, message, details = {}) {
|
|
17
|
+
const allowedCodes = new Set([
|
|
18
|
+
'bad-request', 'cancelled', 'internal', 'settings-rejected', 'command-error'
|
|
19
|
+
]);
|
|
20
|
+
const safeCode = allowedCodes.has(code) ? code : 'bad-request';
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
error: {
|
|
24
|
+
code: safeCode,
|
|
25
|
+
message,
|
|
26
|
+
details: { issues: [{ message }], ...details },
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 把登录态里的二维码载荷渲染成浏览器可展示的 dataURL(带缓存,避免重复生成)
|
|
32
|
+
async function renderQr(loginState) {
|
|
33
|
+
if (!loginState?.qrPayload) return null;
|
|
34
|
+
const cacheKey = `${loginState.qrKind}:${loginState.qrPayload.slice(0, 80)}`;
|
|
35
|
+
if (renderQr.cache && renderQr.cache.key === cacheKey) return renderQr.cache.url;
|
|
36
|
+
let url = null;
|
|
37
|
+
const payload = loginState.qrPayload;
|
|
38
|
+
if (loginState.qrKind === 'img') {
|
|
39
|
+
url = /^data:/i.test(payload) ? payload : `data:image/png;base64,${payload}`;
|
|
40
|
+
} else {
|
|
41
|
+
try {
|
|
42
|
+
url = await QRCode.toDataURL(payload, {
|
|
43
|
+
width: 300, margin: 2, color: { dark: '#1F2421', light: '#FFFFFF' },
|
|
44
|
+
});
|
|
45
|
+
} catch { url = null; }
|
|
46
|
+
}
|
|
47
|
+
renderQr.cache = { key: cacheKey, url };
|
|
48
|
+
return url;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 归一化 wechat 状态返回:把 loginState 里的 qrPayload 渲染成 qr dataURL。 */
|
|
52
|
+
async function wechatStatusValue(wechatService, logger) {
|
|
53
|
+
const status = wechatService.getStatus();
|
|
54
|
+
const qr = await renderQr(status.login).catch((err) => {
|
|
55
|
+
logger.warn('dsh-bridge: render wechat qr failed: %s', err?.message ?? err);
|
|
56
|
+
return null;
|
|
57
|
+
});
|
|
58
|
+
return { ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function checkAdminAuth(authManager, payload) {
|
|
62
|
+
if (!authManager) return null;
|
|
63
|
+
if (authManager.adminPolicy === 'open') return null;
|
|
64
|
+
// 若系统尚未设置任何管理密码或访客密码,允许免密管理
|
|
65
|
+
const hasAnyPassword = authManager.hasAdminPassword || authManager.hasPassword;
|
|
66
|
+
if (!hasAnyPassword) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
// 已设置密码时,必须提供经服务端校验有效的 adminToken(绝不依赖客户端自称的 isLocalhost)
|
|
70
|
+
if (payload?.adminToken && authManager.validateAdminSession(payload.adminToken)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return fail('bad-request', '操作已被拦截:需要管理员权限,请先在控制台输入管理密码解锁');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function installBridgeRpc(ctx, { service, authManager, wechat, platformManager, logger, saveCustomTunnelConfig, exportBackup, importBackup }) {
|
|
77
|
+
if (!ctx?.connection?.rpc?.handle) {
|
|
78
|
+
logger.warn('dsh-bridge: Connection RPC unavailable — UI will not work');
|
|
79
|
+
return () => {};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return ctx.connection.rpc.handle(
|
|
83
|
+
BRIDGE_RPC_CHANNEL,
|
|
84
|
+
async (endpoint, payload = {}, signal) => {
|
|
85
|
+
if (signal?.aborted) return fail('cancelled', 'Request was cancelled');
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
if (endpoint === BRIDGE_ENDPOINTS.getStatus) {
|
|
89
|
+
const isAdmin = checkAdminAuth(authManager, payload) === null;
|
|
90
|
+
const status = await service.getStatus({ adminAuthValid: isAdmin });
|
|
91
|
+
return ok(status);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---- 访问安全认证 ----
|
|
95
|
+
|
|
96
|
+
if (endpoint === BRIDGE_ENDPOINTS.authGetStatus) {
|
|
97
|
+
if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
|
|
98
|
+
const isAdmin = checkAdminAuth(authManager, payload) === null;
|
|
99
|
+
return ok(authManager.getStatus({ masked: !isAdmin }));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (endpoint === BRIDGE_ENDPOINTS.authUpdateConfig) {
|
|
103
|
+
if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
|
|
104
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
105
|
+
if (adminErr) return adminErr;
|
|
106
|
+
|
|
107
|
+
const { enabled, mode, scope, adminPolicy, password, adminPassword } = payload;
|
|
108
|
+
if (enabled != null) await authManager.setEnabled(enabled);
|
|
109
|
+
if (mode != null) await authManager.setMode(mode);
|
|
110
|
+
if (scope != null) await authManager.setScope(scope);
|
|
111
|
+
if (adminPolicy != null) await authManager.setAdminPolicy(adminPolicy);
|
|
112
|
+
if (password !== undefined) await authManager.setPassword(password);
|
|
113
|
+
if (adminPassword !== undefined) await authManager.setAdminPassword(adminPassword);
|
|
114
|
+
return ok(authManager.getStatus({ masked: false }));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (endpoint === BRIDGE_ENDPOINTS.authRegenerateToken) {
|
|
118
|
+
if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
|
|
119
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
120
|
+
if (adminErr) return adminErr;
|
|
121
|
+
|
|
122
|
+
await authManager.regenerateSecretToken();
|
|
123
|
+
return ok(authManager.getStatus({ masked: false }));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (endpoint === BRIDGE_ENDPOINTS.authAdminUnlock) {
|
|
127
|
+
if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
|
|
128
|
+
const { password } = payload;
|
|
129
|
+
const res = authManager.unlockAdmin(password);
|
|
130
|
+
if (res.ok) return ok({ adminToken: res.adminToken });
|
|
131
|
+
return fail('bad-request', res.error || '管理员密码错误');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (endpoint === BRIDGE_ENDPOINTS.authAdminLock) {
|
|
135
|
+
if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
|
|
136
|
+
if (payload?.adminToken) authManager.revokeAdminSession(payload.adminToken);
|
|
137
|
+
return ok({ locked: true });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (endpoint === BRIDGE_ENDPOINTS.saveCustomTunnelConfig) {
|
|
141
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
142
|
+
if (adminErr) return adminErr;
|
|
143
|
+
|
|
144
|
+
const { serverUrl = '', accessToken = '' } = payload;
|
|
145
|
+
await saveCustomTunnelConfig(serverUrl.trim(), accessToken.trim());
|
|
146
|
+
const status = await service.getStatus();
|
|
147
|
+
return ok(status);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (endpoint === BRIDGE_ENDPOINTS.saveCloudflaredConfig) {
|
|
151
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
152
|
+
if (adminErr) return adminErr;
|
|
153
|
+
|
|
154
|
+
const { token = '', hostname = '' } = payload;
|
|
155
|
+
await service.saveCloudflaredConfig({ token, hostname });
|
|
156
|
+
const status = await service.getStatus();
|
|
157
|
+
return ok(status);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (endpoint === BRIDGE_ENDPOINTS.setTunnelAutoStart) {
|
|
161
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
162
|
+
if (adminErr) return adminErr;
|
|
163
|
+
|
|
164
|
+
const { tunnel, autoStart } = payload;
|
|
165
|
+
await service.setTunnelAutoStart({ tunnel, autoStart });
|
|
166
|
+
const status = await service.getStatus();
|
|
167
|
+
return ok(status);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (endpoint === BRIDGE_ENDPOINTS.setLanIp) {
|
|
171
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
172
|
+
if (adminErr) return adminErr;
|
|
173
|
+
|
|
174
|
+
const { ip } = payload;
|
|
175
|
+
const status = await service.setLanIp({ ip });
|
|
176
|
+
return ok(status);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---- 公网直连网关(0.0.0.0:port HTTPS + 强制登录门禁)----
|
|
180
|
+
|
|
181
|
+
if (endpoint === BRIDGE_ENDPOINTS.gatewayGetStatus) {
|
|
182
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
183
|
+
if (adminErr) return adminErr;
|
|
184
|
+
return ok(service.gatewayStatus());
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (endpoint === BRIDGE_ENDPOINTS.gatewaySaveConfig) {
|
|
188
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
189
|
+
if (adminErr) return adminErr;
|
|
190
|
+
const { port, autoStart } = payload;
|
|
191
|
+
await service.saveGatewayConfig({ port, autoStart });
|
|
192
|
+
return ok(service.getStatus());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (endpoint === BRIDGE_ENDPOINTS.gatewaySetAutoStart) {
|
|
196
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
197
|
+
if (adminErr) return adminErr;
|
|
198
|
+
await service.setGatewayAutoStart({ autoStart: payload?.autoStart });
|
|
199
|
+
return ok(service.getStatus());
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (endpoint === BRIDGE_ENDPOINTS.gatewayStart) {
|
|
203
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
204
|
+
if (adminErr) return adminErr;
|
|
205
|
+
try {
|
|
206
|
+
await service.startGateway();
|
|
207
|
+
return ok(service.getStatus());
|
|
208
|
+
} catch (err) {
|
|
209
|
+
logger.error('Failed to start public gateway: %s', err.message);
|
|
210
|
+
return fail('bad-request', err.message);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (endpoint === BRIDGE_ENDPOINTS.gatewayStop) {
|
|
215
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
216
|
+
if (adminErr) return adminErr;
|
|
217
|
+
await service.stopGateway();
|
|
218
|
+
return ok(service.getStatus());
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (endpoint === BRIDGE_ENDPOINTS.startCustomTunnel) {
|
|
222
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
223
|
+
if (adminErr) return adminErr;
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
await service.startCustomTunnel();
|
|
227
|
+
const status = await service.getStatus();
|
|
228
|
+
return ok(status);
|
|
229
|
+
} catch (err) {
|
|
230
|
+
logger.error('Failed to start custom tunnel: %s', err.message);
|
|
231
|
+
return fail('bad-request', err.message);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (endpoint === BRIDGE_ENDPOINTS.stopCustomTunnel) {
|
|
236
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
237
|
+
if (adminErr) return adminErr;
|
|
238
|
+
|
|
239
|
+
service.stopCustomTunnel();
|
|
240
|
+
const status = await service.getStatus();
|
|
241
|
+
return ok(status);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (endpoint === BRIDGE_ENDPOINTS.startCloudflared) {
|
|
245
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
246
|
+
if (adminErr) return adminErr;
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
await service.startCloudflared();
|
|
250
|
+
const status = await service.getStatus();
|
|
251
|
+
return ok(status);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
logger.error('Failed to start cloudflared: %s', err.message);
|
|
254
|
+
return fail('bad-request', err.message);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (endpoint === BRIDGE_ENDPOINTS.stopCloudflared) {
|
|
259
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
260
|
+
if (adminErr) return adminErr;
|
|
261
|
+
|
|
262
|
+
service.stopCloudflared();
|
|
263
|
+
const status = await service.getStatus();
|
|
264
|
+
return ok(status);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (endpoint === BRIDGE_ENDPOINTS.resetCloudflared) {
|
|
268
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
269
|
+
if (adminErr) return adminErr;
|
|
270
|
+
|
|
271
|
+
await service.resetCloudflared();
|
|
272
|
+
const status = await service.getStatus();
|
|
273
|
+
return ok(status);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (endpoint === BRIDGE_ENDPOINTS.checkVersion) {
|
|
277
|
+
const result = await service.checkVersion();
|
|
278
|
+
return ok(result);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (endpoint === BRIDGE_ENDPOINTS.upgradePlugin) {
|
|
282
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
283
|
+
if (adminErr) return adminErr;
|
|
284
|
+
|
|
285
|
+
const result = await service.upgradePlugin(payload);
|
|
286
|
+
return ok(result);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (endpoint === BRIDGE_ENDPOINTS.restartDsh) {
|
|
290
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
291
|
+
if (adminErr) return adminErr;
|
|
292
|
+
|
|
293
|
+
const result = await service.restartDsh();
|
|
294
|
+
return ok(result);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (endpoint === BRIDGE_ENDPOINTS.exportBackup) {
|
|
298
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
299
|
+
if (adminErr) return adminErr;
|
|
300
|
+
|
|
301
|
+
if (!exportBackup) return fail('bad-request', '备份导出服务不可用');
|
|
302
|
+
const backup = await exportBackup();
|
|
303
|
+
return ok(backup);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (endpoint === BRIDGE_ENDPOINTS.importBackup) {
|
|
307
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
308
|
+
if (adminErr) return adminErr;
|
|
309
|
+
|
|
310
|
+
if (!importBackup) return fail('bad-request', '备份导入服务不可用');
|
|
311
|
+
const result = await importBackup(payload?.backup);
|
|
312
|
+
const status = await service.getStatus({ adminAuthValid: true });
|
|
313
|
+
return ok({ result, status });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (endpoint === BRIDGE_ENDPOINTS.diagnoseNetwork) {
|
|
317
|
+
const result = await service.diagnoseNetwork();
|
|
318
|
+
return ok(result);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (endpoint === BRIDGE_ENDPOINTS.getSystemMetrics) {
|
|
322
|
+
const metrics = service.getSystemMetrics();
|
|
323
|
+
return ok(metrics);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ---- 远程工作区管理与目录浏览 ----
|
|
327
|
+
|
|
328
|
+
if (endpoint === BRIDGE_ENDPOINTS.listRemoteDirectories) {
|
|
329
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
330
|
+
if (adminErr) return adminErr;
|
|
331
|
+
|
|
332
|
+
const clientKey = payload?.clientIp || payload?.adminToken || 'default';
|
|
333
|
+
const rateCheck = rpcRateLimiter.check(clientKey, 30);
|
|
334
|
+
if (!rateCheck.allowed) {
|
|
335
|
+
return fail('bad-request', `请求过于频繁,请等待 ${rateCheck.retryAfterSec} 秒后再试`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const result = await service.listRemoteDirectories(payload?.path);
|
|
339
|
+
return ok(result);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (endpoint === BRIDGE_ENDPOINTS.addRemoteWorkspace) {
|
|
343
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
344
|
+
if (adminErr) return adminErr;
|
|
345
|
+
|
|
346
|
+
const clientKey = payload?.clientIp || payload?.adminToken || 'default';
|
|
347
|
+
const rateCheck = rpcRateLimiter.check(clientKey, 20);
|
|
348
|
+
if (!rateCheck.allowed) {
|
|
349
|
+
return fail('bad-request', `添加工作区请求过于频繁,请等待 ${rateCheck.retryAfterSec} 秒后再试`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const result = await service.addWorkspace(payload?.path);
|
|
353
|
+
return ok(result);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (endpoint === BRIDGE_ENDPOINTS.listWorkspaces) {
|
|
357
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
358
|
+
if (adminErr) return adminErr;
|
|
359
|
+
|
|
360
|
+
const result = await service.getWorkspaces();
|
|
361
|
+
return ok(result);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ---- 平台管理器(多 IM 平台)----
|
|
365
|
+
|
|
366
|
+
if (endpoint === BRIDGE_ENDPOINTS.listPlatforms) {
|
|
367
|
+
if (!platformManager) return ok({});
|
|
368
|
+
// 每个平台的 login.qrPayload 渲染为 dataURL 后返回
|
|
369
|
+
const raw = platformManager.getStatus();
|
|
370
|
+
const out = {};
|
|
371
|
+
for (const [id, status] of Object.entries(raw)) {
|
|
372
|
+
let qr = null;
|
|
373
|
+
try { qr = await renderQr(status.login).catch(() => null); } catch { /* ignore */ }
|
|
374
|
+
out[id] = { ...status, login: { ...(status.login ?? {}), qr, qrPayload: undefined, qrKind: undefined } };
|
|
375
|
+
}
|
|
376
|
+
return ok(out);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ---- 平台操作(统一接口)----
|
|
380
|
+
|
|
381
|
+
if (endpoint === BRIDGE_ENDPOINTS.platformLogin) {
|
|
382
|
+
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
383
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
384
|
+
if (adminErr) return adminErr;
|
|
385
|
+
|
|
386
|
+
const { platformId, qrType } = payload;
|
|
387
|
+
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
388
|
+
const platform = platformManager.get(platformId);
|
|
389
|
+
if (!platform) return fail('bad-request', `平台未注册: ${platformId}`);
|
|
390
|
+
const result = await platform.login({ qrType });
|
|
391
|
+
if (!result.ok) return fail('bad-request', result.error ?? '登录启动失败');
|
|
392
|
+
const status = platform.getStatus();
|
|
393
|
+
const qr = await renderQr(status.login).catch(() => null);
|
|
394
|
+
return ok({ ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (endpoint === BRIDGE_ENDPOINTS.platformSetAllowFrom) {
|
|
398
|
+
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
399
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
400
|
+
if (adminErr) return adminErr;
|
|
401
|
+
|
|
402
|
+
const { platformId, allowFrom } = payload;
|
|
403
|
+
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
404
|
+
const platform = platformManager.get(platformId);
|
|
405
|
+
if (!platform) return fail('bad-request', `平台未注册: ${platformId}`);
|
|
406
|
+
await platform.setAllowFrom(allowFrom);
|
|
407
|
+
const status = platform.getStatus();
|
|
408
|
+
const qr = await renderQr(status.login).catch(() => null);
|
|
409
|
+
return ok({ ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (endpoint === BRIDGE_ENDPOINTS.platformSetConfig) {
|
|
413
|
+
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
414
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
415
|
+
if (adminErr) return adminErr;
|
|
416
|
+
|
|
417
|
+
const { platformId, ...config } = payload;
|
|
418
|
+
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
419
|
+
const platform = platformManager.get(platformId);
|
|
420
|
+
if (!platform) return fail('bad-request', `平台未注册: ${platformId}`);
|
|
421
|
+
await platform.setConfig(config);
|
|
422
|
+
const status = platform.getStatus();
|
|
423
|
+
const qr = await renderQr(status.login).catch(() => null);
|
|
424
|
+
return ok({ ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } });
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (endpoint === BRIDGE_ENDPOINTS.platformStop) {
|
|
428
|
+
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
429
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
430
|
+
if (adminErr) return adminErr;
|
|
431
|
+
|
|
432
|
+
const { platformId } = payload;
|
|
433
|
+
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
434
|
+
const platform = platformManager.get(platformId);
|
|
435
|
+
if (!platform) return fail('bad-request', `平台未注册: ${platformId}`);
|
|
436
|
+
await platform.stop();
|
|
437
|
+
const status = platform.getStatus();
|
|
438
|
+
const qr = await renderQr(status.login).catch(() => null);
|
|
439
|
+
return ok({ ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } });
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (endpoint === BRIDGE_ENDPOINTS.platformStart) {
|
|
443
|
+
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
444
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
445
|
+
if (adminErr) return adminErr;
|
|
446
|
+
|
|
447
|
+
const { platformId } = payload;
|
|
448
|
+
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
449
|
+
const platform = platformManager.get(platformId);
|
|
450
|
+
if (!platform) return fail('bad-request', `平台未注册: ${platformId}`);
|
|
451
|
+
await platform.start();
|
|
452
|
+
const status = platform.getStatus();
|
|
453
|
+
const qr = await renderQr(status.login).catch(() => null);
|
|
454
|
+
return ok({ ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (endpoint === BRIDGE_ENDPOINTS.platformUnbind) {
|
|
458
|
+
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
459
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
460
|
+
if (adminErr) return adminErr;
|
|
461
|
+
|
|
462
|
+
const { platformId } = payload;
|
|
463
|
+
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
464
|
+
const platform = platformManager.get(platformId);
|
|
465
|
+
if (!platform) return fail('bad-request', `平台未注册: ${platformId}`);
|
|
466
|
+
await platform.unbind();
|
|
467
|
+
const status = platform.getStatus();
|
|
468
|
+
const qr = await renderQr(status.login).catch(() => null);
|
|
469
|
+
return ok({ ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } });
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ---- 微信 Bot(v1.x 向后兼容别名,deprecated)----
|
|
473
|
+
|
|
474
|
+
if (endpoint === BRIDGE_ENDPOINTS.wechatGetStatus) {
|
|
475
|
+
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
476
|
+
const value = await wechatStatusValue(wechat, logger);
|
|
477
|
+
return ok(value);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (endpoint === BRIDGE_ENDPOINTS.wechatLogin) {
|
|
481
|
+
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
482
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
483
|
+
if (adminErr) return adminErr;
|
|
484
|
+
|
|
485
|
+
const { qrType } = payload;
|
|
486
|
+
const result = await wechat.login({ qrType });
|
|
487
|
+
if (!result.ok) return fail('bad-request', result.error ?? '登录启动失败');
|
|
488
|
+
const value = await wechatStatusValue(wechat, logger);
|
|
489
|
+
return ok(value);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (endpoint === BRIDGE_ENDPOINTS.wechatSetAllowFrom) {
|
|
493
|
+
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
494
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
495
|
+
if (adminErr) return adminErr;
|
|
496
|
+
|
|
497
|
+
await wechat.setAllowFrom(payload.allowFrom);
|
|
498
|
+
const value = await wechatStatusValue(wechat, logger);
|
|
499
|
+
return ok(value);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (endpoint === BRIDGE_ENDPOINTS.wechatSetConfig) {
|
|
503
|
+
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
504
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
505
|
+
if (adminErr) return adminErr;
|
|
506
|
+
|
|
507
|
+
await wechat.setConfig(payload);
|
|
508
|
+
const value = await wechatStatusValue(wechat, logger);
|
|
509
|
+
return ok(value);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (endpoint === BRIDGE_ENDPOINTS.wechatStop) {
|
|
513
|
+
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
514
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
515
|
+
if (adminErr) return adminErr;
|
|
516
|
+
|
|
517
|
+
await wechat.stop();
|
|
518
|
+
const value = await wechatStatusValue(wechat, logger);
|
|
519
|
+
return ok(value);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (endpoint === BRIDGE_ENDPOINTS.wechatStart) {
|
|
523
|
+
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
524
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
525
|
+
if (adminErr) return adminErr;
|
|
526
|
+
|
|
527
|
+
await wechat.gateway.start().catch((err) => {
|
|
528
|
+
logger.error('wechat start enabled: %s', err?.message ?? err);
|
|
529
|
+
});
|
|
530
|
+
const value = await wechatStatusValue(wechat, logger);
|
|
531
|
+
return ok(value);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (endpoint === BRIDGE_ENDPOINTS.wechatUnbind) {
|
|
535
|
+
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
536
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
537
|
+
if (adminErr) return adminErr;
|
|
538
|
+
|
|
539
|
+
await wechat.unbind();
|
|
540
|
+
const value = await wechatStatusValue(wechat, logger);
|
|
541
|
+
return ok(value);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return fail('bad-request', `Unknown endpoint: ${endpoint}`);
|
|
545
|
+
} catch (err) {
|
|
546
|
+
logger.error('RPC endpoint %s failed: %s', endpoint, err.message);
|
|
547
|
+
return fail('bad-request', err.message);
|
|
548
|
+
}
|
|
549
|
+
},
|
|
550
|
+
{ authority: 'loopback' }
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|