fix: 降级BangDream启动健康检查
This commit is contained in:
parent
2a30049600
commit
d6135a2012
@ -843,13 +843,42 @@ export class QqbotPluginPlatformService
|
||||
persistedInstallation,
|
||||
);
|
||||
|
||||
try {
|
||||
await this.startWorker(installation, version);
|
||||
startedCount += 1;
|
||||
} catch (error) {
|
||||
await this.recordBuiltinWorkerStartFailure(installation, error);
|
||||
}
|
||||
}
|
||||
|
||||
return startedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records one failed built-in worker boot without blocking other built-in plugin packages.
|
||||
* @param installation - Persisted installation whose worker failed during module startup.
|
||||
* @param error - Startup error from worker load, activate, or health checks.
|
||||
*/
|
||||
private async recordBuiltinWorkerStartFailure(
|
||||
installation: QqbotPluginInstallation,
|
||||
error: unknown,
|
||||
) {
|
||||
const message = error instanceof Error ? error.message : `${error}`;
|
||||
await this.updateInstallationRuntime(
|
||||
installation,
|
||||
'enabled',
|
||||
'unhealthy',
|
||||
);
|
||||
await this.recordRuntimeEvent(
|
||||
installation,
|
||||
'builtin-start-failed',
|
||||
'error',
|
||||
{
|
||||
message,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures descriptor-discovered built-in package state is persisted.
|
||||
* @param descriptor - Manifest descriptor discovered from a controlled plugin package root.
|
||||
|
||||
@ -137,6 +137,28 @@ function buildBangDreamRuntimePlugin(
|
||||
(error instanceof Error ? error.message : `${error}`) ||
|
||||
'BangDream 命令执行失败');
|
||||
|
||||
/**
|
||||
* Checks BangDream runtime health without turning external catalog outages into worker startup failures.
|
||||
* @returns Health payload consumed by the plugin platform and Admin health view.
|
||||
*/
|
||||
const checkBangDreamHealth = async () => {
|
||||
const checkedAt = formatBangDreamCheckedAt(new Date());
|
||||
try {
|
||||
await context.checkHealth();
|
||||
return {
|
||||
checkedAt,
|
||||
message: 'BangDream 插件可用',
|
||||
status: 'healthy',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
checkedAt,
|
||||
message: normalizeError(error) || 'BangDream 插件不可用',
|
||||
status: 'degraded',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行 BangDream 插件局部步骤。
|
||||
* @param operationKey - operationKey 输入;影响 executeOperation 的返回值。
|
||||
@ -176,27 +198,11 @@ function buildBangDreamRuntimePlugin(
|
||||
/**
|
||||
* 执行 BangDream回调。
|
||||
*/
|
||||
health: () => context.checkHealth(),
|
||||
health: checkBangDreamHealth,
|
||||
/**
|
||||
* 执行 BangDream回调。
|
||||
*/
|
||||
healthCheck: async () => {
|
||||
const checkedAt = formatBangDreamCheckedAt(new Date());
|
||||
try {
|
||||
await context.checkHealth();
|
||||
return {
|
||||
checkedAt,
|
||||
message: 'BangDream 插件可用',
|
||||
status: 'healthy',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
checkedAt,
|
||||
message: normalizeError(error) || 'BangDream 插件不可用',
|
||||
status: 'degraded',
|
||||
};
|
||||
}
|
||||
},
|
||||
healthCheck: checkBangDreamHealth,
|
||||
key: options.pluginKey || 'bangdream',
|
||||
legacyKeys: options.legacyAliases,
|
||||
name: options.name || 'BangDream 查询',
|
||||
|
||||
@ -851,6 +851,176 @@ describe('QQBot plugin platform lifecycle runtime contract', () => {
|
||||
expect(worker.executeOperation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('continues starting other built-in workers when one worker health check fails', async () => {
|
||||
/**
|
||||
* 创建测试 manifest。
|
||||
* @param pluginKey - 插件 key;用于区分当前被发现的内置插件包。
|
||||
* @returns 可被插件平台解析的内置插件 manifest。
|
||||
*/
|
||||
const createManifest = (pluginKey: string) => ({
|
||||
assets: [],
|
||||
configSchema: {},
|
||||
entry: 'src/index.ts',
|
||||
events: [],
|
||||
legacyAliases: [],
|
||||
migrations: [],
|
||||
minApiSdkVersion: '1.0.0',
|
||||
name: pluginKey,
|
||||
operations: [
|
||||
{
|
||||
handlerName: 'echo',
|
||||
key: `${pluginKey}.echo`,
|
||||
name: 'Echo',
|
||||
permissions: [],
|
||||
timeoutMs: 123,
|
||||
},
|
||||
],
|
||||
permissions: [],
|
||||
pluginKey,
|
||||
runtime: {
|
||||
maxConcurrency: 1,
|
||||
memoryMb: 128,
|
||||
timeoutMs: 456,
|
||||
workerType: 'node-worker',
|
||||
},
|
||||
version: '0.1.0',
|
||||
});
|
||||
const failingManifest = createManifest('failing-plugin');
|
||||
const healthyManifest = createManifest('healthy-plugin');
|
||||
const savedPlugins: any[] = [];
|
||||
const savedVersions: any[] = [];
|
||||
const savedInstallations: any[] = [];
|
||||
const runtimeEvents: any[] = [];
|
||||
const pluginRepository = {
|
||||
find: jest.fn(async () => savedPlugins),
|
||||
findAndCount: jest.fn(async () => [savedPlugins, savedPlugins.length]),
|
||||
findOne: jest.fn(async ({ where }: any) =>
|
||||
savedPlugins.find((plugin) => plugin.id === where?.id),
|
||||
),
|
||||
save: jest.fn(async (value) => {
|
||||
const row = { id: `plugin-${value.pluginKey}`, ...value };
|
||||
savedPlugins.push(row);
|
||||
return row;
|
||||
}),
|
||||
update: jest.fn(async () => ({ affected: 1 })),
|
||||
};
|
||||
const versionRepository = {
|
||||
findOne: jest.fn(async ({ where }: any) =>
|
||||
savedVersions.find((version) => version.id === where?.id),
|
||||
),
|
||||
save: jest.fn(async (value) => {
|
||||
const row = { id: `version-${value.pluginId}`, ...value };
|
||||
savedVersions.push(row);
|
||||
return row;
|
||||
}),
|
||||
update: jest.fn(async () => ({ affected: 1 })),
|
||||
};
|
||||
const installationRepository = {
|
||||
find: jest.fn(async () => savedInstallations),
|
||||
findAndCount: jest.fn(async () => [
|
||||
savedInstallations,
|
||||
savedInstallations.length,
|
||||
]),
|
||||
findOne: jest.fn(async ({ where }: any) =>
|
||||
savedInstallations.find(
|
||||
(installation) => installation.id === where?.id,
|
||||
),
|
||||
),
|
||||
save: jest.fn(async (value) => {
|
||||
const row = { id: `installation-${value.pluginId}`, ...value };
|
||||
savedInstallations.push(row);
|
||||
return row;
|
||||
}),
|
||||
update: jest.fn(async () => ({ affected: 1 })),
|
||||
};
|
||||
const runtimeEventRepository = {
|
||||
save: jest.fn(async (value) => {
|
||||
runtimeEvents.push(value);
|
||||
return value;
|
||||
}),
|
||||
};
|
||||
const failingWorker = {
|
||||
activate: jest.fn(async () => ({ ok: true })),
|
||||
deactivate: jest.fn(async () => ({ ok: true })),
|
||||
dispose: jest.fn(async () => undefined),
|
||||
drainRuntimeEvents: jest.fn(() => []),
|
||||
executeOperation: jest.fn(),
|
||||
handleEvent: jest.fn(),
|
||||
health: jest.fn(async () => {
|
||||
throw new Error('Bestdori unavailable');
|
||||
}),
|
||||
load: jest.fn(async () => ({ ok: true })),
|
||||
};
|
||||
const healthyWorker = {
|
||||
activate: jest.fn(async () => ({ ok: true })),
|
||||
deactivate: jest.fn(async () => ({ ok: true })),
|
||||
dispose: jest.fn(async () => undefined),
|
||||
drainRuntimeEvents: jest.fn(() => []),
|
||||
executeOperation: jest.fn(async () => ({ replyText: 'ok' })),
|
||||
handleEvent: jest.fn(async () => true),
|
||||
health: jest.fn(async () => ({ ok: true })),
|
||||
load: jest.fn(async () => ({ ok: true })),
|
||||
};
|
||||
const runtimeFactory = {
|
||||
create: jest.fn((_installation, version) =>
|
||||
version.manifestJson.pluginKey === 'failing-plugin'
|
||||
? failingWorker
|
||||
: healthyWorker,
|
||||
),
|
||||
};
|
||||
const packageSource = {
|
||||
discoverPackages: jest.fn(async () =>
|
||||
[failingManifest, healthyManifest].map((manifest) => ({
|
||||
entry: manifest.entry,
|
||||
entryFile: `D:/plugins/${manifest.pluginKey}/src/index.ts`,
|
||||
manifest,
|
||||
packageRoot: `D:/plugins/${manifest.pluginKey}`,
|
||||
pluginKey: manifest.pluginKey,
|
||||
})),
|
||||
),
|
||||
};
|
||||
const service = new (QqbotPluginPlatformService as any)(
|
||||
pluginRepository,
|
||||
versionRepository,
|
||||
installationRepository,
|
||||
{ update: jest.fn(async () => ({ affected: 1 })) },
|
||||
{ update: jest.fn(async () => ({ affected: 1 })) },
|
||||
{ find: jest.fn(async () => []) },
|
||||
{},
|
||||
{},
|
||||
runtimeEventRepository,
|
||||
undefined,
|
||||
runtimeFactory,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
packageSource,
|
||||
) as QqbotPluginPlatformService;
|
||||
|
||||
await expect(service.onModuleInit()).resolves.toBeUndefined();
|
||||
|
||||
expect(failingWorker.dispose).toHaveBeenCalled();
|
||||
expect(healthyWorker.health).toHaveBeenCalled();
|
||||
await expect(
|
||||
service.executeOperation({
|
||||
input: {},
|
||||
operationKey: 'healthy-plugin.echo',
|
||||
pluginKey: 'healthy-plugin',
|
||||
}),
|
||||
).resolves.toEqual({ replyText: 'ok' });
|
||||
expect(runtimeEvents).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
eventType: 'builtin-start-failed',
|
||||
level: 'error',
|
||||
safeSummary: expect.objectContaining({
|
||||
message: 'Bestdori unavailable',
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('persists built-in installations before syncing manifest tasks', async () => {
|
||||
const manifest = {
|
||||
assets: [],
|
||||
|
||||
@ -214,7 +214,10 @@ describe('BangDream package entry', () => {
|
||||
});
|
||||
|
||||
await plugin.activate();
|
||||
await expect(plugin.health()).resolves.toBe(true);
|
||||
await expect(plugin.health()).resolves.toMatchObject({
|
||||
message: 'BangDream 插件可用',
|
||||
status: 'healthy',
|
||||
});
|
||||
await expect(
|
||||
plugin.executeOperation('bangdream.song.search', { text: '夏祭り' }),
|
||||
).resolves.toMatchObject({
|
||||
@ -236,6 +239,20 @@ describe('BangDream package entry', () => {
|
||||
expect(manifestOperations).toHaveLength(15);
|
||||
});
|
||||
|
||||
it('reports degraded health when catalog health fails instead of throwing', async () => {
|
||||
mockContext.checkHealth.mockRejectedValueOnce(
|
||||
new Error('Bestdori unavailable'),
|
||||
);
|
||||
const plugin = createPlugin({
|
||||
operations: manifestOperations,
|
||||
});
|
||||
|
||||
await expect(plugin.health()).resolves.toMatchObject({
|
||||
message: 'Bestdori unavailable',
|
||||
status: 'degraded',
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizes operation errors without an application-service wrapper', async () => {
|
||||
const plugin = createPlugin({
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user