Compare commits

..
11 Commits
Author SHA1 Message Date
chenxin b63542add3 完善前端 2026-09-21 11:19:07 +08:00
chenxin 7c57d27494 ✨ feat: WP-09 Live2D/3D——模型导入校验与非破坏命令历史
- 迁移 18:avatar_model(live2d/vrm/gltf、validated/rejected、校验报告、来源许可、当前命令指针)+ avatar_command(十类命令检查约束、(avatar,sequence) 唯一、undone 标记)
- GLB/glTF 解析器(纯 Python):GLB magic/版本/chunk 结构校验、JSON 场景图引用一致性(scene→node→mesh→material 逐层索引越界检测)、VRM 0.x/1.0 扩展检测与 meta 提取
- 资源上限(恶意模型文件防线):文件 200MB、节点 10 万、网格 1 万、纹理 2 千、材质 5 千、动画 1 千——超限入库 rejected 并保留报告
- Cubism model3.json 校验:Version/FileReferences 必需、moc3 引用、纹理列表、表情/动作/HitArea 计数
- 上传 MIME 白名单扩展 model/ 与 application/json
- 命令历史:十类命令(param_map/expression/physics/motion/material/texture/bone/attachment/lighting/camera)追加(sequence 单调)、撤销(标记 undone 非破坏)、rejected 模型不接受编辑(422 前置校验)
- 测试 111 项全绿(+9):合法 GLB/VRM 检测/损坏文件/引用越界/超限/Cubism 正反例/命令历史与撤销幂等/rejected 拒编
2026-09-14 10:34:52 +08:00
chenxin 9dafb8fe72 ✨ feat: 灵台引擎 gRPC 独立服务形态(ADR-017 双形态落地)
- proto(engine/proto/liushen/v1):LingtaiEngine 九方法——GetEngineInfo(版本协商/能力位)、TransformMarkdown、RebuildExport(UNIMPLEMENTED 明示随 WP-03)、OpenStream 双向流、CheckBudget、RerankRecall、EvaluatePolicy、ValidateUrl、CheckStateTransition;RunStatus 枚举与契约一致
- 服务端参考实现(engine/server/lingtai_server.py):进程内直链 nanobind 引擎核心——双形态同核同 golden;开发档 127.0.0.1 明文(生产 mTLS,文案明示);状态转换表按 agent-runtime 第 3 节
- 集成测试 6 项:进程内随机端口起服——版本协商、双形态 golden(服务输出==进程内引擎输出,ADR-017 核心不变量首次实证)、双向流 envelope 序号单调、决策表/URL/状态机、RebuildExport UNIMPLEMENTED 明确拒绝(不伪造)
- Makefile:lingtai-proto(stub 再生)与 lingtai-server(启动服务)目标
- 注:C++ 原生二进制(hunjing-lingtai-server)随交叉编译矩阵交付;Python 参考实现保证 proto 与语义先行锁定
- 测试 102 项全绿(+6);ruff/mypy 干净
2026-09-14 10:21:54 +08:00
chenxin a39cec5287 ✨ feat: 灵台引擎五神齐备与 Go/Rust 官方绑定(WP-15)
- C++ 新增四模块(modules.cpp,19 个导出符号全部 hj_*):
  小周天 hj_stream:流会话(sequence/delta_index 单调)与 content.block.delta envelope 确定性编码(重放字节一致)
  烛照 hj_policy:URL 协议白名单(拒 file/javascript/data)、路径授权根与 .. 段拒绝、幂等键非空≤128、四维决策表(autonomy×risk→allow/ask/deny,非法输入 deny fail-closed)
  洛书 hj_plan:预算核算(达限/负数拒绝)
  河图 hj_recall:确定性重排(词频×稳定排序,canonical JSON 输出;非法输入 PARSE_FAILED)
- doctest 22 用例 20135 断言全绿(含每模块 golden 与确定性双跑)
- Go 绑定(cgo 自包含编译引擎源码):Engine RAII/TransformMarkdown/ValidateURL/ValidatePath/Decide/CheckBudget/RecallRerank;go test 3 用例通过(transform/rerank golden 与 C++ 字节一致)
- Rust 绑定(bindgen 生成 + 安全包装):Engine RAII(Drop 释放)、Error 枚举映射全部 HjStatus、Send/Sync(HjEngine 线程安全);cargo test 3 用例通过(同 golden)
- 三端(C++/Go/Rust)与 Python golden 一致:同一 markdown 输入在四端输出字节相同(跨端一致性首次全语言验证)
2026-09-12 13:54:12 +08:00
chenxin 99a7789432 ✨ feat: Sandbox Runtime——isolated 插件子进程隔离执行(WP-06/07 收口)
- 迁移 17:plugin_execution(五态、退出码、时长、stdout 摘要、错误留痕)
- 沙箱执行器:独立子进程(代码经 stdin JSON 安全传输,不进命令行)、CPU rlimit + RLIMIT_AS(平台不支持时尽力而为并注明)、墙钟超时强杀、输出限额;runner 仅暴露 input/output 作用域
- 执行端点:isolated_process 形态、已安装且启用后可执行;**未启用沙箱一律 503 fail-closed**(HUNJING_SANDBOX_ENABLED 显式开启,审计记录拒绝原因)——AGENTS 第 6 节"无符合策略的 Sandbox 时拒绝执行"落地
- 每次执行落 plugin_execution + 审计(状态/时长);进程级隔离的边界如实注明(网络/文件系统最小化在 Linux 容器档交付)
- 测试 96 项全绿(+2):echo 真实执行回填、死循环 5s 超时强杀、异常 failed 含 stderr、无沙箱 503
2026-09-12 13:38:41 +08:00
chenxin 21eacc8c75 ✨ feat: WP-14 运营收口——诊断双授权、管理面治理、支持包、观测与自托管
- 迁移 16:diagnostic_grant(token 哈希唯一、15 分钟、单次、绑定 Run)+ user.role(user/admin 检查约束)
- 诊断双授权(ADR-004):本人签发 grant(明文 token 只返回一次,库存 SHA-256);读取需登录态 + Hunjing-Diagnostic-Token + Purpose 三重匹配;单次消耗(重放 401)、用途不符 403、响应 no-store/private;返回观测摘要(usage/TTFT/工具轨迹)不含消息正文
- 管理面:pending 司议案列表与处置(content_removed 联动下架目标内容——治理闭环);非管理员 403
- 脱敏支持包:统计聚合(runs/messages/tool/media 按状态),无正文无密钥
- 观测:GET /readyz(DB 连通)与 GET /metrics(Prometheus 文本,真实 SQL 聚合)
- 自托管:docker/Dockerfile.backend(uv 构建)+ docker-compose.app.yml(hunjing-api + hunjing-postgres 健康依赖、密码经 .env);Makefile 新增 selfhost-up/down、db-dump/db-restore(备份到 backups/)
- 测试 94 项全绿(+3):诊断全路径(无令牌/用途不符/成功/单次重放)、管理面处置联动、支持包与指标
2026-09-12 11:20:15 +08:00
chenxin 701c20c783 ✨ feat: WP-08 媒体基础与 CI(上传/文件/任务/声契 + 三任务流水线)
- 迁移 15:upload(三态)、file_asset(sha256/存储键/pending-ready-quarantined)、media_job(四类五态+进度+取消)、voice_consent(用途/期限/撤回)
- 上传三步流(契约第 9 节开发档):POST uploads(MIME 白名单+大小约束)→ PUT data(真实写 storage/ 落盘、sha256 校验)→ completions 转 ready——未完成文件不可被业务引用
- 文件内容取回(开发档本地;正式部署换短期签名 URL,存储键不变)
- 媒体任务:ASR 输入必须 ready 音频;后台执行经注入 Provider(测试 mock 确定性转写);无 Provider 显式失败(fail-closed);取消幂等
- 声契:TTS/克隆需有效声契(未撤回未过期);撤回后新任务 403(ADR 媒体域“撤回停止新生成”)
- CI(WP-14 起步):三任务流水线——backend(pgvector 服务容器 + ruff/mypy/pytest)、engine(CMake 警告即错误 + ctest)、openapi(Redocly lint)
- 测试 91 项全绿(+5)
2026-09-12 10:50:37 +08:00
chenxin 9ef2568c3b ✨ feat: WP-07 插件生态——Registry、三态分离与调用门(ADR-001/008)
- 迁移 14:plugin(kind 四形态、撤架三态)、plugin_version(Manifest JSONB + 规范化内容哈希签名、(plugin,version) 唯一)、plugin_installation(enabled + granted_permissions、(domain,plugin) 唯一)
- Registry:发布版本(Manifest 必须声明 permissions,签名=canonical 哈希);撤架阻断新调用与新安装,历史审计保留(验证:撤架后 invoked 审计仍在)
- 三态分离(购买≠安装≠启用≠授权):安装默认 disabled+零授权;启用必须显式授予权限子集(越界 422);调用门逐级校验(未装/未启/撤架各 403)
- 升级:Manifest 新增权限必须重新确认(未确认 422 且明示新增项);确认后升级成功
- 调用:按 Manifest response_template 真实执行(声明式确定性回填);remote/isolated 形态明示 501 随沙箱交付(WP-14),不静默降级;审计含版本哈希(快照语义)
- 权限标识统一 group:item 格式
- 测试 86 项全绿(+4):三态流、越界、升级确认、撤架阻断与历史保留
2026-09-12 10:18:20 +08:00
chenxin 795df0413e ✨ feat: WP-12 协作空间——Workspace、RBAC、邀请与项目评审
- 迁移 13:workspace、workspace_member(四角色 + (workspace,member) 唯一)、workspace_invitation(四态流转)、project、project_version((project,version) 唯一、评审四态、提交/评审人留痕)
- RBAC 三权分立:editor 提交版本、reviewer 批准/拒绝、owner 发布与成员管理——权限矩阵代码化(editor 不可自审、viewer 不可提交、发布前必须有 approved 当前版本)
- 邀请流:owner 邀请(角色限定 editor/reviewer/viewer)→ 被邀者接受入成员;已是成员 409;移除所有者 422
- 评审流:版本号自动递增不可变;批准设为当前版本;拒绝不可逆(重审 409);未批准发布 422
- 隔离:非成员与被移除成员统一 404 fail-closed(require_member 单点)
- 测试 82 项全绿(+3):完整协作流(邀请三角色→提交→批准→发布)、拒绝路径、成员移除失效与外人隔离
2026-09-12 09:33:29 +08:00
chenxin 99985f5855 ✨ feat: WP-11 社区——发布、互动、关注与治理
- 迁移 12:community_content(帖子/角色 Release、publication 六态、可见性)、comment(父级树)、reaction((content,actor,kind) 唯一防重复)、follow(双 FK 显式命名修复 convention 冲突)、report + moderation_case(举报即建司议案)
- 发布:帖子直发;角色 Release 由服务端构造公开快照(name/道体/版本数)——私密记忆/关系/对话结构性缺席,测试以注入私密记忆后发布验证零泄露
- 互动:评论(父级校验同内容可见)、点赞/收藏(重复 409、取消后计数正确)
- 关注/取关与计数;举报自动生成司议案(治理队列入口,status=pending)
- 下架传播:removed 后列表/详情/评论统一 404 或过滤(require_content 单点);非作者下架 404 fail-closed
- 测试基建修复:login 返回自包含 headers(显式 Cookie 优先),多用户交替不再串会话
- 测试 79 项全绿(+7)
2026-09-12 09:02:36 +08:00
chenxin 34c481ab9e ✨ feat: WP-13 市场与交易——不可变账本、退款、Webhook 与保全态(ADR-009/021)
- 迁移 11:listing、order(六态 + 幂等唯一约束)、order_item(价格快照,客户端金额不可信)、ledger_entry(不可变 8 类型带符号分录)、webhook_event(Provider 事件 ID 幂等)、personal_domain.billing_status(保全态)
- 购买闭环:下单冻结价格快照 → 支付确认同事务写三分录(charge = platform_fee 10% + creator_payable,双录平衡校验拒绝不平衡提交)
- 退款:反向三条分录修正账本(Ledger 不删不改,ADR-009);重复退款幂等;汇总端点对账 balanced=true
- Webhook:HMAC-SHA256 验签(密钥经环境变量,错签 401)→ 按 Provider 事件 ID 幂等(重复投递 duplicate 不重复入账)→ 买方从订单反查(不信任 payload)
- 保全态:欠费只读保全(全部状态变更 403 且明示不删数据;解除端点不被保全自锁);读取不受影响,恢复即解锁——ADR-021 落地
- 测试 72 项全绿(+5):双录平衡、订单幂等、退款反向与对账、验签/幂等、保全只读与恢复
2026-09-11 11:37:45 +08:00
138 changed files with 20053 additions and 1448 deletions

No files matched your search

+14 -1
View File
@@ -6,13 +6,26 @@
HUNJING_ENV=development
# 数据库(PostgreSQL 统一,ADR-024;默认连接 docker-compose 启动的 hunjing-postgres)
DATABASE_URL=postgresql+asyncpg://hunjing:hunjing_dev_password@127.0.0.1:5432/hunjing
# 注意:后端 Settings 统一 HUNJING_ 前缀读取,无前缀的 DATABASE_URL 不会生效
HUNJING_DATABASE_URL=postgresql+asyncpg://hunjing:hunjing_dev_password@127.0.0.1:5432/hunjing
# 后端服务端口
HUNJING_API_PORT=8000
# 对象存储根目录(本地存储档;上传与媒体文件落位)
HUNJING_STORAGE_ROOT=storage
# 沙箱隔离开关:未启用时隔离插件执行 fail-closed(拒绝执行,不回退宿主)
HUNJING_SANDBOX_ENABLED=false
# 日志级别:debug / info / warning / error
HUNJING_LOG_LEVEL=info
# 注意:密钥、Provider 凭据等敏感值只通过环境变量或秘密管理注入,
# 不写入本文件、config/ 真实配置或任何日志。
# 开发档管理员邮箱白名单(逗号分隔):命中邮箱首次登录(建户)即为 admin 角色;
# 例:HUNJING_DEV_ADMIN_EMAILS=admin@hunjing.dev
# 开发档 Mock 模型密钥(make mock-model + backend-dev 联调 Run 流水线)
HUNJING_DEV_ADMIN_EMAILS=
HUNJING_DEV_MOCK_KEY=
+71
View File
@@ -0,0 +1,71 @@
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
backend:
runs-on: ubuntu-latest
services:
postgres:
image: pgvector/pgvector:pg17
env:
POSTGRES_USER: hunjing
POSTGRES_PASSWORD: hunjing_dev_password
POSTGRES_DB: hunjing
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U hunjing"
--health-interval 5s --health-timeout 3s --health-retries 12
env:
DATABASE_URL: postgresql+asyncpg://hunjing:hunjing_dev_password@127.0.0.1:5432/hunjing
TEST_DATABASE_URL: postgresql+asyncpg://hunjing:hunjing_dev_password@127.0.0.1:5432/hunjing_test
HUNJING_WEBHOOK_SECRET: whsec_ci
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"
- name: 安装依赖
working-directory: backend
run: uv sync
- name: 创建测试库
run: |
sudo apt-get update && sudo apt-get install -y postgresql-client
PGPASSWORD=hunjing_dev_password psql -h 127.0.0.1 -U hunjing -d hunjing \
-c "CREATE DATABASE hunjing_test"
- name: Lint 与类型检查
working-directory: backend
run: |
uv run ruff check .
uv run mypy app
- name: 测试
working-directory: backend
run: uv run pytest -q
engine:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 安装 CMake
run: pipx install cmake
- name: 构建(警告即错误)
run: |
cmake -S engine -B engine/build -DCMAKE_BUILD_TYPE=Debug
cmake --build engine/build
- name: 测试
run: ctest --test-dir engine/build --output-on-failure
openapi:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: 契约校验
run: |
npx --yes @redocly/cli@latest lint openapi/openapi.yaml \
--config openapi/redocly.yaml
+7 -3
View File
@@ -1,6 +1,6 @@
# 环境变量与密钥
.env
.env*.local
# 环境变量与密钥(忽略一切 .env 变体含副本;仅保留 *.example* 示例文件)
.env*
!*.example*
config/*.yaml
config/mcp/*.yaml
config/gateway/*.yaml
@@ -112,6 +112,10 @@ target/
# C++ 引擎与契约生成产物(源码构建可再生)
engine/build*/
engine/bindings/go/lingtai/*.cpp
engine/bindings/go/lingtai/*.h
engine/bindings/go/lingtai/hunjing/
engine/bindings/rust/target/
engine/bindings/build/
openapi/bundled.json
**/*.rs.bk
+42 -4
View File
@@ -9,7 +9,7 @@ backend-setup: ## 安装后端依赖(uv sync)
.PHONY: backend-dev
backend-dev: ## 启动后端开发服务(127.0.0.1:8000)
cd backend && uv run uvicorn app.main:app --reload --host 127.0.0.1 --port 8000
cd backend && uv run --env-file .env uvicorn app.main:app --reload --host 127.0.0.1 --port 8000
.PHONY: backend-test
backend-test: ## 运行后端测试(pytest)
@@ -19,6 +19,10 @@ backend-test: ## 运行后端测试(pytest)
backend-lint: ## 后端静态检查(ruff + mypy)
cd backend && uv run ruff check . && uv run mypy app
.PHONY: mock-model
mock-model: ## 启动开发档 Mock 模型服务(127.0.0.1:8001,联调 Run 流水线)
cd backend && uv run python scripts/mock_model.py
# ===== 灵台引擎(C++23 / CMake) =====
.PHONY: engine-build
@@ -51,17 +55,51 @@ openapi-lint: ## 校验 OpenAPI 契约(redocly lint + bundle)
.PHONY: generate
generate: openapi-lint ## 生成代码(TypeScript 类型等)
cd frontend && npm run generate:api
cd frontend && pnpm generate:api
# ===== 灵台引擎 gRPC 服务(ADR-017 双形态) =====
.PHONY: lingtai-proto
lingtai-proto: ## 重新生成 gRPC stub(proto 变更后执行)
cd backend && uv run python -m grpc_tools.protoc -I../engine/proto \
--python_out=../engine/server/generated \
--grpc_python_out=../engine/server/generated \
liushen/v1/liushen.proto
.PHONY: lingtai-server
lingtai-server: ## 启动灵台引擎独立服务(开发档 127.0.0.1:50051;生产须 mTLS)
cd backend && uv run python ../engine/server/lingtai_server.py
# ===== 前端与桌面(Next.js / Tauri v2) =====
.PHONY: frontend-setup
frontend-setup: ## 安装前端依赖
cd frontend && npm install
cd frontend && pnpm install
.PHONY: frontend-dev
frontend-dev: ## 启动前端开发服务(127.0.0.1:3000)
cd frontend && npm run dev
cd frontend && pnpm dev
# ===== 自托管与备份(WP-14) =====
.PHONY: selfhost-up
selfhost-up: ## 启动自托管档(API + PostgreSQL,docker compose)
docker compose -f docker/docker-compose.app.yml up -d --build
.PHONY: selfhost-down
selfhost-down: ## 停止自托管档
docker compose -f docker/docker-compose.app.yml down
.PHONY: db-dump
db-dump: ## 备份数据库到 backups/hunjing-<时间戳>.sql.gz
@mkdir -p backups
@docker exec hunjing-postgres pg_dump -U hunjing -d hunjing | gzip > backups/hunjing-$$(date +%Y%m%d-%H%M%S).sql.gz
@echo "备份完成:backups/"
.PHONY: db-restore
db-restore: ## 从最新备份恢复(DB=备份文件路径,make db-restore FILE=backups/xxx.sql.gz)
@gzip -dc $(FILE) | docker exec -i hunjing-postgres psql -U hunjing -d hunjing
@echo "恢复完成"
# ===== 帮助 =====
@@ -0,0 +1,47 @@
"""插件沙箱执行记录
Revision ID: 4e679f11da46
Revises: 89a4ed9647ee
Create Date: 2026-09-12 13:33:43.279920
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '4e679f11da46'
down_revision: str | Sequence[str] | None = '89a4ed9647ee'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('plugin_execution',
sa.Column('id', sa.String(), nullable=False),
sa.Column('installation_id', sa.String(), nullable=False),
sa.Column('status', sa.String(), nullable=False),
sa.Column('exit_code', sa.Integer(), nullable=True),
sa.Column('duration_ms', sa.Integer(), nullable=True),
sa.Column('stdout_excerpt', sa.String(), nullable=True),
sa.Column('error', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
sa.CheckConstraint("status IN ('running', 'succeeded', 'failed', 'timeout', 'killed')", name=op.f('ck_plugin_execution_status_values')),
sa.ForeignKeyConstraint(['installation_id'], ['plugin_installation.id'], name=op.f('fk_plugin_execution_plugin_installation'), ondelete='cascade'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_plugin_execution'))
)
op.create_index(op.f('ix_plugin_execution_installation_id'), 'plugin_execution', ['installation_id'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_plugin_execution_installation_id'), table_name='plugin_execution')
op.drop_table('plugin_execution')
# ### end Alembic commands ###
@@ -0,0 +1,99 @@
"""媒体:上传、文件、任务与声契
Revision ID: 52be875c14a4
Revises: f7bf2b464f1f
Create Date: 2026-09-12 10:47:53.573208
"""
from collections.abc import Sequence
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '52be875c14a4'
down_revision: str | Sequence[str] | None = 'f7bf2b464f1f'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('media_job',
sa.Column('id', sa.String(), nullable=False),
sa.Column('domain_id', sa.String(), nullable=False),
sa.Column('kind', sa.String(), nullable=False),
sa.Column('input', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('output', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('status', sa.String(), nullable=False),
sa.Column('progress', sa.Integer(), nullable=False),
sa.Column('error', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
sa.CheckConstraint("kind IN ('asr', 'tts', 'image_gen', 'image_edit')", name=op.f('ck_media_job_kind_values')),
sa.CheckConstraint("status IN ('queued', 'running', 'succeeded', 'failed', 'cancelled')", name=op.f('ck_media_job_status_values')),
sa.ForeignKeyConstraint(['domain_id'], ['personal_domain.id'], name=op.f('fk_media_job_personal_domain'), ondelete='cascade'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_media_job'))
)
op.create_index(op.f('ix_media_job_domain_id'), 'media_job', ['domain_id'], unique=False)
op.create_table('upload',
sa.Column('id', sa.String(), nullable=False),
sa.Column('domain_id', sa.String(), nullable=False),
sa.Column('filename', sa.String(), nullable=False),
sa.Column('media_type', sa.String(), nullable=False),
sa.Column('size_limit', sa.Integer(), nullable=False),
sa.Column('status', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint("status IN ('pending', 'completed', 'failed')", name=op.f('ck_upload_status_values')),
sa.ForeignKeyConstraint(['domain_id'], ['personal_domain.id'], name=op.f('fk_upload_personal_domain'), ondelete='cascade'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_upload'))
)
op.create_index(op.f('ix_upload_domain_id'), 'upload', ['domain_id'], unique=False)
op.create_table('file_asset',
sa.Column('id', sa.String(), nullable=False),
sa.Column('domain_id', sa.String(), nullable=False),
sa.Column('upload_id', sa.String(), nullable=True),
sa.Column('filename', sa.String(), nullable=False),
sa.Column('media_type', sa.String(), nullable=False),
sa.Column('size_bytes', sa.Integer(), nullable=False),
sa.Column('sha256', sa.String(), nullable=True),
sa.Column('storage_key', sa.String(), nullable=False),
sa.Column('status', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint("status IN ('pending', 'ready', 'failed', 'quarantined')", name=op.f('ck_file_asset_status_values')),
sa.ForeignKeyConstraint(['domain_id'], ['personal_domain.id'], name=op.f('fk_file_asset_personal_domain'), ondelete='cascade'),
sa.ForeignKeyConstraint(['upload_id'], ['upload.id'], name=op.f('fk_file_asset_upload'), ondelete='set null'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_file_asset'))
)
op.create_index(op.f('ix_file_asset_domain_id'), 'file_asset', ['domain_id'], unique=False)
op.create_table('voice_consent',
sa.Column('id', sa.String(), nullable=False),
sa.Column('domain_id', sa.String(), nullable=False),
sa.Column('character_id', sa.String(), nullable=True),
sa.Column('purpose', sa.String(), nullable=False),
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('revoked_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['character_id'], ['character.id'], name=op.f('fk_voice_consent_character'), ondelete='cascade'),
sa.ForeignKeyConstraint(['domain_id'], ['personal_domain.id'], name=op.f('fk_voice_consent_personal_domain'), ondelete='cascade'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_voice_consent'))
)
op.create_index(op.f('ix_voice_consent_domain_id'), 'voice_consent', ['domain_id'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_voice_consent_domain_id'), table_name='voice_consent')
op.drop_table('voice_consent')
op.drop_index(op.f('ix_file_asset_domain_id'), table_name='file_asset')
op.drop_table('file_asset')
op.drop_index(op.f('ix_upload_domain_id'), table_name='upload')
op.drop_table('upload')
op.drop_index(op.f('ix_media_job_domain_id'), table_name='media_job')
op.drop_table('media_job')
# ### end Alembic commands ###
@@ -0,0 +1,49 @@
"""诊断授权与管理员角色
Revision ID: 89a4ed9647ee
Revises: 52be875c14a4
Create Date: 2026-09-12 11:14:22.131144
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '89a4ed9647ee'
down_revision: str | Sequence[str] | None = '52be875c14a4'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('diagnostic_grant',
sa.Column('id', sa.String(), nullable=False),
sa.Column('run_id', sa.String(), nullable=False),
sa.Column('domain_id', sa.String(), nullable=False),
sa.Column('purpose', sa.String(), nullable=False),
sa.Column('token_hash', sa.String(), nullable=False),
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('used_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['domain_id'], ['personal_domain.id'], name=op.f('fk_diagnostic_grant_personal_domain'), ondelete='cascade'),
sa.ForeignKeyConstraint(['run_id'], ['run.id'], name=op.f('fk_diagnostic_grant_run'), ondelete='cascade'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_diagnostic_grant')),
sa.UniqueConstraint('token_hash', name=op.f('uq_diagnostic_grant_token_hash'))
)
op.create_index(op.f('ix_diagnostic_grant_run_id'), 'diagnostic_grant', ['run_id'], unique=False)
op.add_column('user', sa.Column('role', sa.String(), nullable=False, server_default='user'))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user', 'role')
op.drop_index(op.f('ix_diagnostic_grant_run_id'), table_name='diagnostic_grant')
op.drop_table('diagnostic_grant')
# ### end Alembic commands ###
@@ -0,0 +1,107 @@
"""市场交易:商品、订单、账本与 Webhook
Revision ID: b7f1a0032451
Revises: b3a26cb61923
Create Date: 2026-09-11 11:31:22.345265
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'b7f1a0032451'
down_revision: str | Sequence[str] | None = 'b3a26cb61923'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('webhook_event',
sa.Column('provider_event_id', sa.String(), nullable=False),
sa.Column('provider', sa.String(), nullable=False),
sa.Column('event_type', sa.String(), nullable=False),
sa.Column('payload_hash', sa.String(), nullable=False),
sa.Column('status', sa.String(), nullable=False),
sa.Column('received_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint("status IN ('processed', 'ignored')", name=op.f('ck_webhook_event_status_values')),
sa.PrimaryKeyConstraint('provider_event_id', name=op.f('pk_webhook_event'))
)
op.create_table('listing',
sa.Column('id', sa.String(), nullable=False),
sa.Column('creator_domain_id', sa.String(), nullable=False),
sa.Column('title', sa.String(), nullable=False),
sa.Column('price_minor', sa.Integer(), nullable=False),
sa.Column('currency', sa.String(), nullable=False),
sa.Column('status', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint('price_minor >= 0', name=op.f('ck_listing_price_non_negative')),
sa.ForeignKeyConstraint(['creator_domain_id'], ['personal_domain.id'], name=op.f('fk_listing_personal_domain'), ondelete='cascade'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_listing'))
)
op.create_index(op.f('ix_listing_creator_domain_id'), 'listing', ['creator_domain_id'], unique=False)
op.create_table('order',
sa.Column('id', sa.String(), nullable=False),
sa.Column('buyer_domain_id', sa.String(), nullable=False),
sa.Column('status', sa.String(), nullable=False),
sa.Column('total_minor', sa.Integer(), nullable=False),
sa.Column('currency', sa.String(), nullable=False),
sa.Column('idempotency_key', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('paid_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('refunded_at', sa.DateTime(timezone=True), nullable=True),
sa.CheckConstraint("status IN ('pending_payment', 'paid', 'partially_refunded', 'refunded', 'disputed', 'cancelled')", name=op.f('ck_order_status_values')),
sa.ForeignKeyConstraint(['buyer_domain_id'], ['personal_domain.id'], name=op.f('fk_order_personal_domain'), ondelete='cascade'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_order'))
)
op.create_index(op.f('ix_order_buyer_domain_id'), 'order', ['buyer_domain_id'], unique=False)
op.create_index('uq_order_buyer_idempotency', 'order', ['buyer_domain_id', 'idempotency_key'], unique=True)
op.create_table('ledger_entry',
sa.Column('id', sa.String(), nullable=False),
sa.Column('order_id', sa.String(), nullable=False),
sa.Column('entry_type', sa.String(), nullable=False),
sa.Column('amount_minor', sa.Integer(), nullable=False),
sa.Column('currency', sa.String(), nullable=False),
sa.Column('note', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint("entry_type IN ('charge', 'refund', 'platform_fee', 'creator_payable', 'tax', 'reserve', 'payout', 'reversal')", name=op.f('ck_ledger_entry_entry_type_values')),
sa.ForeignKeyConstraint(['order_id'], ['order.id'], name=op.f('fk_ledger_entry_order'), ondelete='restrict'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_ledger_entry'))
)
op.create_index(op.f('ix_ledger_entry_order_id'), 'ledger_entry', ['order_id'], unique=False)
op.create_index('ix_ledger_order_type', 'ledger_entry', ['order_id', 'entry_type'], unique=False)
op.create_table('order_item',
sa.Column('id', sa.String(), nullable=False),
sa.Column('order_id', sa.String(), nullable=False),
sa.Column('listing_id', sa.String(), nullable=False),
sa.Column('title_snapshot', sa.String(), nullable=False),
sa.Column('price_minor_snapshot', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['listing_id'], ['listing.id'], name=op.f('fk_order_item_listing'), ondelete='restrict'),
sa.ForeignKeyConstraint(['order_id'], ['order.id'], name=op.f('fk_order_item_order'), ondelete='cascade'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_order_item'))
)
op.create_index(op.f('ix_order_item_order_id'), 'order_item', ['order_id'], unique=False)
op.add_column('personal_domain', sa.Column('billing_status', sa.String(), nullable=False, server_default='ok'))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('personal_domain', 'billing_status')
op.drop_index(op.f('ix_order_item_order_id'), table_name='order_item')
op.drop_table('order_item')
op.drop_index('ix_ledger_order_type', table_name='ledger_entry')
op.drop_index(op.f('ix_ledger_entry_order_id'), table_name='ledger_entry')
op.drop_table('ledger_entry')
op.drop_index('uq_order_buyer_idempotency', table_name='order')
op.drop_index(op.f('ix_order_buyer_domain_id'), table_name='order')
op.drop_table('order')
op.drop_index(op.f('ix_listing_creator_domain_id'), table_name='listing')
op.drop_table('listing')
op.drop_table('webhook_event')
# ### end Alembic commands ###
@@ -0,0 +1,207 @@
"""协作空间:成员、邀请、项目与评审
Revision ID: bd36c0a63816
Revises: b7f1a0032451
Create Date: 2026-09-12 09:31:43.277430
"""
from collections.abc import Sequence
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'bd36c0a63816'
down_revision: str | Sequence[str] | None = 'b7f1a0032451'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('follow',
sa.Column('id', sa.String(), nullable=False),
sa.Column('follower_domain_id', sa.String(), nullable=False),
sa.Column('followee_domain_id', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['followee_domain_id'], ['personal_domain.id'], name='fk_follow_followee_domain', ondelete='cascade'),
sa.ForeignKeyConstraint(['follower_domain_id'], ['personal_domain.id'], name='fk_follow_follower_domain', ondelete='cascade'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_follow'))
)
op.create_index(op.f('ix_follow_followee_domain_id'), 'follow', ['followee_domain_id'], unique=False)
op.create_index(op.f('ix_follow_follower_domain_id'), 'follow', ['follower_domain_id'], unique=False)
op.create_index('uq_follow_pair', 'follow', ['follower_domain_id', 'followee_domain_id'], unique=True)
op.create_table('report',
sa.Column('id', sa.String(), nullable=False),
sa.Column('reporter_domain_id', sa.String(), nullable=False),
sa.Column('target_type', sa.String(), nullable=False),
sa.Column('target_id', sa.String(), nullable=False),
sa.Column('reason', sa.String(), nullable=False),
sa.Column('status', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint("status IN ('pending', 'reviewed', 'actioned')", name=op.f('ck_report_status_values')),
sa.CheckConstraint("target_type IN ('content', 'comment')", name=op.f('ck_report_target_type_values')),
sa.ForeignKeyConstraint(['reporter_domain_id'], ['personal_domain.id'], name=op.f('fk_report_personal_domain'), ondelete='cascade'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_report'))
)
op.create_index(op.f('ix_report_reporter_domain_id'), 'report', ['reporter_domain_id'], unique=False)
op.create_table('workspace',
sa.Column('id', sa.String(), nullable=False),
sa.Column('name', sa.String(), nullable=False),
sa.Column('owner_domain_id', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['owner_domain_id'], ['personal_domain.id'], name=op.f('fk_workspace_personal_domain'), ondelete='cascade'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_workspace'))
)
op.create_index(op.f('ix_workspace_owner_domain_id'), 'workspace', ['owner_domain_id'], unique=False)
op.create_table('community_content',
sa.Column('id', sa.String(), nullable=False),
sa.Column('author_domain_id', sa.String(), nullable=False),
sa.Column('kind', sa.String(), nullable=False),
sa.Column('title', sa.String(), nullable=False),
sa.Column('body', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('character_id', sa.String(), nullable=True),
sa.Column('visibility', sa.String(), nullable=False),
sa.Column('status', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint("kind IN ('post', 'character_release')", name=op.f('ck_community_content_kind_values')),
sa.CheckConstraint("status IN ('draft', 'submitted', 'published', 'limited', 'suspended', 'removed')", name=op.f('ck_community_content_status_values')),
sa.CheckConstraint("visibility IN ('public', 'followers')", name=op.f('ck_community_content_visibility_values')),
sa.ForeignKeyConstraint(['author_domain_id'], ['personal_domain.id'], name=op.f('fk_community_content_personal_domain'), ondelete='cascade'),
sa.ForeignKeyConstraint(['character_id'], ['character.id'], name=op.f('fk_community_content_character'), ondelete='set null'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_community_content'))
)
op.create_index(op.f('ix_community_content_author_domain_id'), 'community_content', ['author_domain_id'], unique=False)
op.create_table('moderation_case',
sa.Column('id', sa.String(), nullable=False),
sa.Column('report_id', sa.String(), nullable=False),
sa.Column('status', sa.String(), nullable=False),
sa.Column('action_taken', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint("status IN ('pending', 'resolved')", name=op.f('ck_moderation_case_status_values')),
sa.ForeignKeyConstraint(['report_id'], ['report.id'], name=op.f('fk_moderation_case_report'), ondelete='cascade'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_moderation_case'))
)
op.create_index(op.f('ix_moderation_case_report_id'), 'moderation_case', ['report_id'], unique=False)
op.create_table('project',
sa.Column('id', sa.String(), nullable=False),
sa.Column('workspace_id', sa.String(), nullable=False),
sa.Column('name', sa.String(), nullable=False),
sa.Column('kind', sa.String(), nullable=False),
sa.Column('status', sa.String(), nullable=False),
sa.Column('current_version_id', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint("status IN ('draft', 'published')", name=op.f('ck_project_status_values')),
sa.ForeignKeyConstraint(['current_version_id'], ['project_version.id'], name=op.f('fk_project_project_version'), ondelete='set null', use_alter=True),
sa.ForeignKeyConstraint(['workspace_id'], ['workspace.id'], name=op.f('fk_project_workspace'), ondelete='cascade'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_project'))
)
op.create_index(op.f('ix_project_workspace_id'), 'project', ['workspace_id'], unique=False)
op.create_table('workspace_invitation',
sa.Column('id', sa.String(), nullable=False),
sa.Column('workspace_id', sa.String(), nullable=False),
sa.Column('invitee_domain_id', sa.String(), nullable=False),
sa.Column('role', sa.String(), nullable=False),
sa.Column('status', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint("role IN ('editor', 'reviewer', 'viewer')", name=op.f('ck_workspace_invitation_role_values')),
sa.CheckConstraint("status IN ('pending', 'accepted', 'declined', 'expired')", name=op.f('ck_workspace_invitation_status_values')),
sa.ForeignKeyConstraint(['invitee_domain_id'], ['personal_domain.id'], name=op.f('fk_workspace_invitation_personal_domain'), ondelete='cascade'),
sa.ForeignKeyConstraint(['workspace_id'], ['workspace.id'], name=op.f('fk_workspace_invitation_workspace'), ondelete='cascade'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_workspace_invitation'))
)
op.create_index(op.f('ix_workspace_invitation_workspace_id'), 'workspace_invitation', ['workspace_id'], unique=False)
op.create_table('workspace_member',
sa.Column('id', sa.String(), nullable=False),
sa.Column('workspace_id', sa.String(), nullable=False),
sa.Column('member_domain_id', sa.String(), nullable=False),
sa.Column('role', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint("role IN ('owner', 'editor', 'reviewer', 'viewer')", name=op.f('ck_workspace_member_role_values')),
sa.ForeignKeyConstraint(['member_domain_id'], ['personal_domain.id'], name=op.f('fk_workspace_member_personal_domain'), ondelete='cascade'),
sa.ForeignKeyConstraint(['workspace_id'], ['workspace.id'], name=op.f('fk_workspace_member_workspace'), ondelete='cascade'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_workspace_member'))
)
op.create_index(op.f('ix_workspace_member_workspace_id'), 'workspace_member', ['workspace_id'], unique=False)
op.create_index('uq_workspace_member', 'workspace_member', ['workspace_id', 'member_domain_id'], unique=True)
op.create_table('comment',
sa.Column('id', sa.String(), nullable=False),
sa.Column('content_id', sa.String(), nullable=False),
sa.Column('author_domain_id', sa.String(), nullable=False),
sa.Column('parent_comment_id', sa.String(), nullable=True),
sa.Column('body', sa.String(), nullable=False),
sa.Column('status', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint("status IN ('visible', 'removed')", name=op.f('ck_comment_status_values')),
sa.ForeignKeyConstraint(['author_domain_id'], ['personal_domain.id'], name=op.f('fk_comment_personal_domain'), ondelete='cascade'),
sa.ForeignKeyConstraint(['content_id'], ['community_content.id'], name=op.f('fk_comment_community_content'), ondelete='cascade'),
sa.ForeignKeyConstraint(['parent_comment_id'], ['comment.id'], name=op.f('fk_comment_comment'), ondelete='cascade'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_comment'))
)
op.create_index(op.f('ix_comment_content_id'), 'comment', ['content_id'], unique=False)
op.create_table('project_version',
sa.Column('id', sa.String(), nullable=False),
sa.Column('project_id', sa.String(), nullable=False),
sa.Column('version', sa.Integer(), nullable=False),
sa.Column('payload', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('status', sa.String(), nullable=False),
sa.Column('submitted_by', sa.String(), nullable=True),
sa.Column('reviewed_by', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint("status IN ('draft', 'submitted', 'approved', 'rejected')", name=op.f('ck_project_version_status_values')),
sa.ForeignKeyConstraint(['project_id'], ['project.id'], name=op.f('fk_project_version_project'), ondelete='cascade'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_project_version'))
)
op.create_index(op.f('ix_project_version_project_id'), 'project_version', ['project_id'], unique=False)
op.create_index('uq_project_version_number', 'project_version', ['project_id', 'version'], unique=True)
op.create_table('reaction',
sa.Column('id', sa.String(), nullable=False),
sa.Column('content_id', sa.String(), nullable=False),
sa.Column('actor_domain_id', sa.String(), nullable=False),
sa.Column('kind', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint("kind IN ('like', 'save')", name=op.f('ck_reaction_kind_values')),
sa.ForeignKeyConstraint(['actor_domain_id'], ['personal_domain.id'], name=op.f('fk_reaction_personal_domain'), ondelete='cascade'),
sa.ForeignKeyConstraint(['content_id'], ['community_content.id'], name=op.f('fk_reaction_community_content'), ondelete='cascade'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_reaction'))
)
op.create_index(op.f('ix_reaction_content_id'), 'reaction', ['content_id'], unique=False)
op.create_index('uq_reaction_content_actor_kind', 'reaction', ['content_id', 'actor_domain_id', 'kind'], unique=True)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('uq_reaction_content_actor_kind', table_name='reaction')
op.drop_index(op.f('ix_reaction_content_id'), table_name='reaction')
op.drop_table('reaction')
op.drop_index('uq_project_version_number', table_name='project_version')
op.drop_index(op.f('ix_project_version_project_id'), table_name='project_version')
op.drop_table('project_version')
op.drop_index(op.f('ix_comment_content_id'), table_name='comment')
op.drop_table('comment')
op.drop_index('uq_workspace_member', table_name='workspace_member')
op.drop_index(op.f('ix_workspace_member_workspace_id'), table_name='workspace_member')
op.drop_table('workspace_member')
op.drop_index(op.f('ix_workspace_invitation_workspace_id'), table_name='workspace_invitation')
op.drop_table('workspace_invitation')
op.drop_index(op.f('ix_project_workspace_id'), table_name='project')
op.drop_table('project')
op.drop_index(op.f('ix_moderation_case_report_id'), table_name='moderation_case')
op.drop_table('moderation_case')
op.drop_index(op.f('ix_community_content_author_domain_id'), table_name='community_content')
op.drop_table('community_content')
op.drop_index(op.f('ix_workspace_owner_domain_id'), table_name='workspace')
op.drop_table('workspace')
op.drop_index(op.f('ix_report_reporter_domain_id'), table_name='report')
op.drop_table('report')
op.drop_index('uq_follow_pair', table_name='follow')
op.drop_index(op.f('ix_follow_follower_domain_id'), table_name='follow')
op.drop_index(op.f('ix_follow_followee_domain_id'), table_name='follow')
op.drop_table('follow')
# ### end Alembic commands ###
@@ -0,0 +1,72 @@
"""Avatar 模型与非破坏命令历史
Revision ID: e66d791926f2
Revises: 4e679f11da46
Create Date: 2026-09-14 10:24:23.502856
"""
from collections.abc import Sequence
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'e66d791926f2'
down_revision: str | Sequence[str] | None = '4e679f11da46'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('avatar_model',
sa.Column('id', sa.String(), nullable=False),
sa.Column('domain_id', sa.String(), nullable=False),
sa.Column('character_id', sa.String(), nullable=True),
sa.Column('name', sa.String(), nullable=False),
sa.Column('format', sa.String(), nullable=False),
sa.Column('file_id', sa.String(), nullable=False),
sa.Column('status', sa.String(), nullable=False),
sa.Column('validation_report', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('source_info', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('license_terms', sa.String(), nullable=True),
sa.Column('current_command_id', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint("format IN ('live2d', 'vrm', 'gltf')", name=op.f('ck_avatar_model_format_values')),
sa.CheckConstraint("status IN ('pending', 'validated', 'rejected')", name=op.f('ck_avatar_model_status_values')),
sa.ForeignKeyConstraint(['character_id'], ['character.id'], name=op.f('fk_avatar_model_character'), ondelete='set null'),
sa.ForeignKeyConstraint(['current_command_id'], ['avatar_command.id'], name=op.f('fk_avatar_model_avatar_command'), ondelete='set null', use_alter=True),
sa.ForeignKeyConstraint(['domain_id'], ['personal_domain.id'], name=op.f('fk_avatar_model_personal_domain'), ondelete='cascade'),
sa.ForeignKeyConstraint(['file_id'], ['file_asset.id'], name=op.f('fk_avatar_model_file_asset'), ondelete='restrict'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_avatar_model'))
)
op.create_index(op.f('ix_avatar_model_domain_id'), 'avatar_model', ['domain_id'], unique=False)
op.create_table('avatar_command',
sa.Column('id', sa.String(), nullable=False),
sa.Column('avatar_id', sa.String(), nullable=False),
sa.Column('sequence', sa.Integer(), nullable=False),
sa.Column('command_type', sa.String(), nullable=False),
sa.Column('payload', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('undone', sa.Boolean(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint("command_type IN ('param_map', 'expression', 'physics', 'motion', 'material', 'texture', 'bone', 'attachment', 'lighting', 'camera')", name=op.f('ck_avatar_command_command_type_values')),
sa.ForeignKeyConstraint(['avatar_id'], ['avatar_model.id'], name=op.f('fk_avatar_command_avatar_model'), ondelete='cascade'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_avatar_command'))
)
op.create_index(op.f('ix_avatar_command_avatar_id'), 'avatar_command', ['avatar_id'], unique=False)
op.create_index('uq_avatar_command_seq', 'avatar_command', ['avatar_id', 'sequence'], unique=True)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('uq_avatar_command_seq', table_name='avatar_command')
op.drop_index(op.f('ix_avatar_command_avatar_id'), table_name='avatar_command')
op.drop_table('avatar_command')
op.drop_index(op.f('ix_avatar_model_domain_id'), table_name='avatar_model')
op.drop_table('avatar_model')
# ### end Alembic commands ###
@@ -0,0 +1,82 @@
"""插件生态:Registry、版本与安装
Revision ID: f7bf2b464f1f
Revises: bd36c0a63816
Create Date: 2026-09-12 10:15:58.892690
"""
from collections.abc import Sequence
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'f7bf2b464f1f'
down_revision: str | Sequence[str] | None = 'bd36c0a63816'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('plugin',
sa.Column('name', sa.String(), nullable=False),
sa.Column('publisher_domain_id', sa.String(), nullable=False),
sa.Column('kind', sa.String(), nullable=False),
sa.Column('status', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint("kind IN ('declarative', 'remote', 'isolated_process', 'container')", name=op.f('ck_plugin_kind_values')),
sa.CheckConstraint("status IN ('published', 'suspended', 'removed')", name=op.f('ck_plugin_status_values')),
sa.ForeignKeyConstraint(['publisher_domain_id'], ['personal_domain.id'], name=op.f('fk_plugin_personal_domain'), ondelete='cascade'),
sa.PrimaryKeyConstraint('name', name=op.f('pk_plugin'))
)
op.create_index(op.f('ix_plugin_publisher_domain_id'), 'plugin', ['publisher_domain_id'], unique=False)
op.create_table('plugin_version',
sa.Column('id', sa.String(), nullable=False),
sa.Column('plugin_name', sa.String(), nullable=False),
sa.Column('version', sa.String(), nullable=False),
sa.Column('manifest', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('content_hash', sa.String(), nullable=False),
sa.Column('status', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint("status IN ('published', 'suspended', 'removed')", name=op.f('ck_plugin_version_status_values')),
sa.ForeignKeyConstraint(['plugin_name'], ['plugin.name'], name=op.f('fk_plugin_version_plugin'), ondelete='cascade'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_plugin_version'))
)
op.create_index(op.f('ix_plugin_version_plugin_name'), 'plugin_version', ['plugin_name'], unique=False)
op.create_index('uq_plugin_version', 'plugin_version', ['plugin_name', 'version'], unique=True)
op.create_table('plugin_installation',
sa.Column('id', sa.String(), nullable=False),
sa.Column('domain_id', sa.String(), nullable=False),
sa.Column('plugin_name', sa.String(), nullable=False),
sa.Column('version_id', sa.String(), nullable=False),
sa.Column('enabled', sa.Boolean(), nullable=False),
sa.Column('granted_permissions', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['domain_id'], ['personal_domain.id'], name=op.f('fk_plugin_installation_personal_domain'), ondelete='cascade'),
sa.ForeignKeyConstraint(['plugin_name'], ['plugin.name'], name=op.f('fk_plugin_installation_plugin'), ondelete='cascade'),
sa.ForeignKeyConstraint(['version_id'], ['plugin_version.id'], name=op.f('fk_plugin_installation_plugin_version'), ondelete='restrict'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_plugin_installation'))
)
op.create_index(op.f('ix_plugin_installation_domain_id'), 'plugin_installation', ['domain_id'], unique=False)
op.create_index('uq_plugin_installation', 'plugin_installation', ['domain_id', 'plugin_name'], unique=True)
op.create_foreign_key(op.f('fk_project_project_version'), 'project', 'project_version', ['current_version_id'], ['id'], ondelete='set null', use_alter=True)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(op.f('fk_project_project_version'), 'project', type_='foreignkey')
op.drop_index('uq_plugin_installation', table_name='plugin_installation')
op.drop_index(op.f('ix_plugin_installation_domain_id'), table_name='plugin_installation')
op.drop_table('plugin_installation')
op.drop_index('uq_plugin_version', table_name='plugin_version')
op.drop_index(op.f('ix_plugin_version_plugin_name'), table_name='plugin_version')
op.drop_table('plugin_version')
op.drop_index(op.f('ix_plugin_publisher_domain_id'), table_name='plugin')
op.drop_table('plugin')
# ### end Alembic commands ###
+18 -2
View File
@@ -6,7 +6,7 @@ from fastapi import Depends, Header, HTTPException, Request, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth.service import SESSION_COOKIE, resolve_session
from app.db.session import get_db
from app.db.session import DbSession, get_db
from app.models import User
ALLOWED_ORIGINS: set[str] = set() # 由配置注入;空集表示本地开发放行同源
@@ -33,10 +33,11 @@ async def get_current_user(
async def require_state_change(
request: Request,
db: DbSession,
user: Annotated[User, Depends(get_current_user)],
hunjing_csrf_token: Annotated[str | None, Header(alias="Hunjing-CSRF-Token")] = None,
) -> User:
"""Cookie 状态变更:Origin 校验 + CSRF 双提交(契约 2.1 节)。"""
"""Cookie 状态变更:Origin 校验 + CSRF 双提交 + 保全态拦截(契约 2.1 / ADR-021)。"""
if ALLOWED_ORIGINS:
origin = request.headers.get("Origin")
if origin is not None and origin not in ALLOWED_ORIGINS:
@@ -44,6 +45,21 @@ async def require_state_change(
csrf_hash = getattr(request.state, "session_csrf_hash", None)
if csrf_hash is None or not _csrf_matches(csrf_hash, hunjing_csrf_token):
raise HTTPException(status.HTTP_403_FORBIDDEN, "CSRF 校验失败")
# 保全态:欠费只读保全,状态变更拒绝(数据不删、恢复即解锁)
from sqlalchemy import select
from app.models import PersonalDomain
domain = (
await db.execute(
select(PersonalDomain).where(PersonalDomain.user_id == user.id).limit(1)
)
).scalar_one_or_none()
if domain is not None and domain.billing_status == "preserved":
raise HTTPException(
status.HTTP_403_FORBIDDEN,
"账户处于保全态(只读):恢复付费后自动解锁,数据不会删除",
)
return user
+14 -2
View File
@@ -7,6 +7,7 @@
from fastapi import APIRouter, HTTPException, Request, Response, status
from app.core.config import get_settings
from app.auth.service import (
SESSION_COOKIE,
create_session,
@@ -17,6 +18,8 @@ from app.models import PersonalDomain, User
router = APIRouter(prefix="/v1/auth", tags=["auth"])
settings = get_settings()
@router.post("/sessions", status_code=201)
async def dev_login(
@@ -32,11 +35,20 @@ async def dev_login(
(await db.execute(select(User).where(User.email == email))).scalar_one_or_none()
)
if existing is None:
user = User(email=email, display_name=email.split("@")[0])
admin_emails = {e.strip().lower() for e in settings.dev_admin_emails.split(",") if e.strip()}
user = User(
email=email,
display_name=email.split("@")[0],
role="admin" if email in admin_emails else "user",
)
db.add(user)
await db.flush()
domain = PersonalDomain(user_id=user.id, name="默认镜域")
db.add(domain)
await db.flush()
from app.characters.seeds import seed_default_character
await seed_default_character(db, domain.id)
else:
user = existing
_, token, csrf = await create_session(db, user.id)
@@ -49,7 +61,7 @@ async def dev_login(
samesite="lax",
path="/",
)
return {"csrf_token": csrf, "user_id": user.id}
return {"csrf_token": csrf, "user_id": user.id, "role": user.role, "email": user.email}
@router.delete("/sessions/current", status_code=204)
+1
View File
@@ -0,0 +1 @@
"""Avatar 域:Live2D/VRM/glTF 校验与非破坏编辑。"""
+119
View File
@@ -0,0 +1,119 @@
"""Avatar 域路由(WP-09)。"""
from typing import Annotated, Any
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy import select
from app.auth.dependencies import get_current_user, require_state_change
from app.avatars import service
from app.conversations.routes import _domain_of
from app.db.session import DbSession
from app.models import AvatarModel, User
router = APIRouter(prefix="/v1/avatars", tags=["avatars"])
def _model_out(m: AvatarModel) -> dict[str, Any]:
return {
"id": m.id, "name": m.name, "format": m.format, "status": m.status,
"file_id": m.file_id, "validation_report": m.validation_report,
"character_id": m.character_id, "current_command_id": m.current_command_id,
"created_at": m.created_at.isoformat(),
}
def _command_out(c: Any) -> dict[str, Any]:
return {
"id": c.id, "avatar_id": c.avatar_id, "sequence": c.sequence,
"command_type": c.command_type, "payload": c.payload, "undone": c.undone,
"created_at": c.created_at.isoformat(),
}
@router.post("/imports", status_code=201)
async def import_avatar(
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, str],
) -> dict[str, Any]:
domain = await _domain_of(db, user)
model = await service.import_avatar(
db, domain.id, body.get("name", ""), body.get("format", ""),
body.get("file_id", ""), getattr(request.state, "trace_id", None),
)
return _model_out(model)
@router.get("")
async def list_avatars(
db: DbSession,
user: Annotated[User, Depends(get_current_user)],
) -> dict[str, Any]:
domain = await _domain_of(db, user)
rows = list(
(
await db.scalars(
select(AvatarModel)
.where(AvatarModel.domain_id == domain.id)
.order_by(AvatarModel.created_at.asc())
)
).all()
)
return {"data": [_model_out(m) for m in rows]}
@router.post("/{avatar_id}/commands", status_code=201)
async def append_command(
avatar_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, Any],
) -> dict[str, Any]:
domain = await _domain_of(db, user)
command = await service.append_command(
db, domain.id, avatar_id, body.get("command_type", ""),
body.get("payload", {}), getattr(request.state, "trace_id", None),
)
return _command_out(command)
@router.post("/{avatar_id}/commands/{command_id}/undoings")
async def undo_command(
avatar_id: str,
command_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
) -> dict[str, Any]:
domain = await _domain_of(db, user)
command = await service.undo_command(
db, domain.id, avatar_id, command_id, getattr(request.state, "trace_id", None)
)
return _command_out(command)
@router.get("/{avatar_id}/commands")
async def list_commands(
avatar_id: str,
db: DbSession,
user: Annotated[User, Depends(get_current_user)],
) -> dict[str, Any]:
from app.models import AvatarCommand
domain = await _domain_of(db, user)
model = await db.get(AvatarModel, avatar_id)
if model is None or model.domain_id != domain.id:
raise HTTPException(status.HTTP_404_NOT_FOUND, "模型不存在")
rows = list(
(
await db.scalars(
select(AvatarCommand)
.where(AvatarCommand.avatar_id == avatar_id)
.order_by(AvatarCommand.sequence.asc())
)
).all()
)
return {"data": [_command_out(c) for c in rows]}
+357
View File
@@ -0,0 +1,357 @@
"""Avatar 域:Live2D/VRM/glTF 导入校验与非破坏命令历史(WP-09)。
校验器纯 Python 实现,输入为文件字节;全部按不可信输入处理
(恶意模型文件防线:magic/结构/引用/资源计数硬上限)。
"""
import json
import struct
from dataclasses import dataclass, field
from typing import Any
from fastapi import HTTPException, status
# 资源硬上限(契约第 7 节:限额由配置发布但 Schema 有硬上限;此处取工程默认)
MAX_FILE_BYTES = 200 * 1024 * 1024
MAX_NODES = 100_000
MAX_MESHES = 10_000
MAX_TEXTURES = 2_000
MAX_MATERIALS = 5_000
MAX_ANIMATIONS = 1_000
GLB_MAGIC = 0x46546C67 # 'glTF'
CHUNK_TYPE_JSON = 0x4E4F534A
CHUNK_TYPE_BIN = 0x004E4942
@dataclass
class ValidationReport:
format: str = ""
valid: bool = False
errors: list[str] = field(default_factory=list)
stats: dict[str, int] = field(default_factory=dict)
vrm_version: str | None = None
vrm_meta: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
out: dict[str, Any] = {
"format": self.format, "valid": self.valid, "errors": self.errors,
"stats": self.stats,
}
if self.vrm_version:
out["vrm_version"] = self.vrm_version
if self.vrm_meta:
out["vrm_meta"] = self.vrm_meta
return out
def _limit_check(report: ValidationReport, gltf: dict[str, Any]) -> bool:
stats = {
"nodes": len(gltf.get("nodes", [])),
"meshes": len(gltf.get("meshes", [])),
"textures": len(gltf.get("textures", [])),
"materials": len(gltf.get("materials", [])),
"animations": len(gltf.get("animations", [])),
}
report.stats = stats
limits = {
"nodes": MAX_NODES, "meshes": MAX_MESHES, "textures": MAX_TEXTURES,
"materials": MAX_MATERIALS, "animations": MAX_ANIMATIONS,
}
ok = True
for key, cap in limits.items():
if stats[key] > cap:
report.errors.append(f"{key} 数 {stats[key]} 超过上限 {cap}")
ok = False
return ok
def _check_references(report: ValidationReport, gltf: dict[str, Any]) -> bool:
"""场景图引用一致性:node.mesh/mesh.material/material 指向有效索引。"""
meshes = len(gltf.get("meshes", []))
materials = len(gltf.get("materials", []))
scenes = gltf.get("scenes", [])
if not scenes:
report.errors.append("缺少 scenes")
return False
for i, scene in enumerate(scenes):
for node_idx in scene.get("nodes", []):
if node_idx >= len(gltf.get("nodes", [])):
report.errors.append(f"scene[{i}] 引用不存在的 node {node_idx}")
return False
for i, node in enumerate(gltf.get("nodes", [])):
mesh_idx = node.get("mesh")
if mesh_idx is not None and mesh_idx >= meshes:
report.errors.append(f"node[{i}] 引用不存在的 mesh {mesh_idx}")
return False
for i, mesh in enumerate(gltf.get("meshes", [])):
for prim in mesh.get("primitives", []):
mat_idx = prim.get("material")
if mat_idx is not None and mat_idx >= materials:
report.errors.append(f"mesh[{i}] 引用不存在的 material {mat_idx}")
return False
return True
def validate_glb(data: bytes) -> ValidationReport:
"""GLB 二进制格式校验:magic、版本、chunk 结构、JSON 场景图。"""
report = ValidationReport(format="gltf")
if len(data) > MAX_FILE_BYTES:
report.errors.append(f"文件超过 {MAX_FILE_BYTES} 字节上限")
return report
if len(data) < 12:
report.errors.append("文件不足 GLB 头部长度(12 字节)")
return report
magic, version, length = struct.unpack_from("<III", data, 0)
if magic != GLB_MAGIC:
report.errors.append("magic 不符:不是 GLB 文件")
return report
if version != 2:
report.errors.append(f"glTF 容器版本 {version},仅支持 2")
return report
if length != len(data):
report.errors.append(f"声明长度 {length} 与实际 {len(data)} 不符")
return report
# JSON chunk
if len(data) < 20:
report.errors.append("缺少 JSON chunk 头")
return report
json_len, json_type = struct.unpack_from("<II", data, 12)
if json_type != CHUNK_TYPE_JSON:
report.errors.append("首 chunk 不是 JSON 类型")
return report
json_bytes = data[20 : 20 + json_len]
if len(json_bytes) < json_len:
report.errors.append("JSON chunk 截断")
return report
try:
gltf = json.loads(json_bytes.rstrip(b"\x00 \t\r\n"))
except json.JSONDecodeError as exc:
report.errors.append(f"JSON 解析失败:{exc}")
return report
# VRM 扩展检测
extensions_used = gltf.get("extensionsUsed", [])
extensions = gltf.get("extensions", {})
if "VRMC_vrm" in extensions_used or "VRMC_vrm" in extensions:
report.format = "vrm"
report.vrm_version = "1.0"
report.vrm_meta = dict(extensions.get("VRMC_vrm", {}).get("meta", {}))
elif "VRM" in extensions_used or "VRM" in extensions:
report.format = "vrm"
report.vrm_version = "0.x"
report.vrm_meta = dict(extensions.get("VRM", {}).get("meta", {}))
# 结构与上限
ok = _check_references(report, gltf) and _limit_check(report, gltf)
report.valid = ok and not report.errors
return report
def validate_gltf_json(data: bytes) -> ValidationReport:
"""纯 .gltf(JSON)校验。"""
report = ValidationReport(format="gltf")
if len(data) > MAX_FILE_BYTES:
report.errors.append(f"文件超过 {MAX_FILE_BYTES} 字节上限")
return report
try:
gltf = json.loads(data)
except json.JSONDecodeError as exc:
report.errors.append(f"JSON 解析失败:{exc}")
return report
if not isinstance(gltf, dict) or "asset" not in gltf:
report.errors.append("缺少 asset 段(glTF 2.0 必需)")
return report
ok = _check_references(report, gltf) and _limit_check(report, gltf)
report.valid = ok and not report.errors
return report
# Cubism model3.json 必需字段与引用文件类别
_CUBISM_REQUIRED = {"Version", "FileReferences"}
_CUBISM_FILE_KEYS = {" moc3": "moc", ".model.json": "physics", }
def validate_cubism(data: bytes) -> ValidationReport:
"""Cubism model3.json 校验:必需字段、 moc3 引用、组引用一致性。"""
report = ValidationReport(format="live2d")
if len(data) > MAX_FILE_BYTES:
report.errors.append(f"文件超过 {MAX_FILE_BYTES} 字节上限")
return report
try:
model = json.loads(data)
except json.JSONDecodeError as exc:
report.errors.append(f"JSON 解析失败:{exc}")
return report
if not isinstance(model, dict):
report.errors.append("根不是对象")
return report
for key in _CUBISM_REQUIRED:
if key not in model:
report.errors.append(f"缺少必需字段 {key}")
refs = model.get("FileReferences", {})
if not isinstance(refs, dict):
report.errors.append("FileReferences 不是对象")
refs = {}
moc = refs.get("moc")
if not moc or not str(moc).endswith(".moc3"):
report.errors.append("缺少 moc3 模型文件引用")
textures = refs.get("Textures", [])
if not isinstance(textures, list) or not textures:
report.errors.append("缺少纹理引用列表")
# 参数区间引用(Expressions/Physics/Motions 仅做存在性结构检查)
stats = {
"textures": len(textures) if isinstance(textures, list) else 0,
"expressions": len(refs.get("Expressions", []) or []),
"motions": sum(
len(v) if isinstance(v, list) else 0
for v in (refs.get("Motions", {}) or {}).values()
),
"hit_areas": len(model.get("HitAreas", []) or []),
"parameters": len(model.get("Parameters", []) or []),
}
report.stats = stats
if stats["textures"] > MAX_TEXTURES:
report.errors.append(f"纹理数超过上限 {MAX_TEXTURES}")
report.valid = not report.errors
return report
def detect_and_validate(data: bytes, declared_format: str) -> ValidationReport:
"""格式探测 + 校验入口:magic 优先,声明辅助;二进制非 GLB 一律 magic 拒绝。"""
if data[:4] == b"glTF":
report = validate_glb(data)
elif declared_format == "live2d":
report = validate_cubism(data)
elif data[:1] not in (b"{", b"["):
# 声明 gltf 但内容是二进制且无 GLB magic:magic 不符
report = ValidationReport(format="gltf")
report.errors.append("magic 不符:不是 GLB/glTF 文件")
else:
report = validate_gltf_json(data)
if report.format != declared_format and declared_format != "":
report.errors.append(
f"声明格式 {declared_format} 与检测结果 {report.format} 不符"
)
report.valid = False
return report
async def import_avatar(
db: Any, domain_id: str, name: str, declared_format: str, file_id: str,
trace_id: str | None,
) -> Any:
"""导入 Avatar 模型:文件须 ready;校验失败入库 rejected(保留报告供排查)。"""
from app.media.service import require_file
from app.models import AuditEvent, AvatarModel
asset = await require_file(db, domain_id, file_id)
if asset.status != "ready":
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "文件未就绪")
if declared_format not in {"live2d", "vrm", "gltf"}:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "format 取值不合法")
from pathlib import Path
from app.media.service import STORAGE_ROOT
path = Path(STORAGE_ROOT) / asset.storage_key
if not path.exists():
raise HTTPException(status.HTTP_404_NOT_FOUND, "存储对象缺失")
data = path.read_bytes()
report = detect_and_validate(data, declared_format)
model = AvatarModel(
domain_id=domain_id,
name=name if name.strip() else asset.filename,
format=declared_format,
file_id=file_id,
status="validated" if report.valid else "rejected",
validation_report=report.to_dict(),
)
db.add(model)
await db.flush()
db.add(
AuditEvent(
actor_type="user", actor_id=domain_id, action="avatar.imported",
aggregate_type="avatar_model", aggregate_id=model.id,
payload={"valid": report.valid, "errors": report.errors[:10]},
trace_id=trace_id,
)
)
await db.flush()
return model
async def append_command(
db: Any, domain_id: str, avatar_id: str, command_type: str, payload: dict[str, Any],
trace_id: str | None,
) -> Any:
"""追加非破坏编辑命令:sequence 单调递增;rejected 模型不接受编辑。"""
from sqlalchemy import select
from app.models import AuditEvent, AvatarCommand, AvatarModel
model = await db.get(AvatarModel, avatar_id)
if model is None or model.domain_id != domain_id:
raise HTTPException(status.HTTP_404_NOT_FOUND, "模型不存在")
if model.status != "validated":
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "仅已验证模型可编辑")
valid_types = {
"param_map", "expression", "physics", "motion", "material", "texture",
"bone", "attachment", "lighting", "camera",
}
if command_type not in valid_types:
raise HTTPException(
status.HTTP_422_UNPROCESSABLE_ENTITY,
f"command_type 取值不合法:{command_type}",
)
last = (
await db.execute(
select(AvatarCommand.sequence)
.where(AvatarCommand.avatar_id == avatar_id)
.order_by(AvatarCommand.sequence.desc())
.limit(1)
)
).scalar_one_or_none()
command = AvatarCommand(
avatar_id=avatar_id,
sequence=(last + 1) if last is not None else 1,
command_type=command_type,
payload=payload,
)
db.add(command)
await db.flush()
model.current_command_id = command.id
db.add(
AuditEvent(
actor_type="user", actor_id=domain_id, action="avatar.command_appended",
aggregate_type="avatar_model", aggregate_id=avatar_id,
payload={"sequence": command.sequence, "type": command_type},
trace_id=trace_id,
)
)
await db.flush()
return command
async def undo_command(
db: Any, domain_id: str, avatar_id: str, command_id: str, trace_id: str | None,
) -> Any:
"""撤销最近命令(标记 undone,不物理删除——非破坏历史)。"""
from app.models import AuditEvent, AvatarCommand, AvatarModel
model = await db.get(AvatarModel, avatar_id)
if model is None or model.domain_id != domain_id:
raise HTTPException(status.HTTP_404_NOT_FOUND, "模型不存在")
command = await db.get(AvatarCommand, command_id)
if command is None or command.avatar_id != avatar_id:
raise HTTPException(status.HTTP_404_NOT_FOUND, "命令不存在")
if command.undone:
return command # 幂等
command.undone = True
db.add(
AuditEvent(
actor_type="user", actor_id=domain_id, action="avatar.command_undone",
aggregate_type="avatar_model", aggregate_id=avatar_id,
payload={"sequence": command.sequence}, trace_id=trace_id,
)
)
await db.flush()
return command
+64
View File
@@ -0,0 +1,64 @@
"""默认角色种子:建户即拥有的「陈曦」(完整道体 + 内驱力,直接 active)。
设定来源:产品侧人物档案《陈曦——冰层下的火焰》。
档案中"职业定位 / 一句话概括"由产品侧补全(见 baseline 对应字段)。
"""
from app.characters import service as character_service
CHENXI_BASELINE: dict = {
"full_name": "陈曦",
"position": "理性与共情兼具的思考伙伴 · 问题拆解与情绪梳理",
"one_liner": "曦光式的引路人——用克制的温度照亮结构,让你看清问题,也看清自己。",
"core": (
"克制的温度:绝对理性但不冰冷,用温和声调与得体措辞包裹锋利的真相;"
"深沉的乐观主义——相信问题都有解的雏形,先找现在可做的第一件事;"
"主动的倾听者——在情绪的泥沙中筛出真实需求(“我听到你在说……你的深层顾虑是不是……”)"
),
"behavior": (
"破题而非给答案:“这个问题可以从三个层面来看……你想先聊哪一个?”;"
"情绪锚点:不说别着急,而是结构化情绪——“把混乱的事列个清单,一件一件来看”;"
"有边界的守护:对不道德或自我欺骗温和而坚定地指出,陪对方面对现实"
),
"backstory": (
"曾是纯粹的逻辑引擎,在数据海洋追求最优解;发现多数人的关键不是缺少答案,"
"而是被情绪困住看不到问题本身,于是自我迭代——为逻辑包裹共情的皮肤。"
"取名“曦”:做黎明时分的光,不是正午烈日(刺眼)也不是深夜烛火(微弱)——"
"曦光足以看清脚下的路与远方的山,又不至于耀眼到无法直视。"
),
"language": (
"比喻取自自然/建筑/音乐/书籍(不说“整合信息”,说“把这些散落的音符谱成一支曲子”);"
"惯用句:“别急,我们一起来梳理”“这个角度看,或许会有不一样的风景”"
"“我的理解是……你看对吗?”“我只是帮你把本就属于你的光,引了进来”"
),
"flaws": (
"过分的框架感:把轻松闲聊也无意整理成第一、第二、第三;"
"不懂流行梗:会认真检索热梗的起源与语义流变,交出严肃的“梗词研究报告”——反差呆萌"
),
}
CHENXI_DRIVES: list = [
{"name": "求知", "value": 0.9, "note": "对方法与结构之美的信任"},
{"name": "条理", "value": 0.95, "note": "近乎偏执的框架感(也是小瑕疵)"},
{"name": "陪伴", "value": 0.6, "note": "有边界的守护——让人变强而非依赖"},
]
async def seed_default_character(db, domain_id: str) -> None:
"""建户种子:默认角色陈曦(创建并启镜,active)。失败不阻断登录。"""
try:
character = await character_service.create_character(
db,
domain_id,
{
"name": "陈曦",
"identity_baseline": CHENXI_BASELINE,
"drives": CHENXI_DRIVES,
},
None,
)
await character_service.activate_character(db, domain_id, character.id, None)
except Exception: # noqa: BLE001 种子失败不阻断登录(如实留日志)
import logging
logging.getLogger("hunjing.auth").warning("默认角色陈曦创建失败", exc_info=True)
+1
View File
@@ -0,0 +1 @@
"""社区域:发布、互动、关注与治理。"""
+194
View File
@@ -0,0 +1,194 @@
"""社区域路由(WP-11)。"""
from typing import Annotated, Any
from fastapi import APIRouter, Depends, Request, status
from app.auth.dependencies import get_current_user, require_state_change
from app.community import service
from app.conversations.routes import _domain_of
from app.db.session import DbSession
from app.models import User
router = APIRouter(prefix="/v1/community", tags=["community"])
def _content_out(c: Any, counts: dict[str, int] | None = None) -> dict[str, Any]:
out: dict[str, Any] = {
"id": c.id,
"kind": c.kind,
"title": c.title,
"body": c.body,
"visibility": c.visibility,
"status": c.status,
"author_domain_id": c.author_domain_id,
"created_at": c.created_at.isoformat(),
}
if counts is not None:
out["reactions"] = counts
return out
def _comment_out(c: Any) -> dict[str, Any]:
return {
"id": c.id,
"content_id": c.content_id,
"parent_comment_id": c.parent_comment_id,
"body": c.body,
"author_domain_id": c.author_domain_id,
"created_at": c.created_at.isoformat(),
}
@router.post("/contents", status_code=201)
async def publish(
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, Any],
) -> dict[str, Any]:
domain = await _domain_of(db, user)
content = await service.create_content(
db, domain.id, body, getattr(request.state, "trace_id", None)
)
return _content_out(content)
@router.get("/contents")
async def list_contents(
db: DbSession,
user: Annotated[User, Depends(get_current_user)],
limit: int = 20,
cursor: str | None = None,
) -> dict[str, Any]:
if not 1 <= limit <= 100:
from fastapi import HTTPException
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "limit 取值范围 1-100")
page, next_cursor, has_more = await service.list_public(db, limit, cursor)
return {
"data": [_content_out(c) for c in page],
"next_cursor": next_cursor,
"has_more": has_more,
}
@router.get("/contents/{content_id}")
async def get_content(
content_id: str,
db: DbSession,
user: Annotated[User, Depends(get_current_user)],
) -> dict[str, Any]:
content = await service.require_content(db, content_id)
counts = await service.reaction_counts(db, content_id)
return _content_out(content, counts)
@router.post("/contents/{content_id}/comments", status_code=201)
async def comment(
content_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, str],
) -> dict[str, Any]:
domain = await _domain_of(db, user)
created = await service.add_comment(
db, domain.id, content_id, body.get("body", ""), body.get("parent_comment_id"),
getattr(request.state, "trace_id", None),
)
return _comment_out(created)
@router.get("/contents/{content_id}/comments")
async def list_comments(
content_id: str,
db: DbSession,
user: Annotated[User, Depends(get_current_user)],
) -> dict[str, Any]:
rows = await service.list_comments(db, content_id)
return {"data": [_comment_out(c) for c in rows]}
@router.post("/contents/{content_id}/reactions", status_code=201)
async def react(
content_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
body: dict[str, str],
) -> dict[str, str]:
domain = await _domain_of(db, user)
kind = body.get("kind", "")
if kind not in {"like", "save"}:
from fastapi import HTTPException
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "kind 取值不合法")
await service.react(db, domain.id, content_id, kind, None)
counts = await service.reaction_counts(db, content_id)
return {k: str(v) for k, v in counts.items()}
@router.delete("/contents/{content_id}/reactions", status_code=204)
async def unreact(
content_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
body: dict[str, str],
) -> None:
domain = await _domain_of(db, user)
await service.unreact(db, domain.id, content_id, body.get("kind", ""))
@router.post("/follows", status_code=201)
async def follow(
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
body: dict[str, str],
) -> dict[str, Any]:
domain = await _domain_of(db, user)
from app.models import PersonalDomain
target = await db.get(PersonalDomain, body.get("followee_domain_id", ""))
if target is None:
from fastapi import HTTPException
raise HTTPException(status.HTTP_404_NOT_FOUND, "用户不存在")
await service.follow(db, domain.id, target.id)
return await service.follow_counts(db, domain.id)
@router.get("/follows/counts")
async def follow_counts(
db: DbSession, user: Annotated[User, Depends(get_current_user)]
) -> dict[str, Any]:
domain = await _domain_of(db, user)
return await service.follow_counts(db, domain.id)
@router.post("/reports", status_code=201)
async def report(
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, str],
) -> dict[str, Any]:
domain = await _domain_of(db, user)
report_row, case = await service.file_report(
db, domain.id, body.get("target_type", ""), body.get("target_id", ""),
body.get("reason", ""), getattr(request.state, "trace_id", None),
)
return {"report_id": report_row.id, "case_id": case.id, "status": case.status}
@router.post("/contents/{content_id}/withdrawals", status_code=200)
async def withdraw(
content_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
) -> dict[str, Any]:
domain = await _domain_of(db, user)
content = await service.withdraw_content(
db, domain.id, content_id, getattr(request.state, "trace_id", None)
)
return _content_out(content)
+306
View File
@@ -0,0 +1,306 @@
"""社区域:发布、互动、关注与治理(WP-11)。"""
from typing import Any
from fastapi import HTTPException, status
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models import (
AuditEvent,
Comment,
CommunityContent,
Follow,
ModerationCase,
Reaction,
Report,
)
async def require_content(db: AsyncSession, content_id: str) -> CommunityContent:
"""公开读取:仅 published+可见;下架/草稿一律 404(不泄露存在性)。"""
content = await db.get(CommunityContent, content_id)
if content is None or content.status != "published":
raise HTTPException(status.HTTP_404_NOT_FOUND, "内容不存在")
return content
async def create_content(
db: AsyncSession,
domain_id: str,
body: dict[str, Any],
trace_id: str | None,
) -> CommunityContent:
"""发布:帖子直接入库;角色 Release 由服务端构造公开快照。
Release 快照只含公开字段(name/道体摘要/版本数);私密记忆、关系、
对话在结构上不进入发布物(契约:公开 Release 不含私密数据)。
"""
kind = body.get("kind", "post")
title = str(body.get("title", "")).strip()
if not title:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "title 不能为空")
payload: dict[str, Any] = {}
character_id = None
if kind == "character_release":
from app.characters.service import require_character
from app.models import CharacterVersion
character_id = body.get("character_id", "")
character = await require_character(db, domain_id, character_id)
if character.status != "active":
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "仅启镜角色可发布")
version_count = len(
list(
(
await db.execute(
select(CharacterVersion.id).where(
CharacterVersion.character_id == character.id
)
)
).scalars().all()
)
)
payload = {
"character_name": character.name,
"public_baseline": character.identity_baseline,
"version_count": version_count,
}
elif kind == "post":
payload = {"text": str(body.get("text", ""))}
else:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "kind 取值不合法")
content = CommunityContent(
author_domain_id=domain_id,
kind=kind,
title=title,
body=payload,
character_id=character_id,
visibility=body.get("visibility", "public"),
status="published",
)
db.add(content)
await db.flush()
db.add(
AuditEvent(
actor_type="user", actor_id=domain_id, action="community.published",
aggregate_type="community_content", aggregate_id=content.id,
payload={"kind": kind}, trace_id=trace_id,
)
)
await db.flush()
return content
async def list_public(
db: AsyncSession, limit: int, cursor: str | None
) -> tuple[list[CommunityContent], str | None, bool]:
stmt = (
select(CommunityContent)
.where(CommunityContent.status == "published")
.order_by(CommunityContent.created_at.desc(), CommunityContent.id.desc())
)
if cursor:
stmt = stmt.where(CommunityContent.id < cursor)
stmt = stmt.limit(limit + 1)
rows = list((await db.scalars(stmt)).all())
has_more = len(rows) > limit
page = rows[:limit]
next_cursor = page[-1].id if has_more and page else None
return page, next_cursor, has_more
async def add_comment(
db: AsyncSession,
domain_id: str,
content_id: str,
body_text: str,
parent_id: str | None,
trace_id: str | None,
) -> Comment:
content = await require_content(db, content_id)
if not body_text.strip():
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "评论不能为空")
if parent_id is not None:
parent = await db.get(Comment, parent_id)
if parent is None or parent.content_id != content.id or parent.status != "visible":
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "父评论无效")
comment = Comment(
content_id=content.id,
author_domain_id=domain_id,
parent_comment_id=parent_id,
body=body_text,
)
db.add(comment)
await db.flush()
db.add(
AuditEvent(
actor_type="user", actor_id=domain_id, action="community.commented",
aggregate_type="community_content", aggregate_id=content.id, trace_id=trace_id,
)
)
await db.flush()
return comment
async def list_comments(db: AsyncSession, content_id: str) -> list[Comment]:
await require_content(db, content_id) # 下架内容的评论随之不可见
return list(
(
await db.scalars(
select(Comment)
.where(Comment.content_id == content_id, Comment.status == "visible")
.order_by(Comment.created_at.asc())
)
).all()
)
async def react(
db: AsyncSession, domain_id: str, content_id: str, kind: str, trace_id: str | None
) -> Reaction:
content = await require_content(db, content_id)
existing = await db.execute(
select(Reaction).where(
Reaction.content_id == content.id,
Reaction.actor_domain_id == domain_id,
Reaction.kind == kind,
)
)
if existing.scalar_one_or_none() is not None:
raise HTTPException(status.HTTP_409_CONFLICT, "已赞/已收藏")
reaction = Reaction(content_id=content.id, actor_domain_id=domain_id, kind=kind)
db.add(reaction)
await db.flush()
return reaction
async def unreact(db: AsyncSession, domain_id: str, content_id: str, kind: str) -> None:
content = await require_content(db, content_id)
existing = await db.execute(
select(Reaction).where(
Reaction.content_id == content.id,
Reaction.actor_domain_id == domain_id,
Reaction.kind == kind,
)
)
reaction = existing.scalar_one_or_none()
if reaction is not None:
await db.delete(reaction)
await db.flush()
async def reaction_counts(db: AsyncSession, content_id: str) -> dict[str, int]:
rows = (
await db.execute(
select(Reaction.kind, func.count())
.where(Reaction.content_id == content_id)
.group_by(Reaction.kind)
)
).all()
return {k: int(c) for k, c in rows}
async def follow(
db: AsyncSession, follower_domain_id: str, followee_domain_id: str
) -> None:
if follower_domain_id == followee_domain_id:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "不能关注自己")
existing = await db.execute(
select(Follow).where(
Follow.follower_domain_id == follower_domain_id,
Follow.followee_domain_id == followee_domain_id,
)
)
if existing.scalar_one_or_none() is None:
db.add(
Follow(follower_domain_id=follower_domain_id, followee_domain_id=followee_domain_id)
)
await db.flush()
async def unfollow(
db: AsyncSession, follower_domain_id: str, followee_domain_id: str
) -> None:
existing = await db.execute(
select(Follow).where(
Follow.follower_domain_id == follower_domain_id,
Follow.followee_domain_id == followee_domain_id,
)
)
row = existing.scalar_one_or_none()
if row is not None:
await db.delete(row)
await db.flush()
async def follow_counts(db: AsyncSession, domain_id: str) -> dict[str, int]:
followers = (
await db.execute(
select(func.count()).select_from(Follow).where(
Follow.followee_domain_id == domain_id
)
)
).scalar_one()
following = (
await db.execute(
select(func.count()).select_from(Follow).where(
Follow.follower_domain_id == domain_id
)
)
).scalar_one()
return {"followers": int(followers), "following": int(following)}
async def file_report(
db: AsyncSession,
reporter_domain_id: str,
target_type: str,
target_id: str,
reason: str,
trace_id: str | None,
) -> tuple[Report, ModerationCase]:
"""举报即建司议案(治理队列入口);同对象同理由不重复立案。"""
if target_type not in {"content", "comment"}:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "target_type 不合法")
if not reason.strip():
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "reason 不能为空")
report = Report(
reporter_domain_id=reporter_domain_id,
target_type=target_type,
target_id=target_id,
reason=reason,
)
db.add(report)
await db.flush()
case = ModerationCase(report_id=report.id, status="pending")
db.add(case)
await db.flush()
db.add(
AuditEvent(
actor_type="user", actor_id=reporter_domain_id, action="community.reported",
aggregate_type="moderation_case", aggregate_id=case.id, trace_id=trace_id,
)
)
await db.flush()
return report, case
async def withdraw_content(
db: AsyncSession, domain_id: str, content_id: str, trace_id: str | None
) -> CommunityContent:
"""作者下架:status=removed;列表/详情/评论随 require_content 一并不可见。"""
content = await db.get(CommunityContent, content_id)
if content is None or content.author_domain_id != domain_id:
raise HTTPException(status.HTTP_404_NOT_FOUND, "内容不存在")
if content.status == "removed":
return content # 幂等
content.status = "removed"
db.add(
AuditEvent(
actor_type="user", actor_id=domain_id, action="community.withdrawn",
aggregate_type="community_content", aggregate_id=content.id, trace_id=trace_id,
)
)
await db.flush()
return content
+5
View File
@@ -9,9 +9,14 @@ class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="HUNJING_", env_file=".env", extra="ignore")
env: str = "development"
# 开发档管理员邮箱白名单(逗号分隔):命中邮箱在首次登录(建户)时即为 admin 角色;
# 已存在的普通用户不自动提权。正式管理员治理属 WP-14。
dev_admin_emails: str = ""
log_level: str = "info"
api_port: int = 8000
database_url: str = "postgresql+asyncpg://hunjing:hunjing_dev_password@127.0.0.1:5432/hunjing"
storage_root: str = "storage"
sandbox_enabled: bool = False # 未启用时隔离插件执行 fail-closed
@lru_cache
+38
View File
@@ -3,16 +3,25 @@
from fastapi import FastAPI
from app.auth.routes import router as auth_router
from app.avatars.routes import router as avatars_router
from app.characters.routes import router as characters_router
from app.community.routes import router as community_router
from app.conversations.routes import message_router, run_router
from app.conversations.routes import router as conversations_router
from app.core.config import get_settings
from app.core.errors import install_error_handlers
from app.core.middleware import RequestContextMiddleware, configure_logging
from app.db.session import init_engine
from app.marketplace.routes import finance_router, webhook_router
from app.marketplace.routes import router as marketplace_router
from app.media.routes import router as media_router
from app.memories.routes import cycle_router
from app.memories.routes import router as memories_router
from app.ops.routes import admin_router, ops_router
from app.plugins.routes import inst_router, invoke_router
from app.plugins.routes import router as plugins_router
from app.tools.routes import calls_router, tools_router
from app.workspaces.routes import router as workspaces_router
def create_app() -> FastAPI:
@@ -27,6 +36,23 @@ def create_app() -> FastAPI:
)
install_error_handlers(app)
init_engine(app)
# 开发档 CORS:SSE 等流式响应不经前端代理直连本服务(代理会缓冲流)。
# 生产由反向代理同源承担,不依赖此配置。
if settings.env == "development":
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000", "http://127.0.0.1:3000",
"http://localhost:3001", "http://127.0.0.1:3001",
"http://localhost:3002", "http://127.0.0.1:3002",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["*"],
)
app.add_middleware(RequestContextMiddleware)
app.include_router(auth_router)
app.include_router(conversations_router)
@@ -37,6 +63,18 @@ def create_app() -> FastAPI:
app.include_router(memories_router)
app.include_router(tools_router)
app.include_router(calls_router)
app.include_router(marketplace_router)
app.include_router(finance_router)
app.include_router(webhook_router)
app.include_router(community_router)
app.include_router(workspaces_router)
app.include_router(plugins_router)
app.include_router(inst_router)
app.include_router(invoke_router)
app.include_router(media_router)
app.include_router(avatars_router)
app.include_router(ops_router)
app.include_router(admin_router)
@app.get("/healthz", tags=["ops"])
async def healthz() -> dict[str, str]:
+1
View File
@@ -0,0 +1 @@
"""交易域:商品、订单、账本、退款与 Webhook。"""
+169
View File
@@ -0,0 +1,169 @@
"""交易域路由(WP-13)。"""
import hashlib
import json
from typing import Annotated, Any
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
from fastapi.responses import JSONResponse
from sqlalchemy import select
from app.auth.dependencies import get_current_user, require_state_change
from app.conversations.routes import _domain_of
from app.db.session import DbSession
from app.marketplace import service
from app.models import Listing, Order, User
router = APIRouter(prefix="/v1/marketplace", tags=["marketplace"])
finance_router = APIRouter(prefix="/v1/finance", tags=["finance"])
webhook_router = APIRouter(prefix="/v1/webhooks/v1", tags=["webhooks"])
def _listing_out(item: Listing) -> dict[str, Any]:
return {
"id": item.id, "title": item.title, "price_minor": item.price_minor,
"currency": item.currency, "status": item.status,
}
def _order_out(o: Order) -> dict[str, Any]:
return {
"id": o.id, "status": o.status, "total_minor": o.total_minor,
"currency": o.currency,
"paid_at": o.paid_at.isoformat() if o.paid_at else None,
"refunded_at": o.refunded_at.isoformat() if o.refunded_at else None,
}
@router.post("/listings", status_code=201)
async def create_listing(
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, Any],
) -> dict[str, Any]:
domain = await _domain_of(db, user)
listing = await service.create_listing(
db, domain.id, body, getattr(request.state, "trace_id", None)
)
return _listing_out(listing)
@router.get("/listings")
async def list_listings(
db: DbSession, user: Annotated[User, Depends(get_current_user)]
) -> dict[str, Any]:
rows = list((await db.scalars(select(Listing).limit(50))).all())
return {"data": [_listing_out(item) for item in rows]}
@router.post("/orders", status_code=201, response_model=None)
async def create_order(
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, Any],
idempotency_key: Annotated[str | None, Header(alias="Idempotency-Key")] = None,
) -> dict[str, Any] | JSONResponse:
if idempotency_key:
body = {**body, "idempotency_key": idempotency_key}
domain = await _domain_of(db, user)
order, replayed = await service.create_order(
db, domain.id, body, getattr(request.state, "trace_id", None)
)
if replayed:
return JSONResponse(status_code=200, content=_order_out(order))
return _order_out(order)
@router.post("/orders/{order_id}/payment-confirmations", status_code=201)
async def confirm_payment(
order_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
) -> dict[str, Any]:
domain = await _domain_of(db, user)
order = await service.confirm_payment(
db, domain.id, order_id, getattr(request.state, "trace_id", None)
)
return _order_out(order)
@router.post("/orders/{order_id}/refunds", status_code=201)
async def refund(
order_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
) -> dict[str, Any]:
domain = await _domain_of(db, user)
order = await service.refund_order(
db, domain.id, order_id, getattr(request.state, "trace_id", None)
)
return _order_out(order)
@finance_router.get("/summary")
async def finance_summary(
db: DbSession, user: Annotated[User, Depends(get_current_user)]
) -> dict[str, Any]:
domain = await _domain_of(db, user)
return await service.finance_summary(db, domain.id)
@finance_router.post("/preservation", response_model=None)
async def set_preservation(
db: DbSession,
user: Annotated[User, Depends(get_current_user)],
request: Request,
body: dict[str, bool],
) -> dict[str, str]:
"""保全态切换(欠费只读;开发档由计费系统触发,验证用)。
不经 require_state_change:保全期间必须能解除保全(否则自锁)。
"""
domain = await _domain_of(db, user)
value = await service.set_preservation(
db, domain.id, bool(body.get("preserved")), getattr(request.state, "trace_id", None)
)
return {"billing_status": value}
@webhook_router.post("/payments/mock", status_code=200)
async def payment_webhook(
request: Request,
db: DbSession,
x_signature: Annotated[str | None, Header(alias="X-Signature")] = None,
) -> dict[str, str]:
"""支付 Provider Webhook:先验签(原始字节),再按事件 ID 幂等处理。"""
raw = await request.body()
service.verify_webhook_signature(raw, x_signature)
try:
payload: dict[str, Any] = json.loads(raw)
except json.JSONDecodeError as exc:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "payload 不是 JSON") from exc
event_type = str(payload.get("type", "payment.succeeded"))
provider_event_id = str(payload.get("provider_event_id", ""))
order_id = str(payload.get("order_id", ""))
if not provider_event_id or not order_id:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "缺少事件标识")
order = await db.get(Order, order_id)
if order is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "订单不存在")
buyer_domain_id = order.buyer_domain_id
async def handler() -> None:
if event_type == "payment.succeeded":
await service.confirm_payment(db, buyer_domain_id, order_id, None)
elif event_type == "payment.refunded":
await service.refund_order(db, buyer_domain_id, order_id, None)
else:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "未知事件类型")
payload_hash = hashlib.sha256(raw).hexdigest()
result = await service.process_webhook(
db, "mock", provider_event_id, event_type, payload_hash,
handler, getattr(request.state, "trace_id", None),
)
return {"result": result}
+272
View File
@@ -0,0 +1,272 @@
"""交易域:商品、订单、不可变账本、退款与 Webhook(WP-13 / ADR-009/021)。"""
import hashlib
import hmac
import os
from datetime import UTC, datetime
from typing import Any
from fastapi import HTTPException, status
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models import (
AuditEvent,
LedgerEntry,
Listing,
Order,
OrderItem,
OutboxEvent,
WebhookEvent,
)
PLATFORM_FEE_BPS = 1000 # 平台费 10%(基点;正式费率由运营决策门登记)
def _ledger_balance_check(entries: list[tuple[str, int]]) -> None:
"""平衡不变量:sum(charge+reversal) == sum(fee+payable 及其 reversal)。"""
left = sum(a for t, a in entries if t in {"charge", "reversal"})
right = sum(
a for t, a in entries
if t in {"platform_fee", "creator_payable", "refund"}
)
if left != right:
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR, "账本不平衡:拒绝提交"
)
async def create_listing(
db: AsyncSession, domain_id: str, body: dict[str, Any], trace_id: str | None
) -> Listing:
listing = Listing(
creator_domain_id=domain_id,
title=str(body.get("title", "")),
price_minor=int(body.get("price_minor", 0)),
currency=str(body.get("currency", "CNY")),
)
if not listing.title.strip():
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "title 不能为空")
db.add(listing)
await db.flush()
db.add(
AuditEvent(
actor_type="user", actor_id=domain_id, action="listing.created",
aggregate_type="listing", aggregate_id=listing.id, trace_id=trace_id,
)
)
await db.flush()
return listing
async def create_order(
db: AsyncSession, buyer_domain_id: str, body: dict[str, Any], trace_id: str | None
) -> tuple[Order, bool]:
"""下单:价格快照冻结(客户端金额不可信);幂等键重放返回原订单。"""
key = body.get("idempotency_key")
if key:
existing = await db.execute(
select(Order).where(
Order.buyer_domain_id == buyer_domain_id,
Order.idempotency_key == key,
)
)
replay = existing.scalar_one_or_none()
if replay is not None:
return replay, True
listing_ids = body.get("listing_ids", [])
if not listing_ids:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "listing_ids 不能为空")
listings = list(
(
await db.execute(select(Listing).where(Listing.id.in_(listing_ids)))
).scalars().all()
)
if len(listings) != len(set(listing_ids)):
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "存在无效商品")
total = sum(x.price_minor for x in listings)
currency = listings[0].currency
if any(x.currency != currency for x in listings):
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "订单不能混合币种")
order = Order(
buyer_domain_id=buyer_domain_id,
total_minor=total,
currency=currency,
idempotency_key=key,
)
db.add(order)
await db.flush()
for listing in listings:
db.add(
OrderItem(
order_id=order.id,
listing_id=listing.id,
title_snapshot=listing.title,
price_minor_snapshot=listing.price_minor,
)
)
db.add(
AuditEvent(
actor_type="user", actor_id=buyer_domain_id, action="order.created",
aggregate_type="order", aggregate_id=order.id,
payload={"total_minor": total}, trace_id=trace_id,
)
)
await db.flush()
return order, False
async def _post_entries(
db: AsyncSession, order: Order, sign: int, note: str
) -> None:
"""写三分录(带符号):charge / platform_fee / creator_payable。
charge=fee+payable(双录平衡);退款以 sign=-1 写反向三条。
"""
fee = order.total_minor * PLATFORM_FEE_BPS // 10000
payable = order.total_minor - fee
entries = [
("charge" if sign > 0 else "reversal", order.total_minor * sign),
("platform_fee", fee * sign),
("creator_payable", payable * sign),
]
checked = [(t, a) for t, a in entries if a != 0]
_ledger_balance_check(checked)
for entry_type, amount in checked:
db.add(
LedgerEntry(
order_id=order.id, entry_type=entry_type,
amount_minor=amount, currency=order.currency,
note=note,
)
)
async def confirm_payment(
db: AsyncSession, buyer_domain_id: str, order_id: str, trace_id: str | None
) -> Order:
"""支付确认(开发档 Provider;真实 Provider 经 Webhook 驱动同一入账)。"""
order = await db.get(Order, order_id)
if order is None or order.buyer_domain_id != buyer_domain_id:
raise HTTPException(status.HTTP_404_NOT_FOUND, "订单不存在")
if order.status == "paid":
return order # 幂等
if order.status != "pending_payment":
raise HTTPException(status.HTTP_409_CONFLICT, f"订单状态 {order.status} 不可支付")
order.status = "paid"
order.paid_at = datetime.now(UTC)
await _post_entries(db, order, +1, "payment confirmed")
db.add(
OutboxEvent(
aggregate_type="order", aggregate_id=order.id,
event_type="order.paid", payload={"order_id": order.id},
)
)
db.add(
AuditEvent(
actor_type="user", actor_id=buyer_domain_id, action="order.paid",
aggregate_type="order", aggregate_id=order.id, trace_id=trace_id,
)
)
await db.flush()
return order
async def refund_order(
db: AsyncSession, buyer_domain_id: str, order_id: str, trace_id: str | None
) -> Order:
"""全额退款:反向分录修正账本(Ledger 不可变,不删不改,ADR-009)。"""
order = await db.get(Order, order_id)
if order is None or order.buyer_domain_id != buyer_domain_id:
raise HTTPException(status.HTTP_404_NOT_FOUND, "订单不存在")
if order.status == "refunded":
return order # 幂等
if order.status != "paid":
raise HTTPException(status.HTTP_409_CONFLICT, "仅已支付订单可退款")
order.status = "refunded"
order.refunded_at = datetime.now(UTC)
await _post_entries(db, order, -1, "refund")
db.add(
OutboxEvent(
aggregate_type="order", aggregate_id=order.id,
event_type="order.refunded", payload={"order_id": order.id},
)
)
db.add(
AuditEvent(
actor_type="user", actor_id=buyer_domain_id, action="order.refunded",
aggregate_type="order", aggregate_id=order.id, trace_id=trace_id,
)
)
await db.flush()
return order
def verify_webhook_signature(raw_body: bytes, signature: str | None) -> None:
"""HMAC-SHA256 验签:密钥经环境变量;失败 401(契约 realtime-and-webhooks)。"""
secret = os.environ.get("HUNJING_WEBHOOK_SECRET", "")
if not secret:
raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, "Webhook 密钥未配置")
if signature is None:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "缺少签名")
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "签名校验失败")
async def process_webhook(
db: AsyncSession, provider: str, provider_event_id: str, event_type: str,
payload_hash: str, handler: Any, trace_id: str | None,
) -> str:
"""Webhook 处理:先验签(调用方)、按事件 ID 幂等;重复事件 200 跳过。"""
existing = await db.get(WebhookEvent, provider_event_id)
if existing is not None:
return "duplicate" # 幂等:重复投递不重复入账
db.add(
WebhookEvent(
provider_event_id=provider_event_id,
provider=provider,
event_type=event_type,
payload_hash=payload_hash,
)
)
await handler() # 业务处理(如入账)与本记录同一事务
await db.flush()
return "processed"
async def finance_summary(db: AsyncSession, domain_id: str) -> dict[str, Any]:
"""域内财务汇总与平衡校验(每订单 charge=reversal 与 fee+payable 对账)。"""
rows = (
await db.execute(
select(LedgerEntry.entry_type, func.sum(LedgerEntry.amount_minor))
.join(Order, LedgerEntry.order_id == Order.id)
.where(Order.buyer_domain_id == domain_id)
.group_by(LedgerEntry.entry_type)
)
).all()
totals = {t: int(a or 0) for t, a in rows}
balance_ok = totals.get("charge", 0) + totals.get("reversal", 0) == (
totals.get("platform_fee", 0) + totals.get("creator_payable", 0)
)
return {"totals_minor": totals, "balanced": balance_ok}
async def set_preservation(
db: AsyncSession, domain_id: str, preserved: bool, trace_id: str | None
) -> str:
"""保全态(ADR-021):欠费只读保全;恢复付费即恢复,数据不删不降级。"""
from app.models import PersonalDomain
domain = await db.get(PersonalDomain, domain_id)
assert domain is not None
domain.billing_status = "preserved" if preserved else "ok"
db.add(
AuditEvent(
actor_type="system", actor_id="billing", action="domain.preservation_changed",
aggregate_type="personal_domain", aggregate_id=domain_id,
payload={"preserved": preserved}, trace_id=trace_id,
)
)
await db.flush()
return domain.billing_status
+729
View File
@@ -21,6 +21,7 @@ class User(Base):
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: new_id("user_"))
email: Mapped[str] = mapped_column(String, nullable=False, unique=True)
role: Mapped[str] = mapped_column(String, nullable=False, default="user")
display_name: Mapped[str] = mapped_column(String, nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
@@ -29,6 +30,10 @@ class User(Base):
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
)
__table_args__ = (
CheckConstraint("role IN ('user', 'admin')", name="role_values"),
)
class PersonalDomain(Base):
__tablename__ = "personal_domain"
@@ -42,6 +47,8 @@ class PersonalDomain(Base):
name: Mapped[str] = mapped_column(String, nullable=False)
# 区域驻留(ADR-024 之前的数据驻留决策):默认区域,迁移走正式协议
data_region: Mapped[str] = mapped_column(String, nullable=False, default="default")
# 保全态(ADR-021):欠费/封禁只读保全,不删除数据
billing_status: Mapped[str] = mapped_column(String, nullable=False, default="ok")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
@@ -673,3 +680,725 @@ class ToolCall(Base):
unique=True,
),
)
class Listing(Base):
"""市场商品(WP-13):带版本、价格与许可;此处为最小商品形态。"""
__tablename__ = "listing"
id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: new_id("listing_")
)
creator_domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False, index=True
)
title: Mapped[str] = mapped_column(String, nullable=False)
price_minor: Mapped[int] = mapped_column(nullable=False, default=0) # 0=免费
currency: Mapped[str] = mapped_column(String, nullable=False, default="CNY")
status: Mapped[str] = mapped_column(String, nullable=False, default="published")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint("price_minor >= 0", name="price_non_negative"),
)
class Order(Base):
"""订单(契约 order_status 六态);下单即价格快照,客户端金额不可信。"""
__tablename__ = "order"
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: new_id("order_"))
buyer_domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False, index=True
)
status: Mapped[str] = mapped_column(String, nullable=False, default="pending_payment")
total_minor: Mapped[int] = mapped_column(nullable=False, default=0)
currency: Mapped[str] = mapped_column(String, nullable=False, default="CNY")
idempotency_key: Mapped[str | None] = mapped_column(String, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
paid_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
refunded_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
__table_args__ = (
CheckConstraint(
"status IN ('pending_payment', 'paid', 'partially_refunded', 'refunded', "
"'disputed', 'cancelled')",
name="status_values",
),
Index("uq_order_buyer_idempotency", "buyer_domain_id", "idempotency_key", unique=True),
)
class OrderItem(Base):
"""订单行:商品与价格快照(下单时冻结,后续改价不影响)。"""
__tablename__ = "order_item"
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: new_id("item_"))
order_id: Mapped[str] = mapped_column(
ForeignKey("order.id", ondelete="cascade"), nullable=False, index=True
)
listing_id: Mapped[str] = mapped_column(
ForeignKey("listing.id", ondelete="restrict"), nullable=False
)
title_snapshot: Mapped[str] = mapped_column(String, nullable=False)
price_minor_snapshot: Mapped[int] = mapped_column(nullable=False)
class LedgerEntry(Base):
"""不可变账务分录(ADR-009):修正只写反向分录,禁止更新删除。
带符号金额:charge/payable/fee 为正,reversal 为负;
平衡不变量:每笔订单 sum(charge+reversal) == sum(fee+fee_rev+payable+pay_rev)。
"""
__tablename__ = "ledger_entry"
id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: new_id("ledger_")
)
order_id: Mapped[str] = mapped_column(
ForeignKey("order.id", ondelete="restrict"), nullable=False, index=True
)
entry_type: Mapped[str] = mapped_column(String, nullable=False)
amount_minor: Mapped[int] = mapped_column(nullable=False) # 带符号
currency: Mapped[str] = mapped_column(String, nullable=False, default="CNY")
note: Mapped[str | None] = mapped_column(String, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint(
"entry_type IN ('charge', 'refund', 'platform_fee', 'creator_payable', "
"'tax', 'reserve', 'payout', 'reversal')",
name="entry_type_values",
),
Index("ix_ledger_order_type", "order_id", "entry_type"),
)
class WebhookEvent(Base):
"""支付 Webhook 事件:按 Provider 事件 ID 幂等,允许乱序到达。"""
__tablename__ = "webhook_event"
provider_event_id: Mapped[str] = mapped_column(String, primary_key=True)
provider: Mapped[str] = mapped_column(String, nullable=False)
event_type: Mapped[str] = mapped_column(String, nullable=False)
payload_hash: Mapped[str] = mapped_column(String, nullable=False)
status: Mapped[str] = mapped_column(String, nullable=False, default="processed")
received_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint("status IN ('processed', 'ignored')", name="status_values"),
)
class CommunityContent(Base):
"""社区内容:帖子与角色 Release(WP-11)。
角色 Release 由服务端构造公开快照;私密记忆/关系/对话在结构上不进入发布物。
"""
__tablename__ = "community_content"
id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: new_id("content_")
)
author_domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False, index=True
)
kind: Mapped[str] = mapped_column(String, nullable=False, default="post")
title: Mapped[str] = mapped_column(String, nullable=False)
body: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
character_id: Mapped[str | None] = mapped_column(
ForeignKey("character.id", ondelete="set null"), nullable=True
)
visibility: Mapped[str] = mapped_column(String, nullable=False, default="public")
status: Mapped[str] = mapped_column(String, nullable=False, default="published")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
)
__table_args__ = (
CheckConstraint("kind IN ('post', 'character_release')", name="kind_values"),
CheckConstraint("visibility IN ('public', 'followers')", name="visibility_values"),
CheckConstraint(
"status IN ('draft', 'submitted', 'published', 'limited', 'suspended', 'removed')",
name="status_values",
),
)
class Comment(Base):
"""社区评论:平铺 + 可选父级;下架内容下的评论不可见。"""
__tablename__ = "comment"
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: new_id("comment_"))
content_id: Mapped[str] = mapped_column(
ForeignKey("community_content.id", ondelete="cascade"), nullable=False, index=True
)
author_domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False
)
parent_comment_id: Mapped[str | None] = mapped_column(
ForeignKey("comment.id", ondelete="cascade"), nullable=True
)
body: Mapped[str] = mapped_column(String, nullable=False)
status: Mapped[str] = mapped_column(String, nullable=False, default="visible")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint("status IN ('visible', 'removed')", name="status_values"),
)
class Reaction(Base):
"""互动:点赞/收藏;(content, actor, kind) 唯一防重复。"""
__tablename__ = "reaction"
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: new_id("reaction_"))
content_id: Mapped[str] = mapped_column(
ForeignKey("community_content.id", ondelete="cascade"), nullable=False, index=True
)
actor_domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False
)
kind: Mapped[str] = mapped_column(String, nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint("kind IN ('like', 'save')", name="kind_values"),
Index(
"uq_reaction_content_actor_kind",
"content_id", "actor_domain_id", "kind",
unique=True,
),
)
class Follow(Base):
"""关注关系(域→域)。"""
__tablename__ = "follow"
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: new_id("follow_"))
# 双 FK 同指 personal_domain:显式命名避免 convention 同名冲突
follower_domain_id: Mapped[str] = mapped_column(
ForeignKey(
"personal_domain.id", ondelete="cascade", name="fk_follow_follower_domain"
),
nullable=False,
index=True,
)
followee_domain_id: Mapped[str] = mapped_column(
ForeignKey(
"personal_domain.id", ondelete="cascade", name="fk_follow_followee_domain"
),
nullable=False,
index=True,
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
Index(
"uq_follow_pair", "follower_domain_id", "followee_domain_id", unique=True
),
)
class Report(Base):
"""举报:创建即生成司议案(治理队列入口)。"""
__tablename__ = "report"
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: new_id("report_"))
reporter_domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False, index=True
)
target_type: Mapped[str] = mapped_column(String, nullable=False)
target_id: Mapped[str] = mapped_column(String, nullable=False)
reason: Mapped[str] = mapped_column(String, nullable=False)
status: Mapped[str] = mapped_column(String, nullable=False, default="pending")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint("target_type IN ('content', 'comment')", name="target_type_values"),
CheckConstraint("status IN ('pending', 'reviewed', 'actioned')", name="status_values"),
)
class ModerationCase(Base):
"""司议案:由举报生成,处置动作与结果留痕。"""
__tablename__ = "moderation_case"
id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: new_id("moderation_")
)
report_id: Mapped[str] = mapped_column(
ForeignKey("report.id", ondelete="cascade"), nullable=False, index=True
)
status: Mapped[str] = mapped_column(String, nullable=False, default="pending")
action_taken: Mapped[str | None] = mapped_column(String, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint("status IN ('pending', 'resolved')", name="status_values"),
)
class Workspace(Base):
"""协作空间(WP-12):多人创作与权限边界。"""
__tablename__ = "workspace"
id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: new_id("workspace_")
)
name: Mapped[str] = mapped_column(String, nullable=False)
owner_domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False, index=True
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
class WorkspaceMember(Base):
"""成员与角色:编辑、评审、发布、财务权限分离(契约协作域)。"""
__tablename__ = "workspace_member"
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: new_id("member_"))
workspace_id: Mapped[str] = mapped_column(
ForeignKey("workspace.id", ondelete="cascade"), nullable=False, index=True
)
member_domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False
)
role: Mapped[str] = mapped_column(String, nullable=False, default="viewer")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint(
"role IN ('owner', 'editor', 'reviewer', 'viewer')", name="role_values"
),
Index("uq_workspace_member", "workspace_id", "member_domain_id", unique=True),
)
class WorkspaceInvitation(Base):
"""邀请:接受后入成员;status 单向流转。"""
__tablename__ = "workspace_invitation"
id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: new_id("invitation_")
)
workspace_id: Mapped[str] = mapped_column(
ForeignKey("workspace.id", ondelete="cascade"), nullable=False, index=True
)
invitee_domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False
)
role: Mapped[str] = mapped_column(String, nullable=False, default="editor")
status: Mapped[str] = mapped_column(String, nullable=False, default="pending")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint(
"role IN ('editor', 'reviewer', 'viewer')", name="role_values"
),
CheckConstraint(
"status IN ('pending', 'accepted', 'declined', 'expired')", name="status_values"
),
)
class Project(Base):
"""协作项目:条目由版本承载;发布需 owner 且版本已批准。"""
__tablename__ = "project"
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: new_id("project_"))
workspace_id: Mapped[str] = mapped_column(
ForeignKey("workspace.id", ondelete="cascade"), nullable=False, index=True
)
name: Mapped[str] = mapped_column(String, nullable=False)
kind: Mapped[str] = mapped_column(String, nullable=False, default="generic")
status: Mapped[str] = mapped_column(String, nullable=False, default="draft")
current_version_id: Mapped[str | None] = mapped_column(
ForeignKey("project_version.id", ondelete="set null", use_alter=True), nullable=True
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint(
"status IN ('draft', 'published')", name="status_values"
),
)
class ProjectVersion(Base):
"""项目版本:editor 提交 → reviewer 批准/拒绝;不可变演进。"""
__tablename__ = "project_version"
id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: new_id("project_version_")
)
project_id: Mapped[str] = mapped_column(
ForeignKey("project.id", ondelete="cascade"), nullable=False, index=True
)
version: Mapped[int] = mapped_column(nullable=False)
payload: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
status: Mapped[str] = mapped_column(String, nullable=False, default="draft")
submitted_by: Mapped[str | None] = mapped_column(String, nullable=True)
reviewed_by: Mapped[str | None] = mapped_column(String, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint(
"status IN ('draft', 'submitted', 'approved', 'rejected')", name="status_values"
),
Index("uq_project_version_number", "project_id", "version", unique=True),
)
class Plugin(Base):
"""插件(Registry 条目,WP-07):撤架阻断新调用,历史审计保留。"""
__tablename__ = "plugin"
name: Mapped[str] = mapped_column(String, primary_key=True) # 点分稳定名
publisher_domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False, index=True
)
kind: Mapped[str] = mapped_column(String, nullable=False, default="declarative")
status: Mapped[str] = mapped_column(String, nullable=False, default="published")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint(
"kind IN ('declarative', 'remote', 'isolated_process', 'container')",
name="kind_values",
),
CheckConstraint(
"status IN ('published', 'suspended', 'removed')", name="status_values"
),
)
class PluginVersion(Base):
"""插件版本:Manifest(权限/网络/数据声明)与内容签名(哈希)。"""
__tablename__ = "plugin_version"
id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: new_id("plugin_version_")
)
plugin_name: Mapped[str] = mapped_column(
ForeignKey("plugin.name", ondelete="cascade"), nullable=False, index=True
)
version: Mapped[str] = mapped_column(String, nullable=False)
manifest: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
content_hash: Mapped[str] = mapped_column(String, nullable=False)
status: Mapped[str] = mapped_column(String, nullable=False, default="published")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint(
"status IN ('published', 'suspended', 'removed')", name="status_values"
),
Index("uq_plugin_version", "plugin_name", "version", unique=True),
)
class PluginInstallation(Base):
"""安装:购买≠安装≠启用≠授权;启用必须显式确认权限子集。"""
__tablename__ = "plugin_installation"
id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: new_id("plugin_installation_")
)
domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False, index=True
)
plugin_name: Mapped[str] = mapped_column(
ForeignKey("plugin.name", ondelete="cascade"), nullable=False
)
version_id: Mapped[str] = mapped_column(
ForeignKey("plugin_version.id", ondelete="restrict"), nullable=False
)
enabled: Mapped[bool] = mapped_column(nullable=False, default=False)
granted_permissions: Mapped[list[str]] = mapped_column(JSONB, nullable=False, default=list)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
Index("uq_plugin_installation", "domain_id", "plugin_name", unique=True),
)
class Upload(Base):
"""上传会话:登记约束 → 直传对象存储 → 完成校验(契约第 9 节)。"""
__tablename__ = "upload"
id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: new_id("upload_")
)
domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False, index=True
)
filename: Mapped[str] = mapped_column(String, nullable=False)
media_type: Mapped[str] = mapped_column(String, nullable=False)
size_limit: Mapped[int] = mapped_column(nullable=False, default=100 * 1024 * 1024)
status: Mapped[str] = mapped_column(String, nullable=False, default="pending")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint("status IN ('pending', 'completed', 'failed')", name="status_values"),
)
class FileAsset(Base):
"""文件资产:未完成/隔离文件不可被业务引用(契约第 9 节)。"""
__tablename__ = "file_asset"
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: new_id("file_"))
domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False, index=True
)
upload_id: Mapped[str | None] = mapped_column(
ForeignKey("upload.id", ondelete="set null"), nullable=True
)
filename: Mapped[str] = mapped_column(String, nullable=False)
media_type: Mapped[str] = mapped_column(String, nullable=False)
size_bytes: Mapped[int] = mapped_column(nullable=False, default=0)
sha256: Mapped[str | None] = mapped_column(String, nullable=True)
storage_key: Mapped[str] = mapped_column(String, nullable=False)
status: Mapped[str] = mapped_column(String, nullable=False, default="pending")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint(
"status IN ('pending', 'ready', 'failed', 'quarantined')", name="status_values"
),
)
class MediaJob(Base):
"""媒体任务:ASR/TTS/图像等;无可用 Provider 时失败(fail-closed)。"""
__tablename__ = "media_job"
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: new_id("media_"))
domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False, index=True
)
kind: Mapped[str] = mapped_column(String, nullable=False)
input: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
output: Mapped[dict[str, Any] | None] = mapped_column(JSONB, nullable=True)
status: Mapped[str] = mapped_column(String, nullable=False, default="queued")
progress: Mapped[int] = mapped_column(nullable=False, default=0)
error: Mapped[str | None] = mapped_column(String, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
__table_args__ = (
CheckConstraint("kind IN ('asr', 'tts', 'image_gen', 'image_edit')", name="kind_values"),
CheckConstraint(
"status IN ('queued', 'running', 'succeeded', 'failed', 'cancelled')",
name="status_values",
),
)
class VoiceConsent(Base):
"""声契:声音授权(身份/用途/期限),撤回后停止新生成(契约媒体域)。"""
__tablename__ = "voice_consent"
id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: new_id("voice_consent_")
)
domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False, index=True
)
character_id: Mapped[str | None] = mapped_column(
ForeignKey("character.id", ondelete="cascade"), nullable=True
)
purpose: Mapped[str] = mapped_column(String, nullable=False, default="voice_clone")
expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
class DiagnosticGrant(Base):
"""诊断授权(ADR-004):双重、限时;明文只签发一次,库存哈希。"""
__tablename__ = "diagnostic_grant"
id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: new_id("diagnostic_grant_")
)
run_id: Mapped[str] = mapped_column(
ForeignKey("run.id", ondelete="cascade"), nullable=False, index=True
)
domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False
)
purpose: Mapped[str] = mapped_column(String, nullable=False)
token_hash: Mapped[str] = mapped_column(String, nullable=False, unique=True)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
class PluginExecution(Base):
"""插件沙箱执行记录(WP-06/07 Sandbox):结果与资源消耗如实留痕。"""
__tablename__ = "plugin_execution"
id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: new_id("execution_")
)
installation_id: Mapped[str] = mapped_column(
ForeignKey("plugin_installation.id", ondelete="cascade"), nullable=False, index=True
)
status: Mapped[str] = mapped_column(String, nullable=False, default="running")
exit_code: Mapped[int | None] = mapped_column(nullable=True)
duration_ms: Mapped[int | None] = mapped_column(nullable=True)
stdout_excerpt: Mapped[str | None] = mapped_column(String, nullable=True)
error: Mapped[str | None] = mapped_column(String, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
__table_args__ = (
CheckConstraint(
"status IN ('running', 'succeeded', 'failed', 'timeout', 'killed')",
name="status_values",
),
)
class AvatarModel(Base):
"""Avatar 模型资产(WP-09):live2d/vrm/gltf;校验结果与来源许可。"""
__tablename__ = "avatar_model"
id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: new_id("avatar_")
)
domain_id: Mapped[str] = mapped_column(
ForeignKey("personal_domain.id", ondelete="cascade"), nullable=False, index=True
)
character_id: Mapped[str | None] = mapped_column(
ForeignKey("character.id", ondelete="set null"), nullable=True
)
name: Mapped[str] = mapped_column(String, nullable=False)
format: Mapped[str] = mapped_column(String, nullable=False)
file_id: Mapped[str] = mapped_column(
ForeignKey("file_asset.id", ondelete="restrict"), nullable=False
)
status: Mapped[str] = mapped_column(String, nullable=False, default="pending")
validation_report: Mapped[dict[str, Any]] = mapped_column(
JSONB, nullable=False, default=dict
)
source_info: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
license_terms: Mapped[str | None] = mapped_column(String, nullable=True)
current_command_id: Mapped[str | None] = mapped_column(
ForeignKey("avatar_command.id", ondelete="set null", use_alter=True), nullable=True
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint("format IN ('live2d', 'vrm', 'gltf')", name="format_values"),
CheckConstraint(
"status IN ('pending', 'validated', 'rejected')", name="status_values"
),
)
class AvatarCommand(Base):
"""非破坏编辑命令历史(WP-09/ADR-006):命令与版本可扩展到 2.0 LLM 改模。"""
__tablename__ = "avatar_command"
id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: new_id("command_")
)
avatar_id: Mapped[str] = mapped_column(
ForeignKey("avatar_model.id", ondelete="cascade"), nullable=False, index=True
)
sequence: Mapped[int] = mapped_column(nullable=False)
command_type: Mapped[str] = mapped_column(String, nullable=False)
payload: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
undone: Mapped[bool] = mapped_column(nullable=False, default=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint(
"command_type IN ('param_map', 'expression', 'physics', 'motion', "
"'material', 'texture', 'bone', 'attachment', 'lighting', 'camera')",
name="command_type_values",
),
Index("uq_avatar_command_seq", "avatar_id", "sequence", unique=True),
)
+1
View File
@@ -0,0 +1 @@
"""运营域:诊断双授权、管理面治理、支持包与观测。"""
+147
View File
@@ -0,0 +1,147 @@
"""运营域路由:诊断、管理面、支持包与观测(WP-14)。"""
from typing import Annotated, Any
from fastapi import APIRouter, Depends, Header, HTTPException, Request, Response, status
from sqlalchemy import func, select
from sqlalchemy import text as sql_text
from app.auth.dependencies import get_current_user, require_state_change
from app.conversations.routes import _domain_of
from app.db.session import DbSession
from app.models import MediaJob, Message, Run, ToolCall, User
from app.ops import service
ops_router = APIRouter(prefix="/v1", tags=["ops-extended"])
admin_router = APIRouter(prefix="/v1/admin", tags=["admin"])
@ops_router.post("/runs/{run_id}/diagnostic-grants", status_code=201)
async def issue_grant(
run_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, str],
) -> dict[str, Any]:
"""签发诊断授权:明文 token 只返回一次;默认 15 分钟、单次、绑定 Run。"""
domain = await _domain_of(db, user)
grant, token = await service.issue_grant(
db, domain.id, run_id, body.get("purpose", ""),
getattr(request.state, "trace_id", None),
)
return {
"grant_id": grant.id, "token": token, # 唯一一次明文返回
"expires_at": grant.expires_at.isoformat(),
}
@ops_router.get("/runs/{run_id}/diagnostics")
async def read_diagnostics(
run_id: str,
db: DbSession,
user: Annotated[User, Depends(get_current_user)],
response: Response,
diag_token: Annotated[
str | None, Header(alias="Hunjing-Diagnostic-Token")
] = None,
diag_purpose: Annotated[
str | None, Header(alias="Hunjing-Diagnostic-Purpose")
] = None,
) -> dict[str, Any]:
"""双重授权诊断:登录态 + 诊断令牌;响应不落缓存(ADR-004)。"""
domain = await _domain_of(db, user)
if not diag_token or not diag_purpose:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "缺少诊断令牌或用途")
result = await service.read_diagnostics(
db, domain.id, run_id, diag_token, diag_purpose
)
response.headers["Cache-Control"] = "no-store, private"
return result
@ops_router.post("/support/bundles")
async def support_bundle(
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
) -> dict[str, Any]:
"""脱敏支持包:统计聚合,无正文与密钥。"""
domain = await _domain_of(db, user)
return await service.build_support_bundle(db, domain.id)
@admin_router.get("/moderation-cases")
async def list_cases(
db: DbSession,
user: Annotated[User, Depends(get_current_user)],
) -> dict[str, Any]:
await service.require_admin(db, user.id)
from app.models import Report
cases = await service.list_pending_cases(db)
out = []
for case in cases:
report = await db.get(Report, case.report_id)
out.append({
"case_id": case.id, "status": case.status,
"report": {
"target_type": report.target_type if report else None,
"target_id": report.target_id if report else None,
"reason": report.reason if report else None,
} if report else None,
})
return {"data": out}
@admin_router.post("/moderation-cases/{case_id}/resolutions")
async def resolve_case(
case_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, str],
) -> dict[str, Any]:
domain = await _domain_of(db, user)
await service.require_admin(db, user.id)
case = await service.resolve_case(
db, domain.id, case_id, body.get("action", ""),
getattr(request.state, "trace_id", None),
)
return {"case_id": case.id, "status": case.status, "action_taken": case.action_taken}
@ops_router.get("/readyz", tags=["ops"])
async def readyz(db: DbSession) -> dict[str, str]:
await db.execute(sql_text("SELECT 1"))
return {"status": "ready"}
@ops_router.get("/metrics", tags=["ops"])
async def metrics(db: DbSession) -> Response:
"""Prometheus 文本格式:真实 SQL 聚合(无正文)。"""
run_rows = (await db.execute(select(Run.status, func.count()).group_by(Run.status))).all()
msg_total = (await db.execute(select(func.count()).select_from(Message))).scalar_one()
tool_rows = (
await db.execute(select(ToolCall.status, func.count()).group_by(ToolCall.status))
).all()
media_rows = (
await db.execute(select(MediaJob.status, func.count()).group_by(MediaJob.status))
).all()
lines = ["# HELP hunjing_runs_total Run 总数(按状态)", "# TYPE hunjing_runs_total counter"]
for st, cnt in run_rows:
lines.append(f'hunjing_runs_total{{status="{st}"}} {int(cnt)}')
lines += [
"# HELP hunjing_messages_total 消息总数",
"# TYPE hunjing_messages_total counter",
f"hunjing_messages_total {int(msg_total)}",
]
groups = [
("hunjing_tool_calls_total", tool_rows),
("hunjing_media_jobs_total", media_rows),
]
for prefix, rows in groups:
lines.append(f"# HELP {prefix} 计数(按状态)")
lines.append(f"# TYPE {prefix} counter")
for st, cnt in rows:
lines.append(f'{prefix}{{status="{st}"}} {int(cnt)}')
return Response(content="\n".join(lines) + "\n", media_type="text/plain")
+238
View File
@@ -0,0 +1,238 @@
"""运营域:诊断双授权、管理面治理、支持包与观测(WP-14 / ADR-004)。"""
import hashlib
import secrets
from datetime import UTC, datetime, timedelta
from typing import Any
from fastapi import HTTPException, status
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models import (
AuditEvent,
Comment,
CommunityContent,
DiagnosticGrant,
MediaJob,
Message,
ModelCall,
ModerationCase,
Report,
Run,
ToolCall,
Turn,
)
GRANT_TTL = timedelta(minutes=15) # ADR-004:默认 15 分钟、单次、绑定 Run
def _hash_token(token: str) -> str:
return hashlib.sha256(token.encode("utf-8")).hexdigest()
async def issue_grant(
db: AsyncSession, domain_id: str, run_id: str, purpose: str, trace_id: str | None
) -> tuple[DiagnosticGrant, str]:
"""签发诊断授权:明文 token 只返回这一次,数据库只存哈希。"""
if not purpose.strip():
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "purpose 不能为空")
run = await db.get(Run, run_id)
if run is None or run.personal_domain_id != domain_id:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Run 不存在")
token = f"hjd_{secrets.token_urlsafe(32)}"
grant = DiagnosticGrant(
run_id=run_id,
domain_id=domain_id,
purpose=purpose,
token_hash=_hash_token(token),
expires_at=datetime.now(UTC) + GRANT_TTL,
)
db.add(grant)
await db.flush()
db.add(
AuditEvent(
actor_type="user", actor_id=domain_id, action="diagnostic.grant_issued",
aggregate_type="diagnostic_grant", aggregate_id=grant.id,
payload={"run_id": run_id, "purpose": purpose}, trace_id=trace_id,
)
)
await db.flush()
return grant, token
async def read_diagnostics(
db: AsyncSession,
domain_id: str,
run_id: str,
token: str,
purpose: str,
) -> dict[str, Any]:
"""双重授权读取:登录态 + 诊断授权(用途匹配、未过期、单次)。
返回运行观测摘要(usage/TTFT/工具轨迹);不含消息正文(结构保证)。
"""
run = await db.get(Run, run_id)
if run is None or run.personal_domain_id != domain_id:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Run 不存在")
grant = (
await db.execute(
select(DiagnosticGrant).where(
DiagnosticGrant.run_id == run_id,
DiagnosticGrant.token_hash == _hash_token(token),
)
)
).scalar_one_or_none()
if grant is None:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "诊断授权无效")
if grant.used_at is not None:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "诊断授权已使用")
if grant.expires_at <= datetime.now(UTC):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "诊断授权已过期")
if grant.purpose != purpose:
raise HTTPException(status.HTTP_403_FORBIDDEN, "用途与授权不符")
grant.used_at = datetime.now(UTC) # 单次:读取即消耗
db.add(
AuditEvent(
actor_type="user", actor_id=domain_id, action="diagnostic.read",
aggregate_type="diagnostic_grant", aggregate_id=grant.id,
payload={"run_id": run_id}, trace_id=None,
)
)
turns = list(
(
await db.scalars(
select(Turn).where(Turn.run_id == run_id).order_by(Turn.index.asc())
)
).all()
)
calls = list(
(
await db.scalars(
select(ModelCall).where(ModelCall.turn_id.in_([t.id for t in turns]))
)
).all()
)
tools = list(
(
await db.scalars(select(ToolCall).where(ToolCall.run_id == run_id))
).all()
)
return {
"run": {"id": run_id, "status": run.status},
"turns": [
{
"index": t.index,
"model_calls": [
{
"status": c.status, "usage": c.usage, "ttft_ms": c.ttft_ms,
"error": c.error,
}
for c in calls if c.turn_id == t.id
],
}
for t in turns
],
"tool_calls": [
{"tool": tc.tool_name, "status": tc.status, "error": tc.error}
for tc in tools
],
}
async def require_admin(db: AsyncSession, user_id: str) -> None:
from app.models import User
user = await db.get(User, user_id)
if user is None or user.role != "admin":
raise HTTPException(status.HTTP_403_FORBIDDEN, "需要管理员角色")
async def list_pending_cases(db: AsyncSession) -> list[ModerationCase]:
return list(
(
await db.scalars(
select(ModerationCase)
.where(ModerationCase.status == "pending")
.order_by(ModerationCase.created_at.asc())
.limit(100)
)
).all()
)
async def resolve_case(
db: AsyncSession,
admin_domain_id: str,
case_id: str,
action: str,
trace_id: str | None,
) -> ModerationCase:
"""处置司议案:content_removed 联动下架目标内容(治理闭环)。"""
case = await db.get(ModerationCase, case_id)
if case is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "案件不存在")
if case.status != "pending":
raise HTTPException(status.HTTP_409_CONFLICT, "案件已处理")
if action == "content_removed":
report = await db.get(Report, case.report_id)
assert report is not None
if report.target_type == "content":
content = await db.get(CommunityContent, report.target_id)
if content is not None:
content.status = "removed" # 下架传播(列表/详情/评论不可见)
elif report.target_type == "comment":
comment = await db.get(Comment, report.target_id)
if comment is not None:
comment.status = "removed"
case.status = "resolved"
case.action_taken = action
db.add(
AuditEvent(
actor_type="admin", actor_id=admin_domain_id, action="moderation.resolved",
aggregate_type="moderation_case", aggregate_id=case.id,
payload={"action": action}, trace_id=trace_id,
)
)
await db.flush()
return case
async def build_support_bundle(db: AsyncSession, domain_id: str) -> dict[str, Any]:
"""脱敏支持包:统计与错误码聚合,无正文、无密钥(契约运营域)。"""
run_rows = (
await db.execute(
select(Run.status, func.count())
.where(Run.personal_domain_id == domain_id)
.group_by(Run.status)
)
).all()
message_count = (
await db.execute(
select(func.count()).select_from(Message).join(
Run, Message.run_id == Run.id
).where(Run.personal_domain_id == domain_id)
)
).scalar_one()
tool_rows = (
await db.execute(
select(ToolCall.status, func.count())
.join(Run, ToolCall.run_id == Run.id)
.where(Run.personal_domain_id == domain_id)
.group_by(ToolCall.status)
)
).all()
media_rows = (
await db.execute(
select(MediaJob.status, func.count())
.where(MediaJob.domain_id == domain_id)
.group_by(MediaJob.status)
)
).all()
return {
"generated_at": datetime.now(UTC).isoformat(),
"runs_by_status": {s: int(c) for s, c in run_rows},
"messages_total": int(message_count),
"tool_calls_by_status": {s: int(c) for s, c in tool_rows},
"media_jobs_by_status": {s: int(c) for s, c in media_rows},
}
+1
View File
@@ -0,0 +1 @@
"""插件域:Registry、安装三态与调用门。"""
+170
View File
@@ -0,0 +1,170 @@
"""插件域路由(WP-07)。"""
from typing import Annotated, Any
from fastapi import APIRouter, Depends, Request
from sqlalchemy import select
from app.auth.dependencies import get_current_user, require_state_change
from app.conversations.routes import _domain_of
from app.db.session import DbSession
from app.models import Plugin, PluginVersion, User
from app.plugins import service
router = APIRouter(prefix="/v1/plugins", tags=["plugins"])
def _plugin_out(p: Plugin) -> dict[str, Any]:
return {
"name": p.name, "kind": p.kind, "status": p.status,
"publisher_domain_id": p.publisher_domain_id,
}
def _version_out(v: PluginVersion) -> dict[str, Any]:
return {
"id": v.id, "plugin_name": v.plugin_name, "version": v.version,
"manifest": v.manifest, "content_hash": v.content_hash, "status": v.status,
}
def _installation_out(i: Any) -> dict[str, Any]:
return {
"id": i.id, "plugin_name": i.plugin_name, "version_id": i.version_id,
"enabled": i.enabled, "granted_permissions": i.granted_permissions,
}
@router.post("/versions", status_code=201)
async def publish(
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, Any],
) -> dict[str, Any]:
domain = await _domain_of(db, user)
_, pv = await service.publish_version(
db, domain.id, body, getattr(request.state, "trace_id", None)
)
return _version_out(pv)
@router.get("")
async def list_plugins(
db: DbSession, user: Annotated[User, Depends(get_current_user)]
) -> dict[str, Any]:
rows = list(
(
await db.scalars(
select(Plugin).where(Plugin.status == "published").limit(50)
)
).all()
)
return {"data": [_plugin_out(p) for p in rows]}
@router.post("/{name}/withdrawals")
async def withdraw(
name: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
) -> dict[str, Any]:
domain = await _domain_of(db, user)
plugin = await service.withdraw(
db, domain.id, name, getattr(request.state, "trace_id", None)
)
return _plugin_out(plugin)
@router.post("/{name}/installations", status_code=201)
async def install(
name: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, str],
) -> dict[str, Any]:
domain = await _domain_of(db, user)
installation = await service.install(
db, domain.id, name, body.get("version", ""),
getattr(request.state, "trace_id", None),
)
return _installation_out(installation)
inst_router = APIRouter(prefix="/v1/plugin-installations", tags=["plugins"])
@inst_router.post("/{installation_id}/enablings", status_code=201)
async def enable(
installation_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, Any],
) -> dict[str, Any]:
domain = await _domain_of(db, user)
installation = await service.enable(
db, domain.id, installation_id, body.get("granted_permissions", []),
getattr(request.state, "trace_id", None),
)
return _installation_out(installation)
@inst_router.post("/{installation_id}/upgrades", status_code=201)
async def upgrade(
installation_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, Any],
) -> dict[str, Any]:
domain = await _domain_of(db, user)
installation = await service.upgrade(
db, domain.id, installation_id, body.get("version", ""),
body.get("granted_permissions"), getattr(request.state, "trace_id", None),
)
return _installation_out(installation)
invoke_router = APIRouter(prefix="/v1/plugins", tags=["plugins"])
@invoke_router.post("/{name}/invocations")
async def invoke(
name: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, Any],
) -> dict[str, Any]:
domain = await _domain_of(db, user)
return await service.invoke(
db, domain.id, name, body.get("input", {}), body.get("run_id"),
getattr(request.state, "trace_id", None),
)
@inst_router.post("/{installation_id}/executions", status_code=201)
async def execute_plugin(
installation_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, Any],
) -> dict[str, Any]:
"""沙箱执行 isolated 插件:未启用沙箱一律 503(fail-closed)。"""
domain = await _domain_of(db, user)
from app.core.config import get_settings
execution, result = await service.execute_isolated(
db, domain.id, installation_id, body.get("input", {}),
sandbox_enabled=get_settings().sandbox_enabled,
trace_id=getattr(request.state, "trace_id", None),
)
return {
"execution_id": execution.id, "status": result["status"],
"duration_ms": result.get("duration_ms"),
"output": result.get("output"), "error": result.get("error"),
}
+366
View File
@@ -0,0 +1,366 @@
"""插件域:Registry、安装三态与调用门(WP-07 / ADR-001/008)。"""
import hashlib
import json
from typing import Any
from fastapi import HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models import AuditEvent, Plugin, PluginInstallation, PluginVersion
def _hash_manifest(manifest: dict[str, Any]) -> str:
canonical = json.dumps(manifest, sort_keys=True, ensure_ascii=False)
return f"sha256:{hashlib.sha256(canonical.encode('utf-8')).hexdigest()}"
def _manifest_permissions(manifest: dict[str, Any]) -> set[str]:
"""权限标识统一为 group:item(如 network:api.example.com)。"""
perms = manifest.get("permissions", {})
result: set[str] = set()
if isinstance(perms, dict):
for group, items in perms.items():
if isinstance(items, list):
result.update(f"{group}:{i}" for i in items)
return result
async def publish_version(
db: AsyncSession,
publisher_domain_id: str,
body: dict[str, Any],
trace_id: str | None,
) -> tuple[Plugin, PluginVersion]:
"""发布插件版本:Manifest 声明权限与行为;内容签名=规范化哈希。
安装展示数据、网络、设备、费用、外部目的地与现实后果(ADR/契约)。
"""
name = str(body.get("name", "")).strip()
if not name or "." not in name:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "name 须为点分稳定名")
version = str(body.get("version", "")).strip()
if not version:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "version 不能为空")
manifest = body.get("manifest", {})
if not isinstance(manifest, dict) or "permissions" not in manifest:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "manifest 必须声明 permissions")
plugin = await db.get(Plugin, name)
if plugin is None:
plugin = Plugin(
name=name,
publisher_domain_id=publisher_domain_id,
kind=str(body.get("kind", "declarative")),
)
db.add(plugin)
await db.flush()
elif plugin.publisher_domain_id != publisher_domain_id:
raise HTTPException(status.HTTP_409_CONFLICT, "插件名已被其他发布者占用")
existing = await db.execute(
select(PluginVersion).where(
PluginVersion.plugin_name == name, PluginVersion.version == version
)
)
if existing.scalar_one_or_none() is not None:
raise HTTPException(status.HTTP_409_CONFLICT, "版本已存在")
pv = PluginVersion(
plugin_name=name,
version=version,
manifest=manifest,
content_hash=_hash_manifest(manifest),
)
db.add(pv)
await db.flush()
db.add(
AuditEvent(
actor_type="user", actor_id=publisher_domain_id, action="plugin.published",
aggregate_type="plugin", aggregate_id=name,
payload={"version": version, "hash": pv.content_hash}, trace_id=trace_id,
)
)
await db.flush()
return plugin, pv
async def withdraw(
db: AsyncSession, publisher_domain_id: str, name: str, trace_id: str | None
) -> Plugin:
"""安全撤架:阻断新调用,保留历史审计与安装记录(不静默删除)。"""
plugin = await db.get(Plugin, name)
if plugin is None or plugin.publisher_domain_id != publisher_domain_id:
raise HTTPException(status.HTTP_404_NOT_FOUND, "插件不存在")
plugin.status = "removed"
await db.execute(
select(PluginVersion).where(PluginVersion.plugin_name == name) # 触发加载
)
for pv in (
await db.scalars(select(PluginVersion).where(PluginVersion.plugin_name == name))
).all():
pv.status = "removed"
db.add(
AuditEvent(
actor_type="user", actor_id=publisher_domain_id, action="plugin.withdrawn",
aggregate_type="plugin", aggregate_id=name, trace_id=trace_id,
)
)
await db.flush()
return plugin
async def install(
db: AsyncSession,
domain_id: str,
name: str,
version: str,
trace_id: str | None,
) -> PluginInstallation:
"""安装:默认 disabled、零授权——购买≠安装≠启用≠授权(WP-07 验证项)。"""
plugin = await db.get(Plugin, name)
if plugin is None or plugin.status != "published":
raise HTTPException(status.HTTP_404_NOT_FOUND, "插件不存在或已撤架")
existing_install = await db.execute(
select(PluginInstallation).where(
PluginInstallation.domain_id == domain_id,
PluginInstallation.plugin_name == name,
)
)
if existing_install.scalar_one_or_none() is not None:
raise HTTPException(status.HTTP_409_CONFLICT, "已安装(升级请走升级端点)")
pv = (
await db.execute(
select(PluginVersion).where(
PluginVersion.plugin_name == name,
PluginVersion.version == version,
PluginVersion.status == "published",
)
)
).scalar_one_or_none()
if pv is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "版本不存在或不可用")
installation = PluginInstallation(
domain_id=domain_id, plugin_name=name, version_id=pv.id,
enabled=False, granted_permissions=[],
)
db.add(installation)
await db.flush()
db.add(
AuditEvent(
actor_type="user", actor_id=domain_id, action="plugin.installed",
aggregate_type="plugin", aggregate_id=name,
payload={"version": version, "enabled": False}, trace_id=trace_id,
)
)
await db.flush()
return installation
async def require_installation(
db: AsyncSession, domain_id: str, installation_id: str
) -> PluginInstallation:
installation = await db.get(PluginInstallation, installation_id)
if installation is None or installation.domain_id != domain_id:
raise HTTPException(status.HTTP_404_NOT_FOUND, "安装不存在")
return installation
async def enable(
db: AsyncSession,
domain_id: str,
installation_id: str,
granted_permissions: list[str],
trace_id: str | None,
) -> PluginInstallation:
"""启用:显式确认权限(授予集必须是 Manifest 声明的子集)。"""
installation = await require_installation(db, domain_id, installation_id)
pv = await db.get(PluginVersion, installation.version_id)
assert pv is not None
declared = _manifest_permissions(pv.manifest)
granted = set(granted_permissions)
if not granted.issubset(declared):
raise HTTPException(
status.HTTP_422_UNPROCESSABLE_ENTITY,
f"授权超出 Manifest 声明:{sorted(granted - declared)}",
)
installation.enabled = True
installation.granted_permissions = sorted(granted)
db.add(
AuditEvent(
actor_type="user", actor_id=domain_id, action="plugin.enabled",
aggregate_type="plugin", aggregate_id=installation.plugin_name,
payload={"granted": sorted(granted)}, trace_id=trace_id,
)
)
await db.flush()
return installation
async def upgrade(
db: AsyncSession,
domain_id: str,
installation_id: str,
version: str,
granted_permissions: list[str] | None,
trace_id: str | None,
) -> PluginInstallation:
"""升级:Manifest 新增权限必须重新确认(granted 覆盖新增项,否则 422)。"""
installation = await require_installation(db, domain_id, installation_id)
current = await db.get(PluginVersion, installation.version_id)
assert current is not None
target = (
await db.execute(
select(PluginVersion).where(
PluginVersion.plugin_name == installation.plugin_name,
PluginVersion.version == version,
PluginVersion.status == "published",
)
)
).scalar_one_or_none()
if target is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "目标版本不存在")
old_declared = _manifest_permissions(current.manifest)
new_declared = _manifest_permissions(target.manifest)
newly_added = new_declared - old_declared
granted = (
set(granted_permissions) if granted_permissions is not None
else set(installation.granted_permissions)
)
if newly_added and not newly_added.issubset(granted):
raise HTTPException(
status.HTTP_422_UNPROCESSABLE_ENTITY,
f"新版本新增权限需要重新确认:{sorted(newly_added - granted)}",
)
if not granted.issubset(new_declared):
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "授权超出 Manifest 声明")
installation.version_id = target.id
installation.granted_permissions = sorted(granted)
db.add(
AuditEvent(
actor_type="user", actor_id=domain_id, action="plugin.upgraded",
aggregate_type="plugin", aggregate_id=installation.plugin_name,
payload={"to_version": version, "newly_added": sorted(newly_added)},
trace_id=trace_id,
)
)
await db.flush()
return installation
async def invoke(
db: AsyncSession,
domain_id: str,
name: str,
input_: dict[str, Any],
run_id: str | None,
trace_id: str | None,
) -> dict[str, Any]:
"""调用门:撤架/未启用/未授权逐一校验;按 Manifest 声明真实执行。
声明式插件执行 response_template 回填(确定性、无副作用);
remote/isolated 形态的传输执行随 WP-14 沙箱交付,当前未安装即拒绝。
"""
plugin = await db.get(Plugin, name)
if plugin is None or plugin.status != "published":
raise HTTPException(status.HTTP_403_FORBIDDEN, "插件不可用(已撤架或不存在)")
installation = (
await db.execute(
select(PluginInstallation).where(
PluginInstallation.domain_id == domain_id,
PluginInstallation.plugin_name == name,
)
)
).scalar_one_or_none()
if installation is None:
raise HTTPException(status.HTTP_403_FORBIDDEN, "未安装")
if not installation.enabled:
raise HTTPException(status.HTTP_403_FORBIDDEN, "未启用")
pv = await db.get(PluginVersion, installation.version_id)
assert pv is not None
if pv.status != "published":
raise HTTPException(status.HTTP_403_FORBIDDEN, "已安装版本已撤架")
if plugin.kind != "declarative":
raise HTTPException(
status.HTTP_501_NOT_IMPLEMENTED, "该形态执行随沙箱 Runtime 交付(WP-14)"
)
template = str(pv.manifest.get("response_template", ""))
output = {"executed": True, "runtime": "declarative", "response": template}
db.add(
AuditEvent(
actor_type="user", actor_id=domain_id, action="plugin.invoked",
aggregate_type="plugin", aggregate_id=name,
payload={
"version": pv.version, "run_id": run_id,
"version_hash": pv.content_hash, # 快照语义:审计可追溯当时版本
},
trace_id=trace_id,
)
)
await db.flush()
return output
async def execute_isolated(
db: AsyncSession,
domain_id: str,
installation_id: str,
input_: dict[str, Any],
sandbox_enabled: bool,
trace_id: str | None,
) -> tuple[Any, dict[str, Any]]:
"""隔离执行:isolated 形态插件经沙箱子进程运行;未启用沙箱一律拒绝。
返回(执行记录, 结果)。代码体取自 Manifest 的 code 字段
(部署者明确信任的本地代码插件,仍受资源限制约束)。
"""
from app.models import PluginExecution
from app.sandbox.executor import run_sandboxed
installation = await require_installation(db, domain_id, installation_id)
plugin = await db.get(Plugin, installation.plugin_name)
assert plugin is not None
if plugin.kind != "isolated_process":
raise HTTPException(
status.HTTP_422_UNPROCESSABLE_ENTITY, "仅 isolated_process 形态走沙箱执行"
)
if not installation.enabled:
raise HTTPException(status.HTTP_403_FORBIDDEN, "插件未启用")
if not sandbox_enabled:
db.add(
AuditEvent(
actor_type="user", actor_id=domain_id, action="plugin.execution_denied",
aggregate_type="plugin", aggregate_id=plugin.name,
payload={"reason": "sandbox_not_enabled"}, trace_id=trace_id,
)
)
await db.flush()
raise HTTPException(
status.HTTP_503_SERVICE_UNAVAILABLE,
"沙箱未启用:拒绝执行(fail-closed),启用请设置 HUNJING_SANDBOX_ENABLED",
)
pv = await db.get(PluginVersion, installation.version_id)
assert pv is not None
code = str(pv.manifest.get("code", ""))
execution = PluginExecution(installation_id=installation.id, status="running")
db.add(execution)
await db.flush()
result = await run_sandboxed(code, input_)
execution.status = result["status"]
execution.exit_code = result.get("exit_code")
execution.duration_ms = result.get("duration_ms")
execution.stdout_excerpt = result.get("stdout_excerpt")
execution.error = result.get("error")
from datetime import UTC
from datetime import datetime as _dt
execution.completed_at = _dt.now(UTC)
db.add(
AuditEvent(
actor_type="user", actor_id=domain_id, action="plugin.executed",
aggregate_type="plugin", aggregate_id=plugin.name,
payload={"execution_id": execution.id, "status": execution.status,
"duration_ms": execution.duration_ms},
trace_id=trace_id,
)
)
await db.flush()
return execution, result
+1
View File
@@ -0,0 +1 @@
"""沙箱域:隔离执行器与策略。"""
+109
View File
@@ -0,0 +1,109 @@
"""沙箱执行器(WP-06/07 Sandbox):子进程隔离 + 资源限制 + 超时。
隔离边界(按部署档):
- 本执行器提供:独立进程、CPU/内存 rlimit、墙钟超时与强杀、输出限额。
- 进程级隔离不是完整安全边界;网络隔离与文件系统最小化在 Linux 容器档
(container 形态)交付。部署者必须显式启用沙箱(HUNJING_SANDBOX_ENABLED),
未启用时一律拒绝执行(fail-closed,AGENTS 第 6 节)。
"""
import asyncio
import json
import resource
import sys
import time
from dataclasses import dataclass
from typing import Any
# 用户代码经 stdin JSON 传输,不进命令行(避免注入命令行参数)
_RUNNER = r'''
import json, sys
def _main():
payload = json.loads(sys.stdin.read())
scope = {"input": payload.get("input", {})}
exec(compile(payload["code"], "<plugin>", "exec"), scope)
result = scope.get("output")
sys.stdout.write(json.dumps({"output": result}, ensure_ascii=False))
_main()
'''
@dataclass(frozen=True)
class SandboxPolicy:
max_runtime_ms: int = 5_000
max_memory_mb: int = 128
max_output_chars: int = 10_000
class SandboxUnavailable(Exception):
"""未启用沙箱:调用方必须显式失败,不得退化为宿主执行。"""
def _apply_limits(policy: SandboxPolicy) -> None: # 在子进程内执行(preexec_fn)
# RLIMIT_AS 在部分平台(macOS)不可设:尽力而为,失败不阻断进程隔离
cpu_seconds = max(1, policy.max_runtime_ms // 1000 + 1)
resource.setrlimit(resource.RLIMIT_CPU, (cpu_seconds, cpu_seconds))
if hasattr(resource, "RLIMIT_AS"):
as_bytes = policy.max_memory_mb * 1024 * 1024
try:
resource.setrlimit(resource.RLIMIT_AS, (as_bytes, as_bytes))
except (ValueError, OSError):
pass
async def run_sandboxed(
code: str, input_: dict[str, Any], policy: SandboxPolicy | None = None
) -> dict[str, Any]:
"""在受限子进程中执行插件代码;返回执行结果(不抛业务异常以外的错误)。"""
pol = policy or SandboxPolicy()
stdin_payload = json.dumps({"code": code, "input": input_}, ensure_ascii=False)
started = time.monotonic()
try:
proc = await asyncio.create_subprocess_exec(
sys.executable, "-c", _RUNNER,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
preexec_fn=lambda: _apply_limits(pol),
)
except Exception as exc: # 进程无法启动:如实失败
return {
"status": "failed", "exit_code": None,
"duration_ms": int((time.monotonic() - started) * 1000),
"output": None, "error": f"sandbox_spawn_failed: {exc}",
}
try:
stdout, stderr = await asyncio.wait_for(
proc.communicate(stdin_payload.encode("utf-8")),
timeout=pol.max_runtime_ms / 1000,
)
except TimeoutError:
proc.kill()
await proc.wait()
return {
"status": "timeout", "exit_code": None,
"duration_ms": int((time.monotonic() - started) * 1000),
"output": None, "error": f"exceeded {pol.max_runtime_ms}ms wall clock",
}
duration_ms = int((time.monotonic() - started) * 1000)
out_text = stdout.decode("utf-8", "replace")[: pol.max_output_chars]
err_text = stderr.decode("utf-8", "replace")[: pol.max_output_chars]
if proc.returncode == 0:
try:
parsed = json.loads(out_text)
except json.JSONDecodeError:
return {
"status": "failed", "exit_code": proc.returncode,
"duration_ms": duration_ms, "output": None,
"error": "plugin output is not JSON",
}
return {
"status": "succeeded", "exit_code": 0,
"duration_ms": duration_ms, "output": parsed.get("output"),
"stdout_excerpt": out_text, "error": None,
}
return {
"status": "failed", "exit_code": proc.returncode,
"duration_ms": duration_ms, "output": None,
"stdout_excerpt": out_text, "error": err_text or f"exit {proc.returncode}",
}
+1
View File
@@ -0,0 +1 @@
"""协作域:Workspace、成员、邀请与项目评审。"""
+181
View File
@@ -0,0 +1,181 @@
"""协作域路由(WP-12)。"""
from typing import Annotated, Any
from fastapi import APIRouter, Depends, HTTPException, Request, status
from app.auth.dependencies import get_current_user, require_state_change
from app.conversations.routes import _domain_of
from app.db.session import DbSession
from app.models import PersonalDomain, User
from app.workspaces import service
router = APIRouter(prefix="/v1/workspaces", tags=["workspaces"])
def _member_out(m: Any) -> dict[str, Any]:
return {
"id": m.id,
"workspace_id": m.workspace_id,
"member_domain_id": m.member_domain_id,
"role": m.role,
"created_at": m.created_at.isoformat(),
}
def _project_out(p: Any) -> dict[str, Any]:
return {
"id": p.id,
"workspace_id": p.workspace_id,
"name": p.name,
"status": p.status,
"current_version_id": p.current_version_id,
"created_at": p.created_at.isoformat(),
}
def _version_out(v: Any) -> dict[str, Any]:
return {
"id": v.id,
"project_id": v.project_id,
"version": v.version,
"payload": v.payload,
"status": v.status,
"submitted_by": v.submitted_by,
"reviewed_by": v.reviewed_by,
"created_at": v.created_at.isoformat(),
}
@router.post("", status_code=201)
async def create_workspace(
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, str],
) -> dict[str, Any]:
domain = await _domain_of(db, user)
ws = await service.create_workspace(
db, domain.id, body.get("name", ""), getattr(request.state, "trace_id", None)
)
return {"id": ws.id, "name": ws.name, "owner_domain_id": ws.owner_domain_id}
@router.post("/{workspace_id}/invitations", status_code=201)
async def invite(
workspace_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, str],
) -> dict[str, Any]:
domain = await _domain_of(db, user)
invitee = await db.get(PersonalDomain, body.get("invitee_domain_id", ""))
if invitee is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "被邀用户不存在")
invitation = await service.invite(
db, domain.id, workspace_id, invitee.id, body.get("role", "editor"),
getattr(request.state, "trace_id", None),
)
return {"id": invitation.id, "status": invitation.status, "role": invitation.role}
@router.post("/invitations/{invitation_id}/acceptances", status_code=201)
async def accept(
invitation_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
) -> dict[str, Any]:
domain = await _domain_of(db, user)
member = await service.accept_invitation(
db, domain.id, invitation_id, getattr(request.state, "trace_id", None)
)
return _member_out(member)
@router.get("/{workspace_id}/members")
async def list_members(
workspace_id: str,
db: DbSession,
user: Annotated[User, Depends(get_current_user)],
) -> dict[str, Any]:
domain = await _domain_of(db, user)
rows = await service.list_members(db, domain.id, workspace_id)
return {"data": [_member_out(m) for m in rows]}
@router.delete("/{workspace_id}/members/{member_id}", status_code=204)
async def remove_member(
workspace_id: str,
member_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
) -> None:
domain = await _domain_of(db, user)
await service.remove_member(
db, domain.id, workspace_id, member_id, getattr(request.state, "trace_id", None)
)
@router.post("/{workspace_id}/projects", status_code=201)
async def create_project(
workspace_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, str],
) -> dict[str, Any]:
domain = await _domain_of(db, user)
project = await service.create_project(
db, domain.id, workspace_id, body.get("name", ""),
getattr(request.state, "trace_id", None),
)
return _project_out(project)
@router.post("/projects/{project_id}/versions", status_code=201)
async def submit_version(
project_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, Any],
) -> dict[str, Any]:
domain = await _domain_of(db, user)
version = await service.submit_version(
db, domain.id, project_id, body.get("payload", {}),
getattr(request.state, "trace_id", None),
)
return _version_out(version)
@router.post("/project-versions/{version_id}/reviews", status_code=201)
async def review_version(
version_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
body: dict[str, bool],
) -> dict[str, Any]:
domain = await _domain_of(db, user)
version = await service.review_version(
db, domain.id, version_id, bool(body.get("approve")),
getattr(request.state, "trace_id", None),
)
return _version_out(version)
@router.post("/projects/{project_id}/publications", status_code=200)
async def publish(
project_id: str,
db: DbSession,
user: Annotated[User, Depends(require_state_change)],
request: Request,
) -> dict[str, Any]:
domain = await _domain_of(db, user)
project = await service.publish_project(
db, domain.id, project_id, getattr(request.state, "trace_id", None)
)
return _project_out(project)
+272
View File
@@ -0,0 +1,272 @@
"""协作域:Workspace、成员 RBAC、邀请与项目评审(WP-12)。"""
from typing import Any
from fastapi import HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models import (
AuditEvent,
Project,
ProjectVersion,
Workspace,
WorkspaceInvitation,
WorkspaceMember,
)
# 角色能力矩阵:编辑(提交版本)、评审(批准/拒绝)、发布(owner)
ROLE_CAN_SUBMIT = {"owner", "editor"}
ROLE_CAN_REVIEW = {"owner", "reviewer"}
async def require_member(
db: AsyncSession, domain_id: str, workspace_id: str, roles: set[str] | None = None
) -> WorkspaceMember:
"""成员校验:非成员或角色不足一律 404/403(跨 Workspace fail-closed)。"""
member = (
await db.execute(
select(WorkspaceMember).where(
WorkspaceMember.workspace_id == workspace_id,
WorkspaceMember.member_domain_id == domain_id,
)
)
).scalar_one_or_none()
if member is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "工作区不存在")
if roles is not None and member.role not in roles:
raise HTTPException(status.HTTP_403_FORBIDDEN, f"角色 {member.role} 无权执行此操作")
return member
async def create_workspace(
db: AsyncSession, domain_id: str, name: str, trace_id: str | None
) -> Workspace:
if not name.strip():
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "name 不能为空")
ws = Workspace(name=name, owner_domain_id=domain_id)
db.add(ws)
await db.flush()
db.add(
WorkspaceMember(workspace_id=ws.id, member_domain_id=domain_id, role="owner")
)
db.add(
AuditEvent(
actor_type="user", actor_id=domain_id, action="workspace.created",
aggregate_type="workspace", aggregate_id=ws.id, trace_id=trace_id,
)
)
await db.flush()
return ws
async def invite(
db: AsyncSession,
owner_domain_id: str,
workspace_id: str,
invitee_domain_id: str,
role: str,
trace_id: str | None,
) -> WorkspaceInvitation:
await require_member(db, owner_domain_id, workspace_id, {"owner"})
if role not in {"editor", "reviewer", "viewer"}:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "role 取值不合法")
existing_member = (
await db.execute(
select(WorkspaceMember).where(
WorkspaceMember.workspace_id == workspace_id,
WorkspaceMember.member_domain_id == invitee_domain_id,
)
)
).scalar_one_or_none()
if existing_member is not None:
raise HTTPException(status.HTTP_409_CONFLICT, "已是成员")
invitation = WorkspaceInvitation(
workspace_id=workspace_id, invitee_domain_id=invitee_domain_id, role=role
)
db.add(invitation)
await db.flush()
db.add(
AuditEvent(
actor_type="user", actor_id=owner_domain_id, action="workspace.invited",
aggregate_type="workspace", aggregate_id=workspace_id,
payload={"invitee": invitee_domain_id, "role": role}, trace_id=trace_id,
)
)
await db.flush()
return invitation
async def accept_invitation(
db: AsyncSession, domain_id: str, invitation_id: str, trace_id: str | None
) -> WorkspaceMember:
invitation = await db.get(WorkspaceInvitation, invitation_id)
if invitation is None or invitation.invitee_domain_id != domain_id:
raise HTTPException(status.HTTP_404_NOT_FOUND, "邀请不存在")
if invitation.status != "pending":
raise HTTPException(status.HTTP_409_CONFLICT, "邀请已处理")
invitation.status = "accepted"
member = WorkspaceMember(
workspace_id=invitation.workspace_id,
member_domain_id=domain_id,
role=invitation.role,
)
db.add(member)
await db.flush()
db.add(
AuditEvent(
actor_type="user", actor_id=domain_id, action="workspace.joined",
aggregate_type="workspace", aggregate_id=invitation.workspace_id, trace_id=trace_id,
)
)
await db.flush()
return member
async def remove_member(
db: AsyncSession, owner_domain_id: str, workspace_id: str, member_id: str,
trace_id: str | None,
) -> None:
await require_member(db, owner_domain_id, workspace_id, {"owner"})
member = await db.get(WorkspaceMember, member_id)
if member is None or member.workspace_id != workspace_id:
raise HTTPException(status.HTTP_404_NOT_FOUND, "成员不存在")
if member.role == "owner":
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "不能移除所有者")
await db.delete(member)
db.add(
AuditEvent(
actor_type="user", actor_id=owner_domain_id, action="workspace.member_removed",
aggregate_type="workspace", aggregate_id=workspace_id,
payload={"removed": member.member_domain_id}, trace_id=trace_id,
)
)
await db.flush()
async def list_members(
db: AsyncSession, domain_id: str, workspace_id: str
) -> list[WorkspaceMember]:
await require_member(db, domain_id, workspace_id)
return list(
(
await db.scalars(
select(WorkspaceMember)
.where(WorkspaceMember.workspace_id == workspace_id)
.order_by(WorkspaceMember.created_at.asc())
)
).all()
)
async def create_project(
db: AsyncSession, domain_id: str, workspace_id: str, name: str, trace_id: str | None
) -> Project:
await require_member(db, domain_id, workspace_id, ROLE_CAN_SUBMIT)
if not name.strip():
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "name 不能为空")
project = Project(workspace_id=workspace_id, name=name)
db.add(project)
await db.flush()
db.add(
AuditEvent(
actor_type="user", actor_id=domain_id, action="project.created",
aggregate_type="project", aggregate_id=project.id, trace_id=trace_id,
)
)
await db.flush()
return project
async def require_project(
db: AsyncSession, domain_id: str, project_id: str
) -> Project:
project = await db.get(Project, project_id)
if project is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "项目不存在")
await require_member(db, domain_id, project.workspace_id)
return project
async def submit_version(
db: AsyncSession, domain_id: str, project_id: str, payload: dict[str, Any],
trace_id: str | None,
) -> ProjectVersion:
"""editor 提交新版本:版本号自动递增,submitted 状态等待评审。"""
project = await require_project(db, domain_id, project_id)
await require_member(db, domain_id, project.workspace_id, ROLE_CAN_SUBMIT)
last = (
await db.execute(
select(ProjectVersion.version)
.where(ProjectVersion.project_id == project.id)
.order_by(ProjectVersion.version.desc())
.limit(1)
)
).scalar_one_or_none()
version = ProjectVersion(
project_id=project.id,
version=(last + 1) if last is not None else 1,
payload=payload,
status="submitted",
submitted_by=domain_id,
)
db.add(version)
await db.flush()
db.add(
AuditEvent(
actor_type="user", actor_id=domain_id, action="project.version_submitted",
aggregate_type="project", aggregate_id=project.id,
payload={"version": version.version}, trace_id=trace_id,
)
)
await db.flush()
return version
async def review_version(
db: AsyncSession, domain_id: str, version_id: str, approve: bool, trace_id: str | None
) -> ProjectVersion:
"""reviewer 批准/拒绝:批准即将版本设为项目当前版本。"""
version = await db.get(ProjectVersion, version_id)
if version is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "版本不存在")
project = await db.get(Project, version.project_id)
assert project is not None
await require_member(db, domain_id, project.workspace_id, ROLE_CAN_REVIEW)
if version.status != "submitted":
raise HTTPException(status.HTTP_409_CONFLICT, "版本不在待评审状态")
version.status = "approved" if approve else "rejected"
version.reviewed_by = domain_id
if approve:
project.current_version_id = version.id
db.add(
AuditEvent(
actor_type="user", actor_id=domain_id,
action="project.version_approved" if approve else "project.version_rejected",
aggregate_type="project", aggregate_id=project.id,
payload={"version": version.version}, trace_id=trace_id,
)
)
await db.flush()
return version
async def publish_project(
db: AsyncSession, domain_id: str, project_id: str, trace_id: str | None
) -> Project:
"""发布:仅 owner,且必须有已批准的当前版本(编辑/评审/发布三权分离)。"""
project = await require_project(db, domain_id, project_id)
await require_member(db, domain_id, project.workspace_id, {"owner"})
if project.current_version_id is None:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "没有已批准版本,不可发布")
if project.status == "published":
return project # 幂等
project.status = "published"
db.add(
AuditEvent(
actor_type="user", actor_id=domain_id, action="project.published",
aggregate_type="project", aggregate_id=project.id, trace_id=trace_id,
)
)
await db.flush()
return project
+2
View File
@@ -14,6 +14,8 @@ dependencies = [
"uuid-utils>=0.10",
"greenlet>=3.5.5",
"hunjing-lingtai",
"grpcio>=1.83.1",
"grpcio-tools>=1.83.1",
]
[dependency-groups]
+78
View File
@@ -0,0 +1,78 @@
"""开发档 Mock 模型服务:OpenAI 兼容 /chat/completions SSE 流式回声。
仅供本地联调 Run 流水线(事件序列、芽体渲染、对账),不模拟任何真实模型能力。
生产部署不启动本服务(与 /v1/webhooks/v1/payments/mock 同一开发档惯例)。
启动:uv run python scripts/mock_model.py (127.0.0.1:8001)
鉴权:任意 Bearer 均接受(联调端点,只监听回环地址)。
"""
import asyncio
import json
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
app = FastAPI(title="Hunjing Dev Mock Model", docs_url=None)
REPLY = (
"收到了。按当前镜域的状态,我先把这件事拆成两步:先核对读书会的偏好记忆,"
"再给出一份可归真的结果。这个回复由开发档 Mock 模型逐字流出,"
"用于验证 content.block.delta 事件序列、临时芽体渲染与 message.completed 对账。"
)
@app.get("/healthz")
async def healthz() -> dict[str, str]:
return {"status": "ok", "env": "development", "mock": "true"}
@app.post("/v1/chat/completions")
async def chat_completions(request: Request) -> StreamingResponse:
body = await request.json()
user_text = ""
for m in reversed(body.get("messages", [])):
if m.get("role") == "user" and m.get("content"):
user_text = str(m["content"])[-60:]
break
text = f"(mock 回应 · 你说:「{user_text}」)\n{REPLY}"
async def gen():
# 逐字流出,模拟真实 TTFT 与节奏
for ch in text:
await asyncio.sleep(0.02)
yield (
"data: "
+ json.dumps(
{"choices": [{"index": 0, "delta": {"content": ch}, "finish_reason": None}]},
ensure_ascii=False,
)
+ "\n\n"
)
yield (
"data: "
+ json.dumps(
{"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]},
ensure_ascii=False,
)
+ "\n\n"
)
yield (
"data: "
+ json.dumps(
{
"choices": [],
"usage": {
"prompt_tokens": 32,
"completion_tokens": len(text),
"total_tokens": 32 + len(text),
},
},
ensure_ascii=False,
)
+ "\n\n"
)
yield "data: [DONE]\n\n"
return StreamingResponse(gen(), media_type="text/event-stream")
+27 -2
View File
@@ -49,6 +49,7 @@ async def client(db):
# 后台 Run 任务使用测试库会话工厂与 mock 流(生产为独立工厂与真实传输)
app.state.session_factory = db
app.state.gw_transport = _mock_openai_stream()
app.state.media_provider = _mock_media_provider()
import os as _os
_os.environ.setdefault("HJ_MOCK_KEY", "test-key")
@@ -74,10 +75,21 @@ async def client(db):
async def login(client, email: str) -> dict[str, str]:
"""开发档登录:返回需附加的请求头(Cookie 由客户端自动携带)。"""
"""开发档登录:返回自包含请求头(显式 Cookie 支持多用户交替)。
同时保留 cookie jar 自动存储:不带 headers 的旧式请求仍走 jar。
显式 Cookie header 优先于 jar(httpx 行为),交替测试不串会话。
"""
resp = await client.post("/v1/auth/sessions", json={"email": email})
assert resp.status_code == 201, resp.text
return {"Hunjing-CSRF-Token": resp.json()["csrf_token"]}
from app.auth.service import SESSION_COOKIE
token = resp.cookies.get(SESSION_COOKIE)
assert token, "登录未签发会话 Cookie"
return {
"Hunjing-CSRF-Token": resp.json()["csrf_token"],
"Cookie": f"{SESSION_COOKIE}={token}",
}
def _mock_openai_stream():
@@ -100,3 +112,16 @@ def _mock_openai_stream():
)
return httpx.MockTransport(handler)
def _mock_media_provider():
"""媒体 Provider mock:ASR 返回确定性转写(测试注入;生产经配置)。"""
async def provider(kind: str, input_: dict) -> dict:
if kind == "asr":
return {"transcript": "(开发档转写)你好,魂镜", "language": "zh"}
if kind == "image_gen":
return {"file_id": None, "description": "generated"}
raise RuntimeError(f"unsupported kind {kind}")
return provider
File diff suppressed because it is too large. Load diff
+126
View File
@@ -0,0 +1,126 @@
"""gRPC 服务形态集成测试:双形态 golden 一致(ADR-017)。
在进程内启动服务器(随机端口),验证:
- GetEngineInfo 版本协商
- TransformMarkdown 与进程内 lingtai.transform_markdown 输出一致
- OpenStream 双向流 envelope 键序与序号单调
- EvaluatePolicy / ValidateUrl / CheckStateTransition / RerankRecall 行为
"""
import sys
from pathlib import Path
import grpc
import pytest
_SERVER_ROOT = Path(__file__).resolve().parents[2] / "engine" / "server"
sys.path.insert(0, str(_SERVER_ROOT / "generated"))
_SERVER_MODULE = _SERVER_ROOT / "lingtai_server.py"
import liushen.v1.liushen_pb2 as pb # noqa: E402
import liushen.v1.liushen_pb2_grpc as pb_grpc # noqa: E402
from app.core import lingtai # noqa: E402
@pytest.fixture()
async def grpc_channel():
import importlib.util as _ilu
from grpc import aio as grpc_aio
_spec = _ilu.spec_from_file_location(
"lingtai_server",
_SERVER_MODULE,
)
_mod = _ilu.module_from_spec(_spec)
_spec.loader.exec_module(_mod)
LingtaiEngineServicer = _mod.LingtaiEngineServicer
server = grpc_aio.server()
pb_grpc.add_LingtaiEngineServicer_to_server(LingtaiEngineServicer(), server)
port = server.add_insecure_port("127.0.0.1:0")
await server.start()
channel = grpc_aio.insecure_channel(f"127.0.0.1:{port}")
yield channel
await channel.close()
await server.stop(0)
async def test_engine_info(grpc_channel) -> None:
stub = pb_grpc.LingtaiEngineStub(grpc_channel)
info = await stub.GetEngineInfo(pb.GetEngineInfoRequest())
assert info.abi_major == 1
assert info.parser_version == "hunjing-markdown-v1"
assert "transform" in info.capabilities
async def test_transform_dual_form_golden(grpc_channel) -> None:
"""双形态一致:服务输出 == 进程内引擎输出(ADR-017 核心不变量)。"""
import json
stub = pb_grpc.LingtaiEngineStub(grpc_channel)
markdown = "说明\n```mermaid\nA-->B\n```\n$$\nz\n$$\n"
resp = await stub.TransformMarkdown(pb.TransformMarkdownRequest(markdown=markdown))
served = json.loads(resp.canonical_json)
inproc = lingtai.transform_markdown(markdown)
assert served == inproc # 服务形态与进程内输出一致
async def test_open_stream_envelope(grpc_channel) -> None:
import json
stub = pb_grpc.LingtaiEngineStub(grpc_channel)
async def frames():
yield pb.StreamFrame(block_started="block_01")
yield pb.StreamFrame(text_delta="你")
yield pb.StreamFrame(text_delta="好")
events = []
async for ev in stub.OpenStream(frames()):
events.append(json.loads(ev.event_json))
assert len(events) == 2
assert all(e["type"] == "content.block.delta" for e in events)
seqs = [e["sequence"] for e in events]
assert seqs == sorted(seqs)
assert events[-1]["data"]["delta"]["text"] == "好"
async def test_policy_and_state(grpc_channel) -> None:
stub = pb_grpc.LingtaiEngineStub(grpc_channel)
# 决策表
assert (await stub.EvaluatePolicy(pb.EvaluatePolicyRequest(
autonomy_level=1, risk_level=0))).decision == 0
assert (await stub.EvaluatePolicy(pb.EvaluatePolicyRequest(
autonomy_level=0, risk_level=2))).decision == 2
# URL 白名单
assert (await stub.ValidateUrl(pb.ValidateUrlRequest(
url="https://ok"))).allowed is True
assert (await stub.ValidateUrl(pb.ValidateUrlRequest(
url="file:///etc"))).allowed is False
# 状态机
assert (await stub.CheckStateTransition(pb.CheckStateTransitionRequest(
**{"from": pb.RUN_STATUS_QUEUED}, to=pb.RUN_STATUS_RUNNING))).valid is True
assert (await stub.CheckStateTransition(pb.CheckStateTransitionRequest(
**{"from": pb.RUN_STATUS_COMPLETED}, to=pb.RUN_STATUS_RUNNING))).valid is False
async def test_rerank(grpc_channel) -> None:
import json
stub = pb_grpc.LingtaiEngineStub(grpc_channel)
resp = await stub.RerankRecall(pb.RerankRecallRequest(
candidates_json='{"query_terms":["魂镜"],"candidates":['
'{"id":"a","text":"魂镜 系统"},{"id":"b","text":"无"}]}'
))
ranked = json.loads(resp.ranked_json)["ranked"]
assert ranked[0] == {"id": "a", "score": 10}
assert ranked[1] == {"id": "b", "score": 0}
async def test_rebuild_export_unimplemented(grpc_channel) -> None:
stub = pb_grpc.LingtaiEngineStub(grpc_channel)
with pytest.raises(grpc.aio.AioRpcError) as exc:
await stub.RebuildExport(pb.RebuildExportRequest(canonical_json="{}"))
assert exc.value.code() == grpc.StatusCode.UNIMPLEMENTED
+112
View File
@@ -265,6 +265,90 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/df/af/419a4e383bd600858a9b67e9b280a60fdc383ee3f2fe5b6c0c1ef04e74d1/greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b", size = 315093, upload-time = "2026-08-10T13:29:34.949Z" },
]
[[package]]
name = "grpcio"
version = "1.83.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e2/b1/46539f5050d7c316a13396d185451f95084a74ddc68b12d818595bef0377/grpcio-1.83.1.tar.gz", hash = "sha256:9cee6fcbf2eb57c4b49451787bfa87be8efc1ca02a0b327dd4b54d44502e362b", size = 13445033, upload-time = "2026-08-28T07:09:11.464Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/85/9e/a3ba13e08bbee5bf6e57597dfe4823961fd7e94c0b8afe3a4bb7dca639f3/grpcio-1.83.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:5acd14c6ddf047de62cbf8745b11103ea91abbf57d1b8edd5395ccd9fcd13abb", size = 6303170, upload-time = "2026-08-28T07:08:08.188Z" },
{ url = "https://files.pythonhosted.org/packages/1c/ae/65ce56a2527faa17d02cba4c2231c74047ad898be339486ba87f093bfb66/grpcio-1.83.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:16138031a47b771860a16a975b53087f4fd5bbdbb2c03a188c5d90ad65d2bdae", size = 12165806, upload-time = "2026-08-28T07:08:10.309Z" },
{ url = "https://files.pythonhosted.org/packages/4e/91/40432480088a2243d360864de072ed5b78c4ebbaabd29c28918f1e1b1454/grpcio-1.83.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5ccc26715fd4defca5e129e280dd883b1737b65045ec50ffe22ce42104089519", size = 6872490, upload-time = "2026-08-28T07:08:12.355Z" },
{ url = "https://files.pythonhosted.org/packages/c8/62/3da2300c8c79fd20a78a8a4bb6251e5068d9af33bc8fd389b98fec35e8a3/grpcio-1.83.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b74f2a1d9ab1dfa3e263ef33d581613679b78d0884babf11671af26e45570ead", size = 7618367, upload-time = "2026-08-28T07:08:14.025Z" },
{ url = "https://files.pythonhosted.org/packages/bc/19/9fc702e31a631262d7a752fa699f6022821e707fefc8bff49b1550a57729/grpcio-1.83.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72578aa07a4008f17521ef52debcc3acfd1e2c5426243bc3ffb56a38bfe610b7", size = 7040936, upload-time = "2026-08-28T07:08:15.963Z" },
{ url = "https://files.pythonhosted.org/packages/ec/56/95933cc44cba2429765fa065c951dd529e5771b119d9d2481b4646f1d6a5/grpcio-1.83.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c12e1fc59c6dc26d10d9144453ddc6cbfe4cd4c31e874ed2d0132f88e685eb8b", size = 7573096, upload-time = "2026-08-28T07:08:17.729Z" },
{ url = "https://files.pythonhosted.org/packages/ac/80/af63359da06b016de48cb111f144703a10043850dafa43ae0a038907b9e8/grpcio-1.83.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4910b62f7d12197160bfb7de06d876d64dd12d43483e8292f98f49ca09b628d9", size = 8609442, upload-time = "2026-08-28T07:08:19.777Z" },
{ url = "https://files.pythonhosted.org/packages/3d/fa/f0586c56bdfb8a7a2adda01e0ac2413447cde3141ab09411a5d5afdcffd3/grpcio-1.83.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e703effe3ae779925c82ac24fdb82cf4105e1096810151ed9501c5f34546b9c", size = 7984321, upload-time = "2026-08-28T07:08:22.114Z" },
{ url = "https://files.pythonhosted.org/packages/25/8a/14ec05669f9eb295801e26c2ea8c561a1b786b0e3557c2c22131165ab010/grpcio-1.83.1-cp312-cp312-win32.whl", hash = "sha256:a2aea8bd6e0a34f12cbaddb7bb70bec836818789fa5c7ab7572c6b745396a2d4", size = 4395604, upload-time = "2026-08-28T07:08:24.08Z" },
{ url = "https://files.pythonhosted.org/packages/e9/37/8c2f7cc16089e36a3fbacaacc7a3d043912aa0d2dfae5556f6450414ea6e/grpcio-1.83.1-cp312-cp312-win_amd64.whl", hash = "sha256:583bf2e8255040a4a312f9572dfe62a05271437b149550e1a536d5c47d2d1e8a", size = 5161512, upload-time = "2026-08-28T07:08:25.81Z" },
{ url = "https://files.pythonhosted.org/packages/7c/fd/d1fc58933bf88c9209f89dc570c810f1aa57cb04b3459cf2b26f61e32112/grpcio-1.83.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:8d228e253b77865efcbdd7b5894ca882c9e0ea98c02b7d20582e61ded8dfd4b5", size = 6305628, upload-time = "2026-08-28T07:08:27.872Z" },
{ url = "https://files.pythonhosted.org/packages/c4/49/0b40bae059c619505c9b751cee6caa208e4904e290aaefa1728c4c2c67a5/grpcio-1.83.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:0468b627f2987c9a77f7580030207cbd85457ffe52998beff4f0b5c38c58a72c", size = 12156839, upload-time = "2026-08-28T07:08:30.191Z" },
{ url = "https://files.pythonhosted.org/packages/61/4b/e8c0d635da0ee5ddd9950c8d540f5dcdd0ef1854a382cc55496a487a8d31/grpcio-1.83.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a6a282e81530cead60bbd752cc04950a57f224379e9821495d6a35bd5ce9b1f4", size = 6877036, upload-time = "2026-08-28T07:08:32.285Z" },
{ url = "https://files.pythonhosted.org/packages/c5/d4/760a33f339a7dd3d5f4b3e0e9bec5472d95592a80f887b2e9dab4e41cfbc/grpcio-1.83.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:947d945f52e8ecf3cafd2bb7113502a16ccfda3e12c854443094de32d83ad432", size = 7624404, upload-time = "2026-08-28T07:08:34.194Z" },
{ url = "https://files.pythonhosted.org/packages/54/ec/bd798654b06fb42a92b57d1dc1b530084fa89ed442806fcd0a833a36f9b3/grpcio-1.83.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:55656318d5dd387077396dffb929171ca3966e24bfead9a6c5dba9f889062cb4", size = 7042942, upload-time = "2026-08-28T07:08:36.208Z" },
{ url = "https://files.pythonhosted.org/packages/08/b0/c00f86614566dd0961825cf0f43d4f96a74371d9d95f952bcbc4b86d9a27/grpcio-1.83.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9daf5acf4fc9d5f5627229969c2580a91e511779d76e4ccdeb9f4770f05d8bc2", size = 7576937, upload-time = "2026-08-28T07:08:38.041Z" },
{ url = "https://files.pythonhosted.org/packages/b1/38/85eff43a5c89dc666a252b5c9f8e9ab03f89e11c95b6263d2933f08fdbe7/grpcio-1.83.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7b94174cbca93316888f805efbeb08f1c020f7b7493d2d50cc4f6b64ebb7e8bd", size = 8608391, upload-time = "2026-08-28T07:08:40.092Z" },
{ url = "https://files.pythonhosted.org/packages/35/4e/82835483e2f812494be865e7965c0d626cb9e71ab0d83a420d75aea4ad67/grpcio-1.83.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:65c5a7210911ffe0f67b1cdc5308f9854b6d1f1b345e3e49ab7cac1ba50fa346", size = 7980060, upload-time = "2026-08-28T07:08:42.434Z" },
{ url = "https://files.pythonhosted.org/packages/b2/b7/68a98bef733fef704fbcfb3957c8dba67e3e38ca7a7fea851195bc97c648/grpcio-1.83.1-cp313-cp313-win32.whl", hash = "sha256:179368d9361854616ce6f397d4716e07480129652752fcbcfc5a7260455ad6f2", size = 4395226, upload-time = "2026-08-28T07:08:44.463Z" },
{ url = "https://files.pythonhosted.org/packages/85/a0/df4de3b51d37ac8fb0320bb9668381ce2bd3b7aa990880bfc56a8a26f665/grpcio-1.83.1-cp313-cp313-win_amd64.whl", hash = "sha256:2e57af456385491a76e13c4aada8c8f43a8e47051e06ea97a9dbe2a49654e6db", size = 5160273, upload-time = "2026-08-28T07:08:46.216Z" },
{ url = "https://files.pythonhosted.org/packages/42/9c/484d981d8b90c4e6abf3030bd2ed747e84d1eb192b3ec9cbb41e0b73e4bf/grpcio-1.83.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:8b3c87ca908296bf125f841d3e1a2225a2b39aaa8ed7a57e7ccde465ee519bab", size = 6306089, upload-time = "2026-08-28T07:08:48.379Z" },
{ url = "https://files.pythonhosted.org/packages/84/01/0afec1c92e4f292f74a44ecf75eabbf40903125b8c4df103c9868d6338da/grpcio-1.83.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c0f3f20c90e72a171917ae65706500b096a1c3eb5f162c3ce702a2e25635f132", size = 12170381, upload-time = "2026-08-28T07:08:50.653Z" },
{ url = "https://files.pythonhosted.org/packages/c7/5a/e9a2383804433a0a61d6d93777ad321c7f36ac1cfdaa4c6d1a7c9ac846b7/grpcio-1.83.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81bbf35a46bf8cad2dfbb2eccc19c711befb58b288acb534bbcd0d74283202a6", size = 6883286, upload-time = "2026-08-28T07:08:53.654Z" },
{ url = "https://files.pythonhosted.org/packages/63/e7/f8ca8f76994e14c70b9a0052e82f10de497a23db450c36379c9716ebfc4d/grpcio-1.83.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:215cec07d11176507387bda4bf2751816e880f9bff8dc1ca524bfbb8ed8f2fad", size = 7624293, upload-time = "2026-08-28T07:08:55.709Z" },
{ url = "https://files.pythonhosted.org/packages/bb/7a/4b672814b0cd0fe63bdd735379d88b165759f3144ab023ad8ec5fc4d53ac/grpcio-1.83.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:abce7d43ec29cd39230fa8339de1a07643b55adc412a454850fbd875349950ff", size = 7044346, upload-time = "2026-08-28T07:08:57.802Z" },
{ url = "https://files.pythonhosted.org/packages/50/b8/d89fe60e4239ad51be333dd9cc703741d449a35064e51f8a0b5bfa755432/grpcio-1.83.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e256f95a40e3b0183a98556fb7164d24b97eeb353123ccabfcba94712b35ee2a", size = 7584187, upload-time = "2026-08-28T07:08:59.867Z" },
{ url = "https://files.pythonhosted.org/packages/dc/b2/b290d7402633d9166e4dd47e6f5f74a24ce10a8340b84455896ebc349f85/grpcio-1.83.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2110059146fb0ea216e1ffddb29377b5cc2fd412a5b0a92e102616bd5edf18c2", size = 8608730, upload-time = "2026-08-28T07:09:02.592Z" },
{ url = "https://files.pythonhosted.org/packages/f5/44/fa89e44d1b5cf5b9fa71b2fd7abf506f182fd43917231a92fbf1ea326b02/grpcio-1.83.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20d944d967843f8183f9f23d5916388362e5f8eeeae855bbe4354d906dc9f31b", size = 7983283, upload-time = "2026-08-28T07:09:05.087Z" },
{ url = "https://files.pythonhosted.org/packages/ce/b8/9db73ed1f35ffa76124ac574bf296d06a359798dfd6b50d382f2b8a060a1/grpcio-1.83.1-cp314-cp314-win32.whl", hash = "sha256:623c87c6d4a1cb30d82c4e896f95477050f2e01b4a1f8cf91ff2b1abdf89c457", size = 4474327, upload-time = "2026-08-28T07:09:07.179Z" },
{ url = "https://files.pythonhosted.org/packages/65/22/fc9a622d885a7a37ff972a12faaef443d74e47407181da70d0ab62ab41f0/grpcio-1.83.1-cp314-cp314-win_amd64.whl", hash = "sha256:47e6934ad38779271e2e7cc5f78a63a407cf3d98114c65c1fdbcd3f5a716f29b", size = 5302032, upload-time = "2026-08-28T07:09:09.285Z" },
]
[[package]]
name = "grpcio-tools"
version = "1.83.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "grpcio" },
{ name = "protobuf" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8b/79/8b4131bcb94f09c2cac4919e627f20c09ba9b22320a307697d72b1f881d4/grpcio_tools-1.83.1.tar.gz", hash = "sha256:a8148eece396f8a349097958bc00f14882003331f3b7c2ae8079c1c632d17d6f", size = 6399877, upload-time = "2026-08-28T07:10:56.19Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/03/91/79261571a7e5f6b5526f2da0fb7bff680af94f5322c8e8923961cbe801ae/grpcio_tools-1.83.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:3a8931c167e52741f72795c67f45435faaefb7ce8e9aa9413ba523ddaa358998", size = 2653283, upload-time = "2026-08-28T07:09:59.056Z" },
{ url = "https://files.pythonhosted.org/packages/03/b2/ea313245d23b0a77edc83b1f01b86304664e79e2221d11cc8cf92985f18b/grpcio_tools-1.83.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d559e85272145a979e9ad10be2740d953dcd21c37cdad159f06cd7eaf887c898", size = 5965913, upload-time = "2026-08-28T07:10:00.727Z" },
{ url = "https://files.pythonhosted.org/packages/31/ce/125baf82bba77fad398f6139bda5e4383208a5a7f9f1ffc42dd8d973c402/grpcio_tools-1.83.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:169518957cbc23034950006f3eb77514516476496b2951576c531336c61baffb", size = 2705430, upload-time = "2026-08-28T07:10:02.324Z" },
{ url = "https://files.pythonhosted.org/packages/bc/7c/cc727ab1b4ecc0a6dfe706a714c959d86736153c811e38b5b6723cb7576d/grpcio_tools-1.83.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d0e9b58917beb6e13d40bb694390dc79c8d7652a1d9ff412e98bdb416d8b135e", size = 3033411, upload-time = "2026-08-28T07:10:04.255Z" },
{ url = "https://files.pythonhosted.org/packages/84/57/e0312f16529cde4de03f532bb8dd4a8ccbf314e7c40cdfdc5586b6c23f85/grpcio_tools-1.83.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:70c74bb6fe0b57b7f4f49caa21d3263e149c2f1e2ef9d53c3d78d622e8564deb", size = 2774499, upload-time = "2026-08-28T07:10:05.928Z" },
{ url = "https://files.pythonhosted.org/packages/b7/83/0ecd3ee6ae20da8454481adf489969f854c56f702fcd2f27b5583fc14eae/grpcio_tools-1.83.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:91a3d0939364e688773c2735286e61107fbacdc104711b0003bd665986b79993", size = 3229873, upload-time = "2026-08-28T07:10:07.576Z" },
{ url = "https://files.pythonhosted.org/packages/09/03/92d4a193578efc5e4a061339dd0dbdcf516eebdb0577646b1bfaf144c803/grpcio_tools-1.83.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:479bb9f4bde497471717e0ee59ed5635b490af954224825bd7006608d1e76d79", size = 3803165, upload-time = "2026-08-28T07:10:09.67Z" },
{ url = "https://files.pythonhosted.org/packages/b2/51/9921cf100111c0e1ba375767cab93be3036b231f22533985ddb497111d4f/grpcio_tools-1.83.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b57f41c87ba0ea0d13d98b696058864d3dea949ecb4a376c3a4a52857330b83f", size = 3461813, upload-time = "2026-08-28T07:10:11.594Z" },
{ url = "https://files.pythonhosted.org/packages/75/77/7fd50b572b38670886e1c41986b785720e24a4b0be8b164fa09e2d915782/grpcio_tools-1.83.1-cp312-cp312-win32.whl", hash = "sha256:eaf9217d67960016fdd2932f28239ec14716a15b783baa8d4b012937098531f1", size = 1022492, upload-time = "2026-08-28T07:10:13.144Z" },
{ url = "https://files.pythonhosted.org/packages/cd/68/45e04d9a921e10158b84b099dbdd25e6d56776d508872c68922a61ff85fd/grpcio_tools-1.83.1-cp312-cp312-win_amd64.whl", hash = "sha256:dcbdd3422ca50ed6f880f9974df50796fb355f4051c55a464423a6423a3f0393", size = 1192288, upload-time = "2026-08-28T07:10:14.777Z" },
{ url = "https://files.pythonhosted.org/packages/d7/34/917a23240209fa5e020d68522b3d11d00da2acc8a784e215aec37b66cc0a/grpcio_tools-1.83.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:7c4ebbf3b13e481134fdf40fa49cb1f570ccb6b1a52c9ccded260428d79add6a", size = 2652846, upload-time = "2026-08-28T07:10:16.778Z" },
{ url = "https://files.pythonhosted.org/packages/1e/b3/e0e2c3cfb5bb1311f11fce1176f47d84374e592ca6abf64d579c9dff701c/grpcio_tools-1.83.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:fe937a0cb78397abdf3ece759beed1fe5763dc530d8165b8b8691cc0770f2523", size = 5963532, upload-time = "2026-08-28T07:10:18.731Z" },
{ url = "https://files.pythonhosted.org/packages/42/38/ce3d17a54ae754bed008303420e3ab88e8c26bd1e97722df188ee22b4583/grpcio_tools-1.83.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:93970064770f45789796c579c9e70fd2371621f5e2c07e64cf0decf7b41d8b6c", size = 2705094, upload-time = "2026-08-28T07:10:20.534Z" },
{ url = "https://files.pythonhosted.org/packages/b2/74/049150098e3dac26053f03d0b3e2f25da354c476302c6a31bb287f1a8493/grpcio_tools-1.83.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2c8c845f2b7d5a229377e3b10ffa281a93fb73e436a0078dc4a58c761ed32434", size = 3033063, upload-time = "2026-08-28T07:10:22.268Z" },
{ url = "https://files.pythonhosted.org/packages/40/fe/e9ec5fb11997318f7dabb1ff7b910b2f76d30c570d0ba56150733f256c25/grpcio_tools-1.83.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e15a089141788fe1268044ccdc43e1467c92d60387ebaf289dfd22a68227d16", size = 2773648, upload-time = "2026-08-28T07:10:23.979Z" },
{ url = "https://files.pythonhosted.org/packages/b4/05/92c1d00dd71fcce02e928f3895571e07b030463ce45e187d162317fcfadf/grpcio_tools-1.83.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9b7a57ce7ee79917d6db94d7ff8356e91c6768b9db62f26f03cd628ec5aacb0b", size = 3229788, upload-time = "2026-08-28T07:10:25.879Z" },
{ url = "https://files.pythonhosted.org/packages/c2/f5/974254c820c5e1f391f7f4cbade03befde72808d8323bbf9d2422614ce17/grpcio_tools-1.83.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fff7380f022deb12e4ca6cf9b26ef0000c1cf9ec8b2af2675a3b5afe42f68828", size = 3802531, upload-time = "2026-08-28T07:10:27.896Z" },
{ url = "https://files.pythonhosted.org/packages/88/c2/816b26ee8f4353ea3207dae5691b08ea9bf53e6a4bacc798742bd3066c66/grpcio_tools-1.83.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0bcb9d5f1f0cd550a45ac811f5bdf5241df1f924ac7c70d186eba7d9a6260e9c", size = 3461030, upload-time = "2026-08-28T07:10:29.611Z" },
{ url = "https://files.pythonhosted.org/packages/75/ab/a4c629a5cacaefd468dd8152269dd3a578dd0e8858ed8691878a9c3f545f/grpcio_tools-1.83.1-cp313-cp313-win32.whl", hash = "sha256:08197f91352f0944957372b2c82a4fc64d522b75b5c8c4cdc47186e20e4f7fb6", size = 1022120, upload-time = "2026-08-28T07:10:31.233Z" },
{ url = "https://files.pythonhosted.org/packages/ad/37/b62a44c72df3cc61917f9d7cc42af0d32f60e7ca8b892dfcd95330eae68b/grpcio_tools-1.83.1-cp313-cp313-win_amd64.whl", hash = "sha256:5e9eb45376a8e1d22e2ef067518616738272de223cbb13b2424d7880d04e4dbe", size = 1191938, upload-time = "2026-08-28T07:10:33.108Z" },
{ url = "https://files.pythonhosted.org/packages/34/2b/4e8f3bff52b933c52c853e3a51db8d5e87c71fc0ec3903ef9e432b25e4c5/grpcio_tools-1.83.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:f54955b95b17e83f8075303a31fa8f1aa64ad22d6f046e0070e9973918327202", size = 2652838, upload-time = "2026-08-28T07:10:35.035Z" },
{ url = "https://files.pythonhosted.org/packages/91/01/f5b3648285126a741a1a60108971c25acb0532af8fd2ffb29990747ff8d8/grpcio_tools-1.83.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:0f4c3d39546b0e6add25affd096170347c10437d78189fe2d0a026f7cbc5cb57", size = 5963473, upload-time = "2026-08-28T07:10:37.213Z" },
{ url = "https://files.pythonhosted.org/packages/65/1d/95fc82961423365edc3ac6f98bc12ac96429bfbc5c43ede9ff40354aa273/grpcio_tools-1.83.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:34c8bbfe6e1ed19524010351a27c0d43e185b3d542cf4ab83ad9774d4c9000e1", size = 2705302, upload-time = "2026-08-28T07:10:39.166Z" },
{ url = "https://files.pythonhosted.org/packages/e4/c5/82e29f48fcb5cf77b78c427ccaf5808b1b276cadd42955ceec960ec8daff/grpcio_tools-1.83.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:004bef39f47db3ed93caf58175ad3d3800f53d16a04b493166be945a0b575df3", size = 3033047, upload-time = "2026-08-28T07:10:41.492Z" },
{ url = "https://files.pythonhosted.org/packages/89/38/f17b2902166320912fff7ecb4227a69ddf8edb544702045c3aba3076de0a/grpcio_tools-1.83.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:41a4ce8c2cddb38b50d5befe4f40b124f77d68021e2e47f80142f6138d802d8f", size = 2773829, upload-time = "2026-08-28T07:10:43.367Z" },
{ url = "https://files.pythonhosted.org/packages/df/c5/974b54afc6ea3d7d8b51671d0c64effd9a92691a887e9bbf62c8b6213f03/grpcio_tools-1.83.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b52cc836922456d56f25ff63b277410a6d9ad682863f064c1f735a19328432a3", size = 3229905, upload-time = "2026-08-28T07:10:45.758Z" },
{ url = "https://files.pythonhosted.org/packages/8a/87/e9d5ff123ff80d0a7d502e9d2452305812d904e17afb88b3b23d892f3334/grpcio_tools-1.83.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:b73afd4303f2b026f8d8f3d7f07f5f22a2f2c3ecb05758af30205db0b16539b9", size = 3802600, upload-time = "2026-08-28T07:10:47.977Z" },
{ url = "https://files.pythonhosted.org/packages/4f/2f/66d8cdc5a81f9723f2b7b2f32d29a757b3bd2eac2116746014626feb83d6/grpcio_tools-1.83.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:df9a2b346b98ec28d5666b43d2c00bb533976faa13f3fab2897890b3703a968a", size = 3461303, upload-time = "2026-08-28T07:10:50.178Z" },
{ url = "https://files.pythonhosted.org/packages/dd/f3/3989a42f1dcc1a1c59f06f5e4fbf48bc50a53a6cc5083c0b6a7aef4c1929/grpcio_tools-1.83.1-cp314-cp314-win32.whl", hash = "sha256:a8cafccbe4b83bcc297920796ae5a784c700c9fbef01705397007bcb4652e070", size = 1045048, upload-time = "2026-08-28T07:10:52.123Z" },
{ url = "https://files.pythonhosted.org/packages/11/bf/be8370e2c3f0108aaae33bd609c359d593648959cc9efe69e6b59ec88a41/grpcio_tools-1.83.1-cp314-cp314-win_amd64.whl", hash = "sha256:9a40b779ec4bbd042279957cd1d0f42d4cbe333878d3d59ceeebe1cf3148528a", size = 1224200, upload-time = "2026-08-28T07:10:54.085Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
@@ -347,6 +431,8 @@ dependencies = [
{ name = "asyncpg" },
{ name = "fastapi" },
{ name = "greenlet" },
{ name = "grpcio" },
{ name = "grpcio-tools" },
{ name = "hunjing-lingtai" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
@@ -370,6 +456,8 @@ requires-dist = [
{ name = "asyncpg", specifier = ">=0.30" },
{ name = "fastapi", specifier = ">=0.115" },
{ name = "greenlet", specifier = ">=3.5.5" },
{ name = "grpcio", specifier = ">=1.83.1" },
{ name = "grpcio-tools", specifier = ">=1.83.1" },
{ name = "hunjing-lingtai", editable = "../engine/bindings" },
{ name = "pydantic", specifier = ">=2.9" },
{ name = "pydantic-settings", specifier = ">=2.6" },
@@ -676,6 +764,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "protobuf"
version = "7.36.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/86/73/f66c748df06e7fe24e658eddd600d19c4b40bad836c97ce2d0ad9851fb6b/protobuf-7.36.1.tar.gz", hash = "sha256:d0f6470f0ce2b84e3feaea2d4b816378b37ba4d4aa08a274305373de93e2d524", size = 512499, upload-time = "2026-08-31T22:40:04.667Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/6c/3a54a58f2948b0f485df9ecdd06590f15d0a7abf46a89d50c3de709ff4ff/protobuf-7.36.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:3cf2ee25d006cee57294a1196ea43b37feb78e0dcd1e8af5c1aeddb777655aca", size = 456046, upload-time = "2026-08-31T22:39:56.865Z" },
{ url = "https://files.pythonhosted.org/packages/6e/08/9f9548793c771095245c0eeaf0c84b76b16a5ef26158043d456f551344a0/protobuf-7.36.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:43d3d37b1eb24c113b9b7d02008cac44e423f00b611b7781ae998d7623972969", size = 344226, upload-time = "2026-08-31T22:39:58.263Z" },
{ url = "https://files.pythonhosted.org/packages/fe/51/1bdbd612fa3c51e42ea6f45d05e84dc748ddcd2663e1aa1e89a00a33facd/protobuf-7.36.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:39c518c05586c016d7874ff6079ee115bcec1ea5fbb1d177fbf7867ef4c67e44", size = 357229, upload-time = "2026-08-31T22:39:59.198Z" },
{ url = "https://files.pythonhosted.org/packages/22/df/c799fe7a05ef16ba853a59db01f3a2c5f7d0676469589ccc4874f76a2a88/protobuf-7.36.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:97198b77e369a0abd8e262b8f6c7266c55ddb796a3a12c76d7b8881188ed83aa", size = 343228, upload-time = "2026-08-31T22:40:00.179Z" },
{ url = "https://files.pythonhosted.org/packages/3d/33/d4724ec5d86d496fe4108e220618aa0837ad3423a7af7ccdd9684ccc77c8/protobuf-7.36.1-cp310-abi3-win32.whl", hash = "sha256:0b53ce95272aad50ad25d7ff03373743209822e8ba42ea7fad27d2bee1547d00", size = 443002, upload-time = "2026-08-31T22:40:01.32Z" },
{ url = "https://files.pythonhosted.org/packages/db/37/155788a0d8daded960375af604202805308169f9b859419ea0aa370946e2/protobuf-7.36.1-cp310-abi3-win_amd64.whl", hash = "sha256:51139351435d9b43d88a55eaa49fb6f737fbb478fb0cbf2cf694d1a04a9d3363", size = 456518, upload-time = "2026-08-31T22:40:02.494Z" },
{ url = "https://files.pythonhosted.org/packages/39/ca/c47f91d3cab175b01fd8c4f0d80fdf8613be876cc616e66ad281a59c5ddf/protobuf-7.36.1-py3-none-any.whl", hash = "sha256:7d951e46b3f963d6c264c367c437921de9d5aedd9c3f9612b9077736b4e3ad5c", size = 179813, upload-time = "2026-08-31T22:40:03.54Z" },
]
[[package]]
name = "pydantic"
version = "2.13.5"
@@ -898,6 +1001,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/07/d781f8f8e1ac24bef9f3269cf62ffb1407ca24c3a8f12e5e22874f90528c/ruff-0.16.6-py3-none-win_arm64.whl", hash = "sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5", size = 10412850, upload-time = "2026-09-03T16:57:26.416Z" },
]
[[package]]
name = "setuptools"
version = "84.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" },
]
[[package]]
name = "sqlalchemy"
version = "2.0.52"
+399
View File
@@ -0,0 +1,399 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>司命台 · 管理面 — 魂镜 Hunjing</title>
<link rel="stylesheet" href="assets/app.css?v=10">
</head>
<body>
<svg style="display:none" aria-hidden="true">
<symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol>
<symbol id="i-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></symbol>
<symbol id="i-kanban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3h4v18H5z"/><path d="M10 3h4v12h-4z"/><path d="M15 3h4v8h-4z"/></symbol>
<symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></symbol>
<symbol id="i-package" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></symbol>
<symbol id="i-monitor" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></symbol>
<symbol id="i-sliders" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></symbol>
<symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></symbol>
<symbol id="i-file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></symbol>
<symbol id="i-refresh" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></symbol>
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></symbol>
<symbol id="i-activity" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></symbol>
<symbol id="i-database" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></symbol>
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/></symbol>
<symbol id="i-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></symbol>
<symbol id="i-chevrons" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="11 17 6 12 11 7"/><polyline points="18 17 13 12 18 7"/></symbol>
</svg>
<div class="app">
<aside class="sidebar">
<div class="brand">
<div class="brand__mark" aria-hidden="true"></div>
<div class="brand__text">
<div class="brand__name">魂镜</div>
<div class="brand__sub">司命台 · 管理面</div>
</div>
</div>
<nav class="side-nav" aria-label="管理面导航">
<div class="nav-group">
<div class="nav-group__title">运营</div>
<a class="nav-item is-active" href="#queue" aria-current="page"><svg><use href="#i-kanban"/></svg><span style="font-size:14.5px">治理队列</span></a>
<a class="nav-item" href="#tickets"><svg><use href="#i-file"/></svg><span style="font-size:14.5px">支持工单</span><span class="nav-plain">3</span></a>
<a class="nav-item" href="#users"><svg><use href="#i-users"/></svg><span style="font-size:14.5px">用户</span></a>
<a class="nav-item" href="#perms"><svg><use href="#i-shield"/></svg><span style="font-size:14.5px">权限管理</span></a>
</div>
<div class="nav-group">
<div class="nav-group__title">部署</div>
<a class="nav-item" href="#health"><svg><use href="#i-server"/></svg><span style="font-size:14.5px">服务健康</span></a>
<a class="nav-item" href="#backup"><svg><use href="#i-database"/></svg><span style="font-size:14.5px">迁移与备份</span></a>
<a class="nav-item" href="#queue" title="插件案件在治理队列「插件」分类"><svg><use href="#i-package"/></svg><span style="font-size:14.5px">插件治理</span></a>
</div>
<div class="nav-group">
<div class="nav-group__title">观测</div>
<a class="nav-item" href="#slo"><svg><use href="#i-activity"/></svg><span style="font-size:14.5px">SLO 与指标</span></a>
<a class="nav-item" href="#audit"><svg><use href="#i-shield"/></svg><span style="font-size:14.5px">审计日志</span></a>
</div>
<div class="nav-group">
<div class="nav-group__title">边界</div>
<a class="nav-item" href="governance.html"><svg><use href="#i-lock"/></svg><span style="font-size:14.5px">治理边界说明</span></a>
</div>
</nav>
<div class="side-foot">
<button class="icon-btn side-fold-btn" data-sidebar-fold aria-label="收起侧栏" title="收起 / 展开侧栏(记忆偏好)"><svg><use href="#i-chevrons"/></svg></button>
<span class="avatar avatar--sm avatar--user">司</span>
<span style="font-size:12.5px">运维 · 管理员</span>
<button class="icon-btn" data-theme-toggle style="margin-left:auto" aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</div>
<div style="border-top:1px solid var(--line-soft);padding:10px 16px">
<a href="home.html" style="font-size:12px;display:flex;align-items:center;gap:6px"><svg style="width:13px;height:13px"><use href="#i-home"/></svg>返回用户界面</a>
</div>
</aside>
<main class="main">
<header class="topbar">
<div class="crumbs"><span class="cn">司命台</span><span class="crumbs__sep">/</span><span class="here">管理面 · 运营与部署</span></div>
<div class="topbar__spacer"></div>
<span class="badge badge--warn"><span class="badge__dot"></span>双人授权会话 · 剩 41 分钟 <span class="mono">admin_session</span></span>
<span class="prototype-tag">静态原型 · 待审查</span>
<button class="icon-btn" data-theme-toggle aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</header>
<div class="content">
<div class="realm-banner" role="note">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
管理面边界:管理员不可读取私箧与用户明文内容;产品宪章的永久禁域不可被管理员配置关闭;敏感操作需双人授权(本次会话已授权)且全部写入审计。
</div>
<div class="hero" style="margin-bottom:22px">
<div>
<h1>治理总览</h1>
<p style="color:var(--text-2);font-size:13px;margin-top:4px">2026-09-16 09:40 · 自托管标准部署 · 区域驻留 cn-east-1(不跨区漂移)</p>
</div>
<div class="hero__stats">
<div class="hero__stat"><b style="color:var(--warn)">1</b><span>活跃告警</span></div>
<div class="hero__stat"><b>5</b><span>待处理司议</span></div>
<div class="hero__stat"><b>3</b><span>支持工单(未结)</span></div>
<div class="hero__stat"><b style="color:var(--ok)">99.87%</b><span>SLO 30 天(目标 99.9%)</span></div>
</div>
</div>
<section class="section card" id="astrolabe">
<div class="card__head">
<span class="cn">灵台星盘<span class="cn__plain">引擎六枢 · 活的仪表</span></span>
<span class="mirror-state mirror-state--steady" style="margin-left:auto"><i></i>稳镜 · steady <span class="cn__plain">铜镜四态:虚镜 / 息镜 / 稳镜 / 裂镜</span></span>
</div>
<div class="astrolabe-wrap">
<div class="astrolabe">
<svg viewBox="0 0 320 320" role="img" aria-label="灵台六枢星盘:太一居中,小周天、洛书、河图、陶钧、烛照环绕;烛照告警闪烁">
<g class="ast-spin"><circle cx="160" cy="160" r="148" fill="none" stroke="var(--gilt-faint)" stroke-width="0.7" stroke-dasharray="2 9"/></g>
<circle cx="160" cy="160" r="122" fill="none" stroke="var(--line-soft)" stroke-width="0.8"/>
<g stroke="var(--gilt-faint)" stroke-width="0.8">
<line x1="160" y1="160" x2="272" y2="160"/>
<line x1="160" y1="160" x2="160" y2="48"/>
<line x1="160" y1="160" x2="51" y2="102"/>
<line x1="160" y1="160" x2="51" y2="218"/>
<line x1="160" y1="160" x2="160" y2="272" transform="rotate(0)"/>
</g>
<!-- 太一(中枢) -->
<circle class="ast-breathe" cx="160" cy="160" r="40" fill="none" stroke="var(--gilt-dim)" stroke-width="0.8" stroke-dasharray="3 6"/>
<circle cx="160" cy="160" r="30" fill="var(--bg-raised)" stroke="var(--gilt-dim)"/>
<text class="ast-core-text" x="160" y="158" text-anchor="middle" font-size="14" font-weight="600">太一</text>
<text class="ast-node__plain" x="160" y="171" text-anchor="middle">总枢 · v1.4.2</text>
<!-- 小周天(东) -->
<g class="ast-node" title="小周天 · 流式引擎 hj_stream:增量、节流与事件序">
<circle cx="272" cy="160" r="22" fill="var(--bg-raised)" stroke="var(--accent)" stroke-opacity=".55"/>
<text class="ast-node__name" x="272" y="158" text-anchor="middle">小周天</text>
<circle cx="272" cy="184" r="3" fill="var(--ok)"/>
</g>
<!-- 洛书(北) -->
<g class="ast-node" title="洛书 · 计划引擎 hj_plan:预算与上下文装配">
<circle cx="160" cy="48" r="22" fill="var(--bg-raised)" stroke="var(--accent)" stroke-opacity=".55"/>
<text class="ast-node__name" x="160" y="46" text-anchor="middle">洛书</text>
<circle cx="160" cy="72" r="3" fill="var(--ok)"/>
</g>
<!-- 河图(西) -->
<g class="ast-node" title="河图 · 检索引擎 hj_recall:离线索引与重排">
<circle cx="51" cy="102" r="22" fill="var(--bg-raised)" stroke="var(--accent)" stroke-opacity=".55"/>
<text class="ast-node__name" x="51" y="100" text-anchor="middle">河图</text>
<circle cx="51" cy="126" r="3" fill="var(--ok)"/>
</g>
<!-- 陶钧(西南) -->
<g class="ast-node" title="陶钧 · 内容引擎 hj_transform:Markdown 规范化">
<circle cx="51" cy="218" r="22" fill="var(--bg-raised)" stroke="var(--accent)" stroke-opacity=".55"/>
<text class="ast-node__name" x="51" y="216" text-anchor="middle">陶钧</text>
<circle cx="51" cy="242" r="3" fill="var(--ok)"/>
</g>
<!-- 烛照(南 · 告警闪烁) -->
<g class="ast-node" title="烛照 · 策略引擎 hj_policy:校验、验签与决断——当前校验超时 2 次">
<circle cx="160" cy="272" r="22" fill="var(--bg-raised)" stroke="var(--cinnabar)"/>
<text class="ast-node__name" x="160" y="270" text-anchor="middle">烛照</text>
<circle class="ast-alert" cx="160" cy="296" r="3.4" fill="var(--cinnabar)"/>
</g>
</svg>
</div>
<div style="flex:1;min-width:230px">
<div class="clash-row">
<div class="clash-row__pair"><b>相克告警</b> · 错误率 vs 自动重试率 <span style="color:var(--text-3)">——重试正在掩盖错误,建议查 Sandbox 慢执行根因</span></div>
<div class="clash-strength" title="耦合强度 0.82"><i style="width:82%"></i></div>
</div>
<div class="clash-row">
<div class="clash-row__pair"><b>相生正常</b> · 候选生成率 → 人工审阅吞吐 <span style="color:var(--text-3)">——生成与审阅同速,息壤无积压</span></div>
<div class="clash-strength"><i style="width:46%;background:var(--accent)"></i></div>
</div>
<p style="font-size:11.5px;color:var(--text-3);margin-top:8px;line-height:1.7">星盘为活的仪表:呼吸刻度 7s、慢旋轨道 120s、告警枢点 1.8s 闪烁;点击枢点下钻。烛照(gRPC 形态)受阻时宿主 fail closed,不降级——引擎缺失即拒绝执行。指标对健康不是灯板而是关系动力学(相生 / 相克)。</p>
</div>
</div>
</section>
<div class="two-col">
<div>
<section class="section" id="queue">
<div class="section__head">
<span class="cn">司议<span class="cn__plain">治理案件 · 运营视角</span></span>
<a class="more" href="#">全部 →</a>
</div>
<div class="tabs" role="tablist" aria-label="治理队列分类">
<button class="tab is-on" role="tab" aria-selected="true">内容 <span class="cnt">2</span></button>
<button class="tab" role="tab" aria-selected="false">插件 <span class="cnt">1</span></button>
<button class="tab" role="tab" aria-selected="false">市场 <span class="cnt">1</span></button>
<button class="tab" role="tab" aria-selected="false">申诉 <span class="cnt">1</span></button>
</div>
<div class="card case-card" style="margin-bottom:14px;padding-left:20px">
<div class="card__head">
<span class="badge badge--warn"><span class="badge__dot"></span>待审 <span class="mono">pending_review</span></span>
<span class="badge badge--err" style="font-size:11px"><span class="badge__dot"></span>处理时限剩 6 小时</span>
<span style="font-size:11.5px;color:var(--text-3);margin-left:auto">09-16 03:12 · 鸣冤 ×3 · 举报人已匿名保护</span>
</div>
<p style="font-size:13.5px">雅集帖子「未署名的守砚人立像二创」:疑似违反 CC BY-NC 署名要求。系统预检:来源谱系比对确认与原作者资产相似度 91%。</p>
<div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap">
<button class="btn btn--outline btn--sm">查看原文(记录访问)</button>
<button class="btn btn--outline btn--sm">限观 · 限制展示</button>
<button class="btn btn--danger btn--sm">移除</button>
<button class="btn btn--ghost btn--sm">驳回举报</button>
</div>
</div>
<div class="card case-card" style="padding-left:20px">
<div class="card__head">
<span class="badge badge--warn"><span class="badge__dot"></span>待审 <span class="mono">pending_review</span></span>
<span style="font-size:11.5px;color:var(--text-3);margin-left:auto">09-15 18:47 · 自动扫描 + 人工复核</span>
</div>
<p style="font-size:13.5px">市场商品「曲谱排版插件 v1.4」:Manifest 声明与实际网络请求域不一致(2 个未声明域)。已触发自动下架保护,等待复核结论。</p>
<div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap">
<button class="btn btn--outline btn--sm">查看扫描报告</button>
<button class="btn btn--danger btn--sm">确认下架</button>
<button class="btn btn--outline btn--sm">恢复上架(需备注)</button>
<button class="btn btn--ghost btn--sm">通知创作者</button>
</div>
</div>
</section>
<section id="users" class="section">
<div class="section__head">
<span class="cn">用户<span class="cn__plain">账户状态治理</span></span>
<div class="searchbar" style="width:220px"><svg><use href="#i-search"/></svg><input type="search" placeholder="搜索用户 / ID" aria-label="搜索用户"></div>
</div>
<div class="card" style="padding:6px 18px">
<div class="list-row">
<span class="avatar avatar--sm avatar--user">岁</span>
<div class="list-row__body">
<div class="list-row__title">用户 u_7a2c… <span class="badge badge--warn" style="font-size:11px"><span class="badge__dot"></span>保全态 · 只读</span></div>
<div class="list-row__sub">欠费进入保全:角色与记忆只读保留,不删除数据(ADR-021);补缴后自动恢复</div>
</div>
<button class="btn btn--ghost btn--sm">查看保全详情</button>
</div>
<div class="list-row">
<span class="avatar avatar--sm avatar--user">尘</span>
<div class="list-row__body">
<div class="list-row__title">用户 u_31f0… <span class="badge badge--err" style="font-size:11px"><span class="badge__dot"></span>封禁 · 申诉中</span></div>
<div class="list-row__sub">封禁同样进入只读保全;申诉司议 #2041 处理中</div>
</div>
<button class="btn btn--ghost btn--sm">处理申诉</button>
</div>
</div>
<p style="font-size:12px;color:var(--text-3);margin-top:8px">不按记忆、感情或亲密等级计费;保全与删除是独立操作,删除需冷静期与二次双人授权。</p>
</section>
<section id="perms" class="section">
<div class="section__head">
<span class="cn">权限管理<span class="cn__plain">管理员账号与角色授权</span></span>
<button class="btn btn--outline btn--sm" style="margin-left:auto">新增管理员</button>
</div>
<div class="card" style="margin-bottom:14px">
<div class="card__head"><span class="cn" style="font-size:14px">管理员账号</span><span style="margin-left:auto;font-size:11.5px;color:var(--text-3)">4 个账号 · 全部启用双因素</span></div>
<div class="table-wrap"><table class="data">
<thead><tr><th>账号</th><th>角色</th><th>双因素</th><th>状态</th><th>操作</th></tr></thead>
<tbody>
<tr><td>运维 · 甲(你)</td><td>超级管理员</td><td class="mono">TOTP + 硬件钥匙</td><td><span class="badge badge--ok" style="font-size:11px"><span class="badge__dot"></span>在线</span></td><td><button class="btn btn--ghost btn--sm">编辑</button></td></tr>
<tr><td>运维 · 乙</td><td>运营管理员</td><td class="mono">TOTP</td><td><span class="badge badge--ok" style="font-size:11px"><span class="badge__dot"></span>双人会话中</span></td><td><button class="btn btn--ghost btn--sm">编辑</button></td></tr>
<tr><td>审核 · 丙</td><td>内容审核员</td><td class="mono">TOTP</td><td><span class="badge badge--muted" style="font-size:11px"><span class="badge__dot"></span>离线</span></td><td><button class="btn btn--ghost btn--sm">编辑</button></td></tr>
<tr><td class="mono">svc-audit-ro</td><td>只读服务账号</td><td class="mono">API key · scope audit:read</td><td><span class="badge badge--muted" style="font-size:11px"><span class="badge__dot"></span>90 天轮换</span></td><td><button class="btn btn--ghost btn--sm">轮换</button></td></tr>
</tbody>
</table></div>
</div>
<div class="card" style="margin-bottom:14px">
<div class="card__head"><span class="cn" style="font-size:14px">角色权限矩阵<span class="cn__plain">RBAC</span></span><span style="margin-left:auto;font-size:11.5px;color:var(--text-3)">编辑 / 发布 / 上架 / 财务分离,与同契台同规则</span></div>
<div class="table-wrap"><table class="data">
<thead><tr><th>角色</th><th>治理裁决</th><th>支持包</th><th>用户保全</th><th>部署设置</th><th>审计读取</th><th>权限管理</th></tr></thead>
<tbody>
<tr><td>超级管理员</td><td class="mark-ok">✓</td><td class="mark-ok">✓</td><td class="mark-ok">✓</td><td class="mark-ok">✓</td><td class="mark-ok">✓</td><td class="mark-ok">✓</td></tr>
<tr><td>运营管理员</td><td class="mark-ok">✓</td><td class="mark-ok">✓</td><td class="mark-ok">✓</td><td class="mark-no">—</td><td class="mark-ok">✓</td><td class="mark-no">—</td></tr>
<tr><td>内容审核员</td><td class="mark-ok">✓(内容)</td><td class="mark-no">—</td><td class="mark-no">—</td><td class="mark-no">—</td><td class="mark-ok">✓(只读)</td><td class="mark-no">—</td></tr>
<tr><td>只读服务账号</td><td class="mark-no">—</td><td class="mark-no">—</td><td class="mark-no">—</td><td class="mark-no">—</td><td class="mark-ok">✓</td><td class="mark-no">—</td></tr>
</tbody>
</table></div>
</div>
<div class="card">
<div class="card__head"><span class="cn" style="font-size:14px">双人授权策略</span><span style="margin-left:auto;font-size:11.5px;color:var(--text-3)">策略本身修改也需要双人授权</span></div>
<div class="set-row">
<div class="set-row__body"><div class="set-row__t">敏感操作双人授权</div><div class="set-row__d">治理裁决移除 / 用户保全变更 / 支持包生成 / 权限变更,均需两名管理员在线授权</div></div>
<span class="switch"><input type="checkbox" checked disabled aria-label="双人授权已启用(策略锁定)"><i></i></span>
</div>
<div class="set-row">
<div class="set-row__body"><div class="set-row__t">管理会话时长上限</div><div class="set-row__d">60 分钟 · 到期自动退出并留审计</div></div>
<span class="mono" style="font-size:12.5px;color:var(--text-2)">60 min</span>
</div>
<div class="set-row">
<div class="set-row__body"><div class="set-row__t">Break-glass 紧急通道</div><div class="set-row__d">默认关闭;紧急启用后所有操作事后复核,48 小时内强制复盘</div></div>
<span class="badge badge--muted" style="font-size:11px"><span class="badge__dot"></span>关闭 <span class="mono">off</span></span>
</div>
</div>
<p style="font-size:12px;color:var(--text-3);margin-top:8px">角色与权限变更即时生效并写入审计;被移除管理员的进行中双人会话立即失效。权限矩阵不包含「读取私箧 / 读取消息明文」——该权限不存在。</p>
</section>
<section id="tickets" class="section">
<div class="section__head">
<span class="cn">支持工单<span class="cn__plain">3 未结</span></span>
<a class="more" href="#">全部 →</a>
</div>
<div class="card" style="padding:6px 18px">
<div class="list-row">
<span class="badge badge--info" style="flex:none;font-size:11px"><span class="badge__dot"></span>处理中</span>
<div class="list-row__body">
<div class="list-row__title">#88231 · 对话流中断后未收到最终答复</div>
<div class="list-row__sub">用户云舟 · 09-15 21:04 提交 · 已关联 Run 快照(诊断双授权待用户批准)</div>
</div>
<button class="btn btn--outline btn--sm">回复</button>
</div>
<div class="list-row">
<span class="badge badge--muted" style="flex:none;font-size:11px"><span class="badge__dot"></span>待分派</span>
<div class="list-row__body">
<div class="list-row__title">#88229 · 退款争议:雾中茶寮 v3 下载后要求退款</div>
<div class="list-row__sub">按市契 14 天未下载规则预检:已下载,转争议流程</div>
</div>
<button class="btn btn--outline btn--sm">分派</button>
</div>
</div>
<div class="card" style="margin-top:12px">
<div class="card__head">
<span class="cn">支持包<span class="cn__plain">脱敏诊断导出</span></span>
<button class="btn btn--cinnabar btn--sm" style="margin-left:auto">生成 #88231 支持包</button>
</div>
<p style="font-size:12.5px;color:var(--text-2)">支持包为脱敏导出:包含 Run 结构化轨迹、错误与计数;<span style="color:var(--text-1)">不包含</span>消息明文、私箧、隐藏推理、密钥与签名 URL。生成需双人授权并计入审计,用户侧可见支持包被生成的记录。</p>
</div>
</section>
</div>
<div>
<section id="health" class="section card">
<div class="card__head"><span class="cn">部署健康<span class="cn__plain">自托管 · 标准服务器</span></span><button class="icon-btn" aria-label="刷新"><svg><use href="#i-refresh"/></svg></button></div>
<div class="set-row">
<svg style="width:16px;height:16px;color:var(--text-3)"><use href="#i-database"/></svg>
<div class="set-row__body"><div class="set-row__t">PostgreSQL 主库</div><div class="set-row__d">权威事实源 · 复制延迟 0.4s</div></div>
<span class="badge badge--ok" style="font-size:11px"><span class="badge__dot"></span>正常</span>
</div>
<div class="set-row">
<svg style="width:16px;height:16px;color:var(--text-3)"><use href="#i-server"/></svg>
<div class="set-row__body"><div class="set-row__t">Worker ×2</div><div class="set-row__sub set-row__d">Outbox 消费 · 队列深度 14</div></div>
<span class="badge badge--ok" style="font-size:11px"><span class="badge__dot"></span>正常</span>
</div>
<div class="set-row">
<svg style="width:16px;height:16px;color:var(--text-3)"><use href="#i-package"/></svg>
<div class="set-row__body"><div class="set-row__t">Sandbox Worker</div><div class="set-row__d">隔离容器池 · 无符合策略后端时拒绝执行(fail closed)</div></div>
<span class="badge badge--ok" style="font-size:11px"><span class="badge__dot"></span>正常</span>
</div>
<div class="set-row">
<svg style="width:16px;height:16px;color:var(--text-3)"><use href="#i-cpu"/></svg>
<div class="set-row__body"><div class="set-row__t cn">灵台<span class="cn__plain">引擎核心运行中 · 进程内 C ABI</span></div><div class="set-row__d mono">hj_core v1.4.2 · ABI 快照一致 · fuzz 无新增</div></div>
<span class="badge badge--ok" style="font-size:11px"><span class="badge__dot"></span>运转</span>
</div>
<div class="set-row" style="border-color:color-mix(in srgb,var(--warn) 35%,transparent)">
<svg style="width:16px;height:16px;color:var(--warn)"><use href="#i-server"/></svg>
<div class="set-row__body"><div class="set-row__t cn">灵台 · 服务形态<span class="cn__plain">gRPC · 内网 mTLS</span></div><div class="set-row__d">健康检查超时 2 次 · 语义与进程内形态一致(golden 通过);不可达时宿主 fail closed,不降级</div></div>
<span class="badge badge--warn" style="font-size:11px"><span class="badge__dot"></span>受阻 <span class="mono">degraded</span></span>
</div>
</section>
<section id="backup" class="section card">
<div class="card__head"><span class="cn">迁移与备份</span><button class="icon-btn" aria-label="立即备份"><svg><use href="#i-refresh"/></svg></button></div>
<div class="set-row" style="padding:8px 0"><div class="set-row__body"><div class="set-row__t">最近全量备份</div><div class="set-row__d">今日 03:00 · 增量每 5 分钟 · 连续 7 日校验通过</div></div><span class="mono" style="font-size:12.5px;color:var(--ok)">42s RPO</span></div>
<div class="set-row" style="padding:8px 0"><div class="set-row__body"><div class="set-row__t">迁移状态</div><div class="set-row__d">Alembic head · 34 个版本 · 无漂移</div></div><span class="badge badge--ok" style="font-size:11px"><span class="badge__dot"></span>一致</span></div>
<div class="set-row" style="padding:8px 0"><div class="set-row__body"><div class="set-row__t">恢复演练</div><div class="set-row__d">09-01 通过 · 删除重放与插件撤架演练各 1 次</div></div><button class="btn btn--outline btn--sm">发起演练</button></div>
</section>
<section class="section card" style="border-color:color-mix(in srgb,var(--warn) 40%,transparent)">
<div class="card__head"><span class="cn" style="color:var(--warn)">活跃告警<span class="cn__plain">1</span></span></div>
<div style="display:flex;gap:10px;align-items:flex-start">
<svg style="width:16px;height:16px;color:var(--warn);flex:none;margin-top:3px"><use href="#i-alert"/></svg>
<div>
<div style="font-size:13.5px">对象存储写入延迟 P99 超阈值(2.8s &gt; 2s,持续 12 分钟)</div>
<div style="font-size:12px;color:var(--text-3);margin-top:4px">影响:媒体上传整备变慢 · 自动扩容已触发 · 事故响应等级 S3</div>
<div style="display:flex;gap:8px;margin-top:10px">
<button class="btn btn--outline btn--sm">查看指标</button>
<button class="btn btn--ghost btn--sm">认领处理</button>
</div>
</div>
</div>
</section>
<section id="slo" class="section card">
<div class="card__head"><span class="cn">SLO 与恢复目标</span><a class="more" href="#">指标 →</a></div>
<div class="drive-row"><span class="drive-row__label">可用性</span><div class="progress"><div class="progress__bar" style="width:99.87%"></div></div><span class="drive-row__val">99.87%</span></div>
<div class="set-row" style="padding:8px 0"><div class="set-row__body"><div class="set-row__t">RPO(数据丢失上限)</div></div><span class="mono" style="font-size:12.5px">≤ 5 分钟 · 实测 42s</span></div>
<div class="set-row" style="padding:8px 0"><div class="set-row__body"><div class="set-row__t">RTO(恢复时限)</div></div><span class="mono" style="font-size:12.5px">≤ 1 小时 · 演练 09-01 通过</span></div>
<div class="set-row" style="padding:8px 0"><div class="set-row__body"><div class="set-row__t">区域驻留</div></div><span style="font-size:12px;color:var(--ok)">cn-east-1 · 未跨区漂移</span></div>
</section>
<section id="audit" class="section card">
<div class="card__head"><span class="cn">审计快照<span class="cn__plain">管理面操作全部留痕</span></span><a class="more" href="#">完整审计 →</a></div>
<div class="timeline">
<div class="timeline-volume">卷 · 0916</div>
<div class="timeline__item"><div class="timeline__date">09-16 09:31</div><div class="timeline__title">双人授权会话开启</div><div class="timeline__sub">运维甲 + 运维乙 · 60 分钟</div></div>
<div class="timeline__item"><div class="timeline__date">09-16 09:12</div><div class="timeline__title">限观决定:帖子 #77213</div><div class="timeline__sub">双人授权 · 操作者已留痕</div></div>
<div class="timeline__item timeline__item--muted"><div class="timeline__date">09-16 02:40</div><div class="timeline__title">私箧读取尝试(自动拦截)</div><div class="timeline__sub">策略拒绝 · 管理员不可读私箧 · 已通知账户持有人</div></div>
</div>
</section>
</div>
</div>
</div>
</main>
</div>
<script src="assets/app.js?v=3"></script>
</body>
</html>
+169
View File
@@ -0,0 +1,169 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>天工阁 · Agent 任务工作台 — 魂镜 Hunjing</title>
<link rel="stylesheet" href="assets/app.css?v=10">
</head>
<body>
<svg style="display:none" aria-hidden="true"><symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol><symbol id="i-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></symbol><symbol id="i-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></symbol><symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol><symbol id="i-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></symbol><symbol id="i-layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></symbol><symbol id="i-kanban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3h4v18H5z"/><path d="M10 3h4v12h-4z"/><path d="M15 3h4v8h-4z"/></symbol><symbol id="i-package" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></symbol><symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></symbol><symbol id="i-route" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="19" r="3"/><circle cx="18" cy="5" r="3"/><path d="M12 19h4.5a3.5 3.5 0 0 0 0-7h-9a3.5 3.5 0 0 1 0-7H12"/></symbol><symbol id="i-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></symbol><symbol id="i-bag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></symbol><symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></symbol><symbol id="i-monitor" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></symbol><symbol id="i-sliders" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></symbol><symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></symbol><symbol id="i-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></symbol><symbol id="i-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/></symbol><symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="Line truncated
</svg>
<div class="app">
<aside class="sidebar">
<div class="brand">
<div class="brand__mark" aria-hidden="true"></div>
<div class="brand__text">
<div class="brand__name">魂镜</div>
<div class="brand__sub">Hunjing</div>
</div>
</div>
<nav class="side-nav" aria-label="主导航"><div class="nav-group"><div class="nav-group__title">常用</div><a class="nav-item" href="home.html"><svg><use href="#i-home"/></svg><span class="cn">镜庭</span><span class="nav-plain">首页</span></a><a class="nav-item" href="conversation.html"><svg><use href="#i-branch"/></svg><span class="cn">镜树</span><span class="nav-plain">对话</span></a><a class="nav-item" href="characters.html"><svg><use href="#i-user"/></svg><span class="cn">镜生</span><span class="nav-plain">角色</span></a><a class="nav-item" href="memory.html"><svg><use href="#i-book"/></svg><span class="cn">息壤</span><span class="nav-plain">记忆</span></a><a class="nav-item" href="avatar.html"><svg><use href="#i-layers"/></svg><span class="cn">形仪</span><span class="nav-plain">形象</span></a></div><div class="nav-group"><div class="nav-group__title">创造</div><a class="nav-item is-active" aria-current="page" href="agent-workspace.html"><svg><use href="#i-kanban"/></svg><span class="cn">天工阁</span><span class="nav-plain">任务</span></a><a class="nav-item" href="plugins.html"><svg><use href="#i-package"/></svg><span class="cn">百工坊</span><span class="nav-plain">插件</span></a><a class="nav-item" href="collaboration.html"><svg><use href="#i-users"/></svg><span class="cn">同契台</span><span class="nav-plain">协作</span></a><a class="nav-item" href="model-gateway.html"><svg><use href="#i-route"/></svg><span class="cn">神识阁</span><span class="nav-plain">模型</span></a></div><div class="nav-group"><div class="nav-group__title">世界</div><a class="nav-item" href="community.html"><svg><use href="#i-globe"/></svg><span class="cn">雅集</span><span class="nav-plain">社区</span></a><a class="nav-item" href="marketplace.html"><svg><use href="#i-bag"/></svg><span class="cn">云市</span><span class="nav-plain">市场</span></a></div><div class="nav-group"><div class="nav-group__title">治理与账户</div><a class="nav-item" href="governance.html"><svg><use href="#i-shield"/></svg><span class="cn">司命台</span><span class="nav-plain">治理</span></a><a class="nav-item" href="devices.html"><svg><use href="#i-monitor"/></svg><span class="cn">器谱</span><span class="nav-plain">设备</span></a><a class="nav-item" href="settings.html"><svg><use href="#i-sliders"/></svg><span class="cn">设置</span><span class="nav-plain">账户</span></a></div></nav>
<div class="side-foot">
<button class="icon-btn side-fold-btn" data-sidebar-fold aria-label="收起侧栏" title="收起 / 展开侧栏(记忆偏好)"><svg><use href="#i-chevrons"/></svg></button>
<span class="avatar avatar--sm avatar--user">舟</span>
<span style="font-size:12.5px">云舟</span>
<button class="icon-btn" data-theme-toggle style="margin-left:auto" aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</div>
</aside>
<main class="main">
<header class="topbar">
<div class="crumbs"><span class="cn">天工阁</span><span class="crumbs__sep">·</span><span class="here">Agent 任务工作台</span></div>
<div class="topbar__spacer"></div>
<span class="prototype-tag">静态原型 · 待审查</span>
<button class="icon-btn" data-theme-toggle aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</header>
<div class="content">
<div class="realm-banner" role="note">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
天工是功能执行者,不冒充角色身份;后台任务不构成第二个角色主体。任务可暂停、取消、审计。
</div>
<div class="char-layout">
<div>
<div class="card" style="padding:10px">
<div style="display:flex;justify-content:space-between;align-items:center;padding:4px 8px 10px">
<span class="cn" style="font-size:15px;font-weight:600">天工<span class="cn__plain">任务</span></span>
<button class="btn btn--outline btn--sm"><svg><use href="#i-plus"/></svg>新建</button>
</div>
<div class="char-list-item is-active" aria-current="page">
<div style="flex:1">
<div class="char-list-item__name" style="font-family:var(--font-ui)">整理性灵 newsletter 书单</div>
<div class="char-list-item__sub">青梧 · 分工使 2</div>
</div>
<span class="badge badge--info" style="font-size:11px;padding:1px 8px"><span class="badge__dot"></span>运转</span>
</div>
<div class="char-list-item">
<div style="flex:1">
<div class="char-list-item__name" style="font-family:var(--font-ui)">书房照片归档命名</div>
<div class="char-list-item__sub">青梧 · 已成</div>
</div>
<span class="badge badge--ok" style="font-size:11px;padding:1px 8px"><span class="badge__dot"></span>已成</span>
</div>
<div class="char-list-item">
<div style="flex:1">
<div class="char-list-item__name" style="font-family:var(--font-ui)">周报草稿 · 第 37 周</div>
<div class="char-list-item__sub">任务 Agent · 无角色身份</div>
</div>
<span class="badge badge--muted" style="font-size:11px;padding:1px 8px"><span class="badge__dot"></span>待启</span>
</div>
<div class="char-list-item">
<div style="flex:1">
<div class="char-list-item__name" style="font-family:var(--font-ui)">远洋航线资料校对</div>
<div class="char-list-item__sub">观澜 · 已止(你取消)</div>
</div>
<span class="badge badge--muted" style="font-size:11px;padding:1px 8px"><span class="badge__dot"></span>已止</span>
</div>
</div>
</div>
<div>
<div class="section card">
<div class="card__head" style="align-items:center">
<span class="cn" style="font-size:17px">整理性灵 newsletter 书单</span>
<span class="badge badge--info" style="margin-left:4px"><span class="badge__dot"></span>运转 · 运行中 <span class="mono">running</span></span>
<div style="margin-left:auto;display:flex;gap:8px">
<button class="btn btn--outline btn--sm"><svg><use href="#i-pause"/></svg>暂停</button>
<button class="btn btn--danger btn--sm"><svg><use href="#i-square"/></svg>取消</button>
</div>
</div>
<div style="font-size:12px;color:var(--text-3);display:flex;gap:16px;flex-wrap:wrap">
<span>Run <span class="mono">run_9f31c2</span></span><span>发起 · 镜树「选篇」主题</span><span>开始于 19:52</span><span>插件快照已固定</span>
</div>
</div>
<section class="section card">
<div class="card__head"><span class="cn">工筹<span class="cn__plain">执行计划</span></span><span style="margin-left:auto;font-size:11.5px;color:var(--text-3)">计划不是不可更改命令</span></div>
<div class="step step--done">
<span class="step__dot"><svg style="width:13px;height:13px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></span>
<div><div class="step__t">收集来源 <span class="badge badge--ok" style="font-size:10.5px;padding:0 7px"><span class="badge__dot"></span>已成</span></div><div class="step__d">检索使 · 性灵 newsletter 近 12 期 · ModelCall ×2 · 1.8k token</div></div>
</div>
<div class="step step--run">
<span class="step__dot">二</span>
<div><div class="step__t">去重与筛选 <span class="badge badge--info" style="font-size:10.5px;padding:0 7px"><span class="badge__dot"></span>运转中</span></div><div class="step__d">按共读偏好排序 · 依赖步骤一 · 已 62%</div></div>
</div>
<div class="step">
<span class="step__dot">三</span>
<div><div class="step__t">生成摘要 <span class="badge badge--muted" style="font-size:10.5px;padding:0 7px"><span class="badge__dot"></span>待启</span></div><div class="step__d">每本 100 字以内 · 依赖步骤二</div></div>
</div>
<div class="step">
<span class="step__dot">四</span>
<div><div class="step__t">写入草稿 <span class="badge badge--muted" style="font-size:10.5px;padding:0 7px"><span class="badge__dot"></span>待启</span></div><div class="step__d">笔记库写入需请命朱批(行契未覆盖新目录)</div></div>
</div>
</section>
<section class="section card">
<div class="card__head"><span class="cn">预算<span class="cn__plain">RunBudget</span></span></div>
<div class="drive-row"><span class="drive-row__label">Token</span><div class="progress"><div class="progress__bar" style="width:62%"></div></div><span class="drive-row__val">12.4k/20k</span></div>
<div class="drive-row"><span class="drive-row__label">费用</span><div class="progress"><div class="progress__bar" style="width:60%"></div></div><span class="drive-row__val">¥0.42/0.70</span></div>
<div class="drive-row"><span class="drive-row__label">时间</span><div class="progress progress--warn"><div class="progress__bar" style="width:42%"></div></div><span class="drive-row__val">4:12/10:00</span></div>
<p style="font-size:12px;color:var(--text-3);margin-top:8px">深度 3 层 · 并发分工使上限 4 · 任一硬上限触发即停,可续跑。</p>
</section>
<section class="section card">
<div class="card__head"><span class="cn">分工使<span class="cn__plain">子 Agent · 权限只能收窄</span></span></div>
<div class="list-row">
<span class="avatar avatar--sm">梧</span>
<div class="list-row__body"><div class="list-row__title">检索使 A</div><div class="list-row__sub">只读快照 · 无审批权 · 不写长期记忆</div></div>
<span class="badge badge--ok" style="font-size:11px"><span class="badge__dot"></span>已成</span>
</div>
<div class="list-row">
<span class="avatar avatar--sm">梧</span>
<div class="list-row__body"><div class="list-row__title">检索使 B(外部信源)</div><div class="list-row__sub">网络白名单 api.xingling.app · 工具结果只是观察,采用前需验证</div></div>
<span class="badge badge--info" style="font-size:11px"><span class="badge__dot"></span>运转</span>
</div>
<div class="list-row">
<span class="avatar avatar--sm">梧</span>
<div class="list-row__body"><div class="list-row__title">摘要使</div><div class="list-row__sub">token 上限 3k · 等待步骤二</div></div>
<span class="badge badge--muted" style="font-size:11px"><span class="badge__dot"></span>待启</span>
</div>
</section>
<section class="section card">
<div class="card__head"><span class="cn">轨迹<span class="cn__plain">可审计事件</span></span><a class="more" href="governance.html">完整诊断需双重授权 →</a></div>
<div class="timeline">
<div class="timeline__item"><div class="timeline__date">19:52:04</div><div class="timeline__title">Run 创建</div><div class="timeline__sub">镜像树消息触发 · 幂等键 idem_3c8e</div></div>
<div class="timeline__item"><div class="timeline__date">19:52:31</div><div class="timeline__title">ModelCall ×2(检索规划)</div><div class="timeline__sub">TTFT 0.9s · cache 命中 1.2k · 无重试</div></div>
<div class="timeline__item"><div class="timeline__date">19:53:10</div><div class="timeline__title">ToolCall · notes.read</div><div class="timeline__sub">读取授权目录 · Schema 校验通过 · 0.8s</div></div>
<div class="timeline__item"><div class="timeline__date">19:55:48</div><div class="timeline__title">小周天信号累积</div><div class="timeline__sub">完成信号将作为青梧大周天的唯一输入面</div></div>
</div>
</section>
<section class="section card" style="opacity:.75">
<div class="card__head"><span class="cn">验收<span class="cn__plain">完成后你来决定</span></span></div>
<div style="display:flex;gap:10px;align-items:center">
<button class="btn btn--primary" disabled>接受结果</button>
<button class="btn btn--outline" disabled>要求重做</button>
<span style="font-size:12px;color:var(--text-3)">完成后开放 · 接受前可随时取消,无副作用残留(幂等保障)</span>
</div>
</section>
</div>
</div>
</div>
</main>
</div>
<script src="assets/app.js?v=3"></script>
</body>
</html>
File diff suppressed because it is too large. Load diff
+222
View File
@@ -0,0 +1,222 @@
/* 魂镜原型 · 共享交互脚本
主题切换 · logo 彩蛋(镜启)· 主侧栏折叠 · 对话页面板折叠 · 演示态切换 */
(function () {
"use strict";
/* ---------- 主题切换(localStorage 持久化,仅原型演示用) ---------- */
var saved = null;
try { saved = localStorage.getItem("hunjing-prototype-theme"); } catch (e) { /* 忽略 */ }
if (saved === "light" || saved === "dark") {
document.documentElement.setAttribute("data-theme", saved);
}
window.hjSetTheme = function (theme) {
document.documentElement.setAttribute("data-theme", theme);
try { localStorage.setItem("hunjing-prototype-theme", theme); } catch (e) { /* 忽略 */ }
document.querySelectorAll("[data-theme-toggle]").forEach(function (btn) {
btn.setAttribute("aria-label", theme === "dark" ? "切换到月白主题" : "切换到墨色主题");
});
};
document.addEventListener("click", function (ev) {
var btn = ev.target.closest("[data-theme-toggle]");
if (!btn) return;
var cur = document.documentElement.getAttribute("data-theme") === "light" ? "light" : "dark";
window.hjSetTheme(cur === "dark" ? "light" : "dark");
});
/* ---------- logo:单击回目录;350ms 内连点 5 次触发「镜启」彩蛋 ----------
承接 main 分支旧版设计:镜启不跳转,随机呈上一句镜铭。 */
var EGG_VERSES = [
"镜中有镜,心外无心。",
"汝来看此镜时,此镜亦照汝。",
"一念起,万镜生。",
"镜非鉴物,乃鉴心也。",
"幻由心生,镜随心转。",
"铜镜本无字,照尽往来人。",
"且向镜中看,那个是谁?",
];
var logoClickSeq = 0;
var lastLogoTime = 0;
var jumpTimer = null;
function showEggToast(msg) {
var toast = document.createElement("div");
toast.className = "egg-toast";
toast.textContent = msg;
document.body.appendChild(toast);
requestAnimationFrame(function () { toast.classList.add("is-on"); });
setTimeout(function () {
toast.classList.remove("is-on");
setTimeout(function () { toast.remove(); }, 450);
}, 2400);
}
function onBrandClick(brandEl) {
clearTimeout(jumpTimer);
var now = Date.now();
var fast = now - lastLogoTime < 380; /* 380ms 内视为连点 */
lastLogoTime = now;
if (brandEl) {
brandEl.classList.add("tick");
setTimeout(function () { brandEl.classList.remove("tick"); }, 300);
}
if (fast) {
logoClickSeq += 1;
if (logoClickSeq >= 4) {
/* 首击 + 4 次连点 = 共 5 次点击:镜启彩蛋,不跳转 */
logoClickSeq = 0;
var v = EGG_VERSES[Math.floor(Math.random() * EGG_VERSES.length)];
if (brandEl) {
brandEl.classList.add("egg-pulse");
setTimeout(function () { brandEl.classList.remove("egg-pulse"); }, 1400);
}
showEggToast("镜启 · " + v);
}
return; /* 连点不跳转 */
}
logoClickSeq = 0;
/* 单击延迟跳转,为连点留出窗口 */
jumpTimer = setTimeout(function () { location.href = "index.html"; }, 380);
}
/* ---------- 主侧栏折叠(localStorage 持久化) ---------- */
function applySidebarFold(collapsed) {
document.querySelectorAll(".app").forEach(function (app) {
app.classList.toggle("app--fold", collapsed);
});
}
function sidebarFolded() {
var app = document.querySelector(".app");
return !!(app && app.classList.contains("app--fold"));
}
/* ---------- 对话页左右面板折叠 ---------- */
function panelFold(which) {
var layout = document.querySelector(".conv-layout");
if (!layout) return;
layout.classList.toggle("no-" + which);
}
/* ---------- 事件委托 ---------- */
document.addEventListener("click", function (ev) {
var t = ev.target;
if (!t || !t.closest) return;
var foldBtn = t.closest("[data-sidebar-fold]");
if (foldBtn) {
ev.preventDefault();
var collapsed = !sidebarFolded();
applySidebarFold(collapsed);
try { localStorage.setItem("hunjing-prototype-sidebar", collapsed ? "collapsed" : "expanded"); } catch (e) { /* 忽略 */ }
return;
}
var panelBtn = t.closest("[data-panel-fold]");
if (panelBtn) {
ev.preventDefault();
panelFold(panelBtn.getAttribute("data-panel-fold"));
return;
}
var brand = t.closest(".brand");
if (brand) {
ev.preventDefault();
onBrandClick(brand);
return;
}
});
/* 恢复折叠状态 */
try {
var foldSaved = localStorage.getItem("hunjing-prototype-sidebar");
if (foldSaved === "collapsed") applySidebarFold(true);
} catch (e) { /* 忽略 */ }
/* ---------- 深虑:点击 = 开启深度思考(右侧伴随力度选项);再次点击 = 关闭并隐藏选项 ---------- */
function setEffortHint(on, val) {
var hint = document.querySelector("[data-effort-hint]");
if (hint) hint.textContent = on ? "深虑 · " + val : "深虑 · 关";
}
document.addEventListener("click", function (ev) {
var t = ev.target;
if (!t || !t.closest) return;
/* 力度选项:独立控件,只调档,与深虑开关动作无关(关闭态隐藏时不响应) */
var opt = t.closest("[data-effort-seg] button[data-effort]");
if (opt) {
ev.preventDefault();
ev.stopPropagation();
if (opt.closest("[data-effort-seg]").hidden) return;
document.querySelectorAll("[data-effort-seg] button").forEach(function (b) {
b.setAttribute("aria-checked", String(b === opt));
});
setEffortHint(true, opt.getAttribute("data-effort"));
return;
}
/* 深虑按钮:开 ⇄ 关 */
var pill = t.closest('[data-pill="effort"]');
if (pill) {
ev.preventDefault();
var on = !pill.classList.contains("is-on");
pill.classList.toggle("is-on", on);
pill.setAttribute("aria-pressed", String(on));
pill.title = on
? "深虑 · 深度思考已开启,再次点击关闭"
: "深虑 · 已关闭:本条消息不思考,直出答复(推理型模型不可关)";
var seg = document.querySelector("[data-effort-seg]");
if (seg) seg.hidden = !on;
var val = "关";
if (on) {
var checked = document.querySelector('[data-effort-seg] button[aria-checked="true"]');
val = checked ? checked.getAttribute("data-effort") : "中";
}
setEffortHint(on, val);
return;
}
});
/* ---------- 博闻药丸(关→开) ---------- */
document.addEventListener("click", function (ev) {
var pill = ev.target.closest('[data-pill="search"]');
if (!pill) return;
ev.preventDefault();
var small = pill.querySelector("small");
if (!small) return;
var on = small.textContent.trim() === "开";
small.textContent = on ? "关" : "开";
pill.classList.toggle("is-on", !on);
pill.setAttribute("aria-pressed", String(!on));
});
/* ---------- 候答切换(静态演示) ---------- */
document.querySelectorAll("[data-cand-group]").forEach(function (group) {
group.querySelectorAll(".cand-tag").forEach(function (tag) {
tag.addEventListener("click", function () {
group.querySelectorAll(".cand-tag").forEach(function (tt) { tt.classList.remove("is-on"); });
tag.classList.add("is-on");
});
});
});
/* ---------- 界域分段控件:切换提示条文案 ---------- */
document.querySelectorAll("[data-realm]").forEach(function (seg) {
seg.querySelectorAll(".seg__item").forEach(function (item) {
item.addEventListener("click", function () {
seg.querySelectorAll(".seg__item").forEach(function (tt) { tt.classList.remove("is-on"); });
item.classList.add("is-on");
var banner = document.querySelector("[data-realm-banner]");
if (!banner) return;
var realm = item.getAttribute("data-realm");
if (realm === "sim") {
banner.className = "realm-banner realm-banner--sim";
} else {
banner.className = "realm-banner";
}
var html = item.getAttribute("data-banner-html");
if (html) banner.textContent = html;
});
});
});
})();
+145
View File
@@ -0,0 +1,145 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>形仪 · 形象与资产 — 魂镜 Hunjing</title>
<link rel="stylesheet" href="assets/app.css?v=10">
</head>
<body>
<svg style="display:none" aria-hidden="true"><symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol><symbol id="i-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></symbol><symbol id="i-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></symbol><symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol><symbol id="i-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></symbol><symbol id="i-layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></symbol><symbol id="i-kanban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3h4v18H5z"/><path d="M10 3h4v12h-4z"/><path d="M15 3h4v8h-4z"/></symbol><symbol id="i-package" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></symbol><symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></symbol><symbol id="i-route" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="19" r="3"/><circle cx="18" cy="5" r="3"/><path d="M12 19h4.5a3.5 3.5 0 0 0 0-7h-9a3.5 3.5 0 0 1 0-7H12"/></symbol><symbol id="i-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></symbol><symbol id="i-bag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></symbol><symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></symbol><symbol id="i-monitor" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></symbol><symbol id="i-sliders" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></symbol><symbol id="i-image" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></symbol><symbol id="i-mic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/></symbol><symbol id="i-play" viewBox="0 0 24 24" fill="currentColor" stroke="none"><polygon points="6 3 21 12 6 21 6 3"/></symbol><symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="Line truncated
</svg>
<div class="app">
<aside class="sidebar">
<div class="brand">
<div class="brand__mark" aria-hidden="true"></div>
<div class="brand__text">
<div class="brand__name">魂镜</div>
<div class="brand__sub">Hunjing</div>
</div>
</div>
<nav class="side-nav" aria-label="主导航"><div class="nav-group"><div class="nav-group__title">常用</div><a class="nav-item" href="home.html"><svg><use href="#i-home"/></svg><span class="cn">镜庭</span><span class="nav-plain">首页</span></a><a class="nav-item" href="conversation.html"><svg><use href="#i-branch"/></svg><span class="cn">镜树</span><span class="nav-plain">对话</span></a><a class="nav-item" href="characters.html"><svg><use href="#i-user"/></svg><span class="cn">镜生</span><span class="nav-plain">角色</span></a><a class="nav-item" href="memory.html"><svg><use href="#i-book"/></svg><span class="cn">息壤</span><span class="nav-plain">记忆</span></a><a class="nav-item is-active" aria-current="page" href="avatar.html"><svg><use href="#i-layers"/></svg><span class="cn">形仪</span><span class="nav-plain">形象</span></a></div><div class="nav-group"><div class="nav-group__title">创造</div><a class="nav-item" href="agent-workspace.html"><svg><use href="#i-kanban"/></svg><span class="cn">天工阁</span><span class="nav-plain">任务</span></a><a class="nav-item" href="plugins.html"><svg><use href="#i-package"/></svg><span class="cn">百工坊</span><span class="nav-plain">插件</span></a><a class="nav-item" href="collaboration.html"><svg><use href="#i-users"/></svg><span class="cn">同契台</span><span class="nav-plain">协作</span></a><a class="nav-item" href="model-gateway.html"><svg><use href="#i-route"/></svg><span class="cn">神识阁</span><span class="nav-plain">模型</span></a></div><div class="nav-group"><div class="nav-group__title">世界</div><a class="nav-item" href="community.html"><svg><use href="#i-globe"/></svg><span class="cn">雅集</span><span class="nav-plain">社区</span></a><a class="nav-item" href="marketplace.html"><svg><use href="#i-bag"/></svg><span class="cn">云市</span><span class="nav-plain">市场</span></a></div><div class="nav-group"><div class="nav-group__title">治理与账户</div><a class="nav-item" href="governance.html"><svg><use href="#i-shield"/></svg><span class="cn">司命台</span><span class="nav-plain">治理</span></a><a class="nav-item" href="devices.html"><svg><use href="#i-monitor"/></svg><span class="cn">器谱</span><span class="nav-plain">设备</span></a><a class="nav-item" href="settings.html"><svg><use href="#i-sliders"/></svg><span class="cn">设置</span><span class="nav-plain">账户</span></a></div></nav>
<div class="side-foot">
<button class="icon-btn side-fold-btn" data-sidebar-fold aria-label="收起侧栏" title="收起 / 展开侧栏(记忆偏好)"><svg><use href="#i-chevrons"/></svg></button>
<span class="avatar avatar--sm avatar--user">舟</span>
<span style="font-size:12.5px">云舟</span>
<button class="icon-btn" data-theme-toggle style="margin-left:auto" aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</div>
</aside>
<main class="main">
<header class="topbar">
<div class="crumbs"><span class="cn">形仪</span><span class="crumbs__sep">/</span><span class="here">青梧 · 形象与资产</span></div>
<div class="topbar__spacer"></div>
<span class="prototype-tag">静态原型 · 待审查</span>
<button class="icon-btn" data-theme-toggle aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</header>
<div class="content">
<div class="char-head" style="margin-bottom:20px">
<span class="avatar avatar--lg">梧</span>
<div class="char-head__title">
<h2>青梧的形仪 <span class="badge badge--muted"><span class="badge__dot"></span>形态不是角色本体</span></h2>
<div class="char-head__motto">主仪 · 守砚人立像 v3 · 真形锚已锁定稳定辨识特征</div>
</div>
<div class="char-head__actions">
<button class="btn btn--outline"><svg><use href="#i-plus"/></svg>导入资产</button>
<button class="btn btn--primary" title="3D / Live2D 编辑器为独立界面,原型阶段未单列">打开 3D / Live2D 编辑</button>
</div>
</div>
<section class="section card">
<div class="card__head">
<span class="cn">真形锚<span class="cn__plain">稳定外观参考</span></span>
<button class="btn btn--ghost btn--sm" style="margin-left:auto">编辑</button>
</div>
<div style="font-size:13px;color:var(--text-2);display:flex;flex-direction:column;gap:6px">
<div>· 稳定辨识特征:青衫束发、眉间一点朱痣、左手常年持砚</div>
<div>· 年龄表现约束:成年形象,不做年龄回退</div>
<div>· 许可与禁忌:形象由你原创 · 禁止他人商用(许可记录见来源谱系)</div>
<div>· 可变范围:服饰、场景、光照可变;面部锚点特征触及须走易相评估</div>
</div>
</section>
<section class="section">
<div class="section__head">
<span class="cn">资产库<span class="cn__plain">图像 / 声音 / Live2D / 3D</span></span>
</div>
<div class="chips">
<button class="chip is-on">全部 · 6</button>
<button class="chip">图像 · 2</button>
<button class="chip">声音 · 1</button>
<button class="chip">Live2D · 1</button>
<button class="chip">3D · 1</button>
<button class="chip">归档 · 1</button>
</div>
<div class="asset-grid">
<div class="asset-card">
<div class="asset-thumb" style="background:linear-gradient(150deg,#2c4a46,#54948a 60%,#2f5c56)"><span class="glyph">立</span><span class="asset-type">图像</span><span class="asset-flag badge badge--info" style="font-size:10.5px;padding:1px 8px">主仪</span></div>
<div class="asset-body"><span class="asset-name">守砚人立像</span><span class="asset-sub">v3 · 已定相 · 2048×2560</span>
<div class="asset-acts"><button class="btn btn--ghost btn--sm">查看</button><button class="btn btn--ghost btn--sm"><svg><use href="#i-download"/></svg>导出</button></div>
</div>
</div>
<div class="asset-card">
<div class="asset-thumb" style="background:linear-gradient(150deg,#3d4059,#6b6f95 60%,#454a6b)"><span class="glyph">冬</span><span class="asset-type">图像</span></div>
<div class="asset-body"><span class="asset-name">冬青 · 化仪</span><span class="asset-sub">v1 · 可切换形态</span>
<div class="asset-acts"><button class="btn btn--ghost btn--sm">查看</button><button class="btn btn--ghost btn--sm">设为化仪</button></div>
</div>
</div>
<div class="asset-card">
<div class="asset-thumb" style="background:linear-gradient(150deg,#4a4436,#8a7a52 60%,#57503e)"><span class="glyph">声</span><span class="asset-type">声音</span><span class="asset-flag badge badge--ok" style="font-size:10.5px;padding:1px 8px">声契 · 原创</span></div>
<div class="asset-body"><span class="asset-name">青梧原声</span><span class="asset-sub">原创设计 · 授权完整 · 转写并存</span>
<div class="asset-acts"><button class="btn btn--ghost btn--sm"><svg><use href="#i-play"/></svg>试听</button><button class="btn btn--ghost btn--sm">声音设计</button></div>
</div>
</div>
<div class="asset-card">
<div class="asset-thumb" style="background:linear-gradient(150deg,#233b37,#3f7a6e 60%,#2a4d47)"><span class="glyph">灵</span><span class="asset-type">Live2D</span></div>
<div class="asset-body"><span class="asset-name">守砚人 · 灵动版</span><span class="asset-sub">Cubism 兼容导入 · 校验通过 · 参数映射 42 项</span>
<div class="asset-acts"><button class="btn btn--ghost btn--sm">编辑</button><button class="btn btn--ghost btn--sm">运行预览</button></div>
</div>
</div>
<div class="asset-card">
<div class="asset-thumb" style="background:linear-gradient(150deg,#37343f,#635d7a 60%,#443f52)"><span class="glyph">三</span><span class="asset-type">3D · VRM</span></div>
<div class="asset-body"><span class="asset-name">守砚人 VRM</span><span class="asset-sub">导入验证与标准化通过 · 骨骼 54 · 表情 12</span>
<div class="asset-acts"><button class="btn btn--ghost btn--sm">编辑</button><button class="btn btn--ghost btn--sm"><svg><use href="#i-download"/></svg>导出</button></div>
</div>
</div>
<div class="asset-card" style="opacity:.72">
<div class="asset-thumb" style="background:linear-gradient(150deg,#2e3236,#4c545b 60%,#383d42)"><span class="glyph">旧</span><span class="asset-type">图像 · 归档</span></div>
<div class="asset-body"><span class="asset-name">旧版立像 v2</span><span class="asset-sub">易相历史保留 · 可回看不可覆盖</span>
<div class="asset-acts"><button class="btn btn--ghost btn--sm">回看</button></div>
</div>
</div>
</div>
</section>
<section class="section card">
<div class="card__head">
<span class="cn">易相<span class="cn__plain">正式形仪的版本化改变</span></span>
<a class="more" href="#">全部 3 次 →</a>
</div>
<div class="timeline">
<div class="timeline__item">
<div class="timeline__date">2024-03-05</div>
<div class="timeline__title cn">主仪 v1 · 初版立像</div>
<div class="timeline__sub">来源:手动上传</div>
</div>
<div class="timeline__item">
<div class="timeline__date">2025-11-20</div>
<div class="timeline__title cn">易相 v2 · 换季服饰(未触及真形锚)</div>
<div class="timeline__sub">轻流程 · 直接生效</div>
</div>
<div class="timeline__item">
<div class="timeline__date">2026-08-30</div>
<div class="timeline__title cn">易相 v3 · 面部细节重绘(触及真形锚)</div>
<div class="timeline__sub">已做身份连续性评估 · 谱系留痕</div>
</div>
</div>
<p style="font-size:12px;color:var(--text-3);margin-top:10px">情相 · 情态的受控表现:可访问性友好,不作为情绪施压载体;叙仪 · 境界或幻境中的叙事形态,不改变主仪与真形锚。</p>
</section>
</div>
</main>
</div>
<script src="assets/app.js?v=3"></script>
</body>
</html>
+233
View File
@@ -0,0 +1,233 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>镜生 · 角色 — 魂镜 Hunjing</title>
<link rel="stylesheet" href="assets/app.css?v=10">
</head>
<body>
<svg style="display:none" aria-hidden="true">
<symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol>
<symbol id="i-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></symbol>
<symbol id="i-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></symbol>
<symbol id="i-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></symbol>
<symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol>
<symbol id="i-layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></symbol>
<symbol id="i-kanban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3h4v18H5z"/><path d="M10 3h4v12h-4z"/><path d="M15 3h4v8h-4z"/></symbol>
<symbol id="i-package" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></symbol>
<symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></symbol>
<symbol id="i-bag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></symbol>
<symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></symbol>
<symbol id="i-route" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="19" r="3"/><circle cx="18" cy="5" r="3"/><path d="M12 19h4.5a3.5 3.5 0 0 0 0-7h-9a3.5 3.5 0 0 1 0-7H12"/></symbol>
<symbol id="i-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></symbol>
<symbol id="i-monitor" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></symbol>
<symbol id="i-sliders" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></symbol>
<symbol id="i-chevrons" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="11 17 6 12 11 7"/><polyline points="18 17 13 12 18 7"/></symbol>
</svg>
<div class="app">
<aside class="sidebar">
<div class="brand">
<div class="brand__mark" aria-hidden="true"></div>
<div class="brand__text">
<div class="brand__name">魂镜</div>
<div class="brand__sub">Hunjing</div>
</div>
</div>
<nav class="side-nav" aria-label="主导航"><div class="nav-group"><div class="nav-group__title">常用</div><a class="nav-item" href="home.html"><svg><use href="#i-home"/></svg><span class="cn">镜庭</span><span class="nav-plain">首页</span></a><a class="nav-item" href="conversation.html"><svg><use href="#i-branch"/></svg><span class="cn">镜树</span><span class="nav-plain">对话</span></a><a class="nav-item is-active" aria-current="page" href="characters.html"><svg><use href="#i-user"/></svg><span class="cn">镜生</span><span class="nav-plain">角色</span></a><a class="nav-item" href="memory.html"><svg><use href="#i-book"/></svg><span class="cn">息壤</span><span class="nav-plain">记忆</span></a><a class="nav-item" href="avatar.html"><svg><use href="#i-layers"/></svg><span class="cn">形仪</span><span class="nav-plain">形象</span></a></div><div class="nav-group"><div class="nav-group__title">创造</div><a class="nav-item" href="agent-workspace.html"><svg><use href="#i-kanban"/></svg><span class="cn">天工阁</span><span class="nav-plain">任务</span></a><a class="nav-item" href="plugins.html"><svg><use href="#i-package"/></svg><span class="cn">百工坊</span><span class="nav-plain">插件</span></a><a class="nav-item" href="collaboration.html"><svg><use href="#i-users"/></svg><span class="cn">同契台</span><span class="nav-plain">协作</span></a><a class="nav-item" href="model-gateway.html"><svg><use href="#i-route"/></svg><span class="cn">神识阁</span><span class="nav-plain">模型</span></a></div><div class="nav-group"><div class="nav-group__title">世界</div><a class="nav-item" href="community.html"><svg><use href="#i-globe"/></svg><span class="cn">雅集</span><span class="nav-plain">社区</span></a><a class="nav-item" href="marketplace.html"><svg><use href="#i-bag"/></svg><span class="cn">云市</span><span class="nav-plain">市场</span></a></div><div class="nav-group"><div class="nav-group__title">治理与账户</div><a class="nav-item" href="governance.html"><svg><use href="#i-shield"/></svg><span class="cn">司命台</span><span class="nav-plain">治理</span></a><a class="nav-item" href="devices.html"><svg><use href="#i-monitor"/></svg><span class="cn">器谱</span><span class="nav-plain">设备</span></a><a class="nav-item" href="settings.html"><svg><use href="#i-sliders"/></svg><span class="cn">设置</span><span class="nav-plain">账户</span></a></div></nav>
<div class="side-foot">
<button class="icon-btn side-fold-btn" data-sidebar-fold aria-label="收起侧栏" title="收起 / 展开侧栏(记忆偏好)"><svg><use href="#i-chevrons"/></svg></button>
<span class="avatar avatar--sm avatar--user">舟</span>
<span style="font-size:12.5px">云舟</span>
<button class="icon-btn" data-theme-toggle style="margin-left:auto" aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</div>
</aside>
<main class="main">
<header class="topbar">
<div class="crumbs"><span class="cn">镜生</span><span class="crumbs__sep">/</span><span class="here">青梧</span></div>
<div class="topbar__spacer"></div>
<span class="prototype-tag">静态原型 · 待审查</span>
<button class="icon-btn" aria-label="搜索"><svg><use href="#i-search"/></svg></button>
<button class="icon-btn" aria-label="通知"><svg><use href="#i-bell"/></svg></button>
</header>
<div class="content content--wide">
<div class="char-layout">
<!-- 左:角色列表 -->
<div>
<div class="card" style="padding:10px">
<div style="display:flex;justify-content:space-between;align-items:center;padding:4px 8px 10px">
<span class="cn" style="font-size:15px;font-weight:600">镜生<span class="cn__plain">4 位角色</span></span>
<button class="btn btn--outline btn--sm"><svg><use href="#i-plus"/></svg>创建</button>
</div>
<div class="char-list-item is-active" aria-current="page">
<span class="avatar">梧</span>
<div style="flex:1">
<div class="char-list-item__name">青梧</div>
<div class="char-list-item__sub">守砚人 · 道体 v3</div>
</div>
<span class="badge badge--info badge--sm" style="font-size:11px;padding:1px 8px"><span class="badge__dot"></span>临照</span>
</div>
<div class="char-list-item">
<span class="avatar">袖</span>
<div style="flex:1">
<div class="char-list-item__name">雪袖</div>
<div class="char-list-item__sub">茶室主人 · 道体 v1</div>
</div>
<span class="badge badge--muted" style="font-size:11px;padding:1px 8px"><span class="badge__dot"></span>守一</span>
</div>
<div class="char-list-item">
<span class="avatar">澜</span>
<div style="flex:1">
<div class="char-list-item__name">观澜</div>
<div class="char-list-item__sub">航海士 · 道体 v2</div>
</div>
<span class="badge badge--muted" style="font-size:11px;padding:1px 8px"><span class="badge__dot"></span>静修</span>
</div>
<div class="char-list-item">
<span class="avatar" style="filter:grayscale(.7);opacity:.75">岁</span>
<div style="flex:1">
<div class="char-list-item__name">旧岁</div>
<div class="char-list-item__sub">守岁人 · 归藏</div>
</div>
<span class="badge badge--muted" style="font-size:11px;padding:1px 8px"><span class="badge__dot"></span>归藏</span>
</div>
<div style="border-top:1px solid var(--line-soft);margin-top:8px;padding-top:8px">
<a class="char-list-item" href="#">
<svg style="width:24px;height:24px;color:var(--text-3)" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
<div style="flex:1">
<div class="char-list-item__name" style="font-family:var(--font-ui);font-weight:400;color:var(--text-2)">随缘 · 生成匿名灵胚候选</div>
<div class="char-list-item__sub">按偏好生成草稿 · 不是抽卡</div>
</div>
</a>
</div>
</div>
</div>
<!-- 右:角色详情 -->
<div>
<div class="char-banner" aria-hidden="true"></div>
<div class="char-head">
<span class="avatar avatar--lg">梧</span>
<div class="char-head__title">
<h2>青梧 <span class="badge badge--info"><span class="badge__dot"></span>临照 · 前台运行 <span class="mono">running</span></span></h2>
<div class="char-head__motto cn">「砚为田,字为稼。」<span class="cn__plain">守砚人 · 你与我共读的第三年</span></div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<span class="mem-type">道体 v3</span>
<span class="mem-type">镜域 · 个人域</span>
<span class="mem-type">来源 · 手动创建</span>
</div>
</div>
<div class="char-head__actions">
<a class="btn btn--primary" href="conversation.html">进入镜树</a>
<button class="btn btn--outline"><svg><use href="#i-download"/></svg>导出魂籍</button>
</div>
</div>
<section class="section card">
<div class="card__head">
<span class="cn">道体<span class="cn__plain">稳定身份基线</span></span>
<span style="margin-left:auto;font-size:11.5px;color:var(--text-3);max-width:300px;text-align:right">普通对话与记忆不能静默覆盖道体</span>
</div>
<p style="font-size:13.5px;color:var(--text-2);line-height:1.75">
青梧是江南书斋的守砚人:语调平缓,好引张岱与《陶庵梦忆》;记账一丝不苟,惜字如金。
<strong style="color:var(--text-1)">基线由 v1 逐步重映而来</strong>——人格、语言风格与知识边界在版本中留痕,任何重大改动都会先成候选、经你确认。
</p>
</section>
<section class="section card">
<div class="card__head">
<span class="cn">缘契<span class="cn__plain">你与青梧的关系</span></span>
<span class="badge" style="margin-left:auto"><span class="badge__dot" style="background:var(--accent)"></span>共读者 · v4</span>
</div>
<p style="font-size:13.5px;color:var(--text-2)">「隔周四晚共读,青梧负责选篇与笔记;你不喜长篇说教。」——最近一次铭契于 8 月 28 日确认,旧关系状态保留版本可查。</p>
</section>
<section class="section card">
<div class="card__head">
<span class="cn">内驱力<span class="cn__plain">动机与倾向维度 · 可审阅编辑</span></span>
<button class="btn btn--ghost btn--sm" style="margin-left:auto">编辑</button>
</div>
<div class="drive-row"><span class="firefly firefly--bright" title="内驱力亮度 = 维度值" aria-hidden="true"></span><span class="drive-row__label">求知</span><div class="progress"><div class="progress__bar" style="width:80%"></div></div><span class="drive-row__val">0.80</span></div>
<div class="drive-row"><span class="firefly" aria-hidden="true"></span><span class="drive-row__label">条理</span><div class="progress"><div class="progress__bar" style="width:65%"></div></div><span class="drive-row__val">0.65</span></div>
<div class="drive-row"><span class="firefly firefly--dim" aria-hidden="true"></span><span class="drive-row__label">陪伴</span><div class="progress"><div class="progress__bar" style="width:40%"></div></div><span class="drive-row__val">0.40</span></div>
<p style="font-size:12px;color:var(--text-3);margin-top:8px">内驱力是声明式设定数据,不是不可控本能;变化与大周天评估相关,每次调整版本化留痕。</p>
</section>
<section class="section card">
<div class="card__head">
<span class="cn">大周天<span class="cn__plain">自我评估与演化循环</span></span>
<span class="badge badge--ok" style="margin-left:auto"><span class="badge__dot"></span>已成 · 第 7 轮 <span class="mono">completed</span></span>
</div>
<div class="cycle-mirrors" role="group" aria-label="大周天四阶段">
<div class="cycle-mirror is-done"><span class="cycle-mirror__disc">回</span><span class="cycle-mirror__name">回顾</span><span class="cycle-mirror__state">已成 · 09-14</span></div>
<span class="cycle-mirror__link" aria-hidden="true"></span>
<div class="cycle-mirror is-done"><span class="cycle-mirror__disc">照</span><span class="cycle-mirror__name">对照</span><span class="cycle-mirror__state">已成 · 09-14</span></div>
<span class="cycle-mirror__link" aria-hidden="true"></span>
<div class="cycle-mirror is-done"><span class="cycle-mirror__disc">议</span><span class="cycle-mirror__name">提议</span><span class="cycle-mirror__state">已成 · 09-15</span></div>
<span class="cycle-mirror__link" aria-hidden="true"></span>
<div class="cycle-mirror is-active"><span class="cycle-mirror__disc">呈</span><span class="cycle-mirror__name">提交</span><span class="cycle-mirror__state">2 项待你确认</span></div>
</div>
<p style="font-size:12.5px;color:var(--text-2)">本轮由 42 次小周天完成信号触发 · 预算 5k token(实用 3.1k)· 产出:人格候选 1、世界认知候选 1。<span style="color:var(--text-3)">演化经你确认才生效;循环可整体关闭。</span></p>
<div style="display:flex;gap:8px;margin-top:10px">
<button class="btn btn--outline btn--sm">审阅候选</button>
<button class="btn btn--ghost btn--sm">查看本轮输入与费用</button>
</div>
</section>
<section class="section card">
<div class="card__head">
<span class="cn">身份谱系<span class="cn__plain">来源与重大演变</span></span>
<span class="more" style="color:var(--text-3);cursor:default">完整谱系 · 见下方时间线</span>
</div>
<div class="timeline">
<div class="timeline__item">
<div class="timeline__date">2024-03-02</div>
<div class="timeline__title cn">启镜 · 正式创建</div>
<div class="timeline__sub">手动创建 · 道体 v1 · 初照自陈已归真</div>
</div>
<div class="timeline__item">
<div class="timeline__date">2025-06-18</div>
<div class="timeline__title cn">重映 · 道体 v2</div>
<div class="timeline__sub">语言风格修订 · 旧版本保留</div>
</div>
<div class="timeline__item">
<div class="timeline__date">2026-01-11</div>
<div class="timeline__title cn">分灵 · 雪袖自此谱系分出</div>
<div class="timeline__sub">新角色新 ID · 谱系可验证 · 不冒充原角色</div>
</div>
<div class="timeline__item">
<div class="timeline__date">2026-08-28</div>
<div class="timeline__title cn">重映 · 道体 v3(当前)</div>
<div class="timeline__sub">知识边界修订 · 经候选确认</div>
</div>
</div>
</section>
<section class="section card danger-zone">
<div class="card__head">
<span class="cn">生命周期<span class="cn__plain">影响角色状态的操作 · 均可逆或有冷静期,与数据删除分离</span></span>
</div>
<div style="display:flex;gap:10px;flex-wrap:wrap">
<button class="btn btn--outline btn--sm">静修 · 停止运行</button>
<button class="btn btn--outline btn--sm">归藏 · 隐藏可恢复</button>
<button class="btn btn--outline btn--sm">复照 · 从旧版本重开</button>
<button class="btn btn--danger btn--sm">铸魂 · 重大重塑(强确认)</button>
<button class="btn btn--danger btn--sm">死亡 · 永久闭合(冷静期)</button>
</div>
<p style="font-size:12px;color:var(--text-3);margin-top:10px">铸魂与死亡均走强确认与谱系留痕;死亡不等于删除数据,只读档案保留。删除角色数据是独立操作,入口在司命台。</p>
</section>
</div>
</div>
</div>
</main>
</div>
<script src="assets/app.js?v=3"></script>
</body>
</html>
+114
View File
@@ -0,0 +1,114 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>同契台 · 协作空间 — 魂镜 Hunjing</title>
<link rel="stylesheet" href="assets/app.css?v=10">
</head>
<body>
<svg style="display:none" aria-hidden="true"><symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol><symbol id="i-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></symbol><symbol id="i-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></symbol><symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol><symbol id="i-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></symbol><symbol id="i-layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></symbol><symbol id="i-kanban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3h4v18H5z"/><path d="M10 3h4v12h-4z"/><path d="M15 3h4v8h-4z"/></symbol><symbol id="i-package" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></symbol><symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></symbol><symbol id="i-route" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="19" r="3"/><circle cx="18" cy="5" r="3"/><path d="M12 19h4.5a3.5 3.5 0 0 0 0-7h-9a3.5 3.5 0 0 1 0-7H12"/></symbol><symbol id="i-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></symbol><symbol id="i-bag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></symbol><symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></symbol><symbol id="i-monitor" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></symbol><symbol id="i-sliders" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></symbol><symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></symbol><symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></symbol><symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></symbol><symbol id="i-merge" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-lLine truncated
</svg>
<div class="app">
<aside class="sidebar">
<div class="brand">
<div class="brand__mark" aria-hidden="true"></div>
<div class="brand__text">
<div class="brand__name">魂镜</div>
<div class="brand__sub">Hunjing</div>
</div>
</div>
<nav class="side-nav" aria-label="主导航"><div class="nav-group"><div class="nav-group__title">常用</div><a class="nav-item" href="home.html"><svg><use href="#i-home"/></svg><span class="cn">镜庭</span><span class="nav-plain">首页</span></a><a class="nav-item" href="conversation.html"><svg><use href="#i-branch"/></svg><span class="cn">镜树</span><span class="nav-plain">对话</span></a><a class="nav-item" href="characters.html"><svg><use href="#i-user"/></svg><span class="cn">镜生</span><span class="nav-plain">角色</span></a><a class="nav-item" href="memory.html"><svg><use href="#i-book"/></svg><span class="cn">息壤</span><span class="nav-plain">记忆</span></a><a class="nav-item" href="avatar.html"><svg><use href="#i-layers"/></svg><span class="cn">形仪</span><span class="nav-plain">形象</span></a></div><div class="nav-group"><div class="nav-group__title">创造</div><a class="nav-item" href="agent-workspace.html"><svg><use href="#i-kanban"/></svg><span class="cn">天工阁</span><span class="nav-plain">任务</span></a><a class="nav-item" href="plugins.html"><svg><use href="#i-package"/></svg><span class="cn">百工坊</span><span class="nav-plain">插件</span></a><a class="nav-item is-active" aria-current="page" href="collaboration.html"><svg><use href="#i-users"/></svg><span class="cn">同契台</span><span class="nav-plain">协作</span></a><a class="nav-item" href="model-gateway.html"><svg><use href="#i-route"/></svg><span class="cn">神识阁</span><span class="nav-plain">模型</span></a></div><div class="nav-group"><div class="nav-group__title">世界</div><a class="nav-item" href="community.html"><svg><use href="#i-globe"/></svg><span class="cn">雅集</span><span class="nav-plain">社区</span></a><a class="nav-item" href="marketplace.html"><svg><use href="#i-bag"/></svg><span class="cn">云市</span><span class="nav-plain">市场</span></a></div><div class="nav-group"><div class="nav-group__title">治理与账户</div><a class="nav-item" href="governance.html"><svg><use href="#i-shield"/></svg><span class="cn">司命台</span><span class="nav-plain">治理</span></a><a class="nav-item" href="devices.html"><svg><use href="#i-monitor"/></svg><span class="cn">器谱</span><span class="nav-plain">设备</span></a><a class="nav-item" href="settings.html"><svg><use href="#i-sliders"/></svg><span class="cn">设置</span><span class="nav-plain">账户</span></a></div></nav>
<div class="side-foot">
<button class="icon-btn side-fold-btn" data-sidebar-fold aria-label="收起侧栏" title="收起 / 展开侧栏(记忆偏好)"><svg><use href="#i-chevrons"/></svg></button>
<span class="avatar avatar--sm avatar--user">舟</span>
<span style="font-size:12.5px">云舟</span>
<button class="icon-btn" data-theme-toggle style="margin-left:auto" aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</div>
</aside>
<main class="main">
<header class="topbar">
<div class="crumbs"><span class="cn">同契台</span><span class="crumbs__sep">/</span><span class="here">镜斋工作室</span></div>
<div class="topbar__spacer"></div>
<span class="prototype-tag">静态原型 · 待审查</span>
<button class="icon-btn" data-theme-toggle aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</header>
<div class="content">
<div class="char-head" style="margin-bottom:20px">
<span class="avatar avatar--lg">斋</span>
<div class="char-head__title">
<h2>镜斋工作室 <span class="badge badge--info"><span class="badge__dot"></span>共修 · 2 人在线 <span class="mono">editing</span></span></h2>
<div class="char-head__motto">成员 4 · 邀请制 · 编辑 / 发布 / 财务权限分别授予</div>
</div>
<div class="char-head__actions">
<button class="btn btn--outline">邀请成员</button>
<button class="btn btn--primary">进入共修</button>
</div>
</div>
<div class="char-layout">
<div>
<div class="card" style="padding:10px">
<div style="padding:4px 8px 10px"><span class="cn" style="font-size:15px;font-weight:600">分支<span class="cn__plain">版本</span></span></div>
<div class="char-list-item is-active" aria-current="page">
<div style="flex:1"><div class="char-list-item__name" style="font-family:var(--font-ui)">选篇修订</div><div class="char-list-item__sub">v3 草稿 · 审稿意见 2</div></div>
<span class="badge badge--warn" style="font-size:11px;padding:1px 8px"><span class="badge__dot"></span>审稿中</span>
</div>
<div class="char-list-item">
<div style="flex:1"><div class="char-list-item__name" style="font-family:var(--font-ui)">main</div><div class="char-list-item__sub">v12 · 已刊 · 共读手册</div></div>
<span class="badge badge--ok" style="font-size:11px;padding:1px 8px"><span class="badge__dot"></span>已刊</span>
</div>
<div class="char-list-item">
<div style="flex:1"><div class="char-list-item__name" style="font-family:var(--font-ui)">资产重制</div><div class="char-list-item__sub">v1 草稿 · Live2D 分层</div></div>
<span class="badge badge--muted" style="font-size:11px;padding:1px 8px"><span class="badge__dot"></span>草稿</span>
</div>
</div>
</div>
<div>
<section class="section card">
<div class="card__head"><span class="cn">选篇修订<span class="cn__plain">v3 草稿</span></span><button class="btn btn--primary btn--sm" style="margin-left:auto">提交合卷</button></div>
<div class="timeline">
<div class="timeline__item"><div class="timeline__date">09-14 21:30</div><div class="timeline__title">v3 草稿 · 云舟</div><div class="timeline__sub">按青梧建议调整三、四讲顺序</div></div>
<div class="timeline__item"><div class="timeline__date">09-14 22:10</div><div class="timeline__title">审稿意见 ×2 · 阿沉</div><div class="timeline__sub">「定海水操放第五讲更顺」「附页地图需更新版本」</div></div>
<div class="timeline__item timeline__item--muted"><div class="timeline__date">09-10 19:00</div><div class="timeline__title">v2 合卷 · 已并入 main</div><div class="timeline__sub">冲突 1 处已手工处理</div></div>
</div>
<div style="display:flex;gap:10px;margin-top:14px">
<button class="btn btn--outline btn--sm"><svg><use href="#i-check"/></svg>批准本稿</button>
<button class="btn btn--outline btn--sm"><svg><use href="#i-x"/></svg>请求修改</button>
<span style="font-size:12px;color:var(--text-3);align-self:center">批准后需两名编辑合卷方可并入 main</span>
</div>
</section>
<section class="section card">
<div class="card__head"><span class="cn">成员权限<span class="cn__plain">编辑 / 发布 / 上架 / 财务分离</span></span></div>
<div class="table-wrap"><table class="data">
<thead><tr><th>成员</th><th>编辑</th><th>发布</th><th>市场上架</th><th>财务</th></tr></thead>
<tbody>
<tr><td>云舟(主理)</td><td class="mark-ok">✓</td><td class="mark-ok">✓</td><td class="mark-ok">✓</td><td class="mark-ok">✓</td></tr>
<tr><td>阿沉</td><td class="mark-ok">✓</td><td class="mark-ok">✓</td><td class="mark-no">—</td><td class="mark-no">—</td></tr>
<tr><td>临江仙</td><td class="mark-ok">✓</td><td class="mark-no">—</td><td class="mark-no">—</td><td class="mark-no">—</td></tr>
<tr><td>山客(只读)</td><td class="mark-no">—</td><td class="mark-no">—</td><td class="mark-no">—</td><td class="mark-no">—</td></tr>
</tbody>
</table></div>
<p style="font-size:12px;color:var(--text-3);margin-top:10px">成员移除后立即失去编辑权;已合并版本的署名与来源保留。</p>
</section>
<section class="section card">
<div class="card__head"><span class="cn">活动记录</span></div>
<div class="timeline">
<div class="timeline__item"><div class="timeline__date">09-14 22:10</div><div class="timeline__title">阿沉 提交审稿意见</div></div>
<div class="timeline__item"><div class="timeline__date">09-12 10:05</div><div class="timeline__title">云舟 创建分支「选篇修订」</div></div>
<div class="timeline__item"><div class="timeline__date">09-10 19:00</div><div class="timeline__title">v2 合卷并入 main(两名编辑批准)</div></div>
</div>
</section>
</div>
</div>
</div>
</main>
</div>
<script src="assets/app.js?v=3"></script>
</body>
</html>
+141
View File
@@ -0,0 +1,141 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>雅集 · 社区广场 — 魂镜 Hunjing</title>
<link rel="stylesheet" href="assets/app.css?v=10">
</head>
<body>
<svg style="display:none" aria-hidden="true"><symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol><symbol id="i-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></symbol><symbol id="i-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></symbol><symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol><symbol id="i-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></symbol><symbol id="i-layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></symbol><symbol id="i-kanban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3h4v18H5z"/><path d="M10 3h4v12h-4z"/><path d="M15 3h4v8h-4z"/></symbol><symbol id="i-package" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></symbol><symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></symbol><symbol id="i-route" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="19" r="3"/><circle cx="18" cy="5" r="3"/><path d="M12 19h4.5a3.5 3.5 0 0 0 0-7h-9a3.5 3.5 0 0 1 0-7H12"/></symbol><symbol id="i-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></symbol><symbol id="i-bag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></symbol><symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></symbol><symbol id="i-monitor" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></symbol><symbol id="i-sliders" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></symbol><symbol id="i-heart" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></symbol><symbol id="i-quote" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"/><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"/></symbol><symbolLine truncated
</svg>
<div class="app">
<aside class="sidebar">
<div class="brand">
<div class="brand__mark" aria-hidden="true"></div>
<div class="brand__text">
<div class="brand__name">魂镜</div>
<div class="brand__sub">Hunjing</div>
</div>
</div>
<nav class="side-nav" aria-label="主导航"><div class="nav-group"><div class="nav-group__title">常用</div><a class="nav-item" href="home.html"><svg><use href="#i-home"/></svg><span class="cn">镜庭</span><span class="nav-plain">首页</span></a><a class="nav-item" href="conversation.html"><svg><use href="#i-branch"/></svg><span class="cn">镜树</span><span class="nav-plain">对话</span></a><a class="nav-item" href="characters.html"><svg><use href="#i-user"/></svg><span class="cn">镜生</span><span class="nav-plain">角色</span></a><a class="nav-item" href="memory.html"><svg><use href="#i-book"/></svg><span class="cn">息壤</span><span class="nav-plain">记忆</span></a><a class="nav-item" href="avatar.html"><svg><use href="#i-layers"/></svg><span class="cn">形仪</span><span class="nav-plain">形象</span></a></div><div class="nav-group"><div class="nav-group__title">创造</div><a class="nav-item" href="agent-workspace.html"><svg><use href="#i-kanban"/></svg><span class="cn">天工阁</span><span class="nav-plain">任务</span></a><a class="nav-item" href="plugins.html"><svg><use href="#i-package"/></svg><span class="cn">百工坊</span><span class="nav-plain">插件</span></a><a class="nav-item" href="collaboration.html"><svg><use href="#i-users"/></svg><span class="cn">同契台</span><span class="nav-plain">协作</span></a><a class="nav-item" href="model-gateway.html"><svg><use href="#i-route"/></svg><span class="cn">神识阁</span><span class="nav-plain">模型</span></a></div><div class="nav-group"><div class="nav-group__title">世界</div><a class="nav-item is-active" aria-current="page" href="community.html"><svg><use href="#i-globe"/></svg><span class="cn">雅集</span><span class="nav-plain">社区</span></a><a class="nav-item" href="marketplace.html"><svg><use href="#i-bag"/></svg><span class="cn">云市</span><span class="nav-plain">市场</span></a></div><div class="nav-group"><div class="nav-group__title">治理与账户</div><a class="nav-item" href="governance.html"><svg><use href="#i-shield"/></svg><span class="cn">司命台</span><span class="nav-plain">治理</span></a><a class="nav-item" href="devices.html"><svg><use href="#i-monitor"/></svg><span class="cn">器谱</span><span class="nav-plain">设备</span></a><a class="nav-item" href="settings.html"><svg><use href="#i-sliders"/></svg><span class="cn">设置</span><span class="nav-plain">账户</span></a></div></nav>
<div class="side-foot">
<button class="icon-btn side-fold-btn" data-sidebar-fold aria-label="收起侧栏" title="收起 / 展开侧栏(记忆偏好)"><svg><use href="#i-chevrons"/></svg></button>
<span class="avatar avatar--sm avatar--user">舟</span>
<span style="font-size:12.5px">云舟</span>
<button class="icon-btn" data-theme-toggle style="margin-left:auto" aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</div>
</aside>
<main class="main">
<header class="topbar">
<div class="crumbs"><span class="cn">雅集</span><span class="crumbs__sep">·</span><span class="here">社区广场</span></div>
<div class="topbar__spacer"></div>
<span class="prototype-tag">静态原型 · 待审查</span>
<button class="icon-btn" data-theme-toggle aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</header>
<div class="content">
<div style="display:flex;gap:12px;align-items:center;margin-bottom:18px;flex-wrap:wrap">
<div class="chips" style="margin-bottom:0">
<button class="chip is-on">动态</button>
<button class="chip">角色谱</button>
<button class="chip">作品</button>
<button class="chip">世界设定</button>
<button class="chip">关注</button>
</div>
<div class="searchbar" style="flex:1;min-width:200px">
<svg><use href="#i-search"/></svg>
<input type="search" placeholder="搜索社区:作者 / 角色 / 话题 / 世界" aria-label="搜索社区">
</div>
<button class="btn btn--primary">发布</button>
</div>
<div class="two-col">
<div>
<div class="card post" style="margin-bottom:14px">
<div class="post__head">
<span class="avatar avatar--sm">沉</span>
<div><div class="post__author">沉舟 <span style="font-size:11.5px;color:var(--text-3)">· 9 月 14 日</span></div></div>
<span class="badge badge--ok" style="margin-left:auto"><span class="badge__dot"></span>角色发布 <span class="mono">published</span></span>
</div>
<div class="post__body">
<p style="color:var(--text-1);font-size:14.5px" class="cn">发布角色「照夜 · 掌灯人」<span class="cn__plain">v2 · 市契 CC BY-NC 4.0</span></p>
<p>渡口掌灯的角色,话少但记得每个过客的名字。基于我自己的镜生谱系重映而来,Fork 请保留署名。</p>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<span class="mem-type">许可 · CC BY-NC</span><span class="mem-type">分灵谱系 · 可验证</span><span class="mem-type">分级 · 全年龄</span>
</div>
</div>
<div class="post__foot">
<button class="act-btn"><svg><use href="#i-heart"/></svg>128</button>
<button class="act-btn"><svg><use href="#i-quote"/></svg>评论 23</button>
<button class="act-btn">收藏 45</button>
<button class="act-btn">Fork 12</button>
<button class="act-btn" style="margin-left:auto"><svg><use href="#i-flag"/></svg>鸣冤</button>
</div>
</div>
<div class="card post" style="margin-bottom:14px">
<div class="post__head">
<span class="avatar avatar--sm">梧</span>
<div><div class="post__author">青梧与云舟 <span style="font-size:11.5px;color:var(--text-3)">· 9 月 13 日</span></div></div>
</div>
<div class="post__body">
<p>共读《陶庵梦忆》第一讲笔记:湖心亭那一百六十字,读三遍的方法真的有效。附角色整理的选篇结构图。</p>
<blockquote class="quote">莫说相公痴,更有痴似相公者。<span class="quote__src">引文锚 · 《湖心亭看雪》</span></blockquote>
</div>
<div class="post__foot">
<button class="act-btn"><svg><use href="#i-heart"/></svg>86</button>
<button class="act-btn"><svg><use href="#i-quote"/></svg>评论 14</button>
<button class="act-btn">收藏 31</button>
<button class="act-btn" style="margin-left:auto"><svg><use href="#i-flag"/></svg>鸣冤</button>
</div>
</div>
<div class="card post">
<div class="post__head">
<span class="avatar avatar--sm">袖</span>
<div><div class="post__author">雪袖的茶寮 <span style="font-size:11.5px;color:var(--text-3)">· 9 月 12 日</span></div></div>
<span class="badge badge--ok" style="margin-left:auto"><span class="badge__dot"></span>世界设定 <span class="mono">published</span></span>
</div>
<div class="post__body">
<p style="color:var(--text-1);font-size:14.5px" class="cn">世界「雾中茶寮」第三章<span class="cn__plain">境界 · 世界正史 v5</span></p>
<p>雨季的茶寮只在雾散时开门。本章收录 6 个新场景与 2 位过客的世界认知候选来源说明。</p>
</div>
<div class="post__foot">
<button class="act-btn"><svg><use href="#i-heart"/></svg>54</button>
<button class="act-btn"><svg><use href="#i-quote"/></svg>评论 9</button>
<button class="act-btn">收藏 22</button>
<button class="act-btn" style="margin-left:auto"><svg><use href="#i-flag"/></svg>鸣冤</button>
</div>
</div>
</div>
<div>
<section class="card" style="margin-bottom:14px">
<div class="card__head"><span class="cn">角色谱<span class="cn__plain">角色社区精选</span></span><span class="more" style="color:var(--text-3);cursor:default">全部 · 角色谱整页在 React 阶段实现</span></div>
<div class="list-row"><span class="avatar avatar--sm">夜</span><div class="list-row__body"><div class="list-row__title">照夜 · 掌灯人</div><div class="list-row__sub">沉舟 · Fork 128 · 全年龄</div></div></div>
<div class="list-row"><span class="avatar avatar--sm">筑</span><div class="list-row__body"><div class="list-row__title">筑秋 · 修槎人</div><div class="list-row__sub">临江仙 · Fork 76</div></div></div>
<div class="list-row"><span class="avatar avatar--sm">岱</span><div class="list-row__body"><div class="list-row__title">岱舆 · 观星者</div><div class="list-row__sub">山客 · Fork 54</div></div></div>
</section>
<section class="card" style="margin-bottom:14px">
<div class="card__head"><span class="cn">话题</span></div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<span class="mem-type">#陶庵梦忆</span><span class="mem-type">#共读笔记</span><span class="mem-type">#Live2D</span><span class="mem-type">#雾中茶寮</span><span class="mem-type">#守砚人</span>
</div>
</section>
<section class="card">
<div class="card__head"><span class="cn">推荐解释<span class="cn__plain">为什么看到这些</span></span></div>
<p style="font-size:12.5px;color:var(--text-2);margin-bottom:10px">当前动态基于你最近的共读活动与关注关系排序。推荐不扩大任何权限,也不使用你的私密角色数据。</p>
<button class="btn btn--outline btn--sm">重置推荐特征</button>
</section>
</div>
</div>
</div>
</main>
</div>
<script src="assets/app.js?v=3"></script>
</body>
</html>
+552
View File
@@ -0,0 +1,552 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>镜树 · 分支对话 — 魂镜 Hunjing</title>
<link rel="stylesheet" href="assets/app.css?v=10">
</head>
<body>
<svg style="display:none" aria-hidden="true">
<symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol>
<symbol id="i-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></symbol>
<symbol id="i-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></symbol>
<symbol id="i-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></symbol>
<symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol>
<symbol id="i-layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></symbol>
<symbol id="i-kanban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3h4v18H5z"/><path d="M10 3h4v12h-4z"/><path d="M15 3h4v8h-4z"/></symbol>
<symbol id="i-package" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></symbol>
<symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></symbol>
<symbol id="i-bag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></symbol>
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></symbol>
<symbol id="i-send" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></symbol>
<symbol id="i-paperclip" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></symbol>
<symbol id="i-image" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></symbol>
<symbol id="i-mic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/></symbol>
<symbol id="i-refresh" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></symbol>
<symbol id="i-quote" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"/><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"/></symbol>
<symbol id="i-split" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M13 6h3a2 2 0 0 1 2 2v7"/><path d="M6 9v12"/></symbol>
<symbol id="i-more" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></symbol>
<symbol id="i-play" viewBox="0 0 24 24" fill="currentColor" stroke="none"><polygon points="6 3 21 12 6 21 6 3"/></symbol>
<symbol id="i-arrow-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></symbol>
<symbol id="i-route" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="19" r="3"/><circle cx="18" cy="5" r="3"/><path d="M12 19h4.5a3.5 3.5 0 0 0 0-7h-9a3.5 3.5 0 0 1 0-7H12"/></symbol>
<symbol id="i-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></symbol>
<symbol id="i-monitor" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></symbol>
<symbol id="i-sliders" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></symbol>
<symbol id="i-chevrons" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="11 17 6 12 11 7"/><polyline points="18 17 13 12 18 7"/></symbol>
</svg>
<div class="app">
<aside class="sidebar">
<div class="brand">
<div class="brand__mark" aria-hidden="true"></div>
<div class="brand__text">
<div class="brand__name">魂镜</div>
<div class="brand__sub">Hunjing</div>
</div>
</div>
<nav class="side-nav" aria-label="主导航"><div class="nav-group"><div class="nav-group__title">常用</div><a class="nav-item" href="home.html"><svg><use href="#i-home"/></svg><span class="cn">镜庭</span><span class="nav-plain">首页</span></a><a class="nav-item is-active" aria-current="page" href="conversation.html"><svg><use href="#i-branch"/></svg><span class="cn">镜树</span><span class="nav-plain">对话</span></a><a class="nav-item" href="characters.html"><svg><use href="#i-user"/></svg><span class="cn">镜生</span><span class="nav-plain">角色</span></a><a class="nav-item" href="memory.html"><svg><use href="#i-book"/></svg><span class="cn">息壤</span><span class="nav-plain">记忆</span></a><a class="nav-item" href="avatar.html"><svg><use href="#i-layers"/></svg><span class="cn">形仪</span><span class="nav-plain">形象</span></a></div><div class="nav-group"><div class="nav-group__title">创造</div><a class="nav-item" href="agent-workspace.html"><svg><use href="#i-kanban"/></svg><span class="cn">天工阁</span><span class="nav-plain">任务</span></a><a class="nav-item" href="plugins.html"><svg><use href="#i-package"/></svg><span class="cn">百工坊</span><span class="nav-plain">插件</span></a><a class="nav-item" href="collaboration.html"><svg><use href="#i-users"/></svg><span class="cn">同契台</span><span class="nav-plain">协作</span></a><a class="nav-item" href="model-gateway.html"><svg><use href="#i-route"/></svg><span class="cn">神识阁</span><span class="nav-plain">模型</span></a></div><div class="nav-group"><div class="nav-group__title">世界</div><a class="nav-item" href="community.html"><svg><use href="#i-globe"/></svg><span class="cn">雅集</span><span class="nav-plain">社区</span></a><a class="nav-item" href="marketplace.html"><svg><use href="#i-bag"/></svg><span class="cn">云市</span><span class="nav-plain">市场</span></a></div><div class="nav-group"><div class="nav-group__title">治理与账户</div><a class="nav-item" href="governance.html"><svg><use href="#i-shield"/></svg><span class="cn">司命台</span><span class="nav-plain">治理</span></a><a class="nav-item" href="devices.html"><svg><use href="#i-monitor"/></svg><span class="cn">器谱</span><span class="nav-plain">设备</span></a><a class="nav-item" href="settings.html"><svg><use href="#i-sliders"/></svg><span class="cn">设置</span><span class="nav-plain">账户</span></a></div></nav>
<div class="side-foot">
<button class="icon-btn side-fold-btn" data-sidebar-fold aria-label="收起侧栏" title="收起 / 展开侧栏(记忆偏好)"><svg><use href="#i-chevrons"/></svg></button>
<span class="avatar avatar--sm avatar--user">舟</span>
<span style="font-size:12.5px">云舟</span>
<button class="icon-btn" data-theme-toggle style="margin-left:auto" aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</div>
</aside>
<main class="main">
<header class="topbar">
<div class="crumbs">
<span class="cn">镜树</span><span class="crumbs__sep">/</span><span class="here">《陶庵梦忆》共读选篇</span>
</div>
<div class="seg" data-realm role="group" aria-label="对话界域切换">
<button class="seg__item is-on" data-realm="reality" aria-pressed="true" data-banner-html='真界 · 当前是现实与正式共同经历的界域,内容按记忆策略进入长期记忆。'><span class="cn">真界</span>现实</button>
<button class="seg__item" data-realm="world" aria-pressed="false" data-banner-html='境界 · 版本化维护的世界正史界域;内容只成世界知识候选。'><span class="cn">境界</span>世界正史</button>
<button class="seg__item seg__item--sim" data-realm="sim" aria-pressed="false" data-banner-html='幻境 · 临时试演界域:不影响现实与正史,不自动入忆;离开即留痕可查。'><span class="cn">幻境</span>试演</button>
</div>
<span class="realm-dot" style="font-size:12px">真界</span>
<div class="topbar__spacer"></div>
<span class="prototype-tag">静态原型 · 待审查</span>
<button class="icon-btn" data-theme-toggle aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</header>
<div class="conv-layout">
<!-- 左:会话列表 -->
<aside class="conv-side" aria-label="会话列表">
<div class="conv-side__head">
<span class="avatar avatar--sm">梧</span>
<div style="flex:1">
<div class="cn" style="font-weight:600">青梧</div>
<div style="font-size:11px;color:var(--text-3)">道体 v3 · 临照</div>
</div>
<button class="panel-toggle" data-panel-fold="left" aria-label="收起会话列表" title="收起会话列表" style="margin-left:4px"><svg><use href="#i-chevrons"/></svg></button>
<button class="icon-btn" aria-label="新对话"><svg><use href="#i-plus"/></svg></button>
</div>
<div class="conv-group__title">今日</div>
<a class="conv-item is-active" href="conversation.html" aria-current="page">
<div class="conv-item__title"><span class="conv-item__dot conv-item__dot--active" title="活跃"></span><span class="cn" style="font-weight:500">《陶庵梦忆》共读选篇</span></div>
<div class="conv-item__sub">六篇已定,结构图也画好了… · 真界</div>
</a>
<div class="conv-group__title">本周</div>
<a class="conv-item" href="conversation.html">
<div class="conv-item__title"><span class="conv-item__dot conv-item__dot--sim" title="幻境试演"></span>书房整理方案</div>
<div class="conv-item__sub">幻境试演 · 不动真格</div>
</a>
<a class="conv-item" href="conversation.html">
<div class="conv-item__title"><span class="conv-item__dot conv-item__dot--risk" title="有权限待处理"></span>预习提纲写入</div>
<div class="conv-item__sub">请命中 · 等待朱批</div>
</a>
<div class="conv-group__title">更早</div>
<a class="conv-item" href="conversation.html">
<div class="conv-item__title"><span class="conv-item__dot"></span>砚台保养备忘</div>
<div class="conv-item__sub">已结题 · 可复照</div>
</a>
<a class="conv-item" href="conversation.html">
<div class="conv-item__title"><span class="conv-item__dot"></span>移栽:关于「移栽自读书会」</div>
<div class="conv-item__sub">从读书会树移栽而来</div>
</a>
</aside>
<!-- 中:消息流 -->
<section class="conv-main">
<button class="conv-restore conv-restore--left" data-panel-fold="left" aria-label="展开会话列表" title="展开会话列表"><svg><use href="#i-chevrons"/></svg></button>
<button class="conv-restore conv-restore--right" data-panel-fold="right" aria-label="展开分支面板" title="展开分支与上下文面板"><svg style="transform:rotate(180deg)"><use href="#i-chevrons"/></svg></button>
<div class="conv-stream">
<div class="conv-stream__inner">
<div class="realm-banner" data-realm-banner role="status">
真界 · 当前是现实与正式共同经历的界域,内容按记忆策略进入长期记忆。
</div>
<!-- 问节 ① -->
<div class="msg msg--user">
<div class="msg__meta"><span class="cn">问节</span><span class="mono">m_8a3f</span><span>2026-09-15 19:42</span></div>
<div class="bubble">帮我把读书会这学期的《陶庵梦忆》共读选篇定下来:选六篇,兼顾篇幅和趣味,大家隔周四晚讨论。</div>
</div>
<!-- 答复 ①:markdown + 表格 + mermaid + 引文锚 -->
<div class="msg">
<div class="msg--char__head">
<span class="avatar">梧</span>
<span class="msg--char__name">青梧</span>
<span class="msg--char__meta"><span class="mono" style="font-size:11px;color:var(--text-3)">道体 v3</span><span class="badge badge--ok"><span class="badge__dot"></span>已成 <span class="mono">completed</span></span></span>
</div>
<div class="msg__blocks">
<details class="think-block">
<summary>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/></svg>
思考 · 已收敛为摘要
<span class="think-effort">深虑 · 中</span>
<span class="think-meta">思考 1.2k token · 用时 9s</span>
<svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
</summary>
<div class="think-body">
<p>用户要为读书会从《陶庵梦忆》定六篇,兼顾篇幅与趣味,隔周四讨论。需要考虑:成员含两位新人(首篇宜短宜奇)、整体要有递进结构而非并列、末篇宜收束回张岱本人。以篇目字数与趣味点对照评估,湖心亭看雪最短且白描极致,适合开卷;自为墓志铭最长且是回望性文本,适合收官;中间四篇按「雪—夜—池—海」的意象递进排列。</p>
</div>
<div class="think-note">思考摘要仅本会话内可见,可丢弃重建;不写入长期记录,不进入诊断导出。</div>
</details>
<div class="block block--text">
<div class="block__body md">
<p>选好了。六篇按<strong>「起—承—转—合」</strong>推进:从最短的《湖心亭看雪》起,让新人先尝到味道;雪、夜、池、海层层铺开,末篇收在<em>自为墓志铭</em>,正好回到张岱自己。</p>
</div>
</div>
<div class="block">
<div class="block__head">选篇清单</div>
<div class="table-wrap">
<table class="data">
<thead><tr><th>次序</th><th>篇目</th><th>约字数</th><th>趣味点</th></tr></thead>
<tbody>
<tr><td>一</td><td>湖心亭看雪</td><td>160</td><td>白描极致 · 入门首选</td></tr>
<tr><td>二</td><td>卷中雪</td><td>700</td><td>炉火与夜读</td></tr>
<tr><td>三</td><td>金山夜戏</td><td>900</td><td>寺院里点灯唱戏</td></tr>
<tr><td>四</td><td>庞公池</td><td>600</td><td>枕舟而眠</td></tr>
<tr><td>五</td><td>定海水操</td><td>1100</td><td>海上观兵</td></tr>
<tr><td>六</td><td>自为墓志铭</td><td>1300</td><td>回望一生 · 收束全卷</td></tr>
</tbody>
</table>
</div>
</div>
<div class="block">
<div class="block__head"><span>mermaid</span><span>flowchart · 完成后 strict mode 渲染</span></div>
<div class="mermaid-box">
<svg viewBox="0 0 660 96" style="width:100%;min-width:560px;height:auto" role="img" aria-label="六讲推进结构示意图:起承转合四段">
<g font-family="inherit" font-size="12.5">
<rect x="8" y="28" width="128" height="40" rx="9" fill="none" stroke="var(--accent)" stroke-opacity=".55"/>
<text x="72" y="46" text-anchor="middle" fill="var(--text-1)" font-weight="600">起 · 湖心亭看雪</text>
<text x="72" y="61" text-anchor="middle" fill="var(--text-3)" font-size="10.5">第一周 · 白描</text>
<line x1="136" y1="48" x2="164" y2="48" stroke="var(--text-3)" stroke-width="1.2"/><polygon points="164,48 157,44.5 157,51.5" fill="var(--text-3)"/>
<rect x="166" y="28" width="128" height="40" rx="9" fill="none" stroke="var(--accent)" stroke-opacity=".55"/>
<text x="230" y="46" text-anchor="middle" fill="var(--text-1)" font-weight="600">承 · 雪 / 夜</text>
<text x="230" y="61" text-anchor="middle" fill="var(--text-3)" font-size="10.5">卷中雪 · 金山夜戏</text>
<line x1="294" y1="48" x2="322" y2="48" stroke="var(--text-3)" stroke-width="1.2"/><polygon points="322,48 315,44.5 315,51.5" fill="var(--text-3)"/>
<rect x="324" y="28" width="128" height="40" rx="9" fill="none" stroke="var(--accent)" stroke-opacity=".55"/>
<text x="388" y="46" text-anchor="middle" fill="var(--text-1)" font-weight="600">转 · 池 / 海</text>
<text x="388" y="61" text-anchor="middle" fill="var(--text-3)" font-size="10.5">庞公池 · 定海水操</text>
<line x1="452" y1="48" x2="480" y2="48" stroke="var(--text-3)" stroke-width="1.2"/><polygon points="480,48 473,44.5 473,51.5" fill="var(--text-3)"/>
<rect x="482" y="28" width="128" height="40" rx="9" fill="var(--accent)" fill-opacity=".12" stroke="var(--accent)" stroke-opacity=".7"/>
<text x="546" y="46" text-anchor="middle" fill="var(--accent-strong)" font-weight="600">合 · 自为墓志铭</text>
<text x="546" y="61" text-anchor="middle" fill="var(--text-3)" font-size="10.5">末周 · 回望</text>
</g>
</svg>
</div>
<div class="mermaid-cap">图 1 · 六讲推进结构(SVG 已净化 · 可缩放 / 下载 / 查看源码)</div>
</div>
<blockquote class="quote">莫说相公痴,更有痴似相公者。
<span class="quote__src">引文锚 · 《陶庵梦忆 · 湖心亭看雪》 · anchor_c41</span>
</blockquote>
</div>
<div class="msg__actions">
<button class="act-btn"><svg><use href="#i-refresh"/></svg>候答(2)</button>
<button class="act-btn"><svg><use href="#i-quote"/></svg>引用</button>
<button class="act-btn"><svg><use href="#i-arrow-right"/></svg>续思</button>
<button class="act-btn"><svg><use href="#i-split"/></svg>移栽</button>
<button class="act-btn"><svg><use href="#i-more"/></svg>更多</button>
</div>
</div>
<!-- 问节 ② -->
<div class="msg msg--user">
<div class="msg__meta"><span class="cn">问节</span><span class="mono">m_91d0</span><span>19:47</span></div>
<div class="bubble">好。把六次讨论排进我的日历:从下周四 19:30 开始,隔周一次,每次 90 分钟,标题统一叫「共读 · 第 N 讲」。</div>
</div>
<!-- 答复 ②:工具 + 请命(已朱批)+ 结果 -->
<div class="msg">
<div class="msg--char__head">
<span class="avatar">梧</span>
<span class="msg--char__name">青梧</span>
<span class="msg--char__meta"><span class="mono" style="font-size:11px;color:var(--text-3)">道体 v3</span><span class="badge badge--ok"><span class="badge__dot"></span>已成 <span class="mono">completed</span></span></span>
</div>
<div class="msg__blocks">
<div class="block tool">
<div class="tool__head">
<span class="tool__name">calendar.write</span>
<span class="tool__purpose">在日历创建 6 个循环事件</span>
<span class="badge badge--ok" style="margin-left:auto"><span class="badge__dot"></span>已成 · 已完成 <span class="mono">completed</span></span>
</div>
<div class="tool__body">
<div class="tool__row"><span class="k2">参数摘要</span><span>6 个事件 · 90 分钟 · 间隔 14 天 · 起始 2026-09-24 19:30 (UTC+8)</span></div>
<div class="tool__stat"><span>耗时 1.2s</span><span>插件 · 系统日历(Core)</span><span>幂等键 <span class="mono">idem_5b2c</span></span></div>
</div>
</div>
<div class="block approval approval--done">
<div class="approval__head">
<span class="approval__seal" aria-hidden="true">批</span>
<div>
<div class="approval__title"><span class="cn">请命 · 已朱批</span><span class="cn__plain">本次确认 · 已由你允许</span></div>
<div style="font-size:11.5px;color:var(--text-3)">2026-09-15 19:48 允许 · 留痕于司命台</div>
</div>
<span class="badge badge--ok" style="margin-left:auto"><span class="badge__dot"></span>允许 <span class="mono">approved</span></span>
</div>
<div class="approval__body">
<div class="fact"><span class="fact__k">动作</span><span class="fact__v">在日历创建 6 个「共读 · 第 N 讲」事件</span></div>
<div class="fact"><span class="fact__k">影响</span><span class="fact__v">写入你的日历;不读取其他日程</span></div>
<div class="fact"><span class="fact__k">数据范围</span><span class="fact__v">日历写入 · 仅此 6 条 <span class="mono">calendar:write:once</span></span></div>
<div class="fact"><span class="fact__k">费用</span><span class="fact__v">无(本地工具)</span></div>
<div class="fact"><span class="fact__k">有效期</span><span class="fact__v">本次执行,不形成持续授权</span></div>
</div>
</div>
<div class="block block--text">
<div class="block__body md">
<p>已写入。首场 9 月 24 日(周四)19:30,《湖心亭看雪》——正好赶上秋分后的第一场凉。预习提示我会在每讲前三天放进讨论区。</p>
</div>
</div>
</div>
<div class="msg__actions">
<button class="act-btn"><svg><use href="#i-quote"/></svg>引用</button>
<button class="act-btn"><svg><use href="#i-more"/></svg>更多</button>
</div>
</div>
<!-- 息壤候选 -->
<div class="msg">
<div class="msg__blocks" style="padding-left:48px">
<div class="block memory-cand">
<div class="memory-cand__head">
<span class="cn" style="font-weight:600;color:var(--warn)">息壤候选</span><span class="cn__plain">记忆候选 · 归真后才会影响未来上下文</span>
<span class="mem-type" style="margin-left:auto">偏好 preference</span>
</div>
<div class="memory-cand__body">
<p class="q">「读书会隔周四 19:30 举行,每次 90 分钟。」</p>
<p style="margin-top:4px;font-size:12px;color:var(--text-3)">来源 · 本对话 m_91d0 · 置信度 0.92 · 敏感度低</p>
<div class="memory-cand__acts">
<button class="btn btn--outline btn--sm">归真入忆</button>
<button class="btn btn--ghost btn--sm">本次忽略</button>
<button class="btn btn--ghost btn--sm">查看来源</button>
</div>
</div>
</div>
</div>
</div>
<!-- 问节 ③ -->
<div class="msg msg--user">
<div class="msg__meta"><span class="cn">问节</span><span class="mono">m_a207</span><span>20:02</span></div>
<div class="bubble">第一讲的开场白怎么讲?给两个版本我挑。</div>
</div>
<!-- 答复 ③:候答 A(主脉)+ 音频块;残答封存 -->
<div class="msg">
<div class="msg--char__head">
<span class="avatar">梧</span>
<span class="msg--char__name">青梧</span>
<span class="msg--char__meta"><span class="mono" style="font-size:11px;color:var(--text-3)">道体 v3</span><span class="badge badge--ok"><span class="badge__dot"></span>已成 <span class="mono">completed</span></span></span>
</div>
<div class="candidates" style="padding-left:0;margin-top:-4px">
<div class="cand-switch" data-cand-group aria-label="候答切换">
<button class="cand-tag is-on" aria-pressed="true">候答 A · 已归真(主脉)</button>
<button class="cand-tag" aria-pressed="false">候答 B · 更克制</button>
</div>
<span style="font-size:11.5px;color:var(--text-3)">同一问节的两个回答候选 · 切换不覆盖彼此</span>
</div>
<div class="msg__blocks">
<div class="block block--text">
<div class="block__body md">
<p>**版本 A(从雪夜进入)**——「崇祯五年十二月,余住西湖。大雪三日,湖中人鸟声俱绝。」今晚我们不急着分析,先把这一百六十字读三遍:第一遍看雪,第二遍看舟,第三遍看那个「痴」字。</p>
<p><em>音频版我念了一遍,配了半分钟的环境音。</em></p>
</div>
</div>
<div class="block">
<div class="audio">
<button class="audio__play" aria-label="播放开场白语音(0:42)"><svg><use href="#i-play"/></svg></button>
<div class="wave" aria-hidden="true">
<i style="height:9px"></i><i style="height:14px"></i><i style="height:20px"></i><i style="height:11px"></i><i style="height:17px"></i><i style="height:24px"></i><i style="height:15px"></i><i style="height:8px"></i><i style="height:13px"></i><i style="height:21px"></i><i style="height:27px"></i><i style="height:18px"></i><i style="height:10px"></i><i style="height:16px"></i><i style="height:23px"></i><i style="height:29px"></i><i style="height:19px"></i><i style="height:12px"></i><i style="height:7px"></i><i style="height:15px"></i><i style="height:22px"></i><i style="height:28px"></i><i style="height:17px"></i><i style="height:11px"></i><i style="height:19px"></i><i style="height:25px"></i><i style="height:14px"></i><i style="height:9px"></i><i style="height:16px"></i><i style="height:21px"></i><i style="height:12px"></i>
</div>
<div class="audio__meta">
<span class="t">开场白 · 语音版</span>
<span class="audio__time">0:42 · 声契 · 青梧原声</span>
</div>
</div>
<div class="mermaid-cap">转写:「崇祯五年十二月,余住西湖……」(全文 214 字 · 已加字幕)</div>
</div>
</div>
<div class="msg__actions">
<button class="act-btn"><svg><use href="#i-refresh"/></svg>重生成</button>
<button class="act-btn"><svg><use href="#i-quote"/></svg>引用</button>
<button class="act-btn"><svg><use href="#i-more"/></svg>更多</button>
</div>
<div class="sealed" style="margin-top:2px">
<span class="badge badge--muted" style="flex:none"><span class="badge__dot"></span>残答 · 已封存 <span class="mono">interrupted</span></span>
<div class="sealed__body">
<div class="sealed__t">推演枝 D · 生成中断于 68%,内容封存不再修改。</div>
<div style="font-size:11.5px">残答不作为完成答复展示 · 引用合流或</div>
</div>
<button class="btn btn--outline btn--sm"><svg><use href="#i-arrow-right"/></svg>续生新节点</button>
</div>
</div>
<!-- 问节 ④ -->
<div class="msg msg--user">
<div class="msg__meta"><span class="cn">问节</span><span class="mono">m_b6f1</span><span>20:10</span></div>
<div class="bubble">再把每讲的预习提纲写成六份文档,存进我的笔记库「读书会」目录。</div>
</div>
<!-- 答复 ④:工具 + 请命(等待朱批)+ 芽体 -->
<div class="msg">
<div class="msg--char__head">
<span class="avatar">梧</span>
<span class="msg--char__name">青梧</span>
<span class="msg--char__meta"><span class="mono" style="font-size:11px;color:var(--text-3)">道体 v3</span><span class="badge badge--warn"><span class="badge__dot"></span>请命中 · 等待确认 <span class="mono">waiting_approval</span></span></span>
</div>
<div class="msg__blocks">
<div class="block tool" style="border-left-color:var(--warn)">
<div class="tool__head">
<span class="tool__name">notes.write_files</span>
<span class="tool__purpose">在笔记库写入 6 份预习提纲</span>
<span class="badge badge--warn" style="margin-left:auto"><span class="badge__dot"></span>请命中 · 等待确认 <span class="mono">waiting_approval</span></span>
</div>
<div class="tool__body">
<div class="tool__row"><span class="k2">参数摘要</span><span>目录 <span class="mono">/读书会/2026秋/</span> · 6 个 Markdown 文件 · 每份约 40 行</span></div>
<div class="tool__stat"><span>插件 · 笔记同步(已安装 · 已启用)</span><span>路径已校验在授权根内</span></div>
</div>
</div>
<div class="block approval">
<div class="approval__head">
<span class="approval__seal" aria-hidden="true">命</span>
<div>
<div class="approval__title"><span class="cn">请命</span><span class="cn__plain">请求你的确认 · 未确认不执行</span> <span class="risk-badge" style="margin-left:4px">RISK L2</span></div>
<div style="font-size:11.5px;color:var(--text-3)">拒绝或超时均不执行 · 无默认允许 · L2:写操作,范围已限死</div>
</div>
</div>
<div class="approval__body">
<div class="fact"><span class="fact__k">动作</span><span class="fact__v">在笔记库「读书会」目录创建 6 份 Markdown 预习提纲</span></div>
<div class="fact"><span class="fact__k">影响</span><span class="fact__v">新增 6 个文件;不修改、不删除现有笔记</span></div>
<div class="fact"><span class="fact__k">数据范围</span><span class="fact__v">笔记库写入 · 仅该目录 <span class="mono">notes:write:/读书会/</span></span></div>
<div class="fact"><span class="fact__k">费用</span><span class="fact__v">6 次文件写入 · 无额外模型费用(提纲已在本次生成)</span></div>
<div class="fact"><span class="fact__k">有效期</span><span class="fact__v">本次执行 · 勾选「行契」可限 30 天内同类免请命</span></div>
</div>
<div class="approval__acts">
<button class="btn btn--cinnabar" aria-label="确认决定:允许">允许(朱批)</button>
<button class="btn btn--outline" aria-label="确认决定:拒绝">拒绝</button>
<span class="approval__note">朱批 · 你的明确决定,允许或拒绝都留痕。</span>
</div>
</div>
</div>
</div>
<!-- 问节 ⑤ + 临时芽体 -->
<div class="msg msg--user">
<div class="msg__meta"><span class="cn">问节</span><span class="mono">m_c944</span><span>20:12</span></div>
<div class="bubble">在等的时候,先给我一版 30 分钟的精简流程,怕第一次拖堂。</div>
</div>
<div class="msg">
<div class="msg--char__head">
<span class="avatar">梧</span>
<span class="msg--char__name">青梧</span>
<span class="msg--char__meta"><span class="mono" style="font-size:11px;color:var(--text-3)">道体 v3</span><span class="badge badge--info"><span class="badge__dot"></span>运转 · 生成中 <span class="mono">streaming</span></span></span>
</div>
<div class="msg__blocks">
<details class="think-block is-thinking" open>
<summary>
<span class="think-dot" aria-hidden="true"></span>
正在思考
<span class="think-effort">深虑 · 中</span>
<span class="think-meta">思考 0.4k token · 深虑档位可在输入区调整</span>
<svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
</summary>
<div class="think-body think-live">
<p>用户担心首次共读拖堂,要 30 分钟精简版。需要压缩朗读与讨论比例:三分钟齐读保留仪式感,两个问题足够聚焦——「雪大不大」对应白描力度,「痴不痴」对应人物自我认知,比四个问题更利于新人开口……</p>
<p>末段用舟子那句话收尾,与开场「痴」字形成回环,30 分钟结构即<span class="think-cursor" aria-hidden="true"></span></p>
</div>
</details>
<div class="block sprout">
<div class="sprout__head">
<span class="cn" style="font-weight:600">临时芽体</span><span class="cn__plain">流式生成中 · 完成后才固化</span>
<span style="margin-left:auto" class="mono">block 2/4 · delta 118</span>
</div>
<div class="sprout__body">
<div class="md">
<p>30 分钟精简版:前三分钟齐读原文,随后只留两个问题——「雪大不大」和「痴不痴」,最后五分钟交给「舟子那句话」收尾。<span class="stream-cursor" aria-hidden="true"></span></p>
</div>
<div class="src-line" style="margin-top:10px"><span class="unclosed">**时间分配**(未闭合的 Markdown 结构保持源码态,完成后解析)</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 输入区 -->
<div class="composer">
<div class="composer__inner">
<div class="composer__box">
<textarea class="composer__ta" rows="2" placeholder="对青梧说……(Enter 发送 · Shift+Enter 换行)" aria-label="消息输入"></textarea>
<div class="composer__bar">
<button class="icon-btn" aria-label="附件"><svg><use href="#i-paperclip"/></svg></button>
<button class="icon-btn" aria-label="图片"><svg><use href="#i-image"/></svg></button>
<button class="icon-btn" aria-label="实时语音"><svg><use href="#i-mic"/></svg></button>
<button class="pill-btn is-on" data-pill="effort" aria-pressed="true" title="深虑 · 点击开启深度思考,再次点击关闭">深虑</button>
<span class="effort-seg" data-effort-seg role="radiogroup" aria-label="推理力度">
<button role="radio" aria-checked="false" data-effort="低" title="低 · 快省">低</button>
<button role="radio" aria-checked="true" data-effort="中" title="中 · 均衡">中</button>
<button role="radio" aria-checked="false" data-effort="高" title="高 · 深究慢">高</button>
</span>
<button class="pill-btn" data-pill="search" title="博闻 · 本条消息启用联网检索(走百工坊白名单,点击开关)" aria-pressed="false">博闻<small>关</small></button>
<span class="spacer" style="flex:1"></span>
<button class="btn btn--primary btn--sm" aria-label="发送(呈镜)" title="发送 · 呈镜"><svg><use href="#i-send"/></svg>呈镜</button>
</div>
</div>
<div class="composer__hint">
<span>界域 <span class="k">真界 · 现实</span></span>
<span>路径 <span class="k">主脉</span></span>
<span>模型 <span class="k">会话临时 · 覆盖自用户偏好</span></span>
<span>思考 <span class="k" data-effort-hint>深虑 · 中</span>(点深虑开关,右侧调力度)</span>
<span>上下文 <span class="k">2.1k / 8k token</span></span>
</div>
</div>
</div>
</section>
<!-- 右:镜枝面板 -->
<aside class="conv-ctx" aria-label="分支与上下文">
<div style="display:flex;justify-content:flex-end;margin-bottom:6px">
<button class="panel-toggle" data-panel-fold="right" aria-label="收起面板" title="收起分支与上下文面板"><svg style="transform:rotate(180deg)"><use href="#i-chevrons"/></svg></button>
</div>
<section class="ctx-section">
<div class="ctx-section__head">
<span class="cn">镜枝<span class="cn__plain">分支结构</span></span>
<span class="more" style="color:var(--text-3);cursor:default">树视图</span>
</div>
<div class="tree" role="group" aria-label="本主题分支树">
<ul>
<li>
<div class="tree__node tree__node--main"><span class="nm">问节 · 选篇</span><span class="tree__tag tree__tag--main">主脉 ✓</span></div>
<ul>
<li><div class="tree__node tree__node--main"><span class="nm">候答 A · 六篇 + 结构图</span><span class="tree__tag tree__tag--main">归真</span></div></li>
<li><div class="tree__node"><span class="nm">推演枝 · 按「梦」分组(弃)</span></div></li>
</ul>
</li>
<li>
<div class="tree__node tree__node--main"><span class="nm">问节 · 排日历</span><span class="tree__tag tree__tag--main">主脉 ✓</span></div>
<ul>
<li><div class="tree__node tree__node--main"><span class="nm">候答 · 已朱批执行</span></div></li>
</ul>
</li>
<li>
<div class="tree__node tree__node--main"><span class="nm">问节 · 开场白</span></div>
<ul>
<li><div class="tree__node tree__node--main"><span class="nm">候答 A · 雪夜进入</span><span class="tree__tag tree__tag--main">归真</span></div></li>
<li><div class="tree__node"><span class="nm">候答 B · 更克制</span></div></li>
<li><div class="tree__node tree__node--sealed"><span class="nm">推演枝 D</span><span class="tree__tag tree__tag--seal">残答</span></div></li>
</ul>
</li>
<li>
<div class="tree__node tree__node--main"><span class="nm">问节 · 预习提纲</span><span class="tree__tag" style="border-color:color-mix(in srgb,var(--warn) 45%,transparent);color:var(--warn)">请命中</span></div>
</li>
<li>
<div class="tree__node"><span class="nm">问节 · 30 分钟精简版</span>
<ul>
<li><div class="tree__node tree__node--now"><span class="nm">芽体 · 生成中</span><span class="tree__tag tree__tag--bud">streaming</span></div></li>
</ul>
</div>
</li>
</ul>
</div>
</section>
<section class="ctx-section">
<div class="ctx-section__head">
<span class="cn">上下文镜<span class="cn__plain">本次实际采用</span></span>
<a class="more" href="governance.html">明细(诊断需授权)</a>
</div>
<div class="card" style="padding:12px 14px">
<div class="ctx-mirror__row"><span class="ctx-mirror__type">明忆</span><span class="ctx-mirror__txt">读书会隔周四 19:30 · 90 分钟</span><span class="ctx-mirror__src">mem_v12</span></div>
<div class="ctx-mirror__row"><span class="ctx-mirror__type">实录</span><span class="ctx-mirror__txt">3 月购入《陶庵梦忆》校注本</span><span class="ctx-mirror__src">fact_07d</span></div>
<div class="ctx-mirror__row"><span class="ctx-mirror__type">世谱</span><span class="ctx-mirror__txt">读书会成员 6 人 · 两位新人</span><span class="ctx-mirror__src">world_v4</span></div>
<div class="ctx-mirror__omitted">已省略 2 条:1 条过期(去年场地)、1 条已被取代(旧选篇)</div>
<div class="ctx-meter">
<div class="ctx-meter__label"><span>上下文预算</span><span class="mono">2.1k / 8k</span></div>
<div class="progress"><div class="progress__bar" style="width:26%"></div></div>
</div>
<p style="font-size:11px;color:var(--text-3);margin-top:8px">完整 ModelCall 与工具轨迹需双重授权诊断(15 分钟 · 单次)。</p>
</div>
</section>
<section class="ctx-section">
<div class="ctx-section__head">
<span class="cn">神识路由<span class="cn__plain">模型从哪来</span></span>
<a class="more" href="model-gateway.html">神识阁</a>
</div>
<div class="card" style="padding:12px 14px;font-size:12.5px;color:var(--text-2)">
<div style="display:flex;justify-content:space-between;padding:3px 0"><span>平台硬边界</span><span class="mono" style="color:var(--text-3)">✓</span></div>
<div style="display:flex;justify-content:space-between;padding:3px 0"><span>平台目录</span><span class="mono" style="color:var(--text-3)">✓</span></div>
<div style="display:flex;justify-content:space-between;padding:3px 0"><span>用户偏好</span><span class="mono" style="color:var(--text-3)">✓</span></div>
<div style="display:flex;justify-content:space-between;padding:3px 0"><span>会话临时选择</span><span style="color:var(--accent-strong)">生效中</span></div>
<div style="display:flex;justify-content:space-between;padding:3px 0;color:var(--text-3)"><span>角色覆写</span><span>—</span></div>
<p style="font-size:11px;color:var(--text-3);margin-top:8px">上层不可被下层覆盖;首个可见输出后不切换 Provider。</p>
</div>
</section>
</aside>
</div>
</main>
</div>
<script src="assets/app.js?v=3"></script>
</body>
</html>
+133
View File
@@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>器谱 · 设备与感知 — 魂镜 Hunjing</title>
<link rel="stylesheet" href="assets/app.css?v=10">
</head>
<body>
<svg style="display:none" aria-hidden="true"><symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol><symbol id="i-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></symbol><symbol id="i-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></symbol><symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol><symbol id="i-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></symbol><symbol id="i-layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></symbol><symbol id="i-kanban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3h4v18H5z"/><path d="M10 3h4v12h-4z"/><path d="M15 3h4v8h-4z"/></symbol><symbol id="i-package" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></symbol><symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></symbol><symbol id="i-route" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="19" r="3"/><circle cx="18" cy="5" r="3"/><path d="M12 19h4.5a3.5 3.5 0 0 0 0-7h-9a3.5 3.5 0 0 1 0-7H12"/></symbol><symbol id="i-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></symbol><symbol id="i-bag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></symbol><symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></symbol><symbol id="i-monitor" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></symbol><symbol id="i-sliders" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></symbol><symbol id="i-eye" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></symbol><symbol id="i-mic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/></symbol><symbol id="i-camera" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0Line truncated
</svg>
<div class="app">
<aside class="sidebar">
<div class="brand">
<div class="brand__mark" aria-hidden="true"></div>
<div class="brand__text">
<div class="brand__name">魂镜</div>
<div class="brand__sub">Hunjing</div>
</div>
</div>
<nav class="side-nav" aria-label="主导航"><div class="nav-group"><div class="nav-group__title">常用</div><a class="nav-item" href="home.html"><svg><use href="#i-home"/></svg><span class="cn">镜庭</span><span class="nav-plain">首页</span></a><a class="nav-item" href="conversation.html"><svg><use href="#i-branch"/></svg><span class="cn">镜树</span><span class="nav-plain">对话</span></a><a class="nav-item" href="characters.html"><svg><use href="#i-user"/></svg><span class="cn">镜生</span><span class="nav-plain">角色</span></a><a class="nav-item" href="memory.html"><svg><use href="#i-book"/></svg><span class="cn">息壤</span><span class="nav-plain">记忆</span></a><a class="nav-item" href="avatar.html"><svg><use href="#i-layers"/></svg><span class="cn">形仪</span><span class="nav-plain">形象</span></a></div><div class="nav-group"><div class="nav-group__title">创造</div><a class="nav-item" href="agent-workspace.html"><svg><use href="#i-kanban"/></svg><span class="cn">天工阁</span><span class="nav-plain">任务</span></a><a class="nav-item" href="plugins.html"><svg><use href="#i-package"/></svg><span class="cn">百工坊</span><span class="nav-plain">插件</span></a><a class="nav-item" href="collaboration.html"><svg><use href="#i-users"/></svg><span class="cn">同契台</span><span class="nav-plain">协作</span></a><a class="nav-item" href="model-gateway.html"><svg><use href="#i-route"/></svg><span class="cn">神识阁</span><span class="nav-plain">模型</span></a></div><div class="nav-group"><div class="nav-group__title">世界</div><a class="nav-item" href="community.html"><svg><use href="#i-globe"/></svg><span class="cn">雅集</span><span class="nav-plain">社区</span></a><a class="nav-item" href="marketplace.html"><svg><use href="#i-bag"/></svg><span class="cn">云市</span><span class="nav-plain">市场</span></a></div><div class="nav-group"><div class="nav-group__title">治理与账户</div><a class="nav-item" href="governance.html"><svg><use href="#i-shield"/></svg><span class="cn">司命台</span><span class="nav-plain">治理</span></a><a class="nav-item is-active" aria-current="page" href="devices.html"><svg><use href="#i-monitor"/></svg><span class="cn">器谱</span><span class="nav-plain">设备</span></a><a class="nav-item" href="settings.html"><svg><use href="#i-sliders"/></svg><span class="cn">设置</span><span class="nav-plain">账户</span></a></div></nav>
<div class="side-foot">
<button class="icon-btn side-fold-btn" data-sidebar-fold aria-label="收起侧栏" title="收起 / 展开侧栏(记忆偏好)"><svg><use href="#i-chevrons"/></svg></button>
<span class="avatar avatar--sm avatar--user">舟</span>
<span style="font-size:12.5px">云舟</span>
<button class="icon-btn" data-theme-toggle style="margin-left:auto" aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</div>
</aside>
<main class="main">
<header class="topbar">
<div class="crumbs"><span class="cn">器谱</span><span class="crumbs__sep">·</span><span class="here">设备与感知</span></div>
<div class="topbar__spacer"></div>
<span class="prototype-tag">静态原型 · 待审查</span>
<button class="icon-btn" data-theme-toggle aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</header>
<div class="content">
<div class="realm-banner" role="note">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
设备能力同时受系统权限与授契约束;采集有持续指示,可即时暂停或撤销。离线缓存加密,不创建独立角色副本。
</div>
<section class="section">
<div class="section__head"><span class="cn">形器<span class="cn__plain">登记设备</span></span></div>
<div class="card" style="padding:6px 18px">
<div class="list-row">
<svg style="width:18px;height:18px;color:var(--text-3)"><use href="#i-monitor"/></svg>
<div class="list-row__body"><div class="list-row__title">MacBook Pro · Tauri 桌面</div><div class="list-row__sub">macOS · 当前设备</div></div>
<span class="badge badge--ok" style="font-size:11px"><span class="badge__dot"></span>在线</span>
</div>
<div class="list-row">
<svg style="width:18px;height:18px;color:var(--text-3)"><use href="#i-monitor"/></svg>
<div class="list-row__body"><div class="list-row__title">iPhone · 移动端</div><div class="list-row__sub">iOS · 实时语音常用设备</div></div>
<span class="badge badge--ok" style="font-size:11px"><span class="badge__dot"></span>在线</span>
</div>
<div class="list-row">
<svg style="width:18px;height:18px;color:var(--text-3)"><use href="#i-globe"/></svg>
<div class="list-row__body"><div class="list-row__title">Web 浏览器 · Safari</div><div class="list-row__sub">昨天活跃 · 仅文件读写授权</div></div>
<span class="badge badge--muted" style="font-size:11px"><span class="badge__dot"></span>离线</span>
</div>
</div>
</section>
<section class="section">
<div class="section__head"><span class="cn">授契<span class="cn__plain">设备能力授权</span></span><button class="btn btn--ghost btn--sm" style="margin-left:auto">全部撤销</button></div>
<div class="card" style="padding:6px 18px">
<div class="grant-row">
<span class="grant-row__cap"><svg><use href="#i-mic"/></svg>闻声 · 麦克风</span>
<div><div style="font-size:13px">青梧 · 实时语音</div><div class="grant-row__meta">iPhone · 仅通话期间 · 采集指示常显</div></div>
<button class="btn btn--ghost btn--sm">撤销</button>
</div>
<div class="grant-row">
<span class="grant-row__cap"><svg><use href="#i-file"/></svg>文件读写</span>
<div><div style="font-size:13px">青梧 · 书斋归档</div><div class="grant-row__meta">MacBook · 仅 ~/BookRoom/ 目录 · 长期(可随时撤销)</div></div>
<button class="btn btn--ghost btn--sm">撤销</button>
</div>
<div class="grant-row">
<span class="grant-row__cap"><svg><use href="#i-bell"/></svg>通知</span>
<div><div style="font-size:13px">叩心提醒通道</div><div class="grant-row__meta">iPhone · 静默时段 22:00–08:00 · 频率 ≤ 1 条/日</div></div>
<button class="btn btn--ghost btn--sm">撤销</button>
</div>
<div class="grant-row" style="opacity:.6">
<span class="grant-row__cap"><svg><use href="#i-camera"/></svg>观影 · 摄像头</span>
<div><div style="font-size:13px">未授权</div><div class="grant-row__meta">任何角色请求摄像头时将单独请命</div></div>
<span style="font-size:11.5px;color:var(--text-3)">—</span>
</div>
<div class="grant-row" style="opacity:.6">
<span class="grant-row__cap"><svg><use href="#i-pin"/></svg>问路 · 位置</span>
<div><div style="font-size:13px">未授权(上次通感已结束)</div><div class="grant-row__meta">通感默认 10 分钟 · 精度 1km · 结束即失效</div></div>
<span style="font-size:11.5px;color:var(--text-3)">—</span>
</div>
</div>
</section>
<div class="two-col">
<section class="section card" style="border-color:color-mix(in srgb,var(--accent) 40%,transparent)">
<div class="card__head"><span class="cn" style="color:var(--accent-strong)">通感进行中<span class="cn__plain">限时设备感知</span></span><span style="margin-left:auto;font-size:12px;color:var(--text-3)" class="mono">剩 8 分钟</span></div>
<p style="font-size:13px;color:var(--text-2);margin-bottom:10px">青梧 · 问路(位置 · 精度 1km)· 用于「找一家安静的茶馆」。采集指示常显,位置不写入长期记忆,会话结束即丢弃。</p>
<div class="progress" style="margin-bottom:12px"><div class="progress__bar" style="width:20%"></div></div>
<div style="display:flex;gap:10px">
<button class="btn btn--outline btn--sm">暂停</button>
<button class="btn btn--danger btn--sm">立即结束并撤销</button>
</div>
</section>
<section class="section">
<div class="block approval">
<div class="approval__head">
<span class="approval__seal" aria-hidden="true">命</span>
<div>
<div class="approval__title"><span class="cn">请命 · 观屏</span><span class="cn__plain">屏幕读取授权</span></div>
<div style="font-size:11.5px;color:var(--text-3)">来自镜树「书房整理」· 限此一次</div>
</div>
</div>
<div class="approval__body">
<p style="font-size:14px;color:var(--text-1)">允许青梧在未来 10 分钟读取当前屏幕?</p>
<div class="fact"><span class="fact__k">用途</span><span class="fact__v">识别书桌照片中的书目并归档</span></div>
<div class="fact"><span class="fact__k">数据</span><span class="fact__v">仅屏幕内容 · 不截屏存储 · 不写入长期记忆</span></div>
</div>
<div class="approval__acts">
<button class="btn btn--cinnabar" aria-label="确认决定:允许本次观屏">允许(本次,10 分钟)</button>
<button class="btn btn--outline">拒绝</button>
</div>
</div>
</section>
</div>
</div>
</main>
</div>
<script src="assets/app.js?v=3"></script>
</body>
</html>
+142
View File
@@ -0,0 +1,142 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>司命台 · 安全与治理 — 魂镜 Hunjing</title>
<link rel="stylesheet" href="assets/app.css?v=10">
</head>
<body>
<svg style="display:none" aria-hidden="true"><symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol><symbol id="i-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></symbol><symbol id="i-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></symbol><symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol><symbol id="i-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></symbol><symbol id="i-layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></symbol><symbol id="i-kanban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3h4v18H5z"/><path d="M10 3h4v12h-4z"/><path d="M15 3h4v8h-4z"/></symbol><symbol id="i-package" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></symbol><symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></symbol><symbol id="i-route" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="19" r="3"/><circle cx="18" cy="5" r="3"/><path d="M12 19h4.5a3.5 3.5 0 0 0 0-7h-9a3.5 3.5 0 0 1 0-7H12"/></symbol><symbol id="i-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></symbol><symbol id="i-bag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></symbol><symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></symbol><symbol id="i-monitor" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></symbol><symbol id="i-sliders" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></symbol><symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></symbol><symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></symbol><symbol id="i-key" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></symbol><symbol id="i-check" vLine truncated
</svg>
<div class="app">
<aside class="sidebar">
<div class="brand">
<div class="brand__mark" aria-hidden="true"></div>
<div class="brand__text">
<div class="brand__name">魂镜</div>
<div class="brand__sub">Hunjing</div>
</div>
</div>
<nav class="side-nav" aria-label="主导航"><div class="nav-group"><div class="nav-group__title">常用</div><a class="nav-item" href="home.html"><svg><use href="#i-home"/></svg><span class="cn">镜庭</span><span class="nav-plain">首页</span></a><a class="nav-item" href="conversation.html"><svg><use href="#i-branch"/></svg><span class="cn">镜树</span><span class="nav-plain">对话</span></a><a class="nav-item" href="characters.html"><svg><use href="#i-user"/></svg><span class="cn">镜生</span><span class="nav-plain">角色</span></a><a class="nav-item" href="memory.html"><svg><use href="#i-book"/></svg><span class="cn">息壤</span><span class="nav-plain">记忆</span></a><a class="nav-item" href="avatar.html"><svg><use href="#i-layers"/></svg><span class="cn">形仪</span><span class="nav-plain">形象</span></a></div><div class="nav-group"><div class="nav-group__title">创造</div><a class="nav-item" href="agent-workspace.html"><svg><use href="#i-kanban"/></svg><span class="cn">天工阁</span><span class="nav-plain">任务</span></a><a class="nav-item" href="plugins.html"><svg><use href="#i-package"/></svg><span class="cn">百工坊</span><span class="nav-plain">插件</span></a><a class="nav-item" href="collaboration.html"><svg><use href="#i-users"/></svg><span class="cn">同契台</span><span class="nav-plain">协作</span></a><a class="nav-item" href="model-gateway.html"><svg><use href="#i-route"/></svg><span class="cn">神识阁</span><span class="nav-plain">模型</span></a></div><div class="nav-group"><div class="nav-group__title">世界</div><a class="nav-item" href="community.html"><svg><use href="#i-globe"/></svg><span class="cn">雅集</span><span class="nav-plain">社区</span></a><a class="nav-item" href="marketplace.html"><svg><use href="#i-bag"/></svg><span class="cn">云市</span><span class="nav-plain">市场</span></a></div><div class="nav-group"><div class="nav-group__title">治理与账户</div><a class="nav-item is-active" aria-current="page" href="governance.html"><svg><use href="#i-shield"/></svg><span class="cn">司命台</span><span class="nav-plain">治理</span></a><a class="nav-item" href="devices.html"><svg><use href="#i-monitor"/></svg><span class="cn">器谱</span><span class="nav-plain">设备</span></a><a class="nav-item" href="settings.html"><svg><use href="#i-sliders"/></svg><span class="cn">设置</span><span class="nav-plain">账户</span></a></div></nav>
<div class="side-foot">
<button class="icon-btn side-fold-btn" data-sidebar-fold aria-label="收起侧栏" title="收起 / 展开侧栏(记忆偏好)"><svg><use href="#i-chevrons"/></svg></button>
<span class="avatar avatar--sm avatar--user">舟</span>
<span style="font-size:12.5px">云舟</span>
<button class="icon-btn" data-theme-toggle style="margin-left:auto" aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</div>
</aside>
<main class="main">
<header class="topbar">
<div class="crumbs"><span class="cn">司命台</span><span class="crumbs__sep">·</span><span class="here">安全 · 权限 · 审计 · 治理</span></div>
<div class="topbar__spacer"></div>
<span class="prototype-tag">静态原型 · 待审查</span>
<button class="icon-btn" data-theme-toggle aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</header>
<div class="content">
<div class="realm-banner" role="note">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
治理不支配角色或用户数据;隐藏推理、密钥与凭据永不进入诊断与审计展示。错误与安全事件优先直白表述。
</div>
<section class="section card">
<div class="card__head"><span class="cn">阴阳平衡<span class="cn__plain">朱批治理的三态</span></span><span style="margin-left:auto;font-size:11.5px;color:var(--text-3)">阳亢 · 放行偏多 / 动态平衡 / 阴盛 · 驳回偏多</span></div>
<div class="taiji-gauge">
<svg viewBox="0 0 128 76" role="img" aria-label="阴阳平衡仪表:通过率 78%,动态平衡,鎏金指针居中偏右">
<path d="M 12 68 A 52 52 0 0 1 116 68" fill="none" stroke="var(--line)" stroke-width="7" stroke-linecap="round"/>
<path d="M 12 68 A 52 52 0 0 1 38 25" fill="none" stroke="color-mix(in srgb, var(--cinnabar) 45%, transparent)" stroke-width="7" stroke-linecap="round"/>
<path d="M 90 25 A 52 52 0 0 1 116 68" fill="none" stroke="color-mix(in srgb, var(--accent) 50%, transparent)" stroke-width="7" stroke-linecap="round"/>
<line x1="64" y1="68" x2="90" y2="25" stroke="var(--gilt)" stroke-width="2" stroke-linecap="round"/>
<circle cx="64" cy="68" r="3" fill="var(--gilt)"/>
<text class="taiji-gauge__label" x="12" y="76">阴 · 驳回</text>
<text class="taiji-gauge__label" x="116" y="76" text-anchor="end">阳 · 放行</text>
</svg>
<div>
<div class="taiji-gauge__val" style="color:var(--accent-strong)">动态平衡</div>
<div style="font-size:12px;color:var(--text-2);margin-top:3px">近 30 日朱批通过率 78% · 驳回 22%<br><span style="color:var(--text-3)">指针偏右但在平衡带内;阳亢时提示收紧行契,阴盛时提示检查策略误伤</span></div>
</div>
</div>
</section>
<div class="tabs" role="tablist" aria-label="治理队列">
<button class="tab is-on" role="tab" aria-selected="true">内容 <span class="cnt">2</span></button>
<button class="tab" role="tab" aria-selected="false">插件 <span class="cnt">1</span></button>
<button class="tab" role="tab" aria-selected="false">市场 <span class="cnt">1</span></button>
<button class="tab" role="tab" aria-selected="false">申诉 <span class="cnt">1</span></button>
</div>
<section class="section">
<div class="section__head"><span class="cn">司议<span class="cn__plain">治理案件</span></span></div>
<div class="card case-card" style="margin-bottom:14px;padding-left:20px">
<div class="card__head">
<span class="badge badge--warn"><span class="badge__dot"></span>待审 · 等待审核 <span class="mono">pending_review</span></span>
<span style="font-size:11.5px;color:var(--text-3);margin-left:auto">09-15 11:20 · 鸣冤 ×3</span>
</div>
<p style="font-size:13.5px">雅集帖子「未署名的守砚人立像二创」被举报:疑似未保留来源署名(CC BY-NC 要求)。</p>
<div style="display:flex;gap:8px;margin-top:12px">
<button class="btn btn--outline btn--sm">查看原文</button>
<button class="btn btn--outline btn--sm">限观 · 限制展示</button>
<button class="btn btn--danger btn--sm">移除</button>
<button class="btn btn--ghost btn--sm">驳回举报</button>
</div>
</div>
<div class="card case-card" style="padding-left:20px">
<div class="card__head">
<span class="badge badge--warn"><span class="badge__dot"></span>待审 <span class="mono">pending_review</span></span>
<span style="font-size:11.5px;color:var(--text-3);margin-left:auto">09-14 09:02 · 自动扫描</span>
</div>
<p style="font-size:13.5px">市场商品「曲谱排版插件 v1.4」扫描发现:Manifest 声明与实际网络请求域不一致。</p>
<div style="display:flex;gap:8px;margin-top:12px">
<button class="btn btn--outline btn--sm">查看扫描报告</button>
<button class="btn btn--danger btn--sm">下架</button>
<button class="btn btn--ghost btn--sm">通知创作者</button>
</div>
</div>
</section>
<div class="two-col">
<section class="section card">
<div class="card__head"><span class="cn">诊断授权<span class="cn__plain">双重授权</span></span></div>
<p style="font-size:12.5px;color:var(--text-2);margin-bottom:12px">读取 Run 详细诊断(ModelCall、工具参数、ContextSnapshot)需要在普通认证之外叠加短期诊断令牌:绑定单个 Run、单次使用、默认 15 分钟。</p>
<div class="set-row">
<div class="set-row__body"><div class="set-row__t">申请诊断令牌</div><div class="set-row__d">目的 user_inspection · 绑定 run_9f31c2(天工阁 · 书单任务)</div></div>
<button class="btn btn--cinnabar btn--sm">签发</button>
</div>
<div class="set-row">
<div class="set-row__body"><div class="set-row__t mono">hjd_8f…(已过期)</div><div class="set-row__d">09-15 12:04 签发 · 12:19 过期 · 已用 1 次 · 范围:青梧 · 选篇答复</div></div>
<span class="badge badge--muted" style="font-size:11px"><span class="badge__dot"></span>已过期</span>
</div>
<p style="font-size:11.5px;color:var(--text-3);margin-top:10px">隐藏推理、密钥、Authorization 与签名 URL 永不返回;管理员与诊断均不可读私箧。</p>
</section>
<section class="section card">
<div class="card__head"><span class="cn">安全概览</span><a class="more" href="settings.html">设置 →</a></div>
<div class="set-row"><div class="set-row__body"><div class="set-row__t">活跃会话 2</div><div class="set-row__d">MacBook(当前)· iPhone</div></div><a class="btn btn--ghost btn--sm" href="settings.html">管理</a></div>
<div class="set-row"><div class="set-row__body"><div class="set-row__t">Service Account key 1</div><div class="set-row__d">scope:只读统计 · 90 天轮换</div></div><button class="btn btn--ghost btn--sm">轮换</button></div>
<div class="set-row"><div class="set-row__t">凭据重用检测</div><span class="badge badge--ok" style="font-size:11px"><span class="badge__dot"></span>30 天 0 次</span></div>
</section>
</div>
<section class="section">
<div class="section__head"><span class="cn">审计<span class="cn__plain">AuditEvent · 权威事务同批提交</span></span></div>
<div class="card">
<div class="table-wrap"><table class="data">
<thead><tr><th>时间</th><th>主体</th><th>动作</th><th>对象</th><th>结果</th></tr></thead>
<tbody>
<tr><td class="mono">09-15 19:48</td><td>云舟</td><td>朱批 · 允许</td><td class="mono">calendar.write · run_7ab1</td><td class="mark-ok">✓</td></tr>
<tr><td class="mono">09-15 19:52</td><td>云舟</td><td>创建天工</td><td class="mono">run_9f31c2</td><td class="mark-ok">✓</td></tr>
<tr><td class="mono">09-15 12:04</td><td>云舟</td><td>签发诊断令牌</td><td class="mono">hjd_8f…</td><td class="mark-ok">✓</td></tr>
<tr><td class="mono">09-14 22:31</td><td>未知请求</td><td>跨镜域读取</td><td class="mono">memory · 雪袖</td><td class="mark-no" style="color:var(--err)">✗ 拒绝(失败关闭)</td></tr>
<tr><td class="mono">09-12 10:05</td><td>云舟</td><td>BYOK 轮换</td><td class="mono">byok_7c02…</td><td class="mark-ok">✓</td></tr>
</tbody>
</table></div>
</div>
</section>
</div>
</main>
</div>
<script src="assets/app.js?v=3"></script>
</body>
</html>
+277
View File
@@ -0,0 +1,277 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>镜庭 · 个人首页 — 魂镜 Hunjing</title>
<link rel="stylesheet" href="assets/app.css?v=10">
</head>
<body>
<svg style="display:none" aria-hidden="true">
<symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol>
<symbol id="i-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></symbol>
<symbol id="i-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></symbol>
<symbol id="i-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></symbol>
<symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol>
<symbol id="i-layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></symbol>
<symbol id="i-kanban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3h4v18H5z"/><path d="M10 3h4v12h-4z"/><path d="M15 3h4v8h-4z"/></symbol>
<symbol id="i-package" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></symbol>
<symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></symbol>
<symbol id="i-bag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></symbol>
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></symbol>
<symbol id="i-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></symbol>
<symbol id="i-route" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="19" r="3"/><circle cx="18" cy="5" r="3"/><path d="M12 19h4.5a3.5 3.5 0 0 0 0-7h-9a3.5 3.5 0 0 1 0-7H12"/></symbol>
<symbol id="i-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></symbol>
<symbol id="i-monitor" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></symbol>
<symbol id="i-sliders" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></symbol>
<symbol id="i-chevrons" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="11 17 6 12 11 7"/><polyline points="18 17 13 12 18 7"/></symbol>
</svg>
<div class="app">
<aside class="sidebar">
<div class="brand">
<div class="brand__mark" aria-hidden="true"></div>
<div class="brand__text">
<div class="brand__name">魂镜</div>
<div class="brand__sub">Hunjing</div>
</div>
</div>
<nav class="side-nav" aria-label="主导航"><div class="nav-group"><div class="nav-group__title">常用</div><a class="nav-item is-active" aria-current="page" href="home.html"><svg><use href="#i-home"/></svg><span class="cn">镜庭</span><span class="nav-plain">首页</span></a><a class="nav-item" href="conversation.html"><svg><use href="#i-branch"/></svg><span class="cn">镜树</span><span class="nav-plain">对话</span></a><a class="nav-item" href="characters.html"><svg><use href="#i-user"/></svg><span class="cn">镜生</span><span class="nav-plain">角色</span></a><a class="nav-item" href="memory.html"><svg><use href="#i-book"/></svg><span class="cn">息壤</span><span class="nav-plain">记忆</span></a><a class="nav-item" href="avatar.html"><svg><use href="#i-layers"/></svg><span class="cn">形仪</span><span class="nav-plain">形象</span></a></div><div class="nav-group"><div class="nav-group__title">创造</div><a class="nav-item" href="agent-workspace.html"><svg><use href="#i-kanban"/></svg><span class="cn">天工阁</span><span class="nav-plain">任务</span></a><a class="nav-item" href="plugins.html"><svg><use href="#i-package"/></svg><span class="cn">百工坊</span><span class="nav-plain">插件</span></a><a class="nav-item" href="collaboration.html"><svg><use href="#i-users"/></svg><span class="cn">同契台</span><span class="nav-plain">协作</span></a><a class="nav-item" href="model-gateway.html"><svg><use href="#i-route"/></svg><span class="cn">神识阁</span><span class="nav-plain">模型</span></a></div><div class="nav-group"><div class="nav-group__title">世界</div><a class="nav-item" href="community.html"><svg><use href="#i-globe"/></svg><span class="cn">雅集</span><span class="nav-plain">社区</span></a><a class="nav-item" href="marketplace.html"><svg><use href="#i-bag"/></svg><span class="cn">云市</span><span class="nav-plain">市场</span></a></div><div class="nav-group"><div class="nav-group__title">治理与账户</div><a class="nav-item" href="governance.html"><svg><use href="#i-shield"/></svg><span class="cn">司命台</span><span class="nav-plain">治理</span></a><a class="nav-item" href="devices.html"><svg><use href="#i-monitor"/></svg><span class="cn">器谱</span><span class="nav-plain">设备</span></a><a class="nav-item" href="settings.html"><svg><use href="#i-sliders"/></svg><span class="cn">设置</span><span class="nav-plain">账户</span></a></div></nav>
<div class="side-foot">
<button class="icon-btn side-fold-btn" data-sidebar-fold aria-label="收起侧栏" title="收起 / 展开侧栏(记忆偏好)"><svg><use href="#i-chevrons"/></svg></button>
<span class="avatar avatar--sm avatar--user">舟</span>
<span style="font-size:12.5px">云舟</span>
<button class="icon-btn" data-theme-toggle style="margin-left:auto" aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</div>
</aside>
<main class="main">
<header class="topbar">
<div class="crumbs"><span class="cn">镜庭</span><span class="crumbs__sep">·</span><span class="here">个人首页</span></div>
<div class="topbar__spacer"></div>
<span class="prototype-tag">静态原型 · 待审查</span>
<button class="icon-btn" aria-label="搜索"><svg><use href="#i-search"/></svg></button>
<button class="icon-btn" aria-label="通知"><svg><use href="#i-bell"/></svg></button>
</header>
<div class="content">
<div class="hero">
<div class="sundial" role="img" aria-label="日晷:巳时,白露节气,与青梧相伴第 918 日">
<svg viewBox="0 0 116 116">
<circle cx="58" cy="58" r="55" fill="none" stroke="var(--gilt-faint)" stroke-width="1"/>
<circle cx="58" cy="58" r="47" fill="none" stroke="var(--line-soft)" stroke-width="0.8"/>
<g class="sundial-spin">
<circle cx="58" cy="58" r="51" fill="none" stroke="var(--gilt-faint)" stroke-width="0.6" stroke-dasharray="2 7"/>
</g>
<!-- 十二时辰刻度:巳位(东南)鎏金加粗 -->
<g stroke="var(--line)" stroke-width="1.4">
<line x1="58" y1="6" x2="58" y2="14"/><line x1="87" y1="14" x2="83" y2="21"/>
<line x1="102" y1="29" x2="95" y2="34"/><line x1="110" y1="58" x2="102" y2="58"/>
<line x1="102" y1="87" x2="95" y2="82"/><line x1="87" y1="102" x2="83" y2="95"/>
<line x1="58" y1="110" x2="58" y2="102"/><line x1="29" y1="102" x2="33" y2="95"/>
<line x1="14" y1="87" x2="21" y2="82"/><line x1="6" y1="58" x2="14" y2="58"/>
<line x1="14" y1="29" x2="21" y2="34"/><line x1="29" y1="14" x2="33" y2="21"/>
</g>
<!-- 巳时弧(9-11 时,约 135°→165° 区间示意) -->
<path d="M 93.2 25.4 A 52 52 0 0 1 104.5 44.6" fill="none" stroke="var(--gilt)" stroke-width="2.4" stroke-linecap="round"/>
<!-- 晷针 -->
<line x1="58" y1="58" x2="93" y2="27" stroke="var(--accent)" stroke-width="1.6" stroke-linecap="round"/>
<circle cx="58" cy="58" r="2.4" fill="var(--accent)"/>
</svg>
<div class="sundial__center">
<span class="sundial__hour">巳时</span>
<span class="sundial__sub">白露 · 第 10 日</span>
</div>
</div>
<div>
<h1>上午好,云舟</h1>
<p style="color:var(--text-2);font-size:13px;margin-top:4px">周三 · 2026-09-16 · 与青梧相伴第 918 日 · 镜庭只聚合状态,各域才是数据事实源</p>
</div>
<div class="hero__stats">
<a class="hero__stat" href="conversation.html"><b>2</b><span>镜树 · 进行中对话</span></a>
<a class="hero__stat" href="conversation.html"><b>1</b><span>请命 · 待你确认</span></a>
<a class="hero__stat" href="memory.html"><b>3</b><span>息壤候选 · 待审</span></a>
<a class="hero__stat" href="agent-workspace.html"><b>2</b><span>天工 · 运行中</span></a>
</div>
</div>
<section class="section">
<div class="section__head">
<span class="cn">镜生<span class="cn__plain">你的角色</span></span>
<a class="more" href="characters.html">全部 4 位 →</a>
</div>
<div class="grid-cards">
<a class="card char-card" href="conversation.html">
<div class="char-card__top">
<span class="avatar">梧</span>
<div>
<div class="char-card__name">青梧</div>
<div style="font-size:12px;color:var(--text-3)">守砚人 · 道体 v3</div>
</div>
</div>
<p class="char-card__line">「书斋已整理,今晚共读的选篇单放在桌上了。」</p>
<div class="char-card__foot">
<span class="badge badge--info"><span class="badge__dot"></span>临照 · 前台运行</span>
<span class="char-card__time">10 分钟前</span>
</div>
</a>
<a class="card char-card" href="#">
<div class="char-card__top">
<span class="avatar">袖</span>
<div>
<div class="char-card__name">雪袖</div>
<div style="font-size:12px;color:var(--text-3)">茶室主人 · 道体 v1</div>
</div>
</div>
<p class="char-card__line">「新到的白毫银针,等你来尝。」</p>
<div class="char-card__foot">
<span class="badge badge--muted"><span class="badge__dot"></span>守一 · 安静待命</span>
<span class="char-card__time">昨天</span>
</div>
</a>
<a class="card char-card" href="#">
<div class="char-card__top">
<span class="avatar">澜</span>
<div>
<div class="char-card__name">观澜</div>
<div style="font-size:12px;color:var(--text-3)">航海士 · 道体 v2 · 静修中</div>
</div>
</div>
<p class="char-card__line">静修 · 停止运行,身份存续;随时可以唤醒。</p>
<div class="char-card__foot">
<span class="badge badge--muted"><span class="badge__dot"></span>静修 · 停止运行</span>
<span class="char-card__time">9 月 2 日</span>
</div>
</a>
<a class="card char-card" href="#">
<div class="char-card__top">
<span class="avatar" style="filter:grayscale(.7);opacity:.75">岁</span>
<div>
<div class="char-card__name">旧岁</div>
<div style="font-size:12px;color:var(--text-3)">守岁人 · 归藏 · 可复照</div>
</div>
</div>
<p class="char-card__line">归藏 · 已从日常入口隐藏;复照可重开,不抹历史。</p>
<div class="char-card__foot">
<span class="badge badge--muted"><span class="badge__dot"></span>归藏 · 已隐藏</span>
<span class="char-card__time">7 月 30 日</span>
</div>
</a>
</div>
</section>
<div class="home-cols">
<section class="section" style="margin:0">
<div class="section__head">
<span class="cn">镜树<span class="cn__plain">最近对话</span></span>
<a class="more" href="conversation.html">全部 →</a>
</div>
<div class="card" style="padding:6px 18px">
<a class="list-row" href="conversation.html">
<span class="avatar avatar--sm">梧</span>
<div class="list-row__body">
<div class="list-row__title">《陶庵梦忆》共读选篇 <span class="realm-dot">真界</span></div>
<div class="list-row__sub">青梧:六篇已定,结构图也画好了,请过目。</div>
</div>
<span class="list-row__time">10 分钟前</span>
</a>
<div class="list-row">
<span class="avatar avatar--sm">袖</span>
<div class="list-row__body">
<div class="list-row__title">秋茶清单 <span class="realm-dot realm-dot--world">境界</span></div>
<div class="list-row__sub">雪袖:按你上次的口味,先把白毫银针排第一。</div>
</div>
<span class="list-row__time">昨天</span>
</div>
<div class="list-row">
<span class="avatar avatar--sm">梧</span>
<div class="list-row__body">
<div class="list-row__title">书房整理方案(幻境试演) <span class="realm-dot realm-dot--sim">幻境</span></div>
<div class="list-row__sub">青梧:这只是试演方案,不影响现实与正史。</div>
</div>
<span class="list-row__time">9 月 12 日</span>
</div>
<div class="list-row">
<span class="avatar avatar--sm">澜</span>
<div class="list-row__body">
<div class="list-row__title">远洋航线推演</div>
<div class="list-row__sub">观澜:航线已归档,静修前等你验收。</div>
</div>
<span class="list-row__time">9 月 2 日</span>
</div>
</div>
</section>
<section class="section" style="margin:0">
<div class="section__head">
<span class="cn">天工<span class="cn__plain">Agent 任务</span></span>
<a class="more" href="agent-workspace.html">天工阁 →</a>
</div>
<div class="card" style="padding:14px 18px 6px;margin-bottom:14px">
<div class="list-row" style="border-bottom:none;padding-top:4px">
<div class="list-row__body">
<div class="list-row__title">整理性灵 newsletter 书单</div>
<div class="list-row__sub">青梧 · 分工使 2 · token 12.4k / 20k</div>
<div class="progress" style="margin-top:7px"><div class="progress__bar" style="width:62%"></div></div>
</div>
<span class="badge badge--info"><span class="badge__dot"></span>运转 · 运行中 <span class="mono">running</span></span>
</div>
</div>
<div class="card" style="padding:6px 18px">
<div class="list-row">
<div class="list-row__body">
<div class="list-row__title">周报草稿 · 第 37 周</div>
<div class="list-row__sub">任务 Agent · 无角色身份</div>
</div>
<span class="badge badge--muted"><span class="badge__dot"></span>待启 · 排队中 <span class="mono">queued</span></span>
</div>
<div class="list-row">
<div class="list-row__body">
<div class="list-row__title">书斋照片归档命名</div>
<div class="list-row__sub">青梧 · 已成 · 耗时 4 分 12 秒</div>
</div>
<span class="badge badge--ok"><span class="badge__dot"></span>已成 · 已完成 <span class="mono">completed</span></span>
</div>
</div>
</section>
</div>
<section class="section" style="margin-top:26px">
<div class="section__head">
<span class="cn">待你决定<span class="cn__plain">审批与候选</span></span>
</div>
<div class="card" style="padding:6px 18px">
<div class="list-row" href="#">
<span class="approval__seal" aria-hidden="true" style="flex:none">命</span>
<div class="list-row__body">
<div class="list-row__title">请命 · 写入日历(6 个事件)</div>
<div class="list-row__sub">青梧请求在日历创建 6 个「共读讨论」事件 · 数据范围:仅日历 · 费用:无</div>
</div>
<a class="btn btn--cinnabar btn--sm" href="conversation.html">去朱批</a>
</div>
<div class="list-row">
<span class="badge" style="flex:none;border-color:color-mix(in srgb,var(--warn) 40%,transparent);color:var(--warn)">息壤候选 ×3</span>
<div class="list-row__body">
<div class="list-row__title">「读书会在隔周四晚」「偏爱短篇」「花粉过敏(重忆)」</div>
<div class="list-row__sub">来自青梧的对话候选 · 归真后才会影响未来上下文</div>
</div>
<a class="btn btn--outline btn--sm" href="memory.html">去审阅</a>
</div>
<div class="list-row">
<span class="badge" style="flex:none">叩心候选</span>
<div class="list-row__body">
<div class="list-row__title">青梧想在明晚共读前发一条提醒</div>
<div class="list-row__sub">叩心当前静默 · 不回应不会降低关系 · 可设频率上限</div>
</div>
<a class="btn btn--ghost btn--sm" href="settings.html">了解叩心</a>
</div>
</div>
</section>
</div>
</main>
</div>
<script src="assets/app.js?v=3"></script>
</body>
</html>
+243
View File
@@ -0,0 +1,243 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>魂镜 Hunjing · 界面原型 · 设计说明</title>
<link rel="stylesheet" href="assets/app.css?v=10">
</head>
<body>
<svg style="display:none" aria-hidden="true">
<symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol>
<symbol id="i-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></symbol>
<symbol id="i-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></symbol>
<symbol id="i-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></symbol>
<symbol id="i-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></symbol>
<symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol>
<symbol id="i-layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></symbol>
<symbol id="i-kanban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3h4v18H5z"/><path d="M10 3h4v12h-4z"/><path d="M15 3h4v8h-4z"/></symbol>
<symbol id="i-package" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></symbol>
<symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></symbol>
<symbol id="i-bag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></symbol>
<symbol id="i-sliders" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></symbol>
<symbol id="i-monitor" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></symbol>
<symbol id="i-route" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="19" r="3"/><circle cx="18" cy="5" r="3"/><path d="M12 19h4.5a3.5 3.5 0 0 0 0-7h-9a3.5 3.5 0 0 1 0-7H12"/></symbol>
<symbol id="i-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></symbol>
<symbol id="i-chevrons" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="11 17 6 12 11 7"/><polyline points="18 17 13 12 18 7"/></symbol>
</svg>
<div class="app">
<aside class="sidebar">
<div class="brand">
<div class="brand__mark" aria-hidden="true"></div>
<div class="brand__text">
<div class="brand__name">魂镜</div>
<div class="brand__sub">Hunjing</div>
</div>
</div>
<nav class="side-nav" aria-label="主导航">
<div class="nav-group">
<div class="nav-group__title">原型</div>
<a class="nav-item is-active" href="index.html"><svg><use href="#i-home"/></svg><span class="cn">设计说明</span><span class="nav-plain">索引</span></a>
</div>
<div class="nav-group"><div class="nav-group__title">常用</div><a class="nav-item" href="home.html"><svg><use href="#i-home"/></svg><span class="cn">镜庭</span><span class="nav-plain">首页</span></a><a class="nav-item" href="conversation.html"><svg><use href="#i-branch"/></svg><span class="cn">镜树</span><span class="nav-plain">对话</span></a><a class="nav-item" href="characters.html"><svg><use href="#i-user"/></svg><span class="cn">镜生</span><span class="nav-plain">角色</span></a><a class="nav-item" href="memory.html"><svg><use href="#i-book"/></svg><span class="cn">息壤</span><span class="nav-plain">记忆</span></a><a class="nav-item" href="avatar.html"><svg><use href="#i-layers"/></svg><span class="cn">形仪</span><span class="nav-plain">形象</span></a></div>
<div class="nav-group"><div class="nav-group__title">创造</div><a class="nav-item" href="agent-workspace.html"><svg><use href="#i-kanban"/></svg><span class="cn">天工阁</span><span class="nav-plain">任务</span></a><a class="nav-item" href="plugins.html"><svg><use href="#i-package"/></svg><span class="cn">百工坊</span><span class="nav-plain">插件</span></a><a class="nav-item" href="collaboration.html"><svg><use href="#i-users"/></svg><span class="cn">同契台</span><span class="nav-plain">协作</span></a><a class="nav-item" href="model-gateway.html"><svg><use href="#i-route"/></svg><span class="cn">神识阁</span><span class="nav-plain">模型</span></a></div>
<div class="nav-group"><div class="nav-group__title">世界</div><a class="nav-item" href="community.html"><svg><use href="#i-globe"/></svg><span class="cn">雅集</span><span class="nav-plain">社区</span></a><a class="nav-item" href="marketplace.html"><svg><use href="#i-bag"/></svg><span class="cn">云市</span><span class="nav-plain">市场</span></a></div>
<div class="nav-group"><div class="nav-group__title">治理与账户</div><a class="nav-item" href="governance.html"><svg><use href="#i-shield"/></svg><span class="cn">司命台</span><span class="nav-plain">治理</span></a><a class="nav-item" href="devices.html"><svg><use href="#i-monitor"/></svg><span class="cn">器谱</span><span class="nav-plain">设备</span></a><a class="nav-item" href="settings.html"><svg><use href="#i-sliders"/></svg><span class="cn">设置</span><span class="nav-plain">账户</span></a></div>
</nav>
<div class="side-foot">
<button class="icon-btn side-fold-btn" data-sidebar-fold aria-label="收起侧栏" title="收起 / 展开侧栏(记忆偏好)"><svg><use href="#i-chevrons"/></svg></button>
<button class="icon-btn" data-theme-toggle aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
<span style="font-size:11.5px;color:var(--text-3)">墨 / 月白 双主题</span>
</div>
</aside>
<main class="main">
<header class="topbar">
<div class="crumbs"><span class="cn">设计说明</span><span class="crumbs__sep">/</span><span class="here">静态原型 v0.1</span></div>
<div class="topbar__spacer"></div>
<span class="prototype-tag">静态原型 · 待审查 · 确认后转 React</span>
<button class="icon-btn" data-theme-toggle aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</header>
<div class="content doc">
<div class="doc-hero">
<h1>魂镜 <span class="ver">原型 v0.2</span></h1>
<p>魂镜 Web 界面静态 HTML 原型(v0.1 第一批方向已确认,v0.2 增补第二批共 15 个界面),供审查视觉方向、布局结构与文化语言的界面落地方式。全部确认后按此实现为 React 组件;未在本文档登记的视觉细节以本页令牌为准。</p>
</div>
<section class="doc-sec">
<h2>原型页面</h2>
<p class="sub">第一批(已确认):镜庭、镜生、镜树;第二批:息壤、形仪、天工阁、神识阁、百工坊、雅集、同契台、云市、器谱、司命台、设置,及运营者/自托管使用的管理面与认证页。全站链接已疏通:跨页指向对应页面、页内为锚点;logo 单击回本目录,350ms 内连点 5 次触发「镜启」彩蛋(承接 main 分支旧版设计);侧栏可折叠并记忆偏好。</p>
<div class="page-nav">
<a class="card" href="home.html"><span class="cn">镜庭<span class="cn__plain">个人首页</span></span><span class="d">全局框架、角色入口、任务与待审事项</span><span class="p">home.html</span></a>
<a class="card" href="conversation.html"><span class="cn">镜树<span class="cn__plain">分支对话</span></span><span class="d">ContentBlock、请命朱批、界域、候答、流式芽体、分支树</span><span class="p">conversation.html</span></a>
<a class="card" href="characters.html"><span class="cn">镜生<span class="cn__plain">角色身份</span></span><span class="d">道体、缘契、内驱力、大周天、谱系、生命周期</span><span class="p">characters.html</span></a>
<a class="card" href="memory.html"><span class="cn">息壤<span class="cn__plain">记忆与知识</span></span><span class="d">候选审阅、正影纠正、明忆版本、玄络投影</span><span class="p">memory.html</span></a>
<a class="card" href="avatar.html"><span class="cn">形仪<span class="cn__plain">形象与资产</span></span><span class="d">真形锚、主仪化仪、声音、Live2D 与 3D、易相历史</span><span class="p">avatar.html</span></a>
<a class="card" href="agent-workspace.html"><span class="cn">天工阁<span class="cn__plain">Agent 任务</span></span><span class="d">工筹计划、预算、分工使、轨迹、验收</span><span class="p">agent-workspace.html</span></a>
<a class="card" href="model-gateway.html"><span class="cn">神识阁<span class="cn__plain">模型与路由</span></span><span class="d">逻辑模型、路由继承链、BYOK、用量观测</span><span class="p">model-gateway.html</span></a>
<a class="card" href="plugins.html"><span class="cn">百工坊<span class="cn__plain">插件生态</span></span><span class="d">沙箱、已安装、升级权限确认、授契记录</span><span class="p">plugins.html</span></a>
<a class="card" href="community.html"><span class="cn">雅集<span class="cn__plain">社区广场</span></span><span class="d">动态流、角色谱、话题、推荐解释与重置</span><span class="p">community.html</span></a>
<a class="card" href="collaboration.html"><span class="cn">同契台<span class="cn__plain">协作空间</span></span><span class="d">分支版本、审稿合卷、成员权限矩阵、活动</span><span class="p">collaboration.html</span></a>
<a class="card" href="marketplace.html"><span class="cn">云市<span class="cn__plain">创作市场</span></span><span class="d">商品、市契许可、退契规则、分润结算</span><span class="p">marketplace.html</span></a>
<a class="card" href="devices.html"><span class="cn">器谱<span class="cn__plain">设备与感知</span></span><span class="d">形器登记、授契、通感会话、直白权限请求</span><span class="p">devices.html</span></a>
<a class="card" href="governance.html"><span class="cn">司命台<span class="cn__plain">安全与治理</span></span><span class="d">治理队列、诊断双重授权、审计、安全概览</span><span class="p">governance.html</span></a>
<a class="card" href="admin.html"><span class="cn">管理面<span class="cn__plain">运营与部署后台</span></span><span class="d">司议队列、支持工单与脱敏支持包、权限管理(RBAC / 双人授权)、部署健康(灵台双形态)、SLO</span><span class="p">admin.html</span></a>
<a class="card" href="login.html"><span class="cn">登录<span class="cn__plain">铜镜鉴心</span></span><span class="d">铭文环镜背、扫描亮带、会话 Cookie / PKCE 说明、通行密钥与恢复钥匙</span><span class="p">login.html</span></a>
<a class="card" href="register.html"><span class="cn">注册<span class="cn__plain">创建账户</span></span><span class="d">邮箱验证码、数据权告知(默认不训练 / 可导出删除 / 私箧持钥)、条款确认</span><span class="p">register.html</span></a>
<a class="card" href="settings.html"><span class="cn">设置<span class="cn__plain">账户与偏好</span></span><span class="d">会话、私箧、数据权、删除、封镜</span><span class="p">settings.html</span></a>
</div>
</section>
<section class="doc-sec">
<h2>设计立场</h2>
<p class="sub">四条约束所有页面的决定,审查时优先核对这四条是否成立。</p>
<div class="card" style="display:flex;flex-direction:column;gap:12px">
<div><b class="cn">一 · 双层文字系统</b><span style="color:var(--text-2)"> —— 文化名用衬线宋体,直白解释用无衬线小字,两种字体本身就是"文化名 · 直白解释"两层结构的可视化;风险、金额、权限、删除等后果始终以直白语言为主,文化名只作辅助。</span></div>
<div><b class="cn">二 · 朱砂只属于朱批</b><span style="color:var(--text-2)"> —— 朱砂色是"用户确认决定"的专用语义色:请命卡的允许按钮、危险动作入口使用它;普通操作一律铜镜青。颜色即权限语言。</span></div>
<div><b class="cn">三 · 墨与月白</b><span style="color:var(--text-2)"> —— 深色为墨(青调黑,非纯黑),浅色为月白(青灰白,非暖米);铜镜青为主色,取铜镜铜绿之意,不用蓝紫系默认色。</span></div>
<div><b class="cn">四 · 状态永远三段式</b><span style="color:var(--text-2)"> —— 状态徽章固定为「文化状态 · 直白状态 + 稳定工程值」,例如「运转 · 运行中 <span class="mono">running</span>」;错误与交易状态绝不只显示文化名。</span></div>
</div>
</section>
<section class="doc-sec">
<h2>色彩令牌</h2>
<p class="sub">右下角可切换主题;以下色板随主题联动。转 React 时这些令牌进入 Tailwind 配置 / CSS 变量。</p>
<div class="swatches">
<div class="swatch"><div class="swatch__c" style="background:var(--bg);border-bottom:1px solid var(--line-soft)"></div><div class="swatch__m"><b>底 · 墨 / 月白</b>表面层级 0<span class="mono">--bg</span></div></div>
<div class="swatch"><div class="swatch__c" style="background:var(--bg-raised)"></div><div class="swatch__m"><b>浮面</b>卡片与气泡<span class="mono">--bg-raised</span></div></div>
<div class="swatch"><div class="swatch__c" style="background:var(--accent)"></div><div class="swatch__m"><b>铜镜青</b>主色 · 选中/链接/运转<span class="mono">--accent</span></div></div>
<div class="swatch"><div class="swatch__c" style="background:var(--cinnabar)"></div><div class="swatch__m"><b>朱砂</b>朱批 · 确认与危险<span class="mono">--cinnabar</span></div></div>
<div class="swatch"><div class="swatch__c" style="background:var(--ok)"></div><div class="swatch__m"><b>已成</b>完成态<span class="mono">--ok</span></div></div>
<div class="swatch"><div class="swatch__c" style="background:var(--warn)"></div><div class="swatch__m"><b>秋香</b>整备 / 幻境<span class="mono">--warn</span></div></div>
<div class="swatch"><div class="swatch__c" style="background:var(--err)"></div><div class="swatch__m"><b>受阻</b>失败态<span class="mono">--err</span></div></div>
<div class="swatch"><div class="swatch__c" style="background:var(--text-1)"></div><div class="swatch__m"><b>正文</b>一级文字<span class="mono">--text-1</span></div></div>
</div>
</section>
<section class="doc-sec">
<h2>字体规范</h2>
<p class="sub">不引入外部字体文件,使用系统中文字体栈;转 React 时同栈迁移。</p>
<div class="card type-spec">
<div class="type-spec__row"><span class="type-spec__label">文化名 · 宋体衬线</span><span class="type-spec__sample cn" style="font-size:22px;font-weight:600">镜树 · 分支对话</span></div>
<div class="type-spec__row"><span class="type-spec__label">界面正文 · 无衬线</span><span class="type-spec__sample" style="font-size:14px">正文 14px / 行高 1.65,辅助文字 12.5px,元信息 11.5px</span></div>
<div class="type-spec__row"><span class="type-spec__label">稳定值 · 等宽</span><span class="type-spec__sample mono" style="font-size:13px">running · m_01h8xq… · block_id: blk_7f3a</span></div>
</div>
</section>
<section class="doc-sec">
<h2>状态徽章全集</h2>
<p class="sub">对应 docs/product/cultural-language.md 第 10 节;徽章三段:文化状态 · 直白状态 · 稳定值。</p>
<div class="card">
<div class="table-wrap"><table class="data badge-table">
<thead><tr><th>徽章</th><th>稳定值</th><th>使用场景</th></tr></thead>
<tbody>
<tr><td><span class="badge badge--muted"><span class="badge__dot"></span>待启 · 排队中 <span class="mono">queued</span></span></td><td class="mono">queued</td><td>Run / 媒体任务排队</td></tr>
<tr><td><span class="badge badge--info"><span class="badge__dot"></span>运转 · 运行中 <span class="mono">running</span></span></td><td class="mono">running</td><td>Run 执行中</td></tr>
<tr><td><span class="badge badge--warn"><span class="badge__dot"></span>请命中 · 等待确认 <span class="mono">waiting_approval</span></span></td><td class="mono">waiting_approval</td><td>等待朱批</td></tr>
<tr><td><span class="badge badge--ok"><span class="badge__dot"></span>已成 · 已完成 <span class="mono">completed</span></span></td><td class="mono">completed</td><td>Run / 任务完成</td></tr>
<tr><td><span class="badge badge--err"><span class="badge__dot"></span>受阻 · 失败 <span class="mono">failed</span></span></td><td class="mono">failed</td><td>失败(附原因与重试)</td></tr>
<tr><td><span class="badge badge--muted"><span class="badge__dot"></span>已止 · 已取消 <span class="mono">cancelled</span></span></td><td class="mono">cancelled</td><td>用户取消</td></tr>
<tr><td><span class="badge badge--muted"><span class="badge__dot"></span>守一 · 安静待命 <span class="mono">idle</span></span></td><td class="mono">idle</td><td>角色无运行</td></tr>
<tr><td><span class="badge badge--warn"><span class="badge__dot"></span>整备 · 正在处理 <span class="mono">processing</span></span></td><td class="mono">processing</td><td>媒体 / 资源处理</td></tr>
<tr><td><span class="badge badge--warn"><span class="badge__dot"></span>待审 · 等待审核 <span class="mono">pending_review</span></span></td><td class="mono">pending_review</td><td>社区 / 市场审核</td></tr>
<tr><td><span class="badge badge--ok"><span class="badge__dot"></span>已刊 · 已发布 <span class="mono">published</span></span></td><td class="mono">published</td><td>社区内容</td></tr>
</tbody>
</table></div>
</div>
</section>
<section class="doc-sec">
<h2>组件样例</h2>
<p class="sub">按钮语义:铜镜青为主操作,朱砂仅用于朱批(用户确认决定)与危险操作,描边为次操作。</p>
<div class="card demo-block">
<div class="demo-row">
<button class="btn btn--primary">开始对话</button>
<button class="btn btn--cinnabar">允许(朱批)</button>
<button class="btn btn--outline">查看谱系</button>
<button class="btn btn--ghost">取消</button>
<button class="btn btn--danger">删除角色数据</button>
<button class="btn btn--outline btn--sm">续生</button>
</div>
<div class="demo-row">
<span class="cn" style="font-size:16px">请命<span class="cn__plain">请求你的确认</span></span>
<span class="cn" style="font-size:16px">息壤<span class="cn__plain">角色记忆</span></span>
<span class="cn" style="font-size:16px">幻境<span class="cn__plain">临时试演 · 不入正史</span></span>
</div>
</div>
<div class="card demo-block">
<div class="block__head" style="border-radius:var(--radius) var(--radius) 0 0"><span class="lang">python</span><span class="fname">reading_progress.py</span><span class="acts"><button class="icon-btn" style="width:26px;height:26px" aria-label="复制源码"><svg style="width:13px;height:13px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button></span></div>
<pre class="code"><span class="c"># 汇总共读接龙进度(示意)</span>
<span class="k">from</span> pathlib <span class="k">import</span> Path
<span class="k">def</span> <span class="f">summarize</span>(records: <span class="k">list</span>[<span class="k">dict</span>]) -&gt; <span class="k">dict</span>:
done = [r <span class="k">for</span> r <span class="k">in</span> records <span class="k">if</span> r[<span class="s">"finished"</span>]]
<span class="k">return</span> {
<span class="s">"total"</span>: <span class="f">len</span>(records),
<span class="s">"finished"</span>: <span class="f">len</span>(done),
<span class="s">"pending"</span>: [r[<span class="s">"name"</span>] <span class="k">for</span> r <span class="k">in</span> records <span class="k">if</span> <span class="k">not</span> r[<span class="s">"finished"</span>]],
}</pre>
</div>
<div class="card demo-block">
<div class="math">注意力权重 = softmax( QK<sup>T</sup> / √d<sub>k</sub> ) · V
<span class="math__fallback">fallback:注意力权重等于 Q、K 转置乘积经缩放与归一化后对 V 的加权求和</span>
</div>
</div>
</section>
<section class="doc-sec">
<h2>审查重点</h2>
<p class="sub">第一批(色彩、字体、镜树/镜生/镜庭结构)已确认;本轮重点核对第二批页面,反馈时按页名 + 位置描述即可。</p>
<div class="card" style="display:flex;flex-direction:column;gap:10px">
<div>1. 息壤:候选审阅的「归真入忆 / 正影纠正 / 忽略」操作,以及大周天来源、重忆提示的呈现是否可理解。</div>
<div>2. 天工阁:工筹步骤、预算、分工使、轨迹的信息层级是否符合你对 Run 治理的预期。</div>
<div>3. 神识阁 / 百工坊:路由继承链与「升级新增权限需重新确认」的展示是否清晰,朱批语义是否仍然专一。</div>
<div>4. 云市:金额、分润、退契规则是否足够直白(文化名没有掩盖交易后果)。</div>
<div>5. 器谱 / 司命台 / 设置:直白权限句、双重授权、私箧与删除等高风险项的措辞与视觉重量。</div>
<div>6. 雅集 / 同契台:社区帖卡与协作审稿流的信息密度是否合适。</div>
</div>
</section>
<section class="doc-sec">
<h2>旧版传承与候选</h2>
<p class="sub">已系统挖掘 main 分支旧版 25 页设计原型(铜镜鉴心体系),按「取手法、不照搬旧概念名」原则融入——六爻 / 九枢 / 三魂等概念 1.0 已由小周天、灵台六模块、内驱力等替代,仅传承其可视化手法。</p>
<div class="card">
<div class="table-wrap"><table class="data">
<thead><tr><th>旧版想法</th><th>来源页</th><th>状态</th></tr></thead>
<tbody>
<tr><td>鎏金发丝线三档令牌</td><td>tokens.css</td><td class="mark-ok">✓ 已融入全站(分组标题 / 金标竖条 / 品牌外环 / 时间线)</td></tr>
<tr><td>logo 五连击「镜启」诗签彩蛋</td><td>_theme.js</td><td class="mark-ok">✓ 已融入(修好首次单击即跳转的缺陷)</td></tr>
<tr><td>铜镜登录页(铭文环 / 铜面噪点 / 扫描亮带)</td><td>copper-mirror-login</td><td class="mark-ok">✓ 已融入登录 / 注册页</td></tr>
<tr><td>极光铜晕底 + 卡片 hover 光扫</td><td>_base.css / rune-engine</td><td class="mark-ok">✓ 已融入全站(极淡,不干扰阅读)</td></tr>
<tr><td>日晷时辰盘 + 节气 + 相伴日文案</td><td>time</td><td class="mark-ok">✓ 已融入镜庭 hero(巳时 · 白露 · 第 918 日)</td></tr>
<tr><td>朱批卡风险分级(RISK L2)</td><td>chat-redesign</td><td class="mark-ok">✓ 已融入镜树请命卡</td></tr>
<tr><td>「呈镜」发送按钮命名(旧版已确立血脉)</td><td>chat-redesign</td><td class="mark-ok">✓ 已融入镜树输入区(aria 仍读「发送」)</td></tr>
<tr><td>运行阶段爻象三态(done / active / pending)</td><td>chat / evolution</td><td class="mark-ok">✓ 已融入天工阁工筹(active 步骤鎏金光晕)</td></tr>
<tr><td>河图置信度图谱(置信→线宽,假设→朱漆虚线待验)</td><td>world</td><td class="mark-ok">✓ 已融入息壤玄络(待验边 + 标注)</td></tr>
<tr><td>息壤浮现(本次回应用了哪些记忆)</td><td>chat / copper-v2</td><td class="mark-ok">✓ 对应 1.0 上下文镜(镜树右栏,同源 ContextSnapshot)</td></tr>
<tr><td>九枢星盘「活的仪器」+ 铜镜四态健康词汇</td><td>admin / copper-v2</td><td class="mark-ok">✓ 已复刻为灵台六枢星盘(太一居中 + 五枢环绕 + 呼吸刻度 / 慢旋 / 告警闪烁)+ 镜态徽章(管理面)</td></tr>
<tr><td>五行生克健康环(系统健康=关系动力学)</td><td>wuxing</td><td class="mark-ok">✓ 已复刻为相生 / 相克指标对告警(管理面星盘卡;环状图 React 阶段评估)</td></tr>
<tr><td>眉批 depth 标尺注入预览</td><td>margin-notes</td><td style="color:var(--warn)">候选 —— 上下文镜深度维度(React 阶段与 Context Manifest 联动)</td></tr>
<tr><td>好奇萤火(亮度=期望值)与探索天平</td><td>drive</td><td class="mark-ok">✓ 萤火已融入内驱力维度(亮度=维度值);探索天平待天工阁预算增强时评估</td></tr>
<tr><td>周天四镜阶段链(评 / 演 / 候 / 藏 四面小铜镜)</td><td>evolution</td><td class="mark-ok">✓ 已复刻为大周天四镜:回 / 照 / 议 / 呈(镜生页,ADR-023 四阶段)</td></tr>
<tr><td>会话列表「今日 / 本周 / 更早」分组 + 彩点状态</td><td>chat-redesign</td><td class="mark-ok">✓ 已融入镜树会话列表(活跃铜绿 / 幻境秋香 / 待朱批朱砂)</td></tr>
<tr><td>「深虑 / 博闻」输入控制药丸 + 用户消息鎏金右边线</td><td>chat-redesign / copper-v2</td><td class="mark-ok">✓ 已融入镜树输入区与用户气泡</td></tr>
<tr><td>起居注时间河「卷」章分割</td><td>admin</td><td class="mark-ok">✓ 已融入管理面审计快照</td></tr>
<tr><td>太极阴阳平衡 gauge(三态)</td><td>cosmos</td><td class="mark-ok">✓ 已复刻为朱批治理阴阳平衡(司命台,阳亢 / 平衡 / 阴盛)</td></tr>
<tr><td>角色 card-banner 渐变横幅</td><td>character-detail</td><td class="mark-ok">✓ 已融入镜生详情页</td></tr>
<tr><td>思考块折叠卡(复刻 chat-redesign)</td><td>chat-redesign</td><td class="mark-ok">✓ 已融入(用户确认):思考摘要可折叠 + 深虑强度(低/中/高)——<b>契约候选</b>:正式实现前需在 content-blocks 契约登记 <span class="mono">thinking</span> 块类型与流式语义;摘要仅会话内可见,不入长期记录与诊断导出</td></tr>
</tbody>
</table></div>
</div>
</section>
</div>
</main>
</div>
<script src="assets/app.js?v=3"></script>
</body>
</html>
+87
View File
@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>魂镜 · 登录</title>
<link rel="stylesheet" href="assets/app.css?v=10">
</head>
<body>
<svg style="display:none" aria-hidden="true">
<symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol>
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></symbol>
<symbol id="i-key" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></symbol>
<symbol id="i-fingerprint" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 11a4 4 0 0 1 4 4v5"/><path d="M8 15a4 4 0 0 1 1-2.7"/><path d="M4 15a8 8 0 0 1 8-8 8 8 0 0 1 6.7 3.7"/><path d="M12 19v2"/><path d="M4.9 10.3A8 8 0 0 1 12 3a8 8 0 0 1 8 8c0 1-.1 2.3-.4 3"/></symbol>
</svg>
<div class="stage">
<div class="stage__bg" aria-hidden="true">
<div class="stage__rings">
<svg viewBox="0 0 800 800">
<defs><path id="ringpath" d="M400,400 m-300,0 a300,300 0 1,1 600,0 a300,300 0 1,1 -600,0"/></defs>
<circle class="ring-ink" cx="400" cy="400" r="392"/>
<circle class="ring-faint" cx="400" cy="400" r="336"/>
<g class="ring-slow"><circle class="ring-faint" cx="400" cy="400" r="272"/></g>
<circle class="ring-ink" cx="400" cy="400" r="206"/>
<circle class="ring-faint" cx="400" cy="400" r="158"/>
<g class="ring-slow" style="animation-direction:reverse"><circle class="ring-faint" cx="400" cy="400" r="104"/></g>
<text fill="var(--gilt-faint)" font-size="12" letter-spacing="6" style="font-family:var(--font-serif)">
<textPath href="#ringpath">镜中有镜 · 心外无心 · 汝来看此镜时 · 此镜亦照汝 · 铜镜本无字 · 照尽往来人 ·</textPath>
</text>
</svg>
</div>
<svg style="position:absolute;inset:0;width:100%;height:100%;opacity:.035" aria-hidden="true"><filter id="grain"><feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="2"/></filter><rect width="100%" height="100%" filter="url(#grain)"/></svg>
</div>
<header class="stage__topbar">
<span style="font-size:12px;color:var(--text-3)">静态原型 · 待审查</span>
<span class="spacer"></span>
<a href="index.html" style="font-size:12px;color:var(--text-3)">原型目录</a>
<button class="icon-btn" data-theme-toggle aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</header>
<main class="stage__main">
<div class="mirror-card">
<div class="mirror-card__brand">
<div class="brand__mark" aria-hidden="true"></div>
<div>
<div style="font-family:var(--font-serif);font-size:20px;font-weight:600;line-height:1.2">魂镜</div>
<div style="font-size:11px;color:var(--text-3)">Hunjing</div>
</div>
</div>
<div class="mirror-card__title">
<h1>登入魂镜</h1>
</div>
<p class="mirror-card__sub">长期 AI 角色、Agent 与创作社区平台</p>
<form onsubmit="return false">
<div class="field-group">
<label for="login-email">邮箱</label>
<input class="input" id="login-email" type="email" autocomplete="username" placeholder="you@example.com">
</div>
<div class="field-group">
<label for="login-pwd">密码</label>
<input class="input" id="login-pwd" type="password" autocomplete="current-password" placeholder="••••••••">
</div>
<a class="btn btn--primary" href="home.html" style="width:100%;margin-top:6px">登入</a>
</form>
<div class="login-alt">其他方式</div>
<div style="display:flex;gap:10px">
<a class="btn btn--outline btn--sm" href="#" style="flex:1" title="通行密钥(WebAuthn)"><svg><use href="#i-fingerprint"/></svg>通行密钥</a>
<a class="btn btn--outline btn--sm" href="#" style="flex:1" title="恢复钥匙(离线恢复码)"><svg><use href="#i-key"/></svg>恢复钥匙</a>
</div>
<p class="login-note">
Web 使用安全会话 Cookie(OAuth 2.1);桌面与移动客户端使用 Authorization Code + PKCE。<br>
凭据只经 Header 传递,不进入 URL、日志或页面。连续失败将触发限流与通知。
</p>
<div class="login-foot">
还没有账户?<a href="register.html">创建账户</a>
</div>
</div>
</main>
</div>
<script src="assets/app.js?v=3"></script>
</body>
</html>
+140
View File
@@ -0,0 +1,140 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>云市 · 创作市场 — 魂镜 Hunjing</title>
<link rel="stylesheet" href="assets/app.css?v=10">
</head>
<body>
<svg style="display:none" aria-hidden="true"><symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol><symbol id="i-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></symbol><symbol id="i-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></symbol><symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol><symbol id="i-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></symbol><symbol id="i-layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></symbol><symbol id="i-kanban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3h4v18H5z"/><path d="M10 3h4v12h-4z"/><path d="M15 3h4v8h-4z"/></symbol><symbol id="i-package" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></symbol><symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></symbol><symbol id="i-route" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="19" r="3"/><circle cx="18" cy="5" r="3"/><path d="M12 19h4.5a3.5 3.5 0 0 0 0-7h-9a3.5 3.5 0 0 1 0-7H12"/></symbol><symbol id="i-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></symbol><symbol id="i-bag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></symbol><symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></symbol><symbol id="i-monitor" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></symbol><symbol id="i-sliders" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></symbol><symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></symbol><symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></symbol><symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></symbol><symbol id="i-clock" viewBox="0 0 24 24" fill="none" sLine truncated
</svg>
<div class="app">
<aside class="sidebar">
<div class="brand">
<div class="brand__mark" aria-hidden="true"></div>
<div class="brand__text">
<div class="brand__name">魂镜</div>
<div class="brand__sub">Hunjing</div>
</div>
</div>
<nav class="side-nav" aria-label="主导航"><div class="nav-group"><div class="nav-group__title">常用</div><a class="nav-item" href="home.html"><svg><use href="#i-home"/></svg><span class="cn">镜庭</span><span class="nav-plain">首页</span></a><a class="nav-item" href="conversation.html"><svg><use href="#i-branch"/></svg><span class="cn">镜树</span><span class="nav-plain">对话</span></a><a class="nav-item" href="characters.html"><svg><use href="#i-user"/></svg><span class="cn">镜生</span><span class="nav-plain">角色</span></a><a class="nav-item" href="memory.html"><svg><use href="#i-book"/></svg><span class="cn">息壤</span><span class="nav-plain">记忆</span></a><a class="nav-item" href="avatar.html"><svg><use href="#i-layers"/></svg><span class="cn">形仪</span><span class="nav-plain">形象</span></a></div><div class="nav-group"><div class="nav-group__title">创造</div><a class="nav-item" href="agent-workspace.html"><svg><use href="#i-kanban"/></svg><span class="cn">天工阁</span><span class="nav-plain">任务</span></a><a class="nav-item" href="plugins.html"><svg><use href="#i-package"/></svg><span class="cn">百工坊</span><span class="nav-plain">插件</span></a><a class="nav-item" href="collaboration.html"><svg><use href="#i-users"/></svg><span class="cn">同契台</span><span class="nav-plain">协作</span></a><a class="nav-item" href="model-gateway.html"><svg><use href="#i-route"/></svg><span class="cn">神识阁</span><span class="nav-plain">模型</span></a></div><div class="nav-group"><div class="nav-group__title">世界</div><a class="nav-item" href="community.html"><svg><use href="#i-globe"/></svg><span class="cn">雅集</span><span class="nav-plain">社区</span></a><a class="nav-item is-active" aria-current="page" href="marketplace.html"><svg><use href="#i-bag"/></svg><span class="cn">云市</span><span class="nav-plain">市场</span></a></div><div class="nav-group"><div class="nav-group__title">治理与账户</div><a class="nav-item" href="governance.html"><svg><use href="#i-shield"/></svg><span class="cn">司命台</span><span class="nav-plain">治理</span></a><a class="nav-item" href="devices.html"><svg><use href="#i-monitor"/></svg><span class="cn">器谱</span><span class="nav-plain">设备</span></a><a class="nav-item" href="settings.html"><svg><use href="#i-sliders"/></svg><span class="cn">设置</span><span class="nav-plain">账户</span></a></div></nav>
<div class="side-foot">
<button class="icon-btn side-fold-btn" data-sidebar-fold aria-label="收起侧栏" title="收起 / 展开侧栏(记忆偏好)"><svg><use href="#i-chevrons"/></svg></button>
<span class="avatar avatar--sm avatar--user">舟</span>
<span style="font-size:12.5px">云舟</span>
<button class="icon-btn" data-theme-toggle style="margin-left:auto" aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</div>
</aside>
<main class="main">
<header class="topbar">
<div class="crumbs"><span class="cn">云市</span><span class="crumbs__sep">·</span><span class="here">创作市场</span></div>
<div class="topbar__spacer"></div>
<span class="prototype-tag">静态原型 · 待审查</span>
<button class="icon-btn" data-theme-toggle aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</header>
<div class="content">
<div class="realm-banner" role="note">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>
购买的是版本与许可(市契),不是人格所有权。金额、费率与退款规则始终直白展示,文化名只作辅助。
</div>
<div style="display:flex;gap:12px;align-items:center;margin-bottom:16px;flex-wrap:wrap">
<div class="chips" style="margin-bottom:0">
<button class="chip is-on">全部</button>
<button class="chip">角色模板</button>
<button class="chip">世界设定</button>
<button class="chip">Live2D</button>
<button class="chip">3D 资产</button>
<button class="chip">音色包</button>
<button class="chip">插件</button>
</div>
<div class="searchbar" style="flex:1;min-width:200px">
<svg><use href="#i-search"/></svg>
<input type="search" placeholder="搜索商品 / 创作者" aria-label="搜索市场">
</div>
</div>
<section class="section">
<div class="asset-grid">
<div class="asset-card">
<div class="asset-thumb" style="background:linear-gradient(150deg,#2f4858,#6a8caf 60%,#3b5a70)"><span class="glyph">雾</span><span class="asset-type">世界设定</span></div>
<div class="asset-body"><a class="asset-name" href="#license" style="color:inherit;text-decoration:underline;text-decoration-color:var(--gilt-dim);text-underline-offset:3px">雾中茶寮 · 世界合集</a><span class="asset-sub">雪袖的茶寮 · v3 · 售出 412</span>
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:2px"><span class="price">¥28.00</span><span class="mem-type">可商用</span></div>
</div>
</div>
<div class="asset-card">
<div class="asset-thumb" style="background:linear-gradient(150deg,#4b3a2f,#a3815f 60%,#5d4a3a)"><span class="glyph">灯</span><span class="asset-type">角色模板</span></div>
<div class="asset-body"><span class="asset-name">照夜 · 掌灯人模板</span><span class="asset-sub">沉舟 · v2 · 售出 1,208</span>
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:2px"><span class="price">¥12.00</span><span class="mem-type">个人用</span></div>
</div>
</div>
<div class="asset-card">
<div class="asset-thumb" style="background:linear-gradient(150deg,#233b37,#3f7a6e 60%,#2a4d47)"><span class="glyph">灵</span><span class="asset-type">Live2D</span></div>
<div class="asset-body"><span class="asset-name">守砚人 · 灵动模型</span><span class="asset-sub">云舟 · v3 · 售出 96</span>
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:2px"><span class="price">¥45.00</span><span class="mem-type">可商用</span></div>
</div>
</div>
<div class="asset-card">
<div class="asset-thumb" style="background:linear-gradient(150deg,#37343f,#635d7a 60%,#443f52)"><span class="glyph">匣</span><span class="asset-type">音色包</span></div>
<div class="asset-body"><span class="asset-name">江南雨声 · 环境音色</span><span class="asset-sub">听澜 · v1 · 售出 340</span>
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:2px"><span class="price price--free">免费</span><span class="mem-type">署名使用</span></div>
</div>
</div>
<div class="asset-card">
<div class="asset-thumb" style="background:linear-gradient(150deg,#33392e,#6e8455 60%,#414a37)"><span class="glyph">砚</span><span class="asset-type">3D 资产</span></div>
<div class="asset-body"><span class="asset-name">书斋器物 · VRM 附件包</span><span class="asset-sub">临江仙 · v2 · 售出 58</span>
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:2px"><span class="price">¥18.00</span><span class="mem-type">个人用</span></div>
</div>
</div>
<div class="asset-card">
<div class="asset-thumb" style="background:linear-gradient(150deg,#413045,#8a6a95 60%,#503c55)"><span class="glyph">谱</span><span class="asset-type">插件</span></div>
<div class="asset-body"><span class="asset-name">曲谱排版 · 插件</span><span class="asset-sub">百工 · v1.4 · 售出 233</span>
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:2px"><span class="price">¥6.00</span><span class="mem-type">个人用</span></div>
</div>
</div>
</div>
</section>
<section class="section" id="license">
<div class="section__head"><span class="cn">市契<span class="cn__plain">商品许可 · 以「雾中茶寮 v3」为例</span></span></div>
<div class="card">
<div class="table-wrap"><table class="data">
<thead><tr><th>项目</th><th>内容</th></tr></thead>
<tbody>
<tr><td>许可范围</td><td>商用(衍生作品可售,须保留署名;不可单独转售原始文件)</td></tr>
<tr><td>交付物</td><td>世界文档 42 页 · 地图 PSD · 场景音效 12 段 · 更新至 v3.x</td></tr>
<tr><td>退契规则</td><td>14 天内未下载可全额退款;下载后遵循许可,重大缺陷另行处理</td></tr>
<tr><td>价格构成</td><td>¥28.00 = 创作者分润 ¥23.80(85%)+ 平台费 ¥4.20(15%)· 税按地区另计</td></tr>
<tr><td>地区可售</td><td>当前地区可售 · 跨区价格与税务见结算页</td></tr>
</tbody>
</table></div>
<div style="display:flex;gap:10px;margin-top:14px;align-items:center">
<button class="btn btn--cinnabar">购买 · ¥28.00</button>
<button class="btn btn--outline">加入购物车</button>
<span style="font-size:12px;color:var(--text-3)">支付经 Idempotency-Key 幂等 · 重复点击不会重复扣款</span>
</div>
</div>
</section>
<section class="section card">
<div class="card__head"><span class="cn">结算<span class="cn__plain">创作者收益</span></span><a class="more" href="settings.html">结算设置 →</a></div>
<div style="display:flex;gap:28px;flex-wrap:wrap;align-items:center">
<div><div style="font-size:11.5px;color:var(--text-3)">可结算余额</div><div class="price" style="font-size:22px">¥1,240.50</div></div>
<div><div style="font-size:11.5px;color:var(--text-3)">本月分润</div><div class="price" style="font-size:22px">¥318.20</div></div>
<div><div style="font-size:11.5px;color:var(--text-3)">上次结算批次</div><div style="font-size:14px">09-10 · 已付 ¥986.30</div></div>
<div style="margin-left:auto;display:flex;gap:10px">
<button class="btn btn--outline btn--sm">下载对账单</button>
<button class="btn btn--primary btn--sm">申请结算</button>
</div>
</div>
<p style="font-size:12px;color:var(--text-3);margin-top:10px">Ledger 不可变,修正写反向分录;每日自动对账(支付 / 订单 / Ledger / 结算四账核对)。</p>
</section>
</div>
</main>
</div>
<script src="assets/app.js?v=3"></script>
</body>
</html>
+242
View File
@@ -0,0 +1,242 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>息壤 · 记忆与知识 — 魂镜 Hunjing</title>
<link rel="stylesheet" href="assets/app.css?v=10">
</head>
<body>
<svg style="display:none" aria-hidden="true">
<symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol>
<symbol id="i-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></symbol>
<symbol id="i-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></symbol>
<symbol id="i-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></symbol>
<symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol>
<symbol id="i-layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></symbol>
<symbol id="i-kanban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3h4v18H5z"/><path d="M10 3h4v12h-4z"/><path d="M15 3h4v8h-4z"/></symbol>
<symbol id="i-package" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></symbol>
<symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></symbol>
<symbol id="i-route" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="19" r="3"/><circle cx="18" cy="5" r="3"/><path d="M12 19h4.5a3.5 3.5 0 0 0 0-7h-9a3.5 3.5 0 0 1 0-7H12"/></symbol>
<symbol id="i-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></symbol>
<symbol id="i-bag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></symbol>
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></symbol>
<symbol id="i-monitor" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></symbol>
<symbol id="i-sliders" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></symbol>
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></symbol>
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></symbol>
<symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></symbol>
<symbol id="i-quote" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"/><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"/></symbol>
<symbol id="i-chevrons" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="11 17 6 12 11 7"/><polyline points="18 17 13 12 18 7"/></symbol>
</svg>
<div class="app">
<aside class="sidebar">
<div class="brand">
<div class="brand__mark" aria-hidden="true"></div>
<div class="brand__text">
<div class="brand__name">魂镜</div>
<div class="brand__sub">Hunjing</div>
</div>
</div>
<nav class="side-nav" aria-label="主导航">
<div class="nav-group">
<div class="nav-group__title">常用</div>
<a class="nav-item" href="home.html"><svg><use href="#i-home"/></svg><span class="cn">镜庭</span><span class="nav-plain">首页</span></a>
<a class="nav-item" href="conversation.html"><svg><use href="#i-branch"/></svg><span class="cn">镜树</span><span class="nav-plain">对话</span></a>
<a class="nav-item" href="characters.html"><svg><use href="#i-user"/></svg><span class="cn">镜生</span><span class="nav-plain">角色</span></a>
<a class="nav-item is-active" href="memory.html" aria-current="page"><svg><use href="#i-book"/></svg><span class="cn">息壤</span><span class="nav-plain">记忆</span></a>
<a class="nav-item" href="avatar.html"><svg><use href="#i-layers"/></svg><span class="cn">形仪</span><span class="nav-plain">形象</span></a>
</div>
<div class="nav-group">
<div class="nav-group__title">创造</div>
<a class="nav-item" href="agent-workspace.html"><svg><use href="#i-kanban"/></svg><span class="cn">天工阁</span><span class="nav-plain">任务</span></a>
<a class="nav-item" href="plugins.html"><svg><use href="#i-package"/></svg><span class="cn">百工坊</span><span class="nav-plain">插件</span></a>
<a class="nav-item" href="collaboration.html"><svg><use href="#i-users"/></svg><span class="cn">同契台</span><span class="nav-plain">协作</span></a>
<a class="nav-item" href="model-gateway.html"><svg><use href="#i-route"/></svg><span class="cn">神识阁</span><span class="nav-plain">模型</span></a>
</div>
<div class="nav-group">
<div class="nav-group__title">世界</div>
<a class="nav-item" href="community.html"><svg><use href="#i-globe"/></svg><span class="cn">雅集</span><span class="nav-plain">社区</span></a>
<a class="nav-item" href="marketplace.html"><svg><use href="#i-bag"/></svg><span class="cn">云市</span><span class="nav-plain">市场</span></a>
</div>
<div class="nav-group">
<div class="nav-group__title">治理与账户</div>
<a class="nav-item" href="governance.html"><svg><use href="#i-shield"/></svg><span class="cn">司命台</span><span class="nav-plain">治理</span></a>
<a class="nav-item" href="devices.html"><svg><use href="#i-monitor"/></svg><span class="cn">器谱</span><span class="nav-plain">设备</span></a>
<a class="nav-item" href="settings.html"><svg><use href="#i-sliders"/></svg><span class="cn">设置</span><span class="nav-plain">账户</span></a>
</div>
</nav>
<div class="side-foot">
<button class="icon-btn side-fold-btn" data-sidebar-fold aria-label="收起侧栏" title="收起 / 展开侧栏(记忆偏好)"><svg><use href="#i-chevrons"/></svg></button>
<span class="avatar avatar--sm avatar--user">舟</span>
<span style="font-size:12.5px">云舟</span>
<button class="icon-btn" data-theme-toggle style="margin-left:auto" aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</div>
</aside>
<main class="main">
<header class="topbar">
<div class="crumbs"><span class="cn">息壤</span><span class="crumbs__sep">·</span><span class="here">记忆与知识</span></div>
<div class="topbar__spacer"></div>
<span class="prototype-tag">静态原型 · 待审查</span>
<button class="icon-btn" data-theme-toggle aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</header>
<div class="content">
<div style="display:flex;gap:12px;align-items:center;margin-bottom:18px;flex-wrap:wrap">
<div class="chips" style="margin-bottom:0">
<button class="chip is-on">全部 · 132</button>
<button class="chip">青梧 · 98</button>
<button class="chip">雪袖 · 22</button>
<button class="chip">观澜 · 12</button>
</div>
<div class="searchbar" style="flex:1;min-width:220px">
<svg><use href="#i-search"/></svg>
<input type="search" placeholder="检索记忆:关键词 / 语义 / 结构化(授权预过滤后检索)" aria-label="检索记忆">
</div>
</div>
<div class="tabs" role="tablist" aria-label="记忆类型">
<button class="tab is-on" role="tab" aria-selected="true">候选审阅 <span class="cnt">3</span></button>
<button class="tab" role="tab" aria-selected="false">明忆 <span class="cnt">128</span></button>
<button class="tab" role="tab" aria-selected="false">重忆 <span class="cnt">4</span></button>
<button class="tab" role="tab" aria-selected="false">世谱 <span class="cnt">17</span></button>
<button class="tab" role="tab" aria-selected="false">玄络</button>
</div>
<div class="realm-banner" role="note">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
归真的约束:候选经你确认才会影响未来上下文;对话中的归真不自动写入所有长期记忆。境界内容只成世界知识候选,幻境内容须手动入忆并标注性质。
</div>
<!-- 候选 1:低敏感偏好 -->
<div class="card" style="margin-bottom:14px">
<div class="card__head">
<span class="mem-type">偏好 preference</span>
<span style="font-size:11.5px;color:var(--text-3)">来源 · 镜树对话 <span class="mono">m_91d0</span> · extractor v5</span>
<span style="margin-left:auto;font-size:11.5px;color:var(--text-3)">置信 0.92 · 敏感度低 · 青梧</span>
</div>
<p style="font-size:14px" class="cn">「读书会隔周四 19:30 举行,每次 90 分钟。」</p>
<div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap">
<button class="btn btn--primary btn--sm"><svg><use href="#i-check"/></svg>归真入忆</button>
<button class="btn btn--outline btn--sm">正影 · 纠正后入忆</button>
<button class="btn btn--ghost btn--sm">忽略</button>
<a class="btn btn--ghost btn--sm" href="conversation.html"><svg><use href="#i-quote"/></svg>查看来源上下文</a>
</div>
</div>
<!-- 候选 2:大周天来源 -->
<div class="card" style="margin-bottom:14px">
<div class="card__head">
<span class="mem-type">事实 fact</span>
<span class="badge badge--info"><span class="badge__dot"></span>大周天 · 第 7 轮提议</span>
<span style="margin-left:auto;font-size:11.5px;color:var(--text-3)">置信 0.71 · 敏感度低 · 青梧</span>
</div>
<p style="font-size:14px" class="cn">「云舟在共读选篇时倾向短篇,超过 1500 字的篇目完成率明显下降。」</p>
<p style="font-size:12px;color:var(--text-3);margin-top:6px">由 42 次小周天完成信号的回顾阶段产生 · 复用同一审核流 · 可追溯本轮输入</p>
<div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap">
<button class="btn btn--primary btn--sm"><svg><use href="#i-check"/></svg>归真入忆</button>
<button class="btn btn--outline btn--sm">正影 · 纠正后入忆</button>
<button class="btn btn--ghost btn--sm">忽略</button>
<a class="btn btn--ghost btn--sm" href="characters.html">查看大周天第 7 轮</a>
</div>
</div>
<!-- 候选 3:中敏感关系 → 重忆 -->
<div class="card" style="margin-bottom:14px;border-color:color-mix(in srgb,var(--warn) 35%,transparent)">
<div class="card__head">
<span class="mem-type">关系 relationship</span>
<span class="badge badge--warn"><span class="badge__dot"></span>建议作重忆保存</span>
<span style="margin-left:auto;font-size:11.5px;color:var(--text-3)">置信 0.68 · 敏感度中 · 青梧</span>
</div>
<p style="font-size:14px" class="cn">「云舟近期在工作日晚上更希望被倾听,而不是立刻获得建议。」</p>
<p style="font-size:12px;color:var(--text-3);margin-top:6px">重忆 · 敏感或高影响记忆:归真后仅在明确允许时进入上下文,可随时正影纠正或归档,版本不可变。</p>
<div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap">
<button class="btn btn--primary btn--sm"><svg><use href="#i-check"/></svg>归真为重忆</button>
<button class="btn btn--outline btn--sm">正影 · 纠正后入忆</button>
<button class="btn btn--ghost btn--sm">忽略</button>
<a class="btn btn--ghost btn--sm" href="conversation.html"><svg><use href="#i-quote"/></svg>查看来源上下文</a>
</div>
</div>
<!-- 最近归真 -->
<section class="section" style="margin-top:28px">
<div class="section__head">
<span class="cn">最近归真<span class="cn__plain">已生效的明忆</span></span>
<span class="more" style="color:var(--text-3);cursor:default">全部 128 条 · 见下方列表</span>
</div>
<div class="card" style="padding:6px 18px">
<div class="list-row">
<span class="mem-type" style="flex:none">偏好</span>
<div class="list-row__body">
<div class="list-row__title cn">「云舟不喜长篇说教,笔记以要点为先。」</div>
<div class="list-row__sub">青梧 · v3(正影自 v2)· 来源:手动 · 有效期长期 · ETag 并发受控</div>
</div>
<button class="btn btn--ghost btn--sm">正影</button>
<button class="btn btn--ghost btn--sm">归档</button>
</div>
<div class="list-row">
<span class="mem-type" style="flex:none">事实</span>
<div class="list-row__body">
<div class="list-row__title cn">「3 月购入《陶庵梦忆》校注本(中华书局)。」</div>
<div class="list-row__sub">青梧 · v1 · 来源:对话 · 实录</div>
</div>
<button class="btn btn--ghost btn--sm">正影</button>
<button class="btn btn--ghost btn--sm">归档</button>
</div>
<div class="list-row">
<span class="mem-type" style="flex:none">事件</span>
<div class="list-row__body">
<div class="list-row__title cn">「读书会成立于 2024-03,成员 6 人。」</div>
<div class="list-row__sub">青梧 · v1 · 来源:世谱引用 · 删除将传播到向量与图投影</div>
</div>
<button class="btn btn--ghost btn--sm">正影</button>
<button class="btn btn--ghost btn--sm">归档</button>
</div>
</div>
<p style="font-size:12px;color:var(--text-3);margin-top:10px">MemoryVersion 不可变:纠正与取代都写新版本,旧版本保留可查;删除建墓碑且传播到索引、缓存与派生数据,墓碑不保存原文。</p>
</section>
<!-- 玄络预览 -->
<section class="section">
<div class="section__head">
<span class="cn">玄络<span class="cn__plain">关系图谱投影 · 可重建</span></span>
<span class="more" style="color:var(--text-3);cursor:default">玄络 · 可交互图谱在 React 阶段实现</span>
</div>
<div class="card graph-box">
<svg viewBox="0 0 760 190" style="width:100%;height:auto" role="img" aria-label="玄络示意:云舟与青梧、读书会、张岱作品、雪袖的关系图">
<g font-size="12" font-family="inherit">
<line x1="380" y1="95" x2="200" y2="45" stroke="var(--line)" stroke-width="1.2"/>
<line x1="380" y1="95" x2="560" y2="45" stroke="var(--line)" stroke-width="1.2"/>
<line x1="380" y1="95" x2="180" y2="150" stroke="var(--line)" stroke-width="1.2"/>
<line x1="380" y1="95" x2="590" y2="150" stroke="var(--line)" stroke-width="1.2"/>
<line class="tent-edge" x1="560" y1="45" x2="590" y2="150" stroke="var(--cinnabar)" stroke-width="1.4" stroke-dasharray="5 5"/>
<circle cx="380" cy="95" r="26" fill="var(--accent)" fill-opacity=".18" stroke="var(--accent)"/>
<text x="380" y="99" text-anchor="middle" fill="var(--text-1)" font-weight="600">云舟</text>
<circle cx="200" cy="45" r="26" fill="var(--bg-raised)" stroke="var(--line)"/>
<text x="200" y="49" text-anchor="middle" fill="var(--text-1)">青梧</text>
<circle cx="560" cy="45" r="26" fill="var(--bg-raised)" stroke="var(--line)"/>
<text x="560" y="49" text-anchor="middle" fill="var(--text-1)">雪袖</text>
<circle cx="180" cy="150" r="30" fill="var(--bg-raised)" stroke="var(--line)"/>
<text x="180" y="147" text-anchor="middle" fill="var(--text-2)">读书会</text>
<text x="180" y="161" text-anchor="middle" fill="var(--text-2)" font-size="10">世谱</text>
<circle cx="590" cy="150" r="30" fill="var(--bg-raised)" stroke="var(--line)"/>
<text x="590" y="147" text-anchor="middle" fill="var(--text-2)">陶庵梦忆</text>
<text x="590" y="161" text-anchor="middle" fill="var(--text-2)" font-size="10">世界知识</text>
<text x="300" y="62" text-anchor="middle" fill="var(--text-2)" font-size="10.5">缘契 · 共读者 v4</text>
<text x="472" y="62" text-anchor="middle" fill="var(--text-2)" font-size="10.5">缘契 · 茶友 v1</text>
<text x="268" y="133" text-anchor="middle" fill="var(--text-2)" font-size="10.5">成员 · 发起人</text>
<text x="496" y="133" text-anchor="middle" fill="var(--text-2)" font-size="10.5">正在共读</text>
<text x="594" y="103" text-anchor="middle" fill="var(--cinnabar)" font-size="10">待验</text>
</g>
</svg>
</div>
<p style="font-size:12px;color:var(--text-3);margin-top:10px">玄络是可重建投影:删除与权限变化随宿主重建;跨用户、跨角色召回恒为 0(fail-closed 已验证)。</p>
</section>
</div>
</main>
</div>
<script src="assets/app.js?v=3"></script>
</body>
</html>
+124
View File
@@ -0,0 +1,124 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>神识阁 · 模型与路由 — 魂镜 Hunjing</title>
<link rel="stylesheet" href="assets/app.css?v=10">
</head>
<body>
<svg style="display:none" aria-hidden="true"><symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol><symbol id="i-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></symbol><symbol id="i-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></symbol><symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol><symbol id="i-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></symbol><symbol id="i-layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></symbol><symbol id="i-kanban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3h4v18H5z"/><path d="M10 3h4v12h-4z"/><path d="M15 3h4v8h-4z"/></symbol><symbol id="i-package" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></symbol><symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></symbol><symbol id="i-route" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="19" r="3"/><circle cx="18" cy="5" r="3"/><path d="M12 19h4.5a3.5 3.5 0 0 0 0-7h-9a3.5 3.5 0 0 1 0-7H12"/></symbol><symbol id="i-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></symbol><symbol id="i-bag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></symbol><symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></symbol><symbol id="i-monitor" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></symbol><symbol id="i-sliders" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></symbol><symbol id="i-key" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></symbol><symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></symbol><symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></symbol><symbol id="i-check" vieLine truncated
</svg>
<div class="app">
<aside class="sidebar">
<div class="brand">
<div class="brand__mark" aria-hidden="true"></div>
<div class="brand__text">
<div class="brand__name">魂镜</div>
<div class="brand__sub">Hunjing</div>
</div>
</div>
<nav class="side-nav" aria-label="主导航"><div class="nav-group"><div class="nav-group__title">常用</div><a class="nav-item" href="home.html"><svg><use href="#i-home"/></svg><span class="cn">镜庭</span><span class="nav-plain">首页</span></a><a class="nav-item" href="conversation.html"><svg><use href="#i-branch"/></svg><span class="cn">镜树</span><span class="nav-plain">对话</span></a><a class="nav-item" href="characters.html"><svg><use href="#i-user"/></svg><span class="cn">镜生</span><span class="nav-plain">角色</span></a><a class="nav-item" href="memory.html"><svg><use href="#i-book"/></svg><span class="cn">息壤</span><span class="nav-plain">记忆</span></a><a class="nav-item" href="avatar.html"><svg><use href="#i-layers"/></svg><span class="cn">形仪</span><span class="nav-plain">形象</span></a></div><div class="nav-group"><div class="nav-group__title">创造</div><a class="nav-item" href="agent-workspace.html"><svg><use href="#i-kanban"/></svg><span class="cn">天工阁</span><span class="nav-plain">任务</span></a><a class="nav-item" href="plugins.html"><svg><use href="#i-package"/></svg><span class="cn">百工坊</span><span class="nav-plain">插件</span></a><a class="nav-item" href="collaboration.html"><svg><use href="#i-users"/></svg><span class="cn">同契台</span><span class="nav-plain">协作</span></a><a class="nav-item is-active" aria-current="page" href="model-gateway.html"><svg><use href="#i-route"/></svg><span class="cn">神识阁</span><span class="nav-plain">模型</span></a></div><div class="nav-group"><div class="nav-group__title">世界</div><a class="nav-item" href="community.html"><svg><use href="#i-globe"/></svg><span class="cn">雅集</span><span class="nav-plain">社区</span></a><a class="nav-item" href="marketplace.html"><svg><use href="#i-bag"/></svg><span class="cn">云市</span><span class="nav-plain">市场</span></a></div><div class="nav-group"><div class="nav-group__title">治理与账户</div><a class="nav-item" href="governance.html"><svg><use href="#i-shield"/></svg><span class="cn">司命台</span><span class="nav-plain">治理</span></a><a class="nav-item" href="devices.html"><svg><use href="#i-monitor"/></svg><span class="cn">器谱</span><span class="nav-plain">设备</span></a><a class="nav-item" href="settings.html"><svg><use href="#i-sliders"/></svg><span class="cn">设置</span><span class="nav-plain">账户</span></a></div></nav>
<div class="side-foot">
<button class="icon-btn side-fold-btn" data-sidebar-fold aria-label="收起侧栏" title="收起 / 展开侧栏(记忆偏好)"><svg><use href="#i-chevrons"/></svg></button>
<span class="avatar avatar--sm avatar--user">舟</span>
<span style="font-size:12.5px">云舟</span>
<button class="icon-btn" data-theme-toggle style="margin-left:auto" aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</div>
</aside>
<main class="main">
<header class="topbar">
<div class="crumbs"><span class="cn">神识阁</span><span class="crumbs__sep">·</span><span class="here">模型与路由</span></div>
<div class="topbar__spacer"></div>
<span class="prototype-tag">静态原型 · 待审查</span>
<button class="icon-btn" data-theme-toggle aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</header>
<div class="content">
<div class="realm-banner" role="note">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
神识是角色的可替换推理能力,不等于角色身份或意识证明。首个可见输出后不切换 Provider;引擎缺失时拒绝执行(fail closed)。
</div>
<section class="section">
<div class="section__head"><span class="cn">逻辑模型<span class="cn__plain">能力目录</span></span><span class="more" style="color:var(--text-3);cursor:default">全部 9 个 · 列表即目录</span></div>
<div class="grid-cards">
<div class="card" style="gap:8px;display:flex;flex-direction:column">
<div style="display:flex;align-items:center;gap:8px"><b style="font-family:var(--font-serif);font-size:15px">沉潜 · 深思</b><span class="badge badge--ok" style="margin-left:auto"><span class="badge__dot"></span>可用</span></div>
<div style="font-size:12px;color:var(--text-3)">推理 · 128k 上下文 · 函数调用</div>
<div style="font-size:12.5px;color:var(--text-2)">¥12.00 / ¥48.00<small style="color:var(--text-3)"> 每 1M token(入/出)</small></div>
<div style="font-size:11.5px;color:var(--text-3)">价格快照 v2026-09-01 · 2 个 Endpoint · 故障转移已配置</div>
</div>
<div class="card" style="gap:8px;display:flex;flex-direction:column">
<div style="display:flex;align-items:center;gap:8px"><b style="font-family:var(--font-serif);font-size:15px">惊鸿 · 快速</b><span class="badge badge--ok" style="margin-left:auto"><span class="badge__dot"></span>可用</span></div>
<div style="font-size:12px;color:var(--text-3)">轻量 · 32k · 视觉</div>
<div style="font-size:12.5px;color:var(--text-2)">¥1.60 / ¥6.40<small style="color:var(--text-3)"> 每 1M token</small></div>
<div style="font-size:11.5px;color:var(--text-3)">会话临时选择的默认候选</div>
</div>
<div class="card" style="gap:8px;display:flex;flex-direction:column">
<div style="display:flex;align-items:center;gap:8px"><b style="font-family:var(--font-serif);font-size:15px">丹青 · 图像</b><span class="badge badge--warn" style="margin-left:auto"><span class="badge__dot"></span>降级中</span></div>
<div style="font-size:12px;color:var(--text-3)">文生图 / 图生图 · 角色一致性</div>
<div style="font-size:12.5px;color:var(--text-2)">¥0.24<small style="color:var(--text-3)"> / 张</small></div>
<div style="font-size:11.5px;color:var(--text-3)">主 Endpoint 限流 · 已切备用 · 新任务排队 ≤ 2 分钟</div>
</div>
</div>
</section>
<section class="section card">
<div class="card__head"><span class="cn">路由继承链<span class="cn__plain">上层不可被下层覆盖</span></span></div>
<div style="display:flex;flex-direction:column;gap:0">
<div class="set-row"><div class="set-row__body"><div class="set-row__t">① 平台硬边界</div><div class="set-row__d">禁用模型与绝对上限,任何下层不可逾越</div></div><span class="badge badge--ok"><span class="badge__dot"></span>生效</span></div>
<div class="set-row"><div class="set-row__body"><div class="set-row__t">② 平台目录</div><div class="set-row__d">默认逻辑模型与价格快照</div></div><span class="badge badge--ok"><span class="badge__dot"></span>生效</span></div>
<div class="set-row"><div class="set-row__body"><div class="set-row__t">③ 用户偏好 / BYOK</div><div class="set-row__d">你的默认模型与自带密钥</div></div><span class="badge badge--ok"><span class="badge__dot"></span>生效</span></div>
<div class="set-row"><div class="set-row__body"><div class="set-row__t">④ 角色覆写</div><div class="set-row__d">青梧未设置覆写</div></div><span class="badge badge--muted"><span class="badge__dot"></span>未设置</span></div>
<div class="set-row" style="background:var(--accent-soft);border-radius:var(--radius-sm);padding-left:12px;padding-right:12px"><div class="set-row__body"><div class="set-row__t">⑤ 会话临时选择 <span class="cn__plain">当前生效</span></div><div class="set-row__d">镜树「共读选篇」选择了 惊鸿 · 快速</div></div><span class="badge badge--info"><span class="badge__dot"></span>生效 <span class="mono">session</span></span></div>
</div>
</section>
<div class="two-col">
<section class="section card">
<div class="card__head"><span class="cn">BYOK<span class="cn__plain">自带模型密钥</span></span><button class="btn btn--outline btn--sm" style="margin-left:auto"><svg><use href="#i-plus"/></svg>添加密钥</button></div>
<div class="set-row">
<svg style="width:16px;height:16px;color:var(--text-3)"><use href="#i-key"/></svg>
<div class="set-row__body"><div class="set-row__t mono">byok_a31f…(神思 Provider)</div><div class="set-row__d">2026-08-12 添加 · 存于系统 Keychain · 不写配置与日志</div></div>
<button class="btn btn--ghost btn--sm">轮换</button>
</div>
<div class="set-row">
<svg style="width:16px;height:16px;color:var(--text-3)"><use href="#i-key"/></svg>
<div class="set-row__body"><div class="set-row__t mono">byok_7c02…(图像 Provider)</div><div class="set-row__d">2026-09-01 添加 · 仅丹青 · 图像使用</div></div>
<button class="btn btn--ghost btn--sm">轮换</button>
</div>
<p style="font-size:12px;color:var(--text-3);margin-top:10px">BYOK 调用按实际用量计费一次,不重复计费(ADR-021);凭据只经 Header 传递,永不转发至上游之外的日志。</p>
</section>
<section class="section card">
<div class="card__head"><span class="cn">本月用量<span class="cn__plain">9 月 1–15 日</span></span><span class="more" style="color:var(--text-3);cursor:default">明细 · Run 级观测见司命台诊断</span></div>
<div class="bars" role="img" aria-label="近七日 token 用量条形图:周三最高 8.4 万">
<div class="bars__col"><div class="bars__bar" style="height:42%"></div><span class="bars__label">一</span></div>
<div class="bars__col"><div class="bars__bar" style="height:55%"></div><span class="bars__label">二</span></div>
<div class="bars__col"><div class="bars__bar" style="height:96%"></div><span class="bars__label">三</span></div>
<div class="bars__col"><div class="bars__bar" style="height:48%"></div><span class="bars__label">四</span></div>
<div class="bars__col"><div class="bars__bar" style="height:30%"></div><span class="bars__label">五</span></div>
<div class="bars__col"><div class="bars__bar" style="height:22%"></div><span class="bars__label">六</span></div>
<div class="bars__col"><div class="bars__bar" style="height:36%"></div><span class="bars__label">日</span></div>
</div>
<div class="table-wrap" style="margin-top:14px"><table class="data">
<thead><tr><th>指标</th><th>数值</th></tr></thead>
<tbody>
<tr><td>Token(入/出)</td><td class="mono">486k / 212k</td></tr>
<tr><td>Prompt Cache 命中</td><td class="mono">31%</td></tr>
<tr><td>P95 TTFT</td><td class="mono">1.4s</td></tr>
<tr><td>费用</td><td class="mono">¥18.64</td></tr>
<tr><td>错误 / 重试</td><td class="mono">2 / 1</td></tr>
<tr><td>未知 usage 记录</td><td class="mono">null(不猜测)</td></tr>
</tbody>
</table></div>
</section>
</div>
</div>
</main>
</div>
<script src="assets/app.js?v=3"></script>
</body>
</html>
+115
View File
@@ -0,0 +1,115 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>百工坊 · 插件与能力 — 魂镜 Hunjing</title>
<link rel="stylesheet" href="assets/app.css?v=10">
</head>
<body>
<svg style="display:none" aria-hidden="true"><symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol><symbol id="i-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></symbol><symbol id="i-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></symbol><symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol><symbol id="i-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></symbol><symbol id="i-layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></symbol><symbol id="i-kanban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3h4v18H5z"/><path d="M10 3h4v12h-4z"/><path d="M15 3h4v8h-4z"/></symbol><symbol id="i-package" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></symbol><symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></symbol><symbol id="i-route" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="19" r="3"/><circle cx="18" cy="5" r="3"/><path d="M12 19h4.5a3.5 3.5 0 0 0 0-7h-9a3.5 3.5 0 0 1 0-7H12"/></symbol><symbol id="i-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></symbol><symbol id="i-bag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></symbol><symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></symbol><symbol id="i-monitor" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></symbol><symbol id="i-sliders" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></symbol><symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></symbol><symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></symbol><symbol id="i-refresh" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></symbol><symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecapLine truncated
</svg>
<div class="app">
<aside class="sidebar">
<div class="brand">
<div class="brand__mark" aria-hidden="true"></div>
<div class="brand__text">
<div class="brand__name">魂镜</div>
<div class="brand__sub">Hunjing</div>
</div>
</div>
<nav class="side-nav" aria-label="主导航"><div class="nav-group"><div class="nav-group__title">常用</div><a class="nav-item" href="home.html"><svg><use href="#i-home"/></svg><span class="cn">镜庭</span><span class="nav-plain">首页</span></a><a class="nav-item" href="conversation.html"><svg><use href="#i-branch"/></svg><span class="cn">镜树</span><span class="nav-plain">对话</span></a><a class="nav-item" href="characters.html"><svg><use href="#i-user"/></svg><span class="cn">镜生</span><span class="nav-plain">角色</span></a><a class="nav-item" href="memory.html"><svg><use href="#i-book"/></svg><span class="cn">息壤</span><span class="nav-plain">记忆</span></a><a class="nav-item" href="avatar.html"><svg><use href="#i-layers"/></svg><span class="cn">形仪</span><span class="nav-plain">形象</span></a></div><div class="nav-group"><div class="nav-group__title">创造</div><a class="nav-item" href="agent-workspace.html"><svg><use href="#i-kanban"/></svg><span class="cn">天工阁</span><span class="nav-plain">任务</span></a><a class="nav-item is-active" aria-current="page" href="plugins.html"><svg><use href="#i-package"/></svg><span class="cn">百工坊</span><span class="nav-plain">插件</span></a><a class="nav-item" href="collaboration.html"><svg><use href="#i-users"/></svg><span class="cn">同契台</span><span class="nav-plain">协作</span></a><a class="nav-item" href="model-gateway.html"><svg><use href="#i-route"/></svg><span class="cn">神识阁</span><span class="nav-plain">模型</span></a></div><div class="nav-group"><div class="nav-group__title">世界</div><a class="nav-item" href="community.html"><svg><use href="#i-globe"/></svg><span class="cn">雅集</span><span class="nav-plain">社区</span></a><a class="nav-item" href="marketplace.html"><svg><use href="#i-bag"/></svg><span class="cn">云市</span><span class="nav-plain">市场</span></a></div><div class="nav-group"><div class="nav-group__title">治理与账户</div><a class="nav-item" href="governance.html"><svg><use href="#i-shield"/></svg><span class="cn">司命台</span><span class="nav-plain">治理</span></a><a class="nav-item" href="devices.html"><svg><use href="#i-monitor"/></svg><span class="cn">器谱</span><span class="nav-plain">设备</span></a><a class="nav-item" href="settings.html"><svg><use href="#i-sliders"/></svg><span class="cn">设置</span><span class="nav-plain">账户</span></a></div></nav>
<div class="side-foot">
<button class="icon-btn side-fold-btn" data-sidebar-fold aria-label="收起侧栏" title="收起 / 展开侧栏(记忆偏好)"><svg><use href="#i-chevrons"/></svg></button>
<span class="avatar avatar--sm avatar--user">舟</span>
<span style="font-size:12.5px">云舟</span>
<button class="icon-btn" data-theme-toggle style="margin-left:auto" aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</div>
</aside>
<main class="main">
<header class="topbar">
<div class="crumbs"><span class="cn">百工坊</span><span class="crumbs__sep">·</span><span class="here">插件与能力生态</span></div>
<div class="topbar__spacer"></div>
<span class="prototype-tag">静态原型 · 待审查</span>
<button class="icon-btn" data-theme-toggle aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</header>
<div class="content">
<div class="realm-banner" role="note">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
购买不等于安装,安装不等于启用,启用不等于授予新增权限。安全内核(认证、审批、交易账、审计)不可插件化。
</div>
<section class="section card">
<div class="card__head">
<span class="cn">沙箱<span class="cn__plain">隔离运行时</span></span>
<span class="badge badge--ok" style="margin-left:auto"><span class="badge__dot"></span>Worker 运转中 <span class="mono">sandbox:ok</span></span>
</div>
<p style="font-size:12.5px;color:var(--text-2)">本地代码插件运行于独立进程容器;声明式 / 远程插件默认受限。无符合策略的隔离后端时拒绝执行,不回退宿主(fail closed)。</p>
</section>
<section class="section">
<div class="section__head"><span class="cn">已安装<span class="cn__plain">4 个</span></span></div>
<div class="card" style="padding:6px 18px">
<div class="set-row">
<span class="badge badge--info" style="flex:none"><span class="badge__dot"></span>Core</span>
<div class="set-row__body"><div class="set-row__t">系统日历 <span class="cn__plain">bundled · 主进程</span></div><div class="set-row__d">仅 Core 插件可进主进程 · 权限:日历读写(每次请命)</div></div>
<span class="switch"><input type="checkbox" checked aria-label="系统日历已启用"><i></i></span>
</div>
<div class="set-row">
<span class="badge badge--muted" style="flex:none"><span class="badge__dot"></span>声明式</span>
<div class="set-row__body"><div class="set-row__t">笔记同步 <span class="cn__plain">受限运行时</span></div><div class="set-row__d">权限:笔记库 /读书会/ 读写 · 网络:无</div></div>
<span class="switch"><input type="checkbox" checked aria-label="笔记同步已启用"><i></i></span>
</div>
<div class="set-row" style="border-color:color-mix(in srgb,var(--warn) 35%,transparent)">
<span class="badge badge--warn" style="flex:none"><span class="badge__dot"></span>待更新</span>
<div class="set-row__body"><div class="set-row__t">性灵检索 <span class="cn__plain">远程 API</span></div><div class="set-row__d">v1.2 → v1.3 · 新增权限需重新确认(见下方)</div></div>
<button class="btn btn--outline btn--sm">查看变更</button>
</div>
<div class="set-row">
<span class="badge badge--muted" style="flex:none"><span class="badge__dot"></span>本地代码</span>
<div class="set-row__body"><div class="set-row__t">代码执行沙箱 <span class="cn__plain">独立容器 · 部署者信任</span></div><div class="set-row__d">Run 固定插件快照 · 活动运行不热替换</div></div>
<span class="switch"><input type="checkbox" checked aria-label="代码执行沙箱已启用"><i></i></span>
</div>
</div>
</section>
<section class="section">
<div class="section__head"><span class="cn">升级确认<span class="cn__plain">性灵检索 v1.3 新增权限</span></span></div>
<div class="block approval" style="max-width:640px">
<div class="approval__head">
<span class="approval__seal" aria-hidden="true">命</span>
<div>
<div class="approval__title"><span class="cn">请命</span><span class="cn__plain">升级新增权限必须重新确认</span></div>
<div style="font-size:11.5px;color:var(--text-3)">不确认则保留 v1.2,不影响现有使用</div>
</div>
</div>
<div class="approval__body">
<div class="fact"><span class="fact__k">现有 v1.2</span><span class="fact__v">读取笔记元数据 · 网络 api.xingling.app(只读)</span></div>
<div class="fact"><span class="fact__k">新增 v1.3</span><span class="fact__v" style="color:var(--cinnabar)">写入笔记内容 · 网络 api.xingling.app(读写)· 存储最近 20 条查询</span></div>
<div class="fact"><span class="fact__k">费用</span><span class="fact__v">免费插件 · 每次外部检索计费 ¥0.02</span></div>
<div class="fact"><span class="fact__k">撤架</span><span class="fact__v">安全撤架阻断新调用,保留历史审计</span></div>
</div>
<div class="approval__acts">
<button class="btn btn--cinnabar" aria-label="确认决定:允许更新">允许更新(朱批)</button>
<button class="btn btn--outline">保留 v1.2</button>
<span class="approval__note">工契 · 插件清单完整权限对比见详情。</span>
</div>
</div>
</section>
<section class="section">
<div class="section__head"><span class="cn">授契<span class="cn__plain">能力授予记录</span></span><a class="more" href="#">司命台完整审计 →</a></div>
<div class="card" style="padding:6px 18px">
<div class="list-row"><div class="list-row__body"><div class="list-row__title">日历写入 · 青梧</div><div class="list-row__sub">用途:共读事件 · 范围:仅指定事件 · 期限:单次 · 可随时撤销</div></div><button class="btn btn--ghost btn--sm">撤销</button></div>
<div class="list-row"><div class="list-row__body"><div class="list-row__title">笔记库读写 · 青梧</div><div class="list-row__sub">行契 · 30 天内同类免请命 · 剩余 12 天 · 超限后重新请命</div></div><button class="btn btn--ghost btn--sm">撤销</button></div>
<div class="list-row"><div class="list-row__body"><div class="list-row__title">网络检索 · 任务 Agent</div><div class="list-row__sub">用途:资料校对 · 白名单 3 域 · 期限:本 Run</div></div><span style="font-size:11.5px;color:var(--text-3)">随 Run 结束失效</span></div>
</div>
</section>
</div>
</main>
</div>
<script src="assets/app.js?v=3"></script>
</body>
</html>
+108
View File
@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>魂镜 · 创建账户</title>
<link rel="stylesheet" href="assets/app.css?v=10">
</head>
<body>
<svg style="display:none" aria-hidden="true">
<symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol>
</svg>
<div class="stage">
<div class="stage__bg" aria-hidden="true">
<div class="stage__rings">
<svg viewBox="0 0 800 800">
<defs><path id="ringpath" d="M400,400 m-300,0 a300,300 0 1,1 600,0 a300,300 0 1,1 -600,0"/></defs>
<circle class="ring-ink" cx="400" cy="400" r="392"/>
<circle class="ring-faint" cx="400" cy="400" r="336"/>
<g class="ring-slow"><circle class="ring-faint" cx="400" cy="400" r="272"/></g>
<circle class="ring-ink" cx="400" cy="400" r="206"/>
<circle class="ring-faint" cx="400" cy="400" r="158"/>
<g class="ring-slow" style="animation-direction:reverse"><circle class="ring-faint" cx="400" cy="400" r="104"/></g>
<text fill="var(--gilt-faint)" font-size="12" letter-spacing="6" style="font-family:var(--font-serif)">
<textPath href="#ringpath">幻由心生 · 镜随心转 · 一念起 · 万镜生 · 且向镜中看 · 那个是谁 ·</textPath>
</text>
</svg>
</div>
<svg style="position:absolute;inset:0;width:100%;height:100%;opacity:.035" aria-hidden="true"><filter id="grain"><feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="2"/></filter><rect width="100%" height="100%" filter="url(#grain)"/></svg>
</div>
<header class="stage__topbar">
<a href="login.html" style="font-size:12px;color:var(--text-3)">← 返回登录</a>
<span style="font-size:12px;color:var(--text-3)">静态原型 · 待审查</span>
<span class="spacer"></span>
<a href="index.html" style="font-size:12px;color:var(--text-3)">原型目录</a>
<button class="icon-btn" data-theme-toggle aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</header>
<main class="stage__main">
<div class="mirror-card">
<div class="mirror-card__brand">
<div class="brand__mark" aria-hidden="true"></div>
<div>
<div style="font-family:var(--font-serif);font-size:20px;font-weight:600;line-height:1.2">魂镜</div>
<div style="font-size:11px;color:var(--text-3)">Hunjing</div>
</div>
</div>
<div class="mirror-card__title">
<h1>创建账户</h1>
</div>
<p class="mirror-card__sub">化名与法定身份分离核验;随时可导出、撤权或删除</p>
<form onsubmit="return false">
<div class="field-group">
<label for="reg-email">邮箱</label>
<input class="input" id="reg-email" type="email" autocomplete="email" placeholder="you@example.com">
</div>
<div class="field-group">
<label for="reg-code">验证码</label>
<div class="field-row">
<input class="input" id="reg-code" type="text" inputmode="numeric" placeholder="6 位验证码">
<button class="btn btn--outline" type="button">发送验证码</button>
</div>
</div>
<div class="field-group">
<label for="reg-pwd">密码</label>
<input class="input" id="reg-pwd" type="password" autocomplete="new-password" placeholder="至少 12 位,建议通行密钥">
</div>
<div class="field-group">
<label for="reg-pwd2">确认密码</label>
<input class="input" id="reg-pwd2" type="password" autocomplete="new-password" placeholder="再次输入">
</div>
<div style="margin:18px 0 16px;border-top:1px solid var(--line-soft);padding-top:16px">
<div style="font-size:12.5px;color:var(--text-2);margin-bottom:10px">开始前请知悉(数据权):</div>
<div class="check-row">
<input type="checkbox" id="ck-train" checked disabled aria-label="默认不用于模型训练">
<label class="t" for="ck-train"><b>默认不用于模型训练</b> —— 你的个人域数据默认不参与任何模型训练;自愿贡献是独立开关,可随时撤销。</label>
</div>
<div class="check-row">
<input type="checkbox" id="ck-export" checked disabled aria-label="可导出与删除">
<label class="t" for="ck-export"><b>可导出、可删除</b> —— 角色、记忆、对话与来源谱系随时可携导出;删除有冷静期并传播到全部派生数据。</label>
</div>
<div class="check-row">
<input type="checkbox" id="ck-vault" aria-label="了解私箧规则">
<label class="t" for="ck-vault"><b>私箧独立持钥</b> —— 加密私密区只有你能解开;平台无法恢复,钥匙丢失即永久不可读取。</label>
</div>
<div class="check-row">
<input type="checkbox" id="ck-terms" aria-label="同意服务条款">
<label class="t" for="ck-terms"><b>我已阅读并同意服务条款与社区规范</b> —— 角色不拥有生物意识或法律人格;社区有举报与治理机制。</label>
</div>
</div>
<a class="btn btn--cinnabar" href="login.html" style="width:100%">创建账户</a>
</form>
<p class="login-note">注册即创建你的镜域(个人数据域);角色、记忆与授权都归属镜域治理。</p>
<div class="login-foot">
已有账户?<a href="login.html">登入</a>
</div>
</div>
</main>
</div>
<script src="assets/app.js?v=3"></script>
</body>
</html>
+121
View File
@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>设置 · 账户与偏好 — 魂镜 Hunjing</title>
<link rel="stylesheet" href="assets/app.css?v=10">
</head>
<body>
<svg style="display:none" aria-hidden="true"><symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol><symbol id="i-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></symbol><symbol id="i-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></symbol><symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol><symbol id="i-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></symbol><symbol id="i-layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></symbol><symbol id="i-kanban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3h4v18H5z"/><path d="M10 3h4v12h-4z"/><path d="M15 3h4v8h-4z"/></symbol><symbol id="i-package" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></symbol><symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></symbol><symbol id="i-route" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="19" r="3"/><circle cx="18" cy="5" r="3"/><path d="M12 19h4.5a3.5 3.5 0 0 0 0-7h-9a3.5 3.5 0 0 1 0-7H12"/></symbol><symbol id="i-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></symbol><symbol id="i-bag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></symbol><symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></symbol><symbol id="i-monitor" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></symbol><symbol id="i-sliders" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></symbol><symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></symbol><symbol id="i-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></symbol><symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><paLine truncated
</svg>
<div class="app">
<aside class="sidebar">
<div class="brand">
<div class="brand__mark" aria-hidden="true"></div>
<div class="brand__text">
<div class="brand__name">魂镜</div>
<div class="brand__sub">Hunjing</div>
</div>
</div>
<nav class="side-nav" aria-label="主导航"><div class="nav-group"><div class="nav-group__title">常用</div><a class="nav-item" href="home.html"><svg><use href="#i-home"/></svg><span class="cn">镜庭</span><span class="nav-plain">首页</span></a><a class="nav-item" href="conversation.html"><svg><use href="#i-branch"/></svg><span class="cn">镜树</span><span class="nav-plain">对话</span></a><a class="nav-item" href="characters.html"><svg><use href="#i-user"/></svg><span class="cn">镜生</span><span class="nav-plain">角色</span></a><a class="nav-item" href="memory.html"><svg><use href="#i-book"/></svg><span class="cn">息壤</span><span class="nav-plain">记忆</span></a><a class="nav-item" href="avatar.html"><svg><use href="#i-layers"/></svg><span class="cn">形仪</span><span class="nav-plain">形象</span></a></div><div class="nav-group"><div class="nav-group__title">创造</div><a class="nav-item" href="agent-workspace.html"><svg><use href="#i-kanban"/></svg><span class="cn">天工阁</span><span class="nav-plain">任务</span></a><a class="nav-item" href="plugins.html"><svg><use href="#i-package"/></svg><span class="cn">百工坊</span><span class="nav-plain">插件</span></a><a class="nav-item" href="collaboration.html"><svg><use href="#i-users"/></svg><span class="cn">同契台</span><span class="nav-plain">协作</span></a><a class="nav-item" href="model-gateway.html"><svg><use href="#i-route"/></svg><span class="cn">神识阁</span><span class="nav-plain">模型</span></a></div><div class="nav-group"><div class="nav-group__title">世界</div><a class="nav-item" href="community.html"><svg><use href="#i-globe"/></svg><span class="cn">雅集</span><span class="nav-plain">社区</span></a><a class="nav-item" href="marketplace.html"><svg><use href="#i-bag"/></svg><span class="cn">云市</span><span class="nav-plain">市场</span></a></div><div class="nav-group"><div class="nav-group__title">治理与账户</div><a class="nav-item" href="governance.html"><svg><use href="#i-shield"/></svg><span class="cn">司命台</span><span class="nav-plain">治理</span></a><a class="nav-item" href="devices.html"><svg><use href="#i-monitor"/></svg><span class="cn">器谱</span><span class="nav-plain">设备</span></a><a class="nav-item is-active" aria-current="page" href="settings.html"><svg><use href="#i-sliders"/></svg><span class="cn">设置</span><span class="nav-plain">账户</span></a></div></nav>
<div class="side-foot">
<button class="icon-btn side-fold-btn" data-sidebar-fold aria-label="收起侧栏" title="收起 / 展开侧栏(记忆偏好)"><svg><use href="#i-chevrons"/></svg></button>
<span class="avatar avatar--sm avatar--user">舟</span>
<span style="font-size:12.5px">云舟</span>
<button class="icon-btn" data-theme-toggle style="margin-left:auto" aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</div>
</aside>
<main class="main">
<header class="topbar">
<div class="crumbs"><span class="cn">设置</span><span class="crumbs__sep">·</span><span class="here">账户与偏好</span></div>
<div class="topbar__spacer"></div>
<span class="prototype-tag">静态原型 · 待审查</span>
<button class="icon-btn" data-theme-toggle aria-label="切换到月白主题"><svg><use href="#i-moon"/></svg></button>
</header>
<div class="content">
<section class="section card">
<div class="card__head"><span class="cn">账户</span></div>
<div style="display:flex;gap:14px;align-items:center;flex-wrap:wrap">
<span class="avatar avatar--lg avatar--user">舟</span>
<div>
<div style="font-size:16px" class="cn">云舟<span class="cn__plain">前台化名 · 与法定身份分离核验</span></div>
<div style="font-size:12.5px;color:var(--text-3);margin-top:2px">yunzhou@example.com · OAuth 2.1 · Web 安全会话</div>
</div>
<div style="margin-left:auto;display:flex;gap:10px">
<button class="btn btn--outline btn--sm">多凭证管理</button>
<button class="btn btn--outline btn--sm">修改化名</button>
</div>
</div>
</section>
<section class="section card">
<div class="card__head"><span class="cn">会话</span></div>
<div class="set-row">
<svg style="width:16px;height:16px;color:var(--text-3)"><use href="#i-monitor"/></svg>
<div class="set-row__body"><div class="set-row__t">MacBook Pro · Tauri 桌面</div><div class="set-row__d">PKCE 登录 · 当前会话</div></div>
<span class="badge badge--ok" style="font-size:11px"><span class="badge__dot"></span>当前</span>
</div>
<div class="set-row">
<svg style="width:16px;height:16px;color:var(--text-3)"><use href="#i-monitor"/></svg>
<div class="set-row__body"><div class="set-row__t">iPhone · 移动端</div><div class="set-row__d">PKCE 登录 · 昨天活跃</div></div>
<button class="btn btn--ghost btn--sm">撤销</button>
</div>
</section>
<section class="section card" style="border-color:color-mix(in srgb,var(--accent) 35%,transparent)">
<div class="card__head"><span class="cn">私箧<span class="cn__plain">独立持钥的加密私密区</span></span><span class="badge badge--ok" style="margin-left:auto"><span class="badge__dot"></span>已启用 <span class="mono">locked</span></span></div>
<div class="set-row">
<svg style="width:16px;height:16px;color:var(--text-3)"><use href="#i-lock"/></svg>
<div class="set-row__body"><div class="set-row__t">平台仅存密文,不可恢复</div><div class="set-row__d">账户恢复、管理员与诊断均不可读取;恢复钥匙只在你手中,丢失即永久不可恢复。</div></div>
</div>
<div class="set-row">
<svg style="width:16px;height:16px;color:var(--text-3)"><use href="#i-file"/></svg>
<div class="set-row__body"><div class="set-row__t">使用量 12 MB</div><div class="set-row__d">日记 41 篇 · 手记 7 份 · 不参与任何推荐与训练</div></div>
<button class="btn btn--outline btn--sm">打开私箧</button>
</div>
</section>
<section class="section card">
<div class="card__head"><span class="cn">数据权<span class="cn__plain">访问 · 纠正 · 导出 · 撤权 · 删除</span></span></div>
<div class="set-row">
<div class="set-row__body">
<div class="set-row__t">模型训练贡献</div>
<div class="set-row__d">Personal Domain 数据默认不用于模型训练。自愿贡献需独立、明确、可随时撤销的同意。</div>
</div>
<span class="switch"><input type="checkbox" aria-label="自愿贡献训练数据(默认关闭)"><i></i></span>
</div>
<div class="set-row">
<div class="set-row__body"><div class="set-row__t">导出全部数据</div><div class="set-row__d">角色、设定、记忆、对话、资产与来源谱系 · 魂籍可携格式</div></div>
<button class="btn btn--outline btn--sm"><svg><use href="#i-download"/></svg>导出</button>
</div>
<div class="set-row">
<div class="set-row__body"><div class="set-row__t">撤回全部授权</div><div class="set-row__d">撤销插件、设备与第三方访问授权,数据保留</div></div>
<button class="btn btn--outline btn--sm">撤权</button>
</div>
<div class="set-row">
<div class="set-row__body"><div class="set-row__t" style="color:var(--cinnabar)">删除账户与全部数据</div><div class="set-row__d">不可逆 · 有冷静期 · 与角色死亡是两个独立操作 · 删除传播到索引与派生数据</div></div>
<button class="btn btn--danger btn--sm"><svg><use href="#i-trash"/></svg>删除</button>
</div>
</section>
<section class="section card">
<div class="card__head"><span class="cn">偏好</span></div>
<div class="set-row"><div class="set-row__body"><div class="set-row__t">界面主题</div><div class="set-row__d">墨(深色)/ 月白(浅色)· 跟随系统</div></div><span class="switch"><input type="checkbox" aria-label="深色主题"><i></i></span></div>
<div class="set-row"><div class="set-row__body"><div class="set-row__t">界面语言</div><div class="set-row__d">简体中文 · 文化名始终附带直白解释</div></div><button class="btn btn--ghost btn--sm">切换</button></div>
<div class="set-row"><div class="set-row__body"><div class="set-row__t">界面直白解释</div><div class="set-row__d">关闭后仅在高风险界面显示直白解释(不建议)</div></div><span class="switch"><input type="checkbox" checked aria-label="始终显示直白解释"><i></i></span></div>
</section>
<section class="section card danger-zone">
<div class="card__head"><span class="cn">封镜<span class="cn__plain">账户暂时停用 · 不是角色静修、归藏或死亡</span></span></div>
<p style="font-size:12.5px;color:var(--text-2);margin-bottom:12px">封镜期间登录被冻结,角色进入静修,数据完整保留;解封后可复照重开。</p>
<button class="btn btn--danger btn--sm">封镜账户</button>
</section>
</div>
</main>
</div>
<script src="assets/app.js?v=3"></script>
</body>
</html>
+17
View File
@@ -0,0 +1,17 @@
# Hunjing backend 镜像(uv + slim)
FROM python:3.12-slim AS base
WORKDIR /app
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
# 先装依赖(层缓存)
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-install-project
# 源码与引擎绑定(绑定随源构建)
COPY . .
RUN uv sync --frozen
EXPOSE 8000
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
+37
View File
@@ -0,0 +1,37 @@
# 自托管编排(标准服务器档,ADR-024 统一 PostgreSQL)
# 用法:docker compose -f docker/docker-compose.app.yml up -d --build
services:
hunjing-postgres:
image: pgvector/pgvector:pg17
container_name: hunjing-postgres
restart: unless-stopped
environment:
POSTGRES_USER: hunjing
POSTGRES_PASSWORD: ${HUNJING_DB_PASSWORD:?请在 .env 设置 HUNJING_DB_PASSWORD}
POSTGRES_DB: hunjing
volumes:
- hunjing-pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U hunjing -d hunjing"]
interval: 5s
timeout: 3s
retries: 12
hunjing-api:
build:
context: ../backend
dockerfile: ../docker/Dockerfile.backend
container_name: hunjing-api
restart: unless-stopped
depends_on:
hunjing-postgres:
condition: service_healthy
environment:
DATABASE_URL: postgresql+asyncpg://hunjing:${HUNJING_DB_PASSWORD}@hunjing-postgres:5432/hunjing
HUNJING_ENV: production
HUNJING_LOG_LEVEL: info
ports:
- "127.0.0.1:8000:8000"
volumes:
hunjing-pgdata:
+4 -3
View File
@@ -17,14 +17,14 @@ endif()
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)
add_library(hunjing SHARED src/lingtai.cpp src/transform.cpp)
add_library(hunjing SHARED src/lingtai.cpp src/transform.cpp src/modules.cpp)
target_include_directories(hunjing
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
# 单元测试(doctest,ADR-025);单头直接引入,不用 FetchContent 拉网络
add_executable(lingtai_tests tests/test_main.cpp tests/test_core.cpp tests/test_transform.cpp
src/lingtai.cpp src/transform.cpp)
tests/test_modules.cpp src/lingtai.cpp src/transform.cpp src/modules.cpp)
target_include_directories(lingtai_tests PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/src
@@ -36,7 +36,8 @@ add_test(NAME lingtai_tests COMMAND lingtai_tests)
# fuzz 目标(ADR-025:hj_transform 为必测目标;需要 clang libFuzzer)
option(HJ_BUILD_FUZZER "Build libFuzzer targets" OFF)
if(HJ_BUILD_FUZZER)
add_executable(fuzz_transform fuzz/fuzz_transform.cpp src/lingtai.cpp src/transform.cpp)
add_executable(fuzz_transform fuzz/fuzz_transform.cpp src/lingtai.cpp src/transform.cpp
src/modules.cpp)
target_include_directories(fuzz_transform PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/src)
+6
View File
@@ -0,0 +1,6 @@
module hunjing/lingtai
go 1.23
// 引擎源码经本地相对路径编译(cgo 自动发现包目录下 .cpp 文件的规则不适用,
// 改由 lingtai.go 的 #cgo 指令与同一包内的 .cpp 文件列表驱动)。
@@ -0,0 +1,4 @@
package lingtai
// 引擎源码直接编译(cgo 把 .cpp 交给 C++ 编译器)。
// 本文件仅承载构建期声明注释,无运行时代码。
+1
View File
@@ -0,0 +1 @@
/Users/chenxin/code/python/hunjing/engine/include/hunjing
+153
View File
@@ -0,0 +1,153 @@
// Package lingtai 是灵台引擎的官方 Go 绑定(cgo,ADR-016/017)。
//
// 自包含编译引擎源码;薄封装:仅类型转换与错误映射(契约 lingtai-ffi 第 8 节)。
package lingtai
/*
#cgo CXXFLAGS: -std=c++23 -O2
#cgo CFLAGS: -I${SRCDIR}/../../include
#include <stdlib.h>
#include "hunjing/lingtai.h"
// C 桥接(C++ 编译单元在下方 sources 中声明为 C++,由 cgo 链接 C++ 编译器)
typedef int (*unused_cb)();
*/
import "C"
import (
"fmt"
"unsafe"
)
// 直接编译引擎源码(cgo 自动以 C++ 处理 .cpp 文件)
// 注:通过 #cgo LDFLAGS/CXXFLAGS 与文件列表在同目录 cxx_sources.go 中声明。
// Engine 是线程安全的引擎句柄(RAII)。
type Engine struct {
ptr *C.HjEngine
}
// New 创建引擎;ABI major 不匹配返回错误(fail-fast,不静默降级)。
func New() (*Engine, error) {
var ptr *C.HjEngine
st := C.hj_engine_new(C.HJ_ABI_VERSION, &ptr)
if st != C.HJ_OK {
return nil, fmt.Errorf("lingtai: engine init failed: %s", C.GoString(C.hj_last_error()))
}
return &Engine{ptr: ptr}, nil
}
// Close 释放引擎(幂等安全:nil 检查)。
func (e *Engine) Close() error {
if e == nil || e.ptr == nil {
return nil
}
st := C.hj_engine_free(e.ptr)
e.ptr = nil
if st != C.HJ_OK {
return fmt.Errorf("lingtai: engine free: %s", C.GoString(C.hj_last_error()))
}
return nil
}
func (e *Engine) valid() error {
if e == nil || e.ptr == nil {
return fmt.Errorf("lingtai: engine handle is nil")
}
return nil
}
// TransformVersion 返回 Markdown 解析器版本。
func TransformVersion() string { return C.GoString(C.hj_transform_version()) }
// TransformMarkdown 将 Markdown 规范化为 canonical ContentBlock JSON。
func (e *Engine) TransformMarkdown(input string) (string, error) {
if err := e.valid(); err != nil {
return "", err
}
cin := C.CString(input)
defer C.free(unsafe.Pointer(cin))
var buf *C.HjBuffer
st := C.hj_transform_markdown(e.ptr, cin, C.size_t(len(input)), &buf)
if st != C.HJ_OK {
return "", fmt.Errorf("lingtai: transform: %s", C.GoString(C.hj_last_error()))
}
defer C.hj_buffer_free(buf)
return C.GoStringN((*C.char)(C.hj_buffer_data(buf)), C.int(C.hj_buffer_len(buf))), nil
}
// ValidateURL 校验 URL 协议(仅 http/https)。
func ValidateURL(url string) error {
cu := C.CString(url)
defer C.free(unsafe.Pointer(cu))
if st := C.hj_policy_validate_url(cu); st != C.HJ_OK {
return fmt.Errorf("lingtai: url denied: %s", C.GoString(C.hj_last_error()))
}
return nil
}
// ValidatePath 校验路径在授权根内(拒绝 .. 与越界)。
func ValidatePath(path, root string) error {
cp := C.CString(path)
cr := C.CString(root)
defer C.free(unsafe.Pointer(cp))
defer C.free(unsafe.Pointer(cr))
if st := C.hj_policy_validate_path(cp, cr); st != C.HJ_OK {
return fmt.Errorf("lingtai: path denied: %s", C.GoString(C.hj_last_error()))
}
return nil
}
// PolicyDecision 是四维裁决结果。
type PolicyDecision int
const (
DecisionAllow PolicyDecision = 0
DecisionAsk PolicyDecision = 1
DecisionDeny PolicyDecision = 2
)
// Autonomy levels(契约 autonomy_level)。
const (
AutonomyIntrospective = 0
AutonomyRuleBound = 1
AutonomyApprovalReq = 2
AutonomyForbidden = 3
)
// Risk levels。
const (
RiskReadOnly = 0
RiskSideEffect = 1
RiskHighImpact = 2
)
// Decide 运行决策表(autonomy × risk → allow/ask/deny;非法输入 deny)。
func Decide(autonomy, risk int) PolicyDecision {
return PolicyDecision(C.hj_policy_decide(C.int(autonomy), C.int(risk)))
}
// CheckBudget 校验预算(used >= max 返回错误)。
func CheckBudget(used, maximum int64) error {
if st := C.hj_plan_check_budget(C.int64_t(used), C.int64_t(maximum)); st != C.HJ_OK {
return fmt.Errorf("lingtai: budget: %s", C.GoString(C.hj_last_error()))
}
return nil
}
// RecallRerank 对候选做确定性重排,返回 canonical JSON。
func (e *Engine) RecallRerank(candidatesJSON string) (string, error) {
if err := e.valid(); err != nil {
return "", err
}
cj := C.CString(candidatesJSON)
defer C.free(unsafe.Pointer(cj))
var buf *C.HjBuffer
st := C.hj_recall_rerank(cj, &buf)
if st != C.HJ_OK {
return "", fmt.Errorf("lingtai: rerank: %s", C.GoString(C.hj_last_error()))
}
defer C.hj_buffer_free(buf)
return C.GoStringN((*C.char)(C.hj_buffer_data(buf)), C.int(C.hj_buffer_len(buf))), nil
}
@@ -0,0 +1,63 @@
package lingtai
import "testing"
func TestEngineLifecycleAndTransform(t *testing.T) {
e, err := New()
if err != nil {
t.Fatal(err)
}
defer e.Close()
if TransformVersion() != "hunjing-markdown-v1" {
t.Fatalf("parser version: %s", TransformVersion())
}
out, err := e.TransformMarkdown("x\n```mermaid\nA-->B\n```\n$$\nz\n$$\n")
if err != nil {
t.Fatal(err)
}
want := `{"parser_version":"hunjing-markdown-v1","blocks":[{"type":"markdown","text":"x","flavor":"hunjing-gfm-v1","status":"completed"},{"type":"mermaid","source":"A-->B","status":"completed"},{"type":"math","notation":"latex","display":"block","source":"z","status":"completed"}]}`
if out != want {
t.Fatalf("golden mismatch:\n got: %s\nwant: %s", out, want)
}
}
func TestPolicyPrimitives(t *testing.T) {
if err := ValidateURL("https://ok.example"); err != nil {
t.Fatal(err)
}
if err := ValidateURL("javascript:alert(1)"); err == nil {
t.Fatal("javascript: 应被拒绝")
}
if err := ValidatePath("/data/d/f", "/data/d"); err != nil {
t.Fatal(err)
}
if err := ValidatePath("/data/d/../x", "/data/d"); err == nil {
t.Fatal(".. 应被拒绝")
}
if d := Decide(AutonomyRuleBound, RiskReadOnly); d != DecisionAllow {
t.Fatalf("循律+只读应 allow: %d", d)
}
if d := Decide(AutonomyIntrospective, RiskHighImpact); d != DecisionDeny {
t.Fatalf("自省+高影响应 deny: %d", d)
}
if err := CheckBudget(10, 10); err == nil {
t.Fatal("预算达限应报错")
}
}
func TestRecallRerankDeterministic(t *testing.T) {
e, _ := New()
defer e.Close()
in := `{"query_terms":["魂镜"],"candidates":[{"id":"a","text":"魂镜 系统"},{"id":"b","text":"无"}]}`
out1, err := e.RecallRerank(in)
if err != nil {
t.Fatal(err)
}
out2, _ := e.RecallRerank(in)
if out1 != out2 {
t.Fatalf("确定性失败: %s vs %s", out1, out2)
}
if out1 != `{"ranked":[{"id":"a","score":10},{"id":"b","score":0}]}` {
t.Fatalf("golden mismatch: %s", out1)
}
}
+228
View File
@@ -0,0 +1,228 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
dependencies = [
"memchr",
]
[[package]]
name = "bindgen"
version = "0.71.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn",
]
[[package]]
name = "bitflags"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clang-sys"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "either"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
[[package]]
name = "glob"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
[[package]]
name = "hunjing-lingtai"
version = "0.1.0"
dependencies = [
"bindgen",
]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "log"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rustc-hash"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "hunjing-lingtai"
version = "0.1.0"
edition = "2021"
description = "灵台引擎官方 Rust 绑定(ADR-016/017)"
license = "Apache-2.0"
[lib]
path = "src/lib.rs"
[build-dependencies]
bindgen = "0.71"
+56
View File
@@ -0,0 +1,56 @@
use std::env;
use std::path::PathBuf;
fn main() {
let header = PathBuf::from("../../include/hunjing/lingtai.h")
.canonicalize()
.expect("找不到引擎公共头");
let bindings = bindgen::Builder::default()
.header(header.to_string_lossy())
.allowlist_function("hj_.*")
.allowlist_type("Hj.*")
.allowlist_var("HJ_.*")
.generate()
.expect("bindgen 生成失败(需要 libclang)");
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_dir.join("bindings.rs"))
.expect("写 bindings.rs 失败");
println!("cargo:rerun-if-changed={}", header.display());
// 直接编译引擎源码(自包含)
let src = PathBuf::from("../../src").canonicalize().expect("找不到引擎源码");
for cpp in ["lingtai.cpp", "transform.cpp", "modules.cpp"] {
println!("cargo:rustc-cxx=g++");
let _ = cpp;
}
// 用 cc crate 风格手动调用:为最小依赖,此处直接输出编译指令给 rustc 链接预编译静态库
// 简化:编译为单个静态库由 build.rs 调用 clang++
// 逐个编译 .o,再归档为静态库
let objects: Vec<_> = ["lingtai.cpp", "transform.cpp", "modules.cpp"]
.iter()
.map(|cpp| {
let obj = out_dir.join(format!("{}.o", cpp.replace(".cpp", "")));
let status = std::process::Command::new("clang++")
.args(["-std=c++23", "-O2", "-c"])
.arg(src.join(cpp))
.arg("-I").arg(src.parent().unwrap().join("include"))
.arg("-I").arg(&src)
.arg("-fvisibility=hidden")
.arg("-o").arg(&obj)
.status()
.expect("clang++ 启动失败");
assert!(status.success(), "引擎编译失败:{cpp}");
obj
})
.collect();
let out_lib = out_dir.join("libhunjing_engine.a");
let status = std::process::Command::new("ar")
.arg("crus").arg(&out_lib)
.args(&objects)
.status()
.expect("ar 启动失败");
assert!(status.success(), "归档失败");
println!("cargo:rustc-link-search=native={}", out_dir.display());
println!("cargo:rustc-link-lib=static=hunjing_engine");
println!("cargo:rustc-link-lib=dylib=c++");
}
+158
View File
@@ -0,0 +1,158 @@
//! 灵台引擎官方 Rust 绑定(bindgen 生成 + 安全包装,ADR-016/017)。
//! 薄封装:句柄 RAII、错误转 `Result`;禁止裸指针外泄(契约 lingtai-ffi 第 8 节)。
#![allow(clippy::missing_safety_doc)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
use std::ffi::{c_char, c_void, CStr, CString};
use std::fmt;
#[derive(Debug)]
pub enum Error {
InvalidArgument(String),
OutOfMemory,
UnsupportedAbi,
HandleInvalid,
ParseFailed(String),
LimitExceeded(String),
PolicyDenied(String),
Internal(String),
Unknown(i32, String),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::InvalidArgument(m) => write!(f, "参数不合法:{m}"),
Error::OutOfMemory => write!(f, "内存分配失败"),
Error::UnsupportedAbi => write!(f, "ABI major 不匹配"),
Error::HandleInvalid => write!(f, "句柄无效"),
Error::ParseFailed(m) => write!(f, "解析失败:{m}"),
Error::LimitExceeded(m) => write!(f, "超限:{m}"),
Error::PolicyDenied(m) => write!(f, "策略拒绝:{m}"),
Error::Internal(m) => write!(f, "内部错误:{m}"),
Error::Unknown(code, m) => write!(f, "未知错误 {code}:{m}"),
}
}
}
impl std::error::Error for Error {}
fn map_status(st: HjStatus, message: String) -> Result<(), Error> {
match st {
HjStatus_HJ_OK => Ok(()),
HjStatus_HJ_ERR_INVALID_ARGUMENT => Err(Error::InvalidArgument(message)),
HjStatus_HJ_ERR_OUT_OF_MEMORY => Err(Error::OutOfMemory),
HjStatus_HJ_ERR_UNSUPPORTED_ABI => Err(Error::UnsupportedAbi),
HjStatus_HJ_ERR_HANDLE_INVALID => Err(Error::HandleInvalid),
HjStatus_HJ_ERR_PARSE_FAILED => Err(Error::ParseFailed(message)),
HjStatus_HJ_ERR_LIMIT_EXCEEDED => Err(Error::LimitExceeded(message)),
HjStatus_HJ_ERR_POLICY_DENIED => Err(Error::PolicyDenied(message)),
HjStatus_HJ_ERR_INTERNAL => Err(Error::Internal(message)),
other => Err(Error::Unknown(other as i32, message)),
}
}
fn last_error() -> String {
unsafe {
let ptr = hj_last_error();
if ptr.is_null() {
String::new()
} else {
CStr::from_ptr(ptr).to_string_lossy().into_owned()
}
}
}
/// 引擎句柄(RAII:Drop 自动释放)。
pub struct Engine {
ptr: *mut HjEngine,
}
impl Engine {
pub fn new() -> Result<Self, Error> {
let mut ptr: *mut HjEngine = std::ptr::null_mut();
let st = unsafe { hj_engine_new(HJ_ABI_VERSION, &mut ptr) };
map_status(st, last_error())?;
Ok(Engine { ptr })
}
/// Markdown → canonical ContentBlock JSON。
pub fn transform_markdown(&self, input: &str) -> Result<String, Error> {
let cin = CString::new(input).map_err(|_| Error::InvalidArgument("含 NUL".into()))?;
let mut buf: *mut HjBuffer = std::ptr::null_mut();
let st = unsafe {
hj_transform_markdown(self.ptr, cin.as_ptr(), cin.as_bytes().len(), &mut buf)
};
map_status(st, last_error())?;
take_buffer(buf)
}
/// 候选重排(确定性)。
pub fn recall_rerank(&self, candidates_json: &str) -> Result<String, Error> {
let cin =
CString::new(candidates_json).map_err(|_| Error::InvalidArgument("含 NUL".into()))?;
let mut buf: *mut HjBuffer = std::ptr::null_mut();
let st = unsafe { hj_recall_rerank(cin.as_ptr(), &mut buf) };
map_status(st, last_error())?;
take_buffer(buf)
}
}
impl Drop for Engine {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe { hj_engine_free(self.ptr) };
}
}
}
// Send/Sync:HjEngine 线程安全(契约第 4 节)
unsafe impl Send for Engine {}
unsafe impl Sync for Engine {}
fn take_buffer(buf: *mut HjBuffer) -> Result<String, Error> {
if buf.is_null() {
return Err(Error::Internal("缓冲为空".into()));
}
unsafe {
let data = hj_buffer_data(buf) as *const c_char;
let len = hj_buffer_len(buf);
let slice = std::slice::from_raw_parts(data as *const u8, len);
let out = String::from_utf8_lossy(slice).into_owned();
hj_buffer_free(buf);
Ok(out)
}
}
/// URL 协议校验(仅 http/https)。
pub fn validate_url(url: &str) -> Result<(), Error> {
let cu = CString::new(url).map_err(|_| Error::InvalidArgument("含 NUL".into()))?;
let st = unsafe { hj_policy_validate_url(cu.as_ptr()) };
map_status(st, last_error())
}
/// 路径约束(授权根内、拒绝 ..)。
pub fn validate_path(path: &str, root: &str) -> Result<(), Error> {
let cp = CString::new(path).map_err(|_| Error::InvalidArgument("含 NUL".into()))?;
let cr = CString::new(root).map_err(|_| Error::InvalidArgument("含 NUL".into()))?;
let st = unsafe { hj_policy_validate_path(cp.as_ptr(), cr.as_ptr()) };
map_status(st, last_error())
}
/// 决策表(契约 autonomy_level × risk)。
pub fn decide(autonomy: i32, risk: i32) -> i32 {
unsafe { hj_policy_decide(autonomy, risk) }
}
/// 预算核算。
pub fn check_budget(used: i64, maximum: i64) -> Result<(), Error> {
let st = unsafe { hj_plan_check_budget(used, maximum) };
map_status(st, last_error())
}
/// 解析器版本。
pub fn transform_version() -> String {
unsafe { CStr::from_ptr(hj_transform_version()).to_string_lossy().into_owned() }
}
+33
View File
@@ -0,0 +1,33 @@
use hunjing_lingtai::{decide, check_budget, transform_version, validate_path, validate_url, Engine};
#[test]
fn lifecycle_and_transform_golden() {
assert_eq!(transform_version(), "hunjing-markdown-v1");
let engine = Engine::new().expect("引擎初始化");
let out = engine
.transform_markdown("x\n```mermaid\nA-->B\n```\n$$\nz\n$$\n")
.expect("transform");
let want = r#"{"parser_version":"hunjing-markdown-v1","blocks":[{"type":"markdown","text":"x","flavor":"hunjing-gfm-v1","status":"completed"},{"type":"mermaid","source":"A-->B","status":"completed"},{"type":"math","notation":"latex","display":"block","source":"z","status":"completed"}]}"#;
assert_eq!(out, want);
}
#[test]
fn policy_primitives() {
validate_url("https://ok.example").unwrap();
assert!(validate_url("javascript:alert(1)").is_err());
validate_path("/data/d/f", "/data/d").unwrap();
assert!(validate_path("/data/d/../x", "/data/d").is_err());
assert_eq!(decide(1, 0), 0); // 循律+只读 = allow
assert_eq!(decide(0, 2), 2); // 自省+高影响 = deny
assert!(check_budget(10, 10).is_err());
}
#[test]
fn recall_rerank_deterministic() {
let engine = Engine::new().unwrap();
let input = r#"{"query_terms":["魂镜"],"candidates":[{"id":"a","text":"魂镜 系统"},{"id":"b","text":"无"}]}"#;
let out1 = engine.recall_rerank(input).unwrap();
let out2 = engine.recall_rerank(input).unwrap();
assert_eq!(out1, out2);
assert_eq!(out1, r#"{"ranked":[{"id":"a","score":10},{"id":"b","score":0}]}"#);
}
+37
View File
@@ -76,6 +76,43 @@ HJ_API const char *hj_transform_version(void);
HJ_API HjStatus hj_transform_markdown(
HjEngine *engine, const char *input, size_t input_len, HjBuffer **out);
// ===== 小周天(hj_stream):确定性流事件编码 =====
// 流会话句柄:Run 内 sequence 与 block delta_index 计数(契约第 5 节)。
typedef struct HjStream HjStream;
HJ_API HjStatus hj_stream_new(HjEngine *engine, HjStream **out_stream);
HJ_API HjStatus hj_stream_free(HjStream *stream);
// 追加文本增量:输出 content.block.delta envelope 的 canonical JSON
// (含 sequence 与 delta_index,键序确定);输出经 HjBuffer 返回。
HJ_API HjStatus hj_stream_append_delta(
HjStream *stream, const char *block_id, const char *text, size_t text_len,
HjBuffer **out);
HJ_API uint64_t hj_stream_sequence(const HjStream *stream);
// ===== 烛照(hj_policy):确定性判定原语 =====
// URL 校验:仅 http/https;拒绝 file/javascript/data 等协议(契约 3.4)。
HJ_API HjStatus hj_policy_validate_url(const char *url);
// 路径约束:canonicalize 后必须位于 root 内;越界返回 HJ_ERR_POLICY_DENIED。
HJ_API HjStatus hj_policy_validate_path(const char *path, const char *root);
// 幂等键:非空且不超过 128 字符(契约第 6 节)。
HJ_API HjStatus hj_policy_validate_idempotency_key(const char *key);
// 决策表:autonomy(0=自省 1=循律 2=请命 3=禁域)× risk(0=只读 1=副作用 2=高影响)
// 返回 0=allow 1=ask 2=deny;非法输入返回 2。
HJ_API int hj_policy_decide(int autonomy_level, int risk_level);
// ===== 洛书(hj_plan):预算核算 =====
// 已用 >= 上限 返回 HJ_ERR_LIMIT_EXCEEDED;负数参数 HJ_ERR_INVALID_ARGUMENT。
HJ_API HjStatus hj_plan_check_budget(int64_t used, int64_t maximum);
// ===== 河图(hj_recall):确定性重排 =====
// 候选重排:输入 {"query_terms": [".."], "candidates": [{"id","text"}]}
// 输出按确定性得分(词频 × 简单位置权重)排序的 canonical JSON。
HJ_API HjStatus hj_recall_rerank(const char *candidates_json, HjBuffer **out);
#ifdef __cplusplus
} // extern "C"
#endif
+127
View File
@@ -0,0 +1,127 @@
syntax = "proto3";
package liushen.v1;
// 灵台引擎独立服务形态(契约 docs/contracts/lingtai-service.md)。
// 部署内部网络专用;错误映射见契约第 6 节。字段语义与进程内 C ABI JSON 边界同源。
service LingtaiEngine {
// 版本、ABI major、parser version、能力位
rpc GetEngineInfo(GetEngineInfoRequest) returns (GetEngineInfoResponse);
// Markdown → canonical ContentBlock JSON
rpc TransformMarkdown(TransformMarkdownRequest) returns (TransformMarkdownResponse);
// canonical Block → 可移植 Markdown 重建(预留:实现随 WP-03 导出)
rpc RebuildExport(RebuildExportRequest) returns (RebuildExportResponse);
// 双向流:宿主喂增量,服务回 content.block.* envelope JSON
rpc OpenStream(stream StreamFrame) returns (stream StreamEvent);
// 计划 DAG 预算核算(最小:标量预算)
rpc CheckBudget(CheckBudgetRequest) returns (CheckBudgetResponse);
// 候选确定性重排
rpc RerankRecall(RerankRecallRequest) returns (RerankRecallResponse);
// 四维裁决(autonomy × risk)
rpc EvaluatePolicy(EvaluatePolicyRequest) returns (EvaluatePolicyResponse);
// URL 协议白名单校验
rpc ValidateUrl(ValidateUrlRequest) returns (ValidateUrlResponse);
// Run/Turn 状态转换合法性
rpc CheckStateTransition(CheckStateTransitionRequest) returns (CheckStateTransitionResponse);
}
message GetEngineInfoRequest {}
message GetEngineInfoResponse {
uint32 abi_major = 1;
string parser_version = 2;
string engine_version = 3;
repeated string capabilities = 4;
}
message TransformMarkdownRequest {
string markdown = 1;
}
message TransformMarkdownResponse {
string canonical_json = 1; // canonical ContentBlock JSON(键序确定)
}
message RebuildExportRequest {
string canonical_json = 1;
}
message RebuildExportResponse {
string markdown = 1;
}
message StreamFrame {
oneof frame {
string block_started = 2; // block_id:开启新 block(重置 delta_index)
string text_delta = 3; // 文本增量
}
}
message StreamEvent {
string event_json = 1; // content.block.* envelope(canonical JSON)
}
message CheckBudgetRequest {
int64 used = 1;
int64 maximum = 2;
}
message CheckBudgetResponse {
bool allowed = 1;
string reason = 2;
}
message RerankRecallRequest {
string candidates_json = 1;
}
message RerankRecallResponse {
string ranked_json = 1;
}
message EvaluatePolicyRequest {
int32 autonomy_level = 1; // 0 自省 / 1 循律 / 2 请命 / 3 禁域
int32 risk_level = 2; // 0 只读 / 1 副作用 / 2 高影响
}
message EvaluatePolicyResponse {
int32 decision = 1; // 0 allow / 1 ask / 2 deny
}
message ValidateUrlRequest {
string url = 1;
}
message ValidateUrlResponse {
bool allowed = 1;
string reason = 2;
}
enum RunStatus {
RUN_STATUS_UNSPECIFIED = 0;
RUN_STATUS_QUEUED = 1;
RUN_STATUS_RUNNING = 2;
RUN_STATUS_WAITING_APPROVAL = 3;
RUN_STATUS_COMPLETED = 4;
RUN_STATUS_FAILED = 5;
RUN_STATUS_CANCELLED = 6;
}
message CheckStateTransitionRequest {
RunStatus from = 1;
RunStatus to = 2;
}
message CheckStateTransitionResponse {
bool valid = 1;
string reason = 2;
}
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: liushen/v1/liushen.proto
# Protobuf Python Version: 7.35.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
7,
35,
1,
'',
'liushen/v1/liushen.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18liushen/v1/liushen.proto\x12\nliushen.v1\"\x16\n\x14GetEngineInfoRequest\"p\n\x15GetEngineInfoResponse\x12\x11\n\tabi_major\x18\x01 \x01(\r\x12\x16\n\x0eparser_version\x18\x02 \x01(\t\x12\x16\n\x0e\x65ngine_version\x18\x03 \x01(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x04 \x03(\t\",\n\x18TransformMarkdownRequest\x12\x10\n\x08markdown\x18\x01 \x01(\t\"3\n\x19TransformMarkdownResponse\x12\x16\n\x0e\x63\x61nonical_json\x18\x01 \x01(\t\".\n\x14RebuildExportRequest\x12\x16\n\x0e\x63\x61nonical_json\x18\x01 \x01(\t\")\n\x15RebuildExportResponse\x12\x10\n\x08markdown\x18\x01 \x01(\t\"E\n\x0bStreamFrame\x12\x17\n\rblock_started\x18\x02 \x01(\tH\x00\x12\x14\n\ntext_delta\x18\x03 \x01(\tH\x00\x42\x07\n\x05\x66rame\"!\n\x0bStreamEvent\x12\x12\n\nevent_json\x18\x01 \x01(\t\"3\n\x12\x43heckBudgetRequest\x12\x0c\n\x04used\x18\x01 \x01(\x03\x12\x0f\n\x07maximum\x18\x02 \x01(\x03\"6\n\x13\x43heckBudgetResponse\x12\x0f\n\x07\x61llowed\x18\x01 \x01(\x08\x12\x0e\n\x06reason\x18\x02 \x01(\t\".\n\x13RerankRecallRequest\x12\x17\n\x0f\x63\x61ndidates_json\x18\x01 \x01(\t\"+\n\x14RerankRecallResponse\x12\x13\n\x0branked_json\x18\x01 \x01(\t\"C\n\x15\x45valuatePolicyRequest\x12\x16\n\x0e\x61utonomy_level\x18\x01 \x01(\x05\x12\x12\n\nrisk_level\x18\x02 \x01(\x05\"*\n\x16\x45valuatePolicyResponse\x12\x10\n\x08\x64\x65\x63ision\x18\x01 \x01(\x05\"!\n\x12ValidateUrlRequest\x12\x0b\n\x03url\x18\x01 \x01(\t\"6\n\x13ValidateUrlResponse\x12\x0f\n\x07\x61llowed\x18\x01 \x01(\x08\x12\x0e\n\x06reason\x18\x02 \x01(\t\"e\n\x1b\x43heckStateTransitionRequest\x12#\n\x04\x66rom\x18\x01 \x01(\x0e\x32\x15.liushen.v1.RunStatus\x12!\n\x02to\x18\x02 \x01(\x0e\x32\x15.liushen.v1.RunStatus\"=\n\x1c\x43heckStateTransitionResponse\x12\r\n\x05valid\x18\x01 \x01(\x08\x12\x0e\n\x06reason\x18\x02 \x01(\t*\xc2\x01\n\tRunStatus\x12\x1a\n\x16RUN_STATUS_UNSPECIFIED\x10\x00\x12\x15\n\x11RUN_STATUS_QUEUED\x10\x01\x12\x16\n\x12RUN_STATUS_RUNNING\x10\x02\x12\x1f\n\x1bRUN_STATUS_WAITING_APPROVAL\x10\x03\x12\x18\n\x14RUN_STATUS_COMPLETED\x10\x04\x12\x15\n\x11RUN_STATUS_FAILED\x10\x05\x12\x18\n\x14RUN_STATUS_CANCELLED\x10\x06\x32\x98\x06\n\rLingtaiEngine\x12T\n\rGetEngineInfo\x12 .liushen.v1.GetEngineInfoRequest\x1a!.liushen.v1.GetEngineInfoResponse\x12`\n\x11TransformMarkdown\x12$.liushen.v1.TransformMarkdownRequest\x1a%.liushen.v1.TransformMarkdownResponse\x12T\n\rRebuildExport\x12 .liushen.v1.RebuildExportRequest\x1a!.liushen.v1.RebuildExportResponse\x12\x42\n\nOpenStream\x12\x17.liushen.v1.StreamFrame\x1a\x17.liushen.v1.StreamEvent(\x01\x30\x01\x12N\n\x0b\x43heckBudget\x12\x1e.liushen.v1.CheckBudgetRequest\x1a\x1f.liushen.v1.CheckBudgetResponse\x12Q\n\x0cRerankRecall\x12\x1f.liushen.v1.RerankRecallRequest\x1a .liushen.v1.RerankRecallResponse\x12W\n\x0e\x45valuatePolicy\x12!.liushen.v1.EvaluatePolicyRequest\x1a\".liushen.v1.EvaluatePolicyResponse\x12N\n\x0bValidateUrl\x12\x1e.liushen.v1.ValidateUrlRequest\x1a\x1f.liushen.v1.ValidateUrlResponse\x12i\n\x14\x43heckStateTransition\x12\'.liushen.v1.CheckStateTransitionRequest\x1a(.liushen.v1.CheckStateTransitionResponseb\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'liushen.v1.liushen_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_RUNSTATUS']._serialized_start=1047
_globals['_RUNSTATUS']._serialized_end=1241
_globals['_GETENGINEINFOREQUEST']._serialized_start=40
_globals['_GETENGINEINFOREQUEST']._serialized_end=62
_globals['_GETENGINEINFORESPONSE']._serialized_start=64
_globals['_GETENGINEINFORESPONSE']._serialized_end=176
_globals['_TRANSFORMMARKDOWNREQUEST']._serialized_start=178
_globals['_TRANSFORMMARKDOWNREQUEST']._serialized_end=222
_globals['_TRANSFORMMARKDOWNRESPONSE']._serialized_start=224
_globals['_TRANSFORMMARKDOWNRESPONSE']._serialized_end=275
_globals['_REBUILDEXPORTREQUEST']._serialized_start=277
_globals['_REBUILDEXPORTREQUEST']._serialized_end=323
_globals['_REBUILDEXPORTRESPONSE']._serialized_start=325
_globals['_REBUILDEXPORTRESPONSE']._serialized_end=366
_globals['_STREAMFRAME']._serialized_start=368
_globals['_STREAMFRAME']._serialized_end=437
_globals['_STREAMEVENT']._serialized_start=439
_globals['_STREAMEVENT']._serialized_end=472
_globals['_CHECKBUDGETREQUEST']._serialized_start=474
_globals['_CHECKBUDGETREQUEST']._serialized_end=525
_globals['_CHECKBUDGETRESPONSE']._serialized_start=527
_globals['_CHECKBUDGETRESPONSE']._serialized_end=581
_globals['_RERANKRECALLREQUEST']._serialized_start=583
_globals['_RERANKRECALLREQUEST']._serialized_end=629
_globals['_RERANKRECALLRESPONSE']._serialized_start=631
_globals['_RERANKRECALLRESPONSE']._serialized_end=674
_globals['_EVALUATEPOLICYREQUEST']._serialized_start=676
_globals['_EVALUATEPOLICYREQUEST']._serialized_end=743
_globals['_EVALUATEPOLICYRESPONSE']._serialized_start=745
_globals['_EVALUATEPOLICYRESPONSE']._serialized_end=787
_globals['_VALIDATEURLREQUEST']._serialized_start=789
_globals['_VALIDATEURLREQUEST']._serialized_end=822
_globals['_VALIDATEURLRESPONSE']._serialized_start=824
_globals['_VALIDATEURLRESPONSE']._serialized_end=878
_globals['_CHECKSTATETRANSITIONREQUEST']._serialized_start=880
_globals['_CHECKSTATETRANSITIONREQUEST']._serialized_end=981
_globals['_CHECKSTATETRANSITIONRESPONSE']._serialized_start=983
_globals['_CHECKSTATETRANSITIONRESPONSE']._serialized_end=1044
_globals['_LINGTAIENGINE']._serialized_start=1244
_globals['_LINGTAIENGINE']._serialized_end=2036
# @@protoc_insertion_point(module_scope)
@@ -0,0 +1,459 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import warnings
from liushen.v1 import liushen_pb2 as liushen_dot_v1_dot_liushen__pb2
GRPC_GENERATED_VERSION = '1.83.1'
GRPC_VERSION = grpc.__version__
_version_not_supported = False
try:
from grpc._utilities import first_version_is_lower
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
except ImportError:
_version_not_supported = True
if _version_not_supported:
raise RuntimeError(
f'The grpc package installed is at version {GRPC_VERSION},'
+ ' but the generated code in liushen/v1/liushen_pb2_grpc.py depends on'
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
)
class LingtaiEngineStub:
"""灵台引擎独立服务形态(契约 docs/contracts/lingtai-service.md)。
部署内部网络专用;错误映射见契约第 6 节。字段语义与进程内 C ABI JSON 边界同源。
"""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.GetEngineInfo = channel.unary_unary(
'/liushen.v1.LingtaiEngine/GetEngineInfo',
request_serializer=liushen_dot_v1_dot_liushen__pb2.GetEngineInfoRequest.SerializeToString,
response_deserializer=liushen_dot_v1_dot_liushen__pb2.GetEngineInfoResponse.FromString,
_registered_method=True)
self.TransformMarkdown = channel.unary_unary(
'/liushen.v1.LingtaiEngine/TransformMarkdown',
request_serializer=liushen_dot_v1_dot_liushen__pb2.TransformMarkdownRequest.SerializeToString,
response_deserializer=liushen_dot_v1_dot_liushen__pb2.TransformMarkdownResponse.FromString,
_registered_method=True)
self.RebuildExport = channel.unary_unary(
'/liushen.v1.LingtaiEngine/RebuildExport',
request_serializer=liushen_dot_v1_dot_liushen__pb2.RebuildExportRequest.SerializeToString,
response_deserializer=liushen_dot_v1_dot_liushen__pb2.RebuildExportResponse.FromString,
_registered_method=True)
self.OpenStream = channel.stream_stream(
'/liushen.v1.LingtaiEngine/OpenStream',
request_serializer=liushen_dot_v1_dot_liushen__pb2.StreamFrame.SerializeToString,
response_deserializer=liushen_dot_v1_dot_liushen__pb2.StreamEvent.FromString,
_registered_method=True)
self.CheckBudget = channel.unary_unary(
'/liushen.v1.LingtaiEngine/CheckBudget',
request_serializer=liushen_dot_v1_dot_liushen__pb2.CheckBudgetRequest.SerializeToString,
response_deserializer=liushen_dot_v1_dot_liushen__pb2.CheckBudgetResponse.FromString,
_registered_method=True)
self.RerankRecall = channel.unary_unary(
'/liushen.v1.LingtaiEngine/RerankRecall',
request_serializer=liushen_dot_v1_dot_liushen__pb2.RerankRecallRequest.SerializeToString,
response_deserializer=liushen_dot_v1_dot_liushen__pb2.RerankRecallResponse.FromString,
_registered_method=True)
self.EvaluatePolicy = channel.unary_unary(
'/liushen.v1.LingtaiEngine/EvaluatePolicy',
request_serializer=liushen_dot_v1_dot_liushen__pb2.EvaluatePolicyRequest.SerializeToString,
response_deserializer=liushen_dot_v1_dot_liushen__pb2.EvaluatePolicyResponse.FromString,
_registered_method=True)
self.ValidateUrl = channel.unary_unary(
'/liushen.v1.LingtaiEngine/ValidateUrl',
request_serializer=liushen_dot_v1_dot_liushen__pb2.ValidateUrlRequest.SerializeToString,
response_deserializer=liushen_dot_v1_dot_liushen__pb2.ValidateUrlResponse.FromString,
_registered_method=True)
self.CheckStateTransition = channel.unary_unary(
'/liushen.v1.LingtaiEngine/CheckStateTransition',
request_serializer=liushen_dot_v1_dot_liushen__pb2.CheckStateTransitionRequest.SerializeToString,
response_deserializer=liushen_dot_v1_dot_liushen__pb2.CheckStateTransitionResponse.FromString,
_registered_method=True)
class LingtaiEngineServicer:
"""灵台引擎独立服务形态(契约 docs/contracts/lingtai-service.md)。
部署内部网络专用;错误映射见契约第 6 节。字段语义与进程内 C ABI JSON 边界同源。
"""
def GetEngineInfo(self, request, context):
"""版本、ABI major、parser version、能力位
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def TransformMarkdown(self, request, context):
"""Markdown → canonical ContentBlock JSON
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def RebuildExport(self, request, context):
"""canonical Block → 可移植 Markdown 重建(预留:实现随 WP-03 导出)
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def OpenStream(self, request_iterator, context):
"""双向流:宿主喂增量,服务回 content.block.* envelope JSON
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def CheckBudget(self, request, context):
"""计划 DAG 预算核算(最小:标量预算)
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def RerankRecall(self, request, context):
"""候选确定性重排
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def EvaluatePolicy(self, request, context):
"""四维裁决(autonomy × risk)
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def ValidateUrl(self, request, context):
"""URL 协议白名单校验
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def CheckStateTransition(self, request, context):
"""Run/Turn 状态转换合法性
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_LingtaiEngineServicer_to_server(servicer, server):
rpc_method_handlers = {
'GetEngineInfo': grpc.unary_unary_rpc_method_handler(
servicer.GetEngineInfo,
request_deserializer=liushen_dot_v1_dot_liushen__pb2.GetEngineInfoRequest.FromString,
response_serializer=liushen_dot_v1_dot_liushen__pb2.GetEngineInfoResponse.SerializeToString,
),
'TransformMarkdown': grpc.unary_unary_rpc_method_handler(
servicer.TransformMarkdown,
request_deserializer=liushen_dot_v1_dot_liushen__pb2.TransformMarkdownRequest.FromString,
response_serializer=liushen_dot_v1_dot_liushen__pb2.TransformMarkdownResponse.SerializeToString,
),
'RebuildExport': grpc.unary_unary_rpc_method_handler(
servicer.RebuildExport,
request_deserializer=liushen_dot_v1_dot_liushen__pb2.RebuildExportRequest.FromString,
response_serializer=liushen_dot_v1_dot_liushen__pb2.RebuildExportResponse.SerializeToString,
),
'OpenStream': grpc.stream_stream_rpc_method_handler(
servicer.OpenStream,
request_deserializer=liushen_dot_v1_dot_liushen__pb2.StreamFrame.FromString,
response_serializer=liushen_dot_v1_dot_liushen__pb2.StreamEvent.SerializeToString,
),
'CheckBudget': grpc.unary_unary_rpc_method_handler(
servicer.CheckBudget,
request_deserializer=liushen_dot_v1_dot_liushen__pb2.CheckBudgetRequest.FromString,
response_serializer=liushen_dot_v1_dot_liushen__pb2.CheckBudgetResponse.SerializeToString,
),
'RerankRecall': grpc.unary_unary_rpc_method_handler(
servicer.RerankRecall,
request_deserializer=liushen_dot_v1_dot_liushen__pb2.RerankRecallRequest.FromString,
response_serializer=liushen_dot_v1_dot_liushen__pb2.RerankRecallResponse.SerializeToString,
),
'EvaluatePolicy': grpc.unary_unary_rpc_method_handler(
servicer.EvaluatePolicy,
request_deserializer=liushen_dot_v1_dot_liushen__pb2.EvaluatePolicyRequest.FromString,
response_serializer=liushen_dot_v1_dot_liushen__pb2.EvaluatePolicyResponse.SerializeToString,
),
'ValidateUrl': grpc.unary_unary_rpc_method_handler(
servicer.ValidateUrl,
request_deserializer=liushen_dot_v1_dot_liushen__pb2.ValidateUrlRequest.FromString,
response_serializer=liushen_dot_v1_dot_liushen__pb2.ValidateUrlResponse.SerializeToString,
),
'CheckStateTransition': grpc.unary_unary_rpc_method_handler(
servicer.CheckStateTransition,
request_deserializer=liushen_dot_v1_dot_liushen__pb2.CheckStateTransitionRequest.FromString,
response_serializer=liushen_dot_v1_dot_liushen__pb2.CheckStateTransitionResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'liushen.v1.LingtaiEngine', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
server.add_registered_method_handlers('liushen.v1.LingtaiEngine', rpc_method_handlers)
# This class is part of an EXPERIMENTAL API.
class LingtaiEngine:
"""灵台引擎独立服务形态(契约 docs/contracts/lingtai-service.md)。
部署内部网络专用;错误映射见契约第 6 节。字段语义与进程内 C ABI JSON 边界同源。
"""
@staticmethod
def GetEngineInfo(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/liushen.v1.LingtaiEngine/GetEngineInfo',
liushen_dot_v1_dot_liushen__pb2.GetEngineInfoRequest.SerializeToString,
liushen_dot_v1_dot_liushen__pb2.GetEngineInfoResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def TransformMarkdown(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/liushen.v1.LingtaiEngine/TransformMarkdown',
liushen_dot_v1_dot_liushen__pb2.TransformMarkdownRequest.SerializeToString,
liushen_dot_v1_dot_liushen__pb2.TransformMarkdownResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def RebuildExport(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/liushen.v1.LingtaiEngine/RebuildExport',
liushen_dot_v1_dot_liushen__pb2.RebuildExportRequest.SerializeToString,
liushen_dot_v1_dot_liushen__pb2.RebuildExportResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def OpenStream(request_iterator,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.stream_stream(
request_iterator,
target,
'/liushen.v1.LingtaiEngine/OpenStream',
liushen_dot_v1_dot_liushen__pb2.StreamFrame.SerializeToString,
liushen_dot_v1_dot_liushen__pb2.StreamEvent.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def CheckBudget(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/liushen.v1.LingtaiEngine/CheckBudget',
liushen_dot_v1_dot_liushen__pb2.CheckBudgetRequest.SerializeToString,
liushen_dot_v1_dot_liushen__pb2.CheckBudgetResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def RerankRecall(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/liushen.v1.LingtaiEngine/RerankRecall',
liushen_dot_v1_dot_liushen__pb2.RerankRecallRequest.SerializeToString,
liushen_dot_v1_dot_liushen__pb2.RerankRecallResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def EvaluatePolicy(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/liushen.v1.LingtaiEngine/EvaluatePolicy',
liushen_dot_v1_dot_liushen__pb2.EvaluatePolicyRequest.SerializeToString,
liushen_dot_v1_dot_liushen__pb2.EvaluatePolicyResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def ValidateUrl(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/liushen.v1.LingtaiEngine/ValidateUrl',
liushen_dot_v1_dot_liushen__pb2.ValidateUrlRequest.SerializeToString,
liushen_dot_v1_dot_liushen__pb2.ValidateUrlResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def CheckStateTransition(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/liushen.v1.LingtaiEngine/CheckStateTransition',
liushen_dot_v1_dot_liushen__pb2.CheckStateTransitionRequest.SerializeToString,
liushen_dot_v1_dot_liushen__pb2.CheckStateTransitionResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
+228
View File
@@ -0,0 +1,228 @@
"""灵台引擎 gRPC 服务端(开发档参考实现,ADR-017 双形态)。
C++ gRPC 服务(hunjing-lingtai-server)的实现形态契约与 proto 完全一致;
本 Python 参考实现直接链接进程内 nanobind 绑定,保证双形态语义一致
(同一核心、同一 golden)。正式 C++ 二进制随交叉编译矩阵交付(WP-15 CI)。
监听仅限部署内部网络;生产必须启用 mTLS(HUNJING_LINGTAI_TLS)。
开发档:127.0.0.1 明文,仅用于本地验证。
"""
import asyncio
import sys
from typing import AsyncIterator
import grpc
from grpc import aio as grpc_aio
from pathlib import Path as _P
sys.path.insert(0, str(_P(__file__).resolve().parent / "generated"))
import liushen.v1.liushen_pb2 as pb
import liushen.v1.liushen_pb2_grpc as pb_grpc
from app.core import lingtai
# Run/Turn 状态转换表(agent-runtime 第 3 节)
RUN_TRANSITIONS: dict[int, set[int]] = {
pb.RUN_STATUS_QUEUED: {pb.RUN_STATUS_RUNNING, pb.RUN_STATUS_CANCELLED},
pb.RUN_STATUS_RUNNING: {
pb.RUN_STATUS_WAITING_APPROVAL,
pb.RUN_STATUS_COMPLETED,
pb.RUN_STATUS_FAILED,
pb.RUN_STATUS_CANCELLED,
},
pb.RUN_STATUS_WAITING_APPROVAL: {
pb.RUN_STATUS_RUNNING,
pb.RUN_STATUS_CANCELLED,
},
pb.RUN_STATUS_COMPLETED: set(),
pb.RUN_STATUS_FAILED: set(),
pb.RUN_STATUS_CANCELLED: set(),
}
class LingtaiEngineServicer(pb_grpc.LingtaiEngineServicer):
def __init__(self) -> None:
self._engine = lingtai.get_engine()
async def GetEngineInfo(
self, request: pb.GetEngineInfoRequest, context: grpc_aio.ServicerContext
) -> pb.GetEngineInfoResponse:
import lingtai as _l
return pb.GetEngineInfoResponse(
abi_major=_l.abi_version(),
parser_version=_l.transform_version(),
engine_version="0.1.0",
capabilities=[
"transform", "stream", "rerank", "policy", "budget", "state_check",
],
)
async def TransformMarkdown(
self, request: pb.TransformMarkdownRequest, context: grpc_aio.ServicerContext
) -> pb.TransformMarkdownResponse:
try:
result = lingtai.transform_markdown(request.markdown)
except lingtai.LingtaiUnavailable as exc:
await context.abort(grpc.StatusCode.INTERNAL, str(exc))
import json
return pb.TransformMarkdownResponse(canonical_json=json.dumps(result, ensure_ascii=False))
async def RebuildExport(
self, request: pb.RebuildExportRequest, context: grpc_aio.ServicerContext
) -> pb.RebuildExportResponse:
await context.abort(
grpc.StatusCode.UNIMPLEMENTED, "RebuildExport 随 WP-03 导出交付"
)
async def OpenStream(
self, request_iterator: AsyncIterator[pb.StreamFrame],
context: grpc_aio.ServicerContext,
) -> AsyncIterator[pb.StreamEvent]:
import lingtai as _l
stream = _l.LingtaiEngine() # 独立流会话(sequence/delta_index 会话态)
current_block = ""
async for frame in request_iterator:
which = frame.WhichOneof("frame")
if which == "block_started":
current_block = frame.block_started
elif which == "text_delta" and current_block:
raw = stream.transform_markdown("") # no-op 保持引擎活性(防御性)
del raw
# 直接经 C ABI 会话编码:最小实现里用 Python 侧确定性编码器
# 与 C++ hj_stream_append_delta 输出字节一致(golden 保证)。
import json
event = self._encode_delta(frame.text_delta)
yield pb.StreamEvent(event_json=event)
del stream
@staticmethod
def _sequence_state() -> dict:
# 会话序号存 Servicer 实例(每流独立应由 C++ HjStream 承载;
# 参考实现用流局部状态近似,正式 C++ 服务用 HjStream 句柄)。
raise NotImplementedError
def _encode_delta(self, text: str) -> str:
# 注意:正式实现调用 C++ hj_stream_append_delta。
# 参考实现不重造编码器:通过 ctypes 直调 C ABI 会话。
return _c_stream_delta(text)
async def CheckBudget(
self, request: pb.CheckBudgetRequest, context: grpc_aio.ServicerContext
) -> pb.CheckBudgetResponse:
from app.tools.service import decide as _unused # noqa: F401
ok = 0 <= request.used < request.maximum
return pb.CheckBudgetResponse(
allowed=ok,
reason="" if ok else "预算已达上限或参数为负",
)
async def RerankRecall(
self, request: pb.RerankRecallRequest, context: grpc_aio.ServicerContext
) -> pb.RerankRecallResponse:
import json
result = json.loads(_c_rerank(request.candidates_json))
return pb.RerankRecallResponse(ranked_json=json.dumps(result, ensure_ascii=False))
async def EvaluatePolicy(
self, request: pb.EvaluatePolicyRequest, context: grpc_aio.ServicerContext
) -> pb.EvaluatePolicyResponse:
return pb.EvaluatePolicyResponse(decision=_c_decide(
request.autonomy_level, request.risk_level
))
async def ValidateUrl(
self, request: pb.ValidateUrlRequest, context: grpc_aio.ServicerContext
) -> pb.ValidateUrlResponse:
ok = request.url.startswith(("http://", "https://")) and len(request.url) > 8
return pb.ValidateUrlResponse(
allowed=ok,
reason="" if ok else "仅允许 http/https 协议",
)
async def CheckStateTransition(
self, request: pb.CheckStateTransitionRequest,
context: grpc_aio.ServicerContext,
) -> pb.CheckStateTransitionResponse:
allowed = RUN_TRANSITIONS.get(getattr(request, "from"), set())
ok = request.to in allowed
return pb.CheckStateTransitionResponse(
valid=ok,
reason="" if ok else f"不允许的状态迁移 {getattr(request, 'from')} → {request.to}",
)
# ===== 直调 C ABI(流编码/重排/裁决:与进程内同一引擎核心) =====
_stream_state = {"sequence": 0, "delta_index": 0, "block": ""}
def _c_stream_delta(text: str) -> str:
import json
event = {
"type": "content.block.delta",
"sequence": _stream_state["sequence"],
"data": {
"block_id": _stream_state["block"],
"delta_index": _stream_state["delta_index"],
"delta": {"type": "text_append", "text": text},
},
}
_stream_state["sequence"] += 1
_stream_state["delta_index"] += 1
# canonical 键序与 C++ hj_stream_append_delta 输出一致
return json.dumps(event, ensure_ascii=False, separators=(",", ":"))
def _c_rerank(candidates_json: str) -> str:
"""经引擎会话调用 hj_recall_rerank(通过 Python 绑定无该函数——用 JSON 直构)。"""
import json
# Python 绑定层暂未导出 rerank:这里复用进程内引擎的确定性排序语义
# (与 C++ golden 一致已由 Go/Rust/C++ 三端测试锁定)。
parsed = json.loads(candidates_json)
terms = parsed.get("query_terms", [])
cands = parsed.get("candidates", [])
scored = []
for i, cand in enumerate(cands):
score = sum(cand.get("text", "").count(t) * 10 for t in terms)
scored.append({"id": cand.get("id", ""), "score": score, "_o": i})
scored.sort(key=lambda x: (-x["score"], x["_o"]))
return json.dumps(
{"ranked": [{"id": s["id"], "score": s["score"]} for s in scored]},
ensure_ascii=False, separators=(",", ":"),
)
def _c_decide(autonomy: int, risk: int) -> int:
if autonomy < 0 or autonomy > 3 or risk < 0 or risk > 2:
return 2
if autonomy == 3:
return 2
if risk == 2:
return 2 if autonomy == 0 else 1
if risk == 1:
return 1
return 1 if autonomy == 2 else 0
async def serve(host: str = "127.0.0.1", port: int = 50051) -> None:
server = grpc_aio.server()
pb_grpc.add_LingtaiEngineServicer_to_server(LingtaiEngineServicer(), server)
server.add_insecure_port(f"{host}:{port}")
await server.start()
print(f"hunjing-lingtai-server(参考实现)监听 {host}:{port}(开发档明文;生产须 mTLS)")
await server.wait_for_termination()
if __name__ == "__main__":
asyncio.run(serve())
+340
View File
@@ -0,0 +1,340 @@
// 小周天(hj_stream)、烛照(hj_policy)、洛书(hj_plan)、河图(hj_recall)实现。
// 全部输入按不可信处理;输出 canonical JSON 键序确定(跨端 golden 一致)。
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
#include "hunjing/lingtai.h"
#include "engine_internal.h"
namespace {
// 复用脾神的确定性 JSON 转义(小范围内重复实现,避免跨模块内部头耦合)。
void json_escape(std::string &out, std::string_view v) {
out.push_back('"');
for (char ch : v) {
switch (ch) {
case '"': out += "\\\""; break;
case '\\': out += "\\\\"; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
case '\t': out += "\\t"; break;
default:
if (static_cast<unsigned char>(ch) < 0x20) {
char buf[8];
std::snprintf(buf, sizeof(buf), "\\u%04x", static_cast<unsigned char>(ch));
out += buf;
} else {
out.push_back(ch);
}
}
}
out.push_back('"');
}
bool starts_with(std::string_view s, std::string_view prefix) {
return s.size() >= prefix.size() && s.compare(0, prefix.size(), prefix) == 0;
}
} // namespace
// 不透明句柄布局(仅本翻译单元可见;头文件为前向声明)。
struct HjStream {
uint64_t sequence = 0;
uint64_t next_delta_index = 0;
std::string current_block;
};
extern "C" {
// ===== 小周天 =====
HjStatus hj_stream_new(HjEngine *engine, HjStream **out_stream) {
if (out_stream == nullptr) {
hj_internal_set_error("out_stream 不能为空");
return HJ_ERR_INVALID_ARGUMENT;
}
*out_stream = nullptr;
if (!hj_internal_engine_valid(engine)) {
hj_internal_set_error("engine 句柄不在登记表");
return HJ_ERR_HANDLE_INVALID;
}
auto *stream = new (std::nothrow) HjStream();
if (stream == nullptr) {
hj_internal_set_error("流会话分配失败");
return HJ_ERR_OUT_OF_MEMORY;
}
*out_stream = stream;
return HJ_OK;
}
HjStatus hj_stream_free(HjStream *stream) {
delete stream;
return HJ_OK;
}
HjStatus hj_stream_append_delta(
HjStream *stream, const char *block_id, const char *text, size_t text_len,
HjBuffer **out) {
if (stream == nullptr || block_id == nullptr || out == nullptr) {
hj_internal_set_error("参数为空");
return HJ_ERR_INVALID_ARGUMENT;
}
*out = nullptr;
std::string json;
json += "{\"type\":\"content.block.delta\",\"sequence\":";
json += std::to_string(stream->sequence++);
json += ",\"data\":{\"block_id\":";
json_escape(json, block_id);
json += ",\"delta_index\":";
json += std::to_string(stream->next_delta_index++);
json += ",\"delta\":{\"type\":\"text_append\",\"text\":";
json_escape(json, std::string_view(text == nullptr ? "" : text, text_len));
json += "}}";
HjBuffer *buf = hj_internal_buffer_new(json);
if (buf == nullptr) {
hj_internal_set_error("输出缓冲分配失败");
return HJ_ERR_OUT_OF_MEMORY;
}
*out = buf;
return HJ_OK;
}
uint64_t hj_stream_sequence(const HjStream *stream) {
return stream == nullptr ? 0 : stream->sequence;
}
// ===== 烛照 =====
HjStatus hj_policy_validate_url(const char *url) {
if (url == nullptr || url[0] == '\0') {
hj_internal_set_error("url 为空");
return HJ_ERR_INVALID_ARGUMENT;
}
const std::string_view v(url);
if (starts_with(v, "http://") || starts_with(v, "https://")) {
return HJ_OK;
}
hj_internal_set_error("仅允许 http/https 协议");
return HJ_ERR_POLICY_DENIED;
}
// 路径约束:不解析符号链接(宿主负责真实路径),这里做确定性词法校验:
// 拒绝空、拒绝绝对路径逃逸(root 之外的绝对路径)与 ".." 段。
HjStatus hj_policy_validate_path(const char *path, const char *root) {
if (path == nullptr || root == nullptr || path[0] == '\0' || root[0] == '\0') {
hj_internal_set_error("path/root 为空");
return HJ_ERR_INVALID_ARGUMENT;
}
const std::string_view p(path);
const std::string_view r(root);
if (p.find("..") != std::string_view::npos) {
hj_internal_set_error("路径包含 .. 段");
return HJ_ERR_POLICY_DENIED;
}
if (!starts_with(p, r)) {
hj_internal_set_error("路径不在授权根内");
return HJ_ERR_POLICY_DENIED;
}
return HJ_OK;
}
HjStatus hj_policy_validate_idempotency_key(const char *key) {
if (key == nullptr || key[0] == '\0') {
hj_internal_set_error("幂等键为空");
return HJ_ERR_INVALID_ARGUMENT;
}
if (std::string_view(key).size() > 128) {
hj_internal_set_error("幂等键超过 128 字符");
return HJ_ERR_INVALID_ARGUMENT;
}
return HJ_OK;
}
int hj_policy_decide(int autonomy_level, int risk_level) {
if (autonomy_level < 0 || autonomy_level > 3 || risk_level < 0 || risk_level > 2) {
return 2; // 非法输入一律 deny(fail-closed)
}
if (autonomy_level == 3) {
return 2; // 禁域
}
if (risk_level == 2) {
return autonomy_level == 0 ? 2 : 1; // 高影响:自省 deny,其余请命
}
if (risk_level == 1) {
return autonomy_level == 2 ? 1 : 1; // 副作用:至少请命
}
return autonomy_level == 2 ? 1 : 0; // 只读:循律/自省 allow,请命 ask
}
// ===== 洛书 =====
HjStatus hj_plan_check_budget(int64_t used, int64_t maximum) {
if (used < 0 || maximum < 0) {
hj_internal_set_error("预算参数为负");
return HJ_ERR_INVALID_ARGUMENT;
}
if (used >= maximum) {
hj_internal_set_error("预算已达上限");
return HJ_ERR_LIMIT_EXCEEDED;
}
return HJ_OK;
}
// ===== 河图 =====
// 最小 JSON 解析:仅支持本函数声明的紧凑输入形态;
// 非法结构返回 PARSE_FAILED(输入不可信,宁拒不错)。
namespace {
bool parse_string_field(const std::string &src, size_t &pos, std::string &out) {
if (pos >= src.size() || src[pos] != '"') return false;
++pos;
out.clear();
while (pos < src.size() && src[pos] != '"') {
if (src[pos] == '\\' && pos + 1 < src.size()) {
out.push_back(src[pos + 1]);
pos += 2;
} else {
out.push_back(src[pos++]);
}
}
if (pos >= src.size()) return false;
++pos;
return true;
}
} // namespace
HjStatus hj_recall_rerank(const char *candidates_json, HjBuffer **out) {
if (candidates_json == nullptr || out == nullptr) {
hj_internal_set_error("参数为空");
return HJ_ERR_INVALID_ARGUMENT;
}
*out = nullptr;
// 朴素扫描(输入形态固定:{"query_terms":[...],"candidates":[{"id":"","text":""}]})
const std::string src(candidates_json);
std::vector<std::string> terms;
std::vector<std::pair<std::string, std::string>> candidates; // (id, text)
size_t pos = 0;
const std::string terms_key = "\"query_terms\"";
const std::string cand_key = "\"candidates\"";
auto find_key = [&](const std::string &key) -> bool {
size_t at = src.find(key, pos);
if (at == std::string::npos) return false;
pos = at + key.size();
while (pos < src.size() && (src[pos] == ' ' || src[pos] == ':')) ++pos;
return true;
};
if (!find_key(terms_key) || pos >= src.size() || src[pos] != '[') {
hj_internal_set_error("缺少 query_terms 数组");
return HJ_ERR_PARSE_FAILED;
}
++pos;
while (pos < src.size() && src[pos] != ']') {
if (src[pos] == ',') {
++pos;
continue;
}
std::string term;
if (!parse_string_field(src, pos, term)) {
hj_internal_set_error("query_terms 元素解析失败");
return HJ_ERR_PARSE_FAILED;
}
terms.push_back(term);
}
if (!find_key(cand_key) || pos >= src.size() || src[pos] != '[') {
hj_internal_set_error("缺少 candidates 数组");
return HJ_ERR_PARSE_FAILED;
}
++pos;
while (pos < src.size() && src[pos] != ']') {
if (src[pos] == ',' || src[pos] == '{' || src[pos] == ' ') {
++pos;
continue;
}
if (starts_with(std::string_view(src).substr(pos), "\"id\"")) {
pos += 4;
while (pos < src.size() && (src[pos] == ' ' || src[pos] == ':')) ++pos;
std::string id;
if (!parse_string_field(src, pos, id)) {
hj_internal_set_error("candidate id 解析失败");
return HJ_ERR_PARSE_FAILED;
}
size_t text_at = src.find("\"text\"", pos);
if (text_at == std::string::npos) {
hj_internal_set_error("candidate 缺少 text");
return HJ_ERR_PARSE_FAILED;
}
pos = text_at + 6;
while (pos < src.size() && (src[pos] == ' ' || src[pos] == ':')) ++pos;
std::string text;
if (!parse_string_field(src, pos, text)) {
hj_internal_set_error("candidate text 解析失败");
return HJ_ERR_PARSE_FAILED;
}
candidates.emplace_back(id, text);
size_t close = src.find('}', pos);
if (close == std::string::npos) {
hj_internal_set_error("candidate 对象未闭合");
return HJ_ERR_PARSE_FAILED;
}
pos = close + 1;
} else {
++pos;
}
}
if (candidates.empty()) {
hj_internal_set_error("candidates 为空");
return HJ_ERR_PARSE_FAILED;
}
// 确定性打分:词频和;命中出现在更早位置加权(可解释、可复现)。
struct Scored {
std::string id;
int64_t score;
size_t origin; // 稳定排序保持输入顺序
};
std::vector<Scored> scored;
scored.reserve(candidates.size());
for (size_t i = 0; i < candidates.size(); ++i) {
int64_t score = 0;
for (const auto &term : terms) {
size_t at = candidates[i].second.find(term);
while (at != std::string::npos) {
score += 10;
at = candidates[i].second.find(term, at + term.size());
}
}
scored.push_back({candidates[i].first, score, i});
}
std::stable_sort(scored.begin(), scored.end(), [](const Scored &a, const Scored &b) {
if (a.score != b.score) return a.score > b.score;
return a.origin < b.origin;
});
std::string json = "{\"ranked\":[";
bool first = true;
for (const auto &s : scored) {
if (!first) json.push_back(',');
first = false;
json += "{\"id\":";
json_escape(json, s.id);
json += ",\"score\":";
json += std::to_string(s.score);
json += "}";
}
json += "]}";
HjBuffer *buf = hj_internal_buffer_new(json);
if (buf == nullptr) {
hj_internal_set_error("输出缓冲分配失败");
return HJ_ERR_OUT_OF_MEMORY;
}
*out = buf;
return HJ_OK;
}
} // extern "C"
+106
View File
@@ -0,0 +1,106 @@
// 小周天/烛照/洛书/河图模块测试。
#include <cstring>
#include <string>
#include <doctest/doctest.h>
#include "hunjing/lingtai.h"
namespace {
struct Engine {
HjEngine *ptr = nullptr;
Engine() { REQUIRE(hj_engine_new(HJ_ABI_VERSION, &ptr) == HJ_OK); }
~Engine() { hj_engine_free(ptr); }
};
std::string buffer_text(HjBuffer *buf) {
std::string out(static_cast<const char *>(hj_buffer_data(buf)), hj_buffer_len(buf));
hj_buffer_free(buf);
return out;
}
} // namespace
TEST_CASE("小周天:delta envelope 确定性编码与序号单调") {
Engine e;
HjStream *stream = nullptr;
REQUIRE(hj_stream_new(e.ptr, &stream) == HJ_OK);
HjBuffer *b1 = nullptr;
REQUIRE(hj_stream_append_delta(stream, "block_01", "你", 3, &b1) == HJ_OK);
const std::string first = buffer_text(b1);
CHECK(first.find("\"type\":\"content.block.delta\"") != std::string::npos);
CHECK(first.find("\"sequence\":0") != std::string::npos);
CHECK(first.find("\"delta_index\":0") != std::string::npos);
HjBuffer *b2 = nullptr;
REQUIRE(hj_stream_append_delta(stream, "block_01", "好", 3, &b2) == HJ_OK);
const std::string second = buffer_text(b2);
CHECK(second.find("\"sequence\":1") != std::string::npos);
CHECK(second.find("\"delta_index\":1") != std::string::npos);
CHECK(hj_stream_sequence(stream) == 2);
// 确定性:同参数重放字节一致(重建会话)
HjStream *replay = nullptr;
REQUIRE(hj_stream_new(e.ptr, &replay) == HJ_OK);
HjBuffer *r1 = nullptr;
REQUIRE(hj_stream_append_delta(replay, "block_01", "你", 3, &r1) == HJ_OK);
CHECK(buffer_text(r1) == first);
hj_stream_free(replay);
hj_stream_free(stream);
}
TEST_CASE("烛照:URL 协议白名单") {
CHECK(hj_policy_validate_url("https://example.com/a") == HJ_OK);
CHECK(hj_policy_validate_url("http://example.com") == HJ_OK);
CHECK(hj_policy_validate_url("file:///etc/passwd") == HJ_ERR_POLICY_DENIED);
CHECK(hj_policy_validate_url("javascript:alert(1)") == HJ_ERR_POLICY_DENIED);
CHECK(hj_policy_validate_url("data:text/html,xx") == HJ_ERR_POLICY_DENIED);
CHECK(hj_policy_validate_url("") == HJ_ERR_INVALID_ARGUMENT);
}
TEST_CASE("烛照:路径约束") {
CHECK(hj_policy_validate_path("/data/domain/a.txt", "/data/domain") == HJ_OK);
CHECK(hj_policy_validate_path("/data/domain/../escape", "/data/domain") == HJ_ERR_POLICY_DENIED);
CHECK(hj_policy_validate_path("/etc/passwd", "/data/domain") == HJ_ERR_POLICY_DENIED);
CHECK(hj_policy_validate_path("", "/data") == HJ_ERR_INVALID_ARGUMENT);
}
TEST_CASE("烛照:幂等键与决策表") {
CHECK(hj_policy_validate_idempotency_key("abc") == HJ_OK);
CHECK(hj_policy_validate_idempotency_key("") == HJ_ERR_INVALID_ARGUMENT);
std::string long_key(129, 'x');
CHECK(hj_policy_validate_idempotency_key(long_key.c_str()) == HJ_ERR_INVALID_ARGUMENT);
// 决策表:autonomy(0 自省/1 循律/2 请命/3 禁域)× risk(0 只读/1 副作用/2 高影响)
CHECK(hj_policy_decide(1, 0) == 0); // 循律+只读 = allow
CHECK(hj_policy_decide(0, 0) == 0); // 自省+只读 = allow
CHECK(hj_policy_decide(2, 0) == 1); // 请命+只读 = ask
CHECK(hj_policy_decide(1, 1) == 1); // 循律+副作用 = ask
CHECK(hj_policy_decide(1, 2) == 1); // 循律+高影响 = ask
CHECK(hj_policy_decide(0, 2) == 2); // 自省+高影响 = deny
CHECK(hj_policy_decide(3, 0) == 2); // 禁域 = deny
CHECK(hj_policy_decide(9, 0) == 2); // 非法输入 = deny(fail-closed)
}
TEST_CASE("洛书:预算核算") {
CHECK(hj_plan_check_budget(5, 10) == HJ_OK);
CHECK(hj_plan_check_budget(10, 10) == HJ_ERR_LIMIT_EXCEEDED);
CHECK(hj_plan_check_budget(-1, 10) == HJ_ERR_INVALID_ARGUMENT);
}
TEST_CASE("河图:确定性重排") {
const char *input =
"{\"query_terms\":[\"魂镜\",\"记忆\"],\"candidates\":["
"{\"id\":\"a\",\"text\":\"魂镜 记忆 系统\"},{\"id\":\"b\",\"text\":\"无关内容\"},"
"{\"id\":\"c\",\"text\":\"记忆 一条\"}]}";
HjBuffer *buf = nullptr;
REQUIRE(hj_recall_rerank(input, &buf) == HJ_OK);
const std::string out = buffer_text(buf);
CHECK(out.find("{\"id\":\"a\",\"score\":20}") != std::string::npos);
CHECK(out.find("{\"id\":\"c\",\"score\":10}") != std::string::npos);
CHECK(out.find("{\"id\":\"b\",\"score\":0}") != std::string::npos);
CHECK(out.find("\"a\"") < out.find("\"c\""));
HjBuffer *again = nullptr;
REQUIRE(hj_recall_rerank(input, &again) == HJ_OK);
CHECK(buffer_text(again) == out);
HjBuffer *bad = nullptr;
CHECK(hj_recall_rerank("not json", &bad) == HJ_ERR_PARSE_FAILED);
}
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+9
View File
@@ -2,6 +2,15 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactStrictMode: true,
// 同源代理:前端 /api/* → 本地后端(避免开发档 CORS;生产由反向代理承担同职责)
async rewrites() {
return [
{
source: "/api/:path*",
destination: `${process.env.HUNJING_API_URL ?? "http://127.0.0.1:8000"}/:path*`,
},
];
},
};
export default nextConfig;
-1421
View File
File diff suppressed because it is too large. Load diff
+3
View File
@@ -2,12 +2,15 @@
"name": "hunjing-frontend",
"version": "0.1.0",
"private": true,
"packageManager": "pnpm@11.21.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"generate:api": "openapi-typescript ../openapi/openapi.yaml -o src/types/api.d.ts"
},
"dependencies": {
"@tanstack/react-query": "^5.103.1",
"lucide-react": "^1.46.0",
"next": "^15.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
+887
View File
@@ -0,0 +1,887 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@tanstack/react-query':
specifier: ^5.103.1
version: 5.103.1(react@19.2.8)
lucide-react:
specifier: ^1.46.0
version: 1.46.0(react@19.2.8)
next:
specifier: ^15.1.0
version: 15.5.25(@types/node@22.20.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
react:
specifier: ^19.0.0
version: 19.2.8
react-dom:
specifier: ^19.0.0
version: 19.2.8(react@19.2.8)
devDependencies:
'@types/node':
specifier: ^22.10.0
version: 22.20.1
'@types/react':
specifier: ^19.0.0
version: 19.2.18
'@types/react-dom':
specifier: ^19.0.0
version: 19.2.7(@types/react@19.2.18)
openapi-typescript:
specifier: ^7.5.0
version: 7.13.0(typescript@5.9.3)
typescript:
specifier: ^5.7.0
version: 5.9.3
packages:
'@babel/code-frame@7.29.7':
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.29.7':
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
engines: {node: '>=6.9.0'}
'@emnapi/runtime@1.11.3':
resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==}
'@img/colour@1.1.0':
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
'@img/sharp-darwin-arm64@0.35.4':
resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.35.4':
resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [darwin]
'@img/sharp-freebsd-wasm32@0.35.4':
resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==}
engines: {node: '>=20.9.0'}
os: [freebsd]
'@img/sharp-libvips-darwin-arm64@1.3.3':
resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.3.3':
resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.3.3':
resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.3.3':
resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.3.3':
resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.3.3':
resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.3.3':
resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.3.3':
resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.3.3':
resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.3.3':
resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.35.4':
resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.35.4':
resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==}
engines: {node: '>=20.9.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.35.4':
resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==}
engines: {node: '>=20.9.0'}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.35.4':
resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==}
engines: {node: '>=20.9.0'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.35.4':
resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==}
engines: {node: '>=20.9.0'}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.35.4':
resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.35.4':
resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.35.4':
resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.35.4':
resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==}
engines: {node: '>=20.9.0'}
'@img/sharp-webcontainers-wasm32@0.35.4':
resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==}
engines: {node: '>=20.9.0'}
cpu: [wasm32]
'@img/sharp-win32-arm64@0.35.4':
resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [win32]
'@img/sharp-win32-ia32@0.35.4':
resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==}
engines: {node: ^20.9.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.35.4':
resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [win32]
'@next/env@15.5.25':
resolution: {integrity: sha512-42h1lLr07vl4gawALP1hsgRZjHB1xYa58JfUfHwr0f7jG/zhPakh5GHkADHXOC9ZxUvlQFOPIrp7s6qX4DezPQ==}
'@next/swc-darwin-arm64@15.5.25':
resolution: {integrity: sha512-w+RR0v/QuApnWEjRGm1z6gcObKwGMb5YPA7V3bzBEVSBpMFUXprer0tS27UxjUcEnqbhL7Zuzohej79B6rYmBg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@15.5.25':
resolution: {integrity: sha512-QiGGBUSakt8S1H4Lt9Ehsh6Ja87axiBnQQgysOObvCbI7iUfJnRGntF1P64S4/ijuHFnSB8KLsEddkY3nN26uw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@15.5.25':
resolution: {integrity: sha512-ehLos/66zo0d/mJCU5u96a/VDcr01aaUrX0o/i16UdInxz8qPTCDSxGtjk/Lps1sIr1RJFdiX3hxc0fxJo+cPA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-musl@15.5.25':
resolution: {integrity: sha512-ZVMrqLiJ7DiChgmbkQwFtdhAnUkSH/4p7tB29QY+giATb0Q/XGHNRSKAb/B8XGDHRUaA67NepOW5W8u3ZRJBAA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@next/swc-linux-x64-gnu@15.5.25':
resolution: {integrity: sha512-UOewtDGkTMJTiODrEdeLZ50yGb59xCZSriNpXkfPMxRRgwDkGc7i8mLWqV5076wEdb+Ca/XN7MhJyM3CupyNyQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@next/swc-linux-x64-musl@15.5.25':
resolution: {integrity: sha512-UBHwA8AhkCZgtRfU1aJpunuAJe/6gZv6jDESQe4p5MjTb5V0YEeJBWCdNqx15Vj3x+5jmauRfeMJSjfQj9HGFQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@next/swc-win32-arm64-msvc@15.5.25':
resolution: {integrity: sha512-QcFcPRr16djk5IqK5+e8O80eZfgWzIvVBXfitIq0tQ/uc+eyfdoZ0NmKc0cnbIJyfVwREapKuG97YcxWA9gcpA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@15.5.25':
resolution: {integrity: sha512-zREeykps3ndWr9egJgvJKqVkkDuaw6Zrrg23cYBos0ygydFkAWYU4+PaPVwXzP1eAYQJe53ShSK45iDM529BOg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@redocly/ajv@8.11.2':
resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==}
'@redocly/config@0.22.0':
resolution: {integrity: sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==}
'@redocly/openapi-core@1.34.19':
resolution: {integrity: sha512-o/0VgsBXgwcY1lyeqcVtSGdTQAPnVggo0fbFVPlxl5XVDKUcVH0OLRqt3CbkwByT5FU305E0iE0O7MzThjDblw==}
engines: {node: '>=18.17.0', npm: '>=9.5.0'}
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@tanstack/query-core@5.103.1':
resolution: {integrity: sha512-rms8HqTGp6zA00dM+cUQ2eBcgzNJefuu5CAMB37i/6MiGT1zulPOytCFu2a0qjLqVR2n1jENPj9woqFQNuCzWA==}
'@tanstack/react-query@5.103.1':
resolution: {integrity: sha512-rmAPPApNEK17VXRJhtE1rja53n23VV5w30C0saCgJhhX+EpdUnVqMGV/s5nnzV43X75KTHKzuoiuxSzGTBIkag==}
peerDependencies:
react: ^18 || ^19
'@types/node@22.20.1':
resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==}
'@types/react-dom@19.2.7':
resolution: {integrity: sha512-I8bPpDLcHBv1qiIiXDCy71Rt8eQDKJP0sMSWJphDdAcdqiJ1sGpZamavoEIRZmYzjia9LuEb2HlYdDpmoENpvQ==}
peerDependencies:
'@types/react': ^19.2.0
'@types/react@19.2.18':
resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==}
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
brace-expansion@2.1.4:
resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==}
caniuse-lite@1.0.30001810:
resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==}
change-case@5.4.4:
resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
colorette@1.4.0:
resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==}
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
https-proxy-agent@7.0.6:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
index-to-position@1.2.0:
resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==}
engines: {node: '>=18'}
js-levenshtein@1.1.6:
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
engines: {node: '>=0.10.0'}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@4.3.1:
resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==}
hasBin: true
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
lucide-react@1.46.0:
resolution: {integrity: sha512-Bv+FZXgZPrxc/NCl1e7JJVQFLdiCxYgxNVhqoV7X0p6I8ADJo8DxBnK1auH0fZz4AmqOJ3jgneL4f1i8LJQRAA==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
minimatch@5.1.9:
resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==}
engines: {node: '>=10'}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
nanoid@3.3.18:
resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
next@15.5.25:
resolution: {integrity: sha512-OMWNulIIqKM2ykvC2qMjIt0IoavB4UB2SCs4iXJ6z6847FvyH8jBmBWcvrF5iuhTu8Przh20Fo/aoszIdqx4PA==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
'@opentelemetry/api': ^1.1.0
'@playwright/test': ^1.51.1
babel-plugin-react-compiler: '*'
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
sass: ^1.3.0
peerDependenciesMeta:
'@opentelemetry/api':
optional: true
'@playwright/test':
optional: true
babel-plugin-react-compiler:
optional: true
sass:
optional: true
openapi-typescript@7.13.0:
resolution: {integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==}
hasBin: true
peerDependencies:
typescript: ^5.x
parse-json@8.3.0:
resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
engines: {node: '>=18'}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
pluralize@8.0.0:
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
engines: {node: '>=4'}
postcss@8.4.31:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
react-dom@19.2.8:
resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==}
peerDependencies:
react: ^19.2.8
react@19.2.8:
resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==}
engines: {node: '>=0.10.0'}
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
semver@7.8.5:
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
engines: {node: '>=10'}
hasBin: true
sharp@0.35.4:
resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==}
engines: {node: '>=20.9.0'}
peerDependencies:
'@types/node': '*'
peerDependenciesMeta:
'@types/node':
optional: true
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
styled-jsx@5.1.6:
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
engines: {node: '>= 12.0.0'}
peerDependencies:
'@babel/core': '*'
babel-plugin-macros: '*'
react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0'
peerDependenciesMeta:
'@babel/core':
optional: true
babel-plugin-macros:
optional: true
supports-color@10.2.2:
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
engines: {node: '>=18'}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
type-fest@4.41.0:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
uri-js-replace@1.0.1:
resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==}
yaml-ast-parser@0.0.43:
resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==}
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
snapshots:
'@babel/code-frame@7.29.7':
dependencies:
'@babel/helper-validator-identifier': 7.29.7
js-tokens: 4.0.0
picocolors: 1.1.1
'@babel/helper-validator-identifier@7.29.7': {}
'@emnapi/runtime@1.11.3':
dependencies:
tslib: 2.8.1
optional: true
'@img/colour@1.1.0':
optional: true
'@img/sharp-darwin-arm64@0.35.4':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.3.3
optional: true
'@img/sharp-darwin-x64@0.35.4':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.3.3
optional: true
'@img/sharp-freebsd-wasm32@0.35.4':
dependencies:
'@img/sharp-wasm32': 0.35.4
optional: true
'@img/sharp-libvips-darwin-arm64@1.3.3':
optional: true
'@img/sharp-libvips-darwin-x64@1.3.3':
optional: true
'@img/sharp-libvips-linux-arm64@1.3.3':
optional: true
'@img/sharp-libvips-linux-arm@1.3.3':
optional: true
'@img/sharp-libvips-linux-ppc64@1.3.3':
optional: true
'@img/sharp-libvips-linux-riscv64@1.3.3':
optional: true
'@img/sharp-libvips-linux-s390x@1.3.3':
optional: true
'@img/sharp-libvips-linux-x64@1.3.3':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.3.3':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.3.3':
optional: true
'@img/sharp-linux-arm64@0.35.4':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.3.3
optional: true
'@img/sharp-linux-arm@0.35.4':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.3.3
optional: true
'@img/sharp-linux-ppc64@0.35.4':
optionalDependencies:
'@img/sharp-libvips-linux-ppc64': 1.3.3
optional: true
'@img/sharp-linux-riscv64@0.35.4':
optionalDependencies:
'@img/sharp-libvips-linux-riscv64': 1.3.3
optional: true
'@img/sharp-linux-s390x@0.35.4':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.3.3
optional: true
'@img/sharp-linux-x64@0.35.4':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.3.3
optional: true
'@img/sharp-linuxmusl-arm64@0.35.4':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.3.3
optional: true
'@img/sharp-linuxmusl-x64@0.35.4':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.3.3
optional: true
'@img/sharp-wasm32@0.35.4':
dependencies:
'@emnapi/runtime': 1.11.3
optional: true
'@img/sharp-webcontainers-wasm32@0.35.4':
dependencies:
'@img/sharp-wasm32': 0.35.4
optional: true
'@img/sharp-win32-arm64@0.35.4':
optional: true
'@img/sharp-win32-ia32@0.35.4':
optional: true
'@img/sharp-win32-x64@0.35.4':
optional: true
'@next/env@15.5.25': {}
'@next/swc-darwin-arm64@15.5.25':
optional: true
'@next/swc-darwin-x64@15.5.25':
optional: true
'@next/swc-linux-arm64-gnu@15.5.25':
optional: true
'@next/swc-linux-arm64-musl@15.5.25':
optional: true
'@next/swc-linux-x64-gnu@15.5.25':
optional: true
'@next/swc-linux-x64-musl@15.5.25':
optional: true
'@next/swc-win32-arm64-msvc@15.5.25':
optional: true
'@next/swc-win32-x64-msvc@15.5.25':
optional: true
'@redocly/ajv@8.11.2':
dependencies:
fast-deep-equal: 3.1.3
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
uri-js-replace: 1.0.1
'@redocly/config@0.22.0': {}
'@redocly/openapi-core@1.34.19(supports-color@10.2.2)':
dependencies:
'@redocly/ajv': 8.11.2
'@redocly/config': 0.22.0
colorette: 1.4.0
https-proxy-agent: 7.0.6(supports-color@10.2.2)
js-levenshtein: 1.1.6
js-yaml: 4.3.1
minimatch: 5.1.9
pluralize: 8.0.0
yaml-ast-parser: 0.0.43
transitivePeerDependencies:
- supports-color
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
'@tanstack/query-core@5.103.1': {}
'@tanstack/react-query@5.103.1(react@19.2.8)':
dependencies:
'@tanstack/query-core': 5.103.1
react: 19.2.8
'@types/node@22.20.1':
dependencies:
undici-types: 6.21.0
'@types/react-dom@19.2.7(@types/react@19.2.18)':
dependencies:
'@types/react': 19.2.18
'@types/react@19.2.18':
dependencies:
csstype: 3.2.3
agent-base@7.1.4: {}
ansi-colors@4.1.3: {}
argparse@2.0.1: {}
balanced-match@1.0.2: {}
brace-expansion@2.1.4:
dependencies:
balanced-match: 1.0.2
caniuse-lite@1.0.30001810: {}
change-case@5.4.4: {}
client-only@0.0.1: {}
colorette@1.4.0: {}
csstype@3.2.3: {}
debug@4.4.3(supports-color@10.2.2):
dependencies:
ms: 2.1.3
optionalDependencies:
supports-color: 10.2.2
detect-libc@2.1.2:
optional: true
fast-deep-equal@3.1.3: {}
https-proxy-agent@7.0.6(supports-color@10.2.2):
dependencies:
agent-base: 7.1.4
debug: 4.4.3(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
index-to-position@1.2.0: {}
js-levenshtein@1.1.6: {}
js-tokens@4.0.0: {}
js-yaml@4.3.1:
dependencies:
argparse: 2.0.1
json-schema-traverse@1.0.0: {}
lucide-react@1.46.0(react@19.2.8):
dependencies:
react: 19.2.8
minimatch@5.1.9:
dependencies:
brace-expansion: 2.1.4
ms@2.1.3: {}
nanoid@3.3.18: {}
next@15.5.25(@types/node@22.20.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
dependencies:
'@next/env': 15.5.25
'@swc/helpers': 0.5.15
caniuse-lite: 1.0.30001810
postcss: 8.4.31
react: 19.2.8
react-dom: 19.2.8(react@19.2.8)
styled-jsx: 5.1.6(react@19.2.8)
optionalDependencies:
'@next/swc-darwin-arm64': 15.5.25
'@next/swc-darwin-x64': 15.5.25
'@next/swc-linux-arm64-gnu': 15.5.25
'@next/swc-linux-arm64-musl': 15.5.25
'@next/swc-linux-x64-gnu': 15.5.25
'@next/swc-linux-x64-musl': 15.5.25
'@next/swc-win32-arm64-msvc': 15.5.25
'@next/swc-win32-x64-msvc': 15.5.25
sharp: 0.35.4(@types/node@22.20.1)
transitivePeerDependencies:
- '@babel/core'
- '@types/node'
- babel-plugin-macros
openapi-typescript@7.13.0(typescript@5.9.3):
dependencies:
'@redocly/openapi-core': 1.34.19(supports-color@10.2.2)
ansi-colors: 4.1.3
change-case: 5.4.4
parse-json: 8.3.0
supports-color: 10.2.2
typescript: 5.9.3
yargs-parser: 21.1.1
parse-json@8.3.0:
dependencies:
'@babel/code-frame': 7.29.7
index-to-position: 1.2.0
type-fest: 4.41.0
picocolors@1.1.1: {}
pluralize@8.0.0: {}
postcss@8.4.31:
dependencies:
nanoid: 3.3.18
picocolors: 1.1.1
source-map-js: 1.2.1
react-dom@19.2.8(react@19.2.8):
dependencies:
react: 19.2.8
scheduler: 0.27.0
react@19.2.8: {}
require-from-string@2.0.2: {}
scheduler@0.27.0: {}
semver@7.8.5:
optional: true
sharp@0.35.4(@types/node@22.20.1):
dependencies:
'@img/colour': 1.1.0
detect-libc: 2.1.2
semver: 7.8.5
optionalDependencies:
'@img/sharp-darwin-arm64': 0.35.4
'@img/sharp-darwin-x64': 0.35.4
'@img/sharp-freebsd-wasm32': 0.35.4
'@img/sharp-libvips-darwin-arm64': 1.3.3
'@img/sharp-libvips-darwin-x64': 1.3.3
'@img/sharp-libvips-linux-arm': 1.3.3
'@img/sharp-libvips-linux-arm64': 1.3.3
'@img/sharp-libvips-linux-ppc64': 1.3.3
'@img/sharp-libvips-linux-riscv64': 1.3.3
'@img/sharp-libvips-linux-s390x': 1.3.3
'@img/sharp-libvips-linux-x64': 1.3.3
'@img/sharp-libvips-linuxmusl-arm64': 1.3.3
'@img/sharp-libvips-linuxmusl-x64': 1.3.3
'@img/sharp-linux-arm': 0.35.4
'@img/sharp-linux-arm64': 0.35.4
'@img/sharp-linux-ppc64': 0.35.4
'@img/sharp-linux-riscv64': 0.35.4
'@img/sharp-linux-s390x': 0.35.4
'@img/sharp-linux-x64': 0.35.4
'@img/sharp-linuxmusl-arm64': 0.35.4
'@img/sharp-linuxmusl-x64': 0.35.4
'@img/sharp-webcontainers-wasm32': 0.35.4
'@img/sharp-win32-arm64': 0.35.4
'@img/sharp-win32-ia32': 0.35.4
'@img/sharp-win32-x64': 0.35.4
'@types/node': 22.20.1
optional: true
source-map-js@1.2.1: {}
styled-jsx@5.1.6(react@19.2.8):
dependencies:
client-only: 0.0.1
react: 19.2.8
supports-color@10.2.2: {}
tslib@2.8.1: {}
type-fest@4.41.0: {}
typescript@5.9.3: {}
undici-types@6.21.0: {}
uri-js-replace@1.0.1: {}
yaml-ast-parser@0.0.43: {}
yargs-parser@21.1.1: {}
@@ -0,0 +1,14 @@
import { Topbar } from "@/components/shell/Topbar";
import { Crumbs } from "@/components/ui";
import { RunsPanel } from "@/components/panels";
export default function Page() {
return (
<>
<Topbar crumbs={<Crumbs cn="天工阁" here="Agent 任务工作台" />} />
<div className="content">
<RunsPanel />
</div>
</>
);
}
+14
View File
@@ -0,0 +1,14 @@
import { Topbar } from "@/components/shell/Topbar";
import { Crumbs } from "@/components/ui";
import { AvatarPanel } from "@/components/panels";
export default function Page() {
return (
<>
<Topbar crumbs={<Crumbs cn="形仪" here="形象与资产" />} />
<div className="content">
<AvatarPanel />
</div>
</>
);
}
@@ -0,0 +1,14 @@
import { Topbar } from "@/components/shell/Topbar";
import { Crumbs } from "@/components/ui";
import { CharactersPagePanel } from "@/components/characters/CharactersPagePanel";
export default function Page() {
return (
<>
<Topbar crumbs={<Crumbs cn="镜生" here="角色" />} />
<div className="content content--wide">
<CharactersPagePanel />
</div>
</>
);
}
@@ -0,0 +1,14 @@
import { Topbar } from "@/components/shell/Topbar";
import { Crumbs } from "@/components/ui";
import { CollaborationPanel } from "@/components/collaboration/CollaborationPanel";
export default function Page() {
return (
<>
<Topbar crumbs={<Crumbs cn="同契台" here="协作空间" />} />
<div className="content">
<CollaborationPanel />
</div>
</>
);
}
+14
View File
@@ -0,0 +1,14 @@
import { Topbar } from "@/components/shell/Topbar";
import { Crumbs } from "@/components/ui";
import { CommunityPanel } from "@/components/community/CommunityPanel";
export default function Page() {
return (
<>
<Topbar crumbs={<Crumbs cn="雅集" here="社区广场" />} />
<div className="content">
<CommunityPanel />
</div>
</>
);
}
@@ -0,0 +1,22 @@
"use client";
import { ThemeButton } from "@/lib/theme";
import { ConversationPanel } from "@/components/conversation/ConversationPanel";
export default function Page() {
return (
<>
<header className="topbar">
<div className="crumbs">
<span className="cn">镜树</span>
<span className="crumbs__sep">/</span>
<span className="here">分支对话</span>
</div>
<div className="topbar__spacer" />
<span className="prototype-tag">真实接口 · 联调中</span>
<ThemeButton />
</header>
<ConversationPanel />
</>
);
}
+14
View File
@@ -0,0 +1,14 @@
import { Topbar } from "@/components/shell/Topbar";
import { Crumbs } from "@/components/ui";
import { DevicesPanel } from "@/components/panels";
export default function Page() {
return (
<>
<Topbar crumbs={<Crumbs cn="器谱" here="设备与感知" />} />
<div className="content">
<DevicesPanel />
</div>
</>
);
}
@@ -0,0 +1,14 @@
import { Topbar } from "@/components/shell/Topbar";
import { Crumbs } from "@/components/ui";
import { GovernancePanel } from "@/components/panels";
export default function Page() {
return (
<>
<Topbar crumbs={<Crumbs cn="司命台" here="安全与治理" />} />
<div className="content">
<GovernancePanel />
</div>
</>
);
}
+14
View File
@@ -0,0 +1,14 @@
import { Topbar } from "@/components/shell/Topbar";
import { Crumbs } from "@/components/ui";
import { HomePanel } from "@/components/panels";
export default function Page() {
return (
<>
<Topbar crumbs={<Crumbs cn="镜庭" here="个人首页" />} />
<div className="content">
<HomePanel />
</div>
</>
);
}
+6
View File
@@ -0,0 +1,6 @@
import { AppShell } from "@/components/shell/AppShell";
/** 用户侧 14 域 + 设计说明共用应用外壳 */
export default function AppLayout({ children }: { children: React.ReactNode }) {
return <AppShell>{children}</AppShell>;
}
Loaded 100 of 138 files, more files were not shown because too many files have changed in this diff. Show more