feat(admin): 搭建 Muse 管理端治理页面

This commit is contained in:
zizi 2026-05-24 20:38:37 +08:00
parent 9df67b2794
commit 912ec7d77f
39 changed files with 30990 additions and 0 deletions

View File

@ -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' },
);
});
});

View 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,
);
}

View 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)}`,
);
}

View File

@ -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',
);
});
});

View 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)}`,
);
}

View 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 };

View File

@ -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,
);
});
});

View File

@ -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,
);
}

View 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,
);
}

View File

@ -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' }),
);
});
});

View 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,
);
}

View File

@ -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',
});
});
});

View 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),
);
}

View File

@ -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' }),
);
});
});

View 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,
);
}

View File

@ -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',
);
});
});

View 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';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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>;

File diff suppressed because it is too large Load Diff

View 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';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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;

View 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>

View File

@ -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();
},
};
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,461 @@
<script setup lang="ts">
/**
* 日志与审计页
*
* 分离接口调用日志和业务审计日志详情只展示脱敏摘要
* 不展示 tokensecret完整 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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();
});
});

View 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>

View File

@ -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();
});
});

View 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>

View 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>