"""器谱应用服务:设备配对、逐项授契、撤回与可用性裁决。 铁律(PRD §8.3):器谱属于镜域、逐项授契;撤回立即生效; 失联设备只允许安全自治与输入暂存(不产生本地人物副本)。 """ from __future__ import annotations import json from sqlalchemy import select from app.core.errors import AppError from app.core.time import utc_now from app.db.session import AsyncSessionLocal from app.domains import one_qi from app.domains.tianzai.models import Device, DeviceGrant # 器谱能力键(可授契面;新增能力必须显式登记于此) GRANTABLE = { "notifications", "file_dialog", "voice_input", "camera", "screen", "location", "local_search", } async def pair_device(*, owner_scope: str, kind: str, name: str, key_fingerprint: str) -> dict: """配对设备(一次配对;指纹登记后续凭证验签依据)。""" async with AsyncSessionLocal() as session: device = Device( owner_scope=owner_scope, kind=kind or "desktop_shell", name=name, key_fingerprint=key_fingerprint, ) session.add(device) await session.commit() session.expunge(device) return _device_dict(device) async def list_devices(owner_scope: str) -> list[dict]: async with AsyncSessionLocal() as session: rows = ( await session.execute( select(Device).where(Device.owner_scope == owner_scope) .order_by(Device.created_at.desc()) ) ).scalars().all() return [_device_dict(r) for r in rows] async def revoke_device(device_id: str, owner_scope: str) -> dict: """撤销设备:立即生效(全部授契随之失效)。""" await _owned_device(device_id, owner_scope) async with AsyncSessionLocal() as session: device = await session.get(Device, device_id) device.status = "revoked" grants = ( await session.execute( select(DeviceGrant).where(DeviceGrant.device_id == device_id, DeviceGrant.status == "active") ) ).scalars().all() now = utc_now() for g in grants: g.status = "revoked" g.revoked_at = now await session.commit() await one_qi.emit( "device.revoked", character_id=None, payload={"device_id": device_id}, source="tianzai", ) return {"device_id": device_id, "status": "revoked", "grants_revoked": len(grants)} async def grant(device_id: str, owner_scope: str, *, capability: str, scope: dict) -> dict: """逐项授契(能力键必须显式登记;设备撤销后不可再授)。""" if capability not in GRANTABLE: raise AppError( "invalid_request", f"未知器谱能力:{capability}(可授面:{sorted(GRANTABLE)})", ) await _owned_device(device_id, owner_scope) async with AsyncSessionLocal() as session: device = await session.get(Device, device_id) if device.status != "active": raise AppError("invalid_request", "设备已撤销,不可授契。", status_code=409) row = DeviceGrant( device_id=device_id, owner_scope=owner_scope, capability=capability, scope_json=json.dumps(scope or {}, ensure_ascii=False), ) session.add(row) await session.commit() session.expunge(row) return _grant_dict(row) async def revoke_grant(grant_id: str, owner_scope: str) -> dict: """撤回单项授契:立即生效。""" async with AsyncSessionLocal() as session: row = await session.get(DeviceGrant, grant_id) if row is None or row.owner_scope != owner_scope: raise AppError("not_found", "授契不存在。", status_code=404) row.status = "revoked" row.revoked_at = utc_now() await session.commit() return {"grant_id": grant_id, "status": "revoked", "effective": "immediate"} async def assert_granted(device_id: str, owner_scope: str, capability: str) -> dict: """能力使用前置裁决:设备 active 且该能力授契 active(失联= revoked 同拒)。""" await _owned_device(device_id, owner_scope) async with AsyncSessionLocal() as session: device = await session.get(Device, device_id) if device.status != "active": raise AppError("invalid_request", "设备已撤销,能力不可用。", status_code=403) row = ( await session.execute( select(DeviceGrant).where( DeviceGrant.device_id == device_id, DeviceGrant.capability == capability, DeviceGrant.status == "active", ) ) ).scalars().first() if row is None: raise AppError( "invalid_request", f"能力 {capability} 未授契(器谱逐项授权)。", status_code=403 ) device.last_seen_at = utc_now() await session.commit() session.expunge(row) return _grant_dict(row) async def list_grants(device_id: str, owner_scope: str) -> list[dict]: await _owned_device(device_id, owner_scope) async with AsyncSessionLocal() as session: rows = ( await session.execute( select(DeviceGrant).where(DeviceGrant.device_id == device_id) .order_by(DeviceGrant.granted_at.desc()) ) ).scalars().all() return [_grant_dict(r) for r in rows] async def _owned_device(device_id: str, owner_scope: str) -> None: async with AsyncSessionLocal() as session: device = await session.get(Device, device_id) if device is None or device.owner_scope != owner_scope: raise AppError("not_found", "设备不存在。", status_code=404) def _device_dict(d: Device) -> dict: return { "id": d.id, "kind": d.kind, "name": d.name, "key_fingerprint": d.key_fingerprint, "status": d.status, "created_at": d.created_at.isoformat() if d.created_at else None, "last_seen_at": d.last_seen_at.isoformat() if d.last_seen_at else None, } def _grant_dict(g: DeviceGrant) -> dict: return { "id": g.id, "device_id": g.device_id, "capability": g.capability, "scope": json.loads(g.scope_json or "{}"), "status": g.status, "granted_at": g.granted_at.isoformat() if g.granted_at else None, "revoked_at": g.revoked_at.isoformat() if g.revoked_at else None, }