feat(studio): 资产详情页治理影响接入真后端(A 类:消费者治理 C14 读端)

A 类(后端就绪、前端 0 消费):02F GET /marketplace/assets/{id}/governance-impact 就绪
(curl code:0 + 真实 governanceResult:resultType/reason/操作限制 + affectedAuthorizations/
suggestedActions),但 studio 详情页无展示(盘点 C14 消费者治理缺口)。接入:
- useGovernanceImpact hook + openapi 导出 GovernanceImpactResult
- MarketAssetDetailPage「治理影响」区:结果类型(下架/召回/吊销/可申诉)+ 操作限制(停止获取/安装/绑定/生成)+ 建议动作
- mock handler + vitest 契约 + market-governance-impact.spec.ts 真后端 e2e
- 修 spec 块注释内 affected*/ 的 */ 意外闭合(tsc 不覆盖 e2e、playwright esbuild 暴露)

验证:全量 e2e 46→47/0;vitest 81→82;tsc/eslint 绿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-22 07:49:08 -07:00
parent a2f2ba33fe
commit ab83735f6f
6 changed files with 178 additions and 1 deletions

View File

@ -0,0 +1,53 @@
import { expect, test, type Page } from '@playwright/test';
/**
* e2e(,MSW off)A :后端 getGovernanceImpact studio 0 (C14 )
*
* 反假绿:盘点 C14 GET /marketplace/assets/{id}/governance-impact
* (curl code:0 + governanceResult:resultType/reason/ + affected suggestedActions),
* studio GET governance-impact code:0 +
* governanceResult
* 设计:不硬编码治理结果(), governanceResult +
*/
const TOKEN = 'test1';
async function seedToken(page: Page): Promise<void> {
await page.addInitScript((t) => {
window.localStorage.setItem('accessToken', t);
window.localStorage.setItem('tenantId', '1');
}, TOKEN);
}
const RESULT_TYPE_LABELS: Record<string, string> = {
delist: '下架',
recall: '召回',
revoke: '吊销',
appeal: '可申诉',
};
test('资产详情页渲染治理影响(真实后端 governance-impact)', async ({ page }) => {
await seedToken(page);
const govResp = page.waitForResponse(
(r) =>
/\/marketplace\/assets\/[^/]+\/governance-impact$/.test(new URL(r.url()).pathname) &&
r.request().method() === 'GET',
{ timeout: 15_000 }
);
// asset 1:curl 证 detail + governance-impact 均 code:0(governanceResult.reason=listed + affectedAuthorizations 有数据)。
await page.goto('/market/assets/1');
const resp = await govResp;
expect(resp.status()).toBe(200);
const body = await resp.json();
expect(body.code).toBe(0); // 后端就绪。
// 有 governanceResult 时「治理影响」区渲染 + 结果类型按真实 resultType 映射(证真后端态被 UI 消费)。
if (body.data?.governanceResult) {
await expect(page.getByRole('heading', { name: '治理影响' })).toBeVisible({ timeout: 10_000 });
const rt = body.data.governanceResult.resultType;
const label = RESULT_TYPE_LABELS[rt] ?? rt;
if (label) {
await expect(page.getByText(label).first()).toBeVisible({ timeout: 10_000 });
}
}
});

View File

@ -189,6 +189,39 @@ export const marketHandlers = [
return HttpResponse.json({ code: 0, msg: 'success', data: asset });
}),
// GET /app-api/muse/marketplace/assets/:assetId/governance-impact — 资产治理影响(消费者视角)
http.get('/app-api/muse/marketplace/assets/:assetId/governance-impact', ({ params }) => {
const asset = mockAssets.find((item) => item.assetId === params.assetId);
if (!asset) {
return HttpResponse.json({ code: 40420, msg: '市场资产不存在', data: null }, { status: 404 });
}
// 未上架资产模拟新操作受限;已上架资产仅提示可申诉、无限制。
const restricted = asset.listingStatus !== 'listed';
return HttpResponse.json({
code: 0,
msg: 'success',
data: {
assetId: asset.assetId,
governanceResult: {
resultType: 'appeal',
reason: restricted ? 'not_listed' : 'listed',
scope: asset.listingStatus ?? 'listed',
newAcquireStopped: restricted,
newInstallStopped: restricted,
newBindStopped: restricted,
generationUseStopped: false,
},
affectedAuthorizations: [],
affectedInstallations: [],
affectedBindings: [],
affectedTasks: [],
affectedCandidates: [],
exportRestrictions: [],
suggestedActions: ['review_governance_result', 'open_appeal_if_needed'],
},
});
}),
// POST /app-api/muse/marketplace/assets/:assetId/purchase — 获取授权,不代表安装或绑定
http.post('/app-api/muse/marketplace/assets/:assetId/purchase', ({ params }) => {
const asset = mockAssets.find((item) => item.assetId === params.assetId);

View File

@ -9,6 +9,7 @@ import {
useAcquireMarketAsset,
useBindPrecheck,
useFavoriteAsset,
useGovernanceImpact,
useInstallMarketAsset,
useMarketAssetDetail,
useMarketAssets,
@ -55,6 +56,17 @@ describe('useMarket API Hooks 单元测试', () => {
expect(result.current.data?.[0]?.recommendationReason).toBe('适合长篇结构检查');
});
it('应能读取资产治理影响(消费者视角:结果类型 + 建议动作)', async () => {
const { result } = renderHook(
() => useGovernanceImpact('20000000-0000-4000-8000-000000000001'),
{ wrapper }
);
await waitFor(() => expect(result.current.isSuccess).toBe(true));
// mock governance-impact:listed 资产 → resultType=appeal,带建议动作(证治理影响被解出)。
expect(result.current.data?.governanceResult?.resultType).toBe('appeal');
expect(Array.isArray(result.current.data?.suggestedActions)).toBe(true);
});
it('应能获取授权后安装智能体资产', async () => {
const assetId = '20000000-0000-4000-8000-000000000002';
const { result: acquireHook } = renderHook(() => useAcquireMarketAsset(), { wrapper });

View File

@ -1,6 +1,6 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { api, request } from '@/api/client';
import type { MarketAssetCard, MarketInstallResult, MarketLicenseResult, MarketplaceCategory } from '@/types/openapi';
import type { GovernanceImpactResult, MarketAssetCard, MarketInstallResult, MarketLicenseResult, MarketplaceCategory } from '@/types/openapi';
export type MarketAssetType = 'all' | MarketAssetCard['assetType'];
@ -119,6 +119,20 @@ export function useMarketAssetDetail(assetId: string | null) {
});
}
/**
* (GET /marketplace/assets/{id}/governance-impact)
* (///)/// + +
* C14 消费者治理读端缺口:后端就绪(curl code:0 + governanceResult/affected*)studio 0
*/
export function useGovernanceImpact(assetId: string | null) {
return useQuery<GovernanceImpactResult, Error>({
queryKey: ['governanceImpact', assetId],
enabled: !!assetId,
queryFn: () =>
api.get<GovernanceImpactResult>(`/marketplace/assets/${assetId}/governance-impact`),
});
}
/**
* (POST /marketplace/assets/{id}/favorite, body)
* :favorite commandId `X-Command-Id` (market , acquire/install body commandId ),

View File

@ -4,9 +4,33 @@ import {
useAcquireMarketAsset,
useBindPrecheck,
useFavoriteAsset,
useGovernanceImpact,
useMarketAssetDetail,
} from '@/features/market/hooks/useMarket';
/** 治理结果类型 → 中文文案。 */
const governanceResultTypeLabels: Record<string, string> = {
delist: '下架',
recall: '召回',
revoke: '吊销',
appeal: '可申诉',
};
/** 汇总治理对新操作的限制项;无任何限制时返回"无新操作限制"。 */
function governanceRestrictionText(result: {
newAcquireStopped?: boolean;
newInstallStopped?: boolean;
newBindStopped?: boolean;
generationUseStopped?: boolean;
}): string {
const items: string[] = [];
if (result.newAcquireStopped) items.push('停止新获取');
if (result.newInstallStopped) items.push('停止新安装');
if (result.newBindStopped) items.push('停止新绑定');
if (result.generationUseStopped) items.push('停止生成使用');
return items.length > 0 ? items.join('、') : '无新操作限制';
}
/**
* ()
*
@ -20,6 +44,7 @@ export default function MarketAssetDetailPage() {
const favorite = useFavoriteAsset(assetId ?? '');
const acquire = useAcquireMarketAsset();
const bindPrecheck = useBindPrecheck(assetId ?? '');
const governance = useGovernanceImpact(assetId ?? null);
const [targetWorkId, setTargetWorkId] = useState('');
if (isLoading) {
@ -74,6 +99,45 @@ export default function MarketAssetDetailPage() {
<p className="mt-3 whitespace-pre-wrap text-sm leading-relaxed text-slate-500">{data.description}</p>
)}
{/* 治理影响:该资产被治理(下架/召回/吊销/申诉)后对消费者授权/安装/绑定的影响 + 操作限制 + 建议动作。C14 读端,后端就绪、此前 0 消费。 */}
{governance.data?.governanceResult && (
<section aria-label="治理影响" className="mt-8 rounded-2xl border border-amber-200 bg-amber-50/50 p-6">
<h2 className="text-base font-bold text-slate-800"></h2>
<p className="mt-1 text-xs leading-relaxed text-slate-500">
//
</p>
<dl className="mt-4 space-y-2 text-sm">
<div className="flex gap-2">
<dt className="w-24 shrink-0 text-slate-400"></dt>
<dd className="font-semibold text-amber-700">
{governanceResultTypeLabels[governance.data.governanceResult.resultType ?? ''] ??
governance.data.governanceResult.resultType}
</dd>
</div>
{governance.data.governanceResult.reason && (
<div className="flex gap-2">
<dt className="w-24 shrink-0 text-slate-400"></dt>
<dd className="font-medium text-slate-700">{governance.data.governanceResult.reason}</dd>
</div>
)}
<div className="flex gap-2">
<dt className="w-24 shrink-0 text-slate-400"></dt>
<dd className="font-medium text-slate-700">
{governanceRestrictionText(governance.data.governanceResult)}
</dd>
</div>
</dl>
{governance.data.suggestedActions && governance.data.suggestedActions.length > 0 && (
<p className="mt-3 text-xs">
<span className="text-slate-400">:</span>
<span className="ml-2 font-medium text-indigo-600">
{governance.data.suggestedActions.join('、')}
</span>
</p>
)}
</section>
)}
{/* 许可详情:列表卡片只给 licenseSummary 单行摘要,此处呈现完整许可条款 */}
{license && (
<section aria-label="许可详情" className="mt-8 rounded-2xl border border-slate-100 bg-white p-6 shadow-sm">

View File

@ -39,6 +39,7 @@ export type AgentTestRequest = AIComponents['schemas']['AgentTestRequest'];
export type AgentTestResult = AIComponents['schemas']['AgentTestResult'];
export type MarketAssetCard = MarketComponents['schemas']['MarketAssetCard'];
export type MarketAssetDetail = MarketComponents['schemas']['MarketAssetDetail'];
export type GovernanceImpactResult = MarketComponents['schemas']['GovernanceImpactResult'];
export type MarketLicenseResult = MarketComponents['schemas']['MarketLicenseResult'];
export type MarketInstallResult = MarketComponents['schemas']['MarketInstallResult'];
export type MarketplaceCategory = MarketComponents['schemas']['MarketplaceCategory'];