@lanbaolu/dsh-wechat-bridge 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/LICENSE +21 -0
- package/README.md +152 -0
- package/cordis.patch.yml +12 -0
- package/docs/feasibility-plan.md +62 -0
- package/lib/bridge/commands/handlers.js +155 -0
- package/lib/bridge/commands/handlers.js.map +1 -0
- package/lib/bridge/commands/router.js +52 -0
- package/lib/bridge/commands/router.js.map +1 -0
- package/lib/bridge/config.js +40 -0
- package/lib/bridge/config.js.map +1 -0
- package/lib/bridge/constants.js +14 -0
- package/lib/bridge/constants.js.map +1 -0
- package/lib/bridge/dsh-client.js +100 -0
- package/lib/bridge/dsh-client.js.map +1 -0
- package/lib/bridge/logger.js +75 -0
- package/lib/bridge/logger.js.map +1 -0
- package/lib/bridge/main.js +546 -0
- package/lib/bridge/main.js.map +1 -0
- package/lib/bridge/pending-queue.js +55 -0
- package/lib/bridge/pending-queue.js.map +1 -0
- package/lib/bridge/session.js +84 -0
- package/lib/bridge/session.js.map +1 -0
- package/lib/bridge/store.js +38 -0
- package/lib/bridge/store.js.map +1 -0
- package/lib/bridge/wechat/accounts.js +50 -0
- package/lib/bridge/wechat/accounts.js.map +1 -0
- package/lib/bridge/wechat/api.js +197 -0
- package/lib/bridge/wechat/api.js.map +1 -0
- package/lib/bridge/wechat/cdn.js +45 -0
- package/lib/bridge/wechat/cdn.js.map +1 -0
- package/lib/bridge/wechat/crypto.js +17 -0
- package/lib/bridge/wechat/crypto.js.map +1 -0
- package/lib/bridge/wechat/login.js +139 -0
- package/lib/bridge/wechat/login.js.map +1 -0
- package/lib/bridge/wechat/media.js +138 -0
- package/lib/bridge/wechat/media.js.map +1 -0
- package/lib/bridge/wechat/monitor.js +104 -0
- package/lib/bridge/wechat/monitor.js.map +1 -0
- package/lib/bridge/wechat/send.js +172 -0
- package/lib/bridge/wechat/send.js.map +1 -0
- package/lib/bridge/wechat/sync-buf.js +11 -0
- package/lib/bridge/wechat/sync-buf.js.map +1 -0
- package/lib/bridge/wechat/types.js +35 -0
- package/lib/bridge/wechat/types.js.map +1 -0
- package/lib/bridge/wechat/upload.js +111 -0
- package/lib/bridge/wechat/upload.js.map +1 -0
- package/lib/client/Panel.js +205 -0
- package/lib/client/Panel.js.map +1 -0
- package/lib/client/index.js +11 -0
- package/lib/client/index.js.map +1 -0
- package/lib/client.js +376 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +828 -0
- package/lib/index.js.map +1 -0
- package/lib/types/bridge/commands/handlers.d.ts +18 -0
- package/lib/types/bridge/commands/router.d.ts +26 -0
- package/lib/types/bridge/config.d.ts +7 -0
- package/lib/types/bridge/constants.d.ts +10 -0
- package/lib/types/bridge/dsh-client.d.ts +39 -0
- package/lib/types/bridge/logger.d.ts +13 -0
- package/lib/types/bridge/main.d.ts +1 -0
- package/lib/types/bridge/pending-queue.d.ts +10 -0
- package/lib/types/bridge/session.d.ts +22 -0
- package/lib/types/bridge/store.d.ts +11 -0
- package/lib/types/bridge/wechat/accounts.d.ts +14 -0
- package/lib/types/bridge/wechat/api.d.ts +32 -0
- package/lib/types/bridge/wechat/cdn.d.ts +2 -0
- package/lib/types/bridge/wechat/crypto.d.ts +4 -0
- package/lib/types/bridge/wechat/login.d.ts +31 -0
- package/lib/types/bridge/wechat/media.d.ts +20 -0
- package/lib/types/bridge/wechat/monitor.d.ts +10 -0
- package/lib/types/bridge/wechat/send.d.ts +6 -0
- package/lib/types/bridge/wechat/sync-buf.d.ts +2 -0
- package/lib/types/bridge/wechat/types.d.ts +135 -0
- package/lib/types/bridge/wechat/upload.d.ts +11 -0
- package/lib/types/client/Panel.d.ts +3 -0
- package/lib/types/client/index.d.ts +9 -0
- package/lib/types/index.d.ts +39 -0
- package/package.json +92 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lanbaolu/dsh-wechat-bridge — DSH 微信桥接插件(hybrid)。
|
|
3
|
+
*
|
|
4
|
+
* Host 侧:
|
|
5
|
+
* - 启动一个仅监听 127.0.0.1 的内部 HTTP + SSE 服务,供桥接守护进程调用;
|
|
6
|
+
* - 用 DSH 自身 Agent(ctx.agents)处理微信转发来的消息;
|
|
7
|
+
* - 提供 wechat_bridge_* 工具与可选的 Web 面板同源路由。
|
|
8
|
+
*
|
|
9
|
+
* 三端通用:Windows / macOS / Linux 都使用纯 Node 进程管理(PID 文件 +
|
|
10
|
+
* spawn/kill),不依赖 launchd/systemd。
|
|
11
|
+
*/
|
|
12
|
+
import { createServer } from 'node:http';
|
|
13
|
+
import { randomBytes } from 'node:crypto';
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, appendFileSync, unlinkSync, chmodSync } from 'node:fs';
|
|
16
|
+
import { join, dirname, resolve } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import z from '@deepseek-ai/schemastery';
|
|
20
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
21
|
+
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
22
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
23
|
+
import { startQrLogin, checkQrStatus } from './bridge/wechat/login.js';
|
|
24
|
+
export const name = '@lanbaolu/dsh-wechat-bridge';
|
|
25
|
+
/** Host services the plugin needs. `webServer` is optional (headless profiles). */
|
|
26
|
+
export const inject = ['tools', 'agents', 'agentDefaultModel'];
|
|
27
|
+
export const Config = z.object({
|
|
28
|
+
dataDir: z.string().default(''),
|
|
29
|
+
host: z.string().default('127.0.0.1'),
|
|
30
|
+
port: z.number().min(0).max(65535).default(0),
|
|
31
|
+
autoStart: z.boolean().default(false),
|
|
32
|
+
provider: z.string().default(''),
|
|
33
|
+
model: z.string().default(''),
|
|
34
|
+
workingDirectory: z.string().default(''),
|
|
35
|
+
});
|
|
36
|
+
const PLUGIN_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
37
|
+
function defaultDataDir() {
|
|
38
|
+
return process.env.DSH_HOME || join(homedir(), '.dsh');
|
|
39
|
+
}
|
|
40
|
+
export function apply(ctx, config) {
|
|
41
|
+
const dshHome = process.env.DSH_HOME || join(homedir(), '.dsh');
|
|
42
|
+
const dataDir = config.dataDir || join(dshHome, 'wechat-bridge');
|
|
43
|
+
const daemonLogPath = join(dataDir, 'logs', 'daemon.log');
|
|
44
|
+
const pidPath = join(dataDir, 'daemon.pid');
|
|
45
|
+
mkdirSync(dataDir, { recursive: true });
|
|
46
|
+
mkdirSync(join(dataDir, 'logs'), { recursive: true });
|
|
47
|
+
const token = randomBytes(24).toString('hex');
|
|
48
|
+
const pluginLogPath = join(dataDir, 'plugin.log');
|
|
49
|
+
function debugLog(message, data) {
|
|
50
|
+
try {
|
|
51
|
+
mkdirSync(dataDir, { recursive: true });
|
|
52
|
+
const line = `[${new Date().toISOString()}] ${message}${data === undefined ? '' : ' ' + JSON.stringify(data)}\n`;
|
|
53
|
+
appendFileSync(pluginLogPath, line, 'utf8');
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// ignore
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const agents = new Map();
|
|
60
|
+
/** Maps the WeChat-facing account id (used by the daemon) to the real DSH session id. */
|
|
61
|
+
const sessionIds = new Map();
|
|
62
|
+
const activeSessionIds = new Set();
|
|
63
|
+
const creating = new Map();
|
|
64
|
+
const streamClients = new Map();
|
|
65
|
+
let bridgeChild;
|
|
66
|
+
let bridgeStartedAt;
|
|
67
|
+
let internalServer;
|
|
68
|
+
let internalPort = 0;
|
|
69
|
+
let pendingSetup;
|
|
70
|
+
// -------------------------------------------------------------------------
|
|
71
|
+
// DSH session id persistence (cross-restart continuation)
|
|
72
|
+
// -------------------------------------------------------------------------
|
|
73
|
+
const sessionIdMapPath = join(dataDir, 'session-ids.json');
|
|
74
|
+
function loadSessionIdMap() {
|
|
75
|
+
try {
|
|
76
|
+
const raw = JSON.parse(readFileSync(sessionIdMapPath, 'utf8'));
|
|
77
|
+
return raw && typeof raw === 'object' ? raw : {};
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function saveSessionIdMap(map) {
|
|
84
|
+
mkdirSync(dataDir, { recursive: true });
|
|
85
|
+
writeFileSync(sessionIdMapPath, JSON.stringify(map, null, 2) + '\n', 'utf8');
|
|
86
|
+
}
|
|
87
|
+
function persistSessionId(accountId, dshSessionId) {
|
|
88
|
+
const map = loadSessionIdMap();
|
|
89
|
+
map[accountId] = dshSessionId;
|
|
90
|
+
saveSessionIdMap(map);
|
|
91
|
+
}
|
|
92
|
+
function removePersistedSessionId(accountId) {
|
|
93
|
+
const map = loadSessionIdMap();
|
|
94
|
+
if (accountId in map) {
|
|
95
|
+
delete map[accountId];
|
|
96
|
+
saveSessionIdMap(map);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function newDshSessionId(accountId) {
|
|
100
|
+
return `wb-${accountId}-${Date.now()}-${randomBytes(4).toString('hex')}`;
|
|
101
|
+
}
|
|
102
|
+
// -------------------------------------------------------------------------
|
|
103
|
+
// Agent management
|
|
104
|
+
// -------------------------------------------------------------------------
|
|
105
|
+
async function ensureAgent(accountId, input) {
|
|
106
|
+
const existing = agents.get(accountId);
|
|
107
|
+
if (existing)
|
|
108
|
+
return existing;
|
|
109
|
+
const pending = creating.get(accountId);
|
|
110
|
+
if (pending)
|
|
111
|
+
return pending;
|
|
112
|
+
// Programmatic agents do not inherit agentDefaultModel automatically, so we
|
|
113
|
+
// resolve the deployment's default model selection explicitly.
|
|
114
|
+
const task = (async () => {
|
|
115
|
+
const selection = ctx.agentDefaultModel?.currentSelection();
|
|
116
|
+
const provider = config.provider || selection?.provider;
|
|
117
|
+
const model = input?.model || config.model || selection?.model;
|
|
118
|
+
const agentOptions = {
|
|
119
|
+
...(provider ? { provider } : {}),
|
|
120
|
+
...(model ? { model } : {}),
|
|
121
|
+
};
|
|
122
|
+
// Resume the persisted DSH session when possible; create a fresh one only
|
|
123
|
+
// when there is no mapping, the old log is gone/corrupt, or the requested
|
|
124
|
+
// workspace differs from the persisted session's cwd.
|
|
125
|
+
let dshSessionId = sessionIds.get(accountId);
|
|
126
|
+
let handle;
|
|
127
|
+
let resumed = false;
|
|
128
|
+
if (!dshSessionId) {
|
|
129
|
+
const sessionMap = loadSessionIdMap();
|
|
130
|
+
dshSessionId = sessionMap[accountId] || undefined;
|
|
131
|
+
}
|
|
132
|
+
if (dshSessionId) {
|
|
133
|
+
try {
|
|
134
|
+
const candidate = await ctx.agents.resume({
|
|
135
|
+
resumeSessionId: SessionId(dshSessionId),
|
|
136
|
+
agentOptions,
|
|
137
|
+
});
|
|
138
|
+
const persistedCwd = candidate.agent.session.header.cwd;
|
|
139
|
+
if (input?.cwd && persistedCwd && resolve(input.cwd) !== resolve(persistedCwd)) {
|
|
140
|
+
debugLog('resume cwd mismatch, create new', {
|
|
141
|
+
accountId,
|
|
142
|
+
dshSessionId,
|
|
143
|
+
requested: resolve(input.cwd),
|
|
144
|
+
persisted: resolve(persistedCwd),
|
|
145
|
+
});
|
|
146
|
+
await candidate.dispose();
|
|
147
|
+
handle = undefined;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
handle = candidate;
|
|
151
|
+
resumed = true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
debugLog('resume failed, create new', {
|
|
156
|
+
accountId,
|
|
157
|
+
dshSessionId,
|
|
158
|
+
error: err instanceof Error ? err.message : String(err),
|
|
159
|
+
});
|
|
160
|
+
handle = undefined;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!handle) {
|
|
164
|
+
dshSessionId = newDshSessionId(accountId);
|
|
165
|
+
debugLog('ensureAgent create', { accountId, dshSessionId, provider, model, selection, resumed: false });
|
|
166
|
+
handle = await ctx.agents.create({
|
|
167
|
+
sessionId: SessionId(dshSessionId),
|
|
168
|
+
meta: input?.cwd ? { cwd: resolve(input.cwd) } : undefined,
|
|
169
|
+
agentOptions,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
debugLog('ensureAgent resume', { accountId, dshSessionId, provider, model, selection, resumed: true });
|
|
174
|
+
}
|
|
175
|
+
const finalSessionId = dshSessionId;
|
|
176
|
+
sessionIds.set(accountId, finalSessionId);
|
|
177
|
+
persistSessionId(accountId, finalSessionId);
|
|
178
|
+
agents.set(accountId, handle);
|
|
179
|
+
activeSessionIds.add(finalSessionId);
|
|
180
|
+
debugLog('agent ready', { accountId, dshSessionId: finalSessionId, provider, model, selection, resumed });
|
|
181
|
+
if (input?.cwd)
|
|
182
|
+
await attachSessionToWorkspace(finalSessionId, resolve(input.cwd));
|
|
183
|
+
return handle;
|
|
184
|
+
})();
|
|
185
|
+
creating.set(accountId, task);
|
|
186
|
+
try {
|
|
187
|
+
return await task;
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
creating.delete(accountId);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async function disposeAgent(accountId) {
|
|
194
|
+
const handle = agents.get(accountId);
|
|
195
|
+
const dshSessionId = sessionIds.get(accountId);
|
|
196
|
+
if (dshSessionId)
|
|
197
|
+
activeSessionIds.delete(dshSessionId);
|
|
198
|
+
sessionIds.delete(accountId);
|
|
199
|
+
agents.delete(accountId);
|
|
200
|
+
removePersistedSessionId(accountId);
|
|
201
|
+
if (handle)
|
|
202
|
+
await handle.dispose();
|
|
203
|
+
closeStreams(accountId);
|
|
204
|
+
}
|
|
205
|
+
/** Register the DSH session under a dedicated workspace so it doesn't stay Ungrouped. */
|
|
206
|
+
async function attachSessionToWorkspace(dshSessionId, cwd) {
|
|
207
|
+
const registry = ctx.get('workspaceRegistry');
|
|
208
|
+
if (!registry)
|
|
209
|
+
return;
|
|
210
|
+
try {
|
|
211
|
+
let ws = await registry.resolveByPath(cwd);
|
|
212
|
+
if (!ws)
|
|
213
|
+
ws = await registry.create(cwd, '微信桥接');
|
|
214
|
+
await ws.attachSession(SessionId(dshSessionId));
|
|
215
|
+
debugLog('workspace attached', { dshSessionId, cwd });
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
debugLog('workspace attach failed', {
|
|
219
|
+
dshSessionId,
|
|
220
|
+
cwd,
|
|
221
|
+
error: err instanceof Error ? err.message : String(err),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// -------------------------------------------------------------------------
|
|
226
|
+
// SSE broadcast
|
|
227
|
+
// -------------------------------------------------------------------------
|
|
228
|
+
function broadcast(sessionId, event) {
|
|
229
|
+
const clients = streamClients.get(sessionId);
|
|
230
|
+
if (!clients || clients.size === 0)
|
|
231
|
+
return;
|
|
232
|
+
const payload = `data: ${JSON.stringify({ ...event, sessionId })}\n\n`;
|
|
233
|
+
for (const res of [...clients]) {
|
|
234
|
+
try {
|
|
235
|
+
res.write(payload);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// Client may have gone away; cleanup below.
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (event.type === 'done' || event.type === 'error') {
|
|
242
|
+
closeStreams(sessionId);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function closeStreams(sessionId) {
|
|
246
|
+
const clients = streamClients.get(sessionId);
|
|
247
|
+
if (!clients)
|
|
248
|
+
return;
|
|
249
|
+
for (const res of [...clients]) {
|
|
250
|
+
try {
|
|
251
|
+
res.end();
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// ignore
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
streamClients.delete(sessionId);
|
|
258
|
+
}
|
|
259
|
+
// Subscribe to every session event and forward assistant chunks to the
|
|
260
|
+
// bridge daemon. Only sessions created by this plugin are forwarded.
|
|
261
|
+
ctx.on('session/event', (session, event) => {
|
|
262
|
+
const sid = String(session.id);
|
|
263
|
+
if (!activeSessionIds.has(sid))
|
|
264
|
+
return;
|
|
265
|
+
const accountId = [...sessionIds.entries()].find(([, v]) => v === sid)?.[0];
|
|
266
|
+
if (!accountId)
|
|
267
|
+
return;
|
|
268
|
+
if (event.type === 'assistant/chunk') {
|
|
269
|
+
const chunk = event.data.chunk;
|
|
270
|
+
if (chunk?.type === 'text-delta' && typeof chunk.text === 'string') {
|
|
271
|
+
broadcast(accountId, { type: 'chunk', text: chunk.text });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else if (event.type === 'turn/end') {
|
|
275
|
+
debugLog('session turn/end', { accountId, sessionId: sid, reason: event.data.reason });
|
|
276
|
+
broadcast(accountId, { type: 'done', turn: event.data.turn, message: 'turn ended' });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
// -------------------------------------------------------------------------
|
|
280
|
+
// Internal HTTP server (daemon-facing, token protected)
|
|
281
|
+
// -------------------------------------------------------------------------
|
|
282
|
+
function isAuthorized(req) {
|
|
283
|
+
const header = req.headers.authorization || '';
|
|
284
|
+
return header === `Bearer ${token}`;
|
|
285
|
+
}
|
|
286
|
+
function sendJson(res, status, body) {
|
|
287
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
288
|
+
res.end(JSON.stringify(body));
|
|
289
|
+
}
|
|
290
|
+
async function readBody(req) {
|
|
291
|
+
const chunks = [];
|
|
292
|
+
for await (const chunk of req) {
|
|
293
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
294
|
+
}
|
|
295
|
+
if (chunks.length === 0)
|
|
296
|
+
return {};
|
|
297
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
298
|
+
}
|
|
299
|
+
async function handleInternal(req, res) {
|
|
300
|
+
const url = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
|
|
301
|
+
if (!isAuthorized(req)) {
|
|
302
|
+
sendJson(res, 401, { ok: false, error: 'unauthorized' });
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
if (req.method === 'GET' && url.pathname === '/api/status') {
|
|
307
|
+
sendJson(res, 200, await statusPayload());
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (req.method === 'POST' && url.pathname === '/api/prompt') {
|
|
311
|
+
const body = await readBody(req);
|
|
312
|
+
const sessionId = String(body.sessionId || 'default');
|
|
313
|
+
const handle = await ensureAgent(sessionId, { cwd: body.cwd, model: body.model });
|
|
314
|
+
const text = String(body.text || '').trim();
|
|
315
|
+
if (!text) {
|
|
316
|
+
sendJson(res, 400, { ok: false, error: 'text is required' });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
handle.agent.followup(createUserMessage({
|
|
320
|
+
source: { kind: 'user' },
|
|
321
|
+
content: [{ type: 'text', text }],
|
|
322
|
+
}));
|
|
323
|
+
sendJson(res, 200, { accepted: true, sessionId });
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (req.method === 'POST' && url.pathname === '/api/stop') {
|
|
327
|
+
const body = await readBody(req);
|
|
328
|
+
const sessionId = String(body.sessionId || 'default');
|
|
329
|
+
const handle = agents.get(sessionId);
|
|
330
|
+
if (handle) {
|
|
331
|
+
handle.agent.cancel({ kind: 'user' });
|
|
332
|
+
}
|
|
333
|
+
sendJson(res, 200, { ok: true });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (req.method === 'POST' && url.pathname === '/api/clear') {
|
|
337
|
+
const body = await readBody(req);
|
|
338
|
+
const sessionId = String(body.sessionId || 'default');
|
|
339
|
+
await disposeAgent(sessionId);
|
|
340
|
+
sendJson(res, 200, { ok: true });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (req.method === 'GET' && url.pathname === '/api/stream') {
|
|
344
|
+
const sessionId = url.searchParams.get('sessionId') || 'default';
|
|
345
|
+
if (!agents.has(sessionId)) {
|
|
346
|
+
sendJson(res, 404, { ok: false, error: 'session not active' });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
res.writeHead(200, {
|
|
350
|
+
'Content-Type': 'text/event-stream',
|
|
351
|
+
'Cache-Control': 'no-store',
|
|
352
|
+
Connection: 'keep-alive',
|
|
353
|
+
'X-Accel-Buffering': 'no',
|
|
354
|
+
});
|
|
355
|
+
res.write('retry: 3000\n\n');
|
|
356
|
+
let set = streamClients.get(sessionId);
|
|
357
|
+
if (!set) {
|
|
358
|
+
set = new Set();
|
|
359
|
+
streamClients.set(sessionId, set);
|
|
360
|
+
}
|
|
361
|
+
set.add(res);
|
|
362
|
+
req.on('close', () => {
|
|
363
|
+
set?.delete(res);
|
|
364
|
+
if (set?.size === 0)
|
|
365
|
+
streamClients.delete(sessionId);
|
|
366
|
+
});
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
sendJson(res, 404, { ok: false, error: 'not found' });
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
373
|
+
sendJson(res, 400, { ok: false, error: message });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// -------------------------------------------------------------------------
|
|
377
|
+
// Daemon process management (cross-platform)
|
|
378
|
+
// -------------------------------------------------------------------------
|
|
379
|
+
function bridgeScript() {
|
|
380
|
+
return join(dirname(fileURLToPath(import.meta.url)), 'bridge', 'main.js');
|
|
381
|
+
}
|
|
382
|
+
function readPid() {
|
|
383
|
+
try {
|
|
384
|
+
const raw = readFileSync(pidPath, 'utf8').trim();
|
|
385
|
+
const pid = Number(raw);
|
|
386
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function isPidAlive(pid) {
|
|
393
|
+
try {
|
|
394
|
+
process.kill(pid, 0);
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function daemonRunning() {
|
|
402
|
+
if (bridgeChild && bridgeChild.pid !== undefined) {
|
|
403
|
+
try {
|
|
404
|
+
if (bridgeChild.exitCode === null)
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
// fall through
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const pid = readPid();
|
|
412
|
+
return pid !== null && isPidAlive(pid);
|
|
413
|
+
}
|
|
414
|
+
function daemonPid() {
|
|
415
|
+
if (bridgeChild && bridgeChild.pid !== undefined && bridgeChild.exitCode === null) {
|
|
416
|
+
return bridgeChild.pid;
|
|
417
|
+
}
|
|
418
|
+
return readPid() ?? undefined;
|
|
419
|
+
}
|
|
420
|
+
async function startDaemon() {
|
|
421
|
+
if (daemonRunning()) {
|
|
422
|
+
return { ok: true, message: `已运行 (PID: ${daemonPid()})` };
|
|
423
|
+
}
|
|
424
|
+
const script = bridgeScript();
|
|
425
|
+
if (!existsSync(script)) {
|
|
426
|
+
return { ok: false, message: `桥接脚本不存在: ${script}(请先 build 插件)` };
|
|
427
|
+
}
|
|
428
|
+
const logFd = await import('node:fs').then(fs => fs.openSync(daemonLogPath, 'a'));
|
|
429
|
+
const child = spawn(process.execPath, [script, 'start'], {
|
|
430
|
+
cwd: dirname(script),
|
|
431
|
+
env: {
|
|
432
|
+
...process.env,
|
|
433
|
+
DSH_HOME: dshHome,
|
|
434
|
+
DSH_BRIDGE_DATA_DIR: dataDir,
|
|
435
|
+
DSH_BRIDGE_API_BASE: `http://127.0.0.1:${internalPort}`,
|
|
436
|
+
DSH_BRIDGE_API_TOKEN: token,
|
|
437
|
+
},
|
|
438
|
+
stdio: ['ignore', logFd, logFd],
|
|
439
|
+
windowsHide: true,
|
|
440
|
+
});
|
|
441
|
+
bridgeChild = child;
|
|
442
|
+
writeFileSync(pidPath, String(child.pid || ''), 'utf8');
|
|
443
|
+
child.on('exit', (code) => {
|
|
444
|
+
try {
|
|
445
|
+
if (bridgeChild === child)
|
|
446
|
+
bridgeChild = undefined;
|
|
447
|
+
const pid = readPid();
|
|
448
|
+
if (pid === child.pid)
|
|
449
|
+
unlinkSync(pidPath);
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
// ignore
|
|
453
|
+
}
|
|
454
|
+
ctx.logger?.info?.('[dsh-wechat-bridge] daemon exited', { code });
|
|
455
|
+
});
|
|
456
|
+
// Give the daemon a moment to fail early (missing account etc.).
|
|
457
|
+
await new Promise(r => setTimeout(r, 300));
|
|
458
|
+
if (child.exitCode !== null) {
|
|
459
|
+
return { ok: false, message: `守护进程启动失败 (exit ${child.exitCode}),请查看日志` };
|
|
460
|
+
}
|
|
461
|
+
bridgeStartedAt = Date.now();
|
|
462
|
+
return { ok: true, message: `已启动 (PID: ${child.pid})` };
|
|
463
|
+
}
|
|
464
|
+
async function stopDaemon() {
|
|
465
|
+
const pid = daemonPid();
|
|
466
|
+
if (bridgeChild && bridgeChild.pid !== undefined && bridgeChild.exitCode === null) {
|
|
467
|
+
bridgeChild.kill();
|
|
468
|
+
await new Promise(r => setTimeout(r, 500));
|
|
469
|
+
if (bridgeChild.exitCode === null) {
|
|
470
|
+
bridgeChild.kill('SIGKILL');
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
else if (pid) {
|
|
474
|
+
try {
|
|
475
|
+
process.kill(pid);
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
// already gone
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
unlinkSync(pidPath);
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
// ignore
|
|
486
|
+
}
|
|
487
|
+
bridgeChild = undefined;
|
|
488
|
+
return { ok: true, message: '已停止' };
|
|
489
|
+
}
|
|
490
|
+
async function restartDaemon() {
|
|
491
|
+
await stopDaemon();
|
|
492
|
+
await new Promise(r => setTimeout(r, 300));
|
|
493
|
+
return startDaemon();
|
|
494
|
+
}
|
|
495
|
+
function bridgeConfigPath() {
|
|
496
|
+
return join(dataDir, 'config.json');
|
|
497
|
+
}
|
|
498
|
+
function readBridgeConfig() {
|
|
499
|
+
try {
|
|
500
|
+
const raw = JSON.parse(readFileSync(bridgeConfigPath(), 'utf8'));
|
|
501
|
+
return {
|
|
502
|
+
workingDirectory: raw.workingDirectory || join(homedir(), 'Documents', 'DSH'),
|
|
503
|
+
model: raw.model,
|
|
504
|
+
systemPrompt: raw.systemPrompt,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
return {
|
|
509
|
+
workingDirectory: join(homedir(), 'Documents', 'DSH'),
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function saveBridgeConfig(config) {
|
|
514
|
+
mkdirSync(dataDir, { recursive: true });
|
|
515
|
+
const data = {
|
|
516
|
+
workingDirectory: config.workingDirectory,
|
|
517
|
+
};
|
|
518
|
+
if (config.model)
|
|
519
|
+
data.model = config.model;
|
|
520
|
+
if (config.systemPrompt)
|
|
521
|
+
data.systemPrompt = config.systemPrompt;
|
|
522
|
+
writeFileSync(bridgeConfigPath(), JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
523
|
+
if (process.platform !== 'win32') {
|
|
524
|
+
chmodSync(bridgeConfigPath(), 0o600);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
async function startSetup(workingDirectory) {
|
|
528
|
+
const dir = (workingDirectory?.trim() || readBridgeConfig().workingDirectory || join(homedir(), 'Documents', 'DSH')).replace(/^~/, homedir());
|
|
529
|
+
const { qrcodeUrl, qrcodeId } = await startQrLogin();
|
|
530
|
+
const QRCode = await import('qrcode');
|
|
531
|
+
const qrcodeDataUrl = await QRCode.toDataURL(qrcodeUrl, {
|
|
532
|
+
width: 320,
|
|
533
|
+
margin: 2,
|
|
534
|
+
});
|
|
535
|
+
pendingSetup = { qrcodeId, workingDirectory: dir };
|
|
536
|
+
return {
|
|
537
|
+
ok: true,
|
|
538
|
+
qrcodeId,
|
|
539
|
+
qrcodeUrl,
|
|
540
|
+
qrcodeDataUrl,
|
|
541
|
+
workingDirectory: dir,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
async function checkSetupStatus(qrcodeId) {
|
|
545
|
+
if (!pendingSetup || pendingSetup.qrcodeId !== qrcodeId) {
|
|
546
|
+
return { ok: false, status: 'idle', message: '没有进行中的扫码绑定,请先点击“扫码绑定”。' };
|
|
547
|
+
}
|
|
548
|
+
const result = await checkQrStatus(qrcodeId);
|
|
549
|
+
if (result.status === 'confirmed') {
|
|
550
|
+
const config = readBridgeConfig();
|
|
551
|
+
config.workingDirectory = pendingSetup.workingDirectory;
|
|
552
|
+
saveBridgeConfig(config);
|
|
553
|
+
pendingSetup = undefined;
|
|
554
|
+
return {
|
|
555
|
+
ok: true,
|
|
556
|
+
status: 'confirmed',
|
|
557
|
+
accountId: result.account.accountId,
|
|
558
|
+
workingDirectory: config.workingDirectory,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
if (result.status === 'expired') {
|
|
562
|
+
pendingSetup = undefined;
|
|
563
|
+
return { ok: false, status: 'expired', message: result.message };
|
|
564
|
+
}
|
|
565
|
+
if (result.status === 'error') {
|
|
566
|
+
return { ok: false, status: 'error', message: result.message, retryable: result.retryable };
|
|
567
|
+
}
|
|
568
|
+
return { ok: true, status: result.status };
|
|
569
|
+
}
|
|
570
|
+
function readDaemonLogs(limit = 100) {
|
|
571
|
+
try {
|
|
572
|
+
if (!existsSync(daemonLogPath))
|
|
573
|
+
return '暂无日志';
|
|
574
|
+
const text = readFileSync(daemonLogPath, 'utf8');
|
|
575
|
+
const lines = text.split('\n').filter(Boolean);
|
|
576
|
+
return lines.slice(-limit).join('\n');
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
return '读取日志失败';
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
async function statusPayload() {
|
|
583
|
+
const accountFiles = [];
|
|
584
|
+
try {
|
|
585
|
+
const accountsDir = join(dataDir, 'accounts');
|
|
586
|
+
if (existsSync(accountsDir)) {
|
|
587
|
+
for (const f of readdirSync(accountsDir)) {
|
|
588
|
+
if (f.endsWith('.json'))
|
|
589
|
+
accountFiles.push(f);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
// ignore
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
ok: true,
|
|
598
|
+
plugin: name,
|
|
599
|
+
running: daemonRunning(),
|
|
600
|
+
pid: daemonPid() ?? null,
|
|
601
|
+
startedAt: bridgeStartedAt ?? null,
|
|
602
|
+
dataDir,
|
|
603
|
+
apiBase: internalPort ? `http://127.0.0.1:${internalPort}` : null,
|
|
604
|
+
workingDirectory: readBridgeConfig().workingDirectory,
|
|
605
|
+
accounts: accountFiles,
|
|
606
|
+
sessions: [...sessionIds.keys()],
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
// -------------------------------------------------------------------------
|
|
610
|
+
// Optional Web panel routes (same origin, no token)
|
|
611
|
+
// -------------------------------------------------------------------------
|
|
612
|
+
function registerWebRoutes() {
|
|
613
|
+
const webServer = ctx.get('webServer');
|
|
614
|
+
if (!webServer)
|
|
615
|
+
return [];
|
|
616
|
+
const disposers = [];
|
|
617
|
+
disposers.push(webServer.register({
|
|
618
|
+
kind: 'exact',
|
|
619
|
+
path: '/@lanbaolu/dsh-wechat-bridge/status',
|
|
620
|
+
handler: async (_req, res) => {
|
|
621
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
622
|
+
res.end(JSON.stringify(await statusPayload()));
|
|
623
|
+
},
|
|
624
|
+
}));
|
|
625
|
+
for (const action of ['start', 'stop', 'restart']) {
|
|
626
|
+
disposers.push(webServer.register({
|
|
627
|
+
kind: 'exact',
|
|
628
|
+
path: `/@lanbaolu/dsh-wechat-bridge/${action}`,
|
|
629
|
+
handler: async (_req, res) => {
|
|
630
|
+
const result = action === 'start' ? await startDaemon()
|
|
631
|
+
: action === 'stop' ? await stopDaemon()
|
|
632
|
+
: await restartDaemon();
|
|
633
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
634
|
+
res.end(JSON.stringify(result));
|
|
635
|
+
},
|
|
636
|
+
}));
|
|
637
|
+
}
|
|
638
|
+
disposers.push(webServer.register({
|
|
639
|
+
kind: 'exact',
|
|
640
|
+
path: '/@lanbaolu/dsh-wechat-bridge/logs',
|
|
641
|
+
handler: async (_req, res) => {
|
|
642
|
+
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
643
|
+
res.end(readDaemonLogs(200));
|
|
644
|
+
},
|
|
645
|
+
}));
|
|
646
|
+
disposers.push(webServer.register({
|
|
647
|
+
kind: 'exact',
|
|
648
|
+
path: '/@lanbaolu/dsh-wechat-bridge/setup/start',
|
|
649
|
+
handler: async (req, res) => {
|
|
650
|
+
let workingDirectory;
|
|
651
|
+
try {
|
|
652
|
+
const body = await readBody(req);
|
|
653
|
+
workingDirectory = typeof body.workingDirectory === 'string' ? body.workingDirectory : undefined;
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
// body optional
|
|
657
|
+
}
|
|
658
|
+
const result = await startSetup(workingDirectory);
|
|
659
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
660
|
+
res.end(JSON.stringify(result));
|
|
661
|
+
},
|
|
662
|
+
}));
|
|
663
|
+
disposers.push(webServer.register({
|
|
664
|
+
kind: 'exact',
|
|
665
|
+
path: '/@lanbaolu/dsh-wechat-bridge/setup/status',
|
|
666
|
+
handler: async (req, res) => {
|
|
667
|
+
const url = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
|
|
668
|
+
const qrcodeId = url.searchParams.get('qrcodeId') || '';
|
|
669
|
+
const result = await checkSetupStatus(qrcodeId);
|
|
670
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
671
|
+
res.end(JSON.stringify(result));
|
|
672
|
+
},
|
|
673
|
+
}));
|
|
674
|
+
return disposers;
|
|
675
|
+
}
|
|
676
|
+
// -------------------------------------------------------------------------
|
|
677
|
+
// Model-facing tools
|
|
678
|
+
// -------------------------------------------------------------------------
|
|
679
|
+
function registerTools() {
|
|
680
|
+
const simpleOutput = {
|
|
681
|
+
schema: {
|
|
682
|
+
type: 'object',
|
|
683
|
+
additionalProperties: false,
|
|
684
|
+
properties: {
|
|
685
|
+
ok: { type: 'boolean', required: true },
|
|
686
|
+
message: { type: 'string', required: true },
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
render: (_args, value) => [{
|
|
690
|
+
type: 'text',
|
|
691
|
+
text: value.message,
|
|
692
|
+
}],
|
|
693
|
+
};
|
|
694
|
+
ctx.tools.register(defineTool({
|
|
695
|
+
name: 'wechat_bridge_status',
|
|
696
|
+
description: '查看 DSH 微信桥接状态:守护进程是否运行、PID、数据目录、已绑定账号与活跃会话。',
|
|
697
|
+
parameters: {},
|
|
698
|
+
output: simpleOutput,
|
|
699
|
+
execute: async () => {
|
|
700
|
+
const p = await statusPayload();
|
|
701
|
+
return {
|
|
702
|
+
ok: true,
|
|
703
|
+
message: `运行中: ${p.running}\nPID: ${p.pid ?? '无'}\n数据目录: ${p.dataDir}\n账号: ${p.accounts.join(', ') || '无'}\n活跃会话: ${p.sessions.join(', ') || '无'}`,
|
|
704
|
+
};
|
|
705
|
+
},
|
|
706
|
+
}));
|
|
707
|
+
ctx.tools.register(defineTool({
|
|
708
|
+
name: 'wechat_bridge_start',
|
|
709
|
+
description: '启动 DSH 微信桥接守护进程。需要先完成微信扫码绑定(wechat_bridge_setup 或 node lib/bridge/main.js setup)。',
|
|
710
|
+
parameters: {},
|
|
711
|
+
output: simpleOutput,
|
|
712
|
+
execute: startDaemon,
|
|
713
|
+
}));
|
|
714
|
+
ctx.tools.register(defineTool({
|
|
715
|
+
name: 'wechat_bridge_stop',
|
|
716
|
+
description: '停止 DSH 微信桥接守护进程。',
|
|
717
|
+
parameters: {},
|
|
718
|
+
output: simpleOutput,
|
|
719
|
+
execute: stopDaemon,
|
|
720
|
+
}));
|
|
721
|
+
ctx.tools.register(defineTool({
|
|
722
|
+
name: 'wechat_bridge_restart',
|
|
723
|
+
description: '重启 DSH 微信桥接守护进程(更新配置或卡死后使用)。',
|
|
724
|
+
parameters: {},
|
|
725
|
+
output: simpleOutput,
|
|
726
|
+
execute: restartDaemon,
|
|
727
|
+
}));
|
|
728
|
+
ctx.tools.register(defineTool({
|
|
729
|
+
name: 'wechat_bridge_logs',
|
|
730
|
+
description: '读取 DSH 微信桥接守护进程最近日志(默认 100 行,可通过参数调整)。',
|
|
731
|
+
parameters: {
|
|
732
|
+
lines: { type: 'number', description: '读取最近多少行日志,默认 100。' },
|
|
733
|
+
},
|
|
734
|
+
output: {
|
|
735
|
+
schema: {
|
|
736
|
+
type: 'object',
|
|
737
|
+
additionalProperties: false,
|
|
738
|
+
properties: {
|
|
739
|
+
ok: { type: 'boolean', required: true },
|
|
740
|
+
logs: { type: 'string', required: true },
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
render: (_args, value) => [{
|
|
744
|
+
type: 'text',
|
|
745
|
+
text: value.logs || '(空日志)',
|
|
746
|
+
}],
|
|
747
|
+
},
|
|
748
|
+
execute: async (args) => ({
|
|
749
|
+
ok: true,
|
|
750
|
+
logs: readDaemonLogs(args.lines && args.lines > 0 ? args.lines : 100),
|
|
751
|
+
}),
|
|
752
|
+
}));
|
|
753
|
+
ctx.tools.register(defineTool({
|
|
754
|
+
name: 'wechat_bridge_setup',
|
|
755
|
+
description: '显示 DSH 微信桥接的扫码绑定方式:在终端运行 node <bridgeScript> setup。该命令会生成二维码并用系统默认应用打开。',
|
|
756
|
+
parameters: {},
|
|
757
|
+
output: {
|
|
758
|
+
schema: {
|
|
759
|
+
type: 'object',
|
|
760
|
+
additionalProperties: false,
|
|
761
|
+
properties: {
|
|
762
|
+
ok: { type: 'boolean', required: true },
|
|
763
|
+
command: { type: 'string', required: true },
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
render: (_args, value) => [{
|
|
767
|
+
type: 'text',
|
|
768
|
+
text: `请在 DSH 所在机器终端执行:\n${value.command}`,
|
|
769
|
+
}],
|
|
770
|
+
},
|
|
771
|
+
execute: async () => ({
|
|
772
|
+
ok: true,
|
|
773
|
+
command: `node "${bridgeScript()}" setup`,
|
|
774
|
+
}),
|
|
775
|
+
}));
|
|
776
|
+
}
|
|
777
|
+
// -------------------------------------------------------------------------
|
|
778
|
+
// Lifecycle
|
|
779
|
+
// -------------------------------------------------------------------------
|
|
780
|
+
ctx.effect(() => {
|
|
781
|
+
const server = createServer((req, res) => {
|
|
782
|
+
handleInternal(req, res).catch((err) => {
|
|
783
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
784
|
+
sendJson(res, 500, { ok: false, error: message });
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
server.listen(config.port, config.host, () => {
|
|
788
|
+
const address = server.address();
|
|
789
|
+
if (address && typeof address === 'object') {
|
|
790
|
+
internalPort = address.port;
|
|
791
|
+
}
|
|
792
|
+
ctx.logger?.info?.('[dsh-wechat-bridge] internal API listening', {
|
|
793
|
+
host: config.host,
|
|
794
|
+
port: internalPort,
|
|
795
|
+
});
|
|
796
|
+
if (config.autoStart) {
|
|
797
|
+
startDaemon().then(result => {
|
|
798
|
+
ctx.logger?.info?.('[dsh-wechat-bridge] autoStart', result);
|
|
799
|
+
}).catch(err => {
|
|
800
|
+
ctx.logger?.warn?.('[dsh-wechat-bridge] autoStart failed', { error: String(err) });
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
internalServer = server;
|
|
805
|
+
registerTools();
|
|
806
|
+
const webDisposers = registerWebRoutes();
|
|
807
|
+
return () => {
|
|
808
|
+
for (const dispose of webDisposers)
|
|
809
|
+
dispose();
|
|
810
|
+
for (const handle of agents.values()) {
|
|
811
|
+
void handle.dispose();
|
|
812
|
+
}
|
|
813
|
+
agents.clear();
|
|
814
|
+
activeSessionIds.clear();
|
|
815
|
+
streamClients.clear();
|
|
816
|
+
if (bridgeChild && bridgeChild.exitCode === null) {
|
|
817
|
+
bridgeChild.kill();
|
|
818
|
+
}
|
|
819
|
+
try {
|
|
820
|
+
server.close();
|
|
821
|
+
}
|
|
822
|
+
catch {
|
|
823
|
+
// ignore
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
//# sourceMappingURL=index.js.map
|