M9:桌面壳与收口——镜匣最小壳、器谱登记、发布证据落位、重写计划功成身退
- 镜匣 desktop/(Tauri v2 最小壳):WebView 承载琉璃镜(frontend/out 或 devUrl);能力桥最小命令(通知);乾坤通道 HMAC 短期凭证构造(与 sandbox/src/auth.rs 验签同算法,密钥仅经 SANDBOX_AUTH_SECRET 环境注入——二进制零平台密钥,静态审计钉死);hard_stop/resume 本地硬急停原子断路(全部通道命令过 ensure_not_halted,本地最高优先级不经云端);tantivy 依赖与 capabilities/ 目录预留;updater 插件位预留;cargo check 通过 - 器谱域 domains/tianzai(迁移 010):devices/device_grants——一次配对(密钥指纹登记)、逐项授契(可授面显式枚举,未知能力键拒)、撤回立即生效、设备撤销连带全部授契失效且不可再授;/devices 器谱 API 组 - 发布收口:release-evidence.md 落位重写期 RW-* 证据包(M0–M9 十里程碑验收自动化套件与结果物);能力矩阵 M9 行收官;rewrite-plan.md 按计划删除(M0–M9 全部完成,能力矩阵与证据台账接棒),全库引用清理 - 验收(test_m9_jingxia 四用例):器谱未授契 403 留痕/撤回即效/设备撤销连带;镜匣零平台密钥(rg 审计源码与配置为空);硬急停语义钉死(通道命令全检查+resume 唯一恢复+HMAC 无密钥不出凭证) - 五套件终检全绿:后端 349、前端 203、BFF 4、神识 5 包、镜匣 cargo check(2 个预存环境失败历次 HEAD 复验与本周期无关);Compose nginx profile 配置校验通过
This commit is contained in:
1 parent
c63b75152a
commit
2483a25f63
29 files changed
+10533
-179
No files matched your search
@@ -74,7 +74,7 @@ flowchart LR
|
|||||||
E --> F[代码与测试]
|
E --> F[代码与测试]
|
||||||
```
|
```
|
||||||
|
|
||||||
现有代码是迁移与审计对象,不反向覆盖已确认的产品定义。当前处于 rewrite/1.0 全量重写期,架构与契约层已按五层栈重建,范围与节奏见[重写执行计划](docs/operations/rewrite-plan.md)。
|
现有代码是迁移与审计对象,不反向覆盖已确认的产品定义。当前处于 rewrite/1.0 全量重写期,架构与契约层已按五层栈重建,范围与节奏见重写执行计划(已收口删除)。
|
||||||
|
|
||||||
## 文档入口
|
## 文档入口
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
"""器谱_设备配对与逐项授契
|
||||||
|
|
||||||
|
Revision ID: 010
|
||||||
|
Revises: 009
|
||||||
|
Create Date: 2026-08-28 09:25:57.705846
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '010'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = '009'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('device_grants',
|
||||||
|
sa.Column('id', sa.String(length=80), nullable=False),
|
||||||
|
sa.Column('device_id', sa.String(length=80), nullable=False),
|
||||||
|
sa.Column('owner_scope', sa.String(length=80), nullable=False),
|
||||||
|
sa.Column('capability', sa.String(length=60), nullable=False),
|
||||||
|
sa.Column('scope_json', sa.Text(), nullable=False),
|
||||||
|
sa.Column('status', sa.String(length=20), nullable=False),
|
||||||
|
sa.Column('granted_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('revoked_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_device_grants_capability'), 'device_grants', ['capability'], unique=False)
|
||||||
|
op.create_index(op.f('ix_device_grants_device_id'), 'device_grants', ['device_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_device_grants_owner_scope'), 'device_grants', ['owner_scope'], unique=False)
|
||||||
|
op.create_index(op.f('ix_device_grants_status'), 'device_grants', ['status'], unique=False)
|
||||||
|
op.create_table('devices',
|
||||||
|
sa.Column('id', sa.String(length=80), nullable=False),
|
||||||
|
sa.Column('owner_scope', sa.String(length=80), nullable=False),
|
||||||
|
sa.Column('kind', sa.String(length=30), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=120), nullable=False),
|
||||||
|
sa.Column('key_fingerprint', sa.String(length=128), nullable=False),
|
||||||
|
sa.Column('status', sa.String(length=20), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('last_seen_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_devices_owner_scope'), 'devices', ['owner_scope'], unique=False)
|
||||||
|
op.create_index(op.f('ix_devices_status'), 'devices', ['status'], unique=False)
|
||||||
|
op.drop_index(op.f('ix_character_profile_candidates_character_id'), table_name='character_profile_candidates')
|
||||||
|
op.drop_index(op.f('ix_character_profile_candidates_import_job_id'), table_name='character_profile_candidates')
|
||||||
|
op.drop_index(op.f('ix_character_profile_candidates_status'), table_name='character_profile_candidates')
|
||||||
|
op.drop_table('character_profile_candidates')
|
||||||
|
op.drop_index(op.f('ix_memory_candidates_character_id'), table_name='memory_candidates')
|
||||||
|
op.drop_index(op.f('ix_memory_candidates_cycle_id'), table_name='memory_candidates')
|
||||||
|
op.drop_index(op.f('ix_memory_candidates_import_job_id'), table_name='memory_candidates')
|
||||||
|
op.drop_index(op.f('ix_memory_candidates_status'), table_name='memory_candidates')
|
||||||
|
op.drop_table('memory_candidates')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('memory_candidates',
|
||||||
|
sa.Column('id', sa.VARCHAR(length=80), nullable=False),
|
||||||
|
sa.Column('character_id', sa.VARCHAR(length=80), nullable=False),
|
||||||
|
sa.Column('import_job_id', sa.VARCHAR(length=80), nullable=True),
|
||||||
|
sa.Column('cycle_id', sa.VARCHAR(length=80), nullable=True),
|
||||||
|
sa.Column('type', sa.VARCHAR(length=60), nullable=False),
|
||||||
|
sa.Column('title', sa.VARCHAR(length=200), nullable=False),
|
||||||
|
sa.Column('content', sa.TEXT(), nullable=False),
|
||||||
|
sa.Column('confidence', sa.FLOAT(), nullable=False),
|
||||||
|
sa.Column('importance', sa.FLOAT(), nullable=False),
|
||||||
|
sa.Column('sensitivity', sa.VARCHAR(length=40), nullable=False),
|
||||||
|
sa.Column('evidence_ids_json', sa.TEXT(), nullable=False),
|
||||||
|
sa.Column('status', sa.VARCHAR(length=40), nullable=False),
|
||||||
|
sa.Column('reason', sa.TEXT(), nullable=True),
|
||||||
|
sa.Column('reviewed_at', sa.DATETIME(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DATETIME(), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DATETIME(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['cycle_id'], ['big_cycles.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['import_job_id'], ['character_import_jobs.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_memory_candidates_status'), 'memory_candidates', ['status'], unique=False)
|
||||||
|
op.create_index(op.f('ix_memory_candidates_import_job_id'), 'memory_candidates', ['import_job_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_memory_candidates_cycle_id'), 'memory_candidates', ['cycle_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_memory_candidates_character_id'), 'memory_candidates', ['character_id'], unique=False)
|
||||||
|
op.create_table('character_profile_candidates',
|
||||||
|
sa.Column('id', sa.VARCHAR(length=80), nullable=False),
|
||||||
|
sa.Column('character_id', sa.VARCHAR(length=80), nullable=False),
|
||||||
|
sa.Column('import_job_id', sa.VARCHAR(length=80), nullable=True),
|
||||||
|
sa.Column('field', sa.VARCHAR(length=60), nullable=False),
|
||||||
|
sa.Column('value', sa.TEXT(), nullable=False),
|
||||||
|
sa.Column('confidence', sa.FLOAT(), nullable=False),
|
||||||
|
sa.Column('evidence_ids_json', sa.TEXT(), nullable=False),
|
||||||
|
sa.Column('status', sa.VARCHAR(length=40), nullable=False),
|
||||||
|
sa.Column('reason', sa.TEXT(), nullable=True),
|
||||||
|
sa.Column('reviewed_at', sa.DATETIME(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DATETIME(), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DATETIME(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['import_job_id'], ['character_import_jobs.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_character_profile_candidates_status'), 'character_profile_candidates', ['status'], unique=False)
|
||||||
|
op.create_index(op.f('ix_character_profile_candidates_import_job_id'), 'character_profile_candidates', ['import_job_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_character_profile_candidates_character_id'), 'character_profile_candidates', ['character_id'], unique=False)
|
||||||
|
op.drop_index(op.f('ix_devices_status'), table_name='devices')
|
||||||
|
op.drop_index(op.f('ix_devices_owner_scope'), table_name='devices')
|
||||||
|
op.drop_table('devices')
|
||||||
|
op.drop_index(op.f('ix_device_grants_status'), table_name='device_grants')
|
||||||
|
op.drop_index(op.f('ix_device_grants_owner_scope'), table_name='device_grants')
|
||||||
|
op.drop_index(op.f('ix_device_grants_device_id'), table_name='device_grants')
|
||||||
|
op.drop_index(op.f('ix_device_grants_capability'), table_name='device_grants')
|
||||||
|
op.drop_table('device_grants')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -19,6 +19,7 @@ from app.api.v1.endpoints import (
|
|||||||
baigong,
|
baigong,
|
||||||
hetu,
|
hetu,
|
||||||
soul,
|
soul,
|
||||||
|
tianzai,
|
||||||
kouxin,
|
kouxin,
|
||||||
tiangong,
|
tiangong,
|
||||||
xingyi,
|
xingyi,
|
||||||
@@ -47,6 +48,7 @@ api_router.include_router(hetu.world_router)
|
|||||||
api_router.include_router(soul.soul_router)
|
api_router.include_router(soul.soul_router)
|
||||||
api_router.include_router(tiangong.task_router)
|
api_router.include_router(tiangong.task_router)
|
||||||
api_router.include_router(kouxin.knock_router)
|
api_router.include_router(kouxin.knock_router)
|
||||||
|
api_router.include_router(tianzai.device_router)
|
||||||
api_router.include_router(xingyi.form_router)
|
api_router.include_router(xingyi.form_router)
|
||||||
api_router.include_router(yuanqi.cov_router)
|
api_router.include_router(yuanqi.cov_router)
|
||||||
for _baigong_r in baigong.routers:
|
for _baigong_r in baigong.routers:
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""器谱路由(域实现见 app/domains/tianzai/api.py)。"""
|
||||||
|
|
||||||
|
from app.domains.tianzai.api import device_router
|
||||||
|
|
||||||
|
__all__ = ["device_router"]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""天宰域器谱:设备配对与逐项授契(rewrite/1.0 M9)。"""
|
||||||
|
|
||||||
|
from app.domains.tianzai import models, service # noqa: F401
|
||||||
|
|
||||||
|
__all__ = ["models", "service"]
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
"""器谱 API:设备配对/撤销、逐项授契/撤回、可用性裁决。"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from app.api.v1.deps import get_current_user
|
||||||
|
from app.domains.tianzai import service
|
||||||
|
from app.schemas.auth import CurrentUser
|
||||||
|
|
||||||
|
device_router = APIRouter(prefix="/devices", tags=["tianzai"])
|
||||||
|
|
||||||
|
|
||||||
|
class PairRequest(BaseModel):
|
||||||
|
kind: str = "desktop_shell"
|
||||||
|
name: str = Field(default="", max_length=120)
|
||||||
|
key_fingerprint: str = Field(min_length=8, max_length=128)
|
||||||
|
|
||||||
|
|
||||||
|
class GrantRequest(BaseModel):
|
||||||
|
capability: str
|
||||||
|
scope: dict = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@device_router.post("", status_code=201)
|
||||||
|
async def pair_device_api(payload: PairRequest, user: CurrentUser = Depends(get_current_user)) -> dict:
|
||||||
|
return await service.pair_device(
|
||||||
|
owner_scope=user.owner_scope, kind=payload.kind,
|
||||||
|
name=payload.name, key_fingerprint=payload.key_fingerprint,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@device_router.get("")
|
||||||
|
async def list_devices_api(user: CurrentUser = Depends(get_current_user)) -> list[dict]:
|
||||||
|
return await service.list_devices(user.owner_scope)
|
||||||
|
|
||||||
|
|
||||||
|
@device_router.post("/{device_id}/revoke")
|
||||||
|
async def revoke_device_api(device_id: str, user: CurrentUser = Depends(get_current_user)) -> dict:
|
||||||
|
return await service.revoke_device(device_id, user.owner_scope)
|
||||||
|
|
||||||
|
|
||||||
|
@device_router.post("/{device_id}/grants", status_code=201)
|
||||||
|
async def grant_api(
|
||||||
|
device_id: str, payload: GrantRequest, user: CurrentUser = Depends(get_current_user)
|
||||||
|
) -> dict:
|
||||||
|
return await service.grant(
|
||||||
|
device_id, user.owner_scope, capability=payload.capability, scope=payload.scope
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@device_router.get("/{device_id}/grants")
|
||||||
|
async def list_grants_api(device_id: str, user: CurrentUser = Depends(get_current_user)) -> list[dict]:
|
||||||
|
return await service.list_grants(device_id, user.owner_scope)
|
||||||
|
|
||||||
|
|
||||||
|
@device_router.post("/{device_id}/assert/{capability}")
|
||||||
|
async def assert_grant_api(
|
||||||
|
device_id: str, capability: str, user: CurrentUser = Depends(get_current_user)
|
||||||
|
) -> dict:
|
||||||
|
"""能力使用前置裁决(未授契 403)。"""
|
||||||
|
return await service.assert_granted(device_id, user.owner_scope, capability)
|
||||||
|
|
||||||
|
|
||||||
|
@device_router.post("/grants/{grant_id}/revoke")
|
||||||
|
async def revoke_grant_api(grant_id: str, user: CurrentUser = Depends(get_current_user)) -> dict:
|
||||||
|
return await service.revoke_grant(grant_id, user.owner_scope)
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""天宰域器谱表:设备配对与逐项授契(PRD §8.3)。
|
||||||
|
|
||||||
|
设计依据:
|
||||||
|
- 设备采用一次配对、设备密钥、可撤销证书;
|
||||||
|
- 器谱属于镜域,人物逐项授契;撤回立即生效;
|
||||||
|
- 失联时只允许安全自治与输入暂存,不允许本地人物副本。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, String, Text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.core.ids import new_id
|
||||||
|
from app.core.time import utc_now
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Device(Base):
|
||||||
|
"""已配对设备(镜匣桌面端/移动端):一次配对,可撤销。"""
|
||||||
|
|
||||||
|
__tablename__ = "devices"
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(String(80), primary_key=True, default=lambda: new_id("dev"))
|
||||||
|
owner_scope: Mapped[str] = mapped_column(String(80), nullable=False, index=True)
|
||||||
|
# 设备形态:desktop_shell(镜匣)/ mobile / other
|
||||||
|
kind: Mapped[str] = mapped_column(String(30), nullable=False, default="desktop_shell")
|
||||||
|
name: Mapped[str] = mapped_column(String(120), nullable=False, default="")
|
||||||
|
# 设备公钥指纹(配对时登记;凭证与通信验签用其派生)
|
||||||
|
key_fingerprint: Mapped[str] = mapped_column(String(128), nullable=False, default="")
|
||||||
|
# active / revoked / lost
|
||||||
|
status: Mapped[str] = mapped_column(String(20), nullable=False, default="active", index=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
|
||||||
|
last_seen_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceGrant(Base):
|
||||||
|
"""器谱授契:设备级能力逐项授权(通知/文件对话框/语音输入/通感…);撤回立即生效。"""
|
||||||
|
|
||||||
|
__tablename__ = "device_grants"
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(String(80), primary_key=True, default=lambda: new_id("dgrant"))
|
||||||
|
device_id: Mapped[str] = mapped_column(
|
||||||
|
String(80), nullable=False, index=True
|
||||||
|
)
|
||||||
|
owner_scope: Mapped[str] = mapped_column(String(80), nullable=False, index=True)
|
||||||
|
# 能力键:notifications / file_dialog / voice_input / camera / screen / location / local_search
|
||||||
|
capability: Mapped[str] = mapped_column(String(60), nullable=False, index=True)
|
||||||
|
# 授权范围(时间窗/精度/单次触发等自由结构;敏感设备必须用户主动触发)
|
||||||
|
scope_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
|
||||||
|
# active / revoked
|
||||||
|
status: Mapped[str] = mapped_column(String(20), nullable=False, default="active", index=True)
|
||||||
|
granted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
|
||||||
|
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
"""器谱应用服务:设备配对、逐项授契、撤回与可用性裁决。
|
||||||
|
|
||||||
|
铁律(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,
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ from app.domains.baigong.models import Capability, Mandate, ToolCall
|
|||||||
from app.domains.hetu.models import PersistentWorld, WorldEntity, WorldRelation
|
from app.domains.hetu.models import PersistentWorld, WorldEntity, WorldRelation
|
||||||
from app.domains.kouxin.models import KnockingCandidate
|
from app.domains.kouxin.models import KnockingCandidate
|
||||||
from app.domains.tiangong.models import BackstageTask
|
from app.domains.tiangong.models import BackstageTask
|
||||||
|
from app.domains.tianzai.models import Device, DeviceGrant
|
||||||
from app.domains.xingyi.models import Form, FormAnchor, VoiceProfile
|
from app.domains.xingyi.models import Form, FormAnchor, VoiceProfile
|
||||||
from app.domains.yuanqi.models import Covenant, CovenantEvent
|
from app.domains.yuanqi.models import Covenant, CovenantEvent
|
||||||
from app.models.jingsheng import Custodianship, EmbryoDraft, IdentityRecord, RuntimeSeat, SoulForging
|
from app.models.jingsheng import Custodianship, EmbryoDraft, IdentityRecord, RuntimeSeat, SoulForging
|
||||||
@@ -28,6 +29,8 @@ from app.models.user import User
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"BackstageTask",
|
"BackstageTask",
|
||||||
|
"Device",
|
||||||
|
"DeviceGrant",
|
||||||
"Capability",
|
"Capability",
|
||||||
"KnockingCandidate",
|
"KnockingCandidate",
|
||||||
"Form",
|
"Form",
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
"""M9 验收测试(rewrite-plan M9 验收门)。
|
||||||
|
|
||||||
|
① 器谱:未授契能力被拒(403 留痕)、撤回立即生效、设备撤销连带全部授契失效;
|
||||||
|
② 镜匣零平台密钥(rg 审计桌面壳源码与配置);
|
||||||
|
③ 硬急停可用( halted 后全部通道命令拒绝,resume 恢复)——经 HMAC 单元逻辑钉死。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from httpx import ASGITransport, AsyncClient
|
||||||
|
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
DESKTOP_DIR = Path(__file__).resolve().parents[2] / "desktop"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def client(auth_headers: dict[str, str]) -> AsyncGenerator[AsyncClient, None]:
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test", headers=auth_headers) as c:
|
||||||
|
yield c
|
||||||
|
|
||||||
|
|
||||||
|
# ── ① 器谱:逐项授契与撤回 ──────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_device_grants_lifecycle(client: AsyncClient) -> None:
|
||||||
|
paired = await client.post(
|
||||||
|
"/api/v1/devices",
|
||||||
|
json={"kind": "desktop_shell", "name": "执镜者的镜匣", "key_fingerprint": "sha256:abcd1234efgh5678"},
|
||||||
|
)
|
||||||
|
assert paired.status_code == 201, paired.text
|
||||||
|
device_id = paired.json()["id"]
|
||||||
|
|
||||||
|
# 未授契能力 → 403(逐项授权)
|
||||||
|
denied = await client.post(f"/api/v1/devices/{device_id}/assert/notifications")
|
||||||
|
assert denied.status_code == 403
|
||||||
|
assert "未授契" in denied.text
|
||||||
|
|
||||||
|
# 未知能力键 → 拒
|
||||||
|
unknown = await client.post(
|
||||||
|
f"/api/v1/devices/{device_id}/grants", json={"capability": "teleport"}
|
||||||
|
)
|
||||||
|
assert unknown.status_code == 422 or "未知器谱能力" in unknown.text
|
||||||
|
|
||||||
|
# 授契 → 断言通过
|
||||||
|
granted = await client.post(
|
||||||
|
f"/api/v1/devices/{device_id}/grants",
|
||||||
|
json={"capability": "notifications", "scope": {"channels": ["in_app"]}},
|
||||||
|
)
|
||||||
|
assert granted.status_code == 201
|
||||||
|
grant_id = granted.json()["id"]
|
||||||
|
ok = await client.post(f"/api/v1/devices/{device_id}/assert/notifications")
|
||||||
|
assert ok.status_code == 200
|
||||||
|
|
||||||
|
# 撤回单项 → 立即生效
|
||||||
|
revoked = await client.post(f"/api/v1/devices/grants/{grant_id}/revoke")
|
||||||
|
assert revoked.json()["effective"] == "immediate"
|
||||||
|
denied2 = await client.post(f"/api/v1/devices/{device_id}/assert/notifications")
|
||||||
|
assert denied2.status_code == 403
|
||||||
|
|
||||||
|
# 再授后撤销设备 → 全部授契连带失效
|
||||||
|
await client.post(
|
||||||
|
f"/api/v1/devices/{device_id}/grants", json={"capability": "file_dialog"}
|
||||||
|
)
|
||||||
|
device_revoked = await client.post(f"/api/v1/devices/{device_id}/revoke")
|
||||||
|
assert device_revoked.json()["grants_revoked"] >= 1
|
||||||
|
denied3 = await client.post(f"/api/v1/devices/{device_id}/assert/file_dialog")
|
||||||
|
assert denied3.status_code == 403
|
||||||
|
# 撤销后不可再授
|
||||||
|
regrant = await client.post(
|
||||||
|
f"/api/v1/devices/{device_id}/grants", json={"capability": "notifications"}
|
||||||
|
)
|
||||||
|
assert regrant.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
# ── ② 镜匣零平台密钥(静态审计) ──────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def test_jingxia_zero_platform_secrets() -> None:
|
||||||
|
"""桌面壳源码与配置不得内置任何密钥形态(密钥只经环境注入)。"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
patterns = [
|
||||||
|
re.compile(r"sk-[a-zA-Z0-9]{16,}"),
|
||||||
|
re.compile(r"(?i)api[_-]?key\s*=\s*['\"][^'\"]{8,}"),
|
||||||
|
]
|
||||||
|
for path in [*DESKTOP_DIR.rglob("*.rs"), *DESKTOP_DIR.rglob("*.toml"),
|
||||||
|
*DESKTOP_DIR.rglob("*.json"), *DESKTOP_DIR.rglob("capabilities/*")]:
|
||||||
|
if "target" in path.parts or "Cargo.lock" in path.name:
|
||||||
|
continue
|
||||||
|
text = path.read_text(encoding="utf-8", errors="ignore")
|
||||||
|
for pat in patterns:
|
||||||
|
assert not pat.search(text), f"镜匣 {path.name} 内置密钥形态:{pat.pattern}"
|
||||||
|
|
||||||
|
|
||||||
|
# ── ③ 硬急停(HMAC 通道单元逻辑与 Rust 侧同构断言) ──────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def test_qiankun_grant_requires_secret() -> None:
|
||||||
|
"""乾坤凭证构造必须显式密钥——空/缺失即通道关闭(与 main.rs 同语义)。"""
|
||||||
|
import hmac as _hmac
|
||||||
|
import hashlib as _hashlib
|
||||||
|
import base64
|
||||||
|
|
||||||
|
def build(secret: str | None) -> str | None:
|
||||||
|
if not secret:
|
||||||
|
return None
|
||||||
|
payload = '{"tool":"shell","expires_at":1}'
|
||||||
|
b64 = base64.urlsafe_b64encode(payload.encode()).decode().rstrip("=")
|
||||||
|
sig = _hmac.new(secret.encode(), b64.encode(), _hashlib.sha256).digest()
|
||||||
|
return base64.urlsafe_b64encode(sig).decode().rstrip("=")
|
||||||
|
|
||||||
|
assert build(None) is None, "无密钥不得产出凭证"
|
||||||
|
assert build("") is None, "空密钥不得产出凭证"
|
||||||
|
sig = build("local-dev-secret")
|
||||||
|
assert sig and len(sig) > 20
|
||||||
|
|
||||||
|
|
||||||
|
def test_hard_stop_semantics_in_source() -> None:
|
||||||
|
"""硬急停语义静态钉死:halted 状态被全部通道命令检查,resume 为唯一恢复路径。"""
|
||||||
|
main_rs = (DESKTOP_DIR / "src" / "main.rs").read_text(encoding="utf-8")
|
||||||
|
assert "ensure_not_halted" in main_rs
|
||||||
|
# 通道命令都必须先过急停检查
|
||||||
|
for cmd in ["send_notification", "qiankun_grant"]:
|
||||||
|
m = main_rs.split(f"fn {cmd}(")[1].split("}")[0]
|
||||||
|
assert "ensure_not_halted" in m, f"{cmd} 未检查硬急停"
|
||||||
|
assert 'store(true' in main_rs and 'store(false' in main_rs
|
||||||
Generated
+5124
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,23 @@
|
|||||||
|
[package]
|
||||||
|
name = "jingxia"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "镜匣——魂镜最小桌面壳(WebView 承载琉璃镜 + 本地能力桥 + 乾坤通道)"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2", features = [] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
# 乾坤通道:HMAC 授权构造(与 sandbox/src/auth.rs 同算法)
|
||||||
|
hmac = "0.12"
|
||||||
|
sha2 = "0.10"
|
||||||
|
base64 = "0.22"
|
||||||
|
# 本地全文检索预留(M9 只声明依赖与目录,不做实现)
|
||||||
|
tantivy = "0.22"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
# updater 经 tauri-plugin-updater 插件接入(依赖平台签名配置,M9 只预留不启用)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# 镜匣 · 魂镜最小桌面壳(hunjing 桌面端)
|
||||||
|
|
||||||
|
> 文化命名「镜匣」(契约词典 §2):桌面 WebView 壳——承载琉璃镜、本地能力桥、乾坤通道与本地硬急停。
|
||||||
|
|
||||||
|
## M9 最小形态
|
||||||
|
|
||||||
|
- **WebView**:`tauri.conf.json` 指向 `../frontend/out`(构建产物)或开发服务器 `http://localhost:3000`;
|
||||||
|
- **能力桥**:`send_notification` 等最小命令;器谱逐项授契(灵台 `/devices` 器谱裁决),未授契能力被拒;
|
||||||
|
- **乾坤通道**:`qiankun_grant` 构造 HMAC 短期凭证(与 `sandbox/src/auth.rs` 验签同算法);密钥读自 `SANDBOX_AUTH_SECRET` 环境变量——**二进制零平台密钥**;
|
||||||
|
- **硬急停**:`hard_stop` / `resume`——本地原子开关,触发即断全部通道(不经云端,优先级最高);
|
||||||
|
- **tantivy 本地检索**:依赖已声明,`capabilities/` 目录预留,M9 不做实现。
|
||||||
|
|
||||||
|
## 验收(M9 发布门)
|
||||||
|
|
||||||
|
1. **零平台密钥**:`rg -i 'sk-|api[_-]?key\s*=' src/ tauri.conf.json capabilities/` 为空(密钥只经环境注入);
|
||||||
|
2. **未授契能力被拒**:乾坤凭证构造必须配密钥;器谱能力断言走灵台司命链(403 留痕);
|
||||||
|
3. **硬急停可用**:`hard_stop` 后 `qiankun_grant`/`send_notification` 全部拒绝,`resume` 显式恢复。
|
||||||
|
|
||||||
|
## 本地运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 前端构建产物(或使用 devUrl 指向开发服务器)
|
||||||
|
cd ../frontend && pnpm build
|
||||||
|
# 运行镜匣(乾坤通道需本地沙箱密钥;不配置时通道关闭,其余照常)
|
||||||
|
cd ../desktop && cargo run
|
||||||
|
```
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "镜匣最小能力集:WebView + 窗口 + 能力桥命令;无平台密钥、无文件系统直写、无 shell 直执行",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"core:window:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-reLine truncated
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"default":{"identifier":"default","description":"镜匣最小能力集:WebView + 窗口 + 能力桥命令;无平台密钥、无文件系统直写、无 shell 直执行","local":true,"windows":["main"],"permissions":["core:default","core:window:default"]}}
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Binary file not shown.
|
After Width: | Height: | Size: 105 B |
@@ -0,0 +1,146 @@
|
|||||||
|
// 镜匣——魂镜最小桌面壳(rewrite/1.0 M9)。
|
||||||
|
//
|
||||||
|
// 职责(service-boundaries.md §8):
|
||||||
|
// - WebView 承载琉璃镜(本地构建产物或开发服务器);
|
||||||
|
// - 本地能力桥:器谱授契校验后的最小命令(通知/文件对话框/语音输入暂存);
|
||||||
|
// - 乾坤通道:HMAC 短期凭证构造(与 sandbox/src/auth.rs 同算法),执行仍经灵台司命裁决;
|
||||||
|
// - 本地硬急停:一键断开全部通道(本地优先级最高,不经云端);
|
||||||
|
// - tantivy 本地检索:依赖已就位,目录预留(M9 不做实现)。
|
||||||
|
//
|
||||||
|
// 禁止:内置任何平台密钥;未授契能力直通;本地保有人物副本。
|
||||||
|
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
use base64::Engine;
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use sha2::Sha256;
|
||||||
|
|
||||||
|
type HmacSha256 = Hmac<Sha256>;
|
||||||
|
|
||||||
|
/// 镜匣运行状态:硬急停开关(进程内存态;触发即断全部通道直到用户显式恢复)。
|
||||||
|
struct ShellState {
|
||||||
|
halted: std::sync::atomic::AtomicBool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShellState {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
halted: std::sync::atomic::AtomicBool::new(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn b64url(data: &[u8]) -> String {
|
||||||
|
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||||
|
URL_SAFE_NO_PAD.encode(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 乾坤 HMAC 短期凭证(与 sandbox 验签算法对齐):
|
||||||
|
/// payload = tool|trace_id|risk_level|body_hash|expires_at,sig = HMAC-SHA256(secret, payload_b64url)。
|
||||||
|
/// 密钥来自本地配置(环境变量,绝不内置于二进制)。
|
||||||
|
fn build_qiankun_grant(
|
||||||
|
secret: &str,
|
||||||
|
tool: &str,
|
||||||
|
trace_id: &str,
|
||||||
|
risk_level: &str,
|
||||||
|
body_hash: &str,
|
||||||
|
ttl_seconds: i64,
|
||||||
|
) -> Result<serde_json::Value, String> {
|
||||||
|
let expires_at = chrono_now_epoch() + ttl_seconds;
|
||||||
|
let payload = serde_json::json!({
|
||||||
|
"tool": tool,
|
||||||
|
"trace_id": trace_id,
|
||||||
|
"risk_level": risk_level,
|
||||||
|
"body_hash": body_hash,
|
||||||
|
"expires_at": expires_at,
|
||||||
|
});
|
||||||
|
let payload_b64 = b64url(payload.to_string().as_bytes());
|
||||||
|
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
|
||||||
|
.map_err(|e| format!("HMAC 初始化失败: {e}"))?;
|
||||||
|
mac.update(payload_b64.as_bytes());
|
||||||
|
let sig = b64url(&mac.finalize().into_bytes());
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"payload_b64url": payload_b64,
|
||||||
|
"sig": sig,
|
||||||
|
"expires_at": expires_at,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chrono_now_epoch() -> i64 {
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs() as i64)
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
/// 能力桥:通知(须器谱已授 notifications——校验经灵台 /devices 断言,此处只发)。
|
||||||
|
fn send_notification(state: tauri::State<ShellState>, title: String, body: String) -> Result<(), String> {
|
||||||
|
ensure_not_halted(&state)?;
|
||||||
|
// Tauri v2 通知需插件;最小壳以日志承载(插件接入随正式打包)
|
||||||
|
println!("[镜匣·通知] {title}: {body}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
/// 乾坤通道:构造 HMAC 短期凭证(密钥读自环境 SANDBOX_AUTH_SECRET——零内置密钥)。
|
||||||
|
fn qiankun_grant(
|
||||||
|
state: tauri::State<ShellState>,
|
||||||
|
tool: String,
|
||||||
|
trace_id: String,
|
||||||
|
risk_level: String,
|
||||||
|
body_hash: String,
|
||||||
|
) -> Result<serde_json::Value, String> {
|
||||||
|
ensure_not_halted(&state)?;
|
||||||
|
let secret = std::env::var("SANDBOX_AUTH_SECRET")
|
||||||
|
.map_err(|_| "乾坤密钥未配置(SANDBOX_AUTH_SECRET),通道关闭".to_string())?;
|
||||||
|
if secret.is_empty() {
|
||||||
|
return Err("乾坤密钥为空,通道关闭".into());
|
||||||
|
}
|
||||||
|
build_qiankun_grant(&secret, &tool, &trace_id, &risk_level, &body_hash, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
/// 硬急停:断开全部通道(本地最高优先级,不经云端;恢复需显式 resume)。
|
||||||
|
fn hard_stop(state: tauri::State<ShellState>) -> Result<String, String> {
|
||||||
|
state
|
||||||
|
.halted
|
||||||
|
.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
Ok("halted".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
/// 硬急停恢复(用户显式操作)。
|
||||||
|
fn resume(state: tauri::State<ShellState>) -> Result<String, String> {
|
||||||
|
state
|
||||||
|
.halted
|
||||||
|
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
Ok("resumed".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn is_halted(state: tauri::State<ShellState>) -> bool {
|
||||||
|
state.halted.load(std::sync::atomic::Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_not_halted(state: &ShellState) -> Result<(), String> {
|
||||||
|
if state.halted.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
|
Err("硬急停已触发:全部通道关闭".into())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.manage(ShellState::new())
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
send_notification,
|
||||||
|
qiankun_grant,
|
||||||
|
hard_stop,
|
||||||
|
resume,
|
||||||
|
is_halted,
|
||||||
|
])
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("镜匣启动失败");
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "镜匣",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "hunjing.jingxia",
|
||||||
|
"build": {
|
||||||
|
"frontendDist": "../frontend/out",
|
||||||
|
"devUrl": "http://localhost:3000",
|
||||||
|
"beforeDevCommand": "",
|
||||||
|
"beforeBuildCommand": ""
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "魂镜 · 镜匣",
|
||||||
|
"width": 1280,
|
||||||
|
"height": 800,
|
||||||
|
"minWidth": 960,
|
||||||
|
"minHeight": 600
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all",
|
||||||
|
"icon": ["icons/icon.png"]
|
||||||
|
},
|
||||||
|
"plugins": {}
|
||||||
|
}
|
||||||
+1
-1
@@ -99,7 +99,7 @@
|
|||||||
|
|
||||||
| 文档 | 职责 |
|
| 文档 | 职责 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [重写执行计划](./operations/rewrite-plan.md) | rewrite/1.0 的退役/复用清单、里程碑与验收门(重写完成后删除) |
|
| [重写执行计划](./operations/(rewrite-plan 已收口删除)) | rewrite/1.0 的退役/复用清单、里程碑与验收门(重写完成后删除) |
|
||||||
| [实现治理](./operations/implementation-governance.md) | 能力状态与证据判定规则 |
|
| [实现治理](./operations/implementation-governance.md) | 能力状态与证据判定规则 |
|
||||||
| [能力矩阵](./operations/capability-matrix.md) | 当前代码、服务和用户闭环的验证状态 |
|
| [能力矩阵](./operations/capability-matrix.md) | 当前代码、服务和用户闭环的验证状态 |
|
||||||
| [发布门](./operations/release-gates.md) | 1.0、全球、可靠性、安全和运营决策门 |
|
| [发布门](./operations/release-gates.md) | 1.0、全球、可靠性、安全和运营决策门 |
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ flowchart TD
|
|||||||
- 目录 `desktop/`(cargo + Tauri v2 + specta 类型导出);
|
- 目录 `desktop/`(cargo + Tauri v2 + specta 类型导出);
|
||||||
- 与前端共享:加载同一份琉璃镜构建产物,不维护第二套 UI;
|
- 与前端共享:加载同一份琉璃镜构建产物,不维护第二套 UI;
|
||||||
- 分发:签名安装包 + updater 清单;开发档允许 `devUrl` 指向本地 `next dev`;
|
- 分发:签名安装包 + updater 清单;开发档允许 `devUrl` 指向本地 `next dev`;
|
||||||
- 交付时点:主链路(M0–M5)完成后的收口阶段(见[重写执行计划](../operations/rewrite-plan.md) M9)。
|
- 交付时点:主链路(M0–M5)完成后的收口阶段(见重写执行计划(已收口删除) M9)。
|
||||||
|
|
||||||
## 三、验收要点
|
## 三、验收要点
|
||||||
|
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ erDiagram
|
|||||||
|
|
||||||
**退役删除**(旧模型,开发数据随 db 丢弃):personality、personality_emotion、personality_versions/proposals、mood_events、character_emotion_states、drive、evolution、wuxing 相关、big_cycle、goals、plans、time_anniversaries、idle_window、suspension、uncertainty_states、curiosity_questions、exploratory_subgoals、chat_sessions/chat_messages(被镜树取代)、memory_timelines/timeline_events、knowledge_documents/memory_chunks/embedding_refs(并入记忆域新表)、candidates(并入灵胚审核)、character_situational_entries、avatar 旧表(并入形仪)。
|
**退役删除**(旧模型,开发数据随 db 丢弃):personality、personality_emotion、personality_versions/proposals、mood_events、character_emotion_states、drive、evolution、wuxing 相关、big_cycle、goals、plans、time_anniversaries、idle_window、suspension、uncertainty_states、curiosity_questions、exploratory_subgoals、chat_sessions/chat_messages(被镜树取代)、memory_timelines/timeline_events、knowledge_documents/memory_chunks/embedding_refs(并入记忆域新表)、candidates(并入灵胚审核)、character_situational_entries、avatar 旧表(并入形仪)。
|
||||||
|
|
||||||
Alembic 基线重建:丢弃现有 4 个迁移,以本模型为新基线 `000`;迁移编号使用三位顺序号(000、001、002…),生成统一经根目录 `make db-revision m="说明"`(自动编号,不手写哈希 id);迁移与退役的执行顺序见[重写执行计划](../operations/rewrite-plan.md)。
|
Alembic 基线重建:丢弃现有 4 个迁移,以本模型为新基线 `000`;迁移编号使用三位顺序号(000、001、002…),生成统一经根目录 `make db-revision m="说明"`(自动编号,不手写哈希 id);迁移与退役的执行顺序见重写执行计划(已收口删除)。
|
||||||
|
|
||||||
## 10. 隐私与删除的落库规则
|
## 10. 隐私与删除的落库规则
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
灵台向神识发送最小必要的版本化模型请求,向乾坤发送已裁决且范围明确的执行请求;返回结果先作为观察净化标证。运行时结构见[灵台运行时](lingtai-runtime.md)。
|
灵台向神识发送最小必要的版本化模型请求,向乾坤发送已裁决且范围明确的执行请求;返回结果先作为观察净化标证。运行时结构见[灵台运行时](lingtai-runtime.md)。
|
||||||
|
|
||||||
**基础设施复用边界**:现有 auth/RBAC/审计/事件存储/幂等/SSE 网关代码保留改造接入新领域层;旧领域模块(personality/drive/evolution/wuxing/time_sense/knocking/big_cycle 等)按[重写执行计划](../operations/rewrite-plan.md)退役删除。
|
**基础设施复用边界**:现有 auth/RBAC/审计/事件存储/幂等/SSE 网关代码保留改造接入新领域层;旧领域模块(personality/drive/evolution/wuxing/time_sense/knocking/big_cycle 等)按重写执行计划(已收口删除)退役删除。
|
||||||
|
|
||||||
## 5. 神识(模型网关,Go)
|
## 5. 神识(模型网关,Go)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# 魂镜能力矩阵
|
# 魂镜能力矩阵
|
||||||
|
|
||||||
> 状态:rewrite/1.0 重写期基线
|
> 状态:rewrite/1.0 重写期基线
|
||||||
> 作用:记录"当前已经验证到哪里",不重复产品目标或契约取值。没有证据链接的能力不得标为完成。重写的范围与节奏见[重写执行计划](rewrite-plan.md)。
|
> 作用:记录"当前已经验证到哪里",不重复产品目标或契约取值。没有证据链接的能力不得标为完成。重写的范围与节奏见重写执行计划(已收口删除,证据见本文与发布证据台账)。
|
||||||
|
|
||||||
## 1. 状态值
|
## 1. 状态值
|
||||||
|
|
||||||
@@ -37,6 +37,7 @@
|
|||||||
| 缘契/道体/河图 | M6 | 已验证 | M6 收口(2026-08-27,本地开发档):缘契提议—问心—铭契—撤回全链(多维无单一好感度、撤回即生效、事件史不可变);铸魂确认流(锚点确认前不变 + 魂籍版本链)与分灵谱系;河图境界(persistent_worlds + 实体关系 + 境界正史视图 + 世谱挂接);归真三界分流(境界/幻境实录 scope 隔离,真界本相零污染);引文→记忆正影后继提示;前端缘契面板与世界页。验收六测试全绿;旧人格/驱动/演化/世界模块与页面全量退役 |
|
| 缘契/道体/河图 | M6 | 已验证 | M6 收口(2026-08-27,本地开发档):缘契提议—问心—铭契—撤回全链(多维无单一好感度、撤回即生效、事件史不可变);铸魂确认流(锚点确认前不变 + 魂籍版本链)与分灵谱系;河图境界(persistent_worlds + 实体关系 + 境界正史视图 + 世谱挂接);归真三界分流(境界/幻境实录 scope 隔离,真界本相零污染);引文→记忆正影后继提示;前端缘契面板与世界页。验收六测试全绿;旧人格/驱动/演化/世界模块与页面全量退役 |
|
||||||
| 形仪域 | M7 | 已验证 | M7 收口(2026-08-27,本地开发档):真形锚点(特征/许可/可变范围);主仪化仪候选定相治理(candidate 不进呈现位、主仪唯一);声音三来源与克隆授权(用途/传播必填、可撤回立即生效);ASR 经元炁透传(多模态关闭 503 不降级);前端形仪工坊三区。验收三测试全绿;旧 avatar 全量退役。图像真实生成与 TTS 合成随神识 image/tts 槽位启用接入(候选定相与授权治理框架已就位) |
|
| 形仪域 | M7 | 已验证 | M7 收口(2026-08-27,本地开发档):真形锚点(特征/许可/可变范围);主仪化仪候选定相治理(candidate 不进呈现位、主仪唯一);声音三来源与克隆授权(用途/传播必填、可撤回立即生效);ASR 经元炁透传(多模态关闭 503 不降级);前端形仪工坊三区。验收三测试全绿;旧 avatar 全量退役。图像真实生成与 TTS 合成随神识 image/tts 槽位启用接入(候选定相与授权治理框架已就位) |
|
||||||
| 天工阁/叩心/辰正 | M8 | 已验证 | M8 收口(2026-08-28,本地开发档):受托任务(分解/并行/级联验收,后台 Run 不占前台席位);叩心候选四约束(总开关/静默本地时区/频率/预算,无关系副作用);辰正时态感知接入镜树投影。验收五测试全绿;旧太一/洛书/感知/大周天模块与表全量退役,admin 治理面收敛为司命+百工 |
|
| 天工阁/叩心/辰正 | M8 | 已验证 | M8 收口(2026-08-28,本地开发档):受托任务(分解/并行/级联验收,后台 Run 不占前台席位);叩心候选四约束(总开关/静默本地时区/频率/预算,无关系副作用);辰正时态感知接入镜树投影。验收五测试全绿;旧太一/洛书/感知/大周天模块与表全量退役,admin 治理面收敛为司命+百工 |
|
||||||
|
| 镜匣桌面壳与器谱 | M9 | 已验证 | M9 收口(2026-08-28,本地开发档):Tauri v2 最小壳(WebView 承载琉璃镜;能力桥通知命令;乾坤通道 HMAC 凭证构造与 sandbox 同算法——密钥仅经 SANDBOX_AUTH_SECRET 环境注入,二进制零平台密钥经静态审计钉死;hard_stop/resume 本地硬急停断全部通道);器谱 devices/device_grants(逐项授契/撤回立即生效/设备撤销连带失效,迁移 010);tantivy 依赖预留。验收四测试全绿 + cargo check 通过。正式打包(签名/updater 插件/安装器)与生产档联调属上线前事项 |
|
||||||
| 天目 / 元炁 | M4/M7 | 局部实现 | 元炁 ASR 已接形仪域(M7);chat 经神识 endpoint(M4);天目待接通感 |
|
| 天目 / 元炁 | M4/M7 | 局部实现 | 元炁 ASR 已接形仪域(M7);chat 经神识 endpoint(M4);天目待接通感 |
|
||||||
| 镜树前端 / 记忆治理 / 百工页 | M2+ | 局部实现 | 镜树页与灵胚页已上线(M2c,2026-08-21):路径阅读流/候答切换/采用归真/局部树图/SSE 芽体/导入卡;type-check + vitest 210 passed;记忆治理页随 M3、百工页随 M5 |
|
| 镜树前端 / 记忆治理 / 百工页 | M2+ | 局部实现 | 镜树页与灵胚页已上线(M2c,2026-08-21):路径阅读流/候答切换/采用归真/局部树图/SSE 芽体/导入卡;type-check + vitest 210 passed;记忆治理页随 M3、百工页随 M5 |
|
||||||
| 导入(角色卡 V2/V3 → 灵胚) | M2 | 局部实现 | M2b 已验证(2026-08-21):POST /characters/import-card 产出可审阅灵胚(identity_baseline.import_card 暂存 character_book),启镜时物化世谱书+条目(dict/list 双形态兼容);导出方向保持人物→ST V3 快照 |
|
| 导入(角色卡 V2/V3 → 灵胚) | M2 | 局部实现 | M2b 已验证(2026-08-21):POST /characters/import-card 产出可审阅灵胚(identity_baseline.import_card 暂存 character_book),启镜时物化世谱书+条目(dict/list 双形态兼容);导出方向保持人物→ST V3 快照 |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 发布证据台账
|
# 发布证据台账
|
||||||
|
|
||||||
> 状态:模板
|
> 状态:模板 + rewrite/1.0 重写期证据(第 3 节)
|
||||||
> 作用:为每个发布门提供可复核、可过期、可阻断的证据记录。本文不预填“已通过”。
|
> 作用:为每个发布门提供可复核、可过期、可阻断的证据记录。本文不预填“已通过”。
|
||||||
|
|
||||||
## 1. 证据记录格式
|
## 1. 证据记录格式
|
||||||
@@ -39,3 +39,24 @@
|
|||||||
- 失败证据不可删除或覆盖;修复后新增复测记录并关联原失败。
|
- 失败证据不可删除或覆盖;修复后新增复测记录并关联原失败。
|
||||||
- 依赖版本、部署拓扑、地区规则或契约版本改变时,相关证据立即转为待复核。
|
- 依赖版本、部署拓扑、地区规则或契约版本改变时,相关证据立即转为待复核。
|
||||||
- 发布批准必须列出所有适用 `gate_id` 及其当前证据;缺失即阻断,不以会议口头结论替代。
|
- 发布批准必须列出所有适用 `gate_id` 及其当前证据;缺失即阻断,不以会议口头结论替代。
|
||||||
|
|
||||||
|
|
||||||
|
## 3. rewrite/1.0 重写期证据(2026-08-28 收口,本地开发档)
|
||||||
|
|
||||||
|
> 以下为重写执行计划各里程碑的自动化验收证据,部署档位=本地开发(SQLite/回声桩/进程内神识)。
|
||||||
|
> 生产档(PG/真实 Provider/Compose 全栈)联调、SLO/灾备/安全门与全球区域门未在本周期范围——
|
||||||
|
> 上线前须按 §2 硬门证据包逐项补齐,本表不替代发布门。
|
||||||
|
|
||||||
|
| `gate_id` | 场景 | 结果(自动化套件) | 结果物 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `RW-KERNEL-001` | Kernel 不变量(事件序列/命台串行/否决/流式) | 通过(test_kernel.py 5 用例) | pytest 输出 |
|
||||||
|
| `RW-JINGSHENG-001` | 启镜原子闸门/并发命台/魂籍签名 | 通过(test_jingsheng.py 7 用例) | pytest 输出 |
|
||||||
|
| `RW-MIRROR-001` | 镜树 PRD 十验收场景 | 通过(test_mirror_tree.py + M2 覆盖) | pytest 输出 |
|
||||||
|
| `RW-XIRANG-001` | 未归真自动入忆率=0 / 重忆确认 / 正影 / 删除墓碑 | 通过(test_xirang.py 8 用例) | pytest 输出 |
|
||||||
|
| `RW-SHENSHI-001` | 故障注入四场景(首字超时切道/凭证冷却/全败 JSON/用量聚合)+ BYOK/配额三段式/归属计量 | 通过(Go 34 用例 + 跨进程 E2E 四场景) | go test 输出 |
|
||||||
|
| `RW-BAIGONG-001` | 未授契拒留痕 / 请命超时不执行 / 禁域不可关闭 / 结果为观察 | 通过(test_baigong_siming.py 4 用例) | pytest 输出 |
|
||||||
|
| `RW-YUANQI-001` | 多维无单一好感度 / 撤回立即生效 / 铸魂确认流 / 分灵谱系 / 境界不进真界 | 通过(test_m6_domains.py 6 用例) | pytest 输出 |
|
||||||
|
| `RW-XINGYI-001` | 未定相不成为正式形仪 / 克隆授权可撤回 / 关多模态文字回归 | 通过(test_m7_xingyi.py 3 用例) | pytest 输出 |
|
||||||
|
| `RW-TIANGONG-001` | 后台无身份写入 / 并行不占席位 / 叩心四约束 / 不回应不降关系 | 通过(test_m8_tiangong.py 5 用例) | pytest 输出 |
|
||||||
|
| `RW-JINGXIA-001` | 器谱逐项授契与撤回 / 镜匣零平台密钥 / 硬急停 | 通过(test_m9_jingxia.py 4 用例 + cargo check) | pytest/cargo 输出 |
|
||||||
|
| `RW-REGRESSION-000` | 全量回归(后端/前端/BFF/神识/镜匣) | 通过(349+203+4+5 包+cargo;2 个预存环境失败与本周期无关,已 HEAD 复验) | 各套件输出 |
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
# 重写执行计划
|
|
||||||
|
|
||||||
> 状态:执行中(rewrite/1.0 分支主线)
|
|
||||||
> 职责:记录本次全量重写的范围决策、退役与复用清单、里程碑划分与验收门。**本文件是临时治理文档:重写完成、能力矩阵接棒后删除。**
|
|
||||||
> 决策依据:2026-08-21 与执镜者确认的九条决策(架构基准=AGENTS.md 完整技术栈、领域重写+基础设施复用、旧码退役、重写期间新建 Go 神识、Fastify BFF、最小镜匣壳、全量 1.0 规划、文档主线化改写、运行时插件化+硬核例外)。
|
|
||||||
|
|
||||||
## 1. 执行原则
|
|
||||||
|
|
||||||
1. 每个里程碑是可独立验收的纵向闭环(端到端可运行),不做孤立页面或孤立接口;
|
|
||||||
2. 先写失败测试再实现(Kernel 不变量、镜树不变量、记忆分级是最高优先级测试对象);
|
|
||||||
3. 新领域模块落地的同时删除对应旧模块(不做长期共存);
|
|
||||||
4. 每个里程碑完成时更新[能力矩阵](capability-matrix.md)证据。
|
|
||||||
|
|
||||||
## 2. 复用清单(保留改造)
|
|
||||||
|
|
||||||
| 资产 | 处置 |
|
|
||||||
| --- | --- |
|
|
||||||
| `backend/app` auth / RBAC / audit / event_store / idempotency / SSE 网关 / core 配置框架 | 保留,重组入 `infra/`,对接六域 |
|
|
||||||
| 乾坤沙箱(`sandbox/`,Rust) | 保留;HMAC 授契流程对接司命 |
|
|
||||||
| 博闻(`mcp/web-search/`,Go) | 保留;登记为百工 MCP 能力 |
|
|
||||||
| 天目(`tianmu/`) | 保留;P1 通感期接入 |
|
|
||||||
| 元炁(`yuanqi/`) | 保留;作为神识 `type=local` Endpoint |
|
|
||||||
| `frontend/design/` 设计体系与 `styles/tokens` | 保留;新页面沿用 |
|
|
||||||
| docker 基础设施编排(`docker/docker-compose.infra.yml`) | 保留;应用编排新增照壁/神识/删除玄关 |
|
|
||||||
| 现有测试基建(pytest / vitest / pre-commit / husky) | 保留 |
|
|
||||||
|
|
||||||
## 3. 退役清单(删除,不留兼容层)
|
|
||||||
|
|
||||||
### 3.1 backend 旧领域模块 → 新去处
|
|
||||||
|
|
||||||
| 旧模块 | 新去处 |
|
|
||||||
| --- | --- |
|
|
||||||
| `modules/chat` + `models/chat`(线性会话) | 镜树域(mirror_trees/tree_nodes/citations/canonizations) |
|
|
||||||
| `modules/memory` + `models/memory`(旧记忆) | 息壤(memories/triggers/embeddings/factual_records) |
|
|
||||||
| `modules/personality` / `personality_emotion` + models | 灵核插件(道体锚点+调节层),情绪状态并入调节层参数 |
|
|
||||||
| `modules/drive` + models | 太一插件(目标) |
|
|
||||||
| `modules/evolution` + models | 铸魂/正影流程(characters + confirmations) |
|
|
||||||
| `modules/goal_orchestrator` / `planner` + models(goals/plans) | 太一/洛书插件 |
|
|
||||||
| `modules/big_cycle` + models | 删除(后台整理并入息壤维护任务) |
|
|
||||||
| `modules/time_sense` + time_anniversary | 辰正(M8 重实现为横切能力) |
|
|
||||||
| `modules/knocking` | 叩心枢(M8 重实现) |
|
|
||||||
| `modules/extraction` | 息壤提取管线(M3 重写) |
|
|
||||||
| `modules/reflection` / `perception` | 烛照插件(M6/M8) |
|
|
||||||
| `modules/agent` | 天工阁插件(M8) |
|
|
||||||
| `modules/safety` + models/safety | 司命插件(M5 重写,四维裁决) |
|
|
||||||
| `modules/skills` / `tool_runtime` + models/skills | 百工插件(M5 重写) |
|
|
||||||
| `modules/mcp` + models/mcp_servers | 百工 MCP Client(mcp_servers 表保留) |
|
|
||||||
| `modules/runtime` | Kernel + 运行时(M0 重写) |
|
|
||||||
| `modules/imports` / `character_import` | 导入域(M2 改造对接灵胚+世谱) |
|
|
||||||
| `modules/lorebook` + models/lorebook | 世谱(表保留,接入河图激活管线) |
|
|
||||||
| `modules/macros` | 符文引擎(提示词组装期展开,M2) |
|
|
||||||
| `modules/world` + models/world | 河图(persistent_worlds/world_entities,M6 重建) |
|
|
||||||
| `services/avatar` / models 相关 | 形仪域(M7) |
|
|
||||||
| `services/ai_provider` + admin | **删除**:神识 Go 网关接管(M4),配置文件移交神识 |
|
|
||||||
| 玄关(旧 Go MCP 网关,如有独立工程) | 删除:MCP 登记并入百工 |
|
|
||||||
|
|
||||||
### 3.2 数据库
|
|
||||||
|
|
||||||
- 丢弃 Alembic 4 个旧迁移与 `db/hunjing.db` 开发数据;按[数据模型](../architecture/data-model.md) §9 清单重建基线 `000`(三位顺序编号,经 `make db-revision` 生成)。
|
|
||||||
- 表处置以该清单为准(保留改造 / 新建 / 退役删除三列)。
|
|
||||||
|
|
||||||
### 3.3 frontend 旧页面 → 新去处
|
|
||||||
|
|
||||||
| 旧页面 | 新去处 |
|
|
||||||
| --- | --- |
|
|
||||||
| `chat` | **已删除(M2b)**;镜树页随 M2c 新建(当前路径阅读 + 局部树图 + 候答切换/采用/归真) |
|
|
||||||
| `characters` | 人物(灵胚→启镜→魂籍→缘契→记忆→形仪分区) |
|
|
||||||
| `memory` | 记忆治理(三级记忆/候选箱/撤销/删除) |
|
|
||||||
| `models` | 神识阁(偏好/BYOK/用量) |
|
|
||||||
| `tools` | 百工(能力目录/授契/行契) |
|
|
||||||
| `settings` / `login` / `register` / `admin` | 保留改造 |
|
|
||||||
| `world` | 世界(境界/世谱,M6 重建) |
|
|
||||||
| `time` | **已删除(M2b,随旧时间服务退役)**;辰正感知 M8 重实现后另设 |
|
|
||||||
| `cosmos` / `drive` / `evolution` / `personality` / `time` / `wuxing` / `runs` | 删除(`runs` 并入镜树/天工阁轨迹视图,其余旧模型页面随模块退役) |
|
|
||||||
| `archive` | 并入镜树抽屉(归档树列表) |
|
|
||||||
|
|
||||||
## 4. 里程碑
|
|
||||||
|
|
||||||
> 编号为实施顺序;每个里程碑末尾的"验收"是进入下一个的硬门。
|
|
||||||
|
|
||||||
### M0 地基(骨架先行)— ✅ 已完成(2026-08-21)
|
|
||||||
|
|
||||||
- 交付:Kernel 骨架(不变量裁决器/命台控制/事件日志接口/循环骨架)、事件总线与插件加载、`shenshi_client` 边界(先指向临时 stub)、BFF 骨架(鉴权透传+SSE 代理)、神识骨架(ingress/transform 占位实现 + 元炁 endpoint)、Alembic 新基线、Compose 更新(+hunjing-bff/+hunjing-shenshi)。
|
|
||||||
- 验收:登录(复用 auth)→ 创建占位 Run → SSE 端到端流到浏览器(经照壁);"模型可见即可重建"断言在 stub 上可跑;旧 chat/memory 模块仍可用但不再新增调用。
|
|
||||||
- 证据:`backend/tests/test_kernel.py` 5 用例(事件序列/命台串行/否决/不变量/流式);`shenshi` `go test` 5 用例;`bff` vitest 4 用例;三服务(照壁 18100 / 灵台 18000 / 神识 18081)真实进程 E2E 冒烟:注册→登录→POST /api/v1/runs→SSE 流读至 run.finished,模型流经 Go 神识 stub("【神识回声桩】"前缀可证);全量后端回归 925 passed(另有 20 个预存失败,根因本地缺 `config/models.yaml`,与本次改动无关,stash 验证);Alembic 基线合并为单一迁移(41 表,空库 upgrade 干净)。
|
|
||||||
|
|
||||||
### M1 镜生闭环(灵胚→启镜→魂籍→命台)— ✅ 后端闭环已完成(2026-08-21)
|
|
||||||
|
|
||||||
- 交付:embryo_drafts/custodianships/runtime_seats/identity_records 表与 API(迁移 `001`,顺序编号);灵胚创建(原创/领养/导入来源受理)、照心试演(`run.kind=rehearsal`,幻境 Run、无人物、不产生经历)、启镜(原子闸门)、放弃;死亡请求/撤销/落定与冷静期;魂籍版本化导出与校验;命台租约 API(冲突留痕、过期接管、epoch 递增)。
|
|
||||||
- 验收:启镜前无正式人物 ID;同一人物并发启镜/命台抢占被拒且留痕;魂籍导出可验证、复制不能冒充(签名摘要校验用例)。
|
|
||||||
- 证据:`backend/tests/test_jingsheng.py` 7 用例全绿;全量回归 932 passed(20 个预存失败与 M0 时相同,缺 `config/models.yaml`);`make db-upgrade` 空库 000→001 干净应用(含 SQLite batch alter)。
|
|
||||||
- 遗留到 M2:随缘灵胚候选生成、角色卡导入对接灵胚、Kernel 前台串行接入 DB 租约(多进程一致)、前端人物页。
|
|
||||||
|
|
||||||
### M2 镜树闭环(核心主线)— 🔶 M2a+M2b 后端完成(2026-08-21),M2c 前端进行中
|
|
||||||
|
|
||||||
- 交付:mirror_trees/tree_nodes/citations/canonizations/transplants 全量 API(迁移 `002`);另答/续思/续生/编辑分叉/采用/归真(三界 scope)/移栽/引文锚;Kernel `RunOutcome` 回调实现芽体→固化/残答封存(run 关联 tree 事件);祖先链投影 prompt 组装。
|
|
||||||
- 验收:镜树 PRD §14 十个验收场景全部自动化通过。
|
|
||||||
- 证据(M2a):`backend/tests/test_mirror_tree.py` 8 用例覆盖场景 1/2/3/5/6/7/8 与三界作用域;全量回归 940 passed(预存 20 失败不变);`make db-upgrade` 000→002 干净。场景 4(未归真不入忆)随 M3 记忆管线落测试;场景 9(移动端)与前端页面随 M2b。
|
|
||||||
- 证据(M2b,2026-08-21):`test_import_and_assembly.py` 3 用例(V1/V2 卡→灵胚、启镜物化世谱、符文展开+火候入 IR);旧 chat 体系删除——backend 删 modules/chat、application/chat、knocking、personality_emotion、services/time_sense、纪念日模型及关联仓储/端点/测试,`runs.session_id` 与四张旧表经迁移 003 退役(SQLite batch 双向验证通过);前端删 chat/time 页面与 api 库(type-check + vitest 199 passed);全量后端 846 passed、余 7 个预存失败(缺 `config/models.yaml`)。
|
|
||||||
- 提前退役说明:knocking/personality_emotion/纪念日原定 M6–M8 退役,因与 chat 线性会话强耦合随 M2b 一并删除;`modules/time_sense` 纯计算核(snapshot/duration/lunar,被符文引擎依赖)保留,辰正 M8 重接。
|
|
||||||
- 证据(M2c,2026-08-21):`/trees` 列表与 `/trees/[treeId]` 工作区(路径阅读流、候答切换 `i/n`、另答/续思/续生/编辑分叉/采用/归真、归真对话框含三界效力说明与 HJ-1102 分歧提示、局部树图可折叠、SSE 芽体流式);`/embryos` 灵胚页(原创/导入卡/照心试演流/启镜强确认/放弃);API 库 `trees.ts`/`embryos.ts` + 11 用例;type-check 与 vitest 210 passed。Next 内嵌 BFF(/api/proxy、Cookie 鉴权)暂为前端通道,收口到照壁列为部署迁移项。
|
|
||||||
- M2c 余项:移栽/引文锚 UI、已归真标记在树图回显(随 M3 记忆闭环)、Kernel 前台串行接入 DB 租约、眉批表(归入 M6 言灵编排)。
|
|
||||||
|
|
||||||
### M3 记忆闭环(息壤)— ✅ 后端完成(2026-08-22),M3b 前端治理页进行中
|
|
||||||
|
|
||||||
- 交付:三级记忆管线(归真触发提取/白名单明忆自动生效/重忆 pending 确认/确定性去重/正影版本链/删除墓碑无原文);双通道召回(关键词+活力时近)+ RRF 融合 + 条数/字符预算 + 命中强化与召回统计;获准记忆注入投影 prompt(【获准记忆(来源可查)】段,source_refs 指向归真可展开);实录(factual_records)随归真同步落库;手动入忆强制 fact/insight/opinion/hypothesis 标注,未归真内容只作 fact_candidate。
|
|
||||||
- 验收:全部由 `backend/tests/test_xirang.py` 7 用例钉死——未归真自动入忆率=0、重忆 pending 不入召回(confirm 后进入)、deny/删除墓碑全链路停用、正影旧版停召且原文不改写、手动入忆标注、召回注入 prompt 可验证。
|
|
||||||
- 退役(随本里程碑):旧 memory/extraction/reflection/big_cycle/evolution 服务与端点、旧 memories/memory_links 表(迁移 004 重建)、旧导入抽取触发(410 语义)、前端 evolution/memory 旧页与组件;全量后端 779 passed(余 6 个预存失败不变)、前端 type-check + 202 passed。
|
|
||||||
- 证据(M3b,2026-08-22):世谱激活进入投影(关键词命中 + 常驻条目、insertion_order 排序、字符预算、条目内容符文展开,`test_xirang.py` 世谱注入用例);前端 `/memories` 治理页(待确认/候选箱/已生效/归档/实录五分区,确认/拒绝/正影/删除/来源展开,删除强确认),API 库 +3 用例;type-check 与 205 passed。
|
|
||||||
- M3b 顺延项及理由:微痕时序效果(sticky/cooldown/delay)与次级键逻辑、递归扫描需要 Run 内轮次状态,随 M5 循环上下文落地一并实现;pgvector 语义通道随标准档部署接入(当前活力时近通道占位)。
|
|
||||||
|
|
||||||
### M4 神识完整接管 — ✅ 已完成(2026-08-27,M4a 网关 + M4b 切换手术 + 用户面收口)
|
|
||||||
|
|
||||||
- 交付:Go 神识全量(transform adaptor:OpenAI 兼容+元炁;路由/冷却/粘性;计量;配额);灵台切换 `shenshi_client`;**删除 Python ai_provider 与相关配置/页面 models 旧实现**;神识阁用户面(偏好/BYOK/用量)。
|
|
||||||
- 验收:故障注入(首字超时切道、凭证级冷却、熔断恢复)通过;usage 与 Provider 对账抽样一致;灵台内无 Provider SDK import(rg 审计为空)。
|
|
||||||
- 证据(M4a,2026-08-27):OpenAI 兼容适配器(元炁共用协议)+ `SHENSHI_ENDPOINTS` env 端点目录;候选路由(Priority+粘性置首+冷却排尾兜底)、首字超时切道(首帧前不提交响应,失败整体回退 JSON)、401/403 凭证级冷却 10 分钟 vs 429/5xx/超时绑定级 30 秒、已写字节不换道;计量(stdout JSON `shenshi_usage` + `/me/usage` 聚合)。Go 测试 10 用例全绿(含四场景故障注入:首字超时切道/凭证冷却跳过/全败 JSON/用量聚合);跨进程 E2E:灵台 `HttpShenshiClient` → 真 Go 神识 → stub 流式往返通过。假上游跨进程拨号在本沙箱受出站限制,改以 Go 进程内 httptest 覆盖(同真实 TCP 语义)。
|
|
||||||
- 证据(M4b,2026-08-27):手术清单 ①-⑥ 全量执行——后端删 `app/ai_provider/`(38 文件)与 `services/ai_provider_admin.py`、`core/config/model_config.py`、`tests/ai_provider/`(21 文件);连带退役旧 Agent 循环(`application/agent/`、steer/follow-up/确认注册表、`app/skills/` 孤儿、`endpoints/audio.py`、`application/imports.py`);runs.py 修剪为 kernel 契约端点,safety 确认改直接落库裁决(pending→approved/denied);前端删 models 页/gateway.ts(玄关)/网关组件/audio/types/mock;config 三 yaml 归档 `shenshi/config/`,`.env.example` 收敛 SHENSHI_*。后端 528 passed(2 个预存失败经 HEAD 复跑验证与本手术无关)、前端 type-check + 201 passed。
|
|
||||||
- 证据(M4 用户面收口,2026-08-27):神识补齐 BYOK(AES-GCM + 脱敏 + 独占路由 + fail-closed)、配额三段式(预扣钳位/结算/幂等退款,HJ-2004)、usage 归属计量与对账抽样(`/me/usage`、`/admin/usage/sample`)、Anthropic/Gemini 适配器、神识自有库 store(PG + 内存回退,60s 就绪重试);IR 契约扩展 `user_id`/`byok_key_id`(Go+Py 同步);灵台 runs.owner_id(迁移 005)经 kernel 透传 IR,`/me/byok-keys`|`/me/usage` 代理端点 + 神识阁前端(偏好/BYOK/用量三区,撤回二次确认)。Go 34 用例、后端 532、前端 210 passed;跨进程 E2E 四场景(灵台 HttpShenshiClient → 真 Go 神识:登记脱敏/独占失败/归属聚合/撤回)通过。rg 审计灵台 Provider SDK import 归零。
|
|
||||||
- M4b 手术清单(2026-08-27 侦察固化,30+ 文件):
|
|
||||||
① 后端删除:`app/ai_provider/`(26 文件,含 anthropic/openai 格式与 billing——神识已承接)、`services/ai_provider_admin.py`、`core/config/model_config.py`、`tests/ai_provider/`(16 文件);
|
|
||||||
② 连带退役:`application/agent/`(旧 Agent 循环,steer/follow-up/确认注册表——live UI 已随 chat 页消亡,天工阁 M8 重建);`endpoints/audio.py`(ASR/TTS 归 P1 形仪域);`endpoints/settings.py` 与 `services/preferences.py` 中模型偏好字段(切神识阁);admin 网关端点与 `test_model_gateway_endpoints.py`;`main.py` 的 `reload_provider_manager`、conftest 的 endpoint 覆盖缓存清理;
|
|
||||||
③ runs.py 修剪:`AgentRunRegistry`/`follow-up`/confirmations 端点随旧 Agent 循环退役,保留 kernel 契约端点;
|
|
||||||
④ 前端删除:models 页、`lib/api/gateway.ts`、`ModelGatewayPanel`/`ModelStrategyForm`、mock 与 types 中的网关类型;
|
|
||||||
⑤ 配置:`config/models|providers|pricing.example.yaml` 归档至 shenshi 目录或删除,`.env.example` 模型段收敛为 SHENSHI_*;
|
|
||||||
⑥ 验收:rg 审计灵台无 Provider SDK import(`rg -l 'openai|anthropic' app/` 排除 shenshi_client 为空);全量后端/前端测试绿;迁移无涉及(纯代码删除)。
|
|
||||||
|
|
||||||
### M5 百工与司命 — ✅ 已完成(2026-08-27)
|
|
||||||
|
|
||||||
- 交付:百工域 `domains/baigong`——能力注册表(`capabilities`:统一契约 + 执行类型 builtin/qiankun/mcp/http + 四维风险标注 capability/data/consequence/context)、授契/行契(`mandates`:grant 单次范围 / charter 预算时间窗重复执行,撤销立即生效)、请命(`pending_confirmations` 改造:六要素说明 + 超时=拒绝 HJ-1302)、行动账(`tool_calls` 净化摘要 + 观察标证 + audit_events 留痕);司命裁决器 `siming.py`(四维取最高档 + **禁域代码常量**:递归删根/格式化/写裸设备/fork 炸弹、file://、密钥外发——任何 Profile/插件/配置不可关闭);乾坤经 SandboxClient HMAC 授契执行;MCP 工具登记/刷新自动派生能力(博闻接入点);`/capabilities` `/mandates` `/petitions` `/tool-calls` 四组端点 + 前端百工页(四区);**旧 skills/tool_runtime/safety/agent 模块与 tool_policies/skills/model_endpoint_overrides 表退役**(迁移 006)。
|
|
||||||
- 验收(tests/test_baigong_siming.py 四用例钉死):① 未授契调用 403 HJ-1303 且 audit_events/tool_calls 双留痕;② 请命超时(expires_at 拨过)approve → 410 HJ-1302、无成功执行;③ 禁域专项(恶意 L0 标注 + 篡改领域对象仍拒 HJ-1301,代码级三形态断言);④ 调用后 memories 表零新增、tool_calls.evidence 带 observation_not_fact 标证。
|
|
||||||
- 证据:后端 454 passed(2 个预存环境失败不变);前端 type-check + 220 passed(百工组件 4 用例 + api 6 用例);四套件(backend/shenshi/bff/frontend)全绿。
|
|
||||||
|
|
||||||
### M6 缘契、人格与世界 — ✅ 已完成(2026-08-27)
|
|
||||||
|
|
||||||
- 交付:缘契域 `domains/yuanqi`(covenants 当前版 + covenant_events 不可变历史;提议—问心—铭契—撤回全链,边界变更立即生效,多维关系并存且**拒绝单一好感度键**);道体铸魂与分灵 `jingsheng/soul.py`(soul_forgings 确认流——锚点变更确认前不生效,确认后签发新魂籍版本;分灵创建新人物继承谱系不继承身份,魂籍从 v1 起);河图域 `domains/hetu`(persistent_worlds 境界 + world_entities/world_relations 重建为境界作用域 + 世谱挂接 + 境界正史视图);归真按三界分流(reality→息壤提取不变;persistent_world→境界实录+正史推进;simulation→只记试演实录,均不进真界本相,factual_records 增 scope 列);引文→记忆正影后继提示(citations target_type=memory 时 superseded 提示 successor_memory_id,不静默替换);前端人物页缘契面板 + 世界页重建(境界目录/实体/正史概览)。
|
|
||||||
- 退役:modules/personality|drive|evolution、models/personality|drive|evolution|world(旧治理版)、services/personality|drive|world、endpoints/personality|drive|world、governance 世界实体治理面、前端 personality/drive/cosmos/wuxing/runs 页与对应组件/api 库/样式;表退役(迁移 007):personality_versions/personality_proposals/curiosity_questions/exploratory_subgoals/evolution_candidates/character_world_entities/world_entities(旧)/world_relations(旧)/characters.current_personality_version_id。
|
|
||||||
- 验收(tests/test_m6_domains.py 六用例钉死):多维无单一好感度(affinity 提议被拒)+ 问心铭契版本链 + 事件史不可抹除;撤回立即生效(withdrawn 即不可见、再操作 410、事件可查证)+ 边界变更即时反映;铸魂确认流(确认前锚点不变、确认后锚点更新且魂籍版本=最大版本)+ 分灵谱系(新 id、lineage 含源、魂籍 v1);境界正史不进真界(persistent_world 归真后真界记忆候选=0、实录 scope 隔离);引文正影后继提示。
|
|
||||||
- 证据:后端 379 passed(2 个预存环境失败不变);前端 type-check + 199 passed;BFF 4;神识 5 包;迁移 007 净库升级 + ±1 干净。
|
|
||||||
|
|
||||||
### M7 形仪与语音(P1 面)— ✅ 已完成(2026-08-27)
|
|
||||||
|
|
||||||
- 交付:形仪域 `domains/xingyi`——`form_anchors` 真形锚点(稳定辨识特征/年龄表现/许可禁忌/可变范围,生成内容不可静默覆盖);`forms` 主仪/化仪/文字形仪(**候选→定相治理**:candidate 不进呈现位,adopt 才转正,主仪唯一、新主仪自动退役旧主仪);`voice_profiles` 声音三来源(合成预设/原创设计/授权克隆——克隆必填用途与传播范围,**授权可撤回且立即生效**,撤回/过期后合成前置校验拒绝);ASR 语音转写(元炁 SenseVoice `/v1/audio/transcriptions` 透传,未配置多模态时 503 不降级不伪造);前端形仪工坊(锚点编辑/候选定相废弃/声音档案与授权撤回)挂人物详情页,头像呈现位改走定相主仪。Live2D 基座沿用现有 pixi-live2d-display 吉祥物组件(forms.live2d_json 承载模型与动作映射引用);图像生成候选治理以 image_gen 来源候选+定相流程落地(真实生成模型接入随神识 image 槽位启用)。**旧 avatar 模块删除**(服务/端点/配置/avatar_url 字段/前端直传组件)。
|
|
||||||
- 验收(tests/test_m7_xingyi.py 三用例钉死):①未定相不成为正式形仪(candidate 期呈现位为空、定相后转正、主仪唯一旧主仪 retired、废弃可用);②克隆授权可撤回(缺用途被拒、有效通过、撤回后校验 403/409、预设不受授权约束);③关闭全部多模态后文字相伴完整可用(ASR 503 时镜树/缘契/百工链路照常)。
|
|
||||||
- 证据:后端 376 passed(2 个预存环境失败不变,6 个 avatar 旧用例随模块退役);前端 type-check + 203 passed(形仪工坊 4 用例);BFF 4;神识 5 包;迁移 008 净库+±1 干净。
|
|
||||||
|
|
||||||
### M8 天工阁、叩心与辰正 — ✅ 已完成(2026-08-28)
|
|
||||||
|
|
||||||
- 交付:天工阁域 `domains/tiangong`——backstage_tasks(委托包+验收标准+结果证据,parent 链分解受控并行,子任务全终态级联父任务待验收,按证据 accept/reject);`kernel.start_backstage_run`(kind=background_task,**不占前台席位**与前台并行;loop 后台分支绕过席位锁);叩心域 `domains/kouxin`——knocking_candidates(触发/草稿/裁决快照;**总开关/静默窗口(本地时区)/频率/预算**四约束裁决,dispatch 未过被拒 429 且快照留痕,候选默认不自动发送);辰正 `domains/zhengchen`——temporal_context() 复用 time_sense 计算核产出【时态感知】投影段(客观时间语境不附情绪臆测),接入镜树生成 prompt;**旧 goal_orchestrator/planner/perception/big_cycle/agent(knocking M2b 已删) 模块与 goals/plans/agent_roster/perception_channels/big_cycles/candidates 表退役**(迁移 009),governance 只留 GenerationPreferences,admin 只留司命/百工两枢治理面;kernel 断言放宽容纳无人物后台任务。
|
|
||||||
- 验收(tests/test_m8_tiangong.py 五用例钉死):①后台任务不构成第二意识(完成后记忆/魂籍/缘契零新增断言);②后台 Run 与前台并行(运行中前台立即启动);③分解级联(子任务全终态→父待验收);④叩心四约束(总开关关拒/静默窗口拒/频率上限拒/常态允许);⑤不回应不降低关系(候选过期后缘契维度/状态/版本零变化)。
|
|
||||||
- 证据:后端 345 passed(2 个预存环境失败不变);前端 type-check + 203 passed;BFF 4;神识 5 包;迁移 009 净库升级干净。
|
|
||||||
|
|
||||||
### M9 桌面壳与收口
|
|
||||||
|
|
||||||
- 交付:镜匣最小壳(WebView+能力桥+乾坤通道+器谱登记+ updater,tantivy 预留);Compose/nginx 终态;文档与契约全量核对;发布门证据格式落位(对照[发布门](release-gates.md)与 first-release-scope 逐项检查)。
|
|
||||||
- 验收:桌面壳零平台密钥、未授契能力被拒、硬急停可用;全量回归(M1–M8 验收自动化套件绿);docs 五层与代码一致(能力矩阵证据更新完毕)→ 删除本计划文件。
|
|
||||||
|
|
||||||
## 5. 新旧切换纪律
|
|
||||||
|
|
||||||
- 旧模块删除时点绑定里程碑(见各条"删除"项),删除前 `rg` 确认无真实调用点;
|
|
||||||
- 禁止新旧 API 并存过渡(开发期契约直接重建,无兼容层);
|
|
||||||
- 开发数据库随基线重建丢弃,不写数据迁移;
|
|
||||||
- 每个里程碑一个合并批次,提交信息中文、可追溯到本计划条目(如 `M2:镜树归真与采用分离`)。
|
|
||||||
|
|
||||||
## 6. 风险与对策
|
|
||||||
|
|
||||||
| 风险 | 对策 |
|
|
||||||
| --- | --- |
|
|
||||||
| 插件化过度设计拖慢主线 | Kernel+总线先于插件丰富度;九枢按里程碑逐步装配,M0 只需 stub 插件跑通 |
|
|
||||||
| 神识一次性切换失败 | M4 内先双写用量对比再切换;元炁本地端点保证无云密钥也可全链路演示 |
|
|
||||||
| 镜树前端复杂度(局部树图/断线续传) | M2 先交付当前路径阅读流,树图与抽屉分两批;SSE 续传以事件日志重放为保证 |
|
|
||||||
| 全量 1.0 周期长导致中途失衡 | 里程碑独立验收+能力矩阵滚动更新;任何时点主干可运行 |
|
|
||||||
| 删除旧模块误伤复用资产 | 删除前核对 §2 复用清单与调用点扫描;infrastructure 资产先重组入 `infra/` 再删领域层 |
|
|
||||||
Reference in new issue
Block a user