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/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())