diff --git a/src/muse/接入/__init__.py b/src/muse/接入/__init__.py new file mode 100644 index 0000000..a4c13ca --- /dev/null +++ b/src/muse/接入/__init__.py @@ -0,0 +1 @@ +"""身份与协议接入;业务行为由所属模块维护。""" diff --git a/src/muse/接入/cli/__init__.py b/src/muse/接入/cli/__init__.py new file mode 100644 index 0000000..d127de2 --- /dev/null +++ b/src/muse/接入/cli/__init__.py @@ -0,0 +1 @@ +"""本地具名命令接入。""" diff --git a/src/muse/接入/cli/入口.py b/src/muse/接入/cli/入口.py new file mode 100644 index 0000000..51a64b1 --- /dev/null +++ b/src/muse/接入/cli/入口.py @@ -0,0 +1,225 @@ +"""Python 命令入口。 + +安装后 ``python -m muse`` 与 CLI(W07)使用一致的参数、退出码和业务结果。 +已实现子命令:自检(W02)、迁移(W03,显式维护命令,不在启动时自动执行)。 +未实现或未注入的能力明确失败,不返回伪成功。 +""" + +from __future__ import annotations + +import argparse +import json +import sys + +from muse.共享.错误 import Muse错误 +from muse.启动 import 构建, 自检 +from muse.配置 import 读取配置 + +_退出码_成功 = 0 +_退出码_用法错误 = 2 +_退出码_业务失败 = 1 + + +def _解析参数(argv: list[str] | None) -> argparse.Namespace: + 解析器 = argparse.ArgumentParser( + prog="python -m muse", + description="Muse 应用命令入口", + ) + 子命令 = 解析器.add_subparsers(dest="命令", required=True) + + 自检解析 = 子命令.add_parser("自检", help="报告装配状态;不连接外部服务") + 自检解析.add_argument("配置", help="应用配置 TOML 路径") + + 迁移解析 = 子命令.add_parser("迁移", help="执行数据库迁移(显式维护命令)") + 迁移解析.add_argument("配置", help="应用配置 TOML 路径") + 迁移解析.add_argument("--目标版本", type=int, default=None, help="执行到该版本为止(默认全部)") + 服务解析 = 子命令.add_parser("服务", help="启动已配置的作者工作台") + 服务解析.add_argument("配置", help="应用配置 TOML 路径") + 任务解析 = 子命令.add_parser("任务", help="查看、暂停、取消或恢复持久任务") + 任务解析.add_argument("配置", help="作者配置 TOML 路径") + 任务解析.add_argument("动作", choices=["列表", "查看", "事件", "控制"]) + 任务解析.add_argument("目标", nargs="?", help="任务 ID;控制时为具名请求 JSON 文件") + 任务解析.add_argument("--游标", type=int, default=0) + 结构解析 = 子命令.add_parser("结构", help="发布内置种子或查看已发布的固定结构") + 结构解析.add_argument("配置", help="应用配置 TOML 路径") + 结构解析.add_argument("动作", choices=["发布内置", "查看", "维护"]) + 结构解析.add_argument("目标", nargs="?", help="结构身份") + 结构解析.add_argument("--版本", type=int, default=1) + 作品解析 = 子命令.add_parser("作品", help="管理作品档案、章节和人工正文版本") + 作品解析.add_argument("配置", help="作者配置 TOML 路径") + 作品解析.add_argument( + "动作", + choices=[ + "结构", + "查看", + "目录", + "节点目录", + "保存节点", + "进度", + "正文", + "保存档案", + "新增章节", + "重排章节", + "保存正文", + "回退正文", + ], + ) + 作品解析.add_argument("目标", help="查询身份或写入请求 JSON 文件") + 作品解析.add_argument("--结构", default="work_core") + 作品解析.add_argument("--版本", type=int, default=1) + 作品解析.add_argument("--分支", default="main") + 审阅解析 = 子命令.add_parser("审阅", help="比较候选、修改候选并提交作者决定") + 审阅解析.add_argument("配置", help="作者配置TOML路径") + 审阅解析.add_argument("动作", choices=["列表", "查看", "建立候选", "修改候选", "打开", "决定"]) + 审阅解析.add_argument("目标", help="查询身份或操作请求JSON文件") + 审阅解析.add_argument("--分支", default="main") + 探索解析 = 子命令.add_parser("探索", help="保存私人想法与检查最终整理稿") + 探索解析.add_argument("配置", help="作者配置TOML路径") + 探索解析.add_argument("动作", choices=["列表", "查看", "保存", "检查", "选定", "查看选定"]) + 探索解析.add_argument("目标", nargs="?", help="稿件身份或保存请求JSON文件") + 探索解析.add_argument("--预期哈希") + 规划解析 = 子命令.add_parser("规划", help="保存规划候选、查看版本并执行明确作者决定") + 规划解析.add_argument("配置") + 规划解析.add_argument( + "动作", + choices=[ + "结构", + "列表", + "查看", + "创建候选", + "编辑候选", + "查看候选", + "打开审阅", + "决定", + "读取细纲", + ], + ) + 规划解析.add_argument("目标") + 规划解析.add_argument("--结构", default="outline") + 规划解析.add_argument("--版本", type=int, default=1) + 规划解析.add_argument("--章节") + 配置解析 = 子命令.add_parser("配置", help="保存或查看运行配置草案;不自动验证启用") + 配置解析.add_argument("配置", help="应用连接配置TOML路径") + 配置解析.add_argument("动作", choices=["保存", "查看"]) + 配置解析.add_argument("配置ID") + 配置解析.add_argument("版本") + 配置解析.add_argument("--内容", help="运行配置草案TOML路径") + 管理解析 = 子命令.add_parser("管理", help="管理当前配置的受控暂存;不访问其他根") + 管理解析.add_argument("配置", help="明确的应用连接配置") + 管理解析.add_argument("动作", choices=["清理无租约暂存", "查看清理回执"]) + 管理解析.add_argument("对象ID", nargs="?") + return 解析器.parse_args(argv) + + +def _运行迁移(参数: argparse.Namespace) -> int: + from muse.基础设施.数据库.迁移 import 迁移目标库 + + 已执行 = 迁移目标库(读取配置(参数.配置), 目标版本=参数.目标版本) + if 已执行: + for 项 in 已执行: + print(f"已应用 V{项.版本:04d} {项.文件名}") + else: + print("无待执行迁移(账本已是最新)") + return _退出码_成功 + + +def main(argv: list[str] | None = None) -> int: + 参数 = _解析参数(argv) + try: + if 参数.命令 == "规划": + from muse.接入.cli.作品命令 import 运行规划命令 + + 结果 = 运行规划命令( + 构建(读取配置(参数.配置)), + 参数.动作, + 参数.目标, + 结构ID=参数.结构, + 版本=参数.版本, + 章节ID=参数.章节, + ) + print(json.dumps(结果, ensure_ascii=False)) + return _退出码_成功 + if 参数.命令 == "探索": + from muse.接入.cli.作品命令 import 运行探索命令 + + 结果 = 运行探索命令( + 构建(读取配置(参数.配置)), 参数.动作, 参数.目标, 预期哈希=参数.预期哈希 + ) + print(json.dumps(结果, ensure_ascii=False)) + return ( + _退出码_业务失败 + if isinstance(结果, dict) and 结果.get("valid") is False + else _退出码_成功 + ) + if 参数.命令 == "审阅": + from muse.接入.cli.审阅命令 import 运行审阅命令 + + 结果 = 运行审阅命令(构建(读取配置(参数.配置)), 参数.动作, 参数.目标, 分支=参数.分支) + print(json.dumps(结果, ensure_ascii=False)) + return _退出码_成功 + if 参数.命令 == "管理": + from muse.接入.cli.管理命令 import 运行管理命令 + + 结果 = 运行管理命令(构建(读取配置(参数.配置)), 参数.动作, 参数.对象ID) + print(json.dumps(结果, ensure_ascii=False)) + return _退出码_成功 + if 参数.命令 == "配置": + from muse.接入.cli.管理命令 import 运行配置命令 + + 结果 = 运行配置命令( + 构建(读取配置(参数.配置)), 参数.动作, 参数.配置ID, 参数.版本, 参数.内容 + ) + print(json.dumps(结果, ensure_ascii=False)) + return _退出码_成功 + if 参数.命令 == "作品": + from muse.接入.cli.作品命令 import 运行作品命令 + + 结果 = 运行作品命令( + 构建(读取配置(参数.配置)), + 参数.动作, + 参数.目标, + 结构ID=参数.结构, + 版本=参数.版本, + 分支=参数.分支, + ) + print(json.dumps(结果, ensure_ascii=False)) + return _退出码_成功 + if 参数.命令 == "结构": + from muse.接入.cli.结构命令 import 运行结构命令 + + 结果 = 运行结构命令(构建(读取配置(参数.配置)), 参数.动作, 参数.目标, 参数.版本) + print(json.dumps(结果, ensure_ascii=False)) + return _退出码_成功 + if 参数.命令 == "任务": + from muse.接入.cli.任务命令 import 运行任务命令 + + 结果 = 运行任务命令(构建(读取配置(参数.配置)), 参数.动作, 参数.目标, 参数.游标) + print(json.dumps(结果, ensure_ascii=False, default=str)) + return _退出码_成功 + if 参数.命令 == "自检": + 装配 = 构建(读取配置(参数.配置)) + for 键, 值 in 自检(装配).items(): + print(f"{键}: {值}") + return _退出码_成功 + if 参数.命令 == "迁移": + return _运行迁移(参数) + if 参数.命令 == "服务": + import uvicorn + + from muse.基础设施.可观测性 import 配置日志 + from muse.接入.http.应用 import 创建应用 + + 配置 = 读取配置(参数.配置) + 应用 = 创建应用(配置) + 配置日志() + assert 配置.HTTP is not None + uvicorn.run(应用, host=配置.HTTP.监听地址, port=配置.HTTP.端口, access_log=False) + return _退出码_成功 + except Muse错误 as exc: + print(json.dumps(exc.呈现(), ensure_ascii=False), file=sys.stderr) + return _退出码_业务失败 + return _退出码_用法错误 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/muse/接入/http/__init__.py b/src/muse/接入/http/__init__.py new file mode 100644 index 0000000..2352aa5 --- /dev/null +++ b/src/muse/接入/http/__init__.py @@ -0,0 +1 @@ +"""单作者 HTTP 接入。""" diff --git a/src/muse/接入/http/事件订阅.py b/src/muse/接入/http/事件订阅.py new file mode 100644 index 0000000..40cd53c --- /dev/null +++ b/src/muse/接入/http/事件订阅.py @@ -0,0 +1,95 @@ +"""SSE 与有限事件页共用 PostgreSQL 持久化游标。""" + +import asyncio +from datetime import datetime +from typing import Any + +from fastapi import APIRouter, Header, Query, Request +from fastapi.responses import StreamingResponse +from pydantic import BaseModel +from starlette.concurrency import run_in_threadpool + +from muse.任务运行.接口 import 事件续接 +from muse.接入.http.作者会话 import 作者依赖 +from muse.接入.http.依赖 import 取得任务服务, 读取作者任务 + +路由 = APIRouter(prefix="/api/v1/tasks", tags=["任务事件"]) + + +class 事件响应(BaseModel): + event_id: str + sequence: int + event_type: str + occurred_at: datetime + payload: dict[str, Any] + + +class 事件页(BaseModel): + items: list[事件响应] + reset_required: bool + first_sequence: int + last_sequence: int + + +def 呈现事件(结果: 事件续接) -> 事件页: + return 事件页( + items=[ + 事件响应( + event_id=项.事件ID, + sequence=项.序号, + event_type=项.类型.value, + occurred_at=项.发生时间, + payload=项.载荷, + ) + for 项 in 结果.事件 + ], + reset_required=结果.需要快照, + first_sequence=结果.可用首序号, + last_sequence=结果.最后序号, + ) + + +@路由.get("/{task_id}/events", response_model=事件页, operation_id="read_task_events") +def 读取事件( + task_id: str, + request: Request, + 作者: 作者依赖, + cursor: int = Query(0, ge=0), + work_id: str | None = None, +) -> 事件页: + 读取作者任务(request, task_id, work_id) + return 呈现事件(取得任务服务(request).续接事件(task_id, cursor)) + + +@路由.get("/{task_id}/events/stream", operation_id="stream_task_events") +async def 订阅事件( + task_id: str, + request: Request, + 作者: 作者依赖, + cursor: int = Query(0, ge=0), + work_id: str | None = None, + last_event_id: int | None = Header(None, ge=0), +): + await run_in_threadpool(读取作者任务, request, task_id, work_id) + 服务 = 取得任务服务(request) + + async def 数据流(): + 游标 = max(cursor, last_event_id or 0) + while not await request.is_disconnected(): + 结果 = await run_in_threadpool(服务.续接事件, task_id, 游标) + 页 = 呈现事件(结果) + if 页.reset_required: + yield f"event: reset\ndata: {页.model_dump_json()}\n\n" + return + for 事件 in 页.items: + yield f"id: {事件.sequence}\nevent: task-event\ndata: {事件.model_dump_json()}\n\n" + 游标 = 事件.sequence + if not 页.items: + yield ": keep-alive\n\n" + await asyncio.sleep(1) + + return StreamingResponse( + 数据流(), + media_type="text/event-stream", + headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, + ) diff --git a/src/muse/接入/http/作者会话.py b/src/muse/接入/http/作者会话.py new file mode 100644 index 0000000..9d3092a --- /dev/null +++ b/src/muse/接入/http/作者会话.py @@ -0,0 +1,53 @@ +"""作者凭据只用于建立服务端签名会话,不接受客户端自报身份。""" + +import secrets +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, Request +from pydantic import BaseModel, ConfigDict, SecretStr + +from muse.配置 import 服务配置 + + +class 登录请求(BaseModel): + model_config = ConfigDict(extra="forbid") + password: SecretStr + + +class 会话响应(BaseModel): + author_id: str + + +def 要求作者(request: Request) -> str: + 配置: 服务配置 = request.app.state.服务配置 + if request.session.get("author_id") != 配置.作者ID: + raise HTTPException(401, "请先连接作者工作台") + return 配置.作者ID + + +作者依赖 = Annotated[str, Depends(要求作者)] + + +def 会话路由() -> APIRouter: + 路由 = APIRouter(prefix="/api/v1/session", tags=["作者会话"]) + + @路由.post("", response_model=会话响应, operation_id="open_session") + def 登录(请求: 登录请求, request: Request) -> 会话响应: + 配置: 服务配置 = request.app.state.服务配置 + 口令: str = request.app.state.口令 + if not secrets.compare_digest(请求.password.get_secret_value().encode(), 口令.encode()): + raise HTTPException(401, "访问口令不正确") + request.session.clear() + request.session["author_id"] = 配置.作者ID + return 会话响应(author_id=配置.作者ID) + + @路由.get("", response_model=会话响应, operation_id="read_session") + def 读取(作者: 作者依赖) -> 会话响应: + return 会话响应(author_id=作者) + + @路由.delete("", operation_id="close_session") + def 退出(request: Request, 作者: 作者依赖) -> dict[str, bool]: + request.session.clear() + return {"closed": True} + + return 路由 diff --git a/src/muse/接入/http/依赖.py b/src/muse/接入/http/依赖.py new file mode 100644 index 0000000..ab8aef6 --- /dev/null +++ b/src/muse/接入/http/依赖.py @@ -0,0 +1,18 @@ +"""从已认证请求和应用装配取得任务能力,不接受客户端作者身份。""" + +from fastapi import HTTPException, Request + +from muse.任务运行.接口 import 任务快照, 任务服务 +from muse.接入.http.作者会话 import 要求作者 + + +def 取得任务服务(request: Request) -> 任务服务: + return request.app.state.装配.任务运行 + + +def 读取作者任务(request: Request, 任务ID: str, 作品ID: str | None = None) -> 任务快照: + 作者 = 要求作者(request) + 任务 = 取得任务服务(request).读取任务(任务ID) + if 任务.作者 != 作者 or (作品ID is not None and 任务.冻结输入.get("作品ID") != 作品ID): + raise HTTPException(403, "任务不属于当前作者或作品") + return 任务 diff --git a/src/muse/接入/http/健康检查.py b/src/muse/接入/http/健康检查.py new file mode 100644 index 0000000..ef28fe8 --- /dev/null +++ b/src/muse/接入/http/健康检查.py @@ -0,0 +1,25 @@ +"""分别报告进程存活和依赖就绪,不推断业务任务成功。""" + +from fastapi import APIRouter, Request +from fastapi.responses import JSONResponse + +from muse.共享.错误 import Muse错误, 资源缺失错误 +from muse.资源加载 import 核对资源 + +路由 = APIRouter(tags=["系统状态"]) + + +@路由.get("/health", operation_id="read_health") +def 存活() -> dict[str, str]: + return {"status": "alive"} + + +@路由.get("/ready", operation_id="read_readiness") +def 就绪(request: Request) -> JSONResponse: + try: + request.app.state.装配.要求数据库().检查连接() + if 核对资源()["发布身份"] != request.app.state.装配.配置.资源发布身份: + raise 资源缺失错误("应用配置与资源发布身份不一致") + except Muse错误 as exc: + return JSONResponse(status_code=503, content={"status": "unavailable", "code": exc.错误码}) + return JSONResponse({"status": "ready"}) diff --git a/src/muse/接入/http/应用.py b/src/muse/接入/http/应用.py new file mode 100644 index 0000000..c10008c --- /dev/null +++ b/src/muse/接入/http/应用.py @@ -0,0 +1,94 @@ +"""装配 HTTP 生命周期、单作者会话、来源校验与具名业务路由。""" + +import hashlib +from contextlib import asynccontextmanager +from time import monotonic +from uuid import uuid4 + +from fastapi import FastAPI, Request +from starlette.middleware.sessions import SessionMiddleware + +from muse.共享.错误 import 配置错误 +from muse.启动 import 构建 +from muse.基础设施.凭据读取 import 读取凭据 +from muse.基础设施.可观测性 import 记录运行 +from muse.接入.http.事件订阅 import 路由 as 事件路由 +from muse.接入.http.作者会话 import 会话路由 +from muse.接入.http.健康检查 import 路由 as 健康路由 +from muse.接入.http.路由.任务运行 import 路由 as 任务路由 +from muse.接入.http.路由.作品规划 import 路由 as 作品路由 +from muse.接入.http.路由.作者审阅 import 路由 as 审阅路由 +from muse.接入.http.路由.元数据 import 路由 as 元数据路由 +from muse.接入.http.路由.故事世界 import 路由 as 世界路由 +from muse.接入.http.路由.正文写作 import 路由 as 正文路由 +from muse.接入.http.错误响应 import 协议错误, 安装错误响应 +from muse.接入.http.静态资源 import 挂载工作台 +from muse.配置 import 应用配置 + + +def 创建应用(配置: 应用配置) -> FastAPI: + 服务 = 配置.HTTP + if 服务 is None: + raise 配置错误("启动工作台需要明确的 HTTP 与作者口令配置") + 口令 = 读取凭据("受控存储", 服务.口令文件) + 装配 = 构建(配置) + + @asynccontextmanager + async def 生命周期(app: FastAPI): + app.state.装配 = 装配 + app.state.服务配置 = 服务 + yield + + 应用 = FastAPI( + title="Muse 作者工作台", version="0.1.0", lifespan=生命周期, docs_url=None, redoc_url=None + ) + 应用.state.口令 = 口令 + 应用.state.装配 = 装配 + 应用.state.服务配置 = 服务 + 应用.add_middleware( + SessionMiddleware, + secret_key=hashlib.sha256(("muse-session:" + 口令).encode()).hexdigest(), + session_cookie="muse_session", + same_site="strict", + max_age=12 * 3600, + https_only=服务.公开地址.startswith("https://"), + ) + + @应用.middleware("http") + async def 写入来源(request: Request, call_next): + 开始 = monotonic() + 请求ID = str(uuid4()) + request.state.request_id = 请求ID + if request.method not in {"GET", "HEAD", "OPTIONS"}: + if request.headers.get("origin") not in 服务.允许来源: + return 协议错误("SCOPE_DENIED", "写入请求的来源不在允许范围", 403) + 响应 = await call_next(request) + 响应.headers["X-Request-ID"] = 请求ID + 响应.headers["X-Content-Type-Options"] = "nosniff" + 记录运行("http.completed", 请求ID=请求ID, 状态=响应.status_code, 耗时秒=monotonic() - 开始) + return 响应 + + 安装错误响应(应用) + 注册API(应用) + 挂载工作台(应用) + return 应用 + + +def 注册API(应用: FastAPI) -> None: + """运行和合同生成共享同一组路由;不创建连接或加载凭据。""" + 应用.include_router(任务路由) + 应用.include_router(元数据路由) + 应用.include_router(作品路由) + 应用.include_router(正文路由) + 应用.include_router(审阅路由) + 应用.include_router(世界路由) + 应用.include_router(事件路由) + 应用.include_router(健康路由) + 应用.include_router(会话路由()) + + +def 导出合同() -> dict: + """生成静态外壳,动态作品结构由 S04 运行时提供。""" + 应用 = FastAPI(title="Muse 作者工作台", version="0.1.0") + 注册API(应用) + return 应用.openapi() diff --git a/src/muse/接入/http/路由/__init__.py b/src/muse/接入/http/路由/__init__.py new file mode 100644 index 0000000..853ef69 --- /dev/null +++ b/src/muse/接入/http/路由/__init__.py @@ -0,0 +1 @@ +"""具名业务 HTTP 路由。""" diff --git a/src/muse/接入/http/错误响应.py b/src/muse/接入/http/错误响应.py new file mode 100644 index 0000000..da8c919 --- /dev/null +++ b/src/muse/接入/http/错误响应.py @@ -0,0 +1,57 @@ +"""统一错误外壳;验证错误不回显请求值。""" + +from typing import Any + +from fastapi import FastAPI, Request +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse +from starlette.exceptions import HTTPException + +from muse.共享.错误 import Muse错误 +from muse.接入.主会话 import 参数合同错误 + + +def 错误内容(错误: Muse错误, *, target_ref: str | None = None) -> dict[str, Any]: + return {**错误.呈现(), "target_ref": target_ref} + + +def 协议错误(code: str, message: str, status: int, details: Any = None) -> JSONResponse: + return JSONResponse( + status_code=status, + content={ + "code": code, + "message": message, + "target_ref": None, + "retryable": False, + "details": details or {}, + "actions": [], + }, + ) + + +def 安装错误响应(应用: FastAPI) -> None: + @应用.exception_handler(Muse错误) + async def 业务错误(request: Request, exc: Muse错误) -> JSONResponse: + code = exc.错误码 + if code in {"SCOPE_DENIED", "MUSE_PURPOSE_VIOLATION"}: + status = 403 + elif code.endswith("NOT_FOUND"): + status = 404 + elif code.endswith(("STALE", "CONFLICT", "MISMATCH")): + status = 409 + elif exc.可重试: + status = 503 + else: + status = 422 + return JSONResponse(status_code=status, content=错误内容(exc)) + + @应用.exception_handler(RequestValidationError) + async def 参数错误(request: Request, exc: RequestValidationError) -> JSONResponse: + return JSONResponse(status_code=422, content=参数合同错误(exc.errors()).呈现()) + + @应用.exception_handler(HTTPException) + async def 访问错误(request: Request, exc: HTTPException) -> JSONResponse: + code = {401: "UNAUTHENTICATED", 403: "SCOPE_DENIED", 404: "NOT_FOUND"}.get( + exc.status_code, "HTTP_ERROR" + ) + return 协议错误(code, str(exc.detail), exc.status_code) diff --git a/src/muse/接入/http/静态资源.py b/src/muse/接入/http/静态资源.py new file mode 100644 index 0000000..9fe48be --- /dev/null +++ b/src/muse/接入/http/静态资源.py @@ -0,0 +1,25 @@ +"""只提供已发布工作台资源,SPA 路由不接管 API 错误。""" + +import mimetypes +from pathlib import PurePosixPath + +from fastapi import FastAPI, HTTPException, Response + +from muse.共享.错误 import 资源缺失错误 +from muse.资源加载 import 打开资源 + + +def 挂载工作台(应用: FastAPI) -> None: + @应用.get("/{path:path}", include_in_schema=False) + def 工作台文件(path: str) -> Response: + if path.startswith("api/"): + raise HTTPException(404, "接口不存在") + 名称 = path if PurePosixPath(path).suffix else "index.html" + try: + 数据 = 打开资源(f"工作台/{名称}") + except 资源缺失错误: + raise HTTPException(404, "工作台资源不存在,请核对安装包") from None + 缓存 = "no-cache" if 名称 == "index.html" else "public, max-age=31536000, immutable" + return Response( + 数据, media_type=mimetypes.guess_type(名称)[0], headers={"Cache-Control": 缓存} + ) diff --git a/src/muse/接入/主会话.py b/src/muse/接入/主会话.py new file mode 100644 index 0000000..a1f8344 --- /dev/null +++ b/src/muse/接入/主会话.py @@ -0,0 +1,58 @@ +"""主会话、HTTP 和 CLI 共用的具名任务控制输入与转交。""" + +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field + +from muse.任务运行.接口 import 任务服务, 任务状态 +from muse.元数据.接口 import 有效结构, 生成结构合同 +from muse.共享.错误 import Muse错误 + + +def 参数合同错误(字段错误, *, 前缀: tuple[str, ...] = ()) -> Muse错误: + 错误 = Muse错误( + "请求字段不符合接口合同", + 上下文={"fields": [{"field": [*前缀, *e["loc"]], "type": e["type"]} for e in 字段错误]}, + ) + 错误.错误码 = "INVALID_REQUEST" + return 错误 + + +class 结构读取结果(BaseModel): + schema_id: str + schema_version: int + type_id: str + effective_schema_hash: str + contract: dict + + +def 呈现结构(结构: 有效结构) -> 结构读取结果: + return 结构读取结果( + schema_id=结构.基础.schema_id, + schema_version=结构.基础.schema_version, + type_id=结构.基础.type_id, + effective_schema_hash=结构.effective_schema_hash, + contract=生成结构合同(结构), + ) + + +class 任务控制请求(BaseModel): + model_config = ConfigDict(extra="forbid") + command_id: str = Field(min_length=1) + target_ref: str = Field(min_length=1) + expected_state: 任务状态 + action: Literal["暂停", "取消", "恢复"] + + +class 任务控制回执(BaseModel): + command_id: str + task_id: str + state: 任务状态 + last_sequence: int + + +def 执行任务控制(服务: 任务服务, 作者: str, 请求: 任务控制请求) -> 任务控制回执: + 回执 = 服务.控制任务( + 请求.target_ref, 作者, 请求.expected_state, 请求.action, 命令ID=请求.command_id + ) + return 任务控制回执.model_validate(回执) diff --git a/tests/集成/test_HTTP任务接入.py b/tests/集成/test_HTTP任务接入.py new file mode 100644 index 0000000..2f2d6b4 --- /dev/null +++ b/tests/集成/test_HTTP任务接入.py @@ -0,0 +1,189 @@ +"""真实 ASGI、CLI 与隔离 PostgreSQL 共用任务合同;不使用仓储替身。""" + +import json +import subprocess +import sys +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +from muse.任务运行.接口 import 任务服务, 任务请求, 步骤处理器, 步骤结果, 步骤计划 +from muse.共享.调用身份 import 内容用途, 用途 +from muse.接入.http.应用 import 创建应用 +from muse.编排.接口 import 流程定义, 流程服务, 流程登记 +from muse.配置 import 应用配置, 服务配置 + +pytestmark = pytest.mark.数据库 + + +def test_具名CLI按字段传参不依赖JSON排列__793c2a(任务接入环境, tmp_path: Path) -> None: + 服务, 身份, _, 配置文件 = 任务接入环境 + 请求文件 = tmp_path / "作者决定.json" + 请求文件.write_text( + json.dumps( + { + "action": "取消", + "expected_state": "queued", + "target_ref": 身份, + "command_id": "作者决定-1", + }, + ensure_ascii=False, + ) + ) + 进程 = subprocess.run( + [sys.executable, "-m", "muse", "任务", str(配置文件), "控制", str(请求文件)], + cwd="/", + capture_output=True, + text=True, + timeout=20, + ) + assert 进程.returncode == 0, 进程.stderr + assert json.loads(进程.stdout)["command_id"] == "作者决定-1" + assert 服务.读取任务(身份).状态.value == "cancelled" + + +@pytest.mark.parametrize( + "请求文本,额外参数,退出码", + [ + ("{}", [], 1), + ('["x", 1, null]', [], 1), + ("{}", ["--stdin"], 2), + ("not json", [], 1), + ('{"a": 1}', [], 1), + ('{"sql": "CREATE TABLE t (id int)"}', [], 1), + ('{"sql": "DELETE FROM t"}', [], 1), + ('{"sql": "ALTER TABLE t ADD COLUMN x int", "params": []}', [], 1), + ('{"sql": "DELETE FROM t", "params": []}', [], 1), + ('{"sql": "UPDATE t SET x=1 WHERE id=1"}', [], 1), + ('{"sql": "DELETE FROM t WHERE id=%s", "params": [1]}', [], 1), + ], + ids=[ + "empty", + "typed-array", + "competing-input", + "invalid-json", + "unknown-object", + "create-sql", + "bare-delete", + "alter-params", + "delete-params", + "update-where", + "delete-where", + ], +) +def test_具名CLI拒绝未登记输入且任务不变__a71002( + 任务接入环境, + tmp_path: Path, + 请求文本: str, + 额外参数: list[str], + 退出码: int, +) -> None: + """旧通用 SQL 入口已退出;只接受具名命令合同,不能以 WHERE 或参数列表换取写能力。""" + 服务, 身份, _, 配置文件 = 任务接入环境 + 请求文件 = tmp_path / "请求.json" + 请求文件.write_text(请求文本) + 进程 = subprocess.run( + [sys.executable, "-m", "muse", "任务", str(配置文件), "控制", str(请求文件), *额外参数], + cwd="/", + capture_output=True, + text=True, + timeout=20, + ) + assert 进程.returncode == 退出码, 进程.stdout + if 退出码 == 1: + assert json.loads(进程.stderr)["code"] == "INVALID_REQUEST" + assert 服务.读取任务(身份).状态.value == "queued" + + +@pytest.fixture +def 任务接入环境(应用测试库, tmp_path: Path): + registry = 流程登记() + registry.登记处理器(步骤处理器("synthetic", "1", lambda _: 步骤结果({"ok": True}), "v1", "v1")) + registry.登记类型("synthetic", 必需保护=()) + service = 任务服务(应用测试库[用途.生产], registry) + flow = 流程服务(service, registry) + flow.发布(流程定义("synthetic", "1", (步骤计划("合成步骤", "synthetic", "1"),))) + req = 任务请求( + "合成任务", + "create", + "author-local", + 用途.生产, + 内容用途.抽取, + {}, + "v1", + "test", + { + "source_scope": {}, + "schema_versions": {}, + "authorization": "synthetic", + "budget": {}, + "stop_conditions": [], + }, + 作品ID="work-A", + ) + task_id = flow.发起(req, "synthetic", "1") + token = tmp_path / "口令.txt" + token.write_text("synthetic-password") + config = 应用配置( + 应用测试库[用途.生产].引用, + "test", + HTTP=服务配置(str(token), 公开地址="http://testserver", 允许来源=("http://testserver",)), + ) + file = tmp_path / "应用.toml" + file.write_text(f"""["数据库"] +"取值方式" = "受控存储" +"位置" = {json.dumps(str(tmp_path / "production.txt"))} +["资源"] +"发布身份" = "test" +[HTTP] +"口令文件" = {json.dumps(str(token))} +""") + return service, task_id, config, file + + +def test_HTTP与CLI控制回执和拒绝保持一致__a71001(任务接入环境, tmp_path: Path) -> None: + service, task_id, config, file = 任务接入环境 + with TestClient(创建应用(config)) as client: + assert client.get(f"/api/v1/tasks/{task_id}").status_code == 401 + client.headers["Origin"] = "http://testserver" + assert ( + client.post("/api/v1/session", json={"password": "synthetic-password"}).status_code + == 200 + ) + assert ( + client.get("/api/v1/tasks", params={"work_id": "work-A"}).json()[0]["task_id"] + == task_id + ) + assert ( + client.get(f"/api/v1/tasks/{task_id}", params={"work_id": "work-B"}).status_code == 403 + ) + body = { + "command_id": "cancel-once", + "target_ref": task_id, + "expected_state": "queued", + "action": "取消", + } + response = client.post(f"/api/v1/tasks/{task_id}/controls", json=body) + assert response.status_code == 200 + request = tmp_path / "请求.json" + request.write_text(json.dumps(body)) + args = [sys.executable, "-m", "muse", "任务", str(file), "控制", str(request)] + cli = subprocess.run(args, cwd="/", capture_output=True, text=True, timeout=20) + assert cli.returncode == 0, cli.stderr + assert json.loads(cli.stdout) == response.json() + body["action"] = "暂停" + request.write_text(json.dumps(body)) + response = client.post(f"/api/v1/tasks/{task_id}/controls", json=body) + cli = subprocess.run(args, cwd="/", capture_output=True, text=True, timeout=20) + assert response.status_code == 409 and cli.returncode == 1 + assert response.json() == json.loads(cli.stderr) + events = client.get(f"/api/v1/tasks/{task_id}/events").json() + assert [event["event_type"] for event in events["items"]] == [ + "task.created", + "task.cancelled", + ] + assert ( + client.get(f"/api/v1/tasks/{task_id}/events", params={"cursor": 2}).json()["items"] + == [] + ) diff --git a/web/src/main.tsx b/web/src/main.tsx new file mode 100644 index 0000000..4bbba2b --- /dev/null +++ b/web/src/main.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { Ui应用 } from "./应用/应用"; +import { Ui依赖装配 } from "./应用/依赖装配"; +import "./界面/主题.css"; +import "./界面/排版.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + , +); diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/web/src/功能/任务运行/index.ts b/web/src/功能/任务运行/index.ts new file mode 100644 index 0000000..2dba094 --- /dev/null +++ b/web/src/功能/任务运行/index.ts @@ -0,0 +1 @@ +export { Ui任务列表 } from "./任务列表"; diff --git a/web/src/功能/任务运行/任务列表.tsx b/web/src/功能/任务运行/任务列表.tsx new file mode 100644 index 0000000..accd1a3 --- /dev/null +++ b/web/src/功能/任务运行/任务列表.tsx @@ -0,0 +1,148 @@ +import { useState, type FormEvent } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { 客户端 } from "../../接口/生成/客户端"; +import { Ui反馈状态 } from "../../界面/反馈状态"; +import { useMuse作品会话 } from "../../应用/作品会话"; +import { 查询键 } from "../../接口/查询键"; +import { 状态 } from "./数据"; +import { Ui任务详情 } from "./任务详情"; + +export function Ui任务列表() { + const work = useMuse作品会话(); + const allTasks = useQuery({ + queryKey: ["任务总览"], + queryFn: async ({ signal }) => + (await 客户端.GET("/api/v1/tasks", { signal })).data!, + }); + const works = [ + ...new Set( + (allTasks.data ?? []).flatMap((item) => + item.work_id ? [item.work_id] : [], + ), + ), + ]; + const list = useQuery({ + queryKey: 查询键(work.workId, "任务列表"), + queryFn: async ({ signal }) => + ( + await 客户端.GET("/api/v1/tasks", { + params: { query: { work_id: work.workId ?? undefined } }, + signal, + }) + ).data!, + }); + const [input, setInput] = useState(""); + const [taskId, setTaskId] = useState(""); + const task = useQuery({ + queryKey: 查询键(work.workId, taskId), + enabled: !!taskId, + queryFn: async ({ signal }) => + ( + await 客户端.GET("/api/v1/tasks/{task_id}", { + params: { + path: { task_id: taskId }, + query: { work_id: work.workId ?? undefined }, + }, + signal, + }) + ).data!, + }); + function find(event: FormEvent) { + event.preventDefault(); + setTaskId(input.trim()); + } + return ( +
+
+
+

创作进程

+

每一步,都可以接续

+

查看已发起的任务,跟进进度与处理结果。

+
+ 01 / 任务记录 +
+
+ + +
+
+ +
+ setInput(e.target.value)} + placeholder="粘贴已有任务编号" + required + /> + +
+
+ {!taskId ? ( +
+
+

最近任务

+ {list.data?.length ?? 0} 条记录 +
+ {list.isPending ? ( + + ) : list.error ? ( + + ) : list.data?.length ? ( +
    + {list.data.map((item) => ( +
  • + +
  • + ))} +
+ ) : ( +
+

这里会保留每一步进度

+

当前范围还没有任务记录。

+
+ )} +
+ ) : task.isPending ? ( + + ) : task.error ? ( + + ) : task.data ? ( + + ) : null} +
+ ); +} diff --git a/web/src/功能/任务运行/任务详情.tsx b/web/src/功能/任务运行/任务详情.tsx new file mode 100644 index 0000000..8d2bc6a --- /dev/null +++ b/web/src/功能/任务运行/任务详情.tsx @@ -0,0 +1,79 @@ +import { useEffect, useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { Ui恢复控制 } from "./恢复控制"; +import { 订阅任务事件 } from "../../接口/事件连接"; +import { 查询键 } from "../../接口/查询键"; +import { Ui反馈状态 } from "../../界面/反馈状态"; +import { 状态, 事件名, type 任务, type 任务事件 } from "./数据"; + +export function Ui任务详情({ + task, + scope, +}: { + task: 任务; + scope: string | null; +}) { + const cache = useQueryClient(); + const [events, setEvents] = useState<任务事件[]>([]); + const [notice, setNotice] = useState(""); + useEffect( + () => + 订阅任务事件(task.task_id, { + 收到: (事件) => { + setEvents((旧) => [...旧, 事件].slice(-40)); + setNotice(""); + void cache.invalidateQueries({ + queryKey: 查询键(scope, task.task_id), + }); + }, + 缺口: () => setNotice("较早的事件已归档,请重新读取任务进度。"), + 重连: () => setNotice("连接正在恢复,任务会继续保留。"), + }), + [task.task_id, scope, cache], + ); + return ( +
+
+
+

任务进度

+ {状态[task.state]} +
+

{task.task_id}

+ +
    + {task.steps.map((step, index) => ( +
  1. + + {String(index + 1).padStart(2, "0")} + +
    + {step.step_id} + {状态[step.state] ?? step.state} +
    +
  2. + ))} +
+
+
+
+

过程记录

+ 实时更新 +
+ {notice && } +
    + {events.map((event) => ( +
  1. + + {事件名[event.event_type] ?? "进度更新"} +
  2. + ))} +
+
+
+ ); +} diff --git a/web/src/功能/任务运行/恢复控制.tsx b/web/src/功能/任务运行/恢复控制.tsx new file mode 100644 index 0000000..062a8db --- /dev/null +++ b/web/src/功能/任务运行/恢复控制.tsx @@ -0,0 +1,65 @@ +import { useRef, useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { 客户端 } from "../../接口/生成/客户端"; +import { Ui反馈状态 } from "../../界面/反馈状态"; +import type { 任务 } from "./数据"; + +export function Ui恢复控制({ task }: { task: 任务 }) { + const cache = useQueryClient(); + const [busy, setBusy] = useState(false); + const [controlError, setControlError] = useState(""); + const pending = useRef<{ + action: "暂停" | "取消" | "恢复"; + id: string; + } | null>(null); + async function 控制(action: "暂停" | "取消" | "恢复") { + if (pending.current?.action !== action) + pending.current = { action, id: crypto.randomUUID() }; + setBusy(true); + setControlError(""); + try { + await 客户端.POST("/api/v1/tasks/{task_id}/controls", { + params: { path: { task_id: task.task_id } }, + body: { + command_id: pending.current.id, + target_ref: task.task_id, + expected_state: task.state, + action, + }, + }); + pending.current = null; + await cache.invalidateQueries(); + } catch (e) { + setControlError(e instanceof Error ? e.message : "控制请求未完成。"); + } finally { + setBusy(false); + } + } + return ( + <> + {(["queued", "running"] as string[]).includes(task.state) && ( +
+ + +
+ )} + {(["paused", "failed", "reconciling"] as string[]).includes( + task.state, + ) && ( +
+ + +
+ )} + {controlError && } + + ); +} diff --git a/web/src/功能/任务运行/数据.ts b/web/src/功能/任务运行/数据.ts new file mode 100644 index 0000000..7d9d616 --- /dev/null +++ b/web/src/功能/任务运行/数据.ts @@ -0,0 +1,29 @@ +import type { operations } from "../../接口/生成/类型"; + +export type 任务 = + operations["read_task"]["responses"][200]["content"]["application/json"]; +export type { 任务事件 } from "../../接口/事件连接"; +export const 状态: Record = { + queued: "等待运行", + running: "正在进行", + paused: "已暂停", + reconciling: "等待核对", + failed: "未能完成", + cancelled: "已取消", + completed: "已完成", + pending: "待开始", + unknown: "等待核对", +}; +export const 事件名: Record = { + "task.created": "任务已建立", + "step.started": "开始处理", + "step.completed": "步骤完成", + "task.completed": "任务完成", + "task.cancelled": "任务已取消", + "task.paused": "任务已暂停", + "task.resumed": "继续处理", + "step.failed": "步骤未完成", + "text.delta": "正文更新", + "step.checkpoint": "进度已保存", + "call.unknown": "等待结果核对", +}; diff --git a/web/src/应用/作品会话.ts b/web/src/应用/作品会话.ts new file mode 100644 index 0000000..1b8a59e --- /dev/null +++ b/web/src/应用/作品会话.ts @@ -0,0 +1,13 @@ +import { useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; + +/** 缓存身份和取消共用 React Query,避免维护第二套请求代次。 */ +export function useMuse作品会话() { + const [workId, setWorkId] = useState(null); + const cache = useQueryClient(); + function change(next: string | null) { + void cache.cancelQueries({ queryKey: ["作品", workId] }); + setWorkId(next); + } + return { workId, change }; +} diff --git a/web/src/应用/作者会话.ts b/web/src/应用/作者会话.ts new file mode 100644 index 0000000..88c6b23 --- /dev/null +++ b/web/src/应用/作者会话.ts @@ -0,0 +1,9 @@ +import { useQuery } from "@tanstack/react-query"; +import { 客户端 } from "../接口/生成/客户端"; + +export function useMuse作者会话() { + return useQuery({ + queryKey: ["作者会话"], + queryFn: async () => (await 客户端.GET("/api/v1/session")).data!, + }); +} diff --git a/web/src/应用/依赖装配.tsx b/web/src/应用/依赖装配.tsx new file mode 100644 index 0000000..806b5e8 --- /dev/null +++ b/web/src/应用/依赖装配.tsx @@ -0,0 +1,14 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useState, type PropsWithChildren } from "react"; + +export function Ui依赖装配({ children }: PropsWithChildren) { + const [client] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { retry: false, refetchOnWindowFocus: false }, + }, + }), + ); + return {children}; +} diff --git a/web/src/应用/工作台布局.tsx b/web/src/应用/工作台布局.tsx new file mode 100644 index 0000000..0694587 --- /dev/null +++ b/web/src/应用/工作台布局.tsx @@ -0,0 +1,62 @@ +import { useState, type PropsWithChildren } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { NavLink } from "react-router-dom"; +import { 客户端 } from "../接口/生成/客户端"; +import { Ui反馈状态 } from "../界面/反馈状态"; + +export function Ui工作台布局({ children }: PropsWithChildren) { + const cache = useQueryClient(); + const [disconnectError, setDisconnectError] = useState(""); + async function 断开() { + try { + await 客户端.DELETE("/api/v1/session"); + await cache.cancelQueries(); + cache.setQueryData(["作者会话"], null); + cache.removeQueries({ + predicate: (query) => query.queryKey[0] !== "作者会话", + }); + } catch (e) { + setDisconnectError(e instanceof Error ? e.message : "断开请求未完成。"); + } + } + return ( +
+ +
+
+ 我的工作台 + 每一步,都有记录 +
+ {disconnectError && } + {children} +
+
+ ); +} diff --git a/web/src/应用/应用.tsx b/web/src/应用/应用.tsx new file mode 100644 index 0000000..3eb9068 --- /dev/null +++ b/web/src/应用/应用.tsx @@ -0,0 +1,96 @@ +import { useState, type FormEvent } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { BrowserRouter } from "react-router-dom"; +import { 客户端 } from "../接口/生成/客户端"; +import { 接口错误 } from "../接口/传输"; +import { Ui反馈状态 } from "../界面/反馈状态"; +import { useMuse作者会话 } from "./作者会话"; +import { Ui工作台布局 } from "./工作台布局"; +import { Ui页面路由 } from "./路由"; + +function Ui连接表单() { + const cache = useQueryClient(); + const [password, setPassword] = useState(""); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + async function 连接(event: FormEvent) { + event.preventDefault(); + setBusy(true); + setError(""); + try { + const result = await 客户端.POST("/api/v1/session", { + body: { password }, + }); + setPassword(""); + cache.setQueryData(["作者会话"], result.data); + } catch (e) { + setError(e instanceof Error ? e.message : "连接未完成。"); + } finally { + setBusy(false); + } + } + return ( +
+
+ M +

MUSE · 创作工作台

+

+ 把故事 +
+ 接着写下去。 +

+

+ 整理想法,守住每一次选择。 +
+ 从你的工作台继续。 +

+
+
+

作者会话

+

连接工作台

+

使用本机配置的访问口令。

+ + setPassword(e.target.value)} + required + /> + {error && } + + +
+ ); +} + +export function Ui应用() { + const session = useMuse作者会话(); + if (session.isPending) + return ( +
+ +
+ ); + if ( + session.error && + !(session.error instanceof 接口错误 && session.error.status === 401) + ) + return ( +
+ + +
+ ); + if (!session.data) return ; + return ( + + + + + + ); +} diff --git a/web/src/应用/路由.tsx b/web/src/应用/路由.tsx new file mode 100644 index 0000000..03f84bb --- /dev/null +++ b/web/src/应用/路由.tsx @@ -0,0 +1,59 @@ +import { useState } from "react"; +import { Navigate, Route, Routes } from "react-router-dom"; +import { Ui任务列表 } from "../功能/任务运行"; +import { + Ui作品首页, + Ui作品档案, + Ui探索工作区, + Ui规划工作区, +} from "../功能/作品规划"; +import { Ui候选审阅页, Ui正文页 } from "../功能/正文写作"; +import { 客户端 } from "../接口/生成/客户端"; + +export function Ui页面路由() { + return ( + + } /> + } /> + } /> + } /> + } /> + } + /> + } /> + } /> + } /> + + ); +} + +function Ui连接状态() { + const [message, setMessage] = useState("查看工作台当前的连接情况。"); + const [busy, setBusy] = useState(false); + async function 检查() { + setBusy(true); + try { + await 客户端.GET("/ready"); + setMessage("连接正常,工作台已就绪。"); + } catch (e) { + setMessage(e instanceof Error ? e.message : "连接暂不可用。"); + } finally { + setBusy(false); + } + } + return ( +
+

工作台

+

连接状态

+
+

服务连接

+

{message}

+ +
+
+ ); +} diff --git a/web/src/接口/事件连接.ts b/web/src/接口/事件连接.ts new file mode 100644 index 0000000..1b2abad --- /dev/null +++ b/web/src/接口/事件连接.ts @@ -0,0 +1,34 @@ +import type { operations } from "./生成/类型"; + +export type 任务事件 = + operations["read_task_events"]["responses"][200]["content"]["application/json"]["items"][number]; + +/** 原生 EventSource 携带 Last-Event-ID 重连;页面离开时显式关闭。 */ +export function 订阅任务事件( + 任务ID: string, + 回调: { 收到: (事件: 任务事件) => void; 缺口: () => void; 重连: () => void }, +) { + const 连接 = new EventSource( + `/api/v1/tasks/${encodeURIComponent(任务ID)}/events/stream`, + ); + let 活跃 = true; + let 最后序号 = 0; + 连接.addEventListener("task-event", (消息) => { + if (!活跃) return; + const 事件 = JSON.parse((消息 as MessageEvent).data) as 任务事件; + if (事件.sequence <= 最后序号) return; + 最后序号 = 事件.sequence; + 回调.收到(事件); + }); + 连接.addEventListener("reset", () => { + if (活跃) 回调.缺口(); + 连接.close(); + }); + 连接.onerror = () => { + if (活跃) 回调.重连(); + }; + return () => { + 活跃 = false; + 连接.close(); + }; +} diff --git a/web/src/接口/传输.ts b/web/src/接口/传输.ts new file mode 100644 index 0000000..da181eb --- /dev/null +++ b/web/src/接口/传输.ts @@ -0,0 +1,34 @@ +/** 统一凭据、取消和错误;服务器错误保留稳定代码与中文说明。 */ +export class 接口错误 extends Error { + constructor( + public code: string, + message: string, + public status: number, + ) { + super(message); + } +} + +export async function 传输(request: Request): Promise { + let response: Response; + try { + response = await fetch(request); + } catch (error) { + if (error instanceof DOMException && error.name === "AbortError") + throw error; + throw new 接口错误( + "NETWORK_UNAVAILABLE", + "暂时无法连接工作台,请稍后重试。", + 0, + ); + } + if (!response.ok) { + const body = await response.json().catch(() => ({})); + throw new 接口错误( + body.code ?? "HTTP_ERROR", + body.message ?? "请求未完成,请检查连接后重试。", + response.status, + ); + } + return response; +} diff --git a/web/src/接口/查询键.ts b/web/src/接口/查询键.ts new file mode 100644 index 0000000..918c4a0 --- /dev/null +++ b/web/src/接口/查询键.ts @@ -0,0 +1,9 @@ +/** 查询身份包含作品和目标,切换作品不会共用同一条缓存。 */ +export function 查询键( + 作品: string | null, + 目标: string, + 版本: string = "current", + 用途: string = "production", +) { + return ["作品", 作品, 目标, 版本, 用途] as const; +} diff --git a/web/src/接口/生成/客户端.ts b/web/src/接口/生成/客户端.ts new file mode 100644 index 0000000..5e95dc0 --- /dev/null +++ b/web/src/接口/生成/客户端.ts @@ -0,0 +1,9 @@ +// 由工具/生成接口.py生成;稳定外壳来自同一OpenAPI。 +import createClient from "openapi-fetch"; +import type { paths } from "./类型"; +import { 传输 } from "../传输"; + +export const 客户端 = createClient({ + baseUrl: globalThis.location.origin, fetch: 传输, + credentials: "same-origin" +}); diff --git a/web/src/接口/生成/类型.ts b/web/src/接口/生成/类型.ts new file mode 100644 index 0000000..968b95e --- /dev/null +++ b/web/src/接口/生成/类型.ts @@ -0,0 +1,4499 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/api/v1/tasks/{task_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 读取任务 */ + get: operations["read_task"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/tasks": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 列出任务 */ + get: operations["list_tasks"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/tasks/{task_id}/controls": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 控制任务 */ + post: operations["control_task"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/tasks/{task_id}/role-session-authorizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 批准角色会话 */ + post: operations["approve_role_session"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/tasks/{task_id}/raw-authorizations/{authorization_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** 撤销原文批准 */ + delete: operations["revoke_raw_retention"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/tasks/{task_id}/raw-authorizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 批准原文 */ + post: operations["approve_raw_retention"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/tasks/{task_id}/raw-leases": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 创建原文租约 */ + post: operations["create_raw_lease"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/tasks/{task_id}/raw-leases/{lease_id}/content/{content_hash}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** 写入原文 */ + put: operations["write_raw_bytes"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/tasks/{task_id}/raw-leases/{lease_id}/archive": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 归档原文 */ + post: operations["archive_raw_bytes"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/tasks/{task_id}/raw-leases/{lease_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** 清理原文 */ + delete: operations["cleanup_raw_lease"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/tasks/{task_id}/raw-recovery": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 恢复原文 */ + post: operations["recover_raw_leases"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/tasks/{task_id}/evidence/{evidence_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 读取证据回执 */ + get: operations["read_evidence_receipt"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/tasks/{task_id}/evidence/{evidence_id}/content": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** 补交证据 */ + put: operations["backfill_evidence_bytes"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/schemas/commands": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 维护结构 */ + post: operations["schema_command"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/schemas/{schema_id}/versions/{version}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 读取结构 */ + get: operations["read_schema"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/works/{work_id}/plan-nodes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 读取规划节点 */ + get: operations["read_plan_nodes"]; + put?: never; + /** 保存规划节点 */ + post: operations["save_plan_nodes"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/works/{work_id}/progress": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 读取进度 */ + get: operations["read_work_progress"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/works": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 列出作品 */ + get: operations["list_works"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/works/{work_id}/form": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 读取表单 */ + get: operations["read_work_form"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/works/{work_id}/profile": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 保存档案 */ + post: operations["save_work_profile"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/works/{work_id}/chapters": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 读取目录 */ + get: operations["read_work_chapters"]; + put?: never; + /** 新增章节 */ + post: operations["append_chapter"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/works/{work_id}/chapters/reorder": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 重排章节 */ + post: operations["reorder_chapters"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/explorations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 列出探索 */ + get: operations["list_explorations"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/explorations/{draft_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 读取探索 */ + get: operations["read_exploration"]; + put?: never; + /** 保存探索 */ + post: operations["save_exploration"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/explorations/{draft_id}/check": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 检查探索 */ + post: operations["check_exploration_handoff"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/works/{work_id}/exploration-selection": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 读取选定 */ + get: operations["read_exploration_selection"]; + put?: never; + /** 选定探索 */ + post: operations["select_exploration"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/works/{work_id}/plans/schema": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 读取规划结构入口 */ + get: operations["read_plan_schema"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/works/{work_id}/plans": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 列出规划入口 */ + get: operations["list_plans"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/works/{work_id}/plans/candidates": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 列出规划候选 */ + get: operations["list_plan_candidates"]; + put?: never; + /** 创建规划候选 */ + post: operations["create_author_plan_candidate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/plan-candidates/{candidate_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 读取规划候选 */ + get: operations["read_plan_candidate"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/plan-candidates/{candidate_id}/versions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 编辑规划候选 */ + post: operations["edit_plan_candidate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/plans/{plan_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 读取正式规划 */ + get: operations["read_plan"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/plan-candidates/{candidate_id}/reviews": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 打开规划审阅 */ + post: operations["open_plan_review"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/plan-candidates/{candidate_id}/decisions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 决定规划候选 */ + post: operations["decide_plan_candidate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/works/{work_id}/chapters/{chapter_id}/fine-outline": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 读取可用细纲入口 */ + get: operations["read_usable_fine_outline"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/works/{work_id}/plans/form": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 读取规划表单 */ + get: operations["read_plan_form"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/chapters/{chapter_id}/documents/history": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 读取历史 */ + get: operations["list_document_history"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/chapters/{chapter_id}/documents": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 读取正文 */ + get: operations["read_document"]; + put?: never; + /** 保存正文 */ + post: operations["save_document"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/chapters/{chapter_id}/documents/restore": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 回退正文 */ + post: operations["restore_document"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/chapters/{chapter_id}/writing-candidates": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 列出候选 */ + get: operations["list_writing_candidates"]; + put?: never; + /** 创建人工候选 */ + post: operations["create_author_candidate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/writing-candidates/{candidate_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 读取候选 */ + get: operations["read_writing_candidate"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/writing-candidates/{candidate_id}/edits": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 修改候选 */ + post: operations["edit_writing_candidate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/author-reviews": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 打开审阅 */ + post: operations["open_writing_review"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/writing-candidates/{candidate_id}/decisions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 决定候选 */ + post: operations["decide_writing_candidate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/works/{work_id}/facts/schema": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 读取事实结构 */ + get: operations["read_fact_schema"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/works/{work_id}/facts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 查询事实 */ + get: operations["query_world_state"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/works/{work_id}/fact-proposals": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 提出事实 */ + post: operations["propose_world_fact"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/fact-proposals/{proposal_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 读取提案 */ + get: operations["read_fact_proposal"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/fact-proposals/{proposal_id}/reviews": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 打开事实审阅 */ + post: operations["open_fact_review"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/fact-proposals/{proposal_id}/decisions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** 决定事实 */ + post: operations["decide_world_fact"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/tasks/{task_id}/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 读取事件 */ + get: operations["read_task_events"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/tasks/{task_id}/events/stream": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 订阅事件 */ + get: operations["stream_task_events"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 存活 */ + get: operations["read_health"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/ready": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 就绪 */ + get: operations["read_readiness"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/session": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** 读取 */ + get: operations["read_session"]; + put?: never; + /** 登录 */ + post: operations["open_session"]; + /** 退出 */ + delete: operations["close_session"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components["schemas"]["ValidationError"][]; + }; + /** ValidationError */ + ValidationError: { + /** Location */ + loc: (string | number)[]; + /** Message */ + msg: string; + /** Error Type */ + type: string; + /** Input */ + input?: unknown; + /** Context */ + ctx?: Record; + }; + /** 段落 */ + __: { + /** Paragraph Id */ + paragraph_id: string; + /** Content */ + content: (components["schemas"]["muse__________________8"] | components["schemas"]["muse__________________9"])[]; + }; + /** 值规则 */ + "___-Input": { + /** 类型 */ + "\u7C7B\u578B": string; + /** + * 枚举 + * @default [] + */ + "\u679A\u4E3E": (string | number | boolean)[]; + "\u5143\u7D20"?: components["schemas"]["___-Input"] | null; + /** + * 字段 + * @default [] + */ + "\u5B57\u6BB5": components["schemas"]["muse_________________2"][]; + /** + * 分支 + * @default [] + */ + "\u5206\u652F": components["schemas"]["___-Input"][]; + /** 最小值 */ + "\u6700\u5C0F\u503C"?: number | null; + /** 最大值 */ + "\u6700\u5927\u503C"?: number | null; + "\u5F15\u7528"?: components["schemas"]["muse_________________3"] | null; + /** 最少项 */ + "\u6700\u5C11\u9879"?: number | null; + /** 最多项 */ + "\u6700\u591A\u9879"?: number | null; + }; + /** 事件页 */ + "___-Output": { + /** Items */ + items: components["schemas"]["muse______http______________2"][]; + /** Reset Required */ + reset_required: boolean; + /** First Sequence */ + first_sequence: number; + /** Last Sequence */ + last_sequence: number; + }; + /** 任务控制请求 */ + "muse_________________-Input": { + /** Command Id */ + command_id: string; + /** Target Ref */ + target_ref: string; + expected_state: components["schemas"]["muse__________________1"]; + /** + * Action + * @enum {string} + */ + action: "暂停" | "取消" | "恢复"; + }; + /** 字段用途 */ + muse_________________1: { + /** + * Uivisible + * @default true + */ + uiVisible: boolean; + /** + * Usereditable + * @default true + */ + userEditable: boolean; + /** + * Usersearchable + * @default true + */ + userSearchable: boolean; + /** + * Exportable + * @default true + */ + exportable: boolean; + /** + * Aicontext + * @default false + */ + aiContext: boolean | string[]; + }; + /** 字段定义 */ + muse_________________2: { + /** Field Id */ + field_id: string; + /** Key */ + key: string; + /** 名称 */ + "\u540D\u79F0": string; + "\u503C": components["schemas"]["___-Input"]; + /** + * 说明 + * @default + */ + "\u8BF4\u660E": string; + /** + * 必填 + * @default false + */ + "\u5FC5\u586B": boolean; + /** + * 推荐 + * @default false + */ + "\u63A8\u8350": boolean; + /** + * 别名 + * @default [] + */ + "\u522B\u540D": string[]; + "\u7528\u9014"?: components["schemas"]["muse_________________1"]; + /** + * 存储 + * @default extension_json + */ + "\u5B58\u50A8": string; + /** 绑定身份 */ + "\u7ED1\u5B9A\u8EAB\u4EFD"?: string | null; + /** + * 已停用 + * @default false + */ + "\u5DF2\u505C\u7528": boolean; + }; + /** 引用规则 */ + muse_________________3: { + /** 目标类型 */ + "\u76EE\u6807\u7C7B\u578B": string[]; + /** + * 作品范围 + * @default same_work + */ + "\u4F5C\u54C1\u8303\u56F4": string; + /** + * 确认状态 + * @default confirmed + */ + "\u786E\u8BA4\u72B6\u6001": string; + /** + * 关系用途 + * @default 依据 + */ + "\u5173\u7CFB\u7528\u9014": string; + }; + /** 类型定义 */ + muse_________________4: { + /** Type Id */ + type_id: string; + /** 名称 */ + "\u540D\u79F0": string; + /** 语义域 */ + "\u8BED\u4E49\u57DF": string; + /** 实例粒度 */ + "\u5B9E\u4F8B\u7C92\u5EA6": string | string[]; + /** 实例族 */ + "\u5B9E\u4F8B\u65CF": string[]; + /** + * 说明 + * @default + */ + "\u8BF4\u660E": string; + }; + /** 字段条件 */ + muse_________________5: { + /** Key */ + key: string; + /** 等于 */ + "\u7B49\u4E8E": string; + /** + * 必填 + * @default [] + */ + "\u5FC5\u586B": string[]; + /** + * 禁止 + * @default [] + */ + "\u7981\u6B62": string[]; + }; + /** 结构定义 */ + muse_________________6: { + /** Type Id */ + type_id: string; + /** Schema Id */ + schema_id: string; + /** Schema Version */ + schema_version: number; + /** 字段 */ + "\u5B57\u6BB5": components["schemas"]["muse_________________2"][]; + /** 父版本 */ + "\u7236\u7248\u672C"?: number | null; + /** + * 条件 + * @default [] + */ + "\u6761\u4EF6": components["schemas"]["muse_________________5"][]; + }; + /** + * 字段变更 + * @description 业务content内的不重叠位置变更;数组结构变化用完整字段set。 + */ + muse_________________7: { + /** 路径 */ + "\u8DEF\u5F84": (string | number)[]; + /** 动作 */ + "\u52A8\u4F5C": string; + /** 值 */ + "\u503C"?: unknown; + }; + /** + * 任务状态 + * @enum {string} + */ + muse__________________1: "queued" | "running" | "paused" | "reconciling" | "failed" | "cancelled" | "completed"; + /** 正文草稿 */ + muse__________________10: { + /** Paragraphs */ + paragraphs: components["schemas"]["__"][]; + /** + * Format Version + * @default 1 + */ + format_version: number; + }; + /** 事实结构 */ + muse__________________11: { + /** Schema Id */ + schema_id: string; + /** Base Version */ + base_version: number; + /** Effective Schema Hash */ + effective_schema_hash: string; + /** Projection Version */ + projection_version: string; + /** Extension Version */ + extension_version?: number | null; + }; + /** 引文锚点 */ + muse__________________12: { + /** Paragraph Id */ + paragraph_id: string; + /** Start */ + start: number; + /** End */ + end: number; + /** Quote */ + quote: string; + }; + /** 正文依据 */ + muse__________________13: { + /** Chapter Id */ + chapter_id: string; + /** Branch Id */ + branch_id: string; + /** Revision */ + revision: number; + /** Document Hash */ + document_hash: string; + /** Evidence */ + evidence: components["schemas"]["muse__________________12"][]; + }; + /** 知情记录 */ + muse__________________14: { + /** Actor Id */ + actor_id: string; + /** From Chapter Id */ + from_chapter_id: string; + }; + /** 叙事窗口 */ + muse__________________15: { + /** From Chapter Id */ + from_chapter_id: string; + /** To Chapter Id */ + to_chapter_id: string; + }; + /** 节点条目 */ + muse__________________16: { + /** Node Id */ + node_id: string; + /** + * Kind + * @enum {string} + */ + kind: "volume" | "chapter" | "scene"; + /** Title */ + title: string; + /** Parent Id */ + parent_id: string | null; + /** Position */ + position: number; + }; + /** 作品概要 */ + muse__________________17: { + /** Work Id */ + work_id: string; + /** Revision */ + revision: number; + /** Content */ + content: { + [key: string]: unknown; + }; + }; + /** 章节条目 */ + muse__________________18: { + /** Chapter Id */ + chapter_id: string; + /** Title */ + title: string; + /** Position */ + position: number; + }; + /** 目录结果 */ + muse__________________19: { + /** Work Id */ + work_id: string; + /** Revision */ + revision: number; + /** Chapters */ + chapters: components["schemas"]["muse__________________18"][]; + }; + /** + * 内容用途 + * @description 内容消费用途。 + * @enum {string} + */ + muse__________________2: "planning" | "generation" | "detection" | "extraction"; + /** 规划结果 */ + muse__________________20: { + /** Plan Id */ + plan_id: string; + /** Work Id */ + work_id: string; + /** Chapter Id */ + chapter_id: string | null; + /** Node Id */ + node_id?: string | null; + /** Type Id */ + type_id: string; + /** Revision */ + revision: number; + /** Content */ + content: { + [key: string]: unknown; + }; + /** Schema Binding */ + schema_binding: { + [key: string]: unknown; + }; + /** Source Dependencies */ + source_dependencies: { + [key: string]: string; + }[]; + }; + /** 目录节点 */ + muse__________________3: { + /** Node Id */ + node_id: string; + /** + * Kind + * @enum {string} + */ + kind: "volume" | "chapter" | "scene"; + /** Title */ + title: string; + /** Parent Id */ + parent_id?: string | null; + }; + /** 档案结构 */ + muse__________________4: { + /** Schema Id */ + schema_id: string; + /** Base Version */ + base_version: number; + /** Effective Schema Hash */ + effective_schema_hash: string; + /** Projection Version */ + projection_version: string; + /** Extension Version */ + extension_version?: number | null; + }; + /** 探索选择 */ + muse__________________5: { + /** Work Id */ + work_id: string; + /** Draft Id */ + draft_id: string; + /** Expected Hash */ + expected_hash: string; + /** Expected Revision */ + expected_revision: number; + }; + /** 规划保存 */ + muse__________________6: { + /** Work Id */ + work_id: string; + /** Expected Revision */ + expected_revision: number; + schema: components["schemas"]["muse__________________4"]; + /** Content */ + content: { + [key: string]: unknown; + }; + /** Chapter Id */ + chapter_id?: string | null; + /** Selection Revision */ + selection_revision?: number | null; + /** + * Changes + * @default [] + */ + changes: components["schemas"]["muse_________________7"][]; + /** Node Id */ + node_id?: string | null; + }; + /** 规划定位 */ + muse__________________7: { + /** Candidate Id */ + candidate_id: string; + /** Candidate Revision */ + candidate_revision: number; + /** Candidate Hash */ + candidate_hash: string; + /** Plan Id */ + plan_id: string; + /** Expected Revision */ + expected_revision: number; + }; + /** 文本节点 */ + muse__________________8: { + /** Text */ + text: string; + /** + * Marks + * @default [] + */ + marks: ("strong" | "emphasis")[]; + /** + * Type + * @default text + * @constant + */ + type: "text"; + }; + /** 换行节点 */ + muse__________________9: { + /** + * Newline + * @default + * @enum {string} + */ + newline: "\n" | "\r\n" | "\r"; + /** + * Type + * @default line_break + * @constant + */ + type: "line_break"; + }; + /** 任务控制回执 */ + muse___________________2: { + /** Command Id */ + command_id: string; + /** Task Id */ + task_id: string; + state: components["schemas"]["muse__________________1"]; + /** Last Sequence */ + last_sequence: number; + }; + /** 结构读取结果 */ + muse___________________3: { + /** Schema Id */ + schema_id: string; + /** Schema Version */ + schema_version: number; + /** Type Id */ + type_id: string; + /** Effective Schema Hash */ + effective_schema_hash: string; + /** Contract */ + contract: { + [key: string]: unknown; + }; + }; + /** 探索稿概要 */ + muse___________________4: { + /** Draft Id */ + draft_id: string; + /** Title */ + title: string; + /** Revision */ + revision: number; + /** Draft Hash */ + draft_hash: string; + }; + /** 探索稿结果 */ + muse___________________5: { + /** Draft Id */ + draft_id: string; + /** Title */ + title: string; + /** Revision */ + revision: number; + /** Draft Hash */ + draft_hash: string; + /** Content */ + content: string; + }; + /** 已选探索稿 */ + muse___________________6: { + /** Work Id */ + work_id: string; + /** Revision */ + revision: number; + snapshot: components["schemas"]["muse___________________5"]; + /** Selected By */ + selected_by: string; + /** Selected At */ + selected_at: string; + }; + /** 结构候选请求 */ + muse____________________1: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + action: "登记候选"; + "\u7C7B\u578B": components["schemas"]["muse_________________4"]; + "\u7ED3\u6784": components["schemas"]["muse_________________6"]; + }; + /** 探索检查请求 */ + muse____________________10: { + /** Expected Hash */ + expected_hash: string; + }; + /** 探索选定请求 */ + muse____________________11: { + /** Command Id */ + command_id: string; + selection: components["schemas"]["muse__________________5"]; + }; + /** 规划候选请求 */ + muse____________________12: { + /** Command Id */ + command_id: string; + request: components["schemas"]["muse__________________6"]; + }; + /** 规划编辑请求 */ + muse____________________13: { + /** Command Id */ + command_id: string; + candidate: components["schemas"]["muse__________________7"]; + /** Changes */ + changes: components["schemas"]["muse_________________7"][]; + }; + /** 规划决定请求 */ + muse____________________14: { + /** Command Id */ + command_id: string; + candidate: components["schemas"]["muse__________________7"]; + /** + * Action + * @enum {string} + */ + action: "adopt" | "reject" | "defer"; + /** Author Review Id */ + author_review_id: string; + /** Review Hash */ + review_hash: string; + /** Approved Changes */ + approved_changes: string[]; + }; + /** 人工保存请求 */ + muse____________________15: { + /** Command Id */ + command_id: string; + /** Chapter Id */ + chapter_id: string; + /** + * Branch Id + * @default main + */ + branch_id: string; + /** Expected Revision */ + expected_revision: number; + document: components["schemas"]["muse__________________10"]; + }; + /** 回退正文请求 */ + muse____________________16: { + /** Command Id */ + command_id: string; + /** Chapter Id */ + chapter_id: string; + /** + * Branch Id + * @default main + */ + branch_id: string; + /** Expected Revision */ + expected_revision: number; + /** Restore From */ + restore_from: number; + }; + /** 候选修改请求 */ + muse____________________17: { + /** Command Id */ + command_id: string; + /** Candidate Id */ + candidate_id: string; + /** Expected Candidate Revision */ + expected_candidate_revision: number; + /** Expected Candidate Hash */ + expected_candidate_hash: string; + document?: components["schemas"]["muse__________________10"] | null; + /** Selected Changes */ + selected_changes?: string[]; + }; + /** 候选审阅请求 */ + muse____________________18: { + /** Candidate Id */ + candidate_id: string; + /** Candidate Revision */ + candidate_revision: number; + /** Candidate Hash */ + candidate_hash: string; + /** Target Ref */ + target_ref: string; + /** Expected Revision */ + expected_revision: number; + }; + /** 候选决定请求 */ + muse____________________19: { + /** Command Id */ + command_id: string; + /** Candidate Id */ + candidate_id: string; + /** Candidate Revision */ + candidate_revision: number; + /** Candidate Hash */ + candidate_hash: string; + /** Target Ref */ + target_ref: string; + /** Expected Revision */ + expected_revision: number; + /** + * Action + * @enum {string} + */ + action: "adopt" | "reject" | "defer"; + /** Author Review Id */ + author_review_id: string; + /** Review Hash */ + review_hash: string; + /** Approved Changes */ + approved_changes?: string[]; + }; + /** 结构发布请求 */ + muse____________________2: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + action: "发布版本"; + /** Schema Id */ + schema_id: string; + /** Version */ + version: number; + /** Content Hash */ + content_hash: string; + }; + /** 事实提案请求 */ + muse____________________20: { + /** Work Id */ + work_id: string; + /** Object Id */ + object_id: string; + /** Expected Revision */ + expected_revision: number; + schema: components["schemas"]["muse__________________11"]; + /** Content */ + content: { + [key: string]: unknown; + }; + /** Sources */ + sources: components["schemas"]["muse__________________13"][]; + /** Effective From */ + effective_from: string | null; + /** + * Instance Family + * @default entity + * @enum {string} + */ + instance_family: "entity" | "relation"; + /** + * Independent + * @default false + */ + independent: boolean; + /** + * Knowledge Mode + * @default fact + * @enum {string} + */ + knowledge_mode: "fact" | "belief" | "plan" | "setting"; + /** + * Known To + * @default [] + */ + known_to: components["schemas"]["muse__________________14"][]; + /** + * Reader Visible + * @default true + */ + reader_visible: boolean; + planned_window?: components["schemas"]["muse__________________15"] | null; + }; + /** 事实创建请求 */ + muse____________________21: { + /** Command Id */ + command_id: string; + proposal: components["schemas"]["muse____________________20"]; + }; + /** 事实审阅请求 */ + muse____________________22: { + /** Proposal Id */ + proposal_id: string; + /** Proposal Revision */ + proposal_revision: number; + /** Proposal Hash */ + proposal_hash: string; + /** Target Ref */ + target_ref: string; + /** Expected Revision */ + expected_revision: number; + }; + /** 事实决定请求 */ + muse____________________23: { + /** Command Id */ + command_id: string; + /** Proposal Id */ + proposal_id: string; + /** Proposal Revision */ + proposal_revision: number; + /** Proposal Hash */ + proposal_hash: string; + /** Target Ref */ + target_ref: string; + /** Expected Revision */ + expected_revision: number; + /** + * Action + * @enum {string} + */ + action: "adopt" | "reject" | "defer"; + /** Author Review Id */ + author_review_id: string; + /** Review Hash */ + review_hash: string; + /** Approved Changes */ + approved_changes?: string[]; + }; + /** 节点目录结果 */ + muse____________________24: { + /** Work Id */ + work_id: string; + /** Revision */ + revision: number; + /** Nodes */ + nodes: components["schemas"]["muse__________________16"][]; + }; + /** 正式回执结果 */ + muse____________________25: { + /** Receipt Id */ + receipt_id: string; + /** Command Id */ + command_id: string; + /** Target Ref */ + target_ref: string; + /** Action */ + action: string; + /** Author Review Id */ + author_review_id: string | null; + /** Basis */ + basis: { + [key: string]: unknown; + }; + /** Results */ + results: { + [key: string]: unknown; + }[]; + /** Impact Ids */ + impact_ids: string[]; + }; + /** 候选进度结果 */ + muse____________________26: { + /** Candidate Id */ + candidate_id: string; + /** Revision */ + revision: number; + /** Decision */ + decision: string; + /** Stale */ + stale: boolean; + }; + /** 章节进度结果 */ + muse____________________27: { + /** Chapter Id */ + chapter_id: string; + /** Title */ + title: string; + /** Position */ + position: number; + /** Document Revision */ + document_revision: number; + /** Document Hash */ + document_hash: string; + /** Visible Characters */ + visible_characters: number; + /** Candidates */ + candidates: components["schemas"]["muse____________________26"][]; + }; + /** 任务进度结果 */ + muse____________________28: { + /** Task Id */ + task_id: string; + /** State */ + state: string; + /** Event Cursor */ + event_cursor: number; + }; + /** 作品进度结果 */ + muse____________________29: { + /** Work Id */ + work_id: string; + /** Directory Revision */ + directory_revision: number; + /** Total Chapters */ + total_chapters: number; + /** Saved Chapters */ + saved_chapters: number; + /** Undecided Candidates */ + undecided_candidates: number; + /** Chapters */ + chapters: components["schemas"]["muse____________________27"][]; + /** Tasks */ + tasks: components["schemas"]["muse____________________28"][]; + }; + /** 结构对照请求 */ + muse____________________3: { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + action: "兼容检查"; + /** Schema Id */ + schema_id: string; + /** Base Version */ + base_version: number; + "\u7ED3\u6784": components["schemas"]["muse_________________6"]; + }; + /** 作品表单结果 */ + muse____________________30: { + /** Work Id */ + work_id: string; + /** Revision */ + revision: number; + /** Content */ + content: { + [key: string]: unknown; + }; + schema_binding: components["schemas"]["muse__________________4"]; + /** Visible Schema */ + visible_schema: { + [key: string]: unknown; + }; + /** Editable Schema */ + editable_schema: { + [key: string]: unknown; + }; + /** Visible Projection Version */ + visible_projection_version: string; + }; + /** 交付格式问题 */ + muse____________________31: { + /** Code */ + code: string; + /** Message */ + message: string; + }; + /** 探索检查结果 */ + muse____________________32: { + /** Draft Id */ + draft_id: string; + /** Draft Hash */ + draft_hash: string; + /** Valid */ + valid: boolean; + /** Problems */ + problems: components["schemas"]["muse____________________31"][]; + /** Setting Count */ + setting_count: number; + /** Contract Version */ + contract_version: string; + }; + /** 探索选定状态 */ + muse____________________33: { + /** Work Id */ + work_id: string; + /** Revision */ + revision: number; + selection: components["schemas"]["muse___________________6"] | null; + }; + /** 规划候选结果 */ + muse____________________34: { + /** Candidate Id */ + candidate_id: string; + /** Plan Id */ + plan_id: string; + /** Revision */ + revision: number; + /** Candidate Hash */ + candidate_hash: string; + /** Work Id */ + work_id: string; + /** Chapter Id */ + chapter_id: string | null; + /** Node Id */ + node_id?: string | null; + /** Expected Revision */ + expected_revision: number; + schema_binding: components["schemas"]["muse__________________4"]; + /** Content */ + content: { + [key: string]: unknown; + }; + /** Decision */ + decision: string; + /** Visible Schema */ + visible_schema: { + [key: string]: unknown; + }; + /** Editable Schema */ + editable_schema: { + [key: string]: unknown; + }; + }; + /** 规划审阅结果 */ + muse____________________35: { + /** Review Id */ + review_id: string; + /** Review Hash */ + review_hash: string; + /** Snapshot */ + snapshot: { + [key: string]: unknown; + }; + candidate: components["schemas"]["muse____________________34"]; + before: components["schemas"]["muse__________________20"] | null; + }; + /** 规划表单结果 */ + muse____________________36: { + /** Work Id */ + work_id: string; + /** Chapter Id */ + chapter_id: string | null; + /** Node Id */ + node_id?: string | null; + /** Plan Id */ + plan_id: string; + /** Revision */ + revision: number; + schema_binding: components["schemas"]["muse__________________4"]; + /** Content */ + content: { + [key: string]: unknown; + }; + /** Visible Schema */ + visible_schema: { + [key: string]: unknown; + }; + /** Editable Schema */ + editable_schema: { + [key: string]: unknown; + }; + }; + /** 正文历史结果 */ + muse____________________37: { + /** Revision */ + revision: number; + /** Document Hash */ + document_hash: string; + /** Visible Text Hash */ + visible_text_hash: string; + /** Restored From */ + restored_from: number | null; + /** + * Created At + * Format: date-time + */ + created_at: string; + }; + /** 正文读取结果 */ + muse____________________38: { + /** Work Id */ + work_id: string; + /** Document Id */ + document_id: string; + /** Chapter Id */ + chapter_id: string; + /** Branch Id */ + branch_id: string; + /** Revision */ + revision: number; + /** + * State + * @enum {string} + */ + state: "saved" | "unsaved"; + document: components["schemas"]["muse__________________10"]; + /** Visible Text */ + visible_text: string; + /** Document Hash */ + document_hash: string; + /** Visible Text Hash */ + visible_text_hash: string; + }; + /** 候选检查结果 */ + muse____________________39: { + /** Format */ + format: string; + /** Format Version */ + format_version: number; + /** Quality */ + quality: string; + }; + /** 节点目录保存 */ + muse____________________4: { + /** Work Id */ + work_id: string; + /** Expected Revision */ + expected_revision: number; + /** Nodes */ + nodes: components["schemas"]["muse__________________3"][]; + }; + /** 候选差异结果 */ + muse____________________40: { + /** Change Id */ + change_id: string; + /** Start */ + start: number; + /** End */ + end: number; + /** Before */ + before: components["schemas"]["__"][]; + /** After */ + after: components["schemas"]["__"][]; + /** Before Text */ + before_text: string; + /** After Text */ + after_text: string; + }; + /** 正文候选结果 */ + muse____________________41: { + /** Candidate Id */ + candidate_id: string; + /** Revision */ + revision: number; + /** Candidate Hash */ + candidate_hash: string; + /** Document Id */ + document_id: string; + /** Chapter Id */ + chapter_id: string; + /** Branch Id */ + branch_id: string; + /** Origin */ + origin: string; + /** Base Revision */ + base_revision: number; + /** Current Document Revision */ + current_document_revision: number; + document: components["schemas"]["muse__________________10"]; + /** Visible Text */ + visible_text: string; + checks: components["schemas"]["muse____________________39"]; + /** + * Decision + * @enum {string} + */ + decision: "undecided" | "adopt" | "reject" | "defer"; + /** Stale */ + stale: boolean; + /** Changes */ + changes: components["schemas"]["muse____________________40"][]; + }; + /** 候选审阅结果 */ + muse____________________42: { + /** Review Id */ + review_id: string; + /** Review Hash */ + review_hash: string; + /** Snapshot */ + snapshot: { + [key: string]: unknown; + }; + candidate: components["schemas"]["muse____________________41"]; + before: components["schemas"]["muse____________________38"]; + }; + /** 事实来源版本 */ + muse____________________43: { + /** Chapter Id */ + chapter_id: string; + /** Branch Id */ + branch_id: string; + /** Revision */ + revision: number; + /** Document Hash */ + document_hash: string; + }; + /** 世界事实结果 */ + muse____________________44: { + /** Object Id */ + object_id: string; + /** Type Id */ + type_id: string; + /** Revision */ + revision: number; + /** System Revision */ + system_revision: number; + /** Content */ + content: { + [key: string]: unknown; + }; + /** Effective From */ + effective_from: string | null; + /** Knowledge Mode */ + knowledge_mode: string; + /** Independent */ + independent: boolean; + /** Content Hash */ + content_hash: string; + /** Projection Version */ + projection_version: string; + /** Schema Binding */ + schema_binding: { + [key: string]: unknown; + }; + /** Sources */ + sources: components["schemas"]["muse____________________43"][]; + /** Planned Window */ + planned_window?: { + [key: string]: string; + } | null; + }; + /** 事实省略原因 */ + muse____________________45: { + /** Object Id */ + object_id: string; + /** Reason */ + reason: string; + }; + /** 世界查询结果 */ + muse____________________46: { + /** Work Id */ + work_id: string; + /** Directory Revision */ + directory_revision: number; + /** System Revision */ + system_revision: number; + /** Facts */ + facts: components["schemas"]["muse____________________44"][]; + /** Beliefs */ + beliefs: components["schemas"]["muse____________________44"][]; + /** Plans */ + plans: components["schemas"]["muse____________________44"][]; + /** Omitted */ + omitted: components["schemas"]["muse____________________45"][]; + }; + /** 事实提案结果 */ + muse____________________47: { + /** Proposal Id */ + proposal_id: string; + /** Revision */ + revision: number; + /** Proposal Hash */ + proposal_hash: string; + /** Work Id */ + work_id: string; + /** Object Id */ + object_id: string; + /** Expected Revision */ + expected_revision: number; + schema: components["schemas"]["muse__________________11"]; + /** Content */ + content: { + [key: string]: unknown; + }; + /** Independent */ + independent: boolean; + /** Sources */ + sources: { + [key: string]: unknown; + }[]; + /** Effective From */ + effective_from: string | null; + /** Knowledge Mode */ + knowledge_mode: string; + /** Known To */ + known_to: components["schemas"]["muse__________________14"][]; + /** Planned Window */ + planned_window: { + [key: string]: string; + } | null; + /** Decision */ + decision: string; + }; + /** 事实审阅结果 */ + muse____________________48: { + /** Review Id */ + review_id: string; + /** Review Hash */ + review_hash: string; + /** Snapshot */ + snapshot: { + [key: string]: unknown; + }; + proposal: components["schemas"]["muse____________________47"]; + }; + /** 节点目录请求 */ + muse____________________5: { + /** Command Id */ + command_id: string; + directory: components["schemas"]["muse____________________4"]; + }; + /** 档案保存请求 */ + muse____________________6: { + /** Command Id */ + command_id: string; + /** Work Id */ + work_id: string; + /** Expected Revision */ + expected_revision: number; + /** Content */ + content?: { + [key: string]: unknown; + }; + schema_binding: components["schemas"]["muse__________________4"]; + /** Changes */ + changes?: components["schemas"]["muse_________________7"][]; + }; + /** 新增章节请求 */ + muse____________________7: { + /** Command Id */ + command_id: string; + /** Work Id */ + work_id: string; + /** Chapter Id */ + chapter_id: string; + /** Title */ + title: string; + /** Expected Directory Revision */ + expected_directory_revision: number; + }; + /** 重排章节请求 */ + muse____________________8: { + /** Command Id */ + command_id: string; + /** Work Id */ + work_id: string; + /** Chapter Ids */ + chapter_ids: string[]; + /** Expected Directory Revision */ + expected_directory_revision: number; + }; + /** 探索保存请求 */ + muse____________________9: { + /** Draft Id */ + draft_id: string; + /** Title */ + title: string; + /** Content */ + content: string; + /** Expected Hash */ + expected_hash: string | null; + }; + /** 登录请求 */ + "muse______http____________-Input": { + /** + * Password + * Format: password + */ + password: string; + }; + /** 事件响应 */ + muse______http______________2: { + /** Event Id */ + event_id: string; + /** Sequence */ + sequence: number; + /** Event Type */ + event_type: string; + /** + * Occurred At + * Format: date-time + */ + occurred_at: string; + /** Payload */ + payload: { + [key: string]: unknown; + }; + }; + /** 会话响应 */ + muse______http______________3: { + /** Author Id */ + author_id: string; + }; + /** 步骤响应 */ + muse______http__________________1: { + /** Step Id */ + step_id: string; + /** State */ + state: string; + }; + /** 任务响应 */ + muse______http__________________2: { + /** Task Id */ + task_id: string; + state: components["schemas"]["muse__________________1"]; + /** Work Id */ + work_id: string | null; + /** Task Type */ + task_type: string; + /** Last Sequence */ + last_sequence: number; + /** Steps */ + steps: components["schemas"]["muse______http__________________1"][]; + }; + /** 会话授权请求 */ + muse______http____________________1: { + /** Command Id */ + command_id: string; + /** Step Id */ + step_id: string; + /** Stage */ + stage: string; + /** Initial Request Hash */ + initial_request_hash: string; + /** Max Model Calls */ + max_model_calls: number; + /** Max Tool Calls */ + max_tool_calls: number; + /** + * Valid Until + * Format: date-time + */ + valid_until: string; + }; + /** 原文授权请求 */ + muse______http____________________2: { + /** Command Id */ + command_id: string; + /** Source Version */ + source_version: string; + /** Content Hashes */ + content_hashes: string[]; + content_purpose: components["schemas"]["muse__________________2"]; + /** + * Retention Mode + * @enum {string} + */ + retention_mode: "temporary" | "archive" | "persistent"; + /** + * Valid Until + * Format: date-time + */ + valid_until: string; + /** Call Id */ + call_id?: string | null; + /** Call Request Hash */ + call_request_hash?: string | null; + }; + /** 原文租约请求 */ + muse______http____________________3: { + /** Authorization Id */ + authorization_id: string; + /** Command Id */ + command_id: string; + /** + * Retain Until + * Format: date-time + */ + retain_until: string; + /** Min Remaining */ + min_remaining: number; + }; + /** 原文归档请求 */ + muse______http____________________4: { + /** Authorization Id */ + authorization_id: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + read_task: { + parameters: { + query?: { + work_id?: string | null; + }; + header?: never; + path: { + task_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse______http__________________2"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_tasks: { + parameters: { + query?: { + work_id?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse______http__________________2"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + control_task: { + parameters: { + query?: never; + header?: never; + path: { + task_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse_________________-Input"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse___________________2"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + approve_role_session: { + parameters: { + query?: never; + header?: never; + path: { + task_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse______http____________________1"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + revoke_raw_retention: { + parameters: { + query?: never; + header?: never; + path: { + task_id: string; + authorization_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + approve_raw_retention: { + parameters: { + query?: never; + header?: never; + path: { + task_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse______http____________________2"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_raw_lease: { + parameters: { + query?: never; + header?: never; + path: { + task_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse______http____________________3"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + write_raw_bytes: { + parameters: { + query?: never; + header?: never; + path: { + task_id: string; + lease_id: string; + content_hash: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/octet-stream": string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + archive_raw_bytes: { + parameters: { + query?: never; + header?: never; + path: { + task_id: string; + lease_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse______http____________________4"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + cleanup_raw_lease: { + parameters: { + query?: never; + header?: never; + path: { + task_id: string; + lease_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + recover_raw_leases: { + parameters: { + query?: never; + header?: never; + path: { + task_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }[]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + read_evidence_receipt: { + parameters: { + query?: never; + header?: never; + path: { + task_id: string; + evidence_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + backfill_evidence_bytes: { + parameters: { + query: { + authorization_id: string; + }; + header?: never; + path: { + task_id: string; + evidence_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/octet-stream": string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + schema_command: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse____________________1"] | components["schemas"]["muse____________________2"] | components["schemas"]["muse____________________3"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + read_schema: { + parameters: { + query?: never; + header?: never; + path: { + schema_id: string; + version: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse___________________3"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + read_plan_nodes: { + parameters: { + query?: { + version?: number | null; + }; + header?: never; + path: { + work_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________24"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + save_plan_nodes: { + parameters: { + query?: never; + header?: never; + path: { + work_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse____________________5"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________25"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + read_work_progress: { + parameters: { + query?: never; + header?: never; + path: { + work_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________29"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_works: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse__________________17"][]; + }; + }; + }; + }; + read_work_form: { + parameters: { + query?: { + schema_id?: string | null; + base_version?: number | null; + }; + header?: never; + path: { + work_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________30"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + save_work_profile: { + parameters: { + query?: never; + header?: never; + path: { + work_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse____________________6"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________25"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + read_work_chapters: { + parameters: { + query?: never; + header?: never; + path: { + work_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse__________________19"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + append_chapter: { + parameters: { + query?: never; + header?: never; + path: { + work_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse____________________7"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________25"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + reorder_chapters: { + parameters: { + query?: never; + header?: never; + path: { + work_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse____________________8"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________25"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_explorations: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse___________________4"][]; + }; + }; + }; + }; + read_exploration: { + parameters: { + query?: never; + header?: never; + path: { + draft_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse___________________5"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + save_exploration: { + parameters: { + query?: never; + header?: never; + path: { + draft_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse____________________9"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse___________________5"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + check_exploration_handoff: { + parameters: { + query?: never; + header?: never; + path: { + draft_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse____________________10"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________32"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + read_exploration_selection: { + parameters: { + query?: never; + header?: never; + path: { + work_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________33"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + select_exploration: { + parameters: { + query?: never; + header?: never; + path: { + work_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse____________________11"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________25"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + read_plan_schema: { + parameters: { + query: { + schema_id: string; + base_version: number; + }; + header?: never; + path: { + work_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse__________________4"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_plans: { + parameters: { + query?: never; + header?: never; + path: { + work_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse__________________20"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_plan_candidates: { + parameters: { + query?: never; + header?: never; + path: { + work_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________34"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_author_plan_candidate: { + parameters: { + query?: never; + header?: never; + path: { + work_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse____________________12"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________25"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + read_plan_candidate: { + parameters: { + query?: { + version?: number | null; + }; + header?: never; + path: { + candidate_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________34"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + edit_plan_candidate: { + parameters: { + query?: never; + header?: never; + path: { + candidate_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse____________________13"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________25"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + read_plan: { + parameters: { + query?: never; + header?: never; + path: { + plan_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse__________________20"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + open_plan_review: { + parameters: { + query?: never; + header?: never; + path: { + candidate_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse__________________7"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________35"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + decide_plan_candidate: { + parameters: { + query?: never; + header?: never; + path: { + candidate_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse____________________14"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________25"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + read_usable_fine_outline: { + parameters: { + query?: never; + header?: never; + path: { + work_id: string; + chapter_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse__________________20"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + read_plan_form: { + parameters: { + query: { + schema_id: string; + base_version: number; + chapter_id?: string | null; + node_id?: string | null; + }; + header?: never; + path: { + work_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________36"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_document_history: { + parameters: { + query?: { + branch_id?: string; + }; + header?: never; + path: { + chapter_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________37"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + read_document: { + parameters: { + query?: { + branch_id?: string; + revision?: number | null; + }; + header?: never; + path: { + chapter_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________38"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + save_document: { + parameters: { + query?: never; + header?: never; + path: { + chapter_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse____________________15"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________25"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + restore_document: { + parameters: { + query?: never; + header?: never; + path: { + chapter_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse____________________16"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________25"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_writing_candidates: { + parameters: { + query?: { + branch_id?: string; + }; + header?: never; + path: { + chapter_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________41"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_author_candidate: { + parameters: { + query?: never; + header?: never; + path: { + chapter_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse____________________15"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________25"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + read_writing_candidate: { + parameters: { + query?: { + revision?: number | null; + }; + header?: never; + path: { + candidate_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________41"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + edit_writing_candidate: { + parameters: { + query?: never; + header?: never; + path: { + candidate_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse____________________17"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________25"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + open_writing_review: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse____________________18"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________42"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + decide_writing_candidate: { + parameters: { + query?: never; + header?: never; + path: { + candidate_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse____________________19"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________25"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + read_fact_schema: { + parameters: { + query: { + object_id: string; + schema_id: string; + base_version?: number; + instance_family?: "entity" | "relation"; + extension_version?: number | null; + }; + header?: never; + path: { + work_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse__________________11"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + query_world_state: { + parameters: { + query?: { + as_of?: string | null; + system_revision?: number | null; + view?: "author" | "reader" | "character"; + character_id?: string | null; + }; + header?: never; + path: { + work_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________46"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + propose_world_fact: { + parameters: { + query?: never; + header?: never; + path: { + work_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse____________________21"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________25"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + read_fact_proposal: { + parameters: { + query?: never; + header?: never; + path: { + proposal_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________47"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + open_fact_review: { + parameters: { + query?: never; + header?: never; + path: { + proposal_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse____________________22"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________48"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + decide_world_fact: { + parameters: { + query?: never; + header?: never; + path: { + proposal_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse____________________23"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse____________________25"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + read_task_events: { + parameters: { + query?: { + cursor?: number; + work_id?: string | null; + }; + header?: never; + path: { + task_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["___-Output"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + stream_task_events: { + parameters: { + query?: { + cursor?: number; + work_id?: string | null; + }; + header?: { + "last-event-id"?: number | null; + }; + path: { + task_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + read_health: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: string; + }; + }; + }; + }; + }; + read_readiness: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + }; + }; + read_session: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse______http______________3"]; + }; + }; + }; + }; + open_session: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["muse______http____________-Input"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["muse______http______________3"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + close_session: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: boolean; + }; + }; + }; + }; + }; +} diff --git a/web/src/界面/主题.css b/web/src/界面/主题.css new file mode 100644 index 0000000..8eba819 --- /dev/null +++ b/web/src/界面/主题.css @@ -0,0 +1,563 @@ +:root { + font-family: "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; + color: #30372e; + background: #f4f1e9; + font-synthesis: none; + --paper: #fffdf7; + --line: #dddccf; + --accent: #405e43; + --muted: #7d8073; +} +* { + box-sizing: border-box; +} +body { + margin: 0; + min-width: 320px; +} +button, +input { + font: inherit; +} +button { + border: 1px solid var(--line); + padding: 12px 19px; + background: var(--paper); + color: inherit; + border-radius: 5px; + cursor: pointer; + transition: + transform 0.12s, + background 0.12s; +} +button:active { + transform: scale(0.98); +} +button:disabled { + opacity: 0.55; + cursor: wait; +} +button.primary { + background: var(--accent); + color: #fffdf5; + border-color: var(--accent); +} +button:hover { + filter: brightness(0.97); +} +input { + border: 1px solid #d7d7c9; + background: #fffcf6; + color: #30372e; + border-radius: 5px; + padding: 13px 15px; + min-width: 0; +} +input:focus, +button:focus-visible, +a:focus-visible { + outline: 2px solid #789472; + outline-offset: 3px; +} +label { + font-size: 13px; + font-weight: 600; +} +h1 { + font-size: 32px; + letter-spacing: -0.9px; + line-height: 1.3; + font-weight: 550; + margin: 10px 0 16px; +} +h2 { + font-size: 18px; + font-weight: 550; + margin: 0 0 16px; +} +p { + line-height: 1.9; +} +.muted { + color: var(--muted); + font-size: 13px; +} +.eyebrow { + font-size: 11px; + font-weight: 600; + letter-spacing: 2.5px; + color: #77806a; + margin: 0; +} +.brand-mark { + display: inline-grid; + place-items: center; + width: 52px; + height: 52px; + border: 1px solid #687361; + color: #53644c; + border-radius: 14px; + font-size: 30px; + font-family: inherit; + font-style: italic; +} +.brand-mark.small { + width: 38px; + height: 38px; + font-size: 23px; + border-radius: 10px; +} +.connect-page { + max-width: 1120px; + min-height: 100dvh; + margin: auto; + display: grid; + grid-template-columns: 1.25fr 1fr; + align-items: center; + gap: 90px; + padding: 70px; +} +.connect-intro h1 { + font-size: 58px; + line-height: 1.45; + letter-spacing: 1px; + margin: 32px 0 20px; +} +.connect-intro > .eyebrow { + margin-top: 28px; +} +.connect-intro > p:last-child { + color: #7c8272; + line-height: 2.2; + font-size: 15px; +} +.connect-form { + background: var(--paper); + border: 1px solid var(--line); + border-radius: 9px; + padding: 45px; + box-shadow: 0 18px 50px #41472d08; +} +.connect-form h2 { + font-size: 25px; + margin-top: 12px; +} +.connect-form label { + display: block; + margin: 25px 0 9px; +} +.connect-form input, +.connect-form button { + width: 100%; +} +.connect-form button { + margin-top: 22px; +} +.loading-page { + min-height: 100dvh; + display: grid; + place-content: center; + gap: 20px; +} +.workbench { + display: grid; + grid-template-columns: 218px 1fr; + min-height: 100dvh; +} +.sidebar { + border-right: 1px solid var(--line); + padding: 31px 23px; + display: flex; + flex-direction: column; + background: #eeeee4; +} +.brand { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 60px; +} +.brand strong { + font-size: 23px; + font-weight: 550; + letter-spacing: 0.4px; + display: block; +} +.brand div > span { + display: block; + font-size: 10px; + color: var(--muted); + letter-spacing: 2px; + margin-top: 4px; +} +.nav-caption { + color: #929788; + font-size: 10px; + letter-spacing: 2px; + padding-left: 13px; + margin-bottom: 15px; +} +nav { + display: grid; + gap: 8px; +} +nav a { + padding: 13px; + color: #727969; + text-decoration: none; + border-radius: 5px; + font-size: 13px; + display: flex; + justify-content: space-between; +} +nav a.active { + background: #dde5d4; + color: #3f5a3b; +} +nav a > span { + font-size: 10px; + opacity: 0.55; + padding-top: 2px; +} +.sidebar-note { + margin-top: auto; + padding: 30px 3px 5px; + color: #68705e; + font-size: 11px; +} +.sidebar-note p { + font-size: 10px; + color: #9a9e90; + line-height: 1.8; +} +.status-dot { + display: inline-block; + width: 5px; + height: 5px; + border-radius: 50%; + background: #6f8c60; + margin-right: 7px; +} +.text-button { + border: 0; + padding: 8px 2px; + background: none; + text-align: left; + color: #959b8c; + font-size: 11px; +} +.workspace { + min-width: 0; +} +.topbar { + height: 72px; + border-bottom: 1px solid var(--line); + padding: 0 48px; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 12px; + color: #757b6d; +} +.topbar > .muted { + font-size: 10px; + letter-spacing: 1px; +} +.content { + max-width: 1400px; + padding: 43px 48px 64px; + margin: 0 auto; +} +.page-heading { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 35px; +} +.page-heading .muted { + margin: 0; +} +.page-number { + font-size: 10px; + color: #a0a492; + letter-spacing: 1px; + padding-top: 7px; +} +.task-search { + padding: 24px 28px; + background: #eeede4; + border: 1px solid var(--line); + border-radius: 7px; +} +.task-search > div { + display: grid; + grid-template-columns: 1fr auto; + gap: 12px; + margin-top: 12px; +} +.task-search input { + font-size: 13px; +} +.empty-state { + text-align: center; + padding: 73px 20px 60px; + color: #7e8574; +} +.empty-symbol { + font-size: 35px; + color: #a5ae94; + display: block; + margin-bottom: 20px; +} +.empty-state h2 { + color: #59674e; + font-size: 17px; + font-weight: 500; + letter-spacing: 0.5px; +} +.empty-state p { + font-size: 12px; + line-height: 2.2; + color: #9a9f90; +} +.task-columns { + display: grid; + grid-template-columns: 1.35fr 1fr; + gap: 27px; + margin-top: 26px; +} +.paper-panel { + background: var(--paper); + border: 1px solid var(--line); + border-radius: 7px; + padding: 28px; +} +.panel-heading { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} +.panel-heading h2 { + margin: 0; + font-size: 16px; +} +.state-pill { + padding: 5px 9px; + border: 1px solid #d5dfc9; + background: #edf1e5; + color: #617551; + font-size: 10px; + border-radius: 3px; +} +.mono { + font-family: ui-monospace, monospace; + font-size: 10px; + overflow-wrap: anywhere; +} +.step-list, +.event-list { + list-style: none; + padding: 0; + margin: 27px 0 0; +} +.step-list li { + display: flex; + gap: 20px; + padding: 19px 0; + border-top: 1px solid #e8e7de; +} +.step-number { + font-size: 11px; + color: #a3ab98; + padding-top: 3px; +} +.step-list strong { + font-size: 13px; + font-weight: 500; +} +.step-list li div > span { + display: block; + font-size: 11px; + color: #8a9380; + margin-top: 7px; +} +.event-panel { + padding: 28px 0; +} +.event-list li { + display: flex; + gap: 18px; + font-size: 12px; + line-height: 1.8; + padding: 11px 0; + border-bottom: 1px solid #e4e4d9; +} +.event-list time { + color: #9ca18f; + font-family: ui-monospace, monospace; + font-size: 10px; + padding-top: 2px; +} +.feedback { + padding: 15px 18px; + border: 1px solid #d7dfca; + background: #edf0e3; + color: #70815d; + font-size: 12px; + border-radius: 4px; + margin-top: 15px; +} +.feedback.error { + background: #f2eae1; + color: #8e5b3a; + border-color: #e3cfb9; +} +@media (max-width: 900px) { + .connect-page { + gap: 35px; + padding: 40px; + } + .connect-intro h1 { + font-size: 42px; + } + .workbench { + grid-template-columns: 175px 1fr; + } + .sidebar { + padding: 25px 15px; + } + .topbar { + padding: 0 26px; + } + .content { + padding: 32px 26px; + } + .task-columns { + grid-template-columns: 1fr; + } + .page-number { + display: none; + } +} +@media (max-width: 600px) { + .connect-page { + grid-template-columns: 1fr; + padding: 35px 22px; + gap: 26px; + } + .connect-intro h1 { + font-size: 34px; + margin: 20px 0 12px; + } + .connect-intro > p:last-child { + display: none; + } + .connect-form { + padding: 28px; + } + .workbench { + grid-template-columns: 1fr; + } + .sidebar { + border-right: 0; + border-bottom: 1px solid var(--line); + padding: 18px 22px; + display: block; + } + .brand { + margin: 0 0 17px; + } + .nav-caption, + .sidebar-note, + .text-button { + display: none; + } + nav { + display: flex; + } + nav a { + gap: 35px; + } + .topbar { + height: 55px; + padding: 0 22px; + } + .content { + padding: 29px 22px; + } + h1 { + font-size: 26px; + } + .task-search { + padding: 19px; + } + .task-search > div { + grid-template-columns: 1fr; + } + .empty-state { + padding-top: 45px; + } +} +@media (prefers-reduced-motion: reduce) { + * { + transition: none !important; + scroll-behavior: auto !important; + } +} + +.scope-filter { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 18px; +} +.scope-filter select { + font: inherit; + font-size: 12px; + color: #59674e; + padding: 9px 13px; + border: 1px solid var(--line); + border-radius: 4px; + background: #fffdf7; +} +.recent-tasks { + margin-top: 34px; +} +.recent-tasks ul { + list-style: none; + padding: 0; + margin-top: 20px; +} +.recent-tasks li { + border-top: 1px solid var(--line); +} +.recent-tasks li button { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + border: 0; + background: none; + padding: 21px 5px; + text-align: left; +} +.recent-tasks strong { + font-size: 14px; + font-weight: 500; +} +.recent-tasks small { + display: block; + color: #919985; + font-size: 11px; + margin-top: 8px; +} +.task-actions { + display: flex; + gap: 12px; + margin-top: 23px; +} +.task-actions button { + font-size: 12px; + padding: 9px 14px; +} diff --git a/web/src/界面/反馈状态.tsx b/web/src/界面/反馈状态.tsx new file mode 100644 index 0000000..ed2cc9a --- /dev/null +++ b/web/src/界面/反馈状态.tsx @@ -0,0 +1,16 @@ +export function Ui反馈状态({ + message, + error = false, +}: { + message: string; + error?: boolean; +}) { + return ( +
+ {message} +
+ ); +} diff --git a/web/tests/交互/退出会话.test.tsx b/web/tests/交互/退出会话.test.tsx new file mode 100644 index 0000000..6e332f3 --- /dev/null +++ b/web/tests/交互/退出会话.test.tsx @@ -0,0 +1,37 @@ +/** 组件级会话观察:传输替身不计真实登录旅程。 */ +import { afterEach, expect, it, vi } from "vitest"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { Ui应用 } from "../../src/应用/应用"; +import { Ui依赖装配 } from "../../src/应用/依赖装配"; + +afterEach(() => { + cleanup(); + vi.unstubAllGlobals(); +}); + +it("NC-ui-session-close:退出成功卸载任务并回到连接表单", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async (request: Request) => { + const path = new URL(request.url).pathname; + const data = path.endsWith("session") + ? request.method === "DELETE" + ? { closed: true } + : { author_id: "author-local" } + : []; + return new Response(JSON.stringify(data), { + headers: { "Content-Type": "application/json" }, + }); + }), + ); + render( + + + , + ); + fireEvent.click(await screen.findByRole("button", { name: "断开会话" })); + expect( + await screen.findByRole("button", { name: "进入工作台" }), + ).toBeTruthy(); + expect(screen.queryByRole("button", { name: "断开会话" })).toBeNull(); +});