im_wower
·
2026-03-28
ARTIFACT_STATIC_SERVICE.md
1# Artifact 静态服务方案
2
3日期:`2026-03-28`
4
5## 状态
6
7- `设计中`
8- 依赖当前主分支:`main`
9- canonical local API:`http://100.71.210.78:4317`
10- canonical public host:`https://conductor.makefile.so`
11
12## 关联文档
13
14- [`./STATUS_SUMMARY.md`](./STATUS_SUMMARY.md)
15- [`./BAA_INSTRUCTION_SYSTEM.md`](./BAA_INSTRUCTION_SYSTEM.md)
16
17## 1. 背景与动机
18
19当前 BAA 指令闭环中,执行结果只能通过文本回送给 AI 对话。存在三个问题:
20
211. **大结果截断**:超长输出(如测试日志、文件内容)只能截断回送,AI 看不到完整结果
222. **文件传输不可行**:通过浏览器扩展注入文件到 AI 对话,链路复杂且脆弱
233. **跨会话断裂**:新对话无法获取之前的执行历史和消息记录
24
25## 2. 核心思路
26
27把所有消息和执行结果持久化到数据库,同时生成可通过 HTTP GET 直接访问的静态文件。AI 通过 fetch URL 获取完整内容,不需要浏览器扩展参与。
28
29```
30AI 回复 → conductor 收到 browser.final_message
31 → 全量写入 SQLite + D1(消息表)
32 → 生成静态文件:/artifact/msg/{id}.txt + .json
33 → 提取 baa 指令 → 执行
34 → 全量写入 SQLite + D1(执行表)
35 → 生成静态文件:/artifact/exec/{id}.txt + .json
36 → 更新会话索引
37 → 回送给 AI:
38 结果 ≤ 2000 字符 → 内联全文
39 结果 > 2000 字符 → 前 500 字符摘要 + exact URL
40```
41
42## 3. 设计决策
43
44以下决策经过 ChatGPT 和 Gemini 实测验证。
45
46### 3.1 已确认的决策
47
48| 项 | 决策 | 原因 |
49|---|---|---|
50| 静态文件路由 | `/artifact/` | 通俗易懂,与内部 API `/v1/` 分离 |
51| 文件格式 | 同时提供 `.txt` 和 `.json` | ChatGPT / Gemini 都建议两种格式并存 |
52| `.txt` Content-Type | `text/plain; charset=utf-8` | AI 浏览工具最稳定的格式 |
53| `.json` Content-Type | `application/json; charset=utf-8` | 需要字段提取时使用 |
54| `.txt` 内容结构 | frontmatter 元数据 + 正文 | 两家 AI 都推荐此格式 |
55| URL 中的 ID | hash/UUID 格式 | 不用递增数字,防止遍历猜解 |
56| 文件过期 | 永不过期 | 所有内容永久保留 |
57| 访问控制 | 当前公开访问,无 token | AI 无法带自定义 Header,只能 GET |
58| 鉴权预留 | 后续用签名 URL `?sig=xxx&exp=xxx` | 比纯 token 更安全,AI 兼容性最高 |
59| 回送 AI 时 | 直接给 exact URL | ChatGPT 强调不能让 AI 猜路径 |
60| 截断阈值 | 2000 字符(可配置) | 超过此值开始截断 |
61| 摘要长度 | 500 字符(可配置) | 截取前 500 字符作为内联摘要 |
62| robots.txt | `Allow: /artifact/` | 允许 AI 爬虫访问 |
63| 静态文件由谁 serve | conductor HTTP server | nginx 在 VPS 上不在本地,走 conductor |
64
65### 3.2 AI 平台访问约束(实测确认)
66
67| 约束 | 说明 |
68|---|---|
69| 只能 GET | AI 不能发 POST,不能带自定义 Header |
70| 不能带 Cookie/Auth Header | 鉴权只能用 URL 参数 |
71| 超时约 10-15 秒 | 必须返回预生成的静态文件,不能现算 |
72| exact URL 最稳 | AI 访问明确给出的 URL 成功率最高,自己推导相邻 URL 可能被拒 |
73| `/api` 路径不被拦截 | 实测确认,但给 AI 读的接口和内部 API 分开是好习惯 |
74| JSON 和纯文本都能读 | 但纯文本/Markdown 更稳、更省 token |
75
76## 4. 数据库设计
77
78### 4.1 存储架构
79
80- **本地 SQLite**:先写,保证主链路不阻塞
81- **Cloudflare D1**:异步同步,分布式备份和跨设备访问
82- **新建数据库**:不复用旧版 D1 数据库
83- **D1 适配器**:TypeScript async 重写,接口兼容旧版 `D1Client`
84
85### 4.2 表结构
86
87#### messages 表
88
89存储所有 AI 对话消息(`browser.final_message` 到达时写入)。
90
91```sql
92CREATE TABLE IF NOT EXISTS messages (
93 id TEXT PRIMARY KEY, -- assistant_message_id
94 platform TEXT NOT NULL, -- 'claude' | 'chatgpt' | 'gemini'
95 conversation_id TEXT, -- 平台对话 ID
96 role TEXT NOT NULL, -- 'assistant' | 'human'
97 raw_text TEXT NOT NULL, -- 完整消息文本(永不截断)
98 summary TEXT, -- 摘要(前 N 字符或 AI 生成)
99 observed_at INTEGER NOT NULL, -- 毫秒时间戳
100 static_path TEXT, -- 静态文件相对路径
101 page_url TEXT, -- 来源页面 URL
102 page_title TEXT, -- 来源页面标题
103 organization_id TEXT, -- Claude org ID
104 created_at INTEGER NOT NULL -- 入库时间戳
105);
106
107CREATE INDEX IF NOT EXISTS idx_messages_conversation
108 ON messages(conversation_id);
109CREATE INDEX IF NOT EXISTS idx_messages_platform
110 ON messages(platform, observed_at);
111```
112
113#### executions 表
114
115存储所有指令执行记录。
116
117```sql
118CREATE TABLE IF NOT EXISTS executions (
119 instruction_id TEXT PRIMARY KEY, -- inst_{hash前16位}
120 message_id TEXT NOT NULL, -- 关联 messages.id
121 target TEXT NOT NULL, -- 'conductor' | 'system' | 'browser.claude'
122 tool TEXT NOT NULL, -- 'exec' | 'files/read' | 'send' 等
123 params TEXT, -- JSON,指令参数
124 params_kind TEXT, -- 'none' | 'body' | 'inline_json' | 'inline_string'
125 result_ok INTEGER NOT NULL, -- 1=成功 0=失败
126 result_data TEXT, -- JSON,完整执行结果(永不截断)
127 result_summary TEXT, -- 摘要文本
128 result_error TEXT, -- 错误信息
129 http_status INTEGER, -- HTTP 响应状态码
130 executed_at INTEGER NOT NULL, -- 执行完成时间戳
131 static_path TEXT, -- 静态文件相对路径
132 created_at INTEGER NOT NULL -- 入库时间戳
133);
134
135CREATE INDEX IF NOT EXISTS idx_executions_message
136 ON executions(message_id);
137CREATE INDEX IF NOT EXISTS idx_executions_target_tool
138 ON executions(target, tool);
139```
140
141#### sessions 表
142
143会话级别索引,用于跨会话接续。
144
145```sql
146CREATE TABLE IF NOT EXISTS sessions (
147 id TEXT PRIMARY KEY, -- 自动生成 UUID
148 platform TEXT NOT NULL,
149 conversation_id TEXT, -- 平台对话 ID
150 started_at INTEGER NOT NULL,
151 last_activity_at INTEGER NOT NULL,
152 message_count INTEGER NOT NULL DEFAULT 0,
153 execution_count INTEGER NOT NULL DEFAULT 0,
154 summary TEXT, -- 会话摘要
155 created_at INTEGER NOT NULL
156);
157
158CREATE INDEX IF NOT EXISTS idx_sessions_platform
159 ON sessions(platform, last_activity_at);
160CREATE INDEX IF NOT EXISTS idx_sessions_conversation
161 ON sessions(conversation_id);
162```
163
164## 5. URL 结构
165
166### 5.1 静态文件(AI 直接 fetch)
167
168```
169/artifact/msg/{id}.txt -- 消息全文(frontmatter + 正文)
170/artifact/msg/{id}.json -- 消息 JSON
171/artifact/exec/{id}.txt -- 执行结果(frontmatter + 正文)
172/artifact/exec/{id}.json -- 执行结果 JSON
173/artifact/session/latest.txt -- 最近活跃会话摘要
174/artifact/session/{id}.txt -- 单个会话时间线
175```
176
177### 5.2 查询接口(内部/管理用)
178
179```
180GET /v1/messages -- 消息列表(分页、按 conversation 过滤)
181GET /v1/messages/{id} -- 单条消息详情
182GET /v1/executions -- 执行记录列表(分页、按 message 过滤)
183GET /v1/executions/{id} -- 单条执行详情
184GET /v1/sessions -- 会话索引
185GET /v1/sessions/latest -- 最近活跃会话
186```
187
188### 5.3 辅助路由
189
190```
191GET /robots.txt -- Allow: /artifact/
192```
193
194## 6. 静态文件格式
195
196### 6.1 消息 .txt 格式
197
198```
199kind: message
200id: m_a8b9c2d7
201platform: claude
202conversation_id: conv_d4e5f6a1
203role: assistant
204observed_at: 2026-03-28T16:09:00+08:00
205
206---
207
208(此处为消息完整原文)
209```
210
211### 6.2 执行结果 .txt 格式
212
213```
214kind: execution
215id: inst_a8b9c2d7
216target: conductor
217tool: exec
218status: ok
219executed_at: 2026-03-28T16:09:00+08:00
220message_id: m_d4e5f6a1
221message_url: https://conductor.makefile.so/artifact/msg/m_d4e5f6a1.txt
222
223---
224
225command: pnpm test
226exit_code: 0
227
228(此处为完整执行输出)
229```
230
231### 6.3 消息 .json 格式
232
233```json
234{
235 "kind": "message",
236 "id": "m_a8b9c2d7",
237 "platform": "claude",
238 "conversation_id": "conv_d4e5f6a1",
239 "role": "assistant",
240 "observed_at": "2026-03-28T16:09:00+08:00",
241 "raw_text": "(完整消息文本)",
242 "summary": "(前 500 字符)",
243 "artifact_url": "https://conductor.makefile.so/artifact/msg/m_a8b9c2d7.txt"
244}
245```
246
247### 6.4 执行结果 .json 格式
248
249```json
250{
251 "kind": "execution",
252 "id": "inst_a8b9c2d7",
253 "target": "conductor",
254 "tool": "exec",
255 "params": {"command": "pnpm test"},
256 "status": "ok",
257 "executed_at": "2026-03-28T16:09:00+08:00",
258 "message_id": "m_d4e5f6a1",
259 "message_url": "https://conductor.makefile.so/artifact/msg/m_d4e5f6a1.txt",
260 "result": {
261 "ok": true,
262 "data": {"exit_code": 0, "stdout": "..."},
263 "error": null
264 },
265 "summary": "pnpm test 完成,12 项通过",
266 "artifact_url": "https://conductor.makefile.so/artifact/exec/inst_a8b9c2d7.txt"
267}
268```
269
270### 6.5 会话索引 latest.txt 格式
271
272```
273kind: session_index
274generated_at: 2026-03-28T16:30:00+08:00
275count: 3
276
277---
278
279## [2026-03-28 16:09] Claude conv_abc123
280messages: 5, executions: 3, last_activity: 16:09
281latest_message: https://conductor.makefile.so/artifact/msg/m_001.txt
282session: https://conductor.makefile.so/artifact/session/s_abc.txt
283
284## [2026-03-28 15:30] ChatGPT conv_def456
285messages: 2, executions: 1, last_activity: 15:31
286latest_message: https://conductor.makefile.so/artifact/msg/m_002.txt
287session: https://conductor.makefile.so/artifact/session/s_def.txt
288```
289
290## 7. 回送策略
291
292### 7.1 内联回送(结果 ≤ 2000 字符)
293
294```
295执行完成。
296
297command: pnpm test
298exit_code: 0
299
300Tests: 12 passed, 0 failed
301Time: 3.2s
302
303记录:https://conductor.makefile.so/artifact/exec/inst_a8b9c2d7.txt
304```
305
306### 7.2 截断回送(结果 > 2000 字符)
307
308```
309执行完成(输出 8420 字符,已截断)。
310
311command: pnpm test
312exit_code: 1
313
314FAIL src/api.test.ts
315 ● should handle timeout
316 Expected: 408
317 Received: 500
318(...前 500 字符...)
319
320完整结果:https://conductor.makefile.so/artifact/exec/inst_a8b9c2d7.txt
321```
322
323### 7.3 配置项
324
325```typescript
326interface ArtifactServiceConfig {
327 /** 超过此字符数开始截断回送,默认 2000 */
328 inlineThreshold: number;
329 /** 截断时保留的前 N 字符,默认 500 */
330 summaryLength: number;
331 /** 静态文件存储目录 */
332 artifactDir: string;
333 /** 公开访问的 base URL */
334 publicBaseUrl: string;
335}
336```
337
338## 8. D1 同步策略
339
340```
341写入流程:
342 本地 SQLite 写入(同步,阻塞)
343 → 生成静态文件(同步,写入 artifactDir)
344 → D1 异步推送(后台,不阻塞主链路)
345 → 推送失败时记录重试队列
346
347读取流程:
348 静态文件:直接读磁盘,conductor HTTP serve
349 查询接口:读本地 SQLite
350 D1:跨设备/远程访问时使用
351```
352
353D1 同步细节:
354
355- 写入后触发异步推送,不等待结果
356- 失败时记录到本地重试队列(SQLite 表 `d1_sync_queue`)
357- 后台定时扫描重试队列,指数退避
358- D1 不可用时,本地功能完全不受影响
359
360## 9. 跨会话接续
361
362新对话的 AI 通过以下方式了解历史:
363
3641. AI 发送 `@conductor::describe` → conductor 回复中包含最近会话 URL
3652. AI fetch `https://conductor.makefile.so/artifact/session/latest.txt`
3663. 看到所有历史对话、消息、执行记录的 exact URL
3674. 按需 fetch 任意 URL 获取详情
368
369不需要人工复制粘贴历史,不需要浏览器扩展参与。
370
371## 10. 实施顺序
372
3731. **新建 SQLite 数据库 + 表结构**(messages / executions / sessions)
3742. **静态文件生成模块**(写入时生成 .txt + .json)
3753. **conductor HTTP 路由**(`/artifact/` serve 静态文件 + `/robots.txt`)
3764. **接入主链路**(browser.final_message → 写库 + 生成文件;executor → 写库 + 生成文件)
3775. **回送策略**(阈值截断 + URL 拼接)
3786. **查询路由**(`/v1/messages`、`/v1/executions`、`/v1/sessions`)
3797. **D1 异步适配器**(TypeScript async 重写)
3808. **D1 同步队列**(后台推送 + 重试)
3819. **会话索引自动更新**(事件触发)
38210. **stagit 集成**(后续)
383
384## 11. 不做的事
385
386- 不做文件上传/下载注入到浏览器
387- 不做 artifact 物化/manifest/upload receipt(v5 规范中的复杂方案)
388- 不做 TTL/自动过期
389- 不做多租户隔离(当前单用户)
390- 不做 AI 调用生成摘要(纯截断,简单可靠)