feat(admin): 搭建 Muse 管理端治理页面
This commit is contained in:
parent
9df67b2794
commit
912ec7d77f
@ -0,0 +1,140 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
createCallAttributionJobApi,
|
||||
createNewApiBindingApi,
|
||||
createQuotaAdjustmentApi,
|
||||
createQuotaRequestApi,
|
||||
getBalanceSnapshotsApi,
|
||||
getCallAttributionJobApi,
|
||||
getIntegrationCallByCorrelationApi,
|
||||
getUserEntitlementsApi,
|
||||
listAccountUsersApi,
|
||||
listPurchaseRecordsApi,
|
||||
listQuotaAdjustmentsApi,
|
||||
listUsageRecordsApi,
|
||||
} from '../index';
|
||||
|
||||
const { museAdminApiMock } = vi.hoisted(() => ({
|
||||
museAdminApiMock: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('#/api/muse/client', () => ({
|
||||
museAdminApi: museAdminApiMock,
|
||||
}));
|
||||
|
||||
describe('muse account api', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('按契约查询用户账户摘要列表', async () => {
|
||||
await listAccountUsersApi({ pageNo: 1, pageSize: 20, status: 'active' });
|
||||
|
||||
expect(museAdminApiMock.get).toHaveBeenCalledWith('/account/users', {
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
status: 'active',
|
||||
});
|
||||
});
|
||||
|
||||
it('按用户查询权益、配额调整和余额快照', async () => {
|
||||
await getUserEntitlementsApi('user/1');
|
||||
await listQuotaAdjustmentsApi('user/1', {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
sourceType: 'manual',
|
||||
});
|
||||
await getBalanceSnapshotsApi('user/1', { pageNo: 1, pageSize: 10 });
|
||||
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/account/users/user%2F1/entitlements',
|
||||
);
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/account/users/user%2F1/quota-adjustments',
|
||||
{ pageNo: 1, pageSize: 10, sourceType: 'manual' },
|
||||
);
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'/account/users/user%2F1/balance-snapshots',
|
||||
{ pageNo: 1, pageSize: 10 },
|
||||
);
|
||||
});
|
||||
|
||||
it('提交需要审计和幂等键的账户命令', async () => {
|
||||
await createQuotaAdjustmentApi('u1', {
|
||||
adjustments: [
|
||||
{
|
||||
beforeSnapshot: { limit: 100, remaining: 20, used: 80 },
|
||||
delta: 10,
|
||||
resourceType: 'ai_tokens',
|
||||
},
|
||||
],
|
||||
commandId: 'cmd-1',
|
||||
reason: '人工补偿',
|
||||
});
|
||||
await createNewApiBindingApi('u1', {
|
||||
commandId: 'cmd-2',
|
||||
reason: '绑定重验',
|
||||
});
|
||||
await createQuotaRequestApi('u1', {
|
||||
commandId: 'cmd-3',
|
||||
requestType: 'balance_config',
|
||||
});
|
||||
|
||||
expect(museAdminApiMock.post).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/account/users/u1/quota-adjustments',
|
||||
expect.objectContaining({ commandId: 'cmd-1', reason: '人工补偿' }),
|
||||
);
|
||||
expect(museAdminApiMock.post).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/account/users/u1/new-api-binding',
|
||||
expect.objectContaining({ commandId: 'cmd-2' }),
|
||||
);
|
||||
expect(museAdminApiMock.post).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'/account/users/u1/quota-requests',
|
||||
expect.objectContaining({ commandId: 'cmd-3' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('查询 New-API 调用归属、外部调用、用量和购买摘要', async () => {
|
||||
await createCallAttributionJobApi({
|
||||
commandId: 'cmd-4',
|
||||
correlationId: 'corr-1',
|
||||
});
|
||||
await getCallAttributionJobApi('job/1');
|
||||
await getIntegrationCallByCorrelationApi('corr/1');
|
||||
await listUsageRecordsApi({ pageNo: 1, pageSize: 20, userId: 'u1' });
|
||||
await listPurchaseRecordsApi({ pageNo: 1, pageSize: 20, status: 'failed' });
|
||||
|
||||
expect(museAdminApiMock.post).toHaveBeenCalledWith(
|
||||
'/account/call-attribution-jobs',
|
||||
expect.objectContaining({ commandId: 'cmd-4' }),
|
||||
);
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/account/call-attribution-jobs/job%2F1',
|
||||
);
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/account/integration-calls/by-correlation/corr%2F1',
|
||||
);
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'/account/usage-records',
|
||||
{ pageNo: 1, pageSize: 20, userId: 'u1' },
|
||||
);
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
'/account/purchase-records',
|
||||
{ pageNo: 1, pageSize: 20, status: 'failed' },
|
||||
);
|
||||
});
|
||||
});
|
||||
599
muse-admin/apps/web-antd/src/api/muse/account/index.ts
Normal file
599
muse-admin/apps/web-antd/src/api/muse/account/index.ts
Normal file
@ -0,0 +1,599 @@
|
||||
import type { PageResult } from '@vben/request';
|
||||
|
||||
import { museAdminApi } from '#/api/muse/client';
|
||||
|
||||
/** Muse 管理端通用 ID,后端对业务 ID 统一按字符串安全处理。 */
|
||||
export type MuseAdminId = number | string;
|
||||
|
||||
/** 管理端分页查询参数。 */
|
||||
export interface MuseAdminPageQuery {
|
||||
/** 查询参数索引签名,兼容 Muse client 的可序列化参数约束。 */
|
||||
[key: string]: boolean | number | string | undefined;
|
||||
/** 当前页码,从 1 开始。 */
|
||||
pageNo: number;
|
||||
/** 每页数量。 */
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
/** 账户状态:正常、受限、封禁。 */
|
||||
export type AccountStatus = 'active' | 'restricted' | 'suspended';
|
||||
|
||||
/** 配额状态:正常、低余量、耗尽、过期。 */
|
||||
export type QuotaStatus = 'expired' | 'exhausted' | 'low' | 'normal';
|
||||
|
||||
/** New-API 网关绑定状态。 */
|
||||
export type NewApiBindingStatus =
|
||||
| 'bound'
|
||||
| 'pending'
|
||||
| 'sync_failed'
|
||||
| 'unbound';
|
||||
|
||||
/** 配额调整来源类型。 */
|
||||
export type QuotaAdjustmentSourceType =
|
||||
| 'export_rollback'
|
||||
| 'manual'
|
||||
| 'market_compensation'
|
||||
| 'newapi_callback'
|
||||
| 'plan_change';
|
||||
|
||||
/** 调用归属状态。 */
|
||||
export type AttributionStatus = 'attributed' | 'failed' | 'pending';
|
||||
|
||||
/** 购买记录状态。 */
|
||||
export type PurchaseStatus = 'completed' | 'failed' | 'processing' | 'refunded';
|
||||
|
||||
/** 账户列表查询条件。 */
|
||||
export interface AccountUserQuery extends MuseAdminPageQuery {
|
||||
/** 昵称或账号 ID 关键词。 */
|
||||
keyword?: string;
|
||||
/** 账号状态筛选。 */
|
||||
status?: AccountStatus;
|
||||
/** 权益来源筛选。 */
|
||||
entitlementSource?: string;
|
||||
}
|
||||
|
||||
/** 管理端用户账户治理摘要。 */
|
||||
export interface AdminAccountUserSummary {
|
||||
/** 用户 ID。 */
|
||||
userId: string;
|
||||
/** 用户昵称,允许为脱敏昵称。 */
|
||||
nickname: string;
|
||||
/** 头像 URL。 */
|
||||
avatar?: string;
|
||||
/** 账号状态。 */
|
||||
status: AccountStatus;
|
||||
/** 权益来源摘要。 */
|
||||
entitlementSource: string;
|
||||
/** 配额状态。 */
|
||||
quotaStatus: QuotaStatus;
|
||||
/** New-API 网关绑定状态。 */
|
||||
newApiBindingStatus?: NewApiBindingStatus;
|
||||
/** 风险标记列表。 */
|
||||
riskFlags?: string[];
|
||||
/** 注册时间。 */
|
||||
createdAt?: string;
|
||||
/** 最近活跃时间。 */
|
||||
lastActiveAt?: string;
|
||||
}
|
||||
|
||||
/** 用户权益项。 */
|
||||
export interface Entitlement {
|
||||
/** 权益 ID。 */
|
||||
id: string;
|
||||
/** 权益资源类型,例如 ai_tokens、storage、publish_slots。 */
|
||||
resourceType: string;
|
||||
/** 额度上限,-1 表示无限制。 */
|
||||
limit: number;
|
||||
/** 已使用量。 */
|
||||
used: number;
|
||||
/** 剩余额度。 */
|
||||
remaining?: number;
|
||||
/** 权益来源,例如套餐、管理员授权或市场补偿。 */
|
||||
source?: string;
|
||||
/** 到期时间。 */
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
/** 用户配额项。 */
|
||||
export interface Quota {
|
||||
/** 配额 ID。 */
|
||||
id: string;
|
||||
/** 配额资源类型。 */
|
||||
resourceType: string;
|
||||
/** 总配额。 */
|
||||
total: number;
|
||||
/** 已使用量。 */
|
||||
used?: number;
|
||||
/** 剩余配额。 */
|
||||
remaining: number;
|
||||
/** 配额重置时间。 */
|
||||
resetAt: string;
|
||||
/** 每分钟频率限制。 */
|
||||
limitPerMinute?: number;
|
||||
}
|
||||
|
||||
/** 管理端用户权益和配额详情。 */
|
||||
export interface AdminUserEntitlementDetail {
|
||||
/** 用户 ID。 */
|
||||
userId: string;
|
||||
/** 权益列表。 */
|
||||
entitlements: Entitlement[];
|
||||
/** 配额列表。 */
|
||||
quotas: Quota[];
|
||||
/** 权益来源说明。 */
|
||||
entitlementSource?: string;
|
||||
/** 最近权益变更时间。 */
|
||||
lastModifiedAt?: string;
|
||||
/** 最近变更操作者脱敏摘要。 */
|
||||
lastModifiedBy?: string;
|
||||
}
|
||||
|
||||
/** 配额快照摘要。 */
|
||||
export interface QuotaSnapshot {
|
||||
/** 额度上限。 */
|
||||
limit?: number;
|
||||
/** 已使用量。 */
|
||||
used?: number;
|
||||
/** 剩余额度。 */
|
||||
remaining?: number;
|
||||
}
|
||||
|
||||
/** 配额调整项。 */
|
||||
export interface QuotaAdjustmentItem {
|
||||
/** 权益资源类型。 */
|
||||
resourceType: string;
|
||||
/** 调整增量,正数增加,负数减少。 */
|
||||
delta: number;
|
||||
/** 调整前权益快照,用于后端校验和审计。 */
|
||||
beforeSnapshot: QuotaSnapshot;
|
||||
/** 新的到期时间。 */
|
||||
expiresAt?: string;
|
||||
}
|
||||
|
||||
/** 管理端配额调整请求。 */
|
||||
export interface QuotaAdjustmentRequest {
|
||||
/** 幂等键,防止重复调整。 */
|
||||
commandId: string;
|
||||
/** 调整项列表。 */
|
||||
adjustments: QuotaAdjustmentItem[];
|
||||
/** 调整原因,写入审计日志。 */
|
||||
reason: string;
|
||||
/** 关联的外部调用 ID。 */
|
||||
correlationId?: string;
|
||||
/** 审批或复核引用。 */
|
||||
approvalRef?: string;
|
||||
}
|
||||
|
||||
/** 配额调整结果。 */
|
||||
export interface QuotaAdjustmentResult {
|
||||
/** 调整记录 ID。 */
|
||||
adjustmentId: string;
|
||||
/** 请求幂等键。 */
|
||||
commandId: string;
|
||||
/** 调整结果状态。 */
|
||||
status: 'applied' | 'idempotency_conflict' | 'idempotent_hit';
|
||||
/** 调整后权益快照。 */
|
||||
afterSnapshot?: QuotaSnapshot & {
|
||||
/** 权益资源类型。 */
|
||||
resourceType?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/** 配额调整记录查询条件。 */
|
||||
export interface QuotaAdjustmentQuery extends MuseAdminPageQuery {
|
||||
/** 调整来源类型筛选。 */
|
||||
sourceType?: QuotaAdjustmentSourceType;
|
||||
}
|
||||
|
||||
/** 配额调整 ledger 记录。 */
|
||||
export interface QuotaAdjustmentLedgerEntry {
|
||||
/** 调整记录 ID。 */
|
||||
adjustmentId: string;
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
/** 调整来源类型。 */
|
||||
sourceType: QuotaAdjustmentSourceType;
|
||||
/** 权益资源类型。 */
|
||||
resourceType: string;
|
||||
/** 调整增量。 */
|
||||
delta: number;
|
||||
/** 调整前快照。 */
|
||||
beforeSnapshot?: QuotaSnapshot;
|
||||
/** 调整后快照。 */
|
||||
afterSnapshot?: QuotaSnapshot;
|
||||
/** 关联外部调用 ID。 */
|
||||
correlationId?: string;
|
||||
/** 调整原因。 */
|
||||
reason?: string;
|
||||
/** 操作者脱敏摘要。 */
|
||||
operatorSummary: string;
|
||||
/** 审计状态。 */
|
||||
auditStatus?: 'audited' | 'pending_audit';
|
||||
/** 调整时间。 */
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/** New-API 绑定列表查询条件。 */
|
||||
export interface NewApiBindingQuery extends MuseAdminPageQuery {
|
||||
/** 绑定状态筛选。 */
|
||||
bindingStatus?: NewApiBindingStatus;
|
||||
}
|
||||
|
||||
/** New-API 网关用户绑定摘要。 */
|
||||
export interface NewApiBindingSummary {
|
||||
/** Muse 用户 ID。 */
|
||||
userId: string;
|
||||
/** 用户昵称。 */
|
||||
nickname?: string;
|
||||
/** 绑定状态。 */
|
||||
bindingStatus: NewApiBindingStatus;
|
||||
/** 绑定时间。 */
|
||||
boundAt?: string;
|
||||
/** 最近同步时间。 */
|
||||
lastSyncAt?: string;
|
||||
/** 同步失败原因,仅失败时返回。 */
|
||||
syncErrorMessage?: string;
|
||||
}
|
||||
|
||||
/** 创建或刷新 New-API 绑定请求。 */
|
||||
export interface NewApiBindingCreateRequest {
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
/** 是否强制刷新已有绑定。 */
|
||||
forceRefresh?: boolean;
|
||||
/** 创建或刷新原因。 */
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/** New-API 网关绑定结果。 */
|
||||
export interface NewApiBindingResult {
|
||||
/** 绑定记录 ID。 */
|
||||
bindingId: string;
|
||||
/** 绑定操作结果状态。 */
|
||||
status: 'created' | 'idempotency_conflict' | 'idempotent_hit' | 'refreshed';
|
||||
/** 请求幂等键。 */
|
||||
commandId: string;
|
||||
/** 绑定时间。 */
|
||||
boundAt?: string;
|
||||
}
|
||||
|
||||
/** 管理端发起 New-API 额度配置请求。 */
|
||||
export interface AdminQuotaRequestCreate {
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
/** 额度配置类型。 */
|
||||
requestType: 'balance_config' | 'plan_config' | 'subscription_sync';
|
||||
/** 请求原因,写入审计日志。 */
|
||||
reason?: string;
|
||||
/** 目标额度配置。 */
|
||||
targetQuota?: {
|
||||
/** 权益资源类型。 */
|
||||
resourceType?: string;
|
||||
/** 目标额度上限。 */
|
||||
limit?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/** 额度配置请求创建结果。 */
|
||||
export interface QuotaRequestResult {
|
||||
/** 额度请求 ID。 */
|
||||
requestId: string;
|
||||
/** 外部调用关联 ID。 */
|
||||
correlationId: string;
|
||||
/** 请求初始状态。 */
|
||||
status: 'idempotent_hit' | 'processing' | 'queued';
|
||||
}
|
||||
|
||||
/** New-API 余额和权益快照条目。 */
|
||||
export interface BalanceSnapshotEntry {
|
||||
/** 快照 ID。 */
|
||||
snapshotId: string;
|
||||
/** 权益资源类型。 */
|
||||
resourceType: string;
|
||||
/** 余额,不暴露供应商路由或成本策略。 */
|
||||
balance: number;
|
||||
/** 已使用配额。 */
|
||||
usedQuota?: number;
|
||||
/** 总配额。 */
|
||||
totalQuota?: number;
|
||||
/** 到期时间。 */
|
||||
expiresAt?: string;
|
||||
/** 快照捕获时间。 */
|
||||
capturedAt: string;
|
||||
/** 快照来源。 */
|
||||
source?: string;
|
||||
}
|
||||
|
||||
/** 创建调用归属 job 请求。 */
|
||||
export interface CallAttributionJobCreate {
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
/** New-API 调用关联 ID。 */
|
||||
correlationId: string;
|
||||
/** 可选的 New-API callId 白名单。 */
|
||||
callIds?: string[];
|
||||
/** 调用审计记录 revision 乐观锁。 */
|
||||
expectedCallRevision?: number;
|
||||
/** 归因校验模式,当前仅支持严格模式。 */
|
||||
verificationMode?: 'strict';
|
||||
/** 归属原因。 */
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/** 调用归属 job 创建结果。 */
|
||||
export interface CallAttributionJobResult {
|
||||
/** 调用归属 job ID。 */
|
||||
jobId: string;
|
||||
/** job 初始状态。 */
|
||||
status: 'idempotent_hit' | 'processing' | 'queued';
|
||||
}
|
||||
|
||||
/** 调用归属 job 详情。 */
|
||||
export interface CallAttributionJobDetail {
|
||||
/** 调用归属 job ID。 */
|
||||
jobId: string;
|
||||
/** 执行状态。 */
|
||||
status:
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'partially_completed'
|
||||
| 'processing'
|
||||
| 'queued';
|
||||
/** 归属用户 ID。 */
|
||||
userId: string;
|
||||
/** 已归属调用数。 */
|
||||
attributedCallCount?: number;
|
||||
/** 待归属调用数。 */
|
||||
pendingCallCount?: number;
|
||||
/** 归属失败调用数。 */
|
||||
failedCallCount?: number;
|
||||
/** 失败原因。 */
|
||||
failedReason?: string;
|
||||
/** 补偿建议。 */
|
||||
compensationSuggestion?: string;
|
||||
/** 创建时间。 */
|
||||
createdAt: string;
|
||||
/** 完成时间。 */
|
||||
completedAt?: string;
|
||||
}
|
||||
|
||||
/** 外部集成调用详情。 */
|
||||
export interface IntegrationCallDetail {
|
||||
/** 外部调用关联 ID。 */
|
||||
correlationId: string;
|
||||
/** 外部调用状态。 */
|
||||
status:
|
||||
| 'attributed'
|
||||
| 'deduplicated'
|
||||
| 'failed'
|
||||
| 'pending_attribution'
|
||||
| 'retrying'
|
||||
| 'success';
|
||||
/** 请求类型。 */
|
||||
requestType?: string;
|
||||
/** 重试组内调用 ID 列表。 */
|
||||
retryGroup?: string[];
|
||||
/** 归属状态。 */
|
||||
attributionStatus?: AttributionStatus;
|
||||
/** 归属用户 ID。 */
|
||||
attributedUserId?: string;
|
||||
/** 首次调用时间。 */
|
||||
createdAt: string;
|
||||
/** 最近重试时间。 */
|
||||
lastRetryAt?: string;
|
||||
/** 错误信息。 */
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
/** 用量摘要查询条件。 */
|
||||
export interface UsageRecordQuery extends MuseAdminPageQuery {
|
||||
/** 用户 ID 筛选。 */
|
||||
userId?: string;
|
||||
/** 统计周期。 */
|
||||
period?: 'billing_cycle' | 'month' | 'today' | 'week';
|
||||
/** 归属状态筛选。 */
|
||||
attributionStatus?: AttributionStatus;
|
||||
}
|
||||
|
||||
/** 按作品归属的用量摘要。 */
|
||||
export interface WorkUsageSummary {
|
||||
/** 作品 ID。 */
|
||||
workId?: string;
|
||||
/** 作品标题,允许为脱敏摘要。 */
|
||||
workTitle?: string;
|
||||
/** Token 数。 */
|
||||
tokens?: number;
|
||||
}
|
||||
|
||||
/** 按智能体归属的用量摘要。 */
|
||||
export interface AgentUsageSummary {
|
||||
/** 智能体 ID。 */
|
||||
agentId?: string;
|
||||
/** 智能体名称。 */
|
||||
agentName?: string;
|
||||
/** Token 数。 */
|
||||
tokens?: number;
|
||||
}
|
||||
|
||||
/** 管理端用量记录。 */
|
||||
export interface AdminUsageRecord {
|
||||
/** 记录 ID。 */
|
||||
recordId: string;
|
||||
/** 用户 ID。 */
|
||||
userId: string;
|
||||
/** 用户昵称。 */
|
||||
nickname?: string;
|
||||
/** 统计周期。 */
|
||||
period: string;
|
||||
/** 总输入 Token 数。 */
|
||||
totalInputTokens?: number;
|
||||
/** 总输出 Token 数。 */
|
||||
totalOutputTokens?: number;
|
||||
/** 总 Token 数。 */
|
||||
totalTokens: number;
|
||||
/** 待归属记录数。 */
|
||||
pendingAttributionCount?: number;
|
||||
/** 归属失败记录数。 */
|
||||
failedAttributionCount?: number;
|
||||
/** 归属状态。 */
|
||||
attributionStatus: AttributionStatus;
|
||||
/** 按作品归属摘要。 */
|
||||
byWork?: WorkUsageSummary[];
|
||||
/** 按智能体归属摘要。 */
|
||||
byAgent?: AgentUsageSummary[];
|
||||
}
|
||||
|
||||
/** 购买记录查询条件。 */
|
||||
export interface PurchaseRecordQuery extends MuseAdminPageQuery {
|
||||
/** 用户 ID 筛选。 */
|
||||
userId?: string;
|
||||
/** 购买状态筛选。 */
|
||||
status?: PurchaseStatus;
|
||||
}
|
||||
|
||||
/** 管理端购买记录摘要。 */
|
||||
export interface AdminPurchaseRecord {
|
||||
/** 记录 ID。 */
|
||||
recordId: string;
|
||||
/** 用户 ID。 */
|
||||
userId: string;
|
||||
/** 用户昵称。 */
|
||||
nickname?: string;
|
||||
/** 资产类型。 */
|
||||
assetType: 'agent' | 'knowledge_base' | 'work';
|
||||
/** 资产名称。 */
|
||||
assetName?: string;
|
||||
/** 购买类型。 */
|
||||
purchaseType?: 'admin_grant' | 'external' | 'free' | 'paid';
|
||||
/** 金额,免费为 0。 */
|
||||
amount?: number;
|
||||
/** 购买状态。 */
|
||||
status: PurchaseStatus;
|
||||
/** 外部订单引用,必须为脱敏摘要。 */
|
||||
externalOrderRef?: string;
|
||||
/** 授权结果摘要。 */
|
||||
authorizationResult?: string;
|
||||
/** 购买时间。 */
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/** 将路径参数编码,避免斜杠等字符破坏 Muse 管理端 URL。 */
|
||||
function encodePath(value: MuseAdminId) {
|
||||
return encodeURIComponent(String(value));
|
||||
}
|
||||
|
||||
/** 查询用户账户治理摘要列表。 */
|
||||
export function listAccountUsersApi(params: AccountUserQuery) {
|
||||
return museAdminApi.get<PageResult<AdminAccountUserSummary>>(
|
||||
'/account/users',
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询指定用户的权益和配额详情。 */
|
||||
export function getUserEntitlementsApi(userId: MuseAdminId) {
|
||||
return museAdminApi.get<AdminUserEntitlementDetail>(
|
||||
`/account/users/${encodePath(userId)}/entitlements`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 人工调整用户配额,必须带原因和幂等键。 */
|
||||
export function createQuotaAdjustmentApi(
|
||||
userId: MuseAdminId,
|
||||
data: QuotaAdjustmentRequest,
|
||||
) {
|
||||
return museAdminApi.post<QuotaAdjustmentResult>(
|
||||
`/account/users/${encodePath(userId)}/quota-adjustments`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询指定用户的配额调整 ledger。 */
|
||||
export function listQuotaAdjustmentsApi(
|
||||
userId: MuseAdminId,
|
||||
params: QuotaAdjustmentQuery,
|
||||
) {
|
||||
return museAdminApi.get<PageResult<QuotaAdjustmentLedgerEntry>>(
|
||||
`/account/users/${encodePath(userId)}/quota-adjustments`,
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询 New-API 网关用户绑定状态列表。 */
|
||||
export function listNewApiBindingsApi(params: NewApiBindingQuery) {
|
||||
return museAdminApi.get<PageResult<NewApiBindingSummary>>(
|
||||
'/account/new-api-bindings',
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
/** 创建或刷新指定用户的 New-API 网关绑定。 */
|
||||
export function createNewApiBindingApi(
|
||||
userId: MuseAdminId,
|
||||
data: NewApiBindingCreateRequest,
|
||||
) {
|
||||
return museAdminApi.post<NewApiBindingResult>(
|
||||
`/account/users/${encodePath(userId)}/new-api-binding`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 向 New-API 发起额度配置请求。 */
|
||||
export function createQuotaRequestApi(
|
||||
userId: MuseAdminId,
|
||||
data: AdminQuotaRequestCreate,
|
||||
) {
|
||||
return museAdminApi.post<QuotaRequestResult>(
|
||||
`/account/users/${encodePath(userId)}/quota-requests`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询指定用户的 New-API 余额和权益快照摘要。 */
|
||||
export function getBalanceSnapshotsApi(
|
||||
userId: MuseAdminId,
|
||||
params: MuseAdminPageQuery,
|
||||
) {
|
||||
return museAdminApi.get<PageResult<BalanceSnapshotEntry>>(
|
||||
`/account/users/${encodePath(userId)}/balance-snapshots`,
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
/** 创建调用归属 job。 */
|
||||
export function createCallAttributionJobApi(data: CallAttributionJobCreate) {
|
||||
return museAdminApi.post<CallAttributionJobResult>(
|
||||
'/account/call-attribution-jobs',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询调用归属 job 的执行状态。 */
|
||||
export function getCallAttributionJobApi(jobId: MuseAdminId) {
|
||||
return museAdminApi.get<CallAttributionJobDetail>(
|
||||
`/account/call-attribution-jobs/${encodePath(jobId)}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 按 correlationId 查询外部调用去重、重试和归属状态。 */
|
||||
export function getIntegrationCallByCorrelationApi(correlationId: MuseAdminId) {
|
||||
return museAdminApi.get<IntegrationCallDetail>(
|
||||
`/account/integration-calls/by-correlation/${encodePath(correlationId)}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询全平台脱敏用量摘要。 */
|
||||
export function listUsageRecordsApi(params: UsageRecordQuery) {
|
||||
return museAdminApi.get<PageResult<AdminUsageRecord>>(
|
||||
'/account/usage-records',
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询全平台脱敏购买记录摘要。 */
|
||||
export function listPurchaseRecordsApi(params: PurchaseRecordQuery) {
|
||||
return museAdminApi.get<PageResult<AdminPurchaseRecord>>(
|
||||
'/account/purchase-records',
|
||||
params,
|
||||
);
|
||||
}
|
||||
543
muse-admin/apps/web-antd/src/api/muse/ai/index.ts
Normal file
543
muse-admin/apps/web-antd/src/api/muse/ai/index.ts
Normal file
@ -0,0 +1,543 @@
|
||||
import { museAdminApi } from '#/api/muse/client';
|
||||
|
||||
/** AI 配置域分页查询基础参数。 */
|
||||
export interface MuseAiPageParams {
|
||||
/** 允许业务查询扩展字段透传到 URL query。 */
|
||||
[key: string]: boolean | number | string | undefined;
|
||||
/** 页码,从 1 开始。 */
|
||||
pageNo: number;
|
||||
/** 每页条数,后端契约上限为 100。 */
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
/** AI 配置域分页返回结构。 */
|
||||
export interface MuseAiPageResult<T> {
|
||||
/** 当前页数据列表。 */
|
||||
list: T[];
|
||||
/** 当前页码。 */
|
||||
pageNo?: number;
|
||||
/** 每页条数。 */
|
||||
pageSize?: number;
|
||||
/** 总记录数。 */
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** Prompt 模板摘要。 */
|
||||
export interface PromptSummaryVO {
|
||||
/** Prompt 唯一标识。 */
|
||||
promptKey: string;
|
||||
/** Prompt 展示名称。 */
|
||||
name?: string;
|
||||
/** 当前激活版本号。 */
|
||||
activeVersion: number;
|
||||
/** 最新版本号。 */
|
||||
latestVersion?: number;
|
||||
/** 模板变量数量。 */
|
||||
variableCount?: number;
|
||||
/** 最近更新时间,ISO date-time 字符串。 */
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** Prompt 列表查询参数。 */
|
||||
export interface PromptPageParams extends MuseAiPageParams {
|
||||
/** Prompt key 精确或模糊筛选,由后端实现匹配语义。 */
|
||||
promptKey?: string;
|
||||
}
|
||||
|
||||
/** Prompt 变量类型。 */
|
||||
export type PromptVariableType = 'boolean' | 'number' | 'string';
|
||||
|
||||
/** Prompt 模板变量定义。 */
|
||||
export interface PromptVariableDTO {
|
||||
/** 变量名称。 */
|
||||
name: string;
|
||||
/** 变量类型。 */
|
||||
type: PromptVariableType;
|
||||
/** 是否必填。 */
|
||||
required?: boolean;
|
||||
/** 默认值,统一以字符串保存,运行时由后端按 type 解析。 */
|
||||
defaultValue?: string;
|
||||
}
|
||||
|
||||
/** 新建 Prompt 版本请求。 */
|
||||
export interface CreatePromptVersionDTO {
|
||||
/** Prompt 正文内容。 */
|
||||
content: string;
|
||||
/** 模板变量定义。 */
|
||||
variables?: PromptVariableDTO[];
|
||||
/** 版本变更说明。 */
|
||||
changeNote?: string;
|
||||
/** 幂等键。 */
|
||||
commandId?: string;
|
||||
}
|
||||
|
||||
/** 新建 Prompt 版本返回。 */
|
||||
export interface CreatePromptVersionResultVO {
|
||||
/** Prompt 唯一标识。 */
|
||||
promptKey?: string;
|
||||
/** 创建出的版本号。 */
|
||||
version?: number;
|
||||
}
|
||||
|
||||
/** 激活 Prompt 版本请求。 */
|
||||
export interface ActivatePromptVersionDTO {
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
/** 激活原因。 */
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/** 激活 Prompt 版本返回。 */
|
||||
export interface ActivatePromptVersionResultVO {
|
||||
/** Prompt 唯一标识。 */
|
||||
promptKey?: string;
|
||||
/** 激活后的版本号。 */
|
||||
activeVersion?: number;
|
||||
}
|
||||
|
||||
/** 管理端 Agent 范围,user 只允许治理摘要只读展示。 */
|
||||
export type AdminAgentScope = 'system' | 'user';
|
||||
|
||||
/** Agent 状态。 */
|
||||
export type AdminAgentStatus = 'active' | 'archived';
|
||||
|
||||
/** Agent 列表筛选范围。 */
|
||||
export type AdminAgentScopeFilter = AdminAgentScope | 'all';
|
||||
|
||||
/** 管理端 Agent 摘要。 */
|
||||
export interface AdminAgentSummaryVO {
|
||||
/** Agent ID,前端按字符串处理以避免 int64 精度风险。 */
|
||||
agentId: string;
|
||||
/** Agent 名称。 */
|
||||
name: string;
|
||||
/** Agent 描述。 */
|
||||
description?: string;
|
||||
/** Agent 范围:system=系统预置,user=用户创建。 */
|
||||
scope: AdminAgentScope;
|
||||
/** 当前激活版本号。 */
|
||||
activeVersion?: number;
|
||||
/** 关联的 Prompt key。 */
|
||||
promptKey?: string;
|
||||
/** Agent 状态。 */
|
||||
status: AdminAgentStatus;
|
||||
/** 最近更新时间,ISO date-time 字符串。 */
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** Agent 列表查询参数。 */
|
||||
export interface AdminAgentPageParams extends MuseAiPageParams {
|
||||
/** Agent 范围筛选,默认 all。 */
|
||||
scope?: AdminAgentScopeFilter;
|
||||
}
|
||||
|
||||
/** 创建系统 Agent 请求。 */
|
||||
export interface CreateSystemAgentDTO {
|
||||
/** 固定为 system;管理端不能创建 user scope Agent。 */
|
||||
scope?: 'system';
|
||||
/** Agent 名称。 */
|
||||
name: string;
|
||||
/** Agent 描述。 */
|
||||
description?: string;
|
||||
/** 关联的 Prompt key。 */
|
||||
promptKey: string;
|
||||
/** 槽位绑定配置,例如知识库、MetaSchema 或模型参数摘要。 */
|
||||
slotBindings?: Record<string, unknown>;
|
||||
/** 已经由 Security facade 批准的系统级工具授权 ID 列表。 */
|
||||
toolGrantIds?: string[];
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
}
|
||||
|
||||
/** 创建系统 Agent 返回。 */
|
||||
export interface CreateSystemAgentResultVO {
|
||||
/** 创建出的系统 Agent ID。 */
|
||||
agentId?: string;
|
||||
}
|
||||
|
||||
/** 新建系统 Agent 版本请求。 */
|
||||
export interface CreateSystemAgentVersionDTO {
|
||||
/** 新版本名称。 */
|
||||
name?: string;
|
||||
/** 新版本描述。 */
|
||||
description?: string;
|
||||
/** 新版本绑定的 Prompt key。 */
|
||||
promptKey?: string;
|
||||
/** 新版本槽位绑定配置。 */
|
||||
slotBindings?: Record<string, unknown>;
|
||||
/** 已经由 Security facade 批准的系统级工具授权 ID 列表。 */
|
||||
toolGrantIds?: string[];
|
||||
/** 版本变更说明。 */
|
||||
changeNote: string;
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
}
|
||||
|
||||
/** 新建系统 Agent 版本返回。 */
|
||||
export interface CreateSystemAgentVersionResultVO {
|
||||
/** 系统 Agent ID。 */
|
||||
agentId?: string;
|
||||
/** 创建出的版本号。 */
|
||||
version?: number;
|
||||
}
|
||||
|
||||
/** Tool Grant 审批状态。 */
|
||||
export type ToolGrantStatus = 'approved' | 'pending' | 'rejected' | 'revoked';
|
||||
|
||||
/** Tool Grant 授权范围类型。 */
|
||||
export type ToolGrantScopeType =
|
||||
| 'chapter'
|
||||
| 'external_endpoint'
|
||||
| 'knowledge_base'
|
||||
| 'market_asset'
|
||||
| 'system'
|
||||
| 'work';
|
||||
|
||||
/** Tool Grant 范围 owner 类型。 */
|
||||
export type ToolGrantOwnerType =
|
||||
| 'knowledge'
|
||||
| 'market'
|
||||
| 'system'
|
||||
| 'user'
|
||||
| 'work';
|
||||
|
||||
/** Tool Grant 资源引用类型。 */
|
||||
export type ToolGrantResourceType =
|
||||
| 'block'
|
||||
| 'chapter'
|
||||
| 'endpoint'
|
||||
| 'knowledge_base'
|
||||
| 'market_asset'
|
||||
| 'work';
|
||||
|
||||
/** Tool Grant 可访问资源引用。 */
|
||||
export interface ToolGrantResourceRefVO {
|
||||
/** 资源类型。 */
|
||||
resourceType: ToolGrantResourceType;
|
||||
/** 资源 ID 或受控 endpoint key。 */
|
||||
resourceId: string;
|
||||
}
|
||||
|
||||
/** Security facade 批准后的工具可用范围。 */
|
||||
export interface ToolGrantScopeVO {
|
||||
/** 授权范围类型。 */
|
||||
scopeType: ToolGrantScopeType;
|
||||
/** 范围 owner 类型。 */
|
||||
ownerType: ToolGrantOwnerType;
|
||||
/** 可访问资源引用,跨作品资源必须由 Security facade 单独批准。 */
|
||||
resourceRefs: ToolGrantResourceRefVO[];
|
||||
/** 是否允许跨作品访问,只能由 Security facade 审批结果置为 true。 */
|
||||
crossWorkAllowed?: boolean;
|
||||
}
|
||||
|
||||
/** Tool Grant 预算周期。 */
|
||||
export type ToolGrantBudgetPeriod = 'daily' | 'monthly' | 'per_task';
|
||||
|
||||
/** Security facade 批准后的工具调用预算。 */
|
||||
export interface ToolGrantBudgetVO {
|
||||
/** 预算周期。 */
|
||||
period: ToolGrantBudgetPeriod;
|
||||
/** 周期内最大调用次数。 */
|
||||
maxCalls: number;
|
||||
/** 周期内最大 token 用量;0 表示该工具不消耗 token 预算。 */
|
||||
maxTokens?: number;
|
||||
/** 周期内最大成本,单位为分。 */
|
||||
maxCostCents?: number;
|
||||
}
|
||||
|
||||
/** Tool Grant 网络外发模式。 */
|
||||
export type ToolGrantNetworkMode = 'allowlist' | 'internal_only' | 'none';
|
||||
|
||||
/** Tool Grant 用户内容外发策略。 */
|
||||
export type ToolGrantDataPolicy =
|
||||
| 'approved_context_only'
|
||||
| 'no_user_content'
|
||||
| 'redacted_user_content';
|
||||
|
||||
/** Security facade 批准后的外发策略。 */
|
||||
export interface ToolGrantOutboundPolicyVO {
|
||||
/** 网络外发模式。 */
|
||||
networkMode: ToolGrantNetworkMode;
|
||||
/** 允许访问的外部目的地 key 或域名;networkMode=allowlist 时必填非空。 */
|
||||
allowedDestinations: string[];
|
||||
/** 用户内容外发策略。 */
|
||||
dataPolicy: ToolGrantDataPolicy;
|
||||
}
|
||||
|
||||
/** Tool Grant 摘要。 */
|
||||
export interface ToolGrantSummaryVO {
|
||||
/** Tool Grant ID。 */
|
||||
grantId: string;
|
||||
/** 目标智能体 ID。 */
|
||||
agentId: string;
|
||||
/** 目标智能体名称。 */
|
||||
agentName?: string;
|
||||
/** 工具类型。 */
|
||||
toolType: string;
|
||||
/** Security facade 批准后的工具可用范围。 */
|
||||
scope: ToolGrantScopeVO;
|
||||
/** Security facade 批准后的工具调用预算。 */
|
||||
budget: ToolGrantBudgetVO;
|
||||
/** Security facade 批准后的外发策略。 */
|
||||
outboundPolicy: ToolGrantOutboundPolicyVO;
|
||||
/** 审批状态。 */
|
||||
status: ToolGrantStatus;
|
||||
/** 审批人。 */
|
||||
approvedBy?: string;
|
||||
/** Security facade 审批记录 ID。 */
|
||||
securityApprovalId?: string;
|
||||
/** 创建时间,ISO date-time 字符串。 */
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/** Tool Grant 列表查询参数。 */
|
||||
export interface ToolGrantPageParams extends MuseAiPageParams {
|
||||
/** 按审批状态筛选。 */
|
||||
status?: ToolGrantStatus;
|
||||
}
|
||||
|
||||
/** 登记或调整 Tool Grant 请求。 */
|
||||
export interface CreateOrAdjustToolGrantDTO {
|
||||
/** 目标智能体 ID。 */
|
||||
agentId: string;
|
||||
/** 工具类型,例如 web_search、file_read、api_call。 */
|
||||
toolType: string;
|
||||
/** Security facade 批准后的工具可用范围。 */
|
||||
scope: ToolGrantScopeVO;
|
||||
/** Security facade 批准后的工具调用预算。 */
|
||||
budget: ToolGrantBudgetVO;
|
||||
/** Security facade 批准后的外发策略。 */
|
||||
outboundPolicy: ToolGrantOutboundPolicyVO;
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
/** 授权或调整原因。 */
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/** 登记或调整 Tool Grant 返回。 */
|
||||
export interface CreateOrAdjustToolGrantResultVO {
|
||||
/** Tool Grant ID。 */
|
||||
grantId?: string;
|
||||
/** 调整后的状态。 */
|
||||
status?: Extract<ToolGrantStatus, 'approved' | 'revoked'>;
|
||||
}
|
||||
|
||||
/** 质量策略摘要。 */
|
||||
export interface QualityPolicySummaryVO {
|
||||
/** 质量策略唯一标识。 */
|
||||
policyKey: string;
|
||||
/** 质量策略名称。 */
|
||||
name?: string;
|
||||
/** 质量策略描述。 */
|
||||
description?: string;
|
||||
/** 当前激活版本号。 */
|
||||
activeVersion: number;
|
||||
/** 最新版本号。 */
|
||||
latestVersion?: number;
|
||||
/** 质量维度数量。 */
|
||||
dimensionCount?: number;
|
||||
/** 最近更新时间,ISO date-time 字符串。 */
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** 质量策略列表查询参数。 */
|
||||
export type QualityPolicyPageParams = MuseAiPageParams;
|
||||
|
||||
/** 质量维度配置。 */
|
||||
export interface QualityDimensionDTO {
|
||||
/** 质量维度名称,例如 canon_compliance、character_voice。 */
|
||||
name: string;
|
||||
/** 通过阈值。 */
|
||||
threshold: number;
|
||||
/** 维度权重。 */
|
||||
weight?: number;
|
||||
}
|
||||
|
||||
/** 新建质量策略版本请求。 */
|
||||
export interface CreateQualityPolicyVersionDTO {
|
||||
/** 质量维度配置列表。 */
|
||||
dimensions: QualityDimensionDTO[];
|
||||
/** 版本变更说明。 */
|
||||
changeNote?: string;
|
||||
/** 幂等键。 */
|
||||
commandId?: string;
|
||||
}
|
||||
|
||||
/** 新建质量策略版本返回。 */
|
||||
export interface CreateQualityPolicyVersionResultVO {
|
||||
/** 质量策略唯一标识。 */
|
||||
policyKey?: string;
|
||||
/** 创建出的版本号。 */
|
||||
version?: number;
|
||||
}
|
||||
|
||||
/** 评估运行状态。 */
|
||||
export type EvaluationRunStatus =
|
||||
| 'cancelled'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'queued'
|
||||
| 'running';
|
||||
|
||||
/** 离线评估运行详情。 */
|
||||
export interface EvaluationRunVO {
|
||||
/** 评估运行 ID。 */
|
||||
runId: string;
|
||||
/** 质量策略唯一标识。 */
|
||||
policyKey: string;
|
||||
/** 质量策略版本;为空时表示使用 active 版本。 */
|
||||
policyVersion?: number;
|
||||
/** 评估数据集引用。 */
|
||||
datasetReference?: string;
|
||||
/** 采样数量。 */
|
||||
sampleSize?: number;
|
||||
/** 评估运行状态。 */
|
||||
status: EvaluationRunStatus;
|
||||
/** 总样本数。 */
|
||||
totalSamples?: number;
|
||||
/** 通过样本数。 */
|
||||
passedSamples?: number;
|
||||
/** 未通过样本数。 */
|
||||
failedSamples?: number;
|
||||
/** 总体评分。 */
|
||||
overallScore?: number;
|
||||
/** 各维度评分,key 为维度名称,value 为评分。 */
|
||||
dimensionScores?: Record<string, number>;
|
||||
/** 开始时间,ISO date-time 字符串。 */
|
||||
startedAt?: string;
|
||||
/** 完成时间,ISO date-time 字符串。 */
|
||||
completedAt?: string;
|
||||
/** 创建时间,ISO date-time 字符串。 */
|
||||
createdAt: string;
|
||||
/** 失败原因。 */
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
/** 启动离线评估请求。 */
|
||||
export interface StartEvaluationRunDTO {
|
||||
/** 使用的质量策略 key。 */
|
||||
policyKey: string;
|
||||
/** 使用的质量策略版本,不填则使用 active 版本。 */
|
||||
policyVersion?: number;
|
||||
/** 评估数据集引用。 */
|
||||
datasetReference: string;
|
||||
/** 采样数量。 */
|
||||
sampleSize?: number;
|
||||
/** 幂等键。 */
|
||||
commandId?: string;
|
||||
}
|
||||
|
||||
/** 启动离线评估返回。 */
|
||||
export interface StartEvaluationRunResultVO {
|
||||
/** 评估运行 ID。 */
|
||||
runId?: string;
|
||||
/** 后端异步任务 ID。 */
|
||||
jobId?: string;
|
||||
}
|
||||
|
||||
/** 查询 Prompt 分页列表。 */
|
||||
export function getPromptPageApi(params: PromptPageParams) {
|
||||
return museAdminApi.get<MuseAiPageResult<PromptSummaryVO>>(
|
||||
'/ai/prompts',
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新建 Prompt 版本。 */
|
||||
export function createPromptVersionApi(
|
||||
promptKey: string,
|
||||
data: CreatePromptVersionDTO,
|
||||
) {
|
||||
return museAdminApi.post<CreatePromptVersionResultVO>(
|
||||
`/ai/prompts/${encodeURIComponent(promptKey)}/versions`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 激活 Prompt 版本。 */
|
||||
export function activatePromptVersionApi(
|
||||
promptKey: string,
|
||||
version: number,
|
||||
data: ActivatePromptVersionDTO,
|
||||
) {
|
||||
return museAdminApi.post<ActivatePromptVersionResultVO>(
|
||||
`/ai/prompts/${encodeURIComponent(promptKey)}/versions/${version}/activate`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询管理端 Agent 分页列表,user scope 只用于治理摘要只读展示。 */
|
||||
export function getAgentPageApi(params: AdminAgentPageParams) {
|
||||
return museAdminApi.get<MuseAiPageResult<AdminAgentSummaryVO>>(
|
||||
'/ai/agents',
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
/** 创建系统 Agent;管理端不提供用户私有 Agent 写接口。 */
|
||||
export function createSystemAgentApi(data: CreateSystemAgentDTO) {
|
||||
return museAdminApi.post<CreateSystemAgentResultVO>('/ai/agents', {
|
||||
...data,
|
||||
scope: 'system',
|
||||
});
|
||||
}
|
||||
|
||||
/** 新建系统 Agent 版本;服务端仍必须拒绝 user scope Agent。 */
|
||||
export function createSystemAgentVersionApi(
|
||||
agentId: string,
|
||||
data: CreateSystemAgentVersionDTO,
|
||||
) {
|
||||
return museAdminApi.post<CreateSystemAgentVersionResultVO>(
|
||||
`/ai/agents/${encodeURIComponent(agentId)}/versions`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询 Tool Grant 分页列表。 */
|
||||
export function getToolGrantPageApi(params: ToolGrantPageParams) {
|
||||
return museAdminApi.get<MuseAiPageResult<ToolGrantSummaryVO>>(
|
||||
'/ai/tool-grants',
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
/** 登记或调整 Security facade 审批后的 Tool Grant。 */
|
||||
export function createOrAdjustToolGrantApi(data: CreateOrAdjustToolGrantDTO) {
|
||||
return museAdminApi.post<CreateOrAdjustToolGrantResultVO>(
|
||||
'/ai/tool-grants',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询质量策略分页列表。 */
|
||||
export function getQualityPolicyPageApi(params: QualityPolicyPageParams) {
|
||||
return museAdminApi.get<MuseAiPageResult<QualityPolicySummaryVO>>(
|
||||
'/ai/quality-policies',
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新建质量策略版本。 */
|
||||
export function createQualityPolicyVersionApi(
|
||||
policyKey: string,
|
||||
data: CreateQualityPolicyVersionDTO,
|
||||
) {
|
||||
return museAdminApi.post<CreateQualityPolicyVersionResultVO>(
|
||||
`/ai/quality-policies/${encodeURIComponent(policyKey)}/versions`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 启动离线评估运行。 */
|
||||
export function startEvaluationRunApi(data: StartEvaluationRunDTO) {
|
||||
return museAdminApi.post<StartEvaluationRunResultVO>(
|
||||
'/ai/evaluation-runs',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询离线评估运行详情。 */
|
||||
export function getEvaluationRunApi(runId: string) {
|
||||
return museAdminApi.get<EvaluationRunVO>(
|
||||
`/ai/evaluation-runs/${encodeURIComponent(runId)}`,
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
getApiAccessLogApi,
|
||||
getBusinessAuditEventApi,
|
||||
listApiAccessLogsApi,
|
||||
listBusinessAuditEventsApi,
|
||||
} from '../index';
|
||||
|
||||
const { museAdminApiMock } = vi.hoisted(() => ({
|
||||
museAdminApiMock: {
|
||||
get: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('#/api/muse/client', () => ({
|
||||
museAdminApi: museAdminApiMock,
|
||||
}));
|
||||
|
||||
describe('muse audit api', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('查询接口调用日志和脱敏详情', async () => {
|
||||
await listApiAccessLogsApi({
|
||||
module: 'account',
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
status: 'failed',
|
||||
});
|
||||
await getApiAccessLogApi('log/1');
|
||||
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/audit/api-logs',
|
||||
{ module: 'account', pageNo: 1, pageSize: 20, status: 'failed' },
|
||||
);
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/audit/api-logs/log%2F1',
|
||||
);
|
||||
});
|
||||
|
||||
it('查询 append-only 业务审计日志和脱敏 before/after 摘要', async () => {
|
||||
await listBusinessAuditEventsApi({
|
||||
eventType: 'quota_adjusted',
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
await getBusinessAuditEventApi('event/1');
|
||||
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/audit/business-events',
|
||||
{ eventType: 'quota_adjusted', pageNo: 1, pageSize: 20 },
|
||||
);
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/audit/business-events/event%2F1',
|
||||
);
|
||||
});
|
||||
});
|
||||
173
muse-admin/apps/web-antd/src/api/muse/audit/index.ts
Normal file
173
muse-admin/apps/web-antd/src/api/muse/audit/index.ts
Normal file
@ -0,0 +1,173 @@
|
||||
import type { PageResult } from '@vben/request';
|
||||
|
||||
import { museAdminApi } from '#/api/muse/client';
|
||||
|
||||
/** 审计日志通用 ID。 */
|
||||
export type MuseAuditId = number | string;
|
||||
|
||||
/** 审计分页查询参数。 */
|
||||
export interface MuseAuditPageQuery {
|
||||
/** 查询参数索引签名,兼容 Muse client 的可序列化参数约束。 */
|
||||
[key: string]: boolean | number | string | undefined;
|
||||
/** 当前页码,从 1 开始。 */
|
||||
pageNo: number;
|
||||
/** 每页数量。 */
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
/** 接口调用状态。 */
|
||||
export type ApiAccessLogStatus =
|
||||
| 'failed'
|
||||
| 'redacted'
|
||||
| 'sensitive'
|
||||
| 'slow'
|
||||
| 'success';
|
||||
|
||||
/** HTTP 方法。 */
|
||||
export type ApiAccessMethod = 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT';
|
||||
|
||||
/** 接口调用日志查询条件。 */
|
||||
export interface ApiAccessLogQuery extends MuseAuditPageQuery {
|
||||
/** 请求链路 ID 筛选。 */
|
||||
requestId?: string;
|
||||
/** 调用人摘要筛选。 */
|
||||
actor?: string;
|
||||
/** 业务模块筛选。 */
|
||||
module?: string;
|
||||
/** 调用状态筛选。 */
|
||||
status?: ApiAccessLogStatus;
|
||||
/** 开始时间。 */
|
||||
startTime?: string;
|
||||
/** 结束时间。 */
|
||||
endTime?: string;
|
||||
}
|
||||
|
||||
/** 接口调用日志摘要。 */
|
||||
export interface ApiAccessLogSummary {
|
||||
/** 日志 ID。 */
|
||||
logId: number;
|
||||
/** 请求链路 ID。 */
|
||||
requestId: string;
|
||||
/** 调用人脱敏摘要。 */
|
||||
actor: string;
|
||||
/** 调用人类型。 */
|
||||
actorType?: 'admin' | 'anonymous' | 'system' | 'user';
|
||||
/** 业务模块。 */
|
||||
module?: string;
|
||||
/** HTTP 方法。 */
|
||||
method: ApiAccessMethod;
|
||||
/** 脱敏路径摘要。 */
|
||||
pathSummary: string;
|
||||
/** 关联对象脱敏摘要。 */
|
||||
objectSummary?: string;
|
||||
/** 客户端 IP 脱敏摘要。 */
|
||||
clientIp?: string;
|
||||
/** 调用状态。 */
|
||||
status: ApiAccessLogStatus;
|
||||
/** HTTP 状态码。 */
|
||||
statusCode: number;
|
||||
/** 接口耗时毫秒数。 */
|
||||
durationMs: number;
|
||||
/** 错误码。 */
|
||||
errorCode?: string;
|
||||
/** 错误摘要。 */
|
||||
errorSummary?: string;
|
||||
/** 创建时间。 */
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/** 脱敏摘要对象。 */
|
||||
export type RedactedSummary = Record<string, unknown>;
|
||||
|
||||
/** 接口调用日志脱敏详情。 */
|
||||
export interface ApiAccessLogDetail extends ApiAccessLogSummary {
|
||||
/** 请求头脱敏摘要,不含 token、secret、cookie 或完整 header。 */
|
||||
requestHeadersSummary?: Record<string, string>;
|
||||
/** 请求体脱敏摘要,不含用户私有正文全文。 */
|
||||
requestBodySummary?: RedactedSummary;
|
||||
/** 响应体脱敏摘要,不含用户私有正文全文。 */
|
||||
responseBodySummary?: RedactedSummary;
|
||||
/** 查看敏感详情时写入的业务审计事件 ID。 */
|
||||
relatedAuditEventId?: number;
|
||||
}
|
||||
|
||||
/** 业务审计日志查询条件。 */
|
||||
export interface BusinessAuditEventQuery extends MuseAuditPageQuery {
|
||||
/** 审计事件类型筛选。 */
|
||||
eventType?: string;
|
||||
/** 开始时间。 */
|
||||
startTime?: string;
|
||||
/** 结束时间。 */
|
||||
endTime?: string;
|
||||
}
|
||||
|
||||
/** 业务审计事件摘要。 */
|
||||
export interface BusinessAuditEventSummary {
|
||||
/** 审计事件 ID。 */
|
||||
eventId: number;
|
||||
/** 审计事件类型。 */
|
||||
eventType: string;
|
||||
/** 操作者脱敏摘要。 */
|
||||
operator: string;
|
||||
/** 操作资源类型。 */
|
||||
resourceType?: string;
|
||||
/** 操作资源 ID。 */
|
||||
resourceId?: string;
|
||||
/** 操作摘要。 */
|
||||
summary?: string;
|
||||
/** 创建时间。 */
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/** 业务审计事件详情。 */
|
||||
export interface BusinessAuditEventDetail extends BusinessAuditEventSummary {
|
||||
/** 操作理由。 */
|
||||
reason?: string;
|
||||
/** 操作结果。 */
|
||||
result?: 'failed' | 'pending_review' | 'rejected' | 'success';
|
||||
/** 变更前脱敏快照摘要。 */
|
||||
beforeSnapshot?: RedactedSummary;
|
||||
/** 变更后脱敏快照摘要。 */
|
||||
afterSnapshot?: RedactedSummary;
|
||||
/** 影响范围摘要。 */
|
||||
impactSummary?: string;
|
||||
/** 审批或复核摘要。 */
|
||||
approvalSummary?: string;
|
||||
/** 是否 append-only。 */
|
||||
immutable?: boolean;
|
||||
}
|
||||
|
||||
/** 编码路径参数,避免特殊字符破坏审计 URL。 */
|
||||
function encodePath(value: MuseAuditId) {
|
||||
return encodeURIComponent(String(value));
|
||||
}
|
||||
|
||||
/** 查询接口调用日志列表。 */
|
||||
export function listApiAccessLogsApi(params: ApiAccessLogQuery) {
|
||||
return museAdminApi.get<PageResult<ApiAccessLogSummary>>(
|
||||
'/audit/api-logs',
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询单条接口调用日志脱敏详情。 */
|
||||
export function getApiAccessLogApi(logId: MuseAuditId) {
|
||||
return museAdminApi.get<ApiAccessLogDetail>(
|
||||
`/audit/api-logs/${encodePath(logId)}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询业务审计事件摘要列表。 */
|
||||
export function listBusinessAuditEventsApi(params: BusinessAuditEventQuery) {
|
||||
return museAdminApi.get<PageResult<BusinessAuditEventSummary>>(
|
||||
'/audit/business-events',
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询业务审计事件脱敏详情。 */
|
||||
export function getBusinessAuditEventApi(eventId: MuseAuditId) {
|
||||
return museAdminApi.get<BusinessAuditEventDetail>(
|
||||
`/audit/business-events/${encodePath(eventId)}`,
|
||||
);
|
||||
}
|
||||
63
muse-admin/apps/web-antd/src/api/muse/client.ts
Normal file
63
muse-admin/apps/web-antd/src/api/muse/client.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* Muse 管理端 API 客户端
|
||||
*
|
||||
* 统一收口 `/admin-api/muse/**` 请求,避免页面层拼错前缀或遗漏版本头。
|
||||
* Vben 的 requestClient 已经按 `code/data/msg` 解包,这里返回值即为后端 data。
|
||||
*/
|
||||
const MUSE_ADMIN_BASE_URL = '/admin-api/muse';
|
||||
|
||||
/** Muse API v1 强制版本头,所有管理端请求都必须显式携带。 */
|
||||
const MUSE_API_HEADERS = {
|
||||
'X-API-Version': '1',
|
||||
} as const;
|
||||
|
||||
/** 查询参数对象;各业务模块用具名 interface 描述字段,这里保持兼容。 */
|
||||
type MuseQueryParams = object;
|
||||
|
||||
/** 请求体对象,保留 unknown 是为了让各业务 API 自己定义 DTO。 */
|
||||
type MuseRequestBody = unknown;
|
||||
|
||||
/** 拼接 Muse 管理端路径,确保调用方不能越过 `/admin-api/muse` 边界。 */
|
||||
function toMuseUrl(url: string) {
|
||||
return `${MUSE_ADMIN_BASE_URL}${url}`;
|
||||
}
|
||||
|
||||
export const museAdminApi = {
|
||||
/** GET 查询接口,适用于列表、详情和只读摘要。 */
|
||||
get: <T>(url: string, params?: MuseQueryParams) =>
|
||||
requestClient.get<T>(toMuseUrl(url), {
|
||||
headers: MUSE_API_HEADERS,
|
||||
params,
|
||||
}),
|
||||
|
||||
/** POST 命令接口,适用于创建、发布、重试等需要审计的动作。 */
|
||||
post: <T>(url: string, data?: MuseRequestBody) =>
|
||||
requestClient.post<T>(toMuseUrl(url), data, {
|
||||
headers: MUSE_API_HEADERS,
|
||||
}),
|
||||
|
||||
/** PUT 更新接口,保留给兼容后端契约使用。 */
|
||||
put: <T>(url: string, data?: MuseRequestBody) =>
|
||||
requestClient.put<T>(toMuseUrl(url), data, {
|
||||
headers: MUSE_API_HEADERS,
|
||||
}),
|
||||
|
||||
/** PATCH 局部更新接口,Vben requestClient 没有快捷方法,因此走通用 request。 */
|
||||
patch: <T>(url: string, data?: MuseRequestBody) =>
|
||||
requestClient.request<T>(toMuseUrl(url), {
|
||||
data,
|
||||
headers: MUSE_API_HEADERS,
|
||||
method: 'PATCH',
|
||||
}),
|
||||
|
||||
/** DELETE 删除或停用接口,只用于契约明确的管理端资源。 */
|
||||
delete: <T>(url: string, params?: MuseQueryParams) =>
|
||||
requestClient.delete<T>(toMuseUrl(url), {
|
||||
headers: MUSE_API_HEADERS,
|
||||
params,
|
||||
}),
|
||||
};
|
||||
|
||||
export type { MuseQueryParams, MuseRequestBody };
|
||||
@ -0,0 +1,179 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
activateMetaSchemaVersionApi,
|
||||
getMetaSchemaApi,
|
||||
listMetaSchemasApi,
|
||||
previewMetaSchemaDraftImpactApi,
|
||||
publishMetaSchemaDraftApi,
|
||||
saveMetaSchemaDraftApi,
|
||||
setMetaSchemaGrayRulesApi,
|
||||
validateMetaSchemaDraftApi,
|
||||
} from '../meta-schema';
|
||||
import {
|
||||
activateFunctionChainVersionApi,
|
||||
getProtectionNodeApi,
|
||||
listFunctionChainsApi,
|
||||
listProtectionNodesApi,
|
||||
previewFunctionChainImpactApi,
|
||||
} from '../function-chain';
|
||||
|
||||
const museAdminApiMock = vi.hoisted(() => ({
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('#/api/muse/client', () => ({
|
||||
museAdminApi: museAdminApiMock,
|
||||
}));
|
||||
|
||||
describe('muse governance api wrappers', () => {
|
||||
beforeEach(() => {
|
||||
museAdminApiMock.get.mockReset();
|
||||
museAdminApiMock.post.mockReset();
|
||||
});
|
||||
|
||||
it('MetaSchema 接口只通过统一 Muse 管理端客户端访问治理路径', async () => {
|
||||
const draftRequest = {
|
||||
commandId: 'cmd-save-draft',
|
||||
displayName: '作品元结构',
|
||||
expectedVersion: 1,
|
||||
fields: [],
|
||||
};
|
||||
const publishRequest = {
|
||||
commandId: 'cmd-publish',
|
||||
expectedVersion: 1,
|
||||
impactPreviewId: 'impact-1',
|
||||
reason: '字段结构发布',
|
||||
validationResultId: 'validation-1',
|
||||
};
|
||||
|
||||
await listMetaSchemasApi({
|
||||
keyword: 'story',
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
scope: 'work',
|
||||
});
|
||||
await getMetaSchemaApi('work/story');
|
||||
await saveMetaSchemaDraftApi('work/story', draftRequest);
|
||||
await validateMetaSchemaDraftApi('work/story', 2);
|
||||
await previewMetaSchemaDraftImpactApi('work/story', 2);
|
||||
await publishMetaSchemaDraftApi('work/story', 2, publishRequest);
|
||||
await activateMetaSchemaVersionApi('work/story', 3, {
|
||||
activateMode: 'gray',
|
||||
commandId: 'cmd-activate',
|
||||
expectedActiveVersion: 1,
|
||||
grayRules: {
|
||||
percentage: 10,
|
||||
strategy: 'percentage',
|
||||
},
|
||||
impactPreviewId: 'impact-1',
|
||||
reason: '灰度激活',
|
||||
validationResultId: 'validation-1',
|
||||
});
|
||||
await setMetaSchemaGrayRulesApi('work/story', 3, {
|
||||
action: 'adjust',
|
||||
commandId: 'cmd-gray',
|
||||
grayRules: {
|
||||
percentage: 20,
|
||||
strategy: 'percentage',
|
||||
},
|
||||
reason: '扩大灰度',
|
||||
});
|
||||
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/governance/meta-schemas',
|
||||
{
|
||||
keyword: 'story',
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
scope: 'work',
|
||||
},
|
||||
);
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/governance/meta-schemas/work%2Fstory',
|
||||
);
|
||||
expect(museAdminApiMock.post).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/governance/meta-schemas/work%2Fstory/drafts',
|
||||
draftRequest,
|
||||
);
|
||||
expect(museAdminApiMock.post).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/governance/meta-schemas/work%2Fstory/drafts/2/validate',
|
||||
);
|
||||
expect(museAdminApiMock.post).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'/governance/meta-schemas/work%2Fstory/drafts/2/impact-preview',
|
||||
);
|
||||
expect(museAdminApiMock.post).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
'/governance/meta-schemas/work%2Fstory/drafts/2/publish',
|
||||
publishRequest,
|
||||
);
|
||||
expect(museAdminApiMock.post).toHaveBeenNthCalledWith(
|
||||
5,
|
||||
'/governance/meta-schemas/work%2Fstory/versions/3/activate',
|
||||
expect.objectContaining({
|
||||
activateMode: 'gray',
|
||||
grayRules: { percentage: 10, strategy: 'percentage' },
|
||||
}),
|
||||
);
|
||||
expect(museAdminApiMock.post).toHaveBeenNthCalledWith(
|
||||
6,
|
||||
'/governance/meta-schemas/work%2Fstory/versions/3/gray-rules',
|
||||
expect.objectContaining({
|
||||
action: 'adjust',
|
||||
grayRules: { percentage: 20, strategy: 'percentage' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('功能编排接口保留保护节点只读与链路激活的治理边界', async () => {
|
||||
const impactRequest = {
|
||||
commandId: 'cmd-preview',
|
||||
expectedCurrentVersion: 4,
|
||||
targetVersion: 5,
|
||||
};
|
||||
const activateRequest = {
|
||||
commandId: 'cmd-activate-chain',
|
||||
expectedActiveVersion: 4,
|
||||
impactPreviewId: 'impact-chain-1',
|
||||
reason: '激活新编排',
|
||||
validationResultId: 'validation-chain-1',
|
||||
};
|
||||
|
||||
await listProtectionNodesApi({ chainKey: 'story-pipeline', pageNo: 1, pageSize: 50 });
|
||||
await getProtectionNodeApi('shadow/canonical-guard');
|
||||
await listFunctionChainsApi({ pageNo: 1, pageSize: 20 });
|
||||
await previewFunctionChainImpactApi('story-pipeline', impactRequest);
|
||||
await activateFunctionChainVersionApi('story-pipeline', 5, activateRequest);
|
||||
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/governance/protection-nodes',
|
||||
{ chainKey: 'story-pipeline', pageNo: 1, pageSize: 50 },
|
||||
);
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/governance/protection-nodes/shadow%2Fcanonical-guard',
|
||||
);
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'/governance/function-chains',
|
||||
{ pageNo: 1, pageSize: 20 },
|
||||
);
|
||||
expect(museAdminApiMock.post).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/governance/function-chains/story-pipeline/impact-preview',
|
||||
impactRequest,
|
||||
);
|
||||
expect(museAdminApiMock.post).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/governance/function-chains/story-pipeline/versions/5/activate',
|
||||
activateRequest,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,271 @@
|
||||
import { museAdminApi } from '#/api/muse/client';
|
||||
|
||||
/** 保护节点类型。 */
|
||||
export type ProtectionNodeType =
|
||||
| 'chunking'
|
||||
| 'input_output_compliance'
|
||||
| 'quality_gate'
|
||||
| 'rag_indexing'
|
||||
| 'semantic_safety'
|
||||
| 'static_check';
|
||||
|
||||
/** 功能链路状态,OpenAPI 未强制返回时仅用于前端兼容展示。 */
|
||||
export type FunctionChainStatus =
|
||||
| 'active'
|
||||
| 'draft'
|
||||
| 'protection_node_missing'
|
||||
| 'publish_failed'
|
||||
| 'slot_contract_missing';
|
||||
|
||||
/** 保护节点变更类型。 */
|
||||
export type ProtectionNodeChangeType = 'added' | 'modified' | 'removed';
|
||||
|
||||
/** 槽位变更类型。 */
|
||||
export type SlotChangeType =
|
||||
| 'added'
|
||||
| 'protection_boundary_violation'
|
||||
| 'protection_upgraded'
|
||||
| 'removed';
|
||||
|
||||
/** 分页查询参数。 */
|
||||
export interface GovernancePageParams {
|
||||
/** 当前页码,从 1 开始。 */
|
||||
pageNo: number;
|
||||
/** 每页条数。 */
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
/** 分页结果。 */
|
||||
export interface GovernancePageResult<T> {
|
||||
/** 当前页数据列表。 */
|
||||
list: T[];
|
||||
/** 符合条件的总条数。 */
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** 保护节点列表查询参数。 */
|
||||
export interface ProtectionNodeListParams
|
||||
extends GovernancePageParams,
|
||||
Record<string, boolean | number | string | undefined> {
|
||||
/** 按所属功能链路筛选。 */
|
||||
chainKey?: string;
|
||||
}
|
||||
|
||||
/** 保护节点摘要。 */
|
||||
export interface ProtectionNodeSummary {
|
||||
/** 保护节点业务标识。 */
|
||||
nodeKey: string;
|
||||
/** 显示名称。 */
|
||||
displayName: string;
|
||||
/** 保护节点类型。 */
|
||||
nodeType: ProtectionNodeType;
|
||||
/** 是否不可替换,保护节点不可被用户替换、覆盖或降级为开放槽位。 */
|
||||
irreplaceable: boolean;
|
||||
/** 不可替换原因。 */
|
||||
irreplaceableReason?: string;
|
||||
/** 所属功能链路标识。 */
|
||||
chainKey?: string;
|
||||
/** 审计摘要。 */
|
||||
auditSummary?: string;
|
||||
}
|
||||
|
||||
/** 保护节点可观测指标。 */
|
||||
export interface ProtectionNodeMetric {
|
||||
/** 指标名称。 */
|
||||
metricName?: string;
|
||||
/** 指标值。 */
|
||||
metricValue?: string;
|
||||
/** 指标说明。 */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/** 保护节点详情。 */
|
||||
export interface ProtectionNodeDetail {
|
||||
/** 保护节点业务标识。 */
|
||||
nodeKey: string;
|
||||
/** 显示名称。 */
|
||||
displayName: string;
|
||||
/** 节点说明。 */
|
||||
description?: string;
|
||||
/** 保护节点类型。 */
|
||||
nodeType: ProtectionNodeType;
|
||||
/** 是否不可替换,服务端必须拒绝降级为用户可替换槽位。 */
|
||||
irreplaceable: boolean;
|
||||
/** 不可替换原因。 */
|
||||
irreplaceableReason?: string;
|
||||
/** 权限要求。 */
|
||||
permissionRequirement?: string;
|
||||
/** 审计要求。 */
|
||||
auditRequirement?: string;
|
||||
/** Shadow 到 Canonical 的边界说明。 */
|
||||
shadowToCanonicalBoundary?: string;
|
||||
/** 所属功能链路标识。 */
|
||||
chainKey?: string;
|
||||
/** 所属功能链路名称。 */
|
||||
chainDisplayName?: string;
|
||||
/** 可观测指标。 */
|
||||
observabilityMetrics?: ProtectionNodeMetric[];
|
||||
/** 更新时间。 */
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
/** 功能链路摘要。 */
|
||||
export interface FunctionChainSummary {
|
||||
/** 功能链路业务标识。 */
|
||||
chainKey: string;
|
||||
/** 显示名称。 */
|
||||
displayName: string;
|
||||
/** 链路说明。 */
|
||||
description?: string;
|
||||
/** 当前激活版本号。 */
|
||||
activeVersion: number;
|
||||
/** 开放槽位数,只表示可替换的非保护节点。 */
|
||||
openSlotCount?: number;
|
||||
/** 不可替换保护节点数。 */
|
||||
protectionNodeCount?: number;
|
||||
/** 更新时间。 */
|
||||
updatedAt?: string;
|
||||
/** 链路状态,OpenAPI 未强制返回时页面降级为根据版本展示。 */
|
||||
status?: FunctionChainStatus;
|
||||
}
|
||||
|
||||
/** 功能链路列表查询参数。 */
|
||||
export interface FunctionChainListParams
|
||||
extends GovernancePageParams,
|
||||
Record<string, boolean | number | string | undefined> {
|
||||
/** 按链路名称或标识搜索,后端若未实现会忽略。 */
|
||||
keyword?: string;
|
||||
/** 按链路状态筛选,后端若未实现会忽略。 */
|
||||
status?: FunctionChainStatus;
|
||||
}
|
||||
|
||||
/** 功能链路影响预览请求。 */
|
||||
export interface FunctionChainImpactPreviewRequest {
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
/** 预发布的目标版本号。 */
|
||||
targetVersion?: number;
|
||||
/** 期望当前激活版本,乐观锁。 */
|
||||
expectedCurrentVersion?: number;
|
||||
}
|
||||
|
||||
/** 保护节点影响明细。 */
|
||||
export interface ProtectionNodeImpactDetail {
|
||||
/** 保护节点业务标识。 */
|
||||
nodeKey?: string;
|
||||
/** 变更类型。 */
|
||||
changeType?: ProtectionNodeChangeType;
|
||||
/** 影响说明。 */
|
||||
impactDescription?: string;
|
||||
}
|
||||
|
||||
/** 开放槽位变更明细。 */
|
||||
export interface SlotChangeDetail {
|
||||
/** 槽位业务标识。 */
|
||||
slotKey?: string;
|
||||
/** 变更类型,protection_boundary_violation 必须阻断激活。 */
|
||||
changeType?: SlotChangeType;
|
||||
/** 影响说明。 */
|
||||
impactDescription?: string;
|
||||
}
|
||||
|
||||
/** 功能链路影响预览。 */
|
||||
export interface FunctionChainImpactPreview {
|
||||
/** 影响预览记录 ID。 */
|
||||
impactPreviewId?: string;
|
||||
/** 功能链路业务标识。 */
|
||||
chainKey?: string;
|
||||
/** 目标版本号。 */
|
||||
targetVersion?: number;
|
||||
/** 受影响智能体槽位绑定数。 */
|
||||
affectedAgentSlotBindings?: number;
|
||||
/** 受影响 AI 运行时任务数。 */
|
||||
affectedAIRuntimeTasks?: number;
|
||||
/** 保护节点变更数。 */
|
||||
affectedProtectionNodeChanges?: number;
|
||||
/** 保护节点影响明细。 */
|
||||
protectionNodeImpactDetails?: ProtectionNodeImpactDetail[];
|
||||
/** 开放槽位变更明细。 */
|
||||
slotChanges?: SlotChangeDetail[];
|
||||
}
|
||||
|
||||
/** 功能链路激活请求。 */
|
||||
export interface FunctionChainActivateRequest {
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
/** 激活理由。 */
|
||||
reason: string;
|
||||
/** 影响预览引用 ID。 */
|
||||
impactPreviewId: string;
|
||||
/** 激活前校验结果引用 ID。 */
|
||||
validationResultId: string;
|
||||
/** 期望当前激活版本,乐观锁。 */
|
||||
expectedActiveVersion: number;
|
||||
}
|
||||
|
||||
/** 功能链路激活响应。 */
|
||||
export interface FunctionChainActivateResponse {
|
||||
/** 功能链路业务标识。 */
|
||||
chainKey?: string;
|
||||
/** 已激活版本号。 */
|
||||
activatedVersion?: number;
|
||||
/** 前一个激活版本号。 */
|
||||
previousActiveVersion?: number;
|
||||
/** 受影响槽位绑定数。 */
|
||||
affectedSlotBindings?: number;
|
||||
/** 受影响运行时任务数。 */
|
||||
affectedRuntimeTasks?: number;
|
||||
}
|
||||
|
||||
/** 编码路径参数,避免业务标识中的斜杠逃逸治理资源路径。 */
|
||||
function encodePathParam(value: string | number) {
|
||||
return encodeURIComponent(String(value));
|
||||
}
|
||||
|
||||
/** 查询保护节点注册表。 */
|
||||
export function listProtectionNodesApi(params: ProtectionNodeListParams) {
|
||||
return museAdminApi.get<GovernancePageResult<ProtectionNodeSummary>>(
|
||||
'/governance/protection-nodes',
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询保护节点详情。 */
|
||||
export function getProtectionNodeApi(nodeKey: string) {
|
||||
return museAdminApi.get<ProtectionNodeDetail>(
|
||||
`/governance/protection-nodes/${encodePathParam(nodeKey)}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询系统功能链路列表。 */
|
||||
export function listFunctionChainsApi(params: FunctionChainListParams) {
|
||||
return museAdminApi.get<GovernancePageResult<FunctionChainSummary>>(
|
||||
'/governance/function-chains',
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
/** 预览功能链路版本发布或激活影响。 */
|
||||
export function previewFunctionChainImpactApi(
|
||||
chainKey: string,
|
||||
data: FunctionChainImpactPreviewRequest,
|
||||
) {
|
||||
return museAdminApi.post<FunctionChainImpactPreview>(
|
||||
`/governance/function-chains/${encodePathParam(chainKey)}/impact-preview`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 激活功能链路版本。 */
|
||||
export function activateFunctionChainVersionApi(
|
||||
chainKey: string,
|
||||
version: number,
|
||||
data: FunctionChainActivateRequest,
|
||||
) {
|
||||
return museAdminApi.post<FunctionChainActivateResponse>(
|
||||
`/governance/function-chains/${encodePathParam(
|
||||
chainKey,
|
||||
)}/versions/${encodePathParam(version)}/activate`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
754
muse-admin/apps/web-antd/src/api/muse/governance/meta-schema.ts
Normal file
754
muse-admin/apps/web-antd/src/api/muse/governance/meta-schema.ts
Normal file
@ -0,0 +1,754 @@
|
||||
import { museAdminApi } from '#/api/muse/client';
|
||||
|
||||
/** MetaSchema 作用域。 */
|
||||
export type MetaSchemaScope = 'global' | 'tenant' | 'user' | 'work';
|
||||
|
||||
/** MetaSchema 字段类型。 */
|
||||
export type MetaFieldType =
|
||||
| 'boolean'
|
||||
| 'date'
|
||||
| 'enum'
|
||||
| 'json'
|
||||
| 'number'
|
||||
| 'relation'
|
||||
| 'string'
|
||||
| 'text';
|
||||
|
||||
/** MetaSchema 版本状态。 */
|
||||
export type MetaSchemaStatus =
|
||||
| 'active'
|
||||
| 'archived'
|
||||
| 'deprecated'
|
||||
| 'draft'
|
||||
| 'published';
|
||||
|
||||
/** MetaSchema 迁移风险等级。 */
|
||||
export type MetaSchemaMigrationRisk = 'high' | 'low' | 'medium' | 'none' | 'unknown';
|
||||
|
||||
/** MetaSchema 灰度策略。 */
|
||||
export type GrayRuleStrategy = 'percentage' | 'tenant_whitelist' | 'user_whitelist';
|
||||
|
||||
/** MetaSchema 激活模式。 */
|
||||
export type MetaSchemaActivateMode = 'full' | 'gray';
|
||||
|
||||
/** MetaSchema 灰度规则操作。 */
|
||||
export type MetaSchemaGrayRulesAction = 'adjust' | 'remove' | 'set';
|
||||
|
||||
/** MetaSchema 废弃后建议动作。 */
|
||||
export type MetaSchemaDeprecateNextAction =
|
||||
| 'activate_replacement'
|
||||
| 'migrate_data'
|
||||
| 'review_impact';
|
||||
|
||||
/** MetaSchema 发布后建议动作。 */
|
||||
export type MetaSchemaPublishNextAction = 'activate' | 'gray_release' | 'impact_review';
|
||||
|
||||
/** 字段兼容性变更类型。 */
|
||||
export type MetaSchemaBreakingChangeType =
|
||||
| 'enum_removed'
|
||||
| 'reference_changed'
|
||||
| 'required_added'
|
||||
| 'type_changed'
|
||||
| 'visibility_restricted';
|
||||
|
||||
/** 分页查询参数。 */
|
||||
export interface GovernancePageParams {
|
||||
/** 当前页码,从 1 开始。 */
|
||||
pageNo: number;
|
||||
/** 每页条数。 */
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
/** 分页结果。 */
|
||||
export interface GovernancePageResult<T> {
|
||||
/** 当前页数据列表。 */
|
||||
list: T[];
|
||||
/** 符合条件的总条数。 */
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** MetaSchema 列表查询参数。 */
|
||||
export interface MetaSchemaListParams
|
||||
extends GovernancePageParams,
|
||||
Record<string, boolean | number | string | undefined> {
|
||||
/** 按 schemaKey 或 displayName 模糊搜索。 */
|
||||
keyword?: string;
|
||||
/** 按作用域筛选。 */
|
||||
scope?: MetaSchemaScope;
|
||||
/** 按目标类型筛选,例如 work / chapter / block / agent / knowledge_base。 */
|
||||
targetType?: string;
|
||||
}
|
||||
|
||||
/** MetaField 可见性策略。 */
|
||||
export interface MetaFieldVisibility {
|
||||
/** 是否进入用户可见投影。 */
|
||||
uiVisible?: boolean;
|
||||
/** 是否允许进入 AI 上下文组装。 */
|
||||
aiContext?: boolean;
|
||||
/** 用户端是否允许保存该动态字段。 */
|
||||
userEditable?: boolean;
|
||||
/** 是否允许用户搜索或筛选。 */
|
||||
userSearchable?: boolean;
|
||||
/** 是否可被导出预检纳入。 */
|
||||
exportable?: boolean;
|
||||
}
|
||||
|
||||
/** 字段废弃信息。 */
|
||||
export interface FieldDeprecationInfo {
|
||||
/** 替代字段标识。 */
|
||||
replacementFieldKey?: string;
|
||||
/** 保留期,例如 6m / 1y。 */
|
||||
retentionPeriod?: string;
|
||||
/** 迁移提示。 */
|
||||
migrationHint?: string;
|
||||
}
|
||||
|
||||
/** MetaField 定义。 */
|
||||
export interface MetaField {
|
||||
/** 字段业务标识。 */
|
||||
fieldKey: string;
|
||||
/** 字段显示名称。 */
|
||||
displayName: string;
|
||||
/** 字段类型。 */
|
||||
fieldType: MetaFieldType;
|
||||
/** 字段作用域。 */
|
||||
scope?: MetaSchemaScope;
|
||||
/** 默认值,按字段类型由服务端解释。 */
|
||||
defaultValue?: unknown;
|
||||
/** 是否必填。 */
|
||||
required?: boolean;
|
||||
/** 枚举字段可选值。 */
|
||||
enumValues?: string[];
|
||||
/** 校验规则,例如 min / max / pattern。 */
|
||||
validationRules?: Record<string, unknown>;
|
||||
/** 字段可见性策略。 */
|
||||
visibility?: MetaFieldVisibility;
|
||||
/** 字段是否已废弃。 */
|
||||
deprecated?: boolean;
|
||||
/** 字段废弃信息。 */
|
||||
deprecatedInfo?: FieldDeprecationInfo;
|
||||
/** 字段继承来源 schemaKey。 */
|
||||
inheritedFrom?: string;
|
||||
/** 该字段是否绑定保护节点边界。 */
|
||||
protectionNodeBound?: boolean;
|
||||
}
|
||||
|
||||
/** 灰度规则摘要。 */
|
||||
export interface GrayRulesSummary {
|
||||
/** 灰度策略。 */
|
||||
strategy?: GrayRuleStrategy;
|
||||
/** 灰度范围描述。 */
|
||||
scope?: string;
|
||||
/** 回滚入口说明。 */
|
||||
rollbackEntry?: string;
|
||||
}
|
||||
|
||||
/** 灰度规则配置。 */
|
||||
export interface GrayRulesConfig {
|
||||
/** 灰度策略。 */
|
||||
strategy?: GrayRuleStrategy;
|
||||
/** 租户白名单,strategy=tenant_whitelist 时生效。 */
|
||||
tenantWhitelist?: string[];
|
||||
/** 灰度百分比,strategy=percentage 时生效。 */
|
||||
percentage?: number;
|
||||
/** 用户白名单,strategy=user_whitelist 时生效。 */
|
||||
userWhitelist?: string[];
|
||||
}
|
||||
|
||||
/** MetaSchema 列表影响摘要。 */
|
||||
export interface MetaSchemaImpactSummary {
|
||||
/** 受影响作品数。 */
|
||||
affectedWorkCount?: number;
|
||||
/** 受影响投影数。 */
|
||||
affectedProjectionCount?: number;
|
||||
/** 受影响 AI 上下文引用数。 */
|
||||
affectedAIContextCount?: number;
|
||||
}
|
||||
|
||||
/** MetaSchema 列表摘要。 */
|
||||
export interface MetaSchemaSummary {
|
||||
/** Schema 业务标识。 */
|
||||
schemaKey: string;
|
||||
/** 显示名称。 */
|
||||
displayName: string;
|
||||
/** 作用域。 */
|
||||
scope: MetaSchemaScope;
|
||||
/** 目标类型,例如 work / chapter / block / agent / knowledge_base。 */
|
||||
targetType?: string;
|
||||
/** 主字段类型,OpenAPI 未强制返回时列表页提示进入详情查看字段集合。 */
|
||||
fieldType?: MetaFieldType;
|
||||
/** 当前激活版本号。 */
|
||||
activeVersion: number;
|
||||
/** 当前灰度版本号。 */
|
||||
grayVersion?: number;
|
||||
/** 最新草稿版本号。 */
|
||||
draftVersion?: number;
|
||||
/** 字段数量。 */
|
||||
fieldCount?: number;
|
||||
/** 更新时间。 */
|
||||
updatedAt?: string;
|
||||
/** 列表状态,OpenAPI 未强制返回时由页面根据版本字段降级展示。 */
|
||||
status?: MetaSchemaStatus;
|
||||
/** 迁移风险等级,OpenAPI 未返回时页面不造风险结论。 */
|
||||
migrationRisk?: MetaSchemaMigrationRisk;
|
||||
/** 轻量影响摘要,OpenAPI 未返回时页面提示进入详情预览。 */
|
||||
impactSummary?: MetaSchemaImpactSummary;
|
||||
}
|
||||
|
||||
/** 字段继承关系。 */
|
||||
export interface InheritedSchemaSummary {
|
||||
/** 被继承的 schemaKey。 */
|
||||
schemaKey?: string;
|
||||
/** 从该 Schema 继承的字段数量。 */
|
||||
inheritedFieldCount?: number;
|
||||
}
|
||||
|
||||
/** MetaSchema 详情。 */
|
||||
export interface MetaSchemaDetail {
|
||||
/** Schema 业务标识。 */
|
||||
schemaKey: string;
|
||||
/** 显示名称。 */
|
||||
displayName: string;
|
||||
/** 说明。 */
|
||||
description?: string;
|
||||
/** 作用域。 */
|
||||
scope: MetaSchemaScope;
|
||||
/** 目标类型。 */
|
||||
targetType?: string;
|
||||
/** 当前激活版本号。 */
|
||||
activeVersion: number;
|
||||
/** 当前激活版本发布时间。 */
|
||||
activeVersionPublishedAt?: string;
|
||||
/** 当前灰度版本号。 */
|
||||
grayVersion?: number;
|
||||
/** 当前灰度版本发布时间。 */
|
||||
grayVersionPublishedAt?: string;
|
||||
/** 灰度规则摘要。 */
|
||||
grayRules?: GrayRulesSummary;
|
||||
/** 当前 active 版本字段列表。 */
|
||||
fields?: MetaField[];
|
||||
/** 字段继承关系。 */
|
||||
inheritedSchemas?: InheritedSchemaSummary[];
|
||||
/** 影响摘要。 */
|
||||
impactSummary?: MetaSchemaImpactSummary;
|
||||
/** 创建时间。 */
|
||||
createdAt?: string;
|
||||
/** 更新时间。 */
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
/** MetaSchema 版本差异。 */
|
||||
export interface MetaSchemaVersionDiff {
|
||||
/** 新增字段 fieldKey 列表。 */
|
||||
addedFields?: string[];
|
||||
/** 删除字段 fieldKey 列表。 */
|
||||
removedFields?: string[];
|
||||
/** 变更字段 fieldKey 列表。 */
|
||||
modifiedFields?: string[];
|
||||
/** 废弃字段 fieldKey 列表。 */
|
||||
deprecatedFields?: string[];
|
||||
}
|
||||
|
||||
/** MetaSchema 发布审计记录。 */
|
||||
export interface MetaSchemaPublishRecord {
|
||||
/** 幂等命令 ID。 */
|
||||
commandId?: string;
|
||||
/** 发布理由。 */
|
||||
reason?: string;
|
||||
/** 关联校验结果 ID。 */
|
||||
validationResultId?: string;
|
||||
/** 关联影响预览 ID。 */
|
||||
impactPreviewId?: string;
|
||||
/** 发布时期望的当前版本。 */
|
||||
expectedVersion?: number;
|
||||
}
|
||||
|
||||
/** MetaSchema 版本详情。 */
|
||||
export interface MetaSchemaVersionDetail {
|
||||
/** Schema 业务标识。 */
|
||||
schemaKey: string;
|
||||
/** 版本号。 */
|
||||
version: number;
|
||||
/** 版本状态。 */
|
||||
status: MetaSchemaStatus;
|
||||
/** 字段列表。 */
|
||||
fields?: MetaField[];
|
||||
/** 与前一版本的字段差异。 */
|
||||
fieldDiff?: MetaSchemaVersionDiff;
|
||||
/** 发布审计记录。 */
|
||||
publishRecord?: MetaSchemaPublishRecord;
|
||||
/** 发布时间。 */
|
||||
publishedAt?: string;
|
||||
/** 发布操作者。 */
|
||||
publishedBy?: string;
|
||||
/** 激活时间。 */
|
||||
activatedAt?: string;
|
||||
/** 废弃时间。 */
|
||||
deprecatedAt?: string;
|
||||
/** 创建时间。 */
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
/** MetaSchema 草稿保存请求。 */
|
||||
export interface MetaSchemaDraftRequest {
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
/** 期望当前 active 版本,用于乐观锁。 */
|
||||
expectedVersion?: number;
|
||||
/** 显示名称。 */
|
||||
displayName?: string;
|
||||
/** 说明。 */
|
||||
description?: string;
|
||||
/** 完整字段定义,全量替换。 */
|
||||
fields?: MetaField[];
|
||||
}
|
||||
|
||||
/** MetaSchema 草稿保存响应。 */
|
||||
export interface MetaSchemaDraftResponse {
|
||||
/** 分配的草稿版本号。 */
|
||||
draftVersion?: number;
|
||||
/** 校验摘要。 */
|
||||
validationSummary?: ValidationResult;
|
||||
/** 当前激活版本号。 */
|
||||
currentActiveVersion?: number;
|
||||
}
|
||||
|
||||
/** 校验错误。 */
|
||||
export interface ValidationError {
|
||||
/** 错误码。 */
|
||||
code: string;
|
||||
/** 错误信息。 */
|
||||
message: string;
|
||||
/** 关联字段标识。 */
|
||||
fieldKey?: string;
|
||||
/** 错误详情。 */
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
/** 校验警告。 */
|
||||
export interface ValidationWarning {
|
||||
/** 警告码。 */
|
||||
code: string;
|
||||
/** 警告信息。 */
|
||||
message: string;
|
||||
/** 关联字段标识。 */
|
||||
fieldKey?: string;
|
||||
/** 警告详情。 */
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
/** 兼容性破坏项。 */
|
||||
export interface BreakingChange {
|
||||
/** 关联字段标识。 */
|
||||
fieldKey?: string;
|
||||
/** 变更类型。 */
|
||||
changeType?: MetaSchemaBreakingChangeType;
|
||||
/** 变更说明。 */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/** 保护节点边界违规。 */
|
||||
export interface ProtectionNodeBoundaryViolation {
|
||||
/** 关联字段标识。 */
|
||||
fieldKey?: string;
|
||||
/** 保护节点标识。 */
|
||||
nodeKey?: string;
|
||||
/** 违规说明。 */
|
||||
violation?: string;
|
||||
}
|
||||
|
||||
/** 兼容性分析结果。 */
|
||||
export interface CompatibilityResult {
|
||||
/** 是否兼容上一版本。 */
|
||||
compatible?: boolean;
|
||||
/** 破坏性变更列表。 */
|
||||
breakingChanges?: BreakingChange[];
|
||||
/** 保护节点边界违规列表。 */
|
||||
protectionNodeBoundaryViolations?: ProtectionNodeBoundaryViolation[];
|
||||
}
|
||||
|
||||
/** 校验结果。 */
|
||||
export interface ValidationResult {
|
||||
/** 是否校验通过。 */
|
||||
valid?: boolean;
|
||||
/** 校验错误列表。 */
|
||||
errors?: ValidationError[];
|
||||
/** 校验警告列表。 */
|
||||
warnings?: ValidationWarning[];
|
||||
/** 兼容性分析结果。 */
|
||||
compatibilityResult?: CompatibilityResult;
|
||||
}
|
||||
|
||||
/** 作品影响摘要。 */
|
||||
export interface WorkImpactSummary {
|
||||
/** 受影响作品数。 */
|
||||
affectedWorkCount?: number;
|
||||
/** 受动态字段影响的作品数。 */
|
||||
affectedDynamicFieldWorkCount?: number;
|
||||
/** 使用已废弃字段的作品数。 */
|
||||
worksWithDeprecatedFields?: number;
|
||||
}
|
||||
|
||||
/** 规划影响摘要。 */
|
||||
export interface PlanningImpactSummary {
|
||||
/** 受影响规划章节数。 */
|
||||
affectedPlanningSections?: number;
|
||||
/** 使用已废弃字段的规划数量。 */
|
||||
planningUsingDeprecatedFields?: number;
|
||||
}
|
||||
|
||||
/** 知识投影影响摘要。 */
|
||||
export interface KnowledgeProjectionImpactSummary {
|
||||
/** 受影响投影数。 */
|
||||
affectedProjections?: number;
|
||||
/** 需要重建的投影数。 */
|
||||
projectionsNeedingRebuild?: number;
|
||||
/** 可见性变化的投影数。 */
|
||||
projectionsWithVisibilityChange?: number;
|
||||
}
|
||||
|
||||
/** AI 上下文影响摘要。 */
|
||||
export interface AIContextImpactSummary {
|
||||
/** 受影响 AI 上下文数。 */
|
||||
affectedAIContexts?: number;
|
||||
/** 字段被移除的上下文数。 */
|
||||
contextsWithFieldRemoved?: number;
|
||||
/** 可见性变化的上下文数。 */
|
||||
contextsWithVisibilityChange?: number;
|
||||
}
|
||||
|
||||
/** 导出影响摘要。 */
|
||||
export interface ExportImpactSummary {
|
||||
/** 受影响导出策略数。 */
|
||||
affectedExportPolicies?: number;
|
||||
/** 字段被移除的导出策略数。 */
|
||||
exportsWithFieldRemoved?: number;
|
||||
/** 可见性变化的导出策略数。 */
|
||||
exportsWithVisibilityChange?: number;
|
||||
}
|
||||
|
||||
/** MetaSchema 影响预览。 */
|
||||
export interface MetaSchemaImpactPreview {
|
||||
/** 影响预览记录 ID,供发布或激活引用。 */
|
||||
impactPreviewId?: string;
|
||||
/** Schema 业务标识。 */
|
||||
schemaKey?: string;
|
||||
/** 草稿版本号。 */
|
||||
draftVersion?: number;
|
||||
/** 作品影响摘要。 */
|
||||
workImpact?: WorkImpactSummary;
|
||||
/** 规划影响摘要。 */
|
||||
planningImpact?: PlanningImpactSummary;
|
||||
/** 知识投影影响摘要。 */
|
||||
knowledgeProjectionImpact?: KnowledgeProjectionImpactSummary;
|
||||
/** AI 上下文影响摘要。 */
|
||||
aiContextImpact?: AIContextImpactSummary;
|
||||
/** 导出影响摘要。 */
|
||||
exportImpact?: ExportImpactSummary;
|
||||
/** 受影响投影总数。 */
|
||||
totalAffectedProjectionCount?: number;
|
||||
}
|
||||
|
||||
/** MetaSchema 发布请求。 */
|
||||
export interface MetaSchemaPublishRequest {
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
/** 发布理由。 */
|
||||
reason: string;
|
||||
/** 期望当前版本,乐观锁。 */
|
||||
expectedVersion: number;
|
||||
/** 校验结果引用 ID。 */
|
||||
validationResultId: string;
|
||||
/** 影响预览引用 ID。 */
|
||||
impactPreviewId: string;
|
||||
/** 是否自动激活;产品约束下应保持 false,由 activate 单独处理。 */
|
||||
autoActivate?: boolean;
|
||||
}
|
||||
|
||||
/** MetaSchema 发布后建议动作。 */
|
||||
export interface MetaSchemaNextAction {
|
||||
/** 建议动作。 */
|
||||
action?: MetaSchemaPublishNextAction;
|
||||
/** 动作说明。 */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/** MetaSchema 发布响应。 */
|
||||
export interface MetaSchemaPublishResponse {
|
||||
/** 发布后的版本号。 */
|
||||
publishedVersion?: number;
|
||||
/** 发布后状态。 */
|
||||
status?: 'published';
|
||||
/** 当前激活版本号。 */
|
||||
currentActiveVersion?: number;
|
||||
/** 受影响投影数。 */
|
||||
affectedProjectionCount?: number;
|
||||
/** 发布后建议动作。 */
|
||||
nextActions?: MetaSchemaNextAction[];
|
||||
}
|
||||
|
||||
/** MetaSchema 激活请求。 */
|
||||
export interface MetaSchemaActivateRequest {
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
/** 激活理由。 */
|
||||
reason: string;
|
||||
/** 激活模式。 */
|
||||
activateMode?: MetaSchemaActivateMode;
|
||||
/** 灰度规则。 */
|
||||
grayRules?: GrayRulesConfig;
|
||||
/** 期望当前激活版本,乐观锁。 */
|
||||
expectedActiveVersion: number;
|
||||
/** 激活前校验结果引用 ID。 */
|
||||
validationResultId: string;
|
||||
/** 激活影响预览引用 ID。 */
|
||||
impactPreviewId: string;
|
||||
}
|
||||
|
||||
/** MetaSchema 激活响应。 */
|
||||
export interface MetaSchemaActivateResponse {
|
||||
/** Schema 业务标识。 */
|
||||
schemaKey?: string;
|
||||
/** 已激活版本号。 */
|
||||
activatedVersion?: number;
|
||||
/** 激活模式。 */
|
||||
activateMode?: MetaSchemaActivateMode;
|
||||
/** 前一个激活版本号。 */
|
||||
previousActiveVersion?: number;
|
||||
/** 受影响投影数。 */
|
||||
affectedProjectionCount?: number;
|
||||
/** 投影重建任务 ID。 */
|
||||
projectionRebuildJobId?: string;
|
||||
}
|
||||
|
||||
/** MetaSchema 回滚请求。 */
|
||||
export interface MetaSchemaRollbackRequest {
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
/** 回滚理由。 */
|
||||
reason: string;
|
||||
/** 期望当前激活版本,乐观锁。 */
|
||||
expectedActiveVersion: number;
|
||||
/** 回滚前校验结果引用 ID。 */
|
||||
validationResultId: string;
|
||||
/** 回滚影响预览引用 ID。 */
|
||||
impactPreviewId: string;
|
||||
}
|
||||
|
||||
/** MetaSchema 回滚响应。 */
|
||||
export interface MetaSchemaRollbackResponse {
|
||||
/** Schema 业务标识。 */
|
||||
schemaKey?: string;
|
||||
/** 回滚目标版本号。 */
|
||||
rolledBackToVersion?: number;
|
||||
/** 前一个激活版本号。 */
|
||||
previousActiveVersion?: number;
|
||||
/** 受影响投影数。 */
|
||||
affectedProjectionCount?: number;
|
||||
/** 投影重建任务 ID。 */
|
||||
projectionRebuildJobId?: string;
|
||||
/** 投影失效任务 ID。 */
|
||||
projectionInvalidateJobId?: string;
|
||||
}
|
||||
|
||||
/** 废弃字段请求项。 */
|
||||
export interface DeprecateFieldRequest {
|
||||
/** 被废弃字段标识。 */
|
||||
fieldKey: string;
|
||||
/** 替代字段标识。 */
|
||||
replacementFieldKey?: string;
|
||||
/** 保留期,例如 6m / 1y。 */
|
||||
retentionPeriod?: string;
|
||||
/** 迁移提示。 */
|
||||
migrationHint?: string;
|
||||
}
|
||||
|
||||
/** MetaSchema 废弃请求。 */
|
||||
export interface MetaSchemaDeprecateRequest {
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
/** 废弃理由。 */
|
||||
reason: string;
|
||||
/** 期望被废弃版本,乐观锁。 */
|
||||
expectedVersion: number;
|
||||
/** 废弃前校验结果引用 ID。 */
|
||||
validationResultId: string;
|
||||
/** 废弃影响预览引用 ID。 */
|
||||
impactPreviewId: string;
|
||||
/** 要废弃的字段列表。 */
|
||||
deprecateFields?: DeprecateFieldRequest[];
|
||||
/** 是否废弃整个版本。 */
|
||||
deprecateEntireVersion?: boolean;
|
||||
/** 替代版本号。 */
|
||||
replacementVersion?: number;
|
||||
}
|
||||
|
||||
/** MetaSchema 废弃响应建议动作。 */
|
||||
export interface MetaSchemaDeprecateNextActionItem {
|
||||
/** 建议动作。 */
|
||||
action?: MetaSchemaDeprecateNextAction;
|
||||
/** 动作说明。 */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/** MetaSchema 废弃响应。 */
|
||||
export interface MetaSchemaDeprecateResponse {
|
||||
/** Schema 业务标识。 */
|
||||
schemaKey?: string;
|
||||
/** 废弃版本号。 */
|
||||
version?: number;
|
||||
/** 已废弃字段列表。 */
|
||||
deprecatedFields?: string[];
|
||||
/** 是否废弃整个版本。 */
|
||||
entireVersionDeprecated?: boolean;
|
||||
/** 受影响投影数。 */
|
||||
affectedProjectionCount?: number;
|
||||
/** 下一步建议动作。 */
|
||||
nextActions?: MetaSchemaDeprecateNextActionItem[];
|
||||
}
|
||||
|
||||
/** MetaSchema 灰度规则请求。 */
|
||||
export interface MetaSchemaGrayRulesRequest {
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
/** 调整理由。 */
|
||||
reason: string;
|
||||
/** 操作类型。 */
|
||||
action?: MetaSchemaGrayRulesAction;
|
||||
/** 灰度规则。 */
|
||||
grayRules?: GrayRulesConfig;
|
||||
}
|
||||
|
||||
/** MetaSchema 灰度规则响应。 */
|
||||
export interface MetaSchemaGrayRulesResponse {
|
||||
/** Schema 业务标识。 */
|
||||
schemaKey?: string;
|
||||
/** 版本号。 */
|
||||
version?: number;
|
||||
/** 灰度规则。 */
|
||||
grayRules?: GrayRulesConfig;
|
||||
/** 当前灰度影响范围描述。 */
|
||||
affectedScope?: string;
|
||||
/** 回滚入口。 */
|
||||
rollbackEntry?: string;
|
||||
}
|
||||
|
||||
/** 编码路径参数,避免 schemaKey 中的斜杠逃逸治理资源路径。 */
|
||||
function encodePathParam(value: string | number) {
|
||||
return encodeURIComponent(String(value));
|
||||
}
|
||||
|
||||
/** 拼接 MetaSchema 资源路径。 */
|
||||
function metaSchemaPath(schemaKey: string) {
|
||||
return `/governance/meta-schemas/${encodePathParam(schemaKey)}`;
|
||||
}
|
||||
|
||||
/** 查询 MetaSchema 列表和版本摘要。 */
|
||||
export function listMetaSchemasApi(params: MetaSchemaListParams) {
|
||||
return museAdminApi.get<GovernancePageResult<MetaSchemaSummary>>(
|
||||
'/governance/meta-schemas',
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询 MetaSchema 详情、当前 active 版本和灰度摘要。 */
|
||||
export function getMetaSchemaApi(schemaKey: string) {
|
||||
return museAdminApi.get<MetaSchemaDetail>(metaSchemaPath(schemaKey));
|
||||
}
|
||||
|
||||
/** 查询指定 MetaSchema 版本详情。 */
|
||||
export function getMetaSchemaVersionApi(schemaKey: string, version: number) {
|
||||
return museAdminApi.get<MetaSchemaVersionDetail>(
|
||||
`${metaSchemaPath(schemaKey)}/versions/${encodePathParam(version)}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 保存 MetaSchema 草稿。 */
|
||||
export function saveMetaSchemaDraftApi(
|
||||
schemaKey: string,
|
||||
data: MetaSchemaDraftRequest,
|
||||
) {
|
||||
return museAdminApi.post<MetaSchemaDraftResponse>(
|
||||
`${metaSchemaPath(schemaKey)}/drafts`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 校验 MetaSchema 草稿。 */
|
||||
export function validateMetaSchemaDraftApi(schemaKey: string, draftVersion: number) {
|
||||
return museAdminApi.post<ValidationResult>(
|
||||
`${metaSchemaPath(schemaKey)}/drafts/${encodePathParam(draftVersion)}/validate`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 预览 MetaSchema 草稿发布影响。 */
|
||||
export function previewMetaSchemaDraftImpactApi(
|
||||
schemaKey: string,
|
||||
draftVersion: number,
|
||||
) {
|
||||
return museAdminApi.post<MetaSchemaImpactPreview>(
|
||||
`${metaSchemaPath(schemaKey)}/drafts/${encodePathParam(
|
||||
draftVersion,
|
||||
)}/impact-preview`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 发布 MetaSchema 草稿为新版本。 */
|
||||
export function publishMetaSchemaDraftApi(
|
||||
schemaKey: string,
|
||||
draftVersion: number,
|
||||
data: MetaSchemaPublishRequest,
|
||||
) {
|
||||
return museAdminApi.post<MetaSchemaPublishResponse>(
|
||||
`${metaSchemaPath(schemaKey)}/drafts/${encodePathParam(draftVersion)}/publish`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 激活指定 MetaSchema 版本,支持全量或灰度。 */
|
||||
export function activateMetaSchemaVersionApi(
|
||||
schemaKey: string,
|
||||
version: number,
|
||||
data: MetaSchemaActivateRequest,
|
||||
) {
|
||||
return museAdminApi.post<MetaSchemaActivateResponse>(
|
||||
`${metaSchemaPath(schemaKey)}/versions/${encodePathParam(version)}/activate`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 回滚到指定 MetaSchema 版本。 */
|
||||
export function rollbackMetaSchemaVersionApi(
|
||||
schemaKey: string,
|
||||
version: number,
|
||||
data: MetaSchemaRollbackRequest,
|
||||
) {
|
||||
return museAdminApi.post<MetaSchemaRollbackResponse>(
|
||||
`${metaSchemaPath(schemaKey)}/versions/${encodePathParam(version)}/rollback`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 废弃指定 MetaSchema 版本或字段。 */
|
||||
export function deprecateMetaSchemaVersionApi(
|
||||
schemaKey: string,
|
||||
version: number,
|
||||
data: MetaSchemaDeprecateRequest,
|
||||
) {
|
||||
return museAdminApi.post<MetaSchemaDeprecateResponse>(
|
||||
`${metaSchemaPath(schemaKey)}/versions/${encodePathParam(version)}/deprecate`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 设置或调整 MetaSchema 灰度规则。 */
|
||||
export function setMetaSchemaGrayRulesApi(
|
||||
schemaKey: string,
|
||||
version: number,
|
||||
data: MetaSchemaGrayRulesRequest,
|
||||
) {
|
||||
return museAdminApi.post<MetaSchemaGrayRulesResponse>(
|
||||
`${metaSchemaPath(schemaKey)}/versions/${encodePathParam(version)}/gray-rules`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
cancelJobApi,
|
||||
getJobApi,
|
||||
getSourceEventApi,
|
||||
listJobsApi,
|
||||
listSourceEventsApi,
|
||||
retryJobApi,
|
||||
retrySourceEventApi,
|
||||
} from '../index';
|
||||
|
||||
const { museAdminApiMock } = vi.hoisted(() => ({
|
||||
museAdminApiMock: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('#/api/muse/client', () => ({
|
||||
museAdminApi: museAdminApiMock,
|
||||
}));
|
||||
|
||||
describe('muse jobs api', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('查询任务列表和任务详情', async () => {
|
||||
await listJobsApi({ owner: 'ai', pageNo: 1, pageSize: 20 });
|
||||
await getJobApi('job/1');
|
||||
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(1, '/jobs', {
|
||||
owner: 'ai',
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(2, '/jobs/job%2F1');
|
||||
});
|
||||
|
||||
it('提交任务重试和取消命令', async () => {
|
||||
await retryJobApi('42', { commandId: 'cmd-retry', reason: '重验通过' });
|
||||
await cancelJobApi('42', { commandId: 'cmd-cancel', reason: '来源召回' });
|
||||
|
||||
expect(museAdminApiMock.post).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/jobs/42/retry',
|
||||
expect.objectContaining({ commandId: 'cmd-retry' }),
|
||||
);
|
||||
expect(museAdminApiMock.post).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/jobs/42/cancel',
|
||||
expect.objectContaining({ commandId: 'cmd-cancel' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('查询和重试来源事件传播', async () => {
|
||||
await listSourceEventsApi({
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
sourceStatus: 'revoked',
|
||||
});
|
||||
await getSourceEventApi('event/1');
|
||||
await retrySourceEventApi('event/1', {
|
||||
commandId: 'cmd-event',
|
||||
reason: '补偿传播',
|
||||
});
|
||||
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(1, '/source-events', {
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
sourceStatus: 'revoked',
|
||||
});
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/source-events/event%2F1',
|
||||
);
|
||||
expect(museAdminApiMock.post).toHaveBeenCalledWith(
|
||||
'/source-events/event%2F1/retry',
|
||||
expect.objectContaining({ commandId: 'cmd-event' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
223
muse-admin/apps/web-antd/src/api/muse/jobs/index.ts
Normal file
223
muse-admin/apps/web-antd/src/api/muse/jobs/index.ts
Normal file
@ -0,0 +1,223 @@
|
||||
import type { PageResult } from '@vben/request';
|
||||
|
||||
import { museAdminApi } from '#/api/muse/client';
|
||||
|
||||
/** Muse 任务治理通用 ID。 */
|
||||
export type MuseJobId = number | string;
|
||||
|
||||
/** 任务分页查询参数。 */
|
||||
export interface MuseJobPageQuery {
|
||||
/** 查询参数索引签名,兼容 Muse client 的可序列化参数约束。 */
|
||||
[key: string]: boolean | number | string | undefined;
|
||||
/** 当前页码,从 1 开始。 */
|
||||
pageNo: number;
|
||||
/** 每页数量。 */
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
/** Muse 异步任务状态。 */
|
||||
export type JobStatus =
|
||||
| 'cancelled'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'queued'
|
||||
| 'running';
|
||||
|
||||
/** 来源状态。 */
|
||||
export type SourceStatus =
|
||||
| 'active'
|
||||
| 'blocked'
|
||||
| 'delisted'
|
||||
| 'owner_missing'
|
||||
| 'recalled'
|
||||
| 'revoked'
|
||||
| 'stale'
|
||||
| 'unauthorized';
|
||||
|
||||
/** 来源状态对应的动作策略。 */
|
||||
export type SourceActionPolicy =
|
||||
| 'allowed'
|
||||
| 'blocked'
|
||||
| 'needs_recheck'
|
||||
| 'read_only';
|
||||
|
||||
/** 任务列表查询条件。 */
|
||||
export interface JobListQuery extends MuseJobPageQuery {
|
||||
/** 任务状态筛选。 */
|
||||
status?: JobStatus;
|
||||
/** owner 模块筛选。 */
|
||||
owner?: string;
|
||||
}
|
||||
|
||||
/** Muse 异步任务摘要。 */
|
||||
export interface JobSummary {
|
||||
/** 任务 ID。 */
|
||||
jobId: number;
|
||||
/** 任务类型。 */
|
||||
type: string;
|
||||
/** 任务状态。 */
|
||||
status: JobStatus;
|
||||
/** owner 模块。 */
|
||||
owner: string;
|
||||
/** 创建者脱敏摘要。 */
|
||||
createdBy?: string;
|
||||
/** 已重试次数。 */
|
||||
retryCount?: number;
|
||||
/** 创建时间。 */
|
||||
createdAt: string;
|
||||
/** 完成时间。 */
|
||||
completedAt?: string;
|
||||
}
|
||||
|
||||
/** Muse 异步任务详情。 */
|
||||
export interface JobDetail extends JobSummary {
|
||||
/** 任务输入参数摘要,不能包含用户私有正文。 */
|
||||
input?: Record<string, unknown>;
|
||||
/** 任务结果摘要,不能包含候选正文全文。 */
|
||||
result?: Record<string, unknown>;
|
||||
/** 失败原因摘要。 */
|
||||
errorMessage?: string;
|
||||
/** 失败阶段。 */
|
||||
failedStage?: string;
|
||||
/** 是否可重试。 */
|
||||
retryable?: boolean;
|
||||
/** 重试组 ID。 */
|
||||
retryGroupId?: string;
|
||||
/** 关联 ID。 */
|
||||
correlationId?: string;
|
||||
/** 开始时间。 */
|
||||
startedAt?: string;
|
||||
/** 建议的下一步操作。 */
|
||||
nextActions?: string[];
|
||||
}
|
||||
|
||||
/** 任务命令请求。 */
|
||||
export interface JobCommandRequest {
|
||||
/** 幂等键。 */
|
||||
commandId?: string;
|
||||
/** 操作原因,写入审计摘要。 */
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/** 任务命令结果。 */
|
||||
export interface JobCommandResult {
|
||||
/** 任务 ID。 */
|
||||
jobId?: number;
|
||||
/** 命令执行后的任务状态。 */
|
||||
status?: JobStatus;
|
||||
}
|
||||
|
||||
/** 来源事件列表查询条件。 */
|
||||
export interface SourceEventQuery extends MuseJobPageQuery {
|
||||
/** 来源类型筛选。 */
|
||||
sourceType?: string;
|
||||
/** 来源状态筛选。 */
|
||||
sourceStatus?: SourceStatus;
|
||||
}
|
||||
|
||||
/** 来源事件摘要。 */
|
||||
export interface SourceEventSummary {
|
||||
/** 来源事件 ID。 */
|
||||
eventId: number;
|
||||
/** 来源对象类型。 */
|
||||
sourceType: string;
|
||||
/** 来源对象 ID。 */
|
||||
sourceId: number;
|
||||
/** 来源状态。 */
|
||||
sourceStatus: SourceStatus;
|
||||
/** 动作策略。 */
|
||||
actionPolicy: SourceActionPolicy;
|
||||
/** 影响范围描述。 */
|
||||
affectedScope?: string;
|
||||
/** 事件创建时间。 */
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/** 来源状态原因归因。 */
|
||||
export interface SourceReasonAttribution {
|
||||
/** 原因摘要。 */
|
||||
reason?: string;
|
||||
/** 原因来源。 */
|
||||
source?: string;
|
||||
}
|
||||
|
||||
/** 来源事件详情。 */
|
||||
export interface SourceEventDetail extends SourceEventSummary {
|
||||
/** 原因归因列表。 */
|
||||
reasonAttributions: SourceReasonAttribution[];
|
||||
/** 受影响绑定数。 */
|
||||
affectedBindings?: number;
|
||||
/** 受影响任务数。 */
|
||||
affectedTasks?: number;
|
||||
/** 传播状态。 */
|
||||
propagationStatus?: string;
|
||||
/** 关联传播任务 ID。 */
|
||||
jobId?: number;
|
||||
/** 建议的下一步操作。 */
|
||||
nextActions?: string[];
|
||||
}
|
||||
|
||||
/** 来源事件重试结果。 */
|
||||
export interface SourceEventRetryResult {
|
||||
/** 新建或复用的传播任务 ID。 */
|
||||
jobId?: number;
|
||||
/** 重试提交后的状态。 */
|
||||
status?: string;
|
||||
}
|
||||
|
||||
/** 编码路径参数,避免特殊字符破坏任务治理 URL。 */
|
||||
function encodePath(value: MuseJobId) {
|
||||
return encodeURIComponent(String(value));
|
||||
}
|
||||
|
||||
/** 查询 Muse 异步任务列表。 */
|
||||
export function listJobsApi(params: JobListQuery) {
|
||||
return museAdminApi.get<PageResult<JobSummary>>('/jobs', params);
|
||||
}
|
||||
|
||||
/** 查看任务详情。 */
|
||||
export function getJobApi(jobId: MuseJobId) {
|
||||
return museAdminApi.get<JobDetail>(`/jobs/${encodePath(jobId)}`);
|
||||
}
|
||||
|
||||
/** 重试任务;管理员必须先完成授权、来源和幂等重验。 */
|
||||
export function retryJobApi(jobId: MuseJobId, data?: JobCommandRequest) {
|
||||
return museAdminApi.post<JobCommandResult>(
|
||||
`/jobs/${encodePath(jobId)}/retry`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 取消任务;已完成任务不可取消,由服务端最终校验。 */
|
||||
export function cancelJobApi(jobId: MuseJobId, data?: JobCommandRequest) {
|
||||
return museAdminApi.post<JobCommandResult>(
|
||||
`/jobs/${encodePath(jobId)}/cancel`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询来源状态事件列表。 */
|
||||
export function listSourceEventsApi(params: SourceEventQuery) {
|
||||
return museAdminApi.get<PageResult<SourceEventSummary>>(
|
||||
'/source-events',
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查看来源事件详情。 */
|
||||
export function getSourceEventApi(eventId: MuseJobId) {
|
||||
return museAdminApi.get<SourceEventDetail>(
|
||||
`/source-events/${encodePath(eventId)}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 重试来源事件传播。 */
|
||||
export function retrySourceEventApi(
|
||||
eventId: MuseJobId,
|
||||
data?: JobCommandRequest,
|
||||
) {
|
||||
return museAdminApi.post<SourceEventRetryResult>(
|
||||
`/source-events/${encodePath(eventId)}/retry`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
disableGlobalKnowledgeBaseApi,
|
||||
getGlobalKnowledgeBaseApi,
|
||||
getGlobalKnowledgeBaseListApi,
|
||||
getGlobalKBSourceBindingsApi,
|
||||
previewGlobalKBImpactApi,
|
||||
reindexGlobalKnowledgeBaseApi,
|
||||
retryGlobalKBSourceEventApi,
|
||||
saveGlobalKBAccessPolicyDraftApi,
|
||||
} from '../index';
|
||||
|
||||
const clientMocks = vi.hoisted(() => ({
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('#/api/muse/client', () => ({
|
||||
museAdminApi: {
|
||||
get: clientMocks.get,
|
||||
post: clientMocks.post,
|
||||
},
|
||||
}));
|
||||
|
||||
describe('muse knowledge admin api', () => {
|
||||
it('按 OpenAPI 契约调用全局知识库查询和治理端点', async () => {
|
||||
await getGlobalKnowledgeBaseListApi({ pageNo: 1, pageSize: 20 });
|
||||
await getGlobalKnowledgeBaseApi('kb-1');
|
||||
await saveGlobalKBAccessPolicyDraftApi('kb-1', {
|
||||
canGenerate: true,
|
||||
canSearch: true,
|
||||
commandId: 'cmd-policy',
|
||||
scope: 'role_based',
|
||||
});
|
||||
await previewGlobalKBImpactApi('kb-1', { actionType: 'disable' });
|
||||
await disableGlobalKnowledgeBaseApi('kb-1', {
|
||||
commandId: 'cmd-disable',
|
||||
reason: '违规资料停用',
|
||||
});
|
||||
await reindexGlobalKnowledgeBaseApi('kb-1', {
|
||||
commandId: 'cmd-reindex',
|
||||
scope: 'failed_only',
|
||||
});
|
||||
await retryGlobalKBSourceEventApi('kb-1', {
|
||||
commandId: 'cmd-source',
|
||||
eventType: 'status_changed',
|
||||
reason: '重新传播来源状态',
|
||||
});
|
||||
await getGlobalKBSourceBindingsApi({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
sourceType: 'global_kb',
|
||||
});
|
||||
|
||||
expect(clientMocks.get).toHaveBeenCalledWith('/knowledge/global-kbs', {
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
expect(clientMocks.get).toHaveBeenCalledWith('/knowledge/global-kbs/kb-1');
|
||||
expect(clientMocks.post).toHaveBeenCalledWith(
|
||||
'/knowledge/global-kbs/kb-1/access-policies/drafts',
|
||||
expect.objectContaining({ commandId: 'cmd-policy' }),
|
||||
);
|
||||
expect(clientMocks.post).toHaveBeenCalledWith(
|
||||
'/knowledge/global-kbs/kb-1/impact-preview',
|
||||
{ actionType: 'disable' },
|
||||
);
|
||||
expect(clientMocks.post).toHaveBeenCalledWith(
|
||||
'/knowledge/global-kbs/kb-1/disable',
|
||||
expect.objectContaining({ reason: '违规资料停用' }),
|
||||
);
|
||||
expect(clientMocks.post).toHaveBeenCalledWith(
|
||||
'/knowledge/global-kbs/kb-1/reindex',
|
||||
expect.objectContaining({ scope: 'failed_only' }),
|
||||
);
|
||||
expect(clientMocks.post).toHaveBeenCalledWith(
|
||||
'/knowledge/global-kbs/kb-1/source-events',
|
||||
expect.objectContaining({ eventType: 'status_changed' }),
|
||||
);
|
||||
expect(clientMocks.get).toHaveBeenCalledWith('/knowledge/source-bindings', {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
sourceType: 'global_kb',
|
||||
});
|
||||
});
|
||||
});
|
||||
885
muse-admin/apps/web-antd/src/api/muse/knowledge/index.ts
Normal file
885
muse-admin/apps/web-antd/src/api/muse/knowledge/index.ts
Normal file
@ -0,0 +1,885 @@
|
||||
import type { PageResult } from '@vben/request';
|
||||
|
||||
import { museAdminApi } from '#/api/muse/client';
|
||||
|
||||
import type { MuseQueryParams } from '#/api/muse/client';
|
||||
|
||||
/** 将强类型查询对象收敛为 Muse client 支持的可序列化查询参数。 */
|
||||
function toMuseQueryParams(params: object): MuseQueryParams {
|
||||
return params as MuseQueryParams;
|
||||
}
|
||||
|
||||
/** 全局知识库启停和处理状态。 */
|
||||
export type GlobalKnowledgeBaseStatus =
|
||||
| 'active'
|
||||
| 'disabled'
|
||||
| 'failed'
|
||||
| 'processing';
|
||||
|
||||
/** 全局知识库可见范围。 */
|
||||
export type GlobalKnowledgeVisibilityScope = 'all' | 'custom' | 'role_based';
|
||||
|
||||
/** 全局知识资料处理状态。 */
|
||||
export type GlobalKBDocumentProcessingStatus =
|
||||
| 'deleting'
|
||||
| 'failed'
|
||||
| 'generative'
|
||||
| 'pending_process'
|
||||
| 'pending_scan'
|
||||
| 'processing'
|
||||
| 'scan_blocked'
|
||||
| 'searchable';
|
||||
|
||||
/** 全局知识任务状态。 */
|
||||
export type GlobalKBProcessingTaskStatus =
|
||||
| 'cancelling'
|
||||
| 'cleaning'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'indexing'
|
||||
| 'parsing'
|
||||
| 'queued'
|
||||
| 'scanning';
|
||||
|
||||
/** 全局知识影响预览动作。 */
|
||||
export type GlobalKBImpactActionType =
|
||||
| 'disable'
|
||||
| 'document_delete'
|
||||
| 'enable'
|
||||
| 'policy_publish'
|
||||
| 'version_activate';
|
||||
|
||||
/** 全局知识来源事件类型。 */
|
||||
export type GlobalKBSourceEventType =
|
||||
| 'document_deleted'
|
||||
| 'kb_disabled'
|
||||
| 'kb_enabled'
|
||||
| 'policy_changed'
|
||||
| 'status_changed'
|
||||
| 'version_changed';
|
||||
|
||||
/** 来源动作策略,重验要求放在策略而不是来源状态里。 */
|
||||
export type SourceActionPolicy =
|
||||
| 'allowed'
|
||||
| 'blocked'
|
||||
| 'needs_recheck'
|
||||
| 'read_only';
|
||||
|
||||
/** 来源绑定状态。 */
|
||||
export type SourceBindingStatus =
|
||||
| 'active'
|
||||
| 'blocked'
|
||||
| 'delisted'
|
||||
| 'owner_missing'
|
||||
| 'recalled'
|
||||
| 'revoked'
|
||||
| 'stale'
|
||||
| 'unauthorized';
|
||||
|
||||
/** 来源绑定类型。 */
|
||||
export type SourceBindingType = 'global_kb' | 'market_kb' | 'user_kb';
|
||||
|
||||
/** 来源目标 owner。 */
|
||||
export type SourceBindingTargetOwner =
|
||||
| 'account'
|
||||
| 'ai'
|
||||
| 'content'
|
||||
| 'knowledge'
|
||||
| 'market';
|
||||
|
||||
/** 通用分页查询参数。 */
|
||||
export interface MusePageQuery {
|
||||
/** 页码,从 1 开始。 */
|
||||
pageNo?: number;
|
||||
/** 每页条数,上限由后端控制。 */
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
/** 全局知识库列表筛选条件。 */
|
||||
export interface GlobalKnowledgeBaseQuery extends MusePageQuery {
|
||||
/** 搜索关键词,匹配名称或说明。 */
|
||||
keyword?: string;
|
||||
/** 知识库状态筛选。 */
|
||||
status?: GlobalKnowledgeBaseStatus;
|
||||
/** 分类标签筛选。 */
|
||||
category?: string;
|
||||
}
|
||||
|
||||
/** 全局知识库处理摘要。 */
|
||||
export interface GlobalKBProcessingSummaryVO {
|
||||
/** 已可用于生成的资料数量。 */
|
||||
generativeCount?: number;
|
||||
/** 处理失败的资料数量。 */
|
||||
failedCount?: number;
|
||||
/** 仍在处理中的资料数量。 */
|
||||
processingCount?: number;
|
||||
/** 已可用于检索的资料数量。 */
|
||||
searchableCount?: number;
|
||||
}
|
||||
|
||||
/** 全局知识库列表项。 */
|
||||
export interface GlobalKnowledgeBaseSummaryVO {
|
||||
/** 当前授权策略版本。 */
|
||||
activePolicyVersion?: number;
|
||||
/** 已绑定作品或目标对象数量。 */
|
||||
bindingCount?: number;
|
||||
/** 分类标签。 */
|
||||
category?: string;
|
||||
/** 当前激活知识库版本。 */
|
||||
currentVersion?: number;
|
||||
/** 创建时间。 */
|
||||
createdAt?: string;
|
||||
/** 知识库说明。 */
|
||||
description?: string;
|
||||
/** 资料数量。 */
|
||||
documentCount?: number;
|
||||
/** 全局知识库 ID。 */
|
||||
kbId: string;
|
||||
/** 知识库名称。 */
|
||||
name: string;
|
||||
/** 当前处理和可用性状态。 */
|
||||
status: GlobalKnowledgeBaseStatus;
|
||||
/** 最近更新时间。 */
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
/** 全局知识库详情。 */
|
||||
export interface GlobalKnowledgeBaseDetailVO
|
||||
extends GlobalKnowledgeBaseSummaryVO {
|
||||
/** 资料处理聚合摘要。 */
|
||||
processingSummary?: GlobalKBProcessingSummaryVO;
|
||||
/** 可见范围。 */
|
||||
visibilityScope?: GlobalKnowledgeVisibilityScope | string;
|
||||
}
|
||||
|
||||
/** 创建全局知识库请求。 */
|
||||
export interface CreateGlobalKnowledgeBaseDTO {
|
||||
/** 分类标签。 */
|
||||
category?: string;
|
||||
/** 幂等命令 ID。 */
|
||||
commandId: string;
|
||||
/** 知识库说明。 */
|
||||
description?: string;
|
||||
/** 知识库名称。 */
|
||||
name: string;
|
||||
/** 可见范围。 */
|
||||
visibilityScope?: GlobalKnowledgeVisibilityScope;
|
||||
}
|
||||
|
||||
/** 创建全局知识库结果。 */
|
||||
export interface CreateGlobalKnowledgeBaseResultVO {
|
||||
/** 新建知识库 ID。 */
|
||||
kbId?: string;
|
||||
/** 新建后的初始状态。 */
|
||||
status?: 'draft';
|
||||
}
|
||||
|
||||
/** 更新全局知识库请求。 */
|
||||
export interface UpdateGlobalKnowledgeBaseDTO {
|
||||
/** 分类标签。 */
|
||||
category?: string;
|
||||
/** 幂等命令 ID。 */
|
||||
commandId: string;
|
||||
/** 停用时的原因摘要。 */
|
||||
disableReason?: string;
|
||||
/** 知识库说明。 */
|
||||
description?: string;
|
||||
/** 知识库名称。 */
|
||||
name?: string;
|
||||
/** 可见范围。 */
|
||||
visibilityScope?: GlobalKnowledgeVisibilityScope;
|
||||
}
|
||||
|
||||
/** 更新全局知识库结果。 */
|
||||
export interface UpdateGlobalKnowledgeBaseResultVO {
|
||||
/** 知识库 ID。 */
|
||||
kbId?: string;
|
||||
/** 更新时间。 */
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
/** 全局知识资料列表筛选条件。 */
|
||||
export interface GlobalKBDocumentQuery extends MusePageQuery {
|
||||
/** 资料处理状态筛选。 */
|
||||
processingStatus?: GlobalKBDocumentProcessingStatus;
|
||||
}
|
||||
|
||||
/** 全局知识资料列表项。 */
|
||||
export interface GlobalKBDocumentSummaryVO {
|
||||
/** 资料 ID。 */
|
||||
documentId: string;
|
||||
/** 失败原因摘要,不返回原文全文。 */
|
||||
failureReason?: string;
|
||||
/** 是否可用于生成。 */
|
||||
isGenerative?: boolean;
|
||||
/** 是否可用于检索。 */
|
||||
isSearchable?: boolean;
|
||||
/** 资料名称。 */
|
||||
name: string;
|
||||
/** 资料处理状态。 */
|
||||
processingStatus?: GlobalKBDocumentProcessingStatus | string;
|
||||
/** 扫描状态。 */
|
||||
scanStatus?: string;
|
||||
/** 资料大小,单位字节。 */
|
||||
size?: number;
|
||||
/** 资料类型,如 file、entry、link。 */
|
||||
type?: string;
|
||||
/** 最近更新时间。 */
|
||||
updatedAt?: string;
|
||||
/** 资料版本。 */
|
||||
version?: number;
|
||||
}
|
||||
|
||||
/** 全局知识资料版本摘要。 */
|
||||
export interface GlobalKBDocumentVersionVO {
|
||||
/** 创建时间。 */
|
||||
createdAt?: string;
|
||||
/** 版本处理状态。 */
|
||||
processingStatus?: string;
|
||||
/** 资料版本号。 */
|
||||
version?: number;
|
||||
}
|
||||
|
||||
/** 全局知识资料详情。 */
|
||||
export interface GlobalKBDocumentDetailVO extends GlobalKBDocumentSummaryVO {
|
||||
/** 当前资料版本。 */
|
||||
currentVersion?: number;
|
||||
/** 索引状态。 */
|
||||
indexStatus?: string;
|
||||
/** 解析状态。 */
|
||||
parseStatus?: string;
|
||||
/** 资料版本列表。 */
|
||||
versions?: GlobalKBDocumentVersionVO[];
|
||||
}
|
||||
|
||||
/** 上传或登记资料请求。 */
|
||||
export interface UploadGlobalKBDocumentDTO {
|
||||
/** 幂等命令 ID。 */
|
||||
commandId: string;
|
||||
/** 手写条目内容。 */
|
||||
entryContent?: string;
|
||||
/** 上传文件,实际调用方可传 File 或 FormData 中的文件字段。 */
|
||||
file?: Blob | File | string;
|
||||
/** 外链资料 URL。 */
|
||||
linkUrl?: string;
|
||||
/** 来源说明。 */
|
||||
sourceDescription?: string;
|
||||
}
|
||||
|
||||
/** 上传或登记资料结果。 */
|
||||
export interface UploadGlobalKBDocumentResultVO {
|
||||
/** 资料 ID。 */
|
||||
documentId?: string;
|
||||
/** 进入待扫描后的处理状态。 */
|
||||
processingStatus?: 'pending_scan';
|
||||
/** 扫描任务 ID。 */
|
||||
scanTaskId?: string;
|
||||
}
|
||||
|
||||
/** 停用资料请求。 */
|
||||
export interface DeleteGlobalKBDocumentDTO {
|
||||
/** 幂等命令 ID。 */
|
||||
commandId: string;
|
||||
/** 停用原因。 */
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/** 停用资料结果。 */
|
||||
export interface DeleteGlobalKBDocumentResultVO {
|
||||
/** 受影响绑定数量。 */
|
||||
affectedBindings?: number;
|
||||
/** 传播出的来源事件 ID。 */
|
||||
sourceEventId?: string;
|
||||
}
|
||||
|
||||
/** 新增资料版本请求。 */
|
||||
export interface CreateGlobalKBDocumentVersionDTO {
|
||||
/** 幂等命令 ID。 */
|
||||
commandId: string;
|
||||
/** 上传文件。 */
|
||||
file?: Blob | File | string;
|
||||
/** 来源说明。 */
|
||||
sourceDescription?: string;
|
||||
}
|
||||
|
||||
/** 新增资料版本结果。 */
|
||||
export interface CreateGlobalKBDocumentVersionResultVO {
|
||||
/** 资料 ID。 */
|
||||
documentId?: string;
|
||||
/** 新版本号。 */
|
||||
newVersion?: number;
|
||||
/** 处理任务 ID。 */
|
||||
processingTaskId?: string;
|
||||
}
|
||||
|
||||
/** 全局知识库版本摘要。 */
|
||||
export interface GlobalKBVersionVO {
|
||||
/** 激活时间。 */
|
||||
activatedAt?: string;
|
||||
/** 变更摘要。 */
|
||||
changeSummary?: string;
|
||||
/** 创建时间。 */
|
||||
createdAt?: string;
|
||||
/** 资料数量。 */
|
||||
documentCount?: number;
|
||||
/** 可生成资料数量。 */
|
||||
generativeCount?: number;
|
||||
/** 是否当前激活版本。 */
|
||||
isActive?: boolean;
|
||||
/** 可检索资料数量。 */
|
||||
searchableCount?: number;
|
||||
/** 版本号。 */
|
||||
version?: number;
|
||||
}
|
||||
|
||||
/** 全局知识库版本列表结果。 */
|
||||
export interface GlobalKBVersionListVO {
|
||||
/** 当前激活版本。 */
|
||||
currentActiveVersion?: number;
|
||||
/** 版本列表。 */
|
||||
versions?: GlobalKBVersionVO[];
|
||||
}
|
||||
|
||||
/** 激活全局知识库版本请求。 */
|
||||
export interface ActivateGlobalKBVersionDTO {
|
||||
/** 幂等命令 ID。 */
|
||||
commandId: string;
|
||||
/** 影响预览引用 ID。 */
|
||||
impactPreviewReference?: string;
|
||||
/** 激活原因。 */
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/** 激活全局知识库版本结果。 */
|
||||
export interface ActivateGlobalKBVersionResultVO {
|
||||
/** 激活后的版本号。 */
|
||||
activeVersion?: number;
|
||||
/** 来源事件 ID。 */
|
||||
sourceEventId?: string;
|
||||
}
|
||||
|
||||
/** 全局知识库授权策略。 */
|
||||
export interface GlobalKBAccessPolicyVO {
|
||||
/** 是否允许复制。 */
|
||||
canCopy?: boolean;
|
||||
/** 是否允许导出或外发。 */
|
||||
canExport?: boolean;
|
||||
/** 是否允许生成使用。 */
|
||||
canGenerate?: boolean;
|
||||
/** 是否允许检索。 */
|
||||
canSearch?: boolean;
|
||||
/** 创建时间。 */
|
||||
createdAt?: string;
|
||||
/** 过期时间。 */
|
||||
expiresAt?: string;
|
||||
/** 是否当前激活策略。 */
|
||||
isActive?: boolean;
|
||||
/** 策略 ID。 */
|
||||
policyId?: string;
|
||||
/** 发布时间。 */
|
||||
publishedAt?: string;
|
||||
/** 授权范围。 */
|
||||
scope?: string;
|
||||
/** 策略版本。 */
|
||||
version?: number;
|
||||
}
|
||||
|
||||
/** 授权策略列表结果。 */
|
||||
export interface GlobalKBAccessPolicyListVO {
|
||||
/** 当前激活授权策略版本。 */
|
||||
activePolicyVersion?: number;
|
||||
/** 授权策略列表。 */
|
||||
policies?: GlobalKBAccessPolicyVO[];
|
||||
}
|
||||
|
||||
/** 保存授权策略草稿请求。 */
|
||||
export interface SaveGlobalKBAccessPolicyDraftDTO {
|
||||
/** 允许的角色列表,角色范围授权时必填。 */
|
||||
allowedRoles?: string[];
|
||||
/** 是否允许复制。 */
|
||||
canCopy?: boolean;
|
||||
/** 是否允许导出或进入外发链路。 */
|
||||
canExport?: boolean;
|
||||
/** 是否允许生成使用。 */
|
||||
canGenerate?: boolean;
|
||||
/** 是否允许检索。 */
|
||||
canSearch?: boolean;
|
||||
/** 幂等命令 ID。 */
|
||||
commandId: string;
|
||||
/** 外发范围限制。 */
|
||||
externalScope?: string;
|
||||
/** 过期时间。 */
|
||||
expiresAt?: string;
|
||||
/** 授权范围。 */
|
||||
scope: string;
|
||||
}
|
||||
|
||||
/** 保存授权策略草稿结果。 */
|
||||
export interface SaveGlobalKBAccessPolicyDraftResultVO {
|
||||
/** 草稿 ID。 */
|
||||
draftId?: string;
|
||||
/** 草稿版本。 */
|
||||
draftVersion?: number;
|
||||
}
|
||||
|
||||
/** 发布授权策略请求。 */
|
||||
export interface PublishGlobalKBAccessPolicyDTO {
|
||||
/** 幂等命令 ID。 */
|
||||
commandId: string;
|
||||
/** 影响预览引用 ID。 */
|
||||
impactPreviewReference?: string;
|
||||
/** 发布原因。 */
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/** 发布授权策略结果。 */
|
||||
export interface PublishGlobalKBAccessPolicyResultVO {
|
||||
/** 发布后的激活策略版本。 */
|
||||
activePolicyVersion?: number;
|
||||
/** 来源事件 ID。 */
|
||||
sourceEventId?: string;
|
||||
}
|
||||
|
||||
/** 启用或停用全局知识库请求。 */
|
||||
export interface GlobalKBEnableDisableDTO {
|
||||
/** 幂等命令 ID。 */
|
||||
commandId: string;
|
||||
/** 操作原因。 */
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/** 启用全局知识库结果。 */
|
||||
export interface EnableGlobalKnowledgeBaseResultVO {
|
||||
/** 启用后的状态。 */
|
||||
status?: 'active';
|
||||
}
|
||||
|
||||
/** 停用全局知识库结果。 */
|
||||
export interface DisableGlobalKnowledgeBaseResultVO {
|
||||
/** 受影响绑定数量。 */
|
||||
affectedBindings?: number;
|
||||
/** 来源事件 ID。 */
|
||||
sourceEventId?: string;
|
||||
/** 停用后的状态。 */
|
||||
status?: 'disabled';
|
||||
}
|
||||
|
||||
/** 全局知识库影响预览请求。 */
|
||||
export interface PreviewGlobalKBImpactDTO {
|
||||
/** 预览动作类型。 */
|
||||
actionType: GlobalKBImpactActionType;
|
||||
/** 目标版本,激活版本时必填。 */
|
||||
targetVersion?: number;
|
||||
}
|
||||
|
||||
/** 全局知识库影响预览结果。 */
|
||||
export interface GlobalKBImpactPreviewVO {
|
||||
/** 受影响绑定数量。 */
|
||||
affectedBindings?: number;
|
||||
/** 受影响安装数量。 */
|
||||
affectedInstalls?: number;
|
||||
/** 受影响作品数量。 */
|
||||
affectedWorks?: number;
|
||||
/** 预览过期时间。 */
|
||||
expiresAt?: string;
|
||||
/** 影响预览引用 ID。 */
|
||||
impactId?: string;
|
||||
/** 风险摘要。 */
|
||||
riskSummary?: string;
|
||||
/** 运行中任务数量。 */
|
||||
runningTasks?: number;
|
||||
}
|
||||
|
||||
/** 重建全局知识库索引请求。 */
|
||||
export interface ReindexGlobalKnowledgeBaseDTO {
|
||||
/** 幂等命令 ID。 */
|
||||
commandId: string;
|
||||
/** 重建范围。 */
|
||||
scope?: 'failed_only' | 'full' | 'incremental';
|
||||
}
|
||||
|
||||
/** 异步任务句柄。 */
|
||||
export interface GlobalKBJobResultVO {
|
||||
/** 任务 ID。 */
|
||||
jobId?: string;
|
||||
/** 轮询地址。 */
|
||||
pollUrl?: string;
|
||||
/** 任务状态。 */
|
||||
status?: 'queued' | string;
|
||||
}
|
||||
|
||||
/** 全局知识库处理任务列表筛选。 */
|
||||
export interface GlobalKBProcessingTaskQuery extends MusePageQuery {
|
||||
/** 任务状态筛选。 */
|
||||
status?: GlobalKBProcessingTaskStatus;
|
||||
}
|
||||
|
||||
/** 全局知识库处理任务摘要。 */
|
||||
export interface GlobalKBProcessingTaskSummaryVO {
|
||||
/** 资料数量。 */
|
||||
documentCount?: number;
|
||||
/** 失败数量。 */
|
||||
failedCount?: number;
|
||||
/** 失败原因摘要。 */
|
||||
failureReason?: string;
|
||||
/** 当前处理阶段。 */
|
||||
stage?: string;
|
||||
/** 开始时间。 */
|
||||
startedAt?: string;
|
||||
/** 任务状态。 */
|
||||
status?: GlobalKBProcessingTaskStatus | string;
|
||||
/** 任务 ID。 */
|
||||
taskId: string;
|
||||
/** 任务类型,如 scan、clean、parse、index。 */
|
||||
taskType?: string;
|
||||
/** 最近更新时间。 */
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
/** 触发来源事件请求。 */
|
||||
export interface TriggerGlobalKBSourceEventDTO {
|
||||
/** 来源动作策略。 */
|
||||
actionPolicy?: SourceActionPolicy;
|
||||
/** 幂等命令 ID。 */
|
||||
commandId: string;
|
||||
/** 事件类型。 */
|
||||
eventType: GlobalKBSourceEventType;
|
||||
/** 事件原因。 */
|
||||
reason: string;
|
||||
/** 目标版本。 */
|
||||
targetVersion?: number;
|
||||
}
|
||||
|
||||
/** 触发来源事件结果。 */
|
||||
export interface TriggerGlobalKBSourceEventResultVO {
|
||||
/** 受影响目标数量。 */
|
||||
affectedTargets?: number;
|
||||
/** 来源事件 ID。 */
|
||||
eventId?: string;
|
||||
}
|
||||
|
||||
/** 来源绑定查询条件。 */
|
||||
export interface SourceBindingQuery extends MusePageQuery {
|
||||
/** 来源状态。 */
|
||||
sourceStatus?: SourceBindingStatus;
|
||||
/** 来源类型。 */
|
||||
sourceType?: SourceBindingType;
|
||||
/** 目标 owner。 */
|
||||
targetOwner?: SourceBindingTargetOwner;
|
||||
}
|
||||
|
||||
/** 来源绑定摘要。 */
|
||||
export interface SourceBindingSummaryVO {
|
||||
/** 动作策略。 */
|
||||
actionPolicy?: SourceActionPolicy | string;
|
||||
/** 受影响作品数量。 */
|
||||
affectedWorkCount?: number;
|
||||
/** 绑定 ID。 */
|
||||
bindingId: string;
|
||||
/** 来源 ID。 */
|
||||
sourceId?: string;
|
||||
/** 来源状态。 */
|
||||
sourceStatus?: SourceBindingStatus | string;
|
||||
/** 来源类型。 */
|
||||
sourceType?: SourceBindingType | string;
|
||||
/** 来源版本。 */
|
||||
sourceVersion?: number;
|
||||
/** 目标 ID。 */
|
||||
targetId?: string;
|
||||
/** 目标 owner。 */
|
||||
targetOwner?: SourceBindingTargetOwner | string;
|
||||
/** 最近更新时间。 */
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
/** 知识草稿治理查询条件。 */
|
||||
export interface KnowledgeDraftGovernanceQuery extends MusePageQuery {
|
||||
/** 来源类型。 */
|
||||
sourceType?: 'external' | 'extraction' | 'import';
|
||||
/** 草稿状态。 */
|
||||
status?: 'confirmed' | 'conflicted' | 'failed' | 'ignored' | 'pending';
|
||||
}
|
||||
|
||||
/** 知识草稿治理摘要。 */
|
||||
export interface KnowledgeDraftGovernanceSummaryVO {
|
||||
/** 冲突数量。 */
|
||||
conflictCount?: number;
|
||||
/** 创建时间。 */
|
||||
createdAt?: string;
|
||||
/** 草稿 ID。 */
|
||||
draftId: string;
|
||||
/** 实体类型。 */
|
||||
entityType?: string;
|
||||
/** 来源类型。 */
|
||||
sourceType?: string;
|
||||
/** 草稿状态。 */
|
||||
status?: string;
|
||||
/** 最近更新时间。 */
|
||||
updatedAt?: string;
|
||||
/** 作品 ID。 */
|
||||
workId?: string;
|
||||
/** 作品标题摘要。 */
|
||||
workTitle?: string;
|
||||
}
|
||||
|
||||
/** 投影任务查询条件。 */
|
||||
export interface KnowledgeProjectionTaskQuery extends MusePageQuery {
|
||||
/** 任务状态。 */
|
||||
status?: 'completed' | 'failed' | 'queued' | 'running';
|
||||
}
|
||||
|
||||
/** 投影任务摘要。 */
|
||||
export interface KnowledgeProjectionTaskSummaryVO {
|
||||
/** 受影响投影数量。 */
|
||||
affectedProjectionCount?: number;
|
||||
/** 完成时间。 */
|
||||
completedAt?: string;
|
||||
/** 失败原因摘要。 */
|
||||
failureReason?: string;
|
||||
/** 来源知识库 ID。 */
|
||||
sourceKbId?: string;
|
||||
/** 来源知识库名称。 */
|
||||
sourceKbName?: string;
|
||||
/** 开始时间。 */
|
||||
startedAt?: string;
|
||||
/** 任务状态。 */
|
||||
status?: string;
|
||||
/** 任务 ID。 */
|
||||
taskId: string;
|
||||
/** 任务类型。 */
|
||||
taskType?: string;
|
||||
}
|
||||
|
||||
/** 查询全局知识库分页列表。 */
|
||||
export function getGlobalKnowledgeBaseListApi(
|
||||
params: GlobalKnowledgeBaseQuery,
|
||||
) {
|
||||
return museAdminApi.get<PageResult<GlobalKnowledgeBaseSummaryVO>>(
|
||||
'/knowledge/global-kbs',
|
||||
toMuseQueryParams(params),
|
||||
);
|
||||
}
|
||||
|
||||
/** 创建全局知识库。 */
|
||||
export function createGlobalKnowledgeBaseApi(
|
||||
data: CreateGlobalKnowledgeBaseDTO,
|
||||
) {
|
||||
return museAdminApi.post<CreateGlobalKnowledgeBaseResultVO>(
|
||||
'/knowledge/global-kbs',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询全局知识库详情。 */
|
||||
export function getGlobalKnowledgeBaseApi(kbId: string) {
|
||||
return museAdminApi.get<GlobalKnowledgeBaseDetailVO>(
|
||||
`/knowledge/global-kbs/${kbId}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 更新全局知识库基础信息。 */
|
||||
export function updateGlobalKnowledgeBaseApi(
|
||||
kbId: string,
|
||||
data: UpdateGlobalKnowledgeBaseDTO,
|
||||
) {
|
||||
return museAdminApi.patch<UpdateGlobalKnowledgeBaseResultVO>(
|
||||
`/knowledge/global-kbs/${kbId}`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询全局知识库资料分页列表。 */
|
||||
export function getGlobalKBDocumentsApi(
|
||||
kbId: string,
|
||||
params: GlobalKBDocumentQuery,
|
||||
) {
|
||||
return museAdminApi.get<PageResult<GlobalKBDocumentSummaryVO>>(
|
||||
`/knowledge/global-kbs/${kbId}/documents`,
|
||||
toMuseQueryParams(params),
|
||||
);
|
||||
}
|
||||
|
||||
/** 上传或登记全局知识库资料。 */
|
||||
export function uploadGlobalKBDocumentApi(
|
||||
kbId: string,
|
||||
data: FormData | UploadGlobalKBDocumentDTO,
|
||||
) {
|
||||
return museAdminApi.post<UploadGlobalKBDocumentResultVO>(
|
||||
`/knowledge/global-kbs/${kbId}/documents`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询全局知识库资料详情。 */
|
||||
export function getGlobalKBDocumentApi(kbId: string, documentId: string) {
|
||||
return museAdminApi.get<GlobalKBDocumentDetailVO>(
|
||||
`/knowledge/global-kbs/${kbId}/documents/${documentId}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 停用全局知识库资料。 */
|
||||
export function deleteGlobalKBDocumentApi(
|
||||
kbId: string,
|
||||
documentId: string,
|
||||
data: DeleteGlobalKBDocumentDTO,
|
||||
) {
|
||||
return museAdminApi.delete<DeleteGlobalKBDocumentResultVO>(
|
||||
`/knowledge/global-kbs/${kbId}/documents/${documentId}`,
|
||||
toMuseQueryParams(data),
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增全局知识库资料版本。 */
|
||||
export function createGlobalKBDocumentVersionApi(
|
||||
kbId: string,
|
||||
documentId: string,
|
||||
data: FormData | CreateGlobalKBDocumentVersionDTO,
|
||||
) {
|
||||
return museAdminApi.post<CreateGlobalKBDocumentVersionResultVO>(
|
||||
`/knowledge/global-kbs/${kbId}/documents/${documentId}/versions`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询全局知识库版本列表。 */
|
||||
export function getGlobalKBVersionsApi(kbId: string) {
|
||||
return museAdminApi.get<GlobalKBVersionListVO>(
|
||||
`/knowledge/global-kbs/${kbId}/versions`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 激活全局知识库版本。 */
|
||||
export function activateGlobalKBVersionApi(
|
||||
kbId: string,
|
||||
version: number,
|
||||
data: ActivateGlobalKBVersionDTO,
|
||||
) {
|
||||
return museAdminApi.post<ActivateGlobalKBVersionResultVO>(
|
||||
`/knowledge/global-kbs/${kbId}/versions/${version}/activate`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询全局知识库授权策略列表。 */
|
||||
export function getGlobalKBAccessPoliciesApi(kbId: string) {
|
||||
return museAdminApi.get<GlobalKBAccessPolicyListVO>(
|
||||
`/knowledge/global-kbs/${kbId}/access-policies`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 保存全局知识库授权策略草稿。 */
|
||||
export function saveGlobalKBAccessPolicyDraftApi(
|
||||
kbId: string,
|
||||
data: SaveGlobalKBAccessPolicyDraftDTO,
|
||||
) {
|
||||
return museAdminApi.post<SaveGlobalKBAccessPolicyDraftResultVO>(
|
||||
`/knowledge/global-kbs/${kbId}/access-policies/drafts`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 发布全局知识库授权策略。 */
|
||||
export function publishGlobalKBAccessPolicyApi(
|
||||
kbId: string,
|
||||
draftId: string,
|
||||
data: PublishGlobalKBAccessPolicyDTO,
|
||||
) {
|
||||
return museAdminApi.post<PublishGlobalKBAccessPolicyResultVO>(
|
||||
`/knowledge/global-kbs/${kbId}/access-policies/drafts/${draftId}/publish`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 启用全局知识库。 */
|
||||
export function enableGlobalKnowledgeBaseApi(
|
||||
kbId: string,
|
||||
data: GlobalKBEnableDisableDTO,
|
||||
) {
|
||||
return museAdminApi.post<EnableGlobalKnowledgeBaseResultVO>(
|
||||
`/knowledge/global-kbs/${kbId}/enable`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 停用全局知识库。 */
|
||||
export function disableGlobalKnowledgeBaseApi(
|
||||
kbId: string,
|
||||
data: GlobalKBEnableDisableDTO,
|
||||
) {
|
||||
return museAdminApi.post<DisableGlobalKnowledgeBaseResultVO>(
|
||||
`/knowledge/global-kbs/${kbId}/disable`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 预览全局知识库治理影响。 */
|
||||
export function previewGlobalKBImpactApi(
|
||||
kbId: string,
|
||||
data: PreviewGlobalKBImpactDTO,
|
||||
) {
|
||||
return museAdminApi.post<GlobalKBImpactPreviewVO>(
|
||||
`/knowledge/global-kbs/${kbId}/impact-preview`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 重建全局知识库索引。 */
|
||||
export function reindexGlobalKnowledgeBaseApi(
|
||||
kbId: string,
|
||||
data: ReindexGlobalKnowledgeBaseDTO,
|
||||
) {
|
||||
return museAdminApi.post<GlobalKBJobResultVO>(
|
||||
`/knowledge/global-kbs/${kbId}/reindex`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询全局知识库资料处理任务。 */
|
||||
export function getGlobalKBProcessingTasksApi(
|
||||
kbId: string,
|
||||
params: GlobalKBProcessingTaskQuery,
|
||||
) {
|
||||
return museAdminApi.get<PageResult<GlobalKBProcessingTaskSummaryVO>>(
|
||||
`/knowledge/global-kbs/${kbId}/processing-tasks`,
|
||||
toMuseQueryParams(params),
|
||||
);
|
||||
}
|
||||
|
||||
/** 触发或重试全局知识库来源状态传播。 */
|
||||
export function retryGlobalKBSourceEventApi(
|
||||
kbId: string,
|
||||
data: TriggerGlobalKBSourceEventDTO,
|
||||
) {
|
||||
return museAdminApi.post<TriggerGlobalKBSourceEventResultVO>(
|
||||
`/knowledge/global-kbs/${kbId}/source-events`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询来源绑定和来源状态。 */
|
||||
export function getGlobalKBSourceBindingsApi(params: SourceBindingQuery) {
|
||||
return museAdminApi.get<PageResult<SourceBindingSummaryVO>>(
|
||||
'/knowledge/source-bindings',
|
||||
toMuseQueryParams(params),
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询知识草稿治理摘要。 */
|
||||
export function getKnowledgeDraftsGovernanceApi(
|
||||
params: KnowledgeDraftGovernanceQuery,
|
||||
) {
|
||||
return museAdminApi.get<PageResult<KnowledgeDraftGovernanceSummaryVO>>(
|
||||
'/knowledge/drafts',
|
||||
toMuseQueryParams(params),
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询知识投影和索引任务。 */
|
||||
export function getKnowledgeProjectionTasksApi(
|
||||
params: KnowledgeProjectionTaskQuery,
|
||||
) {
|
||||
return museAdminApi.get<PageResult<KnowledgeProjectionTaskSummaryVO>>(
|
||||
'/knowledge/projection-tasks',
|
||||
toMuseQueryParams(params),
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,113 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
approveMarketPublishRequestApi,
|
||||
delistMarketAssetApi,
|
||||
getMarketAppealApi,
|
||||
getMarketAssetApi,
|
||||
getMarketAssetListApi,
|
||||
getMarketAppealListApi,
|
||||
getMarketPublishRequestListApi,
|
||||
previewMarketGovernanceImpactApi,
|
||||
recallMarketAssetApi,
|
||||
rejectMarketPublishRequestApi,
|
||||
resolveMarketAppealApi,
|
||||
} from '../index';
|
||||
|
||||
const clientMocks = vi.hoisted(() => ({
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('#/api/muse/client', () => ({
|
||||
museAdminApi: {
|
||||
get: clientMocks.get,
|
||||
post: clientMocks.post,
|
||||
},
|
||||
}));
|
||||
|
||||
describe('muse market admin api', () => {
|
||||
it('按 OpenAPI 契约调用市场审核、治理和申诉端点', async () => {
|
||||
await getMarketAssetListApi({ assetType: 'knowledge_base', pageNo: 1 });
|
||||
await getMarketAssetApi('asset-1');
|
||||
await previewMarketGovernanceImpactApi('asset-1', {
|
||||
actionType: 'recall',
|
||||
commandId: 'cmd-preview',
|
||||
scope: 'full_recall',
|
||||
});
|
||||
await delistMarketAssetApi('asset-1', {
|
||||
commandId: 'cmd-delist',
|
||||
expectedStatus: 'listed',
|
||||
impactPreviewId: 'preview-1',
|
||||
reason: '权利声明失效',
|
||||
scope: 'full_delist',
|
||||
});
|
||||
await recallMarketAssetApi('asset-1', {
|
||||
basis: '合规召回',
|
||||
commandId: 'cmd-recall',
|
||||
expectedStatus: 'listed',
|
||||
impactPreviewId: 'preview-2',
|
||||
reason: '安全风险',
|
||||
scope: 'full_recall',
|
||||
});
|
||||
await getMarketPublishRequestListApi({ status: 'pending' });
|
||||
await approveMarketPublishRequestApi('request-1', {
|
||||
commandId: 'cmd-approve',
|
||||
expectedStatus: 'pending',
|
||||
note: '材料完整',
|
||||
validationResultId: 'validation-1',
|
||||
});
|
||||
await rejectMarketPublishRequestApi('request-2', {
|
||||
commandId: 'cmd-reject',
|
||||
expectedStatus: 'reviewing',
|
||||
reason: '隐私材料不足',
|
||||
validationResultId: 'validation-2',
|
||||
});
|
||||
await getMarketAppealListApi({ status: 'reviewing' });
|
||||
await getMarketAppealApi('appeal-1');
|
||||
await resolveMarketAppealApi('appeal-1', {
|
||||
commandId: 'cmd-resolve',
|
||||
expectedStatus: 'reviewing',
|
||||
impactPreviewId: 'preview-3',
|
||||
reason: '维持原处理',
|
||||
resolution: 'maintained',
|
||||
});
|
||||
|
||||
expect(clientMocks.get).toHaveBeenCalledWith('/market/assets', {
|
||||
assetType: 'knowledge_base',
|
||||
pageNo: 1,
|
||||
});
|
||||
expect(clientMocks.get).toHaveBeenCalledWith('/market/assets/asset-1');
|
||||
expect(clientMocks.post).toHaveBeenCalledWith(
|
||||
'/market/assets/asset-1/governance-impact',
|
||||
expect.objectContaining({ actionType: 'recall' }),
|
||||
);
|
||||
expect(clientMocks.post).toHaveBeenCalledWith(
|
||||
'/market/assets/asset-1/delist',
|
||||
expect.objectContaining({ scope: 'full_delist' }),
|
||||
);
|
||||
expect(clientMocks.post).toHaveBeenCalledWith(
|
||||
'/market/assets/asset-1/recall',
|
||||
expect.objectContaining({ basis: '合规召回' }),
|
||||
);
|
||||
expect(clientMocks.get).toHaveBeenCalledWith('/market/publish-requests', {
|
||||
status: 'pending',
|
||||
});
|
||||
expect(clientMocks.post).toHaveBeenCalledWith(
|
||||
'/market/publish-requests/request-1/approve',
|
||||
expect.objectContaining({ validationResultId: 'validation-1' }),
|
||||
);
|
||||
expect(clientMocks.post).toHaveBeenCalledWith(
|
||||
'/market/publish-requests/request-2/reject',
|
||||
expect.objectContaining({ reason: '隐私材料不足' }),
|
||||
);
|
||||
expect(clientMocks.get).toHaveBeenCalledWith('/market/appeals', {
|
||||
status: 'reviewing',
|
||||
});
|
||||
expect(clientMocks.get).toHaveBeenCalledWith('/market/appeals/appeal-1');
|
||||
expect(clientMocks.post).toHaveBeenCalledWith(
|
||||
'/market/appeals/appeal-1/resolve',
|
||||
expect.objectContaining({ resolution: 'maintained' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
541
muse-admin/apps/web-antd/src/api/muse/market/index.ts
Normal file
541
muse-admin/apps/web-antd/src/api/muse/market/index.ts
Normal file
@ -0,0 +1,541 @@
|
||||
import type { PageResult } from '@vben/request';
|
||||
|
||||
import { museAdminApi } from '#/api/muse/client';
|
||||
|
||||
import type { MuseQueryParams } from '#/api/muse/client';
|
||||
|
||||
/** 将强类型查询对象收敛为 Muse client 支持的可序列化查询参数。 */
|
||||
function toMuseQueryParams(params: object): MuseQueryParams {
|
||||
return params as MuseQueryParams;
|
||||
}
|
||||
|
||||
/** 市场资产类型。 */
|
||||
export type MarketAssetType = 'agent' | 'knowledge_base' | 'work';
|
||||
|
||||
/** 市场资产上架生命周期状态。 */
|
||||
export type MarketListingStatus =
|
||||
| 'delisted'
|
||||
| 'listed'
|
||||
| 'not_listed'
|
||||
| 'recalled';
|
||||
|
||||
/** 市场审核状态。 */
|
||||
export type MarketReviewStatus =
|
||||
| 'approved'
|
||||
| 'compliance_blocked'
|
||||
| 'needs_supplement'
|
||||
| 'pending'
|
||||
| 'rejected'
|
||||
| 'reviewing';
|
||||
|
||||
/** 市场来源状态,只描述来源可用性。 */
|
||||
export type MarketSourceStatus =
|
||||
| 'authorization_expired'
|
||||
| 'available'
|
||||
| 'source_delisted'
|
||||
| 'source_recalled'
|
||||
| 'source_revoked'
|
||||
| 'unavailable';
|
||||
|
||||
/** 管理端资产动作策略筛选值。 */
|
||||
export type MarketActionPolicyFilter =
|
||||
| 'normal'
|
||||
| 'stop_generation'
|
||||
| 'stop_new_acquire'
|
||||
| 'stop_new_bind'
|
||||
| 'stop_new_install';
|
||||
|
||||
/** 治理影响预览动作。 */
|
||||
export type MarketGovernanceActionType =
|
||||
| 'delist'
|
||||
| 'recall'
|
||||
| 'revoke_license'
|
||||
| 'stop_generation';
|
||||
|
||||
/** 市场治理范围。 */
|
||||
export type MarketGovernanceScope =
|
||||
| 'full_delist'
|
||||
| 'full_recall'
|
||||
| 'stop_generation'
|
||||
| 'stop_new_acquire'
|
||||
| 'stop_new_bind'
|
||||
| 'stop_new_install';
|
||||
|
||||
/** 申诉类型。 */
|
||||
export type MarketAppealType =
|
||||
| 'delist'
|
||||
| 'license_revocation'
|
||||
| 'recall'
|
||||
| 'review_rejection'
|
||||
| 'usage_impact';
|
||||
|
||||
/** 申诉状态。 */
|
||||
export type MarketAppealStatus =
|
||||
| 'closed'
|
||||
| 'maintained'
|
||||
| 'pending'
|
||||
| 'restored'
|
||||
| 'reviewing'
|
||||
| 'supplementing';
|
||||
|
||||
/** 申诉处理结论。 */
|
||||
export type MarketAppealResolution =
|
||||
| 'closed'
|
||||
| 'maintained'
|
||||
| 'partially_restored'
|
||||
| 'restored';
|
||||
|
||||
/** 通用分页查询参数。 */
|
||||
export interface MuseMarketPageQuery {
|
||||
/** 页码,从 1 开始。 */
|
||||
pageNo?: number;
|
||||
/** 每页条数,上限由后端控制。 */
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
/** 当前治理策略下的动作限制。 */
|
||||
export interface AssetActionPolicyVO {
|
||||
/** 获取授权策略。 */
|
||||
acquirePolicy?: 'allowed' | 'stop_new_acquire';
|
||||
/** 绑定策略。 */
|
||||
bindPolicy?: 'allowed' | 'stop_new_bind';
|
||||
/** 生成使用策略。 */
|
||||
generationPolicy?: 'allowed' | 'stop_generation';
|
||||
/** 安装策略。 */
|
||||
installPolicy?: 'allowed' | 'stop_new_install';
|
||||
/** 需要目标 owner 重验的原因列表。 */
|
||||
recheckReasons?: string[];
|
||||
}
|
||||
|
||||
/** 市场许可信息。 */
|
||||
export interface LicenseInfoVO {
|
||||
/** 允许用途。 */
|
||||
allowedUses: string[];
|
||||
/** 外部订单或授权引用。 */
|
||||
externalOrderRef?: string;
|
||||
/** 许可类型。 */
|
||||
licenseType: string;
|
||||
/** 价格或权益展示文案。 */
|
||||
priceDisplay?: string;
|
||||
/** 禁止用途。 */
|
||||
prohibitedUses: string[];
|
||||
/** 有效期说明。 */
|
||||
validityPeriod?: string;
|
||||
}
|
||||
|
||||
/** 市场资产列表筛选。 */
|
||||
export interface MarketAssetQuery extends MuseMarketPageQuery {
|
||||
/** 动作策略筛选。 */
|
||||
actionPolicy?: MarketActionPolicyFilter;
|
||||
/** 资产类型筛选。 */
|
||||
assetType?: MarketAssetType;
|
||||
/** 资产名称搜索。 */
|
||||
keyword?: string;
|
||||
/** 上架状态筛选。 */
|
||||
listingStatus?: MarketListingStatus;
|
||||
/** 发布者 ID。 */
|
||||
publisherId?: string;
|
||||
}
|
||||
|
||||
/** 管理端市场资产列表项。 */
|
||||
export interface AdminMarketAssetSummaryVO {
|
||||
/** 动作策略。 */
|
||||
actionPolicy?: AssetActionPolicyVO;
|
||||
/** 申诉数量。 */
|
||||
appealCount?: number;
|
||||
/** 市场资产 ID。 */
|
||||
assetId: string;
|
||||
/** 资产类型。 */
|
||||
assetType: MarketAssetType;
|
||||
/** 绑定数量。 */
|
||||
bindCount?: number;
|
||||
/** 安装数量。 */
|
||||
installCount?: number;
|
||||
/** 上架生命周期状态。 */
|
||||
listingStatus: MarketListingStatus;
|
||||
/** 资产名称。 */
|
||||
name: string;
|
||||
/** 发布者 ID。 */
|
||||
publisherId?: string;
|
||||
/** 发布者名称。 */
|
||||
publisherName: string;
|
||||
/** 审核状态。 */
|
||||
reviewStatus: MarketReviewStatus;
|
||||
/** 来源状态。 */
|
||||
sourceStatus?: MarketSourceStatus;
|
||||
/** 资产版本。 */
|
||||
version?: string;
|
||||
}
|
||||
|
||||
/** 治理历史条目。 */
|
||||
export interface AdminGovernanceHistoryItemVO {
|
||||
/** 治理动作。 */
|
||||
action?: string;
|
||||
/** 操作人 ID。 */
|
||||
operatorId?: string;
|
||||
/** 操作时间。 */
|
||||
operatedAt?: string;
|
||||
/** 治理原因。 */
|
||||
reason?: string;
|
||||
/** 治理范围。 */
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
/** 管理端市场资产详情。 */
|
||||
export interface AdminMarketAssetDetailVO
|
||||
extends AdminMarketAssetSummaryVO {
|
||||
/** 受影响任务数量。 */
|
||||
affectedTaskCount?: number;
|
||||
/** 关联申诉摘要。 */
|
||||
appeals?: AdminAppealSummaryVO[];
|
||||
/** 治理历史。 */
|
||||
governanceHistory?: AdminGovernanceHistoryItemVO[];
|
||||
/** 许可信息。 */
|
||||
licenseInfo?: LicenseInfoVO;
|
||||
/** 来源引用摘要,不包含用户私有正文。 */
|
||||
sourceReferenceSummary?: string;
|
||||
}
|
||||
|
||||
/** 受影响数量聚合。 */
|
||||
export interface AffectedCountsVO {
|
||||
/** 授权记录数量。 */
|
||||
authorizationCount?: number;
|
||||
/** 绑定数量。 */
|
||||
bindingCount?: number;
|
||||
/** 安装数量。 */
|
||||
installationCount?: number;
|
||||
/** 知识草稿数量。 */
|
||||
knowledgeDraftCount?: number;
|
||||
/** 运行中任务数量。 */
|
||||
runningTaskCount?: number;
|
||||
/** Shadow 候选数量。 */
|
||||
shadowCandidateCount?: number;
|
||||
}
|
||||
|
||||
/** 治理影响预览请求。 */
|
||||
export interface PreviewMarketGovernanceImpactDTO {
|
||||
/** 拟执行治理动作类型。 */
|
||||
actionType: MarketGovernanceActionType;
|
||||
/** 幂等命令 ID。 */
|
||||
commandId: string;
|
||||
/** 治理范围。 */
|
||||
scope?: Exclude<MarketGovernanceScope, 'full_delist'>;
|
||||
}
|
||||
|
||||
/** 治理影响预览结果。 */
|
||||
export interface AdminGovernanceImpactPreviewVO {
|
||||
/** 受影响数量聚合。 */
|
||||
affectedCounts: AffectedCountsVO;
|
||||
/** 可替代资产建议。 */
|
||||
alternativeAssets?: string[];
|
||||
/** 治理动作类型。 */
|
||||
actionType: MarketGovernanceActionType;
|
||||
/** 通知范围说明。 */
|
||||
notificationScope?: string;
|
||||
/** 预览 ID。 */
|
||||
previewId: string;
|
||||
/** 治理范围。 */
|
||||
scope: string;
|
||||
/** 来源引用摘要。 */
|
||||
sourceReferenceSummary?: string;
|
||||
}
|
||||
|
||||
/** 下架市场资产请求。 */
|
||||
export interface DelistMarketAssetDTO {
|
||||
/** 幂等命令 ID。 */
|
||||
commandId: string;
|
||||
/** 期望当前状态,避免盲下架。 */
|
||||
expectedStatus: 'listed';
|
||||
/** 影响预览 ID。 */
|
||||
impactPreviewId: string;
|
||||
/** 下架原因。 */
|
||||
reason: string;
|
||||
/** 下架范围。 */
|
||||
scope: Exclude<MarketGovernanceScope, 'full_recall'>;
|
||||
}
|
||||
|
||||
/** 召回市场资产请求。 */
|
||||
export interface RecallMarketAssetDTO {
|
||||
/** 召回依据。 */
|
||||
basis: string;
|
||||
/** 幂等命令 ID。 */
|
||||
commandId: string;
|
||||
/** 是否启动法务保全限制。 */
|
||||
exportPreservation?: boolean;
|
||||
/** 期望当前状态,避免盲召回。 */
|
||||
expectedStatus: 'delisted' | 'listed';
|
||||
/** 影响预览 ID。 */
|
||||
impactPreviewId: string;
|
||||
/** 召回原因。 */
|
||||
reason: string;
|
||||
/** 召回范围。 */
|
||||
scope: 'full_recall' | 'stop_generation';
|
||||
}
|
||||
|
||||
/** 发布申请列表筛选。 */
|
||||
export interface MarketPublishRequestQuery extends MuseMarketPageQuery {
|
||||
/** 资产类型筛选。 */
|
||||
assetType?: MarketAssetType;
|
||||
/** 发布者 ID。 */
|
||||
publisherId?: string;
|
||||
/** 风险标签筛选。 */
|
||||
riskTag?: string;
|
||||
/** 审核状态筛选。 */
|
||||
status?: MarketReviewStatus;
|
||||
}
|
||||
|
||||
/** 管理端发布申请摘要。 */
|
||||
export interface AdminPublishRequestSummaryVO {
|
||||
/** 市场资产 ID。 */
|
||||
assetId: string;
|
||||
/** 资产名称。 */
|
||||
assetName: string;
|
||||
/** 资产类型。 */
|
||||
assetType: MarketAssetType;
|
||||
/** 隐私/密钥检查结果。 */
|
||||
privacyCheckResult?: string;
|
||||
/** 发布者 ID。 */
|
||||
publisherId?: string;
|
||||
/** 发布者名称。 */
|
||||
publisherName?: string;
|
||||
/** 发布申请 ID。 */
|
||||
requestId: string;
|
||||
/** 风险标签。 */
|
||||
riskTags?: string[];
|
||||
/** 权利声明摘要。 */
|
||||
rightsDeclaration?: string;
|
||||
/** 提交时间。 */
|
||||
submittedAt?: string;
|
||||
/** 审核状态。 */
|
||||
status: MarketReviewStatus;
|
||||
/** 资产版本。 */
|
||||
version?: string;
|
||||
}
|
||||
|
||||
/** 审核通过发布申请请求。 */
|
||||
export interface ApproveMarketPublishRequestDTO {
|
||||
/** 幂等命令 ID。 */
|
||||
commandId: string;
|
||||
/** 审核证据标签。 */
|
||||
evidenceTags?: string[];
|
||||
/** 期望当前审核状态。 */
|
||||
expectedStatus: 'needs_supplement' | 'pending' | 'reviewing';
|
||||
/** 审核通过说明。 */
|
||||
note: string;
|
||||
/** 审核校验结果引用 ID。 */
|
||||
validationResultId: string;
|
||||
}
|
||||
|
||||
/** 驳回发布申请请求。 */
|
||||
export interface RejectMarketPublishRequestDTO {
|
||||
/** 幂等命令 ID。 */
|
||||
commandId: string;
|
||||
/** 审核证据标签。 */
|
||||
evidenceTags?: string[];
|
||||
/** 期望当前审核状态。 */
|
||||
expectedStatus: 'needs_supplement' | 'pending' | 'reviewing';
|
||||
/** 驳回理由。 */
|
||||
reason: string;
|
||||
/** 是否要求发布者补充材料后重新提交。 */
|
||||
requireSupplement?: boolean;
|
||||
/** 需要补充的项目列表。 */
|
||||
supplementItems?: string[];
|
||||
/** 审核校验结果引用 ID。 */
|
||||
validationResultId: string;
|
||||
}
|
||||
|
||||
/** 申诉列表筛选。 */
|
||||
export interface MarketAppealQuery extends MuseMarketPageQuery {
|
||||
/** 申诉人 ID。 */
|
||||
appellantId?: string;
|
||||
/** 申诉类型筛选。 */
|
||||
appealType?: MarketAppealType;
|
||||
/** 关联资产 ID。 */
|
||||
assetId?: string;
|
||||
/** 申诉状态筛选。 */
|
||||
status?: MarketAppealStatus;
|
||||
}
|
||||
|
||||
/** 管理端申诉摘要。 */
|
||||
export interface AdminAppealSummaryVO {
|
||||
/** 申诉 ID。 */
|
||||
appealId: string;
|
||||
/** 申诉类型。 */
|
||||
appealType: MarketAppealType;
|
||||
/** 申诉人 ID。 */
|
||||
appellantId?: string;
|
||||
/** 申诉人名称。 */
|
||||
appellantName?: string;
|
||||
/** 资产 ID。 */
|
||||
assetId: string;
|
||||
/** 资产名称。 */
|
||||
assetName?: string;
|
||||
/** 处理人 ID。 */
|
||||
processorId?: string;
|
||||
/** 申诉状态。 */
|
||||
status: MarketAppealStatus;
|
||||
/** 提交时间。 */
|
||||
submittedAt?: string;
|
||||
}
|
||||
|
||||
/** 申诉处理历史条目。 */
|
||||
export interface AppealProcessingHistoryItemVO {
|
||||
/** 处理动作。 */
|
||||
action?: string;
|
||||
/** 处理人 ID。 */
|
||||
processorId?: string;
|
||||
/** 处理时间。 */
|
||||
processedAt?: string;
|
||||
/** 处理原因。 */
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/** 申诉补充材料记录。 */
|
||||
export interface AppealSupplementRecordVO {
|
||||
/** 附件 ID 列表。 */
|
||||
attachmentIds?: string[];
|
||||
/** 补充说明。 */
|
||||
description?: string;
|
||||
/** 提交时间。 */
|
||||
submittedAt?: string;
|
||||
/** 提交人 ID。 */
|
||||
submittedBy?: string;
|
||||
/** 补充材料 ID。 */
|
||||
supplementId?: string;
|
||||
}
|
||||
|
||||
/** 管理端申诉详情。 */
|
||||
export interface AdminAppealDetailVO extends AdminAppealSummaryVO {
|
||||
/** 资产版本。 */
|
||||
assetVersion?: string;
|
||||
/** 当前治理状态。 */
|
||||
currentGovernanceStatus?: string;
|
||||
/** 证据材料标识列表。 */
|
||||
evidenceMaterials?: string[];
|
||||
/** 原审核、下架或召回结论。 */
|
||||
originalResult?: string;
|
||||
/** 处理历史。 */
|
||||
processingHistory?: AppealProcessingHistoryItemVO[];
|
||||
/** 申诉理由。 */
|
||||
reason: string;
|
||||
/** 补充材料记录。 */
|
||||
supplementRecords?: AppealSupplementRecordVO[];
|
||||
}
|
||||
|
||||
/** 处理市场申诉请求。 */
|
||||
export interface ResolveMarketAppealDTO {
|
||||
/** 幂等命令 ID。 */
|
||||
commandId: string;
|
||||
/** 处理证据标签。 */
|
||||
evidenceTags?: string[];
|
||||
/** 期望当前申诉状态。 */
|
||||
expectedStatus: 'pending' | 'reviewing' | 'supplementing';
|
||||
/** 恢复或关闭时关联的影响预览 ID。 */
|
||||
impactPreviewId: string;
|
||||
/** 处理理由。 */
|
||||
reason: string;
|
||||
/** 处理结论。 */
|
||||
resolution: MarketAppealResolution;
|
||||
/** 是否要求申诉人补充材料。 */
|
||||
requireSupplement?: boolean;
|
||||
/** 需要补充的项目。 */
|
||||
supplementItems?: string[];
|
||||
}
|
||||
|
||||
/** 查询市场资产分页列表。 */
|
||||
export function getMarketAssetListApi(params: MarketAssetQuery) {
|
||||
return museAdminApi.get<PageResult<AdminMarketAssetSummaryVO>>(
|
||||
'/market/assets',
|
||||
toMuseQueryParams(params),
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询市场资产治理详情。 */
|
||||
export function getMarketAssetApi(assetId: string) {
|
||||
return museAdminApi.get<AdminMarketAssetDetailVO>(
|
||||
`/market/assets/${assetId}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 预览市场资产治理影响。 */
|
||||
export function previewMarketGovernanceImpactApi(
|
||||
assetId: string,
|
||||
data: PreviewMarketGovernanceImpactDTO,
|
||||
) {
|
||||
return museAdminApi.post<AdminGovernanceImpactPreviewVO>(
|
||||
`/market/assets/${assetId}/governance-impact`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 下架市场资产。 */
|
||||
export function delistMarketAssetApi(
|
||||
assetId: string,
|
||||
data: DelistMarketAssetDTO,
|
||||
) {
|
||||
return museAdminApi.post<void>(`/market/assets/${assetId}/delist`, data);
|
||||
}
|
||||
|
||||
/** 召回市场资产。 */
|
||||
export function recallMarketAssetApi(
|
||||
assetId: string,
|
||||
data: RecallMarketAssetDTO,
|
||||
) {
|
||||
return museAdminApi.post<void>(`/market/assets/${assetId}/recall`, data);
|
||||
}
|
||||
|
||||
/** 查询发布申请审核队列。 */
|
||||
export function getMarketPublishRequestListApi(
|
||||
params: MarketPublishRequestQuery,
|
||||
) {
|
||||
return museAdminApi.get<PageResult<AdminPublishRequestSummaryVO>>(
|
||||
'/market/publish-requests',
|
||||
toMuseQueryParams(params),
|
||||
);
|
||||
}
|
||||
|
||||
/** 审核通过发布申请。 */
|
||||
export function approveMarketPublishRequestApi(
|
||||
requestId: string,
|
||||
data: ApproveMarketPublishRequestDTO,
|
||||
) {
|
||||
return museAdminApi.post<void>(
|
||||
`/market/publish-requests/${requestId}/approve`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 驳回发布申请。 */
|
||||
export function rejectMarketPublishRequestApi(
|
||||
requestId: string,
|
||||
data: RejectMarketPublishRequestDTO,
|
||||
) {
|
||||
return museAdminApi.post<void>(
|
||||
`/market/publish-requests/${requestId}/reject`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询市场申诉列表。 */
|
||||
export function getMarketAppealListApi(params: MarketAppealQuery) {
|
||||
return museAdminApi.get<PageResult<AdminAppealSummaryVO>>(
|
||||
'/market/appeals',
|
||||
toMuseQueryParams(params),
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询市场申诉详情。 */
|
||||
export function getMarketAppealApi(appealId: string) {
|
||||
return museAdminApi.get<AdminAppealDetailVO>(`/market/appeals/${appealId}`);
|
||||
}
|
||||
|
||||
/** 处理市场申诉。 */
|
||||
export function resolveMarketAppealApi(
|
||||
appealId: string,
|
||||
data: ResolveMarketAppealDTO,
|
||||
) {
|
||||
return museAdminApi.post<void>(
|
||||
`/market/appeals/${appealId}/resolve`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
createNewApiCallAttributionJobApi,
|
||||
createNewApiGatewayBindingApi,
|
||||
createNewApiQuotaRequestApi,
|
||||
getNewApiIntegrationCallApi,
|
||||
listNewApiBalanceSnapshotsApi,
|
||||
listNewApiGatewayUsersApi,
|
||||
listNewApiUsageRecordsApi,
|
||||
} from '../index';
|
||||
|
||||
const { museAdminApiMock } = vi.hoisted(() => ({
|
||||
museAdminApiMock: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('#/api/muse/client', () => ({
|
||||
museAdminApi: museAdminApiMock,
|
||||
}));
|
||||
|
||||
describe('muse newapi api', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('只通过账户契约查询网关绑定、余额和用量摘要', async () => {
|
||||
await listNewApiGatewayUsersApi({
|
||||
bindingStatus: 'sync_failed',
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
await listNewApiBalanceSnapshotsApi('u1', { pageNo: 1, pageSize: 10 });
|
||||
await listNewApiUsageRecordsApi({
|
||||
attributionStatus: 'pending',
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/account/new-api-bindings',
|
||||
{ bindingStatus: 'sync_failed', pageNo: 1, pageSize: 20 },
|
||||
);
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/account/users/u1/balance-snapshots',
|
||||
{ pageNo: 1, pageSize: 10 },
|
||||
);
|
||||
expect(museAdminApiMock.get).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'/account/usage-records',
|
||||
{ attributionStatus: 'pending', pageNo: 1, pageSize: 20 },
|
||||
);
|
||||
});
|
||||
|
||||
it('提交网关绑定、额度请求和调用归属命令', async () => {
|
||||
await createNewApiGatewayBindingApi('u1', {
|
||||
commandId: 'cmd-bind',
|
||||
forceRefresh: true,
|
||||
reason: '同步失败重试',
|
||||
});
|
||||
await createNewApiQuotaRequestApi('u1', {
|
||||
commandId: 'cmd-quota',
|
||||
requestType: 'subscription_sync',
|
||||
reason: '套餐对账',
|
||||
});
|
||||
await createNewApiCallAttributionJobApi({
|
||||
commandId: 'cmd-attr',
|
||||
correlationId: 'corr-1',
|
||||
verificationMode: 'strict',
|
||||
});
|
||||
await getNewApiIntegrationCallApi('corr/1');
|
||||
|
||||
expect(museAdminApiMock.post).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/account/users/u1/new-api-binding',
|
||||
expect.objectContaining({ commandId: 'cmd-bind' }),
|
||||
);
|
||||
expect(museAdminApiMock.post).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/account/users/u1/quota-requests',
|
||||
expect.objectContaining({ commandId: 'cmd-quota' }),
|
||||
);
|
||||
expect(museAdminApiMock.post).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'/account/call-attribution-jobs',
|
||||
expect.objectContaining({ commandId: 'cmd-attr' }),
|
||||
);
|
||||
expect(museAdminApiMock.get).toHaveBeenCalledWith(
|
||||
'/account/integration-calls/by-correlation/corr%2F1',
|
||||
);
|
||||
});
|
||||
});
|
||||
35
muse-admin/apps/web-antd/src/api/muse/newapi/index.ts
Normal file
35
muse-admin/apps/web-antd/src/api/muse/newapi/index.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* New-API 网关治理 API
|
||||
*
|
||||
* New-API 的管理端能力由账户域契约承接。本文件提供页面语义化别名,
|
||||
* 避免 New-API 页面直接理解账户域内部命名,同时不新增不存在的后端路径。
|
||||
*/
|
||||
|
||||
export type {
|
||||
AdminQuotaRequestCreate,
|
||||
AdminUsageRecord,
|
||||
BalanceSnapshotEntry,
|
||||
CallAttributionJobCreate,
|
||||
CallAttributionJobDetail,
|
||||
CallAttributionJobResult,
|
||||
IntegrationCallDetail,
|
||||
MuseAdminId,
|
||||
MuseAdminPageQuery,
|
||||
NewApiBindingCreateRequest,
|
||||
NewApiBindingQuery,
|
||||
NewApiBindingResult,
|
||||
NewApiBindingStatus,
|
||||
NewApiBindingSummary,
|
||||
QuotaRequestResult,
|
||||
UsageRecordQuery,
|
||||
} from '../account';
|
||||
|
||||
export {
|
||||
createCallAttributionJobApi as createNewApiCallAttributionJobApi,
|
||||
createNewApiBindingApi as createNewApiGatewayBindingApi,
|
||||
createQuotaRequestApi as createNewApiQuotaRequestApi,
|
||||
getBalanceSnapshotsApi as listNewApiBalanceSnapshotsApi,
|
||||
getIntegrationCallByCorrelationApi as getNewApiIntegrationCallApi,
|
||||
listNewApiBindingsApi as listNewApiGatewayUsersApi,
|
||||
listUsageRecordsApi as listNewApiUsageRecordsApi,
|
||||
} from '../account';
|
||||
2957
muse-admin/apps/web-antd/src/api/types/account.ts
Normal file
2957
muse-admin/apps/web-antd/src/api/types/account.ts
Normal file
File diff suppressed because it is too large
Load Diff
3350
muse-admin/apps/web-antd/src/api/types/ai.ts
Normal file
3350
muse-admin/apps/web-antd/src/api/types/ai.ts
Normal file
File diff suppressed because it is too large
Load Diff
140
muse-admin/apps/web-antd/src/api/types/base.ts
Normal file
140
muse-admin/apps/web-antd/src/api/types/base.ts
Normal file
@ -0,0 +1,140 @@
|
||||
/**
|
||||
* This file was auto-generated by openapi-typescript.
|
||||
* Do not make direct changes to the file.
|
||||
*/
|
||||
|
||||
export type paths = Record<string, never>;
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
schemas: {
|
||||
CommonResult: {
|
||||
/**
|
||||
* @description 业务状态码,0 表示成功
|
||||
* @example 0
|
||||
*/
|
||||
code: number;
|
||||
/** @description 响应数据,类型视具体接口而定 */
|
||||
data?: unknown;
|
||||
/**
|
||||
* @description 提示信息
|
||||
* @example success
|
||||
*/
|
||||
msg: string;
|
||||
};
|
||||
PaginatedResult: {
|
||||
/**
|
||||
* @description 总记录数
|
||||
* @example 150
|
||||
*/
|
||||
total: number;
|
||||
/**
|
||||
* @description 当前页码
|
||||
* @example 1
|
||||
*/
|
||||
pageNo: number;
|
||||
/**
|
||||
* @description 每页条数
|
||||
* @example 20
|
||||
*/
|
||||
pageSize: number;
|
||||
/** @description 当前页数据列表 */
|
||||
list: unknown[];
|
||||
};
|
||||
ErrorResponse: {
|
||||
/**
|
||||
* @description 错误码格式: {system}-{module}-{category}-{sequence}
|
||||
* 示例: MUSE-CONTENT-001-0001
|
||||
* @example MUSE-CONTENT-001-0001
|
||||
*/
|
||||
code: string;
|
||||
/** @description 人类可读的错误描述 */
|
||||
msg: string;
|
||||
/** @description 详细错误信息(仅开发环境返回) */
|
||||
detail?: string;
|
||||
};
|
||||
TimestampMixin: {
|
||||
/**
|
||||
* Format: date-time
|
||||
* @description 创建时间
|
||||
*/
|
||||
createdAt?: string;
|
||||
/**
|
||||
* Format: date-time
|
||||
* @description 最后更新时间
|
||||
*/
|
||||
updatedAt?: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description 请求参数有误 */
|
||||
BadRequest: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description 未认证或 token 过期 */
|
||||
Unauthorized: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description 无权限 */
|
||||
Forbidden: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description 资源不存在 */
|
||||
NotFound: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description 资源冲突 */
|
||||
Conflict: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description 服务器内部错误 */
|
||||
InternalError: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
parameters: {
|
||||
/**
|
||||
* @description API 版本号,当前版本为 1。
|
||||
* 未传时网关可兼容最新稳定版本,但前端 SDK 和 API 客户端必须显式传递该 header。
|
||||
*/
|
||||
XApiVersion: "1";
|
||||
/** @description 页码,从 1 开始 */
|
||||
pageNo: number;
|
||||
/** @description 每页条数,上限 100 */
|
||||
pageSize: number;
|
||||
};
|
||||
requestBodies: never;
|
||||
headers: never;
|
||||
pathItems: never;
|
||||
}
|
||||
export type $defs = Record<string, never>;
|
||||
export type operations = Record<string, never>;
|
||||
3898
muse-admin/apps/web-antd/src/api/types/content.ts
Normal file
3898
muse-admin/apps/web-antd/src/api/types/content.ts
Normal file
File diff suppressed because it is too large
Load Diff
11
muse-admin/apps/web-antd/src/api/types/index.ts
Normal file
11
muse-admin/apps/web-antd/src/api/types/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
// Muse API TypeScript 类型包入口
|
||||
// 从 OpenAPI 3.0 契约自动生成,请勿手动编辑
|
||||
// 生成命令: npx openapi-typescript <yaml> --output <target>
|
||||
|
||||
export type * as Base from './base';
|
||||
export type * as Content from './content';
|
||||
export type * as AI from './ai';
|
||||
export type * as Knowledge from './knowledge';
|
||||
export type * as Market from './market';
|
||||
export type * as Account from './account';
|
||||
export type * as Meta from './meta';
|
||||
4061
muse-admin/apps/web-antd/src/api/types/knowledge.ts
Normal file
4061
muse-admin/apps/web-antd/src/api/types/knowledge.ts
Normal file
File diff suppressed because it is too large
Load Diff
2970
muse-admin/apps/web-antd/src/api/types/market.ts
Normal file
2970
muse-admin/apps/web-antd/src/api/types/market.ts
Normal file
File diff suppressed because it is too large
Load Diff
1560
muse-admin/apps/web-antd/src/api/types/meta.ts
Normal file
1560
muse-admin/apps/web-antd/src/api/types/meta.ts
Normal file
File diff suppressed because it is too large
Load Diff
117
muse-admin/apps/web-antd/src/router/routes/modules/muse.ts
Normal file
117
muse-admin/apps/web-antd/src/router/routes/modules/muse.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
/**
|
||||
* Muse 管理端业务路由
|
||||
*
|
||||
* 这里仅注册管理端 surface。真实权限由后端菜单、页面权限和操作权限控制,
|
||||
* 前端路由只负责把 Vben Admin 的页面组件接入导航。
|
||||
*/
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
icon: 'lucide:shield-check',
|
||||
order: 20,
|
||||
title: 'Muse 管理',
|
||||
},
|
||||
name: 'MuseAdmin',
|
||||
path: '/muse',
|
||||
children: [
|
||||
{
|
||||
component: () =>
|
||||
import('#/views/muse/governance/meta-schema/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:braces',
|
||||
title: '元结构定义',
|
||||
},
|
||||
name: 'MuseMetaSchemaList',
|
||||
path: 'governance/meta-schemas',
|
||||
},
|
||||
{
|
||||
component: () =>
|
||||
import('#/views/muse/governance/meta-schema/detail.vue'),
|
||||
meta: {
|
||||
activePath: '/muse/governance/meta-schemas',
|
||||
hideInMenu: true,
|
||||
title: '元结构编辑',
|
||||
},
|
||||
name: 'MuseMetaSchemaDetail',
|
||||
path: 'governance/meta-schemas/:schemaKey',
|
||||
},
|
||||
{
|
||||
component: () =>
|
||||
import('#/views/muse/governance/function-orch/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:git-branch',
|
||||
title: '功能编排',
|
||||
},
|
||||
name: 'MuseFunctionOrchestration',
|
||||
path: 'governance/function-orchestration',
|
||||
},
|
||||
{
|
||||
component: () => import('#/views/muse/ai/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:bot',
|
||||
title: 'AI 配置',
|
||||
},
|
||||
name: 'MuseAiConfig',
|
||||
path: 'ai',
|
||||
},
|
||||
{
|
||||
component: () => import('#/views/muse/knowledge/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:library',
|
||||
title: '全局知识库',
|
||||
},
|
||||
name: 'MuseGlobalKnowledge',
|
||||
path: 'knowledge',
|
||||
},
|
||||
{
|
||||
component: () => import('#/views/muse/market/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:store',
|
||||
title: '市场治理',
|
||||
},
|
||||
name: 'MuseMarketGovernance',
|
||||
path: 'market',
|
||||
},
|
||||
{
|
||||
component: () => import('#/views/muse/account/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:users',
|
||||
title: '用户与权限',
|
||||
},
|
||||
name: 'MuseAccountGovernance',
|
||||
path: 'account',
|
||||
},
|
||||
{
|
||||
component: () => import('#/views/muse/newapi/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:activity',
|
||||
title: 'New-API 与用量',
|
||||
},
|
||||
name: 'MuseNewApiUsage',
|
||||
path: 'new-api',
|
||||
},
|
||||
{
|
||||
component: () => import('#/views/muse/jobs/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:timer',
|
||||
title: '任务治理',
|
||||
},
|
||||
name: 'MuseTaskOps',
|
||||
path: 'jobs',
|
||||
},
|
||||
{
|
||||
component: () => import('#/views/muse/audit/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:scroll-text',
|
||||
title: '日志与审计',
|
||||
},
|
||||
name: 'MuseAuditLog',
|
||||
path: 'audit',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
844
muse-admin/apps/web-antd/src/views/muse/account/index.vue
Normal file
844
muse-admin/apps/web-antd/src/views/muse/account/index.vue
Normal file
@ -0,0 +1,844 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 用户、角色与权限 / 权益治理聚合页
|
||||
*
|
||||
* 管理员在此查看账号治理摘要、权益配额、用量和购买脱敏记录。
|
||||
* 账号封禁、角色和权限组变更的后端写契约尚未出现在 account openapi 中,
|
||||
* 页面仅提供二次确认、原因、复核和审计字段承载,不伪造未定义的写接口。
|
||||
*/
|
||||
import type {
|
||||
AdminAccountUserSummary,
|
||||
AdminPurchaseRecord,
|
||||
AdminUsageRecord,
|
||||
AdminUserEntitlementDetail,
|
||||
AccountStatus,
|
||||
QuotaAdjustmentLedgerEntry,
|
||||
QuotaStatus,
|
||||
} from '#/api/muse/account';
|
||||
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Descriptions,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
createQuotaAdjustmentApi,
|
||||
getUserEntitlementsApi,
|
||||
listAccountUsersApi,
|
||||
listPurchaseRecordsApi,
|
||||
listQuotaAdjustmentsApi,
|
||||
listUsageRecordsApi,
|
||||
} from '#/api/muse/account';
|
||||
|
||||
/** 账号治理动作类型。 */
|
||||
type GovernanceAction =
|
||||
| 'assign_group'
|
||||
| 'grant_role'
|
||||
| 'restrict'
|
||||
| 'revoke_role'
|
||||
| 'review'
|
||||
| 'suspend';
|
||||
|
||||
/** 表格分页对象,避免依赖 Ant 内部类型。 */
|
||||
interface TablePager {
|
||||
/** 当前页码。 */
|
||||
current?: number;
|
||||
/** 每页数量。 */
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
/** Ant Table 插槽传入的通用记录。 */
|
||||
type TableRecord = Record<string, unknown>;
|
||||
|
||||
/** 账号治理确认表单。 */
|
||||
interface GovernanceCommandForm {
|
||||
/** 目标用户 ID。 */
|
||||
userId: string;
|
||||
/** 动作类型。 */
|
||||
action: GovernanceAction;
|
||||
/** 管理员操作原因。 */
|
||||
reason: string;
|
||||
/** 审批或复核单号。 */
|
||||
approvalRef: string;
|
||||
/** 角色标识,逗号分隔。 */
|
||||
roleKeys: string;
|
||||
/** 权限组标识,逗号分隔。 */
|
||||
permissionGroups: string;
|
||||
/** 是否需要复核。 */
|
||||
reviewRequired: boolean;
|
||||
/** 高危动作确认文本。 */
|
||||
confirmText: string;
|
||||
}
|
||||
|
||||
/** 配额调整表单。 */
|
||||
interface QuotaAdjustmentForm {
|
||||
/** 目标用户 ID。 */
|
||||
userId: string;
|
||||
/** 权益资源类型。 */
|
||||
resourceType: string;
|
||||
/** 调整增量。 */
|
||||
delta?: number;
|
||||
/** 调整前额度上限。 */
|
||||
beforeLimit?: number;
|
||||
/** 调整前已用额度。 */
|
||||
beforeUsed?: number;
|
||||
/** 调整前剩余额度。 */
|
||||
beforeRemaining?: number;
|
||||
/** 调整原因。 */
|
||||
reason: string;
|
||||
/** 审批或复核引用。 */
|
||||
approvalRef: string;
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
}
|
||||
|
||||
/** 账号状态标签颜色。 */
|
||||
const accountStatusColor: Record<AccountStatus, string> = {
|
||||
active: 'green',
|
||||
restricted: 'orange',
|
||||
suspended: 'red',
|
||||
};
|
||||
|
||||
/** 配额状态标签颜色。 */
|
||||
const quotaStatusColor: Record<AdminAccountUserSummary['quotaStatus'], string> =
|
||||
{
|
||||
expired: 'red',
|
||||
exhausted: 'red',
|
||||
low: 'orange',
|
||||
normal: 'green',
|
||||
};
|
||||
|
||||
/** 账号状态中文文案。 */
|
||||
const accountStatusText: Record<AccountStatus, string> = {
|
||||
active: '正常',
|
||||
restricted: '受限',
|
||||
suspended: '封禁',
|
||||
};
|
||||
|
||||
/** 配额状态中文文案。 */
|
||||
const quotaStatusText: Record<AdminAccountUserSummary['quotaStatus'], string> = {
|
||||
expired: '过期',
|
||||
exhausted: '耗尽',
|
||||
low: '低余量',
|
||||
normal: '正常',
|
||||
};
|
||||
|
||||
/** 将 Ant Table 通用记录转回账户摘要类型。 */
|
||||
function asAccountUser(record: TableRecord) {
|
||||
return record as unknown as AdminAccountUserSummary;
|
||||
}
|
||||
|
||||
/** 账号状态颜色,兼容模板中的 unknown 记录。 */
|
||||
function getAccountStatusColor(status: unknown) {
|
||||
return accountStatusColor[status as AccountStatus] ?? 'default';
|
||||
}
|
||||
|
||||
/** 账号状态文案,兼容模板中的 unknown 记录。 */
|
||||
function getAccountStatusText(status: unknown) {
|
||||
return accountStatusText[status as AccountStatus] ?? displayText(String(status));
|
||||
}
|
||||
|
||||
/** 配额状态颜色,兼容模板中的 unknown 记录。 */
|
||||
function getQuotaStatusColor(status: unknown) {
|
||||
return quotaStatusColor[status as QuotaStatus] ?? 'default';
|
||||
}
|
||||
|
||||
/** 配额状态文案,兼容模板中的 unknown 记录。 */
|
||||
function getQuotaStatusText(status: unknown) {
|
||||
return quotaStatusText[status as QuotaStatus] ?? displayText(String(status));
|
||||
}
|
||||
|
||||
/** 用户表格列定义。 */
|
||||
const userColumns = [
|
||||
{ dataIndex: 'userId', key: 'userId', title: '用户 ID', width: 180 },
|
||||
{ dataIndex: 'nickname', key: 'nickname', title: '昵称', width: 160 },
|
||||
{ dataIndex: 'status', key: 'status', title: '账号状态', width: 110 },
|
||||
{ dataIndex: 'entitlementSource', key: 'entitlementSource', title: '权益来源' },
|
||||
{ dataIndex: 'quotaStatus', key: 'quotaStatus', title: '配额状态', width: 110 },
|
||||
{
|
||||
dataIndex: 'newApiBindingStatus',
|
||||
key: 'newApiBindingStatus',
|
||||
title: '网关绑定',
|
||||
width: 120,
|
||||
},
|
||||
{ dataIndex: 'riskFlags', key: 'riskFlags', title: '风险标记' },
|
||||
{ dataIndex: 'lastActiveAt', key: 'lastActiveAt', title: '最近活跃' },
|
||||
{ key: 'actions', title: '治理动作', width: 320 },
|
||||
];
|
||||
|
||||
/** 权益表格列定义。 */
|
||||
const entitlementColumns = [
|
||||
{ dataIndex: 'resourceType', key: 'resourceType', title: '资源类型' },
|
||||
{ dataIndex: 'limit', key: 'limit', title: '上限' },
|
||||
{ dataIndex: 'used', key: 'used', title: '已用' },
|
||||
{ dataIndex: 'remaining', key: 'remaining', title: '剩余' },
|
||||
{ dataIndex: 'source', key: 'source', title: '来源' },
|
||||
{ dataIndex: 'expiresAt', key: 'expiresAt', title: '到期时间' },
|
||||
];
|
||||
|
||||
/** 配额调整 ledger 表格列定义。 */
|
||||
const quotaLedgerColumns = [
|
||||
{ dataIndex: 'adjustmentId', key: 'adjustmentId', title: '调整 ID' },
|
||||
{ dataIndex: 'sourceType', key: 'sourceType', title: '来源' },
|
||||
{ dataIndex: 'resourceType', key: 'resourceType', title: '资源类型' },
|
||||
{ dataIndex: 'delta', key: 'delta', title: '调整量' },
|
||||
{ dataIndex: 'reason', key: 'reason', title: '原因' },
|
||||
{ dataIndex: 'operatorSummary', key: 'operatorSummary', title: '操作者' },
|
||||
{ dataIndex: 'auditStatus', key: 'auditStatus', title: '审计' },
|
||||
{ dataIndex: 'createdAt', key: 'createdAt', title: '调整时间' },
|
||||
];
|
||||
|
||||
/** 用量摘要表格列定义。 */
|
||||
const usageColumns = [
|
||||
{ dataIndex: 'recordId', key: 'recordId', title: '记录 ID' },
|
||||
{ dataIndex: 'userId', key: 'userId', title: '用户 ID' },
|
||||
{ dataIndex: 'nickname', key: 'nickname', title: '昵称' },
|
||||
{ dataIndex: 'period', key: 'period', title: '周期' },
|
||||
{ dataIndex: 'totalTokens', key: 'totalTokens', title: '总 Token' },
|
||||
{
|
||||
dataIndex: 'pendingAttributionCount',
|
||||
key: 'pendingAttributionCount',
|
||||
title: '待归属',
|
||||
},
|
||||
{
|
||||
dataIndex: 'failedAttributionCount',
|
||||
key: 'failedAttributionCount',
|
||||
title: '归属失败',
|
||||
},
|
||||
{ dataIndex: 'attributionStatus', key: 'attributionStatus', title: '归属状态' },
|
||||
];
|
||||
|
||||
/** 购买记录表格列定义。 */
|
||||
const purchaseColumns = [
|
||||
{ dataIndex: 'recordId', key: 'recordId', title: '记录 ID' },
|
||||
{ dataIndex: 'userId', key: 'userId', title: '用户 ID' },
|
||||
{ dataIndex: 'assetType', key: 'assetType', title: '资产类型' },
|
||||
{ dataIndex: 'assetName', key: 'assetName', title: '资产摘要' },
|
||||
{ dataIndex: 'purchaseType', key: 'purchaseType', title: '购买类型' },
|
||||
{ dataIndex: 'amount', key: 'amount', title: '金额' },
|
||||
{ dataIndex: 'status', key: 'status', title: '状态' },
|
||||
{ dataIndex: 'externalOrderRef', key: 'externalOrderRef', title: '外部订单' },
|
||||
{ dataIndex: 'createdAt', key: 'createdAt', title: '时间' },
|
||||
];
|
||||
|
||||
const userLoading = ref(false);
|
||||
const entitlementLoading = ref(false);
|
||||
const usageLoading = ref(false);
|
||||
const purchaseLoading = ref(false);
|
||||
const quotaSubmitting = ref(false);
|
||||
|
||||
const users = ref<AdminAccountUserSummary[]>([]);
|
||||
const selectedUser = ref<AdminAccountUserSummary>();
|
||||
const selectedEntitlement = ref<AdminUserEntitlementDetail>();
|
||||
const quotaLedger = ref<QuotaAdjustmentLedgerEntry[]>([]);
|
||||
const usageRecords = ref<AdminUsageRecord[]>([]);
|
||||
const purchaseRecords = ref<AdminPurchaseRecord[]>([]);
|
||||
|
||||
/** 用户列表筛选条件。 */
|
||||
const userFilters = reactive({
|
||||
keyword: '',
|
||||
status: undefined as AdminAccountUserSummary['status'] | undefined,
|
||||
});
|
||||
|
||||
/** 用量和购买记录筛选条件。 */
|
||||
const recordFilters = reactive({
|
||||
userId: '',
|
||||
});
|
||||
|
||||
/** 用户列表分页状态。 */
|
||||
const userPager = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
/** 用量列表分页状态。 */
|
||||
const usagePager = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
/** 购买列表分页状态。 */
|
||||
const purchasePager = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const governanceModalOpen = ref(false);
|
||||
const quotaModalOpen = ref(false);
|
||||
|
||||
const governanceForm = reactive<GovernanceCommandForm>({
|
||||
action: 'suspend',
|
||||
approvalRef: '',
|
||||
confirmText: '',
|
||||
permissionGroups: '',
|
||||
reason: '',
|
||||
reviewRequired: true,
|
||||
roleKeys: '',
|
||||
userId: '',
|
||||
});
|
||||
|
||||
const quotaForm = reactive<QuotaAdjustmentForm>({
|
||||
approvalRef: '',
|
||||
commandId: '',
|
||||
reason: '',
|
||||
resourceType: '',
|
||||
userId: '',
|
||||
});
|
||||
|
||||
const governanceActionTitle = computed(() => {
|
||||
const titles: Record<GovernanceAction, string> = {
|
||||
assign_group: '调整权限组',
|
||||
grant_role: '授予管理员角色',
|
||||
restrict: '限制账号',
|
||||
revoke_role: '撤销管理员角色',
|
||||
review: '复核高危权限变更',
|
||||
suspend: '封禁账号',
|
||||
};
|
||||
return titles[governanceForm.action];
|
||||
});
|
||||
|
||||
/** 生成前端幂等键,后端仍会做最终幂等校验。 */
|
||||
function createCommandId(prefix: string) {
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
/** 空值统一展示,避免页面出现 undefined。 */
|
||||
function displayText(value?: number | string) {
|
||||
return value === undefined || value === '' ? '-' : String(value);
|
||||
}
|
||||
|
||||
/** 刷新用户账户摘要列表。 */
|
||||
async function loadUsers(page = userPager.current, pageSize = userPager.pageSize) {
|
||||
userLoading.value = true;
|
||||
try {
|
||||
const data = await listAccountUsersApi({
|
||||
entitlementSource: undefined,
|
||||
keyword: userFilters.keyword || undefined,
|
||||
pageNo: page,
|
||||
pageSize,
|
||||
status: userFilters.status,
|
||||
});
|
||||
users.value = data.list;
|
||||
userPager.current = page;
|
||||
userPager.pageSize = pageSize;
|
||||
userPager.total = data.total;
|
||||
} finally {
|
||||
userLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载选中用户的权益、配额和调整 ledger。 */
|
||||
async function loadUserGovernanceDetail(record: AdminAccountUserSummary) {
|
||||
selectedUser.value = record;
|
||||
entitlementLoading.value = true;
|
||||
try {
|
||||
const [entitlement, ledger] = await Promise.all([
|
||||
getUserEntitlementsApi(record.userId),
|
||||
listQuotaAdjustmentsApi(record.userId, { pageNo: 1, pageSize: 10 }),
|
||||
]);
|
||||
selectedEntitlement.value = entitlement;
|
||||
quotaLedger.value = ledger.list;
|
||||
} finally {
|
||||
entitlementLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新用量摘要列表。 */
|
||||
async function loadUsageRecords(
|
||||
page = usagePager.current,
|
||||
pageSize = usagePager.pageSize,
|
||||
) {
|
||||
usageLoading.value = true;
|
||||
try {
|
||||
const data = await listUsageRecordsApi({
|
||||
pageNo: page,
|
||||
pageSize,
|
||||
userId: recordFilters.userId || undefined,
|
||||
});
|
||||
usageRecords.value = data.list;
|
||||
usagePager.current = page;
|
||||
usagePager.pageSize = pageSize;
|
||||
usagePager.total = data.total;
|
||||
} finally {
|
||||
usageLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新购买记录脱敏摘要列表。 */
|
||||
async function loadPurchaseRecords(
|
||||
page = purchasePager.current,
|
||||
pageSize = purchasePager.pageSize,
|
||||
) {
|
||||
purchaseLoading.value = true;
|
||||
try {
|
||||
const data = await listPurchaseRecordsApi({
|
||||
pageNo: page,
|
||||
pageSize,
|
||||
userId: recordFilters.userId || undefined,
|
||||
});
|
||||
purchaseRecords.value = data.list;
|
||||
purchasePager.current = page;
|
||||
purchasePager.pageSize = pageSize;
|
||||
purchasePager.total = data.total;
|
||||
} finally {
|
||||
purchaseLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 用户表格分页变化。 */
|
||||
function handleUserTableChange(pager: TablePager) {
|
||||
loadUsers(pager.current ?? 1, pager.pageSize ?? userPager.pageSize);
|
||||
}
|
||||
|
||||
/** 用量表格分页变化。 */
|
||||
function handleUsageTableChange(pager: TablePager) {
|
||||
loadUsageRecords(pager.current ?? 1, pager.pageSize ?? usagePager.pageSize);
|
||||
}
|
||||
|
||||
/** 购买表格分页变化。 */
|
||||
function handlePurchaseTableChange(pager: TablePager) {
|
||||
loadPurchaseRecords(
|
||||
pager.current ?? 1,
|
||||
pager.pageSize ?? purchasePager.pageSize,
|
||||
);
|
||||
}
|
||||
|
||||
/** 打开账号、角色或权限组治理确认弹窗。 */
|
||||
function openGovernanceModal(
|
||||
record: AdminAccountUserSummary,
|
||||
action: GovernanceAction,
|
||||
) {
|
||||
governanceForm.action = action;
|
||||
governanceForm.approvalRef = '';
|
||||
governanceForm.confirmText = '';
|
||||
governanceForm.permissionGroups = '';
|
||||
governanceForm.reason = '';
|
||||
governanceForm.reviewRequired = action !== 'restrict';
|
||||
governanceForm.roleKeys = '';
|
||||
governanceForm.userId = record.userId;
|
||||
governanceModalOpen.value = true;
|
||||
}
|
||||
|
||||
/** 表格事件包装:查看用户权益治理详情。 */
|
||||
function handleLoadUserGovernanceDetail(record: TableRecord) {
|
||||
return loadUserGovernanceDetail(asAccountUser(record));
|
||||
}
|
||||
|
||||
/** 表格事件包装:打开账号治理弹窗。 */
|
||||
function handleOpenGovernanceModal(record: TableRecord, action: GovernanceAction) {
|
||||
openGovernanceModal(asAccountUser(record), action);
|
||||
}
|
||||
|
||||
/** 表格事件包装:打开配额调整弹窗。 */
|
||||
function handleOpenQuotaModal(record: TableRecord) {
|
||||
openQuotaModal(asAccountUser(record));
|
||||
}
|
||||
|
||||
/** 提交账号治理确认;当前契约无写接口,因此不调用后端伪造结果。 */
|
||||
function submitGovernanceCommand() {
|
||||
if (!governanceForm.reason.trim()) {
|
||||
message.error('请填写操作原因');
|
||||
return;
|
||||
}
|
||||
if (governanceForm.reviewRequired && !governanceForm.approvalRef.trim()) {
|
||||
message.error('高危治理动作必须填写复核或审批引用');
|
||||
return;
|
||||
}
|
||||
if (governanceForm.confirmText !== '确认写入审计') {
|
||||
message.error('请输入确认文本:确认写入审计');
|
||||
return;
|
||||
}
|
||||
message.warning(
|
||||
'当前 account openapi 尚未提供账号状态、角色或权限组写接口,页面未提交后端命令。',
|
||||
);
|
||||
governanceModalOpen.value = false;
|
||||
}
|
||||
|
||||
/** 打开配额调整弹窗,并用当前权益快照预填审计字段。 */
|
||||
function openQuotaModal(record: AdminAccountUserSummary) {
|
||||
const entitlement = selectedEntitlement.value?.entitlements?.[0];
|
||||
quotaForm.approvalRef = '';
|
||||
quotaForm.beforeLimit = entitlement?.limit;
|
||||
quotaForm.beforeRemaining = entitlement?.remaining;
|
||||
quotaForm.beforeUsed = entitlement?.used;
|
||||
quotaForm.commandId = createCommandId('quota');
|
||||
quotaForm.delta = undefined;
|
||||
quotaForm.reason = '';
|
||||
quotaForm.resourceType = entitlement?.resourceType ?? '';
|
||||
quotaForm.userId = record.userId;
|
||||
quotaModalOpen.value = true;
|
||||
}
|
||||
|
||||
/** 提交配额调整命令。 */
|
||||
async function submitQuotaAdjustment() {
|
||||
if (!quotaForm.userId || !quotaForm.resourceType || !quotaForm.reason.trim()) {
|
||||
message.error('请填写用户、资源类型和调整原因');
|
||||
return;
|
||||
}
|
||||
if (!quotaForm.delta) {
|
||||
message.error('请填写非 0 调整量');
|
||||
return;
|
||||
}
|
||||
quotaSubmitting.value = true;
|
||||
try {
|
||||
await createQuotaAdjustmentApi(quotaForm.userId, {
|
||||
adjustments: [
|
||||
{
|
||||
beforeSnapshot: {
|
||||
limit: quotaForm.beforeLimit,
|
||||
remaining: quotaForm.beforeRemaining,
|
||||
used: quotaForm.beforeUsed,
|
||||
},
|
||||
delta: quotaForm.delta,
|
||||
resourceType: quotaForm.resourceType,
|
||||
},
|
||||
],
|
||||
approvalRef: quotaForm.approvalRef || undefined,
|
||||
commandId: quotaForm.commandId || createCommandId('quota'),
|
||||
reason: quotaForm.reason,
|
||||
});
|
||||
message.success('配额调整命令已提交');
|
||||
quotaModalOpen.value = false;
|
||||
const row = users.value.find((item) => item.userId === quotaForm.userId);
|
||||
if (row) {
|
||||
await loadUserGovernanceDetail(row);
|
||||
}
|
||||
} finally {
|
||||
quotaSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 使用选中用户过滤账户记录。 */
|
||||
function filterRecordsBySelectedUser() {
|
||||
recordFilters.userId = selectedUser.value?.userId ?? '';
|
||||
loadUsageRecords(1, usagePager.pageSize);
|
||||
loadPurchaseRecords(1, purchasePager.pageSize);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadUsers();
|
||||
loadUsageRecords();
|
||||
loadPurchaseRecords();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="space-y-4">
|
||||
<Alert
|
||||
show-icon
|
||||
type="info"
|
||||
message="管理员账号页只展示治理和对账脱敏摘要,不提供用户侧完整个人中心或私有正文入口。"
|
||||
/>
|
||||
|
||||
<Tabs>
|
||||
<Tabs.TabPane key="users" tab="用户治理">
|
||||
<Card class="mb-4" title="用户、角色与权限治理">
|
||||
<Space class="mb-4" wrap>
|
||||
<Input
|
||||
v-model:value="userFilters.keyword"
|
||||
allow-clear
|
||||
placeholder="用户 ID / 昵称"
|
||||
style="width: 220px"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="userFilters.status"
|
||||
allow-clear
|
||||
placeholder="账号状态"
|
||||
style="width: 160px"
|
||||
>
|
||||
<Select.Option value="active">正常</Select.Option>
|
||||
<Select.Option value="restricted">受限</Select.Option>
|
||||
<Select.Option value="suspended">封禁</Select.Option>
|
||||
</Select>
|
||||
<Button type="primary" @click="loadUsers(1, userPager.pageSize)">
|
||||
查询
|
||||
</Button>
|
||||
<Button @click="loadUsers()">刷新</Button>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
row-key="userId"
|
||||
:columns="userColumns"
|
||||
:data-source="users"
|
||||
:loading="userLoading"
|
||||
:pagination="userPager"
|
||||
size="middle"
|
||||
@change="handleUserTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<Tag :color="getAccountStatusColor(record.status)">
|
||||
{{ getAccountStatusText(record.status) }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'quotaStatus'">
|
||||
<Tag :color="getQuotaStatusColor(record.quotaStatus)">
|
||||
{{ getQuotaStatusText(record.quotaStatus) }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'newApiBindingStatus'">
|
||||
<Tag>{{ displayText(record.newApiBindingStatus) }}</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'riskFlags'">
|
||||
<Space wrap>
|
||||
<Tag
|
||||
v-for="flag in record.riskFlags ?? []"
|
||||
:key="flag"
|
||||
color="red"
|
||||
>
|
||||
{{ flag }}
|
||||
</Tag>
|
||||
<span v-if="!record.riskFlags?.length">-</span>
|
||||
</Space>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<Space wrap>
|
||||
<Button type="link" @click="handleLoadUserGovernanceDetail(record)">
|
||||
权益
|
||||
</Button>
|
||||
<Button type="link" @click="handleOpenQuotaModal(record)">
|
||||
配额调整
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
@click="handleOpenGovernanceModal(record, 'suspend')"
|
||||
>
|
||||
封禁
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
@click="handleOpenGovernanceModal(record, 'grant_role')"
|
||||
>
|
||||
角色
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
@click="handleOpenGovernanceModal(record, 'assign_group')"
|
||||
>
|
||||
权限组
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
<Card title="权益、配额与复核摘要">
|
||||
<Alert
|
||||
v-if="!selectedUser"
|
||||
show-icon
|
||||
message="请选择用户查看权益、配额和调整 ledger。"
|
||||
/>
|
||||
<div v-else class="space-y-4">
|
||||
<Descriptions bordered size="small" :column="3">
|
||||
<Descriptions.Item label="用户 ID">
|
||||
{{ selectedUser.userId }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="昵称">
|
||||
{{ selectedUser.nickname }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="权益来源">
|
||||
{{ selectedUser.entitlementSource }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最近变更">
|
||||
{{ displayText(selectedEntitlement?.lastModifiedAt) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="变更人">
|
||||
{{ displayText(selectedEntitlement?.lastModifiedBy) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="审计">
|
||||
权益和配额变更必须写入业务审计。
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Table
|
||||
row-key="id"
|
||||
:columns="entitlementColumns"
|
||||
:data-source="selectedEntitlement?.entitlements ?? []"
|
||||
:loading="entitlementLoading"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<Table
|
||||
row-key="adjustmentId"
|
||||
:columns="quotaLedgerColumns"
|
||||
:data-source="quotaLedger"
|
||||
:loading="entitlementLoading"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane key="records" tab="用量与购买摘要">
|
||||
<Card class="mb-4" title="脱敏对账摘要">
|
||||
<Space class="mb-4" wrap>
|
||||
<Input
|
||||
v-model:value="recordFilters.userId"
|
||||
allow-clear
|
||||
placeholder="用户 ID"
|
||||
style="width: 220px"
|
||||
/>
|
||||
<Button
|
||||
:disabled="!selectedUser"
|
||||
@click="filterRecordsBySelectedUser"
|
||||
>
|
||||
使用选中用户
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
@click="
|
||||
() => {
|
||||
loadUsageRecords(1, usagePager.pageSize);
|
||||
loadPurchaseRecords(1, purchasePager.pageSize);
|
||||
}
|
||||
"
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
class="mb-4"
|
||||
row-key="recordId"
|
||||
:columns="usageColumns"
|
||||
:data-source="usageRecords"
|
||||
:loading="usageLoading"
|
||||
:pagination="usagePager"
|
||||
size="small"
|
||||
@change="handleUsageTableChange"
|
||||
/>
|
||||
<Table
|
||||
row-key="recordId"
|
||||
:columns="purchaseColumns"
|
||||
:data-source="purchaseRecords"
|
||||
:loading="purchaseLoading"
|
||||
:pagination="purchasePager"
|
||||
size="small"
|
||||
@change="handlePurchaseTableChange"
|
||||
/>
|
||||
</Card>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
v-model:open="governanceModalOpen"
|
||||
:title="governanceActionTitle"
|
||||
@ok="submitGovernanceCommand"
|
||||
>
|
||||
<Alert
|
||||
class="mb-4"
|
||||
show-icon
|
||||
type="warning"
|
||||
message="账号、角色和权限组变更需要二次确认、原因、复核引用和审计记录。"
|
||||
/>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="用户 ID">
|
||||
<Input v-model:value="governanceForm.userId" disabled />
|
||||
</Form.Item>
|
||||
<Form.Item v-if="governanceForm.action.includes('role')" label="角色标识">
|
||||
<Input
|
||||
v-model:value="governanceForm.roleKeys"
|
||||
placeholder="多个角色用逗号分隔"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
v-if="governanceForm.action === 'assign_group'"
|
||||
label="权限组标识"
|
||||
>
|
||||
<Input
|
||||
v-model:value="governanceForm.permissionGroups"
|
||||
placeholder="多个权限组用逗号分隔"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="操作原因" required>
|
||||
<Input.TextArea
|
||||
v-model:value="governanceForm.reason"
|
||||
:rows="3"
|
||||
placeholder="说明业务依据、风险判断和预期影响"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="复核 / 审批引用">
|
||||
<Input
|
||||
v-model:value="governanceForm.approvalRef"
|
||||
placeholder="高危权限变更必填"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="确认文本" required>
|
||||
<Input
|
||||
v-model:value="governanceForm.confirmText"
|
||||
placeholder="输入:确认写入审计"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
v-model:open="quotaModalOpen"
|
||||
:confirm-loading="quotaSubmitting"
|
||||
title="配额调整"
|
||||
@ok="submitQuotaAdjustment"
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="用户 ID" required>
|
||||
<Input v-model:value="quotaForm.userId" disabled />
|
||||
</Form.Item>
|
||||
<Form.Item label="资源类型" required>
|
||||
<Input v-model:value="quotaForm.resourceType" />
|
||||
</Form.Item>
|
||||
<Form.Item label="调整量" required>
|
||||
<InputNumber v-model:value="quotaForm.delta" class="w-full" />
|
||||
</Form.Item>
|
||||
<Form.Item label="调整前快照">
|
||||
<Space>
|
||||
<InputNumber
|
||||
v-model:value="quotaForm.beforeLimit"
|
||||
addon-before="上限"
|
||||
/>
|
||||
<InputNumber
|
||||
v-model:value="quotaForm.beforeUsed"
|
||||
addon-before="已用"
|
||||
/>
|
||||
<InputNumber
|
||||
v-model:value="quotaForm.beforeRemaining"
|
||||
addon-before="剩余"
|
||||
/>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
<Form.Item label="原因" required>
|
||||
<Input.TextArea v-model:value="quotaForm.reason" :rows="3" />
|
||||
</Form.Item>
|
||||
<Form.Item label="审批 / 复核引用">
|
||||
<Input v-model:value="quotaForm.approvalRef" />
|
||||
</Form.Item>
|
||||
<Form.Item label="幂等键" required>
|
||||
<Input v-model:value="quotaForm.commandId" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Page>
|
||||
</template>
|
||||
@ -0,0 +1,213 @@
|
||||
import { createApp, defineComponent, h, nextTick } from 'vue';
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
vi.mock('@vben/common-ui', () => ({
|
||||
Page: defineComponent({
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', { class: 'page-stub' }, slots.default?.());
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('ant-design-vue', () => {
|
||||
const renderDefaultSlot = (props?: Record<string, unknown>) =>
|
||||
defineComponent({
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', props, slots.default?.());
|
||||
},
|
||||
});
|
||||
const Button = defineComponent({
|
||||
emits: ['click'],
|
||||
props: ['disabled'],
|
||||
setup(props, { attrs, slots, emit }) {
|
||||
return () =>
|
||||
h(
|
||||
'button',
|
||||
{
|
||||
...attrs,
|
||||
disabled: props.disabled,
|
||||
onClick: () => emit('click'),
|
||||
},
|
||||
slots.default?.(),
|
||||
);
|
||||
},
|
||||
});
|
||||
const Modal = defineComponent({
|
||||
props: ['open'],
|
||||
setup(props, { slots }) {
|
||||
return () => (props.open ? h('div', { class: 'modal-stub' }, slots.default?.()) : null);
|
||||
},
|
||||
});
|
||||
const Tabs = renderDefaultSlot({ class: 'tabs-stub' }) as unknown as {
|
||||
TabPane: ReturnType<typeof renderDefaultSlot>;
|
||||
};
|
||||
Tabs.TabPane = renderDefaultSlot({ class: 'tab-pane-stub' });
|
||||
|
||||
return {
|
||||
Alert: renderDefaultSlot(),
|
||||
Badge: renderDefaultSlot(),
|
||||
Button,
|
||||
Card: renderDefaultSlot(),
|
||||
Descriptions: renderDefaultSlot(),
|
||||
Empty: renderDefaultSlot(),
|
||||
Form: renderDefaultSlot(),
|
||||
Input: renderDefaultSlot(),
|
||||
Modal,
|
||||
Progress: renderDefaultSlot(),
|
||||
Select: renderDefaultSlot(),
|
||||
Space: renderDefaultSlot(),
|
||||
Table: renderDefaultSlot(),
|
||||
Tabs,
|
||||
Tag: renderDefaultSlot(),
|
||||
Textarea: renderDefaultSlot(),
|
||||
Tooltip: renderDefaultSlot(),
|
||||
message: {
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('#/api/muse/ai', () => ({
|
||||
getAgentPageApi: vi.fn(async () => ({
|
||||
list: [
|
||||
{
|
||||
activeVersion: 7,
|
||||
agentId: 'agent-system',
|
||||
name: '剧情规划系统智能体',
|
||||
promptKey: 'plot.planner',
|
||||
scope: 'system',
|
||||
status: 'active',
|
||||
updatedAt: '2026-05-24T00:00:00Z',
|
||||
},
|
||||
{
|
||||
activeVersion: 2,
|
||||
agentId: 'agent-user',
|
||||
name: '用户私有润色智能体',
|
||||
promptKey: 'private.polish',
|
||||
scope: 'user',
|
||||
status: 'active',
|
||||
updatedAt: '2026-05-24T00:00:00Z',
|
||||
},
|
||||
],
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
total: 2,
|
||||
})),
|
||||
getPromptPageApi: vi.fn(async () => ({
|
||||
list: [
|
||||
{
|
||||
activeVersion: 3,
|
||||
latestVersion: 4,
|
||||
name: '剧情规划 Prompt',
|
||||
promptKey: 'plot.planner',
|
||||
updatedAt: '2026-05-24T00:00:00Z',
|
||||
variableCount: 5,
|
||||
},
|
||||
],
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
})),
|
||||
getQualityPolicyPageApi: vi.fn(async () => ({
|
||||
list: [
|
||||
{
|
||||
activeVersion: 1,
|
||||
dimensionCount: 3,
|
||||
latestVersion: 2,
|
||||
name: '候选交付质量门控',
|
||||
policyKey: 'candidate.delivery',
|
||||
updatedAt: '2026-05-24T00:00:00Z',
|
||||
},
|
||||
],
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
})),
|
||||
getToolGrantPageApi: vi.fn(async () => ({
|
||||
list: [
|
||||
{
|
||||
agentId: 'agent-system',
|
||||
agentName: '剧情规划系统智能体',
|
||||
approvedBy: 'security-facade',
|
||||
budget: { maxCalls: 20, maxTokens: 8000, period: 'daily' },
|
||||
createdAt: '2026-05-24T00:00:00Z',
|
||||
grantId: 'grant-1',
|
||||
outboundPolicy: {
|
||||
allowedDestinations: ['model-gateway'],
|
||||
dataPolicy: 'redacted_user_content',
|
||||
networkMode: 'internal_only',
|
||||
},
|
||||
scope: {
|
||||
crossWorkAllowed: false,
|
||||
ownerType: 'system',
|
||||
resourceRefs: [{ resourceId: 'global-kb', resourceType: 'knowledge_base' }],
|
||||
scopeType: 'knowledge_base',
|
||||
},
|
||||
securityApprovalId: 'SEC-20260524-001',
|
||||
status: 'approved',
|
||||
toolType: 'knowledge_retrieval',
|
||||
},
|
||||
],
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('muse ai config page', () => {
|
||||
it('renders AI governance domains and blocks user private agent management', async () => {
|
||||
const { container, unmount } = await mountPage();
|
||||
await flushPromises();
|
||||
|
||||
expect(container.textContent).toContain('Prompt 模板');
|
||||
expect(container.textContent).toContain('系统智能体');
|
||||
expect(container.textContent).toContain('质量门控');
|
||||
expect(container.textContent).toContain('Tool Grant');
|
||||
expect(container.textContent).toContain('已激活 v3');
|
||||
expect(container.textContent).toContain('可管理');
|
||||
expect(container.textContent).toContain('只读治理摘要');
|
||||
expect(container.textContent).not.toContain('编辑私有 Agent');
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('opens impact preview before risky quality policy operations', async () => {
|
||||
const { container, unmount } = await mountPage();
|
||||
await flushPromises();
|
||||
|
||||
const button = container.querySelector<HTMLButtonElement>(
|
||||
'[data-test="quality-publish-preview"]',
|
||||
);
|
||||
button?.click();
|
||||
await nextTick();
|
||||
|
||||
expect(container.textContent).toContain('影响预览');
|
||||
expect(container.textContent).toContain('操作原因');
|
||||
expect(container.textContent).toContain('审计回执字段');
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
async function mountPage() {
|
||||
const { default: AiConfigPage } = await import('../index.vue');
|
||||
const container = document.createElement('div');
|
||||
document.body.append(container);
|
||||
|
||||
const app = createApp(AiConfigPage);
|
||||
app.mount(container);
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
|
||||
return {
|
||||
container,
|
||||
unmount: () => {
|
||||
app.unmount();
|
||||
container.remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
1026
muse-admin/apps/web-antd/src/views/muse/ai/index.vue
Normal file
1026
muse-admin/apps/web-antd/src/views/muse/ai/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
461
muse-admin/apps/web-antd/src/views/muse/audit/index.vue
Normal file
461
muse-admin/apps/web-antd/src/views/muse/audit/index.vue
Normal file
@ -0,0 +1,461 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 日志与审计页
|
||||
*
|
||||
* 分离接口调用日志和业务审计日志。详情只展示脱敏摘要:
|
||||
* 不展示 token、secret、完整 header、完整请求/响应体或用户私有正文。
|
||||
* 业务审计事件 append-only,本页面不提供修改或删除入口。
|
||||
*/
|
||||
import type {
|
||||
ApiAccessLogDetail,
|
||||
ApiAccessLogStatus,
|
||||
ApiAccessLogSummary,
|
||||
BusinessAuditEventDetail,
|
||||
BusinessAuditEventSummary,
|
||||
RedactedSummary,
|
||||
} from '#/api/muse/audit';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Descriptions,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
getApiAccessLogApi,
|
||||
getBusinessAuditEventApi,
|
||||
listApiAccessLogsApi,
|
||||
listBusinessAuditEventsApi,
|
||||
} from '#/api/muse/audit';
|
||||
|
||||
/** 表格分页对象。 */
|
||||
interface TablePager {
|
||||
/** 当前页码。 */
|
||||
current?: number;
|
||||
/** 每页数量。 */
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
/** Ant Table 插槽传入的通用记录。 */
|
||||
type TableRecord = Record<string, unknown>;
|
||||
|
||||
const apiLogStatusColor: Record<ApiAccessLogStatus, string> = {
|
||||
failed: 'red',
|
||||
redacted: 'default',
|
||||
sensitive: 'orange',
|
||||
slow: 'gold',
|
||||
success: 'green',
|
||||
};
|
||||
|
||||
/** 将 Ant Table 通用记录转回接口日志摘要。 */
|
||||
function asApiAccessLog(record: TableRecord) {
|
||||
return record as unknown as ApiAccessLogSummary;
|
||||
}
|
||||
|
||||
/** 将 Ant Table 通用记录转回业务审计摘要。 */
|
||||
function asBusinessAuditEvent(record: TableRecord) {
|
||||
return record as unknown as BusinessAuditEventSummary;
|
||||
}
|
||||
|
||||
/** 接口调用状态颜色,兼容模板中的 unknown 记录。 */
|
||||
function getApiLogStatusColor(status: unknown) {
|
||||
return apiLogStatusColor[status as ApiAccessLogStatus] ?? 'default';
|
||||
}
|
||||
|
||||
const apiLogColumns = [
|
||||
{ dataIndex: 'logId', key: 'logId', title: '日志 ID', width: 110 },
|
||||
{ dataIndex: 'requestId', key: 'requestId', title: '请求 ID' },
|
||||
{ dataIndex: 'actor', key: 'actor', title: '调用人' },
|
||||
{ dataIndex: 'module', key: 'module', title: '模块' },
|
||||
{ dataIndex: 'method', key: 'method', title: '方法', width: 90 },
|
||||
{ dataIndex: 'pathSummary', key: 'pathSummary', title: '路径摘要' },
|
||||
{ dataIndex: 'status', key: 'status', title: '状态', width: 110 },
|
||||
{ dataIndex: 'statusCode', key: 'statusCode', title: '状态码', width: 90 },
|
||||
{ dataIndex: 'durationMs', key: 'durationMs', title: '耗时 ms', width: 100 },
|
||||
{ dataIndex: 'createdAt', key: 'createdAt', title: '时间' },
|
||||
{ key: 'actions', title: '操作', width: 100 },
|
||||
];
|
||||
|
||||
const businessAuditColumns = [
|
||||
{ dataIndex: 'eventId', key: 'eventId', title: '事件 ID', width: 110 },
|
||||
{ dataIndex: 'eventType', key: 'eventType', title: '事件类型' },
|
||||
{ dataIndex: 'operator', key: 'operator', title: '操作者' },
|
||||
{ dataIndex: 'resourceType', key: 'resourceType', title: '资源类型' },
|
||||
{ dataIndex: 'resourceId', key: 'resourceId', title: '资源 ID' },
|
||||
{ dataIndex: 'summary', key: 'summary', title: '摘要' },
|
||||
{ dataIndex: 'createdAt', key: 'createdAt', title: '时间' },
|
||||
{ key: 'actions', title: '操作', width: 100 },
|
||||
];
|
||||
|
||||
const apiLogLoading = ref(false);
|
||||
const businessAuditLoading = ref(false);
|
||||
const detailLoading = ref(false);
|
||||
|
||||
const apiLogs = ref<ApiAccessLogSummary[]>([]);
|
||||
const businessEvents = ref<BusinessAuditEventSummary[]>([]);
|
||||
const selectedApiLog = ref<ApiAccessLogDetail>();
|
||||
const selectedBusinessEvent = ref<BusinessAuditEventDetail>();
|
||||
|
||||
const apiLogFilters = reactive({
|
||||
actor: '',
|
||||
module: '',
|
||||
requestId: '',
|
||||
status: undefined as ApiAccessLogStatus | undefined,
|
||||
});
|
||||
|
||||
const businessFilters = reactive({
|
||||
eventType: '',
|
||||
});
|
||||
|
||||
const apiLogPager = reactive({ current: 1, pageSize: 10, total: 0 });
|
||||
const businessPager = reactive({ current: 1, pageSize: 10, total: 0 });
|
||||
|
||||
const apiLogDetailOpen = ref(false);
|
||||
const businessDetailOpen = ref(false);
|
||||
|
||||
/** 统一空值展示。 */
|
||||
function displayText(value?: boolean | number | string) {
|
||||
if (value === undefined || value === '') {
|
||||
return '-';
|
||||
}
|
||||
return typeof value === 'boolean' ? (value ? '是' : '否') : String(value);
|
||||
}
|
||||
|
||||
/** 将脱敏摘要对象格式化展示。 */
|
||||
function stringifySummary(value?: RedactedSummary) {
|
||||
return value ? JSON.stringify(value, null, 2) : '-';
|
||||
}
|
||||
|
||||
/** 查询接口调用日志。 */
|
||||
async function loadApiLogs(
|
||||
page = apiLogPager.current,
|
||||
pageSize = apiLogPager.pageSize,
|
||||
) {
|
||||
apiLogLoading.value = true;
|
||||
try {
|
||||
const data = await listApiAccessLogsApi({
|
||||
actor: apiLogFilters.actor || undefined,
|
||||
module: apiLogFilters.module || undefined,
|
||||
pageNo: page,
|
||||
pageSize,
|
||||
requestId: apiLogFilters.requestId || undefined,
|
||||
status: apiLogFilters.status,
|
||||
});
|
||||
apiLogs.value = data.list;
|
||||
apiLogPager.current = page;
|
||||
apiLogPager.pageSize = pageSize;
|
||||
apiLogPager.total = data.total;
|
||||
} finally {
|
||||
apiLogLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询业务审计事件。 */
|
||||
async function loadBusinessEvents(
|
||||
page = businessPager.current,
|
||||
pageSize = businessPager.pageSize,
|
||||
) {
|
||||
businessAuditLoading.value = true;
|
||||
try {
|
||||
const data = await listBusinessAuditEventsApi({
|
||||
eventType: businessFilters.eventType || undefined,
|
||||
pageNo: page,
|
||||
pageSize,
|
||||
});
|
||||
businessEvents.value = data.list;
|
||||
businessPager.current = page;
|
||||
businessPager.pageSize = pageSize;
|
||||
businessPager.total = data.total;
|
||||
} finally {
|
||||
businessAuditLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开接口调用日志脱敏详情。 */
|
||||
async function openApiLogDetail(record: ApiAccessLogSummary) {
|
||||
detailLoading.value = true;
|
||||
apiLogDetailOpen.value = true;
|
||||
try {
|
||||
selectedApiLog.value = await getApiAccessLogApi(record.logId);
|
||||
} finally {
|
||||
detailLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开业务审计事件脱敏详情。 */
|
||||
async function openBusinessDetail(record: BusinessAuditEventSummary) {
|
||||
detailLoading.value = true;
|
||||
businessDetailOpen.value = true;
|
||||
try {
|
||||
selectedBusinessEvent.value = await getBusinessAuditEventApi(record.eventId);
|
||||
} finally {
|
||||
detailLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格事件包装:打开接口日志详情。 */
|
||||
function handleOpenApiLogDetail(record: TableRecord) {
|
||||
return openApiLogDetail(asApiAccessLog(record));
|
||||
}
|
||||
|
||||
/** 表格事件包装:打开业务审计详情。 */
|
||||
function handleOpenBusinessDetail(record: TableRecord) {
|
||||
return openBusinessDetail(asBusinessAuditEvent(record));
|
||||
}
|
||||
|
||||
/** 接口日志表格分页变化。 */
|
||||
function handleApiLogTableChange(pager: TablePager) {
|
||||
loadApiLogs(pager.current ?? 1, pager.pageSize ?? apiLogPager.pageSize);
|
||||
}
|
||||
|
||||
/** 业务审计表格分页变化。 */
|
||||
function handleBusinessTableChange(pager: TablePager) {
|
||||
loadBusinessEvents(
|
||||
pager.current ?? 1,
|
||||
pager.pageSize ?? businessPager.pageSize,
|
||||
);
|
||||
}
|
||||
|
||||
/** 审计导出摘要契约暂未进入本轮 API 文件,避免前端伪造导出。 */
|
||||
function explainExportBoundary() {
|
||||
message.warning('本轮仅接入查询和详情契约;审计导出摘要需后端审批任务契约。');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadApiLogs();
|
||||
loadBusinessEvents();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="space-y-4">
|
||||
<Alert
|
||||
show-icon
|
||||
type="info"
|
||||
message="接口调用日志用于排障和轻量审计;业务审计日志 append-only,详情只展示脱敏 before/after 摘要。"
|
||||
/>
|
||||
|
||||
<Tabs>
|
||||
<Tabs.TabPane key="api-logs" tab="接口调用日志">
|
||||
<Card title="接口调用日志">
|
||||
<Space class="mb-4" wrap>
|
||||
<Input
|
||||
v-model:value="apiLogFilters.requestId"
|
||||
allow-clear
|
||||
placeholder="请求 ID"
|
||||
style="width: 220px"
|
||||
/>
|
||||
<Input
|
||||
v-model:value="apiLogFilters.actor"
|
||||
allow-clear
|
||||
placeholder="调用人"
|
||||
style="width: 180px"
|
||||
/>
|
||||
<Input
|
||||
v-model:value="apiLogFilters.module"
|
||||
allow-clear
|
||||
placeholder="模块"
|
||||
style="width: 160px"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="apiLogFilters.status"
|
||||
allow-clear
|
||||
placeholder="状态"
|
||||
style="width: 160px"
|
||||
>
|
||||
<Select.Option value="success">正常</Select.Option>
|
||||
<Select.Option value="failed">失败</Select.Option>
|
||||
<Select.Option value="slow">慢请求</Select.Option>
|
||||
<Select.Option value="sensitive">敏感</Select.Option>
|
||||
<Select.Option value="redacted">已脱敏</Select.Option>
|
||||
</Select>
|
||||
<Button
|
||||
type="primary"
|
||||
@click="loadApiLogs(1, apiLogPager.pageSize)"
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
<Button @click="loadApiLogs()">刷新</Button>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
row-key="logId"
|
||||
:columns="apiLogColumns"
|
||||
:data-source="apiLogs"
|
||||
:loading="apiLogLoading"
|
||||
:pagination="apiLogPager"
|
||||
@change="handleApiLogTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<Tag :color="getApiLogStatusColor(record.status)">
|
||||
{{ record.status }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<Button type="link" @click="handleOpenApiLogDetail(record)">
|
||||
详情
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</Card>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane key="business-events" tab="业务审计日志">
|
||||
<Card title="业务审计日志">
|
||||
<Space class="mb-4" wrap>
|
||||
<Input
|
||||
v-model:value="businessFilters.eventType"
|
||||
allow-clear
|
||||
placeholder="事件类型"
|
||||
style="width: 220px"
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
@click="loadBusinessEvents(1, businessPager.pageSize)"
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
<Button @click="loadBusinessEvents()">刷新</Button>
|
||||
<Button @click="explainExportBoundary">导出摘要</Button>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
row-key="eventId"
|
||||
:columns="businessAuditColumns"
|
||||
:data-source="businessEvents"
|
||||
:loading="businessAuditLoading"
|
||||
:pagination="businessPager"
|
||||
@change="handleBusinessTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'actions'">
|
||||
<Button type="link" @click="handleOpenBusinessDetail(record)">
|
||||
详情
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</Card>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
v-model:open="apiLogDetailOpen"
|
||||
:footer="null"
|
||||
:loading="detailLoading"
|
||||
title="接口调用日志详情"
|
||||
width="900px"
|
||||
>
|
||||
<Descriptions v-if="selectedApiLog" bordered size="small" :column="2">
|
||||
<Descriptions.Item label="日志 ID">
|
||||
{{ selectedApiLog.logId }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="请求 ID">
|
||||
{{ selectedApiLog.requestId }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="调用人">
|
||||
{{ selectedApiLog.actor }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="方法">
|
||||
{{ selectedApiLog.method }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="路径摘要">
|
||||
{{ selectedApiLog.pathSummary }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
{{ selectedApiLog.status }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态码">
|
||||
{{ selectedApiLog.statusCode }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="耗时">
|
||||
{{ selectedApiLog.durationMs }} ms
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="对象摘要">
|
||||
{{ displayText(selectedApiLog.objectSummary) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="错误摘要">
|
||||
{{ displayText(selectedApiLog.errorSummary) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="关联审计事件">
|
||||
{{ displayText(selectedApiLog.relatedAuditEventId) }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<div v-if="selectedApiLog" class="mt-4 grid grid-cols-3 gap-4">
|
||||
<Card size="small" title="请求头脱敏摘要">
|
||||
<pre class="m-0 whitespace-pre-wrap">{{ stringifySummary(selectedApiLog.requestHeadersSummary) }}</pre>
|
||||
</Card>
|
||||
<Card size="small" title="请求体脱敏摘要">
|
||||
<pre class="m-0 whitespace-pre-wrap">{{ stringifySummary(selectedApiLog.requestBodySummary) }}</pre>
|
||||
</Card>
|
||||
<Card size="small" title="响应体脱敏摘要">
|
||||
<pre class="m-0 whitespace-pre-wrap">{{ stringifySummary(selectedApiLog.responseBodySummary) }}</pre>
|
||||
</Card>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
v-model:open="businessDetailOpen"
|
||||
:footer="null"
|
||||
:loading="detailLoading"
|
||||
title="业务审计详情"
|
||||
width="900px"
|
||||
>
|
||||
<Descriptions v-if="selectedBusinessEvent" bordered size="small" :column="2">
|
||||
<Descriptions.Item label="事件 ID">
|
||||
{{ selectedBusinessEvent.eventId }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="事件类型">
|
||||
{{ selectedBusinessEvent.eventType }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="操作者">
|
||||
{{ selectedBusinessEvent.operator }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="结果">
|
||||
{{ displayText(selectedBusinessEvent.result) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="资源">
|
||||
{{ displayText(selectedBusinessEvent.resourceType) }} /
|
||||
{{ displayText(selectedBusinessEvent.resourceId) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Append-only">
|
||||
{{ displayText(selectedBusinessEvent.immutable) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="原因">
|
||||
{{ displayText(selectedBusinessEvent.reason) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="影响范围">
|
||||
{{ displayText(selectedBusinessEvent.impactSummary) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="复核摘要">
|
||||
{{ displayText(selectedBusinessEvent.approvalSummary) }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<div v-if="selectedBusinessEvent" class="mt-4 grid grid-cols-2 gap-4">
|
||||
<Card size="small" title="Before 脱敏摘要">
|
||||
<pre class="m-0 whitespace-pre-wrap">{{ stringifySummary(selectedBusinessEvent.beforeSnapshot) }}</pre>
|
||||
</Card>
|
||||
<Card size="small" title="After 脱敏摘要">
|
||||
<pre class="m-0 whitespace-pre-wrap">{{ stringifySummary(selectedBusinessEvent.afterSnapshot) }}</pre>
|
||||
</Card>
|
||||
</div>
|
||||
</Modal>
|
||||
</Page>
|
||||
</template>
|
||||
@ -0,0 +1,499 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 功能编排与保护节点治理页
|
||||
* 明确区分可替换开放槽位和不可替换保护节点,并提供影响预览与激活确认入口。
|
||||
*/
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { TableColumnsType } from 'ant-design-vue';
|
||||
import type {
|
||||
FunctionChainImpactPreview,
|
||||
FunctionChainStatus,
|
||||
FunctionChainSummary,
|
||||
ProtectionNodeSummary,
|
||||
} from '#/api/muse/governance/function-chain';
|
||||
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
activateFunctionChainVersionApi,
|
||||
listFunctionChainsApi,
|
||||
listProtectionNodesApi,
|
||||
previewFunctionChainImpactApi,
|
||||
} from '#/api/muse/governance/function-chain';
|
||||
|
||||
defineOptions({ name: 'MuseGovernanceFunctionOrchestration' });
|
||||
|
||||
/** 功能链路状态展示配置。 */
|
||||
const STATUS_META_MAP: Record<FunctionChainStatus, { color: string; label: string }> = {
|
||||
active: { color: 'green', label: '已启用' },
|
||||
draft: { color: 'blue', label: '草稿' },
|
||||
protection_node_missing: { color: 'red', label: '保护节点缺失' },
|
||||
publish_failed: { color: 'red', label: '发布失败' },
|
||||
slot_contract_missing: { color: 'orange', label: '槽位合同缺失' },
|
||||
};
|
||||
|
||||
/** 查询表单。 */
|
||||
const gridFormSchema: VbenFormSchema[] = [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入链路标识或名称',
|
||||
},
|
||||
fieldName: 'keyword',
|
||||
label: '关键词',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '草稿', value: 'draft' },
|
||||
{ label: '已启用', value: 'active' },
|
||||
{ label: '槽位合同缺失', value: 'slot_contract_missing' },
|
||||
{ label: '保护节点缺失', value: 'protection_node_missing' },
|
||||
{ label: '发布失败', value: 'publish_failed' },
|
||||
],
|
||||
placeholder: '请选择状态',
|
||||
},
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
},
|
||||
];
|
||||
|
||||
/** 保护节点表格列。 */
|
||||
const protectionNodeColumns: TableColumnsType<ProtectionNodeSummary> = [
|
||||
{ dataIndex: 'nodeKey', minWidth: 180, title: 'nodeKey' },
|
||||
{ dataIndex: 'displayName', minWidth: 160, title: '名称' },
|
||||
{ dataIndex: 'nodeType', minWidth: 180, title: '类型' },
|
||||
{ dataIndex: 'irreplaceable', minWidth: 120, title: '不可替换' },
|
||||
{ dataIndex: 'irreplaceableReason', minWidth: 240, title: '不可替换原因' },
|
||||
{ dataIndex: 'auditSummary', minWidth: 220, title: '审计摘要' },
|
||||
];
|
||||
|
||||
const selectedChain = ref<FunctionChainSummary>();
|
||||
const protectionNodes = ref<ProtectionNodeSummary[]>([]);
|
||||
const protectionNodeLoading = ref(false);
|
||||
const previewDrawerOpen = ref(false);
|
||||
const activateDrawerOpen = ref(false);
|
||||
const previewLoading = ref(false);
|
||||
const activating = ref(false);
|
||||
const impactPreview = ref<FunctionChainImpactPreview>();
|
||||
|
||||
/** 影响预览请求表单。 */
|
||||
const previewForm = reactive({
|
||||
expectedCurrentVersion: undefined as number | undefined,
|
||||
targetVersion: undefined as number | undefined,
|
||||
});
|
||||
|
||||
/** 激活确认表单。 */
|
||||
const activateForm = reactive({
|
||||
expectedActiveVersion: undefined as number | undefined,
|
||||
impactPreviewId: '',
|
||||
reason: '',
|
||||
targetVersion: undefined as number | undefined,
|
||||
validationResultId: '',
|
||||
});
|
||||
|
||||
/** 是否存在保护节点边界违规,存在时前端也阻断激活入口。 */
|
||||
const hasProtectionBoundaryViolation = computed(() => {
|
||||
return (impactPreview.value?.slotChanges ?? []).some(
|
||||
(item) => item.changeType === 'protection_boundary_violation',
|
||||
);
|
||||
});
|
||||
|
||||
/** 生成命令幂等键。 */
|
||||
function createCommandId(action: string) {
|
||||
return `muse-admin-function-chain-${action}-${Date.now()}`;
|
||||
}
|
||||
|
||||
/** 推导功能链路状态。 */
|
||||
function resolveChainStatus(row: FunctionChainSummary): FunctionChainStatus {
|
||||
return row.status ?? 'active';
|
||||
}
|
||||
|
||||
/** 选中链路并加载保护节点注册表。 */
|
||||
async function handleSelectChain(row: FunctionChainSummary) {
|
||||
selectedChain.value = row;
|
||||
previewForm.expectedCurrentVersion = row.activeVersion;
|
||||
previewForm.targetVersion = row.activeVersion;
|
||||
activateForm.expectedActiveVersion = row.activeVersion;
|
||||
activateForm.targetVersion = row.activeVersion;
|
||||
protectionNodeLoading.value = true;
|
||||
try {
|
||||
const result = await listProtectionNodesApi({
|
||||
chainKey: row.chainKey,
|
||||
pageNo: 1,
|
||||
pageSize: 100,
|
||||
});
|
||||
protectionNodes.value = result.list;
|
||||
} finally {
|
||||
protectionNodeLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开影响预览抽屉。 */
|
||||
function openPreviewDrawer(row: FunctionChainSummary) {
|
||||
handleSelectChain(row);
|
||||
previewDrawerOpen.value = true;
|
||||
}
|
||||
|
||||
/** 打开激活确认抽屉。 */
|
||||
function openActivateDrawer(row: FunctionChainSummary) {
|
||||
handleSelectChain(row);
|
||||
activateForm.impactPreviewId = impactPreview.value?.impactPreviewId ?? '';
|
||||
activateDrawerOpen.value = true;
|
||||
}
|
||||
|
||||
/** 请求功能链路影响预览。 */
|
||||
async function handlePreviewImpact() {
|
||||
if (!selectedChain.value) {
|
||||
message.warning('请先选择功能链路');
|
||||
return;
|
||||
}
|
||||
previewLoading.value = true;
|
||||
try {
|
||||
impactPreview.value = await previewFunctionChainImpactApi(
|
||||
selectedChain.value.chainKey,
|
||||
{
|
||||
commandId: createCommandId('impact-preview'),
|
||||
expectedCurrentVersion: previewForm.expectedCurrentVersion,
|
||||
targetVersion: previewForm.targetVersion,
|
||||
},
|
||||
);
|
||||
activateForm.impactPreviewId = impactPreview.value.impactPreviewId ?? '';
|
||||
message.success('影响预览已生成');
|
||||
} finally {
|
||||
previewLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 执行功能链路激活。 */
|
||||
async function handleActivateChain() {
|
||||
if (!selectedChain.value || !activateForm.targetVersion) {
|
||||
message.warning('请先选择要激活的功能链路版本');
|
||||
return;
|
||||
}
|
||||
if (hasProtectionBoundaryViolation.value) {
|
||||
message.error('影响预览存在保护节点边界违规,不能激活');
|
||||
return;
|
||||
}
|
||||
if (!activateForm.reason || !activateForm.validationResultId || !activateForm.impactPreviewId) {
|
||||
message.warning('请填写激活理由、校验结果 ID 和影响预览 ID');
|
||||
return;
|
||||
}
|
||||
activating.value = true;
|
||||
try {
|
||||
await activateFunctionChainVersionApi(
|
||||
selectedChain.value.chainKey,
|
||||
activateForm.targetVersion,
|
||||
{
|
||||
commandId: createCommandId('activate'),
|
||||
expectedActiveVersion:
|
||||
activateForm.expectedActiveVersion ?? selectedChain.value.activeVersion,
|
||||
impactPreviewId: activateForm.impactPreviewId,
|
||||
reason: activateForm.reason,
|
||||
validationResultId: activateForm.validationResultId,
|
||||
},
|
||||
);
|
||||
activateDrawerOpen.value = false;
|
||||
message.success('功能链路激活命令已提交');
|
||||
} finally {
|
||||
activating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 激活前二次确认。 */
|
||||
function confirmActivateChain() {
|
||||
Modal.confirm({
|
||||
content:
|
||||
'激活会影响后续系统功能链路;保护节点仍不可替换,存在边界违规时服务端也会拒绝。',
|
||||
onOk: handleActivateChain,
|
||||
title: '确认激活功能编排版本',
|
||||
});
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: gridFormSchema,
|
||||
},
|
||||
gridOptions: {
|
||||
columns: [
|
||||
{ field: 'chainKey', minWidth: 180, title: 'chainKey' },
|
||||
{ field: 'displayName', minWidth: 180, title: '链路名称' },
|
||||
{ field: 'description', minWidth: 220, title: '用途' },
|
||||
{ field: 'activeVersion', minWidth: 120, title: 'activeVersion' },
|
||||
{
|
||||
field: 'openSlotCount',
|
||||
minWidth: 130,
|
||||
slots: { default: 'openSlotCount' },
|
||||
title: '开放槽位',
|
||||
},
|
||||
{
|
||||
field: 'protectionNodeCount',
|
||||
minWidth: 150,
|
||||
slots: { default: 'protectionNodeCount' },
|
||||
title: '保护节点',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
minWidth: 140,
|
||||
slots: { default: 'status' },
|
||||
title: '状态',
|
||||
},
|
||||
{
|
||||
field: 'updatedAt',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
title: '更新时间',
|
||||
},
|
||||
{
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
title: '操作',
|
||||
width: 220,
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await listFunctionChainsApi({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
isHover: true,
|
||||
keyField: 'chainKey',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<FunctionChainSummary>,
|
||||
});
|
||||
|
||||
/** 手动刷新功能链路列表。 */
|
||||
function handleRefresh() {
|
||||
gridApi.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<a-alert
|
||||
class="mb-4"
|
||||
message="开放槽位只表示可替换的非保护节点;保护节点不可被普通用户替换、覆盖或降级为开放槽位。"
|
||||
show-icon
|
||||
type="warning"
|
||||
/>
|
||||
|
||||
<Grid table-title="功能编排">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '刷新',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.REFRESH,
|
||||
onClick: handleRefresh,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #openSlotCount="{ row }">
|
||||
<a-tag color="blue">{{ row.openSlotCount ?? 0 }} 个开放槽位</a-tag>
|
||||
</template>
|
||||
|
||||
<template #protectionNodeCount="{ row }">
|
||||
<a-tag color="orange">{{ row.protectionNodeCount ?? 0 }} 个保护节点</a-tag>
|
||||
</template>
|
||||
|
||||
<template #status="{ row }">
|
||||
<a-tag :color="STATUS_META_MAP[resolveChainStatus(row)].color">
|
||||
{{ STATUS_META_MAP[resolveChainStatus(row)].label }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '保护节点',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.AUDIT,
|
||||
onClick: handleSelectChain.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '影响预览',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.VIEW,
|
||||
onClick: openPreviewDrawer.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '激活确认',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
onClick: openActivateDrawer.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<a-card class="mt-4" title="不可替换保护节点注册表">
|
||||
<a-empty v-if="!selectedChain" description="请选择功能链路查看保护节点" />
|
||||
<a-table
|
||||
v-else
|
||||
:columns="protectionNodeColumns"
|
||||
:data-source="protectionNodes"
|
||||
:loading="protectionNodeLoading"
|
||||
:pagination="false"
|
||||
row-key="nodeKey"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'irreplaceable'">
|
||||
<a-tag :color="record.irreplaceable ? 'orange' : 'red'">
|
||||
{{ record.irreplaceable ? '不可替换' : '异常:可替换' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-drawer
|
||||
v-model:open="previewDrawerOpen"
|
||||
destroy-on-close
|
||||
title="功能链路影响预览"
|
||||
width="560"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="目标版本">
|
||||
<a-input-number
|
||||
v-model:value="previewForm.targetVersion"
|
||||
class="w-full"
|
||||
:min="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="期望当前版本">
|
||||
<a-input-number
|
||||
v-model:value="previewForm.expectedCurrentVersion"
|
||||
class="w-full"
|
||||
:min="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-button type="primary" :loading="previewLoading" @click="handlePreviewImpact">
|
||||
生成影响预览
|
||||
</a-button>
|
||||
|
||||
<a-descriptions v-if="impactPreview" class="mt-4" bordered size="small" :column="1">
|
||||
<a-descriptions-item label="previewId">
|
||||
{{ impactPreview.impactPreviewId ?? '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="槽位绑定影响">
|
||||
{{ impactPreview.affectedAgentSlotBindings ?? 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="运行时任务影响">
|
||||
{{ impactPreview.affectedAIRuntimeTasks ?? 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="保护节点变更">
|
||||
{{ impactPreview.affectedProtectionNodeChanges ?? 0 }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-alert
|
||||
v-if="hasProtectionBoundaryViolation"
|
||||
class="mt-4"
|
||||
message="检测到保护节点被降级为开放槽位,激活入口已阻断。"
|
||||
show-icon
|
||||
type="error"
|
||||
/>
|
||||
|
||||
<a-list
|
||||
v-if="impactPreview?.slotChanges?.length"
|
||||
class="mt-4"
|
||||
header="槽位变更"
|
||||
:data-source="impactPreview.slotChanges"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-tag :color="item.changeType === 'protection_boundary_violation' ? 'red' : 'blue'">
|
||||
{{ item.changeType }}
|
||||
</a-tag>
|
||||
{{ item.slotKey ?? '-' }}:{{ item.impactDescription ?? '-' }}
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-drawer>
|
||||
|
||||
<a-drawer
|
||||
v-model:open="activateDrawerOpen"
|
||||
destroy-on-close
|
||||
title="发布 / 激活确认"
|
||||
width="560"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="目标版本">
|
||||
<a-input-number
|
||||
v-model:value="activateForm.targetVersion"
|
||||
class="w-full"
|
||||
:min="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="激活理由">
|
||||
<a-textarea
|
||||
v-model:value="activateForm.reason"
|
||||
:auto-size="{ minRows: 3, maxRows: 5 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="validationResultId">
|
||||
<a-input v-model:value="activateForm.validationResultId" />
|
||||
</a-form-item>
|
||||
<a-form-item label="impactPreviewId">
|
||||
<a-input v-model:value="activateForm.impactPreviewId" />
|
||||
</a-form-item>
|
||||
<a-form-item label="expectedActiveVersion">
|
||||
<a-input-number
|
||||
v-model:value="activateForm.expectedActiveVersion"
|
||||
class="w-full"
|
||||
:min="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-alert
|
||||
v-if="hasProtectionBoundaryViolation"
|
||||
class="mb-4"
|
||||
message="当前影响预览存在保护节点边界违规,不能激活。"
|
||||
show-icon
|
||||
type="error"
|
||||
/>
|
||||
<template #footer>
|
||||
<a-space>
|
||||
<a-button @click="activateDrawerOpen = false">取消</a-button>
|
||||
<a-button
|
||||
:disabled="hasProtectionBoundaryViolation"
|
||||
type="primary"
|
||||
:loading="activating"
|
||||
@click="confirmActivateChain"
|
||||
>
|
||||
确认激活
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-drawer>
|
||||
</Page>
|
||||
</template>
|
||||
@ -0,0 +1,822 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 元结构详情与草稿编辑页
|
||||
* 提供 active 字段展示、草稿 JSON 编辑、校验、影响预览、发布确认和灰度规则配置。
|
||||
*/
|
||||
import type { TableColumnsType } from 'ant-design-vue';
|
||||
import type { LocationQueryValue } from 'vue-router';
|
||||
import type {
|
||||
GrayRuleStrategy,
|
||||
MetaField,
|
||||
MetaFieldType,
|
||||
MetaSchemaDetail,
|
||||
MetaSchemaImpactPreview,
|
||||
MetaSchemaPublishResponse,
|
||||
MetaSchemaScope,
|
||||
MetaSchemaVersionDetail,
|
||||
ValidationResult,
|
||||
} from '#/api/muse/governance/meta-schema';
|
||||
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
activateMetaSchemaVersionApi,
|
||||
getMetaSchemaApi,
|
||||
getMetaSchemaVersionApi,
|
||||
previewMetaSchemaDraftImpactApi,
|
||||
publishMetaSchemaDraftApi,
|
||||
saveMetaSchemaDraftApi,
|
||||
setMetaSchemaGrayRulesApi,
|
||||
validateMetaSchemaDraftApi,
|
||||
} from '#/api/muse/governance/meta-schema';
|
||||
|
||||
defineOptions({ name: 'MuseGovernanceMetaSchemaDetail' });
|
||||
|
||||
/** 字段类型白名单,用于草稿 JSON 的最低限度校验。 */
|
||||
const FIELD_TYPE_SET = new Set<MetaFieldType>([
|
||||
'boolean',
|
||||
'date',
|
||||
'enum',
|
||||
'json',
|
||||
'number',
|
||||
'relation',
|
||||
'string',
|
||||
'text',
|
||||
]);
|
||||
|
||||
/** 字段作用域白名单,用于草稿 JSON 的最低限度校验。 */
|
||||
const FIELD_SCOPE_SET = new Set<MetaSchemaScope>(['global', 'tenant', 'user', 'work']);
|
||||
|
||||
/** 灰度策略选项。 */
|
||||
const GRAY_STRATEGY_OPTIONS: Array<{ label: string; value: GrayRuleStrategy }> = [
|
||||
{ label: '租户白名单', value: 'tenant_whitelist' },
|
||||
{ label: '百分比', value: 'percentage' },
|
||||
{ label: '用户白名单', value: 'user_whitelist' },
|
||||
];
|
||||
|
||||
/** active 字段表格列。 */
|
||||
const activeFieldColumns: TableColumnsType<MetaField> = [
|
||||
{ dataIndex: 'fieldKey', minWidth: 160, title: 'fieldKey' },
|
||||
{ dataIndex: 'displayName', minWidth: 160, title: '显示名' },
|
||||
{ dataIndex: 'fieldType', minWidth: 120, title: '类型' },
|
||||
{ dataIndex: 'scope', minWidth: 100, title: '作用域' },
|
||||
{ dataIndex: 'required', minWidth: 90, title: '必填' },
|
||||
{ dataIndex: 'protectionNodeBound', minWidth: 140, title: '保护节点边界' },
|
||||
];
|
||||
|
||||
interface VersionHistoryRow {
|
||||
/** 版本号。 */
|
||||
version: number;
|
||||
/** 版本状态。 */
|
||||
status: string;
|
||||
/** 发布时间。 */
|
||||
publishedAt?: string;
|
||||
/** 版本说明。 */
|
||||
note: string;
|
||||
}
|
||||
|
||||
/** 版本历史表格列。 */
|
||||
const versionColumns: TableColumnsType<VersionHistoryRow> = [
|
||||
{ dataIndex: 'version', minWidth: 100, title: '版本' },
|
||||
{ dataIndex: 'status', minWidth: 120, title: '状态' },
|
||||
{ dataIndex: 'publishedAt', minWidth: 180, title: '发布时间' },
|
||||
{ dataIndex: 'note', minWidth: 220, title: '说明' },
|
||||
];
|
||||
|
||||
const route = useRoute();
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const validating = ref(false);
|
||||
const previewing = ref(false);
|
||||
const publishing = ref(false);
|
||||
const activating = ref(false);
|
||||
const graySaving = ref(false);
|
||||
const publishDrawerOpen = ref(false);
|
||||
const grayDrawerOpen = ref(false);
|
||||
const detail = ref<MetaSchemaDetail>();
|
||||
const selectedVersion = ref<MetaSchemaVersionDetail>();
|
||||
const validationResult = ref<ValidationResult>();
|
||||
const impactPreview = ref<MetaSchemaImpactPreview>();
|
||||
const publishResult = ref<MetaSchemaPublishResponse>();
|
||||
const draftVersion = ref<number>();
|
||||
const draftFieldsJson = ref('[]');
|
||||
|
||||
/** 基础表单状态,schemaKey 在新建草稿时由管理员显式输入。 */
|
||||
const baseForm = reactive({
|
||||
description: '',
|
||||
displayName: '',
|
||||
expectedVersion: undefined as number | undefined,
|
||||
schemaKey: '',
|
||||
});
|
||||
|
||||
/** 发布确认表单。 */
|
||||
const publishForm = reactive({
|
||||
expectedVersion: undefined as number | undefined,
|
||||
impactPreviewId: '',
|
||||
reason: '',
|
||||
validationResultId: '',
|
||||
});
|
||||
|
||||
/** 激活与灰度规则表单。 */
|
||||
const grayForm = reactive({
|
||||
activateMode: 'full' as 'full' | 'gray',
|
||||
commandReason: '',
|
||||
expectedActiveVersion: undefined as number | undefined,
|
||||
impactPreviewId: '',
|
||||
percentage: 0,
|
||||
strategy: 'percentage' as GrayRuleStrategy,
|
||||
tenantWhitelistText: '',
|
||||
userWhitelistText: '',
|
||||
validationResultId: '',
|
||||
version: undefined as number | undefined,
|
||||
});
|
||||
|
||||
/** 获取 query 中第一个字符串值。 */
|
||||
function firstQueryValue(value: LocationQueryValue | LocationQueryValue[] | undefined) {
|
||||
if (Array.isArray(value)) {
|
||||
return value[0] ?? '';
|
||||
}
|
||||
return value ?? '';
|
||||
}
|
||||
|
||||
/** 生成命令幂等键。 */
|
||||
function createCommandId(action: string) {
|
||||
return `muse-admin-meta-schema-${action}-${Date.now()}`;
|
||||
}
|
||||
|
||||
/** 判断 unknown 是否为对象记录。 */
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/** 判断字段类型是否符合 OpenAPI 枚举。 */
|
||||
function isMetaFieldType(value: unknown): value is MetaFieldType {
|
||||
return typeof value === 'string' && FIELD_TYPE_SET.has(value as MetaFieldType);
|
||||
}
|
||||
|
||||
/** 判断字段作用域是否符合 OpenAPI 枚举。 */
|
||||
function isMetaSchemaScope(value: unknown): value is MetaSchemaScope {
|
||||
return typeof value === 'string' && FIELD_SCOPE_SET.has(value as MetaSchemaScope);
|
||||
}
|
||||
|
||||
/** 只保留字符串数组。 */
|
||||
function toStringArray(value: unknown) {
|
||||
return Array.isArray(value) && value.every((item) => typeof item === 'string')
|
||||
? value
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** 从对象中解析布尔值,非布尔值不提交。 */
|
||||
function toOptionalBoolean(value: unknown) {
|
||||
return typeof value === 'boolean' ? value : undefined;
|
||||
}
|
||||
|
||||
/** 解析字段可见性策略。 */
|
||||
function parseVisibility(value: unknown) {
|
||||
if (!isRecord(value)) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
aiContext: toOptionalBoolean(value.aiContext),
|
||||
exportable: toOptionalBoolean(value.exportable),
|
||||
uiVisible: toOptionalBoolean(value.uiVisible),
|
||||
userEditable: toOptionalBoolean(value.userEditable),
|
||||
userSearchable: toOptionalBoolean(value.userSearchable),
|
||||
};
|
||||
}
|
||||
|
||||
/** 解析字段废弃信息。 */
|
||||
function parseDeprecatedInfo(value: unknown) {
|
||||
if (!isRecord(value)) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
migrationHint:
|
||||
typeof value.migrationHint === 'string' ? value.migrationHint : undefined,
|
||||
replacementFieldKey:
|
||||
typeof value.replacementFieldKey === 'string'
|
||||
? value.replacementFieldKey
|
||||
: undefined,
|
||||
retentionPeriod:
|
||||
typeof value.retentionPeriod === 'string' ? value.retentionPeriod : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** 将草稿 JSON 转成字段列表,避免把无效 JSON 直接提交给后端。 */
|
||||
function parseDraftFieldsJson() {
|
||||
const parsed: unknown = JSON.parse(draftFieldsJson.value);
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error('草稿 JSON 必须是字段数组');
|
||||
}
|
||||
return parsed.map((item, index): MetaField => {
|
||||
if (!isRecord(item)) {
|
||||
throw new Error(`第 ${index + 1} 个字段必须是对象`);
|
||||
}
|
||||
if (typeof item.fieldKey !== 'string' || item.fieldKey.length === 0) {
|
||||
throw new Error(`第 ${index + 1} 个字段缺少 fieldKey`);
|
||||
}
|
||||
if (typeof item.displayName !== 'string' || item.displayName.length === 0) {
|
||||
throw new Error(`第 ${index + 1} 个字段缺少 displayName`);
|
||||
}
|
||||
if (!isMetaFieldType(item.fieldType)) {
|
||||
throw new Error(`第 ${index + 1} 个字段 fieldType 不合法`);
|
||||
}
|
||||
return {
|
||||
defaultValue: item.defaultValue,
|
||||
deprecated: toOptionalBoolean(item.deprecated),
|
||||
deprecatedInfo: parseDeprecatedInfo(item.deprecatedInfo),
|
||||
displayName: item.displayName,
|
||||
enumValues: toStringArray(item.enumValues),
|
||||
fieldKey: item.fieldKey,
|
||||
fieldType: item.fieldType,
|
||||
inheritedFrom:
|
||||
typeof item.inheritedFrom === 'string' ? item.inheritedFrom : undefined,
|
||||
protectionNodeBound: toOptionalBoolean(item.protectionNodeBound),
|
||||
required: toOptionalBoolean(item.required),
|
||||
scope: isMetaSchemaScope(item.scope) ? item.scope : undefined,
|
||||
validationRules: isRecord(item.validationRules) ? item.validationRules : undefined,
|
||||
visibility: parseVisibility(item.visibility),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** 灰度名单文本转数组。 */
|
||||
function splitWhitelist(text: string) {
|
||||
return text
|
||||
.split(/[\n,]/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/** 当前可用 schemaKey。 */
|
||||
const currentSchemaKey = computed(() => baseForm.schemaKey.trim());
|
||||
|
||||
/** active 字段列表。 */
|
||||
const activeFields = computed(() => detail.value?.fields ?? []);
|
||||
|
||||
/** 基于详情契约构建最小版本历史,不伪造后端未返回的历史版本。 */
|
||||
const versionRows = computed<VersionHistoryRow[]>(() => {
|
||||
const rows: VersionHistoryRow[] = [];
|
||||
if (detail.value?.activeVersion) {
|
||||
rows.push({
|
||||
note: '当前线上 active 版本',
|
||||
publishedAt: detail.value.activeVersionPublishedAt,
|
||||
status: 'active',
|
||||
version: detail.value.activeVersion,
|
||||
});
|
||||
}
|
||||
if (
|
||||
detail.value?.grayVersion &&
|
||||
detail.value.grayVersion !== detail.value.activeVersion
|
||||
) {
|
||||
rows.push({
|
||||
note: detail.value.grayRules?.scope ?? '灰度范围由后端规则决定',
|
||||
publishedAt: detail.value.grayVersionPublishedAt,
|
||||
status: 'gray',
|
||||
version: detail.value.grayVersion,
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
});
|
||||
|
||||
/** 同步详情数据到编辑表单。 */
|
||||
function syncDetailToForm(data: MetaSchemaDetail) {
|
||||
baseForm.schemaKey = data.schemaKey;
|
||||
baseForm.displayName = data.displayName;
|
||||
baseForm.description = data.description ?? '';
|
||||
baseForm.expectedVersion = data.activeVersion;
|
||||
draftFieldsJson.value = JSON.stringify(data.fields ?? [], null, 2);
|
||||
grayForm.expectedActiveVersion = data.activeVersion;
|
||||
grayForm.version = data.grayVersion ?? data.activeVersion;
|
||||
}
|
||||
|
||||
/** 加载 MetaSchema 详情。 */
|
||||
async function loadDetail() {
|
||||
const schemaKey = firstQueryValue(route.query.schemaKey);
|
||||
if (!schemaKey) {
|
||||
draftFieldsJson.value = '[]';
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getMetaSchemaApi(schemaKey);
|
||||
detail.value = data;
|
||||
syncDetailToForm(data);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 选择版本并拉取版本详情。 */
|
||||
async function handleSelectVersion(row: VersionHistoryRow) {
|
||||
if (!currentSchemaKey.value) {
|
||||
return;
|
||||
}
|
||||
selectedVersion.value = await getMetaSchemaVersionApi(
|
||||
currentSchemaKey.value,
|
||||
row.version,
|
||||
);
|
||||
}
|
||||
|
||||
/** Ant Design Vue 表格行事件,保持行数据类型可追踪。 */
|
||||
function createVersionRowHandlers(record: VersionHistoryRow) {
|
||||
return {
|
||||
onClick: () => handleSelectVersion(record),
|
||||
};
|
||||
}
|
||||
|
||||
/** 保存草稿。 */
|
||||
async function handleSaveDraft() {
|
||||
if (!currentSchemaKey.value) {
|
||||
message.warning('请先填写 schemaKey');
|
||||
return;
|
||||
}
|
||||
let fields: MetaField[];
|
||||
try {
|
||||
fields = parseDraftFieldsJson();
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '草稿 JSON 解析失败');
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
const response = await saveMetaSchemaDraftApi(currentSchemaKey.value, {
|
||||
commandId: createCommandId('save-draft'),
|
||||
description: baseForm.description,
|
||||
displayName: baseForm.displayName,
|
||||
expectedVersion: baseForm.expectedVersion,
|
||||
fields,
|
||||
});
|
||||
draftVersion.value = response.draftVersion;
|
||||
validationResult.value = response.validationSummary;
|
||||
message.success('草稿已保存');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 校验草稿。 */
|
||||
async function handleValidateDraft() {
|
||||
if (!currentSchemaKey.value || !draftVersion.value) {
|
||||
message.warning('请先保存草稿获取 draftVersion');
|
||||
return;
|
||||
}
|
||||
validating.value = true;
|
||||
try {
|
||||
validationResult.value = await validateMetaSchemaDraftApi(
|
||||
currentSchemaKey.value,
|
||||
draftVersion.value,
|
||||
);
|
||||
message.success(validationResult.value.valid ? '校验通过' : '校验完成,请查看问题');
|
||||
} finally {
|
||||
validating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取影响预览,只展示聚合影响,不展示用户私有作品正文。 */
|
||||
async function handlePreviewImpact() {
|
||||
if (!currentSchemaKey.value || !draftVersion.value) {
|
||||
message.warning('请先保存草稿获取 draftVersion');
|
||||
return;
|
||||
}
|
||||
previewing.value = true;
|
||||
try {
|
||||
impactPreview.value = await previewMetaSchemaDraftImpactApi(
|
||||
currentSchemaKey.value,
|
||||
draftVersion.value,
|
||||
);
|
||||
publishForm.impactPreviewId = impactPreview.value.impactPreviewId ?? '';
|
||||
grayForm.impactPreviewId = impactPreview.value.impactPreviewId ?? '';
|
||||
message.success('影响预览已生成');
|
||||
} finally {
|
||||
previewing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开发版确认抽屉。 */
|
||||
function openPublishDrawer() {
|
||||
publishForm.expectedVersion = baseForm.expectedVersion;
|
||||
publishForm.impactPreviewId = impactPreview.value?.impactPreviewId ?? '';
|
||||
publishDrawerOpen.value = true;
|
||||
}
|
||||
|
||||
/** 发布草稿为版本,发布不会隐式激活。 */
|
||||
async function handlePublishDraft() {
|
||||
if (!currentSchemaKey.value || !draftVersion.value) {
|
||||
message.warning('请先保存草稿获取 draftVersion');
|
||||
return;
|
||||
}
|
||||
if (!publishForm.reason || !publishForm.validationResultId || !publishForm.impactPreviewId) {
|
||||
message.warning('请填写发布理由、校验结果 ID 和影响预览 ID');
|
||||
return;
|
||||
}
|
||||
publishing.value = true;
|
||||
try {
|
||||
publishResult.value = await publishMetaSchemaDraftApi(
|
||||
currentSchemaKey.value,
|
||||
draftVersion.value,
|
||||
{
|
||||
autoActivate: false,
|
||||
commandId: createCommandId('publish'),
|
||||
expectedVersion: publishForm.expectedVersion ?? baseForm.expectedVersion ?? 0,
|
||||
impactPreviewId: publishForm.impactPreviewId,
|
||||
reason: publishForm.reason,
|
||||
validationResultId: publishForm.validationResultId,
|
||||
},
|
||||
);
|
||||
publishDrawerOpen.value = false;
|
||||
message.success('元结构版本已发布,仍需单独激活或灰度');
|
||||
} finally {
|
||||
publishing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开激活和灰度规则抽屉。 */
|
||||
function openGrayDrawer() {
|
||||
grayForm.version =
|
||||
publishResult.value?.publishedVersion ?? detail.value?.grayVersion ?? detail.value?.activeVersion;
|
||||
grayForm.expectedActiveVersion = detail.value?.activeVersion ?? baseForm.expectedVersion;
|
||||
grayForm.impactPreviewId = impactPreview.value?.impactPreviewId ?? '';
|
||||
grayDrawerOpen.value = true;
|
||||
}
|
||||
|
||||
/** 构建灰度规则配置。 */
|
||||
function buildGrayRules() {
|
||||
return {
|
||||
percentage: grayForm.strategy === 'percentage' ? grayForm.percentage : undefined,
|
||||
strategy: grayForm.strategy,
|
||||
tenantWhitelist:
|
||||
grayForm.strategy === 'tenant_whitelist'
|
||||
? splitWhitelist(grayForm.tenantWhitelistText)
|
||||
: undefined,
|
||||
userWhitelist:
|
||||
grayForm.strategy === 'user_whitelist'
|
||||
? splitWhitelist(grayForm.userWhitelistText)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** 激活版本;灰度模式会携带灰度规则。 */
|
||||
async function handleActivateVersion() {
|
||||
if (!currentSchemaKey.value || !grayForm.version) {
|
||||
message.warning('请先选择要激活的版本');
|
||||
return;
|
||||
}
|
||||
if (!grayForm.commandReason || !grayForm.validationResultId || !grayForm.impactPreviewId) {
|
||||
message.warning('请填写激活理由、校验结果 ID 和影响预览 ID');
|
||||
return;
|
||||
}
|
||||
activating.value = true;
|
||||
try {
|
||||
await activateMetaSchemaVersionApi(currentSchemaKey.value, grayForm.version, {
|
||||
activateMode: grayForm.activateMode,
|
||||
commandId: createCommandId('activate'),
|
||||
expectedActiveVersion: grayForm.expectedActiveVersion ?? baseForm.expectedVersion ?? 0,
|
||||
grayRules: grayForm.activateMode === 'gray' ? buildGrayRules() : undefined,
|
||||
impactPreviewId: grayForm.impactPreviewId,
|
||||
reason: grayForm.commandReason,
|
||||
validationResultId: grayForm.validationResultId,
|
||||
});
|
||||
grayDrawerOpen.value = false;
|
||||
message.success('版本激活命令已提交');
|
||||
await loadDetail();
|
||||
} finally {
|
||||
activating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 单独设置或调整灰度规则。 */
|
||||
async function handleSaveGrayRules() {
|
||||
if (!currentSchemaKey.value || !grayForm.version) {
|
||||
message.warning('请先选择版本');
|
||||
return;
|
||||
}
|
||||
if (!grayForm.commandReason) {
|
||||
message.warning('请填写调整理由');
|
||||
return;
|
||||
}
|
||||
graySaving.value = true;
|
||||
try {
|
||||
await setMetaSchemaGrayRulesApi(currentSchemaKey.value, grayForm.version, {
|
||||
action: 'set',
|
||||
commandId: createCommandId('gray-rules'),
|
||||
grayRules: buildGrayRules(),
|
||||
reason: grayForm.commandReason,
|
||||
});
|
||||
message.success('灰度规则已提交');
|
||||
await loadDetail();
|
||||
} finally {
|
||||
graySaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 发布前二次确认。 */
|
||||
function confirmPublishDraft() {
|
||||
Modal.confirm({
|
||||
content: '发布会改变后续结构表达,但不会修改用户已有私有作品正文。确认继续?',
|
||||
onOk: handlePublishDraft,
|
||||
title: '确认发布 MetaSchema 版本',
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(loadDetail);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<a-alert
|
||||
class="mb-4"
|
||||
message="治理边界:本页只管理 MetaSchema 定义、版本和聚合影响,不展示或修改用户私有作品正文。"
|
||||
show-icon
|
||||
type="info"
|
||||
/>
|
||||
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col :lg="10" :xs="24">
|
||||
<a-card :loading="loading" title="基础信息">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="schemaKey">
|
||||
<a-input v-model:value="baseForm.schemaKey" placeholder="例如 work.story" />
|
||||
</a-form-item>
|
||||
<a-form-item label="显示名称">
|
||||
<a-input v-model:value="baseForm.displayName" placeholder="请输入显示名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="说明">
|
||||
<a-textarea
|
||||
v-model:value="baseForm.description"
|
||||
:auto-size="{ minRows: 3, maxRows: 5 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="期望 activeVersion">
|
||||
<a-input-number
|
||||
v-model:value="baseForm.expectedVersion"
|
||||
class="w-full"
|
||||
:min="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :lg="14" :xs="24">
|
||||
<a-card title="版本历史">
|
||||
<a-table
|
||||
:columns="versionColumns"
|
||||
:data-source="versionRows"
|
||||
:pagination="false"
|
||||
row-key="version"
|
||||
size="small"
|
||||
@row="createVersionRowHandlers"
|
||||
/>
|
||||
<a-descriptions
|
||||
v-if="selectedVersion"
|
||||
class="mt-4"
|
||||
bordered
|
||||
size="small"
|
||||
:column="2"
|
||||
>
|
||||
<a-descriptions-item label="版本">
|
||||
{{ selectedVersion.version }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
{{ selectedVersion.status }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="发布人">
|
||||
{{ selectedVersion.publishedBy ?? '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="发布时间">
|
||||
{{ selectedVersion.publishedAt ?? '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="24">
|
||||
<a-card title="active 字段">
|
||||
<a-table
|
||||
:columns="activeFieldColumns"
|
||||
:data-source="activeFields"
|
||||
:pagination="false"
|
||||
row-key="fieldKey"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'required'">
|
||||
<a-tag :color="record.required ? 'red' : 'default'">
|
||||
{{ record.required ? '必填' : '可选' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'protectionNodeBound'">
|
||||
<a-tag :color="record.protectionNodeBound ? 'orange' : 'default'">
|
||||
{{ record.protectionNodeBound ? '已绑定' : '未绑定' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :lg="14" :xs="24">
|
||||
<a-card title="草稿 JSON 编辑">
|
||||
<a-textarea
|
||||
v-model:value="draftFieldsJson"
|
||||
:auto-size="{ minRows: 18, maxRows: 28 }"
|
||||
class="font-mono"
|
||||
/>
|
||||
<a-space class="mt-4" wrap>
|
||||
<a-button :loading="saving" type="primary" @click="handleSaveDraft">
|
||||
保存草稿
|
||||
</a-button>
|
||||
<a-button :disabled="!draftVersion" :loading="validating" @click="handleValidateDraft">
|
||||
校验草稿
|
||||
</a-button>
|
||||
<a-button :disabled="!draftVersion" :loading="previewing" @click="handlePreviewImpact">
|
||||
影响预览
|
||||
</a-button>
|
||||
<a-button :disabled="!draftVersion" danger @click="openPublishDrawer">
|
||||
发布确认
|
||||
</a-button>
|
||||
<a-button @click="openGrayDrawer">灰度规则 / 激活</a-button>
|
||||
</a-space>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :lg="10" :xs="24">
|
||||
<a-card title="校验结果">
|
||||
<a-result
|
||||
v-if="!validationResult"
|
||||
status="info"
|
||||
sub-title="保存草稿后执行校验"
|
||||
title="暂无校验结果"
|
||||
/>
|
||||
<template v-else>
|
||||
<a-alert
|
||||
:message="validationResult.valid ? '校验通过' : '校验未通过'"
|
||||
show-icon
|
||||
:type="validationResult.valid ? 'success' : 'error'"
|
||||
/>
|
||||
<a-list
|
||||
class="mt-4"
|
||||
size="small"
|
||||
:data-source="validationResult.errors ?? []"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-tag color="red">{{ item.code }}</a-tag>
|
||||
{{ item.fieldKey ? `${item.fieldKey}: ` : '' }}{{ item.message }}
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
<a-list
|
||||
class="mt-4"
|
||||
size="small"
|
||||
:data-source="validationResult.warnings ?? []"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-tag color="orange">{{ item.code }}</a-tag>
|
||||
{{ item.fieldKey ? `${item.fieldKey}: ` : '' }}{{ item.message }}
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</template>
|
||||
</a-card>
|
||||
|
||||
<a-card class="mt-4" title="影响预览">
|
||||
<a-result
|
||||
v-if="!impactPreview"
|
||||
status="info"
|
||||
sub-title="影响预览只返回聚合摘要"
|
||||
title="暂无影响预览"
|
||||
/>
|
||||
<a-descriptions v-else bordered size="small" :column="1">
|
||||
<a-descriptions-item label="previewId">
|
||||
{{ impactPreview.impactPreviewId ?? '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="受影响作品">
|
||||
{{ impactPreview.workImpact?.affectedWorkCount ?? 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="需重建投影">
|
||||
{{ impactPreview.knowledgeProjectionImpact?.projectionsNeedingRebuild ?? 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="AI 上下文影响">
|
||||
{{ impactPreview.aiContextImpact?.affectedAIContexts ?? 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="导出策略影响">
|
||||
{{ impactPreview.exportImpact?.affectedExportPolicies ?? 0 }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-drawer
|
||||
v-model:open="publishDrawerOpen"
|
||||
destroy-on-close
|
||||
title="发布确认"
|
||||
width="520"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="发布理由">
|
||||
<a-textarea
|
||||
v-model:value="publishForm.reason"
|
||||
:auto-size="{ minRows: 3, maxRows: 5 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="validationResultId">
|
||||
<a-input v-model:value="publishForm.validationResultId" />
|
||||
</a-form-item>
|
||||
<a-form-item label="impactPreviewId">
|
||||
<a-input v-model:value="publishForm.impactPreviewId" />
|
||||
</a-form-item>
|
||||
<a-form-item label="expectedVersion">
|
||||
<a-input-number
|
||||
v-model:value="publishForm.expectedVersion"
|
||||
class="w-full"
|
||||
:min="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<a-space>
|
||||
<a-button @click="publishDrawerOpen = false">取消</a-button>
|
||||
<a-button danger :loading="publishing" @click="confirmPublishDraft">
|
||||
确认发布
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-drawer>
|
||||
|
||||
<a-drawer
|
||||
v-model:open="grayDrawerOpen"
|
||||
destroy-on-close
|
||||
title="灰度规则与版本激活"
|
||||
width="560"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="版本号">
|
||||
<a-input-number v-model:value="grayForm.version" class="w-full" :min="0" />
|
||||
</a-form-item>
|
||||
<a-form-item label="理由">
|
||||
<a-textarea
|
||||
v-model:value="grayForm.commandReason"
|
||||
:auto-size="{ minRows: 3, maxRows: 5 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="激活模式">
|
||||
<a-radio-group v-model:value="grayForm.activateMode">
|
||||
<a-radio-button value="full">全量</a-radio-button>
|
||||
<a-radio-button value="gray">灰度</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="灰度策略">
|
||||
<a-select v-model:value="grayForm.strategy" :options="GRAY_STRATEGY_OPTIONS" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="grayForm.strategy === 'percentage'" label="灰度百分比">
|
||||
<a-input-number v-model:value="grayForm.percentage" class="w-full" :max="100" :min="0" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="grayForm.strategy === 'tenant_whitelist'" label="租户白名单">
|
||||
<a-textarea
|
||||
v-model:value="grayForm.tenantWhitelistText"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="grayForm.strategy === 'user_whitelist'" label="用户白名单">
|
||||
<a-textarea
|
||||
v-model:value="grayForm.userWhitelistText"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="validationResultId">
|
||||
<a-input v-model:value="grayForm.validationResultId" />
|
||||
</a-form-item>
|
||||
<a-form-item label="impactPreviewId">
|
||||
<a-input v-model:value="grayForm.impactPreviewId" />
|
||||
</a-form-item>
|
||||
<a-form-item label="expectedActiveVersion">
|
||||
<a-input-number
|
||||
v-model:value="grayForm.expectedActiveVersion"
|
||||
class="w-full"
|
||||
:min="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<a-space>
|
||||
<a-button @click="grayDrawerOpen = false">取消</a-button>
|
||||
<a-button :loading="graySaving" @click="handleSaveGrayRules">
|
||||
保存灰度规则
|
||||
</a-button>
|
||||
<a-button type="primary" :loading="activating" @click="handleActivateVersion">
|
||||
激活版本
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-drawer>
|
||||
</Page>
|
||||
</template>
|
||||
@ -0,0 +1,291 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 元结构列表页
|
||||
* 管理 MetaSchema 的版本、状态、影响摘要和草稿入口。
|
||||
*/
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type {
|
||||
MetaFieldType,
|
||||
MetaSchemaMigrationRisk,
|
||||
MetaSchemaScope,
|
||||
MetaSchemaStatus,
|
||||
MetaSchemaSummary,
|
||||
} from '#/api/muse/governance/meta-schema';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { listMetaSchemasApi } from '#/api/muse/governance/meta-schema';
|
||||
import { router } from '#/router';
|
||||
|
||||
defineOptions({ name: 'MuseGovernanceMetaSchemaList' });
|
||||
|
||||
/** 作用域展示文案。 */
|
||||
const SCOPE_LABEL_MAP: Record<MetaSchemaScope, string> = {
|
||||
global: '全局',
|
||||
tenant: '租户',
|
||||
user: '用户',
|
||||
work: '作品',
|
||||
};
|
||||
|
||||
/** 字段类型展示文案。 */
|
||||
const FIELD_TYPE_LABEL_MAP: Record<MetaFieldType, string> = {
|
||||
boolean: '布尔',
|
||||
date: '日期',
|
||||
enum: '枚举',
|
||||
json: 'JSON',
|
||||
number: '数值',
|
||||
relation: '关系',
|
||||
string: '短文本',
|
||||
text: '长文本',
|
||||
};
|
||||
|
||||
/** 状态标签展示配置。 */
|
||||
const STATUS_META_MAP: Record<MetaSchemaStatus, { color: string; label: string }> = {
|
||||
active: { color: 'green', label: '已激活' },
|
||||
archived: { color: 'default', label: '已归档' },
|
||||
deprecated: { color: 'orange', label: '已废弃' },
|
||||
draft: { color: 'blue', label: '草稿' },
|
||||
published: { color: 'cyan', label: '已发布待激活' },
|
||||
};
|
||||
|
||||
/** 迁移风险展示配置。 */
|
||||
const RISK_META_MAP: Record<MetaSchemaMigrationRisk, { color: string; label: string }> = {
|
||||
high: { color: 'red', label: '高风险' },
|
||||
low: { color: 'green', label: '低风险' },
|
||||
medium: { color: 'orange', label: '中风险' },
|
||||
none: { color: 'default', label: '无风险' },
|
||||
unknown: { color: 'default', label: '未预览' },
|
||||
};
|
||||
|
||||
/** 列表筛选表单。 */
|
||||
const gridFormSchema: VbenFormSchema[] = [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入 schemaKey 或显示名称',
|
||||
},
|
||||
fieldName: 'keyword',
|
||||
label: '关键词',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '全局', value: 'global' },
|
||||
{ label: '租户', value: 'tenant' },
|
||||
{ label: '用户', value: 'user' },
|
||||
{ label: '作品', value: 'work' },
|
||||
],
|
||||
placeholder: '请选择作用域',
|
||||
},
|
||||
fieldName: 'scope',
|
||||
label: '作用域',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: 'work / chapter / block / agent',
|
||||
},
|
||||
fieldName: 'targetType',
|
||||
label: '目标类型',
|
||||
},
|
||||
];
|
||||
|
||||
/** 推导列表状态;后端未返回 status 时不猜发布事实,只基于版本字段做运营展示。 */
|
||||
function resolveSchemaStatus(row: MetaSchemaSummary): MetaSchemaStatus {
|
||||
if (row.status) {
|
||||
return row.status;
|
||||
}
|
||||
if (row.draftVersion && row.draftVersion > row.activeVersion) {
|
||||
return 'draft';
|
||||
}
|
||||
if (row.grayVersion) {
|
||||
return 'published';
|
||||
}
|
||||
return 'active';
|
||||
}
|
||||
|
||||
/** 渲染影响摘要,不展示用户私有作品正文。 */
|
||||
function formatImpactSummary(row: MetaSchemaSummary) {
|
||||
const summary = row.impactSummary;
|
||||
if (!summary) {
|
||||
return '需进入详情预览';
|
||||
}
|
||||
return [
|
||||
`作品 ${summary.affectedWorkCount ?? 0}`,
|
||||
`投影 ${summary.affectedProjectionCount ?? 0}`,
|
||||
`AI 上下文 ${summary.affectedAIContextCount ?? 0}`,
|
||||
].join(' / ');
|
||||
}
|
||||
|
||||
/** 进入详情页。 */
|
||||
function handleViewDetail(row: MetaSchemaSummary) {
|
||||
router.push({
|
||||
path: '/muse/governance/meta-schema/detail',
|
||||
query: { schemaKey: row.schemaKey },
|
||||
});
|
||||
}
|
||||
|
||||
/** 新建草稿入口,详情页负责填写 schemaKey 和字段 JSON。 */
|
||||
function handleCreateDraft() {
|
||||
router.push({
|
||||
path: '/muse/governance/meta-schema/detail',
|
||||
query: { mode: 'create' },
|
||||
});
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: gridFormSchema,
|
||||
},
|
||||
gridOptions: {
|
||||
columns: [
|
||||
{
|
||||
field: 'schemaKey',
|
||||
minWidth: 180,
|
||||
title: 'schemaKey',
|
||||
},
|
||||
{
|
||||
field: 'displayName',
|
||||
minWidth: 180,
|
||||
title: '显示名称',
|
||||
},
|
||||
{
|
||||
field: 'fieldType',
|
||||
minWidth: 120,
|
||||
slots: { default: 'fieldType' },
|
||||
title: '字段类型',
|
||||
},
|
||||
{
|
||||
field: 'scope',
|
||||
minWidth: 100,
|
||||
slots: { default: 'scope' },
|
||||
title: '作用域',
|
||||
},
|
||||
{
|
||||
field: 'activeVersion',
|
||||
minWidth: 120,
|
||||
title: 'activeVersion',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
minWidth: 130,
|
||||
slots: { default: 'status' },
|
||||
title: '状态',
|
||||
},
|
||||
{
|
||||
field: 'impactSummary',
|
||||
minWidth: 260,
|
||||
slots: { default: 'impactSummary' },
|
||||
title: '迁移风险 / 影响摘要',
|
||||
},
|
||||
{
|
||||
field: 'updatedAt',
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 180,
|
||||
title: '更新时间',
|
||||
},
|
||||
{
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
title: '操作',
|
||||
width: 120,
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await listMetaSchemasApi({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
isHover: true,
|
||||
keyField: 'schemaKey',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<MetaSchemaSummary>,
|
||||
});
|
||||
|
||||
/** 手动刷新列表。 */
|
||||
function handleRefresh() {
|
||||
gridApi.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Grid table-title="元结构定义">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '新建草稿',
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
onClick: handleCreateDraft,
|
||||
},
|
||||
{
|
||||
label: '刷新',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.REFRESH,
|
||||
onClick: handleRefresh,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #fieldType="{ row }">
|
||||
<span>{{ row.fieldType ? FIELD_TYPE_LABEL_MAP[row.fieldType] : '字段集合' }}</span>
|
||||
</template>
|
||||
|
||||
<template #scope="{ row }">
|
||||
<a-tag>{{ SCOPE_LABEL_MAP[row.scope] }}</a-tag>
|
||||
</template>
|
||||
|
||||
<template #status="{ row }">
|
||||
<a-tag :color="STATUS_META_MAP[resolveSchemaStatus(row)].color">
|
||||
{{ STATUS_META_MAP[resolveSchemaStatus(row)].label }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template #impactSummary="{ row }">
|
||||
<div class="flex flex-col gap-1">
|
||||
<a-tag :color="RISK_META_MAP[row.migrationRisk ?? 'unknown'].color">
|
||||
{{ RISK_META_MAP[row.migrationRisk ?? 'unknown'].label }}
|
||||
</a-tag>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ formatImpactSummary(row) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '查看详情',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.VIEW,
|
||||
onClick: handleViewDetail.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
705
muse-admin/apps/web-antd/src/views/muse/jobs/index.vue
Normal file
705
muse-admin/apps/web-antd/src/views/muse/jobs/index.vue
Normal file
@ -0,0 +1,705 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 任务治理与异常观察页
|
||||
*
|
||||
* 管理员只能观察任务、查看失败原因、执行有限重试/取消和来源传播补偿。
|
||||
* 页面不提供“代用户重新生成写作候选”的入口;重试前必须完成来源、授权和幂等重验。
|
||||
*/
|
||||
import type {
|
||||
JobDetail,
|
||||
JobStatus,
|
||||
JobSummary,
|
||||
SourceEventDetail,
|
||||
SourceEventSummary,
|
||||
SourceStatus,
|
||||
} from '#/api/muse/jobs';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Descriptions,
|
||||
Form,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
cancelJobApi,
|
||||
getJobApi,
|
||||
getSourceEventApi,
|
||||
listJobsApi,
|
||||
listSourceEventsApi,
|
||||
retryJobApi,
|
||||
retrySourceEventApi,
|
||||
} from '#/api/muse/jobs';
|
||||
|
||||
/** 表格分页对象。 */
|
||||
interface TablePager {
|
||||
/** 当前页码。 */
|
||||
current?: number;
|
||||
/** 每页数量。 */
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
/** Ant Table 插槽传入的通用记录。 */
|
||||
type TableRecord = Record<string, unknown>;
|
||||
|
||||
/** 任务操作类型。 */
|
||||
type JobAction = 'cancel' | 'recheck' | 'retry';
|
||||
|
||||
/** 任务操作表单。 */
|
||||
interface JobActionForm {
|
||||
/** 操作类型。 */
|
||||
action: JobAction;
|
||||
/** 目标任务 ID。 */
|
||||
jobId: number | string;
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
/** 操作原因。 */
|
||||
reason: string;
|
||||
/** owner 权限是否已重验。 */
|
||||
ownerChecked: boolean;
|
||||
/** 来源状态是否已重验。 */
|
||||
sourceChecked: boolean;
|
||||
/** 授权快照是否已重验。 */
|
||||
authorizationChecked: boolean;
|
||||
/** 幂等键和 retry group 是否已重验。 */
|
||||
idempotencyChecked: boolean;
|
||||
}
|
||||
|
||||
/** 来源事件重试表单。 */
|
||||
interface SourceRetryForm {
|
||||
/** 目标事件 ID。 */
|
||||
eventId: number | string;
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
/** 重试原因。 */
|
||||
reason: string;
|
||||
}
|
||||
|
||||
const jobStatusColor: Record<JobStatus, string> = {
|
||||
cancelled: 'default',
|
||||
completed: 'green',
|
||||
failed: 'red',
|
||||
queued: 'blue',
|
||||
running: 'processing',
|
||||
};
|
||||
|
||||
const jobStatusText: Record<JobStatus, string> = {
|
||||
cancelled: '已取消',
|
||||
completed: '已完成',
|
||||
failed: '失败',
|
||||
queued: '排队中',
|
||||
running: '运行中',
|
||||
};
|
||||
|
||||
const sourceStatusColor: Record<SourceStatus, string> = {
|
||||
active: 'green',
|
||||
blocked: 'red',
|
||||
delisted: 'orange',
|
||||
owner_missing: 'red',
|
||||
recalled: 'red',
|
||||
revoked: 'red',
|
||||
stale: 'orange',
|
||||
unauthorized: 'red',
|
||||
};
|
||||
|
||||
/** 将 Ant Table 通用记录转回任务摘要。 */
|
||||
function asJobSummary(record: TableRecord) {
|
||||
return record as unknown as JobSummary;
|
||||
}
|
||||
|
||||
/** 将 Ant Table 通用记录转回来源事件摘要。 */
|
||||
function asSourceEventSummary(record: TableRecord) {
|
||||
return record as unknown as SourceEventSummary;
|
||||
}
|
||||
|
||||
/** 任务状态颜色,兼容模板中的 unknown 记录。 */
|
||||
function getJobStatusColor(status: unknown) {
|
||||
return jobStatusColor[status as JobStatus] ?? 'default';
|
||||
}
|
||||
|
||||
/** 任务状态文案,兼容模板中的 unknown 记录。 */
|
||||
function getJobStatusText(status: unknown) {
|
||||
return jobStatusText[status as JobStatus] ?? displayText(String(status));
|
||||
}
|
||||
|
||||
/** 来源状态颜色,兼容模板中的 unknown 记录。 */
|
||||
function getSourceStatusColor(status: unknown) {
|
||||
return sourceStatusColor[status as SourceStatus] ?? 'default';
|
||||
}
|
||||
|
||||
/** 判断任务是否已完成,用于禁用会污染事实的操作。 */
|
||||
function isCompletedStatus(status: unknown) {
|
||||
return status === 'completed';
|
||||
}
|
||||
|
||||
const jobColumns = [
|
||||
{ dataIndex: 'jobId', key: 'jobId', title: '任务 ID', width: 120 },
|
||||
{ dataIndex: 'type', key: 'type', title: '类型' },
|
||||
{ dataIndex: 'status', key: 'status', title: '状态', width: 120 },
|
||||
{ dataIndex: 'owner', key: 'owner', title: 'Owner' },
|
||||
{ dataIndex: 'createdBy', key: 'createdBy', title: '创建者' },
|
||||
{ dataIndex: 'retryCount', key: 'retryCount', title: '重试次数', width: 100 },
|
||||
{ dataIndex: 'createdAt', key: 'createdAt', title: '创建时间' },
|
||||
{ dataIndex: 'completedAt', key: 'completedAt', title: '完成时间' },
|
||||
{ key: 'actions', title: '操作', width: 280 },
|
||||
];
|
||||
|
||||
const sourceEventColumns = [
|
||||
{ dataIndex: 'eventId', key: 'eventId', title: '事件 ID', width: 120 },
|
||||
{ dataIndex: 'sourceType', key: 'sourceType', title: '来源类型' },
|
||||
{ dataIndex: 'sourceId', key: 'sourceId', title: '来源 ID' },
|
||||
{ dataIndex: 'sourceStatus', key: 'sourceStatus', title: '来源状态' },
|
||||
{ dataIndex: 'actionPolicy', key: 'actionPolicy', title: '动作策略' },
|
||||
{ dataIndex: 'affectedScope', key: 'affectedScope', title: '影响摘要' },
|
||||
{ dataIndex: 'createdAt', key: 'createdAt', title: '创建时间' },
|
||||
{ key: 'actions', title: '操作', width: 180 },
|
||||
];
|
||||
|
||||
const jobLoading = ref(false);
|
||||
const sourceLoading = ref(false);
|
||||
const detailLoading = ref(false);
|
||||
const commandSubmitting = ref(false);
|
||||
|
||||
const jobs = ref<JobSummary[]>([]);
|
||||
const sourceEvents = ref<SourceEventSummary[]>([]);
|
||||
const selectedJob = ref<JobDetail>();
|
||||
const selectedSourceEvent = ref<SourceEventDetail>();
|
||||
|
||||
const jobFilters = reactive({
|
||||
owner: '',
|
||||
status: undefined as JobStatus | undefined,
|
||||
});
|
||||
|
||||
const sourceFilters = reactive({
|
||||
sourceStatus: undefined as SourceStatus | undefined,
|
||||
sourceType: '',
|
||||
});
|
||||
|
||||
const jobPager = reactive({ current: 1, pageSize: 10, total: 0 });
|
||||
const sourcePager = reactive({ current: 1, pageSize: 10, total: 0 });
|
||||
|
||||
const jobDetailOpen = ref(false);
|
||||
const sourceDetailOpen = ref(false);
|
||||
const jobActionOpen = ref(false);
|
||||
const sourceRetryOpen = ref(false);
|
||||
|
||||
const jobActionForm = reactive<JobActionForm>({
|
||||
action: 'retry',
|
||||
authorizationChecked: false,
|
||||
commandId: '',
|
||||
idempotencyChecked: false,
|
||||
jobId: '',
|
||||
ownerChecked: false,
|
||||
reason: '',
|
||||
sourceChecked: false,
|
||||
});
|
||||
|
||||
const sourceRetryForm = reactive<SourceRetryForm>({
|
||||
commandId: '',
|
||||
eventId: '',
|
||||
reason: '',
|
||||
});
|
||||
|
||||
/** 生成任务命令幂等键。 */
|
||||
function createCommandId(prefix: string) {
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
/** 统一空值展示。 */
|
||||
function displayText(value?: number | string) {
|
||||
return value === undefined || value === '' ? '-' : String(value);
|
||||
}
|
||||
|
||||
/** 将脱敏对象摘要格式化展示。 */
|
||||
function stringifySummary(value?: Record<string, unknown>) {
|
||||
return value ? JSON.stringify(value, null, 2) : '-';
|
||||
}
|
||||
|
||||
/** 查询异步任务列表。 */
|
||||
async function loadJobs(page = jobPager.current, pageSize = jobPager.pageSize) {
|
||||
jobLoading.value = true;
|
||||
try {
|
||||
const data = await listJobsApi({
|
||||
owner: jobFilters.owner || undefined,
|
||||
pageNo: page,
|
||||
pageSize,
|
||||
status: jobFilters.status,
|
||||
});
|
||||
jobs.value = data.list;
|
||||
jobPager.current = page;
|
||||
jobPager.pageSize = pageSize;
|
||||
jobPager.total = data.total;
|
||||
} finally {
|
||||
jobLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询来源状态事件列表。 */
|
||||
async function loadSourceEvents(
|
||||
page = sourcePager.current,
|
||||
pageSize = sourcePager.pageSize,
|
||||
) {
|
||||
sourceLoading.value = true;
|
||||
try {
|
||||
const data = await listSourceEventsApi({
|
||||
pageNo: page,
|
||||
pageSize,
|
||||
sourceStatus: sourceFilters.sourceStatus,
|
||||
sourceType: sourceFilters.sourceType || undefined,
|
||||
});
|
||||
sourceEvents.value = data.list;
|
||||
sourcePager.current = page;
|
||||
sourcePager.pageSize = pageSize;
|
||||
sourcePager.total = data.total;
|
||||
} finally {
|
||||
sourceLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查看任务详情。 */
|
||||
async function openJobDetail(record: JobSummary) {
|
||||
detailLoading.value = true;
|
||||
jobDetailOpen.value = true;
|
||||
try {
|
||||
selectedJob.value = await getJobApi(record.jobId);
|
||||
} finally {
|
||||
detailLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查看来源事件详情。 */
|
||||
async function openSourceDetail(record: SourceEventSummary) {
|
||||
detailLoading.value = true;
|
||||
sourceDetailOpen.value = true;
|
||||
try {
|
||||
selectedSourceEvent.value = await getSourceEventApi(record.eventId);
|
||||
} finally {
|
||||
detailLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 任务表格分页变化。 */
|
||||
function handleJobTableChange(pager: TablePager) {
|
||||
loadJobs(pager.current ?? 1, pager.pageSize ?? jobPager.pageSize);
|
||||
}
|
||||
|
||||
/** 来源事件表格分页变化。 */
|
||||
function handleSourceTableChange(pager: TablePager) {
|
||||
loadSourceEvents(pager.current ?? 1, pager.pageSize ?? sourcePager.pageSize);
|
||||
}
|
||||
|
||||
/** 打开任务操作弹窗。 */
|
||||
function openJobAction(record: JobSummary, action: JobAction) {
|
||||
jobActionForm.action = action;
|
||||
jobActionForm.authorizationChecked = false;
|
||||
jobActionForm.commandId = createCommandId(`job-${action}`);
|
||||
jobActionForm.idempotencyChecked = false;
|
||||
jobActionForm.jobId = record.jobId;
|
||||
jobActionForm.ownerChecked = false;
|
||||
jobActionForm.reason = '';
|
||||
jobActionForm.sourceChecked = false;
|
||||
jobActionOpen.value = true;
|
||||
}
|
||||
|
||||
/** 表格事件包装:打开任务详情。 */
|
||||
function handleOpenJobDetail(record: TableRecord) {
|
||||
return openJobDetail(asJobSummary(record));
|
||||
}
|
||||
|
||||
/** 表格事件包装:打开任务操作弹窗。 */
|
||||
function handleOpenJobAction(record: TableRecord, action: JobAction) {
|
||||
openJobAction(asJobSummary(record), action);
|
||||
}
|
||||
|
||||
/** 表格事件包装:打开来源事件详情。 */
|
||||
function handleOpenSourceDetail(record: TableRecord) {
|
||||
return openSourceDetail(asSourceEventSummary(record));
|
||||
}
|
||||
|
||||
/** 表格事件包装:打开来源传播重试。 */
|
||||
function handleOpenSourceRetry(record: TableRecord) {
|
||||
openSourceRetry(asSourceEventSummary(record));
|
||||
}
|
||||
|
||||
/** 提交任务重验、重试或取消。 */
|
||||
async function submitJobAction() {
|
||||
if (!jobActionForm.reason.trim()) {
|
||||
message.error('请填写操作原因');
|
||||
return;
|
||||
}
|
||||
if (jobActionForm.action !== 'cancel') {
|
||||
const checked =
|
||||
jobActionForm.authorizationChecked &&
|
||||
jobActionForm.idempotencyChecked &&
|
||||
jobActionForm.ownerChecked &&
|
||||
jobActionForm.sourceChecked;
|
||||
if (!checked) {
|
||||
message.error('重试前必须完成 owner、来源、授权和幂等重验');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (jobActionForm.action === 'recheck') {
|
||||
message.warning('当前契约无独立任务重验接口;已保留重验清单,不提交后端命令。');
|
||||
jobActionOpen.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
commandSubmitting.value = true;
|
||||
try {
|
||||
if (jobActionForm.action === 'retry') {
|
||||
await retryJobApi(jobActionForm.jobId, {
|
||||
commandId: jobActionForm.commandId,
|
||||
reason: jobActionForm.reason,
|
||||
});
|
||||
message.success('任务重试命令已提交');
|
||||
} else {
|
||||
await cancelJobApi(jobActionForm.jobId, {
|
||||
commandId: jobActionForm.commandId,
|
||||
reason: jobActionForm.reason,
|
||||
});
|
||||
message.success('任务取消命令已提交');
|
||||
}
|
||||
jobActionOpen.value = false;
|
||||
await loadJobs();
|
||||
} finally {
|
||||
commandSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开来源传播重试弹窗。 */
|
||||
function openSourceRetry(record: SourceEventSummary) {
|
||||
sourceRetryForm.commandId = createCommandId('source-retry');
|
||||
sourceRetryForm.eventId = record.eventId;
|
||||
sourceRetryForm.reason = '';
|
||||
sourceRetryOpen.value = true;
|
||||
}
|
||||
|
||||
/** 提交来源传播重试。 */
|
||||
async function submitSourceRetry() {
|
||||
if (!sourceRetryForm.reason.trim()) {
|
||||
message.error('请填写重试原因');
|
||||
return;
|
||||
}
|
||||
commandSubmitting.value = true;
|
||||
try {
|
||||
await retrySourceEventApi(sourceRetryForm.eventId, {
|
||||
commandId: sourceRetryForm.commandId,
|
||||
reason: sourceRetryForm.reason,
|
||||
});
|
||||
message.success('来源传播重试已提交');
|
||||
sourceRetryOpen.value = false;
|
||||
await loadSourceEvents();
|
||||
} finally {
|
||||
commandSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadJobs();
|
||||
loadSourceEvents();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="space-y-4">
|
||||
<Alert
|
||||
show-icon
|
||||
type="warning"
|
||||
message="管理员不能代用户重新生成写作候选;失败任务只能在重验通过后有限重试、取消或要求用户侧重新发起。"
|
||||
/>
|
||||
|
||||
<Tabs>
|
||||
<Tabs.TabPane key="jobs" tab="任务列表">
|
||||
<Card title="异步任务">
|
||||
<Space class="mb-4" wrap>
|
||||
<Select
|
||||
v-model:value="jobFilters.status"
|
||||
allow-clear
|
||||
placeholder="任务状态"
|
||||
style="width: 160px"
|
||||
>
|
||||
<Select.Option value="queued">排队中</Select.Option>
|
||||
<Select.Option value="running">运行中</Select.Option>
|
||||
<Select.Option value="completed">已完成</Select.Option>
|
||||
<Select.Option value="failed">失败</Select.Option>
|
||||
<Select.Option value="cancelled">已取消</Select.Option>
|
||||
</Select>
|
||||
<Input
|
||||
v-model:value="jobFilters.owner"
|
||||
allow-clear
|
||||
placeholder="owner 模块"
|
||||
style="width: 180px"
|
||||
/>
|
||||
<Button type="primary" @click="loadJobs(1, jobPager.pageSize)">
|
||||
查询
|
||||
</Button>
|
||||
<Button @click="loadJobs()">刷新</Button>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
row-key="jobId"
|
||||
:columns="jobColumns"
|
||||
:data-source="jobs"
|
||||
:loading="jobLoading"
|
||||
:pagination="jobPager"
|
||||
@change="handleJobTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<Tag :color="getJobStatusColor(record.status)">
|
||||
{{ getJobStatusText(record.status) }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<Space wrap>
|
||||
<Button type="link" @click="handleOpenJobDetail(record)">
|
||||
详情
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
@click="handleOpenJobAction(record, 'recheck')"
|
||||
>
|
||||
重验
|
||||
</Button>
|
||||
<Button
|
||||
:disabled="isCompletedStatus(record.status)"
|
||||
type="link"
|
||||
@click="handleOpenJobAction(record, 'retry')"
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
:disabled="isCompletedStatus(record.status)"
|
||||
type="link"
|
||||
@click="handleOpenJobAction(record, 'cancel')"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</Card>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane key="source-events" tab="来源事件">
|
||||
<Card title="来源状态事件">
|
||||
<Space class="mb-4" wrap>
|
||||
<Input
|
||||
v-model:value="sourceFilters.sourceType"
|
||||
allow-clear
|
||||
placeholder="来源类型"
|
||||
style="width: 180px"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="sourceFilters.sourceStatus"
|
||||
allow-clear
|
||||
placeholder="来源状态"
|
||||
style="width: 180px"
|
||||
>
|
||||
<Select.Option value="active">正常</Select.Option>
|
||||
<Select.Option value="stale">版本变化</Select.Option>
|
||||
<Select.Option value="revoked">授权撤销</Select.Option>
|
||||
<Select.Option value="recalled">召回</Select.Option>
|
||||
<Select.Option value="delisted">下架</Select.Option>
|
||||
<Select.Option value="blocked">阻断</Select.Option>
|
||||
<Select.Option value="owner_missing">Owner 缺失</Select.Option>
|
||||
<Select.Option value="unauthorized">无授权</Select.Option>
|
||||
</Select>
|
||||
<Button
|
||||
type="primary"
|
||||
@click="loadSourceEvents(1, sourcePager.pageSize)"
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
<Button @click="loadSourceEvents()">刷新</Button>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
row-key="eventId"
|
||||
:columns="sourceEventColumns"
|
||||
:data-source="sourceEvents"
|
||||
:loading="sourceLoading"
|
||||
:pagination="sourcePager"
|
||||
@change="handleSourceTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'sourceStatus'">
|
||||
<Tag :color="getSourceStatusColor(record.sourceStatus)">
|
||||
{{ record.sourceStatus }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<Space>
|
||||
<Button type="link" @click="handleOpenSourceDetail(record)">
|
||||
详情
|
||||
</Button>
|
||||
<Button type="link" @click="handleOpenSourceRetry(record)">
|
||||
重试传播
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</Card>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
v-model:open="jobDetailOpen"
|
||||
:footer="null"
|
||||
:loading="detailLoading"
|
||||
title="任务详情"
|
||||
width="860px"
|
||||
>
|
||||
<Descriptions v-if="selectedJob" bordered size="small" :column="2">
|
||||
<Descriptions.Item label="任务 ID">
|
||||
{{ selectedJob.jobId }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
{{ jobStatusText[selectedJob.status] }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="类型">{{ selectedJob.type }}</Descriptions.Item>
|
||||
<Descriptions.Item label="Owner">{{ selectedJob.owner }}</Descriptions.Item>
|
||||
<Descriptions.Item label="失败阶段">
|
||||
{{ displayText(selectedJob.failedStage) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="失败原因">
|
||||
{{ displayText(selectedJob.errorMessage) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="可重试">
|
||||
{{ selectedJob.retryable ? '是' : '否' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="影响摘要">
|
||||
{{ selectedJob.nextActions?.join(';') || '-' }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<div v-if="selectedJob" class="mt-4 grid grid-cols-2 gap-4">
|
||||
<Card size="small" title="输入摘要">
|
||||
<pre class="m-0 whitespace-pre-wrap">{{ stringifySummary(selectedJob.input) }}</pre>
|
||||
</Card>
|
||||
<Card size="small" title="结果摘要">
|
||||
<pre class="m-0 whitespace-pre-wrap">{{ stringifySummary(selectedJob.result) }}</pre>
|
||||
</Card>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
v-model:open="sourceDetailOpen"
|
||||
:footer="null"
|
||||
:loading="detailLoading"
|
||||
title="来源事件详情"
|
||||
width="760px"
|
||||
>
|
||||
<Descriptions v-if="selectedSourceEvent" bordered size="small" :column="2">
|
||||
<Descriptions.Item label="事件 ID">
|
||||
{{ selectedSourceEvent.eventId }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="来源状态">
|
||||
{{ selectedSourceEvent.sourceStatus }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="来源类型">
|
||||
{{ selectedSourceEvent.sourceType }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="来源 ID">
|
||||
{{ selectedSourceEvent.sourceId }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="动作策略">
|
||||
{{ selectedSourceEvent.actionPolicy }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="传播状态">
|
||||
{{ displayText(selectedSourceEvent.propagationStatus) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="影响绑定数">
|
||||
{{ displayText(selectedSourceEvent.affectedBindings) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="影响任务数">
|
||||
{{ displayText(selectedSourceEvent.affectedTasks) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="建议动作">
|
||||
{{ selectedSourceEvent.nextActions?.join(';') || '-' }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Card v-if="selectedSourceEvent" class="mt-4" size="small" title="原因归因">
|
||||
<Tag
|
||||
v-for="item in selectedSourceEvent.reasonAttributions"
|
||||
:key="`${item.source}-${item.reason}`"
|
||||
class="mb-2"
|
||||
>
|
||||
{{ displayText(item.source) }}:{{ displayText(item.reason) }}
|
||||
</Tag>
|
||||
</Card>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
v-model:open="jobActionOpen"
|
||||
:confirm-loading="commandSubmitting"
|
||||
title="任务治理操作"
|
||||
@ok="submitJobAction"
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="任务 ID">
|
||||
<Input v-model:value="jobActionForm.jobId" disabled />
|
||||
</Form.Item>
|
||||
<Form.Item label="幂等键">
|
||||
<Input v-model:value="jobActionForm.commandId" />
|
||||
</Form.Item>
|
||||
<Form.Item label="重验清单">
|
||||
<Space direction="vertical">
|
||||
<Checkbox v-model:checked="jobActionForm.ownerChecked">
|
||||
owner 和当前操作者权限已重验
|
||||
</Checkbox>
|
||||
<Checkbox v-model:checked="jobActionForm.sourceChecked">
|
||||
来源状态、市场状态和版本已重验
|
||||
</Checkbox>
|
||||
<Checkbox v-model:checked="jobActionForm.authorizationChecked">
|
||||
授权快照和运行权限包已重验
|
||||
</Checkbox>
|
||||
<Checkbox v-model:checked="jobActionForm.idempotencyChecked">
|
||||
幂等键、retry group 和旧上下文复用风险已重验
|
||||
</Checkbox>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
<Form.Item label="原因" required>
|
||||
<Input.TextArea v-model:value="jobActionForm.reason" :rows="3" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
v-model:open="sourceRetryOpen"
|
||||
:confirm-loading="commandSubmitting"
|
||||
title="重试来源传播"
|
||||
@ok="submitSourceRetry"
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="事件 ID">
|
||||
<Input v-model:value="sourceRetryForm.eventId" disabled />
|
||||
</Form.Item>
|
||||
<Form.Item label="幂等键" required>
|
||||
<Input v-model:value="sourceRetryForm.commandId" />
|
||||
</Form.Item>
|
||||
<Form.Item label="原因" required>
|
||||
<Input.TextArea v-model:value="sourceRetryForm.reason" :rows="3" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Page>
|
||||
</template>
|
||||
@ -0,0 +1,71 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createApp, nextTick } from 'vue';
|
||||
|
||||
import KnowledgeIndex from '../index.vue';
|
||||
|
||||
vi.mock('@vben/common-ui', () => ({
|
||||
Page: {
|
||||
name: 'Page',
|
||||
template: '<main><slot /></main>',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('ant-design-vue', () => ({
|
||||
message: {
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('#/api/muse/knowledge', () => ({
|
||||
getGlobalKnowledgeBaseListApi: vi.fn().mockResolvedValue({
|
||||
list: [
|
||||
{
|
||||
activePolicyVersion: 3,
|
||||
bindingCount: 8,
|
||||
category: '系统资料',
|
||||
currentVersion: 5,
|
||||
documentCount: 12,
|
||||
kbId: 'kb-1',
|
||||
name: '平台安全知识库',
|
||||
status: 'active',
|
||||
},
|
||||
],
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
}),
|
||||
getGlobalKBDocumentsApi: vi.fn().mockResolvedValue({ list: [], total: 0 }),
|
||||
getGlobalKBProcessingTasksApi: vi.fn().mockResolvedValue({
|
||||
list: [],
|
||||
total: 0,
|
||||
}),
|
||||
getGlobalKBSourceBindingsApi: vi.fn().mockResolvedValue({
|
||||
list: [],
|
||||
total: 0,
|
||||
}),
|
||||
previewGlobalKBImpactApi: vi.fn(),
|
||||
reindexGlobalKnowledgeBaseApi: vi.fn(),
|
||||
disableGlobalKnowledgeBaseApi: vi.fn(),
|
||||
retryGlobalKBSourceEventApi: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('muse knowledge admin view', () => {
|
||||
it('渲染全局知识管理边界和列表数据', async () => {
|
||||
const root = document.createElement('div');
|
||||
document.body.append(root);
|
||||
const app = createApp(KnowledgeIndex);
|
||||
app.mount(root);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await nextTick();
|
||||
|
||||
expect(root.textContent).toContain('全局知识管理');
|
||||
expect(root.textContent).toContain('管理员治理全局知识库');
|
||||
expect(root.textContent).toContain('平台安全知识库');
|
||||
expect(root.textContent).toContain('不自动写入用户局域知识库');
|
||||
|
||||
app.unmount();
|
||||
root.remove();
|
||||
});
|
||||
});
|
||||
563
muse-admin/apps/web-antd/src/views/muse/knowledge/index.vue
Normal file
563
muse-admin/apps/web-antd/src/views/muse/knowledge/index.vue
Normal file
@ -0,0 +1,563 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 全局知识管理页
|
||||
* 管理员只治理系统级全局知识库、授权策略、资料处理状态和来源状态;
|
||||
* 本页不写入用户局域知识库,也不读取用户私有正文。
|
||||
*/
|
||||
import type {
|
||||
GlobalKBDocumentSummaryVO,
|
||||
GlobalKBImpactActionType,
|
||||
GlobalKBImpactPreviewVO,
|
||||
GlobalKBProcessingTaskSummaryVO,
|
||||
GlobalKnowledgeBaseSummaryVO,
|
||||
SourceBindingSummaryVO,
|
||||
} from '#/api/muse/knowledge';
|
||||
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
disableGlobalKnowledgeBaseApi,
|
||||
getGlobalKBDocumentsApi,
|
||||
getGlobalKnowledgeBaseListApi,
|
||||
getGlobalKBProcessingTasksApi,
|
||||
getGlobalKBSourceBindingsApi,
|
||||
previewGlobalKBImpactApi,
|
||||
reindexGlobalKnowledgeBaseApi,
|
||||
retryGlobalKBSourceEventApi,
|
||||
} from '#/api/muse/knowledge';
|
||||
|
||||
/** 状态标签颜色,保持管理后台状态一眼可扫。 */
|
||||
const KNOWLEDGE_STATUS_COLOR: Record<string, string> = {
|
||||
active: 'green',
|
||||
disabled: 'default',
|
||||
failed: 'red',
|
||||
processing: 'blue',
|
||||
};
|
||||
|
||||
/** 资料处理状态标签颜色。 */
|
||||
const DOCUMENT_STATUS_COLOR: Record<string, string> = {
|
||||
failed: 'red',
|
||||
generative: 'green',
|
||||
pending_process: 'gold',
|
||||
pending_scan: 'gold',
|
||||
processing: 'blue',
|
||||
scan_blocked: 'red',
|
||||
searchable: 'cyan',
|
||||
};
|
||||
|
||||
/** 来源状态标签颜色。 */
|
||||
const SOURCE_STATUS_COLOR: Record<string, string> = {
|
||||
active: 'green',
|
||||
blocked: 'red',
|
||||
delisted: 'orange',
|
||||
owner_missing: 'red',
|
||||
recalled: 'red',
|
||||
revoked: 'orange',
|
||||
stale: 'gold',
|
||||
unauthorized: 'red',
|
||||
};
|
||||
|
||||
/** 知识库列表列配置。 */
|
||||
const knowledgeColumns = [
|
||||
{ dataIndex: 'name', title: '名称' },
|
||||
{ dataIndex: 'category', title: '分类' },
|
||||
{ dataIndex: 'currentVersion', title: '版本' },
|
||||
{ dataIndex: 'documentCount', title: '资料' },
|
||||
{ dataIndex: 'bindingCount', title: '绑定' },
|
||||
{ dataIndex: 'status', title: '状态' },
|
||||
{ dataIndex: 'updatedAt', title: '更新时间' },
|
||||
];
|
||||
|
||||
/** 资料处理列表列配置。 */
|
||||
const documentColumns = [
|
||||
{ dataIndex: 'name', title: '资料' },
|
||||
{ dataIndex: 'type', title: '类型' },
|
||||
{ dataIndex: 'version', title: '版本' },
|
||||
{ dataIndex: 'processingStatus', title: '处理状态' },
|
||||
{ dataIndex: 'failureReason', title: '失败原因' },
|
||||
{ dataIndex: 'updatedAt', title: '更新时间' },
|
||||
];
|
||||
|
||||
/** 处理任务列表列配置。 */
|
||||
const taskColumns = [
|
||||
{ dataIndex: 'taskId', title: '任务 ID' },
|
||||
{ dataIndex: 'taskType', title: '类型' },
|
||||
{ dataIndex: 'stage', title: '阶段' },
|
||||
{ dataIndex: 'status', title: '状态' },
|
||||
{ dataIndex: 'failedCount', title: '失败数' },
|
||||
{ dataIndex: 'failureReason', title: '失败原因' },
|
||||
];
|
||||
|
||||
/** 来源状态列表列配置。 */
|
||||
const sourceColumns = [
|
||||
{ dataIndex: 'bindingId', title: '绑定 ID' },
|
||||
{ dataIndex: 'sourceType', title: '来源类型' },
|
||||
{ dataIndex: 'sourceStatus', title: '来源状态' },
|
||||
{ dataIndex: 'actionPolicy', title: '动作策略' },
|
||||
{ dataIndex: 'targetOwner', title: '目标域' },
|
||||
{ dataIndex: 'affectedWorkCount', title: '影响作品' },
|
||||
];
|
||||
|
||||
/** 列表查询条件,管理页只做聚合筛选,不进入私有正文。 */
|
||||
const queryParams = reactive({
|
||||
keyword: '',
|
||||
status: undefined as GlobalKnowledgeBaseSummaryVO['status'] | undefined,
|
||||
});
|
||||
|
||||
/** 当前页面加载态。 */
|
||||
const loading = ref(false);
|
||||
/** 详情区域加载态。 */
|
||||
const detailLoading = ref(false);
|
||||
/** 全局知识库列表。 */
|
||||
const knowledgeBases = ref<GlobalKnowledgeBaseSummaryVO[]>([]);
|
||||
/** 当前选中的全局知识库。 */
|
||||
const selectedKnowledgeBase = ref<GlobalKnowledgeBaseSummaryVO>();
|
||||
/** 当前知识库资料列表。 */
|
||||
const documents = ref<GlobalKBDocumentSummaryVO[]>([]);
|
||||
/** 当前知识库处理任务列表。 */
|
||||
const processingTasks = ref<GlobalKBProcessingTaskSummaryVO[]>([]);
|
||||
/** 当前来源绑定和状态列表。 */
|
||||
const sourceBindings = ref<SourceBindingSummaryVO[]>([]);
|
||||
/** 最近一次影响预览。 */
|
||||
const impactPreview = ref<GlobalKBImpactPreviewVO>();
|
||||
|
||||
/** 治理动作弹窗是否打开。 */
|
||||
const actionModalOpen = ref(false);
|
||||
/** 当前动作类型。 */
|
||||
const currentAction = ref<'disable' | 'reindex' | 'source' | 'preview'>(
|
||||
'preview',
|
||||
);
|
||||
|
||||
/** 治理动作表单,只保存命令所需摘要,不保存正文。 */
|
||||
const actionForm = reactive({
|
||||
actionType: 'disable' as GlobalKBImpactActionType,
|
||||
eventType: 'status_changed' as const,
|
||||
reason: '',
|
||||
scope: 'failed_only' as 'failed_only' | 'full' | 'incremental',
|
||||
});
|
||||
|
||||
/** 当前选中知识库 ID。 */
|
||||
const selectedKbId = computed(() => selectedKnowledgeBase.value?.kbId);
|
||||
|
||||
/** 汇总卡片数据。 */
|
||||
const summary = computed(() => {
|
||||
const activeCount = knowledgeBases.value.filter(
|
||||
(item) => item.status === 'active',
|
||||
).length;
|
||||
const failedCount = knowledgeBases.value.filter(
|
||||
(item) => item.status === 'failed',
|
||||
).length;
|
||||
const documentCount = knowledgeBases.value.reduce(
|
||||
(total, item) => total + (item.documentCount ?? 0),
|
||||
0,
|
||||
);
|
||||
const bindingCount = knowledgeBases.value.reduce(
|
||||
(total, item) => total + (item.bindingCount ?? 0),
|
||||
0,
|
||||
);
|
||||
return { activeCount, bindingCount, documentCount, failedCount };
|
||||
});
|
||||
|
||||
/** 生成幂等命令 ID,便于后端去重和审计串联。 */
|
||||
function createCommandId(prefix: string) {
|
||||
return `${prefix}-${Date.now()}`;
|
||||
}
|
||||
|
||||
/** 加载全局知识库列表。 */
|
||||
async function loadKnowledgeBases() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await getGlobalKnowledgeBaseListApi({
|
||||
keyword: queryParams.keyword || undefined,
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
status: queryParams.status,
|
||||
});
|
||||
knowledgeBases.value = result.list ?? [];
|
||||
if (!selectedKnowledgeBase.value && knowledgeBases.value[0]) {
|
||||
await selectKnowledgeBase(knowledgeBases.value[0]);
|
||||
}
|
||||
} catch {
|
||||
message.error('全局知识库列表加载失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载选中知识库的资料、处理任务和来源状态。 */
|
||||
async function loadKnowledgeBaseRelations(kbId: string) {
|
||||
detailLoading.value = true;
|
||||
try {
|
||||
const [documentResult, taskResult, sourceResult] = await Promise.all([
|
||||
getGlobalKBDocumentsApi(kbId, { pageNo: 1, pageSize: 10 }),
|
||||
getGlobalKBProcessingTasksApi(kbId, { pageNo: 1, pageSize: 10 }),
|
||||
getGlobalKBSourceBindingsApi({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
sourceType: 'global_kb',
|
||||
}),
|
||||
]);
|
||||
documents.value = documentResult.list ?? [];
|
||||
processingTasks.value = taskResult.list ?? [];
|
||||
sourceBindings.value = sourceResult.list ?? [];
|
||||
} catch {
|
||||
message.error('全局知识库关联状态加载失败');
|
||||
} finally {
|
||||
detailLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 选中知识库并刷新右侧治理状态。 */
|
||||
async function selectKnowledgeBase(row: GlobalKnowledgeBaseSummaryVO) {
|
||||
selectedKnowledgeBase.value = row;
|
||||
impactPreview.value = undefined;
|
||||
await loadKnowledgeBaseRelations(row.kbId);
|
||||
}
|
||||
|
||||
/** 打开治理动作弹窗。 */
|
||||
function openAction(action: typeof currentAction.value) {
|
||||
currentAction.value = action;
|
||||
actionForm.actionType = action === 'source' ? 'policy_publish' : 'disable';
|
||||
actionForm.eventType = 'status_changed';
|
||||
actionForm.reason = '';
|
||||
actionForm.scope = 'failed_only';
|
||||
actionModalOpen.value = true;
|
||||
}
|
||||
|
||||
/** 执行影响预览,后续危险命令必须先让管理员看到影响范围。 */
|
||||
async function handlePreviewImpact() {
|
||||
if (!selectedKbId.value) {
|
||||
return;
|
||||
}
|
||||
impactPreview.value = await previewGlobalKBImpactApi(selectedKbId.value, {
|
||||
actionType: actionForm.actionType,
|
||||
});
|
||||
message.success('影响预览已生成');
|
||||
}
|
||||
|
||||
/** 提交治理动作。 */
|
||||
async function handleSubmitAction() {
|
||||
if (!selectedKbId.value) {
|
||||
return;
|
||||
}
|
||||
if (currentAction.value === 'preview') {
|
||||
await handlePreviewImpact();
|
||||
return;
|
||||
}
|
||||
if (!actionForm.reason.trim()) {
|
||||
message.error('请填写治理原因');
|
||||
return;
|
||||
}
|
||||
if (currentAction.value === 'disable' && !impactPreview.value?.impactId) {
|
||||
message.error('停用前必须先生成影响预览');
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentAction.value === 'disable') {
|
||||
await disableGlobalKnowledgeBaseApi(selectedKbId.value, {
|
||||
commandId: createCommandId('global-kb-disable'),
|
||||
reason: actionForm.reason,
|
||||
});
|
||||
message.success('全局知识库已提交停用');
|
||||
}
|
||||
|
||||
if (currentAction.value === 'reindex') {
|
||||
await reindexGlobalKnowledgeBaseApi(selectedKbId.value, {
|
||||
commandId: createCommandId('global-kb-reindex'),
|
||||
scope: actionForm.scope,
|
||||
});
|
||||
message.success('索引重建任务已创建');
|
||||
}
|
||||
|
||||
if (currentAction.value === 'source') {
|
||||
await retryGlobalKBSourceEventApi(selectedKbId.value, {
|
||||
commandId: createCommandId('global-kb-source-event'),
|
||||
eventType: actionForm.eventType,
|
||||
reason: actionForm.reason,
|
||||
});
|
||||
message.success('来源状态传播已提交');
|
||||
}
|
||||
|
||||
actionModalOpen.value = false;
|
||||
await loadKnowledgeBases();
|
||||
if (selectedKbId.value) {
|
||||
await loadKnowledgeBaseRelations(selectedKbId.value);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadKnowledgeBases);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<a-space class="knowledge-page" direction="vertical" size="middle">
|
||||
<section class="page-heading">
|
||||
<h1>全局知识管理</h1>
|
||||
<p>
|
||||
管理员治理全局知识库、授权策略、资料处理状态和来源状态;这里只处理系统级公共知识对象,不自动写入用户局域知识库。
|
||||
</p>
|
||||
</section>
|
||||
<a-alert
|
||||
message="全局知识管理"
|
||||
show-icon
|
||||
type="info"
|
||||
description="管理员治理全局知识库、授权策略、资料处理状态和来源状态;这里只处理系统级公共知识对象,不自动写入用户局域知识库,也不读取用户私有正文。"
|
||||
/>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic title="可用知识库" :value="summary.activeCount" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic title="异常知识库" :value="summary.failedCount" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic title="资料总数" :value="summary.documentCount" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic title="授权绑定" :value="summary.bindingCount" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-card title="全局知识库列表" size="small">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-input
|
||||
v-model:value="queryParams.keyword"
|
||||
allow-clear
|
||||
placeholder="搜索知识库"
|
||||
style="width: 220px"
|
||||
/>
|
||||
<a-select
|
||||
v-model:value="queryParams.status"
|
||||
allow-clear
|
||||
placeholder="状态"
|
||||
style="width: 160px"
|
||||
>
|
||||
<a-select-option value="active">可用</a-select-option>
|
||||
<a-select-option value="processing">处理中</a-select-option>
|
||||
<a-select-option value="failed">失败</a-select-option>
|
||||
<a-select-option value="disabled">已停用</a-select-option>
|
||||
</a-select>
|
||||
<a-button type="primary" @click="loadKnowledgeBases">
|
||||
查询
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-table
|
||||
:columns="knowledgeColumns"
|
||||
:custom-row="
|
||||
(record: GlobalKnowledgeBaseSummaryVO) => ({
|
||||
onClick: () => selectKnowledgeBase(record),
|
||||
})
|
||||
"
|
||||
:data-source="knowledgeBases"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="kbId"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'status'">
|
||||
<a-tag :color="KNOWLEDGE_STATUS_COLOR[record.status]">
|
||||
{{ record.status }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-card
|
||||
v-if="selectedKnowledgeBase"
|
||||
:loading="detailLoading"
|
||||
size="small"
|
||||
title="知识库治理详情"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="openAction('preview')">影响预览</a-button>
|
||||
<a-button @click="openAction('source')">重试来源状态</a-button>
|
||||
<a-button @click="openAction('reindex')">重建失败索引</a-button>
|
||||
<a-button danger @click="openAction('disable')">停用</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-descriptions bordered size="small" :column="4">
|
||||
<a-descriptions-item label="名称">
|
||||
{{ selectedKnowledgeBase.name }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="当前版本">
|
||||
{{ selectedKnowledgeBase.currentVersion ?? '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="授权策略版本">
|
||||
{{ selectedKnowledgeBase.activePolicyVersion ?? '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag
|
||||
:color="KNOWLEDGE_STATUS_COLOR[selectedKnowledgeBase.status]"
|
||||
>
|
||||
{{ selectedKnowledgeBase.status }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-tabs class="mt-4">
|
||||
<a-tab-pane key="documents" tab="资料处理状态">
|
||||
<a-table
|
||||
:columns="documentColumns"
|
||||
:data-source="documents"
|
||||
:pagination="false"
|
||||
row-key="documentId"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'processingStatus'">
|
||||
<a-tag
|
||||
:color="
|
||||
DOCUMENT_STATUS_COLOR[record.processingStatus] || 'default'
|
||||
"
|
||||
>
|
||||
{{ record.processingStatus || '-' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="tasks" tab="处理任务">
|
||||
<a-table
|
||||
:columns="taskColumns"
|
||||
:data-source="processingTasks"
|
||||
:pagination="false"
|
||||
row-key="taskId"
|
||||
size="small"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="source" tab="来源状态">
|
||||
<a-table
|
||||
:columns="sourceColumns"
|
||||
:data-source="sourceBindings"
|
||||
:pagination="false"
|
||||
row-key="bindingId"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'sourceStatus'">
|
||||
<a-tag
|
||||
:color="SOURCE_STATUS_COLOR[record.sourceStatus] || 'gold'"
|
||||
>
|
||||
{{ record.sourceStatus || '-' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="impact" tab="影响预览">
|
||||
<a-descriptions v-if="impactPreview" bordered size="small">
|
||||
<a-descriptions-item label="预览 ID">
|
||||
{{ impactPreview.impactId }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="受影响绑定">
|
||||
{{ impactPreview.affectedBindings ?? 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="受影响安装">
|
||||
{{ impactPreview.affectedInstalls ?? 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="运行中任务">
|
||||
{{ impactPreview.runningTasks ?? 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="风险摘要">
|
||||
{{ impactPreview.riskSummary || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<a-typography-paragraph v-else type="secondary">
|
||||
暂无影响预览。停用、扩大授权或切换版本前必须先生成预览。
|
||||
</a-typography-paragraph>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
v-model:open="actionModalOpen"
|
||||
:title="currentAction === 'preview' ? '影响预览' : '治理动作'"
|
||||
@ok="handleSubmitAction"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item v-if="currentAction === 'preview'" label="预览动作">
|
||||
<a-select v-model:value="actionForm.actionType">
|
||||
<a-select-option value="disable">停用知识库</a-select-option>
|
||||
<a-select-option value="policy_publish">
|
||||
发布授权策略
|
||||
</a-select-option>
|
||||
<a-select-option value="version_activate">
|
||||
激活版本
|
||||
</a-select-option>
|
||||
<a-select-option value="document_delete">停用资料</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="currentAction === 'source'" label="来源事件">
|
||||
<a-select v-model:value="actionForm.eventType">
|
||||
<a-select-option value="status_changed">状态变化</a-select-option>
|
||||
<a-select-option value="version_changed">版本变化</a-select-option>
|
||||
<a-select-option value="policy_changed">授权变化</a-select-option>
|
||||
<a-select-option value="kb_disabled">知识库停用</a-select-option>
|
||||
<a-select-option value="kb_enabled">知识库启用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="currentAction === 'reindex'" label="重建范围">
|
||||
<a-select v-model:value="actionForm.scope">
|
||||
<a-select-option value="failed_only">仅失败项</a-select-option>
|
||||
<a-select-option value="incremental">增量</a-select-option>
|
||||
<a-select-option value="full">全量</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="currentAction !== 'preview'" label="治理原因">
|
||||
<a-textarea
|
||||
v-model:value="actionForm.reason"
|
||||
:rows="3"
|
||||
placeholder="填写可审计的治理原因"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-alert
|
||||
v-if="currentAction === 'disable'"
|
||||
show-icon
|
||||
type="warning"
|
||||
message="停用只改变全局知识库后续可用性和来源状态,不回滚用户已确认事实。"
|
||||
/>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</a-space>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.knowledge-page {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-heading h1 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.page-heading p {
|
||||
margin: 4px 0 0;
|
||||
color: rgb(0 0 0 / 45%);
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,86 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createApp, nextTick } from 'vue';
|
||||
|
||||
import MarketIndex from '../index.vue';
|
||||
|
||||
vi.mock('@vben/common-ui', () => ({
|
||||
Page: {
|
||||
name: 'Page',
|
||||
template: '<main><slot /></main>',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('ant-design-vue', () => ({
|
||||
message: {
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('#/api/muse/market', () => ({
|
||||
getMarketAppealListApi: vi.fn().mockResolvedValue({
|
||||
list: [
|
||||
{
|
||||
appealId: 'appeal-1',
|
||||
appealType: 'recall',
|
||||
assetId: 'asset-1',
|
||||
assetName: '安全写作助手',
|
||||
status: 'reviewing',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
}),
|
||||
getMarketAssetListApi: vi.fn().mockResolvedValue({
|
||||
list: [
|
||||
{
|
||||
assetId: 'asset-1',
|
||||
assetType: 'agent',
|
||||
bindCount: 5,
|
||||
installCount: 10,
|
||||
listingStatus: 'listed',
|
||||
name: '安全写作助手',
|
||||
publisherName: 'Muse 官方',
|
||||
reviewStatus: 'approved',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
}),
|
||||
getMarketPublishRequestListApi: vi.fn().mockResolvedValue({
|
||||
list: [
|
||||
{
|
||||
assetId: 'asset-2',
|
||||
assetName: '世界观知识库',
|
||||
assetType: 'knowledge_base',
|
||||
requestId: 'request-1',
|
||||
status: 'pending',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
}),
|
||||
previewMarketGovernanceImpactApi: vi.fn(),
|
||||
approveMarketPublishRequestApi: vi.fn(),
|
||||
rejectMarketPublishRequestApi: vi.fn(),
|
||||
delistMarketAssetApi: vi.fn(),
|
||||
recallMarketAssetApi: vi.fn(),
|
||||
resolveMarketAppealApi: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('muse market admin view', () => {
|
||||
it('渲染市场治理边界和审核数据', async () => {
|
||||
const root = document.createElement('div');
|
||||
document.body.append(root);
|
||||
const app = createApp(MarketIndex);
|
||||
app.mount(root);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await nextTick();
|
||||
|
||||
expect(root.textContent).toContain('市场治理');
|
||||
expect(root.textContent).toContain('审核队列');
|
||||
expect(root.textContent).toContain('世界观知识库');
|
||||
expect(root.textContent).toContain('不修改用户私有副本');
|
||||
|
||||
app.unmount();
|
||||
root.remove();
|
||||
});
|
||||
});
|
||||
739
muse-admin/apps/web-antd/src/views/muse/market/index.vue
Normal file
739
muse-admin/apps/web-antd/src/views/muse/market/index.vue
Normal file
@ -0,0 +1,739 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 市场治理页
|
||||
* 管理员处理发布审核、公共市场资产下架/召回和申诉;
|
||||
* 本页只治理公共市场对象和来源状态,不修改用户私有副本或用户已确认正文。
|
||||
*/
|
||||
import type {
|
||||
AdminAppealSummaryVO,
|
||||
AdminGovernanceImpactPreviewVO,
|
||||
AdminMarketAssetSummaryVO,
|
||||
AdminPublishRequestSummaryVO,
|
||||
MarketAppealResolution,
|
||||
MarketAppealStatus,
|
||||
MarketGovernanceActionType,
|
||||
MarketGovernanceScope,
|
||||
MarketListingStatus,
|
||||
MarketReviewStatus,
|
||||
} from '#/api/muse/market';
|
||||
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
approveMarketPublishRequestApi,
|
||||
delistMarketAssetApi,
|
||||
getMarketAppealListApi,
|
||||
getMarketAssetListApi,
|
||||
getMarketPublishRequestListApi,
|
||||
previewMarketGovernanceImpactApi,
|
||||
recallMarketAssetApi,
|
||||
rejectMarketPublishRequestApi,
|
||||
resolveMarketAppealApi,
|
||||
} from '#/api/muse/market';
|
||||
|
||||
/** 市场状态颜色。 */
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
approved: 'green',
|
||||
closed: 'default',
|
||||
compliance_blocked: 'red',
|
||||
delisted: 'orange',
|
||||
listed: 'green',
|
||||
maintained: 'orange',
|
||||
needs_supplement: 'gold',
|
||||
not_listed: 'default',
|
||||
pending: 'blue',
|
||||
recalled: 'red',
|
||||
rejected: 'red',
|
||||
restored: 'green',
|
||||
reviewing: 'cyan',
|
||||
supplementing: 'gold',
|
||||
};
|
||||
|
||||
/** 审核队列表格列。 */
|
||||
const reviewColumns = [
|
||||
{ dataIndex: 'assetName', title: '资产' },
|
||||
{ dataIndex: 'assetType', title: '类型' },
|
||||
{ dataIndex: 'publisherName', title: '发布者' },
|
||||
{ dataIndex: 'status', title: '审核状态' },
|
||||
{ dataIndex: 'riskTags', title: '风险标签' },
|
||||
{ dataIndex: 'privacyCheckResult', title: '隐私/密钥检查' },
|
||||
{ dataIndex: 'actions', title: '操作' },
|
||||
];
|
||||
|
||||
/** 资产治理表格列。 */
|
||||
const assetColumns = [
|
||||
{ dataIndex: 'name', title: '资产' },
|
||||
{ dataIndex: 'assetType', title: '类型' },
|
||||
{ dataIndex: 'publisherName', title: '发布者' },
|
||||
{ dataIndex: 'listingStatus', title: '上架状态' },
|
||||
{ dataIndex: 'reviewStatus', title: '审核状态' },
|
||||
{ dataIndex: 'installCount', title: '安装' },
|
||||
{ dataIndex: 'bindCount', title: '绑定' },
|
||||
{ dataIndex: 'appealCount', title: '申诉' },
|
||||
{ dataIndex: 'actions', title: '治理' },
|
||||
];
|
||||
|
||||
/** 申诉表格列。 */
|
||||
const appealColumns = [
|
||||
{ dataIndex: 'assetName', title: '资产' },
|
||||
{ dataIndex: 'appealType', title: '申诉类型' },
|
||||
{ dataIndex: 'appellantName', title: '申诉人' },
|
||||
{ dataIndex: 'status', title: '状态' },
|
||||
{ dataIndex: 'processorId', title: '处理人' },
|
||||
{ dataIndex: 'actions', title: '处理' },
|
||||
];
|
||||
|
||||
/** 当前治理动作目标,避免把状态字段散落在多个 ref 中。 */
|
||||
interface MarketActionTarget {
|
||||
/** 申诉 ID。 */
|
||||
appealId?: string;
|
||||
/** 资产 ID。 */
|
||||
assetId?: string;
|
||||
/** 期望申诉状态。 */
|
||||
expectedAppealStatus?: Extract<
|
||||
MarketAppealStatus,
|
||||
'pending' | 'reviewing' | 'supplementing'
|
||||
>;
|
||||
/** 期望上架状态。 */
|
||||
expectedListingStatus?: Extract<MarketListingStatus, 'delisted' | 'listed'>;
|
||||
/** 期望审核状态。 */
|
||||
expectedReviewStatus?: Extract<
|
||||
MarketReviewStatus,
|
||||
'needs_supplement' | 'pending' | 'reviewing'
|
||||
>;
|
||||
/** 发布申请 ID。 */
|
||||
requestId?: string;
|
||||
/** 展示名称。 */
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/** 市场页面加载态。 */
|
||||
const loading = ref(false);
|
||||
/** 发布审核队列。 */
|
||||
const publishRequests = ref<AdminPublishRequestSummaryVO[]>([]);
|
||||
/** 公共市场资产列表。 */
|
||||
const marketAssets = ref<AdminMarketAssetSummaryVO[]>([]);
|
||||
/** 申诉列表。 */
|
||||
const appeals = ref<AdminAppealSummaryVO[]>([]);
|
||||
/** 最近一次治理影响预览。 */
|
||||
const impactPreview = ref<AdminGovernanceImpactPreviewVO>();
|
||||
/** 动作弹窗是否打开。 */
|
||||
const actionModalOpen = ref(false);
|
||||
/** 当前动作。 */
|
||||
const currentAction = ref<
|
||||
'approve' | 'delist' | 'preview' | 'recall' | 'reject' | 'resolve'
|
||||
>('preview');
|
||||
/** 当前动作目标。 */
|
||||
const actionTarget = ref<MarketActionTarget>({});
|
||||
|
||||
/** 治理动作表单,所有字段都是审计摘要或引用 ID,不接收私有正文。 */
|
||||
const actionForm = reactive({
|
||||
actionType: 'delist' as MarketGovernanceActionType,
|
||||
basis: '',
|
||||
evidenceTags: '',
|
||||
impactPreviewId: '',
|
||||
note: '',
|
||||
reason: '',
|
||||
resolution: 'maintained' as MarketAppealResolution,
|
||||
scope: 'stop_new_acquire' as MarketGovernanceScope,
|
||||
validationResultId: '',
|
||||
});
|
||||
|
||||
/** 页面聚合摘要。 */
|
||||
const summary = computed(() => {
|
||||
const pendingReview = publishRequests.value.filter(
|
||||
(item) => item.status === 'pending' || item.status === 'reviewing',
|
||||
).length;
|
||||
const listedAssets = marketAssets.value.filter(
|
||||
(item) => item.listingStatus === 'listed',
|
||||
).length;
|
||||
const openAppeals = appeals.value.filter(
|
||||
(item) => item.status === 'pending' || item.status === 'reviewing',
|
||||
).length;
|
||||
const blockedAssets = marketAssets.value.filter(
|
||||
(item) =>
|
||||
item.listingStatus === 'delisted' || item.listingStatus === 'recalled',
|
||||
).length;
|
||||
return { blockedAssets, listedAssets, openAppeals, pendingReview };
|
||||
});
|
||||
|
||||
/** 生成幂等命令 ID。 */
|
||||
function createCommandId(prefix: string) {
|
||||
return `${prefix}-${Date.now()}`;
|
||||
}
|
||||
|
||||
/** 把逗号分隔的证据输入转成后端需要的标签数组。 */
|
||||
function splitEvidenceTags(value: string) {
|
||||
return value
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/** 加载审核队列。 */
|
||||
async function loadReviewQueue() {
|
||||
const result = await getMarketPublishRequestListApi({
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
status: 'pending',
|
||||
});
|
||||
publishRequests.value = result.list ?? [];
|
||||
}
|
||||
|
||||
/** 加载市场资产治理列表。 */
|
||||
async function loadMarketAssets() {
|
||||
const result = await getMarketAssetListApi({
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
marketAssets.value = result.list ?? [];
|
||||
}
|
||||
|
||||
/** 加载市场申诉列表。 */
|
||||
async function loadAppeals() {
|
||||
const result = await getMarketAppealListApi({
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
appeals.value = result.list ?? [];
|
||||
}
|
||||
|
||||
/** 加载整个市场治理工作台。 */
|
||||
async function loadMarketDashboard() {
|
||||
loading.value = true;
|
||||
try {
|
||||
await Promise.all([loadReviewQueue(), loadMarketAssets(), loadAppeals()]);
|
||||
} catch {
|
||||
message.error('市场治理数据加载失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开审核动作。 */
|
||||
function openReviewAction(
|
||||
action: 'approve' | 'reject',
|
||||
row: AdminPublishRequestSummaryVO,
|
||||
) {
|
||||
currentAction.value = action;
|
||||
actionTarget.value = {
|
||||
assetId: row.assetId,
|
||||
expectedReviewStatus: row.status as MarketActionTarget['expectedReviewStatus'],
|
||||
requestId: row.requestId,
|
||||
title: row.assetName,
|
||||
};
|
||||
actionForm.evidenceTags = '';
|
||||
actionForm.note = '';
|
||||
actionForm.reason = '';
|
||||
actionForm.validationResultId = '';
|
||||
actionModalOpen.value = true;
|
||||
}
|
||||
|
||||
/** 打开资产治理动作。 */
|
||||
function openAssetAction(
|
||||
action: 'delist' | 'preview' | 'recall',
|
||||
row: AdminMarketAssetSummaryVO,
|
||||
) {
|
||||
currentAction.value = action;
|
||||
actionTarget.value = {
|
||||
assetId: row.assetId,
|
||||
expectedListingStatus: row.listingStatus as MarketActionTarget['expectedListingStatus'],
|
||||
title: row.name,
|
||||
};
|
||||
actionForm.actionType = action === 'recall' ? 'recall' : 'delist';
|
||||
actionForm.basis = '';
|
||||
actionForm.impactPreviewId = impactPreview.value?.previewId ?? '';
|
||||
actionForm.reason = '';
|
||||
actionForm.scope = action === 'recall' ? 'full_recall' : 'stop_new_acquire';
|
||||
actionModalOpen.value = true;
|
||||
}
|
||||
|
||||
/** 打开申诉处理动作。 */
|
||||
function openAppealAction(row: AdminAppealSummaryVO) {
|
||||
currentAction.value = 'resolve';
|
||||
actionTarget.value = {
|
||||
appealId: row.appealId,
|
||||
assetId: row.assetId,
|
||||
expectedAppealStatus: row.status as MarketActionTarget['expectedAppealStatus'],
|
||||
title: row.assetName,
|
||||
};
|
||||
actionForm.evidenceTags = '';
|
||||
actionForm.impactPreviewId = impactPreview.value?.previewId ?? '';
|
||||
actionForm.reason = '';
|
||||
actionForm.resolution = 'maintained';
|
||||
actionModalOpen.value = true;
|
||||
}
|
||||
|
||||
/** 执行治理影响预览。 */
|
||||
async function handlePreviewImpact() {
|
||||
if (!actionTarget.value.assetId) {
|
||||
return;
|
||||
}
|
||||
const previewScope =
|
||||
actionForm.scope === 'full_delist' ? 'stop_new_acquire' : actionForm.scope;
|
||||
impactPreview.value = await previewMarketGovernanceImpactApi(
|
||||
actionTarget.value.assetId,
|
||||
{
|
||||
actionType: actionForm.actionType,
|
||||
commandId: createCommandId('market-impact-preview'),
|
||||
scope: previewScope as Exclude<MarketGovernanceScope, 'full_delist'>,
|
||||
},
|
||||
);
|
||||
actionForm.impactPreviewId = impactPreview.value.previewId;
|
||||
message.success('市场治理影响预览已生成');
|
||||
}
|
||||
|
||||
/** 提交审核、治理或申诉动作。 */
|
||||
async function handleSubmitAction() {
|
||||
if (currentAction.value === 'preview') {
|
||||
await handlePreviewImpact();
|
||||
return;
|
||||
}
|
||||
if (!actionForm.reason.trim() && currentAction.value !== 'approve') {
|
||||
message.error('请填写可审计原因');
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentAction.value === 'approve') {
|
||||
if (!actionTarget.value.requestId || !actionForm.validationResultId) {
|
||||
message.error('审核通过需要校验结果引用');
|
||||
return;
|
||||
}
|
||||
await approveMarketPublishRequestApi(actionTarget.value.requestId, {
|
||||
commandId: createCommandId('market-review-approve'),
|
||||
evidenceTags: splitEvidenceTags(actionForm.evidenceTags),
|
||||
expectedStatus: actionTarget.value.expectedReviewStatus ?? 'pending',
|
||||
note: actionForm.note || '审核材料完整',
|
||||
validationResultId: actionForm.validationResultId,
|
||||
});
|
||||
message.success('发布申请已审核通过');
|
||||
}
|
||||
|
||||
if (currentAction.value === 'reject') {
|
||||
if (!actionTarget.value.requestId || !actionForm.validationResultId) {
|
||||
message.error('审核驳回需要校验结果引用');
|
||||
return;
|
||||
}
|
||||
await rejectMarketPublishRequestApi(actionTarget.value.requestId, {
|
||||
commandId: createCommandId('market-review-reject'),
|
||||
evidenceTags: splitEvidenceTags(actionForm.evidenceTags),
|
||||
expectedStatus: actionTarget.value.expectedReviewStatus ?? 'pending',
|
||||
reason: actionForm.reason,
|
||||
validationResultId: actionForm.validationResultId,
|
||||
});
|
||||
message.success('发布申请已驳回');
|
||||
}
|
||||
|
||||
if (currentAction.value === 'delist') {
|
||||
if (!actionTarget.value.assetId || !actionForm.impactPreviewId) {
|
||||
message.error('下架前必须先生成影响预览');
|
||||
return;
|
||||
}
|
||||
await delistMarketAssetApi(actionTarget.value.assetId, {
|
||||
commandId: createCommandId('market-asset-delist'),
|
||||
expectedStatus: 'listed',
|
||||
impactPreviewId: actionForm.impactPreviewId,
|
||||
reason: actionForm.reason,
|
||||
scope:
|
||||
actionForm.scope === 'full_recall' ? 'full_delist' : actionForm.scope,
|
||||
});
|
||||
message.success('市场资产下架命令已提交');
|
||||
}
|
||||
|
||||
if (currentAction.value === 'recall') {
|
||||
if (
|
||||
!actionTarget.value.assetId ||
|
||||
!actionForm.impactPreviewId ||
|
||||
!actionForm.basis.trim()
|
||||
) {
|
||||
message.error('召回需要影响预览和合规依据');
|
||||
return;
|
||||
}
|
||||
await recallMarketAssetApi(actionTarget.value.assetId, {
|
||||
basis: actionForm.basis,
|
||||
commandId: createCommandId('market-asset-recall'),
|
||||
expectedStatus: actionTarget.value.expectedListingStatus ?? 'listed',
|
||||
impactPreviewId: actionForm.impactPreviewId,
|
||||
reason: actionForm.reason,
|
||||
scope:
|
||||
actionForm.scope === 'stop_generation'
|
||||
? 'stop_generation'
|
||||
: 'full_recall',
|
||||
});
|
||||
message.success('市场资产召回命令已提交');
|
||||
}
|
||||
|
||||
if (currentAction.value === 'resolve') {
|
||||
if (!actionTarget.value.appealId || !actionForm.impactPreviewId) {
|
||||
message.error('申诉处理需要影响预览引用');
|
||||
return;
|
||||
}
|
||||
await resolveMarketAppealApi(actionTarget.value.appealId, {
|
||||
commandId: createCommandId('market-appeal-resolve'),
|
||||
evidenceTags: splitEvidenceTags(actionForm.evidenceTags),
|
||||
expectedStatus: actionTarget.value.expectedAppealStatus ?? 'reviewing',
|
||||
impactPreviewId: actionForm.impactPreviewId,
|
||||
reason: actionForm.reason,
|
||||
resolution: actionForm.resolution,
|
||||
});
|
||||
message.success('市场申诉已处理');
|
||||
}
|
||||
|
||||
actionModalOpen.value = false;
|
||||
await loadMarketDashboard();
|
||||
}
|
||||
|
||||
onMounted(loadMarketDashboard);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<a-space class="market-page" direction="vertical" size="middle">
|
||||
<section class="page-heading">
|
||||
<h1>市场治理</h1>
|
||||
<p>
|
||||
市场审核、下架、召回和申诉只治理公共市场对象、许可、来源状态和影响范围;不修改用户私有副本。
|
||||
</p>
|
||||
</section>
|
||||
<a-alert
|
||||
message="市场治理"
|
||||
show-icon
|
||||
type="info"
|
||||
description="市场审核、下架、召回和申诉只治理公共市场对象、许可、来源状态和影响范围;不修改用户私有副本,不读取用户私有正文。"
|
||||
/>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic title="待审核" :value="summary.pendingReview" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic title="已上架资产" :value="summary.listedAssets" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic title="开放申诉" :value="summary.openAppeals" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card size="small">
|
||||
<a-statistic title="治理阻断" :value="summary.blockedAssets" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-card :loading="loading" size="small">
|
||||
<template #title>
|
||||
<span>治理工作台</span>
|
||||
</template>
|
||||
<a-typography-paragraph type="secondary">
|
||||
当前待审资产:{{ publishRequests[0]?.assetName || '暂无' }};当前治理资产:{{
|
||||
marketAssets[0]?.name || '暂无'
|
||||
}}。
|
||||
</a-typography-paragraph>
|
||||
|
||||
<a-tabs>
|
||||
<a-tab-pane key="review" tab="审核队列">
|
||||
<h2 class="section-title">审核队列</h2>
|
||||
<a-alert
|
||||
class="mb-3"
|
||||
show-icon
|
||||
type="warning"
|
||||
message="市场审核是发布硬门槛,必须保留权利、隐私、合规、安全和授权范围原因。"
|
||||
/>
|
||||
<a-table
|
||||
:columns="reviewColumns"
|
||||
:data-source="publishRequests"
|
||||
:pagination="false"
|
||||
row-key="requestId"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'status'">
|
||||
<a-tag :color="STATUS_COLOR[record.status] || 'default'">
|
||||
{{ record.status }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'riskTags'">
|
||||
<a-space>
|
||||
<a-tag v-for="tag in record.riskTags || []" :key="tag">
|
||||
{{ tag }}
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'actions'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="openReviewAction('approve', record)">
|
||||
通过
|
||||
</a-button>
|
||||
<a-button
|
||||
danger
|
||||
size="small"
|
||||
@click="openReviewAction('reject', record)"
|
||||
>
|
||||
驳回
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="assets" tab="资产治理">
|
||||
<a-alert
|
||||
class="mb-3"
|
||||
show-icon
|
||||
type="info"
|
||||
message="下架或召回必须先看影响预览;处置只影响后续获取、安装、绑定或生成,不改用户私有副本。"
|
||||
/>
|
||||
<a-table
|
||||
:columns="assetColumns"
|
||||
:data-source="marketAssets"
|
||||
:pagination="false"
|
||||
row-key="assetId"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template
|
||||
v-if="
|
||||
column.dataIndex === 'listingStatus' ||
|
||||
column.dataIndex === 'reviewStatus'
|
||||
"
|
||||
>
|
||||
<a-tag
|
||||
:color="
|
||||
STATUS_COLOR[record[column.dataIndex as string]] ||
|
||||
'default'
|
||||
"
|
||||
>
|
||||
{{ record[column.dataIndex as string] }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'actions'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="openAssetAction('preview', record)">
|
||||
影响预览
|
||||
</a-button>
|
||||
<a-button size="small" @click="openAssetAction('delist', record)">
|
||||
下架
|
||||
</a-button>
|
||||
<a-button
|
||||
danger
|
||||
size="small"
|
||||
@click="openAssetAction('recall', record)"
|
||||
>
|
||||
召回
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="appeals" tab="申诉处理">
|
||||
<a-alert
|
||||
class="mb-3"
|
||||
show-icon
|
||||
type="info"
|
||||
message="申诉只能基于申诉材料、发布资产和治理历史处理;恢复上架不能绕过合规审核。"
|
||||
/>
|
||||
<a-table
|
||||
:columns="appealColumns"
|
||||
:data-source="appeals"
|
||||
:pagination="false"
|
||||
row-key="appealId"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'status'">
|
||||
<a-tag :color="STATUS_COLOR[record.status] || 'default'">
|
||||
{{ record.status }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'actions'">
|
||||
<a-button size="small" @click="openAppealAction(record)">
|
||||
处理
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
|
||||
<a-card v-if="impactPreview" size="small" title="最近一次影响预览">
|
||||
<a-descriptions bordered size="small">
|
||||
<a-descriptions-item label="预览 ID">
|
||||
{{ impactPreview.previewId }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="动作">
|
||||
{{ impactPreview.actionType }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="授权">
|
||||
{{ impactPreview.affectedCounts.authorizationCount ?? 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="安装">
|
||||
{{ impactPreview.affectedCounts.installationCount ?? 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="绑定">
|
||||
{{ impactPreview.affectedCounts.bindingCount ?? 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="运行中任务">
|
||||
{{ impactPreview.affectedCounts.runningTaskCount ?? 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="通知范围">
|
||||
{{ impactPreview.notificationScope || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="来源摘要">
|
||||
{{ impactPreview.sourceReferenceSummary || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
v-model:open="actionModalOpen"
|
||||
:title="`市场治理:${actionTarget.title || ''}`"
|
||||
@ok="handleSubmitAction"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item
|
||||
v-if="currentAction === 'preview' || currentAction === 'recall'"
|
||||
label="影响动作"
|
||||
>
|
||||
<a-select v-model:value="actionForm.actionType">
|
||||
<a-select-option value="delist">下架</a-select-option>
|
||||
<a-select-option value="recall">召回</a-select-option>
|
||||
<a-select-option value="stop_generation">
|
||||
停止生成
|
||||
</a-select-option>
|
||||
<a-select-option value="revoke_license">撤销授权</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="
|
||||
currentAction === 'preview' ||
|
||||
currentAction === 'delist' ||
|
||||
currentAction === 'recall'
|
||||
"
|
||||
label="治理范围"
|
||||
>
|
||||
<a-select v-model:value="actionForm.scope">
|
||||
<a-select-option value="stop_new_acquire">
|
||||
停止新获取
|
||||
</a-select-option>
|
||||
<a-select-option value="stop_new_install">
|
||||
停止新安装
|
||||
</a-select-option>
|
||||
<a-select-option value="stop_new_bind">停止新绑定</a-select-option>
|
||||
<a-select-option value="stop_generation">
|
||||
停止生成使用
|
||||
</a-select-option>
|
||||
<a-select-option value="full_delist">完全下架</a-select-option>
|
||||
<a-select-option value="full_recall">完全召回</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="currentAction === 'approve' || currentAction === 'reject'"
|
||||
label="审核校验结果 ID"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="actionForm.validationResultId"
|
||||
placeholder="validationResultId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="currentAction === 'approve'" label="通过说明">
|
||||
<a-textarea
|
||||
v-model:value="actionForm.note"
|
||||
:rows="3"
|
||||
placeholder="说明审核材料完整性和通过理由"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="
|
||||
currentAction === 'delist' ||
|
||||
currentAction === 'recall' ||
|
||||
currentAction === 'resolve'
|
||||
"
|
||||
label="影响预览 ID"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="actionForm.impactPreviewId"
|
||||
placeholder="先执行影响预览后自动带入"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="currentAction === 'recall'" label="召回依据">
|
||||
<a-input
|
||||
v-model:value="actionForm.basis"
|
||||
placeholder="法律、合规或安全依据"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="currentAction === 'resolve'" label="申诉结论">
|
||||
<a-select v-model:value="actionForm.resolution">
|
||||
<a-select-option value="maintained">维持原处理</a-select-option>
|
||||
<a-select-option value="restored">恢复</a-select-option>
|
||||
<a-select-option value="partially_restored">
|
||||
部分恢复
|
||||
</a-select-option>
|
||||
<a-select-option value="closed">关闭</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="currentAction !== 'approve'" label="原因">
|
||||
<a-textarea
|
||||
v-model:value="actionForm.reason"
|
||||
:rows="3"
|
||||
placeholder="填写可审计的审核、治理或申诉处理原因"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="
|
||||
currentAction === 'approve' ||
|
||||
currentAction === 'reject' ||
|
||||
currentAction === 'resolve'
|
||||
"
|
||||
label="证据标签"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="actionForm.evidenceTags"
|
||||
placeholder="多个标签用英文逗号分隔"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-alert
|
||||
show-icon
|
||||
type="warning"
|
||||
message="所有处置只改变公共市场对象和后续来源可用性,不能直接修改用户私有副本或已确认正文。"
|
||||
/>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</a-space>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.market-page {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-heading h1 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.page-heading p {
|
||||
margin: 4px 0 0;
|
||||
color: rgb(0 0 0 / 45%);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
758
muse-admin/apps/web-antd/src/views/muse/newapi/index.vue
Normal file
758
muse-admin/apps/web-antd/src/views/muse/newapi/index.vue
Normal file
@ -0,0 +1,758 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* New-API 网关用户与用量页
|
||||
*
|
||||
* 只展示网关绑定、余额快照、调用日志归属和异常处理摘要。
|
||||
* 页面不展示任何 New-API 凭据,不把余额快照当作 Muse 本地权威余额。
|
||||
*/
|
||||
import type {
|
||||
AdminUsageRecord,
|
||||
BalanceSnapshotEntry,
|
||||
IntegrationCallDetail,
|
||||
NewApiBindingStatus,
|
||||
NewApiBindingSummary,
|
||||
} from '#/api/muse/newapi';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Descriptions,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
createNewApiCallAttributionJobApi,
|
||||
createNewApiGatewayBindingApi,
|
||||
createNewApiQuotaRequestApi,
|
||||
getNewApiIntegrationCallApi,
|
||||
listNewApiBalanceSnapshotsApi,
|
||||
listNewApiGatewayUsersApi,
|
||||
listNewApiUsageRecordsApi,
|
||||
} from '#/api/muse/newapi';
|
||||
|
||||
/** 表格分页对象。 */
|
||||
interface TablePager {
|
||||
/** 当前页码。 */
|
||||
current?: number;
|
||||
/** 每页数量。 */
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
/** Ant Table 插槽传入的通用记录。 */
|
||||
type TableRecord = Record<string, unknown>;
|
||||
|
||||
/** 创建或刷新网关绑定表单。 */
|
||||
interface BindingCommandForm {
|
||||
/** 目标用户 ID。 */
|
||||
userId: string;
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
/** 是否强制刷新已有绑定。 */
|
||||
forceRefresh: boolean;
|
||||
/** 操作原因。 */
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/** New-API 额度配置请求表单。 */
|
||||
interface QuotaRequestForm {
|
||||
/** 目标用户 ID。 */
|
||||
userId: string;
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
/** 请求类型。 */
|
||||
requestType: 'balance_config' | 'plan_config' | 'subscription_sync';
|
||||
/** 操作原因。 */
|
||||
reason: string;
|
||||
/** 资源类型。 */
|
||||
resourceType: string;
|
||||
/** 目标额度。 */
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/** 调用归属 job 表单。 */
|
||||
interface AttributionJobForm {
|
||||
/** 幂等键。 */
|
||||
commandId: string;
|
||||
/** New-API 调用关联 ID。 */
|
||||
correlationId: string;
|
||||
/** 调用 ID 白名单,逗号分隔。 */
|
||||
callIds: string;
|
||||
/** 调用审计 revision。 */
|
||||
expectedCallRevision?: number;
|
||||
/** 归属原因。 */
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/** 网关绑定状态颜色。 */
|
||||
const bindingStatusColor: Record<NewApiBindingStatus, string> = {
|
||||
bound: 'green',
|
||||
pending: 'blue',
|
||||
sync_failed: 'red',
|
||||
unbound: 'default',
|
||||
};
|
||||
|
||||
/** 网关绑定状态中文文案。 */
|
||||
const bindingStatusText: Record<NewApiBindingStatus, string> = {
|
||||
bound: '已绑定',
|
||||
pending: '处理中',
|
||||
sync_failed: '同步失败',
|
||||
unbound: '未创建',
|
||||
};
|
||||
|
||||
/** 将 Ant Table 通用记录转回 New-API 绑定摘要。 */
|
||||
function asNewApiBinding(record: TableRecord) {
|
||||
return record as unknown as NewApiBindingSummary;
|
||||
}
|
||||
|
||||
/** 将 Ant Table 通用记录转回用量摘要。 */
|
||||
function asUsageRecord(record: TableRecord) {
|
||||
return record as unknown as AdminUsageRecord;
|
||||
}
|
||||
|
||||
/** 网关绑定状态颜色,兼容模板中的 unknown 记录。 */
|
||||
function getBindingStatusColor(status: unknown) {
|
||||
return bindingStatusColor[status as NewApiBindingStatus] ?? 'default';
|
||||
}
|
||||
|
||||
/** 网关绑定状态文案,兼容模板中的 unknown 记录。 */
|
||||
function getBindingStatusText(status: unknown) {
|
||||
return bindingStatusText[status as NewApiBindingStatus] ?? displayText(String(status));
|
||||
}
|
||||
|
||||
const bindingColumns = [
|
||||
{ dataIndex: 'userId', key: 'userId', title: '用户 ID' },
|
||||
{ dataIndex: 'nickname', key: 'nickname', title: '昵称' },
|
||||
{ dataIndex: 'bindingStatus', key: 'bindingStatus', title: '绑定状态' },
|
||||
{ dataIndex: 'boundAt', key: 'boundAt', title: '绑定时间' },
|
||||
{ dataIndex: 'lastSyncAt', key: 'lastSyncAt', title: '最近同步' },
|
||||
{ dataIndex: 'syncErrorMessage', key: 'syncErrorMessage', title: '异常摘要' },
|
||||
{ key: 'actions', title: '操作', width: 280 },
|
||||
];
|
||||
|
||||
const balanceColumns = [
|
||||
{ dataIndex: 'snapshotId', key: 'snapshotId', title: '快照 ID' },
|
||||
{ dataIndex: 'resourceType', key: 'resourceType', title: '资源类型' },
|
||||
{ dataIndex: 'balance', key: 'balance', title: '余额快照' },
|
||||
{ dataIndex: 'usedQuota', key: 'usedQuota', title: '已用配额' },
|
||||
{ dataIndex: 'totalQuota', key: 'totalQuota', title: '总配额' },
|
||||
{ dataIndex: 'source', key: 'source', title: '来源' },
|
||||
{ dataIndex: 'capturedAt', key: 'capturedAt', title: '捕获时间' },
|
||||
];
|
||||
|
||||
const usageColumns = [
|
||||
{ dataIndex: 'recordId', key: 'recordId', title: '记录 ID' },
|
||||
{ dataIndex: 'userId', key: 'userId', title: '用户 ID' },
|
||||
{ dataIndex: 'period', key: 'period', title: '周期' },
|
||||
{ dataIndex: 'totalInputTokens', key: 'totalInputTokens', title: '输入 Token' },
|
||||
{
|
||||
dataIndex: 'totalOutputTokens',
|
||||
key: 'totalOutputTokens',
|
||||
title: '输出 Token',
|
||||
},
|
||||
{ dataIndex: 'totalTokens', key: 'totalTokens', title: '总 Token' },
|
||||
{ dataIndex: 'attributionStatus', key: 'attributionStatus', title: '归属状态' },
|
||||
{
|
||||
dataIndex: 'failedAttributionCount',
|
||||
key: 'failedAttributionCount',
|
||||
title: '归属失败',
|
||||
},
|
||||
{ key: 'actions', title: '异常处理', width: 180 },
|
||||
];
|
||||
|
||||
const bindingLoading = ref(false);
|
||||
const balanceLoading = ref(false);
|
||||
const usageLoading = ref(false);
|
||||
const commandSubmitting = ref(false);
|
||||
|
||||
const bindings = ref<NewApiBindingSummary[]>([]);
|
||||
const balanceSnapshots = ref<BalanceSnapshotEntry[]>([]);
|
||||
const usageRecords = ref<AdminUsageRecord[]>([]);
|
||||
const selectedBinding = ref<NewApiBindingSummary>();
|
||||
const integrationCall = ref<IntegrationCallDetail>();
|
||||
|
||||
const bindingFilters = reactive({
|
||||
bindingStatus: undefined as NewApiBindingStatus | undefined,
|
||||
});
|
||||
|
||||
const usageFilters = reactive({
|
||||
attributionStatus: undefined as AdminUsageRecord['attributionStatus'] | undefined,
|
||||
userId: '',
|
||||
});
|
||||
|
||||
const bindingPager = reactive({ current: 1, pageSize: 10, total: 0 });
|
||||
const balancePager = reactive({ current: 1, pageSize: 10, total: 0 });
|
||||
const usagePager = reactive({ current: 1, pageSize: 10, total: 0 });
|
||||
|
||||
const bindingModalOpen = ref(false);
|
||||
const quotaModalOpen = ref(false);
|
||||
const attributionModalOpen = ref(false);
|
||||
const integrationQuery = ref('');
|
||||
|
||||
const bindingForm = reactive<BindingCommandForm>({
|
||||
commandId: '',
|
||||
forceRefresh: false,
|
||||
reason: '',
|
||||
userId: '',
|
||||
});
|
||||
|
||||
const quotaForm = reactive<QuotaRequestForm>({
|
||||
commandId: '',
|
||||
reason: '',
|
||||
requestType: 'subscription_sync',
|
||||
resourceType: '',
|
||||
userId: '',
|
||||
});
|
||||
|
||||
const attributionForm = reactive<AttributionJobForm>({
|
||||
callIds: '',
|
||||
commandId: '',
|
||||
correlationId: '',
|
||||
reason: '',
|
||||
});
|
||||
|
||||
/** 生成前端幂等键,后端负责最终幂等校验。 */
|
||||
function createCommandId(prefix: string) {
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
/** 统一空值展示。 */
|
||||
function displayText(value?: number | string) {
|
||||
return value === undefined || value === '' ? '-' : String(value);
|
||||
}
|
||||
|
||||
/** 查询网关用户绑定列表。 */
|
||||
async function loadBindings(
|
||||
page = bindingPager.current,
|
||||
pageSize = bindingPager.pageSize,
|
||||
) {
|
||||
bindingLoading.value = true;
|
||||
try {
|
||||
const data = await listNewApiGatewayUsersApi({
|
||||
bindingStatus: bindingFilters.bindingStatus,
|
||||
pageNo: page,
|
||||
pageSize,
|
||||
});
|
||||
bindings.value = data.list;
|
||||
bindingPager.current = page;
|
||||
bindingPager.pageSize = pageSize;
|
||||
bindingPager.total = data.total;
|
||||
} finally {
|
||||
bindingLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询选中用户的余额快照,页面不伪造 New-API 权威余额。 */
|
||||
async function loadBalanceSnapshots(
|
||||
userId = selectedBinding.value?.userId,
|
||||
page = balancePager.current,
|
||||
pageSize = balancePager.pageSize,
|
||||
) {
|
||||
if (!userId) {
|
||||
balanceSnapshots.value = [];
|
||||
return;
|
||||
}
|
||||
balanceLoading.value = true;
|
||||
try {
|
||||
const data = await listNewApiBalanceSnapshotsApi(userId, {
|
||||
pageNo: page,
|
||||
pageSize,
|
||||
});
|
||||
balanceSnapshots.value = data.list;
|
||||
balancePager.current = page;
|
||||
balancePager.pageSize = pageSize;
|
||||
balancePager.total = data.total;
|
||||
} finally {
|
||||
balanceLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询调用日志归属后的用量摘要。 */
|
||||
async function loadUsageRecords(
|
||||
page = usagePager.current,
|
||||
pageSize = usagePager.pageSize,
|
||||
) {
|
||||
usageLoading.value = true;
|
||||
try {
|
||||
const data = await listNewApiUsageRecordsApi({
|
||||
attributionStatus: usageFilters.attributionStatus,
|
||||
pageNo: page,
|
||||
pageSize,
|
||||
userId: usageFilters.userId || undefined,
|
||||
});
|
||||
usageRecords.value = data.list;
|
||||
usagePager.current = page;
|
||||
usagePager.pageSize = pageSize;
|
||||
usagePager.total = data.total;
|
||||
} finally {
|
||||
usageLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 选择网关绑定行并加载余额快照。 */
|
||||
async function selectBinding(record: NewApiBindingSummary) {
|
||||
selectedBinding.value = record;
|
||||
usageFilters.userId = record.userId;
|
||||
await Promise.all([loadBalanceSnapshots(record.userId, 1), loadUsageRecords(1)]);
|
||||
}
|
||||
|
||||
/** 绑定表格分页变化。 */
|
||||
function handleBindingTableChange(pager: TablePager) {
|
||||
loadBindings(pager.current ?? 1, pager.pageSize ?? bindingPager.pageSize);
|
||||
}
|
||||
|
||||
/** 余额快照分页变化。 */
|
||||
function handleBalanceTableChange(pager: TablePager) {
|
||||
loadBalanceSnapshots(
|
||||
selectedBinding.value?.userId,
|
||||
pager.current ?? 1,
|
||||
pager.pageSize ?? balancePager.pageSize,
|
||||
);
|
||||
}
|
||||
|
||||
/** 用量表格分页变化。 */
|
||||
function handleUsageTableChange(pager: TablePager) {
|
||||
loadUsageRecords(pager.current ?? 1, pager.pageSize ?? usagePager.pageSize);
|
||||
}
|
||||
|
||||
/** 打开创建或刷新网关绑定弹窗。 */
|
||||
function openBindingModal(record?: NewApiBindingSummary) {
|
||||
bindingForm.commandId = createCommandId('newapi-bind');
|
||||
bindingForm.forceRefresh = record?.bindingStatus === 'sync_failed';
|
||||
bindingForm.reason = '';
|
||||
bindingForm.userId = record?.userId ?? selectedBinding.value?.userId ?? '';
|
||||
bindingModalOpen.value = true;
|
||||
}
|
||||
|
||||
/** 表格事件包装:选择绑定用户。 */
|
||||
function handleSelectBinding(record: TableRecord) {
|
||||
return selectBinding(asNewApiBinding(record));
|
||||
}
|
||||
|
||||
/** 表格事件包装:打开绑定命令。 */
|
||||
function handleOpenBindingModal(record: TableRecord) {
|
||||
openBindingModal(asNewApiBinding(record));
|
||||
}
|
||||
|
||||
/** 表格事件包装:打开额度配置命令。 */
|
||||
function handleOpenQuotaModal(record: TableRecord) {
|
||||
openQuotaModal(asNewApiBinding(record));
|
||||
}
|
||||
|
||||
/** 表格事件包装:打开用量归属异常处理命令。 */
|
||||
function handleOpenAttributionModal(record: TableRecord) {
|
||||
openAttributionModal(asUsageRecord(record));
|
||||
}
|
||||
|
||||
/** 提交创建或刷新网关绑定命令。 */
|
||||
async function submitBindingCommand() {
|
||||
if (!bindingForm.userId || !bindingForm.reason.trim()) {
|
||||
message.error('请填写用户 ID 和原因');
|
||||
return;
|
||||
}
|
||||
commandSubmitting.value = true;
|
||||
try {
|
||||
await createNewApiGatewayBindingApi(bindingForm.userId, {
|
||||
commandId: bindingForm.commandId,
|
||||
forceRefresh: bindingForm.forceRefresh,
|
||||
reason: bindingForm.reason,
|
||||
});
|
||||
message.success('网关绑定命令已提交');
|
||||
bindingModalOpen.value = false;
|
||||
await loadBindings();
|
||||
} finally {
|
||||
commandSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开 New-API 额度配置请求弹窗。 */
|
||||
function openQuotaModal(record?: NewApiBindingSummary) {
|
||||
quotaForm.commandId = createCommandId('newapi-quota');
|
||||
quotaForm.limit = undefined;
|
||||
quotaForm.reason = '';
|
||||
quotaForm.requestType = 'subscription_sync';
|
||||
quotaForm.resourceType = '';
|
||||
quotaForm.userId = record?.userId ?? selectedBinding.value?.userId ?? '';
|
||||
quotaModalOpen.value = true;
|
||||
}
|
||||
|
||||
/** 提交 New-API 额度配置请求。 */
|
||||
async function submitQuotaRequest() {
|
||||
if (!quotaForm.userId || !quotaForm.reason.trim()) {
|
||||
message.error('请填写用户 ID 和原因');
|
||||
return;
|
||||
}
|
||||
commandSubmitting.value = true;
|
||||
try {
|
||||
await createNewApiQuotaRequestApi(quotaForm.userId, {
|
||||
commandId: quotaForm.commandId,
|
||||
reason: quotaForm.reason,
|
||||
requestType: quotaForm.requestType,
|
||||
targetQuota: quotaForm.resourceType
|
||||
? { limit: quotaForm.limit, resourceType: quotaForm.resourceType }
|
||||
: undefined,
|
||||
});
|
||||
message.success('额度配置请求已创建');
|
||||
quotaModalOpen.value = false;
|
||||
} finally {
|
||||
commandSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开调用归属 job 弹窗。 */
|
||||
function openAttributionModal(record?: AdminUsageRecord) {
|
||||
attributionForm.callIds = '';
|
||||
attributionForm.commandId = createCommandId('newapi-attr');
|
||||
attributionForm.correlationId = '';
|
||||
attributionForm.expectedCallRevision = undefined;
|
||||
attributionForm.reason = record?.recordId
|
||||
? `处理用量记录 ${record.recordId} 的归属异常`
|
||||
: '';
|
||||
attributionModalOpen.value = true;
|
||||
}
|
||||
|
||||
/** 提交调用归属 job。 */
|
||||
async function submitAttributionJob() {
|
||||
if (!attributionForm.correlationId || !attributionForm.reason.trim()) {
|
||||
message.error('请填写 correlationId 和归属原因');
|
||||
return;
|
||||
}
|
||||
commandSubmitting.value = true;
|
||||
try {
|
||||
await createNewApiCallAttributionJobApi({
|
||||
callIds: attributionForm.callIds
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean),
|
||||
commandId: attributionForm.commandId,
|
||||
correlationId: attributionForm.correlationId,
|
||||
expectedCallRevision: attributionForm.expectedCallRevision,
|
||||
reason: attributionForm.reason,
|
||||
verificationMode: 'strict',
|
||||
});
|
||||
message.success('调用归属 job 已创建');
|
||||
attributionModalOpen.value = false;
|
||||
} finally {
|
||||
commandSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 按 correlationId 查询外部调用状态。 */
|
||||
async function queryIntegrationCall() {
|
||||
if (!integrationQuery.value.trim()) {
|
||||
message.error('请填写 correlationId');
|
||||
return;
|
||||
}
|
||||
integrationCall.value = await getNewApiIntegrationCallApi(
|
||||
integrationQuery.value.trim(),
|
||||
);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadBindings();
|
||||
loadUsageRecords();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<div class="space-y-4">
|
||||
<Alert
|
||||
show-icon
|
||||
type="warning"
|
||||
message="New-API 页面只展示治理和对账摘要;不展示凭据,不伪造余额,不管理模型供应商路由。"
|
||||
/>
|
||||
|
||||
<Tabs>
|
||||
<Tabs.TabPane key="binding" tab="网关绑定">
|
||||
<Card title="New-API 网关用户">
|
||||
<Space class="mb-4" wrap>
|
||||
<Select
|
||||
v-model:value="bindingFilters.bindingStatus"
|
||||
allow-clear
|
||||
placeholder="绑定状态"
|
||||
style="width: 180px"
|
||||
>
|
||||
<Select.Option value="bound">已绑定</Select.Option>
|
||||
<Select.Option value="pending">处理中</Select.Option>
|
||||
<Select.Option value="sync_failed">同步失败</Select.Option>
|
||||
<Select.Option value="unbound">未创建</Select.Option>
|
||||
</Select>
|
||||
<Button
|
||||
type="primary"
|
||||
@click="loadBindings(1, bindingPager.pageSize)"
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
<Button @click="openBindingModal()">创建绑定</Button>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
row-key="userId"
|
||||
:columns="bindingColumns"
|
||||
:data-source="bindings"
|
||||
:loading="bindingLoading"
|
||||
:pagination="bindingPager"
|
||||
@change="handleBindingTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'bindingStatus'">
|
||||
<Tag :color="getBindingStatusColor(record.bindingStatus)">
|
||||
{{ getBindingStatusText(record.bindingStatus) }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<Space wrap>
|
||||
<Button type="link" @click="handleSelectBinding(record)">
|
||||
余额/用量
|
||||
</Button>
|
||||
<Button type="link" @click="handleOpenBindingModal(record)">
|
||||
刷新绑定
|
||||
</Button>
|
||||
<Button type="link" @click="handleOpenQuotaModal(record)">
|
||||
配置额度
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</Card>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane key="balance" tab="余额与额度">
|
||||
<Card title="余额快照">
|
||||
<Alert
|
||||
v-if="!selectedBinding"
|
||||
class="mb-4"
|
||||
show-icon
|
||||
message="请选择网关绑定用户后再查询余额快照。"
|
||||
/>
|
||||
<Descriptions
|
||||
v-else
|
||||
bordered
|
||||
class="mb-4"
|
||||
size="small"
|
||||
:column="3"
|
||||
>
|
||||
<Descriptions.Item label="用户 ID">
|
||||
{{ selectedBinding.userId }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="绑定状态">
|
||||
{{ bindingStatusText[selectedBinding.bindingStatus] }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最近同步">
|
||||
{{ displayText(selectedBinding.lastSyncAt) }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Table
|
||||
row-key="snapshotId"
|
||||
:columns="balanceColumns"
|
||||
:data-source="balanceSnapshots"
|
||||
:loading="balanceLoading"
|
||||
:pagination="balancePager"
|
||||
size="small"
|
||||
@change="handleBalanceTableChange"
|
||||
/>
|
||||
</Card>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane key="usage" tab="调用归属">
|
||||
<Card title="调用日志归属与异常处理">
|
||||
<Space class="mb-4" wrap>
|
||||
<Input
|
||||
v-model:value="usageFilters.userId"
|
||||
allow-clear
|
||||
placeholder="用户 ID"
|
||||
style="width: 220px"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="usageFilters.attributionStatus"
|
||||
allow-clear
|
||||
placeholder="归属状态"
|
||||
style="width: 180px"
|
||||
>
|
||||
<Select.Option value="attributed">已归属</Select.Option>
|
||||
<Select.Option value="pending">待归属</Select.Option>
|
||||
<Select.Option value="failed">归属失败</Select.Option>
|
||||
</Select>
|
||||
<Button
|
||||
type="primary"
|
||||
@click="loadUsageRecords(1, usagePager.pageSize)"
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
<Button @click="openAttributionModal()">创建归属 job</Button>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
row-key="recordId"
|
||||
:columns="usageColumns"
|
||||
:data-source="usageRecords"
|
||||
:loading="usageLoading"
|
||||
:pagination="usagePager"
|
||||
@change="handleUsageTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'actions'">
|
||||
<Button type="link" @click="handleOpenAttributionModal(record)">
|
||||
处理异常
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</Card>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane key="integration" tab="外部调用查询">
|
||||
<Card title="按 correlationId 查询">
|
||||
<Space class="mb-4" wrap>
|
||||
<Input
|
||||
v-model:value="integrationQuery"
|
||||
allow-clear
|
||||
placeholder="correlationId"
|
||||
style="width: 360px"
|
||||
/>
|
||||
<Button type="primary" @click="queryIntegrationCall">查询</Button>
|
||||
</Space>
|
||||
<Descriptions
|
||||
v-if="integrationCall"
|
||||
bordered
|
||||
size="small"
|
||||
:column="2"
|
||||
>
|
||||
<Descriptions.Item label="correlationId">
|
||||
{{ integrationCall.correlationId }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
{{ integrationCall.status }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="请求类型">
|
||||
{{ displayText(integrationCall.requestType) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="归属状态">
|
||||
{{ displayText(integrationCall.attributionStatus) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="归属用户">
|
||||
{{ displayText(integrationCall.attributedUserId) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="错误摘要">
|
||||
{{ displayText(integrationCall.errorMessage) }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
v-model:open="bindingModalOpen"
|
||||
:confirm-loading="commandSubmitting"
|
||||
title="创建或刷新网关绑定"
|
||||
@ok="submitBindingCommand"
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="用户 ID" required>
|
||||
<Input v-model:value="bindingForm.userId" />
|
||||
</Form.Item>
|
||||
<Form.Item label="幂等键" required>
|
||||
<Input v-model:value="bindingForm.commandId" />
|
||||
</Form.Item>
|
||||
<Form.Item label="强制刷新">
|
||||
<Checkbox v-model:checked="bindingForm.forceRefresh">
|
||||
同步失败或人工补偿时强制刷新
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item label="原因" required>
|
||||
<Input.TextArea v-model:value="bindingForm.reason" :rows="3" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
v-model:open="quotaModalOpen"
|
||||
:confirm-loading="commandSubmitting"
|
||||
title="New-API 额度配置请求"
|
||||
@ok="submitQuotaRequest"
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="用户 ID" required>
|
||||
<Input v-model:value="quotaForm.userId" />
|
||||
</Form.Item>
|
||||
<Form.Item label="请求类型" required>
|
||||
<Select v-model:value="quotaForm.requestType">
|
||||
<Select.Option value="plan_config">套餐配置</Select.Option>
|
||||
<Select.Option value="balance_config">余额配置</Select.Option>
|
||||
<Select.Option value="subscription_sync">订阅同步</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="目标额度">
|
||||
<Space>
|
||||
<Input
|
||||
v-model:value="quotaForm.resourceType"
|
||||
placeholder="资源类型"
|
||||
/>
|
||||
<InputNumber v-model:value="quotaForm.limit" placeholder="额度" />
|
||||
</Space>
|
||||
</Form.Item>
|
||||
<Form.Item label="幂等键" required>
|
||||
<Input v-model:value="quotaForm.commandId" />
|
||||
</Form.Item>
|
||||
<Form.Item label="原因" required>
|
||||
<Input.TextArea v-model:value="quotaForm.reason" :rows="3" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
v-model:open="attributionModalOpen"
|
||||
:confirm-loading="commandSubmitting"
|
||||
title="创建调用归属 job"
|
||||
@ok="submitAttributionJob"
|
||||
>
|
||||
<Alert
|
||||
class="mb-4"
|
||||
show-icon
|
||||
type="info"
|
||||
message="归属事实由服务端根据调用审计记录、任务、作品、资产和授权关系严格校验。"
|
||||
/>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="correlationId" required>
|
||||
<Input v-model:value="attributionForm.correlationId" />
|
||||
</Form.Item>
|
||||
<Form.Item label="callIds 白名单">
|
||||
<Input
|
||||
v-model:value="attributionForm.callIds"
|
||||
placeholder="多个 callId 用逗号分隔"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="调用 revision">
|
||||
<InputNumber
|
||||
v-model:value="attributionForm.expectedCallRevision"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="幂等键" required>
|
||||
<Input v-model:value="attributionForm.commandId" />
|
||||
</Form.Item>
|
||||
<Form.Item label="原因" required>
|
||||
<Input.TextArea v-model:value="attributionForm.reason" :rows="3" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Page>
|
||||
</template>
|
||||
Loading…
x
Reference in New Issue
Block a user