# fnOS PostgreSQL 共享服务接入指南 本文面向需要接入 `nxsir.postgresql` 的 fnOS 原生应用。共享服务不依赖 Docker,多个应用共用一个 PostgreSQL 15 进程,但每个应用获得独立数据库、独立登录角色和独立随机密码。 ## 1. 服务约定 | 项目 | 默认值 | |---|---| | fnOS 应用名 | `nxsir.postgresql` | | PostgreSQL 地址 | `127.0.0.1:15432` | | 管理/API 地址 | `http://127.0.0.1:15433` | | 传输加密 | 不启用 TLS,仅允许本机回环连接 | | 密码认证 | PostgreSQL SCRAM-SHA-256 | | 可申请扩展 | `vector`(pgvector) | 不要连接 Unix Socket、不要使用服务管理员角色,也不要假设数据库名或角色名。客户端必须通过接入 API 获取完整凭据。 ## 2. 声明 fnOS 依赖 在应用 FPK 的 `manifest` 中声明: ```ini install_dep_apps=nxsir.postgresql ``` 用户应先安装并启动 PostgreSQL 共享服务,再安装你的应用。你的安装/升级向导需要提供一个密码字段,让用户填写安装共享服务时设置的“应用接入令牌”。令牌长度为 20~256 个字符,不允许换行。 接入令牌与 PostgreSQL 管理员密码是两套独立凭据: - 管理员密码只登录 PostgreSQL 管理面板。 - 接入令牌只用于本机应用首次签发或重新签发数据库凭据。 ## 3. 注册客户端 注册接口只接受来自回环地址的请求: ```http POST http://127.0.0.1:15433/internal/v1/enroll Authorization: Bearer <应用接入令牌> Content-Type: application/json ``` 普通 PostgreSQL 客户端: ```json { "appId": "myapp", "displayName": "My fnOS App", "requestedExtensions": [] } ``` 需要 pgvector 的应用: ```json { "appId": "imagefind", "displayName": "ImageFind", "requestedExtensions": ["vector"] } ``` 字段限制: - `appId`:稳定且全局唯一,3~64 个字符;以小写字母开头,只允许小写字母、数字、点、下划线和连字符。发布后不要更改。 - `displayName`:1~100 个字符,用于管理面板展示。 - `requestedExtensions`:目前只能是空数组或包含 `vector`。 curl 示例: ```bash curl --fail --silent --show-error \ -H "Authorization: Bearer $APP_ENROLLMENT_TOKEN" \ -H 'Content-Type: application/json' \ --data '{"appId":"myapp","displayName":"My fnOS App","requestedExtensions":[]}' \ http://127.0.0.1:15433/internal/v1/enroll ``` 成功响应: ```json { "host": "127.0.0.1", "port": 15432, "database": "appdb_myapp_...", "username": "app_myapp_...", "password": "一次性返回的随机密码", "sslMode": "Disable", "serviceVersion": "15" } ``` 每次对同一 `appId` 重新注册都会复用其数据库和角色,但会立即轮换密码,使旧密码失效。因此正常启动时应先读取本地凭据,只有首次安装、凭据丢失或明确执行密码轮换时才重新注册。 常见 HTTP 状态: | 状态 | 含义 | |---:|---| | `200` | 注册成功;响应中包含新密码 | | `400` | `appId`、显示名或扩展参数无效 | | `401` | 接入令牌错误或已被管理员轮换 | | `403` | 请求不是从本机回环地址发起 | | `500` | 共享服务内部错误;查看共享服务日志 | ## 4. fnOS 生命周期脚本建议 安装回调只负责以 `0600` 保存令牌种子,不要在向导校验阶段依赖网络。应用启动时执行注册,并采用临时文件加原子重命名保存响应。 建议的持久化文件: ```text ${TRIM_PKGVAR}/postgres-enrollment-token.seed # 首次注册前,0600 ${TRIM_PKGVAR}/postgres-client.conf # 注册成功后,0600 ``` 推荐配置格式: ```ini host=127.0.0.1 port=15432 database=接口返回值 username=接口返回值 password=接口返回值 ``` 注册成功并安全落盘后应删除令牌种子,避免长期保存高权限接入令牌。不要把密码写入日志、命令行参数、进程标题或 Web 前端。应用卸载时是否保留数据库由用户决定;不要自行执行 `DROP DATABASE`。 服务可能在 NAS 启动时稍晚就绪。建议: - 请求超时 10 秒左右。 - 每 2 秒重试一次,最多等待 1~2 分钟。 - 先探测 `GET /health/ready`,或直接重试注册。 - 已有本地凭据时不要因为管理 API 暂时不可用而重新注册;直接尝试 PostgreSQL 连接。 ## 5. 连接字符串 .NET / Npgsql: ```text Host=127.0.0.1;Port=15432;Database=;Username=;Password=;SSL Mode=Disable;Timeout=15;Command Timeout=120;Keepalive=30 ``` Python / psycopg: ```python import psycopg connection = psycopg.connect( host="127.0.0.1", port=15432, dbname=database, user=username, password=password, sslmode="disable", connect_timeout=15, ) ``` JDBC: ```text jdbc:postgresql://127.0.0.1:15432/?sslmode=disable&connectTimeout=15 ``` 应用角色是目标数据库及 `public` schema 的所有者,可以执行自身 migrations、建表和创建索引,但不能创建数据库、创建角色、成为超级用户或连接其他托管应用的数据库。 ## 6. pgvector 注册时申请 `"requestedExtensions":["vector"]` 后,共享服务会在应用数据库内执行: ```sql CREATE EXTENSION IF NOT EXISTS vector; ``` 应用可直接在 migration 中使用: ```sql CREATE TABLE embeddings ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, embedding vector(768) NOT NULL ); CREATE INDEX embeddings_cosine_idx ON embeddings USING hnsw (embedding vector_cosine_ops); ``` 不要由应用尝试安装系统扩展文件。未在允许列表中的扩展会在注册阶段被拒绝。 ## 7. schema 与迁移 - 新数据库默认撤销 `PUBLIC` 的数据库连接权和 schema 权限。 - 应用角色拥有自己的数据库和 `public` schema,可正常运行 Flyway、EF Core、Alembic、Liquibase 等迁移。 - 用 custom-format `pg_dump` 迁移旧库时建议使用 `--no-owner --no-acl`。 - 恢复前确认目标库为空;不要覆盖其他 `appId` 的数据库。 - 切换前至少校验关键表行数,保留旧数据和带 SHA-256 的转储,确认稳定后再人工清理。 ## 8. 密码轮换、吊销与恢复 管理员可以在 PostgreSQL 管理面板中: - 为某个客户端轮换密码:应用必须立即更新本地凭据。 - 吊销客户端:对应角色变为 `NOLOGIN`,数据库不会被删除。 - 轮换全局接入令牌:旧令牌立即失效,已经签发的数据库密码不受影响。 应用本地凭据丢失时,让用户在升级/修复向导重新输入当前接入令牌,再用相同 `appId` 注册。数据库内容会保留,但旧数据库密码会失效。 不要静默回退到陈旧数据库副本。若应用已经完成共享数据库迁移,但凭据不可用,应停止启动并提示修复凭据,避免产生两套分叉数据。 ## 9. 健康检查与排错 ```bash curl -fsS http://127.0.0.1:15433/health curl -fsS http://127.0.0.1:15433/health/ready ``` 排查顺序: 1. 确认 `nxsir.postgresql` 已安装且状态为运行中。 2. 确认应用使用 `127.0.0.1`,而不是 NAS 局域网 IP。 3. 检查接入 API 端口 `15433` 与数据库端口 `15432` 是否混用。 4. 检查凭据文件权限是否为 `0600`,字段是否完整。 5. `password authentication failed` 通常表示密码已轮换;用相同 `appId` 重新注册并原子更新凭据。 6. `permission denied for database` 通常表示连接了其他应用的数据库;必须使用响应中的 database。 7. 共享服务升级或重启不会要求客户端重新注册,应用应使用已有凭据自动重连。 ## 10. 接入验收清单 - manifest 已声明 `install_dep_apps=nxsir.postgresql`。 - 接入 API 只从回环地址调用。 - `appId` 固定且不会随版本变化。 - 令牌和数据库密码从不写日志。 - 凭据以 `0600` 原子落盘,成功后删除令牌种子。 - 普通启动不重复注册、不意外轮换密码。 - 应用 migrations 能在自己的数据库执行。 - 已验证无法连接另一个测试应用的数据库。 - 共享服务重启后应用能用原凭据恢复。 - 已准备凭据丢失、密码轮换和迁移失败的明确恢复流程。