diff --git a/packages/bridge/src/hook-manager.ts b/packages/bridge/src/hook-manager.ts index 22473fc3..89dc6057 100644 --- a/packages/bridge/src/hook-manager.ts +++ b/packages/bridge/src/hook-manager.ts @@ -309,7 +309,9 @@ export class HookManager { this.runAutoLoad(session, autoLoad); continue; } - if ((session.status === 'connecting' || session.status === 'disconnected') + if ((session.status === 'connecting' + || session.status === 'disconnected' + || session.status === 'error') && this.pipeWatcher.isPipeLive(session.pid)) { session.onPipeUp(); } diff --git a/packages/bridge/src/hook-session.ts b/packages/bridge/src/hook-session.ts index 88f24c90..f6fef1d1 100644 --- a/packages/bridge/src/hook-session.ts +++ b/packages/bridge/src/hook-session.ts @@ -358,6 +358,15 @@ export class HookSession extends EventEmitter { this._error = errMsg(error); this.setStatus('error', this._error); this.log.error('load failed: PID=%d err=%s', this.pid, this._error); + // The module can already be listening even when this call returns + // failure: another load left it resident, or it came up after the + // call returned. A live pipe still has the account state, so connect + // instead of staying on the failed load. + await this.pipeWatcher.tickNow?.(); + if (!this.disposed && this.pipeWatcher.isPipeLive(this.pid)) { + this.log.info('PID=%d load failed but pipe is up; connecting', this.pid); + await this.reconcilePipeUp(); + } } return this.toInfo(); } diff --git a/packages/bridge/tests/hook-session.test.ts b/packages/bridge/tests/hook-session.test.ts index 0e715de7..89267e14 100644 --- a/packages/bridge/tests/hook-session.test.ts +++ b/packages/bridge/tests/hook-session.test.ts @@ -205,6 +205,35 @@ describe('HookSession — runtime TRACE', () => { } }); + it('connects when load fails but the pipe is already up', async () => { + let pipeLive = false; + const clients: FakeClient[] = []; + const session = new HookSession(7108, { + injector: { + inject: () => { + pipeLive = true; + throw new Error('component loading failed [COMPONENT_LOAD_FAILED]'); + }, + unload: () => undefined, + }, + makeClient: () => { + const client = new FakeClient(); + client.fireLogin('10001'); + clients.push(client); + return client as unknown as QqHookClient; + }, + pipeWatcher: { + isPipeLive: () => pipeLive, + tickNow: async () => undefined, + }, + }); + const info = await session.load(); + expect(info.status).toBe('online'); + expect(info.uin).toBe('10001'); + expect(info.connected).toBe(true); + expect(clients).toHaveLength(1); + }); + it('records login, disconnect, and process-gone only when state changes', async () => { const entries: LogEntry[] = []; setLogLevel('trace');