feat: 新增 NapCat WebUI Admin 入口
This commit is contained in:
parent
d038501228
commit
c2cbdf808a
@ -4,15 +4,18 @@ import { requestClient } from '#/api/request';
|
||||
|
||||
import {
|
||||
cancelQqbotAccountScan,
|
||||
createQqbotNapcatWebuiSession,
|
||||
getNapcatLoginProgressLabel,
|
||||
getNapcatNewDeviceStatusMessage,
|
||||
getQqbotAccountScanEventsUrl,
|
||||
getQqbotAccountScanStatus,
|
||||
getQqbotNapcatRuntimeDetail,
|
||||
heartbeatQqbotNapcatWebuiSession,
|
||||
mergeNapcatAccountScanResult,
|
||||
NAPCAT_LOGIN_PROGRESS_LABELS,
|
||||
refreshQqbotAccountScanQrcode,
|
||||
resolveNapcatLoginDisplayQrcode,
|
||||
revokeQqbotNapcatWebuiSession,
|
||||
startQqbotAccountScanCreate,
|
||||
startQqbotAccountScanRefresh,
|
||||
submitQqbotAccountScanCaptcha,
|
||||
@ -234,4 +237,52 @@ describe('napcat login display helpers', () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('owns NapCat WebUI gateway session caller routes', async () => {
|
||||
const sessionResult = {
|
||||
account: {
|
||||
id: 'account-1',
|
||||
name: 'preview',
|
||||
selfId: '10001',
|
||||
},
|
||||
container: {
|
||||
id: 'container-1',
|
||||
name: 'kt-qqbot-napcat-10001',
|
||||
webuiStatus: 'online' as const,
|
||||
},
|
||||
expiresAt: 1_782_000_000,
|
||||
iframeUrl: '/qqbot/napcat/webui/session/session-1/',
|
||||
sessionId: 'session-1',
|
||||
};
|
||||
const lifecycleResult = {
|
||||
sessionId: 'session-1',
|
||||
status: 'active' as const,
|
||||
};
|
||||
vi.mocked(requestClient.post)
|
||||
.mockResolvedValueOnce(sessionResult)
|
||||
.mockResolvedValueOnce(lifecycleResult)
|
||||
.mockResolvedValueOnce({ ...lifecycleResult, status: 'revoked' });
|
||||
|
||||
await expect(
|
||||
createQqbotNapcatWebuiSession({ accountId: 'account-1' }),
|
||||
).resolves.toBe(sessionResult);
|
||||
await expect(heartbeatQqbotNapcatWebuiSession('session-1')).resolves.toBe(
|
||||
lifecycleResult,
|
||||
);
|
||||
await expect(revokeQqbotNapcatWebuiSession('session-1')).resolves.toEqual({
|
||||
sessionId: 'session-1',
|
||||
status: 'revoked',
|
||||
});
|
||||
|
||||
expect(requestClient.post).toHaveBeenCalledWith(
|
||||
'/qqbot/napcat/webui/session',
|
||||
{ accountId: 'account-1' },
|
||||
);
|
||||
expect(requestClient.post).toHaveBeenCalledWith(
|
||||
'/qqbot/napcat/webui/session/session-1/heartbeat',
|
||||
);
|
||||
expect(requestClient.post).toHaveBeenCalledWith(
|
||||
'/qqbot/napcat/webui/session/session-1/revoke',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -24,6 +24,36 @@ export namespace QqbotNapcatApi {
|
||||
sessionBehaviorProfile?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface WebuiGatewaySessionAccount {
|
||||
id: string;
|
||||
name: string;
|
||||
selfId: string;
|
||||
}
|
||||
|
||||
export interface WebuiGatewaySessionContainer {
|
||||
id: string;
|
||||
name: string;
|
||||
webuiStatus: 'offline' | 'online' | 'unknown';
|
||||
}
|
||||
|
||||
export interface WebuiGatewaySession {
|
||||
account: WebuiGatewaySessionAccount;
|
||||
container: WebuiGatewaySessionContainer;
|
||||
expiresAt: number;
|
||||
iframeUrl: string;
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
export interface WebuiGatewaySessionCreateBody {
|
||||
accountId: string;
|
||||
}
|
||||
|
||||
export interface WebuiGatewayLifecycleResult {
|
||||
expiresAt?: number;
|
||||
sessionId: string;
|
||||
status: 'active' | 'revoked';
|
||||
}
|
||||
|
||||
export interface AccountScanResult {
|
||||
accountId?: string;
|
||||
captchaUrl?: string;
|
||||
@ -218,6 +248,36 @@ export function getQqbotNapcatRuntimeDetail(accountId: string) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a short-lived gateway session for opening one account's NapCat WebUI.
|
||||
*/
|
||||
export function createQqbotNapcatWebuiSession(
|
||||
data: QqbotNapcatApi.WebuiGatewaySessionCreateBody,
|
||||
) {
|
||||
return requestClient.post<QqbotNapcatApi.WebuiGatewaySession>(
|
||||
'/qqbot/napcat/webui/session',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends an active NapCat WebUI gateway session while the page is alive.
|
||||
*/
|
||||
export function heartbeatQqbotNapcatWebuiSession(sessionId: string) {
|
||||
return requestClient.post<QqbotNapcatApi.WebuiGatewayLifecycleResult>(
|
||||
`/qqbot/napcat/webui/session/${sessionId}/heartbeat`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revokes a NapCat WebUI gateway session when the page leaves the WebUI view.
|
||||
*/
|
||||
export function revokeQqbotNapcatWebuiSession(sessionId: string) {
|
||||
return requestClient.post<QqbotNapcatApi.WebuiGatewayLifecycleResult>(
|
||||
`/qqbot/napcat/webui/session/${sessionId}/revoke`,
|
||||
);
|
||||
}
|
||||
|
||||
function buildApiUrl(path: string) {
|
||||
const baseUrl = requestClient.getBaseUrl() || '';
|
||||
if (!baseUrl) return path;
|
||||
|
||||
@ -39,6 +39,16 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'QqBotAccountConfig',
|
||||
path: '/qqbot/account/config',
|
||||
},
|
||||
{
|
||||
component: () => import('#/views/qqbot/account/napcat-webui'),
|
||||
meta: {
|
||||
activePath: '/qqbot/account',
|
||||
hideInMenu: true,
|
||||
title: 'NapCat WebUI',
|
||||
},
|
||||
name: 'QqBotAccountNapcatWebui',
|
||||
path: '/qqbot/account/:accountId/napcat-webui',
|
||||
},
|
||||
{
|
||||
component: () => import('#/views/qqbot/rule/list'),
|
||||
meta: {
|
||||
|
||||
@ -173,6 +173,14 @@ export default defineComponent({
|
||||
onClick: openRuntimeProfile,
|
||||
permissionCodes: ['QqBot:Account:Config'],
|
||||
},
|
||||
{
|
||||
disabled: (row) =>
|
||||
!row.napcat?.containerName || getWebuiStatus(row) === 'offline',
|
||||
key: 'napcatWebui',
|
||||
label: 'WebUI',
|
||||
onClick: openNapcatWebui,
|
||||
permissionCodes: ['QqBot:Account:WebUI'],
|
||||
},
|
||||
{
|
||||
confirm: (row) =>
|
||||
`确认删除账号「${row.selfId}」吗?该操作会同时删除该账号专属的 NapCat 容器。`,
|
||||
@ -521,6 +529,16 @@ export default defineComponent({
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the route that will own the NapCat WebUI session lifecycle.
|
||||
*/
|
||||
function openNapcatWebui(row: QqbotApi.Account) {
|
||||
void router.push({
|
||||
name: 'QqBotAccountNapcatWebui',
|
||||
params: { accountId: row.id },
|
||||
});
|
||||
}
|
||||
|
||||
function openEdit(row: QqbotApi.Account) {
|
||||
editingId.value = row.id;
|
||||
accountModalApi
|
||||
|
||||
@ -12,6 +12,19 @@ const accountRoot = resolve(
|
||||
const readAccountSource = (relativePath: string) =>
|
||||
readFileSync(resolve(accountRoot, relativePath), 'utf8');
|
||||
|
||||
/**
|
||||
* Reads QQBot router module source for route boundary assertions.
|
||||
*/
|
||||
const readRouteSource = (relativePath: string) =>
|
||||
readFileSync(
|
||||
resolve(
|
||||
cwd(),
|
||||
'apps/web-antdv-next/src/router/routes/modules',
|
||||
relativePath,
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
describe('qqbot account NapCat login view boundary', () => {
|
||||
it('keeps login session state out of account/list.tsx', () => {
|
||||
const source = readAccountSource('list.tsx');
|
||||
@ -37,4 +50,25 @@ describe('qqbot account NapCat login view boundary', () => {
|
||||
);
|
||||
expect(existsSync(resolve(accountRoot, 'napcat/qrcode.ts'))).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps WebUI gateway lifecycle logic out of account/list.tsx', () => {
|
||||
const source = readAccountSource('list.tsx');
|
||||
|
||||
expect(source).toContain('QqBotAccountNapcatWebui');
|
||||
expect(source).toContain('QqBot:Account:WebUI');
|
||||
expect(source).not.toContain('createQqbotNapcatWebuiSession');
|
||||
expect(source).not.toContain('heartbeatQqbotNapcatWebuiSession');
|
||||
expect(source).not.toContain('revokeQqbotNapcatWebuiSession');
|
||||
expect(source).not.toContain('<iframe');
|
||||
expect(source).not.toContain('iframe');
|
||||
});
|
||||
|
||||
it('registers the hidden NapCat WebUI page route under QQBot account', () => {
|
||||
const source = readRouteSource('qqbot.ts');
|
||||
|
||||
expect(source).toContain('QqBotAccountNapcatWebui');
|
||||
expect(source).toContain('/qqbot/account/:accountId/napcat-webui');
|
||||
expect(source).toContain('hideInMenu: true');
|
||||
expect(source).toContain("activePath: '/qqbot/account'");
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user