feat: 新增 NapCat WebUI Admin 入口
This commit is contained in:
parent
d038501228
commit
c2cbdf808a
@ -4,15 +4,18 @@ import { requestClient } from '#/api/request';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
cancelQqbotAccountScan,
|
cancelQqbotAccountScan,
|
||||||
|
createQqbotNapcatWebuiSession,
|
||||||
getNapcatLoginProgressLabel,
|
getNapcatLoginProgressLabel,
|
||||||
getNapcatNewDeviceStatusMessage,
|
getNapcatNewDeviceStatusMessage,
|
||||||
getQqbotAccountScanEventsUrl,
|
getQqbotAccountScanEventsUrl,
|
||||||
getQqbotAccountScanStatus,
|
getQqbotAccountScanStatus,
|
||||||
getQqbotNapcatRuntimeDetail,
|
getQqbotNapcatRuntimeDetail,
|
||||||
|
heartbeatQqbotNapcatWebuiSession,
|
||||||
mergeNapcatAccountScanResult,
|
mergeNapcatAccountScanResult,
|
||||||
NAPCAT_LOGIN_PROGRESS_LABELS,
|
NAPCAT_LOGIN_PROGRESS_LABELS,
|
||||||
refreshQqbotAccountScanQrcode,
|
refreshQqbotAccountScanQrcode,
|
||||||
resolveNapcatLoginDisplayQrcode,
|
resolveNapcatLoginDisplayQrcode,
|
||||||
|
revokeQqbotNapcatWebuiSession,
|
||||||
startQqbotAccountScanCreate,
|
startQqbotAccountScanCreate,
|
||||||
startQqbotAccountScanRefresh,
|
startQqbotAccountScanRefresh,
|
||||||
submitQqbotAccountScanCaptcha,
|
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>;
|
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 {
|
export interface AccountScanResult {
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
captchaUrl?: 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) {
|
function buildApiUrl(path: string) {
|
||||||
const baseUrl = requestClient.getBaseUrl() || '';
|
const baseUrl = requestClient.getBaseUrl() || '';
|
||||||
if (!baseUrl) return path;
|
if (!baseUrl) return path;
|
||||||
|
|||||||
@ -39,6 +39,16 @@ const routes: RouteRecordRaw[] = [
|
|||||||
name: 'QqBotAccountConfig',
|
name: 'QqBotAccountConfig',
|
||||||
path: '/qqbot/account/config',
|
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'),
|
component: () => import('#/views/qqbot/rule/list'),
|
||||||
meta: {
|
meta: {
|
||||||
|
|||||||
@ -173,6 +173,14 @@ export default defineComponent({
|
|||||||
onClick: openRuntimeProfile,
|
onClick: openRuntimeProfile,
|
||||||
permissionCodes: ['QqBot:Account:Config'],
|
permissionCodes: ['QqBot:Account:Config'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
disabled: (row) =>
|
||||||
|
!row.napcat?.containerName || getWebuiStatus(row) === 'offline',
|
||||||
|
key: 'napcatWebui',
|
||||||
|
label: 'WebUI',
|
||||||
|
onClick: openNapcatWebui,
|
||||||
|
permissionCodes: ['QqBot:Account:WebUI'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
confirm: (row) =>
|
confirm: (row) =>
|
||||||
`确认删除账号「${row.selfId}」吗?该操作会同时删除该账号专属的 NapCat 容器。`,
|
`确认删除账号「${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) {
|
function openEdit(row: QqbotApi.Account) {
|
||||||
editingId.value = row.id;
|
editingId.value = row.id;
|
||||||
accountModalApi
|
accountModalApi
|
||||||
|
|||||||
@ -12,6 +12,19 @@ const accountRoot = resolve(
|
|||||||
const readAccountSource = (relativePath: string) =>
|
const readAccountSource = (relativePath: string) =>
|
||||||
readFileSync(resolve(accountRoot, relativePath), 'utf8');
|
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', () => {
|
describe('qqbot account NapCat login view boundary', () => {
|
||||||
it('keeps login session state out of account/list.tsx', () => {
|
it('keeps login session state out of account/list.tsx', () => {
|
||||||
const source = readAccountSource('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);
|
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