# pi-agent-api 项目设计

pi-agent-api 是安装到 Pi 的扩展包，由 Pi 在当前进程内加载，使用 TypeScript、Fastify 和 Pi SDK 为第三方项目提供 HTTP 接口。接入方通过一个聊天接口发送消息，需要时携带文件；扩展负责延续对话、保存附件，并按聊天的保留时间自动清理托管目录。聊天统一等待执行结束后返回 JSON。

npm 包名为 `pi-agent-api`，后台管理命令为 `pi-agent-api-service`（保留 `pi-http-service` 兼容别名）。新部署默认使用 `/data/pi-agent-api`、`pi-agent-api.config.json`、`pi-agent-api.sqlite`、systemd 单元 `pi-agent-api.service` 和 launchd 标识 `dev.pi-agent-api`。旧配置文件名仍可识别，保留旧默认数据路径和数据库名；旧服务通过 `--legacy-service` 停止或卸载后迁移，步骤见 README。`PI_HTTP_*` 环境变量、`/http` 命令和 HTTP 启动参数保持兼容。GitHub 仓库仍为 `wwyy456258/pi-http`。

本文记录截至 2026 年 10 月 16 日已确定的首版接口与行为，项目代码按此契约实现。安装、启动和调用命令见 [README](../README.md)。

## 首版范围

首版保留两个业务接口，统一使用服务级 API Key 鉴权。

| 方法与路径 | 功能 |
| --- | --- |
| `POST /v1/chat` | 发送消息，可指定模型、携带文件，自动创建或继续聊天 |
| `GET /v1/models` | 获取可选模型列表与当前默认模型 |

聊天只返回普通 JSON，不使用 SSE，不接受 `stream` 参数。首版不提供单独创建会话、上传文件、下载文件或管理文件的接口，也不引入独立的 run API、后台结果查询接口或额外聊天兼容协议。

WebUI、Webhook、定时任务、工作流和 Profile 不属于首版范围。

## Pi 扩展安装与生命周期

`package.json` 通过 `pi.extensions` 声明 `src/extension.ts`。交付物是包含源码的 Pi Package，使用 `pi install <本地目录或 Git 来源>` 安装，HTTP 由 Pi 扩展实现；可选后台启动器交由 systemd 或 launchd 管理真实 Pi CLI。Pi SDK 由宿主提供，放在 `peerDependencies`；开发与验证版本放在 `devDependencies`，不作为运行依赖打入扩展包。

扩展提供 `/http init [data-dir]`、`/http config`、`/http start`、`/http stop` 和 `/http status`。默认加载时只注册命令；使用 Pi 参数 `--http` 可在会话启动时自动打开监听，`--http-config <path>` 指定 HTTP 配置文件。

监听与宿主 Pi 生命周期绑定，退出、重载和切换前台会话时均先关闭监听、停止内部请求，再释放 HTTP 资源。带 `--http` 时，新会话或重载完成后重新启动监听；手动启动模式需再次执行 `/http start`。启动操作串行化，重复启动不重复占用端口，停止操作可重复执行。

HTTP 聊天在 Pi 进程内使用独立的 SDK session，与前台交互对话分别保存上下文。内部 HTTP session 不加载本扩展的管理入口，避免递归启动监听和重复处理宿主关闭事件；其他扩展、工具与资源按 Pi 的加载规则保留。

## API Key 鉴权

### 生成与配置

API Key 是第三方调用 pi-agent-api 的凭据，与 Pi 调用模型供应商所用的 API Key 相互独立。首版使用一个服务级静态 API Key，由部署者本地生成，不提供在线签发、登录或密钥管理接口，不使用 JWT。

生成命令：

```bash
openssl rand -hex 32
```

将生成结果设置到启动 Pi 的环境变量：

```bash
export PI_HTTP_API_KEY='<生成的密钥>'
```

API Key 不写入业务配置文件。初始化在 `paths.config_dir/environment.json` 生成 HTTP Key，扩展优先读取进程环境、其次读取该文件；两者均缺失或值为空时，HTTP 监听拒绝启动，Pi 本身仍可继续使用；生成的真实密钥不写入项目文档或日志。

### 请求与更换

两个业务接口都必须携带：

```http
Authorization: Bearer <API Key>
```

`Bearer` 是认证格式，后面的内容就是部署者生成的静态 API Key。服务先完成鉴权，再接收和处理业务内容，并以恒定时间比较方式校验密钥。缺失或无效时返回 `401 unauthorized`。

所有持有该密钥的调用方拥有相同的 Pi 执行权限，不以密钥区分项目、角色或聊天归属。API Key 不随聊天到期而失效；更新启动环境变量并重新启动 Pi 后，新密钥生效，旧密钥失效。后续如需分别撤销不同项目的凭据，再扩展多 API Key 管理。

## 运行环境与权限

HTTP 接入后的执行权限与宿主 Pi 一致，沿用启动用户的系统权限、环境变量、模型凭据和 Pi 配置。HTTP 层不额外限制 Bash、文件读写、工具调用或网络访问。HTTP 内部会话继承宿主当前启用的工具集合，以及宿主工作目录的项目信任状态；宿主注册的模型供应商和运行时凭据也用于 HTTP 调用。

服务运行时的 `cwd` 是 Pi 工具的默认工作目录，不是文件访问边界。聊天附件目录只是服务管理文件的位置，不自动成为 Pi 的工作目录，也不是沙箱。

每个聊天独立保存对话上下文，但不同聊天仍可能访问相同的系统资源或项目文件。API Key 用于服务入口鉴权，首版不以聊天目录实现租户权限隔离。

实现时应核对 Pi CLI 与 SDK 的加载差异，显式补齐需要的内置扩展、工具和资源。不能仅凭 SDK 默认初始化就认定与 CLI 能力一致。

普通工具正常执行。如果扩展必须发起终端确认、选择或输入，而无法在当前 HTTP 模式完成，则结束本次请求，返回 `interaction_required` 错误并说明所需交互的扩展。不能自动同意，也不能无限等待终端输入。首版不增加交互回答接口，这类扩展的交互适配后续扩展。

参考：[Pi SDK 官方文档](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/sdk.md)。

## 聊天接口

### 自动创建与继续聊天

首次请求不传 `chat_id`，服务自动创建聊天并返回 ID；后续请求携带该 ID，继续使用原有上下文。接入方不需要单独创建会话，也不需要重复发送历史消息。

HTTP 层使用 `chat_id` 标识我们自己的聊天。它与 Pi 原生 session 是不同概念，内部关联由服务维护，接入方不需要处理 Pi session ID 或文件路径。

无附件时，使用 `application/json` 请求体：

```json
{
  "message": "帮我分析一下这个项目",
  "model": "provider-a/model-a",
  "ttl_seconds": 86400
}
```

继续聊天：

```json
{
  "chat_id": "chat_abc",
  "message": "重点看看第二个问题"
}
```

| 字段 | 类型与规则 |
| --- | --- |
| `message` | 必填，非空字符串；纯空白不合法，有附件时也必须说明本轮要求 |
| `chat_id` | 可选，非空字符串；省略时自动创建，传入时继续已有聊天 |
| `model` | 可选，非空字符串；指定本次模型，取值来自模型列表 |
| `ttl_seconds` | 可选，正整数秒数；仅首次创建时允许传入 |

可选字段通过省略表达使用默认值，传 `null` 或错误类型均返回 `400 invalid_request`。未知字段也返回参数错误，包含此前讨论过的 `stream` 字段。

`ttl_seconds` 省略时使用配置默认值，传 `0`、负数、小数或超出可表示到期时间范围的值均报错。该值在创建时保存，续聊时即使传入相同值也返回参数错误，不能静默忽略。

传入未知 `chat_id` 返回 `404 chat_not_found`，已过期返回 `410 chat_expired`，两种情况都不自动创建聊天。

### 成功响应

执行结束后返回 `200 application/json`：

```json
{
  "request_id": "req_abc",
  "chat_id": "chat_abc",
  "model": "provider-a/model-a",
  "reply": "分析结果如下……",
  "expires_at": "2026-10-17T08:00:00Z"
}
```

`request_id` 标识本次聊天提交，用于错误排查和幂等重放。`model` 是实际使用的模型；`reply` 是本次执行结束后的最终回答，不把工具调用之间所有中间文本拼成最终回答。工具进度不向调用方流式发送。

`expires_at` 是本次结束后计算的聊天到期时间。所有接口时间统一使用带 `Z` 的 UTC 时间字符串。

### 并发规则

同一聊天同时只处理一条消息，包括接收附件、执行和停止中的阶段。新请求返回 `409 chat_busy`；同一个幂等请求的重复提交按后文的幂等规则处理。不同聊天可以并行执行，但不会因此隔离它们对相同文件和系统资源的访问。

服务不额外设置聊天执行总时限，模型请求与工具自身的超时沿用 Pi 配置。调用方及反向代理需要允许同步请求等待至执行结束。

## 模型列表与选择

`GET /v1/models` 返回可选模型列表与当前默认模型。模型 ID 应能区分供应商，聊天接口使用返回的 ID 指定模型。

```json
{
  "default_model": "provider-a/model-a",
  "models": [
    {
      "id": "provider-a/model-a",
      "name": "Model A",
      "provider": "provider-a"
    }
  ]
}
```

以上供应商与模型名称均为示例，不代表内置或固定支持的模型。

每次聊天请求按以下优先级选择模型：

1. 请求中的 `model`。
2. 服务配置文件中的 `model.default`。
3. 宿主 Pi 自身的默认选择，即当前选中的模型；未选中时使用 Pi 配置解析出的默认模型。

`default_model` 表示不传请求级 `model` 时会使用的有效默认模型，即扩展配置的默认模型，或回退后的宿主 Pi 默认选择。宿主 Pi 切换模型后，后续 HTTP 请求使用更新后的默认选择。

列表基于 Pi 已发现的模型与本地凭据配置生成，不为每个模型发起收费探测请求；列表存在不保证供应商当前在线或账户余额可用。无可解析的有效默认模型时，`default_model` 为 `null`，列表仍可查询；未指定模型的聊天返回 `503 default_model_unavailable`。

同一聊天允许切换模型，对话上下文继续保留。请求省略 `model` 时，重新按上述优先级选择，不沿用上一条消息临时指定的模型。

显式指定的模型不存在时返回 `400 model_not_found`，缺少所需本地凭据等情况返回 `400 model_unavailable`；供应商实际调用失败按上游错误处理。不向下回退或自动切换其他模型。服务配置的默认模型无效时，同样不能以“未配置”为由静默回退，返回 `503 default_model_unavailable`。

## 文件随聊天提交

### 请求方式

文件是聊天请求的可选附件，不单独上传，也不需要先获取 `file_id` 再发送聊天。

- 没有文件时，可以发送 JSON。
- 有文件时，发送 `multipart/form-data`，其中 `payload` 是上述聊天请求的 JSON 字符串，`files` 是一个或多个文件部分。
- 继续已有聊天时，也可以随消息追加文件。
- 文件扩展名与 MIME 类型不限。

Multipart 必须有且只有一个 `payload`，并排在所有文件之前，便于服务先验证参数、模型和聊天状态。`files` 为重复的文件部分，允许同名附件，不允许其他字段或重复的 `payload`。`payload` 内遵守与 JSON 请求相同的字段规则。

Multipart 结构示意：

```text
payload = {"message":"分析这些资料"}
files   = 报表.xlsx
files   = 截图.png
```

`files` 可以重复出现，表示多个附件。续聊时在 `payload` 中增加 `chat_id` 即可。

### 保存与使用

服务将附件原样保存到当前聊天自己的目录，不写入 Pi 原生 session 目录。后续消息可以继续使用该聊天之前上传的文件。

目录结构示意：

```text
/data/pi-agent-api/chats/chat_abc/
  conversation.jsonl
  files/
    <生成的文件ID>/
      报表.xlsx
    <生成的文件ID>/
      截图.png
```

内部文件 ID 用于组织存储和避免同名覆盖，不要求调用方管理。原文件名经过路径安全处理后保留，不能通过上传名称逃出托管目录。

上传后不主动解析，不自动解压、执行或安装解析依赖，也不把整个文件内容塞进模型上下文。服务只向 Pi 提供新增附件的名称、绝对路径及其与本轮消息的关联，并让这些信息随聊天上下文保留。

聊天任务需要文件时，由 Pi 使用原有工具按需读取或处理；没有用到时，文件留在聊天目录即可。文件可以保存，不代表模型原生支持该格式，或运行环境已经安装相应解析工具。

### 上传资源与失败处理

默认不设置文件大小和数量的业务上限。部署方可以配置 `upload.max_file_bytes`、`upload.max_request_bytes` 和 `upload.max_files`；单位分别为字节、字节和文件个数，`0` 表示不限制。请求总大小上限包含整个 JSON 或 Multipart 请求体。超过已配置的大小或数量上限时返回 `413 upload_limit_exceeded`，文件类型始终不限制。

文件流式写入请求临时目录，不整份读入内存。服务完成鉴权、参数和模型校验，并锁定聊天后接收文件；全部上传成功后，将本次附件提交到聊天目录，再将消息交给 Pi。

上传中断、某个文件失败或请求校验失败时，整条消息不提交给 Pi，删除本次临时文件并释放聊天占用。新聊天只有成功提交消息时才正式保留，不能因失败上传留下可用的空聊天。

消息已经提交给 Pi 后，即使执行失败或取消，也保留已提交附件、已有消息和工具执行结果，供下一轮继续使用。不能为了清理失败请求而删除已经被对话引用的附件，也不会回滚工具已完成的操作。

临时目录位于 `chat.storage_dir` 下的独立暂存区域，清理器与聊天目录区分处理。正常失败立即清理，进程异常退出留下的暂存文件在下次启动时清理。

## 断线与幂等重试

### 客户端断开

客户端在收到完整响应前断开连接，服务请求 Pi 取消本次执行，并等待其停止；聊天在停止完成前仍保持忙碌。取消是停止后续执行，不是回滚，已经完成的命令、文件修改和外部操作继续保留。

取消后将本次提交记录为 `request_cancelled`，保留聊天上下文与已提交附件。若客户端只是因响应丢失而未收到已经完成的结果，则记录仍为成功，可以通过同一幂等 Key 重放。

### 幂等 Key

聊天接口支持可选请求头：

```http
Idempotency-Key: <调用方生成的唯一值>
```

未提供时，每次请求都是一次新的提交；重试可能重复执行。提供时，服务以该 Key 在当前服务的范围内识别同一次提交，新聊天首次请求同样适用。`request_id` 由服务生成，不能替代调用方预先提供的幂等 Key。

请求指纹包含聊天字段以及附件名称、顺序和内容摘要，不比较 Multipart 边界等编码差异。比较请求本身，不因服务默认模型等配置变化而将一次重试当成新请求；执行使用首次提交时选定的模型与保留时间。

| 重复提交情况 | 处理 |
| --- | --- |
| 相同 Key、相同请求，原提交已完成 | 返回原响应，保留原 `request_id`、`chat_id`、模型、回答和到期时间 |
| 相同 Key，原提交仍在处理 | 返回 `409 request_in_progress`，不再执行 |
| 相同 Key、不同请求，指纹已确定 | 返回 `409 idempotency_conflict` |
| 相同 Key、相同请求，原提交已失败、取消或中断 | 返回已记录的错误，不重新执行 |
| 相同 Key 对应的聊天已过期 | 返回 `410 chat_expired`，不创建新聊天 |

接收上传期间如果完整指纹尚未确定，相同 Key 的并发提交先返回 `request_in_progress`。鉴权、请求格式或上传校验失败且尚未提交给 Pi 时，不形成已执行记录，修正后可以重新提交。

幂等查验在鉴权和必要的请求解析之后进行，优先于为新执行选择模型和检查聊天忙碌。重放已有结果不因模型配置后来变化或聊天正在处理另一条消息而重新执行或改报忙碌；已过期聊天仍统一返回 `410`。带附件的重放需要接收并核对附件摘要，核对结束后删除本次临时文件，不向聊天目录重复添加附件。

重放成功或失败结果均不延长聊天保留时间。聊天到期后删除响应正文等关联数据，只保留识别过期聊天与已使用幂等 Key 所需的最小标记，避免旧重试创建新聊天。

调用方确实希望重新执行时，使用新的幂等 Key。HTTP 层不自动重提整条聊天请求；Pi 自身的模型请求重试沿用其配置。

## 错误响应

错误统一返回对应 HTTP 状态码和 JSON：

```json
{
  "request_id": "req_abc",
  "chat_id": "chat_abc",
  "error": {
    "code": "chat_expired",
    "message": "该聊天已过期，请重新开始聊天"
  }
}
```

尚无已建立的聊天时省略 `chat_id`。错误不返回 API Key、模型凭据或内部堆栈。幂等重放使用原提交的 `request_id`；仍在处理时的重复请求应关联已有提交 ID。

| HTTP 状态码 | 错误码 | 含义 |
| --- | --- | --- |
| `400` | `invalid_request` | 字段、类型、Multipart 结构或参数取值错误 |
| `400` | `model_not_found` / `model_unavailable` | 请求指定的模型不存在或本地配置不可用 |
| `401` | `unauthorized` | API Key 缺失或无效 |
| `404` | `chat_not_found` | 聊天 ID 不存在 |
| `409` | `chat_busy` | 同一聊天正在处理其他提交 |
| `409` | `request_in_progress` / `idempotency_conflict` | 重复提交仍在处理或 Key 与请求冲突 |
| `409` | `interaction_required` | 扩展需要当前 HTTP 模式无法完成的交互 |
| `409` | `request_cancelled` / `request_interrupted` | 重试时读取到原提交已取消或中断 |
| `410` | `chat_expired` | 聊天已经到期 |
| `413` | `upload_limit_exceeded` | 超过部署方配置的请求大小或文件数量限制 |
| `415` | `unsupported_media_type` | 请求编码不是 JSON 或 Multipart；不是限制附件 MIME 类型 |
| `502` | `upstream_error` | 模型供应商请求失败 |
| `503` | `default_model_unavailable` / `service_unavailable` | 默认模型不可用或服务正在退出 |
| `500` | `internal_error` | 服务内部故障，包括本地存储失败 |

工具调用失败可以由 Pi 正常处理并继续回答，不因一次工具失败就直接认定整个聊天请求失败。客户端已经断开时无法再发送错误响应，取消状态供后续幂等重试读取。

## 聊天目录自动清理

### 保留时间

使用空闲到期规则，默认保留 24 小时。首次聊天请求中的 `ttl_seconds` 可以覆盖配置文件默认值，每个聊天独立保存自己的保留时间。

已经提交给 Pi 的请求，无论成功、失败还是取消，都在执行或停止结束时重新计算到期时间。鉴权失败、参数错误、上传失败、忙碌拒绝以及幂等重放均不续期。

正在上传、执行或停止的聊天都不能被清理。检查到期、占用聊天和开始清理需要协调，避免请求刚通过校验目录就被删除。

清理周期与保留时间是两个参数：

| 参数 | 默认值 | 含义 |
| --- | --- | --- |
| `chat.ttl_seconds` | `86400` | 聊天空闲多久后可以清理，默认 24 小时 |
| `chat.cleanup_interval_seconds` | `300` | 扫描过期目录的间隔，默认 5 分钟 |

保留时间决定逻辑到期时间；定时扫描负责回收磁盘目录，因此物理删除可能晚于到期时间。过期聊天即使尚未被扫描删除，也不应被新请求继续使用。

### 清理范围

到期后删除整个托管聊天目录及上传文件，删除 SQLite 中对应的正文和幂等响应数据，保留用于识别过期 ID 与 Key 的最小标记。过期 `chat_id` 再次请求时返回 `410 chat_expired`，不悄悄创建同名或新聊天。调用方可以省略 `chat_id`，使用新幂等 Key 重新开始聊天。

清理器只删除服务自己管理的聊天目录，不删除 Pi 原生 session 文件、项目文件或其他系统文件。因此，24 小时保留规则针对托管聊天目录，不代表 Pi 自身的历史记录也会在 24 小时后删除。

每次启动服务时先清理异常退出留下的临时文件，恢复聊天状态并扫描过期目录，再开始对外接收业务请求。某个过期目录删除失败时保留过期标记，后续扫描继续回收，不能使聊天重新有效。

## 状态存储与服务重启

首版使用一个本地 SQLite 文件保存聊天与 Pi session 的关联、保留时间、到期时间、提交状态、幂等请求指纹与结果，以及过期标记。附件保存在聊天目录中；Pi 对话历史仍由 Pi 原生 session 文件管理，不复制为另一套模型历史。

元数据与提交状态需要持久化，不能只放内存。过期标记只保存必要标识，不保留附件内容、用户消息或回答正文。仅支持一个宿主 Pi 进程中的 HTTP 实例管理同一数据库、聊天目录和内部 Pi session，使用进程锁防止多个 Pi 实例误用同一存储。

正常退出时停止接收新请求，给已接收请求默认 30 秒的退出宽限时间；超时后请求取消。异常退出后，未完成且已提交的请求标记为 `request_interrupted`，未提交的上传按暂存文件处理，不自动重新执行。

未过期聊天可继续发送新消息，已持久化的消息和工具结果保留。恢复时先处理未完成提交和对应到期时间，再扫描过期聊天；中断提交的空闲时间从重启恢复时计算，避免刚恢复的聊天立即被误清理。相同幂等 Key 的重试返回中断状态；有需要时调用方使用新 Key 发起新提交。

## 配置与启动

主配置查找顺序为显式 `--http-config <path>`、工作目录 `pi-agent-api.config.json`、工作目录 `pi-http.config.json`、`/data/pi-agent-api/config/pi-agent-api.config.json`、旧 `/data/pi-http/config/pi-http.config.json`。默认文件不存在时使用内置默认值；显式指定的配置文件缺失、JSON 无法解析或参数无效时拒绝启动 HTTP 监听，并向 Pi 报告错误。

API Key 优先从 `PI_HTTP_API_KEY` 环境变量读取，未设置时读取 `paths.config_dir/environment.json`，不属于下方业务 JSON 配置。业务配置示例：

```json
{
  "server": {
    "host": "127.0.0.1",
    "port": 3000,
    "shutdown_grace_seconds": 30
  },
  "pi": {
    "cwd": "."
  },
  "model": {
    "default": "provider-a/model-a"
  },
  "chat": {
    "storage_dir": "/data/pi-agent-api/chats",
    "ttl_seconds": 86400,
    "cleanup_interval_seconds": 300
  },
  "storage": {
    "database_path": "/data/pi-agent-api/pi-agent-api.sqlite"
  },
  "upload": {
    "max_file_bytes": 0,
    "max_request_bytes": 0,
    "max_files": 0
  }
}
```

| 配置 | 省略时的默认行为 |
| --- | --- |
| `server.host` / `server.port` | 监听 `127.0.0.1:3000` |
| `server.shutdown_grace_seconds` | 正常退出时等待 30 秒，再请求取消 |
| `pi.cwd` | 使用宿主 Pi 当前工作目录 |
| `model.default` | 使用宿主 Pi 的默认模型选择 |
| `paths.data_dir` | `/data/pi-agent-api` |
| `paths.config_dir` | `<data_dir>/config` |
| `logging.directory` | `<data_dir>/logs` |
| `chat.storage_dir` | `<data_dir>/chats` |
| `chat.ttl_seconds` | `86400`，默认空闲保留 24 小时 |
| `chat.cleanup_interval_seconds` | `300`，默认每 5 分钟扫描 |
| `storage.database_path` | `<data_dir>/pi-agent-api.sqlite` |
| `upload.max_file_bytes` | `0`，不限制单文件大小 |
| `upload.max_request_bytes` | `0`，不限制请求总大小 |
| `upload.max_files` | `0`，不限制文件数量 |

所有相对路径均相对于配置文件所在目录解析。默认路径使用绝对的 `/data/pi-agent-api` 数据根目录；示例显式写出的 `pi.cwd: "."` 因此表示配置文件所在目录，省略该字段则使用宿主 Pi 当前工作目录。

时间、数量和字节数参数须为有效整数：聊天保留时间与清理间隔必须大于零，上传限制和退出宽限时间允许为零；模型字段配置为空字符串不能视为省略。存储目录或数据库无法使用时拒绝启动。

配置文件修改后通过 `/http stop`、`/http start` 重新读取，首版不做热更新。进程环境中的 API Key 需要重启 Pi 才能更换；前台使用环境文件时重新启停 HTTP 即可，后台使用环境文件时重启后台服务。

## 初始化、路径与接口日志

安装只注册扩展。首次使用前执行 `/http init [data-dir]` 或 `pi-agent-api-service init --data-dir <path>`，自动创建目录、主配置和私有环境文件，已有文件不覆盖。默认主配置为 `/data/pi-agent-api/config/pi-agent-api.config.json`。显式 `--http-config` 优先，其次兼容工作目录已有配置，最后使用默认路径。

`paths.data_dir` 控制默认数据根目录，`paths.config_dir` 控制环境与服务配置目录，`logging.directory` 控制日志目录；附件和 SQLite 路径仍可分别覆盖。显式相对路径以主配置文件目录为基准。主配置自身的位置由启动参数决定。目录创建失败报告具体路径，不静默回退或自动提升权限。

默认启用 `http.jsonl` 接口日志，记录 UTC 时间、请求 ID、方法、路径、来源、输入输出、状态和耗时；文件仅记录数量和字节数，不记录文件名或内容。鉴权失败不记录业务输入，不记录请求头和查询参数。错误详情和日志中的已知凭据脱敏。客户端断开记录 `request_aborted`（499）。

`logging.enabled` 默认 true，`log_bodies` 默认 true，`max_body_chars` 默认 16384。日志超过 `max_file_bytes`（默认 10 MiB）按完整记录轮转，保留 `max_files`（默认 7）个归档。聊天 TTL 不影响全局接口日志保留。初始化、前后台启动、管理命令及迁移细节见 README。

每个聊天目录还保存 `conversation.jsonl`，与附件放在我们自己的聊天目录下，不写入 Pi 原生 session。每个已执行完成的轮次追加一个 JSON 对象，字段包括 `event: "chat_turn"`、UTC 完成时间 `time`、`started_at`、`chat_id`、`request_id`、实际 `model`、`status`、执行耗时 `duration_ms`、完整 JSON `input`、完整 JSON `output` 和文件数量/各文件大小/总大小。正文换行通过 JSON 转义，连续对话和重启后续聊均追加到同一个文件。

对话日志在本轮结果写入数据库后、释放聊天锁前追加，覆盖成功、上游错误和取消；幂等重放不产生重复记录。参数校验失败等未执行请求仍由全局接口日志记录，进程崩溃时未完成的轮次不会补写对话日志。对话正文沿用凭据脱敏和 `logging.log_bodies` 开关，但不做 `max_body_chars` 截断或 `max_file_bytes` 轮转，以保留同一文件内的完整对话。关闭 `logging.enabled` 时也不创建对话日志。新文件权限为 `0600`，运行时写入失败报告 stderr 并保留业务结果。聊天 TTL 清理目录时一并删除对话日志；目录位置由 `chat.storage_dir` 配置。
