feat(hololake): add human-confirmed PNCC mounts

This commit is contained in:
冰朔 2026-08-13 14:01:43 +08:00
commit e70c9201c2
10 changed files with 524 additions and 15 deletions

View file

@ -43,6 +43,26 @@ interface ReceiptProjection {
lastSequence: number
}
interface RepositoryInspection {
personaId: string
humanResponsibilitySubject: string
repositoryPath: string
gitHead: string
repositoryClean: boolean
currentCheckpoint: { relativePath: string; sha256: string }
modelInferenceStarted: boolean
realityExecutionAllowed: boolean
}
interface RepositoryCandidate {
schema: string
state: string
candidateId: string
confirmationToken: string
expiresAtUnixMs: number
inspection: RepositoryInspection
}
const themes: Array<{ id: ThemeId; name: string }> = [
{ id: 'night', name: '夜湖星光' },
{ id: 'dawn', name: '晨湖曦光' },
@ -88,6 +108,9 @@ function HoloLakeApp() {
const [panel, setPanel] = useState<Panel>(null)
const [ticket, setTicket] = useState<DiscoveryTicketReceipt | null>(null)
const [receipts, setReceipts] = useState<ReceiptEvent[]>([])
const [repositoryCandidate, setRepositoryCandidate] = useState<RepositoryCandidate | null>(null)
const [repositoryBusy, setRepositoryBusy] = useState(false)
const [repositoryMessage, setRepositoryMessage] = useState('')
const [message, setMessage] = useState('')
const refreshStatus = useCallback(async () => {
@ -117,6 +140,10 @@ function HoloLakeApp() {
const openReceipts = async () => {
setPanel('receipts')
await loadReceipts()
}
const loadReceipts = async () => {
try {
const projection = await invoke<ReceiptProjection>('query_pncc_receipt_projection', { input: { afterSequence: 0, limit: 25 } })
setReceipts(projection.events)
@ -125,6 +152,42 @@ function HoloLakeApp() {
}
}
const chooseRepository = async () => {
setRepositoryBusy(true)
setRepositoryMessage('')
try {
const candidate = await invoke<RepositoryCandidate | null>('select_pncc_repository_candidate')
setRepositoryCandidate(candidate)
if (!candidate) setRepositoryMessage('已取消选择,没有写入任何绑定。')
} catch (error) {
setRepositoryCandidate(null)
setRepositoryMessage(`这个文件夹不能作为代码通道:${String(error)}`)
} finally {
setRepositoryBusy(false)
}
}
const confirmRepository = async () => {
if (!repositoryCandidate) return
setRepositoryBusy(true)
setRepositoryMessage('')
try {
await invoke('confirm_pncc_repository_mount', {
input: {
candidateId: repositoryCandidate.candidateId,
confirmationToken: repositoryCandidate.confirmationToken,
},
})
setRepositoryCandidate(null)
setRepositoryMessage('代码仓库已按当前固定提交绑定为只读通道。')
await Promise.all([refreshStatus(), loadReceipts()])
} catch (error) {
setRepositoryMessage(`绑定没有写入:${String(error)}`)
} finally {
setRepositoryBusy(false)
}
}
const issueInvitation = async () => {
setMessage('')
try {
@ -238,7 +301,20 @@ function HoloLakeApp() {
<p className="panel-kicker"></p>
<h2 id="panel-title"></h2>
<p className="panel-intro">线</p>
{receipts.length ? <ol className="receipt-list">{receipts.map((receipt) => <li key={receipt.sequence}><span>{receipt.sequence.toString().padStart(2, '0')}</span><div><b>{receipt.kind === 'REPOSITORY_BINDING_REVALIDATED' ? '仓库绑定已复核' : '远端对象读取已验证'}</b><small>{new Date(Number(receipt.observedAtUnixMs)).toLocaleString('zh-CN')} · {receipt.eventHash.slice(0, 12)}</small></div></li>)}</ol> : <div className="empty-state"><i /><b></b><span></span></div>}
{repositoryCandidate ? <div className="candidate-card">
<div className="candidate-heading"><span className="status-light waiting" /><div><b></b><span>HoloLake Git </span></div></div>
<dl className="candidate-facts">
<div><dt></dt><dd>{repositoryCandidate.inspection.personaId}</dd></div>
<div><dt></dt><dd>{repositoryCandidate.inspection.humanResponsibilitySubject}</dd></div>
<div><dt></dt><dd>{repositoryCandidate.inspection.gitHead.slice(0, 12)}</dd></div>
<div><dt></dt><dd>{repositoryCandidate.inspection.repositoryClean ? '干净' : '有未提交变化 · 不读取'}</dd></div>
<div><dt></dt><dd>{repositoryCandidate.inspection.currentCheckpoint.relativePath}</dd></div>
</dl>
<p></p>
<div className="candidate-actions"><button className="primary-action" type="button" disabled={repositoryBusy} onClick={() => void confirmRepository()}><Icon name="arrow" /></button><button className="text-action" type="button" disabled={repositoryBusy} onClick={() => setRepositoryCandidate(null)}></button></div>
</div> : <button className="repository-picker" type="button" disabled={repositoryBusy} onClick={() => void chooseRepository()}><span><b>{status.codeRepositoryMountCount > 0 ? '再绑定一个代码仓库' : '绑定代码通道仓库'}</b><small> macOS </small></span><Icon name="arrow" /></button>}
{repositoryMessage && <p className="panel-message" aria-live="polite">{repositoryMessage}</p>}
{receipts.length ? <ol className="receipt-list">{receipts.map((receipt) => <li key={receipt.sequence}><span>{receipt.sequence.toString().padStart(2, '0')}</span><div><b>{receipt.kind === 'REPOSITORY_BINDING_REVALIDATED' ? '仓库绑定已复核' : '远端对象读取已验证'}</b><small>{new Date(Number(receipt.observedAtUnixMs)).toLocaleString('zh-CN')} · {receipt.eventHash.slice(0, 12)}</small></div></li>)}</ol> : <div className="empty-state compact"><i /><b></b><span></span></div>}
</>}
{panel === 'settings' && <>
<p className="panel-kicker"></p>

View file

@ -60,6 +60,7 @@ button:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 3px
.hero-copy { max-width: 540px; margin: 27px 0 0; color: var(--content-muted); font-size: 14px; line-height: 1.9; letter-spacing: .035em; }
.hero-actions { display: flex; align-items: center; gap: 14px; margin-top: 37px; }
.primary-action, .secondary-action { min-height: 46px; border-radius: 999px; padding: 0 22px; display: inline-flex; align-items: center; justify-content: center; gap: 12px; cursor: pointer; transition: transform 180ms ease, background 180ms ease, box-shadow 180ms ease; }
.primary-action:disabled, .secondary-action:disabled, .repository-picker:disabled, .text-action:disabled { opacity: .55; cursor: wait; transform: none; }
.primary-action { border: 0; color: var(--button-primary-text); background: var(--button-primary-bg); box-shadow: var(--button-primary-shadow); font-weight: 620; }
.secondary-action { color: var(--button-secondary-text); border: 1px solid var(--button-secondary-edge); background: var(--button-secondary-bg); backdrop-filter: blur(18px); }
.primary-action:hover, .secondary-action:hover { transform: translateY(-2px); }
@ -104,6 +105,27 @@ button:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 3px
.empty-state i { width: 7px; height: 7px; margin-bottom: 20px; border-radius: 50%; background: var(--accent-light); box-shadow: 0 0 32px 12px var(--primitive-warm-glow); }
.empty-state b { color: var(--content-secondary); font-size: 13px; font-weight: 570; }
.empty-state span { max-width: 260px; margin-top: 10px; color: var(--content-faint); font-size: 11px; line-height: 1.7; }
.empty-state.compact { min-height: 170px; }
.repository-picker { width: 100%; min-height: 74px; display: flex; align-items: center; justify-content: space-between; gap: 18px; padding: 13px 19px; border: 0; border-radius: 19px; color: var(--content-secondary); background: var(--primitive-glass); box-shadow: inset 0 1px var(--primitive-glass-top); text-align: left; cursor: pointer; }
.repository-picker:hover { background: var(--primitive-glass-hover); }
.repository-picker span { display: grid; gap: 6px; }
.repository-picker b { font-size: 12.5px; font-weight: 590; }
.repository-picker small { color: var(--content-faint); font-size: 10px; }
.repository-picker svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.45; stroke-linecap: round; stroke-linejoin: round; }
.candidate-card { margin-top: 26px; padding: 21px; border-radius: 21px; background: var(--primitive-glass); box-shadow: inset 0 1px var(--primitive-glass-top); }
.candidate-heading { display: flex; align-items: flex-start; gap: 14px; }
.candidate-heading div { display: grid; gap: 5px; }
.candidate-heading b { color: var(--content-secondary); font-size: 12.5px; }
.candidate-heading span { color: var(--content-faint); font-size: 10.5px; }
.candidate-facts { display: grid; gap: 10px; margin: 20px 0; }
.candidate-facts div { display: grid; grid-template-columns: 72px minmax(0, 1fr); gap: 12px; }
.candidate-facts dt { color: var(--content-faint); font-size: 10px; }
.candidate-facts dd { margin: 0; overflow: hidden; color: var(--content-secondary); font: 10.5px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; text-overflow: ellipsis; white-space: nowrap; }
.candidate-card > p { color: var(--content-faint); font-size: 10.5px; line-height: 1.7; }
.candidate-actions { display: flex; align-items: center; gap: 16px; margin-top: 18px; }
.candidate-actions .primary-action { min-height: 42px; padding-inline: 18px; font-size: 12px; }
.text-action { border: 0; color: var(--content-muted); background: transparent; font-size: 11px; cursor: pointer; }
.theme-list { display: grid; gap: 9px; margin-top: 30px; }
.theme-list button { width: 100%; min-height: 66px; display: grid; grid-template-columns: 40px 1fr auto; align-items: center; gap: 14px; padding: 10px 14px; border: 0; border-radius: 17px; color: var(--content-secondary); background: transparent; text-align: left; cursor: pointer; }