diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..458de1c --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,68 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main, "feat/**", "fix/**", "chore/**"] + workflow_dispatch: + +jobs: + docs-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: { python-version: "3.12" } + - run: git diff --check + - run: python scripts/check-doc-links.py + + backend-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: { python-version: "3.12" } + - run: pip install uv==0.9.24 + - run: uv sync --frozen + working-directory: backend + - run: uv run python -m compileall -q app + working-directory: backend + - run: uv run pytest + working-directory: backend + + service-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: { python-version: "3.12" } + - run: pip install uv==0.9.24 + - run: uv sync --frozen + working-directory: backend + - run: uv sync --frozen && uv run pytest + working-directory: server sync + - run: uv sync --frozen && uv run pytest + working-directory: community-server + - run: backend/.venv/bin/python scripts/phase3-isolated-smoke.py + + frontend-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: "22", cache: pnpm, cache-dependency-path: frontend/pnpm-lock.yaml } + - run: corepack enable && corepack prepare pnpm@10.28.0 --activate + - run: pnpm install --frozen-lockfile + working-directory: frontend + - run: pnpm test && pnpm type-check && pnpm build + working-directory: frontend + + rust-core: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: { components: rustfmt, clippy } + - run: cargo fmt --check && cargo test --lib --locked && cargo clippy --lib --locked -- -D warnings + working-directory: frontend/src-tauri diff --git a/README.md b/README.md index 8dab76f..004f188 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ > 本文件用于团队开发期间快速配置环境、启动项目并了解当前实现状态,不是正式的项目 README。 +> 第三阶段分支状态(2026-09-07):已加入 Tauri/Rust 原生 Vault 预览、Sync v1 服务原型、签名社区目录原型和七类社区入口。完整 Sidecar、Stronghold、生产插件隔离、同步客户端、升级回滚及三平台发布门禁仍未交付;详见[第三阶段实施与验收记录](docs/development/第三阶段实施与验收记录.md)。 + NotesAgent 是本地优先的 AI 笔记与知识库项目。当前可运行形态为 Vue/Vite Web 前端与 FastAPI AI Core:Markdown 和附件保存在本地 Vault,SQLite 管理元数据、全文索引、向量空间、搜索历史、AI 会话、任务、Agent Trace、多模态任务及运行诊断。AI 对话已接入知识库检索,会话与消息由后端持久化并供 Web 和桌面客户端共用。 截至 2026-09-06,第一阶段及第二阶段 A~F 的工程范围已经合并到 `main`。当前已完成真实 Workspace、混合检索与知识库问答、Agent/Tool/Permission、Skill/Plugin、MCP 配置与调用、模型提供商与路由、RAG Benchmark,以及本地 Embedding、音频转写和片段级声纹聚类。Tauri/Rust Host、Stronghold、原生多 Vault 文件系统、生产级 MCP 沙箱和 Sync Server 尚未接入。 @@ -15,7 +17,8 @@ NotesAgent/ ├── frontend/ Vue 3 + TypeScript + Vite 前端 ├── backend/ FastAPI AI Core、SQLite 与本地模型运行管理 ├── docs/ 架构、契约、开发说明、协作规范与问题复盘 -└── server sync/ 云同步服务预留目录,当前未实现 +├── community-server/ 社区目录、签名发行与审核原型 +└── server sync/ 独立 Sync v1 服务原型 ``` ## 当前能力 diff --git a/docs/README.md b/docs/README.md index a4ff94d..7e70ef0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,7 +4,7 @@ 本目录集中保存团队开发期间需要长期维护的架构、接口、实现、协作和问题复盘文档。文档按用途分类,避免设计约束、开发记录与故障复盘混放。 -当前文档基线为 2026-09-06:第一阶段和第二阶段 A~F 工程范围已经合并到 `main`,当前可运行形态仍为 Vue/Vite Web 前端与 FastAPI AI Core。Tauri/Rust Host、Stronghold、原生多 Vault 文件系统、生产级 MCP 沙箱和 Sync Server 尚未接入。 +当前第三阶段分支已实现 Tauri/Rust 原生 Vault、Sync v1 和签名社区目录的可测试原型;它仍缺少 Sidecar、Stronghold、生产插件隔离、完整同步客户端和发布门禁,不能作为第三阶段发布候选。状态与证据见[第三阶段实施与验收记录](development/第三阶段实施与验收记录.md)。 仓库入口文档:[项目 README](../README.md)、[前端 README](../frontend/README.md)、[后端 README](../backend/README.md)。 @@ -31,11 +31,16 @@ - [第二阶段接口契约](contracts/第二阶段接口契约-开发版.md) - [前端页面需求说明](contracts/前端页面需求说明-开发版.md) - [Tauri / Rust 桌面客户端需求说明(第三阶段,计划)](contracts/Tauri-Rust桌面客户端需求说明-第三阶段.md) +- [Rust Host v1 预览契约](contracts/Host-v1契约.md) +- [Sync Protocol v1 契约](contracts/Sync-v1契约.md) +- [Community Catalog v1 契约](contracts/Community-v1契约.md) 运行中的后端以 `/openapi.json` 为机器可读事实来源。接口契约用于描述设计意图、联调约束和实现状态;两者不一致时,应先确认代码行为,再在同一个 PR 中同步修正文档或实现。 ## development:开发说明 +- [第三阶段实施与验收记录](development/第三阶段实施与验收记录.md) + - [前端构建分块优化开发说明](development/前端构建分块优化开发说明.md) - [工作区后台索引与保存开发说明](development/工作区后台索引与保存开发说明.md) diff --git a/docs/architecture/第三阶段实施规划.md b/docs/architecture/第三阶段实施规划.md index 0e24f9b..bde3e75 100644 --- a/docs/architecture/第三阶段实施规划.md +++ b/docs/architecture/第三阶段实施规划.md @@ -2,6 +2,8 @@ > 2026-09-06 后续修复补充:本地扩展安装登记、摘要复核恢复、ZIP 卸载清理以及工作区 context_menu/toolbar 入口已在第二阶段补丁实现。下文原始基线仍保留用于追踪;第三阶段应在此基础上完成迁移、签名、升级事务及生产隔离,不重复建设基础登记。真实质量与厂商验收仍未闭环。 +> 2026-09-07 实施进展:分支已交付 Rust 原生 Vault、Sync v1、签名社区目录和社区前端的 Alpha 原型,细粒度状态与未完成门禁见[第三阶段实施与验收记录](../development/第三阶段实施与验收记录.md)。本规划的完整退出条件尚未满足。 + 基线日期:2026-09-06。状态:**计划,尚未交付第三阶段**。本规划以当前第二阶段代码及本地验收记录为起点;本次用户明确要求将各社区、Sync Server、Tauri / Rust 容器纳入第三阶段。未勾选项均为待实施,不以文档编写或接口命名代替实现。 ## 1. 阶段目标与完成口径 diff --git a/docs/development/第三阶段实施与验收记录.md b/docs/development/第三阶段实施与验收记录.md index 6e3eddf..911f526 100644 --- a/docs/development/第三阶段实施与验收记录.md +++ b/docs/development/第三阶段实施与验收记录.md @@ -9,26 +9,31 @@ | 同步入口 | 冻结 `server sync/`,生产 PostgreSQL + S3 | SQLite + 磁盘仅用于隔离协议测试 | | Sync v1 | CAS、幂等、设备撤销、对象上传和历史已实现 | [契约](../contracts/Sync-v1契约.md),完整生产验收待执行 | | 历史保留 | 永久保留 Revision、tombstone 和历史对象 | 暂不 GC,容量达到配额时拒绝上传;可逆地增加保留策略 | -| Host | `frontend/src-tauri/`,Rust 文件日志及 outbox | Tauri 与 Sidecar 生命周期、完整交互待验收 | +| Host | `frontend/src-tauri/`,Rust 文件日志、outbox、原生菜单与 Web/桌面适配 | [Host v1](../contracts/Host-v1契约.md),Sidecar/Stronghold/完整交互待验收 | +| Community | Ed25519 签名目录、七类校验和前端来源入口 | [Community v1](../contracts/Community-v1契约.md),升级事务与生产运营待完成 | | 平台隔离 | 未证明 OS 沙箱的第三方进程默认禁用 | 能力许可不等同 OS 沙箱,不以用户点击绕过 | | 发布 | 不启用自动更新与安装包分发 | 签名、迁移、恢复、三平台实机尚未通过 | ## 已执行验证 +- 后端:887 项 pytest 通过;新增 Web/桌面单写入者门禁测试。 +- 前端:87 个文件、471 项 Vitest 通过;TypeScript 检查与 Vite 生产构建通过,保留既有大 chunk 提示。 - Sync v1:18 项隔离测试通过,Windows / Python 3.13.9 / SQLite / 磁盘对象 Fixture。包括双线程并发 CAS;不是 PostgreSQL 多 Worker 压测。 -- 依赖:后端和前端 frozen 安装成功;Sync 独立 uv.lock 已生成。 +- Community v1:13 项隔离测试通过;前端使用 Python 生成的真实 Ed25519 向量互验。 +- Rust:5 项文件恢复测试、clippy `-D warnings`、Tauri desktop 编译检查和 Windows GNU 开发构建通过。没有安装包或实机 UI 证据。 +- 文档相对链接、`git diff --check` 及 AI Core / Sync / Community 隔离冒烟通过。Gitea 工作流已加入,但未推送,因此无远端 Runner 证据。 - 真实模型、个人 Vault、真实音频和凭据未作为测试输入。 ## 完整范围跟踪 | 里程碑 | 状态 | 未满足退出条件 | | --- | --- | --- | -| M0 | 进行中 | 社区签名契约、平台原型、负责人/预算确认、完整 Host 原型 | -| M1 | 待开始 | 原生编辑完整闭环、Sidecar、Stronghold 迁移、菜单与多窗口 | -| M2 | 待开始 | 社区与安装升级事务、真实包全生命周期、安全隔离 | +| M0 | 进行中 | 平台隔离决策、负责人/预算确认、Sidecar/Stronghold 契约 | +| M1 | 进行中 | Sidecar、Stronghold 迁移、多窗口、文件监听和完整桌面交互 | +| M2 | 进行中 | 安装升级/回滚事务、真实包全生命周期和生产 OS 隔离 | | M3 | 进行中 | PostgreSQL/MinIO 运行与备份恢复、运维和负载门禁 | | M4 | 待开始 | Rust 上传/拉取循环、冲突 UI、两台实机离线同步 | -| M5 | 待开始 | 七类社区、OCR、质量专项和主题/WebView 交互矩阵 | +| M5 | 进行中 | 声明式候选尚未接 Runtime;OCR、质量专项和主题/WebView 交互矩阵 | | M6 | 待开始 | 安装更新签名、升级迁移、三平台和性能门禁 | 本机未发现 Docker 命令;Compose 尚未运行。真实厂商调用需获准账号与费用上限,音频质量需有授权且带标注的数据,当前均未执行。不能以这些外部条件缺失解释尚未实现的普通工程范围。 diff --git a/docs/guides/代码注释与TODO约定.md b/docs/guides/代码注释与TODO约定.md index c5125d1..7bd5dab 100644 --- a/docs/guides/代码注释与TODO约定.md +++ b/docs/guides/代码注释与TODO约定.md @@ -33,10 +33,10 @@ | 领域 | 当前边界 | | --- | --- | | Agent / Streaming | 运行事件仍在进程内保存,后续需要持久化、`Last-Event-ID` 和断线重连 | -| Security | 本地主密钥目前保存在数据目录,桌面端接入后迁移到系统凭据库 | -| Extension | 扩展安装状态尚未持久化;MCP Host、进程隔离、签名与来源校验属于第二阶段 | +| Security | 本地主密钥目前保存在数据目录;Rust Vault 文件服务已接入,Stronghold 迁移尚未完成 | +| Extension | 安装状态已持久化、社区发行签名原型已接入;生产 OS 隔离和桌面 Extension Manager 尚未完成 | | AI Core | 音频转写当前只读取文本或 Host 预生成旁路文本,后续接入本地 ASR 队列 | -| Desktop | Web Workspace 已连接 FastAPI 单 Vault;后续由 Tauri IPC 增加原生目录选择、多 Vault 和文件监听 | +| Desktop | Tauri IPC 已支持单个原生 Vault 的选择、版本保存和恢复;最近列表、多窗口、监听、Sidecar 与同步循环尚未完成 | | Editor / Chat | 待补文件冲突合并及受控链接对话框;会话持久化已接入后端 SQLite | | Performance | Shiki 已复用单例,后续按首屏指标评估延迟加载或 Web Worker | diff --git a/scripts/check-doc-links.py b/scripts/check-doc-links.py new file mode 100644 index 0000000..6ed97a0 --- /dev/null +++ b/scripts/check-doc-links.py @@ -0,0 +1,40 @@ +"""检查仓库 Markdown 相对链接;忽略远程 URL、页内锚点与示例占位。""" + +from __future__ import annotations + +import re +from pathlib import Path +from urllib.parse import unquote + +ROOT = Path(__file__).resolve().parents[1] +LINK = re.compile(r"(? int: + failures: list[str] = [] + for document in ROOT.rglob("*.md"): + if any(part in {".build", ".venv", "node_modules", "target"} for part in document.parts): + continue + source = document.read_text(encoding="utf-8") + for raw in LINK.findall(source): + target = raw.strip().strip("<>").split()[0] + if not target or target.startswith(("#", "http://", "https://", "mailto:")): + continue + path = unquote(target.split("#", 1)[0]) + resolved = (document.parent / path).resolve() + try: + resolved.relative_to(ROOT) + except ValueError: + failures.append(f"{document.relative_to(ROOT)}: 链接越出仓库: {target}") + continue + if not resolved.exists(): + failures.append(f"{document.relative_to(ROOT)}: 不存在: {target}") + if failures: + print("\n".join(failures)) + return 1 + print("Markdown 相对链接检查通过") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/phase3-isolated-smoke.py b/scripts/phase3-isolated-smoke.py new file mode 100644 index 0000000..93bf3bb --- /dev/null +++ b/scripts/phase3-isolated-smoke.py @@ -0,0 +1,61 @@ +"""在临时目录启动三个 ASGI 应用,不读取仓库 data 或调用外部服务。""" + +from __future__ import annotations + +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def main() -> int: + with tempfile.TemporaryDirectory(prefix="notesagent-phase3-") as temporary: + root = Path(temporary) + os.environ.update({ + "APP_DATA_DIR": str(root / "backend"), + "APP_DB_PATH": str(root / "backend" / "app.db"), + "APP_VAULT_PATH": str(root / "vault"), + "APP_ATTACHMENTS_PATH": str(root / "backend" / "attachments"), + }) + sys.path.insert(0, str(ROOT / "backend")) + from fastapi.testclient import TestClient + from app.main import app as core + + with TestClient(core) as client: + assert client.get("/health").json()["status"] == "ok" + assert client.get("/api/status").status_code == 200 + + def project_python(project: Path) -> Path: + candidates = [project / ".venv" / "Scripts" / "python.exe", project / ".venv" / "bin" / "python"] + return next(path for path in candidates if path.exists()) + + sync_code = """from pathlib import Path +from fastapi.testclient import TestClient +from sync_server.app import create_app +from sync_server.database import Database +from sync_server.storage import DiskObjects +root=Path(r'%s') +with TestClient(create_app(Database('sqlite:///'+str(root/'sync.db')),DiskObjects(root/'objects'),root/'staging')) as c: + assert c.get('/ready').json()=={'status':'ready','schema':1} + assert c.get('/sync/v1/handshake').json()['protocol']==1 +""" % root + subprocess.run([project_python(ROOT / "server sync"), "-c", sync_code], cwd=ROOT / "server sync", check=True) + + community_code = """from pathlib import Path +from fastapi.testclient import TestClient +from community.app import Registry,create_app +root=Path(r'%s') +with TestClient(create_app(Registry(root/'community.db'))) as c: + assert c.get('/health').json()['status']=='ok' + assert c.get('/catalog/v1/sources').json()['schema_version']==1 +""" % root + subprocess.run([project_python(ROOT / "community-server"), "-c", community_code], cwd=ROOT / "community-server", check=True) + print("AI Core、Sync v1、Community v1 隔离冒烟通过") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())