init: AIGC-Hub/AVCC 方案文档 + TCS-IPTV 内容可信锁定系统 MVP

- 方案文档: AVCC 体系建设、IPTV TCS 需求(0-req)/PRD(1-prd)/任务(2-task)/二三四期任务
- tcs-iptv: Go 后端(哈希SDK/MA码生成/可信数据空间mock/业务编排/HTTP API+HMAC鉴权)
- web-console: React+AntD 监管大屏(角色工作台/全流程演示/监管片库)
- 一剧一码+集级哈希, 集级下架/恢复, 全栈测试通过
This commit is contained in:
selfrelease
2026-06-14 16:50:31 +08:00
commit a329d4906b
103 changed files with 20052 additions and 0 deletions
+276
View File
@@ -0,0 +1,276 @@
import React, { useState } from 'react'
import {
Card, Steps, Button, Space, Input, Select, Form, Tag, Timeline,
Descriptions, message, Row, Col, Typography, Divider, Alert, Table,
} from 'antd'
import {
PlayCircleOutlined, RedoOutlined, StopOutlined, SafetyCertificateOutlined,
} from '@ant-design/icons'
import { api } from './api.js'
const { Text, Paragraph } = Typography
const roleColor = { cp: 'green', regulator: 'red', reviewer: 'blue', operator: 'orange' }
const roleLabel = { cp: '内容提供商', regulator: '监管主体', reviewer: '审核/媒资', operator: '运营商' }
// 七步流水线定义(审核在前,发码在后——审过才发证发码)
const STEPS = [
{ key: 'register', title: 'CP 送审', role: 'cp', desc: '原片送 CSPS 既有审核渠道 + 哈希包上链(链上不存原片)' },
{ key: 'csps', title: 'CSPS 合规审核', role: 'reviewer', desc: '基于原片审画面/台词/声音;验真送审哈希=上链哈希(审播一致)' },
{ key: 'issue', title: '审核通过·发码签发', role: 'regulator', desc: '审过才发码:按号段生成 MA 码,1:1 强绑定哈希' },
{ key: 'ingest', title: '媒资库入库', role: 'reviewer', desc: '审合格入库,建立媒资编码映射' },
{ key: 'publish', title: '发布给运营商', role: 'reviewer', desc: '携 MA 码+哈希证书发布' },
{ key: 'inject', title: 'CDN 注入校验', role: 'operator', desc: '注入前哈希比对,匹配放行(防偷换)' },
]
export default function FlowDemo() {
const [form] = Form.useForm()
const [current, setCurrent] = useState(0)
const [running, setRunning] = useState(false)
const [logs, setLogs] = useState([])
const [ctx, setCtx] = useState({}) // reviewID/ctid/maCode/cert/fileHash/episodeHashes
const [done, setDone] = useState(false)
const [episodes, setEpisodes] = useState([]) // 集级哈希列表
const [epVerify, setEpVerify] = useState({}) // {episode: 'match'|'mismatch'}
function addLog(role, title, ok, detail) {
setLogs((prev) => [...prev, { role, title, ok, detail, t: new Date().toLocaleTimeString() }])
}
function reset() {
setCurrent(0); setLogs([]); setCtx({}); setDone(false); setRunning(false)
setEpisodes([]); setEpVerify({})
}
// 执行单步,返回是否成功
async function runStep(idx, shared) {
const step = STEPS[idx]
const v = form.getFieldsValue()
const fileHash = shared.fileHash
let res
switch (step.key) {
case 'register':
// 一剧一码 + 集级哈希:按集数生成每集独立哈希
shared.episodeHashes = []
const epArr = []
const epCount = Number(v.episodes) || 1
for (let i = 1; i <= epCount; i++) {
const eh = `${fileHash}-E${i}`
shared.episodeHashes.push({ episode: i, hash: eh })
epArr.push({ episode: i, file_sha256: eh, merkle_root: `mr-${eh}` })
}
res = await api.register({
title: v.title, episode_count: epCount, category: v.category,
file_sha256: fileHash, merkle_root: 'mr-' + fileHash, perceptual_hash: 'ph-' + fileHash,
episodes: epArr,
cp_media_id: v.cpId, cp_name: v.cpName,
})
if (res.ok) { shared.reviewID = res.data.data.review_id; shared.ctid = res.data.data.content_twin_id }
addLog(step.role, step.title, res.ok, res.ok ? `流水号 ${shared.reviewID}${epCount}集,每集独立哈希)` : res.data.message)
break
case 'issue':
res = await api.issue({ review_id: shared.reviewID, issuer: '陕西IPTV运营公司' })
if (res.ok) { shared.maCode = res.data.data.ma_code; shared.cert = res.data.data.certificate }
addLog(step.role, step.title, res.ok, res.ok ? `MA码 ${shared.maCode}` : res.data.message)
break
case 'csps':
res = await api.csps({ review_id: shared.reviewID, approved: true, reviewer_id: 'sxiptv-审核01' })
addLog(step.role, step.title, res.ok, res.ok ? '审核通过(发码前置)' : res.data.message)
break
case 'ingest':
res = await api.ingest({
ma_code: shared.maCode, content_twin_id: shared.ctid,
media_asset_id: 'SXMEDIA-' + fileHash, lib_name: '陕西IPTV媒体资源库',
})
addLog(step.role, step.title, res.ok, res.ok ? '已入媒资库' : res.data.message)
break
case 'publish':
res = await api.publish({ ma_code: shared.maCode, certificate: shared.cert })
addLog(step.role, step.title, res.ok, res.ok ? '已发布' : res.data.message)
break
case 'inject':
res = await api.inject({
content_twin_id: shared.ctid, ma_code: shared.maCode, file_sha256: fileHash,
operator_id: v.opId, cdn_endpoint: v.cdn,
})
addLog(step.role, step.title, res.ok,
res.ok ? `注入成功 ${res.data.data.distribution_id}` : res.data.message)
break
default:
res = { ok: false }
}
return res.ok
}
// 一键全流程
async function runAll() {
setRunning(true); setLogs([]); setDone(false); setCurrent(0)
const shared = { fileHash: 'fh-' + Date.now().toString(36) }
for (let i = 0; i < STEPS.length; i++) {
setCurrent(i)
const ok = await runStep(i, shared)
if (!ok) {
message.error(`${i + 1} 步「${STEPS[i].title}」失败,流程中断`)
setRunning(false); setCtx(shared)
return
}
await new Promise((r) => setTimeout(r, 500)) // 放慢便于演示观看
}
setCurrent(STEPS.length)
setCtx(shared); setDone(true); setRunning(false)
message.success('全流程跑通:审过即锁定,锁定即通行')
await loadEpisodes(shared.maCode)
}
async function loadEpisodes(maCode) {
const res = await api.episodes(maCode)
if (res.ok) setEpisodes(res.data.data.episodes || [])
}
// 按集验真:correct=true 用正确哈希,false 用篡改哈希
async function verifyEp(ep, correct) {
const realHash = (ctx.episodeHashes || []).find((e) => e.episode === ep)?.hash
const submit = correct ? realHash : 'TAMPERED-' + realHash
const res = await api.verifyEpisode(ctx.maCode, ep, submit)
const matched = res.ok && res.data.data?.match
setEpVerify((prev) => ({ ...prev, [ep]: matched ? 'match' : 'mismatch' }))
if (matched) message.success(`${ep}集验真通过`)
else message.warning(`${ep}集不匹配(疑似该集被替换)`)
}
async function doTakedown() {
const res = await api.takedown(ctx.maCode, '监管演示下架')
if (res.ok) {
addLog('regulator', '违规应急下架', true,
`秒级下架,受影响 CDN: ${(res.data.data.cdn_endpoints || []).join(', ')}`)
message.success('已全网下架')
} else message.error(res.data.message)
}
async function doTamperTest() {
const res = await api.inject({
content_twin_id: ctx.ctid, ma_code: ctx.maCode, file_sha256: 'TAMPERED-' + ctx.fileHash,
operator_id: 'OP-X', cdn_endpoint: 'cdn://x',
})
addLog('operator', '篡改注入测试', !res.ok, res.ok ? '⚠️ 异常:篡改竟通过' : '✅ 已拒绝:' + res.data.message)
if (!res.ok) message.success('篡改内容被正确拦截')
}
return (
<Row gutter={16}>
<Col span={9}>
<Card title="演示参数" size="small" style={{ marginBottom: 16 }}>
<Form form={form} layout="vertical" size="small" initialValues={{
title: '长安少年行', category: 'WD', episodes: 6,
cpId: 'XAQJSL-2026-001', cpName: '西安曲江丝路文化传播有限公司',
opId: 'CT-SX-IPTV', cdn: 'cdn://ct-sx/iptv/vod/changan001',
}}>
<Form.Item label="作品标题" name="title"><Input /></Form.Item>
<Row gutter={8}>
<Col span={12}>
<Form.Item label="类目" name="category">
<Select options={[
{ value: 'WD', label: '微短剧' }, { value: 'WJ', label: '网络剧' },
{ value: 'DY', label: '网络电影' }, { value: 'DH', label: '网络动画' },
]} />
</Form.Item>
</Col>
<Col span={12}><Form.Item label="集数" name="episodes"><Input type="number" /></Form.Item></Col>
</Row>
<Form.Item label="内容提供商 (CP)" name="cpName"><Input /></Form.Item>
<Form.Item label="CP 媒资编码" name="cpId"><Input /></Form.Item>
<Form.Item label="运营商编码" name="opId"><Input /></Form.Item>
<Form.Item label="CDN 端点" name="cdn"><Input /></Form.Item>
</Form>
<Space>
<Button type="primary" icon={<PlayCircleOutlined />} loading={running} onClick={runAll}>
一键全流程
</Button>
<Button icon={<RedoOutlined />} onClick={reset} disabled={running}>重置</Button>
</Space>
</Card>
</Col>
<Col span={15}>
<Card title="内容生命周期流水线" style={{ marginBottom: 16 }}>
<Steps
direction="vertical" size="small" current={current}
status={running ? 'process' : done ? 'finish' : 'wait'}
items={STEPS.map((s) => ({
title: <Space>{s.title}<Tag color={roleColor[s.role]}>{roleLabel[s.role]}</Tag></Space>,
description: s.desc,
}))}
/>
</Card>
{done && (
<Card title={<Space><SafetyCertificateOutlined />赋码结果双锚定</Space>} style={{ marginBottom: 16 }}>
<Descriptions bordered size="small" column={1}>
<Descriptions.Item label="MA 码(监管锚点)"><Text strong copyable>{ctx.maCode}</Text></Descriptions.Item>
<Descriptions.Item label="文件哈希(技术锚点)">{ctx.fileHash}</Descriptions.Item>
<Descriptions.Item label="CTID(机器主键)">{ctx.ctid}</Descriptions.Item>
</Descriptions>
<Divider />
<Space>
<Button onClick={doTamperTest}>篡改注入测试应拒绝</Button>
<Button danger icon={<StopOutlined />} onClick={doTakedown}>违规应急下架</Button>
</Space>
</Card>
)}
{done && episodes.length > 0 && (
<Card
title={<Space>集级面板<Tag color="purple">一剧一码 · {episodes.length} 集独立哈希</Tag></Space>}
style={{ marginBottom: 16 }}
extra={<Text type="secondary">集级子标识{ctx.maCode}#E01 </Text>}
>
<Table
size="small" rowKey="episode" pagination={false}
dataSource={episodes}
columns={[
{ title: '集号', dataIndex: 'episode', width: 70, render: (e) => <Tag> {e} </Tag> },
{ title: '集级子标识', render: (_, r) => <Text code>{`${ctx.maCode}#E${String(r.episode).padStart(2, '0')}`}</Text> },
{ title: '该集哈希', dataIndex: 'hash_value', ellipsis: true },
{
title: '验真状态', width: 110, render: (_, r) => {
const st = epVerify[r.episode]
if (st === 'match') return <Tag color="green">匹配</Tag>
if (st === 'mismatch') return <Tag color="red">不匹配</Tag>
return <Tag>未验</Tag>
},
},
{
title: '按集验真', width: 200, render: (_, r) => (
<Space>
<Button size="small" onClick={() => verifyEp(r.episode, true)}>正确</Button>
<Button size="small" danger onClick={() => verifyEp(r.episode, false)}>模拟篡改</Button>
</Space>
),
},
]}
/>
</Card>
)}
<Card title="执行日志" size="small">
{logs.length === 0
? <Alert type="info" message='点击「一键全流程」开始演示' />
: <Timeline items={logs.map((l) => ({
color: l.ok ? 'green' : 'red',
children: (
<Space direction="vertical" size={0}>
<Space>
<Tag color={roleColor[l.role]}>{roleLabel[l.role]}</Tag>
<Text strong>{l.title}</Text>
<Text type="secondary" style={{ fontSize: 12 }}>{l.t}</Text>
</Space>
<Text type={l.ok ? 'success' : 'danger'}>{l.detail}</Text>
</Space>
),
}))} />
}
</Card>
</Col>
</Row>
)
}