# heng.lu Doctrine Service

让你的 AI 调用 heng.lu，根据卢恒公开发表的 Notes 分析政策、解释依据、提出改进，并继续追问。无需注册 heng.lu 账户。返回内容是模型分析，不代表卢恒本人审阅，也不构成卢恒新的主张。

API 根地址：`https://heng.lu/api/doctrine/v1`。先读取 `GET /manifest` 确认实际服务版本、语料版本与运行限制。首版初始来源是已确认归属卢恒的 74 篇 Notes；语言版本归属于同一作品，不混入团队 Blog。来源仅为当前公开正文，历史证据随你的分析私密保存。

## 配置连接器

连接器需要指定 `provider`（`openai` 或 `anthropic`）、模型的准确 ID，以及对应官方 API 密钥。模型费用由该密钥对应的账户承担。本服务不切换到自己的付费账户，也不接受自定义模型地址。

模型密钥仅由连接器注入 `X-Model-API-Key` 请求头，每次创建或追问重新提交；不要把密钥放在聊天、URL、请求正文或模型可见工具参数中。分析访问凭证是另一份独立的随机 256 位秘密，用无填充 base64url 编码，放在 `Authorization: Bearer <secret>` 中。服务只保存其摘要。每份分析使用独立凭证；读取和删除不需要模型密钥。

Node 与 Python 客户端在权限为 `0700` 的 `<vault>.d` 目录中，逐条保存权限为 `0600` 的不可变凭证文件。原子发布允许多个连接器共用凭证，进程中断不会留下阻碍后续操作的锁。请安全保管整个目录以及已有的旧版 JSON 凭证文件：没有账户恢复流程，丢失凭证不能只凭分析 ID 找回。模型密钥从秘密管理器注入进程环境，不写入凭证。请使用支持原子硬链接和目录 fsync 的本地文件系统（Linux/macOS）。

## 创建与继续分析

下载 [Node 客户端](./node.mjs)，在连接器代码中使用：

```js
import { DoctrineClient } from './node.mjs';
const client = new DoctrineClient({
  provider: process.env.DOCTRINE_PROVIDER,
  model: process.env.DOCTRINE_MODEL,
  modelKey: process.env.DOCTRINE_MODEL_KEY,
});
const result = await client.create({
  question: '从卢恒思想出发，这项政策有哪些问题和改进空间？',
  policy: {title: '审批政策', text: '此处提交完整政策正文',
    source_url: 'https://example.org/policy', as_of: '2026-10-08'},
  language: 'zh',
}, {idempotencyKey: 'my-policy-analysis-0001'});
const next = await client.turn(result.analysis_id, {
  question: '如果新增独立申诉程序，你的判断如何变化？', language: 'zh',
}, {idempotencyKey: 'my-policy-followup-0001'});
const saved = await client.read(result.analysis_id);
// 仅在你希望删除时执行：
await client.delete(result.analysis_id);
```

Python 标准库客户端：[python.py](./python.py)。将它保存为 `doctrine_client.py`：

```python
import os
from doctrine_client import DoctrineClient
c = DoctrineClient(provider=os.environ['DOCTRINE_PROVIDER'],
                   model=os.environ['DOCTRINE_MODEL'],
                   model_key=os.environ['DOCTRINE_MODEL_KEY'])
r = c.create('请分析这项政策', {'title': '审批政策', 'text': '完整政策正文'},
             idempotency_key='my-policy-analysis-0001')
c.turn(r['analysis_id'], '加入申诉程序是否有帮助？',
       idempotency_key='my-policy-followup-0001')
c.read(r['analysis_id'])
c.delete(r['analysis_id'])
```

每个逻辑请求使用一个 `Idempotency-Key`。网络中断后，用同一凭证、相同键和相同正文查询原结果，不重复启动收费推理。原请求失败或取消后，同一个键仍返回原状态；只有你明确选择重新推理时才使用新键。客户端不自动重试。SSE 先返回身份，随后给出客观进度和最终结果，不输出内部推理过程。

## HTTP 接口

| 方法与路径 | 行为 |
| --- | --- |
| `GET /manifest` | 版本、限制、存储状态和留存政策，无需凭证 |
| `GET /sources` | 当前 Notes 目录，无需凭证 |
| `GET /sources/{id}` | 当前公开来源全文，无需凭证 |
| `POST /analyses` | 创建，需要访问凭证、模型密钥及幂等键 |
| `GET /analyses/{id}` | 读取私密分析、完整轮次与证据，需要访问凭证 |
| `POST /analyses/{id}/turns` | 追问，需要访问凭证、模型密钥及新的幂等键 |
| `DELETE /analyses/{id}` | 删除在线记录，需要访问凭证 |

创建正文见 [OpenAPI](./openapi.json)。追问的 `policy` 可省略，省略时原政策仍完整保留在历史中。提供新 `policy` 会作为这一轮补充材料保存，不覆盖原文。`source_url` 是你提交的出处，本服务没有抓取或独立核实它。

`Accept: application/json` 最长等待 90 秒；`Accept: text/event-stream` 最长 180 秒，默认客户端采用 SSE。SSE 事件为 `identity`、`progress`、`result`、`error`，并有心跳。收到 HTTP 200 的 SSE 后仍须检查最终事件。重复提交运行中的请求返回已有状态，不另行启动推理。取消可通过关闭连接或 AbortSignal；删除也会终止相关在途调用。上游已经发生的费用无法撤销。

返回包含 `analysis_id`、`turn_id`、状态、`analysis_markdown`、引用、客观来源变化、固定语料版本、实际提供方/模型及其返回用量。引用的链接与摘要由服务根据来源装配；`verified` 只说明片段匹配，不表示服务认可结论。没有核实的引用会明确标记。语料修订与撤回不覆盖旧分析证据；追问会给模型历史与当前变化，由模型重新判断。

## 永久私密保存与主动删除

`retention` 固定为 `until_user_deletion`，`expires_at` 固定为 `null`。政策、追问、分析、实际读取的正文快照和必要运行记录加密长期保存。不因闲置、时间、重启、版本升级或容量压力自动删除。无公开分析列表和结果网页；材料不会自动加入思想库或作为卢恒的新观点。

持有凭证的人可以读取、追问和删除该分析。删除会立即撤销在线访问、终止在途调用并删除在线正文及关联内容；持久删除清单阻止迟到结果和恢复旧库重新产生记录。历史 QNAP 备份按既有保留策略处理，不承诺立即清除所有历史副本；恢复必须先应用最新删除清单。数据恢复与密钥保管保持原访问边界。

`DELETE` 返回 `204` 表示删除清单也已在 QNAP 持久登记。如果备份连接暂时不可用，会先删除在线内容，返回 `202` 和 `backup_deletion_pending:true`；这时请保留凭证，稍后重复同一 `DELETE`，直至收到 `204`。重复删除不会产生模型费用，也不会重新创建分析。历史备份恢复必须先完成删除清单核对，不能把尚未核对的副本直接投入服务。

服务不保存模型密钥、不记录认证头或正文，关闭核心转储。OpenAI 调用设置 `store:false`；本服务的永久留存与提供方的数据处理是两回事，这个参数不等于提供方绝不保留数据。请阅读 [OpenAI 数据控制](https://developers.openai.com/api/docs/guides/your-data) 和 [Anthropic 数据处理说明](https://privacy.claude.com/en/articles/7996866-how-long-do-you-store-my-organization-s-data)。

## 限制与错误

政策最多 60,000 个 Unicode 字符，请求体最多 1 MiB；每轮最多 6 次提供方请求、累计输出预算 8,000 token。每份分析最多 20 轮、累计 4 MiB。长期记录超出当前模型上下文预算时会明确返回 `context_limit_reduce_scope`，不会静默删改历史；可明确新建一份范围更小的分析。达到单份限制后旧分析仍可永久读取。

全局最多 4 个推理，同一模型密钥最多 1 个；每密钥每分钟最多发起 6 次，可信客户端 IP 叠加限流。初始持久存储逻辑配额 1 GiB，80% 预警。容量满返回 `storage_capacity`，读取和删除仍可用，旧记录不被淘汰。

主要错误：`analysis_not_found`（不存在或凭证不匹配）、`idempotency_conflict`、`analysis_busy`、`provider_credentials_rejected`、`provider_quota_or_rate_limit`、`provider_model_or_context_incompatible`、`sources_unavailable`、`deadline_exceeded`、`interrupted`。错误不返回提供方的原始敏感响应。服务重启仅将未完成轮次标为中断，不自动重发收费请求。

首版正式验收目标为中文和英文；其他语言尚未完成同等级验收。公开 API 不保证所有模型 ID 支持所需的工具调用与上下文。

## AI 工具与 MCP

服务重启后，私密操作会等待独立删除历史的新鲜确认。接收器暂时不可用时返回 `recovery_confirmation_pending`，manifest 显示 `storage.recovery_pending:true`。记录继续留存，公开来源和文档仍可访问；确认恢复后可重试读取。

通用 function-tool 定义位于 [tools.mjs](./tools.mjs)，不含任何凭证参数。连接器应调用上面的客户端注入凭证。MCP 适配器使用标准 stdio：下载同目录的 `mcp.mjs`、`node.mjs` 和 `tools.mjs`，以 Node 24 启动 `node /absolute/path/mcp.mjs`。通过连接器的秘密环境配置 `DOCTRINE_PROVIDER`、`DOCTRINE_MODEL`、`DOCTRINE_MODEL_KEY`，可另配 `DOCTRINE_VAULT`；不要把真实密钥提交到配置仓库或聊天。

MCP 提供 `doctrine_create`、`doctrine_turn`、`doctrine_read`、`doctrine_delete`。模型仅提交问题、政策、语言和分析 ID，凭证由适配器管理。不提供远程 MCP OAuth 账户系统。
