From b2f6d8d7d22b1911f7c4501171654f3a5ab82325 Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Mon, 19 Oct 2026 10:13:58 +0800 Subject: [PATCH] =?UTF-8?q?README=E3=80=81=E9=83=A8=E7=BD=B2=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=E8=A1=A5=E4=B8=8A=E7=94=A8=20PostgreSQL=EF=BC=9AAGENT?= =?UTF-8?q?LAB=5FDATABASE=5FURL=E3=80=81=E6=96=AD=E7=82=B9=E3=80=81?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E6=B1=A0=EF=BC=88=E5=90=AB=20PgBouncer?= =?UTF-8?q?=EF=BC=89=E3=80=81=E5=92=8C=20SQLite=20=E4=B8=80=E8=87=B4?= =?UTF-8?q?=E7=9A=84=E9=82=A3=E5=87=A0=E6=9D=A1=E3=80=81=E4=BB=8E=20SQLite?= =?UTF-8?q?=20=E5=AF=BC=E5=85=A5=E7=9A=84=E6=AD=A5=E9=AA=A4=EF=BC=88copy-t?= =?UTF-8?q?o-postgres=EF=BC=8C=E5=85=88=E5=81=9C=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E3=80=81=E5=85=88=20--dry-run=EF=BC=89=E3=80=81=E5=A4=87?= =?UTF-8?q?=E4=BB=BD=EF=BC=88pg=5Fdump=20=E5=8A=A0=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=EF=BC=89=E3=80=81=E5=A4=9A=E5=8F=B0=E6=9C=BA?= =?UTF-8?q?=E5=99=A8=E8=A6=81=E6=8A=8A=E6=95=B0=E6=8D=AE=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E6=8C=82=E5=9C=A8=E5=85=B1=E4=BA=AB=E5=AD=98=E5=82=A8=E7=9A=84?= =?UTF-8?q?=E5=90=8C=E4=B8=80=E4=B8=AA=E8=B7=AF=E5=BE=84=E4=B8=8A=EF=BC=8C?= =?UTF-8?q?=E4=BB=A5=E5=8F=8A=E5=AE=9E=E8=AF=9D=EF=BC=9A=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E9=87=8C=E7=9A=84=20SQLite=20=E6=96=87?= =?UTF-8?q?=E4=BB=B6=EF=BC=88=E8=AE=B0=E5=BD=95=E8=A1=A8=E5=8E=9F=E5=9C=B0?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E5=86=99=E3=80=81=E4=B8=8A=E4=BC=A0=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E7=89=88=E6=9C=AC=E5=BA=93=E7=9A=84=20flock=EF=BC=89?= =?UTF-8?q?=E5=9C=A8=E7=BD=91=E7=BB=9C=E6=96=87=E4=BB=B6=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E4=B8=8A=E5=B9=B6=E5=8F=91=E5=86=99=E4=B8=8D=E5=AE=89=E5=85=A8?= =?UTF-8?q?=EF=BC=8C=E7=9C=BC=E4=B8=8B=E4=BB=8D=E6=8C=89=E5=8D=95=E8=BF=9B?= =?UTF-8?q?=E7=A8=8B=E9=83=A8=E7=BD=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 依赖与锁文件:[postgres] 里装的是什么、为什么 langgraph-checkpoint 单独卡小版本、checkpoint-postgres 锁在 3.1.2;数据位置:PostgreSQL 时不再用 agentlab.db / checkpoints.db;开发与验证:scripts/test-postgres.sh - environment.yml 带上 [postgres];.env.example 补应用库一节 - deploy.sh:数据目录里没有 agentlab.db(库在 PostgreSQL)时,认 .secret_key、uploads/ 也算 AgentLab 的数据目录,不再拒绝改属主 - 断点库的连接串不带只给 asyncpg 看的参数(prepared_statement_cache_size 之类,psycopg 不认);地址规范化、启动日志不带密码的测试 --- .env.example | 11 ++++ README.md | 90 ++++++++++++++++++++++++++++--- backend/app/db/urls.py | 4 +- backend/tests/test_db_portable.py | 28 ++++++++++ deploy/README.md | 34 +++++++++++- deploy/deploy.sh | 6 ++- environment.yml | 3 +- 7 files changed, 163 insertions(+), 13 deletions(-) diff --git a/.env.example b/.env.example index cede4581..76e377ba 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,17 @@ # AGENTLAB_PORT=8000 # AGENTLAB_WEB_PORT=5273 +# ---- 应用库 ---- +# 不设:数据目录里的 SQLite(data/agentlab.db,断点 data/checkpoints.db),单机单进程。 +# 设成 PostgreSQL 时整个应用库和断点都放进去(要 [postgres] 额外依赖,environment.yml 已带);数据目录照样要有。 +# 从现有的 SQLite 数据目录搬过去:python -m app.db copy-to-postgres data(README「用 PostgreSQL」) +# AGENTLAB_DATABASE_URL=postgresql+asyncpg://agentlab:密码@127.0.0.1:5432/agentlab +# 断点放另一个库时才设(不设就和应用库同一个库) +# AGENTLAB_CHECKPOINT_URL=postgresql://agentlab:密码@127.0.0.1:5432/agentlab_checkpoints +# 连接池:每个进程常驻几条、高峰再多开几条;设成 0 不用连接池(前面有 PgBouncer 时) +# AGENTLAB_DB_POOL_SIZE=10 +# AGENTLAB_DB_MAX_OVERFLOW=20 + # ---- 沙箱 ---- # auto(挑第一个已就绪的:microvm → bubblewrap → seatbelt → local) # 可选值:microvm | seatbelt | bubblewrap | local | off diff --git a/README.md b/README.md index 2c2f2f2c..4d1ba2e8 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ - [快速开始](#快速开始) · [生产部署](#生产部署) · [核心能力](#核心能力) · [受限动态编排](#受限动态编排模板定骨架agent-填关节) - [可点击证据](#可点击证据报告里的每个数都点得开出处) · [评测与口径账本](#回归评测与口径账本) · [应用](#应用已发布版本的使用面) · [身份与权限](#身份与权限) · [能力登记与端点](#能力登记与端点) - [数据目录](#数据目录让助手和-agent-看懂业务数据) · [工作流形态](#工作流形态) · [节点类型](#节点类型) · [架构](#架构) -- [关键设计决策](#关键设计决策) · [运行环境](#运行环境) · [常见问题](#常见问题) · [开发与验证](#开发与验证) +- [关键设计决策](#关键设计决策) · [运行环境](#运行环境) · [用 PostgreSQL](#用-postgresql) · [常见问题](#常见问题) · [开发与验证](#开发与验证) --- @@ -64,7 +64,8 @@ Mock 不会写引用标记:探索运行里报告撰写节点照样跑完,但 http://127.0.0.1:8000 。前端产物不存在或比源码旧时才重新构建,`--build` / `--no-build` 强制构建或跳过。 `--port`(或 `AGENTLAB_PORT`)改端口;`--host` 监听所有网卡,默认同样**不开登录**,多人使用请开登录 ([身份与权限](#身份与权限))。 -数据目录默认为仓库下的 `data/`,`AGENTLAB_DATA_DIR` 可改。只能单进程运行:运行管理器在进程内、数据库是 SQLite;后台任务 +数据目录默认为仓库下的 `data/`,`AGENTLAB_DATA_DIR` 可改。应用库默认是数据目录里的 SQLite,也可以换成 PostgreSQL +([用 PostgreSQL](#用-postgresql)),但目前仍只能单进程运行:运行管理器在进程内;后台任务 (探查、起草、剖析、评测、影子起草、口径比对与召回)的排队、限流和按需探查的锁也都在进程内,多进程部署要先换成库里的锁。 上传表格的版本存储同样只支持单进程运行(不要给 uvicorn 加 `--workers`):启动时会锁住数据目录,同一个数据目录 起了第二个进程时,后起的那个不做任何清理,上传、启用版本等写操作一律返回 503。 @@ -739,8 +740,8 @@ flowchart TB B5["sandbox
microVM / Seatbelt / bubblewrap"] end subgraph ST["存储"] - S1[("agentlab.db
业务数据")] - S2[("checkpoints.db
执行断点")] + S1[("agentlab.db 或 PostgreSQL
业务数据")] + S2[("checkpoints.db 或同一个 PostgreSQL
执行断点")] S3[("artifacts/
内容寻址工件库")] end FE <-->|REST + WebSocket| BE @@ -991,6 +992,7 @@ sudo setfacl -m u:$USER:rw /dev/kvm # 当前会话立即可用 pip install -e 'backend[docs]' -c backend/requirements.lock # PDF / Word / PowerPoint / Excel 解析 pip install -e 'backend[db]' -c backend/requirements.lock # MySQL / PostgreSQL / Oracle 驱动 pip install -e 'backend[microvm]' -c backend/requirements.lock # microVM 沙箱 +pip install -e 'backend[postgres]' -c backend/requirements.lock # 应用库用 PostgreSQL(asyncpg、psycopg 3、官方断点库) pip install -e 'backend[dev]' -c backend/requirements.lock # pytest ``` @@ -1001,8 +1003,16 @@ pip install -e 'backend[dev]' -c backend/requirements.lock # pytest 装包时以它为约束,所以换一台机器装到的也是同一套。 LangGraph / LangChain 一族在 `pyproject.toml` 里只放行到下一个小版本:引擎直接依赖 -它们的断点格式、interrupt / resume 和流式事件,这些在小版本里也改过行为。aiosqlite -与 SQLAlchemy 同理——`db/base.py` 给 aiosqlite 打了补丁,连接池断开连接时会走到它。 +它们的断点格式、interrupt / resume 和流式事件,这些在小版本里也改过行为。断点格式本身在 +`langgraph-checkpoint` 里(langgraph 只要求它 <5,所以单独卡住);SQLite、PostgreSQL 两个断点库 +(`langgraph-checkpoint-sqlite`、`langgraph-checkpoint-postgres`)都按它读写,`copy-to-postgres` 搬断点也靠两边是同一版格式。 +`langgraph-checkpoint-postgres` 3.1.3 起要 `langgraph-checkpoint` 4.3,锁在 3.1.2,升级时三个一起动。aiosqlite +与 SQLAlchemy 同理——`db/base.py` 给 aiosqlite 打了补丁,连接池断开连接时会走到它;应用库换成 PostgreSQL 之后 +补丁照样打着:上传表格、手工登记的 SQLite 数据源还走 aiosqlite。 + +`[postgres]` 里是应用库用 PostgreSQL 时要的:SQLAlchemy 这边走 asyncpg(和 `[db]` 里连业务库的是同一个包), +断点库走官方的 `langgraph-checkpoint-postgres`,它只认 psycopg 3 和 `psycopg-pool`。psycopg 带 `[binary]`: +wheel 里自带 libpq,镜像和本机都不用另装 PostgreSQL 客户端。 升级依赖要主动做,改完跑一遍测试再提交: @@ -1010,7 +1020,7 @@ LangGraph / LangChain 一族在 `pyproject.toml` 里只放行到下一个小版 ./scripts/lock-deps.sh --upgrade-package langgraph # 升一个包(连带它必须跟着动的) ./scripts/lock-deps.sh --upgrade # 全部升到 pyproject 允许的最新 ./scripts/lock-deps.sh # 只加了新依赖:补上它,已锁的不动 -pip install -e 'backend[microvm,dev,db,docs]' -c backend/requirements.lock # 装到当前环境 +pip install -e 'backend[microvm,dev,db,docs,postgres]' -c backend/requirements.lock # 装到当前环境 ``` 重新生成锁文件需要 [uv](https://docs.astral.sh/uv/);装依赖本身只用 pip。 @@ -1020,7 +1030,65 @@ pip install -e 'backend[microvm,dev,db,docs]' -c backend/requirements.lock # 全部位于 `data/`:`agentlab.db` 业务数据、`checkpoints.db` 执行断点、 `artifacts/` 工件库、`workspace/` 沙箱文件、`uploads/` 上传文件、`records/` 填报登记的记录表、`files/` 表单附件、 `.secret_key` 加密密钥。 -删除 `data/` 即恢复出厂设置。 +删除 `data/` 即恢复出厂设置。应用库用 PostgreSQL 时(下一节)不再用 `agentlab.db`、`checkpoints.db`,业务数据和断点 +都在 PostgreSQL 里;别的照旧在 `data/`,数据目录照样要有、要备份。 + +### 用 PostgreSQL + +缺省的应用库是数据目录里的 SQLite:单机单进程,什么都不用装。要把应用库(工作流、运行、事件、审批、待办、数据目录、 +评测……)和执行断点放进 PostgreSQL,设一项: + +```bash +AGENTLAB_DATABASE_URL=postgresql+asyncpg://agentlab:<密码>@127.0.0.1:5432/agentlab # postgresql:// 也认 +``` + +- 要 `[postgres]` 额外依赖(`environment.yml`、Docker 镜像都已带上);库要先建好(`CREATE DATABASE agentlab`), + 表由服务启动时建,老版本建的库启动时补列补索引,同 SQLite。启动日志第一句写用的是哪种库、哪台主机哪个库(不写密码)。 +- 断点(LangGraph 的 checkpoints、checkpoint_blobs、checkpoint_writes、checkpoint_migrations 四张表,不和应用的表重名) + 缺省放在同一个库;要放别处设 `AGENTLAB_CHECKPOINT_URL=postgresql://…`。 +- 连接池:每个进程常驻 `AGENTLAB_DB_POOL_SIZE`(缺省 10)条、高峰再多开 `AGENTLAB_DB_MAX_OVERFLOW`(缺省 20)条,取连接前探活, + 30 分钟换一次;断点库另有一个小池子。前面有 PgBouncer(事务模式)时:`AGENTLAB_DB_POOL_SIZE` 设成 0(不用连接池)、 + 地址后加 `?prepared_statement_cache_size=0`;断点库用预备语句,要 PgBouncer 1.21 以上开 `max_prepared_statements`, + 或者用 `AGENTLAB_CHECKPOINT_URL` 让它直连 PostgreSQL。 +- 和 SQLite 上的行为一致:字符串列不限长(SQLite 本来就不查长度,模型里写的长度只是示意)、整数列是 BIGINT、JSON 列存原文 + (PostgreSQL 的 json,不是 JSONB:JSONB 会重排对象里的键)、时间存不带时区的 UTC、排序时可空列的 NULL 照 SQLite 的位置 + (升序在前、降序在后)。两处 SQLite 照存而 PostgreSQL 不收的值会被换掉:JSON 里的 NaN / Infinity 写成 null,字符串里的 + NUL 字符去掉。 +- `AGENTLAB_DATA_DIR` 照旧要设:工件、上传表格的版本库、原件、附件、记录表、沙箱工作区、`.secret_key` 都还是文件。 + +**从 SQLite 搬过来。** `python -m app.db copy-to-postgres` 把数据目录里的 `agentlab.db` 和 `checkpoints.db` 导进 PostgreSQL, +停在审批上的、中断了的运行导过去之后照样能接着跑: + +```bash +# 1. 停服务(导入会检查:来源旁边还留着 agentlab.db-wal / checkpoints.db-wal,或者目标库上已经有服务连着,就拒绝) +# 2. 备份数据目录(停着服务拷一份) +# 3. 只核对:数来源有多少、目标上已有多少,什么都不写 +cd backend && python -m app.db copy-to-postgres ../data postgresql+asyncpg://agentlab:<密码>@127.0.0.1:5432/agentlab --dry-run +# 4. 导入:建表、按外键先后逐表分批插入,结束时逐表按主键核对、断点按条数核对,有没导进去的退出码非零 +cd backend && python -m app.db copy-to-postgres ../data postgresql+asyncpg://agentlab:<密码>@127.0.0.1:5432/agentlab +# 5. 设 AGENTLAB_DATABASE_URL(AGENTLAB_DATA_DIR 还指着原来的数据目录),起服务 +``` + +- 目标地址不给就用 `AGENTLAB_DATABASE_URL`;`--checkpoint-url` 把断点导到另一个库;`--work-dir` 放快照(要留出和两个库一样大的 + 空间,缺省系统临时目录)。读的是 SQLite 备份接口拷出的一份快照,在快照上先补到当前的表结构,来源的内容不改。 +- 重跑是幂等的:目标上已有同一主键(或撞了别的唯一约束)的行一律不动,只补上还没有的,不重复、也不覆盖——所以导入之后又在 + SQLite 上改过的行,重跑不会带过去;要整份重来就清空目标库再导。断点同理,已有的不重写。 +- 能查到的只有上面两样:服务连着别的数据目录、另一台机器上的服务连着目标库,查不出来,要自己保证。来源旁边的 -wal 是上次 + 强杀留下的,确认服务已停后加 `--assume-stopped`。 +- 导完 `agentlab.db` 留在数据目录里也没关系(可以当回退用):它照样算 AgentLab 自己的文件,不能登记成数据源。 + +**备份。** PostgreSQL 用 `pg_dump`(应用表和断点在同一个库里就一次带上),数据目录照旧整份拷。两样最好在停服务时一起做; +不停服务的话先 dump 库、再拷数据目录:工件、原件按内容寻址、只增不改,库里引用到的文件拷数据目录时都在。 + +**多台机器。** 应用库换成 PostgreSQL 之后,几台机器要用同一份数据目录:放在共享存储(NFS 之类)上,每台挂在**同一个绝对路径** +(库里记着上传表格各版本库文件的绝对路径)。文件都是先写临时文件再改名,读的一方看不到写了一半的。但要说实话:数据目录里还有 +SQLite 文件——上传表格的各版本库(`uploads/tables/`,发布后只读,读没问题)、填报的记录表(`records/*.db`,原地追加写)、 +沙箱里代码自己建的库——**SQLite 文件放在网络文件系统上并发写是不安全的**(文件锁在 NFS 上不可靠),上传表格的版本存储启动时 +取的 flock 在 NFS 上也不一定管用。所以眼下即使换了 PostgreSQL,仍按单进程部署([生产部署](#生产部署)那一段的限制都还在); +多进程、多台机器要等这些写入收归一个进程或换成库里的锁之后。 + +**测试。** `scripts/test-postgres.sh` 用本机 docker 已有的 `postgres:16` 镜像起一个临时容器(只绑 127.0.0.1 的随机端口,跑完删掉), +在上面跑后端全量;参数原样交给 pytest。只对 SQLite 成立的测试标了 `sqlite_only`,在 PostgreSQL 上跳过。 --- @@ -1082,6 +1150,12 @@ CHECK_DATA_SRC=<数据目录> node scripts/check-all.mjs --stacks 2 --lanes 12 node scripts/check-all.mjs evidence publish ``` +应用库换成 PostgreSQL 跑同一套(起临时的 `postgres:16` 容器,跑完删掉,见[用 PostgreSQL](#用-postgresql)): + +```bash +PYTHON=$(conda run -n agentlab which python) ./scripts/test-postgres.sh +``` + pytest 前面的 `env -u` 不能省:没配 Key 的 provider 会退回环境变量里的密钥,启动时的 seed 也会拿它种一个 provider——环境里留着真密钥,测试就可能真的去调接口。 diff --git a/backend/app/db/urls.py b/backend/app/db/urls.py index c1b8855f..62064ac9 100644 --- a/backend/app/db/urls.py +++ b/backend/app/db/urls.py @@ -15,6 +15,8 @@ _CHECKPOINT_SCHEMES = {"postgresql", "postgres", "postgresql+psycopg", "postgres #: asyncpg 和 libpq 对 SSL 的参数名不一样:asyncpg 是 ssl=require,libpq 是 sslmode=require(取值同一套) _ASYNCPG_TO_LIBPQ = {"ssl": "sslmode"} +#: 只给 SQLAlchemy / asyncpg 看的参数,libpq 不认(psycopg 连的时候会报「invalid connection option」) +_ASYNCPG_ONLY = {"prepared_statement_cache_size", "prepared_statement_name_func", "command_timeout", "async_fallback"} def _parse(raw: str, schemes: set[str], example: str) -> URL: @@ -44,7 +46,7 @@ def normalize_checkpoint_url(raw: str) -> str: def libpq_conninfo(database_url: str) -> str: """SQLAlchemy 的地址 → psycopg 的连接串:去掉驱动名,asyncpg 写法的参数换成 libpq 的。""" url = make_url(database_url) - query = {_ASYNCPG_TO_LIBPQ.get(k, k): v for k, v in url.query.items()} + query = {_ASYNCPG_TO_LIBPQ.get(k, k): v for k, v in url.query.items() if k not in _ASYNCPG_ONLY} return url.set(drivername="postgresql", query=query).render_as_string(hide_password=False) diff --git a/backend/tests/test_db_portable.py b/backend/tests/test_db_portable.py index 7dcbfc11..88e9e879 100644 --- a/backend/tests/test_db_portable.py +++ b/backend/tests/test_db_portable.py @@ -106,6 +106,34 @@ def test_postgres_integer_binds_are_bigint(): assert "runs.last_seq > $1::BIGINT" in sql +def test_database_urls_are_normalized_and_never_logged_with_the_password(): + from app.db.urls import describe, libpq_conninfo, normalize_checkpoint_url, normalize_database_url + + assert normalize_database_url("postgres://u:p%40w@h:5433/db") == "postgresql+asyncpg://u:p%40w@h:5433/db" + assert normalize_checkpoint_url("postgresql+asyncpg://u:pw@h/db") == "postgresql://u:pw@h/db" + # asyncpg 的 ssl= 换成 libpq 的 sslmode=,只给 asyncpg 看的参数不带给 psycopg + assert libpq_conninfo("postgresql+asyncpg://u:pw@h/db?ssl=require&prepared_statement_cache_size=0") \ + == "postgresql://u:pw@h/db?sslmode=require" + assert "pw" not in describe("postgresql+asyncpg://u:pw@h:5433/db") + for bad in ("mysql://u@h/db", "sqlite:///x.db", "postgresql://h", "这不是地址"): + with pytest.raises(ValueError): + normalize_database_url(bad) + from app.core.config import Settings + + with pytest.raises(ValueError, match="AGENTLAB_DATABASE_URL"): + Settings(database_url="mysql://u@h/db") + assert Settings(database_url=" ").db_dialect == "sqlite" + + +def test_the_startup_line_names_the_database_without_the_password(): + from sqlalchemy.engine import make_url + + line = db_base.describe_database() + password = make_url(db_base.settings.db_url).password + assert ("PostgreSQL" if PG else "SQLite") in line + assert not password or password not in line + + # -------------------------------------------------------------------------- # 连着库:两种库同一套结果 # -------------------------------------------------------------------------- diff --git a/deploy/README.md b/deploy/README.md index 4570f34c..db66533e 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -58,7 +58,8 @@ deploy.sh 依次:检查 docker 和架构 → 导入镜像(`docker load -i im 所有数据都在 `DATA_DIR`(默认是包目录下的 `data/`):数据库 `agentlab.db`、执行断点 `checkpoints.db`、 工件、上传文件、沙箱工作区,以及加密密钥 `.secret_key`。**要备份的就是这个目录**, -丢了 `.secret_key`,存进去的 API Key 就解不开了。 +丢了 `.secret_key`,存进去的 API Key 就解不开了。数据库放在 PostgreSQL 时(下面「用 PostgreSQL」)没有这两个库文件, +另用 `pg_dump` 备份数据库。 deploy.sh 会把数据目录的属主改成容器里的用户(uid 10001),所以备份、删除要用 sudo。 停下来备份最稳妥(以默认的 `./data` 为例,在包目录里执行): @@ -71,6 +72,37 @@ sudo tar -czf ../agentlab-data-$(date +%Y%m%d).tar.gz data 恢复:停掉容器,把备份解回 `DATA_DIR`,再 `./deploy.sh start`。 +## 用 PostgreSQL + +缺省数据库是数据目录里的 SQLite。要放进 PostgreSQL(库要先建好),在 `agentlab.env` 里加一行再 `./deploy.sh upgrade` +(或 stop 再 start): + +```bash +AGENTLAB_DATABASE_URL=postgresql+asyncpg://agentlab:<密码>@<数据库地址>:5432/agentlab +``` + +容器里的 `127.0.0.1` 是容器自己:数据库在别的机器上就写那台的地址;就在这台机器上,写宿主机在 docker 网桥上的地址 +(一般是 `172.17.0.1`),并让 PostgreSQL 监听这个地址、`pg_hba.conf` 放行容器的网段。执行断点也进同一个库。 +数据目录照样要挂、要备份:工件、上传文件、记录表、加密密钥 `.secret_key` 还在里面。 + +已经在用 SQLite 的,先把数据导过去(同一个镜像里带着导入命令;`--dry-run` 只核对、不写): + +```bash +./deploy.sh stop +sudo tar -czf ../agentlab-data-$(date +%Y%m%d).tar.gz data # 先备份 +docker run --rm --user 10001:10001 -v "$(pwd)/data:/data" --env-file agentlab.env --entrypoint python \ + agentlab:<版本> -m app.db copy-to-postgres /data --dry-run +docker run --rm --user 10001:10001 -v "$(pwd)/data:/data" --env-file agentlab.env --entrypoint python \ + agentlab:<版本> -m app.db copy-to-postgres /data +./deploy.sh start # agentlab.env 里已经有 AGENTLAB_DATABASE_URL +``` + +导入结束时逐表打印来源和目标的行数、断点的条数,对不上的列出来、退出码非零;重跑只补没有的行,不重复、不覆盖。 +停在审批上、中断了的运行导过去之后照样能接着跑。导完 `data/agentlab.db` 可以留着当回退:把那一行删掉、重新部署就回到 SQLite +(导入之后在 PostgreSQL 上新产生的东西不会回来)。备份时除了数据目录,再用 `pg_dump` 备份这个库。 + +现在仍按一台机器、一个容器部署:多台机器共用一份数据目录的限制见仓库 README「用 PostgreSQL」。 + ## 升级 把新版本的离线包拷到机器上,解到一个新目录,然后: diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 144a55a9..9e74e29f 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -266,8 +266,10 @@ fix_data_owner() { owner="$(ls -nd "$DATA_ABS" | awk '{print $3}')" [ "$owner" = "$uid" ] && return 0 # 改属主前确认这确实是一个新目录或者 AgentLab 的数据目录,免得 DATA_DIR 写错改了别的东西 - if [ -n "$(ls -A "$DATA_ABS" 2>/dev/null)" ] && [ ! -e "$DATA_ABS/agentlab.db" ]; then - die "DATA_DIR 指向的 $DATA_ABS 不是空目录,也不像 AgentLab 的数据目录(里面没有 agentlab.db)。 + # 应用库放在 PostgreSQL 时(AGENTLAB_DATABASE_URL)数据目录里没有 agentlab.db,认 .secret_key 和 uploads/ + if [ -n "$(ls -A "$DATA_ABS" 2>/dev/null)" ] && [ ! -e "$DATA_ABS/agentlab.db" ] \ + && [ ! -e "$DATA_ABS/.secret_key" ] && [ ! -d "$DATA_ABS/uploads" ]; then + die "DATA_DIR 指向的 $DATA_ABS 不是空目录,也不像 AgentLab 的数据目录(里面没有 agentlab.db、.secret_key、uploads)。 为免改错别的文件的属主,请指向一个新目录,或者原来的 AgentLab 数据目录" fi say "把数据目录的属主改成容器里的用户(uid ${uid}),容器里的服务才写得进去" diff --git a/environment.yml b/environment.yml index 5d995bdb..f173db38 100644 --- a/environment.yml +++ b/environment.yml @@ -20,4 +20,5 @@ dependencies: # 沙箱会退回 Seatbelt —— 代价是内存限额在 macOS 上限不住。 # [docs] 让 PDF / Word 传得进知识库。约十几兆,但少了它上传这类文件 # 只会得到一句"要先装解析库"——而界面上并不会拦着你去传 - - -e ./backend[microvm,dev,db,docs] + # [postgres] 应用库用 PostgreSQL 时要的驱动和断点库(AGENTLAB_DATABASE_URL),不设那一项就用不上 + - -e ./backend[microvm,dev,db,docs,postgres]