fix: 保持Bilibili卡片事件异常不外抛

This commit is contained in:
sunlei 2026-06-19 19:15:27 +08:00
parent c14bcea2dc
commit 98e26616bb
2 changed files with 124 additions and 9 deletions

View File

@ -122,6 +122,7 @@ export class BilibiliCardApplication {
if (cached && cached.expiresAt > current) return cached.value; if (cached && cached.expiresAt > current) return cached.value;
const config = readBilibiliCardRuntimeConfig(this.host); const config = readBilibiliCardRuntimeConfig(this.host);
try {
const value = ( const value = (
await this.host.getBoundEventPluginKeys(normalizedSelfId) await this.host.getBoundEventPluginKeys(normalizedSelfId)
).includes(this.manifest.pluginKey); ).includes(this.manifest.pluginKey);
@ -130,6 +131,10 @@ export class BilibiliCardApplication {
value, value,
}); });
return value; return value;
} catch (error) {
this.warn(`Bilibili 事件绑定查询失败: ${normalizeError(error)}`);
return false;
}
} }
/** /**
@ -147,7 +152,14 @@ export class BilibiliCardApplication {
* @param message - Warning message safe for platform logs. * @param message - Warning message safe for platform logs.
*/ */
private warn(message: string) { private warn(message: string) {
this.host.warn?.(message); try {
const result = this.host.warn?.(message) as unknown;
if (isThenable(result)) {
result.catch(() => undefined);
}
} catch {
return;
}
} }
} }
@ -182,6 +194,22 @@ function isB23ShortLink(url: string) {
} }
} }
/**
* Detects promise-like warning results so rejected async loggers cannot escape later.
* @param value - Return value from the optional host warning hook.
* @returns `true` when the value exposes a callable `catch` method.
*/
function isThenable(
value: unknown,
): value is { catch: (handler: () => void) => unknown } {
return (
typeof value === 'object' &&
value !== null &&
'catch' in value &&
typeof value.catch === 'function'
);
}
/** /**
* Converts thrown values to stable warning text. * Converts thrown values to stable warning text.
* @param error - Error or arbitrary thrown value from host or domain code. * @param error - Error or arbitrary thrown value from host or domain code.

View File

@ -25,6 +25,29 @@ describe('Bilibili card application', () => {
expect(host.sendText).not.toHaveBeenCalled(); expect(host.sendText).not.toHaveBeenCalled();
}); });
it('warns and returns false when binding lookup fails', async () => {
const host = createHost({
getBoundEventPluginKeys: jest
.fn()
.mockRejectedValue(new Error('binding down')),
});
const application = new BilibiliCardApplication(host, createManifest());
await expect(
application.handleMessage(
createMessage({
messageText: 'https://www.bilibili.com/video/BV1xx411c7mD',
}),
),
).resolves.toBe(false);
expect(host.warn).toHaveBeenCalledWith(
expect.stringContaining('binding down'),
);
expect(host.requestJson).not.toHaveBeenCalled();
expect(host.sendText).not.toHaveBeenCalled();
});
it('ignores messages sent by the bot itself', async () => { it('ignores messages sent by the bot itself', async () => {
const host = createHost({ const host = createHost({
getBoundEventPluginKeys: jest.fn().mockResolvedValue(['bilibili-card']), getBoundEventPluginKeys: jest.fn().mockResolvedValue(['bilibili-card']),
@ -169,6 +192,70 @@ describe('Bilibili card application', () => {
expect(host.sendText).not.toHaveBeenCalled(); expect(host.sendText).not.toHaveBeenCalled();
}); });
it('returns false when warning throws during binding lookup failure', async () => {
const host = createHost({
getBoundEventPluginKeys: jest
.fn()
.mockRejectedValue(new Error('binding down')),
warn: jest.fn(() => {
throw new Error('logger down');
}),
});
const application = new BilibiliCardApplication(host, createManifest());
await expect(
application.handleMessage(
createMessage({
messageText: 'https://www.bilibili.com/video/BV1xx411c7mD',
}),
),
).resolves.toBe(false);
});
it('returns false when warning throws during short-link failure', async () => {
const host = createHost({
getBoundEventPluginKeys: jest.fn().mockResolvedValue(['bilibili-card']),
resolveRedirect: jest.fn().mockRejectedValue(new Error('redirect down')),
warn: jest.fn(() => {
throw new Error('logger down');
}),
});
const application = new BilibiliCardApplication(host, createManifest());
await expect(
application.handleMessage(
createMessage({
messageText: 'https://b23.tv/abc123',
}),
),
).resolves.toBe(false);
});
it('attaches a rejection handler when warning returns a promise', async () => {
const catchSpy = jest.fn();
const host = createHost({
getBoundEventPluginKeys: jest.fn().mockResolvedValue(['bilibili-card']),
resolveRedirect: jest.fn().mockRejectedValue(new Error('redirect down')),
warn: jest.fn(
() =>
({
catch: catchSpy,
}) as unknown as void,
),
});
const application = new BilibiliCardApplication(host, createManifest());
await expect(
application.handleMessage(
createMessage({
messageText: 'https://b23.tv/abc123',
}),
),
).resolves.toBe(false);
expect(catchSpy).toHaveBeenCalledWith(expect.any(Function));
});
it('warns and returns false when short-link resolution fails', async () => { it('warns and returns false when short-link resolution fails', async () => {
const host = createHost({ const host = createHost({
getBoundEventPluginKeys: jest.fn().mockResolvedValue(['bilibili-card']), getBoundEventPluginKeys: jest.fn().mockResolvedValue(['bilibili-card']),